/*
   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 <string.h> // May need to be included explicitly in some environments.

#include "declarations_definitions_and_enumerations.h"
#include "editing.h"

// ED = editing; indicates that these enums are only used in this file.
enum { ED_FILTER_SELECTED_PATH, ED_VISUALISE_RECURSIVELY, ED_NUMBER_OF_FILTER_VISUALISATION_ELEMENTS };

static gboolean add_all_strs_of_tree_column_to_liststore (              GtkTreeModel *foreach_model, 
                                                          G_GNUC_UNUSED GtkTreePath  *foreach_path, 
                                                                        GtkTreeIter  *foreach_iter, 
                                                                        gpointer      treestore_pos_ptr);
static gboolean check_and_adjust_dependent_element_visibilities (GtkTreeModel *filter_model, 
                                                                 GtkTreePath  *filter_path, 
                                                                 GtkTreeIter  *filter_iter, 
                                                                 gpointer     *filter_visualization);
static void empty_label_msg (void);
static gboolean image_type_filter (              const GtkFileFilterInfo *filter_info, 
                                   G_GNUC_UNUSED       gpointer           user_data);
static void sort_execute_or_startupnotify_options (      GtkTreeIter *parent, 
                                                   const gchar       *execute_or_startupnotify);

/* 

    First sorts the Execute or Startupnotify options, then selects the newly inserted option.

*/

void sort_execute_or_startupnotify_options_after_insertion (      GtkTreeSelection *selection, 
                                                                  GtkTreeIter      *parent, 
                                                            const gchar            *execute_or_startupnotify, 
                                                            const gchar            *option)
{
    GtkTreeIter iter_loop;

    sort_execute_or_startupnotify_options (parent, execute_or_startupnotify);

    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);
        if (STREQ (menu_element_txt_loop, option)) {
            break;
        }
    }

    gtk_tree_selection_select_iter (selection, &iter_loop);
}

/* 

    Sorts Execute or Startupnotify options in the following order:
    Execute:       1. prompt, 2. command, 3. startupnotify
    Startupnotify: 1. enabled, 2. name, 3. wmclass, 4. icon

*/

static void sort_execute_or_startupnotify_options (      GtkTreeIter *parent, 
                                                   const gchar       *execute_or_startupnotify)
{
    const gboolean execute = (STREQ (execute_or_startupnotify, "Execute"));
    const guint number_of_options = (execute) ? NUMBER_OF_EXECUTE_OPTS : NUMBER_OF_STARTUPNOTIFY_OPTS;

    for (gint ch_cnt = 0; ch_cnt < gtk_tree_model_iter_n_children (ks.ts_model, parent) - 1; ++ch_cnt) {
        GtkTreeIter iter_loop_ch;
        g_autofree gchar *menu_element_txt_loop_ch;
        gboolean swapped = FALSE; // Default value.

        gtk_tree_model_iter_nth_child (ks.ts_model, &iter_loop_ch, parent, ch_cnt);
        gtk_tree_model_get (ks.ts_model, &iter_loop_ch, TS_MENU_ELEMENT, &menu_element_txt_loop_ch, -1);

        for (guint8 opt_cnt = 0; opt_cnt < number_of_options && !swapped; ++opt_cnt) {        
            if (STREQ (menu_element_txt_loop_ch, 
                (execute) ? ks.execute_options[opt_cnt] : ks.startupnotify_options[opt_cnt])) {
                break;
            }
            else {
                for (gint follow_ch_cnt = ch_cnt + 1; follow_ch_cnt < gtk_tree_model_iter_n_children (ks.ts_model, parent); ++follow_ch_cnt) {
                    GtkTreeIter iter_loop_follow_ch;
                    g_autofree gchar *menu_element_txt_loop_follow_ch;

                    gtk_tree_model_iter_nth_child (ks.ts_model, &iter_loop_follow_ch, parent, follow_ch_cnt);
                    gtk_tree_model_get (ks.ts_model, &iter_loop_follow_ch, TS_MENU_ELEMENT, &menu_element_txt_loop_follow_ch, -1);

                    if (STREQ ((execute) ? ks.execute_options[opt_cnt] : ks.startupnotify_options[opt_cnt], 
                        menu_element_txt_loop_follow_ch)) {
                        gtk_tree_store_swap (ks.treestore, &iter_loop_ch, &iter_loop_follow_ch);
                        swapped = TRUE;

                        break;
                    }
                }
            }
        }
    }
}

/* 

    Called after autosorting of options is enabled.
    Sorts all Execute and Startupnotify options in the tree store.

*/

gboolean sort_loop_after_sorting_activation (              GtkTreeModel *foreach_model, 
                                                           GtkTreePath  *foreach_path,
                                                           GtkTreeIter  *foreach_iter,
                                             G_GNUC_UNUSED gpointer      user_data)
{
    if (gtk_tree_path_get_depth (foreach_path) == 1 || gtk_tree_model_iter_n_children (foreach_model, foreach_iter) < 2) {
        return FALSE;
    }

    g_autofree gchar *menu_element_txt_loop, 
                     *type_txt_loop;

    gtk_tree_model_get (foreach_model, foreach_iter, 
                        TS_MENU_ELEMENT, &menu_element_txt_loop, 
                        TS_TYPE, &type_txt_loop,
                        -1);

    if ((STREQ (type_txt_loop, "action") && STREQ (menu_element_txt_loop, "Execute")) || 
         STREQ (type_txt_loop, "option block")) {
        sort_execute_or_startupnotify_options (foreach_iter, menu_element_txt_loop);
    }

    return FALSE;
}

/* 

    Moves the current selection up, down, to the top, or to the bottom.

*/

void move_selection (gpointer direction_pointer)
{
    const guint8 direction = GPOINTER_TO_UINT (direction_pointer);
          GtkTreeIter iter_new_pos = ks.iter;

    switch (direction) {
        case MOVE_UP:
        case MOVE_DOWN:
            if (direction == MOVE_UP) {
                gtk_tree_model_iter_previous (ks.ts_model, &iter_new_pos);
            }
            else {
                gtk_tree_model_iter_next (ks.ts_model, &iter_new_pos);
            }
            gtk_tree_store_swap (GTK_TREE_STORE (ks.ts_model), &ks.iter, &iter_new_pos);
            break;
        case MOVE_TOP:
            gtk_tree_store_move_after (ks.treestore, &ks.iter, NULL);
            break;
        case MOVE_BOTTOM:
            gtk_tree_store_move_before (ks.treestore, &ks.iter, NULL);
    }

    push_new_item_on_undo_stack (NOT_AFTER_SAVE); // First update undo stack so ...
    activate_change_done ();
    row_selected ();                              /* ... the toolbar buttons and 
                                                     application menu's undo/redo items get the correct sensitivity settings. */
}

/* 

    Sets the element visibility of menus, pipe menus, items, and separators 
    that depend on the menu element being visualized.

*/

static gboolean check_and_adjust_dependent_element_visibilities (GtkTreeModel *filter_model, 
                                                                 GtkTreePath  *filter_path, 
                                                                 GtkTreeIter  *filter_iter, 
                                                                 gpointer     *filter_visualization)
{
    g_autofree gchar *element_visibility_txt;

    gtk_tree_model_get (filter_model, filter_iter, TS_ELEMENT_VISIBILITY, &element_visibility_txt, -1);

    if (!element_visibility_txt) {
        return FALSE;
    }

    GtkTreeIter model_iter;

    g_autofree gchar *menu_element_txt_filter, 
                     *type_txt_filter;

    /*
        This makes the following condition easier to read.
        If the selected path is at the top level, filter_visualization[ED_FILTER_SELECTED_PATH] == NULL.
    */
    GtkTreePath *filter_selected_path = (GtkTreePath *) filter_visualization[ED_FILTER_SELECTED_PATH];
    const gboolean recursively_and_row_is_dsct = (GPOINTER_TO_UINT (filter_visualization[ED_VISUALISE_RECURSIVELY]) && 
                                                  (!filter_selected_path || // Selected path is at the top level = dsct. of NULL.
                                                   gtk_tree_path_is_descendant (filter_path, filter_selected_path)));

    gtk_tree_model_get (filter_model, filter_iter, 
                        TS_MENU_ELEMENT, &menu_element_txt_filter, 
                        TS_TYPE, &type_txt_filter, 
                        -1);
    gtk_tree_model_filter_convert_iter_to_child_iter ((GtkTreeModelFilter *) filter_model, &model_iter, filter_iter);

    /*
	    The current row is an ancestor of, or equal to, the selected row.

        A menu element can only be visible if all its ancestors are visible. 
        When visualizing a (pipe) menu, item, or separator, all ancestors’ visibility statuses 
        are set to "visible". 
        For simplicity, this is done without first checking the current status. 
        Visualized menus and items are given a placeholder label; separators are not, 
        as they are always visible if not under an invisible menu.
    */
    if (filter_selected_path && // Selected path is not at the top level.
        (gtk_tree_path_is_ancestor (filter_path, filter_selected_path) || 
        gtk_tree_path_compare (filter_path, filter_selected_path) == 0)) {
        gtk_tree_store_set (ks.treestore, &model_iter, TS_ELEMENT_VISIBILITY, "visible", -1);
        if (!menu_element_txt_filter && !STREQ (type_txt_filter, "separator")) {
            gtk_tree_store_set (ks.treestore, &model_iter, TS_MENU_ELEMENT, _("(Newly Created Label)"),  -1);
        }
    }
	// Current row is a descendant of the selected row, or unrelated to it.
    else {
        const guint8 invisible_ancestor = check_if_invisible_ancestor_exists (filter_model, filter_path);
        const gboolean unlabeled_menu_or_item = !menu_element_txt_filter && !STREQ (type_txt_filter, "separator");

        gchar *new_element_visibility_txt = 
            (invisible_ancestor || (!recursively_and_row_is_dsct && unlabeled_menu_or_item)) ? 
            ((invisible_ancestor) ? "invisible dsct. of invisible menu" : 
            ((STREQ (type_txt_filter, "item") ? "invisible item" : "invisible menu"))) : "visible";

        if (recursively_and_row_is_dsct && unlabeled_menu_or_item) {
            gtk_tree_store_set (ks.treestore, &model_iter, TS_MENU_ELEMENT, _("(Newly Created Label)"), -1);
        }

        gtk_tree_store_set (ks.treestore, &model_iter, TS_ELEMENT_VISIBILITY, new_element_visibility_txt, -1);
    }

    return FALSE;
}

/* 

    Makes one or more menus, pipe menus, items, or separators visible.

*/

void visualize_menus_items_and_separators (gpointer recursively_pointer)
{
    GtkTreeSelection *selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (ks.treeview));
#if GLIB_CHECK_VERSION(2,56,0)
    g_autolist(GtkTreePath) selected_rows = gtk_tree_selection_get_selected_rows (selection, &ks.ts_model);
#else
    GList *selected_rows = gtk_tree_selection_get_selected_rows (selection, &ks.ts_model);
#endif

    GList *selected_rows_loop;

    FOREACH_IN_LIST (selected_rows, selected_rows_loop) {
        gpointer filter_visualization[ED_NUMBER_OF_FILTER_VISUALISATION_ELEMENTS];
        GtkTreeIter iter_toplevel;

        g_autofree gchar *menu_element_txt_loop;
    
        get_toplevel_iter_from_path (&iter_toplevel, selected_rows_loop->data);
        gtk_tree_store_set (ks.treestore, &iter_toplevel, TS_ELEMENT_VISIBILITY, "visible", -1);

        gtk_tree_model_get (ks.ts_model, &iter_toplevel, TS_MENU_ELEMENT, &menu_element_txt_loop, -1);

        if (!menu_element_txt_loop) {
            gtk_tree_store_set (ks.treestore, &iter_toplevel, TS_MENU_ELEMENT, _("(Newly Created Label)"),  -1);
        }

        g_autoptr(GtkTreePath) path_toplevel = gtk_tree_model_get_path (ks.ts_model, &iter_toplevel);
        g_autoptr(GtkTreeModel) filter_model = gtk_tree_model_filter_new (ks.ts_model, path_toplevel);

        /*
            -menu
            -menu
                -menu
                    -item
                    -menu --- invisible ---   Path 1:0:1   selected_rows_loop->data
                -item
                -menu
            -menu

            is converted to

            -menu
                -item
                -menu --- invisible ---   Path 1:0   filter_visualization[ED_FILTER_SELECTED_PATH]
            -item

            -----------------------------------------------------------------------------------

            -menu
            -menu --- invisible ---   Path 1
                -item
            -menu

            is converted to NULL.
        */
        filter_visualization[ED_FILTER_SELECTED_PATH] =  
            (gpointer) gtk_tree_model_filter_convert_child_path_to_path ((GtkTreeModelFilter *) filter_model, 
                                                                         selected_rows_loop->data);

        filter_visualization[ED_VISUALISE_RECURSIVELY] = recursively_pointer;

        gtk_tree_model_foreach (filter_model, (GtkTreeModelForeachFunc) check_and_adjust_dependent_element_visibilities, 
                                filter_visualization);

        // Cleanup
        gtk_tree_path_free ((GtkTreePath *) filter_visualization[ED_FILTER_SELECTED_PATH]);
    }

    push_new_item_on_undo_stack (NOT_AFTER_SAVE); // First update undo stack so ...
    activate_change_done ();
    row_selected ();                              /* ... the toolbar buttons and 
                                                     application menu's undo/redo items get the correct sensitivity settings. */

#if !(GLIB_CHECK_VERSION(2,56,0))
    // Cleanup
    g_list_free_full (selected_rows, (GDestroyNotify) gtk_tree_path_free);
#endif
}

/* 

    File filter that allows only image files to be displayed.

*/

static gboolean image_type_filter (              const GtkFileFilterInfo *filter_info, 
                                   G_GNUC_UNUSED       gpointer           user_data)
{
    return (g_regex_match_simple ("image/.*", filter_info->mime_type, 0, 0));
}

/* 

    Opens a dialog for selecting an icon for a (pipe) menu or item.

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

*/

gchar *choose_icon (void)
{
	// Translation note: the verb "Open".
    GtkWidget *dialog = gtk_file_chooser_dialog_new (_("Open Image File"),
                                                     GTK_WINDOW (ks.window), 
                                                     GTK_FILE_CHOOSER_ACTION_OPEN,
                                                     C_("Cancel|File Dialogue", "_Cancel"), GTK_RESPONSE_CANCEL, 
													 // Translation note: the verb "Open".
                                                     _("_Open"), GTK_RESPONSE_ACCEPT, 
                                                     NULL);

    GtkFileFilter *open_image_file_filter = gtk_file_filter_new ();
    gchar *icon_filename = NULL; // Default value.

    gtk_file_chooser_set_current_folder (GTK_FILE_CHOOSER (dialog), "/");

    gtk_file_filter_set_name (open_image_file_filter, _("Image Files"));
    // The last two parameters (data and destroy function) are unused.
    gtk_file_filter_add_custom (open_image_file_filter, GTK_FILE_FILTER_MIME_TYPE, (GtkFileFilterFunc) image_type_filter, 
                                NULL, NULL);
    gtk_file_chooser_add_filter (GTK_FILE_CHOOSER (dialog), open_image_file_filter);

    if (gtk_dialog_run (GTK_DIALOG (dialog)) == GTK_RESPONSE_ACCEPT) {
        icon_filename = gtk_file_chooser_get_filename (GTK_FILE_CHOOSER (dialog));
    }

    gtk_widget_destroy (dialog);

    return icon_filename;
}

/* 

    Lets the user choose an icon after clicking the icon chooser button 
    or selecting the corresponding action from the context menu.

*/

void icon_choosing_by_button_or_context_menu (void)
{
    g_autofree gchar *icon_path;

    if (!(icon_path = choose_icon ())) {
        return;
    }
	// TRUE = show an error message on failure, FALSE = not during undo/redo.
    set_icon (&ks.iter, icon_path, TRUE, FALSE);

    // The icon was not chosen during the creation of a new element, so this must be recorded internally as a change that has been applied.
    // The icon hasn't been chosen during the addition of a new (pipe) menu or item, so changes have been applied.
    if (!gtk_widget_get_visible (ks.enter_values_box)) {
        push_new_item_on_undo_stack (NOT_AFTER_SAVE); // First update undo stack so ...
        activate_change_done ();
        row_selected ();                              /* ... the toolbar buttons and 
                                                         application menu's undo/redo items get the correct sensitivity settings. */
    }
}

/* 

    Sets an icon at the specified position if possible.

*/

gboolean set_icon (      GtkTreeIter *icon_iter, 
                   const gchar       *icon_path, 
                   const gboolean     display_err_msg, 
                   const gboolean     during_undo_redo)
{
    g_autoptr(GdkPixbuf) icon_in_original_size;
    g_autoptr(GError) error = NULL;

    if (G_UNLIKELY (!(icon_in_original_size = gdk_pixbuf_new_from_file (icon_path, &error)))) {
        if (display_err_msg) {
            g_autoptr(GString) err_msg = g_string_new (NULL);

            if (gtk_widget_get_visible (ks.remove_icon)) { // When entry fields are displayed at the bottom.
                g_autofree gchar *err_msg_substr;

                if (STREQ (ks.txt_fields[TYPE_TXT], "menu")) {
                    err_msg_substr = g_strdup_printf (_("The following error occurred while trying to add an icon to the menu '%s'"), 
                                                      ks.txt_fields[MENU_ELEMENT_TXT]);
                }
                else if (STREQ (ks.txt_fields[TYPE_TXT], "pipe menu")) {
                    err_msg_substr = g_strdup_printf (_("The following error occurred while trying to add an icon to the pipe menu '%s'"), 
                                                      ks.txt_fields[MENU_ELEMENT_TXT]);
                }
                else { // item
                    err_msg_substr = g_strdup_printf (_("The following error occurred while trying to add an icon to the item '%s'"), 
                                                      ks.txt_fields[MENU_ELEMENT_TXT]);
                }

                g_string_append (err_msg, err_msg_substr);
            }
            else { // When entry fields are displayed at the top (during the entry of a new menu element).
                const gchar *err_msg_substr;

                if (gtk_widget_get_visible (ks.entry_fields[EXECUTE_ENTRY])) {
                    err_msg_substr = _("The following error occurred while trying to add an icon to the new pipe menu");
                }
                else if (gtk_widget_get_visible (ks.entry_fields[MENU_ID_ENTRY])) {
                    err_msg_substr = _("The following error occurred while trying to add an icon to the new menu");
                }
                else { // item
                    err_msg_substr = _("The following error occurred while trying to add an icon to the new item");
                }

                g_string_append (err_msg, err_msg_substr);
            }

            g_string_append_printf (err_msg, ":\n\n<span foreground='%s'>%s</span>", ks.red_hue, error->message);

            show_errmsg (err_msg->str);
        }

        return FALSE;
    }

    // The icon was chosen during creation of a new element; store it later.
    if (gtk_widget_get_visible (ks.enter_values_box)) {
        gtk_entry_set_text (GTK_ENTRY (ks.entry_fields[ICON_PATH_ENTRY]), icon_path);

        return TRUE;
    }

    GtkTreeSelection *selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (ks.treeview));

    g_autoptr(GdkPixbuf) icon = gdk_pixbuf_scale_simple (icon_in_original_size, 
                                                         ks.font_size + 10, ks.font_size + 10, GDK_INTERP_BILINEAR);
    g_autofree gchar *icon_modification_time = get_modification_time_for_icon (icon_path);

    gtk_tree_store_set (GTK_TREE_STORE (ks.ts_model), icon_iter, 
                        TS_ICON_IMG, icon, 
                        TS_ICON_IMG_STATUS, NONE_OR_NORMAL, 
                        TS_ICON_PATH, icon_path, 
                        TS_ICON_MODIFICATION_TIME, icon_modification_time, 
                        -1);

    // This may be called by the timeout function when no row is selected.
    if (!during_undo_redo && gtk_tree_selection_count_selected_rows (selection)) {
        repopulate_txt_fields_array ();
        set_entry_fields ();
    }

    return TRUE;
}

/* 

    Removes icons from the selected menus or items.

*/

void remove_icons_from_menus_or_items (gpointer push_on_undo_stack)
{
    GtkTreeSelection *selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (ks.treeview));
#if GLIB_CHECK_VERSION(2,56,0)
    g_autolist(GtkTreePath) selected_rows = gtk_tree_selection_get_selected_rows (selection, &ks.ts_model);
#else
    GList *selected_rows = gtk_tree_selection_get_selected_rows (selection, &ks.ts_model);
#endif
  
    GList *selected_rows_loop;


    FOREACH_IN_LIST (selected_rows, selected_rows_loop) {
        GtkTreeIter iter_loop;

        gtk_tree_model_get_iter (ks.ts_model, &iter_loop, selected_rows_loop->data);
        gtk_tree_store_set (GTK_TREE_STORE (ks.ts_model), &iter_loop,
                            TS_ICON_IMG, NULL,
                            TS_ICON_IMG_STATUS, NONE_OR_NORMAL,
                            TS_ICON_MODIFICATION_TIME, NULL, 
                            TS_ICON_PATH, NULL, 
                            -1);
    }

    if (gtk_tree_selection_count_selected_rows (selection) == 1) {
        repopulate_txt_fields_array (); // No need to adjust menu item or toolbar button status here.
        set_entry_fields ();
    }

    if (GPOINTER_TO_UINT (push_on_undo_stack)) {
        push_new_item_on_undo_stack (NOT_AFTER_SAVE); // First update undo stack so ...
    }
    activate_change_done ();
    row_selected ();                         /* ... the toolbar buttons and 
                                                    application menu's undo/redo items get the correct sensitivity settings. */

#if !(GLIB_CHECK_VERSION(2,56,0))
    // Cleanup
    g_list_free_full (selected_rows, (GDestroyNotify) gtk_tree_path_free);
#endif
}

/* 

    Changes one or more values of a row after an entry field is modified.

*/

void change_row (gpointer changed_entry_field_ptr)
{
    const gchar *entry_txt[NUMBER_OF_ENTRY_FIELDS];
    const guint changed_entry_field = GPOINTER_TO_UINT (changed_entry_field_ptr);
    gchar *txt_field_to_compare = NULL; // Initialization avoids compiler warning.

    for (guint8 entry_field_cnt = 0; entry_field_cnt < NUMBER_OF_ENTRY_FIELDS; ++entry_field_cnt) {
        entry_txt[entry_field_cnt] = gtk_entry_get_text (GTK_ENTRY (ks.entry_fields[entry_field_cnt]));
    }

    switch (changed_entry_field) {
        case MENU_ELEMENT_OR_VALUE_ENTRY:
            txt_field_to_compare = (STREQ (ks.txt_fields[TYPE_TXT], "option")) ? 
                                   ks.txt_fields[VALUE_TXT] : ks.txt_fields[MENU_ELEMENT_TXT];
            break;
        case ICON_PATH_ENTRY:
            txt_field_to_compare = ks.txt_fields[ICON_PATH_TXT];
            break;
        case MENU_ID_ENTRY:
            txt_field_to_compare = ks.txt_fields[MENU_ID_TXT];
            break;
        case EXECUTE_ENTRY:
            txt_field_to_compare = ks.txt_fields[EXECUTE_TXT];
    }

    // Do not continue if the new value equals the old value.
    if (STREQ (entry_txt[changed_entry_field], txt_field_to_compare) || 
        (!(*entry_txt[changed_entry_field]) && !txt_field_to_compare)) {
        return;
    }

    if (ks.txt_fields[ELEMENT_VISIBILITY_TXT]) { // menu, pipe menu, item or separator
        if (!STREQ (ks.txt_fields[TYPE_TXT], "separator")) {
            if (!(*entry_txt[MENU_ELEMENT_OR_VALUE_ENTRY])) {
                empty_label_msg ();
                set_entry_fields ();
                return;
            }
            if (streq_any (ks.txt_fields[TYPE_TXT], "menu", "pipe menu", NULL)) {
                if (STREQ (ks.txt_fields[TYPE_TXT], "pipe menu") && !*entry_txt[EXECUTE_ENTRY]) {
                    show_errmsg (_("Pipe menus need a command to execute."));
                    set_entry_fields ();
                    return;
                }
                if (!STREQ (ks.txt_fields[MENU_ID_TXT], entry_txt[MENU_ID_ENTRY])) {
                    if (G_UNLIKELY (g_slist_find_custom (ks.menu_ids, entry_txt[MENU_ID_ENTRY], (GCompareFunc) strcmp))) {
                        show_errmsg (_("This menu ID already exists. Please choose another one."));
                        set_entry_fields ();
                        return;
                    }
                    remove_menu_id (ks.txt_fields[MENU_ID_TXT]);
                    ks.menu_ids = g_slist_prepend (ks.menu_ids, g_strdup (entry_txt[MENU_ID_ENTRY]));
                    gtk_tree_store_set (ks.treestore, &ks.iter, TS_MENU_ID, entry_txt[MENU_ID_ENTRY], -1);
                }

                if (STREQ (ks.txt_fields[TYPE_TXT], "pipe menu")) {
                    gtk_tree_store_set (ks.treestore, &ks.iter, TS_EXECUTE, entry_txt[EXECUTE_ENTRY], -1);
                }
            }
            if (ks.txt_fields[ICON_PATH_TXT] && !(*entry_txt[ICON_PATH_ENTRY])) {
                // FALSE = do not push to undo stack here; will be done later in this function.
                remove_icons_from_menus_or_items (GUINT_TO_POINTER (FALSE));
            }
            else if (*entry_txt[ICON_PATH_ENTRY] && !STREQ (entry_txt[ICON_PATH_ENTRY], ks.txt_fields[ICON_PATH_TXT])) {
                // TRUE = show error message on failure, FALSE = not during undo/redo.
                if (set_icon (&ks.iter, entry_txt[ICON_PATH_ENTRY], TRUE, FALSE)) {
                    gtk_style_context_remove_class (gtk_widget_get_style_context (ks.entry_fields[ICON_PATH_ENTRY]), 
                                                    "user_intervention_requested");
                    gtk_widget_set_sensitive (ks.remove_icon, TRUE);
                }
                else {
                    set_entry_fields ();
                    return;
                }
            }
        }
        gtk_tree_store_set (ks.treestore, &ks.iter, TS_MENU_ELEMENT, 
                            (*entry_txt[MENU_ELEMENT_OR_VALUE_ENTRY]) ? entry_txt[MENU_ELEMENT_OR_VALUE_ENTRY] : NULL, -1);
    }
    // Option: "Enabled" is never shown here because it is edited directly in the tree view.
    else {
        gtk_tree_store_set (ks.treestore, &ks.iter, TS_VALUE, entry_txt[MENU_ELEMENT_OR_VALUE_ENTRY], -1);
    }

    push_new_item_on_undo_stack (NOT_AFTER_SAVE); // First update undo stack so ...
    activate_change_done ();
    row_selected ();                              /* ... the toolbar buttons and 
                                                     application menu's undo/redo items get the correct sensitivity settings. */
}

/* 

     Applies changes from editable cells; prevents duplicate menu IDs.

*/

void cell_edited (G_GNUC_UNUSED       GtkCellRendererText *renderer, 
                                const gchar               *path, 
                                const gchar               *new_text, 
                                      gpointer             column_number_pointer)
{
    if (G_UNLIKELY (!gtk_tree_model_get_iter_from_string (ks.ts_model, &ks.iter, path))) {
        return;
    }

    const guint column_number = GPOINTER_TO_UINT (column_number_pointer);
          GtkTreeSelection *selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (ks.treeview));
    const guint number_of_selected_rows = gtk_tree_selection_count_selected_rows (selection);

    gboolean erased_separator_label = FALSE; // Default value.

    const guint8 treestore_pos = column_number + 4; // Columns start with TS_MENU_ELEMENT, which has index 4 inside the treestore.
    const guint8 txt_fields_pos = column_number + 1; // ICON_PATH_TXT is ommitted.

    if (G_UNLIKELY (number_of_selected_rows > 1)) { // This means that ks.txt_fields are currently not set.
	    /*
            It is simpler to call repopulate_txt_fields_array() than to manually free and reload 
            the current value. There always needs to be a free operation, because if two rows are 
            selected and a cell in them is edited more than once without deselecting, 
            cell_edited() may be called without row_selected() being invoked in between. 
            Without freeing first, this would cause gtk_tree_model_get() to be called twice in 
            succession, leading to a memory leak.
        */
        repopulate_txt_fields_array ();
    }

    if (G_UNLIKELY (STREQ (ks.txt_fields[txt_fields_pos], new_text))) { // New = Old.
        return;
    }

    if (column_number == COL_MENU_ELEMENT && !(*new_text)) {
        if (G_UNLIKELY (!STREQ (ks.txt_fields[TYPE_TXT], "separator"))) {
            empty_label_msg ();
            return;
        }
        erased_separator_label = TRUE;
    }
    else if (column_number == COL_MENU_ID) {
        if (G_UNLIKELY (g_slist_find_custom (ks.menu_ids, new_text, (GCompareFunc) strcmp))) {
            show_errmsg (_("This menu ID already exists. Please choose another one."));
            return;
        }
        remove_menu_id (ks.txt_fields[MENU_ID_TXT]);
        ks.menu_ids = g_slist_prepend (ks.menu_ids, g_strdup (new_text));
    }
    else if (G_UNLIKELY (column_number == COL_EXECUTE && !(*new_text))) {
        show_errmsg (_("Pipe menus need a command to execute."));
        return;
    }

    gtk_tree_store_set (ks.treestore, &ks.iter, treestore_pos, (!erased_separator_label) ? new_text : NULL, -1);
    if (G_LIKELY (number_of_selected_rows == 1)) {
        set_entry_fields ();
    }

    push_new_item_on_undo_stack (NOT_AFTER_SAVE); // First update undo stack so ...
    activate_change_done ();
    row_selected ();                              /* ... the toolbar buttons and 
                                                     application menu's undo/redo items get the correct sensitivity settings. */
}
                                                                        
/*

    Builds the entry list store for a given tree store column, ensuring unique entries only.

*/

static gboolean add_all_strs_of_tree_column_to_liststore (              GtkTreeModel *foreach_model, 
                                                          G_GNUC_UNUSED GtkTreePath  *foreach_path, 
                                                                        GtkTreeIter  *foreach_iter, 
                                                                        gpointer      treestore_pos_ptr)
{
    const guint8 treestore_pos = GPOINTER_TO_UINT (treestore_pos_ptr);
          g_autofree gchar *str;

    gtk_tree_model_get (foreach_model, foreach_iter, treestore_pos, &str, -1);

    if (!str) {
        return FALSE;
    }

    GtkTreeIter iter_loop;
    gboolean key_does_not_exist_yet_in_liststore = TRUE; // Default value.

    if (treestore_pos == TS_MENU_ELEMENT) {
        g_autofree gchar *type_txt;

        gtk_tree_model_get (foreach_model, foreach_iter, TS_TYPE, &type_txt, -1);

        /* Skip adding names of actions, options, and option blocks, 
           as they are inserted by the program, not the user. */
        if (streq_any (type_txt, "action", "option", "option block", NULL)) {
            return FALSE;
        }
    }
    else if (treestore_pos == TS_VALUE) {
        g_autofree gchar *menu_element_txt;

        gtk_tree_model_get (foreach_model, foreach_iter, TS_MENU_ELEMENT, &menu_element_txt, -1);

        // Show only matches for the same option type (e.g., only for "command" or only for "wmclass").
        if (!STREQ (menu_element_txt, ks.txt_fields[MENU_ELEMENT_TXT])) {
            return FALSE;
        }
    }

    if (gtk_tree_model_get_iter_first (ks.ls_model, &iter_loop)) {
        // Ensure each list store entry is unique.
        do {
            g_autofree gchar *str_loop;

            gtk_tree_model_get (ks.ls_model, &iter_loop, 0, &str_loop, -1);

            if (STREQ (str, str_loop)) {
                key_does_not_exist_yet_in_liststore = FALSE;
                break;
            }
        } while (gtk_tree_model_iter_next (ks.ls_model, &iter_loop));
    }

    if (key_does_not_exist_yet_in_liststore) {
        GtkTreeIter new_iter;

        if (gtk_tree_model_get_iter_first (ks.ls_model, &iter_loop)) { // At least one element in list store.
            GtkTreeIter iter_loop_next = iter_loop;

            if (!gtk_tree_model_iter_next (ks.ls_model, &iter_loop_next)) { // Only one element in list store.
                g_autofree gchar *str_loop;

                gtk_tree_model_get (ks.ls_model, &iter_loop, 0, &str_loop, -1);

                if (g_utf8_collate (str, str_loop) < 0) {
                    gtk_list_store_insert_before (ks.liststore, &new_iter, &iter_loop);
                }
                else {
                    gtk_list_store_insert_after (ks.liststore, &new_iter, &iter_loop);
                }
            }
            else { // At least two elements in list store.
                do {
                    g_autofree gchar *str_loop;

                    gtk_tree_model_get (ks.ls_model, &iter_loop, 0, &str_loop, -1);

                    if (g_utf8_collate (str, str_loop) < 0) {
                        gtk_list_store_insert_before (ks.liststore, &new_iter, &iter_loop);

                        break;
                    }
                    else {
                        GtkTreeIter iter_loop_next = iter_loop;

                        if (!gtk_tree_model_iter_next (ks.ls_model, &iter_loop_next)) {
                            gtk_list_store_insert_after (ks.liststore, &new_iter, &iter_loop);

                            break;
                        }
                    }
                } while (gtk_tree_model_iter_next (ks.ls_model, &iter_loop));
            }

            gtk_list_store_set (ks.liststore, &new_iter, 0, str, -1);
        }
        else { // No element in list store yet.
            gtk_list_store_insert_with_values (ks.liststore, &new_iter, -1, 0, str, -1);
        }
    }

    return FALSE;
}

/*

    Initializes building of the entry list store if the focus is on an entry field and the list store is currently empty.

*/

void build_entry_liststore (gpointer treestore_pos_ptr)
{
    const guint8 treestore_pos = GPOINTER_TO_UINT (treestore_pos_ptr);
          guint8 entry_field_idx;

    switch (treestore_pos) {
        case TS_MENU_ID:
            entry_field_idx = MENU_ID_ENTRY;
            break;
        case TS_EXECUTE:
            entry_field_idx = EXECUTE_ENTRY;
            break;
        case TS_ICON_PATH:
            entry_field_idx = ICON_PATH_ENTRY;
            break;
        default:
            entry_field_idx = MENU_ELEMENT_OR_VALUE_ENTRY;
    }

    if (!gtk_widget_has_focus (ks.entry_fields[entry_field_idx])) {
        return;
    }

    GtkTreeIter test_iter;

    if (gtk_tree_model_get_iter_first (ks.ls_model, &test_iter)) {
        return;
    }

    gtk_tree_model_foreach (ks.ts_model, (GtkTreeModelForeachFunc) add_all_strs_of_tree_column_to_liststore, treestore_pos_ptr);
}

/*

    Checks the current text in an entry field for a possible match in the tree store.

*/

gboolean entry_completion_func (                    GtkEntryCompletion *entry_completion, 
                                G_GNUC_UNUSED const gchar              *key, 
                                                    GtkTreeIter        *match_iter, 
                                G_GNUC_UNUSED       gpointer            user_data)
{
    g_autofree gchar *current_str;

    gtk_tree_model_get (GTK_TREE_MODEL (ks.liststore), match_iter, 0, &current_str, -1);

    if (!current_str) {
        return FALSE;
    }

    const gchar *key_entry = gtk_entry_get_text (GTK_ENTRY (gtk_entry_completion_get_entry (entry_completion)));

    return (g_regex_match_simple (key_entry, current_str, G_REGEX_CASELESS, G_REGEX_MATCH_ANCHORED));
}

/*

    Clears the entry list store if focus leaves the entry field.

*/

gboolean focus_no_longer_on_entry_field (G_GNUC_UNUSED gpointer user_data)
{
    GtkTreeIter test_iter;

    if (gtk_tree_model_get_iter_first (GTK_TREE_MODEL (ks.liststore), &test_iter)) {
        gtk_list_store_clear (ks.liststore);
    }

    // Also propagate to the default handler.
    return FALSE;
}

/* 

    Empty labels for menus, pipe menus, or items are blocked because they make the element invisible.

*/

static void empty_label_msg (void)
{
    const gchar *msg_substr; 

    if (STREQ (ks.txt_fields[TYPE_TXT], "menu")) {
        msg_substr = _("The creation of an empty label for this menu is blocked, since it would make the menu invisible.");
    }
    else if (STREQ (ks.txt_fields[TYPE_TXT], "pipe menu")) {
        msg_substr = _("The creation of an empty label for this pipe menu is blocked, since it would make the pipe menu invisible.");
    }
    else { // item
        msg_substr = _("The creation of an empty label for this item is blocked, since it would make the item invisible.");
    }

    g_autofree gchar *label_txt = g_strdup_printf ("<b>%s</b>\n\n%s", msg_substr, 
                                                   _("Menu files with invisible elements can be opened. "
                                                     "During the loading phase, the user can choose to either visualize or delete all these elements all at once. "
                                                     "If not addressed at this stage, they will be highlighted afterward."));

    show_message_dialog (_("Entry of Empty Label Blocked"),
                         "dialog-error",
                         label_txt,
                         // Translation note: the verb "Close".
                         _("_Close"));
}

/* 

    Toggles the value of boolean cells in the tree view.

*/

void boolean_toggled (void)
{
    gtk_tree_store_set (GTK_TREE_STORE (ks.ts_model), &ks.iter, TS_VALUE, 
                        (STREQ (ks.txt_fields[VALUE_TXT], "yes")) ? "no" : "yes", -1);

    push_new_item_on_undo_stack (NOT_AFTER_SAVE); // First update undo stack so ...
    activate_change_done ();
    row_selected ();                              /* ... the toolbar buttons and 
                                                     application menu's undo/redo items get the correct sensitivity settings. */
}
