/* SPDX-FileCopyrightText: 2010 Andriy Rysin SPDX-License-Identifier: GPL-2.0-or-later */ #include "x11_helper.h" #include "debug.h" #include #include #include #include #include #include #include #include // more information about the limit https://bugs.freedesktop.org/show_bug.cgi?id=19501 const int X11Helper::MAX_GROUP_COUNT = 4; const char X11Helper::LEFT_VARIANT_STR[] = "("; const char X11Helper::RIGHT_VARIANT_STR[] = ")"; bool X11Helper::xkbSupported(int *xkbOpcode) { if (!QX11Info::isPlatformX11()) { return false; } // Verify the Xlib has matching XKB extension. int major = XkbMajorVersion; int minor = XkbMinorVersion; if (!XkbLibraryVersion(&major, &minor)) { qCWarning(KCM_KEYBOARD) << "Xlib XKB extension " << major << '.' << minor << " != " << XkbMajorVersion << '.' << XkbMinorVersion; return false; } // Verify the X server has matching XKB extension. int opcode_rtrn; int error_rtrn; int xkb_opcode; if (!XkbQueryExtension(QX11Info::display(), &opcode_rtrn, &xkb_opcode, &error_rtrn, &major, &minor)) { qCWarning(KCM_KEYBOARD) << "X server XKB extension " << major << '.' << minor << " != " << XkbMajorVersion << '.' << XkbMinorVersion; return false; } if (xkbOpcode != nullptr) { *xkbOpcode = xkb_opcode; } return true; } void X11Helper::switchToNextLayout() { int size = getLayoutsList().size(); // TODO: could optimize a bit as we don't need the layouts - just count int group = (X11Helper::getGroup() + 1) % size; X11Helper::setGroup(group); } void X11Helper::scrollLayouts(int delta) { int size = getLayoutsList().size(); // TODO: could optimize a bit as we don't need the layouts - just count int group = X11Helper::getGroup() + delta; group = group < 0 ? size - ((-group) % size) : group % size; X11Helper::setGroup(group); } QStringList X11Helper::getLayoutsListAsString(const QList &layoutsList) { QStringList stringList; for (const auto &layoutUnit : layoutsList) { stringList << layoutUnit.toString(); } return stringList; } bool X11Helper::setLayout(const LayoutUnit &layout) { QList currentLayouts = getLayoutsList(); int idx = currentLayouts.indexOf(layout); if (idx == -1 || idx >= X11Helper::MAX_GROUP_COUNT) { qCWarning(KCM_KEYBOARD) << "Layout" << layout.toString() << "is not found in current layout list" << getLayoutsListAsString(currentLayouts); return false; } return X11Helper::setGroup((unsigned int)idx); } bool X11Helper::setDefaultLayout() { return X11Helper::setGroup(0); } bool X11Helper::isDefaultLayout() { return X11Helper::getGroup() == 0; } LayoutUnit X11Helper::getCurrentLayout() { if (!QX11Info::isPlatformX11()) { return LayoutUnit(); } QList currentLayouts = getLayoutsList(); unsigned int group = X11Helper::getGroup(); if (group < static_cast(currentLayouts.size())) return currentLayouts.at(static_cast(group)); qCWarning(KCM_KEYBOARD) << "Current group number" << group << "is outside of current layout list" << getLayoutsListAsString(currentLayouts); return LayoutUnit(); } LayoutSet X11Helper::getCurrentLayouts() { LayoutSet layoutSet; QList currentLayouts = getLayoutsList(); layoutSet.layouts = currentLayouts; unsigned int group = X11Helper::getGroup(); if (group < (unsigned int)currentLayouts.size()) { layoutSet.currentLayout = currentLayouts[group]; } else { qCWarning(KCM_KEYBOARD) << "Current group number" << group << "is outside of current layout list" << getLayoutsListAsString(currentLayouts); layoutSet.currentLayout = LayoutUnit(); } return layoutSet; } QList X11Helper::getLayoutsList() { if (!QX11Info::isPlatformX11()) { return QList(); } XkbConfig xkbConfig; QList layouts; if (X11Helper::getGroupNames(QX11Info::display(), &xkbConfig, X11Helper::LAYOUTS_ONLY)) { for (int i = 0; i < xkbConfig.layouts.size(); i++) { QString layout(xkbConfig.layouts[i]); QString variant; if (i < xkbConfig.variants.size() && !xkbConfig.variants[i].isEmpty()) { variant = xkbConfig.variants[i]; } layouts << LayoutUnit(layout, variant); } } else { qCWarning(KCM_KEYBOARD) << "Failed to get layout groups from X server"; } return layouts; } bool X11Helper::setGroup(unsigned int group) { qCDebug(KCM_KEYBOARD) << group; xcb_void_cookie_t cookie; cookie = xcb_xkb_latch_lock_state(QX11Info::connection(), XCB_XKB_ID_USE_CORE_KBD, 0, 0, 1, group, 0, 0, 0); xcb_generic_error_t *error = nullptr; error = xcb_request_check(QX11Info::connection(), cookie); if (error) { qCDebug(KCM_KEYBOARD) << "Couldn't change the group" << error->error_code; return false; } return true; } unsigned int X11Helper::getGroup() { XkbStateRec xkbState; XkbGetState(QX11Info::display(), XkbUseCoreKbd, &xkbState); return xkbState.group; } bool X11Helper::getGroupNames(Display *display, XkbConfig *xkbConfig, FetchType fetchType) { static const char OPTIONS_SEPARATOR[] = ","; Atom real_prop_type; int fmt; unsigned long nitems, extra_bytes; char *prop_data = nullptr; Status ret; Atom rules_atom = XInternAtom(display, _XKB_RF_NAMES_PROP_ATOM, False); /* no such atom! */ if (rules_atom == None) { /* property cannot exist */ qCWarning(KCM_KEYBOARD) << "Failed to fetch layouts from server:" << "could not find the atom" << _XKB_RF_NAMES_PROP_ATOM; return false; } ret = XGetWindowProperty(display, DefaultRootWindow(display), rules_atom, 0L, _XKB_RF_NAMES_PROP_MAXLEN, False, XA_STRING, &real_prop_type, &fmt, &nitems, &extra_bytes, (unsigned char **)(void *)&prop_data); /* property not found! */ if (ret != Success) { qCWarning(KCM_KEYBOARD) << "Failed to fetch layouts from server:" << "Could not get the property"; return false; } /* has to be array of strings */ if ((extra_bytes > 0) || (real_prop_type != XA_STRING) || (fmt != 8)) { if (prop_data) XFree(prop_data); qCWarning(KCM_KEYBOARD) << "Failed to fetch layouts from server:" << "Wrong property format"; return false; } // qCDebug(KCM_KEYBOARD) << "prop_data:" << nitems << prop_data; QStringList names; for (char *p = prop_data; p - prop_data < (long)nitems && p != nullptr; p += strlen(p) + 1) { names.append(p); // qDebug() << " " << p; } if (names.count() < 4) { //{ rules, model, layouts, variants, options } XFree(prop_data); return false; } if (fetchType == ALL || fetchType == LAYOUTS_ONLY) { QStringList layouts = names[2].split(OPTIONS_SEPARATOR); QStringList variants = names[3].split(OPTIONS_SEPARATOR); for (int ii = 0; ii < layouts.count(); ii++) { xkbConfig->layouts << layouts[ii]; xkbConfig->variants << variants.value(ii); // Empty if not specified } qCDebug(KCM_KEYBOARD) << "Fetched layout groups from X server:" << "\tlayouts:" << xkbConfig->layouts << "\tvariants:" << xkbConfig->variants; } if (fetchType == ALL || fetchType == MODEL_ONLY) { xkbConfig->keyboardModel = names[1]; qCDebug(KCM_KEYBOARD) << "Fetched keyboard model from X server:" << xkbConfig->keyboardModel; } if (fetchType == ALL) { if (names.count() >= 5) { if (!names[4].isEmpty()) xkbConfig->options = names[4].split(OPTIONS_SEPARATOR); else xkbConfig->options.clear(); qCDebug(KCM_KEYBOARD) << "Fetched xkbOptions from X server:" << xkbConfig->options; } } XFree(prop_data); return true; } XEventNotifier::XEventNotifier() : xkbOpcode(-1) { if (QCoreApplication::instance() == nullptr) { qCWarning(KCM_KEYBOARD) << "Layout Widget won't work properly without QCoreApplication instance"; } } void XEventNotifier::start() { qCDebug(KCM_KEYBOARD) << "qCoreApp" << QCoreApplication::instance(); if (QCoreApplication::instance() != nullptr && X11Helper::xkbSupported(&xkbOpcode)) { registerForXkbEvents(QX11Info::display()); // start the event loop QCoreApplication::instance()->installNativeEventFilter(this); } } void XEventNotifier::stop() { if (QCoreApplication::instance() != nullptr) { // TODO: unregister // XEventNotifier::unregisterForXkbEvents(QX11Info::display()); // stop the event loop QCoreApplication::instance()->removeNativeEventFilter(this); } } bool XEventNotifier::isXkbEvent(xcb_generic_event_t *event) { // qDebug() << "event response type:" << (event->response_type & ~0x80) << xkbOpcode << ((event->response_type & ~0x80) == xkbOpcode + XkbEventCode); return (event->response_type & ~0x80) == xkbOpcode + XkbEventCode; } bool XEventNotifier::processOtherEvents(xcb_generic_event_t * /*event*/) { return true; } bool XEventNotifier::processXkbEvents(xcb_generic_event_t *event) { _xkb_event *xkbevt = reinterpret_cast<_xkb_event *>(event); if (XEventNotifier::isGroupSwitchEvent(xkbevt)) { // qDebug() << "group switch event"; Q_EMIT layoutChanged(); } else if (XEventNotifier::isLayoutSwitchEvent(xkbevt)) { // qDebug() << "layout switch event"; Q_EMIT layoutMapChanged(); } return true; } bool XEventNotifier::nativeEventFilter(const QByteArray &eventType, void *message, qintptr *) { // qDebug() << "event type:" << eventType; if (eventType == "xcb_generic_event_t") { xcb_generic_event_t *ev = static_cast(message); if (isXkbEvent(ev)) { processXkbEvents(ev); } else { processOtherEvents(ev); } } return false; } bool XEventNotifier::isGroupSwitchEvent(_xkb_event *xkbEvent) { // XkbEvent *xkbEvent = (XkbEvent*) event; #define GROUP_CHANGE_MASK (XkbGroupStateMask | XkbGroupBaseMask | XkbGroupLatchMask | XkbGroupLockMask) return xkbEvent->any.xkbType == XkbStateNotify && (xkbEvent->state_notify.changed & GROUP_CHANGE_MASK); } bool XEventNotifier::isLayoutSwitchEvent(_xkb_event *xkbEvent) { // XkbEvent *xkbEvent = (XkbEvent*) event; return //( (xkbEvent->any.xkb_type == XkbMapNotify) && (xkbEvent->map.changed & XkbKeySymsMask) ) || /* || ( (xkbEvent->any.xkb_type == XkbNamesNotify) && (xkbEvent->names.changed & XkbGroupNamesMask) || )*/ (xkbEvent->any.xkbType == XkbNewKeyboardNotify); } int XEventNotifier::registerForXkbEvents(Display *display) { int eventMask = XkbNewKeyboardNotifyMask | XkbStateNotifyMask; if (!XkbSelectEvents(display, XkbUseCoreKbd, eventMask, eventMask)) { qCWarning(KCM_KEYBOARD) << "Couldn't select desired XKB events"; return false; } return true; } static const char LAYOUT_VARIANT_SEPARATOR_PREFIX[] = "("; static const char LAYOUT_VARIANT_SEPARATOR_SUFFIX[] = ")"; static QString &stripVariantName(QString &variant) { if (variant.endsWith(LAYOUT_VARIANT_SEPARATOR_SUFFIX)) { int suffixLen = strlen(LAYOUT_VARIANT_SEPARATOR_SUFFIX); return variant.remove(variant.length() - suffixLen, suffixLen); } return variant; } LayoutUnit::LayoutUnit(const QString &fullLayoutName) { QStringList lv = fullLayoutName.split(LAYOUT_VARIANT_SEPARATOR_PREFIX); m_layout = lv[0]; m_variant = lv.size() > 1 ? stripVariantName(lv[1]) : QString(); } QString LayoutUnit::toString() const { if (m_variant.isEmpty()) return m_layout; return m_layout + LAYOUT_VARIANT_SEPARATOR_PREFIX + m_variant + LAYOUT_VARIANT_SEPARATOR_SUFFIX; } const int LayoutUnit::MAX_LABEL_LENGTH = 3;