/* This file is part of the KDE libraries SPDX-FileCopyrightText: 1998 Mark Donohoe SPDX-FileCopyrightText: 1997 Nicolas Hadacek SPDX-FileCopyrightText: 1998 Matthias Ettrich SPDX-FileCopyrightText: 2001 Ellis Whitehead SPDX-FileCopyrightText: 2006 Hamish Rodda SPDX-FileCopyrightText: 2007 Roberto Raggi SPDX-FileCopyrightText: 2007 Andreas Hartmetz SPDX-FileCopyrightText: 2008 Michael Jansen SPDX-License-Identifier: LGPL-2.0-or-later */ #include "config-xmlgui.h" #include "kshortcutseditor.h" // The following is needed for KShortcutsEditorPrivate #include "debug.h" #include "kshortcutsdialog_p.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #if HAVE_GLOBALACCEL #include #endif #include "kactioncategory.h" #include "kactioncollection.h" #include //--------------------------------------------------------------------- // KShortcutsEditor //--------------------------------------------------------------------- KShortcutsEditor::KShortcutsEditor(KActionCollection *collection, QWidget *parent, ActionTypes actionType, LetterShortcuts allowLetterShortcuts) : QWidget(parent) , d(new KShortcutsEditorPrivate(this)) { d->initGUI(actionType, allowLetterShortcuts); addCollection(collection); } KShortcutsEditor::KShortcutsEditor(QWidget *parent, ActionTypes actionType, LetterShortcuts allowLetterShortcuts) : QWidget(parent) , d(new KShortcutsEditorPrivate(this)) { d->initGUI(actionType, allowLetterShortcuts); } KShortcutsEditor::~KShortcutsEditor() { // reset all pending changes undo(); } bool KShortcutsEditor::isModified() const { // Iterate over all items QTreeWidgetItemIterator it(d->ui.list, QTreeWidgetItemIterator::NoChildren); for (; (*it); ++it) { KShortcutsEditorItem *item = dynamic_cast(*it); if (item && item->isModified()) { return true; } } return false; } void KShortcutsEditor::clearCollections() { d->delegate->contractAll(); d->ui.list->clear(); d->actionCollections.clear(); QTimer::singleShot(0, this, &KShortcutsEditor::resizeColumns); } void KShortcutsEditor::addCollection(KActionCollection *collection, const QString &title) { // KXmlGui add action collections unconditionally. If some plugin doesn't // provide actions we don't want to create empty subgroups. if (collection->isEmpty()) { return; } // We add a bunch of items. Prevent the treewidget from permanently // updating. setUpdatesEnabled(false); d->actionCollections.append(collection); // Forward our actionCollections to the delegate which does the conflict // checking. d->delegate->setCheckActionCollections(d->actionCollections); QString displayTitle = title; if (displayTitle.isEmpty()) { // Use the programName (Translated). displayTitle = collection->componentDisplayName(); } KShortcutsEditorPrivate::HierarchyInfo hierarchy; hierarchy.root = d->ui.list->invisibleRootItem(); hierarchy.program = d->findOrMakeItem(hierarchy.root, displayTitle); hierarchy.action = nullptr; // Set to remember which actions we have seen. QSet actionsSeen; // Add all categories in their own subtree below the collections root node const QList categories = collection->findChildren(); for (KActionCategory *category : categories) { hierarchy.action = d->findOrMakeItem(hierarchy.program, category->text()); const auto categoryActions = category->actions(); for (QAction *action : categoryActions) { // Set a marker that we have seen this action actionsSeen.insert(action); d->addAction(action, hierarchy.action); } } // The rest of the shortcuts are added as direct children of the action // collections root node const auto collectionActions = collection->actions(); for (QAction *action : collectionActions) { if (actionsSeen.contains(action)) { continue; } d->addAction(action, hierarchy.program); } // sort the list d->ui.list->sortItems(Name, Qt::AscendingOrder); // Hide Global shortcuts columns if there are no global shortcuts d->setGlobalColumnsHidden(!d->m_hasAnyGlobalShortcuts); // ...And hide local shortcuts columns if there are no local shortcuts d->setLocalColumnsHidden(!d->m_hasAnyLocalShortcuts); // re-enable updating setUpdatesEnabled(true); QTimer::singleShot(0, this, &KShortcutsEditor::resizeColumns); } void KShortcutsEditor::importConfiguration(KConfigBase *config) { d->importConfiguration(config); } void KShortcutsEditor::exportConfiguration(KConfigBase *config) const { Q_ASSERT(config); if (!config) { return; } if (d->actionTypes & KShortcutsEditor::GlobalAction) { QString groupName(QStringLiteral("Global Shortcuts")); KConfigGroup group(config, groupName); for (KActionCollection *collection : std::as_const(d->actionCollections)) { collection->exportGlobalShortcuts(&group, true); } } if (d->actionTypes & ~KShortcutsEditor::GlobalAction) { QString groupName(QStringLiteral("Shortcuts")); KConfigGroup group(config, groupName); for (KActionCollection *collection : std::as_const(d->actionCollections)) { collection->writeSettings(&group, true); } } } void KShortcutsEditor::writeConfiguration(KConfigGroup *config) const { for (KActionCollection *collection : std::as_const(d->actionCollections)) { collection->writeSettings(config); } } // slot void KShortcutsEditor::resizeColumns() { // skip Name column as its section resize mode will take care of resizing to contents for (int i = Name + 1; i < d->ui.list->columnCount(); i++) { if (d->ui.list->isColumnHidden(i)) { continue; } d->ui.list->resizeColumnToContents(i); } } void KShortcutsEditor::save() { writeConfiguration(); // we have to call commit. If we wouldn't do that the changes would be // undone on deletion! That would lead to weird problems. Changes to // Global Shortcuts would vanish completely. Changes to local shortcuts // would vanish for this session. for (QTreeWidgetItemIterator it(d->ui.list); (*it); ++it) { if (KShortcutsEditorItem *item = dynamic_cast(*it)) { item->commit(); } } } void KShortcutsEditor::undo() { // This function used to crash sometimes when invoked by clicking on "cancel" // with Qt 4.2.something. Apparently items were deleted too early by Qt. // It seems to work with 4.3-ish Qt versions. Keep an eye on this. for (QTreeWidgetItemIterator it(d->ui.list); (*it); ++it) { if (KShortcutsEditorItem *item = dynamic_cast(*it)) { item->undo(); } } } // We ask the user here if there are any conflicts, as opposed to undo(). // They don't do the same thing anyway, this just not to confuse any readers. // slot void KShortcutsEditor::allDefault() { d->allDefault(); } void KShortcutsEditor::printShortcuts() const { d->printShortcuts(); } KShortcutsEditor::ActionTypes KShortcutsEditor::actionTypes() const { return d->actionTypes; } void KShortcutsEditor::setActionTypes(ActionTypes actionTypes) { d->setActionTypes(actionTypes); } //--------------------------------------------------------------------- // KShortcutsEditorPrivate //--------------------------------------------------------------------- KShortcutsEditorPrivate::KShortcutsEditorPrivate(KShortcutsEditor *qq) : q(qq) , delegate(nullptr) { } void KShortcutsEditorPrivate::initGUI(KShortcutsEditor::ActionTypes types, KShortcutsEditor::LetterShortcuts allowLetterShortcuts) { actionTypes = types; ui.setupUi(q); q->layout()->setContentsMargins(0, 0, 0, 0); ui.searchFilter->searchLine()->setTreeWidget(ui.list); // Plug into search line ui.list->header()->setSectionResizeMode(QHeaderView::ResizeToContents); ui.list->header()->hideSection(ShapeGesture); // mouse gestures didn't make it in time... ui.list->header()->hideSection(RockerGesture); #if HAVE_GLOBALACCEL const bool hideGlobals = !(actionTypes & KShortcutsEditor::GlobalAction); #else const bool hideGlobals = true; #endif if (hideGlobals) { setGlobalColumnsHidden(true); } else if (!(actionTypes & ~KShortcutsEditor::GlobalAction)) { setLocalColumnsHidden(true); } // Create the Delegate. It is responsible for the KKeySeqeunceWidgets that // really change the shortcuts. delegate = new KShortcutsEditorDelegate(ui.list, allowLetterShortcuts == KShortcutsEditor::LetterShortcutsAllowed); ui.list->setItemDelegate(delegate); ui.list->setSelectionBehavior(QAbstractItemView::SelectItems); ui.list->setSelectionMode(QAbstractItemView::SingleSelection); // we have our own editing mechanism ui.list->setEditTriggers(QAbstractItemView::NoEditTriggers); ui.list->setAlternatingRowColors(true); // TODO listen to changes to global shortcuts QObject::connect(delegate, &KShortcutsEditorDelegate::shortcutChanged, q, [this](const QVariant &newShortcut, const QModelIndex &index) { capturedShortcut(newShortcut, index); }); // hide the editor widget chen its item becomes hidden QObject::connect(ui.searchFilter->searchLine(), &KTreeWidgetSearchLine::hiddenChanged, delegate, &KShortcutsEditorDelegate::hiddenBySearchLine); ui.searchFilter->setFocus(); } void KShortcutsEditorPrivate::setGlobalColumnsHidden(bool hide) { QHeaderView *header = ui.list->header(); header->setSectionHidden(GlobalPrimary, hide); header->setSectionHidden(GlobalAlternate, hide); } void KShortcutsEditorPrivate::setLocalColumnsHidden(bool hide) { QHeaderView *header = ui.list->header(); header->setSectionHidden(LocalPrimary, hide); header->setSectionHidden(LocalAlternate, hide); } void KShortcutsEditorPrivate::setActionTypes(KShortcutsEditor::ActionTypes types) { if (actionTypes == types) { return; } actionTypes = types; // Show/hide columns based on the newly set action types setGlobalColumnsHidden(!(actionTypes & KShortcutsEditor::GlobalAction)); setLocalColumnsHidden(!(actionTypes & ~KShortcutsEditor::GlobalAction)); } bool KShortcutsEditorPrivate::addAction(QAction *action, QTreeWidgetItem *parent) { // If the action name starts with unnamed- spit out a warning and ignore // it. That name will change at will and will break loading and writing QString actionName = action->objectName(); if (actionName.isEmpty() || actionName.startsWith(QLatin1String("unnamed-"))) { qCCritical(DEBUG_KXMLGUI) << "Skipping action without name " << action->text() << "," << actionName << "!"; return false; } const QVariant value = action->property("isShortcutConfigurable"); if (!value.isValid() || value.toBool()) { new KShortcutsEditorItem(parent, action); #if HAVE_GLOBALACCEL if (!m_hasAnyGlobalShortcuts) { // If one global action was found, skip m_hasAnyGlobalShortcuts = KGlobalAccel::self()->hasShortcut(action); } if (!m_hasAnyLocalShortcuts) { // If one local action was found, skip m_hasAnyLocalShortcuts = !KGlobalAccel::self()->hasShortcut(action); } #else m_hasAnyLocalShortcuts = true; #endif return true; } return false; } void KShortcutsEditorPrivate::allDefault() { for (QTreeWidgetItemIterator it(ui.list); (*it); ++it) { if (!(*it)->parent() || (*it)->type() != ActionItem) { continue; } KShortcutsEditorItem *item = static_cast(*it); QAction *act = item->m_action; QList defaultShortcuts = act->property("defaultShortcuts").value>(); if (act->shortcuts() != defaultShortcuts) { QKeySequence primary = defaultShortcuts.isEmpty() ? QKeySequence() : defaultShortcuts.at(0); QKeySequence alternate = defaultShortcuts.size() <= 1 ? QKeySequence() : defaultShortcuts.at(1); changeKeyShortcut(item, LocalPrimary, primary); changeKeyShortcut(item, LocalAlternate, alternate); } #if HAVE_GLOBALACCEL if (KGlobalAccel::self()->shortcut(act) != KGlobalAccel::self()->defaultShortcut(act)) { QList defaultShortcut = KGlobalAccel::self()->defaultShortcut(act); changeKeyShortcut(item, GlobalPrimary, primarySequence(defaultShortcut)); changeKeyShortcut(item, GlobalAlternate, alternateSequence(defaultShortcut)); } #endif } } // static KShortcutsEditorItem *KShortcutsEditorPrivate::itemFromIndex(QTreeWidget *const w, const QModelIndex &index) { QTreeWidgetItem *item = w->itemFromIndex(index); if (item && item->type() == ActionItem) { return static_cast(item); } return nullptr; } QTreeWidgetItem *KShortcutsEditorPrivate::findOrMakeItem(QTreeWidgetItem *parent, const QString &name) { for (int i = 0; i < parent->childCount(); i++) { QTreeWidgetItem *child = parent->child(i); if (child->text(0) == name) { return child; } } QTreeWidgetItem *ret = new QTreeWidgetItem(parent, NonActionItem); ret->setText(0, name); ui.list->expandItem(ret); ret->setFlags(ret->flags() & ~Qt::ItemIsSelectable); return ret; } // private slot void KShortcutsEditorPrivate::capturedShortcut(const QVariant &newShortcut, const QModelIndex &index) { // dispatch to the right handler if (!index.isValid()) { return; } int column = index.column(); KShortcutsEditorItem *item = itemFromIndex(ui.list, index); Q_ASSERT(item); if (column >= LocalPrimary && column <= GlobalAlternate) { changeKeyShortcut(item, column, newShortcut.value()); } } void KShortcutsEditorPrivate::changeKeyShortcut(KShortcutsEditorItem *item, uint column, const QKeySequence &capture) { // The keySequence we get is cleared by KKeySequenceWidget. No conflicts. if (capture == item->keySequence(column)) { return; } item->setKeySequence(column, capture); Q_EMIT q->keyChange(); // force view update item->setText(column, capture.toString(QKeySequence::NativeText)); } void KShortcutsEditorPrivate::importConfiguration(KConfigBase *config) { Q_ASSERT(config); if (!config) { return; } KConfigGroup globalShortcutsGroup(config, QStringLiteral("Global Shortcuts")); if ((actionTypes & KShortcutsEditor::GlobalAction) && globalShortcutsGroup.exists()) { for (QTreeWidgetItemIterator it(ui.list); (*it); ++it) { if (!(*it)->parent()) { continue; } KShortcutsEditorItem *item = static_cast(*it); const QString actionId = item->data(Id).toString(); if (!globalShortcutsGroup.hasKey(actionId)) { continue; } QList sc = QKeySequence::listFromString(globalShortcutsGroup.readEntry(actionId, QString())); changeKeyShortcut(item, GlobalPrimary, primarySequence(sc)); changeKeyShortcut(item, GlobalAlternate, alternateSequence(sc)); } } if (actionTypes & ~KShortcutsEditor::GlobalAction) { const KConfigGroup localShortcutsGroup(config, QStringLiteral("Shortcuts")); for (QTreeWidgetItemIterator it(ui.list); (*it); ++it) { if (!(*it)->parent()) { continue; } KShortcutsEditorItem *item = static_cast(*it); const QString actionId = item->data(Id).toString(); if (!localShortcutsGroup.hasKey(actionId)) { continue; } QList sc = QKeySequence::listFromString(localShortcutsGroup.readEntry(actionId, QString())); changeKeyShortcut(item, LocalPrimary, primarySequence(sc)); changeKeyShortcut(item, LocalAlternate, alternateSequence(sc)); } } } /*TODO for the printShortcuts function Nice to have features (which I'm not sure I can do before may due to more important things): - adjust the general page borders, IMHO they're too wide - add a custom printer options page that allows to filter out all actions that don't have a shortcut set to reduce this list. IMHO this should be optional as people might want to simply print all and when they find a new action that they assign a shortcut they can simply use a pen to fill out the empty space - find a way to align the Main/Alternate/Global entries in the shortcuts column without adding borders. I first did this without a nested table but instead simply added 3 rows and merged the 3 cells in the Action name and description column, but unfortunately I didn't find a way to remove the borders between the 6 shortcut cells. */ void KShortcutsEditorPrivate::printShortcuts() const { // One can't print on wince #ifndef _WIN32_WCE QTreeWidgetItem *root = ui.list->invisibleRootItem(); QTextDocument doc; doc.setDefaultFont(QFontDatabase::systemFont(QFontDatabase::GeneralFont)); QTextCursor cursor(&doc); cursor.beginEditBlock(); QTextCharFormat headerFormat; headerFormat.setProperty(QTextFormat::FontSizeAdjustment, 3); headerFormat.setFontWeight(QFont::Bold); cursor.insertText(i18nc("header for an applications shortcut list", "Shortcuts for %1", QGuiApplication::applicationDisplayName()), headerFormat); QTextCharFormat componentFormat; componentFormat.setProperty(QTextFormat::FontSizeAdjustment, 2); componentFormat.setFontWeight(QFont::Bold); QTextBlockFormat componentBlockFormat = cursor.blockFormat(); componentBlockFormat.setTopMargin(16); componentBlockFormat.setBottomMargin(16); QTextTableFormat tableformat; tableformat.setHeaderRowCount(1); tableformat.setCellPadding(4.0); tableformat.setCellSpacing(0); tableformat.setBorderStyle(QTextFrameFormat::BorderStyle_Solid); tableformat.setBorder(0.5); struct ColumnInfo { QString title; ColumnDesignation column; }; const ColumnInfo shortcutTitleToColumnMap[] = { {i18n("Main:"), LocalPrimary}, {i18n("Alternate:"), LocalAlternate}, {i18n("Global:"), GlobalPrimary}, {i18n("Global alternate:"), GlobalAlternate}, }; for (int i = 0; i < root->childCount(); i++) { QTreeWidgetItem *item = root->child(i); cursor.insertBlock(componentBlockFormat, componentFormat); cursor.insertText(item->text(0)); QTextTable *table = cursor.insertTable(1, 3); table->setFormat(tableformat); int currow = 0; QTextTableCell cell = table->cellAt(currow, 0); QTextCharFormat format = cell.format(); format.setFontWeight(QFont::Bold); cell.setFormat(format); cell.firstCursorPosition().insertText(i18n("Action Name")); cell = table->cellAt(currow, 1); cell.setFormat(format); cell.firstCursorPosition().insertText(i18n("Shortcuts")); cell = table->cellAt(currow, 2); cell.setFormat(format); cell.firstCursorPosition().insertText(i18n("Description")); currow++; for (QTreeWidgetItemIterator it(item); *it; ++it) { if ((*it)->type() != ActionItem) { continue; } KShortcutsEditorItem *editoritem = static_cast(*it); table->insertRows(table->rows(), 1); QVariant data = editoritem->data(Name, Qt::DisplayRole); table->cellAt(currow, 0).firstCursorPosition().insertText(data.toString()); QTextTable *shortcutTable = nullptr; for (const auto &[title, column] : shortcutTitleToColumnMap) { data = editoritem->data(column, Qt::DisplayRole); QString key = data.value().toString(); if (!key.isEmpty()) { if (!shortcutTable) { shortcutTable = table->cellAt(currow, 1).firstCursorPosition().insertTable(1, 2); QTextTableFormat shortcutTableFormat = tableformat; shortcutTableFormat.setCellSpacing(0.0); shortcutTableFormat.setHeaderRowCount(0); shortcutTableFormat.setBorder(0.0); shortcutTable->setFormat(shortcutTableFormat); } else { shortcutTable->insertRows(shortcutTable->rows(), 1); } shortcutTable->cellAt(shortcutTable->rows() - 1, 0).firstCursorPosition().insertText(title); shortcutTable->cellAt(shortcutTable->rows() - 1, 1).firstCursorPosition().insertText(key); } } QAction *action = editoritem->m_action; cell = table->cellAt(currow, 2); format = cell.format(); format.setProperty(QTextFormat::FontSizeAdjustment, -1); cell.setFormat(format); cell.firstCursorPosition().insertHtml(action->whatsThis()); currow++; } cursor.movePosition(QTextCursor::End); } cursor.endEditBlock(); QPrinter printer; QPrintDialog *dlg = new QPrintDialog(&printer, q); if (dlg->exec() == QDialog::Accepted) { doc.print(&printer); } delete dlg; #endif } #include "moc_kshortcutseditor.cpp"