5726cc76a941df4f5751328a08fa41ea29794d59
[pspp] / src / ui / gui / psppire-dialog.c
1 /* PSPPIRE - a graphical user interface for PSPP.
2    Copyright (C) 2007, 2010, 2011, 2012, 2015  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 static void
45 psppire_dialog_finalize (GObject *object)
46 {
47   PsppireDialog *dialog = PSPPIRE_DIALOG (object);
48
49   g_free (dialog->help_page);
50
51   if (G_OBJECT_CLASS (parent_class)->finalize)
52     G_OBJECT_CLASS (parent_class)->finalize (object);
53 }
54
55 static void
56 psppire_dialog_base_init (PsppireDialogClass *class)
57 {
58   GObjectClass *object_class = G_OBJECT_CLASS (class);
59
60   object_class->finalize = psppire_dialog_finalize;
61 }
62
63 GType
64 psppire_dialog_get_type (void)
65 {
66   static GType dialog_type = 0;
67
68   if (!dialog_type)
69     {
70       static const GTypeInfo dialog_info =
71       {
72         sizeof (PsppireDialogClass),
73         (GBaseInitFunc) psppire_dialog_base_init,
74         NULL, /* base_finalize */
75         (GClassInitFunc) psppire_dialog_class_init,
76         NULL, /* class_finalize */
77         NULL, /* class_data */
78         sizeof (PsppireDialog),
79         0,
80         (GInstanceInitFunc) psppire_dialog_init,
81       };
82
83       dialog_type = g_type_register_static (PSPPIRE_TYPE_WINDOW_BASE,
84                                             "PsppireDialog", &dialog_info, 0);
85     }
86
87   return dialog_type;
88 }
89
90
91
92 /* Properties */
93 enum
94 {
95   PROP_0,
96   PROP_ORIENTATION,
97   PROP_SLIDING,
98   PROP_HELP_PAGE,
99 };
100
101
102 static void
103 psppire_dialog_get_property (GObject         *object,
104                              guint            prop_id,
105                              GValue          *value,
106                              GParamSpec      *pspec)
107 {
108   PsppireDialog *dialog = PSPPIRE_DIALOG (object);
109
110   switch (prop_id)
111     {
112     case PROP_SLIDING:
113       g_value_set_boolean (value, dialog->slidable);
114       break;
115     case PROP_HELP_PAGE:
116       g_value_set_string (value, dialog->help_page);
117       break;
118     default:
119       G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
120       break;
121     };
122 }
123
124 static void
125 psppire_dialog_set_property (GObject         *object,
126                              guint            prop_id,
127                              const GValue    *value,
128                              GParamSpec      *pspec)
129
130 {
131   PsppireDialog *dialog = PSPPIRE_DIALOG (object);
132
133   switch (prop_id)
134     {
135     case PROP_SLIDING:
136       dialog->slidable = g_value_get_boolean (value);
137       break;
138     case PROP_HELP_PAGE:
139       dialog->help_page = g_value_dup_string (value);
140       break;
141     default:
142       G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
143       break;
144     };
145 }
146
147 static void
148 psppire_dialog_class_init (PsppireDialogClass *class)
149 {
150   GObjectClass *object_class = (GObjectClass *) class;
151
152   GParamSpec *sliding_spec ;
153   GParamSpec *help_page_spec ;
154
155   help_page_spec = 
156     g_param_spec_string ("help-page", 
157                          "Help Page",
158                          "The section of the manual to load when the Help button is clicked",
159                          NULL,
160                          G_PARAM_READWRITE);
161
162   sliding_spec =
163     g_param_spec_boolean ("slidable",
164                           "Slidable",
165                           "Can the container be sized by the user",
166                           FALSE,
167                           G_PARAM_CONSTRUCT_ONLY |G_PARAM_READWRITE);
168
169   object_class->set_property = psppire_dialog_set_property;
170   object_class->get_property = psppire_dialog_get_property;
171
172   g_object_class_install_property (object_class,
173                                    PROP_SLIDING,
174                                    sliding_spec);
175
176   g_object_class_install_property (object_class,
177                                    PROP_HELP_PAGE,
178                                    help_page_spec);
179
180
181   signals [DIALOG_REFRESH] =
182     g_signal_new ("refresh",
183                   G_TYPE_FROM_CLASS (class),
184                   G_SIGNAL_RUN_FIRST,
185                   0,
186                   NULL, NULL,
187                   g_cclosure_marshal_VOID__VOID,
188                   G_TYPE_NONE,
189                   0);
190
191
192   signals [RESPONSE] =
193     g_signal_new ("response",
194                   G_TYPE_FROM_CLASS (class),
195                   G_SIGNAL_RUN_FIRST,
196                   0,
197                   NULL, NULL,
198                   g_cclosure_marshal_VOID__INT,
199                   G_TYPE_NONE,
200                   1,
201                   G_TYPE_INT);
202
203
204   signals [VALIDITY_CHANGED] =
205     g_signal_new ("validity-changed",
206                   G_TYPE_FROM_CLASS (class),
207                   G_SIGNAL_RUN_FIRST,
208                   0,
209                   NULL, NULL,
210                   g_cclosure_marshal_VOID__BOOLEAN,
211                   G_TYPE_NONE,
212                   1,
213                   G_TYPE_BOOLEAN);
214
215
216   signals [DIALOG_HELP] =
217     g_signal_new ("help",
218                   G_TYPE_FROM_CLASS (class),
219                   G_SIGNAL_RUN_FIRST,
220                   0,
221                   NULL, NULL,
222                   g_cclosure_marshal_VOID__STRING,
223                   G_TYPE_NONE,
224                   1,
225                   G_TYPE_STRING);
226
227   parent_class = g_type_class_peek_parent (class);
228 }
229
230 static void
231 close_dialog (GtkWidget *w, gpointer data)
232 {
233   PsppireDialog *dialog = data;
234
235   psppire_dialog_close (dialog);
236 }
237
238 void
239 psppire_dialog_close (PsppireDialog *dialog)
240 {
241   g_main_loop_quit (dialog->loop);
242   gtk_widget_hide (GTK_WIDGET (dialog));
243 }
244
245 static void
246 delete_event_callback (GtkWidget *w, GdkEvent *e, gpointer data)
247 {
248   close_dialog (w, data);
249 }
250
251
252 static void
253 psppire_dialog_init (PsppireDialog *dialog)
254 {
255   dialog->contents_are_valid = NULL;
256   dialog->validity_data = NULL;
257   dialog->contents_are_acceptable = NULL;
258   dialog->acceptable_data = NULL;
259   dialog->slidable = FALSE;
260   dialog->help_page = NULL;
261
262   gtk_window_set_type_hint (GTK_WINDOW (dialog),
263         GDK_WINDOW_TYPE_HINT_DIALOG);
264
265   g_signal_connect (dialog, "delete-event",
266                     G_CALLBACK (delete_event_callback),
267                     dialog);
268
269   gtk_window_set_type_hint (GTK_WINDOW (dialog),
270         GDK_WINDOW_TYPE_HINT_DIALOG);
271
272   g_object_set (dialog, "icon-name", "pspp", NULL);
273 }
274
275 GtkWidget*
276 psppire_dialog_new (void)
277 {
278   PsppireDialog *dialog ;
279
280   dialog = g_object_new (psppire_dialog_get_type (),
281                          NULL);
282
283   return GTK_WIDGET (dialog) ;
284 }
285
286
287 void
288 psppire_dialog_notify_change (PsppireDialog *dialog)
289 {
290   if ( dialog->contents_are_valid )
291     {
292       gboolean valid = dialog->contents_are_valid (dialog->validity_data);
293
294       g_signal_emit (dialog, signals [VALIDITY_CHANGED], 0, valid);
295     }
296 }
297
298
299 static void
300 remove_notify_handlers (PsppireDialog *dialog, GObject *sel)
301 {
302   g_signal_handlers_disconnect_by_data (sel, dialog);
303 }
304
305
306 /* Descend the widget tree, connecting appropriate signals to the
307    psppire_dialog_notify_change callback */
308 static void
309 connect_notify_signal (GtkWidget *w, gpointer data)
310 {
311   PsppireDialog *dialog = data;
312
313   if ( PSPPIRE_IS_BUTTONBOX (w))
314     return;
315
316   if ( GTK_IS_CONTAINER (w))
317     {
318       gtk_container_foreach (GTK_CONTAINER (w),
319                              connect_notify_signal,
320                              dialog);
321     }
322
323
324   /* It's unfortunate that GTK+ doesn't have a generic
325      "user-modified-state-changed" signal.  Instead, we have to try and
326      predict what widgets and signals are likely to exist in our dialogs. */
327
328   if ( GTK_IS_TOGGLE_BUTTON (w))
329     {
330       g_signal_connect_swapped (w, "toggled",
331                                 G_CALLBACK (psppire_dialog_notify_change),
332                                 dialog);
333     }
334
335   if ( PSPPIRE_IS_SELECTOR (w))
336     {
337       g_signal_connect_swapped (w, "selected",
338                                 G_CALLBACK (psppire_dialog_notify_change),
339                                 dialog);
340
341       g_signal_connect_swapped (w, "de-selected",
342                                 G_CALLBACK (psppire_dialog_notify_change),
343                                 dialog);
344     }
345
346   if ( GTK_IS_EDITABLE (w))
347     {
348       g_signal_connect_swapped (w, "changed",
349                                 G_CALLBACK (psppire_dialog_notify_change),
350                                 dialog);
351     }
352
353   if ( GTK_IS_CELL_EDITABLE (w))
354     {
355       g_signal_connect_swapped (w, "editing-done",
356                                 G_CALLBACK (psppire_dialog_notify_change),
357                                 dialog);
358     }
359
360   if ( GTK_IS_TEXT_VIEW (w))
361     {
362       GtkTextBuffer *buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (w));
363
364       g_signal_connect_swapped (buffer, "changed",
365                                 G_CALLBACK (psppire_dialog_notify_change),
366                                 dialog);
367     }
368
369   if ( GTK_IS_TREE_VIEW (w))
370     {
371       gint i = 0;
372       GtkTreeView *tv = GTK_TREE_VIEW (w);
373       GtkTreeSelection *selection =
374         gtk_tree_view_get_selection (tv);
375       GtkTreeViewColumn *col;
376       GtkTreeModel *model = gtk_tree_view_get_model (tv);
377
378       if ( model)
379         {
380           g_signal_connect_swapped (model, "row-changed",
381                                     G_CALLBACK (psppire_dialog_notify_change),
382                                     dialog);
383
384           g_signal_connect_swapped (model, "row-deleted",
385                                     G_CALLBACK (psppire_dialog_notify_change),
386                                     dialog);
387
388           g_signal_connect_swapped (model, "row-inserted",
389                                     G_CALLBACK (psppire_dialog_notify_change),
390                                     dialog);
391
392           g_signal_connect (dialog, "destroy", G_CALLBACK (remove_notify_handlers),
393                             model);
394         }
395       
396       g_signal_connect_swapped (selection, "changed",
397                                 G_CALLBACK (psppire_dialog_notify_change),
398                                 dialog);
399
400       while ((col = gtk_tree_view_get_column (tv, i++)))
401         {
402           GList *renderers = gtk_cell_layout_get_cells (GTK_CELL_LAYOUT (col));
403           GList *start = renderers;
404           while (renderers)
405             {
406               if ( GTK_IS_CELL_RENDERER_TOGGLE (renderers->data))
407                 g_signal_connect_swapped (renderers->data, "toggled",
408                                           G_CALLBACK (psppire_dialog_notify_change), dialog);
409               renderers = renderers->next;
410             }
411           g_list_free (start);
412         }
413     }
414 }
415
416
417 gint
418 psppire_dialog_run (PsppireDialog *dialog)
419 {
420   gchar *title = NULL;
421   g_object_get (dialog, "title", &title, NULL);
422
423   if (title == NULL)
424     g_warning ("PsppireDialog %s has no title", gtk_widget_get_name (GTK_WIDGET (dialog)));
425   
426   if ( dialog->contents_are_valid != NULL )
427     gtk_container_foreach (GTK_CONTAINER (gtk_bin_get_child(GTK_BIN(dialog))),
428                            connect_notify_signal,
429                            dialog);
430
431   dialog->loop = g_main_loop_new (NULL, FALSE);
432
433   gtk_widget_show (GTK_WIDGET (dialog));
434
435   if ( dialog->contents_are_valid != NULL)
436     g_signal_emit (dialog, signals [VALIDITY_CHANGED], 0, FALSE);
437
438   g_signal_emit (dialog, signals [DIALOG_REFRESH], 0);
439
440   g_main_loop_run (dialog->loop);
441
442   g_main_loop_unref (dialog->loop);
443
444   g_signal_emit (dialog, signals [RESPONSE], 0, dialog->response);
445
446   return dialog->response;
447 }
448
449
450 void
451 psppire_dialog_reload (PsppireDialog *dialog)
452 {
453   g_signal_emit (dialog, signals [DIALOG_REFRESH], 0);
454 }
455
456
457 void
458 psppire_dialog_help (PsppireDialog *dialog)
459 {
460   const char *page = NULL;
461
462   g_object_get (dialog, "help-page", &page, NULL);
463
464   online_help (page);
465
466   g_signal_emit (dialog, signals [DIALOG_HELP], 0, page);
467 }
468
469 /* Sets a predicate function that is checked after each change that the user
470    makes to the dialog's state.  If the predicate function returns false, then
471    "OK" and other buttons that accept the dialog's settings will be
472    disabled. */
473 void
474 psppire_dialog_set_valid_predicate (PsppireDialog *dialog,
475                                     ContentsAreValid contents_are_valid,
476                                     gpointer data)
477 {
478   dialog->contents_are_valid = contents_are_valid;
479   dialog->validity_data = data;
480 }
481
482 /* Sets a predicate function that is called after "OK" or another button that
483    accepts the dialog's settings is pushed.  If the predicate function returns
484    false, then the button push is ignored.  (If the predicate function returns
485    false, then it should take some action to notify the user why the contents
486    are unacceptable, e.g. pop up a dialog box.)
487
488    An accept predicate is preferred over a validity predicate when the reason
489    why the dialog settings are unacceptable may not be obvious to the user, so
490    that the user needs a helpful message to explain. */
491 void
492 psppire_dialog_set_accept_predicate (PsppireDialog *dialog,
493                                      ContentsAreValid contents_are_acceptable,
494                                      gpointer data)
495 {
496   dialog->contents_are_acceptable = contents_are_acceptable;
497   dialog->acceptable_data = data;
498 }
499
500 gboolean
501 psppire_dialog_is_acceptable (const PsppireDialog *dialog)
502 {
503   return (dialog->contents_are_acceptable == NULL
504           || dialog->contents_are_acceptable (dialog->acceptable_data));
505 }