File indexing completed on 2024-04-21 16:13:18

0001 /*
0002 SPDX-FileCopyrightText: 1999 Martin R. Jones <mjones@kde.org>
0003 SPDX-FileCopyrightText: 2002 Luboš Luňák <l.lunak@kde.org>
0004 SPDX-FileCopyrightText: 2003 Oswald Buddenhagen <ossi@kde.org>
0005 SPDX-FileCopyrightText: 2008 Chani Armitage <chanika@gmail.com>
0006 SPDX-FileCopyrightText: 2011 Martin Gräßlin <mgraesslin@kde.org>
0007 
0008 SPDX-License-Identifier: GPL-2.0-or-later
0009 */
0010 #include "x11locker.h"
0011 #include "globalaccel.h"
0012 // KDE
0013 // Qt
0014 #include <QApplication>
0015 #include <QScreen>
0016 #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
0017 #include <private/qtx11extras_p.h>
0018 #else
0019 #include <QDesktopWidget>
0020 #include <QX11Info>
0021 #endif
0022 // X11
0023 #include <X11/Xatom.h>
0024 #include <xcb/xcb.h>
0025 
0026 #include <kscreenlocker_logging.h>
0027 
0028 static Window gVRoot = 0;
0029 static Window gVRootData = 0;
0030 static Atom gXA_VROOT;
0031 static Atom gXA_SCREENSAVER_VERSION;
0032 
0033 namespace ScreenLocker
0034 {
0035 X11Locker::X11Locker(QObject *parent)
0036     : AbstractLocker(parent)
0037     , QAbstractNativeEventFilter()
0038     , m_focusedLockWindow(XCB_WINDOW_NONE)
0039 {
0040     initialize();
0041 }
0042 
0043 X11Locker::~X11Locker()
0044 {
0045     qApp->removeNativeEventFilter(this);
0046 }
0047 
0048 void X11Locker::initialize()
0049 {
0050     qApp->installNativeEventFilter(this);
0051 
0052     XWindowAttributes rootAttr;
0053     XGetWindowAttributes(QX11Info::display(), QX11Info::appRootWindow(), &rootAttr);
0054     XSelectInput(QX11Info::display(), QX11Info::appRootWindow(), SubstructureNotifyMask | rootAttr.your_event_mask);
0055     // Get root window size
0056     updateGeo();
0057 
0058     // virtual root property
0059     gXA_VROOT = XInternAtom(QX11Info::display(), "__SWM_VROOT", False);
0060     gXA_SCREENSAVER_VERSION = XInternAtom(QX11Info::display(), "_SCREENSAVER_VERSION", False);
0061 
0062     // read the initial information about all toplevel windows
0063     Window r, p;
0064     Window *real;
0065     unsigned nreal;
0066     if (XQueryTree(QX11Info::display(), QX11Info::appRootWindow(), &r, &p, &real, &nreal) && real != nullptr) {
0067         for (unsigned i = 0; i < nreal; ++i) {
0068             XWindowAttributes winAttr;
0069             if (XGetWindowAttributes(QX11Info::display(), real[i], &winAttr)) {
0070                 WindowInfo info;
0071                 info.window = real[i];
0072                 info.viewable = (winAttr.map_state == IsViewable);
0073                 m_windowInfo.append(info); // ordered bottom to top
0074             }
0075         }
0076         XFree(real);
0077     }
0078 
0079     // monitor for screen geometry changes
0080     connect(qGuiApp, &QGuiApplication::screenAdded, this, [this](QScreen *screen) {
0081         connect(screen, &QScreen::geometryChanged, this, &X11Locker::updateGeo);
0082         updateGeo();
0083     });
0084     connect(qGuiApp, &QGuiApplication::screenRemoved, this, &X11Locker::updateGeo);
0085     const auto screens = QGuiApplication::screens();
0086     for (auto *screen : screens) {
0087         connect(screen, &QScreen::geometryChanged, this, &X11Locker::updateGeo);
0088     }
0089 }
0090 
0091 void X11Locker::showLockWindow()
0092 {
0093     m_background->hide();
0094 
0095     // Some xscreensaver hacks check for this property
0096     const char *version = "KDE 4.0";
0097 
0098     XChangeProperty(QX11Info::display(),
0099                     m_background->winId(),
0100                     gXA_SCREENSAVER_VERSION,
0101                     XA_STRING,
0102                     8,
0103                     PropModeReplace,
0104                     (unsigned char *)version,
0105                     strlen(version));
0106 
0107     qCDebug(KSCREENLOCKER) << "Lock window Id: " << m_background->winId();
0108 
0109     m_background->setPosition(0, 0);
0110     XSync(QX11Info::display(), False);
0111 
0112     setVRoot(m_background->winId(), m_background->winId());
0113 }
0114 
0115 //---------------------------------------------------------------------------
0116 //
0117 // Hide the screen locker window
0118 //
0119 void X11Locker::hideLockWindow()
0120 {
0121     Q_EMIT userActivity();
0122     m_background->hide();
0123     m_background->lower();
0124     removeVRoot(m_background->winId());
0125     XDeleteProperty(QX11Info::display(), m_background->winId(), gXA_SCREENSAVER_VERSION);
0126     if (gVRoot) {
0127         unsigned long vroot_data[1] = {gVRootData};
0128         XChangeProperty(QX11Info::display(), gVRoot, gXA_VROOT, XA_WINDOW, 32, PropModeReplace, (unsigned char *)vroot_data, 1);
0129         gVRoot = 0;
0130     }
0131     XSync(QX11Info::display(), False);
0132     m_allowedWindows.clear();
0133 }
0134 
0135 //---------------------------------------------------------------------------
0136 static int ignoreXError(Display *, XErrorEvent *)
0137 {
0138     return 0;
0139 }
0140 
0141 //---------------------------------------------------------------------------
0142 //
0143 // Save the current virtual root window
0144 //
0145 void X11Locker::saveVRoot()
0146 {
0147     Window rootReturn, parentReturn, *children;
0148     unsigned int numChildren;
0149     Window root = QX11Info::appRootWindow();
0150 
0151     gVRoot = 0;
0152     gVRootData = 0;
0153 
0154     int (*oldHandler)(Display *, XErrorEvent *);
0155     oldHandler = XSetErrorHandler(ignoreXError);
0156 
0157     if (XQueryTree(QX11Info::display(), root, &rootReturn, &parentReturn, &children, &numChildren)) {
0158         for (unsigned int i = 0; i < numChildren; i++) {
0159             Atom actual_type;
0160             int actual_format;
0161             unsigned long nitems, bytesafter;
0162             unsigned char *newRoot = nullptr;
0163 
0164             if ((XGetWindowProperty(QX11Info::display(),
0165                                     children[i],
0166                                     gXA_VROOT,
0167                                     0,
0168                                     1,
0169                                     False,
0170                                     XA_WINDOW,
0171                                     &actual_type,
0172                                     &actual_format,
0173                                     &nitems,
0174                                     &bytesafter,
0175                                     &newRoot)
0176                  == Success)
0177                 && newRoot) {
0178                 gVRoot = children[i];
0179                 Window *dummy = (Window *)newRoot;
0180                 gVRootData = *dummy;
0181                 XFree((char *)newRoot);
0182                 break;
0183             }
0184         }
0185         if (children) {
0186             XFree((char *)children);
0187         }
0188     }
0189 
0190     XSetErrorHandler(oldHandler);
0191 }
0192 
0193 //---------------------------------------------------------------------------
0194 //
0195 // Set the virtual root property
0196 //
0197 void X11Locker::setVRoot(Window win, Window vr)
0198 {
0199     if (gVRoot) {
0200         removeVRoot(gVRoot);
0201     }
0202 
0203     unsigned long rw = QX11Info::appRootWindow();
0204     unsigned long vroot_data[1] = {vr};
0205 
0206     Window rootReturn, parentReturn, *children;
0207     unsigned int numChildren;
0208     Window top = win;
0209     while (1) {
0210         if (!XQueryTree(QX11Info::display(), top, &rootReturn, &parentReturn, &children, &numChildren)) {
0211             return;
0212         }
0213         if (children) {
0214             XFree((char *)children);
0215         }
0216         if (parentReturn == rw) {
0217             break;
0218         } else {
0219             top = parentReturn;
0220         }
0221     }
0222 
0223     XChangeProperty(QX11Info::display(), top, gXA_VROOT, XA_WINDOW, 32, PropModeReplace, (unsigned char *)vroot_data, 1);
0224 }
0225 
0226 //---------------------------------------------------------------------------
0227 //
0228 // Remove the virtual root property
0229 //
0230 void X11Locker::removeVRoot(Window win)
0231 {
0232     XDeleteProperty(QX11Info::display(), win, gXA_VROOT);
0233 }
0234 
0235 void X11Locker::fakeFocusIn(WId window)
0236 {
0237     if (window == m_focusedLockWindow) {
0238         return;
0239     }
0240 
0241     // We have keyboard grab, so this application will
0242     // get keyboard events even without having focus.
0243     // Fake FocusIn to make Qt realize it has the active
0244     // window, so that it will correctly show cursor in the dialog.
0245     XEvent ev;
0246     memset(&ev, 0, sizeof(ev));
0247     ev.xfocus.display = QX11Info::display();
0248     ev.xfocus.type = FocusIn;
0249     ev.xfocus.window = window;
0250     ev.xfocus.mode = NotifyNormal;
0251     ev.xfocus.detail = NotifyAncestor;
0252     XSendEvent(QX11Info::display(), window, False, NoEventMask, &ev);
0253     XFlush(QX11Info::display());
0254 
0255     m_focusedLockWindow = window;
0256 }
0257 
0258 template<typename T>
0259 void coordFromEvent(xcb_generic_event_t *event, int *x, int *y)
0260 {
0261     T *e = reinterpret_cast<T *>(event);
0262     *x = e->event_x;
0263     *y = e->event_y;
0264 }
0265 
0266 template<typename T>
0267 void sendEvent(xcb_generic_event_t *event, xcb_window_t target, int x, int y)
0268 {
0269     T e = *(reinterpret_cast<T *>(event));
0270     e.event = target;
0271     e.child = target;
0272     e.event_x = x;
0273     e.event_y = y;
0274     xcb_send_event(QX11Info::connection(), false, target, XCB_EVENT_MASK_NO_EVENT, reinterpret_cast<const char *>(&e));
0275 }
0276 
0277 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
0278 bool X11Locker::nativeEventFilter(const QByteArray &eventType, void *message, long int *)
0279 #else
0280 bool X11Locker::nativeEventFilter(const QByteArray &eventType, void *message, qintptr *)
0281 #endif
0282 {
0283     if (eventType != QByteArrayLiteral("xcb_generic_event_t")) {
0284         return false;
0285     }
0286     xcb_generic_event_t *event = reinterpret_cast<xcb_generic_event_t *>(message);
0287     const uint8_t responseType = event->response_type & ~0x80;
0288     if (globalAccel() && responseType == XCB_KEY_PRESS) {
0289         if (globalAccel()->checkKeyPress(reinterpret_cast<xcb_key_press_event_t *>(event))) {
0290             Q_EMIT userActivity();
0291             return true;
0292         }
0293     }
0294     bool ret = false;
0295     switch (responseType) {
0296     case XCB_BUTTON_PRESS:
0297     case XCB_BUTTON_RELEASE:
0298     case XCB_KEY_PRESS:
0299     case XCB_KEY_RELEASE:
0300     case XCB_MOTION_NOTIFY:
0301         Q_EMIT userActivity();
0302         if (!m_lockWindows.isEmpty()) {
0303             int x = 0;
0304             int y = 0;
0305             if (responseType == XCB_KEY_PRESS || responseType == XCB_KEY_RELEASE) {
0306                 coordFromEvent<xcb_key_press_event_t>(event, &x, &y);
0307             } else if (responseType == XCB_BUTTON_PRESS || responseType == XCB_BUTTON_RELEASE) {
0308                 coordFromEvent<xcb_button_press_event_t>(event, &x, &y);
0309             } else if (responseType == XCB_MOTION_NOTIFY) {
0310                 coordFromEvent<xcb_motion_notify_event_t>(event, &x, &y);
0311             }
0312             Window root_return;
0313             int x_return, y_return;
0314             unsigned int width_return, height_return, border_width_return, depth_return;
0315             for (WId window : qAsConst(m_lockWindows)) {
0316                 if (XGetGeometry(QX11Info::display(),
0317                                  window,
0318                                  &root_return,
0319                                  &x_return,
0320                                  &y_return,
0321                                  &width_return,
0322                                  &height_return,
0323                                  &border_width_return,
0324                                  &depth_return)
0325                     && (x >= x_return && x <= x_return + (int)width_return) && (y >= y_return && y <= y_return + (int)height_return)) {
0326                     // We need to do our own focus handling (see comment in fakeFocusIn).
0327                     // For now: Focus on clicks inside the window
0328                     if (responseType == XCB_BUTTON_PRESS) {
0329                         fakeFocusIn(window);
0330                     }
0331                     const int targetX = x - x_return;
0332                     const int targetY = y - y_return;
0333                     if (responseType == XCB_KEY_PRESS || responseType == XCB_KEY_RELEASE) {
0334                         sendEvent<xcb_key_press_event_t>(event, window, targetX, targetY);
0335                     } else if (responseType == XCB_BUTTON_PRESS || responseType == XCB_BUTTON_RELEASE) {
0336                         sendEvent<xcb_button_press_event_t>(event, window, targetX, targetY);
0337                     } else if (responseType == XCB_MOTION_NOTIFY) {
0338                         sendEvent<xcb_motion_notify_event_t>(event, window, targetX, targetY);
0339                     }
0340                     break;
0341                 }
0342             }
0343             ret = true;
0344         }
0345         break;
0346     case XCB_CONFIGURE_NOTIFY: { // from SubstructureNotifyMask on the root window
0347         xcb_configure_notify_event_t *xc = reinterpret_cast<xcb_configure_notify_event_t *>(event);
0348         if (xc->event == QX11Info::appRootWindow()) {
0349             int index = findWindowInfo(xc->window);
0350             if (index >= 0) {
0351                 int index2 = xc->above_sibling ? findWindowInfo(xc->above_sibling) : 0;
0352                 if (index2 < 0) {
0353                     qCDebug(KSCREENLOCKER) << "Unknown above for ConfigureNotify";
0354                 } else { // move just above the other window
0355                     if (index2 < index) {
0356                         ++index2;
0357                     }
0358                     m_windowInfo.move(index, index2);
0359                 }
0360             } else {
0361                 qCDebug(KSCREENLOCKER) << "Unknown toplevel for ConfigureNotify";
0362             }
0363             // kDebug() << "ConfigureNotify:";
0364             // the stacking order changed, so let's change the stacking order again to what we want
0365             stayOnTop();
0366             ret = true;
0367         }
0368         break;
0369     }
0370     case XCB_MAP_NOTIFY: { // from SubstructureNotifyMask on the root window
0371         xcb_map_notify_event_t *xm = reinterpret_cast<xcb_map_notify_event_t *>(event);
0372         if (xm->event == QX11Info::appRootWindow()) {
0373             qCDebug(KSCREENLOCKER) << "MapNotify:" << xm->window;
0374             int index = findWindowInfo(xm->window);
0375             if (index >= 0) {
0376                 m_windowInfo[index].viewable = true;
0377             } else {
0378                 qCDebug(KSCREENLOCKER) << "Unknown toplevel for MapNotify";
0379             }
0380             if (m_allowedWindows.contains(xm->window)) {
0381                 if (m_lockWindows.contains(xm->window)) {
0382                     qCDebug(KSCREENLOCKER) << "uhoh! duplicate!";
0383                 } else {
0384                     if (!m_background->isVisible()) {
0385                         // not yet shown and we have a lock window, so we show our own window
0386                         m_background->show();
0387                     }
0388                     m_lockWindows.prepend(xm->window);
0389                     fakeFocusIn(xm->window);
0390                 }
0391             }
0392             if (xm->window == m_background->winId()) {
0393                 m_background->update();
0394                 Q_EMIT lockWindowShown();
0395                 return false;
0396             }
0397             stayOnTop();
0398             ret = true;
0399         }
0400         break;
0401     }
0402     case XCB_UNMAP_NOTIFY: {
0403         xcb_unmap_notify_event_t *xu = reinterpret_cast<xcb_unmap_notify_event_t *>(event);
0404         if (xu->event == QX11Info::appRootWindow()) {
0405             qCDebug(KSCREENLOCKER) << "UnmapNotify:" << xu->window;
0406             int index = findWindowInfo(xu->window);
0407             if (index >= 0) {
0408                 m_windowInfo[index].viewable = false;
0409             } else {
0410                 qCDebug(KSCREENLOCKER) << "Unknown toplevel for MapNotify";
0411             }
0412             m_lockWindows.removeAll(xu->window);
0413             if (m_focusedLockWindow == xu->event && !m_lockWindows.empty()) {
0414                 // The currently focused window vanished, just focus the first one in the list
0415                 fakeFocusIn(m_lockWindows[0]);
0416             }
0417             ret = true;
0418         }
0419         break;
0420     }
0421     case XCB_CREATE_NOTIFY: {
0422         xcb_create_notify_event_t *xc = reinterpret_cast<xcb_create_notify_event_t *>(event);
0423         if (xc->parent == QX11Info::appRootWindow()) {
0424             qCDebug(KSCREENLOCKER) << "CreateNotify:" << xc->window;
0425             int index = findWindowInfo(xc->window);
0426             if (index >= 0) {
0427                 qCDebug(KSCREENLOCKER) << "Already existing toplevel for CreateNotify";
0428             } else {
0429                 WindowInfo info;
0430                 info.window = xc->window;
0431                 info.viewable = false;
0432                 m_windowInfo.append(info);
0433             }
0434             ret = true;
0435         }
0436         break;
0437     }
0438     case XCB_DESTROY_NOTIFY: {
0439         xcb_destroy_notify_event_t *xd = reinterpret_cast<xcb_destroy_notify_event_t *>(event);
0440         if (xd->event == QX11Info::appRootWindow()) {
0441             int index = findWindowInfo(xd->window);
0442             if (index >= 0) {
0443                 m_windowInfo.removeAt(index);
0444             } else {
0445                 qCDebug(KSCREENLOCKER) << "Unknown toplevel for DestroyNotify";
0446             }
0447             ret = true;
0448         }
0449         break;
0450     }
0451     case XCB_REPARENT_NOTIFY: {
0452         xcb_reparent_notify_event_t *xr = reinterpret_cast<xcb_reparent_notify_event_t *>(event);
0453         if (xr->event == QX11Info::appRootWindow() && xr->parent != QX11Info::appRootWindow()) {
0454             int index = findWindowInfo(xr->window);
0455             if (index >= 0) {
0456                 m_windowInfo.removeAt(index);
0457             } else {
0458                 qCDebug(KSCREENLOCKER) << "Unknown toplevel for ReparentNotify away";
0459             }
0460         } else if (xr->parent == QX11Info::appRootWindow()) {
0461             int index = findWindowInfo(xr->window);
0462             if (index >= 0) {
0463                 qCDebug(KSCREENLOCKER) << "Already existing toplevel for ReparentNotify";
0464             } else {
0465                 WindowInfo info;
0466                 info.window = xr->window;
0467                 info.viewable = false;
0468                 m_windowInfo.append(info);
0469             }
0470         }
0471         break;
0472     }
0473     case XCB_CIRCULATE_NOTIFY: {
0474         xcb_circulate_notify_event_t *xc = reinterpret_cast<xcb_circulate_notify_event_t *>(event);
0475         if (xc->event == QX11Info::appRootWindow()) {
0476             int index = findWindowInfo(xc->window);
0477             if (index >= 0) {
0478                 m_windowInfo.move(index, xc->place == PlaceOnTop ? m_windowInfo.size() - 1 : 0);
0479             } else {
0480                 qCDebug(KSCREENLOCKER) << "Unknown toplevel for CirculateNotify";
0481             }
0482         }
0483         break;
0484     }
0485     }
0486     return ret;
0487 }
0488 
0489 int X11Locker::findWindowInfo(Window w)
0490 {
0491     for (int i = 0; i < m_windowInfo.size(); ++i) {
0492         if (m_windowInfo[i].window == w) {
0493             return i;
0494         }
0495     }
0496     return -1;
0497 }
0498 
0499 void X11Locker::stayOnTop()
0500 {
0501     // this restacking is written in a way so that
0502     // if the stacking positions actually don't change,
0503     // all restacking operations will be no-op,
0504     // and no ConfigureNotify will be generated,
0505     // thus avoiding possible infinite loops
0506     QVector<Window> stack(m_lockWindows.count() + 1);
0507     int count = 0;
0508     for (WId w : qAsConst(m_lockWindows)) {
0509         stack[count++] = w;
0510     }
0511     // finally, the lock window
0512     stack[count++] = m_background->winId();
0513     // do the actual restacking if needed
0514     XRaiseWindow(QX11Info::display(), stack[0]);
0515     if (count > 1) {
0516         XRestackWindows(QX11Info::display(), stack.data(), count);
0517     }
0518     XFlush(QX11Info::display());
0519 }
0520 
0521 void X11Locker::updateGeo()
0522 {
0523     QRect geometry;
0524     const auto screens = QGuiApplication::screens();
0525     for (auto *screen : screens) {
0526         geometry |= screen->geometry();
0527     }
0528     m_background->setGeometry(geometry);
0529     m_background->update();
0530 }
0531 
0532 void X11Locker::addAllowedWindow(quint32 window)
0533 {
0534     m_allowedWindows << window;
0535     // test whether it's to show
0536     const int index = findWindowInfo(window);
0537     if (index == -1 || !m_windowInfo[index].viewable) {
0538         return;
0539     }
0540     if (m_lockWindows.contains(window)) {
0541         qCDebug(KSCREENLOCKER) << "uhoh! duplicate!";
0542     } else {
0543         if (!m_background->isVisible()) {
0544             // not yet shown and we have a lock window, so we show our own window
0545             m_background->show();
0546         }
0547 
0548         if (m_lockWindows.empty()) {
0549             // Make sure to focus the first window
0550             m_focusedLockWindow = XCB_WINDOW_NONE;
0551             fakeFocusIn(window);
0552         }
0553 
0554         m_lockWindows.prepend(window);
0555         stayOnTop();
0556     }
0557 }
0558 
0559 }