FreeCAD
1/***************************************************************************
2* Copyright (c) 2005 Werner Mayer <wmayer[at]users.sourceforge.net> *
3* *
4* This file is part of the FreeCAD CAx development system. *
5* *
6* This library is free software; you can redistribute it and/or *
7* modify it under the terms of the GNU Library General Public *
8* License as published by the Free Software Foundation; either *
9* version 2 of the License, or (at your option) any later version. *
10* *
11* This library is distributed in the hope that it will be useful, *
12* but WITHOUT ANY WARRANTY; without even the implied warranty of *
13* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
14* GNU Library General Public License for more details. *
15* *
16* You should have received a copy of the GNU Library General Public *
17* License along with this library; see the file COPYING.LIB. If not, *
18* write to the Free Software Foundation, Inc., 59 Temple Place, *
19* Suite 330, Boston, MA 02111-1307, USA *
20* *
21***************************************************************************/
22
23#include "PreCompiled.h"24#ifndef _PreComp_25# include <QApplication>26# include <QMenu>27# include <QMenuBar>28#endif29
30#include "MenuManager.h"31#include "Application.h"32#include "Command.h"33#include "MainWindow.h"34
35
36using namespace Gui;37
38
39MenuItem::MenuItem() = default;40
41MenuItem::MenuItem(MenuItem* item)42{
43if (item) {44item->appendItem(this);45}46}
47
48MenuItem::~MenuItem()49{
50clear();51}
52
53void MenuItem::setCommand(const std::string& name)54{
55_name = name;56}
57
58std::string MenuItem::command() const59{
60return _name;61}
62
63bool MenuItem::hasItems() const64{
65return _items.count() > 0;66}
67
68MenuItem* MenuItem::findItem(const std::string& name)69{
70if (_name == name) {71return this;72}73else {74for (auto& item : _items) {75if (item->_name == name) {76return item;77}78}79}80
81return nullptr;82}
83
84MenuItem* MenuItem::findParentOf(const std::string& name)85{
86for (auto& item : _items) {87if (item->_name == name) {88return this;89}90}91
92for (auto& item : _items) {93if (item->findParentOf(name)) {94return item;95}96}97
98return nullptr;99}
100
101MenuItem* MenuItem::copy() const102{
103auto root = new MenuItem;104root->setCommand(command());105
106for (auto& item : _items)107{108root->appendItem(item->copy());109}110
111return root;112}
113
114uint MenuItem::count() const115{
116return _items.count();117}
118
119void MenuItem::appendItem(MenuItem* item)120{
121_items.push_back(item);122}
123
124bool MenuItem::insertItem(MenuItem* before, MenuItem* item)125{
126int pos = _items.indexOf(before);127if (pos != -1) {128_items.insert(pos, item);129return true;130}131
132return false;133}
134
135MenuItem* MenuItem::afterItem(MenuItem* item) const136{
137int pos = _items.indexOf(item);138if (pos < 0 || pos+1 == _items.size()) {139return nullptr;140}141return _items.at(pos+1);142}
143
144void MenuItem::removeItem(MenuItem* item)145{
146int pos = _items.indexOf(item);147if (pos != -1) {148_items.removeAt(pos);149}150}
151
152void MenuItem::clear()153{
154for (auto& item : _items) {155delete item;156}157_items.clear();158}
159
160MenuItem& MenuItem::operator << (const std::string& command)161{
162auto item = new MenuItem(this);163item->setCommand(command);164return *this;165}
166
167MenuItem& MenuItem::operator << (MenuItem* item)168{
169appendItem(item);170return *this;171}
172
173QList<MenuItem*> MenuItem::getItems() const174{
175return _items;176}
177
178// -----------------------------------------------------------
179
180MenuManager* MenuManager::_instance=nullptr;181
182MenuManager* MenuManager::getInstance()183{
184if ( !_instance ) {185_instance = new MenuManager;186}187return _instance;188}
189
190void MenuManager::destruct()191{
192delete _instance;193_instance = nullptr;194}
195
196MenuManager::MenuManager() = default;197
198MenuManager::~MenuManager() = default;199
200void MenuManager::setup(MenuItem* menuItems) const201{
202if (!menuItems) {203return; // empty menu bar204}205
206QMenuBar* menuBar = getMainWindow()->menuBar();207
208#if 0209#if defined(FC_OS_MACOSX) && QT_VERSION >= 0x050900210// Unknown Qt macOS bug observed with Qt >= 5.9.4 causes random crashes when viewing reused top level menus.211menuBar->clear();212#endif213
214// On Kubuntu 18.10 global menu has issues with FreeCAD 0.18 menu bar.215// Optional parameter, clearing the menu bar, can be set as a workaround.216// Clearing the menu bar can cause issues, when trying to access menu bar through Python.217// https://forum.freecad.org/viewtopic.php?f=10&t=30340&start=440#p289330218if (App::GetApplication().GetParameterGroupByPath219("User parameter:BaseApp/Preferences/MainWindow")->GetBool("ClearMenuBar",false)) {220menuBar->clear();221}222#else223// In addition to the reason described in the above comments, there is224// another more subtle one that's making clearing menu bar a necessity for225// all platforms.226//227// By right, it should be fine for more than one command action having the228// same shortcut but in different workbench. It should not require manual229// conflict resolving in this case, as the action in an inactive workbench230// is expected to be inactive as well, or else user may experience231// seemingly random shortcut miss firing based on the order he/she232// switches workbenches. In fact, this may be considered as an otherwise233// difficult to implement feature of context aware shortcut, where a234// specific shortcut can activate different actions under different235// workbenches.236//237// This works as expected for action adding to a toolbar. As Qt will ignore238// actions inside an invisible toolbar. However, Qt refuse to do the same239// for actions in a hidden menu action of a menu bar. This is very likely a240// Qt bug, as the behavior does not seem to conform to Qt's documentation241// of Qt::ShortcutContext.242//243// Clearing the menu bar, and recreate it every time when switching244// workbench with only the active actions can solve this problem.245menuBar->clear();246#endif247
248QList<QAction*> actions = menuBar->actions();249for (auto& item : menuItems->getItems())250{251// search for the menu action252QAction* action = findAction(actions, QString::fromLatin1(item->command().c_str()));253if (!action) {254// There must be not more than one separator in the menu bar, so255// we can safely remove it if available and append it at the end256if (item->command() == "Separator") {257action = menuBar->addSeparator();258action->setObjectName(QLatin1String("Separator"));259}260else {261// create a new menu262std::string menuName = item->command();263QMenu* menu = menuBar->addMenu(264QApplication::translate("Workbench", menuName.c_str()));265action = menu->menuAction();266menu->setObjectName(QString::fromLatin1(menuName.c_str()));267action->setObjectName(QString::fromLatin1(menuName.c_str()));268}269
270// set the menu user data271action->setData(QString::fromLatin1(item->command().c_str()));272}273else {274// put the menu at the end275menuBar->removeAction(action);276menuBar->addAction(action);277action->setVisible(true);278int index = actions.indexOf(action);279actions.removeAt(index);280}281
282// flll up the menu283if (!action->isSeparator()) {284setup(item, action->menu());285}286}287
288// hide all menus which we don't need for the moment289for (auto& action : actions) {290action->setVisible(false);291}292
293// enable update again294//menuBar->setUpdatesEnabled(true);295}
296
297void MenuManager::setup(MenuItem* item, QMenu* menu) const298{
299CommandManager& mgr = Application::Instance->commandManager();300QList<QAction*> actions = menu->actions();301for (auto& item : item->getItems()) {302// search for the menu item303QList<QAction*> used_actions = findActions(actions, QString::fromLatin1(item->command().c_str()));304if (used_actions.isEmpty()) {305if (item->command() == "Separator") {306QAction* action = menu->addSeparator();307action->setObjectName(QLatin1String("Separator"));308action->setData(QLatin1String("Separator"));309used_actions.append(action);310}311else {312if (item->hasItems()) {313// Creste a submenu314std::string menuName = item->command();315QMenu* submenu = menu->addMenu(QApplication::translate("Workbench", menuName.c_str()));316QAction* action = submenu->menuAction();317submenu->setObjectName(QString::fromLatin1(item->command().c_str()));318action->setObjectName(QString::fromLatin1(item->command().c_str()));319// set the menu user data320action->setData(QString::fromLatin1(item->command().c_str()));321used_actions.append(action);322}323else {324// A command can have more than one QAction325int count = menu->actions().count();326// Check if action was added successfully327if (mgr.addTo(item->command().c_str(), menu)) {328QList<QAction*> acts = menu->actions();329for (int i=count; i < acts.count(); i++) {330QAction* act = acts[i];331// set the menu user data332act->setData(QString::fromLatin1(item->command().c_str()));333used_actions.append(act);334}335}336}337}338}339else {340for (auto& action : used_actions) {341// put the menu item at the end342menu->removeAction(action);343menu->addAction(action);344int index = actions.indexOf(action);345actions.removeAt(index);346}347}348
349// fill up the submenu350if (item->hasItems()) {351setup(item, used_actions.front()->menu());352}353}354
355// remove all menu items which we don't need for the moment356for (auto& action : actions) {357menu->removeAction(action);358}359}
360
361void MenuManager::retranslate() const362{
363QMenuBar* menuBar = getMainWindow()->menuBar();364for (auto& action : menuBar->actions()) {365if (action->menu()) {366retranslate(action->menu());367}368}369}
370
371void MenuManager::retranslate(QMenu* menu) const372{
373// Note: Here we search for all menus and submenus to retranslate their374// titles. To ease the translation for each menu the native name is set375// as user data. However, there are special menus that are created by376// actions for which the name of the according command name is set. For377// such menus we have to use the command's menu text instead. Examples378// for such actions are Std_RecentFiles, Std_Workbench or Std_FreezeViews.379CommandManager& mgr = Application::Instance->commandManager();380QByteArray menuName = menu->menuAction()->data().toByteArray();381Command* cmd = mgr.getCommandByName(menuName);382if (cmd) {383menu->setTitle(384QApplication::translate(cmd->className(),385cmd->getMenuText()));386}387else {388menu->setTitle(389QApplication::translate("Workbench",390(const char*)menuName));391}392for (auto& action : menu->actions()) {393if (action->menu()) {394retranslate(action->menu());395}396}397}
398
399QAction* MenuManager::findAction(const QList<QAction*>& acts, const QString& item) const400{
401for (auto& action : acts) {402if (action->data().toString() == item) {403return action;404}405}406
407return nullptr; // no item with the user data found408}
409
410QList<QAction*> MenuManager::findActions(const QList<QAction*>& acts, const QString& item) const411{
412// It is possible that the user text of several actions match with 'item'.413// But for the first match all following actions must match. For example414// the Std_WindowsMenu command provides several actions with the same user415// name.416bool first_match = false;417QList<QAction*> used;418for (auto& action : acts) {419if (action->data().toString() == item) {420used.append(action);421first_match = true;422// get only one separator per request423if (item == QLatin1String("Separator")) {424break;425}426}427else if (first_match) {428break;429}430}431
432return used;433}
434
435void MenuManager::setupContextMenu(MenuItem* item, QMenu &menu) const436{
437setup(item, &menu);438}
439