From 1581a760bef39dd35c2df265d4f0fecc41077389 Mon Sep 17 00:00:00 2001 From: Julian Date: Fri, 11 Sep 2026 08:58:51 +0200 Subject: [PATCH] Drop Qt Quick Controls and Qt Widgets The binary now links Core, Gui, Quick and QuickShapes, nothing else. Qt Widgets was there for one reason, QSystemTrayIcon, so the tray icon is gone and QApplication is back to QGuiApplication. The cat itself is the whole interface: right-click or long-press opens the settings window, which carries the tricks and a quit button. Ctrl+C in the terminal works as well. The settings window is rebuilt from Qt Quick primitives. AppButton, AppSlider, AppToggle and SettingsGroup replace the Controls types, keyboard handling and accessible roles included, and a Flickable with plain positioners replaces QtQuick.Layouts. Co-Authored-By: Claude Opus 5 --- CMakeLists.txt | 9 +- README.md | 25 ++- qml/AppButton.qml | 44 +++++ qml/AppSlider.qml | 114 ++++++++++++ qml/AppToggle.qml | 72 ++++++++ qml/SettingsGroup.qml | 38 ++++ qml/SettingsWindow.qml | 410 ++++++++++++++++++++++++----------------- src/main.cpp | 23 +-- src/traycontroller.cpp | 74 -------- src/traycontroller.h | 25 --- 10 files changed, 540 insertions(+), 294 deletions(-) create mode 100644 qml/AppButton.qml create mode 100644 qml/AppSlider.qml create mode 100644 qml/AppToggle.qml create mode 100644 qml/SettingsGroup.qml delete mode 100644 src/traycontroller.cpp delete mode 100644 src/traycontroller.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 3629c33..bf636b5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -8,13 +8,13 @@ project(TNCat set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) -find_package(Qt6 6.5 REQUIRED COMPONENTS Core Gui Quick QuickControls2 Widgets) +# No Qt Quick Controls and no Qt Widgets: the whole interface is plain Qt Quick. +find_package(Qt6 6.5 REQUIRED COMPONENTS Core Gui Quick QuickShapes) qt_standard_project_setup(REQUIRES 6.5) qt_add_executable(tncat src/main.cpp src/catcontroller.h src/catcontroller.cpp - src/traycontroller.h src/traycontroller.cpp src/appsettings.h src/appsettings.cpp src/platforminfo.h src/platforminfo.cpp ) @@ -22,7 +22,8 @@ qt_add_executable(tncat target_include_directories(tncat PRIVATE src) # QML files live in qml/ but register at the module root (TNCat.Main etc.) -set(TNCAT_QML Main Cat Chip Cup CatWindow ChipWindow CupWindow OverlayWindow SettingsWindow CatDefs) +set(TNCAT_QML Main Cat Chip Cup CatWindow ChipWindow CupWindow OverlayWindow + SettingsWindow AppButton AppSlider AppToggle SettingsGroup CatDefs) foreach(qmlfile IN LISTS TNCAT_QML) set_source_files_properties(qml/${qmlfile}.qml PROPERTIES QT_RESOURCE_ALIAS ${qmlfile}.qml) list(APPEND TNCAT_QML_FILES qml/${qmlfile}.qml) @@ -37,7 +38,7 @@ qt_add_qml_module(tncat ) target_link_libraries(tncat PRIVATE - Qt6::Core Qt6::Gui Qt6::Quick Qt6::QuickControls2 Qt6::Widgets) + Qt6::Core Qt6::Gui Qt6::Quick Qt6::QuickShapes) set_target_properties(tncat PROPERTIES MACOSX_BUNDLE TRUE diff --git a/README.md b/README.md index 86fab54..9e40343 100644 --- a/README.md +++ b/README.md @@ -22,10 +22,10 @@ plain Qt 6 / QML project. |---|---| | `CMakeLists.txt` | `qt_add_executable` + `qt_add_qml_module` (URI `TNCat`), macOS bundle, `.desktop` file on Linux | | `src/main.cpp` | Wires `AppSettings` and `CatController` into the QML engine, warns on Wayland | +| `qml/AppButton.qml`, `qml/AppSlider.qml`, `qml/AppToggle.qml`, `qml/SettingsGroup.qml` | The controls, built from primitives | | `src/catcontroller.*` | Mood machine (walk, run, sit, groom, stretch, sleep, love, alert, chase, bat, drink), movement tick, chip chase, coffee break, cursor tracking | | `src/appsettings.*` | `QSettings`-backed coat, size, pace, ground line, eye tracking, pet counter | | `src/platforminfo.*` | QML singleton `Platform`: platform name plus the X11 escape hatches | -| `src/traycontroller.*` | Tray icon and menu, silently skipped when the desktop has no tray | | `qml/Main.qml` | Non-visual root that picks the window layout for the platform | | `qml/CatWindow.qml`, `qml/ChipWindow.qml`, `qml/CupWindow.qml` | One small window each, positioned by the app. X11, macOS, Windows | | `qml/OverlayWindow.qml` | One screen-filling click-through window holding both. Wayland | @@ -56,9 +56,16 @@ cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release cmake --build build ``` -Needs Qt 6.5 or newer. Required modules: Core, Gui, Quick, QuickControls2, -Widgets. Widgets is only there because `QSystemTrayIcon` needs it for its -context menu. +Needs Qt 6.5 or newer. Required modules: Core, Gui, Quick, QuickShapes. There is +no dependency on Qt Quick Controls and none on Qt Widgets: every control in the +settings window is built from Qt Quick primitives, and there is no tray icon, +which is the only thing that pulled Widgets in. + +Check what actually got linked with: + +```bash +otool -L build/tncat.app/Contents/MacOS/tncat | grep -o 'Qt[A-Za-z]*\.framework' | sort -u +``` ## Environment switches @@ -93,9 +100,9 @@ Untested on real hardware so far. What the code already does for that target: - **X11 today, Wayland later.** Both layouts are in the build; the platform picks one at startup. -- **No tray required.** If the desktop has no status-notifier host, the tray icon - is skipped and the cat itself stays the entry point: long-press or right-click - opens the settings window. +- **No tray, no menu bar item.** The cat itself is the whole interface: + right-click or long-press opens the settings window. Quit from the button + there, with Ctrl+C in the terminal that started it, or by killing the process. - **Touch works without hover.** Tap pets the cat, drag picks it up. The blush on hover is a mouse-only extra, not a requirement. - **Ground line setting.** The cat normally stands on the bottom of the available @@ -122,5 +129,5 @@ What still needs checking on the actual machine: | Click or tap | Pet the cat, it closes its eyes and purrs | | Drag | Pick the cat up and put it somewhere else | | Hover | Blush and purr wiggle | -| Double-click or right-click | Settings window | -| Tray menu | Tricks, nap toggle, settings, quit | +| Double-click, right-click or long-press | Settings window | +| Settings window | Coat, size, pace, ground line, habits, tricks, quit | diff --git a/qml/AppButton.qml b/qml/AppButton.qml new file mode 100644 index 0000000..8a626d9 --- /dev/null +++ b/qml/AppButton.qml @@ -0,0 +1,44 @@ +pragma ComponentBehavior: Bound +import QtQuick + +// A push button built from primitives, so the app needs no Qt Quick Controls. +Rectangle { + id: button + + property alias text: label.text + signal clicked() + + implicitWidth: label.implicitWidth + 26 + implicitHeight: 30 + radius: 6 + color: tap.pressed ? "#e2dcd2" : hover.hovered ? "#efeae1" : "#f7f4ee" + border.width: button.activeFocus ? 2 : 1 + border.color: button.activeFocus ? "#4d7ea8" : "#d8d2ca" + antialiasing: true + activeFocusOnTab: true + + Accessible.role: Accessible.Button + Accessible.name: label.text + Accessible.onPressAction: button.clicked() + + Text { + id: label + anchors.centerIn: parent + color: "#2f2a36" + font.pixelSize: 13 + } + + HoverHandler { id: hover } + + TapHandler { + id: tap + onTapped: button.clicked() + } + + Keys.onPressed: function(event) { + if (event.key === Qt.Key_Space || event.key === Qt.Key_Return || event.key === Qt.Key_Enter) { + button.clicked() + event.accepted = true + } + } +} diff --git a/qml/AppSlider.qml b/qml/AppSlider.qml new file mode 100644 index 0000000..beab680 --- /dev/null +++ b/qml/AppSlider.qml @@ -0,0 +1,114 @@ +pragma ComponentBehavior: Bound +import QtQuick + +// A horizontal slider built from primitives. Reports every change through +// moved() rather than writing back, so the owner stays the single source. +Item { + id: slider + + property real from: 0 + property real to: 100 + property real stepSize: 1 + property real value: 0 + property string suffix: "" + + signal moved(real value) + + readonly property real span: Math.max(0.0001, to - from) + readonly property real position: Math.min(1, Math.max(0, (value - from) / span)) + + function valueAt(px) { + const usable = Math.max(1, track.width - handle.width) + const ratio = Math.min(1, Math.max(0, (px - handle.width / 2) / usable)) + const raw = from + ratio * span + return Math.min(to, Math.max(from, Math.round(raw / stepSize) * stepSize)) + } + + function nudge(steps) { + slider.moved(Math.min(to, Math.max(from, value + steps * stepSize))) + } + + implicitWidth: 220 + implicitHeight: 26 + activeFocusOnTab: true + + Accessible.role: Accessible.Slider + Accessible.name: qsTr("Slider") + Accessible.onIncreaseAction: slider.nudge(1) + Accessible.onDecreaseAction: slider.nudge(-1) + + Text { + id: readout + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + width: slider.suffix.length > 0 ? 52 : 0 + visible: slider.suffix.length > 0 + horizontalAlignment: Text.AlignRight + text: Math.round(slider.value) + " " + slider.suffix + color: "#7a7484" + font.pixelSize: 12 + } + + Item { + id: track + anchors.left: parent.left + anchors.right: readout.visible ? readout.left : parent.right + anchors.rightMargin: readout.visible ? 8 : 0 + anchors.verticalCenter: parent.verticalCenter + height: parent.height + + Rectangle { + anchors.verticalCenter: parent.verticalCenter + width: parent.width + height: 4 + radius: 2 + color: "#ded8cf" + antialiasing: true + + Rectangle { + width: parent.width * slider.position + height: parent.height + radius: parent.radius + color: "#4d7ea8" + antialiasing: true + } + } + + Rectangle { + id: handle + x: (track.width - width) * slider.position + anchors.verticalCenter: parent.verticalCenter + width: 18 + height: 18 + radius: 9 + color: "white" + border.width: slider.activeFocus ? 3 : 2 + border.color: slider.activeFocus ? "#2f5f85" : "#4d7ea8" + antialiasing: true + } + + TapHandler { + onTapped: function(point) { slider.moved(slider.valueAt(point.position.x)) } + } + + DragHandler { + target: null + xAxis.enabled: true + yAxis.enabled: false + onCentroidChanged: { + if (active) + slider.moved(slider.valueAt(centroid.position.x)) + } + } + } + + Keys.onPressed: function(event) { + if (event.key === Qt.Key_Left || event.key === Qt.Key_Down) { + slider.nudge(-1) + event.accepted = true + } else if (event.key === Qt.Key_Right || event.key === Qt.Key_Up) { + slider.nudge(1) + event.accepted = true + } + } +} diff --git a/qml/AppToggle.qml b/qml/AppToggle.qml new file mode 100644 index 0000000..a5cbf08 --- /dev/null +++ b/qml/AppToggle.qml @@ -0,0 +1,72 @@ +pragma ComponentBehavior: Bound +import QtQuick + +// An on/off switch built from primitives. +Item { + id: toggle + + property bool checked: false + property alias text: label.text + signal toggled(bool checked) + + function flip() { + toggle.toggled(!toggle.checked) + } + + implicitWidth: track.width + 10 + label.implicitWidth + implicitHeight: Math.max(22, label.implicitHeight) + activeFocusOnTab: true + + Accessible.role: Accessible.CheckBox + Accessible.name: label.text + Accessible.checked: toggle.checked + Accessible.onToggleAction: toggle.flip() + + Rectangle { + id: track + anchors.verticalCenter: parent.verticalCenter + width: 40 + height: 22 + radius: 11 + color: toggle.checked ? "#4d7ea8" : "#cfc9c0" + border.width: toggle.activeFocus ? 2 : 0 + border.color: "#2f5f85" + antialiasing: true + + Behavior on color { ColorAnimation { duration: 150 } } + + Rectangle { + y: 3 + x: toggle.checked ? track.width - width - 3 : 3 + width: 16 + height: 16 + radius: 8 + color: "white" + antialiasing: true + + Behavior on x { NumberAnimation { duration: 150; easing.type: Easing.OutQuad } } + } + } + + Text { + id: label + anchors.left: track.right + anchors.leftMargin: 10 + anchors.verticalCenter: parent.verticalCenter + width: Math.max(0, toggle.width - track.width - 10) + elide: Text.ElideRight + color: "#2f2a36" + font.pixelSize: 13 + } + + TapHandler { + onTapped: toggle.flip() + } + + Keys.onPressed: function(event) { + if (event.key === Qt.Key_Space || event.key === Qt.Key_Return || event.key === Qt.Key_Enter) { + toggle.flip() + event.accepted = true + } + } +} diff --git a/qml/SettingsGroup.qml b/qml/SettingsGroup.qml new file mode 100644 index 0000000..5fd07a7 --- /dev/null +++ b/qml/SettingsGroup.qml @@ -0,0 +1,38 @@ +pragma ComponentBehavior: Bound +import QtQuick + +// A titled panel. Children go into the column below the title. +Rectangle { + id: group + + property alias title: heading.text + property real spacing: 10 + default property alias content: column.data + + implicitHeight: heading.height + 8 + column.implicitHeight + 24 + radius: 8 + color: "#ffffff" + border.width: 1 + border.color: "#e7e1d8" + antialiasing: true + + Text { + id: heading + x: 14 + y: 12 + color: "#7a7484" + font.pixelSize: 11 + font.bold: true + } + + Column { + id: column + anchors.left: parent.left + anchors.right: parent.right + anchors.leftMargin: 14 + anchors.rightMargin: 14 + anchors.top: heading.bottom + anchors.topMargin: 8 + spacing: group.spacing + } +} diff --git a/qml/SettingsWindow.qml b/qml/SettingsWindow.qml index 2277b40..c704202 100644 --- a/qml/SettingsWindow.qml +++ b/qml/SettingsWindow.qml @@ -1,9 +1,8 @@ pragma ComponentBehavior: Bound import QtQuick -import QtQuick.Controls.Basic -import QtQuick.Layouts import TNCat +// Plain Qt Quick: no Controls, no Widgets, no Layouts. Window { id: win @@ -18,194 +17,275 @@ Window { { name: qsTr("Sage"), color: "#a9bfa0" }, { name: qsTr("Plum"), color: "#9a7bb0" } ] + readonly property real margin: 22 + readonly property real labelWidth: 96 width: 440 - height: 700 + // Tall enough for the whole panel, but never taller than the screen; the + // Flickable takes over when it has to be shorter. + height: Math.min(830, Screen.desktopAvailableHeight - 60) minimumWidth: 380 - minimumHeight: 600 + minimumHeight: 420 title: qsTr("TNCat") color: "#faf7f2" - flags: Qt.Dialog | Qt.WindowStaysOnTopHint + flags: Qt.Dialog - ColumnLayout { + Flickable { + id: flick anchors.fill: parent - anchors.margins: 24 - spacing: 18 + contentWidth: width + contentHeight: content.implicitHeight + win.margin * 2 + boundsBehavior: Flickable.StopAtBounds - RowLayout { - Layout.fillWidth: true + Column { + id: content + x: win.margin + y: win.margin + width: flick.width - win.margin * 2 spacing: 16 - Loader { - Layout.preferredWidth: 110 - Layout.preferredHeight: Math.round(110 * CatDefs.aspect) - active: win.visible - sourceComponent: Cat { - coat: win.settings.coat - mood: "sitting" - showCodes: false - petted: previewHover.hovered - HoverHandler { id: previewHover } - } - } - ColumnLayout { - Layout.fillWidth: true - spacing: 4 - Label { - Layout.fillWidth: true - text: qsTr("TNCat") - font.pixelSize: 26 - font.bold: true - color: "#2f2a36" - } - Label { - Layout.fillWidth: true - wrapMode: Text.WordWrap - text: qsTr("A little cat that wanders along the bottom of your screen. Drag it around, pet it with a click, and it will purr.") - color: "#7a7484" - } - } - } - GroupBox { - Layout.fillWidth: true - title: qsTr("Coat") - Flow { - anchors.fill: parent - spacing: 10 - Repeater { - model: win.coats - Column { - id: swatch - required property var modelData - readonly property bool selected: Qt.colorEqual(win.settings.coat, modelData.color) - spacing: 4 - Rectangle { - width: 40; height: 40 - radius: 20 - color: swatch.modelData.color - border.width: swatch.selected ? 3 : 1 - border.color: swatch.selected ? "#f25f7a" : "#d8d2ca" - antialiasing: true - Accessible.role: Accessible.RadioButton - Accessible.name: swatch.modelData.name - Accessible.checked: swatch.selected - activeFocusOnTab: true - Keys.onSpacePressed: win.settings.coat = swatch.modelData.color - TapHandler { onTapped: win.settings.coat = swatch.modelData.color } - } - Label { - anchors.horizontalCenter: parent.horizontalCenter - text: swatch.modelData.name - font.pixelSize: 11 - color: "#7a7484" + // header + Item { + width: parent.width + height: Math.max(96, headerText.implicitHeight) + + Loader { + id: preview + width: 106 + height: Math.round(106 * CatDefs.aspect) + anchors.verticalCenter: parent.verticalCenter + active: win.visible + sourceComponent: Cat { + coat: win.settings.coat + mood: "sitting" + showCodes: false + petted: previewHover.hovered + HoverHandler { id: previewHover } + } + } + + Column { + id: headerText + anchors.left: preview.right + anchors.leftMargin: 14 + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + spacing: 4 + + Text { + text: qsTr("TNCat") + font.pixelSize: 26 + font.bold: true + color: "#2f2a36" + } + Text { + width: headerText.width + wrapMode: Text.WordWrap + text: qsTr("A little cat that wanders along the bottom of your screen. Click to pet it, drag it somewhere else.") + color: "#7a7484" + font.pixelSize: 12 + } + } + } + + SettingsGroup { + width: parent.width + title: qsTr("COAT") + + Flow { + width: parent.width + spacing: 12 + + Repeater { + model: win.coats + + Column { + id: swatch + required property var modelData + readonly property bool selected: Qt.colorEqual(win.settings.coat, modelData.color) + spacing: 4 + + Rectangle { + width: 40 + height: 40 + radius: 20 + color: swatch.modelData.color + border.width: swatch.selected ? 3 : 1 + border.color: swatch.selected ? "#f25f7a" : "#d8d2ca" + antialiasing: true + activeFocusOnTab: true + + Accessible.role: Accessible.RadioButton + Accessible.name: swatch.modelData.name + Accessible.checked: swatch.selected + Accessible.onPressAction: win.settings.coat = swatch.modelData.color + + TapHandler { onTapped: win.settings.coat = swatch.modelData.color } + Keys.onSpacePressed: win.settings.coat = swatch.modelData.color + } + + Text { + anchors.horizontalCenter: parent.horizontalCenter + text: swatch.modelData.name + font.pixelSize: 11 + color: "#7a7484" + } } } } } - } - GridLayout { - Layout.fillWidth: true - columns: 2 - columnSpacing: 14 - rowSpacing: 12 + SettingsGroup { + width: parent.width + title: qsTr("THE CAT") + spacing: 12 - Label { text: qsTr("Size") } - Slider { - Layout.fillWidth: true - from: 72; to: 260; stepSize: 4 - value: win.settings.catSize - onMoved: win.settings.catSize = value - } - - Label { text: qsTr("Pace") } - Slider { - Layout.fillWidth: true - from: 10; to: 200; stepSize: 5 - value: win.settings.walkSpeed - onMoved: win.settings.walkSpeed = value - } - - Label { text: qsTr("Ground line") } - RowLayout { - Layout.fillWidth: true - Slider { - id: marginSlider - Layout.fillWidth: true - from: 0; to: 600; stepSize: 10 - value: win.settings.bottomMargin - onMoved: win.settings.bottomMargin = value + Item { + width: parent.width + height: 26 + Text { + id: sizeLabel + anchors.verticalCenter: parent.verticalCenter + width: win.labelWidth + text: qsTr("Size") + color: "#2f2a36" + font.pixelSize: 13 + } + AppSlider { + anchors.left: sizeLabel.right + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + from: 72; to: 260; stepSize: 4 + value: win.settings.catSize + suffix: qsTr("px") + onMoved: function(v) { win.settings.catSize = v } + } } - Label { - Layout.preferredWidth: 54 - text: qsTr("%1 px").arg(Math.round(marginSlider.value)) + + Item { + width: parent.width + height: 26 + Text { + id: paceLabel + anchors.verticalCenter: parent.verticalCenter + width: win.labelWidth + text: qsTr("Pace") + color: "#2f2a36" + font.pixelSize: 13 + } + AppSlider { + anchors.left: paceLabel.right + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + from: 10; to: 200; stepSize: 5 + value: win.settings.walkSpeed + onMoved: function(v) { win.settings.walkSpeed = v } + } + } + + Item { + width: parent.width + height: 26 + Text { + id: groundLabel + anchors.verticalCenter: parent.verticalCenter + width: win.labelWidth + text: qsTr("Ground line") + color: "#2f2a36" + font.pixelSize: 13 + } + AppSlider { + anchors.left: groundLabel.right + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + from: 0; to: 600; stepSize: 10 + value: win.settings.bottomMargin + suffix: qsTr("px") + onMoved: function(v) { win.settings.bottomMargin = v } + } + } + + Text { + width: parent.width + wrapMode: Text.WordWrap + text: qsTr("The ground line lifts the cat off the bottom edge, so it can walk above a taskbar or a row of softkeys.") + color: "#9b95a3" + font.pixelSize: 11 } } - Label { text: qsTr("Eyes") } - Switch { - text: qsTr("Follow the mouse") - checked: win.settings.followCursor - onToggled: win.settings.followCursor = checked + SettingsGroup { + width: parent.width + title: qsTr("HABITS") + spacing: 12 + + AppToggle { + width: parent.width + checked: win.settings.followCursor + text: qsTr("Follow the mouse with its eyes") + onToggled: function(v) { win.settings.followCursor = v } + } + AppToggle { + width: parent.width + checked: win.settings.showCodes + text: qsTr("Announce what it does in G-code") + onToggled: function(v) { win.settings.showCodes = v } + } + AppToggle { + width: parent.width + checked: win.settings.chipsEnabled + text: qsTr("Chase a chip now and then") + onToggled: function(v) { win.settings.chipsEnabled = v } + } + AppToggle { + width: parent.width + checked: win.settings.coffeeEnabled + text: qsTr("Stop for coffee now and then") + onToggled: function(v) { win.settings.coffeeEnabled = v } + } + AppToggle { + width: parent.width + checked: win.cat.paused + text: win.cat.paused ? qsTr("Napping, nothing will wake it") : qsTr("Awake and on the move") + onToggled: function(v) { win.cat.paused = v } + } } - Label { text: qsTr("Codes") } - Switch { - text: qsTr("Show G-code above the cat") - checked: win.settings.showCodes - onToggled: win.settings.showCodes = checked + SettingsGroup { + width: parent.width + title: qsTr("TRICKS") + + Flow { + width: parent.width + spacing: 8 + + AppButton { text: qsTr("Sit"); onClicked: win.cat.doTrick(CatController.Sitting) } + AppButton { text: qsTr("Groom"); onClicked: win.cat.doTrick(CatController.Grooming) } + AppButton { text: qsTr("Stretch"); onClicked: win.cat.doTrick(CatController.Stretching) } + AppButton { text: qsTr("Dash"); onClicked: win.cat.doTrick(CatController.Running) } + AppButton { text: qsTr("Nap"); onClicked: win.cat.doTrick(CatController.Sleeping) } + AppButton { text: qsTr("Chip"); onClicked: win.cat.tossChip() } + AppButton { text: qsTr("Coffee"); onClicked: win.cat.coffeeBreak() } + } } - Label { text: qsTr("Chips") } - Switch { - text: qsTr("Let a chip skitter past now and then") - checked: win.settings.chipsEnabled - onToggled: win.settings.chipsEnabled = checked - } + Item { + width: parent.width + height: 34 - Label { text: qsTr("Coffee") } - Switch { - text: qsTr("Take a coffee break now and then") - checked: win.settings.coffeeEnabled - onToggled: win.settings.coffeeEnabled = checked - } - - Label { text: qsTr("Cat") } - Switch { - text: checked ? qsTr("Napping") : qsTr("Awake") - checked: win.cat.paused - onToggled: win.cat.paused = checked - } - } - - GroupBox { - Layout.fillWidth: true - title: qsTr("Tricks") - Flow { - anchors.fill: parent - spacing: 8 - Button { text: qsTr("Sit"); onClicked: win.cat.doTrick(CatController.Sitting) } - Button { text: qsTr("Groom"); onClicked: win.cat.doTrick(CatController.Grooming) } - Button { text: qsTr("Stretch"); onClicked: win.cat.doTrick(CatController.Stretching) } - Button { text: qsTr("Dash"); onClicked: win.cat.doTrick(CatController.Running) } - Button { text: qsTr("Nap"); onClicked: win.cat.doTrick(CatController.Sleeping) } - Button { text: qsTr("Toss a chip"); onClicked: win.cat.tossChip() } - Button { text: qsTr("Coffee"); onClicked: win.cat.coffeeBreak() } - } - } - - Item { Layout.fillHeight: true } - - RowLayout { - Layout.fillWidth: true - Label { - Layout.fillWidth: true - text: qsTr("%n pet(s) so far.", "", win.settings.petCount) - color: "#7a7484" - } - Button { - text: qsTr("Quit") - onClicked: Qt.quit() + Text { + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + text: qsTr("%n pet(s) so far.", "", win.settings.petCount) + color: "#7a7484" + font.pixelSize: 12 + } + AppButton { + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + text: qsTr("Quit") + onClicked: Qt.quit() + } } } } diff --git a/src/main.cpp b/src/main.cpp index fba0790..2ada533 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,31 +1,25 @@ -#include +#include #include -#include -#include #include "appsettings.h" #include "catcontroller.h" -#include "traycontroller.h" int main(int argc, char *argv[]) { QCoreApplication::setOrganizationName("TNCat"); QCoreApplication::setApplicationName("TNCat"); - QApplication::setQuitOnLastWindowClosed(false); + QGuiApplication::setQuitOnLastWindowClosed(false); - // QApplication (not QGuiApplication) so QSystemTrayIcon can show a menu. - QApplication app(argc, argv); - QQuickStyle::setStyle("Basic"); - - AppSettings settings; - CatController cat(&settings); - TrayController tray(&cat); + QGuiApplication app(argc, argv); if (QGuiApplication::platformName().startsWith(QLatin1String("wayland"))) { qWarning("TNCat: Wayland does not let a client place its own window. " "Run with QT_QPA_PLATFORM=xcb for the cat to walk."); } + AppSettings settings; + CatController cat(&settings); + QQmlApplicationEngine engine; engine.setInitialProperties({ { "cat", QVariant::fromValue(&cat) }, @@ -35,10 +29,5 @@ int main(int argc, char *argv[]) [] { QCoreApplication::exit(1); }, Qt::QueuedConnection); engine.loadFromModule("TNCat", "Main"); - if (QObject *root = engine.rootObjects().value(0)) { - QObject::connect(&tray, &TrayController::settingsRequested, root, - [root] { QMetaObject::invokeMethod(root, "openSettings"); }); - } - return app.exec(); } diff --git a/src/traycontroller.cpp b/src/traycontroller.cpp deleted file mode 100644 index 134d116..0000000 --- a/src/traycontroller.cpp +++ /dev/null @@ -1,74 +0,0 @@ -#include "traycontroller.h" -#include "catcontroller.h" - -#include -#include -#include -#include - -TrayController::TrayController(CatController *cat, QObject *parent) - : QObject(parent) - , m_cat(cat) -{ - QMenu *tricks = m_menu.addMenu(tr("Tricks")); - const struct { const char *label; CatController::Mood mood; } entries[] = { - { QT_TR_NOOP("Sit"), CatController::Mood::Sitting }, - { QT_TR_NOOP("Groom"), CatController::Mood::Grooming }, - { QT_TR_NOOP("Stretch"), CatController::Mood::Stretching }, - { QT_TR_NOOP("Dash"), CatController::Mood::Running }, - { QT_TR_NOOP("Nap"), CatController::Mood::Sleeping }, - { QT_TR_NOOP("Coffee break"), CatController::Mood::Drinking }, - }; - for (const auto &e : entries) { - const auto mood = e.mood; - tricks->addAction(tr(e.label), this, [this, mood] { m_cat->doTrick(mood); }); - } - - m_menu.addAction(tr("Toss a chip"), m_cat, &CatController::tossChip); - - m_pauseAction = m_menu.addAction(tr("Let the cat sleep")); - m_pauseAction->setCheckable(true); - connect(m_pauseAction, &QAction::toggled, m_cat, &CatController::setPaused); - connect(m_cat, &CatController::pausedChanged, this, [this] { - m_pauseAction->setChecked(m_cat->paused()); - }); - - m_menu.addAction(tr("Settings…"), this, &TrayController::settingsRequested); - m_menu.addSeparator(); - m_menu.addAction(tr("Quit TNCat"), qApp, &QCoreApplication::quit); - - m_tray.setIcon(makeIcon()); - m_tray.setToolTip(tr("TNCat")); - m_tray.setContextMenu(&m_menu); - - // Not every X11 desktop runs a status-notifier host. Without a tray the - // cat itself is the only entry point, so say so instead of failing mutely. - if (QSystemTrayIcon::isSystemTrayAvailable()) - m_tray.show(); - else - qWarning("TNCat: no system tray on this desktop. Right-click the cat for settings."); -} - -QIcon TrayController::makeIcon() -{ - QPixmap pm(44, 44); - pm.fill(Qt::transparent); - QPainter p(&pm); - p.setRenderHint(QPainter::Antialiasing); - p.setPen(Qt::NoPen); - p.setBrush(Qt::black); - - QPainterPath path; - path.addRoundedRect(QRectF(5, 12, 34, 28), 14, 14); - path.moveTo(8, 22); path.lineTo(9, 5); path.lineTo(21, 14); path.closeSubpath(); - path.moveTo(36, 22); path.lineTo(35, 5); path.lineTo(23, 14); path.closeSubpath(); - p.drawPath(path); - - p.setBrush(Qt::white); - p.drawEllipse(QRectF(13, 22, 6, 7)); - p.drawEllipse(QRectF(25, 22, 6, 7)); - - QIcon icon(pm); - icon.setIsMask(true); - return icon; -} diff --git a/src/traycontroller.h b/src/traycontroller.h deleted file mode 100644 index 9c27fd1..0000000 --- a/src/traycontroller.h +++ /dev/null @@ -1,25 +0,0 @@ -#pragma once - -#include -#include -#include - -class CatController; - -class TrayController : public QObject -{ - Q_OBJECT -public: - explicit TrayController(CatController *cat, QObject *parent = nullptr); - -signals: - void settingsRequested(); - -private: - static QIcon makeIcon(); - - CatController *m_cat; - QSystemTrayIcon m_tray; - QMenu m_menu; - QAction *m_pauseAction = nullptr; -};