1 /*
2 * Rectangle drawing context
3 *
4 * Author:
5 * Lauris Kaplinski <lauris@kaplinski.com>
6 * bulia byak <buliabyak@users.sf.net>
7 * Jon A. Cruz <jon@joncruz.org>
8 * Abhishek Sharma
9 *
10 * Copyright (C) 2006 Johan Engelen <johan@shouraizou.nl>
11 * Copyright (C) 2000-2005 authors
12 * Copyright (C) 2000-2001 Ximian, Inc.
13 *
14 * Released under GNU GPL, read the file 'COPYING' for more information
15 */
17 #include "config.h"
19 #include <gdk/gdkkeysyms.h>
20 #include <cstring>
21 #include <string>
23 #include "macros.h"
24 #include "display/sp-canvas.h"
25 #include "sp-rect.h"
26 #include "document.h"
27 #include "sp-namedview.h"
28 #include "selection.h"
29 #include "selection-chemistry.h"
30 #include "desktop-handles.h"
31 #include "snap.h"
32 #include "desktop.h"
33 #include "desktop-style.h"
34 #include "message-context.h"
35 #include "pixmaps/cursor-rect.xpm"
36 #include "rect-context.h"
37 #include "sp-metrics.h"
38 #include <glibmm/i18n.h>
39 #include "object-edit.h"
40 #include "xml/repr.h"
41 #include "xml/node-event-vector.h"
42 #include "preferences.h"
43 #include "context-fns.h"
44 #include "shape-editor.h"
46 using Inkscape::DocumentUndo;
48 //static const double goldenratio = 1.61803398874989484820; // golden ratio
50 static void sp_rect_context_class_init(SPRectContextClass *klass);
51 static void sp_rect_context_init(SPRectContext *rect_context);
52 static void sp_rect_context_dispose(GObject *object);
54 static void sp_rect_context_setup(SPEventContext *ec);
55 static void sp_rect_context_finish(SPEventContext *ec);
56 static void sp_rect_context_set(SPEventContext *ec, Inkscape::Preferences::Entry *val);
58 static gint sp_rect_context_root_handler(SPEventContext *event_context, GdkEvent *event);
59 static gint sp_rect_context_item_handler(SPEventContext *event_context, SPItem *item, GdkEvent *event);
61 static void sp_rect_drag(SPRectContext &rc, Geom::Point const pt, guint state);
62 static void sp_rect_finish(SPRectContext *rc);
63 static void sp_rect_cancel(SPRectContext *rc);
65 static SPEventContextClass *parent_class;
68 GtkType sp_rect_context_get_type()
69 {
70 static GType type = 0;
71 if (!type) {
72 GTypeInfo info = {
73 sizeof(SPRectContextClass),
74 NULL, NULL,
75 (GClassInitFunc) sp_rect_context_class_init,
76 NULL, NULL,
77 sizeof(SPRectContext),
78 4,
79 (GInstanceInitFunc) sp_rect_context_init,
80 NULL, /* value_table */
81 };
82 type = g_type_register_static(SP_TYPE_EVENT_CONTEXT, "SPRectContext", &info, (GTypeFlags) 0);
83 }
84 return type;
85 }
87 static void sp_rect_context_class_init(SPRectContextClass *klass)
88 {
89 GObjectClass *object_class = (GObjectClass *) klass;
90 SPEventContextClass *event_context_class = (SPEventContextClass *) klass;
92 parent_class = (SPEventContextClass *) g_type_class_peek_parent(klass);
94 object_class->dispose = sp_rect_context_dispose;
96 event_context_class->setup = sp_rect_context_setup;
97 event_context_class->finish = sp_rect_context_finish;
98 event_context_class->set = sp_rect_context_set;
99 event_context_class->root_handler = sp_rect_context_root_handler;
100 event_context_class->item_handler = sp_rect_context_item_handler;
101 }
103 static void sp_rect_context_init(SPRectContext *rect_context)
104 {
105 SPEventContext *event_context = SP_EVENT_CONTEXT(rect_context);
107 event_context->cursor_shape = cursor_rect_xpm;
108 event_context->hot_x = 4;
109 event_context->hot_y = 4;
110 event_context->xp = 0;
111 event_context->yp = 0;
112 event_context->tolerance = 0;
113 event_context->within_tolerance = false;
114 event_context->item_to_select = NULL;
115 event_context->tool_url = "/tools/shapes/rect";
117 rect_context->item = NULL;
119 rect_context->rx = 0.0;
120 rect_context->ry = 0.0;
122 new (&rect_context->sel_changed_connection) sigc::connection();
123 }
125 static void sp_rect_context_finish(SPEventContext *ec)
126 {
127 SPRectContext *rc = SP_RECT_CONTEXT(ec);
128 SPDesktop *desktop = ec->desktop;
130 sp_canvas_item_ungrab(SP_CANVAS_ITEM(desktop->acetate), GDK_CURRENT_TIME);
131 sp_rect_finish(rc);
132 rc->sel_changed_connection.disconnect();
134 if (((SPEventContextClass *) parent_class)->finish) {
135 ((SPEventContextClass *) parent_class)->finish(ec);
136 }
137 }
140 static void sp_rect_context_dispose(GObject *object)
141 {
142 SPRectContext *rc = SP_RECT_CONTEXT(object);
143 SPEventContext *ec = SP_EVENT_CONTEXT(object);
145 ec->enableGrDrag(false);
147 rc->sel_changed_connection.disconnect();
148 rc->sel_changed_connection.~connection();
150 delete ec->shape_editor;
151 ec->shape_editor = NULL;
153 /* fixme: This is necessary because we do not grab */
154 if (rc->item) {
155 sp_rect_finish(rc);
156 }
158 if (rc->_message_context) {
159 delete rc->_message_context;
160 }
162 G_OBJECT_CLASS(parent_class)->dispose(object);
163 }
165 /**
166 \brief Callback that processes the "changed" signal on the selection;
167 destroys old and creates new knotholder
168 */
169 void sp_rect_context_selection_changed(Inkscape::Selection *selection, gpointer data)
170 {
171 SPRectContext *rc = SP_RECT_CONTEXT(data);
172 SPEventContext *ec = SP_EVENT_CONTEXT(rc);
174 ec->shape_editor->unset_item(SH_KNOTHOLDER);
175 SPItem *item = selection->singleItem();
176 ec->shape_editor->set_item(item, SH_KNOTHOLDER);
177 }
179 static void sp_rect_context_setup(SPEventContext *ec)
180 {
181 SPRectContext *rc = SP_RECT_CONTEXT(ec);
183 if (((SPEventContextClass *) parent_class)->setup) {
184 ((SPEventContextClass *) parent_class)->setup(ec);
185 }
187 ec->shape_editor = new ShapeEditor(ec->desktop);
189 SPItem *item = sp_desktop_selection(ec->desktop)->singleItem();
190 if (item) {
191 ec->shape_editor->set_item(item, SH_KNOTHOLDER);
192 }
194 rc->sel_changed_connection.disconnect();
195 rc->sel_changed_connection = sp_desktop_selection(ec->desktop)->connectChanged(
196 sigc::bind(sigc::ptr_fun(&sp_rect_context_selection_changed), (gpointer)rc)
197 );
199 sp_event_context_read(ec, "rx");
200 sp_event_context_read(ec, "ry");
202 Inkscape::Preferences *prefs = Inkscape::Preferences::get();
203 if (prefs->getBool("/tools/shapes/selcue")) {
204 ec->enableSelectionCue();
205 }
207 if (prefs->getBool("/tools/shapes/gradientdrag")) {
208 ec->enableGrDrag();
209 }
211 rc->_message_context = new Inkscape::MessageContext((ec->desktop)->messageStack());
212 }
214 static void sp_rect_context_set(SPEventContext *ec, Inkscape::Preferences::Entry *val)
215 {
216 SPRectContext *rc = SP_RECT_CONTEXT(ec);
218 /* fixme: Proper error handling for non-numeric data. Use a locale-independent function like
219 * g_ascii_strtod (or a thin wrapper that does the right thing for invalid values inf/nan). */
220 Glib::ustring name = val->getEntryName();
221 if ( name == "rx" ) {
222 rc->rx = val->getDoubleLimited(); // prevents NaN and +/-Inf from messing up
223 } else if ( name == "ry" ) {
224 rc->ry = val->getDoubleLimited();
225 }
226 }
228 static gint sp_rect_context_item_handler(SPEventContext *event_context, SPItem *item, GdkEvent *event)
229 {
230 SPDesktop *desktop = event_context->desktop;
232 gint ret = FALSE;
234 switch (event->type) {
235 case GDK_BUTTON_PRESS:
236 if ( event->button.button == 1 && !event_context->space_panning) {
237 Inkscape::setup_for_drag_start(desktop, event_context, event);
238 ret = TRUE;
239 }
240 break;
241 // motion and release are always on root (why?)
242 default:
243 break;
244 }
246 if (((SPEventContextClass *) parent_class)->item_handler) {
247 ret = ((SPEventContextClass *) parent_class)->item_handler(event_context, item, event);
248 }
250 return ret;
251 }
253 static gint sp_rect_context_root_handler(SPEventContext *event_context, GdkEvent *event)
254 {
255 static bool dragging;
257 SPDesktop *desktop = event_context->desktop;
258 Inkscape::Selection *selection = sp_desktop_selection (desktop);
260 SPRectContext *rc = SP_RECT_CONTEXT(event_context);
261 Inkscape::Preferences *prefs = Inkscape::Preferences::get();
263 event_context->tolerance = prefs->getIntLimited("/options/dragtolerance/value", 0, 0, 100);
265 gint ret = FALSE;
266 switch (event->type) {
267 case GDK_BUTTON_PRESS:
268 if (event->button.button == 1 && !event_context->space_panning) {
269 Geom::Point const button_w(event->button.x,
270 event->button.y);
272 // save drag origin
273 event_context->xp = (gint) button_w[Geom::X];
274 event_context->yp = (gint) button_w[Geom::Y];
275 event_context->within_tolerance = true;
277 // remember clicked item, disregarding groups, honoring Alt
278 event_context->item_to_select = sp_event_context_find_item (desktop, button_w, event->button.state & GDK_MOD1_MASK, TRUE);
280 dragging = true;
282 /* Position center */
283 Geom::Point button_dt(desktop->w2d(button_w));
284 rc->center = from_2geom(button_dt);
286 /* Snap center */
287 SnapManager &m = desktop->namedview->snap_manager;
288 m.setup(desktop);
289 m.freeSnapReturnByRef(button_dt, Inkscape::SNAPSOURCE_NODE_HANDLE);
290 m.unSetup();
291 rc->center = from_2geom(button_dt);
293 sp_canvas_item_grab(SP_CANVAS_ITEM(desktop->acetate),
294 ( GDK_KEY_PRESS_MASK |
295 GDK_BUTTON_RELEASE_MASK |
296 GDK_POINTER_MOTION_MASK |
297 GDK_POINTER_MOTION_HINT_MASK |
298 GDK_BUTTON_PRESS_MASK ),
299 NULL, event->button.time);
301 ret = TRUE;
302 }
303 break;
304 case GDK_MOTION_NOTIFY:
305 if ( dragging
306 && (event->motion.state & GDK_BUTTON1_MASK) && !event_context->space_panning)
307 {
308 if ( event_context->within_tolerance
309 && ( abs( (gint) event->motion.x - event_context->xp ) < event_context->tolerance )
310 && ( abs( (gint) event->motion.y - event_context->yp ) < event_context->tolerance ) ) {
311 break; // do not drag if we're within tolerance from origin
312 }
313 // Once the user has moved farther than tolerance from the original location
314 // (indicating they intend to draw, not click), then always process the
315 // motion notify coordinates as given (no snapping back to origin)
316 event_context->within_tolerance = false;
318 Geom::Point const motion_w(event->motion.x, event->motion.y);
319 Geom::Point motion_dt(desktop->w2d(motion_w));
321 sp_rect_drag(*rc, motion_dt, event->motion.state); // this will also handle the snapping
322 gobble_motion_events(GDK_BUTTON1_MASK);
323 ret = TRUE;
324 } else if (!sp_event_context_knot_mouseover(rc)) {
325 SnapManager &m = desktop->namedview->snap_manager;
326 m.setup(desktop);
328 Geom::Point const motion_w(event->motion.x, event->motion.y);
329 Geom::Point motion_dt(desktop->w2d(motion_w));
331 m.preSnap(Inkscape::SnapCandidatePoint(motion_dt, Inkscape::SNAPSOURCE_NODE_HANDLE));
332 m.unSetup();
333 }
334 break;
335 case GDK_BUTTON_RELEASE:
336 event_context->xp = event_context->yp = 0;
337 if (event->button.button == 1 && !event_context->space_panning) {
338 dragging = false;
339 sp_event_context_discard_delayed_snap_event(event_context);
341 if (!event_context->within_tolerance) {
342 // we've been dragging, finish the rect
343 sp_rect_finish(rc);
344 } else if (event_context->item_to_select) {
345 // no dragging, select clicked item if any
346 if (event->button.state & GDK_SHIFT_MASK) {
347 selection->toggle(event_context->item_to_select);
348 } else {
349 selection->set(event_context->item_to_select);
350 }
351 } else {
352 // click in an empty space
353 selection->clear();
354 }
356 event_context->item_to_select = NULL;
357 ret = TRUE;
358 sp_canvas_item_ungrab(SP_CANVAS_ITEM(desktop->acetate),
359 event->button.time);
360 }
361 break;
362 case GDK_KEY_PRESS:
363 switch (get_group0_keyval (&event->key)) {
364 case GDK_Alt_L:
365 case GDK_Alt_R:
366 case GDK_Control_L:
367 case GDK_Control_R:
368 case GDK_Shift_L:
369 case GDK_Shift_R:
370 case GDK_Meta_L: // Meta is when you press Shift+Alt (at least on my machine)
371 case GDK_Meta_R:
372 if (!dragging){
373 sp_event_show_modifier_tip (event_context->defaultMessageContext(), event,
374 _("<b>Ctrl</b>: make square or integer-ratio rect, lock a rounded corner circular"),
375 _("<b>Shift</b>: draw around the starting point"),
376 NULL);
377 }
378 break;
379 case GDK_Up:
380 case GDK_Down:
381 case GDK_KP_Up:
382 case GDK_KP_Down:
383 // prevent the zoom field from activation
384 if (!MOD__CTRL_ONLY)
385 ret = TRUE;
386 break;
388 case GDK_x:
389 case GDK_X:
390 if (MOD__ALT_ONLY) {
391 desktop->setToolboxFocusTo ("altx-rect");
392 ret = TRUE;
393 }
394 break;
396 case GDK_g:
397 case GDK_G:
398 if (MOD__SHIFT_ONLY) {
399 sp_selection_to_guides(desktop);
400 ret = true;
401 }
402 break;
404 case GDK_Escape:
405 if (dragging) {
406 dragging = false;
407 sp_event_context_discard_delayed_snap_event(event_context);
408 // if drawing, cancel, otherwise pass it up for deselecting
409 sp_rect_cancel(rc);
410 ret = TRUE;
411 }
412 break;
414 case GDK_space:
415 if (dragging) {
416 sp_canvas_item_ungrab(SP_CANVAS_ITEM(desktop->acetate),
417 event->button.time);
418 dragging = false;
419 sp_event_context_discard_delayed_snap_event(event_context);
420 if (!event_context->within_tolerance) {
421 // we've been dragging, finish the rect
422 sp_rect_finish(rc);
423 }
424 // do not return true, so that space would work switching to selector
425 }
426 break;
428 default:
429 break;
430 }
431 break;
432 case GDK_KEY_RELEASE:
433 switch (get_group0_keyval (&event->key)) {
434 case GDK_Alt_L:
435 case GDK_Alt_R:
436 case GDK_Control_L:
437 case GDK_Control_R:
438 case GDK_Shift_L:
439 case GDK_Shift_R:
440 case GDK_Meta_L: // Meta is when you press Shift+Alt
441 case GDK_Meta_R:
442 event_context->defaultMessageContext()->clear();
443 break;
444 default:
445 break;
446 }
447 break;
448 default:
449 break;
450 }
452 if (!ret) {
453 if (((SPEventContextClass *) parent_class)->root_handler) {
454 ret = ((SPEventContextClass *) parent_class)->root_handler(event_context, event);
455 }
456 }
458 return ret;
459 }
461 static void sp_rect_drag(SPRectContext &rc, Geom::Point const pt, guint state)
462 {
463 SPDesktop *desktop = SP_EVENT_CONTEXT(&rc)->desktop;
465 if (!rc.item) {
467 if (Inkscape::have_viable_layer(desktop, rc._message_context) == false) {
468 return;
469 }
471 // Create object
472 Inkscape::XML::Document *xml_doc = SP_EVENT_CONTEXT_DOCUMENT(&rc)->getReprDoc();
473 Inkscape::XML::Node *repr = xml_doc->createElement("svg:rect");
475 // Set style
476 sp_desktop_apply_style_tool (desktop, repr, "/tools/shapes/rect", false);
478 rc.item = (SPItem *) desktop->currentLayer()->appendChildRepr(repr);
479 Inkscape::GC::release(repr);
480 rc.item->transform = SP_ITEM(desktop->currentLayer())->i2doc_affine().inverse();
481 rc.item->updateRepr();
483 sp_canvas_force_full_redraw_after_interruptions(desktop->canvas, 5);
484 }
486 Geom::Rect const r = Inkscape::snap_rectangular_box(desktop, rc.item, pt, rc.center, state);
488 sp_rect_position_set(SP_RECT(rc.item), r.min()[Geom::X], r.min()[Geom::Y], r.dimensions()[Geom::X], r.dimensions()[Geom::Y]);
489 if ( rc.rx != 0.0 ) {
490 sp_rect_set_rx (SP_RECT(rc.item), TRUE, rc.rx);
491 }
492 if ( rc.ry != 0.0 ) {
493 if (rc.rx == 0.0)
494 sp_rect_set_ry (SP_RECT(rc.item), TRUE, CLAMP(rc.ry, 0, MIN(r.dimensions()[Geom::X], r.dimensions()[Geom::Y])/2));
495 else
496 sp_rect_set_ry (SP_RECT(rc.item), TRUE, CLAMP(rc.ry, 0, r.dimensions()[Geom::Y]));
497 }
499 // status text
500 double rdimx = r.dimensions()[Geom::X];
501 double rdimy = r.dimensions()[Geom::Y];
502 GString *xs = SP_PX_TO_METRIC_STRING(rdimx, desktop->namedview->getDefaultMetric());
503 GString *ys = SP_PX_TO_METRIC_STRING(rdimy, desktop->namedview->getDefaultMetric());
504 if (state & GDK_CONTROL_MASK) {
505 int ratio_x, ratio_y;
506 bool is_golden_ratio = false;
507 if (fabs (rdimx) > fabs (rdimy)) {
508 if (fabs(rdimx / rdimy - goldenratio) < 1e-6) {
509 is_golden_ratio = true;
510 }
511 ratio_x = (int) rint (rdimx / rdimy);
512 ratio_y = 1;
513 } else {
514 if (fabs(rdimy / rdimx - goldenratio) < 1e-6) {
515 is_golden_ratio = true;
516 }
517 ratio_x = 1;
518 ratio_y = (int) rint (rdimy / rdimx);
519 }
520 if (!is_golden_ratio) {
521 rc._message_context->setF(Inkscape::IMMEDIATE_MESSAGE, _("<b>Rectangle</b>: %s × %s (constrained to ratio %d:%d); with <b>Shift</b> to draw around the starting point"), xs->str, ys->str, ratio_x, ratio_y);
522 } else {
523 if (ratio_y == 1) {
524 rc._message_context->setF(Inkscape::IMMEDIATE_MESSAGE, _("<b>Rectangle</b>: %s × %s (constrained to golden ratio 1.618 : 1); with <b>Shift</b> to draw around the starting point"), xs->str, ys->str);
525 } else {
526 rc._message_context->setF(Inkscape::IMMEDIATE_MESSAGE, _("<b>Rectangle</b>: %s × %s (constrained to golden ratio 1 : 1.618); with <b>Shift</b> to draw around the starting point"), xs->str, ys->str);
527 }
528 }
529 } else {
530 rc._message_context->setF(Inkscape::IMMEDIATE_MESSAGE, _("<b>Rectangle</b>: %s × %s; with <b>Ctrl</b> to make square or integer-ratio rectangle; with <b>Shift</b> to draw around the starting point"), xs->str, ys->str);
531 }
532 g_string_free(xs, FALSE);
533 g_string_free(ys, FALSE);
534 }
536 static void sp_rect_finish(SPRectContext *rc)
537 {
538 rc->_message_context->clear();
540 if ( rc->item != NULL ) {
541 SPRect *rect = SP_RECT(rc->item);
542 if (rect->width.computed == 0 || rect->height.computed == 0) {
543 sp_rect_cancel(rc); // Don't allow the creating of zero sized rectangle, for example when the start and and point snap to the snap grid point
544 return;
545 }
547 SPDesktop *desktop = SP_EVENT_CONTEXT_DESKTOP(rc);
549 SP_OBJECT(rc->item)->updateRepr();
551 sp_canvas_end_forced_full_redraws(desktop->canvas);
553 sp_desktop_selection(desktop)->set(rc->item);
554 DocumentUndo::done(sp_desktop_document(desktop), SP_VERB_CONTEXT_RECT,
555 _("Create rectangle"));
557 rc->item = NULL;
558 }
559 }
561 static void sp_rect_cancel(SPRectContext *rc)
562 {
563 SPDesktop *desktop = SP_EVENT_CONTEXT(rc)->desktop;
565 sp_desktop_selection(desktop)->clear();
566 sp_canvas_item_ungrab(SP_CANVAS_ITEM(desktop->acetate), 0);
568 if (rc->item != NULL) {
569 SP_OBJECT(rc->item)->deleteObject();
570 rc->item = NULL;
571 }
573 rc->within_tolerance = false;
574 rc->xp = 0;
575 rc->yp = 0;
576 rc->item_to_select = NULL;
578 sp_canvas_end_forced_full_redraws(desktop->canvas);
580 DocumentUndo::cancel(sp_desktop_document(desktop));
581 }
584 /*
585 Local Variables:
586 mode:c++
587 c-file-style:"stroustrup"
588 c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
589 indent-tabs-mode:nil
590 fill-column:99
591 End:
592 */
593 // vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :