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:
+2
-1
@@ -85,6 +85,7 @@ qt_add_executable(sloposk
|
||||
backend/KWinFakeInput.h backend/KWinFakeInput.cpp
|
||||
backend/WaylandKeymap.h backend/WaylandKeymap.cpp
|
||||
window/FloatingWindow.h window/FloatingWindow.cpp
|
||||
window/KWinRule.h window/KWinRule.cpp
|
||||
)
|
||||
|
||||
qt6_generate_wayland_protocol_client_sources(sloposk PRIVATE_CODE FILES
|
||||
@@ -100,7 +101,7 @@ target_link_libraries(sloposk PRIVATE
|
||||
sloposk_coreplugin
|
||||
Qt6::DBus
|
||||
Qt6::WaylandClient
|
||||
LayerShellQt::Interface
|
||||
KF6::ConfigCore
|
||||
KF6::WindowSystem
|
||||
PkgConfig::WAYLAND
|
||||
)
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
#include "layout/LayoutModel.h"
|
||||
#include "settings/Settings.h"
|
||||
#include "window/FloatingWindow.h"
|
||||
#include "window/KWinRule.h"
|
||||
#include "window/VisibilityController.h"
|
||||
#include "window/WindowSettings.h"
|
||||
|
||||
@@ -158,6 +159,7 @@ int main(int argc, char *argv[])
|
||||
return 1;
|
||||
}
|
||||
|
||||
installKWinRule(QGuiApplication::desktopFileName());
|
||||
FloatingWindow placement(window);
|
||||
window->setProperty("placement", QVariant::fromValue(&placement));
|
||||
WindowSettings windowSettings;
|
||||
|
||||
@@ -1,68 +1,28 @@
|
||||
#include "FloatingWindow.h"
|
||||
|
||||
#include <KWindowEffects>
|
||||
#include <LayerShellQt/Window>
|
||||
#include <QPainterPath>
|
||||
#include <QGuiApplication>
|
||||
#include <QScreen>
|
||||
#include <QWindow>
|
||||
|
||||
namespace {
|
||||
|
||||
bool isWayland()
|
||||
{
|
||||
return QGuiApplication::platformName().startsWith(QLatin1String("wayland"));
|
||||
}
|
||||
|
||||
constexpr int BottomGap = 24;
|
||||
|
||||
} // namespace
|
||||
|
||||
FloatingWindow::FloatingWindow(QWindow *window, QObject *parent)
|
||||
: QObject(parent)
|
||||
, m_window(window)
|
||||
{
|
||||
if (isWayland()) {
|
||||
m_layerSurface = LayerShellQt::Window::get(window);
|
||||
m_layerSurface->setLayer(LayerShellQt::Window::LayerOverlay);
|
||||
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());
|
||||
window->setFlags(Qt::Tool | Qt::FramelessWindowHint | Qt::WindowStaysOnTopHint | Qt::WindowDoesNotAcceptFocus);
|
||||
for (const auto changed : {&QWindow::xChanged, &QWindow::yChanged, &QWindow::widthChanged, &QWindow::heightChanged})
|
||||
connect(window, changed, this, &FloatingWindow::geometryChanged);
|
||||
}
|
||||
|
||||
QRect FloatingWindow::geometry() const
|
||||
{
|
||||
return {m_position, m_window->size()};
|
||||
return m_window->geometry().translated(-screenArea().topLeft());
|
||||
}
|
||||
|
||||
void FloatingWindow::setGeometry(const QRect &geometry)
|
||||
{
|
||||
const QSize size = geometry.size().expandedTo(m_window->minimumSize()).boundedTo(screenArea().size());
|
||||
m_window->resize(size);
|
||||
m_position = clamped(geometry.topLeft(), size);
|
||||
apply();
|
||||
m_window->setGeometry({screenArea().topLeft() + geometry.topLeft(), size});
|
||||
}
|
||||
|
||||
QString FloatingWindow::screenName() const
|
||||
@@ -70,26 +30,12 @@ QString FloatingWindow::screenName() const
|
||||
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)
|
||||
{
|
||||
const QSize maximum = screenArea().size() - QSize(m_position.x(), m_position.y());
|
||||
const QSize size = (m_window->size() + QSize(qRound(dw), qRound(dh)))
|
||||
.expandedTo(m_window->minimumSize())
|
||||
.boundedTo(maximum);
|
||||
if (size == m_window->size())
|
||||
return;
|
||||
.boundedTo(screenArea().size());
|
||||
m_window->resize(size);
|
||||
Q_EMIT geometryChanged();
|
||||
}
|
||||
|
||||
void FloatingWindow::setInputRegion(const QRectF ®ion)
|
||||
@@ -122,26 +68,3 @@ QRect FloatingWindow::screenArea() const
|
||||
{
|
||||
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 <QRectF>
|
||||
|
||||
class QScreen;
|
||||
class QWindow;
|
||||
namespace LayerShellQt { class Window; }
|
||||
|
||||
// Places a keyboard window as a free-floating overlay that never takes focus.
|
||||
// On Wayland: a layer-shell surface anchored top-left, positioned via margins.
|
||||
// On X11: a frameless, non-focusable, always-on-top tool window.
|
||||
// Geometry is relative to the window's screen and always kept on it, also
|
||||
// when the screen's resolution changes or the window moves to another one.
|
||||
// Makes a keyboard window a frameless, always-on-top window that never takes
|
||||
// focus. The window manager places it and moves it (Super+drag included);
|
||||
// on KWin Wayland that relies on the rule from installKWinRule().
|
||||
// Geometry is relative to the window's screen.
|
||||
class FloatingWindow : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
@@ -22,10 +19,10 @@ public:
|
||||
explicit FloatingWindow(QWindow *window, QObject *parent = nullptr);
|
||||
|
||||
QRect geometry() const;
|
||||
// Wayland clients can't place themselves: only the size applies there.
|
||||
void setGeometry(const QRect &geometry);
|
||||
QString screenName() const;
|
||||
|
||||
Q_INVOKABLE void moveBy(qreal dx, qreal dy);
|
||||
// Grows from the top-left corner; respects the window's minimum size.
|
||||
Q_INVOKABLE void resizeBy(qreal dw, qreal dh);
|
||||
// Only this part of the window takes input; the rest passes through.
|
||||
@@ -36,18 +33,10 @@ public:
|
||||
Q_INVOKABLE void hide();
|
||||
|
||||
Q_SIGNALS:
|
||||
// Only for user-initiated changes (move/resize), not for restores.
|
||||
void geometryChanged();
|
||||
|
||||
private:
|
||||
QRect screenArea() const;
|
||||
QPoint clamped(QPoint position, QSize size) const;
|
||||
QPoint defaultPosition() const;
|
||||
void apply();
|
||||
void followScreen(QScreen *screen);
|
||||
|
||||
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