Files
DeeJanuz--frametop/decoration/contents/ui/main.qml
T
DeeJanuzandClaude Opus 5.5 00dfd61396 Title bar button: fixes from trying it in the live desktop
- The decoration's "bottom" property clashed with Item's (KWin refused it).
- No On All Desktops button with one virtual desktop, like Breeze.
- decoration/apply.sh finds the session's D-Bus through plasmashell (KWin's
  environment isn't readable), and installs each try under a new name: KWin
  keeps a decoration's QML by name until it restarts.
- ft-floatd starts its script with Scripting.start: after a reload, the new
  script gets the old one's id while the old one is still being deleted, so
  run() on /Scripting/Script<id> went to the old script and the new one never
  ran (a second ft-floatd start left floating windows without their script).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 22:11:29 -06:00

271 lines
9.2 KiB
QML

/*
Frametop's window decoration: Breeze's flat title bar, drawn in QML for KWin's Aurorae
engine, plus a button left of Close that floats the window in VR (docs/floating-windows.md,
decision 25). Aurorae loads QML without compiling anything, so this keeps working across
SteamOS's KWin updates, which a C++ decoration wouldn't.
The float button is the Keep Below button (FtButton.qml): Frametop's KWin script floats a
window when keep-below is set and docks it when it's cleared. A Keep Below button in the
configured button order is left out, since the float button stands in for it.
SPDX-License-Identifier: GPL-2.0-or-later
*/
import QtQuick
import org.kde.kwin.decoration
Decoration {
id: root
alpha: false
DecorationOptions {
id: options
deco: decoration
}
TextMetrics {
id: metrics
font: options.titleFont
text: "Mj"
}
readonly property bool maximized: decoration.client.maximized
readonly property int buttonSize: Math.max(16, Math.round(metrics.height * 1.25))
readonly property int titleHeight: buttonSize + 8
readonly property int borderSize: decorationSettings.borderSize
readonly property int side: {
switch (borderSize) {
case DecorationOptions.BorderNone:
case DecorationOptions.BorderNoSides: return 0;
case DecorationOptions.BorderTiny: return 2;
case DecorationOptions.BorderLarge: return 6;
case DecorationOptions.BorderVeryLarge: return 8;
case DecorationOptions.BorderHuge: return 12;
case DecorationOptions.BorderVeryHuge: return 18;
case DecorationOptions.BorderOversized: return 27;
default: return 4;
}
}
readonly property int bottomBorder: borderSize === DecorationOptions.BorderNone ? 0
: borderSize === DecorationOptions.BorderNoSides ? 4 : side
readonly property color outline: Qt.tint(options.titleBarColor, Qt.rgba(0, 0, 0, 0.35))
function applyBorders() {
borders.left = side;
borders.right = side;
borders.bottom = bottomBorder;
borders.top = titleHeight;
maximizedBorders.top = titleHeight;
// Without visible side borders, keep a strip to grab for resizing.
extendedBorders.left = side ? 0 : 4;
extendedBorders.right = side ? 0 : 4;
extendedBorders.bottom = bottomBorder ? 0 : 4;
}
onTitleHeightChanged: applyBorders()
onSideChanged: applyBorders()
onBottomBorderChanged: applyBorders()
Component.onCompleted: applyBorders()
// The configured buttons, with the float button left of Close (or first on the right
// when there's no Close), and no Keep Below of their own.
function order(list, right) {
const out = [];
let placed = false;
for (let i = 0; i < list.length; ++i) {
const t = list[i];
if (t === DecorationOptions.DecorationButtonKeepBelow)
continue;
if (t === DecorationOptions.DecorationButtonClose && !placed) {
if (right) {
out.push(DecorationOptions.DecorationButtonKeepBelow, t);
} else {
out.push(t, DecorationOptions.DecorationButtonKeepBelow);
}
placed = true;
continue;
}
out.push(t);
}
return {buttons: out, placed: placed};
}
readonly property var leftOrder: order(options.titleButtonsLeft || [], false)
readonly property var rightOrder: {
const r = order(options.titleButtonsRight || [], true);
if (!r.placed && !leftOrder.placed)
r.buttons.unshift(DecorationOptions.DecorationButtonKeepBelow);
return r;
}
function componentFor(t) {
switch (t) {
case DecorationOptions.DecorationButtonMenu: return menuButton;
case DecorationOptions.DecorationButtonExplicitSpacer: return spacer;
case DecorationOptions.DecorationButtonClose: return closeButton;
case DecorationOptions.DecorationButtonMaximizeRestore: return maximizeButton;
case DecorationOptions.DecorationButtonMinimize: return minimizeButton;
case DecorationOptions.DecorationButtonKeepBelow: return floatButton;
case DecorationOptions.DecorationButtonKeepAbove: return keepAboveButton;
case DecorationOptions.DecorationButtonShade: return shadeButton;
case DecorationOptions.DecorationButtonOnAllDesktops: return allDesktopsButton;
case DecorationOptions.DecorationButtonQuickHelp: return helpButton;
case DecorationOptions.DecorationButtonApplicationMenu: return appMenuButton;
}
return null;
}
Rectangle {
anchors.fill: parent
color: options.titleBarColor
border.width: root.maximized ? 0 : 1
border.color: root.outline
}
Item {
id: titleBar
x: root.maximized ? 0 : Math.max(root.side, 1)
y: root.maximized ? 0 : 1
width: root.width - 2 * x
height: root.titleHeight - y
Row {
id: leftButtons
anchors.left: parent.left
anchors.leftMargin: 4
anchors.verticalCenter: parent.verticalCenter
spacing: 4
Repeater {
model: root.leftOrder.buttons
delegate: Loader {
required property var modelData
sourceComponent: root.componentFor(modelData)
}
}
}
Row {
id: rightButtons
anchors.right: parent.right
anchors.rightMargin: 4
anchors.verticalCenter: parent.verticalCenter
spacing: 4
layoutDirection: Qt.LeftToRight
Repeater {
model: root.rightOrder.buttons
delegate: Loader {
required property var modelData
sourceComponent: root.componentFor(modelData)
}
}
}
// Centred over the whole bar, like Breeze, but never under the buttons.
Text {
id: caption
readonly property real free: rightButtons.x - (leftButtons.x + leftButtons.width) - 16
width: Math.min(implicitWidth, free)
x: Math.max(leftButtons.x + leftButtons.width + 8,
Math.min((parent.width - width) / 2, rightButtons.x - 8 - width))
anchors.verticalCenter: parent.verticalCenter
text: decoration.client.caption
textFormat: Text.PlainText
font: options.titleFont
color: options.fontColor
elide: Text.ElideRight
renderType: Text.NativeRendering
}
Component.onCompleted: decoration.installTitleItem(titleBar)
}
Component {
id: menuButton
MenuButton {
width: root.buttonSize
height: root.buttonSize
}
}
Component {
id: spacer
Item {
width: root.buttonSize
height: root.buttonSize
}
}
Component {
id: closeButton
FtButton {
buttonType: DecorationOptions.DecorationButtonClose
size: root.buttonSize
fg: options.fontColor
bg: options.titleBarColor
}
}
Component {
id: maximizeButton
FtButton {
buttonType: DecorationOptions.DecorationButtonMaximizeRestore
size: root.buttonSize
fg: options.fontColor
bg: options.titleBarColor
}
}
Component {
id: minimizeButton
FtButton {
buttonType: DecorationOptions.DecorationButtonMinimize
size: root.buttonSize
fg: options.fontColor
bg: options.titleBarColor
}
}
Component {
id: floatButton
FtButton {
buttonType: DecorationOptions.DecorationButtonKeepBelow
size: root.buttonSize
fg: options.fontColor
bg: options.titleBarColor
}
}
Component {
id: keepAboveButton
FtButton {
buttonType: DecorationOptions.DecorationButtonKeepAbove
size: root.buttonSize
fg: options.fontColor
bg: options.titleBarColor
}
}
Component {
id: shadeButton
FtButton {
buttonType: DecorationOptions.DecorationButtonShade
size: root.buttonSize
fg: options.fontColor
bg: options.titleBarColor
}
}
Component {
id: allDesktopsButton
FtButton {
buttonType: DecorationOptions.DecorationButtonOnAllDesktops
size: root.buttonSize
fg: options.fontColor
bg: options.titleBarColor
}
}
Component {
id: helpButton
FtButton {
buttonType: DecorationOptions.DecorationButtonQuickHelp
size: root.buttonSize
fg: options.fontColor
bg: options.titleBarColor
}
}
Component {
id: appMenuButton
FtButton {
buttonType: DecorationOptions.DecorationButtonApplicationMenu
size: root.buttonSize
fg: options.fontColor
bg: options.titleBarColor
}
}
}