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