shift-shell/containments/homescreens/folio/qml/DynamicTilingLayoutMenu.qml

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

298 lines
11 KiB
QML
Raw Normal View History

// 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 }
}
}
}