Warning, file /multimedia/kid3/src/app/qt/shortcutsmodel.cpp was not indexed or was modified since last indexation (in which case cross-reference links may be missing, inaccurate or erroneous).

0001 /**
0002  * \file shortcutsmodel.cpp
0003  * Keyboard shortcuts configuration tree model.
0004  *
0005  * \b Project: Kid3
0006  * \author Urs Fleisch
0007  * \date 29 Dec 2011
0008  *
0009  * Copyright (C) 2011-2024  Urs Fleisch
0010  *
0011  * This file is part of Kid3.
0012  *
0013  * Kid3 is free software; you can redistribute it and/or modify
0014  * it under the terms of the GNU General Public License as published by
0015  * the Free Software Foundation; either version 2 of the License, or
0016  * (at your option) any later version.
0017  *
0018  * Kid3 is distributed in the hope that it will be useful,
0019  * but WITHOUT ANY WARRANTY; without even the implied warranty of
0020  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
0021  * GNU General Public License for more details.
0022  *
0023  * You should have received a copy of the GNU General Public License
0024  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
0025  */
0026 
0027 #include "shortcutsmodel.h"
0028 #include "isettings.h"
0029 #include <QAction>
0030 #include <QFont>
0031 
0032 namespace {
0033 
0034 constexpr int TopLevelId = -1;
0035 
0036 bool isTopLevelItem(const QModelIndex& index)
0037 {
0038   return index.internalId() == static_cast<quintptr>(TopLevelId);
0039 }
0040 
0041 }
0042 
0043 /**
0044  * Constructor.
0045  * @param parent parent widget
0046  */
0047 ShortcutsModel::ShortcutsModel(QObject* parent) : QAbstractItemModel(parent)
0048 {
0049   setObjectName(QLatin1String("ShortcutsModel"));
0050 }
0051 
0052 /**
0053  * Get item flags for index.
0054  * @param index model index
0055  * @return item flags
0056  */
0057 Qt::ItemFlags ShortcutsModel::flags(const QModelIndex& index) const
0058 {
0059   Qt::ItemFlags itemFlags = QAbstractItemModel::flags(index);
0060   if (index.isValid() && index.column() == ShortcutColumn) {
0061     itemFlags |= Qt::ItemIsEditable;
0062   }
0063   return itemFlags;
0064 }
0065 
0066 /**
0067  * Get group if a model index is a valid index for a group item.
0068  * @param index index to check
0069  * @return group if index is for a group item else 0
0070  */
0071 const ShortcutsModel::ShortcutGroup* ShortcutsModel::shortcutGroupForIndex(
0072     const QModelIndex& index) const
0073 {
0074   if (index.column() == 0 &&
0075       index.row() >= 0 && index.row() < m_shortcutGroups.size() &&
0076       isTopLevelItem(index)) {
0077     return &m_shortcutGroups.at(index.row());
0078   }
0079   return nullptr;
0080 }
0081 
0082 /**
0083  * Get data for a given role.
0084  * @param index model index
0085  * @param role item data role
0086  * @return data for role
0087  */
0088 QVariant ShortcutsModel::data(const QModelIndex& index, int role) const
0089 {
0090   if (index.isValid()) {
0091     if (QModelIndex parentIndex = index.parent(); parentIndex.isValid()) {
0092       if (const ShortcutGroup* group = shortcutGroupForIndex(parentIndex)) {
0093         if (index.row() >= 0 && index.row() < group->size()) {
0094           const ShortcutItem& shortcutItem = group->at(index.row());
0095           if (index.column() == ActionColumn) {
0096             if (role == Qt::DisplayRole) {
0097               return shortcutItem.actionText();
0098             }
0099             if (role == Qt::FontRole) {
0100               if (shortcutItem.isCustomShortcutActive()) {
0101                 QFont font;
0102                 font.setBold(true);
0103                 return font;
0104               }
0105             }
0106           } else if (index.column() == ShortcutColumn) {
0107             if (role == Qt::DisplayRole || role == Qt::EditRole) {
0108               QKeySequence keySequence = QKeySequence::fromString(
0109                     shortcutItem.activeShortcut(), QKeySequence::PortableText);
0110               return keySequence.toString(QKeySequence::NativeText);
0111             }
0112             if (role == Qt::ToolTipRole) {
0113               return tr("Press F2 or double click to edit cell contents.");
0114             }
0115           }
0116         }
0117       }
0118     } else {
0119       if (const ShortcutGroup* group = shortcutGroupForIndex(index)) {
0120         if (role == Qt::DisplayRole) {
0121           return group->context();
0122         }
0123       }
0124     }
0125   }
0126   return QVariant();
0127 }
0128 
0129 /**
0130  * Set data for a given role.
0131  * @param index model index
0132  * @param value data value
0133  * @param role item data role
0134  * @return true if successful
0135  */
0136 bool ShortcutsModel::setData(const QModelIndex& index, const QVariant& value,
0137                              int role)
0138 {
0139   if (index.isValid() && index.column() == ShortcutColumn && role == Qt::EditRole) {
0140     if (QModelIndex parentIndex = index.parent(); parentIndex.isValid()) {
0141       if (auto group =
0142           const_cast<ShortcutGroup*>(shortcutGroupForIndex(parentIndex))) {
0143         if (index.row() >= 0 && index.row() < group->size()) {
0144           ShortcutItem si((*group)[index.row()]);
0145           const QString valueString = !value.isNull()
0146               ? value.value<QKeySequence>().toString(QKeySequence::PortableText)
0147               : QString();
0148           si.setCustomShortcut(valueString);
0149           QString keyString(si.activeShortcut());
0150           if (!keyString.isEmpty()) {
0151             const auto gs = m_shortcutGroups;
0152             for (const ShortcutGroup& g : gs) {
0153               for (const ShortcutItem& i : g) {
0154                 if (i.activeShortcut() == keyString &&
0155                     si.action() != i.action() &&
0156                     (si.action()->shortcutContext() != Qt::WidgetShortcut ||
0157                      i.action()->shortcutContext() != Qt::WidgetShortcut)) {
0158                   emit shortcutAlreadyUsed(keyString, g.context(), i.action());
0159                   return false;
0160                 }
0161               }
0162             }
0163           }
0164           (*group)[index.row()].setCustomShortcut(valueString);
0165           emit dataChanged(index.sibling(index.row(), ActionColumn), index);
0166           emit shortcutSet(keyString, group->context(), si.action());
0167           return true;
0168         }
0169       }
0170     }
0171   }
0172   return false;
0173 }
0174 
0175 /**
0176  * Get data for header section.
0177  * @param section column or row
0178  * @param orientation horizontal or vertical
0179  * @param role item data role
0180  * @return header data for role
0181  */
0182 QVariant ShortcutsModel::headerData(int section, Qt::Orientation orientation,
0183                                     int role) const
0184 {
0185   if (role != Qt::DisplayRole)
0186     return QVariant();
0187   if (orientation == Qt::Horizontal) {
0188     if (section == ActionColumn) {
0189       return tr("Action");
0190     }
0191     if (section == ShortcutColumn) {
0192       return tr("Shortcut");
0193     }
0194   }
0195   return section + 1;
0196 }
0197 
0198 /**
0199  * Get number of rows.
0200  * @param parent parent model index
0201  * @return number of rows, if parent is valid number of children
0202  */
0203 int ShortcutsModel::rowCount(const QModelIndex& parent) const
0204 {
0205   if (parent.isValid()) {
0206     if (const ShortcutGroup* group = shortcutGroupForIndex(parent)) {
0207       return group->size();
0208     }
0209     return 0;
0210   }
0211   return m_shortcutGroups.size();
0212 }
0213 
0214 /**
0215  * Get number of columns.
0216  * @param parent parent model index
0217  * @return number of columns for children of given @a parent
0218  */
0219 int ShortcutsModel::columnCount(const QModelIndex& parent) const
0220 {
0221   Q_UNUSED(parent)
0222   return NumColumns;
0223 }
0224 
0225 /**
0226  * Get model index of item.
0227  * @param row row of item
0228  * @param column column of item
0229  * @param parent index of parent item
0230  * @return model index of item
0231  */
0232 QModelIndex ShortcutsModel::index(int row, int column,
0233                                   const QModelIndex& parent) const
0234 {
0235   if (parent.isValid()) {
0236     if (const ShortcutGroup* group;
0237         (group = shortcutGroupForIndex(parent)) != nullptr &&
0238         column >= 0 && column < NumColumns &&
0239         row >= 0 && row <= group->size()) {
0240       return createIndex(row, column, parent.row());
0241     }
0242   } else {
0243     if (column == 0 &&
0244         row >= 0 && row < m_shortcutGroups.size()) {
0245       return createIndex(row, column, TopLevelId);
0246     }
0247   }
0248   return QModelIndex();
0249 }
0250 
0251 /**
0252  * Get parent of item.
0253  * @param index model index of item
0254  * @return model index of parent item
0255  */
0256 QModelIndex ShortcutsModel::parent(const QModelIndex& index) const
0257 {
0258   if (int id = index.internalId();
0259       id >= 0 && id < m_shortcutGroups.size()) {
0260     return createIndex(id, 0, TopLevelId);
0261   }
0262   return QModelIndex();
0263 }
0264 
0265 /**
0266  * Register an action.
0267  *
0268  * @param action action to be added to model
0269  * @param context context of action
0270  */
0271 void ShortcutsModel::registerAction(QAction* action, const QString& context)
0272 {
0273   ShortcutItem item(action);
0274   ShortcutGroup group(context);
0275 
0276   auto it = m_shortcutGroups.begin(); // clazy:exclude=detaching-member
0277   for (; it != m_shortcutGroups.end(); ++it) {
0278     if (it->context() == group.context()) {
0279       it->append(item);
0280       break;
0281     }
0282   }
0283   if (it == m_shortcutGroups.end()) {
0284     group.append(item);
0285     m_shortcutGroups.append(group);
0286   }
0287 }
0288 
0289 /**
0290  * Unregister an action.
0291  *
0292  * @param action action to be removed from model
0293  * @param context context of action
0294  */
0295 void ShortcutsModel::unregisterAction(const QAction* action, const QString& context)
0296 {
0297   for (auto git = m_shortcutGroups.begin(); git != m_shortcutGroups.end(); ++git) { // clazy:exclude=detaching-member
0298     if (git->context() == context) {
0299       for (auto iit = git->begin(); iit != git->end(); ++iit) {
0300         if (iit->action() == action) {
0301           git->erase(iit);
0302           break;
0303         }
0304       }
0305       if (git->isEmpty()) {
0306         m_shortcutGroups.erase(git);
0307       }
0308       break;
0309     }
0310   }
0311 }
0312 
0313 /**
0314  * Get mapping of shortcut names to key sequences.
0315  * @return shortcut map.
0316  */
0317 QMap<QString, QKeySequence> ShortcutsModel::shortcutsMap() const
0318 {
0319   QMap<QString, QKeySequence> map;
0320   for (auto git = m_shortcutGroups.constBegin();
0321        git != m_shortcutGroups.constEnd();
0322        ++git) {
0323     for (auto iit = git->constBegin(); iit != git->constEnd(); ++iit) {
0324       if (const QAction* action = iit->action()) {
0325         if (QString name = action->objectName(); !name.isEmpty()) {
0326           map.insert(name, action->shortcut());
0327         }
0328       }
0329     }
0330   }
0331   return map;
0332 }
0333 
0334 /**
0335  * Assign the shortcuts which have been changed to their actions.
0336  *
0337  * @return true if there was at least one shortcut changed
0338  */
0339 bool ShortcutsModel::assignChangedShortcuts()
0340 {
0341   bool changed = false;
0342   for (auto git = m_shortcutGroups.begin(); git != m_shortcutGroups.end(); ++git) { // clazy:exclude=detaching-member
0343     for (auto iit = git->begin(); iit != git->end(); ++iit) {
0344       if (iit->isCustomShortcutChanged()) {
0345         iit->assignCustomShortcut();
0346         changed = true;
0347       }
0348     }
0349   }
0350   return changed;
0351 }
0352 
0353 /**
0354  * Forget about all changed shortcuts.
0355  */
0356 void ShortcutsModel::discardChangedShortcuts()
0357 {
0358   for (auto git = m_shortcutGroups.begin(); git != m_shortcutGroups.end(); ++git) { // clazy:exclude=detaching-member
0359     for (auto iit = git->begin(); iit != git->end(); ++iit) {
0360       iit->revertCustomShortcut();
0361     }
0362   }
0363 }
0364 
0365 /**
0366  * Clear all shortcuts to their default values.
0367  */
0368 void ShortcutsModel::clearShortcuts()
0369 {
0370   beginResetModel();
0371   for (auto git = m_shortcutGroups.begin(); git != m_shortcutGroups.end(); ++git) { // clazy:exclude=detaching-member
0372     for (auto iit = git->begin(); iit != git->end(); ++iit) {
0373       iit->clearCustomShortcut();
0374     }
0375   }
0376   endResetModel();
0377 }
0378 
0379 /**
0380  * Save the shortcuts to a given configuration.
0381  *
0382  * @param config configuration settings
0383  */
0384 void ShortcutsModel::writeToConfig(ISettings* config) const
0385 {
0386   config->beginGroup(QLatin1String("Shortcuts"));
0387   config->remove(QLatin1String(""));
0388   for (auto git = m_shortcutGroups.constBegin();
0389        git != m_shortcutGroups.constEnd();
0390        ++git) {
0391     for (auto iit = git->constBegin(); iit != git->constEnd(); ++iit) {
0392       if (QString actionName(iit->action() ? iit->action()->objectName()
0393                                            : QLatin1String(""));
0394           !actionName.isEmpty()) {
0395         if (iit->isCustomShortcutActive()) {
0396           config->setValue(actionName, iit->customShortcut());
0397         }
0398       } else {
0399         qWarning("Action %s does not have an object name",
0400                  qPrintable(iit->actionText()));
0401       }
0402     }
0403   }
0404   config->endGroup();
0405 }
0406 
0407 /**
0408  * Read the shortcuts from a given configuration.
0409  *
0410  * @param config configuration settings
0411  */
0412 void ShortcutsModel::readFromConfig(ISettings* config)
0413 {
0414   config->beginGroup(QLatin1String("Shortcuts"));
0415   for (auto git = m_shortcutGroups.begin(); git != m_shortcutGroups.end(); ++git) { // clazy:exclude=detaching-member
0416     for (auto iit = git->begin(); iit != git->end(); ++iit) {
0417       if (QString actionName(iit->action() ? iit->action()->objectName()
0418                                            : QLatin1String(""));
0419           !actionName.isEmpty() && config->contains(actionName)) {
0420         QString keyStr(config->value(actionName, QString()).toString());
0421         // Previous versions stored native text, check if it is such a
0422         // string and try to convert it.
0423         if (QKeySequence::fromString(keyStr, QKeySequence::PortableText)
0424             .toString(QKeySequence::PortableText) != keyStr) {
0425           if (QKeySequence nativeKeySequence =
0426                 QKeySequence::fromString(keyStr, QKeySequence::NativeText);
0427               nativeKeySequence.toString(QKeySequence::NativeText) == keyStr) {
0428             QString nativeKeyStr = keyStr;
0429             keyStr = nativeKeySequence.toString(QKeySequence::PortableText);
0430             qWarning("Converting shortcut '%s' to '%s'",
0431                      qPrintable(nativeKeyStr), qPrintable(keyStr));
0432           }
0433         }
0434         iit->setCustomShortcut(keyStr);
0435         iit->assignCustomShortcut();
0436       }
0437     }
0438   }
0439   config->endGroup();
0440 }
0441 
0442 
0443 ShortcutsModel::ShortcutItem::ShortcutItem(QAction* act)
0444   : m_action(act), m_defaultShortcut(m_action->shortcut().toString())
0445 {
0446 }
0447 
0448 void ShortcutsModel::ShortcutItem::setCustomShortcut(const QString& shortcut)
0449 {
0450   m_customShortcut = shortcut != m_defaultShortcut ? shortcut : QString();
0451 }
0452 
0453 void ShortcutsModel::ShortcutItem::revertCustomShortcut()
0454 {
0455   m_customShortcut = m_oldCustomShortcut;
0456 }
0457 
0458 void ShortcutsModel::ShortcutItem::clearCustomShortcut()
0459 {
0460   m_customShortcut.clear();
0461 }
0462 
0463 void ShortcutsModel::ShortcutItem::assignCustomShortcut()
0464 {
0465   m_action->setShortcut(QKeySequence(activeShortcut()));
0466   m_oldCustomShortcut = m_customShortcut;
0467 }
0468 
0469 QString ShortcutsModel::ShortcutItem::actionText() const
0470 {
0471   return m_action ? m_action->text().remove(QLatin1Char('&'))
0472                   : QLatin1String("");
0473 }
0474 
0475 
0476 ShortcutsModel::ShortcutGroup::ShortcutGroup(const QString& ctx)
0477   : m_context(ctx)
0478 {
0479   m_context.remove(QLatin1Char('&'));
0480 }