Re-enable display of cell reference label
[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/pspp-sheet-selection.h"
30 #include "ui/gui/psppire-data-store.h"
31 #include "ui/gui/psppire-value-entry.h"
32 #include "ui/gui/psppire-var-sheet.h"
33 #include "ui/gui/psppire-conf.h"
34 #include "ui/gui/psppire-var-sheet-header.h"
35
36 #include "ui/gui/efficient-sheet/jmd-sheet.h"
37
38 #include <gettext.h>
39 #define _(msgid) gettext (msgid)
40
41
42 static void psppire_data_editor_class_init          (PsppireDataEditorClass *klass);
43 static void psppire_data_editor_init                (PsppireDataEditor      *de);
44
45 static void disconnect_data_sheets (PsppireDataEditor *);
46 static void refresh_entry (PsppireDataEditor *);
47 static void psppire_data_editor_update_ui_manager (PsppireDataEditor *);
48
49 GType
50 psppire_data_editor_get_type (void)
51 {
52   static GType de_type = 0;
53
54   if (!de_type)
55     {
56       static const GTypeInfo de_info =
57       {
58         sizeof (PsppireDataEditorClass),
59         NULL, /* base_init */
60         NULL, /* base_finalize */
61         (GClassInitFunc) psppire_data_editor_class_init,
62         NULL, /* class_finalize */
63         NULL, /* class_data */
64         sizeof (PsppireDataEditor),
65         0,
66         (GInstanceInitFunc) psppire_data_editor_init,
67       };
68
69       de_type = g_type_register_static (GTK_TYPE_NOTEBOOK, "PsppireDataEditor",
70                                         &de_info, 0);
71     }
72
73   return de_type;
74 }
75
76 static GObjectClass * parent_class = NULL;
77
78 static void
79 psppire_data_editor_dispose (GObject *obj)
80 {
81   PsppireDataEditor *de = (PsppireDataEditor *) obj;
82
83   disconnect_data_sheets (de);
84
85   if (de->data_store)
86     {
87       g_object_unref (de->data_store);
88       de->data_store = NULL;
89     }
90
91   if (de->dict)
92     {
93       g_object_unref (de->dict);
94       de->dict = NULL;
95     }
96
97   if (de->font != NULL)
98     {
99       pango_font_description_free (de->font);
100       de->font = NULL;
101     }
102
103   if (de->ui_manager)
104     {
105       g_object_unref (de->ui_manager);
106       de->ui_manager = NULL;
107     }
108
109   /* Chain up to the parent class */
110   G_OBJECT_CLASS (parent_class)->dispose (obj);
111 }
112
113 enum
114   {
115     PROP_0,
116     PROP_DATA_STORE,
117     PROP_DICTIONARY,
118     PROP_VALUE_LABELS,
119     PROP_SPLIT_WINDOW,
120     PROP_UI_MANAGER
121   };
122
123 static void
124 psppire_data_editor_refresh_model (PsppireDataEditor *de)
125 {
126 }
127
128 static void
129 psppire_data_editor_set_property (GObject         *object,
130                                   guint            prop_id,
131                                   const GValue    *value,
132                                   GParamSpec      *pspec)
133 {
134   PsppireDataEditor *de = PSPPIRE_DATA_EDITOR (object);
135
136   switch (prop_id)
137     {
138     case PROP_SPLIT_WINDOW:
139       psppire_data_editor_split_window (de, g_value_get_boolean (value));
140       break;
141     case PROP_DATA_STORE:
142       if ( de->data_store)
143         {
144           g_signal_handlers_disconnect_by_func (de->data_store,
145                                                 G_CALLBACK (refresh_entry),
146                                                 de);
147           g_object_unref (de->data_store);
148         }
149
150       de->data_store = g_value_get_pointer (value);
151       g_object_ref (de->data_store);
152       g_print ("NEW STORE\n");
153
154       g_object_set (de->data_sheet, "data-model", de->data_store, NULL);
155       psppire_data_editor_refresh_model (de);
156
157       g_signal_connect_swapped (de->data_store, "case-changed",
158                                 G_CALLBACK (refresh_entry), de);
159
160       break;
161     case PROP_DICTIONARY:
162       if (de->dict)
163         g_object_unref (de->dict);
164       de->dict = g_value_get_pointer (value);
165       g_object_ref (de->dict);
166
167       g_object_set (de->data_sheet, "hmodel", de->dict, NULL);
168       g_object_set (de->var_sheet, "data-model", de->dict, NULL);
169
170       break;
171     case PROP_VALUE_LABELS:
172       break;
173     case PROP_UI_MANAGER:
174     default:
175       G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
176       break;
177     };
178 }
179
180 static void
181 psppire_data_editor_get_property (GObject         *object,
182                                   guint            prop_id,
183                                   GValue          *value,
184                                   GParamSpec      *pspec)
185 {
186   PsppireDataEditor *de = PSPPIRE_DATA_EDITOR (object);
187
188   switch (prop_id)
189     {
190     case PROP_SPLIT_WINDOW:
191       g_value_set_boolean (value, de->split);
192       break;
193     case PROP_DATA_STORE:
194       g_value_set_pointer (value, de->data_store);
195       break;
196     case PROP_DICTIONARY:
197       g_value_set_pointer (value, de->dict);
198       break;
199     case PROP_VALUE_LABELS:
200       break;
201     case PROP_UI_MANAGER:
202       g_value_set_object (value, psppire_data_editor_get_ui_manager (de));
203       break;
204     default:
205       G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
206       break;
207     }
208 }
209
210 static void
211 psppire_data_editor_switch_page (GtkNotebook     *notebook,
212                                  GtkWidget *w,
213                                  guint            page_num)
214 {
215   GTK_NOTEBOOK_CLASS (parent_class)->switch_page (notebook, w, page_num);
216   psppire_data_editor_update_ui_manager (PSPPIRE_DATA_EDITOR (notebook));
217 }
218
219 static void
220 psppire_data_editor_set_focus_child (GtkContainer *container,
221                                      GtkWidget    *widget)
222 {
223   GTK_CONTAINER_CLASS (parent_class)->set_focus_child (container, widget);
224   psppire_data_editor_update_ui_manager (PSPPIRE_DATA_EDITOR (container));
225 }
226
227 static void
228 psppire_data_editor_class_init (PsppireDataEditorClass *klass)
229 {
230   GParamSpec *data_store_spec ;
231   GParamSpec *dict_spec ;
232   GParamSpec *value_labels_spec;
233   GParamSpec *split_window_spec;
234   GParamSpec *ui_manager_spec;
235   GObjectClass *object_class = G_OBJECT_CLASS (klass);
236   GtkContainerClass *container_class = GTK_CONTAINER_CLASS (klass);
237   GtkNotebookClass *notebook_class = GTK_NOTEBOOK_CLASS (klass);
238
239   parent_class = g_type_class_peek_parent (klass);
240
241   object_class->dispose = psppire_data_editor_dispose;
242   object_class->set_property = psppire_data_editor_set_property;
243   object_class->get_property = psppire_data_editor_get_property;
244
245   container_class->set_focus_child = psppire_data_editor_set_focus_child;
246
247   notebook_class->switch_page = psppire_data_editor_switch_page;
248
249   data_store_spec =
250     g_param_spec_pointer ("data-store",
251                           "Data Store",
252                           "A pointer to the data store associated with this editor",
253                           G_PARAM_CONSTRUCT_ONLY | G_PARAM_WRITABLE | G_PARAM_READABLE );
254
255   g_object_class_install_property (object_class,
256                                    PROP_DATA_STORE,
257                                    data_store_spec);
258
259   dict_spec =
260     g_param_spec_pointer ("dictionary",
261                           "Dictionary",
262                           "A pointer to the dictionary associated with this editor",
263                           G_PARAM_CONSTRUCT_ONLY | G_PARAM_WRITABLE | G_PARAM_READABLE );
264
265   g_object_class_install_property (object_class,
266                                    PROP_DICTIONARY,
267                                    dict_spec);
268
269   value_labels_spec =
270     g_param_spec_boolean ("value-labels",
271                          "Value Labels",
272                          "Whether or not the data sheet should display labels instead of values",
273                           FALSE,
274                          G_PARAM_WRITABLE | G_PARAM_READABLE);
275
276   g_object_class_install_property (object_class,
277                                    PROP_VALUE_LABELS,
278                                    value_labels_spec);
279
280
281   split_window_spec =
282     g_param_spec_boolean ("split",
283                           "Split Window",
284                           "True iff the data sheet is split",
285                           FALSE,
286                           G_PARAM_READABLE | G_PARAM_WRITABLE);
287
288   g_object_class_install_property (object_class,
289                                    PROP_SPLIT_WINDOW,
290                                    split_window_spec);
291
292   ui_manager_spec =
293     g_param_spec_object ("ui-manager",
294                          "UI Manager",
295                          "UI manager for the active notebook tab.  The client should merge this UI manager with the active UI manager to obtain menu items and tool bar items specific to the active notebook tab.",
296                          GTK_TYPE_UI_MANAGER,
297                          G_PARAM_READABLE);
298   g_object_class_install_property (object_class,
299                                    PROP_UI_MANAGER,
300                                    ui_manager_spec);
301 }
302
303
304 static void
305 on_var_sheet_var_double_clicked (PsppireVarSheet *var_sheet, gint dict_index,
306                                  PsppireDataEditor *de)
307 {
308   gtk_notebook_set_current_page (GTK_NOTEBOOK (de),
309                                  PSPPIRE_DATA_EDITOR_DATA_VIEW);
310
311   jmd_sheet_scroll_to (de->data_sheet, dict_index, -1);
312 }
313
314 static void
315 on_data_sheet_var_double_clicked (JmdSheet *data_sheet, gint dict_index,
316                                  PsppireDataEditor *de)
317 {
318   gtk_notebook_set_current_page (GTK_NOTEBOOK (de),
319                                  PSPPIRE_DATA_EDITOR_VARIABLE_VIEW);
320
321   jmd_sheet_scroll_to (de->var_sheet, -1, dict_index);
322 }
323
324
325 /* Refreshes 'de->cell_ref_label' and 'de->datum_entry' from the currently
326    active cell or cells. */
327 static void
328 refresh_entry (PsppireDataEditor *de)
329 {
330   g_print ("%s\n", __FUNCTION__);
331 }
332
333 static void
334 on_datum_entry_activate (PsppireValueEntry *entry, PsppireDataEditor *de)
335 {
336 }
337
338
339 static void
340 disconnect_data_sheets (PsppireDataEditor *de)
341 {
342 }
343
344 /* Called when the active cell or the selection in the data sheet changes */
345 static void
346 on_data_selection_change (PsppireDataEditor *de, JmdRange *sel)
347 {
348   gchar *ref_cell_text = NULL;
349
350   gint n_cases = abs (sel->end_y - sel->start_y) + 1;
351   gint n_vars = abs (sel->end_x - sel->start_x) + 1;
352
353   if (n_cases == 1 && n_vars == 1)
354     {
355       /* A single cell is selected */
356       const struct variable *var = psppire_dict_get_variable (de->dict, sel->start_x);
357       
358       ref_cell_text = g_strdup_printf (_("%d : %s"),
359                                        sel->start_y + 1, var_get_name (var));
360     }
361   else
362     {
363       struct string s;
364
365       /* The glib string library does not understand the ' printf modifier
366          on all platforms, but the "struct string" library does (because
367          Gnulib fixes that problem), so use the latter.  */
368       ds_init_empty (&s);
369       ds_put_format (&s, ngettext ("%'d case", "%'d cases", n_cases),
370                      n_cases);
371       ds_put_byte (&s, ' ');
372       ds_put_unichar (&s, 0xd7); /* U+00D7 MULTIPLICATION SIGN */
373       ds_put_byte (&s, ' ');
374       ds_put_format (&s, ngettext ("%'d variable", "%'d variables",
375                                    n_vars),
376                      n_vars);
377       ref_cell_text = ds_steal_cstr (&s);
378     }
379   
380   gtk_label_set_label (GTK_LABEL (de->cell_ref_label),
381                        ref_cell_text ? ref_cell_text : "");
382   
383   g_free (ref_cell_text);
384 }
385
386
387 static void set_font_recursively (GtkWidget *w, gpointer data);
388
389 static void
390 psppire_data_editor_init (PsppireDataEditor *de)
391 {
392   GtkWidget *hbox;
393   gchar *fontname = NULL;
394
395   de->font = NULL;
396   de->ui_manager = NULL;
397   de->old_vbox_widget = NULL;
398
399   g_object_set (de, "tab-pos", GTK_POS_BOTTOM, NULL);
400
401   de->cell_ref_label = gtk_label_new ("");
402   gtk_label_set_width_chars (GTK_LABEL (de->cell_ref_label), 25);
403   gtk_widget_set_valign (de->cell_ref_label, GTK_ALIGN_CENTER);
404
405   de->datum_entry = psppire_value_entry_new ();
406   g_signal_connect (GTK_ENTRY (gtk_bin_get_child (GTK_BIN (de->datum_entry))),
407                     "activate", G_CALLBACK (on_datum_entry_activate), de);
408
409   hbox = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 0);
410   gtk_box_pack_start (GTK_BOX (hbox), de->cell_ref_label, FALSE, FALSE, 0);
411   gtk_box_pack_start (GTK_BOX (hbox), de->datum_entry, TRUE, TRUE, 0);
412
413   de->split = FALSE;
414   de->data_sheet = g_object_new (JMD_TYPE_SHEET, NULL);
415   GtkWidget *data_button = jmd_sheet_get_button (JMD_SHEET (de->data_sheet));
416   gtk_button_set_label (GTK_BUTTON (data_button), _("Case"));
417   de->vbox = gtk_box_new (GTK_ORIENTATION_VERTICAL, 0);
418   gtk_box_pack_start (GTK_BOX (de->vbox), hbox, FALSE, FALSE, 0);
419   gtk_box_pack_start (GTK_BOX (de->vbox), de->data_sheet, TRUE, TRUE, 0);
420
421
422   g_signal_connect_swapped (de->data_sheet, "selection-changed",
423                     G_CALLBACK (on_data_selection_change), de);
424   
425   gtk_notebook_append_page (GTK_NOTEBOOK (de), de->vbox,
426                             gtk_label_new_with_mnemonic (_("Data View")));
427
428   gtk_widget_show_all (de->vbox);
429
430   de->var_sheet = g_object_new (JMD_TYPE_SHEET, NULL);
431
432   PsppireVarSheetHeader *vsh = g_object_new (PSPPIRE_TYPE_VAR_SHEET_HEADER, NULL);
433   
434   g_object_set (de->var_sheet, "hmodel", vsh, NULL);
435
436   
437   GtkWidget *var_button = jmd_sheet_get_button (JMD_SHEET (de->var_sheet));
438   gtk_button_set_label (GTK_BUTTON (var_button), _("Variable"));
439   
440   gtk_notebook_append_page (GTK_NOTEBOOK (de), de->var_sheet,
441                             gtk_label_new_with_mnemonic (_("Variable View")));
442
443   gtk_widget_show_all (de->var_sheet);
444   
445   g_signal_connect (de->var_sheet, "row-header-double-clicked",
446                     G_CALLBACK (on_var_sheet_var_double_clicked), de);
447
448   g_signal_connect (de->data_sheet, "column-header-double-clicked",
449                     G_CALLBACK (on_data_sheet_var_double_clicked), de);
450
451   g_object_set (de, "can-focus", FALSE, NULL);
452
453   if (psppire_conf_get_string (psppire_conf_new (),
454                            "Data Editor", "font",
455                                 &fontname) )
456     {
457       de->font = pango_font_description_from_string (fontname);
458       g_free (fontname);
459       set_font_recursively (GTK_WIDGET (de), de->font);
460     }
461
462   psppire_data_editor_update_ui_manager (de);
463 }
464
465 GtkWidget*
466 psppire_data_editor_new (PsppireDict *dict,
467                          PsppireDataStore *data_store)
468 {
469   return  g_object_new (PSPPIRE_DATA_EDITOR_TYPE,
470                         "dictionary",  dict,
471                         "data-store",  data_store,
472                         NULL);
473 }
474 \f
475 /* Turns the visible grid on or off, according to GRID_VISIBLE, for DE's data
476    sheet(s) and variable sheet. */
477 void
478 psppire_data_editor_show_grid (PsppireDataEditor *de, gboolean grid_visible)
479 {
480   GtkTreeViewGridLines grid;
481
482   grid = (grid_visible
483           ? GTK_TREE_VIEW_GRID_LINES_BOTH
484           : GTK_TREE_VIEW_GRID_LINES_NONE);
485
486   pspp_sheet_view_set_grid_lines (PSPP_SHEET_VIEW (de->var_sheet), grid);
487 }
488
489
490 static void
491 set_font_recursively (GtkWidget *w, gpointer data)
492 {
493   PangoFontDescription *font_desc = data;
494
495   gtk_widget_override_font (w, font_desc);
496
497   if ( GTK_IS_CONTAINER (w))
498     gtk_container_foreach (GTK_CONTAINER (w), set_font_recursively, font_desc);
499 }
500
501 /* Sets FONT_DESC as the font used by the data sheet(s) and variable sheet. */
502 void
503 psppire_data_editor_set_font (PsppireDataEditor *de, PangoFontDescription *font_desc)
504 {
505   gchar *font_name;
506   set_font_recursively (GTK_WIDGET (de), font_desc);
507
508   if (de->font)
509     pango_font_description_free (de->font);
510   de->font = pango_font_description_copy (font_desc);
511   font_name = pango_font_description_to_string (de->font);
512
513   psppire_conf_set_string (psppire_conf_new (),
514                            "Data Editor", "font",
515                            font_name);
516
517 }
518
519 /* If SPLIT is TRUE, splits DE's data sheet into four panes.
520    If SPLIT is FALSE, un-splits it into a single pane. */
521 void
522 psppire_data_editor_split_window (PsppireDataEditor *de, gboolean split)
523 {
524   if (split == de->split)
525     return;
526
527   disconnect_data_sheets (de);
528
529   psppire_data_editor_refresh_model (de);
530
531   gtk_widget_show_all (de->vbox);
532
533   if (de->font)
534     set_font_recursively (GTK_WIDGET (de), de->font);
535
536   de->split = split;
537   g_object_notify (G_OBJECT (de), "split");
538   psppire_data_editor_update_ui_manager (de);
539 }
540
541 /* Makes the variable with dictionary index DICT_INDEX in DE's dictionary
542    visible and selected in the active view in DE. */
543 void
544 psppire_data_editor_goto_variable (PsppireDataEditor *de, gint dict_index)
545 {
546 }
547
548
549 /* Returns the UI manager that should be merged into DE's toplevel widget's UI
550    manager to display menu items and toolbar items specific to DE's current
551    page and data sheet.
552
553    DE's toplevel widget can watch for changes by connecting to DE's
554    notify::ui-manager signal. */
555 GtkUIManager *
556 psppire_data_editor_get_ui_manager (PsppireDataEditor *de)
557 {
558   psppire_data_editor_update_ui_manager (de);
559   return de->ui_manager;
560 }
561
562 static void
563 psppire_data_editor_update_ui_manager (PsppireDataEditor *de)
564 {
565   GtkUIManager *ui_manager;
566
567   ui_manager = NULL;
568
569   switch (gtk_notebook_get_current_page (GTK_NOTEBOOK (de)))
570     {
571     case PSPPIRE_DATA_EDITOR_DATA_VIEW:
572       break;
573
574     case PSPPIRE_DATA_EDITOR_VARIABLE_VIEW:
575       break;
576
577     default:
578       /* This happens transiently in psppire_data_editor_init(). */
579       break;
580     }
581
582   if (ui_manager != de->ui_manager)
583     {
584       if (de->ui_manager)
585         g_object_unref (de->ui_manager);
586       if (ui_manager)
587         g_object_ref (ui_manager);
588       de->ui_manager = ui_manager;
589
590       g_object_notify (G_OBJECT (de), "ui-manager");
591     }
592 }
593
594