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