918cae0ec1a1b305abe41383c6ad858c9b78e453
[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 /* Descend the widget tree, connecting appropriate signals to the
300    psppire_dialog_notify_change callback */
301 static void
302 connect_notify_signal (GtkWidget *w, gpointer data)
303 {
304   PsppireDialog *dialog = data;
305
306   if ( PSPPIRE_IS_BUTTONBOX (w))
307     return;
308
309   if ( GTK_IS_CONTAINER (w))
310     {
311       gtk_container_foreach (GTK_CONTAINER (w),
312                              connect_notify_signal,
313                              dialog);
314     }
315
316
317   /* It's unfortunate that GTK+ doesn't have a generic
318      "user-modified-state-changed" signal.  Instead, we have to try and
319      predict what widgets and signals are likely to exist in our dialogs. */
320
321   if ( GTK_IS_TOGGLE_BUTTON (w))
322     {
323       g_signal_connect_swapped (w, "toggled",
324                                 G_CALLBACK (psppire_dialog_notify_change),
325                                 dialog);
326     }
327
328   if ( PSPPIRE_IS_SELECTOR (w))
329     {
330       g_signal_connect_swapped (w, "selected",
331                                 G_CALLBACK (psppire_dialog_notify_change),
332                                 dialog);
333
334       g_signal_connect_swapped (w, "de-selected",
335                                 G_CALLBACK (psppire_dialog_notify_change),
336                                 dialog);
337       
338       psppire_selector_update_subjects (PSPPIRE_SELECTOR (w));
339     }
340
341   if ( GTK_IS_EDITABLE (w))
342     {
343       g_signal_connect_swapped (w, "changed",
344                                 G_CALLBACK (psppire_dialog_notify_change),
345                                 dialog);
346     }
347
348   if ( GTK_IS_CELL_EDITABLE (w))
349     {
350       g_signal_connect_swapped (w, "editing-done",
351                                 G_CALLBACK (psppire_dialog_notify_change),
352                                 dialog);
353     }
354
355   if ( GTK_IS_TEXT_VIEW (w))
356     {
357       GtkTextBuffer *buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (w));
358
359       g_signal_connect_swapped (buffer, "changed",
360                                 G_CALLBACK (psppire_dialog_notify_change),
361                                 dialog);
362     }
363
364   if ( GTK_IS_TREE_VIEW (w))
365     {
366       gint i = 0;
367       GtkTreeView *tv = GTK_TREE_VIEW (w);
368       GtkTreeSelection *selection =
369         gtk_tree_view_get_selection (tv);
370       GtkTreeViewColumn *col;
371       GtkTreeModel *model = gtk_tree_view_get_model (tv);
372
373       if ( model)
374         {
375           g_signal_connect_swapped (model, "row-changed",
376                                     G_CALLBACK (psppire_dialog_notify_change),
377                                     dialog);
378
379           g_signal_connect_swapped (model, "row-deleted",
380                                     G_CALLBACK (psppire_dialog_notify_change),
381                                     dialog);
382
383           g_signal_connect_swapped (model, "row-inserted",
384                                     G_CALLBACK (psppire_dialog_notify_change),
385                                     dialog);
386
387         }
388       
389       g_signal_connect_swapped (selection, "changed",
390                                 G_CALLBACK (psppire_dialog_notify_change),
391                                 dialog);
392
393       while ((col = gtk_tree_view_get_column (tv, i++)))
394         {
395           GList *renderers = gtk_cell_layout_get_cells (GTK_CELL_LAYOUT (col));
396           GList *start = renderers;
397           while (renderers)
398             {
399               if ( GTK_IS_CELL_RENDERER_TOGGLE (renderers->data))
400                 g_signal_connect_swapped (renderers->data, "toggled",
401                                           G_CALLBACK (psppire_dialog_notify_change), dialog);
402               renderers = renderers->next;
403             }
404           g_list_free (start);
405         }
406     }
407 }
408
409
410 gint
411 psppire_dialog_run (PsppireDialog *dialog)
412 {
413   gchar *title = NULL;
414   g_object_get (dialog, "title", &title, NULL);
415
416   if (title == NULL)
417     g_warning ("PsppireDialog %s has no title", gtk_widget_get_name (GTK_WIDGET (dialog)));
418
419   if ( dialog->contents_are_valid != NULL )
420     gtk_container_foreach (GTK_CONTAINER (gtk_bin_get_child(GTK_BIN(dialog))),
421                            connect_notify_signal,
422                            dialog);
423
424   dialog->loop = g_main_loop_new (NULL, FALSE);
425
426   gtk_widget_show (GTK_WIDGET (dialog));
427   psppire_dialog_notify_change (dialog);
428
429   g_main_loop_run (dialog->loop);
430
431   g_main_loop_unref (dialog->loop);
432
433   g_signal_emit (dialog, signals [RESPONSE], 0, dialog->response);
434
435   return dialog->response;
436 }
437
438
439 void
440 psppire_dialog_reload (PsppireDialog *dialog)
441 {
442   g_signal_emit (dialog, signals [DIALOG_REFRESH], 0);
443 }
444
445
446 void
447 psppire_dialog_help (PsppireDialog *dialog)
448 {
449   const char *page = NULL;
450
451   g_object_get (dialog, "help-page", &page, NULL);
452
453   online_help (page);
454
455   g_signal_emit (dialog, signals [DIALOG_HELP], 0, page);
456 }
457
458 /* Sets a predicate function that is checked after each change that the user
459    makes to the dialog's state.  If the predicate function returns false, then
460    "OK" and other buttons that accept the dialog's settings will be
461    disabled. */
462 void
463 psppire_dialog_set_valid_predicate (PsppireDialog *dialog,
464                                     ContentsAreValid contents_are_valid,
465                                     gpointer data)
466 {
467   dialog->contents_are_valid = contents_are_valid;
468   dialog->validity_data = data;
469 }
470
471 /* Sets a predicate function that is called after "OK" or another button that
472    accepts the dialog's settings is pushed.  If the predicate function returns
473    false, then the button push is ignored.  (If the predicate function returns
474    false, then it should take some action to notify the user why the contents
475    are unacceptable, e.g. pop up a dialog box.)
476
477    An accept predicate is preferred over a validity predicate when the reason
478    why the dialog settings are unacceptable may not be obvious to the user, so
479    that the user needs a helpful message to explain. */
480 void
481 psppire_dialog_set_accept_predicate (PsppireDialog *dialog,
482                                      ContentsAreValid contents_are_acceptable,
483                                      gpointer data)
484 {
485   dialog->contents_are_acceptable = contents_are_acceptable;
486   dialog->acceptable_data = data;
487 }
488
489 gboolean
490 psppire_dialog_is_acceptable (const PsppireDialog *dialog)
491 {
492   return (dialog->contents_are_acceptable == NULL
493           || dialog->contents_are_acceptable (dialog->acceptable_data));
494 }