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