mirror of
https://github.com/DeeJanuz/frametop.git
synced 2026-10-06 01:00:06 +02:00
Merge float-titlebar into experimental
This commit is contained in:
commit
1aa88641ec
9 files changed
+510
-6
No files matched your search
@@ -54,7 +54,7 @@ If you work in the desktop for long stretches, or leave the headset on a stand,
|
||||
| Set a screen to On your head (Frametop Display Settings, Visibility & pins) | Pins it to your head where it is, like a HUD. Grab its bar to move it; it stays on your head |
|
||||
| Save current arrangement… (Frametop Display Settings, Layout) | Saves where the screens are, with their sizes and pins, under a name. Pick a saved layout under Arrangement and press Arrange now to switch to it |
|
||||
| Meta+Shift+R in the desktop | Puts the screens back in their layout (also in the menu as Reset Screen Layout, and mappable to a mouse button) |
|
||||
| Meta+Shift+F over a desktop window | Floats that window in VR as a panel of its own, or puts it back on its screen if it floats. It acts on the window under the pointer, or the active one if the pointer is over the wallpaper. Rebind it, or map it to a mouse or controller button, in Frametop Input Settings (Keyboard page, or Buttons and Controllers as Float window in VR). Float in VR is also in every window's menu (Alt+F3) |
|
||||
| Meta+Shift+F over a desktop window | Floats that window in VR as a panel of its own, or puts it back on its screen if it floats. It acts on the window under the pointer, or the active one if the pointer is over the wallpaper. Rebind it, or map it to a mouse or controller button, in Frametop Input Settings (Keyboard page, or Buttons and Controllers as Float window in VR). Float in VR is also in every window's menu (Alt+F3), and the button left of Close in a window's title bar does the same (apps that draw their own title bar, like Chromium and Electron apps, don't have it) |
|
||||
| Meta+Shift+H in the desktop | Hides or shows all screens (also in the menu as Hide/Show Screens, and mappable). The Visibility & pins tab of Frametop Display Settings can instead show them only with the dashboard open, or while you look at your wrist |
|
||||
| Leave the headset on a stand | Its displays turn off once it has gone unused for the time set in Frametop Display Settings → Power, even if the stand covers its proximity sensor. Pick it up, or use any mouse, keyboard, or button, and they come back on |
|
||||
| Play a VR game | The screens hide and your controllers stay in the game. Open the SteamVR dashboard, or press Meta+Shift+H, to see and use them. To keep them visible over games, change During VR games on the Visibility & pins tab; the controllers still stay in the game, and you use the screens with the mouse or the dashboard |
|
||||
@@ -127,6 +127,7 @@ A Plasma session runs nested inside ft-screens (`screens/`), a small Wayland com
|
||||
| `session/` | The desktop session script and its config example. |
|
||||
| `layout/` | ft-layout: where the screens float, and their sizes. |
|
||||
| `float/` | Floating windows: ft-floatd and the KWin script that float a desktop window in VR. |
|
||||
| `decoration/` | The desktop's window decoration: Breeze's look plus the float button. |
|
||||
| `input/` | The input relay (Bluetooth mice and keyboards, button maps). |
|
||||
| `pointer/` | The 3D mouse: SteamVR driver, helper service, and a probe tool. |
|
||||
| `power/` | ft-powerd: turns the displays off while the headset isn't used. |
|
||||
|
||||
Executable
+40
@@ -0,0 +1,40 @@
|
||||
#!/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 D-Bus: from its Plasma shell, which uses Frametop's config folder (KWin's own
|
||||
# environment isn't readable: it runs with extra capabilities).
|
||||
bus=
|
||||
for pid in $(pgrep -x plasmashell); 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
|
||||
# Under a new name each time: KWin keeps a decoration's QML (even a broken one) by name
|
||||
# until it restarts. The next desktop start goes back to the plain name.
|
||||
decos=${XDG_DATA_HOME:-$HOME/.local/share}/kwin/decorations
|
||||
rm -rf "$decos/${deco}"_try*
|
||||
deco=${deco}_try$(date +%s)
|
||||
mkdir -p "$decos"
|
||||
cp -r "$here" "$decos/$deco"
|
||||
rm -f "$decos/$deco/apply.sh"
|
||||
sed -i "s/\"Id\": \"[^\"]*\"/\"Id\": \"$deco\"/" "$decos/$deco/metadata.json"
|
||||
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,123 @@
|
||||
/*
|
||||
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);
|
||||
// Like Breeze: no On All Desktops button with only one virtual desktop.
|
||||
if (buttonType === DecorationOptions.DecorationButtonOnAllDesktops)
|
||||
visible = Qt.binding(() => decorationSettings.onAllDesktopsAvailable);
|
||||
}
|
||||
}
|
||||
@@ -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 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,8 @@ Built so far (2026-09-29; the KWin side tested on the headless test desktop, `sc
|
||||
- ft-screens: a panel per spare output (`frametop.float.N`) with the crop, density, popups and dialogs as small panels over it, title-bar carrying, the corner tab resizing the window, and dock and close buttons. `--spares`, and the commands `float`, `unfloat`, `pose`, `sub`, `minimized`, `carry`.
|
||||
- The session adds `FLOAT_SLOTS` spares and starts ft-floatd from the desktop's autostart; ft-layout leaves the spares alone.
|
||||
- The 3D mouse's drag lock crosses onto other Frametop panels (not while carrying one).
|
||||
- The float key (2026-09-30): the input relay's `float_toggle` (Meta+Shift+F by default) and `dock_all`, through `float pointer` and `dock all` on ft-floatd. Tested on the headless desktop.
|
||||
- The title bar button (2026-09-30, branch `float-titlebar`): `decoration/` (Frametop's QML window decoration, installed by the session script; `decoration/apply.sh` switches the running desktop to it or back to Breeze), and keep-below as the floating flag in the script. Tried on the live desktop: the button floats Dolphin and docks it again, the float key and `dock all` keep the flag in step, and maximized windows look right.
|
||||
|
||||
Not built yet: phase 2 (the ghost, tear-off by dragging, push-flush docking), phase 3 (launching floating, Launch as Standalone, remembered placement), and phase 4.
|
||||
|
||||
|
||||
+35
-4
@@ -12,6 +12,7 @@ const SERVICE = "org.frametop.Float", PATH = "/Float", IFACE = "org.frametop.Flo
|
||||
let screens = 0; // outputs WL-0 .. WL-<screens - 1> are screens; the rest are spares
|
||||
let polling = false;
|
||||
const watched = {}; // id -> true once its signals are connected
|
||||
let marking = false; // the script itself is setting keep-below (see mark)
|
||||
|
||||
function send(ev) {
|
||||
callDBus(SERVICE, PATH, IFACE, "Event", JSON.stringify(ev));
|
||||
@@ -64,7 +65,8 @@ function watch(w) {
|
||||
watched[id] = true;
|
||||
const onSpare = () => isSpare(w.output);
|
||||
w.frameGeometryChanged.connect(() => { if (onSpare()) report("geometry", w); });
|
||||
w.outputChanged.connect(() => report("output", w));
|
||||
w.outputChanged.connect(() => { report("output", w); mark(w); });
|
||||
w.keepBelowChanged.connect(() => keepBelowChanged(w));
|
||||
w.interactiveMoveResizeStarted.connect(() => {
|
||||
if (onSpare()) send({ev: "move-start", id: id, move: w.move, resize: w.resize, frame: rect(w.frameGeometry)});
|
||||
});
|
||||
@@ -90,6 +92,33 @@ workspace.windowActivated.connect(w => {
|
||||
});
|
||||
workspace.windowList().forEach(watch);
|
||||
|
||||
// Keep-below means "floating" in the Frametop desktop. The title bar's float button (Frametop's
|
||||
// window decoration, decoration/) is the Keep Below button, so setting the flag on a window on
|
||||
// the screens floats it, and clearing it on a floating one docks it. The script keeps the flag
|
||||
// set on every floating window, its dialogs included, and cleared everywhere else, however the
|
||||
// window got there. Kept below, a window alone on its own output only has the wallpaper under it.
|
||||
function marked(w) {
|
||||
return w.managed && !w.deleted && !w.specialWindow && !w.popupWindow;
|
||||
}
|
||||
function topOf(w) {
|
||||
let top = w;
|
||||
for (let n = 0; top.transientFor && n < 10; ++n) top = top.transientFor;
|
||||
return top;
|
||||
}
|
||||
function mark(w) {
|
||||
if (screens === 0 || !marked(w)) return;
|
||||
const want = isSpare(w.output);
|
||||
if (w.keepBelow === want) return;
|
||||
marking = true;
|
||||
w.keepBelow = want;
|
||||
marking = false;
|
||||
}
|
||||
function keepBelowChanged(w) {
|
||||
if (marking || screens === 0 || !marked(w)) return;
|
||||
const top = topOf(w);
|
||||
if (w.keepBelow !== isSpare(top.output)) requestFloat(top);
|
||||
}
|
||||
|
||||
function requestFloat(w) {
|
||||
if (!w || !w.normalWindow || w.popupWindow) return;
|
||||
report(isSpare(w.output) ? "dock-request" : "float-request", w);
|
||||
@@ -118,8 +147,7 @@ function underPointer() {
|
||||
if (w.deleted || w.minimized || w.hidden || !w.managed) continue;
|
||||
const g = w.frameGeometry;
|
||||
if (p.x < g.x || p.y < g.y || p.x >= g.x + g.width || p.y >= g.y + g.height) continue;
|
||||
let top = w;
|
||||
for (let n = 0; top.transientFor && n < 10; ++n) top = top.transientFor;
|
||||
const top = topOf(w);
|
||||
return top.normalWindow && !top.popupWindow ? top : null;
|
||||
}
|
||||
return null;
|
||||
@@ -130,7 +158,10 @@ function run(c) {
|
||||
switch (c.cmd) {
|
||||
case "config":
|
||||
screens = c.screens;
|
||||
workspace.windowList().forEach(w => report("window", w));
|
||||
workspace.windowList().forEach(w => { report("window", w); mark(w); });
|
||||
break;
|
||||
case "mark": // after a float that didn't happen: keep-below back as it was
|
||||
if (w) mark(w);
|
||||
break;
|
||||
case "place": { // onto an output, at a frame rectangle (logical, global)
|
||||
if (!w) break;
|
||||
|
||||
+5
-1
@@ -197,7 +197,10 @@ class Daemon:
|
||||
sid = int(kwin.loadScript(os.path.join(HERE, "frametop-float.js"), SCRIPT, signature="ss"))
|
||||
if sid < 0:
|
||||
raise RuntimeError("KWin didn't load the script")
|
||||
bus.get_object("org.kde.KWin", f"/Scripting/Script{sid}").run(dbus_interface="org.kde.kwin.Script")
|
||||
# Scripting.start runs every loaded script that isn't running. Not /Scripting/Script<id>'s
|
||||
# run: a reloaded script gets the old one's id while the old one is still being deleted,
|
||||
# so that path is still the old script's, and the new one would never run.
|
||||
kwin.start()
|
||||
log(f"script loaded ({sid})")
|
||||
# Older scripts registered the float key with KWin; the input relay owns it now. Drop
|
||||
# that shortcut, so System Settings doesn't list one that does nothing.
|
||||
@@ -355,6 +358,7 @@ class Daemon:
|
||||
return
|
||||
slot = self.free_slot()
|
||||
if slot is None:
|
||||
self.command(cmd="mark", id=wid) # the title bar button set keep-below for nothing
|
||||
self.notify(f"All {len(self.slots)} floating windows are in use. Put one back on the desktop "
|
||||
"to float another.")
|
||||
return
|
||||
|
||||
@@ -196,4 +196,23 @@ else
|
||||
rm -f "$autostart"
|
||||
fi
|
||||
|
||||
# With floating windows, the session's windows get Frametop's own decoration: Breeze's look
|
||||
# plus a float button left of Close (decoration/, a QML decoration KWin's Aurorae engine
|
||||
# loads; docs/floating-windows.md). It's copied, not linked: KPackage doesn't list linked
|
||||
# packages. Desktop Mode keeps its own kwinrc, so it keeps Breeze.
|
||||
deco=kwin4_decoration_qml_frametop
|
||||
deco_dir=${XDG_DATA_HOME:-$HOME/.local/share}/kwin/decorations/$deco
|
||||
kwinrc=$XDG_CONFIG_HOME/kwinrc
|
||||
if [ "$float_slots" -gt 0 ]; then
|
||||
rm -rf "$deco_dir" "$deco_dir"_try* # (decoration/apply.sh's copies)
|
||||
mkdir -p "$(dirname "$deco_dir")"
|
||||
cp -r "$here/../decoration" "$deco_dir"
|
||||
rm -f "$deco_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"
|
||||
elif [ "$(kreadconfig6 --file "$kwinrc" --group org.kde.kdecoration2 --key theme)" = "$deco" ]; then
|
||||
kwriteconfig6 --file "$kwinrc" --group org.kde.kdecoration2 --key library --delete
|
||||
kwriteconfig6 --file "$kwinrc" --group org.kde.kdecoration2 --key theme --delete
|
||||
fi
|
||||
|
||||
dbus-run-session startplasma-wayland
|
||||
Reference in new issue
Block a user