7369cbbf60c186ab3ddc51e796927f8f2123702a
[pspp] / src / ui / gui / psppire-dialog.c
1 /* PSPPIRE - a graphical user interface for PSPP.
2    Copyright (C) 2007, 2010, 2011, 2012  Free Software Foundation
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
20 #include <gtk/gtk.h>
21 #include "psppire-dialog.h"
22 #include "psppire-buttonbox.h"
23 #include "psppire-selector.h"
24 #include <string.h>
25 #include "builder-wrapper.h"
26 #include "help-menu.h"
27
28 #include "psppire-window-base.h"
29
30 static void psppire_dialog_class_init          (PsppireDialogClass *);
31 static void psppire_dialog_init                (PsppireDialog      *);
32
33
34 enum  {DIALOG_REFRESH,
35        RESPONSE,
36        VALIDITY_CHANGED,
37        DIALOG_HELP,
38        n_SIGNALS};
39
40 static guint signals [n_SIGNALS];
41
42 static GObjectClass     *parent_class = NULL;
43
44
45 static void psppire_dialog_buildable_init (GtkBuildableIface *iface);
46
47 static void
48 psppire_dialog_finalize (GObject *object)
49 {
50   PsppireDialog *dialog = PSPPIRE_DIALOG (object);
51
52   g_free (dialog->help_page);
53
54   if (G_OBJECT_CLASS (parent_class)->finalize)
55     G_OBJECT_CLASS (parent_class)->finalize (object);
56 }
57
58 static void
59 psppire_dialog_base_init (PsppireDialogClass *class)
60 {
61   GObjectClass *object_class = G_OBJECT_CLASS (class);
62
63   object_class->finalize = psppire_dialog_finalize;
64 }
65
66 GType
67 psppire_dialog_get_type (void)
68 {
69   static GType dialog_type = 0;
70
71   if (!dialog_type)
72     {
73       static const GTypeInfo dialog_info =
74       {
75         sizeof (PsppireDialogClass),
76         psppire_dialog_base_init,
77         NULL, /* base_finalize */
78         (GClassInitFunc) psppire_dialog_class_init,
79         NULL, /* class_finalize */
80         NULL, /* class_data */
81         sizeof (PsppireDialog),
82         0,
83         (GInstanceInitFunc) psppire_dialog_init,
84       };
85
86       static const GInterfaceInfo buildable_info =
87       {
88         (GInterfaceInitFunc) psppire_dialog_buildable_init,
89         NULL,
90         NULL
91       };
92
93       dialog_type = g_type_register_static (PSPPIRE_TYPE_WINDOW_BASE,
94                                             "PsppireDialog", &dialog_info, 0);
95
96       g_type_add_interface_static (dialog_type,
97                                    GTK_TYPE_BUILDABLE,
98                                    &buildable_info);
99     }
100
101   return dialog_type;
102 }
103
104
105
106 /* Properties */
107 enum
108 {
109   PROP_0,
110   PROP_ORIENTATION,
111   PROP_SLIDING,
112   PROP_HELP_PAGE,
113 };
114
115
116 static void
117 psppire_dialog_get_property (GObject         *object,
118                              guint            prop_id,
119                              GValue          *value,
120                              GParamSpec      *pspec)
121 {
122   PsppireDialog *dialog = PSPPIRE_DIALOG (object);
123
124   switch (prop_id)
125     {
126     case PROP_ORIENTATION:
127       {
128         if ( GTK_IS_VBOX (dialog->box) || GTK_VPANED (dialog->box))
129           g_value_set_enum (value, PSPPIRE_VERTICAL);
130         else if ( GTK_IS_HBOX (dialog->box) || GTK_HPANED (dialog->box))
131           g_value_set_enum (value, PSPPIRE_HORIZONTAL);
132         else if ( GTK_IS_TABLE (dialog->box))
133           g_value_set_enum (value, PSPPIRE_TABULAR);
134       }
135       break;
136     case PROP_SLIDING:
137       g_value_set_boolean (value, dialog->slidable);
138       break;
139     case PROP_HELP_PAGE:
140       g_value_set_string (value, dialog->help_page);
141       break;
142     default:
143       G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
144       break;
145     };
146 }
147
148
149 static void
150 dialog_set_container (PsppireDialog *dialog)
151 {
152   if ( dialog->box != NULL)
153     {
154       gtk_container_remove (GTK_CONTAINER (dialog), dialog->box);
155     }
156
157   switch (dialog->orientation)
158     {
159     case PSPPIRE_HORIZONTAL:
160       if ( dialog->slidable)
161         dialog->box = gtk_hpaned_new();
162       else
163         dialog->box = gtk_hbox_new (FALSE, 5);
164       break;
165     case PSPPIRE_VERTICAL:
166       if ( dialog->slidable)
167         dialog->box = gtk_vpaned_new();
168       else
169         dialog->box = gtk_vbox_new (FALSE, 5);
170       break;
171     case PSPPIRE_TABULAR:
172       dialog->box = gtk_table_new (2, 3, FALSE);
173       g_object_set (dialog->box,
174                     "row-spacing", 5,
175                     "column-spacing", 5,
176                     NULL);
177       break;
178     }
179
180   gtk_widget_show_all (dialog->box);
181   gtk_container_add (GTK_CONTAINER (dialog), dialog->box);
182 }
183
184
185 static void
186 psppire_dialog_set_property (GObject         *object,
187                              guint            prop_id,
188                              const GValue    *value,
189                              GParamSpec      *pspec)
190
191 {
192   PsppireDialog *dialog = PSPPIRE_DIALOG (object);
193
194   switch (prop_id)
195     {
196     case PROP_SLIDING:
197       dialog->slidable = g_value_get_boolean (value);
198       break;
199     case PROP_ORIENTATION:
200       dialog->orientation = g_value_get_enum (value);
201       break;
202     case PROP_HELP_PAGE:
203       dialog->help_page = g_value_dup_string (value);
204       break;
205     default:
206       G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
207       break;
208     };
209
210   dialog_set_container (dialog);
211 }
212
213
214 static GParamSpec *orientation_spec ;
215
216 static void
217 psppire_dialog_class_init (PsppireDialogClass *class)
218 {
219   GObjectClass *object_class = (GObjectClass *) class;
220
221   GParamSpec *sliding_spec ;
222   GParamSpec *help_page_spec ;
223
224   help_page_spec = 
225     g_param_spec_string ("help-page", 
226                          "Help Page",
227                          "The section of the manual to load when the Help button is clicked",
228                          NULL,
229                          G_PARAM_READWRITE);
230
231   orientation_spec =
232     g_param_spec_enum ("orientation",
233                        "Orientation",
234                        "Which way widgets are packed",
235                        PSPPIRE_TYPE_ORIENTATION,
236                        PSPPIRE_HORIZONTAL /* default value */,
237                        G_PARAM_CONSTRUCT_ONLY |G_PARAM_READWRITE);
238
239   sliding_spec =
240     g_param_spec_boolean ("slidable",
241                           "Slidable",
242                           "Can the container be sized by the user",
243                           FALSE,
244                           G_PARAM_CONSTRUCT_ONLY |G_PARAM_READWRITE);
245
246   object_class->set_property = psppire_dialog_set_property;
247   object_class->get_property = psppire_dialog_get_property;
248
249   g_object_class_install_property (object_class,
250                                    PROP_ORIENTATION,
251                                    orientation_spec);
252
253
254   g_object_class_install_property (object_class,
255                                    PROP_SLIDING,
256                                    sliding_spec);
257
258   g_object_class_install_property (object_class,
259                                    PROP_HELP_PAGE,
260                                    help_page_spec);
261
262
263   signals [DIALOG_REFRESH] =
264     g_signal_new ("refresh",
265                   G_TYPE_FROM_CLASS (class),
266                   G_SIGNAL_RUN_FIRST,
267                   0,
268                   NULL, NULL,
269                   g_cclosure_marshal_VOID__VOID,
270                   G_TYPE_NONE,
271                   0);
272
273
274   signals [RESPONSE] =
275     g_signal_new ("response",
276                   G_TYPE_FROM_CLASS (class),
277                   G_SIGNAL_RUN_FIRST,
278                   0,
279                   NULL, NULL,
280                   g_cclosure_marshal_VOID__INT,
281                   G_TYPE_NONE,
282                   1,
283                   G_TYPE_INT);
284
285
286   signals [VALIDITY_CHANGED] =
287     g_signal_new ("validity-changed",
288                   G_TYPE_FROM_CLASS (class),
289                   G_SIGNAL_RUN_FIRST,
290                   0,
291                   NULL, NULL,
292                   g_cclosure_marshal_VOID__BOOLEAN,
293                   G_TYPE_NONE,
294                   1,
295                   G_TYPE_BOOLEAN);
296
297
298   signals [DIALOG_HELP] =
299     g_signal_new ("help",
300                   G_TYPE_FROM_CLASS (class),
301                   G_SIGNAL_RUN_FIRST,
302                   0,
303                   NULL, NULL,
304                   g_cclosure_marshal_VOID__STRING,
305                   G_TYPE_NONE,
306                   1,
307                   G_TYPE_STRING);
308
309
310   parent_class = g_type_class_peek_parent (class);
311 }
312
313
314
315
316 static void
317 close_dialog (GtkWidget *w, gpointer data)
318 {
319   PsppireDialog *dialog = data;
320
321   psppire_dialog_close (dialog);
322 }
323
324 void
325 psppire_dialog_close (PsppireDialog *dialog)
326 {
327   g_main_loop_quit (dialog->loop);
328   gtk_widget_hide (GTK_WIDGET (dialog));
329 }
330
331
332 static void
333 delete_event_callback (GtkWidget *w, GdkEvent *e, gpointer data)
334 {
335   close_dialog (w, data);
336 }
337
338
339 static void
340 psppire_dialog_init (PsppireDialog *dialog)
341 {
342   GValue value = {0};
343   dialog->box = NULL;
344   dialog->contents_are_valid = NULL;
345   dialog->validity_data = NULL;
346   dialog->contents_are_acceptable = NULL;
347   dialog->acceptable_data = NULL;
348   dialog->slidable = FALSE;
349   dialog->help_page = NULL;
350
351   g_value_init (&value, orientation_spec->value_type);
352   g_param_value_set_default (orientation_spec, &value);
353
354   gtk_window_set_type_hint (GTK_WINDOW (dialog),
355         GDK_WINDOW_TYPE_HINT_DIALOG);
356
357   g_value_unset (&value);
358
359   g_signal_connect (dialog, "delete-event",
360                     G_CALLBACK (delete_event_callback),
361                     dialog);
362
363   gtk_window_set_type_hint (GTK_WINDOW (dialog),
364         GDK_WINDOW_TYPE_HINT_DIALOG);
365
366   g_object_set (dialog, "icon-name", "pspp", NULL);
367 }
368
369
370 GtkWidget*
371 psppire_dialog_new (void)
372 {
373   PsppireDialog *dialog ;
374
375   dialog = g_object_new (psppire_dialog_get_type (),
376                          NULL);
377
378   return GTK_WIDGET (dialog) ;
379 }
380
381
382 void
383 psppire_dialog_notify_change (PsppireDialog *dialog)
384 {
385   if ( dialog->contents_are_valid )
386     {
387       gboolean valid = dialog->contents_are_valid (dialog->validity_data);
388
389       g_signal_emit (dialog, signals [VALIDITY_CHANGED], 0, valid);
390     }
391 }
392
393
394 static void
395 remove_notify_handlers (PsppireDialog *dialog, GObject *sel)
396 {
397   g_signal_handlers_disconnect_by_data (sel, dialog);
398 }
399
400
401 /* Descend the widget tree, connecting appropriate signals to the
402    psppire_dialog_notify_change callback */
403 static void
404 connect_notify_signal (GtkWidget *w, gpointer data)
405 {
406   PsppireDialog *dialog = data;
407
408   if ( PSPPIRE_IS_BUTTONBOX (w))
409     return;
410
411   if ( GTK_IS_CONTAINER (w))
412     {
413       gtk_container_foreach (GTK_CONTAINER (w),
414                              connect_notify_signal,
415                              dialog);
416     }
417
418
419   /* It's unfortunate that GTK+ doesn't have a generic
420      "user-modified-state-changed" signal.  Instead, we have to try and
421      predict what widgets and signals are likely to exist in our dialogs. */
422
423   if ( GTK_IS_TOGGLE_BUTTON (w))
424     {
425       g_signal_connect_swapped (w, "toggled",
426                                 G_CALLBACK (psppire_dialog_notify_change),
427                                 dialog);
428     }
429
430   if ( PSPPIRE_IS_SELECTOR (w))
431     {
432       g_signal_connect_swapped (w, "selected",
433                                 G_CALLBACK (psppire_dialog_notify_change),
434                                 dialog);
435
436       g_signal_connect_swapped (w, "de-selected",
437                                 G_CALLBACK (psppire_dialog_notify_change),
438                                 dialog);
439     }
440
441   if ( GTK_IS_EDITABLE (w))
442     {
443       g_signal_connect_swapped (w, "changed",
444                                 G_CALLBACK (psppire_dialog_notify_change),
445                                 dialog);
446     }
447
448   if ( GTK_IS_CELL_EDITABLE (w))
449     {
450       g_signal_connect_swapped (w, "editing-done",
451                                 G_CALLBACK (psppire_dialog_notify_change),
452                                 dialog);
453     }
454
455   if ( GTK_IS_TEXT_VIEW (w))
456     {
457       GtkTextBuffer *buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (w));
458
459       g_signal_connect_swapped (buffer, "changed",
460                                 G_CALLBACK (psppire_dialog_notify_change),
461                                 dialog);
462     }
463
464   if ( GTK_IS_TREE_VIEW (w))
465     {
466       gint i = 0;
467       GtkTreeView *tv = GTK_TREE_VIEW (w);
468       GtkTreeSelection *selection =
469         gtk_tree_view_get_selection (tv);
470       GtkTreeViewColumn *col;
471       GtkTreeModel *model = gtk_tree_view_get_model (tv);
472
473       if ( model)
474         {
475           g_signal_connect_swapped (model, "row-changed",
476                                     G_CALLBACK (psppire_dialog_notify_change),
477                                     dialog);
478
479           g_signal_connect_swapped (model, "row-deleted",
480                                     G_CALLBACK (psppire_dialog_notify_change),
481                                     dialog);
482
483           g_signal_connect_swapped (model, "row-inserted",
484                                     G_CALLBACK (psppire_dialog_notify_change),
485                                     dialog);
486
487           g_signal_connect (dialog, "destroy", G_CALLBACK (remove_notify_handlers),
488                             model);
489         }
490       
491       g_signal_connect_swapped (selection, "changed",
492                                 G_CALLBACK (psppire_dialog_notify_change),
493                                 dialog);
494
495       while ((col = gtk_tree_view_get_column (tv, i++)))
496         {
497           GList *renderers = gtk_cell_layout_get_cells (GTK_CELL_LAYOUT (col));
498           GList *start = renderers;
499           while (renderers)
500             {
501               if ( GTK_IS_CELL_RENDERER_TOGGLE (renderers->data))
502                 g_signal_connect_swapped (renderers->data, "toggled",
503                                           G_CALLBACK (psppire_dialog_notify_change), dialog);
504               renderers = renderers->next;
505             }
506           g_list_free (start);
507         }
508     }
509 }
510
511
512 gint
513 psppire_dialog_run (PsppireDialog *dialog)
514 {
515   gchar *title = NULL;
516   g_object_get (dialog, "title", &title, NULL);
517
518   if (title == NULL)
519     g_warning ("PsppireDialog %s has no title", gtk_widget_get_name (GTK_WIDGET (dialog)));
520   
521   if ( dialog->contents_are_valid != NULL )
522     gtk_container_foreach (GTK_CONTAINER (dialog->box),
523                            connect_notify_signal,
524                            dialog);
525
526   dialog->loop = g_main_loop_new (NULL, FALSE);
527
528   gtk_widget_show (GTK_WIDGET (dialog));
529
530   if ( dialog->contents_are_valid != NULL)
531     g_signal_emit (dialog, signals [VALIDITY_CHANGED], 0, FALSE);
532
533   g_signal_emit (dialog, signals [DIALOG_REFRESH], 0);
534
535   gdk_threads_leave ();
536   g_main_loop_run (dialog->loop);
537   gdk_threads_enter ();
538
539   g_main_loop_unref (dialog->loop);
540
541   g_signal_emit (dialog, signals [RESPONSE], 0, dialog->response);
542
543   return dialog->response;
544 }
545
546
547 void
548 psppire_dialog_reload (PsppireDialog *dialog)
549 {
550   g_signal_emit (dialog, signals [DIALOG_REFRESH], 0);
551 }
552
553
554 void
555 psppire_dialog_help (PsppireDialog *dialog)
556 {
557   const char *page = NULL;
558
559   g_object_get (dialog, "help-page", &page, NULL);
560
561   online_help (page);
562
563   g_signal_emit (dialog, signals [DIALOG_HELP], 0, page);
564 }
565
566
567 GType
568 psppire_orientation_get_type (void)
569 {
570   static GType etype = 0;
571   if (etype == 0)
572     {
573       static const GEnumValue values[] =
574         {
575           { PSPPIRE_HORIZONTAL, "PSPPIRE_HORIZONTAL", "Horizontal" },
576           { PSPPIRE_VERTICAL,   "PSPPIRE_VERTICAL",   "Vertical" },
577           { PSPPIRE_TABULAR,   "PSPPIRE_TABULAR",   "Tabular" },
578           { 0, NULL, NULL }
579         };
580
581       etype = g_enum_register_static
582         (g_intern_static_string ("PsppireOrientation"), values);
583
584     }
585   return etype;
586 }
587
588
589 /* Sets a predicate function that is checked after each change that the user
590    makes to the dialog's state.  If the predicate function returns false, then
591    "OK" and other buttons that accept the dialog's settings will be
592    disabled. */
593 void
594 psppire_dialog_set_valid_predicate (PsppireDialog *dialog,
595                                     ContentsAreValid contents_are_valid,
596                                     gpointer data)
597 {
598   dialog->contents_are_valid = contents_are_valid;
599   dialog->validity_data = data;
600 }
601
602 /* Sets a predicate function that is called after "OK" or another button that
603    accepts the dialog's settings is pushed.  If the predicate function returns
604    false, then the button push is ignored.  (If the predicate function returns
605    false, then it should take some action to notify the user why the contents
606    are unacceptable, e.g. pop up a dialog box.)
607
608    An accept predicate is preferred over a validity predicate when the reason
609    why the dialog settings are unacceptable may not be obvious to the user, so
610    that the user needs a helpful message to explain. */
611 void
612 psppire_dialog_set_accept_predicate (PsppireDialog *dialog,
613                                      ContentsAreValid contents_are_acceptable,
614                                      gpointer data)
615 {
616   dialog->contents_are_acceptable = contents_are_acceptable;
617   dialog->acceptable_data = data;
618 }
619
620 gboolean
621 psppire_dialog_is_acceptable (const PsppireDialog *dialog)
622 {
623   return (dialog->contents_are_acceptable == NULL
624           || dialog->contents_are_acceptable (dialog->acceptable_data));
625 }
626
627
628
629
630 static GObject *
631 get_internal_child    (GtkBuildable *buildable,
632                        GtkBuilder *builder,
633                        const gchar *childname)
634 {
635   PsppireDialog *dialog = PSPPIRE_DIALOG (buildable);
636
637   if ( 0 == strcmp (childname, "hbox"))
638     return G_OBJECT (dialog->box);
639
640   return NULL;
641 }
642
643
644
645 static void
646 psppire_dialog_buildable_init (GtkBuildableIface *iface)
647 {
648   iface->get_internal_child = get_internal_child;
649 }