From f8275c3b697f1ee43d93bb4e5e688e87ca0405ce Mon Sep 17 00:00:00 2001 From: Manuel Nickschas Date: Mon, 31 May 2010 19:28:25 +0200 Subject: [PATCH] Allow configuration of shortcuts for platforms other than KDE This adds a shortcuts configuration dialog for Quassel without KDE integration. --- src/common/util.cpp | 18 +- src/common/util.h | 5 +- src/qtui/mainwin.cpp | 15 +- src/qtui/mainwin.h | 3 +- src/qtui/settingspages/keysequencewidget.cpp | 376 ++++++++++++++++++ src/qtui/settingspages/keysequencewidget.h | 107 +++++ src/qtui/settingspages/settingspages.inc | 6 + src/qtui/settingspages/shortcutsmodel.cpp | 232 +++++++++++ src/qtui/settingspages/shortcutsmodel.h | 96 +++++ .../settingspages/shortcutssettingspage.cpp | 154 +++++++ .../settingspages/shortcutssettingspage.h | 72 ++++ .../settingspages/shortcutssettingspage.ui | 123 ++++++ 12 files changed, 1201 insertions(+), 6 deletions(-) create mode 100644 src/qtui/settingspages/keysequencewidget.cpp create mode 100644 src/qtui/settingspages/keysequencewidget.h create mode 100644 src/qtui/settingspages/shortcutsmodel.cpp create mode 100644 src/qtui/settingspages/shortcutsmodel.h create mode 100644 src/qtui/settingspages/shortcutssettingspage.cpp create mode 100644 src/qtui/settingspages/shortcutssettingspage.h create mode 100644 src/qtui/settingspages/shortcutssettingspage.ui diff --git a/src/common/util.cpp b/src/common/util.cpp index 8b9231e1..14435bde 100644 --- a/src/common/util.cpp +++ b/src/common/util.cpp @@ -1,5 +1,5 @@ /*************************************************************************** - * Copyright (C) 2005-09 by the Quassel Project * + * Copyright (C) 2005-2010 by the Quassel Project * * devel@quassel-irc.org * * * * This program is free software; you can redistribute it and/or modify * @@ -60,6 +60,22 @@ QString stripFormatCodes(QString str) { return str; } +QString stripAcceleratorMarkers(const QString &label_) { + QString label = label_; + int p = 0; + forever { + p = label.indexOf('&', p); + if(p < 0 || p + 1 >= label.length()) + break; + + if(label.at(p + 1).isLetterOrNumber() || label.at(p + 1) == '&') + label.remove(p, 1); + + ++p; + } + return label; +} + QString decodeString(const QByteArray &input, QTextCodec *codec) { // First, we check if it's utf8. It is very improbable to encounter a string that looks like // valid utf8, but in fact is not. This means that if the input string passes as valid utf8, it diff --git a/src/common/util.h b/src/common/util.h index 0e205fd8..aee3499e 100644 --- a/src/common/util.h +++ b/src/common/util.h @@ -1,5 +1,5 @@ /*************************************************************************** - * Copyright (C) 2005/06 by the Quassel Project * + * Copyright (C) 2005-2010 by the Quassel Project * * devel@quassel-irc.org * * * * This program is free software; you can redistribute it and/or modify * @@ -36,6 +36,9 @@ bool isChannelName(QString str); //! Strip mIRC format codes QString stripFormatCodes(QString); +//! Remove accelerator markers (&) from the string +QString stripAcceleratorMarkers(const QString &); + QString secondsToString(int timeInSeconds); //! Take a string and decode it using the specified text codec, recognizing utf8. diff --git a/src/qtui/mainwin.cpp b/src/qtui/mainwin.cpp index 598befd0..878168ee 100644 --- a/src/qtui/mainwin.cpp +++ b/src/qtui/mainwin.cpp @@ -120,6 +120,10 @@ #include "settingspages/notificationssettingspage.h" #include "settingspages/topicwidgetsettingspage.h" +#ifndef HAVE_KDE +# include "settingspages/shortcutssettingspage.h" +#endif + MainWin::MainWin(QWidget *parent) #ifdef HAVE_KDE : KMainWindow(parent), @@ -339,6 +343,8 @@ void MainWin::setupActions() { 0, 0))->setCheckable(true); // Settings + coll->addAction("ConfigureShortcuts", new Action(SmallIcon("configure-shortcuts"), tr("Configure &Shortcuts..."), coll, + this, SLOT(showShortcutsDlg()))); coll->addAction("ConfigureQuassel", new Action(SmallIcon("configure"), tr("&Configure Quassel..."), coll, this, SLOT(showSettingsDlg()), QKeySequence(Qt::Key_F7))); @@ -408,6 +414,8 @@ void MainWin::setupMenus() { #ifdef HAVE_KDE _settingsMenu->addAction(KStandardAction::configureNotifications(this, SLOT(showNotificationsDlg()), this)); _settingsMenu->addAction(KStandardAction::keyBindings(this, SLOT(showShortcutsDlg()), this)); +#else + _settingsMenu->addAction(coll->action("ConfigureShortcuts")); #endif _settingsMenu->addAction(coll->action("ConfigureQuassel")); @@ -974,11 +982,14 @@ void MainWin::showAboutDlg() { AboutDlg(this).exec(); } -#ifdef HAVE_KDE void MainWin::showShortcutsDlg() { +#ifdef HAVE_KDE KShortcutsDialog::configure(QtUi::actionCollection("General"), KShortcutsEditor::LetterShortcutsDisallowed); -} +#else + SettingsPageDlg dlg(new ShortcutsSettingsPage(QtUi::actionCollections(), this), this); + dlg.exec(); #endif +} /********************************************************************************************************/ diff --git a/src/qtui/mainwin.h b/src/qtui/mainwin.h index 53548bfe..f9130bd5 100644 --- a/src/qtui/mainwin.h +++ b/src/qtui/mainwin.h @@ -114,9 +114,8 @@ class MainWin void showSettingsDlg(); void showNotificationsDlg(); void showIgnoreList(QString newRule = QString()); -#ifdef HAVE_KDE void showShortcutsDlg(); -#endif + void handleCoreConnectionError(const QString &errorMsg); void userAuthenticationRequired(CoreAccount *, bool *valid, const QString &errorMessage); void handleNoSslInClient(bool *accepted); diff --git a/src/qtui/settingspages/keysequencewidget.cpp b/src/qtui/settingspages/keysequencewidget.cpp new file mode 100644 index 00000000..4673a185 --- /dev/null +++ b/src/qtui/settingspages/keysequencewidget.cpp @@ -0,0 +1,376 @@ +/*************************************************************************** + * Copyright (C) 2010 by the Quassel Project * + * devel@quassel-irc.org * + * * + * This class has been inspired by KDE's KKeySequenceWidget and uses * + * some code snippets of its implementation, part of kdelibs. * + * The original file is * + * Copyright (C) 1998 Mark Donohoe * + * Copyright (C) 2001 Ellis Whitehead * + * Copyright (C) 2007 Andreas Hartmetz * + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License for more details. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program; if not, write to the * + * Free Software Foundation, Inc., * + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * + ***************************************************************************/ + +#include +#include +#include +#include +#include +#include + +#include "action.h" +#include "actioncollection.h" +#include "iconloader.h" +#include "keysequencewidget.h" + +KeySequenceButton::KeySequenceButton(KeySequenceWidget *d_, QWidget *parent) + : QPushButton(parent), + d(d_) +{ + +} + +bool KeySequenceButton::event(QEvent *e) { + if(d->isRecording() && e->type() == QEvent::KeyPress) { + keyPressEvent(static_cast(e)); + return true; + } + + // The shortcut 'alt+c' ( or any other dialog local action shortcut ) + // ended the recording and triggered the action associated with the + // action. In case of 'alt+c' ending the dialog. It seems that those + // ShortcutOverride events get sent even if grabKeyboard() is active. + if(d->isRecording() && e->type() == QEvent::ShortcutOverride) { + e->accept(); + return true; + } + + return QPushButton::event(e); +} + +void KeySequenceButton::keyPressEvent(QKeyEvent *e) { + int keyQt = e->key(); + if(keyQt == -1) { + // Qt sometimes returns garbage keycodes, I observed -1, if it doesn't know a key. + // We cannot do anything useful with those (several keys have -1, indistinguishable) + // and QKeySequence.toString() will also yield a garbage string. + QMessageBox::information(this, + tr("The key you just pressed is not supported by Qt."), + tr("Unsupported Key")); + return d->cancelRecording(); + } + + uint newModifiers = e->modifiers() & (Qt::SHIFT | Qt::CTRL | Qt::ALT | Qt::META); + + //don't have the return or space key appear as first key of the sequence when they + //were pressed to start editing - catch and them and imitate their effect + if(!d->isRecording() && ((keyQt == Qt::Key_Return || keyQt == Qt::Key_Space))) { + d->startRecording(); + d->_modifierKeys = newModifiers; + d->updateShortcutDisplay(); + return; + } + + // We get events even if recording isn't active. + if(!d->isRecording()) + return QPushButton::keyPressEvent(e); + + e->accept(); + d->_modifierKeys = newModifiers; + + switch(keyQt) { + case Qt::Key_AltGr: //or else we get unicode salad + return; + case Qt::Key_Shift: + case Qt::Key_Control: + case Qt::Key_Alt: + case Qt::Key_Meta: + case Qt::Key_Menu: //unused (yes, but why?) + d->updateShortcutDisplay(); + break; + + default: + if(!(d->_modifierKeys & ~Qt::SHIFT)) { + // It's the first key and no modifier pressed. Check if this is + // allowed + if(!d->isOkWhenModifierless(keyQt)) + return; + } + + // We now have a valid key press. + if(keyQt) { + if((keyQt == Qt::Key_Backtab) && (d->_modifierKeys & Qt::SHIFT)) { + keyQt = Qt::Key_Tab | d->_modifierKeys; + } + else if(d->isShiftAsModifierAllowed(keyQt)) { + keyQt |= d->_modifierKeys; + } else + keyQt |= (d->_modifierKeys & ~Qt::SHIFT); + + d->_keySequence = QKeySequence(keyQt); + d->doneRecording(); + } + } +} + +void KeySequenceButton::keyReleaseEvent(QKeyEvent *e) { + if(e->key() == -1) { + // ignore garbage, see keyPressEvent() + return; + } + + if(!d->isRecording()) + return QPushButton::keyReleaseEvent(e); + + e->accept(); + + uint newModifiers = e->modifiers() & (Qt::SHIFT | Qt::CTRL | Qt::ALT | Qt::META); + + // if a modifier that belongs to the shortcut was released... + if((newModifiers & d->_modifierKeys) < d->_modifierKeys) { + d->_modifierKeys = newModifiers; + d->updateShortcutDisplay(); + } +} + +/******************************************************************************/ + +KeySequenceWidget::KeySequenceWidget(QWidget *parent) + : QWidget(parent), + _shortcutsModel(0), + _isRecording(false), + _modifierKeys(0) +{ + QHBoxLayout *layout = new QHBoxLayout(this); + layout->setMargin(0); + + _keyButton = new KeySequenceButton(this, this); + _keyButton->setFocusPolicy(Qt::StrongFocus); + _keyButton->setIcon(SmallIcon("configure")); + _keyButton->setToolTip(tr("Click on the button, then enter the shortcut like you would in the program.\nExample for Ctrl+a: hold the Ctrl key and press a.")); + layout->addWidget(_keyButton); + + _clearButton = new QToolButton(this); + layout->addWidget(_clearButton); + + if(qApp->isLeftToRight()) + _clearButton->setIcon(SmallIcon("edit-clear-locationbar-rtl")); + else + _clearButton->setIcon(SmallIcon("edit-clear-locationbar-ltr")); + + setLayout(layout); + + connect(_keyButton, SIGNAL(clicked()), SLOT(startRecording())); + connect(_keyButton, SIGNAL(clicked()), SIGNAL(clicked())); + connect(_clearButton, SIGNAL(clicked()), SLOT(clear())); + connect(_clearButton, SIGNAL(clicked()), SIGNAL(clicked())); +} + +void KeySequenceWidget::setModel(ShortcutsModel *model) { + Q_ASSERT(!_shortcutsModel); + _shortcutsModel = model; +} + +bool KeySequenceWidget::isOkWhenModifierless(int keyQt) const { + //this whole function is a hack, but especially the first line of code + if(QKeySequence(keyQt).toString().length() == 1) + return false; + + switch(keyQt) { + case Qt::Key_Return: + case Qt::Key_Space: + case Qt::Key_Tab: + case Qt::Key_Backtab: //does this ever happen? + case Qt::Key_Backspace: + case Qt::Key_Delete: + return false; + default: + return true; + } +} + +bool KeySequenceWidget::isShiftAsModifierAllowed(int keyQt) const { + // Shift only works as a modifier with certain keys. It's not possible + // to enter the SHIFT+5 key sequence for me because this is handled as + // '%' by qt on my keyboard. + // The working keys are all hardcoded here :-( + if(keyQt >= Qt::Key_F1 && keyQt <= Qt::Key_F35) + return true; + + if(QChar(keyQt).isLetter()) + return true; + + switch(keyQt) { + case Qt::Key_Return: + case Qt::Key_Space: + case Qt::Key_Backspace: + case Qt::Key_Escape: + case Qt::Key_Print: + case Qt::Key_ScrollLock: + case Qt::Key_Pause: + case Qt::Key_PageUp: + case Qt::Key_PageDown: + case Qt::Key_Insert: + case Qt::Key_Delete: + case Qt::Key_Home: + case Qt::Key_End: + case Qt::Key_Up: + case Qt::Key_Down: + case Qt::Key_Left: + case Qt::Key_Right: + return true; + + default: + return false; + } +} + +void KeySequenceWidget::updateShortcutDisplay() { + // make translators happy + static QString metaKey = tr("Meta", "Meta key"); + static QString altKey = tr("Alt", "Alt key"); + static QString ctrlKey = tr("Ctrl", "Ctrl key"); + static QString shiftKey = tr("Shift", "Shift key"); + + QString s = _keySequence.toString(QKeySequence::NativeText); + s.replace('&', QLatin1String("&&")); + + if(_isRecording) { + if(_modifierKeys) { + if(_modifierKeys & Qt::META) s += metaKey + '+'; +#if defined(Q_WS_MAC) + if(_modifierKeys & Qt::ALT) s += altKey + '+'; + if(_modifierKeys & Qt::CTRL) s += ctrlKey + '+'; +#elif defined(Q_WS_X11) + if(_modifierKeys & Qt::CTRL) s += ctrlKey + '+'; + if(_modifierKeys & Qt::ALT) s += altKey + '+'; +#endif + if(_modifierKeys & Qt::SHIFT) s += shiftKey + '+'; + + } else { + s = tr("Input", "What the user inputs now will be taken as the new shortcut"); + } + // make it clear that input is still going on + s.append(" ..."); + } + + if(s.isEmpty()) { + s = tr("None", "No shortcut defined"); + } + + s.prepend(' '); + s.append(' '); + _keyButton->setText(s); +} + +void KeySequenceWidget::startRecording() { + _modifierKeys = 0; + _oldKeySequence = _keySequence; + _keySequence = QKeySequence(); + _conflictingIndex = QModelIndex(); + _isRecording = true; + _keyButton->grabKeyboard(); + + if(!QWidget::keyboardGrabber()) { + qWarning() << "Failed to grab the keyboard! Most likely qt's nograb option is active"; + } + + _keyButton->setDown(true); + updateShortcutDisplay(); +} + + +void KeySequenceWidget::doneRecording() { + bool wasRecording = _isRecording; + _isRecording = false; + _keyButton->releaseKeyboard(); + _keyButton->setDown(false); + + if(!wasRecording || _keySequence == _oldKeySequence) { + // The sequence hasn't changed + updateShortcutDisplay(); + return; + } + + if(!isKeySequenceAvailable(_keySequence)) { + _keySequence = _oldKeySequence; + } else if(wasRecording) { + emit keySequenceChanged(_keySequence, _conflictingIndex); + } + updateShortcutDisplay(); +} + +void KeySequenceWidget::cancelRecording() { + _keySequence = _oldKeySequence; + doneRecording(); +} + +void KeySequenceWidget::setKeySequence(const QKeySequence &seq) { + // oldKeySequence holds the key sequence before recording started, if setKeySequence() + // is called while not recording then set oldKeySequence to the existing sequence so + // that the keySequenceChanged() signal is emitted if the new and previous key + // sequences are different + if(!isRecording()) + _oldKeySequence = _keySequence; + + _keySequence = seq; + _clearButton->setVisible(!_keySequence.isEmpty()); + doneRecording(); +} + +void KeySequenceWidget::clear() { + setKeySequence(QKeySequence()); + // setKeySequence() won't emit a signal when we're not recording + emit keySequenceChanged(QKeySequence()); +} + +bool KeySequenceWidget::isKeySequenceAvailable(const QKeySequence &seq) { + if(seq.isEmpty()) + return true; + + // We need to access the root model, not the filtered one + for(int cat = 0; cat < _shortcutsModel->rowCount(); cat++) { + QModelIndex catIdx = _shortcutsModel->index(cat, 0); + for(int r = 0; r < _shortcutsModel->rowCount(catIdx); r++) { + QModelIndex actIdx = _shortcutsModel->index(r, 0, catIdx); + Q_ASSERT(actIdx.isValid()); + if(actIdx.data(ShortcutsModel::ActiveShortcutRole).value() != seq) + continue; + + if(!actIdx.data(ShortcutsModel::IsConfigurableRole).toBool()) { + QMessageBox::warning(this, tr("Shortcut Conflict"), + tr("The \"%1\" shortcut is already in use, and cannot be configured.\nPlease choose another one.").arg(seq.toString()), + QMessageBox::Ok); + return false; + } + + QMessageBox box(QMessageBox::Warning, tr("Shortcut Conflict"), + (tr("The \"%1\" shortcut is ambiguous with the shortcut for the following action:") + + "
  • %2

" + + tr("Do you want to reassign this shortcut to the selected action?") + ).arg(seq.toString(), actIdx.data().toString()), + QMessageBox::Cancel, this); + box.addButton(tr("Reassign"), QMessageBox::AcceptRole); + if(box.exec() == QMessageBox::Cancel) + return false; + + _conflictingIndex = actIdx; + return true; + } + } + return true; +} diff --git a/src/qtui/settingspages/keysequencewidget.h b/src/qtui/settingspages/keysequencewidget.h new file mode 100644 index 00000000..82d14dce --- /dev/null +++ b/src/qtui/settingspages/keysequencewidget.h @@ -0,0 +1,107 @@ +/*************************************************************************** + * Copyright (C) 2010 by the Quassel Project * + * devel@quassel-irc.org * + * * + * This class has been inspired by KDE's KKeySequenceWidget and uses * + * some code snippets of its implementation, part of kdelibs. * + * The original file is * + * Copyright (C) 1998 Mark Donohoe * + * Copyright (C) 2001 Ellis Whitehead * + * Copyright (C) 2007 Andreas Hartmetz * + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License for more details. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program; if not, write to the * + * Free Software Foundation, Inc., * + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * + ***************************************************************************/ + +#ifndef KEYSEQUENCEWIDGET_H +#define KEYSEQUENCEWIDGET_H + +#include +#include +#include +#include + +#include "shortcutsmodel.h" + +class Action; +class ActionCollection; +class KeySequenceButton; +class QToolButton; + +class KeySequenceWidget : public QWidget { + Q_OBJECT +public: + KeySequenceWidget(QWidget *parent = 0); + + void setModel(ShortcutsModel *model); + +public slots: + void setKeySequence(const QKeySequence &seq); + +signals: + /** + * This signal is emitted when the current key sequence has changed by user input + * \param seq The key sequence the user has chosen + * \param conflicting The index of an action that needs to have its shortcut removed. The user has already been + * asked to agree (if he declines, this signal won't be emitted at all). + */ + void keySequenceChanged(const QKeySequence &seq, const QModelIndex &conflicting = QModelIndex()); + + void clicked(); + +private slots: + void updateShortcutDisplay(); + void startRecording(); + void cancelRecording(); + void clear(); + +private: + inline bool isRecording() const { return _isRecording; } + void doneRecording(); + + bool isOkWhenModifierless(int keyQt) const; + bool isShiftAsModifierAllowed(int keyQt) const; + bool isKeySequenceAvailable(const QKeySequence &seq); + + ShortcutsModel *_shortcutsModel; + bool _isRecording; + QKeySequence _keySequence, _oldKeySequence; + uint _modifierKeys; + QModelIndex _conflictingIndex; + + KeySequenceButton *_keyButton; + QToolButton *_clearButton; + + friend class KeySequenceButton; +}; + + +/*****************************************************************************/ + +class KeySequenceButton : public QPushButton { + Q_OBJECT +public: + explicit KeySequenceButton(KeySequenceWidget *d, QWidget *parent = 0); + +protected: + virtual bool event(QEvent *event); + virtual void keyPressEvent(QKeyEvent *event); + virtual void keyReleaseEvent(QKeyEvent *event); + +private: + KeySequenceWidget *d; +}; + +#endif // KEYSEQUENCEWIDGET_H diff --git a/src/qtui/settingspages/settingspages.inc b/src/qtui/settingspages/settingspages.inc index 1efe3e29..1659271b 100644 --- a/src/qtui/settingspages/settingspages.inc +++ b/src/qtui/settingspages/settingspages.inc @@ -9,3 +9,9 @@ set(SP_SOURCES aliasesmodel.cpp identityeditwidget.cpp ignorelistmodel.cpp notif set(SP_HEADERS aliasesmodel.h identityeditwidget.h ignorelistmodel.h notificationssettingspage.h previewbufferview.h) set(SP_FORMS buffervieweditdlg.ui coreaccounteditdlg.ui createidentitydlg.ui identityeditwidget.ui ignorelisteditdlg.ui saveidentitiesdlg.ui networkadddlg.ui networkeditdlg.ui nickeditdlg.ui servereditdlg.ui) + +if(NOT HAVE_KDE) + set(SETTINGSPAGES ${SETTINGSPAGES} shortcuts) + set(SP_SOURCES ${SP_SOURCES} keysequencewidget.cpp shortcutsmodel.cpp) + set(SP_HEADERS ${SP_HEADERS} keysequencewidget.h shortcutsmodel.h) +endif(NOT HAVE_KDE) diff --git a/src/qtui/settingspages/shortcutsmodel.cpp b/src/qtui/settingspages/shortcutsmodel.cpp new file mode 100644 index 00000000..f9dcbe26 --- /dev/null +++ b/src/qtui/settingspages/shortcutsmodel.cpp @@ -0,0 +1,232 @@ +/*************************************************************************** + * Copyright (C) 2010 by the Quassel Project * + * devel@quassel-irc.org * + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) version 3. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License for more details. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program; if not, write to the * + * Free Software Foundation, Inc., * + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * + ***************************************************************************/ + +#include "shortcutsmodel.h" + +#include "action.h" +#include "actioncollection.h" +#include "util.h" + +ShortcutsModel::ShortcutsModel(const QHash &actionCollections, QObject *parent) + : QAbstractItemModel(parent), + _changedCount(0) +{ + for(int r = 0; r < actionCollections.values().count(); r++) { + ActionCollection *coll = actionCollections.values().at(r); + Item *item = new Item(); + item->row = r; + item->collection = coll; + for(int i = 0; i < coll->actions().count(); i++) { + Action *action = qobject_cast(coll->actions().at(i)); + if(!action) + continue; + Item *actionItem = new Item(); + actionItem->parentItem = item; + actionItem->row = i; + actionItem->collection = coll; + actionItem->action = action; + actionItem->shortcut = action->shortcut(); + item->actionItems.append(actionItem); + } + _categoryItems.append(item); + } +} + +ShortcutsModel::~ShortcutsModel() { + qDeleteAll(_categoryItems); +} + +QModelIndex ShortcutsModel::parent(const QModelIndex &child) const { + if(!child.isValid()) + return QModelIndex(); + + Item *item = static_cast(child.internalPointer()); + Q_ASSERT(item); + + if(!item->parentItem) + return QModelIndex(); + + return createIndex(item->parentItem->row, 0, item->parentItem); +} + +QModelIndex ShortcutsModel::index(int row, int column, const QModelIndex &parent) const { + + if(parent.isValid()) + return createIndex(row, column, static_cast(parent.internalPointer())->actionItems.at(row)); + + // top level category item + return createIndex(row, column, _categoryItems.at(row)); +} + +int ShortcutsModel::columnCount(const QModelIndex &parent) const { + return 2; + if(!parent.isValid()) + return 2; + + Item *item = static_cast(parent.internalPointer()); + Q_ASSERT(item); + + if(!item->parentItem) + return 2; + + return 2; +} + +int ShortcutsModel::rowCount(const QModelIndex &parent) const { + if(!parent.isValid()) + return _categoryItems.count(); + + Item *item = static_cast(parent.internalPointer()); + Q_ASSERT(item); + + if(!item->parentItem) + return item->actionItems.count(); + + return 0; +} + +QVariant ShortcutsModel::headerData(int section, Qt::Orientation orientation, int role) const { + if(orientation != Qt::Horizontal || role != Qt::DisplayRole) + return QVariant(); + switch(section) { + case 0: + return tr("Action"); + case 1: + return tr("Shortcut"); + default: + return QVariant(); + } +} + +QVariant ShortcutsModel::data(const QModelIndex &index, int role) const { + if(!index.isValid()) + return QVariant(); + + Item *item = static_cast(index.internalPointer()); + Q_ASSERT(item); + + if(!item->parentItem) { + if(index.column() != 0) + return QVariant(); + switch(role) { + case Qt::DisplayRole: + return item->collection->property("Category"); + default: + return QVariant(); + } + } + + Action *action = qobject_cast(item->action); + Q_ASSERT(action); + + switch(role) { + case Qt::DisplayRole: + switch(index.column()) { + case 0: + return stripAcceleratorMarkers(action->text()); + case 1: + return item->shortcut.toString(); + default: + return QVariant(); + } + + case Qt::DecorationRole: + if(index.column() == 0) + return action->icon(); + return QVariant(); + + case ActionRole: + return QVariant::fromValue(action); + + case DefaultShortcutRole: + return action->shortcut(Action::DefaultShortcut); + case ActiveShortcutRole: + return item->shortcut; + + case IsConfigurableRole: + return action->isShortcutConfigurable(); + + default: + return QVariant(); + } +} + +bool ShortcutsModel::setData(const QModelIndex &index, const QVariant &value, int role) { + if(role != ActiveShortcutRole) + return false; + + if(!index.parent().isValid()) + return false; + + Item *item = static_cast(index.internalPointer()); + Q_ASSERT(item); + + QKeySequence newSeq = value.value(); + QKeySequence oldSeq = item->shortcut; + QKeySequence storedSeq = item->action->shortcut(Action::ActiveShortcut); + + item->shortcut = newSeq; + emit dataChanged(index, index.sibling(index.row(), 1)); + + if(oldSeq == storedSeq && newSeq != storedSeq) { + if(++_changedCount == 1) + emit hasChanged(true); + } else if(oldSeq != storedSeq && newSeq == storedSeq) { + if(--_changedCount == 0) + emit hasChanged(false); + } + + return true; +} + +void ShortcutsModel::load() { + foreach(Item *catItem, _categoryItems) { + foreach(Item *actItem, catItem->actionItems) { + actItem->shortcut = actItem->action->shortcut(Action::ActiveShortcut); + } + } + emit dataChanged(index(0, 1), index(rowCount()-1, 1)); + if(_changedCount != 0) { + _changedCount = 0; + emit hasChanged(false); + } +} + +void ShortcutsModel::commit() { + foreach(Item *catItem, _categoryItems) { + foreach(Item *actItem, catItem->actionItems) { + actItem->action->setShortcut(actItem->shortcut, Action::ActiveShortcut); + } + } + if(_changedCount != 0) { + _changedCount = 0; + emit hasChanged(false); + } +} + +void ShortcutsModel::defaults() { + for(int cat = 0; cat < rowCount(); cat++) { + QModelIndex catidx = index(cat, 0); + for(int act = 0; act < rowCount(catidx); act++) { + QModelIndex actidx = index(act, 1, catidx); + setData(actidx, actidx.data(DefaultShortcutRole), ActiveShortcutRole); + } + } +} diff --git a/src/qtui/settingspages/shortcutsmodel.h b/src/qtui/settingspages/shortcutsmodel.h new file mode 100644 index 00000000..a52e073c --- /dev/null +++ b/src/qtui/settingspages/shortcutsmodel.h @@ -0,0 +1,96 @@ +/*************************************************************************** + * Copyright (C) 2010 by the Quassel Project * + * devel@quassel-irc.org * + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) version 3. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License for more details. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program; if not, write to the * + * Free Software Foundation, Inc., * + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * + ***************************************************************************/ + +#ifndef SHORTCUTSMODEL_H +#define SHORTCUTSMODEL_H + +#include +#include + +class Action; +class ActionCollection; + +//! Model that exposes the actions from one or more ActionCollections +/** This model takes one or more ActionCollections and exposes their actions as model items. + * Note that the ShortcutsModel will not react to changes in the ActionCollection (e.g. adding, + * removing actions), because it is supposed to be used after all actions being defined. + */ +class ShortcutsModel : public QAbstractItemModel { + Q_OBJECT +public: + enum Role { + ActionRole = Qt::UserRole, + DefaultShortcutRole, + ActiveShortcutRole, + IsConfigurableRole + }; + + ShortcutsModel(const QHash &actionCollections, QObject *parent = 0); + ~ShortcutsModel(); + + QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const; + QModelIndex parent(const QModelIndex &child) const; + int columnCount(const QModelIndex &parent = QModelIndex()) const; + int rowCount(const QModelIndex &parent = QModelIndex()) const; + QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const; + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const; + bool setData(const QModelIndex &index, const QVariant &value, int role = ActiveShortcutRole); + +public slots: + //! Load shortcuts from the ActionCollections + /** Note that this will not rebuild the internal structure of the model, as we assume the + * ActionCollections to be static during the lifetime of the settingspage. This will merely + * re-read the shortcuts currently set in Quassel. + */ + void load(); + + //! Load default shortcuts from the ActionCollections + /** Note that this will not rebuild the internal structure of the model, as we assume the + * ActionCollections to be static during the lifetime of the settingspage. This will update + * the model's state from the ActionCollections' defaults. + */ + void defaults(); + + //! Commit the model changes to the ActionCollections + void commit(); + + inline bool hasChanged() const { return _changedCount; } + +signals: + //! Reflects the difference between model contents and the ActionCollections we loaded this from + void hasChanged(bool changed); + +private: + struct Item { + inline Item() { parentItem = 0; collection = 0; action = 0; } + inline ~Item() { qDeleteAll(actionItems); } + int row; + Item *parentItem; + ActionCollection *collection; + Action *action; + QKeySequence shortcut; + QList actionItems; + }; + + QList _categoryItems; + int _changedCount; +}; + +#endif // SHORTCUTSMODEL_H diff --git a/src/qtui/settingspages/shortcutssettingspage.cpp b/src/qtui/settingspages/shortcutssettingspage.cpp new file mode 100644 index 00000000..2317a7b6 --- /dev/null +++ b/src/qtui/settingspages/shortcutssettingspage.cpp @@ -0,0 +1,154 @@ + +/*************************************************************************** + * Copyright (C) 2010 by the Quassel Project * + * devel@quassel-irc.org * + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) version 3. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License for more details. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program; if not, write to the * + * Free Software Foundation, Inc., * + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * + ***************************************************************************/ + +#include + +#include "shortcutssettingspage.h" + +#include "action.h" +#include "actioncollection.h" +#include "qtui.h" +#include "shortcutsmodel.h" +#include "util.h" + +ShortcutsFilter::ShortcutsFilter(QObject *parent) : QSortFilterProxyModel(parent) { + setDynamicSortFilter(true); +} + +void ShortcutsFilter::setFilterString(const QString &filterString) { + _filterString = filterString; + invalidateFilter(); +} + +bool ShortcutsFilter::filterAcceptsRow(int source_row, const QModelIndex &source_parent) const { + if(!source_parent.isValid()) + return true; + + QModelIndex index = source_parent.model()->index(source_row, 0, source_parent); + Q_ASSERT(index.isValid()); + if(!qobject_cast(index.data(ShortcutsModel::ActionRole).value())->isShortcutConfigurable()) + return false; + + for(int col = 0; col < source_parent.model()->columnCount(source_parent); col++) { + if(source_parent.model()->index(source_row, col, source_parent).data().toString().contains(_filterString, Qt::CaseInsensitive)) + return true; + } + return false; +} + +/****************************************************************************/ + +ShortcutsSettingsPage::ShortcutsSettingsPage(const QHash &actionCollections, QWidget *parent) + : SettingsPage(tr("Interface"), tr("Shortcuts"), parent), + _shortcutsModel(new ShortcutsModel(actionCollections, this)), + _shortcutsFilter(new ShortcutsFilter(this)) +{ + ui.setupUi(this); + + _shortcutsFilter->setSourceModel(_shortcutsModel); + ui.shortcutsView->setModel(_shortcutsFilter); + ui.shortcutsView->expandAll(); + ui.shortcutsView->resizeColumnToContents(0); + ui.shortcutsView->sortByColumn(0, Qt::AscendingOrder); + + ui.keySequenceWidget->setModel(_shortcutsModel); + connect(ui.keySequenceWidget, SIGNAL(keySequenceChanged(QKeySequence,QModelIndex)), SLOT(keySequenceChanged(QKeySequence,QModelIndex))); + + connect(ui.shortcutsView->selectionModel(), SIGNAL(currentChanged(QModelIndex,QModelIndex)), SLOT(setWidgetStates())); + + setWidgetStates(); + + connect(ui.useDefault, SIGNAL(clicked(bool)), SLOT(toggledCustomOrDefault())); + connect(ui.useCustom, SIGNAL(clicked(bool)), SLOT(toggledCustomOrDefault())); + + connect(_shortcutsModel, SIGNAL(hasChanged(bool)), SLOT(setChangedState(bool))); + + // fugly, but directly setting it from the ctor doesn't seem to work + QTimer::singleShot(0, ui.searchEdit, SLOT(setFocus())); +} + +void ShortcutsSettingsPage::setWidgetStates() { + if(ui.shortcutsView->currentIndex().isValid() && ui.shortcutsView->currentIndex().parent().isValid()) { + QKeySequence active = ui.shortcutsView->currentIndex().data(ShortcutsModel::ActiveShortcutRole).value(); + QKeySequence def = ui.shortcutsView->currentIndex().data(ShortcutsModel::DefaultShortcutRole).value(); + ui.defaultShortcut->setText(def.isEmpty()? tr("None") : def.toString()); + ui.actionBox->setEnabled(true); + if(active == def) { + ui.useDefault->setChecked(true); + ui.keySequenceWidget->setKeySequence(QKeySequence()); + } else { + ui.useCustom->setChecked(true); + ui.keySequenceWidget->setKeySequence(active); + } + } else { + ui.defaultShortcut->setText(tr("None")); + ui.actionBox->setEnabled(false); + ui.useDefault->setChecked(true); + ui.keySequenceWidget->setKeySequence(QKeySequence()); + } +} + +void ShortcutsSettingsPage::on_searchEdit_textChanged(const QString &text) { + _shortcutsFilter->setFilterString(text); +} + +void ShortcutsSettingsPage::keySequenceChanged(const QKeySequence &seq, const QModelIndex &conflicting) { + if(conflicting.isValid()) + _shortcutsModel->setData(conflicting, QKeySequence(), ShortcutsModel::ActiveShortcutRole); + + QModelIndex rowIdx = _shortcutsFilter->mapToSource(ui.shortcutsView->currentIndex()); + Q_ASSERT(rowIdx.isValid()); + _shortcutsModel->setData(rowIdx, seq, ShortcutsModel::ActiveShortcutRole); + setWidgetStates(); +} + +void ShortcutsSettingsPage::toggledCustomOrDefault() { + if(!ui.shortcutsView->currentIndex().isValid()) + return; + + QModelIndex index = _shortcutsFilter->mapToSource(ui.shortcutsView->currentIndex()); + Q_ASSERT(index.isValid()); + + if(ui.useDefault->isChecked()) { + _shortcutsModel->setData(index, index.data(ShortcutsModel::DefaultShortcutRole)); + } else { + _shortcutsModel->setData(index, QKeySequence()); + } + setWidgetStates(); +} + +void ShortcutsSettingsPage::save() { + _shortcutsModel->commit(); + QtUi::saveShortcuts(); + SettingsPage::save(); +} + +void ShortcutsSettingsPage::load() { + _shortcutsModel->load(); + + SettingsPage::load(); +} + +void ShortcutsSettingsPage::defaults() { + _shortcutsModel->defaults(); + + SettingsPage::defaults(); +} diff --git a/src/qtui/settingspages/shortcutssettingspage.h b/src/qtui/settingspages/shortcutssettingspage.h new file mode 100644 index 00000000..bd3413df --- /dev/null +++ b/src/qtui/settingspages/shortcutssettingspage.h @@ -0,0 +1,72 @@ +/*************************************************************************** + * Copyright (C) 2010 by the Quassel Project * + * devel@quassel-irc.org * + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) version 3. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License for more details. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program; if not, write to the * + * Free Software Foundation, Inc., * + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * + ***************************************************************************/ + +#ifndef SHORTCUTSSETTINGSPAGE_H +#define SHORTCUTSSETTINGSPAGE_H + +#include + +#include "settingspage.h" + +#include "ui_shortcutssettingspage.h" + +class ActionCollection; +class ShortcutsModel; + +class ShortcutsFilter : public QSortFilterProxyModel { + Q_OBJECT +public: + ShortcutsFilter(QObject *parent = 0); + +public slots: + void setFilterString(const QString &filterString); + +protected: + virtual bool filterAcceptsRow(int source_row, const QModelIndex &source_parent) const; + +private: + QString _filterString; +}; + +class ShortcutsSettingsPage : public SettingsPage { + Q_OBJECT +public: + ShortcutsSettingsPage(const QHash &actionCollections, QWidget *parent = 0); + + inline bool hasDefaults() const { return true; } + +public slots: + void save(); + void load(); + void defaults(); + +private slots: + void on_searchEdit_textChanged(const QString &text); + void keySequenceChanged(const QKeySequence &seq, const QModelIndex &conflicting); + void setWidgetStates(); + void toggledCustomOrDefault(); + +private: + Ui::ShortcutsSettingsPage ui; + ShortcutsModel *_shortcutsModel; + ShortcutsFilter *_shortcutsFilter; +}; + +#endif // SHORTCUTSSETTINGSPAGE_H diff --git a/src/qtui/settingspages/shortcutssettingspage.ui b/src/qtui/settingspages/shortcutssettingspage.ui new file mode 100644 index 00000000..ead27c7e --- /dev/null +++ b/src/qtui/settingspages/shortcutssettingspage.ui @@ -0,0 +1,123 @@ + + + ShortcutsSettingsPage + + + + 0 + 0 + 497 + 481 + + + + Form + + + + + + + + Search: + + + + + + + + + + + + QAbstractItemView::NoEditTriggers + + + false + + + true + + + true + + + false + + + true + + + true + + + true + + + + + + + Shortcut for Selected Action + + + + + + Default: + + + + + + + None + + + + + + + Custom: + + + + + + + + + + Qt::Horizontal + + + + 346 + 20 + + + + + + + + + + + + KeySequenceWidget + QWidget +
keysequencewidget.h
+ 1 +
+
+ + searchEdit + shortcutsView + useDefault + useCustom + + + +
-- 2.20.1