From 2bd5306d30db0d719f64475e50d11bee15db804f Mon Sep 17 00:00:00 2001 From: Michael James Gratton Date: Wed, 4 Jan 2017 18:53:28 +1100 Subject: [PATCH] Reenable undo/redo composer actions. * src/client/composer/composer-web-view.vala (ComposerWebView): Add new ::command_stack_changed signal to manage undo/redo enabled state, hook it up to a JS callback. Add ::is_empty property as a non-functioning shim in lieu of can_undo going away. Remove ::can_undo and ::can_redo, replace them with methods to actually fire an undo and redo. * src/client/composer/composer-widget.vala (ComposerWidget): Use ClientWebView::is_empty rather inplace of can_redo for determining editing state. Remove old undo/redo signal hookups, replace with ::command_stack_changed and manage action enable state from there. (ComposerWidget::action_entries): Add explicit actions for undo/redo, since they need some custom code over on on the web process side. (ComposerWidget::on_compose_as_html_toggled): Explciitly manage the visibility of rich text toolbar buttons, don't rely on obscure GtkBuilder magic that Glade doesn't support. * ui/composer-web-view.js: Add a mutation observer for the message body and explcit methods for firing undo/redo commands, so we can keep track of how the command stack changes. As it does, fire commandStackChanged messages back to the client process. Explicity set the message body as content-editable after the document has been mutated for quotes, etc. * ui/composer-widget.ui: Add bonus undo/redo toobar buttons for the composer. --- src/client/composer/composer-web-view.vala | 51 ++-- src/client/composer/composer-widget.vala | 35 ++- ui/composer-web-view.js | 61 ++++- ui/composer-widget.ui | 262 +++++++++++++++------ 4 files changed, 308 insertions(+), 101 deletions(-) diff --git a/src/client/composer/composer-web-view.vala b/src/client/composer/composer-web-view.vala index 58e61e62..bfba08b0 100644 --- a/src/client/composer/composer-web-view.vala +++ b/src/client/composer/composer-web-view.vala @@ -12,6 +12,8 @@ public class ComposerWebView : ClientWebView { + private const string COMMAND_STACK_CHANGED = "commandStackChanged"; + private const string HTML_BODY = """ -
%s
+
%s
"""; private const string CURSOR = ""; @@ -68,17 +70,37 @@ public class ComposerWebView : ClientWebView { ); } + /** Determines if the view contains any edited text */ + public bool is_empty { get; private set; default = false; } + /** Determines if the view is in rich text mode */ public bool is_rich_text { get; private set; default = true; } private bool is_shift_down = false; + /** Emitted when the web view's undo/redo stack has changed. */ + public signal void command_stack_changed(bool can_undo, bool can_redo); + public ComposerWebView(Configuration config) { base(config); this.user_content_manager.add_script(ComposerWebView.app_script); // this.should_insert_text.connect(on_should_insert_text); this.key_press_event.connect(on_key_press_event); + + this.user_content_manager.script_message_received[COMMAND_STACK_CHANGED].connect( + (result) => { + try { + string[] values = WebKitUtil.to_string(result).split(","); + command_stack_changed(values[0] == "true", values[1] == "true"); + } catch (Geary.JS.Error err) { + debug("Could not get command stack state: %s", err.message); + } finally { + result.unref(); + } + }); + result.unref(); + register_message_handler(COMMAND_STACK_CHANGED); } /** @@ -98,24 +120,19 @@ public class ComposerWebView : ClientWebView { base.load_html(HTML_BODY.printf(html)); } - public bool can_undo() { - // can_execute_editing_command.begin( - // WebKit.EDITING_COMMAND_UNDO, - // null, - // (obj, res) => { - // return can_execute_editing_command.end(res); - // }); - return false; + + /** + * Undoes the last edit operation. + */ + public void undo() { + this.run_javascript.begin("geary.undo();", null); } - public bool can_redo() { - // can_execute_editing_command.begin( - // WebKit.EDITING_COMMAND_REDO, - // null, - // (obj, res) => { - // return can_execute_editing_command.end(res); - // }); - return false; + /** + * Redoes the last undone edit operation. + */ + public void redo() { + this.run_javascript.begin("geary.redo();", null); } /** diff --git a/src/client/composer/composer-widget.vala b/src/client/composer/composer-widget.vala index b725f409..6c536580 100644 --- a/src/client/composer/composer-widget.vala +++ b/src/client/composer/composer-widget.vala @@ -87,8 +87,8 @@ public class ComposerWidget : Gtk.EventBox { private const ActionEntry[] action_entries = { // Editor commands - {ACTION_UNDO, on_action }, - {ACTION_REDO, on_action }, + {ACTION_UNDO, on_undo }, + {ACTION_REDO, on_redo }, {ACTION_CUT, on_cut }, {ACTION_COPY, on_copy }, {ACTION_COPY_LINK, on_copy_link }, @@ -209,7 +209,7 @@ public class ComposerWidget : Gtk.EventBox { this.bcc_entry.empty && this.reply_to_entry.empty && this.subject_entry.buffer.length == 0 && - !this.editor.can_undo() && + !this.editor.is_empty && this.attached_files.size == 0; } } @@ -286,8 +286,13 @@ public class ComposerWidget : Gtk.EventBox { [GtkChild] private Gtk.Box header_area; [GtkChild] + private Gtk.Box composer_toolbar; [GtkChild] + private Gtk.Box insert_buttons; + [GtkChild] + private Gtk.Box font_style_buttons; + [GtkChild] private Gtk.Button remove_format_button; [GtkChild] private Gtk.Button select_dictionary_button; @@ -295,6 +300,7 @@ public class ComposerWidget : Gtk.EventBox { private Gtk.MenuButton menu_button; [GtkChild] private Gtk.Label info_label; + [GtkChild] private Gtk.Box message_area; @@ -465,12 +471,11 @@ public class ComposerWidget : Gtk.EventBox { this.bcc_entry.changed.connect(validate_send_button); this.reply_to_entry.changed.connect(validate_send_button); this.editor.context_menu.connect(on_context_menu); + this.editor.command_stack_changed.connect(on_command_state_changed); this.editor.load_changed.connect(on_load_changed); this.editor.mouse_target_changed.connect(on_mouse_target_changed); this.editor.get_editor_state().notify["typing-attributes"].connect(on_typing_attributes_changed); // this.editor.move_focus.connect(update_actions); - // this.editor.undo.connect(update_actions); - // this.editor.redo.connect(update_actions); this.editor.selection_changed.connect(on_selection_changed); this.editor.key_press_event.connect(on_editor_key_press); //this.editor.user_changed_contents.connect(reset_draft_timer); @@ -554,6 +559,8 @@ public class ComposerWidget : Gtk.EventBox { this.header.insert_action_group("cmh", this.actions); update_actions(); + get_action(ACTION_UNDO).set_enabled(false); + get_action(ACTION_REDO).set_enabled(false); } /** @@ -1033,7 +1040,7 @@ public class ComposerWidget : Gtk.EventBox { private bool can_save() { return this.draft_manager != null && this.draft_manager.is_open - && this.editor.can_undo() + && this.editor.is_empty && this.account.information.save_drafts; } @@ -1614,6 +1621,14 @@ public class ComposerWidget : Gtk.EventBox { this.editor.execute_editing_command(action_name); } + private void on_undo(SimpleAction action, Variant? param) { + this.editor.undo(); + } + + private void on_redo(SimpleAction action, Variant? param) { + this.editor.redo(); + } + private void on_cut(SimpleAction action, Variant? param) { if (this.container.get_focus() == this.editor) this.editor.cut_clipboard(); @@ -1669,6 +1684,9 @@ public class ComposerWidget : Gtk.EventBox { foreach (string html_action in html_actions) get_action(html_action).set_enabled(compose_as_html); + + this.insert_buttons.visible = compose_as_html; + this.font_style_buttons.visible = compose_as_html; this.remove_format_button.visible = compose_as_html; this.menu_button.menu_model = (compose_as_html) ? this.html_menu : this.plain_menu; @@ -2215,6 +2233,11 @@ public class ComposerWidget : Gtk.EventBox { this.signature_html = account_sig; } + private void on_command_state_changed(bool can_undo, bool can_redo) { + get_action(ACTION_UNDO).set_enabled(can_undo); + get_action(ACTION_REDO).set_enabled(can_redo); + } + private void on_selection_changed(bool has_selection) { get_action(ACTION_CUT).set_enabled(has_selection); get_action(ACTION_COPY).set_enabled(has_selection); diff --git a/ui/composer-web-view.js b/ui/composer-web-view.js index 3c7023e8..62a648d3 100644 --- a/ui/composer-web-view.js +++ b/ui/composer-web-view.js @@ -22,14 +22,26 @@ ComposerPageState.prototype = { init: function() { PageState.prototype.init.apply(this, []); + this.messageBody = null; + + this.undoEnabled = false; + this.redoEnabled = false; + let state = this; + document.addEventListener("click", function(e) { if (e.target.tagName == "A") { state.linkClicked(e.target); } }, true); + + this.bodyObserver = new MutationObserver(function() { + state.checkCommandStack(); + }); }, loaded: function() { + this.messageBody = document.getElementById(ComposerPageState.BODY_ID); + // Search for and remove a particular styling when we quote // text. If that style exists in the quoted text, we alter it // slightly so we don't mess with it later. @@ -58,17 +70,28 @@ ComposerPageState.prototype = { cursor.parentNode.removeChild(cursor); } + // Enable editing and observation machinery only after + // modifying the body above. + this.messageBody.contentEditable = true; + this.setBodyObserverEnabled(true); + // Chain up here so we continue to a preferred size update // after munging the HTML above. PageState.prototype.loaded.apply(this, []); }, + undo: function() { + document.execCommand("undo", false, null); + this.checkCommandStack(); + }, + redo: function() { + document.execCommand("redo", false, null); + this.checkCommandStack(); + }, getHtml: function() { - return document.getElementById(ComposerPageState.BODY_ID).innerHTML; + return this.messageBody.innerHTML; }, getText: function() { - return ComposerPageState.htmlToQuotedText( - document.getElementById(ComposerPageState.BODY_ID) - ); + return ComposerPageState.htmlToQuotedText(this.messageBody); }, setRichText: function(enabled) { if (enabled) { @@ -77,6 +100,36 @@ ComposerPageState.prototype = { document.body.classList.add("plain"); } }, + setBodyObserverEnabled: function(enabled) { + if (enabled) { + let config = { + attributes: true, + childList: true, + characterData: true, + subtree: true + }; + this.bodyObserver.observe(this.messageBody, config); + } else { + this.bodyObserver.disconnect(); + } + }, + checkCommandStack: function() { + let canUndo = document.queryCommandEnabled("undo"); + let canRedo = document.queryCommandEnabled("redo"); + + // Update the body observer - if we can undo we don't need to + // keep an eye on mutations any more, until we can't undo + // again. + this.setBodyObserverEnabled(!canUndo); + + if (canUndo != this.undoEnabled || canRedo != this.redoEnabled) { + this.undoEnabled = canUndo; + this.redoEnabled = canRedo; + window.webkit.messageHandlers.commandStackChanged.postMessage( + this.undoEnabled + "," + this.redoEnabled + ); + } + }, undoBlockquoteStyle: function() { let nodeList = document.querySelectorAll( "blockquote[style=\"margin: 0 0 0 40px; border: none; padding: 0px;\"]" diff --git a/ui/composer-widget.ui b/ui/composer-widget.ui index 9461beaa..df1bdba3 100644 --- a/ui/composer-widget.ui +++ b/ui/composer-widget.ui @@ -1,4 +1,5 @@ +