diff --git a/client/chatroomwidget.cpp b/client/chatroomwidget.cpp index d252bd9dd..55748395b 100644 --- a/client/chatroomwidget.cpp +++ b/client/chatroomwidget.cpp @@ -131,6 +131,11 @@ ChatRoomWidget::ChatRoomWidget(QWidget* parent) m_chatEdit->setAcceptRichText(false); m_chatEdit->setMaximumHeight(maximumChatEditHeight()); connect( m_chatEdit, &KChatEdit::returnPressed, this, &ChatRoomWidget::sendInput ); + connect(m_chatEdit, &KChatEdit::copyRequested, this, [=] { + QApplication::clipboard()->setText( + m_chatEdit->textCursor().hasSelection() ? m_chatEdit->textCursor().selectedText() : selectedText + ); + }); connect(m_chatEdit, &ChatEdit::proposedCompletion, this, [=](const QStringList& matches, int pos) { @@ -725,6 +730,14 @@ void ChatRoomWidget::showMenu(int index, const QString& hoveredLink, menu.exec(QCursor::pos()); } +void ChatRoomWidget::setGlobalSelectionBuffer(QString text) +{ + if (QApplication::clipboard()->supportsSelection()) + QApplication::clipboard()->setText(text, QClipboard::Selection); + + selectedText = text; +} + void ChatRoomWidget::reStartShownTimer() { if (!readMarkerOnScreen || indicesOnScreen.empty() || @@ -798,3 +811,22 @@ bool ChatRoomWidget::pendingMarkRead() const const auto rm = m_currentRoom->readMarker(); return rm != m_currentRoom->timelineEdge() && rm->index() < indexToMaybeRead; } + +void ChatRoomWidget::fileDrop(const QString& url) +{ + attachedFileName = QUrl(url).path(); + m_attachAction->setChecked(true); + m_chatEdit->setPlaceholderText( + tr("Add a message to the file or just push Enter")); + emit showStatusMessage(tr("Attaching %1").arg(attachedFileName)); +} + +void ChatRoomWidget::textDrop(const QString& text) +{ + m_chatEdit->setText(text); +} + +Qt::KeyboardModifiers ChatRoomWidget::getModifierKeys() +{ + return QGuiApplication::keyboardModifiers(); +} diff --git a/client/chatroomwidget.h b/client/chatroomwidget.h index 86f038686..9a4d4c071 100644 --- a/client/chatroomwidget.h +++ b/client/chatroomwidget.h @@ -77,6 +77,10 @@ class ChatRoomWidget: public QWidget void saveFileAs(QString eventId); void quote(const QString& htmlText); void showMenu(int index, const QString& hoveredLink, bool showingDetails); + void fileDrop(const QString& url); + void textDrop(const QString& text); + void setGlobalSelectionBuffer(QString text); + Qt::KeyboardModifiers getModifierKeys(); private slots: void sendInput(); @@ -108,6 +112,7 @@ class ChatRoomWidget: public QWidget bool readMarkerOnScreen; QMap> roomHistories; QString attachedFileName; + QString selectedText; void reStartShownTimer(); QString doSendInput(); diff --git a/client/kchatedit.cpp b/client/kchatedit.cpp index 3351bf0ab..63df51a9e 100644 --- a/client/kchatedit.cpp +++ b/client/kchatedit.cpp @@ -204,6 +204,11 @@ QSize KChatEdit::sizeHint() const void KChatEdit::keyPressEvent(QKeyEvent *event) { + if (event->matches(QKeySequence::Copy)) { + emit copyRequested(); + return; + } + switch (event->key()) { case Qt::Key_Enter: case Qt::Key_Return: diff --git a/client/kchatedit.h b/client/kchatedit.h index 7ca137d48..48b890996 100644 --- a/client/kchatedit.h +++ b/client/kchatedit.h @@ -109,6 +109,11 @@ class KChatEdit : public QTextEdit */ void returnPressed(); + /** + * Emitted when the user presses Ctrl+C. + */ + void copyRequested(); + protected: void keyPressEvent(QKeyEvent *event) Q_DECL_OVERRIDE; diff --git a/client/qml/ActiveLabel.qml b/client/qml/ActiveLabel.qml index 08f754412..117bd0f22 100644 --- a/client/qml/ActiveLabel.qml +++ b/client/qml/ActiveLabel.qml @@ -6,7 +6,7 @@ Label { font.italic: true textFormat: Text.PlainText - MouseArea + TimelineMouseArea { anchors.fill: parent cursorShape: Qt.PointingHandCursor diff --git a/client/qml/FileContent.qml b/client/qml/FileContent.qml index db00a861d..51f496267 100644 --- a/client/qml/FileContent.qml +++ b/client/qml/FileContent.qml @@ -27,7 +27,7 @@ Attachment { textFormat: TextEdit.PlainText wrapMode: Text.Wrap; - MouseArea { + TimelineMouseArea { anchors.fill: parent acceptedButtons: Qt.NoButton hoverEnabled: true @@ -37,7 +37,7 @@ Attachment { ? room.fileSource(eventId) : "") } - MouseArea { + TimelineMouseArea { anchors.fill: parent acceptedButtons: Qt.RightButton cursorShape: Qt.IBeamCursor diff --git a/client/qml/ImageContent.qml b/client/qml/ImageContent.qml index 6c6d9185d..33fcb2018 100644 --- a/client/qml/ImageContent.qml +++ b/client/qml/ImageContent.qml @@ -25,7 +25,7 @@ Attachment { // easing.type: Easing.OutQuad // }} - MouseArea { + TimelineMouseArea { anchors.fill: parent acceptedButtons: Qt.LeftButton hoverEnabled: true @@ -36,7 +36,7 @@ Attachment { onClicked: openExternally() } - MouseArea { + TimelineMouseArea { anchors.fill: parent acceptedButtons: Qt.RightButton cursorShape: Qt.PointingHandCursor diff --git a/client/qml/Timeline.qml b/client/qml/Timeline.qml index 285b268e3..13b48e695 100644 --- a/client/qml/Timeline.qml +++ b/client/qml/Timeline.qml @@ -190,6 +190,27 @@ Rectangle { ? Qt.ScrollBarAlwaysOff : Qt.ScrollBarAlwaysOn style: ScrollViewStyle { transientScrollBars: true } + DropArea { + anchors.fill: parent + onEntered: if (!room) { drag.accepted = false } + onDropped: { + if (drop.hasUrls) { + controller.fileDrop(drop.urls) + drop.acceptProposedAction() + } else if (drop.hasText) { + controller.textDrop(drop.text) + drop.acceptProposedAction() + } + } + } + + // This covers the area above a short chatView. + MouseArea { + anchors.fill: parent + acceptedButtons: Qt.AllButtons + onReleased: controller.focusInput() + } + ListView { id: chatView @@ -220,6 +241,8 @@ Rectangle { contentHeight > 0 && count > 0 ? count / contentHeight : 0.03 // 0.03 is just an arbitrary reasonable number + property var textEditWithSelection + function ensurePreviousContent() { if (noNeedMoreContent) return @@ -262,6 +285,21 @@ Rectangle { contentY = Math.min(originY + contentHeight - height, contentY + height) } + function onWheel(wheel) { + if (wheel.angleDelta.x == 0) { + var yDelta = wheel.angleDelta.y / 120 * 100 + + if (yDelta > 0) { + contentY = Math.max(originY, contentY - yDelta) + } else { + contentY = Math.min(contentY + (originY + contentHeight) - (contentY + height), + contentY + Math.abs(yDelta)) + } + wheel.accepted = true + } else { + wheel.accepted = false + } + } Connections { target: controller onPageUpPressed: chatView.pageUp() diff --git a/client/qml/TimelineItem.qml b/client/qml/TimelineItem.qml index 74a1f2666..275acfb99 100644 --- a/client/qml/TimelineItem.qml +++ b/client/qml/TimelineItem.qml @@ -120,14 +120,19 @@ Item { Rectangle { width: parent.width - height: childrenRect.height + 2 + height: sectionLabel.height + 2 visible: sectionVisible color: defaultPalette.window Label { + id: sectionLabel font.bold: true renderType: settings.render_type text: section } + TimelineMouseArea { + anchors.fill: parent + acceptedButtons: Qt.AllButtons + } } Loader { id: detailsAreaLoader @@ -138,6 +143,11 @@ Item { width: parent.width sourceComponent: detailsArea + + TimelineMouseArea { + anchors.fill: parent + acceptedButtons: Qt.AllButtons + } } Item { @@ -145,6 +155,11 @@ Item { width: parent.width height: childrenRect.height + TimelineMouseArea { + anchors.fill: parent + acceptedButtons: Qt.AllButtons + } + // There are several layout styles (av - author avatar, // al - author label, ts - timestamp, c - content // default (when "timeline_style" is not "xchat"): @@ -191,7 +206,7 @@ Item { text: (actionEvent ? "* " : "") + authorName } - MouseArea { + TimelineMouseArea { anchors.left: authorAvatar.left anchors.right: authorLabel.right anchors.top: authorLabel.top @@ -316,8 +331,11 @@ Item { else Qt.openUrlExternally(link) } + + TimelineTextEditSelector {} } - MouseArea { + + TimelineMouseArea { anchors.fill: parent acceptedButtons: Qt.MiddleButton @@ -328,14 +346,14 @@ Item { } } - MouseArea { + TimelineMouseArea { anchors.fill: parent acceptedButtons: Qt.RightButton onClicked: controller.showMenu(index, textFieldImpl.hoveredLink, showingDetails) } - MouseArea { + TimelineMouseArea { anchors.fill: parent cursorShape: textFieldImpl.hoveredLink ? Qt.PointingHandCursor : Qt.IBeamCursor @@ -480,6 +498,8 @@ Item { anchors.left: parent.left anchors.leftMargin: 3 z: 1 + + TimelineTextEditSelector {} } TextEdit { text: ""+ eventId @@ -495,7 +515,9 @@ Item { onLinkActivated: Qt.openUrlExternally(link) - MouseArea { + TimelineTextEditSelector {} + + TimelineMouseArea { anchors.fill: parent cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : @@ -524,6 +546,8 @@ Item { width: parent.width anchors.top: detailsHeader.bottom + + TimelineTextEditSelector {} } } } diff --git a/client/qml/TimelineMouseArea.qml b/client/qml/TimelineMouseArea.qml new file mode 100644 index 000000000..fbbfbaf27 --- /dev/null +++ b/client/qml/TimelineMouseArea.qml @@ -0,0 +1,6 @@ +import QtQuick 2.2 + +MouseArea { + onWheel: chatView.onWheel(wheel) + onReleased: controller.focusInput() +} diff --git a/client/qml/TimelineTextEditSelector.qml b/client/qml/TimelineTextEditSelector.qml new file mode 100644 index 000000000..84e92043b --- /dev/null +++ b/client/qml/TimelineTextEditSelector.qml @@ -0,0 +1,50 @@ +import QtQuick 2.2 + +/* + * Unfortunately, TextEdit captures LeftButton events for text selection in a way which + * is not compatible with our focus-cancelling mechanism, so we took over the task here. + */ +MouseArea { + property var textEdit: parent + property var selectionMode: TextEdit.SelectCharacters + + anchors.fill: parent + acceptedButtons: Qt.LeftButton + + onPressed: { + var x = mouse.x + var y = mouse.y + if (textEdit.flickableItem) { + x += textEdit.flickableItem.contentX + y += textEdit.flickableItem.contentY + } + var hasSelection = textEdit.selectionEnd > textEdit.selectionStart + if (hasSelection && controller.getModifierKeys() & Qt.ShiftModifier) { + textEdit.moveCursorSelection(textEdit.positionAt(x, y), selectionMode) + } else { + textEdit.cursorPosition = textEdit.positionAt(x, y) + if (chatView.textEditWithSelection) + chatView.textEditWithSelection.deselect() + } + } + onDoubleClicked: { + selectionMode = TextEdit.SelectWords + textEdit.selectWord() + } + onReleased: { + selectionMode = TextEdit.SelectCharacters + controller.setGlobalSelectionBuffer(textEdit.selectedText) + chatView.textEditWithSelection = textEdit + + controller.focusInput() + } + onPositionChanged: { + var x = mouse.x + var y = mouse.y + if (textEdit.flickableItem) { + x += textEdit.flickableItem.contentX + y += textEdit.flickableItem.contentY + } + textEdit.moveCursorSelection(textEdit.positionAt(x, y), selectionMode) + } +} diff --git a/client/resources.qrc b/client/resources.qrc index 78bb4a174..980632465 100644 --- a/client/resources.qrc +++ b/client/resources.qrc @@ -14,5 +14,7 @@ qml/FileContent.qml qml/TimelineItem.qml qml/ActiveLabel.qml + qml/TimelineMouseArea.qml + qml/TimelineTextEditSelector.qml