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