Compare commits

..

13 commits

Author SHA1 Message Date
6df3bd6ff5 Fix REUSE parsing of theme test 2026-05-19 09:26:51 +02:00
8a9f31aee2 Drop po rename invariant 2026-05-19 09:19:57 +02:00
d095eb97bc Guard convergence shell geometry 2026-05-19 09:18:46 +02:00
3076734300 Raise convergence app drawer 2026-05-19 09:18:46 +02:00
6d766892b5 Unpage convergence quick settings 2026-05-19 09:18:46 +02:00
c38b32042b Fit action drawer surface 2026-05-19 09:18:46 +02:00
ae3319b120 Frame convergence work area 2026-05-19 09:18:46 +02:00
8dd3bfbb07 tools: install local preview QML stubs
openSUSE Tumbleweed's plasma6-nm 6.6.4 does not ship the org.kde.plasma.networkmanagement.cellular QML submodule that SignalStrengthInfo.qml imports unconditionally. Without it the MobileShell QML namespace fails to load and preview boots to a black screen.

Keep a minimal fallback module under tools/preview-stubs and install it only for local .prefix preview installs by default, with SHIFT_INSTALL_PREVIEW_QML_STUBS=AUTO/ON/OFF for explicit control. Add a CTest guard so qmldir entries stay backed by real files and the fallback remains tied to the QML import tree.
2026-05-19 09:18:46 +02:00
8e25b68355 quicksettings: request -symbolic icons for masked tiles
Kirigami.Icon { isMask: true } flattens an SVG to its alpha, so when a quick-setting tile resolves to a colored preferences/scalable glyph the whole rounded background collapses into a solid square. Append -symbolic to the five icon names that collided with colored tiles and add symlink aliases where no real symbolic variant exists yet. Guard the regression with a check in tests/check-shift-icon-theme.sh.
2026-05-19 09:18:46 +02:00
d1e55a47a7 Guard explicit Breeze QQC2 fallback
Keep Breeze QQC2 as an intentional dependency until Shift ships a real QQC2 or widget style. Extend the theme identity test to reject fake Shift style selectors and require the fallback to stay documented in launch paths.
2026-05-19 09:18:46 +02:00
1de58ed486 Add initial Shift desktop theme frame assets
Provide the first real Shift Plasma desktop theme SVGs for panel and widget frames used directly by the shell. Cover dark and light modes, including solid panel backgrounds for opaque panel states.

Extend the theme identity test so the desktop themes cannot regress to metadata-only packages.
2026-05-19 09:18:45 +02:00
85f102a2d1 Document Breeze QQC2 fallback
Mirror the runtime launcher and preview comments in HACKING.md so the remaining org.kde.breeze QQC2 style reference is documented as an explicit dependency, not a Shift theme selector.
2026-05-19 09:18:45 +02:00
65efc37410 Unify Shift theme identity 2026-05-19 09:18:32 +02:00
62 changed files with 1049 additions and 75 deletions

View file

@ -13,7 +13,7 @@ License: CC-BY-SA-4.0
Files: icons/org.shift.icons/**/*.svg
Copyright: 2023 Phosphor Icons
2026 Shift contributors
2026 Marco Allegretti
License: MIT
Comment: SVG glyphs are adapted from Phosphor Icons regular 256px paths for the Shift icon theme.
@ -22,3 +22,13 @@ Files: icons/org.shift.icons/index.theme
Copyright: 2026 Marco Allegretti
License: EUPL-1.2
Files: cursors/shift-cursors/index.theme
Copyright: 2026 Marco Allegretti
License: EUPL-1.2
Files: desktoptheme/shift-dark/metadata.json
desktoptheme/shift-light/metadata.json
kwin/decorations/org.shift.decoration/metadata.json
Copyright: 2026 Marco Allegretti
License: EUPL-1.2

View file

@ -149,8 +149,11 @@ plasma_install_package(${SHIFT_SHELL_PACKAGE_DIR} org.kde.plasma.mobileshell she
add_subdirectory(bin)
add_subdirectory(color-schemes)
add_subdirectory(cursors)
add_subdirectory(desktoptheme)
add_subdirectory(fonts)
add_subdirectory(icons)
add_subdirectory(tools/preview-stubs)
add_subdirectory(wallpapers)
add_subdirectory(components)
add_subdirectory(containments)
@ -185,6 +188,13 @@ kde_configure_git_pre_commit_hook(CHECKS CLANG_FORMAT)
ki18n_install(po)
install(CODE "
file(GLOB _stale_shift_lookandfeel_catalogs \"\$ENV{DESTDIR}${CMAKE_INSTALL_FULL_DATADIR}/locale/*/LC_MESSAGES/plasma_lookandfeel_org.kde.breeze.mobile.mo\")
foreach(_catalog IN LISTS _stale_shift_lookandfeel_catalogs)
file(REMOVE \"\${_catalog}\")
endforeach()
")
if (INSTALL_SYSTEMD_SERVICE)
install(FILES
${CMAKE_CURRENT_SOURCE_DIR}/plasma-mobile.service

View file

@ -201,6 +201,7 @@ export WAYLAND_DISPLAY=shift-kwin
export QT_QPA_PLATFORM=wayland
export QT_QPA_PLATFORMTHEME=KDE
export EGL_PLATFORM=wayland
# Shift does not ship a QQC2 style plugin yet, so this remains an explicit runtime dependency.
export QT_QUICK_CONTROLS_STYLE=org.kde.breeze
export QT_QUICK_CONTROLS_MOBILE=true
export PLASMA_PLATFORM=phone:handset

View file

@ -10,6 +10,7 @@
export QT_QPA_PLATFORMTHEME=KDE
export EGL_PLATFORM=wayland
# Shift does not ship a QQC2 style plugin yet, so this remains an explicit runtime dependency.
export QT_QUICK_CONTROLS_STYLE=org.kde.breeze
export QT_ENABLE_GLYPH_CACHE_WORKAROUND=1
export QT_QUICK_CONTROLS_MOBILE=true

View file

@ -1,5 +1,5 @@
# SPDX-FileCopyrightText: 2026 Shift contributors
# SPDX-License-Identifier: GPL-2.0-or-later
# SPDX-FileCopyrightText: 2026 Marco Allegretti
# SPDX-License-Identifier: EUPL-1.2
install(FILES
ShiftDark.colors

View file

@ -141,7 +141,7 @@ ForegroundVisited=155,89,182
[General]
ColorScheme=ShiftDark
Name=Shift Dark
Name=SHIFT Dark
shadeSortColumn=true
[KDE]

View file

@ -140,7 +140,7 @@ ForegroundVisited=155,89,182
[General]
ColorScheme=ShiftLight
Name=Shift Light
Name=SHIFT Light
shadeSortColumn=true
[KDE]

View file

@ -26,6 +26,14 @@ Item {
readonly property real maximizedQuickSettingsOffset: contentContainerLoader.maximizedQuickSettingsOffset
readonly property bool swipeAreaMoving: swipeAreaBase.moving || swipeAreaPortrait.moving
readonly property bool isConvergence: ShellSettings.Settings.convergenceModeEnabled
readonly property real convergenceFrameThickness: MobileShell.Constants.convergenceWorkspaceFrameThickness
readonly property real convergenceSurfaceTopInset: MobileShell.Constants.topPanelHeight
readonly property real convergenceSurfaceBottomInset: MobileShell.Constants.convergenceDockHeight + convergenceFrameThickness
readonly property real convergenceSurfaceSideInset: 0
readonly property real convergenceSurfaceWidth: Math.max(0, width - convergenceSurfaceSideInset * 2)
readonly property real convergenceSurfaceHeight: Math.max(0, height - convergenceSurfaceTopInset - convergenceSurfaceBottomInset)
readonly property real convergenceNotificationRightMargin: Math.max(convergenceSurfaceSideInset, width - (convergenceSurfaceSideInset + convergenceSurfaceWidth * 0.5))
Kirigami.Theme.colorSet: Kirigami.Theme.Complementary
Kirigami.Theme.inherit: false
@ -109,9 +117,9 @@ Item {
opacity: Math.max(0, Math.min(root.brightnessPressedValue, actionDrawer.offsetResistance / root.minimizedQuickSettingsOffset))
anchors {
topMargin: notificationDrawer.height + 1
leftMargin: actionDrawer.mode == MobileShell.ActionDrawer.Portrait ? 0 : (notificationDrawer.isConvergence ? Kirigami.Units.smallSpacing : 10)
rightMargin: actionDrawer.mode == MobileShell.ActionDrawer.Portrait ? 0 : (notificationDrawer.isConvergence ? parent.width * 0.5 : notificationDrawer.notificationWidget.anchors.rightMargin + Kirigami.Units.gridUnit - notificationDrawer.anchors.leftMargin + 370)
topMargin: notificationDrawer.y + notificationDrawer.height + 1
leftMargin: actionDrawer.mode == MobileShell.ActionDrawer.Portrait ? 0 : (notificationDrawer.isConvergence ? root.convergenceSurfaceSideInset : 10)
rightMargin: actionDrawer.mode == MobileShell.ActionDrawer.Portrait ? 0 : (notificationDrawer.isConvergence ? root.convergenceNotificationRightMargin : notificationDrawer.notificationWidget.anchors.rightMargin + Kirigami.Units.gridUnit - notificationDrawer.anchors.leftMargin + 370)
top: parent.top
left: parent.left
right: parent.right
@ -164,7 +172,7 @@ Item {
NotificationDrawer {
id: notificationDrawer
readonly property bool isConvergence: ShellSettings.Settings.convergenceModeEnabled
readonly property bool isConvergence: root.isConvergence
swipeArea: swipeAreaPortrait
actionDrawer: root.actionDrawer
@ -179,13 +187,13 @@ Item {
top: parent.top
left: parent.left
right: parent.right
topMargin: isConvergence ? Kirigami.Units.smallSpacing : 0
rightMargin: root.actionDrawer.mode == MobileShell.ActionDrawer.Portrait ? 0 : (isConvergence ? parent.width * 0.5 : 360)
leftMargin: actionDrawer.mode == MobileShell.ActionDrawer.Portrait ? 0 : (isConvergence ? Kirigami.Units.smallSpacing : notificationDrawer.minWidthHeight * 0.06)
topMargin: isConvergence ? root.convergenceSurfaceTopInset : 0
rightMargin: root.actionDrawer.mode == MobileShell.ActionDrawer.Portrait ? 0 : (isConvergence ? root.convergenceNotificationRightMargin : 360)
leftMargin: actionDrawer.mode == MobileShell.ActionDrawer.Portrait ? 0 : (isConvergence ? root.convergenceSurfaceSideInset : notificationDrawer.minWidthHeight * 0.06)
}
// In convergence, cap the height so it doesn't stretch full-screen
maximumHeight: isConvergence ? root.height * 0.6 : -1
maximumHeight: isConvergence ? root.convergenceSurfaceHeight * 0.6 : -1
toolButtonsItem: toolButtons
}

View file

@ -27,6 +27,13 @@ Item {
readonly property real minimizedQuickSettingsOffset: height
readonly property real maximizedQuickSettingsOffset: height
readonly property bool isConvergence: ShellSettings.Settings.convergenceModeEnabled
readonly property real convergenceFrameThickness: MobileShell.Constants.convergenceWorkspaceFrameThickness
readonly property real convergenceSurfaceTopInset: MobileShell.Constants.topPanelHeight
readonly property real convergenceSurfaceBottomInset: MobileShell.Constants.convergenceDockHeight + convergenceFrameThickness
readonly property real convergenceSurfaceSideInset: 0
readonly property real convergenceSurfaceWidth: Math.max(0, width - convergenceSurfaceSideInset * 2)
readonly property real convergenceSurfaceHeight: Math.max(0, height - convergenceSurfaceTopInset - convergenceSurfaceBottomInset)
readonly property bool isOnLargeScreen: width > quickSettingsPanel.width * 2.5
readonly property real minWidthHeight: Math.min(root.width, root.height)
readonly property real opacityValue: Math.max(0, Math.min(1, actionDrawer.offsetResistance / root.minimizedQuickSettingsOffset))
@ -44,29 +51,31 @@ Item {
// right sidebar
MobileShell.QuickSettingsPanel {
id: quickSettingsPanel
height: quickSettingsPanel.contentImplicitHeight + quickSettingsPanel.topPadding + quickSettingsPanel.bottomPadding
width: intendedWidth
height: Math.min(quickSettingsPanel.contentImplicitHeight + quickSettingsPanel.topPadding + quickSettingsPanel.bottomPadding, quickSettingsPanel.availableHeight)
width: Math.min(intendedWidth, quickSettingsPanel.availableWidth)
readonly property real columnWidth: 6 * Kirigami.Units.gridUnit
readonly property real availableWidth: root.isConvergence ? root.convergenceSurfaceWidth : intendedWidth
readonly property real availableHeight: root.isConvergence ? root.convergenceSurfaceHeight : root.height
// In convergence mode, use up to 45% of screen width so more
// tiles are visible without pagination.
readonly property int convergenceColumns: Math.min(6, Math.floor(root.width * 0.45 / columnWidth))
readonly property int effectiveColumns: ShellSettings.Settings.convergenceModeEnabled
readonly property int effectiveColumns: root.isConvergence
? Math.max(ShellSettings.Settings.quickSettingsColumns, convergenceColumns)
: ShellSettings.Settings.quickSettingsColumns
readonly property real intendedWidth: (columnWidth * effectiveColumns) + Kirigami.Units.gridUnit
readonly property bool isConvergence: ShellSettings.Settings.convergenceModeEnabled
readonly property bool isConvergence: root.isConvergence
property real offsetRatio: (quickSettingsPanel.height + restingTopMargin) / root.height
readonly property real restingTopMargin: isConvergence ? Kirigami.Units.smallSpacing : 0
readonly property real restingTopMargin: root.isConvergence ? root.convergenceSurfaceTopInset : 0
anchors.topMargin: isConvergence ? restingTopMargin : Math.min(root.actionDrawer.offsetResistance * offsetRatio - quickSettingsPanel.height, 0)
anchors.top: parent.top
anchors.right: parent.right
anchors.rightMargin: isConvergence ? Kirigami.Units.smallSpacing : 0
anchors.rightMargin: isConvergence ? root.convergenceSurfaceSideInset : 0
opacity: isConvergence ? Math.max(0, Math.min(1, root.actionDrawer.offset / root.minimizedQuickSettingsOffset)) : 1
actionDrawer: root.actionDrawer
fullScreenHeight: root.height
fullScreenHeight: quickSettingsPanel.availableHeight
}
}
}

View file

@ -222,18 +222,61 @@ Item {
// Quick settings view
ColumnLayout {
Layout.fillWidth: true
Layout.minimumHeight: rowCount * rowHeight
Layout.minimumHeight: root.isConvergence ? convergenceFlow.implicitHeight : rowCount * rowHeight
opacity: brightnessPressedValue
Flow {
id: convergenceFlow
visible: root.isConvergence
Layout.fillWidth: true
Layout.preferredHeight: implicitHeight
spacing: 0
Repeater {
model: root.quickSettingsModel
delegate: MobileShell.BaseItem {
required property var modelData
readonly property bool __hidden: root.isManagementTile(modelData.settingsCommand)
height: __hidden ? 0 : root.rowHeight
width: __hidden ? 0 : root.columnWidth
visible: !__hidden
padding: Kirigami.Units.smallSpacing
contentItem: QuickSettingsFullDelegate {
restrictedPermissions: actionDrawer.restrictedPermissions
text: modelData.text
status: modelData.status
icon: modelData.icon
enabled: modelData.enabled
settingsCommand: modelData.settingsCommand
toggleFunction: modelData.toggle
onCloseRequested: {
actionDrawer.close();
}
onDetailRequested: (pluginId) => {
detailPopup.show(pluginId);
}
}
}
}
}
SwipeView {
id: swipeView
// we need to clip this view here to prevent the other quick settings pages from being visible
// when fullViewProgress is not less then 1 and the base view is no longer being clipped
clip: true
visible: !root.isConvergence
Layout.fillWidth: true
Layout.preferredHeight: rowCount * rowHeight
Layout.preferredHeight: visible ? rowCount * rowHeight : 0
Layout.maximumHeight: visible ? rowCount * rowHeight : 0
Repeater {
model: Math.ceil(quickSettingsCount / pageSize)
@ -292,7 +335,7 @@ Item {
// Avoid wasting space when not loaded
Layout.maximumHeight: active ? item.implicitHeight : 0
active: swipeView.count > 1 ? true: false
active: !root.isConvergence && swipeView.count > 1 ? true: false
asynchronous: true
sourceComponent: PageIndicator {

View file

@ -33,24 +33,26 @@ MobileShell.BaseItem {
* Implicit height of the contents of the panel.
*/
readonly property real contentImplicitHeight: column.implicitHeight
readonly property bool isConvergence: ShellSettings.Settings.convergenceModeEnabled
readonly property real panelPadding: isConvergence ? Kirigami.Units.smallSpacing : Kirigami.Units.smallSpacing * 4
property alias quickSettings: quickSettingsProxy.contentItem
property alias statusBar: statusBarProxy.contentItem
readonly property double brightnessPressedValue: quickSettings.brightnessPressedValue
// we need extra padding since the background side border is enabled
topPadding: Kirigami.Units.smallSpacing * 4
leftPadding: Kirigami.Units.smallSpacing * 4
rightPadding: Kirigami.Units.smallSpacing * 4
bottomPadding: Kirigami.Units.smallSpacing * 4
topPadding: panelPadding
leftPadding: panelPadding
rightPadding: panelPadding
bottomPadding: panelPadding
Kirigami.Theme.inherit: false
Kirigami.Theme.colorSet: Kirigami.Theme.Window
background: MobileShell.PanelBackground {
anchors.fill: parent
anchors.margins: Kirigami.Units.largeSpacing
panelType: MobileShell.PanelBackground.PanelType.Base
anchors.margins: root.isConvergence ? 0 : Kirigami.Units.largeSpacing
panelType: root.isConvergence ? MobileShell.PanelBackground.PanelType.Flat : MobileShell.PanelBackground.PanelType.Base
radius: root.isConvergence ? 0 : Kirigami.Units.cornerRadius
opacity: brightnessPressedValue
Kirigami.Theme.inherit: false

View file

@ -33,6 +33,8 @@ QtObject {
readonly property real defaultGesturePanelThickness: Kirigami.Units.gridUnit
readonly property real convergenceDockHeight: Kirigami.Units.gridUnit * 3
readonly property real convergenceDockRevealHeight: Kirigami.Units.gridUnit
readonly property real convergenceWorkspaceFrameThickness: Math.max(Kirigami.Units.smallSpacing, Math.round(Kirigami.Units.gridUnit * 0.35))
readonly property real convergenceWorkspaceFrameRadius: Math.max(Kirigami.Units.cornerRadius * 2, Kirigami.Units.largeSpacing)
readonly property real navigationPanelThickness: {
if (!ShellSettings.Settings.navigationPanelEnabled) {

View file

@ -5,6 +5,7 @@ import QtQuick
import QtQuick.Window
import QtQuick.Layouts
import QtQuick.Effects
import QtQuick.Shapes 1.8
import Qt5Compat.GraphicalEffects
import org.kde.kirigami as Kirigami
@ -430,8 +431,14 @@ ContainmentItem {
homeScreen: folioHomeScreen
readonly property real popupWidth: Math.min(Kirigami.Units.gridUnit * 28, parent.width * 0.5)
readonly property real popupHeight: Math.min(Kirigami.Units.gridUnit * 32, parent.height * 0.7)
readonly property real dockHeight: MobileShell.Constants.convergenceDockHeight
readonly property real popupTopY: MobileShell.Constants.topPanelHeight
+ MobileShell.Constants.convergenceWorkspaceFrameThickness
+ Kirigami.Units.smallSpacing
readonly property real popupBottomY: parent.height
- dockHeight
- MobileShell.Constants.convergenceWorkspaceFrameThickness
readonly property real popupHeight: Math.max(0, popupBottomY - popupTopY)
width: popupWidth
height: popupHeight
@ -443,7 +450,7 @@ ContainmentItem {
x: Kirigami.Units.smallSpacing
y: (opacity > 0)
? parent.height - dockHeight - popupHeight - Kirigami.Units.smallSpacing + animationY
? popupTopY + animationY
: parent.height
headerHeight: Math.round(Kirigami.Units.gridUnit * 3)
@ -869,6 +876,64 @@ ContainmentItem {
onHomeTriggered: root.homeAction()
contentItem: Item {
Item {
id: workspaceFrame
anchors.fill: parent
visible: ShellSettings.Settings.convergenceModeEnabled && !ShellSettings.Settings.gamingModeEnabled
z: -1
readonly property real frameThickness: MobileShell.Constants.convergenceWorkspaceFrameThickness
readonly property real frameRadius: Math.min(MobileShell.Constants.convergenceWorkspaceFrameRadius, Math.max(0, Math.min(workAreaWidth, workAreaHeight) / 2))
readonly property real topReservedHeight: MobileShell.Constants.topPanelHeight
readonly property real bottomReservedHeight: MobileShell.Constants.convergenceDockHeight
readonly property real workAreaX: frameThickness
readonly property real workAreaY: topReservedHeight + frameThickness
readonly property real workAreaWidth: Math.max(0, width - frameThickness * 2)
readonly property real workAreaHeight: Math.max(0, height - topReservedHeight - bottomReservedHeight - frameThickness * 2)
readonly property color frameColor: Kirigami.Theme.backgroundColor
readonly property color edgeColor: Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g, Kirigami.Theme.textColor.b, 0.1)
Kirigami.Theme.inherit: false
Kirigami.Theme.colorSet: Kirigami.Theme.Window
Shape {
anchors.fill: parent
ShapePath {
fillColor: workspaceFrame.frameColor
fillRule: ShapePath.OddEvenFill
strokeWidth: 0
startX: 0
startY: workspaceFrame.topReservedHeight
PathLine { x: workspaceFrame.width; y: workspaceFrame.topReservedHeight }
PathLine { x: workspaceFrame.width; y: workspaceFrame.height - workspaceFrame.bottomReservedHeight }
PathLine { x: 0; y: workspaceFrame.height - workspaceFrame.bottomReservedHeight }
PathLine { x: 0; y: workspaceFrame.topReservedHeight }
PathMove { x: workspaceFrame.workAreaX + workspaceFrame.frameRadius; y: workspaceFrame.workAreaY }
PathLine { x: workspaceFrame.workAreaX + workspaceFrame.workAreaWidth - workspaceFrame.frameRadius; y: workspaceFrame.workAreaY }
PathArc { x: workspaceFrame.workAreaX + workspaceFrame.workAreaWidth; y: workspaceFrame.workAreaY + workspaceFrame.frameRadius; radiusX: workspaceFrame.frameRadius; radiusY: workspaceFrame.frameRadius }
PathLine { x: workspaceFrame.workAreaX + workspaceFrame.workAreaWidth; y: workspaceFrame.workAreaY + workspaceFrame.workAreaHeight - workspaceFrame.frameRadius }
PathArc { x: workspaceFrame.workAreaX + workspaceFrame.workAreaWidth - workspaceFrame.frameRadius; y: workspaceFrame.workAreaY + workspaceFrame.workAreaHeight; radiusX: workspaceFrame.frameRadius; radiusY: workspaceFrame.frameRadius }
PathLine { x: workspaceFrame.workAreaX + workspaceFrame.frameRadius; y: workspaceFrame.workAreaY + workspaceFrame.workAreaHeight }
PathArc { x: workspaceFrame.workAreaX; y: workspaceFrame.workAreaY + workspaceFrame.workAreaHeight - workspaceFrame.frameRadius; radiusX: workspaceFrame.frameRadius; radiusY: workspaceFrame.frameRadius }
PathLine { x: workspaceFrame.workAreaX; y: workspaceFrame.workAreaY + workspaceFrame.frameRadius }
PathArc { x: workspaceFrame.workAreaX + workspaceFrame.frameRadius; y: workspaceFrame.workAreaY; radiusX: workspaceFrame.frameRadius; radiusY: workspaceFrame.frameRadius }
}
}
Rectangle {
x: workspaceFrame.workAreaX
y: workspaceFrame.workAreaY
width: workspaceFrame.workAreaWidth
height: workspaceFrame.workAreaHeight
radius: workspaceFrame.frameRadius
color: "transparent"
border.width: 1
border.color: workspaceFrame.edgeColor
}
}
// homescreen component
FolioHomeScreen {

View file

@ -76,6 +76,9 @@ ContainmentItem {
}
readonly property real panelHeight: gamingMode ? 0 : MobileShell.Constants.topPanelHeight
readonly property real convergenceWorkspaceFrameThickness: ShellSettings.Settings.convergenceModeEnabled && !gamingMode
? MobileShell.Constants.convergenceWorkspaceFrameThickness
: 0
onPanelHeightChanged: setWindowProperties()
function setWindowProperties() {
@ -150,13 +153,13 @@ ContainmentItem {
visible: ShellSettings.Settings.convergenceModeEnabled && !ShellSettings.Settings.gamingModeEnabled
color: "transparent"
flags: Qt.FramelessWindowHint | Qt.WindowTransparentForInput
height: Math.max(1, root.panelHeight)
height: Math.max(1, root.panelHeight + root.convergenceWorkspaceFrameThickness)
width: 1
LayerShell.Window.scope: "topbar-space"
LayerShell.Window.layer: LayerShell.Window.LayerBottom
LayerShell.Window.anchors: LayerShell.Window.AnchorTop | LayerShell.Window.AnchorLeft | LayerShell.Window.AnchorRight
LayerShell.Window.exclusionZone: Math.max(1, root.panelHeight)
LayerShell.Window.exclusionZone: Math.max(1, root.panelHeight + root.convergenceWorkspaceFrameThickness)
LayerShell.Window.keyboardInteractivity: LayerShell.Window.KeyboardInteractivityNone
}

View file

@ -41,6 +41,9 @@ ContainmentItem {
readonly property bool gamingMode: ShellSettings.Settings.gamingModeEnabled
readonly property real navigationPanelHeight: gamingMode ? 0 : MobileShell.Constants.navigationPanelThickness
readonly property real convergenceWorkspaceFrameThickness: ShellSettings.Settings.convergenceModeEnabled && !gamingMode
? MobileShell.Constants.convergenceWorkspaceFrameThickness
: 0
onNavigationPanelHeightChanged: setWindowProperties()
readonly property real intendedWindowThickness: navigationPanelHeight
@ -166,13 +169,43 @@ ContainmentItem {
color: "transparent"
flags: Qt.FramelessWindowHint | Qt.WindowTransparentForInput
// height is set by layer-shell anchoring; provide a fallback.
height: MobileShell.Constants.convergenceDockHeight
height: MobileShell.Constants.convergenceDockHeight + root.convergenceWorkspaceFrameThickness
width: 1 // layer-shell stretches it via AnchorLeft|AnchorRight
LayerShell.Window.scope: "dock-space"
LayerShell.Window.layer: LayerShell.Window.LayerBottom
LayerShell.Window.anchors: LayerShell.Window.AnchorBottom | LayerShell.Window.AnchorLeft | LayerShell.Window.AnchorRight
LayerShell.Window.exclusionZone: MobileShell.Constants.convergenceDockHeight
LayerShell.Window.exclusionZone: MobileShell.Constants.convergenceDockHeight + root.convergenceWorkspaceFrameThickness
LayerShell.Window.keyboardInteractivity: LayerShell.Window.KeyboardInteractivityNone
}
Window {
id: leftWorkspaceFrameReserver
visible: root.convergenceWorkspaceFrameThickness > 0
color: "transparent"
flags: Qt.FramelessWindowHint | Qt.WindowTransparentForInput
width: Math.max(1, root.convergenceWorkspaceFrameThickness)
height: 1
LayerShell.Window.scope: "workspace-frame-left"
LayerShell.Window.layer: LayerShell.Window.LayerBottom
LayerShell.Window.anchors: LayerShell.Window.AnchorLeft | LayerShell.Window.AnchorTop | LayerShell.Window.AnchorBottom
LayerShell.Window.exclusionZone: Math.max(1, root.convergenceWorkspaceFrameThickness)
LayerShell.Window.keyboardInteractivity: LayerShell.Window.KeyboardInteractivityNone
}
Window {
id: rightWorkspaceFrameReserver
visible: root.convergenceWorkspaceFrameThickness > 0
color: "transparent"
flags: Qt.FramelessWindowHint | Qt.WindowTransparentForInput
width: Math.max(1, root.convergenceWorkspaceFrameThickness)
height: 1
LayerShell.Window.scope: "workspace-frame-right"
LayerShell.Window.layer: LayerShell.Window.LayerBottom
LayerShell.Window.anchors: LayerShell.Window.AnchorRight | LayerShell.Window.AnchorTop | LayerShell.Window.AnchorBottom
LayerShell.Window.exclusionZone: Math.max(1, root.convergenceWorkspaceFrameThickness)
LayerShell.Window.keyboardInteractivity: LayerShell.Window.KeyboardInteractivityNone
}

7
cursors/CMakeLists.txt Normal file
View file

@ -0,0 +1,7 @@
# SPDX-FileCopyrightText: 2026 Marco Allegretti
# SPDX-License-Identifier: EUPL-1.2
install(
DIRECTORY shift-cursors
DESTINATION ${KDE_INSTALL_ICONDIR}
)

View file

@ -0,0 +1,4 @@
[Icon Theme]
Name=SHIFT
Comment=SHIFT cursor theme
Inherits=breeze_cursors

View file

@ -0,0 +1,19 @@
# SPDX-FileCopyrightText: 2026 Marco Allegretti
# SPDX-License-Identifier: EUPL-1.2
install(
DIRECTORY shift-dark shift-light
DESTINATION ${KDE_INSTALL_DATADIR}/plasma/desktoptheme
)
install(
FILES ${CMAKE_SOURCE_DIR}/color-schemes/ShiftDark.colors
DESTINATION ${KDE_INSTALL_DATADIR}/plasma/desktoptheme/shift-dark
RENAME colors
)
install(
FILES ${CMAKE_SOURCE_DIR}/color-schemes/ShiftLight.colors
DESTINATION ${KDE_INSTALL_DATADIR}/plasma/desktoptheme/shift-light
RENAME colors
)

View file

@ -0,0 +1,17 @@
{
"KPlugin": {
"Authors": [
{
"Name": "Marco Allegretti"
}
],
"Category": "",
"Description": "SHIFT dark workspace theme",
"EnabledByDefault": true,
"Id": "shift-dark",
"License": "EUPL-1.2",
"Name": "SHIFT Dark",
"Website": "https://invent.kde.org/marcoa/shift-shell"
},
"X-Plasma-API": "5.0"
}

View file

@ -0,0 +1,8 @@
# SPDX-FileCopyrightText: 2026 Marco Allegretti
# SPDX-License-Identifier: EUPL-1.2
[Settings]
FallbackTheme=breeze-dark
[AdaptiveTransparency]
enabled=true

View file

@ -0,0 +1,23 @@
<!-- SPDX-FileCopyrightText: 2026 Marco Allegretti -->
<!-- SPDX-License-Identifier: EUPL-1.2 -->
<svg width="64" height="64" viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg">
<rect id="hint-tile-center" x="0" y="0" width="2" height="2" fill="#2eb8a8" opacity="0"/>
<rect id="center" x="8" y="8" width="48" height="48" fill="#0e1118"/>
<rect id="top" x="8" y="0" width="48" height="8" fill="#0e1118"/>
<rect id="bottom" x="8" y="56" width="48" height="8" fill="#0e1118"/>
<rect id="left" x="0" y="8" width="8" height="48" fill="#0e1118"/>
<rect id="right" x="56" y="8" width="8" height="48" fill="#0e1118"/>
<rect id="topleft" x="0" y="0" width="8" height="8" fill="#0e1118"/>
<rect id="topright" x="56" y="0" width="8" height="8" fill="#0e1118"/>
<rect id="bottomleft" x="0" y="56" width="8" height="8" fill="#0e1118"/>
<rect id="bottomright" x="56" y="56" width="8" height="8" fill="#0e1118"/>
<rect id="hint-top-margin" x="30" y="0" width="4" height="8" fill="#2eb8a8" opacity="0"/>
<rect id="hint-bottom-margin" x="30" y="56" width="4" height="8" fill="#2eb8a8" opacity="0"/>
<rect id="hint-left-margin" x="0" y="30" width="8" height="4" fill="#2eb8a8" opacity="0"/>
<rect id="hint-right-margin" x="56" y="30" width="8" height="4" fill="#2eb8a8" opacity="0"/>
<rect id="thick-center" x="12" y="12" width="40" height="40" fill="#0e1118"/>
<rect id="thick-hint-top-margin" x="30" y="0" width="4" height="12" fill="#2eb8a8" opacity="0"/>
<rect id="thick-hint-bottom-margin" x="30" y="52" width="4" height="12" fill="#2eb8a8" opacity="0"/>
<rect id="thick-hint-left-margin" x="0" y="30" width="12" height="4" fill="#2eb8a8" opacity="0"/>
<rect id="thick-hint-right-margin" x="52" y="30" width="12" height="4" fill="#2eb8a8" opacity="0"/>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View file

@ -0,0 +1,22 @@
<!-- SPDX-FileCopyrightText: 2026 Marco Allegretti -->
<!-- SPDX-License-Identifier: EUPL-1.2 -->
<svg width="64" height="64" viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg">
<rect id="hint-tile-center" x="0" y="0" width="2" height="2" fill="#2eb8a8" opacity="0"/>
<rect id="center" x="8" y="8" width="48" height="48" rx="3" fill="#171c29"/>
<rect id="top" x="8" y="0" width="48" height="8" fill="#171c29"/>
<rect id="bottom" x="8" y="56" width="48" height="8" fill="#171c29"/>
<rect id="left" x="0" y="8" width="8" height="48" fill="#171c29"/>
<rect id="right" x="56" y="8" width="8" height="48" fill="#171c29"/>
<rect id="topleft" x="0" y="0" width="8" height="8" fill="#171c29"/>
<rect id="topright" x="56" y="0" width="8" height="8" fill="#171c29"/>
<rect id="bottomleft" x="0" y="56" width="8" height="8" fill="#171c29"/>
<rect id="bottomright" x="56" y="56" width="8" height="8" fill="#171c29"/>
<rect id="hint-top-margin" x="30" y="0" width="4" height="8" fill="#2eb8a8" opacity="0"/>
<rect id="hint-bottom-margin" x="30" y="56" width="4" height="8" fill="#2eb8a8" opacity="0"/>
<rect id="hint-left-margin" x="0" y="30" width="8" height="4" fill="#2eb8a8" opacity="0"/>
<rect id="hint-right-margin" x="56" y="30" width="8" height="4" fill="#2eb8a8" opacity="0"/>
<rect id="hint-top-inset" x="30" y="8" width="4" height="1" fill="#2eb8a8" opacity="0"/>
<rect id="hint-bottom-inset" x="30" y="55" width="4" height="1" fill="#2eb8a8" opacity="0"/>
<rect id="hint-left-inset" x="8" y="30" width="1" height="4" fill="#2eb8a8" opacity="0"/>
<rect id="hint-right-inset" x="55" y="30" width="1" height="4" fill="#2eb8a8" opacity="0"/>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View file

@ -0,0 +1,33 @@
<!-- SPDX-FileCopyrightText: 2026 Marco Allegretti -->
<!-- SPDX-License-Identifier: EUPL-1.2 -->
<svg width="64" height="64" viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg">
<defs>
<style id="current-color-scheme" type="text/css">
.ColorScheme-Background { color: #0e1118; }
.ColorScheme-Highlight { color: #2eb8a8; }
</style>
</defs>
<rect id="hint-tile-center" x="0" y="0" width="2" height="2" fill="#2eb8a8" opacity="0"/>
<rect id="center" x="8" y="8" width="48" height="48" rx="0" fill="#111622"/>
<rect id="top" x="8" y="0" width="48" height="8" fill="#111622"/>
<rect id="bottom" x="8" y="56" width="48" height="8" fill="#111622"/>
<rect id="left" x="0" y="8" width="8" height="48" fill="#111622"/>
<rect id="right" x="56" y="8" width="8" height="48" fill="#111622"/>
<rect id="topleft" x="0" y="0" width="8" height="8" fill="#111622"/>
<rect id="topright" x="56" y="0" width="8" height="8" fill="#111622"/>
<rect id="bottomleft" x="0" y="56" width="8" height="8" fill="#111622"/>
<rect id="bottomright" x="56" y="56" width="8" height="8" fill="#111622"/>
<rect id="hint-top-margin" x="30" y="0" width="4" height="8" fill="#2eb8a8" opacity="0"/>
<rect id="hint-bottom-margin" x="30" y="56" width="4" height="8" fill="#2eb8a8" opacity="0"/>
<rect id="hint-left-margin" x="0" y="30" width="8" height="4" fill="#2eb8a8" opacity="0"/>
<rect id="hint-right-margin" x="56" y="30" width="8" height="4" fill="#2eb8a8" opacity="0"/>
<rect id="hint-top-inset" x="30" y="8" width="4" height="1" fill="#2eb8a8" opacity="0"/>
<rect id="hint-bottom-inset" x="30" y="55" width="4" height="1" fill="#2eb8a8" opacity="0"/>
<rect id="hint-left-inset" x="8" y="30" width="1" height="4" fill="#2eb8a8" opacity="0"/>
<rect id="hint-right-inset" x="55" y="30" width="1" height="4" fill="#2eb8a8" opacity="0"/>
<rect id="thick-center" x="12" y="12" width="40" height="40" fill="#111622"/>
<rect id="thick-hint-top-margin" x="30" y="0" width="4" height="12" fill="#2eb8a8" opacity="0"/>
<rect id="thick-hint-bottom-margin" x="30" y="52" width="4" height="12" fill="#2eb8a8" opacity="0"/>
<rect id="thick-hint-left-margin" x="0" y="30" width="12" height="4" fill="#2eb8a8" opacity="0"/>
<rect id="thick-hint-right-margin" x="52" y="30" width="12" height="4" fill="#2eb8a8" opacity="0"/>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

View file

@ -0,0 +1,18 @@
<!-- SPDX-FileCopyrightText: 2026 Marco Allegretti -->
<!-- SPDX-License-Identifier: EUPL-1.2 -->
<svg width="64" height="64" viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg">
<rect id="hint-tile-center" x="0" y="0" width="2" height="2" fill="#2eb8a8" opacity="0"/>
<rect id="center" x="8" y="8" width="48" height="48" fill="#0e1118" opacity="0.72"/>
<rect id="top" x="8" y="0" width="48" height="8" fill="#0e1118" opacity="0.72"/>
<rect id="bottom" x="8" y="56" width="48" height="8" fill="#0e1118" opacity="0.72"/>
<rect id="left" x="0" y="8" width="8" height="48" fill="#0e1118" opacity="0.72"/>
<rect id="right" x="56" y="8" width="8" height="48" fill="#0e1118" opacity="0.72"/>
<rect id="topleft" x="0" y="0" width="8" height="8" fill="#0e1118" opacity="0.72"/>
<rect id="topright" x="56" y="0" width="8" height="8" fill="#0e1118" opacity="0.72"/>
<rect id="bottomleft" x="0" y="56" width="8" height="8" fill="#0e1118" opacity="0.72"/>
<rect id="bottomright" x="56" y="56" width="8" height="8" fill="#0e1118" opacity="0.72"/>
<rect id="hint-top-margin" x="30" y="0" width="4" height="8" fill="#2eb8a8" opacity="0"/>
<rect id="hint-bottom-margin" x="30" y="56" width="4" height="8" fill="#2eb8a8" opacity="0"/>
<rect id="hint-left-margin" x="0" y="30" width="8" height="4" fill="#2eb8a8" opacity="0"/>
<rect id="hint-right-margin" x="56" y="30" width="8" height="4" fill="#2eb8a8" opacity="0"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View file

@ -0,0 +1,17 @@
{
"KPlugin": {
"Authors": [
{
"Name": "Marco Allegretti"
}
],
"Category": "",
"Description": "SHIFT light workspace theme",
"EnabledByDefault": true,
"Id": "shift-light",
"License": "EUPL-1.2",
"Name": "SHIFT Light",
"Website": "https://invent.kde.org/marcoa/shift-shell"
},
"X-Plasma-API": "5.0"
}

View file

@ -0,0 +1,14 @@
# SPDX-FileCopyrightText: 2026 Marco Allegretti
# SPDX-License-Identifier: EUPL-1.2
[Settings]
FallbackTheme=breeze-light
[ContrastEffect]
enabled=true
contrast=0.2
intensity=2.0
saturation=1.7
[AdaptiveTransparency]
enabled=true

View file

@ -0,0 +1,23 @@
<!-- SPDX-FileCopyrightText: 2026 Marco Allegretti -->
<!-- SPDX-License-Identifier: EUPL-1.2 -->
<svg width="64" height="64" viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg">
<rect id="hint-tile-center" x="0" y="0" width="2" height="2" fill="#0f766e" opacity="0"/>
<rect id="center" x="8" y="8" width="48" height="48" fill="#ffffff"/>
<rect id="top" x="8" y="0" width="48" height="8" fill="#ffffff"/>
<rect id="bottom" x="8" y="56" width="48" height="8" fill="#ffffff"/>
<rect id="left" x="0" y="8" width="8" height="48" fill="#ffffff"/>
<rect id="right" x="56" y="8" width="8" height="48" fill="#ffffff"/>
<rect id="topleft" x="0" y="0" width="8" height="8" fill="#ffffff"/>
<rect id="topright" x="56" y="0" width="8" height="8" fill="#ffffff"/>
<rect id="bottomleft" x="0" y="56" width="8" height="8" fill="#ffffff"/>
<rect id="bottomright" x="56" y="56" width="8" height="8" fill="#ffffff"/>
<rect id="hint-top-margin" x="30" y="0" width="4" height="8" fill="#0f766e" opacity="0"/>
<rect id="hint-bottom-margin" x="30" y="56" width="4" height="8" fill="#0f766e" opacity="0"/>
<rect id="hint-left-margin" x="0" y="30" width="8" height="4" fill="#0f766e" opacity="0"/>
<rect id="hint-right-margin" x="56" y="30" width="8" height="4" fill="#0f766e" opacity="0"/>
<rect id="thick-center" x="12" y="12" width="40" height="40" fill="#ffffff"/>
<rect id="thick-hint-top-margin" x="30" y="0" width="4" height="12" fill="#0f766e" opacity="0"/>
<rect id="thick-hint-bottom-margin" x="30" y="52" width="4" height="12" fill="#0f766e" opacity="0"/>
<rect id="thick-hint-left-margin" x="0" y="30" width="12" height="4" fill="#0f766e" opacity="0"/>
<rect id="thick-hint-right-margin" x="52" y="30" width="12" height="4" fill="#0f766e" opacity="0"/>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View file

@ -0,0 +1,22 @@
<!-- SPDX-FileCopyrightText: 2026 Marco Allegretti -->
<!-- SPDX-License-Identifier: EUPL-1.2 -->
<svg width="64" height="64" viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg">
<rect id="hint-tile-center" x="0" y="0" width="2" height="2" fill="#0f766e" opacity="0"/>
<rect id="center" x="8" y="8" width="48" height="48" rx="3" fill="#ffffff"/>
<rect id="top" x="8" y="0" width="48" height="8" fill="#ffffff"/>
<rect id="bottom" x="8" y="56" width="48" height="8" fill="#ffffff"/>
<rect id="left" x="0" y="8" width="8" height="48" fill="#ffffff"/>
<rect id="right" x="56" y="8" width="8" height="48" fill="#ffffff"/>
<rect id="topleft" x="0" y="0" width="8" height="8" fill="#ffffff"/>
<rect id="topright" x="56" y="0" width="8" height="8" fill="#ffffff"/>
<rect id="bottomleft" x="0" y="56" width="8" height="8" fill="#ffffff"/>
<rect id="bottomright" x="56" y="56" width="8" height="8" fill="#ffffff"/>
<rect id="hint-top-margin" x="30" y="0" width="4" height="8" fill="#0f766e" opacity="0"/>
<rect id="hint-bottom-margin" x="30" y="56" width="4" height="8" fill="#0f766e" opacity="0"/>
<rect id="hint-left-margin" x="0" y="30" width="8" height="4" fill="#0f766e" opacity="0"/>
<rect id="hint-right-margin" x="56" y="30" width="8" height="4" fill="#0f766e" opacity="0"/>
<rect id="hint-top-inset" x="30" y="8" width="4" height="1" fill="#0f766e" opacity="0"/>
<rect id="hint-bottom-inset" x="30" y="55" width="4" height="1" fill="#0f766e" opacity="0"/>
<rect id="hint-left-inset" x="8" y="30" width="1" height="4" fill="#0f766e" opacity="0"/>
<rect id="hint-right-inset" x="55" y="30" width="1" height="4" fill="#0f766e" opacity="0"/>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View file

@ -0,0 +1,27 @@
<!-- SPDX-FileCopyrightText: 2026 Marco Allegretti -->
<!-- SPDX-License-Identifier: EUPL-1.2 -->
<svg width="64" height="64" viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg">
<rect id="hint-tile-center" x="0" y="0" width="2" height="2" fill="#0f766e" opacity="0"/>
<rect id="center" x="8" y="8" width="48" height="48" fill="#f6f8fb"/>
<rect id="top" x="8" y="0" width="48" height="8" fill="#f6f8fb"/>
<rect id="bottom" x="8" y="56" width="48" height="8" fill="#f6f8fb"/>
<rect id="left" x="0" y="8" width="8" height="48" fill="#f6f8fb"/>
<rect id="right" x="56" y="8" width="8" height="48" fill="#f6f8fb"/>
<rect id="topleft" x="0" y="0" width="8" height="8" fill="#f6f8fb"/>
<rect id="topright" x="56" y="0" width="8" height="8" fill="#f6f8fb"/>
<rect id="bottomleft" x="0" y="56" width="8" height="8" fill="#f6f8fb"/>
<rect id="bottomright" x="56" y="56" width="8" height="8" fill="#f6f8fb"/>
<rect id="hint-top-margin" x="30" y="0" width="4" height="8" fill="#0f766e" opacity="0"/>
<rect id="hint-bottom-margin" x="30" y="56" width="4" height="8" fill="#0f766e" opacity="0"/>
<rect id="hint-left-margin" x="0" y="30" width="8" height="4" fill="#0f766e" opacity="0"/>
<rect id="hint-right-margin" x="56" y="30" width="8" height="4" fill="#0f766e" opacity="0"/>
<rect id="hint-top-inset" x="30" y="8" width="4" height="1" fill="#0f766e" opacity="0"/>
<rect id="hint-bottom-inset" x="30" y="55" width="4" height="1" fill="#0f766e" opacity="0"/>
<rect id="hint-left-inset" x="8" y="30" width="1" height="4" fill="#0f766e" opacity="0"/>
<rect id="hint-right-inset" x="55" y="30" width="1" height="4" fill="#0f766e" opacity="0"/>
<rect id="thick-center" x="12" y="12" width="40" height="40" fill="#f6f8fb"/>
<rect id="thick-hint-top-margin" x="30" y="0" width="4" height="12" fill="#0f766e" opacity="0"/>
<rect id="thick-hint-bottom-margin" x="30" y="52" width="4" height="12" fill="#0f766e" opacity="0"/>
<rect id="thick-hint-left-margin" x="0" y="30" width="12" height="4" fill="#0f766e" opacity="0"/>
<rect id="thick-hint-right-margin" x="52" y="30" width="12" height="4" fill="#0f766e" opacity="0"/>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

View file

@ -0,0 +1,18 @@
<!-- SPDX-FileCopyrightText: 2026 Marco Allegretti -->
<!-- SPDX-License-Identifier: EUPL-1.2 -->
<svg width="64" height="64" viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg">
<rect id="hint-tile-center" x="0" y="0" width="2" height="2" fill="#0f766e" opacity="0"/>
<rect id="center" x="8" y="8" width="48" height="48" fill="#ffffff" opacity="0.78"/>
<rect id="top" x="8" y="0" width="48" height="8" fill="#ffffff" opacity="0.78"/>
<rect id="bottom" x="8" y="56" width="48" height="8" fill="#ffffff" opacity="0.78"/>
<rect id="left" x="0" y="8" width="8" height="48" fill="#ffffff" opacity="0.78"/>
<rect id="right" x="56" y="8" width="8" height="48" fill="#ffffff" opacity="0.78"/>
<rect id="topleft" x="0" y="0" width="8" height="8" fill="#ffffff" opacity="0.78"/>
<rect id="topright" x="56" y="0" width="8" height="8" fill="#ffffff" opacity="0.78"/>
<rect id="bottomleft" x="0" y="56" width="8" height="8" fill="#ffffff" opacity="0.78"/>
<rect id="bottomright" x="56" y="56" width="8" height="8" fill="#ffffff" opacity="0.78"/>
<rect id="hint-top-margin" x="30" y="0" width="4" height="8" fill="#0f766e" opacity="0"/>
<rect id="hint-bottom-margin" x="30" y="56" width="4" height="8" fill="#0f766e" opacity="0"/>
<rect id="hint-left-margin" x="0" y="30" width="8" height="4" fill="#0f766e" opacity="0"/>
<rect id="hint-right-margin" x="56" y="30" width="8" height="4" fill="#0f766e" opacity="0"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View file

@ -1,5 +1,5 @@
# SPDX-FileCopyrightText: 2026 SHIFT Contributors
# SPDX-License-Identifier: GPL-2.0-or-later
# SPDX-FileCopyrightText: 2026 Marco Allegretti
# SPDX-License-Identifier: EUPL-1.2
# Install bundled font files (if any are present) into the system font dir
# so the shell session can render the configured default font even on

View file

@ -1,6 +1,6 @@
<!--
SPDX-FileCopyrightText: 2026 SHIFT Contributors
SPDX-License-Identifier: GPL-2.0-or-later
SPDX-FileCopyrightText: 2026 Marco Allegretti
SPDX-License-Identifier: EUPL-1.2
-->
# Shift bundled fonts

View file

@ -0,0 +1 @@
dialog-information.svg

View file

@ -0,0 +1 @@
preferences-desktop-display-randr.svg

View file

@ -0,0 +1 @@
redshift-status-on.svg

View file

@ -1,6 +1,6 @@
[Icon Theme]
Name=Shift
Comment=Shift icon theme
Name=SHIFT
Comment=SHIFT icon theme
Inherits=hicolor
Example=folder
DisplayDepth=32

View file

@ -1,5 +1,5 @@
<!-- SPDX-FileCopyrightText: 2026 Shift contributors -->
<!-- SPDX-License-Identifier: GPL-2.0-or-later -->
<!-- SPDX-FileCopyrightText: 2026 Marco Allegretti -->
<!-- SPDX-License-Identifier: EUPL-1.2 -->
<svg viewBox="0 0 96 96" xmlns="http://www.w3.org/2000/svg">
<!-- top-left circle -->
<circle cx="16" cy="16" r="16" fill="#FBFBFB"/>

Before

Width:  |  Height:  |  Size: 509 B

After

Width:  |  Height:  |  Size: 499 B

View file

@ -40,9 +40,9 @@ int main(int argc, char *argv[])
QStringLiteral(PLASMA_MOBILE_VERSION_STRING),
QStringLiteral(""),
KAboutLicense::GPL,
i18n("© 2023-2026 Plasma Mobile and SHIFT contributors"));
i18n("© 2023-2026 Plasma Mobile contributors and Marco Allegretti"));
aboutData.addAuthor(i18n("Devin Lin"), QString(), QStringLiteral("devin@kde.org"));
aboutData.addAuthor(i18n("SHIFT Contributors"));
aboutData.addAuthor(i18n("Marco Allegretti"));
aboutData.setBugAddress("https://invent.kde.org/marcoa/shift-shell/-/issues");
KAboutData::setApplicationData(aboutData);

View file

@ -3,12 +3,12 @@
"KPlugin": {
"Authors": [
{
"Name": "SHIFT Contributors"
"Name": "Marco Allegretti"
}
],
"Description": "Shift window decoration",
"Description": "SHIFT window decoration",
"Id": "org.shift.decoration",
"License": "GPL-2.0-or-later",
"Name": "Shift"
"License": "EUPL-1.2",
"Name": "SHIFT"
}
}

View file

@ -9,7 +9,7 @@ widgetStyle=Breeze
[kdeglobals][General]
ColorScheme=ShiftDark
Name=Shift Dark
Name=SHIFT Dark
font=Atkinson Hyperlegible Next,11,-1,5,400,0,0,0,0,0,0,0,0,0,0,1
fixed=Atkinson Hyperlegible Mono,11,-1,5,400,0,0,0,0,0,0,0,0,0,0,1
smallestReadableFont=Atkinson Hyperlegible Next,9,-1,5,400,0,0,0,0,0,0,0,0,0,0,1
@ -24,14 +24,11 @@ Image=SHIFT
# Set default cursor theme
[kcminputrc][Mouse]
cursorTheme=breeze_cursors
cursorTheme=shift-cursors
[plasmarc][Theme]
name=breeze-dark
name=shift-dark
[kwinrc][org.kde.kdecoration2]
library=org.kde.kwin.aurorae
theme=org.shift.decoration
[KSplash]
Theme=org.kde.Breeze

View file

@ -138,7 +138,7 @@ Item {
ActionButton {
iconSource: "system-reboot"
text: i18nd("plasma_lookandfeel_org.kde.lookandfeel", "Restart")
text: i18nd("plasma_lookandfeel_org.shift.mobile", "Restart")
onClicked: {
closeAnim.closeToBlack = true;
closeAnim.execute(root.rebootRequested);
@ -147,7 +147,7 @@ Item {
ActionButton {
iconSource: "system-shutdown"
text: i18nd("plasma_lookandfeel_org.kde.lookandfeel", "Shut Down")
text: i18nd("plasma_lookandfeel_org.shift.mobile", "Shut Down")
onClicked: {
closeAnim.closeToBlack = true;
closeAnim.execute(root.haltRequested);
@ -156,7 +156,7 @@ Item {
ActionButton {
iconSource: "system-log-out"
text: i18nd("plasma_lookandfeel_org.kde.lookandfeel", "Log Out")
text: i18nd("plasma_lookandfeel_org.shift.mobile", "Log Out")
visible: ShellSettings.Settings.allowLogout
onClicked: {
closeAnim.closeToBlack = true;
@ -172,7 +172,7 @@ Item {
horizontalCenter: parent.horizontalCenter
}
iconSource: "dialog-cancel"
text: i18nd("plasma_lookandfeel_org.kde.lookandfeel", "Cancel")
text: i18nd("plasma_lookandfeel_org.shift.mobile", "Cancel")
onClicked: {
closeAnim.closeToBlack = false;
closeAnim.execute(root.cancelRequested);

View file

@ -0,0 +1,98 @@
/*
SPDX-FileCopyrightText: 2026 Marco Allegretti
SPDX-License-Identifier: EUPL-1.2
*/
import QtQuick
import org.kde.kirigami as Kirigami
Rectangle {
id: root
color: "#0e0f16"
property int stage
readonly property bool busy: stage > 1 && stage < 6
Item {
id: content
anchors.centerIn: parent
width: Math.min(parent.width, Kirigami.Units.gridUnit * 24)
height: Kirigami.Units.gridUnit * 12
opacity: root.stage > 1 ? 1 : 0
Behavior on opacity {
OpacityAnimator {
duration: Kirigami.Units.veryLongDuration
easing.type: Easing.InOutQuad
}
}
Text {
id: wordmark
anchors.centerIn: parent
text: "SHIFT"
color: "#fcfcfc"
font.family: "Atkinson Hyperlegible Next"
font.pixelSize: Math.round(Kirigami.Units.gridUnit * 3.2)
font.weight: Font.DemiBold
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
Rectangle {
width: Math.round(Kirigami.Units.gridUnit * 5)
height: Math.max(2, Math.round(Kirigami.Units.devicePixelRatio * 2))
radius: height / 2
color: "#2eb8a8"
anchors.top: wordmark.bottom
anchors.topMargin: Kirigami.Units.largeSpacing
anchors.horizontalCenter: parent.horizontalCenter
}
Item {
id: busyIndicator
width: Kirigami.Units.gridUnit * 1.6
height: width
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: wordmark.bottom
anchors.topMargin: Kirigami.Units.gridUnit * 2.2
opacity: root.busy ? 1 : 0
Behavior on opacity {
OpacityAnimator {
duration: Kirigami.Units.longDuration
easing.type: Easing.InOutQuad
}
}
Rectangle {
anchors.fill: parent
radius: width / 2
color: "transparent"
border.width: Math.max(2, Math.round(Kirigami.Units.devicePixelRatio * 2))
border.color: "#2eb8a8"
opacity: 0.28
}
Rectangle {
width: parent.width
height: Math.max(2, Math.round(Kirigami.Units.devicePixelRatio * 2))
radius: height / 2
color: "#2eb8a8"
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
}
RotationAnimator on rotation {
from: 0
to: 360
duration: 1400
loops: Animation.Infinite
running: root.busy && Kirigami.Units.longDuration > 1
}
}
}
}

View file

@ -3,14 +3,14 @@
"KPlugin": {
"Authors": [
{
"Name": "SHIFT Contributors"
"Name": "Marco Allegretti"
}
],
"Category": "",
"Description": "SHIFT for mobile and convergent devices",
"Description": "SHIFT shell appearance for mobile and convergent devices",
"Id": "org.shift.mobile",
"License": "GPLv2+",
"Name": "SHIFT Mobile"
"Name": "SHIFT"
},
"Keywords": "Phone;Mobile;Desktop;Workspace;Appearance;Look and Feel;Logout;Lock;Suspend;Shutdown;Hibernate;",
"X-Plasma-APIVersion": "2"

View file

@ -6,7 +6,7 @@
<component>
<id>org.shift.mobile</id>
<developer id="org.shift">
<name translate="no">SHIFT Contributors</name>
<name translate="no">Marco Allegretti</name>
</developer>
<metadata_license>CC0-1.0</metadata_license>
<project_license>GPL-2.0-or-later</project_license>

View file

@ -9,7 +9,7 @@ import org.kde.plasma.private.mobileshell.quicksettingsplugin as QS
QS.QuickSetting {
text: i18n("Docked Mode")
icon: "preferences-desktop-display-randr"
icon: "preferences-desktop-display-randr-symbolic"
enabled: ShellSettings.Settings.convergenceModeEnabled
function toggle() {

View file

@ -8,7 +8,7 @@ import org.kde.plasma.private.mobileshell.quicksettingsplugin as QS
QS.QuickSetting {
text: i18n("Launch Hint")
icon: "dialog-information"
icon: "dialog-information-symbolic"
status: ShellSettings.Settings.gamingDismissHintEnabled ? i18n("On") : i18n("Off")
enabled: true
available: ShellSettings.Settings.gamingModeEnabled

View file

@ -10,7 +10,7 @@ QS.QuickSetting {
id: root
text: i18n("Hotspot")
icon: "network-wireless-hotspot"
icon: "network-wireless-hotspot-symbolic"
enabled: MobileShell.NetworkInfo.wirelessStatus.hotspotSSID.length !== 0
status: enabled ? MobileShell.NetworkInfo.wirelessStatus.hotspotSSID : ""

View file

@ -10,7 +10,7 @@ import org.kde.plasma.workspace.keyboardlayout 1.0 as Keyboards
QS.QuickSetting {
text: i18n("Virtual Keyboard")
icon: "input-keyboard-virtual"
icon: "input-keyboard-virtual-symbolic"
status: enabled ? i18n("On") :
(Keyboards.KWinVirtualKeyboard.available ? i18n("Off") : i18n("Tap to open settings"))
enabled: Keyboards.KWinVirtualKeyboard.enabled && Keyboards.KWinVirtualKeyboard.available

View file

@ -11,7 +11,7 @@ import org.kde.plasma.private.mobileshell.quicksettingsplugin as QS
QS.QuickSetting {
text: i18n("Night Color")
icon: "redshift-status-on"
icon: "redshift-status-on-symbolic"
enabled: NightColor.NightColorUtil.enabled
status: ""
settingsCommand: "plasma-open-settings kcm_nightcolor"

View file

@ -7,7 +7,7 @@
"Name": "Devin Lin"
},
{
"Name": "SHIFT Contributors"
"Name": "Marco Allegretti"
}
],
"Category": "",

View file

@ -20,6 +20,16 @@ if(BASH_EXECUTABLE)
COMMAND ${BASH_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/check-shift-product-metadata.sh
)
add_test(
NAME shift-theme-identity
COMMAND ${BASH_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/check-shift-theme-identity.sh
)
add_test(
NAME shift-preview-qml-stubs
COMMAND ${BASH_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/check-preview-qml-stubs.sh
)
add_test(
NAME shift-initialstart-metadata
COMMAND ${BASH_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/check-shift-initialstart-metadata.sh

View file

@ -7,15 +7,20 @@ set -euo pipefail
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
constants="$repo_root/components/mobileshell/qml/components/Constants.qml"
panel="$repo_root/containments/panel/qml/main.qml"
taskpanel="$repo_root/containments/taskpanel/qml/main.qml"
folio_main="$repo_root/containments/homescreens/folio/qml/main.qml"
folio_home="$repo_root/containments/homescreens/folio/qml/FolioHomeScreen.qml"
action_content="$repo_root/components/mobileshell/qml/actiondrawer/private/ContentContainer.qml"
action_landscape="$repo_root/components/mobileshell/qml/actiondrawer/private/LandscapeContentContainer.qml"
quick_settings="$repo_root/components/mobileshell/qml/actiondrawer/private/QuickSettings.qml"
quick_settings_panel="$repo_root/components/mobileshell/qml/actiondrawer/private/QuickSettingsPanel.qml"
require_line() {
local file="$1"
local needle="$2"
if ! grep -Fq "$needle" "$file"; then
if ! grep -Fq -- "$needle" "$file"; then
echo "Missing invariant in ${file#$repo_root/}: $needle" >&2
exit 1
fi
@ -23,15 +28,75 @@ require_line() {
require_line "$constants" "readonly property real convergenceDockHeight: Kirigami.Units.gridUnit * 3"
require_line "$constants" "readonly property real convergenceDockRevealHeight: Kirigami.Units.gridUnit"
require_line "$constants" "readonly property real convergenceWorkspaceFrameThickness:"
require_line "$constants" "readonly property real convergenceWorkspaceFrameRadius:"
require_line "$taskpanel" "height: MobileShell.Constants.convergenceDockHeight"
require_line "$taskpanel" "LayerShell.Window.exclusionZone: MobileShell.Constants.convergenceDockHeight"
require_line "$panel" "readonly property real convergenceWorkspaceFrameThickness:"
require_line "$panel" "height: Math.max(1, root.panelHeight + root.convergenceWorkspaceFrameThickness)"
require_line "$panel" "LayerShell.Window.exclusionZone: Math.max(1, root.panelHeight + root.convergenceWorkspaceFrameThickness)"
require_line "$taskpanel" "readonly property real convergenceWorkspaceFrameThickness:"
require_line "$taskpanel" "height: MobileShell.Constants.convergenceDockHeight + root.convergenceWorkspaceFrameThickness"
require_line "$taskpanel" "LayerShell.Window.exclusionZone: MobileShell.Constants.convergenceDockHeight + root.convergenceWorkspaceFrameThickness"
require_line "$taskpanel" "LayerShell.Window.scope: \"workspace-frame-left\""
require_line "$taskpanel" "LayerShell.Window.scope: \"workspace-frame-right\""
require_line "$folio_main" "height: MobileShell.Constants.convergenceDockHeight"
require_line "$folio_main" "readonly property real dockHeight: MobileShell.Constants.convergenceDockHeight"
require_line "$folio_main" "readonly property real revealStripHeight: MobileShell.Constants.convergenceDockRevealHeight"
require_line "$folio_main" "import QtQuick.Shapes 1.8"
require_line "$folio_main" "id: workspaceFrame"
require_line "$folio_main" "readonly property real frameThickness: MobileShell.Constants.convergenceWorkspaceFrameThickness"
require_line "$folio_main" "readonly property real frameRadius:"
require_line "$folio_main" "readonly property real workAreaX: frameThickness"
require_line "$folio_main" "readonly property real workAreaY: topReservedHeight + frameThickness"
require_line "$folio_main" "readonly property real workAreaWidth: Math.max(0, width - frameThickness * 2)"
require_line "$folio_main" "readonly property real workAreaHeight: Math.max(0, height - topReservedHeight - bottomReservedHeight - frameThickness * 2)"
require_line "$folio_main" "fillRule: ShapePath.OddEvenFill"
require_line "$folio_main" "readonly property real popupTopY: MobileShell.Constants.topPanelHeight"
require_line "$folio_main" "+ MobileShell.Constants.convergenceWorkspaceFrameThickness"
require_line "$folio_main" "readonly property real popupBottomY: parent.height"
require_line "$folio_main" "- MobileShell.Constants.convergenceWorkspaceFrameThickness"
require_line "$folio_main" "readonly property real popupHeight: Math.max(0, popupBottomY - popupTopY)"
require_line "$folio_main" "? popupTopY + animationY"
require_line "$folio_home" "height: ShellSettings.Settings.convergenceModeEnabled ? MobileShell.Constants.convergenceDockHeight : Kirigami.Units.gridUnit * 6"
require_line "$action_content" "readonly property real convergenceSurfaceTopInset: MobileShell.Constants.topPanelHeight"
require_line "$action_content" "readonly property real convergenceSurfaceBottomInset: MobileShell.Constants.convergenceDockHeight + convergenceFrameThickness"
require_line "$action_content" "readonly property real convergenceSurfaceSideInset: 0"
require_line "$action_content" "topMargin: isConvergence ? root.convergenceSurfaceTopInset : 0"
require_line "$action_content" "leftMargin: actionDrawer.mode == MobileShell.ActionDrawer.Portrait ? 0 : (isConvergence ? root.convergenceSurfaceSideInset"
require_line "$action_content" "maximumHeight: isConvergence ? root.convergenceSurfaceHeight * 0.6 : -1"
require_line "$action_landscape" "readonly property real convergenceSurfaceTopInset: MobileShell.Constants.topPanelHeight"
require_line "$action_landscape" "readonly property real convergenceSurfaceBottomInset: MobileShell.Constants.convergenceDockHeight + convergenceFrameThickness"
require_line "$action_landscape" "readonly property real convergenceSurfaceSideInset: 0"
require_line "$action_landscape" "anchors.topMargin: isConvergence ? restingTopMargin"
require_line "$action_landscape" "anchors.rightMargin: isConvergence ? root.convergenceSurfaceSideInset : 0"
require_line "$action_landscape" "fullScreenHeight: quickSettingsPanel.availableHeight"
require_line "$quick_settings_panel" "readonly property bool isConvergence: ShellSettings.Settings.convergenceModeEnabled"
require_line "$quick_settings_panel" "readonly property real panelPadding: isConvergence ? Kirigami.Units.smallSpacing : Kirigami.Units.smallSpacing * 4"
require_line "$quick_settings_panel" "anchors.margins: root.isConvergence ? 0 : Kirigami.Units.largeSpacing"
require_line "$quick_settings_panel" "panelType: root.isConvergence ? MobileShell.PanelBackground.PanelType.Flat : MobileShell.PanelBackground.PanelType.Base"
require_line "$quick_settings_panel" "radius: root.isConvergence ? 0 : Kirigami.Units.cornerRadius"
require_line "$quick_settings" "id: convergenceFlow"
require_line "$quick_settings" "visible: root.isConvergence"
require_line "$quick_settings" "visible: !root.isConvergence"
require_line "$quick_settings" "active: !root.isConvergence && swipeView.count > 1 ? true: false"
frame_arc_count="$(grep -F "PathArc {" "$folio_main" | wc -l)"
if [[ "$frame_arc_count" -ne 4 ]]; then
echo "Expected the workspace frame cutout to have four rounded inner corners; found $frame_arc_count arcs" >&2
exit 1
fi
if grep -Fq "convergenceWallpaperLayer" "$folio_main"; then
echo "Do not replace the real wallpaper with a custom convergence wallpaper layer" >&2
exit 1
fi
dock_offset_transforms="$(grep -F "transform: Translate { y: dockOverlay.dockOffset }" "$folio_main" | wc -l)"
if [[ "$dock_offset_transforms" -ne 1 ]]; then
echo "Expected only dock contents to slide with dockOverlay.dockOffset; found $dock_offset_transforms transforms" >&2

View file

@ -0,0 +1,44 @@
#!/usr/bin/env bash
# SPDX-FileCopyrightText: 2026 Marco Allegretti
# SPDX-License-Identifier: EUPL-1.2
set -euo pipefail
repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$repo_dir"
fail() {
printf '%s\n' "$1" >&2
exit 1
}
[[ -f tools/preview-stubs/CMakeLists.txt ]] || fail "preview QML stubs must have tracked CMake integration"
grep -Fq 'set(SHIFT_INSTALL_PREVIEW_QML_STUBS "AUTO"' tools/preview-stubs/CMakeLists.txt \
|| fail "preview QML stubs must default to AUTO, not unconditional installation"
grep -Fq 'PROPERTY STRINGS AUTO ON OFF' tools/preview-stubs/CMakeLists.txt \
|| fail "preview QML stubs must provide an explicit OFF mode"
grep -Fq 'CMAKE_INSTALL_PREFIX MATCHES "/\\.prefix$"' tools/preview-stubs/CMakeLists.txt \
|| fail "preview QML stubs must be limited to local .prefix installs by default"
grep -Eq 'DESTINATION \$\{KDE_INSTALL_QMLDIR\}' tools/preview-stubs/CMakeLists.txt \
|| fail "preview QML stubs must install into the Qt QML import tree"
while IFS= read -r -d '' qmldir; do
module_dir="$(dirname "$qmldir")"
while IFS= read -r line; do
line="${line%%#*}"
[[ -n "${line//[[:space:]]/}" ]] || continue
case "$line" in
module\ *|plugin\ *|classname\ *|typeinfo\ *|depends\ *|prefer\ *)
continue
;;
esac
set -- $line
qml_file="${3:-}"
[[ -n "$qml_file" ]] || continue
[[ -f "$module_dir/$qml_file" ]] || fail "$qmldir declares missing file: $qml_file"
done < "$qmldir"
done < <(find tools/preview-stubs/qml -name qmldir -print0)
printf 'preview-qml-stubs-ok\n'

View file

@ -256,3 +256,28 @@ if [[ -s "$missing_icons" ]]; then
sed 's/^/ - /' "$missing_icons" >&2
exit 1
fi
# Quick-setting tiles render with Kirigami.Icon { isMask: true }; if their icon
# name resolves to a colored preferences/scalable tile it collapses to a solid
# rounded square. Require an explicit -symbolic suffix for any quick-setting
# icon name that also exists as a colored tile.
masked_collisions="$(mktemp)"
trap 'rm -f "$required_icons" "$missing_icons" "$broken_icons" "$plasma_files" "$masked_collisions"' EXIT
while IFS= read -r -d '' qml; do
perl -ne 'if (/^\s*icon:\s*"([^"]+)"\s*$/) { print "$1\n"; }' "$qml"
done < <(find quicksettings -name 'main.qml' -print0) \
| sort -u \
| while IFS= read -r icon_name; do
case "$icon_name" in
*-symbolic) continue ;;
esac
[[ -e "$theme_dir/preferences/scalable/$icon_name.svg" ]] || continue
printf '%s\n' "$icon_name"
done > "$masked_collisions"
if [[ -s "$masked_collisions" ]]; then
printf 'Quick-setting icon names must use a -symbolic suffix when a colored preferences/scalable tile exists, otherwise the masked tile renders as a solid square:\n' >&2
sed 's/^/ - /' "$masked_collisions" >&2
exit 1
fi

View file

@ -0,0 +1,188 @@
#!/usr/bin/env bash
# SPDX-FileCopyrightText: 2026 Marco Allegretti
# SPDX-License-Identifier: EUPL-1.2
set -euo pipefail
repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$repo_dir"
fail() {
printf '%s\n' "$1" >&2
exit 1
}
require_line() {
local file="$1"
local pattern="$2"
local message="$3"
grep -Eq "$pattern" "$file" || fail "$message"
}
reject_line() {
local file="$1"
local pattern="$2"
local message="$3"
! grep -Eq "$pattern" "$file" || fail "$message"
}
readonly spdx_copyright_tag='SPDX-FileCopyrightText'
readonly spdx_license_tag='SPDX-License-Identifier'
readonly shift_copyright_pattern="${spdx_copyright_tag}: 2026 Marco Allegretti"
readonly shift_license_pattern="${spdx_license_tag}: EUPL-1[.]2"
require_line CMakeLists.txt '^add_subdirectory\(cursors\)$' \
"top-level build must install the Shift cursor theme"
require_line CMakeLists.txt '^add_subdirectory\(desktoptheme\)$' \
"top-level build must install the Shift Plasma desktop themes"
require_line lookandfeel/metadata.json '"Id": "org\.shift\.mobile"' \
"look-and-feel id must remain org.shift.mobile"
require_line lookandfeel/metadata.json '"Name": "SHIFT"' \
"look-and-feel visible name must be SHIFT"
require_line color-schemes/ShiftDark.colors '^ColorScheme=ShiftDark$' \
"dark color scheme id must remain ShiftDark"
require_line color-schemes/ShiftDark.colors '^Name=SHIFT Dark$' \
"dark color scheme visible name must be SHIFT Dark"
require_line color-schemes/ShiftLight.colors '^ColorScheme=ShiftLight$' \
"light color scheme id must remain ShiftLight"
require_line color-schemes/ShiftLight.colors '^Name=SHIFT Light$' \
"light color scheme visible name must be SHIFT Light"
require_line icons/org.shift.icons/index.theme '^Name=SHIFT$' \
"icon theme visible name must be SHIFT"
require_line kwin/decorations/org.shift.decoration/metadata.json '"Name": "SHIFT"' \
"window decoration visible name must be SHIFT"
require_line wallpapers/SHIFT/metadata.json '"Name": "SHIFT"' \
"wallpaper visible name must be SHIFT"
require_line desktoptheme/shift-dark/metadata.json '"Id": "shift-dark"' \
"dark Plasma desktop theme id must be shift-dark"
require_line desktoptheme/shift-dark/metadata.json '"Name": "SHIFT Dark"' \
"dark Plasma desktop theme visible name must be SHIFT Dark"
require_line desktoptheme/shift-light/metadata.json '"Id": "shift-light"' \
"light Plasma desktop theme id must be shift-light"
require_line desktoptheme/shift-light/metadata.json '"Name": "SHIFT Light"' \
"light Plasma desktop theme visible name must be SHIFT Light"
require_line cursors/shift-cursors/index.theme '^Name=SHIFT$' \
"cursor theme visible name must be SHIFT"
for desktop_asset in \
desktoptheme/shift-dark/widgets/panel-background.svg \
desktoptheme/shift-dark/solid/widgets/panel-background.svg \
desktoptheme/shift-dark/widgets/background.svg \
desktoptheme/shift-dark/widgets/translucentbackground.svg \
desktoptheme/shift-light/widgets/panel-background.svg \
desktoptheme/shift-light/solid/widgets/panel-background.svg \
desktoptheme/shift-light/widgets/background.svg \
desktoptheme/shift-light/widgets/translucentbackground.svg; do
[[ -f "$desktop_asset" ]] || fail "Shift desktop theme must provide $desktop_asset"
require_line "$desktop_asset" "$shift_copyright_pattern" \
"$desktop_asset must attribute Shift-owned work to Marco Allegretti"
require_line "$desktop_asset" "$shift_license_pattern" \
"$desktop_asset must use EUPL-1.2"
done
require_line lookandfeel/contents/defaults '^ColorScheme=ShiftDark$' \
"look-and-feel defaults must select ShiftDark"
require_line lookandfeel/contents/defaults '^Theme=org\.shift\.icons$' \
"look-and-feel defaults must select org.shift.icons"
require_line lookandfeel/contents/defaults '^cursorTheme=shift-cursors$' \
"look-and-feel defaults must select shift-cursors"
require_line lookandfeel/contents/defaults '^name=shift-dark$' \
"look-and-feel defaults must select the Shift dark Plasma desktop theme"
require_line lookandfeel/contents/defaults '^theme=org\.shift\.decoration$' \
"look-and-feel defaults must select org.shift.decoration"
require_line lookandfeel/contents/defaults '^widgetStyle=Breeze$' \
"look-and-feel defaults must keep Breeze widget style until Shift ships a QQC2 style"
reject_line lookandfeel/contents/defaults 'breeze-dark|breeze-light|breeze_cursors|org\.kde\.Breeze' \
"look-and-feel defaults must not select Breeze desktop, cursor, or splash themes"
reject_line lookandfeel/contents/defaults 'widgetStyle=Shift|widgetStyle=SHIFT|widgetStyle=org\.shift' \
"look-and-feel defaults must not reference a non-existent Shift widget style"
[[ -f lookandfeel/contents/splash/Splash.qml ]] || fail "look-and-feel must provide a Shift splash implementation"
reject_line lookandfeel/contents/logout/Logout.qml 'plasma_lookandfeel_org\.kde\.lookandfeel' \
"look-and-feel QML must use the Shift translation domain"
require_line lookandfeel/contents/logout/Logout.qml 'plasma_lookandfeel_org\.shift\.mobile' \
"look-and-feel logout QML must use plasma_lookandfeel_org.shift.mobile"
require_line lookandfeel/Messages.sh 'plasma_lookandfeel_org\.shift\.mobile\.pot' \
"look-and-feel Messages.sh must generate the Shift translation domain"
require_line preview.sh 'PLASMA_THEME=shift-light' \
"preview light mode must select the Shift light Plasma desktop theme"
require_line preview.sh 'PLASMA_THEME=shift-dark' \
"preview dark mode must select the Shift dark Plasma desktop theme"
reject_line preview.sh 'PLASMA_THEME=breeze-' \
"preview must not select Breeze Plasma desktop themes"
require_line preview.sh 'Shift does not ship a QQC2 style plugin yet' \
"preview must document the Breeze QQC2 fallback"
require_line bin/startplasmamobile.in 'Shift does not ship a QQC2 style plugin yet' \
"runtime launcher must document the Breeze QQC2 fallback"
require_line HACKING.md 'Shift does not ship a QQC2 style plugin yet' \
"HACKING.md must document the Breeze QQC2 fallback"
require_line .kde-ci.yml 'plasma/qqc2-breeze-style' \
"CI must keep the Breeze QQC2 runtime dependency until Shift ships a QQC2 style"
reject_line preview.sh 'QT_QUICK_CONTROLS_STYLE=org\.shift|QT_QUICK_CONTROLS_STYLE=SHIFT|QT_QUICK_CONTROLS_STYLE=Shift' \
"preview must not reference a non-existent Shift QQC2 style"
reject_line bin/startplasmamobile.in 'QT_QUICK_CONTROLS_STYLE=org\.shift|QT_QUICK_CONTROLS_STYLE=SHIFT|QT_QUICK_CONTROLS_STYLE=Shift' \
"runtime launcher must not reference a non-existent Shift QQC2 style"
generic_attribution_pattern='Shift contributor[s]|SHIFT Contributor[s]'
if grep -RInE "$generic_attribution_pattern" \
.reuse \
color-schemes/CMakeLists.txt \
cursors \
desktoptheme \
fonts/CMakeLists.txt \
fonts/README.md \
icons/sc-places-start-here-shift.svg \
kwin/decorations/org.shift.decoration/metadata.json \
lookandfeel/contents/splash \
lookandfeel/metadata.json \
org.shift.mobile.metainfo.xml \
shell/metadata.json \
tests/check-shift-theme-identity.sh >/dev/null; then
fail "Shift-owned theme identity files must not use generic Shift contributor attribution"
fi
for license_file in \
color-schemes/CMakeLists.txt \
cursors/CMakeLists.txt \
desktoptheme/CMakeLists.txt \
desktoptheme/shift-dark/plasmarc \
desktoptheme/shift-light/plasmarc \
fonts/CMakeLists.txt \
fonts/README.md \
icons/sc-places-start-here-shift.svg \
lookandfeel/contents/splash/Splash.qml \
tests/check-shift-theme-identity.sh; do
require_line "$license_file" "$shift_copyright_pattern" \
"$license_file must attribute Shift-owned work to Marco Allegretti"
require_line "$license_file" "$shift_license_pattern" \
"$license_file must use EUPL-1.2"
done
for metadata_file in \
desktoptheme/shift-dark/metadata.json \
desktoptheme/shift-light/metadata.json \
kwin/decorations/org.shift.decoration/metadata.json; do
require_line "$metadata_file" '"Name": "Marco Allegretti"' \
"$metadata_file must attribute Shift-owned metadata to Marco Allegretti"
require_line "$metadata_file" '"License": "EUPL-1\.2"' \
"$metadata_file must use EUPL-1.2"
done
for author_file in \
lookandfeel/metadata.json \
shell/metadata.json \
org.shift.mobile.metainfo.xml; do
require_line "$author_file" 'Marco Allegretti' \
"$author_file must not use generic Shift contributor attribution"
done
result='shift-theme-identity-ok'
printf '%s\n' "$result"

View file

@ -0,0 +1,26 @@
# SPDX-FileCopyrightText: 2026 Marco Allegretti
# SPDX-License-Identifier: EUPL-1.2
set(SHIFT_INSTALL_PREVIEW_QML_STUBS "AUTO" CACHE STRING "Install preview fallback QML stubs: AUTO, ON, or OFF")
set_property(CACHE SHIFT_INSTALL_PREVIEW_QML_STUBS PROPERTY STRINGS AUTO ON OFF)
if(NOT SHIFT_INSTALL_PREVIEW_QML_STUBS MATCHES "^(AUTO|ON|OFF)$")
message(FATAL_ERROR "SHIFT_INSTALL_PREVIEW_QML_STUBS must be AUTO, ON, or OFF")
endif()
set(_shift_install_preview_qml_stubs OFF)
if(SHIFT_INSTALL_PREVIEW_QML_STUBS STREQUAL "ON")
set(_shift_install_preview_qml_stubs ON)
elseif(SHIFT_INSTALL_PREVIEW_QML_STUBS STREQUAL "AUTO" AND CMAKE_INSTALL_PREFIX MATCHES "/\\.prefix$")
set(_shift_install_preview_qml_stubs ON)
endif()
if(_shift_install_preview_qml_stubs)
install(
DIRECTORY qml/
DESTINATION ${KDE_INSTALL_QMLDIR}
FILES_MATCHING
PATTERN "*.qml"
PATTERN "qmldir"
)
endif()

View file

@ -0,0 +1,28 @@
<!-- SPDX-FileCopyrightText: 2026 Marco Allegretti -->
<!-- SPDX-License-Identifier: EUPL-1.2 -->
# Preview-only QML stubs
These stub modules satisfy QML imports that **upstream Plasma Mobile expects**
but that are not provided by the QML packages shipped on the host distro used
for preview/development.
They are installed only for local preview prefixes by default:
`SHIFT_INSTALL_PREVIEW_QML_STUBS=AUTO` enables installation when
`CMAKE_INSTALL_PREFIX` ends in `.prefix`. Set it to `ON` to force installation
for another prefix, or `OFF` to disable it. Production installs using normal
system prefixes leave these stubs out.
## Current stubs
- `org/kde/plasma/networkmanagement/cellular/``CellularModemList`,
`CellularModem`. Required by
`components/mobileshell/qml/dataproviders/SignalStrengthInfo.qml`.
openSUSE Tumbleweed's `plasma6-nm 6.6.4` package does not ship this
QML submodule (only the cellular KCM `.so`). Without the stub the
entire MobileShell QML namespace fails to load and the homescreen
is blank.
When the host distro starts providing a real module at the same import path,
disable the local fallback by configuring with `SHIFT_INSTALL_PREVIEW_QML_STUBS=OFF`
before comparing against the system module.

View file

@ -0,0 +1,12 @@
// SPDX-FileCopyrightText: 2026 Marco Allegretti
// SPDX-License-Identifier: EUPL-1.2
// Minimal stub for the upstream plasma-nm cellular QML module.
// See CellularModemList.qml for rationale.
import QtQuick
QtObject {
property bool mobileDataEnabled: false
property bool mobileDataSupported: false
property bool needsAPNAdded: false
property bool simEmpty: true
}

View file

@ -0,0 +1,13 @@
// SPDX-FileCopyrightText: 2026 Marco Allegretti
// SPDX-License-Identifier: EUPL-1.2
// Minimal stub for the upstream plasma-nm cellular QML module.
// openSUSE Tumbleweed's plasma6-nm 6.6.4 package does not yet ship this
// module, but components/mobileshell/.../SignalStrengthInfo.qml imports
// it unconditionally. Without this stub, every MobileShell type fails to
// load and the homescreen is blank.
import QtQuick
QtObject {
property bool modemAvailable: false
property var primaryModem: null
}

View file

@ -0,0 +1,6 @@
# SPDX-FileCopyrightText: 2026 Marco Allegretti
# SPDX-License-Identifier: EUPL-1.2
module org.kde.plasma.networkmanagement.cellular
CellularModemList 1.0 CellularModemList.qml
CellularModem 1.0 CellularModem.qml

View file

@ -1,4 +1,4 @@
# SPDX-FileCopyrightText: 2026 SHIFT Contributors
# SPDX-FileCopyrightText: 2026 Marco Allegretti
# SPDX-License-Identifier: EUPL-1.2
install(DIRECTORY SHIFT

View file

@ -7,6 +7,6 @@
],
"Id": "SHIFT",
"License": "EUPL-1.2",
"Name": "SHIFT Light / SHIFT Dark"
"Name": "SHIFT"
}
}