1
/****************************************************************************
2
* Copyright (c) 2022 Zheng Lei (realthunder) <realthunder.dev@gmail.com> *
4
* This file is part of the FreeCAD CAx development system. *
6
* This library is free software; you can redistribute it and/or *
7
* modify it under the terms of the GNU Library General Public *
8
* License as published by the Free Software Foundation; either *
9
* version 2 of the License, or (at your option) any later version. *
11
* This library is distributed in the hope that it will be useful, *
12
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
13
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
14
* GNU Library General Public License for more details. *
16
* You should have received a copy of the GNU Library General Public *
17
* License along with this library; see the file COPYING.LIB. If not, *
18
* write to the Free Software Foundation, Inc., 59 Temple Place, *
19
* Suite 330, Boston, MA 02111-1307, USA *
21
****************************************************************************/
23
#include "PreCompiled.h"
25
# include <QShortcutEvent>
26
# include <QApplication>
29
#include <boost/algorithm/string/predicate.hpp>
31
#include <Base/Console.h>
32
#include <Base/Tools.h>
33
#include "ShortcutManager.h"
40
ShortcutManager::ShortcutManager()
42
hShortcuts = WindowParameter::getDefaultParameter()->GetGroup("Shortcut");
43
hShortcuts->Attach(this);
44
hPriorities = hShortcuts->GetGroup("Priorities");
45
hPriorities->Attach(this);
46
hSetting = hShortcuts->GetGroup("Settings");
47
hSetting->Attach(this);
48
timeout = hSetting->GetInt("ShortcutTimeout", 300);
49
timer.setSingleShot(true);
51
QObject::connect(&timer, &QTimer::timeout, [this](){onTimer();});
54
for (const auto &v : hPriorities->GetIntMap()) {
55
priorities[v.first] = v.second;
56
if (topPriority < v.second)
57
topPriority = v.second;
62
QApplication::instance()->installEventFilter(this);
65
ShortcutManager::~ShortcutManager()
67
hShortcuts->Detach(this);
68
hSetting->Detach(this);
69
hPriorities->Detach(this);
72
static ShortcutManager *Instance;
73
ShortcutManager *ShortcutManager::instance()
76
Instance = new ShortcutManager;
80
void ShortcutManager::destroy()
86
void ShortcutManager::OnChange(Base::Subject<const char*> &src, const char *reason)
88
if (hSetting == &src) {
89
if (boost::equals(reason, "ShortcutTimeout"))
90
timeout = hSetting->GetInt("ShortcutTimeout");
97
if (hPriorities == &src) {
98
int p = hPriorities->GetInt(reason, 0);
100
priorities.erase(reason);
102
priorities[reason] = p;
105
priorityChanged(reason, p);
109
Base::StateLocker lock(busy);
110
auto cmd = Application::Instance->commandManager().getCommandByName(reason);
112
auto accel = cmd->getAccel();
113
if (!accel) accel = "";
114
QKeySequence oldShortcut = cmd->getShortcut();
115
QKeySequence newShortcut = getShortcut(reason, accel);
116
if (oldShortcut != newShortcut) {
117
cmd->setShortcut(newShortcut.toString());
118
shortcutChanged(reason, oldShortcut);
123
void ShortcutManager::reset(const char *cmd)
126
QKeySequence oldShortcut = getShortcut(cmd);
127
hShortcuts->RemoveASCII(cmd);
128
if (oldShortcut != getShortcut(cmd))
129
shortcutChanged(cmd, oldShortcut);
131
int oldPriority = getPriority(cmd);
132
hPriorities->RemoveInt(cmd);
133
if (oldPriority != getPriority(cmd))
134
priorityChanged(cmd, oldPriority);
138
void ShortcutManager::resetAll()
141
Base::StateLocker lock(busy);
143
hPriorities->Clear();
144
for (auto cmd : Application::Instance->commandManager().getAllCommands()) {
145
if (cmd->getAction()) {
146
auto accel = cmd->getAccel();
147
if (!accel) accel = "";
148
cmd->setShortcut(getShortcut(nullptr, accel));
152
shortcutChanged("", QKeySequence());
153
priorityChanged("", 0);
156
QString ShortcutManager::getShortcut(const char *cmdName, const char *accel)
159
if (auto cmd = Application::Instance->commandManager().getCommandByName(cmdName)) {
160
accel = cmd->getAccel();
167
shortcut = QString::fromLatin1(hShortcuts->GetASCII(cmdName, accel).c_str());
169
shortcut = QString::fromLatin1(accel);
170
return QKeySequence(shortcut).toString(QKeySequence::NativeText);
173
void ShortcutManager::setShortcut(const char *cmdName, const char *accel)
175
if (cmdName && cmdName[0]) {
176
setTopPriority(cmdName);
179
if (auto cmd = Application::Instance->commandManager().getCommandByName(cmdName)) {
180
auto defaultAccel = cmd->getAccel();
183
if (QKeySequence(QString::fromLatin1(accel)) == QKeySequence(QString::fromLatin1(defaultAccel))) {
184
hShortcuts->RemoveASCII(cmdName);
188
hShortcuts->SetASCII(cmdName, accel);
192
bool ShortcutManager::checkShortcut(QObject *o, const QKeySequence &key)
194
auto focus = QApplication::focusWidget();
197
auto action = qobject_cast<QAction*>(o);
201
const auto &index = actionMap.get<1>();
202
auto iter = index.lower_bound(ActionKey(key));
203
if (iter == index.end())
206
// disable and enqueue the action in order to try other alternativeslll
207
action->setEnabled(false);
208
pendingActions.emplace_back(action, key.count(), 0);
210
// check for potential partial match, i.e. longer key sequences
213
for (auto it = iter; it != index.end(); ++it) {
214
if (key.matches(it->key.shortcut) == QKeySequence::NoMatch)
216
if (action == it->action) {
217
// There maybe more than one action with the exact same shortcut.
218
// However, we only disable and enqueue the triggered action.
219
// Because, QAction::isEnabled() does not check if the action is
220
// active under its current ShortcutContext. We would have to check
221
// its parent widgets visibility which may or may not be reliable.
222
// Instead, we rely on QEvent::Shortcut to be sure to enqueue only
223
// active shortcuts. We'll fake the current key sequence below,
224
// which will trigger all possible matches one by one.
225
pendingActions.back().priority = getPriority(it->key.name);
228
else if (it->action && it->action->isEnabled()) {
236
// We'll flush now because there is no potential match with further
237
// keystrokes, so no need to wait for timer.
244
pendingSequence = key;
246
// Qt's shortcut state machine favors shortest match (which is ridiculous,
247
// unless I'm mistaken?). We'll do longest match. We've disabled all
248
// shortcuts that can match the current key sequence. Now replay the sequence
249
// and wait for the next keystroke.
250
for (int i=0; i<key.count(); ++i) {
251
#if QT_VERSION < QT_VERSION_CHECK(6,0,0)
254
int k = key[i].key();
256
Qt::KeyboardModifiers modifiers;
257
if ((k & Qt::SHIFT) == Qt::SHIFT)
258
modifiers |= Qt::ShiftModifier;
259
if ((k & Qt::CTRL) == Qt::CTRL)
260
modifiers |= Qt::ControlModifier;
261
if ((k & Qt::ALT) == Qt::ALT)
262
modifiers |= Qt::AltModifier;
263
if ((k & Qt::META) == Qt::META)
264
modifiers |= Qt::MetaModifier;
265
k &= ~(Qt::SHIFT|Qt::CTRL|Qt::ALT|Qt::META);
266
QKeyEvent *kev = new QKeyEvent(QEvent::KeyPress, k, modifiers, 0, 0, 0);
267
QApplication::postEvent(focus, kev);
268
kev = new QKeyEvent(QEvent::KeyRelease, k, modifiers, 0, 0, 0);
269
QApplication::postEvent(focus, kev);
271
timer.start(timeout);
275
bool ShortcutManager::eventFilter(QObject *o, QEvent *ev)
278
case QEvent::KeyPress:
281
case QEvent::Shortcut:
283
auto sev = static_cast<QShortcutEvent*>(ev);
284
if (checkShortcut(o, sev->key())) {
285
// shortcut event handled here, so filter out the event
288
// Not handled. Clear any existing pending actions.
290
for (const auto &info : pendingActions) {
292
info.action->setEnabled(true);
294
pendingActions.clear();
299
case QEvent::ActionChanged:
300
if (auto action = qobject_cast<QAction*>(o)) {
301
auto &index = actionMap.get<0>();
302
auto it = index.find(reinterpret_cast<intptr_t>(action));
303
if (action->shortcut().isEmpty()) {
304
if (it != index.end()) {
305
QKeySequence oldShortcut = it->key.shortcut;
307
actionShortcutChanged(action, oldShortcut);
313
if (auto fcAction = qobject_cast<Action*>(action->parent())) {
314
if (fcAction->command() && fcAction->command()->getName())
315
name = fcAction->command()->getName();
317
if (name.isEmpty()) {
318
name = action->objectName().size() ?
319
action->objectName().toUtf8() : action->text().toUtf8();
323
name = QByteArray("~ ") + name;
325
if (it != index.end()) {
326
if (it->key.shortcut == action->shortcut() && it->key.name == name)
328
QKeySequence oldShortcut = it->key.shortcut;
329
index.replace(it, ActionData{action, name});
330
actionShortcutChanged(action, oldShortcut);
332
index.insert(ActionData{action, name});
333
actionShortcutChanged(action, QKeySequence());
343
std::vector<std::pair<QByteArray, QAction*>> ShortcutManager::getActionsByShortcut(const QKeySequence &shortcut)
345
const auto &index = actionMap.get<1>();
346
std::vector<std::pair<QByteArray, QAction*>> res;
347
std::multimap<int, const ActionData*, std::greater<>> map;
348
for (auto it = index.lower_bound(ActionKey(shortcut)); it != index.end(); ++it) {
349
if (it->key.shortcut != shortcut)
351
if (it->key.name != "~" && it->action)
352
map.emplace(getPriority(it->key.name), &(*it));
354
for (const auto &v : map)
355
res.emplace_back(v.second->key.name, v.second->action);
359
void ShortcutManager::setPriorities(const std::vector<QByteArray> &actions)
363
// Keep the same top priority of the given action, and adjust the rest. Can
364
// go negative if necessary
366
for (const auto &name : actions)
367
current = std::max(current, getPriority(name));
369
current = (int)actions.size();
370
setPriority(actions.front(), current);
372
for (const auto &name : actions) {
373
int p = getPriority(name);
374
if (p <= 0 || p >= current) {
377
setPriority(name, current);
383
int ShortcutManager::getPriority(const char *cmdName)
387
auto it = priorities.find(cmdName);
388
if (it == priorities.end())
393
void ShortcutManager::setPriority(const char *cmdName, int p)
396
hPriorities->RemoveInt(cmdName);
398
hPriorities->SetInt(cmdName, p);
401
void ShortcutManager::setTopPriority(const char *cmdName)
404
hPriorities->SetInt(cmdName, topPriority);
407
void ShortcutManager::onTimer()
411
QAction *found = nullptr;
412
int priority = -INT_MAX;
414
for (const auto &info : pendingActions) {
416
info.action->setEnabled(true);
417
if (info.seq_length > seq_length
418
|| (info.seq_length == seq_length
419
&& info.priority > priority))
421
priority = info.priority;
422
seq_length = info.seq_length;
428
found->activate(QAction::Trigger);
429
pendingActions.clear();
431
if (lastFocus && lastFocus == QApplication::focusWidget()) {
432
// We are here because we have withheld some previous triggered action.
433
// We then disabled the action, and faked the same key strokes in order
434
// to wait for more potential match of longer key sequence. We use
435
// a timer to end the wait and trigger the pending action.
437
// However, Qt's internal shorcutmap state machine is still armed with
438
// our fake key strokes. So we try to fake some more obscure symbol key
439
// stroke below, hoping to reset Qt's state machine.
441
const auto &index = actionMap.get<1>();
442
static const std::string symbols = "~!@#$%^&*()_+";
443
QString shortcut = pendingSequence.toString() + QStringLiteral(", Ctrl+");
444
for (int s : symbols) {
445
QKeySequence k(shortcut + QLatin1Char(s));
446
auto it = index.lower_bound(ActionKey(k));
447
if (it->key.shortcut != k) {
448
QKeyEvent *kev = new QKeyEvent(QEvent::KeyPress, s, Qt::ControlModifier, 0, 0, 0);
449
QApplication::postEvent(lastFocus, kev);
450
kev = new QKeyEvent(QEvent::KeyRelease, s, Qt::ControlModifier, 0, 0, 0);
451
QApplication::postEvent(lastFocus, kev);
458
#include "moc_ShortcutManager.cpp"