Warnings: missing initializer for value_tables and function type cast (GObject)
[pspp] / src / ui / gui / psppire-output-window.c
1 /* PSPPIRE - a graphical user interface for PSPP.
2    Copyright (C) 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2016  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 #include <config.h>
18
19 #include "ui/gui/psppire-output-window.h"
20
21 #include <errno.h>
22 #include <gtk/gtk.h>
23 #include <stdlib.h>
24 #include <sys/stat.h>
25 #include <sys/types.h>
26 #include <unistd.h>
27
28 #include "libpspp/cast.h"
29 #include "libpspp/message.h"
30 #include "libpspp/string-map.h"
31 #include "output/chart-item.h"
32 #include "output/driver-provider.h"
33 #include "output/message-item.h"
34 #include "output/output-item.h"
35 #include "output/table-item.h"
36 #include "output/text-item.h"
37 #include "ui/gui/help-menu.h"
38 #include "ui/gui/builder-wrapper.h"
39 #include "ui/gui/psppire-output-view.h"
40 #include "ui/gui/psppire-conf.h"
41 #include "ui/gui/windows-menu.h"
42
43 #include "gl/xalloc.h"
44
45 #include "helper.h"
46
47 #include <gettext.h>
48 #define _(msgid) gettext (msgid)
49 #define N_(msgid) msgid
50
51 G_DEFINE_TYPE (PsppireOutputWindow, psppire_output_window, PSPPIRE_TYPE_WINDOW)
52
53 static GObjectClass *parent_class;
54
55 static void
56 psppire_output_window_finalize (GObject *object)
57 {
58   if (G_OBJECT_CLASS (parent_class)->finalize)
59     (*G_OBJECT_CLASS (parent_class)->finalize) (object);
60 }
61
62
63 static void
64 psppire_output_window_dispose (GObject *obj)
65 {
66   PsppireOutputWindow *window = PSPPIRE_OUTPUT_WINDOW (obj);
67
68   if (window->dispose_has_run)
69     return;
70
71   window->dispose_has_run = TRUE;
72   psppire_output_view_destroy (window->view);
73   window->view = NULL;
74
75   /* Chain up to the parent class */
76   G_OBJECT_CLASS (parent_class)->dispose (obj);
77 }
78
79 static void
80 psppire_output_window_class_init (PsppireOutputWindowClass *class)
81 {
82   GObjectClass *object_class = G_OBJECT_CLASS (class);
83
84   parent_class = g_type_class_peek_parent (class);
85   object_class->dispose = psppire_output_window_dispose;
86
87   object_class->finalize = psppire_output_window_finalize;
88 }
89 \f
90 /* Output driver class. */
91
92 struct psppire_output_driver
93   {
94     struct output_driver driver;
95     PsppireOutputWindow *window;
96   };
97
98 static struct output_driver_class psppire_output_class;
99
100 static struct psppire_output_driver *
101 psppire_output_cast (struct output_driver *driver)
102 {
103   assert (driver->class == &psppire_output_class);
104   return UP_CAST (driver, struct psppire_output_driver, driver);
105 }
106
107 static void
108 psppire_output_submit (struct output_driver *this,
109                        const struct output_item *item)
110 {
111   struct psppire_output_driver *pod = psppire_output_cast (this);
112   PsppireOutputWindow *window;
113   bool new;
114
115   new = pod->window == NULL;
116   if (new)
117     {
118       pod->window = PSPPIRE_OUTPUT_WINDOW (psppire_output_window_new ());
119       GApplication *app = g_application_get_default ();
120       gtk_application_add_window (GTK_APPLICATION (app),
121                                   GTK_WINDOW (pod->window));
122
123       pod->window->driver = pod;
124     }
125   window = pod->window;
126
127   psppire_output_view_put (window->view, item);
128
129   if (new)
130     {
131       /* We could have called this earlier in the previous "if (new)" block,
132          but doing it here finds, in a plain GTK+ environment, a bug that
133          otherwise only showed up on an Ubuntu Unity desktop.  See bug
134          #43362. */
135       gtk_widget_show_all (GTK_WIDGET (pod->window));
136     }
137
138   PsppireConf *conf = psppire_conf_new ();
139   {
140     gboolean status = true;
141     psppire_conf_get_boolean (conf, "OutputWindowAction", "alert",
142                               &status);
143     gtk_window_set_urgency_hint (GTK_WINDOW (pod->window), status);
144   }
145
146   {
147     gboolean status ;
148     if (psppire_conf_get_boolean (conf, "OutputWindowAction", "maximize",
149                                   &status) && status)
150       gtk_window_maximize (GTK_WINDOW (pod->window));
151   }
152
153   {
154     gboolean status ;
155     if (psppire_conf_get_boolean (conf, "OutputWindowAction", "raise",
156                                   &status) && status)
157       gtk_window_present (GTK_WINDOW (pod->window));
158   }
159 }
160
161 static struct output_driver_class psppire_output_class =
162   {
163     "PSPPIRE",                  /* name */
164     NULL,                       /* destroy */
165     psppire_output_submit,      /* submit */
166     NULL,                       /* flush */
167   };
168
169 void
170 psppire_output_window_setup (void)
171 {
172   struct psppire_output_driver *pod;
173   struct output_driver *d;
174
175   pod = xzalloc (sizeof *pod);
176   d = &pod->driver;
177   output_driver_init (d, &psppire_output_class, "PSPPIRE",
178                       SETTINGS_DEVICE_UNFILTERED);
179   output_driver_register (d);
180 }
181
182 \f
183
184 /* Callback for the "delete" action (clicking the x on the top right
185    hand corner of the window) */
186 static gboolean
187 on_delete (GtkWidget *w, GdkEvent *event, gpointer user_data)
188 {
189   PsppireOutputWindow *ow = PSPPIRE_OUTPUT_WINDOW (user_data);
190
191   gtk_widget_destroy (GTK_WIDGET (ow));
192
193   ow->driver->window = NULL;
194
195   return FALSE;
196 }
197
198
199
200 static void
201 cancel_urgency (GtkWindow *window,  gpointer data)
202 {
203   gtk_window_set_urgency_hint (window, FALSE);
204 }
205
206 static void psppire_output_window_print (PsppireOutputWindow *window);
207
208
209 static void
210 export_output (PsppireOutputWindow *window, struct string_map *options,
211                const char *format)
212 {
213   string_map_insert (options, "format", format);
214   psppire_output_view_export (window->view, options);
215 }
216
217
218 struct file_types
219 {
220   const gchar *label;
221   const gchar *ext;
222 };
223
224 enum
225   {
226     FT_AUTO = 0,
227     FT_SPV,
228     FT_PDF,
229     FT_HTML,
230     FT_ODT,
231     FT_TXT,
232     FT_ASCII,
233     FT_PS,
234     FT_CSV,
235     n_FT
236   };
237
238 #define N_EXTENSIONS (n_FT - 1)
239
240 struct file_types ft[n_FT] = {
241   {N_("Infer file type from extension"),  NULL},
242   {N_("SPSS Viewer (*.spv)"),             ".spv"},
243   {N_("PDF (*.pdf)"),                     ".pdf"},
244   {N_("HTML (*.html)"),                   ".html"},
245   {N_("OpenDocument (*.odt)"),            ".odt"},
246   {N_("Text (*.txt)"),                    ".txt"},
247   {N_("Text [plain] (*.txt)"),            ".txt"},
248   {N_("PostScript (*.ps)"),               ".ps"},
249   {N_("Comma-Separated Values (*.csv)"),  ".csv"}
250 };
251
252
253 static void
254 on_combo_change (GtkFileChooser *chooser)
255 {
256   gboolean sensitive = FALSE;
257   GtkWidget *combo = gtk_file_chooser_get_extra_widget (chooser);
258
259   int x = 0;
260   gchar *fn = gtk_file_chooser_get_filename (chooser);
261
262   if (combo &&  gtk_widget_get_realized (combo))
263     x = gtk_combo_box_get_active (GTK_COMBO_BOX (combo));
264
265   if (fn == NULL)
266     {
267       sensitive = FALSE;
268     }
269   else
270     {
271       gint i;
272       if (x != 0)
273         sensitive = TRUE;
274
275       for (i = 1 ; i < N_EXTENSIONS ; ++i)
276         {
277           if (g_str_has_suffix (fn, ft[i].ext))
278             {
279               sensitive = TRUE;
280               break;
281             }
282         }
283     }
284
285   g_free (fn);
286
287   gtk_dialog_set_response_sensitive (GTK_DIALOG (chooser), GTK_RESPONSE_ACCEPT, sensitive);
288 }
289
290
291 static void
292 on_file_chooser_change (GObject *w, GParamSpec *pspec, gpointer data)
293 {
294
295   GtkFileChooser *chooser = data;
296   const gchar *name = g_param_spec_get_name (pspec);
297
298   if (! gtk_widget_get_realized (GTK_WIDGET (chooser)))
299     return;
300
301   /* Ignore this one.  It causes recursion. */
302   if (0 == strcmp ("tooltip-text", name))
303     return;
304
305   on_combo_change (chooser);
306 }
307
308
309 /* Recursively descend all the children of W, connecting
310    to their "notify" signal */
311 static void
312 iterate_widgets (GtkWidget *w, gpointer data)
313 {
314   if (GTK_IS_CONTAINER (w))
315     gtk_container_forall (GTK_CONTAINER (w), iterate_widgets, data);
316   else
317     g_signal_connect (w, "notify",  G_CALLBACK (on_file_chooser_change), data);
318 }
319
320
321
322 static GtkListStore *
323 create_file_type_list (void)
324 {
325   int i;
326   GtkTreeIter iter;
327   GtkListStore *list = gtk_list_store_new (2, G_TYPE_STRING, G_TYPE_STRING);
328
329   for (i = 0 ; i < n_FT ; ++i)
330     {
331       gtk_list_store_append (list, &iter);
332       gtk_list_store_set (list, &iter,
333                           0,  gettext (ft[i].label),
334                           1,  ft[i].ext,
335                           -1);
336     }
337
338   return list;
339 }
340
341 static void
342 psppire_output_window_export (PsppireOutputWindow *window)
343 {
344   gint response;
345   GtkWidget *combo;
346   GtkListStore *list;
347
348   GtkFileChooser *chooser;
349
350   GtkWidget *dialog = gtk_file_chooser_dialog_new (_("Export Output"),
351                                         GTK_WINDOW (window),
352                                         GTK_FILE_CHOOSER_ACTION_SAVE,
353                                         _("Cancel"), GTK_RESPONSE_CANCEL,
354                                         _("Save"),   GTK_RESPONSE_ACCEPT,
355                                         NULL);
356
357   g_object_set (dialog, "local-only", FALSE, NULL);
358
359   chooser = GTK_FILE_CHOOSER (dialog);
360
361   list = create_file_type_list ();
362
363   combo = gtk_combo_box_new_with_model (GTK_TREE_MODEL (list));
364
365
366   {
367     /* Create text cell renderer */
368     GtkCellRenderer *cell = gtk_cell_renderer_text_new();
369     gtk_cell_layout_pack_start (GTK_CELL_LAYOUT (combo), cell, FALSE);
370
371     gtk_cell_layout_add_attribute (GTK_CELL_LAYOUT (combo), cell,  "text", 0);
372   }
373
374   g_signal_connect_swapped (combo, "changed", G_CALLBACK (on_combo_change), chooser);
375
376   gtk_combo_box_set_active (GTK_COMBO_BOX (combo), 0);
377
378   gtk_file_chooser_set_extra_widget (chooser, combo);
379
380   /* This kludge is necessary because there is no signal to tell us
381      when the candidate filename of a GtkFileChooser has changed */
382   gtk_container_forall (GTK_CONTAINER (dialog), iterate_widgets, dialog);
383
384
385   gtk_file_chooser_set_do_overwrite_confirmation (chooser, TRUE);
386
387   response = gtk_dialog_run (GTK_DIALOG (dialog));
388
389   if (response == GTK_RESPONSE_ACCEPT)
390     {
391       gint file_type = gtk_combo_box_get_active (GTK_COMBO_BOX (combo));
392       gchar *filename = gtk_file_chooser_get_filename (chooser);
393       struct string_map options;
394
395       g_return_if_fail (filename);
396
397       if (file_type == FT_AUTO)
398         {
399           /* If the "Infer file type from extension" option was chosen,
400              search for the respective type in the list.
401              (It's a O(n) search, but fortunately n is small). */
402           gint i;
403           for (i = 1 ; i < N_EXTENSIONS ; ++i)
404             {
405               if (g_str_has_suffix (filename, ft[i].ext))
406                 {
407                   file_type = i;
408                   break;
409                 }
410             }
411         }
412       else if (! g_str_has_suffix (filename, ft[file_type].ext))
413         {
414           /* If an explicit document format was chosen, and if the chosen
415              filename does not already have that particular "extension",
416              then append it.
417            */
418
419           gchar *of = filename;
420           filename = g_strconcat (filename, ft[file_type].ext, NULL);
421           g_free (of);
422         }
423
424       string_map_init (&options);
425       string_map_insert (&options, "output-file", filename);
426
427       switch (file_type)
428         {
429         case FT_SPV:
430           export_output (window, &options, "spv");
431           break;
432         case FT_PDF:
433           export_output (window, &options, "pdf");
434           break;
435         case FT_HTML:
436           export_output (window, &options, "html");
437           break;
438         case FT_ODT:
439           export_output (window, &options, "odt");
440           break;
441         case FT_PS:
442           export_output (window, &options, "ps");
443           break;
444         case FT_CSV:
445           export_output (window, &options, "csv");
446           break;
447
448         case FT_TXT:
449           string_map_insert (&options, "box", "unicode");
450           /* Fall through */
451
452         case FT_ASCII:
453           string_map_insert (&options, "charts", "none");
454           export_output (window, &options, "txt");
455           break;
456         default:
457           g_assert_not_reached ();
458         }
459
460       string_map_destroy (&options);
461
462       free (filename);
463     }
464
465   gtk_widget_destroy (dialog);
466 }
467
468 static void
469 psppire_output_window_init (PsppireOutputWindow *window)
470 {
471   GtkBuilder *xml = builder_new ("output-window.ui");
472   GtkApplication *app = GTK_APPLICATION (g_application_get_default());
473   GtkWidget *box = gtk_box_new (GTK_ORIENTATION_VERTICAL, 0);
474   gtk_container_add (GTK_CONTAINER (window), box);
475
476   GtkWidget *paned = get_widget_assert (xml, "paned1");
477
478   window->dispose_has_run = FALSE;
479
480   window->view = psppire_output_view_new (
481     GTK_LAYOUT (get_widget_assert (xml, "output")),
482     GTK_TREE_VIEW (get_widget_assert (xml, "overview")));
483
484   g_signal_connect (window,
485                     "focus-in-event",
486                     G_CALLBACK (cancel_urgency),
487                     NULL);
488
489   GObject *menu = get_object_assert (xml, "output-window-menu", G_TYPE_MENU);
490   GtkWidget *menubar = gtk_menu_bar_new_from_model (G_MENU_MODEL (menu));
491   gtk_box_pack_start (GTK_BOX (box), menubar, FALSE, FALSE, 0);
492   gtk_box_pack_start (GTK_BOX (box), paned, TRUE, TRUE, 0);
493
494   gtk_menu_shell_append (GTK_MENU_SHELL (menubar),
495                          create_windows_menu (GTK_WINDOW (window)));
496
497   gtk_menu_shell_append (GTK_MENU_SHELL (menubar),
498                          create_help_menu (GTK_WINDOW (window)));
499
500   {
501     GSimpleAction *print = g_simple_action_new ("print", NULL);
502     g_signal_connect_swapped (print, "activate", G_CALLBACK (psppire_output_window_print), window);
503     g_action_map_add_action (G_ACTION_MAP (window), G_ACTION (print));
504
505
506     const gchar *accels[2] = { "<Primary>P", NULL};
507     gtk_application_set_accels_for_action (app,
508                                            "win.print",
509                                            accels);
510   }
511
512
513   {
514     GSimpleAction *export = g_simple_action_new ("export", NULL);
515     g_signal_connect_swapped (export, "activate", G_CALLBACK (psppire_output_window_export), window);
516     g_action_map_add_action (G_ACTION_MAP (window), G_ACTION (export));
517   }
518
519   {
520     GSimpleAction *select_all = g_simple_action_new ("select-all", NULL);
521     g_action_map_add_action (G_ACTION_MAP (window), G_ACTION (select_all));
522   }
523
524   {
525     GSimpleAction *copy = g_simple_action_new ("copy", NULL);
526     g_action_map_add_action (G_ACTION_MAP (window), G_ACTION (copy));
527
528     const gchar *accels[2] = { "<Primary>C", NULL};
529     gtk_application_set_accels_for_action (app,
530                                            "win.copy",
531                                            accels);
532   }
533
534
535   g_object_unref (xml);
536
537   g_signal_connect (window, "delete-event",
538                     G_CALLBACK (on_delete), window);
539 }
540
541
542 GtkWidget*
543 psppire_output_window_new (void)
544 {
545   return GTK_WIDGET (g_object_new (psppire_output_window_get_type (),
546                                    /* TRANSLATORS: This will be part of a filename.  Please avoid whitespace. */
547                                    "filename", _("Output"),
548                                    "description", _("Output Viewer"),
549                                    NULL));
550 }
551
552 static void
553 psppire_output_window_print (PsppireOutputWindow *window)
554 {
555   psppire_output_view_print (window->view, GTK_WINDOW (window));
556 }