diff --git a/src/Gui/Action.cpp b/src/Gui/Action.cpp index da43f8ffca..965f9ba7bb 100644 --- a/src/Gui/Action.cpp +++ b/src/Gui/Action.cpp @@ -57,6 +57,7 @@ #include "WhatsThis.h" #include "Widgets.h" #include "Workbench.h" +#include "ShortcutManager.h" using namespace Gui; @@ -149,6 +150,11 @@ void Action::setEnabled(bool b) _action->setEnabled(b); } +bool Action::isEnabled() const +{ + return _action->isEnabled(); +} + void Action::setVisible(bool b) { _action->setVisible(b); @@ -157,6 +163,7 @@ void Action::setVisible(bool b) void Action::setShortcut(const QString & key) { _action->setShortcut(key); + setToolTip(_tooltip, _title); } QKeySequence Action::shortcut() const @@ -187,6 +194,8 @@ QString Action::statusTip() const void Action::setText(const QString & s) { _action->setText(s); + if (_title.isEmpty()) + setToolTip(_tooltip); } QString Action::text() const @@ -194,14 +203,112 @@ QString Action::text() const return _action->text(); } -void Action::setToolTip(const QString & s) +void Action::setToolTip(const QString & s, const QString & title) { - _action->setToolTip(s); + _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) @@ -533,7 +640,7 @@ void WorkbenchGroup::addTo(QWidget *w) auto setupBox = [&](QComboBox* box) { box->setIconSize(QSize(16, 16)); - box->setToolTip(_action->toolTip()); + box->setToolTip(_tooltip); box->setStatusTip(_action->statusTip()); box->setWhatsThis(_action->whatsThis()); box->addActions(_group->actions()); @@ -1244,4 +1351,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 6a6be0c5a0..a9ec0b359d 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 @@ public: 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 @@ public: 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 @@ public: 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 @@ private: 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 2c23b7e7bb..68b07a0044 100644 --- a/src/Gui/CMakeLists.txt +++ b/src/Gui/CMakeLists.txt @@ -384,6 +384,7 @@ SET(Command_CPP_SRCS CommandStructure.cpp CommandLink.cpp CommandPyImp.cpp + ShortcutManager.cpp ) SET(Command_SRCS ${Command_CPP_SRCS} @@ -391,6 +392,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 dd7e9b780d..34c4d40c66 100644 --- a/src/Gui/Command.cpp +++ b/src/Gui/Command.cpp @@ -64,6 +64,7 @@ #include "WhatsThis.h" #include "WorkbenchManager.h" #include "Workbench.h" +#include "ShortcutManager.h" FC_LOG_LEVEL_INIT("Command", true, true) @@ -161,6 +162,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 @@ -226,6 +232,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(); @@ -244,6 +263,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(); } @@ -260,6 +285,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()); @@ -869,37 +900,14 @@ const char * Command::endCmdHelp() return "\n\n"; } -void Command::recreateTooltip(const char* context, Action* action) +void Command::applyCommandData(const char* context, Action* action) { - QString tooltip; - tooltip.append(QString::fromLatin1("

")); - tooltip.append(QCoreApplication::translate( + action->setText(QCoreApplication::translate( context, getMenuText())); - tooltip.append(QString::fromLatin1("

")); - QRegularExpression re(QString::fromLatin1("([^&])&([^&])")); - tooltip.replace(re, QString::fromLatin1("\\1\\2")); - tooltip.replace(QString::fromLatin1("&&"), QString::fromLatin1("&")); - tooltip.append(QCoreApplication::translate( + action->setToolTip(QCoreApplication::translate( context, getToolTipText())); - tooltip.append(QString::fromLatin1("
(")); - tooltip.append(QCoreApplication::translate( + action->setWhatsThis(QCoreApplication::translate( context, getWhatsThis())); - tooltip.append(QString::fromLatin1(") ")); - action->setToolTip(tooltip); - - QString accel = action->shortcut().toString(QKeySequence::NativeText); - if (!accel.isEmpty()) { - // show shortcut inside tooltip - QString ttip = QString::fromLatin1("%1 (%2)") - .arg(action->toolTip(), accel); - action->setToolTip(ttip); - - // show shortcut inside status tip - QString stip = QString::fromLatin1("(%1)\t%2") - .arg(accel, action->statusTip()); - action->setStatusTip(stip); - } - if (sStatusTip) action->setStatusTip(QCoreApplication::translate( context, getStatusTip())); @@ -908,14 +916,6 @@ void Command::recreateTooltip(const char* context, Action* action) context, getToolTipText())); } -void Command::applyCommandData(const char* context, Action* action) -{ - action->setText(QCoreApplication::translate( - context, getMenuText())); - recreateTooltip(context, action); - action->setWhatsThis(QCoreApplication::translate( - context, getWhatsThis())); -} const char* Command::keySequenceToAccel(int sk) const { @@ -987,10 +987,6 @@ Action * Command::createAction() { Action *pcAction; pcAction = new Action(this,getMainWindow()); -#ifdef FC_DEBUG - printConflictingAccelerators(); -#endif - pcAction->setShortcut(QString::fromLatin1(sAccel)); applyCommandData(this->className(), pcAction); if (sPixmap) pcAction->setIcon(Gui::BitmapFactory().iconFromTheme(sPixmap)); @@ -1030,8 +1026,15 @@ Command *GroupCommand::addCommand(const char *name) { return cmd; } +Command *GroupCommand::getCommand(int idx) const +{ + if (idx >= 0 && idx < (int)cmds.size()) + return cmds[idx].first; + return nullptr; +} + Action * GroupCommand::createAction() { - auto pcAction = new ActionGroup(this, getMainWindow()); + auto* pcAction = new ActionGroup(this, getMainWindow()); pcAction->setMenuRole(QAction::NoRole); pcAction->setDropDownMenu(true); pcAction->setExclusive(false); @@ -1077,19 +1080,28 @@ void GroupCommand::languageChange() { void GroupCommand::setup(Action *pcAction) { - pcAction->setText(QCoreApplication::translate(className(), getMenuText())); - // The tooltip for the group is the tooltip of the active tool (that is, the tool that will // be activated when the main portion of the button is clicked). int idx = pcAction->property("defaultAction").toInt(); if(idx>=0 && idx<(int)cmds.size() && cmds[idx].first) { auto cmd = cmds[idx].first; - pcAction->setIcon(BitmapFactory().iconFromTheme(cmd->getPixmap())); - pcAction->setChecked(cmd->getAction()->isChecked(),true); + pcAction->setText(QCoreApplication::translate(className(), getMenuText())); + if (auto childAction = cmd->getAction()) + pcAction->setIcon(childAction->icon()); + else + pcAction->setIcon(BitmapFactory().iconFromTheme(cmd->getPixmap())); const char *context = dynamic_cast(cmd) ? cmd->getName() : cmd->className(); - cmd->recreateTooltip(context, cmd->getAction()); - pcAction->setToolTip(cmd->getAction()->toolTip()); - pcAction->setStatusTip(cmd->getAction()->statusTip()); + const char *tooltip = cmd->getToolTipText(); + const char *statustip = cmd->getStatusTip(); + if (!statustip || '\0' == *statustip) + statustip = tooltip; + pcAction->setToolTip(QCoreApplication::translate(context,tooltip), + QCoreApplication::translate(cmd->className(), cmd->getMenuText())); + pcAction->setStatusTip(QCoreApplication::translate(context,statustip)); + } else { + applyCommandData(this->className(), pcAction); + if (sPixmap) + pcAction->setIcon(Gui::BitmapFactory().iconFromTheme(sPixmap)); } } @@ -1157,23 +1169,6 @@ Action * MacroCommand::createAction() pcAction->setWhatsThis(QString::fromUtf8(sWhatsThis)); if (sPixmap) pcAction->setIcon(Gui::BitmapFactory().pixmap(sPixmap)); -#ifdef FC_DEBUG - printConflictingAccelerators(); -#endif - pcAction->setShortcut(QString::fromLatin1(sAccel)); - - QString accel = pcAction->shortcut().toString(QKeySequence::NativeText); - if (!accel.isEmpty()) { - // show shortcut inside tooltip - QString ttip = QString::fromLatin1("%1 (%2)") - .arg(pcAction->toolTip(), accel); - pcAction->setToolTip(ttip); - - // show shortcut inside status tip - QString stip = QString::fromLatin1("(%1)\t%2") - .arg(accel, pcAction->statusTip()); - pcAction->setStatusTip(stip); - } return pcAction; } @@ -1362,10 +1357,6 @@ Action * PythonCommand::createAction() Action *pcAction; pcAction = new Action(this, qtAction, getMainWindow()); -#ifdef FC_DEBUG - printConflictingAccelerators(); -#endif - pcAction->setShortcut(QString::fromLatin1(getAccel())); applyCommandData(this->getName(), pcAction); if (strcmp(getResource("Pixmap"),"") != 0) pcAction->setIcon(Gui::BitmapFactory().iconFromTheme(getResource("Pixmap"))); @@ -1759,15 +1750,25 @@ CommandManager::~CommandManager() void CommandManager::addCommand(Command* pCom) { - _sCommands[pCom->getName()] = pCom;// pCom->Init(); + auto &cmd = _sCommands[pCom->getName()]; + if (cmd) { + if(FC_LOG_INSTANCE.isEnabled(FC_LOGLEVEL_LOG)) + FC_ERR("duplicate command " << pCom->getName()); + return; + } + ++_revision; + cmd = pCom; + signalChanged(); } void CommandManager::removeCommand(Command* pCom) { std::map ::iterator It = _sCommands.find(pCom->getName()); if (It != _sCommands.end()) { + ++_revision; delete It->second; _sCommands.erase(It); + signalChanged(); } } @@ -1796,12 +1797,13 @@ std::string CommandManager::newMacroName() const return name; } - void CommandManager::clearCommands() { for ( std::map::iterator it = _sCommands.begin(); it != _sCommands.end(); ++it ) delete it->second; _sCommands.clear(); + ++_revision; + signalChanged(); } bool CommandManager::addTo(const char* Name, QWidget *pcWidget) @@ -1886,6 +1888,7 @@ void CommandManager::addCommandMode(const char* sContext, const char* sName) void CommandManager::updateCommands(const char* sContext, int mode) { std::map >::iterator it = _sCommandModes.find(sContext); + int rev = _revision; if (it != _sCommandModes.end()) { for (const auto & jt : it->second) { Command* cmd = getCommandByName(jt.c_str()); @@ -1894,6 +1897,8 @@ void CommandManager::updateCommands(const char* sContext, int mode) } } } + if (rev != _revision) + signalChanged(); } const Command* Gui::CommandManager::checkAcceleratorForConflicts(const char* accel, const Command* ignore) const diff --git a/src/Gui/Command.h b/src/Gui/Command.h index e3812bea2a..5c135ce876 100644 --- a/src/Gui/Command.h +++ b/src/Gui/Command.h @@ -25,6 +25,10 @@ #define GUI_COMMAND_H #include +#include +#include +#include +#include #include #include @@ -568,6 +572,11 @@ public: //@} + /// Override shortcut of this command + virtual void setShortcut (const QString &); + /// Obtain the current shortcut of this command + virtual QString getShortcut() const; + /** @name arbitrary helper methods */ //@{ void adjustCameraPosition(); @@ -641,6 +650,7 @@ public: */ Command *addCommand(const char *cmdName); + Command *getCommand(int idx) const; protected: void activated(int iMsg) override; Gui::Action * createAction() override; @@ -871,6 +881,12 @@ public: void addCommandMode(const char* sContext, const char* sName); void updateCommands(const char* sContext, int mode); + /// Return a revision number to check for addition or removal of any command + int getRevision() { return _revision; } + + /// Signal on any addition or removal of command + boost::signals2::signal signalChanged; + /** * Returns a pointer to a conflicting command, or nullptr if there is no conflict. * In the case of multiple conflicts, only the first is returned. @@ -891,6 +907,8 @@ private: void clearCommands(); std::map _sCommands; std::map > _sCommandModes; + + int _revision = 0; }; } // namespace Gui diff --git a/src/Gui/CommandPyImp.cpp b/src/Gui/CommandPyImp.cpp index 9f0f2ad4d6..50005c268f 100644 --- a/src/Gui/CommandPyImp.cpp +++ b/src/Gui/CommandPyImp.cpp @@ -38,6 +38,7 @@ // inclusion of the generated files (generated out of CommandPy.xml) #include "CommandPy.h" #include "CommandPy.cpp" +#include "ShortcutManager.h" // returns a string which represents the object e.g. when printed in python @@ -188,33 +189,8 @@ PyObject* CommandPy::setShortcut(PyObject *args) Command* cmd = this->getCommandPtr(); if (cmd) { - Action* action = cmd->getAction(); - if (action) { - QKeySequence shortcut = QString::fromLatin1(pShortcut); - QString nativeText = shortcut.toString(QKeySequence::NativeText); - action->setShortcut(nativeText); - bool success = action->shortcut() == nativeText; - /** - * avoid cluttering parameters unnecessarily by saving only - * when new shortcut is not the default shortcut - * remove spaces to handle cases such as shortcut = "C,L" or "C, L" - */ - QString default_shortcut = QString::fromLatin1(cmd->getAccel()); - QString spc = QString::fromLatin1(" "); - - ParameterGrp::handle hGrp = WindowParameter::getDefaultParameter()->GetGroup("Shortcut"); - const char* pName = cmd->getName(); - if (success && default_shortcut.remove(spc).toUpper() != nativeText.remove(spc).toUpper()) { - hGrp->SetASCII(pName, pShortcut); - } - else { - hGrp->RemoveASCII(pName); - } - return Py::new_reference_to(Py::Boolean(success)); - } - else { - return Py::new_reference_to(Py::Boolean(false)); - } + ShortcutManager::instance()->setShortcut(cmd->getName(), pShortcut); + return Py::new_reference_to(Py::Boolean(true)); } else { PyErr_Format(Base::PyExc_FC_GeneralError, "No such command"); @@ -230,24 +206,8 @@ PyObject* CommandPy::resetShortcut(PyObject *args) Command* cmd = this->getCommandPtr(); if (cmd) { - Action* action = cmd->getAction(); - if (action){ - QString default_shortcut = QString::fromLatin1(cmd->getAccel()); - action->setShortcut(default_shortcut); - ParameterGrp::handle hGrp = WindowParameter::getDefaultParameter()->GetGroup("Shortcut"); - hGrp->RemoveASCII(cmd->getName()); - /** test to see if we successfully reset the shortcut by loading it back and comparing */ - QString spc = QString::fromLatin1(" "); - QString new_shortcut = action->shortcut().toString(); - if (default_shortcut.remove(spc).toUpper() == new_shortcut.remove(spc).toUpper()){ - return Py::new_reference_to(Py::Boolean(true)); - } else { - return Py::new_reference_to(Py::Boolean(false)); - } - } else { - return Py::new_reference_to(Py::Boolean(false)); - } - + ShortcutManager::instance()->reset(cmd->getName()); + return Py::new_reference_to(Py::Boolean(true)); } else { PyErr_Format(Base::PyExc_FC_GeneralError, "No such command"); return nullptr; diff --git a/src/Gui/CommandView.cpp b/src/Gui/CommandView.cpp index 33652cbb25..ca2c9737af 100644 --- a/src/Gui/CommandView.cpp +++ b/src/Gui/CommandView.cpp @@ -289,10 +289,13 @@ public: const char* className() const override { return "StdCmdFreezeViews"; } + void setShortcut (const QString &) override; + QString getShortcut() const override; + protected: void activated(int iMsg) override; bool isActive() override; - Action * createAction() override; + Action * createAction(void) override; void languageChange() override; private: @@ -356,6 +359,19 @@ Action * StdCmdFreezeViews::createAction() return pcAction; } +void StdCmdFreezeViews::setShortcut(const QString &shortcut) +{ + if (freezeView) + freezeView->setShortcut(shortcut); +} + +QString StdCmdFreezeViews::getShortcut() const +{ + if (freezeView) + return freezeView->shortcut().toString(); + return Command::getShortcut(); +} + void StdCmdFreezeViews::activated(int iMsg) { auto pcAction = qobject_cast(_pcAction); diff --git a/src/Gui/DlgActionsImp.cpp b/src/Gui/DlgActionsImp.cpp index b28c8f45c4..8dbf05de4c 100644 --- a/src/Gui/DlgActionsImp.cpp +++ b/src/Gui/DlgActionsImp.cpp @@ -39,6 +39,7 @@ #include "Application.h" #include "BitmapFactory.h" #include "Command.h" +#include "ShortcutManager.h" #include "ui_DlgChooseIcon.h" @@ -87,6 +88,8 @@ DlgCustomActionsImp::DlgCustomActionsImp( QWidget* parent ) /** Destroys the object and frees any allocated resources */ DlgCustomActionsImp::~DlgCustomActionsImp() { + if (bChanged) + MacroCommand::save(); } bool DlgCustomActionsImp::event(QEvent* e) @@ -126,17 +129,18 @@ bool DlgCustomActionsImp::event(QEvent* e) void DlgCustomActionsImp::onAddMacroAction(const QByteArray&) { - // does nothing + bChanged = true; } -void DlgCustomActionsImp::onRemoveMacroAction(const QByteArray&) +void DlgCustomActionsImp::onRemoveMacroAction(const QByteArray &name) { - // does nothing + bChanged = true; + ShortcutManager::instance()->reset(name.constData()); } void DlgCustomActionsImp::onModifyMacroAction(const QByteArray&) { - // does nothing + bChanged = true; } void DlgCustomActionsImp::showActions() @@ -192,7 +196,8 @@ void DlgCustomActionsImp::on_actionListWidget_itemActivated(QTreeWidgetItem *ite ui->actionMenu -> setText(QString::fromUtf8(pScript->getMenuText())); ui->actionToolTip -> setText(QString::fromUtf8(pScript->getToolTipText())); ui->actionStatus -> setText(QString::fromUtf8(pScript->getStatusTip())); - ui->actionAccel -> setText(QString::fromLatin1(pScript->getAccel())); + ui->actionAccel -> setText(ShortcutManager::instance()->getShortcut( + actionName.constData(), pScript->getAccel())); ui->pixmapLabel->clear(); m_sPixmap.clear(); const char* name = pScript->getPixmap(); @@ -263,7 +268,8 @@ void DlgCustomActionsImp::on_buttonAddAction_clicked() m_sPixmap.clear(); if (!ui->actionAccel->text().isEmpty()) { - macro->setAccel(ui->actionAccel->text().toLatin1()); + ShortcutManager::instance()->setShortcut( + actionName.constData(), ui->actionAccel->text().toLatin1().constData()); } ui->actionAccel->clear(); @@ -335,20 +341,8 @@ void DlgCustomActionsImp::on_buttonReplaceAction_clicked() action->setStatusTip(QString::fromUtf8(macro->getStatusTip())); if (macro->getPixmap()) action->setIcon(Gui::BitmapFactory().pixmap(macro->getPixmap())); - action->setShortcut(QString::fromLatin1(macro->getAccel())); - - QString accel = action->shortcut().toString(QKeySequence::NativeText); - if (!accel.isEmpty()) { - // show shortcut inside tooltip - QString ttip = QString::fromLatin1("%1 (%2)") - .arg(action->toolTip(), accel); - action->setToolTip(ttip); - - // show shortcut inside status tip - QString stip = QString::fromLatin1("(%1)\t%2") - .arg(accel, action->statusTip()); - action->setStatusTip(stip); - } + action->setShortcut(ShortcutManager::instance()->getShortcut( + actionName.constData(), macro->getAccel())); } // emit signal to notify the container widget diff --git a/src/Gui/DlgActionsImp.h b/src/Gui/DlgActionsImp.h index 53e01a7925..a9b8016e55 100644 --- a/src/Gui/DlgActionsImp.h +++ b/src/Gui/DlgActionsImp.h @@ -88,6 +88,7 @@ private: private: std::unique_ptr ui; QString m_sPixmap; /**< Name of the specified pixmap */ + bool bChanged = false; }; class Ui_DlgChooseIcon; diff --git a/src/Gui/DlgKeyboard.ui b/src/Gui/DlgKeyboard.ui index 13266b2f81..f48ea1df12 100644 --- a/src/Gui/DlgKeyboard.ui +++ b/src/Gui/DlgKeyboard.ui @@ -6,225 +6,321 @@ 0 0 - 578 - 344 + 642 + 376 Keyboard - - - 9 - - - 6 - - - - - 6 - - - 0 - - - - - &Category: - - - categoryBox - - - - - - - - - - C&ommands: - - - commandTreeWidget - - - - - - - - 220 - 0 - - - - false - - - - 1 - - - - - - - - - - 6 - - - 0 - - - - - Current shortcut: - - - - - - - false - - - - - - - Press &new shortcut: - - - editShortcut - - - - - - - - - - Currently assigned to: - - - - - - - false - - - - 220 - 0 - - - - false - - - - 1 - - - - - - - - + + + Qt::Horizontal - - QSizePolicy::Preferred - - - - 40 - 20 - - - + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + + + 0 + 0 + + + + &Category: + + + categoryBox + + + + + + + + + + + 220 + 0 + + + + false + + + true + + + + 1 + + + + + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + + Multi-key sequence delay: + + + + + + + Current shortcut: + + + + + + + + 0 + 0 + + + + + 50 + 0 + + + + Time in milliseconds to wait for the next key stroke of the current key sequence. +For example, pressing 'F' twice in less than the time delay setting here will be +be treated as shorctcut key sequence 'F, F'. + + + 10000 + + + 100 + + + 300 + + + Shortcut/Settings + + + ShortcutTimeout + + + + + + + + 220 + 0 + + + + true + + + false + + + + 1 + + + + + + + + true + + + + + + + + + &New shortcut: + + + editShortcut + + + + + + + + + + + + + 0 + 20 + + + + This list shows commands having the same shortcut in the priority from hight +to low. If more than one command with the same shortcut are active at the +same time. The one with the highest prioirty will be triggered. + + + Shorcut priority list: + + + + + + + + + 6 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + &Assign + + + Alt+A + + + + + + + Clear + + + + + + + &Reset + + + Alt+R + + + + + + + Re&set All + + + Alt+S + + + + + + + Qt::Vertical + + + QSizePolicy::Expanding + + + + 41 + 150 + + + + + + + + false + + + Up + + + + + + + false + + + Down + + + + + + + + - - - - 6 - - - 0 + + + + QLayout::SetDefaultConstraint - - - &Assign + + + + 0 + 0 + - - Alt+A - - - - - - - Clear - - - - - - - &Reset - - - Alt+R - - - - - - - Re&set All - - - Alt+S - - - - - - - Qt::Vertical - - - QSizePolicy::Expanding - - - - 41 - 150 - - - - - - - - - - 6 - - - 0 - - - Description: @@ -232,6 +328,12 @@ + + + 0 + 0 + + @@ -239,7 +341,7 @@ - + @@ -248,14 +350,25 @@ QLineEdit
Gui/Widgets.h
+ + Gui::PrefSpinBox + QSpinBox +
Gui/PrefWidgets.h
+
+ editCommand categoryBox commandTreeWidget accelLineEditShortcut buttonAssign + buttonClear buttonReset buttonResetAll + shortcutTimeout + assignedTreeWidget + buttonUp + buttonDown diff --git a/src/Gui/DlgKeyboardImp.cpp b/src/Gui/DlgKeyboardImp.cpp index 170bf659de..5a65721d3b 100644 --- a/src/Gui/DlgKeyboardImp.cpp +++ b/src/Gui/DlgKeyboardImp.cpp @@ -22,13 +22,17 @@ #include "PreCompiled.h" +#include #ifndef _PreComp_ # include # include # include +# include #endif #include +#include +#include #include "DlgKeyboardImp.h" #include "ui_DlgKeyboard.h" @@ -37,6 +41,8 @@ #include "BitmapFactory.h" #include "Command.h" #include "Window.h" +#include "PrefWidgets.h" +#include "ShortcutManager.h" using namespace Gui::Dialog; @@ -71,6 +77,231 @@ DlgCustomKeyboardImp::DlgCustomKeyboardImp( QWidget* parent ) { ui->setupUi(this); + conn = initCommandWidgets(ui->commandTreeWidget, + ui->categoryBox, + ui->editCommand, + ui->assignedTreeWidget, + ui->buttonUp, + ui->buttonDown, + ui->editShortcut, + ui->accelLineEditShortcut); + + ui->shortcutTimeout->onRestore(); + QTimer *timer = new QTimer(this); + QObject::connect(ui->shortcutTimeout, QOverload::of(&QSpinBox::valueChanged), + timer, [=](int) {timer->start(100);}); + QObject::connect(timer, &QTimer::timeout, [=](){ui->shortcutTimeout->onSave();}); +} + +/** Destroys the object and frees any allocated resources */ +DlgCustomKeyboardImp::~DlgCustomKeyboardImp() +{ +} + +void DlgCustomKeyboardImp::initCommandCompleter(QLineEdit *edit, QComboBox *combo, QTreeWidget *commandTreeWidget) +{ + edit->setPlaceholderText(tr("Type to search...")); + auto completer = new CommandCompleter(edit, edit); + + QObject::connect(completer, &CommandCompleter::commandActivated, + [combo, commandTreeWidget](const QByteArray &name) { + CommandManager & cCmdMgr = Application::Instance->commandManager(); + Command *cmd = cCmdMgr.getCommandByName(name.constData()); + if (!cmd) + return; + + QString group = QString::fromLatin1(cmd->getGroupName()); + int index = combo->findData(group); + if (index < 0) + return; + if (index != combo->currentIndex()) { + combo->setCurrentIndex(index); + combo->activated(index); + } + for (int i=0 ; itopLevelItemCount(); ++i) { + QTreeWidgetItem *item = commandTreeWidget->topLevelItem(i); + if (item->data(1, Qt::UserRole).toByteArray() == name) { + commandTreeWidget->setCurrentItem(item); + return; + } + } + }); +} + +boost::signals2::connection +DlgCustomKeyboardImp::initCommandList(QTreeWidget *commandTreeWidget, QComboBox *combo) +{ + QStringList labels; + labels << tr("Icon") << tr("Command") << tr("Shortcut") << tr("Default"); + commandTreeWidget->setHeaderLabels(labels); + commandTreeWidget->setIconSize(QSize(32, 32)); + commandTreeWidget->header()->setSectionResizeMode(0, QHeaderView::ResizeToContents); + commandTreeWidget->header()->setSectionResizeMode(2, QHeaderView::ResizeToContents); + commandTreeWidget->header()->setSectionResizeMode(3, QHeaderView::ResizeToContents); + + QObject::connect(combo, QOverload::of(&QComboBox::activated), [=](int index) { + QByteArray current; + if (auto item = commandTreeWidget->currentItem()) + current = item->data(1, Qt::UserRole).toByteArray(); + + commandTreeWidget->clear(); + CommandManager & cCmdMgr = Application::Instance->commandManager(); + QString group = combo->itemData(index, Qt::UserRole).toString(); + auto cmds = group == QStringLiteral("All") ? cCmdMgr.getAllCommands() + : cCmdMgr.getGroupCommands(group.toLatin1()); + QTreeWidgetItem *currentItem = nullptr; + for (const Command *cmd : cmds) { + QTreeWidgetItem* item = new QTreeWidgetItem(commandTreeWidget); + if (dynamic_cast(cmd)) { + item->setText(1, QString::fromUtf8(cmd->getMenuText())); + item->setToolTip(1, QString::fromUtf8(cmd->getToolTipText())); + } else { + item->setText(1, qApp->translate(cmd->className(), cmd->getMenuText())); + item->setToolTip(1, qApp->translate(cmd->className(), cmd->getToolTipText())); + } + item->setData(1, Qt::UserRole, QByteArray(cmd->getName())); + item->setSizeHint(0, QSize(32, 32)); + if (auto pixmap = cmd->getPixmap()) + item->setIcon(0, BitmapFactory().iconFromTheme(pixmap)); + item->setText(2, cmd->getShortcut()); + if (auto accel = cmd->getAccel()) + item->setText(3, QKeySequence(QString::fromLatin1(accel)).toString()); + + if (current == cmd->getName()) + currentItem = item; + } + if (currentItem) + commandTreeWidget->setCurrentItem(currentItem); + commandTreeWidget->resizeColumnToContents(2); + commandTreeWidget->resizeColumnToContents(3); + }); + + QObject::connect(ShortcutManager::instance(), &ShortcutManager::shortcutChanged, + combo, [combo]() { combo->activated(combo->currentIndex()); }); + + populateCommandGroups(combo); + + return Application::Instance->commandManager().signalChanged.connect([combo](){ + if (combo) { + populateCommandGroups(combo); + combo->activated(combo->currentIndex()); + } + }); +} + +void DlgCustomKeyboardImp::initPriorityList(QTreeWidget *priorityList, + QAbstractButton *buttonUp, + QAbstractButton *buttonDown) +{ + QStringList labels; + labels << tr("Name") << tr("Title"); + priorityList->setHeaderLabels(labels); + priorityList->header()->hide(); + priorityList->header()->setSectionResizeMode(0, QHeaderView::ResizeToContents); + priorityList->header()->setSectionResizeMode(1, QHeaderView::ResizeToContents); + + auto updatePriorityList = [priorityList](bool up) { + auto item = priorityList->currentItem(); + if (!item) + return; + + int index = priorityList->indexOfTopLevelItem(item); + if (index < 0) + return; + if ((index == 0 && up) + || (index == priorityList->topLevelItemCount()-1 && !up)) + return; + + std::vector actions; + for (int i=0; itopLevelItemCount(); ++i) { + auto item = priorityList->topLevelItem(i); + actions.push_back(item->data(0, Qt::UserRole).toByteArray()); + } + + auto it = actions.begin() + index; + auto itNext = up ? it - 1 : it + 1; + std::swap(*it, *itNext); + ShortcutManager::instance()->setPriorities(actions); + }; + + QObject::connect(buttonUp, &QAbstractButton::clicked, [=](){updatePriorityList(true);}); + QObject::connect(buttonDown, &QAbstractButton::clicked, [=](){updatePriorityList(false);}); + QObject::connect(priorityList, &QTreeWidget::currentItemChanged, + [=](QTreeWidgetItem *item){ + buttonUp->setEnabled(item!=nullptr); + buttonDown->setEnabled(item!=nullptr); + } + ); +} + +boost::signals2::connection +DlgCustomKeyboardImp::initCommandWidgets(QTreeWidget *commandTreeWidget, + QComboBox *comboGroups, + QLineEdit *editCommand, + QTreeWidget *priorityList, + QAbstractButton *buttonUp, + QAbstractButton *buttonDown, + AccelLineEdit *editShortcut, + AccelLineEdit *currentShortcut) +{ + initCommandCompleter(editCommand, comboGroups, commandTreeWidget); + auto conn = initCommandList(commandTreeWidget, comboGroups); + + if (priorityList && buttonUp && buttonDown) { + initPriorityList(priorityList, buttonUp, buttonDown); + + auto timer = new QTimer(priorityList); + timer->setSingleShot(true); + if (currentShortcut) + QObject::connect(currentShortcut, &QLineEdit::textChanged, timer, [timer](){timer->start(200);}); + QObject::connect(editShortcut, &QLineEdit::textChanged, timer, [timer](){timer->start(200);}); + QObject::connect(ShortcutManager::instance(), &ShortcutManager::priorityChanged, timer, [timer](){timer->start(200);}); + QObject::connect(timer, &QTimer::timeout, [=](){ + populatePriorityList(priorityList, editShortcut, currentShortcut); + }); + } + + return conn; +} + +void DlgCustomKeyboardImp::populatePriorityList(QTreeWidget *priorityList, + AccelLineEdit *editor, + AccelLineEdit *curShortcut) +{ + QByteArray current; + if (auto currentItem = priorityList->currentItem()) + current = currentItem->data(0, Qt::UserRole).toByteArray(); + + priorityList->clear(); + QString sc; + if (!editor->isNone() && editor->text().size()) + sc = editor->text(); + else if (curShortcut && !curShortcut->isNone()) + sc = curShortcut->text(); + + auto actionList = ShortcutManager::instance()->getActionsByShortcut(sc); + QTreeWidgetItem *currentItem = nullptr; + for (size_t i=0; isetText(0, QString::fromUtf8(info.first)); + item->setText(1, info.second->text()); + item->setToolTip(0, info.second->toolTip()); + item->setIcon(0, info.second->icon()); + item->setData(0, Qt::UserRole, info.first); + if (current == info.first) + currentItem = item; + } + priorityList->resizeColumnToContents(0); + priorityList->resizeColumnToContents(1); + if (currentItem) + priorityList->setCurrentItem(currentItem); +} + +void DlgCustomKeyboardImp::populateCommandGroups(QComboBox *combo) +{ CommandManager & cCmdMgr = Application::Instance->commandManager(); std::map sCommands = cCmdMgr.getCommands(); @@ -97,27 +328,14 @@ DlgCustomKeyboardImp::DlgCustomKeyboardImp( QWidget* parent ) groupMap.push_back(std::make_pair(group, text)); } } + groupMap.push_back(std::make_pair(QLatin1String("All"), tr("All"))); - int index = 0; - for (auto it = groupMap.begin(); it != groupMap.end(); ++it, ++index) { - ui->categoryBox->addItem(it->second); - ui->categoryBox->setItemData(index, QVariant(it->first), Qt::UserRole); + for (GroupMap::iterator it = groupMap.begin(); it != groupMap.end(); ++it) { + if (combo->findData(it->first) < 0) { + combo->addItem(it->second); + combo->setItemData(combo->count()-1, QVariant(it->first), Qt::UserRole); + } } - - QStringList labels; - labels << tr("Icon") << tr("Command"); - ui->commandTreeWidget->setHeaderLabels(labels); - ui->commandTreeWidget->header()->hide(); - ui->commandTreeWidget->setIconSize(QSize(32, 32)); - ui->commandTreeWidget->header()->setSectionResizeMode(0, QHeaderView::ResizeToContents); - - ui->assignedTreeWidget->setHeaderLabels(labels); - ui->assignedTreeWidget->header()->hide(); -} - -/** Destroys the object and frees any allocated resources */ -DlgCustomKeyboardImp::~DlgCustomKeyboardImp() -{ } void DlgCustomKeyboardImp::showEvent(QShowEvent* e) @@ -126,7 +344,7 @@ void DlgCustomKeyboardImp::showEvent(QShowEvent* e) // If we did this already in the constructor we wouldn't get the vertical scrollbar if needed. // The problem was noticed with Qt 4.1.4 but may arise with any later version. if (firstShow) { - on_categoryBox_activated(ui->categoryBox->currentIndex()); + ui->categoryBox->activated(ui->categoryBox->currentIndex()); firstShow = false; } } @@ -143,68 +361,29 @@ void DlgCustomKeyboardImp::on_commandTreeWidget_currentItemChanged(QTreeWidgetIt CommandManager & cCmdMgr = Application::Instance->commandManager(); Command* cmd = cCmdMgr.getCommandByName(name.constData()); if (cmd) { - if (cmd->getAction()) { - QKeySequence ks = cmd->getAction()->shortcut(); - QKeySequence ks2 = QString::fromLatin1(cmd->getAccel()); - QKeySequence ks3 = ui->editShortcut->text(); + QKeySequence ks = ShortcutManager::instance()->getShortcut( + cmd->getName(), cmd->getAccel()); + QKeySequence ks2 = QString::fromLatin1(cmd->getAccel()); + QKeySequence ks3 = ui->editShortcut->text(); + if (ks.isEmpty()) + ui->accelLineEditShortcut->setText( tr("none") ); + else + ui->accelLineEditShortcut->setText(ks.toString(QKeySequence::NativeText)); - if (ks.isEmpty()) - ui->accelLineEditShortcut->setText( tr("none") ); - else - ui->accelLineEditShortcut->setText(ks.toString(QKeySequence::NativeText)); - - ui->buttonAssign->setEnabled(!ui->editShortcut->text().isEmpty() && (ks != ks3)); - ui->buttonReset->setEnabled((ks != ks2)); - } else { - QKeySequence ks = QString::fromLatin1(cmd->getAccel()); - if (ks.isEmpty()) - ui->accelLineEditShortcut->setText( tr("none") ); - else - ui->accelLineEditShortcut->setText(ks.toString(QKeySequence::NativeText)); - ui->buttonAssign->setEnabled(false); - ui->buttonReset->setEnabled(false); - } + ui->buttonAssign->setEnabled(!ui->editShortcut->text().isEmpty() && (ks != ks3)); + ui->buttonReset->setEnabled((ks != ks2)); } ui->textLabelDescription->setText(item->toolTip(1)); } /** Shows all commands of this category */ -void DlgCustomKeyboardImp::on_categoryBox_activated(int index) +void DlgCustomKeyboardImp::on_categoryBox_activated(int) { - QVariant data = ui->categoryBox->itemData(index, Qt::UserRole); - QString group = data.toString(); - ui->commandTreeWidget->clear(); ui->buttonAssign->setEnabled(false); ui->buttonReset->setEnabled(false); ui->accelLineEditShortcut->clear(); ui->editShortcut->clear(); - - CommandManager & cCmdMgr = Application::Instance->commandManager(); - std::vector aCmds = cCmdMgr.getGroupCommands( group.toLatin1() ); - - if (group == QLatin1String("Macros")) { - for (const auto & aCmd : aCmds) { - auto item = new QTreeWidgetItem(ui->commandTreeWidget); - item->setText(1, QString::fromUtf8(aCmd->getMenuText())); - item->setToolTip(1, QString::fromUtf8(aCmd->getToolTipText())); - item->setData(1, Qt::UserRole, QByteArray(aCmd->getName())); - item->setSizeHint(0, QSize(32, 32)); - if (aCmd->getPixmap()) - item->setIcon(0, BitmapFactory().iconFromTheme(aCmd->getPixmap())); - } - } - else { - for (const auto & aCmd : aCmds) { - auto item = new QTreeWidgetItem(ui->commandTreeWidget); - item->setText(1, qApp->translate(aCmd->className(), aCmd->getMenuText())); - item->setToolTip(1, qApp->translate(aCmd->className(), aCmd->getToolTipText())); - item->setData(1, Qt::UserRole, QByteArray(aCmd->getName())); - item->setSizeHint(0, QSize(32, 32)); - if (aCmd->getPixmap()) - item->setIcon(0, BitmapFactory().iconFromTheme(aCmd->getPixmap())); - } - } } void DlgCustomKeyboardImp::setShortcutOfCurrentAction(const QString& accelText) @@ -216,44 +395,21 @@ void DlgCustomKeyboardImp::setShortcutOfCurrentAction(const QString& accelText) QVariant data = item->data(1, Qt::UserRole); QByteArray name = data.toByteArray(); // command name - CommandManager & cCmdMgr = Application::Instance->commandManager(); - Command* cmd = cCmdMgr.getCommandByName(name.constData()); - if (cmd && cmd->getAction()) { - QString nativeText; - Action* action = cmd->getAction(); - if (!accelText.isEmpty()) { - QKeySequence shortcut = accelText; - nativeText = shortcut.toString(QKeySequence::NativeText); - action->setShortcut(nativeText); - ui->accelLineEditShortcut->setText(accelText); - ui->editShortcut->clear(); - } - else { - action->setShortcut(QString()); - ui->accelLineEditShortcut->clear(); - ui->editShortcut->clear(); - } - - // update the tool tip (and status tip) - cmd->recreateTooltip(cmd->className(), action); - - // The shortcuts for macros are store in a different location, - // also override the command's shortcut directly - if (dynamic_cast(cmd)) { - ParameterGrp::handle hGrp = App::GetApplication().GetParameterGroupByPath("User parameter:BaseApp/Macro/Macros"); - if (hGrp->HasGroup(cmd->getName())) { - hGrp = hGrp->GetGroup(cmd->getName()); - hGrp->SetASCII("Accel", ui->accelLineEditShortcut->text().toUtf8()); - cmd->setAccel(ui->accelLineEditShortcut->text().toUtf8()); - } - } - else { - ParameterGrp::handle hGrp = WindowParameter::getDefaultParameter()->GetGroup("Shortcut"); - hGrp->SetASCII(name.constData(), ui->accelLineEditShortcut->text().toUtf8()); - } - ui->buttonAssign->setEnabled(false); - ui->buttonReset->setEnabled(true); + QString nativeText; + if (!accelText.isEmpty()) { + QKeySequence shortcut = accelText; + nativeText = shortcut.toString(QKeySequence::NativeText); + ui->accelLineEditShortcut->setText(accelText); + ui->editShortcut->clear(); } + else { + ui->accelLineEditShortcut->clear(); + ui->editShortcut->clear(); + } + ShortcutManager::instance()->setShortcut(name, nativeText.toLatin1()); + + ui->buttonAssign->setEnabled(false); + ui->buttonReset->setEnabled(true); } /** Assigns a new accelerator to the selected command. */ @@ -277,205 +433,54 @@ void DlgCustomKeyboardImp::on_buttonReset_clicked() QVariant data = item->data(1, Qt::UserRole); QByteArray name = data.toByteArray(); // command name + ShortcutManager::instance()->reset(name); - CommandManager & cCmdMgr = Application::Instance->commandManager(); - Command* cmd = cCmdMgr.getCommandByName(name.constData()); - if (cmd && cmd->getAction()) { - cmd->getAction()->setShortcut(QString::fromLatin1(cmd->getAccel())); - QString txt = cmd->getAction()->shortcut().toString(QKeySequence::NativeText); - ui->accelLineEditShortcut->setText((txt.isEmpty() ? tr("none") : txt)); - ParameterGrp::handle hGrp = WindowParameter::getDefaultParameter()->GetGroup("Shortcut"); - hGrp->RemoveASCII(name.constData()); - - // update the tool tip (and status tip) - cmd->recreateTooltip(cmd->className(), cmd->getAction()); - } - + QString txt = ShortcutManager::instance()->getShortcut(name); + ui->accelLineEditShortcut->setText((txt.isEmpty() ? tr("none") : txt)); ui->buttonReset->setEnabled( false ); } /** Resets the accelerator of all commands to the default. */ void DlgCustomKeyboardImp::on_buttonResetAll_clicked() { - CommandManager & cCmdMgr = Application::Instance->commandManager(); - std::vector cmds = cCmdMgr.getAllCommands(); - for (const auto & cmd : cmds) { - if (cmd->getAction()) { - cmd->getAction()->setShortcut(QKeySequence(QString::fromLatin1(cmd->getAccel())) - .toString(QKeySequence::NativeText)); - - - // update the tool tip (and status tip) - cmd->recreateTooltip(cmd->className(), cmd->getAction()); - } - } - - WindowParameter::getDefaultParameter()->RemoveGrp("Shortcut"); + ShortcutManager::instance()->resetAll(); ui->buttonReset->setEnabled(false); } /** Checks for an already occupied shortcut. */ -void DlgCustomKeyboardImp::on_editShortcut_textChanged(const QString& sc) +void DlgCustomKeyboardImp::on_editShortcut_textChanged(const QString& ) { - ui->assignedTreeWidget->clear(); QTreeWidgetItem* item = ui->commandTreeWidget->currentItem(); - if (!item) - return; - QVariant data = item->data(1, Qt::UserRole); - QByteArray name = data.toByteArray(); // command name - - CommandManager & cCmdMgr = Application::Instance->commandManager(); - Command* cmd = cCmdMgr.getCommandByName(name.constData()); - if (cmd && !cmd->getAction()) { - Base::Console().Warning("Command %s not in use yet\n", cmd->getName()); - QMessageBox::warning(this, - tr("Command not in use yet"), - tr("The command '%1' is loaded but not in use yet, so it can't be assigned a new shortcut.\n" - "To enable assignment, please make '%2' the active workbench").arg(data.toString(),ui->categoryBox->currentText()), - QMessageBox::Ok); - ui->buttonAssign->setEnabled(false); // command not in use - return; - } - - ui->buttonAssign->setEnabled(true); - QKeySequence ks(sc); - if (!ks.isEmpty() && !ui->editShortcut->isNone()) { - int countAmbiguous = 0; - QString ambiguousCommand; - QString ambiguousMenu; - std::vector ambiguousCommands; + if (item) { + QVariant data = item->data(1, Qt::UserRole); + QByteArray name = data.toByteArray(); // command name CommandManager & cCmdMgr = Application::Instance->commandManager(); - std::vector cmds = cCmdMgr.getAllCommands(); - for (const auto & cmd : cmds) { - if (cmd->getAction()) { - // A command may have several QAction's. So, check all of them if one of them matches (See bug #0002160) - QList acts = cmd->getAction()->findChildren(); - for (const auto & act : acts) { - if (act->shortcut() == ks) { - ++countAmbiguous; - ambiguousCommands.push_back(cmd); - ambiguousCommand = QString::fromLatin1(cmd->getName()); // store the last one - ambiguousMenu = qApp->translate(cmd->className(), cmd->getMenuText()); + Command* cmd = cCmdMgr.getCommandByName(name.constData()); - auto item = new QTreeWidgetItem(ui->assignedTreeWidget); - item->setText(1, qApp->translate(cmd->className(), cmd->getMenuText())); - item->setToolTip(1, qApp->translate(cmd->className(), cmd->getToolTipText())); - item->setData(1, Qt::UserRole, QByteArray(cmd->getName())); - item->setSizeHint(0, QSize(32, 32)); - if (cmd->getPixmap()) - item->setIcon(0, BitmapFactory().iconFromTheme(cmd->getPixmap())); - break; - } - } - } - } - - if (countAmbiguous > 0) - ui->assignedTreeWidget->resizeColumnToContents(0); - - if (countAmbiguous > 1) { - QMessageBox::warning(this, tr("Multiple defined keyboard shortcut"), - tr("The keyboard shortcut '%1' is defined more than once. This could result in unexpected behaviour.").arg(sc) ); - ui->editShortcut->setFocus(); - ui->buttonAssign->setEnabled(false); - } - else if (countAmbiguous == 1 && ambiguousCommand != QLatin1String(name)) { - QMessageBox box(this); - box.setIcon(QMessageBox::Warning); - box.setWindowTitle(tr("Already defined keyboard shortcut")); - box.setText(tr("The keyboard shortcut '%1' is already assigned to '%2'.").arg(sc, ambiguousMenu)); - box.setInformativeText(tr("Do you want to override it?")); - box.setStandardButtons(QMessageBox::Yes | QMessageBox::No); - box.setDefaultButton(QMessageBox::No); - box.setEscapeButton(QMessageBox::No); - int ret = box.exec(); - if (ret == QMessageBox::Yes) { - for (auto* cmd : ambiguousCommands) { - Action* action = cmd->getAction(); - action->setShortcut(QString()); - - ParameterGrp::handle hGrp = WindowParameter::getDefaultParameter()->GetGroup("Shortcut"); - hGrp->RemoveASCII(cmd->getName()); - } - } - else { - ui->editShortcut->setFocus(); - ui->buttonAssign->setEnabled(false); - } - } + if (!ui->editShortcut->isNone()) + ui->buttonAssign->setEnabled(true); else { - if (cmd && cmd->getAction() && cmd->getAction()->shortcut() == ks) - ui->buttonAssign->setEnabled(false); - } - } - else { - if (cmd && cmd->getAction() && cmd->getAction()->shortcut().isEmpty()) - ui->buttonAssign->setEnabled(false); // both key sequences are empty - } -} - -void DlgCustomKeyboardImp::onAddMacroAction(const QByteArray& macro) -{ - QVariant data = ui->categoryBox->itemData(ui->categoryBox->currentIndex(), Qt::UserRole); - QString group = data.toString(); - if (group == QLatin1String("Macros")) - { - CommandManager & cCmdMgr = Application::Instance->commandManager(); - Command* pCmd = cCmdMgr.getCommandByName(macro); - - auto item = new QTreeWidgetItem(ui->commandTreeWidget); - item->setText(1, QString::fromUtf8(pCmd->getMenuText())); - item->setToolTip(1, QString::fromUtf8(pCmd->getToolTipText())); - item->setData(1, Qt::UserRole, macro); - item->setSizeHint(0, QSize(32, 32)); - if (pCmd->getPixmap()) - item->setIcon(0, BitmapFactory().iconFromTheme(pCmd->getPixmap())); - } -} - -void DlgCustomKeyboardImp::onRemoveMacroAction(const QByteArray& macro) -{ - QVariant data = ui->categoryBox->itemData(ui->categoryBox->currentIndex(), Qt::UserRole); - QString group = data.toString(); - if (group == QLatin1String("Macros")) - { - for (int i=0; icommandTreeWidget->topLevelItemCount(); i++) { - QTreeWidgetItem* item = ui->commandTreeWidget->topLevelItem(i); - QByteArray command = item->data(1, Qt::UserRole).toByteArray(); - if (command == macro) { - ui->commandTreeWidget->takeTopLevelItem(i); - delete item; - break; - } + if (cmd && cmd->getAction() && cmd->getAction()->shortcut().isEmpty()) + ui->buttonAssign->setEnabled(false); // both key sequences are empty } } } -void DlgCustomKeyboardImp::onModifyMacroAction(const QByteArray& macro) +void DlgCustomKeyboardImp::onAddMacroAction(const QByteArray&) +{ +} + +void DlgCustomKeyboardImp::onRemoveMacroAction(const QByteArray&) +{ +} + +void DlgCustomKeyboardImp::onModifyMacroAction(const QByteArray&) { QVariant data = ui->categoryBox->itemData(ui->categoryBox->currentIndex(), Qt::UserRole); QString group = data.toString(); if (group == QLatin1String("Macros")) - { - CommandManager & cCmdMgr = Application::Instance->commandManager(); - Command* pCmd = cCmdMgr.getCommandByName(macro); - for (int i=0; icommandTreeWidget->topLevelItemCount(); i++) { - QTreeWidgetItem* item = ui->commandTreeWidget->topLevelItem(i); - QByteArray command = item->data(1, Qt::UserRole).toByteArray(); - if (command == macro) { - item->setText(1, QString::fromUtf8(pCmd->getMenuText())); - item->setToolTip(1, QString::fromUtf8(pCmd->getToolTipText())); - item->setData(1, Qt::UserRole, macro); - item->setSizeHint(0, QSize(32, 32)); - if (pCmd->getPixmap()) - item->setIcon(0, BitmapFactory().iconFromTheme(pCmd->getPixmap())); - if (item->isSelected()) - ui->textLabelDescription->setText(item->toolTip(1)); - break; - } - } - } + ui->categoryBox->activated(ui->categoryBox->currentIndex()); } void DlgCustomKeyboardImp::changeEvent(QEvent *e) @@ -493,8 +498,10 @@ void DlgCustomKeyboardImp::changeEvent(QEvent *e) ui->categoryBox->setItemText(i, text); } } - on_categoryBox_activated(ui->categoryBox->currentIndex()); + ui->categoryBox->activated(ui->categoryBox->currentIndex()); } + else if (e->type() == QEvent::StyleChange) + ui->categoryBox->activated(ui->categoryBox->currentIndex()); QWidget::changeEvent(e); } diff --git a/src/Gui/DlgKeyboardImp.h b/src/Gui/DlgKeyboardImp.h index e66162e829..7b07a78c70 100644 --- a/src/Gui/DlgKeyboardImp.h +++ b/src/Gui/DlgKeyboardImp.h @@ -24,12 +24,22 @@ #ifndef GUI_DIALOG_DLGKEYBOARD_IMP_H #define GUI_DIALOG_DLGKEYBOARD_IMP_H -#include "PropertyPage.h" +#include #include +#include +#include +#include "PropertyPage.h" +class QTreeWidget; class QTreeWidgetItem; +class QComboBox; +class QLineEdit; +class QAbstractButton; namespace Gui { + +class AccelLineEdit; + namespace Dialog { class Ui_DlgCustomKeyboard; @@ -48,9 +58,44 @@ public: explicit DlgCustomKeyboardImp( QWidget* parent = nullptr ); ~DlgCustomKeyboardImp() override; + /** Public helper function for handling command widgets + * + * @param commandTreeWidget: a tree widget listing commands + * @param comboGroups: a combo box widget for choosing categories of commands + * @param editCommand: a line edit for searching command with auto complete + * @param priroityList: a tree widget listing commands with the same shortcut in order of priority + * @param buttonUp: a button widget to increase command priority + * @param buttonDown: a button widget to decrease command priority + * @param editShortcut: an accelerator editor for setting user defined shortcut + * @param currentShortcut: optional accelerator editor showing the current shortcut of a command + * + * @return Return a boost signal connection for monitoring command changes. + * Most disconnect the signal before widgets gets destroyed. + */ + static boost::signals2::connection + initCommandWidgets(QTreeWidget *commandTreeWidget, + QComboBox *comboGroups, + QLineEdit *editCommand, + QTreeWidget *priorityList = nullptr, + QAbstractButton *buttonUp = nullptr, + QAbstractButton *buttonDown = nullptr, + AccelLineEdit *editShortcut = nullptr, + AccelLineEdit *currentShortcut = nullptr); + protected: void showEvent(QShowEvent* e) override; + /** @name Internal helper function for handling command list widgets + */ + //@{ + static void initCommandCompleter(QLineEdit *, QComboBox *combo, QTreeWidget *treeWidget); + static boost::signals2::connection initCommandList(QTreeWidget *, QComboBox *combo); + static void initPriorityList(QTreeWidget *, QAbstractButton *buttonUp, QAbstractButton *buttonDown); + static void populateCommandGroups(QComboBox *); + static void populatePriorityList(QTreeWidget *priorityList, + AccelLineEdit *editor, + AccelLineEdit *current); + //@} protected Q_SLOTS: void on_categoryBox_activated(int index); void on_commandTreeWidget_currentItemChanged(QTreeWidgetItem*); @@ -70,6 +115,7 @@ protected: private: std::unique_ptr ui; bool firstShow; + boost::signals2::scoped_connection conn; }; } // namespace Dialog diff --git a/src/Gui/DlgToolbarsImp.cpp b/src/Gui/DlgToolbarsImp.cpp index f3c6a41300..89e881f802 100644 --- a/src/Gui/DlgToolbarsImp.cpp +++ b/src/Gui/DlgToolbarsImp.cpp @@ -31,6 +31,7 @@ #endif #include "DlgToolbarsImp.h" +#include "DlgKeyboardImp.h" #include "ui_DlgToolbars.h" #include "Application.h" #include "BitmapFactory.h" @@ -44,20 +45,6 @@ using namespace Gui::Dialog; -namespace Gui { namespace Dialog { -using GroupMap = std::vector< std::pair >; - -struct GroupMap_find { - const QLatin1String& item; - explicit GroupMap_find(const QLatin1String& item) : item(item) {} - bool operator () (const std::pair& elem) const - { - return elem.first == item; - } -}; -} -} - /* TRANSLATOR Gui::Dialog::DlgCustomToolbars */ /** @@ -78,43 +65,15 @@ DlgCustomToolbars::DlgCustomToolbars(DlgCustomToolbars::Type t, QWidget* parent) ui->moveActionDownButton->setIcon(BitmapFactory().iconFromTheme("button_down")); ui->moveActionUpButton->setIcon(BitmapFactory().iconFromTheme("button_up")); - CommandManager & cCmdMgr = Application::Instance->commandManager(); - std::map sCommands = cCmdMgr.getCommands(); + conn = DlgCustomKeyboardImp::initCommandWidgets(ui->commandTreeWidget, + ui->categoryBox, + ui->editCommand); - GroupMap groupMap; - groupMap.push_back(std::make_pair(QLatin1String("File"), QString())); - groupMap.push_back(std::make_pair(QLatin1String("Edit"), QString())); - groupMap.push_back(std::make_pair(QLatin1String("View"), QString())); - groupMap.push_back(std::make_pair(QLatin1String("Standard-View"), QString())); - groupMap.push_back(std::make_pair(QLatin1String("Tools"), QString())); - groupMap.push_back(std::make_pair(QLatin1String("Window"), QString())); - groupMap.push_back(std::make_pair(QLatin1String("Help"), QString())); - groupMap.push_back(std::make_pair(QLatin1String("Macros"), qApp->translate("Gui::MacroCommand", "Macros"))); - - for (const auto & sCommand : sCommands) { - QLatin1String group(sCommand.second->getGroupName()); - QString text = sCommand.second->translatedGroupName(); - GroupMap::iterator jt; - jt = std::find_if(groupMap.begin(), groupMap.end(), GroupMap_find(group)); - if (jt != groupMap.end()) { - if (jt->second.isEmpty()) - jt->second = text; - } - else { - groupMap.push_back(std::make_pair(group, text)); - } - } - - int index = 0; - for (auto it = groupMap.begin(); it != groupMap.end(); ++it, ++index) { - ui->categoryBox->addItem(it->second); - ui->categoryBox->setItemData(index, QVariant(it->first), Qt::UserRole); - } // fills the combo box with all available workbenches QStringList workbenches = Application::Instance->workbenches(); workbenches.sort(); - index = 1; + int index = 1; ui->workbenchBox->addItem(QApplication::windowIcon(), tr("Global")); ui->workbenchBox->setItemData(0, QVariant(QString::fromLatin1("Global")), Qt::UserRole); for (const auto & workbench : workbenches) { @@ -131,13 +90,7 @@ DlgCustomToolbars::DlgCustomToolbars(DlgCustomToolbars::Type t, QWidget* parent) } QStringList labels; - labels << tr("Icon") << tr("Command"); - ui->commandTreeWidget->setHeaderLabels(labels); - ui->commandTreeWidget->header()->hide(); - ui->commandTreeWidget->setIconSize(QSize(32, 32)); - ui->commandTreeWidget->header()->setSectionResizeMode(0, QHeaderView::ResizeToContents); - - labels.clear(); labels << tr("Command"); + labels << tr("Command"); ui->toolbarTreeWidget->setHeaderLabels(labels); ui->toolbarTreeWidget->header()->hide(); @@ -193,43 +146,13 @@ void DlgCustomToolbars::hideEvent(QHideEvent * event) CustomizeActionPage::hideEvent(event); } -void DlgCustomToolbars::on_categoryBox_activated(int index) +void DlgCustomToolbars::on_categoryBox_activated(int) { - QVariant data = ui->categoryBox->itemData(index, Qt::UserRole); - QString group = data.toString(); - ui->commandTreeWidget->clear(); - - CommandManager & cCmdMgr = Application::Instance->commandManager(); - std::vector aCmds = cCmdMgr.getGroupCommands(group.toLatin1()); - - // Create a separator item - auto sepitem = new QTreeWidgetItem(ui->commandTreeWidget); + QTreeWidgetItem* sepitem = new QTreeWidgetItem; sepitem->setText(1, tr("")); sepitem->setData(1, Qt::UserRole, QByteArray("Separator")); sepitem->setSizeHint(0, QSize(32, 32)); - - if (group == QLatin1String("Macros")) { - for (const auto & aCmd : aCmds) { - auto item = new QTreeWidgetItem(ui->commandTreeWidget); - item->setText(1, QString::fromUtf8(aCmd->getMenuText())); - item->setToolTip(1, QString::fromUtf8(aCmd->getToolTipText())); - item->setData(1, Qt::UserRole, QByteArray(aCmd->getName())); - item->setSizeHint(0, QSize(32, 32)); - if (aCmd->getPixmap()) - item->setIcon(0, BitmapFactory().iconFromTheme(aCmd->getPixmap())); - } - } - else { - for (const auto & aCmd : aCmds) { - auto item = new QTreeWidgetItem(ui->commandTreeWidget); - item->setText(1, qApp->translate(aCmd->className(), aCmd->getMenuText())); - item->setToolTip(1, qApp->translate(aCmd->className(), aCmd->getToolTipText())); - item->setData(1, Qt::UserRole, QByteArray(aCmd->getName())); - item->setSizeHint(0, QSize(32, 32)); - if (aCmd->getPixmap()) - item->setIcon(0, BitmapFactory().iconFromTheme(aCmd->getPixmap())); - } - } + ui->commandTreeWidget->insertTopLevelItem(0, sepitem); } void DlgCustomToolbars::on_workbenchBox_activated(int index) @@ -556,41 +479,12 @@ void DlgCustomToolbars::on_renameButton_clicked() } } -void DlgCustomToolbars::onAddMacroAction(const QByteArray& macro) +void DlgCustomToolbars::onAddMacroAction(const QByteArray&) { - QVariant data = ui->categoryBox->itemData(ui->categoryBox->currentIndex(), Qt::UserRole); - QString group = data.toString(); - if (group == QLatin1String("Macros")) - { - CommandManager & cCmdMgr = Application::Instance->commandManager(); - Command* pCmd = cCmdMgr.getCommandByName(macro); - - auto item = new QTreeWidgetItem(ui->commandTreeWidget); - item->setText(1, QString::fromUtf8(pCmd->getMenuText())); - item->setToolTip(1, QString::fromUtf8(pCmd->getToolTipText())); - item->setData(1, Qt::UserRole, macro); - item->setSizeHint(0, QSize(32, 32)); - if (pCmd->getPixmap()) - item->setIcon(0, BitmapFactory().iconFromTheme(pCmd->getPixmap())); - } } -void DlgCustomToolbars::onRemoveMacroAction(const QByteArray& macro) +void DlgCustomToolbars::onRemoveMacroAction(const QByteArray&) { - QVariant data = ui->categoryBox->itemData(ui->categoryBox->currentIndex(), Qt::UserRole); - QString group = data.toString(); - if (group == QLatin1String("Macros")) - { - for (int i=0; icommandTreeWidget->topLevelItemCount(); i++) { - QTreeWidgetItem* item = ui->commandTreeWidget->topLevelItem(i); - QByteArray command = item->data(1, Qt::UserRole).toByteArray(); - if (command == macro) { - ui->commandTreeWidget->takeTopLevelItem(i); - delete item; - break; - } - } - } } void DlgCustomToolbars::onModifyMacroAction(const QByteArray& macro) @@ -601,20 +495,6 @@ void DlgCustomToolbars::onModifyMacroAction(const QByteArray& macro) { CommandManager & cCmdMgr = Application::Instance->commandManager(); Command* pCmd = cCmdMgr.getCommandByName(macro); - // the left side - for (int i=0; icommandTreeWidget->topLevelItemCount(); i++) { - QTreeWidgetItem* item = ui->commandTreeWidget->topLevelItem(i); - QByteArray command = item->data(1, Qt::UserRole).toByteArray(); - if (command == macro) { - item->setText(1, QString::fromUtf8(pCmd->getMenuText())); - item->setToolTip(1, QString::fromUtf8(pCmd->getToolTipText())); - item->setData(1, Qt::UserRole, macro); - item->setSizeHint(0, QSize(32, 32)); - if (pCmd->getPixmap()) - item->setIcon(0, BitmapFactory().iconFromTheme(pCmd->getPixmap())); - break; - } - } // the right side for (int i=0; itoolbarTreeWidget->topLevelItemCount(); i++) { QTreeWidgetItem* toplevel = ui->toolbarTreeWidget->topLevelItem(i); @@ -628,6 +508,7 @@ void DlgCustomToolbars::onModifyMacroAction(const QByteArray& macro) } } } + on_categoryBox_activated(ui->categoryBox->currentIndex()); } } @@ -648,6 +529,8 @@ void DlgCustomToolbars::changeEvent(QEvent *e) } on_categoryBox_activated(ui->categoryBox->currentIndex()); } + else if (e->type() == QEvent::StyleChange) + ui->categoryBox->activated(ui->categoryBox->currentIndex()); QWidget::changeEvent(e); } diff --git a/src/Gui/DlgToolbarsImp.h b/src/Gui/DlgToolbarsImp.h index 55ffb692fc..89cac3e546 100644 --- a/src/Gui/DlgToolbarsImp.h +++ b/src/Gui/DlgToolbarsImp.h @@ -24,6 +24,7 @@ #ifndef GUI_DIALOG_DLGTOOLBARS_IMP_H #define GUI_DIALOG_DLGTOOLBARS_IMP_H +#include #include "PropertyPage.h" #include @@ -82,6 +83,7 @@ protected: std::unique_ptr ui; private: Type type; + boost::signals2::scoped_connection conn; }; /** This class implements the creation of user defined toolbars. diff --git a/src/Gui/ShortcutManager.cpp b/src/Gui/ShortcutManager.cpp new file mode 100644 index 0000000000..32f3ca731f --- /dev/null +++ b/src/Gui/ShortcutManager.cpp @@ -0,0 +1,455 @@ +/**************************************************************************** + * Copyright (c) 2022 Zheng Lei (realthunder) * + * * + * This file is part of the FreeCAD CAx development system. * + * * + * This library is free software; you can redistribute it and/or * + * modify it under the terms of the GNU Library General Public * + * License as published by the Free Software Foundation; either * + * version 2 of the License, or (at your option) any later version. * + * * + * This library is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU Library General Public License for more details. * + * * + * You should have received a copy of the GNU Library General Public * + * License along with this library; see the file COPYING.LIB. If not, * + * write to the Free Software Foundation, Inc., 59 Temple Place, * + * Suite 330, Boston, MA 02111-1307, USA * + * * + ****************************************************************************/ + +#include "PreCompiled.h" +#ifndef _PreComp_ +# include +# include +#endif + +#include + +#include +#include +#include "ShortcutManager.h" +#include "Command.h" +#include "Window.h" +#include "Action.h" + +using namespace Gui; + +ShortcutManager::ShortcutManager() +{ + hShortcuts = WindowParameter::getDefaultParameter()->GetGroup("Shortcut"); + hShortcuts->Attach(this); + hPriorities = hShortcuts->GetGroup("Priorities"); + hPriorities->Attach(this); + hSetting = hShortcuts->GetGroup("Settings"); + hSetting->Attach(this); + timeout = hSetting->GetInt("ShortcutTimeout", 300); + timer.setSingleShot(true); + + QObject::connect(&timer, &QTimer::timeout, [this](){onTimer();}); + + topPriority = 0; + for (const auto &v : hPriorities->GetIntMap()) { + priorities[v.first] = v.second; + if (topPriority < v.second) + topPriority = v.second; + } + if (topPriority == 0) + topPriority = 100; + + QApplication::instance()->installEventFilter(this); +} + +ShortcutManager::~ShortcutManager() +{ + hShortcuts->Detach(this); + hSetting->Detach(this); + hPriorities->Detach(this); +} + +static ShortcutManager *Instance; +ShortcutManager *ShortcutManager::instance() +{ + if (!Instance) + Instance = new ShortcutManager; + return Instance; +} + +void ShortcutManager::destroy() +{ + delete Instance; + Instance = nullptr; +} + +void ShortcutManager::OnChange(Base::Subject &src, const char *reason) +{ + if (hSetting == &src) { + if (boost::equals(reason, "ShortcutTimeout")) + timeout = hSetting->GetInt("ShortcutTimeout"); + return; + } + + if (busy) + return; + + if (hPriorities == &src) { + int p = hPriorities->GetInt(reason, 0); + if (p == 0) + priorities.erase(reason); + else + priorities[reason] = p; + if (topPriority < p) + topPriority = p; + priorityChanged(reason, p); + return; + } + + Base::StateLocker lock(busy); + auto cmd = Application::Instance->commandManager().getCommandByName(reason); + if (cmd) { + auto accel = cmd->getAccel(); + if (!accel) accel = ""; + QKeySequence oldShortcut = cmd->getShortcut(); + QKeySequence newShortcut = getShortcut(reason, accel); + if (oldShortcut != newShortcut) { + cmd->setShortcut(newShortcut.toString()); + shortcutChanged(reason, oldShortcut); + } + } +} + +void ShortcutManager::reset(const char *cmd) +{ + if (cmd && cmd[0]) { + QKeySequence oldShortcut = getShortcut(cmd); + hShortcuts->RemoveASCII(cmd); + if (oldShortcut != getShortcut(cmd)) + shortcutChanged(cmd, oldShortcut); + + int oldPriority = getPriority(cmd); + hPriorities->RemoveInt(cmd); + if (oldPriority != getPriority(cmd)) + priorityChanged(cmd, oldPriority); + } +} + +void ShortcutManager::resetAll() +{ + { + Base::StateLocker lock(busy); + hShortcuts->Clear(); + hPriorities->Clear(); + for (auto cmd : Application::Instance->commandManager().getAllCommands()) { + if (cmd->getAction()) { + auto accel = cmd->getAccel(); + if (!accel) accel = ""; + cmd->setShortcut(getShortcut(nullptr, accel)); + } + } + } + shortcutChanged("", QKeySequence()); + priorityChanged("", 0); +} + +QString ShortcutManager::getShortcut(const char *cmdName, const char *accel) +{ + if (!accel) { + if (auto cmd = Application::Instance->commandManager().getCommandByName(cmdName)) { + accel = cmd->getAccel(); + if (!accel) + accel = ""; + } + } + QString shortcut; + if (cmdName) + shortcut = QString::fromLatin1(hShortcuts->GetASCII(cmdName, accel).c_str()); + else + shortcut = QString::fromLatin1(accel); + return QKeySequence(shortcut).toString(QKeySequence::NativeText); +} + +void ShortcutManager::setShortcut(const char *cmdName, const char *accel) +{ + if (cmdName && cmdName[0]) { + setTopPriority(cmdName); + if (!accel) + accel = ""; + if (auto cmd = Application::Instance->commandManager().getCommandByName(cmdName)) { + auto defaultAccel = cmd->getAccel(); + if (!defaultAccel) + defaultAccel = ""; + if (QKeySequence(QString::fromLatin1(accel)) == QKeySequence(QString::fromLatin1(defaultAccel))) { + hShortcuts->RemoveASCII(cmdName); + return; + } + } + hShortcuts->SetASCII(cmdName, accel); + } +} + +bool ShortcutManager::checkShortcut(QObject *o, const QKeySequence &key) +{ + auto focus = QApplication::focusWidget(); + if (!focus) + return false; + auto action = qobject_cast(o); + if (!action) + return false; + + const auto &index = actionMap.get<1>(); + auto iter = index.lower_bound(ActionKey(key)); + if (iter == index.end()) + return false; + + auto it = iter; + // skip to the first not exact matched key + for (; it != index.end() && key == it->key.shortcut; ++it); + + // check for potential partial match, i.e. longer key sequences + bool flush = true; + for (; it != index.end(); ++it) { + if (key.matches(it->key.shortcut) == QKeySequence::NoMatch) + break; + if (it->action && it->action->isEnabled()) { + flush = false; + break; + } + } + + int count = 0; + for (it = iter; it != index.end() && key == it->key.shortcut; ++it) { + if (it->action && it->action->isEnabled()) { + if (!flush) { + // temporary disable the action so that we can try potential + // match with further keystrokes. + it->action->setEnabled(false); + } + pendingActions.emplace_back(it->action, key.count(), getPriority(it->key.name)); + ++count; + } + } + if (!count) { + // action not found in the map, shouldn't happen! + pendingActions.emplace_back(action, key.count(), 0); + } + if (flush) { + // We'll flush now because there is no poential match with further + // keystrokes, so no need to wait for timer. + lastFocus = nullptr; + onTimer(); + return true; + } + + lastFocus = focus; + pendingSequence = key; + + // Qt's shortcut state machine favors shortest match (which is ridiculous, + // unless I'm mistaken?). We'll do longest match. We've disabled all + // shortcuts that can match the current key sequence. Now reply the sequence + // and wait for the next keystroke. + for (int i=0; itype()) { + case QEvent::KeyPress: + lastFocus = nullptr; + break; + case QEvent::Shortcut: + if (timeout > 0) { + auto sev = static_cast(ev); + if (checkShortcut(o, sev->key())) { + // shortcut event handled here, so filter out the event + return true; + } else { + // Not handled. Clear any existing pending actions. + timer.stop(); + for (const auto &info : pendingActions) { + if (info.action) + info.action->setEnabled(true); + } + pendingActions.clear(); + lastFocus = nullptr; + } + } + break; + case QEvent::ActionChanged: + if (auto action = qobject_cast(o)) { + auto &index = actionMap.get<0>(); + auto it = index.find(reinterpret_cast(action)); + if (action->shortcut().isEmpty()) { + if (it != index.end()) { + QKeySequence oldShortcut = it->key.shortcut; + index.erase(it); + actionShortcutChanged(action, oldShortcut); + } + break; + } + + QByteArray name; + if (auto fcAction = qobject_cast(action->parent())) { + if (fcAction->command() && fcAction->command()->getName()) + name = fcAction->command()->getName(); + } + if (name.isEmpty()) { + name = action->objectName().size() ? + action->objectName().toUtf8() : action->text().toUtf8(); + if (name.isEmpty()) + name = "~"; + else + name = QByteArray("~ ") + name; + } + if (it != index.end()) { + if (it->key.shortcut == action->shortcut() && it->key.name == name) + break; + QKeySequence oldShortcut = it->key.shortcut; + index.replace(it, {action, name}); + actionShortcutChanged(action, oldShortcut); + } else { + index.insert({action, name}); + actionShortcutChanged(action, QKeySequence()); + } + } + break; + default: + break; + } + return false; +} + +std::vector> ShortcutManager::getActionsByShortcut(const QKeySequence &shortcut) +{ + const auto &index = actionMap.get<1>(); + std::vector> res; + std::multimap> map; + for (auto it = index.lower_bound(ActionKey(shortcut)); it != index.end(); ++it) { + if (it->key.shortcut != shortcut) + break; + if (it->key.name != "~" && it->action) + map.emplace(getPriority(it->key.name), &(*it)); + } + for (const auto &v : map) + res.emplace_back(v.second->key.name, v.second->action); + return res; +} + +void ShortcutManager::setPriorities(const std::vector &actions) +{ + if (actions.empty()) + return; + // Keep the same top priority of the given action, and adjust the rest. Can + // go negative if necessary + int current = 0; + for (const auto &name : actions) + current = std::max(current, getPriority(name)); + if (current == 0) + current = (int)actions.size(); + setPriority(actions.front(), current); + ++current; + for (const auto &name : actions) { + int p = getPriority(name); + if (p <= 0 || p >= current) { + if (--current == 0) + --current; + setPriority(name, current); + } else + current = p; + } +} + +int ShortcutManager::getPriority(const char *cmdName) +{ + if (!cmdName) + return 0; + auto it = priorities.find(cmdName); + if (it == priorities.end()) + return 0; + return it->second; +} + +void ShortcutManager::setPriority(const char *cmdName, int p) +{ + if (p == 0) + hPriorities->RemoveInt(cmdName); + else + hPriorities->SetInt(cmdName, p); +} + +void ShortcutManager::setTopPriority(const char *cmdName) +{ + ++topPriority; + hPriorities->SetInt(cmdName, topPriority); +} + +void ShortcutManager::onTimer() +{ + QAction *found = nullptr; + int priority = -INT_MAX; + int seq_length = 0; + for (const auto &info : pendingActions) { + if (info.action) { + info.action->setEnabled(true); + if (info.seq_length > seq_length + || (info.seq_length == seq_length + && info.priority > priority)) + { + priority = info.priority; + seq_length = info.seq_length; + found = info.action; + } + } + } + if (found) + found->activate(QAction::Trigger); + pendingActions.clear(); + + if (lastFocus && lastFocus == QApplication::focusWidget()) { + // We are here because we have withheld some previous triggered action. + // We then disabled the action, and faked the same key strokes in order + // to wait for more for potential match of longer key sequence. We use + // a timer to end the wait and triggered the pending action. + // + // However, Qt's internal shorcutmap state machine is still armed with + // our fake key strokes. So we try to fake some more obscure symbol key + // stroke below, hoping to reset Qt's state machine. + + const auto &index = actionMap.get<1>(); + static const std::string symbols = "~!@#$%^&*()_+"; + QString shortcut = pendingSequence.toString() + QStringLiteral(", Ctrl+"); + for (int s : symbols) { + QKeySequence k(shortcut + QLatin1Char(s)); + auto it = index.lower_bound(ActionKey(k)); + if (it->key.shortcut != k) { + QKeyEvent *kev = new QKeyEvent(QEvent::KeyPress, s, Qt::ControlModifier, 0, 0, 0); + QApplication::postEvent(lastFocus, kev); + kev = new QKeyEvent(QEvent::KeyRelease, s, Qt::ControlModifier, 0, 0, 0); + QApplication::postEvent(lastFocus, kev); + break; + } + } + } + timer.stop(); +} + +#include "moc_ShortcutManager.cpp" diff --git a/src/Gui/ShortcutManager.h b/src/Gui/ShortcutManager.h new file mode 100644 index 0000000000..a928b811b1 --- /dev/null +++ b/src/Gui/ShortcutManager.h @@ -0,0 +1,171 @@ +/**************************************************************************** + * Copyright (c) 2022 Zheng Lei (realthunder) * + * * + * This file is part of the FreeCAD CAx development system. * + * * + * This library is free software; you can redistribute it and/or * + * modify it under the terms of the GNU Library General Public * + * License as published by the Free Software Foundation; either * + * version 2 of the License, or (at your option) any later version. * + * * + * This library is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU Library General Public License for more details. * + * * + * You should have received a copy of the GNU Library General Public * + * License along with this library; see the file COPYING.LIB. If not, * + * write to the Free Software Foundation, Inc., 59 Temple Place, * + * Suite 330, Boston, MA 02111-1307, USA * + * * + ****************************************************************************/ + +#ifndef GUI_SHORTCUT_MANAGER_H +#define GUI_SHORTCUT_MANAGER_H + +#include + +#include +#include +#include +#include + +#include +#include +#include +#include + +#include + +namespace Gui { + +class Command; +namespace bmi = boost::multi_index; + +class GuiExport ShortcutManager : public QObject, public ParameterGrp::ObserverType +{ + Q_OBJECT + +public: + ShortcutManager(); + ~ShortcutManager(); + + static ShortcutManager *instance(); + static void destroy(); + + void OnChange(Base::Subject &, const char *reason) override; + + /// Clear all user defined shortcut + void resetAll(); + /// Clear the user defined shortcut of a given command + void reset(const char *cmd); + /// Set shortcut of a given command + void setShortcut(const char *cmd, const char *accel); + /** Get shortcut of a given command + * @param cmd: command name + * @param defaultAccel: default shortcut + */ + QString getShortcut(const char *cmd, const char *defaultAccel = nullptr); + + /// Return actions having a given shortcut in order of decreasing priority + std::vector> getActionsByShortcut(const QKeySequence &shortcut); + + /// Set properties for a given list of actions in order of decreasing priority + void setPriorities(const std::vector &actions); + + /** Set the priority of a given command + * @param cmd: command name + * @param priority: priority of the command, bigger value means higher priority + */ + void setPriority(const char *cmd, int priority); + + /// Get the priority of a given command + int getPriority(const char *cmd); + + /** Set the top priority of a given command + * Make the given command the top priority of all commands. + * + * @param cmd: command name + */ + void setTopPriority(const char *cmd); + +Q_SIGNALS: + void shortcutChanged(const char *name, const QKeySequence &oldShortcut); + void actionShortcutChanged(QAction *, const QKeySequence &oldShortcut); + void priorityChanged(const char *name, int priority); + +protected: + bool eventFilter(QObject *, QEvent *ev) override; + bool checkShortcut(QObject *o, const QKeySequence &key); + void onTimer(); + +private: + ParameterGrp::handle hShortcuts; + ParameterGrp::handle hPriorities; + ParameterGrp::handle hSetting; + bool busy = false; + + struct ActionKey { + QKeySequence shortcut; + QByteArray name; + ActionKey(const QKeySequence &shortcut, const char *name = "") + : shortcut(shortcut) + , name(name) + {} + bool operator<(const ActionKey &other) const + { + if (shortcut > other.shortcut) + return false; + if (shortcut < other.shortcut) + return true; + return name < other.name; + } + }; + struct ActionData { + ActionKey key; + intptr_t pointer; + QPointer action; + + ActionData(QAction *action, const char *name = "") + : key(action->shortcut(), name) + , pointer(reinterpret_cast(action)) + , action(action) + {} + }; + bmi::multi_index_container< + ActionData, + bmi::indexed_by< + // hashed index on ActionData::Action pointer + bmi::hashed_unique>, + // ordered index on shortcut + name + bmi::ordered_non_unique> + > + > actionMap; + + std::unordered_map priorities; + int topPriority; + + struct ActionInfo { + QPointer action; + int seq_length; + int priority; + + ActionInfo(QAction *action, int l, int p) + : action(action) + , seq_length(l) + , priority(p) + {} + }; + std::vector pendingActions; + + QKeySequence pendingSequence; + + QPointer lastFocus; + + QTimer timer; + int timeout; +}; + +} + +#endif // GUI_SHORTCUT_MANAGER_H diff --git a/src/Gui/Workbench.cpp b/src/Gui/Workbench.cpp index 8a13497cb4..c72e35e264 100644 --- a/src/Gui/Workbench.cpp +++ b/src/Gui/Workbench.cpp @@ -310,6 +310,8 @@ void Workbench::setupCustomToolbars(ToolBarItem* root, const Base::ReferenceHasGroup("Shortcut")) { @@ -328,6 +330,7 @@ void Workbench::setupCustomShortcuts() const } } } +#endif } void Workbench::setupContextMenu(const char* recipient,MenuItem* item) const