From 06a6a4c08df7e28c2b36795052a3f42a20d6b21e Mon Sep 17 00:00:00 2001 From: miruka Date: Sun, 8 Dec 2019 14:43:41 -0400 Subject: [PATCH] Rewrite SidePane using QQC Drawer Cleaner approach, gets rid of the HPage swipe view hack, better performances, a lot less complex --- TODO.md | 4 + src/qml/Base/HDrawer.qml | 72 ++++++++++++ src/qml/Base/HPage.qml | 113 ++++++++----------- src/qml/Chat/Chat.qml | 6 +- src/qml/Chat/ChatSplitView.qml | 2 +- src/qml/Chat/Composer.qml | 4 +- src/qml/SidePane/AccountDelegate.qml | 6 +- src/qml/SidePane/RoomDelegate.qml | 6 +- src/qml/SidePane/SidePane.qml | 106 +++--------------- src/qml/SidePane/SidePaneList.qml | 1 - src/qml/SidePane/SidePaneToolBar.qml | 28 +++-- src/qml/UI.qml | 159 +++++++++++++-------------- src/themes/Default.qpl | 3 - 13 files changed, 250 insertions(+), 260 deletions(-) create mode 100644 src/qml/Base/HDrawer.qml diff --git a/TODO.md b/TODO.md index 1bc33dad..f6fc3498 100644 --- a/TODO.md +++ b/TODO.md @@ -1,3 +1,7 @@ +- make pageup/down not slippery again +- rename all setfocus() to takefocus() +- refactor roomsidepane too +- better pane minsize - better cancel for all boxes - Media - Confirmation box after picking file to upload diff --git a/src/qml/Base/HDrawer.qml b/src/qml/Base/HDrawer.qml new file mode 100644 index 00000000..9d3ccae9 --- /dev/null +++ b/src/qml/Base/HDrawer.qml @@ -0,0 +1,72 @@ +import QtQuick 2.13 +import QtQuick.Controls 2.13 +import "../utils.js" as Utils + +Drawer { + id: drawer + implicitWidth: calculatedWidth + implicitHeight: parent.height + + // FIXME: https://bugreports.qt.io/browse/QTBUG-59141 + // dragMargin: parent.width / 2 + + interactive: collapse + position: 1 + visible: ! collapse + modal: false + closePolicy: Popup.CloseOnEscape + + background: Rectangle { id: bg; color: theme.colors.strongBackground } + + + signal userResized(int newWidth) + + property int normalWidth: 300 + property int minNormalWidth: resizeAreaWidth + property int maxNormalWidth: parent.width + + property bool collapse: window.width < 400 + property int collapseExpandedWidth: parent.width + + property alias color: bg.color + property alias resizeAreaWidth: resizeArea.width + + readonly property int calculatedWidth: + collapse ? + collapseExpandedWidth : + Math.max(minNormalWidth, Math.min(normalWidth, maxNormalWidth)) + + + Behavior on width { + enabled: ! resizeMouseHandler.drag.active + NumberAnimation { duration: 100 } + } + + Item { + id: resizeArea + anchors.right: parent.right + width: theme.spacing / 2 + height: parent.height + z: 9999 + + MouseArea { + id: resizeMouseHandler + anchors.fill: parent + enabled: ! drawer.collapse + acceptedButtons: Qt.LeftButton + hoverEnabled: true + cursorShape: + containsMouse || drag.active ? + Qt.SizeHorCursor : Qt.ArrowCursor + + onPressed: canResize = true + onReleased: { canResize = false; userResized(drawer.normalWidth) } + + onMouseXChanged: + if (canResize) + drawer.normalWidth = drawer.calculatedWidth + mouseX + + property bool canResize: false + } + } +} diff --git a/src/qml/Base/HPage.qml b/src/qml/Base/HPage.qml index fd7f1b28..daec6620 100644 --- a/src/qml/Base/HPage.qml +++ b/src/qml/Base/HPage.qml @@ -3,14 +3,13 @@ import QtQuick.Controls 2.12 import QtQuick.Layouts 1.12 import "../SidePane" -SwipeView { +Page { + id: innerPage + + default property alias columnChildren: contentColumn.children - property alias page: innerPage - property alias header: innerPage.header - property alias footer: innerPage.header property alias flickable: innerFlickable - property alias headerLabel: innerHeaderLabel property var hideHeaderUnderHeight: null @@ -19,72 +18,56 @@ SwipeView { property bool becomeKeyboardFlickableTarget: true - id: swipeView - clip: true - interactive: sidePane.reduce - currentIndex: 1 - SidePane { - implicitWidth: swipeView.width - animateWidth: false // Without this, the SidePane gets auto-focused - collapse: false - reduce: false - visible: swipeView.interactive - onVisibleChanged: if (currentIndex != 1) swipeView.setCurrentIndex(1) + background: null + + header: Rectangle { + implicitWidth: parent ? parent.width : 0 + color: theme.controls.header.background + + height: innerHeaderLabel.text && ( + ! hideHeaderUnderHeight || + window.height >= + hideHeaderUnderHeight + + theme.baseElementsHeight + + currentSpacing * 2 + ) ? theme.baseElementsHeight : 0 + + Behavior on height { HNumberAnimation {} } + visible: height > 0 + + HLabel { + id: innerHeaderLabel + anchors.fill: parent + textFormat: Text.StyledText + font.pixelSize: theme.fontSize.big + elide: Text.ElideRight + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + + leftPadding: currentSpacing + rightPadding: leftPadding + } } - Page { - id: innerPage - background: null + leftPadding: currentSpacing < theme.spacing ? 0 : currentSpacing + rightPadding: leftPadding + Behavior on leftPadding { HNumberAnimation {} } - header: Rectangle { - implicitWidth: parent ? parent.width : 0 - color: theme.controls.header.background + HFlickable { + id: innerFlickable + anchors.fill: parent + clip: true + contentWidth: parent.width + contentHeight: contentColumn.childrenRect.height - height: innerHeaderLabel.text && ( - ! hideHeaderUnderHeight || - window.height >= - hideHeaderUnderHeight + - theme.baseElementsHeight + - currentSpacing * 2 - ) ? theme.baseElementsHeight : 0 + Component.onCompleted: + if (becomeKeyboardFlickableTarget) shortcuts.flickTarget = this - Behavior on height { HNumberAnimation {} } - visible: height > 0 - - HLabel { - id: innerHeaderLabel - anchors.fill: parent - textFormat: Text.StyledText - font.pixelSize: theme.fontSize.big - elide: Text.ElideRight - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - - leftPadding: currentSpacing - rightPadding: leftPadding - } - } - - leftPadding: currentSpacing < theme.spacing ? 0 : currentSpacing - rightPadding: leftPadding - Behavior on leftPadding { HNumberAnimation {} } - - HFlickable { - id: innerFlickable - anchors.fill: parent - clip: true - contentWidth: parent.width - contentHeight: contentColumn.childrenRect.height - - Component.onCompleted: - if (becomeKeyboardFlickableTarget) shortcuts.flickTarget = this - - HColumnLayout { - id: contentColumn - width: innerFlickable.width - height: innerFlickable.height - } + HColumnLayout { + id: contentColumn + width: innerFlickable.width + height: innerFlickable.height } } } diff --git a/src/qml/Chat/Chat.qml b/src/qml/Chat/Chat.qml index 4aaa0b39..f538bc78 100644 --- a/src/qml/Chat/Chat.qml +++ b/src/qml/Chat/Chat.qml @@ -5,6 +5,9 @@ import "../utils.js" as Utils HPage { id: chatPage + leftPadding: 0 + rightPadding: 0 + // The target will be our EventList, not the page itself becomeKeyboardFlickableTarget: false @@ -49,9 +52,6 @@ HPage { Behavior on height { HNumberAnimation {} } } - page.leftPadding: 0 - page.rightPadding: 0 - HLoader { source: ready ? "ChatSplitView.qml" : "../Base/HBusyIndicator.qml" diff --git a/src/qml/Chat/ChatSplitView.qml b/src/qml/Chat/ChatSplitView.qml index 3705c56c..620ebcd0 100644 --- a/src/qml/Chat/ChatSplitView.qml +++ b/src/qml/Chat/ChatSplitView.qml @@ -8,7 +8,7 @@ import "FileTransfer" HSplitView { id: chatSplitView - Component.onCompleted: composer.setFocus() + Component.onCompleted: composer.takeFocus() HColumnLayout { Layout.fillWidth: true diff --git a/src/qml/Chat/Composer.qml b/src/qml/Chat/Composer.qml index 45aeb635..f5c00ce8 100644 --- a/src/qml/Chat/Composer.qml +++ b/src/qml/Chat/Composer.qml @@ -5,8 +5,6 @@ import "../Dialogs" import "../utils.js" as Utils Rectangle { - function setFocus() { areaScrollView.forceActiveFocus() } - property string indent: " " property var aliases: window.settings.writeAliases @@ -40,6 +38,8 @@ Rectangle { lineTextUntilCursor.match(/ {1,4}/g).slice(-1)[0].length : 1 + function takeFocus() { areaScrollView.forceActiveFocus() } + // property var pr: lineTextUntilCursor // onPrChanged: print( // "y", cursorY, "x", cursorX, diff --git a/src/qml/SidePane/AccountDelegate.qml b/src/qml/SidePane/AccountDelegate.qml index 75ae04a9..a167abb7 100644 --- a/src/qml/SidePane/AccountDelegate.qml +++ b/src/qml/SidePane/AccountDelegate.qml @@ -6,7 +6,7 @@ import "../utils.js" as Utils HTileDelegate { id: accountDelegate spacing: 0 - topPadding: model.index > 0 ? sidePane.currentSpacing / 2 : 0 + topPadding: model.index > 0 ? theme.spacing / 2 : 0 bottomPadding: topPadding backgroundColor: theme.sidePane.account.background opacity: collapsed && ! forceExpand ? @@ -54,7 +54,7 @@ HTileDelegate { title.color: theme.sidePane.account.name title.text: model.data.display_name || model.data.user_id title.font.pixelSize: theme.fontSize.big - title.leftPadding: sidePane.currentSpacing + title.leftPadding: theme.spacing HButton { id: addChat @@ -112,7 +112,7 @@ HTileDelegate { text: qsTr("Sign out") onTriggered: Utils.makePopup( "Popups/SignOutPopup.qml", - mainUI, + window, { "userId": model.data.user_id }, popup => { popup.ok.connect(() => { disconnecting = true }) }, ) diff --git a/src/qml/SidePane/RoomDelegate.qml b/src/qml/SidePane/RoomDelegate.qml index 1243e87e..2ab221a2 100644 --- a/src/qml/SidePane/RoomDelegate.qml +++ b/src/qml/SidePane/RoomDelegate.qml @@ -5,7 +5,7 @@ import "../utils.js" as Utils HTileDelegate { id: roomDelegate - spacing: sidePane.currentSpacing + spacing: theme.spacing backgroundColor: theme.sidePane.room.background opacity: model.data.left ? theme.sidePane.room.leftRoomOpacity : 1 @@ -110,7 +110,7 @@ HTileDelegate { onTriggered: Utils.makePopup( "Popups/LeaveRoomPopup.qml", - sidePane, + window, { userId: model.user_id, roomId: model.data.room_id, @@ -126,7 +126,7 @@ HTileDelegate { onTriggered: Utils.makePopup( "Popups/ForgetRoomPopup.qml", - sidePane, + window, { userId: model.user_id, roomId: model.data.room_id, diff --git a/src/qml/SidePane/SidePane.qml b/src/qml/SidePane/SidePane.qml index f69e03eb..61badaf3 100644 --- a/src/qml/SidePane/SidePane.qml +++ b/src/qml/SidePane/SidePane.qml @@ -3,108 +3,37 @@ import QtQuick.Layouts 1.12 import "../Base" import "../utils.js" as Utils -Rectangle { +HDrawer { id: sidePane clip: true - opacity: mainUI.accountsPresent && ! reduce ? 1 : 0 - visible: opacity > 0 - + opacity: mainUI.accountsPresent ? 1 : 0 color: theme.sidePane.background + normalWidth: window.uiState.sidePaneManualWidth + + onUserResized: { + window.uiState.sidePaneManualWidth = newWidth + window.uiStateChanged() + } + property bool hasFocus: toolBar.filterField.activeFocus property alias sidePaneList: sidePaneList property alias toolBar: toolBar - property real autoWidthRatio: theme.sidePane.autoWidthRatio - property bool manuallyResizing: false - property bool manuallyResized: false - property int manualWidth: 0 - property bool animateWidth: true - Component.onCompleted: { - if (window.uiState.sidePaneManualWidth) { - manualWidth = window.uiState.sidePaneManualWidth - manuallyResized = true - } - } - - onFocusChanged: if (focus) toolBar.filterField.forceActiveFocus() - - onManualWidthChanged: { - window.uiState.sidePaneManualWidth = manualWidth - window.uiStateChanged() - } - - property int maximumCalculatedWidth: Math.min( - manuallyResized ? manualWidth : theme.sidePane.maximumAutoWidth, - window.width - theme.minimumSupportedWidthPlusSpacing - ) - - property int parentWidth: parent.width - // Needed for SplitView since it breaks the binding when user manual sizes - onParentWidthChanged: width = Qt.binding(() => implicitWidth) - - - property int calculatedWidth: Math.min( - manuallyResized ? manualWidth * theme.uiScale : - parentWidth * autoWidthRatio, - - maximumCalculatedWidth - ) - - property bool collapse: - (manuallyResizing ? width : calculatedWidth) < - (manuallyResized ? - (theme.sidePane.collapsedWidth + theme.spacing * 2) : - theme.sidePane.autoCollapseBelowWidth) - - property bool reduce: - window.width < theme.sidePane.autoReduceBelowWindowWidth - - property int implicitWidth: - reduce ? 0 : - collapse ? theme.sidePane.collapsedWidth : - calculatedWidth - - property int currentSpacing: - width <= theme.sidePane.collapsedWidth + theme.spacing * 2 ? - 0 : theme.spacing - - Behavior on currentSpacing { HNumberAnimation {} } - Behavior on implicitWidth { - HNumberAnimation { factor: animateWidth ? 1 : 0 } - } - - - function setFocus() { - forceActiveFocus() - if (reduce) { - pageLoader.item.currentIndex = 0 - } - } - - - Keys.enabled: sidePane.hasFocus - Keys.onUpPressed: sidePaneList.previous(false) // do not activate - Keys.onDownPressed: sidePaneList.next(false) - Keys.onEnterPressed: Keys.onReturnPressed(event) - Keys.onReturnPressed: if (event.modifiers & Qt.ShiftModifier) { - sidePaneList.toggleCollapseAccount() - } else { - if (window.settings.clearRoomFilterOnEnter) { - mainUI.sidePane.toolBar.roomFilter = "" + function toggleFocus() { + if (toolBar.filterField.activeFocus) { + pageLoader.takeFocus() + return } - sidePaneList.activate() - } - Keys.onEscapePressed: { - if (window.settings.clearRoomFilterOnEscape) { - mainUI.sidePane.toolBar.roomFilter = "" - } - mainUI.pageLoader.forceActiveFocus() + sidePane.open() + toolBar.filterField.forceActiveFocus() } + Behavior on opacity { HOpacityAnimator {} } + HColumnLayout { anchors.fill: parent @@ -118,6 +47,7 @@ Rectangle { SidePaneToolBar { id: toolBar + sidePaneList: sidePaneList } } } diff --git a/src/qml/SidePane/SidePaneList.qml b/src/qml/SidePane/SidePaneList.qml index 77149f8a..b0425ded 100644 --- a/src/qml/SidePane/SidePaneList.qml +++ b/src/qml/SidePane/SidePaneList.qml @@ -108,7 +108,6 @@ HListView { { currentIndex = i currentItem.item.toggleCollapse() - activate() } } } diff --git a/src/qml/SidePane/SidePaneToolBar.qml b/src/qml/SidePane/SidePaneToolBar.qml index 214591fa..9234a6e5 100644 --- a/src/qml/SidePane/SidePaneToolBar.qml +++ b/src/qml/SidePane/SidePaneToolBar.qml @@ -5,6 +5,7 @@ import "../Base" HRowLayout { id: toolBar + property SidePaneList sidePaneList readonly property alias addAccountButton: addAccountButton readonly property alias filterField: filterField property alias roomFilter: filterField.text @@ -29,8 +30,7 @@ HRowLayout { backgroundColor: theme.sidePane.filterRooms.background bordered: false - Layout.fillWidth: true - Layout.fillHeight: true + Component.onCompleted: filterField.text = uiState.sidePaneFilter onTextChanged: { if (window.uiState.sidePaneFilter == text) return @@ -38,12 +38,26 @@ HRowLayout { window.uiStateChanged() } - Connections { - target: window - // Keep multiple instances of SidePaneToolBar in sync. - // This also sets the text on startup. - onUiStateChanged: filterField.text = uiState.sidePaneFilter + Layout.fillWidth: true + Layout.fillHeight: true + + Keys.onUpPressed: sidePaneList.previous(false) // do not activate + Keys.onDownPressed: sidePaneList.next(false) + + Keys.onEnterPressed: Keys.onReturnPressed(event) + Keys.onReturnPressed: { + if (event.modifiers & Qt.ShiftModifier) { + sidePaneList.toggleCollapseAccount() + return + } + + if (window.settings.clearRoomFilterOnEnter) text = "" + sidePaneList.activate() } + Keys.onEscapePressed: { + if (window.settings.clearRoomFilterOnEscape) text = "" + mainUI.pageLoader.forceActiveFocus() + } } } diff --git a/src/qml/UI.qml b/src/qml/UI.qml index 61e60324..edb39157 100644 --- a/src/qml/UI.qml +++ b/src/qml/UI.qml @@ -11,7 +11,6 @@ Item { id: mainUI focus: true Component.onCompleted: window.mainUI = mainUI - Keys.forwardTo: [shortcuts] readonly property alias shortcuts: shortcuts readonly property alias sidePane: sidePane @@ -55,107 +54,99 @@ Item { } - HSplitView { - id: uiSplitView + SidePane { + id: sidePane + maxNormalWidth: parent.width - theme.minimumSupportedWidth + } + + HLoader { + id: pageLoader anchors.fill: parent + anchors.leftMargin: sidePane.width * sidePane.position + visible: ! sidePane.hidden || anchors.leftMargin < width - onAnyResizingChanged: if (anyResizing) { - sidePane.manuallyResizing = true - } else { - sidePane.manuallyResizing = false - sidePane.manuallyResized = true - sidePane.manualWidth = sidePane.width + // onSourceChanged: if (sidePane.collapse) sidePane.close() + + + property bool isWide: width > theme.contentIsWideAbove + + // List of previously loaded [componentUrl, {properties}] + property var history: [] + property int historyLength: 20 + + Component.onCompleted: { + if (! py.startupAnyAccountsSaved) { + pageLoader.showPage("AddAccount/AddAccount") + return + } + + let page = window.uiState.page + let props = window.uiState.pageProperties + + if (page == "Chat/Chat.qml") { + pageLoader.showRoom(props.userId, props.roomId) + } else { + pageLoader._show(page, props) + } } - SidePane { - id: sidePane + function _show(componentUrl, properties={}) { + history.unshift([componentUrl, properties]) + if (history.length > historyLength) history.pop() - // Initial width until user manually resizes - width: implicitWidth - Layout.minimumWidth: reduce ? 0 : theme.sidePane.collapsedWidth - Layout.maximumWidth: - window.width - theme.minimumSupportedWidthPlusSpacing - - Behavior on Layout.minimumWidth { HNumberAnimation {} } + pageLoader.setSource(componentUrl, properties) } - HLoader { - id: pageLoader + function showPage(name, properties={}) { + let path = `Pages/${name}.qml` + _show(path, properties) - property bool isWide: width > theme.contentIsWideAbove + window.uiState.page = path + window.uiState.pageProperties = properties + window.uiStateChanged() + } - // List of previously loaded [componentUrl, {properties}] - property var history: [] - property int historyLength: 20 + function showRoom(userId, roomId) { + _show("Chat/Chat.qml", {userId, roomId}) - Component.onCompleted: { - if (! py.startupAnyAccountsSaved) { - pageLoader.showPage("AddAccount/AddAccount") - return - } + window.uiState.page = "Chat/Chat.qml" + window.uiState.pageProperties = {userId, roomId} + window.uiStateChanged() + } - let page = window.uiState.page - let props = window.uiState.pageProperties + function showPrevious(timesBack=1) { + timesBack = Math.min(timesBack, history.length - 1) + if (timesBack < 1) return false - if (page == "Chat/Chat.qml") { - pageLoader.showRoom(props.userId, props.roomId) - } else { - pageLoader._show(page, props) - } - } + let [componentUrl, properties] = history[timesBack] - function _show(componentUrl, properties={}) { - history.unshift([componentUrl, properties]) - if (history.length > historyLength) history.pop() + _show(componentUrl, properties) - pageLoader.setSource(componentUrl, properties) - } + window.uiState.page = componentUrl + window.uiState.pageProperties = properties + window.uiStateChanged() + return true + } - function showPage(name, properties={}) { - let path = `Pages/${name}.qml` - _show(path, properties) + function takeFocus() { + pageLoader.item.forceActiveFocus() + if (sidePane.collapse) sidePane.close() + } - window.uiState.page = path - window.uiState.pageProperties = properties - window.uiStateChanged() - } - function showRoom(userId, roomId) { - _show("Chat/Chat.qml", {userId, roomId}) + onStatusChanged: if (status == Loader.Ready) { + pageLoader.takeFocus() + appearAnimation.start() + } - window.uiState.page = "Chat/Chat.qml" - window.uiState.pageProperties = {userId, roomId} - window.uiStateChanged() - } - - function showPrevious(timesBack=1) { - timesBack = Math.min(timesBack, history.length - 1) - if (timesBack < 1) return false - - let [componentUrl, properties] = history[timesBack] - - _show(componentUrl, properties) - - window.uiState.page = componentUrl - window.uiState.pageProperties = properties - window.uiStateChanged() - return true - } - - onStatusChanged: if (status == Loader.Ready) { - item.forceActiveFocus() - appearAnimation.start() - } - - clip: appearAnimation.running - XAnimator { - id: appearAnimation - target: pageLoader.item - from: -300 - to: 0 - easing.type: Easing.OutBack - duration: theme.animationDuration * 2 - } + clip: appearAnimation.running + XAnimator { + id: appearAnimation + target: pageLoader.item + from: -300 + to: 0 + easing.type: Easing.OutBack + duration: theme.animationDuration * 2 } } diff --git a/src/themes/Default.qpl b/src/themes/Default.qpl index 0b36e9c7..c92394b6 100644 --- a/src/themes/Default.qpl +++ b/src/themes/Default.qpl @@ -261,9 +261,6 @@ ui: sidePane: - real autoWidthRatio: 0.33 * uiScale - int maximumAutoWidth: 320 * uiScale - int autoCollapseBelowWidth: 128 * uiScale int collapsedWidth: controls.avatar.size