diff --git a/CMakeLists.txt b/CMakeLists.txt index 2d3c189f..72190947 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -281,6 +281,7 @@ set(SRC_FILES src/ui/InfoMessage.cpp src/ui/Label.cpp src/ui/LoadingIndicator.cpp + src/ui/NhekoCursorShape.cpp src/ui/NhekoDropArea.cpp src/ui/OverlayModal.cpp src/ui/OverlayWidget.cpp @@ -495,6 +496,7 @@ qt5_wrap_cpp(MOC_HEADERS src/ui/Label.h src/ui/FloatingButton.h src/ui/Menu.h + src/ui/NhekoCursorShape.h src/ui/NhekoDropArea.h src/ui/OverlayWidget.h src/ui/SnackBar.h diff --git a/README.md b/README.md index 2ee06940..b9690fc5 100644 --- a/README.md +++ b/README.md @@ -116,9 +116,7 @@ brew install --cask nheko ### Build Requirements -- Qt5 (5.10 or greater). Qt 5.7 adds support for color font rendering with - Freetype, which is essential to properly support emoji, 5.8 adds some features - to make interopability with Qml easier, 5.10 makes sliders actually visible with different palettes. +- Qt5 (5.12 or greater). Required for overlapping hover handlers in Qml. - CMake 3.15 or greater. (Lower version may work, but may break boost linking) - [mtxclient](https://github.com/Nheko-Reborn/mtxclient) - [LMDB](https://symas.com/lightning-memory-mapped-database/) diff --git a/resources/qml/Avatar.qml b/resources/qml/Avatar.qml index a459fe5a..f01911bb 100644 --- a/resources/qml/Avatar.qml +++ b/resources/qml/Avatar.qml @@ -90,4 +90,9 @@ Rectangle { } } + CursorShape { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + } + } diff --git a/resources/qml/EncryptionIndicator.qml b/resources/qml/EncryptionIndicator.qml index 46ca62c5..00efe9e4 100644 --- a/resources/qml/EncryptionIndicator.qml +++ b/resources/qml/EncryptionIndicator.qml @@ -1,4 +1,4 @@ -import QtQuick 2.5 +import QtQuick 2.12 import QtQuick.Controls 2.1 import im.nheko 1.0 @@ -24,14 +24,11 @@ Rectangle { color: "transparent" width: 16 height: 16 - ToolTip.visible: ma.containsMouse && indicator.visible + ToolTip.visible: ma.hovered && indicator.visible ToolTip.text: getEncryptionTooltip() - MouseArea { + HoverHandler { id: ma - - anchors.fill: parent - hoverEnabled: true } Image { diff --git a/resources/qml/ImageButton.qml b/resources/qml/ImageButton.qml index 9c0faef3..159c750f 100644 --- a/resources/qml/ImageButton.qml +++ b/resources/qml/ImageButton.qml @@ -1,6 +1,7 @@ import "./ui" import QtQuick 2.3 import QtQuick.Controls 2.3 +import im.nheko 1.0 // for cursor shape AbstractButton { id: button @@ -23,11 +24,10 @@ AbstractButton { source: image != "" ? ("image://colorimage/" + image + "?" + ((button.hovered && changeColorOnHover) ? highlightColor : buttonTextColor)) : "" } - MouseArea { + CursorShape { id: mouseArea anchors.fill: parent - onPressed: mouse.accepted = false cursorShape: Qt.PointingHandCursor } diff --git a/resources/qml/MatrixText.qml b/resources/qml/MatrixText.qml index bb3b4296..4ea15518 100644 --- a/resources/qml/MatrixText.qml +++ b/resources/qml/MatrixText.qml @@ -8,6 +8,7 @@ TextEdit { focus: false wrapMode: Text.Wrap selectByMouse: !Settings.mobileMode + enabled: selectByMouse color: colors.text onLinkActivated: { if (/^https:\/\/matrix.to\/#\/(@.*)$/.test(link)) { @@ -25,12 +26,9 @@ TextEdit { ToolTip.visible: hoveredLink ToolTip.text: hoveredLink - MouseArea { - id: ma - + CursorShape { anchors.fill: parent - acceptedButtons: Qt.NoButton - cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor + cursorShape: hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor } } diff --git a/resources/qml/MessageView.qml b/resources/qml/MessageView.qml index 179f6f54..e1641a36 100644 --- a/resources/qml/MessageView.qml +++ b/resources/qml/MessageView.qml @@ -1,6 +1,6 @@ import "./delegates" import QtGraphicalEffects 1.0 -import QtQuick 2.9 +import QtQuick 2.12 import QtQuick.Controls 2.3 import QtQuick.Layouts 1.2 import QtQuick.Window 2.2 @@ -153,12 +153,15 @@ ScrollView { color: TimelineManager.userColor(modelData ? modelData.userId : "", colors.window) textFormat: Text.RichText - MouseArea { + TapHandler { + //cursorShape: Qt.PointingHandCursor + + onSingleTapped: chat.model.openUserProfile(modelData.userId) + } + + CursorShape { anchors.fill: parent - Layout.alignment: Qt.AlignHCenter - onClicked: chat.model.openUserProfile(modelData.userId) cursorShape: Qt.PointingHandCursor - propagateComposedEvents: true } } diff --git a/resources/qml/TimelineRow.qml b/resources/qml/TimelineRow.qml index 5ec23d62..3a2ed627 100644 --- a/resources/qml/TimelineRow.qml +++ b/resources/qml/TimelineRow.qml @@ -1,6 +1,6 @@ import "./delegates" import "./emoji" -import QtQuick 2.6 +import QtQuick 2.12 import QtQuick.Controls 2.3 import QtQuick.Layouts 1.2 import QtQuick.Window 2.2 @@ -12,27 +12,23 @@ Item { height: row.height Rectangle { - color: (Settings.messageHoverHighlight && hoverHandler.containsMouse) ? colors.alternateBase : "transparent" + color: (Settings.messageHoverHighlight && hoverHandler.hovered) ? colors.alternateBase : "transparent" anchors.fill: row } - MouseArea { + HoverHandler { id: hoverHandler - anchors.fill: parent - propagateComposedEvents: true - preventStealing: false - hoverEnabled: true - acceptedButtons: Qt.AllButtons - onClicked: { - if (mouse.button === Qt.RightButton) - messageContextMenu.show(model.id, model.type, model.isEncrypted, model.isEditable, row); - else - mouse.accepted = false; - } - onPressAndHold: { - messageContextMenu.show(model.id, model.type, model.isEncrypted, model.isEditable, row, mapToItem(timelineRoot, mouse.x, mouse.y)); - } + acceptedDevices: PointerDevice.GenericPointer + } + + TapHandler { + acceptedButtons: Qt.RightButton + onSingleTapped: messageContextMenu.show(model.id, model.type, model.isEncrypted, model.isEditable, row, mapToItem(timelineRoot, eventPoint.position.x, eventPoint.position.y)) + } + + TapHandler { + onLongPressed: messageContextMenu.show(model.id, model.type, model.isEncrypted, model.isEditable, row, mapToItem(timelineRoot, point.position.x, point.position.y)) } RowLayout { @@ -151,15 +147,11 @@ Item { text: model.timestamp.toLocaleTimeString("HH:mm") width: Math.max(implicitWidth, text.length * fontMetrics.maximumCharacterWidth) color: inactiveColors.text - ToolTip.visible: ma.containsMouse + ToolTip.visible: ma.hovered ToolTip.text: Qt.formatDateTime(model.timestamp, Qt.DefaultLocaleLongDate) - MouseArea { + HoverHandler { id: ma - - anchors.fill: parent - hoverEnabled: true - propagateComposedEvents: true } } diff --git a/resources/qml/delegates/FileMessage.qml b/resources/qml/delegates/FileMessage.qml index ffd1e82b..4bc202eb 100644 --- a/resources/qml/delegates/FileMessage.qml +++ b/resources/qml/delegates/FileMessage.qml @@ -1,4 +1,4 @@ -import QtQuick 2.6 +import QtQuick 2.12 import QtQuick.Layouts 1.2 import im.nheko 1.0 @@ -31,7 +31,15 @@ Item { MouseArea { anchors.fill: parent - onClicked: TimelineManager.timeline.saveMedia(model.data.id) + cursorShape: Qt.PointingHandCursor + } + + TapHandler { + onSingleTapped: TimelineManager.timeline.saveMedia(model.data.id) + } + + CursorShape { + anchors.fill: parent cursorShape: Qt.PointingHandCursor } diff --git a/resources/qml/delegates/ImageMessage.qml b/resources/qml/delegates/ImageMessage.qml index e8e325f0..3bb9eb05 100644 --- a/resources/qml/delegates/ImageMessage.qml +++ b/resources/qml/delegates/ImageMessage.qml @@ -1,4 +1,4 @@ -import QtQuick 2.6 +import QtQuick 2.12 import im.nheko 1.0 Item { @@ -32,20 +32,20 @@ Item { smooth: true mipmap: true - MouseArea { - id: mouseArea - + TapHandler { enabled: model.data.type == MtxEvent.ImageMessage && img.status == Image.Ready - hoverEnabled: true - anchors.fill: parent - onClicked: TimelineManager.openImageOverlay(model.data.url, model.data.id) + onSingleTapped: TimelineManager.openImageOverlay(model.data.url, model.data.id) + } + + HoverHandler { + id: mouseArea } Item { id: overlay anchors.fill: parent - visible: mouseArea.containsMouse + visible: mouseArea.hovered Rectangle { id: container diff --git a/resources/qml/delegates/PlayableMediaMessage.qml b/resources/qml/delegates/PlayableMediaMessage.qml index 1534da2e..70f39e43 100644 --- a/resources/qml/delegates/PlayableMediaMessage.qml +++ b/resources/qml/delegates/PlayableMediaMessage.qml @@ -1,5 +1,5 @@ import QtMultimedia 5.6 -import QtQuick 2.6 +import QtQuick 2.12 import QtQuick.Controls 2.1 import QtQuick.Layouts 1.2 import im.nheko 1.0 @@ -140,9 +140,8 @@ Rectangle { fillMode: Image.Pad } - MouseArea { - anchors.fill: parent - onClicked: { + TapHandler { + onSingleTapped: { switch (button.state) { case "": TimelineManager.timeline.cacheMedia(model.data.id); @@ -159,6 +158,10 @@ Rectangle { break; } } + } + + CursorShape { + anchors.fill: parent cursorShape: Qt.PointingHandCursor } diff --git a/resources/qml/delegates/Reply.qml b/resources/qml/delegates/Reply.qml index ff1fa657..28c4bf6e 100644 --- a/resources/qml/delegates/Reply.qml +++ b/resources/qml/delegates/Reply.qml @@ -1,4 +1,4 @@ -import QtQuick 2.6 +import QtQuick 2.12 import QtQuick.Controls 2.3 import QtQuick.Layouts 1.2 import QtQuick.Window 2.2 @@ -13,10 +13,12 @@ Item { width: parent.width height: replyContainer.height - MouseArea { + TapHandler { + onSingleTapped: chat.positionViewAtIndex(chat.model.idToIndex(modelData.id), ListView.Contain) + } + + CursorShape { anchors.fill: parent - preventStealing: false - onClicked: chat.positionViewAtIndex(chat.model.idToIndex(modelData.id), ListView.Contain) cursorShape: Qt.PointingHandCursor } @@ -43,10 +45,8 @@ Item { color: replyComponent.userColor textFormat: Text.RichText - MouseArea { - anchors.fill: parent - onClicked: chat.model.openUserProfile(reply.modelData.userId) - cursorShape: Qt.PointingHandCursor + TapHandler { + onSingleTapped: chat.model.openUserProfile(reply.modelData.userId) } } diff --git a/resources/qml/delegates/TextMessage.qml b/resources/qml/delegates/TextMessage.qml index 2c449ca2..82d0d0e4 100644 --- a/resources/qml/delegates/TextMessage.qml +++ b/resources/qml/delegates/TextMessage.qml @@ -8,5 +8,6 @@ MatrixText { width: parent ? parent.width : undefined height: isReply ? Math.round(Math.min(timelineRoot.height / 8, implicitHeight)) : undefined clip: isReply + selectByMouse: !Settings.mobileMode && !isReply font.pointSize: (Settings.enlargeEmojiOnlyMessages && model.data.isOnlyEmoji > 0 && model.data.isOnlyEmoji < 4) ? Settings.fontSize * 3 : Settings.fontSize } diff --git a/src/timeline/TimelineViewManager.cpp b/src/timeline/TimelineViewManager.cpp index e1e2b681..b7d2bfb1 100644 --- a/src/timeline/TimelineViewManager.cpp +++ b/src/timeline/TimelineViewManager.cpp @@ -21,6 +21,7 @@ #include "dialogs/ImageOverlay.h" #include "emoji/EmojiModel.h" #include "emoji/Provider.h" +#include "ui/NhekoCursorShape.h" #include "ui/NhekoDropArea.h" #include //only for debugging @@ -118,6 +119,7 @@ TimelineViewManager::TimelineViewManager(CallManager *callManager, ChatPage *par qmlRegisterType("im.nheko", 1, 0, "DelegateChoice"); qmlRegisterType("im.nheko", 1, 0, "DelegateChooser"); qmlRegisterType("im.nheko", 1, 0, "NhekoDropArea"); + qmlRegisterType("im.nheko", 1, 0, "CursorShape"); qmlRegisterUncreatableType( "im.nheko", 1, 0, "DeviceVerificationFlow", "Can't create verification flow from QML!"); qmlRegisterUncreatableType( @@ -548,4 +550,4 @@ void TimelineViewManager::focusMessageInput() { emit focusInput(); -} \ No newline at end of file +} diff --git a/src/ui/NhekoCursorShape.cpp b/src/ui/NhekoCursorShape.cpp new file mode 100644 index 00000000..06b0a321 --- /dev/null +++ b/src/ui/NhekoCursorShape.cpp @@ -0,0 +1,25 @@ +#include "NhekoCursorShape.h" + +#include + +NhekoCursorShape::NhekoCursorShape(QQuickItem *parent) + : QQuickItem(parent) + , currentShape_(Qt::CursorShape::ArrowCursor) +{} + +Qt::CursorShape +NhekoCursorShape::cursorShape() const +{ + return cursor().shape(); +} + +void +NhekoCursorShape::setCursorShape(Qt::CursorShape cursorShape) +{ + if (currentShape_ == cursorShape) + return; + + currentShape_ = cursorShape; + setCursor(cursorShape); + emit cursorShapeChanged(); +} diff --git a/src/ui/NhekoCursorShape.h b/src/ui/NhekoCursorShape.h new file mode 100644 index 00000000..2eab5e42 --- /dev/null +++ b/src/ui/NhekoCursorShape.h @@ -0,0 +1,26 @@ +#pragma once + +// see +// https://stackoverflow.com/questions/27821054/how-to-change-cursor-shape-in-qml-when-mousearea-is-covered-with-another-mousear/29382092#29382092 + +#include + +class NhekoCursorShape : public QQuickItem +{ + Q_OBJECT + + Q_PROPERTY(Qt::CursorShape cursorShape READ cursorShape WRITE setCursorShape NOTIFY + cursorShapeChanged) + +public: + explicit NhekoCursorShape(QQuickItem *parent = 0); + +private: + Qt::CursorShape cursorShape() const; + void setCursorShape(Qt::CursorShape cursorShape); + + Qt::CursorShape currentShape_; + +signals: + void cursorShapeChanged(); +};