diff --git a/configure.in b/configure.in index 69044555e5..b92e3623e1 100644 --- a/configure.in +++ b/configure.in @@ -1436,6 +1436,7 @@ AC_CONFIG_FILES(po/Makefile.in lib/libqof/qof/Makefile lib/libqof/backend/Makefile lib/libqof/backend/file/Makefile + lib/stf/Makefile packaging/Makefile packaging/win32/Makefile packaging/win32/gnucash.iss @@ -1489,6 +1490,7 @@ AC_CONFIG_FILES(po/Makefile.in src/import-export/schemas/Makefile src/import-export/ofx/Makefile src/import-export/ofx/test/Makefile + src/import-export/csv/Makefile src/import-export/log-replay/Makefile src/import-export/hbci/Makefile src/import-export/hbci/glade/Makefile diff --git a/lib/Makefile.am b/lib/Makefile.am index 496a2cd17e..c55337034a 100644 --- a/lib/Makefile.am +++ b/lib/Makefile.am @@ -1,5 +1,5 @@ -SUBDIRS = libc glib28 guile-www srfi -DIST_SUBDIRS = libc glib28 guile-www srfi libqof +SUBDIRS = libc glib28 guile-www srfi stf +DIST_SUBDIRS = libc glib28 guile-www srfi libqof stf if USE_LIBQOF SUBDIRS += libqof diff --git a/lib/stf/Makefile.am b/lib/stf/Makefile.am new file mode 100644 index 0000000000..5faabb02f5 --- /dev/null +++ b/lib/stf/Makefile.am @@ -0,0 +1,13 @@ +noinst_LTLIBRARIES = libgnc-stf.la + +REALSRCS = stf-parse.c +REALHDRS = stf-parse.h + +libgnc_stf_la_SOURCES = ${REALSRCS} +noinst_HEADERS = ${REALHDRS} + +libgnc_stf_la_LDFLAGS = $(pkg-config --libs libgoffice-0.3) + +AM_CFLAGS = $(GOFFICE_CFLAGS) + +EXTRA_DIST = $(REALSRCS) $(REALHDRS) diff --git a/lib/stf/README b/lib/stf/README new file mode 100644 index 0000000000..bb00f09372 --- /dev/null +++ b/lib/stf/README @@ -0,0 +1,2 @@ +This consists of code taken from Gnumeric's Structured Text Format +parser. It is used for the CSV/Fixed-Width file importer. diff --git a/lib/stf/stf-parse.c b/lib/stf/stf-parse.c new file mode 100644 index 0000000000..88d1c96f5d --- /dev/null +++ b/lib/stf/stf-parse.c @@ -0,0 +1,1414 @@ +/* vim: set sw=8: -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ +/* + * stf-parse.c : Structured Text Format parser. (STF) + * A general purpose engine for parsing data + * in CSV and Fixed width format. + * + * + * Copyright (C) Almer. S. Tigelaar. + * EMail: almer1@dds.nl or almer-t@bigfoot.com + * + * Copyright (C) 2003 Andreas J. Guelzow + * Copyright (C) 2003 Morten Welinder + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + */ + +#define GETTEXT_PACKAGE gnumeric + +#include +/* #include "gnumeric.h" */ +#include "stf-parse.h" + +/* #include "workbook.h" */ +/* #include "cell.h" */ +/* #include "sheet.h" */ +/* #include "clipboard.h" */ +/* #include "sheet-style.h" */ +/* #include "value.h" */ +/* #include "mstyle.h" */ +/* #include "number-match.h" */ +/* #include "gutils.h" */ +/* #include "parse-util.h" */ +#include +#include + +#include +#include +#include +#include +#include + +#define SETUP_LOCALE_SWITCH char *oldlocale = NULL + +#define START_LOCALE_SWITCH if (parseoptions->locale) {\ +oldlocale = g_strdup(go_setlocale (LC_ALL, NULL)); \ +go_setlocale(LC_ALL, parseoptions->locale);} + +#define END_LOCALE_SWITCH if (oldlocale) {\ +go_setlocale(LC_ALL, oldlocale);\ +g_free (oldlocale);} + +/* Source_t struct, used for interchanging parsing information between the low level parse functions */ +typedef struct { + GStringChunk *chunk; + char const *position; /* Indicates the current position within data */ + + /* Used internally for fixed width parsing */ + int splitpos; /* Indicates current position in splitpositions array */ + int linepos; /* Position on the current line */ +} Source_t; + +/* Struct used for autodiscovery */ +typedef struct { + int start; + int stop; +} AutoDiscovery_t; + +/* + * Some silly dude make the length field an unsigned int. C just does + * not deal very well with that. + */ +static inline int +my_garray_len (GArray const *a) +{ + return (int)a->len; +} + +static inline int +my_gptrarray_len (GPtrArray const *a) +{ + return (int)a->len; +} + +static int +compare_terminator (char const *s, StfParseOptions_t *parseoptions) +{ + guchar const *us = (guchar const *)s; + GSList *l; + + if (*us > parseoptions->compiled_terminator.max || + *us < parseoptions->compiled_terminator.min) + return 0; + + for (l = parseoptions->terminator; l; l = l->next) { + char const *term = l->data; + char const *d = s; + + while (*term) { + if (*d != *term) + goto next; + term++; + d++; + } + return d - s; + + next: + ; + } + return 0; +} + + +/******************************************************************************************************* + * STF PARSE OPTIONS : StfParseOptions related + *******************************************************************************************************/ + +/** + * stf_parse_options_new: + * + * This will return a new StfParseOptions_t struct. + * The struct should, after being used, freed with stf_parse_options_free. + **/ +StfParseOptions_t * +stf_parse_options_new (void) +{ + StfParseOptions_t* parseoptions = g_new0 (StfParseOptions_t, 1); + + parseoptions->parsetype = PARSE_TYPE_NOTSET; + + parseoptions->terminator = NULL; + stf_parse_options_add_line_terminator (parseoptions, "\r\n"); + stf_parse_options_add_line_terminator (parseoptions, "\n"); + stf_parse_options_add_line_terminator (parseoptions, "\r"); + + parseoptions->trim_spaces = (TRIM_TYPE_RIGHT | TRIM_TYPE_LEFT); + parseoptions->locale = NULL; + + parseoptions->splitpositions = NULL; + stf_parse_options_fixed_splitpositions_clear (parseoptions); + + parseoptions->stringindicator = '"'; + parseoptions->indicator_2x_is_single = TRUE; + parseoptions->duplicates = FALSE; + parseoptions->trim_seps = FALSE; + + parseoptions->sep.str = NULL; + parseoptions->sep.chr = NULL; + + parseoptions->col_import_array = NULL; + parseoptions->col_import_array_len = 0; + parseoptions->formats = NULL; + + parseoptions->cols_exceeded = FALSE; + + return parseoptions; +} + +/** + * stf_parse_options_free: + * + * will free @parseoptions, note that this will not free the splitpositions + * member (GArray) of the struct, the caller is responsible for that. + **/ +void +stf_parse_options_free (StfParseOptions_t *parseoptions) +{ + g_return_if_fail (parseoptions != NULL); + + g_free (parseoptions->col_import_array); + g_free (parseoptions->locale); + g_free (parseoptions->sep.chr); + + if (parseoptions->sep.str) { + GSList *l; + + for (l = parseoptions->sep.str; l != NULL; l = l->next) + g_free ((char *) l->data); + g_slist_free (parseoptions->sep.str); + } + + g_array_free (parseoptions->splitpositions, TRUE); + + stf_parse_options_clear_line_terminator (parseoptions); + + if (parseoptions->formats) { + unsigned int ui; + GPtrArray *formats = parseoptions->formats; + + for (ui = 0; ui < formats->len; ui++) + go_format_unref (g_ptr_array_index (formats, ui)); + g_ptr_array_free (formats, TRUE); + parseoptions->formats = NULL; + } + + g_free (parseoptions); +} + +void +stf_parse_options_set_type (StfParseOptions_t *parseoptions, StfParseType_t const parsetype) +{ + g_return_if_fail (parseoptions != NULL); + g_return_if_fail (parsetype == PARSE_TYPE_CSV || parsetype == PARSE_TYPE_FIXED); + + parseoptions->parsetype = parsetype; +} + +static gint +long_string_first (gchar const *a, gchar const *b) +{ + /* This actually is UTF-8 safe. */ + return strlen (b) - strlen (a); +} + +static void +compile_terminators (StfParseOptions_t *parseoptions) +{ + GSList *l; + GO_SLIST_SORT (parseoptions->terminator, (GCompareFunc)long_string_first); + + parseoptions->compiled_terminator.min = 255; + parseoptions->compiled_terminator.max = 0; + for (l = parseoptions->terminator; l; l = l->next) { + const guchar *term = l->data; + parseoptions->compiled_terminator.min = + MIN (parseoptions->compiled_terminator.min, *term); + parseoptions->compiled_terminator.max = + MAX (parseoptions->compiled_terminator.max, *term); + } +} + +/** + * stf_parse_options_add_line_terminator: + * + * This will add to the line terminators, in both the Fixed width and CSV delimited importers + * this indicates the end of a row. + * + **/ +void +stf_parse_options_add_line_terminator (StfParseOptions_t *parseoptions, char const *terminator) +{ + g_return_if_fail (parseoptions != NULL); + g_return_if_fail (terminator != NULL && *terminator != 0); + + GO_SLIST_PREPEND (parseoptions->terminator, g_strdup (terminator)); + compile_terminators (parseoptions); +} + +/** + * stf_parse_options_clear_line_terminator: + * + * This will clear the line terminator, in both the Fixed width and CSV delimited importers + * this indicates the end of a row. + * + **/ +void +stf_parse_options_clear_line_terminator (StfParseOptions_t *parseoptions) +{ + g_return_if_fail (parseoptions != NULL); + + go_slist_free_custom (parseoptions->terminator, g_free); + parseoptions->terminator = NULL; + compile_terminators (parseoptions); +} + +/** + * stf_parse_options_set_trim_spaces: + * + * If enabled will trim spaces in every parsed field on left and/or right + * sides. + **/ +void +stf_parse_options_set_trim_spaces (StfParseOptions_t *parseoptions, StfTrimType_t const trim_spaces) +{ + g_return_if_fail (parseoptions != NULL); + + parseoptions->trim_spaces = trim_spaces; +} + +/** + * stf_parse_options_csv_set_separators: + * + * A copy is made of the parameters. + **/ +void +stf_parse_options_csv_set_separators (StfParseOptions_t *parseoptions, char const *character, + GSList const *string) +{ + g_return_if_fail (parseoptions != NULL); + + g_free (parseoptions->sep.chr); + parseoptions->sep.chr = g_strdup (character); + + go_slist_free_custom (parseoptions->sep.str, g_free); + parseoptions->sep.str = go_slist_map (string, (GOMapFunc)g_strdup); +} + +void +stf_parse_options_csv_set_stringindicator (StfParseOptions_t *parseoptions, gunichar const stringindicator) +{ + g_return_if_fail (parseoptions != NULL); + g_return_if_fail (stringindicator != '\0'); + + parseoptions->stringindicator = stringindicator; +} + +/** + * stf_parse_options_csv_set_indicator_2x_is_single: + * @indic_2x : a boolean value indicating whether we want to see two + * adjacent string indicators as a single string indicator + * that is part of the cell, rather than a terminator. + **/ +void +stf_parse_options_csv_set_indicator_2x_is_single (StfParseOptions_t *parseoptions, + gboolean const indic_2x) +{ + g_return_if_fail (parseoptions != NULL); + + parseoptions->indicator_2x_is_single = indic_2x; +} + +/** + * stf_parse_options_csv_set_duplicates: + * @duplicates : a boolean value indicating whether we want to see two + * separators right behind each other as one + **/ +void +stf_parse_options_csv_set_duplicates (StfParseOptions_t *parseoptions, gboolean const duplicates) +{ + g_return_if_fail (parseoptions != NULL); + + parseoptions->duplicates = duplicates; +} + +/** + * stf_parse_options_csv_set_trim_seps: + * @trim_seps : a boolean value indicating whether we want to ignore + * separators at the beginning of lines + **/ +void +stf_parse_options_csv_set_trim_seps (StfParseOptions_t *parseoptions, gboolean const trim_seps) +{ + g_return_if_fail (parseoptions != NULL); + + parseoptions->trim_seps = trim_seps; +} + +/** + * stf_parse_options_fixed_splitpositions_clear: + * + * This will clear the splitpositions (== points on which a line is split) + **/ +void +stf_parse_options_fixed_splitpositions_clear (StfParseOptions_t *parseoptions) +{ + int minus_one = -1; + g_return_if_fail (parseoptions != NULL); + + if (parseoptions->splitpositions) + g_array_free (parseoptions->splitpositions, TRUE); + parseoptions->splitpositions = g_array_new (FALSE, FALSE, sizeof (int)); + + g_array_append_val (parseoptions->splitpositions, minus_one); +} + +/** + * stf_parse_options_fixed_splitpositions_add: + * + * @position will be added to the splitpositions. + **/ +void +stf_parse_options_fixed_splitpositions_add (StfParseOptions_t *parseoptions, int position) +{ + unsigned int ui; + + g_return_if_fail (parseoptions != NULL); + g_return_if_fail (position >= 0); + + for (ui = 0; ui < parseoptions->splitpositions->len - 1; ui++) { + int here = g_array_index (parseoptions->splitpositions, int, ui); + if (position == here) + return; + if (position < here) + break; + } + + g_array_insert_val (parseoptions->splitpositions, ui, position); +} + +void +stf_parse_options_fixed_splitpositions_remove (StfParseOptions_t *parseoptions, int position) +{ + unsigned int ui; + + g_return_if_fail (parseoptions != NULL); + g_return_if_fail (position >= 0); + + for (ui = 0; ui < parseoptions->splitpositions->len - 1; ui++) { + int here = g_array_index (parseoptions->splitpositions, int, ui); + if (position == here) + g_array_remove_index (parseoptions->splitpositions, ui); + if (position <= here) + return; + } +} + +int +stf_parse_options_fixed_splitpositions_count (StfParseOptions_t *parseoptions) +{ + return parseoptions->splitpositions->len; +} + +int +stf_parse_options_fixed_splitpositions_nth (StfParseOptions_t *parseoptions, int n) +{ + return g_array_index (parseoptions->splitpositions, int, n); +} + + +/** + * stf_parse_options_valid: + * @parseoptions : an import options struct + * + * Checks if @parseoptions is correctly filled + * + * returns : TRUE if it is correctly filled, FALSE otherwise. + **/ +static gboolean +stf_parse_options_valid (StfParseOptions_t *parseoptions) +{ + g_return_val_if_fail (parseoptions != NULL, FALSE); + + if (parseoptions->parsetype == PARSE_TYPE_CSV) { + if (parseoptions->stringindicator == '\0') { + g_warning ("STF: Cannot have \\0 as string indicator"); + return FALSE; + } + + } else if (parseoptions->parsetype == PARSE_TYPE_FIXED) { + if (!parseoptions->splitpositions) { + g_warning ("STF: No splitpositions in struct"); + return FALSE; + } + } + + return TRUE; +} + +/******************************************************************************************************* + * STF PARSE : The actual routines that do the 'trick' + *******************************************************************************************************/ + +static void +trim_spaces_inplace (char *field, StfParseOptions_t const *parseoptions) +{ + if (!field) return; + + if (parseoptions->trim_spaces & TRIM_TYPE_LEFT) { + char *s = field; + + while (g_unichar_isspace (g_utf8_get_char (s))) + s = g_utf8_next_char (s); + + if (s != field) + strcpy (field, s); + } + + if (parseoptions->trim_spaces & TRIM_TYPE_RIGHT) { + char *s = field + strlen (field); + + while (field != s) { + s = g_utf8_prev_char (s); + if (!g_unichar_isspace (g_utf8_get_char (s))) + break; + *s = 0; + } + } +} + +/** + * stf_parse_csv_is_separator: + * + * returns NULL if @character is not a separator, a pointer to the character + * after the separator otherwise. + **/ +static char const * +stf_parse_csv_is_separator (char const *character, char const *chr, GSList const *str) +{ + g_return_val_if_fail (character != NULL, NULL); + + if (*character == 0) + return NULL; + + if (str) { + GSList const *l; + + for (l = str; l != NULL; l = l->next) { + char const *s = l->data; + char const *r; + glong cnt; + glong const len = g_utf8_strlen (s, -1); + + /* Don't compare past the end of the buffer! */ + for (r = character, cnt = 0; cnt < len; cnt++, r = g_utf8_next_char (r)) + if (*r == '\0') + break; + + if ((cnt == len) && (memcmp (character, s, len) == 0)) + return g_utf8_offset_to_pointer (character, len); + } + } + + if (chr && g_utf8_strchr (chr, -1, + g_utf8_get_char (character))) + return g_utf8_next_char(character); + + return NULL; +} + +/* + * stf_parse_eat_separators: + * + * skip over leading separators + * + */ + +static void +stf_parse_eat_separators (Source_t *src, StfParseOptions_t *parseoptions) +{ + char const *cur, *next; + + g_return_if_fail (src != NULL); + g_return_if_fail (parseoptions != NULL); + + cur = src->position; + + if (*cur == '\0' || compare_terminator (cur, parseoptions)) + return; + while ((next = stf_parse_csv_is_separator (cur, parseoptions->sep.chr, parseoptions->sep.str))) + cur = next; + src->position = cur; + return; +} + + +typedef enum { + STF_CELL_ERROR, + STF_CELL_EOF, + STF_CELL_EOL, + STF_CELL_FIELD_NO_SEP, + STF_CELL_FIELD_SEP, +} StfParseCellRes; + +static StfParseCellRes +stf_parse_csv_cell (GString *text, Source_t *src, StfParseOptions_t *parseoptions) +{ + char const *cur; + gboolean saw_sep = FALSE; + + g_return_val_if_fail (src != NULL, STF_CELL_ERROR); + g_return_val_if_fail (parseoptions != NULL, STF_CELL_ERROR); + + cur = src->position; + g_return_val_if_fail (cur != NULL, STF_CELL_ERROR); + + /* Skip whitespace, but stop at line terminators. */ + while (1) { + int term_len; + + if (*cur == 0) { + src->position = cur; + return STF_CELL_EOF; + } + + term_len = compare_terminator (cur, parseoptions); + if (term_len) { + src->position = cur + term_len; + return STF_CELL_EOL; + } + + if ((parseoptions->trim_spaces & TRIM_TYPE_LEFT) == 0) + break; + + if (stf_parse_csv_is_separator (cur, parseoptions->sep.chr, + parseoptions->sep.str)) + break; + + if (!g_unichar_isspace (g_utf8_get_char (cur))) + break; + cur = g_utf8_next_char (cur); + } + + if (g_utf8_get_char (cur) == parseoptions->stringindicator) { + cur = g_utf8_next_char (cur); + while (*cur) { + gunichar uc = g_utf8_get_char (cur); + cur = g_utf8_next_char (cur); + + if (uc == parseoptions->stringindicator) { + if (parseoptions->indicator_2x_is_single && + g_utf8_get_char (cur) == parseoptions->stringindicator) + cur = g_utf8_next_char (cur); + else { + /* "field content"dropped-garbage, */ + while (*cur && !compare_terminator (cur, parseoptions)) { + char const *post = stf_parse_csv_is_separator + (cur, parseoptions->sep.chr, parseoptions->sep.str); + if (post) { + cur = post; + saw_sep = TRUE; + break; + } + cur = g_utf8_next_char (cur); + } + break; + } + } + + g_string_append_unichar (text, uc); + } + + /* We silently allow a missing terminating quote. */ + } else { + /* Unquoted field. */ + + while (*cur && !compare_terminator (cur, parseoptions)) { + + char const *post = stf_parse_csv_is_separator + (cur, parseoptions->sep.chr, parseoptions->sep.str); + if (post) { + cur = post; + saw_sep = TRUE; + break; + } + + g_string_append_unichar (text, g_utf8_get_char (cur)); + cur = g_utf8_next_char (cur); + } + + if (parseoptions->trim_spaces & TRIM_TYPE_RIGHT) { + while (text->len) { + const char *last = g_utf8_prev_char (text->str + text->len); + if (!g_unichar_isspace (g_utf8_get_char (last))) + break; + g_string_truncate (text, last - text->str); + } + } + } + + src->position = cur; + + if (saw_sep && parseoptions->duplicates) + stf_parse_eat_separators (src, parseoptions); + + return saw_sep ? STF_CELL_FIELD_SEP : STF_CELL_FIELD_NO_SEP; +} + +/** + * stf_parse_csv_line: + * + * This will parse one line from the current @src->position. + * NOTE: The calling routine is responsible for freeing the result. + * + * returns : a GPtrArray of char*'s + **/ +static GPtrArray * +stf_parse_csv_line (Source_t *src, StfParseOptions_t *parseoptions) +{ + GPtrArray *line; + gboolean cont = FALSE; + + g_return_val_if_fail (src != NULL, NULL); + g_return_val_if_fail (parseoptions != NULL, NULL); + + line = g_ptr_array_new (); + if (parseoptions->trim_seps) + stf_parse_eat_separators (src, parseoptions); + + while (1) { + GString *text = g_string_sized_new (30); + StfParseCellRes res = + stf_parse_csv_cell (text, src, parseoptions); + trim_spaces_inplace (text->str, parseoptions); + switch (res) { + case STF_CELL_FIELD_NO_SEP: + g_ptr_array_add (line, g_string_free (text, FALSE)); + cont = FALSE; + break; + + case STF_CELL_FIELD_SEP: + g_ptr_array_add (line, g_string_free (text, FALSE)); + cont = TRUE; /* Make sure we see one more field. */ + break; + + default: + if (cont) + g_ptr_array_add (line, g_string_free (text, FALSE)); + else + g_string_free (text, TRUE); + return line; + } + } +} + +/** + * stf_parse_fixed_cell: + * + * returns a pointer to the parsed cell contents. + **/ +static char * +stf_parse_fixed_cell (Source_t *src, StfParseOptions_t *parseoptions) +{ + char *res; + char const *cur; + int splitval; + + g_return_val_if_fail (src != NULL, NULL); + g_return_val_if_fail (parseoptions != NULL, NULL); + + cur = src->position; + + if (src->splitpos < my_garray_len (parseoptions->splitpositions)) + splitval = (int) g_array_index (parseoptions->splitpositions, int, src->splitpos); + else + splitval = -1; + + while (*cur != 0 && !compare_terminator (cur, parseoptions) && splitval != src->linepos) { + src->linepos++; + cur = g_utf8_next_char (cur); + } + + res = g_string_chunk_insert_len (src->chunk, + src->position, + cur - src->position); + + src->position = cur; + + return res; +} + +/** + * stf_parse_fixed_line: + * + * This will parse one line from the current @src->position. + * It will return a GPtrArray with the cell contents as strings. + + * NOTE: The calling routine is responsible for freeing result. + **/ +static GPtrArray * +stf_parse_fixed_line (Source_t *src, StfParseOptions_t *parseoptions) +{ + GPtrArray *line; + + g_return_val_if_fail (src != NULL, NULL); + g_return_val_if_fail (parseoptions != NULL, NULL); + + src->linepos = 0; + src->splitpos = 0; + + line = g_ptr_array_new (); + while (*src->position != '\0' && !compare_terminator (src->position, parseoptions)) { + char *field = stf_parse_fixed_cell (src, parseoptions); + + trim_spaces_inplace (field, parseoptions); + g_ptr_array_add (line, field); + + src->splitpos++; + } + + return line; +} + +void +stf_parse_general_free (GPtrArray *lines) +{ + unsigned lineno; + for (lineno = 0; lineno < lines->len; lineno++) { + GPtrArray *line = g_ptr_array_index (lines, lineno); + /* Fields are not free here. */ + g_ptr_array_free (line, TRUE); + } + g_ptr_array_free (lines, TRUE); +} + + +/** + * stf_parse_general: + * + * Returns a GPtrArray of lines, where each line is itself a + * GPtrArray of strings. + * + * The caller must free this entire structure, for example by calling + * stf_parse_general_free. + **/ +GPtrArray * +stf_parse_general (StfParseOptions_t *parseoptions, + GStringChunk *lines_chunk, + char const *data, char const *data_end) +{ + GPtrArray *lines; + Source_t src; + int row; + + g_return_val_if_fail (parseoptions != NULL, NULL); + g_return_val_if_fail (data != NULL, NULL); + g_return_val_if_fail (data_end != NULL, NULL); + g_return_val_if_fail (stf_parse_options_valid (parseoptions), NULL); + g_return_val_if_fail (g_utf8_validate (data, -1, NULL), NULL); + + src.chunk = lines_chunk; + src.position = data; + row = 0; + + lines = g_ptr_array_new (); + while (*src.position != '\0' && src.position < data_end) { + GPtrArray *line; + + line = parseoptions->parsetype == PARSE_TYPE_CSV + ? stf_parse_csv_line (&src, parseoptions) + : stf_parse_fixed_line (&src, parseoptions); + + g_ptr_array_add (lines, line); + if (parseoptions->parsetype != PARSE_TYPE_CSV) + src.position += compare_terminator (src.position, parseoptions); + + if (++row == SHEET_MAX_ROWS) + break; + } + + return lines; +} + +GPtrArray * +stf_parse_lines (StfParseOptions_t *parseoptions, + GStringChunk *lines_chunk, + char const *data, + int maxlines, gboolean with_lineno) +{ + GPtrArray *lines; + int lineno = 1; + + g_return_val_if_fail (data != NULL, NULL); + + lines = g_ptr_array_new (); + while (*data) { + char const *data0 = data; + GPtrArray *line = g_ptr_array_new (); + + if (with_lineno) { + char buf[4 * sizeof (int)]; + sprintf (buf, "%d", lineno); + g_ptr_array_add (line, + g_string_chunk_insert (lines_chunk, buf)); + } + + while (1) { + int termlen = compare_terminator (data, parseoptions); + if (termlen > 0 || *data == 0) { + g_ptr_array_add (line, + g_string_chunk_insert_len (lines_chunk, + data0, + data - data0)); + data += termlen; + break; + } else + data = g_utf8_next_char (data); + } + + g_ptr_array_add (lines, line); + + lineno++; + if (lineno >= maxlines) + break; + } + return lines; +} + +char const * +stf_parse_find_line (StfParseOptions_t *parseoptions, + char const *data, + int line) +{ + while (line > 0) { + int termlen = compare_terminator (data, parseoptions); + if (termlen > 0) { + data += termlen; + line--; + } else if (*data == 0) { + return data; + } else { + data = g_utf8_next_char (data); + } + } + return data; +} + + +/** + * stf_parse_options_fixed_autodiscover: + * @parseoptions: a Parse options struct. + * @data_lines : The number of lines to look at in @data. + * @data : The actual data. + * + * Automatically try to discover columns in the text to be parsed. + * We ignore empty lines (only containing parseoptions->terminator) + * + * FIXME: This is so extremely ugly that I am too tired to rewrite it right now. + * Think hard of a better more flexible solution... + **/ +void +stf_parse_options_fixed_autodiscover (StfParseOptions_t *parseoptions, + char const *data, char const *data_end) +{ + char const *iterator = data; + GSList *list = NULL; + GSList *list_start = NULL; + int lines = 0; + int effective_lines = 0; + int max_line_length = 0; + int *line_begin_hits = NULL; + int *line_end_hits = NULL; + int i; + + stf_parse_options_fixed_splitpositions_clear (parseoptions); + + /* + * First take a look at all possible white space combinations + */ + while (*iterator && iterator < data_end) { + gboolean begin_recorded = FALSE; + AutoDiscovery_t *disc = NULL; + int position = 0; + int termlen = 0; + + while (*iterator && (termlen = compare_terminator (iterator, parseoptions)) == 0) { + if (!begin_recorded && *iterator == ' ') { + disc = g_new0 (AutoDiscovery_t, 1); + + disc->start = position; + + begin_recorded = TRUE; + } else if (begin_recorded && *iterator != ' ') { + disc->stop = position; + list = g_slist_prepend (list, disc); + + begin_recorded = FALSE; + disc = NULL; + } + + position++; + iterator++; + } + + if (position > max_line_length) + max_line_length = position; + + /* + * If there are excess spaces at the end of + * the line : ignore them + */ + g_free (disc); + + /* + * Hop over the terminator + */ + iterator += termlen; + + if (position != 0) + effective_lines++; + + lines++; + } + + list = g_slist_reverse (list); + list_start = list; + + /* + * Kewl stuff : + * Look at the number of hits at each line position + * if the number of hits equals the number of lines + * we can be pretty sure this is the start or end + * of a column, we filter out empty columns + * later + */ + line_begin_hits = g_new0 (int, max_line_length + 1); + line_end_hits = g_new0 (int, max_line_length + 1); + + while (list) { + AutoDiscovery_t *disc = list->data; + + line_begin_hits[disc->start]++; + line_end_hits[disc->stop]++; + + g_free (disc); + + list = g_slist_next (list); + } + g_slist_free (list_start); + + for (i = 0; i < max_line_length + 1; i++) + if (line_begin_hits[i] == effective_lines || line_end_hits[i] == effective_lines) + stf_parse_options_fixed_splitpositions_add (parseoptions, i); + + /* + * Do some corrections to the initial columns + * detected here, we obviously don't need to + * do this if there are no columns at all. + */ + if (my_garray_len (parseoptions->splitpositions) > 0) { + /* + * Try to find columns that look like : + * + * Example 100 + * Example2 9 + * + * (In other words : Columns with left & right justification with + * a minimum of 2 spaces in the middle) + * Split these columns in 2 + */ + + for (i = 0; i < my_garray_len (parseoptions->splitpositions) - 1; i++) { + int begin = g_array_index (parseoptions->splitpositions, int, i); + int end = g_array_index (parseoptions->splitpositions, int, i + 1); + int num_spaces = -1; + int spaces_start = 0; + gboolean right_aligned = TRUE; + gboolean left_aligned = TRUE; + gboolean has_2_spaces = TRUE; + + iterator = data; + lines = 0; + while (*iterator && iterator < data_end) { + gboolean trigger = FALSE; + gboolean space_trigger = FALSE; + int pos = 0; + + num_spaces = -1; + spaces_start = 0; + while (*iterator && !compare_terminator (iterator, parseoptions)) { + if (pos == begin) { + if (*iterator == ' ') + left_aligned = FALSE; + + trigger = TRUE; + } else if (pos == end - 1) { + if (*iterator == ' ') + right_aligned = FALSE; + + trigger = FALSE; + } + + if (trigger || pos == end - 1) { + if (!space_trigger && *iterator == ' ') { + space_trigger = TRUE; + spaces_start = pos; + } else if (space_trigger && *iterator != ' ') { + space_trigger = FALSE; + num_spaces = pos - spaces_start; + } + } + + iterator++; + pos++; + } + + if (num_spaces < 2) + has_2_spaces = FALSE; + + if (*iterator) + iterator++; + + lines++; + } + + /* + * If this column meets all the criteria + * split it into two at the last measured + * spaces_start + num_spaces + */ + if (has_2_spaces && right_aligned && left_aligned) { + int val = (((spaces_start + num_spaces) - spaces_start) / 2) + spaces_start; + + g_array_insert_val (parseoptions->splitpositions, i + 1, val); + + /* + * Skip over the inserted column + */ + i++; + } + } + + /* + * Remove empty columns here if needed + */ + for (i = 0; i < my_garray_len (parseoptions->splitpositions) - 1; i++) { + int begin = g_array_index (parseoptions->splitpositions, int, i); + int end = g_array_index (parseoptions->splitpositions, int, i + 1); + gboolean only_spaces = TRUE; + + iterator = data; + lines = 0; + while (*iterator && iterator < data_end) { + gboolean trigger = FALSE; + int pos = 0; + + while (*iterator && !compare_terminator (iterator, parseoptions)) { + if (pos == begin) + trigger = TRUE; + else if (pos == end) + trigger = FALSE; + + if (trigger) { + if (*iterator != ' ') + only_spaces = FALSE; + } + + iterator++; + pos++; + } + + if (*iterator) + iterator++; + + lines++; + } + + /* + * The column only contains spaces + * remove it + */ + if (only_spaces) { + g_array_remove_index (parseoptions->splitpositions, i); + + /* + * We HAVE to make sure that the next column (end) also + * gets checked out. If we don't decrease "i" here, we + * will skip over it as the indexes shift down after + * the removal + */ + i--; + } + } + } + + g_free (line_begin_hits); + g_free (line_end_hits); +} + +/******************************************************************************************************* + * STF PARSE HL: high-level functions that dump the raw data returned by the low-level parsing + * functions into something meaningful (== application specific) + *******************************************************************************************************/ + +/* gboolean */ +/* stf_parse_sheet (StfParseOptions_t *parseoptions, */ +/* char const *data, char const *data_end, */ +/* Sheet *sheet, int start_col, int start_row) */ +/* { */ +/* int row, col; */ +/* unsigned int lrow, lcol; */ +/* GODateConventions const *date_conv; */ +/* GStringChunk *lines_chunk; */ +/* GPtrArray *lines, *line; */ + +/* SETUP_LOCALE_SWITCH; */ + +/* g_return_val_if_fail (parseoptions != NULL, FALSE); */ +/* g_return_val_if_fail (data != NULL, FALSE); */ +/* g_return_val_if_fail (IS_SHEET (sheet), FALSE); */ + +/* START_LOCALE_SWITCH; */ + +/* date_conv = workbook_date_conv (sheet->workbook); */ + +/* if (!data_end) */ +/* data_end = data + strlen (data); */ +/* lines_chunk = g_string_chunk_new (100 * 1024); */ +/* lines = stf_parse_general (parseoptions, lines_chunk, data, data_end); */ +/* if (lines == NULL) */ +/* return FALSE; */ +/* for (row = start_row, lrow = 0; lrow < lines->len ; row++, lrow++) { */ +/* col = start_col; */ +/* line = g_ptr_array_index (lines, lrow); */ + +/* for (lcol = 0; lcol < line->len; lcol++) */ +/* if (parseoptions->col_import_array == NULL || */ +/* parseoptions->col_import_array_len <= lcol || */ +/* parseoptions->col_import_array[lcol]) { */ +/* if (col >= SHEET_MAX_COLS) { */ +/* if (!parseoptions->cols_exceeded) { */ +/* g_warning (_("There are more columns of data than " */ +/* "there is room for in the sheet. Extra " */ +/* "columns will be ignored.")); */ +/* parseoptions->cols_exceeded = TRUE; */ +/* } */ +/* } else { */ +/* char const *text = g_ptr_array_index (line, lcol); */ +/* if (text && *text) */ +/* gnm_cell_set_text ( */ +/* sheet_cell_fetch (sheet, col, row), */ +/* text); */ +/* } */ +/* col++; */ +/* } */ +/* } */ + +/* stf_parse_general_free (lines); */ +/* g_string_chunk_free (lines_chunk); */ +/* END_LOCALE_SWITCH; */ +/* return TRUE; */ +/* } */ + +/* GnmCellRegion * */ +/* stf_parse_region (StfParseOptions_t *parseoptions, char const *data, char const *data_end, */ +/* Workbook const *wb) */ +/* { */ +/* static GODateConventions const default_conv = {FALSE}; */ +/* GODateConventions const *date_conv = wb ? workbook_date_conv (wb) : &default_conv; */ + +/* GnmCellRegion *cr; */ +/* unsigned int row, colhigh = 0; */ +/* char *text; */ +/* GStringChunk *lines_chunk; */ +/* GPtrArray *lines; */ +/* GnmCellCopy *cc; */ +/* GOFormat *fmt; */ +/* GnmValue *v; */ + +/* SETUP_LOCALE_SWITCH; */ + +/* g_return_val_if_fail (parseoptions != NULL, NULL); */ +/* g_return_val_if_fail (data != NULL, NULL); */ + +/* START_LOCALE_SWITCH; */ + +/* cr = cellregion_new (NULL); */ + +/* if (!data_end) */ +/* data_end = data + strlen (data); */ +/* lines_chunk = g_string_chunk_new (100 * 1024); */ +/* lines = stf_parse_general (parseoptions, lines_chunk, data, data_end); */ +/* for (row = 0; row < lines->len; row++) { */ +/* GPtrArray *line = g_ptr_array_index (lines, row); */ +/* unsigned int col, targetcol = 0; */ +/* for (col = 0; col < line->len; col++) { */ +/* if (parseoptions->col_import_array == NULL || */ +/* parseoptions->col_import_array_len <= col || */ +/* parseoptions->col_import_array[col]) { */ +/* if (NULL != (text = g_ptr_array_index (line, col))) { */ +/* fmt = g_ptr_array_index ( */ +/* parseoptions->formats, col); */ +/* if (NULL == (v = format_match (text, fmt, date_conv))) */ +/* v = value_new_string (text); */ + +/* cc = gnm_cell_copy_new (cr, targetcol, row); */ +/* cc->val = v; */ +/* cc->texpr = NULL; */ +/* targetcol++; */ +/* if (targetcol > colhigh) */ +/* colhigh = targetcol; */ +/* } */ +/* } */ +/* } */ +/* } */ +/* stf_parse_general_free (lines); */ +/* g_string_chunk_free (lines_chunk); */ + +/* END_LOCALE_SWITCH; */ + +/* cr->cols = (colhigh > 0) ? colhigh : 1; */ +/* cr->rows = row; */ + +/* return cr; */ +/* } */ + +static int +int_sort (void const *a, void const *b) +{ + return *(int const *)a - *(int const *)b; +} + +static int +count_character (GPtrArray *lines, gunichar c, double quantile) +{ + int *counts, res; + unsigned int lno, cno; + + if (lines->len == 0) + return 0; + + counts = g_new (int, lines->len); + for (lno = cno = 0; lno < lines->len; lno++) { + int count = 0; + GPtrArray *boxline = g_ptr_array_index (lines, lno); + char const *line = g_ptr_array_index (boxline, 0); + + /* Ignore empty lines. */ + if (*line == 0) + continue; + + while (*line) { + if (g_utf8_get_char (line) == c) + count++; + line = g_utf8_next_char (line); + } + + counts[cno++] = count; + } + + if (cno == 0) + res = 0; + else { + unsigned int qi = (unsigned int)ceil (quantile * cno); + qsort (counts, cno, sizeof (counts[0]), int_sort); + if (qi == cno) + qi--; + res = counts[qi]; + } + + g_free (counts); + + return res; +} + + +StfParseOptions_t * +stf_parse_options_guess (char const *data) +{ + StfParseOptions_t *res; + GStringChunk *lines_chunk; + GPtrArray *lines; + int tabcount; + int sepcount; + /* TODO In the future, use the goffice 0.3. */ + /* gunichar sepchar = go_locale_get_arg_sep (); */ + gunichar sepchar = ','; + + g_return_val_if_fail (data != NULL, NULL); + + res = stf_parse_options_new (); + lines_chunk = g_string_chunk_new (100 * 1024); + lines = stf_parse_lines (res, lines_chunk, data, SHEET_MAX_ROWS, FALSE); + + tabcount = count_character (lines, '\t', 0.2); + sepcount = count_character (lines, sepchar, 0.2); + + /* At least one tab per line and enough to separate every + would-be sepchars. */ + if (tabcount >= 1 && tabcount >= sepcount - 1) + stf_parse_options_csv_set_separators (res, "\t", NULL); + else { + gunichar c; + + /* + * Try a few more or less likely characters and pick the first + * one that occurs on at least half the lines. + * + * The order is mostly random, although ' ' and '!' which + * could very easily occur in text are put last. + */ + /* TODO Replace with the 0.3 goffice call in the future. */ + if (count_character (lines, (c = sepchar), 0.5) > 0 || + /* count_character (lines, (c = go_locale_get_col_sep ()), 0.5) > 0 || */ + count_character (lines, (c = ','), 0.5) > 0 || + count_character (lines, (c = ':'), 0.5) > 0 || + count_character (lines, (c = ','), 0.5) > 0 || + count_character (lines, (c = ';'), 0.5) > 0 || + count_character (lines, (c = '|'), 0.5) > 0 || + count_character (lines, (c = '!'), 0.5) > 0 || + count_character (lines, (c = ' '), 0.5) > 0) { + char sep[7]; + sep[g_unichar_to_utf8 (c, sep)] = 0; + if (c == ' ') + strcat (sep, "\t"); + stf_parse_options_csv_set_separators (res, sep, NULL); + } + } + + if (1) { + /* Separated */ + gboolean dups = + res->sep.chr && + strchr (res->sep.chr, ' ') != NULL; + gboolean trim = + res->sep.chr && + strchr (res->sep.chr, ' ') != NULL; + + stf_parse_options_set_type (res, PARSE_TYPE_CSV); + stf_parse_options_set_trim_spaces (res, TRIM_TYPE_LEFT | TRIM_TYPE_RIGHT); + stf_parse_options_csv_set_indicator_2x_is_single (res, TRUE); + stf_parse_options_csv_set_duplicates (res, dups); + stf_parse_options_csv_set_trim_seps (res, trim); + + stf_parse_options_csv_set_stringindicator (res, '"'); + } else { + /* Fixed-width */ + } + + stf_parse_general_free (lines); + g_string_chunk_free (lines_chunk); + + return res; +} diff --git a/lib/stf/stf-parse.h b/lib/stf/stf-parse.h new file mode 100644 index 0000000000..71e5510a43 --- /dev/null +++ b/lib/stf/stf-parse.h @@ -0,0 +1,112 @@ +#ifndef STF_PARSE_H +#define STF_PARSE_H + +#include + +#define SHEET_MAX_ROWS (16*16*16*16) /* 0, 1, ... */ +#define SHEET_MAX_COLS (4*4*4*4) /* 0, 1, ... */ + +typedef enum { + PARSE_TYPE_NOTSET = 1 << 0, + PARSE_TYPE_CSV = 1 << 1, + PARSE_TYPE_FIXED = 1 << 2 +} StfParseType_t; + +/* Additive. */ +typedef enum { + TRIM_TYPE_NEVER = 0, + TRIM_TYPE_LEFT = 1 << 0, + TRIM_TYPE_RIGHT = 2 << 1 +} StfTrimType_t; + +typedef struct { + StfParseType_t parsetype; /* The type of import to do */ + StfTrimType_t trim_spaces; /* Trim spaces in fields? */ + + GSList * terminator; /* Line terminators */ + char * locale; + + struct { + guchar min, max; + } compiled_terminator; + + /* CSV related */ + struct { + GSList *str; + char *chr; + } sep; + gunichar stringindicator; /* String indicator */ + gboolean indicator_2x_is_single;/* 2 quote chars form a single non-terminating quote */ + gboolean duplicates; /* See two text separators as one? */ + gboolean trim_seps; /* Ignore initial seps. */ + + /* Fixed width related */ + GArray *splitpositions; /* Positions where text will be split vertically */ + + int rowcount; /* Number of rows parsed */ + int colcount; /* Number of columns parsed */ + gboolean *col_import_array; /* 0/1 array indicating */ + /* which cols to import */ + unsigned int col_import_array_len; + GPtrArray *formats ; /* Contains GnmFormat *s */ + gboolean cols_exceeded; /* This is set to TRUE if */ + /* we tried to import more than */ + /* SHEET_MAX_COLS columns */ +} StfParseOptions_t; + +/* CREATION/DESTRUCTION of stf options struct */ + +StfParseOptions_t *stf_parse_options_new (void); +void stf_parse_options_free (StfParseOptions_t *parseoptions); + +StfParseOptions_t *stf_parse_options_guess (char const *data); + +/* MANIPULATION of stf options struct */ + +void stf_parse_options_set_type (StfParseOptions_t *parseoptions, + StfParseType_t const parsetype); +void stf_parse_options_clear_line_terminator (StfParseOptions_t *parseoptions); +void stf_parse_options_add_line_terminator (StfParseOptions_t *parseoptions, + char const *terminator); +void stf_parse_options_set_trim_spaces (StfParseOptions_t *parseoptions, + StfTrimType_t const trim_spaces); +void stf_parse_options_csv_set_separators (StfParseOptions_t *parseoptions, + char const *character, GSList const *string); +void stf_parse_options_csv_set_stringindicator (StfParseOptions_t *parseoptions, + gunichar const stringindicator); +void stf_parse_options_csv_set_indicator_2x_is_single (StfParseOptions_t *parseoptions, + gboolean const indic_2x); +void stf_parse_options_csv_set_duplicates (StfParseOptions_t *parseoptions, + gboolean const duplicates); +void stf_parse_options_csv_set_trim_seps (StfParseOptions_t *parseoptions, + gboolean const trim_seps); +void stf_parse_options_fixed_splitpositions_clear (StfParseOptions_t *parseoptions); +void stf_parse_options_fixed_splitpositions_add (StfParseOptions_t *parseoptions, + int position); +void stf_parse_options_fixed_splitpositions_remove (StfParseOptions_t *parseoptions, + int position); +int stf_parse_options_fixed_splitpositions_count (StfParseOptions_t *parseoptions); +int stf_parse_options_fixed_splitpositions_nth (StfParseOptions_t *parseoptions, int n); + +/* USING the stf structs to actually do some parsing, these are the lower-level functions and utility functions */ + +GPtrArray *stf_parse_general (StfParseOptions_t *parseoptions, + GStringChunk *lines_chunk, + char const *data, + char const *data_end); +void stf_parse_general_free (GPtrArray *lines); +GPtrArray *stf_parse_lines (StfParseOptions_t *parseoptions, + GStringChunk *lines_chunk, + char const *data, + int maxlines, + gboolean with_lineno); + +void stf_parse_options_fixed_autodiscover (StfParseOptions_t *parseoptions, + char const *data, + char const *data_end); + +char const *stf_parse_find_line (StfParseOptions_t *parseoptions, + char const *data, + int line); + +#endif diff --git a/src/bin/gnucash-bin.c b/src/bin/gnucash-bin.c index 928bc0261b..0d68b45334 100644 --- a/src/bin/gnucash-bin.c +++ b/src/bin/gnucash-bin.c @@ -337,6 +337,7 @@ load_gnucash_modules() { "gnucash/register/register-gnome", 0, FALSE }, { "gnucash/import-export/qif-import", 0, FALSE }, { "gnucash/import-export/ofx", 0, TRUE }, + { "gnucash/import-export/csv", 0, TRUE }, { "gnucash/import-export/log-replay", 0, TRUE }, { "gnucash/import-export/hbci", 0, TRUE }, { "gnucash/report/report-system", 0, FALSE }, diff --git a/src/import-export/Makefile.am b/src/import-export/Makefile.am index 8436b8cdb1..75516a6711 100644 --- a/src/import-export/Makefile.am +++ b/src/import-export/Makefile.am @@ -1,7 +1,7 @@ SUBDIRS = . schemas qif qif-import \ - ${OFX_DIR} ${HBCI_DIR} log-replay test + ${OFX_DIR} ${HBCI_DIR} log-replay test csv DIST_SUBDIRS = schemas qif qif-import qif-io-core \ - ofx hbci log-replay test + ofx hbci log-replay test csv pkglib_LTLIBRARIES=libgncmod-generic-import.la diff --git a/src/import-export/csv/Makefile.am b/src/import-export/csv/Makefile.am new file mode 100644 index 0000000000..53efd45594 --- /dev/null +++ b/src/import-export/csv/Makefile.am @@ -0,0 +1,59 @@ +SUBDIRS = . + +pkglib_LTLIBRARIES=libgncmod-csv.la + +libgncmod_csv_la_SOURCES = \ + gncmod-csv-import.c \ + gnc-plugin-csv.c \ + gnc-csv-import.c \ + gnc-csv-model.c \ + gnc-csv-gnumeric-popup.c + +noinst_HEADERS = \ + gnc-plugin-csv.h \ + gnc-csv-import.h \ + gnc-csv-model.h \ + gnc-csv-gnumeric-popup.h + +libgncmod_csv_la_LDFLAGS = -avoid-version $(pkg-config --libs libgoffice-0.3) + +libgncmod_csv_la_LIBADD = \ + ${top_builddir}/src/import-export/libgncmod-generic-import.la \ + ${top_builddir}/src/gnome-utils/libgncmod-gnome-utils.la \ + ${top_builddir}/src/app-utils/libgncmod-app-utils.la \ + ${top_builddir}/src/engine/libgncmod-engine.la \ + ${top_builddir}/src/core-utils/libgnc-core-utils.la \ + ${top_builddir}/src/gnc-module/libgnc-module.la \ + ${top_builddir}/lib/stf/libgnc-stf.la \ + ${QOF_LIBS} \ + ${GLIB_LIBS} + +AM_CFLAGS = \ + -I${top_srcdir}/src \ + -I${top_srcdir}/src/core-utils \ + -I${top_srcdir}/src/engine \ + -I${top_srcdir}/src/gnc-module \ + -I${top_srcdir}/src/app-utils \ + -I${top_srcdir}/src/gnome \ + -I${top_srcdir}/src/gnome-utils \ + -I${top_srcdir}/src/import-export \ + -I${top_srcdir}/lib \ + ${GNOME_CFLAGS} \ + ${GTKHTML_CFLAGS} \ + ${GLADE_CFLAGS} \ + ${GUILE_INCS} \ + ${QOF_CFLAGS} \ + ${GLIB_CFLAGS} \ + $(GOFFICE_CFLAGS) + +uidir = $(GNC_UI_DIR) +ui_DATA = \ + gnc-plugin-csv-ui.xml + +gladedir = ${GNC_GLADE_DIR} +glade_DATA = \ + gnc-csv-preview-dialog.glade + +EXTRA_DIST = $(ui_DATA) + +INCLUDES = -DG_LOG_DOMAIN=\"gnc.import.csv\" diff --git a/src/import-export/csv/example-file.csv b/src/import-export/csv/example-file.csv new file mode 100644 index 0000000000..49c5c5704c --- /dev/null +++ b/src/import-export/csv/example-file.csv @@ -0,0 +1,4 @@ +"This file has colon separators":100:01/03/95 +"and the last line":-50:02.28.96 +"uses a different":-25.13:03/15/00 +"date format.":12.5:30-4-02 diff --git a/src/import-export/csv/gnc-csv-gnumeric-popup.c b/src/import-export/csv/gnc-csv-gnumeric-popup.c new file mode 100644 index 0000000000..04295028e1 --- /dev/null +++ b/src/import-export/csv/gnc-csv-gnumeric-popup.c @@ -0,0 +1,194 @@ +/* The following is code copied from Gnumeric 1.7.8 licensed under the + * GNU General Public License version 2. It is from the file + * gnumeric/src/gui-util.c, and it has been modified slightly to work + * within GnuCash. */ + +/* Miguel de Icaza is not sure specifically who from the Gnumeric + * community is the copyright owner of the code below, so, on his + * recommendation, here is the full list of Gnumeric authors. + * + * Miguel de Icaza, creator. + * Jody Goldberg, maintainer. + * Harald Ashburner, Options pricers + * Sean Atkinson, functions and X-Base importing. + * Michel Berkelaar, Simplex algorithm for Solver (LP Solve). + * Jean Brefort, Core charting engine. + * Grandma Chema Celorio, Tester and sheet copy. + * Frank Chiulli, OLE support. + * Kenneth Christiansen, i18n, misc stuff. + * Zbigniew Chyla, plugin system, i18n. + * J.H.M. Dassen (Ray), debian packaging. + * Jeroen Dirks, Simplex algorithm for Solver (LP Solve). + * Tom Dyas, plugin support. + * Gergo Erdi, Gnumeric hacker. + * John Gotts, rpm packaging. + * Andreas J. Guelzow, Gnumeric hacker. + * Jon K. Hellan, Gnumeric hacker. + * Ross Ihaka, special functions. + * Jukka-Pekka Iivonen, numerous functions and tools. + * Jakub Jelinek, Gnumeric hacker. + * Chris Lahey, number format engine. + * Adrian Likins, documentation, debugging. + * Takashi Matsuda, original text plugin. + * Michael Meeks, Excel and OLE2 importing. + * Lutz Muller, SheetObject improvements. + * Emmanuel Pacaud, Many plot types for charting engine. + * Federico M. Quintero, canvas support. + * Mark Probst, Guile support. + * Rasca, HTML, troff, LaTeX exporters. + * Vincent Renardias, original CSV support, French localization. + * Ariel Rios, Guile support. + * Uwe Steinmann, Paradox Importer. + * Arturo Tena, OLE support. + * Almer S. Tigelaar, Gnumeric hacker. + * Bruno Unna, Excel bits. + * Daniel Veillard, XML support. + * Vladimir Vuksan, financial functions. + * Morten Welinder, Gnumeric hacker and leak plugging demi-god. + */ + +#include "gnc-csv-gnumeric-popup.h" + +#include + +static void +popup_item_activate (GtkWidget *item, gpointer *user_data) +{ + GnumericPopupMenuElement const *elem = + g_object_get_data (G_OBJECT (item), "descriptor"); + GnumericPopupMenuHandler handler = + g_object_get_data (G_OBJECT (item), "handler"); + + g_return_if_fail (elem != NULL); + g_return_if_fail (handler != NULL); + + if (handler (elem, user_data)) + gtk_widget_destroy (gtk_widget_get_toplevel (item)); +} + +static void +gnumeric_create_popup_menu_list (GSList *elements, + GnumericPopupMenuHandler handler, + gpointer user_data, + int display_filter, + int sensitive_filter, + GdkEventButton *event) +{ + GtkWidget *menu, *item; + char const *trans; + + menu = gtk_menu_new (); + + for (; elements != NULL ; elements = elements->next) { + GnumericPopupMenuElement const *element = elements->data; + char const * const name = element->name; + char const * const pix_name = element->pixmap; + + item = NULL; + + if (element->display_filter != 0 && + !(element->display_filter & display_filter)) + continue; + + if (name != NULL && *name != '\0') { + trans = _(name); + item = gtk_image_menu_item_new_with_mnemonic (trans); + if (element->sensitive_filter != 0 && + (element->sensitive_filter & sensitive_filter)) + gtk_widget_set_sensitive (GTK_WIDGET (item), FALSE); + if (pix_name != NULL) { + GtkWidget *image = gtk_image_new_from_stock (pix_name, + GTK_ICON_SIZE_MENU); + gtk_widget_show (image); + gtk_image_menu_item_set_image ( + GTK_IMAGE_MENU_ITEM (item), + image); + } + } else { + /* separator */ + item = gtk_menu_item_new (); + gtk_widget_set_sensitive (item, FALSE); + } + + if (element->index != 0) { + g_signal_connect (G_OBJECT (item), + "activate", + G_CALLBACK (&popup_item_activate), user_data); + g_object_set_data ( + G_OBJECT (item), "descriptor", (gpointer)(element)); + g_object_set_data ( + G_OBJECT (item), "handler", (gpointer)handler); + } + + gtk_widget_show (item); + gtk_menu_shell_append (GTK_MENU_SHELL (menu), item); + } + + gnumeric_popup_menu (GTK_MENU (menu), event); +} + +void +gnumeric_create_popup_menu (GnumericPopupMenuElement const *elements, + GnumericPopupMenuHandler handler, + gpointer user_data, + int display_filter, int sensitive_filter, + GdkEventButton *event) +{ + int i; + GSList *tmp = NULL; + + for (i = 0; elements [i].name != NULL; i++) + tmp = g_slist_prepend (tmp, (gpointer)(elements + i)); + + tmp = g_slist_reverse (tmp); + gnumeric_create_popup_menu_list (tmp, handler, user_data, + display_filter, sensitive_filter, event); + g_slist_free (tmp); +} + +static void +kill_popup_menu (GtkWidget *widget, GtkMenu *menu) +{ + g_return_if_fail (menu != NULL); + g_return_if_fail (GTK_IS_MENU (menu)); + + g_object_unref (G_OBJECT (menu)); +} + +/** + * gnumeric_popup_menu : + * @menu : #GtkMenu + * @event : #GdkEventButton optionally NULL + * + * Bring up a popup and if @event is non-NULL ensure that the popup is on the + * right screen. + **/ +void +gnumeric_popup_menu (GtkMenu *menu, GdkEventButton *event) +{ + g_return_if_fail (menu != NULL); + g_return_if_fail (GTK_IS_MENU (menu)); + +#if GLIB_CHECK_VERSION(2,10,0) && GTK_CHECK_VERSION(2,8,14) + g_object_ref_sink (menu); +#else + g_object_ref (menu); + gtk_object_sink (GTK_OBJECT (menu)); +#endif + + if (event) + gtk_menu_set_screen (menu, + gdk_drawable_get_screen (event->window)); + + g_signal_connect (G_OBJECT (menu), + "hide", + G_CALLBACK (kill_popup_menu), menu); + + /* Do NOT pass the button used to create the menu. + * instead pass 0. Otherwise bringing up a menu with + * the right button will disable clicking on the menu with the left. + */ + gtk_menu_popup (menu, NULL, NULL, NULL, NULL, 0, + (event != NULL) ? event->time + : gtk_get_current_event_time()); +} diff --git a/src/import-export/csv/gnc-csv-gnumeric-popup.h b/src/import-export/csv/gnc-csv-gnumeric-popup.h new file mode 100644 index 0000000000..215164ec33 --- /dev/null +++ b/src/import-export/csv/gnc-csv-gnumeric-popup.h @@ -0,0 +1,78 @@ +/* The following is code copied from Gnumeric 1.7.8 licensed under the + * GNU General Public License version 2. It is from the file + * gnumeric/src/gui-util.h, and it has been modified slightly to work + * within GnuCash. */ + +/* Miguel de Icaza is not sure specifically who from the Gnumeric + * community is the copyright owner of the code below, so, on his + * recommendation, here is the full list of Gnumeric authors. + * + * Miguel de Icaza, creator. + * Jody Goldberg, maintainer. + * Harald Ashburner, Options pricers + * Sean Atkinson, functions and X-Base importing. + * Michel Berkelaar, Simplex algorithm for Solver (LP Solve). + * Jean Brefort, Core charting engine. + * Grandma Chema Celorio, Tester and sheet copy. + * Frank Chiulli, OLE support. + * Kenneth Christiansen, i18n, misc stuff. + * Zbigniew Chyla, plugin system, i18n. + * J.H.M. Dassen (Ray), debian packaging. + * Jeroen Dirks, Simplex algorithm for Solver (LP Solve). + * Tom Dyas, plugin support. + * Gergo Erdi, Gnumeric hacker. + * John Gotts, rpm packaging. + * Andreas J. Guelzow, Gnumeric hacker. + * Jon K. Hellan, Gnumeric hacker. + * Ross Ihaka, special functions. + * Jukka-Pekka Iivonen, numerous functions and tools. + * Jakub Jelinek, Gnumeric hacker. + * Chris Lahey, number format engine. + * Adrian Likins, documentation, debugging. + * Takashi Matsuda, original text plugin. + * Michael Meeks, Excel and OLE2 importing. + * Lutz Muller, SheetObject improvements. + * Emmanuel Pacaud, Many plot types for charting engine. + * Federico M. Quintero, canvas support. + * Mark Probst, Guile support. + * Rasca, HTML, troff, LaTeX exporters. + * Vincent Renardias, original CSV support, French localization. + * Ariel Rios, Guile support. + * Uwe Steinmann, Paradox Importer. + * Arturo Tena, OLE support. + * Almer S. Tigelaar, Gnumeric hacker. + * Bruno Unna, Excel bits. + * Daniel Veillard, XML support. + * Vladimir Vuksan, financial functions. + * Morten Welinder, Gnumeric hacker and leak plugging demi-god. + */ + +#ifndef GNC_CSV_GNUMERIC_POPUP +#define GNC_CSV_GNUMERIC_POPUP + +#include + +typedef struct { + char const *name; + char const *pixmap; + int display_filter; + int sensitive_filter; + + int index; +} GnumericPopupMenuElement; + +typedef gboolean (*GnumericPopupMenuHandler) (GnumericPopupMenuElement const *e, + gpointer user_data); + +/* Use this on menus that are popped up */ +void gnumeric_popup_menu (GtkMenu *menu, GdkEventButton *event); + +void gnumeric_create_popup_menu (GnumericPopupMenuElement const *elements, + GnumericPopupMenuHandler handler, + gpointer user_data, + int display_filter, + int sensitive_filter, + GdkEventButton *event); + + +#endif diff --git a/src/import-export/csv/gnc-csv-import.c b/src/import-export/csv/gnc-csv-import.c new file mode 100644 index 0000000000..0ed5bb5807 --- /dev/null +++ b/src/import-export/csv/gnc-csv-import.c @@ -0,0 +1,1173 @@ +/*******************************************************************\ + * 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 2 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, contact: * + * * + * Free Software Foundation Voice: +1-617-542-5942 * + * 51 Franklin Street, Fifth Floor Fax: +1-617-542-2652 * + * Boston, MA 02110-1301, USA gnu@gnu.org * +\********************************************************************/ +/** @file gnc-csv-import.c + @brief CSV Import GUI code + @author Copyright (c) 2007 Benny Sperisen +*/ +#include "config.h" + +#include +#include +#include +#include + +#include "import-account-matcher.h" +#include "import-main-matcher.h" + +#include "gnc-file.h" +#include "gnc-book.h" +#include "gnc-ui-util.h" +#include "gnc-glib-utils.h" +#include "gnc-gui-query.h" +#include "dialog-utils.h" + +#include "gnc-csv-import.h" +#include "gnc-csv-model.h" +#include "gnc-csv-gnumeric-popup.h" + +#define GCONF_SECTION "dialogs/import/csv" + +static QofLogModule log_module = GNC_MOD_IMPORT; + +/** Enumeration for separator checkbutton types. These are the + * different types of checkbuttons that the user can click to + * configure separators in a delimited file. */ +enum SEP_BUTTON_TYPES {SEP_SPACE, SEP_TAB, SEP_COMMA, SEP_COLON, SEP_SEMICOLON, SEP_HYPHEN, + SEP_NUM_OF_TYPES}; + +/** Data for the preview dialog. This struct contains all of the data + * relevant to the preview dialog that lets the user configure an + * import. */ +typedef struct +{ + GncCsvParseData* parse_data; /**< The actual data we are previewing */ + GtkDialog* dialog; + GOCharmapSel* encselector; /**< The widget for selecting the encoding */ + GtkComboBox* date_format_combo; /**< The widget for selecting the date format */ + GladeXML* xml; /**< The Glade file that contains the dialog. */ + GtkTreeView* treeview; /**< The treeview containing the data */ + GtkTreeView* ctreeview; /**< The treeview containing the column types */ + GtkCheckButton* sep_buttons[SEP_NUM_OF_TYPES]; /**< Checkbuttons for common separators */ + GtkCheckButton* custom_cbutton; /**< The checkbutton for a custom separator */ + GtkEntry* custom_entry; /**< The entry for custom separators */ + gboolean encoding_selected_called; /**< Before encoding_selected is first called, this is FALSE. + * (See description of encoding_selected.) */ + gboolean not_empty; /**< FALSE initially, true after the first type gnc_csv_preview_update is called. */ + gboolean previewing_errors; /**< TRUE if the dialog is displaying + * error lines, instead of all the file + * data. */ + int code_encoding_calls; /**< Normally this is 0. If the computer + * changes encselector, this is set to + * 2. encoding_selected is called twice, + * each time decrementing this by 1. */ + gboolean approved; /**< This is FALSE until the user clicks "OK". */ + GtkWidget** treeview_buttons; /**< This array contains the header buttons in treeview */ + int longest_line; /**< The length of the longest row */ + int fixed_context_col; /**< The number of the column whose the user has clicked */ + int fixed_context_dx; /**< The horizontal coordinate of the pixel in the header of the column + * the user has clicked */ +} GncCsvPreview; + +static void gnc_csv_preview_update(GncCsvPreview* preview); + +/** Event handler for separator changes. This function is called + * whenever one of the widgets for configuring the separators (the + * separator checkbuttons or the custom separator entry) is + * changed. + * @param widget The widget that was changed + * @param preview The data that is being configured + */ +static void sep_button_clicked(GtkWidget* widget, GncCsvPreview* preview) +{ + int i; + char* stock_separator_characters[] = {" ", "\t", ",", ":", ";", "-"}; + GSList* checked_separators = NULL; + GError* error; + + /* Add the corresponding characters to checked_separators for each + * button that is checked. */ + for(i = 0; i < SEP_NUM_OF_TYPES; i++) + { + if(gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(preview->sep_buttons[i]))) + checked_separators = g_slist_append(checked_separators, stock_separator_characters[i]); + } + + /* Add the custom separator if the user checked its button. */ + if(gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(preview->custom_cbutton))) + { + char* custom_sep = (char*)gtk_entry_get_text(preview->custom_entry); + if(custom_sep[0] != '\0') /* Don't add a blank separator (bad things will happen!). */ + checked_separators = g_slist_append(checked_separators, custom_sep); + } + + /* Set the parse options using the checked_separators list. */ + stf_parse_options_csv_set_separators(preview->parse_data->options, NULL, checked_separators); + g_slist_free(checked_separators); + + /* Parse the data using the new options. We don't want to reguess + * the column types because we want to leave the user's + * configurations in tact. */ + if(gnc_csv_parse(preview->parse_data, FALSE, &error)) + { + /* Warn the user there was a problem and try to undo what caused + * the error. (This will cause a reparsing and ideally a usable + * configuration.) */ + gnc_error_dialog(NULL, "Error in parsing"); + /* If the user changed the custom separator, erase that custom separator. */ + if(widget == GTK_WIDGET(preview->custom_entry)) + { + gtk_entry_set_text(GTK_ENTRY(widget), ""); + } + /* If the user checked a checkbutton, toggle that checkbutton back. */ + else + { + gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(widget), + !gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(widget))); + } + return; + } + + /* If we parsed successfully, redisplay the data. */ + gnc_csv_preview_update(preview); +} + +/** Event handler for clicking one of the format type radio + * buttons. This occurs if the format (Fixed-Width or CSV) is changed. + * @param csv_button The "Separated" radio button + * @param preview The display of the data being imported + */ +static void separated_or_fixed_selected(GtkToggleButton* csv_button, GncCsvPreview* preview) +{ + GError* error = NULL; + /* Set the parsing type correctly. */ + if(gtk_toggle_button_get_active(csv_button)) /* If we're in CSV mode ... */ + { + stf_parse_options_set_type(preview->parse_data->options, PARSE_TYPE_CSV); + } + else /* If we're in fixed-width mode ... */ + { + stf_parse_options_set_type(preview->parse_data->options, PARSE_TYPE_FIXED); + } + + /* Reparse the data. */ + if(gnc_csv_parse(preview->parse_data, FALSE, &error)) + { + /* Show an error dialog explaining the problem. */ + gnc_error_dialog(NULL, "%s", error->message); + return; + } + + /* Show the new data. */ + gnc_csv_preview_update(preview); +} + +/** Event handler for a new encoding. This is called when the user + * selects a new encoding; the data is reparsed and shown to the + * user. + * @param selector The widget the user uses to select a new encoding + * @param encoding The encoding that the user selected + * @param preview The display of the data being imported + */ +static void encoding_selected(GOCharmapSel* selector, const char* encoding, + GncCsvPreview* preview) +{ + /* This gets called twice everytime a new encoding is selected. The + * second call actually passes the correct data; thus, we only do + * something the second time this is called. */ + + /* Prevent code-caused calls of this function from having an impact. */ + if(preview->code_encoding_calls > 0) + { + preview->code_encoding_calls--; + return; + } + + /* If this is the second time the function is called ... */ + if(preview->encoding_selected_called) + { + const char* previous_encoding = preview->parse_data->encoding; + GError* error = NULL; + /* Try converting the new encoding and reparsing. */ + if(gnc_csv_convert_encoding(preview->parse_data, encoding, &error) || + gnc_csv_parse(preview->parse_data, FALSE, &error)) + { + /* If it fails, change back to the old encoding. */ + gnc_error_dialog(NULL, _("Invalid encoding selected")); + preview->encoding_selected_called = FALSE; + go_charmap_sel_set_encoding(selector, previous_encoding); + return; + } + + gnc_csv_preview_update(preview); + preview->encoding_selected_called = FALSE; + } + else /* If this is the first call of the function ... */ + { + preview->encoding_selected_called = TRUE; /* ... set the flag and wait for the next call. */ + } +} + +/** Event handler for selecting a new date format. + * @param format_selector The combo box for selecting date formats + * @param preview The display of the data being imported + */ +static void date_format_selected(GtkComboBox* format_selector, GncCsvPreview* preview) +{ + preview->parse_data->date_format = gtk_combo_box_get_active(format_selector); +} + +/** Event handler for the "OK" button. When "OK" is clicked, this + * function updates the parse data with the user's column type + * configuration and closes the preview dialog. + * @param widget The "OK" button + * @param preview The display of the data being imported + */ +static void ok_button_clicked(GtkWidget* widget, GncCsvPreview* preview) +{ + /* Shorten the column_types identifier. */ + GArray* column_types = preview->parse_data->column_types; + int i, ncols = column_types->len; /* ncols is the number of columns in the data. */ + /* store contains the actual strings appearing in the column types treeview. */ + GtkTreeModel* store = gtk_tree_view_get_model(preview->ctreeview); + GtkTreeIter iter; + /* Get an iterator for the first (and only) row. */ + gtk_tree_model_get_iter_first(store, &iter); + + /* Go through each of the columns. */ + for(i = 0; i < ncols; i++) + { + int type; /* The column type contained in this column. */ + gchar* contents; /* The column type string in this column. */ + /* Get the type string first. (store is arranged so that every two + * columns is a pair of the model used for the combobox and the + * string that appears, so that store looks like: + * model 0, string 0, model 1, string 1, ..., model ncols, string ncols. */ + gtk_tree_model_get(store, &iter, 2*i+1, &contents, -1); + + /* Go through each column type until ... */ + for(type = 0; type < GNC_CSV_NUM_COL_TYPES; type++) + { + /* ... we find one that matches with what's in the column. */ + if(!strcmp(contents, _(gnc_csv_column_type_strs[type]))) + { + /* Set the column_types array appropriately and quit. */ + column_types->data[i] = type; + break; + } + } + } + + /* Close the dialog. */ + gtk_widget_hide((GtkWidget*)(preview->dialog)); + preview->approved = TRUE; /* The user has wants to do the import. */ +} + +/** Event handler for the "Cancel" button. When the user clicks + * "Cancel", the dialog is simply closed. + * @param widget The "Cancel" button + * @param preview The display of the data being imported + */ +static void cancel_button_clicked(GtkWidget* widget, GncCsvPreview* preview) +{ + gtk_widget_hide((GtkWidget*)(preview->dialog)); +} + +/** Event handler for the data treeview being resized. When the data + * treeview is resized, the column types treeview's columns are also resized to + * match. + * @param widget The data treeview + * @param allocation The size of the data treeview + * @param preview The display of the data being imported + */ +static void treeview_resized(GtkWidget* widget, GtkAllocation* allocation, GncCsvPreview* preview) +{ + /* ncols is the number of columns in the data. */ + int i, ncols = preview->parse_data->column_types->len; + + /* Go through each column except for the last. (We don't want to set + * the width of the last column because the user won't be able to + * shrink the dialog back if it's expanded.) */ + for(i = 0; i < ncols - 1; i++) + { + gint col_width; /* The width of the column in preview->treeview. */ + GtkTreeViewColumn* ccol; /* The corresponding column in preview->ctreeview. */ + + /* Get the width. */ + col_width = gtk_tree_view_column_get_width(gtk_tree_view_get_column(preview->treeview, i)); + + /* Set ccol's width the same. */ + ccol = gtk_tree_view_get_column(preview->ctreeview, i); + gtk_tree_view_column_set_min_width(ccol, col_width); + gtk_tree_view_column_set_max_width(ccol, col_width); + } +} + +/** Event handler for the user selecting a new column type. When the + * user selects a new column type, that column's text must be changed + * to that selection, and any other columns containing that selection + * must be changed to "None" because we don't allow duplicates. + * @param renderer The renderer of the column the user changed + * @param path There is only 1 row in preview->ctreeview, so this is always 0. + * @param new_text The text the user selected + * @param preview The display of the data being imported + */ +static void column_type_edited(GtkCellRenderer* renderer, gchar* path, + gchar* new_text, GncCsvPreview* preview) +{ + /* ncols is the number of columns in the data. */ + int i, ncols = preview->parse_data->column_types->len; + /* store has the actual strings that appear in preview->ctreeview. */ + GtkTreeModel* store = gtk_tree_view_get_model(preview->ctreeview); + GtkTreeIter iter; + /* Get an iterator for the first (and only) row. */ + gtk_tree_model_get_iter_first(store, &iter); + + /* Go through each column. */ + for(i = 0; i < ncols; i++) + { + /* We need all this stuff so that we can find out whether or not + * this was the column that was edited. */ + GtkCellRenderer* col_renderer; /* The renderer for this column. */ + /* The column in the treeview we are looking at */ + GtkTreeViewColumn* col = gtk_tree_view_get_column(preview->ctreeview, i); + /* The list of renderers for col */ + GList* rend_list = gtk_tree_view_column_get_cell_renderers(col); + /* rend_list has only one entry, which we put in col_renderer. */ + col_renderer = rend_list->data; + g_list_free(rend_list); + + /* If this is not the column that was edited ... */ + if(col_renderer != renderer) + { + /* The string that appears in the column */ + gchar* contents; + /* Get the type string. (store is arranged so that every two + * columns is a pair of the model used for the combobox and the + * string that appears, so that store looks like: + * model 0, string 0, model 1, string 1, ..., model ncols, string ncols. */ + gtk_tree_model_get(store, &iter, 2*i+1, &contents, -1); + /* If this column has the same string that the user selected ... */ + if(!strcmp(contents, new_text)) + { + /* ... set this column to the "None" type. (We can't allow duplicate types.) */ + gtk_list_store_set(GTK_LIST_STORE(store), &iter, 2*i+1, + _(gnc_csv_column_type_strs[GNC_CSV_NONE]), -1); + } + } + else /* If this is the column that was edited ... */ + { + /* Set the text for this column to what the user selected. (See + * comment above "Get the type string. ..." for why we set + * column 2*i+1 in store.) */ + gtk_list_store_set(GTK_LIST_STORE(store), &iter, 2*i+1, new_text, -1); + } + } +} + +/** Constructor for GncCsvPreview. + * @return A new GncCsvPreview* ready for use. + */ +static GncCsvPreview* gnc_csv_preview_new() +{ + int i; + GncCsvPreview* preview = g_new(GncCsvPreview, 1); + GtkWidget *ok_button, *cancel_button, *csv_button; + GtkContainer* date_format_container; + /* The names in the glade file for the sep buttons. */ + char* sep_button_names[] = {"space_cbutton", + "tab_cbutton", + "comma_cbutton", + "colon_cbutton", + "semicolon_cbutton", + "hyphen_cbutton"}; + /* The table containing preview->encselector and the separator configuration widgets */ + GtkTable* enctable; + PangoContext* context; /* Used to set a monotype font on preview->treeview */ + + preview->encselector = GO_CHARMAP_SEL(go_charmap_sel_new(GO_CHARMAP_SEL_TO_UTF8)); + /* Connect the selector to the encoding_selected event handler. */ + g_signal_connect(G_OBJECT(preview->encselector), "charmap_changed", + G_CALLBACK(encoding_selected), (gpointer)preview); + + /* Load the Glade file. */ + preview->xml = gnc_glade_xml_new("gnc-csv-preview-dialog.glade", "dialog"); + /* Load the dialog. */ + preview->dialog = GTK_DIALOG(glade_xml_get_widget(preview->xml, "dialog")); + + /* Load the separator buttons from the glade file into the + * preview->sep_buttons array. */ + for(i = 0; i < SEP_NUM_OF_TYPES; i++) + { + preview->sep_buttons[i] + = (GtkCheckButton*)glade_xml_get_widget(preview->xml, sep_button_names[i]); + /* Connect them to the sep_button_clicked event handler. */ + g_signal_connect(G_OBJECT(preview->sep_buttons[i]), "toggled", + G_CALLBACK(sep_button_clicked), (gpointer)preview); + } + + /* Load and connect the custom separator checkbutton in the same way + * as the other separator buttons. */ + preview->custom_cbutton + = (GtkCheckButton*)glade_xml_get_widget(preview->xml, "custom_cbutton"); + g_signal_connect(G_OBJECT(preview->custom_cbutton), "clicked", + G_CALLBACK(sep_button_clicked), (gpointer)preview); + + /* Load the entry for the custom separator entry. Connect it to the + * sep_button_clicked event handler as well. */ + preview->custom_entry = (GtkEntry*)glade_xml_get_widget(preview->xml, "custom_entry"); + g_signal_connect(G_OBJECT(preview->custom_entry), "changed", + G_CALLBACK(sep_button_clicked), (gpointer)preview); + + /* Get the table from the Glade file. */ + enctable = GTK_TABLE(glade_xml_get_widget(preview->xml, "enctable")); + /* Put the selector in at the top. */ + gtk_table_attach_defaults(enctable, GTK_WIDGET(preview->encselector), 1, 2, 0, 1); + /* Show the table in all its glory. */ + gtk_widget_show_all(GTK_WIDGET(enctable)); + + /* Add in the date format combo box and hook it up to an event handler. */ + preview->date_format_combo = GTK_COMBO_BOX(gtk_combo_box_new_text()); + for(i = 0; i < num_date_formats; i++) + { + gtk_combo_box_append_text(preview->date_format_combo, _(date_format_user[i])); + } + gtk_combo_box_set_active(preview->date_format_combo, 0); + g_signal_connect(G_OBJECT(preview->date_format_combo), "changed", + G_CALLBACK(date_format_selected), (gpointer)preview); + + /* Add it to the dialog. */ + date_format_container = GTK_CONTAINER(glade_xml_get_widget(preview->xml, + "date_format_container")); + gtk_container_add(date_format_container, GTK_WIDGET(preview->date_format_combo)); + gtk_widget_show_all(GTK_WIDGET(date_format_container)); + + /* Connect the "OK" and "Cancel" buttons to their event handlers. */ + ok_button = glade_xml_get_widget(preview->xml, "ok_button"); + g_signal_connect(G_OBJECT(ok_button), "clicked", + G_CALLBACK(ok_button_clicked), (gpointer)preview); + + cancel_button = glade_xml_get_widget(preview->xml, "cancel_button"); + g_signal_connect(G_OBJECT(cancel_button), "clicked", + G_CALLBACK(cancel_button_clicked), (gpointer)preview); + + /* Connect the CSV/Fixed-Width radio button event handler. */ + csv_button = glade_xml_get_widget(preview->xml, "csv_button"); + g_signal_connect(csv_button, "toggled", + G_CALLBACK(separated_or_fixed_selected), (gpointer)preview); + + /* Load the data treeview and connect it to its resizing event handler. */ + preview->treeview = (GtkTreeView*)(glade_xml_get_widget(preview->xml, "treeview")); + g_signal_connect(G_OBJECT(preview->treeview), "size-allocate", + G_CALLBACK(treeview_resized), (gpointer)preview); + context = gtk_widget_create_pango_context(GTK_WIDGET(preview->treeview)); + + /* Load the column type treeview. */ + preview->ctreeview = (GtkTreeView*)(glade_xml_get_widget(preview->xml, "ctreeview")); + + /* This is TRUE only after encoding_selected is called, so we must + * set it initially to FALSE. */ + preview->encoding_selected_called = FALSE; + + /* It is empty at first. */ + preview->not_empty = FALSE; + + return preview; +} + +/** Destructor for GncCsvPreview. This does not free + * preview->parse_data, which must be freed separately. + * @param preview The preview whose memory is freed. + */ +static void gnc_csv_preview_free(GncCsvPreview* preview) +{ + g_object_unref(preview->xml); + g_free(preview); +} + +/** Returns the cell renderer from a column in the preview's treeview. + * @param preview The display of the data being imported + * @param col The number of the column whose cell renderer is being retrieved + * @return The cell renderer of column number col + */ +static GtkCellRenderer* gnc_csv_preview_get_cell_renderer(GncCsvPreview* preview, int col) +{ + GList* renderers = gtk_tree_view_column_get_cell_renderers(gtk_tree_view_get_column(preview->treeview, col)); + GtkCellRenderer* cell = GTK_CELL_RENDERER(renderers->data); + g_list_free(renderers); + return cell; +} + +/* The following is code copied from Gnumeric 1.7.8 licensed under the + * GNU General Public License version 2. It is from the file + * gnumeric/src/dialogs/dialog-stf-fixed-page.c, and it has been + * modified slightly to work within GnuCash. */ + +/* ---- Beginning of Gnumeric Code ---- */ + +/* + * Copyright 2001 Almer S. Tigelaar + * Copyright 2003 Morten Welinder + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + */ + +enum { + CONTEXT_STF_IMPORT_MERGE_LEFT = 1, + CONTEXT_STF_IMPORT_MERGE_RIGHT = 2, + CONTEXT_STF_IMPORT_SPLIT = 3, + CONTEXT_STF_IMPORT_WIDEN = 4, + CONTEXT_STF_IMPORT_NARROW = 5 +}; + +static GnumericPopupMenuElement const popup_elements[] = { + { N_("Merge with column on _left"), GTK_STOCK_REMOVE, + 0, 1 << CONTEXT_STF_IMPORT_MERGE_LEFT, CONTEXT_STF_IMPORT_MERGE_LEFT }, + { N_("Merge with column on _right"), GTK_STOCK_REMOVE, + 0, 1 << CONTEXT_STF_IMPORT_MERGE_RIGHT, CONTEXT_STF_IMPORT_MERGE_RIGHT }, + { "", NULL, 0, 0, 0 }, + { N_("_Split this column"), NULL, + 0, 1 << CONTEXT_STF_IMPORT_SPLIT, CONTEXT_STF_IMPORT_SPLIT }, + { "", NULL, 0, 0, 0 }, + { N_("_Widen this column"), GTK_STOCK_GO_FORWARD, + 0, 1 << CONTEXT_STF_IMPORT_WIDEN, CONTEXT_STF_IMPORT_WIDEN }, + { N_("_Narrow this column"), GTK_STOCK_GO_BACK, + 0, 1 << CONTEXT_STF_IMPORT_NARROW, CONTEXT_STF_IMPORT_NARROW }, + { NULL, NULL, 0, 0, 0 }, +}; + +static gboolean +make_new_column (GncCsvPreview *preview, int col, int dx, gboolean test_only) +{ + PangoLayout *layout; + PangoFontDescription *font_desc; + int charindex, width; + GtkCellRenderer *cell = gnc_csv_preview_get_cell_renderer(preview, col); + int colstart, colend; + GError* error = NULL; + + colstart = (col == 0) + ? 0 + : stf_parse_options_fixed_splitpositions_nth (preview->parse_data->options, col - 1); + colend = stf_parse_options_fixed_splitpositions_nth (preview->parse_data->options, col); + + g_object_get (G_OBJECT (cell), "font_desc", &font_desc, NULL); + layout = gtk_widget_create_pango_layout (GTK_WIDGET (preview->treeview), "x"); + pango_layout_set_font_description (layout, font_desc); + pango_layout_get_pixel_size (layout, &width, NULL); + if (width < 1) width = 1; + charindex = colstart + (dx + width / 2) / width; + g_object_unref (layout); + pango_font_description_free (font_desc); + + if (charindex <= colstart || (colend != -1 && charindex >= colend)) + return FALSE; + + if (!test_only) { + stf_parse_options_fixed_splitpositions_add (preview->parse_data->options, charindex); + if(gnc_csv_parse(preview->parse_data, FALSE, &error)) + { + gnc_error_dialog(NULL, "%s", error->message); + return FALSE; + } + gnc_csv_preview_update (preview); + } + + return TRUE; +} + + +static gboolean +widen_column (GncCsvPreview *preview, int col, gboolean test_only) +{ + int colcount = stf_parse_options_fixed_splitpositions_count (preview->parse_data->options); + int nextstart, nextnextstart; + GError* error = NULL; + + if (col >= colcount - 1) + return FALSE; + + nextstart = stf_parse_options_fixed_splitpositions_nth (preview->parse_data->options, col); + + nextnextstart = (col == colcount - 2) + ? preview->longest_line + : stf_parse_options_fixed_splitpositions_nth (preview->parse_data->options, col + 1); + + if (nextstart + 1 >= nextnextstart) + return FALSE; + + if (!test_only) { + stf_parse_options_fixed_splitpositions_remove (preview->parse_data->options, nextstart); + stf_parse_options_fixed_splitpositions_add (preview->parse_data->options, nextstart + 1); + if(gnc_csv_parse(preview->parse_data, FALSE, &error)) + { + gnc_error_dialog(NULL, "%s", error->message); + return FALSE; + } + gnc_csv_preview_update (preview); + } + return TRUE; +} + +static gboolean +narrow_column (GncCsvPreview *preview, int col, gboolean test_only) +{ + int colcount = stf_parse_options_fixed_splitpositions_count (preview->parse_data->options); + int thisstart, nextstart; + GError* error = NULL; + + if (col >= colcount - 1) + return FALSE; + + thisstart = (col == 0) + ? 0 + : stf_parse_options_fixed_splitpositions_nth (preview->parse_data->options, col - 1); + nextstart = stf_parse_options_fixed_splitpositions_nth (preview->parse_data->options, col); + + if (nextstart - 1 <= thisstart) + return FALSE; + + if (!test_only) { + stf_parse_options_fixed_splitpositions_remove (preview->parse_data->options, nextstart); + stf_parse_options_fixed_splitpositions_add (preview->parse_data->options, nextstart - 1); + if(gnc_csv_parse(preview->parse_data, FALSE, &error)) + { + gnc_error_dialog(NULL, "%s", error->message); + return FALSE; + } + gnc_csv_preview_update (preview); + } + return TRUE; +} + +static gboolean +delete_column (GncCsvPreview *preview, int col, gboolean test_only) +{ + GError* error = NULL; + int colcount = stf_parse_options_fixed_splitpositions_count (preview->parse_data->options); + if (col < 0 || col >= colcount - 1) + return FALSE; + + if (!test_only) { + int nextstart = stf_parse_options_fixed_splitpositions_nth (preview->parse_data->options, col); + stf_parse_options_fixed_splitpositions_remove (preview->parse_data->options, nextstart); + if(gnc_csv_parse(preview->parse_data, FALSE, &error)) + { + gnc_error_dialog(NULL, "%s", error->message); + return FALSE; + } + gnc_csv_preview_update (preview); + } + return TRUE; +} + +static void +select_column (GncCsvPreview *preview, int col) +{ + GError* error = NULL; + int colcount = stf_parse_options_fixed_splitpositions_count (preview->parse_data->options); + GtkTreeViewColumn *column; + + if (col < 0 || col >= colcount) + return; + + column = gtk_tree_view_get_column (preview->treeview, col); + gtk_widget_grab_focus (column->button); +} + +static gboolean +fixed_context_menu_handler (GnumericPopupMenuElement const *element, + gpointer user_data) +{ + GncCsvPreview *preview = user_data; + int col = preview->fixed_context_col; + + switch (element->index) { + case CONTEXT_STF_IMPORT_MERGE_LEFT: + delete_column (preview, col - 1, FALSE); + break; + case CONTEXT_STF_IMPORT_MERGE_RIGHT: + delete_column (preview, col, FALSE); + break; + case CONTEXT_STF_IMPORT_SPLIT: + make_new_column (preview, col, preview->fixed_context_dx, FALSE); + break; + case CONTEXT_STF_IMPORT_WIDEN: + widen_column (preview, col, FALSE); + break; + case CONTEXT_STF_IMPORT_NARROW: + narrow_column (preview, col, FALSE); + break; + default: + ; /* Nothing */ + } + return TRUE; +} + +static void +fixed_context_menu (GncCsvPreview *preview, GdkEventButton *event, + int col, int dx) +{ + int sensitivity_filter = 0; + + preview->fixed_context_col = col; + preview->fixed_context_dx = dx; + + if (!delete_column (preview, col - 1, TRUE)) + sensitivity_filter |= (1 << CONTEXT_STF_IMPORT_MERGE_LEFT); + if (!delete_column (preview, col, TRUE)) + sensitivity_filter |= (1 << CONTEXT_STF_IMPORT_MERGE_RIGHT); + if (!make_new_column (preview, col, dx, TRUE)) + sensitivity_filter |= (1 << CONTEXT_STF_IMPORT_SPLIT); + if (!widen_column (preview, col, TRUE)) + sensitivity_filter |= (1 << CONTEXT_STF_IMPORT_WIDEN); + if (!narrow_column (preview, col, TRUE)) + sensitivity_filter |= (1 << CONTEXT_STF_IMPORT_NARROW); + + select_column (preview, col); + gnumeric_create_popup_menu (popup_elements, &fixed_context_menu_handler, + preview, 0, + sensitivity_filter, event); +} + +/* ---- End of Gnumeric Code ---- */ + +/** Event handler for clicking on column headers. This function is + * called whenever the user clicks on column headers in + * preview->treeview to modify columns when in fixed-width mode. + * @param button The button at the top of a column of the treeview + * @param event The event that happened (where the user clicked) + * @param preview The data being configured + */ +static void header_button_press_handler(GtkWidget* button, GdkEventButton* event, + GncCsvPreview* preview) +{ + /* col is the number of the column that was clicked, and offset is + to correct for the indentation of button. */ + int i, col = 0, offset = GTK_BIN(button)->child->allocation.x - button->allocation.x, + ncols = preview->parse_data->column_types->len; + /* Find the column that was clicked. */ + for(i = 0; i < ncols; i++) + { + if(preview->treeview_buttons[i] == button) + { + col = i; + break; + } + } + + /* Don't let the user affect the last column if it has error messages. */ + if(preview->parse_data->orig_max_row < ncols && ncols - col == 1) + { + return; + } + + /* Double clicks can split columns. */ + if(event->type == GDK_2BUTTON_PRESS && event->button == 1) + { + make_new_column(preview, col, (int)event->x - offset, FALSE); + } + /* Right clicking brings up a context menu. */ + else if(event->type == GDK_BUTTON_PRESS && event->button == 3) + { + fixed_context_menu(preview, event, col, (int)event->x - offset); + } +} + +/* Loads the preview's data into its data treeview. notEmpty is TRUE + * when the data treeview already contains data, FALSE otherwise + * (e.g. the first time this function is called on a preview). + * @param preview The data being previewed + * @param notEmpty Whether this function has been called before or not + */ +static void gnc_csv_preview_update(GncCsvPreview* preview) +{ + /* store has the data from the file being imported. cstores is an + * array of stores that hold the combo box entries for each + * column. ctstore contains both pointers to models in cstore and + * the actual text that appears in preview->ctreeview. */ + GtkListStore *store, **cstores, *ctstore; + GtkTreeIter iter; + /* ncols is the number of columns in the file data. */ + int i, j, ncols = preview->parse_data->column_types->len, + max_str_len = preview->parse_data->file_str.end - preview->parse_data->file_str.begin; + + /* store contains only strings. */ + GType* types = g_new(GType, 2 * ncols); + for(i = 0; i < ncols; i++) + types[i] = G_TYPE_STRING; + store = gtk_list_store_newv(ncols, types); + + /* ctstore is arranged as follows: + * model 0, text 0, model 1, text 1, ..., model ncols, text ncols. */ + for(i = 0; i < 2*ncols; i += 2) + { + types[i] = GTK_TYPE_TREE_MODEL; + types[i+1] = G_TYPE_STRING; + } + ctstore = gtk_list_store_newv(2*ncols, types); + + g_free(types); + + /* Each element in cstores is a single column model. */ + cstores = g_new(GtkListStore*, ncols); + for(i = 0; i < ncols; i++) + { + cstores[i] = gtk_list_store_new(1, G_TYPE_STRING); + /* Add all of the possible entries to the combo box. */ + for(j = 0; j < GNC_CSV_NUM_COL_TYPES; j++) + { + gtk_list_store_append(cstores[i], &iter); + gtk_list_store_set(cstores[i], &iter, 0, _(gnc_csv_column_type_strs[j]), -1); + } + } + + if(preview->not_empty) + { + GList *children, *children_begin; + GList *tv_columns, *tv_columns_begin, *ctv_columns, *ctv_columns_begin; + tv_columns = tv_columns_begin = gtk_tree_view_get_columns(preview->treeview); + ctv_columns = ctv_columns_begin = gtk_tree_view_get_columns(preview->ctreeview); + /* Clear out exisiting columns in preview->treeview. */ + while(tv_columns != NULL) + { + gtk_tree_view_remove_column(preview->treeview, GTK_TREE_VIEW_COLUMN(tv_columns->data)); + tv_columns = g_list_next(tv_columns); + } + /* Do the same in preview->ctreeview. */ + while(ctv_columns != NULL) + { + gtk_tree_view_remove_column(preview->ctreeview, GTK_TREE_VIEW_COLUMN(ctv_columns->data)); + ctv_columns = g_list_next(ctv_columns); + } + g_list_free(tv_columns_begin); + g_list_free(ctv_columns_begin); + g_free(preview->treeview_buttons); + } + + /* Fill the data treeview with data from the file. */ + /* Also, update the longest line value within the following loop (whichever is executed). */ + preview->longest_line = 0; + if(preview->previewing_errors) /* If we are showing only errors ... */ + { + /* ... only pick rows that are in preview->error_lines. */ + GList* error_lines = preview->parse_data->error_lines; + while(error_lines != NULL) + { + int this_line_length = 0; + i = GPOINTER_TO_INT(error_lines->data); + gtk_list_store_append(store, &iter); + for(j = 0; j < ((GPtrArray*)(preview->parse_data->orig_lines->pdata[i]))->len; j++) + { + /* Add this cell's length to the row's length and set the value of the list store. */ + gchar* cell_string = (gchar*)((GPtrArray*)(preview->parse_data->orig_lines->pdata[i]))->pdata[j]; + this_line_length += g_utf8_strlen(cell_string, max_str_len); + gtk_list_store_set(store, &iter, j, cell_string, -1); + } + + if(this_line_length > preview->longest_line) + preview->longest_line = this_line_length; + + error_lines = g_list_next(error_lines); + } + } + else /* Otherwise, put in all of the data. */ + { + for(i = 0; i < preview->parse_data->orig_lines->len; i++) + { + int this_line_length = 0; + gtk_list_store_append(store, &iter); + for(j = 0; j < ((GPtrArray*)(preview->parse_data->orig_lines->pdata[i]))->len; j++) + { + /* Add this cell's length to the row's length and set the value of the list store. */ + gchar* cell_string = (gchar*)((GPtrArray*)(preview->parse_data->orig_lines->pdata[i]))->pdata[j]; + this_line_length += g_utf8_strlen(cell_string, max_str_len); + gtk_list_store_set(store, &iter, j, cell_string, -1); + } + + if(this_line_length > preview->longest_line) + preview->longest_line = this_line_length; + } + } + + /* Set all the column types to what's in the parse data. */ + gtk_list_store_append(ctstore, &iter); + for(i = 0; i < ncols; i++) + { + gtk_list_store_set(ctstore, &iter, 2*i, cstores[i], 2*i+1, + _(gnc_csv_column_type_strs[(int)(preview->parse_data->column_types->data[i])]), + -1); + } + + preview->treeview_buttons = g_new(GtkWidget*, ncols); + /* Insert columns into the data and column type treeviews. */ + for(i = 0; i < ncols; i++) + { + GtkTreeViewColumn* col; /* The column we add to preview->treeview. */ + /* Create renderers for the data treeview (renderer) and the + * column type treeview (crenderer). */ + GtkCellRenderer* renderer = gtk_cell_renderer_text_new(), + *crenderer = gtk_cell_renderer_combo_new(); + /* We want a monospace font for the data in case of fixed-width data. */ + g_object_set(G_OBJECT(renderer), "family", "monospace", NULL); + /* We are using cstores for the combo box entries, and we don't + * want the user to be able to manually enter their own column + * types. */ + g_object_set(G_OBJECT(crenderer), "model", cstores[i], "text-column", 0, + "editable", TRUE, "has-entry", FALSE, NULL); + g_signal_connect(G_OBJECT(crenderer), "edited", + G_CALLBACK(column_type_edited), (gpointer)preview); + + /* Add a single column for the treeview. */ + col = gtk_tree_view_column_new_with_attributes("", renderer, "text", i, NULL); + gtk_tree_view_insert_column(preview->treeview, col, -1); + /* Use the alternating model and text entries from ctstore in + * preview->ctreeview. */ + gtk_tree_view_insert_column_with_attributes(preview->ctreeview, + -1, "", crenderer, "model", 2*i, + "text", 2*i+1, NULL); + + /* We need to allow clicking on the column headers for fixed-width + * column splitting and merging. */ + g_object_set(G_OBJECT(col), "clickable", TRUE, NULL); + g_signal_connect(G_OBJECT(col->button), "button_press_event", + G_CALLBACK(header_button_press_handler), (gpointer)preview); + preview->treeview_buttons[i] = col->button; + } + + /* Set the treeviews to use the models. */ + gtk_tree_view_set_model(preview->treeview, GTK_TREE_MODEL(store)); + gtk_tree_view_set_model(preview->ctreeview, GTK_TREE_MODEL(ctstore)); + + /* Free the memory for the stores. */ + g_object_unref(GTK_TREE_MODEL(store)); + g_object_unref(GTK_TREE_MODEL(ctstore)); + for(i = 0; i < ncols; i++) + g_object_unref(GTK_TREE_MODEL(cstores[i])); + + /* Make the things actually appear. */ + gtk_widget_show_all(GTK_WIDGET(preview->treeview)); + gtk_widget_show_all(GTK_WIDGET(preview->ctreeview)); + + /* Set the encoding selector to the right encoding. */ + preview->code_encoding_calls = 2; + go_charmap_sel_set_encoding(preview->encselector, preview->parse_data->encoding); + + /* Set the date format to what's in the combo box (since we don't + * necessarily know if this will always be the same). */ + preview->parse_data->date_format = gtk_combo_box_get_active(preview->date_format_combo); + + /* It's now been filled with some stuff. */ + preview->not_empty = TRUE; +} + +/** A function that lets the user preview a file's data. This function + * is used to let the user preview and configure the data parsed from + * the file. It doesn't return until the user clicks "OK" or "Cancel" + * on the dialog. + * @param preview The GUI for previewing the data + * @param parse_data The data we want to preview + * @return 0 if the user approved the import; 1 if the user didn't. + */ +static int gnc_csv_preview(GncCsvPreview* preview, GncCsvParseData* parse_data) +{ + /* Set the preview's parse_data to the one we're getting passed. */ + preview->parse_data = parse_data; + preview->previewing_errors = FALSE; /* We're looking at all the data. */ + preview->approved = FALSE; /* This is FALSE until the user clicks "OK". */ + + /* Load the data into the treeview. (This is the first time we've + * called gnc_csv_preview_update on this preview, so we use + * FALSE.) */ + gnc_csv_preview_update(preview); + /* Wait until the user clicks "OK" or "Cancel". */ + gtk_dialog_run(GTK_DIALOG(preview->dialog)); + + if(preview->approved) + return 0; + else + return 1; +} + +/** A function that lets the user preview rows with errors. This + * function must only be called after calling gnc_csv_preview. It is + * essentially identical in behavior to gnc_csv_preview except that it + * displays lines with errors instead of all of the data. + * @param preview The GUI for previewing the data (and the data being previewed) + * @return 0 if the user approved of importing the lines; 1 if the user didn't. + */ +/* TODO Let the user manually edit cells' data? */ +static int gnc_csv_preview_errors(GncCsvPreview* preview) +{ + GtkLabel* instructions_label = GTK_LABEL(glade_xml_get_widget(preview->xml, "instructions_label")); + GtkImage* instructions_image = GTK_IMAGE(glade_xml_get_widget(preview->xml, "instructions_image")); + gchar* name; + GtkIconSize size; + GtkTreeViewColumn* last_col; + + gtk_image_get_stock(instructions_image, &name, &size); + gtk_image_set_from_stock(instructions_image, GTK_STOCK_DIALOG_ERROR, size); + gtk_label_set_text(instructions_label, + _("The rows displayed below had errors. You can attempt to correct these errors by changing the configuration.")); + gtk_widget_show(GTK_WIDGET(instructions_image)); + gtk_widget_show(GTK_WIDGET(instructions_label)); + + preview->previewing_errors = TRUE; + preview->approved = FALSE; /* This is FALSE until the user clicks "OK". */ + + /* Wait until the user clicks "OK" or "Cancel". */ + gnc_csv_preview_update(preview); + + /* Set the last column to have the header "Errors" so that the user + * doesn't find the extra column confusing. */ + last_col = gtk_tree_view_get_column(preview->treeview, + preview->parse_data->column_types->len - 1); + gtk_tree_view_column_set_title(last_col, _("Errors")); + + gtk_dialog_run(GTK_DIALOG(preview->dialog)); + + if(preview->approved) + return 0; + else + return 1; +} + +/** Lets the user import a CSV/Fixed-Width file. */ +void gnc_file_csv_import(void) +{ + /* The name of the file the user selected. */ + char* selected_filename; + /* The default directory for the user to select files. */ + char* default_dir= gnc_get_default_directory(GCONF_SECTION); + /* The generic GUI for importing transactions. */ + GNCImportMainMatcher* gnc_csv_importer_gui = NULL; + + /* Let the user select a file. */ + selected_filename = gnc_file_dialog(_("Select an CSV/Fixed-Width file to import"), + NULL, default_dir, GNC_FILE_DIALOG_IMPORT); + g_free(default_dir); /* We don't need default_dir anymore. */ + + /* If the user actually selected a file ... */ + if(selected_filename!=NULL) + { + int i, user_canceled = 0; + Account* account; /* The account the user will select */ + GError* error = NULL; + GList* transactions; /* A list of the transactions we create */ + GncCsvParseData* parse_data; + GncCsvPreview* preview; + + /* Remember the directory of the selected file as the default. */ + default_dir = g_path_get_dirname(selected_filename); + gnc_set_default_directory(GCONF_SECTION, default_dir); + g_free(default_dir); + + /* Load the file into parse_data. */ + parse_data = gnc_csv_new_parse_data(); + if(gnc_csv_load_file(parse_data, selected_filename, &error)) + { + /* If we couldn't load the file ... */ + gnc_error_dialog(NULL, "%s", error->message); + if(error->code == GNC_CSV_FILE_OPEN_ERR) + { + gnc_csv_parse_data_free(parse_data); + g_free(selected_filename); + return; + } + /* If we couldn't guess the encoding, we are content with just + * displaying an error message and move on with a blank + * display. */ + } + /* Parse the data. */ + if(gnc_csv_parse(parse_data, TRUE, &error)) + { + /* If we couldn't parse the data ... */ + gnc_error_dialog(NULL, "%s", error->message); + } + + /* Preview the data. */ + preview = gnc_csv_preview_new(); + if(gnc_csv_preview(preview, parse_data)) + { + /* If the user clicked "Cancel", free everything and quit. */ + gnc_csv_preview_free(preview); + gnc_csv_parse_data_free(parse_data); + g_free(selected_filename); + return; + } + + /* Let the user select an account to put the transactions in. */ + account = gnc_import_select_account(NULL, NULL, 1, NULL, NULL, 0, NULL, NULL); + if(account == NULL) /* Quit if the user canceled. */ + { + gnc_csv_preview_free(preview); + gnc_csv_parse_data_free(parse_data); + g_free(selected_filename); + return; + } + + /* Create transactions from the parsed data. */ + gnc_csv_parse_to_trans(parse_data, account, FALSE); + + /* If there are errors, let the user try and eliminate them by + * previewing them. Repeat until either there are no errors or the + * user gives up. */ + while(!((parse_data->error_lines == NULL) || user_canceled)) + { + user_canceled = gnc_csv_preview_errors(preview); + gnc_csv_parse_to_trans(parse_data, account, TRUE); + } + + /* Create the genereic transaction importer GUI. */ + gnc_csv_importer_gui = gnc_gen_trans_list_new(NULL, NULL, FALSE, 42); + + /* Get the list of the transactions that were created. */ + transactions = parse_data->transactions; + /* Copy all of the transactions to the importer GUI. */ + while(transactions != NULL) + { + GncCsvTransLine* trans_line = transactions->data; + gnc_gen_trans_list_add_trans(gnc_csv_importer_gui, + trans_line->trans); + transactions = g_list_next(transactions); + } + /* Let the user load those transactions into the account, so long + * as there is at least one transaction to be loaded. */ + if(parse_data->transactions != NULL) + gnc_gen_trans_list_run(gnc_csv_importer_gui); + else + gnc_gen_trans_list_delete(gnc_csv_importer_gui); + + /* Free the memory we allocated. */ + gnc_csv_preview_free(preview); + gnc_csv_parse_data_free(parse_data); + g_free(selected_filename); + } +} diff --git a/src/import-export/csv/gnc-csv-import.h b/src/import-export/csv/gnc-csv-import.h new file mode 100644 index 0000000000..67b74f442f --- /dev/null +++ b/src/import-export/csv/gnc-csv-import.h @@ -0,0 +1,33 @@ +/********************************************************************\ + * 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 2 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, contact: * + * * + * Free Software Foundation Voice: +1-617-542-5942 * + * 51 Franklin Street, Fifth Floor Fax: +1-617-542-2652 * + * Boston, MA 02110-1301, USA gnu@gnu.org * +\********************************************************************/ + /** @file + @brief CSV import GUI + * + gnc-csv-import.h + @author Copyright (c) 2007 Benny Sperisen + */ +#ifndef CSV_IMPORT_H +#define CSV_IMPORT_H + +/** The gnc_file_csv_import() will let the user select a + * CSV/Fixed-Width file to open, select an account to import it to, + * and import the transactions into the account. It also allows the + * user to configure how the file is parsed. */ +void gnc_file_csv_import (void); +#endif diff --git a/src/import-export/csv/gnc-csv-model.c b/src/import-export/csv/gnc-csv-model.c new file mode 100644 index 0000000000..0f8feb6f6a --- /dev/null +++ b/src/import-export/csv/gnc-csv-model.c @@ -0,0 +1,1199 @@ +#include "gnc-csv-model.h" + +#include "gnc-book.h" + +#include +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +static QofLogModule log_module = GNC_MOD_IMPORT; + +const int num_date_formats = 5; + +const gchar* date_format_user[] = {N_("y-m-d"), + N_("d-m-y"), + N_("m-d-y"), + N_("d-m"), + N_("m-d")}; + +/* This array contains all of the different strings for different column types. */ +gchar* gnc_csv_column_type_strs[GNC_CSV_NUM_COL_TYPES] = {N_("None"), + N_("Date"), + N_("Description"), + N_("Balance"), + N_("Deposit"), + N_("Withdrawal"), + N_("Num")}; + +/** A set of sensible defaults for parsing CSV files. + * @return StfParseOptions_t* for parsing a file with comma separators + */ +static StfParseOptions_t* default_parse_options(void) +{ + StfParseOptions_t* options = stf_parse_options_new(); + stf_parse_options_set_type(options, PARSE_TYPE_CSV); + stf_parse_options_csv_set_separators(options, ",", NULL); + return options; +} + +/** Parses a string into a date, given a format. The format must + * include the year. This function should only be called by + * parse_date. + * @param date_str The string containing a date being parsed + * @param format An index specifying a format in date_format_user + * @return The parsed value of date_str on success or -1 on failure + */ +static time_t parse_date_with_year(const char* date_str, int format) +{ + time_t rawtime; /* The integer time */ + struct tm retvalue, test_retvalue; /* The time in a broken-down structure */ + + int i, j, mem_length, orig_year = -1, orig_month = -1, orig_day = -1; + + /* Buffer for containing individual parts (e.g. year, month, day) of a date */ + char date_segment[5]; + + /* The compiled regular expression */ + regex_t preg = {0}; + + /* An array containing indices specifying the matched substrings in date_str */ + regmatch_t pmatch[4] = { {0}, {0}, {0}, {0} }; + + /* The regular expression for parsing dates */ + const char* regex = "^ *([0-9]+) *[-/.'] *([0-9]+) *[-/.'] *([0-9]+).*$|^ *([0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]).*$"; + + /* We get our matches using the regular expression. */ + regcomp(&preg, regex, REG_EXTENDED); + regexec(&preg, date_str, 4, pmatch, 0); + regfree(&preg); + + /* If there wasn't a match, there was an error. */ + if(pmatch[0].rm_eo == 0) + return -1; + + /* If this is a string without separators ... */ + if(pmatch[1].rm_so == -1) + { + /* ... we will fill in the indices based on the user's selection. */ + int k = 0; /* k traverses date_str by keeping track of where separators "should" be. */ + j = 1; /* j traverses pmatch. */ + for(i = 0; date_format_user[format][i]; i++) + { + char segment_type = date_format_user[format][i]; + /* Only do something if this is a meaningful character */ + if(segment_type == 'y' || segment_type == 'm' || segment_type == 'd') + { + pmatch[j].rm_so = k; + switch(segment_type) + { + case 'm': + case 'd': + k += 2; + break; + + case 'y': + k += 4; + break; + } + + pmatch[j].rm_eo = k; + j++; + } + } + } + + /* Put some sane values in retvalue by using the current time for + * the non-year-month-day parts of the date. */ + time(&rawtime); + localtime_r(&rawtime, &retvalue); + + /* j traverses pmatch (index 0 contains the entire string, so we + * start at index 1 for the first meaningful match). */ + j = 1; + /* Go through the date format and interpret the matches in order of + * the sections in the date format. */ + for(i = 0; date_format_user[format][i]; i++) + { + char segment_type = date_format_user[format][i]; + /* Only do something if this is a meaningful character */ + if(segment_type == 'y' || segment_type == 'm' || segment_type == 'd') + { + /* Copy the matching substring into date_segment so that we can + * convert it into an integer. */ + mem_length = pmatch[j].rm_eo - pmatch[j].rm_so; + memcpy(date_segment, date_str + pmatch[j].rm_so, mem_length); + date_segment[mem_length] = '\0'; + + /* Set the appropriate member of retvalue. Save the original + * values so that we can check if the change when we use mktime + * below. */ + switch(segment_type) + { + case 'y': + retvalue.tm_year = atoi(date_segment); + + /* Handle two-digit years. */ + if(retvalue.tm_year < 100) + { + /* We allow two-digit years in the range 1969 - 2068. */ + if(retvalue.tm_year < 69) + retvalue.tm_year += 100; + } + else + retvalue.tm_year -= 1900; + orig_year = retvalue.tm_year; + break; + + case 'm': + orig_month = retvalue.tm_mon = atoi(date_segment) - 1; + break; + + case 'd': + orig_day = retvalue.tm_mday = atoi(date_segment); + break; + } + j++; + } + } + /* Convert back to an integer. If mktime leaves retvalue unchanged, + * everything is okay; otherwise, an error has occurred. */ + /* We have to use a "test" date value to account for changes in + * daylight savings time, which can cause a date change with mktime + * near midnight, causing the code to incorrectly think a date is + * incorrect. */ + test_retvalue = retvalue; + mktime(&test_retvalue); + retvalue.tm_isdst = test_retvalue.tm_isdst; + rawtime = mktime(&retvalue); + if(retvalue.tm_mday == orig_day && + retvalue.tm_mon == orig_month && + retvalue.tm_year == orig_year) + { + return rawtime; + } + else + { + return -1; + } +} + +/** Parses a string into a date, given a format. The format cannot + * include the year. This function should only be called by + * parse_date. + * @param date_str The string containing a date being parsed + * @param format An index specifying a format in date_format_user + * @return The parsed value of date_str on success or -1 on failure + */ +static time_t parse_date_without_year(const char* date_str, int format) +{ + time_t rawtime; /* The integer time */ + struct tm retvalue, test_retvalue; /* The time in a broken-down structure */ + + int i, j, mem_length, orig_year = -1, orig_month = -1, orig_day = -1; + + /* Buffer for containing individual parts (e.g. year, month, day) of a date */ + gchar* date_segment; + + /* The compiled regular expression */ + regex_t preg = {0}; + + /* An array containing indices specifying the matched substrings in date_str */ + regmatch_t pmatch[3] = { {0}, {0}, {0} }; + + /* The regular expression for parsing dates */ + const char* regex = "^ *([0-9]+) *[-/.'] *([0-9]+).*$"; + + /* We get our matches using the regular expression. */ + regcomp(&preg, regex, REG_EXTENDED); + regexec(&preg, date_str, 3, pmatch, 0); + regfree(&preg); + + /* If there wasn't a match, there was an error. */ + if(pmatch[0].rm_eo == 0) + return -1; + + /* Put some sane values in retvalue by using the current time for + * the non-year-month-day parts of the date. */ + time(&rawtime); + localtime_r(&rawtime, &retvalue); + orig_year = retvalue.tm_year; + + /* j traverses pmatch (index 0 contains the entire string, so we + * start at index 1 for the first meaningful match). */ + j = 1; + /* Go through the date format and interpret the matches in order of + * the sections in the date format. */ + for(i = 0; date_format_user[format][i]; i++) + { + char segment_type = date_format_user[format][i]; + /* Only do something if this is a meaningful character */ + if(segment_type == 'm' || segment_type == 'd') + { + /* Copy the matching substring into date_segment so that we can + * convert it into an integer. */ + mem_length = pmatch[j].rm_eo - pmatch[j].rm_so; + date_segment = g_new(gchar, mem_length); + memcpy(date_segment, date_str + pmatch[j].rm_so, mem_length); + date_segment[mem_length] = '\0'; + + /* Set the appropriate member of retvalue. Save the original + * values so that we can check if the change when we use mktime + * below. */ + switch(segment_type) + { + case 'm': + orig_month = retvalue.tm_mon = atoi(date_segment) - 1; + break; + + case 'd': + orig_day = retvalue.tm_mday = atoi(date_segment); + break; + } + g_free(date_segment); + j++; + } + } + /* Convert back to an integer. If mktime leaves retvalue unchanged, + * everything is okay; otherwise, an error has occurred. */ + /* We have to use a "test" date value to account for changes in + * daylight savings time, which can cause a date change with mktime + * near midnight, causing the code to incorrectly think a date is + * incorrect. */ + test_retvalue = retvalue; + mktime(&test_retvalue); + retvalue.tm_isdst = test_retvalue.tm_isdst; + rawtime = mktime(&retvalue); + if(retvalue.tm_mday == orig_day && + retvalue.tm_mon == orig_month && + retvalue.tm_year == orig_year) + { + return rawtime; + } + else + { + return -1; + } +} + +/** Parses a string into a date, given a format. This function + * requires only knowing the order in which the year, month and day + * appear. For example, 01-02-2003 will be parsed the same way as + * 01/02/2003. + * @param date_str The string containing a date being parsed + * @param format An index specifying a format in date_format_user + * @return The parsed value of date_str on success or -1 on failure + */ +static time_t parse_date(const char* date_str, int format) +{ + if(strchr(date_format_user[format], 'y')) + return parse_date_with_year(date_str, format); + else + return parse_date_without_year(date_str, format); +} + +/** Constructor for GncCsvParseData. + * @return Pointer to a new GncCSvParseData + */ +GncCsvParseData* gnc_csv_new_parse_data(void) +{ + GncCsvParseData* parse_data = g_new(GncCsvParseData, 1); + parse_data->encoding = "UTF-8"; + /* All of the data pointers are initially NULL. This is so that, if + * gnc_csv_parse_data_free is called before all of the data is + * initialized, only the data that needs to be freed is freed. */ + parse_data->raw_str.begin = parse_data->raw_str.end + = parse_data->file_str.begin = parse_data->file_str.end = NULL; + parse_data->orig_lines = NULL; + parse_data->orig_row_lengths = NULL; + parse_data->column_types = NULL; + parse_data->error_lines = parse_data->transactions = NULL; + parse_data->options = default_parse_options(); + parse_data->date_format = -1; + parse_data->chunk = g_string_chunk_new(100 * 1024); + return parse_data; +} + +/** Destructor for GncCsvParseData. + * @param parse_data Parse data whose memory will be freed + */ +void gnc_csv_parse_data_free(GncCsvParseData* parse_data) +{ + /* All non-NULL pointers have been initialized and must be freed. */ + + if(parse_data->raw_mapping != NULL) + g_mapped_file_free(parse_data->raw_mapping); + + if(parse_data->file_str.begin != NULL) + g_free(parse_data->file_str.begin); + + if(parse_data->orig_lines != NULL) + stf_parse_general_free(parse_data->orig_lines); + + if(parse_data->orig_row_lengths != NULL) + g_array_free(parse_data->orig_row_lengths, FALSE); + + if(parse_data->options != NULL) + stf_parse_options_free(parse_data->options); + + if(parse_data->column_types != NULL) + g_array_free(parse_data->column_types, TRUE); + + if(parse_data->error_lines != NULL) + g_list_free(parse_data->error_lines); + + if(parse_data->transactions != NULL) + { + GList* transactions = parse_data->transactions; + /* We have to free the GncCsvTransLine's that are at each node in + * the list before freeing the entire list. */ + do + { + g_free(transactions->data); + transactions = g_list_next(transactions); + } while(transactions != NULL); + g_list_free(parse_data->transactions); + } + + g_free(parse_data->chunk); + g_free(parse_data); +} + +/** Converts raw file data using a new encoding. This function must be + * called after gnc_csv_load_file only if gnc_csv_load_file guessed + * the wrong encoding. + * @param parse_data Data that is being parsed + * @param encoding Encoding that data should be translated using + * @param error Will point to an error on failure + * @return 0 on success, 1 on failure + */ +int gnc_csv_convert_encoding(GncCsvParseData* parse_data, const char* encoding, + GError** error) +{ + gsize bytes_read, bytes_written; + + /* If parse_data->file_str has already been initialized it must be + * freed first. (This should always be the case, since + * gnc_csv_load_file should always be called before this + * function.) */ + if(parse_data->file_str.begin != NULL) + g_free(parse_data->file_str.begin); + + /* Do the actual translation to UTF-8. */ + parse_data->file_str.begin = g_convert(parse_data->raw_str.begin, + parse_data->raw_str.end - parse_data->raw_str.begin, + "UTF-8", encoding, &bytes_read, &bytes_written, + error); + /* Handle errors that occur. */ + if(parse_data->file_str.begin == NULL) + return 1; + + /* On success, save the ending pointer of the translated data and + * the encoding type and return 0. */ + parse_data->file_str.end = parse_data->file_str.begin + bytes_written; + parse_data->encoding = (gchar*)encoding; + return 0; +} + +/** Loads a file into a GncCsvParseData. This is the first function + * that must be called after createing a new GncCsvParseData. If this + * fails because the file couldn't be opened, no more functions can be + * called on the parse data until this succeeds (or until it fails + * because of an encoding guess error). If it fails because the + * encoding could not be guessed, gnc_csv_convert_encoding must be + * called until it succeeds. + * @param parse_data Data that is being parsed + * @param filename Name of the file that should be opened + * @param error Will contain an error if there is a failure + * @return 0 on success, 1 on failure + */ +int gnc_csv_load_file(GncCsvParseData* parse_data, const char* filename, + GError** error) +{ + const char* guess_enc; + + /* Get the raw data first and handle an error if one occurs. */ + parse_data->raw_mapping = g_mapped_file_new(filename, FALSE, error); + if(parse_data->raw_mapping == NULL) + { + /* TODO Handle file opening errors more specifically, + * e.g. inexistent file versus no read permission. */ + parse_data->raw_str.begin = NULL; + g_set_error(error, 0, GNC_CSV_FILE_OPEN_ERR, _("File opening failed.")); + return 1; + } + + /* Copy the mapping's contents into parse-data->raw_str. */ + parse_data->raw_str.begin = g_mapped_file_get_contents(parse_data->raw_mapping); + parse_data->raw_str.end = parse_data->raw_str.begin + g_mapped_file_get_length(parse_data->raw_mapping); + + /* Make a guess at the encoding of the data. */ + guess_enc = go_guess_encoding((const char*)(parse_data->raw_str.begin), + (size_t)(parse_data->raw_str.end - parse_data->raw_str.begin), + "UTF-8", NULL); + if(guess_enc == NULL) + { + g_set_error(error, 0, GNC_CSV_ENCODING_ERR, _("Unknown encoding.")); + return 1; + } + + /* Convert using the guessed encoding into parse_data->file_str and + * handle any errors that occur. */ + gnc_csv_convert_encoding(parse_data, guess_enc, error); + if(parse_data->file_str.begin == NULL) + { + g_set_error(error, 0, GNC_CSV_ENCODING_ERR, _("Unknown encoding.")); + return 1; + } + else + return 0; +} + +/** Parses a file into cells. This requires having an encoding that + * works (see gnc_csv_convert_encoding). parse_data->options should be + * set according to how the user wants before calling this + * function. (Note: this function must be called with guessColTypes as + * TRUE before it is ever called with it as FALSE.) (Note: if + * guessColTypes is TRUE, all the column types will be GNC_CSV_NONE + * right now.) + * @param parse_data Data that is being parsed + * @param guessColTypes TRUE to guess what the types of columns are based on the cell contents + * @error error Will contain an error if there is a failure + * @return 0 on success, 1 on failure + */ +int gnc_csv_parse(GncCsvParseData* parse_data, gboolean guessColTypes, GError** error) +{ + /* max_cols is the number of columns in the row with the most columns. */ + int i, max_cols = 0; + + if(parse_data->orig_lines != NULL) + { + stf_parse_general_free(parse_data->orig_lines); + } + + /* If everything is fine ... */ + if(parse_data->file_str.begin != NULL) + { + /* Do the actual parsing. */ + parse_data->orig_lines = stf_parse_general(parse_data->options, parse_data->chunk, + parse_data->file_str.begin, + parse_data->file_str.end); + } + /* If we couldn't get the encoding right, we just want an empty array. */ + else + { + parse_data->orig_lines = g_ptr_array_new(); + } + + /* Record the original row lengths of parse_data->orig_lines. */ + if(parse_data->orig_row_lengths != NULL) + g_array_free(parse_data->orig_row_lengths, FALSE); + + parse_data->orig_row_lengths = + g_array_sized_new(FALSE, FALSE, sizeof(int), parse_data->orig_lines->len); + g_array_set_size(parse_data->orig_row_lengths, parse_data->orig_lines->len); + parse_data->orig_max_row = 0; + for(i = 0; i < parse_data->orig_lines->len; i++) + { + int length = ((GPtrArray*)parse_data->orig_lines->pdata[i])->len; + parse_data->orig_row_lengths->data[i] = length; + if(length > parse_data->orig_max_row) + parse_data->orig_max_row = length; + } + + /* If it failed, generate an error. */ + if(parse_data->orig_lines == NULL) + { + g_set_error(error, 0, 0, "Parsing failed."); + return 1; + } + + /* Now that we have data, let's set max_cols. */ + for(i = 0; i < parse_data->orig_lines->len; i++) + { + if(max_cols < ((GPtrArray*)(parse_data->orig_lines->pdata[i]))->len) + max_cols = ((GPtrArray*)(parse_data->orig_lines->pdata[i]))->len; + } + + if(guessColTypes) + { + /* Free parse_data->column_types if it's already been created. */ + if(parse_data->column_types != NULL) + g_array_free(parse_data->column_types, TRUE); + + /* Create parse_data->column_types and fill it with guesses based + * on the contents of each column. */ + parse_data->column_types = g_array_sized_new(FALSE, FALSE, sizeof(int), + max_cols); + g_array_set_size(parse_data->column_types, max_cols); + /* TODO Make it actually guess. */ + for(i = 0; i < parse_data->column_types->len; i++) + { + parse_data->column_types->data[i] = GNC_CSV_NONE; + } + } + else + { + /* If we don't need to guess column types, we will simply set any + * new columns that are created that didn't exist before to "None" + * since we don't want gibberish to appear. Note: + * parse_data->column_types should have already been + * initialized, so we don't check for it being NULL. */ + int i = parse_data->column_types->len; + g_array_set_size(parse_data->column_types, max_cols); + for(; i < parse_data->column_types->len; i++) + { + parse_data->column_types->data[i] = GNC_CSV_NONE; + } + } + + return 0; +} + +/** A struct containing TransProperties that all describe a single transaction. */ +typedef struct +{ + int date_format; /**< The format for parsing dates */ + Account* account; /**< The account the transaction belongs to */ + GList* properties; /**< List of TransProperties */ +} TransPropertyList; + +/** A struct encapsulating a property of a transaction. */ +typedef struct +{ + int type; /**< A value from the GncCsvColumnType enum except + * GNC_CSV_NONE and GNC_CSV_NUM_COL_TYPES */ + void* value; /**< Pointer to the data that will be used to configure a transaction */ + TransPropertyList* list; /**< The list the property belongs to */ +} TransProperty; + +/** Constructor for TransProperty. + * @param type The type of the new property (see TransProperty.type for possible values) + */ +static TransProperty* trans_property_new(int type, TransPropertyList* list) +{ + TransProperty* prop = g_new(TransProperty, 1); + prop->type = type; + prop->list = list; + prop->value = NULL; + return prop; +} + +/** Destructor for TransProperty. + * @param prop The property to be freed + */ +static void trans_property_free(TransProperty* prop) +{ + switch(prop->type) + { + /* The types for "Date" and "Balance" (time_t and gnc_numeric, + * respectively) are typically not pointed to, we have to free + * them, unlike types like char* ("Description"). */ + case GNC_CSV_DATE: + case GNC_CSV_BALANCE: + case GNC_CSV_DEPOSIT: + case GNC_CSV_WITHDRAWAL: + if(prop->value != NULL) + g_free(prop->value); + break; + } + g_free(prop); +} + +/** Sets the value of the property by parsing str. Note: this should + * only be called once on an instance of TransProperty, as calling it + * more than once can cause memory leaks. + * @param prop The property being set + * @param str The string to be parsed + * @return TRUE on success, FALSE on failure + */ +static gboolean trans_property_set(TransProperty* prop, char* str) +{ + char *endptr, *possible_currency_symbol, *str_dupe; + double value; + switch(prop->type) + { + case GNC_CSV_DATE: + prop->value = g_new(time_t, 1); + *((time_t*)(prop->value)) = parse_date(str, prop->list->date_format); + return *((time_t*)(prop->value)) != -1; + + case GNC_CSV_DESCRIPTION: + case GNC_CSV_NUM: + prop->value = g_strdup(str); + return TRUE; + + case GNC_CSV_BALANCE: + case GNC_CSV_DEPOSIT: + case GNC_CSV_WITHDRAWAL: + str_dupe = g_strdup(str); /* First, we make a copy so we can't mess up real data. */ + + /* Go through str_dupe looking for currency symbols. */ + for(possible_currency_symbol = str_dupe; *possible_currency_symbol; + possible_currency_symbol = g_utf8_next_char(possible_currency_symbol)) + { + if(g_unichar_type(g_utf8_get_char(possible_currency_symbol)) == G_UNICODE_CURRENCY_SYMBOL) + { + /* If we find a currency symbol, save the position just ahead + * of the currency symbol (next_symbol), and find the null + * terminator of the string (last_symbol). */ + char *next_symbol = g_utf8_next_char(possible_currency_symbol), *last_symbol = next_symbol; + while(*last_symbol) + last_symbol = g_utf8_next_char(last_symbol); + + /* Move all of the string (including the null byte, which is + * why we have +1 in the size parameter) following the + * currency symbol back one character, thereby overwriting the + * currency symbol. */ + memmove(possible_currency_symbol, next_symbol, last_symbol - next_symbol + 1); + break; + } + } + + /* Translate the string (now clean of currency symbols) into a number. */ + value = strtod(str_dupe, &endptr); + + /* If this isn't a valid numeric string, this is an error. */ + if(endptr != str_dupe + strlen(str_dupe)) + { + g_free(str_dupe); + return FALSE; + } + + g_free(str_dupe); + + if(abs(value) > 0.00001) + { + prop->value = g_new(gnc_numeric, 1); + *((gnc_numeric*)(prop->value)) = + double_to_gnc_numeric(value, xaccAccountGetCommoditySCU(prop->list->account), + GNC_RND_ROUND); + } + return TRUE; + } + return FALSE; /* We should never actually get here. */ +} + +/** Constructor for TransPropertyList. + * @param account The account with which transactions should be built + * @param date_format An index from date_format_user for how date properties should be parsed + * @return A pointer to a new TransPropertyList + */ +static TransPropertyList* trans_property_list_new(Account* account, int date_format) +{ + TransPropertyList* list = g_new(TransPropertyList, 1); + list->account = account; + list->date_format = date_format; + list->properties = NULL; + return list; +} + +/** Destructor for TransPropertyList. + * @param list The list to be freed + */ +static void trans_property_list_free(TransPropertyList* list) +{ + /* Free all of the properties in this list before freeeing the list itself. */ + GList* properties_begin = list->properties; + while(list->properties != NULL) + { + trans_property_free((TransProperty*)(list->properties->data)); + list->properties = g_list_next(list->properties); + } + g_list_free(properties_begin); + g_free(list); +} + +/** Adds a property to the list it's linked with. + * (The TransPropertyList is not passed as a parameter because the property is + * associated with a list when it's constructed.) + * @param property The property to be added to its list + */ +static void trans_property_list_add(TransProperty* property) +{ + property->list->properties = g_list_append(property->list->properties, property); +} + +/** Adds a split to a transaction. + * @param trans The transaction to add a split to + * @param account The account used for the split + * @param book The book where the split should be stored + * @param amount The amount of the split + */ +static void trans_add_split(Transaction* trans, Account* account, GNCBook* book, + gnc_numeric amount) +{ + Split* split = xaccMallocSplit(book); + xaccSplitSetAccount(split, account); + xaccSplitSetParent(split, trans); + xaccSplitSetAmount(split, amount); + xaccSplitSetValue(split, amount); + xaccSplitSetAction(split, "Deposit"); +} + +/** Tests a TransPropertyList for having enough essential properties. + * Essential properties are "Date" and one of the following: "Balance", "Deposit", or + * "Withdrawal". + * @param list The list we are checking + * @param error Contains an error message on failure + * @return TRUE if there are enough essentials; FALSE otherwise + */ +static gboolean trans_property_list_verify_essentials(TransPropertyList* list, gchar** error) +{ + int i; + /* possible_errors lists the ways in which a list can fail this test. */ + enum PossibleErrorTypes {NO_DATE, NO_AMOUNT, NUM_OF_POSSIBLE_ERRORS}; + gchar* possible_errors[NUM_OF_POSSIBLE_ERRORS] = {N_("No date column."), + N_("No balance, deposit, or withdrawal column.")}; + int possible_error_lengths[NUM_OF_POSSIBLE_ERRORS] = {0}; + GList *properties_begin = list->properties, *errors_list = NULL; + + /* Go through each of the properties and erase possible errors. */ + while(list->properties) + { + switch(((TransProperty*)(list->properties->data))->type) + { + case GNC_CSV_DATE: + possible_errors[NO_DATE] = NULL; + break; + + case GNC_CSV_BALANCE: + case GNC_CSV_DEPOSIT: + case GNC_CSV_WITHDRAWAL: + possible_errors[NO_AMOUNT] = NULL; + break; + } + list->properties = g_list_next(list->properties); + } + list->properties = properties_begin; + + /* Accumulate a list of the actual errors. */ + for(i = 0; i < NUM_OF_POSSIBLE_ERRORS; i++) + { + if(possible_errors[i] != NULL) + { + errors_list = g_list_append(errors_list, GINT_TO_POINTER(i)); + /* Since we added an error, we want to also store its length for + * when we construct the full error string. */ + possible_error_lengths[i] = strlen(_(possible_errors[i])); + } + } + + /* If there are no errors, we can quit now. */ + if(errors_list == NULL) + return TRUE; + else + { + /* full_error_size is the full length of the error message. */ + int full_error_size = 0, string_length = 0; + GList* errors_list_begin = errors_list; + gchar *error_message, *error_message_begin; + + /* Find the value for full_error_size. */ + while(errors_list) + { + /* We add an extra 1 to account for spaces in between messages. */ + full_error_size += possible_error_lengths[GPOINTER_TO_INT(errors_list->data)] + 1; + errors_list = g_list_next(errors_list); + } + errors_list = errors_list_begin; + + /* Append the error messages one after another. */ + error_message = error_message_begin = g_new(gchar, full_error_size); + while(errors_list) + { + i = GPOINTER_TO_INT(errors_list->data); + string_length = possible_error_lengths[i]; + + /* Copy the error message and put a space after it. */ + strncpy(error_message, _(possible_errors[i]), string_length); + error_message += string_length; + *error_message = ' '; + error_message++; + + errors_list = g_list_next(errors_list); + } + *error_message = '\0'; /* Replace the last space with the null byte. */ + g_list_free(errors_list_begin); + + *error = error_message_begin; + return FALSE; + } +} + +/** Create a Transaction from a TransPropertyList. + * @param list The list of properties + * @param error Contains an error on failure + * @return On success, a GncCsvTransLine; on failure, the trans pointer is NULL + */ +static GncCsvTransLine* trans_property_list_to_trans(TransPropertyList* list, gchar** error) +{ + GncCsvTransLine* trans_line = g_new(GncCsvTransLine, 1); + GList* properties_begin = list->properties; + GNCBook* book = gnc_account_get_book(list->account); + gnc_commodity* currency = xaccAccountGetCommodity(list->account); + gnc_numeric amount = double_to_gnc_numeric(0.0, xaccAccountGetCommoditySCU(list->account), + GNC_RND_ROUND); + + /* This flag is set to TRUE if we can use the "Deposit" or "Withdrawal" column. */ + gboolean amount_set = FALSE; + + /* The balance is 0 by default. */ + trans_line->balance_set = FALSE; + trans_line->balance = amount; + + /* We make the line_no -1 just to mark that it hasn't been set. We + * may get rid of line_no soon anyway, so it's not particularly + * important. */ + trans_line->line_no = -1; + + /* Make sure this is a transaction with all the columns we need. */ + if(!trans_property_list_verify_essentials(list, error)) + { + g_free(trans_line); + return NULL; + } + + trans_line->trans = xaccMallocTransaction(book); + xaccTransBeginEdit(trans_line->trans); + xaccTransSetCurrency(trans_line->trans, currency); + + /* Go through each of the properties and edit the transaction accordingly. */ + list->properties = properties_begin; + while(list->properties != NULL) + { + TransProperty* prop = (TransProperty*)(list->properties->data); + switch(prop->type) + { + case GNC_CSV_DATE: + xaccTransSetDatePostedSecs(trans_line->trans, *((time_t*)(prop->value))); + break; + + case GNC_CSV_DESCRIPTION: + xaccTransSetDescription(trans_line->trans, (char*)(prop->value)); + break; + + case GNC_CSV_NUM: + xaccTransSetNum(trans_line->trans, (char*)(prop->value)); + break; + + case GNC_CSV_DEPOSIT: /* Add deposits to the existing amount. */ + if(prop->value != NULL) + { + amount = gnc_numeric_add(*((gnc_numeric*)(prop->value)), + amount, + xaccAccountGetCommoditySCU(list->account), + GNC_RND_ROUND); + amount_set = TRUE; + /* We will use the "Deposit" and "Withdrawal" columns in preference to "Balance". */ + trans_line->balance_set = FALSE; + } + break; + + case GNC_CSV_WITHDRAWAL: /* Withdrawals are just negative deposits. */ + if(prop->value != NULL) + { + amount = gnc_numeric_add(gnc_numeric_neg(*((gnc_numeric*)(prop->value))), + amount, + xaccAccountGetCommoditySCU(list->account), + GNC_RND_ROUND); + amount_set = TRUE; + /* We will use the "Deposit" and "Withdrawal" columns in preference to "Balance". */ + trans_line->balance_set = FALSE; + } + break; + + case GNC_CSV_BALANCE: /* The balance gets stored in a separate field in trans_line. */ + /* We will use the "Deposit" and "Withdrawal" columns in preference to "Balance". */ + if(!amount_set && prop->value != NULL) + { + /* This gets put into the actual transaction at the end of gnc_csv_parse_to_trans. */ + trans_line->balance = *((gnc_numeric*)(prop->value)); + trans_line->balance_set = TRUE; + } + break; + } + list->properties = g_list_next(list->properties); + } + + /* Add a split with the cumulative amount value. */ + trans_add_split(trans_line->trans, list->account, book, amount); + + return trans_line; +} + +/** Creates a list of transactions from parsed data. Transactions that + * could be created from rows are placed in parse_data->transactions; + * rows that fail are placed in parse_data->error_lines. (Note: there + * is no way for this function to "fail," i.e. it only returns 0, so + * it may be changed to a void function in the future.) + * @param parse_data Data that is being parsed + * @param account Account with which transactions are created + * @param redo_errors TRUE to convert only error data, FALSE for all data + * @return 0 on success, 1 on failure + */ +int gnc_csv_parse_to_trans(GncCsvParseData* parse_data, Account* account, + gboolean redo_errors) +{ + gboolean hasBalanceColumn; + int i, j, max_cols = 0; + GArray* column_types = parse_data->column_types; + GList *error_lines = NULL, *begin_error_lines = NULL; + + /* last_transaction points to the last element in + * parse_data->transactions, or NULL if it's empty. */ + GList* last_transaction = NULL; + + /* Free parse_data->error_lines and parse_data->transactions if they + * already exist. */ + if(redo_errors) /* If we're redoing errors, we save freeing until the end. */ + { + begin_error_lines = error_lines = parse_data->error_lines; + } + else + { + if(parse_data->error_lines != NULL) + { + g_list_free(parse_data->error_lines); + } + if(parse_data->transactions != NULL) + { + g_list_free(parse_data->transactions); + } + } + parse_data->error_lines = NULL; + + if(redo_errors) /* If we're looking only at error data ... */ + { + if(parse_data->transactions == NULL) + { + last_transaction = NULL; + } + else + { + /* Move last_transaction to the end. */ + last_transaction = parse_data->transactions; + while(g_list_next(last_transaction) != NULL) + { + last_transaction = g_list_next(last_transaction); + } + } + /* ... we use only the lines in error_lines. */ + if(error_lines == NULL) + i = parse_data->orig_lines->len; /* Don't go into the for loop. */ + else + i = GPOINTER_TO_INT(error_lines->data); + } + else /* Otherwise, we look at all the data. */ + { + /* The following while-loop effectively behaves like the following for-loop: + * for(i = 0; i < parse_data->orig_lines->len; i++). */ + i = 0; + last_transaction = NULL; + } + while(i < parse_data->orig_lines->len) + { + GPtrArray* line = parse_data->orig_lines->pdata[i]; + /* This flag is TRUE if there are any errors in this row. */ + gboolean errors = FALSE; + gchar* error_message = NULL; + TransPropertyList* list = trans_property_list_new(account, parse_data->date_format); + GncCsvTransLine* trans_line = NULL; + + for(j = 0; j < line->len; j++) + { + /* We do nothing in "None" columns. */ + if(column_types->data[j] != GNC_CSV_NONE) + { + /* Affect the transaction appropriately. */ + TransProperty* property = trans_property_new(column_types->data[j], list); + gboolean succeeded = trans_property_set(property, line->pdata[j]); + /* TODO Maybe move error handling to within TransPropertyList functions? */ + if(succeeded) + { + trans_property_list_add(property); + } + else + { + errors = TRUE; + error_message = g_strdup_printf(_("%s column could not be understood."), + _(gnc_csv_column_type_strs[property->type])); + trans_property_free(property); + break; + } + } + } + + /* If we had success, add the transaction to parse_data->transaction. */ + if(!errors) + { + trans_line = trans_property_list_to_trans(list, &error_message); + errors = trans_line == NULL; + } + + trans_property_list_free(list); + + /* If there were errors, add this line to parse_data->error_lines. */ + if(errors) + { + parse_data->error_lines = g_list_append(parse_data->error_lines, + GINT_TO_POINTER(i)); + /* If there's already an error message, we need to replace it. */ + if(line->len > (int)(parse_data->orig_row_lengths->data[i])) + { + g_free(line->pdata[line->len - 1]); + line->pdata[line->len - 1] = error_message; + } + else + { + /* Put the error message at the end of the line. */ + g_ptr_array_add(line, error_message); + } + } + else + { + /* If all went well, add this transaction to the list. */ + trans_line->line_no = i; + + /* We keep the transactions sorted by date. We start at the end + * of the list and go backward, simply because the file itself + * is probably also sorted by date (but we need to handle the + * exception anyway). */ + + /* If we can just put it at the end, do so and increment last_transaction. */ + if(last_transaction == NULL || + xaccTransGetDate(((GncCsvTransLine*)(last_transaction->data))->trans) <= xaccTransGetDate(trans_line->trans)) + { + parse_data->transactions = g_list_append(parse_data->transactions, trans_line); + /* If this is the first transaction, we need to get last_transaction on track. */ + if(last_transaction == NULL) + last_transaction = parse_data->transactions; + else /* Otherwise, we can just continue. */ + last_transaction = g_list_next(last_transaction); + } + /* Otherwise, search backward for the correct spot. */ + else + { + GList* insertion_spot = last_transaction; + while(insertion_spot != NULL && + xaccTransGetDate(((GncCsvTransLine*)(insertion_spot->data))->trans) > xaccTransGetDate(trans_line->trans)) + { + insertion_spot = g_list_previous(insertion_spot); + } + /* Move insertion_spot one location forward since we have to + * use the g_list_insert_before function. */ + if(insertion_spot == NULL) /* We need to handle the case of inserting at the beginning of the list. */ + insertion_spot = parse_data->transactions; + else + insertion_spot = g_list_next(insertion_spot); + + parse_data->transactions = g_list_insert_before(parse_data->transactions, insertion_spot, trans_line); + } + } + + /* Increment to the next row. */ + if(redo_errors) + { + /* Move to the next error line in the list. */ + error_lines = g_list_next(error_lines); + if(error_lines == NULL) + i = parse_data->orig_lines->len; /* Don't continue the for loop. */ + else + i = GPOINTER_TO_INT(error_lines->data); + } + else + { + i++; + } + } + + /* If we have a balance column, set the appropriate amounts on the transactions. */ + hasBalanceColumn = FALSE; + for(i = 0; i < parse_data->column_types->len; i++) + { + if(parse_data->column_types->data[i] == GNC_CSV_BALANCE) + { + hasBalanceColumn = TRUE; + break; + } + } + + if(hasBalanceColumn) + { + GList* transactions = parse_data->transactions; + + /* balance_offset is how much the balance currently in the account + * differs from what it will be after the transactions are + * imported. This will be sum of all the previous transactions for + * any given transaction. */ + gnc_numeric balance_offset = double_to_gnc_numeric(0.0, + xaccAccountGetCommoditySCU(account), + GNC_RND_ROUND); + while(transactions != NULL) + { + GncCsvTransLine* trans_line = (GncCsvTransLine*)transactions->data; + if(trans_line->balance_set) + { + time_t date = xaccTransGetDate(trans_line->trans); + /* Find what the balance should be by adding the offset to the actual balance. */ + gnc_numeric existing_balance = gnc_numeric_add(balance_offset, + xaccAccountGetBalanceAsOfDate(account, date), + xaccAccountGetCommoditySCU(account), + GNC_RND_ROUND); + + /* The amount of the transaction is the difference between the new and existing balance. */ + gnc_numeric amount = gnc_numeric_sub(trans_line->balance, + existing_balance, + xaccAccountGetCommoditySCU(account), + GNC_RND_ROUND); + + SplitList* splits = xaccTransGetSplitList(trans_line->trans); + while(splits) + { + SplitList* next_splits = g_list_next(splits); + xaccSplitDestroy((Split*)splits->data); + splits = next_splits; + } + + trans_add_split(trans_line->trans, account, gnc_account_get_book(account), amount); + + /* This new transaction needs to be added to the balance offset. */ + balance_offset = gnc_numeric_add(balance_offset, + amount, + xaccAccountGetCommoditySCU(account), + GNC_RND_ROUND); + } + transactions = g_list_next(transactions); + } + } + + if(redo_errors) /* Now that we're at the end, we do the freeing. */ + { + g_list_free(begin_error_lines); + } + + /* We need to resize parse_data->column_types since errors may have added columns. */ + for(i = 0; i < parse_data->orig_lines->len; i++) + { + if(max_cols < ((GPtrArray*)(parse_data->orig_lines->pdata[i]))->len) + max_cols = ((GPtrArray*)(parse_data->orig_lines->pdata[i]))->len; + } + i = parse_data->column_types->len; + parse_data->column_types = g_array_set_size(parse_data->column_types, max_cols); + for(; i < max_cols; i++) + { + parse_data->column_types->data[i] = GNC_CSV_NONE; + } + + return 0; +} diff --git a/src/import-export/csv/gnc-csv-model.h b/src/import-export/csv/gnc-csv-model.h new file mode 100644 index 0000000000..f9a97d71e5 --- /dev/null +++ b/src/import-export/csv/gnc-csv-model.h @@ -0,0 +1,122 @@ +/********************************************************************\ + * 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 2 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, contact: * + * * + * Free Software Foundation Voice: +1-617-542-5942 * + * 51 Franklin Street, Fifth Floor Fax: +1-617-542-2652 * + * Boston, MA 02110-1301, USA gnu@gnu.org * +\********************************************************************/ + +/** @file + @brief CSV import GUI + * + gnc-csv-import.h + @author Copyright (c) 2007 Benny Sperisen + */ + +#ifndef GNC_CSV_MODEL_H +#define GNC_CSV_MODEL_H + +#include "config.h" + +#include "Account.h" +#include "Transaction.h" + +#include "stf/stf-parse.h" + +/** Enumeration for column types. These are the different types of + * columns that can exist in a CSV/Fixed-Width file. There should be + * no two columns with the same type except for the GNC_CSV_NONE + * type. */ +enum GncCsvColumnType {GNC_CSV_NONE, + GNC_CSV_DATE, + GNC_CSV_DESCRIPTION, + GNC_CSV_BALANCE, + GNC_CSV_DEPOSIT, + GNC_CSV_WITHDRAWAL, + GNC_CSV_NUM, + GNC_CSV_NUM_COL_TYPES}; + +/** Enumeration for error types. These are the different types of + * errors that various functions used for the CSV/Fixed-Width importer + * can have. */ +enum GncCsvErrorType {GNC_CSV_FILE_OPEN_ERR, + GNC_CSV_ENCODING_ERR}; + +/** Struct for containing a string. This struct simply contains + * pointers to the beginning and end of a string. We need this because + * the STF code that gnc_csv_parse calls requires these pointers. */ +typedef struct +{ + char* begin; + char* end; +} GncCsvStr; + +/* TODO We now sort transactions by date, not line number, so we + * should probably get rid of this struct and uses of it. */ + +/** Struct pairing a transaction with a line number. This struct is + * used to keep the transactions in order. When rows are separated + * into "valid" and "error" lists (in case some of the rows have cells + * that are unparseable), we want the user to still be able to + * "correct" the error list. If we keep the line numbers of valid + * transactions, we can then put transactions created from the newly + * corrected rows into the right places. */ +typedef struct +{ + int line_no; + Transaction* trans; + gnc_numeric balance; /**< The (supposed) balance after this transaction takes place */ + gboolean balance_set; /**< TRUE if balance has been set from user data, FALSE otherwise */ +} GncCsvTransLine; + +extern const int num_date_formats; +/* A set of date formats that the user sees. */ +extern const gchar* date_format_user[]; + +/* This array contains all of the different strings for different column types. */ +extern gchar* gnc_csv_column_type_strs[]; + +/** Struct containing data for parsing a CSV/Fixed-Width file. */ +typedef struct +{ + gchar* encoding; + GMappedFile* raw_mapping; /**< The mapping containing raw_str */ + GncCsvStr raw_str; /**< Untouched data from the file as a string */ + GncCsvStr file_str; /**< raw_str translated into UTF-8 */ + GPtrArray* orig_lines; /**< file_str parsed into a two-dimensional array of strings */ + GArray* orig_row_lengths; /**< The lengths of rows in orig_lines + * before error messages are appended */ + int orig_max_row; /**< Holds the maximum value in orig_row_lengths */ + GStringChunk* chunk; /**< A chunk of memory in which the contents of orig_lines is stored */ + StfParseOptions_t* options; /**< Options controlling how file_str should be parsed */ + GArray* column_types; /**< Array of values from the GncCsvColumnType enumeration */ + GList* error_lines; /**< List of row numbers in orig_lines that have errors */ + GList* transactions; /**< List of GncCsvTransLine*s created using orig_lines and column_types */ + int date_format; /**< The format of the text in the date columns from date_format_internal. */ +} GncCsvParseData; + +GncCsvParseData* gnc_csv_new_parse_data(void); + +void gnc_csv_parse_data_free(GncCsvParseData* parse_data); + +int gnc_csv_load_file(GncCsvParseData* parse_data, const char* filename, + GError** error); + +int gnc_csv_convert_encoding(GncCsvParseData* parse_data, const char* encoding, GError** error); + +int gnc_csv_parse(GncCsvParseData* parse_data, gboolean guessColTypes, GError** error); + +int gnc_csv_parse_to_trans(GncCsvParseData* parse_data, Account* account, gboolean redo_errors); + +#endif diff --git a/src/import-export/csv/gnc-csv-preview-dialog.glade b/src/import-export/csv/gnc-csv-preview-dialog.glade new file mode 100644 index 0000000000..4310ac73f5 --- /dev/null +++ b/src/import-export/csv/gnc-csv-preview-dialog.glade @@ -0,0 +1,496 @@ + + + + + + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + 5 + Import CSV/Fixed-Width File + GTK_WIN_POS_CENTER_ON_PARENT + GDK_WINDOW_TYPE_HINT_DIALOG + False + + + True + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + 2 + + + True + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + + + + + + True + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + 4 + 2 + + + + + + True + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + + + 1 + 2 + 3 + 4 + GTK_SHRINK | GTK_FILL + GTK_FILL + 3 + + + + + True + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + + + 3 + 4 + GTK_SHRINK | GTK_FILL + GTK_FILL + 3 + + + + + True + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + + + True + True + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + Separated + True + True + + + + + True + True + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + Fixed-Width + True + True + csv_button + + + 1 + + + + + 1 + 2 + 2 + 3 + GTK_FILL + + + + + True + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + Data type: + + + 2 + 3 + GTK_FILL + GTK_FILL + + + + + True + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + + + 1 + 2 + 1 + 2 + GTK_FILL + + + + + True + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + + + 1 + 2 + GTK_SHRINK | GTK_FILL + GTK_FILL + 3 + + + + + True + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + Encoding: + + + GTK_FILL + GTK_FILL + + + + + False + 1 + + + + + True + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + 0 + GTK_SHADOW_NONE + + + True + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + 12 + + + True + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + 3 + 3 + 3 + 3 + + + True + True + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + Space + True + + + GTK_FILL + + + + + True + True + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + Tab + True + + + 1 + 2 + GTK_FILL + + + + + True + True + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + Comma (,) + True + True + + + 2 + 3 + GTK_FILL + + + + + True + True + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + Colon (:) + True + + + 1 + 2 + GTK_FILL + GTK_FILL + + + + + True + True + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + Semicolon (;) + True + + + 1 + 2 + 1 + 2 + GTK_FILL + GTK_FILL + + + + + True + True + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + Hyphen (-) + True + + + 2 + 3 + 1 + 2 + GTK_FILL + GTK_FILL + + + + + True + True + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + Custom + True + + + 2 + 3 + GTK_FILL + GTK_FILL + + + + + True + True + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + + + 1 + 3 + 2 + 3 + GTK_FILL + GTK_FILL + + + + + + + + + True + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + Separators + True + + + label_item + + + + + False + 2 + + + + + True + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + + + False + 3 + + + + + True + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + + + False + 3 + 4 + + + + + True + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + 0 + GTK_SHADOW_NONE + + + True + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + 12 + + + + + + + + True + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + Date Format + True + + + label_item + + + + + False + 5 + + + + + True + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + + + False + 3 + 6 + + + + + True + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + + + True + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + gtk-dialog-info + + + False + 2 + + + + + True + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + 0 + Select the type of each column below. + True + + + 1 + + + + + False + 7 + + + + + True + True + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + False + True + GTK_TREE_VIEW_GRID_LINES_BOTH + + + False + 8 + + + + + True + True + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + GTK_POLICY_AUTOMATIC + GTK_POLICY_AUTOMATIC + + + True + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + GTK_RESIZE_QUEUE + + + True + True + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + True + GTK_TREE_VIEW_GRID_LINES_BOTH + + + + + + + 9 + + + + + 1 + + + + + True + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + GTK_BUTTONBOX_END + + + True + True + True + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + gtk-cancel + True + + + + + True + True + True + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + gtk-ok + True + + + 1 + + + + + False + GTK_PACK_END + + + + + + diff --git a/src/import-export/csv/gnc-csv2glist.c b/src/import-export/csv/gnc-csv2glist.c deleted file mode 100644 index b00921e414..0000000000 --- a/src/import-export/csv/gnc-csv2glist.c +++ /dev/null @@ -1,187 +0,0 @@ - -/* - Copyright 2004 Kevin dot Hammack at comcast dot net - - 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 2 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, write to the Free Software - Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA - 02110-1301 USA - - - */ - -//#include "config.h" - -#include -#include -#include -#include -#include - -//#include "file-utils.h" - -#define TEST_CSV - -/* - Yea, just returning a GList would be fine.. Or a GList of - GLists, one per row.. -*/ - - -/* - split_csv_list - Take a string, split on commas into a GList. - Tricky part: honor quotes. - Quotes aren't stripped. - This would have been both easier and cleaner in scheme, and / or - using regular expressions. - I was thinking small and fast when I wrote it. :-P -*/ - -static GList * -split_csv_line(char *line) { - GList *csvlist = NULL; - gchar *begin; - gchar *current; - gchar *cell; - gchar quote=0; - gboolean eol = FALSE; - - current = line; - begin = current; - - while (!eol) { - - if (quote && - (*current == quote) && (*(current-1) != '\\') && - (current != begin)) { - - quote = 0; - } - else if (!quote && (*current == '"')) { quote = '"'; } - else if (!quote && (*current == '\'')) { quote = '\''; } - - if (!quote && (*current == ',')) { *current = 0; } - - if (*current == '\n') { - *current = 0; - eol = TRUE; - } - - if (*current == 0) { - cell = g_strdup( begin ); - csvlist = g_list_prepend(csvlist, cell); - current++; - begin = current; - quote = 0; - } - else { - current++; - } - - } - - return g_list_reverse(csvlist); -} - - -#if 1 -gint64 -gnc_getline (gchar **line, FILE *file) -{ - char str[BUFSIZ]; - gint64 len; - GString *gs = g_string_new(""); - - if (!line || !file) return 0; - - while (fgets(str, sizeof(str), file) != NULL) { - g_string_append(gs, str); - - len = strlen(str); - if (str[len-1] == '\n') - break; - } - - len = gs->len; - *line = g_string_free(gs, FALSE); - return len; -} -#endif - - -GList * -gnc_csv_parse (FILE *handle) -{ - GList *csvlists = NULL; - ssize_t bytes_read; - char *line; - - while (bytes_read = gnc_getline (&line, handle) > 0) { - csvlists = g_list_prepend(csvlists, split_csv_line(line)); - g_free(line); - } - - return g_list_reverse(csvlists); -} - - - -#ifdef TEST_CSV - -static void print_glist_rec (GList *list) { - -} -// print_glist_rec(list); - -static void print_glist(GList *list, gpointer dummy) { - - printf("%d: (", g_list_length(list)); - - while (list != NULL) { - if ((list->data != NULL) && ( *((char *) list->data) != 0)) { - printf( "%s", list->data) ; - } - else { - printf( "\"\"" ); - } - list = list->next; - if (list) { - printf(" "); - } - } - - printf(")\n"); -} - -int main (int argc, char **argv) { - - FILE *fp; - int result; - GList *parsed_csv; - GList *current; - int dummy = 1; - - if (argc < 2) { - printf("usage:\n\tcsv2glist fname.csv\n"); - } - - fp = g_fopen (argv[1], "r"); - if (fp == NULL) return 1; - - parsed_csv = gnc_csv_parse(fp); - - g_list_foreach(parsed_csv, (GFunc)print_glist, &dummy); - -} - -#endif diff --git a/src/import-export/csv/gnc-csv2glist.h b/src/import-export/csv/gnc-csv2glist.h deleted file mode 100644 index 879928c808..0000000000 --- a/src/import-export/csv/gnc-csv2glist.h +++ /dev/null @@ -1,39 +0,0 @@ -/* - * gnc-csv2glist.h -- Parse Comma Separated Variable files - * - * Created by: Derek Atkins - * Copyright (c) 2004 Derek Atkins - * - * 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 2 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, contact: - * - * Free Software Foundation Voice: +1-617-542-5942 - * 51 Franklin Street, Fifth Floor Fax: +1-617-542-2652 - * Boston, MA 02110-1301, USA gnu@gnu.org - */ - -#ifndef GNC_IMPORT_CSV_2GLIST_H -#define GNC_IMPORT_CSV_2GLIST_H - -/** - * gnc_csv_parse -- parse a Comma Separated Variable FILE into - * a list of lists of values. - * - * Args: file : the input file to read - * Returns : a list of lists of strings. Both lists and all strings - * must be g_free()'d by the caller. - */ - -GList * gnc_csv_parse (FILE *file); - -#endif diff --git a/src/import-export/csv/gnc-plugin-csv-ui.xml b/src/import-export/csv/gnc-plugin-csv-ui.xml new file mode 100644 index 0000000000..b5c906f483 --- /dev/null +++ b/src/import-export/csv/gnc-plugin-csv-ui.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/src/import-export/csv/gnc-plugin-csv.c b/src/import-export/csv/gnc-plugin-csv.c new file mode 100644 index 0000000000..58af7ce271 --- /dev/null +++ b/src/import-export/csv/gnc-plugin-csv.c @@ -0,0 +1,160 @@ +/* + * gnc-plugin-csv.c -- + * Copyright (C) 2003 David Hampton + * + * 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 2 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, contact: + * + * Free Software Foundation Voice: +1-617-542-5942 + * 51 Franklin Street, Fifth Floor Fax: +1-617-542-2652 + * Boston, MA 02110-1301, USA gnu@gnu.org + */ + +#include "config.h" + +#include +#include + +#include "gnc-plugin-csv.h" +#include "gnc-plugin-manager.h" + +#include "gnc-csv-import.h" + +static void gnc_plugin_csv_class_init (GncPluginCsvClass *klass); +static void gnc_plugin_csv_init (GncPluginCsv *plugin); +static void gnc_plugin_csv_finalize (GObject *object); + +/* Command callbacks */ +static void gnc_plugin_csv_cmd_import (GtkAction *action, GncMainWindowActionData *data); + + +#define PLUGIN_ACTIONS_NAME "gnc-plugin-csv-actions" +#define PLUGIN_UI_FILENAME "gnc-plugin-csv-ui.xml" + +static GtkActionEntry gnc_plugin_actions [] = { + { "CsvImportAction", GTK_STOCK_CONVERT, N_("Import _CSV/Fixed-Width..."), NULL, + N_(" a CSV/Fixed-Width file"), + G_CALLBACK (gnc_plugin_csv_cmd_import) }, +}; +static guint gnc_plugin_n_actions = G_N_ELEMENTS (gnc_plugin_actions); + +typedef struct GncPluginCsvPrivate +{ + gpointer dummy; +} GncPluginCsvPrivate; + +#define GNC_PLUGIN_CSV_GET_PRIVATE(o) \ + (G_TYPE_INSTANCE_GET_PRIVATE ((o), GNC_TYPE_PLUGIN_CSV, GncPluginCsvPrivate)) + +static GObjectClass *parent_class = NULL; + +GType +gnc_plugin_csv_get_type (void) +{ + static GType gnc_plugin_csv_type = 0; + + if (gnc_plugin_csv_type == 0) { + static const GTypeInfo our_info = { + sizeof (GncPluginCsvClass), + NULL, /* base_init */ + NULL, /* base_finalize */ + (GClassInitFunc) gnc_plugin_csv_class_init, + NULL, /* class_finalize */ + NULL, /* class_data */ + sizeof (GncPluginCsv), + 0, /* n_preallocs */ + (GInstanceInitFunc) gnc_plugin_csv_init, + }; + + gnc_plugin_csv_type = g_type_register_static (GNC_TYPE_PLUGIN, + "GncPluginCsv", + &our_info, 0); + } + + return gnc_plugin_csv_type; +} + +GncPlugin * +gnc_plugin_csv_new (void) +{ + return GNC_PLUGIN (g_object_new (GNC_TYPE_PLUGIN_CSV, NULL)); +} + +static void +gnc_plugin_csv_class_init (GncPluginCsvClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GncPluginClass *plugin_class = GNC_PLUGIN_CLASS (klass); + + parent_class = g_type_class_peek_parent (klass); + + object_class->finalize = gnc_plugin_csv_finalize; + + /* plugin info */ + plugin_class->plugin_name = GNC_PLUGIN_CSV_NAME; + + /* widget addition/removal */ + plugin_class->actions_name = PLUGIN_ACTIONS_NAME; + plugin_class->actions = gnc_plugin_actions; + plugin_class->n_actions = gnc_plugin_n_actions; + plugin_class->ui_filename = PLUGIN_UI_FILENAME; + + g_type_class_add_private(klass, sizeof(GncPluginCsvPrivate)); +} + +static void +gnc_plugin_csv_init (GncPluginCsv *plugin) +{ +} + +static void +gnc_plugin_csv_finalize (GObject *object) +{ + GncPluginCsv *plugin; + GncPluginCsvPrivate *priv; + + g_return_if_fail (GNC_IS_PLUGIN_CSV (object)); + + plugin = GNC_PLUGIN_CSV (object); + priv = GNC_PLUGIN_CSV_GET_PRIVATE(plugin); + + G_OBJECT_CLASS (parent_class)->finalize (object); +} + +/************************************************************ + * Plugin Function Implementation * + ************************************************************/ + +/************************************************************ + * Command Callbacks * + ************************************************************/ + +static void +gnc_plugin_csv_cmd_import (GtkAction *action, + GncMainWindowActionData *data) +{ + gnc_file_csv_import(); +} + + +/************************************************************ + * Plugin Bootstrapping * + ************************************************************/ + +void +gnc_plugin_csv_create_plugin (void) +{ + GncPlugin *plugin = gnc_plugin_csv_new (); + + gnc_plugin_manager_add_plugin (gnc_plugin_manager_get (), plugin); +} diff --git a/src/import-export/csv/gnc-plugin-csv.h b/src/import-export/csv/gnc-plugin-csv.h new file mode 100644 index 0000000000..f89edc0767 --- /dev/null +++ b/src/import-export/csv/gnc-plugin-csv.h @@ -0,0 +1,60 @@ +/* + * gnc-plugin-csv.h -- + * Copyright (C) 2003 David Hampton + * + * 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 2 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, contact: + * + * Free Software Foundation Voice: +1-617-542-5942 + * 51 Franklin Street, Fifth Floor Fax: +1-617-542-2652 + * Boston, MA 02110-1301, USA gnu@gnu.org + */ + +#ifndef __GNC_PLUGIN_CSV_H +#define __GNC_PLUGIN_CSV_H + +#include + +#include "gnc-plugin.h" + +G_BEGIN_DECLS + +/* type macros */ +#define GNC_TYPE_PLUGIN_CSV (gnc_plugin_csv_get_type ()) +#define GNC_PLUGIN_CSV(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), GNC_TYPE_PLUGIN_CSV, GncPluginCsv)) +#define GNC_PLUGIN_CSV_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), GNC_TYPE_PLUGIN_CSV, GncPluginCsvClass)) +#define GNC_IS_PLUGIN_CSV(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), GNC_TYPE_PLUGIN_CSV)) +#define GNC_IS_PLUGIN_CSV_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), GNC_TYPE_PLUGIN_CSV)) +#define GNC_PLUGIN_CSV_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), GNC_TYPE_PLUGIN_CSV, GncPluginCsvClass)) + +#define GNC_PLUGIN_CSV_NAME "gnc-plugin-csv" + +/* typedefs & structures */ +typedef struct { + GncPlugin gnc_plugin; +} GncPluginCsv; + +typedef struct { + GncPluginClass gnc_plugin; +} GncPluginCsvClass; + +/* function prototypes */ +GType gnc_plugin_csv_get_type (void); + +GncPlugin *gnc_plugin_csv_new (void); + +void gnc_plugin_csv_create_plugin (void); + +G_END_DECLS + +#endif /* __GNC_PLUGIN_CSV_H */ diff --git a/src/import-export/csv/gncmod-csv-import.c b/src/import-export/csv/gncmod-csv-import.c new file mode 100644 index 0000000000..da1fad9335 --- /dev/null +++ b/src/import-export/csv/gncmod-csv-import.c @@ -0,0 +1,91 @@ +/********************************************************************\ + * 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 2 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, contact: * + * * + * Free Software Foundation Voice: +1-617-542-5942 * + * 51 Franklin Street, Fifth Floor Fax: +1-617-542-2652 * + * Boston, MA 02110-1301, USA gnu@gnu.org * +\********************************************************************/ +/** @addtogroup Import_Export + @{ */ + /**@internal + @file gncmod-csv-import.c + @brief module definition/initialization for the csv importer + @author Copyright (c) 2002 Benoit Grégoire bock@step.polymtl.ca + */ +#include "config.h" + +#include + +#include "gnc-module.h" +#include "gnc-module-api.h" +#include "gnc-plugin-csv.h" + +GNC_MODULE_API_DECL(libgncmod_csv) + +/* version of the gnc module system interface we require */ +int libgncmod_csv_gnc_module_system_interface = 0; + +/* module versioning uses libtool semantics. */ +int libgncmod_csv_gnc_module_current = 0; +int libgncmod_csv_gnc_module_revision = 0; +int libgncmod_csv_gnc_module_age = 0; + +//static GNCModule bus_core; +//static GNCModule file; + + +char * +libgncmod_csv_gnc_module_path(void) +{ + return g_strdup("gnucash/import-export/csv"); +} + +char * +libgncmod_csv_gnc_module_description(void) +{ + return g_strdup("Gnome GUI and C code for CSV importer using libcsv"); +} + +int +libgncmod_csv_gnc_module_init(int refcount) +{ + if(!gnc_module_load("gnucash/engine", 0)) + { + return FALSE; + } + if(!gnc_module_load("gnucash/app-utils", 0)) + { + return FALSE; + } + if(!gnc_module_load("gnucash/gnome-utils", 0)) + { + return FALSE; + } + if(!gnc_module_load("gnucash/import-export", 0)) + { + return FALSE; + } + + /* Add menu items with C callbacks */ + gnc_plugin_csv_create_plugin(); + + return TRUE; +} + +int +libgncmod_csv_gnc_module_end(int refcount) +{ + return TRUE; +} +/** @}*/