dc3cdfba63549a72d40bc1473c134841d2587b4b
[pspp] / src / ui / gui / psppire-data-sheet.c
1 /* PSPPIRE - a graphical user interface for PSPP.
2    Copyright (C) 2017  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
21 #include <gettext.h>
22 #define _(msgid) gettext (msgid)
23 #define P_(X) (X)
24
25 #include "value-variant.h"
26
27 #include "ui/gui/executor.h"
28 #include "psppire-data-window.h"
29 #include "ssw-axis-model.h"
30
31 static void
32 do_sort (PsppireDataSheet *sheet, GtkSortType order)
33 {
34   SswRange *range = SSW_SHEET(sheet)->selection;
35
36   PsppireDataStore *data_store = NULL;
37   g_object_get (sheet, "data-model", &data_store, NULL);
38
39   int n_vars = 0;
40   int i;
41
42   PsppireDataWindow *pdw =
43      psppire_data_window_for_data_store (data_store);
44
45   GString *syntax = g_string_new ("SORT CASES BY");
46   for (i = range->start_x ; i <= range->end_x; ++i)
47     {
48       const struct variable *var = psppire_dict_get_variable (data_store->dict, i);
49       if (var != NULL)
50         {
51           g_string_append_printf (syntax, " %s", var_get_name (var));
52           n_vars++;
53         }
54     }
55   if (n_vars > 0)
56     {
57       if (order == GTK_SORT_DESCENDING)
58         g_string_append (syntax, " (DOWN)");
59       g_string_append_c (syntax, '.');
60       execute_const_syntax_string (pdw, syntax->str);
61     }
62   g_string_free (syntax, TRUE);
63 }
64
65
66 static void
67 sort_ascending (PsppireDataSheet *sheet)
68 {
69   do_sort (sheet, GTK_SORT_ASCENDING);
70
71   gtk_widget_queue_draw (GTK_WIDGET (sheet));
72 }
73
74 static void
75 sort_descending (PsppireDataSheet *sheet)
76 {
77   do_sort (sheet, GTK_SORT_DESCENDING);
78
79   gtk_widget_queue_draw (GTK_WIDGET (sheet));
80 }
81
82 \f
83
84 static void
85 change_data_value (PsppireDataSheet *sheet, gint col, gint row, GValue *value)
86 {
87   PsppireDataStore *store = NULL;
88   g_object_get (sheet, "data-model", &store, NULL);
89
90   const struct variable *var = psppire_dict_get_variable (store->dict, col);
91
92   if (NULL == var)
93     return;
94
95   union value v;
96
97   GVariant *vrnt = g_value_get_variant (value);
98
99   value_variant_get (&v, vrnt);
100
101   psppire_data_store_set_value (store, row, var, &v);
102
103   value_destroy_from_variant (&v, vrnt);
104 }
105
106 gboolean myreversefunc (GtkTreeModel *model, gint col, gint row, const gchar *in,
107                     GValue *out);
108
109 \f
110
111 static void
112 show_cases_row_popup (PsppireDataSheet *sheet, int row,
113                       uint button, uint state, gpointer p)
114 {
115   GListModel *vmodel = NULL;
116   g_object_get (sheet, "vmodel", &vmodel, NULL);
117   if (vmodel == NULL)
118     return;
119
120   guint n_items = g_list_model_get_n_items (vmodel);
121
122   if (row >= n_items)
123     return;
124
125   if (button != 3)
126     return;
127
128   g_object_set_data (G_OBJECT (sheet->data_sheet_cases_row_popup), "item",
129                      GINT_TO_POINTER (row));
130
131   gtk_menu_popup_at_pointer (GTK_MENU (sheet->data_sheet_cases_row_popup), NULL);
132 }
133
134
135 static void
136 insert_new_case (PsppireDataSheet *sheet)
137 {
138   PsppireDataStore *data_store = NULL;
139   g_object_get (sheet, "data-model", &data_store, NULL);
140
141   gint posn = GPOINTER_TO_INT (g_object_get_data
142                                 (G_OBJECT (sheet->data_sheet_cases_row_popup), "item"));
143
144   psppire_data_store_insert_new_case (data_store, posn);
145
146   gtk_widget_queue_draw (GTK_WIDGET (sheet));
147 }
148
149 static void
150 delete_cases (PsppireDataSheet *sheet)
151 {
152   SswRange *range = SSW_SHEET(sheet)->selection;
153
154   PsppireDataStore *data_store = NULL;
155   g_object_get (sheet, "data-model", &data_store, NULL);
156
157   psppire_data_store_delete_cases (data_store, range->start_y,
158                                    range->end_y - range->start_y + 1);
159
160   gtk_widget_queue_draw (GTK_WIDGET (sheet));
161 }
162
163 static GtkWidget *
164 create_data_row_header_popup_menu (PsppireDataSheet *sheet)
165 {
166   GtkWidget *menu = gtk_menu_new ();
167
168   GtkWidget *item =
169     gtk_menu_item_new_with_mnemonic  (_("_Insert Case"));
170
171   g_signal_connect_swapped (item, "activate", G_CALLBACK (insert_new_case), sheet);
172   gtk_menu_shell_append (GTK_MENU_SHELL (menu), item);
173
174   item = gtk_separator_menu_item_new ();
175   gtk_menu_shell_append (GTK_MENU_SHELL (menu), item);
176
177   sheet->data_clear_cases_menu_item = gtk_menu_item_new_with_mnemonic (_("Cl_ear Cases"));
178   gtk_widget_set_sensitive (sheet->data_clear_cases_menu_item, FALSE);
179   gtk_menu_shell_append (GTK_MENU_SHELL (menu), sheet->data_clear_cases_menu_item);
180   g_signal_connect_swapped (sheet->data_clear_cases_menu_item, "activate",
181                             G_CALLBACK (delete_cases), sheet);
182
183   gtk_widget_show_all (menu);
184   return menu;
185 }
186
187
188 static void
189 show_cases_column_popup (PsppireDataSheet *sheet, int column, uint button, uint state,
190                          gpointer p)
191 {
192   GListModel *hmodel = NULL;
193   g_object_get (sheet, "hmodel", &hmodel, NULL);
194   if (hmodel == NULL)
195     return;
196
197   guint n_items = g_list_model_get_n_items (hmodel);
198
199   if (column >= n_items)
200     return;
201
202   if (button != 3)
203     return;
204
205   g_object_set_data (G_OBJECT (sheet->data_sheet_cases_column_popup), "item",
206                      GINT_TO_POINTER (column));
207
208   gtk_menu_popup_at_pointer (GTK_MENU (sheet->data_sheet_cases_column_popup), NULL);
209 }
210
211 static void
212 insert_new_variable (PsppireDataSheet *sheet)
213 {
214   PsppireDataStore *data_store = NULL;
215   g_object_get (sheet, "data-model", &data_store, NULL);
216
217   gint posn = GPOINTER_TO_INT (g_object_get_data
218                                 (G_OBJECT (sheet->data_sheet_cases_column_popup),
219                                  "item"));
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   gtk_widget_queue_draw (GTK_WIDGET (sheet));
228 }
229
230 static void
231 set_menu_items_sensitivity (PsppireDataSheet *sheet, gpointer selection, gpointer p)
232 {
233   SswRange *range = selection;
234
235   PsppireDataStore *data_store = NULL;
236   g_object_get (sheet, "data-model", &data_store, NULL);
237
238
239   gint width = gtk_tree_model_get_n_columns (GTK_TREE_MODEL (data_store));
240   gint length = psppire_data_store_get_case_count (data_store);
241
242
243   gboolean whole_row_selected = (range->start_x == 0 && range->end_x == width - 1);
244   gtk_widget_set_sensitive (sheet->data_clear_cases_menu_item, whole_row_selected);
245
246   gboolean whole_column_selected =
247     (range->start_y == 0 && range->end_y == length - 1);
248   gtk_widget_set_sensitive (sheet->data_clear_variables_menu_item,
249                             whole_column_selected);
250   gtk_widget_set_sensitive (sheet->data_sort_ascending_menu_item,
251                             whole_column_selected);
252   gtk_widget_set_sensitive (sheet->data_sort_descending_menu_item,
253                             whole_column_selected);
254 }
255
256 static void
257 delete_variables (PsppireDataSheet *sheet)
258 {
259   SswRange *range = SSW_SHEET(sheet)->selection;
260
261   PsppireDataStore *data_store = NULL;
262   g_object_get (sheet, "data-model", &data_store, NULL);
263
264   psppire_dict_delete_variables (data_store->dict, range->start_x,
265                                  (range->end_x - range->start_x + 1));
266
267   gtk_widget_queue_draw (GTK_WIDGET (sheet));
268 }
269
270
271
272 static GtkWidget *
273 create_data_column_header_popup_menu (PsppireDataSheet *sheet)
274 {
275   GtkWidget *menu = gtk_menu_new ();
276
277   GtkWidget *item =
278     gtk_menu_item_new_with_mnemonic  (_("_Insert Variable"));
279   g_signal_connect_swapped (item, "activate", G_CALLBACK (insert_new_variable),
280                             sheet);
281   gtk_menu_shell_append (GTK_MENU_SHELL (menu), item);
282
283   item = gtk_separator_menu_item_new ();
284   gtk_menu_shell_append (GTK_MENU_SHELL (menu), item);
285
286   sheet->data_clear_variables_menu_item =
287     gtk_menu_item_new_with_mnemonic  (_("Cl_ear Variables"));
288   g_signal_connect_swapped (sheet->data_clear_variables_menu_item, "activate",
289                             G_CALLBACK (delete_variables),
290                             sheet);
291   gtk_widget_set_sensitive (sheet->data_clear_variables_menu_item, FALSE);
292   gtk_menu_shell_append (GTK_MENU_SHELL (menu), sheet->data_clear_variables_menu_item);
293
294   item = gtk_separator_menu_item_new ();
295   gtk_menu_shell_append (GTK_MENU_SHELL (menu), item);
296
297
298   sheet->data_sort_ascending_menu_item =
299     gtk_menu_item_new_with_mnemonic (_("Sort _Ascending"));
300   g_signal_connect_swapped (sheet->data_sort_ascending_menu_item, "activate",
301                             G_CALLBACK (sort_ascending), sheet);
302   gtk_widget_set_sensitive (sheet->data_sort_ascending_menu_item, FALSE);
303   gtk_menu_shell_append (GTK_MENU_SHELL (menu), sheet->data_sort_ascending_menu_item);
304
305   sheet->data_sort_descending_menu_item =
306     gtk_menu_item_new_with_mnemonic (_("Sort _Descending"));
307   g_signal_connect_swapped (sheet->data_sort_descending_menu_item, "activate",
308                             G_CALLBACK (sort_descending), sheet);
309   gtk_widget_set_sensitive (sheet->data_sort_descending_menu_item, FALSE);
310   gtk_menu_shell_append (GTK_MENU_SHELL (menu), sheet->data_sort_descending_menu_item);
311
312   gtk_widget_show_all (menu);
313   return menu;
314 }
315
316
317 \f
318
319 G_DEFINE_TYPE (PsppireDataSheet, psppire_data_sheet, SSW_TYPE_SHEET)
320
321 static GObjectClass * parent_class = NULL;
322 static gboolean dispose_has_run = FALSE;
323
324 static void
325 psppire_data_sheet_dispose (GObject *obj)
326 {
327   //  PsppireDataSheet *sheet = PSPPIRE_DATA_SHEET (obj);
328
329   if (dispose_has_run)
330     return;
331
332   dispose_has_run = TRUE;
333
334   /* Chain up to the parent class */
335   G_OBJECT_CLASS (parent_class)->dispose (obj);
336 }
337
338 static void
339 psppire_data_sheet_class_init (PsppireDataSheetClass *class)
340 {
341   GObjectClass *object_class = G_OBJECT_CLASS (class);
342   object_class->dispose = psppire_data_sheet_dispose;
343
344   parent_class = g_type_class_peek_parent (class);
345 }
346
347 GtkWidget*
348 psppire_data_sheet_new (void)
349 {
350   GObject *obj =
351     g_object_new (PSPPIRE_TYPE_DATA_SHEET,
352                   "forward-conversion", psppire_data_store_value_to_string,
353                   "reverse-conversion", myreversefunc,
354                   "editable", TRUE,
355                   NULL);
356
357   return GTK_WIDGET (obj);
358 }
359
360
361 static gboolean
362 indicate_filtered_case (GtkWidget *widget, cairo_t *cr, PsppireDataStore *store)
363 {
364   guint row = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (widget), "row"));
365
366   if (!psppire_data_store_filtered (store, row))
367     return FALSE;
368
369   /* Draw a diagonal line through the widget */
370   guint width = gtk_widget_get_allocated_width (widget);
371   guint height = gtk_widget_get_allocated_height (widget);
372
373   GtkStyleContext *sc = gtk_widget_get_style_context (widget);
374   gtk_render_line (sc, cr, 0, 0, width, height);
375
376   return FALSE;
377 }
378
379 static void
380 button_post_create (GtkWidget *button, uint i, gpointer user_data)
381 {
382   PsppireDataStore *data_store = PSPPIRE_DATA_STORE (user_data);
383
384   g_object_set_data (G_OBJECT (button), "row", GUINT_TO_POINTER (i));
385   g_signal_connect_after (button, "draw", G_CALLBACK (indicate_filtered_case), data_store);
386 }
387
388 static void
389 set_dictionary (PsppireDataSheet *sheet)
390 {
391   GtkTreeModel *data_model = NULL;
392   g_object_get (sheet, "data-model", &data_model, NULL);
393
394   PsppireDataStore *store = PSPPIRE_DATA_STORE (data_model);
395   g_object_set (sheet, "hmodel", store->dict, NULL);
396
397
398   SswAxisModel *vmodel = NULL;
399   g_object_get (sheet, "vmodel", &vmodel, NULL);
400   g_assert (SSW_IS_AXIS_MODEL (vmodel));
401
402   g_object_set (vmodel,
403                 "post-button-create-func", button_post_create,
404                 "post-button-create-func-data", store,
405                 NULL);
406 }
407
408 static void
409 move_variable (PsppireDataSheet *sheet, gint from, gint to, gpointer ud)
410 {
411   PsppireDataStore *data_store = NULL;
412   g_object_get (sheet, "data-model", &data_store, NULL);
413
414   if (data_store == NULL)
415     return;
416
417   PsppireDict *dict = data_store->dict;
418   struct variable *var = psppire_dict_get_variable (dict, from);
419
420   if (var == NULL)
421     return;
422   gint new_pos = to;
423   /* The index refers to the final position, so if the source
424      is less than the destination, then we must subtract 1, to
425      account for the position vacated by the source */
426   if (from < to)
427     new_pos--;
428   dict_reorder_var (dict->dict, var, new_pos);
429 }
430
431 static void
432 psppire_data_sheet_init (PsppireDataSheet *sheet)
433 {
434   sheet->data_sheet_cases_column_popup =
435     create_data_column_header_popup_menu (sheet);
436
437   sheet->data_sheet_cases_row_popup =
438     create_data_row_header_popup_menu (sheet);
439
440   g_signal_connect (sheet, "selection-changed",
441                     G_CALLBACK (set_menu_items_sensitivity), sheet);
442
443   g_signal_connect (sheet, "column-header-pressed",
444                     G_CALLBACK (show_cases_column_popup), sheet);
445
446   g_signal_connect (sheet, "row-header-pressed",
447                     G_CALLBACK (show_cases_row_popup), sheet);
448
449   g_signal_connect (sheet, "value-changed",
450                     G_CALLBACK (change_data_value), NULL);
451
452   g_signal_connect (sheet, "notify::data-model",
453                     G_CALLBACK (set_dictionary), NULL);
454
455   g_signal_connect (sheet, "column-moved", G_CALLBACK (move_variable), NULL);
456 }