~hkdb/geary/disco-3.34.0

« back to all changes in this revision

Viewing changes to src/client/components/main-window.vala

  • Committer: hkdb
  • Date: 2019-09-27 11:28:53 UTC
  • Revision ID: hkdb@3df.io-20190927112853-x02njxcww51q9jfp
Tags: upstream-3.34.0-disco
ImportĀ upstreamĀ versionĀ 3.34.0-disco

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
/*
 
2
 * Copyright 2016 Software Freedom Conservancy Inc.
 
3
 * Copyright 2016, 2019 Michael Gratton <mike@vee.net>
 
4
 *
 
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.
 
7
 */
 
8
 
 
9
[GtkTemplate (ui = "/org/gnome/Geary/main-window.ui")]
 
10
public class MainWindow : Gtk.ApplicationWindow, Geary.BaseInterface {
 
11
 
 
12
 
 
13
    private const int STATUS_BAR_HEIGHT = 18;
 
14
    private const int UPDATE_UI_INTERVAL = 60;
 
15
    private const int MIN_CONVERSATION_COUNT = 50;
 
16
 
 
17
 
 
18
    public new GearyApplication application {
 
19
        get { return (GearyApplication) base.get_application(); }
 
20
        set { base.set_application(value); }
 
21
    }
 
22
 
 
23
    /** Currently selected folder, null if none selected */
 
24
    public Geary.Folder? current_folder { get; private set; default = null; }
 
25
 
 
26
    /** Conversations for the current folder, null if none selected */
 
27
    public Geary.App.ConversationMonitor? conversations {
 
28
        get; private set; default = null;
 
29
    }
 
30
 
 
31
    /** Determines if a composer is currently open in this window. */
 
32
    public bool has_composer {
 
33
        get {
 
34
            return (this.conversation_viewer.current_composer != null);
 
35
        }
 
36
    }
 
37
 
 
38
    /** Specifies if the Shift key is currently being held. */
 
39
    public bool is_shift_down { get; private set; default = false; }
 
40
 
 
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; }
 
45
 
 
46
    // Widget descendants
 
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();
 
54
 
 
55
    private Geary.AggregateProgressMonitor progress_monitor = new Geary.AggregateProgressMonitor();
 
56
    private Geary.TimeoutManager update_ui_timeout;
 
57
    private int64 update_ui_last = 0;
 
58
 
 
59
 
 
60
    [GtkChild]
 
61
    private Gtk.Box main_layout;
 
62
    [GtkChild]
 
63
    private Gtk.Box search_bar_box;
 
64
    [GtkChild]
 
65
    private Gtk.Paned folder_paned;
 
66
    [GtkChild]
 
67
    private Gtk.Paned conversations_paned;
 
68
    [GtkChild]
 
69
    private Gtk.Box folder_box;
 
70
    [GtkChild]
 
71
    private Gtk.ScrolledWindow folder_list_scrolled;
 
72
    [GtkChild]
 
73
    private Gtk.Box conversation_box;
 
74
    [GtkChild]
 
75
    private Gtk.ScrolledWindow conversation_list_scrolled;
 
76
    [GtkChild]
 
77
    private Gtk.Overlay overlay;
 
78
 
 
79
    // This is a frame so users can use F6/Shift-F6 to get to it
 
80
    [GtkChild]
 
81
    private Gtk.Frame info_bar_frame;
 
82
 
 
83
    [GtkChild]
 
84
    private Gtk.Grid info_bar_container;
 
85
 
 
86
    [GtkChild]
 
87
    private Gtk.InfoBar offline_infobar;
 
88
 
 
89
    [GtkChild]
 
90
    private Gtk.InfoBar cert_problem_infobar;
 
91
 
 
92
    [GtkChild]
 
93
    private Gtk.InfoBar auth_problem_infobar;
 
94
 
 
95
    private MainWindowInfoBar? service_problem_infobar = null;
 
96
 
 
97
    /** Fired when the user requests an account status be retried. */
 
98
    public signal void retry_service_problem(Geary.ClientService.Status problem);
 
99
 
 
100
    /** Fired when the shift key is pressed or released. */
 
101
    public signal void on_shift_key(bool pressed);
 
102
 
 
103
 
 
104
    public MainWindow(GearyApplication application) {
 
105
        Object(
 
106
            application: application,
 
107
            show_menubar: false
 
108
        );
 
109
        base_ref();
 
110
 
 
111
        load_config(application.config);
 
112
        restore_saved_window_state();
 
113
 
 
114
        this.application.engine.account_available.connect(on_account_available);
 
115
        this.application.engine.account_unavailable.connect(on_account_unavailable);
 
116
 
 
117
        set_styling();
 
118
        setup_layout(application.config);
 
119
        on_change_orientation();
 
120
 
 
121
        this.update_ui_timeout = new Geary.TimeoutManager.seconds(
 
122
            UPDATE_UI_INTERVAL, on_update_ui_timeout
 
123
        );
 
124
        this.update_ui_timeout.repetition = FOREVER;
 
125
 
 
126
        this.main_layout.show_all();
 
127
    }
 
128
 
 
129
    ~MainWindow() {
 
130
        this.update_ui_timeout.reset();
 
131
        base_unref();
 
132
    }
 
133
 
 
134
    /** Updates the window's account status info bars. */
 
135
    public void update_account_status(Geary.Account.Status status,
 
136
                                      bool has_auth_error,
 
137
                                      bool has_cert_error,
 
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;
 
148
 
 
149
        if (!status.is_online()) {
 
150
            show_offline = true;
 
151
        } else if (status.has_service_problem()) {
 
152
            show_service = true;
 
153
        } else if (has_cert_error) {
 
154
            show_cert = true;
 
155
        } else if (has_auth_error) {
 
156
            show_auth = true;
 
157
        }
 
158
 
 
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
 
164
            );
 
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
 
170
                )
 
171
            );
 
172
            this.service_problem_infobar.retry.connect(on_service_problem_retry);
 
173
 
 
174
            show_infobar(this.service_problem_infobar);
 
175
        }
 
176
 
 
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();
 
181
    }
 
182
 
 
183
    /** Selects the given account and folder. */
 
184
    public void show_folder(Geary.Folder folder) {
 
185
        this.folder_list.select_folder(folder);
 
186
    }
 
187
 
 
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.
 
197
        show_folder(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);
 
202
        }
 
203
    }
 
204
 
 
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();
 
208
        if (text != null) {
 
209
            this.search_bar.set_search_text(text);
 
210
        }
 
211
    }
 
212
 
 
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();
 
217
    }
 
218
 
 
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
 
224
        );
 
225
        composer.to = to.to_full_display();
 
226
        controller.add_composer(composer);
 
227
        show_composer(composer);
 
228
        composer.load.begin(null, null, null);
 
229
    }
 
230
 
 
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);
 
236
        } else {
 
237
            this.conversation_viewer.do_compose(composer);
 
238
            get_action(Application.Controller.ACTION_FIND_IN_CONVERSATION).set_enabled(false);
 
239
        }
 
240
    }
 
241
 
 
242
    /**
 
243
     * Closes any open composers after prompting the user.
 
244
     *
 
245
     * Returns true if none were open or the user approved closing
 
246
     * them.
 
247
     */
 
248
    public bool close_composer() {
 
249
        bool closed = true;
 
250
        ComposerWidget? composer = this.conversation_viewer.current_composer;
 
251
        if (composer != null) {
 
252
            switch (composer.should_close()) {
 
253
            case DO_CLOSE:
 
254
                composer.close();
 
255
                break;
 
256
 
 
257
            case CANCEL_CLOSE:
 
258
                closed = false;
 
259
                break;
 
260
            }
 
261
        }
 
262
        return closed;
 
263
    }
 
264
 
 
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");
 
273
        // Update to layout
 
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;
 
277
        }
 
278
        config.settings.changed[Configuration.FOLDER_LIST_PANE_HORIZONTAL_KEY]
 
279
            .connect(on_change_orientation);
 
280
    }
 
281
 
 
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);
 
288
            }
 
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);
 
293
            }
 
294
        }
 
295
        this.window_position = Gtk.WindowPosition.CENTER;
 
296
        if (this.window_maximized) {
 
297
            maximize();
 
298
        }
 
299
    }
 
300
 
 
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) {
 
305
            bool maximized = (
 
306
                (event.new_window_state & Gdk.WindowState.MAXIMIZED) != 0
 
307
            );
 
308
            if (this.window_maximized != maximized) {
 
309
                this.window_maximized = maximized;
 
310
            }
 
311
        }
 
312
        return base.window_state_event(event);
 
313
    }
 
314
 
 
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);
 
318
 
 
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);
 
324
 
 
325
                // Get the size via ::get_size instead of the
 
326
                // allocation so that the window isn't ever-expanding.
 
327
                int width = 0;
 
328
                int height = 0;
 
329
                get_size(out width, out height);
 
330
 
 
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;
 
336
                }
 
337
                if (this.window_height != height &&
 
338
                    height > 0 && height <= monitor.geometry.height) {
 
339
                    this.window_height = height;
 
340
                }
 
341
            }
 
342
        }
 
343
    }
 
344
 
 
345
    public void add_notification(InAppNotification notification) {
 
346
        this.overlay.add_overlay(notification);
 
347
        notification.show();
 
348
    }
 
349
 
 
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);
 
354
 
 
355
        if (_PROFILE != "") {
 
356
            Gtk.StyleContext ctx = this.get_style_context();
 
357
            ctx.add_class("devel");
 
358
        }
 
359
 
 
360
        provider.parsing_error.connect((section, error) => {
 
361
            uint start = section.get_start_line();
 
362
            uint end = section.get_end_line();
 
363
            if (start == end)
 
364
                debug("Error parsing css on line %u: %s", start, error.message);
 
365
            else
 
366
                debug("Error parsing css on lines %u-%u: %s", start, end, error.message);
 
367
        });
 
368
        try {
 
369
            File file = File.new_for_uri(@"resource:///org/gnome/Geary/geary.css");
 
370
            provider.load_from_file(file);
 
371
        } catch (Error e) {
 
372
            error("Could not load CSS: %s", e.message);
 
373
        }
 
374
    }
 
375
 
 
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);
 
379
 
 
380
        this.conversation_viewer = new ConversationViewer(
 
381
            this.application.config
 
382
        );
 
383
 
 
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) : "";
 
393
 
 
394
                target = "%s%s - %s".printf(folder, account, GearyApplication.NAME);
 
395
 
 
396
                return true;
 
397
            };
 
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);
 
401
        } else {
 
402
            main_toolbar.show_close_button = true;
 
403
            set_titlebar(main_toolbar);
 
404
        }
 
405
 
 
406
        // Search bar
 
407
        this.search_bar_box.pack_start(this.search_bar, false, false, 0);
 
408
        // Folder list
 
409
        this.folder_list_scrolled.add(this.folder_list);
 
410
        // Conversation list
 
411
        this.conversation_list_scrolled.add(this.conversation_list_view);
 
412
        // Conversation viewer
 
413
        this.conversations_paned.pack2(this.conversation_viewer, true, true);
 
414
 
 
415
        // Status bar
 
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);
 
421
    }
 
422
 
 
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();
 
428
    }
 
429
 
 
430
    /** {@inheritDoc} */
 
431
    public override bool key_press_event(Gdk.EventKey event) {
 
432
        check_shift_event(event);
 
433
 
 
434
        /* Ensure that single-key command (SKC) shortcuts don't
 
435
         * interfere with text input.
 
436
         *
 
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
 
446
         * sucks.
 
447
         *
 
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
 
459
         * events.
 
460
         *
 
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.
 
465
         *
 
466
         * [0] - <https://bugs.webkit.org/show_bug.cgi?id=136430>
 
467
         */
 
468
 
 
469
        bool handled = false;
 
470
        Gdk.ModifierType state = (
 
471
            event.state & Gtk.accelerator_get_default_mod_mask()
 
472
        );
 
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
 
479
            // `!`.
 
480
            handled = base.key_press_event(event);
 
481
        } else {
 
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);
 
485
            if (!handled) {
 
486
                handled = activate_key(event);
 
487
            }
 
488
            if (!handled) {
 
489
                handled = Gtk.bindings_activate_event(this, event);
 
490
            }
 
491
        }
 
492
        return handled;
 
493
    }
 
494
 
 
495
    /** {@inheritDoc} */
 
496
    public override bool key_release_event(Gdk.EventKey event) {
 
497
        check_shift_event(event);
 
498
        return base.key_release_event(event);
 
499
    }
 
500
 
 
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();
 
507
        }
 
508
 
 
509
        this.current_folder = folder;
 
510
 
 
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);
 
515
        }
 
516
 
 
517
        update_headerbar();
 
518
    }
 
519
 
 
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;
 
525
 
 
526
            if (this.conversation_viewer.current_list != null) {
 
527
                this.conversation_viewer.current_list.update_display();
 
528
            }
 
529
 
 
530
            ConversationListStore? list_store =
 
531
                this.conversation_list_view.get_model() as ConversationListStore;
 
532
            if (list_store != null) {
 
533
                list_store.update_display();
 
534
            }
 
535
        }
 
536
    }
 
537
 
 
538
    private async void open_conversation_monitor(GLib.Cancellable cancellable) {
 
539
        this.conversations = new Geary.App.ConversationMonitor(
 
540
            this.current_folder,
 
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
 
544
            // back to the db
 
545
            ConversationListStore.REQUIRED_FIELDS |
 
546
            ConversationListBox.REQUIRED_FIELDS |
 
547
            ConversationEmail.REQUIRED_FOR_CONSTRUCT,
 
548
            MIN_CONVERSATION_COUNT
 
549
        );
 
550
 
 
551
        this.conversations.scan_completed.connect(on_scan_completed);
 
552
        this.conversations.scan_error.connect(on_scan_error);
 
553
 
 
554
        this.conversations.scan_completed.connect(
 
555
            on_conversation_count_changed
 
556
        );
 
557
        this.conversations.conversations_added.connect(
 
558
            on_conversation_count_changed
 
559
        );
 
560
        this.conversations.conversations_removed.connect(
 
561
            on_conversation_count_changed
 
562
        );
 
563
 
 
564
        ConversationListStore new_model = new ConversationListStore(
 
565
            this.conversations
 
566
        );
 
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);
 
570
 
 
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(
 
575
            cancellable,
 
576
            (obj, res) => {
 
577
                try {
 
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)
 
584
                    );
 
585
                }
 
586
            }
 
587
        );
 
588
    }
 
589
 
 
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);
 
596
        }
 
597
 
 
598
        this.conversations.scan_completed.disconnect(on_scan_completed);
 
599
        this.conversations.scan_error.disconnect(on_scan_error);
 
600
 
 
601
        this.conversations.scan_completed.disconnect(
 
602
            on_conversation_count_changed
 
603
        );
 
604
        this.conversations.conversations_added.disconnect(
 
605
            on_conversation_count_changed
 
606
        );
 
607
        this.conversations.conversations_removed.disconnect(
 
608
            on_conversation_count_changed
 
609
        );
 
610
 
 
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(
 
615
            null,
 
616
            (obj, res) => {
 
617
                try {
 
618
                    conversations.stop_monitoring_async.end(res);
 
619
                } catch (Error err) {
 
620
                    warning(
 
621
                        "Error closing conversation monitor %s: %s",
 
622
                        this.conversations.base_folder.to_string(),
 
623
                        err.message
 
624
                    );
 
625
                }
 
626
            }
 
627
        );
 
628
 
 
629
        this.conversations = null;
 
630
    }
 
631
 
 
632
    private void load_more() {
 
633
        if (this.conversations != null) {
 
634
            this.conversations.min_window_count += MIN_CONVERSATION_COUNT;
 
635
        }
 
636
    }
 
637
 
 
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();
 
646
                } else {
 
647
                    this.conversation_viewer.show_empty_folder();
 
648
                }
 
649
                this.application.controller.enable_message_buttons(false);
 
650
            } else {
 
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);
 
658
                }
 
659
            }
 
660
        }
 
661
    }
 
662
 
 
663
    private void on_account_available(Geary.AccountInformation account) {
 
664
        try {
 
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);
 
667
        } catch (Error e) {
 
668
            debug("Could not access account progress monitors: %s", e.message);
 
669
        }
 
670
    }
 
671
 
 
672
    private void on_account_unavailable(Geary.AccountInformation account) {
 
673
        try {
 
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);
 
676
        } catch (Error e) {
 
677
            debug("Could not access account progress monitors: %s", e.message);
 
678
        }
 
679
    }
 
680
 
 
681
    private void on_change_orientation() {
 
682
        bool horizontal = this.application.config.folder_list_pane_horizontal;
 
683
        bool initial = true;
 
684
 
 
685
        if (this.status_bar.parent != null) {
 
686
            this.status_bar.parent.remove(status_bar);
 
687
            initial = false;
 
688
        }
 
689
 
 
690
        GLib.Settings.unbind(this.folder_paned, "position");
 
691
        this.folder_paned.orientation = horizontal ? Gtk.Orientation.HORIZONTAL :
 
692
            Gtk.Orientation.VERTICAL;
 
693
 
 
694
        int folder_list_width =
 
695
            this.application.config.folder_list_pane_position_horizontal;
 
696
        if (horizontal) {
 
697
            if (!initial)
 
698
                this.conversations_paned.position += folder_list_width;
 
699
            this.folder_box.pack_start(status_bar, false, false);
 
700
        } else {
 
701
            if (!initial)
 
702
                this.conversations_paned.position -= folder_list_width;
 
703
            this.conversation_box.pack_start(status_bar, false, false);
 
704
        }
 
705
 
 
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");
 
710
    }
 
711
 
 
712
    private void update_headerbar() {
 
713
        if (this.current_folder == null) {
 
714
            this.main_toolbar.account = null;
 
715
            this.main_toolbar.folder = null;
 
716
 
 
717
            return;
 
718
        }
 
719
 
 
720
        this.main_toolbar.account =
 
721
            this.current_folder.account.information.display_name;
 
722
 
 
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
 
725
        int 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;
 
730
            break;
 
731
 
 
732
            default:
 
733
                count = this.current_folder.properties.email_unread;
 
734
            break;
 
735
        }
 
736
 
 
737
        if (count > 0)
 
738
            this.main_toolbar.folder = _("%s (%d)").printf(this.current_folder.get_display_name(), count);
 
739
        else
 
740
            this.main_toolbar.folder = this.current_folder.get_display_name();
 
741
    }
 
742
 
 
743
    private void update_infobar_frame() {
 
744
        // Ensure the info bar frame is shown only when it has visible
 
745
        // children
 
746
        bool show_frame = false;
 
747
        this.info_bar_container.foreach((child) => {
 
748
                if (child.visible) {
 
749
                    show_frame = true;
 
750
                }
 
751
            });
 
752
        this.info_bar_frame.set_visible(show_frame);
 
753
    }
 
754
 
 
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();
 
761
            if (focus == null ||
 
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()
 
767
                );
 
768
                on_shift_key(this.is_shift_down);
 
769
            }
 
770
        }
 
771
    }
 
772
 
 
773
    private SimpleAction get_action(string name) {
 
774
        return (SimpleAction) lookup_action(name);
 
775
    }
 
776
 
 
777
    private bool current_folder_supports_trash() {
 
778
        Geary.Folder? current = this.current_folder;
 
779
        return (
 
780
            current != null &&
 
781
            current.special_folder_type != TRASH &&
 
782
            !current_folder.properties.is_local_only &&
 
783
            (current_folder as Geary.FolderSupport.Move) != null
 
784
        );
 
785
    }
 
786
 
 
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();
 
790
        if (is_visible() &&
 
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());
 
796
            load_more();
 
797
        }
 
798
    }
 
799
 
 
800
    private void on_scan_error(Geary.App.ConversationMonitor monitor,
 
801
                               GLib.Error err) {
 
802
        Geary.AccountInformation account =
 
803
            monitor.base_folder.account.information;
 
804
        this.application.controller.report_problem(
 
805
            new Geary.ServiceProblemReport(account, account.incoming, err)
 
806
        );
 
807
    }
 
808
 
 
809
    private void on_load_more() {
 
810
        load_more();
 
811
    }
 
812
 
 
813
    [GtkCallback]
 
814
    private void on_map() {
 
815
        this.update_ui_timeout.start();
 
816
        update_ui();
 
817
    }
 
818
 
 
819
    [GtkCallback]
 
820
    private void on_unmap() {
 
821
        this.update_ui_timeout.reset();
 
822
    }
 
823
 
 
824
    [GtkCallback]
 
825
    private bool on_focus_event() {
 
826
        on_shift_key(false);
 
827
        return false;
 
828
    }
 
829
 
 
830
    [GtkCallback]
 
831
    private bool on_delete_event() {
 
832
        if (this.application.config.startup_notifications) {
 
833
            if (this.application.controller.close_composition_windows(true)) {
 
834
                hide();
 
835
            }
 
836
        } else {
 
837
            this.application.exit();
 
838
        }
 
839
        return Gdk.EVENT_STOP;
 
840
    }
 
841
 
 
842
    [GtkCallback]
 
843
    private void on_offline_infobar_response() {
 
844
        this.offline_infobar.hide();
 
845
        update_infobar_frame();
 
846
    }
 
847
 
 
848
    private void on_service_problem_retry() {
 
849
        this.service_problem_infobar = null;
 
850
        retry_service_problem(Geary.ClientService.Status.CONNECTION_FAILED);
 
851
    }
 
852
 
 
853
    [GtkCallback]
 
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);
 
858
    }
 
859
 
 
860
    [GtkCallback]
 
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);
 
865
    }
 
866
 
 
867
    [GtkCallback]
 
868
    private void on_info_bar_container_remove() {
 
869
        update_infobar_frame();
 
870
    }
 
871
 
 
872
    private void on_update_ui_timeout() {
 
873
        update_ui();
 
874
    }
 
875
 
 
876
}