From: John Darrington Date: Sat, 10 Oct 2020 06:21:56 +0000 (+0200) Subject: Rework the spreadsheet import feature of the grapic user interface X-Git-Url: https://pintos-os.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=77e2017715a58c01d3e63ad90fb28b5e39eb2a31;p=pspp 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. --- 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 0000000000..dea5ce0415 Binary files /dev/null and b/tests/data/holey.gnumeric differ diff --git a/tests/data/holey.ods b/tests/data/holey.ods new file mode 100644 index 0000000000..46deafb9e4 Binary files /dev/null and b/tests/data/holey.ods differ diff --git a/tests/data/multisheet.gnumeric b/tests/data/multisheet.gnumeric new file mode 100644 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 0000000000..a8811bd318 Binary files /dev/null and b/tests/data/multisheet.ods differ diff --git a/tests/data/one-thousand-by-fifty-three.gnumeric b/tests/data/one-thousand-by-fifty-three.gnumeric new file mode 100644 index 0000000000..600bc77097 Binary files /dev/null and b/tests/data/one-thousand-by-fifty-three.gnumeric differ diff --git a/tests/data/one-thousand-by-fifty-three.ods b/tests/data/one-thousand-by-fifty-three.ods new file mode 100644 index 0000000000..a9fa56d548 Binary files /dev/null and b/tests/data/one-thousand-by-fifty-three.ods differ diff --git a/tests/data/repeating.gnumeric b/tests/data/repeating.gnumeric new file mode 100644 index 0000000000..3c59c28b9a Binary files /dev/null and b/tests/data/repeating.gnumeric differ diff --git a/tests/data/repeating.ods b/tests/data/repeating.ods new file mode 100644 index 0000000000..880f78b6cf Binary files /dev/null and b/tests/data/repeating.ods differ diff --git a/tests/data/simple.gnumeric b/tests/data/simple.gnumeric new file mode 100644 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 0000000000..8aba1dcb76 Binary files /dev/null and b/tests/data/simple.ods differ diff --git a/tests/data/sparse.gnumeric b/tests/data/sparse.gnumeric new file mode 100644 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 0000000000..8c1dcedcce Binary files /dev/null and b/tests/data/sparse.ods differ diff --git a/tests/data/spreadsheet-test.at b/tests/data/spreadsheet-test.at new file mode 100644 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.