/** * RobloxKeyboardConfig.cpp * Copyright (c) 2013 ROBLOX Corp. All Rights Reserved. */ #include "stdafx.h" #include "RobloxKeyboardConfig.h" #include #include #include #include #include #include #include #include #include "RobloxMainWindow.h" #include "StudioUtilities.h" FASTFLAG(StudioShowTutorialsByDefault) // shortcuts that should not be visible to the average user static const QString ShortcutExceptions = "toggleBuildModeAction filePublishedProjectsAction screenShotAction"; enum eShortcutTableColumns { STC_NAME = 0, STC_SHORTCUT, STC_DESCRIPTION, STC_MAX, }; // A comparator for sorting shortcuts and their overrides. class ShortcutComparator { public: ShortcutComparator(int columnArg, Qt::SortOrder orderArg, ShortcutsModel& modelArg) : column(columnArg) , order(orderArg) , model(modelArg) {} bool operator()(const int& a, const int& b) const { QString aData = model.m_shortcutData[a].toString(column); QString bData = model.m_shortcutData[b].toString(column); if (order == Qt::AscendingOrder) return aData.toLower() > bData.toLower(); else return aData.toLower() < bData.toLower(); } int column; Qt::SortOrder order; ShortcutsModel& model; }; KeySequenceEdit::KeySequenceEdit(QWidget* parent) : QWidget(parent) , m_num(0) { setContentsMargins(0, 0, 0, 0); QHBoxLayout *layout = new QHBoxLayout(this); m_lineEdit = new QLineEdit(this); m_lineEdit->setFixedHeight(20); layout->addWidget(m_lineEdit); layout->setMargin(0); layout->setContentsMargins(0, 0, 0, 0); m_lineEdit->installEventFilter(this); m_lineEdit->setReadOnly(true); m_lineEdit->setFocusProxy(this); setFocusPolicy(m_lineEdit->focusPolicy()); setAttribute(Qt::WA_InputMethodEnabled); } bool KeySequenceEdit::eventFilter(QObject* o, QEvent* e) { // Create a right click context menu for clearing the QKeySequence set. if (o == m_lineEdit && e->type() == QEvent::ContextMenu) { QContextMenuEvent *c = static_cast(e); QMenu *menu = m_lineEdit->createStandardContextMenu(); menu->clear(); QAction *clearAction = new QAction(tr("Clear Shortcut"), menu); menu->insertAction(NULL, clearAction); clearAction->setEnabled(!m_keySequence.isEmpty()); connect(clearAction, SIGNAL(triggered()), this, SLOT(slotClearShortcut())); menu->exec(c->globalPos()); delete menu; e->accept(); return true; } return QWidget::eventFilter(o, e); } void KeySequenceEdit::slotClearShortcut() { if (m_keySequence.isEmpty()) return; setKeySequence(QKeySequence()); Q_EMIT keySequenceChanged(m_keySequence); } void KeySequenceEdit::handleKeyEvent(QKeyEvent* e) { // Main key processing function. All keys pressed will pass through // here and then be converted to QKeySequences. if (e->key() == Qt::Key_Enter || e->key() == Qt::Key_Return) { Q_EMIT(editingFinished()); return; } int key = e->key(); if (key == Qt::Key_Control || key == Qt::Key_Shift || key == Qt::Key_Meta || key == Qt::Key_Alt || key == Qt::Key_Super_L || key == Qt::Key_AltGr) return; key |= StudioUtilities::translateKeyModifiers(e->modifiers(), e->text()); m_keySequence = QKeySequence(key); m_lineEdit->setText(m_keySequence.toString(QKeySequence::NativeText)); e->accept(); Q_EMIT keySequenceChanged(m_keySequence); } void KeySequenceEdit::setKeySequence(const QKeySequence &sequence) { if (sequence == m_keySequence) return; m_num = 0; m_keySequence = sequence; m_lineEdit->setText(m_keySequence.toString(QKeySequence::NativeText)); } QKeySequence KeySequenceEdit::keySequence() const { return m_keySequence; } void KeySequenceEdit::focusInEvent(QFocusEvent *e) { m_lineEdit->event(e); m_lineEdit->selectAll(); QWidget::focusInEvent(e); } void KeySequenceEdit::focusOutEvent(QFocusEvent *e) { m_num = 0; m_lineEdit->event(e); QWidget::focusOutEvent(e); } void KeySequenceEdit::keyPressEvent(QKeyEvent *e) { handleKeyEvent(e); e->accept(); } void KeySequenceEdit::keyReleaseEvent(QKeyEvent *e) { m_lineEdit->event(e); } bool KeySequenceEdit::event(QEvent *e) { if (e->type() == QEvent::Shortcut || e->type() == QEvent::ShortcutOverride || e->type() == QEvent::KeyRelease) { e->accept(); return true; } return QWidget::event(e); } //////////////////////////////////////////////////////////////////////////////// QString ShortcutData::toString(int column) const { if (column == STC_NAME) { return m_action->text(); } else if (column == STC_SHORTCUT) { if (hasOverride()) { const QKeySequence& override = m_shortcutOverride; return override.toString(QKeySequence::NativeText); } return m_action->shortcut().toString(QKeySequence::NativeText); } else if (column == STC_DESCRIPTION) { return m_action->statusTip(); } return QString(); } //////////////////////////////////////////////////////////////////////////////// ShortcutsModel::ShortcutsModel(RobloxMainWindow& mainWindow, QObject *parent) : QAbstractTableModel(parent) , m_mainWindow(mainWindow) { resetData(); } void ShortcutsModel::resetData() { // Performs a complete flush of the model and repopulates it based // off of the mainWindow's action loadout. m_shortcutData.clear(); m_sortOrder.clear(); int i = 0; QObjectList objects = m_mainWindow.children(); QObjectList::iterator iter = objects.begin(); for ( ; iter != objects.end() ; ++iter ) { RBX::BaldPtr action = dynamic_cast(*iter); if ( action && !action->text().isEmpty() && !action->text().startsWith("&") && !ShortcutExceptions.contains(action->objectName()) && !(!FFlag::StudioShowTutorialsByDefault && action->objectName() == "viewTutorialsAction")) { m_shortcutData.push_back(ShortcutData(action)); m_sortOrder.push_back(i++); } } Q_EMIT(dataChanged(index(0,0), index(rowCount(), columnCount()))); } int ShortcutsModel::rowCount(const QModelIndex& /*parent*/) const { return m_shortcutData.size(); } int ShortcutsModel::columnCount(const QModelIndex& /*parent*/) const { return STC_MAX; } // awagnerTODO: add a search bar and use a QSortFilterProxyModel void ShortcutsModel::sort(int column, Qt::SortOrder order) { ShortcutComparator comparator(column, order, *this); qSort(m_sortOrder.begin(), m_sortOrder.end(), comparator); Q_EMIT(layoutChanged()); } QVariant ShortcutsModel::data(const QModelIndex &index, int role) const { const ShortcutData& shortcutData = m_shortcutData[m_sortOrder[index.row()]]; const QAction* action = shortcutData.action(); if (role == Qt::DecorationRole) { if (index.column() == STC_NAME) { QIcon icon = action->icon(); if ( icon.isNull() ) { QPixmap pix(16,16); pix.fill(QColor(0,0,0,0)); icon = QIcon(pix); } return icon; } } if (role == Qt::EditRole) { return QVariant::fromValue(shortcutData); } if (role == Qt::DisplayRole) { return shortcutData.toString(index.column()); } return QVariant(); } bool ShortcutsModel::setData(const QModelIndex & index, const QVariant& value, int role) { if (role == Qt::EditRole && index.column() == STC_SHORTCUT) { ShortcutData shortcut = qvariant_cast(value); ShortcutData& shortcutData = m_shortcutData[m_sortOrder[index.row()]]; shortcutData = shortcut; Q_EMIT(dataChanged(index, index)); return true; } return false; } QVariant ShortcutsModel::headerData(int section, Qt::Orientation orientation, int role) const { if (role == Qt::DisplayRole) { if (orientation == Qt::Horizontal) { switch (section) { case STC_NAME: return QString("Name"); case STC_SHORTCUT: return QString("Shortcut"); case STC_DESCRIPTION: return QString("Description"); } } } return QVariant(); } Qt::ItemFlags ShortcutsModel::flags(const QModelIndex& index) const { if (index.column() == STC_SHORTCUT) return Qt::ItemIsSelectable | Qt::ItemIsEditable | Qt::ItemIsEnabled; else return Qt::ItemIsSelectable | Qt::ItemIsEnabled; } void ShortcutsModel::commitOverrides() { for (QList::iterator iter = m_shortcutData.begin(); iter != m_shortcutData.end(); iter++) { if (iter->hasOverride()) { iter->action()->setShortcut(iter->shortcutOverride()); } } } void ShortcutsModel::resetOverrides() { for (QList::iterator iter = m_shortcutData.begin(); iter != m_shortcutData.end(); iter++) { if (iter->hasOverride()) { iter->removeShortcutOverride(); } } } bool ShortcutsModel::removeShortcut(const QKeySequence& keySequence) { for (QList::iterator iter = m_shortcutData.begin(); iter != m_shortcutData.end(); iter++) { // If an action already has the shortcut, then add a override. Or if // the shortcut is overriden and it matches the keysequence, then set // the override to empty. if (iter->hasOverride()) { if (iter->shortcutOverride().toString() == keySequence.toString()) { iter->setShortcutOverride(QKeySequence()); } } else if (iter->action()->shortcut().toString() == keySequence.toString()) { iter->setShortcutOverride(QKeySequence()); } } return false; } //////////////////////////////////////////////////////////////////////////////// QWidget* ShortcutDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const { if (index.column() == STC_SHORTCUT) { KeySequenceEdit *editor = new KeySequenceEdit(parent); return editor; } return QStyledItemDelegate::createEditor(parent, option, index); } void ShortcutDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const { if (index.column() == STC_SHORTCUT) { const ShortcutsModel* model = qobject_cast(index.model()); if (!model) return; ShortcutData shortcutData = qvariant_cast(model->data(index, Qt::EditRole)); KeySequenceEdit *keySequenceEdit = qobject_cast(editor); if (shortcutData.hasOverride()) { const QKeySequence& override = shortcutData.shortcutOverride(); keySequenceEdit->setKeySequence(override); return; } keySequenceEdit->setKeySequence(shortcutData.action()->shortcut()); } else { QStyledItemDelegate::setEditorData(editor, index); } } QSize ShortcutDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const { if (index.column() == STC_SHORTCUT) { QSize size = QStyledItemDelegate::sizeHint(option, index); size.setHeight(28); return size; } else { return QStyledItemDelegate::sizeHint(option, index); } } void ShortcutDelegate::setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const { // Called when the edit has finished. Saves the QKeySequence generated // to a shortcut override on the model, first removing it from any action // that may already have it. if (index.column() == STC_SHORTCUT) { KeySequenceEdit *keySequenceEdit = qobject_cast(editor); if (!keySequenceEdit) return; ShortcutsModel* shortcutsModel = qobject_cast(model); if (!shortcutsModel) return; ShortcutData shortcut = qvariant_cast(shortcutsModel->data(index, Qt::EditRole)); const QKeySequence& keySequence = keySequenceEdit->keySequence(); // Remove the shortcut from all actions if it's already assigned. shortcutsModel->removeShortcut(keySequence); shortcut.setShortcutOverride(keySequence); shortcutsModel->setData(index, QVariant::fromValue(shortcut)); } else { QStyledItemDelegate::setModelData(editor, model, index); } } //////////////////////////////////////////////////////////////////////////////// RobloxKeyboardConfig::RobloxKeyboardConfig() { } RobloxKeyboardConfig& RobloxKeyboardConfig::singleton() { static RobloxKeyboardConfig* robloxKeyboardConfig = new RobloxKeyboardConfig(); return *robloxKeyboardConfig; } void RobloxKeyboardConfig::storeDefaults(RobloxMainWindow& mainWindow) { // Store off all the action shortcuts in their default state. QObjectList objects = mainWindow.children(); QObjectList::iterator iter = objects.begin(); for (; iter != objects.end(); ++iter) { RBX::BaldPtr action = dynamic_cast(*iter); if (action && !action->text().isEmpty() && !action->text().startsWith("&") && !ShortcutExceptions.contains(action->objectName())) { m_defaultShortcuts[action->objectName()] = action->shortcut(); } } } void RobloxKeyboardConfig::saveKeyboardConfig(RobloxMainWindow& mainWindow) { // Save any action shortcut that deviates from its default in a QSetting. QMap keymap; QObjectList objects = mainWindow.children(); QObjectList::iterator iter = objects.begin(); for ( ; iter != objects.end() ; ++iter ) { RBX::BaldPtr action = dynamic_cast(*iter); if (action && !action->text().isEmpty() && !action->text().startsWith("&") && !ShortcutExceptions.contains(action->objectName()) && !(!FFlag::StudioShowTutorialsByDefault && action->objectName() == "viewTutorialsAction")) { QMap::iterator iter = m_defaultShortcuts.find(action->objectName()); if (iter != m_defaultShortcuts.end()) { if(iter.value().toString(QKeySequence::NativeText) != action->shortcut().toString(QKeySequence::NativeText)) { keymap[action->objectName()] = action->shortcut(); } } } } QSettings settings; // The only way to clear a group is by calling remove on an empty string. // http://qt-project.org/doc/qt-4.8/qsettings.html#remove // settings.beginGroup("Roblox Studio Key Mapping"); settings.remove(""); settings.endGroup(); settings.beginGroup("Roblox Studio Key Mapping"); QMap::const_iterator i = keymap.constBegin(); while (i != keymap.constEnd()) { settings.setValue(i.key(), i.value().toString()); ++i; } settings.endGroup(); } void RobloxKeyboardConfig::loadKeyboardConfig(RobloxMainWindow& mainWindow) { // Load the shortcut QSettings group into a map and map it back to each // action by objectName(). QMap keymap; QSettings settings; settings.beginGroup("Roblox Studio Key Mapping"); QStringList keys = settings.childKeys(); Q_FOREACH (QString key, keys) { keymap[key] = QKeySequence(settings.value(key).toString()); } settings.endGroup(); QObjectList objects = mainWindow.children(); QObjectList::iterator iter = objects.begin(); for ( ; iter != objects.end() ; ++iter ) { RBX::BaldPtr action = dynamic_cast(*iter); if (action && !action->text().isEmpty() && !action->text().startsWith("&") && !ShortcutExceptions.contains(action->objectName()) && !(!FFlag::StudioShowTutorialsByDefault && action->objectName() == "viewTutorialsAction")) { QMap::iterator iter = keymap.find(action->objectName()); if (iter != keymap.end()) { action->setShortcut(iter.value()); } } } } //////////////////////////////////////////////////////////////////////////////// RobloxKeyboardConfigWidget::RobloxKeyboardConfigWidget(RobloxMainWindow& MainWindow, QWidget* parent) : QWidget(parent) , m_mainWindow(MainWindow) , m_filterEdit(NULL) { initialize(); } void RobloxKeyboardConfigWidget::initialize() { RBXASSERT(!layout()); RBX::BaldPtr layout = new QVBoxLayout; m_filterEdit = new QLineEdit(this); m_filterEdit->setPlaceholderText("Search Actions"); m_filterEdit->setFocusPolicy(Qt::ClickFocus); layout->addWidget(m_filterEdit); connect(m_filterEdit, SIGNAL(textChanged(QString)), this, SLOT(setFilter(QString))); m_treeView = new QTreeView(this); layout->addWidget(m_treeView); setLayout(layout); m_model = new ShortcutsModel(m_mainWindow, this); m_treeView->setModel(m_model); m_treeView->setItemsExpandable(false); m_treeView->setRootIsDecorated(false); m_treeView->setSortingEnabled(true); m_treeView->header()->setDefaultSectionSize(200); connect(m_model, SIGNAL(dataChanged(const QModelIndex&, const QModelIndex&)), this, SLOT(dataChanged(const QModelIndex&, const QModelIndex&))); m_treeView->setItemDelegateForColumn(STC_SHORTCUT, new ShortcutDelegate(this)); } void RobloxKeyboardConfigWidget::cancel() { m_model->resetOverrides(); } void RobloxKeyboardConfigWidget::accept() { m_model->commitOverrides(); m_mainWindow.updateShortcutSet(); RobloxKeyboardConfig::singleton().saveKeyboardConfig(m_mainWindow); } void RobloxKeyboardConfigWidget::restoreAllDefaults() { // Iterate through all the actions on mainWindow and set them back to the // defaultShortcuts we stored off after application load. m_treeView->closePersistentEditor(m_treeView->currentIndex()); QObjectList objects = m_mainWindow.children(); QObjectList::iterator iter = objects.begin(); for (; iter != objects.end(); ++iter) { RBX::BaldPtr action = dynamic_cast(*iter); if (action && !action->text().isEmpty() && !action->text().startsWith("&") && !ShortcutExceptions.contains(action->objectName())) { QMap::const_iterator iter = RobloxKeyboardConfig::singleton().defaultShortcuts().find(action->objectName()); if (iter != RobloxKeyboardConfig::singleton().defaultShortcuts().end()) { action->setShortcut(iter.value()); } } } m_model->resetData(); m_treeView->sortByColumn(0, Qt::DescendingOrder); Q_EMIT(dataChanged()); } void RobloxKeyboardConfigWidget::dataChanged(const QModelIndex&, const QModelIndex&) { Q_EMIT(dataChanged()); } void RobloxKeyboardConfigWidget::setFilter(QString filter) { setUpdatesEnabled(false); QAbstractItemModel* treeViewModel = m_treeView->model(); int numRows = treeViewModel->rowCount(); QString text; for (int ii = 0; ii < numRows; ++ii) { text = treeViewModel->data(treeViewModel->index(ii, 0)).toString(); if (filter.isEmpty() || text.contains(filter, Qt::CaseInsensitive)) m_treeView->setRowHidden(ii, QModelIndex(), false); else m_treeView->setRowHidden(ii, QModelIndex(), true); } setUpdatesEnabled(true); }