From ce1630e09d2c25be1c61a7373491605a3b045318 Mon Sep 17 00:00:00 2001 From: Michael Gratton Date: Wed, 6 Nov 2019 11:53:36 +1100 Subject: [PATCH 1/7] Add Components.EntryUndo class to manage undo for GTK Entries --- po/POTFILES.in | 1 + src/client/application/geary-application.vala | 1 + .../components/components-entry-undo.vala | 346 ++++++++++++++++++ src/client/meson.build | 1 + 4 files changed, 349 insertions(+) create mode 100644 src/client/components/components-entry-undo.vala diff --git a/po/POTFILES.in b/po/POTFILES.in index 502e679a..2a4b49a4 100644 --- a/po/POTFILES.in +++ b/po/POTFILES.in @@ -30,6 +30,7 @@ src/client/application/main.vala src/client/application/secret-mediator.vala src/client/components/client-web-view.vala src/client/components/components-attachment-pane.vala +src/client/components/components-entry-undo.vala src/client/components/components-in-app-notification.vala src/client/components/components-inspector.vala src/client/components/components-placeholder-pane.vala diff --git a/src/client/application/geary-application.vala b/src/client/application/geary-application.vala index 4fb97a0e..3b416cd5 100644 --- a/src/client/application/geary-application.vala +++ b/src/client/application/geary-application.vala @@ -449,6 +449,7 @@ public class GearyApplication : Gtk.Application { MainWindow.add_window_accelerators(this); ComposerWidget.add_window_accelerators(this); + Components.EntryUndo.add_window_accelerators(this); Components.Inspector.add_window_accelerators(this); if (this.is_background_service) { diff --git a/src/client/components/components-entry-undo.vala b/src/client/components/components-entry-undo.vala new file mode 100644 index 00000000..18382e34 --- /dev/null +++ b/src/client/components/components-entry-undo.vala @@ -0,0 +1,346 @@ +/* + * Copyright 2019 Michael Gratton + * + * This software is licensed under the GNU Lesser General Public License + * (version 2.1 or later). See the COPYING file in this distribution. + */ + +/** + * Provides per-GTK Entry undo and redo using a command stack. + */ +public class Components.EntryUndo : Geary.BaseObject { + + + private const string ACTION_GROUP = "ceu"; + private const ActionEntry[] action_entries = { + { GearyApplication.ACTION_UNDO, on_undo }, + { GearyApplication.ACTION_REDO, on_redo }, + }; + + + private enum EditType { NONE, INSERT, DELETE; } + + + private class EditCommand : Application.Command { + + + private weak EntryUndo manager; + private EditType edit; + private int position; + private string text; + + + public EditCommand(EntryUndo manager, + EditType edit, + int position, + string text) { + this.manager = manager; + this.edit = edit; + this.position = position; + this.text = text; + } + + public override async void execute(GLib.Cancellable? cancellable) + throws GLib.Error { + // No-op, has already been executed + } + + public override async void undo(GLib.Cancellable? cancellable) + throws GLib.Error { + EntryUndo? manager = this.manager; + if (manager != null) { + manager.events_enabled = false; + switch (this.edit) { + case INSERT: + do_delete(manager.target); + break; + case DELETE: + do_insert(manager.target); + break; + } + manager.events_enabled = true; + } + } + + public override async void redo(GLib.Cancellable? cancellable) + throws GLib.Error { + EntryUndo? manager = this.manager; + if (manager != null) { + manager.events_enabled = false; + switch (this.edit) { + case INSERT: + do_insert(manager.target); + break; + case DELETE: + do_delete(manager.target); + break; + } + manager.events_enabled = true; + } + } + + private void do_insert(Gtk.Entry target) { + int position = this.position; + target.insert_text(this.text, -1, ref position); + target.set_position(position); + } + + private void do_delete(Gtk.Entry target) { + target.delete_text( + this.position, this.position + this.text.char_count() + ); + } + + } + + + public static void add_window_accelerators(GearyApplication application) { + application.set_accels_for_action( + ACTION_GROUP + "." + GearyApplication.ACTION_UNDO, + { "z" } + ); + application.set_accels_for_action( + ACTION_GROUP + "." + GearyApplication.ACTION_REDO, + { "z" } + ); + } + + + /** The entry being managed */ + public Gtk.Entry target { get; private set; } + + private Application.CommandStack commands; + private EditType last_edit = NONE; + private int edit_start = 0; + private int edit_end = 0; + private GLib.StringBuilder edit_accumuluator = new GLib.StringBuilder(); + + private bool events_enabled = true; + + private GLib.SimpleActionGroup entry_actions = new SimpleActionGroup(); + + + public EntryUndo(Gtk.Entry target) { + this.target = target; + this.target.insert_action_group(ACTION_GROUP, this.entry_actions); + this.target.insert_text.connect(on_inserted); + this.target.delete_text.connect(on_deleted); + + this.commands = new Application.CommandStack(); + this.commands.executed.connect(this.update_command_actions); + this.commands.undone.connect(this.update_command_actions); + this.commands.redone.connect(this.update_command_actions); + + this.entry_actions.add_action_entries(EntryUndo.action_entries, this); + } + + ~EntryUndo() { + this.target.insert_text.disconnect(on_inserted); + this.target.delete_text.disconnect(on_deleted); + } + + /** Resets the editing stack for the target entry. */ + public void reset() { + this.last_edit = NONE; + this.commands.clear(); + } + + private void execute(Application.Command command) { + bool complete = false; + this.commands.execute.begin( + command, + null, + (obj, res) => { + try { + this.commands.execute.end(res); + } catch (GLib.Error thrown) { + debug( + "Failed to execute entry edit command: %s", + thrown.message + ); + } + complete = true; + } + ); + while (!complete) { + Gtk.main_iteration(); + } + } + + private void do_undo() { + flush_command(); + bool complete = false; + this.commands.undo.begin( + null, + (obj, res) => { + try { + this.commands.undo.end(res); + } catch (GLib.Error thrown) { + debug( + "Failed to undo entry edit command: %s", + thrown.message + ); + } + complete = true; + } + ); + while (!complete) { + Gtk.main_iteration(); + } + } + + private void do_redo() { + flush_command(); + bool complete = false; + this.commands.redo.begin( + null, + (obj, res) => { + try { + this.commands.redo.end(res); + } catch (GLib.Error thrown) { + debug( + "Failed to undo entry edit command: %s", + thrown.message + ); + } + complete = true; + } + ); + while (!complete) { + Gtk.main_iteration(); + } + } + + private void flush_command() { + EditCommand? command = extract_command(); + if (command != null) { + execute(command); + } + } + + private EditCommand? extract_command() { + EditCommand? command = null; + if (this.last_edit != NONE) { + command = new EditCommand( + this, + this.last_edit, + this.edit_start, + this.edit_accumuluator.str + ); + this.edit_accumuluator.truncate(); + } + this.last_edit = NONE; + return command; + } + + private void update_command_actions() { + ((GLib.SimpleAction) this.entry_actions.lookup_action( + GearyApplication.ACTION_UNDO)).set_enabled(this.commands.can_undo); + ((GLib.SimpleAction) this.entry_actions.lookup_action( + GearyApplication.ACTION_REDO)).set_enabled(this.commands.can_redo); + } + + private void on_inserted(string inserted, int inserted_len, ref int pos) { + if (this.events_enabled) { + // Normalise to something useful + inserted_len = inserted.char_count(); + + bool is_non_trivial = inserted_len > 1; + bool insert_handled = false; + if (this.last_edit == DELETE) { + Application.Command? command = extract_command(); + if (command != null && + this.edit_start == pos && + is_non_trivial) { + // Delete followed by a non-trivial insert at the + // same position indicates something was probably + // pasted/spellchecked/completed/etc, so execute + // together as a single command. + this.last_edit = INSERT; + this.edit_start = pos; + this.edit_accumuluator.append(inserted); + command = new Application.CommandSequence({ + command, extract_command() + }); + insert_handled = true; + } + if (command != null) { + execute(command); + } + } + + if (!insert_handled) { + bool is_disjoint_edit = ( + this.last_edit == INSERT && this.edit_end != pos + ); + bool is_non_alpha_num = ( + inserted_len == 1 && !inserted.get_char(0).isalnum() + ); + + // Flush any existing edits if any of the special + // cases hold + if (is_disjoint_edit || is_non_alpha_num || is_non_trivial) { + flush_command(); + } + + if (this.last_edit == NONE) { + this.last_edit = INSERT; + this.edit_start = pos; + this.edit_end = pos; + } + + this.edit_end += inserted_len; + this.edit_accumuluator.append(inserted); + + // Flush the new edit if we don't want to coalesce + // with subsequent inserts + if (is_non_alpha_num || is_non_trivial) { + flush_command(); + } + } + } + } + + private void on_deleted(int start, int end) { + if (this.events_enabled) { + // Normalise value of end to be something useful if needed + string text = this.target.buffer.get_text(); + if (end < 0) { + end = text.char_count(); + } + + // Don't flush non-trivial deletes since we want to be + // able to combine them with non-trivial inserts for + // better handling of pasting/spell-checking + // replacement/etc. + bool is_disjoint_edit = ( + this.last_edit == DELETE && this.edit_start != end + ); + if (this.last_edit == INSERT || is_disjoint_edit) { + flush_command(); + } + + if (this.last_edit == NONE) { + this.last_edit = DELETE; + this.edit_end = end; + } + + this.edit_start = start; + this.edit_accumuluator.prepend( + text.slice( + text.index_of_nth_char(start), + text.index_of_nth_char(end) + ) + ); + } + } + + private void on_undo() { + do_undo(); + } + + private void on_redo() { + do_redo(); + } + +} diff --git a/src/client/meson.build b/src/client/meson.build index cf64427b..47a261f6 100644 --- a/src/client/meson.build +++ b/src/client/meson.build @@ -27,6 +27,7 @@ geary_client_vala_sources = files( 'components/client-web-view.vala', 'components/components-attachment-pane.vala', + 'components/components-entry-undo.vala', 'components/components-inspector.vala', 'components/components-in-app-notification.vala', 'components/components-inspector-error-view.vala', From 8000e7ca63301e2472c9007795222ca741a212d3 Mon Sep 17 00:00:00 2001 From: Michael Gratton Date: Thu, 7 Nov 2019 09:46:18 +1100 Subject: [PATCH 2/7] 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 +++-- .../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 from_list = new Gee.ArrayList(); + [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 address_parts = new Gee.ArrayList(); // 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); } } - public async void search_contacts(string query, - GLib.Cancellable? cancellable) { + 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( + // 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); + } + } + + private async void search_contacts(string query, + GLib.Cancellable? cancellable) { Gee.Collection? 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; } } - From dae817b037dc59933ac05125f8709a4ebb8e8170 Mon Sep 17 00:00:00 2001 From: Michael Gratton Date: Thu, 7 Nov 2019 09:55:30 +1100 Subject: [PATCH 3/7] Add undo support for account editor text entries Add an EntryUndo instance to all editor rows that use Gtk.Entry values. --- .../accounts/accounts-editor-add-pane.vala | 23 +++++++++++++------ .../accounts/accounts-editor-edit-pane.vala | 18 +++++++++++++-- .../accounts-editor-servers-pane.vala | 19 +++++++++++---- 3 files changed, 46 insertions(+), 14 deletions(-) diff --git a/src/client/accounts/accounts-editor-add-pane.vala b/src/client/accounts/accounts-editor-add-pane.vala index 9a0d4400..cfbb3276 100644 --- a/src/client/accounts/accounts-editor-add-pane.vala +++ b/src/client/accounts/accounts-editor-add-pane.vala @@ -517,11 +517,19 @@ private abstract class Accounts.AddPaneRow : private abstract class Accounts.EntryRow : AddPaneRow { - protected EntryRow(string label, string? placeholder = null) { + private Components.EntryUndo undo; + + + protected EntryRow(string label, + string? initial_value = null, + string? placeholder = null) { base(label, new Gtk.Entry()); + this.value.text = initial_value ?? ""; this.value.placeholder_text = placeholder ?? ""; this.value.width_chars = 32; + + this.undo = new Components.EntryUndo(this.value); } public override bool focus(Gtk.DirectionType direction) { @@ -548,12 +556,12 @@ private class Accounts.NameRow : EntryRow { public NameRow(string default_name) { // Translators: Label for the person's actual name when adding // an account - base(_("Your name")); + base(_("Your name"), default_name.strip()); this.validator = new Components.Validator(this.value); - if (default_name.strip() != "") { - // Set the text after hooking up the validator, so if the - // string is non-null it will already be valid - this.value.set_text(default_name); + if (this.value.text != "") { + // Validate if the string is non-empty so it will be good + // to go from the start + this.value.activate(); } } @@ -566,6 +574,7 @@ private class Accounts.EmailRow : EntryRow { public EmailRow() { base( _("Email address"), + null, // Translators: Placeholder for the default sender address // when adding an account _("person@example.com") @@ -634,7 +643,7 @@ private class Accounts.HostnameRow : EntryRow { break; } - base(label, placeholder); + base(label, null, placeholder); this.type = type; this.validator = new Components.NetworkAddressValidator(this.value, 0); diff --git a/src/client/accounts/accounts-editor-edit-pane.vala b/src/client/accounts/accounts-editor-edit-pane.vala index 5cfcfdbb..0c052b44 100644 --- a/src/client/accounts/accounts-editor-edit-pane.vala +++ b/src/client/accounts/accounts-editor-edit-pane.vala @@ -264,6 +264,7 @@ internal class Accounts.EditorEditPane : private class Accounts.DisplayNameRow : AccountRow { + private Components.EntryUndo value_undo; private Application.CommandStack commands; private GLib.Cancellable? cancellable; @@ -284,12 +285,19 @@ private class Accounts.DisplayNameRow : AccountRow { update(); + // Hook up after updating the value so the default value isn't + // undoable + this.value_undo = new Components.EntryUndo(this.value); + this.value.focus_out_event.connect(on_focus_out); } public override void update() { - this.value.set_placeholder_text(this.account.primary_mailbox.address); - this.value.set_text(this.account.display_name); + this.value.placeholder_text = this.account.primary_mailbox.address; + // Only update if changed to avoid adding more undo edits + if (this.value.text != this.account.display_name) { + this.value.text = this.account.display_name; + } } private void commit() { @@ -434,7 +442,9 @@ internal class Accounts.MailboxEditorPopover : EditorPopover { private Gtk.Entry name_entry = new Gtk.Entry(); + private Components.EntryUndo name_undo; private Gtk.Entry address_entry = new Gtk.Entry(); + private Components.EntryUndo address_undo; private Components.EmailValidator address_validator; private Gtk.Button remove_button; @@ -460,6 +470,8 @@ internal class Accounts.MailboxEditorPopover : EditorPopover { this.name_entry.activate.connect(on_activate); this.name_entry.show(); + this.name_undo = new Components.EntryUndo(this.name_entry); + this.address_entry.input_purpose = Gtk.InputPurpose.EMAIL; this.address_entry.set_text(address ?? ""); this.address_entry.set_placeholder_text( @@ -473,6 +485,8 @@ internal class Accounts.MailboxEditorPopover : EditorPopover { this.address_entry.activate.connect(on_activate); this.address_entry.show(); + this.address_undo = new Components.EntryUndo(this.address_entry); + this.address_validator = new Components.EmailValidator(this.address_entry); diff --git a/src/client/accounts/accounts-editor-servers-pane.vala b/src/client/accounts/accounts-editor-servers-pane.vala index 1eb402c9..7bfa6f09 100644 --- a/src/client/accounts/accounts-editor-servers-pane.vala +++ b/src/client/accounts/accounts-editor-servers-pane.vala @@ -711,6 +711,7 @@ private class Accounts.ServiceHostRow : } } + private Components.EntryUndo value_undo; private Application.CommandStack commands; private GLib.Cancellable? cancellable; @@ -741,9 +742,11 @@ private class Accounts.ServiceHostRow : this.validator = new Components.NetworkAddressValidator(this.value); // Update after the validator is wired up to ensure the value - // is validated + // is validated, wire up undo after updating so the default + // value isn't undoable. setup_validator(); update(); + this.value_undo = new Components.EntryUndo(this.value); } public override void update() { @@ -862,6 +865,7 @@ private class Accounts.ServiceLoginRow : } } + private Components.EntryUndo value_undo; private Application.CommandStack commands; private GLib.Cancellable? cancellable; private ServicePasswordRow? password_row; @@ -894,9 +898,11 @@ private class Accounts.ServiceLoginRow : } // Update after the validator is wired up to ensure the value - // is validated - update(); + // is validated, wire up undo after updating so the default + // value isn't undoable. setup_validator(); + update(); + this.value_undo = new Components.EntryUndo(this.value); } public override void update() { @@ -983,6 +989,7 @@ private class Accounts.ServicePasswordRow : } } + private Components.EntryUndo value_undo; private Application.CommandStack commands; private GLib.Cancellable? cancellable; @@ -1008,9 +1015,11 @@ private class Accounts.ServicePasswordRow : this.validator = new Components.Validator(this.value); // Update after the validator is wired up to ensure the value - // is validated - update(); + // is validated, wire up undo after updating so the default + // value isn't undoable. setup_validator(); + update(); + this.value_undo = new Components.EntryUndo(this.value); } public override void update() { From a5d72891eb5ddd5f6962037b14256afe0373df0b Mon Sep 17 00:00:00 2001 From: Michael Gratton Date: Thu, 7 Nov 2019 11:47:41 +1100 Subject: [PATCH 4/7] Rework action names groups across the client Introduce a new standard edit action group "edt" for editing actions such as copy and undo, separate from the "win" window action group, so that editing actions can be scoped to specific widgets and overridden by children. Add new Action namespace with sub-namespaces for the app, win and edt namespaces and move consts from GearyApplication there. Update call sites and UI files, use consistent approach to setting up action groups across all classes that use them. --- po/POTFILES.in | 1 + .../accounts/accounts-editor-list-pane.vala | 4 +- src/client/accounts/accounts-editor.vala | 18 +-- src/client/application/geary-application.vala | 90 +++++------ src/client/client-action.vala | 73 +++++++++ .../components/components-entry-undo.vala | 35 ++-- .../components/components-inspector.vala | 28 ++-- src/client/components/main-window.vala | 81 ++++++---- src/client/composer/composer-widget.vala | 151 +++++++++--------- .../dialogs-problem-details-dialog.vala | 29 ++-- src/client/meson.build | 2 + .../desktop-notifications.vala | 6 +- ui/accounts_editor_edit_pane.ui | 2 +- ui/components-inspector.ui | 2 +- ui/composer-menus.ui | 34 ++-- ui/composer-widget.ui | 26 +-- 16 files changed, 334 insertions(+), 248 deletions(-) create mode 100644 src/client/client-action.vala diff --git a/po/POTFILES.in b/po/POTFILES.in index 2a4b49a4..4cc9fb4d 100644 --- a/po/POTFILES.in +++ b/po/POTFILES.in @@ -28,6 +28,7 @@ src/client/application/geary-application.vala src/client/application/goa-mediator.vala src/client/application/main.vala src/client/application/secret-mediator.vala +src/client/client-action.vala src/client/components/client-web-view.vala src/client/components/components-attachment-pane.vala src/client/components/components-entry-undo.vala diff --git a/src/client/accounts/accounts-editor-list-pane.vala b/src/client/accounts/accounts-editor-list-pane.vala index 17676098..67b7c26b 100644 --- a/src/client/accounts/accounts-editor-list-pane.vala +++ b/src/client/accounts/accounts-editor-list-pane.vala @@ -245,7 +245,7 @@ internal class Accounts.EditorListPane : Gtk.Grid, EditorPane, CommandPane { if (command.executed_label != null) { Components.InAppNotification ian = new Components.InAppNotification(command.executed_label); - ian.set_button(_("Undo"), "win." + GearyApplication.ACTION_UNDO); + ian.set_button(_("Undo"), Action.Edit.prefix(Action.Edit.UNDO)); this.editor.add_notification(ian); } } @@ -254,7 +254,7 @@ internal class Accounts.EditorListPane : Gtk.Grid, EditorPane, CommandPane { if (command.undone_label != null) { Components.InAppNotification ian = new Components.InAppNotification(command.undone_label); - ian.set_button(_("Redo"), "win." + GearyApplication.ACTION_REDO); + ian.set_button(_("Redo"), Action.Edit.prefix(Action.Edit.REDO)); this.editor.add_notification(ian); } } diff --git a/src/client/accounts/accounts-editor.vala b/src/client/accounts/accounts-editor.vala index 740de633..7f3252bb 100644 --- a/src/client/accounts/accounts-editor.vala +++ b/src/client/accounts/accounts-editor.vala @@ -18,9 +18,9 @@ public class Accounts.Editor : Gtk.Dialog { - private const ActionEntry[] ACTION_ENTRIES = { - { GearyApplication.ACTION_REDO, on_redo }, - { GearyApplication.ACTION_UNDO, on_undo }, + private const ActionEntry[] EDIT_ACTIONS = { + { Action.Edit.REDO, on_redo }, + { Action.Edit.UNDO, on_undo }, }; @@ -40,7 +40,7 @@ public class Accounts.Editor : Gtk.Dialog { get; private set; } - private SimpleActionGroup actions = new SimpleActionGroup(); + private GLib.SimpleActionGroup edit_actions = new GLib.SimpleActionGroup(); [GtkChild] private Gtk.Overlay notifications_pane; @@ -67,8 +67,8 @@ public class Accounts.Editor : Gtk.Dialog { this.accounts = application.controller.account_manager; - this.actions.add_action_entries(ACTION_ENTRIES, this); - insert_action_group("win", this.actions); + this.edit_actions.add_action_entries(EDIT_ACTIONS, this); + insert_action_group(Action.Edit.GROUP_NAME, this.edit_actions); this.editor_list_pane = new EditorListPane(this); push(this.editor_list_pane); @@ -227,8 +227,8 @@ public class Accounts.Editor : Gtk.Dialog { can_redo = pane.commands.can_redo; } - get_action(GearyApplication.ACTION_UNDO).set_enabled(can_undo); - get_action(GearyApplication.ACTION_REDO).set_enabled(can_redo); + get_action(Action.Edit.UNDO).set_enabled(can_undo); + get_action(Action.Edit.REDO).set_enabled(can_redo); } private inline EditorPane? get_current_pane() { @@ -236,7 +236,7 @@ public class Accounts.Editor : Gtk.Dialog { } private inline GLib.SimpleAction get_action(string name) { - return (GLib.SimpleAction) this.actions.lookup_action(name); + return (GLib.SimpleAction) this.edit_actions.lookup_action(name); } private void on_undo() { diff --git a/src/client/application/geary-application.vala b/src/client/application/geary-application.vala index 3b416cd5..2b633edc 100644 --- a/src/client/application/geary-application.vala +++ b/src/client/application/geary-application.vala @@ -20,6 +20,7 @@ extern const string _PROFILE; extern const string _VERSION; extern const string _REVNO; + /** * The interface between Geary and the desktop environment. */ @@ -52,25 +53,6 @@ public class GearyApplication : Gtk.Application { null }; - // Common window actions - public const string ACTION_CLOSE = "close"; - public const string ACTION_COPY = "copy"; - public const string ACTION_HELP_OVERLAY = "show-help-overlay"; - public const string ACTION_REDO = "redo"; - public const string ACTION_UNDO = "undo"; - - // App-wide actions - public const string ACTION_ABOUT = "about"; - public const string ACTION_ACCOUNTS = "accounts"; - public const string ACTION_COMPOSE = "compose"; - public const string ACTION_INSPECT = "inspect"; - public const string ACTION_HELP = "help"; - public const string ACTION_MAILTO = "mailto"; - public const string ACTION_PREFERENCES = "preferences"; - public const string ACTION_SHOW_EMAIL = "show-email"; - public const string ACTION_SHOW_FOLDER = "show-folder"; - public const string ACTION_QUIT = "quit"; - // Local-only command line options private const string OPTION_VERSION = "version"; @@ -90,16 +72,16 @@ public class GearyApplication : Gtk.Application { private const string OPTION_REVOKE_CERTS = "revoke-certs"; private const ActionEntry[] ACTION_ENTRIES = { - {ACTION_ABOUT, on_activate_about}, - {ACTION_ACCOUNTS, on_activate_accounts}, - {ACTION_COMPOSE, on_activate_compose}, - {ACTION_HELP, on_activate_help}, - {ACTION_INSPECT, on_activate_inspect}, - {ACTION_MAILTO, on_activate_mailto, "s"}, - {ACTION_PREFERENCES, on_activate_preferences}, - {ACTION_QUIT, on_activate_quit}, - {ACTION_SHOW_EMAIL, on_activate_show_email, "(svv)"}, - {ACTION_SHOW_FOLDER, on_activate_show_folder, "(sv)"} + { Action.Application.ABOUT, on_activate_about}, + { Action.Application.ACCOUNTS, on_activate_accounts}, + { Action.Application.COMPOSE, on_activate_compose}, + { Action.Application.HELP, on_activate_help}, + { Action.Application.INSPECT, on_activate_inspect}, + { Action.Application.MAILTO, on_activate_mailto, "s"}, + { Action.Application.PREFERENCES, on_activate_preferences}, + { Action.Application.QUIT, on_activate_quit}, + { Action.Application.SHOW_EMAIL, on_activate_show_email, "(svv)"}, + { Action.Application.SHOW_FOLDER, on_activate_show_folder, "(sv)"} }; // This is also the order in which they are presented to the user, @@ -435,22 +417,26 @@ public class GearyApplication : Gtk.Application { Gtk.Window.set_default_icon_name(APP_ID); // Application accels - add_app_accelerators(ACTION_COMPOSE, { "N" }); - add_app_accelerators(ACTION_HELP, { "F1" }); - add_app_accelerators(ACTION_INSPECT, { "I" }); - add_app_accelerators(ACTION_QUIT, { "Q" }); + add_app_accelerators(Action.Application.COMPOSE, { "N" }); + add_app_accelerators(Action.Application.HELP, { "F1" }); + add_app_accelerators(Action.Application.INSPECT, { "I" }); + add_app_accelerators(Action.Application.QUIT, { "Q" }); // Common window accels - add_window_accelerators(ACTION_CLOSE, { "W" }); - add_window_accelerators(ACTION_COPY, { "C" }); - add_window_accelerators(ACTION_HELP_OVERLAY, { "F1", "question" }); - add_window_accelerators(ACTION_REDO, { "Z" }); - add_window_accelerators(ACTION_UNDO, { "Z" }); + add_window_accelerators(Action.Window.CLOSE, { "W" }); + add_window_accelerators( + Action.Window.SHORTCUT_HELP, { "F1", "question" } + ); - MainWindow.add_window_accelerators(this); - ComposerWidget.add_window_accelerators(this); - Components.EntryUndo.add_window_accelerators(this); - Components.Inspector.add_window_accelerators(this); + // Common edit accels + add_edit_accelerators(Action.Edit.COPY, { "C" }); + add_edit_accelerators(Action.Edit.REDO, { "Z" }); + add_edit_accelerators(Action.Edit.UNDO, { "Z" }); + + MainWindow.add_accelerators(this); + ComposerWidget.add_accelerators(this); + Components.Inspector.add_accelerators(this); + Dialogs.ProblemDetailsDialog.add_accelerators(this); if (this.is_background_service) { // Since command_line won't be called below if running as @@ -495,7 +481,18 @@ public class GearyApplication : Gtk.Application { public void add_window_accelerators(string action, string[] accelerators, Variant? param = null) { - string name = "win." + action; + string name = Action.Window.prefix(action); + string[] all_accel = get_accels_for_action(name); + foreach (string accel in accelerators) { + all_accel += accel; + } + set_accels_for_action(name, all_accel); + } + + public void add_edit_accelerators(string action, + string[] accelerators, + Variant? param = null) { + string name = Action.Edit.prefix(action); string[] all_accel = get_accels_for_action(name); foreach (string accel in accelerators) { all_accel += accel; @@ -876,13 +873,10 @@ public class GearyApplication : Gtk.Application { foreach (string arg in args) { // the only acceptable arguments are mailto:'s if (arg == MAILTO_URI_SCHEME_PREFIX) { - activate_action(GearyApplication.ACTION_COMPOSE, null); + activate_action(Action.Application.COMPOSE, null); activated = true; } else if (arg.down().has_prefix(MAILTO_URI_SCHEME_PREFIX)) { - activate_action( - GearyApplication.ACTION_MAILTO, - new GLib.Variant.string(arg) - ); + activate_action(Action.Application.MAILTO, new GLib.Variant.string(arg)); activated = true; } else { command_line.printerr("%s: ", this.binary); diff --git a/src/client/client-action.vala b/src/client/client-action.vala new file mode 100644 index 00000000..a5bcf047 --- /dev/null +++ b/src/client/client-action.vala @@ -0,0 +1,73 @@ +/* + * Copyright 2019 Michael Gratton + * + * This software is licensed under the GNU Lesser General Public License + * (version 2.1 or later). See the COPYING file in this distribution. + */ + +/** Common client GAction and action group names */ +namespace Action { + + + /** Common application GAction names. */ + namespace Application { + + /** Application GAction group name */ + public const string GROUP_NAME = "app"; + + public const string ABOUT = "about"; + public const string ACCOUNTS = "accounts"; + public const string COMPOSE = "compose"; + public const string INSPECT = "inspect"; + public const string HELP = "help"; + public const string MAILTO = "mailto"; + public const string PREFERENCES = "preferences"; + public const string SHOW_EMAIL = "show-email"; + public const string SHOW_FOLDER = "show-folder"; + public const string QUIT = "quit"; + + + /** Returns the given action name prefixed with the group name. */ + public string prefix(string action_name) { + return GROUP_NAME + "." + action_name; + } + + } + + + /** Common window GAction names. */ + namespace Window { + + /** Window GAction group name */ + public const string GROUP_NAME = "win"; + + public const string CLOSE = "close"; + public const string SHORTCUT_HELP = "show-help-overlay"; + + + /** Returns the given action name prefixed with the group name. */ + public string prefix(string action_name) { + return GROUP_NAME + "." + action_name; + } + + } + + /** Common editing GAction names. */ + namespace Edit { + + /** Editing GAction group name */ + public const string GROUP_NAME = "edt"; + + public const string COPY = "copy"; + public const string REDO = "redo"; + public const string UNDO = "undo"; + + + /** Returns the given action name prefixed with the group name. */ + public string prefix(string action_name) { + return GROUP_NAME + "." + action_name; + } + + } + +} diff --git a/src/client/components/components-entry-undo.vala b/src/client/components/components-entry-undo.vala index 18382e34..9928c524 100644 --- a/src/client/components/components-entry-undo.vala +++ b/src/client/components/components-entry-undo.vala @@ -11,10 +11,9 @@ public class Components.EntryUndo : Geary.BaseObject { - private const string ACTION_GROUP = "ceu"; - private const ActionEntry[] action_entries = { - { GearyApplication.ACTION_UNDO, on_undo }, - { GearyApplication.ACTION_REDO, on_redo }, + private const ActionEntry[] EDIT_ACTIONS = { + { Action.Edit.UNDO, on_undo }, + { Action.Edit.REDO, on_redo }, }; @@ -94,18 +93,6 @@ public class Components.EntryUndo : Geary.BaseObject { } - public static void add_window_accelerators(GearyApplication application) { - application.set_accels_for_action( - ACTION_GROUP + "." + GearyApplication.ACTION_UNDO, - { "z" } - ); - application.set_accels_for_action( - ACTION_GROUP + "." + GearyApplication.ACTION_REDO, - { "z" } - ); - } - - /** The entry being managed */ public Gtk.Entry target { get; private set; } @@ -117,12 +104,14 @@ public class Components.EntryUndo : Geary.BaseObject { private bool events_enabled = true; - private GLib.SimpleActionGroup entry_actions = new SimpleActionGroup(); + private GLib.SimpleActionGroup edit_actions = new GLib.SimpleActionGroup(); public EntryUndo(Gtk.Entry target) { + this.edit_actions.add_action_entries(EDIT_ACTIONS, this); + this.target = target; - this.target.insert_action_group(ACTION_GROUP, this.entry_actions); + this.target.insert_action_group(Action.Edit.GROUP_NAME, this.edit_actions); this.target.insert_text.connect(on_inserted); this.target.delete_text.connect(on_deleted); @@ -130,8 +119,6 @@ public class Components.EntryUndo : Geary.BaseObject { this.commands.executed.connect(this.update_command_actions); this.commands.undone.connect(this.update_command_actions); this.commands.redone.connect(this.update_command_actions); - - this.entry_actions.add_action_entries(EntryUndo.action_entries, this); } ~EntryUndo() { @@ -234,10 +221,10 @@ public class Components.EntryUndo : Geary.BaseObject { } private void update_command_actions() { - ((GLib.SimpleAction) this.entry_actions.lookup_action( - GearyApplication.ACTION_UNDO)).set_enabled(this.commands.can_undo); - ((GLib.SimpleAction) this.entry_actions.lookup_action( - GearyApplication.ACTION_REDO)).set_enabled(this.commands.can_redo); + ((GLib.SimpleAction) this.edit_actions.lookup_action(Action.Edit.UNDO)) + .set_enabled(this.commands.can_undo); + ((GLib.SimpleAction) this.edit_actions.lookup_action(Action.Edit.REDO)) + .set_enabled(this.commands.can_redo); } private void on_inserted(string inserted, int inserted_len, ref int pos) { diff --git a/src/client/components/components-inspector.vala b/src/client/components/components-inspector.vala index f7d1b1a4..5af29b41 100644 --- a/src/client/components/components-inspector.vala +++ b/src/client/components/components-inspector.vala @@ -28,16 +28,20 @@ public class Components.Inspector : Gtk.ApplicationWindow { private const string ACTION_SEARCH_TOGGLE = "toggle-search"; private const string ACTION_SEARCH_ACTIVATE = "activate-search"; - private const ActionEntry[] action_entries = { - {GearyApplication.ACTION_CLOSE, on_close }, - {GearyApplication.ACTION_COPY, on_copy_clicked }, - {ACTION_CLOSE, on_close }, - {ACTION_PLAY_TOGGLE, on_logs_play_toggled, null, "true" }, - {ACTION_SEARCH_TOGGLE, on_logs_search_toggled, null, "false" }, - {ACTION_SEARCH_ACTIVATE, on_logs_search_activated }, + private const ActionEntry[] EDIT_ACTIONS = { + { Action.Edit.COPY, on_copy_clicked }, }; - public static void add_window_accelerators(GearyApplication app) { + private const ActionEntry[] WINDOW_ACTIONS = { + { Action.Window.CLOSE, on_close }, + { ACTION_CLOSE, on_close }, + { ACTION_PLAY_TOGGLE, on_logs_play_toggled, null, "true" }, + { ACTION_SEARCH_TOGGLE, on_logs_search_toggled, null, "false" }, + { ACTION_SEARCH_ACTIVATE, on_logs_search_activated }, + }; + + + public static void add_accelerators(GearyApplication app) { app.add_window_accelerators(ACTION_CLOSE, { "Escape" } ); app.add_window_accelerators(ACTION_PLAY_TOGGLE, { "space" } ); app.add_window_accelerators(ACTION_SEARCH_ACTIVATE, { "F" } ); @@ -67,7 +71,13 @@ public class Components.Inspector : Gtk.ApplicationWindow { Object(application: application); this.title = this.header_bar.title = _("Inspector"); - add_action_entries(Inspector.action_entries, this); + // Edit actions + GLib.SimpleActionGroup edit_actions = new GLib.SimpleActionGroup(); + edit_actions.add_action_entries(EDIT_ACTIONS, this); + insert_action_group(Action.Edit.GROUP_NAME, edit_actions); + + // Window actions + add_action_entries(WINDOW_ACTIONS, this); this.log_pane = new InspectorLogView(application.config, null); this.log_pane.record_selection_changed.connect( diff --git a/src/client/components/main-window.vala b/src/client/components/main-window.vala index 397ffce4..bc000bbb 100644 --- a/src/client/components/main-window.vala +++ b/src/client/components/main-window.vala @@ -34,14 +34,13 @@ public class MainWindow : Gtk.ApplicationWindow, Geary.BaseInterface { public const string ACTION_TRASH_CONVERSATION = "trash-conversation"; public const string ACTION_ZOOM = "zoom"; - private const int STATUS_BAR_HEIGHT = 18; - private const int UPDATE_UI_INTERVAL = 60; - private const int MIN_CONVERSATION_COUNT = 50; + private const ActionEntry[] EDIT_ACTIONS = { + { Action.Edit.UNDO, on_undo }, + { Action.Edit.REDO, on_redo }, + }; - private const ActionEntry[] win_action_entries = { - { GearyApplication.ACTION_CLOSE, on_close }, - { GearyApplication.ACTION_UNDO, on_undo }, - { GearyApplication.ACTION_REDO, on_redo }, + private const ActionEntry[] WINDOW_ACTIONS = { + { Action.Window.CLOSE, on_close }, { ACTION_CONVERSATION_LIST, on_conversation_list }, { ACTION_FIND_IN_CONVERSATION, on_find_in_conversation_action }, @@ -70,8 +69,12 @@ public class MainWindow : Gtk.ApplicationWindow, Geary.BaseInterface { { ACTION_ZOOM, on_zoom, "s" }, }; + private const int STATUS_BAR_HEIGHT = 18; + private const int UPDATE_UI_INTERVAL = 60; + private const int MIN_CONVERSATION_COUNT = 50; - public static void add_window_accelerators(GearyApplication owner) { + + public static void add_accelerators(GearyApplication owner) { // Marking actions // // Unread is the primary action, so it doesn't get the @@ -209,6 +212,8 @@ public class MainWindow : Gtk.ApplicationWindow, Geary.BaseInterface { private Application.Controller.AccountContext? context = null; + private GLib.SimpleActionGroup edit_actions = new GLib.SimpleActionGroup(); + // Determines if the conversation viewer should autoselect on next // load private bool previous_selection_was_interactive = false; @@ -277,7 +282,13 @@ public class MainWindow : Gtk.ApplicationWindow, Geary.BaseInterface { load_config(application.config); restore_saved_window_state(); - add_action_entries(win_action_entries, this); + + // Edit actions + this.edit_actions.add_action_entries(EDIT_ACTIONS, this); + insert_action_group(Action.Edit.GROUP_NAME, this.edit_actions); + + // Window actions + add_action_entries(MainWindow.WINDOW_ACTIONS, this); set_styling(); setup_layout(application.config); @@ -572,7 +583,7 @@ public class MainWindow : Gtk.ApplicationWindow, Geary.BaseInterface { new ComposerWindow(composer, this.application); } else { this.conversation_viewer.do_compose(composer); - get_action(ACTION_FIND_IN_CONVERSATION).set_enabled(false); + get_window_action(ACTION_FIND_IN_CONVERSATION).set_enabled(false); } } @@ -971,10 +982,10 @@ public class MainWindow : Gtk.ApplicationWindow, Geary.BaseInterface { private void update_command_actions() { Application.Controller.AccountContext? selected = this.context; - get_action(GearyApplication.ACTION_UNDO).set_enabled( + get_edit_action(Action.Edit.UNDO).set_enabled( selected != null && selected.commands.can_undo ); - get_action(GearyApplication.ACTION_REDO).set_enabled( + get_edit_action(Action.Edit.REDO).set_enabled( selected != null && selected.commands.can_redo ); } @@ -1381,7 +1392,7 @@ public class MainWindow : Gtk.ApplicationWindow, Geary.BaseInterface { bool sensitive = (count != NONE); bool multiple = (count == MULTIPLE); - get_action(ACTION_FIND_IN_CONVERSATION).set_enabled( + get_window_action(ACTION_FIND_IN_CONVERSATION).set_enabled( sensitive && !multiple ); @@ -1391,29 +1402,29 @@ public class MainWindow : Gtk.ApplicationWindow, Geary.BaseInterface { this.selected_folder != null && this.selected_folder.special_folder_type != DRAFTS ); - get_action(ACTION_REPLY_CONVERSATION).set_enabled(reply_sensitive); - get_action(ACTION_REPLY_ALL_CONVERSATION).set_enabled(reply_sensitive); - get_action(ACTION_FORWARD_CONVERSATION).set_enabled(reply_sensitive); + get_window_action(ACTION_REPLY_CONVERSATION).set_enabled(reply_sensitive); + get_window_action(ACTION_REPLY_ALL_CONVERSATION).set_enabled(reply_sensitive); + get_window_action(ACTION_FORWARD_CONVERSATION).set_enabled(reply_sensitive); bool move_enabled = ( sensitive && (selected_folder is Geary.FolderSupport.Move) ); this.main_toolbar.move_message_button.set_sensitive(move_enabled); - get_action(ACTION_SHOW_MOVE_MENU).set_enabled(move_enabled); + get_window_action(ACTION_SHOW_MOVE_MENU).set_enabled(move_enabled); bool copy_enabled = ( sensitive && (selected_folder is Geary.FolderSupport.Copy) ); this.main_toolbar.copy_message_button.set_sensitive(copy_enabled); - get_action(ACTION_SHOW_COPY_MENU).set_enabled(move_enabled); + get_window_action(ACTION_SHOW_COPY_MENU).set_enabled(move_enabled); - get_action(ACTION_ARCHIVE_CONVERSATION).set_enabled( + get_window_action(ACTION_ARCHIVE_CONVERSATION).set_enabled( sensitive && (selected_folder is Geary.FolderSupport.Archive) ); - get_action(ACTION_TRASH_CONVERSATION).set_enabled( + get_window_action(ACTION_TRASH_CONVERSATION).set_enabled( sensitive && this.selected_folder_supports_trash ); - get_action(ACTION_DELETE_CONVERSATION).set_enabled( + get_window_action(ACTION_DELETE_CONVERSATION).set_enabled( sensitive && (selected_folder is Geary.FolderSupport.Remove) ); @@ -1456,15 +1467,15 @@ public class MainWindow : Gtk.ApplicationWindow, Geary.BaseInterface { supported_operations.add_all(selected_operations.get_values()); } - get_action(ACTION_SHOW_MARK_MENU).set_enabled( + get_window_action(ACTION_SHOW_MARK_MENU).set_enabled( sensitive && (typeof(Geary.FolderSupport.Mark) in supported_operations) ); - get_action(ACTION_SHOW_COPY_MENU).set_enabled( + get_window_action(ACTION_SHOW_COPY_MENU).set_enabled( sensitive && (supported_operations.contains(typeof(Geary.FolderSupport.Copy))) ); - get_action(ACTION_SHOW_MOVE_MENU).set_enabled( + get_window_action(ACTION_SHOW_MOVE_MENU).set_enabled( sensitive && (supported_operations.contains(typeof(Geary.FolderSupport.Move))) ); @@ -1491,10 +1502,14 @@ public class MainWindow : Gtk.ApplicationWindow, Geary.BaseInterface { } } - private SimpleAction get_action(string name) { + private SimpleAction get_window_action(string name) { return (SimpleAction) lookup_action(name); } + private SimpleAction get_edit_action(string name) { + return (SimpleAction) this.edit_actions.lookup_action(name); + } + private void on_scan_completed(Geary.App.ConversationMonitor monitor) { // Done scanning. Check if we have enough messages to fill // the conversation list; if not, trigger a load_more(); @@ -1612,7 +1627,7 @@ public class MainWindow : Gtk.ApplicationWindow, Geary.BaseInterface { if (command.undone_label != null) { Components.InAppNotification ian = new Components.InAppNotification(command.undone_label); - ian.set_button(_("Redo"), "win." + GearyApplication.ACTION_REDO); + ian.set_button(_("Redo"), Action.Edit.prefix(Action.Edit.REDO)); add_notification(ian); } update_command_actions(); @@ -1621,8 +1636,8 @@ public class MainWindow : Gtk.ApplicationWindow, Geary.BaseInterface { private void on_command_redo(Application.Command command) { if (command.executed_label != null) { Components.InAppNotification ian = - new Components.InAppNotification(command.executed_label); - ian.set_button(_("Undo"), "win." + GearyApplication.ACTION_UNDO); + new Components.InAppNotification(command.executed_label); + ian.set_button(_("Undo"), Action.Edit.prefix(Action.Edit.UNDO)); add_notification(ian); } update_command_actions(); @@ -1783,14 +1798,14 @@ public class MainWindow : Gtk.ApplicationWindow, Geary.BaseInterface { unstarred_selected = true; } } - get_action(ACTION_MARK_AS_READ).set_enabled(unread_selected); - get_action(ACTION_MARK_AS_UNREAD).set_enabled(read_selected); - get_action(ACTION_MARK_AS_STARRED).set_enabled(unstarred_selected); - get_action(ACTION_MARK_AS_UNSTARRED).set_enabled(starred_selected); + get_window_action(ACTION_MARK_AS_READ).set_enabled(unread_selected); + get_window_action(ACTION_MARK_AS_UNREAD).set_enabled(read_selected); + get_window_action(ACTION_MARK_AS_STARRED).set_enabled(unstarred_selected); + get_window_action(ACTION_MARK_AS_UNSTARRED).set_enabled(starred_selected); // If we're in Drafts/Outbox, we also shouldn't set a message as SPAM. bool in_spam_folder = selected_folder.special_folder_type == Geary.SpecialFolderType.SPAM; - get_action(ACTION_TOGGLE_SPAM).set_enabled(!in_spam_folder && + get_window_action(ACTION_TOGGLE_SPAM).set_enabled(!in_spam_folder && selected_folder.special_folder_type != Geary.SpecialFolderType.DRAFTS && selected_folder.special_folder_type != Geary.SpecialFolderType.OUTBOX); } diff --git a/src/client/composer/composer-widget.vala b/src/client/composer/composer-widget.vala index 791a285b..11ce04f5 100644 --- a/src/client/composer/composer-widget.vala +++ b/src/client/composer/composer-widget.vala @@ -90,70 +90,71 @@ public class ComposerWidget : Gtk.EventBox, Geary.BaseInterface { // ACTION_INSERT_LINK and ACTION_REMOVE_FORMAT are missing from // here since they are handled in update_selection_actions - private const string[] html_actions = { + private const string[] HTML_ACTIONS = { ACTION_BOLD, ACTION_ITALIC, ACTION_UNDERLINE, ACTION_STRIKETHROUGH, ACTION_FONT_SIZE, ACTION_FONT_FAMILY, ACTION_COLOR, ACTION_JUSTIFY, ACTION_INSERT_IMAGE, ACTION_COPY_LINK, ACTION_OLIST, ACTION_ULIST }; - private const ActionEntry[] editor_action_entries = { - {GearyApplication.ACTION_UNDO, on_undo }, - {GearyApplication.ACTION_REDO, on_redo }, - {GearyApplication.ACTION_COPY, on_copy }, - {ACTION_CUT, on_cut }, - {ACTION_COPY_LINK, on_copy_link }, - {ACTION_PASTE, on_paste }, - {ACTION_PASTE_WITHOUT_FORMATTING, on_paste_without_formatting }, - {ACTION_SELECT_ALL, on_select_all }, - {ACTION_BOLD, on_action, null, "false" }, - {ACTION_ITALIC, on_action, null, "false" }, - {ACTION_UNDERLINE, on_action, null, "false" }, - {ACTION_STRIKETHROUGH, on_action, null, "false" }, - {ACTION_FONT_SIZE, on_font_size, "s", "'medium'" }, - {ACTION_FONT_FAMILY, on_font_family, "s", "'sans'" }, - {ACTION_REMOVE_FORMAT, on_remove_format, null, "false" }, - {ACTION_INDENT, on_indent }, - {ACTION_OLIST, on_olist }, - {ACTION_ULIST, on_ulist }, - {ACTION_OUTDENT, on_action }, - {ACTION_JUSTIFY, on_justify, "s", "'left'" }, - {ACTION_COLOR, on_select_color }, - {ACTION_INSERT_IMAGE, on_insert_image }, - {ACTION_INSERT_LINK, on_insert_link }, - {ACTION_OPEN_INSPECTOR, on_open_inspector }, + private const ActionEntry[] EDITOR_ACTIONS = { + { Action.Edit.COPY, on_copy }, + { Action.Edit.REDO, on_redo }, + { Action.Edit.UNDO, on_undo }, + { ACTION_BOLD, on_action, null, "false" }, + { ACTION_COLOR, on_select_color }, + { ACTION_COPY_LINK, on_copy_link }, + { ACTION_CUT, on_cut }, + { ACTION_FONT_FAMILY, on_font_family, "s", "'sans'" }, + { ACTION_FONT_SIZE, on_font_size, "s", "'medium'" }, + { ACTION_INDENT, on_indent }, + { ACTION_INSERT_IMAGE, on_insert_image }, + { ACTION_INSERT_LINK, on_insert_link }, + { ACTION_ITALIC, on_action, null, "false" }, + { ACTION_JUSTIFY, on_justify, "s", "'left'" }, + { ACTION_OLIST, on_olist }, + { ACTION_OUTDENT, on_action }, + { ACTION_PASTE, on_paste }, + { ACTION_PASTE_WITHOUT_FORMATTING, on_paste_without_formatting }, + { ACTION_REMOVE_FORMAT, on_remove_format, null, "false" }, + { ACTION_SELECT_ALL, on_select_all }, + { ACTION_STRIKETHROUGH, on_action, null, "false" }, + { ACTION_ULIST, on_ulist }, + { ACTION_UNDERLINE, on_action, null, "false" }, }; - private const ActionEntry[] composer_action_entries = { - {GearyApplication.ACTION_CLOSE, on_close }, - {ACTION_CLOSE, on_close }, - {ACTION_ADD_ATTACHMENT, on_add_attachment }, - {ACTION_ADD_ORIGINAL_ATTACHMENTS, on_pending_attachments }, - {ACTION_CLOSE_AND_DISCARD, on_close_and_discard }, - {ACTION_CLOSE_AND_SAVE, on_close_and_save }, - {ACTION_COMPOSE_AS_HTML, on_toggle_action, null, "true", on_compose_as_html_toggled }, - {ACTION_DETACH, on_detach }, - {ACTION_SELECT_DICTIONARY, on_select_dictionary }, - {ACTION_SEND, on_send }, - {ACTION_SHOW_EXTENDED, on_toggle_action, null, "false", on_show_extended_toggled }, + private const ActionEntry[] COMPOSER_ACTIONS = { + { Action.Window.CLOSE, on_close }, + { ACTION_ADD_ATTACHMENT, on_add_attachment }, + { ACTION_ADD_ORIGINAL_ATTACHMENTS, on_pending_attachments }, + { ACTION_CLOSE, on_close }, + { ACTION_CLOSE_AND_DISCARD, on_close_and_discard }, + { ACTION_CLOSE_AND_SAVE, on_close_and_save }, + { ACTION_COMPOSE_AS_HTML, on_toggle_action, null, "true", on_compose_as_html_toggled }, + { ACTION_DETACH, on_detach }, + { ACTION_OPEN_INSPECTOR, on_open_inspector }, + { ACTION_SELECT_DICTIONARY, on_select_dictionary }, + { ACTION_SEND, on_send }, + { ACTION_SHOW_EXTENDED, on_toggle_action, null, "false", on_show_extended_toggled }, }; - public static void add_window_accelerators(GearyApplication application) { + public static void add_accelerators(GearyApplication application) { application.add_window_accelerators(ACTION_CLOSE, { "Escape" } ); - application.add_window_accelerators(ACTION_CUT, { "x" } ); - application.add_window_accelerators(ACTION_PASTE, { "v" } ); - application.add_window_accelerators(ACTION_PASTE_WITHOUT_FORMATTING, { "v" } ); - application.add_window_accelerators(ACTION_INSERT_IMAGE, { "g" } ); - application.add_window_accelerators(ACTION_INSERT_LINK, { "l" } ); - application.add_window_accelerators(ACTION_INDENT, { "bracketright" } ); - application.add_window_accelerators(ACTION_OUTDENT, { "bracketleft" } ); - application.add_window_accelerators(ACTION_REMOVE_FORMAT, { "space" } ); - application.add_window_accelerators(ACTION_BOLD, { "b" } ); - application.add_window_accelerators(ACTION_ITALIC, { "i" } ); - application.add_window_accelerators(ACTION_UNDERLINE, { "u" } ); - application.add_window_accelerators(ACTION_STRIKETHROUGH, { "k" } ); application.add_window_accelerators(ACTION_ADD_ATTACHMENT, { "t" } ); application.add_window_accelerators(ACTION_DETACH, { "d" } ); + + application.add_edit_accelerators(ACTION_CUT, { "x" } ); + application.add_edit_accelerators(ACTION_PASTE, { "v" } ); + application.add_edit_accelerators(ACTION_PASTE_WITHOUT_FORMATTING, { "v" } ); + application.add_edit_accelerators(ACTION_INSERT_IMAGE, { "g" } ); + application.add_edit_accelerators(ACTION_INSERT_LINK, { "l" } ); + application.add_edit_accelerators(ACTION_INDENT, { "bracketright" } ); + application.add_edit_accelerators(ACTION_OUTDENT, { "bracketleft" } ); + application.add_edit_accelerators(ACTION_REMOVE_FORMAT, { "space" } ); + application.add_edit_accelerators(ACTION_BOLD, { "b" } ); + application.add_edit_accelerators(ACTION_ITALIC, { "i" } ); + application.add_edit_accelerators(ACTION_UNDERLINE, { "u" } ); + application.add_edit_accelerators(ACTION_STRIKETHROUGH, { "k" } ); } private const string DRAFT_SAVED_TEXT = _("Saved"); @@ -338,8 +339,8 @@ public class ComposerWidget : Gtk.EventBox, Geary.BaseInterface { [GtkChild] private Gtk.Label info_label; - private SimpleActionGroup composer_actions = new SimpleActionGroup(); - private SimpleActionGroup editor_actions = new SimpleActionGroup(); + private GLib.SimpleActionGroup composer_actions = new GLib.SimpleActionGroup(); + private GLib.SimpleActionGroup editor_actions = new GLib.SimpleActionGroup(); private Menu html_menu; private Menu plain_menu; @@ -875,29 +876,23 @@ public class ComposerWidget : Gtk.EventBox, Geary.BaseInterface { // Initializes all actions and adds them to the action group private void initialize_actions() { // Composer actions - this.composer_actions.add_action_entries( - ComposerWidget.composer_action_entries, this - ); - // Main actions use 'win' prefix so they override main window - // action. But for some reason, we can't use the same prefix - // for the headerbar. - insert_action_group("win", this.composer_actions); + this.composer_actions.add_action_entries(COMPOSER_ACTIONS, this); + // Main actions use the window prefix so they override main + // window actions. But for some reason, we can't use the same + // prefix for the headerbar. + insert_action_group(Action.Window.GROUP_NAME, this.composer_actions); this.header.insert_action_group("cmh", this.composer_actions); - // Editor actions - scoped to the editor only. Need to include - // composer actions however since if not found in this group, - // ancestors (including the composer's) will not be consulted. - this.editor_actions.add_action_entries( - ComposerWidget.composer_action_entries, this + // Editor actions - scoped to the editor only. + this.editor_actions.add_action_entries(EDITOR_ACTIONS, this); + this.editor_container.insert_action_group( + Action.Edit.GROUP_NAME, this.editor_actions ); - this.editor_actions.add_action_entries( - ComposerWidget.editor_action_entries, this - ); - this.editor_container.insert_action_group("win", this.editor_actions); - SimpleActionGroup[] composer_action_entries_users - = {this.editor_actions, this.composer_actions}; - foreach (SimpleActionGroup entries_users in composer_action_entries_users) { + GLib.SimpleActionGroup[] composer_action_entries_users = { + this.editor_actions, this.composer_actions + }; + foreach (var entries_users in composer_action_entries_users) { entries_users.change_action_state(ACTION_SHOW_EXTENDED, false); entries_users.change_action_state( ACTION_COMPOSE_AS_HTML, this.application.config.compose_as_html @@ -905,8 +900,8 @@ public class ComposerWidget : Gtk.EventBox, Geary.BaseInterface { } get_action(ACTION_CLOSE_AND_SAVE).set_enabled(false); - get_action(GearyApplication.ACTION_UNDO).set_enabled(false); - get_action(GearyApplication.ACTION_REDO).set_enabled(false); + get_action(Action.Edit.UNDO).set_enabled(false); + get_action(Action.Edit.REDO).set_enabled(false); update_cursor_actions(); } @@ -914,7 +909,7 @@ public class ComposerWidget : Gtk.EventBox, Geary.BaseInterface { private void update_cursor_actions() { bool has_selection = this.editor.has_selection; get_action(ACTION_CUT).set_enabled(has_selection); - get_action(GearyApplication.ACTION_COPY).set_enabled(has_selection); + get_action(Action.Edit.COPY).set_enabled(has_selection); get_action(ACTION_INSERT_LINK).set_enabled( this.editor.is_rich_text && (has_selection || this.cursor_url != null) @@ -1913,7 +1908,7 @@ public class ComposerWidget : Gtk.EventBox, Geary.BaseInterface { bool compose_as_html = new_state.get_boolean(); action.set_state(compose_as_html); - foreach (string html_action in html_actions) + foreach (string html_action in HTML_ACTIONS) get_action(html_action).set_enabled(compose_as_html); update_cursor_actions(); @@ -2362,8 +2357,8 @@ public class ComposerWidget : Gtk.EventBox, Geary.BaseInterface { } private void on_command_state_changed(bool can_undo, bool can_redo) { - get_action(GearyApplication.ACTION_UNDO).set_enabled(can_undo); - get_action(GearyApplication.ACTION_REDO).set_enabled(can_redo); + get_action(Action.Edit.UNDO).set_enabled(can_undo); + get_action(Action.Edit.REDO).set_enabled(can_redo); } private void on_editor_content_loaded() { diff --git a/src/client/dialogs/dialogs-problem-details-dialog.vala b/src/client/dialogs/dialogs-problem-details-dialog.vala index 72478079..59e4c97f 100644 --- a/src/client/dialogs/dialogs-problem-details-dialog.vala +++ b/src/client/dialogs/dialogs-problem-details-dialog.vala @@ -16,15 +16,18 @@ public class Dialogs.ProblemDetailsDialog : Hdy.Dialog { private const string ACTION_SEARCH_TOGGLE = "toggle-search"; private const string ACTION_SEARCH_ACTIVATE = "activate-search"; - private const ActionEntry[] action_entries = { - {GearyApplication.ACTION_CLOSE, on_close }, - {GearyApplication.ACTION_COPY, on_copy_clicked }, - {ACTION_CLOSE, on_close }, - {ACTION_SEARCH_TOGGLE, on_logs_search_toggled, null, "false" }, - {ACTION_SEARCH_ACTIVATE, on_logs_search_activated }, + private const ActionEntry[] EDIT_ACTIONS = { + { Action.Edit.COPY, on_copy_clicked }, }; - public static void add_window_accelerators(GearyApplication app) { + private const ActionEntry[] WINDOW_ACTIONS = { + { Action.Window.CLOSE, on_close }, + { ACTION_CLOSE, on_close }, + { ACTION_SEARCH_TOGGLE, on_logs_search_toggled, null, "false" }, + { ACTION_SEARCH_ACTIVATE, on_logs_search_activated }, + }; + + public static void add_accelerators(GearyApplication app) { app.add_window_accelerators(ACTION_CLOSE, { "Escape" } ); app.add_window_accelerators(ACTION_SEARCH_ACTIVATE, { "F" } ); } @@ -64,9 +67,15 @@ public class Dialogs.ProblemDetailsDialog : Hdy.Dialog { this.account = (account_report != null) ? account_report.account : null; this.service = (service_report != null) ? service_report.service : null; - GLib.SimpleActionGroup actions = new GLib.SimpleActionGroup(); - actions.add_action_entries(ProblemDetailsDialog.action_entries, this); - insert_action_group("win", actions); + // Edit actions + GLib.SimpleActionGroup edit_actions = new GLib.SimpleActionGroup(); + edit_actions.add_action_entries(EDIT_ACTIONS, this); + insert_action_group(Action.Edit.GROUP_NAME, edit_actions); + + // Window actions + GLib.SimpleActionGroup window_actions = new GLib.SimpleActionGroup(); + window_actions.add_action_entries(WINDOW_ACTIONS, this); + insert_action_group(Action.Window.GROUP_NAME, window_actions); this.error_pane = new Components.InspectorErrorView( error, account, service diff --git a/src/client/meson.build b/src/client/meson.build index 47a261f6..405e93af 100644 --- a/src/client/meson.build +++ b/src/client/meson.build @@ -25,6 +25,8 @@ geary_client_vala_sources = files( 'accounts/accounts-signature-web-view.vala', 'accounts/accounts-manager.vala', + 'client-action.vala', + 'components/client-web-view.vala', 'components/components-attachment-pane.vala', 'components/components-entry-undo.vala', diff --git a/src/client/plugin/desktop-notifications/desktop-notifications.vala b/src/client/plugin/desktop-notifications/desktop-notifications.vala index 506fa3c3..700a0db6 100644 --- a/src/client/plugin/desktop-notifications/desktop-notifications.vala +++ b/src/client/plugin/desktop-notifications/desktop-notifications.vala @@ -149,9 +149,9 @@ public class Plugin.DesktopNotifications : Notification { }; if (id == null) { - action = GearyApplication.ACTION_SHOW_FOLDER; + action = Action.Application.SHOW_FOLDER; } else { - action = GearyApplication.ACTION_SHOW_EMAIL; + action = Action.Application.SHOW_EMAIL; target_param += new GLib.Variant.variant(id.to_variant()); } @@ -159,7 +159,7 @@ public class Plugin.DesktopNotifications : Notification { ARRIVED_ID, summary, body, - "app." + action, + Action.Application.prefix(action), new GLib.Variant.tuple(target_param) ); } diff --git a/ui/accounts_editor_edit_pane.ui b/ui/accounts_editor_edit_pane.ui index 2ece4d54..3516e5aa 100644 --- a/ui/accounts_editor_edit_pane.ui +++ b/ui/accounts_editor_edit_pane.ui @@ -44,7 +44,7 @@ True True True - win.undo + edt.undo True diff --git a/ui/components-inspector.ui b/ui/components-inspector.ui index b910e61d..6cc06fe8 100644 --- a/ui/components-inspector.ui +++ b/ui/components-inspector.ui @@ -82,7 +82,7 @@ True Copy to clipboard - win.copy + edt.copy True diff --git a/ui/composer-menus.ui b/ui/composer-menus.ui index e6854a4e..1bd5b64b 100644 --- a/ui/composer-menus.ui +++ b/ui/composer-menus.ui @@ -5,41 +5,41 @@
S_ans Serif - win.font-family + edt.font-family sans S_erif - win.font-family + edt.font-family serif _Fixed Width - win.font-family + edt.font-family monospace
_Small - win.font-size + edt.font-size small _Medium - win.font-size + edt.font-size medium Lar_ge - win.font-size + edt.font-size large
C_olor - win.color + edt.color
@@ -76,49 +76,49 @@
_Undo - win.undo + edt.undo _Redo - win.redo + edt.redo
Cu_t - win.cut + edt.cut _Copy - win.copy + edt.copy _Paste - win.paste + edt.paste Paste _Without Formatting - win.paste-without-formatting + edt.paste-without-formatting
Cu_t - win.cut + edt.cut _Copy - win.copy + edt.copy _Paste - win.paste + edt.paste
Select _All - win.select-all + edt.select-all
diff --git a/ui/composer-widget.ui b/ui/composer-widget.ui index 95e1ef46..b65a91ff 100644 --- a/ui/composer-widget.ui +++ b/ui/composer-widget.ui @@ -351,7 +351,7 @@ False False Undo last edit (Ctrl+Z) - win.undo + edt.undo True @@ -375,7 +375,7 @@ False False Redo last edit (Ctrl+Shift+Z) - win.redo + edt.redo True @@ -413,7 +413,7 @@ False False Bold (Ctrl+B) - win.bold + edt.bold True @@ -437,7 +437,7 @@ False False Italic (Ctrl+I) - win.italic + edt.italic True @@ -461,7 +461,7 @@ False False Underline (Ctrl+U) - win.underline + edt.underline True @@ -485,7 +485,7 @@ False False Strikethrough (Ctrl+K) - win.strikethrough + edt.strikethrough True @@ -523,7 +523,7 @@ False False Insert unordered list - win.ulist + edt.ulist True @@ -547,7 +547,7 @@ False False Insert ordered list - win.olist + edt.olist True @@ -585,7 +585,7 @@ False False Quote text (Ctrl+]) - win.indent + edt.indent True @@ -609,7 +609,7 @@ False False Unquote text (Ctrl+[) - win.outdent + edt.outdent True @@ -647,7 +647,7 @@ False False Insert or update selection link (Ctrl+L) - win.insert-link + edt.insert-link True @@ -671,7 +671,7 @@ False False Insert an image (Ctrl+G) - win.insert-image + edt.insert-image True @@ -705,7 +705,7 @@ False False Remove selection formatting (Ctrl+Space) - win.remove-format + edt.remove-format True From 95fdaa70ca53c3ad77fcd4b3d9bf50d661c26ae3 Mon Sep 17 00:00:00 2001 From: Michael Gratton Date: Thu, 7 Nov 2019 12:48:45 +1100 Subject: [PATCH 5/7] Add undo support for MainWindow search and find entries --- src/client/components/search-bar.vala | 12 +++++++++++- .../conversation-viewer/conversation-viewer.vala | 6 ++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/client/components/search-bar.vala b/src/client/components/search-bar.vala index db1c7920..d79cc8f4 100644 --- a/src/client/components/search-bar.vala +++ b/src/client/components/search-bar.vala @@ -11,6 +11,7 @@ public class SearchBar : Gtk.SearchBar { public bool search_entry_has_focus { get { return search_entry.has_focus; } } private Gtk.SearchEntry search_entry = new Gtk.SearchEntry(); + private Components.EntryUndo search_undo; private Geary.ProgressMonitor? search_upgrade_progress_monitor = null; private MonitoredProgressBar search_upgrade_progress_bar = new MonitoredProgressBar(); private Geary.Account? current_account = null; @@ -29,6 +30,10 @@ public class SearchBar : Gtk.SearchBar { }); search_entry.has_focus = true; + this.search_undo = new Components.EntryUndo(this.search_entry); + + this.notify["search-mode-enabled"].connect(on_search_mode_changed); + // Search upgrade progress bar. search_upgrade_progress_bar.show_text = true; search_upgrade_progress_bar.visible = false; @@ -41,7 +46,7 @@ public class SearchBar : Gtk.SearchBar { } public void set_search_text(string text) { - search_entry.text = text; + this.search_entry.text = text; } public void give_search_focus() { @@ -110,4 +115,9 @@ public class SearchBar : Gtk.SearchBar { _("Search %s account").printf(current_account.information.display_name)); } + private void on_search_mode_changed() { + if (!this.search_mode_enabled) { + this.search_undo.reset(); + } + } } diff --git a/src/client/conversation-viewer/conversation-viewer.vala b/src/client/conversation-viewer/conversation-viewer.vala index d1386ab6..f7dcde19 100644 --- a/src/client/conversation-viewer/conversation-viewer.vala +++ b/src/client/conversation-viewer/conversation-viewer.vala @@ -52,6 +52,7 @@ public class ConversationViewer : Gtk.Stack, Geary.BaseInterface { [GtkChild] internal Gtk.SearchEntry conversation_find_entry; + private Components.EntryUndo conversation_find_undo; [GtkChild] private Gtk.Button conversation_find_next; @@ -126,6 +127,10 @@ public class ConversationViewer : Gtk.Stack, Geary.BaseInterface { ); this.empty_search_page.add(empty_search); + this.conversation_find_undo = new Components.EntryUndo( + this.conversation_find_entry + ); + // XXX GTK+ Bug 778190 workaround new_conversation_scroller(); @@ -431,6 +436,7 @@ public class ConversationViewer : Gtk.Stack, Geary.BaseInterface { this.current_list.conversation.base_folder as Geary.SearchFolder ); + this.conversation_find_undo.reset(); if (search_folder != null) { Geary.SearchQuery? search_query = search_folder.search_query; if (search_query != null) { From 8833b6d2424e2854991d814fa6f80d277713a65b Mon Sep 17 00:00:00 2001 From: Michael Gratton Date: Fri, 8 Nov 2019 10:04:25 +1100 Subject: [PATCH 6/7] Update MainWindow undo/redo actions before in-app notifications This prevents the in-app notifications button needing to have it state updated after the actions are updated. --- src/client/components/main-window.vala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/client/components/main-window.vala b/src/client/components/main-window.vala index bc000bbb..3779edee 100644 --- a/src/client/components/main-window.vala +++ b/src/client/components/main-window.vala @@ -1612,6 +1612,7 @@ public class MainWindow : Gtk.ApplicationWindow, Geary.BaseInterface { } private void on_command_undo(Application.Command command) { + update_command_actions(); Application.EmailCommand? email = command as Application.EmailCommand; if (email != null) { if (email.conversations.size > 1) { @@ -1630,17 +1631,16 @@ public class MainWindow : Gtk.ApplicationWindow, Geary.BaseInterface { ian.set_button(_("Redo"), Action.Edit.prefix(Action.Edit.REDO)); add_notification(ian); } - update_command_actions(); } private void on_command_redo(Application.Command command) { + update_command_actions(); if (command.executed_label != null) { Components.InAppNotification ian = new Components.InAppNotification(command.executed_label); ian.set_button(_("Undo"), Action.Edit.prefix(Action.Edit.UNDO)); add_notification(ian); } - update_command_actions(); } private void on_conversation_view_added(ConversationListBox list) { From 2e651d38d092ba1ec29f8cb05c974669e0157657 Mon Sep 17 00:00:00 2001 From: Michael Gratton Date: Thu, 7 Nov 2019 13:13:45 +1100 Subject: [PATCH 7/7] Update appdata with the new feature --- desktop/org.gnome.Geary.appdata.xml.in.in | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/desktop/org.gnome.Geary.appdata.xml.in.in b/desktop/org.gnome.Geary.appdata.xml.in.in index 2cbf24d5..2357ccf7 100644 --- a/desktop/org.gnome.Geary.appdata.xml.in.in +++ b/desktop/org.gnome.Geary.appdata.xml.in.in @@ -93,7 +93,8 @@

Enhancements included in this release:

    -
  • Improved and pervasive undo support for email actions
  • +
  • Unlimited undo for all email actions such as archiving, marking
  • +
  • Undo support for all text entry fields, including the composer
  • App-wide notification preferences now handled by desktop
  • Improved missing attachment detection in composer
  • Initial plugin system