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