Allow adding new variables in the var sheet
[pspp] / src / ui / gui / missing-val-dialog.c
1 /* PSPPIRE - a graphical user interface for PSPP.
2    Copyright (C) 2005, 2006, 2009, 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 /*  This module describes the behaviour of the Missing Values dialog box,
18     used for input of the missing values in the variable sheet */
19
20 #include <config.h>
21
22 #include "ui/gui/missing-val-dialog.h"
23
24 #include "builder-wrapper.h"
25 #include "helper.h"
26 #include <data/format.h>
27 #include "missing-val-dialog.h"
28 #include <data/missing-values.h>
29 #include <data/variable.h>
30 #include <data/data-in.h>
31
32 #include <gtk/gtk.h>
33
34 #include <string.h>
35
36 #include <gettext.h>
37 #define _(msgid) gettext (msgid)
38 #define N_(msgid) msgid
39
40 static GObject *psppire_missing_val_dialog_constructor (
41   GType type, guint, GObjectConstructParam *);
42 static void psppire_missing_val_dialog_finalize (GObject *);
43
44 G_DEFINE_TYPE (PsppireMissingValDialog,
45                psppire_missing_val_dialog,
46                PSPPIRE_TYPE_DIALOG);
47 enum
48   {
49     PROP_0,
50     PROP_VARIABLE,
51     PROP_MISSING_VALUES
52   };
53
54 static void
55 psppire_missing_val_dialog_set_property (GObject      *object,
56                                          guint         prop_id,
57                                          const GValue *value,
58                                          GParamSpec   *pspec)
59 {
60   PsppireMissingValDialog *obj = PSPPIRE_MISSING_VAL_DIALOG (object);
61
62   switch (prop_id)
63     {
64     case PROP_VARIABLE:
65       psppire_missing_val_dialog_set_variable (obj,
66                                                g_value_get_pointer (value));
67       break;
68     case PROP_MISSING_VALUES:
69     default:
70       G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
71       break;
72     }
73 }
74
75 static void
76 psppire_missing_val_dialog_get_property (GObject      *object,
77                                          guint         prop_id,
78                                          GValue       *value,
79                                          GParamSpec   *pspec)
80 {
81   PsppireMissingValDialog *obj = PSPPIRE_MISSING_VAL_DIALOG (object);
82
83   switch (prop_id)
84     {
85     case PROP_MISSING_VALUES:
86       g_value_set_pointer (value, &obj->mvl);
87       break;
88     case PROP_VARIABLE:
89     default:
90       G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
91       break;
92     }
93 }
94
95 static void
96 psppire_missing_val_dialog_class_init (PsppireMissingValDialogClass *class)
97 {
98   GObjectClass *gobject_class;
99   gobject_class = G_OBJECT_CLASS (class);
100
101   gobject_class->constructor = psppire_missing_val_dialog_constructor;
102   gobject_class->finalize = psppire_missing_val_dialog_finalize;
103   gobject_class->set_property = psppire_missing_val_dialog_set_property;
104   gobject_class->get_property = psppire_missing_val_dialog_get_property;
105
106   g_object_class_install_property (
107     gobject_class, PROP_VARIABLE,
108     g_param_spec_pointer ("variable",
109                           "Variable",
110                           "Variable whose missing values are to be edited.  "
111                           "The variable's print format and encoding are also "
112                           "used for editing.",
113                           G_PARAM_WRITABLE));
114
115   g_object_class_install_property (
116     gobject_class, PROP_MISSING_VALUES,
117     g_param_spec_pointer ("missing-values",
118                           "Missing Values",
119                           "Edited missing values.",
120                           G_PARAM_READABLE));
121 }
122
123 static void
124 psppire_missing_val_dialog_init (PsppireMissingValDialog *dialog)
125 {
126   /* We do all of our work on widgets in the constructor function, because that
127      runs after the construction properties have been set.  Otherwise
128      PsppireDialog's "orientation" property hasn't been set and therefore we
129      have no box to populate. */
130   mv_init (&dialog->mvl, 0);
131   dialog->encoding = NULL;
132 }
133
134 static void
135 psppire_missing_val_dialog_finalize (GObject *obj)
136 {
137   PsppireMissingValDialog *dialog = PSPPIRE_MISSING_VAL_DIALOG (obj);
138
139   mv_destroy (&dialog->mvl);
140   g_free (dialog->encoding);
141
142   G_OBJECT_CLASS (psppire_missing_val_dialog_parent_class)->finalize (obj);
143 }
144
145 PsppireMissingValDialog *
146 psppire_missing_val_dialog_new (const struct variable *var)
147 {
148   return PSPPIRE_MISSING_VAL_DIALOG (
149     g_object_new (PSPPIRE_TYPE_MISSING_VAL_DIALOG,
150                   "variable", var,
151                   NULL));
152 }
153
154 void
155 psppire_missing_val_dialog_run (GtkWindow *parent_window,
156                                 const struct variable *var,
157                                 struct missing_values *mv)
158 {
159   PsppireMissingValDialog *dialog;
160
161   dialog = psppire_missing_val_dialog_new (var);
162   gtk_window_set_transient_for (GTK_WINDOW (dialog), parent_window);
163   gtk_window_set_modal (GTK_WINDOW (dialog), TRUE);
164   gtk_widget_show (GTK_WIDGET (dialog));
165
166   if (psppire_dialog_run (PSPPIRE_DIALOG (dialog)) == GTK_RESPONSE_OK)
167     mv_copy (mv, psppire_missing_val_dialog_get_missing_values (dialog));
168   else
169     mv_copy (mv, var_get_missing_values (var));
170
171   gtk_widget_destroy (GTK_WIDGET (dialog));
172 }
173
174
175 /* A simple (sub) dialog box for displaying user input errors */
176 static void
177 err_dialog (const gchar *msg, GtkWindow *window)
178 {
179   GtkWidget *dialog =
180     gtk_message_dialog_new (window,
181                             GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT,
182                             GTK_MESSAGE_ERROR,
183                             GTK_BUTTONS_CLOSE,
184                             "%s",msg);
185
186   gtk_dialog_run (GTK_DIALOG (dialog));
187   gtk_widget_destroy (dialog);
188 }
189
190 /* Interpret text, display error dialog
191    If parsing is o.k., the value is initialized and it is the responsibility of
192    the caller to destroy the variable. */
193 static gboolean
194 try_missing_value(const PsppireMissingValDialog *dialog, const gchar *text, union value *vp)
195 {
196   const int var_width = fmt_var_width (&dialog->format);
197   char *error_txt = NULL;
198
199   value_init(vp, var_width);
200   error_txt = data_in (ss_cstr(text), "UTF-8", dialog->format.type,
201                        vp, var_width, dialog->encoding);
202   if (error_txt)
203     {
204       err_dialog (error_txt, GTK_WINDOW (dialog));
205       free (error_txt);
206       goto error;
207     }
208   else
209     {
210       if (mv_is_acceptable (vp, var_width))
211         return TRUE;
212       else
213         {
214           err_dialog (_("The maximum length of a missing value"
215                         " for a string variable is 8 in UTF-8."),
216                       GTK_WINDOW (dialog));
217           goto error;
218         }
219     }
220  error:
221   value_destroy (vp, var_width);
222   return FALSE;
223 }
224
225 /* Acceptability predicate for PsppireMissingValDialog.
226
227    This function is also the only place that dialog->mvl gets updated. */
228 static gboolean
229 missing_val_dialog_acceptable (gpointer data)
230 {
231   PsppireMissingValDialog *dialog = data;
232   int var_width = fmt_var_width (&dialog->format);
233
234   if ( gtk_toggle_button_get_active (dialog->button_discrete))
235     {
236       gint nvals = 0;
237       gint i;
238
239       mv_clear(&dialog->mvl);
240       for(i = 0 ; i < 3 ; ++i )
241         {
242           gchar *text =
243             g_strdup (gtk_entry_get_text (GTK_ENTRY (dialog->mv[i])));
244
245           union value v;
246           if ( !text || strlen (g_strstrip (text)) == 0 )
247             {
248               g_free (text);
249               continue;
250             }
251
252           if (!try_missing_value (dialog, text, &v))
253             {
254               g_free (text);
255               gtk_widget_grab_focus (dialog->mv[i]);
256               return FALSE;
257             }
258           mv_add_value (&dialog->mvl, &v);
259           nvals++;
260           g_free (text);
261           value_destroy (&v, var_width);
262         }
263       if ( nvals == 0 )
264         {
265           err_dialog (_("At least one value must be specified"),
266                       GTK_WINDOW (dialog));
267           gtk_widget_grab_focus (dialog->mv[0]);
268           return FALSE;
269         }
270     }
271
272   if (gtk_toggle_button_get_active (dialog->button_range))
273     {
274       gchar *discrete_text;
275       union value low_val ;
276       union value high_val;
277       const gchar *low_text = gtk_entry_get_text (GTK_ENTRY (dialog->low));
278       const gchar *high_text = gtk_entry_get_text (GTK_ENTRY (dialog->high));
279
280       assert (var_width == 0); /* Ranges are only for numeric variables */
281
282       if (!try_missing_value(dialog, low_text, &low_val))
283         {
284           gtk_widget_grab_focus (dialog->low);
285           return FALSE;
286         }
287       if (!try_missing_value (dialog, high_text, &high_val))
288         {
289           gtk_widget_grab_focus (dialog->high);
290           value_destroy (&low_val, var_width);
291           return FALSE;
292         }
293       if (low_val.f > high_val.f)
294         {
295           err_dialog (_("Incorrect range specification"),
296                       GTK_WINDOW (dialog));
297           value_destroy (&low_val, var_width);
298           value_destroy (&high_val, var_width);
299           gtk_widget_grab_focus (dialog->low);
300           return FALSE;
301         }
302       mv_clear (&dialog->mvl);
303       mv_add_range (&dialog->mvl, low_val.f, high_val.f);
304       value_destroy (&low_val, var_width);
305       value_destroy (&high_val, var_width);
306
307       discrete_text = g_strdup (gtk_entry_get_text (GTK_ENTRY (dialog->discrete)));
308
309       if ( discrete_text && strlen (g_strstrip (discrete_text)) > 0 )
310         {
311           union value discrete_val;
312           if (!try_missing_value (dialog, discrete_text, &discrete_val))
313             {
314               g_free (discrete_text);
315               gtk_widget_grab_focus (dialog->discrete);
316               return FALSE;
317             }
318           mv_add_value (&dialog->mvl, &discrete_val);
319           value_destroy (&discrete_val, var_width);
320         }
321       g_free (discrete_text);
322     }
323
324   if (gtk_toggle_button_get_active (dialog->button_none))
325     mv_clear (&dialog->mvl);
326
327   return TRUE;
328 }
329
330
331 /* Callback which occurs when the 'discrete' radiobutton is toggled */
332 static void
333 discrete (GtkToggleButton *button, gpointer data)
334 {
335   gint i;
336   PsppireMissingValDialog *dialog = data;
337
338   for (i = 0 ; i < 3 ; ++i )
339     {
340       gtk_widget_set_sensitive (dialog->mv[i],
341                                gtk_toggle_button_get_active (button));
342     }
343 }
344
345 /* Callback which occurs when the 'range' radiobutton is toggled */
346 static void
347 range (GtkToggleButton *button, gpointer data)
348 {
349   PsppireMissingValDialog *dialog = data;
350
351   const gboolean active = gtk_toggle_button_get_active (button);
352
353   gtk_widget_set_sensitive (dialog->low, active);
354   gtk_widget_set_sensitive (dialog->high, active);
355   gtk_widget_set_sensitive (dialog->discrete, active);
356 }
357
358
359
360 /* Shows the dialog box and sets default values */
361 static GObject *
362 psppire_missing_val_dialog_constructor (GType                  type,
363                                         guint                  n_properties,
364                                         GObjectConstructParam *properties)
365 {
366   PsppireMissingValDialog *dialog;
367   GtkContainer *content_area;
368   GtkBuilder *xml;
369   GObject *obj;
370
371   obj = G_OBJECT_CLASS (psppire_missing_val_dialog_parent_class)->constructor (
372     type, n_properties, properties);
373   dialog = PSPPIRE_MISSING_VAL_DIALOG (obj);
374
375   content_area = GTK_CONTAINER (PSPPIRE_DIALOG (dialog));
376   xml = builder_new ("missing-val-dialog.ui");
377   gtk_container_add (GTK_CONTAINER (content_area),
378                      get_widget_assert (xml, "missing-values-dialog"));
379
380   dialog->mv[0] = get_widget_assert (xml, "mv0");
381   dialog->mv[1] = get_widget_assert (xml, "mv1");
382   dialog->mv[2] = get_widget_assert (xml, "mv2");
383
384   dialog->low = get_widget_assert (xml, "mv-low");
385   dialog->high = get_widget_assert (xml, "mv-high");
386   dialog->discrete = get_widget_assert (xml, "mv-discrete");
387
388
389   dialog->button_none     =
390     GTK_TOGGLE_BUTTON (get_widget_assert (xml, "no_missing"));
391
392   dialog->button_discrete =
393     GTK_TOGGLE_BUTTON (get_widget_assert (xml, "discrete_missing"));
394
395   dialog->button_range    =
396     GTK_TOGGLE_BUTTON (get_widget_assert (xml, "range_missing"));
397
398   psppire_dialog_set_accept_predicate (PSPPIRE_DIALOG (dialog),
399                                        missing_val_dialog_acceptable,
400                                        dialog);
401
402   g_signal_connect (dialog->button_discrete, "toggled",
403                    G_CALLBACK (discrete), dialog);
404
405   g_signal_connect (dialog->button_range, "toggled",
406                    G_CALLBACK (range), dialog);
407
408   g_object_unref (xml);
409
410   return obj;
411 }
412
413 void
414 psppire_missing_val_dialog_set_variable (PsppireMissingValDialog *dialog,
415                                          const struct variable *var)
416 {
417   enum val_type var_type;
418   gint i;
419
420   mv_destroy (&dialog->mvl);
421   g_free (dialog->encoding);
422
423   if (var != NULL)
424     {
425       const struct missing_values *vmv = var_get_missing_values (var);
426       if (mv_is_empty(vmv))
427         mv_init (&dialog->mvl, var_get_width(var));
428       else
429         mv_copy (&dialog->mvl, vmv);
430       dialog->encoding = g_strdup (var_get_encoding (var));
431       dialog->format = *var_get_print_format (var);
432     }
433   else
434     {
435       mv_init (&dialog->mvl, 0);
436       dialog->encoding = NULL;
437       dialog->format = F_8_0;
438     }
439
440   /* Blank all entry boxes and make them insensitive */
441   gtk_entry_set_text (GTK_ENTRY (dialog->low), "");
442   gtk_entry_set_text (GTK_ENTRY (dialog->high), "");
443   gtk_entry_set_text (GTK_ENTRY (dialog->discrete), "");
444   gtk_widget_set_sensitive (dialog->low, FALSE);
445   gtk_widget_set_sensitive (dialog->high, FALSE);
446   gtk_widget_set_sensitive (dialog->discrete, FALSE);
447
448   var_type = val_type_from_width (fmt_var_width (&dialog->format));
449   gtk_widget_set_sensitive (GTK_WIDGET (dialog->button_range),
450                             var_type == VAL_NUMERIC);
451
452   if (var == NULL)
453     return;
454
455   for (i = 0 ; i < 3 ; ++i )
456     {
457       gtk_entry_set_text (GTK_ENTRY (dialog->mv[i]), "");
458       gtk_widget_set_sensitive (dialog->mv[i], FALSE);
459     }
460
461   if ( mv_has_range (&dialog->mvl))
462     {
463       union value low, high;
464       gchar *low_text;
465       gchar *high_text;
466       mv_get_range (&dialog->mvl, &low.f, &high.f);
467
468
469       low_text = value_to_text__ (low, &dialog->format, dialog->encoding);
470       high_text = value_to_text__ (high, &dialog->format, dialog->encoding);
471
472       gtk_entry_set_text (GTK_ENTRY (dialog->low), low_text);
473       gtk_entry_set_text (GTK_ENTRY (dialog->high), high_text);
474       g_free (low_text);
475       g_free (high_text);
476
477       if ( mv_has_value (&dialog->mvl))
478         {
479           gchar *text;
480           text = value_to_text__ (*mv_get_value (&dialog->mvl, 0),
481                                   &dialog->format, dialog->encoding);
482           gtk_entry_set_text (GTK_ENTRY (dialog->discrete), text);
483           g_free (text);
484         }
485
486       gtk_toggle_button_set_active (dialog->button_range, TRUE);
487       gtk_widget_set_sensitive (dialog->low, TRUE);
488       gtk_widget_set_sensitive (dialog->high, TRUE);
489       gtk_widget_set_sensitive (dialog->discrete, TRUE);
490
491     }
492   else if ( mv_has_value (&dialog->mvl))
493     {
494       const int n = mv_n_values (&dialog->mvl);
495
496       for (i = 0 ; i < 3 ; ++i )
497         {
498           if ( i < n)
499             {
500               gchar *text ;
501
502               text = value_to_text__ (*mv_get_value (&dialog->mvl, i),
503                                       &dialog->format, dialog->encoding);
504               gtk_entry_set_text (GTK_ENTRY (dialog->mv[i]), text);
505               g_free (text);
506             }
507           gtk_widget_set_sensitive (dialog->mv[i], TRUE);
508         }
509       gtk_toggle_button_set_active (dialog->button_discrete, TRUE);
510     }
511   else if ( mv_is_empty (&dialog->mvl))
512     {
513       gtk_toggle_button_set_active (dialog->button_none, TRUE);
514     }
515 }
516
517 const struct missing_values *
518 psppire_missing_val_dialog_get_missing_values (
519   const PsppireMissingValDialog *dialog)
520 {
521   return &dialog->mvl;
522 }