From da6f5f7da0343f5ce073bdc65cb9d2d641ed79ba Mon Sep 17 00:00:00 2001
From: "Zheng, Lei"
Date: Mon, 28 Feb 2022 15:32:49 +0800
Subject: [PATCH 01/19] Gui: add ShortcutManager to unify shortcut handling
Support longest key sequence match with user defined delay (configurable
through 'Customize -> Keyboard -> Key sequence delay').
Support user defined priority to resolve shortcut conflict through
'Customize -> Keyboard')
Add 'All' category in 'Customize -> Keyboard' to list all command and
showing their shortcuts
Unify macro command shortcut setting (BaseApp/Preferences/Shortcut).
---
src/Gui/Action.cpp | 319 ++++++++++++++++++++-
src/Gui/Action.h | 36 ++-
src/Gui/CMakeLists.txt | 2 +
src/Gui/Command.cpp | 136 ++++-----
src/Gui/Command.h | 18 ++
src/Gui/CommandPyImp.cpp | 50 +---
src/Gui/CommandView.cpp | 26 +-
src/Gui/DlgActionsImp.cpp | 34 +--
src/Gui/DlgActionsImp.h | 1 +
src/Gui/DlgKeyboard.ui | 523 ++++++++++++++++++++-------------
src/Gui/DlgKeyboardImp.cpp | 556 ++++++++++++++++++------------------
src/Gui/DlgKeyboardImp.h | 48 +++-
src/Gui/DlgToolbarsImp.cpp | 145 +---------
src/Gui/DlgToolbarsImp.h | 2 +
src/Gui/ShortcutManager.cpp | 455 +++++++++++++++++++++++++++++
src/Gui/ShortcutManager.h | 171 +++++++++++
src/Gui/Workbench.cpp | 3 +
17 files changed, 1773 insertions(+), 752 deletions(-)
create mode 100644 src/Gui/ShortcutManager.cpp
create mode 100644 src/Gui/ShortcutManager.h
diff --git a/src/Gui/Action.cpp b/src/Gui/Action.cpp
index ad210694c1ab..033fb84449af 100644
--- a/src/Gui/Action.cpp
+++ b/src/Gui/Action.cpp
@@ -53,6 +53,7 @@
#include "WhatsThis.h"
#include "Widgets.h"
#include "Workbench.h"
+#include "ShortcutManager.h"
using namespace Gui;
@@ -145,6 +146,11 @@ void Action::setEnabled(bool b)
_action->setEnabled(b);
}
+bool Action::isEnabled() const
+{
+ return _action->isEnabled();
+}
+
void Action::setVisible(bool b)
{
_action->setVisible(b);
@@ -153,6 +159,7 @@ void Action::setVisible(bool b)
void Action::setShortcut(const QString & key)
{
_action->setShortcut(key);
+ setToolTip(_tooltip, _title);
}
QKeySequence Action::shortcut() const
@@ -183,6 +190,8 @@ QString Action::statusTip() const
void Action::setText(const QString & s)
{
_action->setText(s);
+ if (_title.isEmpty())
+ setToolTip(_tooltip);
}
QString Action::text() const
@@ -190,14 +199,112 @@ QString Action::text() const
return _action->text();
}
-void Action::setToolTip(const QString & s)
-{
- _action->setToolTip(s);
+void Action::setToolTip(const QString & s, const QString & title)
+{
+ _tooltip = s;
+ _title = title;
+ _action->setToolTip(createToolTip(s,
+ title.isEmpty() ? _action->text() : title,
+ _action->font(),
+ _action->shortcut().toString(QKeySequence::NativeText),
+ this));
+}
+
+QString Action::createToolTip(QString _tooltip,
+ const QString & title,
+ const QFont &font,
+ const QString &sc,
+ Action *act)
+{
+ QString text = title;
+ text.remove(QLatin1Char('&'));;
+ while(text.size() && text[text.size()-1].isPunct())
+ text.resize(text.size()-1);
+
+ if (text.isEmpty())
+ return _tooltip;
+
+ // The following code tries to make a more useful tooltip by inserting at
+ // the beginning of the tooltip the action title in bold followed by the
+ // shortcut.
+ //
+ // The long winding code is to deal with the fact that Qt will auto wrap
+ // a rich text tooltip but the width is too short. We can escape the auto
+ // wrappin using .
+
+ QString shortcut = sc;
+ if (shortcut.size() && _tooltip.endsWith(shortcut))
+ _tooltip.resize(_tooltip.size() - shortcut.size());
+ if (shortcut.size())
+ shortcut = QString::fromLatin1(" (%1)").arg(shortcut);
+
+ QString tooltip = QString::fromLatin1(
+ "
%1%2
").arg(
+ text.toHtmlEscaped(), shortcut.toHtmlEscaped());
+
+ QString cmdName;
+ auto pcCmd = act ? act->_pcCmd : nullptr;
+ if (pcCmd && pcCmd->getName()) {
+ cmdName = QString::fromLatin1(pcCmd->getName());
+ if (auto groupcmd = dynamic_cast(pcCmd)) {
+ int idx = act->property("defaultAction").toInt();
+ auto cmd = groupcmd->getCommand(idx);
+ if (cmd && cmd->getName())
+ cmdName = QStringLiteral("%1 (%2:%3)")
+ .arg(QString::fromLatin1(cmd->getName()))
+ .arg(cmdName)
+ .arg(idx);
+ }
+ cmdName = QStringLiteral("%1
")
+ .arg(cmdName.toHtmlEscaped());
+ }
+
+ if (shortcut.size() && _tooltip.endsWith(shortcut))
+ _tooltip.resize(_tooltip.size() - shortcut.size());
+
+ if (_tooltip.isEmpty()
+ || _tooltip == text
+ || _tooltip == title)
+ {
+ return tooltip + cmdName;
+ }
+ if (Qt::mightBeRichText(_tooltip)) {
+ // already rich text, so let it be to avoid duplicated unwrapping
+ return tooltip + _tooltip + cmdName;
+ }
+
+ tooltip += QString::fromLatin1(
+ "");
+
+ // If the user supplied tooltip contains line break, we shall honour it.
+ if (_tooltip.indexOf(QLatin1Char('\n')) >= 0)
+ tooltip += _tooltip.toHtmlEscaped() + QString::fromLatin1("
") ;
+ else {
+ // If not, try to end the non wrapping paragraph at some pre defined
+ // width, so that the following text can wrap at that width.
+ float tipWidth = 400;
+ QFontMetrics fm(font);
+ int width = fm.width(_tooltip);
+ if (width <= tipWidth)
+ tooltip += _tooltip.toHtmlEscaped() + QString::fromLatin1("
") ;
+ else {
+ int index = tipWidth / width * _tooltip.size();
+ // Try to only break at white space
+ for(int i=0; i<50 && index<_tooltip.size(); ++i, ++index) {
+ if (_tooltip[index] == QLatin1Char(' '))
+ break;
+ }
+ tooltip += _tooltip.left(index).toHtmlEscaped()
+ + QString::fromLatin1("")
+ + _tooltip.right(_tooltip.size()-index).trimmed().toHtmlEscaped();
+ }
+ }
+ return tooltip + cmdName;
}
QString Action::toolTip() const
{
- return _action->toolTip();
+ return _tooltip;
}
void Action::setWhatsThis(const QString & s)
@@ -544,7 +651,7 @@ void WorkbenchGroup::addTo(QWidget *w)
QToolBar* bar = qobject_cast(w);
QComboBox* box = new WorkbenchComboBox(this, w);
box->setIconSize(QSize(16, 16));
- box->setToolTip(_action->toolTip());
+ box->setToolTip(_tooltip);
box->setStatusTip(_action->statusTip());
box->setWhatsThis(_action->whatsThis());
box->addActions(_group->actions());
@@ -1243,4 +1350,206 @@ void WindowAction::addTo ( QWidget * w )
}
}
+// --------------------------------------------------------------------
+
+struct CmdInfo {
+ Command *cmd = nullptr;
+ QString text;
+ QString tooltip;
+ QIcon icon;
+ bool iconChecked = false;
+};
+static std::vector _Commands;
+static int _CommandRevision;
+
+class CommandModel : public QAbstractItemModel
+{
+public:
+
+public:
+ CommandModel(QObject* parent)
+ : QAbstractItemModel(parent)
+ {
+ update();
+ }
+
+ void update()
+ {
+ auto &manager = Application::Instance->commandManager();
+ if (_CommandRevision == manager.getRevision())
+ return;
+ beginResetModel();
+ _CommandRevision = manager.getRevision();
+ _Commands.clear();
+ for (auto &v : manager.getCommands()) {
+ _Commands.emplace_back();
+ auto &info = _Commands.back();
+ info.cmd = v.second;
+ }
+ endResetModel();
+ }
+
+ virtual QModelIndex parent(const QModelIndex &) const
+ {
+ return QModelIndex();
+ }
+
+ virtual QVariant data(const QModelIndex & index, int role) const
+ {
+ if (index.row() < 0 || index.row() >= (int)_Commands.size())
+ return QVariant();
+
+ auto &info = _Commands[index.row()];
+
+ switch(role) {
+ case Qt::DisplayRole:
+ case Qt::EditRole:
+ if (info.text.isEmpty()) {
+#if QT_VERSION>=QT_VERSION_CHECK(5,2,0)
+ info.text = QString::fromLatin1("%2 (%1)").arg(
+ QString::fromLatin1(info.cmd->getName()),
+ qApp->translate(info.cmd->className(), info.cmd->getMenuText()));
+#else
+ info.text = qApp->translate(info.cmd->className(), info.cmd->getMenuText());
+#endif
+ info.text.replace(QLatin1Char('&'), QString());
+ if (info.text.isEmpty())
+ info.text = QString::fromLatin1(info.cmd->getName());
+ }
+ return info.text;
+
+ case Qt::DecorationRole:
+ if (!info.iconChecked) {
+ info.iconChecked = true;
+ if(info.cmd->getPixmap())
+ info.icon = BitmapFactory().iconFromTheme(info.cmd->getPixmap());
+ }
+ return info.icon;
+
+ case Qt::ToolTipRole:
+ if (info.tooltip.isEmpty()) {
+ info.tooltip = QString::fromLatin1("%1: %2").arg(
+ QString::fromLatin1(info.cmd->getName()),
+ qApp->translate(info.cmd->className(), info.cmd->getMenuText()));
+ QString tooltip = qApp->translate(info.cmd->className(), info.cmd->getToolTipText());
+ if (tooltip.size())
+ info.tooltip += QString::fromLatin1("\n\n") + tooltip;
+ }
+ return info.tooltip;
+
+ case Qt::UserRole:
+ return QByteArray(info.cmd->getName());
+
+ default:
+ return QVariant();
+ }
+ }
+
+ virtual QModelIndex index(int row, int, const QModelIndex &) const
+ {
+ return this->createIndex(row, 0);
+ }
+
+ virtual int rowCount(const QModelIndex &) const
+ {
+ return (int)(_Commands.size());
+ }
+
+ virtual int columnCount(const QModelIndex &) const
+ {
+ return 1;
+ }
+};
+
+
+// --------------------------------------------------------------------
+
+CommandCompleter::CommandCompleter(QLineEdit *lineedit, QObject *parent)
+ : QCompleter(parent)
+{
+ this->setModel(new CommandModel(this));
+#if QT_VERSION>=QT_VERSION_CHECK(5,2,0)
+ this->setFilterMode(Qt::MatchContains);
+#endif
+ this->setCaseSensitivity(Qt::CaseInsensitive);
+ this->setCompletionMode(QCompleter::PopupCompletion);
+ this->setWidget(lineedit);
+ connect(lineedit, SIGNAL(textEdited(QString)), this, SLOT(onTextChanged(QString)));
+ connect(this, SIGNAL(activated(QModelIndex)), this, SLOT(onCommandActivated(QModelIndex)));
+ connect(this, SIGNAL(highlighted(QString)), lineedit, SLOT(setText(QString)));
+}
+
+bool CommandCompleter::eventFilter(QObject *o, QEvent *ev)
+{
+ if (ev->type() == QEvent::KeyPress
+ && (o == this->widget() || o == this->popup()))
+ {
+ QKeyEvent * ke = static_cast(ev);
+ switch(ke->key()) {
+ case Qt::Key_Escape: {
+ auto edit = qobject_cast(this->widget());
+ if (edit && edit->text().size()) {
+ edit->setText(QString());
+ popup()->hide();
+ return true;
+ } else if (popup()->isVisible()) {
+ popup()->hide();
+ return true;
+ }
+ break;
+ }
+ case Qt::Key_Tab: {
+ if (this->popup()->isVisible()) {
+ QKeyEvent kevent(ke->type(),Qt::Key_Down,0);
+ qApp->sendEvent(this->popup(), &kevent);
+ return true;
+ }
+ break;
+ }
+ case Qt::Key_Backtab: {
+ if (this->popup()->isVisible()) {
+ QKeyEvent kevent(ke->type(),Qt::Key_Up,0);
+ qApp->sendEvent(this->popup(), &kevent);
+ return true;
+ }
+ break;
+ }
+ case Qt::Key_Enter:
+ case Qt::Key_Return:
+ if (o == this->widget()) {
+ auto index = currentIndex();
+ if (index.isValid())
+ onCommandActivated(index);
+ else
+ complete();
+ ev->setAccepted(true);
+ return true;
+ }
+ default:
+ break;
+ }
+ }
+ return QCompleter::eventFilter(o, ev);
+}
+
+void CommandCompleter::onCommandActivated(const QModelIndex &index)
+{
+ QByteArray name = completionModel()->data(index, Qt::UserRole).toByteArray();
+ Q_EMIT commandActivated(name);
+}
+
+void CommandCompleter::onTextChanged(const QString &txt)
+{
+ if (txt.size() < 3 || !widget())
+ return;
+
+ static_cast(this->model())->update();
+
+ this->setCompletionPrefix(txt);
+ QRect rect = widget()->rect();
+ if (rect.width() < 300)
+ rect.setWidth(300);
+ this->complete(rect);
+}
+
#include "moc_Action.cpp"
diff --git a/src/Gui/Action.h b/src/Gui/Action.h
index d50deb8cb0b8..5c656fc746c6 100644
--- a/src/Gui/Action.h
+++ b/src/Gui/Action.h
@@ -28,6 +28,7 @@
#include
#include
#include
+#include
namespace Gui
{
@@ -57,6 +58,7 @@ class GuiExport Action : public QObject
void setCheckable(bool);
void setChecked (bool, bool no_signal=false);
bool isChecked() const;
+ bool isEnabled() const;
void setShortcut (const QString &);
QKeySequence shortcut() const;
@@ -66,7 +68,7 @@ class GuiExport Action : public QObject
QString statusTip() const;
void setText (const QString &);
QString text() const;
- void setToolTip (const QString &);
+ void setToolTip (const QString &, const QString &title = QString());
QString toolTip() const;
void setWhatsThis (const QString &);
QString whatsThis() const;
@@ -75,6 +77,16 @@ class GuiExport Action : public QObject
return _action;
}
+ static QString createToolTip(QString tooltip,
+ const QString &title,
+ const QFont &font,
+ const QString &shortcut,
+ Action *action = nullptr);
+
+ Command *command() const {
+ return _pcCmd;
+ }
+
public Q_SLOTS:
virtual void onActivated ();
virtual void onToggled (bool);
@@ -82,6 +94,8 @@ public Q_SLOTS:
protected:
QAction* _action;
Command *_pcCmd;
+ QString _tooltip;
+ QString _title;
};
// --------------------------------------------------------------------
@@ -357,6 +371,26 @@ class GuiExport WindowAction : public ActionGroup
QMenu* _menu;
};
+/**
+ * Command name completer.
+ */
+class GuiExport CommandCompleter : public QCompleter
+{
+ Q_OBJECT
+public:
+ CommandCompleter(QLineEdit *edit, QObject *parent = nullptr);
+
+Q_SIGNALS:
+ void commandActivated(const QByteArray &name);
+
+protected Q_SLOTS:
+ void onTextChanged(const QString &);
+ void onCommandActivated(const QModelIndex &);
+
+protected:
+ bool eventFilter(QObject *, QEvent *ev);
+};
+
} // namespace Gui
#endif // GUI_ACTION_H
diff --git a/src/Gui/CMakeLists.txt b/src/Gui/CMakeLists.txt
index 0f9dd088c0dc..431f62f9204a 100644
--- a/src/Gui/CMakeLists.txt
+++ b/src/Gui/CMakeLists.txt
@@ -395,6 +395,7 @@ SET(Command_CPP_SRCS
CommandStructure.cpp
CommandLink.cpp
CommandPyImp.cpp
+ ShortcutManager.cpp
)
SET(Command_SRCS
${Command_CPP_SRCS}
@@ -402,6 +403,7 @@ SET(Command_SRCS
ActionFunction.h
Command.h
CommandT.h
+ ShortcutManager.h
)
SOURCE_GROUP("Command" FILES ${Command_SRCS})
diff --git a/src/Gui/Command.cpp b/src/Gui/Command.cpp
index 83c31092124d..7d90f071c237 100644
--- a/src/Gui/Command.cpp
+++ b/src/Gui/Command.cpp
@@ -61,6 +61,7 @@
#include "WhatsThis.h"
#include "WorkbenchManager.h"
#include "Workbench.h"
+#include "ShortcutManager.h"
FC_LOG_LEVEL_INIT("Command", true, true)
@@ -158,6 +159,11 @@ CommandBase::~CommandBase()
//Note: The Action object becomes a children of MainWindow which gets destroyed _before_ the
//command manager hence before any command object. So the action pointer is a dangling pointer
//at this state.
+
+ // Command can be destroyed before the the MainWindow, for example, dynamic
+ // command created (and later deleted) by user for a pie menu.
+ if (getMainWindow())
+ delete _pcAction;
}
Action* CommandBase::getAction() const
@@ -223,6 +229,19 @@ Command::~Command()
{
}
+void Command::setShortcut(const QString &shortcut)
+{
+ if (_pcAction)
+ _pcAction->setShortcut(shortcut);
+}
+
+QString Command::getShortcut() const
+{
+ if (_pcAction)
+ return _pcAction->shortcut().toString();
+ return ShortcutManager::instance()->getShortcut(getName());
+}
+
bool Command::isViewOfType(Base::Type t) const
{
Gui::Document *d = getGuiApplication()->activeDocument();
@@ -239,6 +258,12 @@ void Command::addTo(QWidget *pcWidget)
{
if (!_pcAction) {
_pcAction = createAction();
+#ifdef FC_DEBUG
+ // Accelerator conflict can now be dynamically resolved in ShortcutManager
+ //
+ // printConflictingAccelerators();
+#endif
+ setShortcut(ShortcutManager::instance()->getShortcut(getName(), getAccel()));
testActive();
}
@@ -255,6 +280,12 @@ void Command::addToGroup(ActionGroup* group)
{
if (!_pcAction) {
_pcAction = createAction();
+#ifdef FC_DEBUG
+ // Accelerator conflict can now be dynamically resolved in ShortcutManager
+ //
+ // printConflictingAccelerators();
+#endif
+ setShortcut(ShortcutManager::instance()->getShortcut(getName(), getAccel()));
testActive();
}
group->addAction(_pcAction->findChild());
@@ -856,37 +887,14 @@ const char * Command::endCmdHelp(void)
return "