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
Closed
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
319 changes: 314 additions & 5 deletions src/Gui/Action.cpp
Expand Up @@ -53,6 +53,7 @@
#include "WhatsThis.h"
#include "Widgets.h"
#include "Workbench.h"
#include "ShortcutManager.h"


using namespace Gui;
Expand Down Expand Up @@ -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);
Expand All @@ -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
Expand Down Expand Up @@ -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('&'));;
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 (&&).

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)
Expand Down Expand Up @@ -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());
Expand Down Expand Up @@ -1243,4 +1350,206 @@ void WindowAction::addTo ( QWidget * w )
}
}

// --------------------------------------------------------------------

Copy link
Contributor

Choose a reason for hiding this comment

The 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:

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:

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)
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.

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:
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.

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
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);

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)));
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.

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;
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.

} 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"