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