File indexing completed on 2024-04-21 05:27:35

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