// SPDX-License-Identifier: LGPL-2.1-or-later /*************************************************************************** * Copyright (c) 2005 Werner Mayer * * * * This file is part of the FreeCAD CAx development system. * * * * This library is free software; you can redistribute it and/or * * modify it under the terms of the GNU Library General Public * * License as published by the Free Software Foundation; either * * version 2 of the License, or (at your option) any later version. * * * * This library is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU Library General Public License for more details. * * * * You should have received a copy of the GNU Library General Public * * License along with this library; see the file COPYING.LIB. If not, * * write to the Free Software Foundation, Inc., 59 Temple Place, * * Suite 330, Boston, MA 02111-1307, USA * * * ***************************************************************************/ #include #include #include #include #include #include #include #include #include "Dialogs/DlgKeyboardImp.h" #include "ui_DlgKeyboard.h" #include "Action.h" #include "Application.h" #include "BitmapFactory.h" #include "Command.h" #include "Window.h" #include "PrefWidgets.h" #include "ShortcutManager.h" #include "CommandCompleter.h" using namespace Gui::Dialog; namespace Gui { namespace Dialog { using GroupMap = std::vector>; struct GroupMap_find { const QLatin1String& item; explicit GroupMap_find(const QLatin1String& item) : item(item) {} bool operator()(const std::pair& elem) const { return elem.first == item; } }; } // namespace Dialog } // namespace Gui /* TRANSLATOR Gui::Dialog::DlgCustomKeyboardImp */ /** * Constructs a DlgCustomKeyboardImp which is a child of 'parent', with the * name 'name' and widget flags set to 'f' * * The dialog will by default be modeless, unless you set 'modal' to * true to construct a modal dialog. */ DlgCustomKeyboardImp::DlgCustomKeyboardImp(QWidget* parent) : CustomizeActionPage(parent) , ui(new Ui_DlgCustomKeyboard) , firstShow(true) { ui->setupUi(this); setupConnections(); // Force create actions for all commands with shortcut to register with ShortcutManager for (auto cmd : Application::Instance->commandManager().getAllCommands()) { if (cmd->getShortcut().size()) { cmd->initAction(); } } QObject::connect( ShortcutManager::instance(), &ShortcutManager::shortcutChanged, this, [](const char* cmdName) { if (auto cmd = Application::Instance->commandManager().getCommandByName(cmdName)) { cmd->initAction(); } } ); conn = initCommandWidgets( ui->commandTreeWidget, nullptr, ui->categoryBox, ui->editCommand, ui->assignedTreeWidget, ui->buttonUp, ui->buttonDown, ui->editShortcut, ui->accelLineEditShortcut ); ui->shortcutTimeout->onRestore(); QTimer* timer = new QTimer(this); QObject::connect(ui->shortcutTimeout, qOverload(&QSpinBox::valueChanged), timer, [=](int) { timer->start(100); }); QObject::connect(timer, &QTimer::timeout, [this]() { ui->shortcutTimeout->onSave(); }); } /** Destroys the object and frees any allocated resources */ DlgCustomKeyboardImp::~DlgCustomKeyboardImp() = default; void DlgCustomKeyboardImp::setupConnections() { // clang-format off connect(ui->categoryBox, qOverload(&QComboBox::activated), this, &DlgCustomKeyboardImp::onCategoryBoxActivated); connect(ui->commandTreeWidget, &QTreeWidget::currentItemChanged, this, &DlgCustomKeyboardImp::onCommandTreeWidgetCurrentItemChanged); connect(ui->buttonAssign, &QPushButton::clicked, this, &DlgCustomKeyboardImp::onButtonAssignClicked); connect(ui->buttonClear, &QPushButton::clicked, this, &DlgCustomKeyboardImp::onButtonClearClicked); connect(ui->buttonReset, &QPushButton::clicked, this, &DlgCustomKeyboardImp::onButtonResetClicked); connect(ui->buttonResetAll, &QPushButton::clicked, this, &DlgCustomKeyboardImp::onButtonResetAllClicked); connect(ui->editShortcut, &AccelLineEdit::keySequenceChanged, this, &DlgCustomKeyboardImp::onEditShortcutTextChanged); // clang-format on } void DlgCustomKeyboardImp::initCommandCompleter( QLineEdit* edit, QComboBox* combo, QTreeWidget* commandTreeWidget, QTreeWidgetItem* separatorItem ) { edit->setPlaceholderText(tr("Type to search…")); auto completer = new CommandCompleter(edit, edit); QObject::connect(completer, &CommandCompleter::commandActivated, [=](const QByteArray& name) { CommandManager& cCmdMgr = Application::Instance->commandManager(); Command* cmd = cCmdMgr.getCommandByName(name.constData()); if (!cmd) { return; } QString group = QString::fromLatin1(cmd->getGroupName()); int index = combo->findData(group); if (index < 0) { return; } if (index != combo->currentIndex()) { QSignalBlocker blocker(combo); combo->setCurrentIndex(index); populateCommandList(commandTreeWidget, separatorItem, combo); } for (int i = 0; i < commandTreeWidget->topLevelItemCount(); ++i) { QTreeWidgetItem* item = commandTreeWidget->topLevelItem(i); if (item->data(1, Qt::UserRole).toByteArray() == name) { commandTreeWidget->setCurrentItem(item); return; } } }); } void DlgCustomKeyboardImp::populateCommandList( QTreeWidget* commandTreeWidget, QTreeWidgetItem* separatorItem, QComboBox* combo ) { QByteArray current; if (auto item = commandTreeWidget->currentItem()) { current = item->data(1, Qt::UserRole).toByteArray(); } if (separatorItem) { commandTreeWidget->takeTopLevelItem(commandTreeWidget->indexOfTopLevelItem(separatorItem)); } commandTreeWidget->clear(); if (separatorItem) { commandTreeWidget->addTopLevelItem(separatorItem); } CommandManager& cCmdMgr = Application::Instance->commandManager(); auto group = combo->itemData(combo->currentIndex(), Qt::UserRole).toByteArray(); auto cmds = group == "All" ? cCmdMgr.getAllCommands() : cCmdMgr.getGroupCommands(group.constData()); QTreeWidgetItem* currentItem = nullptr; for (const Command* cmd : cmds) { QTreeWidgetItem* item = new QTreeWidgetItem(commandTreeWidget); item->setText(1, Action::commandMenuText(cmd)); item->setToolTip(1, Action::commandToolTip(cmd)); item->setData(1, Qt::UserRole, QByteArray(cmd->getName())); item->setSizeHint(0, QSize(32, 32)); if (auto pixmap = cmd->getPixmap()) { item->setIcon(0, BitmapFactory().iconFromTheme(pixmap)); } item->setText(2, cmd->getShortcut()); if (auto accel = cmd->getAccel()) { item->setText(3, QKeySequence(QString::fromLatin1(accel)).toString()); } if (current == cmd->getName()) { currentItem = item; } } if (currentItem) { commandTreeWidget->setCurrentItem(currentItem); } commandTreeWidget->resizeColumnToContents(2); commandTreeWidget->resizeColumnToContents(3); } fastsignals::connection DlgCustomKeyboardImp::initCommandList( QTreeWidget* commandTreeWidget, QTreeWidgetItem* separatorItem, QComboBox* combo ) { QStringList labels; labels << tr("Icon") << tr("Command") << tr("Shortcut") << tr("Default"); commandTreeWidget->setHeaderLabels(labels); commandTreeWidget->setIconSize(QSize(32, 32)); commandTreeWidget->header()->setSectionResizeMode(0, QHeaderView::ResizeToContents); commandTreeWidget->header()->setSectionResizeMode(2, QHeaderView::ResizeToContents); commandTreeWidget->header()->setSectionResizeMode(3, QHeaderView::ResizeToContents); populateCommandGroups(combo); // Using a timer to respond to command change for performance, and also // because macro command may be added before proper initialization (null // menu text, etc.) QTimer* timer = new QTimer(combo); timer->setSingleShot(true); QObject::connect(timer, &QTimer::timeout, [=]() { populateCommandGroups(combo); populateCommandList(commandTreeWidget, separatorItem, combo); }); QObject::connect(ShortcutManager::instance(), &ShortcutManager::shortcutChanged, timer, [timer]() { timer->start(100); }); QObject::connect(combo, qOverload(&QComboBox::activated), timer, [timer]() { timer->start(100); }); return Application::Instance->commandManager().signalChanged.connect([timer]() { timer->start(100); }); } void DlgCustomKeyboardImp::initPriorityList( QTreeWidget* priorityList, QAbstractButton* buttonUp, QAbstractButton* buttonDown ) { QStringList labels; labels << tr("Name") << tr("Title"); priorityList->setHeaderLabels(labels); priorityList->header()->hide(); priorityList->header()->setSectionResizeMode(0, QHeaderView::ResizeToContents); priorityList->header()->setSectionResizeMode(1, QHeaderView::ResizeToContents); auto updatePriorityList = [priorityList](bool up) { auto item = priorityList->currentItem(); if (!item) { return; } int index = priorityList->indexOfTopLevelItem(item); if (index < 0) { return; } if ((index == 0 && up) || (index == priorityList->topLevelItemCount() - 1 && !up)) { return; } std::vector actions; for (int i = 0; i < priorityList->topLevelItemCount(); ++i) { auto item = priorityList->topLevelItem(i); actions.push_back(item->data(0, Qt::UserRole).toByteArray()); } auto it = actions.begin() + index; auto itNext = up ? it - 1 : it + 1; std::swap(*it, *itNext); ShortcutManager::instance()->setPriorities(actions); }; QObject::connect(buttonUp, &QAbstractButton::clicked, [=]() { updatePriorityList(true); }); QObject::connect(buttonDown, &QAbstractButton::clicked, [=]() { updatePriorityList(false); }); QObject::connect(priorityList, &QTreeWidget::currentItemChanged, [=](QTreeWidgetItem* item) { buttonUp->setEnabled(item != nullptr); buttonDown->setEnabled(item != nullptr); }); } fastsignals::connection DlgCustomKeyboardImp::initCommandWidgets( QTreeWidget* commandTreeWidget, QTreeWidgetItem* separatorItem, QComboBox* comboGroups, QLineEdit* editCommand, QTreeWidget* priorityList, QAbstractButton* buttonUp, QAbstractButton* buttonDown, Gui::AccelLineEdit* editShortcut, Gui::AccelLineEdit* currentShortcut ) { initCommandCompleter(editCommand, comboGroups, commandTreeWidget, separatorItem); auto conn = initCommandList(commandTreeWidget, separatorItem, comboGroups); if (priorityList && buttonUp && buttonDown) { initPriorityList(priorityList, buttonUp, buttonDown); auto timer = new QTimer(priorityList); timer->setSingleShot(true); if (currentShortcut) { QObject::connect(currentShortcut, &AccelLineEdit::keySequenceChanged, timer, [timer]() { timer->start(200); }); } QObject::connect(editShortcut, &AccelLineEdit::keySequenceChanged, timer, [timer]() { timer->start(200); }); QObject::connect( ShortcutManager::instance(), &ShortcutManager::priorityChanged, timer, [timer]() { timer->start(200); } ); QObject::connect(timer, &QTimer::timeout, [=]() { populatePriorityList(priorityList, editShortcut, currentShortcut); }); } return conn; } void DlgCustomKeyboardImp::populatePriorityList( QTreeWidget* priorityList, Gui::AccelLineEdit* editor, Gui::AccelLineEdit* curShortcut ) { QByteArray current; if (auto currentItem = priorityList->currentItem()) { current = currentItem->data(0, Qt::UserRole).toByteArray(); } priorityList->clear(); QString sc; if (!editor->isEmpty()) { sc = editor->text(); } else if (curShortcut && !curShortcut->isEmpty()) { sc = curShortcut->text(); } auto actionList = ShortcutManager::instance()->getActionsByShortcut(sc); QTreeWidgetItem* currentItem = nullptr; for (const auto& info : actionList) { if (!info.second) { continue; } QTreeWidgetItem* item = new QTreeWidgetItem(priorityList); item->setText(0, QString::fromUtf8(info.first)); item->setText(1, Action::cleanTitle(info.second->text())); item->setToolTip(0, info.second->toolTip()); item->setIcon(0, info.second->icon()); item->setData(0, Qt::UserRole, info.first); if (current == info.first) { currentItem = item; } } priorityList->resizeColumnToContents(0); priorityList->resizeColumnToContents(1); if (currentItem) { priorityList->setCurrentItem(currentItem); } } void DlgCustomKeyboardImp::populateCommandGroups(QComboBox* combo) { CommandManager& cCmdMgr = Application::Instance->commandManager(); std::map sCommands = cCmdMgr.getCommands(); GroupMap groupMap; groupMap.push_back(std::make_pair(QLatin1String("File"), QString())); groupMap.push_back(std::make_pair(QLatin1String("Edit"), QString())); groupMap.push_back(std::make_pair(QLatin1String("View"), QString())); groupMap.push_back(std::make_pair(QLatin1String("Standard-View"), QString())); groupMap.push_back(std::make_pair(QLatin1String("Tools"), QString())); groupMap.push_back(std::make_pair(QLatin1String("Window"), QString())); groupMap.push_back(std::make_pair(QLatin1String("Help"), QString())); groupMap.push_back( std::make_pair(QLatin1String("Macros"), qApp->translate("Gui::MacroCommand", "Macros")) ); for (const auto& sCommand : sCommands) { QLatin1String group(sCommand.second->getGroupName()); QString text = sCommand.second->translatedGroupName(); GroupMap::iterator jt; jt = std::find_if(groupMap.begin(), groupMap.end(), GroupMap_find(group)); if (jt != groupMap.end()) { if (jt->second.isEmpty()) { jt->second = text; } } else { groupMap.push_back(std::make_pair(group, text)); } } groupMap.push_back(std::make_pair(QLatin1String("All"), tr("All"))); for (const auto& it : groupMap) { if (combo->findData(it.first) < 0) { combo->addItem(it.second); combo->setItemData(combo->count() - 1, QVariant(it.first), Qt::UserRole); } } } void DlgCustomKeyboardImp::showEvent(QShowEvent* e) { Q_UNUSED(e); // If we did this already in the constructor we wouldn't get the vertical scrollbar if needed. // The problem was noticed with Qt 4.1.4 but may arise with any later version. if (firstShow) { ui->categoryBox->activated(ui->categoryBox->currentIndex()); firstShow = false; } } /** Shows the description for the corresponding command */ void DlgCustomKeyboardImp::onCommandTreeWidgetCurrentItemChanged(QTreeWidgetItem* item) { if (!item) { return; } QVariant data = item->data(1, Qt::UserRole); QByteArray name = data.toByteArray(); // command name CommandManager& cCmdMgr = Application::Instance->commandManager(); Command* cmd = cCmdMgr.getCommandByName(name.constData()); if (cmd) { QKeySequence ks = ShortcutManager::instance()->getShortcut(cmd->getName(), cmd->getAccel()); QKeySequence ks2 = QString::fromLatin1(cmd->getAccel()); QKeySequence ks3 = ui->editShortcut->text(); if (ks.isEmpty()) { ui->accelLineEditShortcut->clear(); } else { ui->accelLineEditShortcut->setKeySequence(ks); } ui->buttonAssign->setEnabled(!ui->editShortcut->text().isEmpty() && (ks != ks3)); ui->buttonReset->setEnabled((ks != ks2)); } } /** Shows all commands of this category */ void DlgCustomKeyboardImp::onCategoryBoxActivated(int) { ui->buttonAssign->setEnabled(false); ui->buttonReset->setEnabled(false); ui->accelLineEditShortcut->clear(); ui->editShortcut->clear(); } void DlgCustomKeyboardImp::setShortcutOfCurrentAction(const QString& accelText) { QTreeWidgetItem* item = ui->commandTreeWidget->currentItem(); if (!item) { return; } QVariant data = item->data(1, Qt::UserRole); QByteArray name = data.toByteArray(); // command name QString portableText; if (!accelText.isEmpty()) { QKeySequence shortcut = accelText; portableText = shortcut.toString(QKeySequence::PortableText); ui->accelLineEditShortcut->setKeySequence(shortcut); ui->editShortcut->clear(); } else { ui->accelLineEditShortcut->clear(); ui->editShortcut->clear(); } ShortcutManager::instance()->setShortcut(name, portableText.toLatin1()); ui->buttonAssign->setEnabled(false); ui->buttonReset->setEnabled(true); } /** Assigns a new accelerator to the selected command. */ void DlgCustomKeyboardImp::onButtonAssignClicked() { setShortcutOfCurrentAction(ui->editShortcut->text()); } /** Clears the accelerator of the selected command. */ void DlgCustomKeyboardImp::onButtonClearClicked() { setShortcutOfCurrentAction(QString()); } /** Resets the accelerator of the selected command to the default. */ void DlgCustomKeyboardImp::onButtonResetClicked() { QTreeWidgetItem* item = ui->commandTreeWidget->currentItem(); if (!item) { return; } QVariant data = item->data(1, Qt::UserRole); QByteArray name = data.toByteArray(); // command name ShortcutManager::instance()->reset(name); QString txt = ShortcutManager::instance()->getShortcut(name); ui->accelLineEditShortcut->setKeySequence(QKeySequence(txt)); ui->buttonReset->setEnabled(false); } /** Resets the accelerator of all commands to the default. */ void DlgCustomKeyboardImp::onButtonResetAllClicked() { ShortcutManager::instance()->resetAll(); ui->buttonReset->setEnabled(false); } /** Checks for an already occupied shortcut. */ void DlgCustomKeyboardImp::onEditShortcutTextChanged(const QKeySequence&) { QTreeWidgetItem* item = ui->commandTreeWidget->currentItem(); if (item) { QVariant data = item->data(1, Qt::UserRole); QByteArray name = data.toByteArray(); // command name CommandManager& cCmdMgr = Application::Instance->commandManager(); Command* cmd = cCmdMgr.getCommandByName(name.constData()); if (!ui->editShortcut->isEmpty()) { ui->buttonAssign->setEnabled(true); } else { if (cmd && cmd->getAction() && cmd->getAction()->shortcut().isEmpty()) { ui->buttonAssign->setEnabled(false); // both key sequences are empty } } } } void DlgCustomKeyboardImp::onAddMacroAction(const QByteArray&) {} void DlgCustomKeyboardImp::onRemoveMacroAction(const QByteArray&) {} void DlgCustomKeyboardImp::onModifyMacroAction(const QByteArray&) { QVariant data = ui->categoryBox->itemData(ui->categoryBox->currentIndex(), Qt::UserRole); QString group = data.toString(); if (group == QLatin1String("Macros")) { ui->categoryBox->activated(ui->categoryBox->currentIndex()); } } void DlgCustomKeyboardImp::changeEvent(QEvent* e) { if (e->type() == QEvent::LanguageChange) { ui->retranslateUi(this); int count = ui->categoryBox->count(); CommandManager& cCmdMgr = Application::Instance->commandManager(); for (int i = 0; i < count; i++) { QVariant data = ui->categoryBox->itemData(i, Qt::UserRole); std::vector aCmds = cCmdMgr.getGroupCommands(data.toByteArray()); if (!aCmds.empty()) { QString text = aCmds[0]->translatedGroupName(); ui->categoryBox->setItemText(i, text); } } ui->categoryBox->activated(ui->categoryBox->currentIndex()); } else if (e->type() == QEvent::StyleChange) { ui->categoryBox->activated(ui->categoryBox->currentIndex()); } QWidget::changeEvent(e); } #include "moc_DlgKeyboardImp.cpp"