728604d5138db9afdc2ac14d87a09cb1a515fd47
[pspp] / src / ui / gui / psppire-data-sheet.c
1 /* PSPPIRE - a graphical user interface for PSPP.
2    Copyright (C) 2017, 2019, 2020  Free Software Foundation
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_finalize (GObject *obj)
353 {
354   /* Chain up to the parent class */
355   G_OBJECT_CLASS (parent_class)->finalize (obj);
356 }
357
358 static void
359 psppire_data_sheet_dispose (GObject *obj)
360 {
361   PsppireDataSheet *sheet = PSPPIRE_DATA_SHEET (obj);
362
363   if (dispose_has_run)
364     return;
365
366   dispose_has_run = TRUE;
367
368   g_object_unref (sheet->data_sheet_cases_column_popup);
369   g_object_unref (sheet->data_sheet_cases_row_popup);
370
371   /* Chain up to the parent class */
372   G_OBJECT_CLASS (parent_class)->dispose (obj);
373 }
374
375
376 static void
377 psppire_data_sheet_realize (GtkWidget *widget)
378 {
379   g_object_set (widget,
380                 "forward-conversion", psppire_data_store_value_to_string,
381                 "reverse-conversion", psppire_data_store_string_to_value,
382                 "editable", TRUE,
383                 "horizontal-draggable", TRUE,
384                 NULL);
385
386   /* Chain up to the parent class */
387   GTK_WIDGET_CLASS (parent_class)->realize (widget);
388 }
389
390 static void
391 psppire_data_sheet_class_init (PsppireDataSheetClass *class)
392 {
393   GObjectClass *object_class = G_OBJECT_CLASS (class);
394   GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (class);
395
396   widget_class->realize = psppire_data_sheet_realize;
397   object_class->dispose = psppire_data_sheet_dispose;
398   object_class->finalize = psppire_data_sheet_finalize;
399
400   parent_class = g_type_class_peek_parent (class);
401 }
402
403 GtkWidget*
404 psppire_data_sheet_new (void)
405 {
406   return g_object_new (PSPPIRE_TYPE_DATA_SHEET, NULL);
407 }
408
409
410 static gboolean
411 indicate_filtered_case (GtkWidget *widget, cairo_t *cr, PsppireDataStore *store)
412 {
413   guint row = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (widget), "row"));
414
415   if (!psppire_data_store_filtered (store, row))
416     return FALSE;
417
418   /* Draw a diagonal line through the widget */
419   guint width = gtk_widget_get_allocated_width (widget);
420   guint height = gtk_widget_get_allocated_height (widget);
421
422   GtkStyleContext *sc = gtk_widget_get_style_context (widget);
423   gtk_render_line (sc, cr, 0, 0, width, height);
424
425   return FALSE;
426 }
427
428 static void
429 button_post_create (GtkWidget *button, guint i, gpointer user_data)
430 {
431   PsppireDataStore *data_store = PSPPIRE_DATA_STORE (user_data);
432
433   g_object_set_data (G_OBJECT (button), "row", GUINT_TO_POINTER (i));
434   g_signal_connect_after (button, "draw", G_CALLBACK (indicate_filtered_case), data_store);
435 }
436
437
438 static gboolean
439 resize_display_width (PsppireDict *dict, gint pos, gint size, gpointer user_data)
440 {
441   if (pos < 0)
442     return FALSE;
443
444   PsppireDataSheet *sheet = PSPPIRE_DATA_SHEET (user_data);
445   PangoContext *context = gtk_widget_create_pango_context (GTK_WIDGET (sheet));
446   PangoLayout *layout = pango_layout_new (context);
447   PangoRectangle rect;
448
449   pango_layout_set_text (layout, "M", 1);
450   pango_layout_get_extents (layout, NULL, &rect);
451
452   gdouble width_of_M = rect.width / (gdouble) PANGO_SCALE;
453
454   g_object_unref (G_OBJECT (layout));
455   g_object_unref (G_OBJECT (context));
456
457   gint Ms = round ((size / width_of_M) - 0.25);
458   struct variable *var = psppire_dict_get_variable (dict, pos);
459   g_return_val_if_fail (var, TRUE);
460   var_set_display_width (var, Ms);
461   return TRUE;
462 }
463
464 static void
465 set_dictionary (PsppireDataSheet *sheet)
466 {
467   GtkTreeModel *data_model = NULL;
468   g_object_get (sheet, "data-model", &data_model, NULL);
469
470   g_return_if_fail (data_model);
471
472   PsppireDataStore *store = PSPPIRE_DATA_STORE (data_model);
473   g_object_set (sheet, "hmodel", store->dict, NULL);
474
475   g_signal_connect (store->dict, "resize-item", G_CALLBACK (resize_display_width),
476                     sheet);
477
478   SswAxisModel *vmodel = NULL;
479   g_object_get (sheet, "vmodel", &vmodel, NULL);
480   g_assert (SSW_IS_AXIS_MODEL (vmodel));
481
482   g_object_set (vmodel,
483                 "post-button-create-func", button_post_create,
484                 "post-button-create-func-data", store,
485                 NULL);
486 }
487
488 static void
489 move_variable (PsppireDataSheet *sheet, gint from, gint to, gpointer ud)
490 {
491   PsppireDataStore *data_store = NULL;
492   g_object_get (sheet, "data-model", &data_store, NULL);
493
494   if (data_store == NULL)
495     return;
496
497   PsppireDict *dict = data_store->dict;
498   struct variable *var = psppire_dict_get_variable (dict, from);
499
500   if (var == NULL)
501     return;
502   gint new_pos = to;
503   /* The index refers to the final position, so if the source
504      is less than the destination, then we must subtract 1, to
505      account for the position vacated by the source */
506   if (from < to)
507     new_pos--;
508   dict_reorder_var (dict->dict, var, new_pos);
509 }
510
511 static void
512 psppire_data_sheet_init (PsppireDataSheet *sheet)
513 {
514   sheet->data_sheet_cases_column_popup =
515     create_data_column_header_popup_menu (sheet);
516
517   sheet->data_sheet_cases_row_popup =
518     create_data_row_header_popup_menu (sheet);
519
520   g_signal_connect (sheet, "selection-changed",
521                     G_CALLBACK (set_menu_items_sensitivity), sheet);
522
523   g_signal_connect (sheet, "column-header-pressed",
524                     G_CALLBACK (show_cases_column_popup), sheet);
525
526   g_signal_connect (sheet, "row-header-pressed",
527                     G_CALLBACK (show_cases_row_popup), sheet);
528
529   g_signal_connect (sheet, "value-changed",
530                     G_CALLBACK (change_data_value), NULL);
531
532   g_signal_connect (sheet, "notify::data-model",
533                     G_CALLBACK (set_dictionary), NULL);
534
535   g_signal_connect (sheet, "column-moved", G_CALLBACK (move_variable), NULL);
536 }