feat: aggiungi tema SDDM costruttivista

This commit is contained in:
Matteo Benedetto
2026-08-16 16:34:00 +02:00
commit 8c8c61f4de
12 changed files with 777 additions and 0 deletions
+88
View File
@@ -0,0 +1,88 @@
import QtQuick 2.15
import QtQuick.Controls 2.15 as QQC2
// Selettore a tendina etichettato in stile costruttivista.
Column {
id: comboWrap
property string label: ""
property alias model: combo.model
property alias textRole: combo.textRole
property alias currentIndex: combo.currentIndex
property alias currentText: combo.currentText
signal activated(int index)
width: parent.width
spacing: 4
Text {
text: comboWrap.label
color: "#f0dfba"
font.pixelSize: 12
font.bold: true
font.letterSpacing: 2
}
QQC2.ComboBox {
id: combo
width: comboWrap.width
height: 40
font.pixelSize: 16
font.bold: true
onActivated: comboWrap.activated(index)
background: Rectangle {
color: "#cc101012"
border.color: combo.hovered ? "#d83a28" : "#66f0dfba"
border.width: 1
}
contentItem: Text {
leftPadding: 10
rightPadding: 10
text: combo.displayText
color: "#f0dfba"
font: combo.font
verticalAlignment: Text.AlignVCenter
elide: Text.ElideRight
}
indicator: Item { width: 0; height: 0 }
delegate: QQC2.ItemDelegate {
width: combo.width
height: 34
highlighted: combo.highlightedIndex === index
contentItem: Text {
text: combo.textRole ? model[combo.textRole] : modelData
color: combo.highlightedIndex === index ? "#f6f1e5" : "#f0dfba"
font: combo.font
verticalAlignment: Text.AlignVCenter
leftPadding: 10
}
background: Rectangle {
color: combo.highlightedIndex === index ? "#d83a28" : "#e6101012"
}
}
popup: QQC2.Popup {
y: combo.height + 2
width: combo.width
implicitHeight: contentItem.implicitHeight
padding: 1
contentItem: ListView {
clip: true
implicitHeight: contentHeight
model: combo.popup.visible ? combo.delegateModel : null
currentIndex: combo.highlightedIndex
}
background: Rectangle {
color: "#e6101012"
border.color: "#66f0dfba"
border.width: 1
}
}
}
}
+47
View File
@@ -0,0 +1,47 @@
import QtQuick 2.15
// Campo di input etichettato in stile costruttivista.
Column {
id: field
property string label: ""
property alias text: input.text
property alias input: input
property bool password: false
property bool focusOnStart: false
signal accepted()
width: parent.width
spacing: 4
Text {
text: field.label
color: "#f0dfba"
font.pixelSize: 12
font.bold: true
font.letterSpacing: 2
}
Rectangle {
width: field.width
height: 40
color: "#cc101012"
border.color: input.activeFocus ? "#d83a28" : "#66f0dfba"
border.width: input.activeFocus ? 2 : 1
Behavior on border.color { ColorAnimation { duration: 150 } }
TextInput {
id: input
anchors.fill: parent
anchors.leftMargin: 10
anchors.rightMargin: 10
verticalAlignment: Text.AlignVCenter
color: "#f0dfba"
font.pixelSize: 16
font.bold: true
echoMode: field.password ? TextInput.Password : TextInput.Normal
selectByMouse: true
focus: field.focusOnStart
onAccepted: field.accepted()
}
}
}
+391
View File
@@ -0,0 +1,391 @@
/*
Costruttivista — tema SDDM
Schermata di accesso in stile costruttivista sovietico per SDDM.
Autosufficiente: solo QtQuick 2.15 e QtQuick.Controls 2.15, nessuna dipendenza esterna.
*/
import QtQuick 2.15
import QtQuick.Layouts 1.15
Rectangle {
id: root
color: "#101012"
width: 1920
height: 1080
readonly property color red: "#d83a28"
readonly property color cream: "#f0dfba"
readonly property color dark: "#181719"
readonly property color panelBg: "#d9151517"
readonly property color paper: "#f6f1e5"
// Utente selezionato dalla lista (quando il modello espone utenti).
property string selectedUser: userModel.lastUser || ""
// Data in italiano senza dipendere dalla locale del greeter.
function italianDate(d) {
var days = ["DOMENICA", "LUNEDÌ", "MARTEDÌ", "MERCOLEDÌ", "GIOVEDÌ", "VENERDÌ", "SABATO"]
var months = ["GENNAIO", "FEBBRAIO", "MARZO", "APRILE", "MAGGIO", "GIUGNO", "LUGLIO", "AGOSTO", "SETTEMBRE", "OTTOBRE", "NOVEMBRE", "DICEMBRE"]
return days[d.getDay()] + " " + d.getDate() + " " + months[d.getMonth()] + " " + d.getFullYear()
}
function tryLogin() {
var username = userModel.count > 0 ? root.selectedUser : userField.text
if (username.length === 0) {
errorText.text = "INSERISCI L'UTENTE"
shake.start()
return
}
errorText.text = ""
sddm.login(username, passField.text, sessionCombo.currentIndex)
}
Item {
id: ui
anchors.fill: parent
opacity: 0
// ---------- sfondo ----------
Image {
id: background
anchors.fill: parent
source: "assets/sfondo_costruttivista_fhd.png"
fillMode: Image.PreserveAspectCrop
asynchronous: true
cache: true
}
Rectangle {
anchors.fill: parent
color: "#12000000"
opacity: overlayFade.value
}
// ---------- accenti geometrici ----------
Item {
id: accents
anchors.fill: parent
Rectangle {
id: redBar
x: parent.width * 0.07
y: parent.height * 0.15
width: Math.max(5, parent.width * 0.008)
height: parent.height * 0.27
color: root.red
opacity: 0.9
transform: Translate { id: redShift; y: -root.height * 0.10 }
}
Rectangle {
id: creamLine
x: parent.width * 0.07
y: parent.height * 0.44
width: parent.width * 0.20
height: Math.max(2, parent.height * 0.004)
color: root.cream
opacity: 0.85
transform: Translate { id: lineShift; x: -root.width * 0.12 }
}
Rectangle {
id: darkBar
x: parent.width * 0.82
y: parent.height * 0.60
width: Math.max(4, parent.width * 0.005)
height: parent.height * 0.20
color: root.dark
opacity: 0.75
transform: Translate { id: darkShift; y: root.height * 0.09 }
}
}
// ---------- orologio ----------
Rectangle {
id: clockBackdrop
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
anchors.topMargin: Math.max(36, root.height * 0.055)
width: clockBlock.width + Math.max(36, root.width * 0.02)
height: clockBlock.height + Math.max(22, root.height * 0.022)
color: "#66101012"
border.color: "#33f0dfba"
border.width: 1
Column {
id: clockBlock
anchors.centerIn: parent
spacing: Math.max(2, root.height * 0.004)
Text {
id: clockText
anchors.horizontalCenter: parent.horizontalCenter
text: Qt.formatTime(new Date(), "HH:mm")
color: root.cream
font.pixelSize: Math.max(44, Math.min(root.width * 0.05, 88))
font.bold: true
font.letterSpacing: 4
}
Text {
id: dateText
anchors.horizontalCenter: parent.horizontalCenter
text: root.italianDate(new Date())
color: "#ef6a55"
font.pixelSize: Math.max(13, Math.min(root.width * 0.014, 22))
font.bold: true
font.letterSpacing: 2
}
Timer {
interval: 1000
repeat: true
running: true
onTriggered: {
clockText.text = Qt.formatTime(new Date(), "HH:mm")
dateText.text = root.italianDate(new Date())
}
}
}
}
// ---------- pannello di accesso ----------
Item {
id: panelHost
anchors.centerIn: parent
width: panel.width
height: panel.height
transform: Translate { id: panelShift; x: 0 }
Rectangle {
id: panel
width: Math.min(root.width * 0.40, 500)
height: panelCol.implicitHeight + Math.max(52, root.height * 0.056)
color: root.panelBg
border.color: "#66f0dfba"
border.width: Math.max(1, root.height * 0.002)
Rectangle {
width: Math.max(8, panel.width * 0.02)
anchors.left: parent.left
anchors.top: parent.top
anchors.bottom: parent.bottom
color: root.red
}
Column {
id: panelCol
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
anchors.leftMargin: Math.max(26, root.height * 0.028)
anchors.rightMargin: Math.max(26, root.height * 0.028)
anchors.topMargin: Math.max(26, root.height * 0.028)
spacing: Math.max(14, root.height * 0.016)
// intestazione: marca + titolo
RowLayout {
width: parent.width
spacing: Math.max(16, root.width * 0.011)
Mark {
Layout.preferredWidth: Math.max(58, Math.min(panel.width * 0.15, 96))
Layout.preferredHeight: Layout.preferredWidth
Layout.alignment: Qt.AlignVCenter
}
Column {
Layout.fillWidth: true
Layout.alignment: Qt.AlignVCenter
spacing: 2
Text {
text: "ACCESSO"
color: root.cream
font.pixelSize: Math.max(20, Math.min(root.width * 0.024, 34))
font.bold: true
font.letterSpacing: 3
}
Text {
text: "INSERISCI LE CREDENZIALI"
color: "#e85a48"
font.pixelSize: Math.max(10, Math.min(root.width * 0.011, 15))
font.bold: true
font.letterSpacing: 1
}
}
}
// lista utenti con avatar (solo se il modello espone utenti)
Item {
width: parent.width
height: userListRow.height
UserList {
id: userListRow
anchors.horizontalCenter: parent.horizontalCenter
onUserSelected: root.selectedUser = name
}
}
// campo utente (solo se non ci sono utenti da selezionare)
Field {
id: userField
visible: userModel.count === 0
label: "UTENTE"
text: userModel.lastUser || ""
}
// campo password
Field {
id: passField
label: "PASSWORD"
password: true
onAccepted: root.tryLogin()
}
// selettore sessione
Combo {
id: sessionCombo
label: "SESSIONE"
model: sessionModel
textRole: "name"
currentIndex: sessionModel.lastIndex >= 0 ? sessionModel.lastIndex : 0
}
// messaggio di errore
Text {
id: errorText
width: parent.width
text: ""
color: root.red
font.bold: true
font.letterSpacing: 1
font.pixelSize: Math.max(11, Math.min(root.width * 0.012, 16))
visible: text.length > 0
wrapMode: Text.WordWrap
}
// pulsante di accesso
Rectangle {
id: loginButton
width: parent.width
height: Math.max(42, root.height * 0.05)
color: root.red
border.color: root.cream
border.width: 1
Text {
anchors.centerIn: parent
text: "ENTRA"
color: root.paper
font.bold: true
font.letterSpacing: 3
font.pixelSize: Math.max(14, Math.min(root.width * 0.015, 20))
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
onClicked: root.tryLogin()
onPressed: loginButton.color = Qt.darker(root.red, 1.3)
onReleased: loginButton.color = root.red
}
}
// separatore
Rectangle {
width: parent.width
height: 2
color: "#66f0dfba"
}
// pulsanti di sistema
RowLayout {
width: parent.width
spacing: Math.max(8, root.width * 0.006)
PowerButton {
Layout.fillWidth: true
label: "SOSPENDI"
enabled: sddm.canSuspend
onClicked: sddm.suspend()
}
PowerButton {
Layout.fillWidth: true
label: "RIAVVIA"
enabled: sddm.canReboot
onClicked: sddm.reboot()
}
PowerButton {
Layout.fillWidth: true
label: "SPEGNI"
enabled: sddm.canPowerOff
onClicked: sddm.powerOff()
}
}
}
}
}
}
// ---------- animazioni ----------
QtObject { id: overlayFade; property real value: 0.0 }
ParallelAnimation {
id: intro
NumberAnimation { target: ui; property: "opacity"; from: 0; to: 1; duration: 700; easing.type: Easing.OutCubic }
NumberAnimation { target: overlayFade; property: "value"; from: 0; to: 1; duration: 900 }
NumberAnimation { target: redShift; property: "y"; to: 0; duration: 850; easing.type: Easing.OutCubic }
NumberAnimation { target: lineShift; property: "x"; to: 0; duration: 950; easing.type: Easing.OutCubic }
NumberAnimation { target: darkShift; property: "y"; to: 0; duration: 1050; easing.type: Easing.OutCubic }
}
SequentialAnimation {
id: shake
NumberAnimation { target: panelShift; property: "x"; to: -14; duration: 45 }
NumberAnimation { target: panelShift; property: "x"; to: 14; duration: 45 }
NumberAnimation { target: panelShift; property: "x"; to: -10; duration: 45 }
NumberAnimation { target: panelShift; property: "x"; to: 10; duration: 45 }
NumberAnimation { target: panelShift; property: "x"; to: 0; duration: 45 }
}
NumberAnimation {
id: leave
target: root
property: "opacity"
to: 0
duration: 500
easing.type: Easing.InOutQuad
}
Component.onCompleted: {
intro.start()
focusTimer.start()
}
Timer {
id: focusTimer
interval: 200
onTriggered: {
if (userModel.count > 0) {
passField.input.forceActiveFocus()
} else {
userField.input.forceActiveFocus()
}
}
}
Connections {
target: sddm
function onLoginFailed() {
errorText.text = "ACCESSO NEGATO"
passField.text = ""
shake.start()
}
function onLoginSucceeded() {
leave.start()
}
}
}
+55
View File
@@ -0,0 +1,55 @@
import QtQuick 2.15
// Marca geometrica rotante — stessa identità visiva dello splash costruttivista.
Item {
id: mark
Rectangle {
anchors.fill: parent
anchors.margins: mark.width * 0.09
radius: width / 2
color: "transparent"
border.color: "#f0dfba"
border.width: Math.max(4, mark.width * 0.075)
opacity: 0.92
}
Item {
id: rotor
anchors.fill: parent
Rectangle {
width: parent.width * 0.48
height: Math.max(10, parent.height * 0.16)
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
color: "#d83a28"
}
Rectangle {
width: Math.max(9, parent.width * 0.14)
height: parent.height * 0.42
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
color: "#f0dfba"
}
RotationAnimator on rotation {
from: 0
to: 360
duration: 1200
loops: Animation.Infinite
running: true
}
}
Rectangle {
width: parent.width * 0.25
height: width
anchors.centerIn: parent
color: "#171719"
border.color: "#d83a28"
border.width: Math.max(3, width * 0.15)
rotation: 45
}
}
+32
View File
@@ -0,0 +1,32 @@
import QtQuick 2.15
// Pulsante di sistema (sospendi / riavvia / spegni).
Rectangle {
id: pbtn
property string label: ""
property bool enabled: true
property bool hovered: false
signal clicked()
height: 34
color: !pbtn.enabled ? "#77101012" : (pbtn.hovered ? "#33d83a28" : "#cc101012")
border.color: pbtn.enabled ? "#99f0dfba" : "#55f0dfba"
border.width: 1
Text {
anchors.centerIn: parent
text: pbtn.label
color: pbtn.enabled ? "#f0dfba" : "#aaf0dfba"
font.pixelSize: 13
font.bold: true
font.letterSpacing: 1
}
MouseArea {
anchors.fill: parent
enabled: pbtn.enabled
hoverEnabled: true
onClicked: pbtn.clicked()
onEntered: pbtn.hovered = true
onExited: pbtn.hovered = false
}
}
+60
View File
@@ -0,0 +1,60 @@
# Costruttivista — tema SDDM
Schermata di accesso in stile costruttivista sovietico per SDDM, gemella del tema splash KDE `org.enne2.costruttivista.splash`. Stessa identità visiva: palette rosso/crema/nero, accenti geometrici, marca rotante e pannello con bordo crema.
Autosufficiente: solo QtQuick 2.15 e QtQuick.Controls 2.15, nessuna dipendenza esterna (niente Kirigami, Plasma components o rete). L'immagine di sfondo è inclusa nel pacchetto.
## Funzionalità
- Lista utenti con avatar (dal modello `userModel` di SDDM, con fallback geometrico se manca l'avatar)
- Campo password con focus automatico
- Selettore sessione
- Orologio e data in italiano su pannello scuro
- Pulsanti di sistema: sospendi / riavvia / spegni
- Animazione di ingresso, scossa su errore, dissolvenza al successo
## Struttura
```
org.enne2.costruttivista.sddm/
├── theme.conf # metadati del tema
├── metadata.desktop # metadati (compatibilità)
├── Main.qml # schermata principale
├── UserList.qml # lista utenti con avatar
├── Field.qml # campo di input etichettato
├── Combo.qml # selettore a tendina etichettato
├── PowerButton.qml # pulsante di sistema
├── Mark.qml # marca geometrica rotante
├── assets/
│ └── sfondo_costruttivista_fhd.png
└── dist/
└── org.enne2.costruttivista.sddm.tar.gz
```
## Installazione
Per l'utente corrente (consigliato per la prova):
```sh
mkdir -p ~/.local/share/sddm/themes
cp -r org.enne2.costruttivista.sddm ~/.local/share/sddm/themes/
```
Per tutti gli utenti:
```sh
sudo cp -r org.enne2.costruttivista.sddm /usr/share/sddm/themes/
```
## Attivazione
Impostare il tema in `/etc/sddm.conf` (o in un drop-in in `/etc/sddm.conf.d/`):
```ini
[Theme]
Current=org.enne2.costruttivista.sddm
```
In alternativa, dal KCM **Impostazioni di sistema → Sessione → Schermata di accesso (SDDM)** selezionare **Costruttivista**.
Il pacchetto non viene installato né attivato automaticamente.
+97
View File
@@ -0,0 +1,97 @@
import QtQuick 2.15
// Lista orizzontale di utenti con avatar, in stile costruttivista.
// Usa il modello `userModel` esposto da SDDM (ruoli: name, realName, icon, isCurrent).
Row {
id: userList
property int currentIndex: userModel.lastIndex >= 0 ? userModel.lastIndex : 0
signal userSelected(string name)
spacing: Math.max(8, parent.width * 0.02)
Repeater {
id: repeater
model: userModel
delegate: Rectangle {
id: card
required property int index
required property string name
required property string icon
width: 80
height: 100
color: userList.currentIndex === index ? "#33d83a28" : "#cc101012"
border.color: userList.currentIndex === index ? "#d83a28" : "#66f0dfba"
border.width: userList.currentIndex === index ? 2 : 1
// cornice circolare con avatar
Rectangle {
id: avatarFrame
width: 54
height: 54
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
anchors.topMargin: 7
radius: width / 2
color: "#181719"
border.color: "#f0dfba"
border.width: 2
clip: true
Image {
id: avatarImage
anchors.fill: parent
anchors.margins: 2
source: card.icon
fillMode: Image.PreserveAspectCrop
sourceSize: Qt.size(108, 108)
asynchronous: true
}
// fallback geometrico se l'avatar non è disponibile
Text {
anchors.centerIn: parent
text: card.name.length > 0 ? card.name.charAt(0).toUpperCase() : "?"
color: "#f0dfba"
font.pixelSize: 22
font.bold: true
visible: avatarImage.status !== Image.Ready
}
}
// nome utente
Text {
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: parent.bottom
anchors.bottomMargin: 6
width: card.width - 6
horizontalAlignment: Text.AlignHCenter
elide: Text.ElideRight
text: card.name.toUpperCase()
color: userList.currentIndex === index ? "#f6f1e5" : "#f0dfba"
font.pixelSize: 11
font.bold: true
font.letterSpacing: 1
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
onClicked: userList.select(index)
}
}
}
// Seleziona l'utente all'indice i (click o tastiera).
function select(i) {
if (i < 0 || i >= userModel.count) return
currentIndex = i
var item = repeater.itemAt(i)
if (item) {
userSelected(item.name)
}
}
Keys.onLeftPressed: select(currentIndex - 1)
Keys.onRightPressed: select(currentIndex + 1)
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.
+4
View File
@@ -0,0 +1,4 @@
[Desktop Entry]
Type=X-SDDM-Theme
Name=Costruttivista
Comment=Schermata di accesso costruttivista per SDDM
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 348 KiB

+3
View File
@@ -0,0 +1,3 @@
[General]
Name=Costruttivista
Comment=Schermata di accesso costruttivista per SDDM