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