Set the variable display width instead of adding an override.
[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 #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 gboolean myreversefunc (GtkTreeModel *model, gint col, gint row, const gchar *in,
108                     GValue *out);
109
110 \f
111
112 static void
113 show_cases_row_popup (PsppireDataSheet *sheet, int row,
114                       guint button, guint state, gpointer p)
115 {
116   GListModel *vmodel = NULL;
117   g_object_get (sheet, "vmodel", &vmodel, NULL);
118   if (vmodel == NULL)
119     return;
120
121   guint n_items = g_list_model_get_n_items (vmodel);
122
123   if (row >= n_items)
124     return;
125
126   if (button != 3)
127     return;
128
129   g_object_set_data (G_OBJECT (sheet->data_sheet_cases_row_popup), "item",
130                      GINT_TO_POINTER (row));
131
132   gtk_menu_popup_at_pointer (GTK_MENU (sheet->data_sheet_cases_row_popup), NULL);
133 }
134
135
136 static void
137 insert_new_case (PsppireDataSheet *sheet)
138 {
139   PsppireDataStore *data_store = NULL;
140   g_object_get (sheet, "data-model", &data_store, NULL);
141
142   gint posn = GPOINTER_TO_INT (g_object_get_data
143                                 (G_OBJECT (sheet->data_sheet_cases_row_popup), "item"));
144
145   psppire_data_store_insert_new_case (data_store, posn);
146
147   gtk_widget_queue_draw (GTK_WIDGET (sheet));
148 }
149
150 static void
151 delete_cases (PsppireDataSheet *sheet)
152 {
153   SswRange *range = SSW_SHEET(sheet)->selection;
154
155   PsppireDataStore *data_store = NULL;
156   g_object_get (sheet, "data-model", &data_store, NULL);
157
158   psppire_data_store_delete_cases (data_store, range->start_y,
159                                    range->end_y - range->start_y + 1);
160
161   gtk_widget_queue_draw (GTK_WIDGET (sheet));
162 }
163
164 static GtkWidget *
165 create_data_row_header_popup_menu (PsppireDataSheet *sheet)
166 {
167   GtkWidget *menu = gtk_menu_new ();
168
169   GtkWidget *item =
170     gtk_menu_item_new_with_mnemonic  (_("_Insert Case"));
171
172   g_signal_connect_swapped (item, "activate", G_CALLBACK (insert_new_case), sheet);
173   gtk_menu_shell_append (GTK_MENU_SHELL (menu), item);
174
175   item = gtk_separator_menu_item_new ();
176   gtk_menu_shell_append (GTK_MENU_SHELL (menu), item);
177
178   sheet->data_clear_cases_menu_item = gtk_menu_item_new_with_mnemonic (_("Cl_ear Cases"));
179   gtk_widget_set_sensitive (sheet->data_clear_cases_menu_item, FALSE);
180   gtk_menu_shell_append (GTK_MENU_SHELL (menu), sheet->data_clear_cases_menu_item);
181   g_signal_connect_swapped (sheet->data_clear_cases_menu_item, "activate",
182                             G_CALLBACK (delete_cases), sheet);
183
184   gtk_widget_show_all (menu);
185   return menu;
186 }
187
188
189 static void
190 show_cases_column_popup (PsppireDataSheet *sheet, int column, guint button, guint state,
191                          gpointer p)
192 {
193   GListModel *hmodel = NULL;
194   g_object_get (sheet, "hmodel", &hmodel, NULL);
195   if (hmodel == NULL)
196     return;
197
198   guint n_items = g_list_model_get_n_items (hmodel);
199
200   if (column >= n_items)
201     return;
202
203   if (button != 3)
204     return;
205
206   g_object_set_data (G_OBJECT (sheet->data_sheet_cases_column_popup), "item",
207                      GINT_TO_POINTER (column));
208
209   gtk_menu_popup_at_pointer (GTK_MENU (sheet->data_sheet_cases_column_popup), NULL);
210 }
211
212 static void
213 insert_new_variable (PsppireDataSheet *sheet)
214 {
215   PsppireDataStore *data_store = NULL;
216   g_object_get (sheet, "data-model", &data_store, NULL);
217
218   gint posn = GPOINTER_TO_INT (g_object_get_data
219                                 (G_OBJECT (sheet->data_sheet_cases_column_popup),
220                                  "item"));
221
222   const struct variable *v = psppire_dict_insert_variable (data_store->dict,
223                                                            posn, NULL);
224
225   psppire_data_store_insert_value (data_store, var_get_width(v),
226                                    var_get_case_index (v));
227
228   gtk_widget_queue_draw (GTK_WIDGET (sheet));
229 }
230
231 static void
232 set_menu_items_sensitivity (PsppireDataSheet *sheet, gpointer selection, gpointer p)
233 {
234   SswRange *range = selection;
235
236   PsppireDataStore *data_store = NULL;
237   g_object_get (sheet, "data-model", &data_store, NULL);
238
239
240   gint width = gtk_tree_model_get_n_columns (GTK_TREE_MODEL (data_store));
241   gint length = psppire_data_store_get_case_count (data_store);
242
243
244   gboolean whole_row_selected = (range->start_x == 0 && range->end_x == width - 1);
245   gtk_widget_set_sensitive (sheet->data_clear_cases_menu_item, whole_row_selected);
246
247   gboolean whole_column_selected =
248     (range->start_y == 0 && range->end_y == length - 1);
249   gtk_widget_set_sensitive (sheet->data_clear_variables_menu_item,
250                             whole_column_selected);
251   gtk_widget_set_sensitive (sheet->data_sort_ascending_menu_item,
252                             whole_column_selected);
253   gtk_widget_set_sensitive (sheet->data_sort_descending_menu_item,
254                             whole_column_selected);
255 }
256
257 static void
258 delete_variables (PsppireDataSheet *sheet)
259 {
260   SswRange *range = SSW_SHEET(sheet)->selection;
261
262   PsppireDataStore *data_store = NULL;
263   g_object_get (sheet, "data-model", &data_store, NULL);
264
265   psppire_dict_delete_variables (data_store->dict, range->start_x,
266                                  (range->end_x - range->start_x + 1));
267
268   gtk_widget_queue_draw (GTK_WIDGET (sheet));
269 }
270
271
272
273 static GtkWidget *
274 create_data_column_header_popup_menu (PsppireDataSheet *sheet)
275 {
276   GtkWidget *menu = gtk_menu_new ();
277
278   GtkWidget *item =
279     gtk_menu_item_new_with_mnemonic  (_("_Insert Variable"));
280   g_signal_connect_swapped (item, "activate", G_CALLBACK (insert_new_variable),
281                             sheet);
282   gtk_menu_shell_append (GTK_MENU_SHELL (menu), item);
283
284   item = gtk_separator_menu_item_new ();
285   gtk_menu_shell_append (GTK_MENU_SHELL (menu), item);
286
287   sheet->data_clear_variables_menu_item =
288     gtk_menu_item_new_with_mnemonic  (_("Cl_ear Variables"));
289   g_signal_connect_swapped (sheet->data_clear_variables_menu_item, "activate",
290                             G_CALLBACK (delete_variables),
291                             sheet);
292   gtk_widget_set_sensitive (sheet->data_clear_variables_menu_item, FALSE);
293   gtk_menu_shell_append (GTK_MENU_SHELL (menu), sheet->data_clear_variables_menu_item);
294
295   item = gtk_separator_menu_item_new ();
296   gtk_menu_shell_append (GTK_MENU_SHELL (menu), item);
297
298
299   sheet->data_sort_ascending_menu_item =
300     gtk_menu_item_new_with_mnemonic (_("Sort _Ascending"));
301   g_signal_connect_swapped (sheet->data_sort_ascending_menu_item, "activate",
302                             G_CALLBACK (sort_ascending), sheet);
303   gtk_widget_set_sensitive (sheet->data_sort_ascending_menu_item, FALSE);
304   gtk_menu_shell_append (GTK_MENU_SHELL (menu), sheet->data_sort_ascending_menu_item);
305
306   sheet->data_sort_descending_menu_item =
307     gtk_menu_item_new_with_mnemonic (_("Sort _Descending"));
308   g_signal_connect_swapped (sheet->data_sort_descending_menu_item, "activate",
309                             G_CALLBACK (sort_descending), sheet);
310   gtk_widget_set_sensitive (sheet->data_sort_descending_menu_item, FALSE);
311   gtk_menu_shell_append (GTK_MENU_SHELL (menu), sheet->data_sort_descending_menu_item);
312
313   gtk_widget_show_all (menu);
314   return menu;
315 }
316
317
318 \f
319
320 G_DEFINE_TYPE (PsppireDataSheet, psppire_data_sheet, SSW_TYPE_SHEET)
321
322 static GObjectClass * parent_class = NULL;
323 static gboolean dispose_has_run = FALSE;
324
325 static void
326 psppire_data_sheet_dispose (GObject *obj)
327 {
328   //  PsppireDataSheet *sheet = PSPPIRE_DATA_SHEET (obj);
329
330   if (dispose_has_run)
331     return;
332
333   dispose_has_run = TRUE;
334
335   /* Chain up to the parent class */
336   G_OBJECT_CLASS (parent_class)->dispose (obj);
337 }
338
339 static void
340 psppire_data_sheet_class_init (PsppireDataSheetClass *class)
341 {
342   GObjectClass *object_class = G_OBJECT_CLASS (class);
343   object_class->dispose = psppire_data_sheet_dispose;
344
345   parent_class = g_type_class_peek_parent (class);
346 }
347
348 GtkWidget*
349 psppire_data_sheet_new (void)
350 {
351   GObject *obj =
352     g_object_new (PSPPIRE_TYPE_DATA_SHEET,
353                   "forward-conversion", psppire_data_store_value_to_string,
354                   "reverse-conversion", myreversefunc,
355                   "editable", TRUE,
356                   "horizontal-draggable", TRUE,
357                   NULL);
358
359   return GTK_WIDGET (obj);
360 }
361
362
363 static gboolean
364 indicate_filtered_case (GtkWidget *widget, cairo_t *cr, PsppireDataStore *store)
365 {
366   guint row = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (widget), "row"));
367
368   if (!psppire_data_store_filtered (store, row))
369     return FALSE;
370
371   /* Draw a diagonal line through the widget */
372   guint width = gtk_widget_get_allocated_width (widget);
373   guint height = gtk_widget_get_allocated_height (widget);
374
375   GtkStyleContext *sc = gtk_widget_get_style_context (widget);
376   gtk_render_line (sc, cr, 0, 0, width, height);
377
378   return FALSE;
379 }
380
381 static void
382 button_post_create (GtkWidget *button, guint i, gpointer user_data)
383 {
384   PsppireDataStore *data_store = PSPPIRE_DATA_STORE (user_data);
385
386   g_object_set_data (G_OBJECT (button), "row", GUINT_TO_POINTER (i));
387   g_signal_connect_after (button, "draw", G_CALLBACK (indicate_filtered_case), data_store);
388 }
389
390
391 static gboolean
392 resize_display_width (PsppireDict *dict, gint pos, gint size, gpointer user_data)
393 {
394   if (pos < 0)
395     return FALSE;
396
397   PsppireDataSheet *sheet = PSPPIRE_DATA_SHEET (user_data);
398   PangoContext *context = gtk_widget_create_pango_context (GTK_WIDGET (sheet));
399   PangoLayout *layout = pango_layout_new (context);
400   PangoRectangle rect;
401   
402   pango_layout_set_text (layout, "M", 1);
403   pango_layout_get_extents (layout, NULL, &rect);
404   
405   gdouble width_of_M = rect.width / (gdouble) PANGO_SCALE;
406   
407   g_object_unref (G_OBJECT (layout));
408   g_object_unref (G_OBJECT (context));
409   
410   gint Ms = round ((size / width_of_M) - 0.25);
411   struct variable *var = psppire_dict_get_variable (dict, pos);
412   g_return_val_if_fail (var, TRUE);
413   var_set_display_width (var, Ms);
414   return TRUE;
415 }
416
417 static void
418 set_dictionary (PsppireDataSheet *sheet)
419 {
420   GtkTreeModel *data_model = NULL;
421   g_object_get (sheet, "data-model", &data_model, NULL);
422
423   PsppireDataStore *store = PSPPIRE_DATA_STORE (data_model);
424   g_object_set (sheet, "hmodel", store->dict, NULL);
425
426   g_signal_connect (store->dict, "resize-item", G_CALLBACK (resize_display_width),
427                     sheet);
428
429   SswAxisModel *vmodel = NULL;
430   g_object_get (sheet, "vmodel", &vmodel, NULL);
431   g_assert (SSW_IS_AXIS_MODEL (vmodel));
432
433   g_object_set (vmodel,
434                 "post-button-create-func", button_post_create,
435                 "post-button-create-func-data", store,
436                 NULL);
437 }
438
439 static void
440 move_variable (PsppireDataSheet *sheet, gint from, gint to, gpointer ud)
441 {
442   PsppireDataStore *data_store = NULL;
443   g_object_get (sheet, "data-model", &data_store, NULL);
444
445   if (data_store == NULL)
446     return;
447
448   PsppireDict *dict = data_store->dict;
449   struct variable *var = psppire_dict_get_variable (dict, from);
450
451   if (var == NULL)
452     return;
453   gint new_pos = to;
454   /* The index refers to the final position, so if the source
455      is less than the destination, then we must subtract 1, to
456      account for the position vacated by the source */
457   if (from < to)
458     new_pos--;
459   dict_reorder_var (dict->dict, var, new_pos);
460 }
461
462 static void
463 psppire_data_sheet_init (PsppireDataSheet *sheet)
464 {
465   sheet->data_sheet_cases_column_popup =
466     create_data_column_header_popup_menu (sheet);
467
468   sheet->data_sheet_cases_row_popup =
469     create_data_row_header_popup_menu (sheet);
470
471   g_signal_connect (sheet, "selection-changed",
472                     G_CALLBACK (set_menu_items_sensitivity), sheet);
473
474   g_signal_connect (sheet, "column-header-pressed",
475                     G_CALLBACK (show_cases_column_popup), sheet);
476
477   g_signal_connect (sheet, "row-header-pressed",
478                     G_CALLBACK (show_cases_row_popup), sheet);
479
480   g_signal_connect (sheet, "value-changed",
481                     G_CALLBACK (change_data_value), NULL);
482
483   g_signal_connect (sheet, "notify::data-model",
484                     G_CALLBACK (set_dictionary), NULL);
485
486   g_signal_connect (sheet, "column-moved", G_CALLBACK (move_variable), NULL);
487 }