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

+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
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()
}
}
}
}