diff --git a/src/config/chatwindowappearance_config.ui b/src/config/chatwindowappearance_config.ui index 10dba3a4..c9a15c4c 100644 --- a/src/config/chatwindowappearance_config.ui +++ b/src/config/chatwindowappearance_config.ui @@ -1,395 +1,428 @@ 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. ChatWindowAppearance_Config 0 0 401 - 502 + 558 - + + 0 + + + 0 + + + 0 + + 0 Enable &Timestamps true - - - - Sho&w dates + + + + true &Format: false kcfg_TimestampFormat - - - - true + + + + Sho&w dates Qt::Horizontal QSizePolicy::Expanding 191 20 + + + + Show date marker when date changes + + + Show &Backlog true &Lines: false kcfg_BacklogLines 10 100 Qt::Horizontal QSizePolicy::Expanding 251 20 &Layout &Show channel topic Show channel &mode buttons Show channel &nick list and quick buttons Qt::Horizontal QSizePolicy::Fixed 16 20 false Show hostmas&ks in nickname list Qt::Horizontal QSizePolicy::Fixed 16 20 false Show real names in nickname list Qt::Horizontal QSizePolicy::Fixed 16 20 false Show &quick buttons Show bo&x to change own nickname Show sc&rollbar Enable Back&ground Image true P&ath: false kcfg_BackgroundImage Qt::Vertical QSizePolicy::Expanding 20 20 KUrlRequester QFrame
kurlrequester.h
+ 1
KComboBox QComboBox
kcombobox.h
kcfg_Timestamping kcfg_TimestampFormat kcfg_ShowDate kcfg_ShowBacklog kcfg_BacklogLines kcfg_ShowTopic kcfg_ShowModeButtons kcfg_ShowNickList kcfg_AutoUserhost kcfg_ShowRealNames kcfg_ShowQuickButtons kcfg_ShowNicknameBox kcfg_ShowIRCViewScrollBar kcfg_ShowBackgroundImage kcfg_BackgroundImage kurlrequester.h klineedit.h kcfg_ShowNickList toggled(bool) kcfg_AutoUserhost setEnabled(bool) - 20 - 20 + 32 + 313 - 20 - 20 + 55 + 338 kcfg_ShowNickList toggled(bool) kcfg_ShowQuickButtons setEnabled(bool) - 20 - 20 + 32 + 313 - 20 - 20 + 90 + 413 kcfg_ShowNickList toggled(bool) kcfg_ShowRealNames setEnabled(bool) - 20 - 20 + 32 + 313 + + + 55 + 364 + + + + + kcfg_ShowDate + toggled(bool) + kcfg_ShowDateLine + setDisabled(bool) + + + 55 + 78 - 20 - 20 + 61 + 104
diff --git a/src/config/konversation.kcfg b/src/config/konversation.kcfg index 0f201fbe..1d59eafd 100644 --- a/src/config/konversation.kcfg +++ b/src/config/konversation.kcfg @@ -1,1043 +1,1048 @@ 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/ircview.cpp b/src/viewer/ircview.cpp index 8251f936..c129f615 100644 --- a/src/viewer/ircview.cpp +++ b/src/viewer/ircview.cpp @@ -1,2400 +1,2441 @@ // -*- 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) +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 *b = new Burr(this, m_lastMarkerLine, block, type == MarkerLine? BlockIsMarker : BlockIsRemember); + Burr *prevBurr = m_lastMarkerLine; + if(type == DateLine) + prevBurr = nullptr; + Burr *b = new Burr(this, prevBurr, block, objectFormatToBlockState(type)); block.setUserData(b); - m_lastMarkerLine = 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(); - QTime time = serverTime.isValid() ? serverTime.time() : QTime::currentTime(); + 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(time.toString(timeFormat)); + timeString = QString(QLatin1String("[%1] ")).arg(dateTime.time().toString(timeFormat)); + m_showDate = Preferences::self()->showDateLine() && dateTime.date() != m_prevTimestamp.date(); } else { - QDate date = serverTime.isValid() ? serverTime.date() : QDate::currentDate(); timeString = QString("[%1%2 %3%4] ") .arg((dateRtl==rtl) ? QString() : (dateRtl ? RLM : LRM), - QLocale().toString(date, QLocale::ShortFormat), - time.toString(timeFormat), + 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; } diff --git a/src/viewer/ircview.h b/src/viewer/ircview.h index a6da223d..bcd370fe 100644 --- a/src/viewer/ircview.h +++ b/src/viewer/ircview.h @@ -1,348 +1,354 @@ /* 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-2008 Eike Hein */ #ifndef IRCVIEW_H #define IRCVIEW_H #include "common.h" #include "irccontextmenus.h" #include #include #include #include +#include class Server; class ChatWindow; struct Burr; class IrcViewMarkerLine: public QObject, public QTextObjectInterface { Q_OBJECT Q_INTERFACES(QTextObjectInterface) public: IrcViewMarkerLine(QObject *p) : QObject(p), QTextObjectInterface() {} ~IrcViewMarkerLine() {} void drawObject(QPainter *painter, const QRectF &rect, QTextDocument *doc, int posInDocument, const QTextFormat &format) Q_DECL_OVERRIDE; QSizeF intrinsicSize(QTextDocument *doc, int posInDocument, const QTextFormat &format) Q_DECL_OVERRIDE; }; /// Helper struct which remembers the openHtmlTags, the fore and /// background color, if the reverse char is set and the defaultcolor (for reverse) /// while the ircrichtext -> html generation is in progress struct TextHtmlData { TextHtmlData() : reverse(false) { } QList openHtmlTags; QString lastFgColor; QString lastBgColor; bool reverse; QString defaultColor; }; class IRCView : public QTextBrowser { Q_OBJECT public: explicit IRCView(QWidget* parent); ~IRCView(); //! this function is proper given it is not nessary for the ircview to have a server for DCC. void setServer(Server* server); //! FIXME assumes the IRCView looks at a chatwin void setChatWin(ChatWindow* chatWin); bool search(const QString& pattern, QTextDocument::FindFlags flags, bool fromCursor); bool searchNext(bool reversed = false); //! FIXME maybe we should create some sort of palette of our own? QColor highlightColor() { return m_highlightColor; } void updateAppearance(); void setContextMenuOptions(IrcContextMenus::MenuOptions options, bool on); Q_SIGNALS: void gotFocus(); // So we can set focus to input line void textToLog(const QString& text); ///< send the to the log file void sendFile(); ///< a command for a target to which we can DCC send void autoText(const QString& text); ///< helper for autotext-on-highlight void textPasted(bool useSelection); ///< middle button with no m_copyUrlMenu void urlsDropped(const QList& urls); void doSearch(); /// Emitted when a search should be started void doSearchNext(); /// Emitted when there's a request to go to the next search result. void doSearchPrevious(); /// Emitted when there's a request to go to the previous search result. void setStatusBarTempText(const QString&); //! these two look like mixins to me void clearStatusBarTempText();//! these two look like mixins to me //// Marker lines public: /// Are there any markers or a remember lines in the view? ///Is used internally now. bool hasLines(); /// QTextBlockFormat states for setUserState. - enum BlockStates { None = -1, BlockIsMarker = 1, BlockIsRemember = 2 }; + enum BlockStates { None = -1, BlockIsMarker = 1, BlockIsRemember = 2, BlockIsDateMarker = 3 }; /// QTextCharFormat object types. - enum ObjectFormats { MarkerLine = QTextFormat::UserObject, RememberLine}; + enum ObjectFormats { MarkerLine = QTextFormat::UserObject, RememberLine, DateLine }; public Q_SLOTS: /// Inserts a marker line. /// Does not disturb m_rememberLineDirtyBit. void insertMarkerLine(); /// Insert a remember line now, or when text is appended. Sets the m_rememberLineDirtyBit /// unless configured to add a remember line at any time. void insertRememberLine(); /// Prevents the next append from inserting a remember line. ///Simply clears m_rememberLineDirtyBit. void cancelRememberLine(); /// Remove all of the marker lines, and the remember line. /// Does not effect m_rememberLineDirtyBit. void clearLines(); protected: QMimeData* createMimeDataFromSelection() const Q_DECL_OVERRIDE; void dragEnterEvent(QDragEnterEvent* e) Q_DECL_OVERRIDE; void dragMoveEvent(QDragMoveEvent* e) Q_DECL_OVERRIDE; void dropEvent(QDropEvent* e) Q_DECL_OVERRIDE; private: /// The internal mechanics of inserting a line. /// Clears m_rememberLineDirtyBit. void appendRememberLine(); /// Create a remember line and insert it. /// @return - Pointer to the Burr that was inserted into the block Burr* appendLine(ObjectFormats=MarkerLine); /// Convenience method - forget the position of the remember line and markers. void wipeLineParagraphs(); /// Convenience method - is the last block any sort of line, or a specific line? /// @param select - default value is -1, meaning "is any kind of line" bool lastBlockIsLine(int select=-1); /// Causes a block to stop being a marker. void voidLineBlock(const QTextBlock &rem); /// Shortcut to get an object format of the desired type QTextCharFormat getFormat(ObjectFormats); + BlockStates objectFormatToBlockState(ObjectFormats format); + public Q_SLOTS: // Doesn't have to be a slot, but what the hay. /// Called *only* from ~Burr(), by QTextBlockData::free void blockDeleted(Burr* b); private Q_SLOTS: /** Called every time a change occurs to the document. * * Used to infer the clearing of the entire document, * because Trolltech removed virtual from the method * that would indicate authoritatively. */ void cullMarkedLine(int, int, int); private: //marker/remember line data Burr *m_rememberLine, *m_lastMarkerLine; bool m_rememberLineDirtyBit; ///< the next append needs a remember line IrcViewMarkerLine markerFormatObject; ///< a QTextObjectInterface //// Other stuff public Q_SLOTS: //! FIXME enum { Raw, Query, Query+Action, Channel+Action, Server Message, Command Message, Backlog message } this looks more like a tuple void append(const QString& nick, const QString& message, const QHash &messageTags = QHash(), const QString& label = QString()); void appendRaw(const QString& message, bool self = false); void appendLog(const QString& message); void appendQuery(const QString& nick, const QString& message, const QHash &messageTags, bool inChannel = false); void appendQueryAction(const QString& nick, const QString& message, const QHash &messageTags); protected: //! FIXME why is this protected, and all alone down there? void appendAction(const QString& nick, const QString& message, const QHash &messageTags); /// Appends a new line without any scrollback or notification checks void doRawAppend(const QString& newLine, bool rtl); public Q_SLOTS: void appendChannelAction(const QString& nick, const QString& message, const QHash &messageTags); void appendServerMessage(const QString& type, const QString& message, const QHash &messageTags = QHash(), bool parseURL = true); void appendCommandMessage(const QString& command, const QString& message, const QHash &messageTags, bool parseURL=true, bool self=false); void appendBacklogMessage(const QString& firstColumn, const QString& message); protected: void doAppend(const QString& line, bool rtl, bool self=false); public Q_SLOTS: /// Emits the doSearch signal. void findText(); /// Emits the doSearchNext signal. void findNextText(); /// Emits the doSearchPrevious signal. void findPreviousText(); void increaseFontSize(); void decreaseFontSize(); void resetFontSize(); protected Q_SLOTS: void highlightedSlot(const QString& link); void anchorClicked(const QUrl& url); protected: void openLink(const QUrl &url); QString filter(const QString& line, const QString& defaultColor, const QString& who=QString(), bool doHighlight=true, bool parseURL=true, bool self=false, QChar::Direction* direction = 0); void replaceDecoration(QString& line, char decoration, char replacement); private: /// Returns a string where all irc-richtext chars are replaced with proper /// html tags and all urls are parsed if parseURL is true inline QString ircTextToHtml(const QString& text, bool parseURL, const QString& defaultColor, const QString& whoSent, bool closeAllTags = true, QChar::Direction* direction = 0); /// Returns a string that closes all open html tags to tag /// The closed tag is removed from opentagList in data inline QString closeToTagString(TextHtmlData* data, const QString& tag); /// Returns a html open span line with given backgroundcolor style inline QString spanColorOpenTag(const QString& bgColor); /// Returns a html open font line with given foregroundcolor inline QString fontColorOpenTag(const QString& fgColor); /// Insert a string that closes as open html tags to tag and reopen the remaining ones. /// For the next example I will use [b] as boldchar and [i] as italic char /// If are currently working on text like /// "aabbcc[b]dd[i]ee" /// it would generate for the next [b], "". /// is reopened as it is still relevant /// Returns the Length of the inserted String inline int defaultHtmlReplace(QString& htmlText, TextHtmlData* data, int pos, const QString& tag); /// Returns a string that opens all tags starting from index from inline QString openTags(TextHtmlData* data, int from = 0); /// Returns a string that closes all open tags /// but does not remove them from the opentaglist in data inline QString closeTags(TextHtmlData* data); /// This function looks in codes which tags it open/closes /// and appends/removes them from opentagList in data. /// This way we avoid pointless empty tags after the url like "" /// The returned string consists of all codes that this function could not deal with, /// which is the best case empty. QString removeDuplicateCodes(const QString& codes, TextHtmlData* data, bool allowColors); /// Helperfunction for removeDuplicateCodes, for dealing with simple irc richtext /// chars as bold, italic, underline and strikethrou. /// The default behaivor is to look if the tag is already in the /// opentagList in data and remove it if in case if is in, or /// append it in case it is not. inline void defaultRemoveDuplicateHandling(TextHtmlData* data, const QString& tag); /// Changes the ranges in urlRanges, that are found in /// strippedText, to match in richText. /// This is needed for cases were the url is tainted by ircrichtext chars inline void adjustUrlRanges(QList< QPair< int, int > >& urlRanges, const QStringList& fixedUrls, QString& richtext, const QString& strippedText); /// Parses the colors in text starting from start /// and returns them in the given fg and bg string, as well as information /// if the values are valid inline QString getColors(const QString& text, int start, QString& _fgColor, QString& _bgColor, bool* invalidFgVal, bool* invalidBgValue); protected: void resizeEvent(QResizeEvent *event) Q_DECL_OVERRIDE; void mouseReleaseEvent(QMouseEvent* ev) Q_DECL_OVERRIDE; void mousePressEvent(QMouseEvent* ev) Q_DECL_OVERRIDE; void mouseMoveEvent(QMouseEvent* ev) Q_DECL_OVERRIDE; void keyPressEvent(QKeyEvent* ev) Q_DECL_OVERRIDE; void contextMenuEvent(QContextMenuEvent* ev) Q_DECL_OVERRIDE; void wheelEvent(QWheelEvent* ev) Q_DECL_OVERRIDE; QChar::Direction basicDirection(const QString &string); /// Returns true if the timestamp string is RTL, otherwise false bool dateRtlDirection(); /// Format the line by adding needed bidi marks QString formatFinalLine(bool rtl, const QString &lineColor, const QString &label, const QString &nickLine, const QString &nickStar, const QString &text); /// Returns a formated timestamp if timestamps are enabled else it returns QString::null QString timeStamp(QHash messageTags, bool rtl); /// Returns a formated nick string //! FIXME formatted in what way? QString createNickLine(const QString& nick, const QString& defaultColor, bool encapsulateNick = true, bool privMsg = false); //// Search QTextDocument::FindFlags m_searchFlags; bool m_forward; QString m_pattern; //used in ::filter QColor m_highlightColor; QString m_lastStatusText; //last sent status text to the statusbar. Is empty after clearStatusBarTempText() //used in ::filter QString m_autoTextToSend; //TODO FIXME light this on fire and send it sailing down an uncharted river riddled with arrows Konversation::TabNotifyType m_tabNotification; Server* m_server; //! FIXME assumes we have a server //// RTL hack static QChar LRM; static QChar RLM; static QChar LRE; static QChar RLE; static QChar RLO; static QChar LRO; static QChar PDF; IrcContextMenus::MenuOptions m_contextMenuOptions; QString m_currentNick; QString m_currentChannel; QString m_urlToCopy; ///< the URL we might be about to copy bool m_isOnNick; ///< context menu click hit a nickname bool m_isOnChannel; ///< context menu click hit a channel bool m_mousePressedOnUrl; ///< currently processing a mouse press QPoint m_mousePressPosition; ///< x,y of the click, relative to the GPS location of tip of Phantom's left ear QString m_dragUrl; ///< we took a stab at whatever was clicked on, may or may not actually be a URL //! TODO FIXME i'll bite. why do we have this in here? QFontDatabase m_fontDataBase; int m_fontSizeDelta; ChatWindow* m_chatWin; friend class IRCStyleSheet; + + QDateTime m_prevTimestamp; + bool m_showDate; }; #endif