treewide: Use struct fmt_spec by value instead of pointer in most cases.
[pspp] / src / ui / gui / psppire-variable-sheet.c
1 /* PSPPIRE - a graphical user interface for PSPP.
2    Copyright (C) 2017, 2020  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
18 #include <config.h>
19 #include "psppire-variable-sheet.h"
20
21 #include "ui/gui/psppire-var-sheet-header.h"
22
23 #include "psppire-dict.h"
24 #include "var-type-dialog.h"
25 #include "missing-val-dialog.h"
26 #include "val-labs-dialog.h"
27 #include "var-display.h"
28 #include "data/format.h"
29 #include "data/value-labels.h"
30 #include "helper.h"
31
32 #include <gettext.h>
33 #define _(msgid) gettext (msgid)
34 #define P_(X) (X)
35
36
37 G_DEFINE_TYPE (PsppireVariableSheet, psppire_variable_sheet, SSW_TYPE_SHEET)
38
39 static void
40 set_var_type (PsppireVariableSheet *sheet)
41 {
42   gint row = -1, col = -1;
43   ssw_sheet_get_active_cell (SSW_SHEET (sheet), &col, &row);
44
45   PsppireDict *dict = NULL;
46   g_object_get (sheet, "data-model", &dict, NULL);
47
48   struct variable *var =
49     psppire_dict_get_variable (PSPPIRE_DICT (dict), row);
50
51   if (var == NULL)
52     return;
53
54   struct fmt_spec fmt = var_get_write_format (var);
55   GtkWindow *win = GTK_WINDOW (gtk_widget_get_toplevel (GTK_WIDGET (sheet)));
56   if (GTK_RESPONSE_OK == psppire_var_type_dialog_run (win, &fmt))
57     var_set_width_and_formats (var, fmt_var_width (fmt), &fmt, &fmt);
58 }
59
60 static void
61 set_missing_values (PsppireVariableSheet *sheet)
62 {
63   gint row = -1, col = -1;
64   ssw_sheet_get_active_cell (SSW_SHEET (sheet), &col, &row);
65
66   PsppireDict *dict = NULL;
67   g_object_get (sheet, "data-model", &dict, NULL);
68
69   struct variable *var =
70     psppire_dict_get_variable (PSPPIRE_DICT (dict), row);
71
72   if (var == NULL)
73     return;
74
75   struct missing_values mv;
76   if (GTK_RESPONSE_OK ==
77       psppire_missing_val_dialog_run (GTK_WINDOW (gtk_widget_get_toplevel (GTK_WIDGET (sheet))),
78                                       var, &mv))
79     {
80       var_set_missing_values (var, &mv);
81     }
82
83   mv_destroy (&mv);
84 }
85
86 static void
87 set_value_labels (PsppireVariableSheet *sheet)
88 {
89   gint row = -1, col = -1;
90   ssw_sheet_get_active_cell (SSW_SHEET (sheet), &col, &row);
91
92   PsppireDict *dict = NULL;
93   g_object_get (sheet, "data-model", &dict, NULL);
94
95   struct variable *var =
96     psppire_dict_get_variable (PSPPIRE_DICT (dict), row);
97
98   if (var == NULL)
99     return;
100
101   struct val_labs *vls =
102     psppire_val_labs_dialog_run (GTK_WINDOW (gtk_widget_get_toplevel (GTK_WIDGET (sheet))), var);
103
104   if (vls)
105     {
106       var_set_value_labels (var, vls);
107       val_labs_destroy (vls);
108     }
109 }
110
111 static GtkCellRenderer *
112 create_spin_renderer (GType type)
113 {
114   GtkCellRenderer *r = gtk_cell_renderer_spin_new ();
115
116   GtkAdjustment *adj = gtk_adjustment_new (0,
117                                            0, G_MAXDOUBLE,
118                                            1, 1,
119                                            0);
120   g_object_set (r,
121                 "adjustment", adj,
122                 NULL);
123
124   return r;
125 }
126
127 static GtkCellRenderer *
128 create_combo_renderer (GType type)
129 {
130   GtkListStore *list_store = gtk_list_store_new (2, G_TYPE_INT, G_TYPE_STRING);
131
132   GEnumClass *ec = g_type_class_ref (type);
133
134   const GEnumValue *ev ;
135   for (ev = ec->values; ev->value_name; ++ev)
136     {
137       GtkTreeIter iter;
138
139       gtk_list_store_append (list_store, &iter);
140
141       gtk_list_store_set (list_store, &iter,
142                           0, ev->value,
143                           1, gettext (ev->value_nick),
144                           -1);
145     }
146
147   GtkCellRenderer *r = gtk_cell_renderer_combo_new ();
148
149   g_object_set (r,
150                 "model", list_store,
151                 "text-column", 1,
152                 "has-entry", TRUE,
153                 NULL);
154
155   return r;
156 }
157
158 static GtkCellRenderer *spin_renderer;
159 static GtkCellRenderer *column_width_renderer;
160 static GtkCellRenderer *measure_renderer;
161 static GtkCellRenderer *alignment_renderer;
162
163 static GtkCellRenderer *
164 select_renderer_func (PsppireVariableSheet *sheet, gint col, gint row, GType type, gpointer ud)
165 {
166   if (!spin_renderer)
167     spin_renderer = create_spin_renderer (type);
168
169   if (col == DICT_TVM_COL_ROLE && !column_width_renderer)
170     column_width_renderer = create_combo_renderer (type);
171
172   if (col == DICT_TVM_COL_MEASURE && !measure_renderer)
173     measure_renderer = create_combo_renderer (type);
174
175   if (col == DICT_TVM_COL_ALIGNMENT && !alignment_renderer)
176     alignment_renderer = create_combo_renderer (type);
177
178   switch  (col)
179     {
180     case DICT_TVM_COL_WIDTH:
181     case DICT_TVM_COL_DECIMAL:
182     case DICT_TVM_COL_COLUMNS:
183       return spin_renderer;
184
185     case DICT_TVM_COL_TYPE:
186       return sheet->var_type_renderer;
187
188     case DICT_TVM_COL_VALUE_LABELS:
189       return sheet->value_label_renderer;
190
191     case DICT_TVM_COL_MISSING_VALUES:
192       return sheet->missing_values_renderer;
193
194     case DICT_TVM_COL_ALIGNMENT:
195       return alignment_renderer;
196
197     case DICT_TVM_COL_MEASURE:
198       return measure_renderer;
199
200     case DICT_TVM_COL_ROLE:
201       return column_width_renderer;
202     }
203
204   return NULL;
205 }
206
207 \f
208
209 static void
210 show_variables_row_popup (SswSheet *sheet, int row, guint button,
211                           guint state, gpointer p)
212 {
213   PsppireVariableSheet *var_sheet = PSPPIRE_VARIABLE_SHEET (sheet);
214   GListModel *vmodel = NULL;
215   g_object_get (sheet, "vmodel", &vmodel, NULL);
216   if (vmodel == NULL)
217     return;
218
219   guint n_items = g_list_model_get_n_items (vmodel);
220
221   if (row >= n_items)
222     return;
223
224   if (button != 3)
225     return;
226
227   g_object_set_data (G_OBJECT (var_sheet->row_popup), "item",
228                      GINT_TO_POINTER (row));
229
230   gtk_menu_popup_at_pointer (GTK_MENU (var_sheet->row_popup), NULL);
231 }
232
233 static void
234 insert_new_variable_var (PsppireVariableSheet *var_sheet)
235 {
236   gint item = GPOINTER_TO_INT (g_object_get_data
237                                 (G_OBJECT (var_sheet->row_popup),
238                                  "item"));
239
240   PsppireDict *dict = NULL;
241   g_object_get (var_sheet, "data-model", &dict, NULL);
242
243   psppire_dict_insert_variable (dict, item, NULL);
244
245   gtk_widget_queue_draw (GTK_WIDGET (var_sheet));
246 }
247
248
249 static void
250 delete_variables (SswSheet *sheet)
251 {
252   SswRange *range = sheet->selection;
253
254   PsppireDict *dict = NULL;
255   g_object_get (sheet, "data-model", &dict, NULL);
256
257   if (range->start_x > range->end_x)
258     {
259       gint temp = range->start_x;
260       range->start_x = range->end_x;
261       range->end_x = temp;
262     }
263
264   psppire_dict_delete_variables (dict, range->start_y,
265                                  (range->end_y - range->start_y + 1));
266
267   gtk_widget_queue_draw (GTK_WIDGET (sheet));
268 }
269
270 static GtkWidget *
271 create_var_row_header_popup_menu (PsppireVariableSheet *var_sheet)
272 {
273   GtkWidget *menu = gtk_menu_new ();
274
275   /* gtk_menu_shell_append does not sink/ref this object,
276      so we must do it ourselves (and remember to unref it).  */
277   g_object_ref_sink (menu);
278
279   GtkWidget *item =
280     gtk_menu_item_new_with_mnemonic  (_("_Insert Variable"));
281   g_signal_connect_swapped (item, "activate", G_CALLBACK (insert_new_variable_var),
282                             var_sheet);
283   gtk_menu_shell_append (GTK_MENU_SHELL (menu), item);
284
285   item = gtk_separator_menu_item_new ();
286   gtk_menu_shell_append (GTK_MENU_SHELL (menu), item);
287
288   var_sheet->clear_variables_menu_item =
289     gtk_menu_item_new_with_mnemonic (_("Cl_ear Variables"));
290
291   g_signal_connect_swapped (var_sheet->clear_variables_menu_item, "activate",
292                             G_CALLBACK (delete_variables), var_sheet);
293
294   gtk_widget_set_sensitive (var_sheet->clear_variables_menu_item, FALSE);
295   gtk_menu_shell_append (GTK_MENU_SHELL (menu),
296                          var_sheet->clear_variables_menu_item);
297
298   gtk_widget_show_all (menu);
299   return menu;
300 }
301
302
303 static void
304 set_var_popup_sensitivity (SswSheet *sheet, gpointer selection, gpointer p)
305 {
306   PsppireVariableSheet *var_sheet = PSPPIRE_VARIABLE_SHEET (sheet);
307   SswRange *range = selection;
308   gint width = gtk_tree_model_get_n_columns (sheet->data_model);
309
310   gboolean whole_row_selected = (range->start_x == 0 &&
311                                  range->end_x == width - 1 - 1);
312   /*  PsppireDict has an "extra" column: TVM_COL_VAR   ^^^ */
313   gtk_widget_set_sensitive (var_sheet->clear_variables_menu_item,
314                             whole_row_selected);
315 }
316
317 \f
318
319 static void
320 change_var_property (PsppireVariableSheet *var_sheet, gint col, gint row, const GValue *value)
321 {
322   PsppireDict *dict = NULL;
323   g_object_get (var_sheet, "data-model", &dict, NULL);
324
325   int n_rows = psppire_dict_get_n_vars (dict);
326   if (row > n_rows)
327     return;
328
329   /* Return the IDXth variable */
330   struct variable *var =  psppire_dict_get_variable (dict, row);
331
332   if (NULL == var)
333     var = psppire_dict_insert_variable (dict, row, NULL);
334
335   g_return_if_fail (var);
336
337   switch (col)
338     {
339     case DICT_TVM_COL_NAME:
340       {
341         const char *name = g_value_get_string (value);
342         if (psppire_dict_check_name (dict, name))
343           dict_rename_var (dict->dict, var, g_value_get_string (value));
344       }
345       break;
346     case DICT_TVM_COL_WIDTH:
347       {
348       gint width = g_value_get_int (value);
349       if (var_is_numeric (var))
350         {
351           struct fmt_spec format = var_get_print_format (var);
352           fmt_change_width (&format, width, FMT_FOR_OUTPUT);
353           var_set_both_formats (var, format);
354         }
355       else
356         {
357           var_set_width (var, width);
358         }
359       }
360       break;
361     case DICT_TVM_COL_DECIMAL:
362       {
363       gint decimals = g_value_get_int (value);
364       if (decimals >= 0)
365         {
366           struct fmt_spec format = var_get_print_format (var);
367           fmt_change_decimals (&format, decimals, FMT_FOR_OUTPUT);
368           var_set_both_formats (var, format);
369         }
370       }
371       break;
372     case DICT_TVM_COL_LABEL:
373       var_set_label (var, g_value_get_string (value));
374       break;
375     case DICT_TVM_COL_COLUMNS:
376       var_set_display_width (var, g_value_get_int (value));
377       break;
378     case DICT_TVM_COL_MEASURE:
379       var_set_measure (var, g_value_get_int (value));
380       break;
381     case DICT_TVM_COL_ALIGNMENT:
382       var_set_alignment (var, g_value_get_int (value));
383       break;
384     case DICT_TVM_COL_ROLE:
385       var_set_role (var, g_value_get_int (value));
386       break;
387     default:
388       g_warning ("Changing unknown column %d of variable sheet column not supported",
389                  col);
390       break;
391     }
392 }
393
394 static gchar *
395 var_sheet_data_to_string (SswSheet *sheet, GtkTreeModel *m,
396                           gint col, gint row, const GValue *in)
397 {
398   if (col >= n_DICT_COLS - 1) /* -1 because psppire-dict has an extra column */
399     return NULL;
400
401   const struct variable *var = psppire_dict_get_variable (PSPPIRE_DICT (m), row);
402   if (var == NULL)
403     return NULL;
404
405   if (col == DICT_TVM_COL_TYPE)
406     {
407       struct fmt_spec print = var_get_print_format (var);
408       return strdup (fmt_gui_name (print.type));
409     }
410   else if (col == DICT_TVM_COL_MISSING_VALUES)
411     return missing_values_to_string (var, NULL);
412   else if (col == DICT_TVM_COL_VALUE_LABELS)
413     {
414       const struct val_labs *vls = var_get_value_labels (var);
415       if (vls == NULL || val_labs_count (vls) == 0)
416         return strdup (_("None"));
417       const struct val_lab **labels = val_labs_sorted (vls);
418       const struct val_lab *vl = labels[0];
419       gchar *vstr = value_to_text (vl->value, var);
420       char *text = xasprintf (_("{%s, %s}..."), vstr,
421                               val_lab_get_escaped_label (vl));
422       free (vstr);
423       free (labels);
424       return text;
425     }
426
427   return ssw_sheet_default_forward_conversion (sheet, m, col, row, in);
428 }
429
430 \f
431
432 static GObjectClass * parent_class = NULL;
433
434 static void
435 psppire_variable_sheet_dispose (GObject *obj)
436 {
437   PsppireVariableSheet *sheet = PSPPIRE_VARIABLE_SHEET (obj);
438
439   if (sheet->dispose_has_run)
440     return;
441
442   sheet->dispose_has_run = TRUE;
443
444   g_object_unref (sheet->value_label_renderer);
445   g_object_unref (sheet->missing_values_renderer);
446   g_object_unref (sheet->var_type_renderer);
447   g_object_unref (sheet->row_popup);
448
449   /* Chain up to the parent class */
450   G_OBJECT_CLASS (parent_class)->dispose (obj);
451 }
452
453 static void
454 psppire_variable_sheet_finalize (GObject *object)
455 {
456   PsppireVariableSheet *sheet = PSPPIRE_VARIABLE_SHEET (object);
457
458   g_free (sheet->value_label_dispatch);
459   g_free (sheet->missing_values_dispatch);
460   g_free (sheet->var_type_dispatch);
461
462   if (G_OBJECT_CLASS (parent_class)->finalize)
463     (*G_OBJECT_CLASS (parent_class)->finalize) (object);
464 }
465
466 static void
467 psppire_variable_sheet_realize (GtkWidget *widget)
468 {
469   /* This is a kludge.  These are properties from the parent class.
470      They should really be set immediately after initialisation, but there is no
471      simple way to do that.  */
472   g_object_set (widget,
473                 "editable", TRUE,
474                 "select-renderer-func", select_renderer_func,
475                 "vertical-draggable", TRUE,
476                 "forward-conversion", var_sheet_data_to_string,
477                 NULL);
478
479   if (GTK_WIDGET_CLASS (parent_class)->realize)
480     (*GTK_WIDGET_CLASS (parent_class)->realize) (widget);
481 }
482
483
484 static void
485 psppire_variable_sheet_class_init (PsppireVariableSheetClass *class)
486 {
487   GObjectClass *object_class = G_OBJECT_CLASS (class);
488   GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (class);
489
490   object_class->dispose = psppire_variable_sheet_dispose;
491
492   parent_class = g_type_class_peek_parent (class);
493
494   widget_class->realize = psppire_variable_sheet_realize;
495   object_class->finalize = psppire_variable_sheet_finalize;
496 }
497
498 GtkWidget*
499 psppire_variable_sheet_new (void)
500 {
501   return g_object_new (PSPPIRE_TYPE_VARIABLE_SHEET, NULL);
502 }
503
504 static void
505 move_variable (PsppireVariableSheet *sheet, gint from, gint to, gpointer ud)
506 {
507   PsppireDict *dict = NULL;
508   g_object_get (sheet, "data-model", &dict, NULL);
509
510   if (dict == NULL)
511     return;
512
513   struct variable *var = psppire_dict_get_variable (dict, from);
514
515   if (var == NULL)
516     return;
517   gint new_pos = to;
518   /* The index refers to the final position, so if the source
519      is less than the destination, then we must subtract 1, to
520      account for the position vacated by the source */
521   if (from < to)
522     new_pos--;
523   dict_reorder_var (dict->dict, var, new_pos);
524 }
525
526
527 static gboolean
528 is_printable_key (gint keyval)
529 {
530   switch (keyval)
531     {
532     case GDK_KEY_Return:
533     case GDK_KEY_ISO_Left_Tab:
534     case GDK_KEY_Tab:
535       return FALSE;
536       break;
537     }
538
539   return (0 != gdk_keyval_to_unicode (keyval));
540 }
541
542 struct dispatch
543 {
544   PsppireVariableSheet *sheet;
545   void (*payload) (PsppireVariableSheet *);
546 };
547
548
549 static gboolean
550 on_key_press (GtkWidget *w, GdkEventKey *e, gpointer user_data)
551 {
552   const struct dispatch *d = user_data;
553   if (is_printable_key (e->keyval))
554     {
555       d->payload (d->sheet);
556       return TRUE;
557     }
558
559   return FALSE;
560 }
561
562 static gboolean
563 on_button_press (GtkWidget *w, GdkEventButton *e, gpointer user_data)
564 {
565   const struct dispatch *d = user_data;
566   if (e->button != 1)
567     return TRUE;
568
569   d->payload (d->sheet);
570   return TRUE;
571 }
572
573 static void
574 on_edit_start (GtkCellRenderer *renderer,
575      GtkCellEditable *editable,
576      gchar           *path,
577      gpointer         user_data)
578 {
579   gtk_widget_grab_focus (GTK_WIDGET (editable));
580   g_signal_connect (editable, "key-press-event",
581                     G_CALLBACK (on_key_press), user_data);
582   g_signal_connect (editable, "button-press-event",
583                     G_CALLBACK (on_button_press), user_data);
584
585 }
586
587 static void
588 psppire_variable_sheet_init (PsppireVariableSheet *sheet)
589 {
590   sheet->dispose_has_run = FALSE;
591
592   sheet->value_label_renderer = gtk_cell_renderer_text_new ();
593   sheet->value_label_dispatch = g_malloc (sizeof *sheet->value_label_dispatch);
594   sheet->value_label_dispatch->sheet = sheet;
595   sheet->value_label_dispatch->payload = set_value_labels;
596   g_signal_connect_after (sheet->value_label_renderer,
597                           "editing-started", G_CALLBACK (on_edit_start),
598                           sheet->value_label_dispatch);
599
600   sheet->missing_values_renderer = gtk_cell_renderer_text_new ();
601   sheet->missing_values_dispatch = g_malloc (sizeof *sheet->missing_values_dispatch);
602   sheet->missing_values_dispatch->sheet = sheet;
603   sheet->missing_values_dispatch->payload = set_missing_values;
604   g_signal_connect_after (sheet->missing_values_renderer,
605                           "editing-started", G_CALLBACK (on_edit_start),
606                           sheet->missing_values_dispatch);
607
608   sheet->var_type_renderer = gtk_cell_renderer_text_new ();
609   sheet->var_type_dispatch = g_malloc (sizeof *sheet->var_type_dispatch);
610   sheet->var_type_dispatch->sheet = sheet;
611   sheet->var_type_dispatch->payload = set_var_type;
612   g_signal_connect_after (sheet->var_type_renderer,
613                           "editing-started", G_CALLBACK (on_edit_start),
614                           sheet->var_type_dispatch);
615
616   sheet->row_popup = create_var_row_header_popup_menu (sheet);
617
618   g_signal_connect (sheet, "selection-changed",
619                     G_CALLBACK (set_var_popup_sensitivity), sheet);
620
621   g_signal_connect (sheet, "row-header-pressed",
622                     G_CALLBACK (show_variables_row_popup), sheet);
623
624   g_signal_connect_swapped (sheet, "value-changed",
625                             G_CALLBACK (change_var_property), sheet);
626
627   g_signal_connect (sheet, "row-moved",
628                     G_CALLBACK (move_variable), NULL);
629
630   PsppireVarSheetHeader *vsh =
631     g_object_new (PSPPIRE_TYPE_VAR_SHEET_HEADER, NULL);
632
633   g_object_set (sheet,
634                 "hmodel", vsh,
635                 NULL);
636 }