/*
   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> // Sometimes this might have to be included explicitly.

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

// ED = editing, indicating that the enums are only used here
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 initiates the sorting of Execute or startupnotify options, then selects the 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 according to the 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

        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;
                    }
                }
            }
        }
    }
}

/* 

    This function is run after autosorting of options has been activated.
    All Execute and startupnotify options of the treestore are sorted by it.

*/

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 selection up or down, to the top or 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 element visibility of menus, pipe menus, items, and separators 
    that are dependent on the menu element that has been 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;

    /*
        Makes the following conditional statement more readable.
        If the selected path is on toplevel, 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 on toplevel = 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);

    /*
        Current row is an ancestor of the selected row or the selected row itself.

        A menu element can only be visible if its ancestors are visible as well, 
        so if a visualization is done for a (pipe) menu, item or separator, the visibility statuses for all ancestors are 
        set to "visible". 
        To keep the code simple there is no check if the visibility status is already set to "visible", it is done anyway. 
        Visualized menus and items get a "dummy" label, whilst separators don't, because they are always visible if they
        are not descendants of an invisible menu, whether they have a label or not.
    */
    if (filter_selected_path && // Selected path is not on toplevel.
        (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 not an ascendant of the selected row/the selected row itself.
    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;
}

/* 

    Changes the status of one or more menus, pipe menus, items, and separators to 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 is limited to display only image files.

*/

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));
}

/* 

    Dialog for choosing an icon for a (pipe) menu or item.

    The returned filename is a newly allocated string that should 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

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

    gtk_file_filter_set_name (open_image_file_filter, _("Image Files"));
    // The last two arguments (data and function to call to free data) aren't used.
    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 (s)he clicked on the icon chooser button or
    (s)he chose 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 = Display error message when error occurs, FALSE = Not during undo/redo
    set_icon (&ks.iter, icon_path, TRUE, FALSE);

    // 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. */
    }
}

/* 

    Adds an icon, if possible, at a position passed to this function.

*/

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 has been chosen during the addition of a new (pipe) menu or item; it is stored 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 function might have been called from the timeout function, with currently no selection done at that time.
    if (!during_undo_redo && gtk_tree_selection_count_selected_rows (selection)) {
        repopulate_txt_fields_array ();
        set_entry_fields ();
    }

    return TRUE;
}

/* 

    Removes icons from 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 (); // There is no need to change the status of any application menu item or toolbar button.
        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 at least one of the entry fields has been altered.

*/

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 = don't push a new item on the undo stack, as it is done anyway at the end of 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 = display error message when error occurs, 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, since it is edited directly inside the treeview.
    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. */
}

/* 

    Adopts changes from editable cells, in case it's a menu ID prevents a duplicate.

*/

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

    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 free the current txt_field and read it (again) 
            with gtk_tree_model_get (). There always has to be a freeing, since if two rows are currently being selected 
            and a cell of these is changed more than once without unselecting these cells, cell_edited () is called without 
            calling row_selected () in between, which would without freeing cause gtk_tree_model_get () to get called twice 
            in a row, thus causing 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 liststore for one tree store column, allowing only unique entries.

*/

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

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

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

        /* If the currently activated entry field refers to a label, do not add the names of actions, 
           options, and option blocks to the entry completion list, as an addition of these names to the 
           menu is done by the program and not a user. Actions, options, and option blocks are not labels,  
           so they remain invisible and are thus different from (pipe) menus, items, and separators.
           Handled like this, the texts "Execute", "Exit", "Reconfigure", "Restart", and "SessionLogout"
           will be added if they do not appear in the context of an action. The same goes for the options 
           "command", "prompt", "startupnotify", "enabled", "name", icon", and "wmclass". */
        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 from the same option type, e.g. only matches for command, for wmclass, etc.
        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 that each text in the list store 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 the building of the entry list store, if the focus is on an entry field and the list store does not already exist.

*/

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 inside 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));
}

/*

    Checks if the focus is no longer on an entry field, so that the entry list store can be cleared since it is no longer needed.

*/

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);
    }

    // Propagate also default handler
    return FALSE;
}

/* 

    Empty labels for menus, pipe menus or items are blocked, because they would make these elements 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 value of toggle buttons in the treeview.

*/

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. */
}
