// Copyright 2007-2023 The Mumble Developers. All rights reserved.
// Use of this source code is governed by a BSD-style license
// that can be found in the LICENSE file at the root of the
// Mumble source tree or at <https://www.mumble.info/LICENSE>.

#include "GlobalShortcut.h"

#include "AudioInput.h"
#include "Channel.h"
#include "ClientUser.h"
#include "Database.h"
#include "EnvUtils.h"
#include "MainWindow.h"
#include "ServerHandler.h"
#include "widgets/EventFilters.h"
#include "Global.h"
#include "GlobalShortcutButtons.h"

#include <QtCore/QProcess>
#include <QtCore/QSortFilterProxyModel>
#include <QtGui/QHelpEvent>
#include <QtWidgets/QItemEditorFactory>
#include <QtWidgets/QToolTip>

#ifdef Q_OS_MAC
#	include <ApplicationServices/ApplicationServices.h>
#	include <QtCore/QOperatingSystemVersion>
#endif

#include <cassert>

const QString GlobalShortcutConfig::name = QLatin1String("GlobalShortcutConfig");

/**
 * Used to save the global, unique, platform specific GlobalShortcutEngine.
 */
GlobalShortcutEngine *GlobalShortcutEngine::engine = nullptr;

static ConfigWidget *GlobalShortcutConfigDialogNew(Settings &st) {
	return new GlobalShortcutConfig(st);
}

static ConfigRegistrar registrarGlobalShortcut(1200, GlobalShortcutConfigDialogNew);

static const QString UPARROW = QString::fromUtf8("\xE2\x86\x91 ");

ShortcutActionWidget::ShortcutActionWidget(QWidget *p) : QWidget(p) {
	int idx = 0;

	m_comboBox = new MUComboBox();
	m_comboBox->insertItem(idx, tr("Unassigned"));
	m_comboBox->setItemData(idx, -1);
#ifndef Q_OS_MAC
	m_comboBox->setSizeAdjustPolicy(QComboBox::AdjustToContents);
#endif

	idx++;

	QFontMetrics fontMetrics = m_comboBox->fontMetrics();
	int maxWidth             = m_comboBox->minimumWidth();

	foreach (GlobalShortcut *gs, GlobalShortcutEngine::engine->qmShortcuts) {
		m_comboBox->insertItem(idx, gs->name);
		m_comboBox->setItemData(idx, gs->idx);
		if (!gs->qsToolTip.isEmpty()) {
			m_comboBox->setItemData(idx, gs->qsToolTip, Qt::ToolTipRole);
		}
		if (!gs->qsWhatsThis.isEmpty()) {
			m_comboBox->setItemData(idx, gs->qsWhatsThis, Qt::WhatsThisRole);
		}
		idx++;

		maxWidth = std::max(maxWidth, fontMetrics.horizontalAdvance(gs->name));
	}

	// Sort the ShortcutActionWidget items
	QSortFilterProxyModel *proxy = new QSortFilterProxyModel(this);
	proxy->setSourceModel(m_comboBox->model());

	m_comboBox->model()->setParent(proxy);
	m_comboBox->setModel(proxy);

	m_comboBox->model()->sort(0);

	m_comboBox->setFocusPolicy(Qt::NoFocus);

	setMinimumWidth(maxWidth);
	m_comboBox->view()->setMinimumWidth(maxWidth);
	m_comboBox->adjustSize();

	m_comboBox->setParent(this);

	adjustSize();

	KeyEventObserver *eventFilter = new KeyEventObserver(this, QEvent::KeyPress, true, { Qt::Key_Space });
	connect(eventFilter, &KeyEventObserver::keyEventObserved, this, [this]() { m_comboBox->showPopup(); });
	installEventFilter(eventFilter);

	QTreeWidget *treeWidget = qobject_cast< QTreeWidget * >(p->parentWidget());
	if (treeWidget) {
		treeWidget->resizeColumnToContents(0);
	}
}

void ShortcutActionWidget::setIndex(unsigned int idx) {
	m_comboBox->setCurrentIndex(m_comboBox->findData(idx));
}

unsigned int ShortcutActionWidget::index() const {
	return m_comboBox->itemData(m_comboBox->currentIndex()).toUInt();
}

ShortcutToggleWidget::ShortcutToggleWidget(QWidget *p) : MUComboBox(p) {
	int idx = 0;

	insertItem(idx, tr("Off"));
	setItemData(idx, -1);
	idx++;

	insertItem(idx, tr("Toggle"));
	setItemData(idx, 0);
	idx++;

	insertItem(idx, tr("On"));
	setItemData(idx, 1);
	idx++;
}

void ShortcutToggleWidget::setIndex(int idx) {
	setCurrentIndex(findData(idx));
}

int ShortcutToggleWidget::index() const {
	return itemData(currentIndex()).toInt();
}

ChannelSelectWidget::ChannelSelectWidget(QWidget *parent) : MUComboBox(parent) {
	QReadLocker lock(&Channel::c_qrwlChannels);

	int index = 0;

	for (const Channel *currentChannel : Channel::c_qhChannels) {
		assert(currentChannel);

		insertItem(index, currentChannel->qsName);
		setItemData(index, currentChannel->iId);

		index++;
	}

	model()->sort(0);
}

void ChannelSelectWidget::setCurrentChannel(const ChannelTarget &target) {
	setCurrentIndex(findData(target.channelID));
}

ChannelTarget ChannelSelectWidget::currentChannel() const {
	return itemData(currentIndex()).toUInt();
}

void iterateChannelChildren(QTreeWidgetItem *root, Channel *chan, QMap< int, QTreeWidgetItem * > &map) {
	foreach (Channel *c, chan->qlChannels) {
		QTreeWidgetItem *sub = new QTreeWidgetItem(root, QStringList(c->qsName));
		sub->setData(0, Qt::UserRole, c->iId);
		map.insert(static_cast< int >(c->iId), sub);
		iterateChannelChildren(sub, c, map);
	}
}

ShortcutTargetDialog::ShortcutTargetDialog(const ShortcutTarget &st, QWidget *pw) : QDialog(pw) {
	stTarget = st;
	setupUi(this);


	// Load current shortcut configuration
	qcbForceCenter->setChecked(st.bForceCenter);
	qgbModifiers->setVisible(true);

	if (st.bCurrentSelection) {
		qcbTarget->setCurrentIndex(Target::SELECTION);
		qswStack->setCurrentWidget(qwSelectionPage);
	} else if (st.bUsers) {
		qcbTarget->setCurrentIndex(Target::USERLIST);
		qswStack->setCurrentWidget(qwUserPage);
	} else {
		qcbTarget->setCurrentIndex(Target::CHANNEL);
		qswStack->setCurrentWidget(qwChannelPage);
	}

	qcbLinks->setChecked(st.bLinks);
	qcbShoutLinks->setChecked(st.bLinks);
	qcbChildren->setChecked(st.bChildren);
	qcbShoutSubchans->setChecked(st.bChildren);

	// Insert all known friends into the possible targets list
	const QMap< QString, QString > &friends = Global::get().db->getFriends();
	if (!friends.isEmpty()) {
		QMap< QString, QString >::const_iterator i;
		for (i = friends.constBegin(); i != friends.constEnd(); ++i) {
			qcbUser->addItem(i.key(), i.value());
			qmHashNames.insert(i.value(), i.key());
		}
		qcbUser->insertSeparator(qcbUser->count());
	}

	// If we are connected to a server also add all connected players with certificates to the list
	if (Global::get().uiSession) {
		QMap< QString, QString > others;
		QMap< QString, QString >::const_iterator i;

		QReadLocker lock(&ClientUser::c_qrwlUsers);
		foreach (ClientUser *p, ClientUser::c_qmUsers) {
			if ((p->uiSession != Global::get().uiSession) && p->qsFriendName.isEmpty() && !p->qsHash.isEmpty()) {
				others.insert(p->qsName, p->qsHash);
				qmHashNames.insert(p->qsHash, p->qsName);
			}
		}

		for (i = others.constBegin(); i != others.constEnd(); ++i) {
			qcbUser->addItem(i.key(), i.value());
		}
	}

	QMap< QString, QString > users;

	foreach (const QString &hash, st.qlUsers) {
		if (qmHashNames.contains(hash))
			users.insert(qmHashNames.value(hash), hash);
		else
			users.insert(QString::fromLatin1("#%1").arg(hash), hash);
	}

	{
		QMap< QString, QString >::const_iterator i;
		for (i = users.constBegin(); i != users.constEnd(); ++i) {
			QListWidgetItem *itm = new QListWidgetItem(i.key());
			itm->setData(Qt::UserRole, i.value());
			qlwUsers->addItem(itm);
		}
	}

	// Now generate the tree of possible channel targets, first add the default ones
	QMap< int, QTreeWidgetItem * > qmTree;

	QTreeWidgetItem *root = new QTreeWidgetItem(qtwChannels, QStringList(tr("Root")));
	root->setData(0, Qt::UserRole, SHORTCUT_TARGET_ROOT);
	root->setExpanded(true);
	qmTree.insert(-1, root);

	QTreeWidgetItem *parent_item = new QTreeWidgetItem(root, QStringList(tr("Parent")));
	parent_item->setData(0, Qt::UserRole, SHORTCUT_TARGET_PARENT);
	parent_item->setExpanded(true);
	qmTree.insert(-2, parent_item);

	QTreeWidgetItem *current = new QTreeWidgetItem(parent_item, QStringList(tr("Current")));
	current->setData(0, Qt::UserRole, SHORTCUT_TARGET_CURRENT);
	qmTree.insert(-3, current);

	for (int i = 0; i < 8; ++i) {
		QTreeWidgetItem *sub = new QTreeWidgetItem(current, QStringList(tr("Subchannel #%1").arg(i + 1)));
		sub->setData(0, Qt::UserRole, SHORTCUT_TARGET_SUBCHANNEL - i);
		qmTree.insert(SHORTCUT_TARGET_SUBCHANNEL - i, sub);
	}

	for (int i = 0; i < 8; ++i) {
		QTreeWidgetItem *psub =
			new QTreeWidgetItem(parent_item, QStringList(UPARROW + tr("Subchannel #%1").arg(i + 1)));
		psub->setData(0, Qt::UserRole, SHORTCUT_TARGET_PARENT_SUBCHANNEL - i);
		qmTree.insert(SHORTCUT_TARGET_PARENT_SUBCHANNEL - i, psub);
	}

	// And if we are connected add the channels on the current server
	if (Global::get().uiSession) {
		Channel *c             = Channel::get(Channel::ROOT_ID);
		QTreeWidgetItem *sroot = new QTreeWidgetItem(qtwChannels, QStringList(c->qsName));
		qmTree.insert(0, sroot);
		iterateChannelChildren(sroot, c, qmTree);
	}

	qtwChannels->sortByColumn(0, Qt::AscendingOrder);

	QTreeWidgetItem *qtwi;
	if (Global::get().uiSession) {
		qtwi = qmTree.value(static_cast< int >(ClientUser::get(Global::get().uiSession)->cChannel->iId));
		if (qtwi)
			qtwChannels->scrollToItem(qtwi);
	}

	qtwi = qmTree.value(st.iChannel);
	if (qtwi) {
		qtwChannels->scrollToItem(qtwi);
		qtwChannels->setCurrentItem(qtwi);
	}

	qleGroup->setText(stTarget.qsGroup);
}

ShortcutTarget ShortcutTargetDialog::target() const {
	return stTarget;
}

void ShortcutTargetDialog::accept() {
	if (qswStack->currentWidget() == qwSelectionPage) {
		stTarget.bLinks    = qcbShoutLinks->isChecked();
		stTarget.bChildren = qcbShoutSubchans->isChecked();
	} else {
		stTarget.bLinks    = qcbLinks->isChecked();
		stTarget.bChildren = qcbChildren->isChecked();
	}

	stTarget.bForceCenter = qcbForceCenter->isChecked();

	stTarget.qlUsers.clear();
	stTarget.qsGroup.clear();
	stTarget.iChannel = -3; // Reset it to the value it has been assigned in the constructor of ShotcutTarget

	if (!stTarget.bCurrentSelection) {
		QList< QListWidgetItem * > ql = qlwUsers->findItems(QString(), Qt::MatchStartsWith);
		foreach (QListWidgetItem *itm, ql) { stTarget.qlUsers << itm->data(Qt::UserRole).toString(); }

		QTreeWidgetItem *qtwi = qtwChannels->currentItem();
		if (qtwi) {
			stTarget.iChannel = qtwi->data(0, Qt::UserRole).toInt();
			stTarget.qsGroup  = qleGroup->text().trimmed();
		}
	}

	QDialog::accept();
}

void ShortcutTargetDialog::on_qcbTarget_currentIndexChanged(int index) {
	switch (index) {
		default:
			qWarning("GlobalShortcutDialog: Encountered impossible target index! => bug");
			// Fallthrough
		case Target::SELECTION:
			stTarget.bUsers            = false;
			stTarget.bCurrentSelection = true;

			qswStack->setCurrentWidget(qwSelectionPage);
			break;
		case Target::USERLIST:
			stTarget.bUsers            = true;
			stTarget.bCurrentSelection = false;

			qswStack->setCurrentWidget(qwUserPage);
			break;
		case Target::CHANNEL:
			stTarget.bUsers            = false;
			stTarget.bCurrentSelection = false;

			qswStack->setCurrentWidget(qwChannelPage);
			break;
	}
}

void ShortcutTargetDialog::on_qpbAdd_clicked() {
	if (qcbUser->currentIndex() < 0)
		return;

	QListWidgetItem *itm = new QListWidgetItem(qcbUser->currentText());
	itm->setData(Qt::UserRole, qcbUser->itemData(qcbUser->currentIndex()));
	qlwUsers->addItem(itm);
}

void ShortcutTargetDialog::on_qpbRemove_clicked() {
	QListWidgetItem *itm = qlwUsers->currentItem();
	delete itm;
}

ShortcutTargetWidget::ShortcutTargetWidget(QWidget *p) : QFrame(p) {
	qleTarget = new QLineEdit();
	qleTarget->setReadOnly(true);
	qleTarget->setFocusPolicy(Qt::NoFocus);

	qtbEdit = new QToolButton();
	qtbEdit->setText(tr("..."));
	qtbEdit->setFocusPolicy(Qt::ClickFocus);
	qtbEdit->setObjectName(QLatin1String("qtbEdit"));

	QHBoxLayout *l = new QHBoxLayout(this);
	l->setContentsMargins(0, 0, 0, 0);
	l->addWidget(qleTarget, 1);
	l->addWidget(qtbEdit);

	KeyEventObserver *eventFilter = new KeyEventObserver(this, QEvent::KeyPress, true, { Qt::Key_Space });
	connect(eventFilter, &KeyEventObserver::keyEventObserved, this, [this]() { qtbEdit->click(); });
	installEventFilter(eventFilter);

	QMetaObject::connectSlotsByName(this);
}

TextEditWidget::TextEditWidget(QWidget *p) : QWidget(p) {
	m_lineEdit = new QLineEdit();
	m_lineEdit->setFocusPolicy(Qt::ClickFocus);

	QHBoxLayout *l = new QHBoxLayout(this);
	l->setContentsMargins(0, 0, 0, 0);
	l->setSpacing(0);
	l->addWidget(m_lineEdit);

	KeyEventObserver *eventFilter = new KeyEventObserver(this, QEvent::KeyPress, true, { Qt::Key_Space });
	connect(eventFilter, &KeyEventObserver::keyEventObserved, this,
			[this]() { m_lineEdit->setFocus(Qt::MouseFocusReason); });
	installEventFilter(eventFilter);

	QMetaObject::connectSlotsByName(this);
}

QString TextEditWidget::currentString() const {
	return m_lineEdit->text();
}

void TextEditWidget::setCurrentString(const QString &str) {
	m_lineEdit->setText(str);
}

/**
 * This function returns a textual representation of the given shortcut target st.
 */
QString ShortcutTargetWidget::targetString(const ShortcutTarget &st) {
	if (st.bCurrentSelection) {
		return tr("Current selection");
	} else if (st.bUsers) {
		if (!st.qlUsers.isEmpty()) {
			QMap< QString, QString > hashes;

			QReadLocker lock(&ClientUser::c_qrwlUsers);
			foreach (ClientUser *p, ClientUser::c_qmUsers) {
				if (!p->qsHash.isEmpty()) {
					hashes.insert(p->qsHash, p->qsName);
				}
			}

			QStringList users;
			foreach (const QString &hash, st.qlUsers) {
				QString name;
				if (hashes.contains(hash)) {
					name = hashes.value(hash);
				} else {
					name = Global::get().db->getFriend(hash);
					if (name.isEmpty())
						name = QString::fromLatin1("#%1").arg(hash);
				}
				users << name;
			}

			users.sort();
			return users.join(tr(", "));
		}
	} else {
		if (st.iChannel < 0) {
			switch (st.iChannel) {
				case SHORTCUT_TARGET_ROOT:
					return tr("Root");
				case SHORTCUT_TARGET_PARENT:
					return tr("Parent");
				case SHORTCUT_TARGET_CURRENT:
					return tr("Current");
				default:
					if (st.iChannel <= SHORTCUT_TARGET_PARENT_SUBCHANNEL)
						return (UPARROW
								+ tr("Subchannel #%1").arg(SHORTCUT_TARGET_PARENT_SUBCHANNEL + 1 - st.iChannel));
					else
						return tr("Subchannel #%1").arg(SHORTCUT_TARGET_CURRENT - st.iChannel);
			}
		} else {
			Channel *c = Channel::get(static_cast< unsigned int >(st.iChannel));
			if (c)
				return c->qsName;
			else
				return tr("Invalid");
		}
	}
	return tr("Empty");
}

ShortcutTarget ShortcutTargetWidget::target() const {
	return stTarget;
}

void ShortcutTargetWidget::setTarget(const ShortcutTarget &st) {
	stTarget = st;
	qleTarget->setText(ShortcutTargetWidget::targetString(st));
}

void ShortcutTargetWidget::on_qtbEdit_clicked() {
	ShortcutTargetDialog *std = new ShortcutTargetDialog(stTarget, this);
	if (std->exec() == QDialog::Accepted) {
		stTarget = std->target();
		qleTarget->setText(ShortcutTargetWidget::targetString(stTarget));

		// Qt bug? Who knows, but since there won't be focusOut events for this widget anymore,
		// we need to force the commit.
		QWidget *p = parentWidget();
		while (p) {
			if (QAbstractItemView *qaiv = qobject_cast< QAbstractItemView * >(p)) {
				QStyledItemDelegate *qsid = qobject_cast< QStyledItemDelegate * >(qaiv->itemDelegate());
				if (qsid) {
					QMetaObject::invokeMethod(qsid, "_q_commitDataAndCloseEditor", Qt::QueuedConnection,
											  Q_ARG(QWidget *, this));
				}
				break;
			}
			p = p->parentWidget();
		}
	}
	delete std;
}

ShortcutDelegate::ShortcutDelegate(QObject *p) : QStyledItemDelegate(p) {
	QItemEditorFactory *factory = new QItemEditorFactory;

	factory->registerEditor(QVariant::List, new QStandardItemEditorCreator< GlobalShortcutButtons >());
	factory->registerEditor(QVariant::UInt, new QStandardItemEditorCreator< ShortcutActionWidget >());
	factory->registerEditor(QVariant::Int, new QStandardItemEditorCreator< ShortcutToggleWidget >());
	factory->registerEditor(static_cast< int >(QVariant::fromValue(ShortcutTarget()).userType()),
							new QStandardItemEditorCreator< ShortcutTargetWidget >());
	factory->registerEditor(static_cast< int >(QVariant::fromValue(ChannelTarget()).userType()),
							new QStandardItemEditorCreator< ChannelSelectWidget >());
	factory->registerEditor(QVariant::String, new QStandardItemEditorCreator< TextEditWidget >());
	factory->registerEditor(QVariant::Invalid, new QStandardItemEditorCreator< QWidget >());
	setItemEditorFactory(factory);
}

ShortcutDelegate::~ShortcutDelegate() {
	delete itemEditorFactory();
	setItemEditorFactory(nullptr);
}

/**
 * Provides textual representations for the mappings done for the edit behaviour.
 */
QString ShortcutDelegate::displayText(const QVariant &item, const QLocale &loc) const {
	if (item.userType() == QVariant::fromValue(ShortcutTarget()).userType()) {
		return ShortcutTargetWidget::targetString(item.value< ShortcutTarget >());
	} else if (item.userType() == QVariant::fromValue(ChannelTarget()).userType()) {
		ChannelTarget target = item.value< ChannelTarget >();

		const Channel *c = Channel::get(target.channelID);
		if (c) {
			return c->qsName;
		} else {
			return tr("< Unknown Channel >");
		}
	}

	switch (item.type()) {
		case QVariant::Int: {
			const auto v = item.toInt();
			if (v > 0) {
				return tr("On");
			} else if (v < 0) {
				return tr("Off");
			}

			return tr("Toggle");
		}
		case QVariant::UInt: {
			const auto shortcut = GlobalShortcutEngine::engine->qmShortcuts.value(item.toInt());
			return shortcut ? shortcut->name : tr("Unassigned");
		}
		case QVariant::List: {
			const auto buttons = item.value< QList< QVariant > >();
			if (buttons.count() > 0) {
				QString text;
				for (const auto &button : buttons) {
					if (!text.isEmpty()) {
						text += ' ';
					}

					const auto info = GlobalShortcutEngine::engine->buttonInfo(button);
					text += QString("'%1%2'").arg(info.devicePrefix, info.name);
				}

				return text;
			} else {
				return tr("No buttons assigned");
			}
		}
		default:
			qWarning("ShortcutDelegate::displayText(): Unknown type %d", item.type());
	}

	return QStyledItemDelegate::displayText(item, loc);
}

bool ShortcutDelegate::helpEvent(QHelpEvent *event, QAbstractItemView *, const QStyleOptionViewItem &,
								 const QModelIndex &index) {
	if (event->type() != QEvent::ToolTip || !index.isValid()) {
		return false;
	}

	QToolTip::showText(event->globalPos(), tr("Press to show button combination"));
	return true;
}

GlobalShortcutConfig::GlobalShortcutConfig(Settings &st) : ConfigWidget(st) {
	setupUi(this);
	installEventFilter(this);

	bool canSuppress = GlobalShortcutEngine::engine->canSuppress();
	bool canDisable  = GlobalShortcutEngine::engine->canDisable();

	qwWarningContainer->setVisible(false);

#ifdef Q_OS_WIN
	qgbWindowsShortcutEngines->setVisible(true);
#else
	qgbWindowsShortcutEngines->setVisible(false);
#endif

	qtwShortcuts->setColumnCount(canSuppress ? 4 : 3);
	qtwShortcuts->setItemDelegate(new ShortcutDelegate(qtwShortcuts));

	qtwShortcuts->headerItem()->setData(0, Qt::AccessibleTextRole, tr("Shortcut action"));
	qtwShortcuts->headerItem()->setData(1, Qt::AccessibleTextRole, tr("Shortcut data"));
	qtwShortcuts->headerItem()->setData(2, Qt::AccessibleTextRole, tr("Shortcut input combinations"));

	qtwShortcuts->header()->setSectionResizeMode(0, QHeaderView::Fixed);
	qtwShortcuts->header()->resizeSection(0, 150);
	qtwShortcuts->header()->setSectionResizeMode(2, QHeaderView::Stretch);
	if (canSuppress) {
		qtwShortcuts->header()->setSectionResizeMode(3, QHeaderView::ResizeToContents);
	}

	qcbEnableGlobalShortcuts->setVisible(canDisable);

	qlWaylandNote->setVisible(false);
#ifdef Q_OS_LINUX
	if (EnvUtils::waylandIsUsed()) {
		// Our global shortcut system doesn't work properly with Wayland
		qlWaylandNote->setVisible(true);
	}
#endif

#ifdef Q_OS_MAC
	// Help Mac users enable accessibility access for Mumble...
#	if QT_VERSION >= QT_VERSION_CHECK(5, 9, 0)
	const QOperatingSystemVersion current = QOperatingSystemVersion::current();
	if (current >= QOperatingSystemVersion::OSXMavericks) {
#	else
	if (QSysInfo::MacintoshVersion >= QSysInfo::MV_MAVERICKS) {
#	endif
		qpbOpenAccessibilityPrefs->setHidden(true);
		label->setText(tr("<html><head/><body>"
						  "<p>"
						  "Mumble can currently only use mouse buttons and keyboard modifier keys (Alt, Ctrl, Cmd, "
						  "etc.) for global shortcuts."
						  "</p>"
						  "<p>"
						  "If you want more flexibility, you can add Mumble as a trusted accessibility program in the "
						  "Security & Privacy section "
						  "of your Mac's System Preferences."
						  "</p>"
						  "<p>"
						  "In the Security & Privacy preference pane, change to the Privacy tab. Then choose "
						  "Accessibility (near the bottom) in "
						  "the list to the left. Finally, add Mumble to the list of trusted accessibility programs."
						  "</body></html>"));
	}
#endif
}

bool GlobalShortcutConfig::eventFilter(QObject * /*object*/, QEvent *e) {
#ifdef Q_OS_MAC
	if (e->type() == QEvent::WindowActivate) {
		if (!Global::get().s.bSuppressMacEventTapWarning) {
			qwWarningContainer->setVisible(showWarning());
		}
	}
#else
	Q_UNUSED(e)
#endif
	return false;
}

bool GlobalShortcutConfig::showWarning() const {
#ifdef Q_OS_MAC
#	if MAC_OS_X_VERSION_MAX_ALLOWED >= 1090
#		if QT_VERSION >= QT_VERSION_CHECK(5, 9, 0)
	const QOperatingSystemVersion current = QOperatingSystemVersion::current();
	if (current >= QOperatingSystemVersion::OSXMavericks) {
#		else
	if (QSysInfo::MacintoshVersion >= QSysInfo::MV_MAVERICKS) {
#		endif
		return !AXIsProcessTrustedWithOptions(nullptr);
	} else
#	endif
	{
		return !QFile::exists(QLatin1String("/private/var/db/.AccessibilityAPIEnabled"));
	}
#endif
	return false;
}

void GlobalShortcutConfig::on_qpbOpenAccessibilityPrefs_clicked() {
	QStringList args;
	args << QLatin1String("/Applications/System Preferences.app");
	args << QLatin1String("/System/Library/PreferencePanes/UniversalAccessPref.prefPane");
	(void) QProcess::startDetached(QLatin1String("/usr/bin/open"), args);
}

void GlobalShortcutConfig::on_qpbSkipWarning_clicked() {
	// Store to both global and local settings.  The 'Skip' is live, as in
	// we don't expect the user to click Apply for their choice to work.
	Global::get().s.bSuppressMacEventTapWarning = s.bSuppressMacEventTapWarning = true;
	qwWarningContainer->setVisible(false);
}

void GlobalShortcutConfig::commit() {
	qtwShortcuts->closePersistentEditor(qtwShortcuts->currentItem(), qtwShortcuts->currentColumn());
}

void GlobalShortcutConfig::on_qcbEnableGlobalShortcuts_stateChanged(int state) {
	bool b = state == Qt::Checked;
	qpbAdd->setEnabled(b);
	if (!b)
		qpbRemove->setEnabled(false);
	else
		qpbRemove->setEnabled(qtwShortcuts->currentItem() ? true : false);
	qtwShortcuts->setEnabled(b);

	// We have to enable this here. Otherwise, adding new shortcuts wouldn't work.
	GlobalShortcutEngine::engine->setEnabled(b);
}

void GlobalShortcutConfig::on_qpbAdd_clicked(bool) {
	commit();
	Shortcut sc;
	sc.iIndex    = -1;
	sc.bSuppress = false;
	qlShortcuts << sc;
	reload();
	qtwShortcuts->setFocus(Qt::TabFocusReason);
}

void GlobalShortcutConfig::on_qpbRemove_clicked(bool) {
	commit();
	QTreeWidgetItem *qtwi = qtwShortcuts->currentItem();
	if (!qtwi)
		return;
	int idx = qtwShortcuts->indexOfTopLevelItem(qtwi);
	delete qtwi;
	qlShortcuts.removeAt(idx);

	// Clear the selected item. If we don't do this, the next shortcut in the list will be
	// automatically selected. This allows for the user to accidentally hit remove a second
	// time (as the mouse is over the remove button at this point already) leading to
	// another shortcut being deleted accidentally.
	qtwShortcuts->setCurrentItem(nullptr);
}

void GlobalShortcutConfig::on_qtwShortcuts_currentItemChanged(QTreeWidgetItem *item, QTreeWidgetItem *) {
	qpbRemove->setEnabled(item ? true : false);
}

void GlobalShortcutConfig::on_qtwShortcuts_itemChanged(QTreeWidgetItem *item, int) {
	int idx = qtwShortcuts->indexOfTopLevelItem(item);

	Shortcut &sc = qlShortcuts[idx];
	sc.iIndex    = item->data(0, Qt::DisplayRole).toInt();
	sc.qvData    = item->data(1, Qt::DisplayRole);
	sc.qlButtons = item->data(2, Qt::DisplayRole).toList();
	sc.bSuppress = item->checkState(3) == Qt::Checked;

	const ::GlobalShortcut *gs = GlobalShortcutEngine::engine->qmShortcuts.value(sc.iIndex);
	if (gs && sc.qvData.userType() != gs->qvDefault.userType()) {
		item->setData(1, Qt::DisplayRole, gs->qvDefault);
	}

	if (gs) {
		item->setData(0, Qt::AccessibleTextRole, gs->name);
	} else {
		item->setData(0, Qt::AccessibleTextRole, tr("Unassigned"));
	}
	item->setData(1, Qt::AccessibleTextRole, sc.qvData);
	item->setData(3, Qt::AccessibleDescriptionRole,
				  item->checkState(3) == Qt::Checked ? tr("checked") : tr("unchecked"));

	qtwShortcuts->resizeColumnToContents(0);
}

QString GlobalShortcutConfig::title() const {
	return tr("Shortcuts");
}

const QString &GlobalShortcutConfig::getName() const {
	return GlobalShortcutConfig::name;
}

QIcon GlobalShortcutConfig::icon() const {
	return QIcon(QLatin1String("skin:config_shortcuts.png"));
}

void GlobalShortcutConfig::load(const Settings &r) {
	qlShortcuts = r.qlShortcuts;

	// The 'Skip' button is supposed to be live, meaning users do not need to click Apply for
	// their choice of skipping to apply.
	//
	// To make this work well, we set the setting on load. This is to make 'Reset' and 'Restore Defaults'
	// work as expected.
	Global::get().s.bSuppressMacEventTapWarning = s.bSuppressMacEventTapWarning = r.bSuppressMacEventTapWarning;
	if (!Global::get().s.bSuppressMacEventTapWarning) {
		qwWarningContainer->setVisible(showWarning());
	}

	qcbEnableUIAccess->setChecked(r.bEnableUIAccess);
	qcbEnableGKey->setChecked(r.bEnableGKey);
	qcbEnableXboxInput->setChecked(r.bEnableXboxInput);

	qcbEnableGlobalShortcuts->setCheckState(r.bShortcutEnable ? Qt::Checked : Qt::Unchecked);
	on_qcbEnableGlobalShortcuts_stateChanged(qcbEnableGlobalShortcuts->checkState());
	reload();
}

void GlobalShortcutConfig::save() const {
	s.qlShortcuts     = qlShortcuts;
	s.bShortcutEnable = qcbEnableGlobalShortcuts->checkState() == Qt::Checked;

	bool oldUIAccess  = s.bEnableUIAccess;
	s.bEnableUIAccess = qcbEnableUIAccess->checkState() == Qt::Checked;

	bool oldGKey  = s.bEnableGKey;
	s.bEnableGKey = qcbEnableGKey->checkState() == Qt::Checked;

	bool oldXboxInput  = s.bEnableXboxInput;
	s.bEnableXboxInput = qcbEnableXboxInput->checkState() == Qt::Checked;

	if (s.bEnableUIAccess != oldUIAccess || s.bEnableGKey != oldGKey || s.bEnableXboxInput != oldXboxInput) {
		s.requireRestartToApply = true;
	}

	if (Global::get().sh && Global::get().sh->hasSynchronized()) {
		// If we are connected to a server (that has finished synchronizing) there is the change, the user has
		// configured server-specific shortcuts, which we save in the DB instead of in the regular settings. Thus we
		// have to explicitly save them here.
		// Note that the server needs to have finished synchronizing, since only then did we load any previously
		// configured shortcuts from the DB in the first place. Otherwise, "saving" the shortcuts (which are not
		// loaded yet) would effectively delete them.
		Global::get().db->setShortcuts(Global::get().sh->qbaDigest, s.qlShortcuts);
	}
}

QTreeWidgetItem *GlobalShortcutConfig::itemForShortcut(const Shortcut &sc) const {
	QTreeWidgetItem *item = new QTreeWidgetItem();
	::GlobalShortcut *gs  = GlobalShortcutEngine::engine->qmShortcuts.value(sc.iIndex);

	item->setData(0, Qt::DisplayRole, static_cast< unsigned int >(sc.iIndex));
	if (sc.qvData.isValid() && gs && (sc.qvData.type() == gs->qvDefault.type()))
		item->setData(1, Qt::DisplayRole, sc.qvData);
	else if (gs)
		item->setData(1, Qt::DisplayRole, gs->qvDefault);
	item->setData(2, Qt::DisplayRole, sc.qlButtons);
	item->setCheckState(3, sc.bSuppress ? Qt::Checked : Qt::Unchecked);
	item->setFlags(item->flags() | Qt::ItemIsEditable);


	if (gs) {
		if (!gs->qsToolTip.isEmpty())
			item->setData(0, Qt::ToolTipRole, gs->qsToolTip);
		if (!gs->qsWhatsThis.isEmpty())
			item->setData(0, Qt::WhatsThisRole, gs->qsWhatsThis);
	}

	item->setData(2, Qt::WhatsThisRole,
				  tr("<b>This is the global shortcut key combination.</b><br />"
					 "Click this field and then press the desired key/button combo "
					 "to rebind. Double-click to clear."));

	item->setData(3, Qt::ToolTipRole, tr("Suppress keys from other applications"));
	item->setData(3, Qt::WhatsThisRole,
				  tr("<b>This hides the button presses from other applications.</b><br />"
					 "Enabling this will hide the button (or the last button of a multi-button combo) "
					 "from other applications. Note that not all buttons can be suppressed."));

	return item;
}

void GlobalShortcutConfig::reload() {
	std::stable_sort(qlShortcuts.begin(), qlShortcuts.end());
	qtwShortcuts->clear();

	foreach (const Shortcut &sc, qlShortcuts) {
		QTreeWidgetItem *item = itemForShortcut(sc);
		qtwShortcuts->addTopLevelItem(item);
		on_qtwShortcuts_itemChanged(item, 0);
	}
#ifdef Q_OS_MAC
	if (!Global::get().s.bSuppressMacEventTapWarning) {
		qwWarningContainer->setVisible(showWarning());
	} else {
		qwWarningContainer->setVisible(false);
	}
#endif
}

void GlobalShortcutConfig::accept() const {
	GlobalShortcutEngine::engine->bNeedRemap = true;
	GlobalShortcutEngine::engine->needRemap();
	GlobalShortcutEngine::engine->setEnabled(Global::get().s.bShortcutEnable);
}


GlobalShortcutEngine::GlobalShortcutEngine(QObject *p) : QThread(p) {
	bNeedRemap = true;
	needRemap();
}

GlobalShortcutEngine::~GlobalShortcutEngine() {
	QSet< ShortcutKey * > qs;
	foreach (const QList< ShortcutKey * > &ql, qlShortcutList) {
#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
		qs += QSet< ShortcutKey * >(ql.begin(), ql.end());
#else
		// In Qt 5.14 QList::toSet() has been deprecated as there exists a dedicated constructor of QSet for this now
		qs += ql.toSet();
#endif
	}

	foreach (ShortcutKey *sk, qs)
		delete sk;
}

void GlobalShortcutEngine::remap() {
	bNeedRemap = false;

	QSet< ShortcutKey * > qs;
	foreach (const QList< ShortcutKey * > &ql, qlShortcutList) {
#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
		qs += QSet< ShortcutKey * >(ql.begin(), ql.end());
#else
		// In Qt 5.14 QList::toSet() has been deprecated as there exists a dedicated constructor of QSet for this now
		qs += ql.toSet();
#endif
	}

	foreach (ShortcutKey *sk, qs)
		delete sk;

	qlButtonList.clear();
	qlShortcutList.clear();
	qlDownButtons.clear();

	foreach (const Shortcut &sc, Global::get().s.qlShortcuts) {
		GlobalShortcut *gs = qmShortcuts.value(sc.iIndex);
		if (gs && !sc.qlButtons.isEmpty()) {
			ShortcutKey *sk = new ShortcutKey;
			sk->s           = sc;
			sk->iNumUp      = sc.qlButtons.count();
			sk->gs          = gs;

			foreach (const QVariant &button, sc.qlButtons) {
				int idx = qlButtonList.indexOf(button);
				if (idx == -1) {
					qlButtonList << button;
					qlShortcutList << QList< ShortcutKey * >();
					idx = qlButtonList.count() - 1;
				}
				qlShortcutList[idx] << sk;
			}
		}
	}
}

void GlobalShortcutEngine::run() {
}

bool GlobalShortcutEngine::canSuppress() {
	return false;
}

void GlobalShortcutEngine::setEnabled(bool) {
}

bool GlobalShortcutEngine::enabled() {
	return true;
}

bool GlobalShortcutEngine::canDisable() {
	return false;
}

void GlobalShortcutEngine::resetMap() {
	tReset.restart();
	qlActiveButtons.clear();
}

void GlobalShortcutEngine::needRemap() {
}

/**
 * This function gets called internally to update the state
 * of a button.
 *
 * @return True if button is suppressed, otherwise false
 */
bool GlobalShortcutEngine::handleButton(const QVariant &button, bool down) {
	bool already = qlDownButtons.contains(button);
	if (already == down)
		return qlSuppressed.contains(button);
	if (down)
		qlDownButtons << button;
	else
		qlDownButtons.removeAll(button);

	if (tReset.elapsed() > 100000) {
		if (down) {
			qlActiveButtons.removeAll(button);
			qlActiveButtons << button;
		}
		emit buttonPressed(!down);
	}

	if (down) {
		AudioInputPtr ai = Global::get().ai;
		if (ai.get()) {
			// XXX: This is a data race: we write to ai->activityState
			// (accessed by the AudioInput thread) from the main thread.
			if (ai->activityState == AudioInput::ActivityStateIdle) {
				ai->activityState = AudioInput::ActivityStateReturnedFromIdle;
			}
			ai->tIdle.restart();
		}
	}

	int idx = qlButtonList.indexOf(button);
	if (idx == -1)
		return false;

	bool suppress = false;

	foreach (ShortcutKey *sk, qlShortcutList.at(idx)) {
		if (down) {
			sk->iNumUp--;
			if (sk->iNumUp == 0) {
				GlobalShortcut *gs = sk->gs;
				if (sk->s.bSuppress) {
					suppress = true;
					qlSuppressed << button;
				}
				if (!gs->qlActive.contains(sk->s.qvData)) {
					gs->qlActive << sk->s.qvData;
					emit gs->triggered(true, sk->s.qvData);
					emit gs->down(sk->s.qvData);
				}
			} else if (sk->iNumUp < 0) {
				sk->iNumUp = 0;
			}
		} else {
			if (qlSuppressed.contains(button)) {
				suppress = true;
				qlSuppressed.removeAll(button);
			}
			sk->iNumUp++;
			if (sk->iNumUp == 1) {
				GlobalShortcut *gs = sk->gs;
				if (gs->qlActive.contains(sk->s.qvData)) {
					gs->qlActive.removeAll(sk->s.qvData);
					emit gs->triggered(false, sk->s.qvData);
				}
			} else if (sk->iNumUp > sk->s.qlButtons.count()) {
				sk->iNumUp = sk->s.qlButtons.count();
			}
		}
	}
	return suppress;
}

void GlobalShortcutEngine::add(GlobalShortcut *gs) {
	if (!GlobalShortcutEngine::engine) {
		GlobalShortcutEngine::engine = GlobalShortcutEngine::platformInit();
		GlobalShortcutEngine::engine->setEnabled(Global::get().s.bShortcutEnable);
	}

	GlobalShortcutEngine::engine->qmShortcuts.insert(gs->idx, gs);
	GlobalShortcutEngine::engine->bNeedRemap = true;
	GlobalShortcutEngine::engine->needRemap();
}

void GlobalShortcutEngine::remove(GlobalShortcut *gs) {
	engine->qmShortcuts.remove(gs->idx);
	engine->bNeedRemap = true;
	engine->needRemap();
	if (engine->qmShortcuts.isEmpty()) {
		delete engine;
		GlobalShortcutEngine::engine = nullptr;
	}
}

GlobalShortcut::GlobalShortcut(QObject *p, int index, QString qsName, QVariant def) : QObject(p) {
	idx       = index;
	name      = qsName;
	qvDefault = def;
	GlobalShortcutEngine::add(this);
}

GlobalShortcut::~GlobalShortcut() {
	GlobalShortcutEngine::remove(this);
}
