1 /*
2  * textbox-item.c
3  *
4  *
5  * Authors:
6  *  Richard Hult <rhult@hem.passagen.se>
7  *  Ricardo Markiewicz <rmarkie@fi.uba.ar>
8  *  Andres de Barbara <adebarbara@fi.uba.ar>
9  *  Marc Lorber <lorber.marc@wanadoo.fr>
10  *  Bernhard Schuster <bernhard@ahoi.io>
11  *
12  * Web page: https://ahoi.io/project/oregano
13  *
14  * Copyright (C) 1999-2001  Richard Hult
15  * Copyright (C) 2003,2006  Ricardo Markiewicz
16  * Copyright (C) 2009-2012  Marc Lorber
17  * Copyright (C) 2013       Bernhard Schuster
18  *
19  * This program is free software; you can redistribute it and/or
20  * modify it under the terms of the GNU General Public License as
21  * published by the Free Software Foundation; either version 2 of the
22  * License, or (at your option) any later version.
23  *
24  * This program is distributed in the hope that it will be useful,
25  * but WITHOUT ANY WARRANTY; without even the implied warranty of
26  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
27  * General Public License for more details.
28  *
29  * You should have received a copy of the GNU General Public
30  * License along with this program; if not, write to the
31  * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
32  * Boston, MA 02110-1301, USA.
33  */
34 
35 #include <string.h>
36 #include <glib/gi18n.h>
37 
38 #include "cursors.h"
39 #include "coords.h"
40 #include "textbox-item.h"
41 #include "textbox.h"
42 #include "dialogs.h"
43 
44 #define NORMAL_COLOR "black"
45 #define SELECTED_COLOR "green"
46 #define TEXTBOX_FONT "Arial 10"
47 
48 static void textbox_item_class_init (TextboxItemClass *klass);
49 static void textbox_item_init (TextboxItem *item);
50 static void textbox_item_finalize (GObject *object);
51 static void textbox_item_moved (SheetItem *object);
52 static void textbox_rotated_callback (ItemData *data, int angle, SheetItem *sheet_item);
53 static void textbox_flipped_callback (ItemData *data, IDFlip direction, SheetItem *sheet_item);
54 static void textbox_moved_callback (ItemData *data, Coords *pos, SheetItem *item);
55 static void textbox_text_changed_callback (ItemData *data, gchar *new_text, SheetItem *item);
56 static void textbox_item_paste (Sheet *sheet, ItemData *data);
57 static void selection_changed (TextboxItem *item, gboolean select, gpointer user_data);
58 static int select_idle_callback (TextboxItem *item);
59 static int deselect_idle_callback (TextboxItem *item);
60 static gboolean is_in_area (SheetItem *object, Coords *p1, Coords *p2);
61 inline static void get_cached_bounds (TextboxItem *item, Coords *p1, Coords *p2);
62 static void textbox_item_place (SheetItem *item, Sheet *sheet);
63 static void textbox_item_place_ghost (SheetItem *item, Sheet *sheet);
64 static void edit_textbox (SheetItem *sheet_item);
65 static void edit_cmd (GtkWidget *widget, Sheet *sheet);
66 
67 #include "debug.h"
68 
69 typedef struct
70 {
71 	GtkDialog *dialog;
72 	GtkEntry *entry;
73 } TextboxPropDialog;
74 
75 static TextboxPropDialog *prop_dialog = NULL;
76 
77 static const char *textbox_item_context_menu = "<ui>"
78                                                "  <popup name='ItemMenu'>"
79                                                "    <menuitem action='EditText'/>"
80                                                "  </popup>"
81                                                "</ui>";
82 
83 static GtkActionEntry action_entries[] = {{"EditText", GTK_STOCK_PROPERTIES,
84                                            N_ ("_Edit the text..."), NULL, N_ ("Edit the text"),
85                                            G_CALLBACK (edit_cmd)}};
86 
87 enum { TEXTBOX_ITEM_ARG_0, TEXTBOX_ITEM_ARG_NAME };
88 
89 struct _TextboxItemPriv
90 {
91 	guint cache_valid : 1;
92 	guint highlight : 1;
93 	GooCanvasItem *text_canvas_item;
94 	// Cached bounding box. This is used to make
95 	// the rubberband selection a bit faster.
96 	Coords bbox_start;
97 	Coords bbox_end;
98 };
99 
G_DEFINE_TYPE(TextboxItem,textbox_item,TYPE_SHEET_ITEM)100 G_DEFINE_TYPE (TextboxItem, textbox_item, TYPE_SHEET_ITEM)
101 
102 static void textbox_item_class_init (TextboxItemClass *textbox_item_class)
103 {
104 	GObjectClass *object_class;
105 	SheetItemClass *sheet_item_class;
106 
107 	object_class = G_OBJECT_CLASS (textbox_item_class);
108 	sheet_item_class = SHEET_ITEM_CLASS (textbox_item_class);
109 	textbox_item_parent_class = g_type_class_peek_parent (textbox_item_class);
110 
111 	object_class->finalize = textbox_item_finalize;
112 
113 	sheet_item_class->moved = textbox_item_moved;
114 	sheet_item_class->paste = textbox_item_paste;
115 	sheet_item_class->is_in_area = is_in_area;
116 	sheet_item_class->selection_changed = (gpointer)selection_changed;
117 	sheet_item_class->edit_properties = edit_textbox;
118 	sheet_item_class->place = textbox_item_place;
119 	sheet_item_class->place_ghost = textbox_item_place_ghost;
120 }
121 
textbox_item_init(TextboxItem * item)122 static void textbox_item_init (TextboxItem *item)
123 {
124 	TextboxItemPriv *priv;
125 
126 	priv = g_new0 (TextboxItemPriv, 1);
127 	item->priv = priv;
128 
129 	priv->highlight = FALSE;
130 	priv->cache_valid = FALSE;
131 
132 	sheet_item_add_menu (SHEET_ITEM (item), textbox_item_context_menu, action_entries,
133 	                     G_N_ELEMENTS (action_entries));
134 }
135 
textbox_item_finalize(GObject * object)136 static void textbox_item_finalize (GObject *object)
137 {
138 	TextboxItem *item;
139 
140 	item = TEXTBOX_ITEM (object);
141 
142 	g_free (item->priv);
143 	item->priv = NULL;
144 
145 	G_OBJECT_CLASS (textbox_item_parent_class)->finalize (object);
146 }
147 
148 // "moved" signal handler. Invalidates the bounding box cache.
textbox_item_moved(SheetItem * object)149 static void textbox_item_moved (SheetItem *object)
150 {
151 	TextboxItem *item;
152 	TextboxItemPriv *priv;
153 
154 	item = TEXTBOX_ITEM (object);
155 	priv = item->priv;
156 
157 	priv->cache_valid = FALSE;
158 }
159 
textbox_item_new(Sheet * sheet,Textbox * textbox)160 TextboxItem *textbox_item_new (Sheet *sheet, Textbox *textbox)
161 {
162 	GooCanvasItem *item;
163 	TextboxItem *textbox_item;
164 	TextboxItemPriv *priv;
165 	Coords pos;
166 	ItemData *item_data;
167 
168 	g_return_val_if_fail (sheet != NULL, NULL);
169 	g_return_val_if_fail (IS_SHEET (sheet), NULL);
170 
171 	item_data_get_pos (ITEM_DATA (textbox), &pos);
172 
173 	item = g_object_new (TYPE_TEXTBOX_ITEM, NULL);
174 
175 	g_object_set (item, "parent", sheet->object_group, NULL);
176 
177 	textbox_item = TEXTBOX_ITEM (item);
178 	g_object_set (textbox_item, "data", textbox, NULL);
179 
180 	priv = textbox_item->priv;
181 
182 	priv->text_canvas_item = goo_canvas_text_new (
183 	    GOO_CANVAS_ITEM (textbox_item), textbox_get_text (textbox), 0.0, 0.0, -1,
184 	    GOO_CANVAS_ANCHOR_SW, "font", TEXTBOX_FONT, "fill-color", NORMAL_COLOR, NULL);
185 
186 	item_data = ITEM_DATA (textbox);
187 
188 	item_data->rotated_handler_id =
189 	    g_signal_connect_object (G_OBJECT (textbox), "rotated",
190 	                             G_CALLBACK (textbox_rotated_callback), G_OBJECT (textbox_item), 0);
191 	item_data->flipped_handler_id =
192 	    g_signal_connect_object (G_OBJECT (textbox), "flipped",
193 	                             G_CALLBACK (textbox_flipped_callback), G_OBJECT (textbox_item), 0);
194 	item_data->moved_handler_id =
195 	    g_signal_connect_object (G_OBJECT (textbox), "moved", G_CALLBACK (textbox_moved_callback),
196 	                             G_OBJECT (textbox_item), 0);
197 	textbox->text_changed_handler_id = g_signal_connect_object (
198 	    G_OBJECT (textbox), "text_changed", G_CALLBACK (textbox_text_changed_callback),
199 	    G_OBJECT (textbox_item), 0);
200 
201 	textbox_update_bbox (textbox);
202 
203 	return textbox_item;
204 }
205 
textbox_item_signal_connect_placed(TextboxItem * textbox_item,Sheet * sheet)206 void textbox_item_signal_connect_placed (TextboxItem *textbox_item, Sheet *sheet)
207 {
208 	g_signal_connect (G_OBJECT (textbox_item), "button_press_event", G_CALLBACK (sheet_item_event),
209 	                  sheet);
210 
211 	g_signal_connect (G_OBJECT (textbox_item), "button_release_event",
212 	                  G_CALLBACK (sheet_item_event), sheet);
213 
214 	g_signal_connect (G_OBJECT (textbox_item), "key_press_event", G_CALLBACK (sheet_item_event),
215 	                  sheet);
216 }
217 
textbox_rotated_callback(ItemData * data,int angle,SheetItem * sheet_item)218 static void textbox_rotated_callback (ItemData *data, int angle, SheetItem *sheet_item)
219 {
220 	TextboxItem *item;
221 
222 	g_return_if_fail (sheet_item != NULL);
223 	g_return_if_fail (IS_TEXTBOX_ITEM (sheet_item));
224 
225 	item = TEXTBOX_ITEM (sheet_item);
226 
227 	item->priv->cache_valid = FALSE;
228 }
229 
textbox_flipped_callback(ItemData * data,IDFlip direction,SheetItem * sheet_item)230 static void textbox_flipped_callback (ItemData *data, IDFlip direction, SheetItem *sheet_item)
231 {
232 	TextboxItem *item;
233 
234 	g_return_if_fail (sheet_item);
235 	g_return_if_fail (IS_TEXTBOX_ITEM (sheet_item));
236 
237 	item = TEXTBOX_ITEM (sheet_item);
238 
239 	item->priv->cache_valid = FALSE;
240 }
241 
select_idle_callback(TextboxItem * item)242 static int select_idle_callback (TextboxItem *item)
243 {
244 	Coords bbox_start, bbox_end;
245 	TextboxItemPriv *priv = item->priv;
246 
247 	get_cached_bounds (item, &bbox_start, &bbox_end);
248 	g_object_set (priv->text_canvas_item, "fill_color", SELECTED_COLOR, NULL);
249 
250 	priv->highlight = TRUE;
251 
252 	return FALSE;
253 }
254 
deselect_idle_callback(TextboxItem * item)255 static int deselect_idle_callback (TextboxItem *item)
256 {
257 	TextboxItemPriv *priv = item->priv;
258 
259 	g_object_set (priv->text_canvas_item, "fill_color", NORMAL_COLOR, NULL);
260 
261 	priv->highlight = FALSE;
262 
263 	return FALSE;
264 }
265 
selection_changed(TextboxItem * item,gboolean select,gpointer user_data)266 static void selection_changed (TextboxItem *item, gboolean select, gpointer user_data)
267 {
268 	if (select)
269 		g_idle_add ((gpointer)select_idle_callback, item);
270 	else
271 		g_idle_add ((gpointer)deselect_idle_callback, item);
272 }
273 
is_in_area(SheetItem * object,Coords * p1,Coords * p2)274 static gboolean is_in_area (SheetItem *object, Coords *p1, Coords *p2)
275 {
276 	TextboxItem *item;
277 	Coords bbox_start, bbox_end;
278 
279 	item = TEXTBOX_ITEM (object);
280 
281 	get_cached_bounds (item, &bbox_start, &bbox_end);
282 
283 	if (p1->x < bbox_start.x && p2->x > bbox_end.x && p1->y < bbox_start.y && p2->y > bbox_end.y)
284 		return TRUE;
285 
286 	return FALSE;
287 }
288 
289 // Retrieves the bounding box. We use a caching scheme for this
290 // since it's too expensive to calculate it every time we need it.
get_cached_bounds(TextboxItem * item,Coords * p1,Coords * p2)291 inline static void get_cached_bounds (TextboxItem *item, Coords *p1, Coords *p2)
292 {
293 	PangoFontDescription *font;
294 	Coords pos;
295 
296 	TextboxItemPriv *priv;
297 	priv = item->priv;
298 
299 	if (!priv->cache_valid) {
300 		Coords start_pos, end_pos;
301 
302 		font = pango_font_description_from_string (TEXTBOX_FONT);
303 
304 		item_data_get_pos (sheet_item_get_data (SHEET_ITEM (item)), &pos);
305 
306 		start_pos.x = pos.x;
307 		start_pos.y = pos.y - 5; // - font->ascent;
308 		end_pos.x = pos.x + 5;   // + rbearing;
309 		end_pos.y = pos.y + 5;   // + font->descent;
310 
311 		priv->bbox_start = start_pos;
312 		priv->bbox_end = end_pos;
313 		priv->cache_valid = TRUE;
314 		pango_font_description_free (font);
315 	}
316 
317 	memcpy (p1, &priv->bbox_start, sizeof(Coords));
318 	memcpy (p2, &priv->bbox_end, sizeof(Coords));
319 }
320 
textbox_item_paste(Sheet * sheet,ItemData * data)321 static void textbox_item_paste (Sheet *sheet, ItemData *data)
322 {
323 	g_return_if_fail (sheet != NULL);
324 	g_return_if_fail (IS_SHEET (sheet));
325 	g_return_if_fail (data != NULL);
326 	g_return_if_fail (IS_TEXTBOX (data));
327 
328 	sheet_add_ghost_item (sheet, data);
329 }
330 
331 // This is called when the textbox data was moved. Update the view accordingly.
textbox_moved_callback(ItemData * data,Coords * pos,SheetItem * item)332 static void textbox_moved_callback (ItemData *data, Coords *pos, SheetItem *item)
333 {
334 	TextboxItem *textbox_item;
335 
336 	g_return_if_fail (data != NULL);
337 	g_return_if_fail (IS_ITEM_DATA (data));
338 	g_return_if_fail (item != NULL);
339 	g_return_if_fail (IS_TEXTBOX_ITEM (item));
340 
341 	if (pos == NULL)
342 		return;
343 
344 	textbox_item = TEXTBOX_ITEM (item);
345 
346 	// Move the canvas item and invalidate the bbox cache.
347 	goo_canvas_item_translate (GOO_CANVAS_ITEM (item), pos->x, pos->y);
348 	textbox_item->priv->cache_valid = FALSE;
349 }
350 
textbox_text_changed_callback(ItemData * data,gchar * new_text,SheetItem * item)351 static void textbox_text_changed_callback (ItemData *data, gchar *new_text, SheetItem *item)
352 {
353 	TextboxItem *textbox_item;
354 
355 	g_return_if_fail (data != NULL);
356 	g_return_if_fail (IS_ITEM_DATA (data));
357 	g_return_if_fail (item != NULL);
358 	g_return_if_fail (IS_TEXTBOX_ITEM (item));
359 
360 	textbox_item = TEXTBOX_ITEM (item);
361 
362 	g_object_set (textbox_item->priv->text_canvas_item, "text", new_text, NULL);
363 	goo_canvas_item_ensure_updated (GOO_CANVAS_ITEM (textbox_item));
364 }
365 
textbox_item_place(SheetItem * item,Sheet * sheet)366 static void textbox_item_place (SheetItem *item, Sheet *sheet)
367 {
368 	textbox_item_signal_connect_placed (TEXTBOX_ITEM (item), sheet);
369 
370 	g_signal_connect (G_OBJECT (item), "double_clicked", G_CALLBACK (edit_textbox), item);
371 }
372 
textbox_item_place_ghost(SheetItem * item,Sheet * sheet)373 static void textbox_item_place_ghost (SheetItem *item, Sheet *sheet)
374 {
375 	//	textbox_item_signal_connect_placed (TEXTBOX_ITEM (item));
376 }
377 
create_textbox_event(Sheet * sheet,GdkEvent * event)378 static gboolean create_textbox_event (Sheet *sheet, GdkEvent *event)
379 {
380 	switch (event->type) {
381 	case GDK_3BUTTON_PRESS:
382 	case GDK_2BUTTON_PRESS:
383 		return TRUE;
384 
385 	case GDK_BUTTON_PRESS:
386 		if (event->button.button == 4 || event->button.button == 5)
387 			return FALSE;
388 
389 		if (event->button.button == 1) {
390 			if (sheet->state == SHEET_STATE_TEXTBOX_WAIT)
391 				sheet->state = SHEET_STATE_TEXTBOX_START;
392 
393 			return TRUE;
394 		} else
395 			return FALSE;
396 
397 	case GDK_BUTTON_RELEASE:
398 		if (event->button.button == 4 || event->button.button == 5)
399 			return FALSE;
400 
401 		if (sheet->state == SHEET_STATE_TEXTBOX_START) {
402 			Textbox *textbox;
403 			Coords pos;
404 
405 			sheet->state = SHEET_STATE_NONE;
406 
407 			sheet_get_pointer (sheet, &pos.x, &pos.y);
408 			textbox = textbox_new (NULL);
409 
410 			textbox_set_text (textbox, _ ("Label"));
411 
412 			item_data_set_pos (ITEM_DATA (textbox), &pos);
413 			schematic_add_item (schematic_view_get_schematic_from_sheet (sheet),
414 			                    ITEM_DATA (textbox));
415 
416 			schematic_view_reset_tool (schematic_view_get_schematicview_from_sheet (sheet));
417 			g_signal_handlers_disconnect_by_func (G_OBJECT (sheet),
418 			                                      G_CALLBACK (create_textbox_event), sheet);
419 		}
420 
421 		return TRUE;
422 
423 	default:
424 		return FALSE;
425 	}
426 
427 	return TRUE;
428 }
429 
textbox_item_cancel_listen(Sheet * sheet)430 void textbox_item_cancel_listen (Sheet *sheet)
431 {
432 	g_return_if_fail (sheet != NULL);
433 	g_return_if_fail (IS_SHEET (sheet));
434 
435 	sheet->state = SHEET_STATE_NONE;
436 	g_signal_handlers_disconnect_by_func (G_OBJECT (sheet), G_CALLBACK (create_textbox_event),
437 	                                      sheet);
438 }
439 
textbox_item_listen(Sheet * sheet)440 void textbox_item_listen (Sheet *sheet)
441 {
442 	g_return_if_fail (sheet != NULL);
443 	g_return_if_fail (IS_SHEET (sheet));
444 
445 	// Connect to a signal handler that will let the user create a new textbox.
446 	sheet->state = SHEET_STATE_TEXTBOX_WAIT;
447 	g_signal_connect (G_OBJECT (sheet), "event", G_CALLBACK (create_textbox_event), sheet);
448 }
449 
450 // Go through the properties and commit the changes.
edit_dialog_ok(TextboxItem * item)451 void edit_dialog_ok (TextboxItem *item)
452 {
453 	const gchar *value;
454 	Textbox *textbox;
455 
456 	g_return_if_fail (item != NULL);
457 	g_return_if_fail (IS_TEXTBOX_ITEM (item));
458 
459 	textbox = TEXTBOX (sheet_item_get_data (SHEET_ITEM (item)));
460 	value = gtk_entry_get_text (GTK_ENTRY (prop_dialog->entry));
461 	textbox_set_text (textbox, value);
462 }
463 
edit_textbox(SheetItem * sheet_item)464 static void edit_textbox (SheetItem *sheet_item)
465 {
466 	TextboxItem *item;
467 	Textbox *textbox;
468 	const char *value;
469 	GtkBuilder *builder;
470 	GError *e = NULL;
471 
472 	g_return_if_fail (sheet_item != NULL);
473 	g_return_if_fail (IS_TEXTBOX_ITEM (sheet_item));
474 
475 	if ((builder = gtk_builder_new ()) == NULL) {
476 		oregano_error (_ ("Could not create textbox properties dialog"));
477 		return;
478 	}
479 	gtk_builder_set_translation_domain (builder, NULL);
480 
481 	item = TEXTBOX_ITEM (sheet_item);
482 	textbox = TEXTBOX (sheet_item_get_data (sheet_item));
483 
484 	if (!gtk_builder_add_from_file (builder, OREGANO_UIDIR "/textbox-properties-dialog.ui", &e)) {
485 		oregano_error_with_title (_ ("Could not create textbox properties dialog."), e->message);
486 		g_clear_error (&e);
487 		return;
488 	}
489 
490 	prop_dialog = g_new0 (TextboxPropDialog, 1);
491 	prop_dialog->dialog =
492 	    GTK_DIALOG (gtk_builder_get_object (builder, "textbox-properties-dialog"));
493 
494 	prop_dialog->entry = GTK_ENTRY (gtk_builder_get_object (builder, "entry"));
495 
496 	value = textbox_get_text (textbox);
497 	gtk_entry_set_text (GTK_ENTRY (prop_dialog->entry), value);
498 
499 	gtk_dialog_run (GTK_DIALOG (prop_dialog->dialog));
500 	edit_dialog_ok (item);
501 
502 	gtk_widget_destroy (GTK_WIDGET (prop_dialog->dialog));
503 	g_free (prop_dialog);
504 	g_object_unref (builder);
505 }
506 
edit_cmd(GtkWidget * widget,Sheet * sheet)507 static void edit_cmd (GtkWidget *widget, Sheet *sheet)
508 {
509 	GList *list;
510 
511 	list = sheet_get_selection (sheet);
512 	if ((list != NULL) && IS_TEXTBOX_ITEM (list->data))
513 		edit_textbox (list->data);
514 	g_list_free_full (list, g_object_unref);
515 }
516