Find dialog: Don't crash when searching for a non-existant label.
[pspp] / src / ui / gui / find-dialog.c
1 /* PSPPIRE - a graphical user interface for PSPP.
2    Copyright (C) 2007, 2009, 2011, 2012  Free Software Foundation
3
4    This program is free software: you can redistribute it and/or modify
5    it under the terms of the GNU General Public License as published by
6    the Free Software Foundation, either version 3 of the License, or
7    (at your option) any later version.
8
9    This program is distributed in the hope that it will be useful,
10    but WITHOUT ANY WARRANTY; without even the implied warranty of
11    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12    GNU General Public License for more details.
13
14    You should have received a copy of the GNU General Public License
15    along with this program.  If not, see <http://www.gnu.org/licenses/>. */
16
17
18 /* This module implements the "Find" dialog; a dialog box to locate cases
19 which match particular strings */
20
21 #include <config.h>
22
23 #include "find-dialog.h"
24 #include "psppire-selector.h"
25 #include "psppire-dialog.h"
26 #include "builder-wrapper.h"
27 #include "helper.h"
28 #include "psppire-data-window.h"
29 #include "dict-display.h"
30 #include <data/value.h>
31 #include <data/format.h>
32 #include <data/datasheet.h>
33 #include <data/data-in.h>
34 #include "psppire-data-store.h"
35 #include <ctype.h>
36 #include <sys/types.h>
37 #include <regex.h>
38 #include <libpspp/cast.h>
39 #include <libpspp/message.h>
40
41 #include <gtk/gtk.h>
42 #include <stdlib.h>
43
44 #include "xalloc.h"
45
46 #include <gettext.h>
47 #define _(msgid) gettext (msgid)
48 #define N_(msgid) msgid
49
50
51 /* FIXME: These shouldn't be here */
52 #include "psppire-var-store.h"
53
54 struct find_dialog
55 {
56   GtkBuilder *xml;
57   PsppireDict *dict;
58   struct datasheet *data;
59   PsppireDataWindow *de;
60   GtkWidget *variable_entry;
61   GtkWidget *value_entry;
62   GtkWidget *value_labels_checkbox;
63   GtkWidget *match_regexp_checkbox;
64   GtkWidget *match_substring_checkbox;
65 };
66
67 static void
68 find_value (const struct find_dialog *fd, casenumber current_row,
69            casenumber *row, int *column);
70
71
72 /* A callback which occurs whenever the "Refresh" button is clicked,
73    and when the dialog pops up.
74    It restores the dialog to its default state.
75 */
76 static void
77 refresh (GObject *obj, const struct find_dialog *fd)
78 {
79   gtk_toggle_button_set_active
80     (GTK_TOGGLE_BUTTON (get_widget_assert (fd->xml, "find-wrap")),
81      FALSE);
82
83   gtk_toggle_button_set_active
84     (GTK_TOGGLE_BUTTON (get_widget_assert (fd->xml, "find-backwards")),
85      FALSE);
86
87   gtk_entry_set_text (GTK_ENTRY (fd->variable_entry), "");
88   gtk_entry_set_text (GTK_ENTRY (fd->value_entry), "");
89
90   gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (fd->match_regexp_checkbox),
91                                 FALSE);
92
93   gtk_toggle_button_set_active
94     (GTK_TOGGLE_BUTTON (fd->match_substring_checkbox), FALSE);
95
96
97   gtk_toggle_button_set_active
98     (GTK_TOGGLE_BUTTON (fd->match_substring_checkbox), FALSE);
99 }
100
101 /* Callback on the "Find" button */
102 static void
103 do_find (GObject *obj, const struct find_dialog *fd)
104 {
105   casenumber x = -1;
106   gint column = -1;
107   glong row;
108
109   g_object_get (fd->de->data_editor, "current-case", &row, NULL);
110
111   if ( row < 0 )
112     row = 0;
113
114   find_value (fd, row, &x, &column);
115
116
117   if ( x != -1)
118     {
119       gtk_notebook_set_current_page (GTK_NOTEBOOK (fd->de->data_editor),
120                                      PSPPIRE_DATA_EDITOR_DATA_VIEW);
121
122       g_object_set (fd->de->data_editor,
123                     "current-case", x,
124                     "current-variable", column,
125                     NULL);
126     }
127
128 }
129
130 /* Callback on the selector.
131    It gets invoked whenever a variable is selected */
132 static void
133 on_select (GtkEntry *entry, gpointer data)
134 {
135   struct find_dialog *fd = data;
136   const char *var_name = gtk_entry_get_text (GTK_ENTRY (fd->variable_entry));
137   struct variable *var = dict_lookup_var (fd->dict->dict, var_name);
138   gboolean search_labels ;
139
140   g_return_if_fail (var);
141
142   gtk_widget_set_sensitive (fd->value_labels_checkbox,
143                             var_has_value_labels (var));
144
145   search_labels =
146     gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (fd->value_labels_checkbox));
147
148   gtk_widget_set_sensitive (fd->match_regexp_checkbox,
149                             var_is_alpha (var) || search_labels);
150
151
152   gtk_widget_set_sensitive (fd->match_substring_checkbox,
153                             var_is_alpha (var) || search_labels);
154 }
155
156 /* Callback on the selector.
157    It gets invoked whenever a variable is unselected */
158 static void
159 on_deselect (GtkEntry *entry, gpointer data)
160 {
161   struct find_dialog *fd = data;
162
163   gtk_widget_set_sensitive (fd->value_labels_checkbox, FALSE);
164   gtk_widget_set_sensitive (fd->match_substring_checkbox, FALSE);
165   gtk_widget_set_sensitive (fd->match_regexp_checkbox, FALSE);
166 }
167
168 static void
169 value_labels_toggled (GtkToggleButton *tb, gpointer data)
170 {
171   struct find_dialog *fd = data;
172
173   const char *var_name = gtk_entry_get_text (GTK_ENTRY (fd->variable_entry));
174   const struct variable *var = dict_lookup_var (fd->dict->dict, var_name);
175
176   gboolean active = gtk_toggle_button_get_active  (tb) ;
177
178   gtk_widget_set_sensitive (fd->match_substring_checkbox,
179                             active || (var && var_is_alpha (var)));
180
181   gtk_widget_set_sensitive (fd->match_regexp_checkbox,
182                               active || (var && var_is_alpha (var)));
183 }
184
185 /* Pops up the Find dialog box
186  */
187 void
188 find_dialog (PsppireDataWindow *de)
189 {
190   struct find_dialog fd;
191
192   GtkWidget *dialog ;
193   GtkWidget *source ;
194   GtkWidget *selector;
195   GtkWidget *find_button;
196
197   GtkWidget *buttonbox;
198
199   PsppireVarStore *vs ;
200   PsppireDataStore *ds ;
201
202   fd.xml = builder_new ("find.ui");
203   fd.de = de;
204
205   find_button = gtk_button_new_from_stock  (GTK_STOCK_FIND);
206   gtk_widget_show (find_button);
207
208   buttonbox = get_widget_assert (fd.xml, "find-buttonbox");
209
210   psppire_box_pack_start_defaults (GTK_BOX (buttonbox), find_button);
211   gtk_box_reorder_child (GTK_BOX (buttonbox), find_button, 0);
212
213   dialog = get_widget_assert (fd.xml, "find-dialog");
214   source = get_widget_assert (fd.xml, "find-variable-treeview");
215   selector = get_widget_assert (fd.xml, "find-selector");
216
217   g_object_get (de->data_editor,
218                 "var-store", &vs,
219                 "data-store", &ds,
220                 NULL);
221
222   g_object_get (vs, "dictionary", &fd.dict, NULL);
223
224   fd.data = ds->datasheet;
225
226   fd.variable_entry        = get_widget_assert (fd.xml, "find-variable-entry");
227   fd.value_entry           = get_widget_assert (fd.xml, "find-value-entry");
228   fd.value_labels_checkbox =
229     get_widget_assert (fd.xml,
230                        "find-value-labels-checkbutton");
231
232   fd.match_regexp_checkbox =
233     get_widget_assert (fd.xml,
234                        "find-match-regexp-checkbutton");
235
236   fd.match_substring_checkbox =
237     get_widget_assert (fd.xml,
238                        "find-match-substring-checkbutton");
239
240
241
242   gtk_window_set_transient_for (GTK_WINDOW (dialog), GTK_WINDOW (de));
243
244
245   g_object_set (source, "model", fd.dict,
246         "selection-mode", GTK_SELECTION_SINGLE,
247         NULL);
248
249
250   psppire_selector_set_filter_func (PSPPIRE_SELECTOR (selector),
251                                     is_currently_in_entry);
252
253   g_signal_connect (dialog, "refresh", G_CALLBACK (refresh),  &fd);
254
255   g_signal_connect (find_button, "clicked", G_CALLBACK (do_find),  &fd);
256
257   g_signal_connect (selector, "selected",
258                     G_CALLBACK (on_select),  &fd);
259
260   g_signal_connect (selector, "de-selected",
261                     G_CALLBACK (on_deselect),  &fd);
262
263   g_signal_connect (fd.value_labels_checkbox, "toggled",
264                     G_CALLBACK (value_labels_toggled),  &fd);
265
266
267   psppire_dialog_run (PSPPIRE_DIALOG (dialog));
268
269   g_object_unref (fd.xml);
270 }
271
272 \f
273 /* Iterators */
274
275 static void
276 forward (casenumber *i, struct datasheet *data UNUSED)
277 {
278   ++*i;
279 }
280
281
282 static void
283 forward_wrap (casenumber *i, struct datasheet *data)
284 {
285   if ( ++*i >=  datasheet_get_n_rows (data) ) *i = 0;
286 }
287
288 static void
289 backward (casenumber *i, struct datasheet *data UNUSED)
290 {
291   --*i;
292 }
293
294
295 static void
296 backward_wrap (casenumber *i, struct datasheet *data)
297 {
298   if ( --*i < 0 )
299     *i = datasheet_get_n_rows (data) - 1;
300 }
301
302
303 /* Current plus one */
304 static casenumber
305 cp1 (casenumber current, struct datasheet *data)
306 {
307   return current + 1;
308 }
309
310 /* Current plus one, circular */
311 static casenumber
312 cp1c (casenumber current, struct datasheet *data)
313 {
314   casenumber next = current;
315
316   forward_wrap (&next, data);
317
318   return next;
319 }
320
321
322 /* Current minus one */
323 static casenumber
324 cm1 (casenumber current, struct datasheet *data)
325 {
326   return current - 1;
327 }
328
329 /* Current minus one, circular */
330 static casenumber
331 cm1c (casenumber current, struct datasheet *data)
332 {
333   casenumber next = current;
334
335   backward_wrap (&next, data);
336
337   return next;
338 }
339
340
341 static casenumber
342 last (casenumber current, struct datasheet *data)
343 {
344   return datasheet_get_n_rows (data) ;
345 }
346
347 static casenumber
348 minus1 (casenumber current, struct datasheet *data)
349 {
350   return -1;
351 }
352
353 /* An type to facilitate iterating through casenumbers */
354 struct casenum_iterator
355 {
356   /* returns the first case to access */
357   casenumber (*start) (casenumber, struct datasheet *);
358
359   /* Returns one past the last case to access */
360   casenumber (*end) (casenumber, struct datasheet *);
361
362   /* Sets the first arg to the next case to access */
363   void (*next) (casenumber *, struct datasheet *);
364 };
365
366 enum iteration_type{
367   FORWARD = 0,
368   FORWARD_WRAP,
369   REVERSE,
370   REVERSE_WRAP,
371   n_iterators
372 };
373
374 static const struct casenum_iterator ip[n_iterators] =
375   {
376     {cp1, last, forward},
377     {cp1c, cm1, forward_wrap},
378     {cm1, minus1, backward},
379     {cm1c, cp1, backward_wrap}
380   };
381
382
383 \f
384 /* A factory returning an iterator according to the dialog box's settings */
385 static const struct casenum_iterator *
386 get_iteration_params (const struct find_dialog *fd)
387 {
388   gboolean wrap = gtk_toggle_button_get_active
389     (GTK_TOGGLE_BUTTON (get_widget_assert (fd->xml, "find-wrap")));
390
391   gboolean reverse = gtk_toggle_button_get_active
392     (GTK_TOGGLE_BUTTON (get_widget_assert (fd->xml, "find-backwards")));
393
394   if ( wrap )
395     {
396       if ( reverse )
397         return &ip[REVERSE_WRAP];
398       else
399         return &ip[FORWARD_WRAP];
400     }
401   else
402     {
403       if ( reverse )
404         return &ip[REVERSE];
405       else
406         return &ip[FORWARD];
407     }
408 }
409
410
411 enum string_cmp_flags
412   {
413     STR_CMP_SUBSTR = 0x01, /* Find strings which are substrings of the
414                               values */
415     STR_CMP_REGEXP = 0x02, /* Match against a regular expression */
416
417     STR_CMP_LABELS = 0x04  /* Match against the values' labels instead
418                               of the data */
419   };
420
421
422 /* An abstract base type for comparing union values against a reference */
423 struct comparator
424 {
425   const struct variable *var;
426   enum string_cmp_flags flags;
427
428   bool (*compare) (const struct comparator *,
429                    const union value *);
430
431   void (*destroy) (struct comparator *);
432 };
433
434
435 /* A comparator which operates on the unadulterated union values */
436 struct value_comparator
437 {
438   struct comparator parent;
439   union value pattern;
440 };
441
442 /* A comparator which matches string values or parts thereof */
443 struct string_comparator
444 {
445   struct comparator parent;
446   const char *pattern;
447 };
448
449 /* A comparator to match string values against a POSIX.2 regular expression */
450 struct regexp_comparator
451 {
452   struct comparator parent;
453   regex_t re;
454 };
455
456
457 static bool
458 value_compare (const struct comparator *cmptr,
459                const union value *v)
460 {
461   const struct value_comparator *vc = (const struct value_comparator *) cmptr;
462   return 0 == value_compare_3way (v, &vc->pattern, var_get_width (cmptr->var));
463 }
464
465
466 /* Return true if the label of VAL matches the reference string*/
467 static bool
468 string_label_compare (const struct comparator *cmptr,
469                 const union value *val)
470 {
471   const struct string_comparator *ssc =
472     (const struct string_comparator *) cmptr;
473
474   int width;
475
476   const char *text = var_lookup_value_label (cmptr->var, val);
477   if (text == NULL)
478     return false;
479
480   width = strlen (text);
481
482   assert ( cmptr->flags & STR_CMP_LABELS);
483
484   g_return_val_if_fail (width > 0, false);
485
486   if ( cmptr->flags & STR_CMP_SUBSTR)
487     return (NULL != g_strstr_len (text, width, ssc->pattern));
488   else
489     return (0 == strncmp (text, ssc->pattern, width));
490 }
491
492 /* Return true if VAL matches the reference string*/
493 static bool
494 string_value_compare (const struct comparator *cmptr,
495                       const union value *val)
496 {
497   bool found;
498   char *text;
499   const struct string_comparator *ssc =
500     (const struct string_comparator *) cmptr;
501
502   int width = var_get_width (cmptr->var);
503   g_return_val_if_fail (width > 0, false);
504   assert ( ! (cmptr->flags & STR_CMP_LABELS));
505
506   text = value_to_text (*val, cmptr->var);
507
508   if ( cmptr->flags & STR_CMP_SUBSTR)
509     found =  (NULL != g_strstr_len (text, width, ssc->pattern));
510   else
511     found = (0 == strncmp (text, ssc->pattern, width));
512
513   free (text);
514   return found;
515 }
516
517
518
519 /* Return true if VAL matched the regexp */
520 static bool
521 regexp_value_compare (const struct comparator *cmptr,
522                 const union value *val)
523 {
524   char *text;
525   bool retval;
526   const struct regexp_comparator *rec =
527     (const struct regexp_comparator *) cmptr;
528
529   int width = var_get_width (cmptr->var);
530
531   assert  ( ! (cmptr->flags & STR_CMP_LABELS) );
532
533   g_return_val_if_fail (width > 0, false);
534
535   text = value_to_text (*val, cmptr->var);
536   /* We must remove trailing whitespace, otherwise $ will not match where
537      one would expect */
538   g_strchomp (text);
539
540   retval = (0 == regexec (&rec->re, text, 0, 0, 0));
541
542   g_free (text);
543
544   return retval;
545 }
546
547 /* Return true if the label of VAL matched the regexp */
548 static bool
549 regexp_label_compare (const struct comparator *cmptr,
550                       const union value *val)
551 {
552   const char *text;
553   const struct regexp_comparator *rec =
554     (const struct regexp_comparator *) cmptr;
555
556   int width ;
557
558   assert ( cmptr->flags & STR_CMP_LABELS);
559
560   text = var_lookup_value_label (cmptr->var, val);
561   width = strlen (text);
562
563   g_return_val_if_fail (width > 0, false);
564
565   return (0 == regexec (&rec->re, text, 0, 0, 0));
566 }
567
568
569
570 static void
571 regexp_destroy (struct comparator *cmptr)
572 {
573   struct regexp_comparator *rec
574     = UP_CAST (cmptr, struct regexp_comparator, parent);
575
576   regfree (&rec->re);
577 }
578
579 static void
580 cmptr_value_destroy (struct comparator *cmptr)
581 {
582   struct value_comparator *vc
583     = UP_CAST (cmptr, struct value_comparator, parent);
584   value_destroy (&vc->pattern, var_get_width (cmptr->var));
585 }
586
587
588 static struct comparator *
589 value_comparator_create (const struct variable *var, const char *target)
590 {
591   struct value_comparator *vc = xzalloc (sizeof (*vc));
592   struct comparator *cmptr = &vc->parent;
593
594   cmptr->flags = 0;
595   cmptr->var = var;
596   cmptr->compare  = value_compare ;
597   cmptr->destroy = cmptr_value_destroy;
598
599   text_to_value (target, var, &vc->pattern);
600
601   return cmptr;
602 }
603
604 static struct comparator *
605 string_comparator_create (const struct variable *var, const char *target,
606                           enum string_cmp_flags flags)
607 {
608   struct string_comparator *ssc = xzalloc (sizeof (*ssc));
609   struct comparator *cmptr = &ssc->parent;
610
611   cmptr->flags = flags;
612   cmptr->var = var;
613
614   if ( flags & STR_CMP_LABELS)
615     cmptr->compare = string_label_compare;
616   else
617     cmptr->compare = string_value_compare;
618
619   ssc->pattern = target;
620
621   return cmptr;
622 }
623
624
625 static struct comparator *
626 regexp_comparator_create (const struct variable *var, const char *target,
627                           enum string_cmp_flags flags)
628 {
629   int code;
630   struct regexp_comparator *rec = xzalloc (sizeof (*rec));
631   struct comparator *cmptr = &rec->parent;
632
633   cmptr->flags = flags;
634   cmptr->var = var;
635   cmptr->compare  = (flags & STR_CMP_LABELS)
636     ? regexp_label_compare : regexp_value_compare ;
637
638   cmptr->destroy  = regexp_destroy;
639
640   code = regcomp (&rec->re, target, 0);
641   if ( code != 0 )
642     {
643       char *errbuf = NULL;
644       size_t errbuf_size = regerror (code, &rec->re, errbuf,  0);
645
646       errbuf = xmalloc (errbuf_size);
647
648       regerror (code, &rec->re, errbuf, errbuf_size);
649
650       msg (ME, _("Bad regular expression: %s"), errbuf);
651
652       free ( cmptr);
653       free (errbuf);
654       return NULL;
655     }
656
657   return cmptr;
658 }
659
660
661 /* Compare V against CMPTR's reference */
662 static bool
663 comparator_compare (const struct comparator *cmptr,
664                     const union value *v)
665 {
666   return cmptr->compare (cmptr, v);
667 }
668
669 /* Destroy CMPTR */
670 static void
671 comparator_destroy (struct comparator *cmptr)
672 {
673   if ( ! cmptr )
674     return ;
675
676   if ( cmptr->destroy )
677     cmptr->destroy (cmptr);
678
679   free (cmptr);
680 }
681
682
683 static struct comparator *
684 comparator_factory (const struct variable *var, const char *str,
685                     enum string_cmp_flags flags)
686 {
687   if ( flags & STR_CMP_REGEXP )
688     return regexp_comparator_create (var, str, flags);
689
690   if ( flags & (STR_CMP_SUBSTR | STR_CMP_LABELS) )
691     return string_comparator_create (var, str, flags);
692
693   return value_comparator_create (var, str);
694 }
695
696
697 /* Find the row and column specified by the dialog FD, starting at CURRENT_ROW.
698    After the function returns, *ROW contains the row and *COLUMN the column.
699    If no such case is found, then *ROW will be set to -1
700  */
701 static void
702 find_value (const struct find_dialog *fd, casenumber current_row,
703            casenumber *row, int *column)
704 {
705   int width;
706   const struct variable *var;
707   const char *var_name = gtk_entry_get_text (GTK_ENTRY (fd->variable_entry));
708   const char *target_string = gtk_entry_get_text (GTK_ENTRY (fd->value_entry));
709
710   enum string_cmp_flags flags = 0;
711   g_assert (current_row >= 0);
712
713   var = dict_lookup_var (fd->dict->dict, var_name);
714   if ( ! var )
715     return ;
716
717   width = var_get_width (var);
718
719   *column = var_get_dict_index (var);
720   *row = -1;
721
722   if ( gtk_toggle_button_get_active
723        (GTK_TOGGLE_BUTTON (fd->match_substring_checkbox)))
724     flags |= STR_CMP_SUBSTR;
725
726   if ( gtk_toggle_button_get_active
727        (GTK_TOGGLE_BUTTON (fd->match_regexp_checkbox)))
728     flags |= STR_CMP_REGEXP;
729
730   if ( gtk_toggle_button_get_active
731        (GTK_TOGGLE_BUTTON (fd->value_labels_checkbox)))
732     flags |= STR_CMP_LABELS;
733
734   {
735     union value val;
736     casenumber i;
737     const struct casenum_iterator *ip = get_iteration_params (fd);
738     struct comparator *cmptr =
739       comparator_factory (var, target_string, flags);
740
741     value_init (&val, width);
742     if ( ! cmptr)
743       goto finish;
744
745     for (i = ip->start (current_row, fd->data);
746          i != ip->end (current_row, fd->data);
747          ip->next (&i, fd->data))
748       {
749         datasheet_get_value (fd->data, i, var_get_case_index (var), &val);
750
751         if ( comparator_compare (cmptr, &val))
752           {
753             *row = i;
754             break;
755           }
756       }
757
758   finish:
759     comparator_destroy (cmptr);
760     value_destroy (&val, width);
761   }
762 }