diff --git a/src/document.c b/src/document.c index e144cc1d10..9fad86fca4 100644 --- a/src/document.c +++ b/src/document.c @@ -657,6 +657,8 @@ static GeanyDocument *document_create(const gchar *utf8_filename) doc->priv->last_check = time(NULL); #endif + g_datalist_init(&doc->priv->data); + sidebar_openfiles_add(doc); /* sets doc->iter */ notebook_new_tab(doc); @@ -712,6 +714,8 @@ static gboolean remove_page(guint page_num) if (! main_status.closing_all && doc->real_path != NULL) ui_add_recent_document(doc); + g_datalist_clear(&doc->priv->data); + doc->is_valid = FALSE; doc->id = 0; @@ -3839,3 +3843,22 @@ GEANY_API_SYMBOL GType document_get_type (void); G_DEFINE_BOXED_TYPE(GeanyDocument, document, copy_, free_); + + +gpointer document_get_data(const GeanyDocument *doc, const gchar *key) +{ + return g_datalist_get_data(&doc->priv->data, key); +} + + +void document_set_data(GeanyDocument *doc, const gchar *key, gpointer data) +{ + g_datalist_set_data(&doc->priv->data, key, data); +} + + +void document_set_data_full(GeanyDocument *doc, const gchar *key, + gpointer data, GDestroyNotify free_func) +{ + g_datalist_set_data_full(&doc->priv->data, key, data, free_func); +} diff --git a/src/document.h b/src/document.h index 24c5a4e8a0..ad3716b7b1 100644 --- a/src/document.h +++ b/src/document.h @@ -316,6 +316,13 @@ void document_grab_focus(GeanyDocument *doc); GeanyDocument *document_clone(GeanyDocument *old_doc); +gpointer document_get_data(const GeanyDocument *doc, const gchar *key); + +void document_set_data(GeanyDocument *doc, const gchar *key, gpointer data); + +void document_set_data_full(GeanyDocument *doc, const gchar *key, + gpointer data, GDestroyNotify free_func); + #endif /* GEANY_PRIVATE */ G_END_DECLS diff --git a/src/documentprivate.h b/src/documentprivate.h index 772e34466a..eab2f0dbaf 100644 --- a/src/documentprivate.h +++ b/src/documentprivate.h @@ -111,6 +111,8 @@ typedef struct GeanyDocumentPrivate gint protected; /* Save pointer to info bars allowing to cancel them programatically (to avoid multiple ones) */ GtkWidget *info_bars[NUM_MSG_TYPES]; + /* Keyed Data List to attach arbitrary data to the document */ + GData *data; } GeanyDocumentPrivate; diff --git a/src/plugins.c b/src/plugins.c index 938f03dc61..8ab9acbaa3 100644 --- a/src/plugins.c +++ b/src/plugins.c @@ -31,6 +31,7 @@ #include "app.h" #include "dialogs.h" +#include "documentprivate.h" #include "encodings.h" #include "geanyobject.h" #include "geanywraplabel.h" @@ -59,6 +60,14 @@ #include +typedef struct +{ + gchar *prefix; + GeanyDocument *document; +} +ForEachDocData; + + GList *active_plugin_list = NULL; /* list of only actually loaded plugins, always valid */ @@ -850,6 +859,35 @@ static gboolean is_active_plugin(Plugin *plugin) } +static void remove_each_doc_data(GQuark key_id, gpointer data, gpointer user_data) +{ + const ForEachDocData *doc_data = user_data; + const gchar *key = g_quark_to_string(key_id); + if (g_str_has_prefix(key, doc_data->prefix)) + g_datalist_remove_data(&doc_data->document->priv->data, key); +} + + +static void remove_doc_data(Plugin *plugin) +{ + ForEachDocData data; + + data.prefix = g_strdup_printf("geany/plugins/%s/", plugin->public.info->name); + + for (guint i = 0; i < documents_array->len; i++) + { + GeanyDocument *doc = documents_array->pdata[i]; + if (DOC_VALID(doc)) + { + data.document = doc; + g_datalist_foreach(&doc->priv->data, remove_each_doc_data, &data); + } + } + + g_free(data.prefix); +} + + /* Clean up anything used by an active plugin */ static void plugin_cleanup(Plugin *plugin) @@ -859,6 +897,7 @@ plugin_cleanup(Plugin *plugin) /* With geany_register_plugin cleanup is mandatory */ plugin->cbs.cleanup(&plugin->public, plugin->cb_data); + remove_doc_data(plugin); remove_callbacks(plugin); remove_sources(plugin); diff --git a/src/pluginutils.c b/src/pluginutils.c index fbbe78ba6d..546b1575b8 100644 --- a/src/pluginutils.c +++ b/src/pluginutils.c @@ -44,6 +44,14 @@ #include "utils.h" +typedef struct +{ + gpointer data; + GDestroyNotify free_func; +} +PluginDocDataProxy; + + /** Inserts a toolbar item before the Quit button, or after the previous plugin toolbar item. * A separator is added on the first call to this function, and will be shown when @a item is * shown; hidden when @a item is hidden. @@ -599,4 +607,149 @@ void geany_plugin_set_data(GeanyPlugin *plugin, gpointer pdata, GDestroyNotify f } +static void plugin_doc_data_proxy_free(gpointer pdata) +{ + PluginDocDataProxy *prox = pdata; + if (prox != NULL) + { + if (prox->free_func) + prox->free_func(prox->data); + g_slice_free(PluginDocDataProxy, prox); + } +} + + +/** + * Retrieve plugin-specific data attached to a document. + * + * @param plugin The plugin who attached the data. + * @param doc The document which the data was attached to. + * @param key The key name of the attached data. + * + * @return The attached data pointer or `NULL` if the key is not found + * for the given plugin. + * + * @since 1.29 (Plugin API 228) + * @see plugin_set_document_data plugin_set_document_data_full + */ +GEANY_API_SYMBOL +gpointer plugin_get_document_data(struct GeanyPlugin *plugin, + struct GeanyDocument *doc, const gchar *key) +{ + gchar *real_key; + PluginDocDataProxy *data; + + g_return_val_if_fail(plugin != NULL, NULL); + g_return_val_if_fail(doc != NULL, NULL); + g_return_val_if_fail(key != NULL && *key != '\0', NULL); + + real_key = g_strdup_printf("geany/plugins/%s/%s", plugin->info->name, key); + data = document_get_data(doc, real_key); + g_free(real_key); + + return (data != NULL) ? data->data : NULL; +} + + +/** + * Attach plugin-specific data to a document. + * + * @param plugin The plugin attaching data to the document. + * @param doc The document to attach the data to. + * @param key The key name for the data. + * @param data The pointer to attach to the document. + * + * @since 1.29 (Plugin API 228) + * @see plugin_get_document_data plugin_set_document_data_full + */ +GEANY_API_SYMBOL +void plugin_set_document_data(struct GeanyPlugin *plugin, struct GeanyDocument *doc, + const gchar *key, gpointer data) +{ + plugin_set_document_data_full(plugin, doc, key, data, NULL); +} + + +/** + * Attach plugin-specific data and a free function to a document. + * + * This is useful for plugins who want to keep some additional data with + * the document and even have it auto-released appropriately (see below). + * + * This is a simple example showing how a plugin might use this to + * attach a string to each document and print it when the document is + * saved: + * + * @code + * void on_document_open(GObject *unused, GeanyDocument *doc, GeanyPlugin *plugin) + * { + * plugin_set_document_data_full(plugin, doc, "my-data", + * g_strdup("some-data"), g_free); + * } + * + * void on_document_save(GObject *unused, GeanyDocument *doc, GeanyPlugin *plugin) + * { + * const gchar *some_data = plugin_get_document_data(plugin, doc, "my-data"); + * g_print("my-data: %s", some_data); + * } + * + * gboolean plugin_init(GeanyPlugin *plugin, gpointer unused) + * { + * plugin_signal_connect(plugin, NULL, "document-open", TRUE, + * G_CALLBACK(on_document_open), plugin); + * plugin_signal_connect(plugin, NULL, "document-new", TRUE, + * G_CALLBACK(on_document_open), plugin); + * plugin_signal_connect(plugin, NULL, "document-save", TRUE, + * G_CALLBACK(on_document_save), plugin); + * return TRUE; + * } + * + * void geany_load_module(GeanyPlugin *plugin) + * { + * // ... + * plugin->funcs->init = plugin_init; + * // ... + * } + * @endcode + * + * The @a free_func can be used to tie the lifetime of the data to that + * of the @a doc and/or the @a plugin. The @a free_func will be called + * in any of the following cases: + * + * - When a document is closed. + * - When the plugin is unloaded. + * - When the document data is set again using the same key. + * + * @param plugin The plugin attaching data to the document. + * @param doc The document to attach the data to. + * @param key The key name for the data. + * @param data The pointer to attach to the document. + * @param free_func The function to call with data when removed. + * + * @since 1.29 (Plugin API 228) + * @see plugin_get_document_data plugin_set_document_data + */ +GEANY_API_SYMBOL +void plugin_set_document_data_full(struct GeanyPlugin *plugin, + struct GeanyDocument *doc, const gchar *key, gpointer data, + GDestroyNotify free_func) +{ + PluginDocDataProxy *prox; + + g_return_if_fail(plugin != NULL); + g_return_if_fail(doc != NULL); + g_return_if_fail(key != NULL); + + prox = g_slice_new(PluginDocDataProxy); + if (prox != NULL) + { + gchar *real_key = g_strdup_printf("geany/plugins/%s/%s", plugin->info->name, key); + prox->data = data; + prox->free_func = free_func; + document_set_data_full(doc, real_key, prox, plugin_doc_data_proxy_free); + g_free(real_key); + } +} + + #endif diff --git a/src/pluginutils.h b/src/pluginutils.h index ccc426a567..d679e4b07b 100644 --- a/src/pluginutils.h +++ b/src/pluginutils.h @@ -33,6 +33,7 @@ G_BEGIN_DECLS /* avoid including plugindata.h otherwise this redefines the GEANY() macro */ struct GeanyPlugin; +struct GeanyDocument; void plugin_add_toolbar_item(struct GeanyPlugin *plugin, GtkToolItem *item); @@ -62,6 +63,16 @@ void plugin_show_configure(struct GeanyPlugin *plugin); void plugin_builder_connect_signals(struct GeanyPlugin *plugin, GtkBuilder *builder, gpointer user_data); +gpointer plugin_get_document_data(struct GeanyPlugin *plugin, + struct GeanyDocument *doc, const gchar *key); + +void plugin_set_document_data(struct GeanyPlugin *plugin, struct GeanyDocument *doc, + const gchar *key, gpointer data); + +void plugin_set_document_data_full(struct GeanyPlugin *plugin, + struct GeanyDocument *doc, const gchar *key, gpointer data, + GDestroyNotify free_func); + G_END_DECLS #endif /* HAVE_PLUGINS */