From da4a5ab5cdd02b1c4744682b3850a4c50f06418e Mon Sep 17 00:00:00 2001 From: miruka Date: Thu, 25 Jun 2020 08:32:08 -0400 Subject: [PATCH] Rework HBox-based pages and account settings - Refactor everything about HBox, and adapt all the pages and popups that used it - Replace HTabContainer by HTabbedBox - Make boxes swippable - Make esc presses in boxes click the cancel button - Make all boxes and popups scrollable when needed - Replace generic apply button icons in popups - Fix tab focus for error and invite popups - Rework (still WIP) the account settings page: - Use the standard tabbed design of other pages - Ditch the horizontal profile layout, hacky and impossible to extend - Add real-time coloring for the display name field - Implement a device list in account settings (Sessions, still WIP) --- TODO.md | 25 +- src/backend/matrix_client.py | 26 +- src/gui/Base/ButtonLayout/ApplyButton.qml | 2 +- src/gui/Base/ButtonLayout/CancelButton.qml | 2 +- src/gui/Base/ButtonLayout/OtherButton.qml | 10 + src/gui/Base/HBox.qml | 119 +------- src/gui/Base/HButtonContent.qml | 1 - src/gui/Base/HCheckBox.qml | 11 +- src/gui/Base/HColumnPage.qml | 6 + src/gui/Base/HFlickableColumnPage.qml | 22 +- src/gui/Base/HLabeledItem.qml | 2 +- src/gui/Base/HMenuSeparator.qml | 12 + src/gui/Base/HPage.qml | 6 +- src/gui/Base/HPageHeader.qml | 50 ---- src/gui/Base/HPopup.qml | 5 +- src/gui/Base/HTabContainer.qml | 39 --- src/gui/Base/HTabbedBox.qml | 45 +++ src/gui/Dialogs/ExportKeys.qml | 4 +- src/gui/Dialogs/ImportKeys.qml | 12 +- src/gui/Pages/AccountSettings/Account.qml | 247 ++++++++++++++++ .../Pages/AccountSettings/AccountSettings.qml | 59 +--- .../Pages/AccountSettings/DeviceDelegate.qml | 113 ++++++++ .../Pages/AccountSettings/DeviceSection.qml | 75 +++++ .../{ImportExportKeys.qml => Encryption.qml} | 52 ++-- src/gui/Pages/AccountSettings/Profile.qml | 271 ------------------ src/gui/Pages/AccountSettings/Sessions.qml | 95 ++++++ src/gui/Pages/AddAccount/AddAccount.qml | 21 +- src/gui/Pages/AddAccount/Register.qml | 26 +- src/gui/Pages/AddAccount/Reset.qml | 31 +- src/gui/Pages/AddAccount/SignIn.qml | 170 +++++------ src/gui/Pages/AddChat/AddChat.qml | 27 +- src/gui/Pages/AddChat/CreateRoom.qml | 91 +++--- src/gui/Pages/AddChat/CurrentUserAvatar.qml | 13 +- src/gui/Pages/AddChat/DirectChat.qml | 124 ++++---- src/gui/Pages/AddChat/JoinRoom.qml | 97 ++++--- src/gui/Pages/Chat/ChatPage.qml | 1 + .../Pages/Chat/RoomPane/MemberDelegate.qml | 7 +- src/gui/Pages/Chat/RoomPane/SettingsView.qml | 5 +- src/gui/Popups/BoxPopup.qml | 79 ----- src/gui/Popups/ClearMessagesPopup.qml | 43 ++- src/gui/Popups/DetailsLabel.qml | 12 + src/gui/Popups/ForgetRoomPopup.qml | 80 ++++-- src/gui/Popups/HColumnPopup.qml | 27 ++ src/gui/Popups/HFlickableColumnPopup.qml | 25 ++ src/gui/Popups/InviteToRoomPopup.qml | 102 ++++--- src/gui/Popups/LeaveRoomPopup.qml | 45 ++- src/gui/Popups/PasswordPopup.qml | 97 ++++--- src/gui/Popups/RedactPopup.qml | 64 +++-- src/gui/Popups/RemoveMemberPopup.qml | 78 +++-- src/gui/Popups/SignOutPopup.qml | 87 +++--- src/gui/Popups/SummaryLabel.qml | 13 + src/gui/Popups/UnexpectedErrorPopup.qml | 39 ++- src/gui/ShortcutBundles/TabShortcuts.qml | 2 +- src/icons/thin/check-mark-partial.svg | 3 + src/icons/thin/device-action-menu.svg | 6 +- src/icons/thin/device-blacklisted.svg | 3 + src/icons/thin/device-current.svg | 3 + src/icons/thin/device-delete-checked.svg | 3 + src/icons/thin/device-delete.svg | 3 + src/icons/thin/device-ignored.svg | 3 + src/icons/thin/device-rename.svg | 3 + src/icons/thin/device-unset.svg | 3 + src/icons/thin/device-verified.svg | 3 + src/icons/thin/device-verify.svg | 3 + src/themes/Glass.qpl | 7 +- src/themes/Midnight.qpl | 7 +- 66 files changed, 1594 insertions(+), 1173 deletions(-) create mode 100644 src/gui/Base/ButtonLayout/OtherButton.qml create mode 100644 src/gui/Base/HMenuSeparator.qml delete mode 100644 src/gui/Base/HPageHeader.qml delete mode 100644 src/gui/Base/HTabContainer.qml create mode 100644 src/gui/Base/HTabbedBox.qml create mode 100644 src/gui/Pages/AccountSettings/Account.qml create mode 100644 src/gui/Pages/AccountSettings/DeviceDelegate.qml create mode 100644 src/gui/Pages/AccountSettings/DeviceSection.qml rename src/gui/Pages/AccountSettings/{ImportExportKeys.qml => Encryption.qml} (50%) delete mode 100644 src/gui/Pages/AccountSettings/Profile.qml create mode 100644 src/gui/Pages/AccountSettings/Sessions.qml delete mode 100644 src/gui/Popups/BoxPopup.qml create mode 100644 src/gui/Popups/DetailsLabel.qml create mode 100644 src/gui/Popups/HColumnPopup.qml create mode 100644 src/gui/Popups/HFlickableColumnPopup.qml create mode 100644 src/gui/Popups/SummaryLabel.qml create mode 100644 src/icons/thin/check-mark-partial.svg create mode 100644 src/icons/thin/device-blacklisted.svg create mode 100644 src/icons/thin/device-current.svg create mode 100644 src/icons/thin/device-delete-checked.svg create mode 100644 src/icons/thin/device-delete.svg create mode 100644 src/icons/thin/device-ignored.svg create mode 100644 src/icons/thin/device-rename.svg create mode 100644 src/icons/thin/device-unset.svg create mode 100644 src/icons/thin/device-verified.svg create mode 100644 src/icons/thin/device-verify.svg diff --git a/TODO.md b/TODO.md index d91767c7..1bec8b87 100644 --- a/TODO.md +++ b/TODO.md @@ -1,19 +1,27 @@ # TODO +- sessions page size +- menu click-through padding to close it easily +- clear listview checked on message clear +- unregister popup/menu when destroyed without being closed +- flickshortcuts +- Account: wait until accountInfo available +- avatar upload/change component +- show scrollbars for a few secs if there's content to scroll on beginning +- can leave room with a reason? +- field/area focus line in popups weird +- use new nio `restore_login()` + ## Refactoring -- Rewrite account settings using `HTabbedContainer` - - Use new default/reset controls system - - Display name field text should be colored - -- Drop the `HBox` `buttonModel`/`buttonCallbacks` `HBox` approach, - be more declarative - - Reorder QML object declarations, conform to https://doc-snapshots.qt.io/qt5-dev/qml-codingconventions.html ## Issues +- Bottom focus line for an `HTextArea` inside a `ScrollView` is invisible, + put the background on `ScrollView` instead? + - Don't send typing notification when switching to a room where the composer has preloaded text @@ -21,9 +29,6 @@ the marker will only be updated for accounts that have already received it (server lag) -- Popups can't be scrolled when not enough height to show all -- `TextArea`s in Popups grow past window height instead of being scrollable - - Jumping between accounts (clicking in account bar or alt+(Shift+)N) is laggy with hundreds of rooms in between - On startup, if a room's last event is a membership change, diff --git a/src/backend/matrix_client.py b/src/backend/matrix_client.py index 7bdb54cf..45481d9a 100644 --- a/src/backend/matrix_client.py +++ b/src/backend/matrix_client.py @@ -12,7 +12,6 @@ import sys import traceback from contextlib import suppress from copy import deepcopy -from dataclasses import asdict from datetime import datetime, timedelta from functools import partial from pathlib import Path @@ -1261,8 +1260,11 @@ class MatrixClient(nio.AsyncClient): async def devices_info(self) -> List[Dict[str, Any]]: """Get list of devices and their info for our user.""" - def get_trust(device_id: str) -> str: - # Returns "verified", "blacklisted", "ignored" or "unset" + def get_type(device_id: str) -> str: + # Return "current", "verified", "blacklisted", "ignored" or "unset" + + if device_id == self.device_id: + return "current" if device_id not in self.olm.device_store[self.user_id]: return "unset" @@ -1274,19 +1276,27 @@ class MatrixClient(nio.AsyncClient): { "id": device.id, "display_name": device.display_name or "", - "last_seen_ip": device.last_seen_ip or "", + "last_seen_ip": (device.last_seen_ip or "").strip(" -"), "last_seen_date": device.last_seen_date or ZeroDate, "last_seen_country": "", - "trusted": get_trust(device.id) == "verified", - "blacklisted": get_trust(device.id) == "blacklisted", + "type": get_type(device.id), } for device in (await self.devices()).devices ] + # Reversed due to sorted(reverse=True) call below + types_order = { + "current": 4, + "unset": 3, + "verified": 2, + "ignored": 1, + "blacklisted": 0, + } + + # Sort by type, then by descending date return sorted( devices, - # The current device will always be first - key = lambda d: (d["id"] == self.device_id, d["last_seen_date"]), + key = lambda d: (types_order[d["type"]], d["last_seen_date"]), reverse = True, ) diff --git a/src/gui/Base/ButtonLayout/ApplyButton.qml b/src/gui/Base/ButtonLayout/ApplyButton.qml index 89c8bc05..148c8d4e 100644 --- a/src/gui/Base/ButtonLayout/ApplyButton.qml +++ b/src/gui/Base/ButtonLayout/ApplyButton.qml @@ -5,10 +5,10 @@ import QtQuick.Layouts 1.12 import ".." HButton { - implicitHeight: theme.baseElementsHeight text: qsTr("Apply") icon.name: "apply" icon.color: theme.colors.positiveBackground + Layout.preferredHeight: theme.baseElementsHeight Layout.fillWidth: true } diff --git a/src/gui/Base/ButtonLayout/CancelButton.qml b/src/gui/Base/ButtonLayout/CancelButton.qml index fa53c69e..0728ced9 100644 --- a/src/gui/Base/ButtonLayout/CancelButton.qml +++ b/src/gui/Base/ButtonLayout/CancelButton.qml @@ -5,10 +5,10 @@ import QtQuick.Layouts 1.12 import ".." HButton { - implicitHeight: theme.baseElementsHeight text: qsTr("Cancel") icon.name: "cancel" icon.color: theme.colors.negativeBackground + Layout.preferredHeight: theme.baseElementsHeight Layout.fillWidth: true } diff --git a/src/gui/Base/ButtonLayout/OtherButton.qml b/src/gui/Base/ButtonLayout/OtherButton.qml new file mode 100644 index 00000000..525477a9 --- /dev/null +++ b/src/gui/Base/ButtonLayout/OtherButton.qml @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later + +import QtQuick 2.12 +import QtQuick.Layouts 1.12 +import ".." + +HButton { + Layout.preferredHeight: theme.baseElementsHeight + Layout.fillWidth: true +} diff --git a/src/gui/Base/HBox.qml b/src/gui/Base/HBox.qml index 4d5846c9..861caf09 100644 --- a/src/gui/Base/HBox.qml +++ b/src/gui/Base/HBox.qml @@ -3,34 +3,17 @@ import QtQuick 2.12 import QtQuick.Layouts 1.12 -Rectangle { - id: box - color: theme.controls.box.background - radius: theme.controls.box.radius - implicitWidth: theme.controls.box.defaultWidth - implicitHeight: childrenRect.height +HFlickableColumnPage { + implicitWidth: Math.min(parent.width, theme.controls.box.defaultWidth) + implicitHeight: Math.min(parent.height, flickable.contentHeight) - Keys.onReturnPressed: if (clickButtonOnEnter) enterClickButton() - Keys.onEnterPressed: Keys.onReturnPressed(event) + // XXX + // Keys.onReturnPressed: if (clickButtonOnEnter) enterClickButton() + // Keys.onEnterPressed: Keys.onReturnPressed(event) - - property alias buttonModel: buttonRepeater.model - property var buttonCallbacks: [] - property string focusButton: "" - property string clickButtonOnEnter: "" - - property bool fillAvailableHeight: false - - property HButton firstButton: null - - default property alias body: interfaceBody.data - - - function enterClickButton() { - for (let i = 0; i < buttonModel.length; i++) { - const btn = buttonRepeater.itemAt(i) - if (btn.enabled && btn.name === clickButtonOnEnter) btn.clicked() - } + background: Rectangle { + color: theme.controls.box.background + radius: theme.controls.box.radius } @@ -41,86 +24,6 @@ Rectangle { overshoot: 3 } - HColumnLayout { - id: mainColumn - width: parent.width - - Binding on height { - value: box.height - when: box.fillAvailableHeight - } - - HColumnLayout { - id: interfaceBody - spacing: theme.spacing * 1.5 - - Layout.margins: spacing - } - - HGridLayout { - id: buttonGrid - visible: buttonModel.length > 0 - flow: width >= buttonRepeater.summedImplicitWidth ? - GridLayout.LeftToRight : GridLayout.TopToBottom - - HRepeater { - id: buttonRepeater - model: [] - - onItemAdded: if (index === 0 && box) - box.firstButton = buttonRepeater.itemAt(0) - - onItemRemoved: if (index === 0 && box) - box.firstButton = null - - HButton { - id: button - text: modelData.text - icon.name: modelData.iconName || "" - icon.color: modelData.iconColor || ( - name === "ok" || name === "apply" || name === "retry" ? - theme.colors.positiveBackground : - - name === "cancel" ? - theme.colors.negativeBackground : - - theme.icons.colorize - ) - - enabled: - modelData.enabled === undefined ? - true : modelData.enabled - - loading: modelData.loading || false - - disableWhileLoading: - modelData.disableWhileLoading === undefined ? - true : modelData.disableWhileLoading - - onClicked: buttonCallbacks[name](button) - - Keys.onLeftPressed: previous.forceActiveFocus() - Keys.onUpPressed: previous.forceActiveFocus() - Keys.onRightPressed: next.forceActiveFocus() - Keys.onDownPressed: next.forceActiveFocus() - - Component.onCompleted: - if (name === focusButton) forceActiveFocus() - - Layout.fillWidth: true - Layout.preferredHeight: theme.baseElementsHeight - - - property string name: modelData.name - - property Item next: buttonRepeater.itemAt( - utils.numberWrapAt(index + 1, buttonRepeater.count), - ) - property Item previous: buttonRepeater.itemAt( - utils.numberWrapAt(index - 1, buttonRepeater.count), - ) - } - } - } - } + Behavior on implicitWidth { HNumberAnimation {} } + Behavior on implicitHeight { HNumberAnimation {} } } diff --git a/src/gui/Base/HButtonContent.qml b/src/gui/Base/HButtonContent.qml index 3dc7a831..0da9fb1f 100644 --- a/src/gui/Base/HButtonContent.qml +++ b/src/gui/Base/HButtonContent.qml @@ -6,7 +6,6 @@ import QtQuick.Layouts 1.12 HRowLayout { id: buttonContent - implicitHeight: theme.baseElementsHeight spacing: button.spacing opacity: loading ? theme.loadingElementsOpacity : enabled ? 1 : theme.disabledElementsOpacity diff --git a/src/gui/Base/HCheckBox.qml b/src/gui/Base/HCheckBox.qml index a892822d..1b307c45 100644 --- a/src/gui/Base/HCheckBox.qml +++ b/src/gui/Base/HCheckBox.qml @@ -7,7 +7,7 @@ import QtQuick.Layouts 1.12 CheckBox { id: box checked: defaultChecked - spacing: theme.spacing + spacing: contentItem.visible ? theme.spacing : 0 padding: 0 indicator: Rectangle { @@ -33,21 +33,26 @@ CheckBox { HIcon { anchors.centerIn: parent dimension: parent.width - 2 - svgName: "check-mark" colorize: theme.controls.checkBox.checkIconColorize + svgName: + box.checkState === Qt.PartiallyChecked ? + "check-mark-partial" : + "check-mark" - scale: box.checked ? 1 : 0 + scale: box.checkState === Qt.Unchecked ? 0 : 1 Behavior on scale { HNumberAnimation { overshoot: 4 easing.type: Easing.InOutBack + factor: 0.5 } } } } contentItem: HColumnLayout { + visible: mainText.text || subtitleText.text opacity: box.enabled ? 1 : theme.disabledElementsOpacity HLabel { diff --git a/src/gui/Base/HColumnPage.qml b/src/gui/Base/HColumnPage.qml index f640b7d2..9903acb8 100644 --- a/src/gui/Base/HColumnPage.qml +++ b/src/gui/Base/HColumnPage.qml @@ -7,10 +7,16 @@ HPage { default property alias columnData: column.data + property alias column: column + + + implicitWidth: theme.controls.box.defaultWidth + contentHeight: column.childrenRect.height HColumnLayout { id: column anchors.fill: parent + spacing: theme.spacing * 1.5 } } diff --git a/src/gui/Base/HFlickableColumnPage.qml b/src/gui/Base/HFlickableColumnPage.qml index 6ae05a21..de497161 100644 --- a/src/gui/Base/HFlickableColumnPage.qml +++ b/src/gui/Base/HFlickableColumnPage.qml @@ -5,6 +5,9 @@ import "../ShortcutBundles" HPage { id: page + implicitWidth: theme.controls.box.defaultWidth + contentHeight: + flickable.contentHeight + flickable.topMargin + flickable.bottomMargin default property alias columnData: column.data @@ -19,9 +22,14 @@ HPage { HFlickable { id: flickable anchors.fill: parent - clip: true contentWidth: parent.width - contentHeight: column.childrenRect.height + column.padding * 2 + contentHeight: column.implicitHeight + clip: true + + topMargin: theme.spacing + bottomMargin: topMargin + leftMargin: topMargin + rightMargin: topMargin FlickShortcuts { id: flickShortcuts @@ -31,13 +39,9 @@ HPage { HColumnLayout { id: column - x: padding - y: padding - width: flickable.width - padding * 2 - height: flickable.height - padding * 2 - - property int padding: - page.currentSpacing < theme.spacing ? 0 : page.currentSpacing + width: + flickable.width - flickable.leftMargin - flickable.rightMargin + spacing: theme.spacing * 1.5 } } diff --git a/src/gui/Base/HLabeledItem.qml b/src/gui/Base/HLabeledItem.qml index 70a39a62..c81ef800 100644 --- a/src/gui/Base/HLabeledItem.qml +++ b/src/gui/Base/HLabeledItem.qml @@ -9,7 +9,7 @@ HColumnLayout { default property alias insideData: itemHolder.data - readonly property Item item: itemHolder.visibleChildren[0] + readonly property Item item: itemHolder.children[0] readonly property alias label: label readonly property alias errorLabel: errorLabel readonly property alias toolTip: toolTip diff --git a/src/gui/Base/HMenuSeparator.qml b/src/gui/Base/HMenuSeparator.qml new file mode 100644 index 00000000..3853d97a --- /dev/null +++ b/src/gui/Base/HMenuSeparator.qml @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later + +import QtQuick 2.12 +import QtQuick.Controls 2.12 + +MenuSeparator { + id: separator + padding: 0 + contentItem: Item { + implicitHeight: theme.spacing + } +} diff --git a/src/gui/Base/HPage.qml b/src/gui/Base/HPage.qml index 419319f1..379861f4 100644 --- a/src/gui/Base/HPage.qml +++ b/src/gui/Base/HPage.qml @@ -12,7 +12,11 @@ Page { property int currentSpacing: useVariableSpacing ? - Math.min(theme.spacing * width / 400, theme.spacing) : + Math.min( + theme.spacing * width / 400, + theme.spacing * height / 400, + theme.spacing, + ) : theme.spacing diff --git a/src/gui/Base/HPageHeader.qml b/src/gui/Base/HPageHeader.qml deleted file mode 100644 index 36041609..00000000 --- a/src/gui/Base/HPageHeader.qml +++ /dev/null @@ -1,50 +0,0 @@ -// SPDX-License-Identifier: LGPL-3.0-or-later - -import QtQuick 2.12 -import QtQuick.Layouts 1.12 - -Rectangle { - width: page.width - height: show ? theme.baseElementsHeight : 0 - visible: height > 0 - color: theme.controls.header.background - - - property HPage page: parent - property bool show: mainUI.mainPane.collapse - - - Behavior on height { HNumberAnimation {} } - - HRowLayout { - anchors.fill: parent - - HButton { - id: goToMainPaneButton - padded: false - backgroundColor: "transparent" - icon.name: "go-back-to-main-pane" - toolTip.text: qsTr("Go back to main pane") - - onClicked: mainUI.mainPane.toggleFocus() - - Layout.preferredWidth: theme.baseElementsHeight - Layout.fillHeight: true - } - - HLabel { - text: page.title - elide: Text.ElideRight - horizontalAlignment: Qt.AlignHCenter - verticalAlignment: Qt.AlignVCenter - - Layout.fillWidth: true - Layout.fillHeight: true - } - - Item { - Layout.preferredWidth: goToMainPaneButton.width - Layout.fillHeight: true - } - } -} diff --git a/src/gui/Base/HPopup.qml b/src/gui/Base/HPopup.qml index 2f05b5fe..711eced4 100644 --- a/src/gui/Base/HPopup.qml +++ b/src/gui/Base/HPopup.qml @@ -6,12 +6,15 @@ import CppUtils 0.1 Popup { id: popup - anchors.centerIn: Overlay.overlay modal: true focus: true padding: 0 margins: theme.spacing + // FIXME: Qt 5.15: `anchors.centerIn: Overlay.overlay` + transition broken + x: (parent.width - width) / 2 + y: (parent.height - height) / 2 + enter: Transition { HNumberAnimation { property: "scale"; from: 0; to: 1; overshoot: 4 } } diff --git a/src/gui/Base/HTabContainer.qml b/src/gui/Base/HTabContainer.qml deleted file mode 100644 index 3fd51bec..00000000 --- a/src/gui/Base/HTabContainer.qml +++ /dev/null @@ -1,39 +0,0 @@ -// SPDX-License-Identifier: LGPL-3.0-or-later - -import QtQuick 2.12 -import QtQuick.Controls 2.12 -import QtQuick.Layouts 1.12 - -HColumnLayout { - Layout.alignment: Qt.AlignCenter - Layout.fillWidth: false - Layout.fillHeight: false - Layout.maximumWidth: parent.width - - property alias tabIndex: tabBar.currentIndex - property alias tabModel: tabRepeater.model - default property alias data: swipeView.contentData - - HTabBar { - id: tabBar - - Layout.fillWidth: true - - Repeater { - id: tabRepeater - HTabButton { text: modelData } - } - } - - SwipeView { - id: swipeView - clip: true - currentIndex: tabBar.currentIndex - interactive: false - - Layout.fillWidth: true - - Behavior on implicitWidth { HNumberAnimation {} } - Behavior on implicitHeight { HNumberAnimation {} } - } -} diff --git a/src/gui/Base/HTabbedBox.qml b/src/gui/Base/HTabbedBox.qml new file mode 100644 index 00000000..932512e4 --- /dev/null +++ b/src/gui/Base/HTabbedBox.qml @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later + +import QtQuick 2.12 +import QtQuick.Controls 2.12 +import QtQuick.Layouts 1.12 + +HPage { + default property alias swipeViewData: swipeView.contentData + + + contentWidth: + Math.max(swipeView.contentWidth, theme.controls.box.defaultWidth) + + header: HTabBar {} + + background: Rectangle { + color: theme.controls.box.background + radius: theme.controls.box.radius + } + + HNumberAnimation on scale { + running: true + from: 0 + to: 1 + overshoot: 3 + } + + Behavior on implicitWidth { HNumberAnimation {} } + Behavior on implicitHeight { HNumberAnimation {} } + + Binding { + target: header + property: "currentIndex" + value: swipeView.currentIndex + } + + SwipeView { + id: swipeView + anchors.fill: parent + clip: true + currentIndex: header.currentIndex + + onCurrentItemChanged: currentItem.takeFocus() + } +} diff --git a/src/gui/Dialogs/ExportKeys.qml b/src/gui/Dialogs/ExportKeys.qml index 8a6bac35..784825ff 100644 --- a/src/gui/Dialogs/ExportKeys.qml +++ b/src/gui/Dialogs/ExportKeys.qml @@ -37,8 +37,8 @@ HFileDialogOpener { PasswordPopup { id: exportPasswordPopup - details.text: qsTr("Passphrase to protect this file:") - okText: qsTr("Export") + summary.text: qsTr("Passphrase to protect this file:") + validateButton.text: qsTr("Export") onAcceptedPasswordChanged: exportKeys(file, acceptedPassword) diff --git a/src/gui/Dialogs/ImportKeys.qml b/src/gui/Dialogs/ImportKeys.qml index afc8a812..42c2a46d 100644 --- a/src/gui/Dialogs/ImportKeys.qml +++ b/src/gui/Dialogs/ImportKeys.qml @@ -16,17 +16,16 @@ HFileDialogOpener { property string userId: "" - property bool importing: false property Future importFuture: null PasswordPopup { id: importPasswordPopup - details.text: - importing ? + summary.text: + importFuture ? qsTr("This might take a while...") : qsTr("Passphrase used to protect this file:") - okText: qsTr("Import") + validateButton.text: qsTr("Import") onClosed: if (importFuture) importFuture.cancel() @@ -35,13 +34,10 @@ HFileDialogOpener { function verifyPassword(pass, callback) { - importing = true - const call = py.callClientCoro const path = file.toString().replace(/^file:\/\//, "") importFuture = call(userId, "import_keys", [path, pass], () => { - importing = false importFuture = null callback(true) @@ -78,7 +74,7 @@ HFileDialogOpener { Binding on closePolicy { value: Popup.CloseOnEscape - when: importing + when: importFuture } } } diff --git a/src/gui/Pages/AccountSettings/Account.qml b/src/gui/Pages/AccountSettings/Account.qml new file mode 100644 index 00000000..30681a9d --- /dev/null +++ b/src/gui/Pages/AccountSettings/Account.qml @@ -0,0 +1,247 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later + +import QtQuick 2.12 +import QtQuick.Controls 2.12 +import QtQuick.Layouts 1.12 +import "../.." +import "../../Base" +import "../../Base/ButtonLayout" +import "../../Dialogs" + +HFlickableColumnPage { + id: page + + + property string userId + readonly property QtObject account: ModelStore.get("accounts").find(userId) + + + function takeFocus() { + nameField.item.forceActiveFocus() + } + + function applyChanges() { + if (nameField.item.changed) { + saveButton.nameChangeRunning = true + + py.callClientCoro( + userId, "set_displayname", [nameField.item.text], () => { + py.callClientCoro(userId, "update_own_profile", [], () => { + saveButton.nameChangeRunning = false + }) + } + ) + } + + if (aliasField.item.changed) { + window.settings.writeAliases[userId] = aliasField.item.text + window.settingsChanged() + } + + if (avatar.changed) { + saveButton.avatarChangeRunning = true + + const path = + Qt.resolvedUrl(avatar.sourceOverride).replace(/^file:/, "") + + py.callClientCoro(userId, "set_avatar_from_file", [path], () => { + py.callClientCoro(userId, "update_own_profile", [], () => { + saveButton.avatarChangeRunning = false + }) + }, (errType, [httpCode]) => { + console.error("Avatar upload failed:", httpCode, errType) + saveButton.avatarChangeRunning = false + }) + } + } + + function cancel() { + nameField.item.reset() + aliasField.item.reset() + fileDialog.selectedFile = "" + fileDialog.file = "" + } + + + footer: ButtonLayout { + ApplyButton { + id: saveButton + + property bool nameChangeRunning: false + property bool avatarChangeRunning: false + + disableWhileLoading: false + loading: nameChangeRunning || avatarChangeRunning + enabled: + avatar.changed || + nameField.item.changed || + (aliasField.item.changed && ! aliasField.alreadyTakenBy) + + onClicked: applyChanges() + } + + CancelButton { + enabled: saveButton.enabled && ! saveButton.loading + onClicked: cancel() + } + } + + Keys.onEscapePressed: cancel() + + + HUserAvatar { + property bool changed: Boolean(sourceOverride) + + id: avatar + userId: page.userId + displayName: nameField.item.text + mxc: account.avatar_url + toolTipMxc: "" + sourceOverride: fileDialog.selectedFile || fileDialog.file + + Layout.alignment: Qt.AlignHCenter + Layout.fillWidth: true + // Layout.preferredWidth: 256 * theme.uiScale + Layout.preferredHeight: width + + Rectangle { + z: 10 + visible: opacity > 0 + opacity: ! fileDialog.dialog.visible && + ((! avatar.mxc && ! avatar.changed) || avatar.hovered) ? + 1 : 0 + + anchors.fill: parent + color: utils.hsluv( + 0, 0, 0, (! avatar.mxc && overlayHover.hovered) ? 0.8 : 0.7, + ) + + Behavior on opacity { HNumberAnimation {} } + Behavior on color { HColorAnimation {} } + + HoverHandler { id: overlayHover } + + MouseArea { + anchors.fill: parent + acceptedButtons: Qt.NoButton + cursorShape: + overlayHover.hovered ? + Qt.PointingHandCursor : Qt.ArrowCursor + } + + HColumnLayout { + anchors.centerIn: parent + spacing: currentSpacing + width: parent.width + + HIcon { + svgName: "upload-avatar" + colorize: (! avatar.mxc && overlayHover.hovered) ? + theme.colors.accentText : theme.icons.colorize + dimension: avatar.width / 3 + + Layout.alignment: Qt.AlignCenter + } + + Item { Layout.preferredHeight: theme.spacing } + + HLabel { + text: avatar.mxc ? + qsTr("Change profile picture") : + qsTr("Upload profile picture") + + color: (! avatar.mxc && overlayHover.hovered) ? + theme.colors.accentText : theme.colors.brightText + Behavior on color { HColorAnimation {} } + + font.pixelSize: theme.fontSize.small + wrapMode: Text.WordWrap + horizontalAlignment: Qt.AlignHCenter + + Layout.fillWidth: true + } + } + } + + HFileDialogOpener { + id: fileDialog + fileType: HFileDialogOpener.FileType.Images + dialog.title: qsTr("Select profile picture for %1") + .arg(account.display_name) + } + } + + HLabel { + text: qsTr("User ID:
%1") + .arg(utils.coloredNameHtml(userId, userId, userId)) + textFormat: Text.StyledText + wrapMode: Text.Wrap + lineHeight: 1.1 + + Layout.fillWidth: true + } + + HLabeledItem { + id: nameField + label.text: qsTr("Display name:") + + Layout.fillWidth: true + + HTextField { + width: parent.width + defaultText: account.display_name + maximumLength: 255 + + // TODO: Qt 5.14+: use a Binding enabled when text not empty + color: utils.nameColor(text) + + onAccepted: applyChanges() + } + } + + HLabeledItem { + readonly property var aliases: window.settings.writeAliases + readonly property string currentAlias: aliases[userId] || "" + + readonly property string alreadyTakenBy: { + if (! item.text) return "" + + for (const [id, idAlias] of Object.entries(aliases)) + if (id !== userId && idAlias === item.text) return id + + return "" + } + + + id: aliasField + + label.text: qsTr("Composer alias:") + + errorLabel.text: + alreadyTakenBy ? + qsTr("Taken by %1").arg(alreadyTakenBy) : + "" + + toolTip.text: qsTr( + "From any chat, start a message with specified alias " + + "followed by a space to type and send as this " + + "account.\n" + + "The account must have permission to talk in the room.\n"+ + "To ignore the alias when typing, prepend it with a space." + ) + + Layout.fillWidth: true + + HTextField { + width: parent.width + error: aliasField.alreadyTakenBy !== "" + onAccepted: applyChanges() + defaultText: aliasField.currentAlias + placeholderText: qsTr("e.g. %1").arg(( + nameField.item.text || + account.display_name || + userId.substring(1) + )[0]) + } + } +} diff --git a/src/gui/Pages/AccountSettings/AccountSettings.qml b/src/gui/Pages/AccountSettings/AccountSettings.qml index cda105ce..ce7a0cea 100644 --- a/src/gui/Pages/AccountSettings/AccountSettings.qml +++ b/src/gui/Pages/AccountSettings/AccountSettings.qml @@ -6,55 +6,26 @@ import QtQuick.Layouts 1.12 import "../.." import "../../Base" -HFlickableColumnPage { - id: accountSettings - title: qsTr("Account settings") - header: HPageHeader {} +HPage { + id: page - property int avatarPreferredSize: 256 * theme.uiScale - - property string userId: "" - - readonly property bool ready: - accountInfo !== null && accountInfo.profile_updated > new Date(1) - - readonly property QtObject accountInfo: - ModelStore.get("accounts").find(userId) - - property string headerName: ready ? accountInfo.display_name : userId + property string userId - HSpacer {} + HTabbedBox { + anchors.centerIn: parent + width: Math.min(implicitWidth, page.availableWidth) + height: Math.min(implicitHeight, page.availableHeight) - Repeater { - id: repeater - model: ["Profile.qml", "ImportExportKeys.qml"] - - Rectangle { - color: ready ? theme.controls.box.background : "transparent" - Behavior on color { HColorAnimation {} } - - Layout.alignment: Qt.AlignCenter - Layout.topMargin: index > 0 ? theme.spacing : 0 - Layout.bottomMargin: index < repeater.count - 1 ? theme.spacing : 0 - - Layout.maximumWidth: Math.min(parent.width, 640) - Layout.preferredWidth: - pageLoader.isWide ? parent.width : avatarPreferredSize - - Layout.preferredHeight: childrenRect.height - - HLoader { - anchors.centerIn: parent - width: ready ? parent.width : 96 - source: ready ? - modelData : - (modelData === "Profile.qml" ? - "../../Base/HBusyIndicator.qml" : "") - } + header: HTabBar { + HTabButton { text: qsTr("Account") } + HTabButton { text: qsTr("Encryption") } + HTabButton { text: qsTr("Sessions") } } - } - HSpacer {} + Account { userId: page.userId } + Encryption { userId: page.userId } + Sessions { userId: page.userId } + } } diff --git a/src/gui/Pages/AccountSettings/DeviceDelegate.qml b/src/gui/Pages/AccountSettings/DeviceDelegate.qml new file mode 100644 index 00000000..1aa4732c --- /dev/null +++ b/src/gui/Pages/AccountSettings/DeviceDelegate.qml @@ -0,0 +1,113 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later + +import QtQuick 2.12 +import QtQuick.Layouts 1.12 +import "../../Base" +import "../../Base/ButtonLayout" +import "../../Base/HTile" + +HTile { + id: device + + + property HListView view + + + backgroundColor: "transparent" + compact: false + + leftPadding: theme.spacing * 2 + rightPadding: 0 + + contentItem: ContentRow { + tile: device + spacing: 0 + + HCheckBox { + id: checkBox + checked: view.checked[model.id] || false + onClicked: view.toggleCheck(model.index) + } + + HColumnLayout { + Layout.leftMargin: theme.spacing + + HRowLayout { + spacing: theme.spacing + + TitleLabel { + text: model.display_name || qsTr("Unnamed") + } + + TitleRightInfoLabel { + tile: device + text: utils.smartFormatDate(model.last_seen_date) + } + } + + SubtitleLabel { + tile: device + font.family: theme.fontFamily.mono + text: + model.last_seen_ip ? + model.id + " " + model.last_seen_ip : + model.id + } + } + + HButton { + icon.name: "device-action-menu" + toolTip.text: qsTr("Rename, verify or sign out") + backgroundColor: "transparent" + onClicked: contextMenuLoader.active = true + + Layout.fillHeight: true + } + } + + contextMenu: HMenu { + id: actionMenu + implicitWidth: Math.min(320 * theme.uiScale, window.width) + onOpened: nameField.forceActiveFocus() + + HLabeledItem { + width: parent.width + label.topPadding: theme.spacing / 2 + label.text: qsTr("Public display name:") + label.horizontalAlignment: Qt.AlignHCenter + + HTextField { + id: nameField + width: parent.width + defaultText: model.display_name + horizontalAlignment: Qt.AlignHCenter + } + } + + HMenuSeparator {} + + HLabeledItem { + width: parent.width + label.text: qsTr("Actions:") + label.horizontalAlignment: Qt.AlignHCenter + + ButtonLayout { + width: parent.width + + ApplyButton { + enabled: + model.type !== "current" && model.type !== "verified" + text: qsTr("Verify") + icon.name: "device-verify" + } + + CancelButton { + text: qsTr("Sign out") + icon.name: "device-delete" + } + } + } + } + + onLeftClicked: checkBox.clicked() +} diff --git a/src/gui/Pages/AccountSettings/DeviceSection.qml b/src/gui/Pages/AccountSettings/DeviceSection.qml new file mode 100644 index 00000000..8fb4fe3d --- /dev/null +++ b/src/gui/Pages/AccountSettings/DeviceSection.qml @@ -0,0 +1,75 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later + +import QtQuick 2.12 +import QtQuick.Layouts 1.12 +import "../../Base" + +HRowLayout { + property HListView view + + readonly property int sectionCheckedCount: + Object.values(deviceList.checked).filter( + item => item.type === section + ).length + + readonly property int sectionTotalCount: + deviceList.sectionItemCounts[section] || 0 + + + HCheckBox { + padding: theme.spacing + topPadding: padding * (section === "current" ? 1 : 2) + + text: + section === "current" ? qsTr("Current session") : + section === "verified" ? qsTr("Verified") : + section === "ignored" ? qsTr("Ignored") : + section === "blacklisted" ? qsTr("Blacklisted") : + qsTr("Unverified") + + tristate: true + + checkState: + sectionTotalCount === sectionCheckedCount ? Qt.Checked : + ! sectionCheckedCount ? Qt.Unchecked : + Qt.PartiallyChecked + + nextCheckState: + checkState === Qt.Checked ? Qt.Unchecked : Qt.Checked + + onClicked: { + const indice = [] + + for (let i = 0; i < deviceList.count; i++) { + if (deviceList.model.get(i).type === section) + indice.push(i) + } + + const checkedItems = Object.values(deviceList.checked) + + checkedItems.some(item => item.type === section) ? + deviceList.uncheck(...indice) : + deviceList.check(...indice) + } + + Layout.fillWidth: true + } + + HLabel { + text: + sectionCheckedCount ? + qsTr("%1 / %2") + .arg(sectionCheckedCount).arg(sectionTotalCount) : + sectionTotalCount + + rightPadding: theme.spacing * 1.5 + color: + section === "current" || section === "verified" ? + theme.colors.positiveText : + + section === "unset" || section === "ignored" ? + theme.colors.warningText : + + theme.colors.errorText + } +} diff --git a/src/gui/Pages/AccountSettings/ImportExportKeys.qml b/src/gui/Pages/AccountSettings/Encryption.qml similarity index 50% rename from src/gui/Pages/AccountSettings/ImportExportKeys.qml rename to src/gui/Pages/AccountSettings/Encryption.qml index 85845989..f6a9d5db 100644 --- a/src/gui/Pages/AccountSettings/ImportExportKeys.qml +++ b/src/gui/Pages/AccountSettings/Encryption.qml @@ -3,41 +3,53 @@ import QtQuick 2.12 import QtQuick.Layouts 1.12 import "../../Base" +import "../../Base/ButtonLayout" -HBox { - buttonModel: [ - { name: "export", text: qsTr("Export"), iconName: "export-keys"}, - { name: "import", text: qsTr("Import"), iconName: "import-keys"}, - ] +HFlickableColumnPage { + id: page - buttonCallbacks: ({ - export: button => { - utils.makeObject( + + property string userId + + + function takeFocus() { exportButton.forceActiveFocus() } + + + footer: ButtonLayout { + OtherButton { + id: exportButton + text: qsTr("Export") + icon.name: "export-keys" + + onClicked: utils.makeObject( "Dialogs/ExportKeys.qml", - accountSettings, - { userId: accountSettings.userId }, + page, + { userId: page.userId }, obj => { - button.loading = Qt.binding(() => obj.exporting) + loading = Qt.binding(() => obj.exporting) obj.dialog.open() } ) - }, - import: button => { - utils.makeObject( + } + + OtherButton { + text: qsTr("Import") + icon.name: "import-keys" + + onClicked: utils.makeObject( "Dialogs/ImportKeys.qml", - accountSettings, - { userId: accountSettings.userId }, + page, + { userId: page.userId }, obj => { obj.dialog.open() } ) - }, - }) - + } + } HLabel { wrapMode: Text.Wrap text: qsTr( "The decryption keys for messages received in encrypted rooms " + - "until present time can be backed up " + + "until present time can be saved " + "to a passphrase-protected file.

" + "You can then import this file on any Matrix account or " + diff --git a/src/gui/Pages/AccountSettings/Profile.qml b/src/gui/Pages/AccountSettings/Profile.qml deleted file mode 100644 index 7d7527ea..00000000 --- a/src/gui/Pages/AccountSettings/Profile.qml +++ /dev/null @@ -1,271 +0,0 @@ -// SPDX-License-Identifier: LGPL-3.0-or-later - -import QtQuick 2.12 -import QtQuick.Controls 2.12 -import QtQuick.Layouts 1.12 -import "../../Base" -import "../../Dialogs" - -HGridLayout { - function applyChanges() { - if (nameField.changed) { - saveButton.nameChangeRunning = true - - py.callClientCoro( - userId, "set_displayname", [nameField.item.text], () => { - py.callClientCoro(userId, "update_own_profile", [], () => { - saveButton.nameChangeRunning = false - accountSettings.headerName = - Qt.binding(() => accountInfo.display_name) - }) - } - ) - } - - if (aliasField.changed) { - window.settings.writeAliases[userId] = aliasField.item.text - window.settingsChanged() - } - - if (avatar.changed) { - saveButton.avatarChangeRunning = true - - const path = - Qt.resolvedUrl(avatar.sourceOverride).replace(/^file:/, "") - - py.callClientCoro(userId, "set_avatar_from_file", [path], () => { - py.callClientCoro(userId, "update_own_profile", [], () => { - saveButton.avatarChangeRunning = false - }) - }, (errType, [httpCode]) => { - console.error("Avatar upload failed:", httpCode, errType) - saveButton.avatarChangeRunning = false - }) - } - } - - function cancelChanges() { - nameField.item.text = accountInfo.display_name - aliasField.item.text = aliasField.currentAlias - fileDialog.selectedFile = "" - fileDialog.file = "" - - accountSettings.headerName = Qt.binding(() => accountInfo.display_name) - } - - columns: 2 - flow: pageLoader.isWide ? GridLayout.LeftToRight : GridLayout.TopToBottom - rowSpacing: currentSpacing - - Component.onCompleted: nameField.item.forceActiveFocus() - - HUserAvatar { - property bool changed: Boolean(sourceOverride) - - id: avatar - userId: accountSettings.userId - displayName: nameField.item.text - mxc: accountInfo.avatar_url - toolTipMxc: "" - sourceOverride: fileDialog.selectedFile || fileDialog.file - - Layout.alignment: Qt.AlignHCenter - - Layout.preferredWidth: Math.min(flickable.height, avatarPreferredSize) - Layout.preferredHeight: Layout.preferredWidth - - Rectangle { - z: 10 - visible: opacity > 0 - opacity: ! fileDialog.dialog.visible && - ((! avatar.mxc && ! avatar.changed) || avatar.hovered) ? - 1 : 0 - - anchors.fill: parent - color: utils.hsluv(0, 0, 0, - (! avatar.mxc && overlayHover.hovered) ? 0.8 : 0.7 - ) - - Behavior on opacity { HNumberAnimation {} } - Behavior on color { HColorAnimation {} } - - HoverHandler { id: overlayHover } - - MouseArea { - anchors.fill: parent - acceptedButtons: Qt.NoButton - cursorShape: - overlayHover.hovered ? - Qt.PointingHandCursor : Qt.ArrowCursor - } - - HColumnLayout { - anchors.centerIn: parent - spacing: currentSpacing - width: parent.width - - HIcon { - svgName: "upload-avatar" - colorize: (! avatar.mxc && overlayHover.hovered) ? - theme.colors.accentText : theme.icons.colorize - dimension: avatar.width / 3 - - Layout.alignment: Qt.AlignCenter - } - - Item { Layout.preferredHeight: theme.spacing } - - HLabel { - text: avatar.mxc ? - qsTr("Change profile picture") : - qsTr("Upload profile picture") - - color: (! avatar.mxc && overlayHover.hovered) ? - theme.colors.accentText : theme.colors.brightText - Behavior on color { HColorAnimation {} } - - font.pixelSize: theme.fontSize.big * - avatar.height / avatarPreferredSize - wrapMode: Text.WordWrap - horizontalAlignment: Qt.AlignHCenter - - Layout.fillWidth: true - } - } - } - - HFileDialogOpener { - id: fileDialog - fileType: HFileDialogOpener.FileType.Images - dialog.title: qsTr("Select profile picture for %1") - .arg(accountInfo.display_name) - } - } - - HColumnLayout { - id: profileInfo - spacing: theme.spacing - - HColumnLayout { - spacing: theme.spacing - Layout.margins: currentSpacing - - HLabel { - text: qsTr("User ID:
%1") - .arg(utils.coloredNameHtml(userId, userId, userId)) - textFormat: Text.StyledText - wrapMode: Text.Wrap - - Layout.fillWidth: true - } - - HLabeledItem { - property bool changed: item.text !== accountInfo.display_name - - id: nameField - label.text: qsTr("Display name:") - - Layout.fillWidth: true - Layout.maximumWidth: 480 - - HTextField { - width: parent.width - maximumLength: 255 - - onAccepted: applyChanges() - onTextChanged: accountSettings.headerName = text - Component.onCompleted: text = accountInfo.display_name - - Keys.onEscapePressed: cancelChanges() - } - } - - HLabeledItem { - property string currentAlias: aliases[userId] || "" - property bool changed: item.text !== currentAlias - - readonly property var aliases: window.settings.writeAliases - - readonly property string alreadyTakenBy: { - if (! item.text) return "" - - for (const [id, idAlias] of Object.entries(aliases)) - if (id !== userId && idAlias === item.text) return id - - return "" - } - - - id: aliasField - - label.text: qsTr("Composer alias:") - - errorLabel.text: - alreadyTakenBy ? - qsTr("Taken by %1").arg(alreadyTakenBy) : - "" - - toolTip.text: qsTr( - "From any chat, start a message with specified alias " + - "followed by a space to type and send as this " + - "account.\n" + - "The account must have permission to talk in the room.\n"+ - "To ignore the alias when typing, prepend it with a space." - ) - - Layout.fillWidth: true - Layout.maximumWidth: 480 - - HTextField { - width: parent.width - error: aliasField.alreadyTakenBy !== "" - onAccepted: applyChanges() - placeholderText: qsTr("e.g. %1").arg(( - nameField.item.text || - accountInfo.display_name || - userId.substring(1) - )[0]) - - Component.onCompleted: text = aliasField.currentAlias - - Keys.onEscapePressed: cancelChanges() - } - } - } - - HRowLayout { - Layout.alignment: Qt.AlignBottom - - HButton { - property bool nameChangeRunning: false - property bool avatarChangeRunning: false - - id: saveButton - icon.name: "apply" - icon.color: theme.colors.positiveBackground - text: qsTr("Save") - loading: nameChangeRunning || avatarChangeRunning - enabled: - avatar.changed || - nameField.changed || - (aliasField.changed && ! aliasField.alreadyTakenBy) - - onClicked: applyChanges() - - Layout.fillWidth: true - Layout.alignment: Qt.AlignBottom - } - - HButton { - icon.name: "cancel" - icon.color: theme.colors.negativeBackground - text: qsTr("Cancel") - enabled: saveButton.enabled && ! saveButton.loading - onClicked: cancelChanges() - - Layout.fillWidth: true - Layout.alignment: Qt.AlignBottom - } - } - } -} diff --git a/src/gui/Pages/AccountSettings/Sessions.qml b/src/gui/Pages/AccountSettings/Sessions.qml new file mode 100644 index 00000000..dd895dec --- /dev/null +++ b/src/gui/Pages/AccountSettings/Sessions.qml @@ -0,0 +1,95 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later + +import QtQuick 2.12 +import QtQuick.Layouts 1.12 +import "../../Base" +import "../../Base/ButtonLayout" +import "../../PythonBridge" + +HColumnPage { + id: page + + + property string userId + + property Future loadFuture: null + + + function takeFocus() {} // XXX + + function loadDevices() { + loadFuture = py.callClientCoro(userId, "devices_info", [], devices => { + deviceList.checked = {} + deviceList.model.clear() + + for (const device of devices) + deviceList.model.append(device) + + loadFuture = null + }) + } + + + footer: ButtonLayout { + visible: height >= 0 + height: deviceList.selectedCount ? implicitHeight : 0 + + Behavior on height { HNumberAnimation {} } + + OtherButton { + text: + deviceList.selectedCount === 1 ? + qsTr("Sign out checked session") : + qsTr("Sign out %1 sessions").arg(deviceList.selectedCount) + + icon.name: "device-delete-checked" + icon.color: theme.colors.negativeBackground + } + } + + + HListView { + id: deviceList + + readonly property var sectionItemCounts: { + const counts = {} + + for (let i = 0; i < count; i++) { + const section = model.get(i).type + section in counts ? counts[section] += 1 : counts[section] = 1 + } + + return counts + } + + clip: true + model: ListModel {} + delegate: DeviceDelegate { + width: deviceList.width + view: deviceList + } + + section.property: "type" + section.delegate: DeviceSection { + width: deviceList.width + view: deviceList + } + + Component.onCompleted: page.loadDevices() + + Layout.fillWidth: true + Layout.fillHeight: true + + HLoader { + anchors.centerIn: parent + width: 96 * theme.uiScale + height: width + + source: "../../Base/HBusyIndicator.qml" + active: page.loadFuture + opacity: active ? 1 : 0 + + Behavior on opacity { HNumberAnimation { factor: 2 } } + } + } +} diff --git a/src/gui/Pages/AddAccount/AddAccount.qml b/src/gui/Pages/AddAccount/AddAccount.qml index 8809636b..f8c51d2c 100644 --- a/src/gui/Pages/AddAccount/AddAccount.qml +++ b/src/gui/Pages/AddAccount/AddAccount.qml @@ -4,16 +4,21 @@ import QtQuick 2.12 import QtQuick.Layouts 1.12 import "../../Base" -HFlickableColumnPage { - title: qsTr("Add an account") - header: HPageHeader {} +HPage { + id: page - HTabContainer { - tabModel: [ - qsTr("Sign in"), qsTr("Register"), qsTr("Reset"), - ] + HTabbedBox { + anchors.centerIn: parent + width: Math.min(implicitWidth, page.availableWidth) + height: Math.min(implicitHeight, page.availableHeight) - SignIn { Component.onCompleted: forceActiveFocus() } + header: HTabBar { + HTabButton { text: qsTr("Sign in") } + HTabButton { text: qsTr("Register") } + HTabButton { text: qsTr("Reset") } + } + + SignIn {} Register {} Reset {} } diff --git a/src/gui/Pages/AddAccount/Register.qml b/src/gui/Pages/AddAccount/Register.qml index d8fb6ba8..23b2293d 100644 --- a/src/gui/Pages/AddAccount/Register.qml +++ b/src/gui/Pages/AddAccount/Register.qml @@ -3,28 +3,30 @@ import QtQuick 2.12 import QtQuick.Layouts 1.12 import "../../Base" +import "../../Base/ButtonLayout" -HBox { - id: signInBox - clickButtonOnEnter: "ok" +HFlickableColumnPage { + function takeFocus() { registerButton.forceActiveFocus() } - buttonModel: [ - { name: "ok", text: qsTr("Register from Riot"), iconName: "register" }, - ] - buttonCallbacks: ({ - ok: button => { - Qt.openUrlExternally("https://riot.im/app/#/register") + footer: ButtonLayout { + ApplyButton { + id: registerButton + text: qsTr("Register from Riot") + icon.name: "register" + onClicked: Qt.openUrlExternally("https://riot.im/app/#/register") + + Layout.fillWidth: true } - }) + } HLabel { wrapMode: Text.Wrap horizontalAlignment: Qt.AlignHCenter text: qsTr( - "Not yet implemented\n\nYou can create a new " + - "account from another client such as Riot." + "Not implemented yet\n\n" + + "You can create a new account from another client such as Riot." ) Layout.fillWidth: true diff --git a/src/gui/Pages/AddAccount/Reset.qml b/src/gui/Pages/AddAccount/Reset.qml index 1cd015f6..21bec39d 100644 --- a/src/gui/Pages/AddAccount/Reset.qml +++ b/src/gui/Pages/AddAccount/Reset.qml @@ -3,32 +3,31 @@ import QtQuick 2.12 import QtQuick.Layouts 1.12 import "../../Base" +import "../../Base/ButtonLayout" -HBox { - id: signInBox - clickButtonOnEnter: "ok" +HFlickableColumnPage { + function takeFocus() { resetButton.forceActiveFocus() } - buttonModel: [ - { - name: "ok", - text: qsTr("Reset password from Riot"), - iconName: "reset-password" - }, - ] - buttonCallbacks: ({ - ok: button => { - Qt.openUrlExternally("https://riot.im/app/#/forgot_password") + footer: ButtonLayout { + ApplyButton { + id: resetButton + text: qsTr("Reset password from Riot") + icon.name: "reset-password" + onClicked: + Qt.openUrlExternally("https://riot.im/app/#/forgot_password") + + Layout.fillWidth: true } - }) + } HLabel { wrapMode: Text.Wrap horizontalAlignment: Qt.AlignHCenter text: qsTr( - "Not yet implemented\n\nYou can reset your " + - "password using another client such as Riot." + "Not implemented yet\n\n" + + "You can reset your password from another client such as Riot." ) Layout.fillWidth: true diff --git a/src/gui/Pages/AddAccount/SignIn.qml b/src/gui/Pages/AddAccount/SignIn.qml index 98b187ed..a443cb3c 100644 --- a/src/gui/Pages/AddAccount/SignIn.qml +++ b/src/gui/Pages/AddAccount/SignIn.qml @@ -3,82 +3,10 @@ import QtQuick 2.12 import QtQuick.Layouts 1.12 import "../../Base" +import "../../Base/ButtonLayout" -HBox { - id: signInBox - clickButtonOnEnter: "apply" - - onFocusChanged: idField.item.forceActiveFocus() - - buttonModel: [ - { - name: "apply", - text: qsTr("Sign in"), - enabled: canSignIn, - iconName: "sign-in", - loading: loginFuture !== null, - disableWhileLoading: false, - }, - { name: "cancel", text: qsTr("Cancel"), iconName: "cancel"}, - ] - - buttonCallbacks: ({ - apply: button => { - if (loginFuture) loginFuture.cancel() - - signInTimeout.restart() - - errorMessage.text = "" - - const args = [ - idField.item.text.trim(), passwordField.item.text, - undefined, serverField.item.text.trim(), - ] - - loginFuture = py.callCoro("login_client", args, userId => { - signInTimeout.stop() - errorMessage.text = "" - loginFuture = null - - py.callCoro( - rememberAccount.checked ? - "saved_accounts.add": "saved_accounts.delete", - - [userId] - ) - - pageLoader.showPage( - "AccountSettings/AccountSettings", {userId} - ) - - }, (type, args, error, traceback, uuid) => { - loginFuture = null - signInTimeout.stop() - - let txt = qsTr( - "Invalid request, login type or unknown error: %1", - ).arg(type) - - type === "MatrixForbidden" ? - txt = qsTr("Invalid username or password") : - - type === "MatrixUserDeactivated" ? - txt = qsTr("This account was deactivated") : - - utils.showError(type, traceback, uuid) - - errorMessage.text = txt - }) - }, - - cancel: button => { - if (! loginFuture) return - - signInTimeout.stop() - loginFuture.cancel() - loginFuture = null - } - }) +HFlickableColumnPage { + id: page property var loginFuture: null @@ -90,6 +18,83 @@ HBox { passwordField.item.text && ! serverField.item.error + function takeFocus() { idField.item.forceActiveFocus() } + + function signIn() { + if (page.loginFuture) page.loginFuture.cancel() + + signInTimeout.restart() + + errorMessage.text = "" + + const args = [ + idField.item.text.trim(), passwordField.item.text, + undefined, serverField.item.text.trim(), + ] + + page.loginFuture = py.callCoro("login_client", args, userId => { + signInTimeout.stop() + errorMessage.text = "" + page.loginFuture = null + + py.callCoro( + rememberAccount.checked ? + "saved_accounts.add": "saved_accounts.delete", + + [userId] + ) + + pageLoader.showPage( + "AccountSettings/AccountSettings", {userId} + ) + + }, (type, args, error, traceback, uuid) => { + page.loginFuture = null + signInTimeout.stop() + + let txt = qsTr( + "Invalid request, login type or unknown error: %1", + ).arg(type) + + type === "MatrixForbidden" ? + txt = qsTr("Invalid username or password") : + + type === "MatrixUserDeactivated" ? + txt = qsTr("This account was deactivated") : + + utils.showError(type, traceback, uuid) + + errorMessage.text = txt + }) + } + + function cancel() { + if (! page.loginFuture) return + + signInTimeout.stop() + page.loginFuture.cancel() + page.loginFuture = null + } + + + footer: ButtonLayout { + ApplyButton { + enabled: page.canSignIn + text: qsTr("Sign in") + icon.name: "sign-in" + loading: page.loginFuture !== null + disableWhileLoading: false + onClicked: page.signIn() + } + + CancelButton { + onClicked: page.cancel() + } + } + + Keys.onEscapePressed: page.cancel() + + Timer { id: signInTimeout interval: 30 * 1000 @@ -120,10 +125,10 @@ HBox { HButton { icon.name: modelData circle: true - checked: signInWith === modelData + checked: page.signInWith === modelData enabled: modelData === "username" autoExclusive: true - onClicked: signInWith = modelData + onClicked: page.signInWith = modelData } } } @@ -131,8 +136,8 @@ HBox { HLabeledItem { id: idField label.text: qsTr( - signInWith === "email" ? "Email:" : - signInWith === "phone" ? "Phone:" : + page.signInWith === "email" ? "Email:" : + page.signInWith === "phone" ? "Phone:" : "Username:" ) @@ -157,9 +162,6 @@ HBox { HLabeledItem { id: serverField - label.text: qsTr("Homeserver:") - - Layout.fillWidth: true // 2019-11-11 https://www.hello-matrix.net/public_servers.php readonly property var knownServers: [ @@ -182,6 +184,10 @@ HBox { readonly property bool knownServerChosen: knownServers.includes(item.cleanText) + label.text: qsTr("Homeserver:") + + Layout.fillWidth: true + HTextField { width: parent.width text: "https://matrix.org" diff --git a/src/gui/Pages/AddChat/AddChat.qml b/src/gui/Pages/AddChat/AddChat.qml index b8bebf93..4ad4a259 100644 --- a/src/gui/Pages/AddChat/AddChat.qml +++ b/src/gui/Pages/AddChat/AddChat.qml @@ -2,27 +2,28 @@ import QtQuick 2.12 import QtQuick.Layouts 1.12 -import "../.." import "../../Base" -HFlickableColumnPage { - id: addChatPage - title: qsTr("Add new chat") - header: HPageHeader {} +HPage { + id: page property string userId - readonly property QtObject account: ModelStore.get("accounts").find(userId) + HTabbedBox { + anchors.centerIn: parent + width: Math.min(implicitWidth, page.availableWidth) + height: Math.min(implicitHeight, page.availableHeight) - HTabContainer { - tabModel: [ - qsTr("Direct chat"), qsTr("Join room"), qsTr("Create room"), - ] + header: HTabBar { + HTabButton { text: qsTr("Direct chat") } + HTabButton { text: qsTr("Join room") } + HTabButton { text: qsTr("Create room") } + } - DirectChat { Component.onCompleted: forceActiveFocus() } - JoinRoom {} - CreateRoom {} + DirectChat { userId: page.userId } + JoinRoom { userId: page.userId } + CreateRoom { userId: page.userId } } } diff --git a/src/gui/Pages/AddChat/CreateRoom.qml b/src/gui/Pages/AddChat/CreateRoom.qml index 5b47ffee..279e4acd 100644 --- a/src/gui/Pages/AddChat/CreateRoom.qml +++ b/src/gui/Pages/AddChat/CreateRoom.qml @@ -2,57 +2,69 @@ import QtQuick 2.12 import QtQuick.Layouts 1.12 +import "../.." import "../../Base" +import "../../Base/ButtonLayout" -HBox { - id: addChatBox - clickButtonOnEnter: "apply" +HFlickableColumnPage { + id: page - onFocusChanged: nameField.item.forceActiveFocus() - buttonModel: [ - { name: "apply", text: qsTr("Create"), iconName: "room-create" }, - { name: "cancel", text: qsTr("Cancel"), iconName: "cancel" }, - ] + property string userId + readonly property QtObject account: ModelStore.get("accounts").find(userId) - buttonCallbacks: ({ - apply: button => { - button.loading = true - errorMessage.text = "" - const args = [ - nameField.item.text, - topicArea.item.text, - publicCheckBox.checked, - encryptCheckBox.checked, - ! blockOtherServersCheckBox.checked, - ] + function takeFocus() { nameField.item.forceActiveFocus() } - py.callClientCoro(userId, "new_group_chat", args, roomId => { - button.loading = false - pageLoader.showRoom(userId, roomId) - mainPane.roomList.startCorrectItemSearch() + function create() { + applyButton.loading = true + errorMessage.text = "" - }, (type, args) => { - button.loading = false - errorMessage.text = - qsTr("Unknown error - %1: %2").arg(type).arg(args) - }) - }, + const args = [ + nameField.item.text, + topicArea.item.text, + publicCheckBox.checked, + encryptCheckBox.checked, + ! blockOtherServersCheckBox.checked, + ] - cancel: button => { - nameField.item.text = "" - topicArea.item.text = "" - publicCheckBox.checked = false - encryptCheckBox.checked = false - blockOtherServersCheckBox.checked = false + py.callClientCoro(userId, "new_group_chat", args, roomId => { + applyButton.loading = false + pageLoader.showRoom(userId, roomId) + mainPane.roomList.startCorrectItemSearch() - pageLoader.showPrevious() + }, (type, args) => { + applyButton.loading = false + errorMessage.text = + qsTr("Unknown error - %1: %2").arg(type).arg(args) + }) + } + + function cancel() { + nameField.item.reset() + topicArea.item.reset() + publicCheckBox.reset() + encryptCheckBox.reset() + blockOtherServersCheckBox.reset() + + pageLoader.showPrevious() + } + + + footer: ButtonLayout { + ApplyButton { + id: applyButton + text: qsTr("Create") + icon.name: "room-create" + onClicked: create() } - }) + CancelButton { + onClicked: cancel() + } + } - readonly property string userId: addChatPage.userId + Keys.onEscapePressed: cancel() HRoomAvatar { @@ -70,6 +82,9 @@ HBox { opacity: nameField.item.text ? 0 : 1 visible: opacity > 0 + userId: page.userId + account: page.account + Behavior on opacity { HNumberAnimation {} } } } diff --git a/src/gui/Pages/AddChat/CurrentUserAvatar.qml b/src/gui/Pages/AddChat/CurrentUserAvatar.qml index 3c99b738..2593ff90 100644 --- a/src/gui/Pages/AddChat/CurrentUserAvatar.qml +++ b/src/gui/Pages/AddChat/CurrentUserAvatar.qml @@ -1,10 +1,17 @@ // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 +import QtQuick.Layouts 1.12 import "../../Base" HUserAvatar { - userId: addChatPage.userId - displayName: addChatPage.account ? addChatPage.account.display_name : "" - mxc: addChatPage.account ? addChatPage.account.avatar_url : "" + property QtObject account + + // userId: (set me) + displayName: account ? account.display_name : "" + mxc: account ? account.avatar_url : "" + + Layout.alignment: Qt.AlignCenter + Layout.preferredWidth: 128 + Layout.preferredHeight: Layout.preferredWidth } diff --git a/src/gui/Pages/AddChat/DirectChat.qml b/src/gui/Pages/AddChat/DirectChat.qml index 3c174759..337fa0b8 100644 --- a/src/gui/Pages/AddChat/DirectChat.qml +++ b/src/gui/Pages/AddChat/DirectChat.qml @@ -2,77 +2,91 @@ import QtQuick 2.12 import QtQuick.Layouts 1.12 +import "../.." import "../../Base" +import "../../Base/ButtonLayout" -HBox { - id: addChatBox - clickButtonOnEnter: "apply" +HFlickableColumnPage { + id: page - onFocusChanged: userField.item.forceActiveFocus() - buttonModel: [ - { - name: "apply", - text: qsTr("Start chat"), - iconName: "start-direct-chat", - enabled: Boolean(userField.item.text.trim()) - }, - { name: "cancel", text: qsTr("Cancel"), iconName: "cancel" }, - ] + property string userId + readonly property QtObject account: ModelStore.get("accounts").find(userId) - buttonCallbacks: ({ - apply: button => { - button.loading = true - errorMessage.text = "" - const args = [userField.item.text.trim(), encryptCheckBox.checked] + function takeFocus() { + userField.item.forceActiveFocus() + } - py.callClientCoro(userId, "new_direct_chat", args, roomId => { - button.loading = false - errorMessage.text = "" - pageLoader.showRoom(userId, roomId) - mainPane.roomList.startCorrectItemSearch() + function startChat() { + applyButton.loading = true + errorMessage.text = "" - }, (type, args) => { - button.loading = false + const args = [userField.item.text.trim(), encryptCheckBox.checked] - let txt = qsTr("Unknown error - %1: %2").arg(type).arg(args) - - if (type === "InvalidUserInContext") - txt = qsTr("Can't start chatting with yourself") - - if (type === "InvalidUserId") - txt = qsTr("Invalid user ID, expected format is " + - "@username:homeserver") - - if (type === "MatrixNotFound") - txt = qsTr("User not found, please verify the entered ID") - - if (type === "MatrixBadGateway") - txt = qsTr( - "Could not contact this user's server, " + - "please verify the entered ID" - ) - - errorMessage.text = txt - }) - }, - - cancel: button => { - userField.item.text = "" + py.callClientCoro(userId, "new_direct_chat", args, roomId => { + applyButton.loading = false errorMessage.text = "" - pageLoader.showPrevious() + pageLoader.showRoom(userId, roomId) + mainPane.roomList.startCorrectItemSearch() + + }, (type, args) => { + applyButton.loading = false + + let txt = qsTr("Unknown error - %1: %2").arg(type).arg(args) + + if (type === "InvalidUserInContext") + txt = qsTr("Can't start chatting with yourself") + + if (type === "InvalidUserId") + txt = qsTr("Invalid user ID, expected format is " + + "@username:homeserver") + + if (type === "MatrixNotFound") + txt = qsTr("User not found, please verify the entered ID") + + if (type === "MatrixBadGateway") + txt = qsTr( + "Could not contact this user's server, " + + "please verify the entered ID" + ) + + errorMessage.text = txt + }) + } + + function cancel() { + userField.item.reset() + errorMessage.text = "" + + pageLoader.showPrevious() + } + + + footer: ButtonLayout { + ApplyButton { + id: applyButton + text: qsTr("Start chat") + icon.name: "start-direct-chat" + enabled: Boolean(userField.item.text.trim()) + onClicked: startChat() } - }) + CancelButton { + onClicked: { + userField.item.text = "" + errorMessage.text = "" + pageLoader.showPrevious() + } + } + } - readonly property string userId: addChatPage.userId + Keys.onEscapePressed: cancel() CurrentUserAvatar { - Layout.alignment: Qt.AlignCenter - Layout.preferredWidth: 128 - Layout.preferredHeight: Layout.preferredWidth + userId: page.userId + account: page.account } HLabeledItem { diff --git a/src/gui/Pages/AddChat/JoinRoom.qml b/src/gui/Pages/AddChat/JoinRoom.qml index 083644ee..f1317b85 100644 --- a/src/gui/Pages/AddChat/JoinRoom.qml +++ b/src/gui/Pages/AddChat/JoinRoom.qml @@ -2,70 +2,79 @@ import QtQuick 2.12 import QtQuick.Layouts 1.12 +import "../.." import "../../Base" +import "../../Base/ButtonLayout" -HBox { - id: addChatBox - clickButtonOnEnter: "apply" +HFlickableColumnPage { + id: page - onFocusChanged: roomField.item.forceActiveFocus() - buttonModel: [ - { - name: "apply", - text: qsTr("Join"), - iconName: "room-join", - enabled: Boolean(roomField.item.text.trim()), - }, - { name: "cancel", text: qsTr("Cancel"), iconName: "cancel" }, - ] + property string userId + readonly property QtObject account: ModelStore.get("accounts").find(userId) - buttonCallbacks: ({ - apply: button => { - button.loading = true - errorMessage.text = "" - const args = [roomField.item.text.trim()] + function takeFocus() { + roomField.item.forceActiveFocus() + } - py.callClientCoro(userId, "room_join", args, roomId => { - button.loading = false - errorMessage.text = "" - pageLoader.showRoom(userId, roomId) - mainPane.roomList.startCorrectItemSearch() + function join() { + joinButton.loading = true + errorMessage.text = "" - }, (type, args) => { - button.loading = false + const args = [roomField.item.text.trim()] - let txt = qsTr("Unknown error - %1: %2").arg(type).arg(args) + py.callClientCoro(userId, "room_join", args, roomId => { + joinButton.loading = false + errorMessage.text = "" + pageLoader.showRoom(userId, roomId) + mainPane.roomList.startCorrectItemSearch() - if (type === "ValueError") - txt = qsTr("Unrecognized alias, room ID or URL") + }, (type, args) => { + joinButton.loading = false - if (type === "MatrixNotFound") - txt = qsTr("Room not found") + let txt = qsTr("Unknown error - %1: %2").arg(type).arg(args) - if (type === "MatrixForbidden") - txt = qsTr("You do not have permission to join this room") + if (type === "ValueError") + txt = qsTr("Unrecognized alias, room ID or URL") - errorMessage.text = txt - }) - }, + if (type === "MatrixNotFound") + txt = qsTr("Room not found") - cancel: button => { - roomField.item.text = "" - errorMessage.text = "" - pageLoader.showPrevious() + if (type === "MatrixForbidden") + txt = qsTr("You do not have permission to join this room") + + errorMessage.text = txt + }) + } + + function cancel() { + roomField.item.reset() + errorMessage.reset() + + pageLoader.showPrevious() + } + + + footer: ButtonLayout { + ApplyButton { + text: qsTr("Join") + icon.name: "room-join" + enabled: Boolean(roomField.item.text.trim()) + onClicked: join() } - }) + CancelButton { + onClicked: cancel() + } + } - readonly property string userId: addChatPage.userId + Keys.onEscapePressed: cancel() CurrentUserAvatar { - Layout.alignment: Qt.AlignCenter - Layout.preferredWidth: 128 - Layout.preferredHeight: Layout.preferredWidth + userId: page.userId + account: page.account } HLabeledItem { diff --git a/src/gui/Pages/Chat/ChatPage.qml b/src/gui/Pages/Chat/ChatPage.qml index 34d000d9..e2fea337 100644 --- a/src/gui/Pages/Chat/ChatPage.qml +++ b/src/gui/Pages/Chat/ChatPage.qml @@ -11,6 +11,7 @@ import "Timeline" HColumnPage { id: chatPage padding: 0 + column.spacing: 0 onLoadEventListChanged: if (loadEventList) loadedOnce = true Component.onDestruction: if (loadMembersFuture) loadMembersFuture.cancel() diff --git a/src/gui/Pages/Chat/RoomPane/MemberDelegate.qml b/src/gui/Pages/Chat/RoomPane/MemberDelegate.qml index 351c572d..f9a78bd1 100644 --- a/src/gui/Pages/Chat/RoomPane/MemberDelegate.qml +++ b/src/gui/Pages/Chat/RoomPane/MemberDelegate.qml @@ -67,10 +67,7 @@ HTile { roomId: chat.roomId, targetUserId: model.id, targetDisplayName: model.display_name, - operation: - model.invited ? - RemoveMemberPopup.Operation.Disinvite : - RemoveMemberPopup.Operation.Kick, + operation: model.invited ? "disinvite" : "kick", }) Component.onCompleted: py.callClientCoro( @@ -94,7 +91,7 @@ HTile { roomId: chat.roomId, targetUserId: model.id, targetDisplayName: model.display_name, - operation: RemoveMemberPopup.Operation.Ban, + operation: "ban", }) Component.onCompleted: py.callClientCoro( diff --git a/src/gui/Pages/Chat/RoomPane/SettingsView.qml b/src/gui/Pages/Chat/RoomPane/SettingsView.qml index 7ae80127..492eb602 100644 --- a/src/gui/Pages/Chat/RoomPane/SettingsView.qml +++ b/src/gui/Pages/Chat/RoomPane/SettingsView.qml @@ -57,9 +57,6 @@ HFlickableColumnPage { } - useVariableSpacing: false - column.spacing: theme.spacing * 1.5 - flickShortcuts.active: ! mainUI.debugConsole.visible && ! chat.composerHasFocus @@ -82,6 +79,8 @@ HFlickableColumnPage { } } + Keys.onEscapePressed: cancel() + HRoomAvatar { id: avatar diff --git a/src/gui/Popups/BoxPopup.qml b/src/gui/Popups/BoxPopup.qml deleted file mode 100644 index f7833aa9..00000000 --- a/src/gui/Popups/BoxPopup.qml +++ /dev/null @@ -1,79 +0,0 @@ -// SPDX-License-Identifier: LGPL-3.0-or-later - -import QtQuick 2.12 -import QtQuick.Layouts 1.12 -import "../Base" - -HPopup { - id: popup - onAboutToShow: okClicked = false - - - signal ok() - signal cancel() - - - default property alias boxData: box.body - property alias box: box - property bool fillAvailableHeight: false - - property alias summary: summary - property alias details: details - - property string okText: qsTr("OK") - property string okIcon: "ok" - property bool okEnabled: true - property bool okClicked: false - property string cancelText: qsTr("Cancel") - - - Binding on height { - value: popup.maximumPreferredHeight - when: popup.fillAvailableHeight - } - - HBox { - id: box - implicitWidth: Math.min( - window.width - popup.leftMargin - popup.rightMargin, - theme.controls.popup.defaultWidth, - ) - fillAvailableHeight: popup.fillAvailableHeight - clickButtonOnEnter: "ok" - - buttonModel: [ - { name: "ok", text: okText, iconName: okIcon, enabled: okEnabled}, - { name: "cancel", text: cancelText, iconName: "cancel" }, - ] - - buttonCallbacks: ({ - ok: button => { okClicked = true; popup.ok(); popup.close() }, - cancel: button => { - okClicked = false; popup.cancel(); popup.close() - }, - }) - - - Binding on height { - value: popup.maximumPreferredHeight - when: popup.fillAvailableHeight - } - - HLabel { - id: summary - wrapMode: Text.Wrap - font.bold: true - visible: Boolean(text) - - Layout.fillWidth: true - } - - HLabel { - id: details - wrapMode: Text.Wrap - visible: Boolean(text) - - Layout.fillWidth: true - } - } -} diff --git a/src/gui/Popups/ClearMessagesPopup.qml b/src/gui/Popups/ClearMessagesPopup.qml index 888c2c1d..6652e291 100644 --- a/src/gui/Popups/ClearMessagesPopup.qml +++ b/src/gui/Popups/ClearMessagesPopup.qml @@ -1,19 +1,42 @@ // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 +import "../Base/ButtonLayout" -BoxPopup { - summary.text: qsTr("Clear this room's messages?") - details.text: qsTr( - "The messages will only be removed on your side. " + - "They will be available again after you restart the application." - ) - okText: qsTr("Clear") - box.focusButton: "ok" - - onOk: py.callClientCoro(userId, "clear_events", [roomId]) +HFlickableColumnPopup { + id: popup property string userId: "" property string roomId: "" + + + page.footer: ButtonLayout { + ApplyButton { + id: clearButton + text: qsTr("Clear") + icon.name: "clear-messages" + onClicked: { + py.callClientCoro(userId, "clear_events", [roomId]) + popup.close() + } + } + + CancelButton { + onClicked: popup.close() + } + } + + SummaryLabel { + text: qsTr("Clear this room's messages?") + } + + DetailsLabel { + text: qsTr( + "The messages will only be removed on your side. " + + "They will be available again after you restart the application." + ) + } + + onOpened: clearButton.forceActiveFocus() } diff --git a/src/gui/Popups/DetailsLabel.qml b/src/gui/Popups/DetailsLabel.qml new file mode 100644 index 00000000..1bc54487 --- /dev/null +++ b/src/gui/Popups/DetailsLabel.qml @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later + +import QtQuick 2.12 +import QtQuick.Layouts 1.12 +import "../Base" + +HLabel { + wrapMode: Text.Wrap + visible: Boolean(text) + + Layout.fillWidth: true +} diff --git a/src/gui/Popups/ForgetRoomPopup.qml b/src/gui/Popups/ForgetRoomPopup.qml index 8b3feb9a..04f12486 100644 --- a/src/gui/Popups/ForgetRoomPopup.qml +++ b/src/gui/Popups/ForgetRoomPopup.qml @@ -1,35 +1,10 @@ // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 +import "../Base/ButtonLayout" -BoxPopup { +HFlickableColumnPopup { id: popup - summary.text: qsTr("Leave %1 and lose the history?").arg(roomName) - summary.textFormat: Text.StyledText - details.text: qsTr( - "You will not be able to see the messages you received in " + - "this room anymore.\n\n" + - - "If all members forget the room, it will be removed from the servers." - ) - - okText: qsTr("Forget") - box.focusButton: "ok" - - onOk: py.callClientCoro(userId, "room_forget", [roomId], () => { - if (window.uiState.page === "Pages/Chat/Chat.qml" && - window.uiState.pageProperties.userId === userId && - window.uiState.pageProperties.roomId === roomId) - { - window.mainUI.pageLoader.showPrevious() || - window.mainUI.pageLoader.showPage("Default") - - Qt.callLater(popup.destroy) - } - }) - - onCancel: canDestroy = true - onClosed: if (canDestroy) Qt.callLater(popup.destroy) property string userId: "" @@ -37,4 +12,55 @@ BoxPopup { property string roomName: "" property bool canDestroy: false + + + function forget() { + py.callClientCoro(userId, "room_forget", [roomId], () => { + if (window.uiState.page === "Pages/Chat/Chat.qml" && + window.uiState.pageProperties.userId === userId && + window.uiState.pageProperties.roomId === roomId) + { + window.mainUI.pageLoader.showPrevious() || + window.mainUI.pageLoader.showPage("Default") + + Qt.callLater(popup.destroy) + } + }) + } + + + page.footer: ButtonLayout { + ApplyButton { + id: forgetButton + text: qsTr("Forget") + icon.name: "room-forget" + onClicked: forget() + } + + CancelButton { + onClicked: { + canDestroy = true + popup.close() + } + } + } + + onOpened: forgetButton.forceActiveFocus() + onClosed: if (canDestroy) Qt.callLater(popup.destroy) + + + SummaryLabel { + text: qsTr("Leave %1 and lose the history?").arg(roomName) + textFormat: Text.StyledText + } + + DetailsLabel { + text: qsTr( + "You will not be able to see the messages you received in " + + "this room anymore.\n\n" + + + "If all members forget the room, it will be removed from the " + + "servers." + ) + } } diff --git a/src/gui/Popups/HColumnPopup.qml b/src/gui/Popups/HColumnPopup.qml new file mode 100644 index 00000000..08531bd8 --- /dev/null +++ b/src/gui/Popups/HColumnPopup.qml @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later + +import QtQuick 2.12 +import "../Base" + +HPopup { + id: popup + + + default property alias pageData: page.columnData + readonly property alias page: page + + + HColumnPage { + id: page + implicitWidth: Math.min( + popup.maximumPreferredWidth, + theme.controls.popup.defaultWidth, + ) + implicitHeight: Math.min( + popup.maximumPreferredHeight, + implicitHeaderHeight + implicitFooterHeight + + topPadding + bottomPadding + implicitContentHeight, + ) + useVariableSpacing: false + } +} diff --git a/src/gui/Popups/HFlickableColumnPopup.qml b/src/gui/Popups/HFlickableColumnPopup.qml new file mode 100644 index 00000000..a2aefec0 --- /dev/null +++ b/src/gui/Popups/HFlickableColumnPopup.qml @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later + +import QtQuick 2.12 +import "../Base" + +HPopup { + id: popup + + + default property alias pageData: page.columnData + readonly property alias page: page + + + HFlickableColumnPage { + id: page + implicitWidth: Math.min( + popup.maximumPreferredWidth, + theme.controls.popup.defaultWidth, + ) + implicitHeight: Math.min( + popup.maximumPreferredHeight, + implicitHeaderHeight + implicitFooterHeight + contentHeight, + ) + } +} diff --git a/src/gui/Popups/InviteToRoomPopup.qml b/src/gui/Popups/InviteToRoomPopup.qml index 58588d7d..799da2ee 100644 --- a/src/gui/Popups/InviteToRoomPopup.qml +++ b/src/gui/Popups/InviteToRoomPopup.qml @@ -4,51 +4,10 @@ import QtQuick 2.12 import QtQuick.Controls 2.12 import QtQuick.Layouts 1.12 import "../Base" +import "../Base/ButtonLayout" -BoxPopup { +HColumnPopup { id: popup - // fillAvailableHeight: true - summary.text: qsTr("Invite members to %1").arg(roomName) - summary.textFormat: Text.StyledText - okText: qsTr("Invite") - okEnabled: invitingAllowed && Boolean(inviteArea.text.trim()) - - onOpened: inviteArea.forceActiveFocus() - - onInvitingAllowedChanged: - if (! invitingAllowed && inviteFuture) inviteFuture.cancel() - - box.buttonCallbacks: ({ - ok: button => { - button.loading = true - - const inviteesLeft = inviteArea.text.trim().split(/\s+/).filter( - user => ! successfulInvites.includes(user) - ) - - inviteFuture = py.callClientCoro( - userId, - "room_mass_invite", - [roomId, ...inviteesLeft], - - ([successes, errors]) => { - if (errors.length < 1) { - popup.close() - return - } - - successfulInvites = successes - failedInvites = errors - button.loading = false - } - ) - }, - - cancel: button => { - if (inviteFuture) inviteFuture.cancel() - popup.close() - }, - }) property string userId @@ -61,13 +20,68 @@ BoxPopup { property var failedInvites: [] + function invite() { + inviteButton.loading = true + + const inviteesLeft = inviteArea.text.trim().split(/\s+/).filter( + user => ! successfulInvites.includes(user) + ) + + inviteFuture = py.callClientCoro( + userId, + "room_mass_invite", + [roomId, ...inviteesLeft], + + ([successes, errors]) => { + if (errors.length < 1) { + popup.close() + return + } + + successfulInvites = successes + failedInvites = errors + inviteButton.loading = false + } + ) + } + + + page.footer: ButtonLayout { + ApplyButton { + id: inviteButton + text: qsTr("Invite") + icon.name: "room-send-invite" + enabled: invitingAllowed && Boolean(inviteArea.text.trim()) + onClicked: invite() + } + + CancelButton { + id: cancelButton + onClicked: popup.close() + } + } + + onOpened: inviteArea.forceActiveFocus() + onClosed: if (inviteFuture) inviteFuture.cancel() + + onInvitingAllowedChanged: + if (! invitingAllowed && inviteFuture) inviteFuture.cancel() + + + SummaryLabel { + text: qsTr("Invite members to %1").arg(roomName) + textFormat: Text.StyledText + } + HScrollView { + clip: true + Layout.fillWidth: true Layout.fillHeight: true HTextArea { id: inviteArea - focusItemOnTab: box.firstButton + focusItemOnTab: inviteButton.enabled ? inviteButton : cancelButton placeholderText: qsTr("User IDs (e.g. @bob:matrix.org @alice:localhost)") } diff --git a/src/gui/Popups/LeaveRoomPopup.qml b/src/gui/Popups/LeaveRoomPopup.qml index 2be62842..ebcfbd60 100644 --- a/src/gui/Popups/LeaveRoomPopup.qml +++ b/src/gui/Popups/LeaveRoomPopup.qml @@ -1,21 +1,46 @@ // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 +import "../Base/ButtonLayout" -BoxPopup { - summary.text: qsTr("Leave %1?").arg(roomName) - summary.textFormat: Text.StyledText - details.text: qsTr( - "If this room is private, you will not be able to rejoin it." - ) - okText: qsTr("Leave") - box.focusButton: "ok" - - onOk: py.callClientCoro(userId, "room_leave", [roomId], leftCallback) +HFlickableColumnPopup { + id: popup property string userId: "" property string roomId: "" property string roomName: "" property var leftCallback: null + + + page.footer: ButtonLayout { + ApplyButton { + id: leaveButton + icon.name: "room-leave" + text: qsTr("Leave") + + onClicked: { + py.callClientCoro(userId, "room_leave", [roomId], leftCallback) + popup.close() + } + } + + CancelButton { + onClicked: popup.close() + } + } + + onOpened: leaveButton.forceActiveFocus() + + + SummaryLabel { + text: qsTr("Leave %1?").arg(roomName) + textFormat: Text.StyledText + } + + DetailsLabel { + text: qsTr( + "If this room is private, you will not be able to rejoin it." + ) + } } diff --git a/src/gui/Popups/PasswordPopup.qml b/src/gui/Popups/PasswordPopup.qml index b13b1355..53bbcb56 100644 --- a/src/gui/Popups/PasswordPopup.qml +++ b/src/gui/Popups/PasswordPopup.qml @@ -3,29 +3,22 @@ import QtQuick 2.12 import QtQuick.Layouts 1.12 import "../Base" +import "../Base/ButtonLayout" -BoxPopup { +HFlickableColumnPopup { id: popup - okEnabled: Boolean(passwordField.text) - - onAboutToShow: { - okClicked = false - acceptedPassword = "" - passwordValid = null - errorMessage.text = "" - } - onOpened: passwordField.forceActiveFocus() - - - signal cancelled() property bool validateWhileTyping: false property string acceptedPassword: "" property var passwordValid: null + property bool okClicked: false - property alias field: passwordField + readonly property alias summary: summary + readonly property alias validateButton: validateButton + + signal cancelled() function verifyPassword(pass, callback) { @@ -35,40 +28,59 @@ BoxPopup { callback(true) } + function validate() { + const password = passwordField.text + okClicked = true + validateButton.loading = true + errorMessage.text = "" - box.buttonCallbacks: ({ - ok: button => { - const password = passwordField.text - okClicked = true - button.loading = true - errorMessage.text = "" + verifyPassword(password, result => { + if (result === true) { + passwordValid = true + popup.acceptedPassword = password + popup.close() + } else if (result === false) { + passwordValid = false + } else { + errorMessage.text = result + } - verifyPassword(password, result => { - if (result === true) { - passwordValid = true - popup.acceptedPassword = password - popup.close() - } else if (result === false) { - passwordValid = false - } else { - errorMessage.text = result - } + validateButton.loading = false + }) + } - button.loading = false - }) - }, - cancel: button => { - popup.close() - cancelled() - }, - }) + page.footer: ButtonLayout { + ApplyButton { + id: validateButton + text: qsTr("Validate") + enabled: Boolean(passwordField.text) + onClicked: validate() + } + + CancelButton { + onClicked: { + popup.close() + cancelled() + } + } + } + + onAboutToShow: { + okClicked = false + acceptedPassword = "" + passwordValid = null + errorMessage.text = "" + } + + onOpened: passwordField.forceActiveFocus() + + + SummaryLabel { id: summary } HRowLayout { spacing: theme.spacing - Layout.fillWidth: true - HTextField { id: passwordField echoMode: TextInput.Password @@ -78,6 +90,8 @@ BoxPopup { onTextChanged: passwordValid = validateWhileTyping ? verifyPassword(text) : null + onAccepted: popup.validate() + Layout.fillWidth: true } @@ -91,7 +105,8 @@ BoxPopup { Layout.preferredWidth: passwordValid === null || (validateWhileTyping && ! okClicked && ! passwordValid) ? - 0 :implicitWidth + 0 : + implicitWidth Behavior on Layout.preferredWidth { HNumberAnimation {} } } diff --git a/src/gui/Popups/RedactPopup.qml b/src/gui/Popups/RedactPopup.qml index 96090438..b8984030 100644 --- a/src/gui/Popups/RedactPopup.qml +++ b/src/gui/Popups/RedactPopup.qml @@ -3,28 +3,21 @@ import QtQuick 2.12 import QtQuick.Layouts 1.12 import "../Base" +import "../Base/ButtonLayout" -BoxPopup { - summary.text: - isLast ? - qsTr("Remove your last message?") : +HFlickableColumnPopup { + id: popup - eventSenderAndIds.length > 1 ? - qsTr("Remove %1 messages?").arg(eventSenderAndIds.length) : - qsTr("Remove this message?") + property string preferUserId: "" + property string roomId: "" - details.color: theme.colors.warningText - details.text: - onlyOwnMessageWarning ? - qsTr("Only your messages can be removed") : - "" + property var eventSenderAndIds: [] // [[senderId, event.id], ...] + property bool onlyOwnMessageWarning: false + property bool isLast: false - okText: qsTr("Remove") - // box.focusButton: "ok" - onOpened: reasonField.item.forceActiveFocus() - onOk: { + function remove() { const idsForSender = {} // {senderId: [event.id, ...]} for (const [senderId, eventClientId] of eventSenderAndIds) { @@ -40,16 +33,44 @@ BoxPopup { "room_mass_redact", [roomId, reasonField.item.text, ...eventClientIds] ) + + popup.close() } - property string preferUserId: "" - property string roomId: "" + page.footer: ButtonLayout { + ApplyButton { + text: qsTr("Remove") + icon.name: "remove-message" + onClicked: remove() + } - property var eventSenderAndIds: [] // [[senderId, event.id], ...] - property bool onlyOwnMessageWarning: false - property bool isLast: false + CancelButton { + onClicked: popup.close() + } + } + onOpened: reasonField.item.forceActiveFocus() + + + SummaryLabel { + text: + isLast ? + qsTr("Remove your last message?") : + + eventSenderAndIds.length > 1 ? + qsTr("Remove %1 messages?").arg(eventSenderAndIds.length) : + + qsTr("Remove this message?") + } + + DetailsLabel { + color: theme.colors.warningText + text: + onlyOwnMessageWarning ? + qsTr("Only your messages can be removed") : + "" + } HLabeledItem { id: reasonField @@ -59,6 +80,7 @@ BoxPopup { HTextField { width: parent.width + onAccepted: popup.remove() } } } diff --git a/src/gui/Popups/RemoveMemberPopup.qml b/src/gui/Popups/RemoveMemberPopup.qml index c13ed83d..2d5d5c01 100644 --- a/src/gui/Popups/RemoveMemberPopup.qml +++ b/src/gui/Popups/RemoveMemberPopup.qml @@ -3,48 +3,65 @@ import QtQuick 2.12 import QtQuick.Layouts 1.12 import "../Base" +import "../Base/ButtonLayout" -BoxPopup { - summary.textFormat: Text.StyledText - summary.text: - operation === RemoveMemberPopup.Operation.Disinvite ? - qsTr("Disinvite %1 from the room?").arg(coloredTarget) : +HFlickableColumnPopup { + id: popup - operation === RemoveMemberPopup.Operation.Kick ? - qsTr("Kick %1 out of the room?").arg(coloredTarget) : - - qsTr("Ban %1 from the room?").arg(coloredTarget) - - okText: - operation === RemoveMemberPopup.Operation.Disinvite ? - qsTr("Disinvite") : - - operation === RemoveMemberPopup.Operation.Kick ? - qsTr("Kick") : - - qsTr("Ban") - - onOpened: reasonField.item.forceActiveFocus() - onOk: py.callClientCoro( - userId, - operation === RemoveMemberPopup.Operation.Ban ? - "room_ban" : "room_kick", - [roomId, targetUserId, reasonField.item.text || null], - ) - - - enum Operation { Disinvite, Kick, Ban } property string userId property string roomId property string targetUserId property string targetDisplayName - property int operation + property string operation // "disinvite", "kick" or "ban" readonly property string coloredTarget: utils.coloredNameHtml(targetDisplayName, targetUserId) + function remove() { + py.callClientCoro( + userId, + operation === "ban" ? "room_ban" : "room_kick", + [roomId, targetUserId, reasonField.item.text || null], + ) + + popup.close() + } + + + page.footer: ButtonLayout { + ApplyButton { + text: + operation === "disinvite" ? qsTr("Disinvite") : + operation === "kick" ? qsTr("Kick") : + qsTr("Ban") + + icon.name: operation === "ban" ? "room-ban" : "room-kick" + + onClicked: remove() + } + + CancelButton { + onClicked: popup.close() + } + } + + onOpened: reasonField.item.forceActiveFocus() + + + SummaryLabel { + textFormat: Text.StyledText + text: + operation === "disinvite" ? + qsTr("Disinvite %1 from the room?").arg(coloredTarget) : + + operation === "kick" ? + qsTr("Kick %1 out of the room?").arg(coloredTarget) : + + qsTr("Ban %1 from the room?").arg(coloredTarget) + } + HLabeledItem { id: reasonField label.text: qsTr("Optional reason:") @@ -53,6 +70,7 @@ BoxPopup { HTextField { width: parent.width + onAccepted: popup.remove() } } } diff --git a/src/gui/Popups/SignOutPopup.qml b/src/gui/Popups/SignOutPopup.qml index 8b512a4a..e0df1347 100644 --- a/src/gui/Popups/SignOutPopup.qml +++ b/src/gui/Popups/SignOutPopup.qml @@ -2,61 +2,74 @@ import QtQuick 2.12 import ".." +import "../Base/ButtonLayout" -BoxPopup { +HFlickableColumnPopup { id: popup - summary.text: qsTr("Backup your decryption keys before signing out?") - details.text: qsTr( - "Signing out will delete your device's information and the keys " + - "required to decrypt messages in encrypted rooms.\n\n" + - "You can export your keys to a passphrase-protected file " + - "before signing out.\n\n" + - "This will allow you to restore access to your messages when " + - "you sign in again, by importing this file in your account settings." - ) + property string userId: "" - box.focusButton: "ok" - box.buttonModel: [ - { name: "ok", text: qsTr("Export keys"), iconName: "export-keys" }, - { name: "signout", text: qsTr("Sign out now"), iconName: "sign-out", - iconColor: theme.colors.middleBackground }, - { name: "cancel", text: qsTr("Cancel"), iconName: "cancel" }, - ] - box.buttonCallbacks: ({ - ok: button => { - utils.makeObject( + page.footer: ButtonLayout { + ApplyButton { + id: exportButton + text: qsTr("Export keys") + icon.name: "export-keys" + + onClicked: utils.makeObject( "Dialogs/ExportKeys.qml", window.mainUI, { userId }, obj => { - button.loading = Qt.binding(() => obj.exporting) - obj.done.connect(() => { - box.buttonCallbacks["signout"](button) - }) + loading = Qt.binding(() => obj.exporting) + obj.done.connect(signOutButton.clicked) obj.dialog.open() } ) - }, + } - signout: button => { - okClicked = true - popup.ok() + OtherButton { + id: signOutButton + text: qsTr("Sign out now") + icon.name: "sign-out" + icon.color: theme.colors.middleBackground - if (ModelStore.get("accounts").count < 2 || - window.uiState.pageProperties.userId === userId) { - window.mainUI.pageLoader.showPage("AddAccount/AddAccount") + onClicked: { + if (ModelStore.get("accounts").count < 2 || + window.uiState.pageProperties.userId === userId) + { + window.mainUI.pageLoader.showPage("AddAccount/AddAccount") + } + + py.callCoro("logout_client", [userId]) + popup.close() } + } - py.callCoro("logout_client", [userId]) - popup.close() - }, + CancelButton { + onClicked: popup.close() + } + } - cancel: button => { okClicked = false; popup.cancel(); popup.close() }, - }) + onOpened: exportButton.forceActiveFocus() - property string userId: "" + SummaryLabel { + text: qsTr("Backup your decryption keys before signing out?") + } + + DetailsLabel { + text: qsTr( + "Signing out will delete your device's information and the keys " + + "required to decrypt messages in encrypted rooms.\n\n" + + + "You can export your keys to a passphrase-protected file " + + "before signing out.\n\n" + + + "This will allow you to restore access to your messages when " + + "you sign in again, by importing this file in your account " + + "settings." + ) + } } diff --git a/src/gui/Popups/SummaryLabel.qml b/src/gui/Popups/SummaryLabel.qml new file mode 100644 index 00000000..82d2c210 --- /dev/null +++ b/src/gui/Popups/SummaryLabel.qml @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later + +import QtQuick 2.12 +import QtQuick.Layouts 1.12 +import "../Base" + +HLabel { + wrapMode: Text.Wrap + font.bold: true + visible: Boolean(text) + + Layout.fillWidth: true +} diff --git a/src/gui/Popups/UnexpectedErrorPopup.qml b/src/gui/Popups/UnexpectedErrorPopup.qml index fd72e354..e97b128b 100644 --- a/src/gui/Popups/UnexpectedErrorPopup.qml +++ b/src/gui/Popups/UnexpectedErrorPopup.qml @@ -4,16 +4,10 @@ import QtQuick 2.12 import QtQuick.Controls 2.12 import QtQuick.Layouts 1.12 import "../Base" +import "../Base/ButtonLayout" -BoxPopup { - summary.text: qsTr("Unexpected error occured: %1").arg(errorType) - summary.textFormat: Text.StyledText - - okText: qsTr("Report") - okIcon: "report-error" - okEnabled: false // TODO - cancelText: qsTr("Ignore") - box.focusButton: "cancel" +HColumnPopup { + id: popup property string errorType @@ -21,17 +15,44 @@ BoxPopup { property string traceback: "" + page.footer: ButtonLayout { + ApplyButton { + text: qsTr("Report") + icon.name: "report-error" + enabled: false // TODO + } + + CancelButton { + id: cancelButton + text: qsTr("Ignore") + onClicked: popup.close() + } + } + + onOpened: cancelButton.forceActiveFocus() + + + SummaryLabel { + text: qsTr("Unexpected error occured: %1").arg(errorType) + textFormat: Text.StyledText + } + HScrollView { + clip: true + Layout.fillWidth: true + Layout.fillHeight: true HTextArea { text: [message, traceback].join("\n\n") || qsTr("No info available") readOnly: true font.family: theme.fontFamily.mono + focusOnTab: hideCheckBox } } HCheckBox { + id: hideCheckBox text: qsTr("Hide this type of error until restart") onCheckedChanged: checked ? diff --git a/src/gui/ShortcutBundles/TabShortcuts.qml b/src/gui/ShortcutBundles/TabShortcuts.qml index ba4abaf7..52759de8 100644 --- a/src/gui/ShortcutBundles/TabShortcuts.qml +++ b/src/gui/ShortcutBundles/TabShortcuts.qml @@ -8,7 +8,7 @@ HQtObject { property Item container: parent - property bool active: true + property bool active: container.count > 1 HShortcut { diff --git a/src/icons/thin/check-mark-partial.svg b/src/icons/thin/check-mark-partial.svg new file mode 100644 index 00000000..c916d92b --- /dev/null +++ b/src/icons/thin/check-mark-partial.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/icons/thin/device-action-menu.svg b/src/icons/thin/device-action-menu.svg index 1de1c096..5bf163ca 100644 --- a/src/icons/thin/device-action-menu.svg +++ b/src/icons/thin/device-action-menu.svg @@ -1,3 +1,7 @@ - + + + + + diff --git a/src/icons/thin/device-blacklisted.svg b/src/icons/thin/device-blacklisted.svg new file mode 100644 index 00000000..85cba558 --- /dev/null +++ b/src/icons/thin/device-blacklisted.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/icons/thin/device-current.svg b/src/icons/thin/device-current.svg new file mode 100644 index 00000000..807d6aa2 --- /dev/null +++ b/src/icons/thin/device-current.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/icons/thin/device-delete-checked.svg b/src/icons/thin/device-delete-checked.svg new file mode 100644 index 00000000..5609acb3 --- /dev/null +++ b/src/icons/thin/device-delete-checked.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/icons/thin/device-delete.svg b/src/icons/thin/device-delete.svg new file mode 100644 index 00000000..5609acb3 --- /dev/null +++ b/src/icons/thin/device-delete.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/icons/thin/device-ignored.svg b/src/icons/thin/device-ignored.svg new file mode 100644 index 00000000..807d6aa2 --- /dev/null +++ b/src/icons/thin/device-ignored.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/icons/thin/device-rename.svg b/src/icons/thin/device-rename.svg new file mode 100644 index 00000000..5136cd7d --- /dev/null +++ b/src/icons/thin/device-rename.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/icons/thin/device-unset.svg b/src/icons/thin/device-unset.svg new file mode 100644 index 00000000..3df91554 --- /dev/null +++ b/src/icons/thin/device-unset.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/icons/thin/device-verified.svg b/src/icons/thin/device-verified.svg new file mode 100644 index 00000000..7cd47f35 --- /dev/null +++ b/src/icons/thin/device-verified.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/icons/thin/device-verify.svg b/src/icons/thin/device-verify.svg new file mode 100644 index 00000000..96b915db --- /dev/null +++ b/src/icons/thin/device-verify.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/themes/Glass.qpl b/src/themes/Glass.qpl index f2726d1a..e30e4c4a 100644 --- a/src/themes/Glass.qpl +++ b/src/themes/Glass.qpl @@ -66,9 +66,10 @@ colors: color halfDimText: hsluv(0, 0, intensity * 72) color dimText: hsluv(0, 0, intensity * 60) - color warningText: hsluv(60, coloredTextSaturation, coloredTextIntensity) - color errorText: hsluv(0, coloredTextSaturation, coloredTextIntensity) - color accentText: hsluv(hue, coloredTextSaturation, coloredTextIntensity) + color positiveText: hsluv(155, coloredTextSaturation, coloredTextIntensity) + color warningText: hsluv(60, coloredTextSaturation, coloredTextIntensity) + color errorText: hsluv(0, coloredTextSaturation, coloredTextIntensity) + color accentText: hsluv(hue, coloredTextSaturation, coloredTextIntensity) color link: hsluv(hue, coloredTextSaturation, coloredTextIntensity) color code: hsluv(hue + 10, coloredTextSaturation, coloredTextIntensity) diff --git a/src/themes/Midnight.qpl b/src/themes/Midnight.qpl index 8e0ee84c..e41718a2 100644 --- a/src/themes/Midnight.qpl +++ b/src/themes/Midnight.qpl @@ -69,9 +69,10 @@ colors: color halfDimText: hsluv(0, 0, intensity * 72) color dimText: hsluv(0, 0, intensity * 60) - color warningText: hsluv(60, coloredTextSaturation, coloredTextIntensity) - color errorText: hsluv(0, coloredTextSaturation, coloredTextIntensity) - color accentText: hsluv(hue, coloredTextSaturation, coloredTextIntensity) + color positiveText: hsluv(155, coloredTextSaturation, coloredTextIntensity) + color warningText: hsluv(60, coloredTextSaturation, coloredTextIntensity) + color errorText: hsluv(0, coloredTextSaturation, coloredTextIntensity) + color accentText: hsluv(hue, coloredTextSaturation, coloredTextIntensity) color link: hsluv(hue, coloredTextSaturation, coloredTextIntensity) color code: hsluv(hue + 10, coloredTextSaturation, coloredTextIntensity)