1319 lines
47 KiB
Vala
1319 lines
47 KiB
Vala
/*
|
|
* Copyright © 2016 Software Freedom Conservancy Inc.
|
|
* Copyright © 2019-2020 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.
|
|
*/
|
|
|
|
// Defined by CMake build script.
|
|
extern const string _INSTALL_PREFIX;
|
|
extern const string _GSETTINGS_DIR;
|
|
extern const string _WEB_EXTENSIONS_DIR;
|
|
extern const string _PLUGINS_DIR;
|
|
extern const string _SOURCE_ROOT_DIR;
|
|
extern const string _BUILD_ROOT_DIR;
|
|
extern const string GETTEXT_PACKAGE;
|
|
extern const string _APP_ID;
|
|
extern const string _NAME_SUFFIX;
|
|
extern const string _PROFILE;
|
|
extern const string _VERSION;
|
|
extern const string _REVNO;
|
|
|
|
|
|
/**
|
|
* The client application's main point of entry and desktop integration.
|
|
*/
|
|
public class Application.Client : Gtk.Application {
|
|
|
|
public const string NAME = "Geary" + _NAME_SUFFIX;
|
|
public const string APP_ID = _APP_ID;
|
|
public const string RESOURCE_BASE_PATH = "/org/gnome/Geary";
|
|
public const string SCHEMA_ID = "org.gnome.Geary";
|
|
public const string DESCRIPTION = _("Send and receive email");
|
|
public const string COPYRIGHT_1 = _("Copyright 2016 Software Freedom Conservancy Inc.");
|
|
public const string COPYRIGHT_2 = _("Copyright 2016-2020 Geary Development Team.");
|
|
public const string WEBSITE = "https://wiki.gnome.org/Apps/Geary";
|
|
public const string WEBSITE_LABEL = _("Visit the Geary web site");
|
|
public const string BUGREPORT = "https://wiki.gnome.org/Apps/Geary/ReportingABug";
|
|
|
|
public const string VERSION = _VERSION;
|
|
public const string INSTALL_PREFIX = _INSTALL_PREFIX;
|
|
public const string GSETTINGS_DIR = _GSETTINGS_DIR;
|
|
public const string SOURCE_ROOT_DIR = _SOURCE_ROOT_DIR;
|
|
public const string BUILD_ROOT_DIR = _BUILD_ROOT_DIR;
|
|
|
|
public const string[] AUTHORS = {
|
|
"Jim Nelson <jim@yorba.org>",
|
|
"Eric Gregory <eric@yorba.org>",
|
|
"Nate Lillich <nate@yorba.org>",
|
|
"Matthew Pirocchi <matthew@yorba.org>",
|
|
"Charles Lindsay <chaz@yorba.org>",
|
|
"Robert Schroll <rschroll@gmail.com>",
|
|
"Michael Gratton <mike@vee.net>",
|
|
null
|
|
};
|
|
|
|
/** Default size of avatar images, in virtual pixels */
|
|
public const int AVATAR_SIZE_PIXELS = 48;
|
|
|
|
// Local-only command line options
|
|
private const string OPTION_VERSION = "version";
|
|
|
|
// Local command line options
|
|
private const string OPTION_DEBUG = "debug";
|
|
private const string OPTION_INSPECTOR = "inspector";
|
|
private const string OPTION_LOG_CONVERSATIONS = "log-conversations";
|
|
private const string OPTION_LOG_DESERIALIZER = "log-deserializer";
|
|
private const string OPTION_LOG_FOLDER_NORM = "log-folder-normalization";
|
|
private const string OPTION_LOG_IMAP = "log-imap";
|
|
private const string OPTION_LOG_REPLAY_QUEUE = "log-replay-queue";
|
|
private const string OPTION_LOG_SMTP = "log-smtp";
|
|
private const string OPTION_LOG_SQL = "log-sql";
|
|
private const string OPTION_HIDDEN = "hidden";
|
|
private const string OPTION_NEW_WINDOW = "new-window";
|
|
private const string OPTION_QUIT = "quit";
|
|
private const string OPTION_REVOKE_CERTS = "revoke-certs";
|
|
|
|
private const ActionEntry[] ACTION_ENTRIES = {
|
|
{ Action.Application.ABOUT, on_activate_about},
|
|
{ Action.Application.ACCOUNTS, on_activate_accounts},
|
|
{ Action.Application.COMPOSE, on_activate_compose},
|
|
{ Action.Application.HELP, on_activate_help},
|
|
{ Action.Application.INSPECT, on_activate_inspect},
|
|
{ Action.Application.MAILTO, on_activate_mailto, "s"},
|
|
{ Action.Application.NEW_WINDOW, on_activate_new_window },
|
|
{ Action.Application.PREFERENCES, on_activate_preferences},
|
|
{ Action.Application.QUIT, on_activate_quit},
|
|
{ Action.Application.SHOW_EMAIL, on_activate_show_email, "(sv)"},
|
|
{ Action.Application.SHOW_FOLDER, on_activate_show_folder, "(sv)"}
|
|
};
|
|
|
|
// This is also the order in which they are presented to the user,
|
|
// so it's probably best to keep them alphabetical
|
|
private const GLib.OptionEntry[] OPTION_ENTRIES = {
|
|
{ OPTION_DEBUG, 'd', 0, GLib.OptionArg.NONE, null,
|
|
/// Command line option
|
|
N_("Print debug logging"), null },
|
|
{ OPTION_HIDDEN, 0, 0, GLib.OptionArg.NONE, null,
|
|
/// Command line option
|
|
N_("Start with the main window hidden (deprecated)"), null },
|
|
{ OPTION_INSPECTOR, 'i', 0, GLib.OptionArg.NONE, null,
|
|
/// Command line option
|
|
N_("Enable WebKitGTK Inspector in web views"), null },
|
|
{ OPTION_LOG_CONVERSATIONS, 0, 0, GLib.OptionArg.NONE, null,
|
|
/// Command line option
|
|
N_("Log conversation monitoring"), null },
|
|
{ OPTION_LOG_DESERIALIZER, 0, 0, GLib.OptionArg.NONE, null,
|
|
/// Command line option
|
|
N_("Log IMAP network deserialization"), null },
|
|
{ OPTION_LOG_FOLDER_NORM, 0, 0, GLib.OptionArg.NONE, null,
|
|
/// Command line option. "Normalization" can also be called
|
|
/// "synchronization".
|
|
N_("Log folder normalization"), null },
|
|
{ OPTION_LOG_IMAP, 0, 0, GLib.OptionArg.NONE, null,
|
|
/// Command line option
|
|
N_("Log IMAP network activity"), null },
|
|
{ OPTION_LOG_REPLAY_QUEUE, 0, 0, GLib.OptionArg.NONE, null,
|
|
/// Command line option. The IMAP replay queue is how changes
|
|
/// on the server are replicated on the client. It could
|
|
/// also be called the IMAP events queue.
|
|
N_("Log IMAP replay queue"), null },
|
|
{ OPTION_LOG_SMTP, 0, 0, GLib.OptionArg.NONE, null,
|
|
/// Command line option
|
|
N_("Log SMTP network activity"), null },
|
|
{ OPTION_LOG_SQL, 0, 0, GLib.OptionArg.NONE, null,
|
|
/// Command line option
|
|
N_("Log database queries (generates lots of messages)"), null },
|
|
{ OPTION_QUIT, 'q', 0, GLib.OptionArg.NONE, null,
|
|
/// Command line option
|
|
N_("Perform a graceful quit"), null },
|
|
{ OPTION_NEW_WINDOW, 'n', 0, GLib.OptionArg.NONE, null,
|
|
N_("Open a new window"), null },
|
|
{ OPTION_REVOKE_CERTS, 0, 0, GLib.OptionArg.NONE, null,
|
|
/// Command line option
|
|
N_("Revoke all pinned TLS server certificates"), null },
|
|
{ OPTION_VERSION, 'v', 0, GLib.OptionArg.NONE, null,
|
|
/// Command line option
|
|
N_("Display program version"), null },
|
|
// Use this to specify arguments in the help section
|
|
{ GLib.OPTION_REMAINING, 0, 0, GLib.OptionArg.STRING_ARRAY, null, null,
|
|
"[mailto:[...]]" },
|
|
{ null }
|
|
};
|
|
|
|
private const string MAILTO_URI_SCHEME_PREFIX = "mailto:";
|
|
private const int64 USEC_PER_SEC = 1000000;
|
|
private const int64 FORCE_SHUTDOWN_USEC = 5 * USEC_PER_SEC;
|
|
|
|
private const string ERROR_NOTIFICATION_ID = "error";
|
|
|
|
|
|
|
|
/** Object returned by {@link get_runtime_information}. */
|
|
public struct RuntimeDetail {
|
|
|
|
public string name;
|
|
public string value;
|
|
|
|
}
|
|
|
|
/**
|
|
* The global email subsystem controller for this app instance.
|
|
*/
|
|
public Geary.Engine? engine {
|
|
get; private set; default = null;
|
|
}
|
|
|
|
/**
|
|
* The user's desktop settings for the application.
|
|
*
|
|
* This will be null until {@link startup} has been called, and
|
|
* hence will only ever become non-null for the primary instance.
|
|
*/
|
|
public Configuration? config {
|
|
get; private set; default = null;
|
|
}
|
|
|
|
/**
|
|
* The last active main window.
|
|
*
|
|
* This will be null if no main windows exist, see {@link
|
|
* get_active_main_window} if you want to be guaranteed an
|
|
* instance.
|
|
*/
|
|
public MainWindow? last_active_main_window {
|
|
get; private set; default = null;
|
|
}
|
|
|
|
/**
|
|
* Manages the autostart desktop file.
|
|
*
|
|
* This will be null until {@link startup} has been called, and
|
|
* hence will only ever become non-null for the primary instance.
|
|
*/
|
|
public StartupManager? autostart {
|
|
get; private set; default = null;
|
|
}
|
|
|
|
/**
|
|
* 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 exiting as usual.
|
|
*/
|
|
public bool is_background_service {
|
|
get {
|
|
return (
|
|
(this.flags & ApplicationFlags.IS_SERVICE) != 0 ||
|
|
this.start_hidden
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* The global controller for this application instance.
|
|
*
|
|
* This will be non-null in the primary application instance, only
|
|
* after initial activation, or after startup if {@link
|
|
* is_background_service} is true.
|
|
*/
|
|
internal Controller? controller {
|
|
get; private set; default = null;
|
|
}
|
|
|
|
/**
|
|
* Determines if this instance is running from the install directory.
|
|
*/
|
|
internal bool is_installed {
|
|
get {
|
|
return this.exec_dir.has_prefix(this.install_prefix);
|
|
}
|
|
}
|
|
|
|
/** Returns the compile-time configured installation directory. */
|
|
internal GLib.File install_prefix {
|
|
get; private set; default = GLib.File.new_for_path(INSTALL_PREFIX);
|
|
}
|
|
|
|
|
|
private File exec_dir;
|
|
private string binary;
|
|
private bool start_hidden = false;
|
|
private Gtk.CssProvider single_key_shortcuts = new Gtk.CssProvider();
|
|
private GLib.Cancellable controller_cancellable = new GLib.Cancellable();
|
|
private Components.Inspector? inspector = null;
|
|
private Geary.Nonblocking.Mutex controller_mutex = new Geary.Nonblocking.Mutex();
|
|
private GLib.Notification? error_notification = null;
|
|
|
|
|
|
/**
|
|
* Returns name/value pairs of application information.
|
|
*
|
|
* This includes Geary library version information, the current
|
|
* desktop, and so on.
|
|
*/
|
|
public Gee.Collection<RuntimeDetail?> get_runtime_information() {
|
|
Gee.LinkedList<RuntimeDetail?> info =
|
|
new Gee.LinkedList<RuntimeDetail?>();
|
|
|
|
/// Application runtime information label
|
|
info.add({ _("Geary version"), VERSION });
|
|
/// Application runtime information label
|
|
info.add({ _("Geary revision"), _REVNO });
|
|
/// Application runtime information label
|
|
info.add({ _("GTK version"),
|
|
"%u.%u.%u".printf(
|
|
Gtk.get_major_version(),
|
|
Gtk.get_minor_version(),
|
|
Gtk.get_micro_version()
|
|
)});
|
|
/// Applciation runtime information label
|
|
info.add({ _("GLib version"),
|
|
"%u.%u.%u".printf(
|
|
GLib.Version.major,
|
|
GLib.Version.minor,
|
|
GLib.Version.micro
|
|
)});
|
|
/// Application runtime information label
|
|
info.add({ _("WebKitGTK version"),
|
|
"%u.%u.%u".printf(
|
|
WebKit.get_major_version(),
|
|
WebKit.get_minor_version(),
|
|
WebKit.get_micro_version()
|
|
)});
|
|
/// Application runtime information label
|
|
info.add({ _("Desktop environment"),
|
|
Environment.get_variable("XDG_CURRENT_DESKTOP") ??
|
|
_("Unknown")
|
|
});
|
|
|
|
// Distro name and version using LSB util
|
|
|
|
GLib.SubprocessLauncher launcher = new GLib.SubprocessLauncher(
|
|
GLib.SubprocessFlags.STDOUT_PIPE |
|
|
GLib.SubprocessFlags.STDERR_SILENCE
|
|
);
|
|
// Reset lang vars so we can guess the strings below
|
|
launcher.setenv("LANGUAGE", "C", true);
|
|
launcher.setenv("LANG", "C", true);
|
|
launcher.setenv("LC_ALL", "C", true);
|
|
|
|
string lsb_output = "";
|
|
try {
|
|
GLib.Subprocess lsb_release = launcher.spawnv(
|
|
{ "lsb_release", "-ir" }
|
|
);
|
|
lsb_release.communicate_utf8(null, null, out lsb_output, null);
|
|
} catch (GLib.Error err) {
|
|
warning("Failed to exec lsb_release: %s", err.message);
|
|
}
|
|
if (lsb_output != "") {
|
|
foreach (string line in lsb_output.split("\n")) {
|
|
string[] parts = line.split(":", 2);
|
|
if (parts.length > 1) {
|
|
if (parts[0].has_prefix("Distributor ID")) {
|
|
/// Application runtime information label
|
|
info.add(
|
|
{ _("Distribution name"), parts[1].strip() }
|
|
);
|
|
} else if (parts[0].has_prefix("Release")) {
|
|
/// Application runtime information label
|
|
info.add(
|
|
{ _("Distribution release"), parts[1].strip() }
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Application runtime information label
|
|
info.add({ _("Installation prefix"), INSTALL_PREFIX });
|
|
|
|
return info;
|
|
}
|
|
|
|
|
|
public Client() {
|
|
Object(
|
|
application_id: APP_ID,
|
|
resource_base_path: RESOURCE_BASE_PATH,
|
|
flags: (
|
|
GLib.ApplicationFlags.HANDLES_OPEN |
|
|
GLib.ApplicationFlags.HANDLES_COMMAND_LINE
|
|
)
|
|
);
|
|
this.add_main_option_entries(OPTION_ENTRIES);
|
|
this.window_removed.connect_after(on_window_removed);
|
|
}
|
|
|
|
public override bool local_command_line(ref unowned string[] args,
|
|
out int exit_status) {
|
|
this.binary = args[0];
|
|
string? current_path = Posix.realpath(
|
|
GLib.Environment.find_program_in_path(this.binary)
|
|
);
|
|
if (current_path == null) {
|
|
// Couldn't find the path, are being run as a unit test?
|
|
// Probably should deal with the null either way though.
|
|
current_path = this.binary;
|
|
}
|
|
this.exec_dir = GLib.File.new_for_path(current_path).get_parent();
|
|
|
|
return base.local_command_line(ref args, out exit_status);
|
|
}
|
|
|
|
public override int handle_local_options(GLib.VariantDict options) {
|
|
int ret = -1;
|
|
if (options.contains(OPTION_DEBUG)) {
|
|
Geary.Logging.log_to(GLib.stdout);
|
|
}
|
|
if (options.contains(OPTION_VERSION)) {
|
|
GLib.stdout.printf(
|
|
"%s: %s\n", this.binary, Client.VERSION
|
|
);
|
|
ret = 0;
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
public override void startup() {
|
|
Environment.set_application_name(NAME);
|
|
Util.I18n.init(GETTEXT_PACKAGE, this.binary);
|
|
Util.Date.init();
|
|
|
|
Configuration.init(this.is_installed, GSETTINGS_DIR);
|
|
|
|
// Add application's actions before chaining up so they are
|
|
// present when the application is first registered on the
|
|
// session bus.
|
|
add_action_entries(ACTION_ENTRIES, this);
|
|
|
|
// Calls Gtk.init(), amongst other things
|
|
base.startup();
|
|
Hdy.init();
|
|
|
|
this.engine = new Geary.Engine(get_resource_directory());
|
|
this.config = new Configuration(SCHEMA_ID);
|
|
this.autostart = new StartupManager(
|
|
this.config, this.get_desktop_directory()
|
|
);
|
|
|
|
// Ensure all geary windows have an icon
|
|
Gtk.Window.set_default_icon_name(APP_ID);
|
|
|
|
// Application accels
|
|
add_app_accelerators(Action.Application.COMPOSE, { "<Ctrl>N" });
|
|
add_app_accelerators(Action.Application.HELP, { "F1" });
|
|
add_app_accelerators(Action.Application.INSPECT, { "<Alt><Shift>I" });
|
|
add_app_accelerators(Action.Application.NEW_WINDOW, { "<Ctrl><Shift>N" });
|
|
add_app_accelerators(Action.Application.QUIT, { "<Ctrl>Q" });
|
|
|
|
// Common window accels
|
|
add_window_accelerators(Action.Window.CLOSE, { "<Ctrl>W" });
|
|
add_window_accelerators(
|
|
Action.Window.SHORTCUT_HELP, { "<Ctrl>F1", "<Ctrl>question" }
|
|
);
|
|
|
|
// Common edit accels
|
|
add_edit_accelerators(Action.Edit.COPY, { "<Ctrl>C" });
|
|
add_edit_accelerators(Action.Edit.REDO, { "<Ctrl><Shift>Z" });
|
|
add_edit_accelerators(Action.Edit.UNDO, { "<Ctrl>Z" });
|
|
|
|
// Load Geary GTK CSS
|
|
var provider = new Gtk.CssProvider();
|
|
Gtk.StyleContext.add_provider_for_screen(
|
|
Gdk.Display.get_default().get_default_screen(),
|
|
provider,
|
|
Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION
|
|
);
|
|
load_css(provider,
|
|
"resource:///org/gnome/Geary/geary.css");
|
|
load_css(this.single_key_shortcuts,
|
|
"resource:///org/gnome/Geary/single-key-shortcuts.css");
|
|
update_single_key_shortcuts();
|
|
this.config.notify[Configuration.SINGLE_KEY_SHORTCUTS].connect(
|
|
on_single_key_shortcuts_toggled
|
|
);
|
|
|
|
MainWindow.add_accelerators(this);
|
|
Composer.Widget.add_accelerators(this);
|
|
Components.Inspector.add_accelerators(this);
|
|
Components.PreferencesWindow.add_accelerators(this);
|
|
Dialogs.ProblemDetailsDialog.add_accelerators(this);
|
|
|
|
// Manually place a hold on the application otherwise the
|
|
// application will exit when the async call to
|
|
// ::create_controller next returns without having yet created
|
|
// a window.
|
|
hold();
|
|
|
|
// Finally, start the controller.
|
|
this.create_controller.begin();
|
|
}
|
|
|
|
public override int command_line(GLib.ApplicationCommandLine command_line) {
|
|
int exit_value = handle_general_options(command_line);
|
|
if (exit_value != -1)
|
|
return exit_value;
|
|
|
|
return -1;
|
|
}
|
|
|
|
public override void shutdown() {
|
|
bool controller_closed = false;
|
|
this.destroy_controller.begin((obj, res) => {
|
|
this.destroy_controller.end(res);
|
|
controller_closed = true;
|
|
});
|
|
|
|
// GApplication will stop the main loop, so we need to keep
|
|
// pumping here to allow destroy_controller() to exit. To
|
|
// avoid bug(s) where Geary hangs at exit, shut the whole
|
|
// thing down if it takes too long to complete
|
|
int64 start_usec = get_monotonic_time();
|
|
while (!controller_closed) {
|
|
Gtk.main_iteration();
|
|
|
|
int64 delta_usec = get_monotonic_time() - start_usec;
|
|
if (delta_usec >= FORCE_SHUTDOWN_USEC) {
|
|
// Use a warning here so a) it's usually logged
|
|
// and b) we can run under gdb with
|
|
// G_DEBUG=fatal-warnings and have it break when
|
|
// this happens, and maybe debug it.
|
|
warning("Forcing shutdown of Geary, %ss passed...",
|
|
(delta_usec / USEC_PER_SEC).to_string());
|
|
Posix.exit(2);
|
|
}
|
|
}
|
|
|
|
this.engine = null;
|
|
this.config = null;
|
|
this.autostart = null;
|
|
|
|
Util.Date.terminate();
|
|
Geary.Logging.clear();
|
|
|
|
base.shutdown();
|
|
}
|
|
|
|
public override void activate() {
|
|
base.activate();
|
|
this.present.begin();
|
|
}
|
|
|
|
public override void open(GLib.File[] targets, string hint) {
|
|
foreach (GLib.File target in targets) {
|
|
if (target.get_uri_scheme() == "mailto") {
|
|
string mailto = target.get_uri();
|
|
// Due to GNOME/glib#1886, the email address may be
|
|
// prefixed by a '///'. If so, remove it.
|
|
const string B0RKED_GLIB_MAILTO_PREFIX = "mailto:///";
|
|
if (mailto.has_prefix(B0RKED_GLIB_MAILTO_PREFIX)) {
|
|
mailto = (
|
|
MAILTO_URI_SCHEME_PREFIX +
|
|
mailto.substring(B0RKED_GLIB_MAILTO_PREFIX.length)
|
|
);
|
|
}
|
|
this.new_composer_mailto.begin(mailto);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns a collection of open main windows.
|
|
*/
|
|
public Gee.Collection<MainWindow> get_main_windows() {
|
|
var windows = new Gee.LinkedList<MainWindow>();
|
|
foreach (Gtk.Window window in get_windows()) {
|
|
MainWindow? main = window as MainWindow;
|
|
if (main != null) {
|
|
windows.add(main);
|
|
}
|
|
}
|
|
return windows;
|
|
}
|
|
|
|
/**
|
|
* Returns the mostly recently active main window or a new instance.
|
|
*
|
|
* This returns the value of {@link last_active_main_window} if
|
|
* not null, else it constructs a new MainWindow instance and
|
|
* shows it.
|
|
*/
|
|
public MainWindow get_active_main_window() {
|
|
if (this.last_active_main_window == null) {
|
|
this.last_active_main_window = new_main_window(true);
|
|
}
|
|
return last_active_main_window;
|
|
}
|
|
|
|
public void add_window_accelerators(string action,
|
|
string[] accelerators,
|
|
Variant? param = null) {
|
|
string name = Action.Window.prefix(action);
|
|
string[] all_accel = get_accels_for_action(name);
|
|
foreach (string accel in accelerators) {
|
|
all_accel += accel;
|
|
}
|
|
set_accels_for_action(name, all_accel);
|
|
}
|
|
|
|
public void add_edit_accelerators(string action,
|
|
string[] accelerators,
|
|
Variant? param = null) {
|
|
string name = Action.Edit.prefix(action);
|
|
string[] all_accel = get_accels_for_action(name);
|
|
foreach (string accel in accelerators) {
|
|
all_accel += accel;
|
|
}
|
|
set_accels_for_action(name, all_accel);
|
|
}
|
|
|
|
public async void show_about() {
|
|
yield this.present();
|
|
|
|
Gtk.show_about_dialog(get_active_window(),
|
|
"program-name", NAME,
|
|
"comments", DESCRIPTION,
|
|
"authors", AUTHORS,
|
|
"copyright", string.join("\n", COPYRIGHT_1, COPYRIGHT_2),
|
|
"license-type", Gtk.License.LGPL_2_1,
|
|
"logo-icon-name", APP_ID,
|
|
"version", _REVNO == "" ? VERSION : "%s (%s)".printf(VERSION, _REVNO),
|
|
"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")
|
|
);
|
|
}
|
|
|
|
public async void show_accounts() {
|
|
yield this.present();
|
|
|
|
Accounts.Editor editor = new Accounts.Editor(
|
|
this, get_active_main_window()
|
|
);
|
|
editor.run();
|
|
editor.destroy();
|
|
this.controller.expunge_accounts.begin();
|
|
}
|
|
|
|
/**
|
|
* Displays a specific email in a main window.
|
|
*
|
|
* This method is invoked by the application `show-email`
|
|
* action. A folder containing the email will be selected if the
|
|
* current folder does not select it.
|
|
*
|
|
* The email is identified by the given variant, which can be
|
|
* obtained by calling {@link Plugin.EmailIdentifier.to_variant}.
|
|
*/
|
|
public async void show_email(GLib.Variant? id) {
|
|
MainWindow main = yield this.present();
|
|
if (id != null) {
|
|
EmailStoreFactory email = this.controller.plugins.globals.email;
|
|
AccountContext? context = email.get_account_for_variant(id);
|
|
Geary.EmailIdentifier? email_id =
|
|
email.get_email_identifier_for_variant(id);
|
|
if (context != null && email_id != null) {
|
|
// Determine what folders the email is in
|
|
Gee.MultiMap<Geary.EmailIdentifier,Geary.FolderPath>? folders = null;
|
|
try {
|
|
folders = yield context.account.get_containing_folders_async(
|
|
Geary.Collection.single(email_id),
|
|
context.cancellable
|
|
);
|
|
} catch (GLib.Error err) {
|
|
warning("Error listing folders for email: %s", err.message);
|
|
}
|
|
if (folders != null) {
|
|
// Determine what folder containing the email
|
|
// should be selected. If the current folder
|
|
// contains the email then use that, else use the
|
|
// inbox if that contains it, else use the first
|
|
// path found
|
|
var paths = folders.get(email_id);
|
|
Geary.Folder? selected = main.selected_folder;
|
|
if (selected == null ||
|
|
selected.account != context.account ||
|
|
!(selected.path in paths)) {
|
|
selected = null;
|
|
foreach (var path in paths) {
|
|
try {
|
|
Geary.Folder folder =
|
|
context.account.get_folder(path);
|
|
if (folder.used_as == INBOX) {
|
|
selected = folder;
|
|
break;
|
|
}
|
|
} catch (GLib.Error err) {
|
|
warning(
|
|
"Error getting folder for email: %s",
|
|
err.message
|
|
);
|
|
}
|
|
}
|
|
|
|
if (selected == null && !paths.is_empty) {
|
|
try {
|
|
selected = context.account.get_folder(
|
|
Geary.Collection.first(paths)
|
|
);
|
|
} catch (GLib.Error err) {
|
|
warning(
|
|
"Error getting folder for email: %s",
|
|
err.message
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (selected != null) {
|
|
yield main.show_email(
|
|
selected,
|
|
Geary.Collection.single(email_id),
|
|
true
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Selects a specific folder in a main window.
|
|
*
|
|
* This method is invoked by the application `show-folder` action.
|
|
*
|
|
* The folder is identified by the given variant, which can be
|
|
* obtained by calling {@link Plugin.Folder.to_variant}.
|
|
*/
|
|
public async void show_folder(GLib.Variant? id) {
|
|
MainWindow main = yield this.present();
|
|
if (id != null) {
|
|
Geary.Folder? folder =
|
|
this.controller.plugins.globals.folders.get_folder_for_variant(id);
|
|
if (folder != null) {
|
|
yield main.select_folder(folder, true);
|
|
}
|
|
}
|
|
}
|
|
|
|
public async void show_inspector() {
|
|
yield this.present();
|
|
|
|
if (this.inspector == null) {
|
|
this.inspector = new Components.Inspector(this);
|
|
this.inspector.destroy.connect(() => {
|
|
this.inspector = null;
|
|
});
|
|
|
|
// Create a new window group for the inspector so it is
|
|
// not affected by the app's modal dialogs
|
|
var group = new Gtk.WindowGroup();
|
|
group.add_window(this.inspector);
|
|
|
|
this.inspector.show();
|
|
} else {
|
|
this.inspector.present();
|
|
}
|
|
}
|
|
|
|
public async void show_preferences() {
|
|
yield this.present();
|
|
|
|
Components.PreferencesWindow prefs = new Components.PreferencesWindow(
|
|
get_active_main_window(),
|
|
this.controller.plugins
|
|
);
|
|
prefs.show();
|
|
}
|
|
|
|
public async void new_composer(Geary.RFC822.MailboxAddress? to = null) {
|
|
MainWindow main = yield this.present();
|
|
AccountContext? account = null;
|
|
if (main.selected_account == null) {
|
|
account = this.controller.get_context_for_account(
|
|
main.selected_account.information
|
|
);
|
|
}
|
|
if (account == null) {
|
|
account = Geary.Collection.first(
|
|
this.controller.get_account_contexts()
|
|
);
|
|
}
|
|
if (account != null) {
|
|
Composer.Widget composer = yield this.controller.compose_blank(
|
|
account,
|
|
to
|
|
);
|
|
this.controller.present_composer(composer);
|
|
}
|
|
}
|
|
|
|
public async void new_composer_mailto(string? mailto) {
|
|
yield this.present();
|
|
yield this.controller.compose_mailto(mailto);
|
|
}
|
|
|
|
public async void new_window(Geary.Folder? select_folder,
|
|
Gee.Collection<Geary.App.Conversation>? select_conversations) {
|
|
yield create_controller();
|
|
|
|
bool do_select = (
|
|
select_folder != null &&
|
|
select_conversations != null &&
|
|
!select_conversations.is_empty
|
|
);
|
|
|
|
MainWindow main = new_main_window(!do_select);
|
|
main.present();
|
|
|
|
if (do_select) {
|
|
if (select_conversations == null || select_conversations.is_empty) {
|
|
main.select_folder.begin(select_folder, true);
|
|
} else {
|
|
main.show_conversations.begin(
|
|
select_folder,
|
|
select_conversations,
|
|
true
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
/** Returns the application's base user configuration directory. */
|
|
public GLib.File get_user_config_directory() {
|
|
return GLib.File.new_for_path(
|
|
Environment.get_user_config_dir()
|
|
).get_child("geary");
|
|
}
|
|
|
|
/** Returns the application's base user cache directory. */
|
|
public GLib.File get_user_cache_directory() {
|
|
return GLib.File.new_for_path(
|
|
GLib.Environment.get_user_cache_dir()
|
|
).get_child("geary");
|
|
}
|
|
|
|
/** Returns the application's base user data directory. */
|
|
public GLib.File get_user_data_directory() {
|
|
return GLib.File.new_for_path(
|
|
GLib.Environment.get_user_data_dir()
|
|
).get_child("geary");
|
|
}
|
|
|
|
/** Returns the application's base static resources directory. */
|
|
public GLib.File get_resource_directory() {
|
|
return (is_installed)
|
|
? this.install_prefix.get_child("share").get_child("geary")
|
|
: GLib.File.new_for_path(SOURCE_ROOT_DIR);
|
|
}
|
|
|
|
/** Returns the location of the application's desktop files. */
|
|
public GLib.File get_desktop_directory() {
|
|
return (is_installed)
|
|
? this.install_prefix.get_child("share").get_child("applications")
|
|
: GLib.File.new_for_path(BUILD_ROOT_DIR).get_child("desktop");
|
|
}
|
|
|
|
/**
|
|
* Returns the directory containing the application's WebExtension libs.
|
|
*
|
|
* When running from the installation prefix, this will be based
|
|
* on the Meson `libdir` option, and can be set by invoking `meson
|
|
* configure` as appropriate.
|
|
*/
|
|
public GLib.File get_web_extensions_dir() {
|
|
return (is_installed)
|
|
? GLib.File.new_for_path(_WEB_EXTENSIONS_DIR)
|
|
: GLib.File.new_for_path(BUILD_ROOT_DIR).get_child("src");
|
|
}
|
|
|
|
/**
|
|
* Returns the directory containing the application's plugins.
|
|
*
|
|
* When running from the installation prefix, this will be based
|
|
* on the Meson `libdir` option, and can be set by invoking `meson
|
|
* configure` as appropriate.
|
|
*/
|
|
public GLib.File get_app_plugins_dir() {
|
|
return (is_installed)
|
|
? GLib.File.new_for_path(_PLUGINS_DIR)
|
|
: GLib.File.new_for_path(BUILD_ROOT_DIR)
|
|
.get_child("src").get_child("client").get_child("plugin");
|
|
}
|
|
|
|
/** Displays a URI on the current active window, if any. */
|
|
public async void show_uri(string uri) {
|
|
yield create_controller();
|
|
|
|
if (uri.down().has_prefix(MAILTO_URI_SCHEME_PREFIX)) {
|
|
yield this.new_composer_mailto(uri);
|
|
} else {
|
|
string uri_ = uri;
|
|
// Support web URLs that omit the protocol.
|
|
if (!uri.contains(":")) {
|
|
uri_ = "http://" + uri;
|
|
}
|
|
|
|
try {
|
|
Gtk.show_uri_on_window(
|
|
get_active_window(), uri_, Gdk.CURRENT_TIME
|
|
);
|
|
} catch (GLib.Error err) {
|
|
this.controller.report_problem(new Geary.ProblemReport(err));
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Closes the controller and all open windows, exiting if possible.
|
|
*
|
|
* Any open composers with unsaved or un-savable changes will be
|
|
* prompted about and if cancelled, will cancel shut-down here.
|
|
*/
|
|
public new void quit() {
|
|
if (this.controller == null ||
|
|
this.controller.check_open_composers()) {
|
|
this.last_active_main_window = null;
|
|
base.quit();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Displays an error notification.
|
|
*
|
|
* Use _very_ sparingly.
|
|
*/
|
|
internal void send_error_notification(string summary, string body) {
|
|
if (this.error_notification != null) {
|
|
clear_error_notification();
|
|
}
|
|
|
|
GLib.Notification error = new GLib.Notification(summary);
|
|
error.set_body(body);
|
|
error.set_icon(
|
|
new GLib.ThemedIcon("%s-symbolic".printf(Client.APP_ID))
|
|
);
|
|
send_notification(ERROR_NOTIFICATION_ID, error);
|
|
this.error_notification = error;
|
|
}
|
|
|
|
internal void clear_error_notification() {
|
|
this.error_notification = null;
|
|
withdraw_notification(ERROR_NOTIFICATION_ID);
|
|
}
|
|
|
|
// Presents a main window, opening the controller and window if
|
|
// needed.
|
|
private async MainWindow present() {
|
|
yield create_controller();
|
|
MainWindow main = get_active_main_window();
|
|
main.present();
|
|
return main;
|
|
}
|
|
|
|
private MainWindow new_main_window(bool select_first_inbox) {
|
|
MainWindow window = new MainWindow(this);
|
|
this.controller.register_window(window);
|
|
window.focus_in_event.connect(on_main_window_focus_in);
|
|
if (select_first_inbox) {
|
|
if (!window.select_first_inbox(true)) {
|
|
// The first inbox wasn't selected, so the account is
|
|
// likely still loading folders after being
|
|
// opened. Add a listener to try again later.
|
|
try {
|
|
Geary.Account? first = Geary.Collection.first<Geary.Account>(
|
|
this.engine.get_accounts()
|
|
);
|
|
if (first != null) {
|
|
first.folders_available_unavailable.connect_after(
|
|
on_folders_first_available
|
|
);
|
|
}
|
|
} catch (GLib.Error error) {
|
|
debug("Error getting Inbox for first account");
|
|
}
|
|
}
|
|
}
|
|
return window;
|
|
}
|
|
|
|
// Opens the controller
|
|
private async void create_controller() {
|
|
bool first_run = false;
|
|
bool open_failed = false;
|
|
int mutex_token = Geary.Nonblocking.Mutex.INVALID_TOKEN;
|
|
try {
|
|
mutex_token = yield this.controller_mutex.claim_async();
|
|
if (this.controller == null) {
|
|
message(
|
|
"%s %s%s prefix=%s exec_dir=%s is_installed=%s",
|
|
NAME,
|
|
VERSION,
|
|
_REVNO != "" ? " (%s)".printf(_REVNO) : "",
|
|
INSTALL_PREFIX,
|
|
exec_dir.get_path(),
|
|
this.is_installed.to_string()
|
|
);
|
|
|
|
this.controller = yield new Controller(
|
|
this, this.controller_cancellable
|
|
);
|
|
first_run = !this.engine.has_accounts;
|
|
}
|
|
} catch (Error err) {
|
|
open_failed = true;
|
|
warning("Error creating controller: %s", err.message);
|
|
var dialog = new Dialogs.ProblemDetailsDialog(
|
|
null,
|
|
this,
|
|
new Geary.ProblemReport(err)
|
|
);
|
|
dialog.show();
|
|
}
|
|
|
|
if (mutex_token != Geary.Nonblocking.Mutex.INVALID_TOKEN) {
|
|
try {
|
|
this.controller_mutex.release(ref mutex_token);
|
|
} catch (GLib.Error error) {
|
|
warning("Failed to release controller mutex: %s",
|
|
error.message);
|
|
}
|
|
}
|
|
|
|
if (open_failed) {
|
|
quit();
|
|
}
|
|
|
|
if (first_run) {
|
|
yield show_accounts();
|
|
if (!this.engine.has_accounts) {
|
|
// No accounts were added after showing the accounts
|
|
// editor, so nothing else to do but exit.
|
|
quit();
|
|
}
|
|
}
|
|
}
|
|
|
|
// Closes the controller, if running
|
|
private async void destroy_controller() {
|
|
try {
|
|
int mutex_token = yield this.controller_mutex.claim_async();
|
|
if (this.controller != null) {
|
|
yield this.controller.close();
|
|
this.controller = null;
|
|
}
|
|
this.controller_mutex.release(ref mutex_token);
|
|
} catch (GLib.Error err) {
|
|
warning("Error destroying controller: %s", err.message);
|
|
}
|
|
|
|
try {
|
|
this.engine.close();
|
|
} catch (GLib.Error error) {
|
|
warning("Error shutting down the engine: %s", error.message);
|
|
}
|
|
}
|
|
|
|
private int handle_general_options(GLib.ApplicationCommandLine command_line) {
|
|
GLib.VariantDict options = command_line.get_options_dict();
|
|
if (options.contains(OPTION_QUIT)) {
|
|
quit();
|
|
return 0;
|
|
}
|
|
|
|
bool activated = false;
|
|
|
|
// Suppress some noisy domains from third-party libraries
|
|
Geary.Logging.suppress_domain("GdkPixbuf");
|
|
Geary.Logging.suppress_domain("GLib-Net");
|
|
|
|
// Suppress the engine's sub-domains that are extremely
|
|
// verbose by default unless requested not to do so
|
|
if (!options.contains(OPTION_LOG_CONVERSATIONS)) {
|
|
Geary.Logging.suppress_domain(
|
|
Geary.App.ConversationMonitor.LOGGING_DOMAIN
|
|
);
|
|
}
|
|
if (!options.contains(OPTION_LOG_DESERIALIZER)) {
|
|
Geary.Logging.suppress_domain(
|
|
Geary.Imap.ClientService.DESERIALISATION_LOGGING_DOMAIN
|
|
);
|
|
}
|
|
if (!options.contains(OPTION_LOG_IMAP)) {
|
|
Geary.Logging.suppress_domain(
|
|
Geary.Imap.ClientService.PROTOCOL_LOGGING_DOMAIN
|
|
);
|
|
}
|
|
if (!options.contains(OPTION_LOG_REPLAY_QUEUE)) {
|
|
Geary.Logging.suppress_domain(
|
|
Geary.Imap.ClientService.REPLAY_QUEUE_LOGGING_DOMAIN
|
|
);
|
|
}
|
|
if (!options.contains(OPTION_LOG_SMTP)) {
|
|
Geary.Logging.suppress_domain(
|
|
Geary.Smtp.ClientService.PROTOCOL_LOGGING_DOMAIN
|
|
);
|
|
}
|
|
if (options.contains(OPTION_LOG_SQL)) {
|
|
Geary.Db.Context.enable_sql_logging = true;
|
|
} else {
|
|
Geary.Logging.suppress_domain(Geary.Db.Context.LOGGING_DOMAIN);
|
|
}
|
|
|
|
if (options.contains(OPTION_HIDDEN)) {
|
|
warning(
|
|
/// Warning printed to the console when a deprecated
|
|
/// command line option is used.
|
|
_("The `--hidden` option is deprecated and will be removed in the future.")
|
|
);
|
|
this.start_hidden = true;
|
|
// Update the autostart file so that it stops using the
|
|
// --hidden option.
|
|
this.update_autostart_file.begin();
|
|
// Then manually start the controller
|
|
this.create_controller.begin();
|
|
activated = true;
|
|
}
|
|
if (options.contains(OPTION_NEW_WINDOW)) {
|
|
activate_action(Action.Application.NEW_WINDOW, null);
|
|
activated = true;
|
|
}
|
|
|
|
if (options.contains(GLib.OPTION_REMAINING)) {
|
|
string[] args = options.lookup_value(
|
|
GLib.OPTION_REMAINING,
|
|
GLib.VariantType.STRING_ARRAY
|
|
).get_strv();
|
|
foreach (string arg in args) {
|
|
// the only acceptable arguments are mailto:'s
|
|
if (arg == MAILTO_URI_SCHEME_PREFIX) {
|
|
activate_action(Action.Application.COMPOSE, null);
|
|
activated = true;
|
|
} else if (arg.down().has_prefix(MAILTO_URI_SCHEME_PREFIX)) {
|
|
activate_action(Action.Application.MAILTO, new GLib.Variant.string(arg));
|
|
activated = true;
|
|
} else {
|
|
command_line.printerr("%s: ", this.binary);
|
|
command_line.printerr(
|
|
/// Command line warning, string substitution
|
|
/// is the given argument
|
|
_("Unrecognised program argument: “%s”"), arg
|
|
);
|
|
command_line.printerr("\n");
|
|
return 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
this.config.enable_debug = options.contains(OPTION_DEBUG);
|
|
this.config.enable_inspector = options.contains(OPTION_INSPECTOR);
|
|
this.config.revoke_certs = options.contains(OPTION_REVOKE_CERTS);
|
|
|
|
if (!activated) {
|
|
activate();
|
|
}
|
|
|
|
return -1;
|
|
}
|
|
|
|
/** Removes and re-adds the autostart file if needed. */
|
|
private async void update_autostart_file() {
|
|
try {
|
|
this.autostart.delete_startup_file();
|
|
if (this.config.startup_notifications) {
|
|
this.autostart.install_startup_file();
|
|
}
|
|
} catch (GLib.Error err) {
|
|
warning("Could not update autostart file");
|
|
}
|
|
}
|
|
|
|
private void add_app_accelerators(string action,
|
|
string[] accelerators,
|
|
GLib.Variant? param = null) {
|
|
set_accels_for_action("app." + action, accelerators);
|
|
}
|
|
|
|
private void update_single_key_shortcuts() {
|
|
if (this.config.single_key_shortcuts) {
|
|
Gtk.StyleContext.add_provider_for_screen(
|
|
Gdk.Display.get_default().get_default_screen(),
|
|
this.single_key_shortcuts,
|
|
Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION
|
|
);
|
|
} else {
|
|
Gtk.StyleContext.remove_provider_for_screen(
|
|
Gdk.Display.get_default().get_default_screen(),
|
|
this.single_key_shortcuts
|
|
);
|
|
}
|
|
}
|
|
|
|
private void load_css(Gtk.CssProvider provider, string resource_uri) {
|
|
provider.parsing_error.connect(on_css_parse_error);
|
|
try {
|
|
var file = GLib.File.new_for_uri(resource_uri);
|
|
provider.load_from_file(file);
|
|
} catch (GLib.Error error) {
|
|
warning("Could not load CSS: %s", error.message);
|
|
}
|
|
}
|
|
|
|
private void on_activate_about() {
|
|
this.show_about.begin();
|
|
}
|
|
|
|
private void on_activate_accounts() {
|
|
this.show_accounts.begin();
|
|
}
|
|
|
|
private void on_activate_compose() {
|
|
this.new_composer.begin();
|
|
}
|
|
|
|
private void on_activate_inspect() {
|
|
this.show_inspector.begin();
|
|
}
|
|
|
|
private void on_activate_mailto(SimpleAction action, Variant? param) {
|
|
if (param != null) {
|
|
this.new_composer_mailto.begin(param.get_string());
|
|
}
|
|
}
|
|
|
|
private void on_activate_new_window() {
|
|
// If there was an existing active main, select the same
|
|
// account/folder/conversation.
|
|
Geary.Folder? folder = null;
|
|
Gee.Collection<Geary.App.Conversation>? conversations = null;
|
|
MainWindow? current = this.last_active_main_window;
|
|
if (current != null) {
|
|
folder = current.selected_folder;
|
|
conversations = current.conversation_list_view.copy_selected();
|
|
}
|
|
this.new_window.begin(folder, conversations);
|
|
}
|
|
|
|
private void on_activate_preferences() {
|
|
this.show_preferences.begin();
|
|
}
|
|
|
|
private void on_activate_quit() {
|
|
quit();
|
|
}
|
|
|
|
private void on_activate_show_email(GLib.SimpleAction action,
|
|
GLib.Variant? target) {
|
|
this.show_email.begin(target);
|
|
}
|
|
|
|
private void on_activate_show_folder(GLib.SimpleAction action,
|
|
GLib.Variant? target) {
|
|
this.show_folder.begin(target);
|
|
}
|
|
|
|
private void on_activate_help() {
|
|
try {
|
|
if (this.is_installed) {
|
|
this.show_uri.begin("help:geary");
|
|
} else {
|
|
Pid pid;
|
|
File exec_dir = this.exec_dir;
|
|
string[] argv = new string[3];
|
|
argv[0] = "yelp";
|
|
argv[1] = Client.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();
|
|
}
|
|
}
|
|
|
|
private void on_folders_first_available(Geary.Account account,
|
|
Gee.BidirSortedSet<Geary.Folder>? available,
|
|
Gee.BidirSortedSet<Geary.Folder>? unavailable
|
|
) {
|
|
if (get_active_main_window().select_first_inbox(true)) {
|
|
// The handler has done its job, so disconnect it
|
|
account.folders_available_unavailable.disconnect(
|
|
on_folders_first_available
|
|
);
|
|
}
|
|
}
|
|
|
|
private bool on_main_window_focus_in(Gtk.Widget widget,
|
|
Gdk.EventFocus event) {
|
|
MainWindow? main = widget as MainWindow;
|
|
if (main != null) {
|
|
this.last_active_main_window = main;
|
|
}
|
|
return Gdk.EVENT_PROPAGATE;
|
|
}
|
|
|
|
private void on_window_removed(Gtk.Window window) {
|
|
MainWindow? main = window as MainWindow;
|
|
if (main != null) {
|
|
this.controller.unregister_window(main);
|
|
if (this.last_active_main_window == main) {
|
|
this.last_active_main_window = Geary.Collection.first(
|
|
get_main_windows()
|
|
);
|
|
}
|
|
}
|
|
|
|
// Since ::startup above took out a manual hold on the
|
|
// application, manually work out if the application should
|
|
// quit here.
|
|
if (!this.is_background_service && get_windows().length() == 0) {
|
|
quit();
|
|
}
|
|
}
|
|
|
|
private void on_single_key_shortcuts_toggled() {
|
|
update_single_key_shortcuts();
|
|
}
|
|
|
|
private void on_css_parse_error(Gtk.CssSection section, GLib.Error error) {
|
|
uint start = section.get_start_line();
|
|
uint end = section.get_end_line();
|
|
if (start == end) {
|
|
warning(
|
|
"Error parsing %s:%u: %s",
|
|
section.get_file().get_uri(), start, error.message
|
|
);
|
|
} else {
|
|
warning(
|
|
"Error parsing %s:%u-%u: %s",
|
|
section.get_file().get_uri(), start, end, error.message
|
|
);
|
|
}
|
|
}
|
|
}
|