Disable drag and drop on sheet axes for which there is no connected signal
[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                   "horizontal-draggable", TRUE,
356                   NULL);
357
358   return GTK_WIDGET (obj);
359 }
360
361
362 static gboolean
363 indicate_filtered_case (GtkWidget *widget, cairo_t *cr, PsppireDataStore *store)
364 {
365   guint row = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (widget), "row"));
366
367   if (!psppire_data_store_filtered (store, row))
368     return FALSE;
369
370   /* Draw a diagonal line through the widget */
371   guint width = gtk_widget_get_allocated_width (widget);
372   guint height = gtk_widget_get_allocated_height (widget);
373
374   GtkStyleContext *sc = gtk_widget_get_style_context (widget);
375   gtk_render_line (sc, cr, 0, 0, width, height);
376
377   return FALSE;
378 }
379
380 static void
381 button_post_create (GtkWidget *button, uint i, gpointer user_data)
382 {
383   PsppireDataStore *data_store = PSPPIRE_DATA_STORE (user_data);
384
385   g_object_set_data (G_OBJECT (button), "row", GUINT_TO_POINTER (i));
386   g_signal_connect_after (button, "draw", G_CALLBACK (indicate_filtered_case), data_store);
387 }
388
389 static void
390 set_dictionary (PsppireDataSheet *sheet)
391 {
392   GtkTreeModel *data_model = NULL;
393   g_object_get (sheet, "data-model", &data_model, NULL);
394
395   PsppireDataStore *store = PSPPIRE_DATA_STORE (data_model);
396   g_object_set (sheet, "hmodel", store->dict, NULL);
397
398
399   SswAxisModel *vmodel = NULL;
400   g_object_get (sheet, "vmodel", &vmodel, NULL);
401   g_assert (SSW_IS_AXIS_MODEL (vmodel));
402
403   g_object_set (vmodel,
404                 "post-button-create-func", button_post_create,
405                 "post-button-create-func-data", store,
406                 NULL);
407 }
408
409 static void
410 move_variable (PsppireDataSheet *sheet, gint from, gint to, gpointer ud)
411 {
412   PsppireDataStore *data_store = NULL;
413   g_object_get (sheet, "data-model", &data_store, NULL);
414
415   if (data_store == NULL)
416     return;
417
418   PsppireDict *dict = data_store->dict;
419   struct variable *var = psppire_dict_get_variable (dict, from);
420
421   if (var == NULL)
422     return;
423   gint new_pos = to;
424   /* The index refers to the final position, so if the source
425      is less than the destination, then we must subtract 1, to
426      account for the position vacated by the source */
427   if (from < to)
428     new_pos--;
429   dict_reorder_var (dict->dict, var, new_pos);
430 }
431
432 static void
433 psppire_data_sheet_init (PsppireDataSheet *sheet)
434 {
435   sheet->data_sheet_cases_column_popup =
436     create_data_column_header_popup_menu (sheet);
437
438   sheet->data_sheet_cases_row_popup =
439     create_data_row_header_popup_menu (sheet);
440
441   g_signal_connect (sheet, "selection-changed",
442                     G_CALLBACK (set_menu_items_sensitivity), sheet);
443
444   g_signal_connect (sheet, "column-header-pressed",
445                     G_CALLBACK (show_cases_column_popup), sheet);
446
447   g_signal_connect (sheet, "row-header-pressed",
448                     G_CALLBACK (show_cases_row_popup), sheet);
449
450   g_signal_connect (sheet, "value-changed",
451                     G_CALLBACK (change_data_value), NULL);
452
453   g_signal_connect (sheet, "notify::data-model",
454                     G_CALLBACK (set_dictionary), NULL);
455
456   g_signal_connect (sheet, "column-moved", G_CALLBACK (move_variable), NULL);
457 }