Fix the collapsed cat window on Linux, warn about the compositor

The CatDefs singleton did not resolve on the reporter's Linux build, which
showed up as "property mood of object CatDefs is not a function". The same
lookup failure made CatDefs.aspect undefined, so the cat window's height became
NaN and the cat rendered as a moving line. The mug survived because its height
used a literal ratio.

The singleton is gone rather than repaired, since nothing in it had to be
shared. The mood name now comes from C++, derived from the enum key, so there
is one source of truth instead of a list in QML that had to match. Window sizes
now follow the implicit size of the drawing they contain, so the proportions
live in the one file that defines them.

Also for X11: TNCat now warns, naming Xfce's compositor setting, when its
window comes up without an alpha channel, which is what a missing compositor
looks like and why the mug had a black background. Opaque mode no longer hides
the chip and the mug. TNCAT_DEBUG=1 logs window geometry.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
JulianandClaude Opus 5 committed 2026-09-11 09:33:44 +02:00
1 parent 9106ce47f4
commit 0aceb3665d
12 files changed
+94 -36

No files matched your search

+1 -3
View File
@@ -23,14 +23,12 @@ 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 Desktop Cat Chip Cup CatWindow ChipWindow CupWindow OverlayWindow set(TNCAT_QML Main Desktop Cat Chip Cup CatWindow ChipWindow CupWindow OverlayWindow
SettingsWindow AppButton AppSlider AppToggle SettingsGroup CatDefs) SettingsWindow AppButton AppSlider AppToggle SettingsGroup)
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)
endforeach() endforeach()
set_source_files_properties(qml/CatDefs.qml PROPERTIES QT_QML_SINGLETON_TYPE TRUE)
qt_add_qml_module(tncat qt_add_qml_module(tncat
URI TNCat URI TNCat
VERSION 1.0 VERSION 1.0
+17
View File
@@ -75,6 +75,7 @@ otool -L build/tncat.app/Contents/MacOS/tncat | grep -o 'Qt[A-Za-z]*\.framework'
| `TNCAT_OVERLAY=1` | Forces the screen-filling overlay layout that Wayland needs, for testing it on X11. | | `TNCAT_OVERLAY=1` | Forces the screen-filling overlay layout that Wayland needs, for testing it on X11. |
| `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_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. | | `TNCAT_POSE=Sitting` | Freezes the cat in one mood. Handy for screenshots. Accepts any `CatController::Mood` name. |
| `TNCAT_DEBUG=1` | Logs the cat window's geometry and the screen it sits on, every time either changes. First thing to try when a window comes up the wrong size. |
| `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. | | `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. |
## Two window layouts ## Two window layouts
@@ -94,6 +95,22 @@ 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 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. input mask, which would make the overlay swallow every click on the desktop.
## Xfce and X11
This is the main target. Two things to know.
**Transparency needs a compositor.** Without one, X11 has no alpha channel to
composite against and the transparent window background renders as a black box
around the cat. In Xfce it is one checkbox: Settings, Window Manager Tweaks,
Compositor, Enable display compositing. TNCat prints a warning naming that
setting when its window comes up without an alpha channel. `TNCAT_OPAQUE=1`
paints a solid background instead, which is usable but not pretty.
**Keeping the cat on top.** The default is a frameless `Qt::Tool` window that
stays on top. If the window manager puts it behind other windows or shows it in
the taskbar, `TNCAT_X11_BYPASS=1` makes it an override-redirect window that the
window manager does not manage at all.
## Running on a HEIDENHAIN control ## Running on a HEIDENHAIN control
Untested on real hardware so far. What the code already does for that target: Untested on real hardware so far. What the code already does for that target:
-19
View File
@@ -1,19 +0,0 @@
pragma Singleton
import QtQuick
// Shared facts about the drawing, so every window sizes the cat the same way.
QtObject {
// Cat.qml draws in a 100 x 90 box inside a 120 x 106 window box. The slack
// holds the whiskers, the tail and the glyphs that float above the head.
readonly property real aspect: 106 / 120
// Chip.qml draws in a 52 x 58 box, large enough to hold the tumbling curl.
readonly property real chipAspect: 58 / 52
readonly property var moodNames: ["walking", "running", "sitting", "grooming", "stretching",
"sleeping", "loving", "alert", "chasing", "batting", "drinking"]
function mood(value) {
return moodNames[value] !== undefined ? moodNames[value] : "sitting"
}
}
+22 -3
View File
@@ -13,7 +13,8 @@ Window {
signal settingsRequested() signal settingsRequested()
width: settings.catSize width: settings.catSize
height: Math.round(settings.catSize * CatDefs.aspect) // The drawing knows its own proportions; no constant to keep in sync.
height: Math.round(width * sprite.implicitHeight / sprite.implicitWidth)
x: cat.x x: cat.x
y: Screen.desktopAvailableHeight - height - settings.bottomMargin y: Screen.desktopAvailableHeight - height - settings.bottomMargin
color: Platform.opaqueBackground ? settings.coat : "transparent" color: Platform.opaqueBackground ? settings.coat : "transparent"
@@ -26,8 +27,25 @@ Window {
Component.onCompleted: { Component.onCompleted: {
cat.stageWidth = Screen.desktopAvailableWidth cat.stageWidth = Screen.desktopAvailableWidth
cat.catWidth = width cat.catWidth = width
Platform.checkTranslucency(win)
if (Platform.debug)
win.logGeometry("created")
}
function logGeometry(what) {
console.log("TNCat cat window", what,
"x", win.x, "y", win.y, "w", win.width, "h", win.height,
"| screen", Screen.width + "x" + Screen.height,
"available", Screen.desktopAvailableWidth + "x" + Screen.desktopAvailableHeight,
"| platform", Platform.name, "overlay", Platform.overlayMode)
}
onHeightChanged: if (Platform.debug) logGeometry("height changed")
onWidthChanged: {
cat.catWidth = width
if (Platform.debug)
logGeometry("width changed")
} }
onWidthChanged: cat.catWidth = width
Screen.onDesktopAvailableWidthChanged: cat.stageWidth = Screen.desktopAvailableWidth Screen.onDesktopAvailableWidthChanged: cat.stageWidth = Screen.desktopAvailableWidth
// The eyes need to know where the face sits on the desktop. // The eyes need to know where the face sits on the desktop.
@@ -38,10 +56,11 @@ Window {
} }
Cat { Cat {
id: sprite
anchors.fill: parent anchors.fill: parent
coat: win.settings.coat coat: win.settings.coat
facingRight: win.cat.facingRight facingRight: win.cat.facingRight
mood: CatDefs.mood(win.cat.mood) mood: win.cat.moodName
lifted: win.cat.dragging lifted: win.cat.dragging
petted: win.cat.hovered && !win.cat.dragging petted: win.cat.hovered && !win.cat.dragging
look: win.cat.look look: win.cat.look
+4 -3
View File
@@ -13,21 +13,22 @@ Window {
readonly property int chipSize: Math.round(settings.catSize * 0.46) readonly property int chipSize: Math.round(settings.catSize * 0.46)
width: chipSize width: chipSize
height: Math.round(chipSize * CatDefs.chipAspect) height: Math.round(width * curl.implicitHeight / curl.implicitWidth)
x: cat.chipX - width / 2 x: cat.chipX - width / 2
y: Screen.desktopAvailableHeight - height - settings.bottomMargin y: Screen.desktopAvailableHeight - height - settings.bottomMargin
color: "transparent" color: Platform.opaqueBackground ? "#d8d8d8" : "transparent"
flags: Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint | Qt.Tool flags: Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint | Qt.Tool
| Qt.NoDropShadowWindowHint | Qt.WindowDoesNotAcceptFocus | Qt.NoDropShadowWindowHint | Qt.WindowDoesNotAcceptFocus
| Qt.WindowTransparentForInput | Qt.WindowTransparentForInput
| (Platform.bypassWindowManager ? Qt.X11BypassWindowManagerHint : 0) | (Platform.bypassWindowManager ? Qt.X11BypassWindowManagerHint : 0)
visible: cat.chipVisible && !Platform.opaqueBackground visible: cat.chipVisible
Behavior on x { Behavior on x {
NumberAnimation { duration: 320; easing.type: Easing.OutQuad } NumberAnimation { duration: 320; easing.type: Easing.OutQuad }
} }
Chip { Chip {
id: curl
anchors.fill: parent anchors.fill: parent
spin: win.cat.chipSpin spin: win.cat.chipSpin
} }
+4 -3
View File
@@ -12,17 +12,18 @@ Window {
readonly property int cupSize: Math.round(settings.catSize * 0.38) readonly property int cupSize: Math.round(settings.catSize * 0.38)
width: cupSize width: cupSize
height: Math.round(cupSize * 1.18) height: Math.round(width * mug.implicitHeight / mug.implicitWidth)
x: cat.cupX - width / 2 x: cat.cupX - width / 2
y: Screen.desktopAvailableHeight - height - settings.bottomMargin y: Screen.desktopAvailableHeight - height - settings.bottomMargin
color: "transparent" color: Platform.opaqueBackground ? "#d8d8d8" : "transparent"
flags: Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint | Qt.Tool flags: Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint | Qt.Tool
| Qt.NoDropShadowWindowHint | Qt.WindowDoesNotAcceptFocus | Qt.NoDropShadowWindowHint | Qt.WindowDoesNotAcceptFocus
| Qt.WindowTransparentForInput | Qt.WindowTransparentForInput
| (Platform.bypassWindowManager ? Qt.X11BypassWindowManagerHint : 0) | (Platform.bypassWindowManager ? Qt.X11BypassWindowManagerHint : 0)
visible: cat.cupVisible && !Platform.opaqueBackground visible: cat.cupVisible
Cup { Cup {
id: mug
anchors.fill: parent anchors.fill: parent
} }
} }
+7 -4
View File
@@ -18,7 +18,7 @@ Window {
property bool overlayActive: false property bool overlayActive: false
readonly property int catW: settings.catSize readonly property int catW: settings.catSize
readonly property int catH: Math.round(settings.catSize * CatDefs.aspect) readonly property int catH: Math.round(catW * sprite.implicitHeight / sprite.implicitWidth)
readonly property real groundY: height - catH - settings.bottomMargin readonly property real groundY: height - catH - settings.bottomMargin
// Only the cat itself should swallow clicks; everything else falls through. // Only the cat itself should swallow clicks; everything else falls through.
@@ -57,10 +57,11 @@ Window {
} }
Chip { Chip {
id: curl
x: win.cat.chipX - width / 2 x: win.cat.chipX - width / 2
y: win.height - height - win.settings.bottomMargin y: win.height - height - win.settings.bottomMargin
width: Math.round(win.settings.catSize * 0.46) width: Math.round(win.settings.catSize * 0.46)
height: Math.round(width * CatDefs.chipAspect) height: Math.round(width * curl.implicitHeight / curl.implicitWidth)
visible: win.cat.chipVisible visible: win.cat.chipVisible
spin: win.cat.chipSpin spin: win.cat.chipSpin
@@ -70,10 +71,11 @@ Window {
} }
Cup { Cup {
id: mug
x: win.cat.cupX - width / 2 x: win.cat.cupX - width / 2
y: win.height - height - win.settings.bottomMargin y: win.height - height - win.settings.bottomMargin
width: Math.round(win.settings.catSize * 0.38) width: Math.round(win.settings.catSize * 0.38)
height: Math.round(width * 1.18) height: Math.round(width * mug.implicitHeight / mug.implicitWidth)
visible: win.cat.cupVisible visible: win.cat.cupVisible
} }
@@ -85,10 +87,11 @@ Window {
height: win.catH height: win.catH
Cat { Cat {
id: sprite
anchors.fill: parent anchors.fill: parent
coat: win.settings.coat coat: win.settings.coat
facingRight: win.cat.facingRight facingRight: win.cat.facingRight
mood: CatDefs.mood(win.cat.mood) mood: win.cat.moodName
lifted: win.cat.dragging lifted: win.cat.dragging
petted: win.cat.hovered && !win.cat.dragging petted: win.cat.hovered && !win.cat.dragging
look: win.cat.look look: win.cat.look
+1 -1
View File
@@ -52,7 +52,7 @@ Window {
Loader { Loader {
id: preview id: preview
width: 106 width: 106
height: Math.round(106 * CatDefs.aspect) height: Math.round(width * 106 / 120)
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
active: win.visible active: win.visible
sourceComponent: Cat { sourceComponent: Cat {
+6
View File
@@ -48,6 +48,12 @@ CatController::CatController(AppSettings *settings, QObject *parent)
} }
} }
QString CatController::moodName() const
{
const QMetaEnum me = QMetaEnum::fromType<Mood>();
return QString::fromLatin1(me.valueToKey(static_cast<int>(m_mood))).toLower();
}
qreal CatController::catNose() const qreal CatController::catNose() const
{ {
return m_facingRight ? m_x + m_catWidth * 0.86 : m_x + m_catWidth * 0.14; return m_facingRight ? m_x + m_catWidth * 0.86 : m_x + m_catWidth * 0.14;
+5
View File
@@ -5,6 +5,7 @@
#include <QTimer> #include <QTimer>
#include <QElapsedTimer> #include <QElapsedTimer>
#include <QPointF> #include <QPointF>
#include <QString>
class AppSettings; class AppSettings;
@@ -22,6 +23,7 @@ class CatController : public QObject
Q_PROPERTY(QPointF catCenter READ catCenter WRITE setCatCenter NOTIFY catCenterChanged) 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(QString moodName READ moodName NOTIFY moodChanged)
Q_PROPERTY(bool paused READ paused WRITE setPaused NOTIFY pausedChanged) Q_PROPERTY(bool paused READ paused WRITE setPaused NOTIFY pausedChanged)
Q_PROPERTY(bool dragging READ dragging WRITE setDragging NOTIFY draggingChanged) Q_PROPERTY(bool dragging READ dragging WRITE setDragging NOTIFY draggingChanged)
Q_PROPERTY(bool hovered READ hovered WRITE setHovered NOTIFY hoveredChanged) Q_PROPERTY(bool hovered READ hovered WRITE setHovered NOTIFY hoveredChanged)
@@ -55,6 +57,9 @@ public:
bool facingRight() const { return m_facingRight; } bool facingRight() const { return m_facingRight; }
Mood mood() const { return m_mood; } Mood mood() const { return m_mood; }
// Lower-case enum key, which is exactly what Cat.qml switches on.
QString moodName() const;
bool paused() const { return m_paused; } bool paused() const { return m_paused; }
void setPaused(bool paused); void setPaused(bool paused);
+16
View File
@@ -9,9 +9,25 @@ PlatformInfo::PlatformInfo(QObject *parent)
, m_overlay(qEnvironmentVariableIntValue("TNCAT_OVERLAY") != 0) , m_overlay(qEnvironmentVariableIntValue("TNCAT_OVERLAY") != 0)
, m_bypass(qEnvironmentVariableIntValue("TNCAT_X11_BYPASS") != 0) , m_bypass(qEnvironmentVariableIntValue("TNCAT_X11_BYPASS") != 0)
, m_opaque(qEnvironmentVariableIntValue("TNCAT_OPAQUE") != 0) , m_opaque(qEnvironmentVariableIntValue("TNCAT_OPAQUE") != 0)
, m_debug(qEnvironmentVariableIntValue("TNCAT_DEBUG") != 0)
{ {
} }
void PlatformInfo::checkTranslucency(QQuickWindow *window)
{
if (!window || m_opaque || m_warnedTranslucency)
return;
if (window->format().alphaBufferSize() > 0)
return;
m_warnedTranslucency = true;
qWarning("TNCat: this window has no alpha channel, so the transparent "
"background will render as a black box. On X11 that means no "
"compositor is running. In Xfce turn on Settings, Window Manager "
"Tweaks, Compositor, Enable display compositing. Or start TNCat "
"with TNCAT_OPAQUE=1 to paint a solid background instead.");
}
void PlatformInfo::setInputRegion(QQuickWindow *window, const QRectF &region) void PlatformInfo::setInputRegion(QQuickWindow *window, const QRectF &region)
{ {
if (!window) if (!window)
+11
View File
@@ -20,6 +20,7 @@ class PlatformInfo : public QObject
Q_PROPERTY(bool overlayMode READ overlayMode 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)
Q_PROPERTY(bool debug READ debug CONSTANT)
public: public:
explicit PlatformInfo(QObject *parent = nullptr); explicit PlatformInfo(QObject *parent = nullptr);
@@ -41,6 +42,14 @@ public:
// 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; }
// TNCAT_DEBUG=1 logs window geometry, which is the fastest way to see a
// window that a window manager or a bad binding collapsed.
bool debug() const { return m_debug; }
// Warns once if the window came up without an alpha channel, which is what
// a missing compositor looks like: transparency renders as black.
Q_INVOKABLE void checkTranslucency(QQuickWindow *window);
// Restricts the window's input region to one rectangle so clicks land on // 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 // whatever is behind the rest of the overlay. Implemented by the X11 and
// Wayland plugins; a no-op on platforms that ignore the mask. // Wayland plugins; a no-op on platforms that ignore the mask.
@@ -51,4 +60,6 @@ private:
bool m_overlay = false; bool m_overlay = false;
bool m_bypass = false; bool m_bypass = false;
bool m_opaque = false; bool m_opaque = false;
bool m_debug = false;
bool m_warnedTranslucency = false;
}; };