diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index c2077b3a..89616a67 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,254 +1,252 @@ include(ECMAddAppIcon) include_directories(${CMAKE_CURRENT_SOURCE_DIR}/config) include_directories(config dcc irc viewer upnp) set(core_SRCS #==================================== #=== Application config/preferences.cpp application.cpp dbus.cpp mainwindow.cpp main.cpp common.cpp sound.cpp ssllabel.cpp statusbar.cpp bookmarkhandler.cpp scriptlauncher.cpp konsolepanel.cpp notificationhandler.cpp awaymanager.cpp connectionmanager.cpp connectionsettings.cpp identity.cpp identitydialog.cpp #=== GUI urlcatcher.cpp queuetuner.cpp quickconnectdialog.cpp ) set (irc_SRCS #== IRC irc/server.cpp irc/query.cpp irc/channel.cpp irc/channellistpanel.cpp irc/channelnick.cpp irc/modebutton.cpp irc/joinchanneldialog.cpp irc/invitedialog.cpp irc/topichistorymodel.cpp irc/irccharsets.cpp irc/nick.cpp irc/nickinfo.cpp irc/nicklistview.cpp irc/nicksonline.cpp irc/nicksonlineitem.cpp #=== Server irc/inputfilter.cpp irc/outputfilter.cpp irc/outputfilterresolvejob.cpp irc/ircqueue.cpp irc/servergroupdialog.cpp irc/servergroupsettings.cpp irc/serverison.cpp irc/serverlistdialog.cpp irc/serverlistview.cpp irc/serversettings.cpp ) ki18n_wrap_ui(irc_SRCS irc/channellistpanelui.ui irc/channeldialogui.ui irc/joinchannelui.ui irc/serverdialogui.ui irc/servergroupdialogui.ui irc/serverlistdialogui.ui irc/invitedialog.ui ) #=== Configuration dialog pages set(config_SRCS config/configdialog.cpp config/settingsdialog.cpp config/alias_config.cpp config/autoreplace_config.cpp config/dcc_config.cpp config/highlight_config.cpp config/ignore_config.cpp config/nicklistbehavior_config.cpp config/osd_config.cpp config/tabs_config.cpp config/theme_config.cpp config/quickbuttons_config.cpp config/warnings_config.cpp config/connectionbehavior_config.cpp config/highlighttreewidget.cpp ) ki18n_wrap_ui(config_SRCS config/alias_configui.ui config/autoreplace_configui.ui config/chatwindowappearance_config.ui config/chatwindowbehaviour_config.ui config/colorsappearance_config.ui config/connectionbehavior_config.ui config/dcc_configui.ui config/fontappearance_config.ui config/generalbehavior_configui.ui config/highlight_configui.ui config/ignore_configui.ui config/log_config.ui config/nicklistbehavior_configui.ui config/osd_configui.ui config/quickbuttons_configui.ui config/tabnotifications_config.ui config/tabs_configui.ui config/theme_configui.ui config/warnings_configui.ui config/watchednicknames_configui.ui ) #=== Viewer set(viewer_SRCS viewer/ircinput.cpp viewer/ircview.cpp viewer/chatwindow.cpp viewer/rawlog.cpp viewer/statuspanel.cpp viewer/ircviewbox.cpp viewer/viewcontainer.cpp viewer/pasteeditor.cpp viewer/highlight.cpp viewer/highlightviewitem.cpp viewer/ignore.cpp viewer/ignorelistviewitem.cpp viewer/irccolorchooser.cpp viewer/logfilereader.cpp viewer/insertchardialog.cpp viewer/osd.cpp viewer/topiclabel.cpp viewer/awaylabel.cpp viewer/editnotifydialog.cpp - viewer/emoticons.cpp viewer/images.cpp viewer/quickbutton.cpp viewer/searchbar.cpp viewer/irccontextmenus.cpp viewer/trayicon.cpp viewer/viewspringloader.cpp viewer/channeloptionsdialog.cpp viewer/topicedit.cpp viewer/topichistoryview.cpp viewer/viewtree.cpp ) ki18n_wrap_ui(viewer_SRCS viewer/channeloptionsui.ui viewer/irccolorchooserui.ui viewer/pasteeditor.ui ) #=== DCC set(dcc_SRCS dcc/chat.cpp dcc/chatcontainer.cpp dcc/dcccommon.cpp dcc/dccfiledialog.cpp dcc/recipientdialog.cpp dcc/resumedialog.cpp dcc/transfer.cpp dcc/transferdetailedinfopanel.cpp dcc/transfermanager.cpp dcc/transferpanel.cpp dcc/transferrecv.cpp dcc/transfersend.cpp dcc/transferlistmodel.cpp dcc/transferview.cpp dcc/whiteboard.cpp dcc/whiteboardcolorchooser.cpp dcc/whiteboardfontchooser.cpp dcc/whiteboardglobals.cpp dcc/whiteboardpaintarea.cpp dcc/whiteboardtoolbar.cpp ) ki18n_wrap_ui(dcc_SRCS dcc/transferdetailedinfopanelui.ui dcc/transferdetailedtimeinfopanelui.ui dcc/whiteboardtoolbarui.ui dcc/whiteboardfontchooserui.ui ) if (Qca-qt5_FOUND) set(cipher_SRCS cipher.cpp) endif (Qca-qt5_FOUND) set(upnp_SRCS upnp/soap.cpp upnp/upnpdescriptionparser.cpp upnp/upnpmcastsocket.cpp upnp/upnprouter.cpp ) set (completed_SRCS ${core_SRCS} ${irc_SRCS} ${viewer_SRCS} ${config_SRCS} ${cipher_SRCS} ${upnp_SRCS} ${dcc_SRCS}) set (konversation_SRCS ${completed_SRCS}) ki18n_wrap_ui(konversation_SRCS identitydialog.ui queuetunerbase.ui viewer/searchbarbase.ui ) kconfig_add_kcfg_files(konversation_SRCS config/preferences_base.kcfgc) # Sets the icon on Windows and OSX file(GLOB ICONS_SRCS "${CMAKE_CURRENT_SOURCE_DIR}/../data/images/icons/*apps-konversation.png") ecm_add_app_icon(konversation_SRCS ICONS ${ICONS_SRCS}) add_executable(konversation ${konversation_SRCS}) target_link_libraries(konversation Qt5::Network Qt5::Widgets KF5::Archive KF5::Bookmarks KF5::ConfigWidgets KF5::Crash - KF5::Emoticons KF5::I18n KF5::IdleTime KF5::NotifyConfig KF5::KIOFileWidgets KF5::KIOWidgets KF5::Parts KF5::Solid KF5::Wallet KF5::WidgetsAddons KF5::GlobalAccel KF5::DBusAddons KF5::CoreAddons KF5::Notifications KF5::WindowSystem KF5::IconThemes KF5::ItemViews Phonon::phonon4qt5) if (Qca-qt5_FOUND) target_link_libraries(konversation qca-qt5) endif () if (WIN32) target_link_libraries(konversation ws2_32) # for symbols from winsock2.h: ntohl, etc. endif() set_target_properties(konversation PROPERTIES MACOSX_BUNDLE_INFO_PLIST ${CMAKE_CURRENT_SOURCE_DIR}/MacOSXBundleInfo.plist.in) install(TARGETS konversation ${INSTALL_TARGETS_DEFAULT_ARGS}) diff --git a/src/config/konversation.kcfg b/src/config/konversation.kcfg index 1d59eafd..3f0b7a6a 100644 --- a/src/config/konversation.kcfg +++ b/src/config/konversation.kcfg @@ -1,1048 +1,1040 @@ qfont.h qsize.h QDir kuser.h QStandardPaths QFontDatabase QUrl QStyle QApplication true QFontDatabase::systemFont(QFontDatabase::GeneralFont) QFontDatabase::systemFont(QFontDatabase::GeneralFont) QFontDatabase::systemFont(QFontDatabase::GeneralFont) false false false true false false hh:mm true 10 true false true false false false false QApplication::style()->layoutSpacing(QSizePolicy::DefaultType, QSizePolicy::DefaultType, Qt::Horizontal) QApplication::style()->pixelMetric(QStyle::PM_LayoutLeftMargin) #ffffff #000000 #000080 #008000 #ff0000 #a52a2a #800080 #ff8000 #808000 #00ff00 #008080 #00ffff #0000ff #ffc0cb #a0a0a0 #c0c0c0 true #E90E7F #8E55E9 #B30E0E #18B33C #58ADB3 #9E54B3 #B39875 #3176B3 #000001 true true true true false false true true false Enable if you want all the IRC input lines to check your spelling as you type false false Enabling this will cause the input box to grow vertically when it fills up. / false false false true false false 100 1000 200 true 90 false /QUERY %u%n /QUERY %u%n false false 180 false false 10 10 false true false false false true false false false false false QFontDatabase::systemFont(QFontDatabase::GeneralFont) false 3000 0 false 30 50 0 #ffffff true 20 true false false true #FF0000 false #ff0000 0 : false 16384 1 0.0.0.0 true 1026 7000 true 1026 7000 false false true false true false true 180 false eth0 false false 47,90,103,173,70,87,157,87,96,165 0,1,2,3,4,5,6,7,8,9 1,1,1,1,1,1,1,1,1,1 1 true QUrl::fromLocalFile((KUser(KUser::UseRealUserID).homeDir()+"/logs")) QUrl::fromLocalFile(QStandardPaths::writableLocation(QStandardPaths::DownloadLocation)) true true false false Left false true false false false 1 false true 10 0 0 true false true false false firefox '%u' false default - - false - - - - Default - - ActionMessage BacklogMessage ChannelMessage CommandMessage QueryMessage ServerMessage Time Action TextViewBackground AlternateBackground Hyperlink #0000ff #aaaaaa #000000 #960096 #000000 #91640a #709070 #0000ff #ffffff #EDF4F9 #0000ff true false true #C3C300 true #008000 true #800000 false #008000 true #FF0000 true #FF0000 false qpohv- QList< QList<int> > defaultRate; QList< int > defaultRateInit; defaultRateInit.append( 15 ); defaultRateInit.append( 60 ); defaultRateInit.append( 0 ); defaultRate.append( defaultRateInit ); defaultRateInit.clear(); defaultRateInit.append( 40 ); defaultRateInit.append( 60 ); defaultRateInit.append( 0 ); defaultRate.append( defaultRateInit ); defaultRateInit.clear(); defaultRateInit.append( 1 ); defaultRateInit.append( 1 ); defaultRateInit.append( 0 ); defaultRate.append( defaultRateInit ); defaultRate[$(QueueIndex)] false false Socksv5Proxy 8080 diff --git a/src/viewer/emoticons.cpp b/src/viewer/emoticons.cpp deleted file mode 100644 index cc608edd..00000000 --- a/src/viewer/emoticons.cpp +++ /dev/null @@ -1,45 +0,0 @@ -/* - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation; either version 2 of the License, or - (at your option) any later version. - -*/ - -/* - Based on kopeteemoticons.cpp (as of KDE 4.2) - Kopete Preferences Container-Class - - Copyright (C) 2002-2005 by the Kopete developers - Copyright (C) 2002 Stefan Gehn - Copyright (C) 2002-2006 Olivier Goffart - Copyright (C) 2005 Engin Aydogan - Copyright (C) 2005 Peter Simonsson - Copyright (C) 2008 Modestas Vainius -*/ - -#include "emoticons.h" -#include "config/preferences.h" - -namespace Konversation -{ - - Q_GLOBAL_STATIC(KEmoticons, s_self) - - KEmoticons *Emoticons::self() - { - return s_self; - } - - QString Emoticons::parseEmoticons(const QString &text, KEmoticonsTheme::ParseMode mode, const QStringList &exclude) - { - // Disable emoticons support until IRCView supports them - if (Preferences::self()->enableEmotIcons()) - { - return Konversation::Emoticons::self()->theme().parseEmoticons(text, mode, exclude); - } - else - { - return text; - } - } -} diff --git a/src/viewer/emoticons.h b/src/viewer/emoticons.h deleted file mode 100644 index ea9c5b9d..00000000 --- a/src/viewer/emoticons.h +++ /dev/null @@ -1,41 +0,0 @@ -/* - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation; either version 2 of the License, or - (at your option) any later version. - - Copyright (C) 2005 Peter Simonsson - Copyright (C) 2008 Modestas Vainius -*/ - -/* - Based on kopeteemoticons.cpp (as of KDE 4.2) - Kopete Preferences Container-Class - - Copyright (C) 2002-2005 by the Kopete developers - Copyright (C) 2002 Stefan Gehn - Copyright (C) 2002-2006 Olivier Goffart - Copyright (C) 2005 Engin Aydogan -*/ - -#ifndef KONVERSATIONEMOTICONS_H -#define KONVERSATIONEMOTICONS_H - -#include - -namespace Konversation { - - class Emoticons - { - public: - /** - * The emoticons container-class by default is a singleton object. - * Use this method to retrieve the instance. - */ - static KEmoticons *self(); - - static QString parseEmoticons(const QString &text, KEmoticonsTheme::ParseMode mode = KEmoticonsTheme::DefaultParse, const QStringList &exclude = QStringList()); - }; - -} //END namespace Konversation - -#endif diff --git a/src/viewer/ircview.cpp b/src/viewer/ircview.cpp index c129f615..15f78ca6 100644 --- a/src/viewer/ircview.cpp +++ b/src/viewer/ircview.cpp @@ -1,2441 +1,2438 @@ // -*- mode: c++; c-file-style: "bsd"; c-basic-offset: 4; tabs-width: 4; indent-tabs-mode: nil -*- /* This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. */ /* Copyright (C) 2002 Dario Abatianni Copyright (C) 2005-2016 Peter Simonsson Copyright (C) 2006-2010 Eike Hein Copyright (C) 2004-2011 Eli Mackenzie */ #include "ircview.h" #include "channel.h" #include "dcc/chatcontainer.h" #include "application.h" #include "highlight.h" #include "sound.h" -#include "emoticons.h" #include "notificationhandler.h" #include #include #include #include #include #include #include #include #include #include #include using namespace Konversation; class ScrollBarPin { QPointer m_bar; public: ScrollBarPin(QScrollBar *scrollBar) : m_bar(scrollBar) { if (m_bar) m_bar = m_bar->value() == m_bar->maximum()? m_bar : nullptr; } ~ScrollBarPin() { if (m_bar) m_bar->setValue(m_bar->maximum()); } }; // Scribe bug - if the cursor position or anchor points to the last character in the document, // the cursor becomes glued to the end of the document instead of retaining the actual position. // This causes the selection to expand when something is appended to the document. class SelectionPin { int pos, anc; QPointer d; public: SelectionPin(IRCView *doc) : pos(0), anc(0), d(doc) { if (d->textCursor().hasSelection()) { int end = d->document()->rootFrame()->lastPosition(); //WARNING if selection pins don't work in some build environments, we need to keep the result d->document()->lastBlock(); pos = d->textCursor().position(); anc = d->textCursor().anchor(); if (pos != end && anc != end) anc = pos = 0; } } ~SelectionPin() { if (d && (pos || anc)) { QTextCursor mv(d->textCursor()); mv.setPosition(anc); mv.setPosition(pos, QTextCursor::KeepAnchor); d->setTextCursor(mv); } } }; IRCView::IRCView(QWidget* parent) : QTextBrowser(parent), m_rememberLine(nullptr), m_lastMarkerLine(nullptr), m_rememberLineDirtyBit(false), markerFormatObject(this), m_prevTimestamp(QDateTime::currentDateTime()) { m_mousePressedOnUrl = false; m_isOnNick = false; m_isOnChannel = false; m_chatWin = nullptr; m_server = nullptr; m_fontSizeDelta = 0; m_showDate = false; setAcceptDrops(false); // Marker lines connect(document(), &QTextDocument::contentsChange, this, &IRCView::cullMarkedLine); //This assert is here because a bad build environment can cause this to fail. There is a note // in the Qt source that indicates an error should be output, but there is no such output. QTextObjectInterface *iface = qobject_cast(&markerFormatObject); if (!iface) { Q_ASSERT(iface); } document()->documentLayout()->registerHandler(IRCView::MarkerLine, &markerFormatObject); document()->documentLayout()->registerHandler(IRCView::RememberLine, &markerFormatObject); document()->documentLayout()->registerHandler(IRCView::DateLine, &markerFormatObject); connect(this, SIGNAL(anchorClicked(QUrl)), this, SLOT(anchorClicked(QUrl))); connect( this, SIGNAL(highlighted(QString)), this, SLOT(highlightedSlot(QString)) ); setOpenLinks(false); setUndoRedoEnabled(0); document()->setDefaultStyleSheet(QStringLiteral("a.nick:link {text-decoration: none}")); setWordWrapMode(QTextOption::WrapAtWordBoundaryOrAnywhere); setFocusPolicy(Qt::ClickFocus); setReadOnly(true); viewport()->setCursor(Qt::ArrowCursor); setTextInteractionFlags(Qt::TextBrowserInteraction); viewport()->setMouseTracking(true); //HACK to workaround an issue with the QTextDocument //doing a relayout/scrollbar over and over resulting in 100% //proc usage. See bug 215256 setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); setContextMenuOptions(IrcContextMenus::ShowTitle | IrcContextMenus::ShowFindAction, true); } IRCView::~IRCView() { } void IRCView::increaseFontSize() { QFont newFont(Preferences::self()->customTextFont() ? Preferences::self()->textFont() : QFontDatabase::systemFont(QFontDatabase::GeneralFont)); ++m_fontSizeDelta; newFont.setPointSize(newFont.pointSize() + m_fontSizeDelta); setFont(newFont); } void IRCView::decreaseFontSize() { QFont newFont(Preferences::self()->customTextFont() ? Preferences::self()->textFont() : QFontDatabase::systemFont(QFontDatabase::GeneralFont)); --m_fontSizeDelta; newFont.setPointSize(newFont.pointSize() + m_fontSizeDelta); setFont(newFont); } void IRCView::resetFontSize() { QFont newFont(Preferences::self()->customTextFont() ? Preferences::self()->textFont() : QFontDatabase::systemFont(QFontDatabase::GeneralFont)); m_fontSizeDelta = 0; setFont(newFont); } void IRCView::setServer(Server* newServer) { if (m_server == newServer) return; m_server = newServer; } void IRCView::setChatWin(ChatWindow* chatWin) { m_chatWin = chatWin; } void IRCView::findText() { emit doSearch(); } void IRCView::findNextText() { emit doSearchNext(); } void IRCView::findPreviousText() { emit doSearchPrevious(); } bool IRCView::search(const QString& pattern, QTextDocument::FindFlags flags, bool fromCursor) { if (pattern.isEmpty()) return true; m_pattern = pattern; m_searchFlags = flags; if (!fromCursor) moveCursor(QTextCursor::End); else moveCursor(QTextCursor::StartOfWord); // Do this to that if possible the same position is kept when changing search options return searchNext(); } bool IRCView::searchNext(bool reversed) { QTextDocument::FindFlags flags = m_searchFlags; if(!reversed) flags |= QTextDocument::FindBackward; return find(m_pattern, flags); } class IrcViewMimeData : public QMimeData { public: IrcViewMimeData(const QTextDocumentFragment& _fragment): fragment(_fragment) {} QStringList formats() const Q_DECL_OVERRIDE; protected: QVariant retrieveData(const QString &mimeType, QVariant::Type type) const Q_DECL_OVERRIDE; private: mutable QTextDocumentFragment fragment; }; QStringList IrcViewMimeData::formats() const { if (!fragment.isEmpty()) return QStringList() << QStringLiteral("text/plain"); else return QMimeData::formats(); } QVariant IrcViewMimeData::retrieveData(const QString &mimeType, QVariant::Type type) const { if (!fragment.isEmpty()) { IrcViewMimeData *that = const_cast(this); //Copy the text, skipping any QChar::ObjectReplacementCharacter QRegExp needle(QString("\\xFFFC\\n?")); that->setText(fragment.toPlainText().remove(needle)); fragment = QTextDocumentFragment(); } return QMimeData::retrieveData(mimeType, type); } QMimeData *IRCView::createMimeDataFromSelection() const { const QTextDocumentFragment fragment(textCursor()); return new IrcViewMimeData(fragment); } void IRCView::dragEnterEvent(QDragEnterEvent* e) { if (e->mimeData()->hasUrls()) e->acceptProposedAction(); else e->ignore(); } void IRCView::dragMoveEvent(QDragMoveEvent* e) { if (e->mimeData()->hasUrls()) e->accept(); else e->ignore(); } void IRCView::dropEvent(QDropEvent* e) { if (e->mimeData() && e->mimeData()->hasUrls()) emit urlsDropped(KUrlMimeData::urlsFromMimeData(e->mimeData(), KUrlMimeData::PreferLocalUrls)); } // Marker lines #define _S(x) #x << (x) QDebug operator<<(QDebug dbg, QTextBlockUserData *bd); QDebug operator<<(QDebug d, QTextFrame* feed); QDebug operator<<(QDebug d, QTextDocument* document); QDebug operator<<(QDebug d, const QTextBlock &b); // This object gets stuffed into the userData field of a text block. // Qt does not give us a way to track blocks, so we have to // rely on the destructor of this object to notify us that a // block we care about was removed from the document. This does not // prevent the first block bug from deleting the wrong block's data, // however that should not result in a crash. struct Burr: public QTextBlockUserData { Burr(IRCView* o, Burr* prev, const QTextBlock &b, int objFormat) : m_block(b), m_format(objFormat), m_prev(prev), m_next(nullptr), m_owner(o) { if (m_prev) m_prev->m_next = this; } ~Burr() { m_owner->blockDeleted(this); unlink(); } void unlink() { if (m_prev) m_prev->m_next = m_next; if (m_next) m_next->m_prev = m_prev; } QTextBlock m_block; int m_format; Burr* m_prev, *m_next; IRCView* m_owner; }; void IrcViewMarkerLine::drawObject(QPainter *painter, const QRectF &r, QTextDocument *doc, int posInDocument, const QTextFormat &format) { Q_UNUSED(format); QTextBlock block=doc->findBlock(posInDocument); QPen pen; Burr* b = dynamic_cast(block.userData()); Q_ASSERT(b); // remember kids, only YOU can makes this document support two user data types switch (b->m_format) { case IRCView::BlockIsMarker: pen.setColor(Preferences::self()->color(Preferences::ActionMessage)); break; case IRCView::BlockIsRemember: pen.setColor(Preferences::self()->color(Preferences::CommandMessage)); // pen.setStyle(Qt::DashDotDotLine); break; case IRCView::BlockIsDateMarker: pen.setColor(Preferences::self()->color(Preferences::Time)); pen.setStyle(Qt::DashLine); break; default: //nice color, eh? pen.setColor(Qt::cyan); } pen.setWidth(2); // FIXME this is a hardcoded value... painter->setPen(pen); qreal y = (r.top() + r.height() / 2); QLineF line(r.left(), y, r.right(), y); painter->drawLine(line); } QSizeF IrcViewMarkerLine::intrinsicSize(QTextDocument *doc, int posInDocument, const QTextFormat &format) { Q_UNUSED(posInDocument); Q_UNUSED(format); QTextFrameFormat f=doc->rootFrame()->frameFormat(); qreal width = doc->pageSize().width()-(f.leftMargin()+f.rightMargin()); return QSizeF(width, 6); // FIXME this is a hardcoded value... } QTextCharFormat IRCView::getFormat(ObjectFormats x) { QTextCharFormat f; f.setObjectType(x); return f; } void IRCView::blockDeleted(Burr* b) //slot { Q_ASSERT(b); // this method only to be called from a ~Burr(); //tracking only the tail if (b == m_lastMarkerLine) m_lastMarkerLine = b->m_prev; if (b == m_rememberLine) m_rememberLine = nullptr; } void IRCView::cullMarkedLine(int, int, int) //slot { QTextBlock prime = document()->firstBlock(); if (prime.length() == 1 && document()->blockCount() == 1) //the entire document was wiped. was a signal such a burden? apparently.. wipeLineParagraphs(); } void IRCView::insertMarkerLine() //slot { //if the last line is already a marker of any kind, skip out if (lastBlockIsLine(BlockIsMarker)) return; //the code used to preserve the dirty bit status, but that was never affected by appendLine... //maybe i missed something appendLine(IRCView::MarkerLine); } void IRCView::insertRememberLine() //slot { m_rememberLineDirtyBit = true; // means we're going to append a remember line if some text gets inserted if (!Preferences::self()->automaticRememberLineOnlyOnTextChange()) { appendRememberLine(); } } void IRCView::cancelRememberLine() //slot { m_rememberLineDirtyBit = false; } bool IRCView::lastBlockIsLine(int select) { Burr *b = dynamic_cast(document()->lastBlock().userData()); int state = -1; if (b) state = b->m_format; if (select == -1) return (state == BlockIsRemember || state == BlockIsMarker); return state == select; } void IRCView::appendRememberLine() { //clear this now, so that doAppend doesn't double insert m_rememberLineDirtyBit = false; //if the last line is already the remember line, do nothing if (lastBlockIsLine(BlockIsRemember)) return; if (m_rememberLine) { QTextBlock rem = m_rememberLine->m_block; voidLineBlock(rem); if (m_rememberLine != nullptr) { // this probably means we had a block containing only 0x2029, so Scribe merged the userData/userState into the next m_rememberLine = nullptr; } } m_rememberLine = appendLine(IRCView::RememberLine); } void IRCView::voidLineBlock(const QTextBlock &rem) { QTextCursor c(rem); c.select(QTextCursor::BlockUnderCursor); c.removeSelectedText(); } void IRCView::clearLines() { while (hasLines()) { //IRCView::blockDeleted takes care of the pointers voidLineBlock(m_lastMarkerLine->m_block); }; } void IRCView::wipeLineParagraphs() { m_rememberLine = m_lastMarkerLine = nullptr; } bool IRCView::hasLines() { return m_lastMarkerLine != nullptr; } Burr* IRCView::appendLine(IRCView::ObjectFormats type) { ScrollBarPin barpin(verticalScrollBar()); SelectionPin selpin(this); QTextCursor cursor(document()); cursor.movePosition(QTextCursor::End); if (cursor.block().length() > 1) // this will be a 0x2029 cursor.insertBlock(); cursor.insertText(QString(QChar::ObjectReplacementCharacter), getFormat(type)); QTextBlock block = cursor.block(); Burr *prevBurr = m_lastMarkerLine; if(type == DateLine) prevBurr = nullptr; Burr *b = new Burr(this, prevBurr, block, objectFormatToBlockState(type)); block.setUserData(b); if(type != DateLine) m_lastMarkerLine = b; //TODO figure out what this is for cursor.setPosition(block.position()); return b; } IRCView::BlockStates IRCView::objectFormatToBlockState(ObjectFormats format) { BlockStates state; switch(format) { case MarkerLine: state = BlockIsMarker; break; case RememberLine: state = BlockIsRemember; break; case DateLine: state = BlockIsDateMarker; break; } return state; } // Other stuff void IRCView::updateAppearance() { QFont newFont(Preferences::self()->customTextFont() ? Preferences::self()->textFont() : QFontDatabase::systemFont(QFontDatabase::GeneralFont)); newFont.setPointSize(newFont.pointSize() + m_fontSizeDelta); setFont(newFont); setVerticalScrollBarPolicy(Preferences::self()->showIRCViewScrollBar() ? Qt::ScrollBarAlwaysOn : Qt::ScrollBarAlwaysOff); if (Preferences::self()->showBackgroundImage()) { QUrl url = Preferences::self()->backgroundImage(); if (url.isValid()) { viewport()->setStyleSheet("QWidget { background-image: url("+url.path()+"); background-attachment:fixed; }"); return; } } if (!viewport()->styleSheet().isEmpty()) viewport()->setStyleSheet(QString()); QPalette p; p.setColor(QPalette::Base, Preferences::self()->color(Preferences::TextViewBackground)); viewport()->setPalette(p); } bool IRCView::dateRtlDirection() { // Keep format synced with IRCView::timeStamp return QLocale().toString(QDate::currentDate(), QLocale::ShortFormat).isRightToLeft(); } // To minimize the use of bidi marks, for cases below, some bidi marks are // needed. // * left aligned lines in LTR locales, and // * right aligned lines in RTL locales // // First, check if the direction of the message is the same as the // direction of the timestamp, if not, then add a mark depending on // message's direction, so that timestamp don't be first strong character. // // If we have a LTR label, and the message is right-aligned, we prepend // it with LRM to look correct (check nickname case below), and then append // it with LRM also and then a RLM to preserve the direction of the // right-aligned line. // // Later, if the message is RTL, nicknames like "_nick]" will appear // as "[nick_". // First, add a LRM mark to make underscore on the left, next add the // nickname, and then another LRM mark. Since we use RTL/LTR count, the // message may start with a LTR word, and appear to the right of the // nickname. That's why we add a RLM mark before the nick to force it // appearing on left. QString IRCView::formatFinalLine(bool rtl, const QString &lineColor, const QString &label, const QString &nickLine, const QString &nickStar, const QString &text) { // Nick correctly displayed: <_nick]> QString line; // It's right-aligned under LTR locale, or left-aligned under RTL locale if (!rtl == dateRtlDirection()) line += (rtl ? RLM : LRM); line += "%1"; if (!label.isEmpty()) { // Label correctly displayed: [_label.] if (rtl) { line += LRM; // [.label_] -> [._label] line += " [%4]"; } else { line += "[%4] "; } if (!label.isRightToLeft() == rtl) line += LRM + RLM; // [._label] -> [_label.] } if (!nickStar.isEmpty()) // Used for [timeStamp] * nick action line += nickStar; if (rtl) line += LRM; // <[nick_> -> <[_nick]> line += nickLine; if (rtl) { line += LRM; // <[_nick]> -> <_nick]> // It might start with an English word, but it's RTL because of counting if (!text.isEmpty() && !text.isRightToLeft()) line += RLM; // ARABIC_TEXT <_nick]> Hi -> ARABIC_TEXT Hi <_nick]> } if (text.isEmpty()) line += QLatin1String(""); else line += QLatin1String(" %3"); return line; } // Data insertion void IRCView::append(const QString& nick, const QString& message, const QHash &messageTags, const QString& label) { QString channelColor = Preferences::self()->color(Preferences::ChannelMessage).name(); m_tabNotification = Konversation::tnfNormal; QString nickLine = createNickLine(nick, channelColor); QChar::Direction dir; QString text(filter(message, channelColor, nick, true, true, false, &dir)); QString line; bool rtl = (dir == QChar::DirR); // Normal chat lines // [timestamp] chat message line = formatFinalLine(rtl, channelColor, label, nickLine, QString(), text); line = line.arg(timeStamp(messageTags, rtl), nick, text); if (!label.isEmpty()) { line = line.arg(label); } emit textToLog(QStringLiteral("<%1>\t%2").arg(nick, message)); doAppend(line, rtl); } void IRCView::appendRaw(const QString& message, bool self) { QColor color = self ? Preferences::self()->color(Preferences::ChannelMessage) : Preferences::self()->color(Preferences::ServerMessage); m_tabNotification = Konversation::tnfNone; // Raw log is always left-aligned // [timestamp] << server line // If the timedate string is RTL, prepend a LTR mark to force the direction // to be LTR, as the datetime string is already returned as it's for a // left-aligned line. QString line; if (dateRtlDirection()) line += LRM; line += (timeStamp(QHash(), false) + " " + message + ""); doAppend(line, false, self); } void IRCView::appendLog(const QString & message) { QColor channelColor = Preferences::self()->color(Preferences::ChannelMessage); m_tabNotification = Konversation::tnfNone; // Log view is plain log files. // Direction will be depending on the logfile line direction. QString line("" + message + ""); doRawAppend(line, message.isRightToLeft()); } void IRCView::appendQuery(const QString& nick, const QString& message, const QHash &messageTags, bool inChannel) { QString queryColor=Preferences::self()->color(Preferences::QueryMessage).name(); m_tabNotification = Konversation::tnfPrivate; QString nickLine = createNickLine(nick, queryColor, true, inChannel); QString line; QChar::Direction dir; QString text(filter(message, queryColor, nick, true, true, false, &dir)); bool rtl = (dir == QChar::DirR); // Private chat lines // [timestamp] chat message line = formatFinalLine(rtl, queryColor, QString(), nickLine, QString(), text); line = line.arg(timeStamp(messageTags, rtl), nick, text); if (inChannel) { emit textToLog(QStringLiteral("<-> %1>\t%2").arg(nick, message)); } else { emit textToLog(QStringLiteral("<%1>\t%2").arg(nick, message)); } doAppend(line, rtl); } void IRCView::appendChannelAction(const QString& nick, const QString& message, const QHash &messageTags) { m_tabNotification = Konversation::tnfNormal; appendAction(nick, message, messageTags); } void IRCView::appendQueryAction(const QString& nick, const QString& message, const QHash &messageTags) { m_tabNotification = Konversation::tnfPrivate; appendAction(nick, message, messageTags); } void IRCView::appendAction(const QString& nick, const QString& message, const QHash &messageTags) { QString actionColor = Preferences::self()->color(Preferences::ActionMessage).name(); QString line; QString nickLine = createNickLine(nick, actionColor, false); if (message.isEmpty()) { // No text to check direction. Better to check last line, if it's RTL, // treat it as that. QTextCursor formatCursor(document()->lastBlock()); bool rtl = (formatCursor.blockFormat().alignment().testFlag(Qt::AlignRight)); line = formatFinalLine(rtl, actionColor, QString(), nickLine, QStringLiteral(" * "), QString()); line = line.arg(timeStamp(messageTags, rtl), nick); emit textToLog(QStringLiteral("\t * %1").arg(nick)); doAppend(line, rtl); } else { QChar::Direction dir; QString text(filter(message, actionColor, nick, true,true, false, &dir)); bool rtl = (dir == QChar::DirR); // Actions line // [timestamp] * nickname action line = formatFinalLine(rtl, actionColor, QString(), nickLine, QStringLiteral(" * "), text); line = line.arg(timeStamp(messageTags, rtl), nick, text); emit textToLog(QStringLiteral("\t * %1 %2").arg(nick, message)); doAppend(line, rtl); } } void IRCView::appendServerMessage(const QString& type, const QString& message, const QHash &messageTags, bool parseURL) { QString serverColor = Preferences::self()->color(Preferences::ServerMessage).name(); m_tabNotification = Konversation::tnfControl; // Fixed width font option for MOTD QString fixed; if(Preferences::self()->fixedMOTD() && !m_fontDataBase.isFixedPitch(font().family())) { if(type == i18n("MOTD")) fixed=" face=\"" + QFontDatabase::systemFont(QFontDatabase::FixedFont).family() + "\""; } QString line; QChar::Direction dir; QString text(filter(message, serverColor, nullptr , true, parseURL, false, &dir)); // Server text may be translated strings. It's not user input: treat with first strong. bool rtl = text.isRightToLeft(); // It's right-aligned under LTR locale, or left-aligned under RTL locale if (!rtl == dateRtlDirection()) line += (rtl ? RLM : LRM); line += "%1 [%2]"; if (!rtl == type.isRightToLeft()) line += (rtl ? RLM : LRM); // [50 [ARABIC_TEXT users -> [ARABIC_TEXT] 50 users line += QLatin1String(" %3"); line = line.arg(timeStamp(messageTags, rtl), type, text); emit textToLog(QStringLiteral("%1\t%2").arg(type, message)); doAppend(line, rtl); } void IRCView::appendCommandMessage(const QString& type, const QString& message, const QHash &messageTags, bool parseURL, bool self) { QString commandColor = Preferences::self()->color(Preferences::CommandMessage).name(); QString prefix=QStringLiteral("***"); m_tabNotification = Konversation::tnfControl; if(type == i18nc("Message type", "Join")) { prefix=QStringLiteral("-->"); parseURL=false; } else if(type == i18nc("Message type", "Part") || type == i18nc("Message type", "Quit")) { prefix=QStringLiteral("<--"); } prefix=prefix.toHtmlEscaped(); QString line; QChar::Direction dir; QString text(filter(message, commandColor, nullptr, true, parseURL, self, &dir)); // Commands are translated and contain LTR IP addresses. Treat with first strong. bool rtl = text.isRightToLeft(); // It's right-aligned under LTR locale, or left-aligned under RTL locale if (!rtl == dateRtlDirection()) line += (rtl ? RLM : LRM); line += "%1 %2 %3"; line = line.arg(timeStamp(messageTags, rtl), prefix, text); emit textToLog(QStringLiteral("%1\t%2").arg(type, message)); doAppend(line, rtl, self); } void IRCView::appendBacklogMessage(const QString& firstColumn,const QString& rawMessage) { QString time; QString message = rawMessage; QString nick = firstColumn; QString backlogColor = Preferences::self()->color(Preferences::BacklogMessage).name(); m_tabNotification = Konversation::tnfNone; //The format in Chatwindow::logText is not configurable, so as long as nobody allows square brackets in a date/time format.... int eot = nick.lastIndexOf(' '); time = nick.left(eot); nick = nick.mid(eot+1); if(!nick.isEmpty() && !nick.startsWith('<') && !nick.startsWith('*')) { nick = '|' + nick + '|'; } // Nicks are in "" format so replace the "<>" nick.replace('<',QLatin1String("<")); nick.replace('>',QLatin1String(">")); QString line; QChar::Direction dir; QString text(filter(message, backlogColor, nullptr, false, false, false, &dir)); bool rtl = nick.startsWith('|') ? text.isRightToLeft() : (dir == QChar::DirR); // It's right-aligned under LTR locale, or left-aligned under RTL locale if (!rtl == time.isRightToLeft()) line += (rtl ? RLM : LRM); line += ""; // Prepend and append timestamp's correct bidi mark if the time and text // directions are different. if (rtl == time.isRightToLeft()) line += QLatin1String("%1"); else line += (time.isRightToLeft() ? RLM+"%1"+RLM : LRM+"%1"+LRM); // Partially copied from IRCView::formatFinalLine if (rtl) { // Return back to the normal direction after setting mark if (!rtl == time.isRightToLeft()) line += (!time.isRightToLeft() ? RLM : LRM); line += LRM; // <[nick_> -> <[_nick]> } line += QLatin1String("%2"); if (rtl) { line += LRM; // <[_nick]> -> <_nick]> if (!text.isRightToLeft()) line += RLM; // ARABIC_TEXT <_nick]> Hi -> ARABIC_TEXT Hi <_nick]> } line += QLatin1String(" %3"); line = line.arg(time, nick, text); doAppend(line, rtl); } void IRCView::doAppend(const QString& newLine, bool rtl, bool self) { if (m_rememberLineDirtyBit) appendRememberLine(); if (!self && m_chatWin) m_chatWin->activateTabNotification(m_tabNotification); int scrollMax = Preferences::self()->scrollbackMax(); if (scrollMax != 0) { //don't remove lines if the user has scrolled up to read old lines bool atBottom = (verticalScrollBar()->value() == verticalScrollBar()->maximum()); document()->setMaximumBlockCount(atBottom ? scrollMax : document()->maximumBlockCount() + 1); } if (m_showDate) { QString timeColor = Preferences::self()->color(Preferences::Time).name(); doRawAppend(QString("%2").arg(timeColor, QLocale().toString(m_prevTimestamp.date(), QLocale::ShortFormat)), rtl); appendLine(DateLine); m_showDate = false; } doRawAppend(newLine, rtl); //FIXME: Disable auto-text for DCC Chats since we don't have a server to parse wildcards. if (!m_autoTextToSend.isEmpty() && m_server) { // replace placeholders in autoText QString sendText = m_server->parseWildcards(m_autoTextToSend,m_server->getNickname(), QString(), QString(), QString(), QString()); // avoid recursion due to signalling m_autoTextToSend.clear(); // send signal only now emit autoText(sendText); } else { m_autoTextToSend.clear(); } if (!m_lastStatusText.isEmpty()) emit clearStatusBarTempText(); } void IRCView::doRawAppend(const QString& newLine, bool rtl) { SelectionPin selpin(this); // HACK stop selection at end from growing QString line(newLine); line.remove('\n'); QTextBrowser::append(line); QTextCursor formatCursor(document()->lastBlock()); QTextBlockFormat format = formatCursor.blockFormat(); format.setAlignment(Qt::AlignAbsolute|(rtl ? Qt::AlignRight : Qt::AlignLeft)); formatCursor.setBlockFormat(format); } QString IRCView::timeStamp(QHash messageTags, bool rtl) { if(Preferences::self()->timestamping()) { QDateTime serverTime; if (messageTags.contains(QLatin1String("time"))) // If it exists use the supplied server time. serverTime = QDateTime::fromString(messageTags[QStringLiteral("time")], Qt::ISODate).toLocalTime(); QDateTime dateTime = serverTime.isValid() ? serverTime : QDateTime::currentDateTime(); QString timeColor = Preferences::self()->color(Preferences::Time).name(); QString timeFormat = Preferences::self()->timestampFormat(); QString timeString; bool dateRtl = dateRtlDirection(); if(!Preferences::self()->showDate()) { timeString = QString(QLatin1String("[%1] ")).arg(dateTime.time().toString(timeFormat)); m_showDate = Preferences::self()->showDateLine() && dateTime.date() != m_prevTimestamp.date(); } else { timeString = QString("[%1%2 %3%4] ") .arg((dateRtl==rtl) ? QString() : (dateRtl ? RLM : LRM), QLocale().toString(dateTime.date(), QLocale::ShortFormat), dateTime.time().toString(timeFormat), (dateRtl==rtl) ? QString() : (!dateRtl ? RLM : LRM)); } m_prevTimestamp = dateTime; return timeString; } return QString(); } QString IRCView::createNickLine(const QString& nick, const QString& defaultColor, bool encapsulateNick, bool privMsg) { QString nickLine =QStringLiteral("%2"); QString nickColor; if (Preferences::self()->useColoredNicks()) { if (m_server) { if (nick != m_server->getNickname()) nickColor = Preferences::self()->nickColor(m_server->obtainNickInfo(nick)->getNickColor()).name(); else nickColor = Preferences::self()->nickColor(8).name(); } else if (m_chatWin->getType() == ChatWindow::DccChat) { QString ownNick = qobject_cast(m_chatWin)->ownNick(); if (nick != ownNick) nickColor = Preferences::self()->nickColor(Konversation::colorForNick(ownNick)).name(); else nickColor = Preferences::self()->nickColor(8).name(); } } else nickColor = defaultColor; nickLine = QLatin1String("") + nickLine + QLatin1String(""); if (Preferences::self()->useClickableNicks()) nickLine = "" + nickLine + ""; if (privMsg) nickLine.prepend(QLatin1String("-> ")); if(encapsulateNick) nickLine = QLatin1String("<") + nickLine + QLatin1String(">"); if(Preferences::self()->useBoldNicks()) nickLine = QLatin1String("") + nickLine + QLatin1String(""); return nickLine; } void IRCView::replaceDecoration(QString& line, char decoration, char replacement) { int pos; bool decorated = false; while((pos=line.indexOf(decoration))!=-1) { line.replace(pos,1,(decorated) ? QStringLiteral("").arg(replacement) : QStringLiteral("<%1>").arg(replacement)); decorated = !decorated; } } QString IRCView::filter(const QString& line, const QString& defaultColor, const QString& whoSent, bool doHighlight, bool parseURL, bool self, QChar::Direction* direction) { QString filteredLine(line); Application* konvApp = Application::instance(); //Since we can't turn off whitespace simplification withouteliminating text wrapping, // if the line starts with a space turn it into a non-breaking space. // (which magically turns back into a space on copy) if (filteredLine[0] == ' ') { filteredLine[0] = '\xA0'; } // TODO: Use QStyleSheet::escape() here // Replace all < with < filteredLine.replace('<', "\x0blt;"); // Replace all > with > filteredLine.replace('>', "\x0bgt;"); if (filteredLine.contains('\x07')) { if (Preferences::self()->beep()) { qApp->beep(); } //remove char after beep filteredLine.remove('\x07'); } filteredLine = ircTextToHtml(filteredLine, parseURL, defaultColor, whoSent, true, direction); // Highlight QString ownNick; if (m_server) { ownNick = m_server->getNickname(); } else if (m_chatWin->getType() == ChatWindow::DccChat) { ownNick = qobject_cast(m_chatWin)->ownNick(); } if(doHighlight && (whoSent != ownNick) && !self) { QString highlightColor; if (Preferences::self()->highlightNick() && line.toLower().contains(QRegExp("(^|[^\\d\\w])" + QRegExp::escape(ownNick.toLower()) + "([^\\d\\w]|$)"))) { // highlight current nickname highlightColor = Preferences::self()->highlightNickColor().name(); m_tabNotification = Konversation::tnfNick; } else { QList highlightList = Preferences::highlightList(); QListIterator it(highlightList); Highlight* highlight; QStringList highlightChatWindowList; bool patternFound = false; QStringList captures; while (it.hasNext()) { highlight = it.next(); highlightChatWindowList = highlight->getChatWindowList(); if (highlightChatWindowList.isEmpty() || highlightChatWindowList.contains(m_chatWin->getName(), Qt::CaseInsensitive)) { if (highlight->getRegExp()) { QRegExp needleReg(highlight->getPattern()); needleReg.setCaseSensitivity(Qt::CaseInsensitive); // highlight regexp in text patternFound = ((line.contains(needleReg)) || // highlight regexp in nickname (whoSent.contains(needleReg))); // remember captured patterns for later captures = needleReg.capturedTexts(); } else { QString needle = highlight->getPattern(); // highlight patterns in text patternFound = ((line.contains(needle, Qt::CaseInsensitive)) || // highlight patterns in nickname (whoSent.contains(needle, Qt::CaseInsensitive))); } if (patternFound) { break; } } } if (patternFound) { highlightColor = highlight->getColor().name(); m_highlightColor = highlightColor; if (highlight->getNotify()) { m_tabNotification = Konversation::tnfHighlight; if (Preferences::self()->highlightSoundsEnabled() && m_chatWin->notificationsEnabled()) { konvApp->sound()->play(highlight->getSoundURL()); } konvApp->notificationHandler()->highlight(m_chatWin, whoSent, line); } m_autoTextToSend = highlight->getAutoText(); // replace %0 - %9 in regex groups for (int capture = 0; capture < captures.count(); capture++) { m_autoTextToSend.replace(QStringLiteral("%%1").arg(capture), captures[capture]); } m_autoTextToSend.remove(QRegExp("%[0-9]")); } } // apply found highlight color to line if (!highlightColor.isEmpty()) { filteredLine = QLatin1String("") + filteredLine + QLatin1String(""); } } else if (doHighlight && (whoSent == ownNick) && Preferences::self()->highlightOwnLines()) { // highlight own lines filteredLine = QLatin1String("highlightOwnLinesColor().name() + QLatin1String("\">") + filteredLine + QLatin1String(""); } - filteredLine = Konversation::Emoticons::parseEmoticons(filteredLine); - return filteredLine; } QString IRCView::ircTextToHtml(const QString& text, bool parseURL, const QString& defaultColor, const QString& whoSent, bool closeAllTags, QChar::Direction* direction) { TextHtmlData data; data.defaultColor = defaultColor; QString htmlText(text); bool allowColors = Preferences::self()->allowColorCodes(); QString linkColor = Preferences::self()->color(Preferences::Hyperlink).name(); unsigned int rtl_chars = 0; unsigned int ltr_chars = 0; QString fromNick; TextUrlData urlData; TextChannelData channelData; if (parseURL) { QString strippedText(removeIrcMarkup(htmlText)); urlData = extractUrlData(strippedText); if (!urlData.urlRanges.isEmpty()) { // we detected the urls on a clean richtext-char-less text // to make 100% sure we get the correct urls, but as a result // we have to map them back to the original url adjustUrlRanges(urlData.urlRanges, urlData.fixedUrls, htmlText, strippedText); //Only set fromNick if we actually have a url, //yes this is a ultra-minor-optimization if (whoSent.isEmpty()) fromNick = m_chatWin->getName(); else fromNick = whoSent; } channelData = extractChannelData(strippedText); adjustUrlRanges(channelData.channelRanges, channelData.fixedChannels , htmlText, strippedText); } else { // Change & to & to prevent html entities to do strange things to the text htmlText.replace('&', QLatin1String("&")); htmlText.replace("\x0b", QLatin1String("&")); } int linkPos = -1; int linkOffset = 0; bool doChannel = false; if (parseURL) { //get next recent channel or link pos if (!urlData.urlRanges.isEmpty() && !channelData.channelRanges.isEmpty()) { if (urlData.urlRanges.first() < channelData.channelRanges.first()) { doChannel = false; linkPos = urlData.urlRanges.first().first; } else { doChannel = true; linkPos = channelData.channelRanges.first().first; } } else if (!urlData.urlRanges.isEmpty() && channelData.channelRanges.isEmpty()) { doChannel = false; linkPos = urlData.urlRanges.first().first; } else if (urlData.urlRanges.isEmpty() && !channelData.channelRanges.isEmpty()) { doChannel = true; linkPos = channelData.channelRanges.first().first; } else { linkPos = -1; } } // Remember last char for pair of spaces situation, see default in switch (htmlText.at(pos)... QChar lastChar; int offset; for (int pos = 0; pos < htmlText.length(); ++pos) { //check for next relevant url or channel link to insert if (parseURL && pos == linkPos+linkOffset) { if (doChannel) { QString fixedChannel = channelData.fixedChannels.takeFirst(); const QPair& range = channelData.channelRanges.takeFirst(); QString oldChannel = htmlText.mid(pos, range.second); QString strippedChannel = removeIrcMarkup(oldChannel); QString colorCodes = extractColorCodes(oldChannel); QString link("%1%3%4%5"); link = link.arg(closeTags(&data), fixedChannel, strippedChannel, openTags(&data, 0), colorCodes); htmlText.replace(pos, oldChannel.length(), link); pos += link.length() - colorCodes.length() - 1; linkOffset += link.length() - oldChannel.length(); } else { QString fixedUrl = urlData.fixedUrls.takeFirst(); const QPair& range = urlData.urlRanges.takeFirst(); QString oldUrl = htmlText.mid(pos, range.second); QString strippedUrl = removeIrcMarkup(oldUrl); QString closeTagsString(closeTags(&data)); QString colorCodes = extractColorCodes(oldUrl); colorCodes = removeDuplicateCodes(colorCodes, &data, allowColors); QString link("%1%3%4%5"); link = link.arg(closeTagsString, fixedUrl, strippedUrl, openTags(&data, 0), colorCodes); htmlText.replace(pos, oldUrl.length(), link); //url catcher QMetaObject::invokeMethod(Application::instance(), "storeUrl", Qt::QueuedConnection, Q_ARG(QString, fromNick), Q_ARG(QString, fixedUrl), Q_ARG(QDateTime, QDateTime::currentDateTime())); pos += link.length() - colorCodes.length() - 1; linkOffset += link.length() - oldUrl.length(); } bool invalidNextLink = false; do { if (!urlData.urlRanges.isEmpty() && !channelData.channelRanges.isEmpty()) { if (urlData.urlRanges.first() < channelData.channelRanges.first()) { doChannel = false; linkPos = urlData.urlRanges.first().first; } else { doChannel = true; linkPos = channelData.channelRanges.first().first; } } else if (!urlData.urlRanges.isEmpty() && channelData.channelRanges.isEmpty()) { doChannel = false; linkPos = urlData.urlRanges.first().first; } else if (urlData.urlRanges.isEmpty() && !channelData.channelRanges.isEmpty()) { doChannel = true; linkPos = channelData.channelRanges.first().first; } else { linkPos = -1; } //for cases like "#www.some.url" we get first channel //and also url, the channel->clickable-channel replace we are //already after the url, so just forget it, as a clickable //channel is correct in this case if (linkPos > -1 && linkPos+linkOffset < pos) { invalidNextLink = true; if (doChannel) { channelData.channelRanges.removeFirst(); channelData.fixedChannels.removeFirst(); } else { urlData.urlRanges.removeFirst(); urlData.fixedUrls.removeFirst(); } } else { invalidNextLink = false; } } while (invalidNextLink); continue; } switch (htmlText.at(pos).toLatin1()) { case '\x02': //bold offset = defaultHtmlReplace(htmlText, &data, pos, QStringLiteral("b")); pos += offset -1; linkOffset += offset -1; break; case '\x1d': //italic offset = defaultHtmlReplace(htmlText, &data, pos, QStringLiteral("i")); pos += offset -1; linkOffset += offset -1; break; case '\x15': //mirc underline case '\x1f': //kvirc underline offset = defaultHtmlReplace(htmlText, &data, pos, QStringLiteral("u")); pos += offset -1; linkOffset += offset -1; break; case '\x13': //strikethru offset = defaultHtmlReplace(htmlText, &data, pos, QStringLiteral("s")); pos += offset -1; linkOffset += offset -1; break; case '\x03': //color { QString fgColor, bgColor; bool fgOK = true, bgOK = true; QString colorMatch(getColors(htmlText, pos, fgColor, bgColor, &fgOK, &bgOK)); if (!allowColors) { htmlText.remove(pos, colorMatch.length()); pos -= 1; linkOffset -= colorMatch.length(); break; } QString colorString; // check for color reset conditions //TODO check if \x11 \017 is really valid here if (colorMatch == QLatin1String("\x03") || colorMatch == QLatin1String("\x11") || (fgColor.isEmpty() && bgColor.isEmpty()) || (!fgOK && !bgOK)) { //in reverse mode, just reset both colors //color tags are already closed before the reverse start if (data.reverse) { data.lastFgColor.clear(); data.lastBgColor.clear(); } else { if (data.openHtmlTags.contains(QLatin1String("font")) && data.openHtmlTags.contains(QLatin1String("span"))) { colorString += closeToTagString(&data, QStringLiteral("span")); data.lastBgColor.clear(); colorString += closeToTagString(&data, QStringLiteral("font")); data.lastFgColor.clear(); } else if (data.openHtmlTags.contains(QLatin1String("font"))) { colorString += closeToTagString(&data, QStringLiteral("font")); data.lastFgColor.clear(); } } htmlText.replace(pos, colorMatch.length(), colorString); pos += colorString.length() - 1; linkOffset += colorString.length() -colorMatch.length(); break; } if (!fgOK) { fgColor = defaultColor; } if (!bgOK) { bgColor = fontColorOpenTag(Preferences::self()->color(Preferences::TextViewBackground).name()); } // if we are in reverse mode, just remember the new colors if (data.reverse) { if (!fgColor.isEmpty()) { data.lastFgColor = fgColor; if (!bgColor.isEmpty()) { data.lastBgColor = bgColor; } } } // do we have a new fgColor? // NOTE: there is no new bgColor is there is no fgColor else if (!fgColor.isEmpty()) { if (data.openHtmlTags.contains(QLatin1String("font")) && data.openHtmlTags.contains(QLatin1String("span"))) { colorString += closeToTagString(&data, QStringLiteral("span")); colorString += closeToTagString(&data, QStringLiteral("font")); } else if (data.openHtmlTags.contains(QLatin1String("font"))) { colorString += closeToTagString(&data, QStringLiteral("font")); } data.lastFgColor = fgColor; if (!bgColor.isEmpty()) data.lastBgColor = bgColor; if (!data.lastFgColor.isEmpty()) { colorString += fontColorOpenTag(data.lastFgColor); data.openHtmlTags.append(QStringLiteral("font")); if (!data.lastBgColor.isEmpty()) { colorString += spanColorOpenTag(data.lastBgColor); data.openHtmlTags.append(QStringLiteral("span")); } } } htmlText.replace(pos, colorMatch.length(), colorString); pos += colorString.length() - 1; linkOffset += colorString.length() -colorMatch.length(); } break; case '\x0f': //reset to default { QString closeText; while (!data.openHtmlTags.isEmpty()) { closeText += QLatin1String("'); } data.lastBgColor.clear(); data.lastFgColor.clear(); data.reverse = false; htmlText.replace(pos, 1, closeText); pos += closeText.length() - 1; linkOffset += closeText.length() - 1; } break; case '\x16': //reverse { // treat inverse as color and block it if colors are not allowed if (!allowColors) { htmlText.remove(pos, 1); pos -= 1; linkOffset -= 1; break; } QString colorString; // close current color strings and open reverse tags if (!data.reverse) { if (data.openHtmlTags.contains(QLatin1String("span"))) { colorString += closeToTagString(&data, QStringLiteral("span")); } if (data.openHtmlTags.contains(QLatin1String("font"))) { colorString += closeToTagString(&data, QStringLiteral("font")); } data.reverse = true; colorString += fontColorOpenTag(Preferences::self()->color(Preferences::TextViewBackground).name()); data.openHtmlTags.append(QStringLiteral("font")); colorString += spanColorOpenTag(defaultColor); data.openHtmlTags.append(QStringLiteral("span")); } else { // if reset reverse, close reverse and set old fore- and // back-groundcolor if set in data colorString += closeToTagString(&data, QStringLiteral("span")); colorString += closeToTagString(&data, QStringLiteral("font")); data.reverse = false; if (!data.lastFgColor.isEmpty()) { colorString += fontColorOpenTag(data.lastFgColor); data.openHtmlTags.append(QStringLiteral("font")); if (!data.lastBgColor.isEmpty()) { colorString += spanColorOpenTag(data.lastBgColor); data.openHtmlTags.append(QStringLiteral("span")); } } } htmlText.replace(pos, 1, colorString); pos += colorString.length() -1; linkOffset += colorString.length() -1; } break; default: { const QChar& dirChar = htmlText.at(pos); // Replace pairs of spaces with " " to preserve some semblance of text wrapping //filteredLine.replace(" ", " \xA0"); // This used to work like above. But just for normal text like "test test" // It got replaced as "test \xA0 \xA0test" and QTextEdit showed 4 spaces. // In case of color/italic/bold codes we don't necessary get a real pair of spaces // just "test test" and QTextEdit shows it as 1 space. // Now if we remember the last char, to ignore html tags, and check if current and last ones are spaces // we replace the current one with \xA0 (a forced space) and get // "test \xA0 \xA0test", which QTextEdit correctly shows as 4 spaces. //NOTE: replacing all spaces with forced spaces will break text wrapping if (dirChar == ' ' && !lastChar.isNull() && lastChar == ' ') { htmlText[pos] = '\xA0'; lastChar = '\xA0'; } else { lastChar = dirChar; } if (!(dirChar.isNumber() || dirChar.isSymbol() || dirChar.isSpace() || dirChar.isPunct() || dirChar.isMark())) { switch(dirChar.direction()) { case QChar::DirL: case QChar::DirLRO: case QChar::DirLRE: ltr_chars++; break; case QChar::DirR: case QChar::DirAL: case QChar::DirRLO: case QChar::DirRLE: rtl_chars++; break; default: break; } } } } } if (direction) { // in case we found no right or left direction chars both // values are 0, but rtl_chars > ltr_chars is still false and QChar::DirL // is returned as default. if (rtl_chars > ltr_chars) *direction = QChar::DirR; else *direction = QChar::DirL; } if (parseURL) { // Change & to & to prevent html entities to do strange things to the text htmlText.replace('&', QLatin1String("&")); htmlText.replace("\x0b", QLatin1String("&")); } if (closeAllTags) { htmlText += closeTags(&data); } return htmlText; } int IRCView::defaultHtmlReplace(QString& htmlText, TextHtmlData* data, int pos, const QString& tag) { QString replace; if (data->openHtmlTags.contains(tag)) { replace = closeToTagString(data, tag); } else { data->openHtmlTags.append(tag); replace = QLatin1Char('<') + tag + QLatin1Char('>'); } htmlText.replace(pos, 1, replace); return replace.length(); } QString IRCView::closeToTagString(TextHtmlData* data, const QString& _tag) { QString ret; QString tag; int i = data->openHtmlTags.count() - 1; //close all tags to _tag for ( ; i >= 0 ; --i) { tag = data->openHtmlTags.at(i); ret += QLatin1String("'); if (tag == _tag) { data->openHtmlTags.removeAt(i); break; } } // reopen relevant tags if (i > -1) ret += openTags(data, i); return ret; } QString IRCView::openTags(TextHtmlData* data, int from) { QString ret, tag; int i = from > -1 ? from : 0; for ( ; i < data->openHtmlTags.count(); ++i) { tag = data->openHtmlTags.at(i); if (tag == QLatin1String("font")) { if (data->reverse) { ret += fontColorOpenTag(Preferences::self()->color(Preferences::TextViewBackground).name()); } else { ret += fontColorOpenTag(data->lastFgColor); } } else if (tag == QLatin1String("span")) { if (data->reverse) { ret += spanColorOpenTag(data->defaultColor); } else { ret += spanColorOpenTag(data->lastBgColor); } } else { ret += QLatin1Char('<') + tag + QLatin1Char('>'); } } return ret; } QString IRCView::closeTags(TextHtmlData* data) { QString ret; QListIterator< QString > i(data->openHtmlTags); i.toBack(); while (i.hasPrevious()) { ret += QLatin1String("'); } return ret; } QString IRCView::fontColorOpenTag(const QString& fgColor) { return QLatin1String(""); } QString IRCView::spanColorOpenTag(const QString& bgColor) { return QLatin1String(""); } QString IRCView::removeDuplicateCodes(const QString& codes, TextHtmlData* data, bool allowColors) { int pos = 0; QString ret; while (pos < codes.length()) { switch (codes.at(pos).toLatin1()) { case '\x02': //bold defaultRemoveDuplicateHandling(data, QStringLiteral("b")); ++pos; break; case '\x1d': //italic defaultRemoveDuplicateHandling(data, QStringLiteral("i")); ++pos; break; case '\x15': //mirc underline case '\x1f': //kvirc underline defaultRemoveDuplicateHandling(data, QStringLiteral("u")); ++pos; break; case '\x13': //strikethru defaultRemoveDuplicateHandling(data, QStringLiteral("s")); ++pos; break; case '\x0f': //reset to default data->openHtmlTags.clear(); data->lastBgColor.clear(); data->lastFgColor.clear(); data->reverse = false; ++pos; break; case '\x16': //reverse if (!allowColors) { pos += 1; continue; } if (data->reverse) { data->openHtmlTags.removeOne(QStringLiteral("span")); data->openHtmlTags.removeOne(QStringLiteral("font")); data->reverse = false; if (!data->lastFgColor.isEmpty()) { data->openHtmlTags.append(QStringLiteral("font")); if (!data->lastBgColor.isEmpty()) { data->openHtmlTags.append(QStringLiteral("span")); } } } else { data->openHtmlTags.removeOne(QStringLiteral("span")); data->openHtmlTags.removeOne(QStringLiteral("font")); data->reverse = true; data->openHtmlTags.append(QStringLiteral("font")); data->openHtmlTags.append(QStringLiteral("span")); } ++pos; break; case '\x03': //color { QString fgColor, bgColor; bool fgOK = true, bgOK = true; QString colorMatch(getColors(codes, pos, fgColor, bgColor, &fgOK, &bgOK)); if (!allowColors) { pos += colorMatch.length(); continue; } // check for color reset conditions //TODO check if \x11 \017 is really valid here if (colorMatch == QLatin1String("\x03") || colorMatch == QLatin1String("\x11") || (fgColor.isEmpty() && bgColor.isEmpty()) || (!fgOK && !bgOK)) { if (!data->lastBgColor.isEmpty()) { data->lastBgColor.clear(); data->openHtmlTags.removeOne(QStringLiteral("span")); } if (!data->lastFgColor.isEmpty()) { data->lastFgColor.clear(); data->openHtmlTags.removeOne(QStringLiteral("font")); } pos += colorMatch.length(); break; } if (!fgOK) { fgColor = data->defaultColor; } if (!bgOK) { bgColor = fontColorOpenTag(Preferences::self()->color(Preferences::TextViewBackground).name()); } if (!fgColor.isEmpty()) { data->lastFgColor = fgColor; data->openHtmlTags.append(QStringLiteral("font")); if (!bgColor.isEmpty()) { data->lastBgColor = bgColor; data->openHtmlTags.append(QStringLiteral("span")); } } pos += colorMatch.length(); } break; default: // qDebug() << "unsupported duplicate code:" << QString::number(codes.at(pos).toLatin1(), 16); ret += codes.at(pos); ++pos; } } return ret; } void IRCView::defaultRemoveDuplicateHandling(TextHtmlData* data, const QString& tag) { if (data->openHtmlTags.contains(tag)) { data->openHtmlTags.removeOne(tag); } else { data->openHtmlTags.append(tag); } } void IRCView::adjustUrlRanges(QList< QPair >& urlRanges, const QStringList& fixedUrls, QString& richtext, const QString& strippedText) { Q_UNUSED(fixedUrls); QRegExp ircRichtextRegExp(colorRegExp); int start = 0, j; int i = 0; QString url; int htmlTextLength = richtext.length(), urlCount = urlRanges.count(); for (int x = 0; x < urlCount; ++x) { if (x == 0) i = urlRanges.first().first; j = 0; const QPair& range = urlRanges.at(x); url = strippedText.mid(range.first, range.second); for ( ; i < htmlTextLength; ++i) { if (richtext.at(i) == url.at(j)) { if (j == 0) start = i; ++j; if (j == url.length()) { urlRanges[x].first = start; urlRanges[x].second = i - start + 1; break; } } else if (ircRichtextRegExp.exactMatch(richtext.at(i))) { ircRichtextRegExp.indexIn(richtext, i); i += ircRichtextRegExp.matchedLength() - 1; } else { j = 0; } } } } QString IRCView::getColors(const QString& text, int start, QString& _fgColor, QString& _bgColor, bool* fgValueOK, bool* bgValueOK) { QRegExp ircColorRegExp("(\003([0-9][0-9]|[0-9]|)(,([0-9][0-9]|[0-9]|)|,|)|\017)"); if (ircColorRegExp.indexIn(text,start) == -1) return QString(); QString ret(ircColorRegExp.cap(0)); QString fgColor(ircColorRegExp.cap(2)), bgColor(ircColorRegExp.cap(4)); if (!fgColor.isEmpty()) { int foregroundColor = fgColor.toInt(); if (foregroundColor > -1 && foregroundColor < 16) { _fgColor = Preferences::self()->ircColorCode(foregroundColor).name(); if (fgValueOK) *fgValueOK = true; } else { if (fgValueOK) *fgValueOK = false; } } else { if (fgValueOK) *fgValueOK = true; } if (!bgColor.isEmpty()) { int backgroundColor = bgColor.toInt(); if (backgroundColor > -1 && backgroundColor < 16) { _bgColor = Preferences::self()->ircColorCode(backgroundColor).name(); if (bgValueOK) *bgValueOK = true; } else { if (bgValueOK) *bgValueOK = false; } } else { if (bgValueOK) *bgValueOK = true; } return ret; } void IRCView::resizeEvent(QResizeEvent *event) { ScrollBarPin b(verticalScrollBar()); QTextBrowser::resizeEvent(event); } void IRCView::mouseMoveEvent(QMouseEvent* ev) { if (m_mousePressedOnUrl && (m_mousePressPosition - ev->pos()).manhattanLength() > QApplication::startDragDistance()) { m_mousePressedOnUrl = false; QTextCursor textCursor = this->textCursor(); textCursor.clearSelection(); setTextCursor(textCursor); QPointer drag = new QDrag(this); QMimeData* mimeData = new QMimeData; QUrl url(m_dragUrl); mimeData->setUrls(QList() << url); drag->setMimeData(mimeData); QPixmap pixmap = KIO::pixmapForUrl(url, 0, KIconLoader::Desktop, KIconLoader::SizeMedium); drag->setPixmap(pixmap); drag->exec(); return; } else { // Store the url here instead of in highlightedSlot as the link given there is decoded. m_urlToCopy = anchorAt(ev->pos()); } QTextBrowser::mouseMoveEvent(ev); } void IRCView::mousePressEvent(QMouseEvent* ev) { if (ev->button() == Qt::LeftButton) { m_dragUrl = anchorAt(ev->pos()); if (!m_dragUrl.isEmpty() && Konversation::isUrl(m_dragUrl)) { m_mousePressedOnUrl = true; m_mousePressPosition = ev->pos(); } } QTextBrowser::mousePressEvent(ev); } void IRCView::wheelEvent(QWheelEvent *ev) { if(ev->modifiers()==Qt::ControlModifier) { if(ev->delta() < 0) decreaseFontSize(); if(ev->delta() > 0) increaseFontSize(); } QTextBrowser::wheelEvent(ev); } void IRCView::mouseReleaseEvent(QMouseEvent *ev) { if (ev->button() == Qt::LeftButton) { m_mousePressedOnUrl = false; } else if (ev->button() == Qt::MidButton) { if (m_contextMenuOptions.testFlag(IrcContextMenus::ShowLinkActions)) { // The QUrl magic is what QTextBrowser's anchorClicked() does internally; // we copy it here for consistent behavior between left and middle clicks. openLink(QUrl::fromEncoded(m_urlToCopy.toUtf8())); // krazy:exclude=qclasses return; } else { emit textPasted(true); return; } } QTextBrowser::mouseReleaseEvent(ev); } void IRCView::keyPressEvent(QKeyEvent* ev) { const int key = ev->key() | ev->modifiers(); if (KStandardShortcut::paste().contains(key)) { emit textPasted(false); ev->accept(); return; } QTextBrowser::keyPressEvent(ev); } void IRCView::anchorClicked(const QUrl& url) { openLink(url); } void IRCView::openLink(const QUrl& url) { QString link(url.toString()); // HACK Replace " " with %20 for channelnames, NOTE there can't be 2 channelnames in one link link.replace (' ', QLatin1String("%20")); // HACK Handle pipe as toString doesn't seem to decode that correctly link.replace (QLatin1String("%7C"), QLatin1String("|")); // HACK Handle ` as toString doesn't seem to decode that correctly link.replace (QLatin1String("%60"), QLatin1String("`")); if (!link.isEmpty() && !link.startsWith('#')) Application::openUrl(url.toEncoded()); //FIXME: Don't do channel links in DCC Chats to begin with since they don't have a server. else if (link.startsWith(QLatin1String("##")) && m_server && m_server->isConnected()) { m_server->sendJoinCommand(link.mid(1)); } //FIXME: Don't do user links in DCC Chats to begin with since they don't have a server. else if (link.startsWith('#') && m_server && m_server->isConnected()) { QString recipient(link); recipient.remove('#'); NickInfoPtr nickInfo = m_server->obtainNickInfo(recipient); m_server->addQuery(nickInfo, true /*we initiated*/); } } void IRCView::highlightedSlot(const QString& /*_link*/) { QString link = m_urlToCopy; // HACK Replace " " with %20 for channelnames, NOTE there can't be 2 channelnames in one link link.replace (' ', QLatin1String("%20")); //we just saw this a second ago. no need to reemit. if (link == m_lastStatusText && !link.isEmpty()) return; if (link.isEmpty()) { if (!m_lastStatusText.isEmpty()) { emit clearStatusBarTempText(); m_lastStatusText.clear(); } } else { m_lastStatusText = link; } if (!link.startsWith(QLatin1Char('#'))) { m_isOnNick = false; m_isOnChannel = false; if (!link.isEmpty()) { //link therefore != m_lastStatusText so emit with this new text emit setStatusBarTempText(link); } if (link.isEmpty() && m_contextMenuOptions.testFlag(IrcContextMenus::ShowLinkActions)) setContextMenuOptions(IrcContextMenus::ShowLinkActions, false); else if (!link.isEmpty() && !m_contextMenuOptions.testFlag(IrcContextMenus::ShowLinkActions)) setContextMenuOptions(IrcContextMenus::ShowLinkActions, true); } else if (link.startsWith(QLatin1Char('#')) && !link.startsWith(QLatin1String("##"))) { m_currentNick = link.mid(1); m_isOnNick = true; emit setStatusBarTempText(i18n("Open a query with %1", m_currentNick)); } else { // link.startsWith("##") m_currentChannel = link.mid(1); m_isOnChannel = true; emit setStatusBarTempText(i18n("Join the channel %1", m_currentChannel)); } } void IRCView::setContextMenuOptions(IrcContextMenus::MenuOptions options, bool on) { if (on) m_contextMenuOptions |= options; else m_contextMenuOptions &= ~options; } void IRCView::contextMenuEvent(QContextMenuEvent* ev) { // Consider the following scenario: (1) context menu opened, (2) mouse // pointer moved, (3) mouse button clicked to dismiss menu, (4) mouse // button clicked to reopen context menu. In this scenario, if there is // no mouse movement between steps (3) and (4), highlighted() is never // emitted, and the data we use here to display the correct context menu // is outdated. Thus what we're going to do here is post a fake mouse // move event using the context menu event coordinate, forcing an update // just before we display the context menu. QMouseEvent fake(QEvent::MouseMove, ev->pos(), Qt::NoButton, Qt::NoButton, Qt::NoModifier); mouseMoveEvent(&fake); if (m_isOnChannel && m_server) { IrcContextMenus::channelMenu(ev->globalPos(), m_server, m_currentChannel); m_isOnChannel = false; return; } if (m_isOnNick && m_server) { IrcContextMenus::nickMenu(ev->globalPos(), m_contextMenuOptions, m_server, QStringList() << m_currentNick, m_chatWin->getName()); m_currentNick.clear(); m_isOnNick = false; return; } int contextMenuActionId = IrcContextMenus::textMenu(ev->globalPos(), m_contextMenuOptions, m_server, textCursor().selectedText(), m_urlToCopy, m_contextMenuOptions.testFlag(IrcContextMenus::ShowNickActions) ? m_chatWin->getName() : QString()); switch (contextMenuActionId) { case -1: break; case IrcContextMenus::TextCopy: copy(); break; case IrcContextMenus::TextSelectAll: selectAll(); break; default: if (m_contextMenuOptions.testFlag(IrcContextMenus::ShowNickActions)) { IrcContextMenus::processNickAction(contextMenuActionId, m_server, QStringList() << m_chatWin->getName(), m_contextMenuOptions.testFlag(IrcContextMenus::ShowChannelActions) ? m_chatWin->getName() : QString()); } break; } } // For more information about these RTFM // http://www.unicode.org/reports/tr9/ // http://www.w3.org/TR/unicode-xml/ QChar IRCView::LRM = (ushort)0x200e; // Right-to-Left Mark QChar IRCView::RLM = (ushort)0x200f; // Left-to-Right Mark QChar IRCView::LRE = (ushort)0x202a; // Left-to-Right Embedding QChar IRCView::RLE = (ushort)0x202b; // Right-to-Left Embedding QChar IRCView::RLO = (ushort)0x202e; // Right-to-Left Override QChar IRCView::LRO = (ushort)0x202d; // Left-to-Right Override QChar IRCView::PDF = (ushort)0x202c; // Previously Defined Format QChar::Direction IRCView::basicDirection(const QString& string) { // The following code decides between LTR or RTL direction for // a line based on the amount of each type of characters pre- // sent. It does so by counting, but stops when one of the two // counters becomes higher than half of the string length to // avoid unnecessary work. unsigned int pos = 0; unsigned int rtl_chars = 0; unsigned int ltr_chars = 0; unsigned int str_len = string.length(); unsigned int str_half_len = str_len/2; for(pos=0; pos < str_len; ++pos) { if (!(string[pos].isNumber() || string[pos].isSymbol() || string[pos].isSpace() || string[pos].isPunct() || string[pos].isMark())) { switch(string[pos].direction()) { case QChar::DirL: case QChar::DirLRO: case QChar::DirLRE: ltr_chars++; break; case QChar::DirR: case QChar::DirAL: case QChar::DirRLO: case QChar::DirRLE: rtl_chars++; break; default: break; } } if (ltr_chars > str_half_len) return QChar::DirL; else if (rtl_chars > str_half_len) return QChar::DirR; } if (rtl_chars > ltr_chars) return QChar::DirR; else return QChar::DirL; } #define dS d.space() #define dN d.nospace() QDebug operator<<(QDebug d, QTextBlockUserData *bd) { Burr* b = dynamic_cast(bd); if (b) { dN; d << "("; d << (void*)(b) << ", format=" << b->m_format << ", blockNumber=" << b->m_block.blockNumber() << " p,n=" << (void*)b->m_prev << ", " << (void*)b->m_next; d << ")"; } else if (bd) dN << "(UNKNOWN! " << (void*)bd << ")"; else d << "(none)"; return d.space(); } QDebug operator<<(QDebug d, QTextFrame* feed) { if (feed) { d << "\nDumping frame..."; dN << hex << (void*)feed << dec; QTextFrame::iterator it = feed->begin(); if (it.currentFrame() == feed) dS << "loop!" << endl; dS << "position" << feed->firstPosition() << feed->lastPosition(); dN << "parentFrame=" << (void*)feed->parentFrame(); dS; while (!it.atEnd()) { //d << "spin"; QTextFrame *frame = it.currentFrame(); if (!frame) // this is a block { //d<<"dumping blocks:"; QTextBlock b = it.currentBlock(); //d << "block" << b.position() << b.length(); d << endl << b; } else if (frame != feed) { d << frame; } ++it; }; d << "\n...done.\n"; } else d << "No frame to dump."; return d; } QDebug operator<<(QDebug d, QTextDocument* document) { d << "====================================================================================================================================================================="; if (document) d << document->rootFrame(); return d; } QDebug operator<<(QDebug d, const QTextBlock &b) { QTextBlock::Iterator it = b.begin(); int fragCount = 0; d << "blockNumber" << b.blockNumber(); d << "position" << b.position(); d << "length" << b.length(); dN << "firstChar 0x" << hex << b.document()->characterAt(b.position()).unicode() << dec; if (b.length() == 2) dN << " second 0x" << hex << b.document()->characterAt(b.position()+1).unicode() << dec; dS << "userState" << b.userState(); dN << "userData " << (void*)b.userData(); //dS << "text" << b.text(); dS << endl; if (b.userData()) d << b.userData(); for (it = b.begin(); !(it.atEnd()); ++it) { QTextFragment f = it.fragment(); if (f.isValid()) { fragCount++; //d << "frag" << fragCount << _S(f.position()) << _S(f.length()); } } d << _S(fragCount); return d; }