1 // SPDX-License-Identifier: GPL-2.0-or-later
2 /*
3 * Inkscape guideline implementation
4 *
5 * Authors:
6 * Lauris Kaplinski <lauris@kaplinski.com>
7 * Peter Moulder <pmoulder@mail.csse.monash.edu.au>
8 * Johan Engelen
9 * Jon A. Cruz <jon@joncruz.org>
10 * Abhishek Sharma
11 *
12 * Copyright (C) 2000-2002 authors
13 * Copyright (C) 2004 Monash University
14 * Copyright (C) 2007 Johan Engelen
15 *
16 * Released under GNU GPL v2+, read the file 'COPYING' for more information.
17 */
18
19 #include <algorithm>
20 #include <cstring>
21 #include <vector>
22 #include <glibmm/i18n.h>
23
24 #include "attributes.h"
25 #include "desktop.h"
26 #include "desktop-events.h"
27 #include "document-undo.h"
28 #include "helper-fns.h"
29 #include "inkscape.h"
30 #include "remove-last.h"
31 #include "verbs.h"
32
33 #include "sp-guide.h"
34 #include "sp-item-notify-moveto.h"
35 #include "sp-namedview.h"
36 #include "sp-root.h"
37
38 #include "display/control/canvas-item-guideline.h"
39
40 #include "svg/stringstream.h"
41 #include "svg/svg-color.h"
42 #include "svg/svg.h"
43
44 #include "ui/widget/canvas.h" // Should really be here
45
46 #include "xml/repr.h"
47
48 using Inkscape::DocumentUndo;
49
50
SPGuide()51 SPGuide::SPGuide()
52 : SPObject()
53 , label(nullptr)
54 , locked(false)
55 , normal_to_line(Geom::Point(0.,1.))
56 , point_on_line(Geom::Point(0.,0.))
57 , color(0x0000ff7f)
58 , hicolor(0xff00007f)
59 {}
60
setColor(guint32 color)61 void SPGuide::setColor(guint32 color)
62 {
63 this->color = color;
64 for (auto view : views) {
65 view->set_stroke(color);
66 }
67 }
68
build(SPDocument * document,Inkscape::XML::Node * repr)69 void SPGuide::build(SPDocument *document, Inkscape::XML::Node *repr)
70 {
71 SPObject::build(document, repr);
72
73 this->readAttr(SPAttr::INKSCAPE_COLOR);
74 this->readAttr(SPAttr::INKSCAPE_LABEL);
75 this->readAttr(SPAttr::INKSCAPE_LOCKED);
76 this->readAttr(SPAttr::ORIENTATION);
77 this->readAttr(SPAttr::POSITION);
78
79 /* Register */
80 document->addResource("guide", this);
81 }
82
release()83 void SPGuide::release()
84 {
85 for(auto view : views) {
86 delete view;
87 }
88 this->views.clear();
89
90 if (this->document) {
91 // Unregister ourselves
92 this->document->removeResource("guide", this);
93 }
94
95 SPObject::release();
96 }
97
set(SPAttr key,const gchar * value)98 void SPGuide::set(SPAttr key, const gchar *value) {
99 switch (key) {
100 case SPAttr::INKSCAPE_COLOR:
101 if (value) {
102 this->setColor(sp_svg_read_color(value, 0x0000ff00) | 0x7f);
103 }
104 break;
105 case SPAttr::INKSCAPE_LABEL:
106 // this->label already freed in sp_guideline_set_label (src/display/guideline.cpp)
107 // see bug #1498444, bug #1469514
108 if (value) {
109 this->label = g_strdup(value);
110 } else {
111 this->label = nullptr;
112 }
113
114 this->set_label(this->label, false);
115 break;
116 case SPAttr::INKSCAPE_LOCKED:
117 if (value) {
118 this->set_locked(helperfns_read_bool(value, false), false);
119 }
120 break;
121 case SPAttr::ORIENTATION:
122 {
123 if (value && !strcmp(value, "horizontal")) {
124 /* Visual representation of a horizontal line, constrain vertically (y coordinate). */
125 this->normal_to_line = Geom::Point(0., 1.);
126 } else if (value && !strcmp(value, "vertical")) {
127 this->normal_to_line = Geom::Point(1., 0.);
128 } else if (value) {
129 gchar ** strarray = g_strsplit(value, ",", 2);
130 double newx, newy;
131 unsigned int success = sp_svg_number_read_d(strarray[0], &newx);
132 success += sp_svg_number_read_d(strarray[1], &newy);
133 g_strfreev (strarray);
134 if (success == 2 && (fabs(newx) > 1e-6 || fabs(newy) > 1e-6)) {
135 Geom::Point direction(newx, newy);
136
137 // <sodipodi:guide> stores inverted y-axis coordinates
138 if (document->is_yaxisdown()) {
139 direction[Geom::X] *= -1.0;
140 }
141
142 direction.normalize();
143 this->normal_to_line = direction;
144 } else {
145 // default to vertical line for bad arguments
146 this->normal_to_line = Geom::Point(1., 0.);
147 }
148 } else {
149 // default to vertical line for bad arguments
150 this->normal_to_line = Geom::Point(1., 0.);
151 }
152 this->set_normal(this->normal_to_line, false);
153 }
154 break;
155 case SPAttr::POSITION:
156 {
157 if (value) {
158 gchar ** strarray = g_strsplit(value, ",", 2);
159 double newx, newy;
160 unsigned int success = sp_svg_number_read_d(strarray[0], &newx);
161 success += sp_svg_number_read_d(strarray[1], &newy);
162 g_strfreev (strarray);
163 if (success == 2) {
164 // If root viewBox set, interpret guides in terms of viewBox (90/96)
165 SPRoot *root = document->getRoot();
166 if( root->viewBox_set ) {
167 if(Geom::are_near((root->width.computed * root->viewBox.height()) / (root->viewBox.width() * root->height.computed), 1.0, Geom::EPSILON)) {
168 // for uniform scaling, try to reduce numerical error
169 double vbunit2px = (root->width.computed / root->viewBox.width() + root->height.computed / root->viewBox.height())/2.0;
170 newx = newx * vbunit2px;
171 newy = newy * vbunit2px;
172 } else {
173 newx = newx * root->width.computed / root->viewBox.width();
174 newy = newy * root->height.computed / root->viewBox.height();
175 }
176 }
177 this->point_on_line = Geom::Point(newx, newy);
178 } else if (success == 1) {
179 // before 0.46 style guideline definition.
180 const gchar *attr = this->getRepr()->attribute("orientation");
181 if (attr && !strcmp(attr, "horizontal")) {
182 this->point_on_line = Geom::Point(0, newx);
183 } else {
184 this->point_on_line = Geom::Point(newx, 0);
185 }
186 }
187
188 // <sodipodi:guide> stores inverted y-axis coordinates
189 if (document->is_yaxisdown()) {
190 this->point_on_line[Geom::Y] = document->getHeight().value("px") - this->point_on_line[Geom::Y];
191 }
192 } else {
193 // default to (0,0) for bad arguments
194 this->point_on_line = Geom::Point(0,0);
195 }
196 // update position in non-committing way
197 // fixme: perhaps we need to add an update method instead, and request_update here
198 this->moveto(this->point_on_line, false);
199 }
200 break;
201 default:
202 SPObject::set(key, value);
203 break;
204 }
205 }
206
207 /* Only used internally and in sp-line.cpp */
createSPGuide(SPDocument * doc,Geom::Point const & pt1,Geom::Point const & pt2)208 SPGuide *SPGuide::createSPGuide(SPDocument *doc, Geom::Point const &pt1, Geom::Point const &pt2)
209 {
210 Inkscape::XML::Document *xml_doc = doc->getReprDoc();
211
212 Inkscape::XML::Node *repr = xml_doc->createElement("sodipodi:guide");
213
214 Geom::Point n = Geom::rot90(pt2 - pt1);
215
216 // If root viewBox set, interpret guides in terms of viewBox (90/96)
217 double newx = pt1.x();
218 double newy = pt1.y();
219
220 SPRoot *root = doc->getRoot();
221
222 // <sodipodi:guide> stores inverted y-axis coordinates
223 if (doc->is_yaxisdown()) {
224 newy = doc->getHeight().value("px") - newy;
225 n[Geom::X] *= -1.0;
226 }
227
228 if( root->viewBox_set ) {
229 // check to see if scaling is uniform
230 if(Geom::are_near((root->viewBox.width() * root->height.computed) / (root->width.computed * root->viewBox.height()), 1.0, Geom::EPSILON)) {
231 double px2vbunit = (root->viewBox.width()/root->width.computed + root->viewBox.height()/root->height.computed)/2.0;
232 newx = newx * px2vbunit;
233 newy = newy * px2vbunit;
234 } else {
235 newx = newx * root->viewBox.width() / root->width.computed;
236 newy = newy * root->viewBox.height() / root->height.computed;
237 }
238 }
239
240 sp_repr_set_point(repr, "position", Geom::Point( newx, newy ));
241 sp_repr_set_point(repr, "orientation", n);
242
243 SPNamedView *namedview = sp_document_namedview(doc, nullptr);
244 if (namedview) {
245 if (namedview->lockguides) {
246 repr->setAttribute("inkscape:locked", "true");
247 }
248 namedview->appendChild(repr);
249 }
250 Inkscape::GC::release(repr);
251
252 SPGuide *guide= SP_GUIDE(doc->getObjectByRepr(repr));
253 return guide;
254 }
255
duplicate()256 SPGuide *SPGuide::duplicate(){
257 return SPGuide::createSPGuide(document, point_on_line, Geom::Point(point_on_line[Geom::X] + normal_to_line[Geom::Y],point_on_line[Geom::Y] - normal_to_line[Geom::X]));
258 }
259
sp_guide_pt_pairs_to_guides(SPDocument * doc,std::list<std::pair<Geom::Point,Geom::Point>> & pts)260 void sp_guide_pt_pairs_to_guides(SPDocument *doc, std::list<std::pair<Geom::Point, Geom::Point> > &pts)
261 {
262 for (auto & pt : pts) {
263 SPGuide::createSPGuide(doc, pt.first, pt.second);
264 }
265 }
266
sp_guide_create_guides_around_page(SPDesktop * dt)267 void sp_guide_create_guides_around_page(SPDesktop *dt)
268 {
269 SPDocument *doc=dt->getDocument();
270 std::list<std::pair<Geom::Point, Geom::Point> > pts;
271
272 Geom::Point A(0, 0);
273 Geom::Point C(doc->getWidth().value("px"), doc->getHeight().value("px"));
274 Geom::Point B(C[Geom::X], 0);
275 Geom::Point D(0, C[Geom::Y]);
276
277 pts.emplace_back(A, B);
278 pts.emplace_back(B, C);
279 pts.emplace_back(C, D);
280 pts.emplace_back(D, A);
281
282 sp_guide_pt_pairs_to_guides(doc, pts);
283
284 DocumentUndo::done(doc, SP_VERB_NONE, _("Create Guides Around the Page"));
285 }
286
sp_guide_delete_all_guides(SPDesktop * dt)287 void sp_guide_delete_all_guides(SPDesktop *dt)
288 {
289 SPDocument *doc=dt->getDocument();
290 std::vector<SPObject *> current = doc->getResourceList("guide");
291 while (!current.empty()){
292 SPGuide* guide = SP_GUIDE(*(current.begin()));
293 sp_guide_remove(guide);
294 current = doc->getResourceList("guide");
295 }
296
297 DocumentUndo::done(doc, SP_VERB_NONE, _("Delete All Guides"));
298 }
299
300 // Actually, create a new guide.
showSPGuide(Inkscape::CanvasItemGroup * group)301 void SPGuide::showSPGuide(Inkscape::CanvasItemGroup *group)
302 {
303 Glib::ustring ulabel = (label?label:"");
304 auto item = new Inkscape::CanvasItemGuideLine(group, ulabel, point_on_line, normal_to_line);
305 item->set_stroke(color);
306 item->set_locked(locked);
307
308 item->connect_event(sigc::bind(sigc::ptr_fun(&sp_dt_guide_event), item, this));
309
310 views.push_back(item);
311 }
312
showSPGuide()313 void SPGuide::showSPGuide()
314 {
315 for (auto view : views) {
316 view->show();
317 }
318 }
319
320 // Actually deleted guide from a particular canvas.
hideSPGuide(Inkscape::UI::Widget::Canvas * canvas)321 void SPGuide::hideSPGuide(Inkscape::UI::Widget::Canvas *canvas)
322 {
323 g_assert(canvas != nullptr);
324 for (auto it = views.begin(); it != views.end(); ++it) {
325 if (canvas == (*it)->get_canvas()) { // A guide can be displayed on more than one desktop with the same document.
326 delete (*it);
327 views.erase(it);
328 return;
329 }
330 }
331
332 assert(false);
333 }
334
hideSPGuide()335 void SPGuide::hideSPGuide()
336 {
337 for(auto view : views) {
338 view->hide();
339 }
340 }
341
sensitize(Inkscape::UI::Widget::Canvas * canvas,bool sensitive)342 void SPGuide::sensitize(Inkscape::UI::Widget::Canvas *canvas, bool sensitive)
343 {
344 g_assert(canvas != nullptr);
345
346 for (auto view : views) {
347 if (canvas == view->get_canvas()) {
348 view->set_sensitive(sensitive);
349 return;
350 }
351 }
352
353 assert(false);
354 }
355
getPositionFrom(Geom::Point const & pt) const356 Geom::Point SPGuide::getPositionFrom(Geom::Point const &pt) const
357 {
358 return -(pt - point_on_line);
359 }
360
getDistanceFrom(Geom::Point const & pt) const361 double SPGuide::getDistanceFrom(Geom::Point const &pt) const
362 {
363 return Geom::dot(pt - point_on_line, normal_to_line);
364 }
365
366 /**
367 * \arg commit False indicates temporary moveto in response to motion event while dragging,
368 * true indicates a "committing" version: in response to button release event after
369 * dragging a guideline, or clicking OK in guide editing dialog.
370 */
moveto(Geom::Point const point_on_line,bool const commit)371 void SPGuide::moveto(Geom::Point const point_on_line, bool const commit)
372 {
373 if(this->locked) {
374 return;
375 }
376
377 for(auto view : this->views) {
378 view->set_origin(point_on_line);
379 }
380
381 /* Calling sp_repr_set_point must precede calling sp_item_notify_moveto in the commit
382 case, so that the guide's new position is available for sp_item_rm_unsatisfied_cns. */
383 if (commit) {
384 // If root viewBox set, interpret guides in terms of viewBox (90/96)
385 double newx = point_on_line.x();
386 double newy = point_on_line.y();
387
388 // <sodipodi:guide> stores inverted y-axis coordinates
389 if (document->is_yaxisdown()) {
390 newy = document->getHeight().value("px") - newy;
391 }
392
393 SPRoot *root = document->getRoot();
394 if( root->viewBox_set ) {
395 // check to see if scaling is uniform
396 if(Geom::are_near((root->viewBox.width() * root->height.computed) / (root->width.computed * root->viewBox.height()), 1.0, Geom::EPSILON)) {
397 double px2vbunit = (root->viewBox.width()/root->width.computed + root->viewBox.height()/root->height.computed)/2.0;
398 newx = newx * px2vbunit;
399 newy = newy * px2vbunit;
400 } else {
401 newx = newx * root->viewBox.width() / root->width.computed;
402 newy = newy * root->viewBox.height() / root->height.computed;
403 }
404 }
405
406 //XML Tree being used here directly while it shouldn't be.
407 sp_repr_set_point(getRepr(), "position", Geom::Point(newx, newy) );
408 }
409
410 /* DISABLED CODE BECAUSE SPGuideAttachment IS NOT USE AT THE MOMENT (johan)
411 for (std::vector<SPGuideAttachment>::const_iterator i(attached_items.begin()),
412 iEnd(attached_items.end());
413 i != iEnd; ++i)
414 {
415 SPGuideAttachment const &att = *i;
416 sp_item_notify_moveto(*att.item, this, att.snappoint_ix, position, commit);
417 }
418 */
419 }
420
421 /**
422 * \arg commit False indicates temporary moveto in response to motion event while dragging,
423 * true indicates a "committing" version: in response to button release event after
424 * dragging a guideline, or clicking OK in guide editing dialog.
425 */
set_normal(Geom::Point const normal_to_line,bool const commit)426 void SPGuide::set_normal(Geom::Point const normal_to_line, bool const commit)
427 {
428 if(this->locked) {
429 return;
430 }
431 for(auto view : this->views) {
432 view->set_normal(normal_to_line);
433 }
434
435 /* Calling sp_repr_set_svg_point must precede calling sp_item_notify_moveto in the commit
436 case, so that the guide's new position is available for sp_item_rm_unsatisfied_cns. */
437 if (commit) {
438 //XML Tree being used directly while it shouldn't be
439 auto normal = normal_to_line;
440
441 // <sodipodi:guide> stores inverted y-axis coordinates
442 if (document->is_yaxisdown()) {
443 normal[Geom::X] *= -1.0;
444 }
445
446 sp_repr_set_point(getRepr(), "orientation", normal);
447 }
448
449 /* DISABLED CODE BECAUSE SPGuideAttachment IS NOT USE AT THE MOMENT (johan)
450 for (std::vector<SPGuideAttachment>::const_iterator i(attached_items.begin()),
451 iEnd(attached_items.end());
452 i != iEnd; ++i)
453 {
454 SPGuideAttachment const &att = *i;
455 sp_item_notify_moveto(*att.item, this, att.snappoint_ix, position, commit);
456 }
457 */
458 }
459
set_color(const unsigned r,const unsigned g,const unsigned b,bool const commit)460 void SPGuide::set_color(const unsigned r, const unsigned g, const unsigned b, bool const commit)
461 {
462 this->color = (r << 24) | (g << 16) | (b << 8) | 0x7f;
463
464 if (! views.empty()) {
465 views[0]->set_stroke(color);
466 }
467
468 if (commit) {
469 std::ostringstream os;
470 os << "rgb(" << r << "," << g << "," << b << ")";
471 //XML Tree being used directly while it shouldn't be
472 setAttribute("inkscape:color", os.str());
473 }
474 }
475
set_locked(const bool locked,bool const commit)476 void SPGuide::set_locked(const bool locked, bool const commit)
477 {
478 this->locked = locked;
479 if ( !views.empty() ) {
480 views[0]->set_locked(locked);
481 }
482
483 if (commit) {
484 setAttribute("inkscape:locked", locked ? "true" : "false");
485 }
486 }
487
set_label(const char * label,bool const commit)488 void SPGuide::set_label(const char* label, bool const commit)
489 {
490 if (!views.empty()) {
491 views[0]->set_label(label);
492 }
493
494 if (commit) {
495 //XML Tree being used directly while it shouldn't be
496 setAttribute("inkscape:label", label);
497 }
498 }
499
500 /**
501 * Returns a human-readable description of the guideline for use in dialog boxes and status bar.
502 * If verbose is false, only positioning information is included (useful for dialogs).
503 *
504 * The caller is responsible for freeing the string.
505 */
description(bool const verbose) const506 char* SPGuide::description(bool const verbose) const
507 {
508 using Geom::X;
509 using Geom::Y;
510
511 char *descr = nullptr;
512 if ( !this->document ) {
513 // Guide has probably been deleted and no longer has an attached namedview.
514 descr = g_strdup(_("Deleted"));
515 } else {
516 SPNamedView *namedview = sp_document_namedview(this->document, nullptr);
517
518 Inkscape::Util::Quantity x_q = Inkscape::Util::Quantity(this->point_on_line[X], "px");
519 Inkscape::Util::Quantity y_q = Inkscape::Util::Quantity(this->point_on_line[Y], "px");
520 Glib::ustring position_string_x = x_q.string(namedview->display_units);
521 Glib::ustring position_string_y = y_q.string(namedview->display_units);
522
523 gchar *shortcuts = g_strdup_printf("; %s", _("<b>Shift+drag</b> to rotate, <b>Ctrl+drag</b> to move origin, <b>Del</b> to delete"));
524
525 if ( are_near(this->normal_to_line, Geom::Point(1., 0.)) ||
526 are_near(this->normal_to_line, -Geom::Point(1., 0.)) ) {
527 descr = g_strdup_printf(_("vertical, at %s"), position_string_x.c_str());
528 } else if ( are_near(this->normal_to_line, Geom::Point(0., 1.)) ||
529 are_near(this->normal_to_line, -Geom::Point(0., 1.)) ) {
530 descr = g_strdup_printf(_("horizontal, at %s"), position_string_y.c_str());
531 } else {
532 double const radians = this->angle();
533 double const degrees = Geom::deg_from_rad(radians);
534 int const degrees_int = (int) round(degrees);
535 descr = g_strdup_printf(_("at %d degrees, through (%s,%s)"),
536 degrees_int, position_string_x.c_str(), position_string_y.c_str());
537 }
538
539 if (verbose) {
540 gchar *oldDescr = descr;
541 descr = g_strconcat(oldDescr, shortcuts, NULL);
542 g_free(oldDescr);
543 }
544
545 g_free(shortcuts);
546 }
547
548 return descr;
549 }
550
sp_guide_remove(SPGuide * guide)551 void sp_guide_remove(SPGuide *guide)
552 {
553 g_assert(SP_IS_GUIDE(guide));
554
555 for (std::vector<SPGuideAttachment>::const_iterator i(guide->attached_items.begin()),
556 iEnd(guide->attached_items.end());
557 i != iEnd; ++i)
558 {
559 SPGuideAttachment const &att = *i;
560 remove_last(att.item->constraints, SPGuideConstraint(guide, att.snappoint_ix));
561 }
562 guide->attached_items.clear();
563
564 //XML Tree being used directly while it shouldn't be.
565 sp_repr_unparent(guide->getRepr());
566 }
567
568 /*
569 Local Variables:
570 mode:c++
571 c-file-style:"stroustrup"
572 c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
573 indent-tabs-mode:nil
574 fill-column:99
575 End:
576 */
577 // vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :
578