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