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