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