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