zathura/database-sqlite.c
Marwan Tanager ae59da30c8 Bookmark the exact position, not just the page number (along with a number of fixes).
This patch adds some enhancements/fixes to the bookmarking feature of Zathura:

    - Bookmark the exact vertical and horizontal adjustments values, along with
      the page number. This is done in a backward-compatible way for both the
      plain and sqlite database backends, so that bookmarks that was taken
      previously (bookmarking only the page number) will still work as expected
      and won't be lost.

    - Fix the issue of not being able to remove bookmarks from the plain
      database; removing a bookmark in plain_remove_bookmark using
      g_key_file_remove_key corrupts the bookmarks file.  This is due to not
      truncating the entire bookmarks file in zathura_db_write_key_file_to_file
      prior to overriding it with a new one not containing the removed line.
      This is why, I guess, plain_remove_bookmark hadn't been implemented as
      expected using g_key_file_remove_key, because apparently, someone thought
      that the problem is caused by this API.

    - Fix not being able to update existing bookmarks persistently; updating a
      bookmark works only during the current session, but after the file is
      closed and reopened, the updated bookmark still has it's old value. This
      is due to changing only the bookmark structure in memory; the proper way
      to do it, is to call zathura_db_remove_bookmark on the old bookmark, then
      follow that by a call to zathura_db_add_bookmark on a bookmark structure
      containing the new position and the same old ID.

    - Make zathura_bookmark_add updates the bookmark if it already exists,
      rather than doing this externally in cmd_bookmark_create. This allows us
      to have all the relevant girara_notify messages in cmd_bookmark_create.

Signed-off-by: Sebastian Ramacher <sebastian+dev@ramacher.at>
2013-06-21 09:45:36 +02:00

480 lines
15 KiB
C

/* See LICENSE file for license and copyright information */
#include <sqlite3.h>
#include <girara/utils.h>
#include <girara/datastructures.h>
#include <girara/input-history.h>
#include <string.h>
#include "database-sqlite.h"
static void zathura_database_interface_init(ZathuraDatabaseInterface* iface);
static void io_interface_init(GiraraInputHistoryIOInterface* iface);
G_DEFINE_TYPE_WITH_CODE(ZathuraSQLDatabase, zathura_sqldatabase, G_TYPE_OBJECT,
G_IMPLEMENT_INTERFACE(ZATHURA_TYPE_DATABASE, zathura_database_interface_init)
G_IMPLEMENT_INTERFACE(GIRARA_TYPE_INPUT_HISTORY_IO, io_interface_init))
static void sqlite_finalize(GObject* object);
static bool sqlite_add_bookmark(zathura_database_t* db, const char* file,
zathura_bookmark_t* bookmark);
static bool sqlite_remove_bookmark(zathura_database_t* db, const char* file,
const char* id);
static girara_list_t* sqlite_load_bookmarks(zathura_database_t* db,
const char* file);
static bool sqlite_set_fileinfo(zathura_database_t* db, const char* file,
zathura_fileinfo_t* file_info);
static bool sqlite_get_fileinfo(zathura_database_t* db, const char* file,
zathura_fileinfo_t* file_info);
static void sqlite_set_property(GObject* object, guint prop_id,
const GValue* value, GParamSpec* pspec);
static void sqlite_io_append(GiraraInputHistoryIO* db, const char*);
static girara_list_t* sqlite_io_read(GiraraInputHistoryIO* db);
typedef struct zathura_sqldatabase_private_s {
sqlite3* session;
} zathura_sqldatabase_private_t;
#define ZATHURA_SQLDATABASE_GET_PRIVATE(obj) \
(G_TYPE_INSTANCE_GET_PRIVATE ((obj), ZATHURA_TYPE_SQLDATABASE, zathura_sqldatabase_private_t))
enum {
PROP_0,
PROP_PATH
};
static void
zathura_database_interface_init(ZathuraDatabaseInterface* iface)
{
/* initialize interface */
iface->add_bookmark = sqlite_add_bookmark;
iface->remove_bookmark = sqlite_remove_bookmark;
iface->load_bookmarks = sqlite_load_bookmarks;
iface->set_fileinfo = sqlite_set_fileinfo;
iface->get_fileinfo = sqlite_get_fileinfo;
}
static void
io_interface_init(GiraraInputHistoryIOInterface* iface)
{
/* initialize interface */
iface->append = sqlite_io_append;
iface->read = sqlite_io_read;
}
static void
zathura_sqldatabase_class_init(ZathuraSQLDatabaseClass* class)
{
/* add private members */
g_type_class_add_private(class, sizeof(zathura_sqldatabase_private_t));
/* override methods */
GObjectClass* object_class = G_OBJECT_CLASS(class);
object_class->finalize = sqlite_finalize;
object_class->set_property = sqlite_set_property;
g_object_class_install_property(object_class, PROP_PATH,
g_param_spec_string("path", "path", "path to the database", NULL,
G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
}
static void
zathura_sqldatabase_init(ZathuraSQLDatabase* db)
{
zathura_sqldatabase_private_t* priv = ZATHURA_SQLDATABASE_GET_PRIVATE(db);
priv->session = NULL;
}
zathura_database_t*
zathura_sqldatabase_new(const char* path)
{
g_return_val_if_fail(path != NULL && strlen(path) != 0, NULL);
zathura_database_t* db = g_object_new(ZATHURA_TYPE_SQLDATABASE, "path", path, NULL);
zathura_sqldatabase_private_t* priv = ZATHURA_SQLDATABASE_GET_PRIVATE(db);
if (priv->session == NULL) {
g_object_unref(G_OBJECT(db));
return NULL;
}
return db;
}
static void
sqlite_finalize(GObject* object)
{
ZathuraSQLDatabase* db = ZATHURA_SQLDATABASE(object);
zathura_sqldatabase_private_t* priv = ZATHURA_SQLDATABASE_GET_PRIVATE(db);
if (priv->session) {
sqlite3_close(priv->session);
}
G_OBJECT_CLASS(zathura_sqldatabase_parent_class)->finalize(object);
}
static void
sqlite_db_init(ZathuraSQLDatabase* db, const char* path)
{
zathura_sqldatabase_private_t* priv = ZATHURA_SQLDATABASE_GET_PRIVATE(db);
/* create bookmarks database */
static const char SQL_BOOKMARK_INIT[] =
"CREATE TABLE IF NOT EXISTS bookmarks ("
"file TEXT,"
"id TEXT,"
"page INTEGER,"
"PRIMARY KEY(file, id));";
static const char SQL_FILEINFO_INIT[] =
"CREATE TABLE IF NOT EXISTS fileinfo ("
"file TEXT PRIMARY KEY,"
"page INTEGER,"
"offset INTEGER,"
"scale FLOAT,"
"rotation INTEGER,"
"pages_per_row INTEGER,"
"first_page_column INTEGER,"
"position_x FLOAT,"
"position_y FLOAT"
");";
static const char SQL_FILEINFO_ALTER[] =
"ALTER TABLE fileinfo ADD COLUMN pages_per_row INTEGER;"
"ALTER TABLE fileinfo ADD COLUMN position_x FLOAT;"
"ALTER TABLE fileinfo ADD COLUMN position_y FLOAT;";
static const char SQL_FILEINFO_ALTER2[] =
"ALTER TABLE fileinfo ADD COLUMN first_page_column INTEGER;";
static const char SQL_BOOKMARK_ALTER[] =
"ALTER TABLE bookmarks ADD COLUMN hadj_ratio FLOAT;"
"ALTER TABLE bookmarks ADD COLUMN vadj_ratio FLOAT;";
static const char SQL_HISTORY_INIT[] =
"CREATE TABLE IF NOT EXISTS history ("
"time TIMESTAMP,"
"line TEXT,"
"PRIMARY KEY(line));";
sqlite3* session = NULL;
if (sqlite3_open(path, &session) != SQLITE_OK) {
girara_error("Could not open database: %s\n", path);
return;
}
if (sqlite3_exec(session, SQL_BOOKMARK_INIT, NULL, 0, NULL) != SQLITE_OK) {
girara_error("Failed to initialize database: %s\n", path);
sqlite3_close(session);
return;
}
if (sqlite3_exec(session, SQL_FILEINFO_INIT, NULL, 0, NULL) != SQLITE_OK) {
girara_error("Failed to initialize database: %s\n", path);
sqlite3_close(session);
return;
}
if (sqlite3_exec(session, SQL_HISTORY_INIT, NULL, 0, NULL) != SQLITE_OK) {
girara_error("Failed to initialize database: %s\n", path);
sqlite3_close(session);
return;
}
const char* data_type = NULL;
if (sqlite3_table_column_metadata(session, NULL, "fileinfo", "pages_per_row", &data_type, NULL, NULL, NULL, NULL) != SQLITE_OK) {
girara_debug("old database table layout detected; updating ...");
if (sqlite3_exec(session, SQL_FILEINFO_ALTER, NULL, 0, NULL) != SQLITE_OK) {
girara_warning("failed to update database table layout");
}
}
data_type = NULL;
if (sqlite3_table_column_metadata(session, NULL, "fileinfo", "first_page_column", &data_type, NULL, NULL, NULL, NULL) != SQLITE_OK) {
girara_debug("old database table layout detected; updating ...");
if (sqlite3_exec(session, SQL_FILEINFO_ALTER2, NULL, 0, NULL) != SQLITE_OK) {
girara_warning("failed to update database table layout");
}
}
data_type = NULL;
if (sqlite3_table_column_metadata(session, NULL, "bookmarks", "hadj_ratio", &data_type, NULL, NULL, NULL, NULL) != SQLITE_OK &&
sqlite3_table_column_metadata(session, NULL, "bookmarks", "vadj_ratio", &data_type, NULL, NULL, NULL, NULL) != SQLITE_OK) {
girara_debug("old database table layout detected; updating ...");
if (sqlite3_exec(session, SQL_BOOKMARK_ALTER, NULL, 0, NULL) != SQLITE_OK) {
girara_warning("failed to update database table layout");
}
}
priv->session = session;
}
static void
sqlite_set_property(GObject* object, guint prop_id, const GValue* value, GParamSpec* pspec)
{
ZathuraSQLDatabase* db = ZATHURA_SQLDATABASE(object);
zathura_sqldatabase_private_t* priv = ZATHURA_SQLDATABASE_GET_PRIVATE(db);
switch (prop_id) {
case PROP_PATH:
g_return_if_fail(priv->session == NULL);
sqlite_db_init(db, g_value_get_string(value));
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
}
}
static sqlite3_stmt*
prepare_statement(sqlite3* session, const char* statement)
{
if (session == NULL || statement == NULL) {
return NULL;
}
const char* pz_tail = NULL;
sqlite3_stmt* pp_stmt = NULL;
if (sqlite3_prepare_v2(session, statement, -1, &pp_stmt, &pz_tail) != SQLITE_OK) {
girara_error("Failed to prepare query: %s", statement);
sqlite3_finalize(pp_stmt);
return NULL;
} else if (pz_tail && *pz_tail != '\0') {
girara_error("Unused portion of statement: %s", pz_tail);
sqlite3_finalize(pp_stmt);
return NULL;
}
return pp_stmt;
}
static bool
sqlite_add_bookmark(zathura_database_t* db, const char* file,
zathura_bookmark_t* bookmark)
{
zathura_sqldatabase_private_t* priv = ZATHURA_SQLDATABASE_GET_PRIVATE(db);
static const char SQL_BOOKMARK_ADD[] =
"REPLACE INTO bookmarks (file, id, page, hadj_ratio, vadj_ratio) VALUES (?, ?, ?, ?, ?);";
sqlite3_stmt* stmt = prepare_statement(priv->session, SQL_BOOKMARK_ADD);
if (stmt == NULL) {
return false;
}
if (sqlite3_bind_text(stmt, 1, file, -1, NULL) != SQLITE_OK ||
sqlite3_bind_text(stmt, 2, bookmark->id, -1, NULL) != SQLITE_OK ||
sqlite3_bind_int(stmt, 3, bookmark->page) != SQLITE_OK ||
sqlite3_bind_double(stmt, 4, bookmark->x) != SQLITE_OK ||
sqlite3_bind_double(stmt, 5, bookmark->y) != SQLITE_OK) {
sqlite3_finalize(stmt);
girara_error("Failed to bind arguments.");
return false;
}
int res = sqlite3_step(stmt);
sqlite3_finalize(stmt);
return (res == SQLITE_DONE) ? true : false;
}
static bool
sqlite_remove_bookmark(zathura_database_t* db, const char* file, const char*
id)
{
zathura_sqldatabase_private_t* priv = ZATHURA_SQLDATABASE_GET_PRIVATE(db);
static const char SQL_BOOKMARK_ADD[] =
"DELETE FROM bookmarks WHERE file = ? AND id = ?;";
sqlite3_stmt* stmt = prepare_statement(priv->session, SQL_BOOKMARK_ADD);
if (stmt == NULL) {
return false;
}
if (sqlite3_bind_text(stmt, 1, file, -1, NULL) != SQLITE_OK ||
sqlite3_bind_text(stmt, 2, id, -1, NULL) != SQLITE_OK) {
sqlite3_finalize(stmt);
girara_error("Failed to bind arguments.");
return false;
}
int res = sqlite3_step(stmt);
sqlite3_finalize(stmt);
return (res == SQLITE_DONE) ? true : false;
}
static girara_list_t*
sqlite_load_bookmarks(zathura_database_t* db, const char* file)
{
zathura_sqldatabase_private_t* priv = ZATHURA_SQLDATABASE_GET_PRIVATE(db);
static const char SQL_BOOKMARK_SELECT[] =
"SELECT id, page, hadj_ratio, vadj_ratio FROM bookmarks WHERE file = ?;";
sqlite3_stmt* stmt = prepare_statement(priv->session, SQL_BOOKMARK_SELECT);
if (stmt == NULL) {
return NULL;
}
if (sqlite3_bind_text(stmt, 1, file, -1, NULL) != SQLITE_OK) {
sqlite3_finalize(stmt);
girara_error("Failed to bind arguments.");
return NULL;
}
girara_list_t* result = girara_sorted_list_new2((girara_compare_function_t) zathura_bookmarks_compare,
(girara_free_function_t) zathura_bookmark_free);
while (sqlite3_step(stmt) == SQLITE_ROW) {
zathura_bookmark_t* bookmark = g_malloc0(sizeof(zathura_bookmark_t));
bookmark->id = g_strdup((const char*) sqlite3_column_text(stmt, 0));
bookmark->page = sqlite3_column_int(stmt, 1);
bookmark->x = sqlite3_column_double(stmt, 2);
bookmark->y = sqlite3_column_double(stmt, 3);
if (bookmark->page > 1) {
bookmark->x = bookmark->x == 0.0 ? DBL_MIN : bookmark->x;
bookmark->y = bookmark->y == 0.0 ? DBL_MIN : bookmark->y;
}
girara_list_append(result, bookmark);
}
sqlite3_finalize(stmt);
return result;
}
static bool
sqlite_set_fileinfo(zathura_database_t* db, const char* file,
zathura_fileinfo_t* file_info)
{
if (db == NULL || file == NULL || file_info == NULL) {
return false;
}
zathura_sqldatabase_private_t* priv = ZATHURA_SQLDATABASE_GET_PRIVATE(db);
static const char SQL_FILEINFO_SET[] =
"REPLACE INTO fileinfo (file, page, offset, scale, rotation, pages_per_row, first_page_column, position_x, position_y) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?);";
sqlite3_stmt* stmt = prepare_statement(priv->session, SQL_FILEINFO_SET);
if (stmt == NULL) {
return false;
}
if (sqlite3_bind_text(stmt, 1, file, -1, NULL) != SQLITE_OK ||
sqlite3_bind_int(stmt, 2, file_info->current_page) != SQLITE_OK ||
sqlite3_bind_int(stmt, 3, file_info->page_offset) != SQLITE_OK ||
sqlite3_bind_double(stmt, 4, file_info->scale) != SQLITE_OK ||
sqlite3_bind_int(stmt, 5, file_info->rotation) != SQLITE_OK ||
sqlite3_bind_int(stmt, 6, file_info->pages_per_row) != SQLITE_OK ||
sqlite3_bind_int(stmt, 7, file_info->first_page_column) != SQLITE_OK ||
sqlite3_bind_double(stmt, 8, file_info->position_x) != SQLITE_OK ||
sqlite3_bind_double(stmt, 9, file_info->position_y) != SQLITE_OK) {
sqlite3_finalize(stmt);
girara_error("Failed to bind arguments.");
return false;
}
int res = sqlite3_step(stmt);
sqlite3_finalize(stmt);
return (res == SQLITE_DONE) ? true : false;
}
static bool
sqlite_get_fileinfo(zathura_database_t* db, const char* file,
zathura_fileinfo_t* file_info)
{
if (db == NULL || file == NULL || file_info == NULL) {
return false;
}
zathura_sqldatabase_private_t* priv = ZATHURA_SQLDATABASE_GET_PRIVATE(db);
static const char SQL_FILEINFO_GET[] =
"SELECT page, offset, scale, rotation, pages_per_row, first_page_column, position_x, position_y FROM fileinfo WHERE file = ?;";
sqlite3_stmt* stmt = prepare_statement(priv->session, SQL_FILEINFO_GET);
if (stmt == NULL) {
return false;
}
if (sqlite3_bind_text(stmt, 1, file, -1, NULL) != SQLITE_OK) {
sqlite3_finalize(stmt);
girara_error("Failed to bind arguments.");
return false;
}
if (sqlite3_step(stmt) != SQLITE_ROW) {
sqlite3_finalize(stmt);
girara_info("No info for file %s available.", file);
return false;
}
file_info->current_page = sqlite3_column_int(stmt, 0);
file_info->page_offset = sqlite3_column_int(stmt, 1);
file_info->scale = sqlite3_column_double(stmt, 2);
file_info->rotation = sqlite3_column_int(stmt, 3);
file_info->pages_per_row = sqlite3_column_int(stmt, 4);
file_info->first_page_column = sqlite3_column_int(stmt, 5);
file_info->position_x = sqlite3_column_double(stmt, 6);
file_info->position_y = sqlite3_column_double(stmt, 7);
sqlite3_finalize(stmt);
return true;
}
static void
sqlite_io_append(GiraraInputHistoryIO* db, const char* input)
{
static const char SQL_HISTORY_SET[] =
"REPLACE INTO history (line, time) VALUES (?, DATETIME('now'));";
zathura_sqldatabase_private_t* priv = ZATHURA_SQLDATABASE_GET_PRIVATE(db);
sqlite3_stmt* stmt = prepare_statement(priv->session, SQL_HISTORY_SET);
if (stmt == NULL) {
return;
}
if (sqlite3_bind_text(stmt, 1, input, -1, NULL) != SQLITE_OK) {
sqlite3_finalize(stmt);
girara_error("Failed to bind arguments.");
return;
}
sqlite3_step(stmt);
sqlite3_finalize(stmt);
}
static girara_list_t*
sqlite_io_read(GiraraInputHistoryIO* db)
{
static const char SQL_HISTORY_GET[] =
"SELECT line FROM history ORDER BY time";
zathura_sqldatabase_private_t* priv = ZATHURA_SQLDATABASE_GET_PRIVATE(db);
sqlite3_stmt* stmt = prepare_statement(priv->session, SQL_HISTORY_GET);
if (stmt == NULL) {
return NULL;
}
girara_list_t* list = girara_list_new2((girara_free_function_t) g_free);
if (list == NULL) {
sqlite3_finalize(stmt);
return NULL;
}
while (sqlite3_step(stmt) == SQLITE_ROW) {
girara_list_append(list, g_strdup((const char*) sqlite3_column_text(stmt, 0)));
}
sqlite3_finalize(stmt);
return list;
}