Change application ID from org.fsf.pspp to org.gnu.pspp.
[pspp] / src / ui / gui / main.c
1 /* PSPPIRE - a graphical user interface for PSPP.
2    Copyright (C) 2004, 2005, 2006, 2010, 2011, 2012, 2013, 2014, 2015,
3    2016, 2020, 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 "pre-initialisation.h"
21
22 #include "ui/gui/psppire.h"
23
24 #include <gtk/gtk.h>
25 #include <stdlib.h>
26
27 #include "language/lexer/include-path.h"
28 #include "libpspp/argv-parser.h"
29 #include "libpspp/array.h"
30 #include "libpspp/assertion.h"
31 #include "libpspp/cast.h"
32 #include "libpspp/copyleft.h"
33 #include "libpspp/message.h"
34 #include "libpspp/str.h"
35 #include "libpspp/string-array.h"
36 #include "libpspp/version.h"
37 #include "ui/source-init-opts.h"
38 #include "ui/gui/psppire-syntax-window.h"
39 #include "ui/gui/psppire-data-window.h"
40 #include "ui/gui/psppire-output-window.h"
41 #include "ui/gui/psppire-conf.h"
42 #include "ui/gui/helper.h"
43
44 #include "gl/configmake.h"
45 #include "gl/progname.h"
46 #include "gl/relocatable.h"
47 #include "gl/version-etc.h"
48 #include "gl/xalloc.h"
49
50 #include "gettext.h"
51 #define _(msgid) gettext (msgid)
52 #define N_(msgid) msgid
53
54
55
56 static gboolean
57 show_version_and_exit (void)
58 {
59   version_etc (stdout, "psppire", PACKAGE_NAME, PACKAGE_VERSION,
60                "Ben Pfaff", "John Darrington", "Jason Stover", NULL_SENTINEL);
61
62   exit (0);
63
64   return TRUE;
65 }
66
67 \f
68
69 static gboolean
70 init_prepare (GSource * source, gint * timeout_)
71 {
72   return TRUE;
73 }
74
75 static gboolean
76 init_check (GSource * source)
77 {
78   return TRUE;
79 }
80
81 static gboolean
82 init_dispatch (GSource * ss, GSourceFunc callback, gpointer user_data)
83 {
84   struct init_source *is = (struct init_source *) ss;
85
86   bool finished = initialize (is);
87   is->state++;
88
89   if (finished)
90     {
91       g_main_loop_quit (is->loop);
92       return FALSE;
93     }
94
95   return TRUE;
96 }
97
98 static GSourceFuncs init_funcs =
99   {init_prepare, init_check, init_dispatch, NULL, NULL, NULL };
100
101 static GtkWidget *wsplash = 0;
102 static gint64 start_time = 0;
103
104
105 static GtkWidget *
106 create_splash_window (void)
107 {
108   GtkWidget *sp = gtk_window_new (GTK_WINDOW_TOPLEVEL);
109
110   const gchar *filename = PKGDATADIR "/splash.png";
111   const char *relocated_filename = relocate (filename);
112   GtkWidget *l = gtk_image_new_from_file (relocated_filename);
113   if (filename != relocated_filename)
114     free (CONST_CAST (char *, relocated_filename));
115
116   gtk_container_add (GTK_CONTAINER (sp), l);
117   gtk_window_set_type_hint (GTK_WINDOW (sp),
118                             GDK_WINDOW_TYPE_HINT_SPLASHSCREEN);
119   gtk_window_set_position (GTK_WINDOW (sp), GTK_WIN_POS_CENTER);
120   gtk_window_set_skip_pager_hint (GTK_WINDOW (sp), TRUE);
121   gtk_window_set_skip_taskbar_hint (GTK_WINDOW (sp), TRUE);
122   gtk_window_set_focus_on_map (GTK_WINDOW (sp), FALSE);
123   gtk_window_set_accept_focus (GTK_WINDOW (sp), FALSE);
124
125   GdkGeometry hints;
126   hints.max_height = 100;
127   hints.max_width = 200;
128   gtk_window_set_geometry_hints (GTK_WINDOW (sp),
129                                  NULL, &hints, GDK_HINT_MAX_SIZE);
130
131
132   gtk_window_set_gravity (GTK_WINDOW (sp), GDK_GRAVITY_CENTER);
133
134   gtk_window_set_modal (GTK_WINDOW (sp), TRUE);
135   gtk_window_set_decorated (GTK_WINDOW (sp), FALSE);
136   gtk_window_set_keep_above (GTK_WINDOW (sp), TRUE);
137   gtk_widget_show_all (sp);
138   return sp;
139 }
140
141
142 static gint
143 on_local_options (GApplication * application,
144                   GVariantDict * options, gpointer user_data)
145 {
146   {
147     GVariant *b =
148       g_variant_dict_lookup_value (options, "no-unique",
149                                    G_VARIANT_TYPE_BOOLEAN);
150     if (b)
151       {
152         GApplicationFlags flags =  g_application_get_flags (application);
153         flags |= G_APPLICATION_NON_UNIQUE;
154         g_application_set_flags (application, flags);
155         g_variant_unref (b);
156       }
157   }
158   {
159     GVariant *b =
160       g_variant_dict_lookup_value (options, "no-splash",
161                                    G_VARIANT_TYPE_BOOLEAN);
162     if (b)
163       g_variant_unref (b);
164     else
165       start_time = g_get_monotonic_time ();
166   }
167
168
169   return -1;
170 }
171
172 /* Use the imperitive mood for all entries in this table.
173    Each entry should end with a period.   */
174 static const char *tips[] =
175   {
176 #ifdef _WIN32
177    N_("PSPP runs best on free platforms such as GNU and GNU/Linux.  Windows is a non-free system.  As such, certain features might work sub-optimally.  For best results use a free system instead."),
178 #endif
179    N_("Right click on variable lists to change between viewing the variables' names and their labels."),
180    N_("Click \"Paste\" instead of \"OK\" when running procedures.  This allows you to edit your commands before running them and you have better control over your work."),
181    N_("Directly import your spreadsheets using the \"File | Import Data\" menu."),
182    N_("For an easy way to convert string variables into numerically encoded variables, use \"Automatic Recode\"  which preserves the variable names as labels."),
183    N_("When browsing large data sets, use \"Windows | Split\" to see both ends of the data in the same view."),
184    N_("Export your reports to ODT format for easy editing with the Libreoffice.org suite."),
185    N_("Use \"Edit | Options\" to have your Output window automatically appear when statistics are generated."),
186    N_("To easily reorder your variables, drag and drop them in the Variable View or the Data View.")
187   };
188
189 #define N_TIPS  (sizeof tips / sizeof tips[0])
190
191 static void
192 user_tip (GApplication *app)
193 {
194   PsppireConf *conf = psppire_conf_new ();
195
196   gboolean show_tip = TRUE;
197   psppire_conf_get_boolean (conf, "startup", "show-user-tips", &show_tip);
198
199   if (!show_tip)
200     return;
201
202   GtkWindow *parent = gtk_application_get_active_window (GTK_APPLICATION (app));
203
204   GtkWidget *d =
205     gtk_dialog_new_with_buttons (_("Psppire User Hint"), parent,
206                                  GTK_DIALOG_MODAL,
207                                  GTK_MESSAGE_INFO,
208                                  0, 0,
209                                  NULL);
210
211   GtkWidget *pictogram = gtk_image_new_from_icon_name ("user-info", GTK_ICON_SIZE_DIALOG);
212
213   GtkWidget *next = gtk_button_new_with_mnemonic (_("_Next Tip"));
214   gtk_dialog_add_action_widget (GTK_DIALOG (d), next, 1);
215
216   GtkWidget *close = gtk_button_new_with_mnemonic (_("_Close"));
217   gtk_dialog_add_action_widget (GTK_DIALOG (d), close, GTK_RESPONSE_CLOSE);
218
219   gtk_window_set_transient_for (GTK_WINDOW (d), parent);
220
221   g_object_set (d,
222                 "decorated", FALSE,
223                 "skip-taskbar-hint", TRUE,
224                 "skip-pager-hint", TRUE,
225                 "application", app,
226                 NULL);
227
228   GtkWidget *ca = gtk_dialog_get_content_area (GTK_DIALOG (d));
229
230   g_object_set (ca, "margin", 5, NULL);
231
232   GtkWidget *check = gtk_check_button_new_with_mnemonic ("_Show tips at startup");
233   gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (check), show_tip);
234
235   srand (time(0));
236   gint x = rand () % N_TIPS;
237   GtkWidget *label = gtk_label_new (gettext (tips[x]));
238
239   /* Make the font of the label a little larger than the other widgets.  */
240   {
241     GtkStyleContext *sc = gtk_widget_get_style_context (label);
242     GtkCssProvider *p = gtk_css_provider_new ();
243     const gchar *css = "* {font-size: 130%;}";
244     if (gtk_css_provider_load_from_data (p, css, strlen (css), NULL))
245       {
246         gtk_style_context_add_provider (sc, GTK_STYLE_PROVIDER (p),
247                                         GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
248       }
249     g_object_unref (p);
250   }
251
252   /* It's more readable if the text is not all in one long line.  */
253   g_object_set (label, "wrap", TRUE, NULL);
254   gint width = PANGO_PIXELS (50.0 * width_of_m (label) * PANGO_SCALE);
255   gtk_window_set_default_size (GTK_WINDOW (d), width, -1);
256
257
258   if (pictogram)
259     gtk_box_pack_start (GTK_BOX (ca), pictogram, FALSE, FALSE, 5);
260   gtk_box_pack_start (GTK_BOX (ca), label, FALSE, FALSE, 5);
261   gtk_box_pack_end (GTK_BOX (ca), check, FALSE, FALSE, 5);
262
263   gtk_widget_show_all (d);
264
265   g_object_set (close,
266                 "has-focus", TRUE,
267                 "is-focus", TRUE,
268                 NULL);
269
270   while (1 == gtk_dialog_run (GTK_DIALOG (d)))
271     {
272       if (++x >= N_TIPS) x = 0;
273       g_object_set (label, "label", gettext (tips[x]), NULL);
274     }
275
276   show_tip = gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (check));
277   psppire_conf_set_boolean (conf,
278                             "startup", "show-user-tips",
279                             show_tip);
280
281   g_object_unref (conf);
282
283   gtk_widget_destroy (d);
284 }
285
286
287 static void
288 on_startup (GApplication * app, gpointer ud)
289 {
290   GMainContext *context = g_main_context_new ();
291
292   if (start_time != 0)
293     {
294       wsplash = create_splash_window ();
295       gtk_application_add_window (GTK_APPLICATION (app),
296                                   GTK_WINDOW (wsplash));
297
298       g_signal_connect_swapped (wsplash, "destroy", G_CALLBACK (user_tip), app);
299     }
300   else
301     {
302       g_signal_connect (app, "activate", G_CALLBACK (user_tip), NULL);
303     }
304
305   GMainLoop *loop = g_main_loop_new (context, FALSE);
306
307   GSource *ss = g_source_new (&init_funcs, sizeof (struct init_source));
308
309   ((struct init_source *) ss)->loop = loop;
310   ((struct init_source *) ss)->state = 0;
311
312   g_source_set_priority (ss, G_PRIORITY_DEFAULT);
313
314   g_source_attach (ss, context);
315   g_main_loop_run (loop);
316 }
317
318
319 static void
320 post_initialise (GApplication * app)
321 {
322   register_selection_functions ();
323   psppire_output_window_setup ();
324
325   GSimpleAction *quit = g_simple_action_new ("quit", NULL);
326   g_signal_connect_swapped (quit, "activate", G_CALLBACK (psppire_quit), app);
327   g_action_map_add_action (G_ACTION_MAP (app), G_ACTION (quit));
328 }
329
330
331 #define SPLASH_DURATION 1000
332
333 static gboolean
334 destroy_splash (gpointer ud)
335 {
336   GtkWidget *sp = GTK_WIDGET (ud);
337   gtk_widget_destroy (sp);
338   wsplash = NULL;
339   return G_SOURCE_REMOVE;
340 }
341
342
343 static void
344 wait_for_splash (GApplication *app, GtkWindow *x)
345 {
346   if (wsplash)
347     {
348       gtk_window_set_transient_for (GTK_WINDOW (wsplash), x);
349       gtk_application_add_window (GTK_APPLICATION (app), GTK_WINDOW (wsplash));
350       gtk_window_set_keep_above (GTK_WINDOW (wsplash), TRUE);
351       gtk_window_present (GTK_WINDOW (wsplash));
352
353       /* Remove the splash screen after SPLASH_DURATION milliseconds */
354       gint64 elapsed_time = (g_get_monotonic_time () - start_time) / 1000;
355       if (SPLASH_DURATION - elapsed_time <= 0)
356         destroy_splash (wsplash);
357       else
358         g_timeout_add (SPLASH_DURATION - elapsed_time, destroy_splash, wsplash);
359     }
360 }
361
362 static GtkWidget *fatal_error_dialog = NULL;
363 static GtkWidget *fatal_error_label;
364 static const char *diagnostic_info;
365
366 static void
367 fatal_error_handler (int sig)
368 {
369   /* Reset SIG to its default handling so that if it happens again we won't
370      recurse. */
371   signal (sig, SIG_DFL);
372
373   static char message [1024];
374   strcpy (message, "proximate cause:    ");
375   switch (sig)
376     {
377     case SIGABRT:
378       strcat (message, "Assertion Failure/Abort");
379       break;
380     case SIGFPE:
381       strcat (message, "Floating Point Exception");
382       break;
383     case SIGSEGV:
384       strcat (message, "Segmentation Violation");
385       break;
386     default:
387       strcat (message, "Unknown");
388       break;
389     }
390   strcat (message, "\n");
391   strcat (message, diagnostic_info);
392
393   g_object_set (fatal_error_label,
394                 "label", message,
395                 NULL);
396
397   gtk_dialog_run (GTK_DIALOG (fatal_error_dialog));
398
399   /* Re-raise the signal so that we terminate with the correct status. */
400   raise (sig);
401 }
402
403 static void
404 on_activate (GApplication * app, gpointer ud)
405 {
406   struct sigaction fatal_error_action;
407   sigset_t sigset;
408   g_return_if_fail (0 == sigemptyset (&sigset));
409   fatal_error_dialog =
410     gtk_message_dialog_new (NULL, 0, GTK_MESSAGE_ERROR, GTK_BUTTONS_CLOSE,
411                             _("Psppire: Fatal Error"));
412
413   diagnostic_info = prepare_diagnostic_information ();
414
415   gtk_message_dialog_format_secondary_text (GTK_MESSAGE_DIALOG (fatal_error_dialog),
416                                             _("You have discovered a bug in PSPP.  "
417                                               "Please report this to %s including all of the following information, "
418                                               "and a description of what you were doing when this happened."),
419                                             PACKAGE_BUGREPORT);
420
421   g_return_if_fail (fatal_error_dialog != NULL);
422
423   GtkWidget *content_area = gtk_dialog_get_content_area (GTK_DIALOG (fatal_error_dialog));
424   fatal_error_label = gtk_label_new ("");
425   g_object_set (fatal_error_label,
426                 "selectable", TRUE,
427                 "wrap", TRUE,
428                 NULL);
429   gtk_container_add (GTK_CONTAINER (content_area), fatal_error_label);
430
431   gtk_widget_show_all (content_area);
432
433   fatal_error_action.sa_handler = fatal_error_handler;
434   fatal_error_action.sa_mask = sigset;
435   fatal_error_action.sa_flags = 0;
436
437   post_initialise (app);
438
439   GtkWindow *x = create_data_window ();
440   gtk_application_add_window (GTK_APPLICATION (app), x);
441
442   wait_for_splash (app, x);
443   sigaction (SIGABRT, &fatal_error_action, NULL);
444   sigaction (SIGSEGV, &fatal_error_action, NULL);
445   sigaction (SIGFPE,  &fatal_error_action, NULL);
446 }
447
448 static GtkWindow *
449 find_empty_data_window (GApplication *app)
450 {
451   GList *wl = gtk_application_get_windows (GTK_APPLICATION (app));
452   while (wl)
453     {
454       if (wl->data && PSPPIRE_IS_DATA_WINDOW (GTK_WINDOW (wl->data)) &&
455           psppire_data_window_is_empty (PSPPIRE_DATA_WINDOW (wl->data)))
456         return GTK_WINDOW (wl->data);
457       wl = wl->next;
458     }
459   return NULL;
460 }
461
462 static GtkWindow *
463 find_psppire_window (GApplication *app)
464 {
465   GList *wl = gtk_application_get_windows (GTK_APPLICATION (app));
466   while (wl)
467     {
468       if (wl->data && PSPPIRE_IS_WINDOW (GTK_WINDOW (wl->data)))
469         return GTK_WINDOW (wl->data);
470       wl = wl->next;
471     }
472   return NULL;
473 }
474
475 static void
476 on_open (GApplication *app, GFile **files, gint n_files, gchar * hint,
477          gpointer ud)
478 {
479   /* If the application is already open and we open another file
480      via xdg-open on GNU/Linux or via the file manager, then open is
481      called. Check if we already have a psppire window. */
482   if (find_psppire_window (app) == NULL)
483     post_initialise (app);
484
485   /* When a new data file is opened, then try to find an empty
486      data window which will then be replaced as in the open file
487      dialog */
488   GtkWindow *victim = find_empty_data_window (app);
489
490   gchar *file = g_file_get_parse_name (files[0]);
491   GtkWindow *x = psppire_preload_file (file, victim);
492   g_free (file);
493
494   wait_for_splash (app, x);
495 }
496
497
498 /* These are arguments which must be processed BEFORE the X server has been initialised */
499 static void
500 process_pre_start_arguments (int *argc, char ***argv)
501 {
502   GOptionEntry oe[] = {
503     {"version", 'V', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK,
504      show_version_and_exit, N_("Show version information and exit"), 0},
505     {NULL, 0, 0, 0, NULL, "", 0}
506   };
507
508   GOptionContext *oc = g_option_context_new ("");
509   g_option_context_set_help_enabled (oc, FALSE);
510   g_option_context_set_ignore_unknown_options (oc, FALSE);
511   g_option_context_add_main_entries (oc, oe, NULL);
512   g_option_context_parse (oc, argc, argv, NULL);
513   g_option_context_free (oc);
514 }
515
516 int
517 main (int argc, char *argv[])
518 {
519   /* Some operating systems need to munge the arguments.  */
520   pre_initialisation (&argc, argv);
521
522   set_program_name (argv[0]);
523
524   GtkApplication *app =
525     gtk_application_new ("org.gnu.pspp", G_APPLICATION_HANDLES_OPEN);
526
527   process_pre_start_arguments (&argc, &argv);
528
529   GOptionEntry oe[] = {
530     {"no-splash", 'q', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, NULL,
531       N_("Do not display the splash screen"), 0},
532     {"no-unique", 'n', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, NULL,
533       N_("Do not attempt single instance negotiation"), 0},
534     {NULL}
535   };
536
537   g_application_add_main_option_entries (G_APPLICATION (app), oe);
538
539   g_signal_connect (app, "startup", G_CALLBACK (on_startup), NULL);
540   g_signal_connect (app, "activate", G_CALLBACK (on_activate), NULL);
541   g_signal_connect (app, "handle-local-options",
542                     G_CALLBACK (on_local_options), NULL);
543   g_signal_connect (app, "open", G_CALLBACK (on_open), NULL);
544
545   {
546     GSimpleAction *act_new_syntax = g_simple_action_new ("new-syntax", NULL);
547     g_signal_connect_swapped (act_new_syntax, "activate",
548                               G_CALLBACK (create_syntax_window), NULL);
549     g_action_map_add_action (G_ACTION_MAP (app), G_ACTION (act_new_syntax));
550   }
551
552   {
553     GSimpleAction *act_new_data = g_simple_action_new ("new-data", NULL);
554     g_signal_connect_swapped (act_new_data, "activate",
555                               G_CALLBACK (create_data_window), NULL);
556     g_action_map_add_action (G_ACTION_MAP (app), G_ACTION (act_new_data));
557   }
558
559   g_object_set (G_OBJECT (app), "register-session", TRUE, NULL);
560   return g_application_run (G_APPLICATION (app), argc, argv);
561 }