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