Merge remote-tracking branch 'origin/master' into 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 gint
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   gint result = psppire_dialog_run (PSPPIRE_DIALOG (dialog));
167   if (result == GTK_RESPONSE_OK)
168     mv_copy (mv, psppire_missing_val_dialog_get_missing_values (dialog));
169   else
170     mv_copy (mv, var_get_missing_values (var));
171
172   gtk_widget_destroy (GTK_WIDGET (dialog));
173   return result;
174 }
175
176
177 /* A simple (sub) dialog box for displaying user input errors */
178 static void
179 err_dialog (const gchar *msg, GtkWindow *window)
180 {
181   GtkWidget *dialog =
182     gtk_message_dialog_new (window,
183                             GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT,
184                             GTK_MESSAGE_ERROR,
185                             GTK_BUTTONS_CLOSE,
186                             "%s",msg);
187
188   gtk_dialog_run (GTK_DIALOG (dialog));
189   gtk_widget_destroy (dialog);
190 }
191
192 /* Interpret text, display error dialog
193    If parsing is o.k., the value is initialized and it is the responsibility of
194    the caller to destroy the variable. */
195 static gboolean
196 try_missing_value(const PsppireMissingValDialog *dialog, const gchar *text, union value *vp)
197 {
198   const int var_width = fmt_var_width (&dialog->format);
199   char *error_txt = NULL;
200
201   value_init(vp, var_width);
202   error_txt = data_in (ss_cstr(text), "UTF-8", dialog->format.type,
203                        vp, var_width, dialog->encoding);
204   if (error_txt)
205     {
206       err_dialog (error_txt, GTK_WINDOW (dialog));
207       free (error_txt);
208       goto error;
209     }
210   else
211     {
212       if (mv_is_acceptable (vp, var_width))
213         return TRUE;
214       else
215         {
216           err_dialog (_("The maximum length of a missing value"
217                         " for a string variable is 8 in UTF-8."),
218                       GTK_WINDOW (dialog));
219           goto error;
220         }
221     }
222  error:
223   value_destroy (vp, var_width);
224   return FALSE;
225 }
226
227 /* Acceptability predicate for PsppireMissingValDialog.
228
229    This function is also the only place that dialog->mvl gets updated. */
230 static gboolean
231 missing_val_dialog_acceptable (gpointer data)
232 {
233   PsppireMissingValDialog *dialog = data;
234   int var_width = fmt_var_width (&dialog->format);
235
236   if ( gtk_toggle_button_get_active (dialog->button_discrete))
237     {
238       gint nvals = 0;
239       gint i;
240
241       mv_clear(&dialog->mvl);
242       for(i = 0 ; i < 3 ; ++i )
243         {
244           gchar *text =
245             g_strdup (gtk_entry_get_text (GTK_ENTRY (dialog->mv[i])));
246
247           union value v;
248           if ( !text || strlen (g_strstrip (text)) == 0 )
249             {
250               g_free (text);
251               continue;
252             }
253
254           if (!try_missing_value (dialog, text, &v))
255             {
256               g_free (text);
257               gtk_widget_grab_focus (dialog->mv[i]);
258               return FALSE;
259             }
260           mv_add_value (&dialog->mvl, &v);
261           nvals++;
262           g_free (text);
263           value_destroy (&v, var_width);
264         }
265       if ( nvals == 0 )
266         {
267           err_dialog (_("At least one value must be specified"),
268                       GTK_WINDOW (dialog));
269           gtk_widget_grab_focus (dialog->mv[0]);
270           return FALSE;
271         }
272     }
273
274   if (gtk_toggle_button_get_active (dialog->button_range))
275     {
276       gchar *discrete_text;
277       union value low_val ;
278       union value high_val;
279       const gchar *low_text = gtk_entry_get_text (GTK_ENTRY (dialog->low));
280       const gchar *high_text = gtk_entry_get_text (GTK_ENTRY (dialog->high));
281
282       assert (var_width == 0); /* Ranges are only for numeric variables */
283
284       if (!try_missing_value(dialog, low_text, &low_val))
285         {
286           gtk_widget_grab_focus (dialog->low);
287           return FALSE;
288         }
289       if (!try_missing_value (dialog, high_text, &high_val))
290         {
291           gtk_widget_grab_focus (dialog->high);
292           value_destroy (&low_val, var_width);
293           return FALSE;
294         }
295       if (low_val.f > high_val.f)
296         {
297           err_dialog (_("Incorrect range specification"),
298                       GTK_WINDOW (dialog));
299           value_destroy (&low_val, var_width);
300           value_destroy (&high_val, var_width);
301           gtk_widget_grab_focus (dialog->low);
302           return FALSE;
303         }
304       mv_clear (&dialog->mvl);
305       mv_add_range (&dialog->mvl, low_val.f, high_val.f);
306       value_destroy (&low_val, var_width);
307       value_destroy (&high_val, var_width);
308
309       discrete_text = g_strdup (gtk_entry_get_text (GTK_ENTRY (dialog->discrete)));
310
311       if ( discrete_text && strlen (g_strstrip (discrete_text)) > 0 )
312         {
313           union value discrete_val;
314           if (!try_missing_value (dialog, discrete_text, &discrete_val))
315             {
316               g_free (discrete_text);
317               gtk_widget_grab_focus (dialog->discrete);
318               return FALSE;
319             }
320           mv_add_value (&dialog->mvl, &discrete_val);
321           value_destroy (&discrete_val, var_width);
322         }
323       g_free (discrete_text);
324     }
325
326   if (gtk_toggle_button_get_active (dialog->button_none))
327     mv_clear (&dialog->mvl);
328
329   return TRUE;
330 }
331
332
333 /* Callback which occurs when the 'discrete' radiobutton is toggled */
334 static void
335 discrete (GtkToggleButton *button, gpointer data)
336 {
337   gint i;
338   PsppireMissingValDialog *dialog = data;
339
340   for (i = 0 ; i < 3 ; ++i )
341     {
342       gtk_widget_set_sensitive (dialog->mv[i],
343                                gtk_toggle_button_get_active (button));
344     }
345 }
346
347 /* Callback which occurs when the 'range' radiobutton is toggled */
348 static void
349 range (GtkToggleButton *button, gpointer data)
350 {
351   PsppireMissingValDialog *dialog = data;
352
353   const gboolean active = gtk_toggle_button_get_active (button);
354
355   gtk_widget_set_sensitive (dialog->low, active);
356   gtk_widget_set_sensitive (dialog->high, active);
357   gtk_widget_set_sensitive (dialog->discrete, active);
358 }
359
360
361
362 /* Shows the dialog box and sets default values */
363 static GObject *
364 psppire_missing_val_dialog_constructor (GType                  type,
365                                         guint                  n_properties,
366                                         GObjectConstructParam *properties)
367 {
368   PsppireMissingValDialog *dialog;
369   GtkContainer *content_area;
370   GtkBuilder *xml;
371   GObject *obj;
372
373   obj = G_OBJECT_CLASS (psppire_missing_val_dialog_parent_class)->constructor (
374     type, n_properties, properties);
375   dialog = PSPPIRE_MISSING_VAL_DIALOG (obj);
376
377   content_area = GTK_CONTAINER (PSPPIRE_DIALOG (dialog));
378   xml = builder_new ("missing-val-dialog.ui");
379   gtk_container_add (GTK_CONTAINER (content_area),
380                      get_widget_assert (xml, "missing-values-dialog"));
381
382   dialog->mv[0] = get_widget_assert (xml, "mv0");
383   dialog->mv[1] = get_widget_assert (xml, "mv1");
384   dialog->mv[2] = get_widget_assert (xml, "mv2");
385
386   dialog->low = get_widget_assert (xml, "mv-low");
387   dialog->high = get_widget_assert (xml, "mv-high");
388   dialog->discrete = get_widget_assert (xml, "mv-discrete");
389
390
391   dialog->button_none     =
392     GTK_TOGGLE_BUTTON (get_widget_assert (xml, "no_missing"));
393
394   dialog->button_discrete =
395     GTK_TOGGLE_BUTTON (get_widget_assert (xml, "discrete_missing"));
396
397   dialog->button_range    =
398     GTK_TOGGLE_BUTTON (get_widget_assert (xml, "range_missing"));
399
400   psppire_dialog_set_accept_predicate (PSPPIRE_DIALOG (dialog),
401                                        missing_val_dialog_acceptable,
402                                        dialog);
403
404   g_signal_connect (dialog->button_discrete, "toggled",
405                    G_CALLBACK (discrete), dialog);
406
407   g_signal_connect (dialog->button_range, "toggled",
408                    G_CALLBACK (range), dialog);
409
410   g_object_unref (xml);
411
412   return obj;
413 }
414
415 void
416 psppire_missing_val_dialog_set_variable (PsppireMissingValDialog *dialog,
417                                          const struct variable *var)
418 {
419   enum val_type var_type;
420   gint i;
421
422   mv_destroy (&dialog->mvl);
423   g_free (dialog->encoding);
424
425   if (var != NULL)
426     {
427       const struct missing_values *vmv = var_get_missing_values (var);
428       if (mv_is_empty(vmv))
429         mv_init (&dialog->mvl, var_get_width(var));
430       else
431         mv_copy (&dialog->mvl, vmv);
432       dialog->encoding = g_strdup (var_get_encoding (var));
433       dialog->format = *var_get_print_format (var);
434     }
435   else
436     {
437       mv_init (&dialog->mvl, 0);
438       dialog->encoding = NULL;
439       dialog->format = F_8_0;
440     }
441
442   /* Blank all entry boxes and make them insensitive */
443   gtk_entry_set_text (GTK_ENTRY (dialog->low), "");
444   gtk_entry_set_text (GTK_ENTRY (dialog->high), "");
445   gtk_entry_set_text (GTK_ENTRY (dialog->discrete), "");
446   gtk_widget_set_sensitive (dialog->low, FALSE);
447   gtk_widget_set_sensitive (dialog->high, FALSE);
448   gtk_widget_set_sensitive (dialog->discrete, FALSE);
449
450   var_type = val_type_from_width (fmt_var_width (&dialog->format));
451   gtk_widget_set_sensitive (GTK_WIDGET (dialog->button_range),
452                             var_type == VAL_NUMERIC);
453
454   if (var == NULL)
455     return;
456
457   for (i = 0 ; i < 3 ; ++i )
458     {
459       gtk_entry_set_text (GTK_ENTRY (dialog->mv[i]), "");
460       gtk_widget_set_sensitive (dialog->mv[i], FALSE);
461     }
462
463   if ( mv_has_range (&dialog->mvl))
464     {
465       union value low, high;
466       gchar *low_text;
467       gchar *high_text;
468       mv_get_range (&dialog->mvl, &low.f, &high.f);
469
470
471       low_text = value_to_text__ (low, &dialog->format, dialog->encoding);
472       high_text = value_to_text__ (high, &dialog->format, dialog->encoding);
473
474       gtk_entry_set_text (GTK_ENTRY (dialog->low), low_text);
475       gtk_entry_set_text (GTK_ENTRY (dialog->high), high_text);
476       g_free (low_text);
477       g_free (high_text);
478
479       if ( mv_has_value (&dialog->mvl))
480         {
481           gchar *text;
482           text = value_to_text__ (*mv_get_value (&dialog->mvl, 0),
483                                   &dialog->format, dialog->encoding);
484           gtk_entry_set_text (GTK_ENTRY (dialog->discrete), text);
485           g_free (text);
486         }
487
488       gtk_toggle_button_set_active (dialog->button_range, TRUE);
489       gtk_widget_set_sensitive (dialog->low, TRUE);
490       gtk_widget_set_sensitive (dialog->high, TRUE);
491       gtk_widget_set_sensitive (dialog->discrete, TRUE);
492
493     }
494   else if ( mv_has_value (&dialog->mvl))
495     {
496       const int n = mv_n_values (&dialog->mvl);
497
498       for (i = 0 ; i < 3 ; ++i )
499         {
500           if ( i < n)
501             {
502               gchar *text ;
503
504               text = value_to_text__ (*mv_get_value (&dialog->mvl, i),
505                                       &dialog->format, dialog->encoding);
506               gtk_entry_set_text (GTK_ENTRY (dialog->mv[i]), text);
507               g_free (text);
508             }
509           gtk_widget_set_sensitive (dialog->mv[i], TRUE);
510         }
511       gtk_toggle_button_set_active (dialog->button_discrete, TRUE);
512     }
513   else if ( mv_is_empty (&dialog->mvl))
514     {
515       gtk_toggle_button_set_active (dialog->button_none, TRUE);
516     }
517 }
518
519 const struct missing_values *
520 psppire_missing_val_dialog_get_missing_values (
521   const PsppireMissingValDialog *dialog)
522 {
523   return &dialog->mvl;
524 }