1 // SPDX-License-Identifier: GPL-2.0-or-later
2 /**
3  * @file Object properties dialog.
4  */
5 /*
6  * Inkscape, an Open Source vector graphics editor
7  *
8  * This program is free software; you can redistribute it and/or
9  * modify it under the terms of the GNU General Public License
10  * as published by the Free Software Foundation; either version 2
11  * of the License, or (at your option) any later version.
12  *
13  * This program is distributed in the hope that it will be useful,
14  * but WITHOUT ANY WARRANTY; without even the implied warranty of
15  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16  * GNU General Public License for more details.
17  *
18  * You should have received a copy of the GNU General Public License
19  * along with this program; if not, write to the Free Software
20  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
21  *
22  * Copyright (C) 2012 Kris De Gussem <Kris.DeGussem@gmail.com>
23  * c++ version based on former C-version (GPL v2+) with authors:
24  *   Lauris Kaplinski <lauris@kaplinski.com>
25  *   bulia byak <buliabyak@users.sf.net>
26  *   Johan Engelen <goejendaagh@zonnet.nl>
27  *   Abhishek Sharma
28  */
29 
30 #include "object-properties.h"
31 
32 #include <glibmm/i18n.h>
33 
34 #include <gtkmm/grid.h>
35 
36 #include "desktop.h"
37 #include "document-undo.h"
38 #include "document.h"
39 #include "inkscape.h"
40 #include "verbs.h"
41 #include "style.h"
42 #include "style-enums.h"
43 
44 #include "object/sp-image.h"
45 
46 #include "widgets/sp-attribute-widget.h"
47 
48 namespace Inkscape {
49 namespace UI {
50 namespace Dialog {
51 
ObjectProperties()52 ObjectProperties::ObjectProperties()
53     : DialogBase("/dialogs/object/", SP_VERB_DIALOG_ITEM)
54     , _blocked(false)
55     , _current_item(nullptr)
56     , _label_id(_("_ID:"), true)
57     , _label_label(_("_Label:"), true)
58     , _label_title(_("_Title:"), true)
59     , _label_dpi(_("_DPI SVG:"), true)
60     , _label_image_rendering(_("_Image Rendering:"), true)
61     , _cb_hide(_("_Hide"), true)
62     , _cb_lock(_("L_ock"), true)
63     , _cb_aspect_ratio(_("Preserve Ratio"), true)
64     , _exp_interactivity(_("_Interactivity"), true)
65     , _attr_table(Gtk::manage(new SPAttributeTable()))
66     , _desktop(nullptr)
67 {
68     //initialize labels for the table at the bottom of the dialog
69     _int_attrs.emplace_back("onclick");
70     _int_attrs.emplace_back("onmouseover");
71     _int_attrs.emplace_back("onmouseout");
72     _int_attrs.emplace_back("onmousedown");
73     _int_attrs.emplace_back("onmouseup");
74     _int_attrs.emplace_back("onmousemove");
75     _int_attrs.emplace_back("onfocusin");
76     _int_attrs.emplace_back("onfocusout");
77     _int_attrs.emplace_back("onload");
78 
79     _int_labels.emplace_back("onclick:");
80     _int_labels.emplace_back("onmouseover:");
81     _int_labels.emplace_back("onmouseout:");
82     _int_labels.emplace_back("onmousedown:");
83     _int_labels.emplace_back("onmouseup:");
84     _int_labels.emplace_back("onmousemove:");
85     _int_labels.emplace_back("onfocusin:");
86     _int_labels.emplace_back("onfocusout:");
87     _int_labels.emplace_back("onload:");
88 
89     _init();
90 }
91 
~ObjectProperties()92 ObjectProperties::~ObjectProperties()
93 {
94     _subselection_changed_connection.disconnect();
95     _selection_changed_connection.disconnect();
96 }
97 
_init()98 void ObjectProperties::_init()
99 {
100     set_spacing(0);
101 
102     auto grid_top = Gtk::manage(new Gtk::Grid());
103     grid_top->set_row_spacing(4);
104     grid_top->set_column_spacing(0);
105     grid_top->set_border_width(4);
106 
107     pack_start(*grid_top, false, false, 0);
108 
109 
110     /* Create the label for the object id */
111     _label_id.set_label(_label_id.get_label() + " ");
112     _label_id.set_halign(Gtk::ALIGN_START);
113     _label_id.set_valign(Gtk::ALIGN_CENTER);
114     grid_top->attach(_label_id, 0, 0, 1, 1);
115 
116     /* Create the entry box for the object id */
117     _entry_id.set_tooltip_text(_("The id= attribute (only letters, digits, and the characters .-_: allowed)"));
118     _entry_id.set_max_length(64);
119     _entry_id.set_hexpand();
120     _entry_id.set_valign(Gtk::ALIGN_CENTER);
121     grid_top->attach(_entry_id, 1, 0, 1, 1);
122 
123     _label_id.set_mnemonic_widget(_entry_id);
124 
125     // pressing enter in the id field is the same as clicking Set:
126     _entry_id.signal_activate().connect(sigc::mem_fun(this, &ObjectProperties::_labelChanged));
127     // focus is in the id field initially:
128     _entry_id.grab_focus();
129 
130 
131     /* Create the label for the object label */
132     _label_label.set_label(_label_label.get_label() + " ");
133     _label_label.set_halign(Gtk::ALIGN_START);
134     _label_label.set_valign(Gtk::ALIGN_CENTER);
135     grid_top->attach(_label_label, 0, 1, 1, 1);
136 
137     /* Create the entry box for the object label */
138     _entry_label.set_tooltip_text(_("A freeform label for the object"));
139     _entry_label.set_max_length(256);
140 
141     _entry_label.set_hexpand();
142     _entry_label.set_valign(Gtk::ALIGN_CENTER);
143     grid_top->attach(_entry_label, 1, 1, 1, 1);
144 
145     _label_label.set_mnemonic_widget(_entry_label);
146 
147     // pressing enter in the label field is the same as clicking Set:
148     _entry_label.signal_activate().connect(sigc::mem_fun(this, &ObjectProperties::_labelChanged));
149 
150 
151     /* Create the label for the object title */
152     _label_title.set_label(_label_title.get_label() + " ");
153     _label_title.set_halign(Gtk::ALIGN_START);
154     _label_title.set_valign(Gtk::ALIGN_CENTER);
155     grid_top->attach(_label_title, 0, 2, 1, 1);
156 
157     /* Create the entry box for the object title */
158     _entry_title.set_sensitive (FALSE);
159     _entry_title.set_max_length (256);
160 
161     _entry_title.set_hexpand();
162     _entry_title.set_valign(Gtk::ALIGN_CENTER);
163     grid_top->attach(_entry_title, 1, 2, 1, 1);
164 
165     _label_title.set_mnemonic_widget(_entry_title);
166     // pressing enter in the label field is the same as clicking Set:
167     _entry_title.signal_activate().connect(sigc::mem_fun(this, &ObjectProperties::_labelChanged));
168 
169     /* Create the frame for the object description */
170     Gtk::Label *label_desc = Gtk::manage(new Gtk::Label(_("_Description:"), true));
171     UI::Widget::Frame *frame_desc = Gtk::manage(new UI::Widget::Frame("", FALSE));
172     frame_desc->set_label_widget(*label_desc);
173     frame_desc->set_padding (0,0,0,0);
174     pack_start(*frame_desc, true, true, 0);
175 
176     /* Create the text view box for the object description */
177     _ft_description.set_border_width(4);
178     _ft_description.set_sensitive(FALSE);
179     frame_desc->add(_ft_description);
180     _ft_description.set_shadow_type(Gtk::SHADOW_IN);
181 
182     _tv_description.set_wrap_mode(Gtk::WRAP_WORD);
183     _tv_description.get_buffer()->set_text("");
184     _ft_description.add(_tv_description);
185     _tv_description.add_mnemonic_label(*label_desc);
186 
187     /* Create the label for the object title */
188     _label_dpi.set_label(_label_dpi.get_label() + " ");
189     _label_dpi.set_halign(Gtk::ALIGN_START);
190     _label_dpi.set_valign(Gtk::ALIGN_CENTER);
191     grid_top->attach(_label_dpi, 0, 3, 1, 1);
192 
193     /* Create the entry box for the SVG DPI */
194     _spin_dpi.set_digits(2);
195     _spin_dpi.set_range(1, 1200);
196     grid_top->attach(_spin_dpi, 1, 3, 1, 1);
197 
198     _label_dpi.set_mnemonic_widget(_spin_dpi);
199     // pressing enter in the label field is the same as clicking Set:
200     _spin_dpi.signal_activate().connect(sigc::mem_fun(this, &ObjectProperties::_labelChanged));
201 
202     /* Image rendering */
203     /* Create the label for the object ImageRendering */
204     _label_image_rendering.set_label(_label_image_rendering.get_label() + " ");
205     _label_image_rendering.set_halign(Gtk::ALIGN_START);
206     _label_image_rendering.set_valign(Gtk::ALIGN_CENTER);
207     grid_top->attach(_label_image_rendering, 0, 4, 1, 1);
208 
209     /* Create the combo box text for the 'image-rendering' property  */
210     for (unsigned i = 0; enum_image_rendering[i].key; ++i) {
211         _combo_image_rendering.append(enum_image_rendering[i].key);
212     }
213     _combo_image_rendering.set_tooltip_text(_("The 'image-rendering' property can influence how a bitmap is re-scaled:\n"
214                                               "\t• 'auto': no preference (scaled image is usually smooth but blurred)\n"
215                                               "\t• 'optimizeQuality': prefer rendering quality (usually smooth but blurred)\n"
216                                               "\t• 'optimizeSpeed': prefer rendering speed (usually blocky)\n"
217                                               "\t• 'crisp-edges': rescale without blurring edges (often blocky)\n"
218                                               "\t• 'pixelated': render blocky\n"
219                                               "Note that the specification of this property is not finalized. "
220                                               "Support and interpretation of these values varies between renderers."));
221 
222     _combo_image_rendering.set_valign(Gtk::ALIGN_CENTER);
223     grid_top->attach(_combo_image_rendering, 1, 4, 1, 1);
224 
225     _label_image_rendering.set_mnemonic_widget(_combo_image_rendering);
226 
227     _combo_image_rendering.signal_changed().connect(
228         sigc::mem_fun(this, &ObjectProperties::_imageRenderingChanged)
229     );
230 
231 
232 
233     /* Check boxes */
234     Gtk::Box *hb_checkboxes = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL));
235     pack_start(*hb_checkboxes, Gtk::PACK_SHRINK, 0);
236 
237     auto grid_cb = Gtk::manage(new Gtk::Grid());
238     grid_cb->set_row_homogeneous();
239     grid_cb->set_column_homogeneous(true);
240 
241     grid_cb->set_border_width(4);
242     hb_checkboxes->pack_start(*grid_cb, true, true, 0);
243 
244     /* Hide */
245     _cb_hide.set_tooltip_text (_("Check to make the object invisible"));
246     _cb_hide.set_hexpand();
247     _cb_hide.set_valign(Gtk::ALIGN_CENTER);
248     grid_cb->attach(_cb_hide, 0, 0, 1, 1);
249 
250     _cb_hide.signal_toggled().connect(sigc::mem_fun(this, &ObjectProperties::_hiddenToggled));
251 
252     /* Lock */
253     // TRANSLATORS: "Lock" is a verb here
254     _cb_lock.set_tooltip_text(_("Check to make the object insensitive (not selectable by mouse)"));
255     _cb_lock.set_hexpand();
256     _cb_lock.set_valign(Gtk::ALIGN_CENTER);
257     grid_cb->attach(_cb_lock, 1, 0, 1, 1);
258 
259     _cb_lock.signal_toggled().connect(sigc::mem_fun(this, &ObjectProperties::_sensitivityToggled));
260 
261     /* Preserve aspect ratio */
262     _cb_aspect_ratio.set_tooltip_text(_("Check to preserve aspect ratio on images"));
263     _cb_aspect_ratio.set_hexpand();
264     _cb_aspect_ratio.set_valign(Gtk::ALIGN_CENTER);
265     grid_cb->attach(_cb_aspect_ratio, 0, 1, 1, 1);
266 
267     _cb_aspect_ratio.signal_toggled().connect(sigc::mem_fun(this, &ObjectProperties::_aspectRatioToggled));
268 
269 
270     /* Button for setting the object's id, label, title and description. */
271     Gtk::Button *btn_set = Gtk::manage(new Gtk::Button(_("_Set"), true));
272     btn_set->set_hexpand();
273     btn_set->set_valign(Gtk::ALIGN_CENTER);
274     grid_cb->attach(*btn_set, 1, 1, 1, 1);
275 
276     btn_set->signal_clicked().connect(sigc::mem_fun(this, &ObjectProperties::_labelChanged));
277 
278     /* Interactivity options */
279     _exp_interactivity.set_vexpand(false);
280     pack_start(_exp_interactivity, Gtk::PACK_SHRINK);
281 
282     show_all();
283     update_entries();
284 }
285 
update_entries()286 void ObjectProperties::update_entries()
287 {
288     if (_blocked || !_desktop) {
289         return;
290     }
291     if (SP_ACTIVE_DESKTOP != _desktop) {
292         return;
293     }
294 
295     Inkscape::Selection *selection = SP_ACTIVE_DESKTOP->getSelection();
296 
297     if (!selection->singleItem()) {
298         set_sensitive (false);
299         _current_item = nullptr;
300         //no selection anymore or multiple objects selected, means that we need
301         //to close the connections to the previously selected object
302         _attr_table->clear();
303         return;
304     } else {
305         set_sensitive (true);
306     }
307 
308     SPItem *item = selection->singleItem();
309     if (_current_item == item)
310     {
311         //otherwise we would end up wasting resources through the modify selection
312         //callback when moving an object (endlessly setting the labels and recreating _attr_table)
313         return;
314     }
315     _blocked = true;
316     _cb_aspect_ratio.set_active(g_strcmp0(item->getAttribute("preserveAspectRatio"), "none") != 0);
317     _cb_lock.set_active(item->isLocked());           /* Sensitive */
318     _cb_hide.set_active(item->isExplicitlyHidden()); /* Hidden */
319 
320     if (item->cloned) {
321         /* ID */
322         _entry_id.set_text("");
323         _entry_id.set_sensitive(FALSE);
324         _label_id.set_text(_("Ref"));
325 
326         /* Label */
327         _entry_label.set_text("");
328         _entry_label.set_sensitive(FALSE);
329         _label_label.set_text(_("Ref"));
330 
331     } else {
332         SPObject *obj = static_cast<SPObject*>(item);
333 
334         /* ID */
335         _entry_id.set_text(obj->getId() ? obj->getId() : "");
336         _entry_id.set_sensitive(TRUE);
337         _label_id.set_markup_with_mnemonic(_("_ID:") + Glib::ustring(" "));
338 
339         /* Label */
340         char const *currentlabel = obj->label();
341         char const *placeholder = "";
342         if (!currentlabel) {
343             currentlabel = "";
344             placeholder = obj->defaultLabel();
345         }
346         _entry_label.set_text(currentlabel);
347         _entry_label.set_placeholder_text(placeholder);
348         _entry_label.set_sensitive(TRUE);
349 
350         /* Title */
351         gchar *title = obj->title();
352         if (title) {
353             _entry_title.set_text(title);
354             g_free(title);
355         }
356         else {
357             _entry_title.set_text("");
358         }
359         _entry_title.set_sensitive(TRUE);
360 
361         /* Image Rendering */
362         if (SP_IS_IMAGE(item)) {
363             _combo_image_rendering.show();
364             _label_image_rendering.show();
365             _combo_image_rendering.set_active(obj->style->image_rendering.value);
366             if (obj->getAttribute("inkscape:svg-dpi")) {
367                 _spin_dpi.set_value(std::stod(obj->getAttribute("inkscape:svg-dpi")));
368                 _spin_dpi.show();
369                 _label_dpi.show();
370             } else {
371                 _spin_dpi.hide();
372                 _label_dpi.hide();
373             }
374         } else {
375             _combo_image_rendering.hide();
376             _combo_image_rendering.unset_active();
377             _label_image_rendering.hide();
378             _spin_dpi.hide();
379             _label_dpi.hide();
380         }
381 
382         /* Description */
383         gchar *desc = obj->desc();
384         if (desc) {
385             _tv_description.get_buffer()->set_text(desc);
386             g_free(desc);
387         } else {
388             _tv_description.get_buffer()->set_text("");
389         }
390         _ft_description.set_sensitive(TRUE);
391 
392         if (_current_item == nullptr) {
393             _attr_table->set_object(obj, _int_labels, _int_attrs, (GtkWidget*) _exp_interactivity.gobj());
394         } else {
395             _attr_table->change_object(obj);
396         }
397         _attr_table->show_all();
398     }
399     _current_item = item;
400     _blocked = false;
401 }
402 
_labelChanged()403 void ObjectProperties::_labelChanged()
404 {
405     if (_blocked) {
406         return;
407     }
408 
409     SPItem *item = SP_ACTIVE_DESKTOP->getSelection()->singleItem();
410     g_return_if_fail (item != nullptr);
411 
412     _blocked = true;
413 
414     /* Retrieve the label widget for the object's id */
415     gchar *id = g_strdup(_entry_id.get_text().c_str());
416     g_strcanon(id, "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_.:", '_');
417     if (g_strcmp0(id, item->getId()) == 0) {
418         _label_id.set_markup_with_mnemonic(_("_ID:") + Glib::ustring(" "));
419     } else if (!*id || !isalnum (*id)) {
420         _label_id.set_text(_("Id invalid! "));
421     } else if (SP_ACTIVE_DOCUMENT->getObjectById(id) != nullptr) {
422         _label_id.set_text(_("Id exists! "));
423     } else {
424         SPException ex;
425         _label_id.set_markup_with_mnemonic(_("_ID:") + Glib::ustring(" "));
426         SP_EXCEPTION_INIT(&ex);
427         item->setAttribute("id", id, &ex);
428         DocumentUndo::done(SP_ACTIVE_DOCUMENT, SP_VERB_DIALOG_ITEM, _("Set object ID"));
429     }
430     g_free(id);
431 
432     /* Retrieve the label widget for the object's label */
433     Glib::ustring label = _entry_label.get_text();
434 
435     /* Give feedback on success of setting the drawing object's label
436      * using the widget's label text
437      */
438     SPObject *obj = static_cast<SPObject*>(item);
439     char const *currentlabel = obj->label();
440     if (label.compare(currentlabel ? currentlabel : "")) {
441         obj->setLabel(label.c_str());
442         DocumentUndo::done(SP_ACTIVE_DOCUMENT, SP_VERB_DIALOG_ITEM,
443                 _("Set object label"));
444     }
445 
446     /* Retrieve the title */
447     if (obj->setTitle(_entry_title.get_text().c_str())) {
448         DocumentUndo::done(SP_ACTIVE_DOCUMENT, SP_VERB_DIALOG_ITEM,
449                 _("Set object title"));
450     }
451 
452     /* Retrieve the DPI */
453     if (SP_IS_IMAGE(obj)) {
454         Glib::ustring dpi_value = Glib::ustring::format(_spin_dpi.get_value());
455         obj->setAttribute("inkscape:svg-dpi", dpi_value);
456         DocumentUndo::done(SP_ACTIVE_DOCUMENT, SP_VERB_DIALOG_ITEM, _("Set image DPI"));
457     }
458 
459     /* Retrieve the description */
460     Gtk::TextBuffer::iterator start, end;
461     _tv_description.get_buffer()->get_bounds(start, end);
462     Glib::ustring desc = _tv_description.get_buffer()->get_text(start, end, TRUE);
463     if (obj->setDesc(desc.c_str())) {
464         DocumentUndo::done(SP_ACTIVE_DOCUMENT, SP_VERB_DIALOG_ITEM,
465                 _("Set object description"));
466     }
467 
468     _blocked = false;
469 }
470 
_imageRenderingChanged()471 void ObjectProperties::_imageRenderingChanged()
472 {
473     if (_blocked) {
474         return;
475     }
476 
477     SPItem *item = SP_ACTIVE_DESKTOP->getSelection()->singleItem();
478     g_return_if_fail (item != nullptr);
479 
480     _blocked = true;
481 
482     Glib::ustring scale = _combo_image_rendering.get_active_text();
483 
484     // We should unset if the parent computed value is auto and the desired value is auto.
485     SPCSSAttr *css = sp_repr_css_attr_new();
486     sp_repr_css_set_property(css, "image-rendering", scale.c_str());
487     Inkscape::XML::Node *image_node = item->getRepr();
488     if (image_node) {
489         sp_repr_css_change(image_node, css, "style");
490         DocumentUndo::done(SP_ACTIVE_DOCUMENT, SP_VERB_DIALOG_ITEM,
491                 _("Set image rendering option"));
492     }
493     sp_repr_css_attr_unref(css);
494 
495     _blocked = false;
496 }
497 
_sensitivityToggled()498 void ObjectProperties::_sensitivityToggled()
499 {
500     if (_blocked) {
501         return;
502     }
503 
504     SPItem *item = SP_ACTIVE_DESKTOP->getSelection()->singleItem();
505     g_return_if_fail(item != nullptr);
506 
507     _blocked = true;
508     item->setLocked(_cb_lock.get_active());
509     DocumentUndo::done(SP_ACTIVE_DOCUMENT, SP_VERB_DIALOG_ITEM,
510                        _cb_lock.get_active() ? _("Lock object") : _("Unlock object"));
511     _blocked = false;
512 }
513 
_aspectRatioToggled()514 void ObjectProperties::_aspectRatioToggled()
515 {
516     if (_blocked) {
517         return;
518     }
519 
520     SPItem *item = SP_ACTIVE_DESKTOP->getSelection()->singleItem();
521     g_return_if_fail(item != nullptr);
522 
523     _blocked = true;
524 
525     const char *active;
526     if (_cb_aspect_ratio.get_active()) {
527         active = "xMidYMid";
528     }
529     else {
530         active = "none";
531     }
532     /* Retrieve the DPI */
533     if (SP_IS_IMAGE(item)) {
534         Glib::ustring dpi_value = Glib::ustring::format(_spin_dpi.get_value());
535         item->setAttribute("preserveAspectRatio", active);
536         DocumentUndo::done(SP_ACTIVE_DOCUMENT, SP_VERB_DIALOG_ITEM, _("Set preserve ratio"));
537     }
538     _blocked = false;
539 }
540 
_hiddenToggled()541 void ObjectProperties::_hiddenToggled()
542 {
543     if (_blocked) {
544         return;
545     }
546 
547     SPItem *item = SP_ACTIVE_DESKTOP->getSelection()->singleItem();
548     g_return_if_fail(item != nullptr);
549 
550     _blocked = true;
551     item->setExplicitlyHidden(_cb_hide.get_active());
552     DocumentUndo::done(SP_ACTIVE_DOCUMENT, SP_VERB_DIALOG_ITEM,
553                _cb_hide.get_active() ? _("Hide object") : _("Unhide object"));
554     _blocked = false;
555 }
556 
update()557 void ObjectProperties::update()
558 {
559     if (!_app) {
560         std::cerr << "ObjectProperties::update(): _app is null" << std::endl;
561         return;
562     }
563 
564     SPDesktop *desktop = getDesktop();
565 
566     if (!desktop) {
567         return;
568     }
569 
570     if (this->_desktop != desktop) {
571         if (this->_desktop) {
572             _subselection_changed_connection.disconnect();
573             _selection_changed_connection.disconnect();
574         }
575         this->_desktop = desktop;
576         if (desktop && desktop->selection) {
577             _selection_changed_connection = desktop->selection->connectChanged(
578                 sigc::hide(sigc::mem_fun(*this, &ObjectProperties::update_entries))
579             );
580             _subselection_changed_connection = desktop->connectToolSubselectionChanged(
581                 sigc::hide(sigc::mem_fun(*this, &ObjectProperties::update_entries))
582             );
583         }
584         update_entries();
585     }
586 }
587 
588 }
589 }
590 }
591 
592 /*
593   Local Variables:
594   mode:c++
595   c-file-style:"stroustrup"
596   c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
597   indent-tabs-mode:nil
598   fill-column:99
599   End:
600 */
601 // vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :
602