File indexing completed on 2024-04-28 05:33:06

0001 /*
0002     SPDX-FileCopyrightText: 2000 Matthias Hölzer-Klüpfel <hoelzer@kde.org>
0003     SPDX-FileCopyrightText: 2014 Frederik Gladhorn <gladhorn@kde.org>
0004 
0005     SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
0006 */
0007 
0008 #include <cmath>
0009 #include <unistd.h>
0010 
0011 #include "kaccess.h"
0012 
0013 #include <QAction>
0014 #include <QApplication>
0015 #include <QDialog>
0016 #include <QDialogButtonBox>
0017 #include <QFile>
0018 #include <QHBoxLayout>
0019 #include <QLoggingCategory>
0020 #include <QPainter>
0021 #include <QProcess>
0022 #include <QScreen>
0023 #include <QStyle>
0024 #include <QTimer>
0025 #include <QVBoxLayout>
0026 #include <QtGui/private/qtx11extras_p.h>
0027 
0028 #include <KAboutData>
0029 #include <KComboBox>
0030 #include <KConfig>
0031 #include <KConfigGroup>
0032 #include <KDBusService>
0033 #include <KGlobalAccel>
0034 #include <KKeyServer>
0035 #include <KLazyLocalizedString>
0036 #include <KLocalizedString>
0037 #include <KNotification>
0038 #include <KSharedConfig>
0039 #include <KUserTimestamp>
0040 #include <KWindowSystem>
0041 #include <KX11Extras>
0042 
0043 #include <netwm.h>
0044 #define XK_MISCELLANY
0045 #define XK_XKB_KEYS
0046 #include <X11/keysymdef.h>
0047 
0048 #include <canberra.h>
0049 
0050 Q_LOGGING_CATEGORY(logKAccess, "kcm_kaccess")
0051 
0052 struct ModifierKey {
0053     const unsigned int mask;
0054     const KeySym keysym;
0055     const char *name;
0056     const KLazyLocalizedString lockedText;
0057     const KLazyLocalizedString latchedText;
0058     const KLazyLocalizedString unlatchedText;
0059 };
0060 
0061 static const ModifierKey modifierKeys[] = {
0062     {ShiftMask,
0063      0,
0064      "Shift",
0065      kli18n("The Shift key has been locked and is now active for all of the following keypresses."),
0066      kli18n("The Shift key is now active."),
0067      kli18n("The Shift key is now inactive.")},
0068     {ControlMask,
0069      0,
0070      "Control",
0071      kli18n("The Control key has been locked and is now active for all of the following keypresses."),
0072      kli18n("The Control key is now active."),
0073      kli18n("The Control key is now inactive.")},
0074     {0,
0075      XK_Alt_L,
0076      "Alt",
0077      kli18n("The Alt key has been locked and is now active for all of the following keypresses."),
0078      kli18n("The Alt key is now active."),
0079      kli18n("The Alt key is now inactive.")},
0080     {0,
0081      0,
0082      "Win",
0083      kli18n("The Win key has been locked and is now active for all of the following keypresses."),
0084      kli18n("The Win key is now active."),
0085      kli18n("The Win key is now inactive.")},
0086     {0,
0087      XK_Meta_L,
0088      "Meta",
0089      kli18n("The Meta key has been locked and is now active for all of the following keypresses."),
0090      kli18n("The Meta key is now active."),
0091      kli18n("The Meta key is now inactive.")},
0092     {0,
0093      XK_Super_L,
0094      "Super",
0095      kli18n("The Super key has been locked and is now active for all of the following keypresses."),
0096      kli18n("The Super key is now active."),
0097      kli18n("The Super key is now inactive.")},
0098     {0,
0099      XK_Hyper_L,
0100      "Hyper",
0101      kli18n("The Hyper key has been locked and is now active for all of the following keypresses."),
0102      kli18n("The Hyper key is now active."),
0103      kli18n("The Hyper key is now inactive.")},
0104     {0,
0105      0,
0106      "Alt Graph",
0107      kli18n("The Alt Graph key has been locked and is now active for all of the following keypresses."),
0108      kli18n("The Alt Graph key is now active."),
0109      kli18n("The Alt Graph key is now inactive.")},
0110     {0, XK_Num_Lock, "Num Lock", kli18n("The Num Lock key has been activated."), {}, kli18n("The Num Lock key is now inactive.")},
0111     {LockMask, 0, "Caps Lock", kli18n("The Caps Lock key has been activated."), {}, kli18n("The Caps Lock key is now inactive.")},
0112     {0, XK_Scroll_Lock, "Scroll Lock", kli18n("The Scroll Lock key has been activated."), {}, kli18n("The Scroll Lock key is now inactive.")},
0113     {0, 0, "", {}, {}, {}}};
0114 
0115 /********************************************************************/
0116 
0117 KAccessApp::KAccessApp()
0118     : m_bellSettings(new BellSettings(this))
0119     , m_keyboardSettings(new KeyboardSettings(this))
0120     , m_keyboardFiltersSettings(new KeyboardFiltersSettings(this))
0121     , m_mouseSettings(new MouseSettings(this))
0122     , m_screenReaderSettings(new ScreenReaderSettings(this))
0123     , overlay(nullptr)
0124     , toggleScreenReaderAction(new QAction(this))
0125 {
0126     m_error = false;
0127 
0128     features = 0;
0129     requestedFeatures = 0;
0130     dialog = nullptr;
0131 
0132     if (!QX11Info::isPlatformX11()) {
0133         m_error = true;
0134         return;
0135     }
0136     _activeWindow = KX11Extras::activeWindow();
0137     connect(KX11Extras::self(), &KX11Extras::activeWindowChanged, this, &KAccessApp::activeWindowChanged);
0138 
0139     initMasks();
0140     XkbStateRec state_return;
0141     XkbGetState(QX11Info::display(), XkbUseCoreKbd, &state_return);
0142     unsigned char latched = XkbStateMods(&state_return);
0143     unsigned char locked = XkbModLocks(&state_return);
0144     state = ((int)locked) << 8 | latched;
0145 
0146     auto service = new KDBusService(KDBusService::Unique, this);
0147     connect(service, &KDBusService::activateRequested, this, &KAccessApp::newInstance);
0148 
0149     QTimer::singleShot(0, this, &KAccessApp::readSettings);
0150 }
0151 
0152 KAccessApp::~KAccessApp()
0153 {
0154     if (m_caContext) {
0155         ca_context_destroy(m_caContext);
0156     }
0157 }
0158 
0159 void KAccessApp::newInstance()
0160 {
0161     KSharedConfig::openConfig()->reparseConfiguration();
0162     m_bellSettings.load();
0163     m_keyboardSettings.load();
0164     m_keyboardFiltersSettings.load();
0165     m_mouseSettings.load();
0166     m_screenReaderSettings.load();
0167     readSettings();
0168 }
0169 
0170 void KAccessApp::readSettings()
0171 {
0172     // select bell events if we need them
0173     int state = (m_bellSettings.customBell() || m_bellSettings.visibleBell()) ? XkbBellNotifyMask : 0;
0174     XkbSelectEvents(QX11Info::display(), XkbUseCoreKbd, XkbBellNotifyMask, state);
0175 
0176     // deactivate system bell if not needed
0177     if (!m_bellSettings.systemBell())
0178         XkbChangeEnabledControls(QX11Info::display(), XkbUseCoreKbd, XkbAudibleBellMask, 0);
0179     else
0180         XkbChangeEnabledControls(QX11Info::display(), XkbUseCoreKbd, XkbAudibleBellMask, XkbAudibleBellMask);
0181 
0182     // get keyboard state
0183     XkbDescPtr xkb = XkbGetMap(QX11Info::display(), 0, XkbUseCoreKbd);
0184     if (!xkb)
0185         return;
0186     if (XkbGetControls(QX11Info::display(), XkbAllControlsMask, xkb) != Success)
0187         return;
0188 
0189     // sticky keys
0190     if (m_keyboardSettings.stickyKeys()) {
0191         if (m_keyboardSettings.stickyKeysLatch())
0192             xkb->ctrls->ax_options |= XkbAX_LatchToLockMask;
0193         else
0194             xkb->ctrls->ax_options &= ~XkbAX_LatchToLockMask;
0195         if (m_keyboardSettings.stickyKeysAutoOff())
0196             xkb->ctrls->ax_options |= XkbAX_TwoKeysMask;
0197         else
0198             xkb->ctrls->ax_options &= ~XkbAX_TwoKeysMask;
0199         if (m_keyboardSettings.stickyKeysBeep())
0200             xkb->ctrls->ax_options |= XkbAX_StickyKeysFBMask;
0201         else
0202             xkb->ctrls->ax_options &= ~XkbAX_StickyKeysFBMask;
0203         xkb->ctrls->enabled_ctrls |= XkbStickyKeysMask;
0204     } else
0205         xkb->ctrls->enabled_ctrls &= ~XkbStickyKeysMask;
0206 
0207     // toggle keys
0208     if (m_keyboardSettings.toggleKeysBeep())
0209         xkb->ctrls->ax_options |= XkbAX_IndicatorFBMask;
0210     else
0211         xkb->ctrls->ax_options &= ~XkbAX_IndicatorFBMask;
0212 
0213     // slow keys
0214     if (m_keyboardFiltersSettings.slowKeys()) {
0215         if (m_keyboardFiltersSettings.slowKeysPressBeep())
0216             xkb->ctrls->ax_options |= XkbAX_SKPressFBMask;
0217         else
0218             xkb->ctrls->ax_options &= ~XkbAX_SKPressFBMask;
0219         if (m_keyboardFiltersSettings.slowKeysAcceptBeep())
0220             xkb->ctrls->ax_options |= XkbAX_SKAcceptFBMask;
0221         else
0222             xkb->ctrls->ax_options &= ~XkbAX_SKAcceptFBMask;
0223         if (m_keyboardFiltersSettings.slowKeysRejectBeep())
0224             xkb->ctrls->ax_options |= XkbAX_SKRejectFBMask;
0225         else
0226             xkb->ctrls->ax_options &= ~XkbAX_SKRejectFBMask;
0227         xkb->ctrls->enabled_ctrls |= XkbSlowKeysMask;
0228     } else
0229         xkb->ctrls->enabled_ctrls &= ~XkbSlowKeysMask;
0230     xkb->ctrls->slow_keys_delay = m_keyboardFiltersSettings.slowKeysDelay();
0231 
0232     // bounce keys
0233     if (m_keyboardFiltersSettings.bounceKeys()) {
0234         if (m_keyboardFiltersSettings.bounceKeysRejectBeep())
0235             xkb->ctrls->ax_options |= XkbAX_BKRejectFBMask;
0236         else
0237             xkb->ctrls->ax_options &= ~XkbAX_BKRejectFBMask;
0238         xkb->ctrls->enabled_ctrls |= XkbBounceKeysMask;
0239     } else
0240         xkb->ctrls->enabled_ctrls &= ~XkbBounceKeysMask;
0241     xkb->ctrls->debounce_delay = m_keyboardFiltersSettings.bounceKeysDelay();
0242 
0243     // gestures for enabling the other features
0244     if (m_mouseSettings.gestures())
0245         xkb->ctrls->enabled_ctrls |= XkbAccessXKeysMask;
0246     else
0247         xkb->ctrls->enabled_ctrls &= ~XkbAccessXKeysMask;
0248 
0249     // timeout
0250     if (m_keyboardSettings.accessXTimeout()) {
0251         xkb->ctrls->ax_timeout = m_keyboardSettings.accessXTimeoutDelay() * 60;
0252         xkb->ctrls->axt_opts_mask = 0;
0253         xkb->ctrls->axt_opts_values = 0;
0254         xkb->ctrls->axt_ctrls_mask = XkbStickyKeysMask | XkbSlowKeysMask;
0255         xkb->ctrls->axt_ctrls_values = 0;
0256         xkb->ctrls->enabled_ctrls |= XkbAccessXTimeoutMask;
0257     } else
0258         xkb->ctrls->enabled_ctrls &= ~XkbAccessXTimeoutMask;
0259 
0260     // gestures for enabling the other features
0261     if (m_keyboardSettings.accessXBeep())
0262         xkb->ctrls->ax_options |= XkbAX_FeatureFBMask | XkbAX_SlowWarnFBMask;
0263     else
0264         xkb->ctrls->ax_options &= ~(XkbAX_FeatureFBMask | XkbAX_SlowWarnFBMask);
0265 
0266     // mouse-by-keyboard
0267 
0268     if (m_mouseSettings.mouseKeys()) {
0269         xkb->ctrls->mk_delay = m_mouseSettings.accelerationDelay();
0270 
0271         const int interval = m_mouseSettings.repetitionInterval();
0272         xkb->ctrls->mk_interval = interval;
0273 
0274         xkb->ctrls->mk_time_to_max = m_mouseSettings.accelerationTime();
0275 
0276         xkb->ctrls->mk_max_speed = m_mouseSettings.maxSpeed();
0277 
0278         xkb->ctrls->mk_curve = m_mouseSettings.profileCurve();
0279         xkb->ctrls->mk_dflt_btn = 0;
0280 
0281         xkb->ctrls->enabled_ctrls |= XkbMouseKeysMask;
0282     } else
0283         xkb->ctrls->enabled_ctrls &= ~XkbMouseKeysMask;
0284 
0285     features = xkb->ctrls->enabled_ctrls & (XkbSlowKeysMask | XkbBounceKeysMask | XkbStickyKeysMask | XkbMouseKeysMask);
0286     if (dialog == nullptr)
0287         requestedFeatures = features;
0288     // set state
0289     XkbSetControls(QX11Info::display(),
0290                    XkbControlsEnabledMask | XkbMouseKeysAccelMask | XkbStickyKeysMask | XkbSlowKeysMask | XkbBounceKeysMask | XkbAccessXKeysMask
0291                        | XkbAccessXTimeoutMask,
0292                    xkb);
0293 
0294     // select AccessX events
0295     XkbSelectEvents(QX11Info::display(), XkbUseCoreKbd, XkbAllEventsMask, XkbAllEventsMask);
0296 
0297     if (!m_bellSettings.customBell() && !m_bellSettings.visibleBell() && !(m_mouseSettings.gestures() && m_mouseSettings.gestureConfirmation())
0298         && !m_keyboardSettings.keyboardNotifyModifiers() && !m_mouseSettings.keyboardNotifyAccess()) {
0299         uint ctrls = XkbStickyKeysMask | XkbSlowKeysMask | XkbBounceKeysMask | XkbMouseKeysMask | XkbAudibleBellMask | XkbControlsNotifyMask;
0300         uint values = xkb->ctrls->enabled_ctrls & ctrls;
0301         XkbSetAutoResetControls(QX11Info::display(), ctrls, &ctrls, &values);
0302     } else {
0303         // reset them after program exit
0304         uint ctrls = XkbStickyKeysMask | XkbSlowKeysMask | XkbBounceKeysMask | XkbMouseKeysMask | XkbAudibleBellMask | XkbControlsNotifyMask;
0305         uint values = XkbAudibleBellMask;
0306         XkbSetAutoResetControls(QX11Info::display(), ctrls, &ctrls, &values);
0307     }
0308 
0309     delete overlay;
0310     overlay = nullptr;
0311 
0312     setScreenReaderEnabled(m_screenReaderSettings.enabled());
0313 
0314     toggleScreenReaderAction->setText(i18n("Toggle Screen Reader On and Off"));
0315     toggleScreenReaderAction->setObjectName(QStringLiteral("Toggle Screen Reader On and Off"));
0316     toggleScreenReaderAction->setProperty("componentDisplayName", i18nc("Name for kaccess shortcuts category", "Accessibility"));
0317     KGlobalAccel::self()->setGlobalShortcut(toggleScreenReaderAction, Qt::META | Qt::ALT | Qt::Key_S);
0318     connect(toggleScreenReaderAction, &QAction::triggered, this, &KAccessApp::toggleScreenReader);
0319 }
0320 
0321 void KAccessApp::toggleScreenReader()
0322 {
0323     KSharedConfig::Ptr _config = KSharedConfig::openConfig();
0324     KConfigGroup screenReaderGroup(_config, QStringLiteral("ScreenReader"));
0325     bool enabled = !screenReaderGroup.readEntry("Enabled", false);
0326     screenReaderGroup.writeEntry("Enabled", enabled);
0327     setScreenReaderEnabled(enabled);
0328 }
0329 
0330 void KAccessApp::setScreenReaderEnabled(bool enabled)
0331 {
0332     if (enabled) {
0333         QStringList args = {QStringLiteral("set"),
0334                             QStringLiteral("org.gnome.desktop.a11y.applications"),
0335                             QStringLiteral("screen-reader-enabled"),
0336                             QStringLiteral("true")};
0337         int ret = QProcess::execute(QStringLiteral("gsettings"), args);
0338         if (ret == 0) {
0339             qint64 pid = 0;
0340             QProcess::startDetached(QStringLiteral("orca"), {QStringLiteral("--replace")}, QString(), &pid);
0341             qCDebug(logKAccess) << "Launching Orca, pid:" << pid;
0342         }
0343     } else {
0344         QProcess::startDetached(
0345             QStringLiteral("gsettings"),
0346             {QStringLiteral("set"), QStringLiteral("org.gnome.desktop.a11y.applications"), QStringLiteral("screen-reader-enabled"), QStringLiteral("false")});
0347     }
0348 }
0349 
0350 static int maskToBit(int mask)
0351 {
0352     for (int i = 0; i < 8; i++)
0353         if (mask & (1 << i))
0354             return i;
0355     return -1;
0356 }
0357 
0358 void KAccessApp::initMasks()
0359 {
0360     for (int i = 0; i < 8; i++)
0361         keys[i] = -1;
0362     state = 0;
0363 
0364     for (int i = 0; strcmp(modifierKeys[i].name, "") != 0; i++) {
0365         int mask = modifierKeys[i].mask;
0366         if (mask == 0) {
0367             if (modifierKeys[i].keysym != 0) {
0368                 mask = XkbKeysymToModifiers(QX11Info::display(), modifierKeys[i].keysym);
0369             } else {
0370                 if (!strcmp(modifierKeys[i].name, "Win")) {
0371                     mask = KKeyServer::modXMeta();
0372                 } else {
0373                     mask = XkbKeysymToModifiers(QX11Info::display(), XK_Mode_switch) | XkbKeysymToModifiers(QX11Info::display(), XK_ISO_Level3_Shift)
0374                         | XkbKeysymToModifiers(QX11Info::display(), XK_ISO_Level3_Latch) | XkbKeysymToModifiers(QX11Info::display(), XK_ISO_Level3_Lock);
0375                 }
0376             }
0377         }
0378 
0379         int bit = maskToBit(mask);
0380         if (bit != -1 && keys[bit] == -1)
0381             keys[bit] = i;
0382     }
0383 }
0384 
0385 struct xkb_any_ {
0386     uint8_t response_type;
0387     uint8_t xkbType;
0388     uint16_t sequence;
0389     xcb_timestamp_t time;
0390     uint8_t deviceID;
0391 };
0392 
0393 bool KAccessApp::nativeEventFilter(const QByteArray &eventType, void *message, qintptr *result)
0394 {
0395     Q_UNUSED(result);
0396 
0397     if (eventType == "xcb_generic_event_t") {
0398         xcb_generic_event_t *event = static_cast<xcb_generic_event_t *>(message);
0399         if ((event->response_type & ~0x80) == XkbEventCode + xkb_opcode) {
0400             xkb_any_ *ev = reinterpret_cast<xkb_any_ *>(event);
0401             // Workaround for an XCB bug. xkbType comes from an EventType that is defined with bits, like
0402             // <item name="BellNotify">             <bit>8</bit>
0403             // while the generated XCB event type enum is defined as a bitmask, like
0404             //     XCB_XKB_EVENT_TYPE_BELL_NOTIFY = 256
0405             // This means if xbkType is 8, we need to set the 8th bit to 1, thus raising 2 to power of 8.
0406             // See also https://bugs.freedesktop.org/show_bug.cgi?id=51295
0407             const int eventType = pow(2, ev->xkbType);
0408             switch (eventType) {
0409             case XCB_XKB_EVENT_TYPE_STATE_NOTIFY:
0410                 xkbStateNotify();
0411                 break;
0412             case XCB_XKB_EVENT_TYPE_BELL_NOTIFY:
0413                 xkbBellNotify(reinterpret_cast<xcb_xkb_bell_notify_event_t *>(event));
0414                 break;
0415             case XCB_XKB_EVENT_TYPE_CONTROLS_NOTIFY:
0416                 xkbControlsNotify(reinterpret_cast<xcb_xkb_controls_notify_event_t *>(event));
0417                 break;
0418             }
0419             return true;
0420         }
0421     }
0422     return false;
0423 }
0424 
0425 void VisualBell::paintEvent(QPaintEvent *event)
0426 {
0427     QWidget::paintEvent(event);
0428     QTimer::singleShot(_pause, this, &QWidget::hide);
0429 }
0430 
0431 void KAccessApp::activeWindowChanged(WId wid)
0432 {
0433     _activeWindow = wid;
0434 }
0435 
0436 void KAccessApp::xkbStateNotify()
0437 {
0438     XkbStateRec state_return;
0439     XkbGetState(QX11Info::display(), XkbUseCoreKbd, &state_return);
0440     unsigned char latched = XkbStateMods(&state_return);
0441     unsigned char locked = XkbModLocks(&state_return);
0442     int mods = ((int)locked) << 8 | latched;
0443 
0444     if (state != mods) {
0445         if (m_keyboardSettings.keyboardNotifyModifiers())
0446             for (int i = 0; i < 8; i++) {
0447                 if (keys[i] != -1) {
0448                     if (modifierKeys[keys[i]].latchedText.isEmpty() && ((((mods >> i) & 0x101) != 0) != (((state >> i) & 0x101) != 0))) {
0449                         if ((mods >> i) & 1) {
0450                             KNotification::event(QStringLiteral("lockkey-locked"), modifierKeys[keys[i]].lockedText.toString());
0451                         } else {
0452                             KNotification::event(QStringLiteral("lockkey-unlocked"), modifierKeys[keys[i]].unlatchedText.toString());
0453                         }
0454                     } else if (!modifierKeys[keys[i]].latchedText.isEmpty() && (((mods >> i) & 0x101) != ((state >> i) & 0x101))) {
0455                         if ((mods >> i) & 0x100) {
0456                             KNotification::event(QStringLiteral("modifierkey-locked"), modifierKeys[keys[i]].lockedText.toString());
0457                         } else if ((mods >> i) & 1) {
0458                             KNotification::event(QStringLiteral("modifierkey-latched"), modifierKeys[keys[i]].latchedText.toString());
0459                         } else {
0460                             KNotification::event(QStringLiteral("modifierkey-unlatched"), modifierKeys[keys[i]].unlatchedText.toString());
0461                         }
0462                     }
0463                 }
0464             }
0465         state = mods;
0466     }
0467 }
0468 
0469 void KAccessApp::xkbBellNotify(xcb_xkb_bell_notify_event_t *event)
0470 {
0471     // bail out if we should not really ring
0472     if (event->eventOnly)
0473         return;
0474 
0475     // flash the visible bell
0476     if (m_bellSettings.visibleBell()) {
0477         // create overlay widget
0478         if (!overlay)
0479             overlay = new VisualBell(m_bellSettings.visibleBellPause());
0480 
0481         WId id = _activeWindow;
0482 
0483         NETRect frame, window;
0484         NETWinInfo net(QX11Info::connection(), id, QX11Info::appRootWindow(), NET::Properties(), NET::Properties2());
0485 
0486         net.kdeGeometry(frame, window);
0487 
0488         overlay->setGeometry(window.pos.x, window.pos.y, window.size.width, window.size.height);
0489 
0490         if (m_bellSettings.invertScreen()) {
0491             QPixmap screen = QGuiApplication::primaryScreen()->grabWindow(id, 0, 0, window.size.width, window.size.height);
0492 
0493 #ifdef __GNUC__
0494 #warning is this the best way to invert a pixmap?
0495 #endif
0496             //    QPixmap invert(window.size.width, window.size.height);
0497             QPalette pal = overlay->palette();
0498             {
0499                 QImage i = screen.toImage();
0500                 i.invertPixels();
0501                 pal.setBrush(overlay->backgroundRole(), QBrush(QPixmap::fromImage(std::move(i))));
0502             }
0503             overlay->setPalette(pal);
0504             /*
0505                   QPainter p(&invert);
0506                   p.setRasterOp(QPainter::NotCopyROP);
0507                   p.drawPixmap(0, 0, screen);
0508                   overlay->setBackgroundPixmap(invert);
0509             */
0510         } else {
0511             QPalette pal = overlay->palette();
0512             pal.setColor(overlay->backgroundRole(), m_bellSettings.visibleBellColor());
0513             overlay->setPalette(pal);
0514         }
0515 
0516         // flash the overlay widget
0517         overlay->raise();
0518         overlay->show();
0519         QCoreApplication::sendPostedEvents();
0520     }
0521 
0522     // ask canberra to ring a nice bell
0523     if (m_bellSettings.customBell()) {
0524         if (!m_caContext) {
0525             int ret = ca_context_create(&m_caContext);
0526             if (ret != CA_SUCCESS) {
0527                 qCWarning(logKAccess) << "Failed to initialize canberra context for audio notification:" << ca_strerror(ret);
0528                 m_caContext = nullptr;
0529                 return;
0530             }
0531 
0532             ret = ca_context_change_props(m_caContext,
0533                                           CA_PROP_APPLICATION_NAME,
0534                                           qApp->applicationDisplayName().toUtf8().constData(),
0535                                           CA_PROP_APPLICATION_ID,
0536                                           qApp->desktopFileName().toUtf8().constData(),
0537                                           nullptr);
0538             if (ret != CA_SUCCESS) {
0539                 qCWarning(logKAccess) << "Failed to set application properties on canberra context for audio notification:" << ca_strerror(ret);
0540             }
0541         } else {
0542             ca_context_cancel(m_caContext, 0);
0543         }
0544 
0545         if (m_currentPlayerSource.isValid()) {
0546             ca_context_play(m_caContext,
0547                             0,
0548                             CA_PROP_MEDIA_FILENAME,
0549                             QFile::encodeName(m_currentPlayerSource.toLocalFile()).constData(),
0550                             CA_PROP_MEDIA_ROLE,
0551                             "event",
0552                             CA_PROP_CANBERRA_CACHE_CONTROL,
0553                             "permanent",
0554                             nullptr);
0555         } else {
0556             ca_context_play(m_caContext, 0, CA_PROP_EVENT_ID, "bell", CA_PROP_MEDIA_ROLE, "event", CA_PROP_CANBERRA_CACHE_CONTROL, "permanent", nullptr);
0557         }
0558     }
0559 }
0560 
0561 QString mouseKeysShortcut(Display *display)
0562 {
0563     // Calculate the keycode
0564     KeySym sym = XK_MouseKeys_Enable;
0565     KeyCode code = XKeysymToKeycode(display, sym);
0566     if (code == 0) {
0567         sym = XK_Pointer_EnableKeys;
0568         code = XKeysymToKeycode(display, sym);
0569         if (code == 0)
0570             return QString(); // No shortcut available?
0571     }
0572 
0573     // Calculate the modifiers by searching the keysym in the X keyboard mapping
0574     XkbDescPtr xkbdesc = XkbGetMap(display, XkbKeyTypesMask | XkbKeySymsMask, XkbUseCoreKbd);
0575 
0576     if (!xkbdesc)
0577         return QString(); // Failed to obtain the mapping from server
0578 
0579     bool found = false;
0580     unsigned char modifiers = 0;
0581     int groups = XkbKeyNumGroups(xkbdesc, code);
0582     for (int grp = 0; grp < groups && !found; grp++) {
0583         int levels = XkbKeyGroupWidth(xkbdesc, code, grp);
0584         for (int level = 0; level < levels && !found; level++) {
0585             if (sym == XkbKeySymEntry(xkbdesc, code, level, grp)) {
0586                 // keysym found => determine modifiers
0587                 int typeIdx = xkbdesc->map->key_sym_map[code].kt_index[grp];
0588                 XkbKeyTypePtr type = &(xkbdesc->map->types[typeIdx]);
0589                 for (int i = 0; i < type->map_count && !found; i++) {
0590                     if (type->map[i].active && (type->map[i].level == level)) {
0591                         modifiers = type->map[i].mods.mask;
0592                         found = true;
0593                     }
0594                 }
0595             }
0596         }
0597     }
0598     XkbFreeClientMap(xkbdesc, 0, true);
0599 
0600     if (!found)
0601         return QString(); // Somehow the keycode -> keysym mapping is flawed
0602 
0603     XEvent ev;
0604     ev.type = KeyPress;
0605     ev.xkey.display = display;
0606     ev.xkey.keycode = code;
0607     ev.xkey.state = 0;
0608     int key;
0609     KKeyServer::xEventToQt(&ev, &key);
0610     QString keyname = QKeySequence(key).toString();
0611 
0612     unsigned int AltMask = KKeyServer::modXAlt();
0613     unsigned int WinMask = KKeyServer::modXMeta();
0614     unsigned int NumMask = KKeyServer::modXNumLock();
0615     unsigned int ScrollMask = KKeyServer::modXScrollLock();
0616 
0617     unsigned int MetaMask = XkbKeysymToModifiers(display, XK_Meta_L);
0618     unsigned int SuperMask = XkbKeysymToModifiers(display, XK_Super_L);
0619     unsigned int HyperMask = XkbKeysymToModifiers(display, XK_Hyper_L);
0620     unsigned int AltGrMask = XkbKeysymToModifiers(display, XK_Mode_switch) | XkbKeysymToModifiers(display, XK_ISO_Level3_Shift)
0621         | XkbKeysymToModifiers(display, XK_ISO_Level3_Latch) | XkbKeysymToModifiers(display, XK_ISO_Level3_Lock);
0622 
0623     unsigned int mods = ShiftMask | ControlMask | AltMask | WinMask | LockMask | NumMask | ScrollMask;
0624 
0625     AltGrMask &= ~mods;
0626     MetaMask &= ~(mods | AltGrMask);
0627     SuperMask &= ~(mods | AltGrMask | MetaMask);
0628     HyperMask &= ~(mods | AltGrMask | MetaMask | SuperMask);
0629 
0630     if ((modifiers & AltGrMask) != 0)
0631         keyname = i18n("AltGraph") + QLatin1Char('+') + keyname;
0632     if ((modifiers & HyperMask) != 0)
0633         keyname = i18n("Hyper") + QLatin1Char('+') + keyname;
0634     if ((modifiers & SuperMask) != 0)
0635         keyname = i18n("Super") + QLatin1Char('+') + keyname;
0636     if ((modifiers & WinMask) != 0)
0637         keyname = i18n("Meta") + QLatin1Char('+') + keyname;
0638     if ((modifiers & WinMask) != 0)
0639         keyname = QKeySequence(Qt::META).toString() + QLatin1Char('+') + keyname;
0640     if ((modifiers & AltMask) != 0)
0641         keyname = QKeySequence(Qt::ALT).toString() + QLatin1Char('+') + keyname;
0642     if ((modifiers & ControlMask) != 0)
0643         keyname = QKeySequence(Qt::CTRL).toString() + QLatin1Char('+') + keyname;
0644     if ((modifiers & ShiftMask) != 0)
0645         keyname = QKeySequence(Qt::SHIFT).toString() + QLatin1Char('+') + keyname;
0646 
0647     return keyname;
0648 }
0649 
0650 void KAccessApp::createDialogContents()
0651 {
0652     if (dialog == nullptr) {
0653         dialog = new QDialog(nullptr);
0654         dialog->setWindowTitle(i18n("Warning"));
0655         dialog->setObjectName(QStringLiteral("AccessXWarning"));
0656         dialog->setModal(true);
0657 
0658         QVBoxLayout *topLayout = new QVBoxLayout();
0659 
0660         QHBoxLayout *lay = new QHBoxLayout();
0661 
0662         QLabel *label1 = new QLabel();
0663         QIcon icon = QIcon::fromTheme(QStringLiteral("dialog-warning"));
0664         if (icon.isNull())
0665             icon = qApp->style()->standardIcon(QStyle::SP_MessageBoxWarning);
0666         label1->setPixmap(icon.pixmap(64, 64));
0667 
0668         lay->addWidget(label1, 0, Qt::AlignCenter);
0669 
0670         QVBoxLayout *vlay = new QVBoxLayout();
0671         lay->addItem(vlay);
0672 
0673         featuresLabel = new QLabel();
0674         featuresLabel->setAlignment(Qt::AlignVCenter);
0675         featuresLabel->setWordWrap(true);
0676         vlay->addWidget(featuresLabel);
0677         vlay->addStretch();
0678 
0679         QHBoxLayout *hlay = new QHBoxLayout();
0680         vlay->addItem(hlay);
0681 
0682         QLabel *showModeLabel = new QLabel(i18n("&When a gesture was used:"));
0683         hlay->addWidget(showModeLabel);
0684 
0685         showModeCombobox = new KComboBox();
0686         hlay->addWidget(showModeCombobox);
0687         showModeLabel->setBuddy(showModeCombobox);
0688         showModeCombobox->insertItem(0, i18n("Change Settings Without Asking"));
0689         showModeCombobox->insertItem(1, i18n("Show This Confirmation Dialog"));
0690         showModeCombobox->insertItem(2, i18n("Deactivate All AccessX Features & Gestures"));
0691         showModeCombobox->setCurrentIndex(1);
0692         topLayout->addLayout(lay);
0693 
0694         auto buttons = new QDialogButtonBox(QDialogButtonBox::Yes | QDialogButtonBox::No, dialog);
0695 
0696         topLayout->addWidget(buttons);
0697         dialog->setLayout(topLayout);
0698 
0699         connect(buttons, &QDialogButtonBox::accepted, dialog, &QDialog::accept);
0700         connect(buttons, &QDialogButtonBox::rejected, dialog, &QDialog::reject);
0701         connect(dialog, &QDialog::accepted, this, &KAccessApp::yesClicked);
0702         connect(dialog, &QDialog::rejected, this, &KAccessApp::noClicked);
0703     }
0704 }
0705 
0706 void KAccessApp::xkbControlsNotify(xcb_xkb_controls_notify_event_t *event)
0707 {
0708     unsigned int newFeatures =
0709         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);
0710 
0711     if (newFeatures != features) {
0712         unsigned int enabled = newFeatures & ~features;
0713         unsigned int disabled = features & ~newFeatures;
0714 
0715         if (!m_mouseSettings.gestureConfirmation()) {
0716             requestedFeatures = enabled | (requestedFeatures & ~disabled);
0717             notifyChanges();
0718             features = newFeatures;
0719         } else {
0720             // set the AccessX features back to what they were. We will
0721             // apply the changes later if the user allows us to do that.
0722             readSettings();
0723 
0724             requestedFeatures = enabled | (requestedFeatures & ~disabled);
0725 
0726             enabled = requestedFeatures & ~features;
0727             disabled = features & ~requestedFeatures;
0728 
0729             QStringList enabledFeatures;
0730             QStringList disabledFeatures;
0731 
0732             if (enabled & XCB_XKB_BOOL_CTRL_SLOW_KEYS)
0733                 enabledFeatures << i18n("Slow keys");
0734             else if (disabled & XCB_XKB_BOOL_CTRL_SLOW_KEYS)
0735                 disabledFeatures << i18n("Slow keys");
0736 
0737             if (enabled & XCB_XKB_BOOL_CTRL_BOUNCE_KEYS)
0738                 enabledFeatures << i18n("Bounce keys");
0739             else if (disabled & XCB_XKB_BOOL_CTRL_BOUNCE_KEYS)
0740                 disabledFeatures << i18n("Bounce keys");
0741 
0742             if (enabled & XCB_XKB_BOOL_CTRL_STICKY_KEYS)
0743                 enabledFeatures << i18n("Sticky keys");
0744             else if (disabled & XCB_XKB_BOOL_CTRL_STICKY_KEYS)
0745                 disabledFeatures << i18n("Sticky keys");
0746 
0747             if (enabled & XCB_XKB_BOOL_CTRL_MOUSE_KEYS)
0748                 enabledFeatures << i18n("Mouse keys");
0749             else if (disabled & XCB_XKB_BOOL_CTRL_MOUSE_KEYS)
0750                 disabledFeatures << i18n("Mouse keys");
0751 
0752             QString question;
0753             switch (enabledFeatures.count()) {
0754             case 0:
0755                 switch (disabledFeatures.count()) {
0756                 case 1:
0757                     question = i18n("Do you really want to deactivate \"%1\"?", disabledFeatures[0]);
0758                     break;
0759                 case 2:
0760                     question = i18n("Do you really want to deactivate \"%1\" and \"%2\"?", disabledFeatures[0], disabledFeatures[1]);
0761                     break;
0762                 case 3:
0763                     question =
0764                         i18n("Do you really want to deactivate \"%1\", \"%2\" and \"%3\"?", disabledFeatures[0], disabledFeatures[1], disabledFeatures[2]);
0765                     break;
0766                 case 4:
0767                     question = i18n("Do you really want to deactivate \"%1\", \"%2\", \"%3\" and \"%4\"?",
0768                                     disabledFeatures[0],
0769                                     disabledFeatures[1],
0770                                     disabledFeatures[2],
0771                                     disabledFeatures[3]);
0772                     break;
0773                 }
0774                 break;
0775             case 1:
0776                 switch (disabledFeatures.count()) {
0777                 case 0:
0778                     question = i18n("Do you really want to activate \"%1\"?", enabledFeatures[0]);
0779                     break;
0780                 case 1:
0781                     question = i18n("Do you really want to activate \"%1\" and to deactivate \"%2\"?", enabledFeatures[0], disabledFeatures[0]);
0782                     break;
0783                 case 2:
0784                     question = i18n("Do you really want to activate \"%1\" and to deactivate \"%2\" and \"%3\"?",
0785                                     enabledFeatures[0],
0786                                     disabledFeatures[0],
0787                                     disabledFeatures[1]);
0788                     break;
0789                 case 3:
0790                     question = i18n("Do you really want to activate \"%1\" and to deactivate \"%2\", \"%3\" and \"%4\"?",
0791                                     enabledFeatures[0],
0792                                     disabledFeatures[0],
0793                                     disabledFeatures[1],
0794                                     disabledFeatures[2]);
0795                     break;
0796                 }
0797                 break;
0798             case 2:
0799                 switch (disabledFeatures.count()) {
0800                 case 0:
0801                     question = i18n("Do you really want to activate \"%1\" and \"%2\"?", enabledFeatures[0], enabledFeatures[1]);
0802                     break;
0803                 case 1:
0804                     question = i18n("Do you really want to activate \"%1\" and \"%2\" and to deactivate \"%3\"?",
0805                                     enabledFeatures[0],
0806                                     enabledFeatures[1],
0807                                     disabledFeatures[0]);
0808                     break;
0809                 case 2:
0810                     question = i18n("Do you really want to activate \"%1\", and \"%2\" and to deactivate \"%3\" and \"%4\"?",
0811                                     enabledFeatures[0],
0812                                     enabledFeatures[1],
0813                                     enabledFeatures[0],
0814                                     disabledFeatures[1]);
0815                     break;
0816                 }
0817                 break;
0818             case 3:
0819                 switch (disabledFeatures.count()) {
0820                 case 0:
0821                     question = i18n("Do you really want to activate \"%1\", \"%2\" and \"%3\"?", enabledFeatures[0], enabledFeatures[1], enabledFeatures[2]);
0822                     break;
0823                 case 1:
0824                     question = i18n("Do you really want to activate \"%1\", \"%2\" and \"%3\" and to deactivate \"%4\"?",
0825                                     enabledFeatures[0],
0826                                     enabledFeatures[1],
0827                                     enabledFeatures[2],
0828                                     disabledFeatures[0]);
0829                     break;
0830                 }
0831                 break;
0832             case 4:
0833                 question = i18n("Do you really want to activate \"%1\", \"%2\", \"%3\" and \"%4\"?",
0834                                 enabledFeatures[0],
0835                                 enabledFeatures[1],
0836                                 enabledFeatures[2],
0837                                 enabledFeatures[3]);
0838                 break;
0839             }
0840             QString explanation;
0841             if (enabledFeatures.count() + disabledFeatures.count() == 1) {
0842                 explanation = i18n("An application has requested to change this setting.");
0843 
0844                 if (m_mouseSettings.gestures()) {
0845                     if ((enabled | disabled) == XCB_XKB_BOOL_CTRL_SLOW_KEYS)
0846                         explanation = i18n("You held down the Shift key for 8 seconds or an application has requested to change this setting.");
0847                     else if ((enabled | disabled) == XCB_XKB_BOOL_CTRL_STICKY_KEYS)
0848                         explanation = i18n("You pressed the Shift key 5 consecutive times or an application has requested to change this setting.");
0849                     else if ((enabled | disabled) == XCB_XKB_BOOL_CTRL_MOUSE_KEYS) {
0850                         QString shortcut = mouseKeysShortcut(QX11Info::display());
0851                         if (!shortcut.isEmpty() && !shortcut.isNull())
0852                             explanation = i18n("You pressed %1 or an application has requested to change this setting.", shortcut);
0853                     }
0854                 }
0855             } else {
0856                 if (m_mouseSettings.gestures())
0857                     explanation = i18n("An application has requested to change these settings, or you used a combination of several keyboard gestures.");
0858                 else
0859                     explanation = i18n("An application has requested to change these settings.");
0860             }
0861 
0862             createDialogContents();
0863             featuresLabel->setText(question + QStringLiteral("\n\n") + explanation + QStringLiteral(" ")
0864                                    + i18n("These AccessX settings are needed for some users with motion impairments and can be configured in the KDE System "
0865                                           "Settings. You can also turn them on and off with standardized keyboard gestures.\n\nIf you do not need them, you "
0866                                           "can select \"Deactivate all AccessX features and gestures\"."));
0867 
0868             dialog->setWindowFlag(Qt::WindowStaysOnTopHint);
0869             KUserTimestamp::updateUserTimestamp(0);
0870             dialog->show();
0871         }
0872     }
0873 }
0874 
0875 void KAccessApp::notifyChanges()
0876 {
0877     if (!m_mouseSettings.keyboardNotifyAccess())
0878         return;
0879 
0880     unsigned int enabled = requestedFeatures & ~features;
0881     unsigned int disabled = features & ~requestedFeatures;
0882 
0883     if (enabled & XCB_XKB_BOOL_CTRL_SLOW_KEYS)
0884         KNotification::event(QStringLiteral("slowkeys"),
0885                              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."));
0886     else if (disabled & XCB_XKB_BOOL_CTRL_SLOW_KEYS)
0887         KNotification::event(QStringLiteral("slowkeys"), i18n("Slow keys has been disabled."));
0888 
0889     if (enabled & XCB_XKB_BOOL_CTRL_BOUNCE_KEYS)
0890         KNotification::event(QStringLiteral("bouncekeys"),
0891                              i18n("Bounce keys has been enabled. From now on, each key will be blocked for a certain length of time after it was used."));
0892     else if (disabled & XCB_XKB_BOOL_CTRL_BOUNCE_KEYS)
0893         KNotification::event(QStringLiteral("bouncekeys"), i18n("Bounce keys has been disabled."));
0894 
0895     if (enabled & XCB_XKB_BOOL_CTRL_STICKY_KEYS)
0896         KNotification::event(QStringLiteral("stickykeys"),
0897                              i18n("Sticky keys has been enabled. From now on, modifier keys will stay latched after you have released them."));
0898     else if (disabled & XCB_XKB_BOOL_CTRL_STICKY_KEYS)
0899         KNotification::event(QStringLiteral("stickykeys"), i18n("Sticky keys has been disabled."));
0900 
0901     if (enabled & XCB_XKB_BOOL_CTRL_MOUSE_KEYS)
0902         KNotification::event(QStringLiteral("mousekeys"),
0903                              i18n("Mouse keys has been enabled. From now on, you can use the number pad of your keyboard in order to control the mouse."));
0904     else if (disabled & XCB_XKB_BOOL_CTRL_MOUSE_KEYS)
0905         KNotification::event(QStringLiteral("mousekeys"), i18n("Mouse keys has been disabled."));
0906 }
0907 
0908 void KAccessApp::applyChanges()
0909 {
0910     notifyChanges();
0911     unsigned int enabled = requestedFeatures & ~features;
0912     unsigned int disabled = features & ~requestedFeatures;
0913 
0914     KConfigGroup config(KSharedConfig::openConfig(), QStringLiteral("Keyboard"));
0915 
0916     if (enabled & XCB_XKB_BOOL_CTRL_SLOW_KEYS)
0917         config.writeEntry("SlowKeys", true);
0918     else if (disabled & XCB_XKB_BOOL_CTRL_SLOW_KEYS)
0919         config.writeEntry("SlowKeys", false);
0920 
0921     if (enabled & XCB_XKB_BOOL_CTRL_BOUNCE_KEYS)
0922         config.writeEntry("BounceKeys", true);
0923     else if (disabled & XCB_XKB_BOOL_CTRL_BOUNCE_KEYS)
0924         config.writeEntry("BounceKeys", false);
0925 
0926     if (enabled & XCB_XKB_BOOL_CTRL_STICKY_KEYS)
0927         config.writeEntry("StickyKeys", true);
0928     else if (disabled & XCB_XKB_BOOL_CTRL_STICKY_KEYS)
0929         config.writeEntry("StickyKeys", false);
0930 
0931     KConfigGroup mousegrp(KSharedConfig::openConfig(), QStringLiteral("Mouse"));
0932 
0933     if (enabled & XCB_XKB_BOOL_CTRL_MOUSE_KEYS)
0934         mousegrp.writeEntry("MouseKeys", true);
0935     else if (disabled & XCB_XKB_BOOL_CTRL_MOUSE_KEYS)
0936         mousegrp.writeEntry("MouseKeys", false);
0937     mousegrp.sync();
0938     config.sync();
0939 }
0940 
0941 void KAccessApp::yesClicked()
0942 {
0943     if (dialog)
0944         dialog->deleteLater();
0945     dialog = nullptr;
0946 
0947     KConfigGroup config(KSharedConfig::openConfig(), QStringLiteral("Keyboard"));
0948     switch (showModeCombobox->currentIndex()) {
0949     case 0:
0950         config.writeEntry("Gestures", true);
0951         config.writeEntry("GestureConfirmation", false);
0952         break;
0953     default:
0954         config.writeEntry("Gestures", true);
0955         config.writeEntry("GestureConfirmation", true);
0956         break;
0957     case 2:
0958         requestedFeatures = 0;
0959         config.writeEntry("Gestures", false);
0960         config.writeEntry("GestureConfirmation", true);
0961     }
0962     config.sync();
0963 
0964     if (features != requestedFeatures) {
0965         notifyChanges();
0966         applyChanges();
0967     }
0968     readSettings();
0969 }
0970 
0971 void KAccessApp::noClicked()
0972 {
0973     if (dialog)
0974         dialog->deleteLater();
0975     dialog = nullptr;
0976     requestedFeatures = features;
0977 
0978     KConfigGroup config(KSharedConfig::openConfig(), QStringLiteral("Keyboard"));
0979     switch (showModeCombobox->currentIndex()) {
0980     case 0:
0981         config.writeEntry("Gestures", true);
0982         config.writeEntry("GestureConfirmation", false);
0983         break;
0984     default:
0985         config.writeEntry("Gestures", true);
0986         config.writeEntry("GestureConfirmation", true);
0987         break;
0988     case 2:
0989         requestedFeatures = 0;
0990         config.writeEntry("Gestures", false);
0991         config.writeEntry("GestureConfirmation", true);
0992     }
0993     config.sync();
0994 
0995     if (features != requestedFeatures)
0996         applyChanges();
0997     readSettings();
0998 }
0999 
1000 void KAccessApp::dialogClosed()
1001 {
1002     if (dialog != nullptr)
1003         dialog->deleteLater();
1004     dialog = nullptr;
1005 
1006     requestedFeatures = features;
1007 }
1008 
1009 void KAccessApp::setXkbOpcode(int opcode)
1010 {
1011     xkb_opcode = opcode;
1012 }