1 // Copyright 2005-2019 The Mumble Developers. All rights reserved.
2 // Use of this source code is governed by a BSD-style license
3 // that can be found in the LICENSE file at the root of the
4 // Mumble source tree or at <https://www.mumble.info/LICENSE>.
5
6 #include "mumble_pch.hpp"
7
8 #include "GlobalShortcut.h"
9
10 #include "AudioInput.h"
11 #include "ClientUser.h"
12 #include "Channel.h"
13 #include "Database.h"
14 #include "MainWindow.h"
15
16 // We define a global macro called 'g'. This can lead to issues when included code uses 'g' as a type or parameter name (like protobuf 3.7 does). As such, for now, we have to make this our last include.
17 #include "Global.h"
18
19 /**
20 * Used to save the global, unique, platform specific GlobalShortcutEngine.
21 */
22 GlobalShortcutEngine *GlobalShortcutEngine::engine = NULL;
23
GlobalShortcutConfigDialogNew(Settings & st)24 static ConfigWidget *GlobalShortcutConfigDialogNew(Settings &st) {
25 return new GlobalShortcutConfig(st);
26 }
27
28 static ConfigRegistrar registrar(1200, GlobalShortcutConfigDialogNew);
29
30 static const QString UPARROW = QString::fromUtf8("\xE2\x86\x91 ");
31
ShortcutKeyWidget(QWidget * p)32 ShortcutKeyWidget::ShortcutKeyWidget(QWidget *p) : QLineEdit(p) {
33 setReadOnly(true);
34 clearFocus();
35 bModified = false;
36 displayKeys();
37 }
38
getShortcut() const39 QList<QVariant> ShortcutKeyWidget::getShortcut() const {
40 return qlButtons;
41 }
42
setShortcut(const QList<QVariant> & buttons)43 void ShortcutKeyWidget::setShortcut(const QList<QVariant> &buttons) {
44 qlButtons = buttons;
45 displayKeys();
46 }
47
focusInEvent(QFocusEvent *)48 void ShortcutKeyWidget::focusInEvent(QFocusEvent *) {
49 setText(tr("Press Shortcut"));
50
51 QPalette pal=parentWidget()->palette();
52 pal.setColor(QPalette::Base, pal.color(QPalette::Base).dark(120));
53 setPalette(pal);
54
55 setForegroundRole(QPalette::Button);
56 GlobalShortcutEngine::engine->resetMap();
57 connect(GlobalShortcutEngine::engine, SIGNAL(buttonPressed(bool)), this, SLOT(updateKeys(bool)));
58 installEventFilter(this);
59 }
60
focusOutEvent(QFocusEvent * e)61 void ShortcutKeyWidget::focusOutEvent(QFocusEvent *e) {
62 if ((e->reason() == Qt::TabFocusReason) || (e->reason() == Qt::BacktabFocusReason))
63 return;
64
65 setPalette(parentWidget()->palette());
66 clearFocus();
67 disconnect(GlobalShortcutEngine::engine, SIGNAL(buttonPressed(bool)), this, SLOT(updateKeys(bool)));
68 displayKeys();
69 removeEventFilter(this);
70 }
71
eventFilter(QObject *,QEvent * evt)72 bool ShortcutKeyWidget::eventFilter(QObject *, QEvent *evt) {
73 if ((evt->type() == QEvent::KeyPress) || (evt->type() == QEvent::MouseButtonPress))
74 return true;
75 return false;
76 }
77
mouseDoubleClickEvent(QMouseEvent *)78 void ShortcutKeyWidget::mouseDoubleClickEvent(QMouseEvent *) {
79 bModified = true;
80 qlButtons.clear();
81 clearFocus();
82 displayKeys();
83 }
84
updateKeys(bool last)85 void ShortcutKeyWidget::updateKeys(bool last) {
86 qlButtons = GlobalShortcutEngine::engine->qlActiveButtons;
87 bModified = true;
88
89 if (qlButtons.isEmpty())
90 return;
91
92 if (last)
93 clearFocus();
94 else
95 displayKeys(false);
96 }
97
displayKeys(bool last)98 void ShortcutKeyWidget::displayKeys(bool last) {
99 QStringList keys;
100
101 foreach(QVariant button, qlButtons) {
102 QString id = GlobalShortcutEngine::engine->buttonName(button);
103 if (! id.isEmpty())
104 keys << id;
105 }
106 setText(keys.join(QLatin1String(" + ")));
107 emit keySet(keys.count() > 0, last);
108 }
109
ShortcutActionWidget(QWidget * p)110 ShortcutActionWidget::ShortcutActionWidget(QWidget *p) : MUComboBox(p) {
111 int idx = 0;
112
113 insertItem(idx, tr("Unassigned"));
114 setItemData(idx, -1);
115 #ifndef Q_OS_MAC
116 setSizeAdjustPolicy(AdjustToContents);
117 #endif
118
119 idx++;
120
121 foreach(GlobalShortcut *gs, GlobalShortcutEngine::engine->qmShortcuts) {
122 insertItem(idx, gs->name);
123 setItemData(idx, gs->idx);
124 if (! gs->qsToolTip.isEmpty())
125 setItemData(idx, gs->qsToolTip, Qt::ToolTipRole);
126 if (! gs->qsWhatsThis.isEmpty())
127 setItemData(idx, gs->qsWhatsThis, Qt::WhatsThisRole);
128 idx++;
129 }
130 }
131
setIndex(int idx)132 void ShortcutActionWidget::setIndex(int idx) {
133 setCurrentIndex(findData(idx));
134 }
135
index() const136 unsigned int ShortcutActionWidget::index() const {
137 return itemData(currentIndex()).toUInt();
138 }
139
ShortcutToggleWidget(QWidget * p)140 ShortcutToggleWidget::ShortcutToggleWidget(QWidget *p) : MUComboBox(p) {
141 int idx = 0;
142
143 insertItem(idx, tr("Off"));
144 setItemData(idx, -1);
145 idx++;
146
147 insertItem(idx, tr("Toggle"));
148 setItemData(idx, 0);
149 idx++;
150
151 insertItem(idx, tr("On"));
152 setItemData(idx, 1);
153 idx++;
154 }
155
setIndex(int idx)156 void ShortcutToggleWidget::setIndex(int idx) {
157 setCurrentIndex(findData(idx));
158 }
159
index() const160 int ShortcutToggleWidget::index() const {
161 return itemData(currentIndex()).toInt();
162 }
163
iterateChannelChildren(QTreeWidgetItem * root,Channel * chan,QMap<int,QTreeWidgetItem * > & map)164 void iterateChannelChildren(QTreeWidgetItem *root, Channel *chan, QMap<int, QTreeWidgetItem *> &map) {
165 foreach(Channel *c, chan->qlChannels) {
166 QTreeWidgetItem *sub = new QTreeWidgetItem(root, QStringList(c->qsName));
167 sub->setData(0, Qt::UserRole, c->iId);
168 map.insert(c->iId, sub);
169 iterateChannelChildren(sub, c, map);
170 }
171 }
172
ShortcutTargetDialog(const ShortcutTarget & st,QWidget * pw)173 ShortcutTargetDialog::ShortcutTargetDialog(const ShortcutTarget &st, QWidget *pw) : QDialog(pw) {
174 stTarget = st;
175 setupUi(this);
176
177 // Load current shortcut configuration
178 qcbForceCenter->setChecked(st.bForceCenter);
179 qgbModifiers->setVisible(true);
180
181 if (st.bUsers) {
182 qrbUsers->setChecked(true);
183 qswStack->setCurrentWidget(qwUserPage);
184 } else {
185 qrbChannel->setChecked(true);
186 qswStack->setCurrentWidget(qwChannelPage);
187 }
188
189 qcbLinks->setChecked(st.bLinks);
190 qcbChildren->setChecked(st.bChildren);
191
192 // Insert all known friends into the possible targets list
193 const QMap<QString, QString> &friends = g.db->getFriends();
194 if (! friends.isEmpty()) {
195 QMap<QString, QString>::const_iterator i;
196 for (i = friends.constBegin(); i != friends.constEnd(); ++i) {
197 qcbUser->addItem(i.key(), i.value());
198 qmHashNames.insert(i.value(), i.key());
199 }
200 qcbUser->insertSeparator(qcbUser->count());
201 }
202
203 // If we are connected to a server also add all connected players with certificates to the list
204 if (g.uiSession) {
205 QMap<QString, QString> others;
206 QMap<QString, QString>::const_iterator i;
207
208 QReadLocker lock(& ClientUser::c_qrwlUsers);
209 foreach(ClientUser *p, ClientUser::c_qmUsers) {
210 if ((p->uiSession != g.uiSession) && p->qsFriendName.isEmpty() && ! p->qsHash.isEmpty()) {
211 others.insert(p->qsName, p->qsHash);
212 qmHashNames.insert(p->qsHash, p->qsName);
213 }
214 }
215
216 for (i = others.constBegin(); i != others.constEnd(); ++i) {
217 qcbUser->addItem(i.key(), i.value());
218 }
219 }
220
221 QMap<QString, QString> users;
222
223 foreach(const QString &hash, st.qlUsers) {
224 if (qmHashNames.contains(hash))
225 users.insert(qmHashNames.value(hash), hash);
226 else
227 users.insert(QString::fromLatin1("#%1").arg(hash), hash);
228 }
229
230 {
231 QMap<QString, QString>::const_iterator i;
232 for (i=users.constBegin(); i != users.constEnd(); ++i) {
233 QListWidgetItem *itm = new QListWidgetItem(i.key());
234 itm->setData(Qt::UserRole, i.value());
235 qlwUsers->addItem(itm);
236 }
237 }
238
239 // Now generate the tree of possible channel targets, first add the default ones
240 QMap<int, QTreeWidgetItem *> qmTree;
241
242 QTreeWidgetItem *root = new QTreeWidgetItem(qtwChannels, QStringList(tr("Root")));
243 root->setData(0, Qt::UserRole, SHORTCUT_TARGET_ROOT);
244 root->setExpanded(true);
245 qmTree.insert(-1, root);
246
247 QTreeWidgetItem *parent_item = new QTreeWidgetItem(root, QStringList(tr("Parent")));
248 parent_item->setData(0, Qt::UserRole, SHORTCUT_TARGET_PARENT);
249 parent_item->setExpanded(true);
250 qmTree.insert(-2, parent_item);
251
252 QTreeWidgetItem *current = new QTreeWidgetItem(parent_item, QStringList(tr("Current")));
253 current->setData(0, Qt::UserRole, SHORTCUT_TARGET_CURRENT);
254 qmTree.insert(-3, current);
255
256 for (int i = 0; i < 8; ++i) {
257 QTreeWidgetItem *sub = new QTreeWidgetItem(current, QStringList(tr("Subchannel #%1").arg(i+1)));
258 sub->setData(0, Qt::UserRole, SHORTCUT_TARGET_SUBCHANNEL - i);
259 qmTree.insert(SHORTCUT_TARGET_SUBCHANNEL - i, sub);
260 }
261
262 for (int i = 0; i < 8; ++i) {
263 QTreeWidgetItem *psub = new QTreeWidgetItem(parent_item, QStringList(UPARROW + tr("Subchannel #%1").arg(i+1)));
264 psub->setData(0, Qt::UserRole, SHORTCUT_TARGET_PARENT_SUBCHANNEL - i);
265 qmTree.insert(SHORTCUT_TARGET_PARENT_SUBCHANNEL - i, psub);
266 }
267
268 // And if we are connected add the channels on the current server
269 if (g.uiSession) {
270 Channel *c = Channel::get(0);
271 QTreeWidgetItem *sroot = new QTreeWidgetItem(qtwChannels, QStringList(c->qsName));
272 qmTree.insert(0, sroot);
273 iterateChannelChildren(sroot, c, qmTree);
274 }
275
276 qtwChannels->sortByColumn(0, Qt::AscendingOrder);
277
278 QTreeWidgetItem *qtwi;
279 if (g.uiSession) {
280 qtwi = qmTree.value(ClientUser::get(g.uiSession)->cChannel->iId);
281 if (qtwi)
282 qtwChannels->scrollToItem(qtwi);
283 }
284
285 qtwi = qmTree.value(st.iChannel);
286 if (qtwi) {
287 qtwChannels->scrollToItem(qtwi);
288 qtwChannels->setCurrentItem(qtwi);
289 }
290
291 qleGroup->setText(stTarget.qsGroup);
292 }
293
target() const294 ShortcutTarget ShortcutTargetDialog::target() const {
295 return stTarget;
296 }
297
accept()298 void ShortcutTargetDialog::accept() {
299 stTarget.bLinks = qcbLinks->isChecked();
300 stTarget.bChildren = qcbChildren->isChecked();
301
302 stTarget.bForceCenter = qcbForceCenter->isChecked();
303
304 stTarget.qlUsers.clear();
305 QList<QListWidgetItem *> ql = qlwUsers->findItems(QString(), Qt::MatchStartsWith);
306 foreach(QListWidgetItem *itm, ql) {
307 stTarget.qlUsers << itm->data(Qt::UserRole).toString();
308 }
309
310 QTreeWidgetItem *qtwi = qtwChannels->currentItem();
311 if (qtwi) {
312 stTarget.iChannel = qtwi->data(0, Qt::UserRole).toInt();
313 stTarget.qsGroup = qleGroup->text().trimmed();
314 }
315
316 QDialog::accept();
317 }
318
on_qrbUsers_clicked()319 void ShortcutTargetDialog::on_qrbUsers_clicked() {
320 stTarget.bUsers = true;
321 qswStack->setCurrentWidget(qwUserPage);
322 }
323
on_qrbChannel_clicked()324 void ShortcutTargetDialog::on_qrbChannel_clicked() {
325 stTarget.bUsers = false;
326 qswStack->setCurrentWidget(qwChannelPage);
327 }
328
on_qpbAdd_clicked()329 void ShortcutTargetDialog::on_qpbAdd_clicked() {
330 if (qcbUser->currentIndex() < 0)
331 return;
332
333 QListWidgetItem *itm = new QListWidgetItem(qcbUser->currentText());
334 itm->setData(Qt::UserRole, qcbUser->itemData(qcbUser->currentIndex()));
335 qlwUsers->addItem(itm);
336 }
337
on_qpbRemove_clicked()338 void ShortcutTargetDialog::on_qpbRemove_clicked() {
339 QListWidgetItem *itm = qlwUsers->currentItem();
340 delete itm;
341 }
342
ShortcutTargetWidget(QWidget * p)343 ShortcutTargetWidget::ShortcutTargetWidget(QWidget *p) : QFrame(p) {
344 qleTarget = new QLineEdit();
345 qleTarget->setReadOnly(true);
346
347 qtbEdit = new QToolButton();
348 qtbEdit->setText(tr("..."));
349 qtbEdit->setFocusPolicy(Qt::ClickFocus);
350 qtbEdit->setObjectName(QLatin1String("qtbEdit"));
351
352 QHBoxLayout *l = new QHBoxLayout(this);
353 l->setContentsMargins(0,0,0,0);
354 l->addWidget(qleTarget, 1);
355 l->addWidget(qtbEdit);
356
357 QMetaObject::connectSlotsByName(this);
358 }
359
360 /**
361 * This function returns a textual representation of the given shortcut target st.
362 */
targetString(const ShortcutTarget & st)363 QString ShortcutTargetWidget::targetString(const ShortcutTarget &st) {
364 if (st.bUsers) {
365 if (! st.qlUsers.isEmpty()) {
366 QMap<QString, QString> hashes;
367
368 QReadLocker lock(& ClientUser::c_qrwlUsers);
369 foreach(ClientUser *p, ClientUser::c_qmUsers) {
370 if (! p->qsHash.isEmpty()) {
371 hashes.insert(p->qsHash, p->qsName);
372 }
373 }
374
375 QStringList users;
376 foreach(const QString &hash, st.qlUsers) {
377 QString name;
378 if (hashes.contains(hash)) {
379 name = hashes.value(hash);
380 } else {
381 name = g.db->getFriend(hash);
382 if (name.isEmpty())
383 name = QString::fromLatin1("#%1").arg(hash);
384 }
385 users << name;
386 }
387
388 users.sort();
389 return users.join(tr(", "));
390 }
391 } else {
392 if (st.iChannel < 0) {
393 switch (st.iChannel) {
394 case SHORTCUT_TARGET_ROOT:
395 return tr("Root");
396 case SHORTCUT_TARGET_PARENT:
397 return tr("Parent");
398 case SHORTCUT_TARGET_CURRENT:
399 return tr("Current");
400 default:
401 if(st.iChannel <= SHORTCUT_TARGET_PARENT_SUBCHANNEL)
402 return (UPARROW + tr("Subchannel #%1").arg(SHORTCUT_TARGET_PARENT_SUBCHANNEL + 1 - st.iChannel));
403 else
404 return tr("Subchannel #%1").arg(SHORTCUT_TARGET_CURRENT - st.iChannel);
405 }
406 } else {
407 Channel *c = Channel::get(st.iChannel);
408 if (c)
409 return c->qsName;
410 else
411 return tr("Invalid");
412 }
413 }
414 return tr("Empty");
415 }
416
target() const417 ShortcutTarget ShortcutTargetWidget::target() const {
418 return stTarget;
419 }
420
setTarget(const ShortcutTarget & st)421 void ShortcutTargetWidget::setTarget(const ShortcutTarget &st) {
422 stTarget = st;
423 qleTarget->setText(ShortcutTargetWidget::targetString(st));
424 }
425
on_qtbEdit_clicked()426 void ShortcutTargetWidget::on_qtbEdit_clicked() {
427 ShortcutTargetDialog *std = new ShortcutTargetDialog(stTarget, this);
428 if (std->exec() == QDialog::Accepted) {
429 stTarget = std->target();
430 qleTarget->setText(ShortcutTargetWidget::targetString(stTarget));
431
432 // Qt bug? Who knows, but since there won't be focusOut events for this widget anymore,
433 // we need to force the commit.
434 QWidget *p = parentWidget();
435 while (p) {
436 if (QAbstractItemView *qaiv = qobject_cast<QAbstractItemView *>(p)) {
437 QStyledItemDelegate *qsid = qobject_cast<QStyledItemDelegate *>(qaiv->itemDelegate());
438 if (qsid) {
439 QMetaObject::invokeMethod(qsid, "_q_commitDataAndCloseEditor",
440 Qt::QueuedConnection, Q_ARG(QWidget*, this));
441 }
442 break;
443 }
444 p = p->parentWidget();
445 }
446 }
447 delete std;
448 }
449
ShortcutDelegate(QObject * p)450 ShortcutDelegate::ShortcutDelegate(QObject *p) : QStyledItemDelegate(p) {
451 QItemEditorFactory *factory = new QItemEditorFactory;
452
453 factory->registerEditor(QVariant::List, new QStandardItemEditorCreator<ShortcutKeyWidget>());
454 factory->registerEditor(QVariant::UInt, new QStandardItemEditorCreator<ShortcutActionWidget>());
455 factory->registerEditor(QVariant::Int, new QStandardItemEditorCreator<ShortcutToggleWidget>());
456 factory->registerEditor(static_cast<QVariant::Type>(QVariant::fromValue(ShortcutTarget()).userType()), new QStandardItemEditorCreator<ShortcutTargetWidget>());
457 factory->registerEditor(QVariant::String, new QStandardItemEditorCreator<QLineEdit>());
458 factory->registerEditor(QVariant::Invalid, new QStandardItemEditorCreator<QWidget>());
459 setItemEditorFactory(factory);
460 }
461
~ShortcutDelegate()462 ShortcutDelegate::~ShortcutDelegate() {
463 delete itemEditorFactory();
464 setItemEditorFactory(NULL);
465 }
466
467 /**
468 * Provides textual representations for the mappings done for the edit behaviour.
469 */
displayText(const QVariant & item,const QLocale & loc) const470 QString ShortcutDelegate::displayText(const QVariant &item, const QLocale &loc) const {
471 if (item.type() == QVariant::List) {
472 return GlobalShortcutEngine::buttonText(item.toList());
473 } else if (item.type() == QVariant::Int) {
474 int v = item.toInt();
475 if (v > 0)
476 return tr("On");
477 else if (v < 0)
478 return tr("Off");
479 else
480 return tr("Toggle");
481 } else if (item.type() == QVariant::UInt) {
482 GlobalShortcut *gs = GlobalShortcutEngine::engine->qmShortcuts.value(item.toInt());
483 if (gs)
484 return gs->name;
485 else
486 return tr("Unassigned");
487 } else if (item.userType() == QVariant::fromValue(ShortcutTarget()).userType()) {
488 return ShortcutTargetWidget::targetString(item.value<ShortcutTarget>());
489 }
490
491 qWarning("ShortcutDelegate::displayText Unknown type %d", item.type());
492
493 return QStyledItemDelegate::displayText(item,loc);
494 }
495
GlobalShortcutConfig(Settings & st)496 GlobalShortcutConfig::GlobalShortcutConfig(Settings &st) : ConfigWidget(st) {
497 setupUi(this);
498 installEventFilter(this);
499
500 bool canSuppress = GlobalShortcutEngine::engine->canSuppress();
501 bool canDisable = GlobalShortcutEngine::engine->canDisable();
502
503 qwWarningContainer->setVisible(false);
504
505 #ifdef Q_OS_WIN
506 qgbWindowsShortcutEngines->setVisible(true);
507 #else
508 qgbWindowsShortcutEngines->setVisible(false);
509 #endif
510
511 qtwShortcuts->setColumnCount(canSuppress ? 4 : 3);
512 qtwShortcuts->setItemDelegate(new ShortcutDelegate(qtwShortcuts));
513
514 #if QT_VERSION >= 0x050000
515 qtwShortcuts->header()->setSectionResizeMode(0, QHeaderView::Fixed);
516 qtwShortcuts->header()->resizeSection(0, 150);
517 qtwShortcuts->header()->setSectionResizeMode(2, QHeaderView::Stretch);
518 if (canSuppress)
519 qtwShortcuts->header()->setSectionResizeMode(3, QHeaderView::ResizeToContents);
520 #else
521 qtwShortcuts->header()->setResizeMode(0, QHeaderView::Fixed);
522 qtwShortcuts->header()->resizeSection(0, 150);
523 qtwShortcuts->header()->setResizeMode(2, QHeaderView::Stretch);
524 if (canSuppress)
525 qtwShortcuts->header()->setResizeMode(3, QHeaderView::ResizeToContents);
526 #endif
527
528
529 qcbEnableGlobalShortcuts->setVisible(canDisable);
530
531 #ifdef Q_OS_MAC
532 // Help Mac users enable accessibility access for Mumble...
533 if (QSysInfo::MacintoshVersion >= QSysInfo::MV_MAVERICKS) {
534 qpbOpenAccessibilityPrefs->setHidden(true);
535 label->setText(tr(
536 "<html><head/><body>"
537 "<p>"
538 "Mumble can currently only use mouse buttons and keyboard modifier keys (Alt, Ctrl, Cmd, etc.) for global shortcuts."
539 "</p>"
540 "<p>"
541 "If you want more flexibility, you can add Mumble as a trusted accessibility program in the Security & Privacy section "
542 "of your Mac's System Preferences."
543 "</p>"
544 "<p>"
545 "In the Security & Privacy preference pane, change to the Privacy tab. Then choose Accessibility (near the bottom) in "
546 "the list to the left. Finally, add Mumble to the list of trusted accessibility programs."
547 "</body></html>"
548 ));
549 }
550 #endif
551 }
552
eventFilter(QObject *,QEvent * e)553 bool GlobalShortcutConfig::eventFilter(QObject* /*object*/, QEvent *e) {
554 #ifdef Q_OS_MAC
555 if (e->type() == QEvent::WindowActivate) {
556 if (! g.s.bSuppressMacEventTapWarning) {
557 qwWarningContainer->setVisible(showWarning());
558 }
559 }
560 #else
561 Q_UNUSED(e)
562 #endif
563 return false;
564 }
565
showWarning() const566 bool GlobalShortcutConfig::showWarning() const {
567 #ifdef Q_OS_MAC
568 # if MAC_OS_X_VERSION_MAX_ALLOWED >= 1090
569 if (QSysInfo::MacintoshVersion >= QSysInfo::MV_MAVERICKS) {
570 return !AXIsProcessTrustedWithOptions(NULL);
571 } else
572 # endif
573 {
574 return !QFile::exists(QLatin1String("/private/var/db/.AccessibilityAPIEnabled"));
575 }
576 #endif
577 return false;
578 }
579
on_qpbOpenAccessibilityPrefs_clicked()580 void GlobalShortcutConfig::on_qpbOpenAccessibilityPrefs_clicked() {
581 QStringList args;
582 args << QLatin1String("/Applications/System Preferences.app");
583 args << QLatin1String("/System/Library/PreferencePanes/UniversalAccessPref.prefPane");
584 (void) QProcess::startDetached(QLatin1String("/usr/bin/open"), args);
585 }
586
on_qpbSkipWarning_clicked()587 void GlobalShortcutConfig::on_qpbSkipWarning_clicked() {
588 // Store to both global and local settings. The 'Skip' is live, as in
589 // we don't expect the user to click Apply for their choice to work.
590 g.s.bSuppressMacEventTapWarning = s.bSuppressMacEventTapWarning = true;
591 qwWarningContainer->setVisible(false);
592 }
593
commit()594 void GlobalShortcutConfig::commit() {
595 qtwShortcuts->closePersistentEditor(qtwShortcuts->currentItem(), qtwShortcuts->currentColumn());
596 }
597
on_qcbEnableGlobalShortcuts_stateChanged(int state)598 void GlobalShortcutConfig::on_qcbEnableGlobalShortcuts_stateChanged(int state) {
599 bool b = state == Qt::Checked;
600 qpbAdd->setEnabled(b);
601 if (!b)
602 qpbRemove->setEnabled(false);
603 else
604 qpbRemove->setEnabled(qtwShortcuts->currentItem() ? true : false);
605 qtwShortcuts->setEnabled(b);
606
607 // We have to enable this here. Otherwise, adding new shortcuts wouldn't work.
608 GlobalShortcutEngine::engine->setEnabled(b);
609 }
610
on_qpbAdd_clicked(bool)611 void GlobalShortcutConfig::on_qpbAdd_clicked(bool) {
612 commit();
613 Shortcut sc;
614 sc.iIndex = -1;
615 sc.bSuppress = false;
616 qlShortcuts << sc;
617 reload();
618 }
619
on_qpbRemove_clicked(bool)620 void GlobalShortcutConfig::on_qpbRemove_clicked(bool) {
621 commit();
622 QTreeWidgetItem *qtwi = qtwShortcuts->currentItem();
623 if (! qtwi)
624 return;
625 int idx = qtwShortcuts->indexOfTopLevelItem(qtwi);
626 delete qtwi;
627 qlShortcuts.removeAt(idx);
628 }
629
on_qtwShortcuts_currentItemChanged(QTreeWidgetItem * item,QTreeWidgetItem *)630 void GlobalShortcutConfig::on_qtwShortcuts_currentItemChanged(QTreeWidgetItem *item, QTreeWidgetItem *) {
631 qpbRemove->setEnabled(item ? true : false);
632 }
633
on_qtwShortcuts_itemChanged(QTreeWidgetItem * item,int)634 void GlobalShortcutConfig::on_qtwShortcuts_itemChanged(QTreeWidgetItem *item, int) {
635 int idx = qtwShortcuts->indexOfTopLevelItem(item);
636
637 Shortcut &sc = qlShortcuts[idx];
638 sc.iIndex = item->data(0, Qt::DisplayRole).toInt();
639 sc.qvData = item->data(1, Qt::DisplayRole);
640 sc.qlButtons = item->data(2, Qt::DisplayRole).toList();
641 sc.bSuppress = item->checkState(3) == Qt::Checked;
642
643 const ::GlobalShortcut *gs = GlobalShortcutEngine::engine->qmShortcuts.value(sc.iIndex);
644 if (gs && sc.qvData.type() != gs->qvDefault.type()) {
645 item->setData(1, Qt::DisplayRole, gs->qvDefault);
646 }
647 }
648
title() const649 QString GlobalShortcutConfig::title() const {
650 return tr("Shortcuts");
651 }
652
icon() const653 QIcon GlobalShortcutConfig::icon() const {
654 return QIcon(QLatin1String("skin:config_shortcuts.png"));
655 }
656
load(const Settings & r)657 void GlobalShortcutConfig::load(const Settings &r) {
658 qlShortcuts = r.qlShortcuts;
659
660 // The 'Skip' button is supposed to be live, meaning users do not need to click Apply for
661 // their choice of skipping to apply.
662 //
663 // To make this work well, we set the setting on load. This is to make 'Reset' and 'Restore Defaults'
664 // work as expected.
665 g.s.bSuppressMacEventTapWarning = s.bSuppressMacEventTapWarning = r.bSuppressMacEventTapWarning;
666 if (! g.s.bSuppressMacEventTapWarning) {
667 qwWarningContainer->setVisible(showWarning());
668 }
669
670 qcbEnableUIAccess->setChecked(r.bEnableUIAccess);
671 qcbEnableWinHooks->setChecked(r.bEnableWinHooks);
672 qcbEnableGKey->setChecked(r.bEnableGKey);
673 qcbEnableXboxInput->setChecked(r.bEnableXboxInput);
674
675 qcbEnableGlobalShortcuts->setCheckState(r.bShortcutEnable ? Qt::Checked : Qt::Unchecked);
676 on_qcbEnableGlobalShortcuts_stateChanged(qcbEnableGlobalShortcuts->checkState());
677 reload();
678 }
679
save() const680 void GlobalShortcutConfig::save() const {
681 s.qlShortcuts = qlShortcuts;
682 s.bShortcutEnable = qcbEnableGlobalShortcuts->checkState() == Qt::Checked;
683
684 bool oldUIAccess = s.bEnableUIAccess;
685 s.bEnableUIAccess = qcbEnableUIAccess->checkState() == Qt::Checked;
686
687 bool oldWinHooks = s.bEnableWinHooks;
688 s.bEnableWinHooks = qcbEnableWinHooks->checkState() == Qt::Checked;
689
690 bool oldGKey = s.bEnableGKey;
691 s.bEnableGKey = qcbEnableGKey->checkState() == Qt::Checked;
692
693 bool oldXboxInput = s.bEnableXboxInput;
694 s.bEnableXboxInput = qcbEnableXboxInput->checkState() == Qt::Checked;
695
696 if (s.bEnableUIAccess != oldUIAccess || s.bEnableWinHooks != oldWinHooks || s.bEnableGKey != oldGKey || s.bEnableXboxInput != oldXboxInput) {
697 s.requireRestartToApply = true;
698 }
699 }
700
itemForShortcut(const Shortcut & sc) const701 QTreeWidgetItem *GlobalShortcutConfig::itemForShortcut(const Shortcut &sc) const {
702 QTreeWidgetItem *item = new QTreeWidgetItem();
703 ::GlobalShortcut *gs = GlobalShortcutEngine::engine->qmShortcuts.value(sc.iIndex);
704
705 item->setData(0, Qt::DisplayRole, static_cast<unsigned int>(sc.iIndex));
706 if (sc.qvData.isValid() && gs && (sc.qvData.type() == gs->qvDefault.type()))
707 item->setData(1, Qt::DisplayRole, sc.qvData);
708 else if (gs)
709 item->setData(1, Qt::DisplayRole, gs->qvDefault);
710 item->setData(2, Qt::DisplayRole, sc.qlButtons);
711 item->setCheckState(3, sc.bSuppress ? Qt::Checked : Qt::Unchecked);
712 item->setFlags(item->flags() | Qt::ItemIsEditable);
713
714
715 if (gs) {
716 if (! gs->qsToolTip.isEmpty())
717 item->setData(0, Qt::ToolTipRole, gs->qsToolTip);
718 if (! gs->qsWhatsThis.isEmpty())
719 item->setData(0, Qt::WhatsThisRole, gs->qsWhatsThis);
720 }
721
722 item->setData(2, Qt::ToolTipRole, tr("Shortcut button combination."));
723 item->setData(2, Qt::WhatsThisRole, tr("<b>This is the global shortcut key combination.</b><br />"
724 "Click this field and then press the desired key/button combo "
725 "to rebind. Double-click to clear."));
726
727 item->setData(3, Qt::ToolTipRole, tr("Suppress keys from other applications"));
728 item->setData(3, Qt::WhatsThisRole, tr("<b>This hides the button presses from other applications.</b><br />"
729 "Enabling this will hide the button (or the last button of a multi-button combo) "
730 "from other applications. Note that not all buttons can be suppressed."));
731
732 return item;
733 }
734
reload()735 void GlobalShortcutConfig::reload() {
736 qStableSort(qlShortcuts);
737 qtwShortcuts->clear();
738 foreach(const Shortcut &sc, qlShortcuts) {
739 QTreeWidgetItem *item = itemForShortcut(sc);
740 qtwShortcuts->addTopLevelItem(item);
741 }
742 #ifdef Q_OS_MAC
743 if (! g.s.bSuppressMacEventTapWarning) {
744 qwWarningContainer->setVisible(showWarning());
745 } else {
746 qwWarningContainer->setVisible(false);
747 }
748 #endif
749 }
750
accept() const751 void GlobalShortcutConfig::accept() const {
752 GlobalShortcutEngine::engine->bNeedRemap = true;
753 GlobalShortcutEngine::engine->needRemap();
754 GlobalShortcutEngine::engine->setEnabled(g.s.bShortcutEnable);
755 }
756
757
GlobalShortcutEngine(QObject * p)758 GlobalShortcutEngine::GlobalShortcutEngine(QObject *p) : QThread(p) {
759 bNeedRemap = true;
760 needRemap();
761 }
762
~GlobalShortcutEngine()763 GlobalShortcutEngine::~GlobalShortcutEngine() {
764 QSet<ShortcutKey *> qs;
765 foreach(const QList<ShortcutKey*> &ql, qlShortcutList)
766 qs += ql.toSet();
767
768 foreach(ShortcutKey *sk, qs)
769 delete sk;
770 }
771
remap()772 void GlobalShortcutEngine::remap() {
773 bNeedRemap = false;
774
775 QSet<ShortcutKey *> qs;
776 foreach(const QList<ShortcutKey*> &ql, qlShortcutList)
777 qs += ql.toSet();
778
779 foreach(ShortcutKey *sk, qs)
780 delete sk;
781
782 qlButtonList.clear();
783 qlShortcutList.clear();
784 qlDownButtons.clear();
785
786 foreach(const Shortcut &sc, g.s.qlShortcuts) {
787 GlobalShortcut *gs = qmShortcuts.value(sc.iIndex);
788 if (gs && ! sc.qlButtons.isEmpty()) {
789 ShortcutKey *sk = new ShortcutKey;
790 sk->s = sc;
791 sk->iNumUp = sc.qlButtons.count();
792 sk->gs = gs;
793
794 foreach(const QVariant &button, sc.qlButtons) {
795 int idx = qlButtonList.indexOf(button);
796 if (idx == -1) {
797 qlButtonList << button;
798 qlShortcutList << QList<ShortcutKey *>();
799 idx = qlButtonList.count() - 1;
800 }
801 qlShortcutList[idx] << sk;
802 }
803 }
804 }
805 }
806
run()807 void GlobalShortcutEngine::run() {
808 }
809
canSuppress()810 bool GlobalShortcutEngine::canSuppress() {
811 return false;
812 }
813
setEnabled(bool)814 void GlobalShortcutEngine::setEnabled(bool) {
815 }
816
enabled()817 bool GlobalShortcutEngine::enabled() {
818 return true;
819 }
820
canDisable()821 bool GlobalShortcutEngine::canDisable() {
822 return false;
823 }
824
resetMap()825 void GlobalShortcutEngine::resetMap() {
826 tReset.restart();
827 qlActiveButtons.clear();
828 }
829
needRemap()830 void GlobalShortcutEngine::needRemap() {
831 }
832
833 /**
834 * This function gets called internally to update the state
835 * of a button.
836 *
837 * @return True if button is suppressed, otherwise false
838 */
handleButton(const QVariant & button,bool down)839 bool GlobalShortcutEngine::handleButton(const QVariant &button, bool down) {
840 bool already = qlDownButtons.contains(button);
841 if (already == down)
842 return qlSuppressed.contains(button);
843 if (down)
844 qlDownButtons << button;
845 else
846 qlDownButtons.removeAll(button);
847
848 if (tReset.elapsed() > 100000) {
849 if (down) {
850 qlActiveButtons.removeAll(button);
851 qlActiveButtons << button;
852 }
853 emit buttonPressed(! down);
854 }
855
856 if (down) {
857 AudioInputPtr ai = g.ai;
858 if (ai.get()) {
859 // XXX: This is a data race: we write to ai->activityState
860 // (accessed by the AudioInput thread) from the main thread.
861 if (ai->activityState == AudioInput::ActivityStateIdle) {
862 ai->activityState = AudioInput::ActivityStateReturnedFromIdle;
863 }
864 ai->tIdle.restart();
865 }
866 }
867
868 int idx = qlButtonList.indexOf(button);
869 if (idx == -1)
870 return false;
871
872 bool suppress = false;
873
874 foreach(ShortcutKey *sk, qlShortcutList.at(idx)) {
875 if (down) {
876 sk->iNumUp--;
877 if (sk->iNumUp == 0) {
878 GlobalShortcut *gs = sk->gs;
879 if (sk->s.bSuppress) {
880 suppress = true;
881 qlSuppressed << button;
882 }
883 if (! gs->qlActive.contains(sk->s.qvData)) {
884 gs->qlActive << sk->s.qvData;
885 emit gs->triggered(true, sk->s.qvData);
886 emit gs->down(sk->s.qvData);
887 }
888 } else if (sk->iNumUp < 0) {
889 sk->iNumUp = 0;
890 }
891 } else {
892 if (qlSuppressed.contains(button)) {
893 suppress = true;
894 qlSuppressed.removeAll(button);
895 }
896 sk->iNumUp++;
897 if (sk->iNumUp == 1) {
898 GlobalShortcut *gs = sk->gs;
899 if (gs->qlActive.contains(sk->s.qvData)) {
900 gs->qlActive.removeAll(sk->s.qvData);
901 emit gs->triggered(false, sk->s.qvData);
902 }
903 } else if (sk->iNumUp > sk->s.qlButtons.count()) {
904 sk->iNumUp = sk->s.qlButtons.count();
905 }
906 }
907 }
908 return suppress;
909 }
910
add(GlobalShortcut * gs)911 void GlobalShortcutEngine::add(GlobalShortcut *gs) {
912 if (! GlobalShortcutEngine::engine) {
913 GlobalShortcutEngine::engine = GlobalShortcutEngine::platformInit();
914 GlobalShortcutEngine::engine->setEnabled(g.s.bShortcutEnable);
915 }
916
917 GlobalShortcutEngine::engine->qmShortcuts.insert(gs->idx, gs);
918 GlobalShortcutEngine::engine->bNeedRemap = true;
919 GlobalShortcutEngine::engine->needRemap();
920 }
921
remove(GlobalShortcut * gs)922 void GlobalShortcutEngine::remove(GlobalShortcut *gs) {
923 engine->qmShortcuts.remove(gs->idx);
924 engine->bNeedRemap = true;
925 engine->needRemap();
926 if (engine->qmShortcuts.isEmpty()) {
927 delete engine;
928 GlobalShortcutEngine::engine = NULL;
929 }
930 }
931
buttonText(const QList<QVariant> & list)932 QString GlobalShortcutEngine::buttonText(const QList<QVariant> &list) {
933 QStringList keys;
934
935 foreach(QVariant button, list) {
936 QString id = GlobalShortcutEngine::engine->buttonName(button);
937 if (! id.isEmpty())
938 keys << id;
939 }
940 return keys.join(QLatin1String(" + "));
941 }
942
GlobalShortcut(QObject * p,int index,QString qsName,QVariant def)943 GlobalShortcut::GlobalShortcut(QObject *p, int index, QString qsName, QVariant def) : QObject(p) {
944 idx = index;
945 name=qsName;
946 qvDefault = def;
947 GlobalShortcutEngine::add(this);
948 }
949
~GlobalShortcut()950 GlobalShortcut::~GlobalShortcut() {
951 GlobalShortcutEngine::remove(this);
952 }
953