/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ /* * 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. * * Authors: * James Willcox */ #ifdef HAVE_CONFIG_H #include #endif #include #include #include #include #include #include #include #include #include #include #include #include #include #include "egg-recent-model.h" #include "egg-recent-item.h" #define EGG_RECENT_MODEL_FILE_PATH "/.recently-used" #define EGG_RECENT_MODEL_BUFFER_SIZE 8192 #define EGG_RECENT_MODEL_MAX_ITEMS 500 #define EGG_RECENT_MODEL_DEFAULT_LIMIT 10 #define EGG_RECENT_MODEL_TIMEOUT_LENGTH 200 #define EGG_RECENT_MODEL_POLL_TIME 3 /* needed for Darwin */ #if !HAVE_DECL_LOCKF int lockf (int filedes, int function, off_t size); #endif #define EGG_RECENT_MODEL_KEY_DIR "/desktop/gnome/recent_files" #define EGG_RECENT_MODEL_DEFAULT_LIMIT_KEY EGG_RECENT_MODEL_KEY_DIR "/default_limit" #define EGG_RECENT_MODEL_EXPIRE_KEY EGG_RECENT_MODEL_KEY_DIR "/expire" struct _EggRecentModelPrivate { GSList *mime_filter_values; /* list of mime types we allow */ GSList *group_filter_values; /* list of groups we allow */ GSList *scheme_filter_values; /* list of URI schemes we allow */ EggRecentModelSort sort_type; /* type of sorting to be done */ int limit; /* soft limit for length of the list */ int expire_days; /* number of days to hold an item */ char *path; /* path to the file we store stuff in */ GHashTable *monitors; GnomeVFSMonitorHandle *monitor; GConfClient *client; gboolean use_default_limit; guint limit_change_notify_id; guint expiration_change_notify_id; guint changed_timeout; guint poll_timeout; time_t last_mtime; }; /* signals */ enum { CHANGED, LAST_SIGNAL }; static GType model_signals[LAST_SIGNAL] = { 0 }; /* properties */ enum { PROP_BOGUS, PROP_MIME_FILTERS, PROP_GROUP_FILTERS, PROP_SCHEME_FILTERS, PROP_SORT_TYPE, PROP_LIMIT }; typedef struct { GSList *states; GList *items; EggRecentItem *current_item; } ParseInfo; typedef enum { STATE_START, STATE_RECENT_FILES, STATE_RECENT_ITEM, STATE_URI, STATE_MIME_TYPE, STATE_TIMESTAMP, STATE_PRIVATE, STATE_GROUPS, STATE_GROUP } ParseState; typedef struct { EggRecentModel *model; GList *list; } ChangedData; #define TAG_RECENT_FILES "RecentFiles" #define TAG_RECENT_ITEM "RecentItem" #define TAG_URI "URI" #define TAG_MIME_TYPE "Mime-Type" #define TAG_TIMESTAMP "Timestamp" #define TAG_PRIVATE "Private" #define TAG_GROUPS "Groups" #define TAG_GROUP "Group" static void start_element_handler (GMarkupParseContext *context, const gchar *element_name, const gchar **attribute_names, const gchar **attribute_values, gpointer user_data, GError **error); static void end_element_handler (GMarkupParseContext *context, const gchar *element_name, gpointer user_data, GError **error); static void text_handler (GMarkupParseContext *context, const gchar *text, gsize text_len, gpointer user_data, GError **error); static void error_handler (GMarkupParseContext *context, GError *error, gpointer user_data); static GMarkupParser parser = {start_element_handler, end_element_handler, text_handler, NULL, error_handler}; static GObjectClass *parent_class; static void egg_recent_model_clear_mime_filter (EggRecentModel *model); static void egg_recent_model_clear_group_filter (EggRecentModel *model); static void egg_recent_model_clear_scheme_filter (EggRecentModel *model); static GObjectClass *parent_class; static gboolean egg_recent_model_string_match (const GSList *list, const gchar *str) { const GSList *tmp; if (list == NULL || str == NULL) return TRUE; tmp = list; while (tmp) { if (g_pattern_match_string (tmp->data, str)) return TRUE; tmp = tmp->next; } return FALSE; } static gboolean egg_recent_model_write_raw (EggRecentModel *model, FILE *file, const gchar *content) { int len; int fd; struct stat sbuf; rewind (file); len = strlen (content); fd = fileno (file); if (fstat (fd, &sbuf) < 0) g_warning ("Couldn't stat XML document."); if ((off_t)len < sbuf.st_size) { ftruncate (fd, len); } if (fputs (content, file) == EOF) return FALSE; #ifndef G_OS_WIN32 fsync (fd); #endif rewind (file); return TRUE; } static GList * egg_recent_model_delete_from_list (GList *list, const gchar *uri) { GList *tmp; if (!uri) return list; tmp = list; while (tmp) { EggRecentItem *item = tmp->data; GList *next; next = tmp->next; if (!strcmp (egg_recent_item_peek_uri (item), uri)) { egg_recent_item_unref (item); list = g_list_remove_link (list, tmp); g_list_free_1 (tmp); } tmp = next; } return list; } static void egg_recent_model_add_new_groups (EggRecentItem *item, EggRecentItem *upd_item) { const GList *tmp; tmp = egg_recent_item_get_groups (upd_item); while (tmp) { char *group = tmp->data; if (!egg_recent_item_in_group (item, group)) egg_recent_item_add_group (item, group); tmp = tmp->next; } } static gboolean egg_recent_model_update_item (GList *items, EggRecentItem *upd_item) { GList *tmp; const char *uri; uri = egg_recent_item_peek_uri (upd_item); tmp = items; while (tmp) { EggRecentItem *item = tmp->data; if (gnome_vfs_uris_match (egg_recent_item_peek_uri (item), uri)) { egg_recent_item_set_timestamp (item, (time_t) -1); egg_recent_model_add_new_groups (item, upd_item); return TRUE; } tmp = tmp->next; } return FALSE; } static gchar * egg_recent_model_read_raw (EggRecentModel *model, FILE *file) { GString *string; char buf[EGG_RECENT_MODEL_BUFFER_SIZE]; rewind (file); string = g_string_new (NULL); while (fgets (buf, EGG_RECENT_MODEL_BUFFER_SIZE, file)) { string = g_string_append (string, buf); } rewind (file); return g_string_free (string, FALSE); } static ParseInfo * parse_info_init (void) { ParseInfo *retval; retval = g_new0 (ParseInfo, 1); retval->states = g_slist_prepend (NULL, STATE_START); retval->items = NULL; return retval; } static void parse_info_free (ParseInfo *info) { g_slist_free (info->states); g_free (info); } static void push_state (ParseInfo *info, ParseState state) { info->states = g_slist_prepend (info->states, GINT_TO_POINTER (state)); } static void pop_state (ParseInfo *info) { g_return_if_fail (info->states != NULL); info->states = g_slist_remove (info->states, info->states->data); } static ParseState peek_state (ParseInfo *info) { g_return_val_if_fail (info->states != NULL, STATE_START); return GPOINTER_TO_INT (info->states->data); } #define ELEMENT_IS(name) (strcmp (element_name, (name)) == 0) static gboolean valid_element (ParseInfo *info, int valid_parent_state, const gchar *element_name, const gchar *valid_element, GError **error) { if (peek_state (info) != valid_parent_state) { g_set_error (error, G_MARKUP_ERROR, G_MARKUP_ERROR_INVALID_CONTENT, "Unexpected tag '%s', tag '%s' expected", element_name, valid_element); return FALSE; } return TRUE; } static void start_element_handler (GMarkupParseContext *context, const gchar *element_name, const gchar **attribute_names, const gchar **attribute_values, gpointer user_data, GError **error) { ParseInfo *info = (ParseInfo *)user_data; if (ELEMENT_IS (TAG_RECENT_FILES)) push_state (info, STATE_RECENT_FILES); else if (ELEMENT_IS (TAG_RECENT_ITEM)) { if (valid_element (info, STATE_RECENT_FILES, TAG_RECENT_ITEM, TAG_RECENT_FILES, error)) { info->current_item = egg_recent_item_new (); push_state (info, STATE_RECENT_ITEM); } } else if (ELEMENT_IS (TAG_URI)) { if (valid_element (info, STATE_RECENT_ITEM, TAG_URI, TAG_RECENT_ITEM, error)) { push_state (info, STATE_URI); } } else if (ELEMENT_IS (TAG_MIME_TYPE)) { if (valid_element (info, STATE_RECENT_ITEM, TAG_MIME_TYPE, TAG_RECENT_ITEM, error)) { push_state (info, STATE_MIME_TYPE); } } else if (ELEMENT_IS (TAG_TIMESTAMP)) { if (valid_element (info, STATE_RECENT_ITEM, TAG_TIMESTAMP, TAG_RECENT_ITEM, error)) { push_state (info, STATE_TIMESTAMP); } } else if (ELEMENT_IS (TAG_PRIVATE)) { if (valid_element (info, STATE_RECENT_ITEM, TAG_PRIVATE, TAG_RECENT_ITEM, error)) { push_state (info, STATE_PRIVATE); egg_recent_item_set_private (info->current_item, TRUE); } } else if (ELEMENT_IS (TAG_GROUPS)) { if (valid_element (info, STATE_RECENT_ITEM, TAG_GROUPS, TAG_RECENT_ITEM, error)) { push_state (info, STATE_GROUPS); } } else if (ELEMENT_IS (TAG_GROUP)) { if (valid_element (info, STATE_GROUPS, TAG_GROUP, TAG_GROUPS, error)) { push_state (info, STATE_GROUP); } } } static gint list_compare_func_mru (gpointer a, gpointer b) { EggRecentItem *item_a = (EggRecentItem *)a; EggRecentItem *item_b = (EggRecentItem *)b; return item_a->timestamp < item_b->timestamp; } static gint list_compare_func_lru (gpointer a, gpointer b) { EggRecentItem *item_a = (EggRecentItem *)a; EggRecentItem *item_b = (EggRecentItem *)b; return item_a->timestamp > item_b->timestamp; } static void end_element_handler (GMarkupParseContext *context, const gchar *element_name, gpointer user_data, GError **error) { ParseInfo *info = (ParseInfo *)user_data; switch (peek_state (info)) { case STATE_RECENT_ITEM: if (!info->current_item) { g_warning ("No recent item found\n"); break; } if (!info->current_item->uri) { g_warning ("Invalid item found\n"); break; } info->items = g_list_prepend (info->items, info->current_item); info->current_item = NULL; break; default: break; } pop_state (info); } static void text_handler (GMarkupParseContext *context, const gchar *text, gsize text_len, gpointer user_data, GError **error) { ParseInfo *info = (ParseInfo *)user_data; gchar *value; value = g_strndup (text, text_len); switch (peek_state (info)) { case STATE_START: case STATE_RECENT_FILES: case STATE_RECENT_ITEM: case STATE_PRIVATE: case STATE_GROUPS: break; case STATE_URI: egg_recent_item_set_uri (info->current_item, value); break; case STATE_MIME_TYPE: egg_recent_item_set_mime_type (info->current_item, value); break; case STATE_TIMESTAMP: egg_recent_item_set_timestamp (info->current_item, (time_t)atoi (value)); break; case STATE_GROUP: egg_recent_item_add_group (info->current_item, text); break; } g_free (value); } static void error_handler (GMarkupParseContext *context, GError *error, gpointer user_data) { g_warning ("Error in parse: %s", error->message); } static void egg_recent_model_enforce_limit (GList *list, int limit) { int len; GList *end; /* limit < 0 means unlimited */ if (limit <= 0) return; len = g_list_length (list); if (len > limit) { GList *next; end = g_list_nth (list, limit-1); next = end->next; end->next = NULL; EGG_RECENT_ITEM_LIST_UNREF (next); } } static GList * egg_recent_model_sort (EggRecentModel *model, GList *list) { switch (model->priv->sort_type) { case EGG_RECENT_MODEL_SORT_MRU: list = g_list_sort (list, (GCompareFunc)list_compare_func_mru); break; case EGG_RECENT_MODEL_SORT_LRU: list = g_list_sort (list, (GCompareFunc)list_compare_func_lru); break; case EGG_RECENT_MODEL_SORT_NONE: break; } return list; } static gboolean egg_recent_model_group_match (EggRecentItem *item, GSList *groups) { GSList *tmp; tmp = groups; while (tmp != NULL) { const gchar * group = (const gchar *)tmp->data; if (egg_recent_item_in_group (item, group)) return TRUE; tmp = tmp->next; } return FALSE; } static GList * egg_recent_model_filter (EggRecentModel *model, GList *list) { GList *newlist = NULL; GList *l; gchar *mime_type; gchar *uri; g_return_val_if_fail (list != NULL, NULL); for (l = list; l != NULL ; l = l->next) { EggRecentItem *item = (EggRecentItem *) l->data; gboolean pass_mime_test = FALSE; gboolean pass_group_test = FALSE; gboolean pass_scheme_test = FALSE; g_assert (item != NULL); uri = egg_recent_item_get_uri (item); /* filter by mime type */ if (model->priv->mime_filter_values != NULL) { mime_type = egg_recent_item_get_mime_type (item); if (egg_recent_model_string_match (model->priv->mime_filter_values, mime_type)) pass_mime_test = TRUE; g_free (mime_type); } else pass_mime_test = TRUE; /* filter by group */ if (pass_mime_test && model->priv->group_filter_values != NULL) { if (egg_recent_model_group_match (item, model->priv->group_filter_values)) pass_group_test = TRUE; } else if (egg_recent_item_get_private (item)) { pass_group_test = FALSE; } else pass_group_test = TRUE; /* filter by URI scheme */ if (pass_mime_test && pass_group_test && model->priv->scheme_filter_values != NULL) { gchar *scheme; scheme = gnome_vfs_get_uri_scheme (uri); if (egg_recent_model_string_match (model->priv->scheme_filter_values, scheme)) pass_scheme_test = TRUE; g_free (scheme); } else pass_scheme_test = TRUE; if (pass_mime_test && pass_group_test && pass_scheme_test) newlist = g_list_prepend (newlist, item); else egg_recent_item_unref (item); g_free (uri); } g_list_free (list); return g_list_reverse (newlist); } #if 0 static void egg_recent_model_monitor_list_cb (GnomeVFSMonitorHandle *handle, const gchar *monitor_uri, const gchar *info_uri, GnomeVFSMonitorEventType event_type, gpointer user_data) { EggRecentModel *model; model = EGG_RECENT_MODEL (user_data); if (event_type == GNOME_VFS_MONITOR_EVENT_DELETED) { egg_recent_model_delete (model, monitor_uri); g_hash_table_remove (model->priv->monitors, monitor_uri); } } static void egg_recent_model_monitor_list (EggRecentModel *model, GList *list) { GList *tmp; tmp = list; while (tmp) { EggRecentItem *item = (EggRecentItem *)tmp->data; GnomeVFSMonitorHandle *handle; GnomeVFSResult res; gchar *uri; tmp = tmp->next; uri = egg_recent_item_get_uri (item); if (g_hash_table_lookup (model->priv->monitors, uri)) { /* already monitoring this one */ g_free (uri); continue; } res = gnome_vfs_monitor_add (&handle, uri, GNOME_VFS_MONITOR_FILE, egg_recent_model_monitor_list_cb, model); if (res == GNOME_VFS_OK) g_hash_table_insert (model->priv->monitors, uri, handle); else g_free (uri); } } #endif static gboolean egg_recent_model_changed_timeout (EggRecentModel *model) { model->priv->changed_timeout = 0; egg_recent_model_changed (model); return FALSE; } static void egg_recent_model_monitor_cb (GnomeVFSMonitorHandle *handle, const gchar *monitor_uri, const gchar *info_uri, GnomeVFSMonitorEventType event_type, gpointer user_data) { EggRecentModel *model; g_return_if_fail (user_data != NULL); g_return_if_fail (EGG_IS_RECENT_MODEL (user_data)); model = EGG_RECENT_MODEL (user_data); if (event_type == GNOME_VFS_MONITOR_EVENT_CHANGED || event_type == GNOME_VFS_MONITOR_EVENT_CREATED || event_type == GNOME_VFS_MONITOR_EVENT_DELETED) { if (model->priv->changed_timeout > 0) { g_source_remove (model->priv->changed_timeout); } model->priv->changed_timeout = g_timeout_add ( EGG_RECENT_MODEL_TIMEOUT_LENGTH, (GSourceFunc)egg_recent_model_changed_timeout, model); } } static gboolean egg_recent_model_poll_timeout (gpointer user_data) { EggRecentModel *model; struct stat stat_buf; int stat_res; model = EGG_RECENT_MODEL (user_data); stat_res = stat (model->priv->path, &stat_buf); if (!stat_res && stat_buf.st_mtime && stat_buf.st_mtime != model->priv->last_mtime) { model->priv->last_mtime = stat_buf.st_mtime; if (model->priv->changed_timeout > 0) g_source_remove (model->priv->changed_timeout); model->priv->changed_timeout = g_timeout_add ( EGG_RECENT_MODEL_TIMEOUT_LENGTH, (GSourceFunc)egg_recent_model_changed_timeout, model); } return TRUE; } static void egg_recent_model_monitor (EggRecentModel *model, gboolean should_monitor) { if (should_monitor && model->priv->monitor == NULL) { char *uri; GnomeVFSResult result; uri = gnome_vfs_get_uri_from_local_path (model->priv->path); result = gnome_vfs_monitor_add (&model->priv->monitor, uri, GNOME_VFS_MONITOR_FILE, egg_recent_model_monitor_cb, model); g_free (uri); /* if the above fails, don't worry about it. * local notifications will still happen */ if (result == GNOME_VFS_ERROR_NOT_SUPPORTED) { if (model->priv->poll_timeout > 0) g_source_remove (model->priv->poll_timeout); model->priv->poll_timeout = g_timeout_add ( EGG_RECENT_MODEL_POLL_TIME * 1000, egg_recent_model_poll_timeout, model); } } else if (!should_monitor && model->priv->monitor != NULL) { gnome_vfs_monitor_cancel (model->priv->monitor); model->priv->monitor = NULL; } } static void egg_recent_model_set_limit_internal (EggRecentModel *model, int limit) { model->priv->limit = limit; if (limit <= 0) egg_recent_model_monitor (model, FALSE); else { egg_recent_model_monitor (model, TRUE); egg_recent_model_changed (model); } } static GList * egg_recent_model_read (EggRecentModel *model, FILE *file) { GList *list=NULL; gchar *content; GMarkupParseContext *ctx; ParseInfo *info; GError *error; content = egg_recent_model_read_raw (model, file); if (strlen (content) <= 0) { g_free (content); return NULL; } info = parse_info_init (); ctx = g_markup_parse_context_new (&parser, 0, info, NULL); error = NULL; if (!g_markup_parse_context_parse (ctx, content, strlen (content), &error)) { g_warning ("Error while parsing the .recently-used file: %s\n", error->message); g_error_free (error); parse_info_free (info); return NULL; } error = NULL; if (!g_markup_parse_context_end_parse (ctx, &error)) { g_warning ("Unable to complete parsing of the .recently-used file: %s\n", error->message); g_error_free (error); g_markup_parse_context_free (ctx); parse_info_free (info); return NULL; } list = g_list_reverse (info->items); g_markup_parse_context_free (ctx); parse_info_free (info); g_free (content); return list; } static gboolean egg_recent_model_write (EggRecentModel *model, FILE *file, GList *list) { GString *string; gchar *data; EggRecentItem *item; const GList *groups; int i; int ret; string = g_string_new ("\n"); string = g_string_append (string, "<" TAG_RECENT_FILES ">\n"); i=0; while (list) { gchar *uri; gchar *mime_type; gchar *escaped_uri; time_t timestamp; item = (EggRecentItem *)list->data; uri = egg_recent_item_get_uri_utf8 (item); escaped_uri = g_markup_escape_text (uri, strlen (uri)); g_free (uri); mime_type = egg_recent_item_get_mime_type (item); timestamp = egg_recent_item_get_timestamp (item); string = g_string_append (string, " <" TAG_RECENT_ITEM ">\n"); g_string_append_printf (string, " <" TAG_URI ">%s\n", escaped_uri); if (mime_type) g_string_append_printf (string, " <" TAG_MIME_TYPE ">%s\n", mime_type); else g_string_append_printf (string, " <" TAG_MIME_TYPE ">\n"); g_string_append_printf (string, " <" TAG_TIMESTAMP ">%d\n", (int)timestamp); if (egg_recent_item_get_private (item)) string = g_string_append (string, " <" TAG_PRIVATE "/>\n"); /* write the groups */ string = g_string_append (string, " <" TAG_GROUPS ">\n"); groups = egg_recent_item_get_groups (item); if (groups == NULL && egg_recent_item_get_private (item)) g_warning ("Item with URI \"%s\" marked as private, but" " does not belong to any groups.\n", uri); while (groups) { const gchar *group = (const gchar *)groups->data; gchar *escaped_group; escaped_group = g_markup_escape_text (group, strlen(group)); g_string_append_printf (string, " <" TAG_GROUP ">%s\n", escaped_group); g_free (escaped_group); groups = groups->next; } string = g_string_append (string, " \n"); string = g_string_append (string, " \n"); g_free (mime_type); g_free (escaped_uri); list = list->next; i++; } string = g_string_append (string, ""); data = g_string_free (string, FALSE); ret = egg_recent_model_write_raw (model, file, data); g_free (data); return ret; } static FILE * egg_recent_model_open_file (EggRecentModel *model, gboolean for_writing) { FILE *file; mode_t prev_umask; file = fopen (model->priv->path, "r+"); if (file == NULL && for_writing) { /* be paranoid */ prev_umask = umask (077); file = fopen (model->priv->path, "w+"); umask (prev_umask); g_return_val_if_fail (file != NULL, NULL); } return file; } static gboolean egg_recent_model_lock_file (FILE *file) { #ifdef HAVE_LOCKF int fd; gint try = 5; rewind (file); fd = fileno (file); /* Attempt to lock the file 5 times, * waiting a random interval (< 1 second) * in between attempts. * We should really be doing asynchronous * locking, but requires substantially larger * changes. */ while (try > 0) { int rand_interval; if (lockf (fd, F_TLOCK, 0) == 0) return TRUE; rand_interval = 1 + (int) (10.0 * rand()/(RAND_MAX + 1.0)); g_usleep (100000 * rand_interval); --try; } return FALSE; #else return TRUE; #endif /* HAVE_LOCKF */ } static gboolean egg_recent_model_unlock_file (FILE *file) { #ifdef HAVE_LOCKF int fd; rewind (file); fd = fileno (file); return (lockf (fd, F_ULOCK, 0) == 0) ? TRUE : FALSE; #else return TRUE; #endif /* HAVE_LOCKF */ } static void egg_recent_model_finalize (GObject *object) { EggRecentModel *model = EGG_RECENT_MODEL (object); if (model->priv->changed_timeout > 0) { g_source_remove (model->priv->changed_timeout); } egg_recent_model_monitor (model, FALSE); g_slist_foreach (model->priv->mime_filter_values, (GFunc) g_pattern_spec_free, NULL); g_slist_free (model->priv->mime_filter_values); model->priv->mime_filter_values = NULL; g_slist_foreach (model->priv->scheme_filter_values, (GFunc) g_pattern_spec_free, NULL); g_slist_free (model->priv->scheme_filter_values); model->priv->scheme_filter_values = NULL; g_slist_foreach (model->priv->group_filter_values, (GFunc) g_free, NULL); g_slist_free (model->priv->group_filter_values); model->priv->group_filter_values = NULL; if (model->priv->limit_change_notify_id) gconf_client_notify_remove (model->priv->client, model->priv->limit_change_notify_id); model->priv->expiration_change_notify_id = 0; if (model->priv->expiration_change_notify_id) gconf_client_notify_remove (model->priv->client, model->priv->expiration_change_notify_id); model->priv->expiration_change_notify_id = 0; g_object_unref (model->priv->client); model->priv->client = NULL; g_free (model->priv->path); model->priv->path = NULL; g_hash_table_destroy (model->priv->monitors); model->priv->monitors = NULL; if (model->priv->poll_timeout > 0) g_source_remove (model->priv->poll_timeout); model->priv->poll_timeout =0; g_free (model->priv); parent_class->finalize (object); } static void egg_recent_model_set_property (GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec) { EggRecentModel *model = EGG_RECENT_MODEL (object); switch (prop_id) { case PROP_MIME_FILTERS: if (model->priv->mime_filter_values != NULL) egg_recent_model_clear_mime_filter (model); model->priv->mime_filter_values = (GSList *)g_value_get_pointer (value); break; case PROP_GROUP_FILTERS: if (model->priv->group_filter_values != NULL) egg_recent_model_clear_group_filter (model); model->priv->group_filter_values = (GSList *)g_value_get_pointer (value); break; case PROP_SCHEME_FILTERS: if (model->priv->scheme_filter_values != NULL) egg_recent_model_clear_scheme_filter (model); model->priv->scheme_filter_values = (GSList *)g_value_get_pointer (value); break; case PROP_SORT_TYPE: model->priv->sort_type = g_value_get_int (value); break; case PROP_LIMIT: egg_recent_model_set_limit (model, g_value_get_int (value)); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); break; } } static void egg_recent_model_get_property (GObject *object, guint prop_id, GValue *value, GParamSpec *pspec) { EggRecentModel *model = EGG_RECENT_MODEL (object); switch (prop_id) { case PROP_MIME_FILTERS: g_value_set_pointer (value, model->priv->mime_filter_values); break; case PROP_GROUP_FILTERS: g_value_set_pointer (value, model->priv->group_filter_values); break; case PROP_SCHEME_FILTERS: g_value_set_pointer (value, model->priv->scheme_filter_values); break; case PROP_SORT_TYPE: g_value_set_int (value, model->priv->sort_type); break; case PROP_LIMIT: g_value_set_int (value, model->priv->limit); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); break; } } static void egg_recent_model_class_init (EggRecentModelClass * klass) { GObjectClass *object_class; parent_class = g_type_class_peek_parent (klass); parent_class = g_type_class_peek_parent (klass); object_class = G_OBJECT_CLASS (klass); object_class->set_property = egg_recent_model_set_property; object_class->get_property = egg_recent_model_get_property; object_class->finalize = egg_recent_model_finalize; model_signals[CHANGED] = g_signal_new ("changed", G_OBJECT_CLASS_TYPE (object_class), G_SIGNAL_RUN_LAST, G_STRUCT_OFFSET (EggRecentModelClass, changed), NULL, NULL, g_cclosure_marshal_VOID__POINTER, G_TYPE_NONE, 1, G_TYPE_POINTER); g_object_class_install_property (object_class, PROP_MIME_FILTERS, g_param_spec_pointer ("mime-filters", "Mime Filters", "List of mime types to be allowed.", G_PARAM_READWRITE)); g_object_class_install_property (object_class, PROP_GROUP_FILTERS, g_param_spec_pointer ("group-filters", "Group Filters", "List of groups to be allowed.", G_PARAM_READWRITE)); g_object_class_install_property (object_class, PROP_SCHEME_FILTERS, g_param_spec_pointer ("scheme-filters", "Scheme Filters", "List of URI schemes to be allowed.", G_PARAM_READWRITE)); g_object_class_install_property (object_class, PROP_SORT_TYPE, g_param_spec_int ("sort-type", "Sort Type", "Type of sorting to be done.", 0, EGG_RECENT_MODEL_SORT_NONE, EGG_RECENT_MODEL_SORT_MRU, G_PARAM_READWRITE)); g_object_class_install_property (object_class, PROP_LIMIT, g_param_spec_int ("limit", "Limit", "Max number of items allowed.", -1, EGG_RECENT_MODEL_MAX_ITEMS, EGG_RECENT_MODEL_DEFAULT_LIMIT, G_PARAM_READWRITE)); klass->changed = NULL; } static void egg_recent_model_limit_changed (GConfClient *client, guint cnxn_id, GConfEntry *entry, gpointer user_data) { EggRecentModel *model; GConfValue *value; model = EGG_RECENT_MODEL (user_data); g_return_if_fail (model != NULL); if (model->priv->use_default_limit == FALSE) return; /* ignore this key */ /* the key was unset, and the schema has apparently failed */ if (entry == NULL) return; value = gconf_entry_get_value (entry); if (value->type != GCONF_VALUE_INT) { g_warning ("Expected GConfValue of type integer, " "got something else"); } egg_recent_model_set_limit_internal (model, gconf_value_get_int (value)); } static void egg_recent_model_expiration_changed (GConfClient *client, guint cnxn_id, GConfEntry *entry, gpointer user_data) { } static void egg_recent_model_init (EggRecentModel * model) { if (!gnome_vfs_init ()) { g_warning ("gnome-vfs initialization failed."); return; } model->priv = g_new0 (EggRecentModelPrivate, 1); model->priv->path = g_strdup_printf ("%s" EGG_RECENT_MODEL_FILE_PATH, g_get_home_dir ()); model->priv->mime_filter_values = NULL; model->priv->group_filter_values = NULL; model->priv->scheme_filter_values = NULL; model->priv->client = gconf_client_get_default (); gconf_client_add_dir (model->priv->client, EGG_RECENT_MODEL_KEY_DIR, GCONF_CLIENT_PRELOAD_ONELEVEL, NULL); model->priv->limit_change_notify_id = gconf_client_notify_add (model->priv->client, EGG_RECENT_MODEL_DEFAULT_LIMIT_KEY, egg_recent_model_limit_changed, model, NULL, NULL); model->priv->expiration_change_notify_id = gconf_client_notify_add (model->priv->client, EGG_RECENT_MODEL_EXPIRE_KEY, egg_recent_model_expiration_changed, model, NULL, NULL); model->priv->expire_days = gconf_client_get_int ( model->priv->client, EGG_RECENT_MODEL_EXPIRE_KEY, NULL); #if 0 /* keep this out, for now */ model->priv->limit = gconf_client_get_int ( model->priv->client, EGG_RECENT_MODEL_DEFAULT_LIMIT_KEY, NULL); model->priv->use_default_limit = TRUE; #endif model->priv->limit = EGG_RECENT_MODEL_DEFAULT_LIMIT; model->priv->use_default_limit = FALSE; model->priv->monitors = g_hash_table_new_full ( g_str_hash, g_str_equal, (GDestroyNotify) g_free, (GDestroyNotify) gnome_vfs_monitor_cancel); model->priv->monitor = NULL; model->priv->poll_timeout = 0; model->priv->last_mtime = 0; egg_recent_model_monitor (model, TRUE); } /** * egg_recent_model_new: * @sort: the type of sorting to use * @limit: maximum number of items in the list * * This creates a new EggRecentModel object. * * Returns: a EggRecentModel object */ EggRecentModel * egg_recent_model_new (EggRecentModelSort sort) { EggRecentModel *model; model = EGG_RECENT_MODEL (g_object_new (egg_recent_model_get_type (), "sort-type", sort, NULL)); g_return_val_if_fail (model, NULL); return model; } /** * egg_recent_model_add_full: * @model: A EggRecentModel object. * @item: A EggRecentItem * * This function adds an item to the list of recently used URIs. * * Returns: gboolean */ gboolean egg_recent_model_add_full (EggRecentModel * model, EggRecentItem *item) { FILE *file; GList *list = NULL; gboolean ret = FALSE; gboolean updated = FALSE; char *uri; time_t t; g_return_val_if_fail (model != NULL, FALSE); g_return_val_if_fail (EGG_IS_RECENT_MODEL (model), FALSE); uri = egg_recent_item_get_uri (item); if (strncmp (uri, "recent-files://", strlen ("recent-files://")) == 0) { g_free (uri); return FALSE; } else { g_free (uri); } file = egg_recent_model_open_file (model, TRUE); g_return_val_if_fail (file != NULL, FALSE); time (&t); egg_recent_item_set_timestamp (item, t); if (egg_recent_model_lock_file (file)) { /* read existing stuff */ list = egg_recent_model_read (model, file); /* if it's already there, we just update it */ updated = egg_recent_model_update_item (list, item); if (!updated) { list = g_list_prepend (list, item); egg_recent_model_enforce_limit (list, EGG_RECENT_MODEL_MAX_ITEMS); } /* write new stuff */ if (!egg_recent_model_write (model, file, list)) g_warning ("Write failed: %s", strerror (errno)); if (!updated) list = g_list_remove (list, item); EGG_RECENT_ITEM_LIST_UNREF (list); ret = TRUE; } else { g_warning ("Failed to lock: %s", strerror (errno)); fclose (file); return FALSE; } if (!egg_recent_model_unlock_file (file)) g_warning ("Failed to unlock: %s", strerror (errno)); fclose (file); if (model->priv->monitor == NULL) { /* since monitoring isn't working, at least give a * local notification */ egg_recent_model_changed (model); } return ret; } /** * egg_recent_model_add: * @model: A EggRecentModel object. * @uri: A string URI * * This function adds an item to the list of recently used URIs. * * Returns: gboolean */ gboolean egg_recent_model_add (EggRecentModel *model, const gchar *uri) { EggRecentItem *item; gboolean ret = FALSE; g_return_val_if_fail (model != NULL, FALSE); g_return_val_if_fail (uri != NULL, FALSE); item = egg_recent_item_new_from_uri (uri); g_return_val_if_fail (item != NULL, FALSE); ret = egg_recent_model_add_full (model, item); egg_recent_item_unref (item); return ret; } /** * egg_recent_model_delete: * @model: A EggRecentModel object. * @uri: The URI you want to delete. * * This function deletes a URI from the file of recently used URIs. * * Returns: gboolean */ gboolean egg_recent_model_delete (EggRecentModel * model, const gchar * uri) { FILE *file; GList *list; unsigned int length; gboolean ret = FALSE; g_return_val_if_fail (model != NULL, FALSE); g_return_val_if_fail (EGG_IS_RECENT_MODEL (model), FALSE); g_return_val_if_fail (uri != NULL, FALSE); file = egg_recent_model_open_file (model, TRUE); g_return_val_if_fail (file != NULL, FALSE); if (egg_recent_model_lock_file (file)) { list = egg_recent_model_read (model, file); if (list == NULL) goto out; length = g_list_length (list); list = egg_recent_model_delete_from_list (list, uri); if (length == g_list_length (list)) { /* nothing was deleted */ EGG_RECENT_ITEM_LIST_UNREF (list); } else { egg_recent_model_write (model, file, list); EGG_RECENT_ITEM_LIST_UNREF (list); ret = TRUE; } } else { g_warning ("Failed to lock: %s", strerror (errno)); return FALSE; } out: if (!egg_recent_model_unlock_file (file)) g_warning ("Failed to unlock: %s", strerror (errno)); fclose (file); g_hash_table_remove (model->priv->monitors, uri); if (model->priv->monitor == NULL && ret) { /* since monitoring isn't working, at least give a * local notification */ egg_recent_model_changed (model); } return ret; } /** * egg_recent_model_get_list: * @model: A EggRecentModel object. * * This function gets the current contents of the file * * Returns: a GList */ GList * egg_recent_model_get_list (EggRecentModel *model) { FILE *file; GList *list = NULL; file = egg_recent_model_open_file (model, FALSE); if (file == NULL) return NULL; if (egg_recent_model_lock_file (file)) list = egg_recent_model_read (model, file); else { g_warning ("Failed to lock: %s", strerror (errno)); fclose (file); return NULL; } if (!egg_recent_model_unlock_file (file)) g_warning ("Failed to unlock: %s", strerror (errno)); if (list != NULL) { list = egg_recent_model_filter (model, list); list = egg_recent_model_sort (model, list); egg_recent_model_enforce_limit (list, model->priv->limit); } fclose (file); return list; } /** * egg_recent_model_set_limit: * @model: A EggRecentModel object. * @limit: The maximum length of the list * * This function sets the maximum length of the list. Note: This only affects * the length of the list emitted in the "changed" signal, not the list stored * on disk. * * Returns: void */ void egg_recent_model_set_limit (EggRecentModel *model, int limit) { model->priv->use_default_limit = FALSE; egg_recent_model_set_limit_internal (model, limit); } /** * egg_recent_model_get_limit: * @model: A EggRecentModel object. * * This function gets the maximum length of the list. * * Returns: int */ int egg_recent_model_get_limit (EggRecentModel *model) { return model->priv->limit; } /** * egg_recent_model_clear: * @model: A EggRecentModel object. * * This function clears the contents of the file * * Returns: void */ void egg_recent_model_clear (EggRecentModel *model) { FILE *file; int fd; file = egg_recent_model_open_file (model, TRUE); g_return_if_fail (file != NULL); fd = fileno (file); if (egg_recent_model_lock_file (file)) { ftruncate (fd, 0); } else { g_warning ("Failed to lock: %s", strerror (errno)); return; } if (!egg_recent_model_unlock_file (file)) g_warning ("Failed to unlock: %s", strerror (errno)); fclose (file); if (model->priv->monitor == NULL) { /* since monitoring isn't working, at least give a * local notification */ egg_recent_model_changed (model); } } static void egg_recent_model_clear_mime_filter (EggRecentModel *model) { g_return_if_fail (model != NULL); if (model->priv->mime_filter_values != NULL) { g_slist_foreach (model->priv->mime_filter_values, (GFunc) g_pattern_spec_free, NULL); g_slist_free (model->priv->mime_filter_values); model->priv->mime_filter_values = NULL; } } /** * egg_recent_model_set_filter_mime_types: * @model: A EggRecentModel object. * * Sets which mime types are allowed in the list. * * Returns: void */ void egg_recent_model_set_filter_mime_types (EggRecentModel *model, ...) { va_list valist; GSList *list = NULL; gchar *str; g_return_if_fail (model != NULL); egg_recent_model_clear_mime_filter (model); va_start (valist, model); str = va_arg (valist, gchar*); while (str != NULL) { list = g_slist_prepend (list, g_pattern_spec_new (str)); str = va_arg (valist, gchar*); } va_end (valist); model->priv->mime_filter_values = list; } static void egg_recent_model_clear_group_filter (EggRecentModel *model) { g_return_if_fail (model != NULL); if (model->priv->group_filter_values != NULL) { g_slist_foreach (model->priv->group_filter_values, (GFunc)g_free, NULL); g_slist_free (model->priv->group_filter_values); model->priv->group_filter_values = NULL; } } /** * egg_recent_model_set_filter_groups: * @model: A EggRecentModel object. * * Sets which groups are allowed in the list. * * Returns: void */ void egg_recent_model_set_filter_groups (EggRecentModel *model, ...) { va_list valist; GSList *list = NULL; gchar *str; g_return_if_fail (model != NULL); egg_recent_model_clear_group_filter (model); va_start (valist, model); str = va_arg (valist, gchar*); while (str != NULL) { list = g_slist_prepend (list, g_strdup (str)); str = va_arg (valist, gchar*); } va_end (valist); model->priv->group_filter_values = list; } static void egg_recent_model_clear_scheme_filter (EggRecentModel *model) { g_return_if_fail (model != NULL); if (model->priv->scheme_filter_values != NULL) { g_slist_foreach (model->priv->scheme_filter_values, (GFunc) g_pattern_spec_free, NULL); g_slist_free (model->priv->scheme_filter_values); model->priv->scheme_filter_values = NULL; } } /** * egg_recent_model_set_filter_uri_schemes: * @model: A EggRecentModel object. * * Sets which URI schemes (file, http, ftp, etc) are allowed in the list. * * Returns: void */ void egg_recent_model_set_filter_uri_schemes (EggRecentModel *model, ...) { va_list valist; GSList *list = NULL; gchar *str; g_return_if_fail (model != NULL); egg_recent_model_clear_scheme_filter (model); va_start (valist, model); str = va_arg (valist, gchar*); while (str != NULL) { list = g_slist_prepend (list, g_pattern_spec_new (str)); str = va_arg (valist, gchar*); } va_end (valist); model->priv->scheme_filter_values = list; } /** * egg_recent_model_set_sort: * @model: A EggRecentModel object. * @sort: A EggRecentModelSort type * * Sets the type of sorting to be used. * * Returns: void */ void egg_recent_model_set_sort (EggRecentModel *model, EggRecentModelSort sort) { g_return_if_fail (model != NULL); model->priv->sort_type = sort; } /** * egg_recent_model_changed: * @model: A EggRecentModel object. * * This function causes a "changed" signal to be emitted. * * Returns: void */ void egg_recent_model_changed (EggRecentModel *model) { GList *list = NULL; if (model->priv->limit > 0) { list = egg_recent_model_get_list (model); /* egg_recent_model_monitor_list (model, list); */ g_signal_emit (G_OBJECT (model), model_signals[CHANGED], 0, list); } if (list) EGG_RECENT_ITEM_LIST_UNREF (list); } static void egg_recent_model_remove_expired_list (EggRecentModel *model, GList *list) { time_t current_time; time_t day_seconds; time (¤t_time); day_seconds = model->priv->expire_days*24*60*60; while (list != NULL) { EggRecentItem *item = list->data; time_t timestamp; timestamp = egg_recent_item_get_timestamp (item); if ((timestamp+day_seconds) < current_time) { gchar *uri = egg_recent_item_get_uri (item); egg_recent_model_delete (model, uri); g_strdup (uri); } list = list->next; } } /** * egg_recent_model_remove_expired: * @model: A EggRecentModel object. * * Goes through the entire list, and removes any items that are older than * the user-specified expiration period. * * Returns: void */ void egg_recent_model_remove_expired (EggRecentModel *model) { FILE *file; GList *list=NULL; g_return_if_fail (model != NULL); file = egg_recent_model_open_file (model, FALSE); if (file == NULL) return; if (egg_recent_model_lock_file (file)) { list = egg_recent_model_read (model, file); } else { g_warning ("Failed to lock: %s", strerror (errno)); return; } if (!egg_recent_model_unlock_file (file)) g_warning ("Failed to unlock: %s", strerror (errno)); if (list != NULL) { egg_recent_model_remove_expired_list (model, list); EGG_RECENT_ITEM_LIST_UNREF (list); } fclose (file); } /** * egg_recent_model_get_type: * * This returns a GType representing a EggRecentModel object. * * Returns: a GType */ GType egg_recent_model_get_type (void) { static GType egg_recent_model_type = 0; if(!egg_recent_model_type) { static const GTypeInfo egg_recent_model_info = { sizeof (EggRecentModelClass), NULL, /* base init */ NULL, /* base finalize */ (GClassInitFunc)egg_recent_model_class_init, /* class init */ NULL, /* class finalize */ NULL, /* class data */ sizeof (EggRecentModel), 0, (GInstanceInitFunc) egg_recent_model_init }; egg_recent_model_type = g_type_register_static (G_TYPE_OBJECT, "EggRecentModel", &egg_recent_model_info, 0); } return egg_recent_model_type; }