Merge branch 'mjog/233-entry-undo' into 'mainline'

Application-wide GtkEntry undo support

Closes #233

See merge request GNOME/geary!360
This commit is contained in:
Michael Gratton 2019-11-12 21:42:10 +00:00
commit 63f626e050
24 changed files with 836 additions and 295 deletions

View file

@ -93,7 +93,8 @@
<description>
<p>Enhancements included in this release:</p>
<ul>
<li>Improved and pervasive undo support for email actions</li>
<li>Unlimited undo for all email actions such as archiving, marking</li>
<li>Undo support for all text entry fields, including the composer</li>
<li>App-wide notification preferences now handled by desktop</li>
<li>Improved missing attachment detection in composer</li>
<li>Initial plugin system</li>

View file

@ -28,8 +28,10 @@ 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
src/client/components/components-in-app-notification.vala
src/client/components/components-inspector.vala
src/client/components/components-placeholder-pane.vala

View file

@ -517,11 +517,19 @@ private abstract class Accounts.AddPaneRow<Value> :
private abstract class Accounts.EntryRow : AddPaneRow<Gtk.Entry> {
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);

View file

@ -264,6 +264,7 @@ internal class Accounts.EditorEditPane :
private class Accounts.DisplayNameRow : AccountRow<EditorEditPane,Gtk.Entry> {
private Components.EntryUndo value_undo;
private Application.CommandStack commands;
private GLib.Cancellable? cancellable;
@ -284,12 +285,19 @@ private class Accounts.DisplayNameRow : AccountRow<EditorEditPane,Gtk.Entry> {
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);

View file

@ -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);
}
}

View file

@ -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() {

View file

@ -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() {

View file

@ -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,21 +417,26 @@ public class GearyApplication : Gtk.Application {
Gtk.Window.set_default_icon_name(APP_ID);
// Application accels
add_app_accelerators(ACTION_COMPOSE, { "<Ctrl>N" });
add_app_accelerators(ACTION_HELP, { "F1" });
add_app_accelerators(ACTION_INSPECT, { "<Alt><Shift>I" });
add_app_accelerators(ACTION_QUIT, { "<Ctrl>Q" });
add_app_accelerators(Action.Application.COMPOSE, { "<Ctrl>N" });
add_app_accelerators(Action.Application.HELP, { "F1" });
add_app_accelerators(Action.Application.INSPECT, { "<Alt><Shift>I" });
add_app_accelerators(Action.Application.QUIT, { "<Ctrl>Q" });
// Common window accels
add_window_accelerators(ACTION_CLOSE, { "<Ctrl>W" });
add_window_accelerators(ACTION_COPY, { "<Ctrl>C" });
add_window_accelerators(ACTION_HELP_OVERLAY, { "<Ctrl>F1", "<Ctrl>question" });
add_window_accelerators(ACTION_REDO, { "<Ctrl><Shift>Z" });
add_window_accelerators(ACTION_UNDO, { "<Ctrl>Z" });
add_window_accelerators(Action.Window.CLOSE, { "<Ctrl>W" });
add_window_accelerators(
Action.Window.SHORTCUT_HELP, { "<Ctrl>F1", "<Ctrl>question" }
);
MainWindow.add_window_accelerators(this);
ComposerWidget.add_window_accelerators(this);
Components.Inspector.add_window_accelerators(this);
// Common edit accels
add_edit_accelerators(Action.Edit.COPY, { "<Ctrl>C" });
add_edit_accelerators(Action.Edit.REDO, { "<Ctrl><Shift>Z" });
add_edit_accelerators(Action.Edit.UNDO, { "<Ctrl>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
@ -494,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;
@ -875,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);

View file

@ -0,0 +1,73 @@
/*
* Copyright 2019 Michael Gratton <mike@vee.net>
*
* 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;
}
}
}

View file

@ -0,0 +1,333 @@
/*
* Copyright 2019 Michael Gratton <mike@vee.net>
*
* 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 ActionEntry[] EDIT_ACTIONS = {
{ Action.Edit.UNDO, on_undo },
{ Action.Edit.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()
);
}
}
/** 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 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.Edit.GROUP_NAME, this.edit_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);
}
~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.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) {
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();
}
}

View file

@ -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, { "<Ctrl>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(

View file

@ -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 <Shift>
@ -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);
}
}
@ -893,6 +904,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
@ -969,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
);
}
@ -1379,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
);
@ -1389,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)
);
@ -1454,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)))
);
@ -1489,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();
@ -1595,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) {
@ -1610,20 +1628,19 @@ 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();
}
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"), "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();
}
private void on_conversation_view_added(ConversationListBox list) {
@ -1781,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);
}

View file

@ -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();
}
}
}

View file

@ -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, { "<Ctrl>x" } );
application.add_window_accelerators(ACTION_PASTE, { "<Ctrl>v" } );
application.add_window_accelerators(ACTION_PASTE_WITHOUT_FORMATTING, { "<Ctrl><Shift>v" } );
application.add_window_accelerators(ACTION_INSERT_IMAGE, { "<Ctrl>g" } );
application.add_window_accelerators(ACTION_INSERT_LINK, { "<Ctrl>l" } );
application.add_window_accelerators(ACTION_INDENT, { "<Ctrl>bracketright" } );
application.add_window_accelerators(ACTION_OUTDENT, { "<Ctrl>bracketleft" } );
application.add_window_accelerators(ACTION_REMOVE_FORMAT, { "<Ctrl>space" } );
application.add_window_accelerators(ACTION_BOLD, { "<Ctrl>b" } );
application.add_window_accelerators(ACTION_ITALIC, { "<Ctrl>i" } );
application.add_window_accelerators(ACTION_UNDERLINE, { "<Ctrl>u" } );
application.add_window_accelerators(ACTION_STRIKETHROUGH, { "<Ctrl>k" } );
application.add_window_accelerators(ACTION_ADD_ATTACHMENT, { "<Ctrl>t" } );
application.add_window_accelerators(ACTION_DETACH, { "<Ctrl>d" } );
application.add_edit_accelerators(ACTION_CUT, { "<Ctrl>x" } );
application.add_edit_accelerators(ACTION_PASTE, { "<Ctrl>v" } );
application.add_edit_accelerators(ACTION_PASTE_WITHOUT_FORMATTING, { "<Ctrl><Shift>v" } );
application.add_edit_accelerators(ACTION_INSERT_IMAGE, { "<Ctrl>g" } );
application.add_edit_accelerators(ACTION_INSERT_LINK, { "<Ctrl>l" } );
application.add_edit_accelerators(ACTION_INDENT, { "<Ctrl>bracketright" } );
application.add_edit_accelerators(ACTION_OUTDENT, { "<Ctrl>bracketleft" } );
application.add_edit_accelerators(ACTION_REMOVE_FORMAT, { "<Ctrl>space" } );
application.add_edit_accelerators(ACTION_BOLD, { "<Ctrl>b" } );
application.add_edit_accelerators(ACTION_ITALIC, { "<Ctrl>i" } );
application.add_edit_accelerators(ACTION_UNDERLINE, { "<Ctrl>u" } );
application.add_edit_accelerators(ACTION_STRIKETHROUGH, { "<Ctrl>k" } );
}
private const string DRAFT_SAVED_TEXT = _("Saved");
@ -268,30 +269,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]
@ -325,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;
@ -386,9 +400,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 +465,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
);
@ -858,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
@ -888,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();
}
@ -897,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)
@ -1896,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();
@ -2345,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() {

View file

@ -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);
}
}
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<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);
}
}
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;
}

View file

@ -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;
}
}

View file

@ -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) {

View file

@ -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, { "<Ctrl>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

View file

@ -25,8 +25,11 @@ 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',
'components/components-inspector.vala',
'components/components-in-app-notification.vala',
'components/components-inspector-error-view.vala',

View file

@ -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)
);
}

View file

@ -44,7 +44,7 @@
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="action_name">win.undo</property>
<property name="action_name">edt.undo</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>

View file

@ -82,7 +82,7 @@
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes"
comments="Tooltip for inspector button">Copy to clipboard</property>
<property name="action_name">win.copy</property>
<property name="action_name">edt.copy</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>

View file

@ -5,41 +5,41 @@
<section>
<item>
<attribute name="label" translatable="yes">S_ans Serif</attribute>
<attribute name="action">win.font-family</attribute>
<attribute name="action">edt.font-family</attribute>
<attribute name="target">sans</attribute>
</item>
<item>
<attribute name="label" translatable="yes">S_erif</attribute>
<attribute name="action">win.font-family</attribute>
<attribute name="action">edt.font-family</attribute>
<attribute name="target">serif</attribute>
</item>
<item>
<attribute name="label" translatable="yes">_Fixed Width</attribute>
<attribute name="action">win.font-family</attribute>
<attribute name="action">edt.font-family</attribute>
<attribute name="target">monospace</attribute>
</item>
</section>
<section>
<item>
<attribute name="label" translatable="yes">_Small</attribute>
<attribute name="action">win.font-size</attribute>
<attribute name="action">edt.font-size</attribute>
<attribute name="target">small</attribute>
</item>
<item>
<attribute name="label" translatable="yes">_Medium</attribute>
<attribute name="action">win.font-size</attribute>
<attribute name="action">edt.font-size</attribute>
<attribute name="target">medium</attribute>
</item>
<item>
<attribute name="label" translatable="yes">Lar_ge</attribute>
<attribute name="action">win.font-size</attribute>
<attribute name="action">edt.font-size</attribute>
<attribute name="target">large</attribute>
</item>
</section>
<section>
<item>
<attribute name="label" translatable="yes">C_olor</attribute>
<attribute name="action">win.color</attribute>
<attribute name="action">edt.color</attribute>
</item>
</section>
<section>
@ -76,49 +76,49 @@
<section>
<item>
<attribute name="label" translatable="yes">_Undo</attribute>
<attribute name="action">win.undo</attribute>
<attribute name="action">edt.undo</attribute>
</item>
<item>
<attribute name="label" translatable="yes">_Redo</attribute>
<attribute name="action">win.redo</attribute>
<attribute name="action">edt.redo</attribute>
</item>
</section>
<section id="context_menu_rich_text">
<item>
<attribute name="label" translatable="yes">Cu_t</attribute>
<attribute name="action">win.cut</attribute>
<attribute name="action">edt.cut</attribute>
</item>
<item>
<attribute name="label" translatable="yes">_Copy</attribute>
<attribute name="action">win.copy</attribute>
<attribute name="action">edt.copy</attribute>
</item>
<item>
<attribute name="label" translatable="yes">_Paste</attribute>
<attribute name="action">win.paste</attribute>
<attribute name="action">edt.paste</attribute>
</item>
<item>
<attribute name="label" translatable="yes" context="Clipboard paste as plain text">Paste _Without Formatting</attribute>
<attribute name="action">win.paste-without-formatting</attribute>
<attribute name="action">edt.paste-without-formatting</attribute>
</item>
</section>
<section id="context_menu_plain_text">
<item>
<attribute name="label" translatable="yes">Cu_t</attribute>
<attribute name="action">win.cut</attribute>
<attribute name="action">edt.cut</attribute>
</item>
<item>
<attribute name="label" translatable="yes">_Copy</attribute>
<attribute name="action">win.copy</attribute>
<attribute name="action">edt.copy</attribute>
</item>
<item>
<attribute name="label" translatable="yes">_Paste</attribute>
<attribute name="action">win.paste</attribute>
<attribute name="action">edt.paste</attribute>
</item>
</section>
<section>
<item>
<attribute name="label" translatable="yes">Select _All</attribute>
<attribute name="action">win.select-all</attribute>
<attribute name="action">edt.select-all</attribute>
</item>
</section>
<section id="context_menu_webkit_text_entry"/>

View file

@ -351,7 +351,7 @@
<property name="focus_on_click">False</property>
<property name="receives_default">False</property>
<property name="tooltip_text" translatable="yes">Undo last edit (Ctrl+Z)</property>
<property name="action_name">win.undo</property>
<property name="action_name">edt.undo</property>
<property name="always_show_image">True</property>
<child>
<object class="GtkImage">
@ -375,7 +375,7 @@
<property name="focus_on_click">False</property>
<property name="receives_default">False</property>
<property name="tooltip_text" translatable="yes">Redo last edit (Ctrl+Shift+Z)</property>
<property name="action_name">win.redo</property>
<property name="action_name">edt.redo</property>
<property name="always_show_image">True</property>
<child>
<object class="GtkImage">
@ -413,7 +413,7 @@
<property name="focus_on_click">False</property>
<property name="receives_default">False</property>
<property name="tooltip_text" translatable="yes">Bold (Ctrl+B)</property>
<property name="action_name">win.bold</property>
<property name="action_name">edt.bold</property>
<property name="always_show_image">True</property>
<child>
<object class="GtkImage" id="bold_image">
@ -437,7 +437,7 @@
<property name="focus_on_click">False</property>
<property name="receives_default">False</property>
<property name="tooltip_text" translatable="yes">Italic (Ctrl+I)</property>
<property name="action_name">win.italic</property>
<property name="action_name">edt.italic</property>
<property name="always_show_image">True</property>
<child>
<object class="GtkImage" id="italics_image">
@ -461,7 +461,7 @@
<property name="focus_on_click">False</property>
<property name="receives_default">False</property>
<property name="tooltip_text" translatable="yes">Underline (Ctrl+U)</property>
<property name="action_name">win.underline</property>
<property name="action_name">edt.underline</property>
<property name="always_show_image">True</property>
<child>
<object class="GtkImage" id="underline_image">
@ -485,7 +485,7 @@
<property name="focus_on_click">False</property>
<property name="receives_default">False</property>
<property name="tooltip_text" translatable="yes">Strikethrough (Ctrl+K)</property>
<property name="action_name">win.strikethrough</property>
<property name="action_name">edt.strikethrough</property>
<property name="always_show_image">True</property>
<child>
<object class="GtkImage" id="strikethrough_image">
@ -523,7 +523,7 @@
<property name="focus_on_click">False</property>
<property name="receives_default">False</property>
<property name="tooltip_text" translatable="yes">Insert unordered list</property>
<property name="action_name">win.ulist</property>
<property name="action_name">edt.ulist</property>
<property name="always_show_image">True</property>
<child>
<object class="GtkImage" id="ulist_image">
@ -547,7 +547,7 @@
<property name="focus_on_click">False</property>
<property name="receives_default">False</property>
<property name="tooltip_text" translatable="yes">Insert ordered list</property>
<property name="action_name">win.olist</property>
<property name="action_name">edt.olist</property>
<property name="always_show_image">True</property>
<child>
<object class="GtkImage" id="olist_image">
@ -585,7 +585,7 @@
<property name="focus_on_click">False</property>
<property name="receives_default">False</property>
<property name="tooltip_text" translatable="yes">Quote text (Ctrl+])</property>
<property name="action_name">win.indent</property>
<property name="action_name">edt.indent</property>
<property name="always_show_image">True</property>
<child>
<object class="GtkImage" id="indent_image">
@ -609,7 +609,7 @@
<property name="focus_on_click">False</property>
<property name="receives_default">False</property>
<property name="tooltip_text" translatable="yes">Unquote text (Ctrl+[)</property>
<property name="action_name">win.outdent</property>
<property name="action_name">edt.outdent</property>
<property name="always_show_image">True</property>
<child>
<object class="GtkImage" id="outdent_image">
@ -647,7 +647,7 @@
<property name="focus_on_click">False</property>
<property name="receives_default">False</property>
<property name="tooltip_text" translatable="yes">Insert or update selection link (Ctrl+L)</property>
<property name="action_name">win.insert-link</property>
<property name="action_name">edt.insert-link</property>
<property name="always_show_image">True</property>
<child>
<object class="GtkImage" id="insert_link_image">
@ -671,7 +671,7 @@
<property name="focus_on_click">False</property>
<property name="receives_default">False</property>
<property name="tooltip_text" translatable="yes">Insert an image (Ctrl+G)</property>
<property name="action_name">win.insert-image</property>
<property name="action_name">edt.insert-image</property>
<property name="always_show_image">True</property>
<child>
<object class="GtkImage">
@ -705,7 +705,7 @@
<property name="focus_on_click">False</property>
<property name="receives_default">False</property>
<property name="tooltip_text" translatable="yes">Remove selection formatting (Ctrl+Space)</property>
<property name="action_name">win.remove-format</property>
<property name="action_name">edt.remove-format</property>
<property name="always_show_image">True</property>
<child>
<object class="GtkImage" id="remove_format_image">