File indexing completed on 2024-05-12 17:07:16

0001 /*
0002     SPDX-FileCopyrightText: 2010 Andriy Rysin <rysin@kde.org>
0003 
0004     SPDX-License-Identifier: GPL-2.0-or-later
0005 */
0006 
0007 #include "x11_helper.h"
0008 #include "debug.h"
0009 
0010 #include <QCoreApplication>
0011 #include <QDebug>
0012 
0013 #include <X11/X.h>
0014 #include <X11/XKBlib.h>
0015 #include <X11/Xatom.h>
0016 #include <X11/Xlib.h>
0017 #include <X11/extensions/XKBrules.h>
0018 
0019 #include <fixx11h.h>
0020 
0021 // more information about the limit https://bugs.freedesktop.org/show_bug.cgi?id=19501
0022 const int X11Helper::MAX_GROUP_COUNT = 4;
0023 
0024 const char X11Helper::LEFT_VARIANT_STR[] = "(";
0025 const char X11Helper::RIGHT_VARIANT_STR[] = ")";
0026 
0027 bool X11Helper::xkbSupported(int *xkbOpcode)
0028 {
0029     if (!QX11Info::isPlatformX11()) {
0030         return false;
0031     }
0032     // Verify the Xlib has matching XKB extension.
0033 
0034     int major = XkbMajorVersion;
0035     int minor = XkbMinorVersion;
0036 
0037     if (!XkbLibraryVersion(&major, &minor)) {
0038         qCWarning(KCM_KEYBOARD) << "Xlib XKB extension " << major << '.' << minor << " != " << XkbMajorVersion << '.' << XkbMinorVersion;
0039         return false;
0040     }
0041 
0042     // Verify the X server has matching XKB extension.
0043 
0044     int opcode_rtrn;
0045     int error_rtrn;
0046     int xkb_opcode;
0047     if (!XkbQueryExtension(QX11Info::display(), &opcode_rtrn, &xkb_opcode, &error_rtrn, &major, &minor)) {
0048         qCWarning(KCM_KEYBOARD) << "X server XKB extension " << major << '.' << minor << " != " << XkbMajorVersion << '.' << XkbMinorVersion;
0049         return false;
0050     }
0051 
0052     if (xkbOpcode != nullptr) {
0053         *xkbOpcode = xkb_opcode;
0054     }
0055 
0056     return true;
0057 }
0058 
0059 void X11Helper::switchToNextLayout()
0060 {
0061     int size = getLayoutsList().size(); // TODO: could optimize a bit as we don't need the layouts - just count
0062     int group = (X11Helper::getGroup() + 1) % size;
0063     X11Helper::setGroup(group);
0064 }
0065 
0066 void X11Helper::scrollLayouts(int delta)
0067 {
0068     int size = getLayoutsList().size(); // TODO: could optimize a bit as we don't need the layouts - just count
0069     int group = X11Helper::getGroup() + delta;
0070     group = group < 0 ? size - ((-group) % size) : group % size;
0071 
0072     X11Helper::setGroup(group);
0073 }
0074 
0075 QStringList X11Helper::getLayoutsListAsString(const QList<LayoutUnit> &layoutsList)
0076 {
0077     QStringList stringList;
0078     for (const auto &layoutUnit : layoutsList) {
0079         stringList << layoutUnit.toString();
0080     }
0081     return stringList;
0082 }
0083 
0084 bool X11Helper::setLayout(const LayoutUnit &layout)
0085 {
0086     QList<LayoutUnit> currentLayouts = getLayoutsList();
0087     int idx = currentLayouts.indexOf(layout);
0088     if (idx == -1 || idx >= X11Helper::MAX_GROUP_COUNT) {
0089         qCWarning(KCM_KEYBOARD) << "Layout" << layout.toString() << "is not found in current layout list" << getLayoutsListAsString(currentLayouts);
0090         return false;
0091     }
0092 
0093     return X11Helper::setGroup((unsigned int)idx);
0094 }
0095 
0096 bool X11Helper::setDefaultLayout()
0097 {
0098     return X11Helper::setGroup(0);
0099 }
0100 
0101 bool X11Helper::isDefaultLayout()
0102 {
0103     return X11Helper::getGroup() == 0;
0104 }
0105 
0106 LayoutUnit X11Helper::getCurrentLayout()
0107 {
0108     if (!QX11Info::isPlatformX11()) {
0109         return LayoutUnit();
0110     }
0111     QList<LayoutUnit> currentLayouts = getLayoutsList();
0112     unsigned int group = X11Helper::getGroup();
0113     if (group < static_cast<unsigned int>(currentLayouts.size()))
0114         return currentLayouts.at(static_cast<int>(group));
0115 
0116     qCWarning(KCM_KEYBOARD) << "Current group number" << group << "is outside of current layout list" << getLayoutsListAsString(currentLayouts);
0117     return LayoutUnit();
0118 }
0119 
0120 LayoutSet X11Helper::getCurrentLayouts()
0121 {
0122     LayoutSet layoutSet;
0123 
0124     QList<LayoutUnit> currentLayouts = getLayoutsList();
0125     layoutSet.layouts = currentLayouts;
0126 
0127     unsigned int group = X11Helper::getGroup();
0128     if (group < (unsigned int)currentLayouts.size()) {
0129         layoutSet.currentLayout = currentLayouts[group];
0130     } else {
0131         qCWarning(KCM_KEYBOARD) << "Current group number" << group << "is outside of current layout list" << getLayoutsListAsString(currentLayouts);
0132         layoutSet.currentLayout = LayoutUnit();
0133     }
0134 
0135     return layoutSet;
0136 }
0137 
0138 QList<LayoutUnit> X11Helper::getLayoutsList()
0139 {
0140     if (!QX11Info::isPlatformX11()) {
0141         return QList<LayoutUnit>();
0142     }
0143     XkbConfig xkbConfig;
0144     QList<LayoutUnit> layouts;
0145     if (X11Helper::getGroupNames(QX11Info::display(), &xkbConfig, X11Helper::LAYOUTS_ONLY)) {
0146         for (int i = 0; i < xkbConfig.layouts.size(); i++) {
0147             QString layout(xkbConfig.layouts[i]);
0148             QString variant;
0149             if (i < xkbConfig.variants.size() && !xkbConfig.variants[i].isEmpty()) {
0150                 variant = xkbConfig.variants[i];
0151             }
0152             layouts << LayoutUnit(layout, variant);
0153         }
0154     } else {
0155         qCWarning(KCM_KEYBOARD) << "Failed to get layout groups from X server";
0156     }
0157     return layouts;
0158 }
0159 
0160 bool X11Helper::setGroup(unsigned int group)
0161 {
0162     qCDebug(KCM_KEYBOARD) << group;
0163     xcb_void_cookie_t cookie;
0164     cookie = xcb_xkb_latch_lock_state(QX11Info::connection(), XCB_XKB_ID_USE_CORE_KBD, 0, 0, 1, group, 0, 0, 0);
0165     xcb_generic_error_t *error = nullptr;
0166     error = xcb_request_check(QX11Info::connection(), cookie);
0167     if (error) {
0168         qCDebug(KCM_KEYBOARD) << "Couldn't change the group" << error->error_code;
0169         return false;
0170     }
0171 
0172     return true;
0173 }
0174 
0175 unsigned int X11Helper::getGroup()
0176 {
0177     XkbStateRec xkbState;
0178     XkbGetState(QX11Info::display(), XkbUseCoreKbd, &xkbState);
0179     return xkbState.group;
0180 }
0181 
0182 bool X11Helper::getGroupNames(Display *display, XkbConfig *xkbConfig, FetchType fetchType)
0183 {
0184     static const char OPTIONS_SEPARATOR[] = ",";
0185 
0186     Atom real_prop_type;
0187     int fmt;
0188     unsigned long nitems, extra_bytes;
0189     char *prop_data = nullptr;
0190     Status ret;
0191 
0192     Atom rules_atom = XInternAtom(display, _XKB_RF_NAMES_PROP_ATOM, False);
0193 
0194     /* no such atom! */
0195     if (rules_atom == None) { /* property cannot exist */
0196         qCWarning(KCM_KEYBOARD) << "Failed to fetch layouts from server:"
0197                                 << "could not find the atom" << _XKB_RF_NAMES_PROP_ATOM;
0198         return false;
0199     }
0200 
0201     ret = XGetWindowProperty(display,
0202                              DefaultRootWindow(display),
0203                              rules_atom,
0204                              0L,
0205                              _XKB_RF_NAMES_PROP_MAXLEN,
0206                              False,
0207                              XA_STRING,
0208                              &real_prop_type,
0209                              &fmt,
0210                              &nitems,
0211                              &extra_bytes,
0212                              (unsigned char **)(void *)&prop_data);
0213 
0214     /* property not found! */
0215     if (ret != Success) {
0216         qCWarning(KCM_KEYBOARD) << "Failed to fetch layouts from server:"
0217                                 << "Could not get the property";
0218         return false;
0219     }
0220 
0221     /* has to be array of strings */
0222     if ((extra_bytes > 0) || (real_prop_type != XA_STRING) || (fmt != 8)) {
0223         if (prop_data)
0224             XFree(prop_data);
0225         qCWarning(KCM_KEYBOARD) << "Failed to fetch layouts from server:"
0226                                 << "Wrong property format";
0227         return false;
0228     }
0229 
0230     //  qCDebug(KCM_KEYBOARD) << "prop_data:" << nitems << prop_data;
0231     QStringList names;
0232     for (char *p = prop_data; p - prop_data < (long)nitems && p != nullptr; p += strlen(p) + 1) {
0233         names.append(p);
0234         //      qDebug() << " " << p;
0235     }
0236 
0237     if (names.count() < 4) { //{ rules, model, layouts, variants, options }
0238         XFree(prop_data);
0239         return false;
0240     }
0241 
0242     if (fetchType == ALL || fetchType == LAYOUTS_ONLY) {
0243         QStringList layouts = names[2].split(OPTIONS_SEPARATOR);
0244         QStringList variants = names[3].split(OPTIONS_SEPARATOR);
0245 
0246         for (int ii = 0; ii < layouts.count(); ii++) {
0247             xkbConfig->layouts << layouts[ii];
0248             xkbConfig->variants << variants.value(ii); // Empty if not specified
0249         }
0250         qCDebug(KCM_KEYBOARD) << "Fetched layout groups from X server:"
0251                               << "\tlayouts:" << xkbConfig->layouts << "\tvariants:" << xkbConfig->variants;
0252     }
0253 
0254     if (fetchType == ALL || fetchType == MODEL_ONLY) {
0255         xkbConfig->keyboardModel = names[1];
0256         qCDebug(KCM_KEYBOARD) << "Fetched keyboard model from X server:" << xkbConfig->keyboardModel;
0257     }
0258 
0259     if (fetchType == ALL) {
0260         if (names.count() >= 5) {
0261             if (!names[4].isEmpty())
0262                 xkbConfig->options = names[4].split(OPTIONS_SEPARATOR);
0263             else
0264                 xkbConfig->options.clear();
0265 
0266             qCDebug(KCM_KEYBOARD) << "Fetched xkbOptions from X server:" << xkbConfig->options;
0267         }
0268     }
0269 
0270     XFree(prop_data);
0271     return true;
0272 }
0273 
0274 XEventNotifier::XEventNotifier()
0275     : xkbOpcode(-1)
0276 {
0277     if (QCoreApplication::instance() == nullptr) {
0278         qCWarning(KCM_KEYBOARD) << "Layout Widget won't work properly without QCoreApplication instance";
0279     }
0280 }
0281 
0282 void XEventNotifier::start()
0283 {
0284     qCDebug(KCM_KEYBOARD) << "qCoreApp" << QCoreApplication::instance();
0285     if (QCoreApplication::instance() != nullptr && X11Helper::xkbSupported(&xkbOpcode)) {
0286         registerForXkbEvents(QX11Info::display());
0287 
0288         // start the event loop
0289         QCoreApplication::instance()->installNativeEventFilter(this);
0290     }
0291 }
0292 
0293 void XEventNotifier::stop()
0294 {
0295     if (QCoreApplication::instance() != nullptr) {
0296         // TODO: unregister
0297         //    XEventNotifier::unregisterForXkbEvents(QX11Info::display());
0298 
0299         // stop the event loop
0300         QCoreApplication::instance()->removeNativeEventFilter(this);
0301     }
0302 }
0303 
0304 bool XEventNotifier::isXkbEvent(xcb_generic_event_t *event)
0305 {
0306     //  qDebug() << "event response type:" << (event->response_type & ~0x80) << xkbOpcode << ((event->response_type & ~0x80) == xkbOpcode + XkbEventCode);
0307     return (event->response_type & ~0x80) == xkbOpcode + XkbEventCode;
0308 }
0309 
0310 bool XEventNotifier::processOtherEvents(xcb_generic_event_t * /*event*/)
0311 {
0312     return true;
0313 }
0314 
0315 bool XEventNotifier::processXkbEvents(xcb_generic_event_t *event)
0316 {
0317     _xkb_event *xkbevt = reinterpret_cast<_xkb_event *>(event);
0318     if (XEventNotifier::isGroupSwitchEvent(xkbevt)) {
0319         //      qDebug() << "group switch event";
0320         Q_EMIT layoutChanged();
0321     } else if (XEventNotifier::isLayoutSwitchEvent(xkbevt)) {
0322         //      qDebug() << "layout switch event";
0323         Q_EMIT layoutMapChanged();
0324     }
0325     return true;
0326 }
0327 
0328 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
0329 bool XEventNotifier::nativeEventFilter(const QByteArray &eventType, void *message, long *)
0330 #else
0331 bool XEventNotifier::nativeEventFilter(const QByteArray &eventType, void *message, qintptr *)
0332 #endif
0333 {
0334     //  qDebug() << "event type:" << eventType;
0335     if (eventType == "xcb_generic_event_t") {
0336         xcb_generic_event_t *ev = static_cast<xcb_generic_event_t *>(message);
0337         if (isXkbEvent(ev)) {
0338             processXkbEvents(ev);
0339         } else {
0340             processOtherEvents(ev);
0341         }
0342     }
0343     return false;
0344 }
0345 
0346 bool XEventNotifier::isGroupSwitchEvent(_xkb_event *xkbEvent)
0347 {
0348 //    XkbEvent *xkbEvent = (XkbEvent*) event;
0349 #define GROUP_CHANGE_MASK (XkbGroupStateMask | XkbGroupBaseMask | XkbGroupLatchMask | XkbGroupLockMask)
0350 
0351     return xkbEvent->any.xkbType == XkbStateNotify && (xkbEvent->state_notify.changed & GROUP_CHANGE_MASK);
0352 }
0353 
0354 bool XEventNotifier::isLayoutSwitchEvent(_xkb_event *xkbEvent)
0355 {
0356     //    XkbEvent *xkbEvent = (XkbEvent*) event;
0357 
0358     return //( (xkbEvent->any.xkb_type == XkbMapNotify) && (xkbEvent->map.changed & XkbKeySymsMask) ) ||
0359         /*        || ( (xkbEvent->any.xkb_type == XkbNamesNotify) && (xkbEvent->names.changed & XkbGroupNamesMask) || )*/
0360         (xkbEvent->any.xkbType == XkbNewKeyboardNotify);
0361 }
0362 
0363 int XEventNotifier::registerForXkbEvents(Display *display)
0364 {
0365     int eventMask = XkbNewKeyboardNotifyMask | XkbStateNotifyMask;
0366     if (!XkbSelectEvents(display, XkbUseCoreKbd, eventMask, eventMask)) {
0367         qCWarning(KCM_KEYBOARD) << "Couldn't select desired XKB events";
0368         return false;
0369     }
0370     return true;
0371 }
0372 
0373 static const char LAYOUT_VARIANT_SEPARATOR_PREFIX[] = "(";
0374 static const char LAYOUT_VARIANT_SEPARATOR_SUFFIX[] = ")";
0375 
0376 static QString &stripVariantName(QString &variant)
0377 {
0378     if (variant.endsWith(LAYOUT_VARIANT_SEPARATOR_SUFFIX)) {
0379         int suffixLen = strlen(LAYOUT_VARIANT_SEPARATOR_SUFFIX);
0380         return variant.remove(variant.length() - suffixLen, suffixLen);
0381     }
0382     return variant;
0383 }
0384 
0385 LayoutUnit::LayoutUnit(const QString &fullLayoutName)
0386 {
0387     QStringList lv = fullLayoutName.split(LAYOUT_VARIANT_SEPARATOR_PREFIX);
0388     m_layout = lv[0];
0389     m_variant = lv.size() > 1 ? stripVariantName(lv[1]) : QString();
0390 }
0391 
0392 QString LayoutUnit::toString() const
0393 {
0394     if (m_variant.isEmpty())
0395         return m_layout;
0396 
0397     return m_layout + LAYOUT_VARIANT_SEPARATOR_PREFIX + m_variant + LAYOUT_VARIANT_SEPARATOR_SUFFIX;
0398 }
0399 
0400 const int LayoutUnit::MAX_LABEL_LENGTH = 3;