2730 lines
102 KiB
Vala
2730 lines
102 KiB
Vala
/*
|
|
* Copyright © 2016 Software Freedom Conservancy Inc.
|
|
* Copyright © 2016, 2019-2021 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.
|
|
*/
|
|
|
|
[GtkTemplate (ui = "/org/gnome/Geary/application-main-window.ui")]
|
|
public class Application.MainWindow :
|
|
Hdy.ApplicationWindow, Geary.BaseInterface {
|
|
|
|
|
|
// Named actions.
|
|
public const string ACTION_ARCHIVE_CONVERSATION = "archive-conversation";
|
|
public const string ACTION_CONVERSATION_DOWN = "down-conversation";
|
|
public const string ACTION_CONVERSATION_UP = "up-conversation";
|
|
public const string ACTION_DELETE_CONVERSATION = "delete-conversation";
|
|
public const string ACTION_FIND_IN_CONVERSATION = "find-in-conversation";
|
|
public const string ACTION_FORWARD_CONVERSATION = "forward-conversation";
|
|
public const string ACTION_MARK_AS_READ = "mark-conversation-read";
|
|
public const string ACTION_MARK_AS_STARRED = "mark-conversation-starred";
|
|
public const string ACTION_MARK_AS_UNREAD = "mark-conversation-unread";
|
|
public const string ACTION_MARK_AS_UNSTARRED = "mark-conversation-unstarred";
|
|
public const string ACTION_REPLY_ALL_CONVERSATION = "reply-all-conversation";
|
|
public const string ACTION_REPLY_CONVERSATION = "reply-conversation";
|
|
public const string ACTION_SEARCH = "search";
|
|
public const string ACTION_SELECT_INBOX = "select-inbox";
|
|
public const string ACTION_SHOW_COPY_MENU = "show-copy-menu";
|
|
public const string ACTION_SHOW_MARK_MENU = "show-mark-menu";
|
|
public const string ACTION_SHOW_MOVE_MENU = "show-move-menu";
|
|
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 },
|
|
{ Action.Edit.REDO, on_redo },
|
|
};
|
|
|
|
private const ActionEntry[] WINDOW_ACTIONS = {
|
|
{ Action.Window.CLOSE, on_close },
|
|
{ Action.Window.SHOW_HELP_OVERLAY, on_show_help_overlay },
|
|
{ Action.Window.SHOW_MENU, on_show_window_menu },
|
|
|
|
{ ACTION_FIND_IN_CONVERSATION, on_find_in_conversation_action },
|
|
{ ACTION_SEARCH, on_search_activated },
|
|
{ ACTION_SELECT_INBOX, on_select_inbox, "i" },
|
|
{ ACTION_NAVIGATION_BACK, focus_previous_pane},
|
|
|
|
// Message actions
|
|
{ ACTION_REPLY_CONVERSATION, on_reply_conversation },
|
|
{ ACTION_REPLY_ALL_CONVERSATION, on_reply_all_conversation },
|
|
{ ACTION_FORWARD_CONVERSATION, on_forward_conversation },
|
|
{ ACTION_ARCHIVE_CONVERSATION, on_archive_conversation },
|
|
{ ACTION_TRASH_CONVERSATION, on_trash_conversation },
|
|
{ ACTION_DELETE_CONVERSATION, on_delete_conversation },
|
|
{ ACTION_SHOW_COPY_MENU, on_show_copy_menu },
|
|
{ ACTION_SHOW_MOVE_MENU, on_show_move_menu },
|
|
{ ACTION_CONVERSATION_UP, on_conversation_up },
|
|
{ ACTION_CONVERSATION_DOWN, on_conversation_down },
|
|
// Message marking actions
|
|
{ ACTION_SHOW_MARK_MENU, on_show_mark_menu },
|
|
{ ACTION_MARK_AS_READ, on_mark_as_read },
|
|
{ ACTION_MARK_AS_UNREAD, on_mark_as_unread },
|
|
{ ACTION_MARK_AS_STARRED, on_mark_as_starred },
|
|
{ ACTION_MARK_AS_UNSTARRED, on_mark_as_unstarred },
|
|
{ ACTION_TOGGLE_JUNK, on_mark_as_junk_toggle },
|
|
// Message viewer
|
|
{ ACTION_ZOOM, on_zoom, "s" },
|
|
};
|
|
|
|
// Handy leaflet children names
|
|
private const string INNER_LEAFLET = "inner_leaflet";
|
|
private const string FOLDER_LIST = "folder_list";
|
|
private const string CONVERSATION_LIST = "conversation_list";
|
|
private const string CONVERSATION_VIEWER = "conversation_viewer";
|
|
|
|
private const int STATUS_BAR_HEIGHT = 18;
|
|
|
|
private const int UPDATE_UI_INTERVAL = 60;
|
|
|
|
private const int MIN_CONVERSATION_COUNT = 50;
|
|
|
|
static construct {
|
|
// Set up default keybindings
|
|
unowned Gtk.BindingSet bindings = Gtk.BindingSet.by_class(
|
|
(ObjectClass) typeof(MainWindow).class_ref()
|
|
);
|
|
|
|
//
|
|
// Replying & forwarding
|
|
Gtk.BindingEntry.add_signal(
|
|
bindings,
|
|
Gdk.Key.R, CONTROL_MASK,
|
|
"reply-conversation-sender", 0
|
|
);
|
|
Gtk.BindingEntry.add_signal(
|
|
bindings,
|
|
Gdk.Key.R, CONTROL_MASK | SHIFT_MASK,
|
|
"reply-conversation-all", 0
|
|
);
|
|
Gtk.BindingEntry.add_signal(
|
|
bindings,
|
|
Gdk.Key.L, CONTROL_MASK,
|
|
"forward-conversation", 0
|
|
);
|
|
|
|
// Marking actions
|
|
//
|
|
// Unread is the primary action, so it doesn't get the <Shift>
|
|
// modifier
|
|
Gtk.BindingEntry.add_signal(
|
|
bindings,
|
|
Gdk.Key.U, CONTROL_MASK,
|
|
"mark-conversations-read", 1, typeof(bool), true
|
|
);
|
|
Gtk.BindingEntry.add_signal(
|
|
bindings,
|
|
Gdk.Key.U, CONTROL_MASK | SHIFT_MASK,
|
|
"mark-conversations-read", 1, typeof(bool), false
|
|
);
|
|
// Ephy uses Ctrl+D for bookmarking
|
|
Gtk.BindingEntry.add_signal(
|
|
bindings,
|
|
Gdk.Key.D, CONTROL_MASK,
|
|
"mark-conversations-starred", 1, typeof(bool), true
|
|
);
|
|
Gtk.BindingEntry.add_signal(
|
|
bindings,
|
|
Gdk.Key.D, CONTROL_MASK | SHIFT_MASK,
|
|
"mark-conversations-starred", 1, typeof(bool), false
|
|
);
|
|
|
|
//
|
|
// Moving & labelling
|
|
Gtk.BindingEntry.add_signal(
|
|
bindings,
|
|
Gdk.Key.B, CONTROL_MASK,
|
|
"show-copy-menu", 0
|
|
);
|
|
Gtk.BindingEntry.add_signal(
|
|
bindings,
|
|
Gdk.Key.M, CONTROL_MASK,
|
|
"show-move-menu", 0
|
|
);
|
|
Gtk.BindingEntry.add_signal(
|
|
bindings,
|
|
Gdk.Key.K, CONTROL_MASK,
|
|
"archive-conversations", 0
|
|
);
|
|
Gtk.BindingEntry.add_signal(
|
|
bindings,
|
|
Gdk.Key.J, CONTROL_MASK,
|
|
"junk-conversations", 0
|
|
);
|
|
// Many ways to trash
|
|
Gtk.BindingEntry.add_signal(
|
|
bindings,
|
|
Gdk.Key.BackSpace, 0,
|
|
"trash-conversations", 0
|
|
);
|
|
Gtk.BindingEntry.add_signal(
|
|
bindings,
|
|
Gdk.Key.Delete, 0,
|
|
"trash-conversations", 0
|
|
);
|
|
Gtk.BindingEntry.add_signal(
|
|
bindings,
|
|
Gdk.Key.KP_Delete, 0,
|
|
"trash-conversations", 0
|
|
);
|
|
// Many ways to delete
|
|
Gtk.BindingEntry.add_signal(
|
|
bindings,
|
|
Gdk.Key.BackSpace, SHIFT_MASK,
|
|
"delete-conversations", 0
|
|
);
|
|
Gtk.BindingEntry.add_signal(
|
|
bindings,
|
|
Gdk.Key.Delete, SHIFT_MASK,
|
|
"delete-conversations", 0
|
|
);
|
|
Gtk.BindingEntry.add_signal(
|
|
bindings,
|
|
Gdk.Key.KP_Delete, SHIFT_MASK,
|
|
"delete-conversations", 0
|
|
);
|
|
|
|
//
|
|
// Find & search
|
|
Gtk.BindingEntry.add_signal(
|
|
bindings,
|
|
Gdk.Key.F, CONTROL_MASK,
|
|
"find", 0
|
|
);
|
|
Gtk.BindingEntry.add_signal(
|
|
bindings,
|
|
Gdk.Key.S, CONTROL_MASK,
|
|
"search", 0
|
|
);
|
|
|
|
//
|
|
// Navigation
|
|
Gtk.BindingEntry.add_signal(
|
|
bindings,
|
|
Gdk.Key.Left, MOD1_MASK,
|
|
"navigate", 1,
|
|
typeof(Gtk.ScrollType), Gtk.ScrollType.PAGE_LEFT
|
|
);
|
|
Gtk.BindingEntry.add_signal(
|
|
bindings,
|
|
Gdk.Key.Back, 0,
|
|
"navigate", 1,
|
|
typeof(Gtk.ScrollType), Gtk.ScrollType.PAGE_LEFT
|
|
);
|
|
Gtk.BindingEntry.add_signal(
|
|
bindings,
|
|
Gdk.Key.Right, MOD1_MASK,
|
|
"navigate", 1,
|
|
typeof(Gtk.ScrollType), Gtk.ScrollType.PAGE_RIGHT
|
|
);
|
|
Gtk.BindingEntry.add_signal(
|
|
bindings,
|
|
Gdk.Key.Forward, 0,
|
|
"navigate", 1,
|
|
typeof(Gtk.ScrollType), Gtk.ScrollType.PAGE_RIGHT
|
|
);
|
|
Gtk.BindingEntry.add_signal(
|
|
bindings,
|
|
Gdk.Key.comma, CONTROL_MASK,
|
|
"navigate", 1,
|
|
typeof(Gtk.ScrollType), Gtk.ScrollType.STEP_UP
|
|
);
|
|
Gtk.BindingEntry.add_signal(
|
|
bindings,
|
|
Gdk.Key.period, CONTROL_MASK,
|
|
"navigate", 1,
|
|
typeof(Gtk.ScrollType), Gtk.ScrollType.STEP_DOWN
|
|
);
|
|
}
|
|
|
|
|
|
public static void add_accelerators(Client owner) {
|
|
for (int i = 1; i <= 9; i++) {
|
|
owner.add_window_accelerators(
|
|
ACTION_SELECT_INBOX+("(%d)".printf(i - 1)), { "<ALT>%d".printf(i) }
|
|
);
|
|
}
|
|
|
|
// Zoom
|
|
owner.add_window_accelerators(
|
|
ACTION_ZOOM+("('in')"), { "<Ctrl>equal", "<Ctrl>plus" }
|
|
);
|
|
owner.add_window_accelerators(
|
|
ACTION_ZOOM+("('out')"), { "<Ctrl>minus" }
|
|
);
|
|
owner.add_window_accelerators(
|
|
ACTION_ZOOM+("('normal')"), { "<Ctrl>0" }
|
|
);
|
|
}
|
|
|
|
|
|
private enum ConversationCount { NONE, SINGLE, MULTIPLE; }
|
|
|
|
|
|
/** Returns the window's associated client application instance. */
|
|
public new Client application {
|
|
get { return (Client) base.get_application(); }
|
|
set { base.set_application(value); }
|
|
}
|
|
|
|
/** Currently selected account, null if none selected */
|
|
public Geary.Account? selected_account { get; private set; default = null; }
|
|
|
|
/** Currently selected folder, null if none selected */
|
|
public Geary.Folder? selected_folder { get; private set; default = null; }
|
|
|
|
/** Conversations for the current folder, null if none selected */
|
|
public Geary.App.ConversationMonitor? conversations {
|
|
get; private set; default = null;
|
|
}
|
|
|
|
/** Specifies if the conversation list is currently displayed. */
|
|
public bool is_folder_list_shown {
|
|
get {
|
|
return (
|
|
(!this.outer_leaflet.folded ||
|
|
this.outer_leaflet.visible_child_name == INNER_LEAFLET) &&
|
|
(!this.inner_leaflet.folded ||
|
|
this.inner_leaflet.visible_child_name == FOLDER_LIST)
|
|
);
|
|
}
|
|
}
|
|
|
|
/** Specifies if the conversation list is currently displayed. */
|
|
public bool is_conversation_list_shown {
|
|
get {
|
|
return (
|
|
(!this.outer_leaflet.folded ||
|
|
this.outer_leaflet.visible_child_name == INNER_LEAFLET) &&
|
|
(!this.inner_leaflet.folded ||
|
|
this.inner_leaflet.visible_child_name == CONVERSATION_LIST)
|
|
);
|
|
}
|
|
}
|
|
|
|
/** Specifies if the conversation viewer is currently displayed. */
|
|
public bool is_conversation_viewer_shown {
|
|
get {
|
|
return (
|
|
(!this.outer_leaflet.folded ||
|
|
this.outer_leaflet.visible_child_name == CONVERSATION_VIEWER) &&
|
|
!this.has_composer
|
|
);
|
|
}
|
|
}
|
|
|
|
/** The attachment manager for this window. */
|
|
public AttachmentManager attachments { get; private set; }
|
|
|
|
/** Determines if conversations in the selected folder can be trashed. */
|
|
public bool selected_folder_supports_trash {
|
|
get {
|
|
return Controller.does_folder_support_trash(this.selected_folder);
|
|
}
|
|
}
|
|
|
|
/** Determines if a composer is currently open in this window. */
|
|
public bool has_composer {
|
|
get {
|
|
return (this.conversation_viewer.current_composer != null);
|
|
}
|
|
}
|
|
|
|
/** Specifies if the Shift key is currently being held. */
|
|
public bool is_shift_down { get; private set; default = false; }
|
|
|
|
// Used to save/load the window state between sessions.
|
|
public int window_width { get; set; }
|
|
public int window_height { get; set; }
|
|
public bool window_maximized { get; set; }
|
|
|
|
// Widget descendants
|
|
public FolderList.Tree folder_list { get; private set; default = new FolderList.Tree(); }
|
|
public Components.MainToolbar main_toolbar { get; private set; }
|
|
public SearchBar search_bar { get; private set; }
|
|
public ConversationListView conversation_list_view { get; private set; }
|
|
public ConversationViewer conversation_viewer { get; private set; }
|
|
|
|
public Components.InfoBarStack conversation_list_info_bars {
|
|
get; private set; default = new Components.InfoBarStack(PRIORITY_QUEUE);
|
|
}
|
|
|
|
public StatusBar status_bar { get; private set; default = new StatusBar(); }
|
|
|
|
private Controller controller;
|
|
|
|
private MonitoredSpinner spinner = new MonitoredSpinner();
|
|
|
|
private Gee.Set<AccountContext> accounts = new Gee.HashSet<AccountContext>();
|
|
|
|
private GLib.SimpleActionGroup edit_actions = new GLib.SimpleActionGroup();
|
|
|
|
// Determines if the conversation viewer should auto-mark messages
|
|
// on next load
|
|
private bool previous_selection_was_interactive = false;
|
|
|
|
// Caches the last non-search folder so it can be re-selected on
|
|
// the search folder closing
|
|
private Geary.Folder? previous_non_search_folder = null;
|
|
|
|
private Geary.AggregateProgressMonitor progress_monitor = new Geary.AggregateProgressMonitor();
|
|
|
|
private GLib.Cancellable action_update_cancellable = new GLib.Cancellable();
|
|
private GLib.Cancellable folder_open = new GLib.Cancellable();
|
|
|
|
private Geary.TimeoutManager update_ui_timeout;
|
|
private int64 update_ui_last = 0;
|
|
|
|
|
|
[GtkChild] private unowned Gtk.Box main_layout;
|
|
|
|
// Folds the inner leaftlet and conversation viewer
|
|
[GtkChild] private unowned Hdy.Leaflet outer_leaflet;
|
|
|
|
// Folds the folder list and the conversation list
|
|
[GtkChild] private unowned Hdy.Leaflet inner_leaflet;
|
|
|
|
[GtkChild] private unowned Gtk.Box folder_box;
|
|
[GtkChild] private unowned Gtk.ScrolledWindow folder_list_scrolled;
|
|
|
|
[GtkChild] private unowned Gtk.Box conversation_list_box;
|
|
[GtkChild] private unowned Gtk.ScrolledWindow conversation_list_scrolled;
|
|
[GtkChild] private unowned Gtk.Revealer conversation_list_actions_revealer;
|
|
[GtkChild] private unowned Components.ConversationActions conversation_list_actions;
|
|
|
|
[GtkChild] private unowned Gtk.Box conversation_viewer_box;
|
|
[GtkChild] private unowned Gtk.Revealer conversation_viewer_actions_revealer;
|
|
[GtkChild] private unowned Components.ConversationActions conversation_viewer_actions;
|
|
[GtkChild] private unowned Gtk.SizeGroup folder_size_group;
|
|
[GtkChild] private unowned Gtk.SizeGroup folder_separator_size_group;
|
|
[GtkChild] private unowned Gtk.SizeGroup conversations_size_group;
|
|
[GtkChild] private unowned Gtk.SizeGroup conversations_separator_size_group;
|
|
[GtkChild] private unowned Gtk.SizeGroup conversation_size_group;
|
|
[GtkChild] private unowned Hdy.SwipeGroup conversations_swipe_group;
|
|
[GtkChild] private unowned Hdy.SwipeGroup conversation_swipe_group;
|
|
|
|
[GtkChild] private unowned Gtk.Overlay overlay;
|
|
|
|
private Components.InfoBarStack info_bars =
|
|
new Components.InfoBarStack(SINGLE);
|
|
|
|
private Components.InfoBar offline_infobar;
|
|
|
|
private Components.InfoBar cert_problem_infobar;
|
|
|
|
private Components.InfoBar auth_problem_infobar;
|
|
|
|
private Components.ProblemReportInfoBar? service_problem_infobar = null;
|
|
|
|
/** Fired when the user requests an account status be retried. */
|
|
public signal void retry_service_problem(Geary.ClientService.Status problem);
|
|
|
|
|
|
/** Keybinding signal for replying to sender for the current conversation. */
|
|
[Signal (action=true)]
|
|
public virtual signal void reply_conversation_sender() {
|
|
activate_action(get_window_action(ACTION_REPLY_CONVERSATION));
|
|
}
|
|
|
|
/** Keybinding signal for replying to all for the current conversation. */
|
|
[Signal (action=true)]
|
|
public virtual signal void reply_conversation_all() {
|
|
activate_action(get_window_action(ACTION_REPLY_ALL_CONVERSATION));
|
|
}
|
|
|
|
/** Keybinding signal for forwarding the current conversation. */
|
|
[Signal (action=true)]
|
|
public virtual signal void forward_conversation() {
|
|
activate_action(get_window_action(ACTION_FORWARD_CONVERSATION));
|
|
}
|
|
|
|
/** Keybinding signal for marking the current selection read. */
|
|
[Signal (action=true)]
|
|
public virtual signal void mark_conversations_read(bool prefer_read) {
|
|
activate_action(
|
|
prefer_read
|
|
? get_window_action(ACTION_MARK_AS_READ)
|
|
: get_window_action(ACTION_MARK_AS_UNREAD)
|
|
);
|
|
}
|
|
|
|
/** Keybinding signal for marking the current selection starred. */
|
|
[Signal (action=true)]
|
|
public virtual signal void mark_conversations_starred(bool prefer_starred) {
|
|
activate_action(
|
|
prefer_starred
|
|
? get_window_action(ACTION_MARK_AS_STARRED)
|
|
: get_window_action(ACTION_MARK_AS_UNSTARRED)
|
|
);
|
|
}
|
|
|
|
/** Keybinding signal for showing the copy/label menu. */
|
|
[Signal (action=true)]
|
|
public virtual signal void show_copy_menu() {
|
|
activate_action(get_window_action(ACTION_SHOW_COPY_MENU));
|
|
}
|
|
|
|
/** Keybinding signal for showing the move menu. */
|
|
[Signal (action=true)]
|
|
public virtual signal void show_move_menu() {
|
|
activate_action(get_window_action(ACTION_SHOW_MOVE_MENU));
|
|
}
|
|
|
|
/** Keybinding signal for archiving the current selection. */
|
|
[Signal (action=true)]
|
|
public virtual signal void archive_conversations() {
|
|
activate_action(get_window_action(ACTION_ARCHIVE_CONVERSATION));
|
|
}
|
|
|
|
/** Keybinding signal for junking the current selection. */
|
|
[Signal (action=true)]
|
|
public virtual signal void junk_conversations() {
|
|
activate_action(get_window_action(ACTION_TOGGLE_JUNK));
|
|
}
|
|
|
|
/** Keybinding signal for trashing the current selection. */
|
|
[Signal (action=true)]
|
|
public virtual signal void trash_conversations() {
|
|
// XXX the Shift+BackSpace combo above doesn't seem to work
|
|
// for delete, so double-check here.
|
|
activate_action(
|
|
!this.is_shift_down
|
|
? get_window_action(ACTION_TRASH_CONVERSATION)
|
|
: get_window_action(ACTION_DELETE_CONVERSATION)
|
|
);
|
|
}
|
|
|
|
/** Keybinding signal for deleting the current selection. */
|
|
[Signal (action=true)]
|
|
public virtual signal void delete_conversations() {
|
|
activate_action(get_window_action(ACTION_DELETE_CONVERSATION));
|
|
}
|
|
|
|
/** Keybinding signal for activating conversation search. */
|
|
[Signal (action=true)]
|
|
public virtual signal void search() {
|
|
activate_action(get_window_action(ACTION_SEARCH));
|
|
}
|
|
|
|
/** Keybinding signal for activating in-conversation find. */
|
|
[Signal (action=true)]
|
|
public virtual signal void find() {
|
|
activate_action(get_window_action(ACTION_FIND_IN_CONVERSATION));
|
|
}
|
|
|
|
/** Keybinding signal for shifting the keyboard focus. */
|
|
[Signal (action=true)]
|
|
public virtual signal void navigate(Gtk.ScrollType type) {
|
|
switch (type) {
|
|
case Gtk.ScrollType.PAGE_LEFT:
|
|
if (get_direction() != RTL) {
|
|
focus_previous_pane();
|
|
} else {
|
|
focus_next_pane();
|
|
}
|
|
break;
|
|
case Gtk.ScrollType.PAGE_RIGHT:
|
|
if (get_direction() != RTL) {
|
|
focus_next_pane();
|
|
} else {
|
|
focus_previous_pane();
|
|
}
|
|
break;
|
|
case Gtk.ScrollType.STEP_UP:
|
|
activate_action(get_window_action(ACTION_CONVERSATION_UP));
|
|
break;
|
|
case Gtk.ScrollType.STEP_DOWN:
|
|
activate_action(get_window_action(ACTION_CONVERSATION_DOWN));
|
|
break;
|
|
default:
|
|
this.get_window().beep();
|
|
break;
|
|
}
|
|
}
|
|
|
|
|
|
internal MainWindow(Client application) {
|
|
Object(
|
|
application: application,
|
|
show_menubar: false
|
|
);
|
|
base_ref();
|
|
|
|
// Keep a ref on the current controller so that even if the
|
|
// application has been shut down, any async tasks that are
|
|
// running (and presumably being cancelled) can still complete
|
|
// without a warning.
|
|
this.controller = application.controller;
|
|
|
|
// Create a new group for the window so attachment file
|
|
// choosers (save attachments & composer attach files) do not
|
|
// block other main windows or composers.
|
|
var group = new Gtk.WindowGroup();
|
|
group.add_window(this);
|
|
|
|
load_config(application.config);
|
|
restore_saved_window_state();
|
|
|
|
if (_PROFILE != Client.PROFILE_RELEASE) {
|
|
this.get_style_context().add_class("devel");
|
|
}
|
|
|
|
this.info_bars.shadow_type = IN;
|
|
this.conversation_list_info_bars.shadow_type = IN;
|
|
|
|
// 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);
|
|
|
|
this.focus_in_event.connect((w, e) => {
|
|
application.controller.window_focus_in();
|
|
return false;
|
|
});
|
|
this.focus_out_event.connect((w, e) => {
|
|
application.controller.window_focus_out();
|
|
return false;
|
|
});
|
|
|
|
setup_layout(application.config);
|
|
this.folder_box.pack_start(status_bar, false, false);
|
|
|
|
update_command_actions();
|
|
update_conversation_actions(NONE);
|
|
|
|
this.attachments = new AttachmentManager(this);
|
|
|
|
this.update_ui_timeout = new Geary.TimeoutManager.seconds(
|
|
UPDATE_UI_INTERVAL, on_update_ui_timeout
|
|
);
|
|
this.update_ui_timeout.repetition = FOREVER;
|
|
|
|
// Add future and existing accounts to the main window
|
|
this.controller.account_available.connect(
|
|
on_account_available
|
|
);
|
|
this.controller.account_unavailable.connect(
|
|
on_account_unavailable
|
|
);
|
|
foreach (AccountContext context in
|
|
this.controller.get_account_contexts()) {
|
|
add_account(context);
|
|
}
|
|
|
|
this.offline_infobar = new Components.InfoBar(
|
|
// Translators: An info bar status label
|
|
_("Working offline"),
|
|
// Translators: An info bar description label
|
|
_("You will not be able to send or receive email until re-connected.")
|
|
);
|
|
this.offline_infobar.show_close_button = true;
|
|
this.offline_infobar.response.connect(on_offline_infobar_response);
|
|
|
|
this.auth_problem_infobar = new Components.InfoBar(
|
|
// Translators: An info bar status label
|
|
_("Login problem"),
|
|
// Translators: An info bar description label
|
|
_("An account has reported an incorrect login or password.")
|
|
);
|
|
// Translators: An info bar button label
|
|
var auth_retry = new Gtk.Button.with_label(_("Login"));
|
|
auth_retry.show();
|
|
// Translators: An info bar button tool-tip
|
|
auth_retry.tooltip_text = _(
|
|
"Retry login, you will be prompted for your password"
|
|
);
|
|
auth_retry.clicked.connect(on_auth_problem_retry);
|
|
this.auth_problem_infobar.get_action_area().add(auth_retry);
|
|
|
|
this.cert_problem_infobar = new Components.InfoBar(
|
|
// Translators: An info bar status label
|
|
_("Security problem"),
|
|
// Translators: An info bar description label
|
|
_("An account has reported an untrusted server.")
|
|
);
|
|
// Translators: An info bar button label
|
|
var cert_retry = new Gtk.Button.with_label(_("Check"));
|
|
cert_retry.show();
|
|
// Translators: An info bar button tool-tip
|
|
cert_retry.tooltip_text = _(
|
|
"Check the security details for the connection"
|
|
);
|
|
cert_retry.clicked.connect(on_cert_problem_retry);
|
|
this.cert_problem_infobar.get_action_area().add(cert_retry);
|
|
|
|
this.conversation_list_view.grab_focus();
|
|
}
|
|
|
|
~MainWindow() {
|
|
base_unref();
|
|
}
|
|
|
|
/** {@inheritDoc} */
|
|
public override void destroy() {
|
|
if (this.application != null) {
|
|
this.controller.account_available.disconnect(
|
|
on_account_available
|
|
);
|
|
this.controller.account_unavailable.disconnect(
|
|
on_account_unavailable
|
|
);
|
|
}
|
|
this.update_ui_timeout.reset();
|
|
base.destroy();
|
|
}
|
|
|
|
/** Updates the window's title and headerbar titles. */
|
|
public void update_title() {
|
|
AccountContext? account = get_selected_account_context();
|
|
FolderContext? folder = (
|
|
account != null && this.selected_folder != null
|
|
? account.get_folder(this.selected_folder)
|
|
: null
|
|
);
|
|
string title = _("Geary");
|
|
string? account_name = null;
|
|
string? folder_name = null;
|
|
if (account != null && folder != null) {
|
|
account_name = account.account.information.display_name;
|
|
folder_name = folder.display_name;
|
|
/// Translators: Main window title, first string
|
|
/// substitution being the currently selected folder name,
|
|
/// the second being the selected account name.
|
|
title = _("%s — %s").printf(folder_name, account_name);
|
|
}
|
|
this.title = title;
|
|
this.main_toolbar.account = account_name ?? "";
|
|
this.main_toolbar.folder = folder_name?? "";
|
|
}
|
|
|
|
/** Updates the window's account status info bars. */
|
|
public void update_account_status(Geary.Account.Status status,
|
|
bool has_auth_error,
|
|
bool has_cert_error,
|
|
Geary.Account? problem_source) {
|
|
// Only ever show one info bar at a time. Offline is primary
|
|
// since nothing else can happen when offline. Service
|
|
// problems are secondary since auth and cert problems can't
|
|
// be resolved when the service isn't talking to the
|
|
// server. Cert problems are tertiary since you can't auth if
|
|
// you can't connect.
|
|
if (!status.is_online()) {
|
|
this.info_bars.add(this.offline_infobar);
|
|
} else if (status.has_service_problem()) {
|
|
Geary.ClientService? service = (
|
|
problem_source.incoming.last_error != null
|
|
? problem_source.incoming
|
|
: problem_source.outgoing
|
|
);
|
|
this.service_problem_infobar = new Components.ProblemReportInfoBar(
|
|
new Geary.ServiceProblemReport(
|
|
problem_source.information,
|
|
service.configuration,
|
|
service.last_error.thrown
|
|
)
|
|
);
|
|
this.service_problem_infobar.retry.connect(on_service_problem_retry);
|
|
this.info_bars.add(this.service_problem_infobar);
|
|
} else if (has_cert_error) {
|
|
this.info_bars.add(this.cert_problem_infobar);
|
|
} else if (has_auth_error) {
|
|
this.info_bars.add(this.auth_problem_infobar);
|
|
} else {
|
|
this.info_bars.remove_all();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Selects and open the given folder.
|
|
*
|
|
* If is_interactive is true, the selection is treated as being
|
|
* caused directly by human request (e.g. clicking on a folder in
|
|
* the folder list), as opposed to some side effect.
|
|
*/
|
|
public async void select_folder(Geary.Folder? to_select,
|
|
bool is_interactive,
|
|
bool inhibit_autoselect = false) {
|
|
if (this.selected_folder != to_select) {
|
|
// Cancel any existing folder loading
|
|
this.folder_open.cancel();
|
|
var cancellable = this.folder_open = new GLib.Cancellable();
|
|
|
|
// Dispose of all existing objects for the currently
|
|
// selected model.
|
|
|
|
if (this.selected_folder != null) {
|
|
this.conversation_actions.copy_folder_menu.enable_disable_folder(
|
|
this.selected_folder, true
|
|
);
|
|
this.conversation_actions.move_folder_menu.enable_disable_folder(
|
|
this.selected_folder, true
|
|
);
|
|
|
|
this.progress_monitor.remove(this.selected_folder.opening_monitor);
|
|
this.selected_folder.properties.notify.disconnect(update_headerbar);
|
|
this.selected_folder = null;
|
|
}
|
|
if (this.conversations != null) {
|
|
this.progress_monitor.remove(this.conversations.progress_monitor);
|
|
close_conversation_monitor(this.conversations);
|
|
this.conversations = null;
|
|
}
|
|
var conversations_model = this.conversation_list_view.get_model();
|
|
if (conversations_model != null) {
|
|
this.progress_monitor.remove(conversations_model.preview_monitor);
|
|
this.conversation_list_view.set_model(null);
|
|
}
|
|
|
|
this.conversation_list_info_bars.remove_all();
|
|
|
|
// With everything disposed of, update existing window
|
|
// state
|
|
|
|
select_account(to_select != null ? to_select.account : null);
|
|
this.selected_folder = to_select;
|
|
|
|
// Ensure that the folder is selected in the UI if
|
|
// this was called by something other than the
|
|
// selection changed callback. That will check to
|
|
// ensure that we're not setting it again.
|
|
if (to_select != null) {
|
|
// Prefer the inboxes branch if it is a thing, but
|
|
// only for non-interactive calls
|
|
if (is_interactive ||
|
|
(to_select.used_as != INBOX ||
|
|
!this.folder_list.select_inbox(to_select.account))) {
|
|
this.folder_list.select_folder(to_select);
|
|
}
|
|
} else {
|
|
this.folder_list.deselect_folder();
|
|
}
|
|
|
|
update_title();
|
|
update_conversation_actions(NONE);
|
|
update_trash_action();
|
|
|
|
this.conversation_viewer.show_loading();
|
|
this.previous_selection_was_interactive = is_interactive;
|
|
|
|
debug("Folder selected: %s",
|
|
(to_select != null) ? to_select.to_string() : "(null)");
|
|
|
|
// Finally, hook up the new folder if any and start
|
|
// loading conversations.
|
|
|
|
if (to_select != null) {
|
|
this.progress_monitor.add(to_select.opening_monitor);
|
|
to_select.properties.notify.connect(update_headerbar);
|
|
|
|
this.conversations = new Geary.App.ConversationMonitor(
|
|
to_select,
|
|
// Include fields for the conversation viewer as well so
|
|
// conversations can be displayed without having to go
|
|
// back to the db
|
|
ConversationListStore.REQUIRED_FIELDS |
|
|
ConversationListBox.REQUIRED_FIELDS |
|
|
ConversationEmail.REQUIRED_FOR_CONSTRUCT,
|
|
MIN_CONVERSATION_COUNT
|
|
);
|
|
this.progress_monitor.add(this.conversations.progress_monitor);
|
|
|
|
conversations_model = new ConversationListStore(
|
|
this.conversations, this.application.config
|
|
|
|
);
|
|
this.progress_monitor.add(conversations_model.preview_monitor);
|
|
if (inhibit_autoselect) {
|
|
this.conversation_list_view.inhibit_next_autoselect();
|
|
}
|
|
this.conversation_list_view.set_model(conversations_model);
|
|
|
|
// disable copy/move to the new folder
|
|
this.conversation_actions.copy_folder_menu.enable_disable_folder(
|
|
to_select, false
|
|
);
|
|
this.conversation_actions.move_folder_menu.enable_disable_folder(
|
|
to_select, false
|
|
);
|
|
|
|
yield open_conversation_monitor(this.conversations, cancellable);
|
|
yield this.controller.process_pending_composers();
|
|
}
|
|
}
|
|
|
|
update_headerbar();
|
|
}
|
|
|
|
/** Selects the given account, folder and conversations. */
|
|
public async void show_conversations(Geary.Folder location,
|
|
Gee.Collection<Geary.App.Conversation> to_show,
|
|
bool is_interactive) {
|
|
bool inhibit_autoselect = (location != this.selected_folder);
|
|
yield select_folder(location, is_interactive, inhibit_autoselect);
|
|
// The folder may have changed again by the type the async
|
|
// call returns, so only continue if still current
|
|
if (this.selected_folder == location) {
|
|
// Since conversation ids don't persist between
|
|
// conversation monitor instances, need to load
|
|
// conversations based on their messages.
|
|
var latest_email = new Gee.HashSet<Geary.EmailIdentifier>();
|
|
foreach (var stale in to_show) {
|
|
Geary.Email? first = stale.get_latest_recv_email(IN_FOLDER);
|
|
if (first != null) {
|
|
latest_email.add(first.id);
|
|
}
|
|
}
|
|
var loaded = yield load_conversations_for_email(
|
|
location, latest_email
|
|
);
|
|
if (!loaded.is_empty) {
|
|
yield select_conversations(
|
|
loaded,
|
|
Gee.Collection.empty<Geary.EmailIdentifier>(),
|
|
is_interactive
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
/** Selects the given account, folder and email. */
|
|
public async void show_email(Geary.Folder location,
|
|
Gee.Collection<Geary.EmailIdentifier> to_show,
|
|
bool is_interactive) {
|
|
bool inhibit_autoselect = (location != this.selected_folder);
|
|
yield select_folder(location, is_interactive, inhibit_autoselect);
|
|
// The folder may have changed again by the type the async
|
|
// call returns, so only continue if still current
|
|
if (this.selected_folder == location) {
|
|
var loaded = yield load_conversations_for_email(location, to_show);
|
|
|
|
if (loaded.size == 1) {
|
|
// A single conversation was loaded, so ensure we
|
|
// scroll to the email in the conversation.
|
|
Geary.App.Conversation? target = Geary.Collection.first(loaded);
|
|
ConversationListBox? current_list =
|
|
this.conversation_viewer.current_list;
|
|
if (current_list != null &&
|
|
current_list.conversation == target) {
|
|
// The target conversation is already loaded, just
|
|
// scroll to the messages.
|
|
//
|
|
// XXX this is actually racy, since the view may
|
|
// still be in the middle of loading the messages
|
|
// obtained from the conversation monitor when
|
|
// this call is made.
|
|
current_list.scroll_to_messages(to_show);
|
|
} else {
|
|
// The target conversation is not loaded, select
|
|
// it and scroll to the messages.
|
|
yield select_conversations(loaded, to_show, is_interactive);
|
|
}
|
|
} else if (!loaded.is_empty) {
|
|
// Multiple conversations found, just select those
|
|
yield select_conversations(
|
|
loaded,
|
|
Gee.Collection.empty<Geary.EmailIdentifier>(),
|
|
is_interactive
|
|
);
|
|
} else {
|
|
}
|
|
}
|
|
}
|
|
|
|
/** Shows the appopriate window menu, if any. */
|
|
public void show_window_menu() {
|
|
if (this.outer_leaflet.folded) {
|
|
this.outer_leaflet.navigate(Hdy.NavigationDirection.BACK);
|
|
}
|
|
if (this.inner_leaflet.folded) {
|
|
this.inner_leaflet.navigate(Hdy.NavigationDirection.BACK);
|
|
}
|
|
this.main_toolbar.show_main_menu();
|
|
}
|
|
|
|
/** Displays and focuses the search bar for the window. */
|
|
public void show_search_bar(string? text = null) {
|
|
this.search_bar.grab_focus();
|
|
if (text != null) {
|
|
this.search_bar.entry.text = text;
|
|
}
|
|
}
|
|
|
|
/** Displays an infobar in the window. */
|
|
public void show_info_bar(Components.InfoBar info_bar) {
|
|
if (!this.info_bars.has_current) {
|
|
this.info_bars.add(info_bar);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Displays a composer in the window if possible, else in a new window.
|
|
*/
|
|
internal void show_composer(Composer.Widget composer) {
|
|
if (this.has_composer) {
|
|
composer.detach(this.application);
|
|
} else {
|
|
// See if the currently displayed conversation contains
|
|
// any of the composer's referred emails (preferring the
|
|
// latest), and if so add it inline, otherwise add it full
|
|
// paned.
|
|
Geary.Email? latest_referred = null;
|
|
if (this.conversation_viewer.current_list != null) {
|
|
Gee.Collection<Geary.EmailIdentifier>? referrants =
|
|
composer.get_referred_ids();
|
|
Geary.App.Conversation selected =
|
|
this.conversation_viewer.current_list.conversation;
|
|
latest_referred = selected.get_emails(
|
|
RECV_DATE_DESCENDING
|
|
).first_match(
|
|
(email) => email.id in referrants
|
|
);
|
|
}
|
|
|
|
if (latest_referred != null) {
|
|
this.conversation_viewer.do_compose_embedded(
|
|
composer, latest_referred
|
|
);
|
|
} else {
|
|
this.conversation_viewer.do_compose(composer);
|
|
}
|
|
// Show the correct leaflet
|
|
this.outer_leaflet.set_visible_child_name(CONVERSATION_VIEWER);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Closes any open composers, after prompting the user if requested.
|
|
*
|
|
* Returns true if none were open or the user approved closing
|
|
* them.
|
|
*/
|
|
internal bool close_composer(bool should_prompt, bool is_shutdown = false) {
|
|
bool closed = true;
|
|
Composer.Widget? composer = this.conversation_viewer.current_composer;
|
|
if (composer != null &&
|
|
composer.conditional_close(should_prompt, is_shutdown) == CANCELLED) {
|
|
closed = false;
|
|
}
|
|
return closed;
|
|
}
|
|
|
|
internal void start_search(string query_text, bool is_interactive) {
|
|
var context = get_selected_account_context();
|
|
if (context != null) {
|
|
// If the current folder is not the search folder, save it
|
|
// so it can be re-selected later when search is closed
|
|
if (this.previous_non_search_folder == null &&
|
|
this.selected_folder != null &&
|
|
this.selected_folder.used_as != SEARCH) {
|
|
this.previous_non_search_folder = this.selected_folder;
|
|
}
|
|
|
|
try {
|
|
var expr_factory = new Util.Email.SearchExpressionFactory(
|
|
this.application.config.get_search_strategy(),
|
|
context.account.information
|
|
);
|
|
var query = context.account.new_search_query(
|
|
expr_factory.parse_query(query_text),
|
|
query_text
|
|
);
|
|
this.folder_list.set_search(
|
|
this.application.engine, context.search
|
|
);
|
|
context.search.update_query(query);
|
|
} catch (GLib.Error error) {
|
|
handle_error(context.account.information, error);
|
|
}
|
|
}
|
|
}
|
|
|
|
internal void stop_search(bool is_interactive) {
|
|
// If the search folder is current selected, deselect and
|
|
// re-select any previously selected folder
|
|
if (this.selected_folder == null ||
|
|
this.selected_folder.used_as == SEARCH) {
|
|
var to_select = this.previous_non_search_folder;
|
|
if (to_select == null) {
|
|
var account = get_selected_account_context();
|
|
if (account != null) {
|
|
to_select = account.inbox;
|
|
}
|
|
|
|
}
|
|
if (to_select != null) {
|
|
this.select_folder.begin(
|
|
this.previous_non_search_folder, is_interactive
|
|
);
|
|
} else {
|
|
select_first_inbox(is_interactive);
|
|
}
|
|
}
|
|
this.folder_list.remove_search();
|
|
|
|
foreach (var context in this.controller.get_account_contexts()) {
|
|
context.search.clear_query();
|
|
}
|
|
}
|
|
|
|
internal bool select_first_inbox(bool is_interactive) {
|
|
bool success = false;
|
|
Geary.Folder? inbox = get_first_inbox();
|
|
if (inbox != null) {
|
|
this.select_folder.begin(inbox, is_interactive);
|
|
success = true;
|
|
}
|
|
return success;
|
|
}
|
|
|
|
private void add_account(AccountContext to_add) {
|
|
if (!this.accounts.contains(to_add)) {
|
|
this.folder_list.set_user_folders_root_name(
|
|
to_add.account, _("Labels")
|
|
);
|
|
|
|
this.progress_monitor.add(to_add.account.background_progress);
|
|
Geary.Smtp.ClientService? smtp = (
|
|
to_add.account.outgoing as Geary.Smtp.ClientService
|
|
);
|
|
if (smtp != null) {
|
|
this.progress_monitor.add(smtp.sending_monitor);
|
|
}
|
|
|
|
to_add.folders_available.connect(on_folders_available);
|
|
to_add.folders_unavailable.connect(on_folders_unavailable);
|
|
to_add.commands.executed.connect(on_command_execute);
|
|
to_add.commands.undone.connect(on_command_undo);
|
|
to_add.commands.redone.connect(on_command_redo);
|
|
|
|
// Sort the folders so FolderListTree adds them all
|
|
// correctly
|
|
var added = new Gee.TreeSet<FolderContext>();
|
|
added.add_all(to_add.get_folders());
|
|
|
|
add_folders(added);
|
|
this.accounts.add(to_add);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Removes the given account from the main window.
|
|
*
|
|
* If `to_select` is not null, the given folder will be selected,
|
|
* otherwise no folder will be.
|
|
*/
|
|
private async void remove_account(AccountContext to_remove,
|
|
Geary.Folder? to_select) {
|
|
if (this.accounts.contains(to_remove)) {
|
|
// Explicitly unset the selected folder if it belongs to the
|
|
// account so we block until it's gone. This also clears the
|
|
// previous search folder, so it won't try to re-load that
|
|
// that when the account is gone.
|
|
if (this.selected_folder != null &&
|
|
this.selected_folder.account == to_remove.account) {
|
|
bool is_account_search_active = (
|
|
this.selected_folder.used_as == SEARCH
|
|
);
|
|
|
|
yield select_folder(to_select, false);
|
|
|
|
if (is_account_search_active) {
|
|
this.search_bar.entry.text = "";
|
|
this.search_bar.search_mode_enabled = false;
|
|
}
|
|
}
|
|
|
|
to_remove.commands.executed.disconnect(on_command_execute);
|
|
to_remove.commands.undone.disconnect(on_command_undo);
|
|
to_remove.commands.redone.disconnect(on_command_redo);
|
|
to_remove.folders_available.disconnect(on_folders_available);
|
|
to_remove.folders_available.disconnect(on_folders_unavailable);
|
|
|
|
this.progress_monitor.remove(to_remove.account.background_progress);
|
|
Geary.Smtp.ClientService? smtp = (
|
|
to_remove.account.outgoing as Geary.Smtp.ClientService
|
|
);
|
|
if (smtp != null) {
|
|
this.progress_monitor.remove(smtp.sending_monitor);
|
|
}
|
|
|
|
// Finally, remove the account and its folders
|
|
remove_folders(to_remove.get_folders(), false);
|
|
this.folder_list.remove_account(to_remove.account);
|
|
this.accounts.remove(to_remove);
|
|
}
|
|
}
|
|
|
|
/** Adds a folder to the window. */
|
|
private void add_folders(Gee.Collection<FolderContext> to_add) {
|
|
foreach (var context in to_add) {
|
|
this.folder_list.add_folder(context);
|
|
if (context.folder.account == this.selected_account) {
|
|
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);
|
|
}
|
|
}
|
|
|
|
/** Removes a folder from the window. */
|
|
private void remove_folders(Gee.Collection<FolderContext> to_remove,
|
|
bool update_selecton) {
|
|
foreach (var context in to_remove) {
|
|
Geary.Folder folder = context.folder;
|
|
if (this.selected_folder == folder) {
|
|
var account = get_selected_account_context();
|
|
if (account != null) {
|
|
this.select_folder.begin(account.inbox, true);
|
|
}
|
|
}
|
|
|
|
folder.use_changed.disconnect(on_use_changed);
|
|
if (folder.account == this.selected_account) {
|
|
this.conversation_actions.copy_folder_menu.remove_folder(folder);
|
|
this.conversation_actions.move_folder_menu.remove_folder(folder);
|
|
}
|
|
this.folder_list.remove_folder(context);
|
|
}
|
|
}
|
|
|
|
private AccountContext? get_selected_account_context() {
|
|
AccountContext? context = null;
|
|
if (this.selected_account != null) {
|
|
context = this.controller.get_context_for_account(
|
|
this.selected_account.information
|
|
);
|
|
}
|
|
return context;
|
|
}
|
|
|
|
private Geary.Folder? get_first_inbox() {
|
|
Geary.Folder? inbox = null;
|
|
try {
|
|
Geary.Account? first = Geary.Collection.first<Geary.Account>(
|
|
this.application.engine.get_accounts()
|
|
);
|
|
if (first != null) {
|
|
inbox = first.get_special_folder(INBOX);
|
|
}
|
|
} catch (GLib.Error error) {
|
|
debug("Error getting inbox for first account");
|
|
}
|
|
return inbox;
|
|
}
|
|
|
|
private void load_config(Configuration config) {
|
|
// 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.WINDOW_WIDTH_KEY, this, "window-width");
|
|
config.bind(Configuration.WINDOW_HEIGHT_KEY, this, "window-height");
|
|
config.bind(Configuration.WINDOW_MAXIMIZE_KEY, this, "window-maximized");
|
|
}
|
|
|
|
private void restore_saved_window_state() {
|
|
Gdk.Display? display = Gdk.Display.get_default();
|
|
if (display != null) {
|
|
Gdk.Monitor? monitor = display.get_primary_monitor();
|
|
if (monitor == null) {
|
|
monitor = display.get_monitor_at_point(1, 1);
|
|
}
|
|
if (monitor != null &&
|
|
this.window_width <= monitor.geometry.width &&
|
|
this.window_height <= monitor.geometry.height) {
|
|
set_default_size(this.window_width, this.window_height);
|
|
}
|
|
}
|
|
this.window_position = Gtk.WindowPosition.CENTER;
|
|
if (this.window_maximized) {
|
|
maximize();
|
|
}
|
|
}
|
|
|
|
// Called on [un]maximize and possibly others. Save maximized state
|
|
// for the next start.
|
|
public override bool window_state_event(Gdk.EventWindowState event) {
|
|
if ((event.new_window_state & Gdk.WindowState.WITHDRAWN) == 0) {
|
|
bool maximized = (
|
|
(event.new_window_state & Gdk.WindowState.MAXIMIZED) != 0
|
|
);
|
|
if (this.window_maximized != maximized) {
|
|
this.window_maximized = maximized;
|
|
}
|
|
}
|
|
return base.window_state_event(event);
|
|
}
|
|
|
|
// Called on window resize. Save window size for the next start.
|
|
public override void size_allocate(Gtk.Allocation allocation) {
|
|
base.size_allocate(allocation);
|
|
|
|
if (!this.window_maximized) {
|
|
Gdk.Display? display = get_display();
|
|
Gdk.Window? window = get_window();
|
|
if (display != null && window != null) {
|
|
Gdk.Monitor monitor = display.get_monitor_at_window(window);
|
|
|
|
// Get the size via ::get_size instead of the
|
|
// allocation so that the window isn't ever-expanding.
|
|
int width = 0;
|
|
int height = 0;
|
|
get_size(out width, out height);
|
|
|
|
// Only store if the values have changed and are
|
|
// reasonable-looking.
|
|
if (this.window_width != width &&
|
|
width > 0 && width <= monitor.geometry.width) {
|
|
this.window_width = width;
|
|
}
|
|
if (this.window_height != height &&
|
|
height > 0 && height <= monitor.geometry.height) {
|
|
this.window_height = height;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public void add_notification(Components.InAppNotification notification) {
|
|
this.overlay.add_overlay(notification);
|
|
notification.show();
|
|
}
|
|
|
|
private void setup_layout(Configuration config) {
|
|
this.notify["has-toplevel-focus"].connect(on_has_toplevel_focus);
|
|
|
|
// Search bar
|
|
this.search_bar = new SearchBar(this.application.engine);
|
|
this.search_bar.search_text_changed.connect(on_search);
|
|
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
|
|
this.conversation_list_box.pack_start(
|
|
this.conversation_list_info_bars, false, false, 0
|
|
);
|
|
this.conversation_list_view = new ConversationListView(
|
|
this.application.config
|
|
);
|
|
this.conversation_list_view.load_more.connect(on_load_more);
|
|
this.conversation_list_view.mark_conversations.connect(on_mark_conversations);
|
|
this.conversation_list_view.conversations_selected.connect(on_conversations_selected);
|
|
this.conversation_list_view.conversation_activated.connect(on_conversation_activated);
|
|
this.conversation_list_view.visible_conversations_changed.connect(on_visible_conversations_changed);
|
|
this.conversation_list_scrolled.add(conversation_list_view);
|
|
|
|
// Conversation viewer
|
|
this.conversation_viewer = new ConversationViewer(
|
|
this.application.config
|
|
);
|
|
this.conversation_viewer.conversation_added.connect(
|
|
on_conversation_view_added
|
|
);
|
|
|
|
this.conversation_viewer.hexpand = true;
|
|
this.conversation_size_group.add_widget(this.conversation_viewer);
|
|
this.conversation_viewer_box.add(this.conversation_viewer);
|
|
|
|
|
|
// Main toolbar
|
|
this.main_toolbar = new Components.MainToolbar(config);
|
|
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",
|
|
SYNC_CREATE | BIDIRECTIONAL
|
|
);
|
|
this.main_toolbar.bind_property(
|
|
"find-open",
|
|
this.conversation_viewer.conversation_find_bar, "search-mode-enabled",
|
|
SYNC_CREATE | BIDIRECTIONAL
|
|
);
|
|
if (config.desktop_environment == UNITY) {
|
|
this.main_toolbar.show_close_button = false;
|
|
this.main_layout.pack_start(main_toolbar, false, true, 0);
|
|
} else {
|
|
this.main_layout.pack_start(main_toolbar, false, true, 0);
|
|
}
|
|
|
|
this.main_layout.pack_start(this.info_bars, false, true, 0);
|
|
|
|
// Status bar
|
|
this.status_bar.set_size_request(-1, STATUS_BAR_HEIGHT);
|
|
this.status_bar.set_border_width(2);
|
|
this.spinner.set_size_request(STATUS_BAR_HEIGHT - 2, -1);
|
|
this.spinner.set_progress_monitor(progress_monitor);
|
|
this.status_bar.add(this.spinner);
|
|
this.status_bar.show_all();
|
|
}
|
|
|
|
/** {@inheritDoc} */
|
|
public override bool key_press_event(Gdk.EventKey event) {
|
|
check_shift_event(event);
|
|
return base.key_press_event(event);
|
|
}
|
|
|
|
/** {@inheritDoc} */
|
|
public override bool key_release_event(Gdk.EventKey event) {
|
|
check_shift_event(event);
|
|
return base.key_release_event(event);
|
|
}
|
|
|
|
internal bool prompt_empty_folder(Geary.Folder.SpecialUse type) {
|
|
var folder_name = Util.I18n.to_folder_type_display_name(type);
|
|
ConfirmationDialog dialog = new ConfirmationDialog(
|
|
this,
|
|
_("Empty all email from your %s folder?").printf(folder_name),
|
|
_("This removes the email from Geary and your email server.") +
|
|
" <b>" + _("This cannot be undone.") + "</b>",
|
|
_("Empty %s").printf(folder_name),
|
|
"destructive-action"
|
|
);
|
|
dialog.use_secondary_markup(true);
|
|
dialog.set_focus_response(Gtk.ResponseType.CANCEL);
|
|
return (dialog.run() == Gtk.ResponseType.OK);
|
|
}
|
|
|
|
/** Un-does the last executed application command, if any. */
|
|
private async void undo() {
|
|
AccountContext? selected = get_selected_account_context();
|
|
if (selected != null) {
|
|
selected.commands.undo.begin(
|
|
selected.cancellable,
|
|
(obj, res) => {
|
|
try {
|
|
selected.commands.undo.end(res);
|
|
} catch (GLib.Error err) {
|
|
handle_error(selected.account.information, err);
|
|
}
|
|
}
|
|
);
|
|
}
|
|
}
|
|
|
|
/** Re-does the last undone application command, if any. */
|
|
private async void redo() {
|
|
AccountContext? selected = get_selected_account_context();
|
|
if (selected != null) {
|
|
selected.commands.redo.begin(
|
|
selected.cancellable,
|
|
(obj, res) => {
|
|
try {
|
|
selected.commands.redo.end(res);
|
|
} catch (GLib.Error err) {
|
|
handle_error(selected.account.information, err);
|
|
}
|
|
}
|
|
);
|
|
}
|
|
}
|
|
|
|
private void update_command_actions() {
|
|
AccountContext? selected = get_selected_account_context();
|
|
get_edit_action(Action.Edit.UNDO).set_enabled(
|
|
selected != null && selected.commands.can_undo
|
|
);
|
|
get_edit_action(Action.Edit.REDO).set_enabled(
|
|
selected != null && selected.commands.can_redo
|
|
);
|
|
}
|
|
|
|
private bool prompt_delete_conversations(int count) {
|
|
ConfirmationDialog dialog = new ConfirmationDialog(
|
|
this,
|
|
/// Translators: Primary text for a confirmation dialog
|
|
ngettext(
|
|
"Do you want to permanently delete this conversation?",
|
|
"Do you want to permanently delete these conversations?",
|
|
count
|
|
),
|
|
null,
|
|
_("Delete"), "destructive-action"
|
|
);
|
|
return (dialog.run() == Gtk.ResponseType.OK);
|
|
}
|
|
|
|
private bool prompt_delete_messages(int count) {
|
|
ConfirmationDialog dialog = new ConfirmationDialog(
|
|
this,
|
|
/// Translators: Primary text for a confirmation dialog
|
|
ngettext(
|
|
"Do you want to permanently delete this message?",
|
|
"Do you want to permanently delete these messages?",
|
|
count
|
|
),
|
|
null,
|
|
_("Delete"), "destructive-action"
|
|
);
|
|
return (dialog.run() == Gtk.ResponseType.OK);
|
|
}
|
|
|
|
private async Gee.Collection<Geary.App.Conversation>
|
|
load_conversations_for_email(
|
|
Geary.Folder location,
|
|
Gee.Collection<Geary.EmailIdentifier> to_load) {
|
|
bool was_loaded = false;
|
|
// Can't assume the conversation monitor is valid, so check
|
|
// it first.
|
|
if (this.conversations != null &&
|
|
this.conversations.base_folder == location) {
|
|
try {
|
|
yield this.conversations.load_email(to_load, this.folder_open);
|
|
was_loaded = true;
|
|
} catch (GLib.Error err) {
|
|
debug("Error loading conversations to show them: %s",
|
|
err.message);
|
|
}
|
|
}
|
|
|
|
// Conversation monitor may have changed since resuming from
|
|
// the last async statement, so check it's still valid again.
|
|
var loaded = new Gee.HashSet<Geary.App.Conversation>();
|
|
if (was_loaded &&
|
|
this.conversations != null &&
|
|
this.conversations.base_folder == location) {
|
|
foreach (var id in to_load) {
|
|
Geary.App.Conversation? conversation =
|
|
this.conversations.get_by_email_identifier(id);
|
|
if (conversation != null) {
|
|
loaded.add(conversation);
|
|
}
|
|
}
|
|
}
|
|
return loaded;
|
|
}
|
|
|
|
private inline void handle_error(Geary.AccountInformation? account,
|
|
GLib.Error error) {
|
|
Geary.ProblemReport? report = (account != null)
|
|
? new Geary.AccountProblemReport(account, error)
|
|
: new Geary.ProblemReport(error);
|
|
this.controller.report_problem(report);
|
|
}
|
|
|
|
private void update_ui() {
|
|
// Only update if we haven't done so within the last while
|
|
int64 now = GLib.get_monotonic_time() / (1000 * 1000);
|
|
if (this.update_ui_last + UPDATE_UI_INTERVAL < now) {
|
|
this.update_ui_last = now;
|
|
|
|
if (this.conversation_viewer.current_list != null) {
|
|
this.conversation_viewer.current_list.update_display();
|
|
}
|
|
|
|
ConversationListStore? list_store =
|
|
this.conversation_list_view.get_model() as ConversationListStore;
|
|
if (list_store != null) {
|
|
list_store.update_display();
|
|
}
|
|
}
|
|
}
|
|
|
|
private void select_account(Geary.Account? account) {
|
|
if (this.selected_account != account) {
|
|
if (this.selected_account != null) {
|
|
this.conversation_actions.copy_folder_menu.clear();
|
|
this.conversation_actions.move_folder_menu.clear();
|
|
}
|
|
|
|
this.selected_account = account;
|
|
this.search_bar.set_account(account);
|
|
|
|
if (account != null) {
|
|
foreach (Geary.Folder folder in account.list_folders()) {
|
|
this.conversation_actions.copy_folder_menu.add_folder(folder);
|
|
this.conversation_actions.move_folder_menu.add_folder(folder);
|
|
}
|
|
}
|
|
|
|
update_command_actions();
|
|
}
|
|
}
|
|
|
|
private async void select_conversations(Gee.Collection<Geary.App.Conversation> to_select,
|
|
Gee.Collection<Geary.EmailIdentifier> scroll_to,
|
|
bool is_interactive) {
|
|
bool start_mark_timer = (
|
|
this.previous_selection_was_interactive && is_interactive
|
|
);
|
|
this.previous_selection_was_interactive = is_interactive;
|
|
|
|
// Ensure that the conversations are selected in the UI if
|
|
// this was called by something other than the selection
|
|
// changed callback. That will check to ensure that we're not
|
|
// setting it again.
|
|
this.conversation_list_view.select_conversations(to_select);
|
|
|
|
this.conversation_list_actions.selected_conversations = to_select.size;
|
|
this.main_toolbar.full_actions.selected_conversations = to_select.size;
|
|
this.main_toolbar.compact_actions.selected_conversations = to_select.size;
|
|
|
|
if (this.selected_folder != null && !this.has_composer) {
|
|
switch(to_select.size) {
|
|
case 0:
|
|
update_conversation_actions(NONE);
|
|
this.conversation_viewer.show_none_selected();
|
|
break;
|
|
|
|
case 1:
|
|
update_conversation_actions(SINGLE);
|
|
Geary.App.Conversation? convo = Geary.Collection.first(to_select);
|
|
|
|
// It's possible for a conversation with zero email to
|
|
// be selected, when it has just evaporated after its
|
|
// last email was removed but the conversation monitor
|
|
// hasn't signalled its removal yet. In this case,
|
|
// just don't load it since it will soon disappear.
|
|
AccountContext? context = get_selected_account_context();
|
|
if (context != null && convo.get_count() > 0) {
|
|
try {
|
|
yield this.conversation_viewer.load_conversation(
|
|
convo,
|
|
scroll_to,
|
|
context.emails,
|
|
context.contacts,
|
|
start_mark_timer
|
|
);
|
|
} catch (Geary.EngineError.NOT_FOUND err) {
|
|
// The first interesting email from the
|
|
// conversation wasn't found. If the
|
|
// conversation has completely evaporated by
|
|
// now then fine, otherwise throw the
|
|
// error. This happens e.g. in the drafts
|
|
// folder, there is a race between the
|
|
// composer being discarded and the draft
|
|
// itself disappearing
|
|
if (convo.get_count() == 0) {
|
|
debug("Ignoring not found error: %s", err.message);
|
|
} else {
|
|
handle_error(
|
|
convo.base_folder.account.information, err
|
|
);
|
|
}
|
|
} catch (GLib.IOError.CANCELLED err) {
|
|
// All good
|
|
} catch (GLib.Error err) {
|
|
handle_error(convo.base_folder.account.information, err);
|
|
}
|
|
}
|
|
break;
|
|
|
|
default:
|
|
update_conversation_actions(MULTIPLE);
|
|
this.conversation_viewer.show_multiple_selected();
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
private async void open_conversation_monitor(Geary.App.ConversationMonitor to_open,
|
|
GLib.Cancellable cancellable) {
|
|
to_open.scan_completed.connect(on_scan_completed);
|
|
to_open.scan_error.connect(on_scan_error);
|
|
|
|
to_open.scan_completed.connect(on_conversation_count_changed);
|
|
to_open.conversations_added.connect(on_conversation_count_changed);
|
|
to_open.conversations_removed.connect(on_conversation_count_changed);
|
|
|
|
to_open.start_monitoring.begin(
|
|
NO_DELAY,
|
|
cancellable,
|
|
(obj, res) => {
|
|
try {
|
|
to_open.start_monitoring.end(res);
|
|
} catch (GLib.Error err) {
|
|
handle_error(to_open.base_folder.account.information, err);
|
|
}
|
|
}
|
|
);
|
|
}
|
|
|
|
private void close_conversation_monitor(Geary.App.ConversationMonitor to_close) {
|
|
to_close.scan_completed.disconnect(on_scan_completed);
|
|
to_close.scan_error.disconnect(on_scan_error);
|
|
|
|
to_close.scan_completed.disconnect(on_conversation_count_changed);
|
|
to_close.conversations_added.disconnect(on_conversation_count_changed);
|
|
to_close.conversations_removed.disconnect(on_conversation_count_changed);
|
|
|
|
to_close.stop_monitoring.begin(
|
|
null,
|
|
(obj, res) => {
|
|
try {
|
|
to_close.stop_monitoring.end(res);
|
|
} catch (GLib.Error err) {
|
|
warning(
|
|
"Error closing conversation monitor %s: %s",
|
|
to_close.base_folder.to_string(),
|
|
err.message
|
|
);
|
|
}
|
|
}
|
|
);
|
|
}
|
|
|
|
private async void create_composer(Geary.Account send_context,
|
|
Composer.Widget.ContextType type,
|
|
Geary.Email context,
|
|
string? quote) {
|
|
var composer = yield this.controller.compose_with_context(
|
|
this.controller.get_context_for_account(send_context.information),
|
|
type,
|
|
context,
|
|
quote ?? ""
|
|
);
|
|
this.controller.present_composer(composer);
|
|
}
|
|
|
|
private async void create_composer_from_viewer(Composer.Widget.ContextType type) {
|
|
Geary.Account? account = this.selected_account;
|
|
ConversationEmail? email_view = null;
|
|
ConversationListBox? list_view = this.conversation_viewer.current_list;
|
|
if (list_view != null) {
|
|
email_view = list_view.get_reply_target();
|
|
}
|
|
if (account != null && email_view != null) {
|
|
string? quote = yield email_view.get_selection_for_quoting();
|
|
yield create_composer(account, type, email_view.email, quote);
|
|
}
|
|
}
|
|
|
|
private void load_more() {
|
|
if (this.conversations != null) {
|
|
this.conversations.min_window_count += MIN_CONVERSATION_COUNT;
|
|
}
|
|
}
|
|
|
|
private void on_conversations_selected(Gee.Set<Geary.App.Conversation> selected) {
|
|
this.select_conversations.begin(selected, Gee.Collection.empty(), true);
|
|
}
|
|
|
|
private void on_conversation_count_changed() {
|
|
// Only update the UI if we don't currently have a composer,
|
|
// so we don't clobber it
|
|
if (!this.has_composer) {
|
|
if (this.conversations.size == 0) {
|
|
// Let the user know if there's no available conversations
|
|
if (this.selected_folder.used_as == SEARCH) {
|
|
this.conversation_viewer.show_empty_search();
|
|
} else {
|
|
this.conversation_viewer.show_empty_folder();
|
|
}
|
|
update_conversation_actions(NONE);
|
|
} else {
|
|
// When not doing autoselect, we never get
|
|
// conversations_selected firing from the convo list,
|
|
// so we need to stop the loading spinner here.
|
|
if (!this.application.config.autoselect &&
|
|
this.conversation_list_view.get_selection().count_selected_rows() == 0) {
|
|
this.conversation_viewer.show_none_selected();
|
|
update_conversation_actions(NONE);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private void update_headerbar() {
|
|
update_title();
|
|
if (this.selected_folder != null) {
|
|
// Current folder's name followed by its unread count,
|
|
// i.e. "Inbox (42)" except for Drafts and Outbox, where
|
|
// we show total count
|
|
int count;
|
|
switch (this.selected_folder.used_as) {
|
|
case DRAFTS:
|
|
case OUTBOX:
|
|
count = this.selected_folder.properties.email_total;
|
|
break;
|
|
|
|
default:
|
|
count = this.selected_folder.properties.email_unread;
|
|
break;
|
|
}
|
|
|
|
if (count > 0) {
|
|
this.main_toolbar.folder = _("%s (%d)").printf(
|
|
this.main_toolbar.folder, count
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void update_conversation_actions(ConversationCount count) {
|
|
bool sensitive = (count != NONE);
|
|
bool multiple = (count == MULTIPLE);
|
|
|
|
get_window_action(ACTION_FIND_IN_CONVERSATION).set_enabled(
|
|
sensitive && !multiple
|
|
);
|
|
|
|
bool reply_sensitive = (
|
|
sensitive &&
|
|
!multiple &&
|
|
this.selected_folder != null &&
|
|
this.selected_folder.used_as != DRAFTS
|
|
);
|
|
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.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.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(
|
|
sensitive && (selected_folder is Geary.FolderSupport.Archive)
|
|
);
|
|
get_window_action(ACTION_TRASH_CONVERSATION).set_enabled(
|
|
sensitive && this.selected_folder_supports_trash
|
|
);
|
|
get_window_action(ACTION_DELETE_CONVERSATION).set_enabled(
|
|
sensitive && (selected_folder is Geary.FolderSupport.Remove)
|
|
);
|
|
|
|
this.update_context_dependent_actions.begin(sensitive);
|
|
switch (count) {
|
|
case NONE:
|
|
this.conversation_list_actions_revealer.reveal_child = false;
|
|
break;
|
|
case SINGLE:
|
|
this.conversation_list_actions_revealer.reveal_child = false;
|
|
break;
|
|
case MULTIPLE:
|
|
this.conversation_list_actions_revealer.reveal_child = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
private void update_trash_action() {
|
|
var show_trash = (
|
|
!this.is_shift_down &&
|
|
this.selected_folder_supports_trash
|
|
);
|
|
this.conversation_list_actions.update_trash_button(show_trash);
|
|
this.main_toolbar.full_actions.update_trash_button(show_trash);
|
|
this.main_toolbar.compact_actions.update_trash_button(show_trash);
|
|
}
|
|
|
|
private async void update_context_dependent_actions(bool sensitive) {
|
|
// Cancel any existing update that is running
|
|
this.action_update_cancellable.cancel();
|
|
GLib.Cancellable cancellable = new Cancellable();
|
|
this.action_update_cancellable = cancellable;
|
|
|
|
Gee.MultiMap<Geary.EmailIdentifier, Type>? selected_operations = null;
|
|
if (this.selected_folder != null) {
|
|
AccountContext? context =
|
|
this.controller.get_context_for_account(
|
|
this.selected_folder.account.information
|
|
);
|
|
if (context != null) {
|
|
Gee.Collection<Geary.EmailIdentifier> ids =
|
|
new Gee.LinkedList<Geary.EmailIdentifier>();
|
|
foreach (Geary.App.Conversation convo in
|
|
this.conversation_list_view.get_selected()) {
|
|
ids.add_all(convo.get_email_ids());
|
|
}
|
|
try {
|
|
selected_operations = yield context.emails.get_supported_operations_async(
|
|
ids, cancellable
|
|
);
|
|
} catch (GLib.Error e) {
|
|
debug("Error checking for what operations are supported in the selected conversations: %s",
|
|
e.message);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!cancellable.is_cancelled()) {
|
|
Gee.HashSet<Type> supported_operations = new Gee.HashSet<Type>();
|
|
if (selected_operations != null) {
|
|
supported_operations.add_all(selected_operations.get_values());
|
|
}
|
|
|
|
get_window_action(ACTION_SHOW_MARK_MENU).set_enabled(
|
|
sensitive &&
|
|
(typeof(Geary.FolderSupport.Mark) in supported_operations)
|
|
);
|
|
get_window_action(ACTION_SHOW_COPY_MENU).set_enabled(
|
|
sensitive &&
|
|
(supported_operations.contains(typeof(Geary.FolderSupport.Copy)))
|
|
);
|
|
get_window_action(ACTION_SHOW_MOVE_MENU).set_enabled(
|
|
sensitive &&
|
|
(supported_operations.contains(typeof(Geary.FolderSupport.Move)))
|
|
);
|
|
}
|
|
}
|
|
|
|
private void set_shift_key_down(bool down) {
|
|
this.is_shift_down = down;
|
|
update_trash_action();
|
|
}
|
|
|
|
private inline void check_shift_event(Gdk.EventKey event) {
|
|
// FIXME: it's possible the user will press two shift keys. We want
|
|
// the shift key to report as released when they release ALL of them.
|
|
// There doesn't seem to be an easy way to do this in Gdk.
|
|
if (event.keyval == Gdk.Key.Shift_L || event.keyval == Gdk.Key.Shift_R) {
|
|
Gtk.Widget? focus = get_focus();
|
|
if (focus == null ||
|
|
(!(focus is Gtk.Entry) && !(focus is Composer.WebView))) {
|
|
set_shift_key_down(event.type == Gdk.EventType.KEY_PRESS);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void focus_next_pane() {
|
|
var focus = get_focus();
|
|
|
|
if (this.outer_leaflet.folded) {
|
|
if (this.outer_leaflet.visible_child_name == INNER_LEAFLET) {
|
|
if (this.inner_leaflet.folded &&
|
|
this.inner_leaflet.visible_child_name == FOLDER_LIST ||
|
|
focus == this.folder_list) {
|
|
this.inner_leaflet.navigate(Hdy.NavigationDirection.FORWARD);
|
|
focus = this.conversation_list_view;
|
|
} else {
|
|
if (this.conversation_list_view.get_selected().size == 1 &&
|
|
this.selected_folder.properties.email_total > 0) {
|
|
this.outer_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;
|
|
} else if (focus == this.conversation_list_view ||
|
|
focus.is_ancestor(this.conversation_list_view)) {
|
|
focus = this.conversation_viewer.visible_child;
|
|
} else if (focus == this.conversation_viewer ||
|
|
focus.is_ancestor(this.conversation_viewer)) {
|
|
focus = this.folder_list;
|
|
}
|
|
}
|
|
|
|
if (focus != null) {
|
|
focus.focus(TAB_FORWARD);
|
|
} else {
|
|
get_window().beep();
|
|
}
|
|
}
|
|
|
|
private void focus_previous_pane() {
|
|
var focus = get_focus();
|
|
|
|
if (this.outer_leaflet.folded) {
|
|
if (this.outer_leaflet.visible_child_name == INNER_LEAFLET) {
|
|
if (this.inner_leaflet.folded) {
|
|
if (this.inner_leaflet.visible_child_name == CONVERSATION_LIST) {
|
|
this.inner_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 {
|
|
this.outer_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;
|
|
} else if (focus == this.conversation_list_view ||
|
|
focus.is_ancestor(this.conversation_list_view)) {
|
|
focus = this.folder_list;
|
|
} else if (focus == this.conversation_viewer ||
|
|
focus.is_ancestor(this.conversation_viewer)) {
|
|
focus = this.conversation_list_view;
|
|
}
|
|
}
|
|
|
|
if (focus != null) {
|
|
focus.focus(TAB_FORWARD);
|
|
} else {
|
|
get_window().beep();
|
|
}
|
|
|
|
}
|
|
|
|
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 activate_action(GLib.Action? action) {
|
|
if (action != null && action.get_enabled()) {
|
|
action.activate(null);
|
|
} else {
|
|
get_window().beep();
|
|
}
|
|
}
|
|
|
|
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();
|
|
Gtk.Scrollbar? scrollbar = (
|
|
this.conversation_list_scrolled.get_vscrollbar() as Gtk.Scrollbar
|
|
);
|
|
if (is_visible() &&
|
|
(scrollbar == null || !scrollbar.get_visible()) &&
|
|
monitor == this.conversations &&
|
|
monitor.can_load_more) {
|
|
debug("Not enough messages, loading more for folder %s",
|
|
this.selected_folder.to_string());
|
|
load_more();
|
|
}
|
|
}
|
|
|
|
private void on_scan_error(Geary.App.ConversationMonitor monitor,
|
|
GLib.Error err) {
|
|
Geary.AccountInformation account =
|
|
monitor.base_folder.account.information;
|
|
this.controller.report_problem(
|
|
new Geary.ServiceProblemReport(account, account.incoming, err)
|
|
);
|
|
}
|
|
|
|
private void on_load_more() {
|
|
load_more();
|
|
}
|
|
|
|
[GtkCallback]
|
|
private void on_map() {
|
|
this.update_ui_timeout.start();
|
|
update_ui();
|
|
}
|
|
|
|
[GtkCallback]
|
|
private void on_unmap() {
|
|
this.update_ui_timeout.reset();
|
|
}
|
|
|
|
[GtkCallback]
|
|
private bool on_focus_event() {
|
|
this.set_shift_key_down(false);
|
|
return false;
|
|
}
|
|
|
|
[GtkCallback]
|
|
private bool on_delete_event() {
|
|
if (close_composer(true, false)) {
|
|
this.sensitive = false;
|
|
this.select_folder.begin(
|
|
null,
|
|
false,
|
|
true,
|
|
(obj, res) => {
|
|
this.select_folder.end(res);
|
|
destroy();
|
|
}
|
|
);
|
|
}
|
|
return Gdk.EVENT_STOP;
|
|
}
|
|
|
|
[GtkCallback]
|
|
private void on_outer_leaflet_changed() {
|
|
if (this.has_composer &&
|
|
this.outer_leaflet.folded &&
|
|
(this.is_folder_list_shown || this.is_conversation_list_shown)) {
|
|
close_composer(false, false);
|
|
}
|
|
}
|
|
|
|
private void on_offline_infobar_response() {
|
|
this.info_bars.remove(this.offline_infobar);
|
|
}
|
|
|
|
private void on_service_problem_retry() {
|
|
this.info_bars.remove(this.service_problem_infobar);
|
|
this.service_problem_infobar = null;
|
|
retry_service_problem(Geary.ClientService.Status.CONNECTION_FAILED);
|
|
}
|
|
|
|
private void on_cert_problem_retry() {
|
|
this.info_bars.remove(this.cert_problem_infobar);
|
|
retry_service_problem(Geary.ClientService.Status.TLS_VALIDATION_FAILED);
|
|
}
|
|
|
|
private void on_auth_problem_retry() {
|
|
this.info_bars.remove(this.auth_problem_infobar);
|
|
retry_service_problem(Geary.ClientService.Status.AUTHENTICATION_FAILED);
|
|
}
|
|
|
|
private void on_update_ui_timeout() {
|
|
update_ui();
|
|
}
|
|
|
|
private void on_account_available(AccountContext account) {
|
|
add_account(account);
|
|
}
|
|
|
|
private void on_account_unavailable(AccountContext account,
|
|
bool is_shutdown) {
|
|
// If we're not shutting down, select the inbox of the first
|
|
// account so that we show something other than empty
|
|
// conversation list/viewer.
|
|
Geary.Folder? to_select = null;
|
|
if (!is_shutdown) {
|
|
to_select = get_first_inbox();
|
|
}
|
|
this.remove_account.begin(account, to_select);
|
|
}
|
|
|
|
private void on_folders_available(Gee.Collection<FolderContext> available) {
|
|
add_folders(available);
|
|
}
|
|
|
|
private void on_folders_unavailable(Gee.Collection<FolderContext> unavailable) {
|
|
remove_folders(unavailable, true);
|
|
}
|
|
|
|
private void on_use_changed(Geary.Folder folder,
|
|
Geary.Folder.SpecialUse old_type,
|
|
Geary.Folder.SpecialUse new_type) {
|
|
// Update the main window
|
|
AccountContext? context = this.controller.get_context_for_account(
|
|
folder.account.information
|
|
);
|
|
if (context != null) {
|
|
FolderContext? folder_context = context.get_folder(folder);
|
|
if (folder_context != null) {
|
|
this.folder_list.remove_folder(folder_context);
|
|
this.folder_list.add_folder(folder_context);
|
|
|
|
// Since removing the folder will also remove its children
|
|
// from the folder list, we need to check for any and re-add
|
|
// them. See issue #11.
|
|
try {
|
|
foreach (Geary.Folder child in
|
|
folder.account.list_matching_folders(folder.path)) {
|
|
FolderContext? child_context = context.get_folder(child);
|
|
if (child_context != null) {
|
|
this.folder_list.add_folder(child_context);
|
|
}
|
|
}
|
|
} catch (Error err) {
|
|
// Oh well
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private void on_command_execute(Command command) {
|
|
if (!(command is TrivialCommand)) {
|
|
// Only show an execute notification for non-trivial
|
|
// commands
|
|
on_command_redo(command);
|
|
} else {
|
|
// Still have to update the undo/redo actions for trivial
|
|
// commands
|
|
update_command_actions();
|
|
}
|
|
}
|
|
|
|
private void on_command_undo(Command command) {
|
|
update_command_actions();
|
|
EmailCommand? email = command as EmailCommand;
|
|
if (email != null) {
|
|
if (email.conversations.size > 1) {
|
|
this.show_conversations.begin(
|
|
email.location, email.conversations, false
|
|
);
|
|
} else {
|
|
this.show_email.begin(
|
|
email.location, email.email, false
|
|
);
|
|
}
|
|
}
|
|
if (command.undone_label != null) {
|
|
Components.InAppNotification ian =
|
|
new Components.InAppNotification(command.undone_label);
|
|
ian.set_button(_("Redo"), Action.Edit.prefix(Action.Edit.REDO));
|
|
add_notification(ian);
|
|
}
|
|
}
|
|
|
|
private void on_command_redo(Command command) {
|
|
update_command_actions();
|
|
if (command.executed_label != null) {
|
|
uint notification_time =
|
|
Components.InAppNotification.DEFAULT_DURATION;
|
|
if (command.executed_notification_brief) {
|
|
notification_time =
|
|
application.config.brief_notification_duration;
|
|
}
|
|
Components.InAppNotification ian = new Components.InAppNotification(
|
|
command.executed_label, notification_time
|
|
);
|
|
ian.set_button(_("Undo"), Action.Edit.prefix(Action.Edit.UNDO));
|
|
add_notification(ian);
|
|
}
|
|
}
|
|
|
|
private void on_conversation_view_added(ConversationListBox list) {
|
|
list.email_loaded.connect(on_email_loaded);
|
|
list.mark_email.connect(on_email_mark);
|
|
list.reply_to_all_email.connect(on_email_reply_to_all);
|
|
list.reply_to_sender_email.connect(on_email_reply_to_sender);
|
|
list.forward_email.connect(on_email_forward);
|
|
list.trash_email.connect(on_email_trash);
|
|
list.delete_email.connect(on_email_delete);
|
|
}
|
|
|
|
// Window-level action callbacks
|
|
|
|
private void on_undo() {
|
|
this.undo.begin();
|
|
}
|
|
|
|
private void on_redo() {
|
|
this.redo.begin();
|
|
}
|
|
|
|
private void on_close() {
|
|
close();
|
|
}
|
|
|
|
// this signal does not necessarily indicate that the application
|
|
// previously didn't have focus and now it does
|
|
private void on_has_toplevel_focus() {
|
|
if (this.selected_folder != null) {
|
|
this.controller.clear_new_messages(
|
|
this.selected_folder,
|
|
this.conversation_list_view.get_visible_conversations()
|
|
);
|
|
}
|
|
}
|
|
|
|
private void on_folder_selected(Geary.Folder? folder) {
|
|
this.select_folder.begin(folder, true);
|
|
}
|
|
|
|
private void on_select_inbox(SimpleAction action, Variant? parameter) {
|
|
if (parameter != null) {
|
|
int account_number = parameter.get_int32();
|
|
try {
|
|
Gee.Collection<Geary.Account> accounts =
|
|
this.application.engine.get_accounts();
|
|
if (account_number < accounts.size) {
|
|
Geary.Account account = accounts.to_array()[account_number];
|
|
Geary.Folder inbox = account.get_special_folder(INBOX);
|
|
this.select_folder.begin(inbox, true);
|
|
}
|
|
} catch (GLib.Error error) {
|
|
debug("Error getting accounts");
|
|
}
|
|
}
|
|
}
|
|
|
|
private void on_search(string text) {
|
|
if (Geary.String.is_empty_or_whitespace(text)) {
|
|
stop_search(true);
|
|
} else {
|
|
this.start_search(text, true);
|
|
}
|
|
}
|
|
|
|
private void on_visible_conversations_changed(Gee.Set<Geary.App.Conversation> visible) {
|
|
if (this.selected_folder != null) {
|
|
this.controller.clear_new_messages(this.selected_folder, visible);
|
|
}
|
|
}
|
|
|
|
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 (this.outer_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.
|
|
Geary.Email draft = activated.get_latest_recv_email(IN_FOLDER);
|
|
this.create_composer.begin(
|
|
this.selected_folder.account,
|
|
EDIT,
|
|
draft,
|
|
null
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void on_find_in_conversation_action() {
|
|
this.conversation_viewer.enable_find();
|
|
}
|
|
|
|
private void on_search_activated() {
|
|
show_search_bar();
|
|
}
|
|
|
|
private void on_zoom(SimpleAction action, Variant? parameter) {
|
|
ConversationListBox? view = this.conversation_viewer.current_list;
|
|
if (view != null && parameter != null) {
|
|
string zoom_action = parameter.get_string();
|
|
if (zoom_action == "in")
|
|
view.zoom_in();
|
|
else if (zoom_action == "out")
|
|
view.zoom_out();
|
|
else
|
|
view.zoom_reset();
|
|
}
|
|
}
|
|
|
|
private void on_reply_conversation() {
|
|
this.create_composer_from_viewer.begin(REPLY_SENDER);
|
|
}
|
|
|
|
private void on_reply_all_conversation() {
|
|
this.create_composer_from_viewer.begin(REPLY_ALL);
|
|
}
|
|
|
|
private void on_forward_conversation() {
|
|
this.create_composer_from_viewer.begin(FORWARD);
|
|
}
|
|
|
|
private void on_show_window_menu() {
|
|
show_window_menu();
|
|
}
|
|
|
|
private void on_show_help_overlay() {
|
|
var overlay = get_help_overlay();
|
|
overlay.section_name = "conversation";
|
|
overlay.show();
|
|
}
|
|
|
|
private void on_show_copy_menu() {
|
|
if (this.is_conversation_list_shown &&
|
|
this.conversation_list_actions_revealer.child_revealed) {
|
|
this.conversation_list_actions.show_copy_menu();
|
|
} else if (this.is_conversation_viewer_shown) {
|
|
this.main_toolbar.shown_actions.show_copy_menu();
|
|
} else {
|
|
this.error_bell();
|
|
}
|
|
}
|
|
|
|
private void on_show_move_menu() {
|
|
if (this.is_conversation_list_shown &&
|
|
this.conversation_list_actions_revealer.child_revealed) {
|
|
this.conversation_list_actions.show_move_menu();
|
|
} else if (this.is_conversation_viewer_shown) {
|
|
this.main_toolbar.shown_actions.show_move_menu();
|
|
} else {
|
|
this.error_bell();
|
|
}
|
|
}
|
|
|
|
private void on_conversation_up() {
|
|
this.conversation_list_view.scroll(Gtk.ScrollType.STEP_UP);
|
|
}
|
|
|
|
private void on_conversation_down() {
|
|
this.conversation_list_view.scroll(Gtk.ScrollType.STEP_DOWN);
|
|
}
|
|
|
|
private void on_show_mark_menu() {
|
|
bool unread_selected = false;
|
|
bool read_selected = false;
|
|
bool starred_selected = false;
|
|
bool unstarred_selected = false;
|
|
foreach (Geary.App.Conversation conversation in
|
|
this.conversation_list_view.get_selected()) {
|
|
if (conversation.is_unread())
|
|
unread_selected = true;
|
|
|
|
// Only check the messages that "Mark as Unread" would mark, so we
|
|
// don't add the menu option and have it not do anything.
|
|
//
|
|
// Sort by Date: field to correspond with ConversationViewer ordering
|
|
Geary.Email? latest = conversation.get_latest_sent_email(
|
|
Geary.App.Conversation.Location.IN_FOLDER_OUT_OF_FOLDER);
|
|
if (latest != null && latest.email_flags != null
|
|
&& !latest.email_flags.contains(Geary.EmailFlags.UNREAD))
|
|
read_selected = true;
|
|
|
|
if (conversation.is_flagged()) {
|
|
starred_selected = true;
|
|
} else {
|
|
unstarred_selected = true;
|
|
}
|
|
}
|
|
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 junk
|
|
bool in_junk_folder = (selected_folder.used_as == JUNK);
|
|
get_window_action(ACTION_TOGGLE_JUNK).set_enabled(
|
|
!in_junk_folder &&
|
|
selected_folder.used_as != DRAFTS &&
|
|
selected_folder.used_as != OUTBOX
|
|
);
|
|
}
|
|
|
|
private void on_mark_conversations(Gee.Collection<Geary.App.Conversation> conversations,
|
|
Geary.NamedFlag flag) {
|
|
Geary.Folder? location = this.selected_folder;
|
|
if (location != null) {
|
|
this.controller.mark_conversations.begin(
|
|
location,
|
|
conversations,
|
|
flag,
|
|
true,
|
|
(obj, res) => {
|
|
try {
|
|
this.controller.mark_conversations.end(res);
|
|
} catch (GLib.Error err) {
|
|
handle_error(location.account.information, err);
|
|
}
|
|
}
|
|
);
|
|
}
|
|
}
|
|
|
|
private void on_mark_as_read() {
|
|
Geary.Folder? location = this.selected_folder;
|
|
if (location != null) {
|
|
this.controller.mark_conversations.begin(
|
|
location,
|
|
this.conversation_list_view.copy_selected(),
|
|
Geary.EmailFlags.UNREAD,
|
|
false,
|
|
(obj, res) => {
|
|
try {
|
|
this.controller.mark_conversations.end(res);
|
|
} catch (GLib.Error err) {
|
|
handle_error(location.account.information, err);
|
|
}
|
|
}
|
|
);
|
|
}
|
|
}
|
|
|
|
private void on_mark_as_unread() {
|
|
Geary.Folder? location = this.selected_folder;
|
|
if (location != null) {
|
|
this.controller.mark_conversations.begin(
|
|
location,
|
|
this.conversation_list_view.copy_selected(),
|
|
Geary.EmailFlags.UNREAD,
|
|
true,
|
|
(obj, res) => {
|
|
try {
|
|
this.controller.mark_conversations.end(res);
|
|
} catch (GLib.Error err) {
|
|
handle_error(location.account.information, err);
|
|
}
|
|
}
|
|
);
|
|
}
|
|
}
|
|
|
|
private void on_mark_as_starred() {
|
|
Geary.Folder? location = this.selected_folder;
|
|
if (location != null) {
|
|
this.controller.mark_conversations.begin(
|
|
location,
|
|
this.conversation_list_view.copy_selected(),
|
|
Geary.EmailFlags.FLAGGED,
|
|
true,
|
|
(obj, res) => {
|
|
try {
|
|
this.controller.mark_conversations.end(res);
|
|
} catch (GLib.Error err) {
|
|
handle_error(location.account.information, err);
|
|
}
|
|
}
|
|
);
|
|
}
|
|
}
|
|
|
|
private void on_mark_as_unstarred() {
|
|
Geary.Folder? location = this.selected_folder;
|
|
if (location != null) {
|
|
this.controller.mark_conversations.begin(
|
|
location,
|
|
this.conversation_list_view.copy_selected(),
|
|
Geary.EmailFlags.FLAGGED,
|
|
false,
|
|
(obj, res) => {
|
|
try {
|
|
this.controller.mark_conversations.end(res);
|
|
} catch (GLib.Error err) {
|
|
handle_error(location.account.information, err);
|
|
}
|
|
}
|
|
);
|
|
}
|
|
}
|
|
|
|
private void on_mark_as_junk_toggle() {
|
|
Geary.Folder? source = this.selected_folder;
|
|
if (source != null) {
|
|
Geary.Folder.SpecialUse destination =
|
|
(source.used_as != JUNK)
|
|
? Geary.Folder.SpecialUse.JUNK
|
|
: Geary.Folder.SpecialUse.INBOX;
|
|
this.controller.move_conversations_special.begin(
|
|
source,
|
|
destination,
|
|
this.conversation_list_view.copy_selected(),
|
|
(obj, res) => {
|
|
try {
|
|
this.controller.move_conversations_special.end(res);
|
|
} catch (GLib.Error err) {
|
|
handle_error(source.account.information, err);
|
|
}
|
|
}
|
|
);
|
|
}
|
|
}
|
|
|
|
private void on_move_conversation(Geary.Folder destination) {
|
|
Geary.FolderSupport.Move source =
|
|
this.selected_folder as Geary.FolderSupport.Move;
|
|
if (source != null) {
|
|
this.controller.move_conversations.begin(
|
|
source,
|
|
destination,
|
|
this.conversation_list_view.copy_selected(),
|
|
(obj, res) => {
|
|
try {
|
|
this.controller.move_conversations.end(res);
|
|
} catch (GLib.Error err) {
|
|
handle_error(source.account.information, err);
|
|
}
|
|
}
|
|
);
|
|
|
|
}
|
|
}
|
|
|
|
private void on_copy_conversation(Geary.Folder destination) {
|
|
Geary.FolderSupport.Copy source =
|
|
this.selected_folder as Geary.FolderSupport.Copy;
|
|
if (source != null) {
|
|
this.controller.copy_conversations.begin(
|
|
source,
|
|
destination,
|
|
this.conversation_list_view.copy_selected(),
|
|
(obj, res) => {
|
|
try {
|
|
this.controller.copy_conversations.end(res);
|
|
} catch (GLib.Error err) {
|
|
handle_error(source.account.information, err);
|
|
}
|
|
}
|
|
);
|
|
|
|
}
|
|
}
|
|
|
|
private void on_archive_conversation() {
|
|
Geary.Folder source = this.selected_folder;
|
|
if (source != null) {
|
|
this.controller.move_conversations_special.begin(
|
|
source,
|
|
ARCHIVE,
|
|
this.conversation_list_view.copy_selected(),
|
|
(obj, res) => {
|
|
try {
|
|
this.controller.move_conversations_special.end(res);
|
|
} catch (GLib.Error err) {
|
|
handle_error(source.account.information, err);
|
|
}
|
|
}
|
|
);
|
|
}
|
|
}
|
|
|
|
private void on_trash_conversation() {
|
|
Geary.Folder source = this.selected_folder;
|
|
if (source != null) {
|
|
this.controller.move_conversations_special.begin(
|
|
source,
|
|
TRASH,
|
|
this.conversation_list_view.copy_selected(),
|
|
(obj, res) => {
|
|
try {
|
|
this.controller.move_conversations_special.end(res);
|
|
} catch (GLib.Error err) {
|
|
handle_error(source.account.information, err);
|
|
}
|
|
}
|
|
);
|
|
}
|
|
}
|
|
|
|
private void on_delete_conversation() {
|
|
Geary.FolderSupport.Remove target =
|
|
this.selected_folder as Geary.FolderSupport.Remove;
|
|
Gee.Collection<Geary.App.Conversation> conversations =
|
|
this.conversation_list_view.copy_selected();
|
|
if (target != null && this.prompt_delete_conversations(conversations.size)) {
|
|
this.controller.delete_conversations.begin(
|
|
target,
|
|
conversations,
|
|
(obj, res) => {
|
|
try {
|
|
this.controller.delete_conversations.end(res);
|
|
} catch (GLib.Error err) {
|
|
handle_error(target.account.information, err);
|
|
}
|
|
}
|
|
);
|
|
}
|
|
}
|
|
|
|
private void on_email_loaded(ConversationListBox view,
|
|
Geary.Email loaded) {
|
|
this.controller.email_loaded(
|
|
view.conversation.base_folder.account.information,
|
|
loaded
|
|
);
|
|
}
|
|
|
|
private void on_email_mark(ConversationListBox view,
|
|
Gee.Collection<Geary.EmailIdentifier> messages,
|
|
Geary.NamedFlag? to_add,
|
|
Geary.NamedFlag? to_remove) {
|
|
Geary.Folder? location = this.selected_folder;
|
|
if (location != null) {
|
|
Geary.EmailFlags add_flags = null;
|
|
if (to_add != null) {
|
|
add_flags = new Geary.EmailFlags();
|
|
add_flags.add(to_add);
|
|
}
|
|
Geary.EmailFlags remove_flags = null;
|
|
if (to_remove != null) {
|
|
remove_flags = new Geary.EmailFlags();
|
|
remove_flags.add(to_remove);
|
|
}
|
|
this.controller.mark_messages.begin(
|
|
location,
|
|
Geary.Collection.single(view.conversation),
|
|
messages,
|
|
add_flags,
|
|
remove_flags,
|
|
(obj, res) => {
|
|
try {
|
|
this.controller.mark_messages.end(res);
|
|
} catch (GLib.Error err) {
|
|
handle_error(location.account.information, err);
|
|
}
|
|
}
|
|
);
|
|
}
|
|
}
|
|
|
|
private void on_email_reply_to_sender(Geary.Email target, string? quote) {
|
|
if (this.selected_account != null) {
|
|
this.create_composer.begin(
|
|
this.selected_account, REPLY_SENDER, target, quote
|
|
);
|
|
}
|
|
}
|
|
|
|
private void on_email_reply_to_all(Geary.Email target, string? quote) {
|
|
if (this.selected_account != null) {
|
|
this.create_composer.begin(
|
|
this.selected_account, REPLY_ALL, target, quote
|
|
);
|
|
}
|
|
}
|
|
|
|
private void on_email_forward(Geary.Email target, string? quote) {
|
|
if (this.selected_account != null) {
|
|
this.create_composer.begin(
|
|
this.selected_account, FORWARD, target, quote
|
|
);
|
|
}
|
|
}
|
|
|
|
private void on_email_trash(ConversationListBox view, Geary.Email target) {
|
|
Geary.Folder? source = this.selected_folder;
|
|
if (source != null) {
|
|
this.controller.move_messages_special.begin(
|
|
source,
|
|
TRASH,
|
|
Geary.Collection.single(view.conversation),
|
|
Geary.Collection.single(target.id),
|
|
(obj, res) => {
|
|
try {
|
|
this.controller.move_messages_special.end(res);
|
|
} catch (GLib.Error err) {
|
|
handle_error(source.account.information, err);
|
|
}
|
|
}
|
|
);
|
|
}
|
|
}
|
|
|
|
private void on_email_delete(ConversationListBox view, Geary.Email target) {
|
|
Geary.FolderSupport.Remove? source =
|
|
this.selected_folder as Geary.FolderSupport.Remove;
|
|
if (source != null && prompt_delete_messages(1)) {
|
|
this.controller.delete_messages.begin(
|
|
source,
|
|
Geary.Collection.single(view.conversation),
|
|
Geary.Collection.single(target.id),
|
|
(obj, res) => {
|
|
try {
|
|
this.controller.delete_messages.end(res);
|
|
} catch (GLib.Error err) {
|
|
handle_error(source.account.information, err);
|
|
}
|
|
}
|
|
);
|
|
}
|
|
}
|
|
|
|
}
|