diff --git a/CMakeLists.txt b/CMakeLists.txt index 9143fbc..1238574 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -23,14 +23,12 @@ target_include_directories(tncat PRIVATE src) # 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 - SettingsWindow AppButton AppSlider AppToggle SettingsGroup CatDefs) + SettingsWindow AppButton AppSlider AppToggle SettingsGroup) foreach(qmlfile IN LISTS TNCAT_QML) set_source_files_properties(qml/${qmlfile}.qml PROPERTIES QT_RESOURCE_ALIAS ${qmlfile}.qml) list(APPEND TNCAT_QML_FILES qml/${qmlfile}.qml) endforeach() -set_source_files_properties(qml/CatDefs.qml PROPERTIES QT_QML_SINGLETON_TYPE TRUE) - qt_add_qml_module(tncat URI TNCat VERSION 1.0 diff --git a/README.md b/README.md index 9e40343..03b4e4a 100644 --- a/README.md +++ b/README.md @@ -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_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_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. | ## 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 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 Untested on real hardware so far. What the code already does for that target: diff --git a/qml/CatDefs.qml b/qml/CatDefs.qml deleted file mode 100644 index 5f49d17..0000000 --- a/qml/CatDefs.qml +++ /dev/null @@ -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" - } -} diff --git a/qml/CatWindow.qml b/qml/CatWindow.qml index 33be2ce..d5799ab 100644 --- a/qml/CatWindow.qml +++ b/qml/CatWindow.qml @@ -13,7 +13,8 @@ Window { signal settingsRequested() 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 y: Screen.desktopAvailableHeight - height - settings.bottomMargin color: Platform.opaqueBackground ? settings.coat : "transparent" @@ -26,8 +27,25 @@ Window { Component.onCompleted: { cat.stageWidth = Screen.desktopAvailableWidth 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 // The eyes need to know where the face sits on the desktop. @@ -38,10 +56,11 @@ Window { } Cat { + id: sprite anchors.fill: parent coat: win.settings.coat facingRight: win.cat.facingRight - mood: CatDefs.mood(win.cat.mood) + mood: win.cat.moodName lifted: win.cat.dragging petted: win.cat.hovered && !win.cat.dragging look: win.cat.look diff --git a/qml/ChipWindow.qml b/qml/ChipWindow.qml index eaad11f..4a63f3e 100644 --- a/qml/ChipWindow.qml +++ b/qml/ChipWindow.qml @@ -13,21 +13,22 @@ Window { readonly property int chipSize: Math.round(settings.catSize * 0.46) width: chipSize - height: Math.round(chipSize * CatDefs.chipAspect) + height: Math.round(width * curl.implicitHeight / curl.implicitWidth) x: cat.chipX - width / 2 y: Screen.desktopAvailableHeight - height - settings.bottomMargin - color: "transparent" + color: Platform.opaqueBackground ? "#d8d8d8" : "transparent" flags: Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint | Qt.Tool | Qt.NoDropShadowWindowHint | Qt.WindowDoesNotAcceptFocus | Qt.WindowTransparentForInput | (Platform.bypassWindowManager ? Qt.X11BypassWindowManagerHint : 0) - visible: cat.chipVisible && !Platform.opaqueBackground + visible: cat.chipVisible Behavior on x { NumberAnimation { duration: 320; easing.type: Easing.OutQuad } } Chip { + id: curl anchors.fill: parent spin: win.cat.chipSpin } diff --git a/qml/CupWindow.qml b/qml/CupWindow.qml index 06af0df..654b85b 100644 --- a/qml/CupWindow.qml +++ b/qml/CupWindow.qml @@ -12,17 +12,18 @@ Window { readonly property int cupSize: Math.round(settings.catSize * 0.38) width: cupSize - height: Math.round(cupSize * 1.18) + height: Math.round(width * mug.implicitHeight / mug.implicitWidth) x: cat.cupX - width / 2 y: Screen.desktopAvailableHeight - height - settings.bottomMargin - color: "transparent" + color: Platform.opaqueBackground ? "#d8d8d8" : "transparent" flags: Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint | Qt.Tool | Qt.NoDropShadowWindowHint | Qt.WindowDoesNotAcceptFocus | Qt.WindowTransparentForInput | (Platform.bypassWindowManager ? Qt.X11BypassWindowManagerHint : 0) - visible: cat.cupVisible && !Platform.opaqueBackground + visible: cat.cupVisible Cup { + id: mug anchors.fill: parent } } diff --git a/qml/OverlayWindow.qml b/qml/OverlayWindow.qml index 570a170..71ad7bd 100644 --- a/qml/OverlayWindow.qml +++ b/qml/OverlayWindow.qml @@ -18,7 +18,7 @@ Window { property bool overlayActive: false 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 // Only the cat itself should swallow clicks; everything else falls through. @@ -57,10 +57,11 @@ Window { } Chip { + id: curl x: win.cat.chipX - width / 2 y: win.height - height - win.settings.bottomMargin 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 spin: win.cat.chipSpin @@ -70,10 +71,11 @@ Window { } Cup { + id: mug x: win.cat.cupX - width / 2 y: win.height - height - win.settings.bottomMargin 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 } @@ -85,10 +87,11 @@ Window { height: win.catH Cat { + id: sprite anchors.fill: parent coat: win.settings.coat facingRight: win.cat.facingRight - mood: CatDefs.mood(win.cat.mood) + mood: win.cat.moodName lifted: win.cat.dragging petted: win.cat.hovered && !win.cat.dragging look: win.cat.look diff --git a/qml/SettingsWindow.qml b/qml/SettingsWindow.qml index c704202..a4c7e28 100644 --- a/qml/SettingsWindow.qml +++ b/qml/SettingsWindow.qml @@ -52,7 +52,7 @@ Window { Loader { id: preview width: 106 - height: Math.round(106 * CatDefs.aspect) + height: Math.round(width * 106 / 120) anchors.verticalCenter: parent.verticalCenter active: win.visible sourceComponent: Cat { diff --git a/src/catcontroller.cpp b/src/catcontroller.cpp index 457e95e..c1ee38f 100644 --- a/src/catcontroller.cpp +++ b/src/catcontroller.cpp @@ -48,6 +48,12 @@ CatController::CatController(AppSettings *settings, QObject *parent) } } +QString CatController::moodName() const +{ + const QMetaEnum me = QMetaEnum::fromType(); + return QString::fromLatin1(me.valueToKey(static_cast(m_mood))).toLower(); +} + qreal CatController::catNose() const { return m_facingRight ? m_x + m_catWidth * 0.86 : m_x + m_catWidth * 0.14; diff --git a/src/catcontroller.h b/src/catcontroller.h index 058dfdc..da2fe52 100644 --- a/src/catcontroller.h +++ b/src/catcontroller.h @@ -5,6 +5,7 @@ #include #include #include +#include class AppSettings; @@ -22,6 +23,7 @@ class CatController : public QObject Q_PROPERTY(QPointF catCenter READ catCenter WRITE setCatCenter NOTIFY catCenterChanged) Q_PROPERTY(bool facingRight READ facingRight NOTIFY facingRightChanged) 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 dragging READ dragging WRITE setDragging NOTIFY draggingChanged) Q_PROPERTY(bool hovered READ hovered WRITE setHovered NOTIFY hoveredChanged) @@ -55,6 +57,9 @@ public: bool facingRight() const { return m_facingRight; } 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; } void setPaused(bool paused); diff --git a/src/platforminfo.cpp b/src/platforminfo.cpp index 3c70e56..a235614 100644 --- a/src/platforminfo.cpp +++ b/src/platforminfo.cpp @@ -9,9 +9,25 @@ PlatformInfo::PlatformInfo(QObject *parent) , m_overlay(qEnvironmentVariableIntValue("TNCAT_OVERLAY") != 0) , m_bypass(qEnvironmentVariableIntValue("TNCAT_X11_BYPASS") != 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 ®ion) { if (!window) diff --git a/src/platforminfo.h b/src/platforminfo.h index a245fbf..8840da1 100644 --- a/src/platforminfo.h +++ b/src/platforminfo.h @@ -20,6 +20,7 @@ class PlatformInfo : public QObject Q_PROPERTY(bool overlayMode READ overlayMode CONSTANT) Q_PROPERTY(bool bypassWindowManager READ bypassWindowManager CONSTANT) Q_PROPERTY(bool opaqueBackground READ opaqueBackground CONSTANT) + Q_PROPERTY(bool debug READ debug CONSTANT) public: explicit PlatformInfo(QObject *parent = nullptr); @@ -41,6 +42,14 @@ public: // compositor. Without compositing X11 renders transparency as black. 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 // whatever is behind the rest of the overlay. Implemented by the X11 and // Wayland plugins; a no-op on platforms that ignore the mask. @@ -51,4 +60,6 @@ private: bool m_overlay = false; bool m_bypass = false; bool m_opaque = false; + bool m_debug = false; + bool m_warnedTranslucency = false; };