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