/*
   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 "declarations_definitions_and_enumerations.h"
#include "context_menu.h"

// CM = context menu, indicating that the enums are only used here
enum { CM_RECURSIVELY, CM_IMMEDIATE, CM_COLLAPSE, CM_NUMBER_OF_EXPANSION_STATUS_CHANGES };

typedef struct {
    bool invalid_node_for_change_of_element_visibility_exists : 1;
    bool at_least_one_selected_node_has_no_children           : 1;
    bool at_least_one_selected_node_is_expanded               : 1;
} b_NodeStatuses;

static void add_startupnotify_or_execute_options_to_context_menu (      GtkWidget    *context_menu, 
                                                                  const gboolean      startupnotify_opts, 
                                                                        GtkTreeIter  *parent, 
                                                                  const guint8        number_of_opts, 
                                                                  const gchar       **options_array);
static void create_cm_headline (      GtkWidget *context_menu, 
                                const gchar     *headline_txt);
static void expand_or_collapse_selected_rows (const gpointer action_pointer);

/* 

    Creates a headline inside the context menu.

*/

static void create_cm_headline (      GtkWidget *context_menu, 
                                const gchar     *headline_txt)
{
    GtkWidget *headline_label = gtk_label_new (headline_txt);
    GtkWidget *menu_item = gtk_menu_item_new ();

    g_object_set (menu_item, "sensitive", FALSE, NULL);

    gtk_label_set_xalign (GTK_LABEL (headline_label), 0.0);
    gtk_style_context_add_class (gtk_widget_get_style_context (headline_label), "cm_class");
    gtk_css_provider_load_from_data (ks.cm_css_provider, ".cm_class { color: #ffffff; background-color: #66669b; }", -1, NULL);
    gtk_style_context_add_provider (gtk_widget_get_style_context (headline_label), 
                                    GTK_STYLE_PROVIDER (ks.cm_css_provider), GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
    gtk_container_add (GTK_CONTAINER (menu_item), headline_label);
    gtk_menu_shell_append (GTK_MENU_SHELL (context_menu), menu_item);
}

/* 

    Expands or collapses all selected rows according to the choice done.

*/

static void expand_or_collapse_selected_rows (const gpointer action_pointer)
{
    const guint8 action = GPOINTER_TO_UINT (action_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) {
        GtkTreePath *path_loop = selected_rows_loop->data;
        /*
            If the nodes are already expanded recursively and only the immediate children shall be expanded now, 
            it is the fastest way to collapse all nodes first.
        */
        gtk_tree_view_collapse_row (GTK_TREE_VIEW (ks.treeview), path_loop);
        if (action == CM_COLLAPSE) {
            gtk_tree_view_columns_autosize (GTK_TREE_VIEW (ks.treeview));
        }
        else {
            gtk_tree_view_expand_row (GTK_TREE_VIEW (ks.treeview), path_loop, action == CM_RECURSIVELY);
        }
    }

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

/* 

    Adds all currently unused startupnotify or Execute options to the context menu.

*/

static void add_startupnotify_or_execute_options_to_context_menu (      GtkWidget    *context_menu, 
                                                                  const gboolean      startupnotify_opts, 
                                                                        GtkTreeIter  *parent, 
                                                                  const guint8        number_of_opts, 
                                                                  const gchar       **options_array)
{
    const gchar *menu_item_txts_exe_opts[] = { // Translation note: Do not translate "Prompt". This is for a context menu.
	                                           N_("Add Prompt"), 
	                                           // Translation note: Do not translate "Command". This is for a context menu.
	                                           N_("Add Command"),
											   // Translation note: Do not translate "Startupnotify". This is for a context menu.
											   N_("Add Startupnotify") };
    const gchar *menu_item_txts_snotify_opts[] = { // Translation note: Do not translate "Enabled". This is for a context menu.
	                                               N_("Add Enabled"), 
	                                               // Translation note: Do not translate "Name". This is for a context menu.
	                                               N_("Add Name"),
												   // Translation note: Do not translate "WM_CLASS". This is for a context menu.
												   N_("Add WM_CLASS") }; // Without Icon, because of context used below.
    g_autofree gboolean *opts_exist = g_malloc0 (sizeof (gboolean) * number_of_opts); // Initialize all elements to FALSE.
    GtkWidget *menu_item;
    gboolean add_new_callback;

    check_for_existing_options (parent, number_of_opts, options_array, opts_exist);

    for (guint8 opts_cnt = 0; opts_cnt < number_of_opts; ++opts_cnt) {    
        if (!opts_exist[opts_cnt]) {
			// Translation note: Do not translate "Icon". This is for a context menu.
            menu_item = gtk_menu_item_new_with_label ((startupnotify_opts) ? ((opts_cnt < number_of_opts - 1) ? _(menu_item_txts_snotify_opts[opts_cnt]) : C_("Context Menu|Child of Startupnotify", "Add Icon")) : 
                                                      _(menu_item_txts_exe_opts[opts_cnt]));

            add_new_callback = startupnotify_opts || opts_cnt != STARTUPNOTIFY;
            g_signal_connect_swapped (menu_item, "activate", 
                                      G_CALLBACK ((add_new_callback) ? add_new : generate_items_for_action_option_combo_box), 
                                                  (add_new_callback) ? (gpointer) options_array[opts_cnt] : "Startupnotify");
            gtk_menu_shell_append (GTK_MENU_SHELL (context_menu), menu_item);
        }
    }
}

/* 

    Right click on the treeview presents a context menu for adding, changing, and deleting menu elements.

*/

void create_context_menu (GdkEventButton *event)
{
    GtkWidget *context_menu;
    GtkWidget *menu_item;

    GtkTreeSelection *selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (ks.treeview));
    /* Not const; if two or more rows had been selected before and another row not one of these has now been rightclicked, 
       number_of_selected_rows has to be reset. */
    gint number_of_selected_rows = gtk_tree_selection_count_selected_rows (selection);
#if GLIB_CHECK_VERSION(2,56,0)
    g_autolist(GtkTreePath) selected_rows = NULL;
#else
    GList *selected_rows = NULL;
#endif
    g_autoptr(GtkTreePath) path;

    GList *selected_rows_loop;
    GtkTreeIter iter_loop;

    // The last three arguments (column, cell_x and cell_y) aren't used.
    gtk_tree_view_get_path_at_pos (GTK_TREE_VIEW (ks.treeview), event->x, event->y, &path, NULL, NULL, NULL);

    // Don't show a context menu when the new menu element screen is open and there is a right click outside the tree view.
    if (!path && gtk_widget_get_visible (ks.enter_values_box)) {
        return;
    }

    context_menu = gtk_menu_new ();

    if (path) {
        gboolean selected_row_is_one_of_the_previously_selected_ones = FALSE; // Default

        if (number_of_selected_rows > 1) {
            /*
                If several rows are selected and a row that is not one of these is rightclicked, 
                select the latter and unselect the former ones.
            */
            selected_rows = gtk_tree_selection_get_selected_rows (selection, &ks.ts_model);
            FOREACH_IN_LIST (selected_rows, selected_rows_loop) {
                if (gtk_tree_path_compare (path, selected_rows_loop->data) == 0) {
                    selected_row_is_one_of_the_previously_selected_ones = TRUE;
                    break;
                }
            }
            if (!selected_row_is_one_of_the_previously_selected_ones) {
                g_list_free_full (selected_rows, (GDestroyNotify) gtk_tree_path_free);
            }
            else {
                gboolean at_least_one_row_has_no_icon = FALSE; // Default

                // Add context menu item for deletion of icons if all selected rows contain icons.
                FOREACH_IN_LIST (selected_rows, selected_rows_loop) {
                    g_autoptr(GdkPixbuf) icon_img_pixbuf_loop;
                
                    gtk_tree_model_get_iter (ks.ts_model, &iter_loop, selected_rows_loop->data);
                    gtk_tree_model_get (ks.ts_model, &iter_loop, TS_ICON_IMG, &icon_img_pixbuf_loop, -1);
                    if (!icon_img_pixbuf_loop) {
                        at_least_one_row_has_no_icon = TRUE;

                        break;
                    }
                }
                if (!at_least_one_row_has_no_icon) {
                    menu_item = gtk_menu_item_new_with_label (_("Remove Icons"));
                    g_signal_connect (menu_item, "activate", G_CALLBACK (remove_icons_from_menus_or_items), GUINT_TO_POINTER (TRUE));
                    gtk_menu_shell_append (GTK_MENU_SHELL (context_menu), menu_item);
                    gtk_menu_shell_append (GTK_MENU_SHELL (context_menu), gtk_separator_menu_item_new ());
                }
            }
        }

        /*
            Either no or one row had been selected before or 
            two or more rows had been selected before and another row not one of these has now been rightclicked.
        */
        if (number_of_selected_rows < 2 || !selected_row_is_one_of_the_previously_selected_ones) {
            gboolean not_all_options_of_selected_action_set = FALSE; // Default

            gtk_tree_selection_unselect_all (selection);
            gtk_tree_selection_select_path (selection, path);
            selected_rows = gtk_tree_selection_get_selected_rows (selection, &ks.ts_model);
            number_of_selected_rows = 1; // If there had not been a selection before.

            // Icon
            if (streq_any (ks.txt_fields[TYPE_TXT], "menu", "pipe menu", "item", NULL)) {
                if (STREQ (ks.txt_fields[TYPE_TXT], "item")) {
                    create_cm_headline (context_menu, _(" Icon")); // Items have "Icon" and "Action" as context menu headlines.
                }

                menu_item = gtk_menu_item_new_with_label ((ks.txt_fields[ICON_PATH_TXT]) ? _("Change Icon") : 
                                                                                           // Translation note: This is for a context menu
                                                                                           C_("Context Menu|Menu or Item", "Add Icon"));
                g_signal_connect (menu_item, "activate", G_CALLBACK (icon_choosing_by_button_or_context_menu), NULL);
                gtk_menu_shell_append (GTK_MENU_SHELL (context_menu), menu_item);
                if (ks.txt_fields[ICON_PATH_TXT]) {
                    menu_item = gtk_menu_item_new_with_label (_("Remove Icon"));
                    g_signal_connect (menu_item, "activate", G_CALLBACK (remove_icons_from_menus_or_items), GUINT_TO_POINTER (TRUE));
                    gtk_menu_shell_append (GTK_MENU_SHELL (context_menu), menu_item);
                }
                if (!STREQ (ks.txt_fields[TYPE_TXT], "item")) { // Items have "Icon" and "Actions" as context menu headlines.
                    gtk_menu_shell_append (GTK_MENU_SHELL (context_menu), gtk_separator_menu_item_new ());
                }
            }

            if (gtk_tree_path_get_depth (path) > 1) {
                GtkTreeIter parent;

                // Startupnotify options
                if (streq_any (ks.txt_fields[TYPE_TXT], "option", "option block", NULL) && 
                    streq_any (ks.txt_fields[MENU_ELEMENT_TXT], "startupnotify", "enabled", "name", "wmclass", "icon", NULL)) {
                    if (!STREQ (ks.txt_fields[MENU_ELEMENT_TXT], "startupnotify")) {
                        gtk_tree_model_iter_parent (ks.ts_model, &parent, &ks.iter);
                    }
                    else {
                        parent = ks.iter;
                    }
                    if (gtk_tree_model_iter_n_children (ks.ts_model, &parent) < NUMBER_OF_STARTUPNOTIFY_OPTS) {
                        GtkTreeIter grandparent;
                        gtk_tree_model_iter_parent (ks.ts_model, &grandparent, &parent);
                        const gint number_of_chldn_of_gp = gtk_tree_model_iter_n_children (ks.ts_model, &grandparent);

                        if (STREQ (ks.txt_fields[TYPE_TXT], "option block") && number_of_chldn_of_gp < NUMBER_OF_EXECUTE_OPTS) {
							// Translation note: Do not translate "Startupnotify"
                            gchar *headline_txt = (gtk_tree_model_iter_n_children (ks.ts_model, &parent) == NUMBER_OF_STARTUPNOTIFY_OPTS - 1) ? _(" Startupnotify Option") : 
                                                                                                                                                // Translation note: Do not translate "Startupnotify"
                                                                                                                                                // 2-4 options are possible.
                                                                                                                                                _(" Startupnotify Options"); 

                            create_cm_headline (context_menu, headline_txt);
                        }

                        add_startupnotify_or_execute_options_to_context_menu (context_menu, TRUE, &parent, 
                                                                              NUMBER_OF_STARTUPNOTIFY_OPTS,
                                                                              ks.startupnotify_options);

                        // No "Execute option(s)" headline following.
                        if (number_of_chldn_of_gp == NUMBER_OF_EXECUTE_OPTS || STREQ (ks.txt_fields[TYPE_TXT], "option")) {
                            gtk_menu_shell_append (GTK_MENU_SHELL (context_menu), gtk_separator_menu_item_new ());
                        }
                    }
                }

                g_autofree gchar *menu_element_txt_parent;

                gtk_tree_model_iter_parent (ks.ts_model, &parent, &ks.iter);
                gtk_tree_model_get (ks.ts_model, &parent, TS_MENU_ELEMENT, &menu_element_txt_parent, -1);

                // Execute options
                if ((STREQ (ks.txt_fields[TYPE_TXT], "action") && STREQ (ks.txt_fields[MENU_ELEMENT_TXT], "Execute")) || 
                    (streq_any (ks.txt_fields[TYPE_TXT], "option", "option block", NULL) && 
                    STREQ (menu_element_txt_parent, "Execute"))) {
                    if (!STREQ (ks.txt_fields[MENU_ELEMENT_TXT], "Execute")) {
                        gtk_tree_model_iter_parent (ks.ts_model, &parent, &ks.iter);
                    }
                    else {
                        parent = ks.iter;
                    }
                    if (gtk_tree_model_iter_n_children (ks.ts_model, &parent) < NUMBER_OF_EXECUTE_OPTS) {
                        if (STREQ (ks.txt_fields[TYPE_TXT], "action") || 
                            (STREQ (ks.txt_fields[TYPE_TXT], "option block") && 
                            gtk_tree_model_iter_n_children (ks.ts_model, &ks.iter) < NUMBER_OF_STARTUPNOTIFY_OPTS)) {
							// Translation note: Do not translate "Execute"
                            gchar *headline_txt = (gtk_tree_model_iter_n_children (ks.ts_model, &parent) == NUMBER_OF_EXECUTE_OPTS - 1) ? _(" Execute Option") : 
                                                                                                                                          // Translation note: Do not translate "Execute"
                                                                                                                                          // 2-5 options are possible.
                                                                                                                                          _(" Execute Options");

                            create_cm_headline (context_menu, headline_txt);
                        }

                        add_startupnotify_or_execute_options_to_context_menu (context_menu, FALSE, &parent, 
                                                                              NUMBER_OF_EXECUTE_OPTS, ks.execute_options);

                        if (STREQ (ks.txt_fields[TYPE_TXT], "action")) {
                            not_all_options_of_selected_action_set = TRUE;
                        }
                        else { // If "Execute" is selected, the headline for the actions follows.
                            gtk_menu_shell_append (GTK_MENU_SHELL (context_menu), gtk_separator_menu_item_new ());
                        }
                    }
                }

                // Option of "Exit"/"SessionLogout" (prompt) or "Restart" action (command)
                if (STREQ (ks.txt_fields[TYPE_TXT], "action") && 
                    streq_any (ks.txt_fields[MENU_ELEMENT_TXT], "Exit", "SessionLogout", "Restart", NULL) && 
                    !gtk_tree_model_iter_has_child (ks.ts_model, &ks.iter)) {
                    const gboolean restart = STREQ (ks.txt_fields[MENU_ELEMENT_TXT], "Restart");
					// Translation note: Do not translate "Restart"
                    create_cm_headline (context_menu, (restart) ? _(" Restart Option") : 
                                        (STREQ (ks.txt_fields[MENU_ELEMENT_TXT], "Exit") ? 
										// Translation note: Do not translate "Exit"
                                        _(" Exit Option") : 
										// Translation note: Do not translate "SessionLogout"
										_(" SessionLogout Option")));
										                                  
                    menu_item = gtk_menu_item_new_with_label ((restart) ? 
					                                         // Translation note: Do not translate "Command". This is for a context menu.
					                                         _("Add Command") : 
					                                         // Translation note: Do not translate "Prompt". This is for a context menu.
					                                         _("Add Prompt"));
                    g_signal_connect_swapped (menu_item, "activate", G_CALLBACK (add_new), (restart) ? "command" : "prompt");
                    gtk_menu_shell_append (GTK_MENU_SHELL (context_menu), menu_item);

                    not_all_options_of_selected_action_set = TRUE;
                }
            }

            // Actions
            if (streq_any (ks.txt_fields[TYPE_TXT], "item", "action", NULL)) {
                if (STREQ (ks.txt_fields[TYPE_TXT], "item") || not_all_options_of_selected_action_set) {
                    create_cm_headline (context_menu, _(" Actions"));
                }

                for (guint8 actions_cnt = 0; actions_cnt < NUMBER_OF_ACTIONS; ++actions_cnt) {
                    const gchar *menu_item_txts[] = { // Translation note: Do not translate "Execute". This is for a context menu.
					                                  N_("Add Execute"), 
					                                  // Translation note: Do not translate "Exit". This is for a context menu.
					                                  N_("Add Exit"), 
													  // Translation note: Do not translate "Reconfigure". This is for a context menu.
													  N_("Add Reconfigure"), 
													  // Translation note: Do not translate "Restart". This is for a context menu.
													  N_("Add Restart"), 
													  // Translation note: Do not translate "SessionLogout". This is for a context menu.
													  N_("Add SessionLogout") };

                    menu_item = gtk_menu_item_new_with_label (_(menu_item_txts[actions_cnt]));

                    g_signal_connect_swapped (menu_item, "activate", 
                                              G_CALLBACK ((actions_cnt == RECONFIGURE) ? 
                                              action_option_insert : generate_items_for_action_option_combo_box), 
                                              (actions_cnt == RECONFIGURE) ? "context menu" : (gpointer) ks.actions[actions_cnt]);

                    gtk_menu_shell_append (GTK_MENU_SHELL (context_menu), menu_item);
                }

                gtk_menu_shell_append (GTK_MENU_SHELL (context_menu), gtk_separator_menu_item_new ());
            }
        }
    }

    // Basic menu elements

    /*
        menu, pipe menu, item & separator -> if (txt_fields[ELEMENT_VISIBILITY_TXT]) == TRUE
        If number of rows is > 1, then txt_fields[ELEMENT_VISIBILITY_TXT] == NULL.
    */
    if (!path || ks.txt_fields[ELEMENT_VISIBILITY_TXT]) {
        const gchar *basic_menu_elements[] = { "menu", "pipe menu", "item", "separator" };
        const gchar *add_basic_menu_elements_for_cm[] = { // Translation note: This is for a context menu.
		                                                  N_("Add Menu"), 
														  // Translation note: This is for a context menu.
														  N_("Add Pipe Menu"), 
														  // Translation note: This is for a context menu.
														  N_("Add Item"), 
														  // Translation note: This is for a context menu.
														  N_("Add Separator") };
        const gchar *basic_menu_elements_for_cm[] = { N_("Menu"), N_("Pipe Menu"), N_("Item"), N_("Separator") };
        const gchar *items[] = { "item+Execute", "item+Exit", "item+Reconfigure", "item+Restart", "item+SessionLogout" };
        enum { MENU, PIPE_MENU, ITEM, SEPARATOR };
        const guint8 number_of_basic_menu_elements = G_N_ELEMENTS (basic_menu_elements);

        GtkWidget *item_submenu = gtk_menu_new ();
        GtkWidget *submenu_item;

        if (!path) {
            gtk_tree_selection_unselect_all (selection);
            // Translation note: "Top Level" refers to the top level of a tree view in a GUI. This is for a context menu.
            create_cm_headline (context_menu, _(" Add at Top Level"));
        }

        for (guint8 basic_menu_elements_cnt = 0; basic_menu_elements_cnt < number_of_basic_menu_elements; ++basic_menu_elements_cnt) {
            const gchar *menu_item_txt = (path) ? _(add_basic_menu_elements_for_cm[basic_menu_elements_cnt]) : 
                                                  _(basic_menu_elements_for_cm[basic_menu_elements_cnt]);

            menu_item = gtk_menu_item_new_with_label (menu_item_txt);

            gtk_menu_shell_append (GTK_MENU_SHELL (context_menu), menu_item);

            if (basic_menu_elements_cnt != ITEM) {
                g_signal_connect_swapped (menu_item, "activate", G_CALLBACK (add_new), 
                                          (gpointer) basic_menu_elements[basic_menu_elements_cnt]);
            }
            else {
                for (guint8 submenu_elements_cnt = 0; submenu_elements_cnt < NUMBER_OF_ACTIONS; ++submenu_elements_cnt) {
                    g_autofree gchar *menu_item_txt = g_strconcat ("+ ", ks.actions[submenu_elements_cnt], NULL);

                    submenu_item = gtk_menu_item_new_with_label (menu_item_txt);

                    gtk_menu_shell_append (GTK_MENU_SHELL (item_submenu), submenu_item);

                    g_signal_connect_swapped (submenu_item, "activate", G_CALLBACK (add_new), (gpointer) items[submenu_elements_cnt]);
                }

                submenu_item = gtk_menu_item_new_with_label (_("No Action"));
                gtk_menu_shell_append (GTK_MENU_SHELL (item_submenu), submenu_item);
                g_signal_connect_swapped (submenu_item, "activate", G_CALLBACK (add_new), "item w/o action");

                gtk_menu_item_set_submenu (GTK_MENU_ITEM (menu_item), item_submenu);
            }
        }
        if (path) {
            gtk_menu_shell_append (GTK_MENU_SHELL (context_menu), gtk_separator_menu_item_new ());
        }
        
        gtk_menu_set_reserve_toggle_size (GTK_MENU (item_submenu), FALSE);        
    }

    if (path) {
        // Defaults
        b_NodeStatuses      node_statuses                  = { FALSE };
        b_ExpansionStatuses expansion_statuses_of_subnodes = { FALSE };
        // The address of this variable is sent to a funtion, so it can't be part of a bit field.
        gboolean at_least_one_descendant_is_invisible = FALSE;

        GtkTreePath *path_loop;

        // Remove
        menu_item = gtk_menu_item_new_with_label (_("Remove"));
        gtk_menu_shell_append (GTK_MENU_SHELL (context_menu), menu_item);
        g_signal_connect_swapped (menu_item, "activate", G_CALLBACK (remove_rows), "context menu");

        FOREACH_IN_LIST (selected_rows, selected_rows_loop) {
            path_loop = selected_rows_loop->data;
            gtk_tree_model_get_iter (ks.ts_model, &iter_loop, path_loop);

            if (!gtk_tree_model_iter_has_child (ks.ts_model, &iter_loop)) {
                node_statuses.at_least_one_selected_node_has_no_children = TRUE;
                break;
            }
            else {
                g_autoptr(GtkTreeModel) filter_model = gtk_tree_model_filter_new (ks.ts_model, path_loop);

                gtk_tree_model_foreach (filter_model, (GtkTreeModelForeachFunc) check_expansion_statuses_of_nodes, 
                                        &expansion_statuses_of_subnodes);

                if (gtk_tree_view_row_expanded (GTK_TREE_VIEW (ks.treeview), path_loop)) {
                    node_statuses.at_least_one_selected_node_is_expanded = TRUE;
                }
            }
        }

        if (!node_statuses.at_least_one_selected_node_has_no_children) {
            // Remove all children
            menu_item = gtk_menu_item_new_with_label (_("Remove All Children"));
            g_signal_connect (menu_item, "activate", G_CALLBACK (remove_all_children), NULL);
            gtk_menu_shell_append (GTK_MENU_SHELL (context_menu), menu_item);

            gtk_menu_shell_append (GTK_MENU_SHELL (context_menu), gtk_separator_menu_item_new ());

            // Expand or collapse node(s)
            for (guint8 exp_status_changes_cnt = 0; exp_status_changes_cnt < CM_NUMBER_OF_EXPANSION_STATUS_CHANGES; ++exp_status_changes_cnt) {
                if ((exp_status_changes_cnt == CM_RECURSIVELY && expansion_statuses_of_subnodes.at_least_one_is_collapsed) || 
                    (exp_status_changes_cnt == CM_IMMEDIATE && 
                     (!node_statuses.at_least_one_selected_node_is_expanded || 
                     expansion_statuses_of_subnodes.at_least_one_imd_ch_is_expanded)) || 
                    (exp_status_changes_cnt == CM_COLLAPSE && node_statuses.at_least_one_selected_node_is_expanded)) {
                    g_autofree gchar *menu_item_txt = g_strdup_printf (
                                                                       (exp_status_changes_cnt == CM_RECURSIVELY) ? ( (number_of_selected_rows == 1) ? _("Expand Row Recursively") : _("Expand Rows Recursively") ) : 
                                                                       (
                                                                        // Translation note: "Imd. Ch. Only" means "immediate children only"
                                                                        (exp_status_changes_cnt == CM_IMMEDIATE) ? ( (number_of_selected_rows == 1) ? _("Expand Row (Imd. Ch. Only)") : 
                                                                        // Translation note: "Imd. Ch. Only" means "immediate children only"
                                                                                                                                                      _("Expand Rows (Imd. Ch. Only)") ) : 
                                                                                                                    (number_of_selected_rows == 1) ? _("Collapse Row") : _("Collapse Rows")
                                                                       )
                                                                      );
                    menu_item = gtk_menu_item_new_with_label (menu_item_txt);

                    g_signal_connect_swapped (menu_item, "activate", G_CALLBACK (expand_or_collapse_selected_rows), 
                                              GUINT_TO_POINTER ((guint) exp_status_changes_cnt));
                    gtk_menu_shell_append (GTK_MENU_SHELL (context_menu), menu_item);
                }
            }
        }

        // Visualization of invisible menus, pipe menus, items, and separators.
        FOREACH_IN_LIST (selected_rows, selected_rows_loop) {
            g_autofree gchar *element_visibility_txt_loop;

            path_loop = selected_rows_loop->data;
            gtk_tree_model_get_iter (ks.ts_model, &iter_loop, path_loop);
            gtk_tree_model_get (ks.ts_model, &iter_loop, TS_ELEMENT_VISIBILITY, &element_visibility_txt_loop, -1);

            if (G_LIKELY (!element_visibility_txt_loop || STREQ (element_visibility_txt_loop, "visible"))) {
                node_statuses.invalid_node_for_change_of_element_visibility_exists = TRUE;

                break;
            }
            else if (gtk_tree_model_iter_has_child (ks.ts_model, &iter_loop)) {
                g_autoptr(GtkTreeModel) filter_model = gtk_tree_model_filter_new (ks.ts_model, path_loop);

                gtk_tree_model_foreach (filter_model, (GtkTreeModelForeachFunc) check_if_invisible_descendant_exists, 
                                        &at_least_one_descendant_is_invisible);
            }
        }

        if (G_UNLIKELY (!node_statuses.invalid_node_for_change_of_element_visibility_exists)) {
            enum { VISUALIZE, VISUALIZE_CM_RECURSIVELY };

            gtk_menu_shell_append (GTK_MENU_SHELL (context_menu), gtk_separator_menu_item_new ());

            for (guint8 visualization_cnt = VISUALIZE; visualization_cnt <= VISUALIZE_CM_RECURSIVELY; ++visualization_cnt) {
                if (!(visualization_cnt == VISUALIZE_CM_RECURSIVELY && !at_least_one_descendant_is_invisible)) {
                    menu_item = gtk_menu_item_new_with_label ((visualization_cnt == VISUALIZE) ? _("Visualize") : _("Visualize Recursively"));
                    g_signal_connect_swapped (menu_item, "activate", G_CALLBACK (visualize_menus_items_and_separators), 
                                              GUINT_TO_POINTER ((guint) visualization_cnt)); // recursively = 1 = TRUE.
                    gtk_menu_shell_append (GTK_MENU_SHELL (context_menu), menu_item);
                }
            }
        }
    }

    gtk_menu_set_reserve_toggle_size (GTK_MENU (context_menu), FALSE); // Hide space reserved for toggle symbols as this is not needed inside the context menu.

    gtk_widget_show_all (context_menu);

#if GTK_CHECK_VERSION(3,22,0)
    gtk_menu_popup_at_pointer (GTK_MENU (context_menu), NULL);
#else
    /*
        Arguments two to five 
        (parent_menu_shell, parent_menu_item, user defined positioning function and user data for that function)
        are not used.
    */
    gtk_menu_popup (GTK_MENU (context_menu), NULL, NULL, NULL, NULL, event->button, gdk_event_get_time ((GdkEvent*) event));
#endif

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