Commit e490905b authored by Michael Gratton's avatar Michael Gratton 🤞

Merge branch 'wip/composer-shortcuts' into 'master'

Refine composer shortcuts

See merge request !106
parents d1883975 1c36b578
Pipeline #58854 passed with stages
in 17 minutes
......@@ -235,10 +235,11 @@ public class GearyApplication : Gtk.Application {
// the other instances called when sending commands to the app via the command-line)
message("%s %s prefix=%s exec_dir=%s is_installed=%s", NAME, VERSION, INSTALL_PREFIX,
exec_dir.get_path(), is_installed().to_string());
config = new Configuration(APP_ID);
ComposerWidget.add_window_accelerators(this);
yield controller.open_async(null);
release();
}
......@@ -253,6 +254,12 @@ public class GearyApplication : Gtk.Application {
is_destroyed = true;
}
public void add_window_accelerators(string action,
string[] accelerators,
Variant? param = null) {
set_accels_for_action("win." + action, accelerators);
}
public void show_accounts() {
activate();
......
......@@ -37,17 +37,10 @@ public class ComposerBox : Gtk.Frame, ComposerContainer {
add(this.composer);
this.main_toolbar.set_conversation_header(composer.header);
this.composer.editor.focus_in_event.connect(on_focus_in);
this.composer.editor.focus_out_event.connect(on_focus_out);
show();
}
public void remove_composer() {
if (this.composer.editor.has_focus)
on_focus_out();
this.composer.editor.focus_in_event.disconnect(on_focus_in);
this.composer.editor.focus_out_event.disconnect(on_focus_out);
remove(this.composer);
close_container();
}
......@@ -56,8 +49,6 @@ public class ComposerBox : Gtk.Frame, ComposerContainer {
hide();
this.main_toolbar.remove_conversation_header(composer.header);
this.composer.state = ComposerWidget.ComposerState.DETACHED;
this.composer.editor.focus_in_event.disconnect(on_focus_in);
this.composer.editor.focus_out_event.disconnect(on_focus_out);
vanished();
}
......@@ -66,5 +57,5 @@ public class ComposerBox : Gtk.Frame, ComposerContainer {
vanish();
destroy();
}
}
}
......@@ -44,81 +44,4 @@ public interface ComposerContainer {
*/
public abstract void remove_composer();
protected virtual bool on_focus_in() {
if (this.old_accelerators == null) {
this.old_accelerators = new Gee.HashMultiMap<string, string>();
add_accelerators();
}
return false;
}
protected virtual bool on_focus_out() {
if (this.old_accelerators != null) {
remove_accelerators();
this.old_accelerators = null;
}
return false;
}
/**
* Adds the accelerators for the child composer, and temporarily removes conflicting
* accelerators from existing actions.
*/
protected virtual void add_accelerators() {
GearyApplication app = GearyApplication.instance;
// Check for actions with conflicting accelerators
foreach (string action in ComposerWidget.action_accelerators.get_keys()) {
foreach (string accelerator in ComposerWidget.action_accelerators[action]) {
string[] actions = app.get_actions_for_accel(accelerator);
foreach (string conflicting_action in actions) {
remove_conflicting_accelerator(conflicting_action, accelerator);
this.old_accelerators[conflicting_action] = accelerator;
}
}
}
// Now add our actions to the window and their accelerators
foreach (string action in ComposerWidget.action_accelerators.get_keys()) {
this.top_window.add_action(composer.get_action(action));
app.set_accels_for_action("win." + action,
ComposerWidget.action_accelerators[action].to_array());
}
}
/**
* Removes the accelerators for the child composer, and restores previously removed accelerators.
*/
protected virtual void remove_accelerators() {
foreach (string action in ComposerWidget.action_accelerators.get_keys())
GearyApplication.instance.set_accels_for_action("win." + action, {});
foreach (string action in old_accelerators.get_keys())
foreach (string accelerator in this.old_accelerators[action])
restore_conflicting_accelerator(action, accelerator);
}
// Helper method. Removes the given conflicting accelerator from the action's accelerators.
private void remove_conflicting_accelerator(string action, string accelerator) {
GearyApplication app = GearyApplication.instance;
string[] accelerators = app.get_accels_for_action(action);
if (accelerators.length == 0)
return;
string[] without_accel = new string[accelerators.length - 1];
foreach (string a in accelerators)
if (a != accelerator)
without_accel += a;
app.set_accels_for_action(action, without_accel);
}
// Helper method. Adds the given accelerator back to the action's accelerators.
private void restore_conflicting_accelerator(string action, string accelerator) {
GearyApplication app = GearyApplication.instance;
string[] accelerators = app.get_accels_for_action(action);
accelerators += accelerator;
app.set_accels_for_action(action, accelerators);
}
}
......@@ -42,8 +42,6 @@ public class ComposerEmbed : Gtk.EventBox, ComposerContainer {
add(composer);
realize.connect(on_realize);
this.composer.editor.focus_in_event.connect(on_focus_in);
this.composer.editor.focus_out_event.connect(on_focus_out);
show();
}
......@@ -71,14 +69,7 @@ public class ComposerEmbed : Gtk.EventBox, ComposerContainer {
}
public void remove_composer() {
if (this.composer.editor.has_focus)
on_focus_out();
this.composer.editor.focus_in_event.disconnect(on_focus_in);
this.composer.editor.focus_out_event.disconnect(on_focus_out);
disable_scroll_reroute(this);
remove(this.composer);
close_container();
}
......@@ -189,8 +180,6 @@ public class ComposerEmbed : Gtk.EventBox, ComposerContainer {
public void vanish() {
hide();
this.composer.state = ComposerWidget.ComposerState.DETACHED;
this.composer.editor.focus_in_event.disconnect(on_focus_in);
this.composer.editor.focus_out_event.disconnect(on_focus_out);
vanished();
}
......
......@@ -125,7 +125,7 @@ public class ComposerWebView : ClientWebView {
// XXX this is a bit of a hack given the docs for is_empty,
// above
this.command_stack_changed.connect((can_undo, can_redo) => {
this.is_empty = !can_redo;
this.is_empty = !can_undo;
});
}
......
......@@ -47,8 +47,6 @@ public class ComposerWidget : Gtk.EventBox, Geary.BaseInterface {
}
}
private SimpleActionGroup actions = new SimpleActionGroup();
private const string ACTION_UNDO = "undo";
private const string ACTION_REDO = "redo";
private const string ACTION_CUT = "cut";
......@@ -90,11 +88,10 @@ public class ComposerWidget : Gtk.EventBox, Geary.BaseInterface {
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
ACTION_OLIST, ACTION_ULIST
};
private const ActionEntry[] action_entries = {
// Editor commands
private const ActionEntry[] editor_action_entries = {
{ACTION_UNDO, on_undo },
{ACTION_REDO, on_redo },
{ACTION_CUT, on_cut },
......@@ -118,41 +115,43 @@ public class ComposerWidget : Gtk.EventBox, Geary.BaseInterface {
{ACTION_COLOR, on_select_color },
{ACTION_INSERT_IMAGE, on_insert_image },
{ACTION_INSERT_LINK, on_insert_link },
// Composer commands
{ACTION_COMPOSE_AS_HTML, on_toggle_action, null, "true", on_compose_as_html_toggled },
{ACTION_SHOW_EXTENDED, on_toggle_action, null, "false", on_show_extended_toggled },
{ACTION_OPEN_INSPECTOR, on_open_inspector },
};
private const ActionEntry[] composer_action_entries = {
{ACTION_ADD_ATTACHMENT, on_add_attachment },
{ACTION_ADD_ORIGINAL_ATTACHMENTS, on_pending_attachments },
{ACTION_CLOSE, on_close },
{ACTION_CLOSE_AND_SAVE, on_close_and_save },
{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_SEND, on_send },
{ACTION_ADD_ATTACHMENT, on_add_attachment },
{ACTION_ADD_ORIGINAL_ATTACHMENTS, on_pending_attachments },
{ACTION_SELECT_DICTIONARY, on_select_dictionary },
{ACTION_OPEN_INSPECTOR, on_open_inspector },
{ACTION_SEND, on_send },
{ACTION_SHOW_EXTENDED, on_toggle_action, null, "false", on_show_extended_toggled },
};
public static Gee.MultiMap<string, string> action_accelerators = new Gee.HashMultiMap<string, string>();
static construct {
action_accelerators.set(ACTION_UNDO, "<Ctrl>z");
action_accelerators.set(ACTION_REDO, "<Ctrl><Shift>z");
action_accelerators.set(ACTION_CUT, "<Ctrl>x");
action_accelerators.set(ACTION_COPY, "<Ctrl>c");
action_accelerators.set(ACTION_PASTE, "<Ctrl>v");
action_accelerators.set(ACTION_PASTE_WITHOUT_FORMATTING, "<Ctrl><Shift>v");
action_accelerators.set(ACTION_INSERT_IMAGE, "<Ctrl>g");
action_accelerators.set(ACTION_INSERT_LINK, "<Ctrl>l");
action_accelerators.set(ACTION_INDENT, "<Ctrl>bracketright");
action_accelerators.set(ACTION_OUTDENT, "<Ctrl>bracketleft");
action_accelerators.set(ACTION_REMOVE_FORMAT, "<Ctrl>space");
action_accelerators.set(ACTION_BOLD, "<Ctrl>b");
action_accelerators.set(ACTION_ITALIC, "<Ctrl>i");
action_accelerators.set(ACTION_UNDERLINE, "<Ctrl>u");
action_accelerators.set(ACTION_STRIKETHROUGH, "<Ctrl>k");
action_accelerators.set(ACTION_CLOSE, "<Ctrl>w");
action_accelerators.set(ACTION_CLOSE, "Escape");
action_accelerators.set(ACTION_ADD_ATTACHMENT, "<Ctrl>t");
action_accelerators.set(ACTION_DETACH, "<Ctrl>d");
public static void add_window_accelerators(GearyApplication application) {
application.add_window_accelerators(ACTION_UNDO, { "<Ctrl>z" } );
application.add_window_accelerators(ACTION_REDO, { "<Ctrl><Shift>z" } );
application.add_window_accelerators(ACTION_CUT, { "<Ctrl>x" } );
application.add_window_accelerators(ACTION_COPY, { "<Ctrl>c" } );
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_CLOSE, { "Escape" } );
application.add_window_accelerators(ACTION_ADD_ATTACHMENT, { "<Ctrl>t" } );
application.add_window_accelerators(ACTION_DETACH, { "<Ctrl>d" } );
}
private const string DRAFT_SAVED_TEXT = _("Saved");
......@@ -252,6 +251,9 @@ public class ComposerWidget : Gtk.EventBox, Geary.BaseInterface {
[GtkChild]
internal Gtk.Grid editor_container;
[GtkChild]
internal Gtk.Grid body_container;
[GtkChild]
private Gtk.Label from_label;
[GtkChild]
......@@ -322,6 +324,9 @@ public class ComposerWidget : Gtk.EventBox, Geary.BaseInterface {
[GtkChild]
private Gtk.Box message_area;
private SimpleActionGroup composer_actions = new SimpleActionGroup();
private SimpleActionGroup editor_actions = new SimpleActionGroup();
private Menu html_menu;
private Menu plain_menu;
......@@ -457,7 +462,7 @@ public class ComposerWidget : Gtk.EventBox, Geary.BaseInterface {
this.editor.set_vexpand(true);
this.editor.show();
this.editor_container.add(this.editor);
this.body_container.add(this.editor);
// Initialize menus
Gtk.Builder builder = new Gtk.Builder.from_resource(
......@@ -807,16 +812,31 @@ public class ComposerWidget : Gtk.EventBox, Geary.BaseInterface {
// Initializes all actions and adds them to the action group
private void initialize_actions() {
this.actions.add_action_entries(action_entries, this);
// for some reason, we can't use the same prefix.
insert_action_group("cmp", this.actions);
this.header.insert_action_group("cmh", this.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.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
);
this.editor_actions.add_action_entries(
ComposerWidget.editor_action_entries, this
);
this.editor_container.insert_action_group("win", this.editor_actions);
this.actions.change_action_state(
this.composer_actions.change_action_state(
ACTION_SHOW_EXTENDED, false
);
this.actions.change_action_state(
this.composer_actions.change_action_state(
ACTION_COMPOSE_AS_HTML, this.config.compose_as_html
);
......@@ -1181,7 +1201,7 @@ public class ComposerWidget : Gtk.EventBox, Geary.BaseInterface {
// conversation back in the main window. The workaround here
// sets a new menu model and hence the menu_button constructs
// a new popover.
this.actions.change_action_state(ACTION_COMPOSE_AS_HTML,
this.composer_actions.change_action_state(ACTION_COMPOSE_AS_HTML,
GearyApplication.instance.config.compose_as_html);
this.state = ComposerWidget.ComposerState.DETACHED;
......@@ -1670,7 +1690,7 @@ public class ComposerWidget : Gtk.EventBox, Geary.BaseInterface {
// the Enter leaking through to the controls, but only
// send if send is available
if ((event.state & Gdk.ModifierType.CONTROL_MASK) != 0) {
this.actions.activate_action(ACTION_SEND, null);
this.composer_actions.activate_action(ACTION_SEND, null);
ret = Gdk.EVENT_STOP;
}
break;
......@@ -2017,8 +2037,12 @@ public class ComposerWidget : Gtk.EventBox, Geary.BaseInterface {
* Helper method, returns a composer action.
* @param action_name - The name of the action (as found in action_entries)
*/
public SimpleAction? get_action(string action_name) {
return this.actions.lookup_action(action_name) as SimpleAction;
public GLib.SimpleAction? get_action(string action_name) {
GLib.Action? action = this.composer_actions.lookup_action(action_name);
if (action == null) {
action = this.editor_actions.lookup_action(action_name);
}
return action as SimpleAction;
}
private bool add_account_emails_to_from_list(Geary.Account other_account, bool set_active = false) {
......@@ -2238,7 +2262,8 @@ public class ComposerWidget : Gtk.EventBox, Geary.BaseInterface {
// without the popover immediately appearing and raining on
// their text selection parade.
if (this.pointer_url != null &&
this.actions.get_action_state(ACTION_COMPOSE_AS_HTML).get_boolean()) {
this.composer_actions.get_action_state(ACTION_COMPOSE_AS_HTML)
.get_boolean()) {
Gdk.EventButton? button = (Gdk.EventButton) event;
Gdk.Rectangle location = Gdk.Rectangle();
location.x = (int) button.x;
......@@ -2260,31 +2285,33 @@ public class ComposerWidget : Gtk.EventBox, Geary.BaseInterface {
this.cursor_url = context.is_link ? context.link_url : null;
update_cursor_actions();
this.actions.change_action_state(ACTION_FONT_FAMILY, context.font_family);
this.editor_actions.change_action_state(
ACTION_FONT_FAMILY, context.font_family
);
if (context.font_size < 11)
this.actions.change_action_state(ACTION_FONT_SIZE, "small");
this.editor_actions.change_action_state(ACTION_FONT_SIZE, "small");
else if (context.font_size > 20)
this.actions.change_action_state(ACTION_FONT_SIZE, "large");
this.editor_actions.change_action_state(ACTION_FONT_SIZE, "large");
else
this.actions.change_action_state(ACTION_FONT_SIZE, "medium");
this.editor_actions.change_action_state(ACTION_FONT_SIZE, "medium");
}
private void on_typing_attributes_changed() {
uint mask = this.editor.get_editor_state().get_typing_attributes();
this.actions.change_action_state(
this.editor_actions.change_action_state(
ACTION_BOLD,
(mask & WebKit.EditorTypingAttributes.BOLD) == WebKit.EditorTypingAttributes.BOLD
);
this.actions.change_action_state(
this.editor_actions.change_action_state(
ACTION_ITALIC,
(mask & WebKit.EditorTypingAttributes.ITALIC) == WebKit.EditorTypingAttributes.ITALIC
);
this.actions.change_action_state(
this.editor_actions.change_action_state(
ACTION_UNDERLINE,
(mask & WebKit.EditorTypingAttributes.UNDERLINE) == WebKit.EditorTypingAttributes.UNDERLINE
);
this.actions.change_action_state(
this.editor_actions.change_action_state(
ACTION_STRIKETHROUGH,
(mask & WebKit.EditorTypingAttributes.STRIKETHROUGH) == WebKit.EditorTypingAttributes.STRIKETHROUGH
);
......
......@@ -36,8 +36,6 @@ public class ComposerWindow : Gtk.ApplicationWindow, ComposerContainer {
set_property("name", "GearyComposerWindow");
add(this.composer);
focus_in_event.connect(on_focus_in);
focus_out_event.connect(on_focus_out);
if (composer.config.desktop_environment == Configuration.DesktopEnvironment.UNITY) {
composer.embed_header();
......@@ -105,10 +103,6 @@ public class ComposerWindow : Gtk.ApplicationWindow, ComposerContainer {
}
public void close_container() {
on_focus_out();
this.composer.editor.focus_in_event.disconnect(on_focus_in);
this.composer.editor.focus_out_event.disconnect(on_focus_out);
this.closing = true;
destroy();
}
......
......@@ -5,53 +5,53 @@
<section>
<item>
<attribute name="label" translatable="yes">S_ans Serif</attribute>
<attribute name="action">cmp.font-family</attribute>
<attribute name="action">win.font-family</attribute>
<attribute name="target">sans</attribute>
</item>
<item>
<attribute name="label" translatable="yes">S_erif</attribute>
<attribute name="action">cmp.font-family</attribute>
<attribute name="action">win.font-family</attribute>
<attribute name="target">serif</attribute>
</item>
<item>
<attribute name="label" translatable="yes">_Fixed Width</attribute>
<attribute name="action">cmp.font-family</attribute>
<attribute name="action">win.font-family</attribute>
<attribute name="target">monospace</attribute>
</item>
</section>
<section>
<item>
<attribute name="label" translatable="yes">_Small</attribute>
<attribute name="action">cmp.font-size</attribute>
<attribute name="action">win.font-size</attribute>
<attribute name="target">small</attribute>
</item>
<item>
<attribute name="label" translatable="yes">_Medium</attribute>
<attribute name="action">cmp.font-size</attribute>
<attribute name="action">win.font-size</attribute>
<attribute name="target">medium</attribute>
</item>
<item>
<attribute name="label" translatable="yes">Lar_ge</attribute>
<attribute name="action">cmp.font-size</attribute>
<attribute name="action">win.font-size</attribute>
<attribute name="target">large</attribute>
</item>
</section>
<section>
<item>
<attribute name="label" translatable="yes">C_olor</attribute>
<attribute name="action">cmp.color</attribute>
<attribute name="action">win.color</attribute>
</item>
</section>
<section>
<item>
<attribute name="label" translatable="yes">_Rich Text</attribute>
<attribute name="action">cmp.compose-as-html</attribute>
<attribute name="action">win.compose-as-html</attribute>
</item>
</section>
<section>
<item>
<attribute name="label" translatable="yes">Show Extended Fields</attribute>
<attribute name="action">cmp.show-extended</attribute>
<attribute name="action">win.show-extended</attribute>
</item>
</section>
</menu>
......@@ -60,13 +60,13 @@
<section>
<item>
<attribute name="label" translatable="yes">_Rich Text</attribute>
<attribute name="action">cmp.compose-as-html</attribute>
<attribute name="action">win.compose-as-html</attribute>
</item>
</section>
<section>
<item>
<attribute name="label" translatable="yes">Show Extended Fields</attribute>
<attribute name="action">cmp.show-extended</attribute>
<attribute name="action">win.show-extended</attribute>
</item>
</section>
</menu>
......@@ -76,56 +76,56 @@
<section>
<item>
<attribute name="label" translatable="yes">_Undo</attribute>
<attribute name="action">cmp.undo</attribute>
<attribute name="action">win.undo</attribute>
</item>
<item>
<attribute name="label" translatable="yes">_Redo</attribute>
<attribute name="action">cmp.redo</attribute>
<attribute name="action">win.redo</attribute>
</item>
</section>
<section id="context_menu_rich_text">
<item>
<attribute name="label" translatable="yes">Cu_t</attribute>
<attribute name="action">cmp.cut</attribute>
<attribute name="action">win.cut</attribute>
</item>
<item>
<attribute name="label" translatable="yes">_Copy</attribute>
<attribute name="action">cmp.copy</attribute>
<attribute name="action">win.copy</attribute>
</item>
<item>
<attribute name="label" translatable="yes">_Paste</attribute>
<attribute name="action">cmp.paste</attribute>
<attribute name="action">win.paste</attribute>
</item>
<item>
<attribute name="label" translatable="yes" context="Clipboard paste as plain text">Paste _Without Formatting</attribute>
<attribute name="action">cmp.paste-without-formatting</attribute>
<attribute name="action">win.paste-without-formatting</attribute>
</item>
</section>
<section id="context_menu_plain_text">
<item>
<attribute name="label" translatable="yes">Cu_t</attribute>
<attribute name="action">cmp.cut</attribute>
<attribute name="action">win.cut</attribute>
</item>
<item>
<attribute name="label" translatable="yes">_Copy</attribute>
<attribute name="action">cmp.copy</attribute>
<attribute name="action">win.copy</attribute>
</item>
<item>
<attribute name="label" translatable="yes">_Paste</attribute>
<attribute name="action">cmp.paste</attribute>
<attribute name="action">win.paste</attribute>
</item>
</section>
<section>
<item>
<attribute name="label" translatable="yes">Select _All</attribute>
<attribute name="action">cmp.select-all</attribute>
<attribute name="action">win.select-all</attribute>
</item>
</section>
<section id="context_menu_webkit_text_entry"/>
<section id="context_menu_inspector">
<item>
<attribute name="label" translatable="yes">_Inspect…</attribute>
<attribute name="action">cmp.open_inspector</attribute>
<attribute name="action">win.open_inspector</attribute>
</item>
</section>
</menu>
......
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment