File indexing completed on 2022-09-20 12:22:40

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