Float as a KWin-managed window instead of a layer surface
The grab bar didn't move the window by mouse, and the corner grip never resized it (the bar's handler took over its drag). The bar and grip now start the window manager's move/resize, and Super+LMB/RMB work too. A KWin rule in kwinrulesrc keeps the window above and unfocused, off the taskbar, pager and switcher, and remembers its position. KWin now places the window and keeps it on screen. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -5,11 +5,10 @@ Open items only; fixed ones are in the history (`3576dd8`..`6ff86fb`).
|
|||||||
## Medium
|
## Medium
|
||||||
|
|
||||||
1. **Fake input can't type characters missing from the keymap.** `KWinFakeInput::typeText` (`src/backend/KWinFakeInput.cpp`) sends keysyms, and KWin has to find each one in the active keymap. Probably all of these do nothing when no text field is focused: long-press alternates (ť on a US keymap) and template symbols missing from the layout. Nothing tells the user it failed. A fix likely needs a temporary keymap (e.g. the virtual-keyboard protocol).
|
1. **Fake input can't type characters missing from the keymap.** `KWinFakeInput::typeText` (`src/backend/KWinFakeInput.cpp`) sends keysyms, and KWin has to find each one in the active keymap. Probably all of these do nothing when no text field is focused: long-press alternates (ť on a US keymap) and template symbols missing from the layout. Nothing tells the user it failed. A fix likely needs a temporary keymap (e.g. the virtual-keyboard protocol).
|
||||||
2. **The layer surface can't follow focus to another output.** `FloatingWindow` (`src/window/FloatingWindow.cpp`) now refits on screen changes, but the layer surface stays on `window->screen()`. Moving it needs a remap.
|
|
||||||
|
|
||||||
## Low
|
## Low
|
||||||
|
|
||||||
3. **Hold-keys caps lock is tracked locally.** `HardwareKeys::m_capsLock` toggles on its own, so a hardware Caps Lock (or a missed event) puts the shown state out of sync. The window never has keyboard focus, so it gets no modifier events to read the real state from.
|
2. **Hold-keys caps lock is tracked locally.** `HardwareKeys::m_capsLock` toggles on its own, so a hardware Caps Lock (or a missed event) puts the shown state out of sync. The window never has keyboard focus, so it gets no modifier events to read the real state from.
|
||||||
|
|
||||||
## Untested
|
## Untested
|
||||||
|
|
||||||
@@ -17,5 +16,4 @@ Fixed without automated tests; check on a live KWin session:
|
|||||||
|
|
||||||
- Focus moving from field to field and deactivate ordering (`KWinInputMethod`)
|
- Focus moving from field to field and deactivate ordering (`KWinInputMethod`)
|
||||||
- `org.kde.keyboard` appearing after startup or KWin restarting (`SystemLayouts`)
|
- `org.kde.keyboard` appearing after startup or KWin restarting (`SystemLayouts`)
|
||||||
- Refit after a resolution change or monitor unplug (`FloatingWindow`)
|
|
||||||
- Fake-input version checks on a KWin older than fake-input v6 (`KWinFakeInput`)
|
- Fake-input version checks on a KWin older than fake-input v6 (`KWinFakeInput`)
|
||||||
|
|||||||
+1
-1
@@ -6,8 +6,8 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
|||||||
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
|
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
|
||||||
|
|
||||||
find_package(Qt6 6.8 REQUIRED COMPONENTS Gui Quick Widgets Test DBus WaylandClient)
|
find_package(Qt6 6.8 REQUIRED COMPONENTS Gui Quick Widgets Test DBus WaylandClient)
|
||||||
find_package(LayerShellQt REQUIRED)
|
|
||||||
find_package(KF6WindowSystem REQUIRED)
|
find_package(KF6WindowSystem REQUIRED)
|
||||||
|
find_package(KF6Config REQUIRED)
|
||||||
find_package(PkgConfig REQUIRED)
|
find_package(PkgConfig REQUIRED)
|
||||||
pkg_check_modules(XKB REQUIRED IMPORTED_TARGET xkbcommon)
|
pkg_check_modules(XKB REQUIRED IMPORTED_TARGET xkbcommon)
|
||||||
pkg_check_modules(WAYLAND REQUIRED IMPORTED_TARGET wayland-client)
|
pkg_check_modules(WAYLAND REQUIRED IMPORTED_TARGET wayland-client)
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ cmake -B build -G Ninja && cmake --build build
|
|||||||
ctest --test-dir build
|
ctest --test-dir build
|
||||||
```
|
```
|
||||||
|
|
||||||
Needs Qt ≥ 6.8 (Quick, WaylandClient, DBus, Test), LayerShellQt, KF6 WindowSystem, xkbcommon, wayland-protocols.
|
Needs Qt ≥ 6.8 (Quick, WaylandClient, DBus, Test), KF6 WindowSystem, KF6 Config, xkbcommon, wayland-protocols.
|
||||||
|
|
||||||
## Run
|
## Run
|
||||||
|
|
||||||
@@ -73,7 +73,7 @@ input/ touch → PointerTracker gestures → KeyboardController rules
|
|||||||
engine/ key output → text ops (DirectEngine; prediction/swipe later)
|
engine/ key output → text ops (DirectEngine; prediction/swipe later)
|
||||||
backend/ text ops → compositor (KWin input-method-v1, or fake input with no text field); the compositor's keymap
|
backend/ text ops → compositor (KWin input-method-v1, or fake input with no text field); the compositor's keymap
|
||||||
layout/ template JSON + keymap letters → Layout → LayoutModel (geometry, hit testing)
|
layout/ template JSON + keymap letters → Layout → LayoutModel (geometry, hit testing)
|
||||||
window/ floating layer-shell placement, visibility, saved geometry
|
window/ floating window (KWin rule: on top, no focus), visibility, saved geometry
|
||||||
app/ D-Bus control and system layouts, dev hooks
|
app/ D-Bus control and system layouts, dev hooks
|
||||||
settings/ persisted user preferences
|
settings/ persisted user preferences
|
||||||
```
|
```
|
||||||
|
|||||||
+4
-10
@@ -1,11 +1,11 @@
|
|||||||
import QtQuick
|
import QtQuick
|
||||||
|
|
||||||
// The grab bar under the floating keyboard. Emits how far it was dragged;
|
// The grab bar under the floating keyboard. Moving the window is up to the
|
||||||
// moving the window is up to the owner.
|
// owner (the window manager's move, so it works for mouse and touch alike).
|
||||||
Item {
|
Item {
|
||||||
id: handle
|
id: handle
|
||||||
|
|
||||||
signal moved(real dx, real dy)
|
signal dragStarted()
|
||||||
|
|
||||||
implicitHeight: 22
|
implicitHeight: 22
|
||||||
|
|
||||||
@@ -19,12 +19,6 @@ Item {
|
|||||||
|
|
||||||
DragHandler {
|
DragHandler {
|
||||||
target: null
|
target: null
|
||||||
// The window follows the pointer, so the pointer stays near its press
|
onActiveChanged: if (active) handle.dragStarted()
|
||||||
// position in window coordinates: measure each step from there.
|
|
||||||
onCentroidChanged: {
|
|
||||||
if (active)
|
|
||||||
handle.moved(centroid.scenePosition.x - centroid.scenePressPosition.x,
|
|
||||||
centroid.scenePosition.y - centroid.scenePressPosition.y)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -63,7 +63,7 @@ Window {
|
|||||||
DragHandle {
|
DragHandle {
|
||||||
id: handle
|
id: handle
|
||||||
anchors { left: parent.left; right: parent.right; bottom: parent.bottom }
|
anchors { left: parent.left; right: parent.right; bottom: parent.bottom }
|
||||||
onMoved: (dx, dy) => root.placement?.moveBy(dx, dy)
|
onDragStarted: root.startSystemMove()
|
||||||
}
|
}
|
||||||
|
|
||||||
SettingsButton {
|
SettingsButton {
|
||||||
@@ -74,7 +74,7 @@ Window {
|
|||||||
|
|
||||||
ResizeHandle {
|
ResizeHandle {
|
||||||
anchors { right: parent.right; bottom: parent.bottom }
|
anchors { right: parent.right; bottom: parent.bottom }
|
||||||
onResized: (dw, dh) => root.placement?.resizeBy(dw, dh)
|
onDragStarted: root.startSystemResize(Qt.RightEdge | Qt.BottomEdge)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-13
@@ -1,11 +1,10 @@
|
|||||||
import QtQuick
|
import QtQuick
|
||||||
|
|
||||||
// Corner grip. The window grows from its top-left corner, so window
|
// Corner grip. Resizing is up to the owner (the window manager's resize).
|
||||||
// coordinates stay put while resizing: report each step's delta.
|
|
||||||
Item {
|
Item {
|
||||||
id: grip
|
id: grip
|
||||||
|
|
||||||
signal resized(real dw, real dh)
|
signal dragStarted()
|
||||||
|
|
||||||
implicitWidth: 28
|
implicitWidth: 28
|
||||||
implicitHeight: 28
|
implicitHeight: 28
|
||||||
@@ -28,17 +27,10 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
DragHandler {
|
DragHandler {
|
||||||
property point last
|
|
||||||
|
|
||||||
target: null
|
target: null
|
||||||
|
// The grab bar lies underneath and would take the drag over.
|
||||||
|
grabPermissions: PointerHandler.TakeOverForbidden
|
||||||
cursorShape: Qt.SizeFDiagCursor
|
cursorShape: Qt.SizeFDiagCursor
|
||||||
onActiveChanged: last = centroid.scenePressPosition
|
onActiveChanged: if (active) grip.dragStarted()
|
||||||
onCentroidChanged: {
|
|
||||||
if (!active)
|
|
||||||
return
|
|
||||||
const p = centroid.scenePosition
|
|
||||||
grip.resized(p.x - last.x, p.y - last.y)
|
|
||||||
last = p
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-1
@@ -85,6 +85,7 @@ qt_add_executable(sloposk
|
|||||||
backend/KWinFakeInput.h backend/KWinFakeInput.cpp
|
backend/KWinFakeInput.h backend/KWinFakeInput.cpp
|
||||||
backend/WaylandKeymap.h backend/WaylandKeymap.cpp
|
backend/WaylandKeymap.h backend/WaylandKeymap.cpp
|
||||||
window/FloatingWindow.h window/FloatingWindow.cpp
|
window/FloatingWindow.h window/FloatingWindow.cpp
|
||||||
|
window/KWinRule.h window/KWinRule.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
qt6_generate_wayland_protocol_client_sources(sloposk PRIVATE_CODE FILES
|
qt6_generate_wayland_protocol_client_sources(sloposk PRIVATE_CODE FILES
|
||||||
@@ -100,7 +101,7 @@ target_link_libraries(sloposk PRIVATE
|
|||||||
sloposk_coreplugin
|
sloposk_coreplugin
|
||||||
Qt6::DBus
|
Qt6::DBus
|
||||||
Qt6::WaylandClient
|
Qt6::WaylandClient
|
||||||
LayerShellQt::Interface
|
KF6::ConfigCore
|
||||||
KF6::WindowSystem
|
KF6::WindowSystem
|
||||||
PkgConfig::WAYLAND
|
PkgConfig::WAYLAND
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
#include "layout/LayoutModel.h"
|
#include "layout/LayoutModel.h"
|
||||||
#include "settings/Settings.h"
|
#include "settings/Settings.h"
|
||||||
#include "window/FloatingWindow.h"
|
#include "window/FloatingWindow.h"
|
||||||
|
#include "window/KWinRule.h"
|
||||||
#include "window/VisibilityController.h"
|
#include "window/VisibilityController.h"
|
||||||
#include "window/WindowSettings.h"
|
#include "window/WindowSettings.h"
|
||||||
|
|
||||||
@@ -158,6 +159,7 @@ int main(int argc, char *argv[])
|
|||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
installKWinRule(QGuiApplication::desktopFileName());
|
||||||
FloatingWindow placement(window);
|
FloatingWindow placement(window);
|
||||||
window->setProperty("placement", QVariant::fromValue(&placement));
|
window->setProperty("placement", QVariant::fromValue(&placement));
|
||||||
WindowSettings windowSettings;
|
WindowSettings windowSettings;
|
||||||
|
|||||||
@@ -1,68 +1,28 @@
|
|||||||
#include "FloatingWindow.h"
|
#include "FloatingWindow.h"
|
||||||
|
|
||||||
#include <KWindowEffects>
|
#include <KWindowEffects>
|
||||||
#include <LayerShellQt/Window>
|
|
||||||
#include <QPainterPath>
|
#include <QPainterPath>
|
||||||
#include <QGuiApplication>
|
|
||||||
#include <QScreen>
|
#include <QScreen>
|
||||||
#include <QWindow>
|
#include <QWindow>
|
||||||
|
|
||||||
namespace {
|
|
||||||
|
|
||||||
bool isWayland()
|
|
||||||
{
|
|
||||||
return QGuiApplication::platformName().startsWith(QLatin1String("wayland"));
|
|
||||||
}
|
|
||||||
|
|
||||||
constexpr int BottomGap = 24;
|
|
||||||
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
FloatingWindow::FloatingWindow(QWindow *window, QObject *parent)
|
FloatingWindow::FloatingWindow(QWindow *window, QObject *parent)
|
||||||
: QObject(parent)
|
: QObject(parent)
|
||||||
, m_window(window)
|
, m_window(window)
|
||||||
{
|
{
|
||||||
if (isWayland()) {
|
window->setFlags(Qt::Tool | Qt::FramelessWindowHint | Qt::WindowStaysOnTopHint | Qt::WindowDoesNotAcceptFocus);
|
||||||
m_layerSurface = LayerShellQt::Window::get(window);
|
for (const auto changed : {&QWindow::xChanged, &QWindow::yChanged, &QWindow::widthChanged, &QWindow::heightChanged})
|
||||||
m_layerSurface->setLayer(LayerShellQt::Window::LayerOverlay);
|
connect(window, changed, this, &FloatingWindow::geometryChanged);
|
||||||
m_layerSurface->setKeyboardInteractivity(LayerShellQt::Window::KeyboardInteractivityNone);
|
|
||||||
m_layerSurface->setAnchors({LayerShellQt::Window::AnchorTop | LayerShellQt::Window::AnchorLeft});
|
|
||||||
m_layerSurface->setExclusiveZone(-1);
|
|
||||||
m_layerSurface->setScope(QStringLiteral("sloposk-keyboard"));
|
|
||||||
m_layerSurface->setActivateOnShow(false);
|
|
||||||
} else {
|
|
||||||
window->setFlags(Qt::Tool | Qt::FramelessWindowHint | Qt::WindowStaysOnTopHint
|
|
||||||
| Qt::WindowDoesNotAcceptFocus);
|
|
||||||
}
|
|
||||||
m_position = defaultPosition();
|
|
||||||
apply();
|
|
||||||
followScreen(window->screen());
|
|
||||||
connect(window, &QWindow::screenChanged, this, &FloatingWindow::followScreen);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Re-fits the window (not saved: the stored geometry is the user's choice).
|
|
||||||
void FloatingWindow::followScreen(QScreen *screen)
|
|
||||||
{
|
|
||||||
if (m_screen)
|
|
||||||
disconnect(m_screen, nullptr, this, nullptr);
|
|
||||||
m_screen = screen;
|
|
||||||
if (!m_screen)
|
|
||||||
return;
|
|
||||||
connect(m_screen, &QScreen::geometryChanged, this, [this] { setGeometry(geometry()); });
|
|
||||||
setGeometry(geometry());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
QRect FloatingWindow::geometry() const
|
QRect FloatingWindow::geometry() const
|
||||||
{
|
{
|
||||||
return {m_position, m_window->size()};
|
return m_window->geometry().translated(-screenArea().topLeft());
|
||||||
}
|
}
|
||||||
|
|
||||||
void FloatingWindow::setGeometry(const QRect &geometry)
|
void FloatingWindow::setGeometry(const QRect &geometry)
|
||||||
{
|
{
|
||||||
const QSize size = geometry.size().expandedTo(m_window->minimumSize()).boundedTo(screenArea().size());
|
const QSize size = geometry.size().expandedTo(m_window->minimumSize()).boundedTo(screenArea().size());
|
||||||
m_window->resize(size);
|
m_window->setGeometry({screenArea().topLeft() + geometry.topLeft(), size});
|
||||||
m_position = clamped(geometry.topLeft(), size);
|
|
||||||
apply();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
QString FloatingWindow::screenName() const
|
QString FloatingWindow::screenName() const
|
||||||
@@ -70,26 +30,12 @@ QString FloatingWindow::screenName() const
|
|||||||
return m_window->screen()->name();
|
return m_window->screen()->name();
|
||||||
}
|
}
|
||||||
|
|
||||||
void FloatingWindow::moveBy(qreal dx, qreal dy)
|
|
||||||
{
|
|
||||||
const QPoint position = clamped(m_position + QPoint(qRound(dx), qRound(dy)), m_window->size());
|
|
||||||
if (position == m_position)
|
|
||||||
return;
|
|
||||||
m_position = position;
|
|
||||||
apply();
|
|
||||||
Q_EMIT geometryChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
void FloatingWindow::resizeBy(qreal dw, qreal dh)
|
void FloatingWindow::resizeBy(qreal dw, qreal dh)
|
||||||
{
|
{
|
||||||
const QSize maximum = screenArea().size() - QSize(m_position.x(), m_position.y());
|
|
||||||
const QSize size = (m_window->size() + QSize(qRound(dw), qRound(dh)))
|
const QSize size = (m_window->size() + QSize(qRound(dw), qRound(dh)))
|
||||||
.expandedTo(m_window->minimumSize())
|
.expandedTo(m_window->minimumSize())
|
||||||
.boundedTo(maximum);
|
.boundedTo(screenArea().size());
|
||||||
if (size == m_window->size())
|
|
||||||
return;
|
|
||||||
m_window->resize(size);
|
m_window->resize(size);
|
||||||
Q_EMIT geometryChanged();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void FloatingWindow::setInputRegion(const QRectF ®ion)
|
void FloatingWindow::setInputRegion(const QRectF ®ion)
|
||||||
@@ -122,26 +68,3 @@ QRect FloatingWindow::screenArea() const
|
|||||||
{
|
{
|
||||||
return m_window->screen()->geometry();
|
return m_window->screen()->geometry();
|
||||||
}
|
}
|
||||||
|
|
||||||
QPoint FloatingWindow::clamped(QPoint position, QSize size) const
|
|
||||||
{
|
|
||||||
const QRect area = screenArea();
|
|
||||||
const int maxX = qMax(0, area.width() - size.width());
|
|
||||||
const int maxY = qMax(0, area.height() - size.height());
|
|
||||||
return {qBound(0, position.x(), maxX), qBound(0, position.y(), maxY)};
|
|
||||||
}
|
|
||||||
|
|
||||||
QPoint FloatingWindow::defaultPosition() const
|
|
||||||
{
|
|
||||||
const QRect area = screenArea();
|
|
||||||
return clamped({(area.width() - m_window->width()) / 2, area.height() - m_window->height() - BottomGap},
|
|
||||||
m_window->size());
|
|
||||||
}
|
|
||||||
|
|
||||||
void FloatingWindow::apply()
|
|
||||||
{
|
|
||||||
if (m_layerSurface)
|
|
||||||
m_layerSurface->setMargins({m_position.x(), m_position.y(), 0, 0});
|
|
||||||
else
|
|
||||||
m_window->setPosition(screenArea().topLeft() + m_position);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -4,15 +4,12 @@
|
|||||||
#include <QRect>
|
#include <QRect>
|
||||||
#include <QRectF>
|
#include <QRectF>
|
||||||
|
|
||||||
class QScreen;
|
|
||||||
class QWindow;
|
class QWindow;
|
||||||
namespace LayerShellQt { class Window; }
|
|
||||||
|
|
||||||
// Places a keyboard window as a free-floating overlay that never takes focus.
|
// Makes a keyboard window a frameless, always-on-top window that never takes
|
||||||
// On Wayland: a layer-shell surface anchored top-left, positioned via margins.
|
// focus. The window manager places it and moves it (Super+drag included);
|
||||||
// On X11: a frameless, non-focusable, always-on-top tool window.
|
// on KWin Wayland that relies on the rule from installKWinRule().
|
||||||
// Geometry is relative to the window's screen and always kept on it, also
|
// Geometry is relative to the window's screen.
|
||||||
// when the screen's resolution changes or the window moves to another one.
|
|
||||||
class FloatingWindow : public QObject
|
class FloatingWindow : public QObject
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
@@ -22,10 +19,10 @@ public:
|
|||||||
explicit FloatingWindow(QWindow *window, QObject *parent = nullptr);
|
explicit FloatingWindow(QWindow *window, QObject *parent = nullptr);
|
||||||
|
|
||||||
QRect geometry() const;
|
QRect geometry() const;
|
||||||
|
// Wayland clients can't place themselves: only the size applies there.
|
||||||
void setGeometry(const QRect &geometry);
|
void setGeometry(const QRect &geometry);
|
||||||
QString screenName() const;
|
QString screenName() const;
|
||||||
|
|
||||||
Q_INVOKABLE void moveBy(qreal dx, qreal dy);
|
|
||||||
// Grows from the top-left corner; respects the window's minimum size.
|
// Grows from the top-left corner; respects the window's minimum size.
|
||||||
Q_INVOKABLE void resizeBy(qreal dw, qreal dh);
|
Q_INVOKABLE void resizeBy(qreal dw, qreal dh);
|
||||||
// Only this part of the window takes input; the rest passes through.
|
// Only this part of the window takes input; the rest passes through.
|
||||||
@@ -36,18 +33,10 @@ public:
|
|||||||
Q_INVOKABLE void hide();
|
Q_INVOKABLE void hide();
|
||||||
|
|
||||||
Q_SIGNALS:
|
Q_SIGNALS:
|
||||||
// Only for user-initiated changes (move/resize), not for restores.
|
|
||||||
void geometryChanged();
|
void geometryChanged();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
QRect screenArea() const;
|
QRect screenArea() const;
|
||||||
QPoint clamped(QPoint position, QSize size) const;
|
|
||||||
QPoint defaultPosition() const;
|
|
||||||
void apply();
|
|
||||||
void followScreen(QScreen *screen);
|
|
||||||
|
|
||||||
QWindow *m_window;
|
QWindow *m_window;
|
||||||
LayerShellQt::Window *m_layerSurface = nullptr;
|
|
||||||
QPoint m_position;
|
|
||||||
QScreen *m_screen = nullptr;
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
#include "KWinRule.h"
|
||||||
|
|
||||||
|
#include <KConfig>
|
||||||
|
#include <KConfigGroup>
|
||||||
|
#include <QDBusConnection>
|
||||||
|
#include <QDBusMessage>
|
||||||
|
#include <QDBusPendingCall>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
const QString RuleName = QStringLiteral("sloposk");
|
||||||
|
|
||||||
|
// KWin rule policies.
|
||||||
|
constexpr auto Force = "2";
|
||||||
|
constexpr auto Remember = "4";
|
||||||
|
|
||||||
|
bool writeEntry(KConfigGroup &group, const QString &key, const QString &value)
|
||||||
|
{
|
||||||
|
if (group.readEntry(key, QString()) == value)
|
||||||
|
return false;
|
||||||
|
group.writeEntry(key, value);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
void installKWinRule(const QString &appId)
|
||||||
|
{
|
||||||
|
KConfig config(QStringLiteral("kwinrulesrc"), KConfig::NoGlobals);
|
||||||
|
KConfigGroup rule = config.group(RuleName);
|
||||||
|
const QList<std::pair<QString, QString>> entries = {
|
||||||
|
{QStringLiteral("Description"), QStringLiteral("sloposk keyboard: on top, never focused, remembered position")},
|
||||||
|
{QStringLiteral("wmclass"), appId},
|
||||||
|
{QStringLiteral("wmclassmatch"), QStringLiteral("1")}, // exact
|
||||||
|
{QStringLiteral("wmclasscomplete"), QStringLiteral("false")},
|
||||||
|
{QStringLiteral("above"), QStringLiteral("true")},
|
||||||
|
{QStringLiteral("aboverule"), QString::fromLatin1(Force)},
|
||||||
|
{QStringLiteral("acceptfocus"), QStringLiteral("false")},
|
||||||
|
{QStringLiteral("acceptfocusrule"), QString::fromLatin1(Force)},
|
||||||
|
{QStringLiteral("skiptaskbar"), QStringLiteral("true")},
|
||||||
|
{QStringLiteral("skiptaskbarrule"), QString::fromLatin1(Force)},
|
||||||
|
{QStringLiteral("skippager"), QStringLiteral("true")},
|
||||||
|
{QStringLiteral("skippagerrule"), QString::fromLatin1(Force)},
|
||||||
|
{QStringLiteral("skipswitcher"), QStringLiteral("true")},
|
||||||
|
{QStringLiteral("skipswitcherrule"), QString::fromLatin1(Force)},
|
||||||
|
{QStringLiteral("positionrule"), QString::fromLatin1(Remember)},
|
||||||
|
};
|
||||||
|
bool changed = false;
|
||||||
|
for (const auto &[key, value] : entries)
|
||||||
|
changed |= writeEntry(rule, key, value);
|
||||||
|
|
||||||
|
KConfigGroup general = config.group(QStringLiteral("General"));
|
||||||
|
QStringList rules = general.readEntry("rules", QStringList());
|
||||||
|
if (!rules.contains(RuleName)) {
|
||||||
|
rules.append(RuleName);
|
||||||
|
general.writeEntry("rules", rules);
|
||||||
|
general.writeEntry("count", rules.size());
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
if (!changed)
|
||||||
|
return;
|
||||||
|
config.sync();
|
||||||
|
QDBusConnection::sessionBus().asyncCall(QDBusMessage::createMethodCall(
|
||||||
|
QStringLiteral("org.kde.KWin"), QStringLiteral("/KWin"), QStringLiteral("org.kde.KWin"),
|
||||||
|
QStringLiteral("reconfigure")));
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QString>
|
||||||
|
|
||||||
|
// Adds (or repairs) a KWin window rule for the keyboard's app id in
|
||||||
|
// kwinrulesrc: keep above, never take focus, skip taskbar/pager/switcher,
|
||||||
|
// remember the position. Wayland gives clients no other way to stay
|
||||||
|
// unfocused. Reloads KWin's rules when anything changed.
|
||||||
|
void installKWinRule(const QString &appId);
|
||||||
Reference in New Issue
Block a user