Add dynamic tiling layout menu

Expose layout requests and state through shell settings so Folio
can offer alternatives for the current tiled workspace. Keep KWin
as the owner of layout changes, snapshot request serials so requests
are not dropped, and hide the edge menu when fewer than two windows
are tiled.
This commit is contained in:
Marco Allegretti 2026-05-27 15:48:09 +02:00
parent 1702027f7e
commit 97abf33597
7 changed files with 662 additions and 2 deletions

View file

@ -49,6 +49,8 @@ MobileShellSettings::MobileShellSettings(QObject *parent)
Q_EMIT dynamicTilingEnabledChanged(); Q_EMIT dynamicTilingEnabledChanged();
Q_EMIT dynamicTilingWindowRequestChanged(); Q_EMIT dynamicTilingWindowRequestChanged();
Q_EMIT dynamicTilingWindowStateChanged(); Q_EMIT dynamicTilingWindowStateChanged();
Q_EMIT dynamicTilingLayoutRequestChanged();
Q_EMIT dynamicTilingLayoutStateChanged();
Q_EMIT snapLayoutsEnabledChanged(); Q_EMIT snapLayoutsEnabledChanged();
Q_EMIT allowLogoutChanged(); Q_EMIT allowLogoutChanged();
} }
@ -371,6 +373,69 @@ void MobileShellSettings::reportDynamicTilingWindowState(const QStringList &maxi
Q_EMIT dynamicTilingWindowStateChanged(); Q_EMIT dynamicTilingWindowStateChanged();
} }
QString MobileShellSettings::dynamicTilingLayoutRequestMode() const
{
auto group = KConfigGroup{m_config, GENERAL_CONFIG_GROUP};
return group.readEntry("dynamicTilingLayoutRequestMode", QString{});
}
int MobileShellSettings::dynamicTilingLayoutRequestSerial() const
{
auto group = KConfigGroup{m_config, GENERAL_CONFIG_GROUP};
return group.readEntry("dynamicTilingLayoutRequestSerial", 0);
}
void MobileShellSettings::requestDynamicTilingLayoutMode(const QString &mode)
{
if (mode.isEmpty()) {
return;
}
auto group = KConfigGroup{m_config, GENERAL_CONFIG_GROUP};
const int serial = group.readEntry("dynamicTilingLayoutRequestSerial", 0) + 1;
group.writeEntry("dynamicTilingLayoutRequestMode", mode, KConfigGroup::Notify);
group.writeEntry("dynamicTilingLayoutRequestSerial", serial, KConfigGroup::Notify);
m_config->sync();
Q_EMIT dynamicTilingLayoutRequestChanged();
}
QString MobileShellSettings::dynamicTilingLayoutMode() const
{
auto group = KConfigGroup{m_config, GENERAL_CONFIG_GROUP};
return group.readEntry("dynamicTilingLayoutMode", QString{});
}
int MobileShellSettings::dynamicTilingLayoutWindowCount() const
{
auto group = KConfigGroup{m_config, GENERAL_CONFIG_GROUP};
return group.readEntry("dynamicTilingLayoutWindowCount", 0);
}
int MobileShellSettings::dynamicTilingLayoutStateSerial() const
{
auto group = KConfigGroup{m_config, GENERAL_CONFIG_GROUP};
return group.readEntry("dynamicTilingLayoutStateSerial", 0);
}
void MobileShellSettings::reportDynamicTilingLayoutState(const QString &mode, int windowCount)
{
const int normalizedWindowCount = windowCount < 0 ? 0 : windowCount;
auto group = KConfigGroup{m_config, GENERAL_CONFIG_GROUP};
if (group.readEntry("dynamicTilingLayoutMode", QString{}) == mode && group.readEntry("dynamicTilingLayoutWindowCount", 0) == normalizedWindowCount) {
return;
}
const int serial = group.readEntry("dynamicTilingLayoutStateSerial", 0) + 1;
group.writeEntry("dynamicTilingLayoutMode", mode, KConfigGroup::Notify);
group.writeEntry("dynamicTilingLayoutWindowCount", normalizedWindowCount, KConfigGroup::Notify);
group.writeEntry("dynamicTilingLayoutStateSerial", serial, KConfigGroup::Notify);
m_config->sync();
Q_EMIT dynamicTilingLayoutStateChanged();
}
bool MobileShellSettings::snapLayoutsEnabled() const bool MobileShellSettings::snapLayoutsEnabled() const
{ {
auto group = KConfigGroup{m_config, GENERAL_CONFIG_GROUP}; auto group = KConfigGroup{m_config, GENERAL_CONFIG_GROUP};

View file

@ -65,6 +65,11 @@ class MobileShellSettings : public QObject
Q_PROPERTY(int dynamicTilingWindowRequestSerial READ dynamicTilingWindowRequestSerial NOTIFY dynamicTilingWindowRequestChanged) Q_PROPERTY(int dynamicTilingWindowRequestSerial READ dynamicTilingWindowRequestSerial NOTIFY dynamicTilingWindowRequestChanged)
Q_PROPERTY(QStringList dynamicTilingMaximizedWindowIds READ dynamicTilingMaximizedWindowIds NOTIFY dynamicTilingWindowStateChanged) Q_PROPERTY(QStringList dynamicTilingMaximizedWindowIds READ dynamicTilingMaximizedWindowIds NOTIFY dynamicTilingWindowStateChanged)
Q_PROPERTY(int dynamicTilingWindowStateSerial READ dynamicTilingWindowStateSerial NOTIFY dynamicTilingWindowStateChanged) Q_PROPERTY(int dynamicTilingWindowStateSerial READ dynamicTilingWindowStateSerial NOTIFY dynamicTilingWindowStateChanged)
Q_PROPERTY(QString dynamicTilingLayoutRequestMode READ dynamicTilingLayoutRequestMode NOTIFY dynamicTilingLayoutRequestChanged)
Q_PROPERTY(int dynamicTilingLayoutRequestSerial READ dynamicTilingLayoutRequestSerial NOTIFY dynamicTilingLayoutRequestChanged)
Q_PROPERTY(QString dynamicTilingLayoutMode READ dynamicTilingLayoutMode NOTIFY dynamicTilingLayoutStateChanged)
Q_PROPERTY(int dynamicTilingLayoutWindowCount READ dynamicTilingLayoutWindowCount NOTIFY dynamicTilingLayoutStateChanged)
Q_PROPERTY(int dynamicTilingLayoutStateSerial READ dynamicTilingLayoutStateSerial NOTIFY dynamicTilingLayoutStateChanged)
// Snap layout picker — only meaningful in convergence mode when dynamic tiling is off. // Snap layout picker — only meaningful in convergence mode when dynamic tiling is off.
Q_PROPERTY(bool snapLayoutsEnabled READ snapLayoutsEnabled WRITE setSnapLayoutsEnabled NOTIFY snapLayoutsEnabledChanged) Q_PROPERTY(bool snapLayoutsEnabled READ snapLayoutsEnabled WRITE setSnapLayoutsEnabled NOTIFY snapLayoutsEnabledChanged)
@ -302,6 +307,13 @@ public:
int dynamicTilingWindowStateSerial() const; int dynamicTilingWindowStateSerial() const;
Q_INVOKABLE bool isDynamicTilingWindowMaximized(const QString &windowId) const; Q_INVOKABLE bool isDynamicTilingWindowMaximized(const QString &windowId) const;
Q_INVOKABLE void reportDynamicTilingWindowState(const QStringList &maximizedWindowIds); Q_INVOKABLE void reportDynamicTilingWindowState(const QStringList &maximizedWindowIds);
QString dynamicTilingLayoutRequestMode() const;
int dynamicTilingLayoutRequestSerial() const;
Q_INVOKABLE void requestDynamicTilingLayoutMode(const QString &mode);
QString dynamicTilingLayoutMode() const;
int dynamicTilingLayoutWindowCount() const;
int dynamicTilingLayoutStateSerial() const;
Q_INVOKABLE void reportDynamicTilingLayoutState(const QString &mode, int windowCount);
/** /**
* Whether the SHIFT snap layout picker is enabled. * Whether the SHIFT snap layout picker is enabled.
@ -362,6 +374,8 @@ Q_SIGNALS:
void dynamicTilingEnabledChanged(); void dynamicTilingEnabledChanged();
void dynamicTilingWindowRequestChanged(); void dynamicTilingWindowRequestChanged();
void dynamicTilingWindowStateChanged(); void dynamicTilingWindowStateChanged();
void dynamicTilingLayoutRequestChanged();
void dynamicTilingLayoutStateChanged();
void snapLayoutsEnabledChanged(); void snapLayoutsEnabledChanged();
void allowLogoutChanged(); void allowLogoutChanged();
void lockscreenLeftButtonActionChanged(); void lockscreenLeftButtonActionChanged();

View file

@ -11,6 +11,7 @@ plasma_add_applet(org.kde.plasma.mobile.homescreen.folio
qml/CategoryPanel.qml qml/CategoryPanel.qml
qml/DelegateDragItem.qml qml/DelegateDragItem.qml
qml/DelegateDropArea.qml qml/DelegateDropArea.qml
qml/DynamicTilingLayoutMenu.qml
qml/FavouritesBar.qml qml/FavouritesBar.qml
qml/FolderView.qml qml/FolderView.qml
qml/FolderViewTitle.qml qml/FolderViewTitle.qml

View file

@ -0,0 +1,298 @@
// SPDX-FileCopyrightText: 2026 Marco Allegretti
// SPDX-License-Identifier: EUPL-1.2
import QtQuick
import QtQuick.Layouts
import QtQuick.Shapes 1.8
import org.kde.kirigami as Kirigami
import org.kde.plasma.components 3.0 as PlasmaComponents
import org.kde.plasma.private.mobileshell as MobileShell
Item {
id: root
property int windowCount: 0
property string currentMode: ""
property color surfaceColor: Kirigami.Theme.backgroundColor
property int animationDuration: Kirigami.Units.shortDuration
property real maxHeight: 0
signal layoutModeRequested(string mode)
signal dismissRequested()
readonly property int clampedWindowCount: clampedLayoutWindowCount(windowCount)
readonly property var layoutOptions: layoutOptionsForWindowCount(clampedWindowCount)
readonly property int optionCount: layoutOptions.length
readonly property real rowHeight: Math.max(Kirigami.Units.gridUnit * 2.4,
Kirigami.Units.iconSizes.medium + Kirigami.Units.smallSpacing * 2)
readonly property real naturalHeight: Kirigami.Units.gridUnit * 2.2
+ Math.max(1, optionCount) * rowHeight
+ Kirigami.Units.smallSpacing * 3
readonly property real preferredHeight: maxHeight > 0 ? Math.min(naturalHeight, maxHeight) : naturalHeight
readonly property real cornerRadius: Math.min(MobileShell.Constants.convergenceWorkspaceFrameRadius, height * 0.24)
clip: true
function clampedLayoutWindowCount(windowCount) {
const count = Math.round(Number(windowCount) || 0)
return Math.max(0, Math.min(4, count))
}
function linearLayoutZones(windowCount, orientation) {
const count = Math.max(1, clampedLayoutWindowCount(windowCount))
let zones = []
for (let i = 0; i < count; i++) {
if (orientation === "horizontal") {
zones.push({ x: 0, y: i / count, w: 1, h: 1 / count })
} else {
zones.push({ x: i / count, y: 0, w: 1 / count, h: 1 })
}
}
return zones
}
function masterLayoutZones(windowCount) {
const count = clampedLayoutWindowCount(windowCount)
if (count <= 2) {
return linearLayoutZones(Math.max(1, count), "vertical")
}
let zones = [{ x: 0, y: 0, w: 0.58, h: 1 }]
const stackCount = count - 1
for (let i = 0; i < stackCount; i++) {
zones.push({ x: 0.58, y: i / stackCount, w: 0.42, h: 1 / stackCount })
}
return zones
}
function layoutOptionsForWindowCount(windowCount) {
const count = clampedLayoutWindowCount(windowCount)
if (count < 2) {
return []
}
if (count === 2) {
return [
{
mode: "columns",
selectedModes: ["master", "columns"],
name: i18n("Side by Side"),
summary: i18n("2 columns"),
zones: linearLayoutZones(count, "vertical")
},
{
mode: "rows",
selectedModes: ["rows"],
name: i18n("Stacked"),
summary: i18n("2 rows"),
zones: linearLayoutZones(count, "horizontal")
}
]
}
return [
{
mode: "master",
selectedModes: ["master"],
name: i18n("Master Stack"),
summary: i18n("1 + %1 stack", count - 1),
zones: masterLayoutZones(count)
},
{
mode: "columns",
selectedModes: ["columns"],
name: i18n("Columns"),
summary: i18n("%1 columns", count),
zones: linearLayoutZones(count, "vertical")
},
{
mode: "rows",
selectedModes: ["rows"],
name: i18n("Rows"),
summary: i18n("%1 rows", count),
zones: linearLayoutZones(count, "horizontal")
}
]
}
function emptyLayoutSummary() {
return clampedWindowCount === 1 ? i18n("1 window") : i18n("0 windows")
}
Behavior on opacity {
MobileShell.MotionNumberAnimation {
type: MobileShell.Motion.EffectsFast
duration: root.animationDuration
}
}
Shape {
anchors.fill: parent
ShapePath {
fillColor: root.surfaceColor
strokeWidth: 0
startX: root.width
startY: 0
PathLine { x: root.cornerRadius; y: 0 }
PathArc {
x: 0
y: root.cornerRadius
radiusX: root.cornerRadius
radiusY: root.cornerRadius
direction: PathArc.Counterclockwise
}
PathLine { x: 0; y: root.height - root.cornerRadius }
PathArc {
x: root.cornerRadius
y: root.height
radiusX: root.cornerRadius
radiusY: root.cornerRadius
direction: PathArc.Counterclockwise
}
PathLine { x: root.width; y: root.height }
PathLine { x: root.width; y: 0 }
}
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
}
ColumnLayout {
anchors.fill: parent
anchors.margins: Kirigami.Units.smallSpacing
spacing: Kirigami.Units.smallSpacing
PlasmaComponents.Label {
Layout.fillWidth: true
text: i18n("Tiling Layout")
font.weight: Font.Medium
elide: Text.ElideRight
maximumLineCount: 1
}
Repeater {
model: root.layoutOptions
delegate: MouseArea {
id: optionButton
required property var modelData
Layout.fillWidth: true
Layout.preferredHeight: root.rowHeight
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
readonly property bool selected: modelData.selectedModes.indexOf(root.currentMode) >= 0
onClicked: {
if (!selected) {
root.layoutModeRequested(modelData.mode)
}
root.dismissRequested()
}
Rectangle {
anchors.fill: parent
radius: Kirigami.Units.cornerRadius
color: optionButton.selected
? Qt.rgba(Kirigami.Theme.highlightColor.r, Kirigami.Theme.highlightColor.g, Kirigami.Theme.highlightColor.b, 0.24)
: optionButton.containsMouse
? Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g, Kirigami.Theme.textColor.b, 0.08)
: "transparent"
border.width: optionButton.selected ? 1 : 0
border.color: Qt.rgba(Kirigami.Theme.highlightColor.r, Kirigami.Theme.highlightColor.g, Kirigami.Theme.highlightColor.b, 0.5)
}
RowLayout {
anchors.fill: parent
anchors.leftMargin: Kirigami.Units.smallSpacing
anchors.rightMargin: Kirigami.Units.smallSpacing
spacing: Kirigami.Units.smallSpacing
Rectangle {
id: layoutPreviewFrame
Layout.preferredWidth: Kirigami.Units.gridUnit * 2.5
Layout.preferredHeight: Kirigami.Units.gridUnit * 1.65
radius: Kirigami.Units.cornerRadius
color: Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g, Kirigami.Theme.textColor.b, 0.06)
border.width: 1
border.color: Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g, Kirigami.Theme.textColor.b, 0.18)
Repeater {
model: optionButton.modelData.zones
delegate: Rectangle {
required property var modelData
x: Math.round(layoutPreviewFrame.width * modelData.x) + Kirigami.Units.smallSpacing / 2
y: Math.round(layoutPreviewFrame.height * modelData.y) + Kirigami.Units.smallSpacing / 2
width: Math.max(1, Math.round(layoutPreviewFrame.width * modelData.w) - Kirigami.Units.smallSpacing)
height: Math.max(1, Math.round(layoutPreviewFrame.height * modelData.h) - Kirigami.Units.smallSpacing)
radius: Math.max(1, Kirigami.Units.cornerRadius - 1)
color: Qt.rgba(Kirigami.Theme.highlightColor.r, Kirigami.Theme.highlightColor.g, Kirigami.Theme.highlightColor.b, 0.58)
}
}
}
ColumnLayout {
Layout.fillWidth: true
spacing: 0
PlasmaComponents.Label {
Layout.fillWidth: true
text: optionButton.modelData.name
elide: Text.ElideRight
maximumLineCount: 1
}
PlasmaComponents.Label {
Layout.fillWidth: true
text: optionButton.modelData.summary
opacity: 0.62
font: Kirigami.Theme.smallFont
elide: Text.ElideRight
maximumLineCount: 1
}
}
}
}
}
ColumnLayout {
Layout.fillWidth: true
Layout.fillHeight: true
visible: root.optionCount === 0
spacing: Kirigami.Units.smallSpacing
Item { Layout.fillHeight: true }
PlasmaComponents.Label {
Layout.fillWidth: true
horizontalAlignment: Text.AlignHCenter
text: i18n("No Alternatives")
font.weight: Font.Medium
elide: Text.ElideRight
maximumLineCount: 1
}
PlasmaComponents.Label {
Layout.fillWidth: true
horizontalAlignment: Text.AlignHCenter
text: root.emptyLayoutSummary()
opacity: 0.62
font: Kirigami.Theme.smallFont
elide: Text.ElideRight
maximumLineCount: 1
}
Item { Layout.fillHeight: true }
}
}
}

View file

@ -306,14 +306,23 @@ ContainmentItem {
readonly property real workAreaWidth: Math.max(0, width - frameThickness * 2) readonly property real workAreaWidth: Math.max(0, width - frameThickness * 2)
readonly property real workAreaHeight: Math.max(0, height - topBarHeight - dockHeight - frameThickness * 2) readonly property real workAreaHeight: Math.max(0, height - topBarHeight - dockHeight - frameThickness * 2)
readonly property real leftEdgeHotzoneWidth: Math.max(frameThickness, Math.round(Kirigami.Units.gridUnit * 0.7)) readonly property real leftEdgeHotzoneWidth: Math.max(frameThickness, Math.round(Kirigami.Units.gridUnit * 0.7))
readonly property real rightEdgeHotzoneWidth: leftEdgeHotzoneWidth
readonly property real leftLauncherWidth: Math.min(Kirigami.Units.gridUnit * 22, width * 0.42) readonly property real leftLauncherWidth: Math.min(Kirigami.Units.gridUnit * 22, width * 0.42)
readonly property real leftLauncherHeight: Math.min(Kirigami.Units.gridUnit * 16, workAreaHeight * 0.66) readonly property real leftLauncherHeight: Math.min(Kirigami.Units.gridUnit * 16, workAreaHeight * 0.66)
readonly property bool leftLauncherEnabled: root.folio.HomeScreenState.appDrawerOpenProgress <= 0 readonly property bool leftLauncherEnabled: root.folio.HomeScreenState.appDrawerOpenProgress <= 0
readonly property real leftFrameBulgeIdleDepth: Math.max(frameThickness * 1.2, Kirigami.Units.gridUnit * 0.35) readonly property real layoutMenuWidth: Math.min(Kirigami.Units.gridUnit * 16, width * 0.34)
readonly property int layoutMenuWindowCount: Math.max(0, ShellSettings.Settings.dynamicTilingLayoutWindowCount)
readonly property bool layoutMenuEnabled: ShellSettings.Settings.dynamicTilingEnabled
&& layoutMenuWindowCount >= 2
&& root.folio.HomeScreenState.appDrawerOpenProgress <= 0
readonly property real leftFrameBulgeIdleDepth: Math.max(frameThickness * 0.45, Kirigami.Units.gridUnit * 0.16)
readonly property real leftFrameBulgeHoverDepth: 0 readonly property real leftFrameBulgeHoverDepth: 0
property real leftFrameBulgeDepth: !leftLauncherEnabled || leftLauncherOpen || leftEdgeHovered property real leftFrameBulgeDepth: !leftLauncherEnabled || leftLauncherOpen || leftEdgeHovered
? leftFrameBulgeHoverDepth ? leftFrameBulgeHoverDepth
: leftFrameBulgeIdleDepth : leftFrameBulgeIdleDepth
property real rightFrameBulgeDepth: !layoutMenuEnabled || layoutMenuOpen || rightEdgeHovered
? leftFrameBulgeHoverDepth
: leftFrameBulgeIdleDepth
// Long, thin thickening of the lower-left workspace wall. Vertical // Long, thin thickening of the lower-left workspace wall. Vertical
// tangents at all three anchors keep the curve smooth as it blends // tangents at all three anchors keep the curve smooth as it blends
// into the straight wall above and below. // into the straight wall above and below.
@ -326,6 +335,13 @@ ContainmentItem {
// Bezier control-handle length along the vertical tangent at each // Bezier control-handle length along the vertical tangent at each
// anchor. ~0.55 of the half-length gives a clean, taut oval profile. // anchor. ~0.55 of the half-length gives a clean, taut oval profile.
readonly property real leftFrameBulgeTangent: leftFrameBulgeHalfLength * 0.55 readonly property real leftFrameBulgeTangent: leftFrameBulgeHalfLength * 0.55
readonly property real rightFrameBulgeEffectiveDepth: Math.max(rightFrameBulgeDepth, 0.01)
readonly property real rightFrameBulgeApexX: workAreaX + workAreaWidth - rightFrameBulgeEffectiveDepth
readonly property real rightFrameBulgeHalfLength: leftFrameBulgeHalfLength
readonly property real rightFrameBulgeApexY: leftFrameBulgeApexY
readonly property real rightFrameBulgeEdgeTopY: rightFrameBulgeApexY - rightFrameBulgeHalfLength
readonly property real rightFrameBulgeEdgeBottomY: rightFrameBulgeApexY + rightFrameBulgeHalfLength
readonly property real rightFrameBulgeTangent: rightFrameBulgeHalfLength * 0.55
readonly property color chromeColor: Kirigami.Theme.backgroundColor readonly property color chromeColor: Kirigami.Theme.backgroundColor
readonly property color edgeColor: Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g, Kirigami.Theme.textColor.b, 0.1) readonly property color edgeColor: Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g, Kirigami.Theme.textColor.b, 0.1)
readonly property int dockAnimationDuration: MobileShell.Motion.duration(MobileShell.Motion.SpatialDefault) readonly property int dockAnimationDuration: MobileShell.Motion.duration(MobileShell.Motion.SpatialDefault)
@ -339,6 +355,9 @@ ContainmentItem {
property bool leftEdgeHovered: false property bool leftEdgeHovered: false
property bool leftLauncherHovered: false property bool leftLauncherHovered: false
property bool leftLauncherOpen: false property bool leftLauncherOpen: false
property bool rightEdgeHovered: false
property bool layoutMenuHovered: false
property bool layoutMenuOpen: false
readonly property bool shouldHide: ShellSettings.Settings.autoHidePanelsEnabled readonly property bool shouldHide: ShellSettings.Settings.autoHidePanelsEnabled
&& windowMaximizedTracker.showingWindow && !hoverRevealing && windowMaximizedTracker.showingWindow && !hoverRevealing
@ -349,12 +368,21 @@ ContainmentItem {
function updateInputRegion() { function updateInputRegion() {
const topBarRegion = Qt.rect(0, 0, width, topBarHitHeight) const topBarRegion = Qt.rect(0, 0, width, topBarHitHeight)
const leftEdgeRegion = Qt.rect(0, topBarHitHeight, leftEdgeHotzoneWidth, Math.max(0, height - topBarHitHeight - dockHeight)) const leftEdgeRegion = Qt.rect(0, topBarHitHeight, leftEdgeHotzoneWidth, Math.max(0, height - topBarHitHeight - dockHeight))
const rightEdgeRegion = Qt.rect(width - rightEdgeHotzoneWidth, topBarHitHeight, rightEdgeHotzoneWidth, Math.max(0, height - topBarHitHeight - dockHeight))
const leftLauncherRegion = Qt.rect(0, const leftLauncherRegion = Qt.rect(0,
Math.max(0, height - dockHeight - leftLauncherHeight), Math.max(0, height - dockHeight - leftLauncherHeight),
leftLauncherWidth, leftLauncherWidth,
leftLauncherHeight) leftLauncherHeight)
const layoutMenuRegion = Qt.rect(rightLayoutMenu.x,
rightLayoutMenu.y,
rightLayoutMenu.width,
rightLayoutMenu.height)
let regions = [topBarRegion, leftEdgeRegion] let regions = [topBarRegion, leftEdgeRegion]
if (layoutMenuEnabled) {
regions.push(rightEdgeRegion)
}
if (shouldHide && dockOffset >= dockHeight) { if (shouldHide && dockOffset >= dockHeight) {
regions.push(Qt.rect(0, height - revealStripHeight, width, revealStripHeight)) regions.push(Qt.rect(0, height - revealStripHeight, width, revealStripHeight))
} else { } else {
@ -365,6 +393,10 @@ ContainmentItem {
regions.push(leftLauncherRegion) regions.push(leftLauncherRegion)
} }
if (layoutMenuEnabled && layoutMenuOpen) {
regions.push(layoutMenuRegion)
}
MobileShell.ShellUtil.setInputRegions(convergenceChrome, regions) MobileShell.ShellUtil.setInputRegions(convergenceChrome, regions)
} }
@ -397,6 +429,25 @@ ContainmentItem {
inputRegionTimer.restart() inputRegionTimer.restart()
} }
function refreshLayoutMenuVisibility() {
if (!layoutMenuEnabled) {
layoutMenuCloseTimer.stop()
rightEdgeHovered = false
layoutMenuHovered = false
layoutMenuOpen = false
inputRegionTimer.restart()
return
}
if (rightEdgeHovered || layoutMenuHovered) {
layoutMenuCloseTimer.stop()
layoutMenuOpen = true
} else {
layoutMenuCloseTimer.restart()
}
inputRegionTimer.restart()
}
function updateLeftLauncherHoverState(pointerX, pointerY, withinWindow) { function updateLeftLauncherHoverState(pointerX, pointerY, withinWindow) {
const insideEdge = withinWindow const insideEdge = withinWindow
&& pointerX >= 0 && pointerX >= 0
@ -421,6 +472,30 @@ ContainmentItem {
refreshLeftLauncherVisibility() refreshLeftLauncherVisibility()
} }
function updateLayoutMenuHoverState(pointerX, pointerY, withinWindow) {
const insideEdge = layoutMenuEnabled
&& withinWindow
&& pointerX >= (width - rightEdgeHotzoneWidth)
&& pointerX <= width
&& pointerY >= topBarHitHeight
&& pointerY <= (height - dockHeight)
const insideMenu = withinWindow
&& layoutMenuOpen
&& pointerX >= rightLayoutMenu.x
&& pointerX <= width
&& pointerY >= rightLayoutMenu.y
&& pointerY <= (rightLayoutMenu.y + rightLayoutMenu.height)
if (rightEdgeHovered !== insideEdge) {
rightEdgeHovered = insideEdge
}
if (layoutMenuHovered !== insideMenu) {
layoutMenuHovered = insideMenu
}
refreshLayoutMenuVisibility()
}
onActiveChanged: { onActiveChanged: {
hoverRevealTimer.stop() hoverRevealTimer.stop()
hoverRevealing = false hoverRevealing = false
@ -438,6 +513,7 @@ ContainmentItem {
inputRegionTimer.restart() inputRegionTimer.restart()
} }
onLeftLauncherEnabledChanged: refreshLeftLauncherVisibility() onLeftLauncherEnabledChanged: refreshLeftLauncherVisibility()
onLayoutMenuEnabledChanged: refreshLayoutMenuVisibility()
// 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
@ -484,6 +560,20 @@ ContainmentItem {
} }
} }
Timer {
id: layoutMenuCloseTimer
interval: Kirigami.Units.shortDuration
repeat: false
onTriggered: {
if (!convergenceChrome.rightEdgeHovered
&& !convergenceChrome.layoutMenuHovered
&& convergenceChrome.layoutMenuOpen) {
convergenceChrome.layoutMenuOpen = false
inputRegionTimer.restart()
}
}
}
Behavior on dockOffset { Behavior on dockOffset {
MobileShell.MotionNumberAnimation { MobileShell.MotionNumberAnimation {
type: MobileShell.Motion.SpatialDefault type: MobileShell.Motion.SpatialDefault
@ -498,6 +588,13 @@ ContainmentItem {
} }
} }
Behavior on rightFrameBulgeDepth {
MobileShell.MotionNumberAnimation {
type: MobileShell.Motion.SpatialDefault
duration: root.shortAnimationDuration
}
}
Rectangle { Rectangle {
id: topBarSurface id: topBarSurface
anchors.top: parent.top anchors.top: parent.top
@ -534,6 +631,23 @@ ContainmentItem {
PathMove { x: convergenceChrome.workAreaX + convergenceChrome.frameRadius; y: convergenceChrome.workAreaY } PathMove { x: convergenceChrome.workAreaX + convergenceChrome.frameRadius; y: convergenceChrome.workAreaY }
PathLine { x: convergenceChrome.workAreaX + convergenceChrome.workAreaWidth - convergenceChrome.frameRadius; y: convergenceChrome.workAreaY } PathLine { x: convergenceChrome.workAreaX + convergenceChrome.workAreaWidth - convergenceChrome.frameRadius; y: convergenceChrome.workAreaY }
PathArc { x: convergenceChrome.workAreaX + convergenceChrome.workAreaWidth; y: convergenceChrome.workAreaY + convergenceChrome.frameRadius; radiusX: convergenceChrome.frameRadius; radiusY: convergenceChrome.frameRadius } PathArc { x: convergenceChrome.workAreaX + convergenceChrome.workAreaWidth; y: convergenceChrome.workAreaY + convergenceChrome.frameRadius; radiusX: convergenceChrome.frameRadius; radiusY: convergenceChrome.frameRadius }
PathLine { x: convergenceChrome.workAreaX + convergenceChrome.workAreaWidth; y: convergenceChrome.rightFrameBulgeEdgeTopY }
PathCubic {
x: convergenceChrome.rightFrameBulgeApexX
y: convergenceChrome.rightFrameBulgeApexY
control1X: convergenceChrome.workAreaX + convergenceChrome.workAreaWidth
control1Y: convergenceChrome.rightFrameBulgeEdgeTopY + convergenceChrome.rightFrameBulgeTangent
control2X: convergenceChrome.rightFrameBulgeApexX
control2Y: convergenceChrome.rightFrameBulgeApexY - convergenceChrome.rightFrameBulgeTangent
}
PathCubic {
x: convergenceChrome.workAreaX + convergenceChrome.workAreaWidth
y: convergenceChrome.rightFrameBulgeEdgeBottomY
control1X: convergenceChrome.rightFrameBulgeApexX
control1Y: convergenceChrome.rightFrameBulgeApexY + convergenceChrome.rightFrameBulgeTangent
control2X: convergenceChrome.workAreaX + convergenceChrome.workAreaWidth
control2Y: convergenceChrome.rightFrameBulgeEdgeBottomY - convergenceChrome.rightFrameBulgeTangent
}
PathLine { x: convergenceChrome.workAreaX + convergenceChrome.workAreaWidth; y: convergenceChrome.workAreaY + convergenceChrome.workAreaHeight - convergenceChrome.frameRadius } PathLine { x: convergenceChrome.workAreaX + convergenceChrome.workAreaWidth; y: convergenceChrome.workAreaY + convergenceChrome.workAreaHeight - convergenceChrome.frameRadius }
PathArc { x: convergenceChrome.workAreaX + convergenceChrome.workAreaWidth - convergenceChrome.frameRadius; y: convergenceChrome.workAreaY + convergenceChrome.workAreaHeight; radiusX: convergenceChrome.frameRadius; radiusY: convergenceChrome.frameRadius } PathArc { x: convergenceChrome.workAreaX + convergenceChrome.workAreaWidth - convergenceChrome.frameRadius; y: convergenceChrome.workAreaY + convergenceChrome.workAreaHeight; radiusX: convergenceChrome.frameRadius; radiusY: convergenceChrome.frameRadius }
PathLine { x: convergenceChrome.workAreaX + convergenceChrome.frameRadius; y: convergenceChrome.workAreaY + convergenceChrome.workAreaHeight } PathLine { x: convergenceChrome.workAreaX + convergenceChrome.frameRadius; y: convergenceChrome.workAreaY + convergenceChrome.workAreaHeight }
@ -580,6 +694,23 @@ ContainmentItem {
controlX: convergenceChrome.workAreaX + convergenceChrome.workAreaWidth controlX: convergenceChrome.workAreaX + convergenceChrome.workAreaWidth
controlY: convergenceChrome.workAreaY controlY: convergenceChrome.workAreaY
} }
PathLine { x: convergenceChrome.workAreaX + convergenceChrome.workAreaWidth; y: convergenceChrome.rightFrameBulgeEdgeTopY }
PathCubic {
x: convergenceChrome.rightFrameBulgeApexX
y: convergenceChrome.rightFrameBulgeApexY
control1X: convergenceChrome.workAreaX + convergenceChrome.workAreaWidth
control1Y: convergenceChrome.rightFrameBulgeEdgeTopY + convergenceChrome.rightFrameBulgeTangent
control2X: convergenceChrome.rightFrameBulgeApexX
control2Y: convergenceChrome.rightFrameBulgeApexY - convergenceChrome.rightFrameBulgeTangent
}
PathCubic {
x: convergenceChrome.workAreaX + convergenceChrome.workAreaWidth
y: convergenceChrome.rightFrameBulgeEdgeBottomY
control1X: convergenceChrome.rightFrameBulgeApexX
control1Y: convergenceChrome.rightFrameBulgeApexY + convergenceChrome.rightFrameBulgeTangent
control2X: convergenceChrome.workAreaX + convergenceChrome.workAreaWidth
control2Y: convergenceChrome.rightFrameBulgeEdgeBottomY - convergenceChrome.rightFrameBulgeTangent
}
PathLine { x: convergenceChrome.workAreaX + convergenceChrome.workAreaWidth; y: convergenceChrome.workAreaY + convergenceChrome.workAreaHeight - convergenceChrome.frameRadius } PathLine { x: convergenceChrome.workAreaX + convergenceChrome.workAreaWidth; y: convergenceChrome.workAreaY + convergenceChrome.workAreaHeight - convergenceChrome.frameRadius }
PathQuad { PathQuad {
x: convergenceChrome.workAreaX + convergenceChrome.workAreaWidth - convergenceChrome.frameRadius x: convergenceChrome.workAreaX + convergenceChrome.workAreaWidth - convergenceChrome.frameRadius
@ -672,6 +803,15 @@ ContainmentItem {
width: convergenceChrome.leftEdgeHotzoneWidth width: convergenceChrome.leftEdgeHotzoneWidth
} }
Item {
id: rightEdgeStrip
anchors.right: parent.right
anchors.top: topBarSurface.bottom
anchors.bottom: dockSurface.top
width: convergenceChrome.layoutMenuEnabled ? convergenceChrome.rightEdgeHotzoneWidth : 0
}
MouseArea { MouseArea {
id: leftLauncherPointerTracker id: leftLauncherPointerTracker
@ -682,9 +822,43 @@ ContainmentItem {
onPositionChanged: (mouse) => { onPositionChanged: (mouse) => {
convergenceChrome.updateLeftLauncherHoverState(mouse.x, mouse.y, true) convergenceChrome.updateLeftLauncherHoverState(mouse.x, mouse.y, true)
convergenceChrome.updateLayoutMenuHoverState(mouse.x, mouse.y, true)
} }
onExited: { onExited: {
convergenceChrome.updateLeftLauncherHoverState(-1, -1, false) convergenceChrome.updateLeftLauncherHoverState(-1, -1, false)
convergenceChrome.updateLayoutMenuHoverState(-1, -1, false)
}
}
DynamicTilingLayoutMenu {
id: rightLayoutMenu
width: convergenceChrome.layoutMenuWidth
height: preferredHeight
x: convergenceChrome.width - width
y: convergenceChrome.height - convergenceChrome.dockHeight - height
visible: convergenceChrome.layoutMenuOpen
opacity: convergenceChrome.layoutMenuOpen ? 1 : 0
maxHeight: convergenceChrome.workAreaHeight * 0.5
windowCount: convergenceChrome.layoutMenuWindowCount
currentMode: ShellSettings.Settings.dynamicTilingLayoutMode
surfaceColor: convergenceChrome.chromeColor
animationDuration: root.shortAnimationDuration
transform: Translate {
y: convergenceChrome.layoutMenuOpen ? 0 : Kirigami.Units.gridUnit
x: convergenceChrome.layoutMenuOpen ? 0 : rightLayoutMenu.width - convergenceChrome.rightEdgeHotzoneWidth
}
onLayoutModeRequested: (mode) => {
if (ShellSettings.Settings.requestDynamicTilingLayoutMode !== undefined) {
ShellSettings.Settings.requestDynamicTilingLayoutMode(mode)
}
}
onDismissRequested: {
convergenceChrome.layoutMenuOpen = false
inputRegionTimer.restart()
} }
} }

View file

@ -71,8 +71,10 @@ Item {
// Values keep callback references so KWin script reloads can disconnect them. // Values keep callback references so KWin script reloads can disconnect them.
property var dragConnectedWindows: ({}) property var dragConnectedWindows: ({})
property int lastWindowRequestSerial: ShellSettings.Settings.dynamicTilingWindowRequestSerial property int lastWindowRequestSerial: -1
property string lastPublishedMaximizedWindowIds: "__unpublished__" property string lastPublishedMaximizedWindowIds: "__unpublished__"
property int lastLayoutRequestSerial: -1
property string lastPublishedLayoutState: "__unpublished__"
// Drag state. // Drag state.
// //
@ -504,6 +506,7 @@ Item {
const focused = Object.assign({}, lastFocusedWindowKeys); const focused = Object.assign({}, lastFocusedWindowKeys);
delete focused[outputName]; delete focused[outputName];
lastFocusedWindowKeys = focused; lastFocusedWindowKeys = focused;
publishDynamicTilingLayoutState();
} }
function clearDisplacedWindowOwnersForLayout(outputName) { function clearDisplacedWindowOwnersForLayout(outputName) {
@ -654,6 +657,8 @@ Item {
for (let i = 0; i < cleanupLayouts.length; i++) { for (let i = 0; i < cleanupLayouts.length; i++) {
cleanupEmptyLayout(cleanupLayouts[i]); cleanupEmptyLayout(cleanupLayouts[i]);
} }
publishDynamicTilingLayoutState();
} }
function makeLeaf(win) { function makeLeaf(win) {
@ -842,6 +847,15 @@ Item {
const win = KWinComponents.Workspace.activeWindow; const win = KWinComponents.Workspace.activeWindow;
const activeName = layoutKeyForWindow(win); const activeName = layoutKeyForWindow(win);
if (activeName !== "") return activeName; if (activeName !== "") return activeName;
const desktop = KWinComponents.Workspace.currentDesktop;
const screens = KWinComponents.Workspace.screens;
if (desktop && screens && screens.length > 0) {
for (let i = 0; i < screens.length; i++) {
const currentDesktopName = layoutKeyFor(screens[i].name, desktop);
if (screenLayouts[currentDesktopName]) return currentDesktopName;
}
return layoutKeyFor(screens[0].name, desktop);
}
for (const name in screenLayouts) { for (const name in screenLayouts) {
return name; return name;
} }
@ -863,6 +877,21 @@ Item {
markLayoutChanged(transaction, outputName); markLayoutChanged(transaction, outputName);
applyLayoutTransaction(transaction); applyLayoutTransaction(transaction);
} }
publishDynamicTilingLayoutState();
}
function setLayoutMode(outputName, mode) {
if (!outputName || layoutModes.indexOf(mode) < 0) return;
setLayoutModeForScreen(outputName, mode);
const windows = orderedWindowsForScreen(outputName);
if (windows.length > 0) {
setStableLayout(outputName, windows);
const transaction = createLayoutTransaction();
markLayoutChanged(transaction, outputName);
applyLayoutTransaction(transaction);
}
publishDynamicTilingLayoutState();
} }
function containsLeaf(node, key) { function containsLeaf(node, key) {
@ -1281,6 +1310,37 @@ Item {
} }
} }
function publishDynamicTilingLayoutState() {
if (!isConvergence()) {
const disabledState = "|0";
if (disabledState === lastPublishedLayoutState) return;
lastPublishedLayoutState = disabledState;
if (ShellSettings.Settings.reportDynamicTilingLayoutState !== undefined) {
ShellSettings.Settings.reportDynamicTilingLayoutState("", 0);
}
return;
}
const outputName = outputNameForActiveWindow();
const mode = outputName !== "" ? layoutModeForScreen(outputName) : "";
const windowCount = outputName !== "" ? windowCountForLayout(outputName) : 0;
const serialized = mode + "|" + windowCount;
if (serialized === lastPublishedLayoutState) return;
lastPublishedLayoutState = serialized;
if (ShellSettings.Settings.reportDynamicTilingLayoutState !== undefined) {
ShellSettings.Settings.reportDynamicTilingLayoutState(mode, windowCount);
}
}
function clearDynamicTilingLayoutState() {
lastPublishedLayoutState = "__unpublished__";
if (ShellSettings.Settings.reportDynamicTilingLayoutState !== undefined) {
ShellSettings.Settings.reportDynamicTilingLayoutState("", 0);
}
}
function maximizedLayoutNameForWindow(win) { function maximizedLayoutNameForWindow(win) {
const key = windowKey(win); const key = windowKey(win);
if (!key) return ""; if (!key) return "";
@ -1682,6 +1742,19 @@ Item {
} }
} }
function handleLayoutModeRequest() {
const serial = ShellSettings.Settings.dynamicTilingLayoutRequestSerial;
if (serial === lastLayoutRequestSerial) return;
lastLayoutRequestSerial = serial;
if (!isConvergence()) return;
const mode = ShellSettings.Settings.dynamicTilingLayoutRequestMode;
if (layoutModes.indexOf(mode) < 0) return;
setLayoutMode(outputNameForActiveWindow(), mode);
}
function promoteWindow(win) { function promoteWindow(win) {
if (!isTileable(win)) return; if (!isTileable(win)) return;
@ -1771,10 +1844,12 @@ Item {
function onActiveWindowChanged() { function onActiveWindowChanged() {
root.rememberFocusedWindow(KWinComponents.Workspace.activeWindow); root.rememberFocusedWindow(KWinComponents.Workspace.activeWindow);
root.publishDynamicTilingLayoutState();
} }
function onCurrentDesktopChanged() { function onCurrentDesktopChanged() {
root.retileCurrentDesktopLayouts(); root.retileCurrentDesktopLayouts();
root.publishDynamicTilingLayoutState();
} }
function onScreensChanged() { function onScreensChanged() {
@ -1788,6 +1863,7 @@ Item {
} }
root.scheduleRetileAll(); root.scheduleRetileAll();
root.publishDynamicTilingLayoutState();
} }
} }
@ -1801,6 +1877,7 @@ Item {
for (let i = 0; i < wins.length; i++) { for (let i = 0; i < wins.length; i++) {
adoptWindow(wins[i]); adoptWindow(wins[i]);
} }
publishDynamicTilingLayoutState();
} else { } else {
// Clear all tiles the convergentwindows script will re-maximize // Clear all tiles the convergentwindows script will re-maximize
restoreAllMaximizedLayouts(); restoreAllMaximizedLayouts();
@ -1809,6 +1886,7 @@ Item {
screenLayoutModes = {}; screenLayoutModes = {};
lastFocusedWindowKeys = {}; lastFocusedWindowKeys = {};
maximizedLayouts = {}; maximizedLayouts = {};
clearDynamicTilingLayoutState();
} }
} }
@ -1820,11 +1898,13 @@ Item {
screenLayoutModes = {}; screenLayoutModes = {};
lastFocusedWindowKeys = {}; lastFocusedWindowKeys = {};
maximizedLayouts = {}; maximizedLayouts = {};
clearDynamicTilingLayoutState();
} else if (isConvergence()) { } else if (isConvergence()) {
const wins = KWinComponents.Workspace.windows; const wins = KWinComponents.Workspace.windows;
for (let i = 0; i < wins.length; i++) { for (let i = 0; i < wins.length; i++) {
adoptWindow(wins[i]); adoptWindow(wins[i]);
} }
publishDynamicTilingLayoutState();
} }
} }
@ -1834,6 +1914,7 @@ Item {
for (let i = 0; i < wins.length; i++) { for (let i = 0; i < wins.length; i++) {
adoptWindow(wins[i]); adoptWindow(wins[i]);
} }
publishDynamicTilingLayoutState();
} else { } else {
// Tiling turned off leave windows where they are. // Tiling turned off leave windows where they are.
restoreAllMaximizedLayouts(); restoreAllMaximizedLayouts();
@ -1842,12 +1923,17 @@ Item {
screenLayoutModes = {}; screenLayoutModes = {};
lastFocusedWindowKeys = {}; lastFocusedWindowKeys = {};
maximizedLayouts = {}; maximizedLayouts = {};
clearDynamicTilingLayoutState();
} }
} }
function onDynamicTilingWindowRequestChanged() { function onDynamicTilingWindowRequestChanged() {
root.handleWindowTilingRequest(); root.handleWindowTilingRequest();
} }
function onDynamicTilingLayoutRequestChanged() {
root.handleLayoutModeRequest();
}
} }
// Drag handlers // Drag handlers
@ -2165,6 +2251,9 @@ Item {
// Component setup // Component setup
Component.onCompleted: { Component.onCompleted: {
lastWindowRequestSerial = ShellSettings.Settings.dynamicTilingWindowRequestSerial;
lastLayoutRequestSerial = ShellSettings.Settings.dynamicTilingLayoutRequestSerial;
// Connect to existing windows // Connect to existing windows
const wins = KWinComponents.Workspace.windows; const wins = KWinComponents.Workspace.windows;
for (let i = 0; i < wins.length; i++) { for (let i = 0; i < wins.length; i++) {
@ -2172,10 +2261,12 @@ Item {
} }
rememberFocusedWindow(KWinComponents.Workspace.activeWindow); rememberFocusedWindow(KWinComponents.Workspace.activeWindow);
publishDynamicTilingWindowState(); publishDynamicTilingWindowState();
publishDynamicTilingLayoutState();
} }
Component.onDestruction: { Component.onDestruction: {
disconnectDragHandlers(); disconnectDragHandlers();
clearDynamicTilingWindowState(); clearDynamicTilingWindowState();
clearDynamicTilingLayoutState();
} }
} }

View file

@ -12,6 +12,7 @@ effects_cmake="$repo_root/kwin/effects/CMakeLists.txt"
tiling_script="$repo_root/kwin/scripts/shift-tiling/contents/ui/main.qml" tiling_script="$repo_root/kwin/scripts/shift-tiling/contents/ui/main.qml"
decoration_qml="$repo_root/kwin/decorations/org.shift.decoration/contents/ui/main.qml" decoration_qml="$repo_root/kwin/decorations/org.shift.decoration/contents/ui/main.qml"
running_apps_panel="$repo_root/containments/homescreens/folio/qml/RunningAppsPanel.qml" running_apps_panel="$repo_root/containments/homescreens/folio/qml/RunningAppsPanel.qml"
folio_main="$repo_root/containments/homescreens/folio/qml/main.qml"
env_config="$repo_root/envmanager/config.h" env_config="$repo_root/envmanager/config.h"
require_line() { require_line() {
@ -60,6 +61,10 @@ require_line "$effect_qml" "showPreview(\"restore\""
require_line "$tiling_script" "readonly property int maxWindowsPerPage: 4" require_line "$tiling_script" "readonly property int maxWindowsPerPage: 4"
require_line "$tiling_script" "readonly property real stablePrimaryRatio: 0.58" require_line "$tiling_script" "readonly property real stablePrimaryRatio: 0.58"
require_line "$tiling_script" "readonly property var layoutModes: [\"master\", \"columns\", \"rows\"]" require_line "$tiling_script" "readonly property var layoutModes: [\"master\", \"columns\", \"rows\"]"
require_line "$tiling_script" "property int lastWindowRequestSerial: -1"
require_line "$tiling_script" "property int lastLayoutRequestSerial: -1"
require_line "$tiling_script" "lastWindowRequestSerial = ShellSettings.Settings.dynamicTilingWindowRequestSerial"
require_line "$tiling_script" "lastLayoutRequestSerial = ShellSettings.Settings.dynamicTilingLayoutRequestSerial"
require_line "$tiling_script" "function desktopKey(desktop)" require_line "$tiling_script" "function desktopKey(desktop)"
require_line "$tiling_script" "function desktopForWindow(win)" require_line "$tiling_script" "function desktopForWindow(win)"
require_line "$tiling_script" "function normalizeWindowDesktopScope(win)" require_line "$tiling_script" "function normalizeWindowDesktopScope(win)"
@ -126,6 +131,16 @@ require_line "$decoration_qml" "borders.bottom = normalCornerRadius;"
require_line "$decoration_qml" "PathArc { x: root.width - root.cornerRadius; y: root.height; radiusX: root.cornerRadius; radiusY: root.cornerRadius }" require_line "$decoration_qml" "PathArc { x: root.width - root.cornerRadius; y: root.height; radiusX: root.cornerRadius; radiusY: root.cornerRadius }"
require_line "$decoration_qml" "PathArc { x: 0; y: root.height - root.cornerRadius; radiusX: root.cornerRadius; radiusY: root.cornerRadius }" require_line "$decoration_qml" "PathArc { x: 0; y: root.height - root.cornerRadius; radiusX: root.cornerRadius; radiusY: root.cornerRadius }"
require_line "$folio_main" "readonly property int layoutMenuWindowCount: Math.max(0, ShellSettings.Settings.dynamicTilingLayoutWindowCount)"
require_line "$folio_main" "&& layoutMenuWindowCount >= 2"
require_line "$folio_main" "if (layoutMenuEnabled) {"
require_line "$folio_main" "regions.push(rightEdgeRegion)"
require_line "$folio_main" "if (layoutMenuEnabled && layoutMenuOpen)"
require_line "$folio_main" "rightEdgeHovered = false"
require_line "$folio_main" "layoutMenuHovered = false"
require_line "$folio_main" "const insideEdge = layoutMenuEnabled"
require_line "$folio_main" "width: convergenceChrome.layoutMenuEnabled ? convergenceChrome.rightEdgeHotzoneWidth : 0"
running_panel_group_disabled_count="$(grep -F "groupMode: TaskManager.TasksModel.GroupDisabled" "$running_apps_panel" | wc -l)" running_panel_group_disabled_count="$(grep -F "groupMode: TaskManager.TasksModel.GroupDisabled" "$running_apps_panel" | wc -l)"
if [[ "$running_panel_group_disabled_count" -ne 2 ]]; then if [[ "$running_panel_group_disabled_count" -ne 2 ]]; then
echo "Expected the Folio Running panel to disable grouping for both task models; found $running_panel_group_disabled_count" >&2 echo "Expected the Folio Running panel to disable grouping for both task models; found $running_panel_group_disabled_count" >&2
@ -155,6 +170,8 @@ reject_line "$tiling_script" "function tileInsertDirection(cursor, rect)"
reject_line "$tiling_script" "showDragOutline(\"insert\"" reject_line "$tiling_script" "showDragOutline(\"insert\""
reject_line "$tiling_script" "KWinComponents.Workspace.showOutline(dragOutlineRect)" reject_line "$tiling_script" "KWinComponents.Workspace.showOutline(dragOutlineRect)"
reject_line "$tiling_script" "KWinComponents.Workspace.hideOutline()" reject_line "$tiling_script" "KWinComponents.Workspace.hideOutline()"
reject_line "$tiling_script" "property int lastWindowRequestSerial: ShellSettings.Settings.dynamicTilingWindowRequestSerial"
reject_line "$tiling_script" "property int lastLayoutRequestSerial: ShellSettings.Settings.dynamicTilingLayoutRequestSerial"
reject_line "$effect_qml" "effect.visible = true" reject_line "$effect_qml" "effect.visible = true"
printf '%s\n' 'dynamic-tiles-motion-ok' printf '%s\n' 'dynamic-tiles-motion-ok'