/*
   Kickshaw - A Menu Editor for Openbox

   Copyright (c) 2010–2025        Marcus Schätzle

   This program is free software; you can redistribute it and/or modify
   it under the terms of the GNU General Public License as published by
   the Free Software Foundation; either version 2 of the License, or
   (at your option) any later version.

   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 General Public License along 
   with Kickshaw. If not, see http://www.gnu.org/licenses/.
*/

#include <gtk/gtk.h>

#include <sys/stat.h>
#include <stdlib.h> // May need to be included explicitly in some environments.
#include <string.h> // May need to be included explicitly in some environments.

#include "declarations_definitions_and_enumerations.h"
#include "auxiliary.h"

/*

    ------------------------------------------------------------------------
       GENERAL — Auxiliary functions not necessarily specific to Kickshaw
    ------------------------------------------------------------------------

*/


/* 

    Version comparison function used when glibc is not installed.

*/

#if !__GLIBC__
gint8 strverscmp_nonglibc (const gchar *version1, const gchar *version2) {
    guint major1 = 0, minor1 = 0, bugfix1 = 0;
    guint major2 = 0, minor2 = 0, bugfix2 = 0;

    sscanf (version1, "%u.%u.%u", &major1, &minor1, &bugfix1);
    sscanf (version2, "%u.%u.%u", &major2, &minor2, &bugfix2);

    if (major1 < major2) return -1;
    if (major1 > major2) return 1;
    if (minor1 < minor2) return -1;
    if (minor1 > minor2) return 1;
    if (bugfix1 < bugfix2) return -1;
    if (bugfix1 > bugfix2) return 1;
    return 0;
}
#endif

/* 

    Expands a row using the path obtained from the given iterator.

*/

void expand_row_from_iter (GtkTreeIter *local_iter)
{
    g_autoptr(GtkTreePath) path = gtk_tree_model_get_path (ks.ts_model, local_iter);

    gtk_tree_view_expand_row (GTK_TREE_VIEW (ks.treeview), path, FALSE); // FALSE = just expand immediate children.
}

/* 

   Extracts a substring from a string using a regular expression

   Returns a newly allocated string that must be freed with g_free() after use.

*/

gchar *extract_substring_via_regex (const gchar *string, const gchar *regex_str)
{
    // No compile or match options; no GError return location.
    g_autoptr(GRegex) regex = g_regex_new (regex_str, 0, 0, NULL);
    g_autoptr(GMatchInfo) match_info; // match_info is created even if g_regex_match returns FALSE.
    gchar *match;

    g_regex_match (regex, string, 0, &match_info); // No match options.
    match = g_match_info_fetch (match_info, 0); // 0 = full text of the match.

    return match;
}

/* 

    Frees dynamically allocated memory for all strings in a static string array.

*/

void free_elements_of_static_string_array (      gchar    **string_array, 
                                                 gint8      number_of_fields, 
                                           const gboolean   set_to_NULL)
{
    do {
        --number_of_fields; // x fields = string_array[0 ... x-1]

        g_free (string_array[number_of_fields]);

        if (set_to_NULL) {
            string_array[number_of_fields] = NULL;
        }
    } while (number_of_fields);
}

/* 

    Retrieves the current font so it can be used with a Pango font description.

    Returns a newly allocated string that must be freed with g_free() after use.

*/

gchar *get_font_name (void)
{
    g_autofree gchar *font_and_size;
    gchar *font_name;

    g_object_get (gtk_settings_get_default (), "gtk-font-name", &font_and_size, NULL);
    font_name = extract_substring_via_regex (font_and_size, ".*(?=( .*)$)"); // e.g. Serif Bold Italic 12 -> Serif Bold Italic

    return font_name;
}

/* 

    Retrieves the current font size so icon image sizes can be adjusted accordingly.

*/

guint get_font_size (void)
{
    g_autofree gchar *font_str, *font_substr;
    guint font_size;

    g_object_get (gtk_settings_get_default (), "gtk-font-name", &font_str, NULL);
    font_substr = extract_substring_via_regex (font_str, "\\d+$"); // e.g. Sans 12 -> 12
    font_size = atoi (font_substr); 

    return font_size;
}

/* 

    Returns the time of last modification of an icon image file as an RFC 3339–encoded string.

    The returned string is dynamically allocated and must be freed with g_free() after use.

*/

gchar *get_modification_time_for_icon (const gchar *icon_path)
{
    g_autoptr(GFile) file = g_file_new_for_path (icon_path);
    g_autoptr(GError) error = NULL;
    g_autoptr(GFileInfo) file_info = g_file_query_info (file, G_FILE_ATTRIBUTE_TIME_MODIFIED, G_FILE_QUERY_INFO_NONE, NULL, &error);

    if (G_UNLIKELY (error)) {
        g_autofree gchar *err_msg = g_strdup_printf (_("<b>Could not retrieve the modification time for the icon</b> <tt>%s</tt> <b>!\n"
                                                       "Error:</b> %s"), 
                                                     icon_path, error->message);

        show_errmsg (err_msg);
    }

#if GLIB_CHECK_VERSION(2,62,0)
    g_autoptr(GDateTime) icon_modification_time = g_file_info_get_modification_date_time (file_info);
#else
    GTimeVal icon_modification_time;

    g_file_info_get_modification_time (file_info, &icon_modification_time);
#endif

#if GLIB_CHECK_VERSION(2,62,0)
    gchar *icon_modification_time_str = g_date_time_format_iso8601 (icon_modification_time);

    return icon_modification_time_str;
#else
    return g_time_val_to_iso8601 (&icon_modification_time);
#endif
}

/* 

    SetS an iterator to the top-level node of a given path.

*/

void get_toplevel_iter_from_path (GtkTreeIter *local_iter, 
                                  GtkTreePath *local_path)
{
    g_autofree gchar *toplevel_str = g_strdup_printf ("%i", gtk_tree_path_get_indices (local_path)[0]);

    gtk_tree_model_get_iter_from_string (ks.ts_model, local_iter, toplevel_str);
}

/* 

    Short helper to check whether a string equals one of several values
    (strcmp(x, y) == 0 || strcmp(x, z) == 0).

*/

gboolean streq_any (const gchar *string, ...)
{
    va_list arguments;

    va_start (arguments, string);

    while (TRUE) {
        const gchar *check = va_arg (arguments, gchar *);

        if (!check || STREQ (string, check)) {
            va_end (arguments); // Mandatory for safety and implementation/platform neutrality.

            /*
                check != NULL -> TRUE -> one of the string in the argument list is equal to the string to be compared against.
                check != NULL -> FALSE -> none of the strings in the argument list is equal to the string to be compared against.
            */
            return (check != NULL);
        }
    }
}

/* 

    Adds elements to a GPtrArray.

*/

void add_elements_to_ptr_array (GPtrArray *array, ...)
{
    va_list arguments;
    gpointer element;

    va_start (arguments, array);

    while ((element = va_arg (arguments, gpointer))) { // Parantheses avoid compiler warning
        g_ptr_array_add (array, element);
    }

    va_end (arguments); // Mandatory for safety and implementation/platform neutrality.
}

/*

    An equivalent of rm -rf.

    This function uses recursion. It is also a particularly suitable case 
    for using the g_autoptr() macro to keep the code tidy.

*/

gboolean rm_rf (GFile         *file, 
                GCancellable  *cancellable, 
                GError       **error)
{
    g_autoptr(GFileEnumerator) enumerator = g_file_enumerate_children (file, G_FILE_ATTRIBUTE_STANDARD_NAME, 
                                            G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS,
                                            cancellable, NULL);

    while (enumerator) {
        GFile *child;

        if (!(g_file_enumerator_iterate (enumerator, NULL, &child, cancellable, error))) {
            return FALSE;
        }
        if (!child) {
            break;
        }
        if (!(rm_rf (child, cancellable, error))) {
            return FALSE;
        }
    }

    return g_file_delete (file, cancellable, error);
}

/*

    Retrieves text stored in a GResource element.

    The returned string is dynamically allocated and must be freed after use.

*/

gchar *get_txt_from_resources (      GResource *resources, 
                               const gchar     *path)
{
    GBytes *data = g_resource_lookup_data (resources, path, G_RESOURCE_LOOKUP_FLAGS_NONE, NULL);
    g_autoptr(GByteArray) gbarray = g_bytes_unref_to_array (data);
    gchar *resource = g_strdup (g_strchomp ((gchar *) gbarray->data));

    return resource;
}

/* 

    ----------------------------------------------------------------------
       KICKSHAW-SPECIFIC — Auxiliary functions for use in Kickshaw only
    ----------------------------------------------------------------------

*/

/*

    A safe way to set the values of several bits of the settings bit field.

*/

void set_settings (gint field, ...)
{
    va_list arguments;

    va_start (arguments, field);

    while (field != -1) {
        const gboolean value = va_arg (arguments, gint);

        switch (field) {
            case M_CREATE_BACKUP_BEFORE_OVERWRITING_MENU:
                ks.settings.create_backup = value;
                break;
            case M_USE_TABS_FOR_INDENTATIONS:
                ks.settings.tabs_for_indentations = value;
                break;
            case M_KEEP_ROOT_MENU_SEPARATE:
                ks.settings.separate_root_menu = value;
                break;
            case M_SORT_EXECUTE_AND_STARTUPN_OPTIONS:
                ks.settings.autosort_options = value;
                break;
            case M_NOTIFY_ABOUT_EXECUTE_OPT_CONVERSIONS:
                ks.settings.always_notify_about_opts_conv = value;
                break;
            case M_USE_HEADER_BAR:
                ks.settings.use_header_bar = value;
                break;
            case M_SHOW_MENU_BUTTON:
                ks.settings.show_menu_button = value;
                break;
        }

        field = va_arg (arguments, gint);
    }

    va_end (arguments);
}

/*

    Deletes all undo stack items.

*/

void delete_undo_stack_items (void)
{
    g_autoptr(GError) error = NULL;

    for (gint stack_file_cnt = 1; stack_file_cnt <= ks.number_of_undo_stack_items; ++stack_file_cnt) {
        g_autofree gchar *undo_stack_item_str = g_strdup_printf ("%s/%i", ks.tmp_path, stack_file_cnt);
        g_autoptr(GFile) undo_stack_item = g_file_new_for_path (undo_stack_item_str);

        if (G_UNLIKELY (!(g_file_delete (undo_stack_item, NULL, &error)))) {
            g_autofree gchar *err_msg = g_strdup_printf (_("<b>Could not remove undo stack item</b> <tt>%1$i</tt> "
                                                           "<b>inside tmp folder</b> <tt>%2$s</tt> <b>!\n"
                                                           "Error:</b> %3$s"), 
                                                         stack_file_cnt, ks.tmp_path, error->message);

            show_errmsg (err_msg);

            break;
        }
    }
}

/*

    Deletes the autosave file from the home folder.

*/

void delete_autosave (void)
{
    g_autofree gchar *autosave_file_path = g_build_filename (ks.home_dir, ".kickshaw_autosave", NULL);
    g_autoptr(GFile) autosave_file = g_file_new_for_path (autosave_file_path);

    /* The autosave file does not exist after ...
       - the immediate start of the program
       - a saving operation when no edit has been done yet. */
    if (g_file_query_exists (autosave_file, NULL)) {
        g_autoptr(GError) error = NULL;

        g_file_delete (autosave_file, NULL, &error);

        if (G_UNLIKELY (error)) {
            g_autofree gchar *err_msg = g_strdup_printf (_("<b>Could not delete the autosave file</b> <tt>%s</tt> <b>!\nError:</b> %s"), 
                                                         autosave_file_path, error->message);

            show_errmsg (err_msg);
        }
    }
}

/* 

    Refreshes the txt_fields array with values from the currently selected row.

*/

void repopulate_txt_fields_array (void)
{
    // FALSE = don't set array elements to NULL after freeing.
    free_elements_of_static_string_array (ks.txt_fields, NUMBER_OF_TXT_FIELDS, FALSE);
    gtk_tree_model_get (ks.ts_model, &ks.iter, 
                        TS_ICON_PATH, &ks.txt_fields[ICON_PATH_TXT],
                        TS_MENU_ELEMENT, &ks.txt_fields[MENU_ELEMENT_TXT],
                        TS_TYPE, &ks.txt_fields[TYPE_TXT],
                        TS_VALUE, &ks.txt_fields[VALUE_TXT],
                        TS_MENU_ID, &ks.txt_fields[MENU_ID_TXT],
                        TS_EXECUTE, &ks.txt_fields[EXECUTE_TXT],
                        TS_ELEMENT_VISIBILITY, &ks.txt_fields[ELEMENT_VISIBILITY_TXT],
                        -1);
}

/* 

    Creates a new label with formatting.

*/

GtkWidget *new_label_with_formattings (const gchar    *label_txt, 
                                       const gboolean  wrap)
{
    return gtk_widget_new (GTK_TYPE_LABEL, "label", label_txt, "xalign", 0.0, "use-markup", TRUE, 
                          "wrap", wrap, "wrap-mode", PANGO_WRAP_WORD_CHAR, "max-width-chars", 1, NULL);
}

/* 

    Convenience function for creating a dialog window.

    The dialog can have up to three buttons. If fewer are needed, 
    set the "button_txt_2" and/or "button_txt_3" argument to NULL.
    If content other than a label should be added to the content area, 
    set "show_immediately" to DONT_SHOW_IMMEDIATELY (= FALSE). 
    Then call gtk_widget_show_all() after all additional widgets have been added to the content area.

*/

GtkWidget *create_dialog (      GtkWidget **dialog, 
                          const gchar      *dialog_title, 
                          const gchar      *icon_name, 
                          const gchar      *label_txt, 
                          const gchar      *button_txt_1, 
                          const gchar      *button_txt_2, 
                          const gchar      *button_txt_3, 
                          const gboolean    show_immediately)
{
    GtkWidget *content_area, *label;
    g_autofree gchar *label_txt_with_addl_border = g_strdup_printf ("\n%s\n", label_txt);

    *dialog = gtk_dialog_new_with_buttons (NULL, GTK_WINDOW (ks.window), GTK_DIALOG_MODAL, 
                                           button_txt_1, 1, button_txt_2, 2, button_txt_3, 3,
                                           NULL);

    set_header_bar_for_dialog (*dialog, dialog_title);

    content_area = gtk_dialog_get_content_area (GTK_DIALOG (*dialog));
    gtk_container_set_border_width (GTK_CONTAINER (content_area), 10);
    gtk_container_add (GTK_CONTAINER (content_area), gtk_image_new_from_icon_name (icon_name, GTK_ICON_SIZE_DIALOG));
    label = new_label_with_formattings (label_txt_with_addl_border, TRUE);
    gtk_widget_set_size_request (label, 570, -1);
    gtk_container_add (GTK_CONTAINER (content_area), label);
    if (show_immediately) {
        gtk_widget_show_all (*dialog);
    }
    gtk_window_set_position (GTK_WINDOW (*dialog), GTK_WIN_POS_CENTER_ALWAYS);

    return content_area;
}

/*

    Creates a formatted header bar for a dialog window.

*/

void set_header_bar_for_dialog (      GtkWidget *dialog, 
                                const gchar     *dialog_title_txt)
{
    GtkWidget *header_bar = gtk_header_bar_new ();
    g_autoptr(GtkCssProvider) header_bar_provider = gtk_css_provider_new ();

    gtk_header_bar_set_title (GTK_HEADER_BAR (header_bar), dialog_title_txt);
    gtk_window_set_titlebar (GTK_WINDOW (dialog), header_bar);

    gtk_css_provider_load_from_data (header_bar_provider, 
                                     "headerbar { min-height:0px;"
                                     "            padding-top:0px;"
                                     "            padding-bottom:0px; }",
                                     -1, NULL);
    gtk_style_context_add_provider (gtk_widget_get_style_context (header_bar), 
                                    GTK_STYLE_PROVIDER (header_bar_provider), 
                                    GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
}

/*

    Shows a simple informational dialog.

*/

void show_message_dialog (const gchar *dialog_title, 
                          const gchar *icon_name, 
                          const gchar *label_txt, 
                          const gchar *button_txt)
{
    GtkWidget *dialog;

    create_dialog (&dialog, dialog_title, icon_name, label_txt, button_txt, NULL, NULL, SHOW_IMMEDIATELY);

    gtk_dialog_run (GTK_DIALOG (dialog));
    gtk_widget_destroy (dialog);
}

/* 

    Creates a file dialog for opening or saving a menu.

*/

void create_file_dialog (      GtkWidget **dialog, 
                         const gboolean    open)
{
    g_autofree gchar *menu_folder_str = (ks.filename) ? g_path_get_dirname (ks.filename) : g_get_current_dir ();
    g_autoptr(GFile) menu_folder = g_file_new_for_path (menu_folder_str);
    GtkFileFilter *file_filter;

    // Translation note: the verb "Open".
    *dialog = gtk_file_chooser_dialog_new ((open) ? _("Open Openbox Menu File") : 
	                                                _("Save Openbox Menu File As…"), 
                                           GTK_WINDOW (ks.window),
                                           (open) ? GTK_FILE_CHOOSER_ACTION_OPEN : GTK_FILE_CHOOSER_ACTION_SAVE, 
                                           C_("Cancel|File Dialogue", "_Cancel"), GTK_RESPONSE_CANCEL,
										   // Translation note: the verb "Open".
                                           (open) ? _("_Open") :
            										_("_Save"), GTK_RESPONSE_ACCEPT,
                                           NULL);

    if (g_file_query_exists (menu_folder, NULL)) {
        gtk_file_chooser_set_current_folder (GTK_FILE_CHOOSER (*dialog), menu_folder_str);
    }

    file_filter = gtk_file_filter_new ();
    gtk_file_filter_set_name (file_filter, _("Openbox Menu Files"));
    gtk_file_filter_add_pattern (file_filter, "*.xml");
    gtk_file_chooser_add_filter (GTK_FILE_CHOOSER (*dialog), file_filter);
}

/*

    Determines whether a dark theme or a theme with a background/gradient is active.

*/

void determine_theme_type (void)
{
    GtkStyleContext *window_context = gtk_widget_get_style_context (ks.window);
    g_autoptr(cairo_pattern_t) background_image = NULL; // Default value.

    gtk_style_context_get (window_context, GTK_STATE_FLAG_NORMAL, GTK_STYLE_PROPERTY_BACKGROUND_IMAGE, &background_image, NULL);

    const gboolean at_startup = (ks.red_hue == NULL);
          gchar *old_color = ks.red_hue;

    if (!background_image) {
        g_autoptr(GdkRGBA) fg_color, bg_color;

        gtk_style_context_get (window_context, GTK_STATE_FLAG_NORMAL, 
                               GTK_STYLE_PROPERTY_COLOR, &fg_color, 
                               GTK_STYLE_PROPERTY_BACKGROUND_COLOR, &bg_color, 
                               NULL);

        if (fg_color->red + fg_color->green + fg_color->blue < bg_color->red + bg_color->green + bg_color->blue) {
            ks.red_hue = "darkred";
            ks.dark_theme = FALSE;
        }
        else {
            ks.red_hue = "salmon";
            ks.dark_theme = TRUE;
        }

        ks.bg_image_or_gradient = FALSE;
    }
    else {
        ks.bg_image_or_gradient = TRUE;
        ks.red_hue = "red";
    }

    if (at_startup || !STREQ (ks.red_hue, old_color)) {
        const gchar *right_to_left = (gtk_widget_get_default_direction () == GTK_TEXT_DIR_RTL) ? "&#8207;" : "";
    
        ks.blue_hue = (ks.bg_image_or_gradient) ? "blue" : ((!ks.dark_theme) ? "darkblue" : "lightblue");

        // Translation note: If the target language distinguishes between definite and indefinite forms, 
        // please use the indefinite form (“Label”), not the definite one (“The Label”).
        g_string_printf (ks.label_field_txt, _(" Label (<span foreground='%s'>*</span>): "), ks.red_hue);
        if (G_UNLIKELY (g_slist_find_custom (ks.menu_ids, "", (GCompareFunc) strcmp))) {
            // Translation note: If the target language distinguishes between definite and indefinite forms, 
            // please use the indefinite form (“Menu ID”), not the definite one (“The Menu ID”).
            g_string_printf (ks.menu_ID_field_txt, _(" Menu ID (<span foreground='%s'>*</span>): "), ks.blue_hue);
        }
        else {
            // Translation note: If the target language distinguishes between definite and indefinite forms, 
            // please use the indefinite form (“Menu ID”), not the definite one (“The Menu ID”).
            g_string_assign (ks.menu_ID_field_txt, _(" Menu ID: "));
        }
        // For right-to-left scripts, the following three require the &#8207 entity so the colon appears on the correct side.
        // For the label texts above it works without, because there are translations.
        g_string_printf (ks.execute_field_txt, "%s Execute (<span foreground='%s'>*</span>): ", right_to_left, ks.red_hue);
        g_string_printf (ks.prompt_field_txt, "%s Prompt (<span foreground='%s'>*</span>): ", right_to_left, ks.red_hue);
        g_string_printf (ks.command_field_txt, "%s Command (<span foreground='%s'>*</span>): ", right_to_left, ks.red_hue);
        g_string_printf (ks.mandatory_label_txt, "(<span foreground='%s'>*</span>) = %s", ks.red_hue, _("mandatory"));
        g_string_printf (ks.mandatory_empty_string_txt, "(<span foreground='%s'>*</span>) = %s; "
                                                        "(<span foreground='%s'>*</span>) = %s",
                                                        ks.red_hue, _("mandatory"), 
														// Translation note: "mand. as" means "mandatory because" here.
														ks.blue_hue, _("mand. as empty-string menu ID exists"));
        g_string_printf (ks.double_menu_id_label_txt, " <span foreground='%s'>!!</span> %s <span foreground='%s'>!!</span> ", 
                                                      ks.red_hue,  _("Menu ID already exists "), ks.red_hue);
    }

    if (!at_startup && !STREQ (ks.red_hue, old_color)) {
        gtk_label_set_markup (GTK_LABEL (ks.entry_labels[MENU_ELEMENT_OR_VALUE_ENTRY]), ks.label_field_txt->str);
        gtk_label_set_markup (GTK_LABEL (ks.entry_labels[MENU_ID_ENTRY]), ks.menu_ID_field_txt->str);
        gtk_label_set_markup (GTK_LABEL (ks.entry_labels[EXECUTE_ENTRY]), ks.execute_field_txt->str);
        if (!gtk_widget_get_visible (ks.enter_values_box)) {
            gtk_label_set_markup (GTK_LABEL (ks.options_labels[PROMPT]), ks.prompt_field_txt->str);
        }
        gtk_label_set_markup (GTK_LABEL (ks.options_labels[COMMAND]), ks.command_field_txt->str);
        gtk_label_set_markup (GTK_LABEL (ks.mandatory), 
                              (G_UNLIKELY (g_slist_find_custom (ks.menu_ids, "", (GCompareFunc) strcmp))) ? 
                              ks.mandatory_empty_string_txt->str : ks.mandatory_label_txt->str);
        gtk_label_set_markup (GTK_LABEL (ks.double_menu_id_label), ks.double_menu_id_label_txt->str);

        for (guint8 snotify_opts_cnt = 0; snotify_opts_cnt < NUMBER_OF_STARTUPNOTIFY_OPTS; ++snotify_opts_cnt) {
            if (gtk_widget_get_visible (ks.suboptions_labels[snotify_opts_cnt]) && 
                strstr (gtk_label_get_text (GTK_LABEL (ks.suboptions_labels[snotify_opts_cnt])), "*")) {
                g_autofree gchar *suboptions_label_txt = g_strdup_printf ("%s (<span foreground='%s'>*</span>): ", 
                                                                          ks.startupnotify_displayed_txts[snotify_opts_cnt], 
                                                                          ks.red_hue);

                gtk_label_set_markup (GTK_LABEL (ks.suboptions_labels[snotify_opts_cnt]), suboptions_label_txt);
            }
        }

        gtk_widget_queue_draw (ks.treeview); // Redraw treeview
    }
}

/* 

    Creates icon images for the case of a broken path or an invalid image file.

*/

void create_invalid_icon_imgs (void)
{
    for (guint8 invalid_icon_img_cnt = 0; invalid_icon_img_cnt < NUMBER_OF_INVALID_ICON_IMGS; ++invalid_icon_img_cnt) {
        // This becomes only true if the font size or icon theme have already been changed during the runtime of this program.
        if (G_UNLIKELY (ks.invalid_icon_imgs[invalid_icon_img_cnt])) {
            g_object_unref (ks.invalid_icon_imgs[invalid_icon_img_cnt]);
        }

        const gchar *icon_name = (invalid_icon_img_cnt == INVALID_PATH_ICON) ? "dialog-question" : "image-missing";
        g_autoptr(GdkPixbuf) invalid_icon_img_pixbuf_dialog_size = gtk_icon_theme_load_icon (gtk_icon_theme_get_default (), 
                                                                                             icon_name, 48, 
                                                                                             GTK_ICON_LOOKUP_FORCE_SIZE, 
                                                                                             NULL);

        ks.invalid_icon_imgs[invalid_icon_img_cnt] = gdk_pixbuf_scale_simple (invalid_icon_img_pixbuf_dialog_size, 
                                                                              ks.font_size + 10, ks.font_size + 10, 
                                                                              GDK_INTERP_BILINEAR);
    }
}

/* 

    Checks expansion states of nodes to determine:
    - how to set the sensitivity of application menu items and toolbar buttons for expanding/collapsing all nodes.

    In this case the function is called after a selection.
    It suffices to check whether there is at least one expanded and one collapsed node,
    regardless of their position in the tree view.
    at_least_one_is_expanded and at_least_one_is_collapsed are set to TRUE if the respective conditions apply.

    - which expansion and collapse options to add to the context menu 
      (expand recursively, expand only immediate children, and collapse).

    In this case the function is called after one or more rows with at least one child each were right-clicked.
    It must be checked whether an immediate child of a row is expanded. If this applies to one of the selected rows,
    or at least one of the selected rows is currently not expanded, the option to expand only immediate children is
    added to the context menu later.
    The second check determines whether there is at least one collapsed node; if one is collapsed, it should be
    possible to expand all nodes recursively. Whether the collapse option should be added can be decided by checking
    if the root node is expanded (not done here).
    at_least_one_imd_ch_is_collapsed and at_least_one_is_collapsed are set to TRUE if the respective conditions apply.

*/

gboolean check_expansion_statuses_of_nodes (GtkTreeModel *foreach_or_filter_model, 
                                            GtkTreePath  *foreach_or_filter_path, 
                                            GtkTreeIter  *foreach_or_filter_iter, 
                                            gpointer      expansion_statuses_of_nodes_ptr)
{
    b_ExpansionStatuses *expansion_statuses_of_nodes = (b_ExpansionStatuses *) expansion_statuses_of_nodes_ptr;
    g_autoptr(GtkTreePath) model_path = NULL; // Default value.

    if (gtk_tree_model_iter_has_child (foreach_or_filter_model, foreach_or_filter_iter)) {
        if (ks.ts_model != foreach_or_filter_model) { // = filter, called from create_context_menu ()
            // The path of the underlying model, not the filter model, is needed to check whether the row is selected.
            model_path = gtk_tree_model_filter_convert_path_to_child_path ((GtkTreeModelFilter *) foreach_or_filter_model, 
                                                                           foreach_or_filter_path);
        }

        if (gtk_tree_view_row_expanded (GTK_TREE_VIEW (ks.treeview), (model_path) ? model_path : foreach_or_filter_path)) {
            if (!model_path) { // = called from row_selected ()
                expansion_statuses_of_nodes->at_least_one_is_expanded = TRUE;
            }
            else if (gtk_tree_path_get_depth (foreach_or_filter_path) == 1) {
                expansion_statuses_of_nodes->at_least_one_imd_ch_is_expanded = TRUE;
            }
        }
        else {
            expansion_statuses_of_nodes->at_least_one_is_collapsed = TRUE;
        }
    }

    // Iterate only until all queried statuses have a positive match.
    return (((model_path) ? expansion_statuses_of_nodes->at_least_one_imd_ch_is_expanded : 
                            expansion_statuses_of_nodes->at_least_one_is_expanded) && 
                            expansion_statuses_of_nodes->at_least_one_is_collapsed);
}

/* 

    Checks for existing options of an Execute action or a startupnotify option.

*/

void check_for_existing_options (      GtkTreeIter  *parent, 
                                 const guint8        number_of_opts, 
                                 const gchar       **options_array, 
                                       gboolean     *opts_exist)
{
    GtkTreeIter iter_loop;

    for (gint ch_cnt = 0; ch_cnt < gtk_tree_model_iter_n_children (ks.ts_model, parent); ++ch_cnt) {
        g_autofree gchar *menu_element_txt_loop;

        gtk_tree_model_iter_nth_child (ks.ts_model, &iter_loop, parent, ch_cnt);
        gtk_tree_model_get (ks.ts_model, &iter_loop, TS_MENU_ELEMENT, &menu_element_txt_loop, -1);

        for (guint8 opts_cnt = 0; opts_cnt < number_of_opts; ++opts_cnt) {
            if (STREQ (menu_element_txt_loop, options_array[opts_cnt])) {
                opts_exist[opts_cnt] = TRUE;
            }
        }
    }
}

/* 

    Looks for invisible descendants of a row.

*/

gboolean check_if_invisible_descendant_exists (              GtkTreeModel *filter_model, 
                                               G_GNUC_UNUSED GtkTreePath  *filter_path,
                                                             GtkTreeIter  *filter_iter, 
                                                             gboolean     *at_least_one_descendant_is_invisible)
{
    g_autofree gchar *menu_element_txt_loop, 
                     *type_txt_loop;

    gtk_tree_model_get (filter_model, filter_iter, 
                        TS_MENU_ELEMENT, &menu_element_txt_loop, 
                        TS_TYPE, &type_txt_loop, 
                        -1);

    *at_least_one_descendant_is_invisible = !menu_element_txt_loop && !STREQ (type_txt_loop, "separator");

    // Stop iterating if an invisible descendant has been found.
    return *at_least_one_descendant_is_invisible;
}

/* 

    Looks for invisible ancestors of a given path and
    returns the visibility status of the first one found.

*/

guint8 check_if_invisible_ancestor_exists (GtkTreeModel *local_model, 
                                           GtkTreePath  *local_path)
{
    if (gtk_tree_path_get_depth (local_path) == 1) {
        return NONE_OR_VISIBLE_ANCESTOR;
    }

    guint8 visibility_of_ancestor;

    g_autoptr(GtkTreePath) path_loop = gtk_tree_path_copy (local_path);

    do {
        g_autofree gchar *element_ancestor_visibility_txt_loop;
        GtkTreeIter iter_loop;

        gtk_tree_path_up (path_loop);
        gtk_tree_model_get_iter (local_model, &iter_loop, path_loop);
        gtk_tree_model_get (local_model, &iter_loop, TS_ELEMENT_VISIBILITY, &element_ancestor_visibility_txt_loop, -1);

        if (G_UNLIKELY (element_ancestor_visibility_txt_loop && 
                        g_str_has_prefix (element_ancestor_visibility_txt_loop, "invisible"))) {
            visibility_of_ancestor = (g_str_has_suffix  (element_ancestor_visibility_txt_loop, "orphaned menu")) ? 
                                      INVISIBLE_ORPHANED_ANCESTOR : INVISIBLE_ANCESTOR;

            return visibility_of_ancestor;
        }
    } while (gtk_tree_path_get_depth (path_loop) > 1);

    return NONE_OR_VISIBLE_ANCESTOR;
}

/* 

    Adds a row that contains an icon to a list.

*/

gboolean add_icon_occurrence_to_list (G_GNUC_UNUSED GtkTreeModel *foreach_model, 
                                                    GtkTreePath  *foreach_path,
                                                    GtkTreeIter  *foreach_iter,
                                      G_GNUC_UNUSED gpointer      user_data)
{
    g_autoptr(GdkPixbuf) icon;

    gtk_tree_model_get (ks.ts_model, foreach_iter, TS_ICON_IMG, &icon, -1);
    if (icon) {
        ks.rows_with_icons = g_slist_prepend (ks.rows_with_icons, gtk_tree_path_copy (foreach_path));
    }

    return CONTINUE_ITERATING;
}

/* 

    Activates or deactivates all other column check buttons when "All columns" is selected or unselected. 

*/

void find_in_columns_management (const gboolean    origin_is_fr, 
                                       GtkWidget **in_columns, 
                                       GtkWidget  *in_all_columns, 
                                       gulong     *handler_ids, 
                                 const gchar      *column_check_button_clicked)
{
    const gboolean all_columns_check_button_clicked = STREQ (column_check_button_clicked, "all");
    const gboolean marking_active = gtk_style_context_has_class (gtk_widget_get_style_context (in_all_columns), 
                                                                 "user_intervention_requested");

    if (marking_active || all_columns_check_button_clicked) {
        const gboolean find_in_all_activated = gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (in_all_columns));

        for (guint8 columns_cnt = 0; columns_cnt < COL_ELEMENT_VISIBILITY; ++columns_cnt) {
            if (origin_is_fr && columns_cnt == COL_TYPE) {
                continue;
            }
            if (all_columns_check_button_clicked) {
                g_signal_handler_block (in_columns[columns_cnt], handler_ids[columns_cnt]);
                gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (in_columns[columns_cnt]), find_in_all_activated);
                g_signal_handler_unblock (in_columns[columns_cnt], handler_ids[columns_cnt]);
                gtk_widget_set_sensitive (in_columns[columns_cnt], !find_in_all_activated);
            }
            if (marking_active) {
                gtk_style_context_remove_class (gtk_widget_get_style_context (in_columns[columns_cnt]), 
                                                "user_intervention_requested");
            }
        }
        if (marking_active) {
            gtk_style_context_remove_class (gtk_widget_get_style_context (in_all_columns), "user_intervention_requested");
        }
    }
}

/*

    If regular expressions are enabled, checks whether the regular expression is valid.
    If not, an error message will be displayed.

*/

gboolean check_if_regex_is_valid (GtkWidget *regex_checkbox, GtkWidget *entry_field)
{
    if (gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (regex_checkbox))) {
        g_autoptr(GError) error = NULL;
        g_autoptr(GRegex) regex = g_regex_new (gtk_entry_get_text (GTK_ENTRY (entry_field)), 0, 0, &error);

        if (G_UNLIKELY (error)) {
            g_autofree gchar *err_msg = g_strdup_printf (_("<b>The regular expression that has been entered is invalid.\n"
                                                           "Error:</b> %s"), error->message);

            show_errmsg (err_msg);           

            return FALSE;
        }
    }

    return TRUE;
}

/* 

    Gives a widget a red border (entry field) or background color (checkbox) to indicate 
    that a mandatory input or setting is missing, or there is something wrong with existing data. 

*/

void visually_indicate_request_for_user_intervention (GtkWidget      *widget, 
                                                      GtkCssProvider *css_provider)
{
    GtkStyleContext *context = gtk_widget_get_style_context (widget);

    if (!gtk_style_context_has_class (context, "user_intervention_requested")) {
        gtk_style_context_add_class (context, "user_intervention_requested");
        gtk_css_provider_load_from_data (css_provider, (GTK_IS_ENTRY (widget)) ? 
        ".user_intervention_requested { border:darkred solid 2px; }" : 
        ".user_intervention_requested { color:white; background:darkred; }", 
        -1, NULL);
    }
}

/*

    Checks whether Kickshaw is installed system-wide and returns the appropriate command for a restart.

*/

const gchar *installed_or_local (void)
{
    g_auto(GStrv) paths = g_strsplit (g_getenv ("PATH"), ":", -1);
    guint paths_idx = 0;
    struct stat sb;
    const gchar *command = "./kickshaw"; // Default value.

    while (paths[paths_idx]) {
        g_autofree gchar *path_with_iconv = g_strconcat (paths[paths_idx], "/kickshaw", NULL);

        if (stat (path_with_iconv, &sb) == 0) {
            command = "kickshaw";

            break;
        }

        ++paths_idx;
    }

    return command;
}
