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