Inkscape
Vector Graphics Editor
Loading...
Searching...
No Matches
contextmenu.cpp
Go to the documentation of this file.
1// SPDX-License-Identifier: GPL-2.0-or-later
6/* Authors:
7 * Tavmjong Bah
8 *
9 * Rewrite of code authored by:
10 * Lauris Kaplinski <lauris@kaplinski.com>
11 * Frank Felfe <innerspace@iname.com>
12 * bulia byak <buliabyak@users.sf.net>
13 * Jon A. Cruz <jon@joncruz.org>
14 * Abhishek Sharma
15 * Kris De Gussem <Kris.DeGussem@gmail.com>
16 *
17 * Copyright (C) 2022 Tavmjong Bah
18 * Copyright (C) 2012 Kris De Gussem
19 * Copyright (C) 2010 authors
20 * Copyright (C) 1999-2005 authors
21 * Copyright (C) 2004 David Turner
22 * Copyright (C) 2001-2002 Ximian, Inc.
23 *
24 * Released under GNU GPL v2+, read the file 'COPYING' for more information.
25 */
26
27#include "contextmenu.h"
28
29#include <glibmm/i18n.h>
30#include <giomm/icon.h>
31#include <giomm/menu.h>
32#include <giomm/menuitem.h>
33#include <giomm/simpleactiongroup.h>
34#include <gtkmm/image.h>
35#include <gtkmm/window.h>
36
37#include "desktop.h"
38#include "document.h"
39#include "document-undo.h"
40#include "inkscape-window.h"
41#include "layer-manager.h"
42#include "page-manager.h"
43#include "selection.h"
44#include "object/sp-anchor.h"
45#include "object/sp-image.h"
46#include "object/sp-page.h"
47#include "object/sp-shape.h"
48#include "object/sp-text.h"
49#include "object/sp-use.h"
51#include "ui/util.h"
53
54static void
55AppendItemFromAction(Glib::RefPtr<Gio::Menu> const &gmenu,
56 Glib::ustring const &action,
57 Glib::ustring const &label,
58 Glib::ustring const &icon = {})
59{
61 bool show_icons = prefs->getInt("/theme/menuIcons", true);
62
63 auto menu_item = Gio::MenuItem::create(label, action);
64 if (icon != "" && show_icons) {
65 auto _icon = Gio::Icon::create(icon);
66 menu_item->set_icon(_icon);
67 }
68 gmenu->append_item(menu_item);
69}
70
77static Glib::RefPtr<Gio::Menu> create_clipboard_actions(bool const paste_only = false)
78{
79 auto result = Gio::Menu::create();
80 if (!paste_only) {
81 AppendItemFromAction(result, "app.cut", _("Cu_t"), "edit-cut");
82 AppendItemFromAction(result, "app.copy", _("_Copy"), "edit-copy");
83 }
84 AppendItemFromAction(result, "win.paste", _("_Paste"), "edit-paste");
85
89 auto gmenu_paste_section = Gio::Menu::create();
90 auto gmenu_paste_submenu = Gio::Menu::create();
91 AppendItemFromAction(gmenu_paste_submenu, "win.paste-in-place", _("_In Place"), "edit-paste-in-place");
92 AppendItemFromAction(gmenu_paste_submenu, "win.paste-on-page", _("_On Page"), "");
93 AppendItemFromAction(gmenu_paste_submenu, "app.paste-style", _("_Style"), "edit-paste-style");
94 AppendItemFromAction(gmenu_paste_submenu, "app.paste-size", _("Si_ze"), "edit-paste-size");
95 AppendItemFromAction(gmenu_paste_submenu, "app.paste-width", _("_Width"), "edit-paste-width");
96 AppendItemFromAction(gmenu_paste_submenu, "app.paste-height", _("_Height"), "edit-paste-height");
97 AppendItemFromAction(gmenu_paste_submenu, "app.paste-size-separately", _("Size Separately"), "edit-paste-size-separately");
98 AppendItemFromAction(gmenu_paste_submenu, "app.paste-width-separately", _("Width Separately"), "edit-paste-width-separately");
99 AppendItemFromAction(gmenu_paste_submenu, "app.paste-height-separately", _("Height Separately"), "edit-paste-height-separately");
100 gmenu_paste_section->append_submenu(_("Paste..."), gmenu_paste_submenu);
101 result->append_section(gmenu_paste_section);
102
103 return result;
104}
105
110void show_all_images(Gtk::Widget &parent)
111{
113 {
114 if (auto const image = dynamic_cast<Gtk::Image *>(&child);
115 image && image->get_storage_type() != Gtk::Image::Type::EMPTY)
116 {
117 image->set_visible(true);
118 }
120 });
121}
122
125{
126 if (auto const *clone = cast<SPUse>(item)) {
127 return is<SPImage>(clone->trueOriginal());
128 }
129 return false;
130}
131
133{
134 return std::any_of(item->children.begin(), item->children.end(), [&selection](auto &child) {
135 if (auto childItem = cast<SPItem>(&child)) {
136 return selection.includes(childItem) || childrenIncludedInSelection(childItem, selection);
137 }
138 return false;
139 });
140}
141
142ContextMenu::ContextMenu(SPDesktop *desktop, SPObject *object, std::vector<SPItem*> const &items, bool hide_layers_and_objects_menu_item)
143{
144 set_name("ContextMenu");
145
146 auto item = cast<SPItem>(object);
147
148 // std::cout << "ContextMenu::ContextMenu: " << (item ? item->getId() : "no item") << std::endl;
149 action_group = Gio::SimpleActionGroup::create();
150 insert_action_group("ctx", action_group);
151 auto document = desktop->getDocument();
152 action_group->add_action("unhide-objects-below-cursor", sigc::bind(sigc::mem_fun(*this, &ContextMenu::unhide_or_unlock), document, true));
153 action_group->add_action("unlock-objects-below-cursor", sigc::bind(sigc::mem_fun(*this, &ContextMenu::unhide_or_unlock), document, false));
154
155 auto gmenu = Gio::Menu::create(); // Main menu
156 auto gmenu_section = Gio::Menu::create(); // Section (used multiple times)
157
158 auto layer = Inkscape::LayerManager::asLayer(item); // Layers have their own context menu in the Object and Layers dialog.
160
161 // Save the items in context
163
164 bool has_hidden_below_cursor = false;
165 bool has_locked_below_cursor = false;
166 for (auto item : items_under_cursor) {
167 if (item->isHidden()) {
168 has_hidden_below_cursor = true;
169 }
170 if (item->isLocked()) {
171 has_locked_below_cursor = true;
172 }
173 }
174 // std::cout << "Items below cursor: " << items_under_cursor.size()
175 // << " hidden: " << std::boolalpha << has_hidden_below_cursor
176 // << " locked: " << std::boolalpha << has_locked_below_cursor
177 // << std::endl;
178
179 // clang-format off
180
181 // Undo/redo
182 // gmenu_section = Gio::Menu::create();
183 // AppendItemFromAction(gmenu_section, "doc.undo", _("Undo"), "edit-undo");
184 // AppendItemFromAction(gmenu_section, "doc.redo", _("Redo"), "edit-redo");
185 // gmenu->append_section(gmenu_section);
186
187 if (auto page = cast<SPPage>(object)) {
188 auto &page_manager = document->getPageManager();
189 page_manager.selectPage(page);
190
191 gmenu_section = Gio::Menu::create();
192 AppendItemFromAction(gmenu_section, "doc.page-new", _("_New Page"), "pages-add");
193 gmenu->append_section(gmenu_section);
194
195 gmenu_section = Gio::Menu::create();
196 AppendItemFromAction(gmenu_section, "doc.page-delete", _("_Delete Page"), "pages-remove");
197 AppendItemFromAction(gmenu_section, "doc.page-move-backward", _("Move Page _Backward"), "pages-order-backwards");
198 AppendItemFromAction(gmenu_section, "doc.page-move-forward", _("Move Page _Forward"), "pages-order-forwards");
199 gmenu->append_section(gmenu_section);
200
201 } else if (!layer || desktop->getSelection()->includes(layer)) {
202 // "item" is the object that was under the mouse when right-clicked. It determines what is shown
203 // in the menu thus it makes the most sense that it is either selected or part of the current
204 // selection.
205 auto &selection = *desktop->getSelection();
206
207 // Do not include this object in the selection if any of its
208 // children have been selected separately.
209 if (object && !selection.includes(object) && item && !childrenIncludedInSelection(item, selection)) {
210 selection.set(object);
211 }
212
213 if (!item) {
214 // Even when there's no item, we should still have the Paste action on top
215 // (see https://gitlab.com/inkscape/inkscape/-/issues/4150)
216 gmenu->append_section(create_clipboard_actions(true));
217
218 gmenu_section = Gio::Menu::create();
219 AppendItemFromAction(gmenu_section, "win.dialog-open('DocumentProperties')", _("Document Properties..."), "document-properties");
220 gmenu->append_section(gmenu_section);
221 } else {
222 // When an item is selected, show all three of Cut, Copy and Paste.
223 gmenu->append_section(create_clipboard_actions());
224
225 gmenu_section = Gio::Menu::create();
226 AppendItemFromAction(gmenu_section, "app.duplicate", _("Duplic_ate"), "edit-duplicate");
227 AppendItemFromAction(gmenu_section, "app.clone", _("_Clone"), "edit-clone");
228 AppendItemFromAction(gmenu_section, "app.delete-selection", _("_Delete"), "edit-delete");
229 gmenu->append_section(gmenu_section);
230
231 // Dialogs
232 auto gmenu_dialogs = Gio::Menu::create();
233 if (!hide_layers_and_objects_menu_item) { // Hidden when context menu is popped up in Layers and Objects dialog!
234 AppendItemFromAction(gmenu_dialogs, "win.dialog-open('Objects')", _("Layers and Objects..."), "dialog-objects" );
235 }
236 AppendItemFromAction(gmenu_dialogs, "win.dialog-open('ObjectProperties')", _("_Object Properties..."), "dialog-object-properties" );
237
238 if (is<SPShape>(item) || is<SPText>(item) || is<SPGroup>(item)) {
239 AppendItemFromAction(gmenu_dialogs, "win.dialog-open('FillStroke')", _("_Fill and Stroke..."), "dialog-fill-and-stroke" );
240 }
241
242 // Image dialogs (mostly).
243 if (auto image = cast<SPImage>(item)) {
244 AppendItemFromAction( gmenu_dialogs, "win.dialog-open('Trace')", _("_Trace Bitmap..."), "bitmap-trace" );
245
246 if (image->getClipObject()) {
247 AppendItemFromAction( gmenu_dialogs, "app.element-image-crop", _("Crop Image to Clip"), "" );
248 }
249 if (strncmp(image->href, "data", 4) == 0) {
250 // Image is embedded.
251 AppendItemFromAction( gmenu_dialogs, "app.org.inkscape.filter.extract-image", _("Extract Image..."), "" );
252 } else {
253 // Image is linked.
254 AppendItemFromAction( gmenu_dialogs, "app.org.inkscape.filter.selected.embed-image", _("Embed Image"), "" );
255 AppendItemFromAction( gmenu_dialogs, "app.element-image-edit", _("Edit Externally..."), "" );
256 }
257 }
258
259 // A clone of an image supports "Edit Externally" as well
260 if (is_clone_of_image(item)) {
261 AppendItemFromAction(gmenu_dialogs, "app.element-image-edit", _("Edit Externally..."), "");
262 }
263
264 // Text dialogs.
265 if (is<SPText>(item)) {
266 AppendItemFromAction( gmenu_dialogs, "win.dialog-open('Text')", _("_Text and Font..."), "dialog-text-and-font" );
267 AppendItemFromAction( gmenu_dialogs, "win.dialog-open('Spellcheck')", _("Check Spellin_g..."), "tools-check-spelling" );
268 }
269 gmenu->append_section(gmenu_dialogs); // We might add to it later...
270
271 if (!is<SPAnchor>(item)) {
272 // Item menu
273
274 // Selection
275 gmenu_section = Gio::Menu::create();
276 auto gmenu_submenu = Gio::Menu::create();
277 AppendItemFromAction( gmenu_submenu, "win.select-same-fill-and-stroke", _("Fill _and Stroke"), "edit-select-same-fill-and-stroke");
278 AppendItemFromAction( gmenu_submenu, "win.select-same-fill", _("_Fill Color"), "edit-select-same-fill" );
279 AppendItemFromAction( gmenu_submenu, "win.select-same-stroke-color", _("_Stroke Color"), "edit-select-same-stroke-color" );
280 AppendItemFromAction( gmenu_submenu, "win.select-same-stroke-style", _("Stroke St_yle"), "edit-select-same-stroke-style" );
281 AppendItemFromAction( gmenu_submenu, "win.select-same-object-type", _("_Object Type"), "edit-select-same-object-type" );
282 gmenu_section->append_submenu(_("Select Sa_me"), gmenu_submenu);
283 gmenu->append_section(gmenu_section);
284
285 // Groups and Layers
286 gmenu_section = Gio::Menu::create();
287 AppendItemFromAction( gmenu_section, "win.selection-move-to-layer", _("_Move to Layer..."), "" );
288 AppendItemFromAction( gmenu_section, "app.selection-link", _("Create Anchor (Hyperlink)"), "" );
289 AppendItemFromAction( gmenu_section, "app.selection-group", _("_Group"), "" );
290 if (is<SPGroup>(item)) {
291 AppendItemFromAction( gmenu_section, "app.selection-ungroup", _("_Ungroup"), "" );
292 Glib::ustring label = Glib::ustring::compose(_("Enter Group %1"), item->defaultLabel());
293 AppendItemFromAction( gmenu_section, "win.selection-group-enter", label, "" );
294 if (!layer && (item->getParentGroup()->isLayer() || item->getParentGroup() == root)) {
295 // A layer should be a child of root or another layer.
296 AppendItemFromAction( gmenu_section, "win.layer-from-group", _("Group to Layer"), "" );
297 }
298 }
299 auto group = cast<SPGroup>(item->parent);
300 if (group && !group->isLayer()) {
301 AppendItemFromAction( gmenu_section, "win.selection-group-exit", _("Exit Group"), "" );
302 AppendItemFromAction( gmenu_section, "app.selection-ungroup-pop", _("_Pop Selection out of Group"), "" );
303 }
304 gmenu->append_section(gmenu_section);
305
306 // Clipping and Masking
307 gmenu_section = Gio::Menu::create();
308 if (selection.size() > 1) {
309 AppendItemFromAction( gmenu_section, "app.object-set-clip", _("Set Cl_ip"), "" );
310 }
311 if (item->getClipObject()) {
312 AppendItemFromAction( gmenu_section, "app.object-release-clip", _("Release C_lip"), "" );
313 } else {
314 AppendItemFromAction( gmenu_section, "app.object-set-clip-group", _("Set Clip G_roup"), "" );
315 }
316 if (selection.size() > 1) {
317 AppendItemFromAction( gmenu_section, "app.object-set-mask", _("Set Mask"), "" );
318 }
319 if (item->getMaskObject()) {
320 AppendItemFromAction( gmenu_section, "app.object-release-mask", _("Release Mask"), "" );
321 }
322 gmenu->append_section(gmenu_section);
323
324 // Hide and Lock
325 gmenu_section = Gio::Menu::create();
326 AppendItemFromAction( gmenu_section, "app.selection-hide", _("Hide Selected Objects"), "" );
327 AppendItemFromAction( gmenu_section, "app.selection-lock", _("Lock Selected Objects"), "" );
328 gmenu->append_section(gmenu_section);
329
330 } else {
331 // Anchor menu
332 gmenu_section = Gio::Menu::create();
333 AppendItemFromAction( gmenu_section, "app.element-a-open-link", _("_Open Link in Browser"), "" );
334 AppendItemFromAction( gmenu_section, "app.selection-ungroup", _("_Remove Link"), "" );
335 AppendItemFromAction( gmenu_section, "win.selection-group-enter", _("Enter Group"), "" );
336 gmenu->append_section(gmenu_section);
337 }
338 }
339
340 // Hidden or locked beneath cursor
341 gmenu_section = Gio::Menu::create();
342 if (has_hidden_below_cursor) {
343 AppendItemFromAction( gmenu_section, "ctx.unhide-objects-below-cursor", _("Unhide Objects Below Cursor"), "" );
344 }
345 if (has_locked_below_cursor) {
346 AppendItemFromAction( gmenu_section, "ctx.unlock-objects-below-cursor", _("Unlock Objects Below Cursor"), "" );
347 }
348 gmenu->append_section(gmenu_section);
349 } else {
350 // Layers: Only used in "Layers and Objects" dialog.
351
352 gmenu_section = Gio::Menu::create();
353 AppendItemFromAction(gmenu_section, "win.layer-new", _("_Add Layer..."), "layer-new");
354 AppendItemFromAction(gmenu_section, "win.layer-duplicate", _("D_uplicate Layer"), "layer-duplicate");
355 AppendItemFromAction(gmenu_section, "win.layer-delete", _("_Delete Layer"), "layer-delete");
356 AppendItemFromAction(gmenu_section, "win.layer-rename", _("Re_name Layer..."), "layer-rename");
357 AppendItemFromAction(gmenu_section, "win.layer-to-group", _("Layer to _Group"), "dialog-objects");
358 gmenu->append_section(gmenu_section);
359
360 gmenu_section = Gio::Menu::create();
361 AppendItemFromAction(gmenu_section, "win.layer-raise", _("_Raise Layer"), "layer-raise");
362 AppendItemFromAction(gmenu_section, "win.layer-lower", _("_Lower Layer"), "layer-lower");
363 gmenu->append_section(gmenu_section);
364
365 gmenu_section = Gio::Menu::create();
366 AppendItemFromAction(gmenu_section, "win.layer-hide-toggle-others", _("_Hide/Show Other Layers"), "");
367 AppendItemFromAction(gmenu_section, "win.layer-hide-all", _("_Hide All Layers"), "");
368 AppendItemFromAction(gmenu_section, "win.layer-unhide-all", _("_Show All Layers"), "");
369 gmenu->append_section(gmenu_section);
370
371 gmenu_section = Gio::Menu::create();
372 AppendItemFromAction(gmenu_section, "win.layer-lock-toggle-others", _("_Lock/Unlock Other Layers"), "");
373 AppendItemFromAction(gmenu_section, "win.layer-lock-all", _("_Lock All Layers"), "");
374 AppendItemFromAction(gmenu_section, "win.layer-unlock-all", _("_Unlock All Layers"), "");
375 gmenu->append_section(gmenu_section);
376 }
377 // clang-format on
378
379 set_menu_model(gmenu);
380 set_position(Gtk::PositionType::BOTTOM);
381 set_halign(Gtk::Align::START);
382 set_has_arrow(false);
383 show_all_images(*this);
384 set_flags(Gtk::PopoverMenu::Flags::NESTED);
385
386 // Do not install this CSS provider; it messes up menus with icons (like popup menu with all dialogs).
387 // It doesn't work well with context menu either, introducing disturbing visual glitch
388 // where menu shifts upon opening.
390 bool const shift_icons = prefs->getInt("/theme/shiftIcons", true);
391 set_tooltips_and_shift_icons(*this, shift_icons);
392 // Set the style and icon theme of the new menu based on the desktop
393 if (auto const window = desktop->getInkscapeWindow()) {
394 if (window->has_css_class("dark")) {
395 add_css_class("dark");
396 } else {
397 add_css_class("bright");
398 }
399 if (prefs->getBool("/theme/symbolicIcons", false)) {
400 add_css_class("symbolic");
401 } else {
402 add_css_class("regular");
403 }
404 }
405}
406
407void
409{
410 for (auto item : items_under_cursor) {
411 if (unhide) {
412 if (item->isHidden()) {
413 item->setHidden(false);
414 }
415 } else {
416 if (item->isLocked()) {
417 item->setLocked(false, true);
418 }
419 }
420 }
421
422 // We wouldn't be here if we didn't make a change.
423 Inkscape::DocumentUndo::done(document, (unhide ? _("Unhid objects") : _("Unlocked objects")), "");
424}
425
426/*
427 Local Variables:
428 mode:c++
429 c-file-style:"stroustrup"
430 c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
431 indent-tabs-mode:nil
432 fill-column:99
433 End:
434*/
435// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :
uint64_t page
Definition canvas.cpp:171
std::vector< SPItem * > items_under_cursor
Definition contextmenu.h:40
ContextMenu(SPDesktop *desktop, SPObject *object, std::vector< SPItem * > const &items_under_cursor, bool hide_layers_and_objects_menu_item=false)
Glib::RefPtr< Gio::SimpleActionGroup > action_group
Definition contextmenu.h:39
void unhide_or_unlock(SPDocument *document, bool unhide)
static void done(SPDocument *document, Glib::ustring const &event_description, Glib::ustring const &undo_icon, unsigned int object_modified_tag=0)
static SPGroup * asLayer(SPObject *object)
Return the SPGroup if we have a layer object.
SPGroup * currentRoot() const
Returns current root (=bottom) layer.
Preference storage class.
Definition preferences.h:61
bool getBool(Glib::ustring const &pref_path, bool def=false)
Retrieve a Boolean value.
static Preferences * get()
Access the singleton Preferences object.
int getInt(Glib::ustring const &pref_path, int def=0)
Retrieve an integer.
The set of selected SPObjects for a given document and layer model.
Definition selection.h:80
void set(XML::Node *repr)
Set the selection to an XML node's SPObject.
Definition selection.h:118
bool includes(XML::Node *repr, bool anyAncestor=false)
Returns true if the given item is selected.
Definition selection.h:140
To do: update description of desktop.
Definition desktop.h:149
SPDocument * getDocument() const
Definition desktop.h:189
InkscapeWindow const * getInkscapeWindow() const
Definition desktop.cpp:975
Inkscape::Selection * getSelection() const
Definition desktop.h:188
Inkscape::LayerManager & layerManager()
Definition desktop.h:287
Typed SVG document implementation.
Definition document.h:101
bool isLayer() const
Base class for visual SVG elements.
Definition sp-item.h:109
SPGroup * getParentGroup() const
Return the parent, only if it's a group object.
Definition sp-item.cpp:480
SPMask * getMaskObject() const
Definition sp-item.cpp:177
bool isHidden() const
Definition sp-item.cpp:242
void setLocked(bool lock, bool recursive=false)
Definition sp-item.cpp:228
bool isLocked() const
Definition sp-item.cpp:218
void setHidden(bool hidden)
Definition sp-item.cpp:248
SPClipPath * getClipObject() const
Definition sp-item.cpp:102
SPObject is an abstract base class of all of the document nodes at the SVG document level.
Definition sp-object.h:160
char const * defaultLabel() const
Returns a default label property for this object.
SPObject * parent
Definition sp-object.h:189
ChildrenList children
Definition sp-object.h:907
RootCluster root
static bool childrenIncludedInSelection(SPItem *item, Inkscape::Selection &selection)
void show_all_images(Gtk::Widget &parent)
Recursively force all Image descendants with :storage-type other than EMPTY to :visible = TRUE.
static Glib::RefPtr< Gio::Menu > create_clipboard_actions(bool const paste_only=false)
Create a menu section containing the standard editing actions: Cut, Copy, Paste, Paste....
static void AppendItemFromAction(Glib::RefPtr< Gio::Menu > const &gmenu, Glib::ustring const &action, Glib::ustring const &label, Glib::ustring const &icon={})
bool is_clone_of_image(SPItem const *item)
Check if the item is a clone of an image.
Css & result
A class to hold:
Editable view implementation.
static char const *const parent
Definition dir-util.cpp:70
TODO: insert short description here.
std::unique_ptr< Magick::Image > image
SPItem * item
Inkscape - An SVG editor.
Glib::ustring label
bool set_tooltips_and_shift_icons(Gtk::Widget &menu, bool const shift_icons)
Go over a widget representing a menu, & set tooltips on its items from app label-to-tooltip map.
Go over a widget representing a menu, & set tooltips on its items from app label-to-tooltip map.
Gtk::Widget * for_each_descendant(Gtk::Widget &widget, Func &&func)
Like for_each_child() but also tests the initial widget & recurses through childrenʼs children.
Definition util.h:132
Ocnode * child[8]
Definition quantize.cpp:33
GList * items
static void unhide(SPItem *item, SPDesktop *desktop)
SVG <image> implementation.
SPPage – a page object.
SPDesktop * desktop