// SPDX-License-Identifier: GPL-2.0-or-later /** * @file * Context menu */ /* Authors: * Tavmjong Bah * * Rewrite of code authored by: * Lauris Kaplinski * Frank Felfe * bulia byak * Jon A. Cruz * Abhishek Sharma * Kris De Gussem * * Copyright (C) 2022 Tavmjong Bah * Copyright (C) 2012 Kris De Gussem * Copyright (C) 2010 authors * Copyright (C) 1999-2005 authors * Copyright (C) 2004 David Turner * Copyright (C) 2001-2002 Ximian, Inc. * * Released under GNU GPL v2+, read the file 'COPYING' for more information. */ #include "contextmenu.h" #include #include "desktop.h" #include "document.h" #include "layer-manager.h" #include "page-manager.h" #include "selection.h" #include "object/sp-anchor.h" #include "object/sp-image.h" #include "object/sp-page.h" #include "object/sp-shape.h" #include "object/sp-text.h" #include "ui/desktop/menu-icon-shift.h" #include "ui/util.h" ContextMenu::ContextMenu(SPDesktop *desktop, SPObject *object, bool hide_layers_and_objects_menu_item) { set_name("ContextMenu"); auto item = cast(object); // std::cout << "ContextMenu::ContextMenu: " << (item ? item->getId() : "no item") << std::endl; action_group = Gio::SimpleActionGroup::create(); insert_action_group("ctx", action_group); auto document = desktop->getDocument(); action_group->add_action("unhide-objects-below-cursor", sigc::bind(sigc::mem_fun(*this, &ContextMenu::unhide_or_unlock), document, true)); action_group->add_action("unlock-objects-below-cursor", sigc::bind(sigc::mem_fun(*this, &ContextMenu::unhide_or_unlock), document, false)); auto gmenu = Gio::Menu::create(); // Main menu auto gmenu_section = Gio::Menu::create(); // Section (used multiple times) auto layer = Inkscape::LayerManager::asLayer(item); // Layers have their own context menu in the Object and Layers dialog. auto root = desktop->layerManager().currentRoot(); // Get a list of items under the cursor, used for unhiding and unlocking. auto point_document = desktop->point() * desktop->dt2doc(); Geom::Rect b(point_document, point_document + Geom::Point(1, 1)); // Seems strange to use a rect! items_under_cursor = document->getItemsPartiallyInBox(desktop->dkey, b, true, true, true, true); bool has_hidden_below_cursor = false; bool has_locked_below_cursor = false; for (auto item : items_under_cursor) { if (item->isHidden()) { has_hidden_below_cursor = true; } if (item->isLocked()) { has_locked_below_cursor = true; } } // std::cout << "Items below cursor: " << items_under_cursor.size() // << " hidden: " << std::boolalpha << has_hidden_below_cursor // << " locked: " << std::boolalpha << has_locked_below_cursor // << std::endl; // clang-tidy off // Undo/redo // gmenu_section = Gio::Menu::create(); // AppendItemFromAction(gmenu_section, "doc.undo", _("Undo"), "edit-undo"); // AppendItemFromAction(gmenu_section, "doc.redo", _("Redo"), "edit-redo"); // gmenu->append_section(gmenu_section); if (auto page = cast(object)) { auto &page_manager = document->getPageManager(); page_manager.selectPage(page); gmenu_section = Gio::Menu::create(); AppendItemFromAction(gmenu_section, "doc.page-new", _("_New Page"), "pages-add"); gmenu->append_section(gmenu_section); gmenu_section = Gio::Menu::create(); AppendItemFromAction(gmenu_section, "doc.page-delete", _("_Delete Page"), "pages-remove"); AppendItemFromAction(gmenu_section, "doc.page-move-backward", _("Move Page _Backward"), "pages-order-backwards"); AppendItemFromAction(gmenu_section, "doc.page-move-forward", _("Move Page _Forward"), "pages-order-forwards"); gmenu->append_section(gmenu_section); } else if (!layer) { // "item" is the object that was under the mouse when right-clicked. It determines what is shown // in the menu thus it makes the most sense that it is either selected or part of the current // selection. auto selection = desktop->getSelection(); if (object && !selection->includes(object)) { selection->set(object); } if (!item) { // Even when there's no item, we should still have the Paste action on top // (see https://gitlab.com/inkscape/inkscape/-/issues/4150) gmenu->append_section(create_clipboard_actions(true)); gmenu_section = Gio::Menu::create(); AppendItemFromAction(gmenu_section, "win.dialog-open('DocumentProperties')", _("Document Properties..."), "document-properties"); gmenu->append_section(gmenu_section); } else { // When an item is selected, show all three of Cut, Copy and Paste. gmenu->append_section(create_clipboard_actions()); gmenu_section = Gio::Menu::create(); AppendItemFromAction(gmenu_section, "app.duplicate", _("Duplic_ate"), "edit-duplicate"); AppendItemFromAction(gmenu_section, "app.clone", _("_Clone"), "edit-clone"); AppendItemFromAction(gmenu_section, "app.delete-selection", _("_Delete"), "edit-delete"); gmenu->append_section(gmenu_section); // Dialogs auto gmenu_dialogs = Gio::Menu::create(); if (!hide_layers_and_objects_menu_item) { // Hidden when context menu is popped up in Layers and Objects dialog! AppendItemFromAction(gmenu_dialogs, "win.dialog-open('Objects')", _("Layers and Objects..."), "dialog-objects" ); } AppendItemFromAction(gmenu_dialogs, "win.dialog-open('ObjectProperties')", _("_Object Properties..."), "dialog-object-properties" ); if (is(item) || is(item) || is(item)) { AppendItemFromAction(gmenu_dialogs, "win.dialog-open('FillStroke')", _("_Fill and Stroke..."), "dialog-fill-and-stroke" ); } // Image dialogs (mostly). if (auto image = cast(item)) { AppendItemFromAction( gmenu_dialogs, "win.dialog-open('ObjectAttributes')", _("Image _Properties..."), "dialog-fill-and-stroke"); AppendItemFromAction( gmenu_dialogs, "win.dialog-open('Trace')", _("_Trace Bitmap..."), "bitmap-trace" ); if (image->getClipObject()) { AppendItemFromAction( gmenu_dialogs, "app.element-image-crop", _("Crop Image to Clip"), "" ); } if (strncmp(image->href, "data", 4) == 0) { // Image is embedded. AppendItemFromAction( gmenu_dialogs, "app.org.inkscape.filter.extract-image", _("Extract Image..."), "" ); } else { // Image is linked. AppendItemFromAction( gmenu_dialogs, "app.org.inkscape.filter.selected.embed-image", _("Embed Image"), "" ); AppendItemFromAction( gmenu_dialogs, "app.element-image-edit", _("Edit Externally..."), "" ); } } // Text dialogs. if (is(item)) { AppendItemFromAction( gmenu_dialogs, "win.dialog-open('Text')", _("_Text and Font..."), "dialog-text-and-font" ); AppendItemFromAction( gmenu_dialogs, "win.dialog-open('Spellcheck')", _("Check Spellin_g..."), "tools-check-spelling" ); } gmenu->append_section(gmenu_dialogs); // We might add to it later... if (!is(item)) { // Item menu // Selection gmenu_section = Gio::Menu::create(); auto gmenu_submenu = Gio::Menu::create(); AppendItemFromAction( gmenu_submenu, "win.select-same-fill-and-stroke", _("Fill _and Stroke"), "edit-select-same-fill-and-stroke"); AppendItemFromAction( gmenu_submenu, "win.select-same-fill", _("_Fill Color"), "edit-select-same-fill" ); AppendItemFromAction( gmenu_submenu, "win.select-same-stroke-color", _("_Stroke Color"), "edit-select-same-stroke-color" ); AppendItemFromAction( gmenu_submenu, "win.select-same-stroke-style", _("Stroke St_yle"), "edit-select-same-stroke-style" ); AppendItemFromAction( gmenu_submenu, "win.select-same-object-type", _("_Object Type"), "edit-select-same-object-type" ); gmenu_section->append_submenu(_("Select Sa_me"), gmenu_submenu); gmenu->append_section(gmenu_section); // Groups and Layers gmenu_section = Gio::Menu::create(); AppendItemFromAction( gmenu_section, "win.selection-move-to-layer", _("_Move to Layer..."), "" ); AppendItemFromAction( gmenu_section, "app.selection-link", _("Create anchor (hyperlink)"), "" ); AppendItemFromAction( gmenu_section, "app.selection-group", _("_Group"), "" ); if (is(item)) { AppendItemFromAction( gmenu_section, "app.selection-ungroup", _("_Ungroup"), "" ); Glib::ustring label = Glib::ustring::compose(_("Enter group %1"), item->defaultLabel()); AppendItemFromAction( gmenu_section, "win.selection-group-enter", label, "" ); if (item->getParentGroup()->isLayer() || item->getParentGroup() == root) { // A layer should be a child of root or another layer. AppendItemFromAction( gmenu_section, "win.layer-from-group", _("Group to Layer"), "" ); } } auto group = cast(item->parent); if (group && !group->isLayer()) { AppendItemFromAction( gmenu_section, "win.selection-group-exit", _("Exit group"), "" ); AppendItemFromAction( gmenu_section, "app.selection-ungroup-pop", _("_Pop selection out of group"), "" ); } gmenu->append_section(gmenu_section); // Clipping and Masking gmenu_section = Gio::Menu::create(); if (selection->size() > 1) { AppendItemFromAction( gmenu_section, "app.object-set-clip", _("Set Cl_ip"), "" ); } if (item->getClipObject()) { AppendItemFromAction( gmenu_section, "app.object-release-clip", _("Release C_lip"), "" ); } else { AppendItemFromAction( gmenu_section, "app.object-set-clip-group", _("Set Clip G_roup"), "" ); } if (selection->size() > 1) { AppendItemFromAction( gmenu_section, "app.object-set-mask", _("Set Mask"), "" ); } if (item->getMaskObject()) { AppendItemFromAction( gmenu_section, "app.object-release-mask", _("Release Mask"), "" ); } gmenu->append_section(gmenu_section); // Hide and Lock gmenu_section = Gio::Menu::create(); AppendItemFromAction( gmenu_section, "app.selection-hide", _("Hide Selected Objects"), "" ); AppendItemFromAction( gmenu_section, "app.selection-lock", _("Lock Selected Objects"), "" ); gmenu->append_section(gmenu_section); } else { // Anchor menu gmenu_section = Gio::Menu::create(); AppendItemFromAction( gmenu_section, "win.dialog-open('ObjectAttributes')", _("Link _Properties..."), "" ); AppendItemFromAction( gmenu_section, "app.element-a-open-link", _("_Open link in browser"), "" ); AppendItemFromAction( gmenu_section, "app.selection-ungroup", _("_Remove Link"), "" ); AppendItemFromAction( gmenu_section, "win.selection-group-enter", _("Enter Group"), "" ); gmenu->append_section(gmenu_section); } } // Hidden or locked beneath cursor gmenu_section = Gio::Menu::create(); if (has_hidden_below_cursor) { AppendItemFromAction( gmenu_section, "ctx.unhide-objects-below-cursor", _("Unhide Objects Below Cursor"), "" ); } if (has_locked_below_cursor) { AppendItemFromAction( gmenu_section, "ctx.unlock-objects-below-cursor", _("Unlock Objects Below Cursor"), "" ); } gmenu->append_section(gmenu_section); } else { // Layers: Only used in "Layers and Objects" dialog. gmenu_section = Gio::Menu::create(); AppendItemFromAction(gmenu_section, "win.layer-new", _("_Add Layer..."), "layer-new"); AppendItemFromAction(gmenu_section, "win.layer-duplicate", _("D_uplicate Layer"), "layer-duplicate"); AppendItemFromAction(gmenu_section, "win.layer-delete", _("_Delete Layer"), "layer-delete"); AppendItemFromAction(gmenu_section, "win.layer-rename", _("Re_name Layer..."), "layer-rename"); AppendItemFromAction(gmenu_section, "win.layer-to-group", _("Layer to _Group"), "dialog-objects"); gmenu->append_section(gmenu_section); gmenu_section = Gio::Menu::create(); AppendItemFromAction(gmenu_section, "win.layer-raise", _("_Raise Layer"), "layer-raise"); AppendItemFromAction(gmenu_section, "win.layer-lower", _("_Lower Layer"), "layer-lower"); gmenu->append_section(gmenu_section); gmenu_section = Gio::Menu::create(); AppendItemFromAction(gmenu_section, "win.layer-hide-toggle-others", _("_Hide/show other layers"), ""); AppendItemFromAction(gmenu_section, "win.layer-hide-all", _("_Hide all layers"), ""); AppendItemFromAction(gmenu_section, "win.layer-unhide-all", _("_Show all layers"), ""); gmenu->append_section(gmenu_section); gmenu_section = Gio::Menu::create(); AppendItemFromAction(gmenu_section, "win.layer-lock-toggle-others", _("_Lock/unlock other layers"), ""); AppendItemFromAction(gmenu_section, "win.layer-lock-all", _("_Lock all layers"), ""); AppendItemFromAction(gmenu_section, "win.layer-unlock-all", _("_Unlock all layers"), ""); gmenu->append_section(gmenu_section); } // clang-tidy on bind_model(gmenu, true); // Do not install this CSS provider; it messes up menus with icons (like popup menu with all dialogs). // It doesn't work well with context menu either, introducing disturbing visual glitch // where menu shifts upon opening. Inkscape::Preferences *prefs = Inkscape::Preferences::get(); if (prefs->getInt("/theme/shiftIcons", true)) { get_style_context()->add_class("shifticonmenu"); shift_icons(this); } // Set the style and icon theme of the new menu based on the desktop if (Gtk::Window *window = desktop->getToplevel()) { if (window->get_style_context()->has_class("dark")) { get_style_context()->add_class("dark"); } else { get_style_context()->add_class("bright"); } if (prefs->getBool("/theme/symbolicIcons", false)) { get_style_context()->add_class("symbolic"); } else { get_style_context()->add_class("regular"); } } } /** @brief Create a menu section containing the standard editing actions: * Cut, Copy, Paste. * * @param paste_only If true, only the Paste action will be included. * @return A new menu containing the requested actions. */ Glib::RefPtr ContextMenu::create_clipboard_actions(bool paste_only) { auto result = Gio::Menu::create(); if (!paste_only) { AppendItemFromAction(result, "app.cut", _("Cu_t"), "edit-cut"); AppendItemFromAction(result, "app.copy", _("_Copy"), "edit-copy"); } AppendItemFromAction(result, "win.paste", _("_Paste"), "edit-paste"); return result; } void ContextMenu::AppendItemFromAction(Glib::RefPtr gmenu, Glib::ustring action, Glib::ustring label, Glib::ustring icon) { Inkscape::Preferences *prefs = Inkscape::Preferences::get(); bool show_icons = prefs->getInt("/theme/menuIcons", true); auto menu_item = Gio::MenuItem::create(label, action); if (icon != "" && show_icons) { auto _icon = Gio::Icon::create(icon); menu_item->set_icon(_icon); } gmenu->append_item(menu_item); } void ContextMenu::unhide_or_unlock(SPDocument* document, bool unhide) { for (auto item : items_under_cursor) { if (unhide) { if (item->isHidden()) { item->setHidden(false); } } else { if (item->isLocked()) { item->setLocked(false); } } } // We wouldn't be here if we didn't make a change. Inkscape::DocumentUndo::done(document, (unhide ? _("Unhid objects") : _("Unlocked objects")), ""); } /* 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 :