mirror of
https://github.com/DeeJanuz/frametop.git
synced 2026-10-06 01:00:06 +02:00
A float button in every window's title bar
Frametop's own window decoration (decoration/), a QML decoration for KWin's Aurorae engine drawn like Breeze, puts a float button left of Close. It's the Keep Below button with its own glyph: the KWin script floats a window when keep-below is set and docks it when it's cleared, and keeps the flag set on every floating window, so the button shows "back to the desktop" there. The session script installs the decoration for the Frametop desktop only; decoration/apply.sh switches a running desktop to it or back to Breeze. Not yet tried in a running KWin. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
86c89f90af
commit
f32187824a
9 files changed
+498
-5
No files matched your search
Executable
+35
@@ -0,0 +1,35 @@
|
||||
#!/usr/bin/env bash
|
||||
# Switch the running Frametop desktop's window decoration without restarting it: Frametop's
|
||||
# own (this folder, with the float button) or back to Breeze. The session script does the
|
||||
# same at every desktop start (session/frametop-session.sh), so this is for trying changes.
|
||||
# decoration/apply.sh install this folder's copy and use it
|
||||
# decoration/apply.sh --off back to Breeze (until the next desktop start)
|
||||
set -euo pipefail
|
||||
here=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
||||
deco=kwin4_decoration_qml_frametop
|
||||
cfg=$HOME/.config/frametop
|
||||
kwinrc=$cfg/kwinrc
|
||||
|
||||
# The desktop's KWin: the one using Frametop's config folder. Its D-Bus is the session's.
|
||||
bus=
|
||||
for pid in $(pgrep -x kwin_wayland); do
|
||||
env=$(tr '\0' '\n' < "/proc/$pid/environ" 2>/dev/null) || continue
|
||||
grep -qx "XDG_CONFIG_HOME=$cfg" <<<"$env" || continue
|
||||
bus=$(sed -n 's/^DBUS_SESSION_BUS_ADDRESS=//p' <<<"$env")
|
||||
done
|
||||
[ -n "$bus" ] || { echo "the Frametop desktop isn't running" >&2; exit 1; }
|
||||
|
||||
if [ "${1:-}" = --off ]; then
|
||||
kwriteconfig6 --file "$kwinrc" --group org.kde.kdecoration2 --key library --delete
|
||||
kwriteconfig6 --file "$kwinrc" --group org.kde.kdecoration2 --key theme --delete
|
||||
else
|
||||
dir=${XDG_DATA_HOME:-$HOME/.local/share}/kwin/decorations/$deco
|
||||
rm -rf "$dir"
|
||||
mkdir -p "$(dirname "$dir")"
|
||||
cp -r "$here" "$dir"
|
||||
rm -f "$dir/apply.sh"
|
||||
kwriteconfig6 --file "$kwinrc" --group org.kde.kdecoration2 --key library org.kde.kwin.aurorae
|
||||
kwriteconfig6 --file "$kwinrc" --group org.kde.kdecoration2 --key theme "$deco"
|
||||
fi
|
||||
DBUS_SESSION_BUS_ADDRESS=$bus gdbus call --session -d org.kde.KWin -o /KWin -m org.kde.KWin.reconfigure >/dev/null
|
||||
echo "decoration: $(kreadconfig6 --file "$kwinrc" --group org.kde.kdecoration2 --key theme --default Breeze)"
|
||||
@@ -0,0 +1,120 @@
|
||||
/*
|
||||
A title bar button drawn the way Breeze draws its own: a glyph in the title's colour,
|
||||
a soft circle behind it on hover, and a red circle for Close.
|
||||
|
||||
The float button is KWin's Keep Below button with a glyph of its own: Frametop's KWin
|
||||
script (float/frametop-float.js) floats a window when keep-below is set on it and docks
|
||||
it when it's cleared, and keeps the flag set on every floating window, so "toggled" here
|
||||
means "floating" and the glyph turns into "back to the desktop".
|
||||
|
||||
SPDX-License-Identifier: GPL-2.0-or-later
|
||||
*/
|
||||
import QtQuick
|
||||
import QtQuick.Shapes
|
||||
import org.kde.kwin.decoration
|
||||
|
||||
DecorationButton {
|
||||
id: button
|
||||
property real size: 20
|
||||
property color fg: "white"
|
||||
property color bg: "black"
|
||||
readonly property bool isClose: buttonType === DecorationOptions.DecorationButtonClose
|
||||
readonly property bool isFloat: buttonType === DecorationOptions.DecorationButtonKeepBelow
|
||||
// The glyph, as polylines in an 18 x 18 box (Breeze's own sizes).
|
||||
readonly property var glyph: {
|
||||
switch (buttonType) {
|
||||
case DecorationOptions.DecorationButtonClose:
|
||||
return [[[5, 5], [13, 13]], [[13, 5], [5, 13]]];
|
||||
case DecorationOptions.DecorationButtonMaximizeRestore:
|
||||
return decoration.client.maximized ? [[[4.5, 9], [9, 4.5], [13.5, 9], [9, 13.5], [4.5, 9]]]
|
||||
: [[[4, 11.5], [9, 6.5], [14, 11.5]]];
|
||||
case DecorationOptions.DecorationButtonMinimize:
|
||||
return [[[4, 7], [9, 12], [14, 7]]];
|
||||
case DecorationOptions.DecorationButtonKeepAbove:
|
||||
return [[[4, 9], [9, 4], [14, 9]], [[4, 14], [9, 9], [14, 14]]];
|
||||
case DecorationOptions.DecorationButtonShade:
|
||||
return [[[4, 5], [14, 5]], [[4, 13], [9, 8], [14, 13]]];
|
||||
case DecorationOptions.DecorationButtonApplicationMenu:
|
||||
return [[[3.5, 5], [14.5, 5]], [[3.5, 9], [14.5, 9]], [[3.5, 13], [14.5, 13]]];
|
||||
case DecorationOptions.DecorationButtonKeepBelow:
|
||||
// A box with an arrow leaving it (float in VR), or coming back into it (put it back).
|
||||
return button.toggled
|
||||
? [[[8, 3.5], [3.5, 3.5], [3.5, 14.5], [14.5, 14.5], [14.5, 10]], [[14.5, 3.5], [8.5, 9.5]],
|
||||
[[8.5, 5], [8.5, 9.5], [13, 9.5]]]
|
||||
: [[[8, 3.5], [3.5, 3.5], [3.5, 14.5], [14.5, 14.5], [14.5, 10]], [[8.5, 9.5], [14.5, 3.5]],
|
||||
[[10, 3.5], [14.5, 3.5], [14.5, 8]]];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
readonly property string tip: isFloat ? (toggled ? "Back to Desktop" : "Float in VR") : ""
|
||||
|
||||
width: size
|
||||
height: size
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: width / 2
|
||||
visible: button.hovered || button.pressed || (button.toggled && !button.isFloat)
|
||||
color: button.isClose ? (button.pressed ? "#c0392b" : "#da4453")
|
||||
: Qt.rgba(button.fg.r, button.fg.g, button.fg.b,
|
||||
button.pressed ? 0.35 : button.hovered ? 0.2 : 0.12)
|
||||
}
|
||||
|
||||
// On all desktops: a dot, filled while the window is on all of them. Help: a question mark.
|
||||
Rectangle {
|
||||
visible: button.buttonType === DecorationOptions.DecorationButtonOnAllDesktops
|
||||
anchors.centerIn: parent
|
||||
width: button.size * 0.3
|
||||
height: width
|
||||
radius: width / 2
|
||||
color: button.toggled ? button.fg : "transparent"
|
||||
border.width: Math.max(1, button.size / 18)
|
||||
border.color: button.fg
|
||||
}
|
||||
Text {
|
||||
visible: button.buttonType === DecorationOptions.DecorationButtonQuickHelp
|
||||
anchors.centerIn: parent
|
||||
text: "?"
|
||||
color: button.fg
|
||||
font.pixelSize: button.size * 0.65
|
||||
font.bold: true
|
||||
}
|
||||
|
||||
// Up to three strokes per glyph.
|
||||
function stroke(i) {
|
||||
const k = button.size / 18;
|
||||
return i < glyph.length ? glyph[i].map(p => Qt.point(p[0] * k, p[1] * k)) : [];
|
||||
}
|
||||
readonly property color strokeColor: isClose && (hovered || pressed) ? "white" : fg
|
||||
readonly property real strokeWidth: Math.max(1, size / 18 * 1.1)
|
||||
// (An inline component can't see the ids around it, so everything comes in as properties.)
|
||||
component Stroke: ShapePath {
|
||||
property var points: []
|
||||
fillColor: "transparent"
|
||||
capStyle: ShapePath.RoundCap
|
||||
joinStyle: ShapePath.RoundJoin
|
||||
PathPolyline { path: points }
|
||||
}
|
||||
Shape {
|
||||
anchors.fill: parent
|
||||
preferredRendererType: Shape.CurveRenderer
|
||||
Stroke { points: button.stroke(0); strokeColor: button.strokeColor; strokeWidth: button.strokeWidth }
|
||||
Stroke { points: button.stroke(1); strokeColor: button.strokeColor; strokeWidth: button.strokeWidth }
|
||||
Stroke { points: button.stroke(2); strokeColor: button.strokeColor; strokeWidth: button.strokeWidth }
|
||||
}
|
||||
|
||||
onHoveredChanged: {
|
||||
if (!tip || typeof decoration.requestShowToolTip !== "function")
|
||||
return;
|
||||
if (hovered)
|
||||
decoration.requestShowToolTip(tip);
|
||||
else
|
||||
decoration.requestHideToolTip();
|
||||
}
|
||||
Component.onCompleted: {
|
||||
if (buttonType === DecorationOptions.DecorationButtonQuickHelp)
|
||||
visible = Qt.binding(() => decoration.client.providesContextHelp);
|
||||
if (buttonType === DecorationOptions.DecorationButtonApplicationMenu)
|
||||
visible = Qt.binding(() => decoration.client.hasApplicationMenu);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,270 @@
|
||||
/*
|
||||
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 bottom: 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 = bottom;
|
||||
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 = bottom ? 0 : 4;
|
||||
}
|
||||
onTitleHeightChanged: applyBorders()
|
||||
onSideChanged: applyBorders()
|
||||
onBottomChanged: 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"KPackageStructure": "KWin/Decoration",
|
||||
"KPlugin": {
|
||||
"Authors": [
|
||||
{
|
||||
"Name": "Frametop"
|
||||
}
|
||||
],
|
||||
"Description": "Breeze-style window decoration with a button that floats the window in VR (Frametop desktop)",
|
||||
"Id": "kwin4_decoration_qml_frametop",
|
||||
"License": "GPL",
|
||||
"Name": "Frametop"
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user