import QtQuick import QtQuick.Layouts import org.kde.plasma.plasmoid import org.kde.plasma.components 3.0 as PC3 import org.kde.plasma.plasma5support as P5Support import org.kde.kirigami as Kirigami PlasmoidItem { id: root // Percorso dell'helper, sostituito da install.sh con il percorso assoluto readonly property string ctl: "@CTL@" readonly property string calibration: "@CALIBRATE@" property bool polling: false property bool calibrating: false property var st: ({ daemon: false, running: false, brightness: 1.0, contrast: 1.0, saturation: 1.0, band: 0.12, leds: 47, fps: 0.0, error: "" }) property bool busy: false property string lastError: "" preferredRepresentation: compactRepresentation toolTipMainText: "Ambilight" toolTipSubText: { if (!st.daemon) return i18n("Demone non raggiungibile") if (!st.running) return i18n("Fermo") return i18n("%1 LED · %2 fps", st.leds, Math.round(st.fps)) } function refresh() { if (root.polling) return root.polling = true poll.connectSource(root.ctl + " state") } Timer { interval: 2000 running: true repeat: true onTriggered: root.refresh() } function run(args) { if (root.busy) return root.busy = true exec.connectSource(root.ctl + " " + args) } function openCalibration() { if (!root.st.daemon || root.calibrating) return root.calibrating = true calibrationSource.connectSource("/usr/bin/python3 " + JSON.stringify(root.calibration)) } // ---- stato (polling) P5Support.DataSource { id: poll engine: "executable" interval: 2000 connectedSources: [] onNewData: function(source, data) { poll.disconnectSource(source) root.polling = false const out = String(data["stdout"] || "").trim() if (!out) return try { const s = JSON.parse(out) root.st = s root.lastError = s.error || "" } catch (e) { root.lastError = "stato illeggibile" } } } // ---- comandi (uno alla volta) P5Support.DataSource { id: exec engine: "executable" interval: 0 connectedSources: [] onNewData: function(source, data) { exec.disconnectSource(source) root.busy = false const out = String(data["stdout"] || "").trim() if (out !== "") { try { const r = JSON.parse(out) root.lastError = (r.ok === false) ? (r.error || "errore") : "" if (r.state) root.st = r.state } catch (e) { root.lastError = "" } } refresh() } } // ---- icona nel vassoio compactRepresentation: Item { implicitWidth: Kirigami.Units.iconSizes.smallMedium implicitHeight: Kirigami.Units.iconSizes.smallMedium Layout.minimumWidth: implicitWidth Layout.minimumHeight: implicitHeight Layout.preferredWidth: implicitWidth Layout.preferredHeight: implicitHeight // Kirigami.Icon assorbe i clic se non ha un MouseArea proprio (bug KDE 518024), // quindi il gesto è duplicato sia sul contenitore sia sull'icona. MouseArea { id: compactMouse anchors.fill: parent hoverEnabled: true acceptedButtons: Qt.LeftButton | Qt.MiddleButton onClicked: function(mouse) { if (mouse.button === Qt.MiddleButton) root.expanded = !root.expanded else root.openCalibration() } onContainsMouseChanged: { if (containsMouse) root.refresh() } Kirigami.Icon { anchors.fill: parent source: "video-television" active: compactMouse.containsMouse opacity: root.st.running ? 1.0 : 0.45 MouseArea { anchors.fill: parent acceptedButtons: Qt.LeftButton | Qt.MiddleButton onClicked: function(mouse) { if (mouse.button === Qt.MiddleButton) root.expanded = !root.expanded else root.openCalibration() } } } } } // ---- popup fullRepresentation: Item { implicitWidth: Kirigami.Units.gridUnit * 17 implicitHeight: col.implicitHeight + 2 * Kirigami.Units.largeSpacing Layout.preferredWidth: implicitWidth Layout.preferredHeight: implicitHeight Layout.minimumWidth: Kirigami.Units.gridUnit * 15 Layout.minimumHeight: implicitHeight ColumnLayout { id: col anchors.top: parent.top anchors.left: parent.left anchors.right: parent.right anchors.margins: Kirigami.Units.largeSpacing spacing: Kirigami.Units.smallSpacing RowLayout { Layout.fillWidth: true spacing: Kirigami.Units.smallSpacing Kirigami.Icon { source: "video-television" Layout.preferredWidth: Kirigami.Units.iconSizes.smallMedium Layout.preferredHeight: Kirigami.Units.iconSizes.smallMedium } PC3.Label { text: i18n("Ambilight") font.bold: true Layout.fillWidth: true } PC3.Switch { id: powerSwitch checked: root.st.running enabled: root.st.daemon onToggled: root.run(powerSwitch.checked ? "start" : "stop") } } PC3.Label { Layout.fillWidth: true text: !root.st.daemon ? i18n("Demone non attivo") : (root.st.running ? i18n("%1 LED · %2 fps · banda %3%", root.st.leds, Math.round(root.st.fps), Math.round(root.st.band * 100)) : i18n("Spento · %1 LED", root.st.leds)) opacity: 0.75 font: Kirigami.Theme.smallFont color: root.st.daemon ? Kirigami.Theme.textColor : Kirigami.Theme.negativeTextColor } component ParamRow: RowLayout { id: row property string label property real from: 0 property real to: 200 property real current: 100 property bool percent: true property bool active: true signal applied(real v) Layout.fillWidth: true spacing: Kirigami.Units.smallSpacing PC3.Label { text: row.label Layout.preferredWidth: Kirigami.Units.gridUnit * 4 font: Kirigami.Theme.smallFont } PC3.Slider { id: sl Layout.fillWidth: true from: row.from to: row.to stepSize: 1 enabled: row.active value: row.current onMoved: row.applied(value) } PC3.Label { text: Math.round(sl.value) + (row.percent ? "%" : "") Layout.preferredWidth: Kirigami.Units.gridUnit * 2.2 horizontalAlignment: Text.AlignRight font: Kirigami.Theme.smallFont opacity: 0.8 } } ParamRow { label: i18n("Luminosità") from: 5 to: 200 active: root.st.daemon current: root.st.brightness * 100 onApplied: function(v) { root.run("set brightness " + (v / 100.0).toFixed(2)) } } ParamRow { label: i18n("Contrasto") from: 50 to: 200 active: root.st.daemon current: root.st.contrast * 100 onApplied: function(v) { root.run("set contrast " + (v / 100.0).toFixed(2)) } } ParamRow { label: i18n("Saturazione") from: 0 to: 200 active: root.st.daemon current: root.st.saturation * 100 onApplied: function(v) { root.run("set saturation " + (v / 100.0).toFixed(2)) } } ParamRow { label: i18n("Banda") from: 2 to: 50 active: root.st.daemon current: root.st.band * 100 onApplied: function(v) { root.run("set band " + (v / 100.0).toFixed(2)) } } PC3.Label { Layout.fillWidth: true visible: root.lastError !== "" text: root.lastError color: Kirigami.Theme.negativeTextColor font: Kirigami.Theme.smallFont wrapMode: Text.Wrap } RowLayout { Layout.fillWidth: true PC3.Button { text: i18n("Calibra…") enabled: root.st.daemon && !root.calibrating onClicked: root.openCalibration() } PC3.Button { text: i18n("Riavvia demone") font: Kirigami.Theme.smallFont onClicked: { root.busy = true restartSource.connectSource("systemctl --user restart ambilight.service") } } Item { Layout.fillWidth: true } PC3.Button { text: i18n("Aggiorna") font: Kirigami.Theme.smallFont onClicked: root.refresh() } } } } // sorgente dedicata al riavvio: nessun ritorno JSON, quindi handler separato P5Support.DataSource { id: restartSource engine: "executable" interval: 0 connectedSources: [] onNewData: function(source, data) { restartSource.disconnectSource(source) root.busy = false root.refresh() } } P5Support.DataSource { id: calibrationSource engine: "executable" connectedSources: [] onNewData: function(source, data) { calibrationSource.disconnectSource(source) root.calibrating = false if (data["exit code"] !== 0) root.lastError = String(data["stderr"] || i18n("Impossibile avviare la calibrazione")) root.refresh() } } Component.onCompleted: root.refresh() }