Compare commits

...

5 commits

Author SHA1 Message Date
e5ec88a12b Improve auto-hide dock reveal
Expand the dock input region as soon as the reveal animation starts
rather than waiting for it to finish, so the cursor can reach dock
items while the panel is still sliding into view.

Introduce updateInputRegion() and call it from onDockOffsetChanged,
onShouldHideChanged, onActiveChanged, onWidthChanged, and onHeightChanged
so the input region is always consistent with the current state.

While hover-revealing with a maximised window present, switch the
LayerShell exclusion zone from -1 to dockHeight so the window tiles
away from the dock instead of being obscured underneath it.

Reduce the hover dwell timer from a hard-coded 300 ms to
Kirigami.Units.shortDuration so the dock is easier to summon.
2026-05-06 13:33:38 +02:00
daf6ec3fd6 Smooth convergence mode transitions
Animate dock item sizing and button opacity when toggling convergence
mode so the dock does not snap abruptly between states.

FavouritesBar: promote cell/icon/nav/pager/trash sizing properties to
writable and attach NumberAnimation Behaviors (InOutCubic, longDuration).
Home, Overview, and trash buttons fade in/out with an InOutQuad opacity
Behavior so they appear gradually rather than popping.

Dock overlay (main.qml): introduce an `active` guard property so the
window can be driven by opacity (InOutQuad, shortDuration) rather than
toggling visibility directly. Switch the dockOffset slide easing from
a directional InExpo/OutExpo pair to a single InOutCubic so the motion
feels symmetrical.

StatusBar: smooth the background colour and convergence affordance
transitions.
2026-05-06 13:33:25 +02:00
3fba9798e4 Add snap layouts shell setting
Expose snapLayoutsEnabled through MobileShellSettings and store it\nin plasmamobilerc.\n\nUse the setting in the Shift snap assist effect eligibility logic\nand add a Convergence KCM switch to control it.\n\nKeep the switch disabled when convergence is off, gaming mode is on,\nor dynamic tiling is enabled so window-placement ownership stays\nconsistent.
2026-05-06 11:44:54 +02:00
8d59ce6153 Add Convergence section to shell KCM
Group convergence controls in one place by adding Convergence Mode, Dynamic Tiling, and Auto Hide Panels to the Shell settings page. Move Auto Hide Panels out of General and keep Dynamic Tiling disabled when convergence is off or gaming mode is active.
2026-05-06 10:02:43 +02:00
4282b30a30 Refine convergence dock task menu
Show only task actions that make sense for the current window and shell mode. Hide free-geometry actions while dynamic tiling owns window placement, collapse hidden menu rows, and filter virtual desktop destinations.
2026-05-06 09:37:14 +02:00
7 changed files with 290 additions and 47 deletions

View file

@ -36,6 +36,10 @@ Item {
*/ */
property color backgroundColor: "transparent" property color backgroundColor: "transparent"
Behavior on backgroundColor {
ColorAnimation { duration: Kirigami.Units.shortDuration; easing.type: Easing.InOutQuad }
}
/** /**
* Whether to show a second row of the status bar, with more information. * Whether to show a second row of the status bar, with more information.
*/ */
@ -93,7 +97,12 @@ Item {
Rectangle { Rectangle {
anchors.fill: parent anchors.fill: parent
color: Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g, Kirigami.Theme.textColor.b, 0.1) color: Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g, Kirigami.Theme.textColor.b, 0.1)
visible: ShellSettings.Settings.convergenceModeEnabled && statusBarHover.hovered visible: opacity > 0
opacity: ShellSettings.Settings.convergenceModeEnabled && statusBarHover.hovered ? 1 : 0
Behavior on opacity {
NumberAnimation { duration: Kirigami.Units.shortDuration; easing.type: Easing.InOutQuad }
}
} }
HoverHandler { HoverHandler {
@ -223,8 +232,8 @@ Item {
anchors.bottom: parent.bottom anchors.bottom: parent.bottom
anchors.bottomMargin: Kirigami.Units.smallSpacing anchors.bottomMargin: Kirigami.Units.smallSpacing
visible: ShellSettings.Settings.convergenceModeEnabled visible: ShellSettings.Settings.convergenceModeEnabled || opacity > 0
opacity: statusBarHover.hovered ? 0.6 : 0.2 opacity: ShellSettings.Settings.convergenceModeEnabled ? (statusBarHover.hovered ? 0.6 : 0.2) : 0
Behavior on opacity { Behavior on opacity {
NumberAnimation { duration: Kirigami.Units.shortDuration } NumberAnimation { duration: Kirigami.Units.shortDuration }

View file

@ -47,6 +47,7 @@ MobileShellSettings::MobileShellSettings(QObject *parent)
Q_EMIT gamingModeEnabledChanged(); Q_EMIT gamingModeEnabledChanged();
Q_EMIT gamingDismissHintEnabledChanged(); Q_EMIT gamingDismissHintEnabledChanged();
Q_EMIT dynamicTilingEnabledChanged(); Q_EMIT dynamicTilingEnabledChanged();
Q_EMIT snapLayoutsEnabledChanged();
Q_EMIT allowLogoutChanged(); Q_EMIT allowLogoutChanged();
} }
if (group.name() == LOCKSCREEN_CONFIG_GROUP) { if (group.name() == LOCKSCREEN_CONFIG_GROUP) {
@ -290,6 +291,19 @@ void MobileShellSettings::setDynamicTilingEnabled(bool enabled)
m_config->sync(); m_config->sync();
} }
bool MobileShellSettings::snapLayoutsEnabled() const
{
auto group = KConfigGroup{m_config, GENERAL_CONFIG_GROUP};
return group.readEntry("snapLayoutsEnabled", true);
}
void MobileShellSettings::setSnapLayoutsEnabled(bool enabled)
{
auto group = KConfigGroup{m_config, GENERAL_CONFIG_GROUP};
group.writeEntry("snapLayoutsEnabled", enabled, KConfigGroup::Notify);
m_config->sync();
}
void MobileShellSettings::updateNavigationBarsInPlasma() void MobileShellSettings::updateNavigationBarsInPlasma()
{ {
// Do not update panels when not in Plasma Mobile // Do not update panels when not in Plasma Mobile

View file

@ -60,6 +60,9 @@ class MobileShellSettings : public QObject
// When false, KWin's native quick-tile behaviour is used unmodified. // When false, KWin's native quick-tile behaviour is used unmodified.
Q_PROPERTY(bool dynamicTilingEnabled READ dynamicTilingEnabled WRITE setDynamicTilingEnabled NOTIFY dynamicTilingEnabledChanged) Q_PROPERTY(bool dynamicTilingEnabled READ dynamicTilingEnabled WRITE setDynamicTilingEnabled NOTIFY dynamicTilingEnabledChanged)
// Snap layout picker — only meaningful in convergence mode when dynamic tiling is off.
Q_PROPERTY(bool snapLayoutsEnabled READ snapLayoutsEnabled WRITE setSnapLayoutsEnabled NOTIFY snapLayoutsEnabledChanged)
// logout dialog // logout dialog
Q_PROPERTY(bool allowLogout READ allowLogout READ allowLogout NOTIFY allowLogoutChanged) Q_PROPERTY(bool allowLogout READ allowLogout READ allowLogout NOTIFY allowLogoutChanged)
@ -286,6 +289,14 @@ public:
bool dynamicTilingEnabled() const; bool dynamicTilingEnabled() const;
void setDynamicTilingEnabled(bool enabled); void setDynamicTilingEnabled(bool enabled);
/**
* Whether the SHIFT snap layout picker is enabled.
* Defaults to true; only takes effect in convergence mode when gaming mode
* and dynamic tiling are off.
*/
bool snapLayoutsEnabled() const;
void setSnapLayoutsEnabled(bool enabled);
/** /**
* Whether logout button is shown in the logout/shutdown dialog. * Whether logout button is shown in the logout/shutdown dialog.
*/ */
@ -335,6 +346,7 @@ Q_SIGNALS:
void gamingModeEnabledChanged(); void gamingModeEnabledChanged();
void gamingDismissHintEnabledChanged(); void gamingDismissHintEnabledChanged();
void dynamicTilingEnabledChanged(); void dynamicTilingEnabledChanged();
void snapLayoutsEnabledChanged();
void allowLogoutChanged(); void allowLogoutChanged();
void lockscreenLeftButtonActionChanged(); void lockscreenLeftButtonActionChanged();
void lockscreenRightButtonActionChanged(); void lockscreenRightButtonActionChanged();

View file

@ -36,13 +36,18 @@ MouseArea {
readonly property int totalItemCount: repeater.count + (showRunningTasks ? taskRepeater.count : 0) readonly property int totalItemCount: repeater.count + (showRunningTasks ? taskRepeater.count : 0)
// In convergence mode, size icons to fit the dock bar instead of using page grid cells // In convergence mode, size icons to fit the dock bar instead of using page grid cells
readonly property real dockCellWidth: convergenceMode ? root.height : folio.HomeScreenState.pageCellWidth property real dockCellWidth: convergenceMode ? root.height : folio.HomeScreenState.pageCellWidth
readonly property real dockCellHeight: convergenceMode ? root.height : folio.HomeScreenState.pageCellHeight property real dockCellHeight: convergenceMode ? root.height : folio.HomeScreenState.pageCellHeight
Behavior on dockCellWidth { NumberAnimation { duration: Kirigami.Units.longDuration; easing.type: Easing.InOutCubic } }
Behavior on dockCellHeight { NumberAnimation { duration: Kirigami.Units.longDuration; easing.type: Easing.InOutCubic } }
// Navigation buttons width (used to offset center positioning) // Navigation buttons width (used to offset center positioning)
readonly property real navButtonWidth: convergenceMode ? root.height : 0 property real navButtonWidth: convergenceMode ? root.height : 0
readonly property real dockItemInset: convergenceMode ? Math.max(2, Kirigami.Units.smallSpacing / 2) : 0 property real dockItemInset: convergenceMode ? Math.max(2, Kirigami.Units.smallSpacing / 2) : 0
readonly property real dockIconSize: Math.min(root.height * 0.56, Kirigami.Units.iconSizes.large) property real dockIconSize: Math.min(root.height * 0.56, Kirigami.Units.iconSizes.large)
Behavior on navButtonWidth { NumberAnimation { duration: Kirigami.Units.longDuration; easing.type: Easing.InOutCubic } }
Behavior on dockItemInset { NumberAnimation { duration: Kirigami.Units.longDuration; easing.type: Easing.InOutCubic } }
Behavior on dockIconSize { NumberAnimation { duration: Kirigami.Units.longDuration; easing.type: Easing.InOutCubic } }
function dockItemColor(pressed, hovered, active) { function dockItemColor(pressed, hovered, active) {
if (pressed) { if (pressed) {
@ -90,10 +95,12 @@ MouseArea {
// Virtual desktop pager (convergence mode, 2+ desktops) // Virtual desktop pager (convergence mode, 2+ desktops)
readonly property bool showPager: convergenceMode && virtualDesktopInfo.numberOfDesktops > 1 readonly property bool showPager: convergenceMode && virtualDesktopInfo.numberOfDesktops > 1
readonly property real pagerButtonWidth: showPager ? Math.min(root.height, Kirigami.Units.gridUnit * 2.5) : 0 property real pagerButtonWidth: showPager ? Math.min(root.height, Kirigami.Units.gridUnit * 2.5) : 0
readonly property int pagerLeftCount: showPager ? Math.ceil(virtualDesktopInfo.numberOfDesktops / 2) : 0 readonly property int pagerLeftCount: showPager ? Math.ceil(virtualDesktopInfo.numberOfDesktops / 2) : 0
readonly property int pagerRightCount: showPager ? virtualDesktopInfo.numberOfDesktops - pagerLeftCount : 0 readonly property int pagerRightCount: showPager ? virtualDesktopInfo.numberOfDesktops - pagerLeftCount : 0
readonly property real trashButtonWidth: convergenceMode ? root.height : 0 property real trashButtonWidth: convergenceMode ? root.height : 0
Behavior on pagerButtonWidth { NumberAnimation { duration: Kirigami.Units.longDuration; easing.type: Easing.InOutCubic } }
Behavior on trashButtonWidth { NumberAnimation { duration: Kirigami.Units.longDuration; easing.type: Easing.InOutCubic } }
function pagerDesktopName(index) { function pagerDesktopName(index) {
let names = virtualDesktopInfo.desktopNames let names = virtualDesktopInfo.desktopNames
@ -102,6 +109,34 @@ MouseArea {
return i18n("Desktop %1", index + 1) return i18n("Desktop %1", index + 1)
} }
function pagerDesktopNameForId(desktopId) {
let ids = virtualDesktopInfo.desktopIds
if (!ids) {
return ""
}
for (let i = 0; i < ids.length; ++i) {
if (String(ids[i]) === String(desktopId)) {
return root.pagerDesktopName(i)
}
}
return ""
}
function menuDesktopIds(isOnAllDesktops) {
let ids = virtualDesktopInfo.desktopIds
if (!ids || ids.length <= 1) {
return []
}
let result = []
for (let i = 0; i < ids.length; ++i) {
if (isOnAllDesktops || String(ids[i]) !== String(virtualDesktopInfo.currentDesktop)) {
result.push(ids[i])
}
}
return result
}
// Returns the desktop ID of the pager button under screen-space x, or "" // Returns the desktop ID of the pager button under screen-space x, or ""
function pagerButtonDesktopAt(x) { function pagerButtonDesktopAt(x) {
if (!showPager) return "" if (!showPager) return ""
@ -176,7 +211,9 @@ MouseArea {
// Home button (convergence mode, left end) // Home button (convergence mode, left end)
Rectangle { Rectangle {
id: homeButton id: homeButton
visible: root.convergenceMode visible: root.convergenceMode || opacity > 0
enabled: root.convergenceMode
opacity: root.convergenceMode ? 1 : 0
activeFocusOnTab: root.convergenceMode activeFocusOnTab: root.convergenceMode
anchors.left: parent.left anchors.left: parent.left
anchors.top: parent.top anchors.top: parent.top
@ -184,6 +221,10 @@ MouseArea {
width: root.navButtonWidth width: root.navButtonWidth
color: "transparent" color: "transparent"
Behavior on opacity {
NumberAnimation { duration: Kirigami.Units.shortDuration; easing.type: Easing.InOutQuad }
}
Accessible.role: Accessible.Button Accessible.role: Accessible.Button
Accessible.name: i18n("Home") Accessible.name: i18n("Home")
Accessible.onPressAction: MobileShellState.ShellDBusClient.openHomeScreen() Accessible.onPressAction: MobileShellState.ShellDBusClient.openHomeScreen()
@ -235,7 +276,9 @@ MouseArea {
// Overview button (convergence mode, right end) // Overview button (convergence mode, right end)
Rectangle { Rectangle {
id: overviewButton id: overviewButton
visible: root.convergenceMode visible: root.convergenceMode || opacity > 0
enabled: root.convergenceMode
opacity: root.convergenceMode ? 1 : 0
activeFocusOnTab: root.convergenceMode activeFocusOnTab: root.convergenceMode
anchors.right: parent.right anchors.right: parent.right
anchors.top: parent.top anchors.top: parent.top
@ -243,6 +286,10 @@ MouseArea {
width: root.navButtonWidth width: root.navButtonWidth
color: "transparent" color: "transparent"
Behavior on opacity {
NumberAnimation { duration: Kirigami.Units.shortDuration; easing.type: Easing.InOutQuad }
}
Accessible.role: Accessible.Button Accessible.role: Accessible.Button
Accessible.name: i18n("Overview") Accessible.name: i18n("Overview")
Accessible.onPressAction: root.folio.triggerOverview() Accessible.onPressAction: root.folio.triggerOverview()
@ -452,7 +499,9 @@ MouseArea {
Rectangle { Rectangle {
id: trashButton id: trashButton
visible: root.convergenceMode visible: root.convergenceMode || opacity > 0
enabled: root.convergenceMode
opacity: root.convergenceMode ? 1 : 0
activeFocusOnTab: root.convergenceMode activeFocusOnTab: root.convergenceMode
x: root.width - root.navButtonWidth - root.trashButtonWidth x: root.width - root.navButtonWidth - root.trashButtonWidth
y: 0 y: 0
@ -460,6 +509,10 @@ MouseArea {
height: root.height height: root.height
color: "transparent" color: "transparent"
Behavior on opacity {
NumberAnimation { duration: Kirigami.Units.shortDuration; easing.type: Easing.InOutQuad }
}
Accessible.role: Accessible.Button Accessible.role: Accessible.Button
Accessible.name: i18n("Trash") Accessible.name: i18n("Trash")
Accessible.onPressAction: Qt.openUrlExternally("trash:/") Accessible.onPressAction: Qt.openUrlExternally("trash:/")
@ -1228,6 +1281,10 @@ MouseArea {
readonly property bool isLocationBottom: folio.HomeScreenState.favouritesBarLocation === Folio.HomeScreenState.Bottom readonly property bool isLocationBottom: folio.HomeScreenState.favouritesBarLocation === Folio.HomeScreenState.Bottom
readonly property string taskStorageId: root.runningTaskStorageId(taskDelegate.model) readonly property string taskStorageId: root.runningTaskStorageId(taskDelegate.model)
readonly property bool isGroupParent: taskDelegate.model.IsGroupParent === true
readonly property bool dynamicTilingActive: root.convergenceMode && ShellSettings.Settings.dynamicTilingEnabled
readonly property bool showFreeGeometryActions: !taskDelegate.isGroupParent && !taskDelegate.dynamicTilingActive
readonly property bool canChangeVirtualDesktops: taskDelegate.model.IsVirtualDesktopsChangeable === true
Accessible.role: Accessible.Button Accessible.role: Accessible.Button
Accessible.name: taskDelegate.model.display || "" Accessible.name: taskDelegate.model.display || ""
@ -1430,52 +1487,123 @@ MouseArea {
id: taskContextMenu id: taskContextMenu
popupType: T.Popup.Window popupType: T.Popup.Window
PC3.MenuItem {
icon.name: "window-new"
text: i18n("Open New Window")
visible: taskDelegate.model.CanLaunchNewInstance === true
height: visible ? implicitHeight : 0
onClicked: tasksModel.requestNewInstance(tasksModel.makeModelIndex(taskDelegate.index))
}
PC3.MenuItem { PC3.MenuItem {
icon.name: "window-pin" icon.name: "window-pin"
text: i18n("Pin to Dock") text: i18n("Pin to Dock")
// repeater.count dependency forces re-evaluation when favourites change // repeater.count dependency forces re-evaluation when favourites change
visible: taskDelegate.taskStorageId !== "" && repeater.count >= 0 && !folio.FavouritesModel.containsApplication(taskDelegate.taskStorageId) visible: taskDelegate.taskStorageId !== "" && repeater.count >= 0 && !folio.FavouritesModel.containsApplication(taskDelegate.taskStorageId)
height: visible ? implicitHeight : 0
enabled: !folio.FolioSettings.lockLayout enabled: !folio.FolioSettings.lockLayout
onClicked: folio.FavouritesModel.addApplication(taskDelegate.taskStorageId) onClicked: folio.FavouritesModel.addApplication(taskDelegate.taskStorageId)
} }
Controls.MenuSeparator {
visible: taskDelegate.model.CanLaunchNewInstance === true
|| (taskDelegate.taskStorageId !== "" && repeater.count >= 0 && !folio.FavouritesModel.containsApplication(taskDelegate.taskStorageId))
height: visible ? implicitHeight : 0
}
PC3.MenuItem {
icon.name: "transform-move"
text: i18n("Move")
visible: taskDelegate.showFreeGeometryActions
height: visible ? implicitHeight : 0
enabled: taskDelegate.model.IsMovable === true
onClicked: tasksModel.requestMove(tasksModel.makeModelIndex(taskDelegate.index))
}
PC3.MenuItem {
icon.name: "transform-scale"
text: i18n("Resize")
visible: taskDelegate.showFreeGeometryActions
height: visible ? implicitHeight : 0
enabled: taskDelegate.model.IsResizable === true
onClicked: tasksModel.requestResize(tasksModel.makeModelIndex(taskDelegate.index))
}
PC3.MenuItem { PC3.MenuItem {
icon.name: taskDelegate.model.IsMinimized ? "window-restore" : "window-minimize" icon.name: taskDelegate.model.IsMinimized ? "window-restore" : "window-minimize"
text: taskDelegate.model.IsMinimized ? i18n("Restore") : i18n("Minimize") text: taskDelegate.model.IsMinimized ? i18n("Restore") : i18n("Minimize")
enabled: taskDelegate.model.IsMinimizable === true
onClicked: tasksModel.requestToggleMinimized(tasksModel.makeModelIndex(taskDelegate.index)) onClicked: tasksModel.requestToggleMinimized(tasksModel.makeModelIndex(taskDelegate.index))
} }
PC3.MenuItem { PC3.MenuItem {
icon.name: taskDelegate.model.IsMaximized ? "window-restore" : "window-maximize" icon.name: taskDelegate.model.IsMaximized ? "window-restore" : "window-maximize"
text: taskDelegate.model.IsMaximized ? i18n("Restore") : i18n("Maximize") text: taskDelegate.model.IsMaximized ? i18n("Restore") : i18n("Maximize")
visible: taskDelegate.model.IsGroupParent !== true visible: taskDelegate.showFreeGeometryActions
height: visible ? implicitHeight : 0
enabled: taskDelegate.model.IsMaximizable === true
onClicked: tasksModel.requestToggleMaximized(tasksModel.makeModelIndex(taskDelegate.index)) onClicked: tasksModel.requestToggleMaximized(tasksModel.makeModelIndex(taskDelegate.index))
} }
PC3.MenuItem {
icon.name: "window-keep-above"
text: taskDelegate.model.IsKeepAbove ? i18n("Do Not Keep Above Others") : i18n("Keep Above Others")
visible: taskDelegate.showFreeGeometryActions
height: visible ? implicitHeight : 0
onClicked: tasksModel.requestToggleKeepAbove(tasksModel.makeModelIndex(taskDelegate.index))
}
PC3.MenuItem {
icon.name: "window-keep-below"
text: taskDelegate.model.IsKeepBelow ? i18n("Do Not Keep Below Others") : i18n("Keep Below Others")
visible: taskDelegate.showFreeGeometryActions
height: visible ? implicitHeight : 0
onClicked: tasksModel.requestToggleKeepBelow(tasksModel.makeModelIndex(taskDelegate.index))
}
PC3.MenuItem {
icon.name: "view-fullscreen"
text: taskDelegate.model.IsFullScreen ? i18n("Leave Fullscreen") : i18n("Fullscreen")
visible: taskDelegate.showFreeGeometryActions
height: visible ? implicitHeight : 0
enabled: taskDelegate.model.IsFullScreenable === true
onClicked: tasksModel.requestToggleFullScreen(tasksModel.makeModelIndex(taskDelegate.index))
}
PC3.MenuItem { PC3.MenuItem {
icon.name: "window-close" icon.name: "window-close"
text: { text: {
var ids = taskDelegate.model.WinIdList var ids = taskDelegate.model.WinIdList
return (ids && ids.length > 1) ? i18n("Close All") : i18n("Close") return (ids && ids.length > 1) ? i18n("Close All") : i18n("Close")
} }
enabled: taskDelegate.model.IsClosable === true
onClicked: tasksModel.requestClose(tasksModel.makeModelIndex(taskDelegate.index)) onClicked: tasksModel.requestClose(tasksModel.makeModelIndex(taskDelegate.index))
} }
Controls.MenuSeparator { Controls.MenuSeparator {
visible: root.showPager && taskDelegate.model.IsVirtualDesktopsChangeable === true visible: taskDelegate.canChangeVirtualDesktops
height: visible ? implicitHeight : 0
}
PC3.MenuItem {
icon.name: "virtual-desktops"
text: taskDelegate.model.IsOnAllVirtualDesktops ? i18n("Show Only on Current Desktop") : i18n("Show on All Desktops")
visible: taskDelegate.canChangeVirtualDesktops && virtualDesktopInfo.numberOfDesktops > 1
height: visible ? implicitHeight : 0
onClicked: tasksModel.requestVirtualDesktops(tasksModel.makeModelIndex(taskDelegate.index),
taskDelegate.model.IsOnAllVirtualDesktops ? [virtualDesktopInfo.currentDesktop] : [])
} }
Instantiator { Instantiator {
model: root.showPager && taskDelegate.model.IsVirtualDesktopsChangeable === true model: root.showPager && taskDelegate.canChangeVirtualDesktops ? root.menuDesktopIds(taskDelegate.model.IsOnAllVirtualDesktops === true) : []
? virtualDesktopInfo.desktopIds : []
delegate: PC3.MenuItem { delegate: PC3.MenuItem {
required property int index
required property var modelData required property var modelData
text: i18n("Move to %1", root.pagerDesktopName(index)) text: i18n("Move to %1", root.pagerDesktopNameForId(modelData))
enabled: String(modelData) !== String(virtualDesktopInfo.currentDesktop)
onTriggered: tasksModel.requestVirtualDesktops( onTriggered: tasksModel.requestVirtualDesktops(
tasksModel.makeModelIndex(taskDelegate.index), [modelData]) tasksModel.makeModelIndex(taskDelegate.index), [modelData])
} }
onObjectAdded: (idx, obj) => taskContextMenu.insertItem(taskContextMenu.count, obj) onObjectAdded: (idx, obj) => taskContextMenu.insertItem(taskContextMenu.count, obj)
onObjectRemoved: (idx, obj) => taskContextMenu.removeItem(obj) onObjectRemoved: (idx, obj) => taskContextMenu.removeItem(obj)
} }
PC3.MenuItem {
icon.name: "list-add"
text: i18n("Move to New Desktop")
visible: taskDelegate.canChangeVirtualDesktops
height: visible ? implicitHeight : 0
onClicked: tasksModel.requestNewVirtualDesktop(tasksModel.makeModelIndex(taskDelegate.index))
}
} }
} }
} }

View file

@ -276,7 +276,10 @@ ContainmentItem {
// task panel containment; this window only provides the visible dock. // task panel containment; this window only provides the visible dock.
Window { Window {
id: dockOverlay id: dockOverlay
visible: ShellSettings.Settings.convergenceModeEnabled && !ShellSettings.Settings.gamingModeEnabled readonly property bool active: ShellSettings.Settings.convergenceModeEnabled && !ShellSettings.Settings.gamingModeEnabled
visible: active
opacity: active ? 1 : 0
color: "transparent" color: "transparent"
width: Screen.width width: Screen.width
height: MobileShell.Constants.convergenceDockHeight height: MobileShell.Constants.convergenceDockHeight
@ -284,7 +287,7 @@ ContainmentItem {
LayerShell.Window.scope: "dock-overlay" LayerShell.Window.scope: "dock-overlay"
LayerShell.Window.layer: LayerShell.Window.LayerTop LayerShell.Window.layer: LayerShell.Window.LayerTop
LayerShell.Window.anchors: LayerShell.Window.AnchorBottom | LayerShell.Window.AnchorLeft | LayerShell.Window.AnchorRight LayerShell.Window.anchors: LayerShell.Window.AnchorBottom | LayerShell.Window.AnchorLeft | LayerShell.Window.AnchorRight
LayerShell.Window.exclusionZone: -1 LayerShell.Window.exclusionZone: shouldReserveSpace ? dockHeight : -1
LayerShell.Window.keyboardInteractivity: LayerShell.Window.KeyboardInteractivityOnDemand LayerShell.Window.keyboardInteractivity: LayerShell.Window.KeyboardInteractivityOnDemand
// Auto-hide: slide dock content off-screen when a window is // Auto-hide: slide dock content off-screen when a window is
@ -301,6 +304,25 @@ ContainmentItem {
readonly property bool shouldHide: ShellSettings.Settings.autoHidePanelsEnabled readonly property bool shouldHide: ShellSettings.Settings.autoHidePanelsEnabled
&& windowMaximizedTracker.showingWindow && !hoverRevealing && windowMaximizedTracker.showingWindow && !hoverRevealing
readonly property bool shouldReserveSpace: ShellSettings.Settings.autoHidePanelsEnabled
&& windowMaximizedTracker.showingWindow && hoverRevealing
function updateInputRegion() {
if (shouldHide && dockOffset >= dockHeight) {
MobileShell.ShellUtil.setInputRegion(dockOverlay,
Qt.rect(0, dockOverlay.height - revealStripHeight,
dockOverlay.width, revealStripHeight))
} else {
MobileShell.ShellUtil.setInputRegion(dockOverlay, Qt.rect(0, 0, 0, 0))
}
}
onActiveChanged: {
hoverRevealTimer.stop()
hoverRevealing = false
dockOffset = shouldHide ? dockHeight : 0
updateInputRegion()
}
onShouldHideChanged: { onShouldHideChanged: {
if (shouldHide) { if (shouldHide) {
@ -308,26 +330,23 @@ ContainmentItem {
} else { } else {
dockOffset = 0 dockOffset = 0
} }
updateInputRegion()
} }
// Narrow the input region to a strip at the screen edge when hidden // Narrow the input region to a strip at the screen edge when hidden
// so that app controls near the bottom edge are not accidentally // so that app controls near the bottom edge are not accidentally
// intercepted. Mirrors the same pattern used by NavigationPanel. // intercepted. Mirrors the same pattern used by NavigationPanel.
onDockOffsetChanged: { onDockOffsetChanged: {
if (dockOffset >= dockHeight) { updateInputRegion()
MobileShell.ShellUtil.setInputRegion(dockOverlay,
Qt.rect(0, dockOverlay.height - revealStripHeight,
dockOverlay.width, revealStripHeight))
} else if (dockOffset === 0) {
MobileShell.ShellUtil.setInputRegion(dockOverlay, Qt.rect(0, 0, 0, 0))
}
} }
onWidthChanged: updateInputRegion()
onHeightChanged: updateInputRegion()
// Delay reveal by 300 ms so a quick edge graze does not pop the // Delay reveal briefly so a quick edge graze does not pop the
// dock up mid-interaction with the underlying application. // dock up mid-interaction with the underlying application.
Timer { Timer {
id: hoverRevealTimer id: hoverRevealTimer
interval: 300 interval: Kirigami.Units.shortDuration
repeat: false repeat: false
onTriggered: dockOverlay.hoverRevealing = true onTriggered: dockOverlay.hoverRevealing = true
} }
@ -346,11 +365,15 @@ ContainmentItem {
Behavior on dockOffset { Behavior on dockOffset {
NumberAnimation { NumberAnimation {
easing.type: dockOverlay.shouldHide ? Easing.InExpo : Easing.OutExpo easing.type: Easing.InOutCubic
duration: Kirigami.Units.longDuration duration: Kirigami.Units.longDuration
} }
} }
Behavior on opacity {
NumberAnimation { duration: Kirigami.Units.shortDuration; easing.type: Easing.InOutQuad }
}
Rectangle { Rectangle {
anchors.fill: parent anchors.fill: parent
visible: !dockOverlay.shouldHide || dockOverlay.dockOffset < dockOverlay.dockHeight visible: !dockOverlay.shouldHide || dockOverlay.dockOffset < dockOverlay.dockHeight

View file

@ -48,21 +48,7 @@ KCM.SimpleKCM {
} }
} }
FormCard.FormDelegateSeparator { above: shellVibrationsButton; below: animationsSwitch } FormCard.FormDelegateSeparator { above: animationsSwitch; below: doubleTapWakeup }
FormCard.FormSwitchDelegate {
id: autoHidePanels
text: i18n("Auto Hide Panels")
description: i18n("Auto-hide the status and navigation panels to allow applications to always be in fullscreen.")
checked: ShellSettings.Settings.autoHidePanelsEnabled
onCheckedChanged: {
if (checked != ShellSettings.Settings.autoHidePanelsEnabled) {
ShellSettings.Settings.autoHidePanelsEnabled = checked;
}
}
}
FormCard.FormDelegateSeparator { above: autoHidePanels; below: doubleTapWakeup }
FormCard.FormSwitchDelegate { FormCard.FormSwitchDelegate {
id: doubleTapWakeup id: doubleTapWakeup
@ -77,6 +63,70 @@ KCM.SimpleKCM {
} }
} }
FormCard.FormHeader {
title: i18n("Convergence")
}
FormCard.FormCard {
FormCard.FormSwitchDelegate {
id: convergenceModeSwitch
text: i18n("Convergence Mode")
description: i18n("Use desktop-style window placement, titlebar controls, Overview, and the dock.")
checked: ShellSettings.Settings.convergenceModeEnabled
onCheckedChanged: {
if (checked != ShellSettings.Settings.convergenceModeEnabled) {
ShellSettings.Settings.convergenceModeEnabled = checked;
}
}
}
FormCard.FormDelegateSeparator { above: convergenceModeSwitch; below: dynamicTilingSwitch }
FormCard.FormSwitchDelegate {
id: dynamicTilingSwitch
text: i18n("Dynamic Tiling")
description: i18n("Automatically arrange windows in convergence mode. Disabled while convergence mode is off or gaming mode is active.")
enabled: ShellSettings.Settings.convergenceModeEnabled && !ShellSettings.Settings.gamingModeEnabled
checked: ShellSettings.Settings.dynamicTilingEnabled
onCheckedChanged: {
if (checked != ShellSettings.Settings.dynamicTilingEnabled) {
ShellSettings.Settings.dynamicTilingEnabled = checked;
}
}
}
FormCard.FormDelegateSeparator { above: dynamicTilingSwitch; below: snapLayoutsSwitch }
FormCard.FormSwitchDelegate {
id: snapLayoutsSwitch
text: i18n("Snap Layouts")
description: i18n("Show the snap layout picker from the maximize button. Disabled while convergence mode is off, gaming mode is active, or dynamic tiling is enabled.")
enabled: ShellSettings.Settings.convergenceModeEnabled
&& !ShellSettings.Settings.gamingModeEnabled
&& !ShellSettings.Settings.dynamicTilingEnabled
checked: ShellSettings.Settings.snapLayoutsEnabled
onCheckedChanged: {
if (checked != ShellSettings.Settings.snapLayoutsEnabled) {
ShellSettings.Settings.snapLayoutsEnabled = checked;
}
}
}
FormCard.FormDelegateSeparator { above: snapLayoutsSwitch; below: autoHidePanels }
FormCard.FormSwitchDelegate {
id: autoHidePanels
text: i18n("Auto Hide Panels")
description: i18n("Allow maximized or fullscreen applications to reclaim panel and dock space.")
checked: ShellSettings.Settings.autoHidePanelsEnabled
onCheckedChanged: {
if (checked != ShellSettings.Settings.autoHidePanelsEnabled) {
ShellSettings.Settings.autoHidePanelsEnabled = checked;
}
}
}
}
FormCard.FormHeader { FormCard.FormHeader {
title: i18n("Status Bar") title: i18n("Status Bar")
} }

View file

@ -24,6 +24,7 @@ KWinComponents.SceneEffect {
readonly property bool snapLayoutsEligible: ShellSettings.Settings.convergenceModeEnabled readonly property bool snapLayoutsEligible: ShellSettings.Settings.convergenceModeEnabled
&& !ShellSettings.Settings.gamingModeEnabled && !ShellSettings.Settings.gamingModeEnabled
&& !ShellSettings.Settings.dynamicTilingEnabled && !ShellSettings.Settings.dynamicTilingEnabled
&& ShellSettings.Settings.snapLayoutsEnabled
readonly property int hoverBarHeight: 30 readonly property int hoverBarHeight: 30
readonly property int decorationButtonSize: 16 readonly property int decorationButtonSize: 16
readonly property int decorationButtonSpacing: 8 readonly property int decorationButtonSpacing: 8
@ -372,6 +373,12 @@ KWinComponents.SceneEffect {
effect.hideSnapLayouts(); effect.hideSnapLayouts();
} }
} }
function onSnapLayoutsEnabledChanged() {
if (!effect.snapLayoutsEligible) {
effect.hideSnapLayouts();
}
}
} }
// Gap constant (must match shift-tiling) // Gap constant (must match shift-tiling)