Rework the spreadsheet import feature of the grapic user interface
authorJohn Darrington <john@darrington.wattle.id.au>
Sat, 10 Oct 2020 06:21:56 +0000 (08:21 +0200)
committerJohn Darrington <john@darrington.wattle.id.au>
Sat, 10 Oct 2020 06:39:40 +0000 (08:39 +0200)
This change adds a feature where the user can preview the data from
a spreadsheet when preparing to import it.    This should make importing
spreadsheets somewhat easier.  Some rework of the other import code
was also necessary as part of this change.

38 files changed:
INSTALL
NEWS
configure.ac
src/data/gnumeric-reader.c
src/data/gnumeric-reader.h
src/data/ods-reader.c
src/data/ods-reader.h
src/data/spreadsheet-reader.c
src/data/spreadsheet-reader.h
src/ui/gui/automake.mk
src/ui/gui/psppire-import-assistant.c
src/ui/gui/psppire-import-assistant.h
src/ui/gui/psppire-import-spreadsheet.c [new file with mode: 0644]
src/ui/gui/psppire-import-spreadsheet.h [new file with mode: 0644]
src/ui/gui/psppire-import-textfile.c [new file with mode: 0644]
src/ui/gui/psppire-import-textfile.h [new file with mode: 0644]
src/ui/gui/psppire-spreadsheet-data-model.c [new file with mode: 0644]
src/ui/gui/psppire-spreadsheet-data-model.h [new file with mode: 0644]
src/ui/gui/psppire-spreadsheet-model.c
src/ui/gui/psppire-spreadsheet-model.h
src/ui/gui/spreadsheet-import.ui [new file with mode: 0644]
src/ui/gui/text-data-import.ui
tests/automake.mk
tests/data/holey.gnumeric [new file with mode: 0644]
tests/data/holey.ods [new file with mode: 0644]
tests/data/multisheet.gnumeric [new file with mode: 0644]
tests/data/multisheet.ods [new file with mode: 0644]
tests/data/one-thousand-by-fifty-three.gnumeric [new file with mode: 0644]
tests/data/one-thousand-by-fifty-three.ods [new file with mode: 0644]
tests/data/repeating.gnumeric [new file with mode: 0644]
tests/data/repeating.ods [new file with mode: 0644]
tests/data/simple.gnumeric [new file with mode: 0644]
tests/data/simple.ods [new file with mode: 0644]
tests/data/sparse.gnumeric [new file with mode: 0644]
tests/data/sparse.ods [new file with mode: 0644]
tests/data/spreadsheet-test.at [new file with mode: 0644]
tests/data/spreadsheet-test.c [new file with mode: 0644]
tests/language/data-io/get-data-spreadsheet.at

diff --git a/INSTALL b/INSTALL
index c131292cb3d9da72d94bd366b99a7ea80c311f9f..15f356a9451d361a3e6cdfb759018374c451b5fb 100644 (file)
--- a/INSTALL
+++ b/INSTALL
@@ -104,6 +104,7 @@ use the GUI, you must run `configure' with --without-gui.
       version 3.4.0 or later.
 
     * GNU Spread Sheet Widget (http://www.gnu.org/software/ssw)
+      version 0.7 or later.
 
 The following packages are optional:
 
diff --git a/NEWS b/NEWS
index 70cf096d1eaf552b8487a8501e3d3cdd054e98d2..c30e558cb26c79f0f29490cc7e77975a5e5647a4 100644 (file)
--- a/NEWS
+++ b/NEWS
@@ -11,6 +11,10 @@ Changes from 1.4.1 to 1.5.2:
  * The Explore GUI dialog supports the "Plots" subdialog. Boxplots, Q-Q Plots
    and Spreadlevel plots are now also available via the GUI.
 
+ * The graphical user interface for importing spreadsheets has been improved.
+   The new interface provides the user with a preview of the data to be imported
+   and interactive methods to select the desired ranges.
+
 Changes from 1.4.0 to 1.4.1:
 
  * Bug fixes.
index f0680e830e2e608f1313922ec2a7de358c4c782d..3bc4ea1fd695d28bc28cc06124cc5aaf5104f349 100644 (file)
@@ -134,8 +134,8 @@ if test "$with_cairo" != no && test "$with_gui" != "no"; then
   PKG_CHECK_MODULES([GLIB], [glib-2.0 >= 2.44], [],
     [PSPP_REQUIRED_PREREQ([glib 2.0 version 2.44 or later (or use --without-gui)])])
 
-  PKG_CHECK_MODULES([SPREAD_SHEET_WIDGET], [spread-sheet-widget >= 0.6], [],
-    [PSPP_REQUIRED_PREREQ([spread-sheet-widget 0.6 (or use --without-gui)])])
+  PKG_CHECK_MODULES([SPREAD_SHEET_WIDGET], [spread-sheet-widget >= 0.7], [],
+    [PSPP_REQUIRED_PREREQ([spread-sheet-widget 0.7 (or use --without-gui)])])
 
   PKG_CHECK_MODULES([LIBRSVG], [librsvg-2.0 >= 2.44],
     [AC_DEFINE([HAVE_RSVG], 1, [Define to 1 if librsvg is available])],
index acfc3c9064741a7c3550a46cad348b756f56121a..c1bf389d262c6ee0368d324d224732d9568fb157 100644 (file)
@@ -1,5 +1,6 @@
 /* PSPP - a program for statistical analysis.
-   Copyright (C) 2007, 2009, 2010, 2011, 2012, 2013, 2016  Free Software Foundation, Inc.
+   Copyright (C) 2007, 2009, 2010, 2011, 2012, 2013, 2016,
+   2020 Free Software Foundation, Inc.
 
    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
@@ -36,6 +37,9 @@
 #include "libpspp/i18n.h"
 #include "libpspp/message.h"
 #include "libpspp/misc.h"
+#include "libpspp/hmap.h"
+#include "libpspp/hash-functions.h"
+
 #include "libpspp/str.h"
 
 #include "gl/c-strtod.h"
 #define _(msgid) gettext (msgid)
 #define N_(msgid) (msgid)
 
+/* Setting this to false can help with debugging and development.
+   Don't forget to set it back to true, or users will complain that
+   all but the smallest spreadsheets display VERY slowly.  */
+static const bool use_cache = true;
+
 /* Shamelessly lifted from the Gnumeric sources:
    https://git.gnome.org/browse/gnumeric/tree/src/value.h
  */
@@ -63,7 +72,6 @@ enum gnm_value_type
 };
 
 
-
 static void gnm_file_casereader_destroy (struct casereader *, void *);
 
 static struct ccase *gnm_file_casereader_read (struct casereader *, void *);
@@ -91,22 +99,10 @@ enum reader_state
     STATE_CELL             /* Found a cell */
   };
 
-struct sheet_detail
-{
-  /* The name of the sheet (utf8 encoding) */
-  char *name;
-
-  int start_col;
-  int stop_col;
-  int start_row;
-  int stop_row;
-
-  int maxcol;
-  int maxrow;
-};
-
 struct state_data
 {
+  gzFile gz;
+
   /* The libxml reader for this instance */
   xmlTextReaderPtr xtr;
 
@@ -137,88 +133,266 @@ struct gnumeric_reader
   struct state_data rsd;
   struct state_data msd;
 
-  int start_col;
-  int stop_col;
-  int start_row;
-  int stop_row;
-
-  struct sheet_detail *sheets;
-
-  const xmlChar *target_sheet;
+  const xmlChar *target_sheet_name;
   int target_sheet_index;
 
-  struct caseproto *proto;
-  struct dictionary *dict;
-  struct ccase *first_case;
-  bool used_first_case;
-
   enum gnm_value_type vtype;
+
+  /* The total number of sheets in the "workbook" */
+  int n_sheets;
+
+  struct hmap cache;
 };
 
+/* A value to be kept in the hash table for cache purposes.  */
+struct cache_datum
+{
+  struct hmap_node node;
+
+  /* The cell's row.  */
+  int row;
+
+  /* The cell's column.  */
+  int col;
+
+  /* The value of the cell.  */
+  char *value;
+};
 
-void
-gnumeric_unref (struct spreadsheet *s)
+static void
+gnumeric_destroy (struct spreadsheet *s)
 {
   struct gnumeric_reader *r = (struct gnumeric_reader *) s;
 
-  if (0 == --s->ref_cnt)
+  int i;
+
+  for (i = 0; i < r->n_sheets; ++i)
     {
-      int i;
+      xmlFree (r->spreadsheet.sheets[i].name);
+    }
 
-      for (i = 0; i < s->n_sheets; ++i)
-       {
-         xmlFree (r->sheets[i].name);
-       }
+  if (s->dict)
+    dict_unref (s->dict);
 
+  free (r->spreadsheet.sheets);
+  state_data_destroy (&r->msd);
 
-      free (r->sheets);
-      state_data_destroy (&r->msd);
+  free (s->file_name);
 
-      dict_unref (r->dict);
+  struct cache_datum *cell;
+  struct cache_datum *next;
+  HMAP_FOR_EACH_SAFE (cell, next, struct cache_datum, node, &r->cache)
+    {
+      free (cell->value);
+      free (cell);
+    }
 
-      free (s->file_name);
+  hmap_destroy (&r->cache);
 
-      free (r);
-    }
+  free (r);
 }
 
 
-const char *
+static const char *
 gnumeric_get_sheet_name (struct spreadsheet *s, int n)
 {
   struct gnumeric_reader *gr = (struct gnumeric_reader *) s;
-  assert (n < s->n_sheets);
+  assert (n < gr->n_sheets);
 
-  return gr->sheets[n].name;
+  return gr->spreadsheet.sheets[n].name;
 }
 
 
 static void process_node (struct gnumeric_reader *r, struct state_data *sd);
 
 
+static int
+gnumeric_get_sheet_n_sheets (struct spreadsheet *s)
+{
+  struct gnumeric_reader *gr = (struct gnumeric_reader *) s;
 
-char *
+  int ret;
+  while (1 == (ret = xmlTextReaderRead (gr->msd.xtr)))
+    {
+      process_node (gr, &gr->msd);
+    }
+
+  return gr->n_sheets;
+}
+
+
+static char *
 gnumeric_get_sheet_range (struct spreadsheet *s, int n)
 {
   int ret;
   struct gnumeric_reader *gr = (struct gnumeric_reader *) s;
 
-  assert (n < s->n_sheets);
-
-  while (
-        (gr->sheets[n].stop_col == -1)
+  while ((gr->spreadsheet.sheets[n].last_col == -1)
         &&
-        (1 == (ret = xmlTextReaderRead (gr->msd.xtr)))
-       )
+        (1 == (ret = xmlTextReaderRead (gr->msd.xtr))))
     {
       process_node (gr, &gr->msd);
     }
 
+  assert (n < gr->n_sheets);
   return create_cell_range (
-                         gr->sheets[n].start_col,
-                         gr->sheets[n].start_row,
-                         gr->sheets[n].stop_col,
-                         gr->sheets[n].stop_row);
+                         gr->spreadsheet.sheets[n].first_col,
+                         gr->spreadsheet.sheets[n].first_row,
+                         gr->spreadsheet.sheets[n].last_col,
+                         gr->spreadsheet.sheets[n].last_row);
+}
+
+
+static unsigned int
+gnumeric_get_sheet_n_rows (struct spreadsheet *s, int n)
+{
+  struct gnumeric_reader *gr = (struct gnumeric_reader *) s;
+
+  while ((gr->spreadsheet.sheets[n].last_col == -1)
+        &&
+        (1 == xmlTextReaderRead (gr->msd.xtr)))
+    {
+      process_node (gr, &gr->msd);
+    }
+
+  assert (n < gr->n_sheets);
+  return gr->spreadsheet.sheets[n].last_row + 1;
+}
+
+static unsigned int
+gnumeric_get_sheet_n_columns (struct spreadsheet *s, int n)
+{
+  struct gnumeric_reader *gr = (struct gnumeric_reader *) s;
+
+  while ((gr->spreadsheet.sheets[n].last_col == -1)
+        &&
+        (1 == xmlTextReaderRead (gr->msd.xtr)))
+    {
+      process_node (gr, &gr->msd);
+    }
+
+  assert (n < gr->n_sheets);
+  return gr->spreadsheet.sheets[n].last_col + 1;
+}
+
+static struct gnumeric_reader *
+gnumeric_reopen (struct gnumeric_reader *r, const char *filename, bool show_errors);
+
+
+static char *
+gnumeric_get_sheet_cell (struct spreadsheet *s, int n, int row, int column)
+{
+  struct gnumeric_reader *gr = (struct gnumeric_reader *) s;
+
+  /* See if this cell is in the cache.  If it is, then use it.  */
+  if (use_cache)
+    {
+      struct cache_datum *lookup = NULL;
+      unsigned int hash = hash_int (row, 0);
+      hash = hash_int (column, hash);
+
+      HMAP_FOR_EACH_WITH_HASH (lookup, struct cache_datum, node, hash,
+                              &gr->cache)
+       {
+         if (lookup->row == row && lookup->col == column)
+           {
+             break;
+           }
+       }
+      if (lookup)
+       {
+         return strdup (lookup->value);
+       }
+    }
+
+  struct state_data sd;
+
+  sd.state = STATE_PRE_INIT;
+  sd.current_sheet = -1;
+  sd.row = -1;
+  sd.col = -1;
+  sd.min_col = 0;
+  sd.gz =  gzopen (s->file_name, "r");
+
+  sd.xtr = xmlReaderForIO ((xmlInputReadCallback) gzread,
+                                (xmlInputCloseCallback) gzclose,
+                                sd.gz,
+                                NULL, NULL,
+                                0);
+
+
+  gr->target_sheet_name = NULL;
+
+  int current_row = -1;
+  int current_col = -1;
+
+  /* Spool to the target cell, caching values of cells as they are encountered.  */
+  for (int ret = 1; ret; )
+    {
+      while ((ret = xmlTextReaderRead (sd.xtr)))
+        {
+          process_node (gr, &sd);
+          if (sd.state == STATE_CELL)
+            {
+              if (sd.current_sheet == n)
+                {
+                  current_row = sd.row;
+                  current_col = sd.col;
+                  break;
+                }
+            }
+        }
+      if (current_row >= row && current_col >= column - 1)
+        break;
+
+      while ((ret = xmlTextReaderRead (sd.xtr)))
+        {
+          process_node (gr, &sd);
+          if (sd.node_type == XML_READER_TYPE_TEXT)
+            break;
+        }
+
+      if (use_cache)
+       {
+         /* See if this cell has already been cached ... */
+         unsigned int hash = hash_int (current_row, 0);
+         hash = hash_int (current_col, hash);
+         struct cache_datum *probe = NULL;
+         HMAP_FOR_EACH_WITH_HASH (probe, struct cache_datum, node, hash,
+                                  &gr->cache)
+           {
+             if (probe->row == current_row && probe->col == current_col)
+               break;
+           }
+         /* If not, then cache it.  */
+         if (!probe)
+           {
+             char *str = CHAR_CAST (char *, xmlTextReaderValue (sd.xtr));
+             struct cache_datum *cell_data = XMALLOC (struct cache_datum);
+             cell_data->row = current_row;
+             cell_data->col = current_col;
+             cell_data->value = str;
+             hmap_insert (&gr->cache, &cell_data->node, hash);
+           }
+       }
+    }
+
+  while (xmlTextReaderRead (sd.xtr))
+    {
+      process_node (gr, &sd);
+      if (sd.state == STATE_CELL && sd.node_type == XML_READER_TYPE_TEXT)
+        {
+          if (sd.current_sheet == n)
+            {
+              if (row == sd.row && column == sd.col)
+                break;
+            }
+        }
+    }
+
+  char *cell_content = CHAR_CAST (char *, xmlTextReaderValue (sd.xtr));
+  xmlFreeTextReader (sd.xtr);
+  return cell_content;
 }
 
 
@@ -232,13 +406,13 @@ gnm_file_casereader_destroy (struct casereader *reader UNUSED, void *r_)
 
   state_data_destroy (&r->rsd);
 
-  if (r->first_case &&  ! r->used_first_case)
-    case_unref (r->first_case);
+  if (r->spreadsheet.first_case &&  ! r->spreadsheet.used_first_case)
+    case_unref (r->spreadsheet.first_case);
 
-  if (r->proto)
-    caseproto_unref (r->proto);
+  if (r->spreadsheet.proto)
+    caseproto_unref (r->spreadsheet.proto);
 
-  gnumeric_unref (&r->spreadsheet);
+  spreadsheet_unref (&r->spreadsheet);
 }
 
 
@@ -267,14 +441,14 @@ process_node (struct gnumeric_reader *r, struct state_data *sd)
          XML_READER_TYPE_ELEMENT  == sd->node_type)
        {
          ++sd->current_sheet;
-         if (sd->current_sheet + 1 > r->spreadsheet.n_sheets)
+         if (sd->current_sheet + 1 > r->n_sheets)
            {
              struct sheet_detail *detail ;
-             r->sheets = xrealloc (r->sheets, (sd->current_sheet + 1) * sizeof *r->sheets);
-             detail = &r->sheets[sd->current_sheet];
-             detail->start_col = detail->stop_col = detail->start_row = detail->stop_row = -1;
+             r->spreadsheet.sheets = xrealloc (r->spreadsheet.sheets, (sd->current_sheet + 1) * sizeof *r->spreadsheet.sheets);
+             detail = &r->spreadsheet.sheets[sd->current_sheet];
+             detail->first_col = detail->last_col = detail->first_row = detail->last_row = -1;
              detail->name = NULL;
-             r->spreadsheet.n_sheets = sd->current_sheet + 1;
+             r->n_sheets = sd->current_sheet + 1;
            }
        }
       else if (0 == xmlStrcasecmp (name, _xml("gnm:SheetNameIndex")) &&
@@ -285,8 +459,9 @@ process_node (struct gnumeric_reader *r, struct state_data *sd)
        }
       else if (XML_READER_TYPE_TEXT == sd->node_type)
        {
-         if (r->sheets [r->spreadsheet.n_sheets - 1].name == NULL)
-           r->sheets [r->spreadsheet.n_sheets - 1].name = CHAR_CAST (char *, xmlTextReaderValue (sd->xtr));
+         if (r->spreadsheet.sheets [r->n_sheets - 1].name == NULL)
+           r->spreadsheet.sheets [r->n_sheets - 1].name =
+              CHAR_CAST (char *, xmlTextReaderValue (sd->xtr));
        }
       break;
 
@@ -318,10 +493,10 @@ process_node (struct gnumeric_reader *r, struct state_data *sd)
        }
       else if (XML_READER_TYPE_TEXT == sd->node_type)
        {
-                 if (r->target_sheet != NULL)
+                 if (r->target_sheet_name != NULL)
            {
              xmlChar *value = xmlTextReaderValue (sd->xtr);
-             if (0 == xmlStrcmp (value, r->target_sheet))
+             if (0 == xmlStrcmp (value, r->target_sheet_name))
                sd->state = STATE_SHEET_FOUND;
              free (value);
            }
@@ -368,7 +543,6 @@ process_node (struct gnumeric_reader *r, struct state_data *sd)
       else if (sd->node_type == XML_READER_TYPE_TEXT)
        {
          xmlChar *value = xmlTextReaderValue (sd->xtr);
-         r->sheets[sd->current_sheet].maxrow = _xmlchar_to_int (value);
          xmlFree (value);
        }
       break;
@@ -381,7 +555,6 @@ process_node (struct gnumeric_reader *r, struct state_data *sd)
       else if (sd->node_type == XML_READER_TYPE_TEXT)
        {
          xmlChar *value = xmlTextReaderValue (sd->xtr);
-         r->sheets[sd->current_sheet].maxcol = _xmlchar_to_int (value);
          xmlFree (value);
        }
       break;
@@ -389,9 +562,7 @@ process_node (struct gnumeric_reader *r, struct state_data *sd)
       if (0 == xmlStrcasecmp (name, _xml ("gnm:Cell"))  &&
          XML_READER_TYPE_ELEMENT  == sd->node_type)
        {
-         xmlChar *attr = NULL;
-
-         attr = xmlTextReaderGetAttribute (sd->xtr, _xml ("Col"));
+         xmlChar *attr = xmlTextReaderGetAttribute (sd->xtr, _xml ("Col"));
          sd->col =  _xmlchar_to_int (attr);
          free (attr);
 
@@ -402,27 +573,29 @@ process_node (struct gnumeric_reader *r, struct state_data *sd)
          sd->row = _xmlchar_to_int (attr);
          free (attr);
 
-         if (r->sheets[sd->current_sheet].start_row == -1)
+         if (r->spreadsheet.sheets[sd->current_sheet].first_row == -1)
            {
-             r->sheets[sd->current_sheet].start_row = sd->row;
+             r->spreadsheet.sheets[sd->current_sheet].first_row = sd->row;
            }
 
-         if (r->sheets[sd->current_sheet].start_col == -1)
+         if (r->spreadsheet.sheets[sd->current_sheet].first_col == -1)
            {
-             r->sheets[sd->current_sheet].start_col = sd->col;
+             r->spreadsheet.sheets[sd->current_sheet].first_col = sd->col;
            }
          if (! xmlTextReaderIsEmptyElement (sd->xtr))
            sd->state = STATE_CELL;
        }
-      else if ((0 == xmlStrcasecmp (name, _xml("gnm:Cells")))  &&  (XML_READER_TYPE_END_ELEMENT  == sd->node_type))
+      else if ((0 == xmlStrcasecmp (name, _xml("gnm:Cells")))
+              &&  (XML_READER_TYPE_END_ELEMENT  == sd->node_type))
        {
-         r->sheets[sd->current_sheet].stop_col = sd->col;
-         r->sheets[sd->current_sheet].stop_row = sd->row;
+         r->spreadsheet.sheets[sd->current_sheet].last_col = sd->col;
+         r->spreadsheet.sheets[sd->current_sheet].last_row = sd->row;
          sd->state = STATE_SHEET_NAME;
        }
       break;
     case STATE_CELL:
-      if (0 == xmlStrcasecmp (name, _xml("gnm:Cell"))  && XML_READER_TYPE_END_ELEMENT  == sd->node_type)
+      if (0 == xmlStrcasecmp (name, _xml("gnm:Cell"))
+         && XML_READER_TYPE_END_ELEMENT  == sd->node_type)
        {
          sd->state = STATE_CELLS_START;
        }
@@ -506,115 +679,7 @@ gnumeric_error_handler (void *ctx, const char *mesg,
        mesg);
 }
 
-static struct gnumeric_reader *
-gnumeric_reopen (struct gnumeric_reader *r, const char *filename, bool show_errors)
-{
-  int ret = -1;
-  struct state_data *sd;
-
-  xmlTextReaderPtr xtr;
-  gzFile gz;
-
-  assert (r == NULL || filename == NULL);
-
-  if (filename)
-    {
-      gz = gzopen (filename, "r");
-    }
-  else
-    {
-      gz = gzopen (r->spreadsheet.file_name, "r");
-    }
-
-  if (NULL == gz)
-    return NULL;
-
-
-  xtr = xmlReaderForIO ((xmlInputReadCallback) gzread,
-                       (xmlInputCloseCallback) gzclose, gz,
-                       NULL, NULL,
-                       show_errors ? 0 : (XML_PARSE_NOERROR | XML_PARSE_NOWARNING));
-
-  if (xtr == NULL)
-    {
-      gzclose (gz);
-      return NULL;
-    }
-
-  if (r == NULL)
-    {
-      r = xzalloc (sizeof *r);
-      r->spreadsheet.n_sheets = -1;
-      r->spreadsheet.file_name = strdup (filename);
-      sd = &r->msd;
-    }
-  else
-    {
-      sd = &r->rsd;
-    }
-
-  if (show_errors)
-    xmlTextReaderSetErrorHandler (xtr, gnumeric_error_handler, r);
-
-  r->target_sheet = NULL;
-  r->target_sheet_index = -1;
-
-  sd->row = sd->col = -1;
-  sd->state = STATE_PRE_INIT;
-  sd->xtr = xtr;
-  r->spreadsheet.ref_cnt++;
-
-
-  /* Advance to the start of the workbook.
-     This gives us some confidence that we are actually dealing with a gnumeric
-     spreadsheet.
-   */
-  while ((sd->state != STATE_INIT)
-         && 1 == (ret = xmlTextReaderRead (sd->xtr)))
-    {
-      process_node (r, sd);
-    }
-
-
-  if (ret != 1)
-    {
-      /* Does not seem to be a gnumeric file */
-      gnumeric_unref (&r->spreadsheet);
-      return NULL;
-    }
-
-  r->spreadsheet.type = SPREADSHEET_GNUMERIC;
-
-  if (show_errors)
-    {
-      const xmlChar *enc = xmlTextReaderConstEncoding (sd->xtr);
-      xmlCharEncoding xce = xmlParseCharEncoding (CHAR_CAST (const char *, enc));
-
-      if (XML_CHAR_ENCODING_UTF8 != xce)
-       {
-         /* I have been told that ALL gnumeric files are UTF8 encoded.  If that is correct, this
-            can never happen. */
-         msg (MW, _("The gnumeric file `%s' is encoded as %s instead of the usual UTF-8 encoding. "
-                    "Any non-ascii characters will be incorrectly imported."),
-              r->spreadsheet.file_name,
-              enc);
-       }
-    }
-
-  return r;
-}
-
-
-struct spreadsheet *
-gnumeric_probe (const char *filename, bool report_errors)
-{
-  struct gnumeric_reader *r = gnumeric_reopen (NULL, filename, report_errors);
-
-  return &r->spreadsheet;
-}
-
-
-struct casereader *
+static struct casereader *
 gnumeric_make_reader (struct spreadsheet *spreadsheet,
                      const struct spreadsheet_read_options *opts)
 {
@@ -635,8 +700,8 @@ gnumeric_make_reader (struct spreadsheet *spreadsheet,
   if (opts->cell_range)
     {
       if (! convert_cell_ref (opts->cell_range,
-                              &r->start_col, &r->start_row,
-                              &r->stop_col, &r->stop_row))
+                              &r->spreadsheet.start_col, &r->spreadsheet.start_row,
+                              &r->spreadsheet.stop_col, &r->spreadsheet.stop_row))
        {
          msg (SE, _("Invalid cell range `%s'"),
               opts->cell_range);
@@ -645,21 +710,21 @@ gnumeric_make_reader (struct spreadsheet *spreadsheet,
     }
   else
     {
-      r->start_col = -1;
-      r->start_row = 0;
-      r->stop_col = -1;
-      r->stop_row = -1;
+      r->spreadsheet.start_col = -1;
+      r->spreadsheet.start_row = 0;
+      r->spreadsheet.stop_col = -1;
+      r->spreadsheet.stop_row = -1;
     }
 
-  r->target_sheet = BAD_CAST opts->sheet_name;
+  r->target_sheet_name = BAD_CAST opts->sheet_name;
   r->target_sheet_index = opts->sheet_index;
   r->rsd.row = r->rsd.col = -1;
   r->rsd.current_sheet = -1;
-  r->first_case = NULL;
-  r->proto = NULL;
+  r->spreadsheet.first_case = NULL;
+  r->spreadsheet.proto = NULL;
 
   /* Advance to the start of the cells for the target sheet */
-  while ((r->rsd.state != STATE_CELL || r->rsd.row < r->start_row)
+  while ((r->rsd.state != STATE_CELL || r->rsd.row < r->spreadsheet.start_row)
          && 1 == (ret = xmlTextReaderRead (r->rsd.xtr)))
     {
       xmlChar *value ;
@@ -677,12 +742,12 @@ gnumeric_make_reader (struct spreadsheet *spreadsheet,
      of cases */
   if (opts->cell_range)
     {
-      n_cases = MIN (n_cases, r->stop_row - r->start_row + 1);
+      n_cases = MIN (n_cases, r->spreadsheet.stop_row - r->spreadsheet.start_row + 1);
     }
 
   if (opts->read_names)
     {
-      r->start_row++;
+      r->spreadsheet.start_row++;
       n_cases --;
     }
 
@@ -690,7 +755,7 @@ gnumeric_make_reader (struct spreadsheet *spreadsheet,
   /* Read in the first row of cells,
      including the headers if read_names was set */
   while (
-        ((r->rsd.state == STATE_CELLS_START && r->rsd.row <= r->start_row) || r->rsd.state == STATE_CELL)
+        ((r->rsd.state == STATE_CELLS_START && r->rsd.row <= r->spreadsheet.start_row) || r->rsd.state == STATE_CELL)
         && (ret = xmlTextReaderRead (r->rsd.xtr))
        )
     {
@@ -708,7 +773,7 @@ gnumeric_make_reader (struct spreadsheet *spreadsheet,
 
       process_node (r, &r->rsd);
 
-      if (r->rsd.row > r->start_row)
+      if (r->rsd.row > r->spreadsheet.start_row)
        {
          xmlChar *attr =
            xmlTextReaderGetAttribute (r->rsd.xtr, _xml ("ValueType"));
@@ -719,11 +784,11 @@ gnumeric_make_reader (struct spreadsheet *spreadsheet,
          break;
        }
 
-      if (r->rsd.col < r->start_col ||
-          (r->stop_col != -1 && r->rsd.col > r->stop_col))
+      if (r->rsd.col < r->spreadsheet.start_col ||
+          (r->spreadsheet.stop_col != -1 && r->rsd.col > r->spreadsheet.stop_col))
        continue;
 
-      idx = r->rsd.col - r->start_col;
+      idx = r->rsd.col - r->spreadsheet.start_col;
 
       if (idx  >= n_var_specs)
        {
@@ -746,7 +811,7 @@ gnumeric_make_reader (struct spreadsheet *spreadsheet,
          xmlChar *value = xmlTextReaderValue (r->rsd.xtr);
          const char *text  = CHAR_CAST (const char *, value);
 
-         if (r->rsd.row < r->start_row)
+         if (r->rsd.row < r->spreadsheet.start_row)
            {
              if (opts->read_names)
                {
@@ -767,7 +832,7 @@ gnumeric_make_reader (struct spreadsheet *spreadsheet,
       else if (r->rsd.node_type == XML_READER_TYPE_ELEMENT
                && r->rsd.state == STATE_CELL)
        {
-         if (r->rsd.row == r->start_row)
+         if (r->rsd.row == r->spreadsheet.start_row)
            {
              xmlChar *attr =
                xmlTextReaderGetAttribute (r->rsd.xtr, _xml ("ValueType"));
@@ -785,7 +850,7 @@ gnumeric_make_reader (struct spreadsheet *spreadsheet,
     if (enc == NULL)
       goto error;
     /* Create the dictionary and populate it */
-    spreadsheet->dict = r->dict = dict_create (CHAR_CAST (const char *, enc));
+    spreadsheet->dict = dict_create (CHAR_CAST (const char *, enc));
   }
 
   for (i = 0 ; i < n_var_specs ; ++i)
@@ -800,13 +865,13 @@ gnumeric_make_reader (struct spreadsheet *spreadsheet,
       if (var_spec[i].width == -1)
        var_spec[i].width = SPREADSHEET_DEFAULT_WIDTH;
 
-      name = dict_make_unique_var_name (r->dict, var_spec[i].name, &vstart);
-      dict_create_var (r->dict, name, var_spec[i].width);
+      name = dict_make_unique_var_name (r->spreadsheet.dict, var_spec[i].name, &vstart);
+      dict_create_var (r->spreadsheet.dict, name, var_spec[i].width);
       free (name);
     }
 
   /* Create the first case, and cache it */
-  r->used_first_case = false;
+  r->spreadsheet.used_first_case = false;
 
   if (n_var_specs ==  0)
     {
@@ -815,9 +880,9 @@ gnumeric_make_reader (struct spreadsheet *spreadsheet,
       goto error;
     }
 
-  r->proto = caseproto_ref (dict_get_proto (r->dict));
-  r->first_case = case_create (r->proto);
-  case_set_missing (r->first_case);
+  r->spreadsheet.proto = caseproto_ref (dict_get_proto (r->spreadsheet.dict));
+  r->spreadsheet.first_case = case_create (r->spreadsheet.proto);
+  case_set_missing (r->spreadsheet.first_case);
 
 
   for (i = 0 ; i < n_var_specs ; ++i)
@@ -827,9 +892,9 @@ gnumeric_make_reader (struct spreadsheet *spreadsheet,
       if ((var_spec[i].name == NULL) && (var_spec[i].first_value == NULL))
        continue;
 
-      var = dict_get_var (r->dict, x++);
+      var = dict_get_var (r->spreadsheet.dict, x++);
 
-      convert_xml_string_to_value (r->first_case, var,
+      convert_xml_string_to_value (r->spreadsheet.first_case, var,
                                   var_spec[i].first_value,
                                   var_spec[i].first_type,
                                   r->rsd.col + i - 1,
@@ -847,7 +912,7 @@ gnumeric_make_reader (struct spreadsheet *spreadsheet,
 
   return casereader_create_sequential
     (NULL,
-     r->proto,
+     r->spreadsheet.proto,
      n_cases,
      &gnm_file_casereader_class, r);
 
@@ -878,17 +943,17 @@ gnm_file_casereader_read (struct casereader *reader UNUSED, void *r_)
   struct gnumeric_reader *r = r_;
   int current_row = r->rsd.row;
 
-  if (!r->used_first_case)
+  if (!r->spreadsheet.used_first_case)
     {
-      r->used_first_case = true;
-      return r->first_case;
+      r->spreadsheet.used_first_case = true;
+      return r->spreadsheet.first_case;
     }
 
-  c = case_create (r->proto);
+  c = case_create (r->spreadsheet.proto);
   case_set_missing (c);
 
-  if (r->start_col == -1)
-    r->start_col = r->rsd.min_col;
+  if (r->spreadsheet.start_col == -1)
+    r->spreadsheet.start_col = r->rsd.min_col;
 
 
   while ((r->rsd.state == STATE_CELL || r->rsd.state == STATE_CELLS_START)
@@ -906,22 +971,22 @@ gnm_file_casereader_read (struct casereader *reader UNUSED, void *r_)
          xmlFree (attr);
        }
 
-      if (r->rsd.col < r->start_col || (r->stop_col != -1 &&
-                                    r->rsd.col > r->stop_col))
+      if (r->rsd.col < r->spreadsheet.start_col || (r->spreadsheet.stop_col != -1 &&
+                                    r->rsd.col > r->spreadsheet.stop_col))
        continue;
 
-      if (r->rsd.col - r->start_col >= caseproto_get_n_widths (r->proto))
+      if (r->rsd.col - r->spreadsheet.start_col >= caseproto_get_n_widths (r->spreadsheet.proto))
        continue;
 
-      if (r->stop_row != -1 && r->rsd.row > r->stop_row)
+      if (r->spreadsheet.stop_row != -1 && r->rsd.row > r->spreadsheet.stop_row)
        break;
 
 
       if (r->rsd.node_type == XML_READER_TYPE_TEXT)
        {
          xmlChar *value = xmlTextReaderValue (r->rsd.xtr);
-         const int idx = r->rsd.col - r->start_col;
-         const struct variable *var = dict_get_var (r->dict, idx);
+         const int idx = r->rsd.col - r->spreadsheet.start_col;
+         const struct variable *var = dict_get_var (r->spreadsheet.dict, idx);
 
          convert_xml_string_to_value (c, var, value, r->vtype,
                                       r->rsd.col, r->rsd.row);
@@ -938,3 +1003,123 @@ gnm_file_casereader_read (struct casereader *reader UNUSED, void *r_)
       return NULL;
     }
 }
+
+static struct gnumeric_reader *
+gnumeric_reopen (struct gnumeric_reader *r, const char *filename, bool show_errors)
+{
+  int ret = -1;
+  struct state_data *sd;
+
+  xmlTextReaderPtr xtr;
+  gzFile gz;
+
+  assert (r == NULL || filename == NULL);
+
+  if (filename)
+    {
+      gz = gzopen (filename, "r");
+    }
+  else
+    {
+      gz = gzopen (r->spreadsheet.file_name, "r");
+    }
+
+  if (NULL == gz)
+    return NULL;
+
+  if (r == NULL)
+    {
+      r = xzalloc (sizeof *r);
+      r->n_sheets = -1;
+      r->spreadsheet.file_name = strdup (filename);
+      struct spreadsheet *s = SPREADSHEET_CAST (r);
+      strcpy (s->type, "GNM");
+      s->destroy = gnumeric_destroy;
+      s->make_reader = gnumeric_make_reader;
+      s->get_sheet_name = gnumeric_get_sheet_name;
+      s->get_sheet_range = gnumeric_get_sheet_range;
+      s->get_sheet_n_sheets = gnumeric_get_sheet_n_sheets;
+      s->get_sheet_n_rows = gnumeric_get_sheet_n_rows;
+      s->get_sheet_n_columns = gnumeric_get_sheet_n_columns;
+      s->get_sheet_cell = gnumeric_get_sheet_cell;
+
+      sd = &r->msd;
+      hmap_init (&r->cache);
+    }
+  else
+    {
+      sd = &r->rsd;
+    }
+  sd->gz = gz;
+
+  r = (struct gnumeric_reader *) spreadsheet_ref (SPREADSHEET_CAST (r));
+
+  {
+    xtr = xmlReaderForIO ((xmlInputReadCallback) gzread,
+                         (xmlInputCloseCallback) gzclose, gz,
+                         NULL, NULL,
+                         show_errors ? 0 : (XML_PARSE_NOERROR | XML_PARSE_NOWARNING));
+
+    if (xtr == NULL)
+      {
+       gzclose (gz);
+       free (r);
+       return NULL;
+      }
+
+    if (show_errors)
+      xmlTextReaderSetErrorHandler (xtr, gnumeric_error_handler, r);
+
+    sd->row = sd->col = -1;
+    sd->state = STATE_PRE_INIT;
+    sd->xtr = xtr;
+  }
+
+  r->target_sheet_name = NULL;
+  r->target_sheet_index = -1;
+
+
+  /* Advance to the start of the workbook.
+     This gives us some confidence that we are actually dealing with a gnumeric
+     spreadsheet.
+   */
+  while ((sd->state != STATE_INIT)
+         && 1 == (ret = xmlTextReaderRead (sd->xtr)))
+    {
+      process_node (r, sd);
+    }
+
+  if (ret != 1)
+    {
+      /* Does not seem to be a gnumeric file */
+      spreadsheet_unref (&r->spreadsheet);
+      return NULL;
+    }
+
+  if (show_errors)
+    {
+      const xmlChar *enc = xmlTextReaderConstEncoding (sd->xtr);
+      xmlCharEncoding xce = xmlParseCharEncoding (CHAR_CAST (const char *, enc));
+
+      if (XML_CHAR_ENCODING_UTF8 != xce)
+       {
+         /* I have been told that ALL gnumeric files are UTF8 encoded.  If that is correct, this
+            can never happen. */
+         msg (MW, _("The gnumeric file `%s' is encoded as %s instead of the usual UTF-8 encoding. "
+                    "Any non-ascii characters will be incorrectly imported."),
+              r->spreadsheet.file_name,
+              enc);
+       }
+    }
+
+  return r;
+}
+
+
+struct spreadsheet *
+gnumeric_probe (const char *filename, bool report_errors)
+{
+  struct gnumeric_reader *r = gnumeric_reopen (NULL, filename, report_errors);
+
+  return &r->spreadsheet;
+}
index edec2b66c46e7e0203087287d211d3d158896ad2..59b0c828a570cdd278e5199a72a4d95a9b340cb1 100644 (file)
 
 #include <stdbool.h>
 
-struct casereader;
-struct dictionary;
-struct spreadsheet_read_info;
-struct spreadsheet_read_options;
-
 struct spreadsheet *gnumeric_probe (const char *filename, bool report_errors);
 
-const char * gnumeric_get_sheet_name (struct spreadsheet *s, int n);
-char * gnumeric_get_sheet_range (struct spreadsheet *s, int n);
-
-struct casereader * gnumeric_make_reader (struct spreadsheet *spreadsheet,
-                                         const struct spreadsheet_read_options *opts);
-
-void gnumeric_unref (struct spreadsheet *r);
-
 
 #endif
index 8f307200b052bb5aa2a8b8d83a6aa93590662d3d..cac060f81d9fa93b518d2f55b0c493a2f4a18dc3 100644 (file)
@@ -1,5 +1,5 @@
 /* PSPP - a program for statistical analysis.
-   Copyright (C) 2011, 2012, 2013, 2016 Free Software Foundation, Inc.
+   Copyright (C) 2011, 2012, 2013, 2016, 2020 Free Software Foundation, Inc.
 
    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
@@ -39,6 +39,9 @@
 #include "libpspp/misc.h"
 #include "libpspp/str.h"
 #include "libpspp/zip-reader.h"
+#include "libpspp/hmap.h"
+#include "libpspp/hash-functions.h"
+
 
 #include "gl/c-strtod.h"
 #include "gl/minmax.h"
 
 #include "gettext.h"
 #define _(msgid) gettext (msgid)
-#define N_(msgid) (msgid)
+
+/* Setting this to false can help with debugging and development.
+   Don't forget to set it back to true, or users will complain that
+   all but the smallest spreadsheets display VERY slowly.  */
+static const bool use_cache = true;
 
 static void ods_file_casereader_destroy (struct casereader *, void *);
 static struct ccase *ods_file_casereader_read (struct casereader *, void *);
 
-
 static const struct casereader_class ods_file_casereader_class =
   {
     ods_file_casereader_read,
@@ -60,18 +66,6 @@ static const struct casereader_class ods_file_casereader_class =
     NULL,
   };
 
-struct sheet_detail
-{
-  /* The name of the sheet (utf8 encoding) */
-  char *name;
-
-  int start_col;
-  int stop_col;
-  int start_row;
-  int stop_row;
-};
-
-
 enum reader_state
   {
     STATE_INIT = 0,        /* Initial state */
@@ -117,67 +111,86 @@ struct ods_reader
   int target_sheet_index;
   xmlChar *target_sheet_name;
 
-  /* State data for the meta data */
-  struct state_data msd;
+  int n_allocated_sheets;
+
+  /* The total number of sheets in the "workbook" */
+  int n_sheets;
 
   /* State data for the reader */
   struct state_data rsd;
 
-  int start_row;
-  int start_col;
-  int stop_row;
-  int stop_col;
+  struct string ods_errs;
 
-  struct sheet_detail *sheets;
-  int n_allocated_sheets;
+  struct string zip_errs;
+  struct hmap cache;
+};
 
-  struct caseproto *proto;
-  struct dictionary *dict;
-  struct ccase *first_case;
-  bool used_first_case;
-  bool read_names;
+/* A value to be kept in the hash table for cache purposes.  */
+struct cache_datum
+{
+  struct hmap_node node;
 
-  struct string ods_errs;
+  /* The the number of the sheet.  */
+  int sheet;
 
-  struct string zip_errs;
+  /* The cell's row.  */
+  int row;
+
+  /* The cell's column.  */
+  int col;
+
+  /* The value of the cell.  */
+  char *value;
 };
 
-void
-ods_unref (struct spreadsheet *s)
+static int
+xml_reader_for_zip_member (void *zm_, char *buffer, int len)
+{
+  struct zip_member *zm = zm_;
+  return zip_member_read (zm, buffer, len);
+}
+
+static void
+ods_destroy (struct spreadsheet *s)
 {
   struct ods_reader *r = (struct ods_reader *) s;
 
-  if (--s->ref_cnt == 0)
+  int i;
+
+  for (i = 0; i < r->n_allocated_sheets; ++i)
     {
-      int i;
+      xmlFree (r->spreadsheet.sheets[i].name);
+    }
 
-      state_data_destroy (&r->msd);
-      for (i = 0; i < r->n_allocated_sheets; ++i)
-       {
-         xmlFree (r->sheets[i].name);
-       }
+  dict_unref (r->spreadsheet.dict);
 
-      dict_unref (r->dict);
+  zip_reader_destroy (r->zreader);
+  free (r->spreadsheet.sheets);
+  free (s->file_name);
 
-      zip_reader_destroy (r->zreader);
-      free (r->sheets);
-      free (s->file_name);
-      free (r);
+  struct cache_datum *cell;
+  struct cache_datum *next;
+  HMAP_FOR_EACH_SAFE (cell, next, struct cache_datum, node, &r->cache)
+    {
+      free (cell->value);
+      free (cell);
     }
-}
 
+  hmap_destroy (&r->cache);
 
+  free (r);
+}
 
 static bool
-reading_target_sheet (const struct ods_reader *r, const struct state_data *msd)
+reading_target_sheet (const struct ods_reader *r, const struct state_data *sd)
 {
   if (r->target_sheet_name != NULL)
     {
-      if (0 == xmlStrcmp (r->target_sheet_name, msd->current_sheet_name))
+      if (0 == xmlStrcmp (r->target_sheet_name, sd->current_sheet_name))
        return true;
     }
 
-  if (r->target_sheet_index == msd->current_sheet + 1)
+  if (r->target_sheet_index == sd->current_sheet + 1)
     return true;
 
   return false;
@@ -187,57 +200,230 @@ reading_target_sheet (const struct ods_reader *r, const struct state_data *msd)
 static void process_node (struct ods_reader *or, struct state_data *r);
 
 
-const char *
+/* Initialise SD using R */
+static bool
+state_data_init (const struct ods_reader *r, struct state_data *sd)
+{
+  memset (sd, 0, sizeof (*sd));
+
+  sd->zm = zip_member_open (r->zreader, "content.xml");
+
+  if (sd->zm == NULL)
+    return false;
+
+  sd->xtr =
+    xmlReaderForIO (xml_reader_for_zip_member, NULL, sd->zm, NULL, NULL,
+                    0);
+
+  if (sd->xtr == NULL)
+    return NULL;
+
+  sd->state = STATE_INIT;
+  return true;
+}
+
+
+static const char *
 ods_get_sheet_name (struct spreadsheet *s, int n)
 {
   struct ods_reader *r = (struct ods_reader *) s;
-  struct state_data *or = &r->msd;
+  struct state_data sd;
+  state_data_init (r, &sd);
 
-  assert (n < s->n_sheets);
-
-  while (
-         (r->n_allocated_sheets <= n)
-         || or->state != STATE_SPREADSHEET
-       )
+  while ((r->n_allocated_sheets <= n)
+        || sd.state != STATE_SPREADSHEET)
     {
-      int ret = xmlTextReaderRead (or->xtr);
+      int ret = xmlTextReaderRead (sd.xtr);
       if (ret != 1)
        break;
 
-      process_node (r, or);
+      process_node (r, &sd);
     }
+  state_data_destroy (&sd);
 
-  return r->sheets[n].name;
+  return r->spreadsheet.sheets[n].name;
 }
 
-char *
+static char *
 ods_get_sheet_range (struct spreadsheet *s, int n)
 {
   struct ods_reader *r = (struct ods_reader *) s;
-  struct state_data *or = &r->msd;
-
-  assert (n < s->n_sheets);
+  struct state_data sd;
+  state_data_init (r, &sd);
 
-  while (
-         (r->n_allocated_sheets <= n)
-         || (r->sheets[n].stop_row == -1)
-         || or->state != STATE_SPREADSHEET
-       )
+  while ((r->n_allocated_sheets <= n)
+         || (r->spreadsheet.sheets[n].last_row == -1)
+        || sd.state != STATE_SPREADSHEET)
     {
-      int ret = xmlTextReaderRead (or->xtr);
+      int ret = xmlTextReaderRead (sd.xtr);
       if (ret != 1)
        break;
 
-      process_node (r, or);
+      process_node (r, &sd);
     }
+  state_data_destroy (&sd);
 
   return create_cell_range (
-                         r->sheets[n].start_col,
-                         r->sheets[n].start_row,
-                         r->sheets[n].stop_col,
-                         r->sheets[n].stop_row);
+                         r->spreadsheet.sheets[n].first_col,
+                         r->spreadsheet.sheets[n].first_row,
+                         r->spreadsheet.sheets[n].last_col,
+                         r->spreadsheet.sheets[n].last_row);
+}
+
+static unsigned int
+ods_get_sheet_n_rows (struct spreadsheet *s, int n)
+{
+  struct ods_reader *r = (struct ods_reader *) s;
+  struct state_data sd;
+
+  if (r->n_allocated_sheets > n && r->spreadsheet.sheets[n].last_row != -1)
+    {
+      return r->spreadsheet.sheets[n].last_row + 1;
+    }
+
+  state_data_init (r, &sd);
+
+  while (1 == xmlTextReaderRead (sd.xtr))
+    {
+      process_node (r, &sd);
+    }
+
+  state_data_destroy (&sd);
+
+  return r->spreadsheet.sheets[n].last_row + 1;
 }
 
+static unsigned int
+ods_get_sheet_n_columns (struct spreadsheet *s, int n)
+{
+  struct ods_reader *r = (struct ods_reader *) s;
+  struct state_data sd;
+
+  if (r->n_allocated_sheets > n && r->spreadsheet.sheets[n].last_col != -1)
+    return r->spreadsheet.sheets[n].last_col + 1;
+
+  state_data_init (r, &sd);
+
+  while (1 == xmlTextReaderRead (sd.xtr))
+    {
+      process_node (r, &sd);
+    }
+
+  state_data_destroy (&sd);
+
+  return r->spreadsheet.sheets[n].last_col + 1;
+}
+
+static char *
+ods_get_sheet_cell (struct spreadsheet *s, int n, int row, int column)
+{
+  struct ods_reader *r = (struct ods_reader *) s;
+  struct state_data sd;
+
+  /* See if this cell is in the cache.  If it is, then use it.  */
+  if (use_cache)
+  {
+    struct cache_datum *lookup = NULL;
+    unsigned int hash = hash_int (n, 0);
+    hash = hash_int (row, hash);
+    hash = hash_int (column, hash);
+
+    HMAP_FOR_EACH_WITH_HASH (lookup, struct cache_datum, node, hash,
+                             &r->cache)
+      {
+        if (lookup->row == row && lookup->col == column
+            && lookup->sheet == n)
+          {
+            break;
+          }
+      }
+    if (lookup)
+      {
+        return lookup->value ? strdup (lookup->value) : NULL;
+      }
+  }
+
+  state_data_init (r, &sd);
+
+  char *cell_content = NULL;
+
+  int prev_col = 0;
+  int prev_row = 0;
+  while (1 == xmlTextReaderRead (sd.xtr))
+    {
+      process_node (r, &sd);
+      if (sd.row > prev_row)
+       prev_col = 0;
+
+      if (sd.state == STATE_CELL_CONTENT
+          && sd.current_sheet == n
+          && sd.node_type == XML_READER_TYPE_TEXT)
+        {
+          /*  When cell contents are encountered, copy and save it, discarding
+              any older content.  */
+          free (cell_content);
+          cell_content = CHAR_CAST (char *, xmlTextReaderValue (sd.xtr));
+        }
+      if (sd.state == STATE_ROW
+               && sd.current_sheet == n
+               && sd.node_type == XML_READER_TYPE_ELEMENT)
+        {
+          /* At the start of a row, free the cell contents and set it to NULL.  */
+          free (cell_content);
+          cell_content = NULL;
+        }
+      if (sd.state == STATE_ROW
+          && sd.current_sheet == n
+          &&
+               (sd.node_type == XML_READER_TYPE_END_ELEMENT
+                ||
+                xmlTextReaderIsEmptyElement (sd.xtr)))
+        {
+          if (use_cache)
+            {
+             for (int c = prev_col; c < sd.col; ++c)
+               {
+                 /* See if this cell has already been cached ... */
+                 unsigned int hash = hash_int (sd.current_sheet, 0);
+                 hash = hash_int (sd.row - 1, hash);
+                 hash = hash_int (c, hash);
+                 struct cache_datum *probe = NULL;
+                 struct cache_datum *next;
+                 HMAP_FOR_EACH_WITH_HASH_SAFE (probe, next, struct cache_datum, node, hash,
+                                               &r->cache)
+                   {
+                     if (probe->row == sd.row - 1 && probe->col == c
+                         && probe->sheet == sd.current_sheet)
+                       break;
+                     probe = NULL;
+                   }
+                 /* If not, then cache it.  */
+                 if (!probe)
+                   {
+                     struct cache_datum *cell_data = XMALLOC (struct cache_datum);
+                     cell_data->row = sd.row - 1;
+                     cell_data->col = c;
+                     cell_data->sheet = sd.current_sheet;
+                     cell_data->value = cell_content ? strdup (cell_content) : NULL;
+
+                     hmap_insert (&r->cache, &cell_data->node, hash);
+                   }
+               }
+            }
+
+          if (sd.row == row + 1 && sd.col >= column + 1)
+           {
+             break;
+           }
+
+         prev_col = sd.col;
+         prev_row = sd.row;
+        }
+    }
+
+  state_data_destroy (&sd);
+  return cell_content;
+}
 
 static void
 ods_file_casereader_destroy (struct casereader *reader UNUSED, void *r_)
@@ -253,24 +439,18 @@ ods_file_casereader_destroy (struct casereader *reader UNUSED, void *r_)
 
   ds_destroy (&r->ods_errs);
 
-  if (r->first_case && ! r->used_first_case)
-    case_unref (r->first_case);
+  if (r->spreadsheet.first_case && ! r->spreadsheet.used_first_case)
+    case_unref (r->spreadsheet.first_case);
 
-
-  caseproto_unref (r->proto);
-  r->proto = NULL;
+  caseproto_unref (r->spreadsheet.proto);
+  r->spreadsheet.proto = NULL;
 
   xmlFree (r->target_sheet_name);
   r->target_sheet_name = NULL;
 
-
-  ods_unref (&r->spreadsheet);
+  spreadsheet_unref (&r->spreadsheet);
 }
 
-
-
-
-
 static void
 process_node (struct ods_reader *or, struct state_data *r)
 {
@@ -305,13 +485,15 @@ process_node (struct ods_reader *or, struct state_data *r)
          if (r->current_sheet >= or->n_allocated_sheets)
            {
              assert (r->current_sheet == or->n_allocated_sheets);
-             or->sheets = xrealloc (or->sheets, sizeof (*or->sheets) * ++or->n_allocated_sheets);
-             or->sheets[or->n_allocated_sheets - 1].start_col = -1;
-             or->sheets[or->n_allocated_sheets - 1].stop_col = -1;
-             or->sheets[or->n_allocated_sheets - 1].start_row = -1;
-             or->sheets[or->n_allocated_sheets - 1].stop_row = -1;
-             or->sheets[or->n_allocated_sheets - 1].name = CHAR_CAST (char *, xmlStrdup (r->current_sheet_name));
+             or->spreadsheet.sheets = xrealloc (or->spreadsheet.sheets, sizeof (*or->spreadsheet.sheets) * ++or->n_allocated_sheets);
+             or->spreadsheet.sheets[or->n_allocated_sheets - 1].first_col = -1;
+             or->spreadsheet.sheets[or->n_allocated_sheets - 1].last_col = -1;
+             or->spreadsheet.sheets[or->n_allocated_sheets - 1].first_row = -1;
+             or->spreadsheet.sheets[or->n_allocated_sheets - 1].last_row = -1;
+             or->spreadsheet.sheets[or->n_allocated_sheets - 1].name = CHAR_CAST (char *, xmlStrdup (r->current_sheet_name));
            }
+         if (or->n_allocated_sheets > or->n_sheets)
+           or->n_sheets = or->n_allocated_sheets;
 
          r->col = 0;
          r->row = 0;
@@ -393,20 +575,21 @@ process_node (struct ods_reader *or, struct state_data *r)
       assert (r->current_sheet >= 0);
       assert (r->current_sheet < or->n_allocated_sheets);
 
-      if (or->sheets[r->current_sheet].start_row == -1)
-       or->sheets[r->current_sheet].start_row = r->row - 1;
+      if (or->spreadsheet.sheets[r->current_sheet].first_row == -1)
+       or->spreadsheet.sheets[r->current_sheet].first_row = r->row - 1;
 
       if (
-         (or->sheets[r->current_sheet].start_col == -1)
+         (or->spreadsheet.sheets[r->current_sheet].first_col == -1)
          ||
-         (or->sheets[r->current_sheet].start_col >= r->col - 1)
+         (or->spreadsheet.sheets[r->current_sheet].first_col >= r->col - 1)
        )
-       or->sheets[r->current_sheet].start_col = r->col - 1;
+       or->spreadsheet.sheets[r->current_sheet].first_col = r->col - 1;
 
-      or->sheets[r->current_sheet].stop_row = r->row - 1;
+      if (or->spreadsheet.sheets[r->current_sheet].last_row < r->row - 1)
+       or->spreadsheet.sheets[r->current_sheet].last_row = r->row - 1;
 
-      if (or->sheets[r->current_sheet].stop_col < r->col - 1)
-       or->sheets[r->current_sheet].stop_col = r->col - 1;
+      if (or->spreadsheet.sheets[r->current_sheet].last_col < r->col - 1)
+       or->spreadsheet.sheets[r->current_sheet].last_col = r->col - 1;
 
       if (XML_READER_TYPE_END_ELEMENT  == r->node_type)
        r->state = STATE_CELL;
@@ -511,13 +694,6 @@ convert_xml_to_value (struct ccase *c, const struct variable *var,
     }
 }
 
-static int
-xml_reader_for_zip_member (void *zm_, char *buffer, int len)
-{
-  struct zip_member *zm = zm_;
-  return zip_member_read (zm, buffer, len);
-}
-
 /* Try to find out how many sheets there are in the "workbook" */
 static int
 get_sheet_count (struct zip_reader *zreader)
@@ -557,6 +733,20 @@ get_sheet_count (struct zip_reader *zreader)
   return -1;
 }
 
+static int
+ods_get_sheet_n_sheets (struct spreadsheet *s)
+{
+  struct ods_reader *r = (struct ods_reader *) s;
+
+  if (r->n_sheets >= 0)
+    return r->n_sheets;
+
+  r->n_sheets = get_sheet_count (r->zreader);
+
+  return r->n_sheets;
+}
+
+
 static void
 ods_error_handler (void *ctx, const char *mesg,
                   xmlParserSeverities sev UNUSED,
@@ -572,82 +762,9 @@ ods_error_handler (void *ctx, const char *mesg,
 }
 
 
-static bool
-init_reader (struct ods_reader *r, bool report_errors,
-             struct state_data *state)
-{
-  struct zip_member *content = zip_member_open (r->zreader, "content.xml");
-  xmlTextReaderPtr xtr;
-
-  if (content == NULL)
-    return NULL;
-
-  xtr = xmlReaderForIO (xml_reader_for_zip_member, NULL, content, NULL, NULL,
-                       report_errors ? 0 : (XML_PARSE_NOERROR | XML_PARSE_NOWARNING));
-
-  if (xtr == NULL)
-      return false;
-
-  *state = (struct state_data) { .xtr = xtr,
-                                 .zm = content,
-                                 .state = STATE_INIT };
-
-  r->spreadsheet.type = SPREADSHEET_ODS;
-
-  if (report_errors)
-    xmlTextReaderSetErrorHandler (xtr, ods_error_handler, r);
-
-  return true;
-}
-
-
-
-struct spreadsheet *
-ods_probe (const char *filename, bool report_errors)
-{
-  int sheet_count;
-  struct ods_reader *r = xzalloc (sizeof *r);
-  struct zip_reader *zr;
-
-  ds_init_empty (&r->zip_errs);
-
-  zr = zip_reader_create (filename, &r->zip_errs);
-
-  if (zr == NULL)
-    {
-      if (report_errors)
-       {
-         msg (ME, _("Cannot open %s as a OpenDocument file: %s"),
-              filename, ds_cstr (&r->zip_errs));
-       }
-      ds_destroy (&r->zip_errs);
-      free (r);
-      return NULL;
-    }
-
-  sheet_count = get_sheet_count (zr);
-
-  r->zreader = zr;
-  r->spreadsheet.ref_cnt = 1;
-
-  if (!init_reader (r, report_errors, &r->msd))
-    goto error;
-
-  r->spreadsheet.n_sheets = sheet_count;
-  r->n_allocated_sheets = 0;
-  r->sheets = NULL;
+static bool init_reader (struct ods_reader *r, bool report_errors, struct state_data *state);
 
-  r->spreadsheet.file_name = strdup (filename);
-  return &r->spreadsheet;
-
- error:
-  ds_destroy (&r->zip_errs);
-  zip_reader_destroy (r->zreader);
-  free (r);
-  return NULL;
-}
-
-struct casereader *
+static struct casereader *
 ods_make_reader (struct spreadsheet *spreadsheet,
                 const struct spreadsheet_read_options *opts)
 {
@@ -663,21 +780,20 @@ ods_make_reader (struct spreadsheet *spreadsheet,
   xmlChar *val_string = NULL;
 
   assert (r);
-  r->read_names = opts->read_names;
   ds_init_empty (&r->ods_errs);
-  ++r->spreadsheet.ref_cnt;
+  r = (struct ods_reader *) spreadsheet_ref (SPREADSHEET_CAST (r));
 
   if (!init_reader (r, true, &r->rsd))
     goto error;
 
-  r->used_first_case = false;
-  r->first_case = NULL;
+  r->spreadsheet.used_first_case = false;
+  r->spreadsheet.first_case = NULL;
 
   if (opts->cell_range)
     {
       if (! convert_cell_ref (opts->cell_range,
-                              &r->start_col, &r->start_row,
-                              &r->stop_col, &r->stop_row))
+                              &r->spreadsheet.start_col, &r->spreadsheet.start_row,
+                              &r->spreadsheet.stop_col, &r->spreadsheet.stop_row))
        {
          msg (SE, _("Invalid cell range `%s'"),
               opts->cell_range);
@@ -686,10 +802,10 @@ ods_make_reader (struct spreadsheet *spreadsheet,
     }
   else
     {
-      r->start_col = 0;
-      r->start_row = 0;
-      r->stop_col = -1;
-      r->stop_row = -1;
+      r->spreadsheet.start_col = 0;
+      r->spreadsheet.start_row = 0;
+      r->spreadsheet.stop_col = -1;
+      r->spreadsheet.stop_row = -1;
     }
 
   r->target_sheet_name = xmlStrdup (BAD_CAST opts->sheet_name);
@@ -697,7 +813,7 @@ ods_make_reader (struct spreadsheet *spreadsheet,
 
   /* Advance to the start of the cells for the target sheet */
   while (! reading_target_sheet (r, &r->rsd)
-         || r->rsd.state != STATE_ROW || r->rsd.row <= r->start_row)
+         || r->rsd.state != STATE_ROW || r->rsd.row <= r->spreadsheet.start_row)
     {
       if (1 != (ret = xmlTextReaderRead (r->rsd.xtr)))
           break;
@@ -719,15 +835,15 @@ ods_make_reader (struct spreadsheet *spreadsheet,
          process_node (r, &r->rsd);
 
          /* If the row is finished then stop for now */
-         if (r->rsd.state == STATE_TABLE && r->rsd.row > r->start_row)
+         if (r->rsd.state == STATE_TABLE && r->rsd.row > r->spreadsheet.start_row)
            break;
 
-         int idx = r->rsd.col - r->start_col - 1;
+         int idx = r->rsd.col - r->spreadsheet.start_col - 1;
 
          if (idx < 0)
            continue;
 
-         if (r->stop_col != -1 && idx > r->stop_col - r->start_col)
+         if (r->spreadsheet.stop_col != -1 && idx > r->spreadsheet.stop_col - r->spreadsheet.start_col)
            continue;
 
          if (r->rsd.state == STATE_CELL_CONTENT
@@ -770,14 +886,14 @@ ods_make_reader (struct spreadsheet *spreadsheet,
 
       /* If the row is finished then stop for now */
       if (r->rsd.state == STATE_TABLE &&
-         r->rsd.row > r->start_row + (opts->read_names ? 1 : 0))
+         r->rsd.row > r->spreadsheet.start_row + (opts->read_names ? 1 : 0))
        break;
 
-      idx = r->rsd.col - r->start_col - 1;
+      idx = r->rsd.col - r->spreadsheet.start_col - 1;
       if (idx < 0)
        continue;
 
-      if (r->stop_col != -1 && idx > r->stop_col - r->start_col)
+      if (r->spreadsheet.stop_col != -1 && idx > r->spreadsheet.stop_col - r->spreadsheet.start_col)
        continue;
 
       if (r->rsd.state == STATE_CELL &&
@@ -812,19 +928,19 @@ ods_make_reader (struct spreadsheet *spreadsheet,
 
 
   /* Create the dictionary and populate it */
-  r->spreadsheet.dict = r->dict = dict_create (
+  r->spreadsheet.dict = dict_create (
     CHAR_CAST (const char *, xmlTextReaderConstEncoding (r->rsd.xtr)));
 
   for (i = 0; i < n_var_specs ; ++i)
     {
       struct fmt_spec fmt;
       struct variable *var = NULL;
-      char *name = dict_make_unique_var_name (r->dict, var_spec[i].name, &vstart);
+      char *name = dict_make_unique_var_name (r->spreadsheet.dict, var_spec[i].name, &vstart);
       int width  = xmv_to_width (&var_spec[i].firstval, opts->asw);
-      dict_create_var (r->dict, name, width);
+      dict_create_var (r->spreadsheet.dict, name, width);
       free (name);
 
-      var = dict_get_var (r->dict, i);
+      var = dict_get_var (r->spreadsheet.dict, i);
 
       if (0 == xmlStrcmp (var_spec[i].firstval.type, _xml("date")))
        {
@@ -846,15 +962,15 @@ ods_make_reader (struct spreadsheet *spreadsheet,
     }
 
   /* Create the first case, and cache it */
-  r->proto = caseproto_ref (dict_get_proto (r->dict));
-  r->first_case = case_create (r->proto);
-  case_set_missing (r->first_case);
+  r->spreadsheet.proto = caseproto_ref (dict_get_proto (r->spreadsheet.dict));
+  r->spreadsheet.first_case = case_create (r->spreadsheet.proto);
+  case_set_missing (r->spreadsheet.first_case);
 
   for (i = 0 ; i < n_var_specs; ++i)
     {
-      const struct variable *var = dict_get_var (r->dict, i);
+      const struct variable *var = dict_get_var (r->spreadsheet.dict, i);
 
-      convert_xml_to_value (r->first_case, var,  &var_spec[i].firstval,
+      convert_xml_to_value (r->spreadsheet.first_case, var,  &var_spec[i].firstval,
                            r->rsd.col - n_var_specs + i,
                            r->rsd.row - 1);
     }
@@ -882,7 +998,7 @@ ods_make_reader (struct spreadsheet *spreadsheet,
 
   return casereader_create_sequential
     (NULL,
-     r->proto,
+     r->spreadsheet.proto,
      n_cases,
      &ods_file_casereader_class, r);
 
@@ -915,10 +1031,10 @@ ods_file_casereader_read (struct casereader *reader UNUSED, void *r_)
   xmlChar *val_string = NULL;
   xmlChar *type = NULL;
 
-  if (!r->used_first_case)
+  if (!r->spreadsheet.used_first_case)
     {
-      r->used_first_case = true;
-      return r->first_case;
+      r->spreadsheet.used_first_case = true;
+      return r->spreadsheet.first_case;
     }
 
 
@@ -933,20 +1049,20 @@ ods_file_casereader_read (struct casereader *reader UNUSED, void *r_)
 
   if (! reading_target_sheet (r, &r->rsd)
        ||  r->rsd.state < STATE_TABLE
-       ||  (r->stop_row != -1 && r->rsd.row > r->stop_row + 1)
+       ||  (r->spreadsheet.stop_row != -1 && r->rsd.row > r->spreadsheet.stop_row + 1)
 )
     {
       return NULL;
     }
 
-  c = case_create (r->proto);
+  c = case_create (r->spreadsheet.proto);
   case_set_missing (c);
 
   while (1 == xmlTextReaderRead (r->rsd.xtr))
     {
       process_node (r, &r->rsd);
 
-      if (r->stop_row != -1 && r->rsd.row > r->stop_row + 1)
+      if (r->spreadsheet.stop_row != -1 && r->rsd.row > r->spreadsheet.stop_row + 1)
        break;
 
       if (r->rsd.state == STATE_CELL &&
@@ -970,16 +1086,16 @@ ods_file_casereader_read (struct casereader *reader UNUSED, void *r_)
          for (col = 0; col < r->rsd.col_span; ++col)
            {
              const struct variable *var;
-             const int idx = r->rsd.col - col - r->start_col - 1;
+             const int idx = r->rsd.col - col - r->spreadsheet.start_col - 1;
              if (idx < 0)
                continue;
-             if (r->stop_col != -1 && idx > r->stop_col - r->start_col)
+             if (r->spreadsheet.stop_col != -1 && idx > r->spreadsheet.stop_col - r->spreadsheet.start_col)
                break;
-             if (idx >= dict_get_var_cnt (r->dict))
+             if (idx >= dict_get_var_cnt (r->spreadsheet.dict))
                break;
 
-              var = dict_get_var (r->dict, idx);
-             convert_xml_to_value (c, var, xmv, idx + r->start_col, r->rsd.row - 1);
+              var = dict_get_var (r->spreadsheet.dict, idx);
+             convert_xml_to_value (c, var, xmv, idx + r->spreadsheet.start_col, r->rsd.row - 1);
            }
 
          xmlFree (xmv->text);
@@ -996,3 +1112,86 @@ ods_file_casereader_read (struct casereader *reader UNUSED, void *r_)
 
   return c;
 }
+
+static bool
+init_reader (struct ods_reader *r, bool report_errors,
+             struct state_data *state)
+{
+  struct spreadsheet *s = SPREADSHEET_CAST (r);
+
+  if (state)
+    {
+      struct zip_member *content = zip_member_open (r->zreader, "content.xml");
+      if (content == NULL)
+       return NULL;
+
+      xmlTextReaderPtr xtr = xmlReaderForIO (xml_reader_for_zip_member, NULL, content, NULL, NULL,
+                                            report_errors
+                                            ? 0
+                                            : (XML_PARSE_NOERROR | XML_PARSE_NOWARNING));
+
+      if (xtr == NULL)
+       return false;
+
+     *state = (struct state_data) { .xtr = xtr,
+                                   .zm = content,
+                                   .state = STATE_INIT };
+     if (report_errors)
+       xmlTextReaderSetErrorHandler (xtr, ods_error_handler, r);
+  }
+
+  strcpy (s->type, "ODS");
+  s->destroy = ods_destroy;
+  s->make_reader = ods_make_reader;
+  s->get_sheet_name = ods_get_sheet_name;
+  s->get_sheet_range = ods_get_sheet_range;
+  s->get_sheet_n_sheets = ods_get_sheet_n_sheets;
+  s->get_sheet_n_rows = ods_get_sheet_n_rows;
+  s->get_sheet_n_columns = ods_get_sheet_n_columns;
+  s->get_sheet_cell = ods_get_sheet_cell;
+
+  return true;
+}
+
+struct spreadsheet *
+ods_probe (const char *filename, bool report_errors)
+{
+  struct ods_reader *r = xzalloc (sizeof *r);
+  struct zip_reader *zr;
+
+  ds_init_empty (&r->zip_errs);
+
+  zr = zip_reader_create (filename, &r->zip_errs);
+
+  if (zr == NULL)
+    {
+      if (report_errors)
+       {
+         msg (ME, _("Cannot open %s as a OpenDocument file: %s"),
+              filename, ds_cstr (&r->zip_errs));
+       }
+      ds_destroy (&r->zip_errs);
+      free (r);
+      return NULL;
+    }
+
+  r->zreader = zr;
+  r->spreadsheet.ref_cnt = 1;
+  hmap_init (&r->cache);
+
+  if (!init_reader (r, report_errors, NULL))
+    goto error;
+
+  r->n_sheets = -1;
+  r->n_allocated_sheets = 0;
+  r->spreadsheet.sheets = NULL;
+
+  r->spreadsheet.file_name = strdup (filename);
+  return &r->spreadsheet;
+
+ error:
+  ds_destroy (&r->zip_errs);
+  zip_reader_destroy (r->zreader);
+  free (r);
+  return NULL;
+}
index 9602a310c1dd0889bebb95eaf7b5b556e3b4e712..8626020c92468c903560a544bca7bccd1664a970 100644 (file)
@@ -1,5 +1,5 @@
 /* PSPP - a program for statistical analysis.
-   Copyright (C) 2011 Free Software Foundation, Inc.
+   Copyright (C) 2011, 2020 Free Software Foundation, Inc.
 
    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
 
 #include <stdbool.h>
 
-struct casereader;
-struct dictionary;
-
-struct spreadsheet_read_options;
-struct spreadsheet;
-
-const char * ods_get_sheet_name (struct spreadsheet *s, int n);
-char * ods_get_sheet_range (struct spreadsheet *s, int n);
-
 struct spreadsheet *ods_probe (const char *filename, bool report_errors);
 
-struct casereader * ods_make_reader (struct spreadsheet *spreadsheet,
-                                    const struct spreadsheet_read_options *opts);
-
-void ods_unref (struct spreadsheet *s);
-
 
 #endif
index 9cd118c758673b93eb89687124f1b95d96954f01..a16b430a52367fd9a670516f04b8eb0e4457e9e0 100644 (file)
@@ -1,5 +1,5 @@
 /* PSPP - a program for statistical analysis.
-   Copyright (C) 2007, 2009, 2010, 2011, 2013 Free Software Foundation, Inc.
+   Copyright (C) 2007, 2009, 2010, 2011, 2013, 2020 Free Software Foundation, Inc.
 
    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
 #include <gl/c-xvasprintf.h>
 #include <stdlib.h>
 
-void
+struct spreadsheet *
 spreadsheet_ref (struct spreadsheet *s)
 {
   s->ref_cnt++;
+  return s;
 }
 
 void
 spreadsheet_unref (struct spreadsheet *s)
 {
-  switch (s->type)
-    {
-    case SPREADSHEET_ODS:
-      ods_unref (s);
-      break;
-
-    case SPREADSHEET_GNUMERIC:
-      gnumeric_unref (s);
-      break;
-    default:
-      NOT_REACHED ();
-      break;
-    }
+  if (--s->ref_cnt == 0)
+    s->destroy (s);
 }
 
 
@@ -58,38 +48,44 @@ struct casereader *
 spreadsheet_make_reader (struct spreadsheet *s,
                          const struct spreadsheet_read_options *opts)
 {
-  if (s->type == SPREADSHEET_ODS)
-    return ods_make_reader (s, opts);
-
-  if (s->type == SPREADSHEET_GNUMERIC)
-    return gnumeric_make_reader (s, opts);
-
-  return NULL;
+  return s->make_reader (s, opts);
 }
 
 const char *
 spreadsheet_get_sheet_name (struct spreadsheet *s, int n)
 {
-  if (s->type == SPREADSHEET_ODS)
-    return ods_get_sheet_name (s, n);
-
-  if (s->type == SPREADSHEET_GNUMERIC)
-    return gnumeric_get_sheet_name (s, n);
-
-  return NULL;
+  return s->get_sheet_name (s, n);
 }
 
 
 char *
 spreadsheet_get_sheet_range (struct spreadsheet *s, int n)
 {
-  if (s->type == SPREADSHEET_ODS)
-    return ods_get_sheet_range (s, n);
+  return s->get_sheet_range (s, n);
+}
 
-  if (s->type == SPREADSHEET_GNUMERIC)
-    return gnumeric_get_sheet_range (s, n);
+int
+spreadsheet_get_sheet_n_sheets (struct spreadsheet *s)
+{
+  return s->get_sheet_n_sheets (s);
+}
+
+unsigned int
+spreadsheet_get_sheet_n_rows (struct spreadsheet *s, int n)
+{
+  return s->get_sheet_n_rows (s, n);
+}
+
+unsigned int
+spreadsheet_get_sheet_n_columns (struct spreadsheet *s, int n)
+{
+  return s->get_sheet_n_columns (s, n);
+}
 
-  return NULL;
+char *
+spreadsheet_get_cell (struct spreadsheet *s, int n, int row, int column)
+{
+  return s->get_sheet_cell (s, n, row, column);
 }
 
 
@@ -114,6 +110,7 @@ reverse (char *s, int len)
    greater than 1 are implicitly incremented by 1, so
    AA  = 0 + 1*26, AB = 1 + 1*26,
    ABC = 2 + 2*26 + 1*26^2 ....
+   On error, this function returns -1
 */
 int
 ps26_to_int (const char *str)
@@ -125,7 +122,10 @@ ps26_to_int (const char *str)
 
   for (i = len - 1 ; i >= 0; --i)
     {
-      int mantissa = (str[i] - 'A');
+      char c = str[i];
+      if (c < 'A' || c > 'Z')
+       return -1;
+      int mantissa = (c - 'A');
 
       assert (mantissa >= 0);
       assert (mantissa < RADIX);
@@ -140,6 +140,9 @@ ps26_to_int (const char *str)
   return result;
 }
 
+/* Convert an integer, which must be non-negative,
+   to pseudo base 26.
+   The caller must free the return value when no longer required.  */
 char *
 int_to_ps26 (int i)
 {
@@ -149,7 +152,8 @@ int_to_ps26 (int i)
   long long int base = RADIX;
   int exp = 1;
 
-  assert (i >= 0);
+  if (i < 0)
+    return NULL;
 
   while (i > lower + base - 1)
     {
@@ -188,7 +192,7 @@ create_cell_ref (int col0, int row0)
   if (col0 < 0) return NULL;
   if (row0 < 0) return NULL;
 
-  cs0 =  int_to_ps26 (col0);
+  cs0 = int_to_ps26 (col0);
   s =  c_xasprintf ("%s%d", cs0, row0 + 1);
 
   free (cs0);
@@ -241,4 +245,3 @@ convert_cell_ref (const char *ref,
 
   return true;
 }
-
index efba6f369fbf3577511aa01f422c64d241893db4..d7783bab7fdf12eb88ae6697e6c12ffd987370d5 100644 (file)
@@ -51,28 +51,56 @@ bool convert_cell_ref (const char *ref,
 
 #define _xmlchar_to_int(X) ((X) ? atoi (CHAR_CAST (const char *, (X))) : -1)
 
-enum spreadsheet_type
-  {
-    SPREADSHEET_NONE,
-    SPREADSHEET_GNUMERIC,
-    SPREADSHEET_ODS
-  };
-
+struct sheet_detail
+{
+  /* The name of the sheet (utf8 encoding) */
+  char *name;
+
+  /* The extents of the sheet.  */
+  int first_col;
+  int last_col;
+  int first_row;
+  int last_row;
+};
 
 struct spreadsheet
 {
+  /** General spreadsheet object related things.  */
+  int ref_cnt;
+
+  /* A 3 letter string (null terminated) which identifies the type of
+     spreadsheet (eg: "ODS" for opendocument; "GNM" for gnumeric etc).  */
+  char type[4];
+
+  void (*destroy) (struct spreadsheet *);
+  struct casereader* (*make_reader) (struct spreadsheet *,
+                                   const struct spreadsheet_read_options *);
+  const char * (*get_sheet_name) (struct spreadsheet *, int);
+  char * (*get_sheet_range) (struct spreadsheet *, int);
+  int (*get_sheet_n_sheets) (struct spreadsheet *);
+  unsigned int (*get_sheet_n_rows) (struct spreadsheet *, int);
+  unsigned int (*get_sheet_n_columns) (struct spreadsheet *, int);
+  char * (*get_sheet_cell) (struct spreadsheet *, int , int , int);
+
   char *file_name;
 
-  enum spreadsheet_type type;
+  struct sheet_detail *sheets;
 
-  /* The total number of sheets in the "workbook" */
-  int n_sheets;
+
+  /** Things specific to casereaders.  */
 
   /* The dictionary for client's reference.
      Client must ref or clone it if it needs a permanent or modifiable copy. */
   struct dictionary *dict;
-
-  int ref_cnt;
+  struct caseproto *proto;
+  struct ccase *first_case;
+  bool used_first_case;
+
+  /* Where the reader should start and stop.  */
+  int start_row;
+  int start_col;
+  int stop_row;
+  int stop_col;
 };
 
 
@@ -80,16 +108,17 @@ struct casereader * spreadsheet_make_reader (struct spreadsheet *, const struct
 
 const char * spreadsheet_get_sheet_name (struct spreadsheet *s, int n) OPTIMIZE(2);
 char * spreadsheet_get_sheet_range (struct spreadsheet *s, int n) OPTIMIZE(2);
+int  spreadsheet_get_sheet_n_sheets (struct spreadsheet *s) OPTIMIZE(2);
+unsigned int  spreadsheet_get_sheet_n_rows (struct spreadsheet *s, int n) OPTIMIZE(2);
+unsigned int  spreadsheet_get_sheet_n_columns (struct spreadsheet *s, int n) OPTIMIZE(2);
 
+char * spreadsheet_get_cell (struct spreadsheet *s, int n, int row, int column);
 
 char * create_cell_ref (int col0, int row0);
 char *create_cell_range (int col0, int row0, int coli, int rowi);
 
+struct spreadsheet * spreadsheet_ref (struct spreadsheet *s) WARN_UNUSED_RESULT;
 void spreadsheet_unref (struct spreadsheet *);
-void spreadsheet_ref (struct spreadsheet *);
-
-
-
 
 
 #define SPREADSHEET_CAST(X) ((struct spreadsheet *)(X))
index fec10f5538689ae438dd4a3ceae04f22d1598526..f169878887f624cb61e7400861c7875cfa1600c4 100644 (file)
@@ -56,6 +56,7 @@ UI_FILES = \
        src/ui/gui/roc.ui \
        src/ui/gui/scatterplot.ui \
        src/ui/gui/select-cases.ui \
+       src/ui/gui/spreadsheet-import.ui \
        src/ui/gui/t-test.ui \
        src/ui/gui/text-data-import.ui \
        src/ui/gui/transpose.ui \
@@ -175,6 +176,10 @@ src_ui_gui_psppire_SOURCES = \
        src/ui/gui/psppire.h \
        src/ui/gui/psppire-import-assistant.c \
        src/ui/gui/psppire-import-assistant.h \
+       src/ui/gui/psppire-import-spreadsheet.c \
+       src/ui/gui/psppire-import-spreadsheet.h \
+       src/ui/gui/psppire-import-textfile.c \
+       src/ui/gui/psppire-import-textfile.h \
        src/ui/gui/psppire-lex-reader.c \
        src/ui/gui/psppire-lex-reader.h \
        src/ui/gui/psppire-output-view.c \
@@ -183,6 +188,8 @@ src_ui_gui_psppire_SOURCES = \
        src/ui/gui/psppire-output-window.h \
        src/ui/gui/psppire-scanf.c \
        src/ui/gui/psppire-scanf.h \
+       src/ui/gui/psppire-spreadsheet-data-model.c \
+       src/ui/gui/psppire-spreadsheet-data-model.h \
        src/ui/gui/psppire-spreadsheet-model.c \
        src/ui/gui/psppire-spreadsheet-model.h \
        src/ui/gui/psppire-syntax-window.c \
index a968431d865bf043759adbdafc8368c5c504cfa4..47744909db9704640e12fcf2bb603e7cecce9223 100644 (file)
    along with this program.  If not, see <http://www.gnu.org/licenses/>. */
 
 #include <config.h>
+#include "psppire-import-assistant.h"
 
 #include <gtk/gtk.h>
 
 #include "data/casereader.h"
 #include "data/data-in.h"
-#include "data/data-out.h"
-#include "data/dictionary.h"
 #include "data/format-guesser.h"
-#include "data/format.h"
 #include "data/gnumeric-reader.h"
 #include "data/ods-reader.h"
-#include "data/spreadsheet-reader.h"
 #include "data/value-labels.h"
 #include "data/casereader-provider.h"
 
 #include "libpspp/i18n.h"
-#include "libpspp/line-reader.h"
-#include "libpspp/message.h"
-#include "libpspp/hmap.h"
-#include "libpspp/hash-functions.h"
-#include "libpspp/str.h"
 
 #include "builder-wrapper.h"
 
 #include "psppire-data-sheet.h"
 #include "psppire-data-store.h"
 #include "psppire-dialog.h"
-#include "psppire-delimited-text.h"
-#include "psppire-dict.h"
 #include "psppire-encoding-selector.h"
-#include "psppire-import-assistant.h"
-#include "psppire-scanf.h"
-#include "psppire-spreadsheet-model.h"
-#include "psppire-text-file.h"
 #include "psppire-variable-sheet.h"
 
+#include "psppire-import-spreadsheet.h"
+#include "psppire-import-textfile.h"
+
 #include "ui/syntax-gen.h"
 
 #include <gettext.h>
 #define _(msgid) gettext (msgid)
 #define N_(msgid) msgid
 
-enum { MAX_LINE_LEN = 16384 }; /* Max length of an acceptable line. */
+typedef void page_func (PsppireImportAssistant *, GtkWidget *page, enum IMPORT_ASSISTANT_DIRECTION dir);
 
 
-/* Chooses a name for each column on the separators page */
-static void choose_column_names (PsppireImportAssistant *ia);
-
-static void intro_page_create (PsppireImportAssistant *ia);
-static void first_line_page_create (PsppireImportAssistant *ia);
-
-static void separators_page_create (PsppireImportAssistant *ia);
 static void formats_page_create (PsppireImportAssistant *ia);
 
 static void psppire_import_assistant_init            (PsppireImportAssistant      *act);
@@ -130,7 +112,8 @@ psppire_import_assistant_finalize (GObject *object)
   dict_unref (ia->dict);
   dict_unref (ia->casereader_dict);
 
-  g_object_unref (ia->builder);
+  g_object_unref (ia->text_builder);
+  g_object_unref (ia->spread_builder);
 
   ia->response = -1;
   g_main_loop_unref (ia->main_loop);
@@ -174,249 +157,19 @@ on_paste (GtkButton *button, PsppireImportAssistant *ia)
 }
 
 
-/* Revises the contents of the fields tree view based on the
-   currently chosen set of separators. */
-static void
-revise_fields_preview (PsppireImportAssistant *ia)
-{
-  choose_column_names (ia);
-}
-
-
-struct separator
-{
-  const char *name;           /* Name (for use with get_widget_assert). */
-  gunichar c;                 /* Separator character. */
-};
-
-/* All the separators in the dialog box. */
-static const struct separator separators[] =
-  {
-    {"space",     ' '},
-    {"tab",       '\t'},
-    {"bang",      '!'},
-    {"colon",     ':'},
-    {"comma",     ','},
-    {"hyphen",    '-'},
-    {"pipe",      '|'},
-    {"semicolon", ';'},
-    {"slash",     '/'},
-  };
-
-#define SEPARATOR_CNT (sizeof separators / sizeof *separators)
-
-struct separator_count_node
-{
-  struct hmap_node node;
-  int occurance; /* The number of times the separator occurs in a line */
-  int quantity;  /* The number of lines with this occurance */
-};
-
-
-/* Picks the most likely separator and quote characters based on
-   IA's file data. */
-static void
-choose_likely_separators (PsppireImportAssistant *ia)
-{
-  gint first_line = 0;
-  g_object_get (ia->delimiters_model, "first-line", &first_line, NULL);
-
-  gboolean valid;
-  GtkTreeIter iter;
-  int j;
-
-  struct hmap count_map[SEPARATOR_CNT];
-  for (j = 0; j < SEPARATOR_CNT; ++j)
-    hmap_init (count_map + j);
-
-  GtkTreePath *p = gtk_tree_path_new_from_indices (first_line, -1);
-
-  for (valid = gtk_tree_model_get_iter (GTK_TREE_MODEL (ia->text_file), &iter, p);
-       valid;
-       valid = gtk_tree_model_iter_next (GTK_TREE_MODEL (ia->text_file), &iter))
-    {
-      gchar *line_text = NULL;
-      gtk_tree_model_get (GTK_TREE_MODEL (ia->text_file), &iter, 1, &line_text, -1);
-
-      gint *counts = xzalloc (sizeof *counts * SEPARATOR_CNT);
-
-      struct substring cs = ss_cstr (line_text);
-      for (;
-          UINT32_MAX != ss_first_mb (cs);
-          ss_get_mb (&cs))
-       {
-         ucs4_t character = ss_first_mb (cs);
-
-         int s;
-         for (s = 0; s < SEPARATOR_CNT; ++s)
-           {
-             if (character == separators[s].c)
-               counts[s]++;
-           }
-       }
-
-      int j;
-      for (j = 0; j < SEPARATOR_CNT; ++j)
-       {
-         if (counts[j] > 0)
-           {
-             struct separator_count_node *cn = NULL;
-             unsigned int hash = hash_int (counts[j], 0);
-             HMAP_FOR_EACH_WITH_HASH (cn, struct separator_count_node, node, hash, &count_map[j])
-               {
-                 if (cn->occurance == counts[j])
-                   break;
-               }
-
-             if (cn == NULL)
-               {
-                 struct separator_count_node *new_cn = xzalloc (sizeof *new_cn);
-                 new_cn->occurance = counts[j];
-                 new_cn->quantity = 1;
-                 hmap_insert (&count_map[j], &new_cn->node, hash);
-               }
-             else
-               cn->quantity++;
-           }
-       }
-
-      free (line_text);
-      free (counts);
-    }
-  gtk_tree_path_free (p);
-
-  if (hmap_count (count_map) > 0)
-    {
-      int most_frequent = -1;
-      int largest = 0;
-      for (j = 0; j < SEPARATOR_CNT; ++j)
-        {
-          struct separator_count_node *cn;
-          HMAP_FOR_EACH (cn, struct separator_count_node, node, &count_map[j])
-            {
-              if (largest < cn->quantity)
-                {
-                  largest = cn->quantity;
-                  most_frequent = j;
-                }
-            }
-          hmap_destroy (&count_map[j]);
-        }
-
-      g_return_if_fail (most_frequent >= 0);
-
-      GtkWidget *toggle =
-        get_widget_assert (ia->builder, separators[most_frequent].name);
-      gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (toggle), TRUE);
-    }
-}
-
-static void
-repopulate_delimiter_columns (PsppireImportAssistant *ia)
-{
-  /* Remove all the columns */
-  while (gtk_tree_view_get_n_columns (GTK_TREE_VIEW (ia->fields_tree_view)) > 0)
-    {
-      GtkTreeViewColumn *tvc = gtk_tree_view_get_column (GTK_TREE_VIEW (ia->fields_tree_view), 0);
-      gtk_tree_view_remove_column (GTK_TREE_VIEW (ia->fields_tree_view), tvc);
-    }
-
-  gint n_fields =
-    gtk_tree_model_get_n_columns (GTK_TREE_MODEL (ia->delimiters_model));
-
-  /* ... and put them back again. */
-  gint f;
-  for (f = gtk_tree_view_get_n_columns (GTK_TREE_VIEW (ia->fields_tree_view));
-       f < n_fields; f++)
-    {
-      GtkCellRenderer *renderer = gtk_cell_renderer_text_new ();
-
-      const gchar *title = NULL;
-
-      if (f == 0)
-       title = _("line");
-      else
-       {
-         if (gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (ia->variable_names_cb)))
-           {
-             title =
-               psppire_delimited_text_get_header_title
-               (PSPPIRE_DELIMITED_TEXT (ia->delimiters_model), f - 1);
-           }
-         if (title == NULL)
-           title = _("var");
-       }
-
-      GtkTreeViewColumn *column =
-       gtk_tree_view_column_new_with_attributes (title,
-                                                 renderer,
-                                                 "text", f,
-                                                 NULL);
-      g_object_set (column,
-                   "resizable", TRUE,
-                   "sizing", GTK_TREE_VIEW_COLUMN_AUTOSIZE,
-                   NULL);
-
-      gtk_tree_view_append_column (GTK_TREE_VIEW (ia->fields_tree_view), column);
-    }
-}
-
-static void
-reset_tree_view_model (PsppireImportAssistant *ia)
-{
-  GtkTreeModel *tm = gtk_tree_view_get_model (GTK_TREE_VIEW (ia->fields_tree_view));
-  g_object_ref (tm);
-  gtk_tree_view_set_model (GTK_TREE_VIEW (ia->fields_tree_view), NULL);
-
-
-  repopulate_delimiter_columns (ia);
-
-  gtk_tree_view_set_model (GTK_TREE_VIEW (ia->fields_tree_view), tm);
-  //  gtk_tree_view_columns_autosize (GTK_TREE_VIEW (ia->fields_tree_view));
-
-  g_object_unref (tm);
-}
-
-/* Called just before the separators page becomes visible in the
-   assistant, and when the Reset button is clicked. */
-static void
-prepare_separators_page (PsppireImportAssistant *ia, GtkWidget *page)
-{
-  gtk_tree_view_set_model (GTK_TREE_VIEW (ia->fields_tree_view),
-                          GTK_TREE_MODEL (ia->delimiters_model));
-
-  g_signal_connect_swapped (GTK_TREE_MODEL (ia->delimiters_model), "notify::delimiters",
-                       G_CALLBACK (reset_tree_view_model), ia);
-
-
-  repopulate_delimiter_columns (ia);
-
-  revise_fields_preview (ia);
-  choose_likely_separators (ia);
-}
-
-/* Resets IA's intro page to its initial state. */
-static void
-reset_intro_page (PsppireImportAssistant *ia)
-{
-  gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (ia->all_cases_button),
-                                TRUE);
-}
-
-
-
-/* Clears the set of user-modified variables from IA's formats
-   substructure.  This discards user modifications to variable
-   formats, thereby causing formats to revert to their
-   defaults.  */
-static void
-reset_formats_page (PsppireImportAssistant *ia, GtkWidget *page)
-{
-}
+/* /\* Clears the set of user-modified variables from IA's formats */
+/*    substructure.  This discards user modifications to variable */
+/*    formats, thereby causing formats to revert to their */
+/*    defaults.  *\/ */
+/* static void */
+/* reset_formats_page (PsppireImportAssistant *ia, GtkWidget *page) */
+/* { */
+/* } */
 
 static void prepare_formats_page (PsppireImportAssistant *ia);
 
-/* Called when the Reset button is clicked. */
+/* Called when the Reset button is clicked.
+   This function marshalls the callback to the relevant page.  */
 static void
 on_reset (GtkButton *button, PsppireImportAssistant *ia)
 {
@@ -424,10 +177,10 @@ on_reset (GtkButton *button, PsppireImportAssistant *ia)
   {
     GtkWidget *page =  gtk_assistant_get_nth_page (GTK_ASSISTANT (ia), pn);
 
-    page_func *on_reset = g_object_get_data (G_OBJECT (page), "on-reset");
+    page_func *xon_reset = g_object_get_data (G_OBJECT (page), "on-reset");
 
-    if (on_reset)
-      on_reset (ia, page);
+    if (xon_reset)
+      xon_reset (ia, page, 0);
   }
 }
 
@@ -449,38 +202,25 @@ on_prepare (GtkAssistant *assistant, GtkWidget *page, PsppireImportAssistant *ia
   gtk_widget_hide (ia->paste_button);
 
   gint pn = gtk_assistant_get_current_page (assistant);
-  gint previous_page_index = ia->current_page;
+  gint previous_page_index = ia->previous_page;
+  g_assert (pn != previous_page_index);
 
   if (previous_page_index >= 0)
     {
       GtkWidget *closing_page = gtk_assistant_get_nth_page (GTK_ASSISTANT (ia), previous_page_index);
 
-      if (pn > previous_page_index)
-       {
-         page_func *on_forward = g_object_get_data (G_OBJECT (closing_page), "on-forward");
-
-         if (on_forward)
-           on_forward (ia, closing_page);
-       }
-      else
-       {
-         page_func *on_back = g_object_get_data (G_OBJECT (closing_page), "on-back");
-
-         if (on_back)
-           on_back (ia, closing_page);
-       }
+        page_func *on_leaving = g_object_get_data (G_OBJECT (closing_page), "on-leaving");
+        if (on_leaving)
+          on_leaving (ia, closing_page, (pn > previous_page_index) ? IMPORT_ASSISTANT_FORWARDS : IMPORT_ASSISTANT_BACKWARDS);
     }
 
-  {
     GtkWidget *new_page = gtk_assistant_get_nth_page (GTK_ASSISTANT (ia), pn);
 
     page_func *on_entering = g_object_get_data (G_OBJECT (new_page), "on-entering");
-
     if (on_entering)
-      on_entering (ia, new_page);
-  }
+      on_entering (ia, new_page, (pn > previous_page_index) ? IMPORT_ASSISTANT_FORWARDS : IMPORT_ASSISTANT_BACKWARDS);
 
-  ia->current_page = pn;
+  ia->previous_page = pn;
 }
 
 /* Called when the Cancel button in the assistant is clicked. */
@@ -499,69 +239,6 @@ on_close (GtkAssistant *assistant, PsppireImportAssistant *ia)
 }
 
 
-static GtkWidget *
-add_page_to_assistant (PsppireImportAssistant *ia,
-                      GtkWidget *page, GtkAssistantPageType type, const gchar *);
-
-
-static void
-on_sheet_combo_changed (GtkComboBox *cb, PsppireImportAssistant *ia)
-{
-  GtkTreeIter iter;
-  gchar *range = NULL;
-  GtkTreeModel *model = gtk_combo_box_get_model (cb);
-  GtkBuilder *builder = ia->builder;
-  GtkWidget *range_entry = get_widget_assert (builder, "cell-range-entry");
-
-  gtk_combo_box_get_active_iter (cb, &iter);
-  gtk_tree_model_get (model, &iter, PSPPIRE_SPREADSHEET_MODEL_COL_RANGE, &range, -1);
-  gtk_entry_set_text (GTK_ENTRY (range_entry), range ?  range : "");
-  g_free (range);
-}
-
-/* Prepares IA's sheet_spec page. */
-static void
-prepare_sheet_spec_page (PsppireImportAssistant *ia)
-{
-  GtkBuilder *builder = ia->builder;
-  GtkWidget *sheet_entry = get_widget_assert (builder, "sheet-entry");
-  GtkWidget *readnames_checkbox = get_widget_assert (builder, "readnames-checkbox");
-
-  GtkTreeModel *model = psppire_spreadsheet_model_new (ia->spreadsheet);
-  gtk_combo_box_set_model (GTK_COMBO_BOX (sheet_entry), model);
-
-  gint items = gtk_tree_model_iter_n_children (model, NULL);
-  gtk_widget_set_sensitive (sheet_entry, items > 1);
-
-  gtk_combo_box_set_active (GTK_COMBO_BOX (sheet_entry), 0);
-
-  gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (readnames_checkbox), FALSE);
-}
-
-
-/* Initializes IA's sheet_spec substructure. */
-static void
-sheet_spec_page_create (PsppireImportAssistant *ia)
-{
-  GtkBuilder *builder = ia->builder;
-  GtkWidget *page = get_widget_assert (builder, "Spreadsheet-Importer");
-
-  GtkWidget *combo_box = get_widget_assert (builder, "sheet-entry");
-  GtkCellRenderer *renderer = gtk_cell_renderer_text_new ();
-  gtk_cell_layout_clear (GTK_CELL_LAYOUT (combo_box));
-  gtk_cell_layout_pack_start (GTK_CELL_LAYOUT (combo_box), renderer, TRUE);
-  gtk_cell_layout_set_attributes (GTK_CELL_LAYOUT (combo_box), renderer,
-                                 "text", 0,
-                                 NULL);
-
-  g_signal_connect (combo_box, "changed", G_CALLBACK (on_sheet_combo_changed), ia);
-
-  add_page_to_assistant (ia, page,
-                        GTK_ASSISTANT_PAGE_CONTENT, _("Importing Spreadsheet Data"));
-
-  g_object_set_data (G_OBJECT (page), "on-entering", prepare_sheet_spec_page);
-}
-
 static void
 on_chosen (PsppireImportAssistant *ia, GtkWidget *page)
 {
@@ -621,26 +298,39 @@ on_map (PsppireImportAssistant *ia, GtkWidget *page)
 
 
 static void
-chooser_page_enter (PsppireImportAssistant *ia, GtkWidget *page)
+chooser_page_enter (PsppireImportAssistant *ia, GtkWidget *page, enum IMPORT_ASSISTANT_DIRECTION dir)
 {
 }
 
 static void
-chooser_page_leave (PsppireImportAssistant *ia, GtkWidget *page)
+chooser_page_leave (PsppireImportAssistant *ia, GtkWidget *page, enum IMPORT_ASSISTANT_DIRECTION dir)
 {
+  if (dir != IMPORT_ASSISTANT_FORWARDS)
+    return;
+
+  GtkFileChooser *fc = GTK_FILE_CHOOSER (page);
+
   g_free (ia->file_name);
-  ia->file_name = gtk_file_chooser_get_filename (GTK_FILE_CHOOSER (page));
-  gchar *encoding = psppire_encoding_selector_get_encoding (ia->encoding_selector);
+  ia->file_name = gtk_file_chooser_get_filename (fc);
+
+  /* Add the chosen file to the recent manager.  */
+  {
+    gchar *uri = gtk_file_chooser_get_uri (fc);
+    GtkRecentManager * manager = gtk_recent_manager_get_default ();
+    gtk_recent_manager_add_item (manager, uri);
+    g_free (uri);
+  }
 
   if (!ia->spreadsheet)
     {
+      g_print ("%s:%d Where does this belong?\n", __FILE__, __LINE__);
+      gchar *encoding = psppire_encoding_selector_get_encoding (ia->encoding_selector);
       ia->text_file = psppire_text_file_new (ia->file_name, encoding);
       gtk_tree_view_set_model (GTK_TREE_VIEW (ia->first_line_tree_view),
                               GTK_TREE_MODEL (ia->text_file));
-    }
-
 
-  g_free (encoding);
+      g_free (encoding);
+    }
 }
 
 static void
@@ -670,7 +360,7 @@ chooser_page_create (PsppireImportAssistant *ia)
 
   g_signal_connect (chooser, "file-activated", G_CALLBACK (on_file_activated), ia);
 
-  g_object_set_data (G_OBJECT (chooser), "on-forward", chooser_page_leave);
+  g_object_set_data (G_OBJECT (chooser), "on-leaving", chooser_page_leave);
   g_object_set_data (G_OBJECT (chooser), "on-reset",   chooser_page_reset);
   g_object_set_data (G_OBJECT (chooser), "on-entering",chooser_page_enter);
 
@@ -741,12 +431,14 @@ chooser_page_create (PsppireImportAssistant *ia)
 static void
 psppire_import_assistant_init (PsppireImportAssistant *ia)
 {
-  ia->builder = builder_new ("text-data-import.ui");
+  ia->text_builder = builder_new ("text-data-import.ui");
+  ia->spread_builder = builder_new ("spreadsheet-import.ui");
 
-  ia->current_page = -1 ;
+  ia->previous_page = -1 ;
   ia->file_name = NULL;
 
   ia->spreadsheet = NULL;
+  ia->updating_selection = FALSE;
   ia->dict = NULL;
   ia->casereader_dict = NULL;
 
@@ -782,7 +474,7 @@ psppire_import_assistant_init (PsppireImportAssistant *ia)
 /* Appends a page of the given TYPE, with PAGE as its content, to
    the GtkAssistant encapsulated by IA.  Returns the GtkWidget
    that represents the page. */
-static GtkWidget *
+GtkWidget *
 add_page_to_assistant (PsppireImportAssistant *ia,
                       GtkWidget *page, GtkAssistantPageType type, const gchar *title)
 {
@@ -797,237 +489,6 @@ add_page_to_assistant (PsppireImportAssistant *ia,
 }
 
 
-/* Called when one of the radio buttons is clicked. */
-static void
-on_intro_amount_changed (PsppireImportAssistant *p)
-{
-  gtk_widget_set_sensitive (p->n_cases_spin,
-                           gtk_toggle_button_get_active
-                           (GTK_TOGGLE_BUTTON (p->n_cases_button)));
-
-  gtk_widget_set_sensitive (p->percent_spin,
-                           gtk_toggle_button_get_active
-                           (GTK_TOGGLE_BUTTON (p->percent_button)));
-}
-
-static void
-on_treeview_selection_change (PsppireImportAssistant *ia)
-{
-  GtkTreeSelection *selection =
-    gtk_tree_view_get_selection (GTK_TREE_VIEW (ia->first_line_tree_view));
-  GtkTreeModel *model = NULL;
-  GtkTreeIter iter;
-  if (gtk_tree_selection_get_selected (selection, &model, &iter))
-    {
-      gint max_lines;
-      int n;
-      GtkTreePath *path = gtk_tree_model_get_path (model, &iter);
-      gint *index = gtk_tree_path_get_indices (path);
-      n = *index;
-      gtk_tree_path_free (path);
-      g_object_get (model, "maximum-lines", &max_lines, NULL);
-      gtk_widget_set_sensitive (ia->variable_names_cb,
-                               (n > 0 && n < max_lines));
-      ia->delimiters_model =
-       psppire_delimited_text_new (GTK_TREE_MODEL (ia->text_file));
-      g_object_set (ia->delimiters_model, "first-line", n, NULL);
-    }
-}
-
-static void
-render_text_preview_line (GtkTreeViewColumn *tree_column,
-               GtkCellRenderer *cell,
-               GtkTreeModel *tree_model,
-               GtkTreeIter *iter,
-               gpointer data)
-{
-  /*
-     Set the text  to a "insensitive" state if the row
-     is greater than what the user declared to be the maximum.
-  */
-  GtkTreePath *path = gtk_tree_model_get_path (tree_model, iter);
-  gint *ii = gtk_tree_path_get_indices (path);
-  gint max_lines;
-  g_object_get (tree_model, "maximum-lines", &max_lines, NULL);
-  g_object_set (cell, "sensitive", (*ii < max_lines), NULL);
-  gtk_tree_path_free (path);
-}
-
-/* Initializes IA's first_line substructure. */
-static void
-first_line_page_create (PsppireImportAssistant *ia)
-{
-  GtkWidget *w =  get_widget_assert (ia->builder, "FirstLine");
-
-  g_object_set_data (G_OBJECT (w), "on-entering", on_treeview_selection_change);
-
-  add_page_to_assistant (ia, w,
-                        GTK_ASSISTANT_PAGE_CONTENT, _("Select the First Line"));
-
-  GtkWidget *scrolled_window = get_widget_assert (ia->builder, "first-line-scroller");
-
-  if (ia->first_line_tree_view == NULL)
-    {
-      ia->first_line_tree_view = gtk_tree_view_new ();
-      g_object_set (ia->first_line_tree_view, "enable-search", FALSE, NULL);
-
-      gtk_tree_view_set_headers_visible (GTK_TREE_VIEW (ia->first_line_tree_view), TRUE);
-
-      GtkCellRenderer *renderer = gtk_cell_renderer_text_new ();
-      GtkTreeViewColumn *column = gtk_tree_view_column_new_with_attributes (_("Line"), renderer,
-                                                                           "text", 0,
-                                                                           NULL);
-
-      gtk_tree_view_column_set_cell_data_func (column, renderer, render_text_preview_line, ia, 0);
-      gtk_tree_view_append_column (GTK_TREE_VIEW (ia->first_line_tree_view), column);
-
-      renderer = gtk_cell_renderer_text_new ();
-      column = gtk_tree_view_column_new_with_attributes (_("Text"), renderer, "text", 1, NULL);
-      gtk_tree_view_column_set_cell_data_func (column, renderer, render_text_preview_line, ia, 0);
-
-      gtk_tree_view_append_column (GTK_TREE_VIEW (ia->first_line_tree_view), column);
-
-      g_signal_connect_swapped (ia->first_line_tree_view, "cursor-changed",
-                               G_CALLBACK (on_treeview_selection_change), ia);
-      gtk_container_add (GTK_CONTAINER (scrolled_window), ia->first_line_tree_view);
-    }
-
-  gtk_widget_show_all (scrolled_window);
-
-  ia->variable_names_cb = get_widget_assert (ia->builder, "variable-names");
-}
-
-static void
-intro_on_leave (PsppireImportAssistant *ia)
-{
-  gint lc = 0;
-  g_object_get (ia->text_file, "line-count", &lc, NULL);
-  if (gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (ia->n_cases_button)))
-    {
-      gint max_lines = gtk_spin_button_get_value_as_int (GTK_SPIN_BUTTON (ia->n_cases_spin));
-      g_object_set (ia->text_file, "maximum-lines", max_lines, NULL);
-    }
-  else if (gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (ia->percent_button)))
-    {
-      gdouble percent = gtk_spin_button_get_value (GTK_SPIN_BUTTON (ia->percent_spin));
-      g_object_set (ia->text_file, "maximum-lines", (gint) (lc * percent / 100.0), NULL);
-    }
-  else
-    {
-      g_object_set (ia->text_file, "maximum-lines", lc, NULL);
-    }
-}
-
-
-static void
-intro_on_enter (PsppireImportAssistant *ia)
-{
-  GtkBuilder *builder = ia->builder;
-  GtkWidget *table  = get_widget_assert (builder, "button-table");
-
-  struct string s;
-
-  ds_init_empty (&s);
-  ds_put_cstr (&s, _("This assistant will guide you through the process of "
-                     "importing data into PSPP from a text file with one line "
-                     "per case,  in which fields are separated by tabs, "
-                     "commas, or other delimiters.\n\n"));
-
-  if (ia->text_file)
-    {
-      if (ia->text_file->total_is_exact)
-       {
-         ds_put_format (
-                        &s, ngettext ("The selected file contains %'lu line of text.  ",
-                                      "The selected file contains %'lu lines of text.  ",
-                                      ia->text_file->total_lines),
-                        ia->text_file->total_lines);
-       }
-      else if (ia->text_file->total_lines > 0)
-       {
-         ds_put_format (
-                        &s, ngettext (
-                                      "The selected file contains approximately %'lu line of text.  ",
-                                      "The selected file contains approximately %'lu lines of text.  ",
-                                      ia->text_file->total_lines),
-                        ia->text_file->total_lines);
-         ds_put_format (
-                        &s, ngettext (
-                                      "Only the first %zu line of the file will be shown for "
-                                      "preview purposes in the following screens.  ",
-                                      "Only the first %zu lines of the file will be shown for "
-                                      "preview purposes in the following screens.  ",
-                                      ia->text_file->line_cnt),
-                        ia->text_file->line_cnt);
-       }
-    }
-
-  ds_put_cstr (&s, _("You may choose below how much of the file should "
-                     "actually be imported."));
-
-  gtk_label_set_text (GTK_LABEL (get_widget_assert (builder, "intro-label")),
-                      ds_cstr (&s));
-  ds_destroy (&s);
-
-  if (gtk_grid_get_child_at (GTK_GRID (table), 1, 1) == NULL)
-    {
-      GtkWidget *hbox_n_cases = psppire_scanf_new (_("Only the first %4d cases"), &ia->n_cases_spin);
-      gtk_grid_attach (GTK_GRID (table), hbox_n_cases,
-                      1, 1,
-                      1, 1);
-    }
-
-  GtkAdjustment *adj = gtk_spin_button_get_adjustment (GTK_SPIN_BUTTON (ia->n_cases_spin));
-  gtk_adjustment_set_lower (adj, 1.0);
-
-  if (gtk_grid_get_child_at (GTK_GRID (table), 1, 2) == NULL)
-    {
-      GtkWidget *hbox_percent = psppire_scanf_new (_("Only the first %3d %% of file (approximately)"),
-                                                  &ia->percent_spin);
-
-      gtk_grid_attach (GTK_GRID (table), hbox_percent,
-                      1, 2,
-                      1, 1);
-    }
-
-  gtk_widget_show_all (table);
-
-  on_intro_amount_changed (ia);
-}
-
-/* Initializes IA's intro substructure. */
-static void
-intro_page_create (PsppireImportAssistant *ia)
-{
-  GtkBuilder *builder = ia->builder;
-
-  GtkWidget *w =  get_widget_assert (builder, "Intro");
-
-  ia->percent_spin = gtk_spin_button_new_with_range (0, 100, 10);
-
-
-  add_page_to_assistant (ia, w,  GTK_ASSISTANT_PAGE_CONTENT, _("Select the Lines to Import"));
-
-  ia->all_cases_button = get_widget_assert (builder, "import-all-cases");
-
-  ia->n_cases_button = get_widget_assert (builder, "import-n-cases");
-
-  ia->percent_button = get_widget_assert (builder, "import-percent");
-
-  g_signal_connect_swapped (ia->all_cases_button, "toggled",
-                           G_CALLBACK (on_intro_amount_changed), ia);
-  g_signal_connect_swapped (ia->n_cases_button, "toggled",
-                           G_CALLBACK (on_intro_amount_changed), ia);
-  g_signal_connect_swapped (ia->percent_button, "toggled",
-                           G_CALLBACK (on_intro_amount_changed), ia);
-
-
-  g_object_set_data (G_OBJECT (w), "on-forward", intro_on_leave);
-  g_object_set_data (G_OBJECT (w), "on-entering", intro_on_enter);
-  g_object_set_data (G_OBJECT (w), "on-reset", reset_intro_page);
-}
-
-
 GtkWidget *
 psppire_import_assistant_new (GtkWindow *toplevel)
 {
@@ -1044,360 +505,24 @@ psppire_import_assistant_new (GtkWindow *toplevel)
 
 \f
 
-/* Chooses a name for each column on the separators page */
-static void
-choose_column_names (PsppireImportAssistant *ia)
-{
-  int i;
-  unsigned long int generated_name_count = 0;
-  dict_clear (ia->dict);
-
-  for (i = 0;
-       i < gtk_tree_model_get_n_columns (GTK_TREE_MODEL (ia->delimiters_model)) - 1;
-       ++i)
-    {
-      const gchar *candidate_name = NULL;
-
-      if (gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (ia->variable_names_cb)))
-       {
-         candidate_name = psppire_delimited_text_get_header_title (PSPPIRE_DELIMITED_TEXT (ia->delimiters_model), i);
-       }
-
-      char *name = dict_make_unique_var_name (ia->dict,
-                                             candidate_name,
-                                             &generated_name_count);
-
-      dict_create_var_assert (ia->dict, name, 0);
-      free (name);
-    }
-}
-
-/* Called when the user toggles one of the separators
-   checkboxes. */
-static void
-on_separator_toggle (GtkToggleButton *toggle UNUSED,
-                     PsppireImportAssistant *ia)
-{
-  int i;
-  GSList *delimiters = NULL;
-  for (i = 0; i < SEPARATOR_CNT; i++)
-    {
-      const struct separator *s = &separators[i];
-      GtkWidget *button = get_widget_assert (ia->builder, s->name);
-      if (gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (button)))
-       {
-         delimiters = g_slist_prepend (delimiters,  GINT_TO_POINTER (s->c));
-       }
-    }
-
-  g_object_set (ia->delimiters_model, "delimiters", delimiters, NULL);
-
-  revise_fields_preview (ia);
-}
-
-
-/* Called when the user changes the entry field for custom
-   separators. */
-static void
-on_separators_custom_entry_notify (GObject *gobject UNUSED,
-                                   GParamSpec *arg1 UNUSED,
-                                   PsppireImportAssistant *ia)
-{
-  revise_fields_preview (ia);
-}
-
-/* Called when the user toggles the checkbox that enables custom
-   separators. */
-static void
-on_separators_custom_cb_toggle (GtkToggleButton *custom_cb,
-                                PsppireImportAssistant *ia)
-{
-  bool is_active = gtk_toggle_button_get_active (custom_cb);
-  gtk_widget_set_sensitive (ia->custom_entry, is_active);
-  revise_fields_preview (ia);
-}
-
-/* Called when the user changes the selection in the combo box
-   that selects a quote character. */
-static void
-on_quote_combo_change (GtkComboBox *combo, PsppireImportAssistant *ia)
-{
-  //  revise_fields_preview (ia);
-}
-
-/* Called when the user toggles the checkbox that enables
-   quoting. */
-static void
-on_quote_cb_toggle (GtkToggleButton *quote_cb, PsppireImportAssistant *ia)
-{
-  bool is_active = gtk_toggle_button_get_active (quote_cb);
-  gtk_widget_set_sensitive (ia->quote_combo, is_active);
-  revise_fields_preview (ia);
-}
-
-/* Initializes IA's separators substructure. */
-static void
-separators_page_create (PsppireImportAssistant *ia)
-{
-  GtkBuilder *builder = ia->builder;
-
-  size_t i;
-
-  GtkWidget *w = get_widget_assert (builder, "Separators");
-
-  g_object_set_data (G_OBJECT (w), "on-entering", prepare_separators_page);
-  g_object_set_data (G_OBJECT (w), "on-reset", prepare_separators_page);
-
-  add_page_to_assistant (ia, w,   GTK_ASSISTANT_PAGE_CONTENT, _("Choose Separators"));
-
-  ia->custom_cb = get_widget_assert (builder, "custom-cb");
-  ia->custom_entry = get_widget_assert (builder, "custom-entry");
-  ia->quote_combo = get_widget_assert (builder, "quote-combo");
-  ia->quote_cb = get_widget_assert (builder, "quote-cb");
-
-  gtk_combo_box_set_active (GTK_COMBO_BOX (ia->quote_combo), 0);
-
-  if (ia->fields_tree_view == NULL)
-    {
-      GtkWidget *scroller = get_widget_assert (ia->builder, "fields-scroller");
-      ia->fields_tree_view = gtk_tree_view_new ();
-      g_object_set (ia->fields_tree_view, "enable-search", FALSE, NULL);
-      gtk_container_add (GTK_CONTAINER (scroller), GTK_WIDGET (ia->fields_tree_view));
-      gtk_widget_show_all (scroller);
-    }
-
-  g_signal_connect (ia->quote_combo, "changed",
-                    G_CALLBACK (on_quote_combo_change), ia);
-  g_signal_connect (ia->quote_cb, "toggled",
-                    G_CALLBACK (on_quote_cb_toggle), ia);
-  g_signal_connect (ia->custom_entry, "notify::text",
-                    G_CALLBACK (on_separators_custom_entry_notify), ia);
-  g_signal_connect (ia->custom_cb, "toggled",
-                    G_CALLBACK (on_separators_custom_cb_toggle), ia);
-  for (i = 0; i < SEPARATOR_CNT; i++)
-    g_signal_connect (get_widget_assert (builder, separators[i].name),
-                      "toggled", G_CALLBACK (on_separator_toggle), ia);
-
-}
-
-
-
-\f
-
-
-static struct casereader_random_class my_casereader_class;
-
-static struct ccase *
-my_read (struct casereader *reader, void *aux, casenumber idx)
-{
-  PsppireImportAssistant *ia = PSPPIRE_IMPORT_ASSISTANT (aux);
-  GtkTreeModel *tm = GTK_TREE_MODEL (ia->delimiters_model);
-
-  GtkTreePath *tp = gtk_tree_path_new_from_indices (idx, -1);
-
-  const struct caseproto *proto = casereader_get_proto (reader);
-
-  GtkTreeIter iter;
-  struct ccase *c = NULL;
-  if (gtk_tree_model_get_iter (tm, &iter, tp))
-    {
-      c = case_create (proto);
-      int i;
-      for (i = 0 ; i < caseproto_get_n_widths (proto); ++i)
-       {
-         GValue value = {0};
-         gtk_tree_model_get_value (tm, &iter, i + 1, &value);
-
-         const struct variable *var = dict_get_var (ia->casereader_dict, i);
-
-         const gchar *ss = g_value_get_string (&value);
-         if (ss)
-           {
-             union value *v = case_data_rw (c, var);
-             /* In this reader we derive the union value from the
-                string in the tree_model. We retrieve the width and format
-                from a dictionary which is stored directly after
-                the reader creation. Changes in ia->dict in the
-                variable window are not reflected here and therefore
-                this is always compatible with the width in the
-                caseproto. See bug #58298 */
-             char *xx = data_in (ss_cstr (ss),
-                                 "UTF-8",
-                                 var_get_write_format (var)->type,
-                                 v, var_get_width (var), "UTF-8");
-
-             /* if (xx) */
-             /*   g_print ("%s:%d Err %s\n", __FILE__, __LINE__, xx); */
-             free (xx);
-           }
-         g_value_unset (&value);
-       }
-    }
-
-  gtk_tree_path_free (tp);
-
-  return c;
-}
-
-static void
-my_destroy (struct casereader *reader, void *aux)
-{
-  g_print ("%s:%d %p\n", __FILE__, __LINE__, reader);
-}
-
-static void
-my_advance (struct casereader *reader, void *aux, casenumber cnt)
-{
-  g_print ("%s:%d\n", __FILE__, __LINE__);
-}
-
-static struct casereader *
-textfile_create_reader (PsppireImportAssistant *ia)
-{
-  int n_vars = dict_get_var_cnt (ia->dict);
-
-  int i;
-
-  struct fmt_guesser **fg = XCALLOC (n_vars,  struct fmt_guesser *);
-  for (i = 0 ; i < n_vars; ++i)
-    {
-      fg[i] = fmt_guesser_create ();
-    }
-
-  gint n_rows = gtk_tree_model_iter_n_children (GTK_TREE_MODEL (ia->delimiters_model), NULL);
-
-  GtkTreeIter iter;
-  gboolean ok;
-  for (ok = gtk_tree_model_get_iter_first (GTK_TREE_MODEL (ia->delimiters_model), &iter);
-       ok;
-       ok = gtk_tree_model_iter_next (GTK_TREE_MODEL (ia->delimiters_model), &iter))
-    {
-      for (i = 0 ; i < n_vars; ++i)
-       {
-         gchar *s = NULL;
-         gtk_tree_model_get (GTK_TREE_MODEL (ia->delimiters_model), &iter, i+1, &s, -1);
-         if (s)
-           fmt_guesser_add (fg[i], ss_cstr (s));
-         free (s);
-       }
-    }
-
-  struct caseproto *proto = caseproto_create ();
-  for (i = 0 ; i < n_vars; ++i)
-    {
-      struct fmt_spec fs;
-      fmt_guesser_guess (fg[i], &fs);
-
-      fmt_fix (&fs, FMT_FOR_INPUT);
-
-      struct variable *var = dict_get_var (ia->dict, i);
-
-      int width = fmt_var_width (&fs);
-
-      var_set_width_and_formats (var, width,
-                                &fs, &fs);
-
-      proto = caseproto_add_width (proto, width);
-      fmt_guesser_destroy (fg[i]);
-    }
-
-  free (fg);
-
-  struct casereader *cr = casereader_create_random (proto, n_rows, &my_casereader_class,  ia);
-  /* Store the dictionary at this point when the casereader is created.
-     my_read depends on the dictionary to interpret the strings in the treeview.
-     This guarantees that the union value is produced according to the
-     caseproto in the reader. */
-  ia->casereader_dict = dict_clone (ia->dict);
-  caseproto_unref (proto);
-  return cr;
-}
-
-/* When during import the variable type is changed, the reader is reinitialized
-   based on the new dictionary with a fresh caseprototype. The default behaviour
-   when a variable type is changed and the column is resized is that the union
-   value is interpreted with new variable type and an overlay for that column
-   is generated. Here we reinit to the original reader based on strings.
-   As a result you can switch from string to numeric to string without loosing
-   the string information. */
-static void
-ia_variable_changed_cb (GObject *obj, gint var_num, guint what,
-                       const struct variable *oldvar, gpointer data)
-{
-  PsppireImportAssistant *ia  = PSPPIRE_IMPORT_ASSISTANT (data);
-
-  struct caseproto *proto = caseproto_create();
-  for (int i = 0; i < dict_get_var_cnt (ia->dict); i++)
-    {
-      const struct variable *var = dict_get_var (ia->dict, i);
-      int width = var_get_width (var);
-      proto = caseproto_add_width (proto, width);
-    }
-
-  gint n_rows = gtk_tree_model_iter_n_children (GTK_TREE_MODEL (ia->delimiters_model), NULL);
-
-  PsppireDataStore *store = NULL;
-  g_object_get (ia->data_sheet, "data-model", &store, NULL);
-
-  struct casereader *cr = casereader_create_random (proto, n_rows,
-                                                   &my_casereader_class, ia);
-  psppire_data_store_set_reader (store, cr);
-  dict_unref (ia->casereader_dict);
-  ia->casereader_dict = dict_clone (ia->dict);
-}
 
 /* Called just before the formats page of the assistant is
    displayed. */
 static void
 prepare_formats_page (PsppireImportAssistant *ia)
 {
-  my_casereader_class.read = my_read;
-  my_casereader_class.destroy = my_destroy;
-  my_casereader_class.advance = my_advance;
-
+/* Set the data model for both the data sheet and the variable sheet.  */
   if (ia->spreadsheet)
-    {
-      GtkBuilder *builder = ia->builder;
-      GtkWidget *range_entry = get_widget_assert (builder, "cell-range-entry");
-      GtkWidget *rnc = get_widget_assert (builder, "readnames-checkbox");
-      GtkWidget *combo_box = get_widget_assert (builder, "sheet-entry");
-
-      struct spreadsheet_read_options opts;
-      opts.sheet_name = NULL;
-      opts.sheet_index = gtk_combo_box_get_active (GTK_COMBO_BOX (combo_box)) + 1;
-      opts.read_names = gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (rnc));
-      opts.cell_range = g_strdup (gtk_entry_get_text (GTK_ENTRY (range_entry)));
-      opts.asw = 8;
-
-      struct casereader *reader = spreadsheet_make_reader (ia->spreadsheet, &opts);
-
-      PsppireDict *dict = psppire_dict_new_from_dict (ia->spreadsheet->dict);
-      PsppireDataStore *store = psppire_data_store_new (dict);
-      psppire_data_store_set_reader (store, reader);
-      g_object_set (ia->data_sheet, "data-model", store, NULL);
-      g_object_set (ia->var_sheet, "data-model", dict, NULL);
-    }
+    spreadsheet_set_data_models (ia);
   else
-    {
-      struct casereader *reader = textfile_create_reader (ia);
-
-      PsppireDict *dict = psppire_dict_new_from_dict (ia->dict);
-      PsppireDataStore *store = psppire_data_store_new (dict);
-      psppire_data_store_set_reader (store, reader);
-      g_signal_connect (dict, "variable-changed",
-                        G_CALLBACK (ia_variable_changed_cb),
-                        ia);
-
-      g_object_set (ia->data_sheet, "data-model", store, NULL);
-      g_object_set (ia->var_sheet, "data-model", dict, NULL);
-    }
+    textfile_set_data_models (ia);
 
+  /* Show half-half the data sheet and the variable sheet.  */
   gint pmax;
-  g_object_get (get_widget_assert (ia->builder, "vpaned1"),
+  g_object_get (get_widget_assert (ia->text_builder, "vpaned1"),
                "max-position", &pmax, NULL);
 
-
-  g_object_set (get_widget_assert (ia->builder, "vpaned1"),
+  g_object_set (get_widget_assert (ia->text_builder, "vpaned1"),
                "position", pmax / 2, NULL);
 
   gtk_widget_show (ia->paste_button);
@@ -1406,33 +531,14 @@ prepare_formats_page (PsppireImportAssistant *ia)
 static void
 formats_page_create (PsppireImportAssistant *ia)
 {
-  GtkBuilder *builder = ia->builder;
+  GtkBuilder *builder = ia->text_builder;
 
   GtkWidget *w = get_widget_assert (builder, "Formats");
   g_object_set_data (G_OBJECT (w), "on-entering", prepare_formats_page);
-  g_object_set_data (G_OBJECT (w), "on-reset", reset_formats_page);
-
-  GtkWidget *vars_scroller = get_widget_assert (builder, "vars-scroller");
-  if (ia->var_sheet == NULL)
-    {
-      ia->var_sheet = psppire_variable_sheet_new ();
-
-      gtk_container_add (GTK_CONTAINER (vars_scroller), ia->var_sheet);
-
-      ia->dict = dict_create (get_default_encoding ());
+  //  g_object_set_data (G_OBJECT (w), "on-reset", reset_formats_page);
 
-      gtk_widget_show_all (vars_scroller);
-    }
-  GtkWidget *data_scroller = get_widget_assert (builder, "data-scroller");
-  if (ia->data_sheet == NULL)
-    {
-      ia->data_sheet = psppire_data_sheet_new ();
-      g_object_set (ia->data_sheet, "editable", FALSE, NULL);
-
-      gtk_container_add (GTK_CONTAINER (data_scroller), ia->data_sheet);
-
-      gtk_widget_show_all (data_scroller);
-    }
+  ia->data_sheet = get_widget_assert (builder, "data-sheet");
+  ia->var_sheet = get_widget_assert (builder, "variable-sheet");
 
   add_page_to_assistant (ia, w,
                         GTK_ASSISTANT_PAGE_CONFIRM, _("Adjust Variable Formats"));
@@ -1448,12 +554,12 @@ separators_append_syntax (const PsppireImportAssistant *ia, struct string *s)
 
   ds_put_cstr (s, "  /DELIMITERS=\"");
 
-  if (gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (get_widget_assert (ia->builder, "tab"))))
+  if (gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (get_widget_assert (ia->text_builder, "tab"))))
     ds_put_cstr (s, "\\t");
   for (i = 0; i < SEPARATOR_CNT; i++)
     {
       const struct separator *seps = &separators[i];
-      GtkWidget *button = get_widget_assert (ia->builder, seps->name);
+      GtkWidget *button = get_widget_assert (ia->text_builder, seps->name);
       if (gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (button)))
        {
          if (seps->c == '\t')
@@ -1606,7 +712,7 @@ apply_dict (const struct dictionary *dict, struct string *s)
 static void
 sheet_spec_gen_syntax (PsppireImportAssistant *ia, struct string *s)
 {
-  GtkBuilder *builder = ia->builder;
+  GtkBuilder *builder = ia->spread_builder;
   GtkWidget *range_entry = get_widget_assert (builder, "cell-range-entry");
   GtkWidget *sheet_entry = get_widget_assert (builder, "sheet-entry");
   GtkWidget *rnc = get_widget_assert (builder, "readnames-checkbox");
@@ -1626,7 +732,7 @@ sheet_spec_gen_syntax (PsppireImportAssistant *ia, struct string *s)
                   "\n  /FILE=%sq"
                   "\n  /SHEET=index %d"
                   "\n  /READNAMES=%ss",
-                  (ia->spreadsheet->type == SPREADSHEET_GNUMERIC) ? "GNM" : "ODS",
+                  ia->spreadsheet->type,
                   filename,
                   sheet_index,
                   read_names ? "ON" : "OFF");
index 3de7e08ff82ce2c123972e2a9f6f85da55e36a42..881fc5bad04427dd74aef0f96cc59d25a2e31913 100644 (file)
@@ -28,6 +28,8 @@
 #include "psppire-text-file.h"
 #include "psppire-delimited-text.h"
 
+#include <ssw-sheet.h>
+
 G_BEGIN_DECLS
 
 struct spreadsheet;
@@ -58,18 +60,25 @@ struct spreadsheet;
 typedef struct _PsppireImportAssistant       PsppireImportAssistant;
 typedef struct _PsppireImportAssistantClass  PsppireImportAssistantClass;
 
-
-typedef void page_func (PsppireImportAssistant *, GtkWidget *page);
+enum IMPORT_ASSISTANT_DIRECTION {IMPORT_ASSISTANT_FORWARDS, IMPORT_ASSISTANT_BACKWARDS};
 
 struct _PsppireImportAssistant
 {
   GtkAssistant parent;
 
-  GtkBuilder *builder;
+  gint previous_page;
+  gchar *file_name;
+  GMainLoop *main_loop;
+  GtkWidget *paste_button;
+  GtkWidget *reset_button;
+  int response;
+
+  struct dictionary *dict;
+  struct dictionary *casereader_dict;
 
-  gint current_page;
+  GtkWidget *var_sheet;
+  GtkWidget *data_sheet;
 
-  gchar *file_name;
 
   /* START The chooser page of the assistant. */
   GtkWidget *encoding_selector;
@@ -77,6 +86,9 @@ struct _PsppireImportAssistant
   /* END The chooser page of the assistant. */
 
 
+  GtkBuilder *text_builder;
+
+
   /* START The introduction page of the assistant. */
     GtkWidget *all_cases_button;
     GtkWidget *n_cases_button;
@@ -86,7 +98,7 @@ struct _PsppireImportAssistant
   /* END The introduction page of the assistant. */
 
 
-/* START Page where the user chooses field separators. */
+  /* START Page where the user chooses field separators. */
 
   /* How to break lines into columns. */
   struct string quotes;       /* Quote characters. */
@@ -106,21 +118,15 @@ struct _PsppireImportAssistant
   GtkWidget *variable_names_cb;
   /* END first line page */
 
-  GMainLoop *main_loop;
-  GtkWidget *paste_button;
-  GtkWidget *reset_button;
-  int response;
-
   PsppireTextFile *text_file;
   PsppireDelimitedText *delimiters_model;
 
-  struct dictionary *dict;
-  struct dictionary *casereader_dict;
-
-  GtkWidget *var_sheet;
-  GtkWidget *data_sheet;
-
+  /* START spreadsheet related things */
+  GtkBuilder *spread_builder;
+  GtkWidget *preview_sheet;
   struct spreadsheet *spreadsheet;
+  SswRange selection;
+  bool updating_selection;
 };
 
 struct _PsppireImportAssistantClass
@@ -137,6 +143,9 @@ gchar *psppire_import_assistant_generate_syntax (PsppireImportAssistant *);
 
 int psppire_import_assistant_run (PsppireImportAssistant *asst);
 
+GtkWidget *add_page_to_assistant (PsppireImportAssistant *ia,
+                                  GtkWidget *page, GtkAssistantPageType type, const gchar *title);
+
 G_END_DECLS
 
 #endif /* __PSPPIRE_IMPORT_ASSISTANT_H__ */
diff --git a/src/ui/gui/psppire-import-spreadsheet.c b/src/ui/gui/psppire-import-spreadsheet.c
new file mode 100644 (file)
index 0000000..636276f
--- /dev/null
@@ -0,0 +1,416 @@
+/* PSPPIRE - a graphical user interface for PSPP.
+   Copyright (C) 2015, 2016, 2017, 2018, 2020  Free Software Foundation
+
+   This program is free software: you can redistribute it and/or modify
+   it under the terms of the GNU General Public License as published by
+   the Free Software Foundation, either version 3 of the License, or
+   (at your option) any later version.
+
+   This program is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+   GNU General Public License for more details.
+
+   You should have received a copy of the GNU General Public License
+   along with this program.  If not, see <http://www.gnu.org/licenses/>. */
+
+#include <config.h>
+
+
+#include "psppire-import-assistant.h"
+#include "psppire-import-spreadsheet.h"
+#include "builder-wrapper.h"
+
+#include "libpspp/misc.h"
+#include "psppire-spreadsheet-model.h"
+#include "psppire-spreadsheet-data-model.h"
+#include "psppire-data-store.h"
+
+#include <gettext.h>
+#define _(msgid) gettext (msgid)
+#define N_(msgid) msgid
+
+static void
+set_column_header_label (GtkWidget *button, uint i, gpointer user_data)
+{
+  gchar *x = int_to_ps26 (i);
+  gtk_button_set_label (GTK_BUTTON (button), x);
+  g_free (x);
+}
+
+static void do_selection_update (PsppireImportAssistant *ia);
+
+static void
+on_sheet_combo_changed (GtkComboBox *cb, PsppireImportAssistant *ia)
+{
+  GtkBuilder *builder = ia->spread_builder;
+  gint sheet_number = gtk_combo_box_get_active (cb);
+
+  gint coli = spreadsheet_get_sheet_n_columns (ia->spreadsheet, sheet_number) - 1;
+  gint rowi = spreadsheet_get_sheet_n_rows (ia->spreadsheet, sheet_number) - 1;
+
+  {
+    /* Now set the spin button upper limits according to the size of the selected sheet.  */
+
+    GtkWidget *sb0 = get_widget_assert (builder, "sb0");
+    GtkWidget *sb1 = get_widget_assert (builder, "sb1");
+    GtkWidget *sb2 = get_widget_assert (builder, "sb2");
+    GtkWidget *sb3 = get_widget_assert (builder, "sb3");
+
+    /* The row spinbuttons contain decimal digits.  So there should be
+       enough space to display them.  */
+    int digits = (rowi > 0) ? intlog10 (rowi + 1): 1;
+    gtk_entry_set_max_width_chars (GTK_ENTRY (sb1), digits);
+    gtk_entry_set_max_width_chars (GTK_ENTRY (sb3), digits);
+
+    /* The column spinbuttons are pseudo-base-26 digits.  The
+       exact formula for the number required is complicated.  However
+       3 is a reasonable amount.  It's not too large, and anyone importing
+       a spreadsheet with more than 3^26 columns is likely to experience
+       other problems anyway.  */
+    gtk_entry_set_max_width_chars (GTK_ENTRY (sb0), 3);
+    gtk_entry_set_max_width_chars (GTK_ENTRY (sb2), 3);
+
+
+    GtkAdjustment *adj = gtk_spin_button_get_adjustment (GTK_SPIN_BUTTON (sb0));
+    gtk_adjustment_set_upper (adj, coli);
+
+    adj = gtk_spin_button_get_adjustment (GTK_SPIN_BUTTON (sb1));
+    gtk_adjustment_set_upper (adj, rowi);
+
+    adj = gtk_spin_button_get_adjustment (GTK_SPIN_BUTTON (sb2));
+    gtk_adjustment_set_upper (adj, coli);
+
+    adj = gtk_spin_button_get_adjustment (GTK_SPIN_BUTTON (sb3));
+    gtk_adjustment_set_upper (adj, rowi);
+  }
+
+  GtkTreeModel *data_model =
+    psppire_spreadsheet_data_model_new (ia->spreadsheet, sheet_number);
+  g_object_set (ia->preview_sheet,
+                "data-model", data_model,
+                "editable", FALSE,
+                NULL);
+  g_object_unref (data_model);
+
+  GObject *hmodel = NULL;
+  g_object_get (ia->preview_sheet, "hmodel", &hmodel, NULL);
+  
+  g_object_set (hmodel,
+                "post-button-create-func", set_column_header_label,
+                NULL);
+
+  ia->selection.start_x = ia->selection.start_y = 0;
+  ia->selection.end_x = coli;
+  ia->selection.end_y = rowi;
+  do_selection_update (ia);
+}
+
+/* Ensure that PARTNER is never less than than SUBJECT.  */
+static void
+on_value_change_lower (GtkSpinButton *subject, GtkSpinButton *partner)
+{
+  gint p = gtk_spin_button_get_value_as_int (partner);
+  gint s = gtk_spin_button_get_value_as_int (subject);
+
+  if (s > p)
+    gtk_spin_button_set_value (partner, s);
+}
+
+/* Ensure that PARTNER is never greater than to SUBJECT.  */
+static void
+on_value_change_upper (GtkSpinButton *subject, GtkSpinButton *partner)
+{
+  gint p = gtk_spin_button_get_value_as_int (partner);
+  gint s = gtk_spin_button_get_value_as_int (subject);
+
+  if (s < p)
+    gtk_spin_button_set_value (partner, s);
+}
+
+
+/* Sets SB to use 1 based display instead of 0 based.  */
+static gboolean
+row_output (GtkSpinButton *sb, gpointer unused)
+{
+  gint value = gtk_spin_button_get_value_as_int (sb);
+  char *text = g_strdup_printf ("%d", value + 1);
+  gtk_entry_set_text (GTK_ENTRY (sb), text);
+  free (text);
+
+  return TRUE;
+}
+
+/* Sets SB to use text like A, B, C instead of 0, 1, 2 etc.  */
+static gboolean
+column_output (GtkSpinButton *sb, gpointer unused)
+{
+  gint value = gtk_spin_button_get_value_as_int (sb);
+  char *text = int_to_ps26 (value);
+  if (text == NULL)
+    return FALSE;
+
+  gtk_entry_set_text (GTK_ENTRY (sb), text);
+  free (text);
+
+  return TRUE;
+}
+
+/* Interprets the SBs text as 1 based instead of zero based.  */
+static gint
+row_input (GtkSpinButton *sb, gpointer new_value, gpointer unused)
+{
+  const char *text = gtk_entry_get_text (GTK_ENTRY (sb));
+  gdouble value = g_strtod (text, NULL) - 1;
+
+  if (value < 0)
+    return FALSE;
+
+  memcpy (new_value, &value, sizeof (value));
+
+  return TRUE;
+}
+
+
+/* Interprets the SBs text of the form A, B, C etc and
+   sets NEW_VALUE as a double.  */
+static gint
+column_input (GtkSpinButton *sb, gpointer new_value, gpointer unused)
+{
+  const char *text = gtk_entry_get_text (GTK_ENTRY (sb));
+  double value = ps26_to_int (text);
+
+  if (value < 0)
+    return FALSE;
+
+  memcpy (new_value, &value, sizeof (value));
+
+  return TRUE;
+}
+
+static void
+reset_page (PsppireImportAssistant *ia)
+{
+  GtkBuilder *builder = ia->spread_builder;
+  GtkWidget *readnames_checkbox = get_widget_assert (builder, "readnames-checkbox");
+  gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (readnames_checkbox), FALSE);
+
+  gint sheet_number = 0;
+  GtkWidget *sheet_entry = get_widget_assert (builder, "sheet-entry");
+  gtk_combo_box_set_active (GTK_COMBO_BOX (sheet_entry), sheet_number);
+
+  gint coli = spreadsheet_get_sheet_n_columns (ia->spreadsheet, sheet_number) - 1;
+  gint rowi = spreadsheet_get_sheet_n_rows (ia->spreadsheet, sheet_number) - 1;
+
+  ia->selection.start_x = ia->selection.start_y = 0;
+  ia->selection.end_x = coli;
+  ia->selection.end_y = rowi;
+  do_selection_update (ia);
+}
+
+/* Prepares IA's sheet_spec page. */
+static void
+prepare_sheet_spec_page (PsppireImportAssistant *ia, GtkWidget *page, enum IMPORT_ASSISTANT_DIRECTION dir)
+{
+  if (dir != IMPORT_ASSISTANT_FORWARDS)
+    return;
+
+  GtkBuilder *builder = ia->spread_builder;
+  GtkWidget *sheet_entry = get_widget_assert (builder, "sheet-entry");
+  GtkWidget *readnames_checkbox = get_widget_assert (builder, "readnames-checkbox");
+
+  GtkTreeModel *model = psppire_spreadsheet_model_new (ia->spreadsheet);
+  gtk_combo_box_set_model (GTK_COMBO_BOX (sheet_entry), model);
+  g_object_unref (model);
+
+  gint items = gtk_tree_model_iter_n_children (model, NULL);
+  gtk_widget_set_sensitive (sheet_entry, items > 1);
+
+  gtk_combo_box_set_active (GTK_COMBO_BOX (sheet_entry), 0);
+  gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (readnames_checkbox), FALSE);
+
+  GtkWidget *file_name_label = get_widget_assert (builder, "file-name-label");
+  gtk_label_set_text (GTK_LABEL (file_name_label), ia->file_name);
+
+  /* Gang the increment/decrement buttons, so that the upper always exceeds the lower.  */
+  GtkWidget *sb0 = get_widget_assert (builder, "sb0");
+  GtkWidget *sb2 = get_widget_assert (builder, "sb2");
+
+  g_signal_connect (sb0, "value-changed", G_CALLBACK (on_value_change_lower), sb2);
+  g_signal_connect (sb2, "value-changed", G_CALLBACK (on_value_change_upper), sb0);
+
+  GtkWidget *sb1 = get_widget_assert (builder, "sb1");
+  GtkWidget *sb3 = get_widget_assert (builder, "sb3");
+
+  g_signal_connect (sb1, "value-changed", G_CALLBACK (on_value_change_lower), sb3);
+  g_signal_connect (sb3, "value-changed", G_CALLBACK (on_value_change_upper), sb1);
+
+
+  /* Set the column spinbuttons to display as A, B, C notation,
+     and the row spinbuttons to display as 1 based instead of zero based. */
+  g_signal_connect (sb0, "output", G_CALLBACK (column_output), NULL);
+  g_signal_connect (sb0, "input", G_CALLBACK (column_input), NULL);
+
+  g_signal_connect (sb2, "output", G_CALLBACK (column_output), NULL);
+  g_signal_connect (sb2, "input", G_CALLBACK (column_input), NULL);
+
+  g_signal_connect (sb1, "output", G_CALLBACK (row_output), NULL);
+  g_signal_connect (sb1, "input", G_CALLBACK (row_input), NULL);
+
+  g_signal_connect (sb3, "output", G_CALLBACK (row_output), NULL);
+  g_signal_connect (sb3, "input", G_CALLBACK (row_input), NULL);
+}
+
+static void
+do_selection_update (PsppireImportAssistant *ia)
+{
+  GtkBuilder *builder = ia->spread_builder;
+
+  /* Stop this function re-entering itself.  */
+  if (ia->updating_selection)
+    return;
+  ia->updating_selection = TRUE;
+
+  /* We must take a copy of the selection.  A pointer will not suffice,
+     because the selection can change under us.  */
+  SswRange sel = ia->selection;
+
+  g_object_set (ia->preview_sheet, "selection", &sel, NULL);
+
+  char *range = create_cell_range (sel.start_x, sel.start_y, sel.end_x, sel.end_y);
+
+  GtkWidget *range_entry = get_widget_assert (builder, "cell-range-entry");
+  if (range)
+    gtk_entry_set_text (GTK_ENTRY (range_entry), range);
+  free (range);
+
+  GtkWidget *sb0 = get_widget_assert (builder, "sb0");
+  GtkWidget *sb1 = get_widget_assert (builder, "sb1");
+  GtkWidget *sb2 = get_widget_assert (builder, "sb2");
+  GtkWidget *sb3 = get_widget_assert (builder, "sb3");
+
+  gtk_spin_button_set_value (GTK_SPIN_BUTTON (sb0), sel.start_x);
+  gtk_spin_button_set_value (GTK_SPIN_BUTTON (sb1), sel.start_y);
+
+  gtk_spin_button_set_value (GTK_SPIN_BUTTON (sb2), sel.end_x);
+  gtk_spin_button_set_value (GTK_SPIN_BUTTON (sb3), sel.end_y);
+
+  ia->updating_selection = FALSE;
+}
+
+static void
+on_preview_selection_changed (SswSheet *sheet, gpointer selection,
+                              PsppireImportAssistant *ia)
+{
+  memcpy (&ia->selection, selection, sizeof (ia->selection));
+  do_selection_update (ia);
+}
+
+static void
+entry_update_selected_range (GtkEntry *entry, PsppireImportAssistant *ia)
+{
+  const char *text = gtk_entry_get_text (entry);
+
+  if (convert_cell_ref (text,
+                        &ia->selection.start_x, &ia->selection.start_y,
+                        &ia->selection.end_x, &ia->selection.end_y))
+    {
+      do_selection_update (ia);
+    }
+}
+
+/* On change of any spinbutton, update the selected range accordingly.   */
+static void
+sb_update_selected_range (PsppireImportAssistant *ia)
+{
+  GtkBuilder *builder = ia->spread_builder;
+  GtkWidget *sb0 = get_widget_assert (builder, "sb0");
+  GtkWidget *sb1 = get_widget_assert (builder, "sb1");
+  GtkWidget *sb2 = get_widget_assert (builder, "sb2");
+  GtkWidget *sb3 = get_widget_assert (builder, "sb3");
+
+  ia->selection.start_x = gtk_spin_button_get_value_as_int (GTK_SPIN_BUTTON (sb0));
+  ia->selection.start_y = gtk_spin_button_get_value_as_int (GTK_SPIN_BUTTON (sb1));
+
+  ia->selection.end_x = gtk_spin_button_get_value_as_int (GTK_SPIN_BUTTON (sb2));
+  ia->selection.end_y = gtk_spin_button_get_value_as_int (GTK_SPIN_BUTTON (sb3));
+
+  do_selection_update (ia);
+}
+
+
+/* Initializes IA's sheet_spec substructure. */
+void
+sheet_spec_page_create (PsppireImportAssistant *ia)
+{
+  GtkBuilder *builder = ia->spread_builder;
+  GtkWidget *page = get_widget_assert (builder, "Spreadsheet-Importer");
+
+  ia->preview_sheet = get_widget_assert (builder, "preview-sheet");
+
+  g_signal_connect (ia->preview_sheet, "selection-changed",
+                   G_CALLBACK (on_preview_selection_changed), ia);
+
+  gtk_widget_show (ia->preview_sheet);
+
+  {
+    GtkWidget *combo_box = get_widget_assert (builder, "sheet-entry");
+    GtkCellRenderer *renderer = gtk_cell_renderer_text_new ();
+    gtk_cell_layout_clear (GTK_CELL_LAYOUT (combo_box));
+    gtk_cell_layout_pack_start (GTK_CELL_LAYOUT (combo_box), renderer, TRUE);
+    gtk_cell_layout_set_attributes (GTK_CELL_LAYOUT (combo_box), renderer,
+                                   "text", 0,
+                                   NULL);
+
+    g_signal_connect (combo_box, "changed", G_CALLBACK (on_sheet_combo_changed), ia);
+  }
+
+  {
+    GtkWidget *range_entry = get_widget_assert (builder, "cell-range-entry");
+    g_signal_connect (range_entry, "changed", G_CALLBACK (entry_update_selected_range), ia);
+
+    GtkWidget *sb0 = get_widget_assert (builder, "sb0");
+    g_signal_connect_swapped (sb0, "value-changed", G_CALLBACK (sb_update_selected_range), ia);
+    GtkWidget *sb1 = get_widget_assert (builder, "sb1");
+    g_signal_connect_swapped (sb1, "value-changed", G_CALLBACK (sb_update_selected_range), ia);
+    GtkWidget *sb2 = get_widget_assert (builder, "sb2");
+    g_signal_connect_swapped (sb2, "value-changed", G_CALLBACK (sb_update_selected_range), ia);
+    GtkWidget *sb3 = get_widget_assert (builder, "sb3");
+    g_signal_connect_swapped (sb3, "value-changed", G_CALLBACK (sb_update_selected_range), ia);
+  }
+
+
+  add_page_to_assistant (ia, page,
+                        GTK_ASSISTANT_PAGE_CONTENT, _("Importing Spreadsheet Data"));
+
+  g_object_set_data (G_OBJECT (page), "on-entering", prepare_sheet_spec_page);
+  g_object_set_data (G_OBJECT (page), "on-reset",   reset_page);
+}
+
+
+/* Set the data model for both the data sheet and the variable sheet.  */
+void
+spreadsheet_set_data_models (PsppireImportAssistant *ia)
+{
+  GtkBuilder *builder = ia->spread_builder;
+  GtkWidget *range_entry = get_widget_assert (builder, "cell-range-entry");
+  GtkWidget *rnc = get_widget_assert (builder, "readnames-checkbox");
+  GtkWidget *combo_box = get_widget_assert (builder, "sheet-entry");
+
+  struct spreadsheet_read_options opts;
+  opts.sheet_name = NULL;
+  opts.sheet_index = gtk_combo_box_get_active (GTK_COMBO_BOX (combo_box)) + 1;
+  opts.read_names = gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (rnc));
+  opts.cell_range = g_strdup (gtk_entry_get_text (GTK_ENTRY (range_entry)));
+  opts.asw = 8;
+
+  struct casereader *reader = spreadsheet_make_reader (ia->spreadsheet, &opts);
+
+  PsppireDict *dict = psppire_dict_new_from_dict (ia->spreadsheet->dict);
+  PsppireDataStore *store = psppire_data_store_new (dict);
+  psppire_data_store_set_reader (store, reader);
+  g_object_set (ia->data_sheet, "data-model", store, NULL);
+  g_object_set (ia->var_sheet, "data-model", dict, NULL);
+}
+
+
diff --git a/src/ui/gui/psppire-import-spreadsheet.h b/src/ui/gui/psppire-import-spreadsheet.h
new file mode 100644 (file)
index 0000000..bd34921
--- /dev/null
@@ -0,0 +1,10 @@
+#ifndef PSPPIRE_IMPORT_SPREADSHEET_H
+#define PSPPIRE_IMPORT_SPREADSHEET_H
+
+/* Initializes IA's sheet_spec substructure. */
+void sheet_spec_page_create (PsppireImportAssistant *ia);
+
+/* Set the data model for both the data sheet and the variable sheet.  */
+void spreadsheet_set_data_models (PsppireImportAssistant *ia);
+
+#endif
diff --git a/src/ui/gui/psppire-import-textfile.c b/src/ui/gui/psppire-import-textfile.c
new file mode 100644 (file)
index 0000000..d293dc0
--- /dev/null
@@ -0,0 +1,862 @@
+/* PSPPIRE - a graphical user interface for PSPP.
+   Copyright (C) 2015, 2016, 2017, 2018, 2020  Free Software Foundation
+
+   This program is free software: you can redistribute it and/or modify
+   it under the terms of the GNU General Public License as published by
+   the Free Software Foundation, either version 3 of the License, or
+   (at your option) any later version.
+
+   This program is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+   GNU General Public License for more details.
+
+   You should have received a copy of the GNU General Public License
+   along with this program.  If not, see <http://www.gnu.org/licenses/>. */
+
+#include <config.h>
+
+#include "psppire-import-textfile.h"
+#include <gtk/gtk.h>
+
+#include "libpspp/i18n.h"
+#include "libpspp/line-reader.h"
+#include "libpspp/message.h"
+#include "libpspp/hmap.h"
+#include "libpspp/hash-functions.h"
+#include "libpspp/str.h"
+#include "libpspp/misc.h"
+
+#include "data/casereader.h"
+#include "data/casereader-provider.h"
+#include "data/data-in.h"
+#include "data/format-guesser.h"
+
+#include "builder-wrapper.h"
+
+#include "psppire-data-store.h"
+#include "psppire-scanf.h"
+
+#include "ui/syntax-gen.h"
+
+#include <gettext.h>
+#define _(msgid) gettext (msgid)
+#define N_(msgid) msgid
+
+/* Chooses a name for each column on the separators page */
+static void choose_column_names (PsppireImportAssistant *ia);
+
+/* Revises the contents of the fields tree view based on the
+   currently chosen set of separators. */
+static void
+revise_fields_preview (PsppireImportAssistant *ia)
+{
+  choose_column_names (ia);
+}
+
+
+struct separator_count_node
+{
+  struct hmap_node node;
+  int occurance; /* The number of times the separator occurs in a line */
+  int quantity;  /* The number of lines with this occurance */
+};
+
+
+/* Picks the most likely separator and quote characters based on
+   IA's file data. */
+static void
+choose_likely_separators (PsppireImportAssistant *ia)
+{
+  gint first_line = 0;
+  g_object_get (ia->delimiters_model, "first-line", &first_line, NULL);
+
+  gboolean valid;
+  GtkTreeIter iter;
+  int j;
+
+  struct hmap count_map[SEPARATOR_CNT];
+  for (j = 0; j < SEPARATOR_CNT; ++j)
+    hmap_init (count_map + j);
+
+  GtkTreePath *p = gtk_tree_path_new_from_indices (first_line, -1);
+
+  for (valid = gtk_tree_model_get_iter (GTK_TREE_MODEL (ia->text_file), &iter, p);
+       valid;
+       valid = gtk_tree_model_iter_next (GTK_TREE_MODEL (ia->text_file), &iter))
+    {
+      gchar *line_text = NULL;
+      gtk_tree_model_get (GTK_TREE_MODEL (ia->text_file), &iter, 1, &line_text, -1);
+
+      gint *counts = xzalloc (sizeof *counts * SEPARATOR_CNT);
+
+      struct substring cs = ss_cstr (line_text);
+      for (;
+          UINT32_MAX != ss_first_mb (cs);
+          ss_get_mb (&cs))
+       {
+         ucs4_t character = ss_first_mb (cs);
+
+         int s;
+         for (s = 0; s < SEPARATOR_CNT; ++s)
+           {
+             if (character == separators[s].c)
+               counts[s]++;
+           }
+       }
+
+      int j;
+      for (j = 0; j < SEPARATOR_CNT; ++j)
+       {
+         if (counts[j] > 0)
+           {
+             struct separator_count_node *cn = NULL;
+             unsigned int hash = hash_int (counts[j], 0);
+             HMAP_FOR_EACH_WITH_HASH (cn, struct separator_count_node, node, hash, &count_map[j])
+               {
+                 if (cn->occurance == counts[j])
+                   break;
+               }
+
+             if (cn == NULL)
+               {
+                 struct separator_count_node *new_cn = xzalloc (sizeof *new_cn);
+                 new_cn->occurance = counts[j];
+                 new_cn->quantity = 1;
+                 hmap_insert (&count_map[j], &new_cn->node, hash);
+               }
+             else
+               cn->quantity++;
+           }
+       }
+
+      free (line_text);
+      free (counts);
+    }
+  gtk_tree_path_free (p);
+
+  if (hmap_count (count_map) > 0)
+    {
+      int most_frequent = -1;
+      int largest = 0;
+      for (j = 0; j < SEPARATOR_CNT; ++j)
+        {
+          struct separator_count_node *cn;
+          struct separator_count_node *next;
+          HMAP_FOR_EACH_SAFE (cn, next, struct separator_count_node, node, &count_map[j])
+            {
+              if (largest < cn->quantity)
+                {
+                  largest = cn->quantity;
+                  most_frequent = j;
+                }
+              free (cn);
+            }
+          hmap_destroy (&count_map[j]);
+        }
+
+      g_return_if_fail (most_frequent >= 0);
+
+      GtkWidget *toggle =
+        get_widget_assert (ia->text_builder, separators[most_frequent].name);
+      gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (toggle), TRUE);
+    }
+}
+
+static void
+repopulate_delimiter_columns (PsppireImportAssistant *ia)
+{
+  /* Remove all the columns */
+  while (gtk_tree_view_get_n_columns (GTK_TREE_VIEW (ia->fields_tree_view)) > 0)
+    {
+      GtkTreeViewColumn *tvc = gtk_tree_view_get_column (GTK_TREE_VIEW (ia->fields_tree_view), 0);
+      gtk_tree_view_remove_column (GTK_TREE_VIEW (ia->fields_tree_view), tvc);
+    }
+
+  gint n_fields =
+    gtk_tree_model_get_n_columns (GTK_TREE_MODEL (ia->delimiters_model));
+
+  /* ... and put them back again. */
+  gint f;
+  for (f = gtk_tree_view_get_n_columns (GTK_TREE_VIEW (ia->fields_tree_view));
+       f < n_fields; f++)
+    {
+      GtkCellRenderer *renderer = gtk_cell_renderer_text_new ();
+
+      const gchar *title = NULL;
+
+      if (f == 0)
+       title = _("line");
+      else
+       {
+         if (gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (ia->variable_names_cb)))
+           {
+             title =
+               psppire_delimited_text_get_header_title
+               (PSPPIRE_DELIMITED_TEXT (ia->delimiters_model), f - 1);
+           }
+         if (title == NULL)
+           title = _("var");
+       }
+
+      GtkTreeViewColumn *column =
+       gtk_tree_view_column_new_with_attributes (title,
+                                                 renderer,
+                                                 "text", f,
+                                                 NULL);
+      g_object_set (column,
+                   "resizable", TRUE,
+                   "sizing", GTK_TREE_VIEW_COLUMN_AUTOSIZE,
+                   NULL);
+
+      gtk_tree_view_append_column (GTK_TREE_VIEW (ia->fields_tree_view), column);
+    }
+}
+
+static void
+reset_tree_view_model (PsppireImportAssistant *ia)
+{
+  GtkTreeModel *tm = gtk_tree_view_get_model (GTK_TREE_VIEW (ia->fields_tree_view));
+  g_object_ref (tm);
+  gtk_tree_view_set_model (GTK_TREE_VIEW (ia->fields_tree_view), NULL);
+
+
+  repopulate_delimiter_columns (ia);
+
+  gtk_tree_view_set_model (GTK_TREE_VIEW (ia->fields_tree_view), tm);
+  //  gtk_tree_view_columns_autosize (GTK_TREE_VIEW (ia->fields_tree_view));
+
+  g_object_unref (tm);
+}
+
+/* Resets IA's intro page to its initial state. */
+static void
+reset_intro_page (PsppireImportAssistant *ia)
+{
+  gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (ia->n_cases_button),
+                                TRUE);
+  gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (ia->percent_button),
+                                TRUE);
+  gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (ia->all_cases_button),
+                                TRUE);
+
+  gtk_spin_button_set_value (GTK_SPIN_BUTTON (ia->n_cases_spin), 1);
+  gtk_spin_button_set_value (GTK_SPIN_BUTTON (ia->percent_spin), 0);
+}
+
+/* Called when one of the radio buttons is clicked. */
+static void
+on_intro_amount_changed (PsppireImportAssistant *ia)
+{
+  gtk_widget_set_sensitive (ia->n_cases_spin,
+                           gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (ia->n_cases_button)));
+
+  gtk_widget_set_sensitive (ia->percent_spin,
+                           gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (ia->percent_button)));
+}
+
+static void
+on_treeview_selection_change (PsppireImportAssistant *ia)
+{
+  GtkTreeSelection *selection =
+    gtk_tree_view_get_selection (GTK_TREE_VIEW (ia->first_line_tree_view));
+  GtkTreeModel *model = NULL;
+  GtkTreeIter iter;
+  if (gtk_tree_selection_get_selected (selection, &model, &iter))
+    {
+      gint max_lines;
+      int n;
+      GtkTreePath *path = gtk_tree_model_get_path (model, &iter);
+      gint *index = gtk_tree_path_get_indices (path);
+      n = *index;
+      gtk_tree_path_free (path);
+      g_object_get (model, "maximum-lines", &max_lines, NULL);
+      gtk_widget_set_sensitive (ia->variable_names_cb,
+                               (n > 0 && n < max_lines));
+      ia->delimiters_model =
+       psppire_delimited_text_new (GTK_TREE_MODEL (ia->text_file));
+      g_object_set (ia->delimiters_model, "first-line", n, NULL);
+    }
+}
+
+static void
+render_text_preview_line (GtkTreeViewColumn *tree_column,
+               GtkCellRenderer *cell,
+               GtkTreeModel *tree_model,
+               GtkTreeIter *iter,
+               gpointer data)
+{
+  /*
+     Set the text  to a "insensitive" state if the row
+     is greater than what the user declared to be the maximum.
+  */
+  GtkTreePath *path = gtk_tree_model_get_path (tree_model, iter);
+  gint *ii = gtk_tree_path_get_indices (path);
+  gint max_lines;
+  g_object_get (tree_model, "maximum-lines", &max_lines, NULL);
+  g_object_set (cell, "sensitive", (*ii < max_lines), NULL);
+  gtk_tree_path_free (path);
+}
+
+/* Resets IA's "first line" page to its initial state. */
+static void
+reset_first_line_page (PsppireImportAssistant *ia)
+{
+  gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (ia->variable_names_cb), FALSE);
+
+  GtkTreeSelection *selection =
+    gtk_tree_view_get_selection (GTK_TREE_VIEW (ia->first_line_tree_view));
+
+  gtk_tree_selection_unselect_all (selection);
+}
+
+/* Initializes IA's first_line substructure. */
+void
+first_line_page_create (PsppireImportAssistant *ia)
+{
+  GtkWidget *w =  get_widget_assert (ia->text_builder, "FirstLine");
+
+  g_object_set_data (G_OBJECT (w), "on-entering", on_treeview_selection_change);
+  g_object_set_data (G_OBJECT (w), "on-reset", reset_first_line_page);
+
+  add_page_to_assistant (ia, w,
+                        GTK_ASSISTANT_PAGE_CONTENT, _("Select the First Line"));
+
+  GtkWidget *scrolled_window = get_widget_assert (ia->text_builder, "first-line-scroller");
+
+  if (ia->first_line_tree_view == NULL)
+    {
+      ia->first_line_tree_view = gtk_tree_view_new ();
+      g_object_set (ia->first_line_tree_view, "enable-search", FALSE, NULL);
+
+      gtk_tree_view_set_headers_visible (GTK_TREE_VIEW (ia->first_line_tree_view), TRUE);
+
+      GtkCellRenderer *renderer = gtk_cell_renderer_text_new ();
+      GtkTreeViewColumn *column = gtk_tree_view_column_new_with_attributes (_("Line"), renderer,
+                                                                           "text", 0,
+                                                                           NULL);
+
+      gtk_tree_view_column_set_cell_data_func (column, renderer, render_text_preview_line, ia, 0);
+      gtk_tree_view_append_column (GTK_TREE_VIEW (ia->first_line_tree_view), column);
+
+      renderer = gtk_cell_renderer_text_new ();
+      column = gtk_tree_view_column_new_with_attributes (_("Text"), renderer, "text", 1, NULL);
+      gtk_tree_view_column_set_cell_data_func (column, renderer, render_text_preview_line, ia, 0);
+
+      gtk_tree_view_append_column (GTK_TREE_VIEW (ia->first_line_tree_view), column);
+
+      g_signal_connect_swapped (ia->first_line_tree_view, "cursor-changed",
+                               G_CALLBACK (on_treeview_selection_change), ia);
+      gtk_container_add (GTK_CONTAINER (scrolled_window), ia->first_line_tree_view);
+    }
+
+  gtk_widget_show_all (scrolled_window);
+
+  ia->variable_names_cb = get_widget_assert (ia->text_builder, "variable-names");
+
+  reset_first_line_page (ia);
+}
+
+static void
+intro_on_leave (PsppireImportAssistant *ia, GtkWidget *page, enum IMPORT_ASSISTANT_DIRECTION dir)
+{
+  if (dir != IMPORT_ASSISTANT_FORWARDS)
+    return;
+
+  gint lc = 0;
+  g_object_get (ia->text_file, "line-count", &lc, NULL);
+  if (gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (ia->n_cases_button)))
+    {
+      gint max_lines = gtk_spin_button_get_value_as_int (GTK_SPIN_BUTTON (ia->n_cases_spin));
+      g_object_set (ia->text_file, "maximum-lines", max_lines, NULL);
+    }
+  else if (gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (ia->percent_button)))
+    {
+      gdouble percent = gtk_spin_button_get_value (GTK_SPIN_BUTTON (ia->percent_spin));
+      g_object_set (ia->text_file, "maximum-lines", (gint) (lc * percent / 100.0), NULL);
+    }
+  else
+    {
+      g_object_set (ia->text_file, "maximum-lines", lc, NULL);
+    }
+}
+
+
+static void
+intro_on_enter (PsppireImportAssistant *ia, GtkWidget *page, enum IMPORT_ASSISTANT_DIRECTION dir)
+{
+  GtkBuilder *builder = ia->text_builder;
+  GtkWidget *table  = get_widget_assert (builder, "button-table");
+
+  struct string s;
+
+  ds_init_empty (&s);
+  ds_put_cstr (&s, _("This assistant will guide you through the process of "
+                     "importing data into PSPP from a text file with one line "
+                     "per case,  in which fields are separated by tabs, "
+                     "commas, or other delimiters.\n\n"));
+
+  if (ia->text_file)
+    {
+      if (ia->text_file->total_is_exact)
+       {
+         ds_put_format (
+                        &s, ngettext ("The selected file contains %'lu line of text.  ",
+                                      "The selected file contains %'lu lines of text.  ",
+                                      ia->text_file->total_lines),
+                        ia->text_file->total_lines);
+       }
+      else if (ia->text_file->total_lines > 0)
+       {
+         ds_put_format (
+                        &s, ngettext (
+                                      "The selected file contains approximately %'lu line of text.  ",
+                                      "The selected file contains approximately %'lu lines of text.  ",
+                                      ia->text_file->total_lines),
+                        ia->text_file->total_lines);
+         ds_put_format (
+                        &s, ngettext (
+                                      "Only the first %zu line of the file will be shown for "
+                                      "preview purposes in the following screens.  ",
+                                      "Only the first %zu lines of the file will be shown for "
+                                      "preview purposes in the following screens.  ",
+                                      ia->text_file->line_cnt),
+                        ia->text_file->line_cnt);
+       }
+    }
+
+  ds_put_cstr (&s, _("You may choose below how much of the file should "
+                     "actually be imported."));
+
+  gtk_label_set_text (GTK_LABEL (get_widget_assert (builder, "intro-label")),
+                      ds_cstr (&s));
+  ds_destroy (&s);
+
+  if (gtk_grid_get_child_at (GTK_GRID (table), 1, 1) == NULL)
+    {
+      GtkWidget *hbox_n_cases = psppire_scanf_new (_("Only the first %4d cases"), &ia->n_cases_spin);
+      gtk_grid_attach (GTK_GRID (table), hbox_n_cases,
+                      1, 1,
+                      1, 1);
+    }
+
+  GtkAdjustment *adj = gtk_spin_button_get_adjustment (GTK_SPIN_BUTTON (ia->n_cases_spin));
+  gtk_adjustment_set_lower (adj, 1.0);
+
+  if (gtk_grid_get_child_at (GTK_GRID (table), 1, 2) == NULL)
+    {
+      GtkWidget *hbox_percent = psppire_scanf_new (_("Only the first %3d %% of file (approximately)"),
+                                                  &ia->percent_spin);
+
+      gtk_grid_attach (GTK_GRID (table), hbox_percent,
+                      1, 2,
+                      1, 1);
+    }
+
+  gtk_widget_show_all (table);
+
+
+  if (dir != IMPORT_ASSISTANT_FORWARDS)
+    return;
+
+  reset_intro_page (ia);
+  on_intro_amount_changed (ia);
+}
+
+/* Initializes IA's intro substructure. */
+void
+intro_page_create (PsppireImportAssistant *ia)
+{
+  GtkBuilder *builder = ia->text_builder;
+
+  GtkWidget *w = get_widget_assert (builder, "Intro");
+
+  ia->percent_spin = gtk_spin_button_new_with_range (0, 100, 1);
+
+  add_page_to_assistant (ia, w,  GTK_ASSISTANT_PAGE_CONTENT, _("Select the Lines to Import"));
+
+  ia->all_cases_button = get_widget_assert (builder, "import-all-cases");
+  ia->n_cases_button = get_widget_assert (builder, "import-n-cases");
+  ia->percent_button = get_widget_assert (builder, "import-percent");
+
+  g_signal_connect_swapped (ia->all_cases_button, "toggled",
+                           G_CALLBACK (on_intro_amount_changed), ia);
+  g_signal_connect_swapped (ia->n_cases_button, "toggled",
+                           G_CALLBACK (on_intro_amount_changed), ia);
+  g_signal_connect_swapped (ia->percent_button, "toggled",
+                           G_CALLBACK (on_intro_amount_changed), ia);
+
+  g_object_set_data (G_OBJECT (w), "on-leaving", intro_on_leave);
+  g_object_set_data (G_OBJECT (w), "on-entering", intro_on_enter);
+  g_object_set_data (G_OBJECT (w), "on-reset", reset_intro_page);
+}
+
+
+\f
+
+/* Chooses a name for each column on the separators page */
+static void
+choose_column_names (PsppireImportAssistant *ia)
+{
+  int i;
+  unsigned long int generated_name_count = 0;
+  char *encoding = NULL;
+  g_object_get (ia->text_file, "encoding", &encoding, NULL);
+  if (ia->dict)
+    dict_unref (ia->dict);
+  ia->dict = dict_create (encoding ? encoding : UTF8);
+  g_free (encoding);
+
+  for (i = 0;
+       i < gtk_tree_model_get_n_columns (GTK_TREE_MODEL (ia->delimiters_model)) - 1;
+       ++i)
+    {
+      const gchar *candidate_name = NULL;
+
+      if (gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (ia->variable_names_cb)))
+       {
+         candidate_name = psppire_delimited_text_get_header_title (PSPPIRE_DELIMITED_TEXT (ia->delimiters_model), i);
+       }
+
+      char *name = dict_make_unique_var_name (ia->dict,
+                                             candidate_name,
+                                             &generated_name_count);
+
+      dict_create_var_assert (ia->dict, name, 0);
+      free (name);
+    }
+}
+
+/* Called when the user toggles one of the separators
+   checkboxes. */
+static void
+on_separator_toggle (GtkToggleButton *toggle UNUSED,
+                     PsppireImportAssistant *ia)
+{
+  int i;
+  GSList *delimiters = NULL;
+  for (i = 0; i < SEPARATOR_CNT; i++)
+    {
+      const struct separator *s = &separators[i];
+      GtkWidget *button = get_widget_assert (ia->text_builder, s->name);
+      if (gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (button)))
+       {
+         delimiters = g_slist_prepend (delimiters,  GINT_TO_POINTER (s->c));
+       }
+    }
+
+  g_object_set (ia->delimiters_model, "delimiters", delimiters, NULL);
+
+  revise_fields_preview (ia);
+}
+
+
+/* Called when the user changes the entry field for custom
+   separators. */
+static void
+on_separators_custom_entry_notify (GObject *gobject UNUSED,
+                                   GParamSpec *arg1 UNUSED,
+                                   PsppireImportAssistant *ia)
+{
+  revise_fields_preview (ia);
+}
+
+/* Called when the user toggles the checkbox that enables custom
+   separators. */
+static void
+on_separators_custom_cb_toggle (GtkToggleButton *custom_cb,
+                                PsppireImportAssistant *ia)
+{
+  bool is_active = gtk_toggle_button_get_active (custom_cb);
+  gtk_widget_set_sensitive (ia->custom_entry, is_active);
+  revise_fields_preview (ia);
+}
+
+/* Called when the user changes the selection in the combo box
+   that selects a quote character. */
+static void
+on_quote_combo_change (GtkComboBox *combo, PsppireImportAssistant *ia)
+{
+  //  revise_fields_preview (ia);
+}
+
+/* Called when the user toggles the checkbox that enables
+   quoting. */
+static void
+on_quote_cb_toggle (GtkToggleButton *quote_cb, PsppireImportAssistant *ia)
+{
+  bool is_active = gtk_toggle_button_get_active (quote_cb);
+  gtk_widget_set_sensitive (ia->quote_combo, is_active);
+  revise_fields_preview (ia);
+}
+
+
+/* Called when the Reset button is clicked. */
+static void
+reset_separators_page (PsppireImportAssistant *ia)
+{
+  gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (ia->custom_cb), FALSE);
+  gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (ia->quote_cb), FALSE);
+  gtk_entry_set_text (GTK_ENTRY (ia->custom_entry), "");
+
+  for (gint i = 0; i < SEPARATOR_CNT; i++)
+    {
+      const struct separator *s = &separators[i];
+      GtkWidget *button = get_widget_assert (ia->text_builder, s->name);
+      gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (button), FALSE);
+    }
+
+  repopulate_delimiter_columns (ia);
+
+  revise_fields_preview (ia);
+  choose_likely_separators (ia);
+}
+
+/* Called just before the separators page becomes visible in the
+   assistant. */
+static void
+prepare_separators_page (PsppireImportAssistant *ia)
+{
+  gtk_tree_view_set_model (GTK_TREE_VIEW (ia->fields_tree_view),
+                          GTK_TREE_MODEL (ia->delimiters_model));
+
+  g_signal_connect_swapped (GTK_TREE_MODEL (ia->delimiters_model), "notify::delimiters",
+                       G_CALLBACK (reset_tree_view_model), ia);
+
+
+  reset_separators_page (ia);
+}
+
+
+/* Initializes IA's separators substructure. */
+void
+separators_page_create (PsppireImportAssistant *ia)
+{
+  GtkBuilder *builder = ia->text_builder;
+
+  size_t i;
+
+  GtkWidget *w = get_widget_assert (builder, "Separators");
+
+  g_object_set_data (G_OBJECT (w), "on-entering", prepare_separators_page);
+  g_object_set_data (G_OBJECT (w), "on-reset", reset_separators_page);
+
+  add_page_to_assistant (ia, w,   GTK_ASSISTANT_PAGE_CONTENT, _("Choose Separators"));
+
+  ia->custom_cb = get_widget_assert (builder, "custom-cb");
+  ia->custom_entry = get_widget_assert (builder, "custom-entry");
+  ia->quote_combo = get_widget_assert (builder, "quote-combo");
+  ia->quote_cb = get_widget_assert (builder, "quote-cb");
+
+  gtk_widget_set_sensitive (ia->custom_entry,
+                           gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (ia->custom_cb)));
+
+  gtk_combo_box_set_active (GTK_COMBO_BOX (ia->quote_combo), 0);
+
+  if (ia->fields_tree_view == NULL)
+    {
+      GtkWidget *scroller = get_widget_assert (ia->text_builder, "fields-scroller");
+      ia->fields_tree_view = gtk_tree_view_new ();
+      g_object_set (ia->fields_tree_view, "enable-search", FALSE, NULL);
+      gtk_container_add (GTK_CONTAINER (scroller), GTK_WIDGET (ia->fields_tree_view));
+      gtk_widget_show_all (scroller);
+    }
+
+  g_signal_connect (ia->quote_combo, "changed",
+                    G_CALLBACK (on_quote_combo_change), ia);
+  g_signal_connect (ia->quote_cb, "toggled",
+                    G_CALLBACK (on_quote_cb_toggle), ia);
+  g_signal_connect (ia->custom_entry, "notify::text",
+                    G_CALLBACK (on_separators_custom_entry_notify), ia);
+  g_signal_connect (ia->custom_cb, "toggled",
+                    G_CALLBACK (on_separators_custom_cb_toggle), ia);
+  for (i = 0; i < SEPARATOR_CNT; i++)
+    g_signal_connect (get_widget_assert (builder, separators[i].name),
+                      "toggled", G_CALLBACK (on_separator_toggle), ia);
+
+  reset_separators_page (ia);
+}
+
+\f
+
+static struct casereader_random_class my_casereader_class;
+
+static struct ccase *
+my_read (struct casereader *reader, void *aux, casenumber idx)
+{
+  PsppireImportAssistant *ia = PSPPIRE_IMPORT_ASSISTANT (aux);
+  GtkTreeModel *tm = GTK_TREE_MODEL (ia->delimiters_model);
+
+  GtkTreePath *tp = gtk_tree_path_new_from_indices (idx, -1);
+
+  const struct caseproto *proto = casereader_get_proto (reader);
+
+  GtkTreeIter iter;
+  struct ccase *c = NULL;
+  if (gtk_tree_model_get_iter (tm, &iter, tp))
+    {
+      c = case_create (proto);
+      int i;
+      for (i = 0 ; i < caseproto_get_n_widths (proto); ++i)
+       {
+         GValue value = {0};
+         gtk_tree_model_get_value (tm, &iter, i + 1, &value);
+
+         const struct variable *var = dict_get_var (ia->casereader_dict, i);
+
+         const gchar *ss = g_value_get_string (&value);
+         if (ss)
+           {
+             union value *v = case_data_rw (c, var);
+             /* In this reader we derive the union value from the
+                string in the tree_model. We retrieve the width and format
+                from a dictionary which is stored directly after
+                the reader creation. Changes in ia->dict in the
+                variable window are not reflected here and therefore
+                this is always compatible with the width in the
+                caseproto. See bug #58298 */
+             char *xx = data_in (ss_cstr (ss),
+                                 "UTF-8",
+                                 var_get_write_format (var)->type,
+                                 v, var_get_width (var), "UTF-8");
+
+             free (xx);
+           }
+         g_value_unset (&value);
+       }
+    }
+
+  gtk_tree_path_free (tp);
+
+  return c;
+}
+
+static void
+my_destroy (struct casereader *reader, void *aux)
+{
+  g_print ("%s:%d %p\n", __FILE__, __LINE__, reader);
+}
+
+static void
+my_advance (struct casereader *reader, void *aux, casenumber cnt)
+{
+  g_print ("%s:%d\n", __FILE__, __LINE__);
+}
+
+static struct casereader *
+textfile_create_reader (PsppireImportAssistant *ia)
+{
+  int n_vars = dict_get_var_cnt (ia->dict);
+
+  int i;
+
+  struct fmt_guesser **fg = XCALLOC (n_vars,  struct fmt_guesser *);
+  for (i = 0 ; i < n_vars; ++i)
+    {
+      fg[i] = fmt_guesser_create ();
+    }
+
+  gint n_rows = gtk_tree_model_iter_n_children (GTK_TREE_MODEL (ia->delimiters_model), NULL);
+
+  GtkTreeIter iter;
+  gboolean ok;
+  for (ok = gtk_tree_model_get_iter_first (GTK_TREE_MODEL (ia->delimiters_model), &iter);
+       ok;
+       ok = gtk_tree_model_iter_next (GTK_TREE_MODEL (ia->delimiters_model), &iter))
+    {
+      for (i = 0 ; i < n_vars; ++i)
+       {
+         gchar *s = NULL;
+         gtk_tree_model_get (GTK_TREE_MODEL (ia->delimiters_model), &iter, i+1, &s, -1);
+         if (s)
+           fmt_guesser_add (fg[i], ss_cstr (s));
+         free (s);
+       }
+    }
+
+  struct caseproto *proto = caseproto_create ();
+  for (i = 0 ; i < n_vars; ++i)
+    {
+      struct fmt_spec fs;
+      fmt_guesser_guess (fg[i], &fs);
+
+      fmt_fix (&fs, FMT_FOR_INPUT);
+
+      struct variable *var = dict_get_var (ia->dict, i);
+
+      int width = fmt_var_width (&fs);
+
+      var_set_width_and_formats (var, width,
+                                &fs, &fs);
+
+      proto = caseproto_add_width (proto, width);
+      fmt_guesser_destroy (fg[i]);
+    }
+
+  free (fg);
+
+  struct casereader *cr = casereader_create_random (proto, n_rows, &my_casereader_class,  ia);
+  /* Store the dictionary at this point when the casereader is created.
+     my_read depends on the dictionary to interpret the strings in the treeview.
+     This guarantees that the union value is produced according to the
+     caseproto in the reader. */
+  ia->casereader_dict = dict_clone (ia->dict);
+  caseproto_unref (proto);
+  return cr;
+}
+
+
+/* When during import the variable type is changed, the reader is reinitialized
+   based on the new dictionary with a fresh caseprototype. The default behaviour
+   when a variable type is changed and the column is resized is that the union
+   value is interpreted with new variable type and an overlay for that column
+   is generated. Here we reinit to the original reader based on strings.
+   As a result you can switch from string to numeric to string without loosing
+   the string information. */
+static void
+ia_variable_changed_cb (GObject *obj, gint var_num, guint what,
+                       const struct variable *oldvar, gpointer data)
+{
+  PsppireImportAssistant *ia  = PSPPIRE_IMPORT_ASSISTANT (data);
+
+  struct caseproto *proto = caseproto_create();
+  for (int i = 0; i < dict_get_var_cnt (ia->dict); i++)
+    {
+      const struct variable *var = dict_get_var (ia->dict, i);
+      int width = var_get_width (var);
+      proto = caseproto_add_width (proto, width);
+    }
+
+  gint n_rows = gtk_tree_model_iter_n_children (GTK_TREE_MODEL (ia->delimiters_model), NULL);
+
+  PsppireDataStore *store = NULL;
+  g_object_get (ia->data_sheet, "data-model", &store, NULL);
+
+  struct casereader *cr = casereader_create_random (proto, n_rows,
+                                                   &my_casereader_class, ia);
+  psppire_data_store_set_reader (store, cr);
+  dict_unref (ia->casereader_dict);
+  ia->casereader_dict = dict_clone (ia->dict);
+}
+
+
+/* Set the data model for both the data sheet and the variable sheet.  */
+void
+textfile_set_data_models (PsppireImportAssistant *ia)
+{
+  my_casereader_class.read = my_read;
+  my_casereader_class.destroy = my_destroy;
+  my_casereader_class.advance = my_advance;
+
+  struct casereader *reader = textfile_create_reader (ia);
+
+  PsppireDict *dict = psppire_dict_new_from_dict (ia->dict);
+  PsppireDataStore *store = psppire_data_store_new (dict);
+  psppire_data_store_set_reader (store, reader);
+  g_signal_connect (dict, "variable-changed",
+                    G_CALLBACK (ia_variable_changed_cb),
+                    ia);
+
+  g_object_set (ia->data_sheet, "data-model", store, NULL);
+  g_object_set (ia->var_sheet, "data-model", dict, NULL);
+}
diff --git a/src/ui/gui/psppire-import-textfile.h b/src/ui/gui/psppire-import-textfile.h
new file mode 100644 (file)
index 0000000..7a3c0f6
--- /dev/null
@@ -0,0 +1,37 @@
+#ifndef PSPPIRE_IMPORT_TEXTFILE_H
+#define PSPPIRE_IMPORT_TEXTFILE_H
+
+#include "psppire-import-assistant.h"
+
+struct separator
+{
+  const char *name;           /* Name (for use with get_widget_assert). */
+  gunichar c;                 /* Separator character. */
+};
+
+/* All the separators in the dialog box. */
+static const struct separator separators[] =
+  {
+    {"space",     ' '},
+    {"tab",       '\t'},
+    {"bang",      '!'},
+    {"colon",     ':'},
+    {"comma",     ','},
+    {"hyphen",    '-'},
+    {"pipe",      '|'},
+    {"semicolon", ';'},
+    {"slash",     '/'},
+  };
+
+#define SEPARATOR_CNT (sizeof separators / sizeof *separators)
+
+/* Initializes IA's intro substructure. */
+void intro_page_create (PsppireImportAssistant *ia);
+void first_line_page_create (PsppireImportAssistant *ia);
+void separators_page_create (PsppireImportAssistant *ia);
+
+/* Set the data model for both the data sheet and the variable sheet.  */
+void textfile_set_data_models (PsppireImportAssistant *ia);
+
+
+#endif
diff --git a/src/ui/gui/psppire-spreadsheet-data-model.c b/src/ui/gui/psppire-spreadsheet-data-model.c
new file mode 100644 (file)
index 0000000..1d0fbc1
--- /dev/null
@@ -0,0 +1,378 @@
+/* PSPPIRE - a graphical user interface for PSPP.
+   Copyright (C) 2020  Free Software Foundation
+
+   This program is free software: you can redistribute it and/or modify
+   it under the terms of the GNU General Public License as published by
+   the Free Software Foundation, either version 3 of the License, or
+   (at your option) any later version.
+
+   This program is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+   GNU General Public License for more details.
+
+   You should have received a copy of the GNU General Public License
+   along with this program.  If not, see <http://www.gnu.org/licenses/>. */
+
+#include <config.h>
+#include <glib.h>
+
+#include <stdint.h>
+
+#include <ui/gui/psppire-marshal.h>
+
+#include "psppire-spreadsheet-data-model.h"
+#include "data/spreadsheet-reader.h"
+
+
+static void psppire_spreadsheet_data_model_init (PsppireSpreadsheetDataModel *
+                                            spreadsheetModel);
+static void psppire_spreadsheet_data_model_class_init (PsppireSpreadsheetDataModelClass
+                                                  * class);
+
+static void psppire_spreadsheet_data_model_finalize (GObject * object);
+static void psppire_spreadsheet_data_model_dispose (GObject * object);
+
+static GObjectClass *parent_class = NULL;
+
+
+static void spreadsheet_tree_model_init (GtkTreeModelIface * iface);
+
+enum
+  {
+    ITEMS_CHANGED,
+    n_SIGNALS
+  };
+
+static guint signals [n_SIGNALS];
+
+G_DEFINE_TYPE_WITH_CODE (PsppireSpreadsheetDataModel,\
+                        psppire_spreadsheet_data_model,\
+                        G_TYPE_OBJECT,
+                        G_IMPLEMENT_INTERFACE (GTK_TYPE_TREE_MODEL,
+                                               spreadsheet_tree_model_init))
+
+/* Properties */
+enum
+{
+  PROP_0,
+  PROP_SPREADSHEET,
+  PROP_SHEET_NUMBER
+};
+
+static void
+psppire_spreadsheet_data_model_get_property (GObject         *object,
+                                             guint            prop_id,
+                                             GValue          *value,
+                                             GParamSpec      *pspec)
+{
+  PsppireSpreadsheetDataModel *sp = PSPPIRE_SPREADSHEET_DATA_MODEL (object);
+
+  switch (prop_id)
+    {
+    case PROP_SPREADSHEET:
+      g_value_set_pointer (value, sp->spreadsheet);
+      break;
+    case PROP_SHEET_NUMBER:
+      g_value_set_int (value, sp->sheet_number);
+      break;
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+      break;
+    };
+}
+
+
+static void
+psppire_spreadsheet_data_model_set_property (GObject * object,
+                                             guint prop_id,
+                                             const GValue * value,
+                                             GParamSpec * pspec)
+{
+  PsppireSpreadsheetDataModel *sp = PSPPIRE_SPREADSHEET_DATA_MODEL (object);
+
+  switch (prop_id)
+    {
+    case PROP_SPREADSHEET:
+      {
+      struct spreadsheet *old = sp->spreadsheet;
+      sp->spreadsheet = spreadsheet_ref (g_value_get_pointer (value));
+      if (old)
+        spreadsheet_unref (old);
+      g_signal_emit (sp, signals[ITEMS_CHANGED], 0, 0, 0, 0);
+      }
+      break;
+    case PROP_SHEET_NUMBER:
+      sp->sheet_number = g_value_get_int (value);
+      g_signal_emit (sp, signals[ITEMS_CHANGED], 0, 0, 0, 0);
+      break;
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+      break;
+    };
+}
+
+
+static void
+psppire_spreadsheet_data_model_dispose (GObject * object)
+{
+  PsppireSpreadsheetDataModel *spreadsheetModel = PSPPIRE_SPREADSHEET_DATA_MODEL (object);
+
+  if (spreadsheetModel->dispose_has_run)
+    return;
+
+  spreadsheetModel->dispose_has_run = TRUE;
+
+  spreadsheet_unref (spreadsheetModel->spreadsheet);
+  /* Chain up to the parent class */
+  G_OBJECT_CLASS (parent_class)->dispose (object);
+}
+
+static void
+psppire_spreadsheet_data_model_finalize (GObject * object)
+{
+  //  PsppireSpreadsheetDataModel *spreadsheetModel = PSPPIRE_SPREADSHEET_DATA_MODEL (object);
+}
+
+static void
+psppire_spreadsheet_data_model_class_init (PsppireSpreadsheetDataModelClass * class)
+{
+  GObjectClass *object_class;
+
+  GParamSpec *spreadsheet_spec = g_param_spec_pointer ("spreadsheet",
+                                                       "Spreadsheet",
+                                                       "The spreadsheet that this model represents",
+                                                       G_PARAM_CONSTRUCT_ONLY
+                                                       | G_PARAM_WRITABLE);
+
+
+  GParamSpec *sheet_number_spec = g_param_spec_int ("sheet-number",
+                                                    "Sheet Number",
+                                                    "The number of the sheet",
+                                                    0, G_MAXINT,
+                                                    0,
+                                                    G_PARAM_READABLE | G_PARAM_WRITABLE);
+
+  parent_class = g_type_class_peek_parent (class);
+  object_class = (GObjectClass *) class;
+
+  signals [ITEMS_CHANGED] =
+    g_signal_new ("items-changed",
+                 G_TYPE_FROM_CLASS (class),
+                 G_SIGNAL_RUN_FIRST,
+                 0,
+                 NULL, NULL,
+                 psppire_marshal_VOID__UINT_UINT_UINT,
+                 G_TYPE_NONE,
+                 3,
+                 G_TYPE_UINT,  /* Index of the start of the change */
+                 G_TYPE_UINT,  /* The number of items deleted */
+                 G_TYPE_UINT); /* The number of items inserted */
+
+
+
+  object_class->set_property = psppire_spreadsheet_data_model_set_property;
+  object_class->get_property = psppire_spreadsheet_data_model_get_property;
+
+  g_object_class_install_property (object_class,
+                                   PROP_SPREADSHEET, spreadsheet_spec);
+
+  g_object_class_install_property (object_class,
+                                   PROP_SHEET_NUMBER, sheet_number_spec);
+
+  object_class->finalize = psppire_spreadsheet_data_model_finalize;
+  object_class->dispose = psppire_spreadsheet_data_model_dispose;
+}
+
+
+static void
+psppire_spreadsheet_data_model_init (PsppireSpreadsheetDataModel * spreadsheetModel)
+{
+  spreadsheetModel->dispose_has_run = FALSE;
+  spreadsheetModel->stamp = g_random_int ();
+}
+
+
+GtkTreeModel *
+psppire_spreadsheet_data_model_new (struct spreadsheet *sp, gint sheet_number)
+{
+  return g_object_new (psppire_spreadsheet_data_model_get_type (),
+                       "spreadsheet", sp,
+                       "sheet-number", sheet_number,
+                       NULL);
+}
+
+\f
+
+static gint
+tree_model_n_columns (GtkTreeModel *model)
+{
+  PsppireSpreadsheetDataModel *sp = PSPPIRE_SPREADSHEET_DATA_MODEL (model);
+
+  return spreadsheet_get_sheet_n_columns (sp->spreadsheet, sp->sheet_number);
+}
+
+static GtkTreeModelFlags
+tree_model_get_flags (GtkTreeModel * model)
+{
+  g_return_val_if_fail (PSPPIRE_IS_SPREADSHEET_DATA_MODEL (model),
+                        (GtkTreeModelFlags) 0);
+
+  return GTK_TREE_MODEL_LIST_ONLY;
+}
+
+static GType
+tree_model_column_type (GtkTreeModel * model, gint index)
+{
+  g_print ("%s:%d %p\n", __FILE__, __LINE__, model);
+  g_return_val_if_fail (PSPPIRE_IS_SPREADSHEET_DATA_MODEL (model), (GType) 0);
+
+  return G_TYPE_STRING;
+}
+
+
+static gboolean
+tree_model_get_iter (GtkTreeModel * model, GtkTreeIter * iter,
+                     GtkTreePath * path)
+{
+  g_print ("%s:%d %p\n", __FILE__, __LINE__, model);
+  PsppireSpreadsheetDataModel *spreadsheetModel =
+    PSPPIRE_SPREADSHEET_DATA_MODEL (model);
+  gint *indices, depth;
+  gint n;
+
+  g_return_val_if_fail (path, FALSE);
+
+  depth = gtk_tree_path_get_depth (path);
+
+  g_return_val_if_fail (depth == 1, FALSE);
+
+  indices = gtk_tree_path_get_indices (path);
+
+  n = indices[0];
+
+  iter->stamp = spreadsheetModel->stamp;
+  iter->user_data = (gpointer) (intptr_t) n;
+
+  return TRUE;
+}
+
+static gboolean
+tree_model_iter_next (GtkTreeModel *model, GtkTreeIter *iter)
+{
+  g_print ("%s:%d %p\n", __FILE__, __LINE__, model);
+  PsppireSpreadsheetDataModel *spreadsheetModel = PSPPIRE_SPREADSHEET_DATA_MODEL (model);
+  g_assert (iter);
+  g_return_val_if_fail (iter->stamp == spreadsheetModel->stamp, FALSE);
+
+
+  iter->user_data = GINT_TO_POINTER (GPOINTER_TO_INT (iter->user_data) + 1);
+
+  return TRUE;
+}
+
+static void
+tree_model_get_value (GtkTreeModel *model, GtkTreeIter *iter,
+                      gint column, GValue *value)
+{
+  PsppireSpreadsheetDataModel *sp = PSPPIRE_SPREADSHEET_DATA_MODEL (model);
+  g_return_if_fail (column >= 0);
+  g_return_if_fail (iter->stamp == sp->stamp);
+
+  gint row = GPOINTER_TO_INT (iter->user_data);
+
+  g_value_init (value, G_TYPE_STRING);
+
+  char *x = spreadsheet_get_cell (sp->spreadsheet, sp->sheet_number, row, column);
+
+  g_value_take_string (value, x);
+}
+
+static gboolean
+tree_model_nth_child (GtkTreeModel *model, GtkTreeIter *iter,
+                      GtkTreeIter *parent, gint n)
+{
+  PsppireSpreadsheetDataModel *spreadsheetModel =
+    PSPPIRE_SPREADSHEET_DATA_MODEL (model);
+
+  if (parent)
+    return FALSE;
+
+  iter->stamp = spreadsheetModel->stamp;
+  iter->user_data = GINT_TO_POINTER (n);
+
+  return TRUE;
+}
+
+static gint
+tree_model_n_children (GtkTreeModel *model, GtkTreeIter *iter)
+{
+  PsppireSpreadsheetDataModel *sp = PSPPIRE_SPREADSHEET_DATA_MODEL (model);
+
+  if (iter == NULL)
+    {
+      return spreadsheet_get_sheet_n_rows (sp->spreadsheet, sp->sheet_number);
+    }
+
+  return 0;
+}
+
+static gboolean
+tree_model_iter_has_child (GtkTreeModel *model, GtkTreeIter *iter)
+{
+  g_print ("%s:%d %p\n", __FILE__, __LINE__, model);
+  return FALSE;
+}
+
+static GtkTreePath *
+tree_model_get_path (GtkTreeModel * model, GtkTreeIter * iter)
+{
+  g_print ("%s:%d %p\n", __FILE__, __LINE__, model);
+  PsppireSpreadsheetDataModel *spreadsheetModel =
+    PSPPIRE_SPREADSHEET_DATA_MODEL (model);
+  GtkTreePath *path;
+  gint index = GPOINTER_TO_INT (iter->user_data);
+
+  g_return_val_if_fail (iter->stamp == spreadsheetModel->stamp, NULL);
+
+  path = gtk_tree_path_new ();
+
+  gtk_tree_path_append_index (path, index);
+
+  return path;
+}
+
+
+static gboolean
+tree_model_children (GtkTreeModel *model, GtkTreeIter *iter, GtkTreeIter *parent)
+{
+  g_print ("%s:%d %p\n", __FILE__, __LINE__, model);
+  PsppireSpreadsheetDataModel *spreadsheetModel = PSPPIRE_SPREADSHEET_DATA_MODEL (model);
+
+  if (parent != NULL)
+    return FALSE;
+
+  iter->stamp = spreadsheetModel->stamp;
+  iter->user_data = 0;
+
+  return TRUE;
+}
+
+static void
+spreadsheet_tree_model_init (GtkTreeModelIface * iface)
+{
+  iface->get_flags = tree_model_get_flags;
+  iface->get_n_columns = tree_model_n_columns;
+  iface->get_column_type = tree_model_column_type;
+  iface->get_iter = tree_model_get_iter;
+  iface->iter_next = tree_model_iter_next;
+  iface->get_value = tree_model_get_value;
+
+  iface->iter_children = tree_model_children;
+  iface->iter_parent = NULL;
+
+  iface->get_path = tree_model_get_path;
+  iface->iter_has_child = tree_model_iter_has_child;
+  iface->iter_n_children = tree_model_n_children;
+  iface->iter_nth_child = tree_model_nth_child;
+}
diff --git a/src/ui/gui/psppire-spreadsheet-data-model.h b/src/ui/gui/psppire-spreadsheet-data-model.h
new file mode 100644 (file)
index 0000000..c06418b
--- /dev/null
@@ -0,0 +1,86 @@
+/* PSPPIRE - a graphical user interface for PSPP.
+   Copyright (C) 2020  Free Software Foundation
+
+   This program is free software: you can redistribute it and/or modify
+   it under the terms of the GNU General Public License as published by
+   the Free Software Foundation, either version 3 of the License, or
+   (at your option) any later version.
+
+   This program is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+   GNU General Public License for more details.
+
+   You should have received a copy of the GNU General Public License
+   along with this program.  If not, see <http://www.gnu.org/licenses/>. */
+
+
+#include <glib-object.h>
+#include <glib.h>
+
+#include <gtk/gtk.h>
+
+#ifndef __PSPPIRE_SPREADSHEET_DATA_MODEL_H__
+#define __PSPPIRE_SPREADSHEET_DATA_MODEL_H__
+
+G_BEGIN_DECLS
+
+
+#define PSPPIRE_TYPE_SPREADSHEET_DATA_MODEL (psppire_spreadsheet_data_model_get_type ())
+
+#define PSPPIRE_SPREADSHEET_DATA_MODEL(obj)    \
+                     (G_TYPE_CHECK_INSTANCE_CAST ((obj), \
+                   PSPPIRE_TYPE_SPREADSHEET_DATA_MODEL, PsppireSpreadsheetDataModel))
+
+#define PSPPIRE_SPREADSHEET_DATA_MODEL_CLASS(klass) \
+                     (G_TYPE_CHECK_CLASS_CAST ((klass), \
+                                PSPPIRE_TYPE_SPREADSHEET_DATA_MODEL, \
+                                 PsppireSpreadsheetDataModelClass))
+
+
+#define PSPPIRE_IS_SPREADSHEET_DATA_MODEL(obj) \
+                    (G_TYPE_CHECK_INSTANCE_TYPE ((obj), PSPPIRE_TYPE_SPREADSHEET_DATA_MODEL))
+
+#define PSPPIRE_IS_SPREADSHEET_DATA_MODEL_CLASS(klass) \
+                     (G_TYPE_CHECK_CLASS_TYPE ((klass), PSPPIRE_TYPE_SPREADSHEET_DATA_MODEL))
+
+
+#define PSPPIRE_SPREADSHEET_DATA_MODEL_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), \
+                                  PSPPIRE_TYPE_SPREADSHEET_DATA_MODEL, \
+                                  PsppireSpreadsheetDataModelClass))
+
+typedef struct _PsppireSpreadsheetDataModel       PsppireSpreadsheetDataModel;
+typedef struct _PsppireSpreadsheetDataModelClass  PsppireSpreadsheetDataModelClass;
+
+
+struct spreadsheet;
+
+struct _PsppireSpreadsheetDataModel
+{
+  GObject parent;
+
+
+  /*< private >*/
+  gint stamp;
+  struct spreadsheet *spreadsheet;
+  gint sheet_number;
+
+  gboolean dispose_has_run ;
+};
+
+
+struct _PsppireSpreadsheetDataModelClass
+{
+  GObjectClass parent_class;
+};
+
+
+GType psppire_spreadsheet_data_model_get_type (void) G_GNUC_CONST;
+
+
+GtkTreeModel * psppire_spreadsheet_data_model_new (struct spreadsheet *sp, gint sheet_number);
+
+
+G_END_DECLS
+
+#endif /* __PSPPIRE_SPREADSHEET_DATA_MODEL_H__ */
index d946197262fe4ac7c01db4db4473e2ca68dec57f..26c803f3a670a90d3923db358422e3a78c209634 100644 (file)
@@ -66,8 +66,12 @@ psppire_spreadsheet_model_set_property (GObject * object,
   switch (prop_id)
     {
     case PROP_SPREADSHEET:
-      spreadsheetModel->spreadsheet = g_value_get_pointer (value);
-      spreadsheet_ref (spreadsheetModel->spreadsheet);
+      {
+       struct spreadsheet *old = spreadsheetModel->spreadsheet;
+       spreadsheetModel->spreadsheet = spreadsheet_ref (g_value_get_pointer (value));
+       if (old)
+         spreadsheet_unref (old);
+      }
       break;
     default:
       G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
@@ -158,6 +162,9 @@ tree_model_column_type (GtkTreeModel * model, gint index)
   g_return_val_if_fail (PSPPIRE_IS_SPREADSHEET_MODEL (model), (GType) 0);
   g_return_val_if_fail (index < PSPPIRE_SPREADSHEET_MODEL_N_COLS, (GType) 0);
 
+  if (index ==  PSPPIRE_SPREADSHEET_MODEL_COL_SHEET_ROWS)
+    return G_TYPE_UINT;
+
   return G_TYPE_STRING;
 }
 
@@ -194,7 +201,8 @@ tree_model_iter_next (GtkTreeModel *model, GtkTreeIter *iter)
   g_assert (iter);
   g_return_val_if_fail (iter->stamp == spreadsheetModel->stamp, FALSE);
 
-  if ((intptr_t) iter->user_data >= spreadsheetModel->spreadsheet->n_sheets - 1)
+  if ((intptr_t) iter->user_data >=
+      spreadsheet_get_sheet_n_sheets (spreadsheetModel->spreadsheet) - 1)
     {
       iter->user_data = NULL;
       iter->stamp = 0;
@@ -216,11 +224,11 @@ tree_model_get_value (GtkTreeModel * model, GtkTreeIter * iter,
   g_return_if_fail (column < PSPPIRE_SPREADSHEET_MODEL_N_COLS);
   g_return_if_fail (iter->stamp == spreadsheetModel->stamp);
 
-  g_value_init (value, G_TYPE_STRING);
   switch (column)
     {
     case PSPPIRE_SPREADSHEET_MODEL_COL_NAME:
       {
+        g_value_init (value, G_TYPE_STRING);
         const char *x =
           spreadsheet_get_sheet_name (spreadsheetModel->spreadsheet,
                                       (intptr_t) iter->user_data);
@@ -230,6 +238,7 @@ tree_model_get_value (GtkTreeModel * model, GtkTreeIter * iter,
       break;
     case PSPPIRE_SPREADSHEET_MODEL_COL_RANGE:
       {
+        g_value_init (value, G_TYPE_STRING);
         char *x =
           spreadsheet_get_sheet_range (spreadsheetModel->spreadsheet,
                                        (intptr_t) iter->user_data);
@@ -238,6 +247,26 @@ tree_model_get_value (GtkTreeModel * model, GtkTreeIter * iter,
        g_free (x);
       }
       break;
+    case PSPPIRE_SPREADSHEET_MODEL_COL_SHEET_ROWS:
+      {
+        g_value_init (value, G_TYPE_UINT);
+        unsigned int rows =
+          spreadsheet_get_sheet_n_rows (spreadsheetModel->spreadsheet,
+                                        (intptr_t) iter->user_data);
+
+        g_value_set_uint (value, rows);
+      }
+      break;
+    case PSPPIRE_SPREADSHEET_MODEL_COL_SHEET_COLUMNS:
+      {
+        g_value_init (value, G_TYPE_UINT);
+        unsigned int columns =
+          spreadsheet_get_sheet_n_columns (spreadsheetModel->spreadsheet,
+                                           (intptr_t) iter->user_data);
+
+        g_value_set_uint (value, columns);
+      }
+      break;
     default:
       g_error ("%s:%d Invalid column in spreadsheet model",
               __FILE__, __LINE__);
@@ -255,7 +284,7 @@ tree_model_nth_child (GtkTreeModel * model, GtkTreeIter * iter,
   if (parent)
     return FALSE;
 
-  if (n >= spreadsheetModel->spreadsheet->n_sheets)
+  if (n >= spreadsheet_get_sheet_n_sheets (spreadsheetModel->spreadsheet))
     return FALSE;
 
   iter->stamp = spreadsheetModel->stamp;
@@ -271,7 +300,7 @@ tree_model_n_children (GtkTreeModel * model, GtkTreeIter * iter)
     PSPPIRE_SPREADSHEET_MODEL (model);
 
   if (iter == NULL)
-    return spreadsheetModel->spreadsheet->n_sheets;
+    return spreadsheet_get_sheet_n_sheets (spreadsheetModel->spreadsheet);
 
   return 0;
 }
index 0366ad4bc18db7c59cf2bb141293994b164cec0a..e60e3fa99cf66e62c43e5f6fdfe88d833d3b2a0a 100644 (file)
@@ -87,6 +87,8 @@ enum
 {
   PSPPIRE_SPREADSHEET_MODEL_COL_NAME,
   PSPPIRE_SPREADSHEET_MODEL_COL_RANGE,
+  PSPPIRE_SPREADSHEET_MODEL_COL_SHEET_ROWS,
+  PSPPIRE_SPREADSHEET_MODEL_COL_SHEET_COLUMNS,
   PSPPIRE_SPREADSHEET_MODEL_N_COLS
 };
 
diff --git a/src/ui/gui/spreadsheet-import.ui b/src/ui/gui/spreadsheet-import.ui
new file mode 100644 (file)
index 0000000..9d225f3
--- /dev/null
@@ -0,0 +1,261 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Generated with glade 3.22.1 -->
+<!-- PSPP - a program for statistical analysis. -->
+<!-- Copyright (C) 2017, 2020 Free Software Foundation, Inc. -->
+<!-- This program is free software: you can redistribute it and/or modify -->
+<!-- it under the terms of the GNU General Public License as published by -->
+<!-- the Free Software Foundation, either version 3 of the License, or -->
+<!-- (at your option) any later version. -->
+<!-- This program is distributed in the hope that it will be useful, -->
+<!-- but WITHOUT ANY WARRANTY; without even the implied warranty of -->
+<!-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the -->
+<!-- GNU General Public License for more details. -->
+<!-- You should have received a copy of the GNU General Public License -->
+<!-- along with this program.  If not, see <http://www.gnu.org/licenses/>. -->
+<interface>
+  <requires lib="gtk+" version="3.22"/>
+  <requires lib="psppire" version="0.0"/>
+  <object class="GtkAdjustment" id="adjustment0">
+    <property name="step_increment">1</property>
+  </object>
+  <object class="GtkAdjustment" id="adjustment1">
+    <property name="step_increment">1</property>
+  </object>
+  <object class="GtkAdjustment" id="adjustment2">
+    <property name="step_increment">1</property>
+  </object>
+  <object class="GtkAdjustment" id="adjustment3">
+    <property name="step_increment">1</property>
+  </object>
+  <object class="GtkBox" id="Spreadsheet-Importer">
+    <property name="visible">True</property>
+    <property name="can_focus">False</property>
+    <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
+    <property name="orientation">vertical</property>
+    <property name="spacing">12</property>
+    <child>
+      <object class="GtkLabel" id="intro-label1">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
+        <property name="label" translatable="yes">Choose below the sheet number and the cell range that you wish to import.</property>
+        <property name="wrap">True</property>
+      </object>
+      <packing>
+        <property name="expand">False</property>
+        <property name="fill">True</property>
+        <property name="position">0</property>
+      </packing>
+    </child>
+    <child>
+      <object class="GtkFrame" id="frame1">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
+        <property name="label_xalign">0</property>
+        <property name="shadow_type">none</property>
+        <child>
+          <object class="GtkGrid" id="table3">
+            <property name="visible">True</property>
+            <property name="can_focus">False</property>
+            <property name="margin_start">12</property>
+            <property name="row_spacing">5</property>
+            <property name="column_spacing">20</property>
+            <child>
+              <object class="GtkBox" id="box-left">
+                <property name="visible">True</property>
+                <property name="can_focus">False</property>
+                <property name="orientation">vertical</property>
+                <property name="spacing">5</property>
+                <child>
+                  <object class="GtkBox" id="file-label-box">
+                    <property name="visible">True</property>
+                    <property name="can_focus">False</property>
+                    <child>
+                      <object class="GtkLabel" id="file-indicator-label">
+                        <property name="visible">True</property>
+                        <property name="can_focus">False</property>
+                        <property name="label" translatable="yes">Importing file: </property>
+                      </object>
+                      <packing>
+                        <property name="expand">False</property>
+                        <property name="fill">False</property>
+                        <property name="position">0</property>
+                      </packing>
+                    </child>
+                    <child>
+                      <object class="GtkLabel" id="file-name-label">
+                        <property name="visible">True</property>
+                        <property name="can_focus">True</property>
+                      </object>
+                      <packing>
+                        <property name="expand">True</property>
+                        <property name="fill">True</property>
+                        <property name="position">1</property>
+                      </packing>
+                    </child>
+                  </object>
+                  <packing>
+                    <property name="expand">False</property>
+                    <property name="fill">True</property>
+                    <property name="position">0</property>
+                  </packing>
+                </child>
+                <child>
+                  <object class="GtkComboBox" id="sheet-entry">
+                    <property name="visible">True</property>
+                    <property name="can_focus">True</property>
+                    <property name="active">0</property>
+                  </object>
+                  <packing>
+                    <property name="expand">False</property>
+                    <property name="fill">True</property>
+                    <property name="position">1</property>
+                  </packing>
+                </child>
+                <child>
+                  <object class="GtkCheckButton" id="readnames-checkbox">
+                    <property name="label" translatable="yes">Use the first selected row as _variable names</property>
+                    <property name="visible">True</property>
+                    <property name="can_focus">True</property>
+                    <property name="receives_default">False</property>
+                    <property name="use_underline">True</property>
+                    <property name="image_position">right</property>
+                    <property name="draw_indicator">True</property>
+                  </object>
+                  <packing>
+                    <property name="expand">False</property>
+                    <property name="fill">True</property>
+                    <property name="position">2</property>
+                  </packing>
+                </child>
+              </object>
+              <packing>
+                <property name="left_attach">0</property>
+                <property name="top_attach">0</property>
+              </packing>
+            </child>
+            <child>
+              <object class="GtkGrid" id="table-right">
+                <property name="visible">True</property>
+                <property name="can_focus">False</property>
+                <property name="margin_start">12</property>
+                <child>
+                  <object class="GtkBox" id="cell-ref-box">
+                    <property name="visible">True</property>
+                    <property name="can_focus">False</property>
+                    <child>
+                      <object class="GtkLabel" id="cell-range-label">
+                        <property name="visible">True</property>
+                        <property name="can_focus">False</property>
+                        <property name="label" translatable="yes">_Cells: </property>
+                        <property name="use_underline">True</property>
+                        <property name="mnemonic_widget">cell-range-entry</property>
+                      </object>
+                      <packing>
+                        <property name="expand">False</property>
+                        <property name="fill">False</property>
+                        <property name="position">0</property>
+                      </packing>
+                    </child>
+                    <child>
+                      <object class="GtkEntry" id="cell-range-entry">
+                        <property name="visible">True</property>
+                        <property name="can_focus">True</property>
+                      </object>
+                      <packing>
+                        <property name="expand">True</property>
+                        <property name="fill">True</property>
+                        <property name="position">1</property>
+                      </packing>
+                    </child>
+                  </object>
+                  <packing>
+                    <property name="left_attach">0</property>
+                    <property name="top_attach">0</property>
+                    <property name="width">2</property>
+                  </packing>
+                </child>
+                <child>
+                  <object class="GtkSpinButton" id="sb0">
+                    <property name="visible">True</property>
+                    <property name="can_focus">True</property>
+                    <property name="adjustment">adjustment0</property>
+                  </object>
+                  <packing>
+                    <property name="left_attach">0</property>
+                    <property name="top_attach">1</property>
+                  </packing>
+                </child>
+                <child>
+                  <object class="GtkSpinButton" id="sb1">
+                    <property name="visible">True</property>
+                    <property name="can_focus">True</property>
+                    <property name="adjustment">adjustment1</property>
+                    <property name="numeric">True</property>
+                  </object>
+                  <packing>
+                    <property name="left_attach">1</property>
+                    <property name="top_attach">1</property>
+                  </packing>
+                </child>
+                <child>
+                  <object class="GtkSpinButton" id="sb2">
+                    <property name="visible">True</property>
+                    <property name="can_focus">True</property>
+                    <property name="adjustment">adjustment2</property>
+                  </object>
+                  <packing>
+                    <property name="left_attach">0</property>
+                    <property name="top_attach">2</property>
+                  </packing>
+                </child>
+                <child>
+                  <object class="GtkSpinButton" id="sb3">
+                    <property name="visible">True</property>
+                    <property name="can_focus">True</property>
+                    <property name="adjustment">adjustment3</property>
+                    <property name="numeric">True</property>
+                  </object>
+                  <packing>
+                    <property name="left_attach">1</property>
+                    <property name="top_attach">2</property>
+                  </packing>
+                </child>
+              </object>
+              <packing>
+                <property name="left_attach">1</property>
+                <property name="top_attach">0</property>
+              </packing>
+            </child>
+            <child>
+              <object class="SswSheet" id="preview-sheet">
+                <property name="visible">True</property>
+                <property name="can_focus">True</property>
+              </object>
+              <packing>
+                <property name="left_attach">0</property>
+                <property name="top_attach">1</property>
+                <property name="width">2</property>
+              </packing>
+            </child>
+          </object>
+        </child>
+        <child type="label">
+          <object class="GtkLabel" id="label4">
+            <property name="visible">True</property>
+            <property name="can_focus">False</property>
+            <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
+            <property name="label" translatable="yes">&lt;b&gt;Cells to Import&lt;/b&gt;</property>
+            <property name="use_markup">True</property>
+          </object>
+        </child>
+      </object>
+      <packing>
+        <property name="expand">True</property>
+        <property name="fill">True</property>
+        <property name="position">1</property>
+      </packing>
+    </child>
+  </object>
+</interface>
index 084f1f5c37954c6219ef1d4723b979fead244891..3f66e53c21176cefcfefae688a9e452418708e18 100644 (file)
@@ -1,23 +1,20 @@
 <?xml version="1.0" encoding="UTF-8"?>
+<!-- Generated with glade 3.22.1 -->
 <!-- PSPP - a program for statistical analysis. -->
-<!-- Copyright (C) 2017 Free Software Foundation, Inc. -->
-
+<!-- Copyright (C) 2017, 2020 Free Software Foundation, Inc. -->
 <!-- This program is free software: you can redistribute it and/or modify -->
 <!-- it under the terms of the GNU General Public License as published by -->
 <!-- the Free Software Foundation, either version 3 of the License, or -->
 <!-- (at your option) any later version. -->
-
 <!-- This program is distributed in the hope that it will be useful, -->
 <!-- but WITHOUT ANY WARRANTY; without even the implied warranty of -->
 <!-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the -->
 <!-- GNU General Public License for more details. -->
-
 <!-- You should have received a copy of the GNU General Public License -->
 <!-- along with this program.  If not, see <http://www.gnu.org/licenses/>. -->
-
-<!-- Generated with glade 3.18.3 -->
 <interface>
   <requires lib="gtk+" version="3.22"/>
+  <requires lib="psppire" version="0.0"/>
   <object class="GtkBox" id="FirstLine">
     <property name="visible">True</property>
     <property name="can_focus">False</property>
@@ -56,7 +53,6 @@
         <property name="can_focus">True</property>
         <property name="receives_default">False</property>
         <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
-        <property name="xalign">0.5</property>
         <property name="draw_indicator">True</property>
       </object>
       <packing>
@@ -93,7 +89,7 @@
         <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
         <property name="orientation">vertical</property>
         <property name="position">94</property>
-       <property name="wide-handle">True</property>
+        <property name="wide_handle">True</property>
         <child>
           <object class="GtkFrame" id="frame4">
             <property name="visible">True</property>
             <property name="label_xalign">0</property>
             <property name="shadow_type">none</property>
             <child>
-              <object class="GtkScrolledWindow" id="vars-scroller">
+              <object class="PsppireVariableSheet" id="variable-sheet">
                 <property name="visible">True</property>
                 <property name="can_focus">True</property>
-                <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
-                <property name="margin_start">12</property>
-               <property name="margin-bottom">12</property>
-                <child>
-                  <placeholder/>
-                </child>
+                <property name="editable">True</property>
               </object>
             </child>
             <child type="label">
             <property name="visible">True</property>
             <property name="can_focus">False</property>
             <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
+            <property name="margin_top">12</property>
             <property name="label_xalign">0</property>
             <property name="shadow_type">none</property>
-            <property name="margin_top">12</property>
             <child>
-              <object class="GtkScrolledWindow" id="data-scroller">
+              <object class="PsppireDataSheet" id="data-sheet">
                 <property name="visible">True</property>
                 <property name="can_focus">True</property>
-                <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
-                <property name="margin_start">12</property>
+                <property name="editable">True</property>
               </object>
             </child>
             <child type="label">
             <property name="can_focus">False</property>
             <property name="margin_start">12</property>
             <property name="orientation">vertical</property>
+            <property name="column_spacing">3</property>
             <child>
               <object class="GtkRadioButton" id="import-all-cases">
                 <property name="visible">True</property>
                 <property name="can_focus">True</property>
                 <property name="receives_default">False</property>
-                <property name="xalign">0</property>
                 <property name="active">True</property>
                 <property name="draw_indicator">True</property>
               </object>
                 <property name="visible">True</property>
                 <property name="can_focus">True</property>
                 <property name="receives_default">False</property>
-                <property name="xalign">0</property>
                 <property name="draw_indicator">True</property>
                 <property name="group">import-all-cases</property>
               </object>
                 <property name="visible">True</property>
                 <property name="can_focus">True</property>
                 <property name="receives_default">False</property>
-                <property name="xalign">0</property>
                 <property name="draw_indicator">True</property>
                 <property name="group">import-all-cases</property>
               </object>
               <object class="GtkLabel" id="label1">
                 <property name="visible">True</property>
                 <property name="can_focus">False</property>
-                <property name="xalign">0</property>
                 <property name="label" translatable="yes">All cases</property>
+                <property name="xalign">0</property>
               </object>
               <packing>
                 <property name="left_attach">1</property>
                     <property name="receives_default">False</property>
                     <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
                     <property name="use_underline">True</property>
-                    <property name="xalign">0.5</property>
                     <property name="draw_indicator">True</property>
                   </object>
                   <packing>
                     <property name="receives_default">False</property>
                     <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
                     <property name="use_underline">True</property>
-                    <property name="xalign">0.5</property>
                     <property name="draw_indicator">True</property>
                   </object>
                   <packing>
                     <property name="receives_default">False</property>
                     <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
                     <property name="use_underline">True</property>
-                    <property name="xalign">0.5</property>
                     <property name="draw_indicator">True</property>
                   </object>
                   <packing>
                     <property name="receives_default">False</property>
                     <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
                     <property name="use_underline">True</property>
-                    <property name="xalign">0.5</property>
                     <property name="draw_indicator">True</property>
                   </object>
                   <packing>
                     <property name="receives_default">False</property>
                     <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
                     <property name="use_underline">True</property>
-                    <property name="xalign">0.5</property>
                     <property name="draw_indicator">True</property>
                   </object>
                   <packing>
                     <property name="receives_default">False</property>
                     <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
                     <property name="use_underline">True</property>
-                    <property name="xalign">0.5</property>
                     <property name="draw_indicator">True</property>
                   </object>
                   <packing>
                     <property name="receives_default">False</property>
                     <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
                     <property name="use_underline">True</property>
-                    <property name="xalign">0.5</property>
                     <property name="draw_indicator">True</property>
                   </object>
                   <packing>
                     <property name="receives_default">False</property>
                     <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
                     <property name="use_underline">True</property>
-                    <property name="xalign">0.5</property>
                     <property name="draw_indicator">True</property>
                   </object>
                   <packing>
                     <property name="receives_default">False</property>
                     <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
                     <property name="use_underline">True</property>
-                    <property name="xalign">0.5</property>
                     <property name="draw_indicator">True</property>
                   </object>
                   <packing>
                     <property name="receives_default">False</property>
                     <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
                     <property name="use_underline">True</property>
-                    <property name="xalign">0.5</property>
                     <property name="draw_indicator">True</property>
                   </object>
                   <packing>
                     <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
                     <property name="has_frame">False</property>
                     <property name="has_entry">True</property>
-                    <child internal-child="entry">
-                      <object class="GtkEntry" id="combobox-entry1">
-                        <property name="can_focus">False</property>
-                      </object>
-                    </child>
                     <items>
                       <item translatable="no">'"</item>
                       <item translatable="no">'</item>
                       <item translatable="no">"</item>
                     </items>
+                    <child internal-child="entry">
+                      <object class="GtkEntry" id="combobox-entry1">
+                        <property name="can_focus">False</property>
+                      </object>
+                    </child>
                   </object>
                   <packing>
                     <property name="left_attach">1</property>
                     <property name="can_focus">True</property>
                     <property name="receives_default">False</property>
                     <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
-                    <property name="xalign">0.5</property>
                     <property name="draw_indicator">True</property>
                   </object>
                   <packing>
       </packing>
     </child>
   </object>
-  <object class="GtkBox" id="Spreadsheet-Importer">
-    <property name="visible">True</property>
-    <property name="can_focus">False</property>
-    <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
-    <property name="orientation">vertical</property>
-    <property name="spacing">12</property>
-    <child>
-      <object class="GtkLabel" id="intro-label1">
-        <property name="visible">True</property>
-        <property name="can_focus">False</property>
-        <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
-        <property name="label" translatable="yes">Enter below the sheet number and the cell range which you wish to import.</property>
-        <property name="wrap">True</property>
-      </object>
-      <packing>
-        <property name="expand">False</property>
-        <property name="fill">True</property>
-        <property name="position">0</property>
-      </packing>
-    </child>
-    <child>
-      <object class="GtkFrame" id="frame1">
-        <property name="visible">True</property>
-        <property name="can_focus">False</property>
-        <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
-        <property name="label_xalign">0</property>
-        <property name="shadow_type">none</property>
-        <child>
-          <object class="GtkGrid" id="table3">
-            <property name="visible">True</property>
-            <property name="can_focus">False</property>
-            <property name="margin_start">12</property>
-            <child>
-              <object class="GtkEntry" id="cell-range-entry">
-                <property name="visible">True</property>
-                <property name="can_focus">True</property>
-                <property name="invisible_char">●</property>
-              </object>
-              <packing>
-                <property name="left_attach">1</property>
-                <property name="top_attach">0</property>
-              </packing>
-            </child>
-            <child>
-              <object class="GtkComboBox" id="sheet-entry">
-                <property name="visible">True</property>
-                <property name="can_focus">False</property>
-                <property name="active">0</property>
-              </object>
-              <packing>
-                <property name="left_attach">1</property>
-                <property name="top_attach">1</property>
-              </packing>
-            </child>
-            <child>
-              <object class="GtkLabel" id="cell-range-label">
-                <property name="visible">True</property>
-                <property name="can_focus">False</property>
-                <property name="xalign">1</property>
-                <property name="label" translatable="yes">_Cells: </property>
-                <property name="use_underline">True</property>
-                <property name="mnemonic_widget">cell-range-entry</property>
-              </object>
-              <packing>
-                <property name="left_attach">0</property>
-                <property name="top_attach">0</property>
-              </packing>
-            </child>
-            <child>
-              <object class="GtkLabel" id="sheet-label">
-                <property name="visible">True</property>
-                <property name="can_focus">False</property>
-                <property name="xalign">1</property>
-                <property name="label" translatable="yes">_Sheet Index: </property>
-                <property name="use_underline">True</property>
-                <property name="mnemonic_widget">sheet-entry</property>
-              </object>
-              <packing>
-                <property name="left_attach">0</property>
-                <property name="top_attach">1</property>
-              </packing>
-            </child>
-            <child>
-              <object class="GtkCheckButton" id="readnames-checkbox">
-                <property name="label" translatable="yes">Use first row as _variable names</property>
-                <property name="visible">True</property>
-                <property name="can_focus">True</property>
-                <property name="receives_default">False</property>
-                <property name="use_underline">True</property>
-                <property name="xalign">0</property>
-                <property name="image_position">right</property>
-                <property name="draw_indicator">True</property>
-              </object>
-              <packing>
-                <property name="left_attach">0</property>
-                <property name="top_attach">2</property>
-                <property name="width">2</property>
-              </packing>
-            </child>
-          </object>
-        </child>
-        <child type="label">
-          <object class="GtkLabel" id="label4">
-            <property name="visible">True</property>
-            <property name="can_focus">False</property>
-            <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
-            <property name="label" translatable="yes">&lt;b&gt;Cells to Import&lt;/b&gt;</property>
-            <property name="use_markup">True</property>
-          </object>
-        </child>
-      </object>
-      <packing>
-        <property name="expand">False</property>
-        <property name="fill">True</property>
-        <property name="position">1</property>
-      </packing>
-    </child>
-  </object>
 </interface>
index 66e17bb2a7010b4cdbd190f20932ee9fec18faec..241f0afd4d6779178979f70e1619fc688f74965d 100644 (file)
@@ -19,6 +19,7 @@
 check_PROGRAMS += \
        tests/data/datasheet-test \
        tests/data/sack \
+       tests/data/spreadsheet-test \
        tests/data/inexactify \
        tests/language/lexer/command-name-test \
        tests/language/lexer/scan-test \
@@ -65,6 +66,12 @@ tests_data_sack_SOURCES = \
 tests_data_sack_LDADD = src/libpspp-core.la
 tests_data_sack_CFLAGS = $(AM_CFLAGS)
 
+
+tests_data_spreadsheet_test_SOURCES = \
+       tests/data/spreadsheet-test.c
+tests_data_spreadsheet_test_LDADD = src/libpspp-core.la
+tests_data_spreadsheet_test_CFLAGS = $(AM_CFLAGS)
+
 tests_libpspp_line_reader_test_SOURCES = tests/libpspp/line-reader-test.c
 tests_libpspp_line_reader_test_LDADD = src/libpspp-core.la
 
@@ -258,6 +265,18 @@ tests_ui_syntax_gen_test_LDADD = \
 
 EXTRA_DIST += \
        tests/coverage.sh \
+       tests/data/simple.ods \
+       tests/data/simple.gnumeric \
+       tests/data/sparse.ods \
+       tests/data/sparse.gnumeric \
+       tests/data/holey.ods \
+       tests/data/holey.gnumeric \
+       tests/data/multisheet.ods \
+       tests/data/multisheet.gnumeric \
+       tests/data/repeating.ods \
+       tests/data/repeating.gnumeric \
+       tests/data/one-thousand-by-fifty-three.ods \
+       tests/data/one-thousand-by-fifty-three.gnumeric \
        tests/data/CVE-2017-10791.sav \
        tests/data/CVE-2017-10792.sav \
        tests/data/bcd-in.expected.cmp.gz \
@@ -300,6 +319,7 @@ TESTSUITE_AT = \
        tests/data/data-in.at \
        tests/data/data-out.at \
        tests/data/datasheet-test.at \
+       tests/data/spreadsheet-test.at \
        tests/data/dictionary.at \
        tests/data/file.at \
        tests/data/format-guesser.at \
diff --git a/tests/data/holey.gnumeric b/tests/data/holey.gnumeric
new file mode 100644 (file)
index 0000000..dea5ce0
Binary files /dev/null and b/tests/data/holey.gnumeric differ
diff --git a/tests/data/holey.ods b/tests/data/holey.ods
new file mode 100644 (file)
index 0000000..46deafb
Binary files /dev/null and b/tests/data/holey.ods differ
diff --git a/tests/data/multisheet.gnumeric b/tests/data/multisheet.gnumeric
new file mode 100644 (file)
index 0000000..424c28b
--- /dev/null
@@ -0,0 +1,245 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<gnm:Workbook xmlns:gnm="http://www.gnumeric.org/v10.dtd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.gnumeric.org/v9.xsd">
+  <gnm:Version Epoch="1" Major="12" Minor="44" Full="1.12.44"/>
+  <gnm:Attributes>
+    <gnm:Attribute>
+      <gnm:name>WorkbookView::show_horizontal_scrollbar</gnm:name>
+      <gnm:value>TRUE</gnm:value>
+    </gnm:Attribute>
+    <gnm:Attribute>
+      <gnm:name>WorkbookView::show_vertical_scrollbar</gnm:name>
+      <gnm:value>TRUE</gnm:value>
+    </gnm:Attribute>
+    <gnm:Attribute>
+      <gnm:name>WorkbookView::show_notebook_tabs</gnm:name>
+      <gnm:value>TRUE</gnm:value>
+    </gnm:Attribute>
+    <gnm:Attribute>
+      <gnm:name>WorkbookView::do_auto_completion</gnm:name>
+      <gnm:value>TRUE</gnm:value>
+    </gnm:Attribute>
+    <gnm:Attribute>
+      <gnm:name>WorkbookView::is_protected</gnm:name>
+      <gnm:value>FALSE</gnm:value>
+    </gnm:Attribute>
+  </gnm:Attributes>
+  <office:document-meta xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:meta="urn:oasis:names:tc:opendocument:xmlns:meta:1.0" xmlns:ooo="http://openoffice.org/2004/office" office:version="1.2">
+    <office:meta>
+      <dc:date>2020-07-29T16:17:03Z</dc:date>
+      <meta:creation-date>2020-07-29T16:16:01Z</meta:creation-date>
+    </office:meta>
+  </office:document-meta>
+  <gnm:Calculation ManualRecalc="0" EnableIteration="1" MaxIterations="100" IterationTolerance="0.001" FloatRadix="2" FloatDigits="53"/>
+  <gnm:SheetNameIndex>
+    <gnm:SheetName gnm:Cols="256" gnm:Rows="65536">Sheet1</gnm:SheetName>
+    <gnm:SheetName gnm:Cols="256" gnm:Rows="65536">Sheet2</gnm:SheetName>
+    <gnm:SheetName gnm:Cols="256" gnm:Rows="65536">Sheet3</gnm:SheetName>
+  </gnm:SheetNameIndex>
+  <gnm:Geometry Width="1438" Height="690"/>
+  <gnm:Sheets>
+    <gnm:Sheet DisplayFormulas="0" HideZero="0" HideGrid="0" HideColHeader="0" HideRowHeader="0" DisplayOutlines="1" OutlineSymbolsBelow="1" OutlineSymbolsRight="1" Visibility="GNM_SHEET_VISIBILITY_VISIBLE" GridColor="0:0:0">
+      <gnm:Name>Sheet1</gnm:Name>
+      <gnm:MaxCol>0</gnm:MaxCol>
+      <gnm:MaxRow>0</gnm:MaxRow>
+      <gnm:Zoom>1</gnm:Zoom>
+      <gnm:Names>
+        <gnm:Name>
+          <gnm:name>Print_Area</gnm:name>
+          <gnm:value>#REF!</gnm:value>
+          <gnm:position>A1</gnm:position>
+        </gnm:Name>
+        <gnm:Name>
+          <gnm:name>Sheet_Title</gnm:name>
+          <gnm:value>&quot;Sheet1&quot;</gnm:value>
+          <gnm:position>A1</gnm:position>
+        </gnm:Name>
+      </gnm:Names>
+      <gnm:PrintInformation>
+        <gnm:Margins>
+          <gnm:top Points="120" PrefUnit="mm"/>
+          <gnm:bottom Points="120" PrefUnit="mm"/>
+          <gnm:left Points="72" PrefUnit="mm"/>
+          <gnm:right Points="72" PrefUnit="mm"/>
+          <gnm:header Points="72" PrefUnit="mm"/>
+          <gnm:footer Points="72" PrefUnit="mm"/>
+        </gnm:Margins>
+        <gnm:Scale type="percentage" percentage="100"/>
+        <gnm:vcenter value="0"/>
+        <gnm:hcenter value="0"/>
+        <gnm:grid value="0"/>
+        <gnm:even_if_only_styles value="0"/>
+        <gnm:monochrome value="0"/>
+        <gnm:draft value="0"/>
+        <gnm:titles value="0"/>
+        <gnm:do_not_print value="0"/>
+        <gnm:print_range value="GNM_PRINT_ACTIVE_SHEET"/>
+        <gnm:order>d_then_r</gnm:order>
+        <gnm:orientation>portrait</gnm:orientation>
+        <gnm:Header Left="" Middle="&amp;[TAB]" Right=""/>
+        <gnm:Footer Left="" Middle="Page &amp;[PAGE]" Right=""/>
+        <gnm:paper>iso_a4</gnm:paper>
+        <gnm:comments placement="GNM_PRINT_COMMENTS_IN_PLACE"/>
+        <gnm:errors PrintErrorsAs="GNM_PRINT_ERRORS_AS_DISPLAYED"/>
+      </gnm:PrintInformation>
+      <gnm:Styles>
+        <gnm:StyleRegion startCol="0" startRow="0" endCol="255" endRow="65535">
+          <gnm:Style HAlign="GNM_HALIGN_GENERAL" VAlign="GNM_VALIGN_BOTTOM" WrapText="0" ShrinkToFit="0" Rotation="0" Shade="0" Indent="0" Locked="1" Hidden="0" Fore="0:0:0" Back="FFFF:FFFF:FFFF" PatternColor="0:0:0" Format="General">
+            <gnm:Font Unit="10" Bold="0" Italic="0" Underline="0" StrikeThrough="0" Script="0">Sans</gnm:Font>
+          </gnm:Style>
+        </gnm:StyleRegion>
+      </gnm:Styles>
+      <gnm:Cols DefaultSizePts="48"/>
+      <gnm:Rows DefaultSizePts="12.75">
+        <gnm:RowInfo No="0" Unit="13.5"/>
+      </gnm:Rows>
+      <gnm:Selections CursorCol="0" CursorRow="1">
+        <gnm:Selection startCol="0" startRow="1" endCol="0" endRow="1"/>
+      </gnm:Selections>
+      <gnm:Cells>
+        <gnm:Cell Row="0" Col="0" ValueType="60">Wrong sheet</gnm:Cell>
+      </gnm:Cells>
+      <gnm:SheetLayout TopLeft="A1"/>
+      <gnm:Solver ModelType="0" ProblemType="0" MaxTime="60" MaxIter="1000" NonNeg="1" Discr="0" AutoScale="0" ProgramR="0" SensitivityR="0"/>
+    </gnm:Sheet>
+    <gnm:Sheet DisplayFormulas="0" HideZero="0" HideGrid="0" HideColHeader="0" HideRowHeader="0" DisplayOutlines="1" OutlineSymbolsBelow="1" OutlineSymbolsRight="1" Visibility="GNM_SHEET_VISIBILITY_VISIBLE" GridColor="0:0:0">
+      <gnm:Name>Sheet2</gnm:Name>
+      <gnm:MaxCol>2</gnm:MaxCol>
+      <gnm:MaxRow>3</gnm:MaxRow>
+      <gnm:Zoom>1</gnm:Zoom>
+      <gnm:Names>
+        <gnm:Name>
+          <gnm:name>Print_Area</gnm:name>
+          <gnm:value>#REF!</gnm:value>
+          <gnm:position>A1</gnm:position>
+        </gnm:Name>
+        <gnm:Name>
+          <gnm:name>Sheet_Title</gnm:name>
+          <gnm:value>&quot;Sheet2&quot;</gnm:value>
+          <gnm:position>A1</gnm:position>
+        </gnm:Name>
+      </gnm:Names>
+      <gnm:PrintInformation>
+        <gnm:Margins>
+          <gnm:top Points="120" PrefUnit="mm"/>
+          <gnm:bottom Points="120" PrefUnit="mm"/>
+          <gnm:left Points="72" PrefUnit="mm"/>
+          <gnm:right Points="72" PrefUnit="mm"/>
+          <gnm:header Points="72" PrefUnit="mm"/>
+          <gnm:footer Points="72" PrefUnit="mm"/>
+        </gnm:Margins>
+        <gnm:Scale type="percentage" percentage="100"/>
+        <gnm:vcenter value="0"/>
+        <gnm:hcenter value="0"/>
+        <gnm:grid value="0"/>
+        <gnm:even_if_only_styles value="0"/>
+        <gnm:monochrome value="0"/>
+        <gnm:draft value="0"/>
+        <gnm:titles value="0"/>
+        <gnm:do_not_print value="0"/>
+        <gnm:print_range value="GNM_PRINT_ACTIVE_SHEET"/>
+        <gnm:order>d_then_r</gnm:order>
+        <gnm:orientation>portrait</gnm:orientation>
+        <gnm:Header Left="" Middle="&amp;[TAB]" Right=""/>
+        <gnm:Footer Left="" Middle="Page &amp;[PAGE]" Right=""/>
+        <gnm:paper>iso_a4</gnm:paper>
+        <gnm:comments placement="GNM_PRINT_COMMENTS_IN_PLACE"/>
+        <gnm:errors PrintErrorsAs="GNM_PRINT_ERRORS_AS_DISPLAYED"/>
+      </gnm:PrintInformation>
+      <gnm:Styles>
+        <gnm:StyleRegion startCol="0" startRow="0" endCol="255" endRow="65535">
+          <gnm:Style HAlign="GNM_HALIGN_GENERAL" VAlign="GNM_VALIGN_BOTTOM" WrapText="0" ShrinkToFit="0" Rotation="0" Shade="0" Indent="0" Locked="1" Hidden="0" Fore="0:0:0" Back="FFFF:FFFF:FFFF" PatternColor="0:0:0" Format="General">
+            <gnm:Font Unit="10" Bold="0" Italic="0" Underline="0" StrikeThrough="0" Script="0">Sans</gnm:Font>
+          </gnm:Style>
+        </gnm:StyleRegion>
+      </gnm:Styles>
+      <gnm:Cols DefaultSizePts="48"/>
+      <gnm:Rows DefaultSizePts="12.75">
+        <gnm:RowInfo No="0" Unit="13.5" Count="4"/>
+      </gnm:Rows>
+      <gnm:Selections CursorCol="0" CursorRow="0">
+        <gnm:Selection startCol="0" startRow="0" endCol="2" endRow="3"/>
+      </gnm:Selections>
+      <gnm:Cells>
+        <gnm:Cell Row="0" Col="0" ValueType="60">hi</gnm:Cell>
+        <gnm:Cell Row="0" Col="1" ValueType="60">tweedle</gnm:Cell>
+        <gnm:Cell Row="0" Col="2" ValueType="40">1</gnm:Cell>
+        <gnm:Cell Row="1" Col="0" ValueType="60">ho</gnm:Cell>
+        <gnm:Cell Row="1" Col="1" ValueType="60">dee</gnm:Cell>
+        <gnm:Cell Row="1" Col="2" ValueType="40">2</gnm:Cell>
+        <gnm:Cell Row="2" Col="0" ValueType="60">hum</gnm:Cell>
+        <gnm:Cell Row="2" Col="1" ValueType="60">dum</gnm:Cell>
+        <gnm:Cell Row="2" Col="2" ValueType="40">3</gnm:Cell>
+        <gnm:Cell Row="3" Col="0" ValueType="40">6</gnm:Cell>
+        <gnm:Cell Row="3" Col="1" ValueType="40">5</gnm:Cell>
+        <gnm:Cell Row="3" Col="2" ValueType="40">4</gnm:Cell>
+      </gnm:Cells>
+      <gnm:SheetLayout TopLeft="A1"/>
+      <gnm:Solver ModelType="0" ProblemType="0" MaxTime="60" MaxIter="1000" NonNeg="1" Discr="0" AutoScale="0" ProgramR="0" SensitivityR="0"/>
+    </gnm:Sheet>
+    <gnm:Sheet DisplayFormulas="0" HideZero="0" HideGrid="0" HideColHeader="0" HideRowHeader="0" DisplayOutlines="1" OutlineSymbolsBelow="1" OutlineSymbolsRight="1" Visibility="GNM_SHEET_VISIBILITY_VISIBLE" GridColor="0:0:0">
+      <gnm:Name>Sheet3</gnm:Name>
+      <gnm:MaxCol>0</gnm:MaxCol>
+      <gnm:MaxRow>0</gnm:MaxRow>
+      <gnm:Zoom>1</gnm:Zoom>
+      <gnm:Names>
+        <gnm:Name>
+          <gnm:name>Print_Area</gnm:name>
+          <gnm:value>#REF!</gnm:value>
+          <gnm:position>A1</gnm:position>
+        </gnm:Name>
+        <gnm:Name>
+          <gnm:name>Sheet_Title</gnm:name>
+          <gnm:value>&quot;Sheet3&quot;</gnm:value>
+          <gnm:position>A1</gnm:position>
+        </gnm:Name>
+      </gnm:Names>
+      <gnm:PrintInformation>
+        <gnm:Margins>
+          <gnm:top Points="120" PrefUnit="mm"/>
+          <gnm:bottom Points="120" PrefUnit="mm"/>
+          <gnm:left Points="72" PrefUnit="mm"/>
+          <gnm:right Points="72" PrefUnit="mm"/>
+          <gnm:header Points="72" PrefUnit="mm"/>
+          <gnm:footer Points="72" PrefUnit="mm"/>
+        </gnm:Margins>
+        <gnm:Scale type="percentage" percentage="100"/>
+        <gnm:vcenter value="0"/>
+        <gnm:hcenter value="0"/>
+        <gnm:grid value="0"/>
+        <gnm:even_if_only_styles value="0"/>
+        <gnm:monochrome value="0"/>
+        <gnm:draft value="0"/>
+        <gnm:titles value="0"/>
+        <gnm:do_not_print value="0"/>
+        <gnm:print_range value="GNM_PRINT_ACTIVE_SHEET"/>
+        <gnm:order>d_then_r</gnm:order>
+        <gnm:orientation>portrait</gnm:orientation>
+        <gnm:Header Left="" Middle="&amp;[TAB]" Right=""/>
+        <gnm:Footer Left="" Middle="Page &amp;[PAGE]" Right=""/>
+        <gnm:paper>iso_a4</gnm:paper>
+        <gnm:comments placement="GNM_PRINT_COMMENTS_IN_PLACE"/>
+        <gnm:errors PrintErrorsAs="GNM_PRINT_ERRORS_AS_DISPLAYED"/>
+      </gnm:PrintInformation>
+      <gnm:Styles>
+        <gnm:StyleRegion startCol="0" startRow="0" endCol="255" endRow="65535">
+          <gnm:Style HAlign="GNM_HALIGN_GENERAL" VAlign="GNM_VALIGN_BOTTOM" WrapText="0" ShrinkToFit="0" Rotation="0" Shade="0" Indent="0" Locked="1" Hidden="0" Fore="0:0:0" Back="FFFF:FFFF:FFFF" PatternColor="0:0:0" Format="General">
+            <gnm:Font Unit="10" Bold="0" Italic="0" Underline="0" StrikeThrough="0" Script="0">Sans</gnm:Font>
+          </gnm:Style>
+        </gnm:StyleRegion>
+      </gnm:Styles>
+      <gnm:Cols DefaultSizePts="48"/>
+      <gnm:Rows DefaultSizePts="12.75">
+        <gnm:RowInfo No="0" Unit="13.5"/>
+      </gnm:Rows>
+      <gnm:Selections CursorCol="0" CursorRow="1">
+        <gnm:Selection startCol="0" startRow="1" endCol="0" endRow="1"/>
+      </gnm:Selections>
+      <gnm:Cells>
+        <gnm:Cell Row="0" Col="0" ValueType="60">Not this sheet!</gnm:Cell>
+      </gnm:Cells>
+      <gnm:SheetLayout TopLeft="A1"/>
+      <gnm:Solver ModelType="0" ProblemType="0" MaxTime="60" MaxIter="1000" NonNeg="1" Discr="0" AutoScale="0" ProgramR="0" SensitivityR="0"/>
+    </gnm:Sheet>
+  </gnm:Sheets>
+  <gnm:UIData SelectedTab="1"/>
+</gnm:Workbook>
diff --git a/tests/data/multisheet.ods b/tests/data/multisheet.ods
new file mode 100644 (file)
index 0000000..a8811bd
Binary files /dev/null and b/tests/data/multisheet.ods differ
diff --git a/tests/data/one-thousand-by-fifty-three.gnumeric b/tests/data/one-thousand-by-fifty-three.gnumeric
new file mode 100644 (file)
index 0000000..600bc77
Binary files /dev/null and b/tests/data/one-thousand-by-fifty-three.gnumeric differ
diff --git a/tests/data/one-thousand-by-fifty-three.ods b/tests/data/one-thousand-by-fifty-three.ods
new file mode 100644 (file)
index 0000000..a9fa56d
Binary files /dev/null and b/tests/data/one-thousand-by-fifty-three.ods differ
diff --git a/tests/data/repeating.gnumeric b/tests/data/repeating.gnumeric
new file mode 100644 (file)
index 0000000..3c59c28
Binary files /dev/null and b/tests/data/repeating.gnumeric differ
diff --git a/tests/data/repeating.ods b/tests/data/repeating.ods
new file mode 100644 (file)
index 0000000..880f78b
Binary files /dev/null and b/tests/data/repeating.ods differ
diff --git a/tests/data/simple.gnumeric b/tests/data/simple.gnumeric
new file mode 100644 (file)
index 0000000..e48c6fe
--- /dev/null
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<gnm:Workbook xmlns:gnm="http://www.gnumeric.org/v10.dtd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.gnumeric.org/v9.xsd">
+  <gnm:Version Epoch="1" Major="12" Minor="44" Full="1.12.44"/>
+  <office:document-meta xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:meta="urn:oasis:names:tc:opendocument:xmlns:meta:1.0" xmlns:ooo="http://openoffice.org/2004/office" office:version="1.2">
+    <office:meta>
+      <dc:date>2020-07-29T14:56:16Z</dc:date>
+      <meta:creation-date>2020-07-29T14:55:24Z</meta:creation-date>
+    </office:meta>
+  </office:document-meta>
+  <gnm:Calculation ManualRecalc="0" EnableIteration="1" MaxIterations="100" IterationTolerance="0.001" FloatRadix="2" FloatDigits="53"/>
+  <gnm:SheetNameIndex>
+    <gnm:SheetName gnm:Cols="256" gnm:Rows="65536">Sheet1</gnm:SheetName>
+  </gnm:SheetNameIndex>
+  <gnm:Sheets>
+    <gnm:Sheet DisplayFormulas="0" HideZero="0" HideGrid="0" HideColHeader="0" HideRowHeader="0" DisplayOutlines="1" OutlineSymbolsBelow="1" OutlineSymbolsRight="1" Visibility="GNM_SHEET_VISIBILITY_VISIBLE" GridColor="0:0:0">
+      <gnm:Name>Sheet1</gnm:Name>
+      <gnm:MaxCol>2</gnm:MaxCol>
+      <gnm:MaxRow>3</gnm:MaxRow>
+      <gnm:Zoom>1</gnm:Zoom>
+      <gnm:Cells>
+        <gnm:Cell Row="0" Col="0" ValueType="60">one</gnm:Cell>
+        <gnm:Cell Row="0" Col="1" ValueType="60">two</gnm:Cell>
+        <gnm:Cell Row="0" Col="2" ValueType="60">three</gnm:Cell>
+        <gnm:Cell Row="1" Col="0" ValueType="60">four</gnm:Cell>
+        <gnm:Cell Row="1" Col="1" ValueType="60">five</gnm:Cell>
+        <gnm:Cell Row="1" Col="2" ValueType="60">six</gnm:Cell>
+        <gnm:Cell Row="2" Col="0" ValueType="60">seven</gnm:Cell>
+        <gnm:Cell Row="2" Col="1" ValueType="60">eight</gnm:Cell>
+        <gnm:Cell Row="2" Col="2" ValueType="60">nine</gnm:Cell>
+        <gnm:Cell Row="3" Col="0" ValueType="60">ten</gnm:Cell>
+        <gnm:Cell Row="3" Col="1" ValueType="60">eleven</gnm:Cell>
+        <gnm:Cell Row="3" Col="2" ValueType="60">twelve</gnm:Cell>
+      </gnm:Cells>
+    </gnm:Sheet>
+  </gnm:Sheets>
+</gnm:Workbook>
diff --git a/tests/data/simple.ods b/tests/data/simple.ods
new file mode 100644 (file)
index 0000000..8aba1dc
Binary files /dev/null and b/tests/data/simple.ods differ
diff --git a/tests/data/sparse.gnumeric b/tests/data/sparse.gnumeric
new file mode 100644 (file)
index 0000000..c13e8dc
--- /dev/null
@@ -0,0 +1,234 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<gnm:Workbook xmlns:gnm="http://www.gnumeric.org/v10.dtd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.gnumeric.org/v9.xsd">
+  <gnm:Version Epoch="1" Major="12" Minor="44" Full="1.12.44"/>
+  <gnm:Attributes>
+    <gnm:Attribute>
+      <gnm:name>WorkbookView::show_horizontal_scrollbar</gnm:name>
+      <gnm:value>TRUE</gnm:value>
+    </gnm:Attribute>
+    <gnm:Attribute>
+      <gnm:name>WorkbookView::show_vertical_scrollbar</gnm:name>
+      <gnm:value>TRUE</gnm:value>
+    </gnm:Attribute>
+    <gnm:Attribute>
+      <gnm:name>WorkbookView::show_notebook_tabs</gnm:name>
+      <gnm:value>TRUE</gnm:value>
+    </gnm:Attribute>
+    <gnm:Attribute>
+      <gnm:name>WorkbookView::do_auto_completion</gnm:name>
+      <gnm:value>TRUE</gnm:value>
+    </gnm:Attribute>
+    <gnm:Attribute>
+      <gnm:name>WorkbookView::is_protected</gnm:name>
+      <gnm:value>FALSE</gnm:value>
+    </gnm:Attribute>
+  </gnm:Attributes>
+  <office:document-meta xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:meta="urn:oasis:names:tc:opendocument:xmlns:meta:1.0" xmlns:ooo="http://openoffice.org/2004/office" office:version="1.2">
+    <office:meta>
+      <dc:date>2020-08-19T11:39:12Z</dc:date>
+      <meta:creation-date>2020-08-19T11:38:28Z</meta:creation-date>
+    </office:meta>
+  </office:document-meta>
+  <gnm:Calculation ManualRecalc="0" EnableIteration="1" MaxIterations="100" IterationTolerance="0.001" FloatRadix="2" FloatDigits="53"/>
+  <gnm:SheetNameIndex>
+    <gnm:SheetName gnm:Cols="256" gnm:Rows="65536">Sheet1</gnm:SheetName>
+    <gnm:SheetName gnm:Cols="256" gnm:Rows="65536">Sheet2</gnm:SheetName>
+    <gnm:SheetName gnm:Cols="256" gnm:Rows="65536">Sheet3</gnm:SheetName>
+  </gnm:SheetNameIndex>
+  <gnm:Geometry Width="958" Height="870"/>
+  <gnm:Sheets>
+    <gnm:Sheet DisplayFormulas="0" HideZero="0" HideGrid="0" HideColHeader="0" HideRowHeader="0" DisplayOutlines="1" OutlineSymbolsBelow="1" OutlineSymbolsRight="1" Visibility="GNM_SHEET_VISIBILITY_VISIBLE" GridColor="0:0:0">
+      <gnm:Name>Sheet1</gnm:Name>
+      <gnm:MaxCol>5</gnm:MaxCol>
+      <gnm:MaxRow>1</gnm:MaxRow>
+      <gnm:Zoom>1</gnm:Zoom>
+      <gnm:Names>
+        <gnm:Name>
+          <gnm:name>Print_Area</gnm:name>
+          <gnm:value>#REF!</gnm:value>
+          <gnm:position>A1</gnm:position>
+        </gnm:Name>
+        <gnm:Name>
+          <gnm:name>Sheet_Title</gnm:name>
+          <gnm:value>&quot;Sheet1&quot;</gnm:value>
+          <gnm:position>A1</gnm:position>
+        </gnm:Name>
+      </gnm:Names>
+      <gnm:PrintInformation>
+        <gnm:Margins>
+          <gnm:top Points="120" PrefUnit="mm"/>
+          <gnm:bottom Points="120" PrefUnit="mm"/>
+          <gnm:left Points="72" PrefUnit="mm"/>
+          <gnm:right Points="72" PrefUnit="mm"/>
+          <gnm:header Points="72" PrefUnit="mm"/>
+          <gnm:footer Points="72" PrefUnit="mm"/>
+        </gnm:Margins>
+        <gnm:Scale type="percentage" percentage="100"/>
+        <gnm:vcenter value="0"/>
+        <gnm:hcenter value="0"/>
+        <gnm:grid value="0"/>
+        <gnm:even_if_only_styles value="0"/>
+        <gnm:monochrome value="0"/>
+        <gnm:draft value="0"/>
+        <gnm:titles value="0"/>
+        <gnm:do_not_print value="0"/>
+        <gnm:print_range value="GNM_PRINT_ACTIVE_SHEET"/>
+        <gnm:order>d_then_r</gnm:order>
+        <gnm:orientation>portrait</gnm:orientation>
+        <gnm:Header Left="" Middle="&amp;[TAB]" Right=""/>
+        <gnm:Footer Left="" Middle="Page &amp;[PAGE]" Right=""/>
+        <gnm:paper>na_letter</gnm:paper>
+        <gnm:comments placement="GNM_PRINT_COMMENTS_IN_PLACE"/>
+        <gnm:errors PrintErrorsAs="GNM_PRINT_ERRORS_AS_DISPLAYED"/>
+      </gnm:PrintInformation>
+      <gnm:Styles>
+        <gnm:StyleRegion startCol="0" startRow="0" endCol="255" endRow="65535">
+          <gnm:Style HAlign="GNM_HALIGN_GENERAL" VAlign="GNM_VALIGN_BOTTOM" WrapText="0" ShrinkToFit="0" Rotation="0" Shade="0" Indent="0" Locked="1" Hidden="0" Fore="0:0:0" Back="FFFF:FFFF:FFFF" PatternColor="0:0:0" Format="General">
+            <gnm:Font Unit="10" Bold="0" Italic="0" Underline="0" StrikeThrough="0" Script="0">Sans</gnm:Font>
+          </gnm:Style>
+        </gnm:StyleRegion>
+      </gnm:Styles>
+      <gnm:Cols DefaultSizePts="48"/>
+      <gnm:Rows DefaultSizePts="12.75">
+        <gnm:RowInfo No="0" Unit="13.5" Count="2"/>
+      </gnm:Rows>
+      <gnm:Selections CursorCol="0" CursorRow="0">
+        <gnm:Selection startCol="0" startRow="0" endCol="0" endRow="0"/>
+      </gnm:Selections>
+      <gnm:Cells>
+        <gnm:Cell Row="0" Col="3" ValueType="40">0</gnm:Cell>
+        <gnm:Cell Row="0" Col="4" ValueType="40">1</gnm:Cell>
+        <gnm:Cell Row="0" Col="5" ValueType="40">2</gnm:Cell>
+        <gnm:Cell Row="1" Col="0" ValueType="60">the</gnm:Cell>
+        <gnm:Cell Row="1" Col="1" ValueType="60">row</gnm:Cell>
+        <gnm:Cell Row="1" Col="2" ValueType="60">above</gnm:Cell>
+        <gnm:Cell Row="1" Col="3" ValueType="60">starts</gnm:Cell>
+        <gnm:Cell Row="1" Col="4" ValueType="60">at</gnm:Cell>
+        <gnm:Cell Row="1" Col="5" ValueType="60">D</gnm:Cell>
+      </gnm:Cells>
+      <gnm:SheetLayout TopLeft="A1"/>
+      <gnm:Solver ModelType="0" ProblemType="0" MaxTime="60" MaxIter="1000" NonNeg="1" Discr="0" AutoScale="0" ProgramR="0" SensitivityR="0"/>
+    </gnm:Sheet>
+    <gnm:Sheet DisplayFormulas="0" HideZero="0" HideGrid="0" HideColHeader="0" HideRowHeader="0" DisplayOutlines="1" OutlineSymbolsBelow="1" OutlineSymbolsRight="1" Visibility="GNM_SHEET_VISIBILITY_VISIBLE" GridColor="0:0:0">
+      <gnm:Name>Sheet2</gnm:Name>
+      <gnm:MaxCol>-1</gnm:MaxCol>
+      <gnm:MaxRow>-1</gnm:MaxRow>
+      <gnm:Zoom>1</gnm:Zoom>
+      <gnm:Names>
+        <gnm:Name>
+          <gnm:name>Print_Area</gnm:name>
+          <gnm:value>#REF!</gnm:value>
+          <gnm:position>A1</gnm:position>
+        </gnm:Name>
+        <gnm:Name>
+          <gnm:name>Sheet_Title</gnm:name>
+          <gnm:value>&quot;Sheet2&quot;</gnm:value>
+          <gnm:position>A1</gnm:position>
+        </gnm:Name>
+      </gnm:Names>
+      <gnm:PrintInformation>
+        <gnm:Margins>
+          <gnm:top Points="120" PrefUnit="mm"/>
+          <gnm:bottom Points="120" PrefUnit="mm"/>
+          <gnm:left Points="72" PrefUnit="mm"/>
+          <gnm:right Points="72" PrefUnit="mm"/>
+          <gnm:header Points="72" PrefUnit="mm"/>
+          <gnm:footer Points="72" PrefUnit="mm"/>
+        </gnm:Margins>
+        <gnm:Scale type="percentage" percentage="100"/>
+        <gnm:vcenter value="0"/>
+        <gnm:hcenter value="0"/>
+        <gnm:grid value="0"/>
+        <gnm:even_if_only_styles value="0"/>
+        <gnm:monochrome value="0"/>
+        <gnm:draft value="0"/>
+        <gnm:titles value="0"/>
+        <gnm:do_not_print value="0"/>
+        <gnm:print_range value="GNM_PRINT_ACTIVE_SHEET"/>
+        <gnm:order>d_then_r</gnm:order>
+        <gnm:orientation>portrait</gnm:orientation>
+        <gnm:Header Left="" Middle="&amp;[TAB]" Right=""/>
+        <gnm:Footer Left="" Middle="Page &amp;[PAGE]" Right=""/>
+        <gnm:paper>na_letter</gnm:paper>
+        <gnm:comments placement="GNM_PRINT_COMMENTS_IN_PLACE"/>
+        <gnm:errors PrintErrorsAs="GNM_PRINT_ERRORS_AS_DISPLAYED"/>
+      </gnm:PrintInformation>
+      <gnm:Styles>
+        <gnm:StyleRegion startCol="0" startRow="0" endCol="255" endRow="65535">
+          <gnm:Style HAlign="GNM_HALIGN_GENERAL" VAlign="GNM_VALIGN_BOTTOM" WrapText="0" ShrinkToFit="0" Rotation="0" Shade="0" Indent="0" Locked="1" Hidden="0" Fore="0:0:0" Back="FFFF:FFFF:FFFF" PatternColor="0:0:0" Format="General">
+            <gnm:Font Unit="10" Bold="0" Italic="0" Underline="0" StrikeThrough="0" Script="0">Sans</gnm:Font>
+          </gnm:Style>
+        </gnm:StyleRegion>
+      </gnm:Styles>
+      <gnm:Cols DefaultSizePts="48"/>
+      <gnm:Rows DefaultSizePts="12.75"/>
+      <gnm:Selections CursorCol="0" CursorRow="0">
+        <gnm:Selection startCol="0" startRow="0" endCol="0" endRow="0"/>
+      </gnm:Selections>
+      <gnm:Cells/>
+      <gnm:SheetLayout TopLeft="A1"/>
+      <gnm:Solver ModelType="0" ProblemType="0" MaxTime="60" MaxIter="1000" NonNeg="1" Discr="0" AutoScale="0" ProgramR="0" SensitivityR="0"/>
+    </gnm:Sheet>
+    <gnm:Sheet DisplayFormulas="0" HideZero="0" HideGrid="0" HideColHeader="0" HideRowHeader="0" DisplayOutlines="1" OutlineSymbolsBelow="1" OutlineSymbolsRight="1" Visibility="GNM_SHEET_VISIBILITY_VISIBLE" GridColor="0:0:0">
+      <gnm:Name>Sheet3</gnm:Name>
+      <gnm:MaxCol>-1</gnm:MaxCol>
+      <gnm:MaxRow>-1</gnm:MaxRow>
+      <gnm:Zoom>1</gnm:Zoom>
+      <gnm:Names>
+        <gnm:Name>
+          <gnm:name>Print_Area</gnm:name>
+          <gnm:value>#REF!</gnm:value>
+          <gnm:position>A1</gnm:position>
+        </gnm:Name>
+        <gnm:Name>
+          <gnm:name>Sheet_Title</gnm:name>
+          <gnm:value>&quot;Sheet3&quot;</gnm:value>
+          <gnm:position>A1</gnm:position>
+        </gnm:Name>
+      </gnm:Names>
+      <gnm:PrintInformation>
+        <gnm:Margins>
+          <gnm:top Points="120" PrefUnit="mm"/>
+          <gnm:bottom Points="120" PrefUnit="mm"/>
+          <gnm:left Points="72" PrefUnit="mm"/>
+          <gnm:right Points="72" PrefUnit="mm"/>
+          <gnm:header Points="72" PrefUnit="mm"/>
+          <gnm:footer Points="72" PrefUnit="mm"/>
+        </gnm:Margins>
+        <gnm:Scale type="percentage" percentage="100"/>
+        <gnm:vcenter value="0"/>
+        <gnm:hcenter value="0"/>
+        <gnm:grid value="0"/>
+        <gnm:even_if_only_styles value="0"/>
+        <gnm:monochrome value="0"/>
+        <gnm:draft value="0"/>
+        <gnm:titles value="0"/>
+        <gnm:do_not_print value="0"/>
+        <gnm:print_range value="GNM_PRINT_ACTIVE_SHEET"/>
+        <gnm:order>d_then_r</gnm:order>
+        <gnm:orientation>portrait</gnm:orientation>
+        <gnm:Header Left="" Middle="&amp;[TAB]" Right=""/>
+        <gnm:Footer Left="" Middle="Page &amp;[PAGE]" Right=""/>
+        <gnm:paper>na_letter</gnm:paper>
+        <gnm:comments placement="GNM_PRINT_COMMENTS_IN_PLACE"/>
+        <gnm:errors PrintErrorsAs="GNM_PRINT_ERRORS_AS_DISPLAYED"/>
+      </gnm:PrintInformation>
+      <gnm:Styles>
+        <gnm:StyleRegion startCol="0" startRow="0" endCol="255" endRow="65535">
+          <gnm:Style HAlign="GNM_HALIGN_GENERAL" VAlign="GNM_VALIGN_BOTTOM" WrapText="0" ShrinkToFit="0" Rotation="0" Shade="0" Indent="0" Locked="1" Hidden="0" Fore="0:0:0" Back="FFFF:FFFF:FFFF" PatternColor="0:0:0" Format="General">
+            <gnm:Font Unit="10" Bold="0" Italic="0" Underline="0" StrikeThrough="0" Script="0">Sans</gnm:Font>
+          </gnm:Style>
+        </gnm:StyleRegion>
+      </gnm:Styles>
+      <gnm:Cols DefaultSizePts="48"/>
+      <gnm:Rows DefaultSizePts="12.75"/>
+      <gnm:Selections CursorCol="0" CursorRow="0">
+        <gnm:Selection startCol="0" startRow="0" endCol="0" endRow="0"/>
+      </gnm:Selections>
+      <gnm:Cells/>
+      <gnm:SheetLayout TopLeft="A1"/>
+      <gnm:Solver ModelType="0" ProblemType="0" MaxTime="60" MaxIter="1000" NonNeg="1" Discr="0" AutoScale="0" ProgramR="0" SensitivityR="0"/>
+    </gnm:Sheet>
+  </gnm:Sheets>
+  <gnm:UIData SelectedTab="0"/>
+</gnm:Workbook>
diff --git a/tests/data/sparse.ods b/tests/data/sparse.ods
new file mode 100644 (file)
index 0000000..8c1dced
Binary files /dev/null and b/tests/data/sparse.ods differ
diff --git a/tests/data/spreadsheet-test.at b/tests/data/spreadsheet-test.at
new file mode 100644 (file)
index 0000000..59836d5
--- /dev/null
@@ -0,0 +1,85 @@
+dnl PSPP - a program for statistical analysis.
+dnl Copyright (C) 2020 Free Software Foundation, Inc.
+dnl
+dnl This program is free software: you can redistribute it and/or modify
+dnl it under the terms of the GNU General Public License as published by
+dnl the Free Software Foundation, either version 3 of the License, or
+dnl (at your option) any later version.
+dnl
+dnl This program is distributed in the hope that it will be useful,
+dnl but WITHOUT ANY WARRANTY; without even the implied warranty of
+dnl MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+dnl GNU General Public License for more details.
+dnl
+dnl You should have received a copy of the GNU General Public License
+dnl along with this program.  If not, see <http://www.gnu.org/licenses/>.
+dnl
+AT_BANNER([spreadsheet])
+
+m4_define([SPREADSHEET_TEST],
+  [AT_SETUP([$1 $2])
+   AT_KEYWORDS([spreadsheet $4])
+   AT_CHECK([spreadsheet-test $2 $top_srcdir/tests/data/$1.gnumeric], [0], [$3], [ignore])
+   AT_CHECK([spreadsheet-test $2 $top_srcdir/tests/data/$1.ods], [0], [$3], [ignore])
+   AT_CLEANUP])
+
+SPREADSHEET_TEST([simple], [--sheet=0], [dnl
+Rows 4; Columns 3
+one    two     three
+four   five    six
+seven  eight   nine
+ten    eleven  twelve
+])
+
+SPREADSHEET_TEST([simple], [--sheet=0 --reverse], [dnl
+Rows 4; Columns 3
+twelve eleven  ten
+nine   eight   seven
+six    five    four
+three  two     one
+])
+
+
+SPREADSHEET_TEST([multisheet], [--sheet=1], [dnl
+Rows 4; Columns 3
+hi     tweedle 1
+ho     dee     2
+hum    dum     3
+6      5       4
+])
+
+
+SPREADSHEET_TEST([repeating], [], [dnl
+Rows 3; Columns 5
+one    one     one     two     two
+two    three   three   three   four
+four   four    five    five    five
+])
+
+SPREADSHEET_TEST([sparse], [], [dnl
+Rows 2; Columns 6
+                       0       1       2
+the    row     above   starts  at      D
+])
+
+SPREADSHEET_TEST([holey], [], [dnl
+Rows 1; Columns 8
+               hi      ho                      hum     hee
+])
+
+
+dnl If this test takes an unreasonably long time, then probably the caching
+dnl code is not working.
+dnl On my machine, this test takes about 7 seconds
+SPREADSHEET_TEST([one-thousand-by-fifty-three], [--refcheck --reverse], [dnl
+Rows 1000; Columns 53
+], [slow])
+
+dnl Check that the worksheet metadata is retrieved correctly
+SPREADSHEET_TEST([multisheet], [--metadata], [dnl
+Number of sheets: 3
+])
+
+SPREADSHEET_TEST([simple], [--metadata], [dnl
+Number of sheets: 1
+])
diff --git a/tests/data/spreadsheet-test.c b/tests/data/spreadsheet-test.c
new file mode 100644 (file)
index 0000000..6606944
--- /dev/null
@@ -0,0 +1,145 @@
+/* PSPP - a program for statistical analysis.
+   Copyright (C) 2020 Free Software Foundation, Inc.
+
+   This program is free software: you can redistribute it and/or modify
+   it under the terms of the GNU General Public License as published by
+   the Free Software Foundation, either version 3 of the License, or
+   (at your option) any later version.
+
+   This program is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+   GNU General Public License for more details.
+
+   You should have received a copy of the GNU General Public License
+   along with this program.  If not, see <http://www.gnu.org/licenses/>. */
+
+#include <config.h>
+#include "progname.h"
+
+#include <assert.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <getopt.h>
+#include <string.h>
+
+#include <data/spreadsheet-reader.h>
+#include <data/gnumeric-reader.h>
+#include <data/ods-reader.h>
+
+enum OPT
+  {
+   OPT_REFCHECK = 0x100,
+   OPT_REVERSE,
+   OPT_SHEET,
+   OPT_METADATA
+  };
+
+static const struct option long_opts[] =
+  {
+   {"refcheck", no_argument, NULL, OPT_REFCHECK},
+   {"reverse", no_argument, NULL, OPT_REVERSE},
+   {"sheet", required_argument, NULL, OPT_SHEET},
+   {"metadata", no_argument, NULL, OPT_METADATA},
+   {0, 0, 0, 0}
+  };
+
+int
+main (int argc, char **argv)
+{
+  set_program_name (argv[0]);
+
+  bool refcheck = false;
+  bool reverse = false;
+  int sheet = 0;
+  bool get_n_sheets = false;
+  int opt;
+  while ((opt = getopt_long (argc, argv, "", long_opts, NULL)) != -1)
+    {
+      switch (opt)
+        {
+       case OPT_METADATA:
+         get_n_sheets = true;
+         break;
+        case OPT_REFCHECK:
+          refcheck = true;
+          break;
+        case OPT_REVERSE:
+          reverse = true;
+          break;
+        case OPT_SHEET:
+          sheet = atoi (optarg);
+          break;
+        default: /* '?' */
+          fprintf (stderr, "Usage: spreadsheet-test [opts] file\n");
+          exit (EXIT_FAILURE);
+        }
+    }
+
+  if (argc < optind)
+    {
+      fprintf (stderr, "Usage: spreadsheet-test [-s n] file\n");
+      exit (EXIT_FAILURE);
+    }
+
+  struct spreadsheet *ss = NULL;
+
+  char *ext = strrchr (argv[optind], '.');
+  if (ext == NULL)
+    return 1;
+  if (0 == strcmp (ext, ".ods"))
+    ss = ods_probe (argv[optind], true);
+  else if (0 == strcmp (ext, ".gnumeric"))
+    ss = gnumeric_probe (argv[optind], true);
+
+  if (ss == NULL)
+    return 1;
+
+  if (get_n_sheets)
+    {
+      int n_sheets = spreadsheet_get_sheet_n_sheets (ss);
+      printf ("Number of sheets: %d\n", n_sheets);
+      goto end;
+    }
+  int rows = spreadsheet_get_sheet_n_rows (ss, sheet);
+  int columns = spreadsheet_get_sheet_n_columns (ss, sheet);
+
+  printf ("Rows %d; Columns %d\n", rows, columns);
+  for (int r_ = 0; r_ < rows; r_++)
+    {
+      int r = reverse ? (rows - r_ - 1) : r_;
+      for (int c_ = 0; c_ < columns; c_++)
+        {
+         int c = reverse ? (columns - c_ - 1) : c_ ;
+          char *s = spreadsheet_get_cell (ss, sheet, r, c);
+          if (refcheck)
+            {
+              int row, col;
+              sscanf (s, "%d:%d", &row, &col);
+              assert (row == r);
+              assert (col == c);
+            }
+         else
+            {
+              fputs (s ? s : "", stdout);
+             if (c_ < columns - 1)
+               putchar ('\t');
+            }
+
+
+          free (s);
+        }
+      if (!refcheck)
+        {
+         putchar ('\n');
+        }
+    }
+
+  rows = spreadsheet_get_sheet_n_rows (ss, sheet);
+  columns = spreadsheet_get_sheet_n_columns (ss, sheet);
+
+ end:
+  spreadsheet_unref (ss);
+
+  return 0;
+}
index f739ca7168f41971a4a78ed0cb89aeaff7624a01..dc812fd8faf469ad7728d820c4e1c14038d10602 100644 (file)
@@ -15,6 +15,7 @@ dnl You should have received a copy of the GNU General Public License
 dnl along with this program.  If not, see <http://www.gnu.org/licenses/>.
 dnl
 m4_define([SPREADSHEET_TEST_PREP],[dnl
+ AT_KEYWORDS([spreadsheet])
  m4_if($1,[GNM],[dnl
     AT_CHECK([gzip -c $top_srcdir/tests/language/data-io/Book1.gnm.unzipped > Book1.gnumeric])dnl
     m4_define([testsheet],[Book1.gnumeric])dnl
@@ -172,6 +173,7 @@ CHECK_SPREADSHEET_READER([GNM])
 
 dnl Check for a bug where gnumeric files were interpreted incorrectly
 AT_SETUP([GET DATA /TYPE=GNM sheet index bug])
+AT_KEYWORDS([spreadsheet])
 AT_DATA([minimal3.gnumeric],[dnl
 <?xml version="1.0" encoding="UTF-8"?>
 <gnm:Workbook xmlns:gnm="http://www.gnumeric.org/v10.dtd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.gnumeric.org/v9.xsd">
@@ -320,6 +322,7 @@ AT_CLEANUP
 
 dnl Check for a bug where certain gnumeric files failed an assertion
 AT_SETUP([GET DATA /TYPE=GNM assert-fail])
+AT_KEYWORDS([spreadsheet])
 AT_DATA([read.sps],[dnl
 GET DATA
        /TYPE=GNM
@@ -376,6 +379,7 @@ CHECK_SPREADSHEET_READER([ODS])
 
 
 AT_SETUP([GET DATA /TYPE=ODS crash])
+AT_KEYWORDS([spreadsheet])
 
 
 AT_CHECK([cp $top_srcdir/tests/language/data-io/newone.ods this.ods])dnl
@@ -391,6 +395,7 @@ AT_CLEANUP
 
 
 AT_SETUP([GET DATA /TYPE=ODS readnames])
+AT_KEYWORDS([spreadsheet])
 
 dnl Check for a bug where in the ODS reader /READNAMES incorrectly
 dnl dealt with repeated names.