[geary/mjog/233-entry-undo: 2/5] Implement undo support for the composer's text entries
- From: Michael Gratton <mjog src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [geary/mjog/233-entry-undo: 2/5] Implement undo support for the composer's text entries
- Date: Thu, 7 Nov 2019 02:02:02 +0000 (UTC)
commit 8000e7ca63301e2472c9007795222ca741a212d3
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]