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
Changes from 4 commits
da6f5f7
ae91008
4c10bb3
62fce1c
2ed5f65
12008ed
b56a169
2d50b09
2cb2bac
1d6020e
f99b031
02982d5
e89453c
866873a
9f40b81
755cd93
1c3719c
ffdb938
1a8d6fe
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||||
---|---|---|---|---|---|---|---|---|---|---|
|
@@ -53,6 +53,7 @@ | |||||||||
#include "WhatsThis.h" | ||||||||||
#include "Widgets.h" | ||||||||||
#include "Workbench.h" | ||||||||||
#include "ShortcutManager.h" | ||||||||||
|
||||||||||
|
||||||||||
using namespace Gui; | ||||||||||
|
@@ -145,6 +146,11 @@ void Action::setEnabled(bool b) | |||||||||
_action->setEnabled(b); | ||||||||||
} | ||||||||||
|
||||||||||
bool Action::isEnabled() const | ||||||||||
{ | ||||||||||
return _action->isEnabled(); | ||||||||||
} | ||||||||||
|
||||||||||
void Action::setVisible(bool b) | ||||||||||
{ | ||||||||||
_action->setVisible(b); | ||||||||||
|
@@ -153,6 +159,7 @@ void Action::setVisible(bool b) | |||||||||
void Action::setShortcut(const QString & key) | ||||||||||
{ | ||||||||||
_action->setShortcut(key); | ||||||||||
setToolTip(_tooltip, _title); | ||||||||||
} | ||||||||||
|
||||||||||
QKeySequence Action::shortcut() const | ||||||||||
|
@@ -183,21 +190,121 @@ QString Action::statusTip() const | |||||||||
void Action::setText(const QString & s) | ||||||||||
{ | ||||||||||
_action->setText(s); | ||||||||||
if (_title.isEmpty()) | ||||||||||
setToolTip(_tooltip); | ||||||||||
} | ||||||||||
|
||||||||||
QString Action::text() const | ||||||||||
{ | ||||||||||
return _action->text(); | ||||||||||
} | ||||||||||
|
||||||||||
void Action::setToolTip(const QString & s) | ||||||||||
{ | ||||||||||
_action->setToolTip(s); | ||||||||||
void Action::setToolTip(const QString & s, const QString & title) | ||||||||||
{ | ||||||||||
_tooltip = s; | ||||||||||
_title = title; | ||||||||||
_action->setToolTip(createToolTip(s, | ||||||||||
title.isEmpty() ? _action->text() : title, | ||||||||||
_action->font(), | ||||||||||
_action->shortcut().toString(QKeySequence::NativeText), | ||||||||||
this)); | ||||||||||
} | ||||||||||
|
||||||||||
QString Action::createToolTip(QString _tooltip, | ||||||||||
const QString & title, | ||||||||||
const QFont &font, | ||||||||||
const QString &sc, | ||||||||||
Action *act) | ||||||||||
{ | ||||||||||
QString text = title; | ||||||||||
text.remove(QLatin1Char('&'));; | ||||||||||
while(text.size() && text[text.size()-1].isPunct()) | ||||||||||
text.resize(text.size()-1); | ||||||||||
|
||||||||||
if (text.isEmpty()) | ||||||||||
return _tooltip; | ||||||||||
|
||||||||||
// The following code tries to make a more useful tooltip by inserting at | ||||||||||
// the beginning of the tooltip the action title in bold followed by the | ||||||||||
// shortcut. | ||||||||||
// | ||||||||||
// The long winding code is to deal with the fact that Qt will auto wrap | ||||||||||
// a rich text tooltip but the width is too short. We can escape the auto | ||||||||||
// wrappin using <p style='white-space:pre'>. | ||||||||||
|
||||||||||
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( | ||||||||||
"<p style='white-space:pre; margin-bottom:0.5em;'><b>%1</b>%2</p>").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<GroupCommand*>(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("<p style='white-space:pre; margin-top:0.5em;'><i>%1</i></p>") | ||||||||||
.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( | ||||||||||
"<p style='white-space:pre; margin:0;'>"); | ||||||||||
|
||||||||||
// If the user supplied tooltip contains line break, we shall honour it. | ||||||||||
if (_tooltip.indexOf(QLatin1Char('\n')) >= 0) | ||||||||||
tooltip += _tooltip.toHtmlEscaped() + QString::fromLatin1("</p>") ; | ||||||||||
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("</p>") ; | ||||||||||
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("</p>") | ||||||||||
+ _tooltip.right(_tooltip.size()-index).trimmed().toHtmlEscaped(); | ||||||||||
} | ||||||||||
} | ||||||||||
return tooltip + cmdName; | ||||||||||
} | ||||||||||
|
||||||||||
QString Action::toolTip() const | ||||||||||
{ | ||||||||||
return _action->toolTip(); | ||||||||||
return _tooltip; | ||||||||||
} | ||||||||||
|
||||||||||
void Action::setWhatsThis(const QString & s) | ||||||||||
|
@@ -544,7 +651,7 @@ void WorkbenchGroup::addTo(QWidget *w) | |||||||||
QToolBar* bar = qobject_cast<QToolBar*>(w); | ||||||||||
QComboBox* box = new WorkbenchComboBox(this, w); | ||||||||||
box->setIconSize(QSize(16, 16)); | ||||||||||
box->setToolTip(_action->toolTip()); | ||||||||||
box->setToolTip(_tooltip); | ||||||||||
box->setStatusTip(_action->statusTip()); | ||||||||||
box->setWhatsThis(_action->whatsThis()); | ||||||||||
box->addActions(_group->actions()); | ||||||||||
|
@@ -1243,4 +1350,206 @@ void WindowAction::addTo ( QWidget * w ) | |||||||||
} | ||||||||||
} | ||||||||||
|
||||||||||
// -------------------------------------------------------------------- | ||||||||||
|
||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since Action.cpp is already fairly big the classes below should possibly go into their own source/header files. |
||||||||||
struct CmdInfo { | ||||||||||
Command *cmd = nullptr; | ||||||||||
QString text; | ||||||||||
QString tooltip; | ||||||||||
QIcon icon; | ||||||||||
bool iconChecked = false; | ||||||||||
}; | ||||||||||
static std::vector<CmdInfo> _Commands; | ||||||||||
static int _CommandRevision; | ||||||||||
|
||||||||||
class CommandModel : public QAbstractItemModel | ||||||||||
{ | ||||||||||
public: | ||||||||||
|
||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||
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) | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||||||||||
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: | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it's usually clearer to make a constant for the |
||||||||||
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 | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||
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))); | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Prefer new-style connections. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||||||||||
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<QKeyEvent*>(ev); | ||||||||||
switch(ke->key()) { | ||||||||||
case Qt::Key_Escape: { | ||||||||||
auto edit = qobject_cast<QLineEdit*>(this->widget()); | ||||||||||
if (edit && edit->text().size()) { | ||||||||||
edit->setText(QString()); | ||||||||||
popup()->hide(); | ||||||||||
return true; | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you refactor to avoid repeating yourself here? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||||||||||
} 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()) | ||||||||||
chennes marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||
return; | ||||||||||
|
||||||||||
static_cast<CommandModel*>(this->model())->update(); | ||||||||||
|
||||||||||
this->setCompletionPrefix(txt); | ||||||||||
QRect rect = widget()->rect(); | ||||||||||
if (rect.width() < 300) | ||||||||||
rect.setWidth(300); | ||||||||||
this->complete(rect); | ||||||||||
} | ||||||||||
|
||||||||||
#include "moc_Action.cpp" |
There was a problem hiding this comment.
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 (&&).