🏡 index : quine-global-labs/plasma-desktop.git

/*
    SPDX-FileCopyrightText: 2000 Matthias Hölzer-Klüpfel <hoelzer@kde.org>
    SPDX-FileCopyrightText: 2014 Frederik Gladhorn <gladhorn@kde.org>

    SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
*/

#include <cmath>
#include <unistd.h>

#include "kaccess.h"

#include <QDesktopWidget>
#include <QMessageBox>
#include <QPainter>
#include <QProcess>
#include <QTimer>

#include <QAction>
#include <QApplication>
#include <QHBoxLayout>
#include <QVBoxLayout>

#include <KAboutData>
#include <KComboBox>
#include <KConfig>
#include <KConfigGroup>
#include <KDBusService>
#include <KGlobalAccel>
#include <KKeyServer>
#include <KLocalizedString>
#include <KNotification>
#include <KSharedConfig>
#include <KUserTimestamp>
#include <KWindowSystem>
#include <QDialog>
#include <QDialogButtonBox>

#include <netwm.h>
#define XK_MISCELLANY
#define XK_XKB_KEYS
#include <X11/keysymdef.h>

#include <QX11Info>

#include <QLoggingCategory>

Q_LOGGING_CATEGORY(logKAccess, "kcm_kaccess")

struct ModifierKey {
    const unsigned int mask;
    const KeySym keysym;
    const char *name;
    const char *lockedText;
    const char *latchedText;
    const char *unlatchedText;
};

static const ModifierKey modifierKeys[] = {
    {ShiftMask,
     0,
     "Shift",
     I18N_NOOP("The Shift key has been locked and is now active for all of the following keypresses."),
     I18N_NOOP("The Shift key is now active."),
     I18N_NOOP("The Shift key is now inactive.")},
    {ControlMask,
     0,
     "Control",
     I18N_NOOP("The Control key has been locked and is now active for all of the following keypresses."),
     I18N_NOOP("The Control key is now active."),
     I18N_NOOP("The Control key is now inactive.")},
    {0,
     XK_Alt_L,
     "Alt",
     I18N_NOOP("The Alt key has been locked and is now active for all of the following keypresses."),
     I18N_NOOP("The Alt key is now active."),
     I18N_NOOP("The Alt key is now inactive.")},
    {0,
     0,
     "Win",
     I18N_NOOP("The Win key has been locked and is now active for all of the following keypresses."),
     I18N_NOOP("The Win key is now active."),
     I18N_NOOP("The Win key is now inactive.")},
    {0,
     XK_Meta_L,
     "Meta",
     I18N_NOOP("The Meta key has been locked and is now active for all of the following keypresses."),
     I18N_NOOP("The Meta key is now active."),
     I18N_NOOP("The Meta key is now inactive.")},
    {0,
     XK_Super_L,
     "Super",
     I18N_NOOP("The Super key has been locked and is now active for all of the following keypresses."),
     I18N_NOOP("The Super key is now active."),
     I18N_NOOP("The Super key is now inactive.")},
    {0,
     XK_Hyper_L,
     "Hyper",
     I18N_NOOP("The Hyper key has been locked and is now active for all of the following keypresses."),
     I18N_NOOP("The Hyper key is now active."),
     I18N_NOOP("The Hyper key is now inactive.")},
    {0,
     0,
     "Alt Graph",
     I18N_NOOP("The Alt Graph key has been locked and is now active for all of the following keypresses."),
     I18N_NOOP("The Alt Graph key is now active."),
     I18N_NOOP("The Alt Graph key is now inactive.")},
    {0, XK_Num_Lock, "Num Lock", I18N_NOOP("The Num Lock key has been activated."), "", I18N_NOOP("The Num Lock key is now inactive.")},
    {LockMask, 0, "Caps Lock", I18N_NOOP("The Caps Lock key has been activated."), "", I18N_NOOP("The Caps Lock key is now inactive.")},
    {0, XK_Scroll_Lock, "Scroll Lock", I18N_NOOP("The Scroll Lock key has been activated."), "", I18N_NOOP("The Scroll Lock key is now inactive.")},
    {0, 0, "", "", "", ""}};

/********************************************************************/

KAccessApp::KAccessApp()
    : overlay(nullptr)
    , _player(nullptr)
    , toggleScreenReaderAction(new QAction(this))
{
    m_error = false;
    _activeWindow = KWindowSystem::activeWindow();
    connect(KWindowSystem::self(), &KWindowSystem::activeWindowChanged, this, &KAccessApp::activeWindowChanged);

    features = 0;
    requestedFeatures = 0;
    dialog = nullptr;

    if (!QX11Info::isPlatformX11()) {
        m_error = true;
        return;
    }

    initMasks();
    XkbStateRec state_return;
    XkbGetState(QX11Info::display(), XkbUseCoreKbd, &state_return);
    unsigned char latched = XkbStateMods(&state_return);
    unsigned char locked = XkbModLocks(&state_return);
    state = ((int)locked) << 8 | latched;

    auto service = new KDBusService(KDBusService::Unique, this);
    connect(service, &KDBusService::activateRequested, this, &KAccessApp::newInstance);

    QTimer::singleShot(0, this, &KAccessApp::readSettings);
}

void KAccessApp::newInstance()
{
    KSharedConfig::openConfig()->reparseConfiguration();
    readSettings();
}

void KAccessApp::readSettings()
{
    KSharedConfig::Ptr _config = KSharedConfig::openConfig();
    KConfigGroup cg(_config, "Bell");

    // bell
    _systemBell = cg.readEntry("SystemBell", true);
    _artsBell = cg.readEntry("ArtsBell", false);
    _currentPlayerSource = cg.readPathEntry("ArtsBellFile", QString());
    _visibleBell = cg.readEntry("VisibleBell", false);
    _visibleBellInvert = cg.readEntry("VisibleBellInvert", false);
    _visibleBellColor = cg.readEntry("VisibleBellColor", QColor(Qt::red));
    _visibleBellPause = cg.readEntry("VisibleBellPause", 500);

    // select bell events if we need them
    int state = (_artsBell || _visibleBell) ? XkbBellNotifyMask : 0;
    XkbSelectEvents(QX11Info::display(), XkbUseCoreKbd, XkbBellNotifyMask, state);

    // deactivate system bell if not needed
    if (!_systemBell)
        XkbChangeEnabledControls(QX11Info::display(), XkbUseCoreKbd, XkbAudibleBellMask, 0);
    else
        XkbChangeEnabledControls(QX11Info::display(), XkbUseCoreKbd, XkbAudibleBellMask, XkbAudibleBellMask);

    // keyboard
    KConfigGroup keyboardGroup(_config, "Keyboard");

    // get keyboard state
    XkbDescPtr xkb = XkbGetMap(QX11Info::display(), 0, XkbUseCoreKbd);
    if (!xkb)
        return;
    if (XkbGetControls(QX11Info::display(), XkbAllControlsMask, xkb) != Success)
        return;

    // sticky keys
    if (keyboardGroup.readEntry("StickyKeys", false)) {
        if (keyboardGroup.readEntry("StickyKeysLatch", true))
            xkb->ctrls->ax_options |= XkbAX_LatchToLockMask;
        else
            xkb->ctrls->ax_options &= ~XkbAX_LatchToLockMask;
        if (keyboardGroup.readEntry("StickyKeysAutoOff", false))
            xkb->ctrls->ax_options |= XkbAX_TwoKeysMask;
        else
            xkb->ctrls->ax_options &= ~XkbAX_TwoKeysMask;
        if (keyboardGroup.readEntry("StickyKeysBeep", false))
            xkb->ctrls->ax_options |= XkbAX_StickyKeysFBMask;
        else
            xkb->ctrls->ax_options &= ~XkbAX_StickyKeysFBMask;
        xkb->ctrls->enabled_ctrls |= XkbStickyKeysMask;
    } else
        xkb->ctrls->enabled_ctrls &= ~XkbStickyKeysMask;

    // toggle keys
    if (keyboardGroup.readEntry("ToggleKeysBeep", false))
        xkb->ctrls->ax_options |= XkbAX_IndicatorFBMask;
    else
        xkb->ctrls->ax_options &= ~XkbAX_IndicatorFBMask;

    // slow keys
    if (keyboardGroup.readEntry("SlowKeys", false)) {
        if (keyboardGroup.readEntry("SlowKeysPressBeep", false))
            xkb->ctrls->ax_options |= XkbAX_SKPressFBMask;
        else
            xkb->ctrls->ax_options &= ~XkbAX_SKPressFBMask;
        if (keyboardGroup.readEntry("SlowKeysAcceptBeep", false))
            xkb->ctrls->ax_options |= XkbAX_SKAcceptFBMask;
        else
            xkb->ctrls->ax_options &= ~XkbAX_SKAcceptFBMask;
        if (keyboardGroup.readEntry("SlowKeysRejectBeep", false))
            xkb->ctrls->ax_options |= XkbAX_SKRejectFBMask;
        else
            xkb->ctrls->ax_options &= ~XkbAX_SKRejectFBMask;
        xkb->ctrls->enabled_ctrls |= XkbSlowKeysMask;
    } else
        xkb->ctrls->enabled_ctrls &= ~XkbSlowKeysMask;
    xkb->ctrls->slow_keys_delay = keyboardGroup.readEntry("SlowKeysDelay", 500);

    // bounce keys
    if (keyboardGroup.readEntry("BounceKeys", false)) {
        if (keyboardGroup.readEntry("BounceKeysRejectBeep", false))
            xkb->ctrls->ax_options |= XkbAX_BKRejectFBMask;
        else
            xkb->ctrls->ax_options &= ~XkbAX_BKRejectFBMask;
        xkb->ctrls->enabled_ctrls |= XkbBounceKeysMask;
    } else
        xkb->ctrls->enabled_ctrls &= ~XkbBounceKeysMask;
    xkb->ctrls->debounce_delay = keyboardGroup.readEntry("BounceKeysDelay", 500);

    // gestures for enabling the other features
    _gestures = keyboardGroup.readEntry("Gestures", false);
    if (_gestures)
        xkb->ctrls->enabled_ctrls |= XkbAccessXKeysMask;
    else
        xkb->ctrls->enabled_ctrls &= ~XkbAccessXKeysMask;

    // timeout
    if (keyboardGroup.readEntry("AccessXTimeout", false)) {
        xkb->ctrls->ax_timeout = keyboardGroup.readEntry("AccessXTimeoutDelay", 30) * 60;
        xkb->ctrls->axt_opts_mask = 0;
        xkb->ctrls->axt_opts_values = 0;
        xkb->ctrls->axt_ctrls_mask = XkbStickyKeysMask | XkbSlowKeysMask;
        xkb->ctrls->axt_ctrls_values = 0;
        xkb->ctrls->enabled_ctrls |= XkbAccessXTimeoutMask;
    } else
        xkb->ctrls->enabled_ctrls &= ~XkbAccessXTimeoutMask;

    // gestures for enabling the other features
    if (keyboardGroup.readEntry("AccessXBeep", true))
        xkb->ctrls->ax_options |= XkbAX_FeatureFBMask | XkbAX_SlowWarnFBMask;
    else
        xkb->ctrls->ax_options &= ~(XkbAX_FeatureFBMask | XkbAX_SlowWarnFBMask);

    _gestureConfirmation = keyboardGroup.readEntry("GestureConfirmation", false);

    _kNotifyModifiers = keyboardGroup.readEntry("kNotifyModifiers", false);
    _kNotifyAccessX = keyboardGroup.readEntry("kNotifyAccessX", false);

    // mouse-by-keyboard

    KConfigGroup mouseGroup(_config, "Mouse");

    if (mouseGroup.readEntry("MouseKeys", false)) {
        xkb->ctrls->mk_delay = mouseGroup.readEntry("MKDelay", 160);

        // Default for initial velocity: 200 pixels/sec
        int interval = mouseGroup.readEntry("MKInterval", 5);
        xkb->ctrls->mk_interval = interval;

        // Default time to reach maximum speed: 5000 msec
        xkb->ctrls->mk_time_to_max = mouseGroup.readEntry("MKTimeToMax", (5000 + interval / 2) / interval);

        // Default maximum speed: 1000 pixels/sec
        //     (The old default maximum speed from KDE <= 3.4
        //     (100000 pixels/sec) was way too fast)
        xkb->ctrls->mk_max_speed = mouseGroup.readEntry("MKMaxSpeed", interval);

        xkb->ctrls->mk_curve = mouseGroup.readEntry("MKCurve", 0);
        xkb->ctrls->mk_dflt_btn = mouseGroup.readEntry("MKDefaultButton", 0);

        xkb->ctrls->enabled_ctrls |= XkbMouseKeysMask;
    } else
        xkb->ctrls->enabled_ctrls &= ~XkbMouseKeysMask;

    features = xkb->ctrls->enabled_ctrls & (XkbSlowKeysMask | XkbBounceKeysMask | XkbStickyKeysMask | XkbMouseKeysMask);
    if (dialog == nullptr)
        requestedFeatures = features;
    // set state
    XkbSetControls(QX11Info::display(),
                   XkbControlsEnabledMask | XkbMouseKeysAccelMask | XkbStickyKeysMask | XkbSlowKeysMask | XkbBounceKeysMask | XkbAccessXKeysMask
                       | XkbAccessXTimeoutMask,
                   xkb);

    // select AccessX events
    XkbSelectEvents(QX11Info::display(), XkbUseCoreKbd, XkbAllEventsMask, XkbAllEventsMask);

    if (!_artsBell && !_visibleBell && !(_gestures && _gestureConfirmation) && !_kNotifyModifiers && !_kNotifyAccessX) {
        uint ctrls = XkbStickyKeysMask | XkbSlowKeysMask | XkbBounceKeysMask | XkbMouseKeysMask | XkbAudibleBellMask | XkbControlsNotifyMask;
        uint values = xkb->ctrls->enabled_ctrls & ctrls;
        XkbSetAutoResetControls(QX11Info::display(), ctrls, &ctrls, &values);
    } else {
        // reset them after program exit
        uint ctrls = XkbStickyKeysMask | XkbSlowKeysMask | XkbBounceKeysMask | XkbMouseKeysMask | XkbAudibleBellMask | XkbControlsNotifyMask;
        uint values = XkbAudibleBellMask;
        XkbSetAutoResetControls(QX11Info::display(), ctrls, &ctrls, &values);
    }

    delete overlay;
    overlay = nullptr;

    KConfigGroup screenReaderGroup(_config, "ScreenReader");
    setScreenReaderEnabled(screenReaderGroup.readEntry("Enabled", false));

    QString shortcut = screenReaderGroup.readEntry("Shortcut", QStringLiteral("Meta+Alt+S"));

    toggleScreenReaderAction->setText(i18n("Toggle Screen Reader On and Off"));
    toggleScreenReaderAction->setObjectName(QStringLiteral("Toggle Screen Reader On and Off"));
    toggleScreenReaderAction->setProperty("componentDisplayName", i18nc("Name for kaccess shortcuts category", "Accessibility"));
    KGlobalAccel::self()->setGlobalShortcut(toggleScreenReaderAction, QKeySequence(shortcut));
    connect(toggleScreenReaderAction, &QAction::triggered, this, &KAccessApp::toggleScreenReader);
}

void KAccessApp::toggleScreenReader()
{
    KSharedConfig::Ptr _config = KSharedConfig::openConfig();
    KConfigGroup screenReaderGroup(_config, "ScreenReader");
    bool enabled = !screenReaderGroup.readEntry("Enabled", false);
    screenReaderGroup.writeEntry("Enabled", enabled);
    setScreenReaderEnabled(enabled);
}

void KAccessApp::setScreenReaderEnabled(bool enabled)
{
    if (enabled) {
        QStringList args = {QStringLiteral("set"),
                            QStringLiteral("org.gnome.desktop.a11y.applications"),
                            QStringLiteral("screen-reader-enabled"),
                            QStringLiteral("true")};
        int ret = QProcess::execute(QStringLiteral("gsettings"), args);
        if (ret == 0) {
            qint64 pid = 0;
            QProcess::startDetached(QStringLiteral("orca"), {QStringLiteral("--replace")}, QString(), &pid);
            qCDebug(logKAccess) << "Launching Orca, pid:" << pid;
        }
    } else {
        QProcess::startDetached(
            QStringLiteral("gsettings"),
            {QStringLiteral("set"), QStringLiteral("org.gnome.desktop.a11y.applications"), QStringLiteral("screen-reader-enabled"), QStringLiteral("false")});
    }
}

static int maskToBit(int mask)
{
    for (int i = 0; i < 8; i++)
        if (mask & (1 << i))
            return i;
    return -1;
}

void KAccessApp::initMasks()
{
    for (int i = 0; i < 8; i++)
        keys[i] = -1;
    state = 0;

    for (int i = 0; strcmp(modifierKeys[i].name, "") != 0; i++) {
        int mask = modifierKeys[i].mask;
        if (mask == 0) {
            if (modifierKeys[i].keysym != 0) {
                mask = XkbKeysymToModifiers(QX11Info::display(), modifierKeys[i].keysym);
            } else {
                if (!strcmp(modifierKeys[i].name, "Win")) {
                    mask = KKeyServer::modXMeta();
                } else {
                    mask = XkbKeysymToModifiers(QX11Info::display(), XK_Mode_switch) | XkbKeysymToModifiers(QX11Info::display(), XK_ISO_Level3_Shift)
                        | XkbKeysymToModifiers(QX11Info::display(), XK_ISO_Level3_Latch) | XkbKeysymToModifiers(QX11Info::display(), XK_ISO_Level3_Lock);
                }
            }
        }

        int bit = maskToBit(mask);
        if (bit != -1 && keys[bit] == -1)
            keys[bit] = i;
    }
}

struct xkb_any_ {
    uint8_t response_type;
    uint8_t xkbType;
    uint16_t sequence;
    xcb_timestamp_t time;
    uint8_t deviceID;
};

bool KAccessApp::nativeEventFilter(const QByteArray &eventType, void *message, long int *result)
{
    if (eventType == "xcb_generic_event_t") {
        xcb_generic_event_t *event = static_cast<xcb_generic_event_t *>(message);
        if ((event->response_type & ~0x80) == XkbEventCode + xkb_opcode) {
            xkb_any_ *ev = reinterpret_cast<xkb_any_ *>(event);
            // Workaround for an XCB bug. xkbType comes from an EventType that is defined with bits, like
            // <item name="BellNotify">             <bit>8</bit>
            // while the generated XCB event type enum is defined as a bitmask, like
            //     XCB_XKB_EVENT_TYPE_BELL_NOTIFY = 256
            // This means if xbkType is 8, we need to set the 8th bit to 1, thus raising 2 to power of 8.
            // See also https://bugs.freedesktop.org/show_bug.cgi?id=51295
            const int eventType = pow(2, ev->xkbType);
            switch (eventType) {
            case XCB_XKB_EVENT_TYPE_STATE_NOTIFY:
                xkbStateNotify();
                break;
            case XCB_XKB_EVENT_TYPE_BELL_NOTIFY:
                xkbBellNotify(reinterpret_cast<xcb_xkb_bell_notify_event_t *>(event));
                break;
            case XCB_XKB_EVENT_TYPE_CONTROLS_NOTIFY:
                xkbControlsNotify(reinterpret_cast<xcb_xkb_controls_notify_event_t *>(event));
                break;
            }
            return true;
        }
    }
    return false;
}

void VisualBell::paintEvent(QPaintEvent *event)
{
    QWidget::paintEvent(event);
    QTimer::singleShot(_pause, this, &QWidget::hide);
}

void KAccessApp::activeWindowChanged(WId wid)
{
    _activeWindow = wid;
}

void KAccessApp::xkbStateNotify()
{
    XkbStateRec state_return;
    XkbGetState(QX11Info::display(), XkbUseCoreKbd, &state_return);
    unsigned char latched = XkbStateMods(&state_return);
    unsigned char locked = XkbModLocks(&state_return);
    int mods = ((int)locked) << 8 | latched;

    if (state != mods) {
        if (_kNotifyModifiers)
            for (int i = 0; i < 8; i++) {
                if (keys[i] != -1) {
                    if (!strcmp(modifierKeys[keys[i]].latchedText, "") && ((((mods >> i) & 0x101) != 0) != (((state >> i) & 0x101) != 0))) {
                        if ((mods >> i) & 1) {
                            KNotification::event(QStringLiteral("lockkey-locked"), i18n(modifierKeys[keys[i]].lockedText));
                        } else {
                            KNotification::event(QStringLiteral("lockkey-unlocked"), i18n(modifierKeys[keys[i]].unlatchedText));
                        }
                    } else if (strcmp(modifierKeys[keys[i]].latchedText, "") && (((mods >> i) & 0x101) != ((state >> i) & 0x101))) {
                        if ((mods >> i) & 0x100) {
                            KNotification::event(QStringLiteral("modifierkey-locked"), i18n(modifierKeys[keys[i]].lockedText));
                        } else if ((mods >> i) & 1) {
                            KNotification::event(QStringLiteral("modifierkey-latched"), i18n(modifierKeys[keys[i]].latchedText));
                        } else {
                            KNotification::event(QStringLiteral("modifierkey-unlatched"), i18n(modifierKeys[keys[i]].unlatchedText));
                        }
                    }
                }
            }
        state = mods;
    }
}

void KAccessApp::xkbBellNotify(xcb_xkb_bell_notify_event_t *event)
{
    // bail out if we should not really ring
    if (event->eventOnly)
        return;

    // flash the visible bell
    if (_visibleBell) {
        // create overlay widget
        if (!overlay)
            overlay = new VisualBell(_visibleBellPause);

        WId id = _activeWindow;

        NETRect frame, window;
        NETWinInfo net(QX11Info::connection(), id, qApp->desktop()->winId(), NET::Properties(), NET::Properties2());

        net.kdeGeometry(frame, window);

        overlay->setGeometry(window.pos.x, window.pos.y, window.size.width, window.size.height);

        if (_visibleBellInvert) {
            QPixmap screen = QPixmap::grabWindow(id, 0, 0, window.size.width, window.size.height);
#ifdef __GNUC__
#warning is this the best way to invert a pixmap?
#endif
            //    QPixmap invert(window.size.width, window.size.height);
            QImage i = screen.toImage();
            i.invertPixels();
            QPalette pal = overlay->palette();
            pal.setBrush(overlay->backgroundRole(), QBrush(QPixmap::fromImage(i)));
            overlay->setPalette(pal);
            /*
                  QPainter p(&invert);
                  p.setRasterOp(QPainter::NotCopyROP);
                  p.drawPixmap(0, 0, screen);
                  overlay->setBackgroundPixmap(invert);
            */
        } else {
            QPalette pal = overlay->palette();
            pal.setColor(overlay->backgroundRole(), _visibleBellColor);
            overlay->setPalette(pal);
        }

        // flash the overlay widget
        overlay->raise();
        overlay->show();
        qApp->flush();
    }

    // ask Phonon to ring a nice bell
    if (_artsBell) {
        if (!_player) { // as creating the player is expensive, delay the creation
            _player = Phonon::createPlayer(Phonon::AccessibilityCategory);
            _player->setParent(this);
            _player->setCurrentSource(_currentPlayerSource);
        }
        _player->play();
    }
}

QString mouseKeysShortcut(Display *display)
{
    // Calculate the keycode
    KeySym sym = XK_MouseKeys_Enable;
    KeyCode code = XKeysymToKeycode(display, sym);
    if (code == 0) {
        sym = XK_Pointer_EnableKeys;
        code = XKeysymToKeycode(display, sym);
        if (code == 0)
            return QString(); // No shortcut available?
    }

    // Calculate the modifiers by searching the keysym in the X keyboard mapping
    XkbDescPtr xkbdesc = XkbGetMap(display, XkbKeyTypesMask | XkbKeySymsMask, XkbUseCoreKbd);

    if (!xkbdesc)
        return QString(); // Failed to obtain the mapping from server

    bool found = false;
    unsigned char modifiers = 0;
    int groups = XkbKeyNumGroups(xkbdesc, code);
    for (int grp = 0; grp < groups && !found; grp++) {
        int levels = XkbKeyGroupWidth(xkbdesc, code, grp);
        for (int level = 0; level < levels && !found; level++) {
            if (sym == XkbKeySymEntry(xkbdesc, code, level, grp)) {
                // keysym found => determine modifiers
                int typeIdx = xkbdesc->map->key_sym_map[code].kt_index[grp];
                XkbKeyTypePtr type = &(xkbdesc->map->types[typeIdx]);
                for (int i = 0; i < type->map_count && !found; i++) {
                    if (type->map[i].active && (type->map[i].level == level)) {
                        modifiers = type->map[i].mods.mask;
                        found = true;
                    }
                }
            }
        }
    }
    XkbFreeClientMap(xkbdesc, 0, true);

    if (!found)
        return QString(); // Somehow the keycode -> keysym mapping is flawed

    XEvent ev;
    ev.type = KeyPress;
    ev.xkey.display = display;
    ev.xkey.keycode = code;
    ev.xkey.state = 0;
    int key;
    KKeyServer::xEventToQt(&ev, &key);
    QString keyname = QKeySequence(key).toString();

    unsigned int AltMask = KKeyServer::modXAlt();
    unsigned int WinMask = KKeyServer::modXMeta();
    unsigned int NumMask = KKeyServer::modXNumLock();
    unsigned int ScrollMask = KKeyServer::modXScrollLock();

    unsigned int MetaMask = XkbKeysymToModifiers(display, XK_Meta_L);
    unsigned int SuperMask = XkbKeysymToModifiers(display, XK_Super_L);
    unsigned int HyperMask = XkbKeysymToModifiers(display, XK_Hyper_L);
    unsigned int AltGrMask = XkbKeysymToModifiers(display, XK_Mode_switch) | XkbKeysymToModifiers(display, XK_ISO_Level3_Shift)
        | XkbKeysymToModifiers(display, XK_ISO_Level3_Latch) | XkbKeysymToModifiers(display, XK_ISO_Level3_Lock);

    unsigned int mods = ShiftMask | ControlMask | AltMask | WinMask | LockMask | NumMask | ScrollMask;

    AltGrMask &= ~mods;
    MetaMask &= ~(mods | AltGrMask);
    SuperMask &= ~(mods | AltGrMask | MetaMask);
    HyperMask &= ~(mods | AltGrMask | MetaMask | SuperMask);

    if ((modifiers & AltGrMask) != 0)
        keyname = i18n("AltGraph") + QLatin1Char('+') + keyname;
    if ((modifiers & HyperMask) != 0)
        keyname = i18n("Hyper") + QLatin1Char('+') + keyname;
    if ((modifiers & SuperMask) != 0)
        keyname = i18n("Super") + QLatin1Char('+') + keyname;
    if ((modifiers & WinMask) != 0)
        keyname = i18n("Meta") + QLatin1Char('+') + keyname;
    if ((modifiers & WinMask) != 0)
        keyname = QKeySequence(Qt::META).toString() + QLatin1Char('+') + keyname;
    if ((modifiers & AltMask) != 0)
        keyname = QKeySequence(Qt::ALT).toString() + QLatin1Char('+') + keyname;
    if ((modifiers & ControlMask) != 0)
        keyname = QKeySequence(Qt::CTRL).toString() + QLatin1Char('+') + keyname;
    if ((modifiers & ShiftMask) != 0)
        keyname = QKeySequence(Qt::SHIFT).toString() + QLatin1Char('+') + keyname;

    return keyname;
}

void KAccessApp::createDialogContents()
{
    if (dialog == nullptr) {
        dialog = new QDialog(nullptr);
        dialog->setWindowTitle(i18n("Warning"));
        dialog->setObjectName(QStringLiteral("AccessXWarning"));
        dialog->setModal(true);

        QVBoxLayout *topLayout = new QVBoxLayout();

        QHBoxLayout *lay = new QHBoxLayout();

        QLabel *label1 = new QLabel();
        QIcon icon = QIcon::fromTheme(QStringLiteral("dialog-warning"));
        if (icon.isNull())
            icon = QMessageBox::standardIcon(QMessageBox::Warning);
        label1->setPixmap(icon.pixmap(64, 64));

        lay->addWidget(label1, 0, Qt::AlignCenter);

        QVBoxLayout *vlay = new QVBoxLayout();
        lay->addItem(vlay);

        featuresLabel = new QLabel();
        featuresLabel->setAlignment(Qt::AlignVCenter);
        featuresLabel->setWordWrap(true);
        vlay->addWidget(featuresLabel);
        vlay->addStretch();

        QHBoxLayout *hlay = new QHBoxLayout();
        vlay->addItem(hlay);

        QLabel *showModeLabel = new QLabel(i18n("&When a gesture was used:"));
        hlay->addWidget(showModeLabel);

        showModeCombobox = new KComboBox();
        hlay->addWidget(showModeCombobox);
        showModeLabel->setBuddy(showModeCombobox);
        showModeCombobox->insertItem(0, i18n("Change Settings Without Asking"));
        showModeCombobox->insertItem(1, i18n("Show This Confirmation Dialog"));
        showModeCombobox->insertItem(2, i18n("Deactivate All AccessX Features & Gestures"));
        showModeCombobox->setCurrentIndex(1);
        topLayout->addLayout(lay);

        auto buttons = new QDialogButtonBox(QDialogButtonBox::Yes | QDialogButtonBox::No, dialog);

        topLayout->addWidget(buttons);
        dialog->setLayout(topLayout);

        connect(buttons, &QDialogButtonBox::accepted, dialog, &QDialog::accept);
        connect(buttons, &QDialogButtonBox::rejected, dialog, &QDialog::reject);
        connect(dialog, &QDialog::accepted, this, &KAccessApp::yesClicked);
        connect(dialog, &QDialog::rejected, this, &KAccessApp::noClicked);
    }
}

void KAccessApp::xkbControlsNotify(xcb_xkb_controls_notify_event_t *event)
{
    unsigned int newFeatures =
        event->enabledControls & (XCB_XKB_BOOL_CTRL_SLOW_KEYS | XCB_XKB_BOOL_CTRL_BOUNCE_KEYS | XCB_XKB_BOOL_CTRL_STICKY_KEYS | XCB_XKB_BOOL_CTRL_MOUSE_KEYS);

    if (newFeatures != features) {
        unsigned int enabled = newFeatures & ~features;
        unsigned int disabled = features & ~newFeatures;

        if (!_gestureConfirmation) {
            requestedFeatures = enabled | (requestedFeatures & ~disabled);
            notifyChanges();
            features = newFeatures;
        } else {
            // set the AccessX features back to what they were. We will
            // apply the changes later if the user allows us to do that.
            readSettings();

            requestedFeatures = enabled | (requestedFeatures & ~disabled);

            enabled = requestedFeatures & ~features;
            disabled = features & ~requestedFeatures;

            QStringList enabledFeatures;
            QStringList disabledFeatures;

            if (enabled & XCB_XKB_BOOL_CTRL_SLOW_KEYS)
                enabledFeatures << i18n("Slow keys");
            else if (disabled & XCB_XKB_BOOL_CTRL_SLOW_KEYS)
                disabledFeatures << i18n("Slow keys");

            if (enabled & XCB_XKB_BOOL_CTRL_BOUNCE_KEYS)
                enabledFeatures << i18n("Bounce keys");
            else if (disabled & XCB_XKB_BOOL_CTRL_BOUNCE_KEYS)
                disabledFeatures << i18n("Bounce keys");

            if (enabled & XCB_XKB_BOOL_CTRL_STICKY_KEYS)
                enabledFeatures << i18n("Sticky keys");
            else if (disabled & XCB_XKB_BOOL_CTRL_STICKY_KEYS)
                disabledFeatures << i18n("Sticky keys");

            if (enabled & XCB_XKB_BOOL_CTRL_MOUSE_KEYS)
                enabledFeatures << i18n("Mouse keys");
            else if (disabled & XCB_XKB_BOOL_CTRL_MOUSE_KEYS)
                disabledFeatures << i18n("Mouse keys");

            QString question;
            switch (enabledFeatures.count()) {
            case 0:
                switch (disabledFeatures.count()) {
                case 1:
                    question = i18n("Do you really want to deactivate \"%1\"?", disabledFeatures[0]);
                    break;
                case 2:
                    question = i18n("Do you really want to deactivate \"%1\" and \"%2\"?", disabledFeatures[0], disabledFeatures[1]);
                    break;
                case 3:
                    question =
                        i18n("Do you really want to deactivate \"%1\", \"%2\" and \"%3\"?", disabledFeatures[0], disabledFeatures[1], disabledFeatures[2]);
                    break;
                case 4:
                    question = i18n("Do you really want to deactivate \"%1\", \"%2\", \"%3\" and \"%4\"?",
                                    disabledFeatures[0],
                                    disabledFeatures[1],
                                    disabledFeatures[2],
                                    disabledFeatures[3]);
                    break;
                }
                break;
            case 1:
                switch (disabledFeatures.count()) {
                case 0:
                    question = i18n("Do you really want to activate \"%1\"?", enabledFeatures[0]);
                    break;
                case 1:
                    question = i18n("Do you really want to activate \"%1\" and to deactivate \"%2\"?", enabledFeatures[0], disabledFeatures[0]);
                    break;
                case 2:
                    question = i18n("Do you really want to activate \"%1\" and to deactivate \"%2\" and \"%3\"?",
                                    enabledFeatures[0],
                                    disabledFeatures[0],
                                    disabledFeatures[1]);
                    break;
                case 3:
                    question = i18n("Do you really want to activate \"%1\" and to deactivate \"%2\", \"%3\" and \"%4\"?",
                                    enabledFeatures[0],
                                    disabledFeatures[0],
                                    disabledFeatures[1],
                                    disabledFeatures[2]);
                    break;
                }
                break;
            case 2:
                switch (disabledFeatures.count()) {
                case 0:
                    question = i18n("Do you really want to activate \"%1\" and \"%2\"?", enabledFeatures[0], enabledFeatures[1]);
                    break;
                case 1:
                    question = i18n("Do you really want to activate \"%1\" and \"%2\" and to deactivate \"%3\"?",
                                    enabledFeatures[0],
                                    enabledFeatures[1],
                                    disabledFeatures[0]);
                    break;
                case 2:
                    question = i18n("Do you really want to activate \"%1\", and \"%2\" and to deactivate \"%3\" and \"%4\"?",
                                    enabledFeatures[0],
                                    enabledFeatures[1],
                                    enabledFeatures[0],
                                    disabledFeatures[1]);
                    break;
                }
                break;
            case 3:
                switch (disabledFeatures.count()) {
                case 0:
                    question = i18n("Do you really want to activate \"%1\", \"%2\" and \"%3\"?", enabledFeatures[0], enabledFeatures[1], enabledFeatures[2]);
                    break;
                case 1:
                    question = i18n("Do you really want to activate \"%1\", \"%2\" and \"%3\" and to deactivate \"%4\"?",
                                    enabledFeatures[0],
                                    enabledFeatures[1],
                                    enabledFeatures[2],
                                    disabledFeatures[0]);
                    break;
                }
                break;
            case 4:
                question = i18n("Do you really want to activate \"%1\", \"%2\", \"%3\" and \"%4\"?",
                                enabledFeatures[0],
                                enabledFeatures[1],
                                enabledFeatures[2],
                                enabledFeatures[3]);
                break;
            }
            QString explanation;
            if (enabledFeatures.count() + disabledFeatures.count() == 1) {
                explanation = i18n("An application has requested to change this setting.");

                if (_gestures) {
                    if ((enabled | disabled) == XCB_XKB_BOOL_CTRL_SLOW_KEYS)
                        explanation = i18n("You held down the Shift key for 8 seconds or an application has requested to change this setting.");
                    else if ((enabled | disabled) == XCB_XKB_BOOL_CTRL_STICKY_KEYS)
                        explanation = i18n("You pressed the Shift key 5 consecutive times or an application has requested to change this setting.");
                    else if ((enabled | disabled) == XCB_XKB_BOOL_CTRL_MOUSE_KEYS) {
                        QString shortcut = mouseKeysShortcut(QX11Info::display());
                        if (!shortcut.isEmpty() && !shortcut.isNull())
                            explanation = i18n("You pressed %1 or an application has requested to change this setting.", shortcut);
                    }
                }
            } else {
                if (_gestures)
                    explanation = i18n("An application has requested to change these settings, or you used a combination of several keyboard gestures.");
                else
                    explanation = i18n("An application has requested to change these settings.");
            }

            createDialogContents();
            featuresLabel->setText(question + QStringLiteral("\n\n") + explanation + QStringLiteral(" ")
                                   + i18n("These AccessX settings are needed for some users with motion impairments and can be configured in the KDE System "
                                          "Settings. You can also turn them on and off with standardized keyboard gestures.\n\nIf you do not need them, you "
                                          "can select \"Deactivate all AccessX features and gestures\"."));

            KWindowSystem::setState(dialog->winId(), NET::KeepAbove);
            KUserTimestamp::updateUserTimestamp(0);
            dialog->show();
        }
    }
}

void KAccessApp::notifyChanges()
{
    if (!_kNotifyAccessX)
        return;

    unsigned int enabled = requestedFeatures & ~features;
    unsigned int disabled = features & ~requestedFeatures;

    if (enabled & XCB_XKB_BOOL_CTRL_SLOW_KEYS)
        KNotification::event(QStringLiteral("slowkeys"),
                             i18n("Slow keys has been enabled. From now on, you need to press each key for a certain length of time before it gets accepted."));
    else if (disabled & XCB_XKB_BOOL_CTRL_SLOW_KEYS)
        KNotification::event(QStringLiteral("slowkeys"), i18n("Slow keys has been disabled."));

    if (enabled & XCB_XKB_BOOL_CTRL_BOUNCE_KEYS)
        KNotification::event(QStringLiteral("bouncekeys"),
                             i18n("Bounce keys has been enabled. From now on, each key will be blocked for a certain length of time after it was used."));
    else if (disabled & XCB_XKB_BOOL_CTRL_BOUNCE_KEYS)
        KNotification::event(QStringLiteral("bouncekeys"), i18n("Bounce keys has been disabled."));

    if (enabled & XCB_XKB_BOOL_CTRL_STICKY_KEYS)
        KNotification::event(QStringLiteral("stickykeys"),
                             i18n("Sticky keys has been enabled. From now on, modifier keys will stay latched after you have released them."));
    else if (disabled & XCB_XKB_BOOL_CTRL_STICKY_KEYS)
        KNotification::event(QStringLiteral("stickykeys"), i18n("Sticky keys has been disabled."));

    if (enabled & XCB_XKB_BOOL_CTRL_MOUSE_KEYS)
        KNotification::event(QStringLiteral("mousekeys"),
                             i18n("Mouse keys has been enabled. From now on, you can use the number pad of your keyboard in order to control the mouse."));
    else if (disabled & XCB_XKB_BOOL_CTRL_MOUSE_KEYS)
        KNotification::event(QStringLiteral("mousekeys"), i18n("Mouse keys has been disabled."));
}

void KAccessApp::applyChanges()
{
    notifyChanges();
    unsigned int enabled = requestedFeatures & ~features;
    unsigned int disabled = features & ~requestedFeatures;

    KConfigGroup config(KSharedConfig::openConfig(), "Keyboard");

    if (enabled & XCB_XKB_BOOL_CTRL_SLOW_KEYS)
        config.writeEntry("SlowKeys", true);
    else if (disabled & XCB_XKB_BOOL_CTRL_SLOW_KEYS)
        config.writeEntry("SlowKeys", false);

    if (enabled & XCB_XKB_BOOL_CTRL_BOUNCE_KEYS)
        config.writeEntry("BounceKeys", true);
    else if (disabled & XCB_XKB_BOOL_CTRL_BOUNCE_KEYS)
        config.writeEntry("BounceKeys", false);

    if (enabled & XCB_XKB_BOOL_CTRL_STICKY_KEYS)
        config.writeEntry("StickyKeys", true);
    else if (disabled & XCB_XKB_BOOL_CTRL_STICKY_KEYS)
        config.writeEntry("StickyKeys", false);

    KConfigGroup mousegrp(KSharedConfig::openConfig(), "Mouse");

    if (enabled & XCB_XKB_BOOL_CTRL_MOUSE_KEYS)
        mousegrp.writeEntry("MouseKeys", true);
    else if (disabled & XCB_XKB_BOOL_CTRL_MOUSE_KEYS)
        mousegrp.writeEntry("MouseKeys", false);
    mousegrp.sync();
    config.sync();
}

void KAccessApp::yesClicked()
{
    if (dialog)
        dialog->deleteLater();
    dialog = nullptr;

    KConfigGroup config(KSharedConfig::openConfig(), "Keyboard");
    switch (showModeCombobox->currentIndex()) {
    case 0:
        config.writeEntry("Gestures", true);
        config.writeEntry("GestureConfirmation", false);
        break;
    default:
        config.writeEntry("Gestures", true);
        config.writeEntry("GestureConfirmation", true);
        break;
    case 2:
        requestedFeatures = 0;
        config.writeEntry("Gestures", false);
        config.writeEntry("GestureConfirmation", true);
    }
    config.sync();

    if (features != requestedFeatures) {
        notifyChanges();
        applyChanges();
    }
    readSettings();
}

void KAccessApp::noClicked()
{
    if (dialog)
        dialog->deleteLater();
    dialog = nullptr;
    requestedFeatures = features;

    KConfigGroup config(KSharedConfig::openConfig(), "Keyboard");
    switch (showModeCombobox->currentIndex()) {
    case 0:
        config.writeEntry("Gestures", true);
        config.writeEntry("GestureConfirmation", false);
        break;
    default:
        config.writeEntry("Gestures", true);
        config.writeEntry("GestureConfirmation", true);
        break;
    case 2:
        requestedFeatures = 0;
        config.writeEntry("Gestures", false);
        config.writeEntry("GestureConfirmation", true);
    }
    config.sync();

    if (features != requestedFeatures)
        applyChanges();
    readSettings();
}

void KAccessApp::dialogClosed()
{
    if (dialog != nullptr)
        dialog->deleteLater();
    dialog = nullptr;

    requestedFeatures = features;
}

void KAccessApp::setXkbOpcode(int opcode)
{
    xkb_opcode = opcode;
}