1 /*
2 * shortcutsettingspage.cpp
3 * Copyright 2019, Thorbjørn Lindeijer <bjorn@lindeijer.nl>
4 *
5 * This file is part of Tiled.
6 *
7 * This program is free software; you can redistribute it and/or modify it
8 * under the terms of the GNU General Public License as published by the Free
9 * Software Foundation; either version 2 of the License, or (at your option)
10 * any later version.
11 *
12 * This program is distributed in the hope that it will be useful, but WITHOUT
13 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
14 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
15 * more details.
16 *
17 * You should have received a copy of the GNU General Public License along with
18 * this program. If not, see <http://www.gnu.org/licenses/>.
19 */
20
21 #include "shortcutsettingspage.h"
22 #include "ui_shortcutsettingspage.h"
23
24 #include "actionmanager.h"
25 #include "savefile.h"
26 #include "utils.h"
27
28 #include <QAbstractListModel>
29 #include <QAction>
30 #include <QApplication>
31 #include <QCoreApplication>
32 #include <QDateTime>
33 #include <QFileDialog>
34 #include <QItemEditorFactory>
35 #include <QKeyEvent>
36 #include <QKeySequenceEdit>
37 #include <QMessageBox>
38 #include <QSortFilterProxyModel>
39 #include <QStyledItemDelegate>
40 #include <QToolButton>
41 #include <QXmlStreamReader>
42 #include <QXmlStreamWriter>
43
44 #include <memory>
45
46 #include "qtcompat_p.h"
47
48 namespace Tiled {
49
50 /**
51 * The ActionsModel makes the list of actions and their shortcuts available
52 * to the view.
53 */
54 class ActionsModel : public QAbstractListModel
55 {
56 Q_OBJECT
57
58 public:
59 enum UserRoles {
60 HasCustomShortcut = Qt::UserRole,
61 HasConflictingShortcut,
62 ActionId,
63 };
64
65 explicit ActionsModel(QObject *parent = nullptr);
66
67 void setVisible(bool visible);
68
69 int rowCount(const QModelIndex &parent) const override;
70 int columnCount(const QModelIndex &parent) const override;
71 QVariant data(const QModelIndex &index, int role) const override;
72 bool setData(const QModelIndex &index, const QVariant &value, int role) override;
73 Qt::ItemFlags flags(const QModelIndex &index) const override;
74 QVariant headerData(int section, Qt::Orientation orientation, int role) const override;
75
76 void refresh();
77
78 private:
79 void refreshConflicts();
80 void emitDataChanged(int row);
81 void actionChanged(Id actionId);
82
83 QList<Id> mActions = ActionManager::actions();
84 QVector<bool> mConflicts;
85 bool mDirty = false;
86 bool mVisible = false;
87 bool mConflictsDirty = true;
88 };
89
ActionsModel(QObject * parent)90 ActionsModel::ActionsModel(QObject *parent)
91 : QAbstractListModel(parent)
92 {
93 connect(ActionManager::instance(), &ActionManager::actionChanged,
94 this, &ActionsModel::actionChanged);
95 connect(ActionManager::instance(), &ActionManager::actionsChanged,
96 this, [this] { mDirty = mConflictsDirty = true; refresh(); });
97
98 refreshConflicts();
99 }
100
setVisible(bool visible)101 void ActionsModel::setVisible(bool visible)
102 {
103 mVisible = visible;
104 refresh();
105 }
106
refresh()107 void ActionsModel::refresh()
108 {
109 if (!mVisible)
110 return;
111
112 if (mDirty) {
113 beginResetModel();
114 mActions = ActionManager::actions();
115 refreshConflicts();
116 mDirty = false;
117 endResetModel();
118 } else {
119 refreshConflicts();
120 }
121 }
122
refreshConflicts()123 void ActionsModel::refreshConflicts()
124 {
125 if (!mConflictsDirty)
126 return;
127
128 QMultiMap<QKeySequence, Id> actionsByKey;
129
130 for (const auto &actionId : qAsConst(mActions)) {
131 if (auto action = ActionManager::findAction(actionId))
132 if (!action->shortcut().isEmpty())
133 actionsByKey.insert(action->shortcut(), actionId);
134 }
135
136 QVector<bool> conflicts;
137 conflicts.reserve(mActions.size());
138
139 for (const auto &actionId : qAsConst(mActions)) {
140 if (auto action = ActionManager::findAction(actionId))
141 conflicts.append(actionsByKey.count(action->shortcut()) > 1);
142 else
143 conflicts.append(false);
144 }
145
146 mConflicts.swap(conflicts);
147 mConflictsDirty = false;
148
149 if (!mDirty && conflicts.size() == mConflicts.size() && conflicts != mConflicts) {
150 emit dataChanged(index(0, 0),
151 index(conflicts.size() - 1, 2),
152 QVector<int> { Qt::ForegroundRole });
153 }
154 }
155
rowCount(const QModelIndex & parent) const156 int ActionsModel::rowCount(const QModelIndex &parent) const
157 {
158 return parent.isValid() ? 0 : mActions.size();
159 }
160
columnCount(const QModelIndex & parent) const161 int ActionsModel::columnCount(const QModelIndex &parent) const
162 {
163 return parent.isValid() ? 0 : 3;
164 }
165
strippedText(QString s)166 static QString strippedText(QString s)
167 {
168 s.remove(QLatin1String("..."));
169 for (int i = 0; i < s.size(); ++i) {
170 if (s.at(i) == QLatin1Char('&'))
171 s.remove(i, 1);
172 }
173 return s.trimmed();
174 }
175
data(const QModelIndex & index,int role) const176 QVariant ActionsModel::data(const QModelIndex &index, int role) const
177 {
178 switch (role) {
179 case Qt::DisplayRole: {
180 const Id actionId = mActions.at(index.row());
181
182 switch (index.column()) {
183 case 0:
184 return actionId.name();
185 case 1:
186 return strippedText(ActionManager::action(actionId)->text());
187 case 2:
188 return ActionManager::action(actionId)->shortcut().toString(QKeySequence::NativeText);
189 }
190
191 break;
192 }
193
194 case Qt::EditRole: {
195 const Id actionId = mActions.at(index.row());
196 return ActionManager::action(actionId)->shortcut();
197 }
198 case Qt::FontRole: {
199 const Id actionId = mActions.at(index.row());
200
201 if (ActionManager::instance()->hasCustomShortcut(actionId)) {
202 QFont font = QApplication::font();
203 font.setBold(true);
204 return font;
205 }
206 break;
207 }
208 case Qt::ForegroundRole: {
209 if (mConflicts.at(index.row()))
210 return QColor(Qt::red);
211 break;
212 }
213 case HasCustomShortcut: {
214 const Id actionId = mActions.at(index.row());
215 return ActionManager::instance()->hasCustomShortcut(actionId);
216 }
217 case HasConflictingShortcut:
218 return mConflicts.at(index.row());
219 case ActionId:
220 return QVariant::fromValue(mActions.at(index.row()));
221 }
222
223 return QVariant();
224 }
225
setData(const QModelIndex & index,const QVariant & value,int role)226 bool ActionsModel::setData(const QModelIndex &index, const QVariant &value, int role)
227 {
228 if (index.column() == 2 && role == Qt::EditRole) {
229 const Id actionId = mActions.at(index.row());
230 const auto action = ActionManager::findAction(actionId);
231
232 if (action) {
233 auto actionManager = ActionManager::instance();
234
235 // Null QVariant used for resetting shortcut to default
236 if (value.isNull() && actionManager->hasCustomShortcut(actionId)) {
237 actionManager->resetCustomShortcut(actionId);
238 emitDataChanged(index.row());
239 refreshConflicts();
240 return true;
241 }
242
243 const auto keySequence = value.value<QKeySequence>();
244 if (action->shortcut() != keySequence) {
245 // Guaranteed to trigger actionChanged, which emits dataChanged
246 actionManager->setCustomShortcut(actionId, keySequence);
247 refreshConflicts();
248 return true;
249 }
250 }
251 }
252
253 return false;
254 }
255
flags(const QModelIndex & index) const256 Qt::ItemFlags ActionsModel::flags(const QModelIndex &index) const
257 {
258 auto f = QAbstractListModel::flags(index);
259
260 if (index.column() == 2)
261 f |= Qt::ItemIsEditable;
262
263 return f;
264 }
265
headerData(int section,Qt::Orientation orientation,int role) const266 QVariant ActionsModel::headerData(int section, Qt::Orientation orientation, int role) const
267 {
268 if (orientation == Qt::Horizontal && role == Qt::DisplayRole) {
269 switch (section) {
270 case 0:
271 return tr("Action");
272 case 1:
273 return tr("Text");
274 case 2:
275 return tr("Shortcut");
276 }
277 }
278 return QVariant();
279 }
280
emitDataChanged(int row)281 void ActionsModel::emitDataChanged(int row)
282 {
283 emit dataChanged(index(row, 0),
284 index(row, 2),
285 QVector<int> { Qt::DisplayRole, Qt::EditRole, Qt::FontRole });
286 }
287
actionChanged(Id actionId)288 void ActionsModel::actionChanged(Id actionId)
289 {
290 int row = mActions.indexOf(actionId);
291 if (row != -1) {
292 mConflictsDirty = true;
293 emitDataChanged(row);
294 }
295 }
296
297
298 /**
299 * Special sort-filter model that is able to filter based on key sequences.
300 */
301 class KeySequenceFilterModel : public QSortFilterProxyModel
302 {
303 public:
KeySequenceFilterModel(QObject * parent=nullptr)304 KeySequenceFilterModel(QObject *parent = nullptr)
305 : QSortFilterProxyModel(parent)
306 {}
307
308 void setFilter(const QString &pattern);
309
310 protected:
311 bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override;
312
313 private:
314 QString mPattern;
315 QKeySequence mKeySequence;
316 };
317
setFilter(const QString & pattern)318 void KeySequenceFilterModel::setFilter(const QString &pattern)
319 {
320 mPattern = pattern;
321
322 if (pattern.startsWith(QLatin1String("key:")))
323 mKeySequence = QKeySequence(pattern.mid(4));
324 else
325 mKeySequence = QKeySequence();
326
327 setFilterFixedString(pattern);
328 }
329
filterAcceptsRow(int sourceRow,const QModelIndex & sourceParent) const330 bool KeySequenceFilterModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const
331 {
332 if (mKeySequence.isEmpty())
333 return QSortFilterProxyModel::filterAcceptsRow(sourceRow, sourceParent);
334
335 auto source = sourceModel();
336 auto keySequence = source->data(source->index(sourceRow, 2, sourceParent), Qt::EditRole).value<QKeySequence>();
337 return !keySequence.isEmpty() && mKeySequence.matches(keySequence) != QKeySequence::NoMatch;
338 }
339
340
341 /**
342 * ShortcutEditor is needed to add a "Clear" button to the QKeySequenceEdit,
343 * which doesn't feature one as of Qt 5.13. It also adds a button to reset
344 * the shortcut.
345 */
346 class ShortcutEditor : public QWidget
347 {
348 Q_OBJECT
349 Q_PROPERTY(QKeySequence keySequence READ keySequence WRITE setKeySequence NOTIFY keySequenceChanged USER true)
350
351 public:
352 ShortcutEditor(QWidget *parent = nullptr);
353
354 QKeySequence keySequence() const;
355
356 void setResetEnabled(bool enabled);
357
358 public slots:
359 void setKeySequence(QKeySequence keySequence);
360
361 signals:
362 void resetRequested();
363 void editingFinished();
364 void keySequenceChanged(QKeySequence keySequence);
365
366 private:
367 QKeySequenceEdit *mKeySequenceEdit;
368 QToolButton *mResetButton;
369 };
370
ShortcutEditor(QWidget * parent)371 ShortcutEditor::ShortcutEditor(QWidget *parent)
372 : QWidget(parent)
373 , mKeySequenceEdit(new QKeySequenceEdit)
374 {
375 auto clearButton = new QToolButton(this);
376 clearButton->setAutoRaise(true);
377 clearButton->setAutoFillBackground(true);
378 clearButton->setToolTip(tr("Remove shortcut"));
379 clearButton->setEnabled(false);
380 clearButton->setIcon(QIcon(QLatin1String("://images/scalable/edit-delete-symbolic.svg")));
381
382 mResetButton = new QToolButton(this);
383 mResetButton->setAutoRaise(true);
384 mResetButton->setAutoFillBackground(true);
385 mResetButton->setToolTip(tr("Reset shortcut to default"));
386 mResetButton->setIcon(QIcon(QLatin1String("://images/scalable/edit-undo-symbolic.svg")));
387
388 auto layout = new QHBoxLayout(this);
389 layout->setContentsMargins(0, 0, 0, 0);
390 layout->setSpacing(0);
391 layout->addWidget(mKeySequenceEdit);
392 layout->addWidget(clearButton);
393 layout->addWidget(mResetButton);
394
395 setFocusProxy(mKeySequenceEdit);
396
397 connect(clearButton, &QToolButton::clicked,
398 mKeySequenceEdit, &QKeySequenceEdit::clear);
399 connect(mResetButton, &QToolButton::clicked,
400 this, &ShortcutEditor::resetRequested);
401
402 connect(mKeySequenceEdit, &QKeySequenceEdit::editingFinished,
403 this, &ShortcutEditor::editingFinished);
404 connect(mKeySequenceEdit, &QKeySequenceEdit::keySequenceChanged,
405 this, &ShortcutEditor::keySequenceChanged);
406 connect(mKeySequenceEdit, &QKeySequenceEdit::keySequenceChanged,
407 this, [=] { clearButton->setEnabled(!keySequence().isEmpty()); });
408 }
409
keySequence() const410 QKeySequence ShortcutEditor::keySequence() const
411 {
412 return mKeySequenceEdit->keySequence();
413 }
414
setResetEnabled(bool enabled)415 void ShortcutEditor::setResetEnabled(bool enabled)
416 {
417 mResetButton->setEnabled(enabled);
418 }
419
setKeySequence(QKeySequence keySequence)420 void ShortcutEditor::setKeySequence(QKeySequence keySequence)
421 {
422 mKeySequenceEdit->setKeySequence(keySequence);
423 }
424
425
426 /**
427 * ShortcutDelegate subclass needed to make the editor close when it emits the
428 * "editingFinished" signal.
429 */
430 class ShortcutDelegate : public QStyledItemDelegate
431 {
432 Q_OBJECT
433
434 public:
435 ShortcutDelegate(QObject *parent = nullptr);
436
437 QWidget *createEditor(QWidget *parent,
438 const QStyleOptionViewItem &option,
439 const QModelIndex &index) const override;
440
441 private:
442 std::unique_ptr<QItemEditorFactory> mItemEditorFactory;
443
444 };
445
ShortcutDelegate(QObject * parent)446 ShortcutDelegate::ShortcutDelegate(QObject *parent)
447 : QStyledItemDelegate(parent)
448 , mItemEditorFactory(new QItemEditorFactory)
449 {
450 mItemEditorFactory->registerEditor(QMetaType::QKeySequence,
451 new QStandardItemEditorCreator<ShortcutEditor>);
452
453 setItemEditorFactory(mItemEditorFactory.get());
454 }
455
createEditor(QWidget * parent,const QStyleOptionViewItem & option,const QModelIndex & index) const456 QWidget *ShortcutDelegate::createEditor(QWidget *parent,
457 const QStyleOptionViewItem &option,
458 const QModelIndex &index) const
459 {
460 auto editor = QStyledItemDelegate::createEditor(parent, option, index);
461
462 if (auto shortcutEditor = qobject_cast<ShortcutEditor*>(editor)) {
463 shortcutEditor->setResetEnabled(index.data(ActionsModel::HasCustomShortcut).toBool());
464
465 const QPersistentModelIndex persistentIndex(index);
466
467 connect(shortcutEditor, &ShortcutEditor::keySequenceChanged, this, [=] {
468 emit const_cast<ShortcutDelegate*>(this)->commitData(editor);
469 shortcutEditor->setResetEnabled(index.data(ActionsModel::HasCustomShortcut).toBool());
470 });
471
472 connect(shortcutEditor, &ShortcutEditor::editingFinished, this, [=] {
473 emit const_cast<ShortcutDelegate*>(this)->closeEditor(editor);
474 });
475
476 connect(shortcutEditor, &ShortcutEditor::resetRequested, this, [=] {
477 auto model = const_cast<QAbstractItemModel*>(persistentIndex.model());
478 model->setData(persistentIndex, QVariant(), Qt::EditRole);
479 shortcutEditor->setKeySequence(index.data(Qt::EditRole).value<QKeySequence>());
480 shortcutEditor->setResetEnabled(index.data(ActionsModel::HasCustomShortcut).toBool());
481 });
482 }
483
484 return editor;
485 }
486
487
488 /**
489 * Allows interactively resizing each column, but also stretches a single one
490 * when the header gets resized.
491 *
492 * Based on HeaderViewStretcher from Qt Creator.
493 */
494 class CustomStretchColumnHeaderView : public QHeaderView
495 {
496 Q_OBJECT
497
498 public:
499 CustomStretchColumnHeaderView(QWidget *parent = nullptr);
500
501 void initialize();
502
503 protected:
504 void resizeEvent(QResizeEvent *event) override;
505 void showEvent(QShowEvent *event) override;
506 void hideEvent(QHideEvent *event) override;
507
508 private:
509 const int mColumnToStretch = 1;
510 };
511
CustomStretchColumnHeaderView(QWidget * parent)512 CustomStretchColumnHeaderView::CustomStretchColumnHeaderView(QWidget *parent)
513 : QHeaderView(Qt::Horizontal, parent)
514 {
515 setStretchLastSection(true);
516 }
517
518 /**
519 * Should be called after setting the model and the header on the view, but
520 * before the view is shown.
521 */
initialize()522 void CustomStretchColumnHeaderView::initialize()
523 {
524 for (int i = 0; i < count(); ++i)
525 setSectionResizeMode(i, i == mColumnToStretch ? Stretch : ResizeToContents);
526 }
527
resizeEvent(QResizeEvent * event)528 void CustomStretchColumnHeaderView::resizeEvent(QResizeEvent *event)
529 {
530 if (sectionResizeMode(mColumnToStretch) == QHeaderView::Interactive) {
531 int diff = event->size().width() - event->oldSize().width();
532 resizeSection(mColumnToStretch, qMax(32, sectionSize(mColumnToStretch) + diff));
533 }
534 QHeaderView::resizeEvent(event);
535 }
536
showEvent(QShowEvent * event)537 void CustomStretchColumnHeaderView::showEvent(QShowEvent *event)
538 {
539 for (int i = 0; i < count(); ++i)
540 setSectionResizeMode(i, QHeaderView::Interactive);
541 QHeaderView::showEvent(event);
542 }
543
hideEvent(QHideEvent * event)544 void CustomStretchColumnHeaderView::hideEvent(QHideEvent *event)
545 {
546 initialize();
547 QHeaderView::hideEvent(event);
548 }
549
550
551 /**
552 * The actual settings page for editing keyboard shortcuts.
553 */
ShortcutSettingsPage(QWidget * parent)554 ShortcutSettingsPage::ShortcutSettingsPage(QWidget *parent)
555 : QWidget(parent)
556 , ui(new Ui::ShortcutSettingsPage)
557 , mActionsModel(new ActionsModel(this))
558 , mProxyModel(new KeySequenceFilterModel(this))
559 {
560 ui->setupUi(this);
561
562 ui->conflictsLabel->setVisible(false);
563
564 ui->filterEdit->setFilteredView(ui->shortcutsView);
565
566 mProxyModel->setSourceModel(mActionsModel);
567 mProxyModel->setSortLocaleAware(true);
568 mProxyModel->setSortCaseSensitivity(Qt::CaseInsensitive);
569 mProxyModel->setFilterCaseSensitivity(Qt::CaseInsensitive);
570 mProxyModel->setFilterKeyColumn(-1);
571 mProxyModel->setDynamicSortFilter(false); // Can mess up ShortcutEditor interaction
572
573 auto header = new CustomStretchColumnHeaderView(this);
574
575 ui->shortcutsView->setModel(mProxyModel);
576 ui->shortcutsView->setHeader(header);
577 ui->shortcutsView->sortByColumn(0, Qt::AscendingOrder);
578 ui->shortcutsView->setItemDelegateForColumn(2, new ShortcutDelegate);
579
580 header->initialize();
581
582 connect(ui->filterEdit, &QLineEdit::textChanged,
583 mProxyModel, &KeySequenceFilterModel::setFilter);
584
585 connect(ui->resetButton, &QPushButton::clicked, this, [this] {
586 ActionManager::instance()->resetAllCustomShortcuts();
587 mActionsModel->refresh();
588 });
589
590 connect(ui->shortcutsView, &QAbstractItemView::activated,
591 this, [this] (const QModelIndex &index) {
592 if (index.isValid()) {
593 auto shortcutIndex = mProxyModel->index(index.row(), 2);
594 ui->shortcutsView->setCurrentIndex(shortcutIndex); // Makes sure editor closes when current index changes
595 ui->shortcutsView->edit(shortcutIndex);
596 }
597 });
598
599 connect(ui->shortcutsView->selectionModel(), &QItemSelectionModel::currentRowChanged,
600 this, &ShortcutSettingsPage::refreshConflicts);
601 connect(mProxyModel, &QAbstractItemModel::dataChanged,
602 this, &ShortcutSettingsPage::refreshConflicts);
603
604 connect(ui->conflictsLabel, &QLabel::linkActivated,
605 this, &ShortcutSettingsPage::searchConflicts);
606
607 connect(ui->importButton, &QPushButton::clicked,
608 this, &ShortcutSettingsPage::importShortcuts);
609 connect(ui->exportButton, &QPushButton::clicked,
610 this, &ShortcutSettingsPage::exportShortcuts);
611 }
612
~ShortcutSettingsPage()613 ShortcutSettingsPage::~ShortcutSettingsPage()
614 {
615 QWidget *w = ui->shortcutsView->indexWidget(ui->shortcutsView->currentIndex());
616 if (auto shortcutEditor = qobject_cast<ShortcutEditor*>(w))
617 emit shortcutEditor->editingFinished();
618
619 delete ui;
620 }
621
sizeHint() const622 QSize ShortcutSettingsPage::sizeHint() const
623 {
624 QSize size = QWidget::sizeHint();
625 size.setWidth(Utils::dpiScaled(500));
626 return size;
627 }
628
showEvent(QShowEvent * event)629 void ShortcutSettingsPage::showEvent(QShowEvent *event)
630 {
631 mActionsModel->setVisible(true);
632 QWidget::showEvent(event);
633 }
634
hideEvent(QHideEvent * event)635 void ShortcutSettingsPage::hideEvent(QHideEvent *event)
636 {
637 mActionsModel->setVisible(false);
638 QWidget::hideEvent(event);
639 }
640
refreshConflicts()641 void ShortcutSettingsPage::refreshConflicts()
642 {
643 auto current = ui->shortcutsView->currentIndex();
644 bool conflicts = current.isValid() &&
645 mProxyModel->data(current, ActionsModel::HasConflictingShortcut).toBool();
646 ui->conflictsLabel->setVisible(conflicts);
647 }
648
searchConflicts()649 void ShortcutSettingsPage::searchConflicts()
650 {
651 auto current = ui->shortcutsView->currentIndex();
652 if (current.isValid()) {
653 auto filterSequence = mProxyModel->data(current, Qt::EditRole).value<QKeySequence>();
654 ui->filterEdit->setText(QLatin1String("key:") + filterSequence.toString(QKeySequence::NativeText));
655 }
656 }
657
importShortcuts()658 void ShortcutSettingsPage::importShortcuts()
659 {
660 QString filter = tr("Keyboard Mapping Scheme (*.kms)");
661 QString fileName = QFileDialog::getOpenFileName(this, tr("Import Shortcuts"),
662 QString(), filter);
663
664 if (fileName.isEmpty())
665 return;
666
667 QFile file(fileName);
668 if (!file.open(QFile::ReadOnly | QIODevice::Text)) {
669 QMessageBox::critical(this,
670 tr("Error Loading Shortcuts"),
671 QCoreApplication::translate("File Errors", "Could not open file for reading."));
672 return;
673 }
674
675 QXmlStreamReader xml(&file);
676
677 if (!xml.readNextStartElement() || xml.name() != QLatin1String("mapping")) {
678 QMessageBox::critical(this,
679 tr("Error Loading Shortcuts"),
680 tr("Invalid shortcuts file."));
681 return;
682 }
683
684 QHash<Id, QKeySequence> result;
685
686 while (xml.readNextStartElement()) {
687 if (xml.name() == QLatin1String("shortcut")) {
688 const Id id { xml.attributes().value(QLatin1String("id")).toUtf8() };
689
690 while (xml.readNextStartElement()) {
691 if (xml.name() == QLatin1String("key")) {
692 QString keyString = xml.attributes().value(QLatin1String("value")).toString();
693 result.insert(id, QKeySequence(keyString));
694 xml.skipCurrentElement(); // skip out of "key" element
695 xml.skipCurrentElement(); // skip out of "shortcut" element
696 break;
697 } else {
698 xml.skipCurrentElement(); // skip unknown element
699 }
700 }
701 } else {
702 xml.skipCurrentElement(); // skip unknown element
703 }
704 }
705
706 ActionManager::instance()->setCustomShortcuts(result);
707 mActionsModel->refresh();
708 }
709
exportShortcuts()710 void ShortcutSettingsPage::exportShortcuts()
711 {
712 QString filter = tr("Keyboard Mapping Scheme (*.kms)");
713 QString fileName = QFileDialog::getSaveFileName(this, tr("Export Shortcuts"),
714 QString(), filter);
715
716 if (fileName.isEmpty())
717 return;
718
719 SaveFile file(fileName);
720
721 if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
722 QMessageBox::critical(this,
723 tr("Error Saving Shortcuts"),
724 QCoreApplication::translate("File Errors", "Could not open file for writing."));
725 return;
726 }
727
728 QXmlStreamWriter xml(file.device());
729
730 xml.setAutoFormatting(true);
731 xml.setAutoFormattingIndent(1);
732
733 xml.writeStartDocument();
734 xml.writeDTD(QLatin1String("<!DOCTYPE KeyboardMappingScheme>"));
735 xml.writeComment(QStringLiteral(" Written by %1 %2, %3. ").
736 arg(QApplication::applicationDisplayName(),
737 QApplication::applicationVersion(),
738 QDateTime::currentDateTime().toString(Qt::ISODate)));
739 xml.writeStartElement(QStringLiteral("mapping"));
740
741 auto actions = ActionManager::actions();
742 std::sort(actions.begin(), actions.end());
743
744 for (Id actionId : qAsConst(actions)) {
745 const auto action = ActionManager::action(actionId);
746 const auto shortcut = action->shortcut();
747
748 xml.writeStartElement(QStringLiteral("shortcut"));
749 xml.writeAttribute(QStringLiteral("id"), actionId.toString());
750
751 if (!shortcut.isEmpty()) {
752 xml.writeEmptyElement(QLatin1String("key"));
753 xml.writeAttribute(QStringLiteral("value"), shortcut.toString());
754 }
755
756 xml.writeEndElement(); // shortcut
757 }
758
759 xml.writeEndElement(); // mapping
760 xml.writeEndDocument();
761
762 if (!file.commit()) {
763 QMessageBox::critical(this,
764 tr("Error Saving Shortcuts"),
765 file.errorString());
766 }
767 }
768
769 } // namespace Tiled
770
771 #include "shortcutsettingspage.moc"
772 #include "moc_shortcutsettingspage.cpp"
773