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