File indexing completed on 2024-04-28 03:40:29

0001 /*
0002     SPDX-FileCopyrightText: 2002-2003 Jeff Roush <jeff@mousetool.com>
0003     SPDX-FileCopyrightText: 2003 Olaf Schmidt <ojschmidt@kde.org>
0004     SPDX-FileCopyrightText: 2003 Gunnar Schmi Dt <gunnar@schmi-dt.de>
0005     SPDX-License-Identifier: GPL-2.0-or-later
0006 */
0007 
0008 #include "kmousetool.h"
0009 
0010 #include <X11/Intrinsic.h> /* Intrinsics Definitions*/
0011 #include <X11/StringDefs.h> /* Standard Name-String definitions*/
0012 #include <X11/Xmd.h>
0013 #include <X11/extensions/XTest.h> /* Standard Name-String definitions*/
0014 #include <X11/extensions/xtestext1.h> /* Standard Name-String definitions*/
0015 #include <fixx11h.h>
0016 
0017 #include <QAbstractEventDispatcher>
0018 #include <QApplication>
0019 #include <QAudioOutput>
0020 #include <QFile>
0021 #include <QFileInfo>
0022 #include <QIcon>
0023 #include <QMediaPlayer>
0024 #include <QMenu>
0025 #include <QScreen>
0026 #include <QStandardPaths>
0027 
0028 #include <KConfig>
0029 #include <KConfigGroup>
0030 #include <KHelpMenu>
0031 #include <KLocalizedString>
0032 #include <KMessageBox>
0033 #include <KSharedConfig>
0034 #include <KStandardGuiItem>
0035 
0036 int currentXPosition;
0037 int currentYPosition;
0038 
0039 #undef long
0040 
0041 int leftButton;
0042 int rightButton;
0043 
0044 /**
0045  * Gnarly X functions
0046  */
0047 void queryPointer(Window *pidRoot, int *pnRx, int *pnRy, int *pnX, int *pnY, unsigned int *puMask);
0048 int CursorHasMoved(int minMovement);
0049 void getMouseButtons();
0050 void LeftClick();
0051 void RightClick();
0052 void DoubleClick();
0053 void LeftDn();
0054 void LeftUp();
0055 
0056 // Declarations
0057 Display *display;
0058 
0059 void KMouseTool::init_vars()
0060 {
0061     mContinue_timer = 1;
0062     mTick_count = 0;
0063     mMax_ticks = mDwell_time + 1;
0064     mMouse_is_down = false;
0065 
0066     loadOptions();
0067 
0068     // If the ~/.mousetool directory doesn't exist, create it
0069     mSoundFileName = QStandardPaths::locate(QStandardPaths::AppLocalDataLocation, QStringLiteral("sounds/mousetool_tap.wav"));
0070     mPlayer = new QMediaPlayer();
0071     QAudioOutput *output = new QAudioOutput();
0072     mPlayer->setAudioOutput(output);
0073     mPlayer->setParent(this);
0074 
0075     // find application file
0076     mAppfilename = QStandardPaths::findExecutable(QStringLiteral("kmousetool"));
0077 
0078     // find the user's autostart directory
0079     mAutostartdirname = QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation) + QStringLiteral("/autostart-scripts/");
0080 
0081     auto screenSize = screen()->availableGeometry();
0082     int w = screenSize.width();
0083     int h = screenSize.height();
0084     MTStroke::setUpperLeft(0, 0);
0085     MTStroke::setUpperRight(w - 1, 0);
0086     MTStroke::setLowerLeft(0, h - 1);
0087     MTStroke::setLowerRight(w - 1, h - 1);
0088 
0089     mButtonQuit = buttonBox->addButton(QString(), QDialogButtonBox::RejectRole);
0090 }
0091 
0092 void KMouseTool::resetSettings()
0093 {
0094     cbDrag->setChecked(mSmart_drag_on);
0095     cbStart->setChecked(isAutostart());
0096     cbClick->setChecked(mPlaySound);
0097     cbStroke->setChecked(mStrokesEnabled);
0098     movementEdit->setValue(mMin_movement);
0099     dwellTimeEdit->setValue(mDwell_time);
0100     dragTimeEdit->setValue(mDrag_time);
0101     settingsChanged();
0102 }
0103 
0104 void KMouseTool::setDefaultSettings()
0105 {
0106     cbDrag->setChecked(false);
0107     cbStart->setChecked(false);
0108     cbClick->setChecked(false);
0109     cbStroke->setChecked(false);
0110     movementEdit->setValue(5);
0111     dwellTimeEdit->setValue(5);
0112     dragTimeEdit->setValue(3);
0113     settingsChanged();
0114 }
0115 
0116 void KMouseTool::timerEvent(QTimerEvent *)
0117 {
0118     if (!mMousetool_is_running)
0119         return;
0120 
0121     if (!mContinue_timer) {
0122         QAbstractEventDispatcher::instance()->unregisterTimers(this);
0123         return;
0124     }
0125 
0126     mMax_ticks = mDwell_time + mDrag_time;
0127     mStroke.addPt(currentXPosition, currentYPosition);
0128 
0129     mMoving = mMoving ? CursorHasMoved(1) : CursorHasMoved(mMin_movement);
0130     if (mMoving) {
0131         if (mMousetool_just_started) {
0132             mMousetool_just_started = false;
0133             mTick_count = mMax_ticks;
0134         } else {
0135             mTick_count = 0;
0136         }
0137         return;
0138     }
0139 
0140     if (mTick_count < mMax_ticks)
0141         mTick_count++;
0142 
0143     // If the mouse has paused ...
0144     if (mTick_count == mDwell_time) {
0145         int strokeType = mStroke.getStrokeType();
0146         getMouseButtons();
0147 
0148         // if we're dragging the mouse, ignore stroke type
0149         if (mMouse_is_down) {
0150             normalClick();
0151             return;
0152         }
0153 
0154         if (!mStrokesEnabled) {
0155             normalClick();
0156             return;
0157         }
0158         if (strokeType == MTStroke::DontClick)
0159             return;
0160         if (strokeType == MTStroke::bumped_mouse)
0161             return;
0162 
0163         if (strokeType == MTStroke::RightClick || strokeType == MTStroke::UpperRightStroke)
0164             RightClick();
0165         else if (strokeType == MTStroke::DoubleClick || strokeType == MTStroke::LowerLeftStroke)
0166             DoubleClick();
0167         else
0168             normalClick();
0169     }
0170 }
0171 
0172 void KMouseTool::normalClick()
0173 {
0174     if (mSmart_drag_on) {
0175         if (!mMouse_is_down) {
0176             LeftDn();
0177             mMouse_is_down = true;
0178             mTick_count = 0;
0179             playTickSound();
0180         } else if (mMouse_is_down) {
0181             LeftUp();
0182             mMouse_is_down = false;
0183             mTick_count = mMax_ticks;
0184         }
0185     } else {
0186         // not smart_drag_on
0187         LeftClick();
0188         playTickSound();
0189     }
0190 }
0191 
0192 // This function isn't happy yet.
0193 void KMouseTool::playTickSound()
0194 {
0195     if (!mPlaySound)
0196         return;
0197 
0198     mPlayer->setSource(QUrl::fromLocalFile(mSoundFileName));
0199     mPlayer->play();
0200 }
0201 
0202 KMouseTool::KMouseTool(QWidget *parent, const char *name)
0203     : QWidget(parent)
0204     , mHelpMenu(new KHelpMenu(nullptr))
0205 {
0206     setupUi(this);
0207     setObjectName(QLatin1String(name));
0208     init_vars();
0209     resetSettings();
0210 
0211     connect(movementEdit, SIGNAL(valueChanged(int)), this, SLOT(settingsChanged()));
0212     connect(dwellTimeEdit, SIGNAL(valueChanged(int)), this, SLOT(settingsChanged()));
0213     connect(dragTimeEdit, SIGNAL(valueChanged(int)), this, SLOT(settingsChanged()));
0214     connect(cbDrag, &QCheckBox::stateChanged, this, &KMouseTool::settingsChanged);
0215     connect(cbStroke, &QCheckBox::stateChanged, this, &KMouseTool::settingsChanged);
0216     connect(cbClick, &QCheckBox::stateChanged, this, &KMouseTool::settingsChanged);
0217     connect(cbStart, &QCheckBox::stateChanged, this, &KMouseTool::settingsChanged);
0218 
0219     connect(buttonStartStop, &QAbstractButton::clicked, this, &KMouseTool::startStopSelected);
0220     connect(buttonBoxSettings->button(QDialogButtonBox::RestoreDefaults), &QAbstractButton::clicked, this, &KMouseTool::defaultSelected);
0221     connect(buttonBoxSettings->button(QDialogButtonBox::Reset), &QAbstractButton::clicked, this, &KMouseTool::resetSelected);
0222     connect(buttonBoxSettings->button(QDialogButtonBox::Apply), &QAbstractButton::clicked, this, &KMouseTool::applySelected);
0223     connect(buttonBox, &QDialogButtonBox::helpRequested, this, &KMouseTool::helpSelected);
0224     connect(buttonBox->button(QDialogButtonBox::Close), &QAbstractButton::clicked, this, &KMouseTool::closeSelected);
0225     KGuiItem::assign(mButtonQuit, KStandardGuiItem::quit());
0226     connect(mButtonQuit, &QAbstractButton::clicked, this, &KMouseTool::quitSelected);
0227 
0228     // When we first start, it's nice to not click immediately.
0229     // So, tell MT we're just starting
0230     mMousetool_just_started = true;
0231 
0232     startTimer(100);
0233     mTrayIcon = new KMouseToolTray(this);
0234     updateStartStopText();
0235     connect(mTrayIcon, &KMouseToolTray::startStopSelected, this, &KMouseTool::startStopSelected);
0236     connect(mTrayIcon, &KMouseToolTray::configureSelected, this, &KMouseTool::configureSelected);
0237     connect(mTrayIcon, &KMouseToolTray::aboutSelected, this, &KMouseTool::aboutSelected);
0238     connect(mTrayIcon, &KMouseToolTray::helpSelected, this, &KMouseTool::helpSelected);
0239 }
0240 
0241 KMouseTool::~KMouseTool()
0242 {
0243 }
0244 
0245 // *********************************************************
0246 // Raw X functions
0247 // Begin mouse monitoring and event generation code.
0248 // these should be moved to a separate file.
0249 void getMouseButtons()
0250 {
0251     unsigned char buttonMap[3];
0252     const int buttonCount = XGetPointerMapping(display, buttonMap, 3);
0253     switch (buttonCount) {
0254     case 0:
0255     case 1:
0256         // ### should not happen
0257         leftButton = 1;
0258         rightButton = 1;
0259         break;
0260     case 2:
0261         leftButton = buttonMap[0];
0262         rightButton = buttonMap[1];
0263         break;
0264     default:
0265         leftButton = buttonMap[0];
0266         rightButton = buttonMap[2];
0267         break;
0268     }
0269 }
0270 
0271 void LeftClick()
0272 {
0273     XTestFakeButtonEvent(display, leftButton, true, 0);
0274     XTestFakeButtonEvent(display, leftButton, false, 0);
0275 }
0276 
0277 void DoubleClick()
0278 {
0279     XTestFakeButtonEvent(display, leftButton, true, 0);
0280     XTestFakeButtonEvent(display, leftButton, false, 100);
0281     XTestFakeButtonEvent(display, leftButton, true, 200);
0282     XTestFakeButtonEvent(display, leftButton, false, 300);
0283 }
0284 
0285 void RightClick()
0286 {
0287     XTestFakeButtonEvent(display, rightButton, true, 0);
0288     XTestFakeButtonEvent(display, rightButton, false, 0);
0289 }
0290 
0291 void LeftDn()
0292 {
0293     XTestFakeButtonEvent(display, leftButton, true, 0);
0294 }
0295 
0296 void LeftUp()
0297 {
0298     XTestFakeButtonEvent(display, leftButton, false, 0);
0299 }
0300 
0301 void queryPointer(Window *pidRoot, int *pnRx, int *pnRy, int *pnX, int *pnY, unsigned int *puMask)
0302 {
0303     Window id2, idRoot;
0304     int screen_num;
0305 
0306     screen_num = DefaultScreen(display);
0307     idRoot = RootWindow(display, screen_num);
0308     XQueryPointer(display, idRoot, &idRoot, &id2, pnRx, pnRy, pnX, pnY, puMask);
0309 
0310     *pidRoot = idRoot;
0311 }
0312 
0313 // return 1 if cursor has moved, 0 otherwise
0314 int CursorHasMoved(int minMovement)
0315 {
0316     static int nOldRootX = -1;
0317     static int nOldRootY = -1;
0318 
0319     // XEvent evButtonEvent;
0320     Window idRootWin;
0321     int nRootX, nRootY, nWinX, nWinY;
0322     unsigned int uintMask;
0323 
0324     queryPointer(&idRootWin, &nRootX, &nRootY, &nWinX, &nWinY, &uintMask);
0325 
0326     int nRes = 0;
0327     if ((nRootX > nOldRootX ? nRootX - nOldRootX : nOldRootX - nRootX) >= minMovement)
0328         nRes = 1;
0329     if ((nRootY > nOldRootY ? nRootY - nOldRootY : nOldRootY - nRootY) >= minMovement)
0330         nRes = 1;
0331 
0332     currentXPosition = nRootX;
0333     currentYPosition = nRootY;
0334 
0335     if (nRes) {
0336         nOldRootX = nRootX;
0337         nOldRootY = nRootY;
0338     }
0339 
0340     return nRes;
0341 }
0342 // End mouse monitoring and event creation code
0343 
0344 bool KMouseTool::isAutostart()
0345 {
0346     QString sym = mAutostartdirname;
0347     sym += QLatin1String("kmousetool"); // sym is now full path to symlink
0348     QFileInfo fi(sym);
0349 
0350     return fi.exists();
0351 }
0352 
0353 void KMouseTool::setAutostart(bool start)
0354 {
0355     QString sym = mAutostartdirname;
0356     sym += QLatin1String("kmousetool"); // sym is now full path to symlink
0357     QFileInfo fi(sym);
0358 
0359     if (start) {
0360         if (!fi.exists()) // if it doesn't exist, make it
0361             QFile(mAppfilename).link(sym);
0362     } else {
0363         if (fi.exists()) // if it exists, delete it
0364             QFile(sym).remove();
0365     }
0366 }
0367 
0368 bool KMouseTool::applySettings()
0369 {
0370     int drag, dwell;
0371 
0372     dwell = dwellTimeEdit->value();
0373     drag = dragTimeEdit->value();
0374 
0375     // The drag time must be less than the dwell time
0376     if (dwell < drag) {
0377         KMessageBox::error(this, i18n("The drag time must be less than or equal to the dwell time."), i18n("Invalid Value"));
0378         return false;
0379     }
0380 
0381     // if we got here, we must be okay.
0382     mMin_movement = movementEdit->value();
0383     mSmart_drag_on = cbDrag->isChecked();
0384     mPlaySound = cbClick->isChecked();
0385     mStrokesEnabled = cbStroke->isChecked();
0386     setAutostart(cbStart->isChecked());
0387 
0388     mDwell_time = dwell;
0389     mDrag_time = drag;
0390     mTick_count = mMax_ticks;
0391 
0392     saveOptions();
0393     settingsChanged();
0394     return true;
0395 }
0396 
0397 // Save options to kmousetoolrc file
0398 void KMouseTool::loadOptions()
0399 {
0400     KConfigGroup cfg = KSharedConfig::openConfig()->group(QStringLiteral("UserOptions"));
0401 
0402     mPlaySound = cfg.readEntry("AudibleClick", false);
0403     mSmart_drag_on = cfg.readEntry("SmartDrag", false);
0404 
0405     mDwell_time = cfg.readEntry("DwellTime", 5);
0406     mDrag_time = cfg.readEntry("DragTime", 3);
0407     mMin_movement = cfg.readEntry("Movement", 5);
0408     mStrokesEnabled = cfg.readEntry("strokesEnabled", false);
0409 
0410     QPoint p;
0411     int x = cfg.readEntry("x", 0);
0412     int y = cfg.readEntry("y", 0);
0413     p.setX(x);
0414     p.setY(y);
0415     move(p);
0416 
0417     mMousetool_is_running = cfg.readEntry("MouseToolIsRunning", false);
0418     display = XOpenDisplay(nullptr);
0419 }
0420 
0421 // Save options to kmousetoolrc file
0422 void KMouseTool::saveOptions()
0423 {
0424     QPoint p = pos();
0425     int x = p.x();
0426     int y = p.y();
0427 
0428     KConfigGroup cfg = KSharedConfig::openConfig()->group(QStringLiteral("ProgramOptions"));
0429     cfg = KSharedConfig::openConfig()->group(QStringLiteral("UserOptions"));
0430     cfg.writeEntry("x", x);
0431     cfg.writeEntry("y", y);
0432     cfg.writeEntry("strokesEnabled", mStrokesEnabled);
0433     cfg.writeEntry("IsMinimized", isHidden());
0434     cfg.writeEntry("DwellTime", mDwell_time);
0435     cfg.writeEntry("DragTime", mDrag_time);
0436     cfg.writeEntry("Movement", mMin_movement);
0437     cfg.writeEntry("SmartDrag", mSmart_drag_on);
0438     cfg.writeEntry("AudibleClick", mPlaySound);
0439     cfg.writeEntry("MouseToolIsRunning", mMousetool_is_running);
0440     cfg.sync();
0441 }
0442 
0443 void KMouseTool::updateStartStopText()
0444 {
0445     if (mMousetool_is_running) {
0446         buttonStartStop->setText(i18n("&Stop"));
0447     } else {
0448         buttonStartStop->setText(i18nc("Start tracking the mouse", "&Start"));
0449     }
0450     mTrayIcon->updateStartStopText(mMousetool_is_running);
0451 }
0452 
0453 bool KMouseTool::newSettings()
0454 {
0455     return ((mDwell_time != dwellTimeEdit->value()) || (mDrag_time != dragTimeEdit->value()) || (mMin_movement != movementEdit->value())
0456             || (mSmart_drag_on != cbDrag->isChecked()) || (mPlaySound != cbClick->isChecked()) || (mStrokesEnabled != cbStroke->isChecked())
0457             || (isAutostart() != cbStart->isChecked()));
0458 }
0459 
0460 bool KMouseTool::defaultSettings()
0461 {
0462     return ((5 == dwellTimeEdit->value()) && (3 == dragTimeEdit->value()) && (5 == movementEdit->value()) && !cbDrag->isChecked() && !cbClick->isChecked()
0463             && !cbStroke->isChecked() && !cbStart->isChecked());
0464 }
0465 
0466 /******** SLOTS **********/
0467 
0468 // Value or state changed
0469 void KMouseTool::settingsChanged()
0470 {
0471     buttonBoxSettings->button(QDialogButtonBox::Reset)->setEnabled(newSettings());
0472     buttonBoxSettings->button(QDialogButtonBox::Apply)->setEnabled(newSettings());
0473     buttonBoxSettings->button(QDialogButtonBox::RestoreDefaults)->setDisabled(defaultSettings());
0474 }
0475 
0476 // Buttons within the dialog
0477 void KMouseTool::startStopSelected()
0478 {
0479     mMousetool_is_running = !mMousetool_is_running;
0480     updateStartStopText();
0481 }
0482 
0483 void KMouseTool::defaultSelected()
0484 {
0485     setDefaultSettings();
0486 }
0487 
0488 void KMouseTool::resetSelected()
0489 {
0490     resetSettings();
0491 }
0492 
0493 void KMouseTool::applySelected()
0494 {
0495     applySettings();
0496 }
0497 
0498 // Buttons at the bottom of the dialog
0499 void KMouseTool::helpSelected()
0500 {
0501     mHelpMenu->appHelpActivated();
0502 }
0503 
0504 void KMouseTool::closeSelected()
0505 {
0506     if (newSettings()) {
0507         int answer =
0508             KMessageBox::questionTwoActionsCancel(this,
0509                                                   i18n("There are unsaved changes in the active module.\nDo you want to apply the changes before closing "
0510                                                        "the configuration window or discard the changes?"),
0511                                                   i18n("Closing Configuration Window"),
0512                                                   KStandardGuiItem::apply(),
0513                                                   KStandardGuiItem::discard(),
0514                                                   KStandardGuiItem::cancel(),
0515                                                   QStringLiteral("AutomaticSave"));
0516         if (answer == KMessageBox::PrimaryAction)
0517             applySettings();
0518         else if (answer == KMessageBox::SecondaryAction)
0519             resetSettings();
0520         if (answer != KMessageBox::Cancel)
0521             hide();
0522     } else {
0523         hide();
0524     }
0525 }
0526 
0527 void KMouseTool::quitSelected()
0528 {
0529     if (newSettings()) {
0530         int answer = KMessageBox::questionTwoActionsCancel(
0531             this,
0532             i18n("There are unsaved changes in the active module.\nDo you want to apply the changes before quitting KMousetool or discard the changes?"),
0533             i18n("Quitting KMousetool"),
0534             KStandardGuiItem::apply(),
0535             KStandardGuiItem::discard(),
0536             KStandardGuiItem::cancel(),
0537             QStringLiteral("AutomaticSave"));
0538         if (answer == KMessageBox::PrimaryAction)
0539             applySettings();
0540         if (answer != KMessageBox::Cancel) {
0541             saveOptions();
0542             qApp->quit();
0543         }
0544     } else {
0545         saveOptions();
0546         qApp->quit();
0547     }
0548 }
0549 
0550 // Menu functions
0551 void KMouseTool::configureSelected()
0552 {
0553     show();
0554     raise();
0555     activateWindow();
0556 }
0557 
0558 void KMouseTool::aboutSelected()
0559 {
0560     mHelpMenu->aboutApplication();
0561 }
0562 
0563 KMouseToolTray::KMouseToolTray(QWidget *parent)
0564     : KStatusNotifierItem(parent)
0565 {
0566     setStatus(KStatusNotifierItem::Active);
0567     mStartStopAct = contextMenu()->addAction(i18nc("Start tracking the mouse", "&Start"), this, &KMouseToolTray::startStopSelected);
0568     contextMenu()->addSeparator();
0569     QAction *act = contextMenu()->addAction(i18n("&Configure KMouseTool..."), this, &KMouseToolTray::configureSelected);
0570     act->setIcon(QIcon::fromTheme(QStringLiteral("configure")));
0571     contextMenu()->addSeparator();
0572     act = contextMenu()->addAction(i18n("KMousetool &Handbook"), this, &KMouseToolTray::helpSelected);
0573     act->setIcon(QIcon::fromTheme(QStringLiteral("help-contents")));
0574     act = contextMenu()->addAction(i18n("&About KMouseTool"), this, &KMouseToolTray::aboutSelected);
0575     act->setIcon(QIcon::fromTheme(QStringLiteral("kmousetool")));
0576 }
0577 
0578 KMouseToolTray::~KMouseToolTray()
0579 {
0580 }
0581 
0582 void KMouseToolTray::updateStartStopText(bool mousetool_is_running)
0583 {
0584     QIcon icon;
0585 
0586     if (mousetool_is_running) {
0587         mStartStopAct->setText(i18n("&Stop"));
0588         icon = QIcon::fromTheme(QStringLiteral("kmousetool_on"));
0589     } else {
0590         mStartStopAct->setText(i18nc("Start tracking the mouse", "&Start"));
0591         icon = QIcon::fromTheme(QStringLiteral("kmousetool_off"));
0592     }
0593     setIconByPixmap(icon);
0594 }
0595 
0596 #include "moc_kmousetool.cpp"