1 /*
2     SPDX-FileCopyrightText: 2002-2003 Jeff Roush <jeff@mousetool.com>
3     SPDX-FileCopyrightText: 2003 Olaf Schmidt <ojschmidt@kde.org>
4     SPDX-FileCopyrightText: 2003 Gunnar Schmi Dt <gunnar@schmi-dt.de>
5     SPDX-License-Identifier: GPL-2.0-or-later
6 */
7 
8 #include "kmousetool.h"
9 
10 // Needs to be be before X11/Intrinsic.h because of qtextstream.h
11 #include <phonon/MediaObject>
12 
13 #include <X11/Xmd.h>
14 #include <X11/Intrinsic.h>     /* Intrinsics Definitions*/
15 #include <X11/StringDefs.h>    /* Standard Name-String definitions*/
16 #include <X11/extensions/xtestext1.h>    /* Standard Name-String definitions*/
17 #include <X11/extensions/XTest.h>    /* Standard Name-String definitions*/
18 #include <fixx11h.h>
19 
20 #include <QAbstractEventDispatcher>
21 #include <QApplication>
22 #include <QDesktopWidget>
23 #include <QIcon>
24 #include <QMenu>
25 #include <QStandardPaths>
26 #include <QFile>
27 #include <QFileInfo>
28 
29 #include <KConfig>
30 #include <KConfigGroup>
31 #include <KHelpMenu>
32 #include <KIconLoader>
33 #include <KLocalizedString>
34 #include <KMessageBox>
35 #include <KSharedConfig>
36 #include <KStandardGuiItem>
37 
38 int currentXPosition;
39 int currentYPosition;
40 
41 #undef long
42 
43 int leftButton;
44 int rightButton;
45 
46 /**
47 * Gnarly X functions
48 */
49 void queryPointer(Window *pidRoot, int *pnRx, int *pnRy, int *pnX, int *pnY, unsigned int *puMask);
50 int CursorHasMoved(int minMovement);
51 void getMouseButtons();
52 void LeftClick();
53 void RightClick();
54 void DoubleClick();
55 void LeftDn();
56 void LeftUp();
57 
58 // Declarations
59 Display *display;
60 
61 
init_vars()62 void KMouseTool::init_vars()
63 {
64     continue_timer = 1;
65     tick_count = 0;
66     max_ticks = dwell_time+1;
67     mouse_is_down = false;
68 
69     loadOptions();
70 
71     // If the ~/.mousetool directory doesn't exist, create it
72     mSoundFileName = QStandardPaths::locate(QStandardPaths::DataLocation, QStringLiteral( "sounds/mousetool_tap.wav" ));
73     mplayer = Phonon::createPlayer(Phonon::AccessibilityCategory);
74     mplayer->setParent(this);
75 
76     // find application file
77     appfilename = QStandardPaths::findExecutable(QStringLiteral( "kmousetool" ));
78 
79     // find the user's autostart directory
80     autostartdirname = QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation) + QStringLiteral("/autostart-scripts/");
81 
82     QDesktopWidget *d = QApplication::desktop();
83     int w = d->width();
84     int h = d->height();
85     MTStroke::setUpperLeft(0,0);
86     MTStroke::setUpperRight(w-1,0);
87     MTStroke::setLowerLeft(0,h-1);
88     MTStroke::setLowerRight(w-1,h-1);
89 
90     buttonQuit = buttonBox->addButton(QString(), QDialogButtonBox::RejectRole);
91 }
92 
resetSettings()93 void KMouseTool::resetSettings()
94 {
95     cbDrag ->setChecked(smart_drag_on);
96     cbStart->setChecked(isAutostart());
97     cbClick->setChecked(playSound);
98     cbStroke->setChecked(strokesEnabled);
99     movementEdit->setValue(min_movement);
100     dwellTimeEdit->setValue(dwell_time);
101     dragTimeEdit->setValue(drag_time);
102     settingsChanged();
103 }
104 
setDefaultSettings()105 void KMouseTool::setDefaultSettings()
106 {
107     cbDrag ->setChecked(false);
108     cbStart->setChecked(false);
109     cbClick->setChecked(false);
110     cbStroke->setChecked(false);
111     movementEdit->setValue(5);
112     dwellTimeEdit->setValue(5);
113     dragTimeEdit->setValue(3);
114     settingsChanged();
115 }
116 
117 
timerEvent(QTimerEvent *)118 void KMouseTool::timerEvent( QTimerEvent * )
119 {
120     if (!mousetool_is_running)
121         return;
122 
123     if (!continue_timer) {
124         QAbstractEventDispatcher::instance()->unregisterTimers(this);
125         return;
126     }
127 
128     max_ticks = dwell_time + drag_time;
129     stroke.addPt(currentXPosition, currentYPosition);
130 
131     moving = moving?CursorHasMoved(1):CursorHasMoved(min_movement);
132     if (moving) {
133         if (mousetool_just_started) {
134             mousetool_just_started = false;
135             tick_count = max_ticks;
136         } else {
137             tick_count = 0;
138         }
139         return;
140     }
141 
142     if (tick_count<max_ticks)
143         tick_count++;
144 
145     // If the mouse has paused ...
146     if (tick_count==dwell_time) {
147         int strokeType = stroke.getStrokeType();
148         getMouseButtons();
149 
150         // if we're dragging the mouse, ignore stroke type
151         if (mouse_is_down) {
152             normalClick();
153             return;
154         }
155 
156         if (!strokesEnabled) {
157             normalClick();
158             return;
159         }
160         if (strokeType == MTStroke::DontClick)
161             return;
162         if (strokeType==MTStroke::bumped_mouse)
163             return;
164 
165         if (strokeType==MTStroke::RightClick || strokeType==MTStroke::UpperRightStroke)
166             RightClick();
167         else if (strokeType==MTStroke::DoubleClick || strokeType==MTStroke::LowerLeftStroke)
168             DoubleClick();
169         else
170             normalClick();
171     }
172 }
173 
normalClick()174 void KMouseTool::normalClick()
175 {
176     if (smart_drag_on) {
177         if (!mouse_is_down) {
178             LeftDn();
179             mouse_is_down = true;
180             tick_count = 0;
181             playTickSound();
182         } else if (mouse_is_down) {
183             LeftUp();
184             mouse_is_down = false;
185             tick_count = max_ticks;
186         }
187     } else {
188         // not smart_drag_on
189         LeftClick();
190         playTickSound();
191     }
192 }
193 
194 // This function isn't happy yet.
playTickSound()195 void KMouseTool::playTickSound()
196 {
197     if (!playSound)
198         return;
199 
200     mplayer->setCurrentSource(QUrl::fromLocalFile(mSoundFileName));
201     mplayer->play();
202 }
203 
KMouseTool(QWidget * parent,const char * name)204 KMouseTool::KMouseTool(QWidget *parent, const char *name)
205  : QWidget(parent), helpMenu(new KHelpMenu(nullptr))
206 {
207     setupUi(this);
208     setObjectName( QLatin1String( name ));
209     init_vars();
210     resetSettings();
211 
212     connect(movementEdit, SIGNAL(valueChanged(int)), this, SLOT(settingsChanged()));
213     connect(dwellTimeEdit, SIGNAL(valueChanged(int)), this, SLOT(settingsChanged()));
214     connect(dragTimeEdit, SIGNAL(valueChanged(int)), this, SLOT(settingsChanged()));
215     connect(cbDrag, &QCheckBox::stateChanged, this, &KMouseTool::settingsChanged);
216     connect(cbStroke, &QCheckBox::stateChanged, this, &KMouseTool::settingsChanged);
217     connect(cbClick, &QCheckBox::stateChanged, this, &KMouseTool::settingsChanged);
218     connect(cbStart, &QCheckBox::stateChanged, this, &KMouseTool::settingsChanged);
219 
220     connect(buttonStartStop, &QAbstractButton::clicked, this, &KMouseTool::startStopSelected);
221     connect(buttonBoxSettings->button(QDialogButtonBox::RestoreDefaults), &QAbstractButton::clicked, this, &KMouseTool::defaultSelected);
222     connect(buttonBoxSettings->button(QDialogButtonBox::Reset), &QAbstractButton::clicked, this, &KMouseTool::resetSelected);
223     connect(buttonBoxSettings->button(QDialogButtonBox::Apply), &QAbstractButton::clicked, this, &KMouseTool::applySelected);
224     connect(buttonBox, &QDialogButtonBox::helpRequested, this, &KMouseTool::helpSelected);
225     connect(buttonBox->button(QDialogButtonBox::Close), &QAbstractButton::clicked, this, &KMouseTool::closeSelected);
226     KGuiItem::assign(buttonQuit, KStandardGuiItem::quit());
227     connect(buttonQuit, &QAbstractButton::clicked, this, &KMouseTool::quitSelected);
228 
229     // When we first start, it's nice to not click immediately.
230     // So, tell MT we're just starting
231     mousetool_just_started = true;
232 
233     startTimer(100);
234     trayIcon = new KMouseToolTray (this);
235     updateStartStopText ();
236     connect(trayIcon, &KMouseToolTray::startStopSelected, this, &KMouseTool::startStopSelected);
237     connect(trayIcon, &KMouseToolTray::configureSelected, this, &KMouseTool::configureSelected);
238     connect(trayIcon, &KMouseToolTray::aboutSelected, this, &KMouseTool::aboutSelected);
239     connect(trayIcon, &KMouseToolTray::helpSelected, this, &KMouseTool::helpSelected);
240     connect(trayIcon, SIGNAL(quitSelected()), this, SLOT(quitSelected()));
241 }
242 
~KMouseTool()243 KMouseTool::~KMouseTool()
244 {
245 }
246 
247 // *********************************************************
248 // Raw X functions
249 // Begin mouse monitoring and event generation code.
250 // these should be moved to a separate file.
getMouseButtons()251 void getMouseButtons()
252 {
253     unsigned char buttonMap[3];
254     const int buttonCount = XGetPointerMapping(display, buttonMap, 3);
255     switch (buttonCount)
256     {
257     case 0:
258     case 1:
259         // ### should not happen
260         leftButton = 1;
261         rightButton = 1;
262         break;
263     case 2:
264         leftButton = buttonMap[0];
265         rightButton = buttonMap[1];
266         break;
267     default:
268         leftButton = buttonMap[0];
269         rightButton = buttonMap[2];
270         break;
271     }
272 }
273 
LeftClick()274 void LeftClick()
275 {
276     XTestFakeButtonEvent(display, leftButton, true, 0);
277     XTestFakeButtonEvent(display, leftButton, false, 0);
278 }
279 
DoubleClick()280 void DoubleClick()
281 {
282     XTestFakeButtonEvent(display, leftButton, true, 0);
283     XTestFakeButtonEvent(display, leftButton, false, 100);
284     XTestFakeButtonEvent(display, leftButton, true, 200);
285     XTestFakeButtonEvent(display, leftButton, false, 300);
286 }
287 
RightClick()288 void RightClick()
289 {
290     XTestFakeButtonEvent(display, rightButton, true, 0);
291     XTestFakeButtonEvent(display, rightButton, false, 0);
292 }
293 
LeftDn()294 void LeftDn()
295 {
296     XTestFakeButtonEvent(display, leftButton, true, 0);
297 }
298 
LeftUp()299 void LeftUp()
300 {
301     XTestFakeButtonEvent(display, leftButton, false, 0);
302 }
303 
304 
queryPointer(Window * pidRoot,int * pnRx,int * pnRy,int * pnX,int * pnY,unsigned int * puMask)305 void queryPointer(Window *pidRoot, int *pnRx, int *pnRy, int *pnX, int *pnY, unsigned int *puMask)
306 {
307     Window id2, idRoot;
308     int screen_num;
309 
310     screen_num = DefaultScreen(display);
311     idRoot = RootWindow(display,screen_num);
312     XQueryPointer(display, idRoot, &idRoot, &id2, pnRx, pnRy, pnX, pnY, puMask);
313 
314     *pidRoot = idRoot;
315 }
316 
317 // return 1 if cursor has moved, 0 otherwise
CursorHasMoved(int minMovement)318 int CursorHasMoved (int minMovement)
319 {
320     static int nOldRootX = -1;
321     static int nOldRootY = -1;
322 
323     // XEvent evButtonEvent;
324     Window idRootWin;
325     int nRootX, nRootY, nWinX, nWinY;
326     unsigned int uintMask;
327 
328     queryPointer(&idRootWin, &nRootX, &nRootY, &nWinX, &nWinY, &uintMask);
329 
330     int nRes = 0;
331     if ((nRootX>nOldRootX?nRootX-nOldRootX:nOldRootX-nRootX) >= minMovement)
332         nRes = 1;
333     if ((nRootY>nOldRootY?nRootY-nOldRootY:nOldRootY-nRootY) >= minMovement)
334         nRes = 1;
335 
336     currentXPosition = nRootX;
337     currentYPosition = nRootY;
338 
339     if (nRes) {
340         nOldRootX = nRootX;
341         nOldRootY = nRootY;
342     }
343 
344     return nRes;
345 }
346 // End mouse monitoring and event creation code
347 
348 
isAutostart()349 bool KMouseTool::isAutostart()
350 {
351     QString sym = autostartdirname;
352     sym += QLatin1String( "kmousetool" ); // sym is now full path to symlink
353     QFileInfo fi(sym);
354 
355     return fi.exists();
356 }
357 
setAutostart(bool start)358 void KMouseTool::setAutostart (bool start)
359 {
360     QString sym = autostartdirname;
361     sym += QLatin1String( "kmousetool" ); // sym is now full path to symlink
362     QFileInfo fi(sym);
363 
364     if (start) {
365         if (!fi.exists()) // if it doesn't exist, make it
366             QFile(appfilename).link(sym);
367     } else {
368         if (fi.exists()) // if it exists, delete it
369             QFile(sym).remove();
370     }
371 }
372 
applySettings()373 bool KMouseTool::applySettings()
374 {
375     int drag, dwell;
376 
377     dwell = dwellTimeEdit->value();
378     drag = dragTimeEdit->value() ;
379 
380     // The drag time must be less than the dwell time
381     if ( dwell < drag) {
382         KMessageBox::sorry(this, i18n("The drag time must be less than or equal to the dwell time."), i18n("Invalid Value"));
383         return false;
384     }
385 
386     // if we got here, we must be okay.
387     min_movement   = movementEdit->value();
388     smart_drag_on  = cbDrag->isChecked();
389     playSound      = cbClick->isChecked();
390     strokesEnabled = cbStroke->isChecked();
391     setAutostart (cbStart->isChecked());
392 
393     dwell_time = dwell;
394     drag_time  = drag;
395     tick_count = max_ticks;
396 
397     saveOptions();
398     settingsChanged();
399     return true;
400 }
401 
402 // Save options to kmousetoolrc file
loadOptions()403 void KMouseTool::loadOptions()
404 {
405     KConfigGroup cfg = KSharedConfig::openConfig()->group("UserOptions");
406 
407     playSound = cfg.readEntry("AudibleClick", false);
408     smart_drag_on = cfg.readEntry("SmartDrag", false);
409 
410     dwell_time = cfg.readEntry("DwellTime",5);
411     drag_time = cfg.readEntry("DragTime",3);
412     min_movement = cfg.readEntry("Movement",5);
413     strokesEnabled = cfg.readEntry("strokesEnabled", false);
414 
415     QPoint p;
416     int x = cfg.readEntry("x",0);
417     int y = cfg.readEntry("y",0);
418     p.setX(x);
419     p.setY(y);
420     move(p);
421 
422     mousetool_is_running = cfg.readEntry("MouseToolIsRunning", false);
423     display = XOpenDisplay(nullptr);
424 }
425 
426 // Save options to kmousetoolrc file
saveOptions()427 void KMouseTool::saveOptions()
428 {
429     QPoint p = pos();
430     int x = p.x();
431     int y = p.y();
432 
433     KConfigGroup cfg = KSharedConfig::openConfig()->group("ProgramOptions");
434     cfg = KSharedConfig::openConfig()->group("UserOptions");
435     cfg.writeEntry("x", x);
436     cfg.writeEntry("y", y);
437     cfg.writeEntry("strokesEnabled", strokesEnabled);
438     cfg.writeEntry("IsMinimized", isHidden());
439     cfg.writeEntry("DwellTime", dwell_time);
440     cfg.writeEntry("DragTime", drag_time);
441     cfg.writeEntry("Movement", min_movement);
442     cfg.writeEntry("SmartDrag", smart_drag_on);
443     cfg.writeEntry("AudibleClick", playSound);
444     cfg.writeEntry("MouseToolIsRunning", mousetool_is_running);
445     cfg.sync();
446 }
447 
updateStartStopText()448 void KMouseTool::updateStartStopText()
449 {
450     if (mousetool_is_running) {
451         buttonStartStop->setText(i18n("&Stop"));
452     } else {
453         buttonStartStop->setText(i18nc("Start tracking the mouse", "&Start"));
454     }
455     trayIcon->updateStartStopText(mousetool_is_running);
456 }
457 
newSettings()458 bool KMouseTool::newSettings()
459 {
460     return ((dwell_time != dwellTimeEdit->value()) ||
461             (drag_time != dragTimeEdit->value()) ||
462             (min_movement != movementEdit->value()) ||
463             (smart_drag_on != cbDrag->isChecked()) ||
464             (playSound != cbClick->isChecked()) ||
465             (strokesEnabled != cbStroke->isChecked()) ||
466             (isAutostart() != cbStart->isChecked()));
467 }
468 
defaultSettings()469 bool KMouseTool::defaultSettings()
470 {
471     return ((5 == dwellTimeEdit->value()) &&
472             (3 == dragTimeEdit->value()) &&
473             (5 == movementEdit->value()) &&
474             !cbDrag->isChecked() &&
475             !cbClick->isChecked() &&
476             !cbStroke->isChecked() &&
477             !cbStart->isChecked());
478 }
479 
480 /******** SLOTS **********/
481 
482 // Value or state changed
settingsChanged()483 void KMouseTool::settingsChanged ()
484 {
485     buttonBoxSettings->button(QDialogButtonBox::Reset)->setEnabled (newSettings());
486     buttonBoxSettings->button(QDialogButtonBox::Apply)->setEnabled (newSettings());
487     buttonBoxSettings->button(QDialogButtonBox::RestoreDefaults)->setDisabled (defaultSettings());
488 }
489 
490 // Buttons within the dialog
startStopSelected()491 void KMouseTool::startStopSelected()
492 {
493     mousetool_is_running = !mousetool_is_running;
494     updateStartStopText();
495 }
496 
defaultSelected()497 void KMouseTool::defaultSelected()
498 {
499     setDefaultSettings();
500 }
501 
resetSelected()502 void KMouseTool::resetSelected()
503 {
504     resetSettings();
505 }
506 
applySelected()507 void KMouseTool::applySelected()
508 {
509     applySettings();
510 }
511 
512 // Buttons at the bottom of the dialog
helpSelected()513 void KMouseTool::helpSelected()
514 {
515     helpMenu->appHelpActivated();
516 }
517 
closeSelected()518 void KMouseTool::closeSelected()
519 {
520     if (newSettings())
521     {
522         int answer = KMessageBox::questionYesNoCancel (this,
523                                                        i18n("There are unsaved changes in the active module.\nDo you want to apply the changes before closing the configuration window or discard the changes?"),
524                                                        i18n("Closing Configuration Window"),
525                                                        KStandardGuiItem::apply(), KStandardGuiItem::discard(),
526                                                        KStandardGuiItem::cancel(), QStringLiteral( "AutomaticSave" ));
527         if (answer == KMessageBox::Yes)
528             applySettings();
529         else if (answer == KMessageBox::No)
530             resetSettings();
531         if (answer != KMessageBox::Cancel)
532             hide();
533     } else {
534         hide();
535     }
536 }
537 
quitSelected()538 void KMouseTool::quitSelected()
539 {
540     if (newSettings())
541     {
542         int answer = KMessageBox::questionYesNoCancel (this,
543                                                        i18n("There are unsaved changes in the active module.\nDo you want to apply the changes before quitting KMousetool or discard the changes?"),
544                                                        i18n("Quitting KMousetool"),
545                                                        KStandardGuiItem::apply(), KStandardGuiItem::discard(),
546                                                        KStandardGuiItem::cancel(), QStringLiteral( "AutomaticSave" ));
547         if (answer == KMessageBox::Yes)
548             applySettings();
549         if (answer != KMessageBox::Cancel)
550         {
551             saveOptions();
552             qApp->quit();
553         }
554     } else {
555         saveOptions();
556         qApp->quit();
557     }
558 }
559 
560 // Menu functions
configureSelected()561 void KMouseTool::configureSelected()
562 {
563     show();
564     raise();
565     activateWindow();
566 }
567 
aboutSelected()568 void KMouseTool::aboutSelected()
569 {
570     helpMenu->aboutApplication();
571 }
572 
573 
574 
KMouseToolTray(QWidget * parent)575 KMouseToolTray::KMouseToolTray (QWidget *parent) : KStatusNotifierItem(parent)
576 {
577     setStatus(KStatusNotifierItem::Active);
578     startStopAct = contextMenu()->addAction (i18nc("Start tracking the mouse", "&Start"), this, &KMouseToolTray::startStopSelected);
579     contextMenu()->addSeparator();
580     QAction* act;
581     act = contextMenu()->addAction (i18n("&Configure KMouseTool..."), this, &KMouseToolTray::configureSelected);
582     act->setIcon(QIcon::fromTheme(QStringLiteral( "configure" )));
583     contextMenu()->addSeparator();
584     act = contextMenu()->addAction (i18n("KMousetool &Handbook"), this, &KMouseToolTray::helpSelected);
585     act->setIcon(QIcon::fromTheme(QStringLiteral( "help-contents" )));
586     act = contextMenu()->addAction (i18n("&About KMouseTool"), this, &KMouseToolTray::aboutSelected);
587     act->setIcon(QIcon::fromTheme(QStringLiteral( "kmousetool" )));
588 }
589 
~KMouseToolTray()590 KMouseToolTray::~KMouseToolTray() {
591 }
592 
updateStartStopText(bool mousetool_is_running)593 void KMouseToolTray::updateStartStopText(bool mousetool_is_running)
594 {
595     QIcon icon;
596 
597     if (mousetool_is_running) {
598         startStopAct->setText(i18n("&Stop"));
599         icon = KIconLoader::global()->loadIcon(QStringLiteral( "kmousetool_on" ), KIconLoader::Small);
600     } else {
601         startStopAct->setText(i18nc("Start tracking the mouse", "&Start"));
602         icon = KIconLoader::global()->loadIcon(QStringLiteral( "kmousetool_off" ), KIconLoader::Small);
603     }
604     setIconByPixmap (icon);
605 }
606