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