// The one window with fields in it. // // Kadence on the desktop is an agent and a countdown: the tray menu drives the // timer, the mini timer shows it, and the blocking happens with no window open // at all. The rules, the task sources, the report and the lengths are the // *server's*, edited in the web app, so a second place to edit them was a second // place to be wrong — the five-page window that used to be here has gone. // // What is left genuinely belongs to this machine, and a fresh install has to be // able to set it somewhere: which backend, and how this box behaves. One column, // hairline sections, exactly one filled button. import QtQuick import QtQuick.Controls import QtQuick.Layouts import QtQuick.Window import "." import "components" Window { id: dialog width: 460 height: Math.min(720, content.implicitHeight + 76) minimumWidth: 420 minimumHeight: 420 title: "Kadence settings" color: Theme.paper flags: Qt.Dialog // The live config, re-read whenever it changes underneath us (the tray can // change view-only mode, and a toggle that fails is reverted in Python). readonly property var config: controller.config // Typed into, not bound: a text field that re-binds on every keystroke // fights the cursor. These are committed on editing finished, or by Save. property string draftUrl: "" property string draftToken: "" function reload() { draftUrl = config.server_url draftToken = config.api_token } onVisibleChanged: if (visible) reload() Component.onCompleted: reload() function commitBackend() { if (draftUrl !== config.server_url) controller.setConfig("server_url", draftUrl) if (draftToken !== config.api_token) controller.setConfig("api_token", draftToken) } // ---- the furniture ----------------------------------------------------- component FieldLabel: Label { color: Theme.muted font.pixelSize: 11 font.family: Theme.sansFamily } component Field: TextField { Layout.fillWidth: true color: Theme.ink font.pixelSize: 13 font.family: Theme.sansFamily background: Rectangle { radius: 8 color: Theme.surface border.width: 1 border.color: parent.activeFocus ? Theme.ember : Theme.rule } } // A labelled switch with its own explanation underneath, because every one // of these turns a real desktop facility on or off and "Inhibit" alone does // not say what it will do to your screen. component Toggle: RowLayout { id: toggleRow property string label: "" property string detail: "" property bool on: false property bool available: true signal toggledTo(bool value) Layout.fillWidth: true spacing: 12 ColumnLayout { Layout.fillWidth: true spacing: 1 Label { text: toggleRow.label color: toggleRow.available ? Theme.ink : Theme.faint font.pixelSize: 13 font.family: Theme.sansFamily } Label { visible: toggleRow.detail !== "" text: toggleRow.detail color: Theme.faint font.pixelSize: 11 font.family: Theme.sansFamily wrapMode: Text.WordWrap Layout.fillWidth: true } } Switch { checked: toggleRow.on enabled: toggleRow.available onToggled: toggleRow.toggledTo(checked) } } ScrollView { anchors.fill: parent contentWidth: availableWidth clip: true ColumnLayout { id: content width: dialog.width spacing: 18 Item { Layout.fillWidth: true; implicitHeight: 4 } ColumnLayout { Layout.leftMargin: 22 Layout.rightMargin: 22 spacing: 2 Label { text: "KADENCE" color: Theme.muted font.pixelSize: 10 font.letterSpacing: 1.4 font.weight: Font.DemiBold font.family: Theme.sansFamily } Label { text: "This machine" color: Theme.ink font.pixelSize: 26 font.family: Theme.displayFamily } Label { text: "Timer lengths, the block lists, the task sources and the " + "report all live on the server — edit those in the web app." color: Theme.muted font.pixelSize: 11 font.family: Theme.sansFamily wrapMode: Text.WordWrap Layout.fillWidth: true Layout.topMargin: 6 } } // ---- backend ------------------------------------------------------- ColumnLayout { Layout.leftMargin: 22 Layout.rightMargin: 22 spacing: 10 SectionTitle { text: "Backend" trailing: controller.connected ? "reachable" : (controller.connectionError !== "" ? "not answering" : "…") } Banner { visible: !controller.connected && controller.connectionError !== "" text: controller.connectionError } FieldLabel { text: "Address" } Field { text: dialog.draftUrl placeholderText: "http://localhost:8000" onTextChanged: dialog.draftUrl = text onEditingFinished: dialog.commitBackend() } FieldLabel { text: "API token — API_TOKEN from the backend's .env, if it has one" } Field { text: dialog.draftToken echoMode: TextInput.Password placeholderText: "none" onTextChanged: dialog.draftToken = text onEditingFinished: dialog.commitBackend() } } // ---- the mini timer ------------------------------------------------ ColumnLayout { Layout.leftMargin: 22 Layout.rightMargin: 22 spacing: 10 SectionTitle { text: "Mini timer" } Toggle { label: "Show it while an interval runs" detail: "Otherwise it appears only when you ask for it — tray, Ctrl+M, or --mini." on: dialog.config.mini_timer_auto onToggledTo: (value) => controller.setConfig("mini_timer_auto", value) } Toggle { label: "Keep it above everything" detail: dialog.config.keep_above_supported ? "A KWin window rule, because Wayland gives an app no way to ask." : "Not available on this desktop — your compositor decides." on: dialog.config.mini_timer_keep_above available: dialog.config.keep_above_supported onToggledTo: (value) => controller.setConfig("mini_timer_keep_above", value) } RowLayout { Layout.fillWidth: true spacing: 12 ColumnLayout { Layout.fillWidth: true spacing: 4 FieldLabel { text: "Where it appears" } ComboBox { Layout.fillWidth: true model: controller.miniCornerLabels currentIndex: controller.miniCornerIndex font.family: Theme.sansFamily onActivated: controller.setMiniCorner(currentIndex) } } ColumnLayout { spacing: 4 FieldLabel { text: "Edge gap" } SpinBox { from: 0 to: 400 stepSize: 4 value: dialog.config.mini_timer_margin onValueModified: controller.setConfig("mini_timer_margin", value) } } } } // ---- enforcement --------------------------------------------------- ColumnLayout { Layout.leftMargin: 22 Layout.rightMargin: 22 spacing: 10 SectionTitle { text: "Focus guard" trailing: controller.blockerActive ? "enforcing" : controller.blockerReason } Banner { visible: dialog.config.enforce_blocking && !controller.helperInstalled text: "The privileged helper is not installed, so websites cannot be " + "blocked on this machine. Run: sudo ./desktop/helper/install.sh" } Banner { visible: controller.blockerError !== "" text: controller.blockerError } Toggle { label: "Enforce the server's rules here" detail: "Off, this machine is a timer and nothing else." on: dialog.config.enforce_blocking onToggledTo: (value) => controller.setConfig("enforce_blocking", value) } Toggle { label: "Block websites" detail: "The managed region of /etc/hosts, plus a local DNS stub for the masks it cannot express." on: dialog.config.block_hosts_file available: dialog.config.enforce_blocking onToggledTo: (value) => controller.setConfig("block_hosts_file", value) } Toggle { label: "Close blocked applications" detail: "/proc is scanned every few seconds and matches are asked to quit, then made to." on: dialog.config.block_apps available: dialog.config.enforce_blocking onToggledTo: (value) => controller.setConfig("block_apps", value) } } // ---- the session --------------------------------------------------- ColumnLayout { Layout.leftMargin: 22 Layout.rightMargin: 22 spacing: 10 SectionTitle { text: "This session" } Toggle { label: "Start with the session" detail: "An XDG autostart entry, minimised to the tray." on: dialog.config.autostart onToggledTo: (value) => controller.setConfig("autostart", value) } Toggle { label: "Run as a systemd user service" detail: "Restarts on failure and logs to the journal: journalctl --user -u kadence-desktop." on: dialog.config.service_enabled onToggledTo: (value) => controller.setConfig("service_enabled", value) } Toggle { label: "Pause when the session locks" detail: "logind says when you walked away; the interval resumes when you come back." on: dialog.config.pause_on_lock onToggledTo: (value) => controller.setConfig("pause_on_lock", value) } Toggle { label: "Pause other people's music" detail: "Any MPRIS player is paused when focus starts; only the ones we paused resume." on: dialog.config.pause_media onToggledTo: (value) => controller.setConfig("pause_media", value) } Toggle { label: "Silence other apps' notifications" detail: "Do Not Disturb for the length of a focus interval, put back afterwards." on: dialog.config.do_not_disturb onToggledTo: (value) => controller.setConfig("do_not_disturb", value) } Toggle { label: "Keep the screen awake while focusing" on: dialog.config.inhibit_screensaver onToggledTo: (value) => controller.setConfig("inhibit_screensaver", value) } } // ---- out ----------------------------------------------------------- RowLayout { Layout.leftMargin: 22 Layout.rightMargin: 22 Layout.bottomMargin: 20 Layout.topMargin: 4 spacing: 10 GhostButton { text: "Open the web app" onClicked: controller.openWebUi() } Item { Layout.fillWidth: true } PrimaryButton { text: "Done" onClicked: { dialog.commitBackend() dialog.close() } } } } } // Esc closes it, like any dialog. Enter commits what was typed. Shortcut { sequence: StandardKey.Cancel onActivated: dialog.close() } }