1 /* Dia -- an diagram creation/manipulation program
2 * Copyright (C) 1998 Alexander Larsson
3 *
4 * find-and-replace.c - common functionality applied to diagram
5 *
6 * Copyright (C) 2008 Hans Breuer
7 * Copyright (C) 2008 Johann Tienhaara (patched)
8 *
9 * This program is free software; you can redistribute it and/or modify
10 * it under the terms of the GNU General Public License as published by
11 * the Free Software Foundation; either version 2 of the License, or
12 * (at your option) any later version.
13 *
14 * This program is distributed in the hope that it will be useful,
15 * but WITHOUT ANY WARRANTY; without even the implied warranty of
16 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 * GNU General Public License for more details.
18 *
19 * You should have received a copy of the GNU General Public License
20 * along with this program; if not, write to the Free Software
21 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
22 */
23
24 #include <config.h>
25
26 #include <gtk/gtk.h>
27
28 #include "intl.h"
29
30 #include "diagram.h"
31 #include "display.h"
32 #include "object.h"
33 #include "object_ops.h"
34 #include "connectionpoint_ops.h"
35 #include "undo.h"
36
37 #include "find-and-replace.h"
38 /* messing with property internals */
39 #include "propinternals.h"
40
41 enum {
42 RESPONSE_FIND = -20,
43 RESPONSE_REPLACE = -21,
44 RESPONSE_REPLACE_ALL = -23
45 };
46
47 enum {
48 MATCH_CASE = (1<<0),
49 MATCH_WORD = (1<<1),
50 /* Don't just match the name/text - match UML attributes etc too? */
51 MATCH_ALL_PROPERTIES = (1<<2)
52 };
53
54 typedef struct _SearchData {
55 const gchar *key;
56 guint flags;
57 Diagram *diagram;
58 DiaObject *first; /* the first one found */
59 DiaObject *found; /* the one we were looking for */
60 DiaObject *last; /* previously found */
61 gboolean seen_last;
62 } SearchData;
63
64
65 /*! Match and possibly modify the given object's given property.
66 * Returns FALSE if not matched or if the input property is NULL. */
67 static gboolean
_match_text_prop(DiaObject * obj,const SearchData * sd,const gchar * replacement,gchar ** value_to_match)68 _match_text_prop (DiaObject *obj, const SearchData *sd, const gchar *replacement, gchar **value_to_match)
69 {
70 gboolean is_match = FALSE;
71 gchar *repl = NULL;
72
73 if (!value_to_match || *value_to_match == NULL)
74 return FALSE;
75
76 /* search part */
77 if (sd->flags & MATCH_CASE) {
78 const gchar *p = strstr (*value_to_match, sd->key);
79 is_match = p != NULL;
80 if (p && replacement) {
81 gchar *a = g_strndup (*value_to_match, p - *value_to_match);
82 gchar *b = g_strdup (p + strlen(sd->key));
83 repl = g_strdup_printf ("%s%s%s", a, replacement, b);
84 g_free (a);
85 g_free (b);
86 }
87 } else {
88 gchar *s1 = g_utf8_casefold (*value_to_match, -1);
89 gchar *s2 = g_utf8_casefold (sd->key, -1);
90 const gchar *p = strstr (s1, s2);
91 is_match = p != NULL;
92 if (p && replacement) {
93 gchar *a = g_strndup (*value_to_match, p - s1);
94 gchar *b = g_strdup (*value_to_match + strlen(a) + strlen(sd->key));
95 repl = g_strdup_printf ("%s%s%s", a, replacement, b);
96 g_free (a);
97 g_free (b);
98 }
99 g_free (s1);
100 g_free (s2);
101 }
102
103 if (sd->flags & MATCH_WORD)
104 is_match = (is_match && strlen(*value_to_match) == strlen(sd->key));
105
106 /* replace part */
107 if (is_match && replacement) {
108 g_free (*value_to_match);
109 *value_to_match = repl;
110 } else {
111 g_free (repl);
112 }
113
114 return is_match;
115 }
116
117
118 /*! Match and possibly modify the given object's name/text property */
119 static GPtrArray *
_match_name_prop(DiaObject * obj,const SearchData * sd,const gchar * replacement)120 _match_name_prop (DiaObject *obj, const SearchData *sd, const gchar *replacement)
121 {
122 Property *prop;
123 gchar **name;
124 gboolean is_match = FALSE;
125 GPtrArray *plist = NULL;
126
127 if ((prop = object_prop_by_name(obj, "name")) != NULL)
128 name = &((StringProperty *)prop)->string_data;
129 else if ((prop = object_prop_by_name(obj, "text")) != NULL)
130 name = &((TextProperty *)prop)->text_data;
131 else
132 return NULL;
133
134 is_match = _match_text_prop (obj, sd, replacement, name);
135
136 if (!is_match) {
137 prop->ops->free (prop);
138 return NULL;
139 }
140
141 plist = prop_list_from_single (prop);
142
143 return plist;
144 }
145
146 /*! Match and possibly modify one property in an object. */
147 static gboolean
_match_prop(DiaObject * obj,const SearchData * sd,const gchar * replacement,Property * prop)148 _match_prop (DiaObject *obj, const SearchData *sd, const gchar *replacement, Property *prop)
149 {
150 PropertyType prop_type;
151 gboolean is_match = FALSE;
152 gchar **text_data;
153
154 if (!prop)
155 return FALSE;
156
157 /* TODO: We could probably speed this up by using the type_quark,
158 * but I don't know enough yet to use it safely... */
159 prop_type = prop->type;
160 if (!prop_type)
161 return FALSE;
162
163 /* Special case: array of sub-properties. Do not continue with
164 * checking text for this property. Instead, just
165 * recurse into _match_prop() for each sub-property in
166 * the array. */
167 if ( strcmp (prop_type, PROP_TYPE_SARRAY) == 0
168 || strcmp (prop_type, PROP_TYPE_DARRAY) == 0) {
169 GPtrArray *records = ((ArrayProperty *) prop)->records;
170 guint rnum;
171
172 if (!records)
173 return FALSE;
174
175 for (rnum = 0; rnum < records->len && !is_match; ++rnum) {
176 GPtrArray *sub_props = g_ptr_array_index (records, rnum);
177 guint sub_num;
178
179 for (sub_num = 0; sub_num < sub_props->len && !is_match; ++sub_num) {
180 Property *sub_prop = g_ptr_array_index (sub_props, sub_num);
181
182 is_match = _match_prop (obj, sd, replacement, sub_prop);
183 }
184 }
185 /* Done. */
186 return is_match;
187 }
188
189
190 /* Check for string / text property. */
191 if ( strcmp (prop_type, PROP_TYPE_MULTISTRING) == 0
192 || strcmp (prop_type, PROP_TYPE_STRING) == 0)
193 {
194 text_data = &((StringProperty *) prop)->string_data;
195 } else if (strcmp (prop_type, PROP_TYPE_TEXT) == 0) {
196 text_data = &((TextProperty *) prop)->text_data;
197 }
198 /* TODO future:
199 else if ( strcmp (prop_type, PROP_TYPE_STRINGLIST) == 0)
200 {
201 }
202 */
203 else {
204 /* Not a type we're interested in (int, real, geometry, etc). */
205 return FALSE;
206 }
207
208 return _match_text_prop (obj, sd, replacement, text_data);
209 }
210
211 /*! Match and possibly modify all the given object's properties. */
212 static GPtrArray *
_match_all_props(DiaObject * obj,const SearchData * sd,const gchar * replacement)213 _match_all_props (DiaObject *obj, const SearchData *sd, const gchar *replacement)
214 {
215 GPtrArray *all_plist = NULL;
216 GPtrArray *matched_plist = NULL;
217 const PropDescription *desc;
218 guint pnum;
219
220 if (!obj)
221 return NULL;
222
223 desc = object_get_prop_descriptions (obj);
224 if (!desc)
225 return NULL;
226
227 all_plist = prop_list_from_descs (desc, pdtpp_true);
228 if (!all_plist)
229 return NULL;
230
231 /* Step though all object properties.
232 * Along the way, construct a list of matching properties (or
233 * replaced properties). */
234 for (pnum = 0; pnum < all_plist->len; ++pnum) {
235 Property *prop = g_ptr_array_index (all_plist, pnum);
236 gboolean is_match = FALSE;
237 const gchar *prop_name;
238
239 if (!prop || !prop->name)
240 continue;
241
242 /* This extra step seems to be necessary to populate the property data. */
243 prop_name = prop->name;
244 prop->ops->free (prop);
245 prop = object_prop_by_name (obj, prop_name);
246
247 is_match = _match_prop (obj, sd, replacement, prop);
248
249 if (!is_match) {
250 prop->ops->free (prop);
251 continue;
252 }
253
254 /* We have a match. */
255 if (!matched_plist) {
256 /* First time. */
257 matched_plist = prop_list_from_single (prop);
258 } else {
259 /* FIXME: do we realy want a replace all here? */
260 /* Subsequent finds. */
261 GPtrArray *append_plist;
262 append_plist = prop_list_from_single (prop);
263 prop_list_add_list (matched_plist, append_plist);
264 prop_list_free (append_plist);
265 }
266
267 } /* Continue stepping through all object properties. */
268
269 return matched_plist;
270 }
271
272
273 /*! Match and possibly modify one or more properties in an object.
274 * Returns a list of modified Properties. */
275 static GPtrArray *
_match_props(DiaObject * obj,const SearchData * sd,const gchar * replacement)276 _match_props (DiaObject *obj, const SearchData *sd, const gchar *replacement)
277 {
278 g_return_val_if_fail (obj && sd, NULL);
279
280 if (sd->flags & MATCH_ALL_PROPERTIES)
281 return _match_all_props (obj, sd, replacement);
282 else
283 return _match_name_prop (obj, sd, replacement);
284 }
285
286
287 /* Only match (find), do not replace any values. */
288 static gboolean
_matches(DiaObject * obj,const SearchData * sd)289 _matches (DiaObject *obj, const SearchData *sd)
290 {
291 GPtrArray *plist = NULL;
292
293 if (!obj)
294 return FALSE;
295
296 plist = _match_props (obj, sd, NULL);
297 if (!plist)
298 return FALSE;
299
300 prop_list_free (plist);
301
302 return TRUE;
303 }
304
305 static void
find_func(gpointer data,gpointer user_data)306 find_func (gpointer data, gpointer user_data)
307 {
308 DiaObject *obj = data;
309 SearchData *sd = (SearchData *)user_data;
310
311 if (!sd->found) {
312 if (_matches (obj, sd)) {
313 if (!sd->first)
314 sd->first = obj;
315 if (obj == sd->last)
316 sd->seen_last = TRUE;
317 else if (sd->seen_last) {
318 sd->found = obj;
319 }
320 }
321 }
322 }
323
324 /* Match and replace property values. */
325 static gboolean
_replace(DiaObject * obj,const SearchData * sd,const char * replacement)326 _replace (DiaObject *obj, const SearchData *sd, const char *replacement)
327 {
328 ObjectChange *obj_change;
329 GPtrArray *plist = NULL;
330
331 plist = _match_props (obj, sd, replacement);
332 if (!plist)
333 return FALSE;
334
335 /* Refresh screen and free the list of modified properties. */
336 obj_change = object_apply_props (obj, plist);
337 prop_list_free (plist);
338
339 if (obj_change)
340 undo_object_change(sd->diagram, obj, obj_change);
341
342 object_add_updates(obj, sd->diagram);
343 diagram_update_connections_object(sd->diagram, obj, TRUE);
344 diagram_modified(sd->diagram);
345 diagram_object_modified(sd->diagram, obj);
346 diagram_update_extents(sd->diagram);
347 diagram_flush(sd->diagram);
348
349 return TRUE;
350 }
351
352 static gint
fnr_respond(GtkWidget * widget,gint response_id,gpointer data)353 fnr_respond (GtkWidget *widget, gint response_id, gpointer data)
354 {
355 const gchar *search = gtk_entry_get_text (g_object_get_data (G_OBJECT (widget), "search-entry"));
356 const gchar *replace;
357 DDisplay *ddisp = (DDisplay*)data;
358 SearchData sd = { 0, };
359 sd.diagram = ddisp->diagram;
360 sd.flags = gtk_toggle_button_get_active( GTK_TOGGLE_BUTTON (
361 g_object_get_data (G_OBJECT (widget), "match-case"))) ? MATCH_CASE : 0;
362 sd.flags |= gtk_toggle_button_get_active( GTK_TOGGLE_BUTTON (
363 g_object_get_data (G_OBJECT (widget), "match-word"))) ? MATCH_WORD : 0;
364 sd.flags |= gtk_toggle_button_get_active( GTK_TOGGLE_BUTTON (
365 g_object_get_data (G_OBJECT (widget), "match-all-properties"))) ? MATCH_ALL_PROPERTIES : 0;
366
367
368 switch (response_id) {
369 case RESPONSE_FIND :
370 sd.key = search;
371 sd.last = g_object_get_data (G_OBJECT (widget), "last-found");
372 if (!_matches (sd.last, &sd))
373 sd.last = NULL; /* reset if we start a new search */
374 diagram_remove_all_selected (ddisp->diagram, TRUE);
375 data_foreach_object (ddisp->diagram->data, find_func, &sd);
376 /* remember it */
377 sd.last = sd.found ? sd.found : sd.first;
378 g_object_set_data (G_OBJECT (widget), "last-found", sd.last);
379 if (sd.last) {
380 if (dia_object_get_parent_layer(sd.last) != ddisp->diagram->data->active_layer) {
381 /* can only select objects in the active layer */
382 data_set_active_layer(ddisp->diagram->data, dia_object_get_parent_layer(sd.last));
383 diagram_add_update_all(ddisp->diagram);
384 diagram_flush(ddisp->diagram);
385 }
386 diagram_select (ddisp->diagram, sd.last);
387 ddisplay_present_object (ddisp, sd.last);
388 }
389 break;
390 case RESPONSE_REPLACE :
391 replace = gtk_entry_get_text (g_object_get_data (G_OBJECT (widget), "replace-entry"));
392 sd.key = search;
393 sd.last = g_object_get_data (G_OBJECT (widget), "last-found");
394 if (!_matches (sd.last, &sd)) {
395 sd.last = NULL; /* reset if we start a new search */
396 data_foreach_object (ddisp->diagram->data, find_func, &sd);
397 }
398 sd.last = sd.found ? sd.found : sd.first;
399 g_object_set_data (G_OBJECT (widget), "last-found", sd.last);
400 if (sd.last) {
401 _replace (sd.last, &sd, replace);
402 undo_set_transactionpoint(ddisp->diagram->undo);
403 }
404 g_object_set_data (G_OBJECT (widget), "last-found", sd.last);
405 break;
406 case RESPONSE_REPLACE_ALL :
407 replace = gtk_entry_get_text (g_object_get_data (G_OBJECT (widget), "replace-entry"));
408 sd.key = search;
409 sd.last = g_object_get_data (G_OBJECT (widget), "last-found");
410 do {
411 if (!_matches (sd.last, &sd)) {
412 sd.last = NULL; /* reset if we start a new search */
413 sd.first = NULL;
414 data_foreach_object (ddisp->diagram->data, find_func, &sd);
415 }
416 sd.last = sd.found ? sd.found : sd.first;
417 if (sd.last)
418 if (!_replace (sd.last, &sd, replace))
419 sd.last = NULL;
420 } while (sd.last);
421 g_object_set_data (G_OBJECT (widget), "last-found", sd.last);
422 undo_set_transactionpoint(ddisp->diagram->undo);
423 break;
424 default:
425 gtk_widget_hide (widget);
426 }
427 return 0;
428 }
429
430 static void
fnr_dialog_setup_common(GtkWidget * dialog,gboolean is_replace,DDisplay * ddisp)431 fnr_dialog_setup_common (GtkWidget *dialog, gboolean is_replace, DDisplay *ddisp)
432 {
433 GtkWidget *vbox;
434 GtkWidget *hbox;
435 GtkWidget *label;
436 GtkWidget *search_entry;
437 GtkWidget *match_case;
438 GtkWidget *match_word;
439 GtkWidget *match_all_properties;
440
441 gtk_dialog_set_default_response (GTK_DIALOG (dialog), RESPONSE_FIND);
442
443 /* don't destroy dialog when window manager close button pressed */
444 g_signal_connect(G_OBJECT (dialog), "response",
445 G_CALLBACK(fnr_respond), ddisp);
446 g_signal_connect(G_OBJECT(dialog), "delete_event",
447 G_CALLBACK(gtk_widget_hide), NULL);
448 g_signal_connect(GTK_OBJECT(dialog), "delete_event",
449 G_CALLBACK(gtk_true), NULL);
450
451 vbox = GTK_DIALOG(dialog)->vbox;
452
453 hbox = gtk_hbox_new (FALSE, 12);
454 label = gtk_label_new_with_mnemonic (_("_Search for:"));
455 gtk_box_pack_start (GTK_BOX (hbox), label, FALSE, FALSE, 0);
456 search_entry = gtk_entry_new ();
457 g_object_set_data (G_OBJECT (dialog), "search-entry", search_entry);
458 gtk_label_set_mnemonic_widget (GTK_LABEL (label), search_entry);
459 gtk_entry_set_width_chars (GTK_ENTRY (search_entry), 30);
460 gtk_box_pack_start (GTK_BOX (hbox), search_entry, TRUE, TRUE, 0);
461 gtk_box_pack_start (GTK_BOX (vbox), hbox, FALSE, FALSE, 6);
462
463 if (is_replace) {
464 GtkWidget *replace_entry;
465
466 hbox = gtk_hbox_new (FALSE, 12);
467 label = gtk_label_new_with_mnemonic (_("Replace _with:"));
468 gtk_box_pack_start (GTK_BOX (hbox), label, FALSE, FALSE, 0);
469 replace_entry = gtk_entry_new ();
470 g_object_set_data (G_OBJECT (dialog), "replace-entry", replace_entry);
471 gtk_label_set_mnemonic_widget (GTK_LABEL (label), replace_entry);
472 gtk_entry_set_width_chars (GTK_ENTRY (replace_entry), 30);
473 gtk_box_pack_start (GTK_BOX (hbox), replace_entry, TRUE, TRUE, 0);
474 gtk_box_pack_start (GTK_BOX (vbox), hbox, FALSE, FALSE, 6);
475 }
476
477 match_case = gtk_check_button_new_with_mnemonic (_("_Match case"));
478 gtk_box_pack_start (GTK_BOX (vbox), match_case, FALSE, FALSE, 6);
479 g_object_set_data (G_OBJECT (dialog), "match-case", match_case);
480
481 match_word = gtk_check_button_new_with_mnemonic (_("Match _entire word only"));
482 gtk_box_pack_start (GTK_BOX (vbox), match_word, FALSE, FALSE, 6);
483 g_object_set_data (G_OBJECT (dialog), "match-word", match_word);
484
485 match_all_properties = gtk_check_button_new_with_mnemonic (_("Match _all properties (not just object name)"));
486 gtk_box_pack_start (GTK_BOX (vbox), match_all_properties, FALSE, FALSE, 6);
487 g_object_set_data (G_OBJECT (dialog), "match-all-properties", match_all_properties);
488 if (is_replace)
489 gtk_widget_set_sensitive (GTK_WIDGET (match_all_properties), FALSE);
490
491
492 gtk_widget_show_all (vbox);
493 }
494
495 /**
496 * React to <Display>/Edit/Find
497 */
498 void
edit_find_callback(gpointer data,guint action,GtkWidget * widget)499 edit_find_callback(gpointer data, guint action, GtkWidget *widget)
500 {
501 DDisplay *ddisp;
502 Diagram *dia;
503 GtkWidget *dialog;
504
505 ddisp = ddisplay_active();
506 if (!ddisp) return;
507 dia = ddisp->diagram;
508
509 /* no static var, instead we are attaching the dialog to the diplay shell */
510 dialog = g_object_get_data (G_OBJECT (ddisp->shell), "edit-find-dialog");
511 if (!dialog) {
512 dialog = gtk_dialog_new_with_buttons (
513 _("Find"),
514 GTK_WINDOW (ddisp->shell), GTK_DIALOG_DESTROY_WITH_PARENT,
515 GTK_STOCK_CLOSE, GTK_RESPONSE_CLOSE,
516 GTK_STOCK_FIND, RESPONSE_FIND,
517 NULL);
518
519 fnr_dialog_setup_common (dialog, FALSE, ddisp);
520 }
521 g_object_set_data (G_OBJECT (ddisp->shell), "edit-find-dialog", dialog);
522
523 gtk_dialog_run (GTK_DIALOG (dialog));
524 }
525
526 /**
527 * React to <Display>/Edit/Replace
528 */
529 void
edit_replace_callback(gpointer data,guint action,GtkWidget * widget)530 edit_replace_callback(gpointer data, guint action, GtkWidget *widget)
531 {
532 DDisplay *ddisp;
533 Diagram *dia;
534 GtkWidget *dialog;
535
536 ddisp = ddisplay_active();
537 if (!ddisp) return;
538 dia = ddisp->diagram;
539
540 /* no static var, instead we are attaching the dialog to the diplay shell */
541 dialog = g_object_get_data (G_OBJECT (ddisp->shell), "edit-replace-dialog");
542 if (!dialog) {
543 GtkWidget *button;
544 dialog = gtk_dialog_new_with_buttons (
545 _("Replace"),
546 GTK_WINDOW (ddisp->shell), GTK_DIALOG_DESTROY_WITH_PARENT,
547 GTK_STOCK_CLOSE, GTK_RESPONSE_CLOSE,
548 _("Replace _All"), RESPONSE_REPLACE_ALL,
549 NULL);
550 /* not adding the button in the list above to modify it's text;
551 * the default "Find and Replace" is just too long for my taste ;)
552 */
553 button = gtk_dialog_add_button (GTK_DIALOG (dialog), _("_Replace"), RESPONSE_REPLACE);
554 gtk_button_set_image (GTK_BUTTON (button),
555 gtk_image_new_from_stock (GTK_STOCK_FIND_AND_REPLACE, GTK_ICON_SIZE_BUTTON));
556
557 gtk_dialog_add_button (GTK_DIALOG (dialog), GTK_STOCK_FIND, RESPONSE_FIND);
558
559 fnr_dialog_setup_common (dialog, TRUE, ddisp);
560 }
561 g_object_set_data (G_OBJECT (ddisp->shell), "edit-replace-dialog", dialog);
562
563 gtk_dialog_run (GTK_DIALOG (dialog));
564 }
565