diff --git a/src/irc/messagemodel.cpp b/src/irc/messagemodel.cpp index b1cb84ba..b71e7b23 100644 --- a/src/irc/messagemodel.cpp +++ b/src/irc/messagemodel.cpp @@ -1,321 +1,293 @@ /* 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) 2017 Eike Hein */ #include "messagemodel.h" #include #include #include +#include + #define MAX_MESSAGES 500000 #define MAX_MESSAGES_TOLERANCE 501000 #define ALLOCATION_BATCH_SIZE 500 FilteredMessageModel::FilteredMessageModel(QObject *parent) : QSortFilterProxyModel(parent) , m_filterView(nullptr) { m_selectionModel = new QItemSelectionModel(this, this); connect(m_selectionModel, &QItemSelectionModel::selectionChanged, this, &FilteredMessageModel::selectionChanged); } FilteredMessageModel::~FilteredMessageModel() { } +void FilteredMessageModel::bla(QObject *obj) +{ + QQuickItem *item = qobject_cast(obj); + //item->setKeepMouseGrab(false); + item->ungrabMouse(); +} + QObject *FilteredMessageModel::filterView() const { return m_filterView; } void FilteredMessageModel::setFilterView(QObject *view) { if (m_filterView != view) { m_filterView = view; - clearSelection(); + m_selectionModel->clear(); invalidateFilter(); emit filterViewChanged(); } } bool FilteredMessageModel::hasSelection() { return m_selectionModel->hasSelection(); } -bool FilteredMessageModel::isSelected(int row) -{ - if (row < 0) { - return false; - } - - return m_selectionModel->isSelected(index(row, 0)); -} - -void FilteredMessageModel::setSelected(int row) -{ - if (row < 0) { - return; - } - - m_selectionModel->select(index(row, 0), QItemSelectionModel::Select); -} - -void FilteredMessageModel::toggleSelected(int row) +void FilteredMessageModel::clearAndSelect(const QVariantList &rows) { - if (row < 0) { - return; - } - - m_selectionModel->select(index(row, 0), QItemSelectionModel::Toggle); -} - -void FilteredMessageModel::setRangeSelected(int anchor, int to) -{ - if (anchor < 0 || to < 0) { - return; - } - - QItemSelection selection(index(anchor, 0), index(to, 0)); - m_selectionModel->select(selection, QItemSelectionModel::ClearAndSelect); -} - -void FilteredMessageModel::updateSelection(const QVariantList &rows, bool toggle) -{ - Q_UNUSED(toggle) - QItemSelection newSelection; int iRow = -1; foreach (const QVariant &row, rows) { iRow = row.toInt(); if (iRow < 0) { return; } const QModelIndex &idx = index(iRow, 0); newSelection.select(idx, idx); } m_selectionModel->select(newSelection, QItemSelectionModel::ClearAndSelect); } -void FilteredMessageModel::clearSelection() -{ - if (m_selectionModel->hasSelection()) { - m_selectionModel->clear(); - } -} - void FilteredMessageModel::selectionChanged(const QItemSelection &selected, const QItemSelection &deselected) { QModelIndexList indices = selected.indexes(); indices.append(deselected.indexes()); foreach(const QModelIndex index, indices) { emit dataChanged(index, index, QVector{MessageModel::Selected}); } + emit hasSelectionChanged(); + copySelectionToClipboard(QClipboard::Selection); } void FilteredMessageModel::copySelectionToClipboard(QClipboard::Mode mode) { if (!hasSelection()) { return; } QClipboard *clipboard = QGuiApplication::clipboard(); if (!clipboard) { return; } QStringList selected; foreach (const QModelIndex &index, m_selectionModel->selectedIndexes()) { selected.append(index.data(MessageModel::ClipboardSerialization).toString()); } clipboard->setText(selected.join('\n'), mode); } bool FilteredMessageModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const { Q_UNUSED(sourceParent) if (!m_filterView) { return false; } const QModelIndex &sourceIdx = sourceModel()->index(sourceRow, 0); const QObject *view = qvariant_cast(sourceIdx.data(MessageModel::View)); if (view && view == m_filterView) { return true; } return false; } QVariant FilteredMessageModel::data(const QModelIndex &index, int role) const { if (role == MessageModel::Selected) { return m_selectionModel->isSelected(index); } else if (role == MessageModel::AuthorMatchesPrecedingMessage) { const int precedingMessageRow = index.row() - 1; if (precedingMessageRow >= 0) { const QModelIndex &precedingMessage = QSortFilterProxyModel::index(precedingMessageRow, 0); return (index.data(MessageModel::Author) == precedingMessage.data(MessageModel::Author)); } return false; } else if (role == MessageModel::TimeStampMatchesPrecedingMessage) { const int precedingMessageRow = index.row() - 1; if (precedingMessageRow >= 0) { const QModelIndex &precedingMessage = QSortFilterProxyModel::index(precedingMessageRow, 0); return (index.data(MessageModel::TimeStamp) == precedingMessage.data(MessageModel::TimeStamp)); } return false; } return QSortFilterProxyModel::data(index, role); } MessageModel::MessageModel(QObject *parent) : QAbstractListModel(parent) , m_allocCount(0) { // Pre-allocate batch. m_messages.reserve(ALLOCATION_BATCH_SIZE); } MessageModel::~MessageModel() { } QHash MessageModel::roleNames() const { QHash roles = QAbstractItemModel::roleNames(); QMetaEnum e = metaObject()->enumerator(metaObject()->indexOfEnumerator("AdditionalRoles")); for (int i = 0; i < e.keyCount(); ++i) { roles.insert(e.value(i), e.key(i)); } return roles; } QVariant MessageModel::data(const QModelIndex &index, int role) const { if (!index.isValid() || index.row() >= m_messages.count()) { return QVariant(); } const Message &msg = m_messages.at(index.row()); if (role == Qt::DisplayRole) { - return msg.text; + return msg.formattedText; } else if (role == Type) { return (msg.action ? ActionMessage : NormalMessage); } else if (role == View) { return qVariantFromValue(msg.view); } else if (role == TimeStamp) { return msg.timeStamp; } else if (role == Author) { return msg.nick; } else if (role == NickColor) { return msg.nickColor; } else if (role == ClipboardSerialization) { return clipboardSerialization(msg); } return QVariant(); } int MessageModel::rowCount(const QModelIndex &parent) const { // Limit model to MAX_MESSAGES. return parent.isValid() ? 0 : qMax(MAX_MESSAGES, m_messages.count()); } void MessageModel::appendMessage(QObject *view, const QString &timeStamp, const QString &nick, const QColor &nickColor, const QString &text, + const QString &formattedText, const MessageType type) { beginInsertRows(QModelIndex(), m_messages.count(), m_messages.count()); Message msg; msg.view = view; msg.timeStamp = timeStamp; msg.nick = nick; msg.nickColor = nickColor; msg.text = text; + msg.formattedText = formattedText; msg.action = (type == ActionMessage); m_messages.append(msg); endInsertRows(); ++m_allocCount; // Grow the list in batches to make the insertion a little // faster. if (m_allocCount == ALLOCATION_BATCH_SIZE) { m_allocCount = 0; m_messages.reserve(m_messages.count() + ALLOCATION_BATCH_SIZE); } // Whenever we grow above MAX_MESSAGES_TOLERANCE, cull to // MAX_MESSAGES. I.e. we cull in batches, not on every new // message. if (m_messages.count() > MAX_MESSAGES_TOLERANCE) { m_messages = m_messages.mid(MAX_MESSAGES_TOLERANCE, MAX_MESSAGES); } } void MessageModel::cullMessages(const QObject *view) { int i = 0; while (i < m_messages.count()) { const Message &msg = m_messages.at(i); if (msg.view == view) { beginRemoveRows(QModelIndex(), i, i); m_messages.removeAt(i); endRemoveRows(); } else { ++i; } } } QString MessageModel::clipboardSerialization(const Message& msg) const { // WIPQTQUICK TODO: msg.text is preformatted HTML, we need the raw in the // model to derive this properly. + if (msg.action) { + return QString("[%1] * %2 %3").arg(msg.timeStamp).arg(msg.nick).arg(msg.text); + } + return QString("[%1] <%2> %3").arg(msg.timeStamp).arg(msg.nick).arg(msg.text); + } diff --git a/src/irc/messagemodel.h b/src/irc/messagemodel.h index 4e637e49..7450f16c 100644 --- a/src/irc/messagemodel.h +++ b/src/irc/messagemodel.h @@ -1,115 +1,117 @@ /* 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) 2017 Eike Hein */ #include "chatwindow.h" #include #include #include #include class QItemSelectionModel; struct Message { QObject *view; QString timeStamp; QString nick; QColor nickColor; QString text; + QString formattedText; bool action; }; class FilteredMessageModel : public QSortFilterProxyModel { Q_OBJECT Q_PROPERTY(QObject* filterView READ filterView WRITE setFilterView NOTIFY filterViewChanged) + Q_PROPERTY(bool hasSelection READ hasSelection NOTIFY hasSelectionChanged) public: explicit FilteredMessageModel(QObject *parent = 0); virtual ~FilteredMessageModel(); QObject *filterView() const; void setFilterView(QObject *view); - Q_INVOKABLE bool hasSelection(); - Q_INVOKABLE bool isSelected(int row); - Q_INVOKABLE void setSelected(int row); - Q_INVOKABLE void toggleSelected(int row); - Q_INVOKABLE void setRangeSelected(int anchor, int to); - Q_INVOKABLE void updateSelection(const QVariantList &rows, bool toggle); - Q_INVOKABLE void clearSelection(); + bool hasSelection(); + + Q_INVOKABLE void clearAndSelect(const QVariantList &rows); Q_INVOKABLE void copySelectionToClipboard(QClipboard::Mode mode = QClipboard::Clipboard); virtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + Q_INVOKABLE void bla(QObject *obj); + Q_SIGNALS: void filterViewChanged() const; + void hasSelectionChanged() const; protected: bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override; private Q_SLOTS: void selectionChanged(const QItemSelection &selected, const QItemSelection &deselected); private: QObject *m_filterView; QItemSelectionModel *m_selectionModel; }; class MessageModel : public QAbstractListModel { Q_OBJECT public: enum AdditionalRoles { Selected = Qt::UserRole + 1, // WIPQTQUICK TODO This is implemented by FMM, maybe I should extend roles there. Type, View, TimeStamp, TimeStampMatchesPrecedingMessage, // Implemented in FilteredMessageModel for search efficiency. Author, AuthorMatchesPrecedingMessage, // Implemented in FilteredMessageModel for search efficiency. NickColor, ClipboardSerialization }; Q_ENUM(AdditionalRoles) enum MessageType { NormalMessage = 0, ActionMessage }; Q_ENUM(MessageType) explicit MessageModel(QObject *parent = 0); virtual ~MessageModel(); QHash roleNames() const override; virtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; int rowCount(const QModelIndex &parent = QModelIndex()) const override; Q_INVOKABLE void appendMessage(QObject *view, const QString &timeStamp, const QString &nick, const QColor &nickColor, const QString &text, + const QString &formattedText, const MessageType type = NormalMessage); void cullMessages(const QObject *view); private: QString clipboardSerialization(const Message &msg) const; QVector m_messages; int m_allocCount; }; diff --git a/src/qtquick/uicomponents/HorizontalDragHandle.qml b/src/qtquick/uicomponents/HorizontalDragHandle.qml index 8d85be0e..23c9f7ea 100644 --- a/src/qtquick/uicomponents/HorizontalDragHandle.qml +++ b/src/qtquick/uicomponents/HorizontalDragHandle.qml @@ -1,77 +1,79 @@ /* 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) 2017 Eike Hein */ import QtQuick 2.7 import org.kde.kirigami 2.1 as Kirigami MouseArea { id: dragHandle anchors.top: parent.top anchors.bottom: parent.bottom width: Kirigami.Units.devicePixelRatio * 2 property var target: null property int defaultWidth: -1 property int dragRange: (Kirigami.Units.gridUnit * 5) property bool _configured: target != null && defaultWidth != -1 property int _pressX: -1 property int _pressTargetWidth: -1 signal newWidth(int width) cursorShape: Qt.SplitHCursor onPressed: { if (!_configured) { return; } _pressX = mapToGlobal(mouse.x, 0).x; _pressTargetWidth = target.width; } onPressedChanged: { + // Runtime search for the 'interactive' property because + // Kirigami currently imports old QQC2. if (target && "interactive" in target) { target.interactive = !pressed; } } onPositionChanged: { if (!_configured) { return; } var mappedX = mapToGlobal(mouse.x, 0).x; if ("edge" in target && target.edge == Qt.RightEdge) { if (mappedX > _pressX) { target.width = Math.max((defaultWidth - dragRange), _pressTargetWidth - (mappedX - _pressX)); } else if (mappedX < _pressX) { target.width = Math.min((defaultWidth + dragRange), _pressTargetWidth + (_pressX - mappedX)); } } else { if (mappedX > _pressX) { dragHandle.newWidth(Math.min((defaultWidth + dragRange), _pressTargetWidth + (mappedX - _pressX))); } else if (mappedX < _pressX) { dragHandle.newWidth(Math.max((defaultWidth - dragRange), _pressTargetWidth - (_pressX - mappedX))); } } } } diff --git a/src/qtquick/uicomponents/Page.qml b/src/qtquick/uicomponents/Page.qml new file mode 100644 index 00000000..93a90b3d --- /dev/null +++ b/src/qtquick/uicomponents/Page.qml @@ -0,0 +1,33 @@ +/* + 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) 2017 Eike Hein +*/ + +import QtQuick 2.7 + +import org.kde.kirigami 2.1 as Kirigami + +Kirigami.Page { + Keys.onPressed: { + if (event.key == Qt.Key_Shift) { + konvUi.shiftPressed = true; + // WIPQTQUICK TODO Evaluating text is not good enough, needs real key event fwd + // to make things like deadkeys work + } else if (konvUi.inputField && !konvUi.inputField.activeFocus && event.text != "") { + event.accept = true; + inputField.textForward(event.text); + } + } + + Keys.onReleased: { + if (event.key == Qt.Key_Shift) { + konvUi.shiftPressed = false; + } + } +} diff --git a/src/qtquick/uicomponents/qmldir b/src/qtquick/uicomponents/qmldir index dade0f1b..cace2027 100644 --- a/src/qtquick/uicomponents/qmldir +++ b/src/qtquick/uicomponents/qmldir @@ -1,4 +1,5 @@ module org.kde.konversation.uicomponents -HorizontalDragHandle 1.0 HorizontalDragHandle.qml singleton ExtraColors 1.0 ExtraColors.qml +HorizontalDragHandle 1.0 HorizontalDragHandle.qml +Page 1.0 Page.qml diff --git a/src/qtquick/uipackages/default/contents/TextView.qml b/src/qtquick/uipackages/default/contents/TextView.qml index 8f5f193b..5ad7181c 100644 --- a/src/qtquick/uipackages/default/contents/TextView.qml +++ b/src/qtquick/uipackages/default/contents/TextView.qml @@ -1,298 +1,704 @@ /* 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) 2017 Eike Hein */ import QtQuick 2.7 import QtQuick.Controls 2.2 as QQC2 import org.kde.kirigami 2.1 as Kirigami +import org.kde.kquickcontrolsaddons 2.0 as KQuickControlsAddons import org.kde.konversation 1.0 as Konversation import org.kde.konversation.uicomponents 1.0 as KUIC Item { id: textView QQC2.ScrollView { anchors.fill: parent ListView { id: textListView anchors.bottom: parent.bottom width: parent.width height: parent.height visible: !konvUi.settingsMode QQC2.ScrollBar.vertical: QQC2.ScrollBar {} + property bool scrollUp: false + property bool scrollDown: false + readonly property int msgWidth: width - QQC2.ScrollBar.vertical.width model: messageModel delegate: msgComponent function scrollToEnd() { + if (messageModel.hasSelection + || (mouseOverlay.inlineSelectionItem + && mouseOverlay.inlineSelectionItem.hasSelectedText)) { + return; + } + var newIndex = (count - 1); positionViewAtEnd(); currentIndex = newIndex; } + function cancelAutoScroll() { + scrollUp = false; + scrollDown = false; + } + + onContentYChanged: { + if (contentY == 0) { + scrollUp = false; + } + + if (contentY == contentItem.height - height) { + scrollDown = false; + } + } + + onScrollUpChanged: { + if (scrollUp && visibleArea.heightRatio < 1.0) { + smoothY.enabled = true; + contentY = 0; + } else { + contentY = contentY; + smoothY.enabled = false; + } + } + + onScrollDownChanged: { + if (scrollDown && visibleArea.heightRatio < 1.0) { + smoothY.enabled = true; + contentY = contentItem.height - height; + } else { + contentY = contentY; + smoothY.enabled = false; + } + } + + Behavior on contentX { id: smoothX; enabled: false; SmoothedAnimation { velocity: 500 } } + Behavior on contentY { id: smoothY; enabled: false; SmoothedAnimation { velocity: 500 } } + Connections { target: textListView.contentItem onHeightChanged: { if (textListView.contentItem.height <= textView.height) { textListView.height = textListView.contentItem.height; } else { textListView.height = textView.height; } } } Connections { target: messageModel onRowsInserted: scrollDownTimer.restart() onRowsRemoved: scrollDownTimer.restart() onModelReset: scrollDownTimer.restart() } Timer { id: scrollDownTimer interval: 0 repeat: false onTriggered: textListView.scrollToEnd() } Component { id: msgComponent Loader { id: msg width: ListView.view.msgWidth height: (active ? (Math.max(avatarSize, konvUi.largerFontSize + messageText.height + Kirigami.Units.gridUnit)) : messageText.height) property int row: index readonly property int avatarSize: konvUi.largerFontSize * 3.3 property var authorSize: Qt.point(0, 0) + property int contentWidth: { + var width = Math.ceil(Kirigami.Units.devicePixelRatio); + + if (timeStamp) { + width += Math.max(timeStamp.x + timeStamp.width, + messageText.x + messageText.contentWidth); + } else { + width += Math.max(messageText.x + messageText.contentWidth, + avatarSize + Kirigami.Units.gridUnit + authorSize.x); + } + + return Math.min(textView.width, width); + } + property bool selected: model.Selected === true + property bool hasSelectedText: (inlineSelectionTextItem + && inlineSelectionTextItem.selectedText.length) + + property bool allowInlineSelection: (mouseOverlay.inlineSelectionItem == msg + && !mouseOverlay.tapSelecting) readonly property bool showTimeStamp: !model.TimeStampMatchesPrecedingMessage - property Item timeStamp: null + property Item timeStamp: null property Item messageTextArea: messageText - property Item selectedBackgroundItem: null + property Item inlineSelectionTextItem: null active: !model.AuthorMatchesPrecedingMessage sourceComponent: metabitsComponent onSelectedChanged: { if (selected && !selectedBackgroundItem) { selectedBackgroundItem = selectedBackgroundItemComponent.createObject(msg); } else if (!selected && selectedBackgroundItem) { selectedBackgroundItem.destroy(); + selectedBackgroundItem = null; } } onShowTimeStampChanged: { if (!showTimeStamp) { if (timeStamp) { timeStamp.destroy(); + timeStamp = null; } } else { timeStamp = timeStampComponent.createObject(msg); } } + onAllowInlineSelectionChanged: { + if (allowInlineSelection && !inlineSelectionTextItem) { + inlineSelectionTextItem = inlineSelectionTextItemComponent.createObject(msg); + } else if (!allowInlineSelection + && inlineSelectionTextItem + && !inlineSelectionTextItem.selectedText.length) { + inlineSelectionTextItem.destroy(); + inlineSelectionTextItem = null; + } + } + Component { id: selectedBackgroundItemComponent Rectangle { - anchors.fill: parent + anchors.top: parent.top anchors.topMargin: msg.active ? (Kirigami.Units.gridUnit / 2) : 0 + x: messageText.x + + height: (messageText.y + messageText.contentHeight) - anchors.topMargin + width: msg.contentWidth - x + z: 0 color: Kirigami.Theme.highlightColor } } Component { id: timeStampComponent Text { id: timeStamp - z: 1 + z: 2 readonly property bool collides: (messageText.x + messageText.implicitWidth + Kirigami.Units.smallSpacing + width > parent.width) readonly property int margin: Kirigami.Units.gridUnit / 2 x: messageText.x + margin + (active ? authorSize.x : messageText.contentWidth) y: { if (!active) { return messageText.y + ((largerFontMetrics.height / 2) - (height / 2)); } else { return (Kirigami.Units.gridUnit / 2) + ((authorSize.y / 2) - (height / 2)); } } renderType: Text.NativeRendering - color: Kirigami.Theme.disabledTextColor + color: selected ? Kirigami.Theme.highlightedTextColor : Kirigami.Theme.disabledTextColor text: model.TimeStamp } } Component { id: metabitsComponent Item { anchors.fill: parent z: 1 Rectangle { id: avatar x: Kirigami.Units.gridUnit / 2 y: Kirigami.Units.gridUnit / 2 width: avatarSize height: avatarSize color: model.NickColor radius: width * 0.5 Text { anchors.fill: parent anchors.margins: Kirigami.Units.devicePixelRatio * 5 renderType: Text.QtRendering color: "white" font.weight: Font.Bold font.pointSize: 100 minimumPointSize: theme.defaultFont.pointSize fontSizeMode: Text.Fit horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter text: { // WIPQTQUICK HACK TODO Probably doesn't work with non-latin1. var match = model.Author.match(/([a-zA-Z])([a-zA-Z])/); var abbrev = match[1].toUpperCase(); if (match.length > 2) { abbrev += match[2].toLowerCase(); } return abbrev; } } } Text { id: author y: Kirigami.Units.gridUnit / 2 anchors.left: parent.left anchors.leftMargin: avatarSize + Kirigami.Units.gridUnit renderType: Text.NativeRendering - color: model.NickColor + color: selected ? Kirigami.Theme.highlightedTextColor : model.NickColor font.weight: Font.Bold font.pixelSize: konvUi.largerFontSize text: model.Author onWidthChanged: msg.authorSize = Qt.point(width, height) } } } + Component { + id: inlineSelectionTextItemComponent + + Item { + id: inlineSelectionText + + anchors.fill: messageText + + z: 1 + + property Item textArea: textArea + property alias selectedText: textArea.selectedText + + Connections { + target: mouseOverlay + + onClearInlineSelectedText: { + inlineSelectionText.destroy(); + msg.inlineSelectionTextItem = null; + } + + onTapSelectingChanged: { + if (!mouseOverlay.tapSelecting) { + inlineSelectionText.destroy(); + msg.inlineSelectionTextItem = null; + } + } + } + + Connections { + target: mouseOverlay.inlineSelectionItem + + enabled: mouseOverlay.inlineSelectionItem != msg + + onHasSelectedTextChanged: { + inlineSelectionText.destroy(); + msg.inlineSelectionTextItem = null; + } + } + + QQC2.TextArea { + id: textArea + + anchors.fill: parent + + // Init from messageText. + renderType: messageText.renderType + textFormat: Text.RichText + font: messageText.font + wrapMode: messageText.wrapMode + color: messageText.color + text: messageText.text + + leftPadding: 0 + rightPadding: 0 + topPadding: 0 + bottomPadding: 0 + + readOnly: true + + selectByMouse: true + persistentSelection: true + + onSelectedTextChanged: { + if (!selectedText.length + && !msg.allowInlineSelection) { + inlineSelectionText.destroy(); + msg.inlineSelectionTextItem = null; + } + } + + Component.onCompleted: { + forceActiveFocus(); + } + } + } + } + Text { id: messageText + opacity: allowInlineSelection ? 0.0 : 1.0 + z: 1 anchors.left: parent.left anchors.leftMargin: avatarSize + Kirigami.Units.gridUnit anchors.right: parent.right anchors.rightMargin: (timeStamp && timeStamp.collides ? timeStamp.margin + timeStamp.width : 0) anchors.bottom: parent.bottom renderType: Text.NativeRendering textFormat: Text.StyledText font.pixelSize: konvUi.largerFontSize wrapMode: Text.Wrap color: selected ? Kirigami.Theme.highlightedTextColor : Kirigami.Theme.textColor text: (model.Type == Konversation.MessageModel.ActionMessage ? actionWrap(model.display) : model.display) function actionWrap(text) { return "" + model.Author + " " + text + ""; } - - onLinkActivated: konvApp.openUrl(link) } } } Keys.onPressed: { if (event.matches(StandardKey.Copy) || event.matches(StandardKey.Cut)) { event.accepted = true; messageModel.copySelectionToClipboard(); } } } } MouseArea { + id: mouseOverlay + anchors.fill: parent - onClicked: { + property var rowsToSelect: [] + property int pressedRow: -1 + property bool tapSelecting: false + + property Item inlineSelectionItem: null + + property string hoveredLink: "" + + signal clearInlineSelectedText + + hoverEnabled: true + + function itemAt(x, y) { + var cPos = mapToItem(textListView.contentItem, x, y); + return textListView.itemAt(cPos.x, cPos.y); + } + + function toggleSelected(row) { + var index = rowsToSelect.indexOf(row); + + if (index != -1) { + var selection = rowsToSelect; + selection.splice(index, 1); + rowsToSelect = selection; + } else { + var selection = rowsToSelect; + selection.push(row); + selection.sort(); + rowsToSelect = selection; + } + } + + function processClick(x, y) { + var item = itemAt(x, y); + + // Adding a gridUnit for bigger finger targets. + if (item + && x >= item.messageTextArea.x + && x <= (item.contentWidth + Kirigami.Units.gridUnit)) { + + if (rowsToSelect.length && konvUi.shiftPressed) { + var start = Math.min(rowsToSelect[0], item.row); + var end = Math.max(rowsToSelect[0], item.row); + var selection = []; + + for (var i = start; i <= end; ++i) { + selection.push(i); + } + + rowsToSelect = selection; + } else { + toggleSelected(item.row); + } + } else { + rowsToSelect = []; + } + } + + onRowsToSelectChanged: messageModel.clearAndSelect(rowsToSelect) + + onContainsMouseChanged: { + if (!containsMouse) { + pressAndHoldTimer.stop(); + hoveredLink = ""; + + if (inlineSelectionItem + && !inlineSelectionItem.hasSelectedText) { + inlineSelectionItem = null; + } + } + } + + onPositionChanged: { mouse.accepted = false; - var cPos = mapToItem(textListView.contentItem, mouse.x, mouse.y); - var item = textListView.itemAt(cPos.x, cPos.y); + var item = itemAt(mouse.x, mouse.y); if (item) { - messageModel.toggleSelected(item.row); + if (pressedRow != -1) { + // Trigger auto-scroll. + // WIPQTQUICK TODO: The selection should be updated even without + // pointer movement while autoscrolling. + textListView.scrollUp = (mouse.y <= 0 && textListView.contentY > 0); + textListView.scrollDown = (mouse.y >= textListView.height + && textListView.contentY < textListView.contentItem.height - textListView.height); + + if (item.row != pressedRow && pressed) { + var start = Math.min(pressedRow, item.row); + var end = Math.max(pressedRow, item.row); + var selection = []; + + for (var i = start; i <= end; ++i) { + selection.push(i); + } + + rowsToSelect = selection; + + pressAndHoldTimer.stop(); + } else if (rowsToSelect.length) { + rowsToSelect = [item.row]; + } + } + + var messageTextPos = mapToItem(item.messageTextArea, mouse.x, mouse.y); + hoveredLink = item.messageTextArea.linkAt(messageTextPos.x, messageTextPos.y); + + if (hoveredLink) { + cursorShape = Qt.PointingHandCursor; + + return; + } + + if (mouse.x >= item.messageTextArea.x && mouse.x <= item.contentWidth) { + cursorShape = Qt.IBeamCursor; + + if (!messageModel.hasSelection && !tapSelecting) { + inlineSelectionItem = item; + + eventGenerator.sendMouseEvent(inlineSelectionItem.inlineSelectionTextItem.textArea, + KQuickControlsAddons.EventGenerator.MouseMove, + messageTextPos.x, + messageTextPos.y, + Qt.LeftButton, + Qt.LeftButton, + 0); + } + + return; + } + } else { + pressAndHoldTimer.stop(); + + if (inlineSelectionItem && !inlineSelectionItem.hasSelectedText) { + inlineSelectionItem = null; + } + + if (pressedRow != -1) { + if (mouse.y < 0) { + var topRow = itemAt(0, 0).row; + var start = Math.min(pressedRow, topRow); + var end = Math.max(pressedRow, topRow); + var selection = []; + + for (var i = start; i <= end; ++i) { + selection.push(i); + } + + rowsToSelect = selection; + } else if (mouse.y > height) { + var bottomRow = itemAt(0, height - 1).row; + var start = Math.min(pressedRow, bottomRow); + var end = Math.max(pressedRow, bottomRow); + var selection = []; + + for (var i = start; i <= end; ++i) { + selection.push(i); + } + + rowsToSelect = selection; + } + } } - if (messageModel.hasSelection()) { - textListView.forceActiveFocus(); + cursorShape = Qt.ArrowCursor; + } + + onPressed: { + mouse.accepted = true; + + textListView.forceActiveFocus(); + + if (tapSelecting) { + processClick(mouse.x, mouse.y); + } else { + rowsToSelect = []; + + var item = itemAt(mouse.x, mouse.y); + + if (item && cursorShape == Qt.IBeamCursor) { + pressedRow = item.row; + + if (inlineSelectionItem) { + var mPos = mapToItem(inlineSelectionItem.inlineSelectionTextItem.textArea, + mouse.x, + mouse.y); + eventGenerator.sendMouseEvent(inlineSelectionItem.inlineSelectionTextItem.textArea, + KQuickControlsAddons.EventGenerator.MouseButtonPress, + mPos.x, + mPos.y, + Qt.LeftButton, + Qt.LeftButton, + 0); + + if (!inlineSelectionItem.hasSelectedText) { + pressAndHoldTimer.restart(); + } + } + } else { + pressedRow = -1; + inlineSelectionItem = null; + clearInlineSelectedText(); + pressAndHoldTimer.stop(); + } } } + + onReleased: { + pressedRow = -1; + pressAndHoldTimer.stop(); + textListView.cancelAutoScroll(); + } + + onClicked: { + if (hoveredLink) { + konvApp.openUrl(hoveredLink) + } + } + + Connections { + target: messageModel + + onHasSelectionChanged: { + if (!messageModel.hasSelection) { + mouseOverlay.tapSelecting = false; + } else { + mouseOverlay.inlineSelectionItem = null; + mouseOverlay.clearInlineSelectedText(); + } + } + } + + Timer { + id: pressAndHoldTimer + + interval: mouseOverlay.pressAndHoldInterval + repeat: false + + onTriggered: { + if (mouseOverlay.inlineSelectionItem + && mouseOverlay.inlineSelectionItem.hasSelectedText) { + return; + } + + if (messageModel.hasSelection) { + return; + } + + mouseOverlay.tapSelecting = true; + mouseOverlay.processClick(mouseOverlay.mouseX, mouseOverlay.mouseY); + } + } + + KQuickControlsAddons.EventGenerator { + id: eventGenerator + } } } diff --git a/src/qtquick/uipackages/default/contents/main.qml b/src/qtquick/uipackages/default/contents/main.qml index ca48af7c..11d013d3 100644 --- a/src/qtquick/uipackages/default/contents/main.qml +++ b/src/qtquick/uipackages/default/contents/main.qml @@ -1,713 +1,715 @@ /* 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) 2017 Eike Hein */ import QtQuick 2.7 import QtQml.Models 2.2 import QtQuick.Controls 1.4 as QQC1 import QtQuick.Controls 2.2 as QQC2 import org.kde.kirigami 2.1 as Kirigami import org.kde.konversation.uicomponents 1.0 as KUIC Kirigami.ApplicationWindow { id: konvUi property int defaultSidebarWidth: Kirigami.Units.gridUnit * 11 property int defaultContextDrawerWidth: Kirigami.Units.gridUnit * 17 property int sidebarWidth: defaultSidebarWidth property int largerFontSize: Kirigami.Theme.defaultFont.pixelSize * 1.1 property int footerHeight: largerFontSize + (Kirigami.Units.smallSpacing * 6) property Item sidebarStackView: null property Item contentStackView: null property Item contentFooterStackView: null property Item inputField: null property bool settingsMode: false property Item settingsModeButtons: null + property bool shiftPressed: false + signal openLegacyConfigDialog signal showMenuBar(bool show) pageStack.defaultColumnWidth: sidebarWidth pageStack.initialPage: [sidebarComponent, contentComponent] pageStack.separatorVisible: false TextMetrics { id: largerFontMetrics font.pixelSize: largerFontSize text: "M" } contextDrawer: Kirigami.OverlayDrawer { id: contextDrawer width: defaultContextDrawerWidth edge: Qt.RightEdge modal: true handleVisible: drawerOpen drawerOpen: false background: Rectangle { color: Kirigami.Theme.viewBackgroundColor } onDrawerOpenChanged: { if (drawerOpen) { userList.forceActiveFocus(); userList.currentIndex = -1; } } leftPadding: 0 rightPadding: 0 topPadding: 0 bottomPadding: 0 Rectangle { id: topicArea visible: viewModel.currentView && viewModel.currentView.description != "" anchors.top: parent.top anchors.left: parent.left anchors.right: parent.right height: visible ? channelName.height + topic.contentHeight + (Kirigami.Units.smallSpacing * 4): 0 color: KUIC.ExtraColors.spotColor Kirigami.Heading { id: channelName x: (Kirigami.Units.smallSpacing * 2) level: 2 text: viewModel.currentView ? viewModel.currentView.name : "" color: KUIC.ExtraColors.alternateSpotTextColor opacity: 1.0 // Override } Text { id: topic x: (Kirigami.Units.smallSpacing * 2) y: channelName.height + (Kirigami.Units.smallSpacing * 2) width: parent.width - (Kirigami.Units.smallSpacing * 4) text: viewModel.currentView ? viewModel.currentView.description : "" textFormat: Text.StyledText font.pixelSize: largerFontSize color: KUIC.ExtraColors.spotTextColor wrapMode: Text.WordWrap onLinkActivated: Qt.openUrlExternally(link) } } MouseArea { anchors.fill: parent onClicked: userList.forceActiveFocus() } QQC2.ScrollView { anchors.top: topicArea.bottom anchors.left: parent.left anchors.right: parent.right anchors.bottom: parent.bottom ListView { id: userList visible: viewModel.currentView && "userModel" in viewModel.currentView onHeightChanged: { if (currentIndex != -1) { positionViewAtIndex(currentIndex, ListView.Contain); } } clip: true currentIndex: -1 onCurrentIndexChanged: positionViewAtIndex(currentIndex, ListView.Contain) model: visible ? viewModel.currentView.userModel : null onModelChanged: currentIndex = -1 delegate: ListItem { width: userList.width text: model.display textMargin: Kirigami.Units.gridUnit function openQuery() { viewModel.currentServer.addQuery(model.display); contextDrawer.close(); } onClicked: { userList.forceActiveFocus(); userList.currentIndex = index; } onDoubleClicked: openQuery(); Keys.onEnterPressed: { event.accept = true; openQuery(); } Keys.onReturnPressed: { event.accept = true; openQuery(); } } Keys.onUpPressed: { event.accept = true; if (currentIndex == -1) { currentIndex = 0; return; } decrementCurrentIndex(); } Keys.onDownPressed: { event.accept = true; if (currentIndex == -1) { currentIndex = 0; return; } incrementCurrentIndex(); } Keys.onPressed: { // WIPQTQUICK TODO Evaluating text is not good enough, needs real key event fwd // to make things like deadkeys work if (event.text != "" && inputField && !inputField.activeFocus) { contextDrawer.close(); event.accept = true; inputField.textForward(event.text); } } } } KUIC.HorizontalDragHandle { id: contextDrawerDragHandle anchors.left: parent.left target: contextDrawer defaultWidth: defaultContextDrawerWidth onNewWidth: contextDrawer.width = width } } KUIC.HorizontalDragHandle { id: sidebarDragHandle visible: pageStack.wideMode x: sidebarWidth - (width / 2) target: sidebarStackView defaultWidth: defaultSidebarWidth onNewWidth: konvUi.sidebarWidth = width } Component { id: sidebarComponent - Kirigami.Page { + KUIC.Page { id: sidebar leftPadding: 0 rightPadding: 0 topPadding: 0 bottomPadding: 0 property Item viewTreeList: null MouseArea { anchors.fill: parent onClicked: { if (viewTreeList) { viewTreeList.forceActiveFocus(); } } } QQC2.StackView { id: sidebarStackView anchors.fill: parent background: Rectangle { color: KUIC.ExtraColors.spotColor } initialItem: viewTreeComponent onBusyChanged: { if (!busy && depth == 2) { currentItem.currentIndex = 0; currentItem.forceActiveFocus(); } } pushEnter: Transition { YAnimator { from: sidebarStackView.height to: 0 duration: Kirigami.Units.longDuration * 2 easing.type: Easing.OutCubic } } pushExit: Transition { OpacityAnimator { from: 1.0 to: 0.0 duration: Kirigami.Units.longDuration * 2 } } popEnter: Transition { OpacityAnimator { from: 0.0 to: 1.0 duration: Kirigami.Units.longDuration * 2 } } popExit: Transition { YAnimator { from: 0 to: sidebarStackView.height duration: Kirigami.Units.longDuration * 2 easing.type: Easing.OutCubic } } Component.onCompleted: konvUi.sidebarStackView = sidebarStackView } Component { id: viewTreeComponent QQC2.ScrollView { ListView { id: viewTreeList clip: true model: viewModel function showView(index, view) { viewTreeList.forceActiveFocus(); viewModel.showView(view); if (!konvUi.pageStack.wideMode) { konvUi.pageStack.currentIndex = 1; } } delegate: Column { property int topLevelIndex: index ListItem { width: viewTreeList.width textColor: KUIC.ExtraColors.spotTextColor backgroundColor: KUIC.ExtraColors.spotColor text: model.display textMargin: Kirigami.Units.gridUnit onClicked: viewTreeList.showView(topLevelIndex, value) } DelegateModel { id: subLevelEntries model: viewModel rootIndex: modelIndex(index) delegate: ListItem { width: viewTreeList.width textColor: KUIC.ExtraColors.spotTextColor backgroundColor: KUIC.ExtraColors.spotColor text: model.display textMargin: Kirigami.Units.gridUnit * 2 onClicked: viewTreeList.showView(topLevelIndex, value) } } Column { Repeater { model: subLevelEntries } } } Keys.onUpPressed: { event.accept = true; viewModel.showPreviousView(); } Keys.onDownPressed: { event.accept = true; viewModel.showNextView(); } Component.onCompleted: sidebar.viewTreeList = viewTreeList } } } Component { id: settingsTreeComponent QQC2.ScrollView { id: viewTree property alias currentIndex: settingsTreeList.currentIndex ListView { id: settingsTreeList focus: true clip: true currentIndex: -1 onCurrentIndexChanged: positionViewAtIndex(currentIndex, ListView.Contain) model: ListModel { ListElement { name: "Dummy 1" } ListElement { name: "Dummy 2" } ListElement { name: "Dummy 3" } } delegate: ListItem { width: settingsTreeList.width textColor: KUIC.ExtraColors.spotTextColor backgroundColor: KUIC.ExtraColors.spotColor text: name textMargin: Kirigami.Units.gridUnit onIsActiveChanged: { if (isActive && konvUi.contentStackView.depth == 1) { konvUi.contentStackView.push("SettingsPage.qml", {"title": name}); //konvUi.settingsModeButtons.enabled = true; } } onClicked: { settingsTreeList.forceActiveFocus(); settingsTreeList.currentIndex = index; } } Keys.onUpPressed: { event.accept = true; if (currentIndex == -1) { currentIndex = 0; return; } decrementCurrentIndex(); } Keys.onDownPressed: { event.accept = true; if (currentIndex == -1) { currentIndex = 0; return; } incrementCurrentIndex(); } } } } PageHandle { id: sidebarRightPaginationHandle anchors.right: parent.right visible: !konvUi.pageStack.wideMode iconName: "go-previous" iconSelected: true color: KUIC.ExtraColors.alternateSpotColor onTriggered: pageStack.currentIndex = 1 } footer: Rectangle { id: sidebarFooter width: parent.width height: footerHeight color: KUIC.ExtraColors.alternateSpotColor Rectangle { id: settingsModeToggleButton width: sidebarFooter.height height: width property bool checked: false color: checked ? Kirigami.Theme.highlightColor: KUIC.ExtraColors.alternateSpotColor onCheckedChanged: { konvUi.settingsMode = checked; if (checked) { sidebarStackView.push(settingsTreeComponent); konvUi.contentFooterStackView.push("SettingsModeButtons.qml", {"enabled": false}); } else { sidebarStackView.pop(); if (konvUi.contentStackView.depth == 2) { konvUi.contentStackView.pop(); konvUi.contentFooterStackView.pop(); } konvUi.showMenuBar(false); inputField.forceActiveFocus(); } } Kirigami.Icon { anchors.centerIn: parent width: parent.width - (Kirigami.Units.smallSpacing * 4) height: width selected: true source: "application-menu" } MouseArea { anchors.fill: parent onClicked: parent.checked = !parent.checked } } QQC1.ComboBox { anchors.fill: sidebarFooter anchors.leftMargin: settingsModeToggleButton.width visible: viewModel.currentServer editable: true model: viewModel.currentServer ? [viewModel.currentServer.nickname] : [] onAccepted: { return; // WIPQTQUICK TODO Server::setNickname does something weird if (viewModel.currentServer) { viewModel.currentServer.setNickname(currentText); } } } } Keys.onPressed: { // WIPQTQUICK TODO Evaluating text is not good enough, needs real key event fwd // to make things like deadkeys work if (event.text != "" && inputField && !inputField.activeFocus) { event.accept = true; inputField.textForward(event.text); } } } } Component { id: contentComponent - Kirigami.Page { + KUIC.Page { leftPadding: 0 rightPadding: 0 topPadding: 0 bottomPadding: 0 onWidthChanged: { konvUi.pageStack.currentIndex = 1; } Rectangle { anchors.fill: parent color: Kirigami.Theme.viewBackgroundColor } QQC2.StackView { id: contentStackView anchors.fill: parent anchors.bottomMargin: Kirigami.Units.smallSpacing pushEnter: null pushExit: null popEnter: null popExit: null Component.onCompleted: { contentStackView.push("TextView.qml"); konvUi.contentStackView = contentStackView; } } footer: QQC2.StackView { id: contentFooterStackView height: footerHeight background: Rectangle { color: Qt.darker(Kirigami.Theme.viewBackgroundColor, 1.02) } initialItem: inputFieldComponent pushEnter: Transition { XAnimator { from: contentFooterStackView.width to: 0 duration: Kirigami.Units.longDuration * 2 easing.type: Easing.OutCubic } } pushExit: Transition { OpacityAnimator { from: 1.0 to: 0.0 duration: Kirigami.Units.longDuration * 2 } } popEnter: Transition { OpacityAnimator { from: 0.0 to: 1.0 duration: Kirigami.Units.longDuration * 2 } } popExit: Transition { XAnimator { from: 0 to: contentFooterStackView.width duration: Kirigami.Units.longDuration * 2 easing.type: Easing.OutCubic } } Component { id: inputFieldComponent QQC2.TextField { id: inputField background: null enabled: viewModel.currentView renderType: Text.NativeRendering font.pixelSize: largerFontSize verticalAlignment: Text.AlignVCenter wrapMode: TextEdit.NoWrap Keys.onPressed: { // WIPQTQUICK TODO Evaluating text is not good enough, needs real key event fwd // to make things like deadkeys work if (text != "" && (event.key == Qt.Key_Enter || event.key == Qt.Key_Return)) { event.accepted = true; viewModel.currentView.sendText(text); text = ""; } } function textForward(text) { forceActiveFocus(); insert(length, text); cursorPosition = length; } Component.onCompleted: { konvUi.inputField = inputField; forceActiveFocus(); } } } Component.onCompleted: konvUi.contentFooterStackView = contentFooterStackView } PageHandle { id: contentLeftPaginationHandle anchors.left: parent.left visible: !pageStack.wideMode iconName: "go-next" onTriggered: pageStack.currentIndex = 0 } PageHandle { id: contentRightPaginationHandle anchors.right: parent.right visible: (!konvUi.settingsMode && viewModel.currentView && !contextDrawer.drawerOpen) iconName: "go-previous" onTriggered: contextDrawer.drawerOpen ? contextDrawer.close() : contextDrawer.open() } } } } diff --git a/src/viewer/ircview.cpp b/src/viewer/ircview.cpp index 3f66aac6..6b337f87 100644 --- a/src/viewer/ircview.cpp +++ b/src/viewer/ircview.cpp @@ -1,2335 +1,2339 @@ // -*- 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 "messagemodel.h" // WIPQTQUICK #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 : 0; } ~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(0), m_lastMarkerLine(0), m_rememberLineDirtyBit(false), markerFormatObject(this) { m_mousePressedOnUrl = false; m_isOnNick = false; m_isOnChannel = false; m_chatWin = 0; m_server = 0; setAcceptDrops(false); // Marker lines connect(document(), SIGNAL(contentsChange(int,int,int)), SLOT(cullMarkedLine(int,int,int))); //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); connect(this, SIGNAL(anchorClicked(QUrl)), this, SLOT(anchorClicked(QUrl))); connect( this, SIGNAL(highlighted(QString)), this, SLOT(highlightedSlot(QString)) ); setOpenLinks(false); setUndoRedoEnabled(0); document()->setDefaultStyleSheet("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; newFont.setPointSize(font().pointSize() + 1); setFont(newFont); } void IRCView::decreaseFontSize() { QFont newFont; newFont.setPointSize(font().pointSize() - 1); setFont(newFont); } void IRCView::resetFontSize() { setFont(Preferences::self()->textFont()); } 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() << QString::fromLatin1("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, 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, QTextBlock b, int objFormat) : m_block(b), m_format(objFormat), m_prev(prev), m_next(0), 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; 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 = 0; } 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 != 0) { // this probably means we had a block containing only 0x2029, so Scribe merged the userData/userState into the next m_rememberLine = 0; } } m_rememberLine = appendLine(IRCView::RememberLine); } void IRCView::voidLineBlock(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 = 0; } bool IRCView::hasLines() { return m_lastMarkerLine != 0; } 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); block.setUserData(b); m_lastMarkerLine = b; //TODO figure out what this is for cursor.setPosition(block.position()); return b; } // Other stuff void IRCView::updateAppearance() { if (Preferences::self()->customTextFont()) setFont(Preferences::self()->textFont()); else setFont(QFontDatabase::systemFont(QFontDatabase::GeneralFont)); 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); } // 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); QChar directionOfLine = rtl ? RLM : LRM; line = directionOfLine; if (!label.isEmpty()) { line += "[%4]"; } line += "%1" + directionOfLine + nickLine + directionOfLine + " %3"; line = line.arg(timeStamp(messageTags), nick, text); if (!label.isEmpty()) { line = line.arg(label); } emit textToLog(QString("<%1>\t%2").arg(nick, message)); /* BEGIN: WIPQTQUICK */ Application* konvApp = Application::instance(); MessageModel* msgModel = konvApp->getMainWindow()->getMessageModel(); QDateTime serverTime; if (messageTags.contains(QStringLiteral("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(); msgModel->appendMessage(m_chatWin, time.toString(Preferences::self()->timestampFormat()), nick, nick != m_server->getNickname() ? Preferences::self()->nickColor(m_server->obtainNickInfo(nick)->getNickColor()) : Preferences::self()->nickColor(8), + Konversation::removeIrcMarkup(message), text ); /* END: WIPQTQUICK */ 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; QString line = QString(timeStamp(QHash()) + " " + message + ""); doAppend(line, false, self); } void IRCView::appendLog(const QString & message) { QColor channelColor = Preferences::self()->color(Preferences::ChannelMessage); m_tabNotification = Konversation::tnfNone; QString line("" + message + ""); doRawAppend(line, !QApplication::isLeftToRight()); } 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); QChar directionOfLine = rtl ? RLM : LRM; line = directionOfLine + "%1" + directionOfLine + nickLine + directionOfLine + " %3"; line = line.arg(timeStamp(messageTags), nick, text); if (inChannel) { emit textToLog(QString("<-> %1>\t%2").arg(nick, message)); } else { emit textToLog(QString("<%1>\t%2").arg(nick, message)); } /* BEGIN: WIPQTQUICK */ Application* konvApp = Application::instance(); MessageModel* msgModel = konvApp->getMainWindow()->getMessageModel(); msgModel->appendMessage(m_chatWin, QTime::currentTime().toString(Preferences::self()->timestampFormat()), nick, nick != m_server->getNickname() ? Preferences::self()->nickColor(m_server->obtainNickInfo(nick)->getNickColor()) : Preferences::self()->nickColor(8), + Konversation::removeIrcMarkup(message), text ); /* END: WIPQTQUICK */ 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()) { line = LRM + "%1 * " + nickLine + ""; line = line.arg(timeStamp(messageTags), nick); emit textToLog(QString("\t * %1").arg(nick)); /* BEGIN: WIPQTQUICK */ Application* konvApp = Application::instance(); MessageModel* msgModel = konvApp->getMainWindow()->getMessageModel(); msgModel->appendMessage(m_chatWin, QTime::currentTime().toString(Preferences::self()->timestampFormat()), nick, nick != m_server->getNickname() ? Preferences::self()->nickColor(m_server->obtainNickInfo(nick)->getNickColor()) : Preferences::self()->nickColor(8), QString(), + QString(), MessageModel::ActionMessage ); /* END: WIPQTQUICK */ doAppend(line, false); } else { QChar::Direction dir; QString text(filter(message, actionColor, nick, true,true, false, &dir)); bool rtl = (dir == QChar::DirR); QChar directionOfLine = rtl ? RLM : LRM; line = directionOfLine + "%1 " + directionOfLine + "* " + nickLine + directionOfLine + " %3"; line = line.arg(timeStamp(messageTags), nick, text); emit textToLog(QString("\t * %1 %2").arg(nick, message)); /* BEGIN: WIPQTQUICK */ Application* konvApp = Application::instance(); MessageModel* msgModel = konvApp->getMainWindow()->getMessageModel(); msgModel->appendMessage(m_chatWin, QTime::currentTime().toString(Preferences::self()->timestampFormat()), nick, nick != m_server->getNickname() ? Preferences::self()->nickColor(m_server->obtainNickInfo(nick)->getNickColor()) : Preferences::self()->nickColor(8), + Konversation::removeIrcMarkup(message), text, MessageModel::ActionMessage ); /* END: WIPQTQUICK */ 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, 0 , true, parseURL, false, &dir)); bool rtl = (dir == QChar::DirR); QChar directionOfLine = rtl ? RLM : LRM; line = directionOfLine + "%1 " + directionOfLine + "[%2]" + directionOfLine + " %3"; line = line.arg(timeStamp(messageTags), type, text); emit textToLog(QString("%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="***"; m_tabNotification = Konversation::tnfControl; if(type == i18nc("Message type", "Join")) { prefix="-->"; parseURL=false; } else if(type == i18nc("Message type", "Part") || type == i18nc("Message type", "Quit")) { prefix="<--"; } prefix=prefix.toHtmlEscaped(); QString line; QChar::Direction dir; QString text(filter(message, commandColor, 0, true, parseURL, self, &dir)); bool rtl = text.isRightToLeft(); QChar directionOfLine = rtl ? RLM : LRM; line = directionOfLine + "%1 %2 %3"; line = line.arg(timeStamp(messageTags), prefix, text); emit textToLog(QString("%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 + '|'; } else //It's a real nick { nick = LRM + nick + LRM; } // Nicks are in "" format so replace the "<>" nick.replace('<',"<"); nick.replace('>',">"); QString line; QChar::Direction dir; QString text(filter(message, backlogColor, NULL, false, false, false, &dir)); bool rtl = nick.startsWith('|') ? text.isRightToLeft() : (dir == QChar::DirR); QChar directionOfLine = rtl ? RLM : LRM; line = directionOfLine + "%1 " + directionOfLine + "%2" + directionOfLine + " %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); } 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) { if(Preferences::self()->timestamping()) { QDateTime serverTime; if (messageTags.contains(QStringLiteral("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(); QString timeColor = Preferences::self()->color(Preferences::Time).name(); QString timeFormat = Preferences::self()->timestampFormat(); QString timeString; bool rtlLocale = (QLocale().zeroDigit() == QChar((ushort)0x0660)) || // ARABIC-INDIC DIGIT ZERO (QLocale().zeroDigit() == QChar((ushort)0x06F0)); // EXTENDED ARABIC-INDIC DIGIT ZERO if(!Preferences::self()->showDate()) { timeString = QString(QLatin1String("[%1] ")).arg(time.toString(timeFormat)); } else { QDate date = serverTime.isValid() ? serverTime.date() : QDate::currentDate(); timeString = QString("[%1%2 %3%4] ") .arg(rtlLocale ? RLM : LRM, QLocale().toString(date, QLocale::ShortFormat), time.toString(timeFormat), !rtlLocale ? RLM : LRM); } return timeString; } return QString(); } QString IRCView::createNickLine(const QString& nick, const QString& defaultColor, bool encapsulateNick, bool privMsg) { QString nickLine = LRM + "%2" + LRM; 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 = static_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) ? QString("").arg(replacement) : QString("<%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 = static_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(QString("%%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('&', "&"); htmlText.replace("\x0b", "&"); } 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, QLatin1String("b")); pos += offset -1; linkOffset += offset -1; break; case '\x1d': //italic offset = defaultHtmlReplace(htmlText, &data, pos, QLatin1String("i")); pos += offset -1; linkOffset += offset -1; break; case '\x15': //mirc underline case '\x1f': //kvirc underline offset = defaultHtmlReplace(htmlText, &data, pos, QLatin1String("u")); pos += offset -1; linkOffset += offset -1; break; case '\x13': //strikethru offset = defaultHtmlReplace(htmlText, &data, pos, QLatin1String("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, QLatin1String("span")); data.lastBgColor.clear(); colorString += closeToTagString(&data, QLatin1String("font")); data.lastFgColor.clear(); } else if (data.openHtmlTags.contains("font")) { colorString += closeToTagString(&data, QLatin1String("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, QLatin1String("span")); colorString += closeToTagString(&data, QLatin1String("font")); } else if (data.openHtmlTags.contains(QLatin1String("font"))) { colorString += closeToTagString(&data, QLatin1String("font")); } data.lastFgColor = fgColor; if (!bgColor.isEmpty()) data.lastBgColor = bgColor; if (!data.lastFgColor.isEmpty()) { colorString += fontColorOpenTag(data.lastFgColor); data.openHtmlTags.append(QLatin1String("font")); if (!data.lastBgColor.isEmpty()) { colorString += spanColorOpenTag(data.lastBgColor); data.openHtmlTags.append(QLatin1String("span")); } } } htmlText.replace(pos, colorMatch.length(), colorString); pos += colorString.length() - 1; linkOffset += colorString.length() -colorMatch.length(); break; } 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, QLatin1String("span")); } if (data.openHtmlTags.contains(QLatin1String("font"))) { colorString += closeToTagString(&data, QLatin1String("font")); } data.reverse = true; colorString += fontColorOpenTag(Preferences::self()->color(Preferences::TextViewBackground).name()); data.openHtmlTags.append(QLatin1String("font")); colorString += spanColorOpenTag(defaultColor); data.openHtmlTags.append(QLatin1String("span")); } else { // if reset reverse, close reverse and set old fore- and // back-groundcolor if set in data colorString += closeToTagString(&data, QLatin1String("span")); colorString += closeToTagString(&data, QLatin1String("font")); data.reverse = false; if (!data.lastFgColor.isEmpty()) { colorString += fontColorOpenTag(data.lastFgColor); data.openHtmlTags.append(QLatin1String("font")); if (!data.lastBgColor.isEmpty()) { colorString += spanColorOpenTag(data.lastBgColor); data.openHtmlTags.append(QLatin1String("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('&', "&"); htmlText.replace("\x0b", "&"); } 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 ret += openTags(data, i); return ret; } QString IRCView::openTags(TextHtmlData* data, int from) { QString ret, tag; int i = from; 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, QLatin1String("b")); ++pos; break; case '\x1d': //italic defaultRemoveDuplicateHandling(data, QLatin1String("i")); ++pos; break; case '\x15': //mirc underline case '\x1f': //kvirc underline defaultRemoveDuplicateHandling(data, QLatin1String("u")); ++pos; break; case '\x13': //strikethru defaultRemoveDuplicateHandling(data, QLatin1String("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(QLatin1String("span")); data->openHtmlTags.removeOne(QLatin1String("font")); data->reverse = false; if (!data->lastFgColor.isEmpty()) { data->openHtmlTags.append(QLatin1String("font")); if (!data->lastBgColor.isEmpty()) { data->openHtmlTags.append(QLatin1String("span")); } } } else { data->openHtmlTags.removeOne(QLatin1String("span")); data->openHtmlTags.removeOne(QLatin1String("font")); data->reverse = true; data->openHtmlTags.append(QLatin1String("font")); data->openHtmlTags.append(QLatin1String("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(QLatin1String("span")); } if (!data->lastFgColor.isEmpty()) { data->lastFgColor.clear(); data->openHtmlTags.removeOne(QLatin1String("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(QLatin1String("font")); if (!bgColor.isEmpty()) { data->lastBgColor = bgColor; data->openHtmlTags.append(QLatin1String("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 = link.replace (' ', "%20"); // HACK Handle pipe as toString doesn't seem to decode that correctly link = link.replace ("%7C", "|"); // HACK Handle ` as toString doesn't seem to decode that correctly link = link.replace ("%60", "`"); 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 = link.replace (' ', "%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, 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; }