From fc83fb32e93b446a6ddf4297e6125af63bdf8536 Mon Sep 17 00:00:00 2001 From: "Bharadwaj Raju (away; can't respond)" Date: Wed, 9 Feb 2022 03:47:36 +0000 Subject: [PATCH] smsapp: Assorted UI improvements - Move the device combobox to the global drawer - Move search field and new button to header - Use Kirigami SearchField instead - Don't switch focus away from search field when typing - Give the New button an icon - Clarify the search field text - Center messages view loading indicator - Make send and attach buttons stick to bottom of text area - Make cursor an I-beam when hovering over text area - Move send button to the right - Give proper padding to messages view top - Move refresh action to global drawer - Show refresh button directly in loading message where it is most useful | Before | After | | ------ | ------ | | ![kdeconnectsms-old1](/uploads/469fa5f198ce81f1f53e8aa73694a824/kdeconnectsms-old1.png) | ![kdeconnectsms-new1](/uploads/c3b2b552d5d1bb73c566c6879c5b2a3c/kdeconnectsms-new1.png) | | ![kdeconnectsms-old2](/uploads/eed795529946ed9ff856d8599bc66fb2/kdeconnectsms-old2.png) | ![kdeconnectsms-new2](/uploads/7abff93670aaea36052f3e3bfe01da62/kdeconnectsms-new2.png) | | ![kdeconnectsms-old3](/uploads/f24dc7a902e33a1317cc8d9b90c39482/kdeconnectsms-old3.png) | ![kdeconnectsms-new3](/uploads/ea7d07f64d1904757dce56e86f1876ba/kdeconnectsms-new3.png) | cc @teams/usability @teams/vdg --- smsapp/qml/ConversationDisplay.qml | 8 +- smsapp/qml/ConversationList.qml | 192 +++++++++++++---------------- smsapp/qml/SendingArea.qml | 87 +++++++------ smsapp/qml/main.qml | 70 +++++++++++ 4 files changed, 213 insertions(+), 144 deletions(-) diff --git a/smsapp/qml/ConversationDisplay.qml b/smsapp/qml/ConversationDisplay.qml index e51148b72..4efe11d1a 100644 --- a/smsapp/qml/ConversationDisplay.qml +++ b/smsapp/qml/ConversationDisplay.qml @@ -6,7 +6,7 @@ * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL */ -import QtQuick 2.1 +import QtQuick 2.15 import QtQuick.Controls 2.2 as Controls import QtQuick.Layouts 1.1 import org.kde.people 1.0 @@ -79,8 +79,14 @@ Kirigami.ScrollablePage Controls.BusyIndicator { running: !isInitalized + anchors.centerIn: parent } + header: Item { + height: Kirigami.Units.largeSpacing * 2 + } + headerPositioning: ListView.InlineHeader + onContentHeightChanged: { if (viewport.contentHeight <= 0) { return diff --git a/smsapp/qml/ConversationList.qml b/smsapp/qml/ConversationList.qml index 44f845c69..ccaa2b4b9 100644 --- a/smsapp/qml/ConversationList.qml +++ b/smsapp/qml/ConversationList.qml @@ -35,29 +35,19 @@ Kirigami.ScrollablePage } } - contextualActions: [ - Kirigami.Action { - text: i18nd("kdeconnect-sms", "Refresh") - icon.name: "view-refresh" - enabled: devicesCombo.count > 0 - onTriggered: { - conversationListModel.refresh() - } - } - ] - ColumnLayout { id: loadingMessage - visible: deviceConnected && view.count == 0 && view.headerItem.childAt(0, 0).text.length == 0 + visible: deviceConnected && view.count == 0 && currentSearchText.length == 0 anchors.centerIn: parent BusyIndicator { + visible: loadingMessage.visible running: loadingMessage.visible Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter } Label { - text: "Loading conversations from device. If this takes a long time, please wake up your device and then click refresh." + text: i18nd("kdeconnect-sms", "Loading conversations from device. If this takes a long time, please wake up your device and then click Refresh.") Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter Layout.preferredWidth: page.width / 2 horizontalAlignment: Text.AlignHCenter @@ -65,16 +55,26 @@ Kirigami.ScrollablePage } Label { - text: "Tip: If you plug in your device, it should not go into doze mode and should load quickly." + text: i18nd("kdeconnect-sms", "Tip: If you plug in your device, it should not go into doze mode and should load quickly.") Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter Layout.preferredWidth: page.width / 2 horizontalAlignment: Text.AlignHCenter wrapMode: Text.Wrap } + + Button { + text: i18nd("kdeconnect-sms", "Refresh") + icon.name: "view-refresh" + Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter + onClicked: { + conversationListModel.refresh(); + } + } } property string initialMessage property string initialDevice + property int currentDeviceIndex: -1 header: Kirigami.InlineMessage { Layout.fillWidth: true @@ -90,27 +90,12 @@ Kirigami.ScrollablePage ] } - footer: ComboBox { - id: devicesCombo - enabled: count > 0 - displayText: !enabled ? i18nd("kdeconnect-sms", "No devices available") : undefined - model: DevicesSortProxyModel { - id: devicesModel - //TODO: make it possible to filter if they can do sms - sourceModel: DevicesModel { displayFilter: DevicesModel.Paired | DevicesModel.Reachable } - onRowsInserted: if (devicesCombo.currentIndex < 0) { - if (page.initialDevice) - devicesCombo.currentIndex = devicesModel.rowForDevice(page.initialDevice); - else - devicesCombo.currentIndex = 0 - } - } - textRole: "display" - } + property int devicesCount + property QtObject device - readonly property bool deviceConnected: devicesCombo.enabled - readonly property QtObject device: devicesCombo.currentIndex >= 0 ? devicesModel.data(devicesModel.index(devicesCombo.currentIndex, 0), DevicesModel.DeviceRole) : null + readonly property bool deviceConnected: devicesCount > 0 readonly property alias lastDeviceId: conversationListModel.deviceId + property string currentSearchText Component { id: chatView @@ -120,6 +105,75 @@ Kirigami.ScrollablePage } } + titleDelegate: RowLayout { + id: headerLayout + width: parent.width + Keys.forwardTo: [filter] + Kirigami.SearchField { + /** + * Used as the filter of the list of messages + */ + id: filter + placeholderText: i18nd("kdeconnect-sms", "Search or start conversation...") + Layout.fillWidth: true + Layout.fillHeight: true + onTextChanged: { + currentSearchText = filter.text; + if (filter.text != "") { + view.model.setConversationsFilterRole(ConversationListModel.AddressesRole) + } else { + view.model.setConversationsFilterRole(ConversationListModel.ConversationIdRole) + } + view.model.setFilterFixedString(SmsHelper.canonicalizePhoneNumber(filter.text)) + + view.currentIndex = 0 + filter.forceActiveFocus(); + } + Keys.onReturnPressed: { + event.accepted = true + view.currentItem.startChat() + } + Keys.onEscapePressed: { + event.accepted = filter.text != "" + filter.text = "" + } + Shortcut { + sequence: "Ctrl+F" + onActivated: filter.forceActiveFocus() + } + } + + Button { + id: newButton + icon.name: "list-add" + text: i18nd("kdeconnect-sms", "New") + visible: true + enabled: SmsHelper.isAddressValid(filter.text) && deviceConnected + ToolTip.visible: hovered + ToolTip.delay: Qt.styleHints.mousePressAndHoldInterval + ToolTip.text: i18nd("kdeconnect-sms", "Start new conversation") + + onClicked: { + // We have to disable the filter temporarily in order to avoid getting key inputs accidently while processing the request + filter.enabled = false + + // If the address entered by the user already exists then ignore adding new contact + if (!view.model.doesAddressExists(filter.text) && SmsHelper.isAddressValid(filter.text)) { + conversationListModel.createConversationForAddress(filter.text) + view.currentIndex = 0 + } + filter.enabled = true + } + + Shortcut { + sequence: "Ctrl+N" + onActivated: newButton.onClicked() + } + } + } + + property alias conversationListModel: conversationListModel + ListView { id: view currentIndex: 0 @@ -133,75 +187,6 @@ Kirigami.ScrollablePage } } - header: RowLayout { - width: parent.width - z: 10 - Keys.forwardTo: [filter] - TextField { - /** - * Used as the filter of the list of messages - */ - id: filter - placeholderText: i18nd("kdeconnect-sms", "Filter...") - Layout.fillWidth: true - Layout.fillHeight: true - onTextChanged: { - if (filter.text != "") { - view.model.setConversationsFilterRole(ConversationListModel.AddressesRole) - } else { - view.model.setConversationsFilterRole(ConversationListModel.ConversationIdRole) - } - view.model.setFilterFixedString(SmsHelper.canonicalizePhoneNumber(filter.text)) - - view.currentIndex = 0 - } - onAccepted: { - view.currentItem.startChat() - } - Keys.onReturnPressed: { - event.accepted = true - filter.onAccepted() - } - Keys.onEscapePressed: { - event.accepted = filter.text != "" - filter.text = "" - } - Shortcut { - sequence: "Ctrl+F" - onActivated: filter.forceActiveFocus() - } - } - - Button { - id: newButton - text: i18nd("kdeconnect-sms", "New") - visible: true - enabled: SmsHelper.isAddressValid(filter.text) && deviceConnected - ToolTip.visible: hovered - ToolTip.delay: Qt.styleHints.mousePressAndHoldInterval - ToolTip.text: i18nd("kdeconnect-sms", "Start new conversation") - - onClicked: { - // We have to disable the filter temporarily in order to avoid getting key inputs accidently while processing the request - filter.enabled = false - - // If the address entered by the user already exists then ignore adding new contact - if (!view.model.doesAddressExists(filter.text) && SmsHelper.isAddressValid(filter.text)) { - conversationListModel.createConversationForAddress(filter.text) - view.currentIndex = 0 - } - filter.enabled = true - } - - Shortcut { - sequence: "Ctrl+N" - onActivated: newButton.onClicked() - } - } - } - - headerPositioning: ListView.OverlayHeader - Keys.forwardTo: [headerItem] delegate: Kirigami.BasicListItem @@ -215,6 +200,7 @@ Kirigami.ScrollablePage property var thumbnail: attachmentPreview function startChat() { + view.currentItem.forceActiveFocus(); applicationWindow().pageStack.push(chatView, { addresses: addresses, conversationId: model.conversationId, @@ -264,7 +250,7 @@ Kirigami.ScrollablePage Kirigami.PlaceholderMessage { anchors.centerIn: parent width: parent.width - (Kirigami.Units.largeSpacing * 4) - visible: deviceConnected && view.count == 0 && view.headerItem.childAt(0, 0).text.length != 0 + visible: deviceConnected && view.count == 0 && currentSearchText.length != 0 text: i18ndc("kdeconnect-sms", "Placeholder message text when no messages are found", "No matches") } } diff --git a/smsapp/qml/SendingArea.qml b/smsapp/qml/SendingArea.qml index af04498d8..84ce26061 100644 --- a/smsapp/qml/SendingArea.qml +++ b/smsapp/qml/SendingArea.qml @@ -74,7 +74,13 @@ ColumnLayout { topPadding: Kirigami.Units.gridUnit * 0.5 bottomPadding: topPadding selectByMouse: true - topInset: height * 2 // This removes background (frame) of the TextArea. Setting `background: Item {}` would cause segfault. + hoverEnabled: true + background: MouseArea { + hoverEnabled: true + acceptedButtons: Qt.NoButton + cursorShape: Qt.IBeamCursor + z: 1 + } Keys.onReturnPressed: { if (event.key === Qt.Key_Return) { if (event.modifiers & Qt.ShiftModifier) { @@ -90,47 +96,9 @@ ColumnLayout { ColumnLayout { id: sendButtonArea + Layout.alignment: Qt.AlignBottom RowLayout { - Controls.ToolButton { - id: sendButton - enabled: messageField.text.length || selectedFileUrls.length - Layout.preferredWidth: Kirigami.Units.gridUnit * 2 - Layout.preferredHeight: Kirigami.Units.gridUnit * 2 - padding: 0 - Kirigami.Icon { - source: "document-send" - enabled: sendButton.enabled - isMask: true - smooth: true - anchors.centerIn: parent - width: Kirigami.Units.gridUnit * 1.5 - height: width - } - - property bool messageSent: false - - onClicked: { - // disable the button to prevent sending - // the same message several times - sendButton.enabled = false - - if (SmsHelper.totalMessageSize(selectedFileUrls, messageField.text) > maxMessageSize) { - messageDialog.visible = true - } else if (page.conversationId === page.invalidId) { - messageSent = conversationModel.startNewConversation(messageField.text, addresses, selectedFileUrls) - } else { - messageSent = conversationModel.sendReplyToConversation(messageField.text, selectedFileUrls) - } - - if (messageSent) { - messageField.text = "" - selectedFileUrls = [] - sendButton.enabled = false - } - } - } - Controls.ToolButton { id: attachFilesButton enabled: true @@ -180,6 +148,45 @@ ColumnLayout { } } } + + Controls.ToolButton { + id: sendButton + enabled: messageField.text.length || selectedFileUrls.length + Layout.preferredWidth: Kirigami.Units.gridUnit * 2 + Layout.preferredHeight: Kirigami.Units.gridUnit * 2 + padding: 0 + Kirigami.Icon { + source: "document-send" + enabled: sendButton.enabled + isMask: true + smooth: true + anchors.centerIn: parent + width: Kirigami.Units.gridUnit * 1.5 + height: width + } + + property bool messageSent: false + + onClicked: { + // disable the button to prevent sending + // the same message several times + sendButton.enabled = false + + if (SmsHelper.totalMessageSize(selectedFileUrls, messageField.text) > maxMessageSize) { + messageDialog.visible = true + } else if (page.conversationId === page.invalidId) { + messageSent = conversationModel.startNewConversation(messageField.text, addresses, selectedFileUrls) + } else { + messageSent = conversationModel.sendReplyToConversation(messageField.text, selectedFileUrls) + } + + if (messageSent) { + messageField.text = "" + selectedFileUrls = [] + sendButton.enabled = false + } + } + } } Controls.Label { diff --git a/smsapp/qml/main.qml b/smsapp/qml/main.qml index 77fff92f1..ae89d3e68 100644 --- a/smsapp/qml/main.qml +++ b/smsapp/qml/main.qml @@ -9,6 +9,7 @@ import QtQuick 2.1 import org.kde.kirigami 2.6 as Kirigami import org.kde.kdeconnect 1.0 +import org.kde.kdeconnect.sms 1.0 Kirigami.ApplicationWindow { @@ -17,9 +18,65 @@ Kirigami.ApplicationWindow width: 800 height: 600 + property int currentDeviceIndex + property int devicesCount + property string initialDevice + property QtObject device + + Component { + id: deviceActionComponent + Kirigami.Action { + property int deviceIndex + onTriggered: { + root.currentDeviceIndex = deviceIndex + } + icon.name: root.currentDeviceIndex === deviceIndex ? "checkmark" : "" + } + } + + DevicesSortProxyModel { + id: devicesModel + //TODO: make it possible to filter if they can do sms + sourceModel: DevicesModel { displayFilter: DevicesModel.Paired | DevicesModel.Reachable } + function populateDevicesMenu() { + root.globalDrawer.actions[0].children = []; + for (var i = 0; i < devicesModel.rowCount(); i++) { + var dev = devicesModel.data(devicesModel.index(i, 0), DevicesSortProxyModel.DisplayRole); + var obj = deviceActionComponent.createObject(root.globalDrawer.actions[0], { + text: dev, + deviceIndex: i + }); + root.globalDrawer.actions[0].children.push(obj); + } + } + onRowsInserted: { + if (root.currentDeviceIndex < 0) { + if (root.initialDevice) { + root.currentDeviceIndex = devicesModel.rowForDevice(root.initialDevice); + } else { + root.currentDeviceIndex = 0; + } + } + root.device = root.currentDeviceIndex >= 0 ? devicesModel.data(devicesModel.index(root.currentDeviceIndex, 0), DevicesModel.DeviceRole) : null + root.devicesCount = devicesModel.rowCount(); + populateDevicesMenu(); + } + onRowsRemoved: { + root.devicesCount = devicesModel.rowCount(); + populateDevicesMenu(); + } + } + onCurrentDeviceIndexChanged: { + root.device = root.currentDeviceIndex >= 0 ? devicesModel.data(devicesModel.index(root.currentDeviceIndex, 0), DevicesModel.DeviceRole) : null + } + pageStack.initialPage: ConversationList { title: i18nd("kdeconnect-sms", "KDE Connect SMS") initialMessage: initialMessage + device: root.device; + initialDevice: initialDevice + currentDeviceIndex: root.currentDeviceIndex; + devicesCount: root.devicesCount; } Component { @@ -32,6 +89,19 @@ Kirigami.ApplicationWindow isMenu: true actions: [ + Kirigami.Action { + text: i18nd("kdeconnect-sms", "Devices") + icon.name: "phone" + visible: devicesCount > 1 + }, + Kirigami.Action { + text: i18nd("kdeconnect-sms", "Refresh") + icon.name: "view-refresh" + enabled: devicesCount > 0 + onTriggered: { + pageStack.initialPage.conversationListModel.refresh(); + } + }, Kirigami.Action { text: i18nd("kdeconnect-sms", "About") icon.name: "help-about"