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