val-labs-dialog: Improve keyboard interface.
[pspp] / src / ui / gui / val-labs-dialog.c
1 /* PSPPIRE - a graphical user interface for PSPP.
2    Copyright (C) 2005, 2009, 2010, 2011, 2012, 2015, 2016  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 describes the behaviour of the Value Labels dialog box,
19     used for input of the value labels in the variable sheet */
20
21 #include <config.h>
22
23 #include "ui/gui/val-labs-dialog.h"
24
25 #include <string.h>
26
27 #include "data/value-labels.h"
28 #include "data/format.h"
29 #include "libpspp/i18n.h"
30 #include "ui/gui/builder-wrapper.h"
31 #include "ui/gui/helper.h"
32
33 #include <gettext.h>
34 #define _(msgid) gettext (msgid)
35 #define N_(msgid) msgid
36
37 static GObject *psppire_val_labs_dialog_constructor (GType type, guint,
38                                                      GObjectConstructParam *);
39 static void psppire_val_labs_dialog_finalize (GObject *);
40
41 G_DEFINE_TYPE (PsppireValLabsDialog,
42                psppire_val_labs_dialog,
43                PSPPIRE_TYPE_DIALOG);
44 enum
45   {
46     PROP_0,
47     PROP_VARIABLE,
48     PROP_VALUE_LABELS
49   };
50
51 static void do_change (PsppireValLabsDialog *);
52
53 static void
54 psppire_val_labs_dialog_set_property (GObject      *object,
55                                       guint         prop_id,
56                                       const GValue *value,
57                                       GParamSpec   *pspec)
58 {
59   PsppireValLabsDialog *obj = PSPPIRE_VAL_LABS_DIALOG (object);
60
61   switch (prop_id)
62     {
63     case PROP_VARIABLE:
64       psppire_val_labs_dialog_set_variable (obj, g_value_get_pointer (value));
65       break;
66     case PROP_VALUE_LABELS:
67     default:
68       G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
69       break;
70     }
71 }
72
73 static void
74 psppire_val_labs_dialog_get_property (GObject      *object,
75                                       guint         prop_id,
76                                       GValue       *value,
77                                       GParamSpec   *pspec)
78 {
79   PsppireValLabsDialog *obj = PSPPIRE_VAL_LABS_DIALOG (object);
80
81   switch (prop_id)
82     {
83     case PROP_VALUE_LABELS:
84       g_value_set_pointer (value, obj->labs);
85       break;
86     case PROP_VARIABLE:
87     default:
88       G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
89       break;
90     }
91 }
92
93 static void
94 psppire_val_labs_dialog_class_init (PsppireValLabsDialogClass *class)
95 {
96   GObjectClass *gobject_class;
97   gobject_class = G_OBJECT_CLASS (class);
98
99   gobject_class->constructor = psppire_val_labs_dialog_constructor;
100   gobject_class->finalize = psppire_val_labs_dialog_finalize;
101   gobject_class->set_property = psppire_val_labs_dialog_set_property;
102   gobject_class->get_property = psppire_val_labs_dialog_get_property;
103
104   g_object_class_install_property (
105     gobject_class, PROP_VARIABLE,
106     g_param_spec_pointer ("variable",
107                           "Variable",
108                           "Variable whose value labels are to be edited.  The "
109                           "variable's print format and encoding are also used "
110                           "for editing.",
111                           G_PARAM_WRITABLE));
112
113   g_object_class_install_property (
114     gobject_class, PROP_VALUE_LABELS,
115     g_param_spec_pointer ("value-labels",
116                           "Value Labels",
117                           "Edited value labels.",
118                           G_PARAM_READABLE));
119 }
120
121 static void
122 psppire_val_labs_dialog_init (PsppireValLabsDialog *obj)
123 {
124   /* We do all of our work on widgets in the constructor function, because that
125      runs after the construction properties have been set.  Otherwise
126      PsppireDialog's "orientation" property hasn't been set and therefore we
127      have no box to populate. */
128   obj->labs = val_labs_create (0);
129 }
130
131 static void
132 psppire_val_labs_dialog_finalize (GObject *obj)
133 {
134   PsppireValLabsDialog *dialog = PSPPIRE_VAL_LABS_DIALOG (obj);
135
136   val_labs_destroy (dialog->labs);
137   g_free (dialog->encoding);
138
139   G_OBJECT_CLASS (psppire_val_labs_dialog_parent_class)->finalize (obj);
140 }
141
142 PsppireValLabsDialog *
143 psppire_val_labs_dialog_new (const struct variable *var)
144 {
145   return PSPPIRE_VAL_LABS_DIALOG (
146     g_object_new (PSPPIRE_TYPE_VAL_LABS_DIALOG,
147                   "variable", var,
148                   NULL));
149 }
150
151 struct val_labs *
152 psppire_val_labs_dialog_run (GtkWindow *parent_window,
153                              const struct variable *var)
154 {
155   PsppireValLabsDialog *dialog;
156   struct val_labs *labs;
157
158   dialog = psppire_val_labs_dialog_new (var);
159   gtk_window_set_transient_for (GTK_WINDOW (dialog), parent_window);
160   gtk_window_set_modal (GTK_WINDOW (dialog), TRUE);
161   gtk_widget_show (GTK_WIDGET (dialog));
162
163   labs = (psppire_dialog_run (PSPPIRE_DIALOG (dialog)) == GTK_RESPONSE_OK
164           ? val_labs_clone (psppire_val_labs_dialog_get_value_labels (dialog))
165           : NULL);
166
167   gtk_widget_destroy (GTK_WIDGET (dialog));
168
169   return labs;
170 }
171
172 /* This callback occurs when the text in the label entry box
173    is changed */
174 static void
175 on_label_entry_change (GtkEntry *entry, gpointer data)
176 {
177   union value v;
178   const gchar *text ;
179   PsppireValLabsDialog *dialog = data;
180   g_assert (dialog->labs);
181
182   text = gtk_entry_get_text (GTK_ENTRY (dialog->value_entry));
183
184   text_to_value__ (text, &dialog->format, dialog->encoding, &v);
185
186   if (val_labs_find (dialog->labs, &v))
187     {
188       gtk_widget_set_sensitive (dialog->change_button, TRUE);
189       gtk_widget_set_sensitive (dialog->add_button, FALSE);
190     }
191   else
192     {
193       gtk_widget_set_sensitive (dialog->change_button, FALSE);
194       gtk_widget_set_sensitive (dialog->add_button, TRUE);
195     }
196
197   value_destroy (&v, val_labs_get_width (dialog->labs));
198 }
199
200 /* This callback occurs when Enter is pressed in the label entry box. */
201 static void
202 on_label_entry_activate (GtkEntry *entry, gpointer data)
203 {
204   PsppireValLabsDialog *dialog = data;
205   do_change (dialog);
206 }
207
208 /* Return the value-label pair currently selected in the dialog box  */
209
210 /* Set the TREEVIEW list cursor to the item which has the value VAL */
211 static void
212 select_treeview_from_value (GtkTreeView *treeview, union value *val)
213 {
214   GtkTreePath *path ;
215
216   /*
217     We do this with a linear search through the model --- hardly
218     efficient, but the list is short ... */
219   GtkTreeIter iter;
220
221   GtkTreeModel * model  = gtk_tree_view_get_model (treeview);
222
223   gboolean success;
224   for (success = gtk_tree_model_get_iter_first (model, &iter);
225        success;
226        success = gtk_tree_model_iter_next (model, &iter))
227     {
228       union value v;
229       GValue gvalue = {0};
230
231       gtk_tree_model_get_value (model, &iter, 1, &gvalue);
232
233       v.f = g_value_get_double (&gvalue);
234
235       if ( 0 == memcmp (&v, val, sizeof (union value)))
236         {
237           break;
238         }
239     }
240
241   path = gtk_tree_model_get_path (model, &iter);
242   if ( path )
243     {
244       gtk_tree_view_set_cursor (treeview, path, 0, 0);
245       gtk_tree_path_free (path);
246     }
247
248 }
249
250
251 /* This callback occurs when the text in the value entry box is
252    changed */
253 static void
254 on_value_entry_change (GtkEntry *entry, gpointer data)
255 {
256   const char *s;
257
258   PsppireValLabsDialog *dialog = data;
259
260   const gchar *text = gtk_entry_get_text (GTK_ENTRY (dialog->value_entry));
261
262   union value v;
263   text_to_value__ (text, &dialog->format, dialog->encoding, &v);
264
265   g_signal_handler_block (GTK_ENTRY (dialog->label_entry),
266                          dialog->change_handler_id);
267
268   gtk_entry_set_text (GTK_ENTRY (dialog->label_entry),"");
269
270
271   if ( (s = val_labs_find (dialog->labs, &v)) )
272     {
273       gtk_entry_set_text (GTK_ENTRY (dialog->label_entry), s);
274       gtk_widget_set_sensitive (dialog->add_button, FALSE);
275       gtk_widget_set_sensitive (dialog->remove_button, TRUE);
276       select_treeview_from_value (GTK_TREE_VIEW (dialog->treeview), &v);
277     }
278   else
279     {
280       gtk_widget_set_sensitive (dialog->remove_button, FALSE);
281       gtk_widget_set_sensitive (dialog->add_button, TRUE);
282     }
283
284   g_signal_handler_unblock (GTK_ENTRY (dialog->label_entry),
285                          dialog->change_handler_id);
286
287   value_destroy (&v, val_labs_get_width (dialog->labs));
288 }
289
290 /* This callback occurs when Enter is pressed in the value entry box. */
291 static void
292 on_value_entry_activate (GtkEntry *entry, gpointer data)
293 {
294   PsppireValLabsDialog *dialog = data;
295
296   gtk_widget_grab_focus (dialog->label_entry);
297 }
298
299 static gboolean
300 get_selected_tuple (PsppireValLabsDialog *dialog,
301                     union value *valuep, const char **label)
302 {
303   GtkTreeView *treeview = GTK_TREE_VIEW (dialog->treeview);
304
305   GtkTreeIter iter ;
306   GValue the_value = {0};
307   union value value;
308
309   GtkTreeSelection* sel =  gtk_tree_view_get_selection (treeview);
310
311   GtkTreeModel * model  = gtk_tree_view_get_model (treeview);
312
313   if (! gtk_tree_selection_get_selected (sel, &model, &iter))
314     return FALSE;
315
316   gtk_tree_model_get_value (model, &iter, 1, &the_value);
317
318   value.f = g_value_get_double (&the_value);
319   g_value_unset (&the_value);
320
321   if (valuep != NULL)
322     *valuep = value;
323   if (label != NULL)
324     {
325       struct val_lab *vl = val_labs_lookup (dialog->labs, &value);
326       if (vl != NULL)
327         *label = val_lab_get_escaped_label (vl);
328     }
329   
330   return TRUE;
331 }
332
333
334 static void repopulate_dialog (PsppireValLabsDialog *dialog);
335
336 /* Callback which occurs when the "Change" button is clicked */
337 static void
338 on_change (GtkWidget *w, gpointer data)
339 {
340   PsppireValLabsDialog *dialog = data;
341   do_change (dialog);
342 }
343
344 static void
345 do_change (PsppireValLabsDialog *dialog)
346 {
347   const gchar *val_text = gtk_entry_get_text (GTK_ENTRY (dialog->value_entry));
348
349   union value v;
350
351   text_to_value__ (val_text, &dialog->format, dialog->encoding, &v);
352
353   val_labs_replace (dialog->labs, &v,
354                     gtk_entry_get_text (GTK_ENTRY (dialog->label_entry)));
355
356   gtk_widget_set_sensitive (dialog->change_button, FALSE);
357
358   repopulate_dialog (dialog);
359   gtk_widget_grab_focus (dialog->value_entry);
360
361   value_destroy (&v, val_labs_get_width (dialog->labs));
362 }
363
364 /* Callback which occurs when the "Add" button is clicked */
365 static void
366 on_add (GtkWidget *w, gpointer data)
367 {
368   PsppireValLabsDialog *dialog = data;
369
370   union value v;
371
372   const gchar *text = gtk_entry_get_text (GTK_ENTRY (dialog->value_entry));
373
374   text_to_value__ (text, &dialog->format, dialog->encoding, &v);
375
376   if (val_labs_add (dialog->labs, &v,
377                     gtk_entry_get_text
378                     ( GTK_ENTRY (dialog->label_entry)) ) )
379     {
380       gtk_widget_set_sensitive (dialog->add_button, FALSE);
381
382       repopulate_dialog (dialog);
383       gtk_widget_grab_focus (dialog->value_entry);
384     }
385
386   value_destroy (&v, val_labs_get_width (dialog->labs));
387 }
388
389 /* Callback which occurs when the "Remove" button is clicked */
390 static void
391 on_remove (GtkWidget *w, gpointer data)
392 {
393   PsppireValLabsDialog *dialog = data;
394
395   union value value;
396   struct val_lab *vl;
397
398   if (! get_selected_tuple (dialog, &value, NULL))
399     return;
400   
401   vl = val_labs_lookup (dialog->labs, &value);
402   if (vl != NULL)
403     val_labs_remove (dialog->labs, vl);
404
405   repopulate_dialog (dialog);
406   gtk_widget_grab_focus (dialog->value_entry);
407
408   gtk_widget_set_sensitive (dialog->remove_button, FALSE);
409 }
410
411
412
413 /* Callback which occurs when a line item is selected in the list of
414    value--label pairs.*/
415 static void
416 on_select_row (GtkTreeView *treeview, gpointer data)
417 {
418   PsppireValLabsDialog *dialog = data;
419
420   union value value;
421   const char *label = NULL;
422
423   gchar *text;
424
425   if (! get_selected_tuple (dialog, &value, &label))
426     return;
427   
428   text = value_to_text__ (value, &dialog->format, dialog->encoding);
429
430   g_signal_handler_block (GTK_ENTRY (dialog->value_entry),
431                          dialog->value_handler_id);
432
433   gtk_entry_set_text (GTK_ENTRY (dialog->value_entry), text);
434
435   g_signal_handler_unblock (GTK_ENTRY (dialog->value_entry),
436                          dialog->value_handler_id);
437   g_free (text);
438
439   g_signal_handler_block (GTK_ENTRY (dialog->label_entry),
440                          dialog->change_handler_id);
441
442
443   gtk_entry_set_text (GTK_ENTRY (dialog->label_entry),
444                       label);
445
446   g_signal_handler_unblock (GTK_ENTRY (dialog->label_entry),
447                          dialog->change_handler_id);
448
449   gtk_widget_set_sensitive (dialog->remove_button, TRUE);
450   gtk_widget_set_sensitive (dialog->change_button, FALSE);
451 }
452
453
454 /* Create a new dialog box
455    (there should  normally be only one)*/
456 static GObject *
457 psppire_val_labs_dialog_constructor (GType                  type,
458                                      guint                  n_properties,
459                                      GObjectConstructParam *properties)
460 {
461   PsppireValLabsDialog *dialog;
462   GtkTreeViewColumn *column;
463
464   GtkCellRenderer *renderer ;
465
466   GtkBuilder *xml = builder_new ("val-labs-dialog.ui");
467
468   GtkContainer *content_area;
469   GObject *obj;
470
471   obj = G_OBJECT_CLASS (psppire_val_labs_dialog_parent_class)->constructor (
472     type, n_properties, properties);
473   dialog = PSPPIRE_VAL_LABS_DIALOG (obj);
474
475   content_area = GTK_CONTAINER (PSPPIRE_DIALOG (dialog));
476   gtk_container_add (GTK_CONTAINER (content_area),
477                      get_widget_assert (xml, "val-labs-dialog"));
478
479   dialog->value_entry = get_widget_assert (xml,"value_entry");
480   dialog->label_entry = get_widget_assert (xml,"label_entry");
481
482   dialog->add_button = get_widget_assert (xml, "val_labs_add");
483   dialog->remove_button = get_widget_assert (xml, "val_labs_remove");
484   dialog->change_button = get_widget_assert (xml, "val_labs_change");
485
486   dialog->treeview = get_widget_assert (xml,"treeview1");
487
488   gtk_tree_view_set_headers_visible (GTK_TREE_VIEW (dialog->treeview), FALSE);
489
490   renderer = gtk_cell_renderer_text_new ();
491
492   column = gtk_tree_view_column_new_with_attributes ("Title",
493                                                      renderer,
494                                                      "text",
495                                                      0,
496                                                      NULL);
497
498   gtk_tree_view_append_column (GTK_TREE_VIEW (dialog->treeview), column);
499
500   dialog->change_handler_id =
501     g_signal_connect (dialog->label_entry,
502                      "changed",
503                      G_CALLBACK (on_label_entry_change), dialog);
504   g_signal_connect (dialog->label_entry, "activate",
505                     G_CALLBACK (on_label_entry_activate), dialog);
506
507   dialog->value_handler_id  =
508     g_signal_connect (dialog->value_entry,
509                      "changed",
510                      G_CALLBACK (on_value_entry_change), dialog);
511   g_signal_connect (dialog->value_entry, "activate",
512                     G_CALLBACK (on_value_entry_activate), dialog);
513
514   g_signal_connect (dialog->change_button,
515                    "clicked",
516                    G_CALLBACK (on_change), dialog);
517
518
519   g_signal_connect (dialog->treeview, "cursor-changed",
520                    G_CALLBACK (on_select_row), dialog);
521
522   g_signal_connect (dialog->remove_button, "clicked",
523                    G_CALLBACK (on_remove), dialog);
524
525   g_signal_connect (dialog->add_button, "clicked",
526                    G_CALLBACK (on_add), dialog);
527
528   dialog->labs = NULL;
529
530   g_object_unref (xml);
531
532   return obj;
533 }
534
535
536 /* Populate the components of the dialog box, from the 'labs' member
537    variable */
538 static void
539 repopulate_dialog (PsppireValLabsDialog *dialog)
540 {
541   const struct val_lab **labels;
542   size_t n_labels;
543   size_t i;
544
545   GtkTreeIter iter;
546
547   GtkListStore *list_store = gtk_list_store_new (2,
548                                                  G_TYPE_STRING,
549                                                  G_TYPE_DOUBLE);
550
551   g_signal_handler_block (GTK_ENTRY (dialog->label_entry),
552                          dialog->change_handler_id);
553   g_signal_handler_block (GTK_ENTRY (dialog->value_entry),
554                          dialog->value_handler_id);
555
556   gtk_entry_set_text (GTK_ENTRY (dialog->value_entry), "");
557   gtk_entry_set_text (GTK_ENTRY (dialog->label_entry), "");
558
559   g_signal_handler_unblock (GTK_ENTRY (dialog->value_entry),
560                          dialog->value_handler_id);
561   g_signal_handler_unblock (GTK_ENTRY (dialog->label_entry),
562                            dialog->change_handler_id);
563
564   labels = val_labs_sorted (dialog->labs);
565   n_labels = val_labs_count (dialog->labs);
566   for (i = 0; i < n_labels; i++)
567     {
568       const struct val_lab *vl = labels[i];
569
570       gchar *const vstr  =
571         value_to_text__ (vl->value, &dialog->format, dialog->encoding);
572
573       gchar *const text = g_strdup_printf (_("%s = `%s'"), vstr,
574                                            val_lab_get_escaped_label (vl));
575
576       gtk_list_store_append (list_store, &iter);
577       gtk_list_store_set (list_store, &iter,
578                           0, text,
579                           1, vl->value.f,
580                           -1);
581
582       g_free (text);
583       g_free (vstr);
584     }
585   free (labels);
586
587   gtk_tree_view_set_model (GTK_TREE_VIEW (dialog->treeview),
588                           GTK_TREE_MODEL (list_store));
589
590   g_object_unref (list_store);
591
592 }
593
594 void
595 psppire_val_labs_dialog_set_variable (PsppireValLabsDialog *dialog,
596                                       const struct variable *var)
597 {
598   val_labs_destroy (dialog->labs);
599   dialog->labs = NULL;
600
601   g_free (dialog->encoding);
602   dialog->encoding = NULL;
603
604   if (var != NULL)
605     {
606       dialog->labs = val_labs_clone (var_get_value_labels (var));
607       dialog->encoding = g_strdup (var_get_encoding (var));
608       dialog->format = *var_get_print_format (var);
609     }
610   else
611     dialog->format = F_8_0;
612
613   if (dialog->labs == NULL)
614     dialog->labs = val_labs_create (var_get_width (var));
615
616   repopulate_dialog (dialog);
617 }
618
619 const struct val_labs *
620 psppire_val_labs_dialog_get_value_labels (const PsppireValLabsDialog *dialog)
621 {
622   return dialog->labs;
623 }