From de3d3b42fb603f1f695fefc7472fb246e9c34abd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Enrico=20Tr=C3=B6ger?= Date: Sun, 18 Jan 2009 18:19:58 +0000 Subject: [PATCH] Add document_save_file_as and document_rename_file to the plugin API. If GIO is available, use GFileMonitor to watch for file disk changes and indicate them immediately using an orange tab label colour. Break plugin ABI for this and the last commits. git-svn-id: https://geany.svn.sourceforge.net/svnroot/geany/trunk@3484 ea778897-0a13-0410-b9d1-a72fbfd435f5 --- ChangeLog | 6 + TODO | 1 - plugins/geanyfunctions.h | 4 + src/dialogs.c | 13 +- src/document.c | 272 ++++++++++++++++++++++++++++++++------- src/document.h | 5 +- src/documentprivate.h | 21 ++- src/plugindata.h | 8 +- src/plugins.c | 4 +- 9 files changed, 268 insertions(+), 66 deletions(-) diff --git a/ChangeLog b/ChangeLog index 6f71cab1d..dcd1ff4eb 100644 --- a/ChangeLog +++ b/ChangeLog @@ -8,6 +8,12 @@ Remove filetype O-Matrix (probably unused for years). * src/keybindings.c, src/keybindings.h: Reorder some keybindings. + * src/dialogs.c, src/document.c, src/document.h, src/documentprivate.h, + src/plugindata.h, src/plugins.c, plugins/geanyfunctions.h: + Add document_save_file_as and document_rename_file to the plugin API. + If GIO is available, use GFileMonitor to watch for file disk changes + and indicate them immediately using an orange tab label colour. + Break plugin ABI for this and the last commits. 2009-01-17 Enrico Tröger diff --git a/TODO b/TODO index 60338f38d..541c9752e 100644 --- a/TODO +++ b/TODO @@ -30,7 +30,6 @@ Note: features included in brackets have lower priority. target...) o (tango-like icons for the symbol list) o (show autocompletion symbol icons - see SCI_REGISTERIMAGE) - o (GFileMonitor support, if/when GIO gets merged with GLib) 1.0: diff --git a/plugins/geanyfunctions.h b/plugins/geanyfunctions.h index 7c7597781..6a651d2b4 100644 --- a/plugins/geanyfunctions.h +++ b/plugins/geanyfunctions.h @@ -40,6 +40,10 @@ geany_functions->p_document->close #define document_index \ geany_functions->p_document->index +#define document_save_file_as \ + geany_functions->p_document->save_file_as +#define document_rename_file \ + geany_functions->p_document->rename_file #define editor_get_indent_prefs \ geany_functions->p_editor->get_indent_prefs #define editor_create_widget \ diff --git a/src/dialogs.c b/src/dialogs.c index 5eb0c2df1..6d86e0242 100644 --- a/src/dialogs.c +++ b/src/dialogs.c @@ -382,8 +382,7 @@ static void on_save_as_new_tab_toggled(GtkToggleButton *togglebutton, gpointer u #if ! GEANY_USE_WIN32_DIALOG -static void handle_save_as(const gchar *utf8_filename, gboolean open_new_tab, - gboolean rename_file) +static void handle_save_as(const gchar *utf8_filename, gboolean open_new_tab, gboolean rename_file) { GeanyDocument *doc = document_get_current(); @@ -400,22 +399,16 @@ static void handle_save_as(const gchar *utf8_filename, gboolean open_new_tab, { if (rename_file) { - gchar *old_filename = utils_get_locale_from_utf8(doc->file_name); - gchar *new_filename = utils_get_locale_from_utf8(utf8_filename); - - g_rename(old_filename, new_filename); - g_free(old_filename); - g_free(new_filename); + document_rename_file(doc, utf8_filename); } /* create a new tm_source_file object otherwise tagmanager won't work correctly */ tm_workspace_remove_object(doc->tm_file, TRUE, TRUE); doc->tm_file = NULL; } document_save_file_as(doc, utf8_filename); - } - if (! open_new_tab) build_menu_update(doc); + } } diff --git a/src/document.c b/src/document.c index 48a726ca2..0727cd287 100644 --- a/src/document.c +++ b/src/document.c @@ -47,6 +47,10 @@ /* gstdio.h also includes sys/stat.h */ #include +#ifdef HAVE_GIO +# include +#endif + #include "document.h" #include "documentprivate.h" #include "filetypes.h" @@ -337,9 +341,7 @@ static void init_doc_struct(GeanyDocument *new_doc) new_doc->encoding = NULL; new_doc->has_bom = FALSE; new_doc->editor = NULL; - new_doc->mtime = 0; new_doc->changed = FALSE; - new_doc->last_check = time(NULL); new_doc->real_path = NULL; new_doc->priv = g_new0(GeanyDocumentPrivate, 1); @@ -384,6 +386,97 @@ static void queue_colourise(GeanyDocument *doc) } +#ifdef HAVE_GIO +static void file_monitor_changed_cb(G_GNUC_UNUSED GFileMonitor *monitor, G_GNUC_UNUSED GFile *file, + G_GNUC_UNUSED GFile *other_file, GFileMonitorEvent event, GeanyDocument *doc) +{ + if (file_prefs.disk_check_timeout == 0) + return; + + switch (event) + { + case G_FILE_MONITOR_EVENT_CHANGED: + { + if (doc->priv->file_disk_status == FILE_IGNORE) + { /* ignore this change completely, used after saving a file */ + doc->priv->file_disk_status = FILE_OK; + return; + } + doc->priv->file_disk_status = FILE_CHANGED; + break; + } + case G_FILE_MONITOR_EVENT_DELETED: + { + doc->priv->file_disk_status = FILE_MISSING; + break; + } + /* ignore */ + case G_FILE_MONITOR_EVENT_CHANGES_DONE_HINT: + case G_FILE_MONITOR_EVENT_ATTRIBUTE_CHANGED: + case G_FILE_MONITOR_EVENT_PRE_UNMOUNT: + case G_FILE_MONITOR_EVENT_UNMOUNTED: + /* we ignore 'created' for now since it causes trouble when renaming files */ + case G_FILE_MONITOR_EVENT_CREATED: + return; + } + + if (doc->priv->file_disk_status != FILE_OK) + { + ui_update_tab_status(doc); + } +} +#endif + + +void document_stop_file_monitoring(GeanyDocument *doc) +{ + g_return_if_fail(doc != NULL); + + if (doc->priv->monitor != NULL) + { + g_object_unref(doc->priv->monitor); + doc->priv->monitor = NULL; + } +} + + +static void file_monitor_setup(GeanyDocument *doc) +{ + g_return_if_fail(doc != NULL); + /* Disable file monitoring completely for remote files (i.e. remote GIO files) as GFileMonitor + * doesn't work at all for remote files and legacy polling is too slow. */ + if (! doc->priv->is_remote) + { +#ifdef HAVE_GIO + gchar *locale_filename; + GFile *file; + + /* stop any previous monitoring */ + document_stop_file_monitoring(doc); + + locale_filename = utils_get_locale_from_utf8(doc->file_name); + if (locale_filename != NULL && g_file_test(locale_filename, G_FILE_TEST_EXISTS)) + { + /* get a file monitor and connect to the 'changed' signal */ + file = g_file_new_for_path(locale_filename); + doc->priv->monitor = g_file_monitor_file(file, G_FILE_MONITOR_NONE, NULL, NULL); + g_signal_connect(doc->priv->monitor, "changed", G_CALLBACK(file_monitor_changed_cb), doc); + + /* we set the rate limit according to the GUI pref but it's most probably not used */ + g_file_monitor_set_rate_limit(doc->priv->monitor, file_prefs.disk_check_timeout * 1000); + + g_object_unref(file); + } + g_free(locale_filename); +#else + doc->priv->last_check = time(NULL); + doc->priv->mtime = 0; +#endif + } + doc->priv->file_disk_status = FILE_OK; +} + + /* Creates a new document and editor, adding a tab in the notebook. * @return The created document */ static GeanyDocument *document_create(const gchar *utf8_filename) @@ -438,7 +531,6 @@ static GeanyDocument *document_create(const gchar *utf8_filename) return this; } - /** * Close the given document. * @@ -497,6 +589,8 @@ gboolean document_remove_page(guint page_num) editor_destroy(doc->editor); doc->editor = NULL; + document_stop_file_monitoring(doc); + doc->is_valid = FALSE; doc->file_name = NULL; doc->real_path = NULL; @@ -571,7 +665,7 @@ GeanyDocument *document_new_file(const gchar *utf8_filename, GeanyFiletype *ft, sci_set_undo_collection(doc->editor->sci, TRUE); sci_empty_undo_buffer(doc->editor->sci); - doc->mtime = time(NULL); + doc->priv->mtime = time(NULL); doc->encoding = g_strdup(encodings[file_prefs.default_new_encoding].charset); /* store the opened encoding for undo/redo */ @@ -593,6 +687,8 @@ GeanyDocument *document_new_file(const gchar *utf8_filename, GeanyFiletype *ft, sci_set_line_numbers(doc->editor->sci, editor_prefs.show_linenumber_margin, 0); sci_goto_pos(doc->editor->sci, 0, TRUE); + file_monitor_setup(doc); + /* "the" SCI signal (connect after initial setup(i.e. adding text)) */ g_signal_connect(doc->editor->sci, "sci-notify", G_CALLBACK(editor_sci_notify_cb), doc->editor); @@ -1082,6 +1178,7 @@ GeanyDocument *document_open_file_full(GeanyDocument *doc, const gchar *filename g_return_val_if_fail(doc != NULL, NULL); /* really should not happen */ doc->priv->is_remote = utils_is_remote_path(locale_filename); + file_monitor_setup(doc); } sci_set_undo_collection(doc->editor->sci, FALSE); /* avoid creation of an undo action */ @@ -1099,7 +1196,7 @@ GeanyDocument *document_open_file_full(GeanyDocument *doc, const gchar *filename sci_set_undo_collection(doc->editor->sci, TRUE); - doc->mtime = filedata.mtime; /* get the modification time from file and keep it */ + doc->priv->mtime = filedata.mtime; /* get the modification time from file and keep it */ g_free(doc->encoding); /* if reloading, free old encoding */ doc->encoding = filedata.enc; doc->has_bom = filedata.bom; @@ -1252,6 +1349,7 @@ gboolean document_reload_file(GeanyDocument *doc, const gchar *forced_enc) static gboolean document_update_timestamp(GeanyDocument *doc) { +#ifndef HAVE_GIO struct stat st; gchar *locale_filename; @@ -1266,8 +1364,9 @@ static gboolean document_update_timestamp(GeanyDocument *doc) return FALSE; } - doc->mtime = st.st_mtime; /* get the modification time from file and keep it */ + doc->priv->mtime = st.st_mtime; /* get the modification time from file and keep it */ g_free(locale_filename); +#endif return TRUE; } @@ -1326,22 +1425,47 @@ static void replace_header_filename(GeanyDocument *doc) } -/* - * Save the %document, detecting the filetype. +/** + * Renames the file in @a doc to @a new_filename. Only the file on disk is actually renamed, + * you still have to call @ref document_save_file_as() to change the @a doc object. + * It also stops monitoring for file changes to prevent receiving too many file change events + * while renaming. File monitoring is setup again in @ref document_save_file_as(). + * + * @param doc The current document which should be renamed. + * @param new_filename The new filename in UTF-8 encoding. + */ +void document_rename_file(GeanyDocument *doc, const gchar *new_filename) +{ + gchar *old_locale_filename = utils_get_locale_from_utf8(doc->file_name); + gchar *new_locale_filename = utils_get_locale_from_utf8(new_filename); + + /* stop file monitoring to avoid getting events for deleting/creating files, + * it's re-setup in document_save_file_as() */ + document_stop_file_monitoring(doc); + + g_rename(old_locale_filename, new_locale_filename); + + g_free(old_locale_filename); + g_free(new_locale_filename); +} + + +/** + * Saves the document, detecting the filetype. * * @param doc The document for the file to save. * @param utf8_fname The new name for the document, in UTF-8, or NULL. * @return @c TRUE if the file was saved or @c FALSE if the file could not be saved. + * * @see document_save_file(). */ gboolean document_save_file_as(GeanyDocument *doc, const gchar *utf8_fname) { gboolean ret; - if (doc == NULL) - return FALSE; + g_return_val_if_fail(doc != NULL, FALSE); - if (utf8_fname) + if (utf8_fname != NULL) { g_free(doc->file_name); doc->file_name = g_strdup(utf8_fname); @@ -1365,6 +1489,12 @@ gboolean document_save_file_as(GeanyDocument *doc, const gchar *utf8_fname) replace_header_filename(doc); ret = document_save_file(doc, TRUE); + + /* file monitoring support, add file monitoring after the file has been saved + * to ignore any earlier events */ + file_monitor_setup(doc); + doc->priv->file_disk_status = FILE_IGNORE; + if (ret) ui_add_recent_file(doc->file_name); return ret; @@ -1551,6 +1681,9 @@ gboolean document_save_file(GeanyDocument *doc, gboolean force) err = write_data_to_disk(doc, data, len); g_free(data); + /* ignore file changed notification after writing the file */ + doc->priv->file_disk_status = FILE_IGNORE; + if (err != 0) { ui_set_statusbar(TRUE, _("Error saving file (%s)."), g_strerror(err)); @@ -2472,6 +2605,7 @@ GdkColor *document_get_status_color(GeanyDocument *doc) { static GdkColor red = {0, 0xFFFF, 0, 0}; static GdkColor green = {0, 0, 0x7FFF, 0}; + static GdkColor orange = {0, 0xFFFF, 0x7FFF, 0}; GdkColor *color = NULL; if (doc == NULL) @@ -2479,6 +2613,11 @@ GdkColor *document_get_status_color(GeanyDocument *doc) if (doc->changed) color = &red; + else if (doc->priv->file_disk_status == FILE_MISSING || + doc->priv->file_disk_status == FILE_CHANGED) + { + color = &orange; + } else if (doc->readonly) color = &green; @@ -2606,62 +2745,103 @@ static gboolean check_reload(GeanyDocument *doc) } +static gboolean check_resave_missing_file(GeanyDocument *doc) +{ + gboolean want_reload = FALSE; + + /* file is missing - set unsaved state */ + document_set_text_changed(doc, TRUE); + /* don't prompt more than once */ + setptr(doc->real_path, NULL); + + if (dialogs_show_question_full(NULL, GTK_STOCK_SAVE, GTK_STOCK_CANCEL, + _("Try to resave the file?"), + _("File \"%s\" was not found on disk!"), doc->file_name)) + { + dialogs_show_save_as(); + want_reload = TRUE; + } + return want_reload; +} + + +static time_t check_disk_status_real(GeanyDocument *doc, gboolean force) +{ + time_t t = 0; +#ifndef HAVE_GIO + struct stat st; + gchar *locale_filename; + + t = time(NULL); + + if (! force && doc->priv->last_check > (t - file_prefs.disk_check_timeout)) + return 0; + + doc->priv->last_check = t; + + locale_filename = utils_get_locale_from_utf8(doc->file_name); + if (g_stat(locale_filename, &st) != 0) + { + doc->priv->file_disk_status = FILE_MISSING; + return 0; + } + else if (doc->priv->mtime > t || st.st_mtime > t) + { + g_warning("%s: Something is wrong with the time stamps.", __func__); + } + else if (doc->priv->mtime < st.st_mtime) + { + doc->priv->file_disk_status = FILE_CHANGED; + /* return st.st_mtime to set it after the file has been possibly reloaded */ + t = st.st_mtime; + } + g_free(locale_filename); +#endif + return t; +} + + /* Set force to force a disk check, otherwise it is ignored if there was a check * in the last file_prefs.disk_check_timeout seconds. * @return @c TRUE if the file has changed. */ gboolean document_check_disk_status(GeanyDocument *doc, gboolean force) { - struct stat st; - time_t t; - gchar *locale_filename; gboolean ret = FALSE; + time_t t; if (file_prefs.disk_check_timeout == 0) return FALSE; if (doc == NULL) return FALSE; /* ignore documents that have never been saved to disk */ - if (doc->real_path == NULL) return FALSE; - - t = time(NULL); - - if (! force && doc->last_check > (t - file_prefs.disk_check_timeout)) + if (doc->real_path == NULL) return FALSE; - doc->last_check = t; + /* check the file's mtime in case we don't have GIO support, otherwise this is a no-op */ + t = check_disk_status_real(doc, force); - locale_filename = utils_get_locale_from_utf8(doc->file_name); - if (g_stat(locale_filename, &st) != 0) + switch (doc->priv->file_disk_status) { - /* file is missing - set unsaved state */ - document_set_text_changed(doc, TRUE); - /* don't prompt more than once */ - setptr(doc->real_path, NULL); - - if (dialogs_show_question_full(NULL, GTK_STOCK_SAVE, GTK_STOCK_CANCEL, - _("Try to resave the file?"), - _("File \"%s\" was not found on disk!"), doc->file_name)) + case FILE_CHANGED: { - dialogs_show_save_as(); + check_reload(doc); + doc->priv->mtime = t; + ret = TRUE; + break; } - } - else if (doc->mtime > t || st.st_mtime > t) - { - geany_debug("Strange: Something is wrong with the time stamps."); - } - else if (doc->mtime < st.st_mtime) - { - if (check_reload(doc)) + case FILE_MISSING: { - /* Update the modification time */ - doc->mtime = st.st_mtime; + check_resave_missing_file(doc); + ret = TRUE; + break; } - else - doc->mtime = st.st_mtime; /* Ignore this change on disk completely */ - - ret = TRUE; /* file has changed */ + default: + break; } - g_free(locale_filename); + + doc->priv->file_disk_status = FILE_OK; + ui_update_tab_status(doc); + return ret; } diff --git a/src/document.h b/src/document.h index bf8d2db7d..9d9d378c1 100644 --- a/src/document.h +++ b/src/document.h @@ -93,10 +93,6 @@ struct GeanyDocument gboolean readonly; /** Whether this %document has been changed since it was last saved. */ gboolean changed; - /** Time of the last disk check. */ - time_t last_check; - /** Modification time of this %document on disk. */ - time_t mtime; /** The link-dereferenced, locale-encoded file name. * If non-NULL, this indicates the file once existed on disk (not just as an * unsaved document with a filename set). @@ -157,6 +153,7 @@ void document_set_text_changed(GeanyDocument *doc, gboolean changed); void document_set_filetype(GeanyDocument *doc, GeanyFiletype *type); +void document_rename_file(GeanyDocument *doc, const gchar *new_filename); GeanyDocument *document_index(gint idx); diff --git a/src/documentprivate.h b/src/documentprivate.h index 21a58eaeb..40ab22c4f 100644 --- a/src/documentprivate.h +++ b/src/documentprivate.h @@ -26,6 +26,7 @@ #ifndef GEANY_DOCUMENT_PRIVATE_H #define GEANY_DOCUMENT_PRIVATE_H + /* available UNDO actions, UNDO_SCINTILLA is a pseudo action to trigger Scintilla's * undo management */ enum @@ -36,6 +37,15 @@ enum UNDO_ACTIONS_MAX }; +typedef enum +{ + FILE_OK, + FILE_CHANGED, + FILE_MISSING, + FILE_IGNORE +} +FileDiskStatus; + typedef struct FileEncoding { @@ -69,7 +79,16 @@ typedef struct GeanyDocumentPrivate gint symbol_list_sort_mode; /* indicates whether a file is on a remote filesystem, works only with GIO/GVFS */ gboolean is_remote; - } + /* File status on disk of the document, can be 'FILE_CHANGED', 'FILE_MISSING' (deleted) or + * 'FILE_OK' if there are no known changes */ + FileDiskStatus file_disk_status; + /* Reference to a GFileMonitor object, only used when GIO file monitoring is used. */ + gpointer monitor; + /* Time of the last disk check, only used when legacy file monitoring is used. */ + time_t last_check; + /* Modification time of the document on disk, only used when legacy file monitoring is used. */ + time_t mtime; +} GeanyDocumentPrivate; #endif diff --git a/src/plugindata.h b/src/plugindata.h index dc93fbfb3..1ff353273 100644 --- a/src/plugindata.h +++ b/src/plugindata.h @@ -45,13 +45,13 @@ enum { /** The Application Programming Interface (API) version, incremented * whenever any plugin data types are modified or appended to. */ - GEANY_API_VERSION = 125, + GEANY_API_VERSION = 126, /** The Application Binary Interface (ABI) version, incremented whenever * existing fields in the plugin data types have to be changed or reordered. */ /* This should usually stay the same if fields are only appended, assuming only pointers to * structs and not structs themselves are declared by plugins. */ - GEANY_ABI_VERSION = 56 + GEANY_ABI_VERSION = 57 }; /** Check the plugin can be loaded by Geany. @@ -248,8 +248,10 @@ typedef struct DocumentFuncs void (*set_encoding) (struct GeanyDocument *doc, const gchar *new_encoding); void (*set_text_changed) (struct GeanyDocument *doc, gboolean changed); void (*set_filetype) (struct GeanyDocument *doc, struct GeanyFiletype *type); - gboolean (*close) (GeanyDocument *doc); + gboolean (*close) (struct GeanyDocument *doc); struct GeanyDocument* (*index)(gint idx); + gboolean (*save_file_as) (struct GeanyDocument *doc, const gchar *utf8_fname); + void (*rename_file) (struct GeanyDocument *doc, const gchar *new_filename); } DocumentFuncs; diff --git a/src/plugins.c b/src/plugins.c index bb3b4ecfd..996cb891f 100644 --- a/src/plugins.c +++ b/src/plugins.c @@ -131,7 +131,9 @@ static DocumentFuncs doc_funcs = { &document_set_text_changed, &document_set_filetype, &document_close, - &document_index + &document_index, + &document_save_file_as, + &document_rename_file }; static EditorFuncs editor_funcs = {