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