diff --git a/.gitmodules b/.gitmodules index e69de29..f24da7e 100644 --- a/.gitmodules +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "libgd"] + path = libgd + url = git://git.gnome.org/libgd.git diff --git a/Makefile.am b/Makefile.am index 3583957..b9abb11 100644 --- a/Makefile.am +++ b/Makefile.am @@ -1,5 +1,5 @@ -ACLOCAL_AMFLAGS = -I m4 -SUBDIRS = src po data help +ACLOCAL_AMFLAGS = -I m4 -I libgd ${ACLOCAL_FLAGS} +SUBDIRS = libgd src po data help EXTRA_DIST = config.rpath ChangeLog diff --git a/autogen.sh b/autogen.sh index d88fb6f..80da03b 100755 --- a/autogen.sh +++ b/autogen.sh @@ -4,6 +4,8 @@ srcdir=`dirname $0` test -z "$srcdir" && srcdir=. +ACLOCAL_FLAGS="-I libgd $ACLOCAL_FLAGS" + PKG_NAME="astrognome" (test -f $srcdir/configure.ac \ @@ -18,5 +20,7 @@ which gnome-autogen.sh || { exit 1 } +git submodule update --init --recursive + REQUIRED_AUTOMAKE_VERSION=1.9 . gnome-autogen.sh diff --git a/configure.ac b/configure.ac index c7eb783..5607950 100644 --- a/configure.ac +++ b/configure.ac @@ -35,10 +35,16 @@ PKG_CHECK_MODULES([GTK], [gtk+-3.0 >= 3.8]) PKG_CHECK_MODULES([LIBXML], [libxml-2.0]) PKG_CHECK_MODULES([LIBXSLT], [libexslt]) PKG_CHECK_MODULES([WEBKIT], [webkit2gtk-3.0]) +PKG_CHECK_MODULES([GDA], [libgda-5.0 libgda-sqlite-5.0]) PKG_CHECK_MODULES([SWE_GLIB], [swe-glib >= 2.1.0]) +LIBGD_INIT([ + main-view +]) + AC_CONFIG_FILES([ Makefile + libgd/Makefile src/Makefile help/Makefile po/Makefile.in diff --git a/libgd b/libgd new file mode 160000 index 0000000..62f9b8b --- /dev/null +++ b/libgd @@ -0,0 +1 @@ +Subproject commit 62f9b8b92599b38d986bd26d5780edd400d318c9 diff --git a/src/Makefile.am b/src/Makefile.am index f1e551c..61e5bb9 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -18,6 +18,7 @@ astrognome_source_files = \ ag-chart.c \ ag-settings.c \ ag-preferences.c \ + ag-db.c \ astrognome.c \ $(NULL) @@ -26,11 +27,11 @@ EXTRA_DIST = \ ag.gresource.xml \ $(NULL) -AM_CPPFLAGS = -DG_LOG_DOMAIN=\"Astrognome\" -DLOCALEDIR=\"$(localedir)\" -DPKGDATADIR=\"$(pkgdatadir)\" +AM_CPPFLAGS = -DG_LOG_DOMAIN=\"Astrognome\" -DLOCALEDIR=\"$(localedir)\" -DPKGDATADIR=\"$(pkgdatadir)\" -I$(top_srcdir)/libgd bin_PROGRAMS = astrognome astrognome_SOURCES = $(astrognome_source_files) $(BUILT_SOURCES) -astrognome_LDADD = $(SWE_GLIB_LIBS) $(GTK_LIBS) $(LIBXML_LIBS) $(LIBXSLT_LIBS) $(WEBKIT_LIBS) +astrognome_LDADD = $(SWE_GLIB_LIBS) $(GTK_LIBS) $(LIBXML_LIBS) $(LIBXSLT_LIBS) $(WEBKIT_LIBS) $(GDA_LIBS) $(top_builddir)/libgd/libgd.la astrognome_LDFLAGS = -rdynamic -astrognome_CFLAGS = $(SWE_GLIB_CFLAGS) $(CFLAGS) $(GTK_CFLAGS) $(LIBXML_CFLAGS) $(LIBXSLT_CFLAGS) $(WEBKIT_CFLAGS) -Wall +astrognome_CFLAGS = $(SWE_GLIB_CFLAGS) $(CFLAGS) $(GTK_CFLAGS) $(LIBXML_CFLAGS) $(LIBXSLT_CFLAGS) $(WEBKIT_CFLAGS) $(GDA_CFLAGS) -Wall diff --git a/src/ag-app.c b/src/ag-app.c index 68ad0dc..fbffba9 100644 --- a/src/ag-app.c +++ b/src/ag-app.c @@ -68,7 +68,10 @@ ag_app_create_window(AgApp *app) static void new_window_cb(GSimpleAction *action, GVariant *parameter, gpointer user_data) { - ag_app_create_window(AG_APP(user_data)); + AgWindow *window = AG_WINDOW(ag_app_create_window(AG_APP(user_data))); + + ag_window_load_chart_list(window); + ag_window_change_tab(window, "list"); } static void @@ -130,12 +133,11 @@ quit_cb(GSimpleAction *action, GVariant *parameter, gpointer user_data) } static void -ag_app_open_chart(AgApp *app, GFile *file) +ag_app_import_chart(AgApp *app, GFile *file) { GtkWidget *window; AgChart *chart; GError *err = NULL; - gchar *uri; if ((chart = ag_chart_load_from_file(file, &err)) == NULL) { g_print("Error: '%s'\n", err->message); @@ -146,14 +148,12 @@ ag_app_open_chart(AgApp *app, GFile *file) window = ag_app_create_window(app); ag_window_set_chart(AG_WINDOW(window), chart); ag_window_update_from_chart(AG_WINDOW(window)); - uri = g_file_get_uri(file); - ag_window_set_uri(AG_WINDOW(window), uri); - g_free(uri); + g_action_group_activate_action(G_ACTION_GROUP(window), "save", NULL); ag_window_change_tab(AG_WINDOW(window), "chart"); } static void -open_cb(GSimpleAction *action, GVariant *parameter, gpointer user_data) +ag_app_import_cb(GSimpleAction *action, GVariant *parameter, gpointer user_data) { gint response; GtkWidget *fs; @@ -163,7 +163,7 @@ open_cb(GSimpleAction *action, GVariant *parameter, gpointer user_data) NULL, GTK_FILE_CHOOSER_ACTION_OPEN, _("_Cancel"), GTK_RESPONSE_CANCEL, - _("_Open"), GTK_RESPONSE_ACCEPT, + _("_Import"), GTK_RESPONSE_ACCEPT, NULL); gtk_file_chooser_add_filter(GTK_FILE_CHOOSER(fs), filter_all); gtk_file_chooser_add_filter(GTK_FILE_CHOOSER(fs), filter_chart); @@ -190,7 +190,7 @@ open_cb(GSimpleAction *action, GVariant *parameter, gpointer user_data) } file = g_file_new_for_commandline_arg(data); - ag_app_open_chart(AG_APP(user_data), file); + ag_app_import_chart(AG_APP(user_data), file); } } @@ -245,13 +245,13 @@ help_cb(GSimpleAction *action, GVariant *parameter, gpointer user_data) } static GActionEntry app_entries[] = { - { "new-window", new_window_cb, NULL, NULL, NULL }, - { "preferences", preferences_cb, NULL, NULL, NULL }, - { "about", about_cb, NULL, NULL, NULL }, - { "quit", quit_cb, NULL, NULL, NULL }, - { "raise", raise_cb, NULL, NULL, NULL }, - { "open", open_cb, NULL, NULL, NULL }, - { "help", help_cb, NULL, NULL, NULL }, + { "new-window", new_window_cb, NULL, NULL, NULL }, + { "preferences", preferences_cb, NULL, NULL, NULL }, + { "about", about_cb, NULL, NULL, NULL }, + { "quit", quit_cb, NULL, NULL, NULL }, + { "raise", raise_cb, NULL, NULL, NULL }, + { "import", ag_app_import_cb, NULL, NULL, NULL }, + { "help", help_cb, NULL, NULL, NULL }, }; static void @@ -348,12 +348,15 @@ startup(GApplication *gapp) } static void -ag_app_open(GApplication *gapp, GFile **files, gint n_files, const gchar *hint) +ag_app_import(GApplication *gapp, + GFile **files, + gint n_files, + const gchar *hint) { gint i; for (i = 0; i < n_files; i++) { - ag_app_open_chart(AG_APP(gapp), files[i]); + ag_app_import_chart(AG_APP(gapp), files[i]); } } @@ -441,7 +444,49 @@ ag_app_class_init(AgAppClass *klass) GApplicationClass *application_class = G_APPLICATION_CLASS(klass); application_class->startup = startup; - application_class->open = ag_app_open; + application_class->open = ag_app_import; +} + +gint +ag_app_buttoned_dialog(GtkWidget *window, + GtkMessageType message_type, + const gchar *message, + const gchar *first_button_text, + ...) +{ + va_list ap; + const gchar *button_text; + gint response_id; + GtkWidget *dialog; + + dialog = gtk_message_dialog_new( + GTK_WINDOW(window), + GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT, + message_type, + GTK_BUTTONS_NONE, + "%s", + message + ); + + if (first_button_text) { + button_text = first_button_text; + + va_start(ap, first_button_text); + response_id = va_arg(ap, gint); + gtk_dialog_add_button(GTK_DIALOG(dialog), button_text, response_id); + + while ((button_text = va_arg(ap, gchar *)) != NULL) { + response_id = va_arg(ap, gint); + gtk_dialog_add_button(GTK_DIALOG(dialog), button_text, response_id); + } + + va_end(ap); + } + + response_id = gtk_dialog_run(GTK_DIALOG(dialog)); + gtk_widget_destroy(dialog); + + return response_id; } void @@ -451,21 +496,18 @@ ag_app_message_dialog(GtkWidget *window, { gchar *msg; va_list args; - GtkWidget *dialog; va_start(args, fmt); msg = g_strdup_vprintf(fmt, args); va_end(args); - dialog = gtk_message_dialog_new( - GTK_WINDOW(window), - GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT, + ag_app_buttoned_dialog( + window, message_type, - GTK_BUTTONS_OK, - "%s", - msg + msg, + _("Close"), GTK_RESPONSE_CLOSE, + NULL ); + g_free(msg); - gtk_dialog_run(GTK_DIALOG(dialog)); - gtk_widget_destroy(dialog); } diff --git a/src/ag-app.h b/src/ag-app.h index 89e393f..6b55258 100644 --- a/src/ag-app.h +++ b/src/ag-app.h @@ -47,6 +47,11 @@ void ag_app_run_action(AgApp *app, gboolean is_remote, const AstrognomeOptions *options); +gint ag_app_buttoned_dialog(GtkWidget *window, + GtkMessageType message_type, + const gchar *message, + const gchar *first_button_text, ...); + void ag_app_message_dialog(GtkWidget *window, GtkMessageType message_type, gchar *fmt, ...); diff --git a/src/ag-chart.c b/src/ag-chart.c index 8950693..1ff2a41 100644 --- a/src/ag-chart.c +++ b/src/ag-chart.c @@ -10,6 +10,7 @@ #include #include +#include "ag-db.h" #include "ag-chart.h" typedef struct _AgChartPrivate { @@ -921,6 +922,70 @@ ag_chart_load_from_file(GFile *file, GError **err) return chart; } +AgChart * +ag_chart_new_from_db_save(AgDbSave *save_data, GError **err) +{ + GsweTimestamp *timestamp; + gchar *house_system_enum_name; + GTypeClass *house_system_class; + GEnumValue *enum_value; + GsweHouseSystem house_system; + AgChart *chart; + + if (save_data == NULL) { + g_set_error( + err, + AG_CHART_ERROR, AG_CHART_ERROR_EMPTY_RECORD, + "Invalid chart" + ); + + return NULL; + } + + house_system_enum_name = g_utf8_strdown(save_data->house_system, -1); + house_system_class = g_type_class_ref(GSWE_TYPE_HOUSE_SYSTEM); + + if ((enum_value = g_enum_get_value_by_nick( + G_ENUM_CLASS(house_system_class), + house_system_enum_name + )) == NULL) { + g_free(house_system_enum_name); + g_set_error( + err, + AG_CHART_ERROR, AG_CHART_ERROR_INVALID_HOUSE_SYSTEM, + "Invalid house system: '%s'", + save_data->house_system + ); + + return NULL; + } + + g_free(house_system_enum_name); + + house_system = enum_value->value; + + timestamp = gswe_timestamp_new_from_gregorian_full( + save_data->year, save_data->month, save_data->day, + save_data->hour, save_data->minute, save_data->second, 0, + save_data->timezone + ); + + chart = ag_chart_new_full( + timestamp, + save_data->longitude, + save_data->latitude, + save_data->altitude, + house_system + ); + + ag_chart_set_name(chart, save_data->name); + ag_chart_set_country(chart, save_data->country); + ag_chart_set_city(chart, save_data->city); + ag_chart_set_note(chart, save_data->note); + + return chart; +} + static xmlDocPtr create_save_doc(AgChart *chart) { @@ -1475,3 +1540,56 @@ const gchar *ag_chart_get_note(AgChart *chart) return priv->note; } +AgDbSave * +ag_chart_get_db_save(AgChart *chart, gint db_id) +{ + GsweCoordinates *coords; + AgChartPrivate *priv = ag_chart_get_instance_private(chart); + AgDbSave *save_data = g_new0(AgDbSave, 1); + GsweTimestamp *timestamp = gswe_moment_get_timestamp(GSWE_MOMENT(chart)); + GEnumClass *house_system_class; + GEnumValue *house_system_enum; + + save_data->db_id = db_id; + + save_data->name = g_strdup(priv->name); + save_data->country = g_strdup(priv->country); + save_data->city = g_strdup(priv->city); + coords = gswe_moment_get_coordinates(GSWE_MOMENT(chart)); + save_data->longitude = coords->longitude; + save_data->latitude = coords->latitude; + save_data->altitude = coords->altitude; + g_free(coords); + save_data->year = gswe_timestamp_get_gregorian_year( + timestamp, + NULL + ); + save_data->month = gswe_timestamp_get_gregorian_month( + timestamp, + NULL + ); + save_data->day = gswe_timestamp_get_gregorian_day(timestamp, NULL); + save_data->hour = gswe_timestamp_get_gregorian_hour( + timestamp, + NULL + ); + save_data->minute = gswe_timestamp_get_gregorian_minute( + timestamp, + NULL + ); + save_data->second = gswe_timestamp_get_gregorian_second( + timestamp, + NULL + ); + save_data->timezone = gswe_timestamp_get_gregorian_timezone(timestamp); + house_system_class = g_type_class_ref(GSWE_TYPE_HOUSE_SYSTEM); + house_system_enum = g_enum_get_value( + house_system_class, + gswe_moment_get_house_system(GSWE_MOMENT(chart)) + ); + save_data->house_system = g_strdup(house_system_enum->value_nick); + g_type_class_unref(house_system_class); + save_data->note = g_strdup(priv->note); + + return save_data; +} diff --git a/src/ag-chart.h b/src/ag-chart.h index e9f8177..735f546 100644 --- a/src/ag-chart.h +++ b/src/ag-chart.h @@ -2,13 +2,18 @@ #define __AG_CHART_H__ #include +#include #include +#include "ag-db.h" + G_BEGIN_DECLS typedef enum { AG_CHART_ERROR_LIBXML, AG_CHART_ERROR_CORRUPT_FILE, + AG_CHART_ERROR_EMPTY_RECORD, + AG_CHART_ERROR_INVALID_HOUSE_SYSTEM, } AgChartError; #define AG_TYPE_CHART (ag_chart_get_type()) @@ -46,6 +51,8 @@ AgChart *ag_chart_new_full(GsweTimestamp *timestamp, AgChart *ag_chart_load_from_file(GFile *file, GError **err); +AgChart *ag_chart_new_from_db_save(AgDbSave *save_data, GError **err); + void ag_chart_save_to_file(AgChart *chart, GFile *file, GError **err); @@ -79,6 +86,8 @@ void ag_chart_set_note(AgChart *chart, const gchar *note); const gchar *ag_chart_get_note(AgChart *chart); +AgDbSave *ag_chart_get_db_save(AgChart *chart, gint db_id); + #define AG_CHART_ERROR (ag_chart_error_quark()) GQuark ag_chart_error_quark(void); diff --git a/src/ag-db.c b/src/ag-db.c new file mode 100644 index 0000000..2a77b80 --- /dev/null +++ b/src/ag-db.c @@ -0,0 +1,1134 @@ +#include +#include +#include +#include +#include +#include + +#include "config.h" +#include "ag-app.h" +#include "ag-db.h" + +#define SCHEMA_VERSION 1 + +static AgDb *singleton = NULL; + +typedef struct _AgDbPrivate { + gchar *dsn; + GdaConnection *conn; +} AgDbPrivate; + +G_DEFINE_QUARK(ag_db_error_quark, ag_db_error); + +G_DEFINE_TYPE_WITH_PRIVATE(AgDb, ag_db, G_TYPE_OBJECT); + +typedef enum { + COLUMN_ID, + COLUMN_NAME, + COLUMN_COUNTRY, + COLUMN_CITY, + COLUMN_LONGITUDE, + COLUMN_LATITUDE, + COLUMN_ALTITUDE, + COLUMN_YEAR, + COLUMN_MONTH, + COLUMN_DAY, + COLUMN_HOUR, + COLUMN_MINUTE, + COLUMN_SECOND, + COLUMN_TIMEZONE, + COLUMN_HOUSE_SYSTEM, + COLUMN_NOTE, + + /* Leave this as the last element */ + COLUMN_COUNT +} ChartTableColumn; + +typedef struct { + ChartTableColumn id; + gchar *name; +} ChartTableColumnDef; + +static ChartTableColumnDef chart_table_column[] = { + { COLUMN_ID, "id" }, + { COLUMN_NAME, "name" }, + { COLUMN_COUNTRY, "country_name" }, + { COLUMN_CITY, "city_name" }, + { COLUMN_LONGITUDE, "longitude" }, + { COLUMN_LATITUDE, "latitude" }, + { COLUMN_ALTITUDE, "altitude" }, + { COLUMN_YEAR, "year" }, + { COLUMN_MONTH, "month" }, + { COLUMN_DAY, "day" }, + { COLUMN_HOUR, "hour" }, + { COLUMN_MINUTE, "minute" }, + { COLUMN_SECOND, "second" }, + { COLUMN_TIMEZONE, "timezone" }, + { COLUMN_HOUSE_SYSTEM, "house_system" }, + { COLUMN_NOTE, "note" }, +}; + +/** + * ag_db_non_select: + * @db: the AgDb to operate on + * @sql: the SQL query to execute + * + * Executes a non-SELECT query on @db. No result is returned right now (TODO) + */ +static void +ag_db_non_select(AgDb *db, const gchar *sql) +{ + GdaStatement *sth; + gint nrows; + const gchar *remain; + GdaSqlParser *parser; + AgDbPrivate *priv = ag_db_get_instance_private(db); + GError *err = NULL; + + parser = g_object_get_data(G_OBJECT(priv->conn), "parser"); + g_assert(GDA_IS_SQL_PARSER(parser)); + + if ((sth = gda_sql_parser_parse_string( + parser, + sql, + &remain, + &err + )) == NULL) { + g_error( + "SQL error: %s", + (err && err->message) + ? err->message + : "no reason" + ); + } + + if ((nrows = gda_connection_statement_execute_non_select( + priv->conn, + sth, + NULL, + NULL, + &err + )) == -1) { + g_error( + "SQL error: %s", + (err && err->message) + ? err->message + : "no details" + ); + } + + g_object_unref(sth); +} + +/** + * ag_db_table_exists: + * @db: the #AgDb object to operate on + * @table: the nawe of the table to check for + * + * Checks if the specified table exists. It is done by querying the + * sqlite_master system table. + * + * Returns: TRUE if the table exists, FALSE otherwise + */ +static gboolean +ag_db_table_exists(AgDb *db, const gchar *table) +{ + GdaSqlParser *parser; + GdaStatement *sth; + GdaDataModel *result; + GdaSet *params; + GdaHolder *holder; + const gchar *remain; + gboolean ret; + GValue table_value = G_VALUE_INIT; + AgDbPrivate *priv = ag_db_get_instance_private(db); + GError *err = NULL; + + parser = g_object_get_data(G_OBJECT(priv->conn), "parser"); + + if ((sth = gda_sql_parser_parse_string( + parser, + "SELECT type \n" \ + " FROM sqlite_master \n" \ + " WHERE type = 'table' \n" \ + " AND name = ##name::string", + &remain, + &err + )) == NULL) { + g_error( + "SQL error: %s", + (err && err->message) + ? err->message + : "no reason" + ); + } + + if (gda_statement_get_parameters(sth, ¶ms, &err) == FALSE) { + g_error( + "Params error: %s", + (err && err->message) + ? err->message + : "no reason" + ); + } + + holder = gda_set_get_holder(params, "name"); + g_value_init(&table_value, G_TYPE_STRING); + g_value_set_string(&table_value, table); + gda_holder_set_value(holder, &table_value, &err); + g_value_unset(&table_value); + + result = gda_connection_statement_execute_select( + priv->conn, + sth, + params, + &err + ); + + if (gda_data_model_get_n_rows(result) > 0) { + ret = TRUE; + } else { + ret = FALSE; + } + + g_object_unref(result); + g_object_unref(sth); + + return ret; +} + +/** + * ag_db_select: + * @db: the database object to work on + * @err: a #GError or NULL + * @sql: the query to execute + * @...: a NULL terminated list of key-value pairs of the query parameters + * + * Returns: (transfer full): the #GdaDataModel as the result of the query + */ +static GdaDataModel * +ag_db_select(AgDb *db, GError **err, const gchar *sql, ...) +{ + GdaSqlParser *parser; + const gchar *remain; + GdaSet *params; + GdaStatement *sth; + GdaDataModel *ret; + gchar *error = NULL; + GError *local_err = NULL; + AgDbPrivate *priv = ag_db_get_instance_private(db); + + parser = g_object_get_data(G_OBJECT(priv->conn), "parser"); + + if ((sth = gda_sql_parser_parse_string( + parser, + sql, + &remain, + &local_err + )) == NULL) { + g_error( + "SQL error: %s", + (local_err && local_err->message) + ? local_err->message + : "no reason" + ); + } + + if (!gda_statement_get_parameters(sth, ¶ms, &local_err)) { + g_error( + "SQL error: %s", + (local_err && local_err->message) + ? local_err->message + : "no reason" + ); + } + + if (params) { + va_list ap; + + va_start(ap, sql); + + while (TRUE) { + gchar *key; + GdaHolder *holder; + GType type; + GValue value = G_VALUE_INIT; + + if ((key = va_arg(ap, gchar *)) == NULL) { + break; + } + + if ((holder = gda_set_get_holder( + params, + (const gchar *)key + )) == NULL) { + g_error("Error: holder %s is not defined in query.", key); + } + + type = gda_holder_get_g_type(holder); + g_value_init(&value, type); + G_VALUE_COLLECT_INIT(&value, type, ap, 0, &error); + + if (error) { + g_error("SQL GValue error: %s", error); + } + + if (!gda_holder_set_value(holder, (const GValue *)&value, err)) { + g_error( + "SQL GdaHolder error: %s", + (*err && (*err)->message) + ? (*err)->message + : "no reason" + ); + } + } + + va_end(ap); + } + + ret = gda_connection_statement_execute_select(priv->conn, sth, params, err); + g_object_unref(sth); + + return ret; +} + +/** + * ag_db_check_version_table: + * @db: the #AgDb object to operate on + * + * Checks if the version table exists, and creates it if necessary. It doesn't + * check if the structure is valid! + */ +static void +ag_db_check_version_table(AgDb *db) +{ + if (ag_db_table_exists(db, "version")) { + GdaDataModel *result; + gint ret; + + g_debug( + "Version table exists. " \ + "Checking if db_version is %d and app_version is %s", + SCHEMA_VERSION, + PACKAGE_VERSION + ); + result = ag_db_select( + db, + NULL, + "SELECT db_version, app_version FROM version", + NULL + ); + + ret = gda_data_model_get_n_rows(result); + + if (ret < 0) { + g_error("No number of rows?"); + } else if (ret > 1) { + g_error("Error in database. Version table has more than one rows!"); + } else if (ret == 0) { + // TODO: Check schema against current one maybe? If it’s fine, we + // may just add a row here. + g_error("Error in database. Version table has no rows!"); + } else { + // Version table has one row + const GValue *value; + GdaDataModelIter *iter = gda_data_model_create_iter(result); + gint version; + + gda_data_model_iter_move_next(iter); + value = gda_data_model_iter_get_value_at(iter, 0); + + if (!G_VALUE_HOLDS_INT(value)) { + g_error( + "Database is invalid. " \ + "version.db_version should be an integer." + ); + } + + version = g_value_get_int(value); + + if (version < SCHEMA_VERSION) { + // TODO + g_error("Update required!"); + } else if (version > SCHEMA_VERSION) { + const GValue *app_version_value; + const gchar *app_version; + + app_version_value = gda_data_model_iter_get_value_at( + iter, + 1 + ); + app_version = g_value_get_string( + app_version_value + ); + + g_error( + "The version of your database is from the future. " \ + "It seems it was created by Astrognome v%s.", + app_version + ); + } else { + g_object_unref(result); + } + } + } else { + GValue db_version = G_VALUE_INIT, + app_version = G_VALUE_INIT; + AgDbPrivate *priv = ag_db_get_instance_private(db); + + g_value_init(&db_version, G_TYPE_INT); + g_value_init(&app_version, G_TYPE_STRING); + + g_value_set_int(&db_version, SCHEMA_VERSION); + g_value_set_static_string(&app_version, PACKAGE_VERSION); + + ag_db_non_select( + db, + "CREATE TABLE version (" \ + "id INTEGER PRIMARY KEY, " \ + "db_version INTEGER UNIQUE NOT NULL, " \ + "app_version TEXT UNIQUE NOT NULL" \ + ")" + ); + + gda_connection_insert_row_into_table( + priv->conn, + "version", + NULL, + "db_version", &db_version, + "app_version", &app_version, + NULL + ); + } +} + +/** + * ag_db_check_chart_table: + * @db: the #AgDb object to operate on + * + * Checks if the chart table exists, and creates it if necessary. It doesn't + * check if the structure is valid! + */ +static void +ag_db_check_chart_table(AgDb *db) +{ + ag_db_non_select( + db, + "CREATE TABLE IF NOT EXISTS chart (" \ + "id INTEGER PRIMARY KEY, " \ + "name TEXT NOT NULL, " \ + "country_name TEXT, " \ + "city_name TEXT, " \ + "longitude DOUBLE NOT NULL, " \ + "latitude DOUBLE NOT NULL, " \ + "altitude DOUBLE, " \ + "year INTEGER NOT NULL, " \ + "month UNSIGNED INTEGER NOT NULL, " \ + "day UNSIGNED INTEGER NOT NULL, " \ + "hour UNSIGNED INTEGER NOT NULL, " \ + "minute UNSIGNED INTEGER NOT NULL, " \ + "second UNSIGNED INTEGER NOT NULL, " \ + "timezone DOUBLE NOT NULL, " \ + "house_system TEXT NOT NULL, " \ + "note TEXT" \ + ")" + ); +} + +/** + * ag_db_verify: + * @db: the #AgDb object to operate on + * + * Checks if the database file is sane. + * + * Returns: the status of the database (TODO: make this an enum!) + */ +static gint +ag_db_verify(AgDb *db) +{ + ag_db_check_version_table(db); + ag_db_check_chart_table(db); + + return 0; +} + +static void +ag_db_init(AgDb *db) +{ + GdaSqlParser *parser; + GFile *user_data_dir = g_file_new_for_path(g_get_user_data_dir()), + *ag_data_dir = g_file_get_child(user_data_dir, "astrognome"); + AgDbPrivate *priv = ag_db_get_instance_private(db); + gchar *path = g_file_get_path(ag_data_dir); + GError *err = NULL; + + gda_init(); + + if (!g_file_query_exists(ag_data_dir, NULL)) { + gchar *path = g_file_get_path(ag_data_dir); + + if (g_mkdir_with_parents(path, 0700) != 0) { + g_error( + "Data directory %s does not exist and can not be created.", + path + ); + } + } + + priv->dsn = g_strdup_printf("SQLite://DB_DIR=%s;DB_NAME=charts", path); + + g_free(path); + g_object_unref(user_data_dir); + g_object_unref(ag_data_dir); + + priv->conn = gda_connection_open_from_string( + NULL, + priv->dsn, + NULL, + GDA_CONNECTION_OPTIONS_NONE, + &err + ); + + if (priv->conn == NULL) { + g_error( + "Unable to initialize database: %s", + (err && err->message) + ? err->message + : "no reason" + ); + } + + if ((parser = gda_connection_create_parser(priv->conn)) == NULL) { + parser = gda_sql_parser_new(); + } + + g_object_set_data_full( + G_OBJECT(priv->conn), + "parser", + parser, + g_object_unref + ); + + ag_db_verify(db); +} + +static void +ag_db_dispose(GObject *gobject) +{ + AgDbPrivate *priv = ag_db_get_instance_private(AG_DB(gobject)); + + g_object_unref(priv->conn); + g_free(priv->dsn); + G_OBJECT_CLASS(ag_db_parent_class)->dispose(gobject); +} + +static void +ag_db_finalize(GObject *gobject) +{ + singleton = NULL; + + G_OBJECT_CLASS(ag_db_parent_class)->finalize(gobject); +} + +static void +ag_db_class_init(AgDbClass *klass) +{ + GObjectClass *gobject_class = G_OBJECT_CLASS(klass); + + gobject_class->dispose = ag_db_dispose; + gobject_class->finalize = ag_db_finalize; +} + +AgDb * +ag_db_get(void) +{ + if (!singleton) { + singleton = AG_DB(g_object_new(AG_TYPE_DB, NULL)); + } else { + g_object_ref(singleton); + } + + g_assert(singleton); + + return singleton; +} + +/** + * ag_db_save_data_free: + * @save_data: the #AgDbSave struct to free + * + * Frees @save_data and all its fields + */ +void +ag_db_save_data_free(AgDbSave *save_data) +{ + if (!save_data) { + return; + } + + if (save_data->name) { + g_free(save_data->name); + } + + if (save_data->country) { + g_free(save_data->country); + } + + if (save_data->city) { + g_free(save_data->city); + } + + if (save_data->house_system) { + g_free(save_data->house_system); + } + + if (save_data->note) { + g_free(save_data->note); + } + + g_free(save_data); +} + +/** + * ag_db_save_chart: + * @db: the #AgDb object to operate on + * @save_data: the data to save. + * @err: a #GError for storing errors + * + * Saves @save_data to the database. If its db_id field is -1, a new record is + * created. In this case, @save_data is updated and db_id is set to the actual + * data record ID. Otherwise the row with the given ID will be updated. + * + * Returns: TRUE if the save succeeds, FALSE otherwise + */ +gboolean +ag_db_save_chart(AgDb *db, AgDbSave *save_data, GError **err) +{ + GError *local_err = NULL; + gboolean save_success = TRUE; + GValue db_id = G_VALUE_INIT, + name = G_VALUE_INIT, + country = G_VALUE_INIT, + city = G_VALUE_INIT, + longitude = G_VALUE_INIT, + latitude = G_VALUE_INIT, + altitude = G_VALUE_INIT, + year = G_VALUE_INIT, + month = G_VALUE_INIT, + day = G_VALUE_INIT, + hour = G_VALUE_INIT, + minute = G_VALUE_INIT, + second = G_VALUE_INIT, + timezone = G_VALUE_INIT, + house_system = G_VALUE_INIT, + note = G_VALUE_INIT; + AgDbPrivate *priv = ag_db_get_instance_private(db); + + g_value_init(&name, G_TYPE_STRING); + g_value_set_string(&name, save_data->name); + + g_value_init(&country, G_TYPE_STRING); + g_value_set_string(&country, save_data->country); + + g_value_init(&city, G_TYPE_STRING); + g_value_set_string(&city, save_data->city); + + g_value_init(&longitude, G_TYPE_DOUBLE); + g_value_set_double(&longitude, save_data->longitude); + + g_value_init(&latitude, G_TYPE_DOUBLE); + g_value_set_double(&latitude, save_data->latitude); + + g_value_init(&altitude, G_TYPE_DOUBLE); + g_value_set_double(&altitude, save_data->altitude); + + g_value_init(&year, G_TYPE_INT); + g_value_set_int(&year, save_data->year); + + g_value_init(&month, G_TYPE_UINT); + g_value_set_uint(&month, save_data->month); + + g_value_init(&day, G_TYPE_UINT); + g_value_set_uint(&day, save_data->day); + + g_value_init(&hour, G_TYPE_UINT); + g_value_set_uint(&hour, save_data->hour); + + g_value_init(&minute, G_TYPE_UINT); + g_value_set_uint(&minute, save_data->minute); + + g_value_init(&second, G_TYPE_UINT); + g_value_set_uint(&second, save_data->second); + + g_value_init(&timezone, G_TYPE_DOUBLE); + g_value_set_double(&timezone, save_data->timezone); + + g_value_init(&house_system, G_TYPE_STRING); + g_value_set_string(&house_system, save_data->house_system); + + g_value_init(¬e, G_TYPE_STRING); + g_value_set_string(¬e, save_data->note); + + /* It is possible to get 0 here, which is as non-existant as -1 */ + if (save_data->db_id < 0) { + if (!gda_connection_insert_row_into_table( + priv->conn, + "chart", + &local_err, + "name", &name, + "country_name", &country, + "city_name", &city, + "longitude", &longitude, + "latitude", &latitude, + "altitude", &altitude, + "year", &year, + "month", &month, + "day", &day, + "hour", &hour, + "minute", &minute, + "second", &second, + "timezone", &timezone, + "house_system", &house_system, + "note", ¬e, + NULL + )) { + + g_set_error( + err, + AG_DB_ERROR, + AG_DB_ERROR_DATABASE_ERROR, + "%s", + (local_err && local_err->message) + ? local_err->message + : _("Reason unknown") + ); + + save_success = FALSE; + } else { + // Get inserted row's id + GdaDataModel *result = ag_db_select( + db, + &local_err, + "SELECT last_insert_rowid()" + ); + + if (result == NULL) { + if (err) { + *err = g_error_copy(local_err); + } + + // TODO: a more reasonable return value should be given here + save_success = FALSE; + } else { + const GValue *value = gda_data_model_get_value_at( + result, + 0, 0, + NULL + ); + + save_data->db_id = g_value_get_int(value); + } + } + } else { + g_value_init(&db_id, G_TYPE_INT); + g_value_set_int(&db_id, save_data->db_id); + + if (!gda_connection_update_row_in_table( + priv->conn, + "chart", + "id", + &db_id, + &local_err, + "name", &name, + "country_name", &country, + "city_name", &city, + "longitude", &longitude, + "latitude", &latitude, + "altitude", &altitude, + "year", &year, + "month", &month, + "day", &day, + "hour", &hour, + "minute", &minute, + "second", &second, + "timezone", &timezone, + "house_system", &house_system, + "note", ¬e, + NULL + )) { + + g_set_error( + err, + AG_DB_ERROR, + AG_DB_ERROR_DATABASE_ERROR, + "%s", + (local_err && local_err->message) + ? local_err->message + : _("Reason unknown") + ); + + save_success = FALSE; + } + + g_value_unset(&db_id); + } + + g_value_unset(¬e); + g_value_unset(&house_system); + g_value_unset(&timezone); + g_value_unset(&second); + g_value_unset(&minute); + g_value_unset(&hour); + g_value_unset(&day); + g_value_unset(&month); + g_value_unset(&year); + g_value_unset(&altitude); + g_value_unset(&latitude); + g_value_unset(&longitude); + g_value_unset(&city); + g_value_unset(&country); + g_value_unset(&name); + + return save_success; +} + +/** + * ag_db_get_chart_list: + * @db: the #AgDb object to operate on + * @err: a #GError + * + * Creates a list of all charts in the database, ordered by name. As the return + * value may be NULL even if there are no charts or if there was an error, you + * may want to check @err if the return value is NULL. + * + * Please be aware that the #AgDbSave objects of the returned value are not + * fully realised chart records. To get one, you need to call + * ag_db_get_chart_data_by_id() + * + * Returns: (element-type AgDbSave) (transfer full): the list of all charts, or + * or NULL if there are none, or if there is an error. + */ +GList * +ag_db_get_chart_list(AgDb *db, GError **err) +{ + GdaDataModelIter *iter; + GList *ret = NULL; + GdaDataModel *result = ag_db_select( + db, + err, + "SELECT id, name FROM chart ORDER BY name", + NULL + ); + + if (result == NULL) { + return NULL; + } + + iter = gda_data_model_create_iter(result); + + while (gda_data_model_iter_move_next(iter)) { + const GValue *value; + AgDbSave *save_data = g_new0(AgDbSave, 1); + + value = gda_data_model_iter_get_value_at(iter, 0); + save_data->db_id = g_value_get_int(value); + + value = gda_data_model_iter_get_value_at(iter, 1); + save_data->name = g_strdup(g_value_get_string(value)); + + ret = g_list_prepend(ret, save_data); + } + + return g_list_reverse(ret); +} + +/** + * ag_db_get_chart_data_by_id: + * @db: the #AgDb object to operate on + * @row_id: the ID field of the requested chart + * @err: a #GError + * + * Fetches the specified row from the chart table. + * + * Returns: (transfer full): A fully filled #AgDbSave record of the chart + */ +AgDbSave * +ag_db_get_chart_data_by_id(AgDb *db, guint row_id, GError **err) +{ + AgDbSave *save_data; + const GValue *value; + GdaValueAttribute attributes; + gchar *query, + *columns; + guint i; + GdaDataModel *result; + GError *local_err = NULL; + + columns = NULL; + + for (i = 1; i < COLUMN_COUNT; i++) { + gchar *tmp; + + if (i == 1) { + columns = g_strjoin( + ", ", + chart_table_column[0].name, + chart_table_column[1].name, + NULL + ); + } else { + tmp = g_strjoin(", ", columns, chart_table_column[i].name, NULL); + g_free(columns); + columns = tmp; + } + } + + query = g_strdup_printf( + "SELECT %s FROM chart WHERE id = ##id::gint", + columns + ); + g_free(columns); + + result = ag_db_select(db, &local_err, query, "id", row_id, NULL); + g_free(query); + + if (local_err && (local_err->message)) { + return NULL; + } + + if (gda_data_model_get_n_rows(result) < 1) { + g_set_error( + err, + AG_DB_ERROR, AG_DB_ERROR_NO_CHART, + "Chart does not exist" + ); + + return NULL; + } + + save_data = g_new0(AgDbSave, 1); + + /* id */ + value = gda_data_model_get_value_at(result, COLUMN_ID, 0, NULL); + save_data->db_id = g_value_get_int(value); + + /* name */ + value = gda_data_model_get_value_at(result, COLUMN_NAME, 0, NULL); + save_data->name = g_strdup(g_value_get_string(value)); + + /* country */ + value = gda_data_model_get_value_at(result, COLUMN_COUNTRY, 0, NULL); + attributes = gda_data_model_get_attributes_at(result, COLUMN_COUNTRY, 0); + + if (attributes | GDA_VALUE_ATTR_IS_NULL) { + save_data->country = NULL; + } else { + save_data->country = g_strdup(g_value_get_string(value)); + } + + value = gda_data_model_get_value_at(result, COLUMN_CITY, 0, NULL); + attributes = gda_data_model_get_attributes_at(result, COLUMN_CITY, 0); + + if (attributes | GDA_VALUE_ATTR_IS_NULL) { + save_data->city = NULL; + } else { + save_data->city = g_strdup(g_value_get_string(value)); + } + + value = gda_data_model_get_value_at( + result, + COLUMN_LONGITUDE, + 0, + NULL + ); + save_data->longitude = g_value_get_double(value); + + value = gda_data_model_get_value_at( + result, + COLUMN_LATITUDE, + 0, + NULL + ); + save_data->latitude = g_value_get_double(value); + + value = gda_data_model_get_value_at(result, COLUMN_ALTITUDE, 0, NULL); + attributes = gda_data_model_get_attributes_at(result, COLUMN_ALTITUDE, 0); + + if (attributes | GDA_VALUE_ATTR_IS_NULL) { + /* TODO: this value should be macroified */ + save_data->altitude = 280.0; + } else { + save_data->altitude = g_value_get_double(value); + } + + value = gda_data_model_get_value_at(result, COLUMN_YEAR, 0, NULL); + save_data->year = g_value_get_int(value); + + value = gda_data_model_get_value_at( + result, + COLUMN_MONTH, + 0, + NULL + ); + save_data->month = g_value_get_uint(value); + + value = gda_data_model_get_value_at(result, COLUMN_DAY, 0, NULL); + save_data->day = g_value_get_uint(value); + + value = gda_data_model_get_value_at(result, COLUMN_HOUR, 0, NULL); + save_data->hour = g_value_get_uint(value); + + value = gda_data_model_get_value_at( + result, + COLUMN_MINUTE, + 0, + NULL + ); + save_data->minute = g_value_get_uint(value); + + value = gda_data_model_get_value_at( + result, + COLUMN_SECOND, + 0, + NULL + ); + save_data->second = g_value_get_uint(value); + + value = gda_data_model_get_value_at( + result, + COLUMN_TIMEZONE, + 0, + NULL + ); + save_data->timezone = g_value_get_double(value); + + value = gda_data_model_get_value_at( + result, + COLUMN_HOUSE_SYSTEM, + 0, + NULL + ); + save_data->house_system = g_strdup(g_value_get_string(value)); + + value = gda_data_model_get_value_at(result, COLUMN_NOTE, 0, NULL); + attributes = gda_data_model_get_attributes_at(result, 15, 0); + + if (attributes | GDA_VALUE_ATTR_IS_NULL) { + save_data->note = NULL; + } else { + save_data->note = g_strdup(g_value_get_string(value)); + } + + g_object_unref(result); + + return save_data; +} + +/** + * string_collate: + * @str1: the first string + * @str2: the second string + * + * A wrapper function around g_utf8_collate() that can handle NULL values. NULL + * precedes any strings (even ""). + * + * Returns: -1 if str1 is ordered before str2, 1 if str2 comes first, or 0 if + * they are identical + */ +static gint +string_collate(const gchar *str1, const gchar *str2) +{ + if (((str1 == NULL) || (str2 == NULL)) && (str1 != str2)) { + return (str1 == NULL) ? -1 : 1; + } + + if (str1 == str2) { + return 0; + } + + return g_utf8_collate(str1, str2); +} + +/** + * ag_db_save_identical: + * @a: the first #AgDbSave structure + * @b: the second #AgDbSave structure + * + * Compares two #AgDbSave structures and their contents. + * + * Returns: TRUE if the two structs hold equal values (strings are also compared + * with string_collate()), FALSE otherwise + */ +gboolean +ag_db_save_identical(const AgDbSave *a, const AgDbSave *b) +{ + if (a == b) { + return TRUE; + } + + if ((a == NULL) || (b == NULL)) { + return FALSE; + } + + if (string_collate(a->name, b->name) != 0) { + return FALSE; + } + + if (string_collate(a->country, b->country) != 0) { + return FALSE; + } + + if (string_collate(a->city, b->city) != 0) { + return FALSE; + } + + if (a->longitude != b->longitude) { + return FALSE; + } + + if (a->latitude != b->latitude) { + return FALSE; + } + + if (a->altitude != b->altitude) { + return FALSE; + } + + if (a->year != b->year) { + return FALSE; + } + + if (a->month != b->month) { + return FALSE; + } + + if (a->day != b->day) { + return FALSE; + } + + if (a->hour != b->hour) { + return FALSE; + } + + if (a->minute != b->minute) { + return FALSE; + } + + if (a->second != b->second) { + return FALSE; + } + + if (a->timezone != b->timezone) { + return FALSE; + } + + if (string_collate(a->house_system, b->house_system) != 0) { + return FALSE; + } + + if (string_collate(a->note, b->note) != 0) { + return FALSE; + } + + return TRUE; +} diff --git a/src/ag-db.h b/src/ag-db.h new file mode 100644 index 0000000..d22303b --- /dev/null +++ b/src/ag-db.h @@ -0,0 +1,78 @@ +#ifndef __AG_DB_H__ +#define __AG_DB_H__ + +#include +#include + +G_BEGIN_DECLS + +#define AG_TYPE_DB (ag_db_get_type()) +#define AG_DB(o) (G_TYPE_CHECK_INSTANCE_CAST((o), \ + AG_TYPE_DB, \ + AgDb)) +#define AG_DB_CLASS(k) (G_TYPE_CHECK_CLASS_CAST((k), \ + AG_TYPE_DB, \ + AgDbClass)) +#define AG_IS_DB(o) (G_TYPE_CHECK_INSTANCE_TYPE((o), AG_TYPE_DB)) +#define AG_IS_DB_CLASS(k) (G_TYPE_CHECK_CLASS_TYPE((k), AG_TYPE_DB)) +#define AG_DB_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS((o), \ + AG_TYPE_DB, \ + AgDbClass)) + +typedef struct _AgDb AgDb; +typedef struct _AgDbClass AgDbClass; + +struct _AgDb { + GObject parent_instance; +}; + +struct _AgDbClass { + GObjectClass parent_class; +}; + +typedef struct _AgDbSave { + gint db_id; + gchar *name; + gchar *country; + gchar *city; + gdouble longitude; + gdouble latitude; + gdouble altitude; + gint year; + guint month; + guint day; + guint hour; + guint minute; + guint second; + gdouble timezone; + gchar *house_system; + gchar *note; +} AgDbSave; + +typedef enum { + AG_DB_ERROR_NO_CHART, + AG_DB_ERROR_DATABASE_ERROR, +} AgDbError; + +GType ag_db_get_type(void) G_GNUC_CONST; + +AgDb *ag_db_get(void); + +void ag_db_save_data_free(AgDbSave *save_data); + +gboolean ag_db_save_chart(AgDb *db, + AgDbSave *save_data, + GError **err); + +GList *ag_db_get_chart_list(AgDb *db, GError **err); + +AgDbSave *ag_db_get_chart_data_by_id(AgDb *db, guint row_id, GError **err); + +gboolean ag_db_save_identical(const AgDbSave *a, const AgDbSave *b); + +#define AG_DB_ERROR (ag_db_error_quark()) +GQuark ag_db_error_quark(void); + +G_END_DECLS + +#endif /* __AG_DB_H__ */ diff --git a/src/ag-window.c b/src/ag-window.c index fc0c363..970e680 100644 --- a/src/ag-window.c +++ b/src/ag-window.c @@ -4,6 +4,7 @@ #include #include #include +#include #include #include @@ -12,9 +13,12 @@ #include "ag-window.h" #include "ag-chart.h" #include "ag-settings.h" +#include "ag-db.h" struct _AgWindowPrivate { GtkWidget *header_bar; + GtkWidget *menubutton_revealer; + GtkWidget *new_back_stack; GtkWidget *stack; GtkWidget *name; GtkWidget *north_lat; @@ -32,6 +36,7 @@ struct _AgWindowPrivate { GtkWidget *timezone; GtkWidget *house_system; + GtkWidget *tab_list; GtkWidget *tab_chart; GtkWidget *tab_edit; GtkWidget *current_tab; @@ -42,10 +47,11 @@ struct _AgWindowPrivate { AgSettings *settings; AgChart *chart; - gchar *uri; gboolean aspect_table_populated; GtkTextBuffer *note_buffer; GtkListStore *house_system_model; + GtkListStore *db_chart_data; + AgDbSave *saved_data; }; G_DEFINE_QUARK(ag_window_error_quark, ag_window_error); @@ -86,19 +92,101 @@ ag_window_view_menu_action(GSimpleAction *action, g_variant_unref(state); } -static void -ag_window_close_action(GSimpleAction *action, - GVariant *parameter, - gpointer user_data) +gboolean +ag_window_can_close(AgWindow *window, gboolean display_dialog) { - AgWindow *window = user_data; + AgWindowPrivate *priv = ag_window_get_instance_private(window); + gint db_id = (priv->saved_data) + ? priv->saved_data->db_id + : -1; + AgDbSave *save_data = NULL; + AgDb *db = ag_db_get(); + GError *err = NULL; + gboolean ret = TRUE; - // TODO: Save unsaved changes! - gtk_widget_destroy(GTK_WIDGET(window)); + if (priv->chart) { + save_data = ag_chart_get_db_save(priv->chart, db_id); + + if ( + !ag_db_save_identical(priv->saved_data, save_data) + || !(priv->saved_data) + || (priv->saved_data->db_id == -1) + ) { + g_debug("Save is needed!"); + + if (display_dialog) { + gint response; + + response = ag_app_buttoned_dialog( + GTK_WIDGET(window), + GTK_MESSAGE_QUESTION, + _("Chart is not saved. Do you want to save it?"), + _("Save and close"), GTK_RESPONSE_YES, + _("Close without saving"), GTK_RESPONSE_NO, + _("Return to chart"), GTK_RESPONSE_CANCEL, + NULL + ); + + switch (response) { + case GTK_RESPONSE_YES: + if (!ag_db_save_chart(db, save_data, &err)) { + ag_app_message_dialog( + GTK_WIDGET(window), + GTK_MESSAGE_ERROR, + "Unable to save chart: %s", + err->message + ); + + ret = FALSE; + } else { + ret = TRUE; + } + + break; + + case GTK_RESPONSE_NO: + ret = TRUE; + + break; + + default: + ret = FALSE; + + break; + } + } else { + ret = FALSE; + } + } + } + + ag_db_save_data_free(save_data); + + return ret; +} + +gboolean +ag_window_delete_event_callback(AgWindow *window, + GdkEvent *event, + gpointer user_data) +{ + return (!ag_window_can_close(window, TRUE)); } static void -ag_window_save_as(AgWindow *window, GError **err) +ag_window_close_action(GSimpleAction *action, + GVariant *parameter, + gpointer user_data) +{ + AgWindow *window = AG_WINDOW(user_data); + + if (ag_window_can_close(window, TRUE)) { + gtk_widget_destroy(GTK_WIDGET(window)); + } +} + +static void +ag_window_export(AgWindow *window, GError **err) { gchar *name; gchar *file_name; @@ -146,7 +234,7 @@ ag_window_save_as(AgWindow *window, GError **err) file_name = g_strdup_printf("%s.agc", name); g_free(name); - fs = gtk_file_chooser_dialog_new(_("Save Chart"), + fs = gtk_file_chooser_dialog_new(_("Export Chart"), GTK_WINDOW(window), GTK_FILE_CHOOSER_ACTION_SAVE, _("_Cancel"), GTK_RESPONSE_CANCEL, @@ -175,42 +263,43 @@ ag_window_save_action(GSimpleAction *action, GVariant *parameter, gpointer user_data) { - gchar *uri; AgWindow *window = AG_WINDOW(user_data); - GError *err = NULL; AgWindowPrivate *priv = ag_window_get_instance_private(window); + AgDb *db = ag_db_get(); + GError *err; + gint old_id; + AgDbSave *save_data; recalculate_chart(window); - uri = ag_window_get_uri(window); - if (uri != NULL) { - GFile *file = g_file_new_for_uri(uri); - g_free(uri); + if (!ag_window_can_close(window, FALSE)) { + old_id = (priv->saved_data) ? priv->saved_data->db_id : -1; + save_data = ag_chart_get_db_save(priv->chart, old_id); - ag_chart_save_to_file(priv->chart, file, &err); - } else { - ag_window_save_as(window, &err); - } + if (!ag_db_save_chart(db, save_data, &err)) { + ag_app_message_dialog( + GTK_WIDGET(window), + GTK_MESSAGE_ERROR, + _("Unable to save: %s"), + err->message + ); + } - if (err) { - ag_app_message_dialog( - GTK_WIDGET(window), - GTK_MESSAGE_ERROR, - "%s", err->message - ); + ag_db_save_data_free(priv->saved_data); + priv->saved_data = save_data; } } static void -ag_window_save_as_action(GSimpleAction *action, - GVariant *parameter, - gpointer user_data) +ag_window_export_action(GSimpleAction *action, + GVariant *parameter, + gpointer user_data) { AgWindow *window = AG_WINDOW(user_data); GError *err = NULL; recalculate_chart(window); - ag_window_save_as(window, &err); + ag_window_export(window, &err); if (err) { ag_app_message_dialog( @@ -874,6 +963,14 @@ ag_window_tab_changed_cb(GtkStack *stack, GParamSpec *pspec, AgWindow *window) gtk_widget_set_size_request(active_tab, 600, 600); } + if (strcmp("list", active_tab_name) == 0) { + gtk_revealer_set_reveal_child(GTK_REVEALER(priv->menubutton_revealer), FALSE); + gtk_stack_set_visible_child_name(GTK_STACK(priv->new_back_stack), "new"); + } else { + gtk_revealer_set_reveal_child(GTK_REVEALER(priv->menubutton_revealer), TRUE); + gtk_stack_set_visible_child_name(GTK_STACK(priv->new_back_stack), "back"); + } + // If we are coming from the Edit tab, let’s assume the chart data has // changed. This is a bad idea, though, it should be checked instead! // (TODO) @@ -900,14 +997,68 @@ ag_window_change_tab_action(GSimpleAction *action, g_action_change_state(G_ACTION(action), parameter); } +static void +ag_window_new_chart_action(GSimpleAction *action, + GVariant *parameter, + gpointer user_data) +{ + AgWindow *window = AG_WINDOW(user_data); + AgWindowPrivate *priv = ag_window_get_instance_private(window); + + if (priv->chart) { + ag_app_message_dialog( + GTK_WIDGET(window), + GTK_MESSAGE_ERROR, + "This window already has a chart. " \ + "This should not happen, " \ + "please consider issuing a bug report!" + ); + + gtk_stack_set_visible_child_name(GTK_STACK(priv->stack), "chart"); + + return; + } + + gtk_stack_set_visible_child_name(GTK_STACK(priv->stack), "edit"); +} + +static void +ag_window_back_action(GSimpleAction *action, + GVariant *parameter, + gpointer user_data) +{ + AgWindow *window = AG_WINDOW(user_data); + AgWindowPrivate *priv = ag_window_get_instance_private(window); + + if (ag_window_can_close(window, TRUE)) { + g_clear_object(&(priv->chart)); + ag_db_save_data_free(priv->saved_data); + priv->saved_data = NULL; + + ag_window_load_chart_list(window); + gtk_stack_set_visible_child_name(GTK_STACK(priv->stack), "list"); + } +} + +static void +ag_window_refresh_action(GSimpleAction *action, + GVariant *parameter, + gpointer user_data) +{ + ag_window_load_chart_list(AG_WINDOW(user_data)); +} + static GActionEntry win_entries[] = { { "close", ag_window_close_action, NULL, NULL, NULL }, { "save", ag_window_save_action, NULL, NULL, NULL }, - { "save-as", ag_window_save_as_action, NULL, NULL, NULL }, + { "export", ag_window_export_action, NULL, NULL, NULL }, { "export-svg", ag_window_export_svg_action, NULL, NULL, NULL }, { "view-menu", ag_window_view_menu_action, NULL, "false", NULL }, { "gear-menu", ag_window_gear_menu_action, NULL, "false", NULL }, { "change-tab", ag_window_change_tab_action, "s", "'edit'", NULL }, + { "new-chart", ag_window_new_chart_action, NULL, NULL, NULL }, + { "back", ag_window_back_action, NULL, NULL, NULL }, + { "refresh", ag_window_refresh_action, NULL, NULL, NULL }, }; static void @@ -972,6 +1123,70 @@ ag_window_set_default_house_system(GtkTreeModel *model, return FALSE; } +static void +ag_window_list_item_activated_cb(GdMainView *view, + const gchar *id, + const GtkTreePath *path, + AgWindow *window) +{ + guint row_id = atoi(id); + AgWindowPrivate *priv = ag_window_get_instance_private(window); + AgDb *db = ag_db_get(); + GError *err = NULL; + + if (priv->saved_data != NULL) { + ag_app_message_dialog( + GTK_WIDGET(window), + GTK_MESSAGE_ERROR, + "Window chart is not saved. " \ + "This is a bug, it should not happen here. " \ + "Please consider opening a bug report!" + ); + + ag_window_change_tab(window, "chart"); + + return; + } + + if ((priv->saved_data = ag_db_get_chart_data_by_id( + db, + row_id, + &err)) == NULL) { + ag_app_message_dialog( + GTK_WIDGET(window), + GTK_MESSAGE_ERROR, + "Could not open chart." + ); + + return; + } + + if (priv->chart) { + g_object_unref(priv->chart); + priv->chart = NULL; + } + + if ((priv->chart = ag_chart_new_from_db_save( + priv->saved_data, + &err + )) == NULL) { + ag_app_message_dialog( + GTK_WIDGET(window), + GTK_MESSAGE_ERROR, + "Error: %s", + err->message + ); + ag_db_save_data_free(priv->saved_data); + priv->saved_data = NULL; + + return; + } + + ag_window_update_from_chart(window); + + ag_window_change_tab(window, "chart"); +} + static void ag_window_init(AgWindow *window) { @@ -1021,8 +1236,29 @@ ag_window_init(AgWindow *window) NULL ); - gtk_stack_set_visible_child_name(GTK_STACK(priv->stack), "edit"); - priv->current_tab = priv->tab_edit; + priv->tab_list = GTK_WIDGET(gd_main_view_new(GD_MAIN_VIEW_ICON)); + gtk_stack_add_titled( + GTK_STACK(priv->stack), + priv->tab_list, + "list", + "Chart list" + ); + + gd_main_view_set_selection_mode(GD_MAIN_VIEW(priv->tab_list), FALSE); + gd_main_view_set_model( + GD_MAIN_VIEW(priv->tab_list), + GTK_TREE_MODEL(priv->db_chart_data) + ); + g_signal_connect( + priv->tab_list, + "item-activated", + G_CALLBACK(ag_window_list_item_activated_cb), + window + ); + + gtk_stack_set_visible_child_name(GTK_STACK(priv->stack), "list"); + priv->current_tab = priv->tab_list; + g_object_set( priv->year_adjust, "lower", (gdouble)G_MININT, @@ -1031,7 +1267,6 @@ ag_window_init(AgWindow *window) ); priv->chart = NULL; - priv->uri = NULL; g_action_map_add_action_entries( G_ACTION_MAP(window), @@ -1071,6 +1306,21 @@ ag_window_class_init(AgWindowClass *klass) AgWindow, header_bar ); + gtk_widget_class_bind_template_child_private( + widget_class, + AgWindow, + new_back_stack + ); + gtk_widget_class_bind_template_child_private( + widget_class, + AgWindow, + menubutton_revealer + ); + gtk_widget_class_bind_template_child_private( + widget_class, + AgWindow, + db_chart_data + ); gtk_widget_class_bind_template_child_private(widget_class, AgWindow, name); gtk_widget_class_bind_template_child_private(widget_class, AgWindow, year); gtk_widget_class_bind_template_child_private(widget_class, AgWindow, month); @@ -1236,9 +1486,12 @@ ag_window_set_chart(AgWindow *window, AgChart *chart) g_clear_object(&(priv->chart)); } + ag_db_save_data_free(priv->saved_data); + priv->chart = chart; g_signal_connect(priv->chart, "changed", G_CALLBACK(chart_changed), window); g_object_ref(chart); + priv->saved_data = ag_chart_get_db_save(chart, -1); } AgChart * @@ -1249,26 +1502,6 @@ ag_window_get_chart(AgWindow *window) return priv->chart; } -void -ag_window_set_uri(AgWindow *window, const gchar *uri) -{ - AgWindowPrivate *priv = ag_window_get_instance_private(window); - - if (priv->uri != NULL) { - g_free(priv->uri); - } - - priv->uri = g_strdup(uri); -} - -gchar * -ag_window_get_uri(AgWindow *window) -{ - AgWindowPrivate *priv = ag_window_get_instance_private(window); - - return g_strdup(priv->uri); -} - void ag_window_settings_restore(GtkWindow *window, GSettings *settings) { @@ -1343,3 +1576,51 @@ ag_window_name_changed_cb(GtkEntry *name_entry, AgWindow *window) gtk_header_bar_set_subtitle(GTK_HEADER_BAR(priv->header_bar), name); } + +static void +ag_window_add_chart_to_list(AgDbSave *save_data, AgWindow *window) +{ + GtkTreeIter iter; + AgWindowPrivate *priv = ag_window_get_instance_private(window); + gchar *id = g_strdup_printf("%d", save_data->db_id); + + gtk_list_store_append(priv->db_chart_data, &iter); + gtk_list_store_set( + priv->db_chart_data, &iter, + 0, id, /* ID */ + 1, NULL, /* URI */ + 2, save_data->name, /* Primary text */ + 3, NULL, /* Secondary text */ + 4, NULL, /* Icon */ + 5, 0, /* mtime */ + 6, FALSE, /* Selected */ + 7, 0, /* Pulse */ + -1 + ); + g_free(id); +} + +static void +ag_window_clear_chart_list(AgWindow *window) +{ + AgWindowPrivate *priv = ag_window_get_instance_private(window); + + gtk_list_store_clear(priv->db_chart_data); +} + +gboolean +ag_window_load_chart_list(AgWindow *window) +{ + AgDb *db = ag_db_get(); + GError *err = NULL; + GList *chart_list = ag_db_get_chart_list(db, &err); + + ag_window_clear_chart_list(window); + /* With only a few charts, this should be fine. Maybe implementing lazy + * loading would be a better idea. See: + * http://blogs.gnome.org/ebassi/documentation/lazy-loading/ + */ + g_list_foreach(chart_list, (GFunc)ag_window_add_chart_to_list, window); + + return TRUE; +} diff --git a/src/ag-window.h b/src/ag-window.h index c49717f..c4ba531 100644 --- a/src/ag-window.h +++ b/src/ag-window.h @@ -50,11 +50,6 @@ AgChart *ag_window_get_chart(AgWindow *window); void ag_window_update_from_chart(AgWindow *window); -void ag_window_set_uri(AgWindow *window, - const gchar *uri); - -gchar *ag_window_get_uri(AgWindow *window); - void ag_window_settings_restore(GtkWindow *window, GSettings *settings); @@ -63,6 +58,8 @@ void ag_window_settings_save(GtkWindow *window, void ag_window_change_tab(AgWindow *window, const gchar *tab_name); +gboolean ag_window_load_chart_list(AgWindow *window); + #define AG_WINDOW_ERROR (ag_window_error_quark()) GQuark ag_window_error_quark(void); diff --git a/src/resources/ui/ag-window.ui b/src/resources/ui/ag-window.ui index 37828aa..94b89c7 100644 --- a/src/resources/ui/ag-window.ui +++ b/src/resources/ui/ag-window.ui @@ -38,9 +38,9 @@ <Primary>s - Save as… - win.save-as - <Primary><Shift>s + Export… + win.export + <Primary><Shift>e
@@ -116,11 +116,32 @@ + + + + + + + + + + + + + + + + + + + +