geary-application.vala 17.6 KB
Newer Older
1
/* Copyright 2016 Software Freedom Conservancy Inc.
2 3
 *
 * This software is licensed under the GNU Lesser General Public License
4
 * (version 2.1 or later).  See the COPYING file in this distribution.
5 6
 */

7 8 9 10
// Defined by CMake build script.
extern const string _INSTALL_PREFIX;
extern const string _GSETTINGS_DIR;
extern const string _SOURCE_ROOT_DIR;
11
extern const string _BUILD_ROOT_DIR;
Charles Lindsay's avatar
Charles Lindsay committed
12
extern const string GETTEXT_PACKAGE;
13

14 15 16
/**
 * The interface between Geary and the desktop environment.
 */
Charles Lindsay's avatar
Charles Lindsay committed
17
public class GearyApplication : Gtk.Application {
18
    public const string NAME = "Geary";
19
    public const string PRGNAME = "geary";
20
    public const string APP_ID = "org.gnome.Geary";
21
    public const string DESCRIPTION = _("Send and receive email");
22 23
    public const string COPYRIGHT_1 = _("Copyright 2016 Software Freedom Conservancy Inc.");
    public const string COPYRIGHT_2 = _("Copyright 2016-2017 Geary Development Team.");
24 25
    public const string WEBSITE = "https://wiki.gnome.org/Apps/Geary";
    public const string WEBSITE_LABEL = _("Visit the Geary web site");
Charles Lindsay's avatar
Charles Lindsay committed
26
    public const string BUGREPORT = "https://wiki.gnome.org/Apps/Geary/ReportingABug";
27 28

    public const string VERSION = Geary.Version.GEARY_VERSION;
29 30 31
    public const string INSTALL_PREFIX = _INSTALL_PREFIX;
    public const string GSETTINGS_DIR = _GSETTINGS_DIR;
    public const string SOURCE_ROOT_DIR = _SOURCE_ROOT_DIR;
32 33
    public const string BUILD_ROOT_DIR = _BUILD_ROOT_DIR;

34
    public const string[] AUTHORS = {
35
        "Jim Nelson <jim@yorba.org>",
36
        "Eric Gregory <eric@yorba.org>",
Adam Dingle's avatar
Adam Dingle committed
37
        "Nate Lillich <nate@yorba.org>",
38
        "Matthew Pirocchi <matthew@yorba.org>",
39
        "Charles Lindsay <chaz@yorba.org>",
40
        "Robert Schroll <rschroll@gmail.com>",
Michael Gratton's avatar
Michael Gratton committed
41
        "Michael Gratton <mike@vee.net>",
42
        null
43
    };
44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60

    private const string ACTION_ABOUT = "about";
    private const string ACTION_ACCOUNTS = "accounts";
    private const string ACTION_COMPOSE = "compose";
    private const string ACTION_MAILTO = "mailto";
    private const string ACTION_HELP = "help";
    private const string ACTION_PREFERENCES = "preferences";
    private const string ACTION_QUIT = "quit";

    private const ActionEntry[] action_entries = {
        {ACTION_ABOUT, on_activate_about},
        {ACTION_ACCOUNTS, on_activate_accounts},
        {ACTION_COMPOSE, on_activate_compose},
        {ACTION_MAILTO, on_activate_mailto, "s"},
        {ACTION_HELP, on_activate_help},
        {ACTION_PREFERENCES, on_activate_preferences},
        {ACTION_QUIT, on_activate_quit},
Charles Lindsay's avatar
Charles Lindsay committed
61
    };
62

63 64
    private const int64 USEC_PER_SEC = 1000000;
    private const int64 FORCE_SHUTDOWN_USEC = 5 * USEC_PER_SEC;
65 66


67
    [Deprecated]
Charles Lindsay's avatar
Charles Lindsay committed
68
    public static GearyApplication instance {
69
        get { return _instance; }
Charles Lindsay's avatar
Charles Lindsay committed
70
        private set {
71 72 73
            // Ensure singleton behavior.
            assert (_instance == null);
            _instance = value;
74 75
        }
    }
76 77 78
    private static GearyApplication _instance = null;


Charles Lindsay's avatar
Charles Lindsay committed
79
    /**
80
     * The global UI controller for this app instance.
Charles Lindsay's avatar
Charles Lindsay committed
81
     */
82 83 84 85
    public GearyController controller {
        get;
        private set;
        default = new GearyController(this);
Charles Lindsay's avatar
Charles Lindsay committed
86
    }
87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104

    /**
     * The global email subsystem controller for this app instance.
     */
    public Geary.Engine engine {
        get {
            // XXX We should be managing the engine's lifecycle here,
            // but until that happens provide this property to
            // encourage access via the application anyway
            return Geary.Engine.instance;
        }
    }

    /**
     * The user's desktop-wide settings for the application.
     */
    public Configuration config { get; private set; }

105 106 107 108 109 110 111 112 113 114 115
    /**
     * Determines if Geary configured to run as as a background service.
     *
     * If this returns `true`, then the primary application instance
     * will continue to run in the background after the last window is
     * closed, instead of existing as usual.
     */
    public bool is_background_service {
        get { return Args.hidden_startup || this.config.startup_notifications; }
    }

116 117 118
    public Gtk.ActionGroup actions {
        get; private set; default = new Gtk.ActionGroup("GearyActionGroup");
    }
119

120 121
    public Gtk.UIManager ui_manager {
        get; private set; default = new Gtk.UIManager();
Eric Gregory's avatar
Eric Gregory committed
122
    }
123

Charles Lindsay's avatar
Charles Lindsay committed
124
    private string bin;
125
    private File exec_dir;
Charles Lindsay's avatar
Charles Lindsay committed
126 127
    private bool exiting_fired = false;
    private int exitcode = 0;
128
    private bool is_destroyed = false;
129

130 131 132 133 134 135 136 137 138 139 140 141

    /**
     * Signal that is activated when 'exit' is called, but before the application actually exits.
     *
     * To cancel an exit, a callback should return GearyApplication.cancel_exit(). To procede with
     * an exit, a callback should return true.
     */
    public virtual signal bool exiting(bool panicked) {
        return true;
    }


142
    public GearyApplication() {
143
        Object(
144
            application_id: APP_ID
145
        );
146
        _instance = this;
147
    }
148

Charles Lindsay's avatar
Charles Lindsay committed
149 150 151 152
    // Application.run() calls this as an entry point.
    public override bool local_command_line(ref unowned string[] args, out int exit_status) {
        bin = args[0];
        exec_dir = (File.new_for_path(Posix.realpath(Environment.find_program_in_path(bin)))).get_parent();
153
        
Charles Lindsay's avatar
Charles Lindsay committed
154 155 156 157 158 159
        try {
            register();
        } catch (Error e) {
            error("Error registering GearyApplication: %s", e.message);
        }
        
160 161 162 163
        if (!Args.parse(args)) {
            exit_status = 1;
            return true;
        }
164 165

        if (!Args.quit) {
166
            // Normal application startup or activation
167 168 169 170 171 172 173 174 175 176
            activate();
            foreach (unowned string arg in args) {
                if (arg != null) {
                    if (arg == Geary.ComposedEmail.MAILTO_SCHEME)
                        activate_action(ACTION_COMPOSE, null);
                    else if (arg.has_prefix(Geary.ComposedEmail.MAILTO_SCHEME))
                        activate_action(ACTION_MAILTO, new Variant.string(arg));
                }
            }
        } else {
177 178 179 180 181
            // User requested quit, only try to if we aren't running
            // already.
            if (this.is_remote) {
                activate_action(ACTION_QUIT, null);
            }
Charles Lindsay's avatar
Charles Lindsay committed
182
        }
183

Charles Lindsay's avatar
Charles Lindsay committed
184 185 186 187 188
        exit_status = 0;
        return true;
    }
    
    public override void startup() {
189
        Configuration.init(is_installed(), GSETTINGS_DIR);
Charles Lindsay's avatar
Charles Lindsay committed
190 191 192 193 194 195
        
        Environment.set_application_name(NAME);
        Environment.set_prgname(PRGNAME);
        International.init(GETTEXT_PACKAGE, bin);
        
        Geary.Logging.init();
196
        Date.init();
197

Charles Lindsay's avatar
Charles Lindsay committed
198
        base.startup();
199

Charles Lindsay's avatar
Charles Lindsay committed
200
        add_action_entries(action_entries, this);
201 202
    }
    
Charles Lindsay's avatar
Charles Lindsay committed
203 204
    public override void activate() {
        base.activate();
205
        
Charles Lindsay's avatar
Charles Lindsay committed
206 207
        if (!present())
            create_async.begin();
208 209
    }
    
Charles Lindsay's avatar
Charles Lindsay committed
210
    public bool present() {
211 212 213 214 215 216 217 218 219 220 221 222 223
        if (controller == null)
            return false;
        
        // if LoginDialog (i.e. the opening dialog for creating the initial account) is present
        // and visible, bring that to top (to prevent opening the hidden main window, which is
        // empty)
        if (controller.login_dialog != null && controller.login_dialog.visible) {
            controller.login_dialog.present_with_time(Gdk.CURRENT_TIME);
            
            return true;
        }
        
        if (controller.main_window == null)
Charles Lindsay's avatar
Charles Lindsay committed
224
            return false;
225 226 227 228 229 230

        // When the app is started hidden, show_all() never gets
        // called, do so here to prevent an empty window appearing.
        controller.main_window.show_all();
        controller.main_window.present();

Charles Lindsay's avatar
Charles Lindsay committed
231 232 233 234 235 236 237 238
        return true;
    }
    
    private async void create_async() {
        // Manually keep the main loop around for the duration of this call.
        // Without this, the main loop will exit as soon as we hit the yield
        // below, before we create the main window.
        hold();
239 240 241 242 243 244 245
        
        // do *after* parsing args, as they dicate where logging is sent to, if anywhere, and only
        // after activate (which means this is only logged for the one user-visible instance, not
        // the other instances called when sending commands to the app via the command-line)
        message("%s %s prefix=%s exec_dir=%s is_installed=%s", NAME, VERSION, INSTALL_PREFIX,
            exec_dir.get_path(), is_installed().to_string());
        
Charles Lindsay's avatar
Charles Lindsay committed
246
        config = new Configuration(APP_ID);
247
        yield controller.open_async();
248
        
Charles Lindsay's avatar
Charles Lindsay committed
249 250 251
        release();
    }
    
252 253 254 255 256 257 258 259 260 261
    private async void destroy_async() {
        // see create_async() for reasoning hold/release is used
        hold();
        
        yield controller.close_async();
        
        release();
        
        is_destroyed = true;
    }
262

263 264 265 266 267 268 269 270
    // NOTE: This assert()'s if the Gtk.Action is not present in the default action group
    public Gtk.Action get_action(string name) {
        Gtk.Action? action = actions.get_action(name);
        assert(action != null);
        
        return action;
    }
    
271
    public File get_user_data_directory() {
272 273
        return File.new_for_path(Environment.get_user_data_dir()).get_child("geary");
    }
274 275 276 277 278

    public File get_user_cache_directory() {
        return File.new_for_path(Environment.get_user_cache_dir()).get_child("geary");
    }

279 280
    public File get_user_config_directory() {
        return File.new_for_path(Environment.get_user_config_dir()).get_child("geary");
281 282 283 284 285 286 287 288 289
    }
    
    /**
     * Returns the base directory that the application's various resource files are stored.  If the
     * application is running from its installed directory, this will point to
     * $(BASEDIR)/share/<program name>.  If it's running from the build directory, this points to
     * that.
     */
    public File get_resource_directory() {
290 291 292 293
        if (get_install_dir() != null)
            return get_install_dir().get_child("share").get_child("geary");
        else
            return File.new_for_path(SOURCE_ROOT_DIR);
294
    }
295 296

    /** Returns the directory the application is currently executing from. */
297
    public File get_exec_dir() {
298
        return this.exec_dir;
299
    }
300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315

    /**
     * Returns the directory containing the application's WebExtension libs.
     *
     * If the application is installed, this will be
     * `$INSTALL_PREFIX/lib/geary/web-extension`, else it will be
     */
    public File get_web_extensions_dir() {
        File? dir = get_install_dir();
        if (dir != null)
            dir = dir.get_child("lib").get_child("geary").get_child("web-extensions");
        else
            dir = File.new_for_path(BUILD_ROOT_DIR).get_child("src");
        return dir;
    }

316
    public File? get_desktop_file() {
317 318
        File? install_dir = get_install_dir();
        File desktop_file = (install_dir != null)
319 320
            ? install_dir.get_child("share").get_child("applications").get_child("org.gnome.Geary.desktop")
            : File.new_for_path(SOURCE_ROOT_DIR).get_child("build").get_child("desktop").get_child("org.gnome.Geary.desktop");
321
        
322
        return desktop_file.query_exists() ? desktop_file : null;
323 324
    }
    
325
    public bool is_installed() {
326
        return exec_dir.has_prefix(get_install_prefix_dir());
327
    }
328 329 330 331 332 333 334
    
    // Returns the configure installation prefix directory, which does not imply Geary is installed
    // or that it's running from this directory.
    public File get_install_prefix_dir() {
        return File.new_for_path(INSTALL_PREFIX);
    }
    
335 336 337
    // Returns the installation directory, or null if we're running outside of the installation
    // directory.
    public File? get_install_dir() {
338 339
        File prefix_dir = get_install_prefix_dir();
        
340 341
        return exec_dir.has_prefix(prefix_dir) ? prefix_dir : null;
    }
342 343 344 345 346 347

    /**
     * Creates a GTK builder given the name of a GResource.
     *
     * @deprecated Use {@link GioUtil.create_builder} instead.
     */
348
    [Deprecated]
349
    public Gtk.Builder create_builder(string name) {
350
        return GioUtil.create_builder(name);
351
    }
352

353 354 355 356 357
    /**
     * Loads a GResource as a string.
     *
     * @deprecated Use {@link GioUtil.read_resource} instead.
     */
358
    [Deprecated]
359
    public string read_resource(string name) throws Error {
360
        return GioUtil.read_resource(name);
361
    }
362

363 364 365
    /**
     * Loads a UI GResource into the UI manager.
     */
366
    [Deprecated]
367
    public void load_ui_resource(string name) {
368
        try {
369
            this.ui_manager.add_ui_from_resource("/org/gnome/Geary/" + name);
370
        } catch(GLib.Error error) {
371 372 373
            critical("Unable to load \"%s\" for Gtk.UIManager: %s".printf(
                name, error.message
            ));
374 375
        }
    }
376

377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395
    /**
     * Displays a URI on the current active window, if any.
     */
    public void show_uri(string uri) throws Error {
        Gtk.Window? window = get_active_window();
#if !GTK_3_22
        bool success = Gtk.show_uri(
            window != null ? window.get_screen() : null, uri, Gdk.CURRENT_TIME
        );
        if (!success) {
            throw new IOError.FAILED("gtk_show_uri() returned false");
        }
#else
        if (!Gtk.show_uri_on_window(window, uri, Gdk.CURRENT_TIME)) {
            throw new IOError.FAILED("gtk_show_uri_on_window() returned false");
        }
#endif
    }

Charles Lindsay's avatar
Charles Lindsay committed
396 397 398 399 400 401 402 403 404 405 406 407 408 409 410
    // This call will fire "exiting" only if it's not already been fired.
    public void exit(int exitcode = 0) {
        if (exiting_fired)
            return;
        
        this.exitcode = exitcode;
        
        exiting_fired = true;
        if (!exiting(false)) {
            exiting_fired = false;
            this.exitcode = 0;
            
            return;
        }
        
411 412 413 414
        // Give asynchronous destroy_async() a chance to complete, but to avoid bug(s) where
        // Geary hangs at exit, shut the whole thing down if destroy_async() takes too long to
        // complete
        int64 start_usec = get_monotonic_time();
415
        destroy_async.begin();
416
        while (!is_destroyed || Gtk.events_pending()) {
417
            Gtk.main_iteration();
418 419 420 421 422 423 424 425
            
            int64 delta_usec = get_monotonic_time() - start_usec;
            if (delta_usec >= FORCE_SHUTDOWN_USEC) {
                debug("Forcing shutdown of Geary, %ss passed...", (delta_usec / USEC_PER_SEC).to_string());
                
                break;
            }
        }
426
        
Charles Lindsay's avatar
Charles Lindsay committed
427 428 429 430
        if (Gtk.main_level() > 0)
            Gtk.main_quit();
        else
            Posix.exit(exitcode);
Jim Nelson's avatar
Jim Nelson committed
431 432
        
        Date.terminate();
Charles Lindsay's avatar
Charles Lindsay committed
433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449
    }
    
    /**
     * A callback for GearyApplication.exiting should return cancel_exit() to prevent the
     * application from exiting.
     */
    public bool cancel_exit() {
        Signal.stop_emission_by_name(this, "exiting");
        return false;
    }
    
    // This call will fire "exiting" only if it's not already been fired and halt the application
    // in its tracks.
    public void panic() {
        if (!exiting_fired) {
            exiting_fired = true;
            exiting(true);
450
        }
Charles Lindsay's avatar
Charles Lindsay committed
451 452
        
        Posix.exit(1);
453
    }
454

455 456 457 458 459
    private void on_activate_about() {
        Gtk.show_about_dialog(get_active_window(),
            "program-name", NAME,
            "comments", DESCRIPTION,
            "authors", AUTHORS,
460
            "copyright", string.join("\n", COPYRIGHT_1, COPYRIGHT_2),
461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493
            "license-type", Gtk.License.LGPL_2_1,
            "logo-icon-name", "geary",
            "version", VERSION,
            "website", WEBSITE,
            "website-label", WEBSITE_LABEL,
            "title", _("About %s").printf(NAME),
            // Translators: add your name and email address to receive
            // credit in the About dialog For example: Yamada Taro
            // <yamada.taro@example.com>
            "translator-credits", _("translator-credits")
        );
    }

    private void on_activate_accounts() {
        AccountDialog dialog = new AccountDialog(get_active_window());
        dialog.show_all();
        dialog.run();
        dialog.destroy();
    }

    private void on_activate_compose() {
        if (this.controller != null) {
            this.controller.compose();
        }
    }

    private void on_activate_mailto(SimpleAction action, Variant? param) {
        if (this.controller != null && param != null) {
            this.controller.compose_mailto(param.get_string());
        }
    }

    private void on_activate_preferences() {
494
        PreferencesDialog dialog = new PreferencesDialog(get_active_window(), this);
495 496 497 498 499 500 501 502 503 504
        dialog.run();
    }

    private void on_activate_quit() {
        exit();
    }

    private void on_activate_help() {
        try {
            if (is_installed()) {
505
                show_uri("ghelp:geary");
506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539
            } else {
                Pid pid;
                File exec_dir = get_exec_dir();
                string[] argv = new string[3];
                argv[0] = "gnome-help";
                argv[1] = GearyApplication.SOURCE_ROOT_DIR + "/help/C/";
                argv[2] = null;
                if (!Process.spawn_async(
                        exec_dir.get_path(),
                        argv,
                        null,
                        SpawnFlags.SEARCH_PATH | SpawnFlags.STDERR_TO_DEV_NULL,
                        null,
                        out pid)) {
                    debug("Failed to launch help locally.");
                }
            }
        } catch (Error error) {
            debug("Error showing help: %s", error.message);
            Gtk.Dialog dialog = new Gtk.Dialog.with_buttons(
                "Error",
                get_active_window(),
                Gtk.DialogFlags.DESTROY_WITH_PARENT,
                Stock._CLOSE, Gtk.ResponseType.CLOSE, null);
            dialog.response.connect(() => { dialog.destroy(); });
            dialog.get_content_area().add(
                new Gtk.Label("Error showing help: %s".printf(error.message))
            );
            dialog.show_all();
            dialog.run();
        }
    }

}