From 48dedcd546ee5468500e409f9c6f0b7d71c76e10 Mon Sep 17 00:00:00 2001 From: Micah Stanley Date: Mon, 25 Nov 2024 17:30:47 +0000 Subject: [PATCH] taskpanel/panel: make navbar and statusbar accessible from within fullscreen windows These changes make the navigation and status bar accessible from within fullscreen applications by allowing the user to tap on the top or tap or drag on the bottom of the screen. Also, since the top panel is now over top of all fullscreen applications, this moves the action drawer swipe area back into the status bar. ![navandstatusbar](/uploads/29bce14baf957059669689345c909896/navandstatusbar.gif) --- components/mobileshell/components/swipearea.h | 3 +- .../qml/actiondrawer/ActionDrawer.qml | 31 +-- .../qml/actiondrawer/ActionDrawerWindow.qml | 17 +- .../qml/components/ScreenEdgeDragEffect.qml | 71 +++++++ components/mobileshell/shellutil.cpp | 8 + components/mobileshell/shellutil.h | 6 + .../mobileshellstate/shelldbusclient.cpp | 23 +++ components/mobileshellstate/shelldbusclient.h | 9 + .../mobileshellstate/shelldbusobject.cpp | 14 ++ components/mobileshellstate/shelldbusobject.h | 7 + .../qml/WindowMaximizedTracker.qml | 25 ++- .../panel/package/contents/ui/main.qml | 167 ++++++++++++++-- .../contents/ui/NavigationPanelComponent.qml | 3 +- .../taskpanel/package/contents/ui/main.qml | 178 +++++++++++++++++- 14 files changed, 495 insertions(+), 67 deletions(-) create mode 100644 components/mobileshell/qml/components/ScreenEdgeDragEffect.qml diff --git a/components/mobileshell/components/swipearea.h b/components/mobileshell/components/swipearea.h index 3de23451..a3bf899b 100644 --- a/components/mobileshell/components/swipearea.h +++ b/components/mobileshell/components/swipearea.h @@ -44,6 +44,7 @@ public: bool pressed() const; Q_INVOKABLE void setSkipSwipeThreshold(bool value); + Q_INVOKABLE void resetSwipe(); Q_SIGNALS: void modeChanged(); @@ -82,8 +83,6 @@ private: void handleReleaseEvent(QPointerEvent *event, QPointF point); void handleMoveEvent(QPointerEvent *event, QPointF point); - void resetSwipe(); - Mode m_mode = Mode::BothAxis; bool m_interactive = true; bool m_pressed = false; diff --git a/components/mobileshell/qml/actiondrawer/ActionDrawer.qml b/components/mobileshell/qml/actiondrawer/ActionDrawer.qml index d805b757..3427d25f 100644 --- a/components/mobileshell/qml/actiondrawer/ActionDrawer.qml +++ b/components/mobileshell/qml/actiondrawer/ActionDrawer.qml @@ -10,7 +10,6 @@ import QtQuick.Controls 2.15 import QtQuick.Layouts 1.1 import QtQuick.Window 2.2 -import org.kde.plasma.private.mobileshell.shellsettingsplugin as ShellSettings import org.kde.plasma.components 3.0 as PlasmaComponents import org.kde.plasma.private.nanoshell 2.0 as NanoShell import org.kde.plasma.private.mobileshell as MobileShell @@ -153,7 +152,7 @@ Item { function cancelAnimations() { root.state = ""; - } + } function open() { cancelAnimations(); @@ -273,35 +272,15 @@ Item { mode: MobileShell.SwipeArea.VerticalOnly anchors.fill: parent - - function startSwipeWithPoint(point) { + function startSwipe() { root.cancelAnimations(); root.dragging = true; // Immediately open action drawer if we interact with it and it's already open // This allows us to have 2 quick flicks from minimized -> expanded - if (root.intendedToBeVisible && !root.opened) { + if (root.visible && !root.opened) { root.opened = true; } - - // if the user swiped from the top left, otherwise it's from the top right - if (!root.intendedToBeVisible) { - if (point.x < root.width / 2) { - root.openToPinnedMode = ShellSettings.Settings.actionDrawerTopLeftMode == ShellSettings.Settings.Pinned; - } else { - root.openToPinnedMode = ShellSettings.Settings.actionDrawerTopRightMode == ShellSettings.Settings.Pinned; - } - - if (root.intendedToBeVisible) { - // ensure the action drawer state is consistent - root.closeImmediately(); - } - - actionDrawer.offset = 0; - actionDrawer.oldOffset = 0; - - intendedToBeVisible = true; - } } function endSwipe() { @@ -313,7 +292,7 @@ Item { root.offset += deltaY; } - onSwipeStarted: (point) => startSwipeWithPoint(point) + onSwipeStarted: startSwipe() onSwipeEnded: endSwipe() onSwipeMove: (totalDeltaX, totalDeltaY, deltaX, deltaY) => moveSwipe(totalDeltaX, totalDeltaY, deltaX, deltaY) @@ -325,8 +304,6 @@ Item { id: contentContainer anchors.fill: parent - opacity: root.opened || swipeArea.moving || drawerAnimation.running || root.offset > 0 || root.intendedToBeVisible ? 1 : 0 - actionDrawer: root quickSettingsModel: root.quickSettingsModel } diff --git a/components/mobileshell/qml/actiondrawer/ActionDrawerWindow.qml b/components/mobileshell/qml/actiondrawer/ActionDrawerWindow.qml index 9feaaa44..48fe6f43 100644 --- a/components/mobileshell/qml/actiondrawer/ActionDrawerWindow.qml +++ b/components/mobileshell/qml/actiondrawer/ActionDrawerWindow.qml @@ -33,28 +33,21 @@ Window { * The ActionDrawer component. */ property alias actionDrawer: drawer - property alias intendedToBeVisible: drawer.intendedToBeVisible property alias state: drawer.state - visible: true + visible: drawer.intendedToBeVisible color: "transparent" - Component.onCompleted: updateTouchArea() + // set input to transparent when closing to prevent window from taking unwanted touch inputs + onStateChanged: ShellUtil.setInputTransparent(window, state == "close") - function updateTouchArea() { - if (state != "" && state != "close") { + onVisibleChanged: { + if (visible) { window.raise(); - ShellUtil.setInputRegion(window, Qt.rect(0, 0, 0, 0)); - } else { - ShellUtil.setInputRegion(window, Qt.rect(0, 0, window.width, MobileShell.Constants.topPanelHeight)); } } - onWidthChanged: updateTouchArea() - onIntendedToBeVisibleChanged: updateTouchArea() - onStateChanged: updateTouchArea() - onActiveChanged: { if (!active) { drawer.close(); diff --git a/components/mobileshell/qml/components/ScreenEdgeDragEffect.qml b/components/mobileshell/qml/components/ScreenEdgeDragEffect.qml new file mode 100644 index 00000000..219590de --- /dev/null +++ b/components/mobileshell/qml/components/ScreenEdgeDragEffect.qml @@ -0,0 +1,71 @@ +// SPDX-FileCopyrightText: 2024 Micah Stanley +// SPDX-License-Identifier: GPL-2.0-or-later + +import QtQuick 2.4 +import QtQuick.Layouts 1.1 +import QtQuick.Window 2.15 +import QtQuick.Shapes 1.8 + +import org.kde.kirigami 2.20 as Kirigami + +Item { + id: root + + property bool isHorizontal: false + property real length: Kirigami.Units.gridUnit * 10 + property real offsetLimit: Kirigami.Units.gridUnit * 2 + + property real startPoint: 0 + property real sidePoint: 0 + property real offsetPoint: 0 + + visible: offsetPoint != 0 + + Shape { + id: shape + + readonly property int flip: root.offsetPoint > 0 ? -1 : 1 + readonly property real position: root.startPoint - root.length / 2 + readonly property real sp: Math.max(Math.min(root.sidePoint, root.length), -Kirigami.Units.gridUnit * 10) + readonly property real op: Math.max(Math.min(-shape.calculateResistance(-root.offsetPoint, 0), 0), -root.offsetLimit + 3) + + transform: [ + Translate { + x: root.isHorizontal ? 0 : shape.position + y: root.isHorizontal ? shape.position : 0 + } + ] + + function calculateResistance(value : double, threshold : int) : double { + if (value > threshold) { + return threshold + Math.pow(value - threshold + 1, Math.max(0.8 - (value - threshold) / ((root.isHorizontal ? Screen.width : Screen.height - threshold) * 2), 0.65)); + } else { + return value; + } + } + + readonly property var shapPath: [ + Qt.point(3 * shape.flip, 0), + Qt.point(2 * shape.flip, 0), + Qt.point(0, root.length * 0.2 + shape.sp * 0.16), + Qt.point(shape.op, root.length * 0.5 + shape.sp * 0.35), + Qt.point(0, root.length * 0.8 + shape.sp * 0.16), + Qt.point(2 * shape.flip, root.length), + Qt.point(3 * shape.flip, root.length), + ] + + ShapePath { + id: shapeVertical + fillColor: "black" + strokeColor: "black" + + startX: shape.shapPath[0].x; startY: shape.shapPath[0].y + PathCurve { x: root.isHorizontal ? shape.shapPath[1].x : shape.shapPath[1].y; y: root.isHorizontal ? shape.shapPath[1].y : shape.shapPath[1].x} + PathCurve { x: root.isHorizontal ? shape.shapPath[2].x : shape.shapPath[2].y; y: root.isHorizontal ? shape.shapPath[2].y : shape.shapPath[2].x} + PathCurve { x: root.isHorizontal ? shape.shapPath[3].x : shape.shapPath[3].y; y: root.isHorizontal ? shape.shapPath[3].y : shape.shapPath[3].x} + PathCurve { x: root.isHorizontal ? shape.shapPath[4].x : shape.shapPath[4].y; y: root.isHorizontal ? shape.shapPath[4].y : shape.shapPath[4].x} + PathCurve { x: root.isHorizontal ? shape.shapPath[5].x : shape.shapPath[5].y; y: root.isHorizontal ? shape.shapPath[5].y : shape.shapPath[5].x} + PathCurve { x: root.isHorizontal ? shape.shapPath[6].x : shape.shapPath[6].y; y: root.isHorizontal ? shape.shapPath[6].y : shape.shapPath[6].x} + } + } +} diff --git a/components/mobileshell/shellutil.cpp b/components/mobileshell/shellutil.cpp index 9ebd96f3..000e4050 100644 --- a/components/mobileshell/shellutil.cpp +++ b/components/mobileshell/shellutil.cpp @@ -24,6 +24,7 @@ #include #include +#include #define FORMAT24H "HH:mm:ss" @@ -105,6 +106,13 @@ void ShellUtil::setInputTransparent(QQuickWindow *window, bool transparent) { } } +void ShellUtil::setWindowLayer(QQuickWindow *window, LayerShellQt::Window::Layer layer) { + if (window) { + auto layerShellWindow = LayerShellQt::Window::get(window); + layerShellWindow->setLayer(layer); + } +} + void ShellUtil::setInputRegion(QWindow *window, const QRect ®ion) { auto waylandWindow = dynamic_cast(window->handle()); if (!waylandWindow) { diff --git a/components/mobileshell/shellutil.h b/components/mobileshell/shellutil.h index 6b61745c..2b6f56a1 100644 --- a/components/mobileshell/shellutil.h +++ b/components/mobileshell/shellutil.h @@ -15,6 +15,7 @@ #include #include #include +#include /** * Miscellaneous class to put utility functions used in the shell. @@ -71,6 +72,11 @@ public: */ Q_INVOKABLE void setInputTransparent(QQuickWindow *window, bool transparent); + /** + * Set the window layer + */ + Q_INVOKABLE void setWindowLayer(QQuickWindow *window, LayerShellQt::Window::Layer layer); + /** * Sets a region where inputs will get registered on a window. * Inputs outside the region will pass through to the surface below. diff --git a/components/mobileshellstate/shelldbusclient.cpp b/components/mobileshellstate/shelldbusclient.cpp index d672e39c..2f33f5d8 100644 --- a/components/mobileshellstate/shelldbusclient.cpp +++ b/components/mobileshellstate/shelldbusclient.cpp @@ -29,6 +29,7 @@ ShellDBusClient::ShellDBusClient(QObject *parent) void ShellDBusClient::connectSignals() { + connect(m_interface, &OrgKdePlasmashellInterface::panelStateChanged, this, &ShellDBusClient::updatePanelState); connect(m_interface, &OrgKdePlasmashellInterface::isActionDrawerOpenChanged, this, &ShellDBusClient::updateIsActionDrawerOpen); connect(m_interface, &OrgKdePlasmashellInterface::doNotDisturbChanged, this, &ShellDBusClient::updateDoNotDisturb); connect(m_interface, &OrgKdePlasmashellInterface::isTaskSwitcherVisibleChanged, this, &ShellDBusClient::updateIsTaskSwitcherVisible); @@ -47,6 +48,16 @@ void ShellDBusClient::connectSignals() updateIsTaskSwitcherVisible(); } +QString ShellDBusClient::panelState() const +{ + return m_panelState; +} + +void ShellDBusClient::setPanelState(QString state) +{ + m_interface->setPanelState(state); +} + bool ShellDBusClient::doNotDisturb() const { return m_doNotDisturb; @@ -113,6 +124,18 @@ void ShellDBusClient::showVolumeOSD() m_interface->showVolumeOSD(); } +void ShellDBusClient::updatePanelState() +{ + auto reply = m_interface->panelState(); + auto watcher = new QDBusPendingCallWatcher(reply, this); + + QObject::connect(watcher, &QDBusPendingCallWatcher::finished, this, [this](auto watcher) { + QDBusPendingReply reply = *watcher; + m_panelState = reply.argumentAt<0>(); + Q_EMIT panelStateChanged(); + }); +} + void ShellDBusClient::updateDoNotDisturb() { auto reply = m_interface->doNotDisturb(); diff --git a/components/mobileshellstate/shelldbusclient.h b/components/mobileshellstate/shelldbusclient.h index 8daa668d..32906141 100644 --- a/components/mobileshellstate/shelldbusclient.h +++ b/components/mobileshellstate/shelldbusclient.h @@ -19,6 +19,7 @@ class ShellDBusClient : public QObject Q_PROPERTY(bool doNotDisturb READ doNotDisturb WRITE setDoNotDisturb NOTIFY doNotDisturbChanged) Q_PROPERTY(bool isActionDrawerOpen READ isActionDrawerOpen WRITE setIsActionDrawerOpen NOTIFY isActionDrawerOpenChanged) Q_PROPERTY(bool isTaskSwitcherVisible READ isTaskSwitcherVisible NOTIFY isTaskSwitcherVisibleChanged) + Q_PROPERTY(QString panelState READ panelState WRITE setPanelState NOTIFY panelStateChanged) public: explicit ShellDBusClient(QObject *parent = nullptr); @@ -31,6 +32,10 @@ public: bool isTaskSwitcherVisible() const; + QString panelState() const; + void setPanelState(QString state); + + Q_INVOKABLE void openActionDrawer(); Q_INVOKABLE void closeActionDrawer(); @@ -43,6 +48,7 @@ public: Q_INVOKABLE void showVolumeOSD(); Q_SIGNALS: + void panelStateChanged(); void isActionDrawerOpenChanged(); void doNotDisturbChanged(); void isTaskSwitcherVisibleChanged(); @@ -57,6 +63,7 @@ private Q_SLOTS: void updateDoNotDisturb(); void updateIsActionDrawerOpen(); void updateIsTaskSwitcherVisible(); + void updatePanelState(); private: void connectSignals(); @@ -64,6 +71,8 @@ private: OrgKdePlasmashellInterface *m_interface; QDBusServiceWatcher *m_watcher; + QString m_panelState = "default"; + bool m_doNotDisturb = false; bool m_isActionDrawerOpen = false; bool m_isTaskSwitcherVisible = false; diff --git a/components/mobileshellstate/shelldbusobject.cpp b/components/mobileshellstate/shelldbusobject.cpp index ec1be630..8df038db 100644 --- a/components/mobileshellstate/shelldbusobject.cpp +++ b/components/mobileshellstate/shelldbusobject.cpp @@ -39,6 +39,20 @@ void ShellDBusObject::setDoNotDisturb(bool value) } } +QString ShellDBusObject::panelState() +{ + return m_panelState; +} + +void ShellDBusObject::setPanelState(QString state) +{ + if (state != m_panelState) { + m_panelState = state; + Q_EMIT panelStateChanged(); + } +} + + bool ShellDBusObject::isActionDrawerOpen() { return m_isActionDrawerOpen; diff --git a/components/mobileshellstate/shelldbusobject.h b/components/mobileshellstate/shelldbusobject.h index f51185f7..7f8cf7e0 100644 --- a/components/mobileshellstate/shelldbusobject.h +++ b/components/mobileshellstate/shelldbusobject.h @@ -28,6 +28,7 @@ public: Q_SIGNALS: Q_SCRIPTABLE void doNotDisturbChanged(); Q_SCRIPTABLE void isActionDrawerOpenChanged(); + Q_SCRIPTABLE void panelStateChanged(); Q_SCRIPTABLE void isTaskSwitcherVisibleChanged(); Q_SCRIPTABLE void openActionDrawerRequested(); Q_SCRIPTABLE void closeActionDrawerRequested(); @@ -44,6 +45,10 @@ public Q_SLOTS: Q_SCRIPTABLE bool isActionDrawerOpen(); Q_SCRIPTABLE void setIsActionDrawerOpen(bool value); + Q_SCRIPTABLE QString panelState(); + Q_SCRIPTABLE void setPanelState(QString state); + + Q_SCRIPTABLE bool isTaskSwitcherVisible(); Q_SCRIPTABLE void setIsTaskSwitcherVisible(bool value); @@ -65,5 +70,7 @@ private: bool m_isActionDrawerOpen{false}; bool m_isTaskSwitcherVisible{false}; + QString m_panelState{}; + StartupFeedbackModel *m_startupFeedbackModel{nullptr}; }; diff --git a/components/windowplugin/qml/WindowMaximizedTracker.qml b/components/windowplugin/qml/WindowMaximizedTracker.qml index 8737e646..75c44545 100644 --- a/components/windowplugin/qml/WindowMaximizedTracker.qml +++ b/components/windowplugin/qml/WindowMaximizedTracker.qml @@ -14,24 +14,39 @@ QtObject { // Set it to Plasmoid.containment.screenGeometry in a plasmoid to accomplish this. property alias screenGeometry: tasksModel.screenGeometry + property bool isCurrentWindowFullscreen: __internal.count > 0 && visibleWindowsModel.currentFullscreen && !WindowPlugin.WindowUtil.isShowingDesktop + readonly property bool showingWindow: __internal.count > 0 && !WindowPlugin.WindowUtil.isShowingDesktop readonly property int windowCount: __internal.count property var __internal: KItemModels.KSortFilterProxyModel { - id: visibleMaximizedWindowsModel - filterRoleName: 'IsMinimized' - filterString: 'false' - sourceModel: TaskManager.TasksModel { + id: visibleWindowsModel + sourceModel: taskModel + filterRowCallback: (sourceRow, sourceParent) => { + const task = sourceModel.index(sourceRow, 0, sourceParent); + let isFullScreen = sourceModel.data(task, TaskManager.AbstractTasksModel.IsFullScreen); + let isMaximized = sourceModel.data(task, TaskManager.AbstractTasksModel.IsMaximized); + if (sourceRow == 0) { + visibleWindowsModel.currentFullscreen = isFullScreen; + } + return isFullScreen || isMaximized; + } + + property bool currentFullscreen: false + + property var taskModel: TaskManager.TasksModel { id: tasksModel filterByVirtualDesktop: true filterByActivity: true - filterNotMaximized: true + filterMinimized: true filterByScreen: true filterHidden: true virtualDesktop: virtualDesktopInfo.currentDesktop activity: activityInfo.currentActivity + sortMode: TaskManager.TasksModel.SortLastActivated + groupMode: TaskManager.TasksModel.GroupDisabled } diff --git a/containments/panel/package/contents/ui/main.qml b/containments/panel/package/contents/ui/main.qml index 5897251c..c4452a27 100644 --- a/containments/panel/package/contents/ui/main.qml +++ b/containments/panel/package/contents/ui/main.qml @@ -19,34 +19,70 @@ import org.kde.plasma.private.mobileshell.windowplugin as WindowPlugin import org.kde.taskmanager as TaskManager import org.kde.notificationmanager as NotificationManager +import org.kde.layershell 1.0 as LayerShell ContainmentItem { id: root Plasmoid.backgroundHints: PlasmaCore.Types.NoBackground Plasmoid.status: PlasmaCore.Types.PassiveStatus // ensure that the panel never takes focus away from the running app - onWidthChanged: maximizeTimer.restart() - // filled in by the shell (Panel.qml) with the plasma-workspace PanelView property var panel: null - onPanelChanged: { - if (panel) { - panel.floating = false; - } + onPanelChanged: setWindowProperties() + + MobileShell.HapticsEffect { + id: haptics } - // Ensure that panel is always the full width of the screen + readonly property real statusPanelHeight: MobileShell.Constants.topPanelHeight + readonly property real intendedWindowThickness: statusPanelHeight + + // use a timer so we don't have to maximize for every single pixel + // - improves performance if the shell is run in a window, and can be resized Timer { id: maximizeTimer running: false interval: 100 - onTriggered: root.panel.maximize() + onTriggered: root.panel.maximize() } + function setWindowProperties() { + if (root.panel) { + root.panel.floating = false; + root.panel.maximize(); // maximize first, then we can apply offsets (otherwise they are overridden) + root.panel.thickness = statusPanelHeight; + MobileShell.ShellUtil.setWindowLayer(root.panel, LayerShell.Window.LayerOverlay) + root.updateTouchArea(); + } + } + + // update the touch area when hidden to minimize the space the panel takes for touch input + function updateTouchArea() { + const hiddenTouchAreaThickness = Kirigami.Units.gridUnit; + + if (statusPanel.state == "hidden") { + MobileShell.ShellUtil.setInputRegion(root.panel, Qt.rect(0, 0, root.panel.width, hiddenTouchAreaThickness)); + } else { + MobileShell.ShellUtil.setInputRegion(root.panel, Qt.rect(0, 0, 0, 0)); + } + } + + + Binding { + target: MobileShellState.ShellDBusClient + property: "isActionDrawerOpen" + value: drawer.visible + } + + // only opaque if there are no maximized windows on this screen readonly property bool showingStartupFeedback: MobileShellState.ShellDBusObject.startupFeedbackModel.activeWindowIsStartupFeedback && windowMaximizedTracker.windowCount === 1 readonly property bool showingApp: windowMaximizedTracker.showingWindow && !showingStartupFeedback readonly property color backgroundColor: topPanel.colorScopeColor + readonly property alias isCurrentWindowFullscreen: windowMaximizedTracker.isCurrentWindowFullscreen + onIsCurrentWindowFullscreenChanged: { + MobileShellState.ShellDBusClient.panelState = isCurrentWindowFullscreen ? "hidden" : "default"; + } WindowPlugin.WindowMaximizedTracker { id: windowMaximizedTracker @@ -89,6 +125,8 @@ ContainmentItem { //END API implementation Component.onCompleted: { + root.setWindowProperties(); + // register dbus MobileShellState.ShellDBusObject.registerObject(); @@ -108,18 +146,119 @@ ContainmentItem { fullHeight: root.height screen: Plasmoid.screen maximizedTracker: windowMaximizedTracker + + visible: !root.isCurrentWindowFullscreen } - // top panel component - MobileShell.StatusBar { - id: topPanel + Rectangle { + id: statusPanel anchors.fill: parent - Kirigami.Theme.colorSet: root.showingApp ? Kirigami.Theme.Header : Kirigami.Theme.Complementary Kirigami.Theme.inherit: false - showDropShadow: !root.showingApp - backgroundColor: !root.showingApp ? "transparent" : root.backgroundColor + color: statusPanel.state == "default" && root.showingApp ? Kirigami.Theme.backgroundColor : "transparent" + + property real offset: 0 + + // top panel component + MobileShell.StatusBar { + id: topPanel + anchors.fill: parent + + showDropShadow: !root.showingApp + backgroundColor: statusPanel.state != "default" ? Qt.rgba(Kirigami.Theme.backgroundColor.r, Kirigami.Theme.backgroundColor.g, Kirigami.Theme.backgroundColor.b, 0.95) : "transparent" + + transform: [ + Translate { + y: statusPanel.offset + } + ] + } + + state: MobileShellState.ShellDBusClient.panelState + onStateChanged: { + if (statusPanel.state != "hidden") { + root.setWindowProperties(); + hiddenTimer.restart(); + } + } + + Timer { + id: hiddenTimer + running: false + interval: 3000 + onTriggered: { + if (statusPanel.state == "visible") { + MobileShellState.ShellDBusClient.panelState = "hidden"; + } + } + } + + states: [ + State { + name: "default" + PropertyChanges { + target: statusPanel; offset: 0 + } + }, + State { + name: "visible" + PropertyChanges { + target: statusPanel; offset: 0 + } + }, + State { + name: "hidden" + PropertyChanges { + target: statusPanel; offset: -root.statusPanelHeight + } + } + ] + + transitions: Transition { + SequentialAnimation { + ParallelAnimation { + PropertyAnimation { + properties: "offset"; easing.type: statusPanel.state == "hidden" ? Easing.InExpo : Easing.OutExpo; duration: Kirigami.Units.longDuration + } + } + ScriptAction { + script: { + root.setWindowProperties(); + } + } + } + } + } + + // swiping area for swipe-down drawer + MobileShell.ActionDrawerOpenSurface { + id: swipeArea + actionDrawer: drawer.actionDrawer + anchors.fill: parent + + readonly property alias drawerVisible: drawer.visible + readonly property alias offset: drawer.actionDrawer.offset + property bool surfacePressed: false + onOffsetChanged: surfacePressed = false + + // allow tapping to bring back up the status bar when it is hidden + onPressedChanged: { + if (!pressed && surfacePressed && root.isCurrentWindowFullscreen) { + haptics.buttonVibrate(); + MobileShellState.ShellDBusClient.panelState = "visible"; + } else { + surfacePressed = true; + } + } + + // if in a fullscreen app, the panels are visible, and the action drawer is opened + // set the panels to a hidden state + onDrawerVisibleChanged: { + if (statusPanel.state == "visible") { + MobileShellState.ShellDBusClient.panelState = "hidden"; + } + } } // swipe-down drawer component diff --git a/containments/taskpanel/package/contents/ui/NavigationPanelComponent.qml b/containments/taskpanel/package/contents/ui/NavigationPanelComponent.qml index d7aff6e1..deb8c0da 100644 --- a/containments/taskpanel/package/contents/ui/NavigationPanelComponent.qml +++ b/containments/taskpanel/package/contents/ui/NavigationPanelComponent.qml @@ -20,12 +20,13 @@ import org.kde.kirigami as Kirigami MobileShell.NavigationPanel { id: root required property bool opaqueBar + required property var navbarState // background is: // - opaque if an app is shown or vkbd is shown // - translucent if the task switcher is open // - transparent if on the homescreen - backgroundColor: (Keyboards.KWinVirtualKeyboard.active || opaqueBar) ? Kirigami.Theme.backgroundColor : "transparent"; + backgroundColor: navbarState != "default" ? Qt.rgba(Kirigami.Theme.backgroundColor.r, Kirigami.Theme.backgroundColor.g, Kirigami.Theme.backgroundColor.b, 0.95) : "transparent" foregroundColorGroup: opaqueBar ? Kirigami.Theme.Window : Kirigami.Theme.Complementary shadow: !opaqueBar diff --git a/containments/taskpanel/package/contents/ui/main.qml b/containments/taskpanel/package/contents/ui/main.qml index 3d092a72..36c6f3d8 100644 --- a/containments/taskpanel/package/contents/ui/main.qml +++ b/containments/taskpanel/package/contents/ui/main.qml @@ -5,6 +5,7 @@ import QtQuick 2.4 import QtQuick.Layouts 1.1 import QtQuick.Window 2.15 +import QtQuick.Shapes 1.8 import org.kde.kirigami 2.20 as Kirigami @@ -17,6 +18,8 @@ import org.kde.plasma.private.mobileshell as MobileShell import org.kde.plasma.private.mobileshell.shellsettingsplugin as ShellSettings import org.kde.plasma.private.mobileshell.windowplugin as WindowPlugin import org.kde.plasma.private.mobileshell.state as MobileShellState +import org.kde.plasma.workspace.keyboardlayout as Keyboards +import org.kde.layershell 1.0 as LayerShell ContainmentItem { id: root @@ -37,6 +40,10 @@ ContainmentItem { } } + MobileShell.HapticsEffect { + id: haptics + } + readonly property bool inLandscape: MobileShell.Constants.navigationPanelOnSide(Screen.width, Screen.height) readonly property real navigationPanelHeight: MobileShell.Constants.navigationPanelThickness @@ -73,7 +80,7 @@ ContainmentItem { interval: 100 onTriggered: { // maximize first, then we can apply offsets (otherwise they are overridden) - root.panel.maximize() + root.panel.maximize(); root.panel.offset = intendedWindowOffset; } } @@ -86,6 +93,23 @@ ContainmentItem { root.panel.offset = intendedWindowOffset; root.panel.thickness = navigationPanelHeight; root.panel.location = intendedWindowLocation; + MobileShell.ShellUtil.setWindowLayer(root.panel, LayerShell.Window.LayerOverlay); + root.updateTouchArea(); + } + } + + // update the touch area when hidden to minimize the space the panel takes for touch input + function updateTouchArea() { + const hiddenTouchAreaThickness = Kirigami.Units.gridUnit; + + if (navigationPanel.state == "hidden") { + if (inLandscape) { + MobileShell.ShellUtil.setInputRegion(root.panel, Qt.rect(root.panel.width - hiddenTouchAreaThickness, 0, hiddenTouchAreaThickness, root.panel.height)); + } else { + MobileShell.ShellUtil.setInputRegion(root.panel, Qt.rect(0, root.panel.height - hiddenTouchAreaThickness, root.panel.width, hiddenTouchAreaThickness)); + } + } else { + MobileShell.ShellUtil.setInputRegion(root.panel, Qt.rect(0, 0, 0, 0)); } } @@ -112,7 +136,9 @@ ContainmentItem { // only opaque if there are no maximized windows on this screen readonly property bool showingStartupFeedback: MobileShellState.ShellDBusObject.startupFeedbackModel.activeWindowIsStartupFeedback && windowMaximizedTracker.windowCount === 1 - readonly property bool opaqueBar: windowMaximizedTracker.showingWindow && !showingStartupFeedback + readonly property bool opaqueBar: (windowMaximizedTracker.showingWindow || isCurrentWindowFullscreen) && !showingStartupFeedback + + readonly property alias isCurrentWindowFullscreen: windowMaximizedTracker.isCurrentWindowFullscreen WindowPlugin.WindowMaximizedTracker { id: windowMaximizedTracker @@ -128,21 +154,161 @@ ContainmentItem { fullHeight: root.height screen: Plasmoid.screen maximizedTracker: windowMaximizedTracker + + visible: !root.isCurrentWindowFullscreen } - Item { + Rectangle { + id: navigationPanel anchors.fill: parent - // contrasting colour - Kirigami.Theme.colorSet: opaqueBar ? Kirigami.Theme.Window : Kirigami.Theme.Complementary + Kirigami.Theme.colorSet: root.opaqueBar ? Kirigami.Theme.Window : Kirigami.Theme.Complementary Kirigami.Theme.inherit: false + color: navigationPanel.state == "default" && (Keyboards.KWinVirtualKeyboard.active || root.opaqueBar) ? Kirigami.Theme.backgroundColor : "transparent" + + property real offset: 0 + // load appropriate system navigation component NavigationPanelComponent { - id: navigationPanel anchors.fill: parent opaqueBar: root.opaqueBar isVertical: root.inLandscape + navbarState: navigationPanel.state + + transform: [ + Translate { + y: inLandscape ? 0 : navigationPanel.offset + x: inLandscape ? navigationPanel.offset : 0 + } + ] + } + + state: MobileShellState.ShellDBusClient.panelState + onStateChanged: { + if (navigationPanel.state != "hidden") { + root.setWindowProperties(); + } + } + + states: [ + State { + name: "default" + PropertyChanges { + target: navigationPanel; offset: 0 + } + }, + State { + name: "visible" + PropertyChanges { + target: navigationPanel; offset: 0 + } + }, + State { + name: "hidden" + PropertyChanges { + target: navigationPanel; offset: root.navigationPanelHeight + } + } + ] + + transitions: Transition { + SequentialAnimation { + ParallelAnimation { + PropertyAnimation { + properties: "offset"; easing.type: navigationPanel.state == "hidden" ? Easing.InExpo : Easing.OutExpo; duration: Kirigami.Units.longDuration + } + } + ScriptAction { + script: { + root.setWindowProperties(); + } + } + } + } + } + + MobileShell.SwipeArea { + id: swipeArea + mode: inLandscape ? MobileShell.SwipeArea.HorizontalOnly : MobileShell.SwipeArea.VerticalOnly + anchors.fill: navigationPanel + enabled: navigationPanel.state == "hidden" + + function startSwipeWithPoint(point) { + root.setWindowProperties(); + resetAn.stop(); + dragEffect.startPoint = inLandscape ? point.y - Screen.height / 2 : point.x - Screen.width / 2; + dragEffect.sidePoint = 0 + dragEffect.offsetPoint = 0; + } + + function updateOffset(offsetX, offsetY) { + dragEffect.sidePoint = inLandscape ? offsetY : offsetX; + dragEffect.offsetPoint = Math.min(0, inLandscape ? offsetX : offsetY); + if (dragEffect.offsetPoint < -Kirigami.Units.gridUnit * 5 && navigationPanel.state == "hidden") { + swipeArea.resetSwipe(); + resetAn.restart(); + haptics.buttonVibrate(); + MobileShellState.ShellDBusClient.panelState = "visible"; + } + } + + onSwipeStarted: (point) => startSwipeWithPoint(point) + onSwipeEnded: resetAn.start() + onSwipeMove: (totalDeltaX, totalDeltaY, deltaX, deltaY) => updateOffset(totalDeltaX, totalDeltaY); + + onPressedChanged: { + if (!pressed && dragEffect.offsetPoint == 0) { + haptics.buttonVibrate(); + MobileShellState.ShellDBusClient.panelState = "visible"; + } + } + + NumberAnimation { + id: resetAn + running: false + target: dragEffect + property: "offsetPoint" + to: 0 + duration: Kirigami.Units.longDuration * 1.5 + easing.type: Easing.OutExpo + onRunningChanged: { + if (!running && navigationPanel.state == "hidden") { + root.setWindowProperties(); + } + } + } + + MobileShell.ScreenEdgeDragEffect { + id: dragEffect + + offsetLimit: root.inLandscape ? swipeArea.width : swipeArea.height + isHorizontal: root.inLandscape + + states: [ + State { + name: "vertical" + when: !root.inLandscape + AnchorChanges { + target: dragEffect + anchors.right: undefined + anchors.bottom: swipeArea.bottom + anchors.horizontalCenter: swipeArea.horizontalCenter + anchors.verticalCenter: undefined + } + }, + State { + name: "horizontal" + when: root.inLandscape + AnchorChanges { + target: dragEffect + anchors.right: swipeArea.right + anchors.bottom: undefined + anchors.horizontalCenter: undefined + anchors.verticalCenter: swipeArea.verticalCenter + } + } + ] } } }