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