Screenedge show support for Clients

This provides a new protocol intended to be used by auto-hiding panels
to make use of the centralized screen edges. To use it a Client can
set an X11 property of type _KDE_NET_WM_SCREEN_EDGE_SHOW to KWin.
As value it takes:
* 0: top edge
* 1: right edge
* 2: bottom edge
* 3: left edge

KWin will hide the Client (hide because unmap or minimize would break
it) and create an Edge. If that Edge gets triggered the Client is shown
again and the property gets deleted. If the Client doesn't border the
specified screen edge the Client gets shown immediately so that we
never end in a situation that we cannot unhide the auto-hidden panel
again. The exact process is described in the documentation of
ScreenEdges. The Client can request to be shown again by deleting the
property.

If KWin gets restarted the state is read from the property and it is
tried to create the edge as described.

As this is a KWin specific extension we need to discuss what it means
for Clients using this feature with other WMs: it does nothing. As
the Client gets hidden by KWin and not by the Client, it just doesn't
get hidden if the WM doesn't provide the feature. In case of an
auto-hiding panel this seems like a good solution given that we don't
want to hide it if we cannot unhide it. Of course there's the option
for the Client to provide that feature itself and if that's wanted we
would need to announce the feature in the _NET_SUPPORTED atom. At the
moment that doesn't sound like being needed as Plasma doesn't want to
provide an own implementation.

The implementation comes with a small test application showing how
the feature is intended to be used.

REVIEW: 115910
icc-effect-5.14.5
Martin Gräßlin 2014-02-20 12:39:23 +01:00
parent 95ab9d05aa
commit ed4a0d0319
10 changed files with 399 additions and 15 deletions

View File

@ -58,6 +58,7 @@ Atoms::Atoms()
, kde_first_in_window_list(QByteArrayLiteral("_KDE_FIRST_IN_WINDOWLIST"))
, kde_color_sheme(QByteArrayLiteral("_KDE_NET_WM_COLOR_SCHEME"))
, kde_skip_close_animation(QByteArrayLiteral("_KDE_NET_WM_SKIP_CLOSE_ANIMATION"))
, kde_screen_edge_show(QByteArrayLiteral("_KDE_NET_WM_SCREEN_EDGE_SHOW"))
, m_dtSmWindowInfo(QByteArrayLiteral("_DT_SM_WINDOW_INFO"))
, m_motifSupport(QByteArrayLiteral("_MOTIF_WM_INFO"))
, m_helpersRetrieved(false)

View File

@ -67,6 +67,7 @@ public:
Xcb::Atom kde_first_in_window_list;
Xcb::Atom kde_color_sheme;
Xcb::Atom kde_skip_close_animation;
Xcb::Atom kde_screen_edge_show;
/**
* @internal

View File

@ -42,6 +42,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
#include "tabbox.h"
#endif
#include "workspace.h"
#include "screenedge.h"
// KDE
#include <KDE/KWindowSystem>
#include <KDE/KColorScheme>
@ -2503,6 +2504,59 @@ xcb_window_t Client::frameId() const
return m_frame;
}
void Client::updateShowOnScreenEdge()
{
auto cookie = xcb_get_property_unchecked(connection(), false, window(), atoms->kde_screen_edge_show, XCB_ATOM_CARDINAL, 0, 1);
ScopedCPointer<xcb_get_property_reply_t> reply(xcb_get_property_reply(connection(), cookie, nullptr));
auto restore = [this]() {
// TODO: add proper unreserve
ScreenEdges::self()->reserve(this, ElectricNone);
hideClient(false);
};
if (!reply.isNull()) {
if (reply->format == 32 && reply->type == XCB_ATOM_CARDINAL && reply->value_len == 1) {
const uint32_t value = *reinterpret_cast<uint32_t*>(xcb_get_property_value(reply.data()));
ElectricBorder border = ElectricNone;
switch (value) {
case 0:
border = ElectricTop;
break;
case 1:
border = ElectricRight;
break;
case 2:
border = ElectricBottom;
break;
case 3:
border = ElectricLeft;
break;
}
if (border != ElectricNone) {
hideClient(true);
ScreenEdges::self()->reserve(this, border);
} else {
// property value is incorrect, delete the property
// so that the client knows that it is not hidden
xcb_delete_property(connection(), window(), atoms->kde_screen_edge_show);
}
} else if (reply->type == XCB_ATOM_NONE) {
// the property got deleted, show the client again
restore();
}
} else {
restore();
}
}
void Client::showOnScreenEdge()
{
hideClient(false);
xcb_delete_property(connection(), window(), atoms->kde_screen_edge_show);
}
} // namespace
#include "client.moc"

View File

@ -648,6 +648,12 @@ public:
QPalette palette() const;
/**
* Restores the Client after it had been hidden due to show on screen edge functionality.
* In addition the property gets deleted so that the Client knows that it is visible again.
**/
void showOnScreenEdge();
public Q_SLOTS:
void closeWindow();
void updateCaption();
@ -834,6 +840,12 @@ private:
bool tabTo(Client *other, bool behind, bool activate);
/**
* Reads the property and creates/destroys the screen edge if required
* and shows/hides the client.
**/
void updateShowOnScreenEdge();
Xcb::Window m_client;
Xcb::Window m_wrapper;
Xcb::Window m_frame;

View File

@ -803,6 +803,8 @@ void Client::propertyNotifyEvent(xcb_property_notify_event_t *e)
updateFirstInTabBox();
else if (e->atom == atoms->kde_color_sheme)
updateColorScheme();
else if (e->atom == atoms->kde_screen_edge_show)
updateShowOnScreenEdge();
break;
}
}

View File

@ -619,6 +619,7 @@ bool Client::manage(xcb_window_t w, bool isMapped)
updateCompositeBlocking(true);
updateColorScheme();
updateShowOnScreenEdge();
// TODO: there's a small problem here - isManaged() depends on the mapping state,
// but this client is not yet in Workspace's client list at this point, will

View File

@ -59,6 +59,7 @@ Edge::Edge(ScreenEdges *parent)
, m_approaching(false)
, m_lastApproachingFactor(0)
, m_blocked(false)
, m_client(nullptr)
{
}
@ -87,6 +88,7 @@ void Edge::unreserve()
m_reserved--;
if (m_reserved == 0) {
// got deactivated
stopApproaching();
deactivate();
}
}
@ -122,20 +124,28 @@ bool Edge::triggersFor(const QPoint &cursorPos) const
return true;
}
void Edge::check(const QPoint &cursorPos, const QDateTime &triggerTime, bool forceNoPushBack)
bool Edge::check(const QPoint &cursorPos, const QDateTime &triggerTime, bool forceNoPushBack)
{
if (!triggersFor(cursorPos)) {
return;
return false;
}
// no pushback so we have to activate at once
bool directActivate = forceNoPushBack || edges()->cursorPushBackDistance().isNull();
if (directActivate || canActivate(cursorPos, triggerTime)) {
m_lastTrigger = triggerTime;
m_lastReset = QDateTime(); // invalidate
markAsTriggered(cursorPos, triggerTime);
handle(cursorPos);
return true;
} else {
pushCursorBack(cursorPos);
m_triggeredPoint = cursorPos;
}
return false;
}
void Edge::markAsTriggered(const QPoint &cursorPos, const QDateTime &triggerTime)
{
m_lastTrigger = triggerTime;
m_lastReset = QDateTime(); // invalidate
m_triggeredPoint = cursorPos;
}
@ -164,6 +174,12 @@ bool Edge::canActivate(const QPoint &cursorPos, const QDateTime &triggerTime)
void Edge::handle(const QPoint &cursorPos)
{
if (m_client) {
pushCursorBack(cursorPos);
m_client->showOnScreenEdge();
unreserve();
return;
}
if ((edges()->isDesktopSwitchingMovingClients() && Workspace::self()->getMovingClient()) ||
(edges()->isDesktopSwitching() && isScreenEdge())) {
// always switch desktops in case:
@ -555,6 +571,12 @@ ScreenEdges::ScreenEdges(QObject *parent)
{
QWidget w;
m_cornerOffset = (w.physicalDpiX() + w.physicalDpiY() + 5) / 6;
connect(workspace(), &Workspace::clientRemoved, [this](KWin::Client *client) {
deleteEdgeForClient(client);
QObject::disconnect(client, &Client::geometryChanged,
ScreenEdges::self(), &ScreenEdges::handleClientGeometryChanged);
});
}
ScreenEdges::~ScreenEdges()
@ -808,6 +830,11 @@ void ScreenEdges::recreateEdges()
oldIt != oldEdges.constEnd();
++oldIt) {
WindowBasedEdge *oldEdge = *oldIt;
if (oldEdge->client()) {
// show the client again and don't recreate the edge
oldEdge->client()->showOnScreenEdge();
continue;
}
if (oldEdge->border() != edge->border()) {
continue;
}
@ -869,15 +896,17 @@ void ScreenEdges::createHorizontalEdge(ElectricBorder border, const QRect &scree
m_edges << createEdge(border, x, y, width, 1);
}
WindowBasedEdge *ScreenEdges::createEdge(ElectricBorder border, int x, int y, int width, int height)
WindowBasedEdge *ScreenEdges::createEdge(ElectricBorder border, int x, int y, int width, int height, bool createAction)
{
WindowBasedEdge *edge = new WindowBasedEdge(this);
edge->setBorder(border);
edge->setGeometry(QRect(x, y, width, height));
const ElectricBorderAction action = actionForEdge(edge);
if (action != KWin::ElectricActionNone) {
edge->reserve();
edge->setAction(action);
if (createAction) {
const ElectricBorderAction action = actionForEdge(edge);
if (action != KWin::ElectricActionNone) {
edge->reserve();
edge->setAction(action);
}
}
if (isDesktopSwitching()) {
if (edge->isCorner()) {
@ -961,8 +990,127 @@ void ScreenEdges::unreserve(ElectricBorder border, QObject *object)
}
}
void ScreenEdges::reserve(Client *client, ElectricBorder border)
{
auto it = m_edges.begin();
while (it != m_edges.end()) {
if ((*it)->client() == client) {
if ((*it)->border() == border) {
if (client->isHiddenInternal() && !(*it)->isReserved()) {
(*it)->reserve();
}
return;
} else {
delete *it;
it = m_edges.erase(it);
}
} else {
it++;
}
}
createEdgeForClient(client, border);
connect(client, &Client::geometryChanged, this, &ScreenEdges::handleClientGeometryChanged);
}
void ScreenEdges::createEdgeForClient(Client *client, ElectricBorder border)
{
int y = 0;
int x = 0;
int width = 0;
int height = 0;
const QRect geo = client->geometry();
const QRect fullArea = workspace()->clientArea(FullArea, 0, 1);
for (int i = 0; i < screens()->count(); ++i) {
const QRect screen = screens()->geometry(i);
if (!screen.contains(geo)) {
// ignoring Clients having a geometry overlapping with multiple screens
// this would make the code more complex. If it's needed in future it can be added
continue;
}
const bool bordersTop = (screen.y() == geo.y());
const bool bordersLeft = (screen.x() == geo.x());
const bool bordersBottom = (screen.y() + screen.height() == geo.y() + geo.height());
const bool bordersRight = (screen.x() + screen.width() == geo.x() + geo.width());
if (bordersTop && border == ElectricTop) {
if (!isTopScreen(screen, fullArea)) {
continue;
}
y = geo.y();
x = geo.x();
height = 1;
width = geo.width();
break;
}
if (bordersBottom && border == ElectricBottom) {
if (!isBottomScreen(screen, fullArea)) {
continue;
}
y = geo.y() + geo.height() - 1;
x = geo.x();
height = 1;
width = geo.width();
break;
}
if (bordersLeft && border == ElectricLeft) {
if (!isLeftScreen(screen, fullArea)) {
continue;
}
x = geo.x();
y = geo.y();
width = 1;
height = geo.height();
break;
}
if (bordersRight && border == ElectricRight) {
if (!isRightScreen(screen, fullArea)) {
continue;
}
x = geo.x() + geo.width() - 1;
y = geo.y();
width = 1;
height = geo.height();
break;
}
}
if (width > 0 && height > 0) {
WindowBasedEdge *edge = createEdge(border, x, y, width, height, false);
edge->setClient(client);
m_edges.append(edge);
if (client->isHiddenInternal()) {
edge->reserve();
}
} else {
// we could not create an edge window, so don't allow the window to hide
client->showOnScreenEdge();
}
}
void ScreenEdges::handleClientGeometryChanged()
{
Client *c = static_cast<Client*>(sender());
deleteEdgeForClient(c);
c->showOnScreenEdge();
}
void ScreenEdges::deleteEdgeForClient(Client* c)
{
auto it = m_edges.begin();
while (it != m_edges.end()) {
if ((*it)->client() == c) {
delete *it;
it = m_edges.erase(it);
} else {
it++;
}
}
}
void ScreenEdges::check(const QPoint &pos, const QDateTime &now, bool forceNoPushBack)
{
bool activatedForClient = false;
for (QList<WindowBasedEdge*>::iterator it = m_edges.begin(); it != m_edges.end(); ++it) {
if (!(*it)->isReserved()) {
continue;
@ -970,7 +1118,15 @@ void ScreenEdges::check(const QPoint &pos, const QDateTime &now, bool forceNoPus
if ((*it)->approachGeometry().contains(pos)) {
(*it)->startApproaching();
}
(*it)->check(pos, now, forceNoPushBack);
if ((*it)->client() != nullptr && activatedForClient) {
(*it)->markAsTriggered(pos, now);
continue;
}
if ((*it)->check(pos, now, forceNoPushBack)) {
if ((*it)->client()) {
activatedForClient = true;
}
}
}
}
@ -992,14 +1148,21 @@ bool ScreenEdges::isEntered(xcb_client_message_event_t *event)
bool ScreenEdges::handleEnterNotifiy(xcb_window_t window, const QPoint &point, const QDateTime &timestamp)
{
bool activated = false;
bool activatedForClient = false;
for (QList<WindowBasedEdge*>::iterator it = m_edges.begin(); it != m_edges.end(); ++it) {
WindowBasedEdge *edge = *it;
if (!edge->isReserved()) {
continue;
}
if (edge->window() == window) {
edge->check(point, timestamp);
return true;
if (edge->check(point, timestamp)) {
if ((*it)->client()) {
activatedForClient = true;
}
}
activated = true;
break;
}
if (edge->approachWindow() == window) {
edge->startApproaching();
@ -1007,7 +1170,14 @@ bool ScreenEdges::handleEnterNotifiy(xcb_window_t window, const QPoint &point, c
return true;
}
}
return false;
if (activatedForClient) {
for (auto it = m_edges.constBegin(); it != m_edges.constEnd(); ++it) {
if ((*it)->client()) {
(*it)->markAsTriggered(point, timestamp);
}
}
}
return activated;
}
bool ScreenEdges::handleDndNotify(xcb_window_t window, const QPoint &point)

View File

@ -56,7 +56,8 @@ public:
bool isCorner() const;
bool isScreenEdge() const;
bool triggersFor(const QPoint &cursorPos) const;
void check(const QPoint &cursorPos, const QDateTime &triggerTime, bool forceNoPushBack = false);
bool check(const QPoint &cursorPos, const QDateTime &triggerTime, bool forceNoPushBack = false);
void markAsTriggered(const QPoint &cursorPos, const QDateTime &triggerTime);
bool isReserved() const;
const QRect &approachGeometry() const;
@ -65,6 +66,8 @@ public:
const QHash<QObject *, QByteArray> &callBacks() const;
void startApproaching();
void stopApproaching();
void setClient(Client *client);
Client *client() const;
public Q_SLOTS:
void reserve();
@ -108,6 +111,7 @@ private:
bool m_approaching;
int m_lastApproachingFactor;
bool m_blocked;
Client *m_client;
};
class WindowBasedEdge : public Edge
@ -242,6 +246,29 @@ public:
* @todo: add pointer to script/effect
*/
void unreserve(ElectricBorder border, QObject *object);
/**
* Reserves an edge for the @p client. The idea behind this is to show the @p client if the
* screen edge which the @p client borders gets triggered.
*
* When first called it is tried to create an Edge for the client. This is only done if the
* client borders with a screen edge specified by @p border. If the client doesn't border the
* screen edge, no Edge gets created and the client is shown again. Otherwise there would not
* be a possibility to show the client again.
*
* On subsequent calls for the client no new Edge is created, but the existing one gets reused
* and if the client is already hidden, the Edge gets reserved.
*
* Once the Edge for the client triggers, the client gets shown again and the Edge unreserved.
* The idea is that the Edge can only get activated if the client is currently hidden.
*
* To make sure that the client can always be shown again the implementation also starts to
* track geometry changes and shows the Client again. The same for screen geometry changes.
*
* The Edge gets automatically destroyed if the client gets released.
* @param client The Client for which an Edge should be reserved
* @param border The border which the client wants to use, only proper borders are supported (no corners)
**/
void reserve(KWin::Client *client, ElectricBorder border);
/**
* Reserve desktop switching for screen edges, if @p isToReserve is @c true. Unreserve otherwise.
* @param reserve indicated weather desktop switching should be reserved or unreseved
@ -317,11 +344,14 @@ private:
void setReActivationThreshold(int threshold);
void createHorizontalEdge(ElectricBorder border, const QRect &screen, const QRect &fullArea);
void createVerticalEdge(ElectricBorder border, const QRect &screen, const QRect &fullArea);
WindowBasedEdge *createEdge(ElectricBorder border, int x, int y, int width, int height);
WindowBasedEdge *createEdge(ElectricBorder border, int x, int y, int width, int height, bool createAction = true);
void setActionForBorder(ElectricBorder border, ElectricBorderAction *oldValue, ElectricBorderAction newValue);
ElectricBorderAction actionForEdge(Edge *edge) const;
bool handleEnterNotifiy(xcb_window_t window, const QPoint &point, const QDateTime &timestamp);
bool handleDndNotify(xcb_window_t window, const QPoint &point);
void createEdgeForClient(Client *client, ElectricBorder border);
void handleClientGeometryChanged();
void deleteEdgeForClient(Client *client);
bool m_desktopSwitching;
bool m_desktopSwitchingMovingClients;
QSize m_cursorPushBackDistance;
@ -433,6 +463,16 @@ inline bool Edge::isBlocked() const
return m_blocked;
}
inline void Edge::setClient(Client *client)
{
m_client = client;
}
inline Client *Edge::client() const
{
return m_client;
}
/**********************************************************
* Inlines WindowBasedEdge
*********************************************************/

View File

@ -3,3 +3,8 @@ if (XCB_ICCCM_FOUND)
add_executable(normalhintsbasesizetest ${normalhintsbasesizetest_SRCS})
target_link_libraries(normalhintsbasesizetest XCB::XCB XCB::ICCCM)
endif()
# next target
set(screenedgeshowtest_SRCS screenedgeshowtest.cpp)
add_executable(screenedgeshowtest ${screenedgeshowtest_SRCS})
target_link_libraries(screenedgeshowtest Qt5::Widgets Qt5::X11Extras ${XCB_XCB_LIBRARY})

View File

@ -0,0 +1,98 @@
/*
* Copyright 2014 Martin Gräßlin <mgraesslin@kde.org>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License as
* published by the Free Software Foundation; either version 2 of
* the License or (at your option) version 3 or any later version
* accepted by the membership of KDE e.V. (or its successor approved
* by the membership of KDE e.V.), which shall act as a proxy
* defined in Section 14 of version 3 of the license.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include <QApplication>
#include <QHBoxLayout>
#include <QMenu>
#include <QPushButton>
#include <QScreen>
#include <QTimer>
#include <QToolButton>
#include <QWidget>
#include "../xcbutils.h"
int main(int argc, char **argv)
{
QApplication app(argc, argv);
QApplication::setApplicationDisplayName(QStringLiteral("Screen Edge Show Test App"));
QScopedPointer<QWidget> widget(new QWidget(nullptr, Qt::FramelessWindowHint));
KWin::Xcb::Atom atom(QByteArrayLiteral("_KDE_NET_WM_SCREEN_EDGE_SHOW"));
uint32_t value = 2;
QPushButton *hideWindowButton = new QPushButton(QStringLiteral("Hide"), widget.data());
QObject::connect(hideWindowButton, &QPushButton::clicked, [&widget, &atom, &value]() {
xcb_change_property(QX11Info::connection(), XCB_PROP_MODE_REPLACE, widget->winId(), atom, XCB_ATOM_CARDINAL, 32, 1, &value);
});
QPushButton *hideAndRestoreButton = new QPushButton(QStringLiteral("Hide and Restore after 10 sec"), widget.data());
QTimer *restoreTimer = new QTimer(hideAndRestoreButton);
restoreTimer->setSingleShot(true);
QObject::connect(hideAndRestoreButton, &QPushButton::clicked, [&widget, &atom, &value, restoreTimer]() {
xcb_change_property(QX11Info::connection(), XCB_PROP_MODE_REPLACE, widget->winId(), atom, XCB_ATOM_CARDINAL, 32, 1, &value);
restoreTimer->start(10000);
});
QObject::connect(restoreTimer, &QTimer::timeout, [&widget, &atom]() {
xcb_delete_property(QX11Info::connection(), widget->winId(), atom);
});
QToolButton *edgeButton = new QToolButton(widget.data());
edgeButton->setText(QStringLiteral("Edge"));
edgeButton->setPopupMode(QToolButton::MenuButtonPopup);
QMenu *edgeButtonMenu = new QMenu(edgeButton);
QObject::connect(edgeButtonMenu->addAction("Top"), &QAction::triggered, [&widget, &value]() {
const QRect geo = QGuiApplication::primaryScreen()->geometry();
widget->setGeometry(geo.x(), geo.y(), geo.width(), 100);
value = 0;
});
QObject::connect(edgeButtonMenu->addAction("Right"), &QAction::triggered, [&widget, &value]() {
const QRect geo = QGuiApplication::primaryScreen()->geometry();
widget->setGeometry(geo.x() + geo.width() - 100, geo.y(), 100, geo.height());
value = 1;
});
QObject::connect(edgeButtonMenu->addAction("Bottom"), &QAction::triggered, [&widget, &value]() {
const QRect geo = QGuiApplication::primaryScreen()->geometry();
widget->setGeometry(geo.x(), geo.y() + geo.height() - 100, geo.width(), 100);
value = 2;
});
QObject::connect(edgeButtonMenu->addAction("Left"), &QAction::triggered, [&widget, &value]() {
const QRect geo = QGuiApplication::primaryScreen()->geometry();
widget->setGeometry(geo.x(), geo.y(), 100, geo.height());
value = 3;
});
edgeButtonMenu->addSeparator();
QObject::connect(edgeButtonMenu->addAction("Floating"), &QAction::triggered, [&widget, &value]() {
const QRect geo = QGuiApplication::primaryScreen()->geometry();
widget->setGeometry(QRect(geo.center(), QSize(100, 100)));
value = 4;
});
edgeButton->setMenu(edgeButtonMenu);
QHBoxLayout *layout = new QHBoxLayout(widget.data());
layout->addWidget(hideWindowButton);
layout->addWidget(hideAndRestoreButton);
layout->addWidget(edgeButton);
widget->setLayout(layout);
const QRect geo = QGuiApplication::primaryScreen()->geometry();
widget->setGeometry(geo.x(), geo.y() + geo.height() - 100, geo.width(), 100);
widget->show();
return app.exec();
}