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