2
* Copyright 2016 Software Freedom Conservancy Inc.
3
* Copyright 2016, 2019 Michael Gratton <mike@vee.net>
5
* This software is licensed under the GNU Lesser General Public License
6
* (version 2.1 or later). See the COPYING file in this distribution.
9
[GtkTemplate (ui = "/org/gnome/Geary/main-window.ui")]
10
public class MainWindow : Gtk.ApplicationWindow, Geary.BaseInterface {
13
private const int STATUS_BAR_HEIGHT = 18;
14
private const int UPDATE_UI_INTERVAL = 60;
15
private const int MIN_CONVERSATION_COUNT = 50;
18
public new GearyApplication application {
19
get { return (GearyApplication) base.get_application(); }
20
set { base.set_application(value); }
23
/** Currently selected folder, null if none selected */
24
public Geary.Folder? current_folder { get; private set; default = null; }
26
/** Conversations for the current folder, null if none selected */
27
public Geary.App.ConversationMonitor? conversations {
28
get; private set; default = null;
31
/** Determines if a composer is currently open in this window. */
32
public bool has_composer {
34
return (this.conversation_viewer.current_composer != null);
38
/** Specifies if the Shift key is currently being held. */
39
public bool is_shift_down { get; private set; default = false; }
41
// Used to save/load the window state between sessions.
42
public int window_width { get; set; }
43
public int window_height { get; set; }
44
public bool window_maximized { get; set; }
47
public FolderList.Tree folder_list { get; private set; default = new FolderList.Tree(); }
48
public MainToolbar main_toolbar { get; private set; }
49
public SearchBar search_bar { get; private set; default = new SearchBar(); }
50
public ConversationListView conversation_list_view { get; private set; }
51
public ConversationViewer conversation_viewer { get; private set; }
52
public StatusBar status_bar { get; private set; default = new StatusBar(); }
53
private MonitoredSpinner spinner = new MonitoredSpinner();
55
private Geary.AggregateProgressMonitor progress_monitor = new Geary.AggregateProgressMonitor();
56
private Geary.TimeoutManager update_ui_timeout;
57
private int64 update_ui_last = 0;
61
private Gtk.Box main_layout;
63
private Gtk.Box search_bar_box;
65
private Gtk.Paned folder_paned;
67
private Gtk.Paned conversations_paned;
69
private Gtk.Box folder_box;
71
private Gtk.ScrolledWindow folder_list_scrolled;
73
private Gtk.Box conversation_box;
75
private Gtk.ScrolledWindow conversation_list_scrolled;
77
private Gtk.Overlay overlay;
79
// This is a frame so users can use F6/Shift-F6 to get to it
81
private Gtk.Frame info_bar_frame;
84
private Gtk.Grid info_bar_container;
87
private Gtk.InfoBar offline_infobar;
90
private Gtk.InfoBar cert_problem_infobar;
93
private Gtk.InfoBar auth_problem_infobar;
95
private MainWindowInfoBar? service_problem_infobar = null;
97
/** Fired when the user requests an account status be retried. */
98
public signal void retry_service_problem(Geary.ClientService.Status problem);
100
/** Fired when the shift key is pressed or released. */
101
public signal void on_shift_key(bool pressed);
104
public MainWindow(GearyApplication application) {
106
application: application,
111
load_config(application.config);
112
restore_saved_window_state();
114
this.application.engine.account_available.connect(on_account_available);
115
this.application.engine.account_unavailable.connect(on_account_unavailable);
118
setup_layout(application.config);
119
on_change_orientation();
121
this.update_ui_timeout = new Geary.TimeoutManager.seconds(
122
UPDATE_UI_INTERVAL, on_update_ui_timeout
124
this.update_ui_timeout.repetition = FOREVER;
126
this.main_layout.show_all();
130
this.update_ui_timeout.reset();
134
/** Updates the window's account status info bars. */
135
public void update_account_status(Geary.Account.Status status,
138
Geary.Account? problem_source) {
139
// Only ever show one at a time. Offline is primary since
140
// nothing else can happen when offline. Service problems are
141
// secondary since auth and cert problems can't be resolved
142
// when the service isn't talking to the server. Cert problems
143
// are tertiary since you can't auth if you can't connect.
144
bool show_offline = false;
145
bool show_service = false;
146
bool show_cert = false;
147
bool show_auth = false;
149
if (!status.is_online()) {
151
} else if (status.has_service_problem()) {
153
} else if (has_cert_error) {
155
} else if (has_auth_error) {
159
if (show_service && this.service_problem_infobar == null) {
160
Geary.ClientService? service = (
161
problem_source.incoming.last_error != null
162
? problem_source.incoming
163
: problem_source.outgoing
165
this.service_problem_infobar = new MainWindowInfoBar.for_problem(
166
new Geary.ServiceProblemReport(
167
problem_source.information,
168
service.configuration,
169
service.last_error.thrown
172
this.service_problem_infobar.retry.connect(on_service_problem_retry);
174
show_infobar(this.service_problem_infobar);
177
this.offline_infobar.set_visible(show_offline);
178
this.cert_problem_infobar.set_visible(show_cert);
179
this.auth_problem_infobar.set_visible(show_auth);
180
update_infobar_frame();
183
/** Selects the given account and folder. */
184
public void show_folder(Geary.Folder folder) {
185
this.folder_list.select_folder(folder);
188
/** Selects the given account, folder and email. */
189
public void show_email(Geary.Folder folder, Geary.EmailIdentifier id) {
190
// XXX this is broken in the case of the email's folder not
191
// being currently selected and loaded, since changing folders
192
// and loading the email in the conversation monitor won't
193
// have completed until well after is it obtained
194
// below. However, it should work in the only case where this
195
// currently used, that is when a user clicks on a
196
// notification for new mail in the current folder.
198
Geary.App.Conversation? conversation =
199
this.conversations.get_by_email_identifier(id);
200
if (conversation != null) {
201
this.conversation_list_view.select_conversation(conversation);
205
/** Displays and focuses the search bar for the window. */
206
public void show_search_bar(string? text = null) {
207
this.search_bar.give_search_focus();
209
this.search_bar.set_search_text(text);
213
/** Displays an infobar in the window. */
214
public void show_infobar(MainWindowInfoBar info_bar) {
215
this.info_bar_container.add(info_bar);
216
this.info_bar_frame.show();
219
/** Displays a composer addressed to a specific email address. */
220
public void open_composer_for_mailbox(Geary.RFC822.MailboxAddress to) {
221
Application.Controller controller = this.application.controller;
222
ComposerWidget composer = new ComposerWidget(
223
this.application, this.current_folder.account, null, NEW_MESSAGE
225
composer.to = to.to_full_display();
226
controller.add_composer(composer);
227
show_composer(composer);
228
composer.load.begin(null, null, null);
231
/** Displays a composer in the window if possible, else in a new window. */
232
public void show_composer(ComposerWidget composer) {
233
if (this.has_composer) {
234
composer.state = ComposerWidget.ComposerState.DETACHED;
235
new ComposerWindow(composer, this.application);
237
this.conversation_viewer.do_compose(composer);
238
get_action(Application.Controller.ACTION_FIND_IN_CONVERSATION).set_enabled(false);
243
* Closes any open composers after prompting the user.
245
* Returns true if none were open or the user approved closing
248
public bool close_composer() {
250
ComposerWidget? composer = this.conversation_viewer.current_composer;
251
if (composer != null) {
252
switch (composer.should_close()) {
265
private void load_config(Configuration config) {
266
// This code both loads AND saves the pane positions with live updating. This is more
267
// resilient against crashes because the value in dconf changes *immediately*, and
268
// stays saved in the event of a crash.
269
config.bind(Configuration.MESSAGES_PANE_POSITION_KEY, this.conversations_paned, "position");
270
config.bind(Configuration.WINDOW_WIDTH_KEY, this, "window-width");
271
config.bind(Configuration.WINDOW_HEIGHT_KEY, this, "window-height");
272
config.bind(Configuration.WINDOW_MAXIMIZE_KEY, this, "window-maximized");
274
if (config.folder_list_pane_position_horizontal == -1) {
275
config.folder_list_pane_position_horizontal = config.folder_list_pane_position_old;
276
config.messages_pane_position += config.folder_list_pane_position_old;
278
config.settings.changed[Configuration.FOLDER_LIST_PANE_HORIZONTAL_KEY]
279
.connect(on_change_orientation);
282
private void restore_saved_window_state() {
283
Gdk.Display? display = Gdk.Display.get_default();
284
if (display != null) {
285
Gdk.Monitor? monitor = display.get_primary_monitor();
286
if (monitor == null) {
287
monitor = display.get_monitor_at_point(1, 1);
289
if (monitor != null &&
290
this.window_width <= monitor.geometry.width &&
291
this.window_height <= monitor.geometry.height) {
292
set_default_size(this.window_width, this.window_height);
295
this.window_position = Gtk.WindowPosition.CENTER;
296
if (this.window_maximized) {
301
// Called on [un]maximize and possibly others. Save maximized state
302
// for the next start.
303
public override bool window_state_event(Gdk.EventWindowState event) {
304
if ((event.new_window_state & Gdk.WindowState.WITHDRAWN) == 0) {
306
(event.new_window_state & Gdk.WindowState.MAXIMIZED) != 0
308
if (this.window_maximized != maximized) {
309
this.window_maximized = maximized;
312
return base.window_state_event(event);
315
// Called on window resize. Save window size for the next start.
316
public override void size_allocate(Gtk.Allocation allocation) {
317
base.size_allocate(allocation);
319
if (!this.window_maximized) {
320
Gdk.Display? display = get_display();
321
Gdk.Window? window = get_window();
322
if (display != null && window != null) {
323
Gdk.Monitor monitor = display.get_monitor_at_window(window);
325
// Get the size via ::get_size instead of the
326
// allocation so that the window isn't ever-expanding.
329
get_size(out width, out height);
331
// Only store if the values have changed and are
332
// reasonable-looking.
333
if (this.window_width != width &&
334
width > 0 && width <= monitor.geometry.width) {
335
this.window_width = width;
337
if (this.window_height != height &&
338
height > 0 && height <= monitor.geometry.height) {
339
this.window_height = height;
345
public void add_notification(InAppNotification notification) {
346
this.overlay.add_overlay(notification);
350
private void set_styling() {
351
Gtk.CssProvider provider = new Gtk.CssProvider();
352
Gtk.StyleContext.add_provider_for_screen(Gdk.Display.get_default().get_default_screen(),
353
provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION);
355
if (_PROFILE != "") {
356
Gtk.StyleContext ctx = this.get_style_context();
357
ctx.add_class("devel");
360
provider.parsing_error.connect((section, error) => {
361
uint start = section.get_start_line();
362
uint end = section.get_end_line();
364
debug("Error parsing css on line %u: %s", start, error.message);
366
debug("Error parsing css on lines %u-%u: %s", start, end, error.message);
369
File file = File.new_for_uri(@"resource:///org/gnome/Geary/geary.css");
370
provider.load_from_file(file);
372
error("Could not load CSS: %s", e.message);
376
private void setup_layout(Configuration config) {
377
this.conversation_list_view = new ConversationListView(this);
378
this.conversation_list_view.load_more.connect(on_load_more);
380
this.conversation_viewer = new ConversationViewer(
381
this.application.config
384
this.main_toolbar = new MainToolbar(config);
385
this.main_toolbar.bind_property("search-open", this.search_bar, "search-mode-enabled",
386
BindingFlags.SYNC_CREATE | BindingFlags.BIDIRECTIONAL);
387
this.main_toolbar.bind_property("find-open", this.conversation_viewer.conversation_find_bar,
388
"search-mode-enabled", BindingFlags.SYNC_CREATE | BindingFlags.BIDIRECTIONAL);
389
if (config.desktop_environment == Configuration.DesktopEnvironment.UNITY) {
390
BindingTransformFunc title_func = (binding, source, ref target) => {
391
string folder = current_folder != null ? current_folder.get_display_name() + " " : "";
392
string account = main_toolbar.account != null ? "(%s)".printf(main_toolbar.account) : "";
394
target = "%s%s - %s".printf(folder, account, GearyApplication.NAME);
398
bind_property("current-folder", this, "title", BindingFlags.SYNC_CREATE, (owned) title_func);
399
main_toolbar.bind_property("account", this, "title", BindingFlags.SYNC_CREATE, (owned) title_func);
400
main_layout.pack_start(main_toolbar, false, true, 0);
402
main_toolbar.show_close_button = true;
403
set_titlebar(main_toolbar);
407
this.search_bar_box.pack_start(this.search_bar, false, false, 0);
409
this.folder_list_scrolled.add(this.folder_list);
411
this.conversation_list_scrolled.add(this.conversation_list_view);
412
// Conversation viewer
413
this.conversations_paned.pack2(this.conversation_viewer, true, true);
416
this.status_bar.set_size_request(-1, STATUS_BAR_HEIGHT);
417
this.status_bar.set_border_width(2);
418
this.spinner.set_size_request(STATUS_BAR_HEIGHT - 2, -1);
419
this.spinner.set_progress_monitor(progress_monitor);
420
this.status_bar.add(this.spinner);
423
// Returns true when there's a conversation list scrollbar visible, i.e. the list is tall
424
// enough to need one. Otherwise returns false.
425
public bool conversation_list_has_scrollbar() {
426
Gtk.Scrollbar? scrollbar = this.conversation_list_scrolled.get_vscrollbar() as Gtk.Scrollbar;
427
return scrollbar != null && scrollbar.get_visible();
431
public override bool key_press_event(Gdk.EventKey event) {
432
check_shift_event(event);
434
/* Ensure that single-key command (SKC) shortcuts don't
435
* interfere with text input.
437
* The default GtkWindow::key_press_event implementation calls
438
* gtk_window_activate_key -- which would activate the SKC,
439
* before calling gtk_window_propagate_key_event -- which
440
* would send the event to any focused text entry control, so
441
* we need to override that. A quick hack is to just call
442
* gtk_window_propagate_key_event here, then chain up. But
443
* that means two calls to that method for every key press,
444
* which in the worst case means all widgets in the focus
445
* chain would be consulted to handle the press twice, which
448
* Worse however, is that due to WK2 Bug 136430[0], WebView
449
* instances duplicate any key events they don't handle. For
450
* the editor, that means simple key presses like 'a' will
451
* only result in a single event, since the web view adds the
452
* letter to the document. But if not handled, e.g. when the
453
* user presses Shift, Ctrl, or similar, then it also produces
454
* a second event. Combined with the
455
* gtk_window_propagate_key_event above, this leads to a
456
* cambrian explosion of key events - an exponential number
457
* are generated, which is bad. This problem also applies to
458
* ConversationWebView instances, since none of them handle
461
* The work around here is completely override the default
462
* implementation to reverse it. So if something related to
463
* key handling breaks in the future, this might be a good
464
* place to start looking. Better alternatives welcome.
466
* [0] - <https://bugs.webkit.org/show_bug.cgi?id=136430>
469
bool handled = false;
470
Gdk.ModifierType state = (
471
event.state & Gtk.accelerator_get_default_mod_mask()
473
if (state > 0 && state != Gdk.ModifierType.SHIFT_MASK) {
474
// Have a modifier held down (Ctrl, Alt, etc) that is used
475
// as an accelerator so we don't need to worry about SKCs,
476
// and the key press can be handled normally. Can't do
477
// this with Shift though since that will stop chars being
478
// typed in the composer that conflict with accels, like
480
handled = base.key_press_event(event);
482
// No modifier used as an accelerator is down, so kluge
483
// input handling to make SKCs work per the above.
484
handled = propagate_key_event(event);
486
handled = activate_key(event);
489
handled = Gtk.bindings_activate_event(this, event);
496
public override bool key_release_event(Gdk.EventKey event) {
497
check_shift_event(event);
498
return base.key_release_event(event);
501
public void folder_selected(Geary.Folder? folder,
502
GLib.Cancellable? cancellable) {
503
if (this.current_folder != null) {
504
this.progress_monitor.remove(this.current_folder.opening_monitor);
505
this.current_folder.properties.notify.disconnect(update_headerbar);
506
close_conversation_monitor();
509
this.current_folder = folder;
511
if (folder != null) {
512
this.progress_monitor.add(folder.opening_monitor);
513
folder.properties.notify.connect(update_headerbar);
514
open_conversation_monitor.begin(cancellable);
520
private void update_ui() {
521
// Only update if we haven't done so within the last while
522
int64 now = GLib.get_monotonic_time() / (1000 * 1000);
523
if (this.update_ui_last + UPDATE_UI_INTERVAL < now) {
524
this.update_ui_last = now;
526
if (this.conversation_viewer.current_list != null) {
527
this.conversation_viewer.current_list.update_display();
530
ConversationListStore? list_store =
531
this.conversation_list_view.get_model() as ConversationListStore;
532
if (list_store != null) {
533
list_store.update_display();
538
private async void open_conversation_monitor(GLib.Cancellable cancellable) {
539
this.conversations = new Geary.App.ConversationMonitor(
541
Geary.Folder.OpenFlags.NO_DELAY,
542
// Include fields for the conversation viewer as well so
543
// conversations can be displayed without having to go
545
ConversationListStore.REQUIRED_FIELDS |
546
ConversationListBox.REQUIRED_FIELDS |
547
ConversationEmail.REQUIRED_FOR_CONSTRUCT,
548
MIN_CONVERSATION_COUNT
551
this.conversations.scan_completed.connect(on_scan_completed);
552
this.conversations.scan_error.connect(on_scan_error);
554
this.conversations.scan_completed.connect(
555
on_conversation_count_changed
557
this.conversations.conversations_added.connect(
558
on_conversation_count_changed
560
this.conversations.conversations_removed.connect(
561
on_conversation_count_changed
564
ConversationListStore new_model = new ConversationListStore(
567
this.progress_monitor.add(new_model.preview_monitor);
568
this.progress_monitor.add(conversations.progress_monitor);
569
this.conversation_list_view.set_model(new_model);
571
// Work on a local copy since the main window's copy may
572
// change if a folder is selected while closing.
573
Geary.App.ConversationMonitor conversations = this.conversations;
574
conversations.start_monitoring_async.begin(
578
conversations.start_monitoring_async.end(res);
579
} catch (Error err) {
580
Geary.AccountInformation account =
581
conversations.base_folder.account.information;
582
this.application.controller.report_problem(
583
new Geary.ServiceProblemReport(account, account.incoming, err)
590
private void close_conversation_monitor() {
591
ConversationListStore? old_model =
592
this.conversation_list_view.get_model();
593
if (old_model != null) {
594
this.progress_monitor.remove(old_model.preview_monitor);
595
this.progress_monitor.remove(old_model.conversations.progress_monitor);
598
this.conversations.scan_completed.disconnect(on_scan_completed);
599
this.conversations.scan_error.disconnect(on_scan_error);
601
this.conversations.scan_completed.disconnect(
602
on_conversation_count_changed
604
this.conversations.conversations_added.disconnect(
605
on_conversation_count_changed
607
this.conversations.conversations_removed.disconnect(
608
on_conversation_count_changed
611
// Work on a local copy since the main window's copy may
612
// change if a folder is selected while closing.
613
Geary.App.ConversationMonitor conversations = this.conversations;
614
conversations.stop_monitoring_async.begin(
618
conversations.stop_monitoring_async.end(res);
619
} catch (Error err) {
621
"Error closing conversation monitor %s: %s",
622
this.conversations.base_folder.to_string(),
629
this.conversations = null;
632
private void load_more() {
633
if (this.conversations != null) {
634
this.conversations.min_window_count += MIN_CONVERSATION_COUNT;
638
private void on_conversation_count_changed() {
639
// Only update the UI if we don't currently have a composer,
640
// so we don't clobber it
641
if (!this.has_composer) {
642
if (this.conversations.size == 0) {
643
// Let the user know if there's no available conversations
644
if (this.current_folder is Geary.SearchFolder) {
645
this.conversation_viewer.show_empty_search();
647
this.conversation_viewer.show_empty_folder();
649
this.application.controller.enable_message_buttons(false);
651
// When not doing autoselect, we never get
652
// conversations_selected firing from the convo list,
653
// so we need to stop the loading spinner here.
654
if (!this.application.config.autoselect &&
655
this.conversation_list_view.get_selection().count_selected_rows() == 0) {
656
this.conversation_viewer.show_none_selected();
657
this.application.controller.enable_message_buttons(false);
663
private void on_account_available(Geary.AccountInformation account) {
665
this.progress_monitor.add(this.application.engine.get_account_instance(account).opening_monitor);
666
this.progress_monitor.add(this.application.engine.get_account_instance(account).sending_monitor);
668
debug("Could not access account progress monitors: %s", e.message);
672
private void on_account_unavailable(Geary.AccountInformation account) {
674
this.progress_monitor.remove(this.application.engine.get_account_instance(account).opening_monitor);
675
this.progress_monitor.remove(this.application.engine.get_account_instance(account).sending_monitor);
677
debug("Could not access account progress monitors: %s", e.message);
681
private void on_change_orientation() {
682
bool horizontal = this.application.config.folder_list_pane_horizontal;
685
if (this.status_bar.parent != null) {
686
this.status_bar.parent.remove(status_bar);
690
GLib.Settings.unbind(this.folder_paned, "position");
691
this.folder_paned.orientation = horizontal ? Gtk.Orientation.HORIZONTAL :
692
Gtk.Orientation.VERTICAL;
694
int folder_list_width =
695
this.application.config.folder_list_pane_position_horizontal;
698
this.conversations_paned.position += folder_list_width;
699
this.folder_box.pack_start(status_bar, false, false);
702
this.conversations_paned.position -= folder_list_width;
703
this.conversation_box.pack_start(status_bar, false, false);
706
this.application.config.bind(
707
horizontal ? Configuration.FOLDER_LIST_PANE_POSITION_HORIZONTAL_KEY
708
: Configuration.FOLDER_LIST_PANE_POSITION_VERTICAL_KEY,
709
this.folder_paned, "position");
712
private void update_headerbar() {
713
if (this.current_folder == null) {
714
this.main_toolbar.account = null;
715
this.main_toolbar.folder = null;
720
this.main_toolbar.account =
721
this.current_folder.account.information.display_name;
723
/// Current folder's name followed by its unread count, i.e. "Inbox (42)"
724
// except for Drafts and Outbox, where we show total count
726
switch (this.current_folder.special_folder_type) {
727
case Geary.SpecialFolderType.DRAFTS:
728
case Geary.SpecialFolderType.OUTBOX:
729
count = this.current_folder.properties.email_total;
733
count = this.current_folder.properties.email_unread;
738
this.main_toolbar.folder = _("%s (%d)").printf(this.current_folder.get_display_name(), count);
740
this.main_toolbar.folder = this.current_folder.get_display_name();
743
private void update_infobar_frame() {
744
// Ensure the info bar frame is shown only when it has visible
746
bool show_frame = false;
747
this.info_bar_container.foreach((child) => {
752
this.info_bar_frame.set_visible(show_frame);
755
private inline void check_shift_event(Gdk.EventKey event) {
756
// FIXME: it's possible the user will press two shift keys. We want
757
// the shift key to report as released when they release ALL of them.
758
// There doesn't seem to be an easy way to do this in Gdk.
759
if (event.keyval == Gdk.Key.Shift_L || event.keyval == Gdk.Key.Shift_R) {
760
Gtk.Widget? focus = get_focus();
762
(!(focus is Gtk.Entry) && !(focus is ComposerWebView))) {
763
this.is_shift_down = (event.type == Gdk.EventType.KEY_PRESS);
764
this.main_toolbar.update_trash_button(
765
!this.is_shift_down &&
766
current_folder_supports_trash()
768
on_shift_key(this.is_shift_down);
773
private SimpleAction get_action(string name) {
774
return (SimpleAction) lookup_action(name);
777
private bool current_folder_supports_trash() {
778
Geary.Folder? current = this.current_folder;
781
current.special_folder_type != TRASH &&
782
!current_folder.properties.is_local_only &&
783
(current_folder as Geary.FolderSupport.Move) != null
787
private void on_scan_completed(Geary.App.ConversationMonitor monitor) {
788
// Done scanning. Check if we have enough messages to fill
789
// the conversation list; if not, trigger a load_more();
791
!conversation_list_has_scrollbar() &&
792
monitor == this.conversations &&
793
monitor.can_load_more) {
794
debug("Not enough messages, loading more for folder %s",
795
this.current_folder.to_string());
800
private void on_scan_error(Geary.App.ConversationMonitor monitor,
802
Geary.AccountInformation account =
803
monitor.base_folder.account.information;
804
this.application.controller.report_problem(
805
new Geary.ServiceProblemReport(account, account.incoming, err)
809
private void on_load_more() {
814
private void on_map() {
815
this.update_ui_timeout.start();
820
private void on_unmap() {
821
this.update_ui_timeout.reset();
825
private bool on_focus_event() {
831
private bool on_delete_event() {
832
if (this.application.config.startup_notifications) {
833
if (this.application.controller.close_composition_windows(true)) {
837
this.application.exit();
839
return Gdk.EVENT_STOP;
843
private void on_offline_infobar_response() {
844
this.offline_infobar.hide();
845
update_infobar_frame();
848
private void on_service_problem_retry() {
849
this.service_problem_infobar = null;
850
retry_service_problem(Geary.ClientService.Status.CONNECTION_FAILED);
854
private void on_cert_problem_retry() {
855
this.cert_problem_infobar.hide();
856
update_infobar_frame();
857
retry_service_problem(Geary.ClientService.Status.TLS_VALIDATION_FAILED);
861
private void on_auth_problem_retry() {
862
this.auth_problem_infobar.hide();
863
update_infobar_frame();
864
retry_service_problem(Geary.ClientService.Status.AUTHENTICATION_FAILED);
868
private void on_info_bar_container_remove() {
869
update_infobar_frame();
872
private void on_update_ui_timeout() {