help: added help page info 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", NULL);
379
380   content_area = GTK_CONTAINER (PSPPIRE_DIALOG (dialog));
381   xml = builder_new ("missing-val-dialog.ui");
382   gtk_container_add (GTK_CONTAINER (content_area),
383                      get_widget_assert (xml, "missing-values-dialog"));
384
385   dialog->mv[0] = get_widget_assert (xml, "mv0");
386   dialog->mv[1] = get_widget_assert (xml, "mv1");
387   dialog->mv[2] = get_widget_assert (xml, "mv2");
388
389   dialog->low = get_widget_assert (xml, "mv-low");
390   dialog->high = get_widget_assert (xml, "mv-high");
391   dialog->discrete = get_widget_assert (xml, "mv-discrete");
392
393
394   dialog->button_none     =
395     GTK_TOGGLE_BUTTON (get_widget_assert (xml, "no_missing"));
396
397   dialog->button_discrete =
398     GTK_TOGGLE_BUTTON (get_widget_assert (xml, "discrete_missing"));
399
400   dialog->button_range    =
401     GTK_TOGGLE_BUTTON (get_widget_assert (xml, "range_missing"));
402
403   psppire_dialog_set_accept_predicate (PSPPIRE_DIALOG (dialog),
404                                        missing_val_dialog_acceptable,
405                                        dialog);
406
407   g_signal_connect (dialog->button_discrete, "toggled",
408                    G_CALLBACK (discrete), dialog);
409
410   g_signal_connect (dialog->button_range, "toggled",
411                    G_CALLBACK (range), dialog);
412
413   g_object_unref (xml);
414
415   return obj;
416 }
417
418 void
419 psppire_missing_val_dialog_set_variable (PsppireMissingValDialog *dialog,
420                                          const struct variable *var)
421 {
422   enum val_type var_type;
423   gint i;
424
425   mv_destroy (&dialog->mvl);
426   g_free (dialog->encoding);
427
428   if (var != NULL)
429     {
430       const struct missing_values *vmv = var_get_missing_values (var);
431       if (mv_is_empty(vmv))
432         mv_init (&dialog->mvl, var_get_width(var));
433       else
434         mv_copy (&dialog->mvl, vmv);
435       dialog->encoding = g_strdup (var_get_encoding (var));
436       dialog->format = *var_get_print_format (var);
437     }
438   else
439     {
440       mv_init (&dialog->mvl, 0);
441       dialog->encoding = NULL;
442       dialog->format = F_8_0;
443     }
444
445   /* Blank all entry boxes and make them insensitive */
446   gtk_entry_set_text (GTK_ENTRY (dialog->low), "");
447   gtk_entry_set_text (GTK_ENTRY (dialog->high), "");
448   gtk_entry_set_text (GTK_ENTRY (dialog->discrete), "");
449   gtk_widget_set_sensitive (dialog->low, FALSE);
450   gtk_widget_set_sensitive (dialog->high, FALSE);
451   gtk_widget_set_sensitive (dialog->discrete, FALSE);
452
453   var_type = val_type_from_width (fmt_var_width (&dialog->format));
454   gtk_widget_set_sensitive (GTK_WIDGET (dialog->button_range),
455                             var_type == VAL_NUMERIC);
456
457   if (var == NULL)
458     return;
459
460   for (i = 0 ; i < 3 ; ++i)
461     {
462       gtk_entry_set_text (GTK_ENTRY (dialog->mv[i]), "");
463       gtk_widget_set_sensitive (dialog->mv[i], FALSE);
464     }
465
466   if (mv_has_range (&dialog->mvl))
467     {
468       union value low, high;
469       gchar *low_text;
470       gchar *high_text;
471       mv_get_range (&dialog->mvl, &low.f, &high.f);
472
473
474       low_text = value_to_text__ (low, &dialog->format, dialog->encoding);
475       high_text = value_to_text__ (high, &dialog->format, dialog->encoding);
476
477       gtk_entry_set_text (GTK_ENTRY (dialog->low), low_text);
478       gtk_entry_set_text (GTK_ENTRY (dialog->high), high_text);
479       g_free (low_text);
480       g_free (high_text);
481
482       if (mv_has_value (&dialog->mvl))
483         {
484           gchar *text;
485           text = value_to_text__ (*mv_get_value (&dialog->mvl, 0),
486                                   &dialog->format, dialog->encoding);
487           gtk_entry_set_text (GTK_ENTRY (dialog->discrete), text);
488           g_free (text);
489         }
490
491       gtk_toggle_button_set_active (dialog->button_range, TRUE);
492       gtk_widget_set_sensitive (dialog->low, TRUE);
493       gtk_widget_set_sensitive (dialog->high, TRUE);
494       gtk_widget_set_sensitive (dialog->discrete, TRUE);
495
496     }
497   else if (mv_has_value (&dialog->mvl))
498     {
499       const int n = mv_n_values (&dialog->mvl);
500
501       for (i = 0 ; i < 3 ; ++i)
502         {
503           if (i < n)
504             {
505               gchar *text ;
506
507               text = value_to_text__ (*mv_get_value (&dialog->mvl, i),
508                                       &dialog->format, dialog->encoding);
509               gtk_entry_set_text (GTK_ENTRY (dialog->mv[i]), text);
510               g_free (text);
511             }
512           gtk_widget_set_sensitive (dialog->mv[i], TRUE);
513         }
514       gtk_toggle_button_set_active (dialog->button_discrete, TRUE);
515     }
516   else if (mv_is_empty (&dialog->mvl))
517     {
518       gtk_toggle_button_set_active (dialog->button_none, TRUE);
519     }
520 }
521
522 const struct missing_values *
523 psppire_missing_val_dialog_get_missing_values (
524   const PsppireMissingValDialog *dialog)
525 {
526   return &dialog->mvl;
527 }