gui: make import assistant window a modal window
[pspp] / src / ui / gui / psppire-import-assistant.c
1 /* PSPPIRE - a graphical user interface for PSPP.
2    Copyright (C) 2015, 2016, 2017, 2018, 2020  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 #include "psppire-import-assistant.h"
19
20 #include <gtk/gtk.h>
21
22 #include "data/casereader.h"
23 #include "data/data-in.h"
24 #include "data/format-guesser.h"
25 #include "data/gnumeric-reader.h"
26 #include "data/ods-reader.h"
27 #include "data/value-labels.h"
28 #include "data/casereader-provider.h"
29
30 #include "libpspp/i18n.h"
31
32 #include "builder-wrapper.h"
33
34 #include "psppire-data-sheet.h"
35 #include "psppire-data-store.h"
36 #include "psppire-dialog.h"
37 #include "psppire-encoding-selector.h"
38 #include "psppire-variable-sheet.h"
39
40 #include "psppire-import-spreadsheet.h"
41 #include "psppire-import-textfile.h"
42
43 #include "ui/syntax-gen.h"
44
45 #include <gettext.h>
46 #define _(msgid) gettext (msgid)
47 #define N_(msgid) msgid
48
49 typedef void page_func (PsppireImportAssistant *, GtkWidget *page, enum IMPORT_ASSISTANT_DIRECTION dir);
50
51
52 static void formats_page_create (PsppireImportAssistant *ia);
53
54 static void psppire_import_assistant_init            (PsppireImportAssistant      *act);
55 static void psppire_import_assistant_class_init      (PsppireImportAssistantClass *class);
56
57 G_DEFINE_TYPE (PsppireImportAssistant, psppire_import_assistant, GTK_TYPE_ASSISTANT);
58
59
60 /* Properties */
61 enum
62   {
63     PROP_0,
64   };
65
66 static void
67 psppire_import_assistant_set_property (GObject         *object,
68                                        guint            prop_id,
69                                        const GValue    *value,
70                                        GParamSpec      *pspec)
71 {
72   //   PsppireImportAssistant *act = PSPPIRE_IMPORT_ASSISTANT (object);
73
74   switch (prop_id)
75     {
76     default:
77       G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
78       break;
79     };
80 }
81
82
83 static void
84 psppire_import_assistant_get_property (GObject    *object,
85                                        guint            prop_id,
86                                        GValue          *value,
87                                        GParamSpec      *pspec)
88 {
89   //  PsppireImportAssistant *assistant = PSPPIRE_IMPORT_ASSISTANT (object);
90
91   switch (prop_id)
92     {
93     default:
94       G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
95       break;
96     };
97 }
98
99 static GObjectClass * parent_class = NULL;
100
101
102 static void
103 psppire_import_assistant_finalize (GObject *object)
104 {
105   PsppireImportAssistant *ia = PSPPIRE_IMPORT_ASSISTANT (object);
106
107   if (ia->spreadsheet)
108     spreadsheet_unref (ia->spreadsheet);
109
110   ds_destroy (&ia->quotes);
111
112   dict_unref (ia->dict);
113   dict_unref (ia->casereader_dict);
114
115   g_object_unref (ia->text_builder);
116   g_object_unref (ia->spread_builder);
117
118   ia->response = -1;
119   g_main_loop_unref (ia->main_loop);
120
121   if (G_OBJECT_CLASS (parent_class)->finalize)
122     G_OBJECT_CLASS (parent_class)->finalize (object);
123 }
124
125
126 static void
127 psppire_import_assistant_class_init (PsppireImportAssistantClass *class)
128 {
129   GObjectClass *object_class = G_OBJECT_CLASS (class);
130
131   parent_class = g_type_class_peek_parent (class);
132
133   object_class->set_property = psppire_import_assistant_set_property;
134   object_class->get_property = psppire_import_assistant_get_property;
135
136   object_class->finalize = psppire_import_assistant_finalize;
137 }
138
139
140 /* Causes the assistant to close, returning RESPONSE for
141    interpretation by text_data_import_assistant. */
142 static void
143 close_assistant (PsppireImportAssistant *ia, int response)
144 {
145   ia->response = response;
146   g_main_loop_quit (ia->main_loop);
147   gtk_widget_hide (GTK_WIDGET (ia));
148 }
149
150
151 /* Called when the Paste button on the last page of the assistant
152    is clicked. */
153 static void
154 on_paste (GtkButton *button, PsppireImportAssistant *ia)
155 {
156   close_assistant (ia, PSPPIRE_RESPONSE_PASTE);
157 }
158
159
160 /* /\* Clears the set of user-modified variables from IA's formats */
161 /*    substructure.  This discards user modifications to variable */
162 /*    formats, thereby causing formats to revert to their */
163 /*    defaults.  *\/ */
164 /* static void */
165 /* reset_formats_page (PsppireImportAssistant *ia, GtkWidget *page) */
166 /* { */
167 /* } */
168
169 static void prepare_formats_page (PsppireImportAssistant *ia);
170
171 /* Called when the Reset button is clicked.
172    This function marshalls the callback to the relevant page.  */
173 static void
174 on_reset (GtkButton *button, PsppireImportAssistant *ia)
175 {
176   gint pn = gtk_assistant_get_current_page (GTK_ASSISTANT (ia));
177   {
178     GtkWidget *page =  gtk_assistant_get_nth_page (GTK_ASSISTANT (ia), pn);
179
180     page_func *xon_reset = g_object_get_data (G_OBJECT (page), "on-reset");
181
182     if (xon_reset)
183       xon_reset (ia, page, 0);
184   }
185 }
186
187
188 static gint
189 next_page_func (gint old_page, gpointer data)
190 {
191   return old_page + 1;
192 }
193
194
195 /* Called just before PAGE is displayed as the current page of
196    IMPORT_ASSISTANT, this updates IA content according to the new
197    page. */
198 static void
199 on_prepare (GtkAssistant *assistant, GtkWidget *page, PsppireImportAssistant *ia)
200 {
201   gtk_widget_show (ia->reset_button);
202   gtk_widget_hide (ia->paste_button);
203
204   gint pn = gtk_assistant_get_current_page (assistant);
205   gint previous_page_index = ia->previous_page;
206   g_assert (pn != previous_page_index);
207
208   if (previous_page_index >= 0)
209     {
210       GtkWidget *closing_page = gtk_assistant_get_nth_page (GTK_ASSISTANT (ia), previous_page_index);
211
212         page_func *on_leaving = g_object_get_data (G_OBJECT (closing_page), "on-leaving");
213         if (on_leaving)
214           on_leaving (ia, closing_page, (pn > previous_page_index) ? IMPORT_ASSISTANT_FORWARDS : IMPORT_ASSISTANT_BACKWARDS);
215     }
216
217     GtkWidget *new_page = gtk_assistant_get_nth_page (GTK_ASSISTANT (ia), pn);
218
219     page_func *on_entering = g_object_get_data (G_OBJECT (new_page), "on-entering");
220     if (on_entering)
221       on_entering (ia, new_page, (pn > previous_page_index) ? IMPORT_ASSISTANT_FORWARDS : IMPORT_ASSISTANT_BACKWARDS);
222
223   ia->previous_page = pn;
224 }
225
226 /* Called when the Cancel button in the assistant is clicked. */
227 static void
228 on_cancel (GtkAssistant *assistant, PsppireImportAssistant *ia)
229 {
230   close_assistant (ia, GTK_RESPONSE_CANCEL);
231 }
232
233 /* Called when the Apply button on the last page of the assistant
234    is clicked. */
235 static void
236 on_close (GtkAssistant *assistant, PsppireImportAssistant *ia)
237 {
238   close_assistant (ia, GTK_RESPONSE_APPLY);
239 }
240
241
242 static void
243 on_chosen (PsppireImportAssistant *ia, GtkWidget *page)
244 {
245   GtkFileChooser *fc = GTK_FILE_CHOOSER (page);
246   gchar *f = gtk_file_chooser_get_filename (fc);
247   int i;
248
249   for(i = gtk_assistant_get_n_pages (GTK_ASSISTANT (ia)); i > 0; --i)
250     gtk_assistant_remove_page (GTK_ASSISTANT (ia), i);
251
252   gtk_assistant_set_page_complete (GTK_ASSISTANT(ia), GTK_WIDGET (fc), FALSE);
253
254   if (f && g_file_test (f, G_FILE_TEST_IS_REGULAR))
255     {
256       gtk_assistant_set_page_complete (GTK_ASSISTANT(ia), GTK_WIDGET (fc), TRUE);
257
258       if (ia->spreadsheet)
259         spreadsheet_unref (ia->spreadsheet);
260
261       ia->spreadsheet = gnumeric_probe (f, FALSE);
262
263       if (!ia->spreadsheet)
264         ia->spreadsheet = ods_probe (f, FALSE);
265
266       if (ia->spreadsheet)
267         {
268           sheet_spec_page_create (ia);
269         }
270       else
271         {
272           intro_page_create (ia);
273           first_line_page_create (ia);
274           separators_page_create (ia);
275         }
276
277       formats_page_create (ia);
278     }
279
280   g_free (f);
281 }
282
283 /* This has to be done on a map signal callback,
284    because GtkFileChooserWidget resets everything when it is mapped. */
285 static void
286 on_map (PsppireImportAssistant *ia, GtkWidget *page)
287 {
288 #if TEXT_FILE
289   GtkFileChooser *fc = GTK_FILE_CHOOSER (page);
290
291   if (ia->file_name)
292     gtk_file_chooser_set_filename (fc, ia->file_name);
293 #endif
294
295   on_chosen (ia, page);
296 }
297
298
299
300 static void
301 chooser_page_enter (PsppireImportAssistant *ia, GtkWidget *page, enum IMPORT_ASSISTANT_DIRECTION dir)
302 {
303 }
304
305 static void
306 chooser_page_leave (PsppireImportAssistant *ia, GtkWidget *page, enum IMPORT_ASSISTANT_DIRECTION dir)
307 {
308   if (dir != IMPORT_ASSISTANT_FORWARDS)
309     return;
310
311   GtkFileChooser *fc = GTK_FILE_CHOOSER (page);
312
313   g_free (ia->file_name);
314   ia->file_name = gtk_file_chooser_get_filename (fc);
315
316   /* Add the chosen file to the recent manager.  */
317   {
318     gchar *uri = gtk_file_chooser_get_uri (fc);
319     GtkRecentManager * manager = gtk_recent_manager_get_default ();
320     gtk_recent_manager_add_item (manager, uri);
321     g_free (uri);
322   }
323
324   if (!ia->spreadsheet)
325     {
326       gchar *encoding = psppire_encoding_selector_get_encoding (ia->encoding_selector);
327       ia->text_file = psppire_text_file_new (ia->file_name, encoding);
328       gtk_tree_view_set_model (GTK_TREE_VIEW (ia->first_line_tree_view),
329                                GTK_TREE_MODEL (ia->text_file));
330
331       g_free (encoding);
332     }
333 }
334
335 static void
336 chooser_page_reset (PsppireImportAssistant *ia, GtkWidget *page)
337 {
338   GtkFileChooser *fc = GTK_FILE_CHOOSER (page);
339
340   gtk_file_chooser_set_filter (fc, ia->default_filter);
341   gtk_file_chooser_unselect_all (fc);
342
343   on_chosen (ia, page);
344 }
345
346
347 static void
348 on_file_activated (GtkFileChooser *chooser, PsppireImportAssistant *ia)
349 {
350   gtk_assistant_next_page (GTK_ASSISTANT (ia));
351 }
352
353 static void
354 chooser_page_create (PsppireImportAssistant *ia)
355 {
356   GtkFileFilter *filter = NULL;
357
358   GtkWidget *chooser = gtk_file_chooser_widget_new (GTK_FILE_CHOOSER_ACTION_OPEN);
359
360   g_signal_connect (chooser, "file-activated", G_CALLBACK (on_file_activated), ia);
361
362   g_object_set_data (G_OBJECT (chooser), "on-leaving", chooser_page_leave);
363   g_object_set_data (G_OBJECT (chooser), "on-reset",   chooser_page_reset);
364   g_object_set_data (G_OBJECT (chooser), "on-entering",chooser_page_enter);
365
366   g_object_set (chooser, "local-only", FALSE, NULL);
367
368
369   ia->default_filter = gtk_file_filter_new ();
370   gtk_file_filter_set_name (ia->default_filter, _("All Files"));
371   gtk_file_filter_add_pattern (ia->default_filter, "*");
372   gtk_file_chooser_add_filter (GTK_FILE_CHOOSER (chooser), ia->default_filter);
373
374   filter = gtk_file_filter_new ();
375   gtk_file_filter_set_name (filter, _("Text Files"));
376   gtk_file_filter_add_mime_type (filter, "text/*");
377   gtk_file_chooser_add_filter (GTK_FILE_CHOOSER (chooser), filter);
378
379   filter = gtk_file_filter_new ();
380   gtk_file_filter_set_name (filter, _("Text (*.txt) Files"));
381   gtk_file_filter_add_pattern (filter, "*.txt");
382   gtk_file_filter_add_pattern (filter, "*.TXT");
383   gtk_file_chooser_add_filter (GTK_FILE_CHOOSER (chooser), filter);
384
385   filter = gtk_file_filter_new ();
386   gtk_file_filter_set_name (filter, _("Plain Text (ASCII) Files"));
387   gtk_file_filter_add_mime_type (filter, "text/plain");
388   gtk_file_chooser_add_filter (GTK_FILE_CHOOSER (chooser), filter);
389
390   filter = gtk_file_filter_new ();
391   gtk_file_filter_set_name (filter, _("Comma Separated Value Files"));
392   gtk_file_filter_add_mime_type (filter, "text/csv");
393   gtk_file_chooser_add_filter (GTK_FILE_CHOOSER (chooser), filter);
394
395   /* I've never encountered one of these, but it's listed here:
396      http://www.iana.org/assignments/media-types/text/tab-separated-values  */
397   filter = gtk_file_filter_new ();
398   gtk_file_filter_set_name (filter, _("Tab Separated Value Files"));
399   gtk_file_filter_add_mime_type (filter, "text/tab-separated-values");
400   gtk_file_chooser_add_filter (GTK_FILE_CHOOSER (chooser), filter);
401
402   filter = gtk_file_filter_new ();
403   gtk_file_filter_set_name (filter, _("Gnumeric Spreadsheet Files"));
404   gtk_file_filter_add_mime_type (filter, "application/x-gnumeric");
405   gtk_file_chooser_add_filter (GTK_FILE_CHOOSER (chooser), filter);
406
407   filter = gtk_file_filter_new ();
408   gtk_file_filter_set_name (filter, _("OpenDocument Spreadsheet Files"));
409   gtk_file_filter_add_mime_type (filter, "application/vnd.oasis.opendocument.spreadsheet");
410   gtk_file_chooser_add_filter (GTK_FILE_CHOOSER (chooser), filter);
411
412   filter = gtk_file_filter_new ();
413   gtk_file_filter_set_name (filter, _("All Spreadsheet Files"));
414   gtk_file_filter_add_mime_type (filter, "application/x-gnumeric");
415   gtk_file_filter_add_mime_type (filter, "application/vnd.oasis.opendocument.spreadsheet");
416   gtk_file_chooser_add_filter (GTK_FILE_CHOOSER (chooser), filter);
417
418   ia->encoding_selector = psppire_encoding_selector_new ("Auto", TRUE);
419   gtk_file_chooser_set_extra_widget (GTK_FILE_CHOOSER (chooser), ia->encoding_selector);
420
421   add_page_to_assistant (ia, chooser,
422                          GTK_ASSISTANT_PAGE_INTRO, _("Select File to Import"));
423
424   g_signal_connect_swapped (chooser, "selection-changed", G_CALLBACK (on_chosen), ia);
425   g_signal_connect_swapped (chooser, "map", G_CALLBACK (on_map), ia);
426 }
427
428
429
430 static void
431 psppire_import_assistant_init (PsppireImportAssistant *ia)
432 {
433   ia->text_builder = builder_new ("text-data-import.ui");
434   ia->spread_builder = builder_new ("spreadsheet-import.ui");
435
436   ia->previous_page = -1 ;
437   ia->file_name = NULL;
438
439   ia->spreadsheet = NULL;
440   ia->updating_selection = FALSE;
441   ia->dict = NULL;
442   ia->casereader_dict = NULL;
443
444   ia->main_loop = g_main_loop_new (NULL, TRUE);
445
446   g_signal_connect (ia, "prepare", G_CALLBACK (on_prepare), ia);
447   g_signal_connect (ia, "cancel", G_CALLBACK (on_cancel), ia);
448   g_signal_connect (ia, "close", G_CALLBACK (on_close), ia);
449
450   ia->paste_button = gtk_button_new_with_label (_("Paste"));
451   ia->reset_button = gtk_button_new_with_label (_("Reset"));
452
453   gtk_assistant_add_action_widget (GTK_ASSISTANT(ia), ia->paste_button);
454
455   g_signal_connect (ia->paste_button, "clicked", G_CALLBACK (on_paste), ia);
456   g_signal_connect (ia->reset_button, "clicked", G_CALLBACK (on_reset), ia);
457
458   gtk_assistant_add_action_widget (GTK_ASSISTANT(ia), ia->reset_button);
459
460   gtk_window_set_title (GTK_WINDOW (ia),
461                         _("Importing Delimited Text Data"));
462
463   gtk_window_set_icon_name (GTK_WINDOW (ia), "pspp");
464   gtk_window_set_modal (GTK_WINDOW(ia), TRUE);
465
466   chooser_page_create (ia);
467
468   gtk_assistant_set_forward_page_func (GTK_ASSISTANT (ia), next_page_func, NULL, NULL);
469
470   gtk_window_maximize (GTK_WINDOW (ia));
471 }
472
473
474 /* Appends a page of the given TYPE, with PAGE as its content, to
475    the GtkAssistant encapsulated by IA.  Returns the GtkWidget
476    that represents the page. */
477 GtkWidget *
478 add_page_to_assistant (PsppireImportAssistant *ia,
479                        GtkWidget *page, GtkAssistantPageType type, const gchar *title)
480 {
481   GtkWidget *content = page;
482
483   gtk_assistant_append_page (GTK_ASSISTANT (ia), content);
484   gtk_assistant_set_page_type (GTK_ASSISTANT(ia), content, type);
485   gtk_assistant_set_page_title (GTK_ASSISTANT(ia), content, title);
486   gtk_assistant_set_page_complete (GTK_ASSISTANT(ia), content, TRUE);
487
488   return content;
489 }
490
491
492 GtkWidget *
493 psppire_import_assistant_new (GtkWindow *toplevel)
494 {
495   return GTK_WIDGET (g_object_new (PSPPIRE_TYPE_IMPORT_ASSISTANT,
496                                    /* Some window managers (notably ratpoison)
497                                       ignore the maximise command when a window is
498                                       transient.  This causes problems for this
499                                       window. */
500                                    /* "transient-for", toplevel, */
501                                    NULL));
502 }
503
504
505
506 \f
507
508
509 /* Called just before the formats page of the assistant is
510    displayed. */
511 static void
512 prepare_formats_page (PsppireImportAssistant *ia)
513 {
514 /* Set the data model for both the data sheet and the variable sheet.  */
515   if (ia->spreadsheet)
516     spreadsheet_set_data_models (ia);
517   else
518     textfile_set_data_models (ia);
519
520   /* Show half-half the data sheet and the variable sheet.  */
521   gint pmax;
522   g_object_get (get_widget_assert (ia->text_builder, "vpaned1"),
523                 "max-position", &pmax, NULL);
524
525   g_object_set (get_widget_assert (ia->text_builder, "vpaned1"),
526                 "position", pmax / 2, NULL);
527
528   gtk_widget_show (ia->paste_button);
529 }
530
531 static void
532 formats_page_create (PsppireImportAssistant *ia)
533 {
534   GtkBuilder *builder = ia->text_builder;
535
536   GtkWidget *w = get_widget_assert (builder, "Formats");
537   g_object_set_data (G_OBJECT (w), "on-entering", prepare_formats_page);
538   //  g_object_set_data (G_OBJECT (w), "on-reset", reset_formats_page);
539
540   ia->data_sheet = get_widget_assert (builder, "data-sheet");
541   ia->var_sheet = get_widget_assert (builder, "variable-sheet");
542
543   add_page_to_assistant (ia, w,
544                          GTK_ASSISTANT_PAGE_CONFIRM, _("Adjust Variable Formats"));
545 }
546
547
548 \f
549
550 static void
551 sheet_spec_gen_syntax (PsppireImportAssistant *ia, struct string *s)
552 {
553   GtkBuilder *builder = ia->spread_builder;
554   GtkWidget *range_entry = get_widget_assert (builder, "cell-range-entry");
555   GtkWidget *sheet_entry = get_widget_assert (builder, "sheet-entry");
556   GtkWidget *rnc = get_widget_assert (builder, "readnames-checkbox");
557   const gchar *range = gtk_entry_get_text (GTK_ENTRY (range_entry));
558   int sheet_index = 1 + gtk_combo_box_get_active (GTK_COMBO_BOX (sheet_entry));
559   gboolean read_names = gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (rnc));
560
561
562   char *filename;
563   if (ia->spreadsheet)
564     filename = ia->spreadsheet->file_name;
565   else
566     g_object_get (ia->text_file, "file-name", &filename, NULL);
567   syntax_gen_pspp (s,
568                    "GET DATA"
569                    "\n  /TYPE=%ss"
570                    "\n  /FILE=%sq"
571                    "\n  /SHEET=index %d"
572                    "\n  /READNAMES=%ss",
573                    ia->spreadsheet->type,
574                    filename,
575                    sheet_index,
576                    read_names ? "ON" : "OFF");
577
578   if (range && 0 != strcmp ("", range))
579     {
580       syntax_gen_pspp (s,
581                        "\n  /CELLRANGE=RANGE %sq", range);
582     }
583   else
584     {
585       syntax_gen_pspp (s,
586                        "\n  /CELLRANGE=FULL");
587     }
588
589
590   syntax_gen_pspp (s, ".\n");
591 }
592
593
594 gchar *
595 psppire_import_assistant_generate_syntax (PsppireImportAssistant *ia)
596 {
597   struct string s = DS_EMPTY_INITIALIZER;
598
599   if (!ia->spreadsheet)
600     {
601       text_spec_gen_syntax (ia, &s);
602     }
603   else
604     {
605       sheet_spec_gen_syntax (ia, &s);
606     }
607
608   return ds_cstr (&s);
609 }
610
611
612 int
613 psppire_import_assistant_run (PsppireImportAssistant *asst)
614 {
615   g_main_loop_run (asst->main_loop);
616   return asst->response;
617 }