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