diff --git a/resources/qml/ActiveCallBar.qml b/resources/qml/ActiveCallBar.qml index 61484625..9344738e 100644 --- a/resources/qml/ActiveCallBar.qml +++ b/resources/qml/ActiveCallBar.qml @@ -1,111 +1,113 @@ import QtQuick 2.9 import QtQuick.Controls 2.3 import QtQuick.Layouts 1.2 - import im.nheko 1.0 Rectangle { - id: activeCallBar - visible: TimelineManager.callState != WebRTCState.DISCONNECTED - color: "#2ECC71" - implicitHeight: rowLayout.height + 8 + id: activeCallBar - RowLayout { - id: rowLayout - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.leftMargin: 8 + visible: TimelineManager.callState != WebRTCState.DISCONNECTED + color: "#2ECC71" + implicitHeight: rowLayout.height + 8 - Avatar { - width: avatarSize - height: avatarSize + RowLayout { + id: rowLayout - url: TimelineManager.callPartyAvatarUrl.replace("mxc://", "image://MxcImage/") - displayName: TimelineManager.callPartyName - } + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.leftMargin: 8 - Label { - font.pointSize: fontMetrics.font.pointSize * 1.1 - text: " " + TimelineManager.callPartyName + " " - } + Avatar { + width: avatarSize + height: avatarSize + url: TimelineManager.callPartyAvatarUrl.replace("mxc://", "image://MxcImage/") + displayName: TimelineManager.callPartyName + } - Image { - Layout.preferredWidth: 24 - Layout.preferredHeight: 24 - source: "qrc:/icons/icons/ui/place-call.png" - } + Label { + font.pointSize: fontMetrics.font.pointSize * 1.1 + text: " " + TimelineManager.callPartyName + " " + } - Label { - id: callStateLabel - font.pointSize: fontMetrics.font.pointSize * 1.1 - } + Image { + Layout.preferredWidth: 24 + Layout.preferredHeight: 24 + source: "qrc:/icons/icons/ui/place-call.png" + } - Connections { - target: TimelineManager - function onCallStateChanged(state) { - switch (state) { - case WebRTCState.INITIATING: - callStateLabel.text = qsTr("Initiating...") - break; - case WebRTCState.OFFERSENT: - callStateLabel.text = qsTr("Calling...") - break; - case WebRTCState.CONNECTING: - callStateLabel.text = qsTr("Connecting...") - break; - case WebRTCState.CONNECTED: - callStateLabel.text = "00:00" - var d = new Date() - callTimer.startTime = Math.floor(d.getTime() / 1000) - break; - case WebRTCState.DISCONNECTED: - callStateLabel.text = "" - } - } - } + Label { + id: callStateLabel - Timer { - id: callTimer - property int startTime - interval: 1000 - running: TimelineManager.callState == WebRTCState.CONNECTED - repeat: true - onTriggered: { - var d = new Date() - let seconds = Math.floor(d.getTime() / 1000 - startTime) - let s = Math.floor(seconds % 60) - let m = Math.floor(seconds / 60) % 60 - let h = Math.floor(seconds / 3600) - callStateLabel.text = (h ? (pad(h) + ":") : "") + pad(m) + ":" + pad(s) - } + font.pointSize: fontMetrics.font.pointSize * 1.1 + } - function pad(n) { - return (n < 10) ? ("0" + n) : n - } - } + Connections { + function onCallStateChanged(state) { + switch (state) { + case WebRTCState.INITIATING: + callStateLabel.text = qsTr("Initiating..."); + break; + case WebRTCState.OFFERSENT: + callStateLabel.text = qsTr("Calling..."); + break; + case WebRTCState.CONNECTING: + callStateLabel.text = qsTr("Connecting..."); + break; + case WebRTCState.CONNECTED: + callStateLabel.text = "00:00"; + var d = new Date(); + callTimer.startTime = Math.floor(d.getTime() / 1000); + break; + case WebRTCState.DISCONNECTED: + callStateLabel.text = ""; + } + } - Item { - Layout.fillWidth: true - } + target: TimelineManager + } - ImageButton { - width: 24 - height: 24 - buttonTextColor: "#000000" - image: TimelineManager.isMicMuted ? - ":/icons/icons/ui/microphone-unmute.png" : - ":/icons/icons/ui/microphone-mute.png" + Timer { + id: callTimer - hoverEnabled: true - ToolTip.visible: hovered - ToolTip.text: TimelineManager.isMicMuted ? qsTr("Unmute Mic") : qsTr("Mute Mic") + property int startTime - onClicked: TimelineManager.toggleMicMute() - } + function pad(n) { + return (n < 10) ? ("0" + n) : n; + } + + interval: 1000 + running: TimelineManager.callState == WebRTCState.CONNECTED + repeat: true + onTriggered: { + var d = new Date(); + let seconds = Math.floor(d.getTime() / 1000 - startTime); + let s = Math.floor(seconds % 60); + let m = Math.floor(seconds / 60) % 60; + let h = Math.floor(seconds / 3600); + callStateLabel.text = (h ? (pad(h) + ":") : "") + pad(m) + ":" + pad(s); + } + } + + Item { + Layout.fillWidth: true + } + + ImageButton { + width: 24 + height: 24 + buttonTextColor: "#000000" + image: TimelineManager.isMicMuted ? ":/icons/icons/ui/microphone-unmute.png" : ":/icons/icons/ui/microphone-mute.png" + hoverEnabled: true + ToolTip.visible: hovered + ToolTip.text: TimelineManager.isMicMuted ? qsTr("Unmute Mic") : qsTr("Mute Mic") + onClicked: TimelineManager.toggleMicMute() + } + + Item { + implicitWidth: 16 + } + + } - Item { - implicitWidth: 16 - } - } } diff --git a/resources/qml/Avatar.qml b/resources/qml/Avatar.qml index df3dd08e..a247bffe 100644 --- a/resources/qml/Avatar.qml +++ b/resources/qml/Avatar.qml @@ -1,69 +1,75 @@ +import QtGraphicalEffects 1.0 import QtQuick 2.6 import QtQuick.Controls 2.3 -import QtGraphicalEffects 1.0 - import im.nheko 1.0 Rectangle { - id: avatar - width: 48 - height: 48 - radius: Settings.avatarCircles ? height/2 : 3 + id: avatar - property alias url: img.source - property string userid - property string displayName + property alias url: img.source + property string userid + property string displayName - Label { - anchors.fill: parent - text: TimelineManager.escapeEmoji(displayName ? String.fromCodePoint(displayName.codePointAt(0)) : "") - textFormat: Text.RichText - font.pixelSize: avatar.height/2 - verticalAlignment: Text.AlignVCenter - horizontalAlignment: Text.AlignHCenter - visible: img.status != Image.Ready - color: colors.text - } + width: 48 + height: 48 + radius: Settings.avatarCircles ? height / 2 : 3 + color: colors.base - Image { - id: img - anchors.fill: parent - asynchronous: true - fillMode: Image.PreserveAspectCrop - mipmap: true - smooth: false + Label { + anchors.fill: parent + text: TimelineManager.escapeEmoji(displayName ? String.fromCodePoint(displayName.codePointAt(0)) : "") + textFormat: Text.RichText + font.pixelSize: avatar.height / 2 + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignHCenter + visible: img.status != Image.Ready + color: colors.text + } - sourceSize.width: avatar.width - sourceSize.height: avatar.height + Image { + id: img - layer.enabled: true - layer.effect: OpacityMask { - maskSource: Rectangle { - anchors.fill: parent - width: avatar.width - height: avatar.height - radius: Settings.avatarCircles ? height/2 : 3 - } - } + anchors.fill: parent + asynchronous: true + fillMode: Image.PreserveAspectCrop + mipmap: true + smooth: false + sourceSize.width: avatar.width + sourceSize.height: avatar.height + layer.enabled: true - } + layer.effect: OpacityMask { - Rectangle { - anchors.bottom: avatar.bottom - anchors.right: avatar.right + maskSource: Rectangle { + anchors.fill: parent + width: avatar.width + height: avatar.height + radius: Settings.avatarCircles ? height / 2 : 3 + } - visible: !!userid + } - height: avatar.height / 6 - width: height - radius: Settings.avatarCircles ? height / 2 : height / 4 - color: switch (TimelineManager.userPresence(userid)) { - case "online": return "#00cc66" - case "unavailable": return "#ff9933" - case "offline": // return "#a82353" don't show anything if offline, since it is confusing, if presence is disabled - default: "transparent" - } - } + } + + Rectangle { + anchors.bottom: avatar.bottom + anchors.right: avatar.right + visible: !!userid + height: avatar.height / 6 + width: height + radius: Settings.avatarCircles ? height / 2 : height / 4 + color: { + switch (TimelineManager.userPresence(userid)) { + case "online": + return "#00cc66"; + case "unavailable": + return "#ff9933"; + case "offline": + default: + // return "#a82353" don't show anything if offline, since it is confusing, if presence is disabled + "transparent"; + } + } + } - color: colors.base } diff --git a/resources/qml/EncryptionIndicator.qml b/resources/qml/EncryptionIndicator.qml index 428c2fae..46ca62c5 100644 --- a/resources/qml/EncryptionIndicator.qml +++ b/resources/qml/EncryptionIndicator.qml @@ -3,39 +3,42 @@ import QtQuick.Controls 2.1 import im.nheko 1.0 Rectangle { - property bool encrypted: false - id: indicator - color: "transparent" - width: 16 - height: 16 + id: indicator - ToolTip.visible: ma.containsMouse && indicator.visible - ToolTip.text: getEncryptionTooltip() + property bool encrypted: false - MouseArea{ - id: ma - anchors.fill: parent - hoverEnabled: true - } + function getEncryptionImage() { + if (encrypted) + return "image://colorimage/:/icons/icons/ui/lock.png?" + colors.buttonText; + else + return "image://colorimage/:/icons/icons/ui/unlock.png?#dd3d3d"; + } - Image { - id: stateImg - anchors.fill: parent - source: getEncryptionImage() - } + function getEncryptionTooltip() { + if (encrypted) + return qsTr("Encrypted"); + else + return qsTr("This message is not encrypted!"); + } - function getEncryptionImage() { - if (encrypted) - return "image://colorimage/:/icons/icons/ui/lock.png?"+colors.buttonText - else - return "image://colorimage/:/icons/icons/ui/unlock.png?#dd3d3d" - } + color: "transparent" + width: 16 + height: 16 + ToolTip.visible: ma.containsMouse && indicator.visible + ToolTip.text: getEncryptionTooltip() + + MouseArea { + id: ma + + anchors.fill: parent + hoverEnabled: true + } + + Image { + id: stateImg + + anchors.fill: parent + source: getEncryptionImage() + } - function getEncryptionTooltip() { - if (encrypted) - return qsTr("Encrypted") - else - return qsTr("This message is not encrypted!") - } } - diff --git a/resources/qml/ImageButton.qml b/resources/qml/ImageButton.qml index 54399ae7..49ddf671 100644 --- a/resources/qml/ImageButton.qml +++ b/resources/qml/ImageButton.qml @@ -2,25 +2,29 @@ import QtQuick 2.3 import QtQuick.Controls 2.3 AbstractButton { - property string image: undefined - property color highlightColor: colors.highlight - property color buttonTextColor: colors.buttonText - width: 16 - height: 16 - id: button + id: button - Image { - id: buttonImg - // Workaround, can't get icon.source working for now... - anchors.fill: parent - source: "image://colorimage/" + image + "?" + (button.hovered ? highlightColor : buttonTextColor) - } + property string image: undefined + property color highlightColor: colors.highlight + property color buttonTextColor: colors.buttonText + + width: 16 + height: 16 + + Image { + id: buttonImg + + // Workaround, can't get icon.source working for now... + anchors.fill: parent + source: "image://colorimage/" + image + "?" + (button.hovered ? highlightColor : buttonTextColor) + } + + MouseArea { + id: mouseArea + + anchors.fill: parent + onPressed: mouse.accepted = false + cursorShape: Qt.PointingHandCursor + } - MouseArea - { - 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 29214168..6c96a539 100644 --- a/resources/qml/MatrixText.qml +++ b/resources/qml/MatrixText.qml @@ -1,35 +1,37 @@ import QtQuick 2.5 import QtQuick.Controls 2.3 - import im.nheko 1.0 TextEdit { - textFormat: TextEdit.RichText - readOnly: true - wrapMode: Text.Wrap - selectByMouse: true - activeFocusOnPress: false - color: colors.text + textFormat: TextEdit.RichText + readOnly: true + wrapMode: Text.Wrap + selectByMouse: true + activeFocusOnPress: false + color: colors.text + onLinkActivated: { + if (/^https:\/\/matrix.to\/#\/(@.*)$/.test(link)) { + chat.model.openUserProfile(/^https:\/\/matrix.to\/#\/(@.*)$/.exec(link)[1]); + } else if (/^https:\/\/matrix.to\/#\/(![^\/]*)$/.test(link)) { + TimelineManager.setHistoryView(/^https:\/\/matrix.to\/#\/(!.*)$/.exec(link)[1]); + } else if (/^https:\/\/matrix.to\/#\/(![^\/]*)\/(\$.*)$/.test(link)) { + var match = /^https:\/\/matrix.to\/#\/(![^\/]*)\/(\$.*)$/.exec(link); + TimelineManager.setHistoryView(match[1]); + chat.positionViewAtIndex(chat.model.idToIndex(match[2]), ListView.Contain); + } else { + TimelineManager.openLink(link); + } + } + ToolTip.visible: hoveredLink + ToolTip.text: hoveredLink - onLinkActivated: { - if (/^https:\/\/matrix.to\/#\/(@.*)$/.test(link)) chat.model.openUserProfile(/^https:\/\/matrix.to\/#\/(@.*)$/.exec(link)[1]) - else if (/^https:\/\/matrix.to\/#\/(![^\/]*)$/.test(link)) TimelineManager.setHistoryView(/^https:\/\/matrix.to\/#\/(!.*)$/.exec(link)[1]) - else if (/^https:\/\/matrix.to\/#\/(![^\/]*)\/(\$.*)$/.test(link)) { - var match = /^https:\/\/matrix.to\/#\/(![^\/]*)\/(\$.*)$/.exec(link) - TimelineManager.setHistoryView(match[1]) - chat.positionViewAtIndex(chat.model.idToIndex(match[2]), ListView.Contain) - } - else TimelineManager.openLink(link) - } - MouseArea - { - id: ma - anchors.fill: parent - propagateComposedEvents: true - acceptedButtons: Qt.NoButton - cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor - } + MouseArea { + id: ma + + anchors.fill: parent + propagateComposedEvents: true + acceptedButtons: Qt.NoButton + cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor + } - ToolTip.visible: hoveredLink - ToolTip.text: hoveredLink } diff --git a/resources/qml/Reactions.qml b/resources/qml/Reactions.qml index ec46f7e0..6487f512 100644 --- a/resources/qml/Reactions.qml +++ b/resources/qml/Reactions.qml @@ -1,94 +1,95 @@ import QtQuick 2.6 import QtQuick.Controls 2.2 - import im.nheko 1.0 // This class is for showing Reactions in the timeline row, not for // adding new reactions via the emoji picker Flow { - id: reactionFlow + id: reactionFlow - // highlight colors for selfReactedEvent background - property real highlightHue: colors.highlight.hslHue - property real highlightSat: colors.highlight.hslSaturation - property real highlightLight: colors.highlight.hslLightness + // highlight colors for selfReactedEvent background + property real highlightHue: colors.highlight.hslHue + property real highlightSat: colors.highlight.hslSaturation + property real highlightLight: colors.highlight.hslLightness + property string eventId + property alias reactions: repeater.model - property string eventId + anchors.left: parent.left + anchors.right: parent.right + spacing: 4 - anchors.left: parent.left - anchors.right: parent.right - spacing: 4 + Repeater { + id: repeater - property alias reactions: repeater.model + delegate: AbstractButton { + id: reaction - Repeater { - id: repeater + hoverEnabled: true + implicitWidth: contentItem.childrenRect.width + contentItem.leftPadding * 2 + implicitHeight: contentItem.childrenRect.height + ToolTip.visible: hovered + ToolTip.text: modelData.users + onClicked: { + console.debug("Picked " + modelData.key + "in response to " + reactionFlow.eventId + ". selfReactedEvent: " + modelData.selfReactedEvent); + TimelineManager.queueReactionMessage(reactionFlow.eventId, modelData.key); + } - delegate: AbstractButton { - id: reaction - hoverEnabled: true - implicitWidth: contentItem.childrenRect.width + contentItem.leftPadding*2 - implicitHeight: contentItem.childrenRect.height + contentItem: Row { + anchors.centerIn: parent + spacing: reactionText.implicitHeight / 4 + leftPadding: reactionText.implicitHeight / 2 + rightPadding: reactionText.implicitHeight / 2 - ToolTip.visible: hovered - ToolTip.text: modelData.users + TextMetrics { + id: textMetrics - onClicked: { - console.debug("Picked " + modelData.key + "in response to " + reactionFlow.eventId + ". selfReactedEvent: " + modelData.selfReactedEvent) - TimelineManager.queueReactionMessage(reactionFlow.eventId, modelData.key) - } + font.family: Settings.emojiFont + elide: Text.ElideRight + elideWidth: 150 + text: modelData.key + } + Text { + id: reactionText - contentItem: Row { - anchors.centerIn: parent - spacing: reactionText.implicitHeight/4 - leftPadding: reactionText.implicitHeight / 2 - rightPadding: reactionText.implicitHeight / 2 + anchors.baseline: reactionCounter.baseline + text: textMetrics.elidedText + (textMetrics.elidedText == modelData.key ? "" : "…") + font.family: Settings.emojiFont + color: reaction.hovered ? colors.highlight : colors.text + maximumLineCount: 1 + } - TextMetrics { - id: textMetrics - font.family: Settings.emojiFont - elide: Text.ElideRight - elideWidth: 150 - text: modelData.key - } + Rectangle { + id: divider - Text { - anchors.baseline: reactionCounter.baseline - id: reactionText - text: textMetrics.elidedText + (textMetrics.elidedText == modelData.key ? "" : "…") - font.family: Settings.emojiFont - color: reaction.hovered ? colors.highlight : colors.text - maximumLineCount: 1 - } + height: Math.floor(reactionCounter.implicitHeight * 1.4) + width: 1 + color: (reaction.hovered || modelData.selfReactedEvent !== '') ? colors.highlight : colors.text + } - Rectangle { - id: divider - height: Math.floor(reactionCounter.implicitHeight * 1.4) - width: 1 - color: (reaction.hovered || modelData.selfReactedEvent !== '') ? colors.highlight : colors.text - } + Text { + id: reactionCounter - Text { - anchors.verticalCenter: divider.verticalCenter - id: reactionCounter - text: modelData.count - font: reaction.font - color: reaction.hovered ? colors.highlight : colors.text - } - } + anchors.verticalCenter: divider.verticalCenter + text: modelData.count + font: reaction.font + color: reaction.hovered ? colors.highlight : colors.text + } - background: Rectangle { - anchors.centerIn: parent + } + + background: Rectangle { + anchors.centerIn: parent + implicitWidth: reaction.implicitWidth + implicitHeight: reaction.implicitHeight + border.color: (reaction.hovered || modelData.selfReactedEvent !== '') ? colors.highlight : colors.text + color: modelData.selfReactedEvent !== '' ? Qt.hsla(highlightHue, highlightSat, highlightLight, 0.2) : colors.base + border.width: 1 + radius: reaction.height / 2 + } + + } + + } - implicitWidth: reaction.implicitWidth - implicitHeight: reaction.implicitHeight - border.color: (reaction.hovered || modelData.selfReactedEvent !== '') ? colors.highlight : colors.text - color: modelData.selfReactedEvent !== '' ? Qt.hsla(highlightHue, highlightSat, highlightLight, 0.20) : colors.base - border.width: 1 - radius: reaction.height / 2.0 - } - } - } } - diff --git a/resources/qml/ScrollHelper.qml b/resources/qml/ScrollHelper.qml index ba7c2648..7dc31464 100644 --- a/resources/qml/ScrollHelper.qml +++ b/resources/qml/ScrollHelper.qml @@ -16,10 +16,6 @@ * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ - -import QtQuick 2.9 -import QtQuick.Controls 2.3 - /* * Shamelessly stolen from: * https://cgit.kde.org/kube.git/tree/framework/qml/ScrollHelper.qml @@ -31,81 +27,82 @@ import QtQuick.Controls 2.3 * ScrollView.qml in qtquickcontrols * qquickwheelarea.cpp in qtquickcontrols */ + +import QtQuick 2.9 +import QtQuick.Controls 2.3 + MouseArea { + // console.warn("Delta: ", wheel.pixelDelta.y); + // console.warn("Old position: ", flickable.contentY); + // console.warn("New position: ", newPos); + id: root - propagateComposedEvents: true property Flickable flickable property alias enabled: root.enabled - //Place the mouse area under the flickable - z: -1 - onFlickableChanged: { - if (enabled) { - flickable.maximumFlickVelocity = 100000 - flickable.boundsBehavior = Flickable.StopAtBounds - root.parent = flickable - } - } - - acceptedButtons: Qt.NoButton - function calculateNewPosition(flickableItem, wheel) { //Nothing to scroll - if (flickableItem.contentHeight < flickableItem.height) { + if (flickableItem.contentHeight < flickableItem.height) return flickableItem.contentY; - } + //Ignore 0 events (happens at least with Christians trackpad) - if (wheel.pixelDelta.y == 0 && wheel.angleDelta.y == 0) { + if (wheel.pixelDelta.y == 0 && wheel.angleDelta.y == 0) return flickableItem.contentY; - } + //pixelDelta seems to be the same as angleDelta/8 - var pixelDelta = 0 + var pixelDelta = 0; //The pixelDelta is a smaller number if both are provided, so pixelDelta can be 0 while angleDelta is still something. So we check the angleDelta if (wheel.angleDelta.y) { - var wheelScrollLines = 3 //Default value of QApplication wheelScrollLines property - var pixelPerLine = 20 //Default value in Qt, originally comes from QTextEdit - var ticks = (wheel.angleDelta.y / 8) / 15.0 //Divide by 8 gives us pixels typically come in 15pixel steps. - pixelDelta = ticks * pixelPerLine * wheelScrollLines + var wheelScrollLines = 3; //Default value of QApplication wheelScrollLines property + var pixelPerLine = 20; //Default value in Qt, originally comes from QTextEdit + var ticks = (wheel.angleDelta.y / 8) / 15; //Divide by 8 gives us pixels typically come in 15pixel steps. + pixelDelta = ticks * pixelPerLine * wheelScrollLines; } else { - pixelDelta = wheel.pixelDelta.y + pixelDelta = wheel.pixelDelta.y; } - - pixelDelta = Math.round(pixelDelta) - - if (!pixelDelta) { + pixelDelta = Math.round(pixelDelta); + if (!pixelDelta) return flickableItem.contentY; - } var minYExtent = flickableItem.originY + flickableItem.topMargin; var maxYExtent = (flickableItem.contentHeight + flickableItem.bottomMargin + flickableItem.originY) - flickableItem.height; - - if (typeof(flickableItem.headerItem) !== "undefined" && flickableItem.headerItem) { - minYExtent += flickableItem.headerItem.height - } + if (typeof (flickableItem.headerItem) !== "undefined" && flickableItem.headerItem) + minYExtent += flickableItem.headerItem.height; //Avoid overscrolling return Math.max(minYExtent, Math.min(maxYExtent, flickableItem.contentY - pixelDelta)); } + propagateComposedEvents: true + //Place the mouse area under the flickable + z: -1 + onFlickableChanged: { + if (enabled) { + flickable.maximumFlickVelocity = 100000; + flickable.boundsBehavior = Flickable.StopAtBounds; + root.parent = flickable; + } + } + acceptedButtons: Qt.NoButton onWheel: { var newPos = calculateNewPosition(flickable, wheel); - // console.warn("Delta: ", wheel.pixelDelta.y); - // console.warn("Old position: ", flickable.contentY); - // console.warn("New position: ", newPos); - // Show the scrollbars flickable.flick(0, 0); flickable.contentY = newPos; - cancelFlickStateTimer.start() + cancelFlickStateTimer.start(); } - Timer { id: cancelFlickStateTimer + //How long the scrollbar will remain visible interval: 500 // Hide the scrollbars - onTriggered: { flickable.cancelFlick(); flickable.movementEnded(); } + onTriggered: { + flickable.cancelFlick(); + flickable.movementEnded(); + } } + } diff --git a/resources/qml/StatusIndicator.qml b/resources/qml/StatusIndicator.qml index ec82ed49..0b18b888 100644 --- a/resources/qml/StatusIndicator.qml +++ b/resources/qml/StatusIndicator.qml @@ -3,37 +3,55 @@ import QtQuick.Controls 2.1 import im.nheko 1.0 Rectangle { - id: indicator - property int state: 0 - color: "transparent" - width: 16 - height: 16 + id: indicator - ToolTip.visible: ma.containsMouse && state != MtxEvent.Empty - ToolTip.text: switch (state) { - case MtxEvent.Failed: return qsTr("Failed") - case MtxEvent.Sent: return qsTr("Sent") - case MtxEvent.Received: return qsTr("Received") - case MtxEvent.Read: return qsTr("Read") - default: return "" - } - MouseArea{ - id: ma - anchors.fill: parent - hoverEnabled: true - } + property int state: 0 + + color: "transparent" + width: 16 + height: 16 + ToolTip.visible: ma.containsMouse && state != MtxEvent.Empty + ToolTip.text: { + switch (state) { + case MtxEvent.Failed: + return qsTr("Failed"); + case MtxEvent.Sent: + return qsTr("Sent"); + case MtxEvent.Received: + return qsTr("Received"); + case MtxEvent.Read: + return qsTr("Read"); + default: + return ""; + } + } + + MouseArea { + id: ma + + anchors.fill: parent + hoverEnabled: true + } + + Image { + id: stateImg + + // Workaround, can't get icon.source working for now... + anchors.fill: parent + source: { + switch (indicator.state) { + case MtxEvent.Failed: + return "image://colorimage/:/icons/icons/ui/remove-symbol.png?" + colors.buttonText; + case MtxEvent.Sent: + return "image://colorimage/:/icons/icons/ui/clock.png?" + colors.buttonText; + case MtxEvent.Received: + return "image://colorimage/:/icons/icons/ui/checkmark.png?" + colors.buttonText; + case MtxEvent.Read: + return "image://colorimage/:/icons/icons/ui/double-tick-indicator.png?" + colors.buttonText; + default: + return ""; + } + } + } - Image { - id: stateImg - // Workaround, can't get icon.source working for now... - anchors.fill: parent - source: switch (indicator.state) { - case MtxEvent.Failed: return "image://colorimage/:/icons/icons/ui/remove-symbol.png?" + colors.buttonText - case MtxEvent.Sent: return "image://colorimage/:/icons/icons/ui/clock.png?" + colors.buttonText - case MtxEvent.Received: return "image://colorimage/:/icons/icons/ui/checkmark.png?" + colors.buttonText - case MtxEvent.Read: return "image://colorimage/:/icons/icons/ui/double-tick-indicator.png?" + colors.buttonText - default: return "" - } - } } - diff --git a/resources/qml/TimelineRow.qml b/resources/qml/TimelineRow.qml index c026d828..38597673 100644 --- a/resources/qml/TimelineRow.qml +++ b/resources/qml/TimelineRow.qml @@ -1,146 +1,148 @@ +import "./delegates" +import "./emoji" import QtQuick 2.6 import QtQuick.Controls 2.3 import QtQuick.Layouts 1.2 import QtQuick.Window 2.2 - import im.nheko 1.0 -import "./delegates" -import "./emoji" - Item { - anchors.left: parent.left - anchors.right: parent.right - height: row.height + anchors.left: parent.left + anchors.right: parent.right + height: row.height - MouseArea { - anchors.fill: parent - propagateComposedEvents: true - preventStealing: true - hoverEnabled: true + MouseArea { + anchors.fill: parent + propagateComposedEvents: true + preventStealing: true + hoverEnabled: true + acceptedButtons: Qt.AllButtons + onClicked: { + if (mouse.button === Qt.RightButton) + messageContextMenu.show(model.id, model.type, model.isEncrypted, row); - acceptedButtons: Qt.AllButtons - onClicked: { - if (mouse.button === Qt.RightButton) - messageContextMenu.show(model.id, model.type, model.isEncrypted, row) - } - onPressAndHold: { - messageContextMenu.show(model.id, model.type, model.isEncrypted, row, mapToItem(timelineRoot, mouse.x, mouse.y)) - } - } - Rectangle { - color: (Settings.messageHoverHighlight && parent.containsMouse) ? colors.base : "transparent" - anchors.fill: row - } - RowLayout { - id: row + } + onPressAndHold: { + messageContextMenu.show(model.id, model.type, model.isEncrypted, row, mapToItem(timelineRoot, mouse.x, mouse.y)); + } + } - anchors.leftMargin: avatarSize + 16 - anchors.left: parent.left - anchors.right: parent.right + Rectangle { + color: (Settings.messageHoverHighlight && parent.containsMouse) ? colors.base : "transparent" + anchors.fill: row + } + RowLayout { + id: row - Column { - Layout.fillWidth: true - Layout.alignment: Qt.AlignTop - spacing: 4 + anchors.leftMargin: avatarSize + 16 + anchors.left: parent.left + anchors.right: parent.right - // fancy reply, if this is a reply - Reply { - visible: model.replyTo - modelData: chat.model.getDump(model.replyTo,model.id) - userColor: TimelineManager.userColor(modelData.userId, colors.window) - } + Column { + Layout.fillWidth: true + Layout.alignment: Qt.AlignTop + spacing: 4 - // actual message content - MessageDelegate { - id: contentItem + // fancy reply, if this is a reply + Reply { + visible: model.replyTo + modelData: chat.model.getDump(model.replyTo, model.id) + userColor: TimelineManager.userColor(modelData.userId, colors.window) + } - width: parent.width + // actual message content + MessageDelegate { + id: contentItem - modelData: model - } + width: parent.width + modelData: model + } - Reactions { - id: reactionRow - reactions: model.reactions - eventId: model.id - } - } + Reactions { + id: reactionRow - StatusIndicator { - state: model.state - Layout.alignment: Qt.AlignRight | Qt.AlignTop - Layout.preferredHeight: 16 - width: 16 - } + reactions: model.reactions + eventId: model.id + } - EncryptionIndicator { - visible: model.isRoomEncrypted - encrypted: model.isEncrypted - Layout.alignment: Qt.AlignRight | Qt.AlignTop - Layout.preferredHeight: 16 - width: 16 - } - EmojiButton { - visible: Settings.buttonsInTimeline - Layout.alignment: Qt.AlignRight | Qt.AlignTop - Layout.preferredHeight: 16 - width: 16 - id: reactButton - hoverEnabled: true - ToolTip.visible: hovered - ToolTip.text: qsTr("React") - emojiPicker: emojiPopup - event_id: model.id - } - ImageButton { - visible: Settings.buttonsInTimeline - Layout.alignment: Qt.AlignRight | Qt.AlignTop - Layout.preferredHeight: 16 - width: 16 - id: replyButton - hoverEnabled: true + } + StatusIndicator { + state: model.state + Layout.alignment: Qt.AlignRight | Qt.AlignTop + Layout.preferredHeight: 16 + width: 16 + } - image: ":/icons/icons/ui/mail-reply.png" + EncryptionIndicator { + visible: model.isRoomEncrypted + encrypted: model.isEncrypted + Layout.alignment: Qt.AlignRight | Qt.AlignTop + Layout.preferredHeight: 16 + width: 16 + } - ToolTip.visible: hovered - ToolTip.text: qsTr("Reply") + EmojiButton { + id: reactButton - onClicked: chat.model.replyAction(model.id) - } - ImageButton { - visible: Settings.buttonsInTimeline - Layout.alignment: Qt.AlignRight | Qt.AlignTop - Layout.preferredHeight: 16 - width: 16 - id: optionsButton - hoverEnabled: true + visible: Settings.buttonsInTimeline + Layout.alignment: Qt.AlignRight | Qt.AlignTop + Layout.preferredHeight: 16 + width: 16 + hoverEnabled: true + ToolTip.visible: hovered + ToolTip.text: qsTr("React") + emojiPicker: emojiPopup + event_id: model.id + } - image: ":/icons/icons/ui/vertical-ellipsis.png" + ImageButton { + id: replyButton - ToolTip.visible: hovered - ToolTip.text: qsTr("Options") + visible: Settings.buttonsInTimeline + Layout.alignment: Qt.AlignRight | Qt.AlignTop + Layout.preferredHeight: 16 + width: 16 + hoverEnabled: true + image: ":/icons/icons/ui/mail-reply.png" + ToolTip.visible: hovered + ToolTip.text: qsTr("Reply") + onClicked: chat.model.replyAction(model.id) + } - onClicked: messageContextMenu.show(model.id, model.type, model.isEncrypted, optionsButton) - } + ImageButton { + id: optionsButton - Label { - Layout.alignment: Qt.AlignRight | Qt.AlignTop - text: model.timestamp.toLocaleTimeString("HH:mm") - width: Math.max(implicitWidth, text.length*fontMetrics.maximumCharacterWidth) - color: inactiveColors.text + visible: Settings.buttonsInTimeline + Layout.alignment: Qt.AlignRight | Qt.AlignTop + Layout.preferredHeight: 16 + width: 16 + hoverEnabled: true + image: ":/icons/icons/ui/vertical-ellipsis.png" + ToolTip.visible: hovered + ToolTip.text: qsTr("Options") + onClicked: messageContextMenu.show(model.id, model.type, model.isEncrypted, optionsButton) + } - MouseArea{ - id: ma - anchors.fill: parent - hoverEnabled: true - propagateComposedEvents: true - } + Label { + Layout.alignment: Qt.AlignRight | Qt.AlignTop + text: model.timestamp.toLocaleTimeString("HH:mm") + width: Math.max(implicitWidth, text.length * fontMetrics.maximumCharacterWidth) + color: inactiveColors.text + ToolTip.visible: ma.containsMouse + ToolTip.text: Qt.formatDateTime(model.timestamp, Qt.DefaultLocaleLongDate) + + MouseArea { + id: ma + + anchors.fill: parent + hoverEnabled: true + propagateComposedEvents: true + } + + } + + } - ToolTip.visible: ma.containsMouse - ToolTip.text: Qt.formatDateTime(model.timestamp, Qt.DefaultLocaleLongDate) - } - } } diff --git a/resources/qml/TimelineView.qml b/resources/qml/TimelineView.qml index 5c9ca348..ab0148e9 100644 --- a/resources/qml/TimelineView.qml +++ b/resources/qml/TimelineView.qml @@ -1,526 +1,582 @@ +import "./delegates" +import "./device-verification" +import "./emoji" +import QtGraphicalEffects 1.0 import QtQuick 2.9 import QtQuick.Controls 2.3 import QtQuick.Layouts 1.2 -import QtGraphicalEffects 1.0 import QtQuick.Window 2.2 - import im.nheko 1.0 import im.nheko.EmojiModel 1.0 -import "./delegates" -import "./emoji" -import "./device-verification" - Page { - id: timelineRoot - - property var colors: currentActivePalette - property var systemInactive: SystemPalette { colorGroup: SystemPalette.Disabled } - property var inactiveColors: currentInactivePalette ? currentInactivePalette : systemInactive - property int avatarSize: 40 - property real highlightHue: colors.highlight.hslHue - property real highlightSat: colors.highlight.hslSaturation - property real highlightLight: colors.highlight.hslLightness - - palette: colors - - FontMetrics { - id: fontMetrics - } - - EmojiPicker { - id: emojiPopup - width: 7 * 52 + 20 - height: 6 * 52 - colors: palette - model: EmojiProxyModel { - category: EmojiCategory.People - sourceModel: EmojiModel {} - } - } - - Menu { - id: messageContextMenu - modal: true - - function show(eventId_, eventType_, isEncrypted_, showAt_, position) { - eventId = eventId_ - eventType = eventType_ - isEncrypted = isEncrypted_ - - if (position) - popup(position, showAt_) - else - popup(showAt_) - } - - property string eventId - property int eventType - property bool isEncrypted - - MenuItem { - text: qsTr("React") - onClicked: emojiPopup.show(messageContextMenu.parent, messageContextMenu.eventId) - } - MenuItem { - text: qsTr("Reply") - onClicked: chat.model.replyAction(messageContextMenu.eventId) - } - MenuItem { - text: qsTr("Read receipts") - onTriggered: chat.model.readReceiptsAction(messageContextMenu.eventId) - } - MenuItem { - text: qsTr("Mark as read") - } - MenuItem { - text: qsTr("View raw message") - onTriggered: chat.model.viewRawMessage(messageContextMenu.eventId) - } - MenuItem { - visible: messageContextMenu.isEncrypted - height: visible ? implicitHeight : 0 - text: qsTr("View decrypted raw message") - onTriggered: chat.model.viewDecryptedRawMessage(messageContextMenu.eventId) - } - MenuItem { - text: qsTr("Redact message") - onTriggered: chat.model.redactEvent(messageContextMenu.eventId) - } - MenuItem { - visible: messageContextMenu.eventType == MtxEvent.ImageMessage || messageContextMenu.eventType == MtxEvent.VideoMessage || messageContextMenu.eventType == MtxEvent.AudioMessage || messageContextMenu.eventType == MtxEvent.FileMessage || messageContextMenu.eventType == MtxEvent.Sticker - height: visible ? implicitHeight : 0 - text: qsTr("Save as") - onTriggered: TimelineManager.timeline.saveMedia(messageContextMenu.eventId) - } - } - - Rectangle { - anchors.fill: parent - color: colors.window - - Component { - id: deviceVerificationDialog - DeviceVerification {} - } - Connections { - target: TimelineManager - function onNewDeviceVerificationRequest(flow,transactionId,userId,deviceId,isRequest) { - var dialog = deviceVerificationDialog.createObject(timelineRoot, {flow: flow}); - dialog.show(); - } - } - Connections { - target: TimelineManager.timeline - function onOpenProfile(profile) { - var userProfile = userProfileComponent.createObject(timelineRoot,{profile: profile}); - userProfile.show(); - } - } - - Label { - visible: !TimelineManager.timeline && !TimelineManager.isInitialSync - anchors.centerIn: parent - text: qsTr("No room open") - font.pointSize: 24 - color: colors.text - } - - BusyIndicator { - visible: running - anchors.centerIn: parent - running: TimelineManager.isInitialSync - height: 200 - width: 200 - z: 3 - } - - ColumnLayout { - anchors.fill: parent - Rectangle { - id: topBar - - Layout.fillWidth: true - implicitHeight: topLayout.height + 16 - z: 3 - - color: colors.base - - MouseArea { - anchors.fill: parent - onClicked: TimelineManager.openRoomSettings(); - } - - GridLayout { - id: topLayout - - anchors.left: parent.left - anchors.right: parent.right - anchors.margins: 8 - anchors.verticalCenter: parent.verticalCenter - - //Layout.margins: 8 - - ImageButton { - id: backToRoomsButton - - Layout.column: 0 - Layout.row: 0 - Layout.rowSpan: 2 - Layout.alignment: Qt.AlignVCenter - - visible: TimelineManager.isNarrowView - - image: ":/icons/icons/ui/angle-pointing-to-left.png" - - ToolTip.visible: hovered - ToolTip.text: qsTr("Back to room list") - - onClicked: TimelineManager.backToRooms() - } - - Avatar { - Layout.column: 1 - Layout.row: 0 - Layout.rowSpan: 2 - Layout.alignment: Qt.AlignVCenter - - width: avatarSize - height: avatarSize - - url: chat.model ? chat.model.roomAvatarUrl.replace("mxc://", "image://MxcImage/") : "" - displayName: chat.model ? chat.model.roomName : qsTr("No room selected") - - MouseArea { - anchors.fill: parent - onClicked: TimelineManager.openRoomSettings(); - } - } - - Label { - Layout.fillWidth: true - Layout.column: 2 - Layout.row: 0 - color: colors.text - - font.pointSize: fontMetrics.font.pointSize * 1.1 - - text: chat.model ? chat.model.roomName : qsTr("No room selected") - - MouseArea { - anchors.fill: parent - onClicked: TimelineManager.openRoomSettings(); - } - } - MatrixText { - Layout.fillWidth: true - Layout.column: 2 - Layout.row: 1 - Layout.maximumHeight: fontMetrics.lineSpacing * 2 // show 2 lines - clip: true - - text: chat.model ? chat.model.roomTopic : "" - } - - ImageButton { - id: roomOptionsButton - - Layout.column: 3 - Layout.row: 0 - Layout.rowSpan: 2 - Layout.alignment: Qt.AlignVCenter - - image: ":/icons/icons/ui/vertical-ellipsis.png" - - ToolTip.visible: hovered - ToolTip.text: qsTr("Room options") - - onClicked: roomOptionsMenu.popup(roomOptionsButton) - - Menu { - id: roomOptionsMenu - MenuItem { - text: qsTr("Invite users") - onTriggered: TimelineManager.openInviteUsersDialog(); - } - MenuItem { - text: qsTr("Members") - onTriggered: TimelineManager.openMemberListDialog(); - } - MenuItem { - text: qsTr("Leave room") - onTriggered: TimelineManager.openLeaveRoomDialog(); - } - MenuItem { - text: qsTr("Settings") - onTriggered: TimelineManager.openRoomSettings(); - } - } - } - } - } - - ListView { - id: chat - - visible: TimelineManager.timeline != null - - cacheBuffer: 400 - - Layout.fillWidth: true - Layout.fillHeight: true - - model: TimelineManager.timeline - - boundsBehavior: Flickable.StopAtBounds - - ScrollHelper { - flickable: parent - anchors.fill: parent - } - - pixelAligned: true - - Shortcut { - sequence: StandardKey.MoveToPreviousPage - onActivated: { chat.contentY = chat.contentY - chat.height / 2; chat.returnToBounds(); } - } - Shortcut { - sequence: StandardKey.MoveToNextPage - onActivated: { chat.contentY = chat.contentY + chat.height / 2; chat.returnToBounds(); } - } - Shortcut { - sequence: StandardKey.Cancel - onActivated: chat.model.reply = undefined - } - Shortcut { - sequence: "Alt+Up" - onActivated: chat.model.reply = chat.model.indexToId(chat.model.reply? chat.model.idToIndex(chat.model.reply) + 1 : 0) - } - Shortcut { - sequence: "Alt+Down" - onActivated: { - var idx = chat.model.reply? chat.model.idToIndex(chat.model.reply) - 1 : -1 - chat.model.reply = idx >= 0 ? chat.model.indexToId(idx) : undefined - } - } - - ScrollBar.vertical: ScrollBar { - id: scrollbar - } - - spacing: 4 - verticalLayoutDirection: ListView.BottomToTop - - onCountChanged: if (atYEnd) model.currentIndex = 0 // Mark last event as read, since we are at the bottom - - property int delegateMaxWidth: (Settings.timelineMaxWidth > 100 && (parent.width - Settings.timelineMaxWidth) > scrollbar.width*2) ? Settings.timelineMaxWidth : (parent.width - scrollbar.width*2) - - delegate: Item { - // This would normally be previousSection, but our model's order is inverted. - property bool sectionBoundary: (ListView.nextSection != "" && ListView.nextSection !== ListView.section) || model.index === chat.count - 1 - - id: wrapper - property Item section - anchors.horizontalCenter: parent ? parent.horizontalCenter : undefined - width: chat.delegateMaxWidth - height: section ? section.height + timelinerow.height : timelinerow.height - - TimelineRow { - id: timelinerow - y: section ? section.y + section.height : 0 - } - - onSectionBoundaryChanged: { - if (sectionBoundary) { - var properties = { - 'modelData': model.dump, - 'section': ListView.section, - 'nextSection': ListView.nextSection - } - section = sectionHeader.createObject(wrapper, properties) - } else { - section.destroy() - section = null - } - } - - Connections { - target: chat - function onMovementEnded() { - if (y + height + 2 * chat.spacing > chat.contentY + chat.height && y < chat.contentY + chat.height) - chat.model.currentIndex = index; - } - } - } - - Component{ - id: userProfileComponent - UserProfile{} - } - - section { - property: "section" - } - Component { - id: sectionHeader - Column { - property var modelData - property string section - property string nextSection - - topPadding: 4 - bottomPadding: 4 - spacing: 8 - - visible: !!modelData - - width: parent.width - height: (section.includes(" ") ? dateBubble.height + 8 + userName.height : userName.height) + 8 - - Label { - id: dateBubble - anchors.horizontalCenter: parent ? parent.horizontalCenter : undefined - visible: section.includes(" ") - text: chat.model.formatDateSeparator(modelData.timestamp) - color: colors.text - - height: fontMetrics.height * 1.4 - width: contentWidth * 1.2 - - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - background: Rectangle { - radius: parent.height / 2 - color: colors.base - } - } - - Row { - height: userName.height - spacing: 8 - - Avatar { - width: avatarSize - height: avatarSize - url: chat.model.avatarUrl(modelData.userId).replace("mxc://", "image://MxcImage/") - displayName: modelData.userName - userid: modelData.userId - - MouseArea { - anchors.fill: parent - onClicked: chat.model.openUserProfile(modelData.userId) - cursorShape: Qt.PointingHandCursor - propagateComposedEvents: true - } - } - - Label { - id: userName - text: TimelineManager.escapeEmoji(modelData.userName) - color: TimelineManager.userColor(modelData.userId, colors.window) - textFormat: Text.RichText - - MouseArea { - anchors.fill: parent - Layout.alignment: Qt.AlignHCenter - onClicked: chat.model.openUserProfile(modelData.userId) - cursorShape: Qt.PointingHandCursor - propagateComposedEvents: true - } - } - } - } - } - - footer: BusyIndicator { - anchors.horizontalCenter: parent.horizontalCenter - running: chat.model && chat.model.paginationInProgress - height: 50 - width: 50 - z: 3 - } - } - - Item { - id: chatFooter - - implicitHeight: Math.max(fontMetrics.height * 1.2, footerContent.height) - Layout.fillWidth: true - z: 3 - - Column { - id: footerContent - anchors.left: parent.left - anchors.right: parent.right - anchors.bottom: parent.bottom - Rectangle { - id: typingRect + id: timelineRoot + + property var colors: currentActivePalette + property var systemInactive + property var inactiveColors: currentInactivePalette ? currentInactivePalette : systemInactive + property int avatarSize: 40 + property real highlightHue: colors.highlight.hslHue + property real highlightSat: colors.highlight.hslSaturation + property real highlightLight: colors.highlight.hslLightness + + palette: colors + + FontMetrics { + id: fontMetrics + } + + EmojiPicker { + id: emojiPopup + + width: 7 * 52 + 20 + height: 6 * 52 + colors: palette + + model: EmojiProxyModel { + category: EmojiCategory.People + + sourceModel: EmojiModel { + } + + } + + } + + Menu { + id: messageContextMenu + + property string eventId + property int eventType + property bool isEncrypted + + function show(eventId_, eventType_, isEncrypted_, showAt_, position) { + eventId = eventId_; + eventType = eventType_; + isEncrypted = isEncrypted_; + if (position) + popup(position, showAt_); + else + popup(showAt_); + } + + modal: true + + MenuItem { + text: qsTr("React") + onClicked: emojiPopup.show(messageContextMenu.parent, messageContextMenu.eventId) + } + + MenuItem { + text: qsTr("Reply") + onClicked: chat.model.replyAction(messageContextMenu.eventId) + } + + MenuItem { + text: qsTr("Read receipts") + onTriggered: chat.model.readReceiptsAction(messageContextMenu.eventId) + } + + MenuItem { + text: qsTr("Mark as read") + } + + MenuItem { + text: qsTr("View raw message") + onTriggered: chat.model.viewRawMessage(messageContextMenu.eventId) + } + + MenuItem { + visible: messageContextMenu.isEncrypted + height: visible ? implicitHeight : 0 + text: qsTr("View decrypted raw message") + onTriggered: chat.model.viewDecryptedRawMessage(messageContextMenu.eventId) + } + + MenuItem { + text: qsTr("Redact message") + onTriggered: chat.model.redactEvent(messageContextMenu.eventId) + } + + MenuItem { + visible: messageContextMenu.eventType == MtxEvent.ImageMessage || messageContextMenu.eventType == MtxEvent.VideoMessage || messageContextMenu.eventType == MtxEvent.AudioMessage || messageContextMenu.eventType == MtxEvent.FileMessage || messageContextMenu.eventType == MtxEvent.Sticker + height: visible ? implicitHeight : 0 + text: qsTr("Save as") + onTriggered: TimelineManager.timeline.saveMedia(messageContextMenu.eventId) + } + + } + + Rectangle { + anchors.fill: parent + color: colors.window + + Component { + id: deviceVerificationDialog + + DeviceVerification { + } + + } + + Connections { + function onNewDeviceVerificationRequest(flow, transactionId, userId, deviceId, isRequest) { + var dialog = deviceVerificationDialog.createObject(timelineRoot, { + "flow": flow + }); + dialog.show(); + } + + target: TimelineManager + } + + Connections { + function onOpenProfile(profile) { + var userProfile = userProfileComponent.createObject(timelineRoot, { + "profile": profile + }); + userProfile.show(); + } + + target: TimelineManager.timeline + } + + Label { + visible: !TimelineManager.timeline && !TimelineManager.isInitialSync + anchors.centerIn: parent + text: qsTr("No room open") + font.pointSize: 24 + color: colors.text + } + + BusyIndicator { + visible: running + anchors.centerIn: parent + running: TimelineManager.isInitialSync + height: 200 + width: 200 + z: 3 + } + + ColumnLayout { + anchors.fill: parent + + Rectangle { + id: topBar + + Layout.fillWidth: true + implicitHeight: topLayout.height + 16 + z: 3 + color: colors.base + + MouseArea { + anchors.fill: parent + onClicked: TimelineManager.openRoomSettings() + } + + GridLayout { + //Layout.margins: 8 + + id: topLayout + anchors.left: parent.left anchors.right: parent.right - color: (chat.model && chat.model.typingUsers.length > 0) ? colors.window : "transparent" - height: typingDisplay.height + anchors.margins: 8 + anchors.verticalCenter: parent.verticalCenter + + ImageButton { + id: backToRoomsButton + + Layout.column: 0 + Layout.row: 0 + Layout.rowSpan: 2 + Layout.alignment: Qt.AlignVCenter + visible: TimelineManager.isNarrowView + image: ":/icons/icons/ui/angle-pointing-to-left.png" + ToolTip.visible: hovered + ToolTip.text: qsTr("Back to room list") + onClicked: TimelineManager.backToRooms() + } + + Avatar { + Layout.column: 1 + Layout.row: 0 + Layout.rowSpan: 2 + Layout.alignment: Qt.AlignVCenter + width: avatarSize + height: avatarSize + url: chat.model ? chat.model.roomAvatarUrl.replace("mxc://", "image://MxcImage/") : "" + displayName: chat.model ? chat.model.roomName : qsTr("No room selected") + + MouseArea { + anchors.fill: parent + onClicked: TimelineManager.openRoomSettings() + } + + } + Label { - id: typingDisplay - anchors.left: parent.left - anchors.leftMargin: 10 - anchors.right: parent.right - anchors.rightMargin: 10 + Layout.fillWidth: true + Layout.column: 2 + Layout.row: 0 color: colors.text - text: chat.model ? chat.model.formatTypingUsers(chat.model.typingUsers, colors.window) : "" - textFormat: Text.RichText + font.pointSize: fontMetrics.font.pointSize * 1.1 + text: chat.model ? chat.model.roomName : qsTr("No room selected") + + MouseArea { + anchors.fill: parent + onClicked: TimelineManager.openRoomSettings() + } + + } + + MatrixText { + Layout.fillWidth: true + Layout.column: 2 + Layout.row: 1 + Layout.maximumHeight: fontMetrics.lineSpacing * 2 // show 2 lines + clip: true + text: chat.model ? chat.model.roomTopic : "" + } + + ImageButton { + id: roomOptionsButton + + Layout.column: 3 + Layout.row: 0 + Layout.rowSpan: 2 + Layout.alignment: Qt.AlignVCenter + image: ":/icons/icons/ui/vertical-ellipsis.png" + ToolTip.visible: hovered + ToolTip.text: qsTr("Room options") + onClicked: roomOptionsMenu.popup(roomOptionsButton) + + Menu { + id: roomOptionsMenu + + MenuItem { + text: qsTr("Invite users") + onTriggered: TimelineManager.openInviteUsersDialog() + } + + MenuItem { + text: qsTr("Members") + onTriggered: TimelineManager.openMemberListDialog() + } + + MenuItem { + text: qsTr("Leave room") + onTriggered: TimelineManager.openLeaveRoomDialog() + } + + MenuItem { + text: qsTr("Settings") + onTriggered: TimelineManager.openRoomSettings() + } + + } + + } + + } + + } + + ListView { + id: chat + + property int delegateMaxWidth: (Settings.timelineMaxWidth > 100 && (parent.width - Settings.timelineMaxWidth) > scrollbar.width * 2) ? Settings.timelineMaxWidth : (parent.width - scrollbar.width * 2) + + visible: TimelineManager.timeline != null + cacheBuffer: 400 + Layout.fillWidth: true + Layout.fillHeight: true + model: TimelineManager.timeline + boundsBehavior: Flickable.StopAtBounds + pixelAligned: true + spacing: 4 + verticalLayoutDirection: ListView.BottomToTop + onCountChanged: { + if (atYEnd) + model.currentIndex = 0; + + } // Mark last event as read, since we are at the bottom + + ScrollHelper { + flickable: parent + anchors.fill: parent + } + + Shortcut { + sequence: StandardKey.MoveToPreviousPage + onActivated: { + chat.contentY = chat.contentY - chat.height / 2; + chat.returnToBounds(); } } - Rectangle { - anchors.left: parent.left - anchors.right: parent.right + Shortcut { + sequence: StandardKey.MoveToNextPage + onActivated: { + chat.contentY = chat.contentY + chat.height / 2; + chat.returnToBounds(); + } + } - id: replyPopup + Shortcut { + sequence: StandardKey.Cancel + onActivated: chat.model.reply = undefined + } - visible: chat.model && chat.model.reply - // Height of child, plus margins, plus border - height: replyPreview.height + 10 - color: colors.base + Shortcut { + sequence: "Alt+Up" + onActivated: chat.model.reply = chat.model.indexToId(chat.model.reply ? chat.model.idToIndex(chat.model.reply) + 1 : 0) + } + Shortcut { + sequence: "Alt+Down" + onActivated: { + var idx = chat.model.reply ? chat.model.idToIndex(chat.model.reply) - 1 : -1; + chat.model.reply = idx >= 0 ? chat.model.indexToId(idx) : undefined; + } + } - Reply { - id: replyPreview + Component { + id: userProfileComponent - anchors.left: parent.left - anchors.leftMargin: 10 - anchors.right: closeReplyButton.left - anchors.rightMargin: 20 - anchors.bottom: parent.bottom + UserProfile { + } - modelData: chat.model ? chat.model.getDump(chat.model.reply, chat.model.id) : {} - userColor: TimelineManager.userColor(modelData.userId, colors.window) - } + } - ImageButton { - id: closeReplyButton + section { + property: "section" + } - anchors.right: parent.right - anchors.rightMargin: 15 - anchors.top: replyPreview.top - hoverEnabled: true - width: 16 - height: 16 + Component { + id: sectionHeader - image: ":/icons/icons/ui/remove-symbol.png" - ToolTip.visible: closeReplyButton.hovered - ToolTip.text: qsTr("Close") + Column { + property var modelData + property string section + property string nextSection - onClicked: chat.model.reply = undefined - } - } - } - } + topPadding: 4 + bottomPadding: 4 + spacing: 8 + visible: !!modelData + width: parent.width + height: (section.includes(" ") ? dateBubble.height + 8 + userName.height : userName.height) + 8 + + Label { + id: dateBubble + + anchors.horizontalCenter: parent ? parent.horizontalCenter : undefined + visible: section.includes(" ") + text: chat.model.formatDateSeparator(modelData.timestamp) + color: colors.text + height: fontMetrics.height * 1.4 + width: contentWidth * 1.2 + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + + background: Rectangle { + radius: parent.height / 2 + color: colors.base + } + + } + + Row { + height: userName.height + spacing: 8 + + Avatar { + width: avatarSize + height: avatarSize + url: chat.model.avatarUrl(modelData.userId).replace("mxc://", "image://MxcImage/") + displayName: modelData.userName + userid: modelData.userId + + MouseArea { + anchors.fill: parent + onClicked: chat.model.openUserProfile(modelData.userId) + cursorShape: Qt.PointingHandCursor + propagateComposedEvents: true + } + + } + + Label { + id: userName + + text: TimelineManager.escapeEmoji(modelData.userName) + color: TimelineManager.userColor(modelData.userId, colors.window) + textFormat: Text.RichText + + MouseArea { + anchors.fill: parent + Layout.alignment: Qt.AlignHCenter + onClicked: chat.model.openUserProfile(modelData.userId) + cursorShape: Qt.PointingHandCursor + propagateComposedEvents: true + } + + } + + } + + } + + } + + ScrollBar.vertical: ScrollBar { + id: scrollbar + } + + delegate: Item { + id: wrapper + + // This would normally be previousSection, but our model's order is inverted. + property bool sectionBoundary: (ListView.nextSection != "" && ListView.nextSection !== ListView.section) || model.index === chat.count - 1 + property Item section + + anchors.horizontalCenter: parent ? parent.horizontalCenter : undefined + width: chat.delegateMaxWidth + height: section ? section.height + timelinerow.height : timelinerow.height + onSectionBoundaryChanged: { + if (sectionBoundary) { + var properties = { + "modelData": model.dump, + "section": ListView.section, + "nextSection": ListView.nextSection + }; + section = sectionHeader.createObject(wrapper, properties); + } else { + section.destroy(); + section = null; + } + } + + TimelineRow { + id: timelinerow + + y: section ? section.y + section.height : 0 + } + + Connections { + function onMovementEnded() { + if (y + height + 2 * chat.spacing > chat.contentY + chat.height && y < chat.contentY + chat.height) + chat.model.currentIndex = index; + + } + + target: chat + } + + } + + footer: BusyIndicator { + anchors.horizontalCenter: parent.horizontalCenter + running: chat.model && chat.model.paginationInProgress + height: 50 + width: 50 + z: 3 + } + + } + + Item { + id: chatFooter + + implicitHeight: Math.max(fontMetrics.height * 1.2, footerContent.height) + Layout.fillWidth: true + z: 3 + + Column { + id: footerContent + + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + + Rectangle { + id: typingRect + + anchors.left: parent.left + anchors.right: parent.right + color: (chat.model && chat.model.typingUsers.length > 0) ? colors.window : "transparent" + height: typingDisplay.height + + Label { + id: typingDisplay + + anchors.left: parent.left + anchors.leftMargin: 10 + anchors.right: parent.right + anchors.rightMargin: 10 + color: colors.text + text: chat.model ? chat.model.formatTypingUsers(chat.model.typingUsers, colors.window) : "" + textFormat: Text.RichText + } + + } + + Rectangle { + id: replyPopup + + anchors.left: parent.left + anchors.right: parent.right + visible: chat.model && chat.model.reply + // Height of child, plus margins, plus border + height: replyPreview.height + 10 + color: colors.base + + Reply { + id: replyPreview + + anchors.left: parent.left + anchors.leftMargin: 10 + anchors.right: closeReplyButton.left + anchors.rightMargin: 20 + anchors.bottom: parent.bottom + modelData: chat.model ? chat.model.getDump(chat.model.reply, chat.model.id) : { + } + userColor: TimelineManager.userColor(modelData.userId, colors.window) + } + + ImageButton { + id: closeReplyButton + + anchors.right: parent.right + anchors.rightMargin: 15 + anchors.top: replyPreview.top + hoverEnabled: true + width: 16 + height: 16 + image: ":/icons/icons/ui/remove-symbol.png" + ToolTip.visible: closeReplyButton.hovered + ToolTip.text: qsTr("Close") + onClicked: chat.model.reply = undefined + } + + } + + } + + } + + ActiveCallBar { + Layout.fillWidth: true + z: 3 + } + + } + + } + + systemInactive: SystemPalette { + colorGroup: SystemPalette.Disabled + } - ActiveCallBar { - Layout.fillWidth: true - z: 3 - } - } - } } diff --git a/resources/qml/UserProfile.qml b/resources/qml/UserProfile.qml index 562dd4f9..2e6758ae 100644 --- a/resources/qml/UserProfile.qml +++ b/resources/qml/UserProfile.qml @@ -1,172 +1,175 @@ +import "./device-verification" import QtQuick 2.9 import QtQuick.Controls 2.3 import QtQuick.Layouts 1.2 import QtQuick.Window 2.3 - import im.nheko 1.0 -import "./device-verification" +ApplicationWindow { + id: userProfileDialog -ApplicationWindow{ - property var profile + property var profile - id: userProfileDialog - height: 650 - width: 420 - minimumHeight: 420 + height: 650 + width: 420 + minimumHeight: 420 + palette: colors - palette: colors + Component { + id: deviceVerificationDialog - Component { - id: deviceVerificationDialog - DeviceVerification {} - } + DeviceVerification { + } - ColumnLayout{ - id: contentL + } - anchors.fill: parent - anchors.margins: 10 + ColumnLayout { + id: contentL - spacing: 10 + anchors.fill: parent + anchors.margins: 10 + spacing: 10 - Avatar { - url: profile.avatarUrl.replace("mxc://", "image://MxcImage/") - height: 130 - width: 130 - displayName: profile.displayName - userid: profile.userid - Layout.alignment: Qt.AlignHCenter - } + Avatar { + url: profile.avatarUrl.replace("mxc://", "image://MxcImage/") + height: 130 + width: 130 + displayName: profile.displayName + userid: profile.userid + Layout.alignment: Qt.AlignHCenter + } - Label { - text: profile.displayName - fontSizeMode: Text.HorizontalFit - font.pixelSize: 20 - color: TimelineManager.userColor(profile.userid, colors.window) - font.bold: true - Layout.alignment: Qt.AlignHCenter - } + Label { + text: profile.displayName + fontSizeMode: Text.HorizontalFit + font.pixelSize: 20 + color: TimelineManager.userColor(profile.userid, colors.window) + font.bold: true + Layout.alignment: Qt.AlignHCenter + } - MatrixText { - text: profile.userid - font.pixelSize: 15 - Layout.alignment: Qt.AlignHCenter - } + MatrixText { + text: profile.userid + font.pixelSize: 15 + Layout.alignment: Qt.AlignHCenter + } - Button { - id: verifyUserButton - text: qsTr("Verify") - Layout.alignment: Qt.AlignHCenter - enabled: !profile.isUserVerified - visible: !profile.isUserVerified + Button { + id: verifyUserButton - onClicked: profile.verify() - } + text: qsTr("Verify") + Layout.alignment: Qt.AlignHCenter + enabled: !profile.isUserVerified + visible: !profile.isUserVerified + onClicked: profile.verify() + } - RowLayout { - Layout.alignment: Qt.AlignHCenter - spacing: 8 + RowLayout { + Layout.alignment: Qt.AlignHCenter + spacing: 8 - ImageButton { - image:":/icons/icons/ui/do-not-disturb-rounded-sign.png" - hoverEnabled: true - ToolTip.visible: hovered - ToolTip.text: qsTr("Ban the user") - onClicked: profile.banUser() - } - // ImageButton{ - // image:":/icons/icons/ui/volume-off-indicator.png" - // Layout.margins: { - // left: 5 - // right: 5 - // } - // ToolTip.visible: hovered - // ToolTip.text: qsTr("Ignore messages from this user") - // onClicked : { - // profile.ignoreUser() - // } - // } - ImageButton{ - image:":/icons/icons/ui/black-bubble-speech.png" - hoverEnabled: true - ToolTip.visible: hovered - ToolTip.text: qsTr("Start a private chat") - onClicked: profile.startChat() - } - ImageButton{ - image:":/icons/icons/ui/round-remove-button.png" - hoverEnabled: true - ToolTip.visible: hovered - ToolTip.text: qsTr("Kick the user") - onClicked: profile.kickUser() - } - } + ImageButton { + image: ":/icons/icons/ui/do-not-disturb-rounded-sign.png" + hoverEnabled: true + ToolTip.visible: hovered + ToolTip.text: qsTr("Ban the user") + onClicked: profile.banUser() + } + // ImageButton{ - ListView{ - id: devicelist + // image:":/icons/icons/ui/volume-off-indicator.png" + // Layout.margins: { + // left: 5 + // right: 5 + // } + // ToolTip.visible: hovered + // ToolTip.text: qsTr("Ignore messages from this user") + // onClicked : { + // profile.ignoreUser() + // } + // } + ImageButton { + image: ":/icons/icons/ui/black-bubble-speech.png" + hoverEnabled: true + ToolTip.visible: hovered + ToolTip.text: qsTr("Start a private chat") + onClicked: profile.startChat() + } - Layout.fillHeight: true - Layout.minimumHeight: 200 - Layout.fillWidth: true + ImageButton { + image: ":/icons/icons/ui/round-remove-button.png" + hoverEnabled: true + ToolTip.visible: hovered + ToolTip.text: qsTr("Kick the user") + onClicked: profile.kickUser() + } - clip: true - spacing: 8 - boundsBehavior: Flickable.StopAtBounds + } - model: profile.deviceList + ListView { + id: devicelist - delegate: RowLayout{ - width: devicelist.width - spacing: 4 + Layout.fillHeight: true + Layout.minimumHeight: 200 + Layout.fillWidth: true + clip: true + spacing: 8 + boundsBehavior: Flickable.StopAtBounds + model: profile.deviceList - ColumnLayout{ - spacing: 0 - Text{ - Layout.fillWidth: true - Layout.alignment: Qt.AlignLeft + delegate: RowLayout { + width: devicelist.width + spacing: 4 - elide: Text.ElideRight - font.bold: true - color: colors.text - text: model.deviceId - } - Text{ - Layout.fillWidth: true - Layout.alignment: Qt.AlignRight + ColumnLayout { + spacing: 0 - elide: Text.ElideRight - color: colors.text - text: model.deviceName - } - } + Text { + Layout.fillWidth: true + Layout.alignment: Qt.AlignLeft + elide: Text.ElideRight + font.bold: true + color: colors.text + text: model.deviceId + } - Image{ - Layout.preferredHeight: 16 - Layout.preferredWidth: 16 + Text { + Layout.fillWidth: true + Layout.alignment: Qt.AlignRight + elide: Text.ElideRight + color: colors.text + text: model.deviceName + } - source: ((model.verificationStatus == VerificationStatus.VERIFIED)?"image://colorimage/:/icons/icons/ui/lock.png?green": - ((model.verificationStatus == VerificationStatus.UNVERIFIED)?"image://colorimage/:/icons/icons/ui/unlock.png?yellow": - "image://colorimage/:/icons/icons/ui/unlock.png?red")) - } - Button{ - id: verifyButton - text: (model.verificationStatus != VerificationStatus.VERIFIED)?"Verify":"Unverify" - onClicked: { - if(model.verificationStatus == VerificationStatus.VERIFIED){ - profile.unverify(model.deviceId) - }else{ - profile.verify(model.deviceId); - } - } - } - } - } - } + } - footer: DialogButtonBox { - standardButtons: DialogButtonBox.Ok + Image { + Layout.preferredHeight: 16 + Layout.preferredWidth: 16 + source: ((model.verificationStatus == VerificationStatus.VERIFIED) ? "image://colorimage/:/icons/icons/ui/lock.png?green" : ((model.verificationStatus == VerificationStatus.UNVERIFIED) ? "image://colorimage/:/icons/icons/ui/unlock.png?yellow" : "image://colorimage/:/icons/icons/ui/unlock.png?red")) + } + + Button { + id: verifyButton + + text: (model.verificationStatus != VerificationStatus.VERIFIED) ? "Verify" : "Unverify" + onClicked: { + if (model.verificationStatus == VerificationStatus.VERIFIED) + profile.unverify(model.deviceId); + else + profile.verify(model.deviceId); + } + } + + } + + } + + } + + footer: DialogButtonBox { + standardButtons: DialogButtonBox.Ok + onAccepted: userProfileDialog.close() + } - onAccepted: userProfileDialog.close() - } } diff --git a/resources/qml/delegates/FileMessage.qml b/resources/qml/delegates/FileMessage.qml index 7a2588f3..c6f213ee 100644 --- a/resources/qml/delegates/FileMessage.qml +++ b/resources/qml/delegates/FileMessage.qml @@ -1,68 +1,75 @@ import QtQuick 2.6 import QtQuick.Layouts 1.2 - import im.nheko 1.0 Item { - height: row.height + 24 - width: parent ? parent.width : undefined + height: row.height + 24 + width: parent ? parent.width : undefined - RowLayout { - id: row + RowLayout { + id: row - anchors.centerIn: parent - width: parent.width - 24 + anchors.centerIn: parent + width: parent.width - 24 + spacing: 15 - spacing: 15 + Rectangle { + id: button - Rectangle { - id: button - color: colors.light - radius: 22 - height: 44 - width: 44 - Image { - id: img - anchors.centerIn: parent + color: colors.light + radius: 22 + height: 44 + width: 44 - source: "qrc:/icons/icons/ui/arrow-pointing-down.png" - fillMode: Image.Pad + Image { + id: img - } - MouseArea { - anchors.fill: parent - onClicked: TimelineManager.timeline.saveMedia(model.data.id) - cursorShape: Qt.PointingHandCursor - } - } - ColumnLayout { - id: col + anchors.centerIn: parent + source: "qrc:/icons/icons/ui/arrow-pointing-down.png" + fillMode: Image.Pad + } - Text { - id: filename - Layout.fillWidth: true - text: model.data.filename - textFormat: Text.PlainText - elide: Text.ElideRight - color: colors.text - } - Text { - id: filesize - Layout.fillWidth: true - text: model.data.filesize - textFormat: Text.PlainText - elide: Text.ElideRight - color: colors.text - } - } - } + MouseArea { + anchors.fill: parent + onClicked: TimelineManager.timeline.saveMedia(model.data.id) + cursorShape: Qt.PointingHandCursor + } - Rectangle { - color: colors.dark - z: -1 - radius: 10 - height: row.height + 24 - width: 44 + 24 + 24 + Math.max(Math.min(filesize.width, filesize.implicitWidth), Math.min(filename.width, filename.implicitWidth)) - } + } + + ColumnLayout { + id: col + + Text { + id: filename + + Layout.fillWidth: true + text: model.data.filename + textFormat: Text.PlainText + elide: Text.ElideRight + color: colors.text + } + + Text { + id: filesize + + Layout.fillWidth: true + text: model.data.filesize + textFormat: Text.PlainText + elide: Text.ElideRight + color: colors.text + } + + } + + } + + Rectangle { + color: colors.dark + z: -1 + radius: 10 + height: row.height + 24 + width: 44 + 24 + 24 + Math.max(Math.min(filesize.width, filesize.implicitWidth), Math.min(filename.width, filename.implicitWidth)) + } } diff --git a/resources/qml/delegates/ImageMessage.qml b/resources/qml/delegates/ImageMessage.qml index b5c51c2c..e2c78fbe 100644 --- a/resources/qml/delegates/ImageMessage.qml +++ b/resources/qml/delegates/ImageMessage.qml @@ -1,42 +1,41 @@ import QtQuick 2.6 - import im.nheko 1.0 Item { - property double tempWidth: Math.min(parent ? parent.width : undefined, model.data.width < 1 ? parent.width : model.data.width) - property double tempHeight: tempWidth * model.data.proportionalHeight + property double tempWidth: Math.min(parent ? parent.width : undefined, model.data.width < 1 ? parent.width : model.data.width) + property double tempHeight: tempWidth * model.data.proportionalHeight + property double divisor: model.isReply ? 4 : 2 + property bool tooHigh: tempHeight > timelineRoot.height / divisor - property double divisor: model.isReply ? 4 : 2 - property bool tooHigh: tempHeight > timelineRoot.height / divisor + height: Math.round(tooHigh ? timelineRoot.height / divisor : tempHeight) + width: Math.round(tooHigh ? (timelineRoot.height / divisor) / model.data.proportionalHeight : tempWidth) - height: Math.round(tooHigh ? timelineRoot.height / divisor : tempHeight) - width: Math.round(tooHigh ? (timelineRoot.height / divisor) / model.data.proportionalHeight : tempWidth) + Image { + id: blurhash - Image { - id: blurhash - anchors.fill: parent - visible: img.status != Image.Ready + anchors.fill: parent + visible: img.status != Image.Ready + source: model.data.blurhash ? ("image://blurhash/" + model.data.blurhash) : ("image://colorimage/:/icons/icons/ui/do-not-disturb-rounded-sign@2x.png?" + colors.buttonText) + asynchronous: true + fillMode: Image.PreserveAspectFit + sourceSize.width: parent.width + sourceSize.height: parent.height + } - source: model.data.blurhash ? ("image://blurhash/" + model.data.blurhash) : ("image://colorimage/:/icons/icons/ui/do-not-disturb-rounded-sign@2x.png?"+colors.buttonText) - asynchronous: true - fillMode: Image.PreserveAspectFit + Image { + id: img - sourceSize.width: parent.width - sourceSize.height: parent.height - } + anchors.fill: parent + source: model.data.url.replace("mxc://", "image://MxcImage/") + asynchronous: true + fillMode: Image.PreserveAspectFit - Image { - id: img - anchors.fill: parent + MouseArea { + enabled: model.data.type == MtxEvent.ImageMessage && img.status == Image.Ready + anchors.fill: parent + onClicked: TimelineManager.openImageOverlay(model.data.url, model.data.id) + } - source: model.data.url.replace("mxc://", "image://MxcImage/") - asynchronous: true - fillMode: Image.PreserveAspectFit + } - MouseArea { - enabled: model.data.type == MtxEvent.ImageMessage && img.status == Image.Ready - anchors.fill: parent - onClicked: TimelineManager.openImageOverlay(model.data.url, model.data.id) - } - } } diff --git a/resources/qml/delegates/MessageDelegate.qml b/resources/qml/delegates/MessageDelegate.qml index a5cb2ae7..cb5d8a95 100644 --- a/resources/qml/delegates/MessageDelegate.qml +++ b/resources/qml/delegates/MessageDelegate.qml @@ -2,215 +2,334 @@ import QtQuick 2.6 import im.nheko 1.0 Item { - // Workaround to have an assignable global property - Item { - id: model - property var data; - property bool isReply: false - } - - property alias modelData: model.data - property alias isReply: model.isReply + property alias modelData: model.data + property alias isReply: model.isReply + property real implicitWidth: (chooser.child && chooser.child.implicitWidth) ? chooser.child.implicitWidth : width - height: chooser.childrenRect.height - property real implicitWidth: (chooser.child && chooser.child.implicitWidth) ? chooser.child.implicitWidth : width + height: chooser.childrenRect.height - DelegateChooser { - id: chooser - //role: "type" //< not supported in our custom implementation, have to use roleValue - roleValue: model.data.type - anchors.fill: parent + // Workaround to have an assignable global property + Item { + id: model + + property var data + property bool isReply: false + } + + DelegateChooser { + id: chooser + + //role: "type" //< not supported in our custom implementation, have to use roleValue + roleValue: model.data.type + anchors.fill: parent + + DelegateChoice { + roleValue: MtxEvent.UnknownMessage + + Placeholder { + text: "Unretrieved event" + } + + } + + DelegateChoice { + roleValue: MtxEvent.TextMessage + + TextMessage { + } + + } + + DelegateChoice { + roleValue: MtxEvent.NoticeMessage + + NoticeMessage { + } + + } + + DelegateChoice { + roleValue: MtxEvent.EmoteMessage + + NoticeMessage { + formatted: TimelineManager.escapeEmoji(modelData.userName) + " " + model.data.formattedBody + color: TimelineManager.userColor(modelData.userId, colors.window) + } + + } + + DelegateChoice { + roleValue: MtxEvent.ImageMessage + + ImageMessage { + } + + } + + DelegateChoice { + roleValue: MtxEvent.Sticker + + ImageMessage { + } + + } + + DelegateChoice { + roleValue: MtxEvent.FileMessage + + FileMessage { + } + + } + + DelegateChoice { + roleValue: MtxEvent.VideoMessage + + PlayableMediaMessage { + } + + } + + DelegateChoice { + roleValue: MtxEvent.AudioMessage + + PlayableMediaMessage { + } + + } + + DelegateChoice { + roleValue: MtxEvent.Redacted + + Pill { + text: qsTr("redacted") + } + + } + + DelegateChoice { + roleValue: MtxEvent.Redaction + + Pill { + text: qsTr("redacted") + } + + } + + DelegateChoice { + roleValue: MtxEvent.Encryption + + Pill { + text: qsTr("Encryption enabled") + } + + } + + DelegateChoice { + roleValue: MtxEvent.Name + + NoticeMessage { + text: model.data.roomName ? qsTr("room name changed to: %1").arg(model.data.roomName) : qsTr("removed room name") + } + + } + + DelegateChoice { + roleValue: MtxEvent.Topic + + NoticeMessage { + text: model.data.roomTopic ? qsTr("topic changed to: %1").arg(model.data.roomTopic) : qsTr("removed topic") + } + + } + + DelegateChoice { + roleValue: MtxEvent.RoomCreate + + NoticeMessage { + text: qsTr("%1 created and configured room: %2").arg(model.data.userName).arg(model.data.roomId) + } + + } + + DelegateChoice { + roleValue: MtxEvent.CallInvite + + NoticeMessage { + text: { + switch (model.data.callType) { + case "voice": + return qsTr("%1 placed a voice call.").arg(model.data.userName); + case "video": + return qsTr("%1 placed a video call.").arg(model.data.userName); + default: + return qsTr("%1 placed a call.").arg(model.data.userName); + } + } + } + + } + + DelegateChoice { + roleValue: MtxEvent.CallAnswer + + NoticeMessage { + text: qsTr("%1 answered the call.").arg(model.data.userName) + } + + } + + DelegateChoice { + roleValue: MtxEvent.CallHangUp + + NoticeMessage { + text: qsTr("%1 ended the call.").arg(model.data.userName) + } + + } + + DelegateChoice { + roleValue: MtxEvent.CallCandidates + + NoticeMessage { + text: qsTr("Negotiating call...") + } + + } + + DelegateChoice { + // TODO: make a more complex formatter for the power levels. + roleValue: MtxEvent.PowerLevels + + NoticeMessage { + text: TimelineManager.timeline.formatPowerLevelEvent(model.data.id) + } + + } + + DelegateChoice { + roleValue: MtxEvent.RoomJoinRules + + NoticeMessage { + text: TimelineManager.timeline.formatJoinRuleEvent(model.data.id) + } + + } + + DelegateChoice { + roleValue: MtxEvent.RoomHistoryVisibility + + NoticeMessage { + text: TimelineManager.timeline.formatHistoryVisibilityEvent(model.data.id) + } + + } + + DelegateChoice { + roleValue: MtxEvent.RoomGuestAccess + + NoticeMessage { + text: TimelineManager.timeline.formatGuestAccessEvent(model.data.id) + } + + } + + DelegateChoice { + roleValue: MtxEvent.Member + + NoticeMessage { + text: TimelineManager.timeline.formatMemberEvent(model.data.id) + } + + } + + DelegateChoice { + roleValue: MtxEvent.KeyVerificationRequest + + NoticeMessage { + text: "KeyVerificationRequest" + } + + } + + DelegateChoice { + roleValue: MtxEvent.KeyVerificationStart + + NoticeMessage { + text: "KeyVerificationStart" + } + + } + + DelegateChoice { + roleValue: MtxEvent.KeyVerificationReady + + NoticeMessage { + text: "KeyVerificationReady" + } + + } + + DelegateChoice { + roleValue: MtxEvent.KeyVerificationCancel + + NoticeMessage { + text: "KeyVerificationCancel" + } + + } + + DelegateChoice { + roleValue: MtxEvent.KeyVerificationKey + + NoticeMessage { + text: "KeyVerificationKey" + } + + } + + DelegateChoice { + roleValue: MtxEvent.KeyVerificationMac + + NoticeMessage { + text: "KeyVerificationMac" + } + + } + + DelegateChoice { + roleValue: MtxEvent.KeyVerificationDone + + NoticeMessage { + text: "KeyVerificationDone" + } + + } + + DelegateChoice { + roleValue: MtxEvent.KeyVerificationDone + + NoticeMessage { + text: "KeyVerificationDone" + } + + } + + DelegateChoice { + roleValue: MtxEvent.KeyVerificationAccept + + NoticeMessage { + text: "KeyVerificationAccept" + } + + } + + DelegateChoice { + Placeholder { + } + + } + + } - DelegateChoice { - roleValue: MtxEvent.UnknownMessage - Placeholder { text: "Unretrieved event" } - } - DelegateChoice { - roleValue: MtxEvent.TextMessage - TextMessage {} - } - DelegateChoice { - roleValue: MtxEvent.NoticeMessage - NoticeMessage {} - } - DelegateChoice { - roleValue: MtxEvent.EmoteMessage - NoticeMessage { - formatted: TimelineManager.escapeEmoji(modelData.userName) + " " + model.data.formattedBody - color: TimelineManager.userColor(modelData.userId, colors.window) - } - } - DelegateChoice { - roleValue: MtxEvent.ImageMessage - ImageMessage {} - } - DelegateChoice { - roleValue: MtxEvent.Sticker - ImageMessage {} - } - DelegateChoice { - roleValue: MtxEvent.FileMessage - FileMessage {} - } - DelegateChoice { - roleValue: MtxEvent.VideoMessage - PlayableMediaMessage {} - } - DelegateChoice { - roleValue: MtxEvent.AudioMessage - PlayableMediaMessage {} - } - DelegateChoice { - roleValue: MtxEvent.Redacted - Pill { - text: qsTr("redacted") - } - } - DelegateChoice { - roleValue: MtxEvent.Redaction - Pill { - text: qsTr("redacted") - } - } - DelegateChoice { - roleValue: MtxEvent.Encryption - Pill { - text: qsTr("Encryption enabled") - } - } - DelegateChoice { - roleValue: MtxEvent.Name - NoticeMessage { - text: model.data.roomName ? qsTr("room name changed to: %1").arg(model.data.roomName) : qsTr("removed room name") - } - } - DelegateChoice { - roleValue: MtxEvent.Topic - NoticeMessage { - text: model.data.roomTopic ? qsTr("topic changed to: %1").arg(model.data.roomTopic) : qsTr("removed topic") - } - } - DelegateChoice { - roleValue: MtxEvent.RoomCreate - NoticeMessage { - text: qsTr("%1 created and configured room: %2").arg(model.data.userName).arg(model.data.roomId) - } - } - DelegateChoice { - roleValue: MtxEvent.CallInvite - NoticeMessage { - text: switch(model.data.callType) { - case "voice": return qsTr("%1 placed a voice call.").arg(model.data.userName) - case "video": return qsTr("%1 placed a video call.").arg(model.data.userName) - default: return qsTr("%1 placed a call.").arg(model.data.userName) - } - } - } - DelegateChoice { - roleValue: MtxEvent.CallAnswer - NoticeMessage { - text: qsTr("%1 answered the call.").arg(model.data.userName) - } - } - DelegateChoice { - roleValue: MtxEvent.CallHangUp - NoticeMessage { - text: qsTr("%1 ended the call.").arg(model.data.userName) - } - } - DelegateChoice { - roleValue: MtxEvent.CallCandidates - NoticeMessage { - text: qsTr("Negotiating call...") - } - } - DelegateChoice { - // TODO: make a more complex formatter for the power levels. - roleValue: MtxEvent.PowerLevels - NoticeMessage { - text: TimelineManager.timeline.formatPowerLevelEvent(model.data.id) - } - } - DelegateChoice { - roleValue: MtxEvent.RoomJoinRules - NoticeMessage { - text: TimelineManager.timeline.formatJoinRuleEvent(model.data.id) - } - } - DelegateChoice { - roleValue: MtxEvent.RoomHistoryVisibility - NoticeMessage { - text: TimelineManager.timeline.formatHistoryVisibilityEvent(model.data.id) - } - } - DelegateChoice { - roleValue: MtxEvent.RoomGuestAccess - NoticeMessage { - text: TimelineManager.timeline.formatGuestAccessEvent(model.data.id) - } - } - DelegateChoice { - roleValue: MtxEvent.Member - NoticeMessage { - text: TimelineManager.timeline.formatMemberEvent(model.data.id); - } - } - DelegateChoice { - roleValue: MtxEvent.KeyVerificationRequest - NoticeMessage { - text: "KeyVerificationRequest"; - } - } - DelegateChoice { - roleValue: MtxEvent.KeyVerificationStart - NoticeMessage { - text: "KeyVerificationStart"; - } - } - DelegateChoice { - roleValue: MtxEvent.KeyVerificationReady - NoticeMessage { - text: "KeyVerificationReady"; - } - } - DelegateChoice { - roleValue: MtxEvent.KeyVerificationCancel - NoticeMessage { - text: "KeyVerificationCancel"; - } - } - DelegateChoice { - roleValue: MtxEvent.KeyVerificationKey - NoticeMessage { - text: "KeyVerificationKey"; - } - } - DelegateChoice { - roleValue: MtxEvent.KeyVerificationMac - NoticeMessage { - text: "KeyVerificationMac"; - } - } - DelegateChoice { - roleValue: MtxEvent.KeyVerificationDone - NoticeMessage { - text: "KeyVerificationDone"; - } - } - DelegateChoice { - roleValue: MtxEvent.KeyVerificationDone - NoticeMessage { - text: "KeyVerificationDone"; - } - } - DelegateChoice { - roleValue: MtxEvent.KeyVerificationAccept - NoticeMessage { - text: "KeyVerificationAccept"; - } - } - DelegateChoice { - Placeholder {} - } - } } diff --git a/resources/qml/delegates/NoticeMessage.qml b/resources/qml/delegates/NoticeMessage.qml index be348329..d9a7a3e7 100644 --- a/resources/qml/delegates/NoticeMessage.qml +++ b/resources/qml/delegates/NoticeMessage.qml @@ -1,6 +1,6 @@ TextMessage { - font.italic: true - color: colors.buttonText - height: isReply ? Math.min(chat.height / 8, implicitHeight) : undefined - clip: true + font.italic: true + color: colors.buttonText + height: isReply ? Math.min(chat.height / 8, implicitHeight) : undefined + clip: true } diff --git a/resources/qml/delegates/Pill.qml b/resources/qml/delegates/Pill.qml index 27985b58..4acf2bef 100644 --- a/resources/qml/delegates/Pill.qml +++ b/resources/qml/delegates/Pill.qml @@ -2,13 +2,14 @@ import QtQuick 2.5 import QtQuick.Controls 2.1 Label { - color: colors.brightText - horizontalAlignment: Text.AlignHCenter + color: colors.brightText + horizontalAlignment: Text.AlignHCenter + height: contentHeight * 1.2 + width: contentWidth * 1.2 + + background: Rectangle { + radius: parent.height / 2 + color: colors.dark + } - height: contentHeight * 1.2 - width: contentWidth * 1.2 - background: Rectangle { - radius: parent.height / 2 - color: colors.dark - } } diff --git a/resources/qml/delegates/Placeholder.qml b/resources/qml/delegates/Placeholder.qml index 26de2067..db023d8a 100644 --- a/resources/qml/delegates/Placeholder.qml +++ b/resources/qml/delegates/Placeholder.qml @@ -1,7 +1,7 @@ import ".." MatrixText { - text: qsTr("unimplemented event: ") + model.data.typeString - width: parent ? parent.width : undefined - color: inactiveColors.text + text: qsTr("unimplemented event: ") + model.data.typeString + width: parent ? parent.width : undefined + color: inactiveColors.text } diff --git a/resources/qml/delegates/PlayableMediaMessage.qml b/resources/qml/delegates/PlayableMediaMessage.qml index 893325b6..9ad115c7 100644 --- a/resources/qml/delegates/PlayableMediaMessage.qml +++ b/resources/qml/delegates/PlayableMediaMessage.qml @@ -1,173 +1,216 @@ -import QtQuick 2.6 -import QtQuick.Layouts 1.2 -import QtQuick.Controls 2.1 import QtMultimedia 5.6 - +import QtQuick 2.6 +import QtQuick.Controls 2.1 +import QtQuick.Layouts 1.2 import im.nheko 1.0 Rectangle { - id: bg - radius: 10 - color: colors.dark - height: Math.round(content.height + 24) - width: parent ? parent.width : undefined + id: bg - Column { - id: content - width: parent.width - 24 - anchors.centerIn: parent + radius: 10 + color: colors.dark + height: Math.round(content.height + 24) + width: parent ? parent.width : undefined - Rectangle { - id: videoContainer - visible: model.data.type == MtxEvent.VideoMessage - property double tempWidth: Math.min(parent ? parent.width : undefined, model.data.width < 1 ? 400 : model.data.width) - property double tempHeight: tempWidth * model.data.proportionalHeight + Column { + id: content - property double divisor: model.isReply ? 4 : 2 - property bool tooHigh: tempHeight > timelineRoot.height / divisor + width: parent.width - 24 + anchors.centerIn: parent - height: tooHigh ? timelineRoot.height / divisor : tempHeight - width: tooHigh ? (timelineRoot.height / divisor) / model.data.proportionalHeight : tempWidth - Image { - anchors.fill: parent - source: model.data.thumbnailUrl.replace("mxc://", "image://MxcImage/") - asynchronous: true - fillMode: Image.PreserveAspectFit + Rectangle { + id: videoContainer - VideoOutput { - anchors.fill: parent - fillMode: VideoOutput.PreserveAspectFit - source: media - } - } - } + property double tempWidth: Math.min(parent ? parent.width : undefined, model.data.width < 1 ? 400 : model.data.width) + property double tempHeight: tempWidth * model.data.proportionalHeight + property double divisor: model.isReply ? 4 : 2 + property bool tooHigh: tempHeight > timelineRoot.height / divisor - RowLayout { - width: parent.width - Text { - id: positionText - text: "--:--:--" - color: colors.text - } - Slider { - Layout.fillWidth: true - id: progress - value: media.position - from: 0 - to: media.duration + visible: model.data.type == MtxEvent.VideoMessage + height: tooHigh ? timelineRoot.height / divisor : tempHeight + width: tooHigh ? (timelineRoot.height / divisor) / model.data.proportionalHeight : tempWidth - onMoved: media.seek(value) - //indeterminate: true - function updatePositionTexts() { - function formatTime(date) { - var hh = date.getUTCHours(); - var mm = date.getUTCMinutes(); - var ss = date.getSeconds(); - if (hh < 10) {hh = "0"+hh;} - if (mm < 10) {mm = "0"+mm;} - if (ss < 10) {ss = "0"+ss;} - return hh+":"+mm+":"+ss; - } - positionText.text = formatTime(new Date(media.position)) - durationText.text = formatTime(new Date(media.duration)) - } - onValueChanged: updatePositionTexts() + Image { + anchors.fill: parent + source: model.data.thumbnailUrl.replace("mxc://", "image://MxcImage/") + asynchronous: true + fillMode: Image.PreserveAspectFit - palette: colors - } - Text { - id: durationText - text: "--:--:--" - color: colors.text - } - } + VideoOutput { + anchors.fill: parent + fillMode: VideoOutput.PreserveAspectFit + source: media + } - RowLayout { - width: parent.width + } - spacing: 15 + } - Rectangle { - id: button - color: colors.window - radius: 22 - height: 44 - width: 44 - Image { - id: img - anchors.centerIn: parent - z: 3 + RowLayout { + width: parent.width - source: "image://colorimage/:/icons/icons/ui/arrow-pointing-down.png?"+colors.text - fillMode: Image.Pad + Text { + id: positionText - } - MouseArea { - anchors.fill: parent - onClicked: { - switch (button.state) { - case "": TimelineManager.timeline.cacheMedia(model.data.id); break; - case "stopped": - media.play(); console.log("play"); - button.state = "playing" - break - case "playing": - media.pause(); console.log("pause"); - button.state = "stopped" - break - } - } - cursorShape: Qt.PointingHandCursor - } - MediaPlayer { - id: media - onError: console.log(errorString) - onStatusChanged: if(status == MediaPlayer.Loaded) progress.updatePositionTexts() - onStopped: button.state = "stopped" - } + text: "--:--:--" + color: colors.text + } - Connections { - target: TimelineManager.timeline - onMediaCached: { - if (mxcUrl == model.data.url) { - media.source = "file://" + cacheUrl - button.state = "stopped" - console.log("media loaded: " + mxcUrl + " at " + cacheUrl) - } - console.log("media cached: " + mxcUrl + " at " + cacheUrl) - } - } + Slider { + id: progress - states: [ - State { - name: "stopped" - PropertyChanges { target: img; source: "image://colorimage/:/icons/icons/ui/play-sign.png?"+colors.text } - }, - State { - name: "playing" - PropertyChanges { target: img; source: "image://colorimage/:/icons/icons/ui/pause-symbol.png?"+colors.text } - } - ] - } - ColumnLayout { - id: col + //indeterminate: true + function updatePositionTexts() { + function formatTime(date) { + var hh = date.getUTCHours(); + var mm = date.getUTCMinutes(); + var ss = date.getSeconds(); + if (hh < 10) + hh = "0" + hh; + + if (mm < 10) + mm = "0" + mm; + + if (ss < 10) + ss = "0" + ss; + + return hh + ":" + mm + ":" + ss; + } + + positionText.text = formatTime(new Date(media.position)); + durationText.text = formatTime(new Date(media.duration)); + } + + Layout.fillWidth: true + value: media.position + from: 0 + to: media.duration + onMoved: media.seek(value) + onValueChanged: updatePositionTexts() + palette: colors + } + + Text { + id: durationText + + text: "--:--:--" + color: colors.text + } + + } + + RowLayout { + width: parent.width + spacing: 15 + + Rectangle { + id: button + + color: colors.window + radius: 22 + height: 44 + width: 44 + states: [ + State { + name: "stopped" + + PropertyChanges { + target: img + source: "image://colorimage/:/icons/icons/ui/play-sign.png?" + colors.text + } + + }, + State { + name: "playing" + + PropertyChanges { + target: img + source: "image://colorimage/:/icons/icons/ui/pause-symbol.png?" + colors.text + } + + } + ] + + Image { + id: img + + anchors.centerIn: parent + z: 3 + source: "image://colorimage/:/icons/icons/ui/arrow-pointing-down.png?" + colors.text + fillMode: Image.Pad + } + + MouseArea { + anchors.fill: parent + onClicked: { + switch (button.state) { + case "": + TimelineManager.timeline.cacheMedia(model.data.id); + break; + case "stopped": + media.play(); + console.log("play"); + button.state = "playing"; + break; + case "playing": + media.pause(); + console.log("pause"); + button.state = "stopped"; + break; + } + } + cursorShape: Qt.PointingHandCursor + } + + MediaPlayer { + id: media + + onError: console.log(errorString) + onStatusChanged: { + if (status == MediaPlayer.Loaded) + progress.updatePositionTexts(); + + } + onStopped: button.state = "stopped" + } + + Connections { + target: TimelineManager.timeline + onMediaCached: { + if (mxcUrl == model.data.url) { + media.source = "file://" + cacheUrl; + button.state = "stopped"; + console.log("media loaded: " + mxcUrl + " at " + cacheUrl); + } + console.log("media cached: " + mxcUrl + " at " + cacheUrl); + } + } + + } + + ColumnLayout { + id: col + + Text { + Layout.fillWidth: true + text: model.data.body + textFormat: Text.PlainText + elide: Text.ElideRight + color: colors.text + } + + Text { + Layout.fillWidth: true + text: model.data.filesize + textFormat: Text.PlainText + elide: Text.ElideRight + color: colors.text + } + + } + + } + + } - Text { - Layout.fillWidth: true - text: model.data.body - textFormat: Text.PlainText - elide: Text.ElideRight - color: colors.text - } - Text { - Layout.fillWidth: true - text: model.data.filesize - textFormat: Text.PlainText - elide: Text.ElideRight - color: colors.text - } - } - } - } } - diff --git a/resources/qml/delegates/Reply.qml b/resources/qml/delegates/Reply.qml index 43fc2814..1471dbdf 100644 --- a/resources/qml/delegates/Reply.qml +++ b/resources/qml/delegates/Reply.qml @@ -2,66 +2,71 @@ import QtQuick 2.6 import QtQuick.Controls 2.3 import QtQuick.Layouts 1.2 import QtQuick.Window 2.2 - import im.nheko 1.0 Item { - id: replyComponent + id: replyComponent - property alias modelData: reply.modelData - property color userColor: "red" + property alias modelData: reply.modelData + property color userColor: "red" - width: parent.width - height: replyContainer.height + width: parent.width + height: replyContainer.height - MouseArea { - anchors.fill: parent - preventStealing: true - onClicked: chat.positionViewAtIndex(chat.model.idToIndex(modelData.id), ListView.Contain) - cursorShape: Qt.PointingHandCursor - } + MouseArea { + anchors.fill: parent + preventStealing: true + onClicked: chat.positionViewAtIndex(chat.model.idToIndex(modelData.id), ListView.Contain) + cursorShape: Qt.PointingHandCursor + } - Rectangle { - id: colorLine + Rectangle { + id: colorLine - anchors.top: replyContainer.top - anchors.bottom: replyContainer.bottom - width: 4 + anchors.top: replyContainer.top + anchors.bottom: replyContainer.bottom + width: 4 + color: TimelineManager.userColor(reply.modelData.userId, colors.window) + } - color: TimelineManager.userColor(reply.modelData.userId, colors.window) - } + Column { + id: replyContainer - Column { - id: replyContainer - anchors.left: colorLine.right - anchors.leftMargin: 4 - width: parent.width - 8 + anchors.left: colorLine.right + anchors.leftMargin: 4 + width: parent.width - 8 - Text { - id: userName - text: TimelineManager.escapeEmoji(reply.modelData.userName) - color: replyComponent.userColor - textFormat: Text.RichText + Text { + id: userName - MouseArea { - anchors.fill: parent - onClicked: chat.model.openUserProfile(reply.modelData.userId) - cursorShape: Qt.PointingHandCursor - } - } + text: TimelineManager.escapeEmoji(reply.modelData.userName) + color: replyComponent.userColor + textFormat: Text.RichText - MessageDelegate { - id: reply - width: parent.width - isReply: true - } - } + MouseArea { + anchors.fill: parent + onClicked: chat.model.openUserProfile(reply.modelData.userId) + cursorShape: Qt.PointingHandCursor + } + + } + + MessageDelegate { + id: reply + + width: parent.width + isReply: true + } + + } + + Rectangle { + id: backgroundItem + + z: -1 + height: replyContainer.height + width: Math.min(Math.max(reply.implicitWidth, userName.implicitWidth) + 8 + 4, parent.width) + color: Qt.rgba(userColor.r, userColor.g, userColor.b, 0.2) + } - Rectangle { - id: backgroundItem - z: -1 - height: replyContainer.height - width: Math.min(Math.max(reply.implicitWidth, userName.implicitWidth) + 8 + 4, parent.width) - color: Qt.rgba(userColor.r, userColor.g, userColor.b, 0.2) - } } diff --git a/resources/qml/delegates/TextMessage.qml b/resources/qml/delegates/TextMessage.qml index 99ff9329..69f2f0e3 100644 --- a/resources/qml/delegates/TextMessage.qml +++ b/resources/qml/delegates/TextMessage.qml @@ -1,12 +1,12 @@ import ".." - import im.nheko 1.0 MatrixText { - property string formatted: model.data.formattedBody - text: "" + formatted.replace("
", "") - width: parent ? parent.width : undefined - height: isReply ? Math.round(Math.min(timelineRoot.height / 8, implicitHeight)) : undefined - clip: true - font.pointSize: (Settings.enlargeEmojiOnlyMessages && model.data.isOnlyEmoji > 0 && model.data.isOnlyEmoji < 4) ? Settings.fontSize * 3 : Settings.fontSize + property string formatted: model.data.formattedBody + + text: "" + formatted.replace("", "") + width: parent ? parent.width : undefined + height: isReply ? Math.round(Math.min(timelineRoot.height / 8, implicitHeight)) : undefined + clip: true + font.pointSize: (Settings.enlargeEmojiOnlyMessages && model.data.isOnlyEmoji > 0 && model.data.isOnlyEmoji < 4) ? Settings.fontSize * 3 : Settings.fontSize } diff --git a/resources/qml/device-verification/AwaitingVerificationConfirmation.qml b/resources/qml/device-verification/AwaitingVerificationConfirmation.qml index cd8ccfd9..42bd68df 100644 --- a/resources/qml/device-verification/AwaitingVerificationConfirmation.qml +++ b/resources/qml/device-verification/AwaitingVerificationConfirmation.qml @@ -1,39 +1,46 @@ import QtQuick 2.3 import QtQuick.Controls 2.10 import QtQuick.Layouts 1.10 - import im.nheko 1.0 Pane { - property string title: qsTr("Awaiting Confirmation") - ColumnLayout { - spacing: 16 - Label { - Layout.maximumWidth: 400 - Layout.fillHeight: true - Layout.fillWidth: true - wrapMode: Text.Wrap - id: content - text: qsTr("Waiting for other side to complete verification.") - color:colors.text - verticalAlignment: Text.AlignVCenter - } - BusyIndicator { - Layout.alignment: Qt.AlignHCenter - } - RowLayout { - Button { - Layout.alignment: Qt.AlignLeft - text: qsTr("Cancel") + property string title: qsTr("Awaiting Confirmation") + + ColumnLayout { + spacing: 16 + + Label { + id: content + + Layout.maximumWidth: 400 + Layout.fillHeight: true + Layout.fillWidth: true + wrapMode: Text.Wrap + text: qsTr("Waiting for other side to complete verification.") + color: colors.text + verticalAlignment: Text.AlignVCenter + } + + BusyIndicator { + Layout.alignment: Qt.AlignHCenter + } + + RowLayout { + Button { + Layout.alignment: Qt.AlignLeft + text: qsTr("Cancel") + onClicked: { + flow.cancel(); + dialog.close(); + } + } + + Item { + Layout.fillWidth: true + } + + } + + } - onClicked: { - flow.cancel(); - dialog.close(); - } - } - Item { - Layout.fillWidth: true - } - } - } } diff --git a/resources/qml/device-verification/DeviceVerification.qml b/resources/qml/device-verification/DeviceVerification.qml index 2e8f7504..5009cc3a 100644 --- a/resources/qml/device-verification/DeviceVerification.qml +++ b/resources/qml/device-verification/DeviceVerification.qml @@ -1,97 +1,144 @@ import QtQuick 2.10 import QtQuick.Controls 2.10 import QtQuick.Window 2.10 - import im.nheko 1.0 ApplicationWindow { - property var flow + id: dialog - onClosing: TimelineManager.removeVerificationFlow(flow) + property var flow - title: stack.currentItem.title - id: dialog + onClosing: TimelineManager.removeVerificationFlow(flow) + title: stack.currentItem.title + flags: Qt.Dialog + palette: colors + height: stack.implicitHeight + width: stack.implicitWidth - flags: Qt.Dialog + StackView { + id: stack - palette: colors + initialItem: newVerificationRequest + implicitWidth: currentItem.implicitWidth + implicitHeight: currentItem.implicitHeight + } - height: stack.implicitHeight - width: stack.implicitWidth + Component { + id: newVerificationRequest - StackView { - id: stack - initialItem: newVerificationRequest - implicitWidth: currentItem.implicitWidth - implicitHeight: currentItem.implicitHeight - } + NewVerificationRequest { + } - Component{ - id: newVerificationRequest - NewVerificationRequest {} - } + } - Component { - id: waiting - Waiting {} - } + Component { + id: waiting - Component { - id: success - Success {} - } + Waiting { + } - Component { - id: failed - Failed {} - } + } - Component { - id: digitVerification - DigitVerification {} - } + Component { + id: success - Component { - id: emojiVerification - EmojiVerification {} - } + Success { + } - Item { - state: flow.state + } + + Component { + id: failed + + Failed { + } + + } + + Component { + id: digitVerification + + DigitVerification { + } + + } + + Component { + id: emojiVerification + + EmojiVerification { + } + + } + + Item { + state: flow.state + states: [ + State { + name: "PromptStartVerification" + + StateChangeScript { + script: stack.replace(newVerificationRequest) + } + + }, + State { + name: "CompareEmoji" + + StateChangeScript { + script: stack.replace(emojiVerification) + } + + }, + State { + name: "CompareNumber" + + StateChangeScript { + script: stack.replace(digitVerification) + } + + }, + State { + name: "WaitingForKeys" + + StateChangeScript { + script: stack.replace(waiting) + } + + }, + State { + name: "WaitingForOtherToAccept" + + StateChangeScript { + script: stack.replace(waiting) + } + + }, + State { + name: "WaitingForMac" + + StateChangeScript { + script: stack.replace(waiting) + } + + }, + State { + name: "Success" + + StateChangeScript { + script: stack.replace(success) + } + + }, + State { + name: "Failed" + + StateChangeScript { + script: stack.replace(failed) + } + + } + ] + } - states: [ - State { - name: "PromptStartVerification" - StateChangeScript { script: stack.replace(newVerificationRequest) } - }, - State { - name: "CompareEmoji" - StateChangeScript { script: stack.replace(emojiVerification) } - }, - State { - name: "CompareNumber" - StateChangeScript { script: stack.replace(digitVerification) } - }, - State { - name: "WaitingForKeys" - StateChangeScript { script: stack.replace(waiting) } - }, - State { - name: "WaitingForOtherToAccept" - StateChangeScript { script: stack.replace(waiting) } - }, - State { - name: "WaitingForMac" - StateChangeScript { script: stack.replace(waiting) } - }, - State { - name: "Success" - StateChangeScript { script: stack.replace(success) } - }, - State { - name: "Failed" - StateChangeScript { script: stack.replace(failed); } - } - ] -} } diff --git a/resources/qml/device-verification/DigitVerification.qml b/resources/qml/device-verification/DigitVerification.qml index ff878a50..11c32d26 100644 --- a/resources/qml/device-verification/DigitVerification.qml +++ b/resources/qml/device-verification/DigitVerification.qml @@ -1,60 +1,69 @@ import QtQuick 2.3 import QtQuick.Controls 2.10 import QtQuick.Layouts 1.10 - import im.nheko 1.0 Pane { - property string title: qsTr("Verification Code") + property string title: qsTr("Verification Code") - ColumnLayout { - spacing: 16 - Label { - Layout.maximumWidth: 400 - Layout.fillHeight: true - Layout.fillWidth: true - wrapMode: Text.Wrap - text: qsTr("Please verify the following digits. You should see the same numbers on both sides. If they differ, please press 'They do not match!' to abort verification!") - color:colors.text - verticalAlignment: Text.AlignVCenter - } - RowLayout { - Layout.alignment: Qt.AlignHCenter - Label { - font.pixelSize: Qt.application.font.pixelSize * 2 - text: flow.sasList[0] - color:colors.text - } - Label { - font.pixelSize: Qt.application.font.pixelSize * 2 - text: flow.sasList[1] - color:colors.text - } - Label { - font.pixelSize: Qt.application.font.pixelSize * 2 - text: flow.sasList[2] - color:colors.text - } - } - RowLayout { - Button { - Layout.alignment: Qt.AlignLeft - text: qsTr("They do not match!") + ColumnLayout { + spacing: 16 - onClicked: { - flow.cancel(); - dialog.close(); - } - } - Item { - Layout.fillWidth: true - } - Button { - Layout.alignment: Qt.AlignRight - text: qsTr("They match!") + Label { + Layout.maximumWidth: 400 + Layout.fillHeight: true + Layout.fillWidth: true + wrapMode: Text.Wrap + text: qsTr("Please verify the following digits. You should see the same numbers on both sides. If they differ, please press 'They do not match!' to abort verification!") + color: colors.text + verticalAlignment: Text.AlignVCenter + } + + RowLayout { + Layout.alignment: Qt.AlignHCenter + + Label { + font.pixelSize: Qt.application.font.pixelSize * 2 + text: flow.sasList[0] + color: colors.text + } + + Label { + font.pixelSize: Qt.application.font.pixelSize * 2 + text: flow.sasList[1] + color: colors.text + } + + Label { + font.pixelSize: Qt.application.font.pixelSize * 2 + text: flow.sasList[2] + color: colors.text + } + + } + + RowLayout { + Button { + Layout.alignment: Qt.AlignLeft + text: qsTr("They do not match!") + onClicked: { + flow.cancel(); + dialog.close(); + } + } + + Item { + Layout.fillWidth: true + } + + Button { + Layout.alignment: Qt.AlignRight + text: qsTr("They match!") + onClicked: flow.next() + } + + } + + } - onClicked: flow.next(); - } - } - } } diff --git a/resources/qml/device-verification/EmojiElement.qml b/resources/qml/device-verification/EmojiElement.qml index 7e364594..73ad1c9b 100644 --- a/resources/qml/device-verification/EmojiElement.qml +++ b/resources/qml/device-verification/EmojiElement.qml @@ -3,24 +3,31 @@ import QtQuick.Controls 2.10 import QtQuick.Layouts 1.10 Rectangle { - color: "red" - implicitHeight: Qt.application.font.pixelSize * 4 - implicitWidth: col.width - height: Qt.application.font.pixelSize * 4 - width: col.width - ColumnLayout { - id: col - anchors.bottom: parent.bottom - property var emoji: emojis.mapping[Math.floor(Math.random()*64)] - Label { - height: font.pixelSize * 2 - Layout.alignment: Qt.AlignHCenter - text: col.emoji.emoji - font.pixelSize: Qt.application.font.pixelSize * 2 - } - Label { - Layout.alignment: Qt.AlignHCenter | Qt.AlignBottom - text: col.emoji.description - } - } + color: "red" + implicitHeight: Qt.application.font.pixelSize * 4 + implicitWidth: col.width + height: Qt.application.font.pixelSize * 4 + width: col.width + + ColumnLayout { + id: col + + property var emoji: emojis.mapping[Math.floor(Math.random() * 64)] + + anchors.bottom: parent.bottom + + Label { + height: font.pixelSize * 2 + Layout.alignment: Qt.AlignHCenter + text: col.emoji.emoji + font.pixelSize: Qt.application.font.pixelSize * 2 + } + + Label { + Layout.alignment: Qt.AlignHCenter | Qt.AlignBottom + text: col.emoji.description + } + + } + } diff --git a/resources/qml/device-verification/EmojiVerification.qml b/resources/qml/device-verification/EmojiVerification.qml index ed7727aa..6ac340e4 100644 --- a/resources/qml/device-verification/EmojiVerification.qml +++ b/resources/qml/device-verification/EmojiVerification.qml @@ -1,140 +1,414 @@ import QtQuick 2.3 import QtQuick.Controls 2.10 import QtQuick.Layouts 1.10 - import im.nheko 1.0 Pane { - property string title: qsTr("Verification Code") + property string title: qsTr("Verification Code") - ColumnLayout { - spacing: 16 - Label { - Layout.maximumWidth: 400 - Layout.fillHeight: true - Layout.fillWidth: true - wrapMode: Text.Wrap - text: qsTr("Please verify the following emoji. You should see the same emoji on both sides. If they differ, please press 'They do not match!' to abort verification!") - color:colors.text - verticalAlignment: Text.AlignVCenter - } - RowLayout { - Layout.alignment: Qt.AlignHCenter - id: emojis - property var mapping: [ - {"number": 0, "emoji": "🐶", "description": "Dog", "unicode": "U+1F436"}, - {"number": 1, "emoji": "🐱", "description": "Cat", "unicode": "U+1F431"}, - {"number": 2, "emoji": "🦁", "description": "Lion", "unicode": "U+1F981"}, - {"number": 3, "emoji": "🐎", "description": "Horse", "unicode": "U+1F40E"}, - {"number": 4, "emoji": "🦄", "description": "Unicorn", "unicode": "U+1F984"}, - {"number": 5, "emoji": "🐷", "description": "Pig", "unicode": "U+1F437"}, - {"number": 6, "emoji": "🐘", "description": "Elephant", "unicode": "U+1F418"}, - {"number": 7, "emoji": "🐰", "description": "Rabbit", "unicode": "U+1F430"}, - {"number": 8, "emoji": "🐼", "description": "Panda", "unicode": "U+1F43C"}, - {"number": 9, "emoji": "🐓", "description": "Rooster", "unicode": "U+1F413"}, - {"number": 10, "emoji": "🐧", "description": "Penguin", "unicode": "U+1F427"}, - {"number": 11, "emoji": "🐢", "description": "Turtle", "unicode": "U+1F422"}, - {"number": 12, "emoji": "🐟", "description": "Fish", "unicode": "U+1F41F"}, - {"number": 13, "emoji": "🐙", "description": "Octopus", "unicode": "U+1F419"}, - {"number": 14, "emoji": "🦋", "description": "Butterfly", "unicode": "U+1F98B"}, - {"number": 15, "emoji": "🌷", "description": "Flower", "unicode": "U+1F337"}, - {"number": 16, "emoji": "🌳", "description": "Tree", "unicode": "U+1F333"}, - {"number": 17, "emoji": "🌵", "description": "Cactus", "unicode": "U+1F335"}, - {"number": 18, "emoji": "🍄", "description": "Mushroom", "unicode": "U+1F344"}, - {"number": 19, "emoji": "🌏", "description": "Globe", "unicode": "U+1F30F"}, - {"number": 20, "emoji": "🌙", "description": "Moon", "unicode": "U+1F319"}, - {"number": 21, "emoji": "☁️", "description": "Cloud", "unicode": "U+2601U+FE0F"}, - {"number": 22, "emoji": "🔥", "description": "Fire", "unicode": "U+1F525"}, - {"number": 23, "emoji": "🍌", "description": "Banana", "unicode": "U+1F34C"}, - {"number": 24, "emoji": "🍎", "description": "Apple", "unicode": "U+1F34E"}, - {"number": 25, "emoji": "🍓", "description": "Strawberry", "unicode": "U+1F353"}, - {"number": 26, "emoji": "🌽", "description": "Corn", "unicode": "U+1F33D"}, - {"number": 27, "emoji": "🍕", "description": "Pizza", "unicode": "U+1F355"}, - {"number": 28, "emoji": "🎂", "description": "Cake", "unicode": "U+1F382"}, - {"number": 29, "emoji": "❤️", "description": "Heart", "unicode": "U+2764U+FE0F"}, - {"number": 30, "emoji": "😀", "description": "Smiley", "unicode": "U+1F600"}, - {"number": 31, "emoji": "🤖", "description": "Robot", "unicode": "U+1F916"}, - {"number": 32, "emoji": "🎩", "description": "Hat", "unicode": "U+1F3A9"}, - {"number": 33, "emoji": "👓", "description": "Glasses", "unicode": "U+1F453"}, - {"number": 34, "emoji": "🔧", "description": "Spanner", "unicode": "U+1F527"}, - {"number": 35, "emoji": "🎅", "description": "Santa", "unicode": "U+1F385"}, - {"number": 36, "emoji": "👍", "description": "Thumbs Up", "unicode": "U+1F44D"}, - {"number": 37, "emoji": "☂️", "description": "Umbrella", "unicode": "U+2602U+FE0F"}, - {"number": 38, "emoji": "⌛", "description": "Hourglass", "unicode": "U+231B"}, - {"number": 39, "emoji": "⏰", "description": "Clock", "unicode": "U+23F0"}, - {"number": 40, "emoji": "🎁", "description": "Gift", "unicode": "U+1F381"}, - {"number": 41, "emoji": "💡", "description": "Light Bulb", "unicode": "U+1F4A1"}, - {"number": 42, "emoji": "📕", "description": "Book", "unicode": "U+1F4D5"}, - {"number": 43, "emoji": "✏️", "description": "Pencil", "unicode": "U+270FU+FE0F"}, - {"number": 44, "emoji": "📎", "description": "Paperclip", "unicode": "U+1F4CE"}, - {"number": 45, "emoji": "✂️", "description": "Scissors", "unicode": "U+2702U+FE0F"}, - {"number": 46, "emoji": "🔒", "description": "Lock", "unicode": "U+1F512"}, - {"number": 47, "emoji": "🔑", "description": "Key", "unicode": "U+1F511"}, - {"number": 48, "emoji": "🔨", "description": "Hammer", "unicode": "U+1F528"}, - {"number": 49, "emoji": "☎️", "description": "Telephone", "unicode": "U+260EU+FE0F"}, - {"number": 50, "emoji": "🏁", "description": "Flag", "unicode": "U+1F3C1"}, - {"number": 51, "emoji": "🚂", "description": "Train", "unicode": "U+1F682"}, - {"number": 52, "emoji": "🚲", "description": "Bicycle", "unicode": "U+1F6B2"}, - {"number": 53, "emoji": "✈️", "description": "Aeroplane", "unicode": "U+2708U+FE0F"}, - {"number": 54, "emoji": "🚀", "description": "Rocket", "unicode": "U+1F680"}, - {"number": 55, "emoji": "🏆", "description": "Trophy", "unicode": "U+1F3C6"}, - {"number": 56, "emoji": "⚽", "description": "Ball", "unicode": "U+26BD"}, - {"number": 57, "emoji": "🎸", "description": "Guitar", "unicode": "U+1F3B8"}, - {"number": 58, "emoji": "🎺", "description": "Trumpet", "unicode": "U+1F3BA"}, - {"number": 59, "emoji": "🔔", "description": "Bell", "unicode": "U+1F514"}, - {"number": 60, "emoji": "⚓", "description": "Anchor", "unicode": "U+2693"}, - {"number": 61, "emoji": "🎧", "description": "Headphones", "unicode": "U+1F3A7"}, - {"number": 62, "emoji": "📁", "description": "Folder", "unicode": "U+1F4C1"}, - {"number": 63, "emoji": "📌", "description": "Pin", "unicode": "U+1F4CC"} - ] - Repeater { - id: repeater - model: 7 - delegate: Rectangle { - color: "transparent" - implicitHeight: Qt.application.font.pixelSize * 8 - implicitWidth: col.width - ColumnLayout { - id: col - Layout.fillWidth: true - anchors.bottom: parent.bottom - property var emoji: emojis.mapping[flow.sasList[index]] - Label { - //height: font.pixelSize * 2 - Layout.alignment: Qt.AlignHCenter - text: col.emoji.emoji - font.pixelSize: Qt.application.font.pixelSize * 2 - font.family: Settings.emojiFont - color:colors.text - } - Label { - Layout.alignment: Qt.AlignHCenter | Qt.AlignBottom - text: col.emoji.description - color:colors.text - } - } - } - } - } - RowLayout { - Button { - Layout.alignment: Qt.AlignLeft - text: qsTr("They do not match!") + ColumnLayout { + spacing: 16 - onClicked: { - flow.cancel(); - dialog.close(); - } - } - Item { - Layout.fillWidth: true - } - Button { - Layout.alignment: Qt.AlignRight - text: qsTr("They match!") + Label { + Layout.maximumWidth: 400 + Layout.fillHeight: true + Layout.fillWidth: true + wrapMode: Text.Wrap + text: qsTr("Please verify the following emoji. You should see the same emoji on both sides. If they differ, please press 'They do not match!' to abort verification!") + color: colors.text + verticalAlignment: Text.AlignVCenter + } + + RowLayout { + id: emojis + + property var mapping: [{ + "number": 0, + "emoji": "🐶", + "description": "Dog", + "unicode": "U+1F436" + }, { + "number": 1, + "emoji": "🐱", + "description": "Cat", + "unicode": "U+1F431" + }, { + "number": 2, + "emoji": "🦁", + "description": "Lion", + "unicode": "U+1F981" + }, { + "number": 3, + "emoji": "🐎", + "description": "Horse", + "unicode": "U+1F40E" + }, { + "number": 4, + "emoji": "🦄", + "description": "Unicorn", + "unicode": "U+1F984" + }, { + "number": 5, + "emoji": "🐷", + "description": "Pig", + "unicode": "U+1F437" + }, { + "number": 6, + "emoji": "🐘", + "description": "Elephant", + "unicode": "U+1F418" + }, { + "number": 7, + "emoji": "🐰", + "description": "Rabbit", + "unicode": "U+1F430" + }, { + "number": 8, + "emoji": "🐼", + "description": "Panda", + "unicode": "U+1F43C" + }, { + "number": 9, + "emoji": "🐓", + "description": "Rooster", + "unicode": "U+1F413" + }, { + "number": 10, + "emoji": "🐧", + "description": "Penguin", + "unicode": "U+1F427" + }, { + "number": 11, + "emoji": "🐢", + "description": "Turtle", + "unicode": "U+1F422" + }, { + "number": 12, + "emoji": "🐟", + "description": "Fish", + "unicode": "U+1F41F" + }, { + "number": 13, + "emoji": "🐙", + "description": "Octopus", + "unicode": "U+1F419" + }, { + "number": 14, + "emoji": "🦋", + "description": "Butterfly", + "unicode": "U+1F98B" + }, { + "number": 15, + "emoji": "🌷", + "description": "Flower", + "unicode": "U+1F337" + }, { + "number": 16, + "emoji": "🌳", + "description": "Tree", + "unicode": "U+1F333" + }, { + "number": 17, + "emoji": "🌵", + "description": "Cactus", + "unicode": "U+1F335" + }, { + "number": 18, + "emoji": "🍄", + "description": "Mushroom", + "unicode": "U+1F344" + }, { + "number": 19, + "emoji": "🌏", + "description": "Globe", + "unicode": "U+1F30F" + }, { + "number": 20, + "emoji": "🌙", + "description": "Moon", + "unicode": "U+1F319" + }, { + "number": 21, + "emoji": "☁️", + "description": "Cloud", + "unicode": "U+2601U+FE0F" + }, { + "number": 22, + "emoji": "🔥", + "description": "Fire", + "unicode": "U+1F525" + }, { + "number": 23, + "emoji": "🍌", + "description": "Banana", + "unicode": "U+1F34C" + }, { + "number": 24, + "emoji": "🍎", + "description": "Apple", + "unicode": "U+1F34E" + }, { + "number": 25, + "emoji": "🍓", + "description": "Strawberry", + "unicode": "U+1F353" + }, { + "number": 26, + "emoji": "🌽", + "description": "Corn", + "unicode": "U+1F33D" + }, { + "number": 27, + "emoji": "🍕", + "description": "Pizza", + "unicode": "U+1F355" + }, { + "number": 28, + "emoji": "🎂", + "description": "Cake", + "unicode": "U+1F382" + }, { + "number": 29, + "emoji": "❤️", + "description": "Heart", + "unicode": "U+2764U+FE0F" + }, { + "number": 30, + "emoji": "😀", + "description": "Smiley", + "unicode": "U+1F600" + }, { + "number": 31, + "emoji": "🤖", + "description": "Robot", + "unicode": "U+1F916" + }, { + "number": 32, + "emoji": "🎩", + "description": "Hat", + "unicode": "U+1F3A9" + }, { + "number": 33, + "emoji": "👓", + "description": "Glasses", + "unicode": "U+1F453" + }, { + "number": 34, + "emoji": "🔧", + "description": "Spanner", + "unicode": "U+1F527" + }, { + "number": 35, + "emoji": "🎅", + "description": "Santa", + "unicode": "U+1F385" + }, { + "number": 36, + "emoji": "👍", + "description": "Thumbs Up", + "unicode": "U+1F44D" + }, { + "number": 37, + "emoji": "☂️", + "description": "Umbrella", + "unicode": "U+2602U+FE0F" + }, { + "number": 38, + "emoji": "⌛", + "description": "Hourglass", + "unicode": "U+231B" + }, { + "number": 39, + "emoji": "⏰", + "description": "Clock", + "unicode": "U+23F0" + }, { + "number": 40, + "emoji": "🎁", + "description": "Gift", + "unicode": "U+1F381" + }, { + "number": 41, + "emoji": "💡", + "description": "Light Bulb", + "unicode": "U+1F4A1" + }, { + "number": 42, + "emoji": "📕", + "description": "Book", + "unicode": "U+1F4D5" + }, { + "number": 43, + "emoji": "✏️", + "description": "Pencil", + "unicode": "U+270FU+FE0F" + }, { + "number": 44, + "emoji": "📎", + "description": "Paperclip", + "unicode": "U+1F4CE" + }, { + "number": 45, + "emoji": "✂️", + "description": "Scissors", + "unicode": "U+2702U+FE0F" + }, { + "number": 46, + "emoji": "🔒", + "description": "Lock", + "unicode": "U+1F512" + }, { + "number": 47, + "emoji": "🔑", + "description": "Key", + "unicode": "U+1F511" + }, { + "number": 48, + "emoji": "🔨", + "description": "Hammer", + "unicode": "U+1F528" + }, { + "number": 49, + "emoji": "☎️", + "description": "Telephone", + "unicode": "U+260EU+FE0F" + }, { + "number": 50, + "emoji": "🏁", + "description": "Flag", + "unicode": "U+1F3C1" + }, { + "number": 51, + "emoji": "🚂", + "description": "Train", + "unicode": "U+1F682" + }, { + "number": 52, + "emoji": "🚲", + "description": "Bicycle", + "unicode": "U+1F6B2" + }, { + "number": 53, + "emoji": "✈️", + "description": "Aeroplane", + "unicode": "U+2708U+FE0F" + }, { + "number": 54, + "emoji": "🚀", + "description": "Rocket", + "unicode": "U+1F680" + }, { + "number": 55, + "emoji": "🏆", + "description": "Trophy", + "unicode": "U+1F3C6" + }, { + "number": 56, + "emoji": "⚽", + "description": "Ball", + "unicode": "U+26BD" + }, { + "number": 57, + "emoji": "🎸", + "description": "Guitar", + "unicode": "U+1F3B8" + }, { + "number": 58, + "emoji": "🎺", + "description": "Trumpet", + "unicode": "U+1F3BA" + }, { + "number": 59, + "emoji": "🔔", + "description": "Bell", + "unicode": "U+1F514" + }, { + "number": 60, + "emoji": "⚓", + "description": "Anchor", + "unicode": "U+2693" + }, { + "number": 61, + "emoji": "🎧", + "description": "Headphones", + "unicode": "U+1F3A7" + }, { + "number": 62, + "emoji": "📁", + "description": "Folder", + "unicode": "U+1F4C1" + }, { + "number": 63, + "emoji": "📌", + "description": "Pin", + "unicode": "U+1F4CC" + }] + + Layout.alignment: Qt.AlignHCenter + + Repeater { + id: repeater + + model: 7 + + delegate: Rectangle { + color: "transparent" + implicitHeight: Qt.application.font.pixelSize * 8 + implicitWidth: col.width + + ColumnLayout { + id: col + + property var emoji: emojis.mapping[flow.sasList[index]] + + Layout.fillWidth: true + anchors.bottom: parent.bottom + + Label { + //height: font.pixelSize * 2 + Layout.alignment: Qt.AlignHCenter + text: col.emoji.emoji + font.pixelSize: Qt.application.font.pixelSize * 2 + font.family: Settings.emojiFont + color: colors.text + } + + Label { + Layout.alignment: Qt.AlignHCenter | Qt.AlignBottom + text: col.emoji.description + color: colors.text + } + + } + + } + + } + + } + + RowLayout { + Button { + Layout.alignment: Qt.AlignLeft + text: qsTr("They do not match!") + onClicked: { + flow.cancel(); + dialog.close(); + } + } + + Item { + Layout.fillWidth: true + } + + Button { + Layout.alignment: Qt.AlignRight + text: qsTr("They match!") + onClicked: flow.next() + } + + } + + } - onClicked: flow.next() - } - } - } } diff --git a/resources/qml/device-verification/Failed.qml b/resources/qml/device-verification/Failed.qml index fcff7893..5f9a2859 100644 --- a/resources/qml/device-verification/Failed.qml +++ b/resources/qml/device-verification/Failed.qml @@ -1,44 +1,56 @@ import QtQuick 2.3 import QtQuick.Controls 2.10 import QtQuick.Layouts 1.10 - import im.nheko 1.0 Pane { - property string title: qsTr("Verification failed") - ColumnLayout { - spacing: 16 - Text { - id: content + property string title: qsTr("Verification failed") - Layout.maximumWidth: 400 - Layout.fillHeight: true - Layout.fillWidth: true + ColumnLayout { + spacing: 16 - wrapMode: Text.Wrap - text: switch (flow.error) { - case DeviceVerificationFlow.UnknownMethod: return qsTr("Other client does not support our verification protocol.") - case DeviceVerificationFlow.MismatchedCommitment: - case DeviceVerificationFlow.MismatchedSAS: - case DeviceVerificationFlow.KeyMismatch: return qsTr("Key mismatch detected!") - case DeviceVerificationFlow.Timeout: return qsTr("Device verification timed out.") - case DeviceVerificationFlow.User: return qsTr("Other party canceled the verification.") - case DeviceVerificationFlow.OutOfOrder: return qsTr("Device verification timed out.") - default: return "Unknown verification error."; - } - color:colors.text - verticalAlignment: Text.AlignVCenter - } - RowLayout { - Item { - Layout.fillWidth: true - } - Button { - Layout.alignment: Qt.AlignRight - text: qsTr("Close") + Text { + id: content + + Layout.maximumWidth: 400 + Layout.fillHeight: true + Layout.fillWidth: true + wrapMode: Text.Wrap + text: { + switch (flow.error) { + case DeviceVerificationFlow.UnknownMethod: + return qsTr("Other client does not support our verification protocol."); + case DeviceVerificationFlow.MismatchedCommitment: + case DeviceVerificationFlow.MismatchedSAS: + case DeviceVerificationFlow.KeyMismatch: + return qsTr("Key mismatch detected!"); + case DeviceVerificationFlow.Timeout: + return qsTr("Device verification timed out."); + case DeviceVerificationFlow.User: + return qsTr("Other party canceled the verification."); + case DeviceVerificationFlow.OutOfOrder: + return qsTr("Device verification timed out."); + default: + return "Unknown verification error."; + } + } + color: colors.text + verticalAlignment: Text.AlignVCenter + } + + RowLayout { + Item { + Layout.fillWidth: true + } + + Button { + Layout.alignment: Qt.AlignRight + text: qsTr("Close") + onClicked: dialog.close() + } + + } + + } - onClicked: dialog.close() - } - } - } } diff --git a/resources/qml/device-verification/NewVerificationRequest.qml b/resources/qml/device-verification/NewVerificationRequest.qml index bd25bb90..d411ed47 100644 --- a/resources/qml/device-verification/NewVerificationRequest.qml +++ b/resources/qml/device-verification/NewVerificationRequest.qml @@ -1,44 +1,46 @@ import QtQuick 2.3 import QtQuick.Controls 2.10 import QtQuick.Layouts 1.10 - import im.nheko 1.0 Pane { - property string title: flow.sender ? qsTr("Send Device Verification Request") : qsTr("Recieved Device Verification Request") + property string title: flow.sender ? qsTr("Send Device Verification Request") : qsTr("Recieved Device Verification Request") - ColumnLayout { - spacing: 16 - Label { - Layout.maximumWidth: 400 - Layout.fillHeight: true - Layout.fillWidth: true - wrapMode: Text.Wrap - text: flow.sender ? - qsTr("To ensure that no malicious user can eavesdrop on your encrypted communications, you can verify this device.") - : qsTr("The device was requested to be verified") - color:colors.text - verticalAlignment: Text.AlignVCenter - } - RowLayout { - Button { - Layout.alignment: Qt.AlignLeft - text: flow.sender ? qsTr("Cancel") : qsTr("Deny") + ColumnLayout { + spacing: 16 - onClicked: { - flow.cancel(); - dialog.close(); - } - } - Item { - Layout.fillWidth: true - } - Button { - Layout.alignment: Qt.AlignRight - text: flow.sender ? qsTr("Start verification") : qsTr("Accept") + Label { + Layout.maximumWidth: 400 + Layout.fillHeight: true + Layout.fillWidth: true + wrapMode: Text.Wrap + text: flow.sender ? qsTr("To ensure that no malicious user can eavesdrop on your encrypted communications, you can verify this device.") : qsTr("The device was requested to be verified") + color: colors.text + verticalAlignment: Text.AlignVCenter + } + + RowLayout { + Button { + Layout.alignment: Qt.AlignLeft + text: flow.sender ? qsTr("Cancel") : qsTr("Deny") + onClicked: { + flow.cancel(); + dialog.close(); + } + } + + Item { + Layout.fillWidth: true + } + + Button { + Layout.alignment: Qt.AlignRight + text: flow.sender ? qsTr("Start verification") : qsTr("Accept") + onClicked: flow.next() + } + + } + + } - onClicked: flow.next(); - } - } - } } diff --git a/resources/qml/device-verification/Success.qml b/resources/qml/device-verification/Success.qml index b17b293c..175f7524 100644 --- a/resources/qml/device-verification/Success.qml +++ b/resources/qml/device-verification/Success.qml @@ -3,29 +3,36 @@ import QtQuick.Controls 2.10 import QtQuick.Layouts 1.10 Pane { - property string title: qsTr("Successful Verification") - ColumnLayout { - spacing: 16 - Label { - Layout.maximumWidth: 400 - Layout.fillHeight: true - Layout.fillWidth: true - wrapMode: Text.Wrap - id: content - text: qsTr("Verification successful! Both sides verified their devices!") - color:colors.text - verticalAlignment: Text.AlignVCenter - } - RowLayout { - Item { - Layout.fillWidth: true - } - Button { - Layout.alignment: Qt.AlignRight - text: qsTr("Close") + property string title: qsTr("Successful Verification") + + ColumnLayout { + spacing: 16 + + Label { + id: content + + Layout.maximumWidth: 400 + Layout.fillHeight: true + Layout.fillWidth: true + wrapMode: Text.Wrap + text: qsTr("Verification successful! Both sides verified their devices!") + color: colors.text + verticalAlignment: Text.AlignVCenter + } + + RowLayout { + Item { + Layout.fillWidth: true + } + + Button { + Layout.alignment: Qt.AlignRight + text: qsTr("Close") + onClicked: dialog.close() + } + + } + + } - onClicked: dialog.close(); - } - } - } } diff --git a/resources/qml/device-verification/Waiting.qml b/resources/qml/device-verification/Waiting.qml index 38abf767..0c4ae405 100644 --- a/resources/qml/device-verification/Waiting.qml +++ b/resources/qml/device-verification/Waiting.qml @@ -1,45 +1,56 @@ import QtQuick 2.3 import QtQuick.Controls 2.10 import QtQuick.Layouts 1.10 - import im.nheko 1.0 Pane { - property string title: qsTr("Waiting for other party") - ColumnLayout { - spacing: 16 - Label { - Layout.maximumWidth: 400 - Layout.fillHeight: true - Layout.fillWidth: true - wrapMode: Text.Wrap - id: content - text: switch (flow.state) { - case "WaitingForOtherToAccept": return qsTr("Waiting for other side to accept the verification request.") - case "WaitingForKeys": return qsTr("Waiting for other side to continue the verification request.") - case "WaitingForMac": return qsTr("Waiting for other side to complete the verification request.") - } + property string title: qsTr("Waiting for other party") - color: colors.text - verticalAlignment: Text.AlignVCenter - } - BusyIndicator { - Layout.alignment: Qt.AlignHCenter - palette: colors - } - RowLayout { - Button { - Layout.alignment: Qt.AlignLeft - text: qsTr("Cancel") + ColumnLayout { + spacing: 16 + + Label { + id: content + + Layout.maximumWidth: 400 + Layout.fillHeight: true + Layout.fillWidth: true + wrapMode: Text.Wrap + text: { + switch (flow.state) { + case "WaitingForOtherToAccept": + return qsTr("Waiting for other side to accept the verification request."); + case "WaitingForKeys": + return qsTr("Waiting for other side to continue the verification request."); + case "WaitingForMac": + return qsTr("Waiting for other side to complete the verification request."); + } + } + color: colors.text + verticalAlignment: Text.AlignVCenter + } + + BusyIndicator { + Layout.alignment: Qt.AlignHCenter + palette: colors + } + + RowLayout { + Button { + Layout.alignment: Qt.AlignLeft + text: qsTr("Cancel") + onClicked: { + flow.cancel(); + dialog.close(); + } + } + + Item { + Layout.fillWidth: true + } + + } + + } - onClicked: { - flow.cancel(); - dialog.close(); - } - } - Item { - Layout.fillWidth: true - } - } - } } diff --git a/resources/qml/emoji/EmojiButton.qml b/resources/qml/emoji/EmojiButton.qml index c5eee4e4..1fcfc0c5 100644 --- a/resources/qml/emoji/EmojiButton.qml +++ b/resources/qml/emoji/EmojiButton.qml @@ -1,17 +1,16 @@ +import "../" import QtQuick 2.10 import QtQuick.Controls 2.1 import im.nheko 1.0 import im.nheko.EmojiModel 1.0 -import "../" - ImageButton { + id: emojiButton + property var colors: currentActivePalette property var emojiPicker property string event_id image: ":/icons/icons/ui/smile.png" - id: emojiButton onClicked: emojiPicker.visible ? emojiPicker.close() : emojiPicker.show(emojiButton, event_id) - } diff --git a/resources/qml/emoji/EmojiPicker.qml b/resources/qml/emoji/EmojiPicker.qml index cbb77beb..3a5ee57a 100644 --- a/resources/qml/emoji/EmojiPicker.qml +++ b/resources/qml/emoji/EmojiPicker.qml @@ -1,25 +1,13 @@ +import "../" +import QtGraphicalEffects 1.0 import QtQuick 2.9 import QtQuick.Controls 2.3 import QtQuick.Layouts 1.3 -import QtGraphicalEffects 1.0 - import im.nheko 1.0 import im.nheko.EmojiModel 1.0 -import "../" - Popup { - - function show(showAt, event_id) { - console.debug("Showing emojiPicker for " + event_id) - if (showAt){ - parent = showAt - x = Math.round((showAt.width - width) / 2) - y = showAt.height - } - emojiPopup.event_id = event_id - open() - } + id: emojiPopup property string event_id property var colors @@ -30,19 +18,28 @@ Popup { property real highlightSat: colors.highlight.hslSaturation property real highlightLight: colors.highlight.hslLightness - id: emojiPopup + function show(showAt, event_id) { + console.debug("Showing emojiPicker for " + event_id); + if (showAt) { + parent = showAt; + x = Math.round((showAt.width - width) / 2); + y = showAt.height; + } + emojiPopup.event_id = event_id; + open(); + } margins: 0 bottomPadding: 1 leftPadding: 1 rightPadding: 1 - modal: true focus: true closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside ColumnLayout { id: columnView + anchors.fill: parent spacing: 0 Layout.bottomMargin: 0 @@ -58,23 +55,41 @@ Popup { Layout.fillWidth: true Layout.fillHeight: true Layout.leftMargin: 4 - cellWidth: 52 cellHeight: 52 - boundsBehavior: Flickable.StopAtBounds - clip: true // Individual emoji delegate: AbstractButton { width: 48 height: 48 + hoverEnabled: true + ToolTip.text: model.shortName + ToolTip.visible: hovered + // TODO: maybe add favorites at some point? + onClicked: { + console.debug("Picked " + model.unicode + "in response to " + emojiPopup.event_id); + emojiPopup.close(); + TimelineManager.queueReactionMessage(emojiPopup.event_id, model.unicode); + } + + // give the emoji a little oomf + DropShadow { + width: parent.width + height: parent.height + horizontalOffset: 3 + verticalOffset: 3 + radius: 8 + samples: 17 + color: "#80000000" + source: parent.contentItem + } + contentItem: Text { horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter font.family: Settings.emojiFont - font.pixelSize: 36 text: model.unicode } @@ -85,76 +100,66 @@ Popup { radius: 5 } - hoverEnabled: true - ToolTip.text: model.shortName - ToolTip.visible: hovered - - // give the emoji a little oomf - DropShadow { - width: parent.width; - height: parent.height; - horizontalOffset: 3 - verticalOffset: 3 - radius: 8.0 - samples: 17 - color: "#80000000" - source: parent.contentItem - } - // TODO: maybe add favorites at some point? - onClicked: { - console.debug("Picked " + model.unicode + "in response to " + emojiPopup.event_id) - emojiPopup.close() - TimelineManager.queueReactionMessage(emojiPopup.event_id, model.unicode) - } } // Search field header: TextField { id: emojiSearch + anchors.left: parent.left anchors.right: parent.right anchors.rightMargin: emojiScroll.width + 4 placeholderText: qsTr("Search") selectByMouse: true rightPadding: clearSearch.width + onTextChanged: searchTimer.restart() + onVisibleChanged: { + if (visible) + forceActiveFocus(); + + } Timer { id: searchTimer + interval: 350 // tweak as needed? onTriggered: { - emojiPopup.model.filter = emojiSearch.text - emojiPopup.model.category = EmojiCategory.Search + emojiPopup.model.filter = emojiSearch.text; + emojiPopup.model.category = EmojiCategory.Search; } } ToolButton { id: clearSearch + + visible: emojiSearch.text !== '' + icon.source: "image://colorimage/:/icons/icons/ui/round-remove-button.png?" + (clearSearch.hovered ? colors.highlight : colors.buttonText) + focusPolicy: Qt.NoFocus + onClicked: emojiSearch.clear() + anchors { verticalCenter: parent.verticalCenter right: parent.right } // clear the default hover effects. - background: Item {} - visible: emojiSearch.text !== '' - icon.source: "image://colorimage/:/icons/icons/ui/round-remove-button.png?" + (clearSearch.hovered ? colors.highlight : colors.buttonText) - focusPolicy: Qt.NoFocus - onClicked: emojiSearch.clear() + + background: Item { + } + } - onTextChanged: searchTimer.restart() - onVisibleChanged: if (visible) forceActiveFocus() } - ScrollBar.vertical: ScrollBar { - id: emojiScroll - } + ScrollBar.vertical: ScrollBar { + id: emojiScroll + } + } // Separator Rectangle { Layout.fillWidth: true Layout.preferredHeight: 1 - color: emojiPopup.colors.dark } @@ -164,23 +169,90 @@ Popup { Layout.preferredHeight: 42 implicitHeight: 42 Layout.alignment: Qt.AlignHCenter | Qt.AlignBottom + // Display the normal categories Repeater { + model: ListModel { // TODO: Would like to get 'simple' icons for the categories - ListElement { image: ":/icons/icons/emoji-categories/people.png"; category: EmojiCategory.People } - ListElement { image: ":/icons/icons/emoji-categories/nature.png"; category: EmojiCategory.Nature } - ListElement { image: ":/icons/icons/emoji-categories/foods.png"; category: EmojiCategory.Food } - ListElement { image: ":/icons/icons/emoji-categories/activity.png"; category: EmojiCategory.Activity } - ListElement { image: ":/icons/icons/emoji-categories/travel.png"; category: EmojiCategory.Travel } - ListElement { image: ":/icons/icons/emoji-categories/objects.png"; category: EmojiCategory.Objects } - ListElement { image: ":/icons/icons/emoji-categories/symbols.png"; category: EmojiCategory.Symbols } - ListElement { image: ":/icons/icons/emoji-categories/flags.png"; category: EmojiCategory.Flags } + ListElement { + image: ":/icons/icons/emoji-categories/people.png" + category: EmojiCategory.People + } + + ListElement { + image: ":/icons/icons/emoji-categories/nature.png" + category: EmojiCategory.Nature + } + + ListElement { + image: ":/icons/icons/emoji-categories/foods.png" + category: EmojiCategory.Food + } + + ListElement { + image: ":/icons/icons/emoji-categories/activity.png" + category: EmojiCategory.Activity + } + + ListElement { + image: ":/icons/icons/emoji-categories/travel.png" + category: EmojiCategory.Travel + } + + ListElement { + image: ":/icons/icons/emoji-categories/objects.png" + category: EmojiCategory.Objects + } + + ListElement { + image: ":/icons/icons/emoji-categories/symbols.png" + category: EmojiCategory.Symbols + } + + ListElement { + image: ":/icons/icons/emoji-categories/flags.png" + category: EmojiCategory.Flags + } + } delegate: AbstractButton { Layout.preferredWidth: 36 Layout.preferredHeight: 36 + hoverEnabled: true + ToolTip.text: { + switch (model.category) { + case EmojiCategory.People: + return qsTr('People'); + case EmojiCategory.Nature: + return qsTr('Nature'); + case EmojiCategory.Food: + return qsTr('Food'); + case EmojiCategory.Activity: + return qsTr('Activity'); + case EmojiCategory.Travel: + return qsTr('Travel'); + case EmojiCategory.Objects: + return qsTr('Objects'); + case EmojiCategory.Symbols: + return qsTr('Symbols'); + case EmojiCategory.Flags: + return qsTr('Flags'); + } + } + ToolTip.visible: hovered + onClicked: { + emojiPopup.model.category = model.category; + } + + MouseArea { + id: mouseArea + + anchors.fill: parent + onPressed: mouse.accepted = false + cursorShape: Qt.PointingHandCursor + } contentItem: Image { horizontalAlignment: Image.AlignHCenter @@ -191,49 +263,15 @@ Popup { source: "image://colorimage/" + model.image + "?" + (hovered ? colors.highlight : colors.buttonText) } - MouseArea - { - id: mouseArea - anchors.fill: parent - onPressed: mouse.accepted = false - cursorShape: Qt.PointingHandCursor - } - background: Rectangle { anchors.fill: parent - - color: emojiPopup.model.category === model.category ? Qt.hsla(highlightHue, highlightSat, highlightLight, 0.20) : 'transparent' + color: emojiPopup.model.category === model.category ? Qt.hsla(highlightHue, highlightSat, highlightLight, 0.2) : 'transparent' radius: 5 border.color: emojiPopup.model.category === model.category ? colors.highlight : 'transparent' } - hoverEnabled: true - ToolTip.text: { - switch (model.category) { - case EmojiCategory.People: - return qsTr('People'); - case EmojiCategory.Nature: - return qsTr('Nature'); - case EmojiCategory.Food: - return qsTr('Food'); - case EmojiCategory.Activity: - return qsTr('Activity'); - case EmojiCategory.Travel: - return qsTr('Travel'); - case EmojiCategory.Objects: - return qsTr('Objects'); - case EmojiCategory.Symbols: - return qsTr('Symbols'); - case EmojiCategory.Flags: - return qsTr('Flags'); - } - } - ToolTip.visible: hovered - - onClicked: { - emojiPopup.model.category = model.category - } } + } // Separator @@ -242,30 +280,37 @@ Popup { Layout.preferredWidth: 1 implicitWidth: 1 height: parent.height - color: emojiPopup.colors.dark } // Search Button is special AbstractButton { id: searchBtn + hoverEnabled: true Layout.alignment: Qt.AlignRight Layout.bottomMargin: 0 - ToolTip.text: qsTr("Search") ToolTip.visible: hovered onClicked: { // clear any filters - emojiPopup.model.category = EmojiCategory.Search - gridView.positionViewAtBeginning() - emojiSearch.forceActiveFocus() + emojiPopup.model.category = EmojiCategory.Search; + gridView.positionViewAtBeginning(); + emojiSearch.forceActiveFocus(); } Layout.preferredWidth: 36 Layout.preferredHeight: 36 implicitWidth: 36 implicitHeight: 36 + MouseArea { + id: mouseArea + + anchors.fill: parent + onPressed: mouse.accepted = false + cursorShape: Qt.PointingHandCursor + } + contentItem: Image { anchors.right: parent.right horizontalAlignment: Image.AlignHCenter @@ -277,14 +322,10 @@ Popup { source: "image://colorimage/:/icons/icons/ui/search.png?" + (parent.hovered ? colors.highlight : colors.buttonText) } - MouseArea - { - id: mouseArea - anchors.fill: parent - onPressed: mouse.accepted = false - cursorShape: Qt.PointingHandCursor - } } + } + } + }