Dynamic Keyboard Shortcuts

Материал из Wiki.crossplatform.ru

Перейти к: навигация, поиск
Image:qt-logo_new.png Image:qq-title-article.png
Qt Quarterly | Выпуск 14 | Документация


by David Boddie

Many modern applications allow their user interfaces to becustomized by letting the user assign new keyboard shortcuts, movemenu items around, and fine-tune the appearance of toolbars.Using Qt's object model and action system together, we can providesome of these customization features in a simple action editor thatcan be integrated into existing Qt 3 applications.

Содержание

[Download Source Code]


Qt's action system is based around the QAction class, which holdsinformation about all the different ways the user can execute aparticular command in an application. For example, a Print menuitem can often be activated via a keyboard shortcut (Ctrl+P), atoolbar icon, and the menu item itself.

In Qt 3, each of these types of input action can be createdseparately but, by using the QAction class to collect themtogether, we can save a lot of time and effort. In a QAction-enabled application, we can easily ensure that the userinterface is always self-consistent.

Qt's introspection features, available in QObject and itssubclasses, let us search for instances of QAction in a runningapplication.This means that we can access information about menu items, toolbaricons, and shortcuts at any time, even if the QAction objectsthemselves are not immediately accessible.

In this article, we create a dialog that lets the user customize thekeyboard shortcuts used in an application, and we extend one of thestandard Qt examples to take advantage of this feature. Customactions 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 shortcutassociated with each action, but we also need to decide which otherpieces of information will also be useful to show.

Each QActionprovides a description, an icon, menu text, a shortcut, tool tip and"What's This?" information. To edit shortcuts, we just need thedescription and the shortcut, provided by the text() andaccel() functions, and we will arrange this information in a table.

center

The user will be able to edit each of the shortcuts in the second columnbefore either accepting the changes with the OK button or rejectingthem with the Cancel button. Since we will allow any text to beentered in each field, we also need a way to check that it can be usedto 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 customslots in the dialog that are used when shortcut text is being editedby the user. The accept() function is used if the dialog isaccepted, and updates all the known actions with new shortcuts.

The constructor performs the task of setting up the user interface forthe dialog. To minimize the impact on the application, the class isconstructed with a QObjectList that actually contains a list of QActions, and we use a QTable to display information abouteach 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 weremove 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, settingthe text for the cells in the table.

Each action that we show is alsoadded to a list that lets us look up the action corresponding to agiven 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 andconnect them to the standard accept() and reject() slots:

    QPushButton *okButton = new QPushButton(tr("&amp;OK"), this);
    QPushButton *cancelButton = new QPushButton(tr("&amp;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 arrangedusing layout classes in the usual way.

[править] The Editing Process

When the user starts to edit a cell item, actionsTable emits thecurrentChanged() signal, and the dialog's recordAction() slotis 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 thecell'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, actionsTableemits the valueChanged() signal, and the dialog'svalidateAction() slot is called with the cell's row and column,giving us the chance to ensure that the text in the cell is suitablefor 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() &amp;&amp; !item->text().isEmpty()) {
        item->setText(oldAccelText);
    } else {
        item->setText(accelText);
    }
}

We use a QKeySequence to check the new text on our behalf. If thenew text couldn't be used by the QKeySequence, the string weobtain in accelText will be empty, so we must write the old textin oldAccelText back to the cell.

Of course, the user may haveintentionally left the field empty, so we only use the old text ifthe cell's text was not empty. If the new text can be used, we writeit 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 itsnew shortcut text. Finally, we call QDialog's accept() functionto close the dialog properly.

If the user closes the dialog with the Cancel button, we do notneed to perform any actions; the changes to the shortcuts will belost.

[править] Taking Action

To enable support for the action editor in our application (the Qt 3action example), we need to provide ways to open the dialog fromwithin the user interface, load shortcut settings, and save them asrequired.

We add two private slots to the ApplicationWindow class to handleediting and saving, and a private function to load shortcuts when theapplication starts:

private slots:
    ...
    void editActions();
    void saveActions();
 
private:
    void loadActions();
    ...

The user interface is extended in the ApplicationWindow constructor with anew Settings menu for the main window's menu bar. We inserttwo 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("&amp;Settings"), settingsMenu);
 
    QAction *editActionsAction = new QAction(this);
    editActionsAction->setMenuText(tr(
                              "&amp;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("&amp;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 afterall the actions have been set up. This lets us override the defaultshortcuts that are hard-coded into the application:

  ...
    loadActions();
    ...
}

The loadActions() function modifies the shortcut of each QActionwhose 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 entrywith a valid shortcut available.

The private editActions() slot performs the task of opening thedialog 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 addedto the menu bar.

The saveActions() slot is called whenever the Save Actions menuitem 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 ifthey are undefined. This allows the user to disable custom shortcuts.

center

With these modifications in place, we can rebuild and run the actionexample to see the result. All the shortcuts used in the main window'smenus 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 itmore useful in real-world applications:

  • The dialog only displays the actions for the main window, but wecould include other actions in the application. Perhaps we could even lookfor groups of actions.
  • Shortcuts could be set with an editor that records keystrokes made bythe user.
  • When a shortcut is cleared in the dialog, the default shortcut for thataction becomes available again the next time the application is run. Youcan use a single space character for that shortcut to work around this,but there are better solutions.