1 /**
2 * \file shortcutsmodel.cpp
3 * Keyboard shortcuts configuration tree model.
4 *
5 * \b Project: Kid3
6 * \author Urs Fleisch
7 * \date 29 Dec 2011
8 *
9 * Copyright (C) 2011-2018 Urs Fleisch
10 *
11 * This file is part of Kid3.
12 *
13 * Kid3 is free software; you can redistribute it and/or modify
14 * it under the terms of the GNU General Public License as published by
15 * the Free Software Foundation; either version 2 of the License, or
16 * (at your option) any later version.
17 *
18 * Kid3 is distributed in the hope that it will be useful,
19 * but WITHOUT ANY WARRANTY; without even the implied warranty of
20 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 * GNU General Public License for more details.
22 *
23 * You should have received a copy of the GNU General Public License
24 * along with this program. If not, see <http://www.gnu.org/licenses/>.
25 */
26
27 #include "shortcutsmodel.h"
28 #include "isettings.h"
29 #include <QAction>
30 #include <QFont>
31
32 namespace {
33
34 const int TopLevelId = -1;
35
isTopLevelItem(const QModelIndex & index)36 bool isTopLevelItem(const QModelIndex& index)
37 {
38 return quintptr(index.internalId()) == quintptr(TopLevelId);
39 }
40
41 }
42
43 /**
44 * Constructor.
45 * @param parent parent widget
46 */
ShortcutsModel(QObject * parent)47 ShortcutsModel::ShortcutsModel(QObject* parent) : QAbstractItemModel(parent)
48 {
49 setObjectName(QLatin1String("ShortcutsModel"));
50 }
51
52 /**
53 * Get item flags for index.
54 * @param index model index
55 * @return item flags
56 */
flags(const QModelIndex & index) const57 Qt::ItemFlags ShortcutsModel::flags(const QModelIndex& index) const
58 {
59 Qt::ItemFlags itemFlags = QAbstractItemModel::flags(index);
60 if (index.isValid() && index.column() == ShortcutColumn) {
61 itemFlags |= Qt::ItemIsEditable;
62 }
63 return itemFlags;
64 }
65
66 /**
67 * Get group if a model index is a valid index for a group item.
68 * @param index index to check
69 * @return group if index is for a group item else 0
70 */
shortcutGroupForIndex(const QModelIndex & index) const71 const ShortcutsModel::ShortcutGroup* ShortcutsModel::shortcutGroupForIndex(
72 const QModelIndex& index) const
73 {
74 if (index.column() == 0 &&
75 index.row() >= 0 && index.row() < m_shortcutGroups.size() &&
76 isTopLevelItem(index)) {
77 return &m_shortcutGroups.at(index.row());
78 }
79 return nullptr;
80 }
81
82 /**
83 * Get data for a given role.
84 * @param index model index
85 * @param role item data role
86 * @return data for role
87 */
data(const QModelIndex & index,int role) const88 QVariant ShortcutsModel::data(const QModelIndex& index, int role) const
89 {
90 if (index.isValid()) {
91 QModelIndex parentIndex = index.parent();
92 if (parentIndex.isValid()) {
93 if (const ShortcutGroup* group = shortcutGroupForIndex(parentIndex)) {
94 if (index.row() >= 0 && index.row() < group->size()) {
95 const ShortcutItem& shortcutItem = group->at(index.row());
96 if (index.column() == ActionColumn) {
97 if (role == Qt::DisplayRole) {
98 return shortcutItem.actionText();
99 } else if (role == Qt::FontRole) {
100 if (shortcutItem.isCustomShortcutActive()) {
101 QFont font;
102 font.setBold(true);
103 return font;
104 }
105 }
106 } else if (index.column() == ShortcutColumn) {
107 if (role == Qt::DisplayRole || role == Qt::EditRole) {
108 QKeySequence keySequence = QKeySequence::fromString(
109 shortcutItem.activeShortcut(), QKeySequence::PortableText);
110 return keySequence.toString(QKeySequence::NativeText);
111 } else if (role == Qt::ToolTipRole) {
112 return tr("Press F2 or double click to edit cell contents.");
113 }
114 }
115 }
116 }
117 } else {
118 if (const ShortcutGroup* group = shortcutGroupForIndex(index)) {
119 if (role == Qt::DisplayRole) {
120 return group->context();
121 }
122 }
123 }
124 }
125 return QVariant();
126 }
127
128 /**
129 * Set data for a given role.
130 * @param index model index
131 * @param value data value
132 * @param role item data role
133 * @return true if successful
134 */
setData(const QModelIndex & index,const QVariant & value,int role)135 bool ShortcutsModel::setData(const QModelIndex& index, const QVariant& value,
136 int role)
137 {
138 if (index.isValid() && index.column() == ShortcutColumn && role == Qt::EditRole) {
139 QModelIndex parentIndex = index.parent();
140 if (parentIndex.isValid()) {
141 if (auto group =
142 const_cast<ShortcutGroup*>(shortcutGroupForIndex(parentIndex))) {
143 if (index.row() >= 0 && index.row() < group->size()) {
144 ShortcutItem si((*group)[index.row()]);
145 const QString valueString = !value.isNull()
146 ? value.value<QKeySequence>().toString(QKeySequence::PortableText)
147 : QString();
148 si.setCustomShortcut(valueString);
149 QString keyString(si.activeShortcut());
150 if (!keyString.isEmpty()) {
151 const auto gs = m_shortcutGroups;
152 for (const ShortcutGroup& g : gs) {
153 for (const ShortcutItem& i : g) {
154 if (i.activeShortcut() == keyString &&
155 si.action() != i.action() &&
156 (si.action()->shortcutContext() != Qt::WidgetShortcut ||
157 i.action()->shortcutContext() != Qt::WidgetShortcut)) {
158 emit shortcutAlreadyUsed(keyString, g.context(), i.action());
159 return false;
160 }
161 }
162 }
163 }
164 (*group)[index.row()].setCustomShortcut(valueString);
165 emit dataChanged(index.sibling(index.row(), ActionColumn), index);
166 emit shortcutSet(keyString, group->context(), si.action());
167 return true;
168 }
169 }
170 }
171 }
172 return false;
173 }
174
175 /**
176 * Get data for header section.
177 * @param section column or row
178 * @param orientation horizontal or vertical
179 * @param role item data role
180 * @return header data for role
181 */
headerData(int section,Qt::Orientation orientation,int role) const182 QVariant ShortcutsModel::headerData(int section, Qt::Orientation orientation,
183 int role) const
184 {
185 if (role != Qt::DisplayRole)
186 return QVariant();
187 if (orientation == Qt::Horizontal) {
188 if (section == ActionColumn) {
189 return tr("Action");
190 } else if (section == ShortcutColumn) {
191 return tr("Shortcut");
192 }
193 }
194 return section + 1;
195 }
196
197 /**
198 * Get number of rows.
199 * @param parent parent model index
200 * @return number of rows, if parent is valid number of children
201 */
rowCount(const QModelIndex & parent) const202 int ShortcutsModel::rowCount(const QModelIndex& parent) const
203 {
204 if (parent.isValid()) {
205 if (const ShortcutGroup* group = shortcutGroupForIndex(parent)) {
206 return group->size();
207 }
208 return 0;
209 } else {
210 return m_shortcutGroups.size();
211 }
212 }
213
214 /**
215 * Get number of columns.
216 * @param parent parent model index
217 * @return number of columns for children of given @a parent
218 */
columnCount(const QModelIndex & parent) const219 int ShortcutsModel::columnCount(const QModelIndex& parent) const
220 {
221 Q_UNUSED(parent)
222 return NumColumns;
223 }
224
225 /**
226 * Get model index of item.
227 * @param row row of item
228 * @param column column of item
229 * @param parent index of parent item
230 * @return model index of item
231 */
index(int row,int column,const QModelIndex & parent) const232 QModelIndex ShortcutsModel::index(int row, int column,
233 const QModelIndex& parent) const
234 {
235 if (parent.isValid()) {
236 const ShortcutGroup* group;
237 if ((group = shortcutGroupForIndex(parent)) != nullptr &&
238 column >= 0 && column < NumColumns &&
239 row >= 0 && row <= group->size()) {
240 return createIndex(row, column, parent.row());
241 }
242 } else {
243 if (column == 0 &&
244 row >= 0 && row < m_shortcutGroups.size()) {
245 return createIndex(row, column, TopLevelId);
246 }
247 }
248 return QModelIndex();
249 }
250
251 /**
252 * Get parent of item.
253 * @param index model index of item
254 * @return model index of parent item
255 */
parent(const QModelIndex & index) const256 QModelIndex ShortcutsModel::parent(const QModelIndex& index) const
257 {
258 int id = index.internalId();
259 if (id >= 0 && id < m_shortcutGroups.size()) {
260 return createIndex(id, 0, TopLevelId);
261 }
262 return QModelIndex();
263 }
264
265 /**
266 * Register an action.
267 *
268 * @param action action to be added to model
269 * @param context context of action
270 */
registerAction(QAction * action,const QString & context)271 void ShortcutsModel::registerAction(QAction* action, const QString& context)
272 {
273 ShortcutItem item(action);
274 ShortcutGroup group(context);
275
276 auto it = m_shortcutGroups.begin(); // clazy:exclude=detaching-member
277 for (; it != m_shortcutGroups.end(); ++it) {
278 if (it->context() == group.context()) {
279 it->append(item);
280 break;
281 }
282 }
283 if (it == m_shortcutGroups.end()) {
284 group.append(item);
285 m_shortcutGroups.append(group);
286 }
287 }
288
289 /**
290 * Unregister an action.
291 *
292 * @param action action to be removed from model
293 * @param context context of action
294 */
unregisterAction(QAction * action,const QString & context)295 void ShortcutsModel::unregisterAction(QAction* action, const QString& context)
296 {
297 for (auto git = m_shortcutGroups.begin(); git != m_shortcutGroups.end(); ++git) { // clazy:exclude=detaching-member
298 if (git->context() == context) {
299 for (auto iit = git->begin(); iit != git->end(); ++iit) {
300 if (iit->action() == action) {
301 git->erase(iit);
302 break;
303 }
304 }
305 if (git->isEmpty()) {
306 m_shortcutGroups.erase(git);
307 }
308 break;
309 }
310 }
311 }
312
313 /**
314 * Get mapping of shortcut names to key sequences.
315 * @return shortcut map.
316 */
shortcutsMap() const317 QMap<QString, QKeySequence> ShortcutsModel::shortcutsMap() const
318 {
319 QMap<QString, QKeySequence> map;
320 for (auto git = m_shortcutGroups.constBegin();
321 git != m_shortcutGroups.constEnd();
322 ++git) {
323 for (auto iit = git->constBegin(); iit != git->constEnd(); ++iit) {
324 if (const QAction* action = iit->action()) {
325 QString name = action->objectName();
326 if (!name.isEmpty()) {
327 map.insert(name, action->shortcut());
328 }
329 }
330 }
331 }
332 return map;
333 }
334
335 /**
336 * Assign the shortcuts which have been changed to their actions.
337 *
338 * @return true if there was at least one shortcut changed
339 */
assignChangedShortcuts()340 bool ShortcutsModel::assignChangedShortcuts()
341 {
342 bool changed = false;
343 for (auto git = m_shortcutGroups.begin(); git != m_shortcutGroups.end(); ++git) { // clazy:exclude=detaching-member
344 for (auto iit = git->begin(); iit != git->end(); ++iit) {
345 if (iit->isCustomShortcutChanged()) {
346 iit->assignCustomShortcut();
347 changed = true;
348 }
349 }
350 }
351 return changed;
352 }
353
354 /**
355 * Forget about all changed shortcuts.
356 */
discardChangedShortcuts()357 void ShortcutsModel::discardChangedShortcuts()
358 {
359 for (auto git = m_shortcutGroups.begin(); git != m_shortcutGroups.end(); ++git) { // clazy:exclude=detaching-member
360 for (auto iit = git->begin(); iit != git->end(); ++iit) {
361 iit->revertCustomShortcut();
362 }
363 }
364 }
365
366 /**
367 * Clear all shortcuts to their default values.
368 */
clearShortcuts()369 void ShortcutsModel::clearShortcuts()
370 {
371 beginResetModel();
372 for (auto git = m_shortcutGroups.begin(); git != m_shortcutGroups.end(); ++git) { // clazy:exclude=detaching-member
373 for (auto iit = git->begin(); iit != git->end(); ++iit) {
374 iit->clearCustomShortcut();
375 }
376 }
377 endResetModel();
378 }
379
380 /**
381 * Save the shortcuts to a given configuration.
382 *
383 * @param config configuration settings
384 */
writeToConfig(ISettings * config) const385 void ShortcutsModel::writeToConfig(ISettings* config) const
386 {
387 config->beginGroup(QLatin1String("Shortcuts"));
388 config->remove(QLatin1String(""));
389 for (auto git = m_shortcutGroups.constBegin();
390 git != m_shortcutGroups.constEnd();
391 ++git) {
392 for (auto iit = git->constBegin(); iit != git->constEnd(); ++iit) {
393 QString actionName(iit->action() ? iit->action()->objectName()
394 : QLatin1String(""));
395 if (!actionName.isEmpty()) {
396 if (iit->isCustomShortcutActive()) {
397 config->setValue(actionName, iit->customShortcut());
398 }
399 } else {
400 qWarning("Action %s does not have an object name",
401 qPrintable(iit->actionText()));
402 }
403 }
404 }
405 config->endGroup();
406 }
407
408 /**
409 * Read the shortcuts from a given configuration.
410 *
411 * @param config configuration settings
412 */
readFromConfig(ISettings * config)413 void ShortcutsModel::readFromConfig(ISettings* config)
414 {
415 config->beginGroup(QLatin1String("Shortcuts"));
416 for (auto git = m_shortcutGroups.begin(); git != m_shortcutGroups.end(); ++git) { // clazy:exclude=detaching-member
417 for (auto iit = git->begin(); iit != git->end(); ++iit) {
418 QString actionName(iit->action() ? iit->action()->objectName()
419 : QLatin1String(""));
420 if (!actionName.isEmpty() && config->contains(actionName)) {
421 QString keyStr(config->value(actionName, QString()).toString());
422 // Previous versions stored native text, check if it is such a
423 // string and try to convert it.
424 if (QKeySequence::fromString(keyStr, QKeySequence::PortableText)
425 .toString(QKeySequence::PortableText) != keyStr) {
426 QKeySequence nativeKeySequence =
427 QKeySequence::fromString(keyStr, QKeySequence::NativeText);
428 if (nativeKeySequence.toString(QKeySequence::NativeText) == keyStr) {
429 QString nativeKeyStr = keyStr;
430 keyStr = nativeKeySequence.toString(QKeySequence::PortableText);
431 qWarning("Converting shortcut '%s' to '%s'",
432 qPrintable(nativeKeyStr), qPrintable(keyStr));
433 }
434 }
435 iit->setCustomShortcut(keyStr);
436 iit->assignCustomShortcut();
437 }
438 }
439 }
440 config->endGroup();
441 }
442
443
ShortcutItem(QAction * act)444 ShortcutsModel::ShortcutItem::ShortcutItem(QAction* act)
445 : m_action(act), m_defaultShortcut(m_action->shortcut().toString())
446 {
447 }
448
setCustomShortcut(const QString & shortcut)449 void ShortcutsModel::ShortcutItem::setCustomShortcut(const QString& shortcut)
450 {
451 m_customShortcut = shortcut != m_defaultShortcut ? shortcut : QString();
452 }
453
revertCustomShortcut()454 void ShortcutsModel::ShortcutItem::revertCustomShortcut()
455 {
456 m_customShortcut = m_oldCustomShortcut;
457 }
458
clearCustomShortcut()459 void ShortcutsModel::ShortcutItem::clearCustomShortcut()
460 {
461 m_customShortcut.clear();
462 }
463
assignCustomShortcut()464 void ShortcutsModel::ShortcutItem::assignCustomShortcut()
465 {
466 m_action->setShortcut(QKeySequence(activeShortcut()));
467 m_oldCustomShortcut = m_customShortcut;
468 }
469
actionText() const470 QString ShortcutsModel::ShortcutItem::actionText() const
471 {
472 return m_action ? m_action->text().remove(QLatin1Char('&'))
473 : QLatin1String("");
474 }
475
476
ShortcutGroup(const QString & ctx)477 ShortcutsModel::ShortcutGroup::ShortcutGroup(const QString& ctx)
478 : m_context(ctx)
479 {
480 m_context.remove(QLatin1Char('&'));
481 }
482