mirror of
https://invent.kde.org/marcoa/shift-shell.git
synced 2026-06-11 08:57:21 +00:00
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.
298 lines
No EOL
11 KiB
QML
298 lines
No EOL
11 KiB
QML
// 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 }
|
|
}
|
|
}
|
|
} |