Delete trailing whitespace at line endings.
[pspp] / src / ui / gui / psppire-variable-sheet.c
1 /* PSPPIRE - a graphical user interface for PSPP.
2    Copyright (C) 2017  John Darrington
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   psppire_dict_delete_variables (dict, range->start_y,
252                                  (range->end_y - range->start_y + 1));
253
254   gtk_widget_queue_draw (GTK_WIDGET (sheet));
255 }
256
257 static GtkWidget *
258 create_var_row_header_popup_menu (PsppireVariableSheet *var_sheet)
259 {
260   GtkWidget *menu = gtk_menu_new ();
261
262   GtkWidget *item =
263     gtk_menu_item_new_with_mnemonic  (_("_Insert Variable"));
264   g_signal_connect_swapped (item, "activate", G_CALLBACK (insert_new_variable_var),
265                             var_sheet);
266   gtk_menu_shell_append (GTK_MENU_SHELL (menu), item);
267
268   item = gtk_separator_menu_item_new ();
269   gtk_menu_shell_append (GTK_MENU_SHELL (menu), item);
270
271   var_sheet->clear_variables_menu_item =
272     gtk_menu_item_new_with_mnemonic (_("Cl_ear Variables"));
273
274   g_signal_connect_swapped (var_sheet->clear_variables_menu_item, "activate",
275                             G_CALLBACK (delete_variables), var_sheet);
276
277   gtk_widget_set_sensitive (var_sheet->clear_variables_menu_item, FALSE);
278   gtk_menu_shell_append (GTK_MENU_SHELL (menu),
279                          var_sheet->clear_variables_menu_item);
280
281   gtk_widget_show_all (menu);
282   return menu;
283 }
284
285
286 static void
287 set_var_popup_sensitivity (SswSheet *sheet, gpointer selection, gpointer p)
288 {
289   PsppireVariableSheet *var_sheet = PSPPIRE_VARIABLE_SHEET (sheet);
290   SswRange *range = selection;
291   gint width = gtk_tree_model_get_n_columns (sheet->data_model);
292
293   gboolean whole_row_selected = (range->start_x == 0 &&
294                                  range->end_x == width - 1 - 1);
295   /*  PsppireDict has an "extra" column: TVM_COL_VAR   ^^^ */
296   gtk_widget_set_sensitive (var_sheet->clear_variables_menu_item,
297                             whole_row_selected);
298 }
299
300 \f
301
302 static void
303 change_var_property (PsppireVariableSheet *var_sheet, gint col, gint row, const GValue *value)
304 {
305   PsppireDict *dict = NULL;
306   g_object_get (var_sheet, "data-model", &dict, NULL);
307
308   int n_rows = psppire_dict_get_var_cnt (dict);
309   if (row > n_rows)
310     return;
311
312   /* Return the IDXth variable */
313   struct variable *var =  psppire_dict_get_variable (dict, row);
314
315   if (NULL == var)
316     var = psppire_dict_insert_variable (dict, row, NULL);
317
318   switch (col)
319     {
320     case DICT_TVM_COL_NAME:
321       {
322         const char *name = g_value_get_string (value);
323         if (psppire_dict_check_name (dict, name, FALSE))
324           dict_rename_var (dict->dict, var, g_value_get_string (value));
325       }
326       break;
327     case DICT_TVM_COL_WIDTH:
328       {
329       gint width = g_value_get_int (value);
330       if (var_is_numeric (var))
331         {
332           struct fmt_spec format = *var_get_print_format (var);
333           fmt_change_width (&format, width, FMT_FOR_OUTPUT);
334           var_set_both_formats (var, &format);
335         }
336       else
337         {
338           var_set_width (var, width);
339         }
340       }
341       break;
342     case DICT_TVM_COL_DECIMAL:
343       {
344       gint decimals = g_value_get_int (value);
345       if (decimals >= 0)
346         {
347           struct fmt_spec format = *var_get_print_format (var);
348           fmt_change_decimals (&format, decimals, FMT_FOR_OUTPUT);
349           var_set_both_formats (var, &format);
350         }
351       }
352       break;
353     case DICT_TVM_COL_LABEL:
354       var_set_label (var, g_value_get_string (value));
355       break;
356     case DICT_TVM_COL_COLUMNS:
357       var_set_display_width (var, g_value_get_int (value));
358       break;
359     case DICT_TVM_COL_MEASURE:
360       var_set_measure (var, g_value_get_int (value));
361       break;
362     case DICT_TVM_COL_ALIGNMENT:
363       var_set_alignment (var, g_value_get_int (value));
364       break;
365     case DICT_TVM_COL_ROLE:
366       var_set_role (var, g_value_get_int (value));
367       break;
368     default:
369       g_warning ("Changing unknown column %d of variable sheet column not supported",
370                  col);
371       break;
372     }
373 }
374
375 static gchar *
376 var_sheet_data_to_string (SswSheet *sheet, GtkTreeModel *m,
377                           gint col, gint row, const GValue *in)
378 {
379   if (col >= n_DICT_COLS - 1) /* -1 because psppire-dict has an extra column */
380     return NULL;
381
382   const struct variable *var = psppire_dict_get_variable (PSPPIRE_DICT (m), row);
383   if (var == NULL)
384     return NULL;
385
386   if (col == DICT_TVM_COL_TYPE)
387     {
388       const struct fmt_spec *print = var_get_print_format (var);
389       return strdup (fmt_gui_name (print->type));
390     }
391   else if (col == DICT_TVM_COL_MISSING_VALUES)
392     return missing_values_to_string (var, NULL);
393   else if (col == DICT_TVM_COL_VALUE_LABELS)
394     {
395       const struct val_labs *vls = var_get_value_labels (var);
396       if (vls == NULL || val_labs_count (vls) == 0)
397         return strdup (_("None"));
398       const struct val_lab **labels = val_labs_sorted (vls);
399       const struct val_lab *vl = labels[0];
400       gchar *vstr = value_to_text (vl->value, var);
401       char *text = xasprintf (_("{%s, %s}..."), vstr,
402                               val_lab_get_escaped_label (vl));
403       free (vstr);
404       free (labels);
405       return text;
406     }
407
408   return ssw_sheet_default_forward_conversion (sheet, m, col, row, in);
409 }
410
411 \f
412
413 static GObjectClass * parent_class = NULL;
414
415 static void
416 psppire_variable_sheet_dispose (GObject *obj)
417 {
418   PsppireVariableSheet *sheet = PSPPIRE_VARIABLE_SHEET (obj);
419
420   if (sheet->dispose_has_run)
421     return;
422
423   sheet->dispose_has_run = TRUE;
424
425   g_object_unref (sheet->value_label_renderer);
426   g_object_unref (sheet->missing_values_renderer);
427   g_object_unref (sheet->var_type_renderer);
428
429   /* Chain up to the parent class */
430   G_OBJECT_CLASS (parent_class)->dispose (obj);
431 }
432
433 static void
434 psppire_variable_sheet_finalize (GObject *object)
435 {
436   PsppireVariableSheet *sheet = PSPPIRE_VARIABLE_SHEET (object);
437
438   g_free (sheet->value_label_dispatch);
439   g_free (sheet->missing_values_dispatch);
440   g_free (sheet->var_type_dispatch);
441
442   if (G_OBJECT_CLASS (parent_class)->finalize)
443     (*G_OBJECT_CLASS (parent_class)->finalize) (object);
444 }
445
446 static void
447 psppire_variable_sheet_class_init (PsppireVariableSheetClass *class)
448 {
449   GObjectClass *object_class = G_OBJECT_CLASS (class);
450   object_class->dispose = psppire_variable_sheet_dispose;
451
452   parent_class = g_type_class_peek_parent (class);
453
454   object_class->finalize = psppire_variable_sheet_finalize;
455 }
456
457 GtkWidget*
458 psppire_variable_sheet_new (void)
459 {
460   PsppireVarSheetHeader *vsh =
461     g_object_new (PSPPIRE_TYPE_VAR_SHEET_HEADER, NULL);
462
463   GObject *obj =
464     g_object_new (PSPPIRE_TYPE_VARIABLE_SHEET,
465                   "select-renderer-func", select_renderer_func,
466                   "hmodel", vsh,
467                   "forward-conversion", var_sheet_data_to_string,
468                   "editable", TRUE,
469                   "vertical-draggable", TRUE,
470                   NULL);
471
472   return GTK_WIDGET (obj);
473 }
474
475 static void
476 move_variable (PsppireVariableSheet *sheet, gint from, gint to, gpointer ud)
477 {
478   PsppireDict *dict = NULL;
479   g_object_get (sheet, "data-model", &dict, NULL);
480
481   if (dict == NULL)
482     return;
483
484   struct variable *var = psppire_dict_get_variable (dict, from);
485
486   if (var == NULL)
487     return;
488   gint new_pos = to;
489   /* The index refers to the final position, so if the source
490      is less than the destination, then we must subtract 1, to
491      account for the position vacated by the source */
492   if (from < to)
493     new_pos--;
494   dict_reorder_var (dict->dict, var, new_pos);
495 }
496
497
498 static gboolean
499 is_printable_key (gint keyval)
500 {
501   switch (keyval)
502     {
503     case GDK_KEY_Return:
504     case GDK_KEY_ISO_Left_Tab:
505     case GDK_KEY_Tab:
506       return FALSE;
507       break;
508     }
509
510   return (0 != gdk_keyval_to_unicode (keyval));
511 }
512
513 struct dispatch
514 {
515   PsppireVariableSheet *sheet;
516   void (*payload) (PsppireVariableSheet *);
517 };
518
519
520 static gboolean
521 on_key_press (GtkWidget *w, GdkEventKey *e, gpointer user_data)
522 {
523   const struct dispatch *d = user_data;
524   if (is_printable_key (e->keyval))
525     {
526       d->payload (d->sheet);
527       return TRUE;
528     }
529
530   return FALSE;
531 }
532
533 static gboolean
534 on_button_press (GtkWidget *w, GdkEventButton *e, gpointer user_data)
535 {
536   const struct dispatch *d = user_data;
537   if (e->button != 1)
538     return TRUE;
539
540   d->payload (d->sheet);
541   return TRUE;
542 }
543
544 static void
545 on_edit_start (GtkCellRenderer *renderer,
546      GtkCellEditable *editable,
547      gchar           *path,
548      gpointer         user_data)
549 {
550   gtk_widget_grab_focus (GTK_WIDGET (editable));
551   g_signal_connect (editable, "key-press-event",
552                     G_CALLBACK (on_key_press), user_data);
553   g_signal_connect (editable, "button-press-event",
554                     G_CALLBACK (on_button_press), user_data);
555
556 }
557
558 static void
559 psppire_variable_sheet_init (PsppireVariableSheet *sheet)
560 {
561   sheet->dispose_has_run = FALSE;
562
563   sheet->value_label_renderer = gtk_cell_renderer_text_new ();
564   sheet->value_label_dispatch = g_malloc (sizeof *sheet->value_label_dispatch);
565   sheet->value_label_dispatch->sheet = sheet;
566   sheet->value_label_dispatch->payload = set_value_labels;
567   g_signal_connect_after (sheet->value_label_renderer,
568                           "editing-started", G_CALLBACK (on_edit_start),
569                           sheet->value_label_dispatch);
570
571   sheet->missing_values_renderer = gtk_cell_renderer_text_new ();
572   sheet->missing_values_dispatch = g_malloc (sizeof *sheet->missing_values_dispatch);
573   sheet->missing_values_dispatch->sheet = sheet;
574   sheet->missing_values_dispatch->payload = set_missing_values;
575   g_signal_connect_after (sheet->missing_values_renderer,
576                           "editing-started", G_CALLBACK (on_edit_start),
577                           sheet->missing_values_dispatch);
578
579   sheet->var_type_renderer = gtk_cell_renderer_text_new ();
580   sheet->var_type_dispatch = g_malloc (sizeof *sheet->var_type_dispatch);
581   sheet->var_type_dispatch->sheet = sheet;
582   sheet->var_type_dispatch->payload = set_var_type;
583   g_signal_connect_after (sheet->var_type_renderer,
584                           "editing-started", G_CALLBACK (on_edit_start),
585                           sheet->var_type_dispatch);
586
587   sheet->row_popup = create_var_row_header_popup_menu (sheet);
588
589
590   g_signal_connect (sheet, "selection-changed",
591                     G_CALLBACK (set_var_popup_sensitivity), sheet);
592
593   g_signal_connect (sheet, "row-header-pressed",
594                     G_CALLBACK (show_variables_row_popup), sheet);
595
596   g_signal_connect_swapped (sheet, "value-changed",
597                             G_CALLBACK (change_var_property), sheet);
598
599   g_signal_connect (sheet, "row-moved",
600                     G_CALLBACK (move_variable), NULL);
601 }