/*************************************************************************** * Copyright (c) 2004 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 #include #include #include #include #include #include #include #include "Action.h" #include "BitmapFactory.h" #include "Command.h" #include "Dialogs/DlgUndoRedo.h" #include "PreferencePages/DlgSettingsWorkbenchesImp.h" #include "Document.h" #include "EditorView.h" #include "Macro.h" #include "ModuleIO.h" #include "MainWindow.h" #include "PythonEditor.h" #include "WhatsThis.h" #include "Widgets.h" #include "Workbench.h" #include "WorkbenchManager.h" #include "WorkbenchSelector.h" #include "ShortcutManager.h" #include "Tools.h" using namespace Gui; using namespace Gui::Dialog; namespace sp = std::placeholders; /** * Constructs an action called \a name with parent \a parent. It also stores a pointer * to the command object. */ Action::Action(Command* pcCmd, QObject* parent) : QObject(parent) , _action(new QAction(this)) , _pcCmd(pcCmd) { _action->setObjectName(QString::fromLatin1(_pcCmd->getName())); _connection = connect(_action, &QAction::triggered, this, &Action::onActivated); } Action::Action(Command* pcCmd, QAction* action, QObject* parent) : QObject(parent) , _action(action) , _pcCmd(pcCmd) { _action->setParent(this); _action->setObjectName(QString::fromLatin1(_pcCmd->getName())); _connection = connect(_action, &QAction::triggered, this, &Action::onActivated); } Action::~Action() { delete _action; } /** * Adds this action to widget \a widget. */ void Action::addTo(QWidget* widget) { widget->addAction(_action); } /** * Activates the command. */ void Action::onActivated() { command()->invoke(0, Command::TriggerAction); } /** * Sets whether the command is toggled. */ void Action::onToggled(bool toggle) { command()->invoke(toggle ? 1 : 0, Command::TriggerAction); } void Action::setCheckable(bool check) { if (check == _action->isCheckable()) { return; } _action->setCheckable(check); if (check) { disconnect(_connection); _connection = connect(_action, &QAction::toggled, this, &Action::onToggled); } else { disconnect(_connection); _connection = connect(_action, &QAction::triggered, this, &Action::onActivated); } } void Action::setChecked(bool check) { _action->setChecked(check); } /*! * \brief Action::setBlockedChecked * \param check * Does the same as \ref setChecked but additionally blocks * any signals. */ void Action::setBlockedChecked(bool check) { QSignalBlocker block(_action); _action->setChecked(check); } bool Action::isChecked() const { return _action->isChecked(); } /** * Sets whether the action is enabled. */ void Action::setEnabled(bool enable) { _action->setEnabled(enable); } bool Action::isEnabled() const { return _action->isEnabled(); } void Action::setVisible(bool visible) { _action->setVisible(visible); } void Action::setShortcut(const QString& key) { _action->setShortcut(key); setToolTip(_tooltip, _title); } QKeySequence Action::shortcut() const { return _action->shortcut(); } void Action::setIcon(const QIcon& icon) { _action->setIcon(icon); } QIcon Action::icon() const { return _action->icon(); } void Action::setStatusTip(const QString& text) { _action->setStatusTip(text); } QString Action::statusTip() const { return _action->statusTip(); } void Action::setText(const QString& text) { _action->setText(text); if (_title.isEmpty()) { setToolTip(_tooltip); } } QString Action::text() const { return _action->text(); } void Action::setToolTip(const QString& text, const QString& title) { _tooltip = text; _title = title; _action->setToolTip(createToolTip( text, title.isEmpty() ? _action->text() : title, _action->font(), _action->shortcut().toString(QKeySequence::NativeText), command() )); } QString Action::cleanTitle(const QString& title) { QString text(title); // Deal with QAction title mnemonic static QRegularExpression re(QStringLiteral("&(.)")); text.replace(re, QStringLiteral("\\1")); // Probably not a good idea to trim ending punctuation #if 0 // Trim line ending punctuation static QRegularExpression rePunct(QStringLiteral("[[:punct:]]+$")); text.replace(rePunct, QString()); #endif return text; } QString Action::commandToolTip(const Command* cmd, bool richFormat) { if (!cmd) { return {}; } if (richFormat) { if (auto action = cmd->getAction()) { return action->_action->toolTip(); } } QString title, tooltip; if (dynamic_cast(cmd)) { if (auto txt = cmd->getMenuText()) { title = QString::fromUtf8(txt); } if (auto txt = cmd->getToolTipText()) { tooltip = QString::fromUtf8(txt); } } else { if (auto txt = cmd->getMenuText()) { title = qApp->translate(cmd->className(), txt); } if (auto txt = cmd->getToolTipText()) { tooltip = qApp->translate(cmd->className(), txt); } } if (!richFormat) { return tooltip; } return createToolTip(tooltip, title, QFont(), cmd->getShortcut(), cmd); } QString Action::commandMenuText(const Command* cmd) { if (!cmd) { return {}; } QString title; if (auto action = cmd->getAction()) { title = action->text(); } else if (dynamic_cast(cmd)) { if (auto txt = cmd->getMenuText()) { title = QString::fromUtf8(txt); } } else { if (auto txt = cmd->getMenuText()) { title = qApp->translate(cmd->className(), txt); } } if (title.isEmpty()) { title = QString::fromUtf8(cmd->getName()); } else { title = cleanTitle(title); } return title; } QString Action::createToolTip( QString helpText, const QString& title, const QFont& font, const QString& shortCut, const Command* command ) { QString text = cleanTitle(title); if (text.isEmpty()) { return helpText; } // The following code tries to make a more useful tooltip by inserting at // the beginning of the tooltip the action title in bold followed by the // shortcut. // // The long winding code is to deal with the fact that Qt will auto wrap // a rich text tooltip but the width is too short. We can escape the auto // wrapping using

. QString shortcut = shortCut; if (!shortcut.isEmpty() && helpText.endsWith(shortcut)) { helpText.resize(helpText.size() - shortcut.size()); } if (!shortcut.isEmpty()) { shortcut = QStringLiteral(" (%1)").arg(shortcut); } QString tooltip = QStringLiteral("

%1%2

") .arg(text.toHtmlEscaped(), shortcut.toHtmlEscaped()); QString cmdName; if (command && command->getName()) { cmdName = QString::fromLatin1(command->getName()); if (auto groupcmd = dynamic_cast(command)) { if (auto act = command->getAction()) { int idx = act->property("defaultAction").toInt(); auto cmd = groupcmd->getCommand(idx); if (cmd && cmd->getName()) { cmdName = QStringLiteral("%1 (%2:%3)") .arg(QString::fromLatin1(cmd->getName()), cmdName) .arg(idx); } } } cmdName = QStringLiteral("

%1

") .arg(cmdName.toHtmlEscaped()); } if (!shortcut.isEmpty() && helpText.endsWith(shortcut)) { helpText.resize(helpText.size() - shortcut.size()); } if (helpText.isEmpty() || helpText == text || helpText == title) { return tooltip + cmdName; } if (Qt::mightBeRichText(helpText)) { // already rich text, so let it be to avoid duplicated unwrapping return tooltip + helpText + cmdName; } tooltip += QStringLiteral("

"); // If the user supplied tooltip contains line break, we shall honour it. if (helpText.indexOf(QLatin1Char('\n')) >= 0) { tooltip += helpText.toHtmlEscaped() + QStringLiteral("

"); } else { // If not, try to end the non wrapping paragraph at some pre defined // width, so that the following text can wrap at that width. float tipWidth = 400; QFontMetrics fm(font); int width = QtTools::horizontalAdvance(fm, helpText); if (width <= tipWidth) { tooltip += helpText.toHtmlEscaped() + QStringLiteral("

"); } else { int index = tipWidth / width * helpText.size(); // Try to only break at white space for (int i = 0; i < 50 && index < helpText.size(); ++i, ++index) { if (helpText[index] == QLatin1Char(' ')) { break; } } tooltip += helpText.left(index).toHtmlEscaped() + QStringLiteral("

") + helpText.right(helpText.size() - index).trimmed().toHtmlEscaped(); } } return tooltip + cmdName; } QString Action::toolTip() const { return _tooltip; } void Action::setWhatsThis(const QString& text) { _action->setWhatsThis(text); } QString Action::whatsThis() const { return _action->whatsThis(); } void Action::setMenuRole(QAction::MenuRole menuRole) { _action->setMenuRole(menuRole); } // -------------------------------------------------------------------- /** * Constructs an action called \a name with parent \a parent. It also stores a pointer * to the command object. */ ActionGroup::ActionGroup(Command* pcCmd, QObject* parent) : Action(pcCmd, parent) , _group(nullptr) , _dropDown(false) , _isMode(false) , _rememberLast(true) { _group = new QActionGroup(this); connect(_group, &QActionGroup::triggered, this, qOverload(&ActionGroup::onActivated)); connect(_group, &QActionGroup::hovered, this, &ActionGroup::onHovered); } ActionGroup::~ActionGroup() { delete _group; } /** * Adds this action to widget \a w. */ void ActionGroup::addTo(QWidget* widget) { // When adding an action that has defined a menu then shortcuts // of the menu actions don't work. To make this working we must // set the menu explicitly. This means calling QAction::setMenu() // and adding this action to the widget doesn't work. if (_dropDown) { if (widget->inherits("QMenu")) { auto menu = new QMenu(widget); QAction* item = qobject_cast(widget)->addMenu(menu); item->setMenuRole(action()->menuRole()); menu->setTitle(action()->text()); menu->addActions(groupAction()->actions()); QObject::connect(menu, &QMenu::aboutToShow, [this, menu]() { Q_EMIT aboutToShow(menu); }); QObject::connect(menu, &QMenu::aboutToHide, [this, menu]() { Q_EMIT aboutToHide(menu); }); } else if (widget->inherits("QToolBar")) { widget->addAction(action()); QToolButton* tb = widget->findChildren().constLast(); tb->setPopupMode(QToolButton::MenuButtonPopup); tb->setObjectName(QStringLiteral("qt_toolbutton_menubutton")); QList acts = groupAction()->actions(); auto menu = new QMenu(tb); menu->addActions(acts); tb->setMenu(menu); QObject::connect(menu, &QMenu::aboutToShow, [this, menu]() { Q_EMIT aboutToShow(menu); }); QObject::connect(menu, &QMenu::aboutToHide, [this, menu]() { Q_EMIT aboutToHide(menu); }); } else { widget->addActions(groupAction()->actions()); // no drop-down } } else { widget->addActions(groupAction()->actions()); } } void ActionGroup::setEnabled(bool check) { Action::setEnabled(check); groupAction()->setEnabled(check); } void ActionGroup::setDisabled(bool check) { Action::setEnabled(!check); groupAction()->setDisabled(check); } void ActionGroup::setExclusive(bool check) { groupAction()->setExclusive(check); } bool ActionGroup::isExclusive() const { return groupAction()->isExclusive(); } void ActionGroup::setVisible(bool check) { Action::setVisible(check); groupAction()->setVisible(check); } void ActionGroup::setRememberLast(bool remember) { _rememberLast = remember; } bool ActionGroup::doesRememberLast() const { return _rememberLast; } QAction* ActionGroup::addAction(QAction* action) { return groupAction()->addAction(action); } QAction* ActionGroup::addAction(const QString& text) { return groupAction()->addAction(text); } QList ActionGroup::actions() const { return groupAction()->actions(); } int ActionGroup::checkedAction() const { auto checked = groupAction()->checkedAction(); return actions().indexOf(checked); } void ActionGroup::setCheckedAction(int index) { auto acts = groupAction()->actions(); QAction* act = acts.at(index); act->setChecked(true); this->setIcon(act->icon()); if (!this->_isMode) { this->action()->setToolTip(act->toolTip()); } this->setProperty("defaultAction", QVariant(index)); } /** * Activates the command. */ void ActionGroup::onActivated() { command()->invoke(this->property("defaultAction").toInt(), Command::TriggerAction); } void ActionGroup::onToggled(bool check) { Q_UNUSED(check) onActivated(); } /** * Activates the command. */ void ActionGroup::onActivated(QAction* act) { if (_rememberLast) { int index = groupAction()->actions().indexOf(act); this->setIcon(act->icon()); if (!this->_isMode) { this->action()->setToolTip(act->toolTip()); } this->setProperty("defaultAction", QVariant(index)); command()->invoke(index, Command::TriggerChildAction); } } /** * Shows tooltip at the right side when hovered. */ void ActionGroup::onHovered(QAction* act) { const auto topLevelWidgets = QApplication::topLevelWidgets(); QMenu* foundMenu = nullptr; for (QWidget* widget : topLevelWidgets) { QList menus = widget->findChildren(); for (QMenu* menu : menus) { if (menu->isVisible() && menu->actions().contains(act)) { foundMenu = menu; break; } } if (foundMenu) { break; } } if (foundMenu) { QRect actionRect = foundMenu->actionGeometry(act); QPoint globalPos = foundMenu->mapToGlobal(actionRect.topRight()); QToolTip::showText(globalPos, act->toolTip(), foundMenu, actionRect); } else { QToolTip::showText(QCursor::pos(), act->toolTip()); } } /* TRANSLATOR Gui::WorkbenchGroup */ WorkbenchGroup::WorkbenchGroup(Command* pcCmd, QObject* parent) : ActionGroup(pcCmd, parent) { refreshWorkbenchList(); // NOLINTBEGIN Application::Instance->signalRefreshWorkbenches.connect( std::bind(&WorkbenchGroup::refreshWorkbenchList, this) ); // NOLINTEND connect(getMainWindow(), &MainWindow::workbenchActivated, this, &WorkbenchGroup::onWorkbenchActivated); } QAction* WorkbenchGroup::getOrCreateAction(const QString& wbName) { if (!actionByWorkbenchName.contains(wbName)) { actionByWorkbenchName[wbName] = new QAction(QApplication::instance()); } return actionByWorkbenchName[wbName]; } void WorkbenchGroup::addTo(QWidget* widget) { if (widget->inherits("QToolBar")) { ParameterGrp::handle hGrp; hGrp = App::GetApplication().GetParameterGroupByPath( "User parameter:BaseApp/Preferences/Workbenches" ); QWidget* workbenchSelectorWidget; if (hGrp->GetInt("WorkbenchSelectorType", 0) == 0) { workbenchSelectorWidget = new WorkbenchComboBox(this, widget); } else { workbenchSelectorWidget = new WorkbenchTabWidget(this, widget); } static_cast(widget)->addWidget(workbenchSelectorWidget); } else if (widget->inherits("QMenu")) { auto menu = qobject_cast(widget); menu = menu->addMenu(action()->text()); menu->addActions(getEnabledWbActions()); connect(this, &WorkbenchGroup::workbenchListRefreshed, this, [menu](QList actions) { menu->clear(); menu->addActions(actions); }); } } void WorkbenchGroup::refreshWorkbenchList() { QStringList enabledWbNames = DlgSettingsWorkbenchesImp::getEnabledWorkbenches(); // Clear the actions. for (QAction* action : actions()) { groupAction()->removeAction(action); } enabledWbsActions.clear(); disabledWbsActions.clear(); std::string activeWbName = WorkbenchManager::instance()->activeName(); // Create action list of enabled wb int index = 0; for (const auto& wbName : enabledWbNames) { QString name = Application::Instance->workbenchMenuText(wbName); QPixmap px = Application::Instance->workbenchIcon(wbName); QString tip = Application::Instance->workbenchToolTip(wbName); QAction* action = getOrCreateAction(wbName); groupAction()->addAction(action); action->setText(name); action->setCheckable(true); action->setData(QVariant(index)); // set the index action->setObjectName(wbName); action->setIcon(px); action->setToolTip(tip); action->setStatusTip(tr("Selects the '%1' workbench").arg(name)); if (index < 9) { action->setShortcut(QKeySequence(QStringLiteral("W,%1").arg(index + 1))); } if (wbName.toStdString() == activeWbName) { action->setChecked(true); } enabledWbsActions.push_back(action); index++; } // Also create action list of disabled wbs QStringList disabledWbNames = DlgSettingsWorkbenchesImp::getDisabledWorkbenches(); for (const auto& wbName : disabledWbNames) { QString name = Application::Instance->workbenchMenuText(wbName); QPixmap px = Application::Instance->workbenchIcon(wbName); QString tip = Application::Instance->workbenchToolTip(wbName); QAction* action = getOrCreateAction(wbName); groupAction()->addAction(action); action->setText(name); action->setCheckable(true); action->setData(QVariant(index)); // set the index action->setObjectName(wbName); action->setIcon(px); action->setToolTip(tip); action->setStatusTip(tr("Select the '%1' workbench").arg(name)); if (wbName.toStdString() == activeWbName) { action->setChecked(true); } disabledWbsActions.push_back(action); index++; } // Signal to the widgets (WorkbenchComboBox & menu) to update the wb list workbenchListRefreshed(enabledWbsActions); } void WorkbenchGroup::onWorkbenchActivated(const QString& name) { // There might be more than only one instance of WorkbenchComboBox there. // However, all of them share the same QAction objects. Thus, if the user // has selected one it also gets checked. Then Application::activateWorkbench // also invokes this slot method by calling the signal workbenchActivated in // MainWindow. If calling activateWorkbench() from within the Python console // the matching action must be set by calling this function. // To avoid to recursively (but only one recursion level) call Application:: // activateWorkbench the method refreshWorkbenchList() shouldn't set the // checked item. // QVariant item = itemData(currentIndex()); for (QAction* action : actions()) { if (action->objectName() == name) { if (!action->isChecked()) { action->trigger(); } break; } } } QList WorkbenchGroup::getEnabledWbActions() const { return enabledWbsActions; } QList WorkbenchGroup::getDisabledWbActions() const { return disabledWbsActions; } // -------------------------------------------------------------------- class RecentFilesAction::Private: public ParameterGrp::ObserverType { public: Private(const Private&) = delete; Private(Private&&) = delete; void operator=(const Private&) = delete; void operator=(Private&&) = delete; Private(RecentFilesAction* master, const char* path) : master(master) { handle = App::GetApplication().GetParameterGroupByPath(path); handle->Attach(this); } ~Private() override { handle->Detach(this); } void OnChange(Base::Subject& sub, const char* reason) override { Q_UNUSED(sub) if (!updating && reason && strcmp(reason, "RecentFiles") == 0) { Base::StateLocker guard(updating); master->restore(); } } void trySaveUserParameter() { // update the XML structure and save the user parameter to disk (#0001989) bool saveParameter = App::GetApplication() .GetParameterGroupByPath("User parameter:BaseApp/Preferences/General") ->GetBool("SaveUserParameter", true); if (saveParameter) { saveUserParameter(); } } void saveUserParameter() { ParameterManager* parmgr = App::GetApplication().GetParameterSet("User parameter"); parmgr->SaveDocument(App::Application::Config()["UserParameter"].c_str()); } public: RecentFilesAction* master; ParameterGrp::handle handle; bool updating = false; }; // -------------------------------------------------------------------- /* TRANSLATOR Gui::RecentFilesAction */ RecentFilesAction::RecentFilesAction(Command* pcCmd, QObject* parent) : ActionGroup(pcCmd, parent) , visibleItems(4) , maximumItems(20) { _pimpl = std::make_unique(this, "User parameter:BaseApp/Preferences/RecentFiles"); restore(); sep.setSeparator(true); sep.setToolTip({}); this->groupAction()->addAction(&sep); //: Empties the list of recent files clearRecentFilesListAction.setText(tr("Clear Recent Files")); clearRecentFilesListAction.setToolTip({}); this->groupAction()->addAction(&clearRecentFilesListAction); auto clearFun = [this, hGrp = _pimpl->handle]() { const size_t recentFilesListSize = hGrp->GetASCIIs("MRU").size(); for (size_t i = 0; i < recentFilesListSize; i++) { const QByteArray key = QStringLiteral("MRU%1").arg(i).toLocal8Bit(); hGrp->SetASCII(key.data(), ""); } restore(); clearRecentFilesListAction.setEnabled(false); }; connect(&clearRecentFilesListAction, &QAction::triggered, this, clearFun); connect( &clearRecentFilesListAction, &QAction::triggered, this, &RecentFilesAction::recentFilesListModified ); } RecentFilesAction::~RecentFilesAction() { _pimpl.reset(nullptr); } /** Adds the new item to the recent files. */ void RecentFilesAction::appendFile(const QString& filename) { // restore the list of recent files QStringList files = this->files(); // if already inside remove and prepend it files.removeAll(filename); files.prepend(filename); setFiles(files); save(); _pimpl->trySaveUserParameter(); clearRecentFilesListAction.setEnabled(true); Q_EMIT recentFilesListModified(); } static QString numberToLabel(int number) { if (number > 0 && number < 10) { // NOLINT: *-magic-numbers return QStringLiteral("&%1").arg(number); } if (number == 10) { // NOLINT: *-magic-numbers return QStringLiteral("1&0"); } // If we have a number greater than 10, we start using the alphabet. // So 11 becomes 'A' and so on. constexpr char lettersStart = 11; constexpr char lettersEnd = lettersStart + ('Z' - 'A'); if (number >= lettersStart && number < lettersEnd) { QChar letter = QChar::fromLatin1('A' + (number - lettersStart)); return QStringLiteral("%1 (&%2)").arg(number).arg(letter); } // Not enough accelerators to cover this number. return QStringLiteral("%1").arg(number); } /** * Set the list of recent files. For each item an action object is * created and added to this action group. */ void RecentFilesAction::setFiles(const QStringList& files) { QList recentFiles = groupAction()->actions(); int numRecentFiles = std::min(recentFiles.count(), files.count()); for (int index = 0; index < numRecentFiles; index++) { QString numberLabel = numberToLabel(index + 1); QFileInfo fi(files[index]); QString fileName {fi.fileName()}; fileName.replace(QLatin1Char('&'), QStringLiteral("&&")); recentFiles[index]->setText(QStringLiteral("%1 %2").arg(numberLabel, fileName)); recentFiles[index]->setStatusTip(tr("Open file %1").arg(files[index])); recentFiles[index]->setToolTip(files[index]); // set the full name that we need later for saving recentFiles[index]->setData(QVariant(index)); recentFiles[index]->setVisible(true); } // if less file names than actions numRecentFiles = std::min(numRecentFiles, this->visibleItems); for (int index = numRecentFiles; index < recentFiles.count(); index++) { if (recentFiles[index] == &sep || recentFiles[index] == &clearRecentFilesListAction) { continue; } recentFiles[index]->setVisible(false); recentFiles[index]->setText(QString()); recentFiles[index]->setToolTip(QString()); } } /** * Returns the list of defined recent files. */ QStringList RecentFilesAction::files() const { QStringList files; QList recentFiles = groupAction()->actions(); for (int index = 0; index < recentFiles.count(); index++) { QString file = recentFiles[index]->toolTip(); if (file.isEmpty()) { break; } files.append(file); } return files; } void RecentFilesAction::activateFile(int id) { // restore the list of recent files QStringList files = this->files(); if (id < 0 || id >= files.count()) { return; // no valid item } QString filename = files[id]; if (!ModuleIO::verifyFile(filename)) { files.removeAll(filename); setFiles(files); save(); } else { ModuleIO::openFile(filename); } } void RecentFilesAction::resizeList(int size) { this->visibleItems = size; int diff = this->visibleItems - this->maximumItems; // create new items if needed for (int i = 0; i < diff; i++) { groupAction()->addAction(QLatin1String(""))->setVisible(false); } setFiles(files()); } /** Loads all recent files from the preferences. */ void RecentFilesAction::restore() { ParameterGrp::handle hGrp = _pimpl->handle; // we want at least 20 items but we do only show the number of files // that is defined in user parameters this->visibleItems = hGrp->GetInt("RecentFiles", this->visibleItems); int count = std::max(this->maximumItems, this->visibleItems); for (int i = 0; i < count; i++) { groupAction()->addAction(QLatin1String(""))->setVisible(false); } std::vector MRU = hGrp->GetASCIIs("MRU"); QStringList files; for (const auto& it : MRU) { auto filePath = QString::fromUtf8(it.c_str()); if (QFileInfo::exists(filePath)) { files.append(filePath); } } setFiles(files); } /** Saves all recent files to the preferences. */ void RecentFilesAction::save() { ParameterGrp::handle hGrp = _pimpl->handle; int count = hGrp->GetInt("RecentFiles", this->visibleItems); // save number of files hGrp->Clear(); // count all set items QList recentFiles = groupAction()->actions(); int num = std::min(count, recentFiles.count()); for (int index = 0; index < num; index++) { QString key = QStringLiteral("MRU%1").arg(index); QString value = recentFiles[index]->toolTip(); if (value.isEmpty()) { break; } hGrp->SetASCII(key.toLatin1(), value.toUtf8()); } Base::StateLocker guard(_pimpl->updating); hGrp->SetInt("RecentFiles", count); // restore } // -------------------------------------------------------------------- /* TRANSLATOR Gui::RecentMacrosAction */ RecentMacrosAction::RecentMacrosAction(Command* pcCmd, QObject* parent) : ActionGroup(pcCmd, parent) , visibleItems(4) , maximumItems(20) { restore(); } /** Adds the new item to the recent files. */ void RecentMacrosAction::appendFile(const QString& filename) { // restore the list of recent files QStringList files = this->files(); // if already inside remove and prepend it files.removeAll(filename); files.prepend(filename); setFiles(files); save(); // update the XML structure and save the user parameter to disk (#0001989) bool saveParameter = App::GetApplication() .GetParameterGroupByPath("User parameter:BaseApp/Preferences/General") ->GetBool("SaveUserParameter", true); if (saveParameter) { ParameterManager* parmgr = App::GetApplication().GetParameterSet("User parameter"); parmgr->SaveDocument(App::Application::Config()["UserParameter"].c_str()); } } /** * Set the list of recent macro files. For each item an action object is * created and added to this action group. */ void RecentMacrosAction::setFiles(const QStringList& files) { ParameterGrp::handle hGrp = App::GetApplication() .GetUserParameter() .GetGroup("BaseApp") ->GetGroup("Preferences") ->GetGroup("RecentMacros"); this->shortcut_modifiers = hGrp->GetASCII("ShortcutModifiers", "Ctrl+Shift+"); this->shortcut_count = std::min(hGrp->GetInt("ShortcutCount", 3), 9); // max = 9, e.g. // Ctrl+Shift+9 this->visibleItems = hGrp->GetInt("RecentMacros", 12); QList recentFiles = groupAction()->actions(); int numRecentFiles = std::min(recentFiles.count(), files.count()); QStringList existingCommands; auto accel_col = QString::fromStdString(shortcut_modifiers); for (int index = 0; index < numRecentFiles; index++) { QFileInfo fi(files[index]); QString numberLabel = numberToLabel(index + 1); recentFiles[index]->setText( QStringLiteral("%1 %2").arg(numberLabel).arg(fi.completeBaseName()) ); recentFiles[index]->setToolTip(files[index]); // set the full name that we need later for saving recentFiles[index]->setData(QVariant(index)); QString accel(tr("none")); if (index < shortcut_count) { auto accel_tmp = QString::fromStdString(shortcut_modifiers); accel_tmp.append(QString::number(index + 1, 10)).toStdString(); auto check = Application::Instance->commandManager().checkAcceleratorForConflicts( qPrintable(accel_tmp) ); if (check) { recentFiles[index]->setShortcut(QKeySequence()); accel_col.append(accel_tmp); existingCommands.append(QLatin1String(check->getName())); } else { accel = accel_tmp; recentFiles[index]->setShortcut(accel); } } recentFiles[index]->setStatusTip( tr("Run macro %1 (Shift+click to edit) keyboard shortcut: %2").arg(files[index], accel) ); recentFiles[index]->setVisible(true); } // if less file names than actions numRecentFiles = std::min(numRecentFiles, this->visibleItems); for (int index = numRecentFiles; index < recentFiles.count(); index++) { recentFiles[index]->setVisible(false); recentFiles[index]->setText(QString()); recentFiles[index]->setToolTip(QString()); } // Raise a single warning no matter how many conflicts if (!existingCommands.isEmpty()) { auto msgMain = QStringLiteral("Recent macros : keyboard shortcuts"); for (int index = 0; index < accel_col.size(); index++) { msgMain += QStringLiteral(" %1").arg(accel_col[index]); } msgMain += QStringLiteral(" disabled because of conflicts with"); for (int index = 0; index < existingCommands.count(); index++) { msgMain += QStringLiteral(" %1").arg(existingCommands[index]); } msgMain += QStringLiteral( " respectively.\nHint: In Preferences → Python → Macro →" " Recent Macros menu → Keyboard Modifiers this should be Ctrl+Shift+" " by default, if this is now blank then you should revert it back to" " Ctrl+Shift+ by pressing both keys at the same time." ); Base::Console().warning("%s\n", qPrintable(msgMain)); } } /** * Returns the list of defined recent files. */ QStringList RecentMacrosAction::files() const { QStringList files; QList recentFiles = groupAction()->actions(); for (int index = 0; index < recentFiles.count(); index++) { QString file = recentFiles[index]->toolTip(); if (file.isEmpty()) { break; } files.append(file); } return files; } void RecentMacrosAction::activateFile(int id) { // restore the list of recent files QStringList files = this->files(); if (id < 0 || id >= files.count()) { return; // no valid item } QString filename = files[id]; QFileInfo fi(filename); if (!ModuleIO::verifyFile(filename)) { files.removeAll(filename); setFiles(files); } else { if (QApplication::keyboardModifiers() == Qt::ShiftModifier) { // open for editing on Shift+click auto editor = new PythonEditor(); editor->setWindowIcon(Gui::BitmapFactory().iconFromTheme("applications-python")); auto edit = new PythonEditorView(editor, getMainWindow()); edit->setDisplayName(PythonEditorView::FileName); edit->open(filename); edit->resize(400, 300); getMainWindow()->addWindow(edit); getMainWindow()->appendRecentMacro(filename); edit->setWindowTitle(fi.fileName()); } else { // execute macro on normal (non-shifted) click try { getMainWindow()->appendRecentMacro(fi.filePath()); Application::Instance->macroManager()->run( Gui::MacroManager::File, fi.filePath().toUtf8() ); // after macro run recalculate the document if (Application::Instance->activeDocument()) { Application::Instance->activeDocument()->getDocument()->recompute(); } } catch (const Base::SystemExitException&) { // handle SystemExit exceptions Base::PyGILStateLocker locker; Base::PyException exc; exc.reportException(); } } } } void RecentMacrosAction::resizeList(int size) { this->visibleItems = size; int diff = this->visibleItems - this->maximumItems; // create new items if needed for (int i = 0; i < diff; i++) { groupAction()->addAction(QLatin1String(""))->setVisible(false); } setFiles(files()); } /** Loads all recent files from the preferences. */ void RecentMacrosAction::restore() { ParameterGrp::handle hGrp = App::GetApplication() .GetUserParameter() .GetGroup("BaseApp") ->GetGroup("Preferences") ->GetGroup("RecentMacros"); for (int i = groupAction()->actions().size(); i < this->maximumItems; i++) { groupAction()->addAction(QLatin1String(""))->setVisible(false); } resizeList(hGrp->GetInt("RecentMacros")); std::vector MRU = hGrp->GetASCIIs("MRU"); QStringList files; for (auto& filename : MRU) { files.append(QString::fromUtf8(filename.c_str())); } setFiles(files); } /** Saves all recent files to the preferences. */ void RecentMacrosAction::save() { ParameterGrp::handle hGrp = App::GetApplication() .GetUserParameter() .GetGroup("BaseApp") ->GetGroup("Preferences") ->GetGroup("RecentMacros"); int count = hGrp->GetInt("RecentMacros", this->visibleItems); // save number of files hGrp->Clear(); // count all set items QList recentFiles = groupAction()->actions(); int num = std::min(count, recentFiles.count()); for (int index = 0; index < num; index++) { QString key = QStringLiteral("MRU%1").arg(index); QString value = recentFiles[index]->toolTip(); if (value.isEmpty()) { break; } hGrp->SetASCII(key.toLatin1(), value.toUtf8()); } hGrp->SetInt("RecentMacros", count); // restore hGrp->SetInt("ShortcutCount", this->shortcut_count); hGrp->SetASCII("ShortcutModifiers", this->shortcut_modifiers.c_str()); } // -------------------------------------------------------------------- UndoAction::UndoAction(Command* pcCmd, QObject* parent) : Action(pcCmd, parent) { _toolAction = new QAction(this); _toolAction->setMenu(new UndoDialog()); connect(_toolAction, &QAction::triggered, this, &UndoAction::onActivated); } UndoAction::~UndoAction() { QMenu* menu = _toolAction->menu(); delete menu; delete _toolAction; } void UndoAction::addTo(QWidget* widget) { if (widget->inherits("QToolBar")) { actionChanged(); connect(action(), &QAction::changed, this, &UndoAction::actionChanged); widget->addAction(_toolAction); } else { widget->addAction(action()); } } void UndoAction::actionChanged() { // Do NOT set the shortcut again for _toolAction since this is already // reserved for _action. Otherwise we get an ambiguity of it with the // result that it doesn't work anymore. _toolAction->setText(action()->text()); _toolAction->setToolTip(action()->toolTip()); _toolAction->setStatusTip(action()->statusTip()); _toolAction->setWhatsThis(action()->whatsThis()); _toolAction->setIcon(action()->icon()); } void UndoAction::setEnabled(bool check) { Action::setEnabled(check); _toolAction->setEnabled(check); } void UndoAction::setVisible(bool check) { Action::setVisible(check); _toolAction->setVisible(check); } // -------------------------------------------------------------------- RedoAction::RedoAction(Command* pcCmd, QObject* parent) : Action(pcCmd, parent) { _toolAction = new QAction(this); _toolAction->setMenu(new RedoDialog()); connect(_toolAction, &QAction::triggered, this, &RedoAction::onActivated); } RedoAction::~RedoAction() { QMenu* menu = _toolAction->menu(); delete menu; delete _toolAction; } void RedoAction::addTo(QWidget* widget) { if (widget->inherits("QToolBar")) { actionChanged(); connect(action(), &QAction::changed, this, &RedoAction::actionChanged); widget->addAction(_toolAction); } else { widget->addAction(action()); } } void RedoAction::actionChanged() { // Do NOT set the shortcut again for _toolAction since this is already // reserved for _action. Otherwise we get an ambiguity of it with the // result that it doesn't work anymore. _toolAction->setText(action()->text()); _toolAction->setToolTip(action()->toolTip()); _toolAction->setStatusTip(action()->statusTip()); _toolAction->setWhatsThis(action()->whatsThis()); _toolAction->setIcon(action()->icon()); } void RedoAction::setEnabled(bool check) { Action::setEnabled(check); _toolAction->setEnabled(check); } void RedoAction::setVisible(bool check) { Action::setVisible(check); _toolAction->setVisible(check); } // -------------------------------------------------------------------- DockWidgetAction::DockWidgetAction(Command* pcCmd, QObject* parent) : Action(pcCmd, parent) , _menu(nullptr) {} DockWidgetAction::~DockWidgetAction() { delete _menu; } void DockWidgetAction::addTo(QWidget* widget) { if (!_menu) { _menu = new QMenu(); action()->setMenu(_menu); getMainWindow()->setDockWindowMenu(_menu); } widget->addAction(action()); } // -------------------------------------------------------------------- ToolBarAction::ToolBarAction(Command* pcCmd, QObject* parent) : Action(pcCmd, parent) , _menu(nullptr) {} ToolBarAction::~ToolBarAction() { delete _menu; } void ToolBarAction::addTo(QWidget* widget) { if (!_menu) { _menu = new QMenu(); action()->setMenu(_menu); getMainWindow()->setToolBarMenu(_menu); } widget->addAction(action()); } // -------------------------------------------------------------------- WindowAction::WindowAction(Command* pcCmd, QObject* parent) : ActionGroup(pcCmd, parent) , _menu(nullptr) {} void WindowAction::addTo(QWidget* widget) { auto menu = qobject_cast(widget); if (!menu) { if (!_menu) { _menu = new QMenu(); action()->setMenu(_menu); _menu->addActions(groupAction()->actions()); getMainWindow()->setWindowsMenu(_menu); } widget->addAction(action()); } else { menu->addActions(groupAction()->actions()); getMainWindow()->setWindowsMenu(menu); } } #include "moc_Action.cpp"