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