Dynamic Keyboard Shortcuts
From QtCentreWiki
| Dynamic Keyboard Shortcuts by David Boddie Qt Quarterly is © Digia Plc. and/or its subsidiaries. Digia, Qt and their respective logos are trademarks of Digia Plc. All other trademarks are property of their respective owners.
|
Many modern applications allow their user interfaces to be
customized by letting the user assign new keyboard shortcuts, move
menu items around, and fine-tune the appearance of toolbars.
Using Qt's object model and action system together, we can provide
some of these customization features in a simple action editor that
can be integrated into existing Qt 3 applications.
Qt's action system is based around the QAction class, which holds
information about all the different ways the user can execute a
particular command in an application. For example, a Print menu
item can often be activated via a keyboard shortcut (Ctrl+P), a
toolbar icon, and the menu item itself.
In Qt 3, each of these types of input action can be created
separately but, by using the QAction class to collect them
together, we can save a lot of time and effort. In a
QAction-enabled application, we can easily ensure that the user
interface is always self-consistent.
Qt's introspection features, available in QObject and its
subclasses, let us search for instances of QAction in a running
application.
This means that we can access information about menu items, toolbar
icons, and shortcuts at any time, even if the QAction objects
themselves are not immediately accessible.
In this article, we create a dialog that lets the user customize the
keyboard shortcuts used in an application, and we extend one of the
standard Qt examples to take advantage of this feature. Custom
actions are loaded and saved using the QSettings class.
Ready for Action
The main feature we want to introduce is the Edit Actions dialog. We have already decided to let users edit the keyboard shortcut associated with each action, but we also need to decide which other pieces of information will also be useful to show.
Each QAction
provides a description, an icon, menu text, a shortcut, tool tip and
"What's This?" information. To edit shortcuts, we just need the
description and the shortcut, provided by the text() and
accel() functions, and we will arrange this information in a table.
The user will be able to edit each of the shortcuts in the second column before either accepting the changes with the OK button or rejecting them with the Cancel button. Since we will allow any text to be entered in each field, we also need a way to check that it can be used to specify a valid shortcut.
The class definition of the ActionsDialog looks like this:
class ActionsDialog : public QDialog { Q_OBJECT public: ActionsDialog(QObjectList *actions, QWidget *parent = 0); protected slots: void accept(); private slots: void recordAction(int row, int column); void validateAction(int row, int column); private: QString oldAccelText; QTable *actionsTable; QValueList<QAction*> actionsList; };
The recordAction() and validateAction() functions are custom slots in the dialog that are used when shortcut text is being edited by the user. The accept() function is used if the dialog is accepted, and updates all the known actions with new shortcuts.
The constructor performs the task of setting up the user interface for
the dialog. To minimize the impact on the application, the class is
constructed with a QObjectList that actually contains a list of
QActions, and we use a QTable to display information about
each of them.
ActionsDialog::ActionsDialog(QObjectList *actions, QWidget *parent) : QDialog(parent) { actionsTable = new QTable(actions->count(), 2, this); actionsTable->horizontalHeader()->setLabel(0, tr("Description")); actionsTable->horizontalHeader()->setLabel(1, tr("Shortcut")); actionsTable->verticalHeader()->hide(); actionsTable->setLeftMargin(0); actionsTable->setColumnReadOnly(0, true);
The table is customized to only allow one column to be edited, and we remove the unnecessary vertical header along the left hand side.
We rely on the application to pass valid QActions to the constructor,
but we carefully iterate over the list to prevent any accidents, setting the text for the cells in the table.
Each action that we show is also added to a list that lets us look up the action corresponding to a
given row in the table. We will use this later when modifying the actions.
QAction *action = static_cast<QAction *>(actions->first()); int row = 0; while (action) { actionsTable->setText(row, 0, action->text()); actionsTable->setText(row, 1, QString(action->accel())); actionsList.append(action); action = static_cast<QAction *>(actions->next()); ++row; }
The dialog needs OK and Cancel buttons. We construct these and connect them to the standard accept() and reject() slots:
QPushButton *okButton = new QPushButton(tr("&OK"), this); QPushButton *cancelButton = new QPushButton(tr("&Cancel"), this); connect(okButton, SIGNAL(clicked()), this, SLOT(accept())); connect(cancelButton, SIGNAL(clicked()), this, SLOT(reject()));
Two signals from the table are also connected to slots in the dialog; these handle the editing process:
connect(actionsTable, SIGNAL(currentChanged(int, int)), this, SLOT(recordAction(int, int))); connect(actionsTable, SIGNAL(valueChanged(int, int)), this, SLOT(validateAction(int, int))); ... setCaption(tr("Edit Actions")); }
After all the widgets are constructed and set up, they are arranged using layout classes in the usual way.
The Editing Process
When the user starts to edit a cell item, actionsTable emits the currentChanged() signal, and the dialog's recordAction() slot is called with the row and column of the cell:
void ActionsDialog::recordAction(int row, int col) { oldAccelText = actionsTable->item(row, col)->text(); }
Before the user gets a chance to modify the contents, we record the cell's current text. Later, if the replacement text is not suitable, we can reset it to this value.
When the user has finished editing the cell item, actionsTable
emits the valueChanged() signal, and the dialog's
validateAction() slot is called with the cell's row and column,
giving us the chance to ensure that the text in the cell is suitable
for use as a shortcut:
void ActionsDialog::validateAction(int row, int column) { QTableItem *item = actionsTable->item(row, column); QString accelText = QString(QKeySequence(item->text())); if (accelText.isEmpty() && !item->text().isEmpty()) { item->setText(oldAccelText); } else { item->setText(accelText); } }
We use a QKeySequence to check the new text on our behalf. If the new text couldn't be used by the QKeySequence, the string we obtain in accelText will be empty, so we must write the old text in oldAccelText back to the cell.
Of course, the user may have
intentionally left the field empty, so we only use the old text if
the cell's text was not empty. If the new text can be used, we write
it to the cell.
If the user clicks the OK button, it emits the clicked()
signal and the dialog's accept() slot is called:
void ActionsDialog::accept() { for (int row = 0; row < actionsList.size(); ++row) { QAction *action = actionsList[row]; action->setAccel(QKeySequence(actionsTable->text(row, 1))); } QDialog::accept(); }
Since we have already validated all the new shortcut text in the table, we simply retrieve an action for each row in the table and set its new shortcut text. Finally, we call QDialog's accept() function to close the dialog properly.
If the user closes the dialog with the Cancel button, we do not
need to perform any actions; the changes to the shortcuts will be
lost.
Taking Action
To enable support for the action editor in our application (the Qt 3 action example), we need to provide ways to open the dialog from within the user interface, load shortcut settings, and save them as required.
We add two private slots to the ApplicationWindow class to handle
editing and saving, and a private function to load shortcuts when the
application starts:
private slots: ... void editActions(); void saveActions(); private: void loadActions(); ...
The user interface is extended in the ApplicationWindow constructor with a new Settings menu for the main window's menu bar. We insert two new actions into it to allow shortcuts to be edited and saved:
ApplicationWindow::ApplicationWindow() : QMainWindow(0, "example application main window", WDestructiveClose) { ... QPopupMenu *settingsMenu = new QPopupMenu(this); menuBar()->insertItem(tr("&Settings"), settingsMenu); QAction *editActionsAction = new QAction(this); editActionsAction->setMenuText(tr("&Edit Actions...")); editActionsAction->setText(tr("Edit Actions")); connect(editActionsAction, SIGNAL(activated()), this, SLOT(editActions())); editActionsAction->addTo(settingsMenu); QAction *saveActionsAction = new QAction(this); saveActionsAction->setMenuText(tr("&Save Actions")); saveActionsAction->setText(tr("Save Actions")); connect(saveActionsAction, SIGNAL(activated()), this, SLOT(saveActions())); saveActionsAction->addTo(settingsMenu); ...
We also include the following code at the end of the constructor after all the actions have been set up. This lets us override the default shortcuts that are hard-coded into the application:
...
loadActions();
...
}The loadActions() function modifies the shortcut of each QAction whose name matches an entry in the settings:
void ApplicationWindow::loadActions() { QSettings settings; settings.setPath("trolltech.com", "Action"); settings.beginGroup("/Action"); QObjectList *actions = queryList("QAction"); QAction *action = static_cast<QAction *>(actions->first()); while (action) { QString accelText = settings.readEntry(action->text()); if (!accelText.isEmpty()) action->setAccel(QKeySequence(accelText)); action = static_cast<QAction *>(actions->next()); } }
This function relies on the default values from QSettings::readEntry() to ensure that each action is only changed if there is a suitable entry with a valid shortcut available.
The private editActions() slot performs the task of opening the
dialog with a list of all the main window's actions:
void ApplicationWindow::editActions() { ActionsDialog actionsDialog(queryList("QAction"), this); actionsDialog.exec(); }
The QObjectList returned by queryList() contains all the QActions in the main window, including the new ones we added to the menu bar.
The saveActions() slot is called whenever the Save Actions menu
item is activated:
void ApplicationWindow::saveActions() { QSettings settings; settings.setPath("trolltech.com", "Action"); settings.beginGroup("/Action"); QObjectList *actions = queryList("QAction"); QAction *action = static_cast<QAction *>(actions->first()); while (action) { QString accelText = QString(action->accel()); settings.writeEntry(action->text(), accelText); action = static_cast<QAction *>(actions->next()); } }
Note that we save information about all shortcuts in the application, even if they are undefined. This allows the user to disable custom shortcuts.
With these modifications in place, we can rebuild and run the action example to see the result. All the shortcuts used in the main window's menus can now be changed to match our personal favorites.
Possible Improvements
The example we have given can be extended in a number of ways to make it more useful in real-world applications:
- The dialog only displays the actions for the main window, but we could include other actions in the application. Perhaps we could even look for groups of actions.
- Shortcuts could be set with an editor that records keystrokes made by the user.
- When a shortcut is cleared in the dialog, the default shortcut for that action becomes available again the next time the application is run. You can use a single space character for that shortcut to work around this, but there are better solutions.





