mirror of
https://invent.kde.org/marcoa/shift-shell.git
synced 2026-04-26 14:23:09 +00:00
Dock and space reserver hid unconditionally on maximize. Gate both on autoHidePanelsEnabled. Make the exclusive zone constant — dynamic changes on a contentless surface never get committed to the compositor.
383 lines
15 KiB
QML
383 lines
15 KiB
QML
// SPDX-FileCopyrightText: 2015 Marco Martin <mart@kde.org>
|
|
// SPDX-FileCopyrightText: 2021-2023 Devin Lin <devin@kde.org>
|
|
// 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 as Kirigami
|
|
|
|
import org.kde.taskmanager 0.1 as TaskManager
|
|
import org.kde.plasma.plasmoid 2.0
|
|
import org.kde.plasma.core as PlasmaCore
|
|
import org.kde.kquickcontrolsaddons 2.0
|
|
|
|
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
|
|
Plasmoid.backgroundHints: PlasmaCore.Types.NoBackground
|
|
Plasmoid.status: PlasmaCore.Types.PassiveStatus // ensure that the panel never takes focus away from the running app
|
|
|
|
// filled in by the shell (Panel.qml) with the plasma-workspace PanelView
|
|
property var panel: null
|
|
onPanelChanged: {
|
|
setWindowProperties()
|
|
}
|
|
|
|
MobileShell.HapticsEffect {
|
|
id: haptics
|
|
}
|
|
|
|
readonly property bool inLandscape: MobileShell.Constants.navigationPanelOnSide(Screen.width, Screen.height)
|
|
|
|
readonly property real navigationPanelHeight: MobileShell.Constants.navigationPanelThickness
|
|
onNavigationPanelHeightChanged: setWindowProperties()
|
|
|
|
readonly property real intendedWindowThickness: navigationPanelHeight
|
|
readonly property real intendedWindowLength: inLandscape ? Screen.height : Screen.width
|
|
readonly property real intendedWindowOffset: inLandscape ? MobileShell.Constants.topPanelHeight : 0; // offset for top panel
|
|
readonly property int intendedWindowLocation: inLandscape ? PlasmaCore.Types.RightEdge : PlasmaCore.Types.BottomEdge
|
|
|
|
onIntendedWindowLengthChanged: maximizeTimer.restart() // ensure it always takes up the full length of the screen
|
|
onIntendedWindowLocationChanged: setPanelLocationTimer.restart()
|
|
onIntendedWindowOffsetChanged: {
|
|
if (root.panel) {
|
|
root.panel.offset = intendedWindowOffset;
|
|
}
|
|
}
|
|
|
|
// HACK: the entire shell seems to crash sometimes if this is applied immediately after a display change (ex. screen rotation)
|
|
// see https://invent.kde.org/plasma/plasma-mobile/-/issues/321
|
|
Timer {
|
|
id: setPanelLocationTimer
|
|
running: false
|
|
interval: 100
|
|
onTriggered: {
|
|
root.panel.location = intendedWindowLocation;
|
|
}
|
|
}
|
|
|
|
// 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: {
|
|
// maximize first, then we can apply offsets (otherwise they are overridden)
|
|
root.panel.maximize();
|
|
root.panel.offset = intendedWindowOffset;
|
|
}
|
|
}
|
|
|
|
|
|
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.offset = intendedWindowOffset;
|
|
root.panel.thickness = navigationPanelHeight;
|
|
root.panel.location = intendedWindowLocation;
|
|
root.panel.visibilityMode = (ShellSettings.Settings.autoHidePanelsEnabled || ShellSettings.Settings.convergenceModeEnabled) ? 3 : 0;
|
|
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));
|
|
}
|
|
}
|
|
|
|
Connections {
|
|
target: root.panel
|
|
|
|
// HACK: There seems to be some component that overrides our initial bindings for the panel,
|
|
// which is particularly problematic on first start (since the panel is misplaced)
|
|
// - We set an event to override any attempts to override our bindings.
|
|
function onLocationChanged() {
|
|
if (root.panel.location !== root.intendedWindowLocation) {
|
|
root.setWindowProperties();
|
|
}
|
|
}
|
|
|
|
function onThicknessChanged() {
|
|
if (root.panel.thickness !== root.intendedWindowThickness) {
|
|
root.setWindowProperties();
|
|
}
|
|
}
|
|
}
|
|
|
|
Connections {
|
|
target: ShellSettings.Settings
|
|
|
|
function onAutoHidePanelsEnabledChanged() {
|
|
root.setWindowProperties();
|
|
}
|
|
|
|
function onConvergenceModeEnabledChanged() {
|
|
root.setWindowProperties();
|
|
}
|
|
}
|
|
|
|
Component.onCompleted: setWindowProperties();
|
|
|
|
// Invisible layer-shell surface that reserves screen space for the dock
|
|
// in convergence mode. Sits at LayerBottom (below windows, above the
|
|
// desktop) with an exclusive zone equal to the dock height so that KWin
|
|
// shrinks MaximizeArea accordingly. Input-transparent so clicks fall
|
|
// through to the homescreen dock underneath.
|
|
// When auto-hide is enabled and a window is maximized the surface is
|
|
// destroyed so the strut is removed and the window reclaims the full
|
|
// screen. Dynamic exclusive-zone changes alone are not enough because
|
|
// the contentless surface never repaints, so the Wayland commit that
|
|
// would apply the new zone never happens.
|
|
Window {
|
|
id: dockSpaceReserver
|
|
visible: ShellSettings.Settings.convergenceModeEnabled
|
|
&& !(ShellSettings.Settings.autoHidePanelsEnabled
|
|
&& windowMaximizedTracker.showingWindow)
|
|
color: "transparent"
|
|
flags: Qt.FramelessWindowHint | Qt.WindowTransparentForInput
|
|
// height is set by layer-shell anchoring; provide a fallback.
|
|
height: Kirigami.Units.gridUnit * 3
|
|
width: 1 // layer-shell stretches it via AnchorLeft|AnchorRight
|
|
|
|
LayerShell.Window.scope: "dock-space"
|
|
LayerShell.Window.layer: LayerShell.Window.LayerBottom
|
|
LayerShell.Window.anchors: LayerShell.Window.AnchorBottom | LayerShell.Window.AnchorLeft | LayerShell.Window.AnchorRight
|
|
LayerShell.Window.exclusionZone: Kirigami.Units.gridUnit * 3
|
|
LayerShell.Window.keyboardInteractivity: LayerShell.Window.KeyboardInteractivityNone
|
|
}
|
|
|
|
// only opaque if there are no maximized windows on this screen
|
|
readonly property bool showingStartupFeedback: MobileShellState.ShellDBusObject.startupFeedbackModel.activeWindowIsStartupFeedback && startupFeedbackColorAnimation.visible && windowMaximizedTracker.windowCount === 1
|
|
readonly property bool opaqueBar: {
|
|
if (Keyboards.KWinVirtualKeyboard.visible) {
|
|
return true;
|
|
}
|
|
return (windowMaximizedTracker.showingWindow || isCurrentWindowFullscreen) && !showingStartupFeedback
|
|
}
|
|
readonly property alias isCurrentWindowFullscreen: windowMaximizedTracker.isCurrentWindowFullscreen
|
|
readonly property bool fullscreen: isCurrentWindowFullscreen || (ShellSettings.Settings.autoHidePanelsEnabled && opaqueBar)
|
|
|
|
WindowPlugin.WindowMaximizedTracker {
|
|
id: windowMaximizedTracker
|
|
screenGeometry: Plasmoid.containment.screenGeometry
|
|
|
|
onShowingWindowChanged: {
|
|
if (windowMaximizedTracker.showingWindow && MobileShellState.ShellDBusClient.isTaskSwitcherVisible && (ShellSettings.Settings.autoHidePanelsEnabled || fullscreen)) {
|
|
navigationPanel.offset = root.navigationPanelHeight;
|
|
}
|
|
}
|
|
}
|
|
|
|
MobileShell.StartupFeedbackPanelFill {
|
|
id: startupFeedbackColorAnimation
|
|
anchors.top: parent.top
|
|
anchors.left: parent.left
|
|
anchors.right: parent.right
|
|
|
|
fullHeight: root.height
|
|
screen: Plasmoid.screen
|
|
maximizedTracker: windowMaximizedTracker
|
|
|
|
visible: !root.fullscreen
|
|
}
|
|
|
|
Item {
|
|
id: navigationPanel
|
|
anchors.fill: parent
|
|
|
|
property real offset: 0
|
|
|
|
Component {
|
|
id: navigationPanelComponent
|
|
|
|
NavigationPanelComponent {
|
|
isVertical: root.inLandscape
|
|
opaqueBar: root.opaqueBar
|
|
forcedComplementary: !opaqueBar && !startupFeedbackColorAnimation.isShowing
|
|
|
|
transform: [
|
|
Translate {
|
|
y: inLandscape ? 0 : navigationPanel.offset
|
|
x: inLandscape ? navigationPanel.offset : 0
|
|
}
|
|
]
|
|
}
|
|
}
|
|
|
|
Component {
|
|
id: gesturePanelComponent
|
|
|
|
GesturePanelComponent {
|
|
opaqueBar: root.opaqueBar
|
|
|
|
Kirigami.Theme.inherit: false
|
|
Kirigami.Theme.colorSet: (!opaqueBar && !startupFeedbackColorAnimation.isShowing) ? Kirigami.Theme.Complementary : Kirigami.Theme.Window
|
|
|
|
transform: [
|
|
Translate {
|
|
y: inLandscape ? 0 : navigationPanel.offset
|
|
x: inLandscape ? navigationPanel.offset : 0
|
|
}
|
|
]
|
|
}
|
|
}
|
|
|
|
// load appropriate system navigation component
|
|
// In convergence mode, always use the button navigation panel (gesture-only mode is hostile to mouse/keyboard)
|
|
Loader {
|
|
anchors.fill: parent
|
|
sourceComponent: (ShellSettings.Settings.navigationPanelEnabled || ShellSettings.Settings.convergenceModeEnabled) ? navigationPanelComponent : gesturePanelComponent
|
|
}
|
|
|
|
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
|
|
}
|
|
}
|
|
]
|
|
}
|
|
}
|
|
}
|