diff --git a/desktop/org.gnome.Geary.gschema.xml b/desktop/org.gnome.Geary.gschema.xml index bbbadc36..89354dc2 100644 --- a/desktop/org.gnome.Geary.gschema.xml +++ b/desktop/org.gnome.Geary.gschema.xml @@ -21,42 +21,12 @@ The last recorded height of the application window. - - 100 - Position of folder list pane - Position of the folder list Paned grabber. - - - - -1 - Position of folder list pane when horizontal - Position of the folder list Paned grabber in the horizontal orientation. - - - - 200 - Position of folder list pane when vertical - Position of the folder list Paned grabber in the vertical orientation. - - - - true - Orientation of the folder list pane - True if the folder list Paned is in the horizontal orientation. - - false Show/hide formatting toolbar True if the formatting toolbar in the composer is shown. - - 250 - Position of message list pane - Position of the message list Paned grabber. - - true Autoselect next message diff --git a/po/POTFILES.in b/po/POTFILES.in index 09c3c970..68e3ca34 100644 --- a/po/POTFILES.in +++ b/po/POTFILES.in @@ -39,6 +39,8 @@ 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-conversation-actions.vala +src/client/components/components-conversation-action-bar.vala src/client/components/components-entry-undo.vala src/client/components/components-in-app-notification.vala src/client/components/components-info-bar-stack.vala @@ -455,6 +457,8 @@ ui/composer-widget.ui ui/components-attachment-pane.ui ui/components-attachment-pane-menus.ui ui/components-attachment-view.ui +ui/components-conversation-actions.ui +ui/components-conversation-action-bar.ui ui/components-in-app-notification.ui ui/components-inspector-error-view.ui ui/components-inspector-log-view.ui diff --git a/src/client/application/application-configuration.vala b/src/client/application/application-configuration.vala index 00c359c9..48542df6 100644 --- a/src/client/application/application-configuration.vala +++ b/src/client/application/application-configuration.vala @@ -19,12 +19,7 @@ public class Application.Configuration : Geary.BaseObject { public const string COMPOSE_AS_HTML_KEY = "compose-as-html"; public const string CONVERSATION_VIEWER_ZOOM_KEY = "conversation-viewer-zoom"; public const string DISPLAY_PREVIEW_KEY = "display-preview"; - public const string FOLDER_LIST_PANE_HORIZONTAL_KEY = "folder-list-pane-horizontal"; - public const string FOLDER_LIST_PANE_POSITION_HORIZONTAL_KEY = "folder-list-pane-position-horizontal"; - public const string FOLDER_LIST_PANE_POSITION_KEY = "folder-list-pane-position"; - public const string FOLDER_LIST_PANE_POSITION_VERTICAL_KEY = "folder-list-pane-position-vertical"; public const string FORMATTING_TOOLBAR_VISIBLE = "formatting-toolbar-visible"; - public const string MESSAGES_PANE_POSITION_KEY = "messages-pane-position"; public const string OPTIONAL_PLUGINS = "optional-plugins"; public const string SEARCH_STRATEGY_KEY = "search-strategy"; public const string SINGLE_KEY_SHORTCUTS = "single-key-shortcuts"; @@ -90,33 +85,11 @@ public class Application.Configuration : Geary.BaseObject { get { return settings.get_boolean(WINDOW_MAXIMIZE_KEY); } } - public int folder_list_pane_position_old { - get { return settings.get_int(FOLDER_LIST_PANE_POSITION_KEY); } - } - - public int folder_list_pane_position_horizontal { - get { return settings.get_int(FOLDER_LIST_PANE_POSITION_HORIZONTAL_KEY); } - set { settings.set_int(FOLDER_LIST_PANE_POSITION_HORIZONTAL_KEY, value); } - } - - public int folder_list_pane_position_vertical { - get { return settings.get_int(FOLDER_LIST_PANE_POSITION_VERTICAL_KEY); } - } - - public bool folder_list_pane_horizontal { - get { return settings.get_boolean(FOLDER_LIST_PANE_HORIZONTAL_KEY); } - } - public bool formatting_toolbar_visible { get { return settings.get_boolean(FORMATTING_TOOLBAR_VISIBLE); } set { settings.set_boolean(FORMATTING_TOOLBAR_VISIBLE, value); } } - public int messages_pane_position { - get { return settings.get_int(MESSAGES_PANE_POSITION_KEY); } - set { settings.set_int(MESSAGES_PANE_POSITION_KEY, value); } - } - public bool autoselect { get { return settings.get_boolean(AUTOSELECT_KEY); } } diff --git a/src/client/application/application-main-window.vala b/src/client/application/application-main-window.vala index 47749019..0e6a89e1 100644 --- a/src/client/application/application-main-window.vala +++ b/src/client/application/application-main-window.vala @@ -31,6 +31,7 @@ public class Application.MainWindow : public const string ACTION_TOGGLE_JUNK = "toggle-conversation-junk"; public const string ACTION_TRASH_CONVERSATION = "trash-conversation"; public const string ACTION_ZOOM = "zoom"; + public const string ACTION_NAVIGATION_BACK = "navigation-back"; private const ActionEntry[] EDIT_ACTIONS = { { Action.Edit.UNDO, on_undo }, @@ -42,6 +43,7 @@ public class Application.MainWindow : { ACTION_FIND_IN_CONVERSATION, on_find_in_conversation_action }, { ACTION_SEARCH, on_search_activated }, + { ACTION_NAVIGATION_BACK, focus_previous_pane}, // Message actions { ACTION_REPLY_CONVERSATION, on_reply_conversation }, { ACTION_REPLY_ALL_CONVERSATION, on_reply_all_conversation }, @@ -282,6 +284,9 @@ public class Application.MainWindow : public ConversationListView conversation_list_view { get; private set; } public ConversationViewer conversation_viewer { get; private set; } + // Actions in the Conversation HeaderBar or ActionBar + private Components.ConversationActions conversation_actions; + public Components.InfoBarStack conversation_list_info_bars { get; private set; default = new Components.InfoBarStack(PRIORITY_QUEUE); } @@ -317,11 +322,9 @@ public class Application.MainWindow : [GtkChild] private Gtk.Box main_layout; [GtkChild] - private Gtk.Box search_bar_box; + private Hdy.Leaflet main_leaflet; [GtkChild] - private Gtk.Paned folder_paned; - [GtkChild] - private Gtk.Paned conversations_paned; + private Hdy.Leaflet conversations_leaflet; [GtkChild] private Gtk.Box folder_box; [GtkChild] @@ -330,9 +333,26 @@ public class Application.MainWindow : private Gtk.Box conversation_list_box; [GtkChild] private Gtk.ScrolledWindow conversation_list_scrolled; + [GtkChild] + private Gtk.SizeGroup folder_size_group; + [GtkChild] + private Gtk.SizeGroup folder_separator_size_group; + [GtkChild] + private Gtk.SizeGroup conversations_size_group; + [GtkChild] + private Gtk.SizeGroup conversations_separator_size_group; + [GtkChild] + private Gtk.SizeGroup conversation_size_group; + [GtkChild] + private Hdy.SwipeGroup conversations_swipe_group; + [GtkChild] + private Hdy.SwipeGroup conversation_swipe_group; + [GtkChild] private Gtk.Overlay overlay; + private Components.ConversationActionBar action_bar; + private Components.InfoBarStack info_bars = new Components.InfoBarStack(SINGLE); @@ -511,7 +531,7 @@ public class Application.MainWindow : }); setup_layout(application.config); - on_change_orientation(); + this.folder_box.pack_start(status_bar, false, false); update_command_actions(); update_conversation_actions(NONE); @@ -677,10 +697,10 @@ public class Application.MainWindow : // selected model. if (this.selected_folder != null) { - this.main_toolbar.copy_folder_menu.enable_disable_folder( + this.conversation_actions.copy_folder_menu.enable_disable_folder( this.selected_folder, true ); - this.main_toolbar.move_folder_menu.enable_disable_folder( + this.conversation_actions.move_folder_menu.enable_disable_folder( this.selected_folder, true ); @@ -725,9 +745,10 @@ public class Application.MainWindow : update_conversation_actions(NONE); update_title(); - this.main_toolbar.update_trash_button( + this.conversation_actions.update_trash_button( !this.is_shift_down && this.selected_folder_supports_trash ); + this.conversation_viewer.show_loading(); this.previous_selection_was_interactive = is_interactive; @@ -764,10 +785,10 @@ public class Application.MainWindow : this.conversation_list_view.set_model(conversations_model); // disable copy/move to the new folder - this.main_toolbar.copy_folder_menu.enable_disable_folder( + this.conversation_actions.copy_folder_menu.enable_disable_folder( to_select, false ); - this.main_toolbar.move_folder_menu.enable_disable_folder( + this.conversation_actions.move_folder_menu.enable_disable_folder( to_select, false ); @@ -901,6 +922,8 @@ public class Application.MainWindow : } else { this.conversation_viewer.do_compose(composer); } + // Show the correct leaflet + this.main_leaflet.set_visible_child_name("conversation"); } } @@ -1073,8 +1096,8 @@ public class Application.MainWindow : foreach (var context in to_add) { this.folder_list.add_folder(context); if (context.folder.account == this.selected_account) { - this.main_toolbar.copy_folder_menu.add_folder(context.folder); - this.main_toolbar.move_folder_menu.add_folder(context.folder); + this.conversation_actions.copy_folder_menu.add_folder(context.folder); + this.conversation_actions.move_folder_menu.add_folder(context.folder); } context.folder.use_changed.connect(on_use_changed); } @@ -1094,8 +1117,8 @@ public class Application.MainWindow : folder.use_changed.disconnect(on_use_changed); if (folder.account == this.selected_account) { - this.main_toolbar.copy_folder_menu.remove_folder(folder); - this.main_toolbar.move_folder_menu.remove_folder(folder); + this.conversation_actions.copy_folder_menu.remove_folder(folder); + this.conversation_actions.move_folder_menu.remove_folder(folder); } this.folder_list.remove_folder(context); } @@ -1130,18 +1153,9 @@ public class Application.MainWindow : // This code both loads AND saves the pane positions with live updating. This is more // resilient against crashes because the value in dconf changes *immediately*, and // stays saved in the event of a crash. - config.bind(Configuration.MESSAGES_PANE_POSITION_KEY, this.conversations_paned, "position"); config.bind(Configuration.WINDOW_WIDTH_KEY, this, "window-width"); config.bind(Configuration.WINDOW_HEIGHT_KEY, this, "window-height"); config.bind(Configuration.WINDOW_MAXIMIZE_KEY, this, "window-maximized"); - // Update to layout - if (config.folder_list_pane_position_horizontal == -1) { - config.folder_list_pane_position_horizontal = config.folder_list_pane_position_old; - config.messages_pane_position += config.folder_list_pane_position_old; - } - config.settings.changed[ - Configuration.FOLDER_LIST_PANE_HORIZONTAL_KEY - ].connect(on_change_orientation); } private void restore_saved_window_state() { @@ -1218,12 +1232,14 @@ public class Application.MainWindow : // Search bar this.search_bar = new SearchBar(this.application.engine); this.search_bar.search_text_changed.connect(on_search); - this.search_bar_box.pack_start(this.search_bar, false, false, 0); + this.conversation_list_box.pack_start(this.search_bar, false, false, 0); + // Folder list this.folder_list.folder_selected.connect(on_folder_selected); this.folder_list.move_conversation.connect(on_move_conversation); this.folder_list.copy_conversation.connect(on_copy_conversation); + this.folder_list.folder_activated.connect(on_folder_activated); this.folder_list_scrolled.add(this.folder_list); // Conversation list @@ -1248,16 +1264,31 @@ public class Application.MainWindow : on_conversation_view_added ); - this.conversations_paned.pack2(this.conversation_viewer, true, false); + this.conversation_viewer.hexpand = true; + this.conversation_size_group.add_widget(this.conversation_viewer); + this.main_leaflet.add_with_properties(this.conversation_viewer, "name", "conversation", null); + + + // Setup conversation actions + this.conversation_actions = new Components.ConversationActions(); + this.conversation_actions.move_folder_menu.folder_selected.connect(on_move_conversation); + this.conversation_actions.copy_folder_menu.folder_selected.connect(on_copy_conversation); + this.conversation_actions.bind_property("find-open", + this.conversation_viewer.conversation_find_bar, + "search-mode-enabled", + BindingFlags.SYNC_CREATE | BindingFlags.BIDIRECTIONAL); // Main toolbar this.main_toolbar = new MainToolbar(config); - this.main_toolbar.move_folder_menu.folder_selected.connect(on_move_conversation); - this.main_toolbar.copy_folder_menu.folder_selected.connect(on_copy_conversation); + this.main_toolbar.add_to_size_groups(this.folder_size_group, + this.folder_separator_size_group, + this.conversations_size_group, + this.conversations_separator_size_group, + this.conversation_size_group); + this.main_toolbar.add_to_swipe_groups(this.conversations_swipe_group, + this.conversation_swipe_group); this.main_toolbar.bind_property("search-open", this.search_bar, "search-mode-enabled", BindingFlags.SYNC_CREATE | BindingFlags.BIDIRECTIONAL); - this.main_toolbar.bind_property("find-open", this.conversation_viewer.conversation_find_bar, - "search-mode-enabled", BindingFlags.SYNC_CREATE | BindingFlags.BIDIRECTIONAL); if (config.desktop_environment == UNITY) { this.main_toolbar.show_close_button = false; this.main_layout.pack_start(main_toolbar, false, true, 0); @@ -1268,6 +1299,8 @@ public class Application.MainWindow : set_titlebar(titlebar); } + this.main_toolbar.add_conversation_actions(this.conversation_actions); + this.main_layout.pack_start(this.info_bars, false, true, 0); // Status bar @@ -1277,6 +1310,12 @@ public class Application.MainWindow : this.spinner.set_progress_monitor(progress_monitor); this.status_bar.add(this.spinner); this.status_bar.show_all(); + + // Action bar + this.action_bar = new Components.ConversationActionBar(); + this.conversation_list_box.add_with_properties(action_bar, + "pack-type", Gtk.PackType.END, + "position", 0); } /** {@inheritDoc} */ @@ -1444,8 +1483,8 @@ public class Application.MainWindow : private void select_account(Geary.Account? account) { if (this.selected_account != account) { if (this.selected_account != null) { - this.main_toolbar.copy_folder_menu.clear(); - this.main_toolbar.move_folder_menu.clear(); + this.conversation_actions.copy_folder_menu.clear(); + this.conversation_actions.move_folder_menu.clear(); } this.selected_account = account; @@ -1453,8 +1492,8 @@ public class Application.MainWindow : if (account != null) { foreach (Geary.Folder folder in account.list_folders()) { - this.main_toolbar.copy_folder_menu.add_folder(folder); - this.main_toolbar.move_folder_menu.add_folder(folder); + this.conversation_actions.copy_folder_menu.add_folder(folder); + this.conversation_actions.move_folder_menu.add_folder(folder); } } @@ -1476,7 +1515,7 @@ public class Application.MainWindow : // setting it again. this.conversation_list_view.select_conversations(to_select); - this.main_toolbar.selected_conversations = to_select.size; + this.conversation_actions.selected_conversations = to_select.size; if (this.selected_folder != null && !this.has_composer) { switch(to_select.size) { case 0: @@ -1642,38 +1681,6 @@ public class Application.MainWindow : } } - private void on_change_orientation() { - bool horizontal = this.application.config.folder_list_pane_horizontal; - bool initial = true; - - if (this.status_bar.parent != null) { - this.status_bar.parent.remove(status_bar); - initial = false; - } - - GLib.Settings.unbind(this.folder_paned, "position"); - this.folder_paned.orientation = horizontal ? Gtk.Orientation.HORIZONTAL : - Gtk.Orientation.VERTICAL; - - int folder_list_width = - this.application.config.folder_list_pane_position_horizontal; - if (horizontal) { - if (!initial) - this.conversations_paned.position += folder_list_width; - this.folder_box.pack_start(status_bar, false, false); - } else { - if (!initial) - this.conversations_paned.position -= folder_list_width; - this.conversation_list_box.pack_start(status_bar, false, false); - } - - this.application.config.bind( - horizontal - ? Configuration.FOLDER_LIST_PANE_POSITION_HORIZONTAL_KEY - : Configuration.FOLDER_LIST_PANE_POSITION_VERTICAL_KEY, - this.folder_paned, "position"); - } - private void update_headerbar() { update_title(); if (this.selected_folder != null) { @@ -1721,13 +1728,13 @@ public class Application.MainWindow : bool move_enabled = ( sensitive && (selected_folder is Geary.FolderSupport.Move) ); - this.main_toolbar.move_message_button.set_sensitive(move_enabled); + this.conversation_actions.move_message_button.set_sensitive(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); + this.conversation_actions.copy_message_button.set_sensitive(copy_enabled); get_window_action(ACTION_SHOW_COPY_MENU).set_enabled(move_enabled); get_window_action(ACTION_ARCHIVE_CONVERSATION).set_enabled( @@ -1741,6 +1748,17 @@ public class Application.MainWindow : ); this.update_context_dependent_actions.begin(sensitive); + switch (count) { + case NONE: + conversation_actions.take_ownership(null); + break; + case SINGLE: + this.main_toolbar.add_conversation_actions(this.conversation_actions); + break; + case MULTIPLE: + this.action_bar.add_conversation_actions(this.conversation_actions); + break; + } } private async void update_context_dependent_actions(bool sensitive) { @@ -1796,7 +1814,7 @@ public class Application.MainWindow : private void set_shift_key_down(bool down) { this.is_shift_down = down; - this.main_toolbar.update_trash_button( + this.conversation_actions.update_trash_button( !down && this.selected_folder_supports_trash ); } @@ -1816,7 +1834,23 @@ public class Application.MainWindow : private void focus_next_pane() { var focus = get_focus(); - if (focus != null) { + + if (main_leaflet.folded) { + if (main_leaflet.visible_child_name == "conversations") { + if (conversations_leaflet.folded && + conversations_leaflet.visible_child_name == "folder" || + focus == this.folder_list) { + conversations_leaflet.navigate(Hdy.NavigationDirection.FORWARD); + focus = this.conversation_list_view; + } else { + if (this.conversation_actions.selected_conversations == 1 && + this.selected_folder.properties.email_total > 0) { + main_leaflet.navigate(Hdy.NavigationDirection.FORWARD); + focus = this.conversation_viewer.visible_child; + } + } + } + } else if (focus != null) { if (focus == this.folder_list || focus.is_ancestor(this.folder_list)) { focus = this.conversation_list_view; @@ -1838,7 +1872,25 @@ public class Application.MainWindow : private void focus_previous_pane() { var focus = get_focus(); - if (focus != null) { + + if (main_leaflet.folded) { + if (main_leaflet.visible_child_name == "conversations") { + if (conversations_leaflet.folded) { + if (conversations_leaflet.visible_child_name == "conversations") { + conversations_leaflet.navigate(Hdy.NavigationDirection.BACK); + focus = this.folder_list; + } + } else { + if (focus == this.conversation_list_view) + focus = this.folder_list; + else + focus = this.conversation_list_view; + } + } else { + main_leaflet.navigate(Hdy.NavigationDirection.BACK); + focus = this.conversation_list_view; + } + } else if (focus != null) { if (focus == this.folder_list || focus.is_ancestor(this.folder_list)) { focus = this.conversation_viewer.visible_child; @@ -1938,6 +1990,17 @@ public class Application.MainWindow : return Gdk.EVENT_STOP; } + [GtkCallback] + private void on_main_leaflet_visible_child_changed() { + if (main_leaflet.child_transition_running) + return; + + if (main_leaflet.visible_child_name == "conversations" && main_leaflet.folded) + if (this.conversation_viewer.current_composer != null) { + this.conversation_viewer.current_composer.activate_close_action(); + } + } + private void on_offline_infobar_response() { this.info_bars.remove(this.offline_infobar); } @@ -2121,13 +2184,21 @@ public class Application.MainWindow : } } - private void on_conversation_activated(Geary.App.Conversation activated) { - if (this.selected_folder != null) { + private void on_folder_activated(Geary.Folder? folder) { + if (folder != null) + focus_next_pane(); + } + + private void on_conversation_activated(Geary.App.Conversation activated, bool single) { + if (single) { + if (main_leaflet.folded) + focus_next_pane(); + } else if (this.selected_folder != null) { if (this.selected_folder.used_as != DRAFTS) { this.application.new_window.begin( this.selected_folder, this.conversation_list_view.copy_selected() - ); + ); } else { // TODO: Determine how to map between conversations // and drafts correctly. @@ -2176,11 +2247,11 @@ public class Application.MainWindow : } private void on_show_copy_menu() { - this.main_toolbar.copy_message_button.clicked(); + this.conversation_actions.copy_message_button.clicked(); } private void on_show_move_menu() { - this.main_toolbar.move_message_button.clicked(); + this.conversation_actions.move_message_button.clicked(); } private void on_conversation_up() { diff --git a/src/client/components/components-conversation-action-bar.vala b/src/client/components/components-conversation-action-bar.vala new file mode 100644 index 00000000..cb574521 --- /dev/null +++ b/src/client/components/components-conversation-action-bar.vala @@ -0,0 +1,39 @@ +/* + * Copyright © 2016 Software Freedom Conservancy Inc. + * Copyright © 2020 Purism SPC + * + * This software is licensed under the GNU Lesser General Public License + * (version 2.1 or later). See the COPYING file in this distribution. + */ + +// Draws the conversation action bar. +[GtkTemplate (ui = "/org/gnome/Geary/components-conversation-action-bar.ui")] +public class Components.ConversationActionBar : Gtk.Revealer { + private ulong owner_notify; + + [GtkChild] + private Gtk.Box action_box; + + public ConversationActionBar() { + } + + /** + * This takes ownership of the ConversationActions and places some of + * the buttons into the ActionBar. + */ + public void add_conversation_actions(Components.ConversationActions actions) { + if (actions.owner == this) + return; + + actions.take_ownership(this); + action_box.pack_start(actions.mark_copy_move_buttons, false, false); + action_box.pack_end(actions.archive_trash_delete_buttons, false, false); + reveal_child = true; + this.owner_notify = actions.notify["owner"].connect(() => { + if (actions.owner != this) { + reveal_child = false; + actions.disconnect (this.owner_notify); + } + }); + } +} diff --git a/src/client/components/components-conversation-actions.vala b/src/client/components/components-conversation-actions.vala new file mode 100644 index 00000000..a8cdaed9 --- /dev/null +++ b/src/client/components/components-conversation-actions.vala @@ -0,0 +1,132 @@ +/* Copyright 2017 Software Freedom Conservancy Inc. + * + * This software is licensed under the GNU Lesser General Public License + * (version 2.1 or later). See the COPYING file in this distribution. + */ + +/** + * Container for actions for a conversation generally placed into the ActionBar or HeaderBar + * The user of the actions needs to take ownership before they can place the actions in a container + */ +public class Components.ConversationActions : GLib.Object { + public Gtk.Widget? owner { get; private set; } + // Copy and Move popovers + public FolderPopover copy_folder_menu { get; private set; default = new FolderPopover(); } + public FolderPopover move_folder_menu { get; private set; default = new FolderPopover(); } + // How many conversations are selected right now. Should automatically be updated. + public int selected_conversations { get; set; } + public bool find_open { get; set; } + + public Gtk.Box mark_copy_move_buttons { get; private set; } + public Gtk.MenuButton mark_message_button { get; private set; } + public Gtk.MenuButton copy_message_button { get; private set; } + public Gtk.MenuButton move_message_button { get; private set; } + + public Gtk.Box reply_forward_buttons { get; private set; } + + public Gtk.Box archive_trash_delete_buttons { get; private set; } + private Gtk.Button archive_button; + private Gtk.Button trash_delete_button; + + public Gtk.ToggleButton find_button { get; private set; } + + private bool show_trash_button = true; + + // Load these at construction time + private Gtk.Image trash_image = new Gtk.Image.from_icon_name("user-trash-symbolic", Gtk.IconSize.MENU); + private Gtk.Image delete_image = new Gtk.Image.from_icon_name("edit-delete-symbolic", Gtk.IconSize.MENU); + + public ConversationActions() { + Gtk.Builder builder = + new Gtk.Builder.from_resource("/org/gnome/Geary/components-conversation-actions.ui"); + // Assemble the mark menus + Gtk.Builder menu_builder = + new Gtk.Builder.from_resource("/org/gnome/Geary/main-toolbar-menus.ui"); + MenuModel mark_menu = (MenuModel) menu_builder.get_object("mark_message_menu"); + + this.mark_copy_move_buttons = (Gtk.Box) builder.get_object("mark_copy_move_buttons"); + this.mark_message_button = (Gtk.MenuButton) builder.get_object("mark_message_button"); + this.copy_message_button = (Gtk.MenuButton) builder.get_object("copy_message_button"); + this.move_message_button = (Gtk.MenuButton) builder.get_object("move_message_button"); + + this.reply_forward_buttons = (Gtk.Box) builder.get_object("reply_forward_buttons"); + + this.archive_trash_delete_buttons = (Gtk.Box) builder.get_object("archive_trash_delete_buttons"); + this.archive_button = (Gtk.Button) builder.get_object("archive_button"); + this.trash_delete_button = (Gtk.Button) builder.get_object("trash_delete_button"); + + this.find_button = (Gtk.ToggleButton) builder.get_object("find_button"); + + this.bind_property("find-open", this.find_button, "active", + BindingFlags.SYNC_CREATE | BindingFlags.BIDIRECTIONAL); + this.notify["selected-conversations"].connect(() => update_conversation_buttons()); + this.mark_message_button.popover = new Gtk.Popover.from_model(null, mark_menu); + this.copy_message_button.popover = copy_folder_menu; + this.move_message_button.popover = move_folder_menu; + } + + /** Sets the new owner and removes the previous owner and parents of the single actions */ + public void take_ownership(Gtk.Widget? new_owner) { + remove_parent(mark_copy_move_buttons); + remove_parent(reply_forward_buttons); + remove_parent(archive_trash_delete_buttons); + remove_parent(find_button); + owner = new_owner; + } + + private void remove_parent (Gtk.Widget widget) { + if (widget.parent != null) + widget.parent.remove(widget); + } + + public void update_trash_button(bool show_trash) { + this.show_trash_button = show_trash; + update_conversation_buttons(); + } + + /** Updates tooltip text depending on number of conversations selected. */ + private void update_conversation_buttons() { + this.mark_message_button.tooltip_text = ngettext( + "Mark conversation", + "Mark conversations", + this.selected_conversations + ); + this.copy_message_button.tooltip_text = ngettext( + "Add label to conversation", + "Add label to conversations", + this.selected_conversations + ); + this.move_message_button.tooltip_text = ngettext( + "Move conversation", + "Move conversations", + this.selected_conversations + ); + this.archive_button.tooltip_text = ngettext( + "Archive conversation", + "Archive conversations", + this.selected_conversations + ); + + if (this.show_trash_button) { + this.trash_delete_button.action_name = Action.Window.prefix( + Application.MainWindow.ACTION_TRASH_CONVERSATION + ); + this.trash_delete_button.image = trash_image; + this.trash_delete_button.tooltip_text = ngettext( + "Move conversation to Trash", + "Move conversations to Trash", + this.selected_conversations + ); + } else { + this.trash_delete_button.action_name = Action.Window.prefix( + Application.MainWindow.ACTION_DELETE_CONVERSATION + ); + this.trash_delete_button.image = delete_image; + this.trash_delete_button.tooltip_text = ngettext( + "Delete conversation", + "Delete conversations", + this.selected_conversations + ); + } + } +} diff --git a/src/client/components/components-preferences-window.vala b/src/client/components/components-preferences-window.vala index b1bdfd7b..ea978a3a 100644 --- a/src/client/components/components-preferences-window.vala +++ b/src/client/components/components-preferences-window.vala @@ -136,16 +136,6 @@ public class Components.PreferencesWindow : Hdy.PreferencesWindow { display_preview_row.activatable_widget = display_preview; display_preview_row.add(display_preview); - var three_pane_view = new Gtk.Switch(); - three_pane_view.valign = CENTER; - - var three_pane_view_row = new Hdy.ActionRow(); - /// Translators: Preferences label - three_pane_view_row.title = _("Use _three pane view"); - three_pane_view_row.use_underline = true; - three_pane_view_row.activatable_widget = three_pane_view; - three_pane_view_row.add(three_pane_view); - var single_key_shortucts = new Gtk.Switch(); single_key_shortucts.valign = CENTER; @@ -180,7 +170,6 @@ public class Components.PreferencesWindow : Hdy.PreferencesWindow { //group.description = _("General application preferences"); group.add(autoselect_row); group.add(display_preview_row); - group.add(three_pane_view_row); group.add(single_key_shortucts_row); group.add(startup_notifications_row); @@ -210,11 +199,6 @@ public class Components.PreferencesWindow : Hdy.PreferencesWindow { display_preview, "state" ); - config.bind( - Application.Configuration.FOLDER_LIST_PANE_HORIZONTAL_KEY, - three_pane_view, - "state" - ); config.bind( Application.Configuration.SINGLE_KEY_SHORTCUTS, single_key_shortucts, diff --git a/src/client/components/main-toolbar.vala b/src/client/components/main-toolbar.vala index 31f87df6..6458b7fb 100644 --- a/src/client/components/main-toolbar.vala +++ b/src/client/components/main-toolbar.vala @@ -6,7 +6,7 @@ // Draws the main toolbar. [GtkTemplate (ui = "/org/gnome/Geary/main-toolbar.ui")] -public class MainToolbar : Gtk.Box { +public class MainToolbar : Hdy.Leaflet { // How wide the left pane should be. Auto-synced with our settings public int left_pane_width { get; set; } // Used to form the title of the folder header @@ -14,145 +14,101 @@ public class MainToolbar : Gtk.Box { public string folder { get; set; } // Close button settings public bool show_close_button { get; set; default = true; } - // Search and find bar + // Search bar public bool search_open { get; set; default = false; } - public bool find_open { get; set; default = false; } - // Copy and Move popovers - public FolderPopover copy_folder_menu { get; private set; default = new FolderPopover(); } - public FolderPopover move_folder_menu { get; private set; default = new FolderPopover(); } - // How many conversations are selected right now. Should automatically be updated. - public int selected_conversations { get; set; } + [GtkChild] + private Hdy.Leaflet conversations_leaflet; // Folder header elements [GtkChild] private Gtk.HeaderBar folder_header; [GtkChild] - private Gtk.ToggleButton search_conversations_button; - [GtkChild] private Gtk.MenuButton main_menu_button; + [GtkChild] + private Gtk.Separator folder_separator; + + // Conversations header elements + [GtkChild] + private Gtk.HeaderBar conversations_header; + [GtkChild] + private Gtk.ToggleButton search_conversations_button; + + [GtkChild] + private Gtk.Separator conversations_separator; + // Conversation header elements [GtkChild] private Gtk.HeaderBar conversation_header; - [GtkChild] - private Gtk.MenuButton mark_message_button; - [GtkChild] - public Gtk.MenuButton copy_message_button; - [GtkChild] - public Gtk.MenuButton move_message_button; - [GtkChild] - private Gtk.Button archive_button; - [GtkChild] - private Gtk.Button trash_delete_button; - [GtkChild] - private Gtk.ToggleButton find_button; [GtkChild] private Hdy.HeaderGroup header_group; - private bool show_trash_button = true; - - // Load these at construction time - private Gtk.Image trash_image = new Gtk.Image.from_icon_name("user-trash-symbolic", Gtk.IconSize.MENU); - private Gtk.Image delete_image = new Gtk.Image.from_icon_name("edit-delete-symbolic", Gtk.IconSize.MENU); - + Gtk.SizeGroup conversation_group; public MainToolbar(Application.Configuration config) { - // Sync headerbar width with left pane - config.bind( - Application.Configuration.MESSAGES_PANE_POSITION_KEY, - this, - "left-pane-width", - SettingsBindFlags.GET - ); - this.bind_property("left-pane-width", this.folder_header, "width-request", BindingFlags.SYNC_CREATE); - if (config.desktop_environment != UNITY) { - this.bind_property("account", this.folder_header, "title", BindingFlags.SYNC_CREATE); - this.bind_property("folder", this.folder_header, "subtitle", BindingFlags.SYNC_CREATE); + this.bind_property("account", this.conversations_header, "title", BindingFlags.SYNC_CREATE); + this.bind_property("folder", this.conversations_header, "subtitle", BindingFlags.SYNC_CREATE); } // Assemble the main/mark menus Gtk.Builder builder = new Gtk.Builder.from_resource("/org/gnome/Geary/main-toolbar-menus.ui"); MenuModel main_menu = (MenuModel) builder.get_object("main_menu"); - MenuModel mark_menu = (MenuModel) builder.get_object("mark_message_menu"); // Setup folder header elements this.main_menu_button.popover = new Gtk.Popover.from_model(null, main_menu); this.bind_property("search-open", this.search_conversations_button, "active", BindingFlags.SYNC_CREATE | BindingFlags.BIDIRECTIONAL); + } - // Setup conversation header elements - this.notify["selected-conversations"].connect(() => update_conversation_buttons()); - this.mark_message_button.popover = new Gtk.Popover.from_model(null, mark_menu); - this.copy_message_button.popover = copy_folder_menu; - this.move_message_button.popover = move_folder_menu; + public void add_conversation_actions(Components.ConversationActions actions) { + if (actions.owner == this) + return; - this.bind_property("find-open", this.find_button, "active", - BindingFlags.SYNC_CREATE | BindingFlags.BIDIRECTIONAL); + actions.take_ownership(this); + conversation_header.pack_start(actions.mark_copy_move_buttons); + conversation_header.pack_start(actions.reply_forward_buttons); + conversation_header.pack_end(actions.find_button); + conversation_header.pack_end(actions.archive_trash_delete_buttons); } public void set_conversation_header(Gtk.HeaderBar header) { - conversation_header.hide(); + remove(conversation_header); this.header_group.add_gtk_header_bar(header); - pack_start(header, true, true); + header.hexpand = true; + conversation_group.remove_widget(conversation_header); + conversation_group.add_widget(header); + add(header); + child_set(header, "name", "conversation", null); } public void remove_conversation_header(Gtk.HeaderBar header) { remove(header); this.header_group.remove_gtk_header_bar(header); - conversation_header.show(); + conversation_group.remove_widget(header); + conversation_group.add_widget(conversation_header); + add(conversation_header); + child_set(conversation_header, "name", "conversation", null); } - public void update_trash_button(bool show_trash) { - this.show_trash_button = show_trash; - update_conversation_buttons(); + public void add_to_size_groups(Gtk.SizeGroup folder_group, + Gtk.SizeGroup folder_separator_group, + Gtk.SizeGroup conversations_group, + Gtk.SizeGroup conversations_separator_group, + Gtk.SizeGroup conversation_group) { + folder_group.add_widget(folder_header); + folder_separator_group.add_widget(folder_separator); + conversations_group.add_widget(conversations_header); + conversations_separator_group.add_widget(conversations_separator); + conversation_group.add_widget(conversation_header); + this.conversation_group = conversation_group; } - // Updates tooltip text depending on number of conversations selected. - private void update_conversation_buttons() { - this.mark_message_button.tooltip_text = ngettext( - "Mark conversation", - "Mark conversations", - this.selected_conversations - ); - this.copy_message_button.tooltip_text = ngettext( - "Add label to conversation", - "Add label to conversations", - this.selected_conversations - ); - this.move_message_button.tooltip_text = ngettext( - "Move conversation", - "Move conversations", - this.selected_conversations - ); - this.archive_button.tooltip_text = ngettext( - "Archive conversation", - "Archive conversations", - this.selected_conversations - ); - - if (this.show_trash_button) { - this.trash_delete_button.action_name = Action.Window.prefix( - Application.MainWindow.ACTION_TRASH_CONVERSATION - ); - this.trash_delete_button.image = trash_image; - this.trash_delete_button.tooltip_text = ngettext( - "Move conversation to Trash", - "Move conversations to Trash", - this.selected_conversations - ); - } else { - this.trash_delete_button.action_name = Action.Window.prefix( - Application.MainWindow.ACTION_DELETE_CONVERSATION - ); - this.trash_delete_button.image = delete_image; - this.trash_delete_button.tooltip_text = ngettext( - "Delete conversation", - "Delete conversations", - this.selected_conversations - ); - } + public void add_to_swipe_groups(Hdy.SwipeGroup conversations_group, + Hdy.SwipeGroup conversation_group) { + conversations_group.add_swipeable(this.conversations_leaflet); + conversation_group.add_swipeable(this); } } diff --git a/src/client/composer/composer-widget.vala b/src/client/composer/composer-widget.vala index 37e93fb4..17430021 100644 --- a/src/client/composer/composer-widget.vala +++ b/src/client/composer/composer-widget.vala @@ -1361,6 +1361,11 @@ public class Composer.Widget : Gtk.EventBox, Geary.BaseInterface { } } + /* Activate the close action */ + public void activate_close_action() { + this.actions.activate_action(ACTION_CLOSE, null); + } + internal void set_mode(PresentationMode new_mode) { this.current_mode = new_mode; this.header.set_mode(new_mode); diff --git a/src/client/conversation-list/conversation-list-view.vala b/src/client/conversation-list/conversation-list-view.vala index a4d5cca6..0baa66b4 100644 --- a/src/client/conversation-list/conversation-list-view.vala +++ b/src/client/conversation-list/conversation-list-view.vala @@ -17,6 +17,7 @@ public class ConversationListView : Gtk.TreeView, Geary.BaseInterface { private Geary.Scheduler.Scheduled? scheduled_update_visible_conversations = null; private Gee.Set selected = new Gee.HashSet(); private Geary.IdleManager selection_update; + private Gtk.GestureMultiPress gesture; // Determines if the next folder scan should avoid selecting a // conversation when autoselect is enabled @@ -26,7 +27,7 @@ public class ConversationListView : Gtk.TreeView, Geary.BaseInterface { public signal void conversations_selected(Gee.Set selected); // Signal for when a conversation has been double-clicked, or selected and enter is pressed. - public signal void conversation_activated(Geary.App.Conversation activated); + public signal void conversation_activated(Geary.App.Conversation activated, bool single = false); public virtual signal void load_more() { enable_load_more = false; @@ -52,11 +53,13 @@ public class ConversationListView : Gtk.TreeView, Geary.BaseInterface { Gtk.TreeSelection selection = get_selection(); selection.set_mode(Gtk.SelectionMode.MULTIPLE); style_updated.connect(on_style_changed); - row_activated.connect(on_row_activated); notify["vadjustment"].connect(on_vadjustment_changed); + key_press_event.connect(on_key_press); button_press_event.connect(on_button_press); + gesture = new Gtk.GestureMultiPress(this); + gesture.pressed.connect(on_gesture_pressed); // Set up drag and drop. Gtk.drag_source_set(this, Gdk.ModifierType.BUTTON1_MASK, FolderList.Tree.TARGET_ENTRY_LIST, @@ -269,6 +272,53 @@ public class ConversationListView : Gtk.TreeView, Geary.BaseInterface { return parent.get_vadjustment(); } + private void on_gesture_pressed(int n_press, double x, double y) { + if (gesture.get_current_button() != Gdk.BUTTON_PRIMARY) + return; + + Gtk.TreePath? path; + get_path_at_pos((int) x, (int) y, out path, null, null, null); + + // If the user clicked in an empty area, do nothing. + if (path == null) + return; + + Geary.App.Conversation? c = get_model().get_conversation_at_path(path); + if (c == null) + return; + + Gdk.Event event = gesture.get_last_event(gesture.get_current_sequence()); + Gdk.ModifierType modifiers = Gtk.accelerator_get_default_mod_mask(); + + Gdk.ModifierType state_mask; + event.get_state(out state_mask); + + if ((state_mask & modifiers) == 0 && n_press == 1) { + conversation_activated(c, true); + } else if ((state_mask & modifiers) == Gdk.ModifierType.SHIFT_MASK && n_press == 2) { + conversation_activated(c); + } + } + + private bool on_key_press(Gdk.EventKey event) { + if (this.selected.size != 1) + return false; + + Geary.App.Conversation? c = this.selected.to_array()[0]; + if (c == null) + return false; + + Gdk.ModifierType modifiers = Gtk.accelerator_get_default_mod_mask(); + + if (event.keyval == Gdk.Key.Return || + event.keyval == Gdk.Key.ISO_Enter || + event.keyval == Gdk.Key.KP_Enter || + event.keyval == Gdk.Key.space || + event.keyval == Gdk.Key.KP_Space) + conversation_activated(c, !((event.state & modifiers) == Gdk.ModifierType.SHIFT_MASK)); + return false; + } + private bool on_button_press(Gdk.EventButton event) { // Get the coordinates on the cell as well as the clicked path. int cell_x; @@ -575,12 +625,6 @@ public class ConversationListView : Gtk.TreeView, Geary.BaseInterface { return false; } - private void on_row_activated(Gtk.TreePath path) { - Geary.App.Conversation? c = get_model().get_conversation_at_path(path); - if (c != null) - conversation_activated(c); - } - // Enable/disable hover effect on all selected cells. private void set_hover_selected(bool hover) { ConversationListCellRenderer.set_hover_selected(hover); diff --git a/src/client/conversation-viewer/conversation-viewer.vala b/src/client/conversation-viewer/conversation-viewer.vala index f04a1d26..eec5f6a4 100644 --- a/src/client/conversation-viewer/conversation-viewer.vala +++ b/src/client/conversation-viewer/conversation-viewer.vala @@ -68,6 +68,10 @@ public class ConversationViewer : Gtk.Stack, Geary.BaseInterface { public signal void conversation_removed(ConversationListBox list); + static construct { + set_css_name("geary-conversation-viewer"); + } + /** * Constructs a new conversation view instance. */ diff --git a/src/client/folder-list/folder-list-tree.vala b/src/client/folder-list/folder-list-tree.vala index fb91347e..f820d12f 100644 --- a/src/client/folder-list/folder-list-tree.vala +++ b/src/client/folder-list/folder-list-tree.vala @@ -16,6 +16,7 @@ public class FolderList.Tree : Sidebar.Tree, Geary.BaseInterface { public signal void folder_selected(Geary.Folder? folder); + public signal void folder_activated(Geary.Folder? folder); public signal void copy_conversation(Geary.Folder folder); public signal void move_conversation(Geary.Folder folder); @@ -30,7 +31,9 @@ public class FolderList.Tree : Sidebar.Tree, Geary.BaseInterface { public Tree() { base(TARGET_ENTRY_LIST, Gdk.DragAction.COPY | Gdk.DragAction.MOVE, drop_handler); base_ref(); + set_activate_on_single_click(true); entry_selected.connect(on_entry_selected); + entry_activated.connect(on_entry_activated); // GtkTreeView binds Ctrl+N to "move cursor to next". Not so interested in that, so we'll // remove it. @@ -87,6 +90,13 @@ public class FolderList.Tree : Sidebar.Tree, Geary.BaseInterface { } } + private void on_entry_activated(Sidebar.SelectableEntry selectable) { + AbstractFolderEntry? entry = selectable as AbstractFolderEntry; + if (entry != null) { + folder_activated(entry.folder); + } + } + public void set_user_folders_root_name(Geary.Account account, string name) { if (account_branches.has_key(account)) account_branches.get(account).user_folder_group.rename(name); diff --git a/src/client/meson.build b/src/client/meson.build index 088f4e47..c0eb0c16 100644 --- a/src/client/meson.build +++ b/src/client/meson.build @@ -48,6 +48,8 @@ client_vala_sources = files( 'components/client-web-view.vala', 'components/components-attachment-pane.vala', + 'components/components-conversation-actions.vala', + 'components/components-conversation-action-bar.vala', 'components/components-entry-undo.vala', 'components/components-info-bar-stack.vala', 'components/components-info-bar.vala', diff --git a/src/client/sidebar/sidebar-tree.vala b/src/client/sidebar/sidebar-tree.vala index a5f95092..78d73e98 100644 --- a/src/client/sidebar/sidebar-tree.vala +++ b/src/client/sidebar/sidebar-tree.vala @@ -80,6 +80,8 @@ public class Sidebar.Tree : Gtk.TreeView { public signal void entry_selected(Sidebar.SelectableEntry selectable); + public signal void entry_activated(Sidebar.SelectableEntry selectable); + public signal void selected_entry_removed(Sidebar.SelectableEntry removed); public signal void branch_added(Sidebar.Branch branch); @@ -298,6 +300,15 @@ public class Sidebar.Tree : Gtk.TreeView { return true; } + public override void row_activated(Gtk.TreePath path, Gtk.TreeViewColumn column) { + EntryWrapper? wrapper = get_wrapper_at_path(path); + if (wrapper != null) { + Sidebar.SelectableEntry? selectable = wrapper.entry as Sidebar.SelectableEntry; + if (selectable != null) + entry_activated(selectable); + } + } + public override void cursor_changed() { Gtk.TreePath? path = get_selected_path(); if (path == null) { diff --git a/ui/application-main-window.ui b/ui/application-main-window.ui index f429a5ee..547b063f 100644 --- a/ui/application-main-window.ui +++ b/ui/application-main-window.ui @@ -24,113 +24,118 @@ False vertical - + True True + True + over + + - + True - False - vertical + True + True + True + over - + True - True + False + vertical - + True False - vertical + True + 0 + in - + + 100 True - False - 0 - in - - - 100 - True - True - never - - - + True + never - - True - True - 0 - - - - - False - False - - - - - True - False - vertical - - - True - False - 0 - in - - - 250 - True - True - - - - - - True - True - end - 0 - - True - False + True + 0 + + + folder + + + + + True + False + vertical - True - True - end - 0 + False + + + + + True + False + vertical + + + True + False + 0 + in + + + 250 + True + True + + + + + + True + True + end + 1 + + + + + conversations - - False - False + conversations - + + True + False + vertical + + + + False + @@ -157,4 +162,41 @@ + + horizontal + + + + + + horizontal + + + + + + horizontal + + + + + + horizontal + + + + + + horizontal + + + + + + + + + + + diff --git a/ui/components-conversation-action-bar.ui b/ui/components-conversation-action-bar.ui new file mode 100644 index 00000000..ae49683f --- /dev/null +++ b/ui/components-conversation-action-bar.ui @@ -0,0 +1,22 @@ + + + + + True + False + mail-archive-symbolic + + + diff --git a/ui/components-conversation-actions.ui b/ui/components-conversation-actions.ui new file mode 100644 index 00000000..b554deda --- /dev/null +++ b/ui/components-conversation-actions.ui @@ -0,0 +1,221 @@ + + + + + + True + False + + + True + True + False + False + True + + + True + False + marker-symbolic + + + + + False + True + 0 + + + + + True + True + False + False + True + + + True + False + tag-symbolic + + + + + False + True + 1 + + + + + True + True + False + False + True + + + True + False + folder-symbolic + + + + + False + True + 2 + + + + + + True + False + + + True + True + False + False + Reply + win.reply-conversation + True + + + True + False + mail-reply-sender-symbolic + + + + + False + True + 0 + + + + + True + True + False + False + Reply All + win.reply-all-conversation + True + + + True + False + mail-reply-all-symbolic + + + + + False + True + 1 + + + + + True + True + False + False + Forward + win.forward-conversation + True + + + True + False + mail-forward-symbolic + + + + + False + True + 2 + + + + + + True + False + mail-archive-symbolic + + + True + False + + + _Archive + True + True + False + False + win.archive-conversation + archive_image + True + True + + + False + True + 0 + + + + + True + True + False + False + win.trash-conversation + True + + + True + False + user-trash-symbolic + + + + + False + True + 1 + + + + + + True + True + False + False + Toggle find bar + True + + + True + False + preferences-system-search-symbolic + + + + diff --git a/ui/geary.css b/ui/geary.css index 0ddfab30..2d1d48c3 100644 --- a/ui/geary.css +++ b/ui/geary.css @@ -12,12 +12,23 @@ border-left-width: 0; border-top-width: 0; border-right-width: 0; + min-width: 300px; } .geary-conversation-frame > border { border-left-width: 0; border-top-width: 0; border-right-width: 0; + min-width: 360px; } + +treeview.sidebar { + border: none; +} + +geary-conversation-viewer { + min-width: 360px; +} + /* For 3-pane mode only */ .geary-sidebar-pane-separator.vertical .conversation-frame > border { border-bottom-width: 0; diff --git a/ui/main-toolbar.ui b/ui/main-toolbar.ui index 874f5b4b..64f888ee 100644 --- a/ui/main-toolbar.ui +++ b/ui/main-toolbar.ui @@ -7,82 +7,145 @@ False mail-archive-symbolic -