1103 lines
33 KiB
C++
1103 lines
33 KiB
C++
// 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, [=]() { 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, [=]() { 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,
|
|
[=]() { 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);
|
|
}
|