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 <noreply@anthropic.com>
This commit is contained in:
JulianandClaude Opus 5 committed 2026-09-11 08:58:51 +02:00
1 parent f6ecf0ddbf
commit 1581a760be
10 files changed
+540 -294

No files matched your search

+5 -4
View File
@@ -8,13 +8,13 @@ project(TNCat
set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON) 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_standard_project_setup(REQUIRES 6.5)
qt_add_executable(tncat qt_add_executable(tncat
src/main.cpp src/main.cpp
src/catcontroller.h src/catcontroller.cpp src/catcontroller.h src/catcontroller.cpp
src/traycontroller.h src/traycontroller.cpp
src/appsettings.h src/appsettings.cpp src/appsettings.h src/appsettings.cpp
src/platforminfo.h src/platforminfo.cpp src/platforminfo.h src/platforminfo.cpp
) )
@@ -22,7 +22,8 @@ qt_add_executable(tncat
target_include_directories(tncat PRIVATE src) target_include_directories(tncat PRIVATE src)
# QML files live in qml/ but register at the module root (TNCat.Main etc.) # 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) foreach(qmlfile IN LISTS TNCAT_QML)
set_source_files_properties(qml/${qmlfile}.qml PROPERTIES QT_RESOURCE_ALIAS ${qmlfile}.qml) set_source_files_properties(qml/${qmlfile}.qml PROPERTIES QT_RESOURCE_ALIAS ${qmlfile}.qml)
list(APPEND TNCAT_QML_FILES qml/${qmlfile}.qml) list(APPEND TNCAT_QML_FILES qml/${qmlfile}.qml)
@@ -37,7 +38,7 @@ qt_add_qml_module(tncat
) )
target_link_libraries(tncat PRIVATE 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 set_target_properties(tncat PROPERTIES
MACOSX_BUNDLE TRUE MACOSX_BUNDLE TRUE
+16 -9
View File
@@ -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 | | `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 | | `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/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/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/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/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/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 | | `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 cmake --build build
``` ```
Needs Qt 6.5 or newer. Required modules: Core, Gui, Quick, QuickControls2, Needs Qt 6.5 or newer. Required modules: Core, Gui, Quick, QuickShapes. There is
Widgets. Widgets is only there because `QSystemTrayIcon` needs it for its no dependency on Qt Quick Controls and none on Qt Widgets: every control in the
context menu. 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 ## 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 - **X11 today, Wayland later.** Both layouts are in the build; the platform picks
one at startup. one at startup.
- **No tray required.** If the desktop has no status-notifier host, the tray icon - **No tray, no menu bar item.** The cat itself is the whole interface:
is skipped and the cat itself stays the entry point: long-press or right-click right-click or long-press opens the settings window. Quit from the button
opens the settings window. 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 - **Touch works without hover.** Tap pets the cat, drag picks it up. The blush on
hover is a mouse-only extra, not a requirement. hover is a mouse-only extra, not a requirement.
- **Ground line setting.** The cat normally stands on the bottom of the available - **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 | | Click or tap | Pet the cat, it closes its eyes and purrs |
| Drag | Pick the cat up and put it somewhere else | | Drag | Pick the cat up and put it somewhere else |
| Hover | Blush and purr wiggle | | Hover | Blush and purr wiggle |
| Double-click or right-click | Settings window | | Double-click, right-click or long-press | Settings window |
| Tray menu | Tricks, nap toggle, settings, quit | | Settings window | Coat, size, pace, ground line, habits, tricks, quit |
+44
View File
@@ -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
}
}
}
+114
View File
@@ -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
}
}
}
+72
View File
@@ -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
}
}
}
+38
View File
@@ -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
}
}
+245 -165
View File
@@ -1,9 +1,8 @@
pragma ComponentBehavior: Bound pragma ComponentBehavior: Bound
import QtQuick import QtQuick
import QtQuick.Controls.Basic
import QtQuick.Layouts
import TNCat import TNCat
// Plain Qt Quick: no Controls, no Widgets, no Layouts.
Window { Window {
id: win id: win
@@ -18,194 +17,275 @@ Window {
{ name: qsTr("Sage"), color: "#a9bfa0" }, { name: qsTr("Sage"), color: "#a9bfa0" },
{ name: qsTr("Plum"), color: "#9a7bb0" } { name: qsTr("Plum"), color: "#9a7bb0" }
] ]
readonly property real margin: 22
readonly property real labelWidth: 96
width: 440 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 minimumWidth: 380
minimumHeight: 600 minimumHeight: 420
title: qsTr("TNCat") title: qsTr("TNCat")
color: "#faf7f2" color: "#faf7f2"
flags: Qt.Dialog | Qt.WindowStaysOnTopHint flags: Qt.Dialog
ColumnLayout { Flickable {
id: flick
anchors.fill: parent anchors.fill: parent
anchors.margins: 24 contentWidth: width
spacing: 18 contentHeight: content.implicitHeight + win.margin * 2
boundsBehavior: Flickable.StopAtBounds
RowLayout { Column {
Layout.fillWidth: true id: content
x: win.margin
y: win.margin
width: flick.width - win.margin * 2
spacing: 16 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 { // header
Layout.fillWidth: true Item {
title: qsTr("Coat") width: parent.width
Flow { height: Math.max(96, headerText.implicitHeight)
anchors.fill: parent
spacing: 10 Loader {
Repeater { id: preview
model: win.coats width: 106
Column { height: Math.round(106 * CatDefs.aspect)
id: swatch anchors.verticalCenter: parent.verticalCenter
required property var modelData active: win.visible
readonly property bool selected: Qt.colorEqual(win.settings.coat, modelData.color) sourceComponent: Cat {
spacing: 4 coat: win.settings.coat
Rectangle { mood: "sitting"
width: 40; height: 40 showCodes: false
radius: 20 petted: previewHover.hovered
color: swatch.modelData.color HoverHandler { id: previewHover }
border.width: swatch.selected ? 3 : 1 }
border.color: swatch.selected ? "#f25f7a" : "#d8d2ca" }
antialiasing: true
Accessible.role: Accessible.RadioButton Column {
Accessible.name: swatch.modelData.name id: headerText
Accessible.checked: swatch.selected anchors.left: preview.right
activeFocusOnTab: true anchors.leftMargin: 14
Keys.onSpacePressed: win.settings.coat = swatch.modelData.color anchors.right: parent.right
TapHandler { onTapped: win.settings.coat = swatch.modelData.color } anchors.verticalCenter: parent.verticalCenter
} spacing: 4
Label {
anchors.horizontalCenter: parent.horizontalCenter Text {
text: swatch.modelData.name text: qsTr("TNCat")
font.pixelSize: 11 font.pixelSize: 26
color: "#7a7484" 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 { SettingsGroup {
Layout.fillWidth: true width: parent.width
columns: 2 title: qsTr("THE CAT")
columnSpacing: 14 spacing: 12
rowSpacing: 12
Label { text: qsTr("Size") } Item {
Slider { width: parent.width
Layout.fillWidth: true height: 26
from: 72; to: 260; stepSize: 4 Text {
value: win.settings.catSize id: sizeLabel
onMoved: win.settings.catSize = value anchors.verticalCenter: parent.verticalCenter
} width: win.labelWidth
text: qsTr("Size")
Label { text: qsTr("Pace") } color: "#2f2a36"
Slider { font.pixelSize: 13
Layout.fillWidth: true }
from: 10; to: 200; stepSize: 5 AppSlider {
value: win.settings.walkSpeed anchors.left: sizeLabel.right
onMoved: win.settings.walkSpeed = value anchors.right: parent.right
} anchors.verticalCenter: parent.verticalCenter
from: 72; to: 260; stepSize: 4
Label { text: qsTr("Ground line") } value: win.settings.catSize
RowLayout { suffix: qsTr("px")
Layout.fillWidth: true onMoved: function(v) { win.settings.catSize = v }
Slider { }
id: marginSlider
Layout.fillWidth: true
from: 0; to: 600; stepSize: 10
value: win.settings.bottomMargin
onMoved: win.settings.bottomMargin = value
} }
Label {
Layout.preferredWidth: 54 Item {
text: qsTr("%1 px").arg(Math.round(marginSlider.value)) 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") } SettingsGroup {
Switch { width: parent.width
text: qsTr("Follow the mouse") title: qsTr("HABITS")
checked: win.settings.followCursor spacing: 12
onToggled: win.settings.followCursor = checked
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") } SettingsGroup {
Switch { width: parent.width
text: qsTr("Show G-code above the cat") title: qsTr("TRICKS")
checked: win.settings.showCodes
onToggled: win.settings.showCodes = checked 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") } Item {
Switch { width: parent.width
text: qsTr("Let a chip skitter past now and then") height: 34
checked: win.settings.chipsEnabled
onToggled: win.settings.chipsEnabled = checked
}
Label { text: qsTr("Coffee") } Text {
Switch { anchors.left: parent.left
text: qsTr("Take a coffee break now and then") anchors.verticalCenter: parent.verticalCenter
checked: win.settings.coffeeEnabled text: qsTr("%n pet(s) so far.", "", win.settings.petCount)
onToggled: win.settings.coffeeEnabled = checked color: "#7a7484"
} font.pixelSize: 12
}
Label { text: qsTr("Cat") } AppButton {
Switch { anchors.right: parent.right
text: checked ? qsTr("Napping") : qsTr("Awake") anchors.verticalCenter: parent.verticalCenter
checked: win.cat.paused text: qsTr("Quit")
onToggled: win.cat.paused = checked onClicked: Qt.quit()
} }
}
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()
} }
} }
} }
+6 -17
View File
@@ -1,31 +1,25 @@
#include <QApplication> #include <QGuiApplication>
#include <QQmlApplicationEngine> #include <QQmlApplicationEngine>
#include <QQuickStyle>
#include <QLoggingCategory>
#include "appsettings.h" #include "appsettings.h"
#include "catcontroller.h" #include "catcontroller.h"
#include "traycontroller.h"
int main(int argc, char *argv[]) int main(int argc, char *argv[])
{ {
QCoreApplication::setOrganizationName("TNCat"); QCoreApplication::setOrganizationName("TNCat");
QCoreApplication::setApplicationName("TNCat"); QCoreApplication::setApplicationName("TNCat");
QApplication::setQuitOnLastWindowClosed(false); QGuiApplication::setQuitOnLastWindowClosed(false);
// QApplication (not QGuiApplication) so QSystemTrayIcon can show a menu. QGuiApplication app(argc, argv);
QApplication app(argc, argv);
QQuickStyle::setStyle("Basic");
AppSettings settings;
CatController cat(&settings);
TrayController tray(&cat);
if (QGuiApplication::platformName().startsWith(QLatin1String("wayland"))) { if (QGuiApplication::platformName().startsWith(QLatin1String("wayland"))) {
qWarning("TNCat: Wayland does not let a client place its own window. " qWarning("TNCat: Wayland does not let a client place its own window. "
"Run with QT_QPA_PLATFORM=xcb for the cat to walk."); "Run with QT_QPA_PLATFORM=xcb for the cat to walk.");
} }
AppSettings settings;
CatController cat(&settings);
QQmlApplicationEngine engine; QQmlApplicationEngine engine;
engine.setInitialProperties({ engine.setInitialProperties({
{ "cat", QVariant::fromValue(&cat) }, { "cat", QVariant::fromValue(&cat) },
@@ -35,10 +29,5 @@ int main(int argc, char *argv[])
[] { QCoreApplication::exit(1); }, Qt::QueuedConnection); [] { QCoreApplication::exit(1); }, Qt::QueuedConnection);
engine.loadFromModule("TNCat", "Main"); 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(); return app.exec();
} }
-74
View File
@@ -1,74 +0,0 @@
#include "traycontroller.h"
#include "catcontroller.h"
#include <QCoreApplication>
#include <QPainter>
#include <QPainterPath>
#include <QLoggingCategory>
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;
}
-25
View File
@@ -1,25 +0,0 @@
#pragma once
#include <QObject>
#include <QSystemTrayIcon>
#include <QMenu>
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;
};