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; -};