psppire-output-window: Factor out output view as psppire-output-view.
[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  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/cairo.h"
32 #include "output/chart-item.h"
33 #include "output/driver-provider.h"
34 #include "output/message-item.h"
35 #include "output/output-item.h"
36 #include "output/tab.h"
37 #include "output/table-item.h"
38 #include "output/text-item.h"
39 #include "ui/gui/help-menu.h"
40 #include "ui/gui/builder-wrapper.h"
41 #include "ui/gui/psppire-output-view.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
143   if (pod->window == NULL)
144     {
145       pod->window = PSPPIRE_OUTPUT_WINDOW (psppire_output_window_new ());
146       gtk_widget_show_all (GTK_WIDGET (pod->window));
147       pod->window->driver = pod;
148     }
149   window = pod->window;
150
151   psppire_output_view_put (window->view, item);
152
153   gtk_window_set_urgency_hint (GTK_WINDOW (pod->window), TRUE);
154 }
155
156 static struct output_driver_class psppire_output_class =
157   {
158     "PSPPIRE",                  /* name */
159     NULL,                       /* destroy */
160     psppire_output_submit,      /* submit */
161     NULL,                       /* flush */
162   };
163
164 void
165 psppire_output_window_setup (void)
166 {
167   struct psppire_output_driver *pod;
168   struct output_driver *d;
169
170   pod = xzalloc (sizeof *pod);
171   d = &pod->driver;
172   output_driver_init (d, &psppire_output_class, "PSPPIRE",
173                       SETTINGS_DEVICE_UNFILTERED);
174   output_driver_register (d);
175 }
176
177 \f
178
179 /* Callback for the "delete" action (clicking the x on the top right
180    hand corner of the window) */
181 static gboolean
182 on_delete (GtkWidget *w, GdkEvent *event, gpointer user_data)
183 {
184   PsppireOutputWindow *ow = PSPPIRE_OUTPUT_WINDOW (user_data);
185
186   gtk_widget_destroy (GTK_WIDGET (ow));
187
188   ow->driver->window = NULL;
189
190   return FALSE;
191 }
192
193
194
195 static void
196 cancel_urgency (GtkWindow *window,  gpointer data)
197 {
198   gtk_window_set_urgency_hint (window, FALSE);
199 }
200
201 static void psppire_output_window_print (PsppireOutputWindow *window);
202
203
204 static void
205 export_output (PsppireOutputWindow *window, struct string_map *options,
206                const char *format)
207 {
208   string_map_insert (options, "format", format);
209   psppire_output_view_export (window->view, options);
210 }
211
212
213 struct file_types
214 {
215   const gchar *label;
216   const gchar *ext;
217 };
218
219 enum 
220   {
221     FT_AUTO = 0,
222     FT_PDF,
223     FT_HTML,
224     FT_ODT,
225     FT_TXT,
226     FT_PS,
227     FT_CSV,
228     n_FT
229   };
230
231 #define N_EXTENSIONS (n_FT - 1)
232
233 struct file_types ft[n_FT] = {
234   {N_("Infer file type from extension"),  NULL},
235   {N_("PDF (*.pdf)"),                     ".pdf"},
236   {N_("HTML (*.html)"),                   ".html"},
237   {N_("OpenDocument (*.odt)"),            ".odt"},
238   {N_("Text (*.txt)"),                    ".txt"},
239   {N_("PostScript (*.ps)"),               ".ps"},
240   {N_("Comma-Separated Values (*.csv)"),  ".csv"}
241 };
242
243
244 static void
245 on_combo_change (GtkFileChooser *chooser)
246 {
247   gboolean sensitive = FALSE;
248   GtkWidget *combo = gtk_file_chooser_get_extra_widget (chooser);
249
250   int x = 0; 
251   gchar *fn = gtk_file_chooser_get_filename (chooser);
252
253   if (combo &&  gtk_widget_get_realized (combo))
254     x = gtk_combo_box_get_active (GTK_COMBO_BOX (combo));
255
256   if (fn == NULL)
257     {
258       sensitive = FALSE;
259     }
260   else
261     {
262       gint i;
263       if ( x != 0 )
264         sensitive = TRUE;
265
266       for (i = 1 ; i < N_EXTENSIONS ; ++i)
267         {
268           if ( g_str_has_suffix (fn, ft[i].ext))
269             {
270               sensitive = TRUE;
271               break;
272             }
273         }
274     }
275
276   g_free (fn);
277
278   gtk_dialog_set_response_sensitive (GTK_DIALOG (chooser), GTK_RESPONSE_ACCEPT, sensitive);
279 }
280
281
282 static void
283 on_file_chooser_change (GObject *w, GParamSpec *pspec, gpointer data)
284 {
285
286   GtkFileChooser *chooser = data;
287   const gchar *name = g_param_spec_get_name (pspec);
288
289   if ( ! gtk_widget_get_realized (GTK_WIDGET (chooser)))
290     return;
291
292   /* Ignore this one.  It causes recursion. */
293   if ( 0 == strcmp ("tooltip-text", name))
294     return;
295
296   on_combo_change (chooser);
297 }
298
299
300 /* Recursively descend all the children of W, connecting
301    to their "notify" signal */
302 static void
303 iterate_widgets (GtkWidget *w, gpointer data)
304 {
305   if ( GTK_IS_CONTAINER (w))
306     gtk_container_forall (GTK_CONTAINER (w), iterate_widgets, data);
307   else
308     g_signal_connect (w, "notify",  G_CALLBACK (on_file_chooser_change), data);
309 }
310
311
312
313 static GtkListStore *
314 create_file_type_list (void)
315 {
316   int i;
317   GtkTreeIter iter;
318   GtkListStore *list = gtk_list_store_new (2, G_TYPE_STRING, G_TYPE_STRING);
319   
320   for (i = 0 ; i < n_FT ; ++i)
321     {
322       gtk_list_store_append (list, &iter);
323       gtk_list_store_set (list, &iter,
324                           0,  gettext (ft[i].label),
325                           1,  ft[i].ext,
326                           -1);
327     }
328   
329   return list;
330 }
331
332 static void
333 psppire_output_window_export (PsppireOutputWindow *window)
334 {
335   gint response;
336   GtkWidget *combo;
337   GtkListStore *list;
338
339   GtkFileChooser *chooser;
340   
341   GtkWidget *dialog = gtk_file_chooser_dialog_new (_("Export Output"),
342                                         GTK_WINDOW (window),
343                                         GTK_FILE_CHOOSER_ACTION_SAVE,
344                                         GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL,
345                                         GTK_STOCK_SAVE,   GTK_RESPONSE_ACCEPT,
346                                         NULL);
347
348   g_object_set (dialog, "local-only", FALSE, NULL);
349
350   chooser = GTK_FILE_CHOOSER (dialog);
351
352   list = create_file_type_list ();
353
354   combo = gtk_combo_box_new_with_model (GTK_TREE_MODEL (list));
355
356
357   {
358     /* Create text cell renderer */
359     GtkCellRenderer *cell = gtk_cell_renderer_text_new();
360     gtk_cell_layout_pack_start (GTK_CELL_LAYOUT (combo), cell, FALSE );
361
362     gtk_cell_layout_add_attribute (GTK_CELL_LAYOUT (combo), cell,  "text", 0);
363   }
364
365   g_signal_connect_swapped (combo, "changed", G_CALLBACK (on_combo_change), chooser);
366
367   gtk_combo_box_set_active (GTK_COMBO_BOX (combo), 0);
368
369   gtk_file_chooser_set_extra_widget (chooser, combo);
370
371   /* This kludge is necessary because there is no signal to tell us
372      when the candidate filename of a GtkFileChooser has changed */
373   gtk_container_forall (GTK_CONTAINER (dialog), iterate_widgets, dialog);
374
375
376   gtk_file_chooser_set_do_overwrite_confirmation (chooser, TRUE);
377
378   response = gtk_dialog_run (GTK_DIALOG (dialog));
379
380   if ( response == GTK_RESPONSE_ACCEPT )
381     {
382       gint file_type = gtk_combo_box_get_active (GTK_COMBO_BOX (combo));
383       gchar *filename = gtk_file_chooser_get_filename (chooser);
384       struct string_map options;
385
386       g_return_if_fail (filename);
387
388       if (file_type == FT_AUTO)
389         {
390           /* If the "Infer file type from extension" option was chosen,
391              search for the respective type in the list.
392              (It's a O(n) search, but fortunately n is small). */
393           gint i;
394           for (i = 1 ; i < N_EXTENSIONS ; ++i)
395             {
396               if ( g_str_has_suffix (filename, ft[i].ext))
397                 {
398                   file_type = i;
399                   break;
400                 }
401             }
402         }
403       else if (! g_str_has_suffix (filename, ft[file_type].ext))
404         {
405           /* If an explicit document format was chosen, and if the chosen
406              filename does not already have that particular "extension",
407              then append it.
408            */
409
410           gchar *of = filename;
411           filename = g_strconcat (filename, ft[file_type].ext, NULL);
412           g_free (of);
413         }
414       
415       string_map_init (&options);
416       string_map_insert (&options, "output-file", filename);
417
418       switch (file_type)
419         {
420         case FT_PDF:
421           export_output (window, &options, "pdf");
422           break;
423         case FT_HTML:
424           export_output (window, &options, "html");
425           break;
426         case FT_ODT:
427           export_output (window, &options, "odt");
428           break;
429         case FT_PS:
430           export_output (window, &options, "ps");
431           break;
432         case FT_CSV:
433           export_output (window, &options, "csv");
434           break;
435
436         case FT_TXT:
437           string_map_insert (&options, "headers", "false");
438           string_map_insert (&options, "paginate", "false");
439           string_map_insert (&options, "squeeze", "true");
440           string_map_insert (&options, "emphasis", "none");
441           string_map_insert (&options, "charts", "none");
442           string_map_insert (&options, "top-margin", "0");
443           string_map_insert (&options, "bottom-margin", "0");
444           export_output (window, &options, "txt");
445           break;
446         default:
447           g_assert_not_reached ();
448         }
449
450       string_map_destroy (&options);
451
452       free (filename);
453     }
454
455   gtk_widget_destroy (dialog);
456 }
457
458 static void
459 psppire_output_window_init (PsppireOutputWindow *window)
460 {
461   GtkBuilder *xml;
462
463   xml = builder_new ("output-window.ui");
464
465   gtk_widget_reparent (get_widget_assert (xml, "vbox1"), GTK_WIDGET (window));
466
467   window->dispose_has_run = FALSE;
468
469   window->view = psppire_output_view_new (
470     GTK_LAYOUT (get_widget_assert (xml, "output")),
471     GTK_TREE_VIEW (get_widget_assert (xml, "overview")),
472     get_action_assert (xml, "edit_copy"),
473     get_action_assert (xml, "edit_select-all"));
474
475
476   connect_help (xml);
477
478   g_signal_connect (window,
479                     "focus-in-event",
480                     G_CALLBACK (cancel_urgency),
481                     NULL);
482
483   g_signal_connect (get_action_assert (xml,"windows_minimise-all"),
484                     "activate",
485                     G_CALLBACK (psppire_window_minimise_all),
486                     NULL);
487
488   {
489     GtkUIManager *uim = GTK_UI_MANAGER (get_object_assert (xml, "uimanager1", GTK_TYPE_UI_MANAGER));
490     merge_help_menu (uim);
491
492     PSPPIRE_WINDOW (window)->menu =
493       GTK_MENU_SHELL (gtk_ui_manager_get_widget (uim,"/ui/menubar/windows_menuitem/windows_minimise-all")->parent);
494   }
495
496   g_signal_connect_swapped (get_action_assert (xml, "file_export"), "activate",
497                             G_CALLBACK (psppire_output_window_export), window);
498
499
500   g_signal_connect_swapped (get_action_assert (xml, "file_print"), "activate",
501                             G_CALLBACK (psppire_output_window_print), window);
502
503   g_object_unref (xml);
504
505   g_signal_connect (window, "delete-event",
506                     G_CALLBACK (on_delete), window);
507 }
508
509
510 GtkWidget*
511 psppire_output_window_new (void)
512 {
513   return GTK_WIDGET (g_object_new (psppire_output_window_get_type (),
514                                    /* TRANSLATORS: This will form a filename.  Please avoid whitespace. */
515                                    "filename", _("Output"),
516                                    "description", _("Output Viewer"),
517                                    NULL));
518 }
519
520 static void
521 psppire_output_window_print (PsppireOutputWindow *window)
522 {
523   psppire_output_view_print (window->view, GTK_WINDOW (window));
524 }