Compare commits

...

3 commits

Author SHA1 Message Date
8f3a94b104 Disable mobile task switcher in convergence mode
The full-screen card carousel is touch-oriented and redundant
when a keyboard and mouse are available. In convergence mode,
disable it so KWin falls back to the standard Alt-Tab tabbox.
Window switching is also covered by the dock bar icons and the
Overview effect.
2026-04-17 07:42:45 +02:00
86283b526d Move notification popup to bottom-right in convergence
The mobile popup system always anchored to top-center which
looked out of place on a desktop screen. In convergence mode,
position popups at the bottom-right corner and slide them in
from below. Swipe-to-dismiss flips to match (drag down to
dismiss instead of up). Stacked card previews and the popup
drawer tap area are hidden since notifications are already
accessible in the action drawer.
2026-04-17 07:10:23 +02:00
c2b864d8b4 Show action drawer as bounded panel in convergence
Replace the full-screen slide-down sheet with two fade-in cards
anchored to the top corners. Quick settings sit top-right,
notifications top-left capped at 60% screen height. Background
scrim reduced to 30%. Redundant clock header and status bar
hidden since both are already visible in the top panel.
2026-04-16 17:42:06 +02:00
13 changed files with 102 additions and 32 deletions

View file

@ -11,6 +11,7 @@ import org.kde.plasma.private.mobileshell as MobileShell
import org.kde.plasma.components 3.0 as PC3 import org.kde.plasma.components 3.0 as PC3
import org.kde.kirigami as Kirigami import org.kde.kirigami as Kirigami
import org.kde.plasma.private.mobileshell.quicksettingsplugin as QS import org.kde.plasma.private.mobileshell.quicksettingsplugin as QS
import org.kde.plasma.private.mobileshell.shellsettingsplugin as ShellSettings
/** /**
* Root element that contains all the ActionDrawer's contents, and is anchored to the screen. * Root element that contains all the ActionDrawer's contents, and is anchored to the screen.
@ -62,7 +63,10 @@ Item {
Kirigami.Theme.backgroundColor.b, Kirigami.Theme.backgroundColor.b,
0.9) 0.9)
Behavior on color { ColorAnimation { duration: Kirigami.Units.longDuration; easing.type: Easing.OutQuad } } Behavior on color { ColorAnimation { duration: Kirigami.Units.longDuration; easing.type: Easing.OutQuad } }
opacity: Math.max(0, Math.min(brightnessPressedValue, actionDrawer.offset / root.minimizedQuickSettingsOffset)) opacity: {
let base = Math.max(0, Math.min(brightnessPressedValue, actionDrawer.offset / root.minimizedQuickSettingsOffset));
return ShellSettings.Settings.convergenceModeEnabled ? base * 0.3 : base;
}
} }
// The base swipe area. // The base swipe area.
@ -106,8 +110,8 @@ Item {
anchors { anchors {
topMargin: notificationDrawer.height + 1 topMargin: notificationDrawer.height + 1
leftMargin: actionDrawer.mode == MobileShell.ActionDrawer.Portrait ? 0 : 10 leftMargin: actionDrawer.mode == MobileShell.ActionDrawer.Portrait ? 0 : (notificationDrawer.isConvergence ? Kirigami.Units.smallSpacing : 10)
rightMargin: actionDrawer.mode == MobileShell.ActionDrawer.Portrait ? 0 : notificationDrawer.notificationWidget.anchors.rightMargin + Kirigami.Units.gridUnit - notificationDrawer.anchors.leftMargin + 370 rightMargin: actionDrawer.mode == MobileShell.ActionDrawer.Portrait ? 0 : (notificationDrawer.isConvergence ? parent.width * 0.5 : notificationDrawer.notificationWidget.anchors.rightMargin + Kirigami.Units.gridUnit - notificationDrawer.anchors.leftMargin + 370)
top: parent.top top: parent.top
left: parent.left left: parent.left
right: parent.right right: parent.right
@ -160,19 +164,28 @@ Item {
NotificationDrawer { NotificationDrawer {
id: notificationDrawer id: notificationDrawer
readonly property bool isConvergence: ShellSettings.Settings.convergenceModeEnabled
swipeArea: swipeAreaPortrait swipeArea: swipeAreaPortrait
actionDrawer: root.actionDrawer actionDrawer: root.actionDrawer
mediaControlsWidget: root.mediaControlsWidget mediaControlsWidget: root.mediaControlsWidget
contentContainer: root contentContainer: root
opacity: Math.max(0, Math.min(root.brightnessPressedValue, actionDrawer.offsetResistance / root.minimizedQuickSettingsOffset)) opacity: {
let base = Math.max(0, Math.min(root.brightnessPressedValue, actionDrawer.offsetResistance / root.minimizedQuickSettingsOffset));
return isConvergence ? Math.max(0, Math.min(1, actionDrawer.offset / root.minimizedQuickSettingsOffset)) : base;
}
anchors { anchors {
top: parent.top top: parent.top
left: parent.left left: parent.left
right: parent.right right: parent.right
rightMargin: root.actionDrawer.mode == MobileShell.ActionDrawer.Portrait ? 0 : 360 topMargin: isConvergence ? Kirigami.Units.smallSpacing : 0
leftMargin: actionDrawer.mode == MobileShell.ActionDrawer.Portrait ? 0 : notificationDrawer.minWidthHeight * 0.06 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)
} }
// In convergence, cap the height so it doesn't stretch full-screen
maximumHeight: isConvergence ? root.height * 0.6 : -1
} }
// Secondary swipe area for uses in portrait. // Secondary swipe area for uses in portrait.

View file

@ -56,10 +56,14 @@ Item {
: ShellSettings.Settings.quickSettingsColumns : ShellSettings.Settings.quickSettingsColumns
readonly property real intendedWidth: (columnWidth * effectiveColumns) + Kirigami.Units.gridUnit readonly property real intendedWidth: (columnWidth * effectiveColumns) + Kirigami.Units.gridUnit
property real offsetRatio: quickSettingsPanel.height / root.height readonly property bool isConvergence: ShellSettings.Settings.convergenceModeEnabled
anchors.topMargin: Math.min(root.actionDrawer.offsetResistance * offsetRatio - quickSettingsPanel.height, 0) property real offsetRatio: (quickSettingsPanel.height + restingTopMargin) / root.height
readonly property real restingTopMargin: isConvergence ? Kirigami.Units.smallSpacing : 0
anchors.topMargin: isConvergence ? restingTopMargin : Math.min(root.actionDrawer.offsetResistance * offsetRatio - quickSettingsPanel.height, 0)
anchors.top: parent.top anchors.top: parent.top
anchors.right: parent.right anchors.right: parent.right
anchors.rightMargin: isConvergence ? Kirigami.Units.smallSpacing : 0
opacity: isConvergence ? Math.max(0, Math.min(1, root.actionDrawer.offset / root.minimizedQuickSettingsOffset)) : 1
actionDrawer: root.actionDrawer actionDrawer: root.actionDrawer
fullScreenHeight: root.height fullScreenHeight: root.height

View file

@ -11,6 +11,7 @@ import QtQuick.Layouts 1.1
import org.kde.plasma.clock import org.kde.plasma.clock
import org.kde.plasma.components 3.0 as PlasmaComponents import org.kde.plasma.components 3.0 as PlasmaComponents
import org.kde.plasma.private.mobileshell as MobileShell import org.kde.plasma.private.mobileshell as MobileShell
import org.kde.plasma.private.mobileshell.shellsettingsplugin as ShellSettings
import org.kde.kirigami as Kirigami import org.kde.kirigami as Kirigami
Item { Item {
@ -24,14 +25,26 @@ Item {
property alias notificationWidget: notificationWidget property alias notificationWidget: notificationWidget
property real contentY: notificationWidget.listView.contentY property real contentY: notificationWidget.listView.contentY
property real topPadding: actionDrawer.mode == MobileShell.ActionDrawer.Portrait ? Kirigami.Units.largeSpacing : date.y + date.height + Kirigami.Units.smallSpacing * 6 property real topPadding: {
if (actionDrawer.mode == MobileShell.ActionDrawer.Portrait)
return Kirigami.Units.largeSpacing;
if (ShellSettings.Settings.convergenceModeEnabled)
return Kirigami.Units.largeSpacing;
return date.y + date.height + Kirigami.Units.smallSpacing * 6;
}
property real topMargin: actionDrawer.mode == MobileShell.ActionDrawer.Portrait ? actionDrawer.offsetResistance + 1 : 0 property real topMargin: actionDrawer.mode == MobileShell.ActionDrawer.Portrait ? actionDrawer.offsetResistance + 1 : 0
readonly property real minWidthHeight: Math.min(actionDrawer.width, actionDrawer.height) readonly property real minWidthHeight: Math.min(actionDrawer.width, actionDrawer.height)
readonly property bool hasNotifications: notificationWidget.hasNotifications readonly property bool hasNotifications: notificationWidget.hasNotifications
readonly property bool listOverflowing: notificationWidget.listView.listOverflowing readonly property bool listOverflowing: notificationWidget.listView.listOverflowing
height: Math.min(actionDrawer.height - toolButtons.height, notificationWidget.listView.contentHeight + 10 + topMargin) // External cap for convergence mode; -1 means uncapped.
property real maximumHeight: -1
height: {
let h = Math.min(actionDrawer.height - toolButtons.height, notificationWidget.listView.contentHeight + 10 + topMargin);
return maximumHeight > 0 ? Math.min(h, maximumHeight) : h;
}
// time source for the time and date whenin landscape mode // time source for the time and date whenin landscape mode
Clock { Clock {
@ -155,6 +168,7 @@ Item {
id: landscapeModeHeader id: landscapeModeHeader
anchors.fill: parent anchors.fill: parent
visible: actionDrawer.mode != MobileShell.ActionDrawer.Portrait visible: actionDrawer.mode != MobileShell.ActionDrawer.Portrait
&& !ShellSettings.Settings.convergenceModeEnabled
transform: [ transform: [
Translate { Translate {

View file

@ -73,9 +73,11 @@ MobileShell.BaseItem {
id: statusBarProxy id: statusBarProxy
Layout.alignment: Qt.AlignTop Layout.alignment: Qt.AlignTop
Layout.fillWidth: true Layout.fillWidth: true
// Hide status bar in convergence already visible in the top panel
visible: !ShellSettings.Settings.convergenceModeEnabled
// Align these to double pixels to aid vertical alignment and sharper icon rendering // Align these to double pixels to aid vertical alignment and sharper icon rendering
Layout.preferredHeight: Math.round(Kirigami.Units.gridUnit * 1.5 * ShellSettings.Settings.statusBarScaleFactor / 2) * 2 Layout.preferredHeight: visible ? Math.round(Kirigami.Units.gridUnit * 1.5 * ShellSettings.Settings.statusBarScaleFactor / 2) * 2 : 0
Layout.maximumHeight: Math.round(Kirigami.Units.gridUnit * 1.5 * ShellSettings.Settings.statusBarScaleFactor / 2) * 2 Layout.maximumHeight: visible ? Math.round(Kirigami.Units.gridUnit * 1.5 * ShellSettings.Settings.statusBarScaleFactor / 2) * 2 : 0
Kirigami.Theme.colorSet: Kirigami.Theme.Window Kirigami.Theme.colorSet: Kirigami.Theme.Window
Kirigami.Theme.inherit: false Kirigami.Theme.inherit: false

View file

@ -27,6 +27,15 @@ Item {
// 'popupWidth' and 'openOffset' is set by the 'notificationPopupManager' // 'popupWidth' and 'openOffset' is set by the 'notificationPopupManager'
property int popupWidth property int popupWidth
property real openOffset property real openOffset
property bool isConvergence: false
// In convergence the popup enters from the bottom-right corner
readonly property real effectiveOpenOffset: isConvergence
? (Screen.height - openOffset - popupHeight)
: openOffset
readonly property real effectiveClosedOffset: isConvergence
? (Screen.height + Kirigami.Units.smallSpacing)
: closedOffset
// calculate the position needed to at when the expanded drawer is active // calculate the position needed to at when the expanded drawer is active
readonly property real fullOpenOffset: popupDrawerOpened ? aboveNotificationFullOffset + aboveNotificationHeight + Kirigami.Units.largeSpacing : 0 readonly property real fullOpenOffset: popupDrawerOpened ? aboveNotificationFullOffset + aboveNotificationHeight + Kirigami.Units.largeSpacing : 0
@ -244,7 +253,7 @@ Item {
waiting = false; waiting = false;
inPopupDrawer = true; inPopupDrawer = true;
if (notificationPopup.popupDrawerOpened && notificationItem.state != "inDrawerClosed" && notificationItem.state != "open") { if (notificationPopup.popupDrawerOpened && notificationItem.state != "inDrawerClosed" && notificationItem.state != "open") {
notificationItem.offset = openOffset; notificationItem.offset = effectiveOpenOffset;
notificationItem.scale = 0.75; notificationItem.scale = 0.75;
notificationItem.popupOpacity = 0.0; notificationItem.popupOpacity = 0.0;
} }
@ -255,7 +264,7 @@ Item {
function openPopup() { function openPopup() {
if (notificationPopup.popupDrawerOpened && notificationItem.state != "open" && notificationItem.state != "inDrawerClosed") { if (notificationPopup.popupDrawerOpened && notificationItem.state != "open" && notificationItem.state != "inDrawerClosed") {
notificationItem.offset = openOffset; notificationItem.offset = effectiveOpenOffset;
notificationItem.scale = 0.75; notificationItem.scale = 0.75;
notificationItem.popupOpacity = 0.0; notificationItem.popupOpacity = 0.0;
} }
@ -343,7 +352,7 @@ Item {
onDismissRequested: closePopup(popupIndex) onDismissRequested: closePopup(popupIndex)
property real offset: closedOffset property real offset: notificationPopup.effectiveClosedOffset
property real scale: 1.0 property real scale: 1.0
property real popupOpacity: 1.0 // controls the opacity of the notification popup when outside the popup drawer property real popupOpacity: 1.0 // controls the opacity of the notification popup when outside the popup drawer
property real drawerScale: { property real drawerScale: {
@ -365,6 +374,9 @@ Item {
return Kirigami.Units.gridUnit * 0.5 * indexClamped; return Kirigami.Units.gridUnit * 0.5 * indexClamped;
} }
property real drawerOpacity: { property real drawerOpacity: {
if (notificationPopup.isConvergence && notificationPopup.inPopupDrawer) {
return 0;
}
let index = notificationPopup.popupIndex - popupNotifications.currentPopupIndex; let index = notificationPopup.popupIndex - popupNotifications.currentPopupIndex;
if (index > 2 && !notificationPopup.popupDrawerOpened) { if (index > 2 && !notificationPopup.popupDrawerOpened) {
return 0; // make this popup invisible if it is below 3 other popups return 0; // make this popup invisible if it is below 3 other popups
@ -399,7 +411,7 @@ Item {
State { State {
name: "open" name: "open"
PropertyChanges { PropertyChanges {
target: notificationItem; offset: notificationPopup.openOffset target: notificationItem; offset: notificationPopup.effectiveOpenOffset
} }
PropertyChanges { PropertyChanges {
target: notificationItem; scale: 1.0 target: notificationItem; scale: 1.0
@ -411,7 +423,7 @@ Item {
State { State {
name: "closeWithMove" name: "closeWithMove"
PropertyChanges { PropertyChanges {
target: notificationItem; offset: notificationPopup.closedOffset target: notificationItem; offset: notificationPopup.effectiveClosedOffset
} }
PropertyChanges { PropertyChanges {
target: notificationItem; scale: 1.0 target: notificationItem; scale: 1.0
@ -423,7 +435,7 @@ Item {
State { State {
name: "closeWithScale" name: "closeWithScale"
PropertyChanges { PropertyChanges {
target: notificationItem; offset: notificationPopup.openOffset target: notificationItem; offset: notificationPopup.effectiveOpenOffset
} }
PropertyChanges { PropertyChanges {
target: notificationItem; scale: 0.75 target: notificationItem; scale: 0.75
@ -435,7 +447,7 @@ Item {
State { State {
name: "inDrawerClosed" name: "inDrawerClosed"
PropertyChanges { PropertyChanges {
target: notificationItem; offset: notificationPopup.openOffset target: notificationItem; offset: notificationPopup.effectiveOpenOffset
} }
PropertyChanges { PropertyChanges {
target: notificationItem; scale: 1 target: notificationItem; scale: 1
@ -556,15 +568,29 @@ Item {
startActive = false; startActive = false;
} }
lastOffset = notificationPopup.dragOffset; lastOffset = notificationPopup.dragOffset;
notificationPopup.dragOffset = calculateResistance(startDragOffset + (translation.y - startPosition), 0); let rawOffset = startDragOffset + (translation.y - startPosition);
if (notificationPopup.isConvergence) {
notificationPopup.dragOffset = -calculateResistance(-rawOffset, 0);
} else {
notificationPopup.dragOffset = calculateResistance(rawOffset, 0);
}
} }
onActiveChanged: { onActiveChanged: {
startActive = active; startActive = active;
notificationPopup.preventDismissTimeout = true; notificationPopup.preventDismissTimeout = true;
if (!active && !(notificationItem.state == "closeWithScale" || notificationItem.state == "closeWithMove")) { if (!active && !(notificationItem.state == "closeWithScale" || notificationItem.state == "closeWithMove")) {
if ((lastOffset - notificationPopup.dragOffset > 1.0 && notificationPopup.dragOffset < 0) || (-(notificationPopup.openOffset - notificationPopup.closedOffset) / 4 > notificationPopup.dragOffset)) { let dominated = false;
// this code is called when the notification is swiped or dragged to the top. if (notificationPopup.isConvergence) {
// convergence: dismiss on swipe down
dominated = (notificationPopup.dragOffset - lastOffset > 1.0 && notificationPopup.dragOffset > 0)
|| (notificationPopup.dragOffset > (notificationPopup.effectiveClosedOffset - notificationPopup.effectiveOpenOffset) / 4);
} else {
// mobile: dismiss on swipe up
dominated = (lastOffset - notificationPopup.dragOffset > 1.0 && notificationPopup.dragOffset < 0)
|| (-(notificationPopup.openOffset - notificationPopup.closedOffset) / 4 > notificationPopup.dragOffset);
}
if (dominated) {
notificationPopup.closedWithSwipe = true; notificationPopup.closedWithSwipe = true;
notificationPopup.closePopup(popupIndex); notificationPopup.closePopup(popupIndex);
return; return;
@ -589,7 +615,7 @@ Item {
height: Kirigami.Units.gridUnit * 2 height: Kirigami.Units.gridUnit * 2
enabled: !notificationPopup.popupDrawerOpened && (notificationPopup.popupCount - popupNotifications.currentPopupIndex > 1) enabled: !notificationPopup.isConvergence && !notificationPopup.popupDrawerOpened && (notificationPopup.popupCount - popupNotifications.currentPopupIndex > 1)
onReleased: { onReleased: {
notificationPopup.openPopupDrawer(); notificationPopup.openPopupDrawer();

View file

@ -11,6 +11,7 @@ import QtQuick.Window
import org.kde.kirigami as Kirigami import org.kde.kirigami as Kirigami
import org.kde.plasma.private.mobileshell as MobileShell import org.kde.plasma.private.mobileshell as MobileShell
import org.kde.plasma.private.mobileshell.state as MobileShellState import org.kde.plasma.private.mobileshell.state as MobileShellState
import org.kde.plasma.private.mobileshell.shellsettingsplugin as ShellSettings
import org.kde.layershell 1.0 as LayerShell import org.kde.layershell 1.0 as LayerShell
@ -31,6 +32,7 @@ Window {
readonly property int popupWidth: Math.min(Kirigami.Units.gridUnit * 20, Screen.width - Kirigami.Units.gridUnit * 2) readonly property int popupWidth: Math.min(Kirigami.Units.gridUnit * 20, Screen.width - Kirigami.Units.gridUnit * 2)
readonly property real openOffset: Kirigami.Units.gridUnit + Kirigami.Units.smallSpacing * 3 readonly property real openOffset: Kirigami.Units.gridUnit + Kirigami.Units.smallSpacing * 3
readonly property int longestLength: Math.max(Screen.width, Screen.height) readonly property int longestLength: Math.max(Screen.width, Screen.height)
readonly property bool isConvergence: ShellSettings.Settings.convergenceModeEnabled
property var keyboardInteractivity: LayerShell.Window.KeyboardInteractivityNone property var keyboardInteractivity: LayerShell.Window.KeyboardInteractivityNone
LayerShell.Window.scope: "notification" LayerShell.Window.scope: "notification"
@ -97,9 +99,15 @@ Window {
console.warn("popupNotification: could not retrieve current popup height - falling back to a default value") console.warn("popupNotification: could not retrieve current popup height - falling back to a default value")
} }
if (isConvergence) {
let regionX = notificationPopupManager.width - notificationPopupManager.popupWidth - Kirigami.Units.gridUnit * 4;
let regionY = Screen.height - openOffset - popupHeight - Kirigami.Units.gridUnit;
ShellUtil.setInputRegion(notificationPopupManager, Qt.rect(regionX, regionY, notificationPopupManager.popupWidth + Kirigami.Units.gridUnit * 2, popupHeight + Kirigami.Units.gridUnit * 2));
} else {
ShellUtil.setInputRegion(notificationPopupManager, Qt.rect((notificationPopupManager.width - notificationPopupManager.popupWidth - Kirigami.Units.gridUnit) / 2, openOffset - Kirigami.Units.gridUnit / 2, notificationPopupManager.popupWidth + Kirigami.Units.gridUnit, popupHeight + Kirigami.Units.gridUnit * ((notifications.count - notifications.currentPopupIndex > 1) ? 4 : 1))); ShellUtil.setInputRegion(notificationPopupManager, Qt.rect((notificationPopupManager.width - notificationPopupManager.popupWidth - Kirigami.Units.gridUnit) / 2, openOffset - Kirigami.Units.gridUnit / 2, notificationPopupManager.popupWidth + Kirigami.Units.gridUnit, popupHeight + Kirigami.Units.gridUnit * ((notifications.count - notifications.currentPopupIndex > 1) ? 4 : 1)));
} }
} }
}
// parent the popup notifications inside a Flickable so that they can be scrollable when the drawer state is active // parent the popup notifications inside a Flickable so that they can be scrollable when the drawer state is active
Flickable { Flickable {
@ -194,9 +202,12 @@ Window {
delegate: NotificationPopup { delegate: NotificationPopup {
id: popup id: popup
anchors.horizontalCenter: parent.horizontalCenter x: notificationPopupManager.isConvergence
? (parent.width - width - Kirigami.Units.gridUnit * 2)
: (parent.width - width) / 2
z: notifications.count - index z: notifications.count - index
isConvergence: notificationPopupManager.isConvergence
popupWidth: notificationPopupManager.popupWidth popupWidth: notificationPopupManager.popupWidth
openOffset: notificationPopupManager.openOffset openOffset: notificationPopupManager.openOffset

View file

@ -57,7 +57,7 @@ QMap<QString, QMap<QString, QVariant>> getKwinrcSettings(KSharedConfig::Ptr m_mo
{ {
{"blurEnabled", false}, // disable blur for performance reasons, we could reconsider in the future for more powerful devices {"blurEnabled", false}, // disable blur for performance reasons, we could reconsider in the future for more powerful devices
{"convergentwindowsEnabled", true}, // enable our convergent window plugin {"convergentwindowsEnabled", true}, // enable our convergent window plugin
{"mobiletaskswitcherEnabled", true}, // ensure the mobile task switcher plugin is enabled {"mobiletaskswitcherEnabled", !convergenceModeEnabled}, // mobile task switcher on phone only; convergence uses standard Alt-Tab tabbox
{"overviewEnabled", convergenceModeEnabled}, // enable KWin Overview effect in convergence mode for desktop-style task switching {"overviewEnabled", convergenceModeEnabled}, // enable KWin Overview effect in convergence mode for desktop-style task switching
{"screenedgeEnabled", convergenceModeEnabled} // enable screen edge visual feedback in convergence mode (mouse hot corners) {"screenedgeEnabled", convergenceModeEnabled} // enable screen edge visual feedback in convergence mode (mouse hot corners)
}}, }},

View file

@ -1,2 +1,2 @@
SPDX-FileCopyrightText: 2025 Marco A. SPDX-FileCopyrightText: 2026 Marco Allegretti.
SPDX-License-Identifier: EUPL-1.2 SPDX-License-Identifier: EUPL-1.2

View file

@ -1,2 +1,2 @@
SPDX-FileCopyrightText: 2025 Marco A. SPDX-FileCopyrightText: 2026 Marco Allegretti.
SPDX-License-Identifier: EUPL-1.2 SPDX-License-Identifier: EUPL-1.2

View file

@ -1,2 +1,2 @@
SPDX-FileCopyrightText: 2025 Marco A. SPDX-FileCopyrightText: 2026 Marco Allegretti.
SPDX-License-Identifier: EUPL-1.2 SPDX-License-Identifier: EUPL-1.2

View file

@ -1,2 +1,2 @@
SPDX-FileCopyrightText: 2025 Marco A. SPDX-FileCopyrightText: 2026 Marco Allegretti.
SPDX-License-Identifier: EUPL-1.2 SPDX-License-Identifier: EUPL-1.2

View file

@ -1,2 +1,2 @@
SPDX-FileCopyrightText: 2025 Marco A. SPDX-FileCopyrightText: 2026 Marco Allegretti.
SPDX-License-Identifier: EUPL-1.2 SPDX-License-Identifier: EUPL-1.2

View file

@ -1,2 +1,2 @@
SPDX-FileCopyrightText: 2025 Marco A. SPDX-FileCopyrightText: 2026 Marco Allegretti.
SPDX-License-Identifier: EUPL-1.2 SPDX-License-Identifier: EUPL-1.2