Revert "Fixed a use after free error when manipulating datasets."
[pspp] / src / ui / gui / psppire-data-sheet.c
1 /* PSPPIRE - a graphical user interface for PSPP.
2    Copyright (C) 2017, 2019  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-data-sheet.h"
20 #include <math.h>
21
22 #include <gettext.h>
23 #define _(msgid) gettext (msgid)
24 #define P_(X) (X)
25
26 #include "value-variant.h"
27
28 #include "ui/gui/executor.h"
29 #include "psppire-data-window.h"
30 #include "ssw-axis-model.h"
31
32 static void
33 do_sort (PsppireDataSheet *sheet, GtkSortType order)
34 {
35   SswRange *range = SSW_SHEET(sheet)->selection;
36
37   PsppireDataStore *data_store = NULL;
38   g_object_get (sheet, "data-model", &data_store, NULL);
39
40   int n_vars = 0;
41   int i;
42
43   PsppireDataWindow *pdw =
44      psppire_data_window_for_data_store (data_store);
45
46   GString *syntax = g_string_new ("SORT CASES BY");
47   for (i = range->start_x ; i <= range->end_x; ++i)
48     {
49       const struct variable *var = psppire_dict_get_variable (data_store->dict, i);
50       if (var != NULL)
51         {
52           g_string_append_printf (syntax, " %s", var_get_name (var));
53           n_vars++;
54         }
55     }
56   if (n_vars > 0)
57     {
58       if (order == GTK_SORT_DESCENDING)
59         g_string_append (syntax, " (DOWN)");
60       g_string_append_c (syntax, '.');
61       execute_const_syntax_string (pdw, syntax->str);
62     }
63   g_string_free (syntax, TRUE);
64 }
65
66
67 static void
68 sort_ascending (PsppireDataSheet *sheet)
69 {
70   do_sort (sheet, GTK_SORT_ASCENDING);
71
72   gtk_widget_queue_draw (GTK_WIDGET (sheet));
73 }
74
75 static void
76 sort_descending (PsppireDataSheet *sheet)
77 {
78   do_sort (sheet, GTK_SORT_DESCENDING);
79
80   gtk_widget_queue_draw (GTK_WIDGET (sheet));
81 }
82
83 \f
84
85 static void
86 change_data_value (PsppireDataSheet *sheet, gint col, gint row, GValue *value)
87 {
88   PsppireDataStore *store = NULL;
89   g_object_get (sheet, "data-model", &store, NULL);
90
91   const struct variable *var = psppire_dict_get_variable (store->dict, col);
92
93   if (NULL == var)
94     return;
95
96   union value v;
97
98   GVariant *vrnt = g_value_get_variant (value);
99
100   value_variant_get (&v, vrnt);
101
102   psppire_data_store_set_value (store, row, var, &v);
103
104   value_destroy_from_variant (&v, vrnt);
105 }
106
107 \f
108
109 static void
110 show_cases_row_popup (PsppireDataSheet *sheet, int row,
111                       guint button, guint state, gpointer p)
112 {
113   GListModel *vmodel = NULL;
114   g_object_get (sheet, "vmodel", &vmodel, NULL);
115   if (vmodel == NULL)
116     return;
117
118   guint n_items = g_list_model_get_n_items (vmodel);
119
120   if (row >= n_items)
121     return;
122
123   if (button != 3)
124     return;
125
126   g_object_set_data (G_OBJECT (sheet->data_sheet_cases_row_popup), "item",
127                      GINT_TO_POINTER (row));
128
129   gtk_menu_popup_at_pointer (GTK_MENU (sheet->data_sheet_cases_row_popup), NULL);
130 }
131
132
133 static void
134 insert_new_case (PsppireDataSheet *sheet)
135 {
136   PsppireDataStore *data_store = NULL;
137   g_object_get (sheet, "data-model", &data_store, NULL);
138
139   gint posn = GPOINTER_TO_INT (g_object_get_data
140                                 (G_OBJECT (sheet->data_sheet_cases_row_popup), "item"));
141
142   psppire_data_store_insert_new_case (data_store, posn);
143
144   gtk_widget_queue_draw (GTK_WIDGET (sheet));
145 }
146
147 static void
148 delete_cases (PsppireDataSheet *sheet)
149 {
150   SswRange *range = SSW_SHEET(sheet)->selection;
151
152   PsppireDataStore *data_store = NULL;
153   g_object_get (sheet, "data-model", &data_store, NULL);
154
155   psppire_data_store_delete_cases (data_store, range->start_y,
156                                    range->end_y - range->start_y + 1);
157
158   gtk_widget_queue_draw (GTK_WIDGET (sheet));
159 }
160
161 static GtkWidget *
162 create_data_row_header_popup_menu (PsppireDataSheet *sheet)
163 {
164   GtkWidget *menu = gtk_menu_new ();
165
166   /* gtk_menu_shell_append does not sink/ref this object,
167      so we must do it ourselves (and remember to unref it).  */
168   g_object_ref_sink (menu);
169
170   GtkWidget *item =
171     gtk_menu_item_new_with_mnemonic  (_("_Insert Case"));
172
173   g_signal_connect_swapped (item, "activate", G_CALLBACK (insert_new_case), sheet);
174   gtk_menu_shell_append (GTK_MENU_SHELL (menu), item);
175
176   item = gtk_separator_menu_item_new ();
177   gtk_menu_shell_append (GTK_MENU_SHELL (menu), item);
178
179   sheet->data_clear_cases_menu_item = gtk_menu_item_new_with_mnemonic (_("Cl_ear Cases"));
180   gtk_widget_set_sensitive (sheet->data_clear_cases_menu_item, FALSE);
181   gtk_menu_shell_append (GTK_MENU_SHELL (menu), sheet->data_clear_cases_menu_item);
182   g_signal_connect_swapped (sheet->data_clear_cases_menu_item, "activate",
183                             G_CALLBACK (delete_cases), sheet);
184
185   gtk_widget_show_all (menu);
186   return menu;
187 }
188
189
190 static void
191 show_cases_column_popup (PsppireDataSheet *sheet, int column, guint button, guint state,
192                          gpointer p)
193 {
194   GListModel *hmodel = NULL;
195   g_object_get (sheet, "hmodel", &hmodel, NULL);
196   if (hmodel == NULL)
197     return;
198
199   guint n_items = g_list_model_get_n_items (hmodel);
200
201   if (column >= n_items)
202     return;
203
204   if (button != 3)
205     return;
206
207   g_object_set_data (G_OBJECT (sheet->data_sheet_cases_column_popup), "item",
208                      GINT_TO_POINTER (column));
209
210   gtk_menu_popup_at_pointer (GTK_MENU (sheet->data_sheet_cases_column_popup), NULL);
211 }
212
213 /* Insert a new variable before the variable at POSN.  */
214 void
215 psppire_data_sheet_insert_new_variable_at_posn (PsppireDataSheet *sheet,
216                                                 gint posn)
217 {
218   PsppireDataStore *data_store = NULL;
219   g_object_get (sheet, "data-model", &data_store, NULL);
220
221   const struct variable *v = psppire_dict_insert_variable (data_store->dict,
222                                                            posn, NULL);
223
224   psppire_data_store_insert_value (data_store, var_get_width(v),
225                                    var_get_case_index (v));
226
227   ssw_sheet_scroll_to (SSW_SHEET (sheet), posn, -1);
228
229   gtk_widget_queue_draw (GTK_WIDGET (sheet));
230 }
231
232 static void
233 insert_new_variable (PsppireDataSheet *sheet)
234 {
235   PsppireDataStore *data_store = NULL;
236   g_object_get (sheet, "data-model", &data_store, NULL);
237
238   gint posn = GPOINTER_TO_INT (g_object_get_data
239                                 (G_OBJECT (sheet->data_sheet_cases_column_popup),
240                                  "item"));
241
242   psppire_data_sheet_insert_new_variable_at_posn (sheet, posn);
243 }
244
245
246 static void
247 set_menu_items_sensitivity (PsppireDataSheet *sheet, gpointer selection, gpointer p)
248 {
249   SswRange *range = selection;
250
251   PsppireDataStore *data_store = NULL;
252   g_object_get (sheet, "data-model", &data_store, NULL);
253
254
255   gint width = gtk_tree_model_get_n_columns (GTK_TREE_MODEL (data_store));
256   gint length = psppire_data_store_get_case_count (data_store);
257
258
259   gboolean whole_row_selected = (range->start_x == 0 && range->end_x == width - 1);
260   gtk_widget_set_sensitive (sheet->data_clear_cases_menu_item, whole_row_selected);
261
262   gboolean whole_column_selected =
263     (range->start_y == 0 && range->end_y == length - 1);
264   gtk_widget_set_sensitive (sheet->data_clear_variables_menu_item,
265                             whole_column_selected);
266   gtk_widget_set_sensitive (sheet->data_sort_ascending_menu_item,
267                             whole_column_selected);
268   gtk_widget_set_sensitive (sheet->data_sort_descending_menu_item,
269                             whole_column_selected);
270 }
271
272 void
273 psppire_data_sheet_delete_variables (PsppireDataSheet *sheet)
274 {
275   SswRange *range = SSW_SHEET(sheet)->selection;
276
277   PsppireDataStore *data_store = NULL;
278   g_object_get (sheet, "data-model", &data_store, NULL);
279
280   if (range->start_x > range->end_x)
281     {
282       gint temp = range->start_x;
283       range->start_x = range->end_x;
284       range->end_x = temp;
285     }
286
287   psppire_dict_delete_variables (data_store->dict, range->start_x,
288                                  (range->end_x - range->start_x + 1));
289
290   ssw_sheet_scroll_to (SSW_SHEET (sheet), range->start_x, -1);
291
292   gtk_widget_queue_draw (GTK_WIDGET (sheet));
293 }
294
295 static GtkWidget *
296 create_data_column_header_popup_menu (PsppireDataSheet *sheet)
297 {
298   GtkWidget *menu = gtk_menu_new ();
299
300   /* gtk_menu_shell_append does not sink/ref this object,
301      so we must do it ourselves (and remember to unref it).  */
302   g_object_ref_sink (menu);
303
304   GtkWidget *item =
305     gtk_menu_item_new_with_mnemonic  (_("_Insert Variable"));
306   g_signal_connect_swapped (item, "activate", G_CALLBACK (insert_new_variable),
307                             sheet);
308   gtk_menu_shell_append (GTK_MENU_SHELL (menu), item);
309
310   item = gtk_separator_menu_item_new ();
311   gtk_menu_shell_append (GTK_MENU_SHELL (menu), item);
312
313   sheet->data_clear_variables_menu_item =
314     gtk_menu_item_new_with_mnemonic  (_("Cl_ear Variables"));
315   g_signal_connect_swapped (sheet->data_clear_variables_menu_item, "activate",
316                             G_CALLBACK (psppire_data_sheet_delete_variables),
317                             sheet);
318   gtk_widget_set_sensitive (sheet->data_clear_variables_menu_item, FALSE);
319   gtk_menu_shell_append (GTK_MENU_SHELL (menu), sheet->data_clear_variables_menu_item);
320
321   item = gtk_separator_menu_item_new ();
322   gtk_menu_shell_append (GTK_MENU_SHELL (menu), item);
323
324
325   sheet->data_sort_ascending_menu_item =
326     gtk_menu_item_new_with_mnemonic (_("Sort _Ascending"));
327   g_signal_connect_swapped (sheet->data_sort_ascending_menu_item, "activate",
328                             G_CALLBACK (sort_ascending), sheet);
329   gtk_widget_set_sensitive (sheet->data_sort_ascending_menu_item, FALSE);
330   gtk_menu_shell_append (GTK_MENU_SHELL (menu), sheet->data_sort_ascending_menu_item);
331
332   sheet->data_sort_descending_menu_item =
333     gtk_menu_item_new_with_mnemonic (_("Sort _Descending"));
334   g_signal_connect_swapped (sheet->data_sort_descending_menu_item, "activate",
335                             G_CALLBACK (sort_descending), sheet);
336   gtk_widget_set_sensitive (sheet->data_sort_descending_menu_item, FALSE);
337   gtk_menu_shell_append (GTK_MENU_SHELL (menu), sheet->data_sort_descending_menu_item);
338
339   gtk_widget_show_all (menu);
340   return menu;
341 }
342
343
344 \f
345
346 G_DEFINE_TYPE (PsppireDataSheet, psppire_data_sheet, SSW_TYPE_SHEET)
347
348 static GObjectClass * parent_class = NULL;
349 static gboolean dispose_has_run = FALSE;
350
351 static void
352 psppire_data_sheet_dispose (GObject *obj)
353 {
354   PsppireDataSheet *sheet = PSPPIRE_DATA_SHEET (obj);
355
356   if (dispose_has_run)
357     return;
358
359   dispose_has_run = TRUE;
360
361   g_object_unref (sheet->data_sheet_cases_column_popup);
362   g_object_unref (sheet->data_sheet_cases_row_popup);
363
364   /* Chain up to the parent class */
365   G_OBJECT_CLASS (parent_class)->dispose (obj);
366 }
367
368 static void
369 psppire_data_sheet_class_init (PsppireDataSheetClass *class)
370 {
371   GObjectClass *object_class = G_OBJECT_CLASS (class);
372   object_class->dispose = psppire_data_sheet_dispose;
373
374   parent_class = g_type_class_peek_parent (class);
375 }
376
377 GtkWidget*
378 psppire_data_sheet_new (void)
379 {
380   GObject *obj =
381     g_object_new (PSPPIRE_TYPE_DATA_SHEET,
382                   "forward-conversion", psppire_data_store_value_to_string,
383                   "reverse-conversion", psppire_data_store_string_to_value,
384                   "editable", TRUE,
385                   "horizontal-draggable", TRUE,
386                   NULL);
387
388   return GTK_WIDGET (obj);
389 }
390
391
392 static gboolean
393 indicate_filtered_case (GtkWidget *widget, cairo_t *cr, PsppireDataStore *store)
394 {
395   guint row = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (widget), "row"));
396
397   if (!psppire_data_store_filtered (store, row))
398     return FALSE;
399
400   /* Draw a diagonal line through the widget */
401   guint width = gtk_widget_get_allocated_width (widget);
402   guint height = gtk_widget_get_allocated_height (widget);
403
404   GtkStyleContext *sc = gtk_widget_get_style_context (widget);
405   gtk_render_line (sc, cr, 0, 0, width, height);
406
407   return FALSE;
408 }
409
410 static void
411 button_post_create (GtkWidget *button, guint i, gpointer user_data)
412 {
413   PsppireDataStore *data_store = PSPPIRE_DATA_STORE (user_data);
414
415   g_object_set_data (G_OBJECT (button), "row", GUINT_TO_POINTER (i));
416   g_signal_connect_after (button, "draw", G_CALLBACK (indicate_filtered_case), data_store);
417 }
418
419
420 static gboolean
421 resize_display_width (PsppireDict *dict, gint pos, gint size, gpointer user_data)
422 {
423   if (pos < 0)
424     return FALSE;
425
426   PsppireDataSheet *sheet = PSPPIRE_DATA_SHEET (user_data);
427   PangoContext *context = gtk_widget_create_pango_context (GTK_WIDGET (sheet));
428   PangoLayout *layout = pango_layout_new (context);
429   PangoRectangle rect;
430
431   pango_layout_set_text (layout, "M", 1);
432   pango_layout_get_extents (layout, NULL, &rect);
433
434   gdouble width_of_M = rect.width / (gdouble) PANGO_SCALE;
435
436   g_object_unref (G_OBJECT (layout));
437   g_object_unref (G_OBJECT (context));
438
439   gint Ms = round ((size / width_of_M) - 0.25);
440   struct variable *var = psppire_dict_get_variable (dict, pos);
441   g_return_val_if_fail (var, TRUE);
442   var_set_display_width (var, Ms);
443   return TRUE;
444 }
445
446 static void
447 set_dictionary (PsppireDataSheet *sheet)
448 {
449   GtkTreeModel *data_model = NULL;
450   g_object_get (sheet, "data-model", &data_model, NULL);
451
452   PsppireDataStore *store = PSPPIRE_DATA_STORE (data_model);
453   g_object_set (sheet, "hmodel", store->dict, NULL);
454
455   g_signal_connect (store->dict, "resize-item", G_CALLBACK (resize_display_width),
456                     sheet);
457
458   SswAxisModel *vmodel = NULL;
459   g_object_get (sheet, "vmodel", &vmodel, NULL);
460   g_assert (SSW_IS_AXIS_MODEL (vmodel));
461
462   g_object_set (vmodel,
463                 "post-button-create-func", button_post_create,
464                 "post-button-create-func-data", store,
465                 NULL);
466 }
467
468 static void
469 move_variable (PsppireDataSheet *sheet, gint from, gint to, gpointer ud)
470 {
471   PsppireDataStore *data_store = NULL;
472   g_object_get (sheet, "data-model", &data_store, NULL);
473
474   if (data_store == NULL)
475     return;
476
477   PsppireDict *dict = data_store->dict;
478   struct variable *var = psppire_dict_get_variable (dict, from);
479
480   if (var == NULL)
481     return;
482   gint new_pos = to;
483   /* The index refers to the final position, so if the source
484      is less than the destination, then we must subtract 1, to
485      account for the position vacated by the source */
486   if (from < to)
487     new_pos--;
488   dict_reorder_var (dict->dict, var, new_pos);
489 }
490
491 static void
492 psppire_data_sheet_init (PsppireDataSheet *sheet)
493 {
494   sheet->data_sheet_cases_column_popup =
495     create_data_column_header_popup_menu (sheet);
496
497   sheet->data_sheet_cases_row_popup =
498     create_data_row_header_popup_menu (sheet);
499
500   g_signal_connect (sheet, "selection-changed",
501                     G_CALLBACK (set_menu_items_sensitivity), sheet);
502
503   g_signal_connect (sheet, "column-header-pressed",
504                     G_CALLBACK (show_cases_column_popup), sheet);
505
506   g_signal_connect (sheet, "row-header-pressed",
507                     G_CALLBACK (show_cases_row_popup), sheet);
508
509   g_signal_connect (sheet, "value-changed",
510                     G_CALLBACK (change_data_value), NULL);
511
512   g_signal_connect (sheet, "notify::data-model",
513                     G_CALLBACK (set_dictionary), NULL);
514
515   g_signal_connect (sheet, "column-moved", G_CALLBACK (move_variable), NULL);
516 }