Merge remote-tracking branch 'origin/master' into sheet
[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   union value v;
246   value_init (&v, var_get_width (var));
247   v.f = g_value_get_double (value);
248   
249   psppire_data_store_set_value (store, row, var, &v);
250
251   value_destroy (&v, var_get_width (var));
252 }
253
254 static void
255 psppire_data_editor_set_property (GObject         *object,
256                                   guint            prop_id,
257                                   const GValue    *value,
258                                   GParamSpec      *pspec)
259 {
260   PsppireDataEditor *de = PSPPIRE_DATA_EDITOR (object);
261
262   switch (prop_id)
263     {
264     case PROP_SPLIT_WINDOW:
265       psppire_data_editor_split_window (de, g_value_get_boolean (value));
266       break;
267     case PROP_DATA_STORE:
268       if ( de->data_store)
269         {
270           g_signal_handlers_disconnect_by_func (de->data_store,
271                                                 G_CALLBACK (refresh_entry),
272                                                 de);
273           g_object_unref (de->data_store);
274         }
275
276       de->data_store = g_value_get_pointer (value);
277       g_object_ref (de->data_store);
278       g_print ("NEW STORE\n");
279
280       g_object_set (de->data_sheet, "data-model", de->data_store, NULL);
281       psppire_data_editor_refresh_model (de);
282
283       g_signal_connect_swapped (de->data_sheet, "value-changed",
284                                 G_CALLBACK (change_data_value), de->data_store);
285       
286       g_signal_connect_swapped (de->data_store, "case-changed",
287                                 G_CALLBACK (refresh_entry), de);
288
289       break;
290     case PROP_DICTIONARY:
291       if (de->dict)
292         g_object_unref (de->dict);
293       de->dict = g_value_get_pointer (value);
294       g_object_ref (de->dict);
295
296       g_object_set (de->data_sheet, "hmodel", de->dict, NULL);
297       g_object_set (de->var_sheet, "data-model", de->dict, NULL);
298       g_signal_connect_swapped (de->var_sheet, "value-changed",
299                                 G_CALLBACK (change_var_property), de->dict);
300
301       break;
302     case PROP_VALUE_LABELS:
303       break;
304
305     default:
306       G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
307       break;
308     };
309 }
310
311 static void
312 psppire_data_editor_get_property (GObject         *object,
313                                   guint            prop_id,
314                                   GValue          *value,
315                                   GParamSpec      *pspec)
316 {
317   PsppireDataEditor *de = PSPPIRE_DATA_EDITOR (object);
318
319   switch (prop_id)
320     {
321     case PROP_SPLIT_WINDOW:
322       g_value_set_boolean (value, de->split);
323       break;
324     case PROP_DATA_STORE:
325       g_value_set_pointer (value, de->data_store);
326       break;
327     case PROP_DICTIONARY:
328       g_value_set_pointer (value, de->dict);
329       break;
330     case PROP_VALUE_LABELS:
331       break;
332     default:
333       G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
334       break;
335     }
336 }
337
338 static void
339 psppire_data_editor_switch_page (GtkNotebook     *notebook,
340                                  GtkWidget *w,
341                                  guint            page_num)
342 {
343   GTK_NOTEBOOK_CLASS (parent_class)->switch_page (notebook, w, page_num);
344
345 }
346
347 static void
348 psppire_data_editor_set_focus_child (GtkContainer *container,
349                                      GtkWidget    *widget)
350 {
351   GTK_CONTAINER_CLASS (parent_class)->set_focus_child (container, widget);
352
353 }
354
355 static void
356 psppire_data_editor_class_init (PsppireDataEditorClass *klass)
357 {
358   GParamSpec *data_store_spec ;
359   GParamSpec *dict_spec ;
360   GParamSpec *value_labels_spec;
361   GParamSpec *split_window_spec;
362
363   GObjectClass *object_class = G_OBJECT_CLASS (klass);
364   GtkContainerClass *container_class = GTK_CONTAINER_CLASS (klass);
365   GtkNotebookClass *notebook_class = GTK_NOTEBOOK_CLASS (klass);
366
367   parent_class = g_type_class_peek_parent (klass);
368
369   object_class->dispose = psppire_data_editor_dispose;
370   object_class->set_property = psppire_data_editor_set_property;
371   object_class->get_property = psppire_data_editor_get_property;
372
373   container_class->set_focus_child = psppire_data_editor_set_focus_child;
374
375   notebook_class->switch_page = psppire_data_editor_switch_page;
376
377   data_store_spec =
378     g_param_spec_pointer ("data-store",
379                           "Data Store",
380                           "A pointer to the data store associated with this editor",
381                           G_PARAM_CONSTRUCT_ONLY | G_PARAM_WRITABLE | G_PARAM_READABLE );
382
383   g_object_class_install_property (object_class,
384                                    PROP_DATA_STORE,
385                                    data_store_spec);
386
387   dict_spec =
388     g_param_spec_pointer ("dictionary",
389                           "Dictionary",
390                           "A pointer to the dictionary associated with this editor",
391                           G_PARAM_CONSTRUCT_ONLY | G_PARAM_WRITABLE | G_PARAM_READABLE );
392
393   g_object_class_install_property (object_class,
394                                    PROP_DICTIONARY,
395                                    dict_spec);
396
397   value_labels_spec =
398     g_param_spec_boolean ("value-labels",
399                          "Value Labels",
400                          "Whether or not the data sheet should display labels instead of values",
401                           FALSE,
402                          G_PARAM_WRITABLE | G_PARAM_READABLE);
403
404   g_object_class_install_property (object_class,
405                                    PROP_VALUE_LABELS,
406                                    value_labels_spec);
407
408
409   split_window_spec =
410     g_param_spec_boolean ("split",
411                           "Split Window",
412                           "True iff the data sheet is split",
413                           FALSE,
414                           G_PARAM_READABLE | G_PARAM_WRITABLE);
415
416   g_object_class_install_property (object_class,
417                                    PROP_SPLIT_WINDOW,
418                                    split_window_spec);
419
420 }
421
422
423 static void
424 on_var_sheet_var_double_clicked (void *var_sheet, gint dict_index,
425                                  PsppireDataEditor *de)
426 {
427   gtk_notebook_set_current_page (GTK_NOTEBOOK (de),
428                                  PSPPIRE_DATA_EDITOR_DATA_VIEW);
429
430   jmd_sheet_scroll_to (JMD_SHEET (de->data_sheet), dict_index, -1);
431 }
432
433
434 static void
435 on_data_sheet_var_double_clicked (JmdSheet *data_sheet, gint dict_index,
436                                  PsppireDataEditor *de)
437 {
438   
439   gtk_notebook_set_current_page (GTK_NOTEBOOK (de),
440                                  PSPPIRE_DATA_EDITOR_VARIABLE_VIEW);
441
442   jmd_sheet_scroll_to (JMD_SHEET (de->var_sheet), -1, dict_index);
443 }
444
445
446
447 /* Refreshes 'de->cell_ref_label' and 'de->datum_entry' from the currently
448    active cell or cells. */
449 static void
450 refresh_entry (PsppireDataEditor *de)
451 {
452   g_print ("%s\n", __FUNCTION__);
453 }
454
455 static void
456 on_datum_entry_activate (PsppireValueEntry *entry, PsppireDataEditor *de)
457 {
458 }
459
460
461 static void
462 disconnect_data_sheets (PsppireDataEditor *de)
463 {
464 }
465
466 /* Called when the active cell or the selection in the data sheet changes */
467 static void
468 on_data_selection_change (PsppireDataEditor *de, JmdRange *sel)
469 {
470   gchar *ref_cell_text = NULL;
471
472   gint n_cases = abs (sel->end_y - sel->start_y) + 1;
473   gint n_vars = abs (sel->end_x - sel->start_x) + 1;
474
475   if (n_cases == 1 && n_vars == 1)
476     {
477       /* A single cell is selected */
478       const struct variable *var = psppire_dict_get_variable (de->dict, sel->start_x);
479       
480       ref_cell_text = g_strdup_printf (_("%d : %s"),
481                                        sel->start_y + 1, var_get_name (var));
482     }
483   else
484     {
485       struct string s;
486
487       /* The glib string library does not understand the ' printf modifier
488          on all platforms, but the "struct string" library does (because
489          Gnulib fixes that problem), so use the latter.  */
490       ds_init_empty (&s);
491       ds_put_format (&s, ngettext ("%'d case", "%'d cases", n_cases),
492                      n_cases);
493       ds_put_byte (&s, ' ');
494       ds_put_unichar (&s, 0xd7); /* U+00D7 MULTIPLICATION SIGN */
495       ds_put_byte (&s, ' ');
496       ds_put_format (&s, ngettext ("%'d variable", "%'d variables",
497                                    n_vars),
498                      n_vars);
499       ref_cell_text = ds_steal_cstr (&s);
500     }
501   
502   gtk_label_set_label (GTK_LABEL (de->cell_ref_label),
503                        ref_cell_text ? ref_cell_text : "");
504   
505   g_free (ref_cell_text);
506 }
507
508
509 static void set_font_recursively (GtkWidget *w, gpointer data);
510
511 static void
512 psppire_data_editor_init (PsppireDataEditor *de)
513 {
514   GtkWidget *hbox;
515   gchar *fontname = NULL;
516
517   de->font = NULL;
518   de->old_vbox_widget = NULL;
519
520   g_object_set (de, "tab-pos", GTK_POS_BOTTOM, NULL);
521
522   de->cell_ref_label = gtk_label_new ("");
523   gtk_label_set_width_chars (GTK_LABEL (de->cell_ref_label), 25);
524   gtk_widget_set_valign (de->cell_ref_label, GTK_ALIGN_CENTER);
525
526   de->datum_entry = psppire_value_entry_new ();
527   g_signal_connect (GTK_ENTRY (gtk_bin_get_child (GTK_BIN (de->datum_entry))),
528                     "activate", G_CALLBACK (on_datum_entry_activate), de);
529
530   hbox = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 0);
531   gtk_box_pack_start (GTK_BOX (hbox), de->cell_ref_label, FALSE, FALSE, 0);
532   gtk_box_pack_start (GTK_BOX (hbox), de->datum_entry, TRUE, TRUE, 0);
533
534   de->split = FALSE;
535   de->data_sheet = g_object_new (JMD_TYPE_SHEET, NULL);
536   GtkWidget *data_button = jmd_sheet_get_button (JMD_SHEET (de->data_sheet));
537   gtk_button_set_label (GTK_BUTTON (data_button), _("Case"));
538   de->vbox = gtk_box_new (GTK_ORIENTATION_VERTICAL, 0);
539   gtk_box_pack_start (GTK_BOX (de->vbox), hbox, FALSE, FALSE, 0);
540   gtk_box_pack_start (GTK_BOX (de->vbox), de->data_sheet, TRUE, TRUE, 0);
541
542
543   g_signal_connect_swapped (de->data_sheet, "selection-changed",
544                     G_CALLBACK (on_data_selection_change), de);
545   
546   gtk_notebook_append_page (GTK_NOTEBOOK (de), de->vbox,
547                             gtk_label_new_with_mnemonic (_("Data View")));
548
549   gtk_widget_show_all (de->vbox);
550
551   de->var_sheet = g_object_new (JMD_TYPE_SHEET, NULL);
552
553   PsppireVarSheetHeader *vsh = g_object_new (PSPPIRE_TYPE_VAR_SHEET_HEADER, NULL);
554   
555   g_object_set (de->var_sheet,
556                 "hmodel", vsh,
557                 "select-renderer-func", select_renderer_func,
558                 NULL);
559
560   
561   GtkWidget *var_button = jmd_sheet_get_button (JMD_SHEET (de->var_sheet));
562   gtk_button_set_label (GTK_BUTTON (var_button), _("Variable"));
563   
564   gtk_notebook_append_page (GTK_NOTEBOOK (de), de->var_sheet,
565                             gtk_label_new_with_mnemonic (_("Variable View")));
566
567   gtk_widget_show_all (de->var_sheet);
568   
569   g_signal_connect (de->var_sheet, "row-header-double-clicked",
570                     G_CALLBACK (on_var_sheet_var_double_clicked), de);
571
572   g_signal_connect (de->data_sheet, "column-header-double-clicked",
573                     G_CALLBACK (on_data_sheet_var_double_clicked), de);
574
575   g_object_set (de, "can-focus", FALSE, NULL);
576
577   if (psppire_conf_get_string (psppire_conf_new (),
578                            "Data Editor", "font",
579                                 &fontname) )
580     {
581       de->font = pango_font_description_from_string (fontname);
582       g_free (fontname);
583       set_font_recursively (GTK_WIDGET (de), de->font);
584     }
585
586 }
587
588 GtkWidget*
589 psppire_data_editor_new (PsppireDict *dict,
590                          PsppireDataStore *data_store)
591 {
592   return  g_object_new (PSPPIRE_DATA_EDITOR_TYPE,
593                         "dictionary",  dict,
594                         "data-store",  data_store,
595                         NULL);
596 }
597 \f
598 /* Turns the visible grid on or off, according to GRID_VISIBLE, for DE's data
599    sheet(s) and variable sheet. */
600 void
601 psppire_data_editor_show_grid (PsppireDataEditor *de, gboolean grid_visible)
602 {
603 #if 0
604   GtkTreeViewGridLines grid;
605
606   grid = (grid_visible
607           ? GTK_TREE_VIEW_GRID_LINES_BOTH
608           : GTK_TREE_VIEW_GRID_LINES_NONE);
609
610   pspp_sheet_view_set_grid_lines (PSPP_SHEET_VIEW (de->var_sheet), grid);
611 #endif  
612 }
613
614
615 static void
616 set_font_recursively (GtkWidget *w, gpointer data)
617 {
618   PangoFontDescription *font_desc = data;
619
620   gtk_widget_override_font (w, font_desc);
621
622   if ( GTK_IS_CONTAINER (w))
623     gtk_container_foreach (GTK_CONTAINER (w), set_font_recursively, font_desc);
624 }
625
626 /* Sets FONT_DESC as the font used by the data sheet(s) and variable sheet. */
627 void
628 psppire_data_editor_set_font (PsppireDataEditor *de, PangoFontDescription *font_desc)
629 {
630   gchar *font_name;
631   set_font_recursively (GTK_WIDGET (de), font_desc);
632
633   if (de->font)
634     pango_font_description_free (de->font);
635   de->font = pango_font_description_copy (font_desc);
636   font_name = pango_font_description_to_string (de->font);
637
638   psppire_conf_set_string (psppire_conf_new (),
639                            "Data Editor", "font",
640                            font_name);
641
642 }
643
644 /* If SPLIT is TRUE, splits DE's data sheet into four panes.
645    If SPLIT is FALSE, un-splits it into a single pane. */
646 void
647 psppire_data_editor_split_window (PsppireDataEditor *de, gboolean split)
648 {
649   if (split == de->split)
650     return;
651
652   disconnect_data_sheets (de);
653
654   psppire_data_editor_refresh_model (de);
655
656   gtk_widget_show_all (de->vbox);
657
658   if (de->font)
659     set_font_recursively (GTK_WIDGET (de), de->font);
660
661   de->split = split;
662   g_object_notify (G_OBJECT (de), "split");
663 }
664
665 /* Makes the variable with dictionary index DICT_INDEX in DE's dictionary
666    visible and selected in the active view in DE. */
667 void
668 psppire_data_editor_goto_variable (PsppireDataEditor *de, gint dict_index)
669 {
670 }
671
672 #if 0
673 /* Returns the "active" data sheet in DE.  If DE is in single-paned mode, this
674    is the only data sheet.  If DE is in split mode (showing four data sheets),
675    this is the focused data sheet or, if none is focused, the data sheet with
676    selected cells or, if none has selected cells, the upper-left data sheet. */
677 PsppireDataSheet *
678 psppire_data_editor_get_active_data_sheet (PsppireDataEditor *de)
679 {
680   if (de->split)
681     {
682       PsppireDataSheet *data_sheet;
683       GtkWidget *scroller;
684       int i;
685
686       /* If one of the datasheet's scrollers is focused, choose that one. */
687       scroller = gtk_container_get_focus_child (
688         GTK_CONTAINER (de->datasheet_vbox_widget));
689       if (scroller != NULL)
690         return PSPPIRE_DATA_SHEET (gtk_bin_get_child (GTK_BIN (scroller)));
691
692       /* Otherwise if there's a nonempty selection in some data sheet, choose
693          that one. */
694       FOR_EACH_DATA_SHEET (data_sheet, i, de)
695         {
696           PsppSheetSelection *selection;
697
698           selection = pspp_sheet_view_get_selection (
699             PSPP_SHEET_VIEW (data_sheet));
700           if (pspp_sheet_selection_count_selected_rows (selection)
701               && pspp_sheet_selection_count_selected_columns (selection))
702             return data_sheet;
703         }
704     }
705
706   return PSPPIRE_DATA_SHEET (de->data_sheets[0]);
707 }
708 #endif