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