PSPPIRE: Avoid memory leaks from popup memnus
[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   /* 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_class_init (PsppireVariableSheetClass *class)
460 {
461   GObjectClass *object_class = G_OBJECT_CLASS (class);
462   object_class->dispose = psppire_variable_sheet_dispose;
463
464   parent_class = g_type_class_peek_parent (class);
465
466   object_class->finalize = psppire_variable_sheet_finalize;
467 }
468
469 GtkWidget*
470 psppire_variable_sheet_new (void)
471 {
472   PsppireVarSheetHeader *vsh =
473     g_object_new (PSPPIRE_TYPE_VAR_SHEET_HEADER, NULL);
474
475   GObject *obj =
476     g_object_new (PSPPIRE_TYPE_VARIABLE_SHEET,
477                   "select-renderer-func", select_renderer_func,
478                   "hmodel", vsh,
479                   "forward-conversion", var_sheet_data_to_string,
480                   "editable", TRUE,
481                   "vertical-draggable", TRUE,
482                   NULL);
483
484   return GTK_WIDGET (obj);
485 }
486
487 static void
488 move_variable (PsppireVariableSheet *sheet, gint from, gint to, gpointer ud)
489 {
490   PsppireDict *dict = NULL;
491   g_object_get (sheet, "data-model", &dict, NULL);
492
493   if (dict == NULL)
494     return;
495
496   struct variable *var = psppire_dict_get_variable (dict, from);
497
498   if (var == NULL)
499     return;
500   gint new_pos = to;
501   /* The index refers to the final position, so if the source
502      is less than the destination, then we must subtract 1, to
503      account for the position vacated by the source */
504   if (from < to)
505     new_pos--;
506   dict_reorder_var (dict->dict, var, new_pos);
507 }
508
509
510 static gboolean
511 is_printable_key (gint keyval)
512 {
513   switch (keyval)
514     {
515     case GDK_KEY_Return:
516     case GDK_KEY_ISO_Left_Tab:
517     case GDK_KEY_Tab:
518       return FALSE;
519       break;
520     }
521
522   return (0 != gdk_keyval_to_unicode (keyval));
523 }
524
525 struct dispatch
526 {
527   PsppireVariableSheet *sheet;
528   void (*payload) (PsppireVariableSheet *);
529 };
530
531
532 static gboolean
533 on_key_press (GtkWidget *w, GdkEventKey *e, gpointer user_data)
534 {
535   const struct dispatch *d = user_data;
536   if (is_printable_key (e->keyval))
537     {
538       d->payload (d->sheet);
539       return TRUE;
540     }
541
542   return FALSE;
543 }
544
545 static gboolean
546 on_button_press (GtkWidget *w, GdkEventButton *e, gpointer user_data)
547 {
548   const struct dispatch *d = user_data;
549   if (e->button != 1)
550     return TRUE;
551
552   d->payload (d->sheet);
553   return TRUE;
554 }
555
556 static void
557 on_edit_start (GtkCellRenderer *renderer,
558      GtkCellEditable *editable,
559      gchar           *path,
560      gpointer         user_data)
561 {
562   gtk_widget_grab_focus (GTK_WIDGET (editable));
563   g_signal_connect (editable, "key-press-event",
564                     G_CALLBACK (on_key_press), user_data);
565   g_signal_connect (editable, "button-press-event",
566                     G_CALLBACK (on_button_press), user_data);
567
568 }
569
570 static void
571 psppire_variable_sheet_init (PsppireVariableSheet *sheet)
572 {
573   sheet->dispose_has_run = FALSE;
574
575   sheet->value_label_renderer = gtk_cell_renderer_text_new ();
576   sheet->value_label_dispatch = g_malloc (sizeof *sheet->value_label_dispatch);
577   sheet->value_label_dispatch->sheet = sheet;
578   sheet->value_label_dispatch->payload = set_value_labels;
579   g_signal_connect_after (sheet->value_label_renderer,
580                           "editing-started", G_CALLBACK (on_edit_start),
581                           sheet->value_label_dispatch);
582
583   sheet->missing_values_renderer = gtk_cell_renderer_text_new ();
584   sheet->missing_values_dispatch = g_malloc (sizeof *sheet->missing_values_dispatch);
585   sheet->missing_values_dispatch->sheet = sheet;
586   sheet->missing_values_dispatch->payload = set_missing_values;
587   g_signal_connect_after (sheet->missing_values_renderer,
588                           "editing-started", G_CALLBACK (on_edit_start),
589                           sheet->missing_values_dispatch);
590
591   sheet->var_type_renderer = gtk_cell_renderer_text_new ();
592   sheet->var_type_dispatch = g_malloc (sizeof *sheet->var_type_dispatch);
593   sheet->var_type_dispatch->sheet = sheet;
594   sheet->var_type_dispatch->payload = set_var_type;
595   g_signal_connect_after (sheet->var_type_renderer,
596                           "editing-started", G_CALLBACK (on_edit_start),
597                           sheet->var_type_dispatch);
598
599   sheet->row_popup = create_var_row_header_popup_menu (sheet);
600
601   g_signal_connect (sheet, "selection-changed",
602                     G_CALLBACK (set_var_popup_sensitivity), sheet);
603
604   g_signal_connect (sheet, "row-header-pressed",
605                     G_CALLBACK (show_variables_row_popup), sheet);
606
607   g_signal_connect_swapped (sheet, "value-changed",
608                             G_CALLBACK (change_var_property), sheet);
609
610   g_signal_connect (sheet, "row-moved",
611                     G_CALLBACK (move_variable), NULL);
612 }