[geary/mjog/233-entry-undo: 2/4] Implement undo support for the composer's text entries



commit 7c15126986c791726e009a1e1934727c42e435be
Author: Michael Gratton <mike vee net>
Date:   Thu Nov 7 09:46:18 2019 +1100

    Implement undo support for the composer's text entries
    
    Add EntryUndo objects for each of the To, CC, BCC, Reply-To and subject
    entries. Fix EmailEntry to ensure that keyboard shortcuts get processed
    when the completion is present. Fix ContactEntryCompleteion to ensure
    it does a precision edit when inserting an adresss (i.e. delete+insert
    rather than complete replacement) so that it integrates into undo.
    
    Fixes #233

 src/client/components/main-window.vala            |   2 +
 src/client/composer/composer-widget.vala          |  31 +++++--
 src/client/composer/contact-entry-completion.vala | 107 +++++++++++++---------
 src/client/composer/email-entry.vala              |  25 +++--
 4 files changed, 111 insertions(+), 54 deletions(-)
---
diff --git a/src/client/components/main-window.vala b/src/client/components/main-window.vala
index 20eb7088..397ffce4 100644
--- a/src/client/components/main-window.vala
+++ b/src/client/components/main-window.vala
@@ -893,6 +893,8 @@ public class MainWindow : Gtk.ApplicationWindow, Geary.BaseInterface {
          * ConversationWebView instances, since none of them handle
          * events.
          *
+         * See also the note in EmailEntry::on_key_press.
+         *
          * The work around here is completely override the default
          * implementation to reverse it. So if something related to
          * key handling breaks in the future, this might be a good
diff --git a/src/client/composer/composer-widget.vala b/src/client/composer/composer-widget.vala
index 2712d6c3..791a285b 100644
--- a/src/client/composer/composer-widget.vala
+++ b/src/client/composer/composer-widget.vala
@@ -268,30 +268,43 @@ public class ComposerWidget : Gtk.EventBox, Geary.BaseInterface {
     [GtkChild]
     private Gtk.ComboBoxText from_multiple;
     private Gee.ArrayList<FromAddressMap> from_list = new Gee.ArrayList<FromAddressMap>();
+
     [GtkChild]
     private Gtk.EventBox to_box;
     [GtkChild]
     private Gtk.Label to_label;
     private EmailEntry to_entry;
+    private Components.EntryUndo to_undo;
+
     [GtkChild]
     private Gtk.EventBox cc_box;
     [GtkChild]
     private Gtk.Label cc_label;
     private EmailEntry cc_entry;
+    private Components.EntryUndo cc_undo;
+
     [GtkChild]
     private Gtk.EventBox bcc_box;
     [GtkChild]
     private Gtk.Label bcc_label;
     private EmailEntry bcc_entry;
+    private Components.EntryUndo bcc_undo;
+
     [GtkChild]
     private Gtk.EventBox reply_to_box;
     [GtkChild]
     private Gtk.Label reply_to_label;
     private EmailEntry reply_to_entry;
+    private Components.EntryUndo reply_to_undo;
+
     [GtkChild]
     private Gtk.Label subject_label;
     [GtkChild]
     private Gtk.Entry subject_entry;
+    private Components.EntryUndo subject_undo;
+    private Gspell.Checker subject_spell_checker = new Gspell.Checker(null);
+    private Gspell.Entry subject_spell_entry;
+
     [GtkChild]
     private Gtk.Label message_overlay_label;
     [GtkChild]
@@ -386,9 +399,6 @@ public class ComposerWidget : Gtk.EventBox, Geary.BaseInterface {
         get { return (ComposerContainer) parent; }
     }
 
-    private Gspell.Checker subject_spell_checker = new Gspell.Checker(null);
-    private Gspell.Entry subject_spell_entry;
-
     private GearyApplication application;
 
 
@@ -454,23 +464,30 @@ public class ComposerWidget : Gtk.EventBox, Geary.BaseInterface {
         this.to_entry = new EmailEntry(this);
         this.to_entry.changed.connect(on_envelope_changed);
         this.to_box.add(to_entry);
+        this.to_label.set_mnemonic_widget(this.to_entry);
+        this.to_undo = new Components.EntryUndo(this.to_entry);
+
         this.cc_entry = new EmailEntry(this);
         this.cc_entry.changed.connect(on_envelope_changed);
         this.cc_box.add(cc_entry);
+        this.cc_label.set_mnemonic_widget(this.cc_entry);
+        this.cc_undo = new Components.EntryUndo(this.cc_entry);
+
         this.bcc_entry = new EmailEntry(this);
         this.bcc_entry.changed.connect(on_envelope_changed);
         this.bcc_box.add(bcc_entry);
+        this.bcc_label.set_mnemonic_widget(this.bcc_entry);
+        this.bcc_undo = new Components.EntryUndo(this.bcc_entry);
+
         this.reply_to_entry = new EmailEntry(this);
         this.reply_to_entry.changed.connect(on_envelope_changed);
         this.reply_to_box.add(reply_to_entry);
-
-        this.to_label.set_mnemonic_widget(this.to_entry);
-        this.cc_label.set_mnemonic_widget(this.cc_entry);
-        this.bcc_label.set_mnemonic_widget(this.bcc_entry);
         this.reply_to_label.set_mnemonic_widget(this.reply_to_entry);
+        this.reply_to_undo = new Components.EntryUndo(this.reply_to_entry);
 
         this.to_entry.margin_top = this.cc_entry.margin_top = this.bcc_entry.margin_top = 
this.reply_to_entry.margin_top = 6;
 
+        this.subject_undo = new Components.EntryUndo(this.subject_entry);
         this.subject_spell_entry = Gspell.Entry.get_from_gtk_entry(
             this.subject_entry
         );
diff --git a/src/client/composer/contact-entry-completion.vala 
b/src/client/composer/contact-entry-completion.vala
index 0b1e6b1f..bd6584d7 100644
--- a/src/client/composer/contact-entry-completion.vala
+++ b/src/client/composer/contact-entry-completion.vala
@@ -34,7 +34,7 @@ public class ContactEntryCompletion : Gtk.EntryCompletion, Geary.BaseInterface {
     private string current_key = "";
 
     // List of (possibly incomplete) email addresses in the entry.
-    private string[] email_addresses = {};
+    private Gee.ArrayList<string> address_parts = new Gee.ArrayList<string>();
 
     // Index of the email address the cursor is currently at
     private int cursor_at_address = -1;
@@ -98,10 +98,11 @@ public class ContactEntryCompletion : Gtk.EntryCompletion, Geary.BaseInterface {
             model.clear();
         }
     }
+
     public void trigger_selection() {
-        if (last_iter != null) {
-            on_match_selected(model, last_iter);
-            last_iter = null;
+        if (this.last_iter != null) {
+            insert_address_at_cursor(this.last_iter);
+            this.last_iter = null;
         }
     }
 
@@ -110,7 +111,7 @@ public class ContactEntryCompletion : Gtk.EntryCompletion, Geary.BaseInterface {
         if (entry != null) {
             this.current_key = "";
             this.cursor_at_address = -1;
-            this.email_addresses = {};
+            this.address_parts.clear();
 
             string text = entry.get_text();
             int cursor_pos = entry.get_position();
@@ -123,7 +124,7 @@ public class ContactEntryCompletion : Gtk.EntryCompletion, Geary.BaseInterface {
             while (text.get_next_char(ref next_idx, out c)) {
                 if (current_char == cursor_pos) {
                     this.current_key = text.slice(start_idx, next_idx).strip();
-                    this.cursor_at_address = this.email_addresses.length;
+                    this.cursor_at_address = this.address_parts.size;
                 }
 
                 switch (c) {
@@ -131,7 +132,7 @@ public class ContactEntryCompletion : Gtk.EntryCompletion, Geary.BaseInterface {
                     if (!in_quote) {
                         // Don't include the comma in the address
                         string address = text.slice(start_idx, next_idx -1);
-                        this.email_addresses += address.strip();
+                        this.address_parts.add(address);
                         // Don't include it in the next one, either
                         start_idx = next_idx;
                     }
@@ -147,12 +148,66 @@ public class ContactEntryCompletion : Gtk.EntryCompletion, Geary.BaseInterface {
 
             // Add any remaining text after the last comma
             string address = text.substring(start_idx);
-            this.email_addresses += address.strip();
+            this.address_parts.add(address);
+        }
+    }
+
+    private void insert_address_at_cursor(Gtk.TreeIter iter) {
+        Gtk.Entry? entry = get_entry() as Gtk.Entry;
+        if (entry != null) {
+            // Take care to do a delete then an insert here so that
+            // Component.EntryUndo can combine the two into a single
+            // undoable command
+            int start_char = this.address_parts.slice(
+                0, this.cursor_at_address
+            ).fold<int>(
+                // address parts don't contain commas, so need to add
+                // an char width for it
+                (a, chars) => a.char_count() + chars + 1, 0
+            );
+            int end_char = (
+                start_char +
+                this.address_parts[this.cursor_at_address].char_count()
+            );
+
+            // Format and use the selected address
+            GLib.Value value;
+            this.model.get_value(iter, Column.MAILBOX, out value);
+            Geary.RFC822.MailboxAddress mailbox =
+                (Geary.RFC822.MailboxAddress) value.get_object();
+            string formatted = mailbox.to_full_display();
+            if (this.cursor_at_address != 0) {
+                // This isn't the first address, so add some
+                // whitespace to pad it out
+                formatted = " " + formatted;
+            }
+            this.address_parts[this.cursor_at_address] = formatted;
+
+            // Update the entry text
+            entry.delete_text(start_char, end_char);
+            entry.insert_text(
+                formatted, formatted.char_count(), ref start_char
+            );
+
+            // Update the entry cursor position. The previous call
+            // updates the start so just use that, but add extra space
+            // for the comma and any white space at the start of the
+            // next address.
+            ++start_char;
+            string? next_address = (
+                this.cursor_at_address + 1 < this.address_parts.size
+                ? this.address_parts[this.cursor_at_address + 1]
+                : ""
+            );
+            for (int i = 0; i < next_address.length && next_address[i] == ' '; i++) {
+                ++start_char;
+            }
+            entry.set_position(start_char);
         }
     }
 
-    public async void search_contacts(string query,
-                                      GLib.Cancellable? cancellable) {
+    private async void search_contacts(string query,
+                                       GLib.Cancellable? cancellable) {
         Gee.Collection<Application.Contact>? results = null;
         try {
             results = yield this.contacts.search(
@@ -283,37 +338,7 @@ public class ContactEntryCompletion : Gtk.EntryCompletion, Geary.BaseInterface {
     }
 
     private bool on_match_selected(Gtk.TreeModel model, Gtk.TreeIter iter) {
-        Gtk.Entry? entry = get_entry() as Gtk.Entry;
-        if (entry != null) {
-            // Update the address
-            GLib.Value value;
-            model.get_value(iter, Column.MAILBOX, out value);
-            Geary.RFC822.MailboxAddress mailbox =
-                (Geary.RFC822.MailboxAddress) value.get_object();
-            this.email_addresses[this.cursor_at_address] =
-                mailbox.to_full_display();
-
-            // Update the entry text
-            bool current_is_last = (
-                this.cursor_at_address == this.email_addresses.length - 1
-            );
-            int new_cursor_pos = -1;
-            GLib.StringBuilder text = new GLib.StringBuilder();
-            int i = 0;
-            while (i < this.email_addresses.length) {
-                text.append(this.email_addresses[i]);
-                if (i == this.cursor_at_address) {
-                    new_cursor_pos = text.str.char_count();
-                }
-
-                i++;
-                if (i != this.email_addresses.length || current_is_last) {
-                    text.append(", ");
-                }
-            }
-            entry.text = text.str;
-            entry.set_position(current_is_last ? -1 : new_cursor_pos);
-        }
+        insert_address_at_cursor(iter);
         return true;
     }
 
diff --git a/src/client/composer/email-entry.vala b/src/client/composer/email-entry.vala
index eb0e35b1..0cc35a4a 100644
--- a/src/client/composer/email-entry.vala
+++ b/src/client/composer/email-entry.vala
@@ -81,13 +81,26 @@ public class EmailEntry : Gtk.Entry {
     }
 
     private bool on_key_press(Gtk.Widget widget, Gdk.EventKey event) {
+        bool ret = Gdk.EVENT_PROPAGATE;
         if (event.keyval == Gdk.Key.Tab) {
-            ((ContactEntryCompletion) get_completion()).trigger_selection();
-            composer.child_focus(Gtk.DirectionType.TAB_FORWARD);
-            return true;
+            ContactEntryCompletion? completion = (
+                get_completion() as ContactEntryCompletion
+            );
+            if (completion != null) {
+                completion.trigger_selection();
+                composer.child_focus(Gtk.DirectionType.TAB_FORWARD);
+                ret = Gdk.EVENT_STOP;
+            }
+        } else {
+            // Keyboard shortcuts for undo/redo won't work when the
+            // completion UI is visible unless we explicitly check for
+            // them there. This may be related to the
+            // single-key-shortcut handling hack in the MainWindow.
+            Gtk.Window? window = get_toplevel() as Gtk.Window;
+            if (window != null) {
+                ret = window.activate_key(event);
+            }
         }
-
-        return false;
+        return ret;
     }
 }
-


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]