FreeCAD

Форк
0
/
ShortcutManager.cpp 
458 строк · 15.5 Кб
1
/****************************************************************************
2
 *   Copyright (c) 2022 Zheng Lei (realthunder) <realthunder.dev@gmail.com> *
3
 *                                                                          *
4
 *   This file is part of the FreeCAD CAx development system.               *
5
 *                                                                          *
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.       *
10
 *                                                                          *
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.                   *
15
 *                                                                          *
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                                 *
20
 *                                                                          *
21
 ****************************************************************************/
22

23
#include "PreCompiled.h"
24
#ifndef _PreComp_
25
# include <QShortcutEvent>
26
# include <QApplication>
27
#endif
28

29
#include <boost/algorithm/string/predicate.hpp>
30

31
#include <Base/Console.h>
32
#include <Base/Tools.h>
33
#include "ShortcutManager.h"
34
#include "Command.h"
35
#include "Window.h"
36
#include "Action.h"
37

38
using namespace Gui;
39

40
ShortcutManager::ShortcutManager()
41
{
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);
50

51
    QObject::connect(&timer, &QTimer::timeout, [this](){onTimer();});
52

53
    topPriority = 0;
54
    for (const auto &v : hPriorities->GetIntMap()) {
55
        priorities[v.first] = v.second;
56
        if (topPriority < v.second)
57
            topPriority = v.second;
58
    }
59
    if (topPriority == 0)
60
        topPriority = 100;
61

62
    QApplication::instance()->installEventFilter(this);
63
}
64

65
ShortcutManager::~ShortcutManager()
66
{
67
    hShortcuts->Detach(this);
68
    hSetting->Detach(this);
69
    hPriorities->Detach(this);
70
}
71

72
static ShortcutManager *Instance;
73
ShortcutManager *ShortcutManager::instance()
74
{
75
    if (!Instance)
76
        Instance = new ShortcutManager;
77
    return Instance;
78
}
79

80
void ShortcutManager::destroy()
81
{
82
    delete Instance;
83
    Instance = nullptr;
84
}
85

86
void ShortcutManager::OnChange(Base::Subject<const char*> &src, const char *reason)
87
{
88
    if (hSetting == &src) {
89
        if (boost::equals(reason, "ShortcutTimeout"))
90
            timeout = hSetting->GetInt("ShortcutTimeout");
91
        return;
92
    }
93

94
    if (busy)
95
        return;
96

97
    if (hPriorities == &src) {
98
        int p = hPriorities->GetInt(reason, 0);
99
        if (p == 0)
100
            priorities.erase(reason);
101
        else
102
            priorities[reason] = p;
103
        if (topPriority < p)
104
            topPriority = p;
105
        priorityChanged(reason, p);
106
        return;
107
    }
108

109
    Base::StateLocker lock(busy);
110
    auto cmd = Application::Instance->commandManager().getCommandByName(reason);
111
    if (cmd) {
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);
119
        }
120
    }
121
}
122

123
void ShortcutManager::reset(const char *cmd)
124
{
125
    if (cmd && cmd[0]) {
126
        QKeySequence oldShortcut = getShortcut(cmd);
127
        hShortcuts->RemoveASCII(cmd);
128
        if (oldShortcut != getShortcut(cmd))
129
            shortcutChanged(cmd, oldShortcut);
130

131
        int oldPriority = getPriority(cmd);
132
        hPriorities->RemoveInt(cmd);
133
        if (oldPriority != getPriority(cmd))
134
            priorityChanged(cmd, oldPriority);
135
    }
136
}
137

138
void ShortcutManager::resetAll()
139
{
140
    {
141
        Base::StateLocker lock(busy);
142
        hShortcuts->Clear();
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));
149
            }
150
        }
151
    }
152
    shortcutChanged("", QKeySequence());
153
    priorityChanged("", 0);
154
}
155

156
QString ShortcutManager::getShortcut(const char *cmdName, const char *accel)
157
{
158
    if (!accel) {
159
        if (auto cmd = Application::Instance->commandManager().getCommandByName(cmdName)) {
160
            accel = cmd->getAccel();
161
            if (!accel)
162
                accel = "";
163
        }
164
    }
165
    QString shortcut;
166
    if (cmdName)
167
        shortcut = QString::fromLatin1(hShortcuts->GetASCII(cmdName, accel).c_str());
168
    else
169
        shortcut = QString::fromLatin1(accel);
170
    return QKeySequence(shortcut).toString(QKeySequence::NativeText);
171
}
172

173
void ShortcutManager::setShortcut(const char *cmdName, const char *accel)
174
{
175
    if (cmdName && cmdName[0]) {
176
        setTopPriority(cmdName);
177
        if (!accel)
178
            accel = "";
179
        if (auto cmd = Application::Instance->commandManager().getCommandByName(cmdName)) {
180
            auto defaultAccel = cmd->getAccel();
181
            if (!defaultAccel)
182
               defaultAccel = "";
183
           if (QKeySequence(QString::fromLatin1(accel)) == QKeySequence(QString::fromLatin1(defaultAccel))) {
184
                hShortcuts->RemoveASCII(cmdName);
185
                return;
186
           }
187
        }
188
        hShortcuts->SetASCII(cmdName, accel);
189
    }
190
}
191

192
bool ShortcutManager::checkShortcut(QObject *o, const QKeySequence &key)
193
{
194
    auto focus = QApplication::focusWidget();
195
    if (!focus)
196
        return false;
197
    auto action = qobject_cast<QAction*>(o);
198
    if (!action)
199
        return false;
200

201
    const auto &index = actionMap.get<1>();
202
    auto iter = index.lower_bound(ActionKey(key));
203
    if (iter == index.end())
204
        return false;
205

206
    // disable and enqueue the action in order to try other alternativeslll
207
    action->setEnabled(false);
208
    pendingActions.emplace_back(action, key.count(), 0);
209

210
    // check for potential partial match, i.e. longer key sequences
211
    bool flush = true;
212
    bool found = false;
213
    for (auto it = iter; it != index.end(); ++it) {
214
        if (key.matches(it->key.shortcut) == QKeySequence::NoMatch)
215
            break;
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);
226
            found = true;
227
        }
228
        else if (it->action && it->action->isEnabled()) {
229
            flush = false;
230
            if (found)
231
                break;
232
        }
233
    }
234

235
    if (flush) {
236
        // We'll flush now because there is no potential match with further
237
        // keystrokes, so no need to wait for timer.
238
        lastFocus = nullptr;
239
        onTimer();
240
        return true;
241
    }
242

243
    lastFocus = focus;
244
    pendingSequence = key;
245

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)
252
        int k = key[i];
253
#else
254
        int k = key[i].key();
255
#endif
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);
270
    }
271
    timer.start(timeout);
272
    return true;
273
}
274

275
bool ShortcutManager::eventFilter(QObject *o, QEvent *ev)
276
{
277
    switch(ev->type()) {
278
    case QEvent::KeyPress:
279
        lastFocus = nullptr;
280
        break;
281
    case QEvent::Shortcut:
282
        if (timeout > 0) {
283
            auto sev = static_cast<QShortcutEvent*>(ev);
284
            if (checkShortcut(o, sev->key())) {
285
                // shortcut event handled here, so filter out the event
286
                return true;
287
            } else {
288
                // Not handled. Clear any existing pending actions.
289
                timer.stop();
290
                for (const auto &info : pendingActions) {
291
                    if (info.action)
292
                        info.action->setEnabled(true);
293
                }
294
                pendingActions.clear();
295
                lastFocus = nullptr;
296
            }
297
        }
298
        break;
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;
306
                    index.erase(it);
307
                    actionShortcutChanged(action, oldShortcut);
308
                }
309
                break;
310
            }
311

312
            QByteArray name;
313
            if (auto fcAction = qobject_cast<Action*>(action->parent())) {
314
                if (fcAction->command() && fcAction->command()->getName())
315
                    name = fcAction->command()->getName();
316
            }
317
            if (name.isEmpty()) {
318
                name = action->objectName().size() ?
319
                    action->objectName().toUtf8() : action->text().toUtf8();
320
                if (name.isEmpty())
321
                    name = "~";
322
                else
323
                    name = QByteArray("~ ") + name;
324
            }
325
            if (it != index.end()) {
326
                if (it->key.shortcut == action->shortcut() && it->key.name == name)
327
                    break;
328
                QKeySequence oldShortcut = it->key.shortcut;
329
                index.replace(it, ActionData{action, name});
330
                actionShortcutChanged(action, oldShortcut);
331
            } else {
332
                index.insert(ActionData{action, name});
333
                actionShortcutChanged(action, QKeySequence());
334
            }
335
        }
336
        break;
337
    default:
338
        break;
339
    }
340
    return false;
341
}
342

343
std::vector<std::pair<QByteArray, QAction*>> ShortcutManager::getActionsByShortcut(const QKeySequence &shortcut)
344
{
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)
350
            break;
351
        if (it->key.name != "~" && it->action)
352
            map.emplace(getPriority(it->key.name), &(*it));
353
    }
354
    for (const auto &v : map)
355
        res.emplace_back(v.second->key.name, v.second->action);
356
    return res;
357
}
358

359
void ShortcutManager::setPriorities(const std::vector<QByteArray> &actions)
360
{
361
    if (actions.empty())
362
        return;
363
    // Keep the same top priority of the given action, and adjust the rest. Can
364
    // go negative if necessary
365
    int current = 0;
366
    for (const auto &name : actions)
367
        current = std::max(current, getPriority(name));
368
    if (current == 0)
369
        current = (int)actions.size();
370
    setPriority(actions.front(), current);
371
    ++current;
372
    for (const auto &name : actions) {
373
        int p = getPriority(name);
374
        if (p <= 0 || p >= current) {
375
            if (--current == 0)
376
                --current;
377
            setPriority(name, current);
378
        } else
379
            current = p;
380
    }
381
}
382

383
int ShortcutManager::getPriority(const char *cmdName)
384
{
385
    if (!cmdName)
386
        return 0;
387
    auto it = priorities.find(cmdName);
388
    if (it == priorities.end())
389
        return 0;
390
    return it->second;
391
}
392

393
void ShortcutManager::setPriority(const char *cmdName, int p)
394
{
395
    if (p == 0)
396
        hPriorities->RemoveInt(cmdName);
397
    else
398
        hPriorities->SetInt(cmdName, p);
399
}
400

401
void ShortcutManager::setTopPriority(const char *cmdName)
402
{
403
    ++topPriority;
404
    hPriorities->SetInt(cmdName, topPriority);
405
}
406

407
void ShortcutManager::onTimer()
408
{
409
    timer.stop();
410

411
    QAction *found = nullptr;
412
    int priority = -INT_MAX;
413
    int seq_length = 0;
414
    for (const auto &info : pendingActions) {
415
        if (info.action) {
416
            info.action->setEnabled(true);
417
            if (info.seq_length > seq_length
418
                    || (info.seq_length == seq_length
419
                        && info.priority > priority))
420
            {
421
                priority = info.priority;
422
                seq_length = info.seq_length;
423
                found = info.action;
424
            }
425
        }
426
    }
427
    if (found)
428
        found->activate(QAction::Trigger);
429
    pendingActions.clear();
430

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.
436
        //
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.
440

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);
452
                break;
453
            }
454
        }
455
    }
456
}
457

458
#include "moc_ShortcutManager.cpp"
459

Использование cookies

Мы используем файлы cookie в соответствии с Политикой конфиденциальности и Политикой использования cookies.

Нажимая кнопку «Принимаю», Вы даете АО «СберТех» согласие на обработку Ваших персональных данных в целях совершенствования нашего веб-сайта и Сервиса GitVerse, а также повышения удобства их использования.

Запретить использование cookies Вы можете самостоятельно в настройках Вашего браузера.