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