Fail more gracefully when selecting cells out of range
[pspp] / src / ui / gui / psppire-data-editor.c
1 /* PSPPIRE - a graphical user interface for PSPP.
2    Copyright (C) 2008, 2009, 2010, 2011, 2012 Free Software Foundation, Inc.
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 #include <config.h>
18
19 #include "ui/gui/psppire-data-editor.h"
20
21 #include <gtk/gtk.h>
22 #include <gtk-contrib/gtkxpaned.h>
23
24 #include "data/datasheet.h"
25 #include "data/value-labels.h"
26 #include "libpspp/range-set.h"
27 #include "libpspp/str.h"
28 #include "ui/gui/helper.h"
29 #include "ui/gui/psppire-data-store.h"
30 #include "ui/gui/psppire-value-entry.h"
31 #include "ui/gui/psppire-conf.h"
32 #include "ui/gui/psppire-var-sheet-header.h"
33
34 #include "ui/gui/efficient-sheet/jmd-sheet.h"
35
36 #include <gettext.h>
37 #define _(msgid) gettext (msgid)
38
39
40 static GtkCellRenderer *
41 create_spin_renderer (GType type)
42 {
43   GtkCellRenderer *r = gtk_cell_renderer_spin_new ();
44       
45   GtkAdjustment *adj = gtk_adjustment_new (0,
46                                            0, G_MAXDOUBLE,
47                                            1, 1,
48                                            0);
49   g_object_set (r,
50                 "adjustment", adj,
51                 NULL);
52   
53   return r;
54 }
55
56 static GtkCellRenderer *
57 create_combo_renderer (GType type)
58 {
59   GtkListStore *list_store = gtk_list_store_new (2, G_TYPE_INT, G_TYPE_STRING);
60   
61   GEnumClass *ec = g_type_class_ref (type);
62   
63   const GEnumValue *ev ;
64   for (ev = ec->values; ev->value_name; ++ev)
65     {
66       GtkTreeIter iter;
67
68       gtk_list_store_append (list_store, &iter);
69
70       gtk_list_store_set (list_store, &iter,
71                           0, ev->value,
72                           1, gettext (ev->value_nick),
73                           -1);
74     }
75
76   GtkCellRenderer *r = gtk_cell_renderer_combo_new ();
77
78   g_object_set (r,
79                 "model", list_store,
80                 "text-column", 1,
81                 "has-entry", TRUE,
82                 NULL);
83
84   return r;
85 }
86
87 GtkCellRenderer *xx ;
88 GtkCellRenderer *column_width_renderer ;
89 GtkCellRenderer *measure_renderer ;
90 GtkCellRenderer *alignment_renderer ;
91
92
93
94 static GtkCellRenderer *
95 select_renderer_func (gint col, gint row, GType type)
96 {
97   if (!xx)
98     xx = create_spin_renderer (type);
99
100   if (col == DICT_TVM_COL_ROLE && !column_width_renderer)
101     column_width_renderer = create_combo_renderer (type);
102
103   if (col == DICT_TVM_COL_MEASURE && !measure_renderer)
104     measure_renderer = create_combo_renderer (type);
105
106   if (col == DICT_TVM_COL_ALIGNMENT && !alignment_renderer)
107     alignment_renderer = create_combo_renderer (type);
108
109   switch  (col)
110     {
111     case DICT_TVM_COL_WIDTH:
112     case DICT_TVM_COL_DECIMAL:
113     case DICT_TVM_COL_COLUMNS:
114       return xx;
115       
116     case DICT_TVM_COL_ALIGNMENT:
117       return alignment_renderer;
118
119     case DICT_TVM_COL_MEASURE:
120       return measure_renderer;
121       
122     case DICT_TVM_COL_ROLE:
123       return column_width_renderer;
124     }
125   
126   return NULL;
127 }
128
129
130 static void psppire_data_editor_class_init          (PsppireDataEditorClass *klass);
131 static void psppire_data_editor_init                (PsppireDataEditor      *de);
132
133 static void disconnect_data_sheets (PsppireDataEditor *);
134 static void refresh_entry (PsppireDataEditor *);
135
136 GType
137 psppire_data_editor_get_type (void)
138 {
139   static GType de_type = 0;
140
141   if (!de_type)
142     {
143       static const GTypeInfo de_info =
144       {
145         sizeof (PsppireDataEditorClass),
146         NULL, /* base_init */
147         NULL, /* base_finalize */
148         (GClassInitFunc) psppire_data_editor_class_init,
149         NULL, /* class_finalize */
150         NULL, /* class_data */
151         sizeof (PsppireDataEditor),
152         0,
153         (GInstanceInitFunc) psppire_data_editor_init,
154       };
155
156       de_type = g_type_register_static (GTK_TYPE_NOTEBOOK, "PsppireDataEditor",
157                                         &de_info, 0);
158     }
159
160   return de_type;
161 }
162
163 static GObjectClass * parent_class = NULL;
164
165 static void
166 psppire_data_editor_dispose (GObject *obj)
167 {
168   PsppireDataEditor *de = (PsppireDataEditor *) obj;
169
170   disconnect_data_sheets (de);
171
172   if (de->data_store)
173     {
174       g_object_unref (de->data_store);
175       de->data_store = NULL;
176     }
177
178   if (de->dict)
179     {
180       g_object_unref (de->dict);
181       de->dict = NULL;
182     }
183
184   if (de->font != NULL)
185     {
186       pango_font_description_free (de->font);
187       de->font = NULL;
188     }
189
190   /* Chain up to the parent class */
191   G_OBJECT_CLASS (parent_class)->dispose (obj);
192 }
193
194 enum
195   {
196     PROP_0,
197     PROP_DATA_STORE,
198     PROP_DICTIONARY,
199     PROP_VALUE_LABELS,
200     PROP_SPLIT_WINDOW
201   };
202
203 static void
204 psppire_data_editor_refresh_model (PsppireDataEditor *de)
205 {
206 }
207
208 static void
209 change_var_property (PsppireDict *dict, gint col, gint row, GValue *value)
210 {
211   /* Return the IDXth variable */
212   struct variable *var =  psppire_dict_get_variable (dict, row);
213
214   switch (col)
215     {
216     case DICT_TVM_COL_NAME:
217       dict_rename_var (dict->dict, var, g_value_get_string (value));
218       break;
219     case DICT_TVM_COL_LABEL:
220       var_set_label (var, g_value_get_string (value));
221       break;
222     case DICT_TVM_COL_COLUMNS:
223       var_set_display_width (var, g_value_get_int (value));
224       break;
225     case DICT_TVM_COL_MEASURE:
226       var_set_measure (var, g_value_get_enum (value));
227       break;
228     case DICT_TVM_COL_ALIGNMENT:
229       var_set_alignment (var, g_value_get_enum (value));
230       break;
231     case DICT_TVM_COL_ROLE:
232       var_set_role (var, g_value_get_enum (value));
233       break;
234     default:
235       g_message ("Changing col %d of var sheet not yet supported", col);
236       break;
237     }
238 }
239
240 static void
241 change_data_value (PsppireDataStore *store, gint col, gint row, GValue *value)
242 {
243   const struct variable *var = psppire_dict_get_variable (store->dict, col);
244
245   if (NULL == var)
246     return;
247
248   union value v;
249   value_init (&v, var_get_width (var));
250   v.f = g_value_get_double (value);
251   
252   psppire_data_store_set_value (store, row, var, &v);
253
254   value_destroy (&v, var_get_width (var));
255 }
256
257 static void
258 psppire_data_editor_set_property (GObject         *object,
259                                   guint            prop_id,
260                                   const GValue    *value,
261                                   GParamSpec      *pspec)
262 {
263   PsppireDataEditor *de = PSPPIRE_DATA_EDITOR (object);
264
265   switch (prop_id)
266     {
267     case PROP_SPLIT_WINDOW:
268       psppire_data_editor_split_window (de, g_value_get_boolean (value));
269       break;
270     case PROP_DATA_STORE:
271       if ( de->data_store)
272         {
273           g_signal_handlers_disconnect_by_func (de->data_store,
274                                                 G_CALLBACK (refresh_entry),
275                                                 de);
276           g_object_unref (de->data_store);
277         }
278
279       de->data_store = g_value_get_pointer (value);
280       g_object_ref (de->data_store);
281       g_print ("NEW STORE\n");
282
283       g_object_set (de->data_sheet, "data-model", de->data_store, NULL);
284       psppire_data_editor_refresh_model (de);
285
286       g_signal_connect_swapped (de->data_sheet, "value-changed",
287                                 G_CALLBACK (change_data_value), de->data_store);
288       
289       g_signal_connect_swapped (de->data_store, "case-changed",
290                                 G_CALLBACK (refresh_entry), de);
291
292       break;
293     case PROP_DICTIONARY:
294       if (de->dict)
295         g_object_unref (de->dict);
296       de->dict = g_value_get_pointer (value);
297       g_object_ref (de->dict);
298
299       g_object_set (de->data_sheet, "hmodel", de->dict, NULL);
300       g_object_set (de->var_sheet, "data-model", de->dict, NULL);
301       g_signal_connect_swapped (de->var_sheet, "value-changed",
302                                 G_CALLBACK (change_var_property), de->dict);
303
304       break;
305     case PROP_VALUE_LABELS:
306       break;
307
308     default:
309       G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
310       break;
311     };
312 }
313
314 static void
315 psppire_data_editor_get_property (GObject         *object,
316                                   guint            prop_id,
317                                   GValue          *value,
318                                   GParamSpec      *pspec)
319 {
320   PsppireDataEditor *de = PSPPIRE_DATA_EDITOR (object);
321
322   switch (prop_id)
323     {
324     case PROP_SPLIT_WINDOW:
325       g_value_set_boolean (value, de->split);
326       break;
327     case PROP_DATA_STORE:
328       g_value_set_pointer (value, de->data_store);
329       break;
330     case PROP_DICTIONARY:
331       g_value_set_pointer (value, de->dict);
332       break;
333     case PROP_VALUE_LABELS:
334       break;
335     default:
336       G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
337       break;
338     }
339 }
340
341 static void
342 psppire_data_editor_switch_page (GtkNotebook     *notebook,
343                                  GtkWidget *w,
344                                  guint            page_num)
345 {
346   GTK_NOTEBOOK_CLASS (parent_class)->switch_page (notebook, w, page_num);
347
348 }
349
350 static void
351 psppire_data_editor_set_focus_child (GtkContainer *container,
352                                      GtkWidget    *widget)
353 {
354   GTK_CONTAINER_CLASS (parent_class)->set_focus_child (container, widget);
355
356 }
357
358 static void
359 psppire_data_editor_class_init (PsppireDataEditorClass *klass)
360 {
361   GParamSpec *data_store_spec ;
362   GParamSpec *dict_spec ;
363   GParamSpec *value_labels_spec;
364   GParamSpec *split_window_spec;
365
366   GObjectClass *object_class = G_OBJECT_CLASS (klass);
367   GtkContainerClass *container_class = GTK_CONTAINER_CLASS (klass);
368   GtkNotebookClass *notebook_class = GTK_NOTEBOOK_CLASS (klass);
369
370   parent_class = g_type_class_peek_parent (klass);
371
372   object_class->dispose = psppire_data_editor_dispose;
373   object_class->set_property = psppire_data_editor_set_property;
374   object_class->get_property = psppire_data_editor_get_property;
375
376   container_class->set_focus_child = psppire_data_editor_set_focus_child;
377
378   notebook_class->switch_page = psppire_data_editor_switch_page;
379
380   data_store_spec =
381     g_param_spec_pointer ("data-store",
382                           "Data Store",
383                           "A pointer to the data store associated with this editor",
384                           G_PARAM_CONSTRUCT_ONLY | G_PARAM_WRITABLE | G_PARAM_READABLE );
385
386   g_object_class_install_property (object_class,
387                                    PROP_DATA_STORE,
388                                    data_store_spec);
389
390   dict_spec =
391     g_param_spec_pointer ("dictionary",
392                           "Dictionary",
393                           "A pointer to the dictionary associated with this editor",
394                           G_PARAM_CONSTRUCT_ONLY | G_PARAM_WRITABLE | G_PARAM_READABLE );
395
396   g_object_class_install_property (object_class,
397                                    PROP_DICTIONARY,
398                                    dict_spec);
399
400   value_labels_spec =
401     g_param_spec_boolean ("value-labels",
402                          "Value Labels",
403                          "Whether or not the data sheet should display labels instead of values",
404                           FALSE,
405                          G_PARAM_WRITABLE | G_PARAM_READABLE);
406
407   g_object_class_install_property (object_class,
408                                    PROP_VALUE_LABELS,
409                                    value_labels_spec);
410
411
412   split_window_spec =
413     g_param_spec_boolean ("split",
414                           "Split Window",
415                           "True iff the data sheet is split",
416                           FALSE,
417                           G_PARAM_READABLE | G_PARAM_WRITABLE);
418
419   g_object_class_install_property (object_class,
420                                    PROP_SPLIT_WINDOW,
421                                    split_window_spec);
422
423 }
424
425
426 static void
427 on_var_sheet_var_double_clicked (void *var_sheet, gint dict_index,
428                                  PsppireDataEditor *de)
429 {
430   gtk_notebook_set_current_page (GTK_NOTEBOOK (de),
431                                  PSPPIRE_DATA_EDITOR_DATA_VIEW);
432
433   jmd_sheet_scroll_to (JMD_SHEET (de->data_sheet), dict_index, -1);
434 }
435
436
437 static void
438 on_data_sheet_var_double_clicked (JmdSheet *data_sheet, gint dict_index,
439                                  PsppireDataEditor *de)
440 {
441   
442   gtk_notebook_set_current_page (GTK_NOTEBOOK (de),
443                                  PSPPIRE_DATA_EDITOR_VARIABLE_VIEW);
444
445   jmd_sheet_scroll_to (JMD_SHEET (de->var_sheet), -1, dict_index);
446 }
447
448
449
450 /* Refreshes 'de->cell_ref_label' and 'de->datum_entry' from the currently
451    active cell or cells. */
452 static void
453 refresh_entry (PsppireDataEditor *de)
454 {
455   g_print ("%s\n", __FUNCTION__);
456 }
457
458 static void
459 on_datum_entry_activate (PsppireValueEntry *entry, PsppireDataEditor *de)
460 {
461 }
462
463
464 static void
465 disconnect_data_sheets (PsppireDataEditor *de)
466 {
467 }
468
469 /* Called when the active cell or the selection in the data sheet changes */
470 static void
471 on_data_selection_change (PsppireDataEditor *de, JmdRange *sel)
472 {
473   gchar *ref_cell_text = NULL;
474
475   gint n_cases = abs (sel->end_y - sel->start_y) + 1;
476   gint n_vars = abs (sel->end_x - sel->start_x) + 1;
477
478   if (n_cases == 1 && n_vars == 1)
479     {
480       /* A single cell is selected */
481       const struct variable *var = psppire_dict_get_variable (de->dict, sel->start_x);
482
483       if (var)
484         ref_cell_text = g_strdup_printf (_("%d : %s"),
485                                          sel->start_y + 1, var_get_name (var));
486     }
487   else
488     {
489       struct string s;
490
491       /* The glib string library does not understand the ' printf modifier
492          on all platforms, but the "struct string" library does (because
493          Gnulib fixes that problem), so use the latter.  */
494       ds_init_empty (&s);
495       ds_put_format (&s, ngettext ("%'d case", "%'d cases", n_cases),
496                      n_cases);
497       ds_put_byte (&s, ' ');
498       ds_put_unichar (&s, 0xd7); /* U+00D7 MULTIPLICATION SIGN */
499       ds_put_byte (&s, ' ');
500       ds_put_format (&s, ngettext ("%'d variable", "%'d variables",
501                                    n_vars),
502                      n_vars);
503       ref_cell_text = ds_steal_cstr (&s);
504     }
505   
506   gtk_label_set_label (GTK_LABEL (de->cell_ref_label),
507                        ref_cell_text ? ref_cell_text : "");
508   
509   g_free (ref_cell_text);
510 }
511
512
513 static void set_font_recursively (GtkWidget *w, gpointer data);
514
515 static void
516 psppire_data_editor_init (PsppireDataEditor *de)
517 {
518   GtkWidget *hbox;
519   gchar *fontname = NULL;
520
521   de->font = NULL;
522   de->old_vbox_widget = NULL;
523
524   g_object_set (de, "tab-pos", GTK_POS_BOTTOM, NULL);
525
526   de->cell_ref_label = gtk_label_new ("");
527   gtk_label_set_width_chars (GTK_LABEL (de->cell_ref_label), 25);
528   gtk_widget_set_valign (de->cell_ref_label, GTK_ALIGN_CENTER);
529
530   de->datum_entry = psppire_value_entry_new ();
531   g_signal_connect (GTK_ENTRY (gtk_bin_get_child (GTK_BIN (de->datum_entry))),
532                     "activate", G_CALLBACK (on_datum_entry_activate), de);
533
534   hbox = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 0);
535   gtk_box_pack_start (GTK_BOX (hbox), de->cell_ref_label, FALSE, FALSE, 0);
536   gtk_box_pack_start (GTK_BOX (hbox), de->datum_entry, TRUE, TRUE, 0);
537
538   de->split = FALSE;
539   de->data_sheet = g_object_new (JMD_TYPE_SHEET, NULL);
540   GtkWidget *data_button = jmd_sheet_get_button (JMD_SHEET (de->data_sheet));
541   gtk_button_set_label (GTK_BUTTON (data_button), _("Case"));
542   de->vbox = gtk_box_new (GTK_ORIENTATION_VERTICAL, 0);
543   gtk_box_pack_start (GTK_BOX (de->vbox), hbox, FALSE, FALSE, 0);
544   gtk_box_pack_start (GTK_BOX (de->vbox), de->data_sheet, TRUE, TRUE, 0);
545
546
547   g_signal_connect_swapped (de->data_sheet, "selection-changed",
548                     G_CALLBACK (on_data_selection_change), de);
549   
550   gtk_notebook_append_page (GTK_NOTEBOOK (de), de->vbox,
551                             gtk_label_new_with_mnemonic (_("Data View")));
552
553   gtk_widget_show_all (de->vbox);
554
555   de->var_sheet = g_object_new (JMD_TYPE_SHEET, NULL);
556
557   PsppireVarSheetHeader *vsh = g_object_new (PSPPIRE_TYPE_VAR_SHEET_HEADER, NULL);
558   
559   g_object_set (de->var_sheet,
560                 "hmodel", vsh,
561                 "select-renderer-func", select_renderer_func,
562                 NULL);
563
564   
565   GtkWidget *var_button = jmd_sheet_get_button (JMD_SHEET (de->var_sheet));
566   gtk_button_set_label (GTK_BUTTON (var_button), _("Variable"));
567   
568   gtk_notebook_append_page (GTK_NOTEBOOK (de), de->var_sheet,
569                             gtk_label_new_with_mnemonic (_("Variable View")));
570
571   gtk_widget_show_all (de->var_sheet);
572   
573   g_signal_connect (de->var_sheet, "row-header-double-clicked",
574                     G_CALLBACK (on_var_sheet_var_double_clicked), de);
575
576   g_signal_connect (de->data_sheet, "column-header-double-clicked",
577                     G_CALLBACK (on_data_sheet_var_double_clicked), de);
578
579   g_object_set (de, "can-focus", FALSE, NULL);
580
581   if (psppire_conf_get_string (psppire_conf_new (),
582                            "Data Editor", "font",
583                                 &fontname) )
584     {
585       de->font = pango_font_description_from_string (fontname);
586       g_free (fontname);
587       set_font_recursively (GTK_WIDGET (de), de->font);
588     }
589
590 }
591
592 GtkWidget*
593 psppire_data_editor_new (PsppireDict *dict,
594                          PsppireDataStore *data_store)
595 {
596   return  g_object_new (PSPPIRE_DATA_EDITOR_TYPE,
597                         "dictionary",  dict,
598                         "data-store",  data_store,
599                         NULL);
600 }
601 \f
602 /* Turns the visible grid on or off, according to GRID_VISIBLE, for DE's data
603    sheet(s) and variable sheet. */
604 void
605 psppire_data_editor_show_grid (PsppireDataEditor *de, gboolean grid_visible)
606 {
607   g_object_set (JMD_SHEET (de->var_sheet), "gridlines", grid_visible, NULL);
608   g_object_set (JMD_SHEET (de->data_sheet), "gridlines", grid_visible, NULL);
609 }
610
611
612 static void
613 set_font_recursively (GtkWidget *w, gpointer data)
614 {
615   PangoFontDescription *font_desc = data;
616
617   gtk_widget_override_font (w, font_desc);
618
619   if ( GTK_IS_CONTAINER (w))
620     gtk_container_foreach (GTK_CONTAINER (w), set_font_recursively, font_desc);
621 }
622
623 /* Sets FONT_DESC as the font used by the data sheet(s) and variable sheet. */
624 void
625 psppire_data_editor_set_font (PsppireDataEditor *de, PangoFontDescription *font_desc)
626 {
627   gchar *font_name;
628   set_font_recursively (GTK_WIDGET (de), font_desc);
629
630   if (de->font)
631     pango_font_description_free (de->font);
632   de->font = pango_font_description_copy (font_desc);
633   font_name = pango_font_description_to_string (de->font);
634
635   psppire_conf_set_string (psppire_conf_new (),
636                            "Data Editor", "font",
637                            font_name);
638
639 }
640
641 /* If SPLIT is TRUE, splits DE's data sheet into four panes.
642    If SPLIT is FALSE, un-splits it into a single pane. */
643 void
644 psppire_data_editor_split_window (PsppireDataEditor *de, gboolean split)
645 {
646   if (split == de->split)
647     return;
648
649   disconnect_data_sheets (de);
650
651   psppire_data_editor_refresh_model (de);
652
653   gtk_widget_show_all (de->vbox);
654
655   if (de->font)
656     set_font_recursively (GTK_WIDGET (de), de->font);
657
658   de->split = split;
659   g_object_notify (G_OBJECT (de), "split");
660 }
661
662 /* Makes the variable with dictionary index DICT_INDEX in DE's dictionary
663    visible and selected in the active view in DE. */
664 void
665 psppire_data_editor_goto_variable (PsppireDataEditor *de, gint dict_index)
666 {
667 }
668
669 #if SHEET_MERGE
670 /* Returns the "active" data sheet in DE.  If DE is in single-paned mode, this
671    is the only data sheet.  If DE is in split mode (showing four data sheets),
672    this is the focused data sheet or, if none is focused, the data sheet with
673    selected cells or, if none has selected cells, the upper-left data sheet. */
674 PsppireDataSheet *
675 psppire_data_editor_get_active_data_sheet (PsppireDataEditor *de)
676 {
677   if (de->split)
678     {
679       PsppireDataSheet *data_sheet;
680       GtkWidget *scroller;
681       int i;
682
683       /* If one of the datasheet's scrollers is focused, choose that one. */
684       scroller = gtk_container_get_focus_child (
685         GTK_CONTAINER (de->datasheet_vbox_widget));
686       if (scroller != NULL)
687         return PSPPIRE_DATA_SHEET (gtk_bin_get_child (GTK_BIN (scroller)));
688
689       /* Otherwise if there's a nonempty selection in some data sheet, choose
690          that one. */
691       FOR_EACH_DATA_SHEET (data_sheet, i, de)
692         {
693           PsppSheetSelection *selection;
694
695           selection = pspp_sheet_view_get_selection (
696             PSPP_SHEET_VIEW (data_sheet));
697           if (pspp_sheet_selection_count_selected_rows (selection)
698               && pspp_sheet_selection_count_selected_columns (selection))
699             return data_sheet;
700         }
701     }
702
703   return PSPPIRE_DATA_SHEET (de->data_sheets[0]);
704 }
705 #endif