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