From 77e2017715a58c01d3e63ad90fb28b5e39eb2a31 Mon Sep 17 00:00:00 2001 From: John Darrington Date: Sat, 10 Oct 2020 08:21:56 +0200 Subject: [PATCH] Rework the spreadsheet import feature of the grapic user interface 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. --- INSTALL | 1 + NEWS | 4 + configure.ac | 4 +- src/data/gnumeric-reader.c | 653 +++++++---- src/data/gnumeric-reader.h | 13 - src/data/ods-reader.c | 637 ++++++---- src/data/ods-reader.h | 16 +- src/data/spreadsheet-reader.c | 79 +- src/data/spreadsheet-reader.h | 61 +- src/ui/gui/automake.mk | 7 + src/ui/gui/psppire-import-assistant.c | 1028 ++--------------- src/ui/gui/psppire-import-assistant.h | 43 +- src/ui/gui/psppire-import-spreadsheet.c | 416 +++++++ src/ui/gui/psppire-import-spreadsheet.h | 10 + src/ui/gui/psppire-import-textfile.c | 862 ++++++++++++++ src/ui/gui/psppire-import-textfile.h | 37 + src/ui/gui/psppire-spreadsheet-data-model.c | 378 ++++++ src/ui/gui/psppire-spreadsheet-data-model.h | 86 ++ src/ui/gui/psppire-spreadsheet-model.c | 41 +- src/ui/gui/psppire-spreadsheet-model.h | 2 + src/ui/gui/spreadsheet-import.ui | 261 +++++ src/ui/gui/text-data-import.ui | 173 +-- tests/automake.mk | 20 + tests/data/holey.gnumeric | Bin 0 -> 1817 bytes tests/data/holey.ods | Bin 0 -> 7276 bytes tests/data/multisheet.gnumeric | 245 ++++ tests/data/multisheet.ods | Bin 0 -> 7523 bytes .../data/one-thousand-by-fifty-three.gnumeric | Bin 0 -> 293436 bytes tests/data/one-thousand-by-fifty-three.ods | Bin 0 -> 556434 bytes tests/data/repeating.gnumeric | Bin 0 -> 1898 bytes tests/data/repeating.ods | Bin 0 -> 8201 bytes tests/data/simple.gnumeric | 36 + tests/data/simple.ods | Bin 0 -> 7957 bytes tests/data/sparse.gnumeric | 234 ++++ tests/data/sparse.ods | Bin 0 -> 7899 bytes tests/data/spreadsheet-test.at | 85 ++ tests/data/spreadsheet-test.c | 145 +++ .../language/data-io/get-data-spreadsheet.at | 5 + 38 files changed, 3904 insertions(+), 1678 deletions(-) create mode 100644 src/ui/gui/psppire-import-spreadsheet.c create mode 100644 src/ui/gui/psppire-import-spreadsheet.h create mode 100644 src/ui/gui/psppire-import-textfile.c create mode 100644 src/ui/gui/psppire-import-textfile.h create mode 100644 src/ui/gui/psppire-spreadsheet-data-model.c create mode 100644 src/ui/gui/psppire-spreadsheet-data-model.h create mode 100644 src/ui/gui/spreadsheet-import.ui create mode 100644 tests/data/holey.gnumeric create mode 100644 tests/data/holey.ods create mode 100644 tests/data/multisheet.gnumeric create mode 100644 tests/data/multisheet.ods create mode 100644 tests/data/one-thousand-by-fifty-three.gnumeric create mode 100644 tests/data/one-thousand-by-fifty-three.ods create mode 100644 tests/data/repeating.gnumeric create mode 100644 tests/data/repeating.ods create mode 100644 tests/data/simple.gnumeric create mode 100644 tests/data/simple.ods create mode 100644 tests/data/sparse.gnumeric create mode 100644 tests/data/sparse.ods create mode 100644 tests/data/spreadsheet-test.at create mode 100644 tests/data/spreadsheet-test.c diff --git a/INSTALL b/INSTALL index c131292cb3..15f356a945 100644 --- 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 70cf096d1e..c30e558cb2 100644 --- 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. diff --git a/configure.ac b/configure.ac index f0680e830e..3bc4ea1fd6 100644 --- a/configure.ac +++ b/configure.ac @@ -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])], diff --git a/src/data/gnumeric-reader.c b/src/data/gnumeric-reader.c index acfc3c9064..c1bf389d26 100644 --- a/src/data/gnumeric-reader.c +++ b/src/data/gnumeric-reader.c @@ -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" @@ -46,6 +50,11 @@ #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; +} diff --git a/src/data/gnumeric-reader.h b/src/data/gnumeric-reader.h index edec2b66c4..59b0c828a5 100644 --- a/src/data/gnumeric-reader.h +++ b/src/data/gnumeric-reader.h @@ -19,20 +19,7 @@ #include -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 diff --git a/src/data/ods-reader.c b/src/data/ods-reader.c index 8f307200b0..cac060f81d 100644 --- a/src/data/ods-reader.c +++ b/src/data/ods-reader.c @@ -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" @@ -46,12 +49,15 @@ #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; +} diff --git a/src/data/ods-reader.h b/src/data/ods-reader.h index 9602a310c1..8626020c92 100644 --- a/src/data/ods-reader.h +++ b/src/data/ods-reader.h @@ -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 @@ -19,21 +19,7 @@ #include -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 diff --git a/src/data/spreadsheet-reader.c b/src/data/spreadsheet-reader.c index 9cd118c758..a16b430a52 100644 --- a/src/data/spreadsheet-reader.c +++ b/src/data/spreadsheet-reader.c @@ -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 @@ -29,28 +29,18 @@ #include #include -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; } - diff --git a/src/data/spreadsheet-reader.h b/src/data/spreadsheet-reader.h index efba6f369f..d7783bab7f 100644 --- a/src/data/spreadsheet-reader.h +++ b/src/data/spreadsheet-reader.h @@ -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)) diff --git a/src/ui/gui/automake.mk b/src/ui/gui/automake.mk index fec10f5538..f169878887 100644 --- a/src/ui/gui/automake.mk +++ b/src/ui/gui/automake.mk @@ -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 \ diff --git a/src/ui/gui/psppire-import-assistant.c b/src/ui/gui/psppire-import-assistant.c index a968431d86..47744909db 100644 --- a/src/ui/gui/psppire-import-assistant.c +++ b/src/ui/gui/psppire-import-assistant.c @@ -15,58 +15,40 @@ along with this program. If not, see . */ #include +#include "psppire-import-assistant.h" #include #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 #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) -/* 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); - -} - - - - - - -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"); diff --git a/src/ui/gui/psppire-import-assistant.h b/src/ui/gui/psppire-import-assistant.h index 3de7e08ff8..881fc5bad0 100644 --- a/src/ui/gui/psppire-import-assistant.h +++ b/src/ui/gui/psppire-import-assistant.h @@ -28,6 +28,8 @@ #include "psppire-text-file.h" #include "psppire-delimited-text.h" +#include + 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 index 0000000000..636276f058 --- /dev/null +++ b/src/ui/gui/psppire-import-spreadsheet.c @@ -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 . */ + +#include + + +#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 +#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 index 0000000000..bd349217cb --- /dev/null +++ b/src/ui/gui/psppire-import-spreadsheet.h @@ -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 index 0000000000..d293dc0e39 --- /dev/null +++ b/src/ui/gui/psppire-import-textfile.c @@ -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 . */ + +#include + +#include "psppire-import-textfile.h" +#include + +#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 +#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); +} + + + + +/* 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); +} + + + +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 index 0000000000..7a3c0f6fd4 --- /dev/null +++ b/src/ui/gui/psppire-import-textfile.h @@ -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 index 0000000000..1d0fbc1558 --- /dev/null +++ b/src/ui/gui/psppire-spreadsheet-data-model.c @@ -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 . */ + +#include +#include + +#include + +#include + +#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); +} + + + +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 index 0000000000..c06418b3e1 --- /dev/null +++ b/src/ui/gui/psppire-spreadsheet-data-model.h @@ -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 . */ + + +#include +#include + +#include + +#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__ */ diff --git a/src/ui/gui/psppire-spreadsheet-model.c b/src/ui/gui/psppire-spreadsheet-model.c index d946197262..26c803f3a6 100644 --- a/src/ui/gui/psppire-spreadsheet-model.c +++ b/src/ui/gui/psppire-spreadsheet-model.c @@ -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; } diff --git a/src/ui/gui/psppire-spreadsheet-model.h b/src/ui/gui/psppire-spreadsheet-model.h index 0366ad4bc1..e60e3fa99c 100644 --- a/src/ui/gui/psppire-spreadsheet-model.h +++ b/src/ui/gui/psppire-spreadsheet-model.h @@ -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 index 0000000000..9d225f3f55 --- /dev/null +++ b/src/ui/gui/spreadsheet-import.ui @@ -0,0 +1,261 @@ + + + + + + + + + + + + + + + + + + + 1 + + + 1 + + + 1 + + + 1 + + + True + False + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + vertical + 12 + + + True + False + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + Choose below the sheet number and the cell range that you wish to import. + True + + + False + True + 0 + + + + + True + False + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + 0 + none + + + True + False + 12 + 5 + 20 + + + True + False + vertical + 5 + + + True + False + + + True + False + Importing file: + + + False + False + 0 + + + + + True + True + + + True + True + 1 + + + + + False + True + 0 + + + + + True + True + 0 + + + False + True + 1 + + + + + Use the first selected row as _variable names + True + True + False + True + right + True + + + False + True + 2 + + + + + 0 + 0 + + + + + True + False + 12 + + + True + False + + + True + False + _Cells: + True + cell-range-entry + + + False + False + 0 + + + + + True + True + + + True + True + 1 + + + + + 0 + 0 + 2 + + + + + True + True + adjustment0 + + + 0 + 1 + + + + + True + True + adjustment1 + True + + + 1 + 1 + + + + + True + True + adjustment2 + + + 0 + 2 + + + + + True + True + adjustment3 + True + + + 1 + 2 + + + + + 1 + 0 + + + + + True + True + + + 0 + 1 + 2 + + + + + + + True + False + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + <b>Cells to Import</b> + True + + + + + True + True + 1 + + + + diff --git a/src/ui/gui/text-data-import.ui b/src/ui/gui/text-data-import.ui index 084f1f5c37..3f66e53c21 100644 --- a/src/ui/gui/text-data-import.ui +++ b/src/ui/gui/text-data-import.ui @@ -1,23 +1,20 @@ + - - + - - - - + True False @@ -56,7 +53,6 @@ True False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - 0.5 True @@ -93,7 +89,7 @@ GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK vertical 94 - True + True True @@ -102,15 +98,10 @@ 0 none - + True True - GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - 12 - 12 - - - + True @@ -133,15 +124,14 @@ True False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + 12 0 none - 12 - + True True - GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - 12 + True @@ -202,12 +192,12 @@ False 12 vertical + 3 True True False - 0 True True @@ -221,7 +211,6 @@ True True False - 0 True import-all-cases @@ -235,7 +224,6 @@ True True False - 0 True import-all-cases @@ -248,8 +236,8 @@ True False - 0 All cases + 0 1 @@ -328,7 +316,6 @@ False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK True - 0.5 True @@ -344,7 +331,6 @@ False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK True - 0.5 True @@ -360,7 +346,6 @@ False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK True - 0.5 True @@ -376,7 +361,6 @@ False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK True - 0.5 True @@ -392,7 +376,6 @@ False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK True - 0.5 True @@ -408,7 +391,6 @@ False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK True - 0.5 True @@ -424,7 +406,6 @@ False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK True - 0.5 True @@ -440,7 +421,6 @@ False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK True - 0.5 True @@ -456,7 +436,6 @@ False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK True - 0.5 True @@ -472,7 +451,6 @@ False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK True - 0.5 True @@ -520,16 +498,16 @@ GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK False True - - - False - - '" ' " + + + False + + 1 @@ -543,7 +521,6 @@ True False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - 0.5 True @@ -608,122 +585,4 @@ - - True - False - GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - vertical - 12 - - - True - False - GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - Enter below the sheet number and the cell range which you wish to import. - True - - - False - True - 0 - - - - - True - False - GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - 0 - none - - - True - False - 12 - - - True - True - ● - - - 1 - 0 - - - - - True - False - 0 - - - 1 - 1 - - - - - True - False - 1 - _Cells: - True - cell-range-entry - - - 0 - 0 - - - - - True - False - 1 - _Sheet Index: - True - sheet-entry - - - 0 - 1 - - - - - Use first row as _variable names - True - True - False - True - 0 - right - True - - - 0 - 2 - 2 - - - - - - - True - False - GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - <b>Cells to Import</b> - True - - - - - False - True - 1 - - - diff --git a/tests/automake.mk b/tests/automake.mk index 66e17bb2a7..241f0afd4d 100644 --- a/tests/automake.mk +++ b/tests/automake.mk @@ -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 index 0000000000000000000000000000000000000000..dea5ce0415fe751a1a7c840e866ce1f4b43667e2 GIT binary patch literal 1817 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 YPAke{q0(vqfE4wDpvG_K#zpP_0H{Uk!~g&Q literal 0 HcmV?d00001 diff --git a/tests/data/multisheet.gnumeric b/tests/data/multisheet.gnumeric new file mode 100644 index 0000000000..424c28bf19 --- /dev/null +++ b/tests/data/multisheet.gnumeric @@ -0,0 +1,245 @@ + + + + + + WorkbookView::show_horizontal_scrollbar + TRUE + + + WorkbookView::show_vertical_scrollbar + TRUE + + + WorkbookView::show_notebook_tabs + TRUE + + + WorkbookView::do_auto_completion + TRUE + + + WorkbookView::is_protected + FALSE + + + + + 2020-07-29T16:17:03Z + 2020-07-29T16:16:01Z + + + + + Sheet1 + Sheet2 + Sheet3 + + + + + Sheet1 + 0 + 0 + 1 + + + Print_Area + #REF! + A1 + + + Sheet_Title + "Sheet1" + A1 + + + + + + + + + + + + + + + + + + + + + + d_then_r + portrait + + + iso_a4 + + + + + + + + + + + + + + + + + Wrong sheet + + + + + + Sheet2 + 2 + 3 + 1 + + + Print_Area + #REF! + A1 + + + Sheet_Title + "Sheet2" + A1 + + + + + + + + + + + + + + + + + + + + + + d_then_r + portrait + + + iso_a4 + + + + + + + + + + + + + + + + + hi + tweedle + 1 + ho + dee + 2 + hum + dum + 3 + 6 + 5 + 4 + + + + + + Sheet3 + 0 + 0 + 1 + + + Print_Area + #REF! + A1 + + + Sheet_Title + "Sheet3" + A1 + + + + + + + + + + + + + + + + + + + + + + d_then_r + portrait + + + iso_a4 + + + + + + + + + + + + + + + + + Not this sheet! + + + + + + + diff --git a/tests/data/multisheet.ods b/tests/data/multisheet.ods new file mode 100644 index 0000000000000000000000000000000000000000..a8811bd318f1ccc60bf6dba42943ed1bf9a53855 GIT binary patch literal 7523 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 F|34?AdzJtI literal 0 HcmV?d00001 diff --git a/tests/data/one-thousand-by-fifty-three.gnumeric b/tests/data/one-thousand-by-fifty-three.gnumeric new file mode 100644 index 0000000000000000000000000000000000000000..600bc77097a1c6d54b6f940233abdb904b1b2813 GIT binary patch literal 293436 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 literal 0 HcmV?d00001 diff --git a/tests/data/one-thousand-by-fifty-three.ods b/tests/data/one-thousand-by-fifty-three.ods new file mode 100644 index 0000000000000000000000000000000000000000..a9fa56d54829dd401931e9f423900c8ef4394c88 GIT binary patch literal 556434 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

xlF&`9?ux> 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 literal 0 HcmV?d00001 diff --git a/tests/data/repeating.ods b/tests/data/repeating.ods new file mode 100644 index 0000000000000000000000000000000000000000..880f78b6cfbee7e1e386b51b5d3f8f6cf54b8c37 GIT binary patch literal 8201 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 literal 0 HcmV?d00001 diff --git a/tests/data/simple.gnumeric b/tests/data/simple.gnumeric new file mode 100644 index 0000000000..e48c6fe079 --- /dev/null +++ b/tests/data/simple.gnumeric @@ -0,0 +1,36 @@ + + + + + + 2020-07-29T14:56:16Z + 2020-07-29T14:55:24Z + + + + + Sheet1 + + + + Sheet1 + 2 + 3 + 1 + + one + two + three + four + five + six + seven + eight + nine + ten + eleven + twelve + + + + diff --git a/tests/data/simple.ods b/tests/data/simple.ods new file mode 100644 index 0000000000000000000000000000000000000000..8aba1dcb76cfde9e5d7591f83752a42e0bc849f2 GIT binary patch literal 7957 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 TjT#EdovR1<>aU#i_ay%pbP-k8 literal 0 HcmV?d00001 diff --git a/tests/data/sparse.gnumeric b/tests/data/sparse.gnumeric new file mode 100644 index 0000000000..c13e8dc9f4 --- /dev/null +++ b/tests/data/sparse.gnumeric @@ -0,0 +1,234 @@ + + + + + + WorkbookView::show_horizontal_scrollbar + TRUE + + + WorkbookView::show_vertical_scrollbar + TRUE + + + WorkbookView::show_notebook_tabs + TRUE + + + WorkbookView::do_auto_completion + TRUE + + + WorkbookView::is_protected + FALSE + + + + + 2020-08-19T11:39:12Z + 2020-08-19T11:38:28Z + + + + + Sheet1 + Sheet2 + Sheet3 + + + + + Sheet1 + 5 + 1 + 1 + + + Print_Area + #REF! + A1 + + + Sheet_Title + "Sheet1" + A1 + + + + + + + + + + + + + + + + + + + + + + d_then_r + portrait + + + na_letter + + + + + + + + + + + + + + + + + 0 + 1 + 2 + the + row + above + starts + at + D + + + + + + Sheet2 + -1 + -1 + 1 + + + Print_Area + #REF! + A1 + + + Sheet_Title + "Sheet2" + A1 + + + + + + + + + + + + + + + + + + + + + + d_then_r + portrait + + + na_letter + + + + + + + + + + + + + + + + + + + Sheet3 + -1 + -1 + 1 + + + Print_Area + #REF! + A1 + + + Sheet_Title + "Sheet3" + A1 + + + + + + + + + + + + + + + + + + + + + + d_then_r + portrait + + + na_letter + + + + + + + + + + + + + + + + + + + + diff --git a/tests/data/sparse.ods b/tests/data/sparse.ods new file mode 100644 index 0000000000000000000000000000000000000000..8c1dcedcceeb9da84c7c5c373411cf928d20243f GIT binary patch literal 7899 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 b<+oqPX;nom)RPAQa1-?cqx@j>w`=|b;2{W# literal 0 HcmV?d00001 diff --git a/tests/data/spreadsheet-test.at b/tests/data/spreadsheet-test.at new file mode 100644 index 0000000000..59836d5bc4 --- /dev/null +++ b/tests/data/spreadsheet-test.at @@ -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 . +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 index 0000000000..66069444a2 --- /dev/null +++ b/tests/data/spreadsheet-test.c @@ -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 . */ + +#include +#include "progname.h" + +#include +#include +#include +#include +#include + +#include +#include +#include + +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; +} diff --git a/tests/language/data-io/get-data-spreadsheet.at b/tests/language/data-io/get-data-spreadsheet.at index f739ca7168..dc812fd8fa 100644 --- a/tests/language/data-io/get-data-spreadsheet.at +++ b/tests/language/data-io/get-data-spreadsheet.at @@ -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 . 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 @@ -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. -- 2.30.2