GUI: Scroll to correct datasheet position after deleting/inserting variables.
[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   GtkWidget *item =
167     gtk_menu_item_new_with_mnemonic  (_("_Insert Case"));
168
169   g_signal_connect_swapped (item, "activate", G_CALLBACK (insert_new_case), sheet);
170   gtk_menu_shell_append (GTK_MENU_SHELL (menu), item);
171
172   item = gtk_separator_menu_item_new ();
173   gtk_menu_shell_append (GTK_MENU_SHELL (menu), item);
174
175   sheet->data_clear_cases_menu_item = gtk_menu_item_new_with_mnemonic (_("Cl_ear Cases"));
176   gtk_widget_set_sensitive (sheet->data_clear_cases_menu_item, FALSE);
177   gtk_menu_shell_append (GTK_MENU_SHELL (menu), sheet->data_clear_cases_menu_item);
178   g_signal_connect_swapped (sheet->data_clear_cases_menu_item, "activate",
179                             G_CALLBACK (delete_cases), sheet);
180
181   gtk_widget_show_all (menu);
182   return menu;
183 }
184
185
186 static void
187 show_cases_column_popup (PsppireDataSheet *sheet, int column, guint button, guint state,
188                          gpointer p)
189 {
190   GListModel *hmodel = NULL;
191   g_object_get (sheet, "hmodel", &hmodel, NULL);
192   if (hmodel == NULL)
193     return;
194
195   guint n_items = g_list_model_get_n_items (hmodel);
196
197   if (column >= n_items)
198     return;
199
200   if (button != 3)
201     return;
202
203   g_object_set_data (G_OBJECT (sheet->data_sheet_cases_column_popup), "item",
204                      GINT_TO_POINTER (column));
205
206   gtk_menu_popup_at_pointer (GTK_MENU (sheet->data_sheet_cases_column_popup), NULL);
207 }
208
209 void
210 psppire_data_sheet_insert_new_variable_at_posn (PsppireDataSheet *sheet, gint posn)
211 {
212   PsppireDataStore *data_store = NULL;
213   g_object_get (sheet, "data-model", &data_store, NULL);
214
215   const struct variable *v = psppire_dict_insert_variable (data_store->dict,
216                                                            posn, NULL);
217
218   psppire_data_store_insert_value (data_store, var_get_width(v),
219                                    var_get_case_index (v));
220
221   ssw_sheet_scroll_to (SSW_SHEET (sheet), posn, -1);
222
223   gtk_widget_queue_draw (GTK_WIDGET (sheet));
224 }
225
226 static void
227 insert_new_variable (PsppireDataSheet *sheet)
228 {
229   PsppireDataStore *data_store = NULL;
230   g_object_get (sheet, "data-model", &data_store, NULL);
231
232   gint posn = GPOINTER_TO_INT (g_object_get_data
233                                 (G_OBJECT (sheet->data_sheet_cases_column_popup),
234                                  "item"));
235
236   psppire_data_sheet_insert_new_variable_at_posn (sheet, posn);
237 }
238
239
240 static void
241 set_menu_items_sensitivity (PsppireDataSheet *sheet, gpointer selection, gpointer p)
242 {
243   SswRange *range = selection;
244
245   PsppireDataStore *data_store = NULL;
246   g_object_get (sheet, "data-model", &data_store, NULL);
247
248
249   gint width = gtk_tree_model_get_n_columns (GTK_TREE_MODEL (data_store));
250   gint length = psppire_data_store_get_case_count (data_store);
251
252
253   gboolean whole_row_selected = (range->start_x == 0 && range->end_x == width - 1);
254   gtk_widget_set_sensitive (sheet->data_clear_cases_menu_item, whole_row_selected);
255
256   gboolean whole_column_selected =
257     (range->start_y == 0 && range->end_y == length - 1);
258   gtk_widget_set_sensitive (sheet->data_clear_variables_menu_item,
259                             whole_column_selected);
260   gtk_widget_set_sensitive (sheet->data_sort_ascending_menu_item,
261                             whole_column_selected);
262   gtk_widget_set_sensitive (sheet->data_sort_descending_menu_item,
263                             whole_column_selected);
264 }
265
266 void
267 psppire_data_sheet_delete_variables (PsppireDataSheet *sheet)
268 {
269   SswRange *range = SSW_SHEET(sheet)->selection;
270
271   PsppireDataStore *data_store = NULL;
272   g_object_get (sheet, "data-model", &data_store, NULL);
273
274   psppire_dict_delete_variables (data_store->dict, range->start_x,
275                                  (range->end_x - range->start_x + 1));
276
277   ssw_sheet_scroll_to (SSW_SHEET (sheet), range->start_x, -1);
278
279   gtk_widget_queue_draw (GTK_WIDGET (sheet));
280 }
281
282
283
284 static GtkWidget *
285 create_data_column_header_popup_menu (PsppireDataSheet *sheet)
286 {
287   GtkWidget *menu = gtk_menu_new ();
288
289   GtkWidget *item =
290     gtk_menu_item_new_with_mnemonic  (_("_Insert Variable"));
291   g_signal_connect_swapped (item, "activate", G_CALLBACK (insert_new_variable),
292                             sheet);
293   gtk_menu_shell_append (GTK_MENU_SHELL (menu), item);
294
295   item = gtk_separator_menu_item_new ();
296   gtk_menu_shell_append (GTK_MENU_SHELL (menu), item);
297
298   sheet->data_clear_variables_menu_item =
299     gtk_menu_item_new_with_mnemonic  (_("Cl_ear Variables"));
300   g_signal_connect_swapped (sheet->data_clear_variables_menu_item, "activate",
301                             G_CALLBACK (psppire_data_sheet_delete_variables),
302                             sheet);
303   gtk_widget_set_sensitive (sheet->data_clear_variables_menu_item, FALSE);
304   gtk_menu_shell_append (GTK_MENU_SHELL (menu), sheet->data_clear_variables_menu_item);
305
306   item = gtk_separator_menu_item_new ();
307   gtk_menu_shell_append (GTK_MENU_SHELL (menu), item);
308
309
310   sheet->data_sort_ascending_menu_item =
311     gtk_menu_item_new_with_mnemonic (_("Sort _Ascending"));
312   g_signal_connect_swapped (sheet->data_sort_ascending_menu_item, "activate",
313                             G_CALLBACK (sort_ascending), sheet);
314   gtk_widget_set_sensitive (sheet->data_sort_ascending_menu_item, FALSE);
315   gtk_menu_shell_append (GTK_MENU_SHELL (menu), sheet->data_sort_ascending_menu_item);
316
317   sheet->data_sort_descending_menu_item =
318     gtk_menu_item_new_with_mnemonic (_("Sort _Descending"));
319   g_signal_connect_swapped (sheet->data_sort_descending_menu_item, "activate",
320                             G_CALLBACK (sort_descending), sheet);
321   gtk_widget_set_sensitive (sheet->data_sort_descending_menu_item, FALSE);
322   gtk_menu_shell_append (GTK_MENU_SHELL (menu), sheet->data_sort_descending_menu_item);
323
324   gtk_widget_show_all (menu);
325   return menu;
326 }
327
328
329 \f
330
331 G_DEFINE_TYPE (PsppireDataSheet, psppire_data_sheet, SSW_TYPE_SHEET)
332
333 static GObjectClass * parent_class = NULL;
334 static gboolean dispose_has_run = FALSE;
335
336 static void
337 psppire_data_sheet_dispose (GObject *obj)
338 {
339   //  PsppireDataSheet *sheet = PSPPIRE_DATA_SHEET (obj);
340
341   if (dispose_has_run)
342     return;
343
344   dispose_has_run = TRUE;
345
346   /* Chain up to the parent class */
347   G_OBJECT_CLASS (parent_class)->dispose (obj);
348 }
349
350 static void
351 psppire_data_sheet_class_init (PsppireDataSheetClass *class)
352 {
353   GObjectClass *object_class = G_OBJECT_CLASS (class);
354   object_class->dispose = psppire_data_sheet_dispose;
355
356   parent_class = g_type_class_peek_parent (class);
357 }
358
359 GtkWidget*
360 psppire_data_sheet_new (void)
361 {
362   GObject *obj =
363     g_object_new (PSPPIRE_TYPE_DATA_SHEET,
364                   "forward-conversion", psppire_data_store_value_to_string,
365                   "reverse-conversion", psppire_data_store_string_to_value,
366                   "editable", TRUE,
367                   "horizontal-draggable", TRUE,
368                   NULL);
369
370   return GTK_WIDGET (obj);
371 }
372
373
374 static gboolean
375 indicate_filtered_case (GtkWidget *widget, cairo_t *cr, PsppireDataStore *store)
376 {
377   guint row = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (widget), "row"));
378
379   if (!psppire_data_store_filtered (store, row))
380     return FALSE;
381
382   /* Draw a diagonal line through the widget */
383   guint width = gtk_widget_get_allocated_width (widget);
384   guint height = gtk_widget_get_allocated_height (widget);
385
386   GtkStyleContext *sc = gtk_widget_get_style_context (widget);
387   gtk_render_line (sc, cr, 0, 0, width, height);
388
389   return FALSE;
390 }
391
392 static void
393 button_post_create (GtkWidget *button, guint i, gpointer user_data)
394 {
395   PsppireDataStore *data_store = PSPPIRE_DATA_STORE (user_data);
396
397   g_object_set_data (G_OBJECT (button), "row", GUINT_TO_POINTER (i));
398   g_signal_connect_after (button, "draw", G_CALLBACK (indicate_filtered_case), data_store);
399 }
400
401
402 static gboolean
403 resize_display_width (PsppireDict *dict, gint pos, gint size, gpointer user_data)
404 {
405   if (pos < 0)
406     return FALSE;
407
408   PsppireDataSheet *sheet = PSPPIRE_DATA_SHEET (user_data);
409   PangoContext *context = gtk_widget_create_pango_context (GTK_WIDGET (sheet));
410   PangoLayout *layout = pango_layout_new (context);
411   PangoRectangle rect;
412   
413   pango_layout_set_text (layout, "M", 1);
414   pango_layout_get_extents (layout, NULL, &rect);
415   
416   gdouble width_of_M = rect.width / (gdouble) PANGO_SCALE;
417   
418   g_object_unref (G_OBJECT (layout));
419   g_object_unref (G_OBJECT (context));
420   
421   gint Ms = round ((size / width_of_M) - 0.25);
422   struct variable *var = psppire_dict_get_variable (dict, pos);
423   g_return_val_if_fail (var, TRUE);
424   var_set_display_width (var, Ms);
425   return TRUE;
426 }
427
428 static void
429 set_dictionary (PsppireDataSheet *sheet)
430 {
431   GtkTreeModel *data_model = NULL;
432   g_object_get (sheet, "data-model", &data_model, NULL);
433
434   PsppireDataStore *store = PSPPIRE_DATA_STORE (data_model);
435   g_object_set (sheet, "hmodel", store->dict, NULL);
436
437   g_signal_connect (store->dict, "resize-item", G_CALLBACK (resize_display_width),
438                     sheet);
439
440   SswAxisModel *vmodel = NULL;
441   g_object_get (sheet, "vmodel", &vmodel, NULL);
442   g_assert (SSW_IS_AXIS_MODEL (vmodel));
443
444   g_object_set (vmodel,
445                 "post-button-create-func", button_post_create,
446                 "post-button-create-func-data", store,
447                 NULL);
448 }
449
450 static void
451 move_variable (PsppireDataSheet *sheet, gint from, gint to, gpointer ud)
452 {
453   PsppireDataStore *data_store = NULL;
454   g_object_get (sheet, "data-model", &data_store, NULL);
455
456   if (data_store == NULL)
457     return;
458
459   PsppireDict *dict = data_store->dict;
460   struct variable *var = psppire_dict_get_variable (dict, from);
461
462   if (var == NULL)
463     return;
464   gint new_pos = to;
465   /* The index refers to the final position, so if the source
466      is less than the destination, then we must subtract 1, to
467      account for the position vacated by the source */
468   if (from < to)
469     new_pos--;
470   dict_reorder_var (dict->dict, var, new_pos);
471 }
472
473 static void
474 psppire_data_sheet_init (PsppireDataSheet *sheet)
475 {
476   sheet->data_sheet_cases_column_popup =
477     create_data_column_header_popup_menu (sheet);
478
479   sheet->data_sheet_cases_row_popup =
480     create_data_row_header_popup_menu (sheet);
481
482   g_signal_connect (sheet, "selection-changed",
483                     G_CALLBACK (set_menu_items_sensitivity), sheet);
484
485   g_signal_connect (sheet, "column-header-pressed",
486                     G_CALLBACK (show_cases_column_popup), sheet);
487
488   g_signal_connect (sheet, "row-header-pressed",
489                     G_CALLBACK (show_cases_row_popup), sheet);
490
491   g_signal_connect (sheet, "value-changed",
492                     G_CALLBACK (change_data_value), NULL);
493
494   g_signal_connect (sheet, "notify::data-model",
495                     G_CALLBACK (set_dictionary), NULL);
496
497   g_signal_connect (sheet, "column-moved", G_CALLBACK (move_variable), NULL);
498 }