medit/moo/mooedit/moorecentmgr.c

675 lines
19 KiB
C

/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4; coding: utf-8 -*-
*
* moorecentmgr.c
*
* Copyright (C) 2004-2005 by Yevgen Muntyan <muntyan@math.tamu.edu>
*
* 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.
*
* See COPYING file that comes with this distribution.
*/
#define MOOEDIT_COMPILATION
#include "mooedit/mooeditprefs.h"
#include "mooedit/mooedit-private.h"
#include "mooutils/moodialogs.h"
#include "mooutils/moomarshals.h"
#include <string.h>
#define NUM_RECENT_FILES 10
typedef struct {
MooEditFileInfo *info;
} RecentEntry;
typedef struct {
GtkMenuItem *parent;
GtkMenu *menu;
gpointer data;
GSList *items; /* GtkMenuItem*, synchronized with recent files list */
} RecentMenu;
struct _MooRecentMgrPrivate {
GSList *files; /* RecentEntry* */
GSList *menus; /* RecentMenu* */
gboolean prefs_loaded;
GtkTooltips *tooltips;
gboolean display_full_name;
char *user_id;
};
static void moo_recent_mgr_class_init (MooRecentMgrClass *klass);
static void moo_recent_mgr_init (MooRecentMgr *mgr);
static void moo_recent_mgr_finalize (GObject *object);
static void moo_recent_mgr_set_property (GObject *object,
guint prop_id,
const GValue *value,
GParamSpec *pspec);
static void moo_recent_mgr_get_property (GObject *object,
guint prop_id,
GValue *value,
GParamSpec *pspec);
static RecentEntry *recent_entry_new (const MooEditFileInfo *info);
static void recent_entry_free (RecentEntry *entry);
static RecentEntry *recent_list_find (MooRecentMgr *mgr,
MooEditFileInfo *info);
static void recent_list_delete_tail(MooRecentMgr *mgr);
static GtkWidget *recent_menu_item_new (MooRecentMgr *mgr,
const MooEditFileInfo *info,
gpointer data);
static void mgr_add_recent (MooRecentMgr *mgr,
const MooEditFileInfo *info);
static void mgr_reorder_recent (MooRecentMgr *mgr,
RecentEntry *top);
static void mgr_load_recent (MooRecentMgr *mgr);
static void mgr_save_recent (MooRecentMgr *mgr);
static void menu_item_activated (GtkMenuItem *item,
MooRecentMgr *mgr);
static void menu_destroyed (GtkMenuItem *parent,
MooRecentMgr *mgr);
/* MOO_TYPE_RECENT_MGR */
G_DEFINE_TYPE (MooRecentMgr, moo_recent_mgr, G_TYPE_OBJECT)
enum {
OPEN_RECENT,
ITEM_ADDED,
NUM_SIGNALS
};
enum {
PROP_0,
PROP_USER_ID
};
static guint signals[NUM_SIGNALS];
static void
moo_recent_mgr_class_init (MooRecentMgrClass *klass)
{
GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
gobject_class->finalize = moo_recent_mgr_finalize;
gobject_class->set_property = moo_recent_mgr_set_property;
gobject_class->get_property = moo_recent_mgr_get_property;
g_object_class_install_property (gobject_class,
PROP_USER_ID,
g_param_spec_string ("user-id",
"user-id",
"user-id",
NULL,
G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY));
signals[OPEN_RECENT] =
g_signal_new ("open-recent",
G_OBJECT_CLASS_TYPE (klass),
G_SIGNAL_RUN_LAST,
G_STRUCT_OFFSET (MooRecentMgrClass, open_recent),
NULL, NULL,
_moo_marshal_VOID__BOXED_POINTER_OBJECT,
G_TYPE_NONE, 3,
MOO_TYPE_EDIT_FILE_INFO | G_SIGNAL_TYPE_STATIC_SCOPE,
G_TYPE_POINTER,
GTK_TYPE_MENU_ITEM);
signals[ITEM_ADDED] =
g_signal_new ("item-added",
G_OBJECT_CLASS_TYPE (klass),
G_SIGNAL_RUN_LAST,
G_STRUCT_OFFSET (MooRecentMgrClass, item_added),
NULL, NULL,
_moo_marshal_VOID__VOID,
G_TYPE_NONE, 0);
}
static void
moo_recent_mgr_init (MooRecentMgr *mgr)
{
mgr->priv = g_new0 (MooRecentMgrPrivate, 1);
mgr->priv->tooltips = gtk_tooltips_new ();
gtk_object_sink (GTK_OBJECT (g_object_ref (mgr->priv->tooltips)));
mgr->priv->display_full_name = FALSE;
}
static void moo_recent_mgr_set_property (GObject *object,
guint prop_id,
const GValue *value,
GParamSpec *pspec)
{
MooRecentMgr *mgr = MOO_RECENT_MGR (object);
switch (prop_id)
{
case PROP_USER_ID:
g_free (mgr->priv->user_id);
mgr->priv->user_id = g_strdup (g_value_get_string (value));
g_object_notify (object, "user-id");
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
break;
}
}
static void moo_recent_mgr_get_property (GObject *object,
guint prop_id,
GValue *value,
GParamSpec *pspec)
{
MooRecentMgr *mgr = MOO_RECENT_MGR (object);
switch (prop_id)
{
case PROP_USER_ID:
g_value_set_string (value, mgr->priv->user_id);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
break;
}
}
static void
moo_recent_mgr_finalize (GObject *object)
{
GSList *l;
MooRecentMgr *mgr = MOO_RECENT_MGR (object);
for (l = mgr->priv->files; l != NULL; l = l->next)
recent_entry_free (l->data);
g_slist_free (mgr->priv->files);
g_object_unref (mgr->priv->tooltips);
g_free (mgr->priv->user_id);
for (l = mgr->priv->menus; l != NULL; l = l->next)
{
RecentMenu *menu = l->data;
GSList *i;
g_signal_handlers_disconnect_by_func (menu->parent,
(gpointer) menu_destroyed,
mgr);
for (i = menu->items; i != NULL; i = i->next)
{
if (GTK_IS_MENU_ITEM (i->data))
g_signal_handlers_disconnect_by_func (i->data,
(gpointer) menu_item_activated, mgr);
}
g_slist_free (menu->items);
menu->items = NULL;
g_free (menu);
}
g_free (mgr->priv);
G_OBJECT_CLASS (moo_recent_mgr_parent_class)->finalize (object);
}
MooRecentMgr*
moo_recent_mgr_new (const char *user_id)
{
return g_object_new (MOO_TYPE_RECENT_MGR, "user-id", user_id, NULL);
}
typedef gboolean (*ListFoundFunc) (GtkTreeModel *model,
GtkTreeIter *iter,
gpointer data);
typedef struct {
GtkTreeIter *iter;
ListFoundFunc func;
gpointer user_data;
gboolean found;
} ListStoreFindData;
void
moo_recent_mgr_add_recent (MooRecentMgr *mgr,
MooEditFileInfo *info)
{
RecentEntry *entry;
g_return_if_fail (MOO_IS_RECENT_MGR (mgr));
g_return_if_fail (info != NULL);
g_return_if_fail (info->filename != NULL);
mgr_load_recent (mgr);
entry = recent_list_find (mgr, info);
if (!entry)
mgr_add_recent (mgr, info);
else
mgr_reorder_recent (mgr, entry);
mgr_save_recent (mgr);
}
static void
mgr_add_recent (MooRecentMgr *mgr,
const MooEditFileInfo *info)
{
GSList *l;
RecentEntry *entry = recent_entry_new (info);
gboolean delete_last = FALSE;
if (g_slist_length (mgr->priv->files) == NUM_RECENT_FILES)
delete_last = TRUE;
mgr->priv->files =
g_slist_prepend (mgr->priv->files, entry);
if (delete_last)
recent_list_delete_tail (mgr);
for (l = mgr->priv->menus; l != NULL; l = l->next)
{
RecentMenu *menu;
GtkWidget *item;
menu = l->data;
item = recent_menu_item_new (mgr, info, menu->data);
gtk_menu_shell_prepend (GTK_MENU_SHELL (menu->menu), item);
menu->items = g_slist_prepend (menu->items, item);
if (delete_last)
{
GSList *tail;
tail = g_slist_last (menu->items);
g_signal_handlers_disconnect_by_func (tail->data,
(gpointer) menu_item_activated, mgr);
gtk_container_remove (GTK_CONTAINER (menu->menu), tail->data);
menu->items = g_slist_delete_link (menu->items, tail);
}
}
g_signal_emit (mgr, signals[ITEM_ADDED], 0);
}
static void
mgr_reorder_recent (MooRecentMgr *mgr,
RecentEntry *top)
{
int index;
GSList *l;
index = g_slist_index (mgr->priv->files, top);
g_return_if_fail (index >= 0);
if (index == 0)
return;
mgr->priv->files =
g_slist_delete_link (mgr->priv->files,
g_slist_nth (mgr->priv->files, index));
mgr->priv->files =
g_slist_prepend (mgr->priv->files, top);
for (l = mgr->priv->menus; l != NULL; l = l->next)
{
RecentMenu *menu;
GtkWidget *item;
menu = l->data;
item = g_slist_nth_data (menu->items, index);
g_assert (item != NULL);
g_object_ref (item);
gtk_container_remove (GTK_CONTAINER (menu->menu), item);
gtk_menu_shell_prepend (GTK_MENU_SHELL (menu->menu), item);
g_object_unref (item);
menu->items = g_slist_delete_link (menu->items,
g_slist_nth (menu->items, index));
menu->items = g_slist_prepend (menu->items, item);
}
}
/***************************************************************************/
/* Loading and saving
*/
#define ELEMENT_RECENT_FILES "recent-files"
#define ELEMENT_ENTRY "file"
#define PROP_ENCODING "encoding"
#define PREFS_SHOW_FULL_NAME "recent_files/show_full_name"
static void
mgr_load_recent (MooRecentMgr *mgr)
{
MooMarkupDoc *xml;
MooMarkupNode *root, *node;
char *root_path;
if (mgr->priv->prefs_loaded)
return;
else
mgr->priv->prefs_loaded = TRUE;
mgr->priv->display_full_name =
moo_prefs_get_bool (moo_edit_setting (PREFS_SHOW_FULL_NAME));
xml = moo_prefs_get_markup ();
g_return_if_fail (xml != NULL);
if (mgr->priv->user_id)
root_path = g_strdup_printf ("%s/" ELEMENT_RECENT_FILES, mgr->priv->user_id);
else
root_path = g_strdup (ELEMENT_RECENT_FILES);
root = moo_markup_get_element (MOO_MARKUP_NODE (xml), root_path);
g_free (root_path);
if (!root)
return;
for (node = root->children; node != NULL; node = node->next)
{
if (!MOO_MARKUP_IS_ELEMENT (node))
continue;
if (!strcmp (node->name, ELEMENT_ENTRY))
{
MooEditFileInfo *info;
const char *encoding = moo_markup_get_prop (node, PROP_ENCODING);
const char *file_utf8 = moo_markup_get_content (node);
char *file;
if (!file_utf8 || !file_utf8[0])
{
g_warning ("%s: empty path in recent entry", G_STRLOC);
continue;
}
file = g_filename_from_utf8 (file_utf8, -1, NULL, NULL, NULL);
if (!file)
{
g_warning ("%s: could not convert '%s' to filename encoding",
G_STRLOC, file_utf8);
continue;
}
info = moo_edit_file_info_new (file, encoding);
mgr->priv->files = g_slist_append (mgr->priv->files, recent_entry_new (info));
moo_edit_file_info_free (info);
g_free (file);
}
else
{
g_warning ("%s: invalid '%s' element", G_STRLOC, node->name);
}
}
}
static void
mgr_save_recent (MooRecentMgr *mgr)
{
MooMarkupDoc *xml;
MooMarkupNode *root;
GSList *l;
char *root_path;
xml = moo_prefs_get_markup ();
g_return_if_fail (xml != NULL);
if (mgr->priv->user_id)
root_path = g_strdup_printf ("%s/" ELEMENT_RECENT_FILES, mgr->priv->user_id);
else
root_path = g_strdup (ELEMENT_RECENT_FILES);
root = moo_markup_get_element (MOO_MARKUP_NODE (xml), root_path);
if (root)
moo_markup_delete_node (root);
if (!mgr->priv->files)
{
g_free (root_path);
return;
}
root = moo_markup_create_element (MOO_MARKUP_NODE (xml), root_path);
g_return_if_fail (root != NULL);
for (l = mgr->priv->files; l != NULL; l = l->next)
{
RecentEntry *entry = l->data;
MooMarkupNode *elm;
char *path_utf8;
g_return_if_fail (entry != NULL && entry->info != NULL);
path_utf8 = g_filename_display_name (entry->info->filename);
g_return_if_fail (path_utf8 != NULL);
elm = moo_markup_create_text_element (root, ELEMENT_ENTRY, path_utf8);
if (entry->info->encoding)
moo_markup_set_prop (elm, PROP_ENCODING, entry->info->encoding);
g_free (path_utf8);
}
g_free (root_path);
}
#undef ELEMENT_RECENT_FILES
#undef ELEMENT_ENTRY
#undef PROP_ENCODING
#undef PREFS_SHOW_FULL_NAME
static gboolean
file_cmp (RecentEntry *entry,
MooEditFileInfo *info)
{
return strcmp (entry->info->filename, info->filename);
}
static RecentEntry*
recent_list_find (MooRecentMgr *mgr,
MooEditFileInfo *info)
{
GSList *l = g_slist_find_custom (mgr->priv->files,
info, (GCompareFunc) file_cmp);
if (l)
return l->data;
else
return NULL;
}
static void
recent_list_delete_tail (MooRecentMgr *mgr)
{
GSList *l = g_slist_last (mgr->priv->files);
g_return_if_fail (l != NULL);
recent_entry_free (l->data);
g_slist_delete_link (mgr->priv->files, l);
}
static GtkWidget*
recent_menu_item_new (MooRecentMgr *mgr,
const MooEditFileInfo *info,
gpointer data)
{
GtkWidget *item;
if (mgr->priv->display_full_name)
{
char *display_name = _moo_edit_filename_to_utf8 (info->filename);
item = gtk_menu_item_new_with_label (display_name);
g_free (display_name);
}
else
{
char *basename = g_path_get_basename (info->filename);
char *display_basename = _moo_edit_filename_to_utf8 (basename);
char *display_fullname = _moo_edit_filename_to_utf8 (info->filename);
item = gtk_menu_item_new_with_label (display_basename);
gtk_tooltips_set_tip (mgr->priv->tooltips, item,
display_fullname, NULL);
g_free (display_basename);
g_free (display_fullname);
g_free (basename);
}
gtk_widget_show (item);
g_signal_connect (item, "activate",
G_CALLBACK (menu_item_activated), mgr);
g_object_set_data_full (G_OBJECT (item), "moo-edit-file-mgr-recent-file",
moo_edit_file_info_copy (info),
(GDestroyNotify) moo_edit_file_info_free);
g_object_set_data (G_OBJECT (item),
"moo-edit-file-mgr-recent-file-data", data);
return item;
}
static void
menu_item_activated (GtkMenuItem *item,
MooRecentMgr *mgr)
{
MooEditFileInfo *info = g_object_get_data (G_OBJECT (item),
"moo-edit-file-mgr-recent-file");
gpointer data = g_object_get_data (G_OBJECT (item),
"moo-edit-file-mgr-recent-file-data");
g_return_if_fail (info != NULL);
g_signal_emit (mgr, signals[OPEN_RECENT], 0, info, data, item);
}
static RecentEntry*
recent_entry_new (const MooEditFileInfo *info)
{
RecentEntry *entry = g_new0 (RecentEntry, 1);
entry->info = moo_edit_file_info_copy (info);
return entry;
}
static void
recent_entry_free (RecentEntry *entry)
{
if (entry)
{
moo_edit_file_info_free (entry->info);
g_free (entry);
}
}
GtkMenuItem*
moo_recent_mgr_create_menu (MooRecentMgr *mgr,
gpointer data,
const char *menu_label)
{
RecentMenu *menu;
GSList *l;
mgr_load_recent (mgr);
menu = g_new0 (RecentMenu, 1);
menu->data = data;
menu->parent = GTK_MENU_ITEM (gtk_image_menu_item_new_with_label
(menu_label ? menu_label : "Open Recent"));
gtk_image_menu_item_set_image (GTK_IMAGE_MENU_ITEM (menu->parent),
gtk_image_new_from_stock (GTK_STOCK_OPEN,
GTK_ICON_SIZE_MENU));
menu->menu = GTK_MENU (gtk_menu_new ());
gtk_menu_item_set_submenu (menu->parent, GTK_WIDGET (menu->menu));
gtk_widget_show (GTK_WIDGET (menu->parent));
gtk_widget_show (GTK_WIDGET (menu->menu));
g_signal_connect (menu->parent, "destroy",
G_CALLBACK (menu_destroyed), mgr);
for (l = mgr->priv->files; l != NULL; l = l->next)
{
RecentEntry *entry = l->data;
GtkWidget *item = recent_menu_item_new (mgr, entry->info, data);
gtk_menu_shell_append (GTK_MENU_SHELL (menu->menu), item);
menu->items = g_slist_append (menu->items, item);
}
mgr->priv->menus = g_slist_prepend (mgr->priv->menus, menu);
return menu->parent;
}
static gboolean
menu_cmp (RecentMenu *menu,
GtkMenuItem *parent)
{
return menu->parent != parent;
}
static void
menu_destroyed (GtkMenuItem *parent,
MooRecentMgr *mgr)
{
RecentMenu *menu;
GSList *l = g_slist_find_custom (mgr->priv->menus,
parent,
(GCompareFunc) menu_cmp);
g_return_if_fail (l != NULL);
menu = l->data;
g_slist_free (menu->items);
g_free (menu);
mgr->priv->menus =
g_slist_remove (mgr->priv->menus, menu);
}
guint
moo_recent_mgr_get_num_items (MooRecentMgr *mgr)
{
g_return_val_if_fail (MOO_IS_RECENT_MGR (mgr), 0);
return g_slist_length (mgr->priv->files);
}