Rename to TNCat, add chip chase, G-codes and a Wayland layout
The project is now TNCat, after HEIDENHAIN's TNC controls. Binary, QML module URI, settings path, bundle id and .desktop entry follow; the environment switches are TNCAT_*. Entertainment: a curled steel chip skitters in every minute or two, the cat runs it down and bats it away a few times before sitting back looking pleased. Mood changes flash the matching machine code above its head, G00 for a sprint, G01 for a stroll, M08 while it washes, G28 for a stretch, M30 for a nap. Window handling was split into two layouts. X11 and macOS keep one small window per object, positioned by the app. Wayland gets a screen-filling overlay whose input region is masked down to the cat, since a Wayland client cannot place its own window. Overlay mode is automatic there and available elsewhere via TNCAT_OVERLAY=1. Also adds a ground-line setting so the cat can walk above a softkey row. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
acef398f49
commit
0f1df75a1d
19 files changed
+817
-185
No files matched your search
+22
-20
@@ -1,17 +1,17 @@
|
|||||||
cmake_minimum_required(VERSION 3.21)
|
cmake_minimum_required(VERSION 3.21)
|
||||||
|
|
||||||
project(Workcat
|
project(TNCat
|
||||||
VERSION 0.2.0
|
VERSION 0.3.0
|
||||||
DESCRIPTION "A cute desktop cat that wanders along the bottom of your screen"
|
DESCRIPTION "A cute desktop cat that wanders along the bottom of your screen"
|
||||||
LANGUAGES CXX)
|
LANGUAGES CXX)
|
||||||
|
|
||||||
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.8 REQUIRED COMPONENTS Core Gui Quick QuickControls2 Widgets)
|
find_package(Qt6 6.5 REQUIRED COMPONENTS Core Gui Quick QuickControls2 Widgets)
|
||||||
qt_standard_project_setup(REQUIRES 6.8)
|
qt_standard_project_setup(REQUIRES 6.5)
|
||||||
|
|
||||||
qt_add_executable(workcat
|
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/traycontroller.h src/traycontroller.cpp
|
||||||
@@ -19,44 +19,46 @@ qt_add_executable(workcat
|
|||||||
src/platforminfo.h src/platforminfo.cpp
|
src/platforminfo.h src/platforminfo.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
target_include_directories(workcat PRIVATE src)
|
target_include_directories(tncat PRIVATE src)
|
||||||
|
|
||||||
# QML files live in qml/ but register at the module root (Workcat.Main etc.)
|
# QML files live in qml/ but register at the module root (TNCat.Main etc.)
|
||||||
set(WORKCAT_QML Main Cat SettingsWindow)
|
set(TNCAT_QML Main Cat Chip CatWindow ChipWindow OverlayWindow SettingsWindow CatMoods)
|
||||||
foreach(qmlfile IN LISTS WORKCAT_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 WORKCAT_QML_FILES qml/${qmlfile}.qml)
|
list(APPEND TNCAT_QML_FILES qml/${qmlfile}.qml)
|
||||||
endforeach()
|
endforeach()
|
||||||
|
|
||||||
qt_add_qml_module(workcat
|
set_source_files_properties(qml/CatMoods.qml PROPERTIES QT_QML_SINGLETON_TYPE TRUE)
|
||||||
URI Workcat
|
|
||||||
|
qt_add_qml_module(tncat
|
||||||
|
URI TNCat
|
||||||
VERSION 1.0
|
VERSION 1.0
|
||||||
QML_FILES ${WORKCAT_QML_FILES}
|
QML_FILES ${TNCAT_QML_FILES}
|
||||||
)
|
)
|
||||||
|
|
||||||
target_link_libraries(workcat PRIVATE
|
target_link_libraries(tncat PRIVATE
|
||||||
Qt6::Core Qt6::Gui Qt6::Quick Qt6::QuickControls2 Qt6::Widgets)
|
Qt6::Core Qt6::Gui Qt6::Quick Qt6::QuickControls2 Qt6::Widgets)
|
||||||
|
|
||||||
set_target_properties(workcat PROPERTIES
|
set_target_properties(tncat PROPERTIES
|
||||||
MACOSX_BUNDLE TRUE
|
MACOSX_BUNDLE TRUE
|
||||||
MACOSX_BUNDLE_INFO_PLIST ${CMAKE_SOURCE_DIR}/Info.plist.in
|
MACOSX_BUNDLE_INFO_PLIST ${CMAKE_SOURCE_DIR}/Info.plist.in
|
||||||
MACOSX_BUNDLE_GUI_IDENTIFIER app.workcat.example
|
MACOSX_BUNDLE_GUI_IDENTIFIER app.tncat.example
|
||||||
MACOSX_BUNDLE_BUNDLE_NAME Workcat
|
MACOSX_BUNDLE_BUNDLE_NAME TNCat
|
||||||
MACOSX_BUNDLE_BUNDLE_VERSION ${PROJECT_VERSION}
|
MACOSX_BUNDLE_BUNDLE_VERSION ${PROJECT_VERSION}
|
||||||
MACOSX_BUNDLE_SHORT_VERSION_STRING ${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}
|
MACOSX_BUNDLE_SHORT_VERSION_STRING ${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}
|
||||||
WIN32_EXECUTABLE TRUE)
|
WIN32_EXECUTABLE TRUE)
|
||||||
|
|
||||||
include(GNUInstallDirs)
|
include(GNUInstallDirs)
|
||||||
install(TARGETS workcat
|
install(TARGETS tncat
|
||||||
BUNDLE DESTINATION .
|
BUNDLE DESTINATION .
|
||||||
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR})
|
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR})
|
||||||
|
|
||||||
if(UNIX AND NOT APPLE)
|
if(UNIX AND NOT APPLE)
|
||||||
install(FILES linux/workcat.desktop DESTINATION ${CMAKE_INSTALL_DATADIR}/applications)
|
install(FILES linux/tncat.desktop DESTINATION ${CMAKE_INSTALL_DATADIR}/applications)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
qt_generate_deploy_qml_app_script(
|
qt_generate_deploy_qml_app_script(
|
||||||
TARGET workcat
|
TARGET tncat
|
||||||
OUTPUT_SCRIPT deploy_script
|
OUTPUT_SCRIPT deploy_script
|
||||||
MACOS_BUNDLE_POST_BUILD
|
MACOS_BUNDLE_POST_BUILD
|
||||||
NO_UNSUPPORTED_PLATFORM_ERROR
|
NO_UNSUPPORTED_PLATFORM_ERROR
|
||||||
|
|||||||
@@ -1,29 +1,41 @@
|
|||||||
# Workcat (Qt Quick / CMake example)
|
# TNCat
|
||||||
|
|
||||||
A little cat that wanders along the bottom edge of your screen, sits down,
|
A little cat that wanders along the bottom edge of the screen, sits down, grooms
|
||||||
grooms itself, stretches, dashes off and takes naps. Drag it around, click to
|
itself, stretches, dashes off and takes naps. Every now and then a curled steel
|
||||||
pet it, and it purrs. It does nothing else — no window closing, no monitoring.
|
chip skitters past and the cat goes after it. Drag it around, tap it to pet it,
|
||||||
|
and it purrs.
|
||||||
|
|
||||||
Inspired by [workcat.app](https://workcat.app/en/), rebuilt as a plain Qt 6 /
|
It announces what it is doing in the machine's own language: `G00` when it
|
||||||
QML project. Primary target is **Linux / X11**; development happens on macOS and
|
sprints, `G01` when it strolls, `M08` while it washes, `G28` when it stretches
|
||||||
both are supported by the same code.
|
back to reference, `M30` when it goes to sleep.
|
||||||
|
|
||||||
|
The name is `TNC` plus `cat`, because the intended home is a HEIDENHAIN control
|
||||||
|
(TNC7, TNC 640). Primary target is therefore **Linux / X11**; development happens
|
||||||
|
on macOS and both are supported by the same code.
|
||||||
|
|
||||||
|
Inspired by [workcat.app](https://workcat.app/en/), rebuilt from scratch as a
|
||||||
|
plain Qt 6 / QML project.
|
||||||
|
|
||||||
## Layout
|
## Layout
|
||||||
|
|
||||||
| Path | Purpose |
|
| Path | Purpose |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `CMakeLists.txt` | `qt_add_executable` + `qt_add_qml_module` (URI `Workcat`), 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 |
|
||||||
| `src/catcontroller.*` | Mood machine (walk, run, sit, groom, stretch, sleep, love, alert), movement tick, cursor tracking |
|
| `src/catcontroller.*` | Mood machine (walk, run, sit, groom, stretch, sleep, love, alert, chase, bat), movement tick, chip chase, cursor tracking |
|
||||||
| `src/appsettings.*` | `QSettings`-backed coat, size, pace, 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 two 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 |
|
| `src/traycontroller.*` | Tray icon and menu, silently skipped when the desktop has no tray |
|
||||||
| `qml/Main.qml` | Frameless, transparent, always-on-top window that *is* the cat |
|
| `qml/Main.qml` | Non-visual root that picks the window layout for the platform |
|
||||||
|
| `qml/CatWindow.qml`, `qml/ChipWindow.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/Cat.qml` | The drawing: rounded primitives plus `Shape` paths for tail, ears and mouth |
|
| `qml/Cat.qml` | The drawing: rounded primitives plus `Shape` paths for tail, ears and mouth |
|
||||||
| `qml/SettingsWindow.qml` | Coat swatches, size, pace, eye tracking, trick buttons |
|
| `qml/Chip.qml` | The curled chip, a stroked `Shape` spiral |
|
||||||
|
| `qml/SettingsWindow.qml` | Coat swatches, size, pace, ground line, eye tracking, codes, chips, tricks |
|
||||||
|
|
||||||
The cat is drawn in a fixed 100 x 90 design space and scaled by `width / 100`,
|
The cat is drawn in a fixed 100 x 90 design space and scaled by `width / 100`,
|
||||||
so every size setting stays crisp. There are no image assets.
|
so every size setting stays crisp. There are no image assets, which keeps the
|
||||||
|
whole thing to one binary plus Qt.
|
||||||
|
|
||||||
## Build
|
## Build
|
||||||
|
|
||||||
@@ -43,34 +55,70 @@ cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release
|
|||||||
cmake --build build
|
cmake --build build
|
||||||
```
|
```
|
||||||
|
|
||||||
Needs Qt 6.8 or newer for `Shape.CurveRenderer`. Required modules: Core, Gui,
|
Needs Qt 6.5 or newer. Required modules: Core, Gui, Quick, QuickControls2,
|
||||||
Quick, QuickControls2, Widgets. Widgets is only there because `QSystemTrayIcon`
|
Widgets. Widgets is only there because `QSystemTrayIcon` needs it for its
|
||||||
needs it for its context menu.
|
context menu.
|
||||||
|
|
||||||
## Running on X11
|
## Environment switches
|
||||||
|
|
||||||
The cat is a frameless, transparent, always-on-top `Qt::Tool` window that places
|
|
||||||
itself. Two environment variables cover the cases where a window manager
|
|
||||||
disagrees:
|
|
||||||
|
|
||||||
| Variable | Effect |
|
| Variable | Effect |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `WORKCAT_X11_BYPASS=1` | Adds `Qt::X11BypassWindowManagerHint`, making an override-redirect window. Use when the WM refuses to keep the cat on top or shows it in the taskbar. |
|
| `TNCAT_X11_BYPASS=1` | Adds `Qt::X11BypassWindowManagerHint`, making an override-redirect window. Use when the window manager refuses to keep the cat on top or shows it in the taskbar. |
|
||||||
| `WORKCAT_OPAQUE=1` | Paints a solid background instead of relying on transparency. Use when no compositor runs and the cat sits in a black box. |
|
| `TNCAT_OVERLAY=1` | Forces the screen-filling overlay layout that Wayland needs, for testing it on X11. |
|
||||||
| `WORKCAT_POSE=Sitting` | Freezes the cat in one mood. Handy for screenshots. Accepts any `CatController::Mood` name. |
|
| `TNCAT_OPAQUE=1` | Paints a solid background instead of relying on transparency. Use when no compositor runs and the cat sits in a black box. |
|
||||||
|
| `TNCAT_POSE=Sitting` | Freezes the cat in one mood. Handy for screenshots. Accepts any `CatController::Mood` name. |
|
||||||
|
| `QT_QUICK_BACKEND=software` | Qt Quick's software rasteriser, for machines without a usable OpenGL driver. `Shape` falls back to its geometry renderer on its own. |
|
||||||
|
|
||||||
Wayland cannot let a client position its own window, so the cat would stay
|
## Two window layouts
|
||||||
wherever the compositor puts it. The app prints a warning and suggests
|
|
||||||
`QT_QPA_PLATFORM=xcb`.
|
|
||||||
|
|
||||||
Desktops without a status-notifier host get no tray icon. The cat itself stays
|
On X11, macOS and Windows the cat and the chip each get their own small,
|
||||||
the entry point: right-click or double-click opens the settings window.
|
frameless, always-on-top window, and the app moves those windows around. Nothing
|
||||||
|
else on screen is covered and no clicks are intercepted.
|
||||||
|
|
||||||
|
Wayland does not let a client place its own window, so that layout cannot work
|
||||||
|
there. In overlay mode the app instead opens one screen-filling transparent
|
||||||
|
window, moves the cat inside it, and restricts the window's input region to the
|
||||||
|
cat's rectangle with `QWindow::setMask`, so every click outside the cat lands on
|
||||||
|
whatever is behind. The mask is refreshed about 30 times a second while the cat
|
||||||
|
walks.
|
||||||
|
|
||||||
|
Overlay mode switches on automatically on Wayland. `TNCAT_OVERLAY=1` turns it on
|
||||||
|
anywhere for testing. It is not the default elsewhere because macOS ignores the
|
||||||
|
input mask, which would make the overlay swallow every click on the desktop.
|
||||||
|
|
||||||
|
## Running on a HEIDENHAIN control
|
||||||
|
|
||||||
|
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.
|
||||||
|
- **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
|
||||||
|
screen area. On a control whose lower edge carries softkeys, raise the ground
|
||||||
|
line in the settings so the cat walks above them.
|
||||||
|
|
||||||
|
What still needs checking on the actual machine:
|
||||||
|
|
||||||
|
- **Qt version and libraries on HEROS.** Build against what the control has, or
|
||||||
|
ship a self-contained bundle with the Qt libraries next to the binary. Building
|
||||||
|
on a newer distribution than the control runs will fail at load time on glibc.
|
||||||
|
- **Graphics stack.** If Qt Quick cannot get an OpenGL context, start with
|
||||||
|
`QT_QUICK_BACKEND=software`. The drawing is simple enough for that path.
|
||||||
|
- **Window management.** The NC user interface runs fullscreen. Try the default
|
||||||
|
flags first, then `TNCAT_X11_BYPASS=1` if the cat disappears behind it.
|
||||||
|
- **Installing anything on a control is subject to your site's rules.** Keep the
|
||||||
|
cat away from anything the operator needs to read, and clear it with whoever
|
||||||
|
administers the machine before it runs on production hardware.
|
||||||
|
|
||||||
## Interaction
|
## Interaction
|
||||||
|
|
||||||
| Input | Result |
|
| Input | Result |
|
||||||
|---|---|
|
|---|---|
|
||||||
| Click | 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 or right-click | Settings window |
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
[Desktop Entry]
|
[Desktop Entry]
|
||||||
Type=Application
|
Type=Application
|
||||||
Name=Workcat
|
Name=TNCat
|
||||||
Comment=A cute desktop cat
|
Comment=A cute desktop cat
|
||||||
Exec=workcat
|
Exec=tncat
|
||||||
Icon=workcat
|
Icon=tncat
|
||||||
Categories=Utility;Amusement;
|
Categories=Utility;Amusement;
|
||||||
StartupNotify=false
|
StartupNotify=false
|
||||||
+69
-6
@@ -13,6 +13,16 @@ Item {
|
|||||||
property bool lifted: false
|
property bool lifted: false
|
||||||
property bool petted: false
|
property bool petted: false
|
||||||
property point look: Qt.point(0, 0)
|
property point look: Qt.point(0, 0)
|
||||||
|
property bool showCodes: true
|
||||||
|
|
||||||
|
// The cat announces what it is doing in the machine's own language.
|
||||||
|
readonly property string moodCode: mood === "running" || mood === "chasing" ? "G00"
|
||||||
|
: mood === "walking" ? "G01"
|
||||||
|
: mood === "sitting" ? "M00"
|
||||||
|
: mood === "grooming" ? "M08"
|
||||||
|
: mood === "stretching" ? "G28"
|
||||||
|
: mood === "sleeping" ? "M30"
|
||||||
|
: ""
|
||||||
|
|
||||||
readonly property bool dark: coat.hslLightness < 0.45
|
readonly property bool dark: coat.hslLightness < 0.45
|
||||||
readonly property color coatShade: Qt.darker(coat, 1.12)
|
readonly property color coatShade: Qt.darker(coat, 1.12)
|
||||||
@@ -23,7 +33,7 @@ Item {
|
|||||||
readonly property color blush: "#f77d95"
|
readonly property color blush: "#f77d95"
|
||||||
|
|
||||||
readonly property bool walking: mood === "walking"
|
readonly property bool walking: mood === "walking"
|
||||||
readonly property bool running: mood === "running"
|
readonly property bool running: mood === "running" || mood === "chasing"
|
||||||
readonly property bool moving: (walking || running) && !lifted
|
readonly property bool moving: (walking || running) && !lifted
|
||||||
readonly property bool sleeping: mood === "sleeping"
|
readonly property bool sleeping: mood === "sleeping"
|
||||||
readonly property bool happy: mood === "loving" || mood === "grooming" || mood === "stretching" || petted
|
readonly property bool happy: mood === "loving" || mood === "grooming" || mood === "stretching" || petted
|
||||||
@@ -299,8 +309,8 @@ Item {
|
|||||||
id: eyeBall
|
id: eyeBall
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
width: eye.width * (cat.mood === "alert" ? 1.15 : 1)
|
width: eye.width * (cat.mood === "alert" || cat.mood === "batting" ? 1.15 : 1)
|
||||||
height: eye.height * eye.open * (cat.mood === "alert" ? 1.15 : 1)
|
height: eye.height * eye.open * (cat.mood === "alert" || cat.mood === "batting" ? 1.15 : 1)
|
||||||
radius: width / 2
|
radius: width / 2
|
||||||
color: cat.eyeColor
|
color: cat.eyeColor
|
||||||
visible: eye.open > 0.08
|
visible: eye.open > 0.08
|
||||||
@@ -449,14 +459,17 @@ Item {
|
|||||||
// speech bubble: "!" / "z" / "♥"
|
// speech bubble: "!" / "z" / "♥"
|
||||||
Text {
|
Text {
|
||||||
id: bubble
|
id: bubble
|
||||||
readonly property string glyph: cat.mood === "alert" ? "!" : cat.sleeping ? "z" : (cat.mood === "loving" || cat.petted) ? "♥" : ""
|
readonly property string glyph: cat.mood === "alert" || cat.mood === "batting" ? "!"
|
||||||
|
: cat.sleeping ? "z"
|
||||||
|
: (cat.mood === "loving" || cat.petted) ? "♥" : ""
|
||||||
x: 82; y: 4
|
x: 82; y: 4
|
||||||
text: glyph
|
text: glyph
|
||||||
visible: glyph.length > 0
|
visible: glyph.length > 0
|
||||||
font.pixelSize: cat.mood === "alert" ? 22 : 16
|
font.pixelSize: cat.mood === "alert" || cat.mood === "batting" ? 22 : 16
|
||||||
font.bold: true
|
font.bold: true
|
||||||
font.family: "Helvetica Neue"
|
font.family: "Helvetica Neue"
|
||||||
color: cat.mood === "alert" ? "#f2a33c" : (glyph === "♥" ? "#f25f7a" : cat.ink)
|
color: cat.mood === "alert" || cat.mood === "batting" ? "#f2a33c"
|
||||||
|
: (glyph === "♥" ? "#ff5b7f" : cat.ink)
|
||||||
transform: Scale { xScale: cat.facingRight ? 1 : -1; origin.x: bubble.width / 2 }
|
transform: Scale { xScale: cat.facingRight ? 1 : -1; origin.x: bubble.width / 2 }
|
||||||
Accessible.ignored: true
|
Accessible.ignored: true
|
||||||
|
|
||||||
@@ -474,6 +487,50 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Machine code tag above the head, flashed whenever the mood changes
|
||||||
|
Item {
|
||||||
|
id: codeTag
|
||||||
|
x: 30; y: 2
|
||||||
|
width: codeBg.width
|
||||||
|
height: codeBg.height
|
||||||
|
opacity: 0
|
||||||
|
visible: cat.showCodes && opacity > 0.01
|
||||||
|
transformOrigin: Item.Center
|
||||||
|
transform: Scale { xScale: cat.facingRight ? 1 : -1; origin.x: codeTag.width / 2 }
|
||||||
|
z: 10
|
||||||
|
scale: cat.unit
|
||||||
|
Accessible.ignored: true
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: codeBg
|
||||||
|
width: codeText.implicitWidth + 10
|
||||||
|
height: codeText.implicitHeight + 6
|
||||||
|
radius: 4
|
||||||
|
color: "#242833"
|
||||||
|
opacity: 0.92
|
||||||
|
antialiasing: true
|
||||||
|
|
||||||
|
Text {
|
||||||
|
id: codeText
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: cat.moodCode
|
||||||
|
color: "#8fd6ff"
|
||||||
|
font.pixelSize: 11
|
||||||
|
font.bold: true
|
||||||
|
font.family: "Menlo"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SequentialAnimation {
|
||||||
|
id: codeFlash
|
||||||
|
NumberAnimation { target: codeTag; property: "opacity"; to: 1; duration: 160 }
|
||||||
|
PauseAnimation { duration: 1500 }
|
||||||
|
NumberAnimation { target: codeTag; property: "opacity"; to: 0; duration: 500 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMoodCodeChanged: if (showCodes && moodCode.length > 0) codeFlash.restart()
|
||||||
|
|
||||||
states: [
|
states: [
|
||||||
State {
|
State {
|
||||||
name: "running"
|
name: "running"
|
||||||
@@ -509,6 +566,12 @@ Item {
|
|||||||
PropertyChanges { head.y: 24; head.x: 44; head.rotation: 4 }
|
PropertyChanges { head.y: 24; head.x: 44; head.rotation: 4 }
|
||||||
PropertyChanges { cat.tailC1X: 24; cat.tailC1Y: 86; cat.tailC2X: 60; cat.tailC2Y: 90; cat.tailX: 76; cat.tailY: 78 }
|
PropertyChanges { cat.tailC1X: 24; cat.tailC1Y: 86; cat.tailC2X: 60; cat.tailC2Y: 90; cat.tailX: 76; cat.tailY: 78 }
|
||||||
},
|
},
|
||||||
|
State {
|
||||||
|
name: "batting"
|
||||||
|
when: cat.mood === "batting" && !cat.lifted
|
||||||
|
PropertyChanges { body.rotation: -10; body.x: 26 }
|
||||||
|
PropertyChanges { head.y: 10; head.x: 50 }
|
||||||
|
},
|
||||||
State {
|
State {
|
||||||
name: "lifted"
|
name: "lifted"
|
||||||
when: cat.lifted
|
when: cat.lifted
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
pragma Singleton
|
||||||
|
import QtQuick
|
||||||
|
import TNCat
|
||||||
|
|
||||||
|
// Maps CatController::Mood onto the strings Cat.qml understands.
|
||||||
|
QtObject {
|
||||||
|
readonly property var names: ["walking", "running", "sitting", "grooming", "stretching",
|
||||||
|
"sleeping", "loving", "alert", "chasing", "batting"]
|
||||||
|
|
||||||
|
function name(mood) {
|
||||||
|
return names[mood] !== undefined ? names[mood] : "sitting"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
pragma ComponentBehavior: Bound
|
||||||
|
import QtQuick
|
||||||
|
import TNCat
|
||||||
|
|
||||||
|
// One small window that is exactly the cat. Used wherever a client may place
|
||||||
|
// its own window, which is everything except Wayland.
|
||||||
|
Window {
|
||||||
|
id: win
|
||||||
|
|
||||||
|
required property CatController cat
|
||||||
|
required property AppSettings settings
|
||||||
|
|
||||||
|
signal settingsRequested()
|
||||||
|
|
||||||
|
width: settings.catSize
|
||||||
|
height: Math.round(settings.catSize * 0.9)
|
||||||
|
x: cat.x
|
||||||
|
y: Screen.desktopAvailableHeight - height - settings.bottomMargin
|
||||||
|
color: Platform.opaqueBackground ? settings.coat : "transparent"
|
||||||
|
flags: Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint | Qt.Tool
|
||||||
|
| Qt.NoDropShadowWindowHint | Qt.WindowDoesNotAcceptFocus
|
||||||
|
| (Platform.bypassWindowManager ? Qt.X11BypassWindowManagerHint : 0)
|
||||||
|
visible: true
|
||||||
|
title: qsTr("TNCat")
|
||||||
|
|
||||||
|
Component.onCompleted: {
|
||||||
|
cat.stageWidth = Screen.desktopAvailableWidth
|
||||||
|
cat.catWidth = width
|
||||||
|
}
|
||||||
|
onWidthChanged: cat.catWidth = width
|
||||||
|
Screen.onDesktopAvailableWidthChanged: cat.stageWidth = Screen.desktopAvailableWidth
|
||||||
|
|
||||||
|
// The eyes need to know where the face sits on the desktop.
|
||||||
|
Binding {
|
||||||
|
target: win.cat
|
||||||
|
property: "catCenter"
|
||||||
|
value: Qt.point(win.x + win.width / 2, win.y + win.height / 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
Cat {
|
||||||
|
anchors.fill: parent
|
||||||
|
coat: win.settings.coat
|
||||||
|
facingRight: win.cat.facingRight
|
||||||
|
mood: CatMoods.name(win.cat.mood)
|
||||||
|
lifted: win.cat.dragging
|
||||||
|
petted: win.cat.hovered && !win.cat.dragging
|
||||||
|
look: win.cat.look
|
||||||
|
showCodes: win.settings.showCodes
|
||||||
|
|
||||||
|
Accessible.role: Accessible.Button
|
||||||
|
Accessible.name: qsTr("TNCat")
|
||||||
|
}
|
||||||
|
|
||||||
|
HoverHandler {
|
||||||
|
onHoveredChanged: win.cat.hovered = hovered
|
||||||
|
}
|
||||||
|
|
||||||
|
DragHandler {
|
||||||
|
target: null
|
||||||
|
onActiveChanged: win.cat.dragging = active
|
||||||
|
onCentroidChanged: {
|
||||||
|
if (active)
|
||||||
|
win.cat.x = Math.max(0, Math.min(win.cat.stageWidth - win.width,
|
||||||
|
win.x + centroid.position.x - centroid.pressPosition.x))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TapHandler {
|
||||||
|
acceptedButtons: Qt.LeftButton
|
||||||
|
onTapped: win.cat.pet()
|
||||||
|
onDoubleTapped: win.settingsRequested()
|
||||||
|
onLongPressed: win.settingsRequested()
|
||||||
|
}
|
||||||
|
|
||||||
|
TapHandler {
|
||||||
|
acceptedButtons: Qt.RightButton
|
||||||
|
onTapped: win.settingsRequested()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
pragma ComponentBehavior: Bound
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Shapes
|
||||||
|
|
||||||
|
// A curled steel chip, the kind that comes off a milling cutter.
|
||||||
|
// Design space is 60 x 44 units.
|
||||||
|
Item {
|
||||||
|
id: chip
|
||||||
|
|
||||||
|
property int kick: 0
|
||||||
|
|
||||||
|
readonly property real unit: width / 60
|
||||||
|
|
||||||
|
implicitWidth: 60
|
||||||
|
implicitHeight: 44
|
||||||
|
|
||||||
|
Item {
|
||||||
|
width: 60
|
||||||
|
height: 44
|
||||||
|
transformOrigin: Item.TopLeft
|
||||||
|
scale: chip.unit
|
||||||
|
|
||||||
|
Rectangle { // ground shadow
|
||||||
|
x: 14; y: 36
|
||||||
|
width: 32; height: 6
|
||||||
|
radius: 3
|
||||||
|
color: "#000000"
|
||||||
|
opacity: 0.18
|
||||||
|
antialiasing: true
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: curl
|
||||||
|
x: 30; y: 22
|
||||||
|
width: 0; height: 0
|
||||||
|
|
||||||
|
Shape {
|
||||||
|
x: -30; y: -22
|
||||||
|
width: 60; height: 44
|
||||||
|
preferredRendererType: Shape.CurveRenderer
|
||||||
|
|
||||||
|
ShapePath { // the curl
|
||||||
|
strokeColor: "#8d99ae"
|
||||||
|
strokeWidth: 5
|
||||||
|
capStyle: ShapePath.RoundCap
|
||||||
|
fillColor: "transparent"
|
||||||
|
startX: 12; startY: 32
|
||||||
|
PathCubic { control1X: 2; control1Y: 20; control2X: 14; control2Y: 6; x: 30; y: 10 }
|
||||||
|
PathCubic { control1X: 44; control1Y: 14; control2X: 48; control2Y: 30; x: 38; y: 34 }
|
||||||
|
PathCubic { control1X: 30; control1Y: 37; control2X: 26; control2Y: 28; x: 32; y: 24 }
|
||||||
|
}
|
||||||
|
ShapePath { // highlight along the outer edge
|
||||||
|
strokeColor: "#dfe6ee"
|
||||||
|
strokeWidth: 1.6
|
||||||
|
capStyle: ShapePath.RoundCap
|
||||||
|
fillColor: "transparent"
|
||||||
|
startX: 13; startY: 29
|
||||||
|
PathCubic { control1X: 5; control1Y: 19; control2X: 16; control2Y: 8; x: 30; y: 12 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RotationAnimation on rotation {
|
||||||
|
running: true
|
||||||
|
loops: Animation.Infinite
|
||||||
|
from: 0; to: 360
|
||||||
|
duration: 2600
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
pragma ComponentBehavior: Bound
|
||||||
|
import QtQuick
|
||||||
|
import TNCat
|
||||||
|
|
||||||
|
// The chip gets its own little click-through window so it can skitter away
|
||||||
|
// from the cat across the whole screen.
|
||||||
|
Window {
|
||||||
|
id: win
|
||||||
|
|
||||||
|
required property CatController cat
|
||||||
|
required property AppSettings settings
|
||||||
|
|
||||||
|
readonly property int chipSize: Math.round(settings.catSize * 0.45)
|
||||||
|
|
||||||
|
width: chipSize
|
||||||
|
height: Math.round(chipSize * 0.73)
|
||||||
|
x: cat.chipX - width / 2
|
||||||
|
y: Screen.desktopAvailableHeight - height - settings.bottomMargin
|
||||||
|
color: "transparent"
|
||||||
|
flags: Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint | Qt.Tool
|
||||||
|
| Qt.NoDropShadowWindowHint | Qt.WindowDoesNotAcceptFocus
|
||||||
|
| Qt.WindowTransparentForInput
|
||||||
|
| (Platform.bypassWindowManager ? Qt.X11BypassWindowManagerHint : 0)
|
||||||
|
visible: cat.chipVisible && !Platform.opaqueBackground
|
||||||
|
|
||||||
|
Behavior on x {
|
||||||
|
NumberAnimation { duration: 320; easing.type: Easing.OutQuad }
|
||||||
|
}
|
||||||
|
|
||||||
|
Chip {
|
||||||
|
anchors.fill: parent
|
||||||
|
kick: win.cat.chipKick
|
||||||
|
}
|
||||||
|
}
|
||||||
+24
-69
@@ -1,87 +1,42 @@
|
|||||||
pragma ComponentBehavior: Bound
|
pragma ComponentBehavior: Bound
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import Workcat
|
import TNCat
|
||||||
|
|
||||||
Window {
|
// Root is not a window: which windows exist depends on the platform.
|
||||||
id: root
|
QtObject {
|
||||||
|
id: app
|
||||||
|
|
||||||
required property CatController cat
|
required property CatController cat
|
||||||
required property AppSettings settings
|
required property AppSettings settings
|
||||||
|
|
||||||
readonly property var moodNames: ["walking", "running", "sitting", "grooming", "stretching", "sleeping", "loving", "alert"]
|
|
||||||
readonly property real floorY: Screen.desktopAvailableHeight - height
|
|
||||||
|
|
||||||
function openSettings() {
|
function openSettings() {
|
||||||
settingsLoader.active = true
|
settingsWindow.show()
|
||||||
if (settingsLoader.status === Loader.Ready)
|
settingsWindow.raise()
|
||||||
(settingsLoader.item as SettingsWindow).show()
|
settingsWindow.requestActivate()
|
||||||
}
|
}
|
||||||
|
|
||||||
width: settings.catSize
|
readonly property CatWindow catWindow: CatWindow {
|
||||||
height: Math.round(settings.catSize * 0.9)
|
cat: app.cat
|
||||||
x: cat.x
|
settings: app.settings
|
||||||
y: floorY
|
visible: !Platform.overlayMode
|
||||||
color: Platform.opaqueBackground ? settings.coat : "transparent"
|
onSettingsRequested: app.openSettings()
|
||||||
flags: Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint | Qt.Tool
|
|
||||||
| Qt.NoDropShadowWindowHint | Qt.WindowDoesNotAcceptFocus
|
|
||||||
| (Platform.bypassWindowManager ? Qt.X11BypassWindowManagerHint : 0)
|
|
||||||
visible: true
|
|
||||||
title: qsTr("Workcat")
|
|
||||||
|
|
||||||
Component.onCompleted: {
|
|
||||||
cat.screenWidth = Screen.desktopAvailableWidth
|
|
||||||
cat.catWidth = width
|
|
||||||
}
|
|
||||||
onWidthChanged: cat.catWidth = width
|
|
||||||
Screen.onDesktopAvailableWidthChanged: cat.screenWidth = Screen.desktopAvailableWidth
|
|
||||||
|
|
||||||
Cat {
|
|
||||||
id: sprite
|
|
||||||
anchors.fill: parent
|
|
||||||
coat: root.settings.coat
|
|
||||||
facingRight: root.cat.facingRight
|
|
||||||
mood: root.moodNames[root.cat.mood]
|
|
||||||
lifted: root.cat.dragging
|
|
||||||
petted: root.cat.hovered && !root.cat.dragging
|
|
||||||
look: root.cat.look
|
|
||||||
|
|
||||||
Accessible.role: Accessible.Button
|
|
||||||
Accessible.name: qsTr("Workcat")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
HoverHandler {
|
readonly property ChipWindow chipWindow: ChipWindow {
|
||||||
onHoveredChanged: root.cat.hovered = hovered
|
cat: app.cat
|
||||||
|
settings: app.settings
|
||||||
|
visible: !Platform.overlayMode && app.cat.chipVisible
|
||||||
}
|
}
|
||||||
|
|
||||||
DragHandler {
|
readonly property OverlayWindow overlayWindow: OverlayWindow {
|
||||||
target: null
|
cat: app.cat
|
||||||
onActiveChanged: root.cat.dragging = active
|
settings: app.settings
|
||||||
onCentroidChanged: {
|
active: Platform.overlayMode
|
||||||
if (active)
|
onSettingsRequested: app.openSettings()
|
||||||
root.cat.x = Math.max(0, Math.min(root.cat.screenWidth - root.width,
|
|
||||||
root.x + centroid.position.x - centroid.pressPosition.x))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
TapHandler {
|
readonly property SettingsWindow settingsWindow: SettingsWindow {
|
||||||
acceptedButtons: Qt.LeftButton
|
cat: app.cat
|
||||||
onTapped: root.cat.pet()
|
settings: app.settings
|
||||||
onDoubleTapped: root.openSettings()
|
|
||||||
}
|
|
||||||
|
|
||||||
TapHandler {
|
|
||||||
acceptedButtons: Qt.RightButton
|
|
||||||
onTapped: root.openSettings()
|
|
||||||
}
|
|
||||||
|
|
||||||
Loader {
|
|
||||||
id: settingsLoader
|
|
||||||
active: false
|
|
||||||
sourceComponent: SettingsWindow {
|
|
||||||
cat: root.cat
|
|
||||||
settings: root.settings
|
|
||||||
onClosing: settingsLoader.active = false
|
|
||||||
}
|
|
||||||
onLoaded: (item as SettingsWindow).show()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
pragma ComponentBehavior: Bound
|
||||||
|
import QtQuick
|
||||||
|
import TNCat
|
||||||
|
|
||||||
|
// One screen-filling, click-through window that holds the cat and the chip.
|
||||||
|
// Wayland does not let a client place its own window, so there the cat moves
|
||||||
|
// inside this overlay instead of the window moving across the desktop.
|
||||||
|
Window {
|
||||||
|
id: win
|
||||||
|
|
||||||
|
required property CatController cat
|
||||||
|
required property AppSettings settings
|
||||||
|
|
||||||
|
signal settingsRequested()
|
||||||
|
|
||||||
|
// Fullscreen is a visibility, not a visible flag, so the mode switch has to
|
||||||
|
// go through visibility or the two fight each other.
|
||||||
|
property bool active: false
|
||||||
|
|
||||||
|
readonly property int catW: settings.catSize
|
||||||
|
readonly property int catH: Math.round(settings.catSize * 0.9)
|
||||||
|
readonly property real groundY: height - catH - settings.bottomMargin
|
||||||
|
|
||||||
|
// Only the cat itself should swallow clicks; everything else falls through.
|
||||||
|
function refreshInputRegion() {
|
||||||
|
Platform.setInputRegion(win, Qt.rect(Math.round(cat.x), Math.round(groundY), catW, catH))
|
||||||
|
}
|
||||||
|
|
||||||
|
visibility: active ? Window.FullScreen : Window.Hidden
|
||||||
|
color: "transparent"
|
||||||
|
flags: Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint | Qt.Tool
|
||||||
|
| Qt.NoDropShadowWindowHint | Qt.WindowDoesNotAcceptFocus
|
||||||
|
title: qsTr("TNCat")
|
||||||
|
|
||||||
|
onActiveChanged: {
|
||||||
|
if (!active)
|
||||||
|
return
|
||||||
|
cat.stageWidth = width
|
||||||
|
cat.catWidth = catW
|
||||||
|
refreshInputRegion()
|
||||||
|
}
|
||||||
|
onWidthChanged: cat.stageWidth = width
|
||||||
|
onCatWChanged: cat.catWidth = catW
|
||||||
|
|
||||||
|
Binding {
|
||||||
|
target: win.cat
|
||||||
|
property: "catCenter"
|
||||||
|
value: Qt.point(win.x + win.cat.x + win.catW / 2, win.y + win.groundY + win.catH / 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Coalesce mask updates: one X11 round trip per frame of walking is plenty.
|
||||||
|
Timer {
|
||||||
|
interval: 33
|
||||||
|
running: win.active
|
||||||
|
repeat: true
|
||||||
|
onTriggered: win.refreshInputRegion()
|
||||||
|
}
|
||||||
|
|
||||||
|
Chip {
|
||||||
|
x: win.cat.chipX - width / 2
|
||||||
|
y: win.height - height - win.settings.bottomMargin
|
||||||
|
width: Math.round(win.settings.catSize * 0.45)
|
||||||
|
height: Math.round(width * 0.73)
|
||||||
|
visible: win.cat.chipVisible
|
||||||
|
kick: win.cat.chipKick
|
||||||
|
|
||||||
|
Behavior on x {
|
||||||
|
NumberAnimation { duration: 320; easing.type: Easing.OutQuad }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: catItem
|
||||||
|
x: win.cat.x
|
||||||
|
y: win.groundY
|
||||||
|
width: win.catW
|
||||||
|
height: win.catH
|
||||||
|
|
||||||
|
Cat {
|
||||||
|
anchors.fill: parent
|
||||||
|
coat: win.settings.coat
|
||||||
|
facingRight: win.cat.facingRight
|
||||||
|
mood: CatMoods.name(win.cat.mood)
|
||||||
|
lifted: win.cat.dragging
|
||||||
|
petted: win.cat.hovered && !win.cat.dragging
|
||||||
|
look: win.cat.look
|
||||||
|
showCodes: win.settings.showCodes
|
||||||
|
|
||||||
|
Accessible.role: Accessible.Button
|
||||||
|
Accessible.name: qsTr("TNCat")
|
||||||
|
}
|
||||||
|
|
||||||
|
HoverHandler {
|
||||||
|
onHoveredChanged: win.cat.hovered = hovered
|
||||||
|
}
|
||||||
|
|
||||||
|
DragHandler {
|
||||||
|
target: null
|
||||||
|
onActiveChanged: win.cat.dragging = active
|
||||||
|
onCentroidChanged: {
|
||||||
|
if (active)
|
||||||
|
win.cat.x = Math.max(0, Math.min(win.cat.stageWidth - win.catW,
|
||||||
|
catItem.x + centroid.position.x - centroid.pressPosition.x))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TapHandler {
|
||||||
|
acceptedButtons: Qt.LeftButton
|
||||||
|
onTapped: win.cat.pet()
|
||||||
|
onDoubleTapped: win.settingsRequested()
|
||||||
|
onLongPressed: win.settingsRequested()
|
||||||
|
}
|
||||||
|
|
||||||
|
TapHandler {
|
||||||
|
acceptedButtons: Qt.RightButton
|
||||||
|
onTapped: win.settingsRequested()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+45
-10
@@ -2,7 +2,7 @@ pragma ComponentBehavior: Bound
|
|||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Controls.Basic
|
import QtQuick.Controls.Basic
|
||||||
import QtQuick.Layouts
|
import QtQuick.Layouts
|
||||||
import Workcat
|
import TNCat
|
||||||
|
|
||||||
Window {
|
Window {
|
||||||
id: win
|
id: win
|
||||||
@@ -20,10 +20,10 @@ Window {
|
|||||||
]
|
]
|
||||||
|
|
||||||
width: 440
|
width: 440
|
||||||
height: 600
|
height: 700
|
||||||
minimumWidth: 380
|
minimumWidth: 380
|
||||||
minimumHeight: 520
|
minimumHeight: 600
|
||||||
title: qsTr("Workcat")
|
title: qsTr("TNCat")
|
||||||
color: "#faf7f2"
|
color: "#faf7f2"
|
||||||
flags: Qt.Dialog | Qt.WindowStaysOnTopHint
|
flags: Qt.Dialog | Qt.WindowStaysOnTopHint
|
||||||
|
|
||||||
@@ -35,20 +35,24 @@ Window {
|
|||||||
RowLayout {
|
RowLayout {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
spacing: 16
|
spacing: 16
|
||||||
Cat {
|
Loader {
|
||||||
Layout.preferredWidth: 110
|
Layout.preferredWidth: 110
|
||||||
Layout.preferredHeight: 99
|
Layout.preferredHeight: 99
|
||||||
coat: win.settings.coat
|
active: win.visible
|
||||||
mood: "sitting"
|
sourceComponent: Cat {
|
||||||
petted: previewHover.hovered
|
coat: win.settings.coat
|
||||||
HoverHandler { id: previewHover }
|
mood: "sitting"
|
||||||
|
showCodes: false
|
||||||
|
petted: previewHover.hovered
|
||||||
|
HoverHandler { id: previewHover }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
spacing: 4
|
spacing: 4
|
||||||
Label {
|
Label {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
text: qsTr("Workcat")
|
text: qsTr("TNCat")
|
||||||
font.pixelSize: 26
|
font.pixelSize: 26
|
||||||
font.bold: true
|
font.bold: true
|
||||||
color: "#2f2a36"
|
color: "#2f2a36"
|
||||||
@@ -122,6 +126,22 @@ Window {
|
|||||||
onMoved: win.settings.walkSpeed = value
|
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
|
||||||
|
}
|
||||||
|
Label {
|
||||||
|
Layout.preferredWidth: 54
|
||||||
|
text: qsTr("%1 px").arg(Math.round(marginSlider.value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Label { text: qsTr("Eyes") }
|
Label { text: qsTr("Eyes") }
|
||||||
Switch {
|
Switch {
|
||||||
text: qsTr("Follow the mouse")
|
text: qsTr("Follow the mouse")
|
||||||
@@ -129,6 +149,20 @@ Window {
|
|||||||
onToggled: win.settings.followCursor = checked
|
onToggled: win.settings.followCursor = checked
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Label { text: qsTr("Codes") }
|
||||||
|
Switch {
|
||||||
|
text: qsTr("Show G-code above the cat")
|
||||||
|
checked: win.settings.showCodes
|
||||||
|
onToggled: win.settings.showCodes = checked
|
||||||
|
}
|
||||||
|
|
||||||
|
Label { text: qsTr("Chips") }
|
||||||
|
Switch {
|
||||||
|
text: qsTr("Let a chip skitter past now and then")
|
||||||
|
checked: win.settings.chipsEnabled
|
||||||
|
onToggled: win.settings.chipsEnabled = checked
|
||||||
|
}
|
||||||
|
|
||||||
Label { text: qsTr("Cat") }
|
Label { text: qsTr("Cat") }
|
||||||
Switch {
|
Switch {
|
||||||
text: checked ? qsTr("Napping") : qsTr("Awake")
|
text: checked ? qsTr("Napping") : qsTr("Awake")
|
||||||
@@ -148,6 +182,7 @@ Window {
|
|||||||
Button { text: qsTr("Stretch"); onClicked: win.cat.doTrick(CatController.Stretching) }
|
Button { text: qsTr("Stretch"); onClicked: win.cat.doTrick(CatController.Stretching) }
|
||||||
Button { text: qsTr("Dash"); onClicked: win.cat.doTrick(CatController.Running) }
|
Button { text: qsTr("Dash"); onClicked: win.cat.doTrick(CatController.Running) }
|
||||||
Button { text: qsTr("Nap"); onClicked: win.cat.doTrick(CatController.Sleeping) }
|
Button { text: qsTr("Nap"); onClicked: win.cat.doTrick(CatController.Sleeping) }
|
||||||
|
Button { text: qsTr("Toss a chip"); onClicked: win.cat.tossChip() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -59,6 +59,46 @@ void AppSettings::setFollowCursor(bool follow)
|
|||||||
emit followCursorChanged();
|
emit followCursorChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int AppSettings::bottomMargin() const
|
||||||
|
{
|
||||||
|
return m_settings.value("bottomMargin", 0).toInt();
|
||||||
|
}
|
||||||
|
|
||||||
|
void AppSettings::setBottomMargin(int pixels)
|
||||||
|
{
|
||||||
|
pixels = qBound(0, pixels, 600);
|
||||||
|
if (pixels == bottomMargin())
|
||||||
|
return;
|
||||||
|
m_settings.setValue("bottomMargin", pixels);
|
||||||
|
emit bottomMarginChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AppSettings::showCodes() const
|
||||||
|
{
|
||||||
|
return m_settings.value("showCodes", true).toBool();
|
||||||
|
}
|
||||||
|
|
||||||
|
void AppSettings::setShowCodes(bool show)
|
||||||
|
{
|
||||||
|
if (show == showCodes())
|
||||||
|
return;
|
||||||
|
m_settings.setValue("showCodes", show);
|
||||||
|
emit showCodesChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AppSettings::chipsEnabled() const
|
||||||
|
{
|
||||||
|
return m_settings.value("chipsEnabled", true).toBool();
|
||||||
|
}
|
||||||
|
|
||||||
|
void AppSettings::setChipsEnabled(bool enabled)
|
||||||
|
{
|
||||||
|
if (enabled == chipsEnabled())
|
||||||
|
return;
|
||||||
|
m_settings.setValue("chipsEnabled", enabled);
|
||||||
|
emit chipsEnabledChanged();
|
||||||
|
}
|
||||||
|
|
||||||
int AppSettings::petCount() const
|
int AppSettings::petCount() const
|
||||||
{
|
{
|
||||||
return m_settings.value("petCount", 0).toInt();
|
return m_settings.value("petCount", 0).toInt();
|
||||||
|
|||||||
@@ -15,6 +15,9 @@ class AppSettings : public QObject
|
|||||||
Q_PROPERTY(int catSize READ catSize WRITE setCatSize NOTIFY catSizeChanged)
|
Q_PROPERTY(int catSize READ catSize WRITE setCatSize NOTIFY catSizeChanged)
|
||||||
Q_PROPERTY(qreal walkSpeed READ walkSpeed WRITE setWalkSpeed NOTIFY walkSpeedChanged)
|
Q_PROPERTY(qreal walkSpeed READ walkSpeed WRITE setWalkSpeed NOTIFY walkSpeedChanged)
|
||||||
Q_PROPERTY(bool followCursor READ followCursor WRITE setFollowCursor NOTIFY followCursorChanged)
|
Q_PROPERTY(bool followCursor READ followCursor WRITE setFollowCursor NOTIFY followCursorChanged)
|
||||||
|
Q_PROPERTY(int bottomMargin READ bottomMargin WRITE setBottomMargin NOTIFY bottomMarginChanged)
|
||||||
|
Q_PROPERTY(bool showCodes READ showCodes WRITE setShowCodes NOTIFY showCodesChanged)
|
||||||
|
Q_PROPERTY(bool chipsEnabled READ chipsEnabled WRITE setChipsEnabled NOTIFY chipsEnabledChanged)
|
||||||
Q_PROPERTY(int petCount READ petCount NOTIFY petCountChanged)
|
Q_PROPERTY(int petCount READ petCount NOTIFY petCountChanged)
|
||||||
|
|
||||||
public:
|
public:
|
||||||
@@ -32,6 +35,15 @@ public:
|
|||||||
bool followCursor() const;
|
bool followCursor() const;
|
||||||
void setFollowCursor(bool follow);
|
void setFollowCursor(bool follow);
|
||||||
|
|
||||||
|
int bottomMargin() const;
|
||||||
|
void setBottomMargin(int pixels);
|
||||||
|
|
||||||
|
bool showCodes() const;
|
||||||
|
void setShowCodes(bool show);
|
||||||
|
|
||||||
|
bool chipsEnabled() const;
|
||||||
|
void setChipsEnabled(bool enabled);
|
||||||
|
|
||||||
int petCount() const;
|
int petCount() const;
|
||||||
void incrementPetCount();
|
void incrementPetCount();
|
||||||
|
|
||||||
@@ -40,6 +52,9 @@ signals:
|
|||||||
void catSizeChanged();
|
void catSizeChanged();
|
||||||
void walkSpeedChanged();
|
void walkSpeedChanged();
|
||||||
void followCursorChanged();
|
void followCursorChanged();
|
||||||
|
void bottomMarginChanged();
|
||||||
|
void showCodesChanged();
|
||||||
|
void chipsEnabledChanged();
|
||||||
void petCountChanged();
|
void petCountChanged();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
|||||||
+128
-18
@@ -4,7 +4,12 @@
|
|||||||
#include <QCursor>
|
#include <QCursor>
|
||||||
#include <QMetaEnum>
|
#include <QMetaEnum>
|
||||||
#include <QRandomGenerator>
|
#include <QRandomGenerator>
|
||||||
#include <QWindow>
|
|
||||||
|
namespace {
|
||||||
|
constexpr qreal kChipReach = 26.0; // how close the nose has to get
|
||||||
|
constexpr int kChipMinDelayMs = 45000;
|
||||||
|
constexpr int kChipMaxDelayMs = 150000;
|
||||||
|
}
|
||||||
|
|
||||||
CatController::CatController(AppSettings *settings, QObject *parent)
|
CatController::CatController(AppSettings *settings, QObject *parent)
|
||||||
: QObject(parent)
|
: QObject(parent)
|
||||||
@@ -23,8 +28,11 @@ CatController::CatController(AppSettings *settings, QObject *parent)
|
|||||||
connect(&m_cursorTimer, &QTimer::timeout, this, &CatController::trackCursor);
|
connect(&m_cursorTimer, &QTimer::timeout, this, &CatController::trackCursor);
|
||||||
m_cursorTimer.start();
|
m_cursorTimer.start();
|
||||||
|
|
||||||
// WORKCAT_POSE=Sleeping freezes the cat in one mood (handy for screenshots).
|
m_chipTimer.setSingleShot(true);
|
||||||
const QByteArray pose = qgetenv("WORKCAT_POSE");
|
connect(&m_chipTimer, &QTimer::timeout, this, &CatController::tossChip);
|
||||||
|
|
||||||
|
// TNCAT_POSE=Sleeping freezes the cat in one mood (handy for screenshots).
|
||||||
|
const QByteArray pose = qgetenv("TNCAT_POSE");
|
||||||
if (!pose.isEmpty()) {
|
if (!pose.isEmpty()) {
|
||||||
const QMetaEnum me = QMetaEnum::fromType<Mood>();
|
const QMetaEnum me = QMetaEnum::fromType<Mood>();
|
||||||
bool ok = false;
|
bool ok = false;
|
||||||
@@ -34,13 +42,15 @@ CatController::CatController(AppSettings *settings, QObject *parent)
|
|||||||
setMood(static_cast<Mood>(value));
|
setMood(static_cast<Mood>(value));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!m_frozen)
|
if (!m_frozen) {
|
||||||
pickNextMood();
|
pickNextMood();
|
||||||
|
scheduleChip();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void CatController::setWindow(QWindow *window)
|
qreal CatController::catNose() const
|
||||||
{
|
{
|
||||||
m_window = window;
|
return m_facingRight ? m_x + m_catWidth * 0.78 : m_x + m_catWidth * 0.22;
|
||||||
}
|
}
|
||||||
|
|
||||||
void CatController::setX(qreal x)
|
void CatController::setX(qreal x)
|
||||||
@@ -51,12 +61,12 @@ void CatController::setX(qreal x)
|
|||||||
emit xChanged();
|
emit xChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
void CatController::setScreenWidth(qreal width)
|
void CatController::setStageWidth(qreal width)
|
||||||
{
|
{
|
||||||
if (qFuzzyCompare(width, m_screenWidth))
|
if (qFuzzyCompare(width, m_stageWidth))
|
||||||
return;
|
return;
|
||||||
m_screenWidth = width;
|
m_stageWidth = width;
|
||||||
emit screenWidthChanged();
|
emit stageWidthChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
void CatController::setCatWidth(qreal width)
|
void CatController::setCatWidth(qreal width)
|
||||||
@@ -67,6 +77,14 @@ void CatController::setCatWidth(qreal width)
|
|||||||
emit catWidthChanged();
|
emit catWidthChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void CatController::setCatCenter(const QPointF ¢er)
|
||||||
|
{
|
||||||
|
if (center == m_catCenter)
|
||||||
|
return;
|
||||||
|
m_catCenter = center;
|
||||||
|
emit catCenterChanged();
|
||||||
|
}
|
||||||
|
|
||||||
void CatController::setPaused(bool paused)
|
void CatController::setPaused(bool paused)
|
||||||
{
|
{
|
||||||
if (paused == m_paused)
|
if (paused == m_paused)
|
||||||
@@ -75,9 +93,12 @@ void CatController::setPaused(bool paused)
|
|||||||
emit pausedChanged();
|
emit pausedChanged();
|
||||||
if (paused) {
|
if (paused) {
|
||||||
m_moodTimer.stop();
|
m_moodTimer.stop();
|
||||||
|
m_chipTimer.stop();
|
||||||
|
endChase();
|
||||||
setMood(Mood::Sleeping);
|
setMood(Mood::Sleeping);
|
||||||
} else {
|
} else {
|
||||||
pickNextMood();
|
pickNextMood();
|
||||||
|
scheduleChip();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -110,6 +131,7 @@ void CatController::pet()
|
|||||||
if (m_paused || m_frozen)
|
if (m_paused || m_frozen)
|
||||||
return;
|
return;
|
||||||
m_settings->incrementPetCount();
|
m_settings->incrementPetCount();
|
||||||
|
endChase();
|
||||||
holdMood(Mood::Loving, 1800);
|
holdMood(Mood::Loving, 1800);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,6 +140,7 @@ void CatController::doTrick(Mood mood)
|
|||||||
if (m_paused)
|
if (m_paused)
|
||||||
return;
|
return;
|
||||||
m_frozen = false;
|
m_frozen = false;
|
||||||
|
endChase();
|
||||||
holdMood(mood, mood == Mood::Sleeping ? 15000 : 3500);
|
holdMood(mood, mood == Mood::Sleeping ? 15000 : 3500);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,17 +150,85 @@ void CatController::holdMood(Mood mood, int ms)
|
|||||||
m_moodTimer.start(ms);
|
m_moodTimer.start(ms);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A curled metal chip skitters in from one side and the cat goes after it.
|
||||||
|
void CatController::tossChip()
|
||||||
|
{
|
||||||
|
if (m_paused || m_frozen || m_chipVisible)
|
||||||
|
return;
|
||||||
|
if (!m_settings->chipsEnabled()) {
|
||||||
|
scheduleChip();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto *rng = QRandomGenerator::global();
|
||||||
|
const bool fromLeft = rng->bounded(2) == 0;
|
||||||
|
const qreal margin = qMin(200.0, m_stageWidth * 0.2);
|
||||||
|
setChipX(fromLeft ? margin : m_stageWidth - margin);
|
||||||
|
|
||||||
|
m_batsLeft = rng->bounded(2, 5);
|
||||||
|
m_chipVisible = true;
|
||||||
|
emit chipVisibleChanged();
|
||||||
|
|
||||||
|
m_moodTimer.stop();
|
||||||
|
setMood(Mood::Chasing);
|
||||||
|
}
|
||||||
|
|
||||||
|
void CatController::batChip()
|
||||||
|
{
|
||||||
|
auto *rng = QRandomGenerator::global();
|
||||||
|
++m_chipKick;
|
||||||
|
emit chipKickChanged();
|
||||||
|
|
||||||
|
const qreal distance = rng->bounded(140, 380);
|
||||||
|
qreal next = m_chipX + (m_facingRight ? distance : -distance);
|
||||||
|
const qreal edge = 40.0;
|
||||||
|
if (next < edge || next > m_stageWidth - edge) {
|
||||||
|
next = qBound(edge, next, m_stageWidth - edge);
|
||||||
|
--m_batsLeft; // a chip against the wall is nearly done for
|
||||||
|
}
|
||||||
|
setChipX(next);
|
||||||
|
|
||||||
|
if (--m_batsLeft <= 0) {
|
||||||
|
endChase();
|
||||||
|
holdMood(Mood::Sitting, 2500); // pleased with itself
|
||||||
|
scheduleChip();
|
||||||
|
} else {
|
||||||
|
holdMood(Mood::Batting, 420);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void CatController::endChase()
|
||||||
|
{
|
||||||
|
if (!m_chipVisible)
|
||||||
|
return;
|
||||||
|
m_chipVisible = false;
|
||||||
|
m_batsLeft = 0;
|
||||||
|
emit chipVisibleChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
void CatController::scheduleChip()
|
||||||
|
{
|
||||||
|
m_chipTimer.start(QRandomGenerator::global()->bounded(kChipMinDelayMs, kChipMaxDelayMs));
|
||||||
|
}
|
||||||
|
|
||||||
|
void CatController::setChipX(qreal x)
|
||||||
|
{
|
||||||
|
if (qFuzzyCompare(x, m_chipX))
|
||||||
|
return;
|
||||||
|
m_chipX = x;
|
||||||
|
emit chipXChanged();
|
||||||
|
}
|
||||||
|
|
||||||
void CatController::trackCursor()
|
void CatController::trackCursor()
|
||||||
{
|
{
|
||||||
if (!m_window || !m_settings->followCursor()) {
|
if (m_catCenter.isNull() || !m_settings->followCursor()) {
|
||||||
if (!m_look.isNull()) {
|
if (!m_look.isNull()) {
|
||||||
m_look = {};
|
m_look = {};
|
||||||
emit lookChanged();
|
emit lookChanged();
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const QPointF center = m_window->geometry().center();
|
QPointF delta = QCursor::pos() - m_catCenter;
|
||||||
QPointF delta = QCursor::pos() - center;
|
|
||||||
if (!m_facingRight)
|
if (!m_facingRight)
|
||||||
delta.setX(-delta.x());
|
delta.setX(-delta.x());
|
||||||
const qreal range = 400.0;
|
const qreal range = 400.0;
|
||||||
@@ -154,14 +245,28 @@ void CatController::tick()
|
|||||||
const qreal dt = qMin(now - m_lastTickMs, qint64(100)) / 1000.0;
|
const qreal dt = qMin(now - m_lastTickMs, qint64(100)) / 1000.0;
|
||||||
m_lastTickMs = now;
|
m_lastTickMs = now;
|
||||||
|
|
||||||
const bool moving = m_mood == Mood::Walking || m_mood == Mood::Running;
|
if (m_dragging || m_paused || m_frozen)
|
||||||
if (!moving || m_dragging || m_paused || m_frozen)
|
return;
|
||||||
|
|
||||||
|
const qreal maxX = qMax(0.0, m_stageWidth - m_catWidth);
|
||||||
|
|
||||||
|
if (m_mood == Mood::Chasing) {
|
||||||
|
const qreal delta = m_chipX - catNose();
|
||||||
|
setFacingRight(delta >= 0);
|
||||||
|
if (qAbs(delta) <= kChipReach) {
|
||||||
|
batChip();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const qreal step = m_settings->walkSpeed() * 4.0 * dt;
|
||||||
|
setX(qBound(0.0, m_x + (delta > 0 ? step : -step), maxX));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_mood != Mood::Walking && m_mood != Mood::Running)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
const qreal speed = m_settings->walkSpeed() * (m_mood == Mood::Running ? 4.5 : 1.0);
|
const qreal speed = m_settings->walkSpeed() * (m_mood == Mood::Running ? 4.5 : 1.0);
|
||||||
const qreal step = speed * dt;
|
qreal next = m_x + (m_facingRight ? speed * dt : -speed * dt);
|
||||||
qreal next = m_x + (m_facingRight ? step : -step);
|
|
||||||
const qreal maxX = qMax(0.0, m_screenWidth - m_catWidth);
|
|
||||||
if (next >= maxX) {
|
if (next >= maxX) {
|
||||||
next = maxX;
|
next = maxX;
|
||||||
setFacingRight(false);
|
setFacingRight(false);
|
||||||
@@ -196,6 +301,11 @@ void CatController::pickNextMood()
|
|||||||
{
|
{
|
||||||
if (m_paused || m_dragging || m_frozen)
|
if (m_paused || m_dragging || m_frozen)
|
||||||
return;
|
return;
|
||||||
|
if (m_chipVisible) { // a chase is still on, keep after the chip
|
||||||
|
setMood(Mood::Chasing);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
auto *rng = QRandomGenerator::global();
|
auto *rng = QRandomGenerator::global();
|
||||||
const int roll = rng->bounded(100);
|
const int roll = rng->bounded(100);
|
||||||
|
|
||||||
|
|||||||
+38
-12
@@ -7,10 +7,9 @@
|
|||||||
#include <QPointF>
|
#include <QPointF>
|
||||||
|
|
||||||
class AppSettings;
|
class AppSettings;
|
||||||
class QWindow;
|
|
||||||
|
|
||||||
// Drives the cat: wanders along the bottom of the screen, occasionally dashes,
|
// Drives the cat: wanders along its ground line, sits, grooms, stretches and
|
||||||
// sits, grooms, stretches or naps, and reacts to petting and dragging.
|
// naps, chases the odd metal chip, and reacts to petting and dragging.
|
||||||
class CatController : public QObject
|
class CatController : public QObject
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
@@ -18,8 +17,9 @@ class CatController : public QObject
|
|||||||
QML_UNCREATABLE("Provided by C++")
|
QML_UNCREATABLE("Provided by C++")
|
||||||
|
|
||||||
Q_PROPERTY(qreal x READ x WRITE setX NOTIFY xChanged)
|
Q_PROPERTY(qreal x READ x WRITE setX NOTIFY xChanged)
|
||||||
Q_PROPERTY(qreal screenWidth READ screenWidth WRITE setScreenWidth NOTIFY screenWidthChanged)
|
Q_PROPERTY(qreal stageWidth READ stageWidth WRITE setStageWidth NOTIFY stageWidthChanged)
|
||||||
Q_PROPERTY(qreal catWidth READ catWidth WRITE setCatWidth NOTIFY catWidthChanged)
|
Q_PROPERTY(qreal catWidth READ catWidth WRITE setCatWidth NOTIFY catWidthChanged)
|
||||||
|
Q_PROPERTY(QPointF catCenter READ catCenter WRITE setCatCenter NOTIFY catCenterChanged)
|
||||||
Q_PROPERTY(bool facingRight READ facingRight NOTIFY facingRightChanged)
|
Q_PROPERTY(bool facingRight READ facingRight NOTIFY facingRightChanged)
|
||||||
Q_PROPERTY(Mood mood READ mood NOTIFY moodChanged)
|
Q_PROPERTY(Mood mood READ mood NOTIFY moodChanged)
|
||||||
Q_PROPERTY(bool paused READ paused WRITE setPaused NOTIFY pausedChanged)
|
Q_PROPERTY(bool paused READ paused WRITE setPaused NOTIFY pausedChanged)
|
||||||
@@ -27,23 +27,29 @@ class CatController : public QObject
|
|||||||
Q_PROPERTY(bool hovered READ hovered WRITE setHovered NOTIFY hoveredChanged)
|
Q_PROPERTY(bool hovered READ hovered WRITE setHovered NOTIFY hoveredChanged)
|
||||||
Q_PROPERTY(QPointF look READ look NOTIFY lookChanged)
|
Q_PROPERTY(QPointF look READ look NOTIFY lookChanged)
|
||||||
|
|
||||||
|
Q_PROPERTY(bool chipVisible READ chipVisible NOTIFY chipVisibleChanged)
|
||||||
|
Q_PROPERTY(qreal chipX READ chipX NOTIFY chipXChanged)
|
||||||
|
Q_PROPERTY(int chipKick READ chipKick NOTIFY chipKickChanged)
|
||||||
|
|
||||||
public:
|
public:
|
||||||
enum class Mood { Walking, Running, Sitting, Grooming, Stretching, Sleeping, Loving, Alert };
|
enum class Mood { Walking, Running, Sitting, Grooming, Stretching, Sleeping, Loving, Alert, Chasing, Batting };
|
||||||
Q_ENUM(Mood)
|
Q_ENUM(Mood)
|
||||||
|
|
||||||
explicit CatController(AppSettings *settings, QObject *parent = nullptr);
|
explicit CatController(AppSettings *settings, QObject *parent = nullptr);
|
||||||
|
|
||||||
void setWindow(QWindow *window);
|
|
||||||
|
|
||||||
qreal x() const { return m_x; }
|
qreal x() const { return m_x; }
|
||||||
void setX(qreal x);
|
void setX(qreal x);
|
||||||
|
|
||||||
qreal screenWidth() const { return m_screenWidth; }
|
qreal stageWidth() const { return m_stageWidth; }
|
||||||
void setScreenWidth(qreal width);
|
void setStageWidth(qreal width);
|
||||||
|
|
||||||
qreal catWidth() const { return m_catWidth; }
|
qreal catWidth() const { return m_catWidth; }
|
||||||
void setCatWidth(qreal width);
|
void setCatWidth(qreal width);
|
||||||
|
|
||||||
|
// Where the cat's face is on the desktop, so the eyes can find the cursor.
|
||||||
|
QPointF catCenter() const { return m_catCenter; }
|
||||||
|
void setCatCenter(const QPointF ¢er);
|
||||||
|
|
||||||
bool facingRight() const { return m_facingRight; }
|
bool facingRight() const { return m_facingRight; }
|
||||||
Mood mood() const { return m_mood; }
|
Mood mood() const { return m_mood; }
|
||||||
|
|
||||||
@@ -58,39 +64,54 @@ public:
|
|||||||
|
|
||||||
QPointF look() const { return m_look; }
|
QPointF look() const { return m_look; }
|
||||||
|
|
||||||
|
bool chipVisible() const { return m_chipVisible; }
|
||||||
|
qreal chipX() const { return m_chipX; }
|
||||||
|
int chipKick() const { return m_chipKick; }
|
||||||
|
|
||||||
Q_INVOKABLE void pet();
|
Q_INVOKABLE void pet();
|
||||||
Q_INVOKABLE void doTrick(Mood mood);
|
Q_INVOKABLE void doTrick(Mood mood);
|
||||||
|
Q_INVOKABLE void tossChip();
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void xChanged();
|
void xChanged();
|
||||||
void screenWidthChanged();
|
void stageWidthChanged();
|
||||||
void catWidthChanged();
|
void catWidthChanged();
|
||||||
|
void catCenterChanged();
|
||||||
void facingRightChanged();
|
void facingRightChanged();
|
||||||
void moodChanged();
|
void moodChanged();
|
||||||
void pausedChanged();
|
void pausedChanged();
|
||||||
void draggingChanged();
|
void draggingChanged();
|
||||||
void hoveredChanged();
|
void hoveredChanged();
|
||||||
void lookChanged();
|
void lookChanged();
|
||||||
|
void chipVisibleChanged();
|
||||||
|
void chipXChanged();
|
||||||
|
void chipKickChanged();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void tick();
|
void tick();
|
||||||
void trackCursor();
|
void trackCursor();
|
||||||
void setMood(Mood mood);
|
void setMood(Mood mood);
|
||||||
void setFacingRight(bool right);
|
void setFacingRight(bool right);
|
||||||
|
void setChipX(qreal x);
|
||||||
void pickNextMood();
|
void pickNextMood();
|
||||||
void holdMood(Mood mood, int ms);
|
void holdMood(Mood mood, int ms);
|
||||||
|
void batChip();
|
||||||
|
void endChase();
|
||||||
|
void scheduleChip();
|
||||||
|
qreal catNose() const;
|
||||||
|
|
||||||
AppSettings *m_settings;
|
AppSettings *m_settings;
|
||||||
QWindow *m_window = nullptr;
|
|
||||||
QTimer m_tickTimer;
|
QTimer m_tickTimer;
|
||||||
QTimer m_moodTimer;
|
QTimer m_moodTimer;
|
||||||
QTimer m_cursorTimer;
|
QTimer m_cursorTimer;
|
||||||
|
QTimer m_chipTimer;
|
||||||
QElapsedTimer m_clock;
|
QElapsedTimer m_clock;
|
||||||
qint64 m_lastTickMs = 0;
|
qint64 m_lastTickMs = 0;
|
||||||
|
|
||||||
qreal m_x = 120;
|
qreal m_x = 120;
|
||||||
qreal m_screenWidth = 1440;
|
qreal m_stageWidth = 1440;
|
||||||
qreal m_catWidth = 120;
|
qreal m_catWidth = 120;
|
||||||
|
QPointF m_catCenter;
|
||||||
bool m_facingRight = true;
|
bool m_facingRight = true;
|
||||||
Mood m_mood = Mood::Walking;
|
Mood m_mood = Mood::Walking;
|
||||||
bool m_paused = false;
|
bool m_paused = false;
|
||||||
@@ -98,4 +119,9 @@ private:
|
|||||||
bool m_hovered = false;
|
bool m_hovered = false;
|
||||||
bool m_frozen = false;
|
bool m_frozen = false;
|
||||||
QPointF m_look;
|
QPointF m_look;
|
||||||
|
|
||||||
|
bool m_chipVisible = false;
|
||||||
|
qreal m_chipX = 0;
|
||||||
|
int m_chipKick = 0;
|
||||||
|
int m_batsLeft = 0;
|
||||||
};
|
};
|
||||||
+5
-8
@@ -1,7 +1,6 @@
|
|||||||
#include <QApplication>
|
#include <QApplication>
|
||||||
#include <QQmlApplicationEngine>
|
#include <QQmlApplicationEngine>
|
||||||
#include <QQuickStyle>
|
#include <QQuickStyle>
|
||||||
#include <QQuickWindow>
|
|
||||||
#include <QLoggingCategory>
|
#include <QLoggingCategory>
|
||||||
|
|
||||||
#include "appsettings.h"
|
#include "appsettings.h"
|
||||||
@@ -10,9 +9,8 @@
|
|||||||
|
|
||||||
int main(int argc, char *argv[])
|
int main(int argc, char *argv[])
|
||||||
{
|
{
|
||||||
QCoreApplication::setOrganizationName("workcat");
|
QCoreApplication::setOrganizationName("TNCat");
|
||||||
QCoreApplication::setOrganizationDomain("workcat.app");
|
QCoreApplication::setApplicationName("TNCat");
|
||||||
QCoreApplication::setApplicationName("Workcat");
|
|
||||||
QApplication::setQuitOnLastWindowClosed(false);
|
QApplication::setQuitOnLastWindowClosed(false);
|
||||||
|
|
||||||
// QApplication (not QGuiApplication) so QSystemTrayIcon can show a menu.
|
// QApplication (not QGuiApplication) so QSystemTrayIcon can show a menu.
|
||||||
@@ -24,7 +22,7 @@ int main(int argc, char *argv[])
|
|||||||
TrayController tray(&cat);
|
TrayController tray(&cat);
|
||||||
|
|
||||||
if (QGuiApplication::platformName().startsWith(QLatin1String("wayland"))) {
|
if (QGuiApplication::platformName().startsWith(QLatin1String("wayland"))) {
|
||||||
qWarning("Workcat: 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.");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,10 +33,9 @@ int main(int argc, char *argv[])
|
|||||||
});
|
});
|
||||||
QObject::connect(&engine, &QQmlApplicationEngine::objectCreationFailed, &app,
|
QObject::connect(&engine, &QQmlApplicationEngine::objectCreationFailed, &app,
|
||||||
[] { QCoreApplication::exit(1); }, Qt::QueuedConnection);
|
[] { QCoreApplication::exit(1); }, Qt::QueuedConnection);
|
||||||
engine.loadFromModule("Workcat", "Main");
|
engine.loadFromModule("TNCat", "Main");
|
||||||
|
|
||||||
if (auto *root = qobject_cast<QQuickWindow *>(engine.rootObjects().value(0))) {
|
if (QObject *root = engine.rootObjects().value(0)) {
|
||||||
cat.setWindow(root);
|
|
||||||
QObject::connect(&tray, &TrayController::settingsRequested, root,
|
QObject::connect(&tray, &TrayController::settingsRequested, root,
|
||||||
[root] { QMetaObject::invokeMethod(root, "openSettings"); });
|
[root] { QMetaObject::invokeMethod(root, "openSettings"); });
|
||||||
}
|
}
|
||||||
|
|||||||
+11
-2
@@ -1,11 +1,20 @@
|
|||||||
#include "platforminfo.h"
|
#include "platforminfo.h"
|
||||||
|
|
||||||
#include <QGuiApplication>
|
#include <QGuiApplication>
|
||||||
|
#include <QRegion>
|
||||||
|
|
||||||
PlatformInfo::PlatformInfo(QObject *parent)
|
PlatformInfo::PlatformInfo(QObject *parent)
|
||||||
: QObject(parent)
|
: QObject(parent)
|
||||||
, m_name(QGuiApplication::platformName())
|
, m_name(QGuiApplication::platformName())
|
||||||
, m_bypass(qEnvironmentVariableIntValue("WORKCAT_X11_BYPASS") != 0)
|
, m_overlay(qEnvironmentVariableIntValue("TNCAT_OVERLAY") != 0)
|
||||||
, m_opaque(qEnvironmentVariableIntValue("WORKCAT_OPAQUE") != 0)
|
, m_bypass(qEnvironmentVariableIntValue("TNCAT_X11_BYPASS") != 0)
|
||||||
|
, m_opaque(qEnvironmentVariableIntValue("TNCAT_OPAQUE") != 0)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void PlatformInfo::setInputRegion(QQuickWindow *window, const QRectF ®ion)
|
||||||
|
{
|
||||||
|
if (!window)
|
||||||
|
return;
|
||||||
|
window->setMask(QRegion(region.toAlignedRect()));
|
||||||
|
}
|
||||||
+18
-3
@@ -2,9 +2,12 @@
|
|||||||
|
|
||||||
#include <QObject>
|
#include <QObject>
|
||||||
#include <QQmlEngine>
|
#include <QQmlEngine>
|
||||||
|
#include <QRectF>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
|
#include <QQuickWindow>
|
||||||
|
|
||||||
// Small facts about the host that QML needs in order to pick window flags.
|
// Small facts about the host that QML needs in order to pick window flags,
|
||||||
|
// plus the input-region call that makes an overlay window click-through.
|
||||||
class PlatformInfo : public QObject
|
class PlatformInfo : public QObject
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
@@ -14,6 +17,7 @@ class PlatformInfo : public QObject
|
|||||||
Q_PROPERTY(QString name READ name CONSTANT)
|
Q_PROPERTY(QString name READ name CONSTANT)
|
||||||
Q_PROPERTY(bool isX11 READ isX11 CONSTANT)
|
Q_PROPERTY(bool isX11 READ isX11 CONSTANT)
|
||||||
Q_PROPERTY(bool isWayland READ isWayland CONSTANT)
|
Q_PROPERTY(bool isWayland READ isWayland CONSTANT)
|
||||||
|
Q_PROPERTY(bool overlayMode READ overlayMode CONSTANT)
|
||||||
Q_PROPERTY(bool bypassWindowManager READ bypassWindowManager CONSTANT)
|
Q_PROPERTY(bool bypassWindowManager READ bypassWindowManager CONSTANT)
|
||||||
Q_PROPERTY(bool opaqueBackground READ opaqueBackground CONSTANT)
|
Q_PROPERTY(bool opaqueBackground READ opaqueBackground CONSTANT)
|
||||||
|
|
||||||
@@ -24,16 +28,27 @@ public:
|
|||||||
bool isX11() const { return m_name == QLatin1String("xcb"); }
|
bool isX11() const { return m_name == QLatin1String("xcb"); }
|
||||||
bool isWayland() const { return m_name.startsWith(QLatin1String("wayland")); }
|
bool isWayland() const { return m_name.startsWith(QLatin1String("wayland")); }
|
||||||
|
|
||||||
// WORKCAT_X11_BYPASS=1 turns the cat into an override-redirect window.
|
// Wayland forbids a client from placing its own window, so there the cat
|
||||||
|
// lives inside one screen-filling overlay and only moves within it.
|
||||||
|
// TNCAT_OVERLAY=1 forces the same mode elsewhere.
|
||||||
|
bool overlayMode() const { return m_overlay || isWayland(); }
|
||||||
|
|
||||||
|
// TNCAT_X11_BYPASS=1 turns the cat into an override-redirect window.
|
||||||
// Useful on window managers that refuse to keep a Qt::Tool window on top.
|
// Useful on window managers that refuse to keep a Qt::Tool window on top.
|
||||||
bool bypassWindowManager() const { return m_bypass; }
|
bool bypassWindowManager() const { return m_bypass; }
|
||||||
|
|
||||||
// WORKCAT_OPAQUE=1 paints a solid background instead of relying on a
|
// TNCAT_OPAQUE=1 paints a solid background instead of relying on a
|
||||||
// compositor. Without compositing X11 renders transparency as black.
|
// compositor. Without compositing X11 renders transparency as black.
|
||||||
bool opaqueBackground() const { return m_opaque; }
|
bool opaqueBackground() const { return m_opaque; }
|
||||||
|
|
||||||
|
// Restricts the window's input region to one rectangle so clicks land on
|
||||||
|
// whatever is behind the rest of the overlay. Implemented by the X11 and
|
||||||
|
// Wayland plugins; a no-op on platforms that ignore the mask.
|
||||||
|
Q_INVOKABLE void setInputRegion(QQuickWindow *window, const QRectF ®ion);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
QString m_name;
|
QString m_name;
|
||||||
|
bool m_overlay = false;
|
||||||
bool m_bypass = false;
|
bool m_bypass = false;
|
||||||
bool m_opaque = false;
|
bool m_opaque = false;
|
||||||
};
|
};
|
||||||
@@ -23,6 +23,8 @@ TrayController::TrayController(CatController *cat, QObject *parent)
|
|||||||
tricks->addAction(tr(e.label), this, [this, mood] { m_cat->doTrick(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 = m_menu.addAction(tr("Let the cat sleep"));
|
||||||
m_pauseAction->setCheckable(true);
|
m_pauseAction->setCheckable(true);
|
||||||
connect(m_pauseAction, &QAction::toggled, m_cat, &CatController::setPaused);
|
connect(m_pauseAction, &QAction::toggled, m_cat, &CatController::setPaused);
|
||||||
@@ -32,10 +34,10 @@ TrayController::TrayController(CatController *cat, QObject *parent)
|
|||||||
|
|
||||||
m_menu.addAction(tr("Settings…"), this, &TrayController::settingsRequested);
|
m_menu.addAction(tr("Settings…"), this, &TrayController::settingsRequested);
|
||||||
m_menu.addSeparator();
|
m_menu.addSeparator();
|
||||||
m_menu.addAction(tr("Quit Workcat"), qApp, &QCoreApplication::quit);
|
m_menu.addAction(tr("Quit TNCat"), qApp, &QCoreApplication::quit);
|
||||||
|
|
||||||
m_tray.setIcon(makeIcon());
|
m_tray.setIcon(makeIcon());
|
||||||
m_tray.setToolTip(tr("Workcat"));
|
m_tray.setToolTip(tr("TNCat"));
|
||||||
m_tray.setContextMenu(&m_menu);
|
m_tray.setContextMenu(&m_menu);
|
||||||
|
|
||||||
// Not every X11 desktop runs a status-notifier host. Without a tray the
|
// Not every X11 desktop runs a status-notifier host. Without a tray the
|
||||||
@@ -43,7 +45,7 @@ TrayController::TrayController(CatController *cat, QObject *parent)
|
|||||||
if (QSystemTrayIcon::isSystemTrayAvailable())
|
if (QSystemTrayIcon::isSystemTrayAvailable())
|
||||||
m_tray.show();
|
m_tray.show();
|
||||||
else
|
else
|
||||||
qWarning("Workcat: no system tray on this desktop. Right-click the cat for settings.");
|
qWarning("TNCat: no system tray on this desktop. Right-click the cat for settings.");
|
||||||
}
|
}
|
||||||
|
|
||||||
QIcon TrayController::makeIcon()
|
QIcon TrayController::makeIcon()
|
||||||
|
|||||||
Reference in new issue
Block a user