Merge branch 'kugel/document-messages2'

This is an extension of the long-lived `document-messages` branch.

It is mostly adjusted according to the feedback on the mailing list.
There might existing some usability issues to be resolved, but this
is the first step towards non-modal document notifications.
This commit is contained in:
Matthew Brush 2014-05-25 18:55:07 -07:00
commit 8ea54993c5
3 changed files with 336 additions and 56 deletions

View File

@ -74,6 +74,7 @@
/*#define USE_GIO_FILEMON 1*/
#include <gio/gio.h>
#include <gdk/gdkkeysyms.h>
GeanyFilePrefs file_prefs;
@ -220,10 +221,47 @@ GeanyDocument *document_find_by_sci(ScintillaObject *sci)
* @since 0.19 */
gint document_get_notebook_page(GeanyDocument *doc)
{
GtkWidget *parent;
g_return_val_if_fail(doc != NULL, -1);
return gtk_notebook_page_num(GTK_NOTEBOOK(main_widgets.notebook),
GTK_WIDGET(doc->editor->sci));
parent = gtk_widget_get_parent(GTK_WIDGET(doc->editor->sci));
g_return_val_if_fail(GTK_IS_BOX(parent), -1);
return gtk_notebook_page_num(GTK_NOTEBOOK(main_widgets.notebook), parent);
}
/*
* Recursively searches a containers children until it finds a
* Scintilla widget, or NULL if one was not found.
*/
static ScintillaObject *locate_sci_in_container(GtkWidget *container)
{
ScintillaObject *sci = NULL;
GList *children, *iter;
g_return_val_if_fail(GTK_IS_CONTAINER(container), NULL);
children = gtk_container_get_children(GTK_CONTAINER(container));
for (iter = children; iter != NULL; iter = g_list_next(iter))
{
if (IS_SCINTILLA(iter->data))
{
sci = SCINTILLA(iter->data);
break;
}
else if (GTK_IS_CONTAINER(iter->data))
{
sci = locate_sci_in_container(iter->data);
if (IS_SCINTILLA(sci))
break;
sci = NULL;
}
}
g_list_free(children);
return sci;
}
@ -236,13 +274,17 @@ gint document_get_notebook_page(GeanyDocument *doc)
**/
GeanyDocument *document_get_from_page(guint page_num)
{
GtkWidget *parent;
ScintillaObject *sci;
if (page_num >= documents_array->len)
return NULL;
sci = (ScintillaObject*)gtk_notebook_get_nth_page(
GTK_NOTEBOOK(main_widgets.notebook), page_num);
parent = gtk_notebook_get_nth_page(GTK_NOTEBOOK(main_widgets.notebook), page_num);
g_return_val_if_fail(GTK_IS_BOX(parent), NULL);
sci = locate_sci_in_container(parent);
g_return_val_if_fail(IS_SCINTILLA(sci), NULL);
return document_find_by_sci(sci);
}
@ -255,14 +297,21 @@ GeanyDocument *document_get_from_page(guint page_num)
**/
GeanyDocument *document_get_current(void)
{
gint cur_page = gtk_notebook_get_current_page(GTK_NOTEBOOK(main_widgets.notebook));
gint cur_page;
GtkWidget *parent;
ScintillaObject *sci;
cur_page = gtk_notebook_get_current_page(GTK_NOTEBOOK(main_widgets.notebook));
if (cur_page == -1)
return NULL;
else
{
ScintillaObject *sci = (ScintillaObject*)
gtk_notebook_get_nth_page(GTK_NOTEBOOK(main_widgets.notebook), cur_page);
parent = gtk_notebook_get_nth_page(GTK_NOTEBOOK(main_widgets.notebook), cur_page);
g_return_val_if_fail(GTK_IS_BOX(parent), NULL);
sci = locate_sci_in_container(parent);
g_return_val_if_fail(IS_SCINTILLA(sci), NULL);
return document_find_by_sci(sci);
}
@ -1190,6 +1239,7 @@ GeanyDocument *document_open_file_full(GeanyDocument *doc, const gchar *filename
doc->readonly = readonly || filedata.readonly;
sci_set_readonly(doc->editor->sci, doc->readonly);
doc->priv->protected = 0;
/* update line number margin width */
doc->priv->line_count = sci_get_line_count(doc->editor->sci);
@ -1322,6 +1372,10 @@ gboolean document_reload_file(GeanyDocument *doc, const gchar *forced_enc)
g_return_val_if_fail(doc != NULL, FALSE);
/* Use cancel because the response handler would call this recursively */
if (doc->priv->info_bars[MSG_TYPE_RELOAD] != NULL)
gtk_info_bar_response(GTK_INFO_BAR(doc->priv->info_bars[MSG_TYPE_RELOAD]), GTK_RESPONSE_CANCEL);
/* try to set the cursor to the position before reloading */
pos = sci_get_current_position(doc->editor->sci);
new_doc = document_open_file_full(doc, NULL, pos, doc->readonly, doc->file_type, forced_enc);
@ -1443,6 +1497,22 @@ void document_rename_file(GeanyDocument *doc, const gchar *new_filename)
}
static void protect_document(GeanyDocument *doc)
{
/* do not call queue_colourise because to we want to keep the text-changed indication! */
if (!doc->priv->protected++)
sci_set_readonly(doc->editor->sci, TRUE);
}
static void unprotect_document(GeanyDocument *doc)
{
g_return_if_fail(doc->priv->protected > 0);
if (!--doc->priv->protected && doc->readonly == FALSE)
sci_set_readonly(doc->editor->sci, FALSE);
}
/* Return TRUE if the document doesn't have a full filename set.
* This makes filenames without a path show the save as dialog, e.g. for file templates.
* Otherwise just use the set filename instead of asking the user - e.g. for command-line
@ -1469,9 +1539,11 @@ gboolean document_need_save_as(GeanyDocument *doc)
gboolean document_save_file_as(GeanyDocument *doc, const gchar *utf8_fname)
{
gboolean ret;
gboolean new_file;
g_return_val_if_fail(doc != NULL, FALSE);
new_file = document_need_save_as(doc) || (utf8_fname != NULL && !strcmp(doc->file_name, utf8_fname));
if (utf8_fname != NULL)
SETPTR(doc->file_name, g_strdup(utf8_fname));
@ -1491,6 +1563,15 @@ gboolean document_save_file_as(GeanyDocument *doc, const gchar *utf8_fname)
ignore_callback = FALSE;
}
}
if (new_file)
{
sci_set_readonly(doc->editor->sci, FALSE);
doc->readonly = FALSE;
if (doc->priv->protected > 0)
unprotect_document(doc);
}
replace_header_filename(doc);
ret = document_save_file(doc, TRUE);
@ -1681,7 +1762,6 @@ static gchar *save_doc(GeanyDocument *doc, const gchar *locale_filename,
return NULL;
}
/**
* Saves the document.
* Also shows the Save As dialog if necessary.
@ -1725,7 +1805,9 @@ gboolean document_save_file(GeanyDocument *doc, gboolean force)
}
/* the "changed" flag should exclude the "readonly" flag, but check it anyway for safety */
if (! force && (! doc->changed || doc->readonly))
if (doc->readonly || doc->priv->protected)
return FALSE;
if (!force && !doc->changed)
return FALSE;
fp = project_get_file_prefs();
@ -2880,6 +2962,7 @@ GeanyDocument *document_clone(GeanyDocument *old_doc)
old_doc->editor->indent_width);
doc->readonly = old_doc->readonly;
doc->has_bom = old_doc->has_bom;
doc->priv->protected = 0;
document_set_encoding(doc, old_doc->encoding);
sci_set_lines_wrapped(doc->editor->sci, doc->editor->line_wrapping);
sci_set_readonly(doc->editor->sci, doc->readonly);
@ -2952,59 +3035,240 @@ gboolean document_close_all(void)
}
/* *
* Shows a message related to a document.
*
* Use this whenever the user needs to see a document-related message,
* for example when the file was externally modified or deleted.
*
* Any of the buttons can be @c NULL. If not @c NULL, @a btn_1's
* @a response_1 response will be the default for the @c GtkInfoBar or
* @c GtkDialog.
*
* @param doc @c GeanyDocument.
* @param msgtype The type of message.
* @param response_cb A callback function called when there's a response.
* @param btn_1 The first action area button.
* @param response_1 The response for @a btn_1.
* @param btn_2 The second action area button.
* @param response_2 The response for @a btn_2.
* @param btn_3 The third action area button.
* @param response_3 The response for @a btn_3.
* @param extra_text Text to show below the main message.
* @param format The text format for the main message.
* @param ... Used with @a format as in @c printf.
*
* @since 1.25
* */
static GtkWidget* document_show_message(GeanyDocument *doc, GtkMessageType msgtype,
void (*response_cb)(GtkWidget *info_bar, gint response_id, GeanyDocument *doc),
const gchar *btn_1, GtkResponseType response_1,
const gchar *btn_2, GtkResponseType response_2,
const gchar *btn_3, GtkResponseType response_3,
const gchar *extra_text, const gchar *format, ...)
{
va_list args;
gchar *text, *markup;
GtkWidget *hbox, *vbox, *icon, *label, *extra_label, *content_area;
GtkWidget *info_widget, *ok_button, *cancel_button, *parent;
parent = gtk_notebook_get_nth_page(GTK_NOTEBOOK(main_widgets.notebook),
document_get_notebook_page(doc));
va_start(args, format);
text = g_strdup_vprintf(format, args);
va_end(args);
markup = g_strdup_printf("<span size=\"larger\">%s</span>", text);
g_free(text);
info_widget = gtk_info_bar_new();
/* must be done now else Gtk-WARNING: widget not within a GtkWindow */
gtk_box_pack_start(GTK_BOX(parent), info_widget, FALSE, TRUE, 0);
gtk_info_bar_set_message_type(GTK_INFO_BAR(info_widget), msgtype);
if (btn_1)
gtk_info_bar_add_button(GTK_INFO_BAR(info_widget), btn_1, response_1);
if (btn_2)
gtk_info_bar_add_button(GTK_INFO_BAR(info_widget), btn_2, response_2);
if (btn_3)
gtk_info_bar_add_button(GTK_INFO_BAR(info_widget), btn_3, response_3);
content_area = gtk_info_bar_get_content_area(GTK_INFO_BAR(info_widget));
label = gtk_label_new(NULL);
gtk_misc_set_alignment(GTK_MISC(label), 0.0, 0.5);
gtk_label_set_markup(GTK_LABEL(label), markup);
g_free(markup);
g_signal_connect(info_widget, "response", G_CALLBACK(response_cb), doc);
g_signal_connect_after(info_widget, "response", G_CALLBACK(gtk_widget_destroy), NULL);
hbox = gtk_hbox_new(FALSE, 12);
gtk_container_add(GTK_CONTAINER(content_area), hbox);
switch (msgtype)
{
case GTK_MESSAGE_INFO:
icon = gtk_image_new_from_stock(GTK_STOCK_DIALOG_INFO, GTK_ICON_SIZE_DIALOG);
break;
case GTK_MESSAGE_WARNING:
icon = gtk_image_new_from_stock(GTK_STOCK_DIALOG_WARNING, GTK_ICON_SIZE_DIALOG);
break;
case GTK_MESSAGE_QUESTION:
icon = gtk_image_new_from_stock(GTK_STOCK_DIALOG_QUESTION, GTK_ICON_SIZE_DIALOG);
break;
case GTK_MESSAGE_ERROR:
icon = gtk_image_new_from_stock(GTK_STOCK_DIALOG_ERROR, GTK_ICON_SIZE_DIALOG);
break;
default:
icon = NULL;
break;
}
if (icon)
gtk_box_pack_start(GTK_BOX(hbox), icon, FALSE, TRUE, 0);
if (extra_text)
{
vbox = gtk_vbox_new(FALSE, 6);
extra_label = gtk_label_new(extra_text);
gtk_misc_set_alignment(GTK_MISC(extra_label), 0.0, 0.5);
gtk_box_pack_start(GTK_BOX(vbox), label, TRUE, TRUE, 0);
gtk_box_pack_start(GTK_BOX(vbox), extra_label, TRUE, TRUE, 0);
gtk_container_add(GTK_CONTAINER(hbox), vbox);
}
else
gtk_container_add(GTK_CONTAINER(hbox), label);
gtk_box_reorder_child(GTK_BOX(parent), info_widget, 0);
gtk_widget_show_all(info_widget);
return info_widget;
}
static void on_monitor_reload_file_response(GtkWidget *bar, gint response_id, GeanyDocument *doc)
{
unprotect_document(doc);
doc->priv->info_bars[MSG_TYPE_RELOAD] = NULL;
if (response_id == GTK_RESPONSE_ACCEPT)
document_reload_file(doc, doc->encoding);
}
static gboolean on_sci_key(GtkWidget *w, GdkEventKey *event, gpointer data)
{
GtkInfoBar *bar = GTK_INFO_BAR(data);
g_return_val_if_fail(event->type == GDK_KEY_PRESS, FALSE);
switch (event->keyval)
{
case GDK_KEY_Tab:
case GDK_KEY_ISO_Left_Tab:
{
GtkWidget *w = gtk_info_bar_get_action_area(bar);
GtkDirectionType dir = event->keyval == GDK_KEY_Tab ? GTK_DIR_TAB_FORWARD : GTK_DIR_TAB_BACKWARD;
gtk_widget_child_focus(w, dir);
return TRUE;
}
case GDK_KEY_Escape:
{
gtk_info_bar_response(bar, GTK_RESPONSE_CANCEL);
return TRUE;
}
default:
return FALSE;
}
}
/* g_signal_handlers_disconnect_by_data is a macro that cannot be used as GCallback */
static gint nonmacro_g_signal_handlers_disconnect_by_data(gpointer instance, gpointer data)
{
return g_signal_handlers_disconnect_by_data(instance, data);
}
static void enable_key_intercept(GeanyDocument *doc, GtkWidget *bar)
{
g_signal_connect(doc->editor->sci, "key-press-event", G_CALLBACK(on_sci_key), bar);
/* make the signal disconnect automatically */
g_signal_connect_swapped(bar, "unrealize",
G_CALLBACK(nonmacro_g_signal_handlers_disconnect_by_data), doc->editor->sci);
}
static void monitor_reload_file(GeanyDocument *doc)
{
gchar *base_name = g_path_get_basename(doc->file_name);
gint ret;
/* we use No instead of Cancel to avoid mnemonic clash */
ret = dialogs_show_prompt(NULL,
GTK_STOCK_CLOSE, GTK_RESPONSE_CLOSE,
GTK_STOCK_NO, GTK_RESPONSE_CANCEL,
_("_Reload"), GTK_RESPONSE_ACCEPT,
_("Do you want to reload it?"),
_("The file '%s' on the disk is more recent than\nthe current buffer."),
base_name);
/* show this message only once */
if (doc->priv->info_bars[MSG_TYPE_RELOAD] == NULL)
{
GtkWidget *bar;
bar = document_show_message(doc, GTK_MESSAGE_QUESTION, on_monitor_reload_file_response,
_("_Reload"), GTK_RESPONSE_ACCEPT,
GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL,
NULL, GTK_RESPONSE_NONE,
_("Do you want to reload it?"),
_("The file '%s' on the disk is more recent than the current buffer."),
base_name);
protect_document(doc);
doc->priv->info_bars[MSG_TYPE_RELOAD] = bar;
enable_key_intercept(doc, bar);
}
g_free(base_name);
if (ret == GTK_RESPONSE_ACCEPT)
document_reload_file(doc, doc->encoding);
else if (ret == GTK_RESPONSE_CLOSE)
document_close(doc);
}
static gboolean monitor_resave_missing_file(GeanyDocument *doc)
static void on_monitor_resave_missing_file_response(GtkWidget *bar,
gint response_id,
GeanyDocument *doc)
{
gboolean want_reload = FALSE;
gboolean file_saved = FALSE;
gint ret;
ret = dialogs_show_prompt(NULL,
_("Close _without saving"), GTK_RESPONSE_CLOSE,
GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL,
GTK_STOCK_SAVE, GTK_RESPONSE_ACCEPT,
_("Try to resave the file?"),
_("File \"%s\" was not found on disk!"),
doc->file_name);
if (ret == GTK_RESPONSE_ACCEPT)
{
unprotect_document(doc);
if (response_id == GTK_RESPONSE_ACCEPT)
file_saved = dialogs_show_save_as();
want_reload = TRUE;
}
else if (ret == GTK_RESPONSE_CLOSE)
if (!file_saved)
{
document_close(doc);
}
if (ret != GTK_RESPONSE_CLOSE && ! file_saved)
{
/* file is missing - set unsaved state */
document_set_text_changed(doc, TRUE);
/* don't prompt more than once */
SETPTR(doc->real_path, NULL);
}
return want_reload;
doc->priv->info_bars[MSG_TYPE_RESAVE] = NULL;
}
static void monitor_resave_missing_file(GeanyDocument *doc)
{
GtkWidget *bar;
if (doc->priv->info_bars[MSG_TYPE_RESAVE] == NULL)
{
GtkWidget *bar;
bar = doc->priv->info_bars[MSG_TYPE_RELOAD];
if (bar != NULL) /* the "file on disk is newer" warning is now moot */
gtk_info_bar_response(GTK_INFO_BAR(bar), GTK_RESPONSE_CANCEL);
bar = document_show_message(doc, GTK_MESSAGE_WARNING,
on_monitor_resave_missing_file_response,
GTK_STOCK_SAVE, GTK_RESPONSE_ACCEPT,
GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL,
NULL, GTK_RESPONSE_NONE,
_("Try to resave the file?"),
_("File \"%s\" was not found on disk!"),
doc->file_name);
protect_document(doc);
doc->priv->info_bars[MSG_TYPE_RESAVE] = bar;
enable_key_intercept(doc, bar);
}
}
@ -3058,6 +3322,7 @@ gboolean document_check_disk_status(GeanyDocument *doc, gboolean force)
}
else if (doc->priv->mtime < st.st_mtime)
{
/* make sure the user is not prompted again after he cancelled the "reload file?" message */
doc->priv->mtime = st.st_mtime;
monitor_reload_file(doc);
/* doc may be closed now */

View File

@ -53,6 +53,13 @@ typedef struct FileEncoding
}
FileEncoding;
enum
{
MSG_TYPE_RELOAD,
MSG_TYPE_RESAVE,
NUM_MSG_TYPES
};
/* Private GeanyDocument fields */
typedef struct GeanyDocumentPrivate
@ -86,6 +93,11 @@ typedef struct GeanyDocumentPrivate
time_t mtime;
/* ID of the idle callback updating the tag list */
guint tag_list_update_source;
/* Whether it's temoporarily protected (read-only and saving is prevented). Does
* not imply doc->readonly as writable files can be protected */
gint protected;
/* Save pointer to info bars allowing to cancel them programatically (to avoid multiple ones) */
GtkWidget *info_bars[NUM_MSG_TYPES];
}
GeanyDocumentPrivate;

View File

@ -608,6 +608,7 @@ static void tab_count_changed(void)
static gboolean notebook_tab_click(GtkWidget *widget, GdkEventButton *event, gpointer data)
{
guint state;
GeanyDocument *doc = (GeanyDocument *) data;
/* toggle additional widgets on double click */
if (event->type == GDK_2BUTTON_PRESS)
@ -620,8 +621,7 @@ static gboolean notebook_tab_click(GtkWidget *widget, GdkEventButton *event, gpo
/* close tab on middle click */
if (event->button == 2)
{
document_remove_page(gtk_notebook_page_num(GTK_NOTEBOOK(main_widgets.notebook),
GTK_WIDGET(data)));
document_close(doc);
return TRUE; /* stop other handlers like notebook_tab_bar_click_cb() */
}
/* switch last used tab on ctrl-click */
@ -656,14 +656,18 @@ static void notebook_tab_close_button_style_set(GtkWidget *btn, GtkRcStyle *prev
/* Returns page number of notebook page, or -1 on error */
gint notebook_new_tab(GeanyDocument *this)
{
GtkWidget *hbox, *ebox;
GtkWidget *hbox, *ebox, *vbox;
gint tabnum;
GtkWidget *page;
gint cur_page;
g_return_val_if_fail(this != NULL, -1);
/* page is packed into a vbox so we can stack infobars above it */
vbox = gtk_vbox_new(FALSE, 0);
page = GTK_WIDGET(this->editor->sci);
gtk_box_pack_start(GTK_BOX(vbox), page, TRUE, TRUE, 0);
gtk_widget_show(vbox);
this->priv->tab_label = gtk_label_new(NULL);
@ -671,7 +675,7 @@ gint notebook_new_tab(GeanyDocument *this)
* the close button, if any */
ebox = gtk_event_box_new();
gtk_widget_set_has_window(ebox, FALSE);
g_signal_connect(ebox, "button-press-event", G_CALLBACK(notebook_tab_click), page);
g_signal_connect(ebox, "button-press-event", G_CALLBACK(notebook_tab_click), this);
/* focus the current document after clicking on a tab */
g_signal_connect_after(ebox, "button-release-event",
G_CALLBACK(focus_sci), NULL);
@ -698,7 +702,7 @@ gint notebook_new_tab(GeanyDocument *this)
g_signal_connect(btn, "clicked", G_CALLBACK(notebook_tab_close_clicked_cb), page);
/* button overrides event box, so make middle click on button also close tab */
g_signal_connect(btn, "button-press-event", G_CALLBACK(notebook_tab_click), page);
g_signal_connect(btn, "button-press-event", G_CALLBACK(notebook_tab_click), this);
/* handle style modification to keep button small as possible even when theme change */
g_signal_connect(btn, "style-set", G_CALLBACK(notebook_tab_close_button_style_set), NULL);
}
@ -712,28 +716,27 @@ gint notebook_new_tab(GeanyDocument *this)
else
cur_page = file_prefs.tab_order_ltr ? -2 /* hack: -2 + 1 = -1, last page */ : 0;
if (file_prefs.tab_order_ltr)
tabnum = gtk_notebook_insert_page_menu(GTK_NOTEBOOK(main_widgets.notebook), page,
tabnum = gtk_notebook_insert_page_menu(GTK_NOTEBOOK(main_widgets.notebook), vbox,
ebox, NULL, cur_page + 1);
else
tabnum = gtk_notebook_insert_page_menu(GTK_NOTEBOOK(main_widgets.notebook), page,
tabnum = gtk_notebook_insert_page_menu(GTK_NOTEBOOK(main_widgets.notebook), vbox,
ebox, NULL, cur_page);
tab_count_changed();
/* enable tab DnD */
gtk_notebook_set_tab_reorderable(GTK_NOTEBOOK(main_widgets.notebook), page, TRUE);
gtk_notebook_set_tab_reorderable(GTK_NOTEBOOK(main_widgets.notebook), vbox, TRUE);
return tabnum;
}
static void
notebook_tab_close_clicked_cb(GtkButton *button, gpointer user_data)
notebook_tab_close_clicked_cb(GtkButton *button, gpointer data)
{
gint cur_page = gtk_notebook_page_num(GTK_NOTEBOOK(main_widgets.notebook),
GTK_WIDGET(user_data));
GeanyDocument *doc = (GeanyDocument *) data;
document_remove_page(cur_page);
document_close(doc);
}