gui: Add .zsav support in File|Open and File|Save As dialogs.
[pspp] / src / ui / gui / psppire-data-window.c
1 /* PSPPIRE - a graphical user interface for PSPP.
2    Copyright (C) 2008, 2009, 2010, 2011, 2012, 2013  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 <gtk/gtk.h>
20 #include <stdlib.h>
21
22 #include "data/dataset.h"
23 #include "data/session.h"
24 #include "language/lexer/lexer.h"
25 #include "libpspp/message.h"
26 #include "libpspp/str.h"
27 #include "ui/gui/aggregate-dialog.h"
28 #include "ui/gui/autorecode-dialog.h"
29 #include "ui/gui/builder-wrapper.h"
30 #include "ui/gui/comments-dialog.h"
31 #include "ui/gui/entry-dialog.h"
32 #include "ui/gui/executor.h"
33 #include "ui/gui/help-menu.h"
34 #include "ui/gui/helper.h"
35 #include "ui/gui/helper.h"
36 #include "ui/gui/psppire-data-window.h"
37 #include "ui/gui/psppire-dialog-action.h"
38 #include "ui/gui/psppire-syntax-window.h"
39 #include "ui/gui/psppire-window.h"
40 #include "ui/gui/psppire.h"
41 #include "ui/gui/recode-dialog.h"
42 #include "ui/gui/select-cases-dialog.h"
43 #include "ui/gui/split-file-dialog.h"
44 #include "ui/gui/text-data-import-dialog.h"
45 #include "ui/gui/weight-cases-dialog.h"
46 #include "ui/syntax-gen.h"
47
48 #include "gl/c-strcase.h"
49 #include "gl/c-strcasestr.h"
50 #include "gl/xvasprintf.h"
51
52 #include <gettext.h>
53 #define _(msgid) gettext (msgid)
54 #define N_(msgid) msgid
55
56 struct session *the_session;
57 struct ll_list all_data_windows = LL_INITIALIZER (all_data_windows);
58
59 static void psppire_data_window_class_init    (PsppireDataWindowClass *class);
60 static void psppire_data_window_init          (PsppireDataWindow      *data_editor);
61
62
63 static void psppire_data_window_iface_init (PsppireWindowIface *iface);
64
65 static void psppire_data_window_dispose (GObject *object);
66 static void psppire_data_window_finalize (GObject *object);
67 static void psppire_data_window_set_property (GObject         *object,
68                                               guint            prop_id,
69                                               const GValue    *value,
70                                               GParamSpec      *pspec);
71 static void psppire_data_window_get_property (GObject         *object,
72                                               guint            prop_id,
73                                               GValue          *value,
74                                               GParamSpec      *pspec);
75
76 static guint psppire_data_window_add_ui (PsppireDataWindow *, GtkUIManager *);
77 static void psppire_data_window_remove_ui (PsppireDataWindow *,
78                                            GtkUIManager *, guint);
79
80 GType
81 psppire_data_window_get_type (void)
82 {
83   static GType psppire_data_window_type = 0;
84
85   if (!psppire_data_window_type)
86     {
87       static const GTypeInfo psppire_data_window_info =
88         {
89           sizeof (PsppireDataWindowClass),
90           NULL,
91           NULL,
92           (GClassInitFunc)psppire_data_window_class_init,
93           (GClassFinalizeFunc) NULL,
94           NULL,
95           sizeof (PsppireDataWindow),
96           0,
97           (GInstanceInitFunc) psppire_data_window_init,
98         };
99
100       static const GInterfaceInfo window_interface_info =
101         {
102           (GInterfaceInitFunc) psppire_data_window_iface_init,
103           NULL,
104           NULL
105         };
106
107       psppire_data_window_type =
108         g_type_register_static (PSPPIRE_TYPE_WINDOW, "PsppireDataWindow",
109                                 &psppire_data_window_info, 0);
110
111
112       g_type_add_interface_static (psppire_data_window_type,
113                                    PSPPIRE_TYPE_WINDOW_MODEL,
114                                    &window_interface_info);
115     }
116
117   return psppire_data_window_type;
118 }
119
120 static GObjectClass *parent_class ;
121
122 enum {
123     PROP_DATASET = 1
124 };
125
126 static void
127 psppire_data_window_class_init (PsppireDataWindowClass *class)
128 {
129   GObjectClass *object_class = G_OBJECT_CLASS (class);
130
131   parent_class = g_type_class_peek_parent (class);
132
133   object_class->dispose = psppire_data_window_dispose;
134   object_class->finalize = psppire_data_window_finalize;
135   object_class->set_property = psppire_data_window_set_property;
136   object_class->get_property = psppire_data_window_get_property;
137
138   g_object_class_install_property (
139     object_class, PROP_DATASET,
140     g_param_spec_pointer ("dataset", "Dataset",
141                           "'struct datset *' represented by the window",
142                           G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE));
143 }
144 \f
145 /* Run the EXECUTE command. */
146 static void
147 execute (PsppireDataWindow *dw)
148 {
149   execute_const_syntax_string (dw, "EXECUTE.");
150 }
151
152 static void
153 transformation_change_callback (bool transformations_pending,
154                                 gpointer data)
155 {
156   PsppireDataWindow  *de = PSPPIRE_DATA_WINDOW (data);
157
158   GtkUIManager *uim = GTK_UI_MANAGER (get_object_assert (de->builder, "uimanager1", GTK_TYPE_UI_MANAGER));
159
160   GtkWidget *menuitem =
161     gtk_ui_manager_get_widget (uim,"/ui/menubar/transform/transform_run-pending");
162
163   GtkWidget *status_label  =
164     get_widget_assert (de->builder, "case-counter-area");
165
166   gtk_widget_set_sensitive (menuitem, transformations_pending);
167
168
169   if ( transformations_pending)
170     gtk_label_set_text (GTK_LABEL (status_label),
171                         _("Transformations Pending"));
172   else
173     gtk_label_set_text (GTK_LABEL (status_label), "");
174 }
175
176 /* Callback for when the dictionary changes its filter variable */
177 static void
178 on_filter_change (GObject *o, gint filter_index, gpointer data)
179 {
180   PsppireDataWindow  *de = PSPPIRE_DATA_WINDOW (data);
181
182   GtkWidget *filter_status_area =
183     get_widget_assert (de->builder, "filter-use-status-area");
184
185   if ( filter_index == -1 )
186     {
187       gtk_label_set_text (GTK_LABEL (filter_status_area), _("Filter off"));
188     }
189   else
190     {
191       PsppireDict *dict = NULL;
192       struct variable *var ;
193       gchar *text ;
194
195       g_object_get (de->data_editor, "dictionary", &dict, NULL);
196
197       var = psppire_dict_get_variable (dict, filter_index);
198
199       text = g_strdup_printf (_("Filter by %s"), var_get_name (var));
200
201       gtk_label_set_text (GTK_LABEL (filter_status_area), text);
202
203       g_free (text);
204     }
205 }
206
207 /* Callback for when the dictionary changes its split variables */
208 static void
209 on_split_change (PsppireDict *dict, gpointer data)
210 {
211   PsppireDataWindow  *de = PSPPIRE_DATA_WINDOW (data);
212
213   size_t n_split_vars = dict_get_split_cnt (dict->dict);
214
215   GtkWidget *split_status_area =
216     get_widget_assert (de->builder, "split-file-status-area");
217
218   if ( n_split_vars == 0 )
219     {
220       gtk_label_set_text (GTK_LABEL (split_status_area), _("No Split"));
221     }
222   else
223     {
224       gint i;
225       GString *text;
226       const struct variable *const * split_vars =
227         dict_get_split_vars (dict->dict);
228
229       text = g_string_new (_("Split by "));
230
231       for (i = 0 ; i < n_split_vars - 1; ++i )
232         {
233           g_string_append_printf (text, "%s, ", var_get_name (split_vars[i]));
234         }
235       g_string_append (text, var_get_name (split_vars[i]));
236
237       gtk_label_set_text (GTK_LABEL (split_status_area), text->str);
238
239       g_string_free (text, TRUE);
240     }
241 }
242
243
244
245
246 /* Callback for when the dictionary changes its weights */
247 static void
248 on_weight_change (GObject *o, gint weight_index, gpointer data)
249 {
250   PsppireDataWindow  *de = PSPPIRE_DATA_WINDOW (data);
251
252   GtkWidget *weight_status_area =
253     get_widget_assert (de->builder, "weight-status-area");
254
255   if ( weight_index == -1 )
256     {
257       gtk_label_set_text (GTK_LABEL (weight_status_area), _("Weights off"));
258     }
259   else
260     {
261       struct variable *var ;
262       PsppireDict *dict = NULL;
263       gchar *text;
264
265       g_object_get (de->data_editor, "dictionary", &dict, NULL);
266
267       var = psppire_dict_get_variable (dict, weight_index);
268
269       text = g_strdup_printf (_("Weight by %s"), var_get_name (var));
270
271       gtk_label_set_text (GTK_LABEL (weight_status_area), text);
272
273       g_free (text);
274     }
275 }
276
277 #if 0
278 static void
279 dump_rm (GtkRecentManager *rm)
280 {
281   GList *items = gtk_recent_manager_get_items (rm);
282
283   GList *i;
284
285   g_print ("Recent Items:\n");
286   for (i = items; i; i = i->next)
287     {
288       GtkRecentInfo *ri = i->data;
289
290       g_print ("Item: %s (Mime: %s) (Desc: %s) (URI: %s)\n",
291                gtk_recent_info_get_short_name (ri),
292                gtk_recent_info_get_mime_type (ri),
293                gtk_recent_info_get_description (ri),
294                gtk_recent_info_get_uri (ri)
295                );
296
297
298       gtk_recent_info_unref (ri);
299     }
300
301   g_list_free (items);
302 }
303 #endif
304
305 static gboolean
306 has_suffix (const gchar *name, const gchar *suffix)
307 {
308   size_t name_len = strlen (name);
309   size_t suffix_len = strlen (suffix);
310   return (name_len > suffix_len
311           && !c_strcasecmp (&name[name_len - suffix_len], suffix));
312 }
313
314 static gboolean
315 name_has_por_suffix (const gchar *name)
316 {
317   return has_suffix (name, ".por");
318 }
319
320 static gboolean
321 name_has_sav_suffix (const gchar *name)
322 {
323   return has_suffix (name, ".sav") || has_suffix (name, ".zsav");
324 }
325
326 /* Returns true if NAME has a suffix which might denote a PSPP file */
327 static gboolean
328 name_has_suffix (const gchar *name)
329 {
330   return name_has_por_suffix (name) || name_has_sav_suffix (name);
331 }
332
333 static gboolean
334 load_file (PsppireWindow *de, const gchar *file_name, gpointer syn)
335 {
336   const char *mime_type = NULL;
337   gchar *syntax = NULL;
338   bool ok;
339
340   if (syn == NULL)
341     {
342       gchar *utf8_file_name;
343       struct string filename;
344       ds_init_empty (&filename);
345       
346       utf8_file_name = g_filename_to_utf8 (file_name, -1, NULL, NULL, NULL);
347     
348       syntax_gen_string (&filename, ss_cstr (utf8_file_name));
349       
350       g_free (utf8_file_name);
351       
352       syntax = g_strdup_printf ("GET FILE=%s.", ds_cstr (&filename));
353       ds_destroy (&filename);
354
355     }
356   else
357     {
358       syntax = syn;
359     }
360
361   ok = execute_syntax (PSPPIRE_DATA_WINDOW (de),
362                        lex_reader_for_string (syntax));
363   g_free (syntax);
364
365   if (ok && syn == NULL)
366     {
367       if (name_has_por_suffix (file_name))
368         mime_type = "application/x-spss-por";
369       else if (name_has_sav_suffix (file_name))
370         mime_type = "application/x-spss-sav";
371       
372       add_most_recent (file_name, mime_type);
373     }
374
375   return ok;
376 }
377
378 static const char *
379 psppire_data_window_format_to_string (enum PsppireDataWindowFormat format)
380 {
381   if (format == PSPPIRE_DATA_WINDOW_SAV)
382     return ".sav";
383   else if (format == PSPPIRE_DATA_WINDOW_ZSAV)
384     return ".zsav";
385   else
386     return ".por";
387 }
388
389 /* Save DE to file */
390 static void
391 save_file (PsppireWindow *w)
392 {
393   const gchar *file_name = NULL;
394   gchar *utf8_file_name = NULL;
395   GString *fnx;
396   struct string filename ;
397   PsppireDataWindow *de = PSPPIRE_DATA_WINDOW (w);
398   gchar *syntax;
399
400   file_name = psppire_window_get_filename (w);
401
402   fnx = g_string_new (file_name);
403
404   if ( ! name_has_suffix (fnx->str))
405     g_string_append (fnx, psppire_data_window_format_to_string (de->format));
406
407   ds_init_empty (&filename);
408
409   utf8_file_name = g_filename_to_utf8 (fnx->str, -1, NULL, NULL, NULL);
410
411   g_string_free (fnx, TRUE);
412
413   syntax_gen_string (&filename, ss_cstr (utf8_file_name));
414   g_free (utf8_file_name);
415
416   if (de->format == PSPPIRE_DATA_WINDOW_SAV)
417     syntax = g_strdup_printf ("SAVE OUTFILE=%s.", ds_cstr (&filename));
418   else if (de->format == PSPPIRE_DATA_WINDOW_ZSAV)
419     syntax = g_strdup_printf ("SAVE /ZCOMPRESSED /OUTFILE=%s.",
420                               ds_cstr (&filename));
421   else
422     syntax = g_strdup_printf ("EXPORT OUTFILE=%s.", ds_cstr (&filename));
423
424   ds_destroy (&filename);
425
426   g_free (execute_syntax_string (de, syntax));
427 }
428
429
430 static void
431 display_dict (PsppireDataWindow *de)
432 {
433   execute_const_syntax_string (de, "DISPLAY DICTIONARY.");
434 }
435
436 static void
437 sysfile_info (PsppireDataWindow *de)
438 {
439   GtkWidget *dialog = psppire_window_file_chooser_dialog (PSPPIRE_WINDOW (de));
440
441   if  ( GTK_RESPONSE_ACCEPT == gtk_dialog_run (GTK_DIALOG (dialog)))
442     {
443       struct string filename;
444       gchar *file_name =
445         gtk_file_chooser_get_filename (GTK_FILE_CHOOSER (dialog));
446
447       gchar *utf8_file_name = g_filename_to_utf8 (file_name, -1, NULL, NULL,
448                                                   NULL);
449
450       gchar *syntax;
451
452       ds_init_empty (&filename);
453
454       syntax_gen_string (&filename, ss_cstr (utf8_file_name));
455
456       g_free (utf8_file_name);
457
458       syntax = g_strdup_printf ("SYSFILE INFO %s.", ds_cstr (&filename));
459       g_free (execute_syntax_string (de, syntax));
460     }
461
462   gtk_widget_destroy (dialog);
463 }
464
465
466 /* PsppireWindow 'pick_filename' callback: prompt for a filename to save as. */
467 static void
468 data_pick_filename (PsppireWindow *window)
469 {
470   GtkListStore *list_store;
471   GtkWidget *combo_box;
472
473   PsppireDataWindow *de = PSPPIRE_DATA_WINDOW (window);
474   GtkFileFilter *filter;
475   GtkWidget *dialog =
476     gtk_file_chooser_dialog_new (_("Save"),
477                                  GTK_WINDOW (de),
478                                  GTK_FILE_CHOOSER_ACTION_SAVE,
479                                  GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL,
480                                  GTK_STOCK_SAVE, GTK_RESPONSE_ACCEPT,
481                                  NULL);
482
483   g_object_set (dialog, "local-only", FALSE, NULL);
484
485   filter = gtk_file_filter_new ();
486   gtk_file_filter_set_name (filter, _("System Files (*.sav)"));
487   gtk_file_filter_add_mime_type (filter, "application/x-spss-sav");
488   gtk_file_chooser_add_filter (GTK_FILE_CHOOSER (dialog), filter);
489
490   filter = gtk_file_filter_new ();
491   gtk_file_filter_set_name (filter, _("Compressed System Files (*.zsav)"));
492   gtk_file_filter_add_pattern (filter, "*.zsav");
493   gtk_file_chooser_add_filter (GTK_FILE_CHOOSER (dialog), filter);
494
495   filter = gtk_file_filter_new ();
496   gtk_file_filter_set_name (filter, _("Portable Files (*.por) "));
497   gtk_file_filter_add_mime_type (filter, "application/x-spss-por");
498   gtk_file_chooser_add_filter (GTK_FILE_CHOOSER (dialog), filter);
499
500   filter = gtk_file_filter_new ();
501   gtk_file_filter_set_name (filter, _("All Files"));
502   gtk_file_filter_add_pattern (filter, "*");
503   gtk_file_chooser_add_filter (GTK_FILE_CHOOSER (dialog), filter);
504
505   {
506     GtkCellRenderer *cell;
507     GtkWidget *label;
508     GtkTreeIter iter;
509     GtkWidget *hbox;
510
511     list_store = gtk_list_store_new (2, G_TYPE_INT, G_TYPE_STRING);
512     combo_box = gtk_combo_box_new_with_model (GTK_TREE_MODEL (list_store));
513
514     gtk_list_store_append (list_store, &iter);
515     gtk_list_store_set (list_store, &iter,
516                         0, PSPPIRE_DATA_WINDOW_SAV,
517                         1, _("System File"),
518                         -1);
519     gtk_combo_box_set_active_iter (GTK_COMBO_BOX (combo_box), &iter);
520
521     gtk_list_store_append (list_store, &iter);
522     gtk_list_store_set (list_store, &iter,
523                         0, PSPPIRE_DATA_WINDOW_ZSAV,
524                         1, _("Compressed System File"),
525                         -1);
526
527     gtk_list_store_append (list_store, &iter);
528     gtk_list_store_set (list_store, &iter,
529                         0, PSPPIRE_DATA_WINDOW_POR,
530                         1, _("Portable File"),
531                         -1);
532
533     label = gtk_label_new (_("Format:"));
534
535     cell = gtk_cell_renderer_text_new ();
536     gtk_cell_layout_pack_start (GTK_CELL_LAYOUT (combo_box), cell, FALSE);
537     gtk_cell_layout_add_attribute (GTK_CELL_LAYOUT (combo_box), cell,
538                                    "text", 1);
539
540     hbox = gtk_hbox_new (FALSE, 0);
541     gtk_box_pack_start (GTK_BOX (hbox), label, FALSE, FALSE, 0);
542     gtk_box_pack_start (GTK_BOX (hbox), combo_box, FALSE, FALSE, 0);
543     gtk_widget_show_all (hbox);
544
545     gtk_file_chooser_set_extra_widget (GTK_FILE_CHOOSER (dialog), hbox);
546   }
547
548   gtk_file_chooser_set_do_overwrite_confirmation (GTK_FILE_CHOOSER (dialog),
549                                                   TRUE);
550
551   switch (gtk_dialog_run (GTK_DIALOG (dialog)))
552     {
553     case GTK_RESPONSE_ACCEPT:
554       {
555         GString *filename =
556           g_string_new
557           (
558            gtk_file_chooser_get_filename (GTK_FILE_CHOOSER (dialog))
559            );
560
561         GtkTreeIter iter;
562         int format;
563
564         gtk_combo_box_get_active_iter (GTK_COMBO_BOX (combo_box), &iter);
565         gtk_tree_model_get (GTK_TREE_MODEL (list_store), &iter,
566                             0, &format,
567                             -1);
568         de->format = format;
569
570         if ( ! name_has_suffix (filename->str))
571           g_string_append (filename,
572                            psppire_data_window_format_to_string (format));
573
574         psppire_window_set_filename (PSPPIRE_WINDOW (de), filename->str);
575
576         g_string_free (filename, TRUE);
577       }
578       break;
579     default:
580       break;
581     }
582
583   gtk_widget_destroy (dialog);
584 }
585
586 static bool
587 confirm_delete_dataset (PsppireDataWindow *de,
588                         const char *old_dataset,
589                         const char *new_dataset,
590                         const char *existing_dataset)
591 {
592   GtkWidget *dialog;
593   int result;
594
595   dialog = gtk_message_dialog_new (
596     GTK_WINDOW (de), 0, GTK_MESSAGE_QUESTION, GTK_BUTTONS_NONE, "%s",
597     _("Delete Existing Dataset?"));
598
599   gtk_message_dialog_format_secondary_text (
600     GTK_MESSAGE_DIALOG (dialog),
601     _("Renaming \"%s\" to \"%s\" will destroy the existing "
602       "dataset named \"%s\".  Are you sure that you want to do this?"),
603     old_dataset, new_dataset, existing_dataset);
604
605   gtk_dialog_add_buttons (GTK_DIALOG (dialog),
606                           GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL,
607                           GTK_STOCK_DELETE, GTK_RESPONSE_OK,
608                           NULL);
609
610   g_object_set (dialog, "icon-name", "pspp", NULL);
611
612   result = gtk_dialog_run (GTK_DIALOG (dialog));
613
614   gtk_widget_destroy (dialog);
615
616   return result == GTK_RESPONSE_OK;
617 }
618
619 static void
620 on_rename_dataset (PsppireDataWindow *de)
621 {
622   struct dataset *ds = de->dataset;
623   struct session *session = dataset_session (ds);
624   const char *old_name = dataset_name (ds);
625   struct dataset *existing_dataset;
626   char *new_name;
627   char *prompt;
628
629   prompt = xasprintf (_("Please enter a new name for dataset \"%s\":"),
630                       old_name);
631   new_name = entry_dialog_run (GTK_WINDOW (de), _("Rename Dataset"), prompt,
632                                old_name);
633   free (prompt);
634
635   if (new_name == NULL)
636     return;
637
638   existing_dataset = session_lookup_dataset (session, new_name);
639   if (existing_dataset == NULL || existing_dataset == ds
640       || confirm_delete_dataset (de, old_name, new_name,
641                                  dataset_name (existing_dataset)))
642     g_free (execute_syntax_string (de, g_strdup_printf ("DATASET NAME %s.",
643                                                         new_name)));
644
645   free (new_name);
646 }
647
648 static void
649 status_bar_activate (PsppireDataWindow  *de, GtkToggleAction *action)
650 {
651   GtkWidget *statusbar = get_widget_assert (de->builder, "status-bar");
652
653   if ( gtk_toggle_action_get_active (action))
654     gtk_widget_show (statusbar);
655   else
656     gtk_widget_hide (statusbar);
657 }
658
659
660 static void
661 grid_lines_activate (PsppireDataWindow  *de, GtkToggleAction *action)
662 {
663   const gboolean grid_visible = gtk_toggle_action_get_active (action);
664
665   psppire_data_editor_show_grid (de->data_editor, grid_visible);
666 }
667
668 static void
669 data_view_activate (PsppireDataWindow  *de)
670 {
671   gtk_notebook_set_current_page (GTK_NOTEBOOK (de->data_editor), PSPPIRE_DATA_EDITOR_DATA_VIEW);
672 }
673
674
675 static void
676 variable_view_activate (PsppireDataWindow  *de)
677 {
678   gtk_notebook_set_current_page (GTK_NOTEBOOK (de->data_editor), PSPPIRE_DATA_EDITOR_VARIABLE_VIEW);
679 }
680
681
682 static void
683 fonts_activate (PsppireDataWindow  *de)
684 {
685   GtkWidget *toplevel = gtk_widget_get_toplevel (GTK_WIDGET (de));
686   PangoFontDescription *current_font;
687   gchar *font_name;
688   GtkWidget *dialog =
689     gtk_font_selection_dialog_new (_("Font Selection"));
690
691
692   current_font = GTK_WIDGET(de->data_editor)->style->font_desc;
693   font_name = pango_font_description_to_string (current_font);
694
695   gtk_font_selection_dialog_set_font_name (GTK_FONT_SELECTION_DIALOG (dialog), font_name);
696
697   g_free (font_name);
698
699   gtk_window_set_transient_for (GTK_WINDOW (dialog),
700                                 GTK_WINDOW (toplevel));
701
702   if ( GTK_RESPONSE_OK == gtk_dialog_run (GTK_DIALOG (dialog)) )
703     {
704       const gchar *font = gtk_font_selection_dialog_get_font_name
705         (GTK_FONT_SELECTION_DIALOG (dialog));
706
707       PangoFontDescription* font_desc =
708         pango_font_description_from_string (font);
709
710       psppire_data_editor_set_font (de->data_editor, font_desc);
711     }
712
713   gtk_widget_hide (dialog);
714 }
715
716
717
718 /* Callback for the value labels action */
719 static void
720 toggle_value_labels (PsppireDataWindow  *de, GtkToggleAction *ta)
721 {
722   g_object_set (de->data_editor, "value-labels", gtk_toggle_action_get_active (ta), NULL);
723 }
724
725 static void
726 toggle_split_window (PsppireDataWindow  *de, GtkToggleAction *ta)
727 {
728   psppire_data_editor_split_window (de->data_editor,
729                                     gtk_toggle_action_get_active (ta));
730 }
731
732
733 static void
734 file_quit (PsppireDataWindow *de)
735 {
736   /* FIXME: Need to be more intelligent here.
737      Give the user the opportunity to save any unsaved data.
738   */
739   psppire_quit ();
740 }
741
742 static void
743 on_recent_data_select (GtkMenuShell *menushell,
744                        PsppireWindow *window)
745 {
746   gchar *file;
747
748   gchar *uri =
749     gtk_recent_chooser_get_current_uri (GTK_RECENT_CHOOSER (menushell));
750
751   file = g_filename_from_uri (uri, NULL, NULL);
752
753   g_free (uri);
754
755   open_data_window (window, file, NULL);
756
757   g_free (file);
758 }
759
760 static char *
761 charset_from_mime_type (const char *mime_type)
762 {
763   const char *charset;
764   struct string s;
765   const char *p;
766
767   if (mime_type == NULL)
768     return NULL;
769
770   charset = c_strcasestr (mime_type, "charset=");
771   if (charset == NULL)
772     return NULL;
773
774   ds_init_empty (&s);
775   p = charset + 8;
776   if (*p == '"')
777     {
778       /* Parse a "quoted-string" as defined by RFC 822. */
779       for (p++; *p != '\0' && *p != '"'; p++)
780         {
781           if (*p != '\\')
782             ds_put_byte (&s, *p);
783           else if (*++p != '\0')
784             ds_put_byte (&s, *p);
785         }
786     }
787   else
788     {
789       /* Parse a "token" as defined by RFC 2045. */
790       while (*p > 32 && *p < 127 && strchr ("()<>@,;:\\\"/[]?=", *p) == NULL)
791         ds_put_byte (&s, *p++);
792     }
793   if (!ds_is_empty (&s))
794     return ds_steal_cstr (&s);
795
796   ds_destroy (&s);
797   return NULL;
798 }
799
800 static void
801 on_recent_files_select (GtkMenuShell *menushell,   gpointer user_data)
802 {
803   GtkRecentInfo *item;
804   char *encoding;
805   GtkWidget *se;
806   gchar *file;
807
808   /* Get the file name and its encoding. */
809   item = gtk_recent_chooser_get_current_item (GTK_RECENT_CHOOSER (menushell));
810   file = g_filename_from_uri (gtk_recent_info_get_uri (item), NULL, NULL);
811   encoding = charset_from_mime_type (gtk_recent_info_get_mime_type (item));
812   gtk_recent_info_unref (item);
813
814   se = psppire_syntax_window_new (encoding);
815
816   free (encoding);
817
818   if ( psppire_window_load (PSPPIRE_WINDOW (se), file, NULL) ) 
819     gtk_widget_show (se);
820   else
821     gtk_widget_destroy (se);
822
823   g_free (file);
824 }
825
826 static void
827 set_unsaved (gpointer w)
828 {
829   psppire_window_set_unsaved (PSPPIRE_WINDOW (w));
830 }
831
832 static void
833 on_switch_page (PsppireDataEditor *de, gpointer p,
834                 gint pagenum, PsppireDataWindow *dw)
835 {
836   GtkWidget *page_menu_item;
837   gboolean is_ds;
838   const char *path;
839
840   is_ds = pagenum == PSPPIRE_DATA_EDITOR_DATA_VIEW;
841   path = (is_ds
842           ? "/ui/menubar/view/view_data"
843           : "/ui/menubar/view/view_variables");
844   page_menu_item = gtk_ui_manager_get_widget (dw->ui_manager, path);
845   gtk_check_menu_item_set_active (GTK_CHECK_MENU_ITEM (page_menu_item), TRUE);
846 }
847
848 static void
849 on_ui_manager_changed (PsppireDataEditor *de,
850                        GParamSpec *pspec UNUSED,
851                        PsppireDataWindow *dw)
852 {
853   GtkUIManager *uim = psppire_data_editor_get_ui_manager (de);
854   if (uim == dw->uim)
855     return;
856
857   if (dw->uim)
858     {
859       psppire_data_window_remove_ui (dw, dw->uim, dw->merge_id);
860       g_object_unref (dw->uim);
861       dw->uim = NULL;
862     }
863
864   dw->uim = uim;
865   if (dw->uim)
866     {
867       g_object_ref (dw->uim);
868       dw->merge_id = psppire_data_window_add_ui (dw, dw->uim);
869     }
870 }
871
872 /* Connects the action called ACTION_NAME to HANDLER passing DW as the auxilliary data.
873    Returns a pointer to the action
874 */
875 static GtkAction *
876 connect_action (PsppireDataWindow *dw, const char *action_name, 
877                                     GCallback handler)
878 {
879   GtkAction *action = get_action_assert (dw->builder, action_name);
880
881   g_signal_connect_swapped (action, "activate", handler, dw);
882
883   return action;
884 }
885
886 /* Only a data file with at least one variable can be saved. */
887 static void
888 enable_save (PsppireDataWindow *dw)
889 {
890   gboolean enable = psppire_dict_get_var_cnt (dw->dict) > 0;
891
892   gtk_action_set_sensitive (get_action_assert (dw->builder, "file_save"),
893                             enable);
894   gtk_action_set_sensitive (get_action_assert (dw->builder, "file_save_as"),
895                             enable);
896 }
897
898 /* Initializes as much of a PsppireDataWindow as we can and must before the
899    dataset has been set.
900
901    In particular, the 'menu' member is required in case the "filename" property
902    is set before the "dataset" property: otherwise PsppireWindow will try to
903    modify the menu as part of the "filename" property_set() function and end up
904    with a Gtk-CRITICAL since 'menu' is NULL.  */
905 static void
906 psppire_data_window_init (PsppireDataWindow *de)
907 {
908   de->builder = builder_new ("data-editor.ui");
909
910   de->ui_manager = GTK_UI_MANAGER (get_object_assert (de->builder, "uimanager1", GTK_TYPE_UI_MANAGER));
911
912   PSPPIRE_WINDOW (de)->menu =
913     GTK_MENU_SHELL (gtk_ui_manager_get_widget (de->ui_manager, "/ui/menubar/windows/windows_minimise_all")->parent);
914
915   de->uim = NULL;
916   de->merge_id = 0;
917 }
918
919 static void
920 psppire_data_window_finish_init (PsppireDataWindow *de,
921                                  struct dataset *ds)
922 {
923   static const struct dataset_callbacks cbs =
924     {
925       set_unsaved,                    /* changed */
926       transformation_change_callback, /* transformations_changed */
927     };
928
929   GtkWidget *menubar;
930   GtkWidget *hb ;
931   GtkWidget *sb ;
932
933   GtkWidget *box = gtk_vbox_new (FALSE, 0);
934
935   de->dataset = ds;
936   de->dict = psppire_dict_new_from_dict (dataset_dict (ds));
937   de->data_store = psppire_data_store_new (de->dict);
938   psppire_data_store_set_reader (de->data_store, NULL);
939
940   menubar = get_widget_assert (de->builder, "menubar");
941   hb = get_widget_assert (de->builder, "handlebox1");
942   sb = get_widget_assert (de->builder, "status-bar");
943
944   de->uim = NULL;
945   de->merge_id = 0;
946
947   de->data_editor =
948     PSPPIRE_DATA_EDITOR (psppire_data_editor_new (de->dict, de->data_store));
949   g_signal_connect (de->data_editor, "switch-page",
950                     G_CALLBACK (on_switch_page), de);
951
952   g_signal_connect_swapped (de->data_store, "case-changed",
953                             G_CALLBACK (set_unsaved), de);
954
955   g_signal_connect_swapped (de->data_store, "case-inserted",
956                             G_CALLBACK (set_unsaved), de);
957
958   g_signal_connect_swapped (de->data_store, "cases-deleted",
959                             G_CALLBACK (set_unsaved), de);
960
961   dataset_set_callbacks (de->dataset, &cbs, de);
962
963   connect_help (de->builder);
964
965   gtk_box_pack_start (GTK_BOX (box), menubar, FALSE, TRUE, 0);
966   gtk_box_pack_start (GTK_BOX (box), hb, FALSE, TRUE, 0);
967   gtk_box_pack_start (GTK_BOX (box), GTK_WIDGET (de->data_editor), TRUE, TRUE, 0);
968   gtk_box_pack_start (GTK_BOX (box), sb, FALSE, TRUE, 0);
969
970   gtk_container_add (GTK_CONTAINER (de), box);
971
972   g_signal_connect (de->dict, "weight-changed",
973                     G_CALLBACK (on_weight_change),
974                     de);
975
976   g_signal_connect (de->dict, "filter-changed",
977                     G_CALLBACK (on_filter_change),
978                     de);
979
980   g_signal_connect (de->dict, "split-changed",
981                     G_CALLBACK (on_split_change),
982                     de);
983
984   g_signal_connect_swapped (de->dict, "backend-changed",
985                             G_CALLBACK (enable_save), de);
986   g_signal_connect_swapped (de->dict, "variable-inserted",
987                             G_CALLBACK (enable_save), de);
988   g_signal_connect_swapped (de->dict, "variable-deleted",
989                             G_CALLBACK (enable_save), de);
990   enable_save (de);
991
992   connect_action (de, "file_new_data", G_CALLBACK (create_data_window));
993   connect_action (de, "file_import", G_CALLBACK (text_data_import_assistant));
994   connect_action (de, "file_save", G_CALLBACK (psppire_window_save));
995   connect_action (de, "file_open", G_CALLBACK (psppire_window_open));
996   connect_action (de, "file_save_as", G_CALLBACK (psppire_window_save_as));
997   connect_action (de, "rename_dataset", G_CALLBACK (on_rename_dataset));
998   connect_action (de, "file_information_working-file", G_CALLBACK (display_dict));
999   connect_action (de, "file_information_external-file", G_CALLBACK (sysfile_info));
1000
1001   g_signal_connect_swapped (get_action_assert (de->builder, "view_value-labels"), "toggled", G_CALLBACK (toggle_value_labels), de);
1002
1003   connect_action (de, "data_select-cases", G_CALLBACK (select_cases_dialog));
1004   connect_action (de, "data_aggregate", G_CALLBACK (aggregate_dialog));
1005   connect_action (de, "transform_autorecode", G_CALLBACK (autorecode_dialog));
1006   connect_action (de, "data_split-file", G_CALLBACK (split_file_dialog));
1007   connect_action (de, "data_weight-cases", G_CALLBACK (weight_cases_dialog));
1008   connect_action (de, "utilities_comments", G_CALLBACK (comments_dialog));
1009   connect_action (de, "transform_recode-same", G_CALLBACK (recode_same_dialog));
1010   connect_action (de, "transform_recode-different", G_CALLBACK (recode_different_dialog));
1011
1012   {
1013     GtkWidget *recent_data =
1014       gtk_ui_manager_get_widget (de->ui_manager, "/ui/menubar/file/file_recent-data");
1015
1016     GtkWidget *recent_files =
1017       gtk_ui_manager_get_widget (de->ui_manager, "/ui/menubar/file/file_recent-files");
1018
1019
1020     GtkWidget *menu_data = gtk_recent_chooser_menu_new_for_manager (
1021       gtk_recent_manager_get_default ());
1022
1023     GtkWidget *menu_files = gtk_recent_chooser_menu_new_for_manager (
1024       gtk_recent_manager_get_default ());
1025
1026     g_object_set (menu_data, "show-tips",  TRUE, NULL);
1027     g_object_set (menu_files, "show-tips",  TRUE, NULL);
1028
1029     {
1030       GtkRecentFilter *filter = gtk_recent_filter_new ();
1031
1032       gtk_recent_filter_add_mime_type (filter, "application/x-spss-sav");
1033       gtk_recent_filter_add_mime_type (filter, "application/x-spss-por");
1034
1035       gtk_recent_chooser_set_sort_type (GTK_RECENT_CHOOSER (menu_data), GTK_RECENT_SORT_MRU);
1036
1037       gtk_recent_chooser_add_filter (GTK_RECENT_CHOOSER (menu_data), filter);
1038     }
1039
1040     gtk_menu_item_set_submenu (GTK_MENU_ITEM (recent_data), menu_data);
1041
1042
1043     g_signal_connect (menu_data, "selection-done", G_CALLBACK (on_recent_data_select), de);
1044
1045     {
1046       GtkRecentFilter *filter = gtk_recent_filter_new ();
1047
1048       gtk_recent_filter_add_pattern (filter, "*.sps");
1049       gtk_recent_filter_add_pattern (filter, "*.SPS");
1050
1051       gtk_recent_chooser_set_sort_type (GTK_RECENT_CHOOSER (menu_files), GTK_RECENT_SORT_MRU);
1052
1053       gtk_recent_chooser_add_filter (GTK_RECENT_CHOOSER (menu_files), filter);
1054     }
1055
1056     gtk_menu_item_set_submenu (GTK_MENU_ITEM (recent_files), menu_files);
1057
1058     g_signal_connect (menu_files, "selection-done", G_CALLBACK (on_recent_files_select), de);
1059
1060   }
1061
1062   connect_action (de, "file_new_syntax", G_CALLBACK (create_syntax_window));
1063
1064
1065   gtk_notebook_set_current_page (GTK_NOTEBOOK (de->data_editor), PSPPIRE_DATA_EDITOR_VARIABLE_VIEW);
1066   gtk_notebook_set_current_page (GTK_NOTEBOOK (de->data_editor), PSPPIRE_DATA_EDITOR_DATA_VIEW);
1067
1068   connect_action (de, "view_statusbar", G_CALLBACK (status_bar_activate));
1069
1070   connect_action (de, "view_gridlines", G_CALLBACK (grid_lines_activate));
1071
1072   connect_action (de, "view_data", G_CALLBACK (data_view_activate));
1073
1074   connect_action (de, "view_variables", G_CALLBACK (variable_view_activate));
1075
1076   connect_action (de, "view_fonts", G_CALLBACK (fonts_activate));
1077
1078   connect_action (de, "file_quit", G_CALLBACK (file_quit));
1079
1080   connect_action (de, "transform_run-pending", G_CALLBACK (execute));
1081
1082   connect_action (de, "windows_minimise_all", G_CALLBACK (psppire_window_minimise_all));
1083
1084   g_signal_connect_swapped (get_action_assert (de->builder, "windows_split"), "toggled", G_CALLBACK (toggle_split_window), de);
1085
1086   merge_help_menu (de->ui_manager);
1087
1088   g_signal_connect (de->data_editor, "notify::ui-manager",
1089                     G_CALLBACK (on_ui_manager_changed), de);
1090   on_ui_manager_changed (de->data_editor, NULL, de);
1091
1092   gtk_widget_show (GTK_WIDGET (de->data_editor));
1093   gtk_widget_show (box);
1094
1095   ll_push_head (&all_data_windows, &de->ll);
1096 }
1097
1098 static void
1099 psppire_data_window_dispose (GObject *object)
1100 {
1101   PsppireDataWindow *dw = PSPPIRE_DATA_WINDOW (object);
1102
1103   if (dw->uim)
1104     {
1105       psppire_data_window_remove_ui (dw, dw->uim, dw->merge_id);
1106       g_object_unref (dw->uim);
1107       dw->uim = NULL;
1108     }
1109
1110   if (dw->builder != NULL)
1111     {
1112       g_object_unref (dw->builder);
1113       dw->builder = NULL;
1114     }
1115
1116   if (dw->dict)
1117     {
1118       g_signal_handlers_disconnect_by_func (dw->dict,
1119                                             G_CALLBACK (enable_save), dw);
1120       g_signal_handlers_disconnect_by_func (dw->dict,
1121                                             G_CALLBACK (on_weight_change), dw);
1122       g_signal_handlers_disconnect_by_func (dw->dict,
1123                                             G_CALLBACK (on_filter_change), dw);
1124       g_signal_handlers_disconnect_by_func (dw->dict,
1125                                             G_CALLBACK (on_split_change), dw);
1126
1127       g_object_unref (dw->dict);
1128       dw->dict = NULL;
1129     }
1130
1131   if (dw->data_store)
1132     {
1133       g_object_unref (dw->data_store);
1134       dw->data_store = NULL;
1135     }
1136
1137   if (dw->ll.next != NULL)
1138     {
1139       ll_remove (&dw->ll);
1140       dw->ll.next = NULL;
1141     }
1142
1143   if (G_OBJECT_CLASS (parent_class)->dispose)
1144     G_OBJECT_CLASS (parent_class)->dispose (object);
1145 }
1146
1147 static void
1148 psppire_data_window_finalize (GObject *object)
1149 {
1150   PsppireDataWindow *dw = PSPPIRE_DATA_WINDOW (object);
1151
1152   if (dw->dataset)
1153     {
1154       struct dataset *dataset = dw->dataset;
1155       struct session *session = dataset_session (dataset);
1156
1157       dw->dataset = NULL;
1158
1159       dataset_set_callbacks (dataset, NULL, NULL);
1160       session_set_active_dataset (session, NULL);
1161       dataset_destroy (dataset);
1162     }
1163
1164   if (G_OBJECT_CLASS (parent_class)->finalize)
1165     G_OBJECT_CLASS (parent_class)->finalize (object);
1166 }
1167
1168 static void
1169 psppire_data_window_set_property (GObject         *object,
1170                                   guint            prop_id,
1171                                   const GValue    *value,
1172                                   GParamSpec      *pspec)
1173 {
1174   PsppireDataWindow *window = PSPPIRE_DATA_WINDOW (object);
1175
1176   switch (prop_id)
1177     {
1178     case PROP_DATASET:
1179       psppire_data_window_finish_init (window, g_value_get_pointer (value));
1180       break;
1181     default:
1182       G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
1183       break;
1184     };
1185 }
1186
1187 static void
1188 psppire_data_window_get_property (GObject         *object,
1189                                   guint            prop_id,
1190                                   GValue          *value,
1191                                   GParamSpec      *pspec)
1192 {
1193   PsppireDataWindow *window = PSPPIRE_DATA_WINDOW (object);
1194
1195   switch (prop_id)
1196     {
1197     case PROP_DATASET:
1198       g_value_set_pointer (value, window->dataset);
1199       break;
1200     default:
1201       G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
1202       break;
1203     };
1204 }
1205
1206 static guint
1207 psppire_data_window_add_ui (PsppireDataWindow *pdw, GtkUIManager *uim)
1208 {
1209   gchar *ui_string;
1210   guint merge_id;
1211   GList *list;
1212
1213   ui_string = gtk_ui_manager_get_ui (uim);
1214   merge_id = gtk_ui_manager_add_ui_from_string (pdw->ui_manager, ui_string,
1215                                                 -1, NULL);
1216   g_free (ui_string);
1217
1218   g_return_val_if_fail (merge_id != 0, 0);
1219
1220   list = gtk_ui_manager_get_action_groups (uim);
1221   for (; list != NULL; list = list->next)
1222     {
1223       GtkActionGroup *action_group = list->data;
1224       GList *actions = gtk_action_group_list_actions (action_group);
1225       GList *action;
1226
1227       for (action = actions; action != NULL; action = action->next)
1228         {
1229           GtkAction *a = action->data;
1230
1231           if (PSPPIRE_IS_DIALOG_ACTION (a))
1232             g_object_set (a, "manager", pdw->ui_manager, NULL);
1233         }
1234
1235       gtk_ui_manager_insert_action_group (pdw->ui_manager, action_group, 0);
1236     }
1237
1238   gtk_window_add_accel_group (GTK_WINDOW (pdw),
1239                               gtk_ui_manager_get_accel_group (uim));
1240
1241   return merge_id;
1242 }
1243
1244 static void
1245 psppire_data_window_remove_ui (PsppireDataWindow *pdw,
1246                                GtkUIManager *uim, guint merge_id)
1247 {
1248   GList *list;
1249
1250   g_return_if_fail (merge_id != 0);
1251
1252   gtk_ui_manager_remove_ui (pdw->ui_manager, merge_id);
1253
1254   list = gtk_ui_manager_get_action_groups (uim);
1255   for (; list != NULL; list = list->next)
1256     {
1257       GtkActionGroup *action_group = list->data;
1258       gtk_ui_manager_remove_action_group (pdw->ui_manager, action_group);
1259     }
1260
1261   gtk_window_remove_accel_group (GTK_WINDOW (pdw),
1262                                  gtk_ui_manager_get_accel_group (uim));
1263 }
1264
1265 GtkWidget*
1266 psppire_data_window_new (struct dataset *ds)
1267 {
1268   GtkWidget *dw;
1269
1270   if (the_session == NULL)
1271     the_session = session_create (NULL);
1272
1273   if (ds == NULL)
1274     {
1275       char *dataset_name = session_generate_dataset_name (the_session);
1276       ds = dataset_create (the_session, dataset_name);
1277       free (dataset_name);
1278     }
1279   assert (dataset_session (ds) == the_session);
1280
1281   dw = GTK_WIDGET (
1282     g_object_new (
1283       psppire_data_window_get_type (),
1284       "description", _("Data Editor"),
1285       "dataset", ds,
1286       NULL));
1287
1288   if (dataset_name (ds) != NULL)
1289     g_object_set (dw, "id", dataset_name (ds), (void *) NULL);
1290
1291   return dw;
1292 }
1293
1294 bool
1295 psppire_data_window_is_empty (PsppireDataWindow *dw)
1296 {
1297   return psppire_dict_get_var_cnt (dw->dict) == 0;
1298 }
1299
1300 static void
1301 psppire_data_window_iface_init (PsppireWindowIface *iface)
1302 {
1303   iface->save = save_file;
1304   iface->pick_filename = data_pick_filename;
1305   iface->load = load_file;
1306 }
1307 \f
1308 PsppireDataWindow *
1309 psppire_default_data_window (void)
1310 {
1311   if (ll_is_empty (&all_data_windows))
1312     create_data_window ();
1313   return ll_data (ll_head (&all_data_windows), PsppireDataWindow, ll);
1314 }
1315
1316 void
1317 psppire_data_window_set_default (PsppireDataWindow *pdw)
1318 {
1319   ll_remove (&pdw->ll);
1320   ll_push_head (&all_data_windows, &pdw->ll);
1321 }
1322
1323 void
1324 psppire_data_window_undefault (PsppireDataWindow *pdw)
1325 {
1326   ll_remove (&pdw->ll);
1327   ll_push_tail (&all_data_windows, &pdw->ll);
1328 }
1329
1330 PsppireDataWindow *
1331 psppire_data_window_for_dataset (struct dataset *ds)
1332 {
1333   PsppireDataWindow *pdw;
1334
1335   ll_for_each (pdw, PsppireDataWindow, ll, &all_data_windows)
1336     if (pdw->dataset == ds)
1337       return pdw;
1338
1339   return NULL;
1340 }
1341
1342 PsppireDataWindow *
1343 psppire_data_window_for_data_store (PsppireDataStore *data_store)
1344 {
1345   PsppireDataWindow *pdw;
1346
1347   ll_for_each (pdw, PsppireDataWindow, ll, &all_data_windows)
1348     if (pdw->data_store == data_store)
1349       return pdw;
1350
1351   return NULL;
1352 }
1353
1354 void
1355 create_data_window (void)
1356 {
1357   gtk_widget_show (psppire_data_window_new (NULL));
1358 }
1359
1360 void
1361 open_data_window (PsppireWindow *victim, const char *file_name, gpointer hint)
1362 {
1363   GtkWidget *window;
1364
1365   if (PSPPIRE_IS_DATA_WINDOW (victim)
1366       && psppire_data_window_is_empty (PSPPIRE_DATA_WINDOW (victim)))
1367     {
1368       window = GTK_WIDGET (victim);
1369       gtk_widget_hide (GTK_WIDGET (PSPPIRE_DATA_WINDOW (window)->data_editor));
1370     }
1371   else
1372     window = psppire_data_window_new (NULL);
1373
1374   psppire_window_load (PSPPIRE_WINDOW (window), file_name, hint);
1375   gtk_widget_show_all (window);
1376 }
1377