Allow for multiple accelerator alternatives

This needs some testing before it could go to 2.4.x eventually
This commit is contained in:
Juergen Spitzmueller 2024-04-04 17:12:48 +02:00
parent 6260689fd5
commit aa7ff14933
3 changed files with 106 additions and 30 deletions

View File

@ -64,6 +64,16 @@ These chars should be somehow used in your translations, however you'll have to
invent your own working shortcuts for dialog and menu entries and resolve invent your own working shortcuts for dialog and menu entries and resolve
possible conflicts of the same shortcut chars in one menu... possible conflicts of the same shortcut chars in one menu...
You will be informed about conflicts in the terminal if you try to access the
menu.
Note that, in the case of '|', if more than one character follows, this means
that LyX will try each of them in turn and use the first one that is not yet
used by another entry in the menu. That way, you can define alternative shortcuts
in the case one works in one context only, and another one only in another. You
can use this possibility also in translations, but please use it only if no
single shortcut that fits could be found.
Note also that there are already used global shortcuts (such as p k x c m s a) Note also that there are already used global shortcuts (such as p k x c m s a)
and you should avoid using these characters for first-level menu shortcuts. and you should avoid using these characters for first-level menu shortcuts.

View File

@ -650,7 +650,7 @@ Menuset
Menu "context-edit-index" Menu "context-edit-index"
OptItem "Insert Subentry|n" "indexmacro-insert subentry" OptItem "Insert Subentry|n" "indexmacro-insert subentry"
OptItem "Insert Sortkey|k" "indexmacro-insert sortkey" OptItem "Insert Sortkey|k" "indexmacro-insert sortkey"
OptItem "Insert See Reference|c" "indexmacro-insert see" OptItem "Insert See Reference|cf" "indexmacro-insert see"
OptItem "Insert See also Reference|a" "indexmacro-insert seealso" OptItem "Insert See also Reference|a" "indexmacro-insert seealso"
End End

View File

@ -233,10 +233,19 @@ public:
} }
/// The keyboard shortcut (usually underlined in the entry) /// The keyboard shortcut (usually underlined in the entry)
QString shortcut() const /// If \p first is true, return only the first character
/// if a multi-character string has been defined.
QString shortcut(bool first = false) const
{ {
int const index = label_.lastIndexOf('|'); int const index = label_.lastIndexOf('|');
return index == -1 ? QString() : label_.mid(index + 1); if (index == -1)
return QString();
QString accelerators = label_.mid(index + 1);
if (accelerators.size() == 1)
return accelerators;
if (first)
return accelerators.left(1);
return accelerators;
} }
/// The complete label, with label and shortcut separated by a '|' /// The complete label, with label and shortcut separated by a '|'
QString fulllabel() const { return label_; } QString fulllabel() const { return label_; }
@ -349,8 +358,12 @@ public:
/// Checks the associated FuncRequest status before adding the /// Checks the associated FuncRequest status before adding the
/// menu item. /// menu item.
void addWithStatusCheck(MenuItem const &); void addWithStatusCheck(MenuItem const &);
// Check whether the menu shortcuts are unique /// Check whether the shortcut of \p mi are unique and valid, and report if not
void checkShortcuts() const; void checkShortcutUnique(MenuItem const & mi) const;
/// Return true if a \p sc is a unique shortcut
bool checkShortcut(QString const sc) const;
/// Try to find a unique shortcut from a string of alternatives
QString getBestShortcut(MenuItem const & mi) const;
/// ///
void expandLastfiles(); void expandLastfiles();
void expandDocuments(); void expandDocuments();
@ -760,28 +773,84 @@ void MenuDefinition::cat(MenuDefinition const & other)
} }
void MenuDefinition::checkShortcuts() const QString MenuDefinition::getBestShortcut(MenuItem const & mi) const
{ {
// This is a quadratic algorithm, but we do not care because // This might be a string of accelerators, a single accelerator
// menus are short enough // or empty
for (const_iterator it1 = begin(); it1 != end(); ++it1) { QString accelerators = mi.shortcut();
QString shortcut = it1->shortcut(); QString const label = mi.label();
if (shortcut.isEmpty()) if (accelerators.size() == 0)
continue; return QString();
if (!it1->label().contains(shortcut)) if (accelerators.size() == 1) {
// check and report clashes
checkShortcutUnique(mi);
return accelerators;
}
for (int i = 0; i < accelerators.length(); i++)
{
// check each character in the string
// and use the first that does not conflict
QString const sc = accelerators.at(i);
if (!label.contains(sc)) {
// invalid shortcut. Report.
LYXERR0("Menu warning: menu entry \"" LYXERR0("Menu warning: menu entry \""
<< it1->label() << label
<< "\" does not contain shortcut `"
<< sc << "'.");
continue;
}
if (checkShortcut(sc))
return sc;
}
// At this point, we found no non-conflicting one
// issue a warning and omit the accelerator
LYXERR0("Menu warning: All accelerators of menu entry "
<< '"' << mi.fulllabel()
<< "\" are already taken. Omitting shortcut.");
return QString();
}
void MenuDefinition::checkShortcutUnique(MenuItem const & mi) const
{
// We know this might only be a single character or nothing
QString const shortcut = mi.shortcut();
QString const label = mi.label();
if (shortcut.isEmpty())
return;
if (!label.contains(shortcut))
// invalid shortcut
LYXERR0("Menu warning: menu entry \""
<< label
<< "\" does not contain shortcut `" << "\" does not contain shortcut `"
<< shortcut << "'."); << shortcut << "'.");
for (const_iterator it2 = begin(); it2 != it1 ; ++it2) { for (const_iterator it = begin(); it != end() ; ++it) {
if (!it2->shortcut().compare(shortcut, Qt::CaseInsensitive)) { if (mi.fulllabel() == it->fulllabel())
// do not compare with itself
continue;
if (!it->shortcut().compare(shortcut, Qt::CaseInsensitive)) {
// conflict
LYXERR0("Menu warning: menu entries " LYXERR0("Menu warning: menu entries "
<< '"' << it1->fulllabel() << '"' << it->fulllabel()
<< "\" and \"" << it2->fulllabel() << "\" and \"" << mi.fulllabel()
<< "\" share the same shortcut."); << "\" share the same shortcut.");
} }
} }
}
bool MenuDefinition::checkShortcut(QString const shortcut) const
{
if (shortcut.isEmpty())
return true;
for (const_iterator it = begin(); it != end(); ++it) {
if (it->shortcut(true).compare(shortcut, Qt::CaseInsensitive) == 0) {
// conflict
return false;
} }
}
// no conflict
return true;
} }
@ -2116,12 +2185,12 @@ struct Menu::Impl
/// Get a MenuDefinition item label from the menu backend /// Get a MenuDefinition item label from the menu backend
static QString label(MenuItem const & mi) static QString label(MenuItem const & mi, MenuDefinition const & menu)
{ {
QString label = mi.label(); QString label = mi.label();
label.replace("&", "&&"); label.replace("&", "&&");
QString shortcut = mi.shortcut(); QString shortcut = menu.getBestShortcut(mi);
if (!shortcut.isEmpty()) { if (!shortcut.isEmpty()) {
int pos = label.indexOf(shortcut); int pos = label.indexOf(shortcut);
if (pos != -1) if (pos != -1)
@ -2158,7 +2227,7 @@ void Menu::Impl::populate(QMenu * qMenu, MenuDefinition const & menu)
qMenu->addSeparator(); qMenu->addSeparator();
break; break;
case MenuItem::Submenu: { case MenuItem::Submenu: {
QMenu * subMenu = qMenu->addMenu(label(m)); QMenu * subMenu = qMenu->addMenu(label(m, menu));
populate(subMenu, m.submenu()); populate(subMenu, m.submenu());
subMenu->setEnabled(!subMenu->isEmpty()); subMenu->setEnabled(!subMenu->isEmpty());
break; break;
@ -2168,7 +2237,7 @@ void Menu::Impl::populate(QMenu * qMenu, MenuDefinition const & menu)
// FIXME: A previous comment assured that MenuItem::Command was the // FIXME: A previous comment assured that MenuItem::Command was the
// only possible case in practice, but this is wrong. It would be // only possible case in practice, but this is wrong. It would be
// good to document which cases are actually treated here. // good to document which cases are actually treated here.
qMenu->addAction(new Action(m.func(), QIcon(), label(m), qMenu->addAction(new Action(m.func(), QIcon(), label(m, menu),
m.tooltip(), qMenu)); m.tooltip(), qMenu));
break; break;
} }
@ -2529,9 +2598,6 @@ void Menus::Impl::expand(MenuDefinition const & frommenu,
// we do not want the menu to end with a separator // we do not want the menu to end with a separator
if (!tomenu.empty() && tomenu.items_.back().kind() == MenuItem::Separator) if (!tomenu.empty() && tomenu.items_.back().kind() == MenuItem::Separator)
tomenu.items_.pop_back(); tomenu.items_.pop_back();
// Check whether the shortcuts are unique
tomenu.checkShortcuts();
} }
@ -2700,7 +2766,7 @@ void Menus::fillMenuBar(QMenuBar * qmb, GuiView * view, bool initial)
} }
Menu * menuptr = new Menu(view, m.submenuname(), true); Menu * menuptr = new Menu(view, m.submenuname(), true);
menuptr->setTitle(label(m)); menuptr->setTitle(label(m, menu));
#if defined(Q_OS_MAC) #if defined(Q_OS_MAC)
// On Mac OS with Qt/Cocoa, the menu is not displayed if there is no action // On Mac OS with Qt/Cocoa, the menu is not displayed if there is no action