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