// The floating mini timer. // // A small pill you keep in a corner while you work: the countdown, what you are // working on, and the three controls you actually reach for. It appears on // demand (tray, Ctrl+M, the D-Bus verb) and — if you let it — by itself // whenever an interval is running and the main window is out of the way. // // Everything has its own row. An earlier version saved space by swapping the // task line for the controls on hover and by drawing the progress bar over the // bottom edge; both of those put things on top of each other, and a timer you // have to hover to operate is a timer you cannot operate at a glance. // // Dragging uses the compositor's own move (`startSystemMove`), the only thing // that works on Wayland, and the window refuses keyboard focus rather than // being a `Qt.Tool` — Qt hides tool windows whenever the application is // deactivated, which made this one vanish on every click in another app. import QtQuick import QtQuick.Controls import QtQuick.Layouts import QtQuick.Window import "." Window { id: mini width: 252 height: card.implicitHeight + 12 color: "transparent" // KWin's rule matches a substring of this, so it must stay distinct from // the main window's caption. Qt appends " — Kadence" to it. title: "Kadence mini" flags: Qt.Window | Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint | Qt.WindowDoesNotAcceptFocus readonly property color accent: Theme.kindColor(controller.kind) readonly property bool paused: controller.status === "paused" // Placement is decided in app.py: it is the only piece that knows about // screens, and on Wayland it has to go through a KWin rule anyway. onXChanged: saveTimer.restart() onYChanged: saveTimer.restart() Timer { id: saveTimer interval: 800 onTriggered: if (mini.visible) controller.saveMiniPosition(mini.x, mini.y) } // A small pill button. `filled` marks the one action you want by default — // Resume when paused, Start when idle. component MiniButton: Rectangle { id: button property string label: "" property bool filled: false signal clicked() implicitWidth: buttonText.implicitWidth + 18 implicitHeight: 24 radius: 7 color: filled ? (buttonHover.hovered ? Qt.lighter(mini.accent, 1.08) : mini.accent) : (buttonHover.hovered ? Qt.alpha(mini.accent, 0.12) : "transparent") border.width: filled ? 0 : 1 border.color: buttonHover.hovered ? mini.accent : Theme.rule Behavior on color { ColorAnimation { duration: 110 } } Label { id: buttonText anchors.fill: parent text: button.label color: button.filled ? "white" : Theme.ink font.pixelSize: 11 font.weight: button.filled ? Font.DemiBold : Font.Normal font.family: Theme.sansFamily horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter } HoverHandler { id: buttonHover; cursorShape: Qt.PointingHandCursor } TapHandler { grabPermissions: PointerHandler.TakeOverForbidden onTapped: button.clicked() } } Rectangle { id: card anchors.fill: parent anchors.margins: 6 radius: 14 color: Theme.surface border.width: 1 border.color: Theme.rule implicitHeight: body.implicitHeight + 24 + progressLane.height + 5 HoverHandler { id: hover } DragHandler { target: null grabPermissions: PointerHandler.CanTakeOverFromAnything onActiveChanged: if (active) mini.startSystemMove() } TapHandler { acceptedButtons: Qt.LeftButton onDoubleTapped: controller.showWindowRequested() } ColumnLayout { id: body anchors.left: parent.left anchors.right: parent.right anchors.top: parent.top anchors.margins: 12 spacing: 3 // ---- state ------------------------------------------------------- RowLayout { Layout.fillWidth: true spacing: 7 Rectangle { width: 6; height: 6; radius: 3 color: controller.running ? mini.accent : Theme.faint SequentialAnimation on opacity { running: controller.running && !mini.paused loops: Animation.Infinite NumberAnimation { to: 0.3; duration: 900 } NumberAnimation { to: 1.0; duration: 900 } } } Label { text: (controller.running ? controller.kindLabel : "Idle").toUpperCase() + (mini.paused ? " · PAUSED" : "") color: mini.paused ? Theme.amber : (controller.running ? mini.accent : Theme.muted) font.pixelSize: 9 font.letterSpacing: 1.2 font.weight: Font.DemiBold font.family: Theme.sansFamily Layout.fillWidth: true elide: Text.ElideRight } Label { text: controller.completedToday + "/" + controller.goal color: Theme.faint font.pixelSize: 10 font.family: Theme.sansFamily } Label { text: "✕" color: closeHover.hovered ? Theme.ink : Theme.faint font.pixelSize: 11 opacity: hover.hovered ? 1 : 0.35 Behavior on opacity { NumberAnimation { duration: 120 } } HoverHandler { id: closeHover; cursorShape: Qt.PointingHandCursor } // The card's DragHandler has CanTakeOverFromAnything so the whole // pill is draggable; without refusing the take-over here, the tiny // press-move jitter of a real click steals this tap into a // startSystemMove() and the close button never fires. TapHandler { grabPermissions: PointerHandler.TakeOverForbidden onTapped: controller.setMiniVisible(false) } } } // ---- the time ---------------------------------------------------- Label { text: controller.clockText color: Theme.ink opacity: mini.paused ? 0.55 : 1 font.pixelSize: 32 font.family: Theme.displayFamily font.letterSpacing: -1 Layout.topMargin: -2 Behavior on opacity { NumberAnimation { duration: 150 } } } // ---- what you are on --------------------------------------------- Label { Layout.fillWidth: true text: controller.taskTitle !== "" ? controller.taskTitle : (controller.running ? "no task attached" : "double-click to open Kadence") color: Theme.muted font.pixelSize: 11 font.family: Theme.sansFamily elide: Text.ElideRight } // ---- controls, always visible -------------------------------------- RowLayout { Layout.fillWidth: true Layout.topMargin: 4 spacing: 6 MiniButton { label: mini.paused ? "Resume" : "Pause" filled: mini.paused visible: controller.running onClicked: mini.paused ? controller.resume() : controller.pause() } MiniButton { label: "Finish" visible: controller.running onClicked: controller.complete() } MiniButton { label: "+5" visible: controller.running onClicked: controller.extend(5) } MiniButton { label: "Start focus" filled: true visible: !controller.running onClicked: controller.startWork() } MiniButton { label: "Break" visible: !controller.running onClicked: controller.startBreak() } Item { Layout.fillWidth: true } } } // ---- progress, in a lane of its own ---------------------------------- Item { id: progressLane objectName: "progressLane" anchors.left: parent.left anchors.right: parent.right anchors.bottom: parent.bottom anchors.leftMargin: 1 anchors.rightMargin: 1 anchors.bottomMargin: 1 height: 3 clip: true Rectangle { width: parent.width * Math.max(0, Math.min(1, controller.progress)) height: parent.height color: mini.accent opacity: controller.running ? (mini.paused ? 0.45 : 1) : 0.25 Behavior on width { NumberAnimation { duration: 400 } } } } } }