diff --git a/atoms.cpp b/atoms.cpp index 904f5efe4..ec2c60fcb 100644 --- a/atoms.cpp +++ b/atoms.cpp @@ -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) diff --git a/atoms.h b/atoms.h index 1690067c5..3566d0a38 100644 --- a/atoms.h +++ b/atoms.h @@ -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 diff --git a/client.cpp b/client.cpp index 36431bfc3..023419254 100644 --- a/client.cpp +++ b/client.cpp @@ -42,6 +42,7 @@ along with this program. If not, see . #include "tabbox.h" #endif #include "workspace.h" +#include "screenedge.h" // KDE #include #include @@ -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 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(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" diff --git a/client.h b/client.h index 6a0dbe4f4..a8ab0027b 100644 --- a/client.h +++ b/client.h @@ -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; diff --git a/events.cpp b/events.cpp index 1fa6e425d..a2ba41f24 100644 --- a/events.cpp +++ b/events.cpp @@ -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; } } diff --git a/manage.cpp b/manage.cpp index 3e385cd6a..563cedb34 100644 --- a/manage.cpp +++ b/manage.cpp @@ -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 diff --git a/screenedge.cpp b/screenedge.cpp index 04cf0d6d5..3c3d1d3ee 100644 --- a/screenedge.cpp +++ b/screenedge.cpp @@ -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(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::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 ×tamp) { + bool activated = false; + bool activatedForClient = false; for (QList::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) diff --git a/screenedge.h b/screenedge.h index 60f5fd669..f1c579cf1 100644 --- a/screenedge.h +++ b/screenedge.h @@ -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 &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 ×tamp); 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 *********************************************************/ diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 5d8cb132a..26524ffa0 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -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}) diff --git a/tests/screenedgeshowtest.cpp b/tests/screenedgeshowtest.cpp new file mode 100644 index 000000000..e8bb1ceb7 --- /dev/null +++ b/tests/screenedgeshowtest.cpp @@ -0,0 +1,98 @@ +/* + * Copyright 2014 Martin Gräßlin + * + * 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 . +*/ +#include +#include +#include +#include +#include +#include +#include +#include +#include "../xcbutils.h" + +int main(int argc, char **argv) +{ + QApplication app(argc, argv); + QApplication::setApplicationDisplayName(QStringLiteral("Screen Edge Show Test App")); + + QScopedPointer 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(); +}