diff --git a/src/e-util/e-misc-utils.c b/src/e-util/e-misc-utils.c index c6f4e10..3801337 100644 --- a/src/e-util/e-misc-utils.c +++ b/src/e-util/e-misc-utils.c @@ -319,7 +319,9 @@ e_display_help (GtkWindow *parent, uri = g_string_new ("help:" PACKAGE); } else { uri = g_string_new ("https://help.gnome.org/users/" PACKAGE "/"); - g_string_append_printf (uri, "%d.%d", EDS_MAJOR_VERSION, EDS_MINOR_VERSION); + /* Use '/stable/' until https://bugzilla.gnome.org/show_bug.cgi?id=785522 is fixed */ + g_string_append (uri, "stable/"); + /* g_string_append_printf (uri, "%d.%d", EDS_MAJOR_VERSION, EDS_MINOR_VERSION); */ } timestamp = gtk_get_current_event_time (); @@ -327,7 +329,6 @@ e_display_help (GtkWindow *parent, if (parent != NULL) screen = gtk_widget_get_screen (GTK_WIDGET (parent)); - if (link_id != NULL) { g_string_append (uri, "/"); g_string_append (uri, link_id); diff --git a/src/e-util/e-misc-utils.c.help-contents-link b/src/e-util/e-misc-utils.c.help-contents-link new file mode 100644 index 0000000..c6f4e10 --- /dev/null +++ b/src/e-util/e-misc-utils.c.help-contents-link @@ -0,0 +1,4209 @@ +/* + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, see . + * + * + * Authors: + * Chris Lahey + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#include "evolution-config.h" + +#include "e-misc-utils.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#ifdef G_OS_WIN32 +#include +#else +#include +#endif + +#include +#include + +#include + +#ifdef HAVE_LDAP +#include +#ifndef SUNLDAP +#include +#endif +#endif /* HAVE_LDAP */ + +#include "e-alert-dialog.h" +#include "e-alert-sink.h" +#include "e-client-cache.h" +#include "e-filter-option.h" +#include "e-mktemp.h" +#include "e-util-private.h" +#include "e-xml-utils.h" + +typedef struct _WindowData WindowData; + +struct _WindowData { + GtkWindow *window; + GSettings *settings; + ERestoreWindowFlags flags; + gint premax_width; + gint premax_height; + guint timeout_id; +}; + +static void +window_data_free (WindowData *data) +{ + if (data->settings != NULL) + g_object_unref (data->settings); + + if (data->timeout_id > 0) + g_source_remove (data->timeout_id); + + g_slice_free (WindowData, data); +} + +static gboolean +window_update_settings (gpointer user_data) +{ + WindowData *data = user_data; + GSettings *settings = data->settings; + + if (data->flags & E_RESTORE_WINDOW_SIZE) { + GdkWindowState state; + GdkWindow *window; + gboolean maximized; + + window = gtk_widget_get_window (GTK_WIDGET (data->window)); + state = gdk_window_get_state (window); + maximized = ((state & GDK_WINDOW_STATE_MAXIMIZED) != 0); + + g_settings_set_boolean (settings, "maximized", maximized); + + if (!maximized) { + gint width, height; + + gtk_window_get_size (data->window, &width, &height); + + g_settings_set_int (settings, "width", width); + g_settings_set_int (settings, "height", height); + } + } + + if (data->flags & E_RESTORE_WINDOW_POSITION) { + gint x, y; + + gtk_window_get_position (data->window, &x, &y); + + g_settings_set_int (settings, "x", x); + g_settings_set_int (settings, "y", y); + } + + data->timeout_id = 0; + + return FALSE; +} + +static void +window_delayed_update_settings (WindowData *data) +{ + if (data->timeout_id > 0) + g_source_remove (data->timeout_id); + + data->timeout_id = e_named_timeout_add_seconds ( + 1, window_update_settings, data); +} + +static gboolean +window_configure_event_cb (GtkWindow *window, + GdkEventConfigure *event, + WindowData *data) +{ + window_delayed_update_settings (data); + + return FALSE; +} + +static gboolean +window_state_event_cb (GtkWindow *window, + GdkEventWindowState *event, + WindowData *data) +{ + gboolean window_was_unmaximized; + + if (data->timeout_id > 0) { + g_source_remove (data->timeout_id); + data->timeout_id = 0; + } + + window_was_unmaximized = + ((event->changed_mask & GDK_WINDOW_STATE_MAXIMIZED) != 0) && + ((event->new_window_state & GDK_WINDOW_STATE_MAXIMIZED) == 0); + + if (window_was_unmaximized) { + gint width, height; + + width = data->premax_width; + data->premax_width = 0; + + height = data->premax_height; + data->premax_height = 0; + + /* This only applies when the window is initially restored + * as maximized and is then unmaximized. GTK+ handles the + * unmaximized window size thereafter. */ + if (width > 0 && height > 0) + gtk_window_resize (window, width, height); + } + + window_delayed_update_settings (data); + + return FALSE; +} + +static gboolean +window_unmap_cb (GtkWindow *window, + WindowData *data) +{ + if (data->timeout_id > 0) { + g_source_remove (data->timeout_id); + data->timeout_id = 0; + } + + /* Reset the flags so the window position and size are not + * accidentally reverted to their default value at the next run. */ + data->flags = 0; + + return FALSE; +} + +/** + * e_get_accels_filename: + * + * Returns the name of the user data file containing custom keyboard + * accelerator specifications. + * + * Returns: filename for accelerator specifications + **/ +const gchar * +e_get_accels_filename (void) +{ + static gchar *filename = NULL; + + if (G_UNLIKELY (filename == NULL)) { + const gchar *config_dir = e_get_user_config_dir (); + filename = g_build_filename (config_dir, "accels", NULL); + } + + return filename; +} + +/** + * e_show_uri: + * @parent: a parent #GtkWindow or %NULL + * @uri: the URI to show + * + * Launches the default application to show the given URI. The URI must + * be of a form understood by GIO. If the URI cannot be shown, it presents + * a dialog describing the error. The dialog is set as transient to @parent + * if @parent is non-%NULL. + **/ +void +e_show_uri (GtkWindow *parent, + const gchar *uri) +{ + GtkWidget *dialog; + GdkScreen *screen = NULL; + GError *error = NULL; + guint32 timestamp; + + g_return_if_fail (uri != NULL); + + timestamp = gtk_get_current_event_time (); + + if (parent != NULL) + screen = gtk_widget_get_screen (GTK_WIDGET (parent)); + + if (gtk_show_uri (screen, uri, timestamp, &error)) + return; + + dialog = gtk_message_dialog_new_with_markup ( + parent, GTK_DIALOG_DESTROY_WITH_PARENT, + GTK_MESSAGE_ERROR, GTK_BUTTONS_OK, + "%s", + _("Could not open the link.")); + + gtk_message_dialog_format_secondary_text ( + GTK_MESSAGE_DIALOG (dialog), "%s", error->message); + + gtk_dialog_run (GTK_DIALOG (dialog)); + + gtk_widget_destroy (dialog); + g_error_free (error); +} + +static gboolean +e_misc_utils_is_help_package_installed (void) +{ + gboolean is_installed; + gchar *path; + + /* Viewing user documentation requires the evolution help + * files. Look for one of the files it installs. */ + path = g_build_filename (EVOLUTION_DATADIR, "help", "C", PACKAGE, "index.page", NULL); + + is_installed = g_file_test (path, G_FILE_TEST_IS_REGULAR); + + g_free (path); + + if (is_installed) { + GAppInfo *help_handler; + + help_handler = g_app_info_get_default_for_uri_scheme ("help"); + + is_installed = help_handler && g_app_info_get_commandline (help_handler); + + g_clear_object (&help_handler); + } + + return is_installed; +} + +/** + * e_display_help: + * @parent: a parent #GtkWindow or %NULL + * @link_id: help section to present or %NULL + * + * Opens the user documentation to the section given by @link_id, or to the + * table of contents if @link_id is %NULL. If the user documentation cannot + * be opened, it presents a dialog describing the error. The dialog is set + * as transient to @parent if @parent is non-%NULL. + **/ +void +e_display_help (GtkWindow *parent, + const gchar *link_id) +{ + GString *uri; + GtkWidget *dialog; + GdkScreen *screen = NULL; + GError *error = NULL; + guint32 timestamp; + + if (e_misc_utils_is_help_package_installed ()) { + uri = g_string_new ("help:" PACKAGE); + } else { + uri = g_string_new ("https://help.gnome.org/users/" PACKAGE "/"); + g_string_append_printf (uri, "%d.%d", EDS_MAJOR_VERSION, EDS_MINOR_VERSION); + } + + timestamp = gtk_get_current_event_time (); + + if (parent != NULL) + screen = gtk_widget_get_screen (GTK_WIDGET (parent)); + + + if (link_id != NULL) { + g_string_append (uri, "/"); + g_string_append (uri, link_id); + } + + if (gtk_show_uri (screen, uri->str, timestamp, &error)) + goto exit; + + dialog = gtk_message_dialog_new_with_markup ( + parent, GTK_DIALOG_DESTROY_WITH_PARENT, + GTK_MESSAGE_ERROR, GTK_BUTTONS_OK, + "%s", + _("Could not display help for Evolution.")); + + gtk_message_dialog_format_secondary_text ( + GTK_MESSAGE_DIALOG (dialog), "%s", error->message); + + gtk_dialog_run (GTK_DIALOG (dialog)); + + gtk_widget_destroy (dialog); + g_error_free (error); + +exit: + g_string_free (uri, TRUE); +} + +/** + * e_restore_window: + * @window: a #GtkWindow + * @settings_path: a #GSettings path + * @flags: flags indicating which window features to restore + * + * This function can restore one of or both a window's size and position + * using #GSettings keys at @settings_path which conform to the relocatable + * schema "org.gnome.evolution.window". + * + * If #E_RESTORE_WINDOW_SIZE is present in @flags, restore @window's + * previously recorded size and maximize state. + * + * If #E_RESTORE_WINDOW_POSITION is present in @flags, move @window to + * the previously recorded screen coordinates. + * + * The respective #GSettings values will be updated when the window is + * resized and/or moved. + **/ +void +e_restore_window (GtkWindow *window, + const gchar *settings_path, + ERestoreWindowFlags flags) +{ + WindowData *data; + GSettings *settings; + const gchar *schema; + + g_return_if_fail (GTK_IS_WINDOW (window)); + g_return_if_fail (settings_path != NULL); + + schema = "org.gnome.evolution.window"; + settings = g_settings_new_with_path (schema, settings_path); + + data = g_slice_new0 (WindowData); + data->window = window; + data->settings = g_object_ref (settings); + data->flags = flags; + + if (flags & E_RESTORE_WINDOW_SIZE) { + gint width, height; + + width = g_settings_get_int (settings, "width"); + height = g_settings_get_int (settings, "height"); + + if (width > 0 && height > 0) + gtk_window_resize (window, width, height); + + if (g_settings_get_boolean (settings, "maximized")) { + GdkScreen *screen; + GdkRectangle monitor_area; + gint x, y, monitor; + + x = g_settings_get_int (settings, "x"); + y = g_settings_get_int (settings, "y"); + + screen = gtk_window_get_screen (window); + gtk_window_get_size (window, &width, &height); + + data->premax_width = width; + data->premax_height = height; + + monitor = gdk_screen_get_monitor_at_point (screen, x, y); + if (monitor < 0) + monitor = 0; + + if (monitor >= gdk_screen_get_n_monitors (screen)) + monitor = 0; + + gdk_screen_get_monitor_workarea ( + screen, monitor, &monitor_area); + + gtk_window_resize ( + window, + monitor_area.width, + monitor_area.height); + + gtk_window_maximize (window); + } + } + + if (flags & E_RESTORE_WINDOW_POSITION) { + gint x, y; + + x = g_settings_get_int (settings, "x"); + y = g_settings_get_int (settings, "y"); + + gtk_window_move (window, x, y); + } + + g_object_set_data_full ( + G_OBJECT (window), + "e-util-window-data", data, + (GDestroyNotify) window_data_free); + + g_signal_connect ( + window, "configure-event", + G_CALLBACK (window_configure_event_cb), data); + + g_signal_connect ( + window, "window-state-event", + G_CALLBACK (window_state_event_cb), data); + + g_signal_connect ( + window, "unmap", + G_CALLBACK (window_unmap_cb), data); + + g_object_unref (settings); +} + +/** + * e_lookup_action: + * @ui_manager: a #GtkUIManager + * @action_name: the name of an action + * + * Returns the first #GtkAction named @action_name by traversing the + * list of action groups in @ui_manager. If no such action exists, the + * function emits a critical warning before returning %NULL, since this + * probably indicates a programming error and most code is not prepared + * to deal with lookup failures. + * + * Returns: the first #GtkAction named @action_name + **/ +GtkAction * +e_lookup_action (GtkUIManager *ui_manager, + const gchar *action_name) +{ + GtkAction *action = NULL; + GList *iter; + + g_return_val_if_fail (GTK_IS_UI_MANAGER (ui_manager), NULL); + g_return_val_if_fail (action_name != NULL, NULL); + + iter = gtk_ui_manager_get_action_groups (ui_manager); + + while (iter != NULL) { + GtkActionGroup *action_group = iter->data; + + action = gtk_action_group_get_action ( + action_group, action_name); + if (action != NULL) + return action; + + iter = g_list_next (iter); + } + + g_critical ("%s: action '%s' not found", G_STRFUNC, action_name); + + return NULL; +} + +/** + * e_lookup_action_group: + * @ui_manager: a #GtkUIManager + * @group_name: the name of an action group + * + * Returns the #GtkActionGroup in @ui_manager named @group_name. If no + * such action group exists, the function emits a critical warnings before + * returning %NULL, since this probably indicates a programming error and + * most code is not prepared to deal with lookup failures. + * + * Returns: the #GtkActionGroup named @group_name + **/ +GtkActionGroup * +e_lookup_action_group (GtkUIManager *ui_manager, + const gchar *group_name) +{ + GList *iter; + + g_return_val_if_fail (GTK_IS_UI_MANAGER (ui_manager), NULL); + g_return_val_if_fail (group_name != NULL, NULL); + + iter = gtk_ui_manager_get_action_groups (ui_manager); + + while (iter != NULL) { + GtkActionGroup *action_group = iter->data; + const gchar *name; + + name = gtk_action_group_get_name (action_group); + if (strcmp (name, group_name) == 0) + return action_group; + + iter = g_list_next (iter); + } + + g_critical ("%s: action group '%s' not found", G_STRFUNC, group_name); + + return NULL; +} + +/** + * e_action_compare_by_label: + * @action1: a #GtkAction + * @action2: a #GtkAction + * + * Compares the labels for @action1 and @action2 using g_utf8_collate(). + * + * Returns: < 0 if @action1 compares before @action2, 0 if they + * compare equal, > 0 if @action1 compares after @action2 + **/ +gint +e_action_compare_by_label (GtkAction *action1, + GtkAction *action2) +{ + gchar *label1; + gchar *label2; + gint result; + + /* XXX This is horribly inefficient but will generally only be + * used on short lists of actions during UI construction. */ + + if (action1 == action2) + return 0; + + g_object_get (action1, "label", &label1, NULL); + g_object_get (action2, "label", &label2, NULL); + + result = g_utf8_collate (label1, label2); + + g_free (label1); + g_free (label2); + + return result; +} + +/** + * e_action_group_remove_all_actions: + * @action_group: a #GtkActionGroup + * + * Removes all actions from the action group. + **/ +void +e_action_group_remove_all_actions (GtkActionGroup *action_group) +{ + GList *list, *iter; + + /* XXX I've proposed this function for inclusion in GTK+. + * GtkActionGroup stores actions in an internal hash + * table and can do this more efficiently by calling + * g_hash_table_remove_all(). + * + * http://bugzilla.gnome.org/show_bug.cgi?id=550485 */ + + g_return_if_fail (GTK_IS_ACTION_GROUP (action_group)); + + list = gtk_action_group_list_actions (action_group); + for (iter = list; iter != NULL; iter = iter->next) + gtk_action_group_remove_action (action_group, iter->data); + g_list_free (list); +} + +/** + * e_radio_action_get_current_action: + * @radio_action: a #GtkRadioAction + * + * Returns the currently active member of the group to which @radio_action + * belongs. + * + * Returns: the currently active group member + **/ +GtkRadioAction * +e_radio_action_get_current_action (GtkRadioAction *radio_action) +{ + GSList *group; + gint current_value; + + g_return_val_if_fail (GTK_IS_RADIO_ACTION (radio_action), NULL); + + group = gtk_radio_action_get_group (radio_action); + current_value = gtk_radio_action_get_current_value (radio_action); + + while (group != NULL) { + gint value; + + radio_action = GTK_RADIO_ACTION (group->data); + g_object_get (radio_action, "value", &value, NULL); + + if (value == current_value) + return radio_action; + + group = g_slist_next (group); + } + + return NULL; +} + +/** + * e_action_group_add_actions_localized: + * @action_group: a #GtkActionGroup to add @entries to + * @translation_domain: a translation domain to use + * to translate label and tooltip strings in @entries + * @entries: (array length=n_entries): an array of action descriptions + * @n_entries: the number of entries + * @user_data: data to pass to the action callbacks + * + * Adds #GtkAction-s defined by @entries to @action_group, with action's + * label and tooltip localized in the given translation domain, instead + * of the domain set on the @action_group. + * + * Since: 3.4 + **/ +void +e_action_group_add_actions_localized (GtkActionGroup *action_group, + const gchar *translation_domain, + const GtkActionEntry *entries, + guint n_entries, + gpointer user_data) +{ + GtkActionGroup *tmp_group; + GList *list, *iter; + gint ii; + + g_return_if_fail (action_group != NULL); + g_return_if_fail (entries != NULL); + g_return_if_fail (n_entries > 0); + g_return_if_fail (translation_domain != NULL); + g_return_if_fail (*translation_domain); + + tmp_group = gtk_action_group_new ("temporary-group"); + gtk_action_group_set_translation_domain (tmp_group, translation_domain); + gtk_action_group_add_actions (tmp_group, entries, n_entries, user_data); + + list = gtk_action_group_list_actions (tmp_group); + for (iter = list; iter != NULL; iter = iter->next) { + GtkAction *action = GTK_ACTION (iter->data); + const gchar *action_name; + + g_object_ref (action); + + action_name = gtk_action_get_name (action); + + for (ii = 0; ii < n_entries; ii++) { + if (g_strcmp0 (entries[ii].name, action_name) == 0) { + gtk_action_group_remove_action ( + tmp_group, action); + gtk_action_group_add_action_with_accel ( + action_group, action, + entries[ii].accelerator); + break; + } + } + + g_object_unref (action); + } + + g_list_free (list); + g_object_unref (tmp_group); +} + +/** + * e_builder_get_widget: + * @builder: a #GtkBuilder + * @widget_name: name of a widget in @builder + * + * Gets the widget named @widget_name. Note that this function does not + * increment the reference count of the returned widget. If @widget_name + * could not be found in the @builder's object tree, a run-time + * warning is emitted since this usually indicates a programming error. + * + * This is a convenience function to work around the awkwardness of + * #GtkBuilder returning #GObject pointers, when the vast majority of + * the time you want a #GtkWidget pointer. + * + * If you need something from @builder other than a #GtkWidget, or you + * want to test for the existence of some widget name without incurring + * a run-time warning, use gtk_builder_get_object(). + * + * Returns: the widget named @widget_name, or %NULL + **/ +GtkWidget * +e_builder_get_widget (GtkBuilder *builder, + const gchar *widget_name) +{ + GObject *object; + + g_return_val_if_fail (GTK_IS_BUILDER (builder), NULL); + g_return_val_if_fail (widget_name != NULL, NULL); + + object = gtk_builder_get_object (builder, widget_name); + if (object == NULL) { + g_warning ("Could not find widget '%s'", widget_name); + return NULL; + } + + return GTK_WIDGET (object); +} + +/** + * e_load_ui_builder_definition: + * @builder: a #GtkBuilder + * @basename: basename of the UI definition file + * + * Loads a UI definition into @builder from Evolution's UI directory. + * Failure here is fatal, since the application can't function without + * its UI definitions. + **/ +void +e_load_ui_builder_definition (GtkBuilder *builder, + const gchar *basename) +{ + gchar *filename; + GError *error = NULL; + + g_return_if_fail (GTK_IS_BUILDER (builder)); + g_return_if_fail (basename != NULL); + + filename = g_build_filename (EVOLUTION_UIDIR, basename, NULL); + gtk_builder_add_from_file (builder, filename, &error); + g_free (filename); + + if (error != NULL) { + g_error ("%s: %s", basename, error->message); + g_warn_if_reached (); + } +} + +static gdouble +e_get_ui_manager_definition_file_version (const gchar *filename) +{ + xmlDocPtr doc; + xmlNode *root; + gdouble version = -1.0; + + g_return_val_if_fail (filename != NULL, version); + + doc = e_xml_parse_file (filename); + if (!doc) + return version; + + root = xmlDocGetRootElement (doc); + if (root && g_strcmp0 ((const gchar *) root->name, "ui") == 0) { + version = e_xml_get_double_prop_by_name_with_default (root, (const xmlChar *) "evolution-ui-version", -1.0); + } + + xmlFreeDoc (doc); + + return version; +} + +static gchar * +e_pick_ui_manager_definition_file (const gchar *basename) +{ + gchar *system_filename, *user_filename; + gdouble system_version, user_version; + + g_return_val_if_fail (basename != NULL, NULL); + + system_filename = g_build_filename (EVOLUTION_UIDIR, basename, NULL); + user_filename = g_build_filename (e_get_user_config_dir (), "ui", basename, NULL); + + if (!g_file_test (user_filename, G_FILE_TEST_EXISTS | G_FILE_TEST_IS_REGULAR)) { + g_free (user_filename); + + return system_filename; + } + + user_version = e_get_ui_manager_definition_file_version (user_filename); + system_version = e_get_ui_manager_definition_file_version (system_filename); + + /* Versions are equal and the system version is a positive number */ + if (user_version - system_version >= -1e-9 && + user_version - system_version <= 1e-9 && + system_version > 1e-9) { + g_free (system_filename); + + return user_filename; + } + + g_warning ("User's UI file '%s' version (%.1f) doesn't match expected version (%.1f), skipping it. Either correct the version or remove the file.", + user_filename, user_version, system_version); + + g_free (user_filename); + + return system_filename; +} + +/** + * e_load_ui_manager_definition: + * @ui_manager: a #GtkUIManager + * @basename: basename of the UI definition file + * + * Loads a UI definition into @ui_manager from Evolution's UI directory. + * Failure here is fatal, since the application can't function without + * its UI definitions. + * + * Returns: The merge ID for the merged UI. The merge ID can be used to + * unmerge the UI with gtk_ui_manager_remove_ui(). + **/ +guint +e_load_ui_manager_definition (GtkUIManager *ui_manager, + const gchar *basename) +{ + gchar *filename; + guint merge_id; + GError *error = NULL; + + g_return_val_if_fail (GTK_IS_UI_MANAGER (ui_manager), 0); + g_return_val_if_fail (basename != NULL, 0); + + filename = e_pick_ui_manager_definition_file (basename); + merge_id = gtk_ui_manager_add_ui_from_file ( + ui_manager, filename, &error); + g_free (filename); + + if (error != NULL) { + g_error ("%s: %s", basename, error->message); + g_warn_if_reached (); + } + + return merge_id; +} + +/* Helper for e_categories_add_change_hook() */ +static void +categories_changed_cb (GObject *useless_opaque_object, + GHookList *hook_list) +{ + /* e_categories_register_change_listener() is broken because + * it requires callbacks to allow for some opaque GObject as + * the first argument (not does it document this). */ + g_hook_list_invoke (hook_list, FALSE); +} + +/* Helper for e_categories_add_change_hook() */ +static void +categories_weak_notify_cb (GHookList *hook_list, + gpointer where_the_object_was) +{ + GHook *hook; + + /* This should not happen, but if we fail to find the hook for + * some reason, g_hook_destroy_link() will warn about the NULL + * pointer, which is all we would do anyway so no need to test + * for it ourselves. */ + hook = g_hook_find_data (hook_list, TRUE, where_the_object_was); + g_hook_destroy_link (hook_list, hook); +} + +/** + * e_categories_add_change_hook: + * @func: a hook function + * @object: a #GObject to be passed to @func, or %NULL + * + * A saner alternative to e_categories_register_change_listener(). + * + * Adds a hook function to be called when a category is added, removed or + * modified. If @object is not %NULL, the hook function is automatically + * removed when @object is finalized. + **/ +void +e_categories_add_change_hook (GHookFunc func, + gpointer object) +{ + static gboolean initialized = FALSE; + static GHookList hook_list; + GHook *hook; + + g_return_if_fail (func != NULL); + + if (object != NULL) + g_return_if_fail (G_IS_OBJECT (object)); + + if (!initialized) { + g_hook_list_init (&hook_list, sizeof (GHook)); + e_categories_register_change_listener ( + G_CALLBACK (categories_changed_cb), &hook_list); + initialized = TRUE; + } + + hook = g_hook_alloc (&hook_list); + + hook->func = func; + hook->data = object; + + if (object != NULL) + g_object_weak_ref ( + G_OBJECT (object), (GWeakNotify) + categories_weak_notify_cb, &hook_list); + + g_hook_append (&hook_list, hook); +} + +/** + * e_flexible_strtod: + * @nptr: the string to convert to a numeric value. + * @endptr: if non-NULL, it returns the character after + * the last character used in the conversion. + * + * Converts a string to a gdouble value. This function detects + * strings either in the standard C locale or in the current locale. + * + * This function is typically used when reading configuration files or + * other non-user input that should not be locale dependent, but may + * have been in the past. To handle input from the user you should + * normally use the locale-sensitive system strtod function. + * + * To convert from a double to a string in a locale-insensitive way, use + * @g_ascii_dtostr. + * + * Returns: the gdouble value + **/ +gdouble +e_flexible_strtod (const gchar *nptr, + gchar **endptr) +{ + gchar *fail_pos; + gdouble val; + struct lconv *locale_data; + const gchar *decimal_point; + gint decimal_point_len; + const gchar *p, *decimal_point_pos; + const gchar *end = NULL; /* Silence gcc */ + gchar *copy, *c; + + g_return_val_if_fail (nptr != NULL, 0); + + fail_pos = NULL; + + locale_data = localeconv (); + decimal_point = locale_data->decimal_point; + decimal_point_len = strlen (decimal_point); + + g_return_val_if_fail (decimal_point_len != 0, 0); + + decimal_point_pos = NULL; + if (!strcmp (decimal_point, ".")) + return strtod (nptr, endptr); + + p = nptr; + + /* Skip leading space */ + while (isspace ((guchar) * p)) + p++; + + /* Skip leading optional sign */ + if (*p == '+' || *p == '-') + p++; + + if (p[0] == '0' && + (p[1] == 'x' || p[1] == 'X')) { + p += 2; + /* HEX - find the (optional) decimal point */ + + while (isxdigit ((guchar) * p)) + p++; + + if (*p == '.') { + decimal_point_pos = p++; + + while (isxdigit ((guchar) * p)) + p++; + + if (*p == 'p' || *p == 'P') + p++; + if (*p == '+' || *p == '-') + p++; + while (isdigit ((guchar) * p)) + p++; + end = p; + } else if (strncmp (p, decimal_point, decimal_point_len) == 0) { + return strtod (nptr, endptr); + } + } else { + while (isdigit ((guchar) * p)) + p++; + + if (*p == '.') { + decimal_point_pos = p++; + + while (isdigit ((guchar) * p)) + p++; + + if (*p == 'e' || *p == 'E') + p++; + if (*p == '+' || *p == '-') + p++; + while (isdigit ((guchar) * p)) + p++; + end = p; + } else if (strncmp (p, decimal_point, decimal_point_len) == 0) { + return strtod (nptr, endptr); + } + } + /* For the other cases, we need not convert the decimal point */ + + if (!decimal_point_pos) + return strtod (nptr, endptr); + + /* We need to convert the '.' to the locale specific decimal point */ + copy = g_malloc (end - nptr + 1 + decimal_point_len); + + c = copy; + memcpy (c, nptr, decimal_point_pos - nptr); + c += decimal_point_pos - nptr; + memcpy (c, decimal_point, decimal_point_len); + c += decimal_point_len; + memcpy (c, decimal_point_pos + 1, end - (decimal_point_pos + 1)); + c += end - (decimal_point_pos + 1); + *c = 0; + + val = strtod (copy, &fail_pos); + + if (fail_pos) { + if (fail_pos > decimal_point_pos) + fail_pos = + (gchar *) nptr + (fail_pos - copy) - + (decimal_point_len - 1); + else + fail_pos = (gchar *) nptr + (fail_pos - copy); + } + + g_free (copy); + + if (endptr) + *endptr = fail_pos; + + return val; +} + +/** + * e_ascii_dtostr: + * @buffer: A buffer to place the resulting string in + * @buf_len: The length of the buffer. + * @format: The printf-style format to use for the + * code to use for converting. + * @d: The double to convert + * + * Converts a double to a string, using the '.' as + * decimal_point. To format the number you pass in + * a printf-style formating string. Allowed conversion + * specifiers are eEfFgG. + * + * If you want to generates enough precision that converting + * the string back using @g_strtod gives the same machine-number + * (on machines with IEEE compatible 64bit doubles) use the format + * string "%.17g". If you do this it is guaranteed that the size + * of the resulting string will never be larger than + * @G_ASCII_DTOSTR_BUF_SIZE bytes. + * + * Returns: the pointer to the buffer with the converted string + **/ +gchar * +e_ascii_dtostr (gchar *buffer, + gint buf_len, + const gchar *format, + gdouble d) +{ + struct lconv *locale_data; + const gchar *decimal_point; + gint decimal_point_len; + gchar *p; + gint rest_len; + gchar format_char; + + g_return_val_if_fail (buffer != NULL, NULL); + g_return_val_if_fail (format[0] == '%', NULL); + g_return_val_if_fail (strpbrk (format + 1, "'l%") == NULL, NULL); + + format_char = format[strlen (format) - 1]; + + g_return_val_if_fail (format_char == 'e' || format_char == 'E' || + format_char == 'f' || format_char == 'F' || + format_char == 'g' || format_char == 'G', + NULL); + + if (format[0] != '%') + return NULL; + + if (strpbrk (format + 1, "'l%")) + return NULL; + + if (!(format_char == 'e' || format_char == 'E' || + format_char == 'f' || format_char == 'F' || + format_char == 'g' || format_char == 'G')) + return NULL; + + g_snprintf (buffer, buf_len, format, d); + + locale_data = localeconv (); + decimal_point = locale_data->decimal_point; + decimal_point_len = strlen (decimal_point); + + g_return_val_if_fail (decimal_point_len != 0, NULL); + + if (strcmp (decimal_point, ".")) { + p = buffer; + + if (*p == '+' || *p == '-') + p++; + + while (isdigit ((guchar) * p)) + p++; + + if (strncmp (p, decimal_point, decimal_point_len) == 0) { + *p = '.'; + p++; + if (decimal_point_len > 1) { + rest_len = strlen (p + (decimal_point_len - 1)); + memmove ( + p, p + (decimal_point_len - 1), + rest_len); + p[rest_len] = 0; + } + } + } + + return buffer; +} + +/** + * e_str_without_underscores: + * @string: the string to strip underscores from + * + * Strips underscores from a string in the same way + * @gtk_label_new_with_mnemonics does. The returned string should be freed + * using g_free(). + * + * Returns: a newly-allocated string without underscores + */ +gchar * +e_str_without_underscores (const gchar *string) +{ + gchar *new_string; + const gchar *sp; + gchar *dp; + + new_string = g_malloc (strlen (string) + 1); + + dp = new_string; + for (sp = string; *sp != '\0'; sp++) { + if (*sp != '_') { + *dp = *sp; + dp++; + } else if (sp[1] == '_') { + /* Translate "__" in "_". */ + *dp = '_'; + dp++; + sp++; + } + } + *dp = 0; + + return new_string; +} + +/** + * e_str_replace_string + * @text: the string to replace + * @before: the string to be replaced + * @after: the string to replaced with + * + * Replaces every occurrence of the string @before with the string @after in + * the string @text and returns a #GString with result that should be freed + * with g_string_free(). + * + * Returns: a newly-allocated #GString + */ +GString * +e_str_replace_string (const gchar *text, + const gchar *before, + const gchar *after) +{ + const gchar *p, *next; + GString *str; + gint find_len; + + g_return_val_if_fail (text != NULL, NULL); + g_return_val_if_fail (before != NULL, NULL); + g_return_val_if_fail (*before, NULL); + + find_len = strlen (before); + str = g_string_new (""); + + p = text; + while (next = strstr (p, before), next) { + if (p < next) + g_string_append_len (str, p, next - p); + + if (after && *after) + g_string_append (str, after); + + p = next + find_len; + } + + g_string_append (str, p); + + return str; +} + +gint +e_str_compare (gconstpointer x, + gconstpointer y) +{ + if (x == NULL || y == NULL) { + if (x == y) + return 0; + else + return x ? -1 : 1; + } + + return strcmp (x, y); +} + +gint +e_str_case_compare (gconstpointer x, + gconstpointer y) +{ + gchar *cx, *cy; + gint res; + + if (x == NULL || y == NULL) { + if (x == y) + return 0; + else + return x ? -1 : 1; + } + + cx = g_utf8_casefold (x, -1); + cy = g_utf8_casefold (y, -1); + + res = g_utf8_collate (cx, cy); + + g_free (cx); + g_free (cy); + + return res; +} + +gint +e_collate_compare (gconstpointer x, + gconstpointer y) +{ + if (x == NULL || y == NULL) { + if (x == y) + return 0; + else + return x ? -1 : 1; + } + + return g_utf8_collate (x, y); +} + +gint +e_int_compare (gconstpointer x, + gconstpointer y) +{ + gint nx = GPOINTER_TO_INT (x); + gint ny = GPOINTER_TO_INT (y); + + return (nx == ny) ? 0 : (nx < ny) ? -1 : 1; +} + +/** + * e_color_to_value: + * @color: a #GdkColor + * + * Converts a #GdkColor to a 24-bit RGB color value. + * + * Returns: a 24-bit color value + **/ +guint32 +e_color_to_value (const GdkColor *color) +{ + GdkRGBA rgba; + + g_return_val_if_fail (color != NULL, 0); + + rgba.red = color->red / 65535.0; + rgba.green = color->green / 65535.0; + rgba.blue = color->blue / 65535.0; + rgba.alpha = 0.0; + + return e_rgba_to_value (&rgba); +} + +/** + * e_rgba_to_value: + * @rgba: a #GdkRGBA + * + * Converts #GdkRGBA to a 24-bit RGB color value + * + * Returns: a 24-bit color value + **/ +guint32 +e_rgba_to_value (const GdkRGBA *rgba) +{ + guint16 red; + guint16 green; + guint16 blue; + + g_return_val_if_fail (rgba != NULL, 0); + + red = 255 * rgba->red; + green = 255 * rgba->green; + blue = 255 * rgba->blue; + + return (guint32) + ((((red & 0xFF) << 16) | + ((green & 0xFF) << 8) | + (blue & 0xFF)) & 0xffffff); +} + +/** + * e_rgba_to_color: + * @rgba: a source #GdkRGBA + * @color: a destination #GdkColor + * + * Converts @rgba into @color, but loses the alpha channel from @rgba. + **/ +void +e_rgba_to_color (const GdkRGBA *rgba, + GdkColor *color) +{ + g_return_if_fail (rgba != NULL); + g_return_if_fail (color != NULL); + + color->pixel = 0; + color->red = rgba->red * 65535.0; + color->green = rgba->green * 65535.0; + color->blue = rgba->blue * 65535.0; +} + +/** + * e_utils_get_theme_color: + * @widget: a #GtkWidget instance + * @color_names: comma-separated theme color names + * @fallback_color_ident: fallback color identificator, in a format for gdk_rgba_parse() + * @rgba: where to store the read color + * + * Reads named theme color from a #GtkStyleContext of @widget. + * The @color_names are read one after another from left to right, + * the next are meant as fallbacks, in case the theme doesn't + * define the previous color. If none is found then the @fallback_color_ident + * is set to @rgba. + **/ +void +e_utils_get_theme_color (GtkWidget *widget, + const gchar *color_names, + const gchar *fallback_color_ident, + GdkRGBA *rgba) +{ + GtkStyleContext *style_context; + gchar **names; + gint ii; + + g_return_if_fail (GTK_IS_WIDGET (widget)); + g_return_if_fail (color_names != NULL); + g_return_if_fail (fallback_color_ident != NULL); + g_return_if_fail (rgba != NULL); + + style_context = gtk_widget_get_style_context (widget); + + names = g_strsplit (color_names, ",", -1); + for (ii = 0; names && names[ii]; ii++) { + if (gtk_style_context_lookup_color (style_context, names[ii], rgba)) { + g_strfreev (names); + return; + } + } + + g_strfreev (names); + + g_warn_if_fail (gdk_rgba_parse (rgba, fallback_color_ident)); +} + +/** + * e_utils_get_theme_color_color: + * @widget: a #GtkWidget instance + * @color_names: comma-separated theme color names + * @fallback_color_ident: fallback color identificator, in a format for gdk_rgba_parse() + * @color: where to store the read color + * + * The same as e_utils_get_theme_color(), only populates #GdkColor, + * instead of #GdkRGBA. + **/ +void +e_utils_get_theme_color_color (GtkWidget *widget, + const gchar *color_names, + const gchar *fallback_color_ident, + GdkColor *color) +{ + GdkRGBA rgba; + + g_return_if_fail (GTK_IS_WIDGET (widget)); + g_return_if_fail (color_names != NULL); + g_return_if_fail (fallback_color_ident != NULL); + g_return_if_fail (color != NULL); + + e_utils_get_theme_color (widget, color_names, fallback_color_ident, &rgba); + + e_rgba_to_color (&rgba, color); +} + +/* This is copied from gtk+ sources */ +static void +rgb_to_hls (gdouble *r, + gdouble *g, + gdouble *b) +{ + gdouble min; + gdouble max; + gdouble red; + gdouble green; + gdouble blue; + gdouble h, l, s; + gdouble delta; + + red = *r; + green = *g; + blue = *b; + + if (red > green) { + if (red > blue) + max = red; + else + max = blue; + + if (green < blue) + min = green; + else + min = blue; + } else { + if (green > blue) + max = green; + else + max = blue; + + if (red < blue) + min = red; + else + min = blue; + } + + l = (max + min) / 2; + s = 0; + h = 0; + + if (max != min) { + if (l <= 0.5) + s = (max - min) / (max + min); + else + s = (max - min) / (2 - max - min); + + delta = max -min; + if (red == max) + h = (green - blue) / delta; + else if (green == max) + h = 2 + (blue - red) / delta; + else if (blue == max) + h = 4 + (red - green) / delta; + + h *= 60; + if (h < 0.0) + h += 360; + } + + *r = h; + *g = l; + *b = s; +} + +/* This is copied from gtk+ sources */ +static void +hls_to_rgb (gdouble *h, + gdouble *l, + gdouble *s) +{ + gdouble hue; + gdouble lightness; + gdouble saturation; + gdouble m1, m2; + gdouble r, g, b; + + lightness = *l; + saturation = *s; + + if (lightness <= 0.5) + m2 = lightness * (1 + saturation); + else + m2 = lightness + saturation - lightness * saturation; + m1 = 2 * lightness - m2; + + if (saturation == 0) { + *h = lightness; + *l = lightness; + *s = lightness; + } else { + hue = *h + 120; + while (hue > 360) + hue -= 360; + while (hue < 0) + hue += 360; + + if (hue < 60) + r = m1 + (m2 - m1) * hue / 60; + else if (hue < 180) + r = m2; + else if (hue < 240) + r = m1 + (m2 - m1) * (240 - hue) / 60; + else + r = m1; + + hue = *h; + while (hue > 360) + hue -= 360; + while (hue < 0) + hue += 360; + + if (hue < 60) + g = m1 + (m2 - m1) * hue / 60; + else if (hue < 180) + g = m2; + else if (hue < 240) + g = m1 + (m2 - m1) * (240 - hue) / 60; + else + g = m1; + + hue = *h - 120; + while (hue > 360) + hue -= 360; + while (hue < 0) + hue += 360; + + if (hue < 60) + b = m1 + (m2 - m1) * hue / 60; + else if (hue < 180) + b = m2; + else if (hue < 240) + b = m1 + (m2 - m1) * (240 - hue) / 60; + else + b = m1; + + *h = r; + *l = g; + *s = b; + } +} + +/* This is copied from gtk+ sources */ +void +e_utils_shade_color (const GdkRGBA *a, + GdkRGBA *b, + gdouble mult) +{ + gdouble red; + gdouble green; + gdouble blue; + + g_return_if_fail (a != NULL); + g_return_if_fail (b != NULL); + + red = a->red; + green = a->green; + blue = a->blue; + + rgb_to_hls (&red, &green, &blue); + + green *= mult; + if (green > 1.0) + green = 1.0; + else if (green < 0.0) + green = 0.0; + + blue *= mult; + if (blue > 1.0) + blue = 1.0; + else if (blue < 0.0) + blue = 0.0; + + hls_to_rgb (&red, &green, &blue); + + b->red = red; + b->green = green; + b->blue = blue; + b->alpha = a->alpha; +} + +static gint +epow10 (gint number) +{ + gint value = 1; + + while (number-- > 0) + value *= 10; + + return value; +} + +gchar * +e_format_number (gint number) +{ + GList *iterator, *list = NULL; + struct lconv *locality; + gint char_length = 0; + gint group_count = 0; + gchar *grouping; + gint last_count = 3; + gint divider; + gchar *value; + gchar *value_iterator; + + locality = localeconv (); + grouping = locality->grouping; + while (number) { + gchar *group; + switch (*grouping) { + default: + last_count = *grouping; + grouping++; + /* coverity[fallthrough] */ + case 0: + divider = epow10 (last_count); + if (number >= divider) { + group = g_strdup_printf ( + "%0*d", last_count, number % divider); + } else { + group = g_strdup_printf ( + "%d", number % divider); + } + number /= divider; + break; + case CHAR_MAX: + group = g_strdup_printf ("%d", number); + number = 0; + break; + } + char_length += strlen (group); + list = g_list_prepend (list, group); + group_count++; + } + + if (list) { + value = g_new ( + gchar, 1 + char_length + (group_count - 1) * + strlen (locality->thousands_sep)); + + iterator = list; + value_iterator = value; + + strcpy (value_iterator, iterator->data); + value_iterator += strlen (iterator->data); + for (iterator = iterator->next; iterator; iterator = iterator->next) { + strcpy (value_iterator, locality->thousands_sep); + value_iterator += strlen (locality->thousands_sep); + + strcpy (value_iterator, iterator->data); + value_iterator += strlen (iterator->data); + } + g_list_foreach (list, (GFunc) g_free, NULL); + g_list_free (list); + return value; + } else { + return g_strdup ("0"); + } +} + +/* Perform a binary search for key in base which has nmemb elements + * of size bytes each. The comparisons are done by (*compare)(). */ +void +e_bsearch (gconstpointer key, + gconstpointer base, + gsize nmemb, + gsize size, + ESortCompareFunc compare, + gpointer closure, + gsize *start, + gsize *end) +{ + gsize l, u, idx; + gconstpointer p; + gint comparison; + if (!(start || end)) + return; + + l = 0; + u = nmemb; + while (l < u) { + idx = (l + u) / 2; + p = (((const gchar *) base) + (idx * size)); + comparison = (*compare) (key, p, closure); + if (comparison < 0) + u = idx; + else if (comparison > 0) + l = idx + 1; + else { + gsize lsave, usave; + lsave = l; + usave = u; + if (start) { + while (l < u) { + idx = (l + u) / 2; + p = (((const gchar *) base) + (idx * size)); + comparison = (*compare) (key, p, closure); + if (comparison <= 0) + u = idx; + else + l = idx + 1; + } + *start = l; + + l = lsave; + u = usave; + } + if (end) { + while (l < u) { + idx = (l + u) / 2; + p = (((const gchar *) base) + (idx * size)); + comparison = (*compare) (key, p, closure); + if (comparison < 0) + u = idx; + else + l = idx + 1; + } + *end = l; + } + return; + } + } + + if (start) + *start = l; + if (end) + *end = l; +} + +/* Function to do a last minute fixup of the AM/PM stuff if the locale + * and gettext haven't done it right. Most English speaking countries + * except the USA use the 24 hour clock (UK, Australia etc). However + * since they are English nobody bothers to write a language + * translation (gettext) file. So the locale turns off the AM/PM, but + * gettext does not turn on the 24 hour clock. Leaving a mess. + * + * This routine checks if AM/PM are defined in the locale, if not it + * forces the use of the 24 hour clock. + * + * The function itself is a front end on strftime and takes exactly + * the same arguments. + * + * TODO: Actually remove the '%p' from the fixed up string so that + * there isn't a stray space. + */ + +gsize +e_strftime_fix_am_pm (gchar *str, + gsize max, + const gchar *fmt, + const struct tm *tm) +{ + gchar buf[10]; + gchar *sp; + gchar *ffmt; + gsize ret; + + if (strstr (fmt, "%p") == NULL && strstr (fmt, "%P") == NULL) { + /* No AM/PM involved - can use the fmt string directly */ + ret = e_strftime (str, max, fmt, tm); + } else { + /* Get the AM/PM symbol from the locale */ + e_strftime (buf, 10, "%p", tm); + + if (buf[0]) { + /* AM/PM have been defined in the locale + * so we can use the fmt string directly. */ + ret = e_strftime (str, max, fmt, tm); + } else { + /* No AM/PM defined by locale + * must change to 24 hour clock. */ + ffmt = g_strdup (fmt); + for (sp = ffmt; (sp = strstr (sp, "%l")); sp++) { + /* Maybe this should be 'k', but I have never + * seen a 24 clock actually use that format. */ + sp[1]='H'; + } + for (sp = ffmt; (sp = strstr (sp, "%I")); sp++) { + sp[1]='H'; + } + ret = e_strftime (str, max, ffmt, tm); + g_free (ffmt); + } + } + + return (ret); +} + +gsize +e_utf8_strftime_fix_am_pm (gchar *str, + gsize max, + const gchar *fmt, + const struct tm *tm) +{ + gsize sz, ret; + gchar *locale_fmt, *buf; + + locale_fmt = g_locale_from_utf8 (fmt, -1, NULL, &sz, NULL); + if (!locale_fmt) + return 0; + + ret = e_strftime_fix_am_pm (str, max, locale_fmt, tm); + if (!ret) { + g_free (locale_fmt); + return 0; + } + + buf = g_locale_to_utf8 (str, ret, NULL, &sz, NULL); + if (!buf) { + g_free (locale_fmt); + return 0; + } + + if (sz >= max) { + gchar *tmp = buf + max - 1; + tmp = g_utf8_find_prev_char (buf, tmp); + if (tmp) + sz = tmp - buf; + else + sz = 0; + } + memcpy (str, buf, sz); + str[sz] = '\0'; + g_free (locale_fmt); + g_free (buf); + return sz; +} + +/** + * e_utf8_strftime_match_lc_messages: + * @string: The string to store the result in. + * @max: The size of the @string. + * @fmt: The formatting to use on @tm. + * @tm: The time value to format. + * + * The UTF-8 equivalent of e_strftime (), which also + * makes sure that the locale used for time and date + * formatting matches the locale used by the + * application so that, for example, the quoted + * message header produced by the mail composer in a + * reply uses only one locale (i.e. LC_MESSAGES, where + * available, overrides LC_TIME for consistency). + * + * Returns: The number of characters placed in @string. + * + * Since: 3.22 + **/ +gsize +e_utf8_strftime_match_lc_messages (gchar *string, + gsize max, + const gchar *fmt, + const struct tm *tm) +{ + gsize ret; +#if defined(LC_MESSAGES) && defined(LC_TIME) + gchar *ctime, *cmessages, *saved_locale; + + /* Use LC_MESSAGES instead of LC_TIME for time + * formatting (for consistency). + */ + ctime = setlocale (LC_TIME, NULL); + saved_locale = g_strdup (ctime); + g_return_val_if_fail (saved_locale != NULL, 0); + cmessages = setlocale (LC_MESSAGES, NULL); + setlocale (LC_TIME, cmessages); +#endif + + ret = e_utf8_strftime(string, max, fmt, tm); + +#if defined(LC_MESSAGES) && defined(LC_TIME) + /* Restore LC_TIME, if it has been changed to match + * LC_MESSAGES. + */ + setlocale (LC_TIME, saved_locale); + g_free (saved_locale); +#endif + + return ret; +} + +/** + * e_get_month_name: + * @month: month index + * @abbreviated: if %TRUE, abbreviate the month name + * + * Returns the localized name for @month. If @abbreviated is %TRUE, + * returns the locale's abbreviated month name. + * + * Returns: localized month name + **/ +const gchar * +e_get_month_name (GDateMonth month, + gboolean abbreviated) +{ + /* Make the indices correspond to the enum values. */ + static const gchar *abbr_names[G_DATE_DECEMBER + 1]; + static const gchar *full_names[G_DATE_DECEMBER + 1]; + static gboolean first_time = TRUE; + + g_return_val_if_fail (month >= G_DATE_JANUARY, NULL); + g_return_val_if_fail (month <= G_DATE_DECEMBER, NULL); + + if (G_UNLIKELY (first_time)) { + gchar buffer[256]; + GDateMonth ii; + GDate date; + + memset (abbr_names, 0, sizeof (abbr_names)); + memset (full_names, 0, sizeof (full_names)); + + /* First Julian day was in January. */ + g_date_set_julian (&date, 1); + + for (ii = G_DATE_JANUARY; ii <= G_DATE_DECEMBER; ii++) { + g_date_strftime (buffer, sizeof (buffer), "%b", &date); + abbr_names[ii] = g_intern_string (buffer); + g_date_strftime (buffer, sizeof (buffer), "%B", &date); + full_names[ii] = g_intern_string (buffer); + g_date_add_months (&date, 1); + } + + first_time = FALSE; + } + + return abbreviated ? abbr_names[month] : full_names[month]; +} + +/** + * e_get_weekday_name: + * @weekday: weekday index + * @abbreviated: if %TRUE, abbreviate the weekday name + * + * Returns the localized name for @weekday. If @abbreviated is %TRUE, + * returns the locale's abbreviated weekday name. + * + * Returns: localized weekday name + **/ +const gchar * +e_get_weekday_name (GDateWeekday weekday, + gboolean abbreviated) +{ + /* Make the indices correspond to the enum values. */ + static const gchar *abbr_names[G_DATE_SUNDAY + 1]; + static const gchar *full_names[G_DATE_SUNDAY + 1]; + static gboolean first_time = TRUE; + + g_return_val_if_fail (weekday >= G_DATE_MONDAY, NULL); + g_return_val_if_fail (weekday <= G_DATE_SUNDAY, NULL); + + if (G_UNLIKELY (first_time)) { + gchar buffer[256]; + GDateWeekday ii; + GDate date; + + memset (abbr_names, 0, sizeof (abbr_names)); + memset (full_names, 0, sizeof (full_names)); + + /* First Julian day was a Monday. */ + g_date_set_julian (&date, 1); + + for (ii = G_DATE_MONDAY; ii <= G_DATE_SUNDAY; ii++) { + g_date_strftime (buffer, sizeof (buffer), "%a", &date); + abbr_names[ii] = g_intern_string (buffer); + g_date_strftime (buffer, sizeof (buffer), "%A", &date); + full_names[ii] = g_intern_string (buffer); + g_date_add_days (&date, 1); + } + + first_time = FALSE; + } + + return abbreviated ? abbr_names[weekday] : full_names[weekday]; +} + +/** + * e_weekday_get_next: + * @weekday: a #GDateWeekday + * + * Returns the #GDateWeekday after @weekday. + * + * Returns: the day after @weekday + **/ +GDateWeekday +e_weekday_get_next (GDateWeekday weekday) +{ + GDateWeekday next; + + /* Verbose for readability. */ + switch (weekday) { + case G_DATE_MONDAY: + next = G_DATE_TUESDAY; + break; + case G_DATE_TUESDAY: + next = G_DATE_WEDNESDAY; + break; + case G_DATE_WEDNESDAY: + next = G_DATE_THURSDAY; + break; + case G_DATE_THURSDAY: + next = G_DATE_FRIDAY; + break; + case G_DATE_FRIDAY: + next = G_DATE_SATURDAY; + break; + case G_DATE_SATURDAY: + next = G_DATE_SUNDAY; + break; + case G_DATE_SUNDAY: + next = G_DATE_MONDAY; + break; + default: + next = G_DATE_BAD_WEEKDAY; + break; + } + + return next; +} + +/** + * e_weekday_get_prev: + * @weekday: a #GDateWeekday + * + * Returns the #GDateWeekday before @weekday. + * + * Returns: the day before @weekday + **/ +GDateWeekday +e_weekday_get_prev (GDateWeekday weekday) +{ + GDateWeekday prev; + + /* Verbose for readability. */ + switch (weekday) { + case G_DATE_MONDAY: + prev = G_DATE_SUNDAY; + break; + case G_DATE_TUESDAY: + prev = G_DATE_MONDAY; + break; + case G_DATE_WEDNESDAY: + prev = G_DATE_TUESDAY; + break; + case G_DATE_THURSDAY: + prev = G_DATE_WEDNESDAY; + break; + case G_DATE_FRIDAY: + prev = G_DATE_THURSDAY; + break; + case G_DATE_SATURDAY: + prev = G_DATE_FRIDAY; + break; + case G_DATE_SUNDAY: + prev = G_DATE_SATURDAY; + break; + default: + prev = G_DATE_BAD_WEEKDAY; + break; + } + + return prev; +} + +/** + * e_weekday_add_days: + * @weekday: a #GDateWeekday + * @n_days: number of days to add + * + * Increments @weekday by @n_days. + * + * Returns: a #GDateWeekday + **/ +GDateWeekday +e_weekday_add_days (GDateWeekday weekday, + guint n_days) +{ + g_return_val_if_fail ( + g_date_valid_weekday (weekday), + G_DATE_BAD_WEEKDAY); + + n_days %= 7; /* Weekdays repeat every 7 days. */ + + while (n_days-- > 0) + weekday = e_weekday_get_next (weekday); + + return weekday; +} + +/** + * e_weekday_subtract_days: + * @weekday: a #GDateWeekday + * @n_days: number of days to subtract + * + * Decrements @weekday by @n_days. + * + * Returns: a #GDateWeekday + **/ +GDateWeekday +e_weekday_subtract_days (GDateWeekday weekday, + guint n_days) +{ + g_return_val_if_fail ( + g_date_valid_weekday (weekday), + G_DATE_BAD_WEEKDAY); + + n_days %= 7; /* Weekdays repeat every 7 days. */ + + while (n_days-- > 0) + weekday = e_weekday_get_prev (weekday); + + return weekday; +} + +/** + * e_weekday_get_days_between: + * @weekday1: a #GDateWeekday + * @weekday2: a #GDateWeekday + * + * Counts the number of days starting at @weekday1 and ending at @weekday2. + * + * Returns: the number of days + **/ +guint +e_weekday_get_days_between (GDateWeekday weekday1, + GDateWeekday weekday2) +{ + guint n_days = 0; + + g_return_val_if_fail (g_date_valid_weekday (weekday1), 0); + g_return_val_if_fail (g_date_valid_weekday (weekday2), 0); + + while (weekday1 != weekday2) { + n_days++; + weekday1 = e_weekday_get_next (weekday1); + } + + return n_days; +} + +/** + * e_weekday_to_tm_wday: + * @weekday: a #GDateWeekday + * + * Converts a #GDateWeekday to the numbering used in + * struct tm. + * + * Returns: number of days since Sunday (0 - 6) + **/ +gint +e_weekday_to_tm_wday (GDateWeekday weekday) +{ + gint tm_wday; + + switch (weekday) { + case G_DATE_MONDAY: + tm_wday = 1; + break; + case G_DATE_TUESDAY: + tm_wday = 2; + break; + case G_DATE_WEDNESDAY: + tm_wday = 3; + break; + case G_DATE_THURSDAY: + tm_wday = 4; + break; + case G_DATE_FRIDAY: + tm_wday = 5; + break; + case G_DATE_SATURDAY: + tm_wday = 6; + break; + case G_DATE_SUNDAY: + tm_wday = 0; + break; + default: + g_return_val_if_reached (-1); + } + + return tm_wday; +} + +/** + * e_weekday_from_tm_wday: + * @tm_wday: number of days since Sunday (0 - 6) + * + * Converts a weekday in the numbering used in + * struct tm to a #GDateWeekday. + * + * Returns: a #GDateWeekday + **/ +GDateWeekday +e_weekday_from_tm_wday (gint tm_wday) +{ + GDateWeekday weekday; + + switch (tm_wday) { + case 0: + weekday = G_DATE_SUNDAY; + break; + case 1: + weekday = G_DATE_MONDAY; + break; + case 2: + weekday = G_DATE_TUESDAY; + break; + case 3: + weekday = G_DATE_WEDNESDAY; + break; + case 4: + weekday = G_DATE_THURSDAY; + break; + case 5: + weekday = G_DATE_FRIDAY; + break; + case 6: + weekday = G_DATE_SATURDAY; + break; + default: + g_return_val_if_reached (G_DATE_BAD_WEEKDAY); + } + + return weekday; +} + +/* Evolution Locks for crash recovery */ +static const gchar * +get_lock_filename (void) +{ + static gchar *filename = NULL; + + if (G_UNLIKELY (filename == NULL)) + filename = g_build_filename ( + e_get_user_config_dir (), ".running", NULL); + + return filename; +} + +gboolean +e_file_lock_create (void) +{ + const gchar *filename = get_lock_filename (); + gboolean status = FALSE; + FILE *file; + + file = g_fopen (filename, "w"); + if (file != NULL) { + /* The lock file also serves as a PID file. */ + g_fprintf ( + file, "%" G_GINT64_FORMAT "\n", + (gint64) getpid ()); + fclose (file); + status = TRUE; + } else { + const gchar *errmsg = g_strerror (errno); + g_warning ("Lock file creation failed: %s", errmsg); + } + + return status; +} + +void +e_file_lock_destroy (void) +{ + const gchar *filename = get_lock_filename (); + + if (g_unlink (filename) == -1) { + const gchar *errmsg = g_strerror (errno); + g_warning ("Lock file deletion failed: %s", errmsg); + } +} + +gboolean +e_file_lock_exists (void) +{ + const gchar *filename = get_lock_filename (); + + return g_file_test (filename, G_FILE_TEST_EXISTS); +} + +/* Returns a PID stored in the lock file; 0 if no such file exists. */ +GPid +e_file_lock_get_pid (void) +{ + const gchar *filename = get_lock_filename (); + gchar *contents = NULL; + GPid pid = (GPid) 0; + gint64 n_int64; + + if (!g_file_get_contents (filename, &contents, NULL, NULL)) { + return pid; + } + + /* Try to extract an integer value from the string. */ + n_int64 = g_ascii_strtoll (contents, NULL, 10); + if (n_int64 > 0 && n_int64 < G_MAXINT64) { + /* XXX Probably not portable. */ + pid = (GPid) n_int64; + } + + g_free (contents); + + return pid; +} + +/** + * e_util_guess_mime_type: + * @filename: a local file name, or URI + * @localfile: %TRUE to check the file content, FALSE to check only the name + * + * Tries to determine the MIME type for @filename. Free the returned + * string with g_free(). + * + * Returns: the MIME type of @filename, or %NULL if the the MIME type could + * not be determined + **/ +gchar * +e_util_guess_mime_type (const gchar *filename, + gboolean localfile) +{ + gchar *mime_type = NULL; + + g_return_val_if_fail (filename != NULL, NULL); + + if (localfile) { + GFile *file; + GFileInfo *fi; + + if (strstr (filename, "://")) + file = g_file_new_for_uri (filename); + else + file = g_file_new_for_path (filename); + + fi = g_file_query_info ( + file, G_FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE, + G_FILE_QUERY_INFO_NONE, NULL, NULL); + if (fi) { + mime_type = g_content_type_get_mime_type ( + g_file_info_get_content_type (fi)); + g_object_unref (fi); + } + + g_object_unref (file); + } + + if (!mime_type) { + /* file doesn't exists locally, thus guess based on the filename */ + gboolean uncertain = FALSE; + gchar *content_type; + + content_type = g_content_type_guess (filename, NULL, 0, &uncertain); + if (content_type) { + mime_type = g_content_type_get_mime_type (content_type); + g_free (content_type); + } + } + + return mime_type; +} + +GSList * +e_util_get_category_filter_options (void) +{ + GSList *res = NULL; + GList *clist, *l; + + clist = e_categories_dup_list (); + for (l = clist; l; l = l->next) { + const gchar *cname = l->data; + struct _filter_option *fo; + + if (!e_categories_is_searchable (cname)) + continue; + + fo = g_new0 (struct _filter_option, 1); + + fo->title = g_strdup (cname); + fo->value = g_strdup (cname); + res = g_slist_prepend (res, fo); + } + + g_list_free_full (clist, g_free); + + return g_slist_reverse (res); +} + +/** + * e_util_dup_searchable_categories: + * + * Returns a list of the searchable categories, with each item being a UTF-8 + * category name. The list should be freed with g_list_free() when done with it, + * and the items should be freed with g_free(). Everything can be freed at once + * using g_list_free_full(). + * + * Returns: (transfer full) (element-type utf8): a list of searchable category + * names; free with g_list_free_full() + */ +GList * +e_util_dup_searchable_categories (void) +{ + GList *res = NULL, *all_categories, *l; + + all_categories = e_categories_dup_list (); + for (l = all_categories; l; l = l->next) { + gchar *cname = l->data; + + /* Steal the string from e_categories_dup_list(). */ + if (e_categories_is_searchable (cname)) + res = g_list_prepend (res, (gpointer) cname); + else + g_free (cname); + } + + /* NOTE: Do *not* free the items. They have been freed or stolen + * above. */ + g_list_free (all_categories); + + return g_list_reverse (res); +} +/** + * e_util_get_open_source_job_info: + * @extension_name: an extension name of the source + * @source_display_name: an ESource's display name + * @description: (out) (transfer-full): a description to use + * @alert_ident: (out) (transfer-full): an alert ident to use on failure + * @alert_arg_0: (out) (transfer-full): an alert argument 0 to use on failure + * + * Populates @desription, @alert_ident and @alert_arg_0 to be used + * to open an #ESource with extension @extension_name. The values + * can be used for functions like e_alert_sink_submit_thread_job(). + * + * If #TRUE is returned, then the caller is responsible to free + * all @desription, @alert_ident and @alert_arg_0 with g_free(), + * when no longer needed. + * + * Returns: #TRUE, if the values for @desription, @alert_ident and @alert_arg_0 + * were set for the given @extension_name; when #FALSE is returned, then + * none of these out variables are changed. + * + * Since: 3.16 + **/ +gboolean +e_util_get_open_source_job_info (const gchar *extension_name, + const gchar *source_display_name, + gchar **description, + gchar **alert_ident, + gchar **alert_arg_0) +{ + g_return_val_if_fail (extension_name != NULL, FALSE); + g_return_val_if_fail (source_display_name != NULL, FALSE); + g_return_val_if_fail (description != NULL, FALSE); + g_return_val_if_fail (alert_ident != NULL, FALSE); + g_return_val_if_fail (alert_arg_0 != NULL, FALSE); + + if (g_ascii_strcasecmp (extension_name, E_SOURCE_EXTENSION_CALENDAR) == 0) { + *alert_ident = g_strdup ("calendar:failed-open-calendar"); + *description = g_strdup_printf (_("Opening calendar “%s”"), source_display_name); + } else if (g_ascii_strcasecmp (extension_name, E_SOURCE_EXTENSION_MEMO_LIST) == 0) { + *alert_ident = g_strdup ("calendar:failed-open-memos"); + *description = g_strdup_printf (_("Opening memo list “%s”"), source_display_name); + } else if (g_ascii_strcasecmp (extension_name, E_SOURCE_EXTENSION_TASK_LIST) == 0) { + *alert_ident = g_strdup ("calendar:failed-open-tasks"); + *description = g_strdup_printf (_("Opening task list “%s”"), source_display_name); + } else if (g_ascii_strcasecmp (extension_name, E_SOURCE_EXTENSION_ADDRESS_BOOK) == 0) { + *alert_ident = g_strdup ("addressbook:load-error"); + *description = g_strdup_printf (_("Opening address book “%s”"), source_display_name); + } else { + return FALSE; + } + + *alert_arg_0 = g_strdup (source_display_name); + + return TRUE; +} + +/** + * e_util_propagate_open_source_job_error: + * @job_data: an #EAlertSinkThreadJobData instance + * @extension_name: what extension name had beeing opened + * @local_error: (allow none): a #GError as obtained in a thread job; can be NULL for success + * @error: (allow none): an output #GError, to which propagate the @local_error + * + * Propagates (and cosumes) the @local_error into the @error, eventually + * changes alert_ident for the @job_data for well-known error codes, + * where is available better error description. + * + * Since: 3.16 + **/ +void +e_util_propagate_open_source_job_error (EAlertSinkThreadJobData *job_data, + const gchar *extension_name, + GError *local_error, + GError **error) +{ + const gchar *alert_ident = NULL; + + g_return_if_fail (job_data != NULL); + g_return_if_fail (extension_name != NULL); + + if (!local_error) + return; + + if (!error) { + g_error_free (local_error); + return; + } + + if (g_error_matches (local_error, E_CLIENT_ERROR, E_CLIENT_ERROR_REPOSITORY_OFFLINE)) { + if (g_ascii_strcasecmp (extension_name, E_SOURCE_EXTENSION_CALENDAR) == 0) { + alert_ident = "calendar:prompt-no-contents-offline-calendar"; + } else if (g_ascii_strcasecmp (extension_name, E_SOURCE_EXTENSION_MEMO_LIST) == 0) { + alert_ident = "calendar:prompt-no-contents-offline-memos"; + } else if (g_ascii_strcasecmp (extension_name, E_SOURCE_EXTENSION_TASK_LIST) == 0) { + alert_ident = "calendar:prompt-no-contents-offline-tasks"; + } else if (g_ascii_strcasecmp (extension_name, E_SOURCE_EXTENSION_ADDRESS_BOOK) == 0) { + } + } + + if (alert_ident) + e_alert_sink_thread_job_set_alert_ident (job_data, alert_ident); + + g_propagate_error (error, local_error); +} + +EClient * +e_util_open_client_sync (EAlertSinkThreadJobData *job_data, + EClientCache *client_cache, + const gchar *extension_name, + ESource *source, + guint32 wait_for_connected_seconds, + GCancellable *cancellable, + GError **error) +{ + gchar *description = NULL, *alert_ident = NULL, *alert_arg_0 = NULL; + EClient *client = NULL; + ESourceRegistry *registry; + gchar *display_name; + GError *local_error = NULL; + + registry = e_client_cache_ref_registry (client_cache); + display_name = e_util_get_source_full_name (registry, source); + g_clear_object (®istry); + + g_warn_if_fail (e_util_get_open_source_job_info (extension_name, + display_name, &description, &alert_ident, &alert_arg_0)); + + g_free (display_name); + + camel_operation_push_message (cancellable, "%s", description); + + client = e_client_cache_get_client_sync (client_cache, source, extension_name, wait_for_connected_seconds, cancellable, &local_error); + + camel_operation_pop_message (cancellable); + + if (!client) { + e_alert_sink_thread_job_set_alert_ident (job_data, alert_ident); + e_alert_sink_thread_job_set_alert_arg_0 (job_data, alert_arg_0); + + e_util_propagate_open_source_job_error (job_data, extension_name, local_error, error); + } + + g_free (description); + g_free (alert_ident); + g_free (alert_arg_0); + + return client; +} + +/** + * e_binding_transform_color_to_string: + * @binding: a #GBinding + * @source_value: a #GValue of type #GDK_TYPE_COLOR + * @target_value: a #GValue of type #G_TYPE_STRING + * @not_used: not used + * + * Transforms a #GdkColor value to a color string specification. + * + * Returns: %TRUE always + **/ +gboolean +e_binding_transform_color_to_string (GBinding *binding, + const GValue *source_value, + GValue *target_value, + gpointer not_used) +{ + const GdkColor *color; + gchar *string; + + g_return_val_if_fail (G_IS_BINDING (binding), FALSE); + + color = g_value_get_boxed (source_value); + if (!color) { + g_value_set_string (target_value, ""); + } else { + /* encode color manually, because css styles expect colors in #rrggbb, + * not in #rrrrggggbbbb, which is a result of gdk_color_to_string() + */ + string = g_strdup_printf ( + "#%02x%02x%02x", + (gint) color->red * 256 / 65536, + (gint) color->green * 256 / 65536, + (gint) color->blue * 256 / 65536); + g_value_set_string (target_value, string); + g_free (string); + } + + return TRUE; +} + +/** + * e_binding_transform_string_to_color: + * @binding: a #GBinding + * @source_value: a #GValue of type #G_TYPE_STRING + * @target_value: a #GValue of type #GDK_TYPE_COLOR + * @not_used: not used + * + * Transforms a color string specification to a #GdkColor. + * + * Returns: %TRUE if color string specification was valid + **/ +gboolean +e_binding_transform_string_to_color (GBinding *binding, + const GValue *source_value, + GValue *target_value, + gpointer not_used) +{ + GdkColor color; + const gchar *string; + gboolean success = FALSE; + + g_return_val_if_fail (G_IS_BINDING (binding), FALSE); + + string = g_value_get_string (source_value); + if (gdk_color_parse (string, &color)) { + g_value_set_boxed (target_value, &color); + success = TRUE; + } + + return success; +} + +/** + * e_binding_transform_source_to_uid: + * @binding: a #GBinding + * @source_value: a #GValue of type #E_TYPE_SOURCE + * @target_value: a #GValue of type #G_TYPE_STRING + * @registry: an #ESourceRegistry + * + * Transforms an #ESource object to its UID string. + * + * Returns: %TRUE if @source_value was an #ESource object + **/ +gboolean +e_binding_transform_source_to_uid (GBinding *binding, + const GValue *source_value, + GValue *target_value, + ESourceRegistry *registry) +{ + ESource *source; + const gchar *string; + gboolean success = FALSE; + + g_return_val_if_fail (G_IS_BINDING (binding), FALSE); + g_return_val_if_fail (E_IS_SOURCE_REGISTRY (registry), FALSE); + + source = g_value_get_object (source_value); + if (E_IS_SOURCE (source)) { + string = e_source_get_uid (source); + g_value_set_string (target_value, string); + success = TRUE; + } + + return success; +} + +/** + * e_binding_transform_uid_to_source: + * @binding: a #GBinding + * @source_value: a #GValue of type #G_TYPE_STRING + * @target_value: a #GValue of type #E_TYPE_SOURCe + * @registry: an #ESourceRegistry + * + * Transforms an #ESource UID string to the corresponding #ESource object + * in @registry. + * + * Returns: %TRUE if @registry had an #ESource object with a matching + * UID string + **/ +gboolean +e_binding_transform_uid_to_source (GBinding *binding, + const GValue *source_value, + GValue *target_value, + ESourceRegistry *registry) +{ + ESource *source; + const gchar *string; + gboolean success = FALSE; + + g_return_val_if_fail (G_IS_BINDING (binding), FALSE); + g_return_val_if_fail (E_IS_SOURCE_REGISTRY (registry), FALSE); + + string = g_value_get_string (source_value); + if (string == NULL || *string == '\0') + return FALSE; + + source = e_source_registry_ref_source (registry, string); + if (source != NULL) { + g_value_take_object (target_value, source); + success = TRUE; + } + + return success; +} + +/** + * e_binding_transform_text_non_null: + * @binding: a #GBinding + * @source_value: a #GValue of type #G_TYPE_STRING + * @target_value: a #GValue of type #G_TYPE_STRING + * @user_data: custom user data, unused + * + * Transforms a text value to a text value which is never NULL; + * an empty string is used instead of NULL. + * + * Returns: %TRUE on success + **/ +gboolean +e_binding_transform_text_non_null (GBinding *binding, + const GValue *source_value, + GValue *target_value, + gpointer user_data) +{ + const gchar *str; + + g_return_val_if_fail (G_IS_BINDING (binding), FALSE); + g_return_val_if_fail (source_value != NULL, FALSE); + g_return_val_if_fail (target_value != NULL, FALSE); + + str = g_value_get_string (source_value); + if (!str) + str = ""; + + g_value_set_string (target_value, str); + + return TRUE; +} + +/** + * e_binding_bind_object_text_property: + * @source: the source #GObject + * @source_property: the text property on the source to bind + * @target: the target #GObject + * @target_property: the text property on the target to bind + * @flags: flags to pass to e_binding_bind_property_full() + * + * Installs a new text property object binding, using e_binding_bind_property_full(), + * with transform functions to make sure that a NULL pointer is not + * passed in either way. Instead of NULL an empty string is used. + * + * Returns: the #GBinding instance representing the binding between the two #GObject instances; + * there applies the same rules to it as for the result of e_binding_bind_property_full(). + **/ +GBinding * +e_binding_bind_object_text_property (gpointer source, + const gchar *source_property, + gpointer target, + const gchar *target_property, + GBindingFlags flags) +{ + GObjectClass *klass; + GParamSpec *property; + + g_return_val_if_fail (G_IS_OBJECT (source), NULL); + g_return_val_if_fail (source_property != NULL, NULL); + g_return_val_if_fail (G_IS_OBJECT (target), NULL); + g_return_val_if_fail (target_property != NULL, NULL); + + klass = G_OBJECT_GET_CLASS (source); + property = g_object_class_find_property (klass, source_property); + g_return_val_if_fail (property != NULL, NULL); + g_return_val_if_fail (property->value_type == G_TYPE_STRING, NULL); + + klass = G_OBJECT_GET_CLASS (target); + property = g_object_class_find_property (klass, target_property); + g_return_val_if_fail (property != NULL, NULL); + g_return_val_if_fail (property->value_type == G_TYPE_STRING, NULL); + + return e_binding_bind_property_full (source, source_property, + target, target_property, + flags, + e_binding_transform_text_non_null, + e_binding_transform_text_non_null, + NULL, NULL); +} + +typedef struct _EConnectNotifyData { + GConnectFlags flags; + GValue *old_value; + + GCallback c_handler; + gpointer user_data; +} EConnectNotifyData; + +static EConnectNotifyData * +e_connect_notify_data_new (GCallback c_handler, + gpointer user_data, + guint32 connect_flags) +{ + EConnectNotifyData *connect_data; + + connect_data = g_new0 (EConnectNotifyData, 1); + connect_data->flags = connect_flags; + connect_data->c_handler = c_handler; + connect_data->user_data = user_data; + + return connect_data; +} + +static void +e_connect_notify_data_free (EConnectNotifyData *data) +{ + if (!data) + return; + + if (data->old_value) { + g_value_unset (data->old_value); + g_free (data->old_value); + } + g_free (data); +} + +static gboolean +e_value_equal (GValue *value1, + GValue *value2) +{ + if (value1 == value2) + return TRUE; + + if (!value1 || !value2) + return FALSE; + + #define testType(_uc,_lc) G_STMT_START { \ + if (G_VALUE_HOLDS_ ## _uc (value1)) \ + return g_value_get_ ## _lc (value1) == g_value_get_ ## _lc (value2); \ + } G_STMT_END + + testType (BOOLEAN, boolean); + testType (BOXED, boxed); + testType (CHAR, schar); + testType (DOUBLE, double); + testType (ENUM, enum); + testType (FLAGS, flags); + testType (FLOAT, float); + testType (GTYPE, gtype); + testType (INT, int); + testType (INT64, int64); + testType (LONG, long); + testType (OBJECT, object); + testType (POINTER, pointer); + testType (UCHAR, uchar); + testType (UINT, uint); + testType (UINT64, uint64); + testType (ULONG, ulong); + + #undef testType + + if (G_VALUE_HOLDS_PARAM (value1)) { + GParamSpec *param1, *param2; + + param1 = g_value_get_param (value1); + param2 = g_value_get_param (value2); + + return param1 && param2 && + g_strcmp0 (param1->name, param2->name) == 0 && + param1->flags == param2->flags && + param1->value_type == param2->value_type && + param1->owner_type == param2->owner_type; + } else if (G_VALUE_HOLDS_STRING (value1)) { + const gchar *string1, *string2; + + string1 = g_value_get_string (value1); + string2 = g_value_get_string (value2); + + return g_strcmp0 (string1, string2) == 0; + } else if (G_VALUE_HOLDS_VARIANT (value1)) { + GVariant *variant1, *variant2; + + variant1 = g_value_get_variant (value1); + variant2 = g_value_get_variant (value2); + + return variant1 == variant2 || + (variant1 && variant2 && g_variant_equal (variant1, variant2)); + } + + return FALSE; +} + +static void +e_signal_connect_notify_cb (gpointer instance, + GParamSpec *param, + gpointer user_data) +{ + EConnectNotifyData *connect_data = user_data; + GValue *value; + + g_return_if_fail (connect_data != NULL); + + value = g_new0 (GValue, 1); + g_value_init (value, param->value_type); + g_object_get_property (instance, param->name, value); + + if (!e_value_equal (connect_data->old_value, value)) { + typedef void (* NotifyCBType) (gpointer instance, GParamSpec *param, gpointer user_data); + NotifyCBType c_handler = (NotifyCBType) connect_data->c_handler; + + if (connect_data->old_value) { + g_value_unset (connect_data->old_value); + g_free (connect_data->old_value); + } + connect_data->old_value = value; + + if (connect_data->flags == G_CONNECT_SWAPPED) { + c_handler (connect_data->user_data, param, instance); + } else { + c_handler (instance, param, connect_data->user_data); + } + } else { + g_value_unset (value); + g_free (value); + } +} + +/** + * e_signal_connect_notify: + * + * This installs a special handler in front of @c_handler, which will + * call the @c_handler only if the property value changed since the last + * time it was checked. Due to this, these handlers cannot be disconnected + * by by any of the g_signal_handlers_* functions, but only with the returned + * handler ID. A convenient e_signal_disconnect_notify_handler() was added + * to make it easier. + **/ +gulong +e_signal_connect_notify (gpointer instance, + const gchar *notify_name, + GCallback c_handler, + gpointer user_data) +{ + EConnectNotifyData *connect_data; + + g_return_val_if_fail (g_str_has_prefix (notify_name, "notify::"), 0); + + connect_data = e_connect_notify_data_new (c_handler, user_data, 0); + + return g_signal_connect_data (instance, + notify_name, + G_CALLBACK (e_signal_connect_notify_cb), + connect_data, + (GClosureNotify) e_connect_notify_data_free, + 0); +} + +/** + * e_signal_connect_notify_after: + * + * This installs a special handler in front of @c_handler, which will + * call the @c_handler only if the property value changed since the last + * time it was checked. Due to this, these handlers cannot be disconnected + * by by any of the g_signal_handlers_* functions, but only with the returned + * handler ID. A convenient e_signal_disconnect_notify_handler() was added + * to make it easier. + **/ +gulong +e_signal_connect_notify_after (gpointer instance, + const gchar *notify_name, + GCallback c_handler, + gpointer user_data) +{ + EConnectNotifyData *connect_data; + + g_return_val_if_fail (g_str_has_prefix (notify_name, "notify::"), 0); + + connect_data = e_connect_notify_data_new (c_handler, user_data, G_CONNECT_AFTER); + + return g_signal_connect_data (instance, + notify_name, + G_CALLBACK (e_signal_connect_notify_cb), + connect_data, + (GClosureNotify) e_connect_notify_data_free, + G_CONNECT_AFTER); +} + +/** + * e_signal_connect_notify_swapped: + * + * This installs a special handler in front of @c_handler, which will + * call the @c_handler only if the property value changed since the last + * time it was checked. Due to this, these handlers cannot be disconnected + * by by any of the g_signal_handlers_* functions, but only with the returned + * handler ID. A convenient e_signal_disconnect_notify_handler() was added + * to make it easier. + **/ +gulong +e_signal_connect_notify_swapped (gpointer instance, + const gchar *notify_name, + GCallback c_handler, + gpointer user_data) +{ + EConnectNotifyData *connect_data; + + g_return_val_if_fail (g_str_has_prefix (notify_name, "notify::"), 0); + + connect_data = e_connect_notify_data_new (c_handler, user_data, G_CONNECT_SWAPPED); + + return g_signal_connect_data (instance, + notify_name, + G_CALLBACK (e_signal_connect_notify_cb), + connect_data, + (GClosureNotify) e_connect_notify_data_free, + 0); +} + +/** + * e_signal_connect_notify_object: + * + * This installs a special handler in front of @c_handler, which will + * call the @c_handler only if the property value changed since the last + * time it was checked. Due to this, these handlers cannot be disconnected + * by by any of the g_signal_handlers_* functions, but only with the returned + * handler ID. A convenient e_signal_disconnect_notify_handler() was added + * to make it easier. + **/ +gulong +e_signal_connect_notify_object (gpointer instance, + const gchar *notify_name, + GCallback c_handler, + gpointer gobject, + GConnectFlags connect_flags) +{ + EConnectNotifyData *connect_data; + GClosure *closure; + + g_return_val_if_fail (g_str_has_prefix (notify_name, "notify::"), 0); + + if (!gobject) { + if ((connect_flags & G_CONNECT_SWAPPED) != 0) + return e_signal_connect_notify_swapped (instance, notify_name, c_handler, gobject); + else if ((connect_flags & G_CONNECT_AFTER) != 0) + e_signal_connect_notify_after (instance, notify_name, c_handler, gobject); + else + g_warn_if_fail (connect_flags == 0); + + return e_signal_connect_notify (instance, notify_name, c_handler, gobject); + } + + g_return_val_if_fail (G_IS_OBJECT (gobject), 0); + + connect_data = e_connect_notify_data_new (c_handler, gobject, connect_flags & G_CONNECT_SWAPPED); + closure = g_cclosure_new ( + G_CALLBACK (e_signal_connect_notify_cb), + connect_data, + (GClosureNotify) e_connect_notify_data_free); + + g_object_watch_closure (G_OBJECT (gobject), closure); + + return g_signal_connect_closure (instance, + notify_name, + closure, + connect_flags & G_CONNECT_AFTER); +} + +/** + * e_signal_disconnect_notify_handler: + * + * Convenient handler disconnect function to be used with + * returned handler IDs from: + * e_signal_connect_notify() + * e_signal_connect_notify_after() + * e_signal_connect_notify_swapped() + * e_signal_connect_notify_object() + * but not necessarily only with these functions. + **/ +void +e_signal_disconnect_notify_handler (gpointer instance, + gulong *handler_id) +{ + g_return_if_fail (instance != NULL); + g_return_if_fail (handler_id != NULL); + + if (!*handler_id) + return; + + g_signal_handler_disconnect (instance, *handler_id); + *handler_id = 0; +} + +static GMutex settings_hash_lock; +static GHashTable *settings_hash = NULL; + +/** + * e_util_ref_settings: + * @schema_id: the id of the schema to reference settings for + * + * Either returns an existing referenced #GSettings object for the given @schema_id, + * or creates a new one and remembers it for later use, to avoid having too many + * #GSettings objects created for the same @schema_id. + * + * Returns: A #GSettings for the given @schema_id. The returned #GSettings object + * is referenced, thus free it with g_object_unref() when done with it. + * + * Since: 3.16 + **/ +GSettings * +e_util_ref_settings (const gchar *schema_id) +{ + GSettings *settings; + + g_return_val_if_fail (schema_id != NULL, NULL); + g_return_val_if_fail (*schema_id, NULL); + + g_mutex_lock (&settings_hash_lock); + + if (!settings_hash) { + settings_hash = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_object_unref); + } + + settings = g_hash_table_lookup (settings_hash, schema_id); + if (!settings) { + settings = g_settings_new (schema_id); + g_hash_table_insert (settings_hash, g_strdup (schema_id), settings); + } + + if (settings) + g_object_ref (settings); + + g_mutex_unlock (&settings_hash_lock); + + return settings; +} + +/** + * e_util_cleanup_settings: + * + * Frees all the memory taken by e_util_ref_settings(). + * + * Since: 3.16 + **/ +void +e_util_cleanup_settings (void) +{ + g_mutex_lock (&settings_hash_lock); + + if (settings_hash) { + g_hash_table_destroy (settings_hash); + settings_hash = NULL; + } + + g_mutex_unlock (&settings_hash_lock); +} + +static gdouble +get_screen_dpi (GdkScreen *screen) +{ + gdouble dpi; + gdouble dp, di; + + dpi = gdk_screen_get_resolution (screen); + if (dpi != -1) + return dpi; + + dp = hypot (gdk_screen_get_width (screen), gdk_screen_get_height (screen)); + di = hypot (gdk_screen_get_width_mm (screen), gdk_screen_get_height_mm (screen)) / 25.4; + + return dp / di; +} + +guint +e_util_normalize_font_size (GtkWidget *widget, + gdouble font_size) +{ + /* WebKit2 uses font sizes in pixels. */ + GdkScreen *screen; + gdouble dpi; + + if (widget) { + screen = gtk_widget_has_screen (widget) ? + gtk_widget_get_screen (widget) : gdk_screen_get_default (); + } else { + screen = gdk_screen_get_default (); + } + + dpi = screen ? get_screen_dpi (screen) : 96; + + return font_size / 72.0 * dpi; +} + +/** + * e_util_prompt_user: + * @parent: parent window + * @settings_schema: name of the settings schema where @promptkey belongs. + * @promptkey: settings key to check if we should prompt the user or not. + * @tag: e_alert tag. + * + * Convenience function to query the user with a Yes/No dialog and a + * "Do not show this dialog again" checkbox. If the user checks that + * checkbox, then @promptkey is set to %FALSE, otherwise it is set to + * %TRUE. + * + * Returns %TRUE if the user clicks Yes or %FALSE otherwise. + **/ +gboolean +e_util_prompt_user (GtkWindow *parent, + const gchar *settings_schema, + const gchar *promptkey, + const gchar *tag, + ...) +{ + GtkWidget *dialog; + GtkWidget *check = NULL; + GtkWidget *container; + va_list ap; + gint button; + GSettings *settings = NULL; + EAlert *alert = NULL; + + if (promptkey) { + settings = e_util_ref_settings (settings_schema); + + if (!g_settings_get_boolean (settings, promptkey)) { + g_object_unref (settings); + return TRUE; + } + } + + va_start (ap, tag); + alert = e_alert_new_valist (tag, ap); + va_end (ap); + + dialog = e_alert_dialog_new (parent, alert); + g_object_unref (alert); + + container = e_alert_dialog_get_content_area (E_ALERT_DIALOG (dialog)); + + if (promptkey) { + check = gtk_check_button_new_with_mnemonic ( + _("_Do not show this message again")); + gtk_box_pack_start ( + GTK_BOX (container), check, FALSE, FALSE, 0); + gtk_widget_show (check); + } + + button = gtk_dialog_run (GTK_DIALOG (dialog)); + if (promptkey) + g_settings_set_boolean ( + settings, promptkey, + !gtk_toggle_button_get_active ( + GTK_TOGGLE_BUTTON (check))); + + gtk_widget_destroy (dialog); + + g_clear_object (&settings); + + return button == GTK_RESPONSE_YES; +} + +typedef struct _EUtilSimpleAsyncResultThreadData { + GSimpleAsyncResult *simple; + GSimpleAsyncThreadFunc func; + GCancellable *cancellable; +} EUtilSimpleAsyncResultThreadData; + +static void +e_util_simple_async_result_thread (gpointer data, + gpointer user_data) +{ + EUtilSimpleAsyncResultThreadData *thread_data = data; + GError *error = NULL; + + g_return_if_fail (thread_data != NULL); + g_return_if_fail (G_IS_SIMPLE_ASYNC_RESULT (thread_data->simple)); + g_return_if_fail (thread_data->func != NULL); + + if (g_cancellable_set_error_if_cancelled (thread_data->cancellable, &error)) { + g_simple_async_result_take_error (thread_data->simple, error); + } else { + thread_data->func (thread_data->simple, + g_async_result_get_source_object (G_ASYNC_RESULT (thread_data->simple)), + thread_data->cancellable); + } + + g_simple_async_result_complete_in_idle (thread_data->simple); + + g_clear_object (&thread_data->simple); + g_clear_object (&thread_data->cancellable); + g_free (thread_data); +} + +/** + * e_util_run_simple_async_result_in_thread: + * @simple: a #GSimpleAsyncResult + * @func: a #GSimpleAsyncThreadFunc to execute in the thread + * @cancellable: an optional #GCancellable, or %NULL + * + * Similar to g_simple_async_result_run_in_thread(), except + * it doesn't use GTask internally, thus doesn't block the GTask + * thread pool with possibly long job. + * + * It doesn't behave exactly the same as the g_simple_async_result_run_in_thread(), + * the @cancellable checking is not done before the finish. + * + * Since: 3.18 + **/ +void +e_util_run_simple_async_result_in_thread (GSimpleAsyncResult *simple, + GSimpleAsyncThreadFunc func, + GCancellable *cancellable) +{ + static GThreadPool *thread_pool = NULL; + static GMutex thread_pool_mutex; + EUtilSimpleAsyncResultThreadData *thread_data; + + g_return_if_fail (G_IS_SIMPLE_ASYNC_RESULT (simple)); + g_return_if_fail (func != NULL); + + g_mutex_lock (&thread_pool_mutex); + + if (!thread_pool) + thread_pool = g_thread_pool_new (e_util_simple_async_result_thread, NULL, 20, FALSE, NULL); + + thread_data = g_new0 (EUtilSimpleAsyncResultThreadData, 1); + thread_data->simple = g_object_ref (simple); + thread_data->func = func; + thread_data->cancellable = cancellable ? g_object_ref (cancellable) : NULL; + + g_thread_pool_push (thread_pool, thread_data, NULL); + + g_mutex_unlock (&thread_pool_mutex); +} + +/** + * e_util_is_running_gnome: + * + * Returns: Whether the current running desktop environment is GNOME. + * + * Since: 3.18 + **/ +gboolean +e_util_is_running_gnome (void) +{ +#ifdef G_OS_WIN32 + return FALSE; +#else + static gint runs_gnome = -1; + + if (runs_gnome == -1) { + runs_gnome = g_strcmp0 (g_getenv ("XDG_CURRENT_DESKTOP"), "GNOME") == 0 ? 1 : 0; + if (runs_gnome) { + GDesktopAppInfo *app_info; + + app_info = g_desktop_app_info_new ("gnome-notifications-panel.desktop"); + if (!app_info) { + runs_gnome = 0; + } + + g_clear_object (&app_info); + } + } + + return runs_gnome != 0; +#endif +} + +/** + * e_util_set_entry_issue_hint: + * @entry: a #GtkEntry + * @hint: (allow none): a hint to set, or %NULL to unset + * + * Sets a @hint on the secondary @entry icon about an entered value issue, + * or unsets it, when the @hint is %NULL. + * + * Since: 3.20 + **/ +void +e_util_set_entry_issue_hint (GtkWidget *entry, + const gchar *hint) +{ + GtkEntry *eentry; + + g_return_if_fail (GTK_IS_ENTRY (entry)); + + eentry = GTK_ENTRY (entry); + + if (hint) { + gtk_entry_set_icon_from_icon_name (eentry, GTK_ENTRY_ICON_SECONDARY, "dialog-warning"); + gtk_entry_set_icon_tooltip_text (eentry, GTK_ENTRY_ICON_SECONDARY, hint); + } else { + gtk_entry_set_icon_from_icon_name (eentry, GTK_ENTRY_ICON_SECONDARY, NULL); + gtk_entry_set_icon_tooltip_text (eentry, GTK_ENTRY_ICON_SECONDARY, NULL); + } +} + +static GThread *main_thread = NULL; + +void +e_util_init_main_thread (GThread *thread) +{ + g_return_if_fail (main_thread == NULL); + + main_thread = thread ? thread : g_thread_self (); +} + +gboolean +e_util_is_main_thread (GThread *thread) +{ + return thread ? thread == main_thread : g_thread_self () == main_thread; +} + +/** + * e_util_save_image_from_clipboard: + * @clipboard: a #GtkClipboard + * @hint: (allow none): a hint to set, or %NULL to unset + * + * Saves the image from @clipboard to a temporary file and returns its URI. + * + * Since: 3.22 + **/ +gchar * +e_util_save_image_from_clipboard (GtkClipboard *clipboard) +{ + GdkPixbuf *pixbuf = NULL; + gchar *tmpl; + gchar *filename = NULL; + gchar *uri = NULL; + GError *error = NULL; + + g_return_val_if_fail (GTK_IS_CLIPBOARD (clipboard), NULL); + + /* Extract the image data from the clipboard. */ + pixbuf = gtk_clipboard_wait_for_image (clipboard); + g_return_val_if_fail (pixbuf != NULL, FALSE); + + tmpl = g_strconcat (_("Image"), "-XXXXXX.png", NULL); + + /* Reserve a temporary file. */ + filename = e_mktemp (tmpl); + + g_free (tmpl); + + if (filename == NULL) { + g_set_error ( + &error, G_FILE_ERROR, + g_file_error_from_errno (errno), + "Could not create temporary file: %s", + g_strerror (errno)); + goto exit; + } + + /* Save the pixbuf as a temporary file in image/png format. */ + if (!gdk_pixbuf_save (pixbuf, filename, "png", &error, NULL)) + goto exit; + + /* Convert the filename to a URI. */ + uri = g_filename_to_uri (filename, NULL, &error); + + exit: + if (error != NULL) { + g_warning ("%s", error->message); + g_error_free (error); + } + + g_object_unref (pixbuf); + g_free (filename); + + return uri; +} + +static void +e_util_stop_signal_emission_cb (gpointer instance, + const gchar *signal_name) +{ + g_signal_stop_emission_by_name (instance, signal_name); +} + +/** + * e_util_check_gtk_bindings_in_key_press_event_cb: + * @widget: a #GtkWidget, most often a #GtkWindow + * @event: a #GdkEventKey + * + * A callback function for GtkWidget::key-press-event signal, + * which checks whether currently focused widget inside @widget, + * if it's a #GtkWindow, or a toplevel window containing the @widget, + * will consume the @event due to gtk+ bindings and if so, then + * it'll stop processing the event further. When it's connected + * on a #GtkWindow, then it can prevent the event to be used + * for shortcuts of actions. + * + * Returns: %TRUE to stop other handlers from being invoked for + * the event, %FALSE to propagate the event further. + * + * Since: 3.22 + **/ +gboolean +e_util_check_gtk_bindings_in_key_press_event_cb (GtkWidget *widget, + GdkEvent *event) +{ + GdkEventKey *key_event = (GdkEventKey *) event; + GtkWindow *window = NULL; + GtkWidget *focused; + + g_return_val_if_fail (GTK_IS_WIDGET (widget), FALSE); + g_return_val_if_fail (event != NULL, FALSE); + g_return_val_if_fail (event->type == GDK_KEY_PRESS, FALSE); + + if (GTK_IS_WINDOW (widget)) { + window = GTK_WINDOW (widget); + } else { + GtkWidget *toplevel; + + toplevel = gtk_widget_get_toplevel (widget); + if (GTK_IS_WINDOW (toplevel)) + window = GTK_WINDOW (toplevel); + } + + if (!window) + return FALSE; + + focused = gtk_window_get_focus (window); + if (!focused) + return FALSE; + + if (gtk_bindings_activate_event (G_OBJECT (focused), key_event)) + return TRUE; + + if (WEBKIT_IS_WEB_VIEW (focused) && + (key_event->state & (GDK_CONTROL_MASK | GDK_MOD1_MASK)) != 0) { + GtkWidget *text_view; + gboolean may_use; + + /* WebKit uses GtkTextView to process key bindings. Do the same. */ + text_view = gtk_text_view_new (); + + /* Stop emissing for clipboard signals, to not populate the text_view */ + g_signal_connect (text_view, "copy-clipboard", G_CALLBACK (e_util_stop_signal_emission_cb), (gpointer) "copy-clipboard"); + g_signal_connect (text_view, "cut-clipboard", G_CALLBACK (e_util_stop_signal_emission_cb), (gpointer) "cut-clipboard"); + g_signal_connect (text_view, "paste-clipboard", G_CALLBACK (e_util_stop_signal_emission_cb), (gpointer) "paste-clipboard"); + + may_use = gtk_bindings_activate_event (G_OBJECT (text_view), key_event); + gtk_widget_destroy (text_view); + + if (may_use) { + gboolean result = FALSE; + + g_signal_emit_by_name (focused, "key-press-event", event, &result); + + return result; + } + } + + return FALSE; +} + +/** + * e_util_claim_dbus_proxy_call_error: + * @dbus_proxy: a #GDBusProxy instance + * @method_name: a method name of the @dbus_proxy + * @in_error: (allow-none): a #GError with the failure, or %NULL + * + * Claims the @in_error on the console as a failure to call + * method @method_name of the @dbus_proxy. It's safe to call this + * with a %NULL @in_error, then the function does nothing. + * Some errors can be ignored, like the G_IO_ERROR_CANCELLED is. + * + * Since: 3.22 + **/ +void +e_util_claim_dbus_proxy_call_error (GDBusProxy *dbus_proxy, + const gchar *method_name, + const GError *in_error) +{ + g_return_if_fail (G_IS_DBUS_PROXY (dbus_proxy)); + g_return_if_fail (method_name != NULL); + + if (in_error && !g_error_matches (in_error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + g_warning ("Failed to call a DBus Proxy method %s::%s: %s", + g_dbus_proxy_get_name (dbus_proxy), method_name, in_error->message); +} + +static void +e_util_finish_dbus_proxy_call_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + gchar *method_name = user_data; + GDBusProxy *dbus_proxy; + GVariant *ret; + GError *error = NULL; + + g_return_if_fail (G_IS_DBUS_PROXY (source_object)); + + dbus_proxy = G_DBUS_PROXY (source_object); + + ret = g_dbus_proxy_call_finish (dbus_proxy, result, &error); + + if (ret) + g_variant_unref (ret); + + if (error) + g_dbus_error_strip_remote_error (error); + + e_util_claim_dbus_proxy_call_error (dbus_proxy, method_name, error); + + g_clear_error (&error); + g_free (method_name); +} + +/** + * e_util_invoke_g_dbus_proxy_call_with_error_check: + * @dbus_proxy: a #GDBusProxy instance + * @method_name: a method name to invoke + * @parameters: (allow-none): parameters of the method, or %NULL + * @cancellable: (allow-none): a #GCancellable, or %NULL + * + * Calls g_dbus_proxy_call() on the @dbus_proxy for the @method_name with @parameters + * and adds its own callback for the result. Then, if an error is returned, passes this + * error into e_util_claim_dbus_proxy_call_error(). + * + * See: e_util_invoke_g_dbus_proxy_call_with_error_check_full() + * + * Since: 3.22 + **/ +void +e_util_invoke_g_dbus_proxy_call_with_error_check (GDBusProxy *dbus_proxy, + const gchar *method_name, + GVariant *parameters, + GCancellable *cancellable) +{ + g_return_if_fail (G_IS_DBUS_PROXY (dbus_proxy)); + g_return_if_fail (method_name != NULL); + + e_util_invoke_g_dbus_proxy_call_with_error_check_full ( + dbus_proxy, method_name, parameters, + G_DBUS_CALL_FLAGS_NONE, -1, cancellable); +} + +/** + * e_util_invoke_g_dbus_proxy_call_with_error_check_full: + * @dbus_proxy: a #GDBusProxy instance + * @method_name: a method name to invoke + * @parameters: (allow-none): parameters of the method, or %NULL + * @flags: a bit-or of #GDBusCallFlags, with the same meaning as in the g_dbus_proxy_call() + * @timeout_msec: timeout in milliseconds, with the same meaning as in the g_dbus_proxy_call(). + * @cancellable: (allow-none): a #GCancellable, or %NULL + * + * Calls g_dbus_proxy_call() on the @dbus_proxy for the @method_name with @parameters + * and adds its own callback for the result. Then, if an error is returned, passes this + * error into e_util_claim_dbus_proxy_call_error(). + * + * See: e_util_invoke_g_dbus_proxy_call_with_error_check() + * + * Since: 3.22 + **/ +void +e_util_invoke_g_dbus_proxy_call_with_error_check_full (GDBusProxy *dbus_proxy, + const gchar *method_name, + GVariant *parameters, + GDBusCallFlags flags, + gint timeout_msec, + GCancellable *cancellable) +{ + g_return_if_fail (G_IS_DBUS_PROXY (dbus_proxy)); + g_return_if_fail (method_name != NULL); + + g_dbus_proxy_call (dbus_proxy, method_name, parameters, + flags, timeout_msec, cancellable, + e_util_finish_dbus_proxy_call_cb, g_strdup (method_name)); +} + +/** + * e_util_invoke_g_dbus_proxy_call_sync_wrapper_with_error_check: + * @dbus_proxy: a #GDBusProxy instance + * @method_name: a method name to invoke + * @parameters: (allow-none): parameters of the method, or %NULL + * @cancellable: (allow-none): a #GCancellable, or %NULL + * + * Calls e_util_invoke_g_dbus_proxy_call_sync_wrapper_full() with some default + * values for flags and timeout_msec, while also providing its own GError and + * after the call is finished it calls e_util_claim_dbus_proxy_call_error() + * with the returned error, if any. + * + * Returns: The result of the method call, or %NULL on error. Free with g_variant_unref(). + * + * Since: 3.22 + **/ +GVariant * +e_util_invoke_g_dbus_proxy_call_sync_wrapper_with_error_check (GDBusProxy *dbus_proxy, + const gchar *method_name, + GVariant *parameters, + GCancellable *cancellable) +{ + GVariant *result; + GError *error = NULL; + + g_return_val_if_fail (G_IS_DBUS_PROXY (dbus_proxy), NULL); + g_return_val_if_fail (method_name != NULL, NULL); + + result = e_util_invoke_g_dbus_proxy_call_sync_wrapper_full (dbus_proxy, method_name, parameters, + G_DBUS_CALL_FLAGS_NONE, -1, cancellable, &error); + + if (error) + g_dbus_error_strip_remote_error (error); + + e_util_claim_dbus_proxy_call_error (dbus_proxy, method_name, error); + g_clear_error (&error); + + return result; +} + +static void +sync_wrapper_result_callback (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + GAsyncResult **out_async_result = user_data; + + g_return_if_fail (out_async_result != NULL); + g_return_if_fail (*out_async_result == NULL); + + *out_async_result = g_object_ref (result); +} + +/** + * e_util_invoke_g_dbus_proxy_call_sync_wrapper_full: + * @dbus_proxy: a #GDBusProxy instance + * @method_name: a method name to invoke + * @parameters: (allow-none): parameters of the method, or %NULL + * @flags: a bit-or of #GDBusCallFlags, with the same meaning as in the g_dbus_proxy_call() + * @timeout_msec: timeout in milliseconds, with the same meaning as in the g_dbus_proxy_call(). + * @cancellable: (allow-none): a #GCancellable, or %NULL + * @error: (allow-none): Return location for error, or %NULL + * + * Wraps GDBusProxy synchronous call into an asynchronous without blocking + * the main context. This can be useful when doing calls on a WebExtension, + * because it can avoid freeze when this is called in the UI process and + * the WebProcess also does its own IPC call. + * + * This function should be called only from the main thread. + * + * Returns: The result of the method call, or %NULL on error. Free with g_variant_unref(). + * + * Since: 3.22 + **/ +GVariant * +e_util_invoke_g_dbus_proxy_call_sync_wrapper_full (GDBusProxy *dbus_proxy, + const gchar *method_name, + GVariant *parameters, + GDBusCallFlags flags, + gint timeout_msec, + GCancellable *cancellable, + GError **error) +{ + GAsyncResult *async_result = NULL; + GVariant *var_result; + GMainContext *main_context; + + g_return_val_if_fail (G_IS_DBUS_PROXY (dbus_proxy), NULL); + g_return_val_if_fail (method_name != NULL, NULL); + + g_warn_if_fail (e_util_is_main_thread (g_thread_self ())); + + g_dbus_proxy_call ( + dbus_proxy, method_name, parameters, flags, timeout_msec, cancellable, + sync_wrapper_result_callback, &async_result); + + main_context = g_main_context_get_thread_default (); + + while (!async_result) { + g_main_context_iteration (main_context, TRUE); + } + + var_result = g_dbus_proxy_call_finish (dbus_proxy, async_result, error); + + g_clear_object (&async_result); + + return var_result; +} + +/** + * e_util_save_file_chooser_folder: + * @file_chooser: a #GtkFileChooser + * + * Saves the current folder of the @file_chooser, thus it could be used + * by e_util_load_file_chooser_folder() to open it in the last chosen folder. + * + * Since: 3.24 + **/ +void +e_util_save_file_chooser_folder (GtkFileChooser *file_chooser) +{ + GSettings *settings; + gchar *uri; + + g_return_if_fail (GTK_IS_FILE_CHOOSER (file_chooser)); + + uri = gtk_file_chooser_get_current_folder_uri (file_chooser); + if (uri && g_str_has_prefix (uri, "file://")) { + settings = e_util_ref_settings ("org.gnome.evolution.shell"); + g_settings_set_string (settings, "file-chooser-folder", uri); + g_object_unref (settings); + } + + g_free (uri); +} + +/** + * e_util_load_file_chooser_folder: + * @file_chooser: a #GtkFileChooser + * + * Sets the current folder to the @file_chooser to the one previously saved + * by e_util_save_file_chooser_folder(). The function does nothing if none + * or invalid is saved. + * + * Since: 3.24 + **/ +void +e_util_load_file_chooser_folder (GtkFileChooser *file_chooser) +{ + GSettings *settings; + gchar *uri; + + g_return_if_fail (GTK_IS_FILE_CHOOSER (file_chooser)); + + settings = e_util_ref_settings ("org.gnome.evolution.shell"); + uri = g_settings_get_string (settings, "file-chooser-folder"); + g_object_unref (settings); + + if (uri && g_str_has_prefix (uri, "file://")) { + gchar *filename; + + filename = g_filename_from_uri (uri, NULL, NULL); + if (filename && g_file_test (filename, G_FILE_TEST_IS_DIR)) + gtk_file_chooser_set_current_folder_uri (file_chooser, uri); + + g_free (filename); + } + + g_free (uri); +} + +/** + * e_util_get_webkit_developer_mode_enabled: + * + * Returns: Whether WebKit developer mode is enabled. This is read only + * once, thus any changes in the GSettings property require restart + * of the Evolution. + * + * Since: 3.24 + **/ +gboolean +e_util_get_webkit_developer_mode_enabled (void) +{ + static gchar enabled = -1; + + if (enabled == -1) { + GSettings *settings; + + settings = e_util_ref_settings ("org.gnome.evolution.shell"); + enabled = g_settings_get_boolean (settings, "webkit-developer-mode") ? 1 : 0; + g_clear_object (&settings); + } + + return enabled != 0; +} + +/** + * e_util_next_uri_from_uri_list: + * @uri_list: array of URIs separated by new lines + * @len: (out): a length of the found URI + * @list_len: (out): a length of the array + * + * Returns: A newly allocated string with found URI. + * + * Since: 3.26 + **/ +gchar * +e_util_next_uri_from_uri_list (guchar **uri_list, + gint *len, + gint *list_len) +{ + guchar *uri, *begin; + + begin = *uri_list; + *len = 0; + while (**uri_list && **uri_list != '\n' && **uri_list != '\r' && *list_len) { + (*uri_list) ++; + (*len) ++; + (*list_len) --; + } + + uri = (guchar *) g_strndup ((gchar *) begin, *len); + + while ((!**uri_list || **uri_list == '\n' || **uri_list == '\r') && *list_len) { + (*uri_list) ++; + (*list_len) --; + } + + return (gchar *) uri; +} + +/** + * e_util_resize_window_for_screen: + * @window: a #GtkWindow + * @window_width: the @window width without @children, or -1 to compute + * @window_height: the @window height without @children, or -1 to compute + * @children: (element-type GtkWidget): (nullable): a #GSList with children to calculate with + * + * Calculates the size of the @window considering preferred sizes of @children, + * and shrinks the @window in case it won't be completely visible on the screen + * it is assigned to. + * + * Since: 3.26 + **/ +void +e_util_resize_window_for_screen (GtkWindow *window, + gint window_width, + gint window_height, + const GSList *children) +{ + gint width = -1, height = -1, content_width = -1, content_height = -1, current_width = -1, current_height = -1; + GtkRequisition requisition; + const GSList *link; + + g_return_if_fail (GTK_IS_WINDOW (window)); + + gtk_window_get_default_size (window, &width, &height); + if (width < 0 || height < 0) { + gtk_widget_get_preferred_size (GTK_WIDGET (window), &requisition, NULL); + + width = requisition.width; + height = requisition.height; + } + + for (link = children; link; link = g_slist_next (link)) { + GtkWidget *widget = link->data; + + if (GTK_IS_SCROLLED_WINDOW (widget)) + widget = gtk_bin_get_child (GTK_BIN (widget)); + + if (GTK_IS_VIEWPORT (widget)) + widget = gtk_bin_get_child (GTK_BIN (widget)); + + if (!GTK_IS_WIDGET (widget)) + continue; + + gtk_widget_get_preferred_size (widget, &requisition, NULL); + + if (requisition.width > content_width) + content_width = requisition.width; + if (requisition.height > content_height) + content_height = requisition.height; + + widget = gtk_widget_get_parent (widget); + if (GTK_IS_VIEWPORT (widget)) + widget = gtk_widget_get_parent (widget); + + if (GTK_IS_WIDGET (widget)) { + gtk_widget_get_preferred_size (widget, &requisition, NULL); + + if (current_width == -1 || current_width < requisition.width) + current_width = requisition.width; + if (current_height == -1 || current_height < requisition.height) + current_height = requisition.height; + } + } + + if (content_width > 0 && content_height > 0 && width > 0 && height > 0) { + GdkScreen *screen; + GdkRectangle monitor_area; + gint x = 0, y = 0, monitor; + + screen = gtk_window_get_screen (GTK_WINDOW (window)); + gtk_window_get_position (GTK_WINDOW (window), &x, &y); + + monitor = gdk_screen_get_monitor_at_point (screen, x, y); + if (monitor < 0 || monitor >= gdk_screen_get_n_monitors (screen)) + monitor = 0; + + gdk_screen_get_monitor_workarea (screen, monitor, &monitor_area); + + /* When the children are packed inside the window then influence the window + size too, thus subtract it, if possible. */ + if (window_width < 0) { + if (current_width > 0 && current_width < width) + width -= current_width; + } else { + width = window_width; + } + + if (window_height < 0) { + if (current_height > 0 && current_height < height) + height -= current_height; + } else { + height = window_height; + } + + if (content_width > monitor_area.width - width) + content_width = monitor_area.width - width; + + if (content_height > monitor_area.height - height) + content_height = monitor_area.height - height; + + if (content_width > 0 && content_height > 0) + gtk_window_set_default_size (GTK_WINDOW (window), width + content_width, height + content_height); + } +} + +/** + * e_util_query_ldap_root_dse_sync: + * @host: an LDAP server host name + * @port: an LDAP server port + * @out_root_dse: (out) (transfer full): NULL-terminated array of the server root DSE-s, or %NULL on error + * @cancellable: optional #GCancellable object, or %NULL + * @error: return location for a #GError, or %NULL + * + * Queries an LDAP server identified by @host and @port for supported + * search bases and returns them as an NULL-terminated array of strings + * at @out_root_dse. It sets @out_root_dse to NULL on error. + * Free the returned @out_root_dse with g_strfreev() when no longer needed. + * + * The function fails and sets @error to G_IO_ERROR_NOT_SUPPORTED when + * Evolution had been compiled without LDAP support. + * + * Returns: Whether succeeded. + * + * Since: 3.28 + **/ +gboolean +e_util_query_ldap_root_dse_sync (const gchar *host, + guint16 port, + gchar ***out_root_dse, + GCancellable *cancellable, + GError **error) +{ +#ifdef HAVE_LDAP + G_LOCK_DEFINE_STATIC (ldap); + LDAP *ldap = NULL; + LDAPMessage *result = NULL; + struct timeval timeout; + gchar **values = NULL, **root_dse; + gint ldap_error; + gint option; + gint version; + gint ii; + const gchar *attrs[] = { "namingContexts", NULL }; + + g_return_val_if_fail (host && *host, FALSE); + g_return_val_if_fail (port > 0, FALSE); + g_return_val_if_fail (out_root_dse != NULL, FALSE); + + *out_root_dse = NULL; + + timeout.tv_sec = 60; + timeout.tv_usec = 0; + + G_LOCK (ldap); + + if (g_cancellable_set_error_if_cancelled (cancellable, error)) + goto exit; + + ldap = ldap_init (host, port); + if (!ldap) { + g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT, + _("This address book server might be unreachable or the server name may be misspelled or your network connection could be down.")); + goto exit; + } + + if (g_cancellable_set_error_if_cancelled (cancellable, error)) + goto exit; + + version = LDAP_VERSION3; + option = LDAP_OPT_PROTOCOL_VERSION; + ldap_error = ldap_set_option (ldap, option, &version); + if (ldap_error != LDAP_OPT_SUCCESS) { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_NOT_INITIALIZED, + _("Failed to set protocol version to LDAPv3 (%d): %s"), ldap_error, + ldap_err2string (ldap_error) ? ldap_err2string (ldap_error) : _("Unknown error")); + goto exit; + } + + if (g_cancellable_set_error_if_cancelled (cancellable, error)) + goto exit; + + /* FIXME Use the user's actual authentication settings. */ + ldap_error = ldap_simple_bind_s (ldap, NULL, NULL); + if (ldap_error != LDAP_SUCCESS) { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_PERMISSION_DENIED, + _("Failed to authenticate with LDAP server (%d): %s"), ldap_error, + ldap_err2string (ldap_error) ? ldap_err2string (ldap_error) : _("Unknown error")); + goto exit; + } + + if (g_cancellable_set_error_if_cancelled (cancellable, error)) + goto exit; + + ldap_error = ldap_search_ext_s ( + ldap, LDAP_ROOT_DSE, LDAP_SCOPE_BASE, + "(objectclass=*)", (gchar **) attrs, 0, + NULL, NULL, &timeout, LDAP_NO_LIMIT, &result); + if (ldap_error != LDAP_SUCCESS) { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + _("This LDAP server may use an older version of LDAP, which does not support this functionality or it may be misconfigured. Ask your administrator for supported search bases.\n\nDetailed error (%d): %s"), + ldap_error, ldap_err2string (ldap_error) ? ldap_err2string (ldap_error) : _("Unknown error")); + goto exit; + } + + if (g_cancellable_set_error_if_cancelled (cancellable, error)) + goto exit; + + values = ldap_get_values (ldap, result, "namingContexts"); + if (values == NULL || values[0] == NULL || *values[0] == '\0') { + g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_FAILED, + _("This LDAP server may use an older version of LDAP, which does not support this functionality or it may be misconfigured. Ask your administrator for supported search bases.")); + goto exit; + } + + if (g_cancellable_set_error_if_cancelled (cancellable, error)) + goto exit; + + root_dse = g_new0 (gchar *, g_strv_length (values) + 1); + + for (ii = 0; values[ii]; ii++) { + root_dse[ii] = g_strdup (values[ii]); + } + + root_dse[ii] = NULL; + + *out_root_dse = root_dse; + + exit: + if (values) + ldap_value_free (values); + + if (result) + ldap_msgfree (result); + + if (ldap) + ldap_unbind_s (ldap); + + G_UNLOCK (ldap); + + return *out_root_dse != NULL; + +#else /* HAVE_LDAP */ + g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED, + _("Evolution had not been compiled with LDAP support")); + + return FALSE; +#endif +}