Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Gui: introduce ShortcutManager for shortcut management and conflict resolving #6506

Closed
wants to merge 19 commits into from

Conversation

realthunder
Copy link
Collaborator

@realthunder realthunder commented Mar 4, 2022

Forum post https://forum.freecadweb.org/viewtopic.php?f=17&t=66821

Add new class ShortcutManager to unify shortcut management. Implement through global event filter to support longest key sequence match with user defined delay (configurable through 'Customize -> Keyboard -> Key sequence delay'). Also supports user defined priority to resolve shortcut conflict through 'Customize -> Keyboard'.

Refactored 'Customize -> Keyboard' page for easier command search and better synchronization.

shortcut-manager.mp4

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).
By reusing code in DlgCustomKeyboardImp
@github-actions github-actions bot added the Core Issue or PR touches core sections (App, Gui, Base) of FreeCAD label Mar 4, 2022
@luzpaz
Copy link
Contributor

luzpaz commented Mar 4, 2022

This looks awesome!

@freecadci
Copy link

pipeline status for feature branch PR_6506. Pipeline 485159467 was triggered at 62fce1c. All CI branches and pipelines.

Action *act)
{
QString text = title;
text.remove(QLatin1Char('&'));;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will remove even "escaped" ampersands (&&).

Comment on lines 1367 to 1368
public:

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
public:

case Qt::DisplayRole:
case Qt::EditRole:
if (info.text.isEmpty()) {
#if QT_VERSION>=QT_VERSION_CHECK(5,2,0)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't support older versions of Qt anymore, so this should be unnecessary.

}
return info.tooltip;

case Qt::UserRole:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's usually clearer to make a constant for the Qt::UserRole with a more descriptive name.

Comment on lines 1471 to 1473
#if QT_VERSION>=QT_VERSION_CHECK(5,2,0)
this->setFilterMode(Qt::MatchContains);
#endif
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
#if QT_VERSION>=QT_VERSION_CHECK(5,2,0)
this->setFilterMode(Qt::MatchContains);
#endif
this->setFilterMode(Qt::MatchContains);

Comment on lines 1477 to 1478
connect(lineedit, SIGNAL(textEdited(QString)), this, SLOT(onTextChanged(QString)));
connect(this, SIGNAL(activated(QModelIndex)), this, SLOT(onCommandActivated(QModelIndex)));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Prefer new-style connections.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Noted. I do use new connection now. These code were added before we fully switch to Qt5.

Comment on lines 1493 to 1494
popup()->hide();
return true;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you refactor to avoid repeating yourself here?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is subtle difference in handling of these two conditions. The ESC is supposed to clear the line edit text regardless of popup visibility. And if the edit is empty and popup is hidden, the ESC key shouldn't be filtered.

src/Gui/Action.cpp Outdated Show resolved Hide resolved
Comment on lines 283 to 287
#ifdef FC_DEBUG
// Accelerator conflict can now be dynamically resolved in ShortcutManager
//
// printConflictingAccelerators();
#endif
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
#ifdef FC_DEBUG
// Accelerator conflict can now be dynamically resolved in ShortcutManager
//
// printConflictingAccelerators();
#endif

Comment on lines 264 to 267
virtual void activated(int iMsg) override;
virtual bool isActive(void) override;
virtual Action * createAction(void) override;
virtual void languageChange() override;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

http://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines.html#c128-virtual-functions-should-specify-exactly-one-of-virtual-override-or-final

Suggested change
virtual void activated(int iMsg) override;
virtual bool isActive(void) override;
virtual Action * createAction(void) override;
virtual void languageChange() override;
void activated(int iMsg) override;
bool isActive(void) override;
Action * createAction(void) override;
void languageChange() override;

priorityList->header()->setSectionResizeMode(0, QHeaderView::ResizeToContents);
priorityList->header()->setSectionResizeMode(1, QHeaderView::ResizeToContents);

auto updatePriorityList = [priorityList](bool up) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using a boolean here feels pretty kludgy. I can live with it here since it's in such an isolated use-case, but I think it would be better to use a more explicit UP/DOWN specifier.

auto timer = new QTimer(priorityList);
timer->setSingleShot(true);
if (currentShortcut)
QObject::connect(currentShortcut, &QLineEdit::textChanged, timer, [timer](){timer->start(200);});
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How did you arrive at the 200ms for these?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to prevent refreshing priority list too often, since it gets triggered in a few signals.

Comment on lines +248 to +249
// Qt's shortcut state machine favors shortest match (which is ridiculous,
// unless I'm mistaken?). We'll do longest match. We've disabled all
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know if you're mistaken, but I agree with you that longest match should be the correct behavior.

return;
// Keep the same top priority of the given action, and adjust the rest. Can
// go negative if necessary
int current = 0;
Copy link
Member

@chennes chennes Mar 5, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If negative priorities are allowed, does it make sense to initialize current to zero?

Suggested change
int current = 0;
int current = std::numeric_limits<int>::lowest();

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suppose it can work. It's just that the priority needs to rise from some base level. If the base is the lowest, then we'd have bunch of huge negative integers in the parameters, which would be confusing.

The reason the priority is allow to go negative is because I try to preserve the top priority (among actions with the same shortcuts) while reordering. This is a partial solution to a corner case where the user may have set priority in the past for some action in a workbench that hasn't been loaded yet. Keep the top priority unchanged can provide some degree of stability in this case.

}
}
}
timer.stop();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems like this would make more sense at the top of the function, rather than the bottom.


ActionData(QAction *action, const char *name = "")
: key(action->shortcut(), name)
, pointer(reinterpret_cast<int64_t>(action))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Type name mismatch, you are using intptr_t above.

@@ -311,6 +311,8 @@ void Workbench::setupCustomToolbars(ToolBarItem* root, const Base::Reference<Par

void Workbench::setupCustomShortcuts() const
{
// Now managed by ShortcutManager
#if 0
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see any reason to leave more than just the comment here.

Copy link
Member

@chennes chennes left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall I really like the feature, and the code looks good. Just a few comments:

  1. The user interface for setting the multi-key delay makes it look like we are storing a delay per-keystroke. I don't have a great suggestion for improving it, but I don't know if we can include it as-is.
  2. I think there needs to be a timeout, after which an in-progress keystroke sequence is cancelled even if the user presses no more keys.
  3. I'm not familiar enough with the Boost multi-index containers to really evaluate, I assume the reinterpret casts are related to the usage there? Obviously those set off red flags and warning sirens when reviewing code.

@realthunder
Copy link
Collaborator Author

I think there needs to be a timeout, after which an in-progress keystroke sequence is cancelled even if the user presses no more keys.

The timer in ShortcutManager is used for this purpose. In fact, the Multi-key delay specifies the time to wait for more keystorke, after which the pending action will be triggered.

I'm not familiar enough with the Boost multi-index containers to really evaluate, I assume the reinterpret casts are related to the usage there? Obviously those set off red flags and warning sirens when reviewing code.

I intend to use QAction* as map key for easy lookup, but the action may be deleted somewhere else. I use QPointer for safe accessing, and because QPointer is not immutable, it can't be used for key. I deliberately change the key type to intptr_t to prevent accidentally using it to access the action.

Because long description may cause undesired dialog layout changes. The
description is available through tool tip of the command tree widget.
Add API Command::initAction() to force create action for all commands
with shortcut in order to register with ShortcutManager to obtain a
complete list of actions with the same shortcut.
@realthunder
Copy link
Collaborator Author

@wwmayer New commits added for suggested changes and bug fix.

@freecadci
Copy link

pipeline status for feature branch PR_6506. Pipeline 490728511 was triggered at 9f40b81. All CI branches and pipelines.

Related FreeCAD#6097

Qt ignores shortcut of actions in invisible toolbar, but not for actions
in a hidden menu action of menu bar, which is likely a Qt bug. The
desired behavior should be that of toolbar actions, so that actions
belong to different workbenches can have the same shortcut without
conflict.

This commit works around this inconsistency by ensuring only the active
actions are added in menu bar. In addition, all active actions will be
added to a zero sized child widget of the main window, which ensures the
shortcuts of these actions being active regardless whether the action is
in toolbar or menu bar, visible or not.
@freecadci
Copy link

pipeline status for feature branch PR_6506. Pipeline 492097097 was triggered at 1a8d6fe. All CI branches and pipelines.

@donovaly
Copy link
Member

donovaly commented Apr 2, 2022

We will have to postpone this to 0.21 to make 0.20 ready.

@realthunder maybe you have a fix or could review pending PRs for the 0.20 milestone?: https://github.com/FreeCAD/FreeCAD/milestone/2

@luzpaz
Copy link
Contributor

luzpaz commented Jun 17, 2022

This would be an incredible upgrade to the UI/UX. Hopefully this gets priority love

@chennes
Copy link
Member

chennes commented Sep 14, 2022

@realthunder if you are bored with working on the TNP mega-merge and want a break 😆, my impression of this PR is that it's ready for a final review once its conflicts are resolved.

@realthunder
Copy link
Collaborator Author

Great. I'll work on it soon.

@wwmayer
Copy link
Contributor

wwmayer commented Nov 10, 2022

@chennes I guess we all still agree in merging this PR? If yes, I will start to rebase it and fix the merge conflicts because this PR offers some functions that are needed in another PR.

@chennes
Copy link
Member

chennes commented Nov 10, 2022

Yes, I think this is good functionality--go for it!

@realthunder
Copy link
Collaborator Author

Sorry, forgot about this. I can rebase this tomorrow. But if @wwmayer have time, go for it sure. BTW, there is a follow up fix at here.

@wwmayer
Copy link
Contributor

wwmayer commented Nov 10, 2022

Thanks, I will start with it now...

@wwmayer
Copy link
Contributor

wwmayer commented Nov 10, 2022

#7771 replaces this PR.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Core Issue or PR touches core sections (App, Gui, Base) of FreeCAD Shortcut UI/UX
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

6 participants