summaryrefslogtreecommitdiffstats
path: root/src/ui/desktop/menubar.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/ui/desktop/menubar.cpp')
-rw-r--r--src/ui/desktop/menubar.cpp635
1 files changed, 635 insertions, 0 deletions
diff --git a/src/ui/desktop/menubar.cpp b/src/ui/desktop/menubar.cpp
new file mode 100644
index 0000000..68c989a
--- /dev/null
+++ b/src/ui/desktop/menubar.cpp
@@ -0,0 +1,635 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * @file
+ * Desktop main menu bar code.
+ */
+/*
+ * Authors:
+ * Tavmjong Bah <tavmjong@free.fr>
+ * Alex Valavanis <valavanisalex@gmail.com>
+ * Patrick Storz <eduard.braun2@gmx.de>
+ * Krzysztof KosiƄski <tweenk.pl@gmail.com>
+ * Kris De Gussem <Kris.DeGussem@gmail.com>
+ *
+ * Copyright (C) 2018 Authors
+ *
+ * The contents of this file may be used under the GNU General Public License Version 2 or later.
+ * Read the file 'COPYING' for more information.
+ *
+ */
+
+#include <gtkmm.h>
+#include <glibmm/i18n.h>
+
+#include <iostream>
+
+#include "inkscape.h"
+#include "inkscape-application.h" // Open recent
+
+#include "message-context.h"
+#include "shortcuts.h"
+
+#include "helper/action.h"
+#include "helper/action-context.h"
+
+#include "object/sp-namedview.h"
+
+#include "ui/contextmenu.h" // Shift to make room for icons
+#include "ui/icon-loader.h"
+#include "ui/view/view.h"
+#include "ui/uxmanager.h" // To Do: Convert to actions
+
+#ifdef GDK_WINDOWING_QUARTZ
+#include <gtkosxapplication.h>
+#endif
+
+// ================= Common ====================
+
+std::vector<std::pair<std::pair<unsigned int, Gtk::MenuItem *>, Inkscape::UI::View::View *>> menuitems;
+unsigned int lastverb = -1;
+
+
+/**
+ * Get menu item (if it was registered in `menuitems`)
+ */
+Gtk::MenuItem *get_menu_item_for_verb(unsigned int verb, Inkscape::UI::View::View *view)
+{
+ for (auto &item : menuitems) {
+ if (item.first.first == verb && item.second == view) {
+ return item.first.second;
+ }
+ }
+ return nullptr;
+}
+
+#ifdef GDK_WINDOWING_QUARTZ
+/**
+ * Update menubar for macOS
+ *
+ * Can be used as a glib timeout callback.
+ */
+static gboolean sync_menubar(gpointer = nullptr)
+{
+ auto osxapp = gtkosx_application_get();
+ if (osxapp) {
+ gtkosx_application_sync_menubar(osxapp);
+ }
+ return false;
+}
+#endif
+
+// Sets tip
+static void
+select_action(SPAction *action)
+{
+ sp_action_get_view(action)->tipsMessageContext()->set(Inkscape::NORMAL_MESSAGE, action->tip);
+}
+
+// Clears tip
+static void
+deselect_action(SPAction *action)
+{
+ sp_action_get_view(action)->tipsMessageContext()->clear();
+}
+
+// Trigger action
+static void item_activate(Gtk::MenuItem *menuitem, SPAction *action)
+{
+ if (action->verb->get_code() == lastverb) {
+ lastverb = -1;
+ return;
+ }
+ lastverb = action->verb->get_code();
+ sp_action_perform(action, nullptr);
+ lastverb = -1;
+
+#ifdef GDK_WINDOWING_QUARTZ
+ sync_menubar();
+#endif
+}
+
+static void set_menuitems(unsigned int emitting_verb, bool value)
+{
+ for (auto menu : menuitems) {
+ if (menu.second == SP_ACTIVE_DESKTOP) {
+ if (emitting_verb == menu.first.first) {
+ if (emitting_verb == lastverb) {
+ lastverb = -1;
+ return;
+ }
+ lastverb = emitting_verb;
+ Gtk::CheckMenuItem *check = dynamic_cast<Gtk::CheckMenuItem *>(menu.first.second);
+ Gtk::RadioMenuItem *radio = dynamic_cast<Gtk::RadioMenuItem *>(menu.first.second);
+ if (radio) {
+ radio->property_active() = value;
+ } else if (check) {
+ check->property_active() = value;
+ }
+ lastverb = -1;
+ }
+ }
+ }
+}
+
+// Change label name (used in the Undo/Redo menu items).
+static void
+set_name(Glib::ustring const &name, Gtk::MenuItem* menuitem)
+{
+ if (menuitem) {
+ Gtk::Widget* widget = menuitem->get_child();
+
+ // Label is either child of menuitem
+ Gtk::Label* label = dynamic_cast<Gtk::Label*>(widget);
+
+ // Or wrapped inside a box which is a child of menuitem (if with icon).
+ if (!label) {
+ Gtk::Box* box = dynamic_cast<Gtk::Box*>(widget);
+ if (box) {
+ std::vector<Gtk::Widget*> children = box->get_children();
+ for (auto child: children) {
+ label = dynamic_cast<Gtk::Label*>(child);
+ if (label) break;
+ }
+ }
+ }
+
+ if (label) {
+ label->set_markup_with_mnemonic(name);
+ } else {
+ std::cerr << "set_name: could not find label!" << std::endl;
+ }
+ }
+}
+
+/* Install CSS to shift icons into the space reserved for toggles (i.e. check and radio items).
+ *
+ * TODO: This code already exists as a C++ version in the class ContextMenu so we can simply wrap
+ * it here. In future ContextMenu and the (to be created) class for the menu bar should then
+ * be derived from one common base class.
+ *
+ * TODO: This code is called everytime a menu is opened. We can certainly find a way to call it once.
+ */
+static void
+shift_icons(Gtk::Menu* menu)
+{
+ ContextMenu *contextmenu = static_cast<ContextMenu*>(menu);
+ contextmenu->ShiftIcons();
+}
+
+// ================= MenuItem ====================
+
+Gtk::MenuItem*
+build_menu_item_from_verb(SPAction* action,
+ bool show_icon,
+ bool radio = false,
+ Gtk::RadioMenuItem::Group *group = nullptr)
+{
+ Gtk::MenuItem* menuitem = nullptr;
+
+ if (radio) {
+ menuitem = Gtk::manage(new Gtk::RadioMenuItem(*group));
+ } else {
+ menuitem = Gtk::manage(new Gtk::MenuItem());
+ }
+
+ Gtk::AccelLabel* label = Gtk::manage(new Gtk::AccelLabel(action->name, true));
+ label->set_xalign(0.0);
+ label->set_accel_widget(*menuitem);
+ sp_shortcut_add_accelerator((GtkWidget*)menuitem->gobj(), sp_shortcut_get_primary(action->verb));
+
+ // If there is an image associated with the action, we can add it as an icon for the menu item.
+ if (show_icon && action->image) {
+ menuitem->set_name("ImageMenuItem"); // custom name to identify our "ImageMenuItems"
+ Gtk::Image* image = Gtk::manage(sp_get_icon_image(action->image, Gtk::ICON_SIZE_MENU));
+
+ // Create a box to hold icon and label as Gtk::MenuItem derives from GtkBin and can
+ // only hold one child
+ Gtk::Box *box = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL));
+ box->pack_start(*image, false, false, 0);
+ box->pack_start(*label, true, true, 0);
+ menuitem->add(*box);
+ } else {
+ menuitem->add(*label);
+ }
+ menuitem->signal_activate().connect(
+ sigc::bind<Gtk::MenuItem*, SPAction*>(sigc::ptr_fun(&item_activate), menuitem, action));
+ menuitem->signal_select().connect( sigc::bind<SPAction*>(sigc::ptr_fun(&select_action), action));
+ menuitem->signal_deselect().connect(sigc::bind<SPAction*>(sigc::ptr_fun(&deselect_action), action));
+
+ action->signal_set_sensitive.connect(
+ sigc::bind<0>(sigc::ptr_fun(&gtk_widget_set_sensitive), (GtkWidget*)menuitem->gobj()));
+ action->signal_set_name.connect(
+ sigc::bind<Gtk::MenuItem*>(sigc::ptr_fun(&set_name), menuitem));
+
+ // initialize sensitivity with verb default
+ sp_action_set_sensitive(action, action->verb->get_default_sensitive());
+
+ return menuitem;
+}
+
+// =============== CheckMenuItem ==================
+
+bool getStateFromPref(SPDesktop *dt, Glib::ustring item)
+{
+ Glib::ustring pref_path;
+
+ if (dt->is_focusMode()) {
+ pref_path = "/focus/";
+ } else if (dt->is_fullscreen()) {
+ pref_path = "/fullscreen/";
+ } else {
+ pref_path = "/window/";
+ }
+
+ pref_path += item;
+ pref_path += "/state";
+
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ return prefs->getBool(pref_path, false);
+}
+
+// I wonder if this can be done without hard coding names.
+static void
+checkitem_update(Gtk::CheckMenuItem* menuitem, SPAction* action)
+{
+ bool active = false;
+ if (action && action->id) {
+ Glib::ustring id = action->id;
+ SPDesktop* dt = static_cast<SPDesktop*>(sp_action_get_view(action));
+
+ if (id == "ToggleGrid") {
+ active = dt->gridsEnabled();
+
+ } else if (id == "EditGuidesToggleLock") {
+ active = dt->namedview->lockguides;
+
+ } else if (id == "ToggleGuides") {
+ active = dt->namedview->getGuides();
+
+ } else if (id == "ToggleRotationLock") {
+ active = dt->get_rotation_lock();
+
+ }
+ else if (id == "ViewCmsToggle") {
+ active = dt->colorProfAdjustEnabled();
+ }
+ else if (id == "ViewSplitModeToggle") {
+ active = dt->splitMode();
+
+ } else if (id == "ViewXRayToggle") {
+ active = dt->xrayMode();
+
+ } else if (id == "ToggleCommandsToolbar") {
+ active = getStateFromPref(dt, "commands");
+
+ } else if (id == "ToggleSnapToolbar") {
+ active = getStateFromPref(dt, "snaptoolbox");
+
+ } else if (id == "ToggleToolToolbar") {
+ active = getStateFromPref(dt, "toppanel");
+
+ } else if (id == "ToggleToolbox") {
+ active = getStateFromPref(dt, "toolbox");
+
+ } else if (id == "ToggleRulers") {
+ active = getStateFromPref(dt, "rulers");
+
+ } else if (id == "ToggleScrollbars") {
+ active = getStateFromPref(dt, "scrollbars");
+
+ } else if (id == "TogglePalette") {
+ active = getStateFromPref(dt, "panels"); // Rename?
+
+ } else if (id == "ToggleStatusbar") {
+ active = getStateFromPref(dt, "statusbar");
+
+ } else if (id == "FlipHorizontal") {
+ active = dt->is_flipped(SPDesktop::FLIP_HORIZONTAL);
+
+ } else if (id == "FlipVertical") {
+ active = dt->is_flipped(SPDesktop::FLIP_VERTICAL);
+
+ } else {
+ std::cerr << "checkitem_update: unhandled item: " << id << std::endl;
+ }
+
+ menuitem->set_active(active);
+ } else {
+ std::cerr << "checkitem_update: unknown action" << std::endl;
+ }
+}
+
+static Gtk::CheckMenuItem*
+build_menu_check_item_from_verb(SPAction* action)
+{
+ // This does not work for some reason!
+ // Gtk::CheckMenuItem* menuitem = Gtk::manage(new Gtk::CheckMenuItem(action->name, true));
+ // sp_shortcut_add_accelerator(GTK_WIDGET(menuitem->gobj()), sp_shortcut_get_primary(action->verb));
+
+ GtkWidget *item = gtk_check_menu_item_new_with_mnemonic(action->name);
+ sp_shortcut_add_accelerator(item, sp_shortcut_get_primary(action->verb));
+ Gtk::CheckMenuItem* menuitem = Gtk::manage(Glib::wrap(GTK_CHECK_MENU_ITEM(item)));
+
+ // Set initial state before connecting signals.
+ checkitem_update(menuitem, action);
+
+ menuitem->signal_toggled().connect(
+ sigc::bind<Gtk::CheckMenuItem*, SPAction*>(sigc::ptr_fun(&item_activate), menuitem, action));
+ menuitem->signal_select().connect( sigc::bind<SPAction*>(sigc::ptr_fun(&select_action), action));
+ menuitem->signal_deselect().connect(sigc::bind<SPAction*>(sigc::ptr_fun(&deselect_action), action));
+
+ return menuitem;
+}
+
+
+// ================= Tasks Submenu ==============
+static void
+task_activated(SPDesktop* dt, int number)
+{
+ Inkscape::UI::UXManager::getInstance()->setTask(dt, number);
+
+#ifdef GDK_WINDOWING_QUARTZ
+ // call later, crashes during startup if called directly
+ g_idle_add(sync_menubar, nullptr);
+#endif
+}
+
+// Sets tip
+static void
+select_task(SPDesktop* dt, Glib::ustring tip)
+{
+ dt->tipsMessageContext()->set(Inkscape::NORMAL_MESSAGE, tip.c_str());
+}
+
+// Clears tip
+static void
+deselect_task(SPDesktop* dt)
+{
+ dt->tipsMessageContext()->clear();
+}
+
+static void
+add_tasks(Gtk::MenuShell* menu, SPDesktop* dt)
+{
+ const Glib::ustring data[3][2] = {
+ { C_("Interface setup", "Default"), _("Default interface setup") },
+ { C_("Interface setup", "Custom"), _("Setup for custom task") },
+ { C_("Interface setup", "Wide"), _("Setup for widescreen work") }
+ };
+
+ int active = Inkscape::UI::UXManager::getInstance()->getDefaultTask(dt);
+
+ Gtk::RadioMenuItem::Group group;
+
+ for (unsigned int i = 0; i < 3; ++i) {
+
+ Gtk::RadioMenuItem* menuitem = Gtk::manage(new Gtk::RadioMenuItem(group, data[i][0]));
+ if (menuitem) {
+ if (active == i) {
+ menuitem->set_active(true);
+ }
+
+ menuitem->signal_toggled().connect(
+ sigc::bind<SPDesktop*, int>(sigc::ptr_fun(&task_activated), dt, i));
+ menuitem->signal_select().connect(
+ sigc::bind<SPDesktop*, Glib::ustring>(sigc::ptr_fun(&select_task), dt, data[i][1]));
+ menuitem->signal_deselect().connect(
+ sigc::bind<SPDesktop*>(sigc::ptr_fun(&deselect_task),dt));
+
+ menu->append(*menuitem);
+ }
+ }
+}
+
+
+static void
+sp_recent_open(Gtk::RecentChooser* recentchooser)
+{
+ Glib::ustring uri = recentchooser->get_current_uri();
+
+ Glib::RefPtr<Gio::File> file = Gio::File::create_for_uri(uri);
+
+ ConcreteInkscapeApplication<Gtk::Application>* app = &(ConcreteInkscapeApplication<Gtk::Application>::get_instance());
+
+ app->create_window(file);
+}
+
+// =================== Main Menu ================
+// Recursively build menu and submenus.
+void
+build_menu(Gtk::MenuShell* menu, Inkscape::XML::Node* xml, Inkscape::UI::View::View* view, bool show_icons = true)
+{
+ if (menu == nullptr) {
+ std::cerr << "build_menu: menu is nullptr" << std::endl;
+ return;
+ }
+
+ if (xml == nullptr) {
+ std::cerr << "build_menu: xml is nullptr" << std::endl;
+ return;
+ }
+
+ // user preference for icons in menus (1: show all, -1: hide all; 0: theme chooses per icon)
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ int show_icons_pref = prefs->getInt("/theme/menuIcons", 0);
+
+ Gtk::RadioMenuItem::Group group;
+
+ for (auto menu_ptr = xml; menu_ptr != nullptr; menu_ptr = menu_ptr->next()) {
+
+ if (menu_ptr->name()) {
+
+ // show menu icons for current item?
+ bool show_icons_curr = show_icons;
+ if (show_icons_pref == 1) { // show all icons per global pref
+ show_icons_curr = true;
+ } else if (show_icons_pref == -1) { // hide all icons per global pref
+ show_icons_curr = false;
+ } else { // set according to 'show-icons' attribute in theme's XML file; value is fully inherited
+ const char *str = menu_ptr->attribute("show-icons");
+ if (str) {
+ Glib::ustring ustr = str;
+ if (ustr == "true") {
+ show_icons_curr = true;
+ } else if (ustr == "false") {
+ show_icons_curr = false;
+ } else {
+ std::cerr << "build_menu: invalid value for 'show-icons' (use 'true' or 'false')."
+ << ustr << std::endl;
+ }
+ }
+ }
+
+ Glib::ustring name = menu_ptr->name();
+
+ if (name == "inkscape") {
+ build_menu(menu, menu_ptr->firstChild(), view, show_icons_curr);
+ continue;
+ }
+
+ if (name == "submenu") {
+ const char *name = menu_ptr->attribute("name");
+ if (!name) {
+ g_warning("menus.xml: skipping submenu without name.");
+ continue;
+ }
+
+ Gtk::MenuItem* menuitem = Gtk::manage(new Gtk::MenuItem(_(name), true));
+ Gtk::Menu* submenu = Gtk::manage(new Gtk::Menu());
+ build_menu(submenu, menu_ptr->firstChild(), view, show_icons_curr);
+ menuitem->set_submenu(*submenu);
+ menu->append(*menuitem);
+
+ submenu->signal_map().connect(
+ sigc::bind<Gtk::Menu*>(sigc::ptr_fun(&shift_icons), submenu));
+
+ continue;
+ }
+
+ if (name == "contextmenu") {
+ if (menu_ptr->attribute("id")) {
+ Glib::ustring id = menu_ptr->attribute("id");
+ if (id == "canvas" || id == "layers" || id == "objects") {
+ Glib::ustring prefname = Glib::ustring::compose("/theme/menuIcons_%1", id);
+ prefs->setBool(prefname, show_icons_curr);
+ } else {
+ std::cerr << "build_menu: invalid contextmenu id: " << id << std::endl;
+ }
+ } else {
+ std::cerr << "build_menu: contextmenu element needs a valid id" << std::endl;
+ }
+ continue;
+ }
+
+ if (name == "verb") {
+ if (menu_ptr->attribute("verb-id") != nullptr) {
+ Glib::ustring verb_name = menu_ptr->attribute("verb-id");
+
+ Inkscape::Verb *verb = Inkscape::Verb::getbyid(verb_name.c_str());
+ if (verb != nullptr && verb->get_code() != SP_VERB_NONE) {
+
+ SPAction* action = verb->get_action(Inkscape::ActionContext(view));
+ if (menu_ptr->attribute("check") != nullptr) {
+ Gtk::MenuItem *menuitem = build_menu_check_item_from_verb(action);
+ if (menuitem) {
+ std::pair<std::pair<unsigned int, Gtk::MenuItem *>, Inkscape::UI::View::View *>
+ verbmenuitem = std::make_pair(std::make_pair(verb->get_code(), menuitem), view);
+ menuitems.push_back(verbmenuitem);
+ menu->append(*menuitem);
+ }
+ } else if (menu_ptr->attribute("radio") != nullptr) {
+ Gtk::MenuItem* menuitem = build_menu_item_from_verb(action, show_icons_curr, true, &group);
+ if (menuitem) {
+ if (menu_ptr->attribute("default") != nullptr) {
+ auto radiomenuitem = dynamic_cast<Gtk::RadioMenuItem*>(menuitem);
+ if (radiomenuitem) {
+ radiomenuitem->set_active(true);
+ }
+ }
+ std::pair<std::pair<unsigned int, Gtk::MenuItem *>, Inkscape::UI::View::View *>
+ verbmenuitem = std::make_pair(std::make_pair(verb->get_code(), menuitem), view);
+ menuitems.push_back(verbmenuitem);
+ menu->append(*menuitem);
+ }
+ } else {
+ Gtk::MenuItem* menuitem = build_menu_item_from_verb(action, show_icons_curr);
+ if (menuitem) {
+ menu->append(*menuitem);
+ }
+
+#ifdef GDK_WINDOWING_QUARTZ
+ // for moving menu items to "Inkscape" menu
+ switch (verb->get_code()) {
+ case SP_VERB_DIALOG_DISPLAY:
+ case SP_VERB_DIALOG_INPUT:
+ case SP_VERB_HELP_ABOUT:
+ menuitems.emplace_back(std::make_pair(verb->get_code(), menuitem), view);
+ }
+#endif
+ }
+ } else if (true
+#ifndef HAVE_ASPELL
+ && strcmp(verb_name.c_str(), "DialogSpellcheck") != 0
+#endif
+ ) {
+ std::cerr << "build_menu: no verb with id: " << verb_name << std::endl;
+ }
+ }
+ continue;
+ }
+
+ // This is used only for wide-screen vs non-wide-screen displays.
+ // The code should be rewritten to use actions like everything else here.
+ if (name == "task-checkboxes") {
+ add_tasks(menu, static_cast<SPDesktop*>(view));
+ continue;
+ }
+
+ if (name == "recent-file-list") {
+
+ // Filter recent files to those already opened in Inkscape.
+ Glib::RefPtr<Gtk::RecentFilter> recentfilter = Gtk::RecentFilter::create();
+ recentfilter->add_application(g_get_prgname());
+ recentfilter->add_application("org.inkscape.Inkscape");
+ recentfilter->add_application("inkscape");
+#ifdef _WIN32
+ recentfilter->add_application("inkscape.exe");
+#endif
+
+ Gtk::RecentChooserMenu* recentchoosermenu = Gtk::manage(new Gtk::RecentChooserMenu());
+ int max = Inkscape::Preferences::get()->getInt("/options/maxrecentdocuments/value");
+ recentchoosermenu->set_limit(max);
+ recentchoosermenu->set_sort_type(Gtk::RECENT_SORT_MRU); // Sort most recent first.
+ recentchoosermenu->set_show_tips(true);
+ recentchoosermenu->set_show_not_found(false);
+ recentchoosermenu->add_filter(recentfilter);
+ recentchoosermenu->signal_item_activated().connect(
+ sigc::bind<Gtk::RecentChooserMenu*>(sigc::ptr_fun(&sp_recent_open), recentchoosermenu));
+
+ Gtk::MenuItem* menuitem = Gtk::manage(new Gtk::MenuItem(_("Open _Recent"), true));
+ menuitem->set_submenu(*recentchoosermenu);
+ menu->append(*menuitem);
+ continue;
+ }
+
+ if (name == "separator") {
+ Gtk::MenuItem* menuitem = Gtk::manage(new Gtk::SeparatorMenuItem());
+ menu->append(*menuitem);
+ continue;
+ }
+
+ // Comments and items handled elsewhere.
+ if (name == "comment" ||
+ name == "filters-list" ||
+ name == "effects-list" ) {
+ continue;
+ }
+
+ std::cerr << "build_menu: unhandled option: " << name << std::endl;
+
+ } else {
+ std::cerr << "build_menu: xml node has no name!" << std::endl;
+ }
+ }
+}
+
+Gtk::MenuBar*
+build_menubar(Inkscape::UI::View::View* view)
+{
+ Gtk::MenuBar* menubar = Gtk::manage(new Gtk::MenuBar());
+ build_menu(menubar, INKSCAPE.get_menus()->parent(), view);
+ SP_ACTIVE_DESKTOP->_menu_update.connect(sigc::ptr_fun(&set_menuitems));
+ return menubar;
+}
+
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :