Blob Blame History Raw
/*
  This file is part of Dconf Editor

  Dconf Editor 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 3 of the License, or
  (at your option) any later version.

  Dconf Editor 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 Dconf Editor.  If not, see <https://www.gnu.org/licenses/>.
*/

using Gtk;

const int MAX_ROW_WIDTH = 1000;

private class ListBoxRowWrapper : ListBoxRow
{
    public override void get_preferred_width (out int minimum_width, out int natural_width)
    {
        base.get_preferred_width (out minimum_width, out natural_width);
        natural_width = MAX_ROW_WIDTH;
    }
}

private class RegistryWarning : Grid
{
    public override void get_preferred_width (out int minimum_width, out int natural_width)
    {
        base.get_preferred_width (out minimum_width, out natural_width);
        natural_width = MAX_ROW_WIDTH;
    }
}

private class ListBoxRowHeader : Grid
{
    public override void get_preferred_width (out int minimum_width, out int natural_width)
    {
        base.get_preferred_width (out minimum_width, out natural_width);
        natural_width = MAX_ROW_WIDTH;
    }

    public ListBoxRowHeader (bool is_first_row, string? header_text)
    {
        if (header_text == null)
        {
            if (is_first_row)
                return;
        }
        else
        {
            orientation = Orientation.VERTICAL;

            Label label = new Label ((!) header_text);
            label.visible = true;
            label.halign = Align.START;
            StyleContext context = label.get_style_context ();
            context.add_class ("dim-label");
            context.add_class ("header-label");
            add (label);
        }

        halign = Align.CENTER;

        Separator separator = new Separator (Orientation.HORIZONTAL);
        separator.visible = true;
        separator.hexpand = true;
        add (separator);
    }
}

private abstract class ClickableListBoxRow : EventBox
{
    public signal void on_popover_disappear ();

    public abstract string get_text ();
    protected Variant get_text_variant () { return new Variant.string (get_text ()); }

    public bool search_result_mode { protected get; construct; default = false; }

    /*\
    * * Dismiss popover on window resize
    \*/

    private int width;

    construct
    {
        size_allocate.connect (on_size_allocate);
    }

    private void on_size_allocate (Allocation allocation)
    {
        if (allocation.width == width)
            return;
        hide_right_click_popover ();
        width = allocation.width;
    }

    /*\
    * * right click popover stuff
    \*/

    private ContextPopover? nullable_popover = null;
    protected virtual bool generate_popover (ContextPopover popover) { return false; }      // no popover should be created

    public void destroy_popover ()
    {
        if (nullable_popover != null)       // check sometimes not useful
            ((!) nullable_popover).destroy ();
    }

    public void hide_right_click_popover ()
    {
        if (nullable_popover != null)
            ((!) nullable_popover).popdown ();
    }

    public bool right_click_popover_visible ()
    {
        return (nullable_popover != null) && (((!) nullable_popover).visible);
    }

    public void show_right_click_popover (int event_x = (int) (get_allocated_width () / 2.0))
    {
        if (nullable_popover == null)
        {
            nullable_popover = new ContextPopover ();
            if (!generate_popover ((!) nullable_popover))
            {
                ((!) nullable_popover).destroy ();  // TODO better, again
                nullable_popover = null;
                return;
            }

            ((!) nullable_popover).closed.connect (() => on_popover_disappear ());
            ((!) nullable_popover).destroy.connect (() => {
                    on_popover_disappear ();
                    nullable_popover = null;
                });

            ((!) nullable_popover).set_relative_to (this);
            ((!) nullable_popover).position = PositionType.BOTTOM;     // TODO better
        }
        else if (((!) nullable_popover).visible)
            warning ("show_right_click_popover() called but popover is visible");   // TODO is called on multi-right-click or long Menu key press

        Gdk.Rectangle rect = { x:event_x, y:get_allocated_height (), width:0, height:0 };
        ((!) nullable_popover).set_pointing_to (rect);
        ((!) nullable_popover).popup ();
    }
}

[GtkTemplate (ui = "/ca/desrt/dconf-editor/ui/folder-list-box-row.ui")]
private class FolderListBoxRow : ClickableListBoxRow
{
    [GtkChild] private Label folder_name_label;
    public string full_name;
    private string parent_path;

    public FolderListBoxRow (string label, string path, string _parent_path, bool search_result_mode = false)
    {
        Object (search_result_mode: search_result_mode);
        folder_name_label.set_text (search_result_mode ? path : label);
        full_name = path;
        parent_path = _parent_path;
    }

    public override string get_text ()
    {
        return full_name;
    }

    protected override bool generate_popover (ContextPopover popover)  // TODO better
    {
        Variant variant = new Variant.string (full_name);

        if (search_result_mode)
        {
            popover.new_gaction ("open_parent", "ui.open-parent(" + variant.print (false) + ")");
            popover.new_section ();
        }

        popover.new_gaction ("open", "ui.open-folder(" + variant.print (false) + ")");
        popover.new_gaction ("copy", "app.copy(" + get_text_variant ().print (false) + ")");

        popover.new_section ();
        popover.new_gaction ("recursivereset", "ui.reset-recursive(" + variant.print (false) + ")");

        return true;
    }
}

[GtkTemplate (ui = "/ca/desrt/dconf-editor/ui/key-list-box-row.ui")]
private abstract class KeyListBoxRow : ClickableListBoxRow
{
    [GtkChild] private Grid key_name_and_value_grid;
    [GtkChild] private Label key_name_label;
    [GtkChild] protected Label key_value_label;
    [GtkChild] protected Label key_info_label;
    protected Switch? boolean_switch = null;

    public bool small_keys_list_rows
    {
        set
        {
            if (value)
            {
                key_value_label.set_lines (2);
                key_info_label.set_lines (1);
            }
            else
            {
                key_value_label.set_lines (3);
                key_info_label.set_lines (2);
            }
        }
    }

    public ModificationsHandler modifications_handler { protected get; construct; }

    construct
    {
        if (abstract_key.type_string == "b" && !modifications_handler.get_current_delay_mode ())
        {
            boolean_switch = new Switch ();
            ((!) boolean_switch).can_focus = false;
            ((!) boolean_switch).valign = Align.CENTER;
            ((!) boolean_switch).show ();
            key_value_label.hide ();
            key_name_and_value_grid.attach ((!) boolean_switch, 1, 0, 1, 2);
        }

        update ();
        key_name_label.set_label (search_result_mode ? abstract_key.full_name : abstract_key.name);

        ulong key_value_changed_handler = abstract_key.value_changed.connect (() => {
                update ();
                destroy_popover ();
            });
        destroy.connect (() => {
                abstract_key.disconnect (key_value_changed_handler);
            });
    }
    private abstract Key abstract_key { get; }
    protected abstract void update ();

    public void toggle_boolean_key ()
    {
        if (boolean_switch == null)
            return;
        ((!) boolean_switch).activate ();
    }

    public void set_delayed_icon ()
    {
        SettingsModel model = modifications_handler.model;
        Key key = abstract_key;
        StyleContext context = get_style_context ();
        if (modifications_handler.key_has_planned_change (key.full_name))
        {
            context.add_class ("delayed");
            if (key is DConfKey)
            {
                if (modifications_handler.get_key_planned_value (key.full_name) == null)
                    context.add_class ("erase");
                else
                    context.remove_class ("erase");
            }
        }
        else
        {
            context.remove_class ("delayed");
            if (key is DConfKey && model.is_key_ghost ((DConfKey) key))
                context.add_class ("erase");
            else
                context.remove_class ("erase");
        }
    }

    protected void change_dismissed ()
    {
        ModelButton actionable = new ModelButton ();
        actionable.visible = false;
        Variant variant = new Variant.string (abstract_key.full_name);
        actionable.set_detailed_action_name ("ui.dismiss-change(" + variant.print (false) + ")");
        ((Container) get_child ()).add (actionable);
        actionable.clicked ();
        ((Container) get_child ()).remove (actionable);
        actionable.destroy ();
    }

    public void on_delete_call ()
    {
        set_key_value ((abstract_key is GSettingsKey), null);
    }

    protected void set_key_value (bool has_schema, Variant? new_value)
    {
        ModelButton actionable = new ModelButton ();
        actionable.visible = false;
        Variant variant;
        if (new_value == null)
        {
            if (has_schema)
            {
                variant = new Variant ("(ss)", abstract_key.full_name, ((GSettingsKey) abstract_key).schema_id);
                actionable.set_detailed_action_name ("bro.set-to-default(" + variant.print (false) + ")");
            }
            else
            {
                variant = new Variant.string (abstract_key.full_name);
                actionable.set_detailed_action_name ("ui.erase(" + variant.print (false) + ")");
            }
        }
        else
        {
            variant = new Variant ("(ssv)", abstract_key.full_name, (has_schema ? ((GSettingsKey) abstract_key).schema_id : ".dconf"), (!) new_value);
            actionable.set_detailed_action_name ("bro.set-key-value(" + variant.print (false) + ")");
        }
        ((Container) get_child ()).add (actionable);
        actionable.clicked ();
        ((Container) get_child ()).remove (actionable);
        actionable.destroy ();
    }
}

private class KeyListBoxRowEditableNoSchema : KeyListBoxRow
{
    public DConfKey key { get; construct; }
    private override Key abstract_key { get { return (Key) key; }}

    construct
    {
        get_style_context ().add_class ("dconf-key");

        key_info_label.get_style_context ().add_class ("italic-label");
        key_info_label.set_label (_("No Schema Found"));
    }

    public KeyListBoxRowEditableNoSchema (DConfKey _key, ModificationsHandler modifications_handler, bool search_result_mode = false)
    {
        Object (key: _key, modifications_handler: modifications_handler, search_result_mode : search_result_mode);
    }

    protected override void update ()
    {
        SettingsModel model = modifications_handler.model;
        if (model.is_key_ghost (key))
        {
            if (boolean_switch != null)
            {
                ((!) boolean_switch).hide ();
                key_value_label.show ();
            }
            key_value_label.set_label (_("Key erased."));
        }
        else
        {
            Variant key_value = model.get_key_value (key);
            if (boolean_switch != null)
            {
                key_value_label.hide ();
                ((!) boolean_switch).show ();

                bool key_value_boolean = key_value.get_boolean ();
                Variant switch_variant = new Variant ("(sb)", key.full_name, !key_value_boolean);
                ((!) boolean_switch).set_action_name ("ui.empty");
                ((!) boolean_switch).set_active (key_value_boolean);
                ((!) boolean_switch).set_detailed_action_name ("bro.toggle-dconf-key-switch(" + switch_variant.print (false) + ")");
            }
            key_value_label.set_label (Key.cool_text_value_from_variant (key_value, key.type_string));
        }
    }

    protected override string get_text ()
    {
        SettingsModel model = modifications_handler.model;
        return model.get_key_copy_text (key.full_name, ".dconf");
    }

    protected override bool generate_popover (ContextPopover popover)
    {
        SettingsModel model = modifications_handler.model;
        Variant variant_s = new Variant.string (key.full_name);
        Variant variant_ss = new Variant ("(ss)", key.full_name, ".dconf");

        if (model.is_key_ghost (key))
        {
            popover.new_gaction ("copy", "app.copy(" + get_text_variant ().print (false) + ")");
            return true;
        }

        if (search_result_mode)
        {
            popover.new_gaction ("open_parent", "ui.open-parent(" + variant_s.print (false) + ")");
            popover.new_section ();
        }

        popover.new_gaction ("customize", "ui.open-object(" + variant_ss.print (false) + ")");
        popover.new_gaction ("copy", "app.copy(" + get_text_variant ().print (false) + ")");

        bool planned_change = modifications_handler.key_has_planned_change (key.full_name);
        Variant? planned_value = modifications_handler.get_key_planned_value (key.full_name);

        if (key.type_string == "b" || key.type_string == "mb")
        {
            popover.new_section ();
            bool delayed_apply_menu = modifications_handler.get_current_delay_mode ();
            Variant key_value = model.get_key_value (key);
            GLib.Action action = popover.create_buttons_list (true, delayed_apply_menu, planned_change, key.type_string,
                                                              planned_change ? planned_value : key_value, null);

            popover.change_dismissed.connect (() => {
                    destroy_popover ();
                    change_dismissed ();
                });
            popover.value_changed.connect ((gvariant) => {
                    hide_right_click_popover ();
                    action.change_state (new Variant.maybe (null, new Variant.maybe (new VariantType (key.type_string), gvariant)));
                    set_key_value (false, gvariant);
                });

            if (!delayed_apply_menu)
            {
                popover.new_section ();
                popover.new_gaction ("erase", "ui.erase(" + variant_s.print (false) + ")");
            }
        }
        else
        {
            if (planned_change)
            {
                popover.new_section ();
                popover.new_gaction (planned_value == null ? "unerase" : "dismiss", "ui.dismiss-change(" + variant_s.print (false) + ")");
            }

            if (!planned_change || planned_value != null) // not &&
            {
                popover.new_section ();
                popover.new_gaction ("erase", "ui.erase(" + variant_s.print (false) + ")");
            }
        }
        return true;
    }
}

private class KeyListBoxRowEditable : KeyListBoxRow
{
    public GSettingsKey key { get; construct; }
    private override Key abstract_key { get { return (Key) key; }}

    construct
    {
        get_style_context ().add_class ("gsettings-key");

        if (key.summary != "")
            key_info_label.set_label (key.summary);
        else
        {
            key_info_label.get_style_context ().add_class ("italic-label");
            key_info_label.set_label (_("No summary provided"));
        }

        if (key.warning_conflicting_key)
        {
            if (key.error_hard_conflicting_key)
            {
                get_style_context ().add_class ("hard-conflict");
                if (boolean_switch != null)
                {
                    ((!) boolean_switch).hide ();
                    key_value_label.show ();
                }
                key_value_label.get_style_context ().add_class ("italic-label");
                key_value_label.set_label (_("conflicting keys"));
            }
            else
                get_style_context ().add_class ("conflict");
        }
    }

    public KeyListBoxRowEditable (GSettingsKey _key, ModificationsHandler modifications_handler, bool search_result_mode = false)
    {
        Object (key: _key, modifications_handler: modifications_handler, search_result_mode : search_result_mode);
    }

    protected override void update ()
    {
        SettingsModel model = modifications_handler.model;
        Variant key_value = model.get_key_value (key);
        if (boolean_switch != null)
        {
            bool key_value_boolean = key_value.get_boolean ();
            Variant switch_variant = new Variant ("(ssbb)", key.full_name, key.schema_id, !key_value_boolean, key.default_value.get_boolean ());
            ((!) boolean_switch).set_action_name ("ui.empty");
            ((!) boolean_switch).set_active (key_value_boolean);
            ((!) boolean_switch).set_detailed_action_name ("bro.toggle-gsettings-key-switch(" + switch_variant.print (false) + ")");
        }

        StyleContext css_context = get_style_context ();
        if (model.is_key_default (key))
            css_context.remove_class ("edited");
        else
            css_context.add_class ("edited");
        key_value_label.set_label (Key.cool_text_value_from_variant (key_value, key.type_string));
    }

    protected override string get_text ()
    {
        SettingsModel model = modifications_handler.model;
        return model.get_key_copy_text (key.full_name, key.schema_id);
    }

    protected override bool generate_popover (ContextPopover popover)
    {
        SettingsModel model = modifications_handler.model;
        Variant variant_s = new Variant.string (key.full_name);
        Variant variant_ss = new Variant ("(ss)", key.full_name, key.schema_id);

        if (search_result_mode)
        {
            popover.new_gaction ("open_parent", "ui.open-parent(" + variant_s.print (false) + ")");
            popover.new_section ();
        }

        if (key.error_hard_conflicting_key)
        {
            popover.new_gaction ("detail", "ui.open-object(" + variant_ss.print (false) + ")");
            popover.new_gaction ("copy", "app.copy(" + get_text_variant ().print (false) + ")");
            return true; // anything else is value-related, so we are done
        }

        bool delayed_apply_menu = modifications_handler.get_current_delay_mode ();
        bool planned_change = modifications_handler.key_has_planned_change (key.full_name);
        Variant? planned_value = modifications_handler.get_key_planned_value (key.full_name);

        popover.new_gaction ("customize", "ui.open-object(" + variant_ss.print (false) + ")");
        popover.new_gaction ("copy", "app.copy(" + get_text_variant ().print (false) + ")");

        if (key.type_string == "b" || key.type_string == "<enum>" || key.type_string == "mb"
            || (
                (key.type_string == "y" || key.type_string == "q" || key.type_string == "u" || key.type_string == "t")
                && (key.range_type == "range")
                && (Key.get_variant_as_uint64 (key.range_content.get_child_value (1)) - Key.get_variant_as_uint64 (key.range_content.get_child_value (0)) < 13)
               )
            || (
                (key.type_string == "n" || key.type_string == "i" || key.type_string == "h" || key.type_string == "x")
                && (key.range_type == "range")
                && (Key.get_variant_as_int64 (key.range_content.get_child_value (1)) - Key.get_variant_as_int64 (key.range_content.get_child_value (0)) < 13)
               ))
        {
            popover.new_section ();
            GLib.Action action;
            if (planned_change)
                action = popover.create_buttons_list (true, delayed_apply_menu, planned_change, key.type_string,
                                                      modifications_handler.get_key_planned_value (key.full_name), key.range_content);
            else if (model.is_key_default (key))
                action = popover.create_buttons_list (true, delayed_apply_menu, planned_change, key.type_string,
                                                      null, key.range_content);
            else
                action = popover.create_buttons_list (true, delayed_apply_menu, planned_change, key.type_string,
                                                      model.get_key_value (key), key.range_content);

            popover.change_dismissed.connect (() => {
                    destroy_popover ();
                    change_dismissed ();
                });
            popover.value_changed.connect ((gvariant) => {
                    hide_right_click_popover ();
                    Variant key_value = model.get_key_value (key);
                    action.change_state (new Variant.maybe (null, new Variant.maybe (new VariantType (key_value.get_type_string ()), gvariant)));
                    set_key_value (true, gvariant);
                });
        }
        else if (!delayed_apply_menu && !planned_change && key.type_string == "<flags>")
        {
            popover.new_section ();

            if (!model.is_key_default (key))
                popover.new_gaction ("default2", "bro.set-to-default(" + variant_ss.print (false) + ")");

            string [] all_flags = key.range_content.get_strv ();
            popover.create_flags_list (key.settings.get_strv (key.name), all_flags);
            ulong delayed_modifications_changed_handler = modifications_handler.delayed_changes_changed.connect (() => {
                    string [] active_flags = modifications_handler.get_key_custom_value (key).get_strv ();
                    foreach (string flag in all_flags)
                        popover.update_flag_status (flag, flag in active_flags);
                });
            popover.destroy.connect (() => modifications_handler.disconnect (delayed_modifications_changed_handler));

            popover.value_changed.connect ((gvariant) => set_key_value (true, gvariant));
        }
        else if (planned_change)
        {
            popover.new_section ();
            popover.new_gaction ("dismiss", "ui.dismiss-change(" + variant_s.print (false) + ")");

            if (planned_value != null)
                popover.new_gaction ("default1", "bro.set-to-default(" + variant_ss.print (false) + ")");
        }
        else if (!model.is_key_default (key))
        {
            popover.new_section ();
            popover.new_gaction ("default1", "bro.set-to-default(" + variant_ss.print (false) + ")");
        }
        return true;
    }
}

private class ContextPopover : Popover
{
    private GLib.Menu menu = new GLib.Menu ();
    private GLib.Menu current_section;

    private ActionMap current_group = new SimpleActionGroup ();

    // public signals
    public signal void value_changed (Variant? gvariant);
    public signal void change_dismissed ();

    public ContextPopover ()
    {
        new_section_real ();

        insert_action_group ("popmenu", (SimpleActionGroup) current_group);

        bind_model (menu, null);
    }

    /*\
    * * Simple actions
    \*/

    public void new_gaction (string action_name, string action_action)
    {
        string action_text;
        switch (action_name)
        {
            /* Translators: "copy to clipboard" action in the right-click menu on the list of keys */
            case "copy":            action_text = _("Copy");                break;

            /* Translators: "open key-editor page" action in the right-click menu on the list of keys */
            case "customize":       action_text = _("Customize…");          break;

            /* Translators: "reset key value" action in the right-click menu on the list of keys */
            case "default1":        action_text = _("Set to default");      break;

            case "default2": new_multi_default_action (action_action);      return;

            /* Translators: "open key-editor page" action in the right-click menu on the list of keys, when key is hard-conflicting */
            case "detail":          action_text = _("Show details…");       break;

            /* Translators: "dismiss change" action in the right-click menu on a key with pending changes */
            case "dismiss":         action_text = _("Dismiss change");      break;

            /* Translators: "erase key" action in the right-click menu on a key without schema */
            case "erase":           action_text = _("Erase key");           break;

            /* Translators: "open folder" action in the right-click menu on a folder */
            case "open":            action_text = _("Open");                break;

            /* Translators: "open parent folder" action in the right-click menu on a folder in a search result */
            case "open_parent":     action_text = _("Open parent folder");  break;

            /* Translators: "reset recursively" action in the right-click menu on a folder */
            case "recursivereset":  action_text = _("Reset recursively");   break;

            /* Translators: "dismiss change" action in the right-click menu on a key without schema planned to be erased */
            case "unerase":         action_text = _("Do not erase");        break;

            default: assert_not_reached ();
        }
        current_section.append (action_text, action_action);
    }

    public void new_section ()
    {
        current_section.freeze ();
        new_section_real ();
    }
    private void new_section_real ()
    {
        current_section = new GLib.Menu ();
        menu.append_section (null, current_section);
    }

    /*\
    * * Flags
    \*/

    public void create_flags_list (string [] active_flags, string [] all_flags)
    {
        foreach (string flag in all_flags)
            create_flag (flag, flag in active_flags, all_flags);

        finalize_menu ();
    }
    private void create_flag (string flag, bool active, string [] all_flags)
    {
        SimpleAction simple_action = new SimpleAction.stateful (flag, null, new Variant.boolean (active));
        current_group.add_action (simple_action);

        current_section.append (flag, @"popmenu.$flag");

        simple_action.change_state.connect ((gaction, gvariant) => {
                gaction.set_state ((!) gvariant);

                string [] new_flags = new string [0];
                foreach (string iter in all_flags)
                {
                    SimpleAction action = (SimpleAction) current_group.lookup_action (iter);
                    if (((!) action.state).get_boolean ())
                        new_flags += action.name;
                }
                Variant variant = new Variant.strv (new_flags);
                value_changed (variant);
            });
    }

    public void update_flag_status (string flag, bool active)
    {
        SimpleAction simple_action = (SimpleAction) current_group.lookup_action (flag);
        if (active != simple_action.get_state ())
            simple_action.set_state (new Variant.boolean (active));
    }

    /*\
    * * Choices
    \*/

    public GLib.Action create_buttons_list (bool display_default_value, bool delayed_apply_menu, bool planned_change, string settings_type, Variant? value_variant, Variant? range_content_or_null)
    {
        // TODO report bug: if using ?: inside ?:, there's a "g_variant_ref: assertion 'value->ref_count > 0' failed"
        const string ACTION_NAME = "choice";
        string group_dot_action = "popmenu.choice";

        string type_string = settings_type == "<enum>" ? "s" : settings_type;
        VariantType original_type = new VariantType (type_string);
        VariantType nullable_type = new VariantType.maybe (original_type);
        VariantType nullable_nullable_type = new VariantType.maybe (nullable_type);

        Variant variant = new Variant.maybe (original_type, value_variant);
        Variant nullable_variant;
        if (delayed_apply_menu && !planned_change)
            nullable_variant = new Variant.maybe (nullable_type, null);
        else
            nullable_variant = new Variant.maybe (nullable_type, variant);

        GLib.Action action = (GLib.Action) new SimpleAction.stateful (ACTION_NAME, nullable_nullable_type, nullable_variant);
        current_group.add_action (action);

        if (display_default_value)
        {
            bool complete_menu = delayed_apply_menu || planned_change;

            if (complete_menu)
                /* Translators: "no change" option in the right-click menu on a key when on delayed mode */
                current_section.append (_("No change"), @"$group_dot_action(@mm$type_string nothing)");

            if (range_content_or_null != null)
                new_multi_default_action (@"$group_dot_action(@mm$type_string just nothing)");
            else if (complete_menu)
                /* Translators: "erase key" option in the right-click menu on a key without schema when on delayed mode */
                current_section.append (_("Erase key"), @"$group_dot_action(@mm$type_string just nothing)");
        }

        switch (settings_type)
        {
            case "b":
                current_section.append (Key.cool_boolean_text_value (true), @"$group_dot_action(@mmb true)");
                current_section.append (Key.cool_boolean_text_value (false), @"$group_dot_action(@mmb false)");
                break;
            case "<enum>":      // defined by the schema
                Variant range = (!) range_content_or_null;
                uint size = (uint) range.n_children ();
                if (size == 0 || (size == 1 && !display_default_value))
                    assert_not_reached ();
                for (uint index = 0; index < size; index++)
                    current_section.append (range.get_child_value (index).print (false), @"$group_dot_action(@mms '" + range.get_child_value (index).get_string () + "')");        // TODO use int settings.get_enum ()
                break;
            case "mb":
                current_section.append (Key.cool_boolean_text_value (null), @"$group_dot_action(@mmmb just just nothing)");
                current_section.append (Key.cool_boolean_text_value (true), @"$group_dot_action(@mmmb true)");
                current_section.append (Key.cool_boolean_text_value (false), @"$group_dot_action(@mmmb false)");
                break;
            case "y":
            case "q":
            case "u":
            case "t":
                Variant range = (!) range_content_or_null;
                for (uint64 number =  Key.get_variant_as_uint64 (range.get_child_value (0));
                            number <= Key.get_variant_as_uint64 (range.get_child_value (1));
                            number++)
                    current_section.append (number.to_string (), @"$group_dot_action(@mm$type_string $number)");
                break;
            case "n":
            case "i":
            case "h":
            case "x":
                Variant range = (!) range_content_or_null;
                for (int64 number =  Key.get_variant_as_int64 (range.get_child_value (0));
                           number <= Key.get_variant_as_int64 (range.get_child_value (1));
                           number++)
                    current_section.append (number.to_string (), @"$group_dot_action(@mm$type_string $number)");
                break;
        }

        ((GLib.ActionGroup) current_group).action_state_changed [ACTION_NAME].connect ((unknown_string, tmp_variant) => {
                Variant? change_variant = tmp_variant.get_maybe ();
                if (change_variant != null)
                    value_changed (((!) change_variant).get_maybe ());
                else
                    change_dismissed ();
            });

        finalize_menu ();

        return action;
    }

    /*\
    * * Multi utilities
    \*/

    private void new_multi_default_action (string action)
    {
        /* Translators: "reset key value" option of a multi-choice list (checks or radios) in the right-click menu on the list of keys */
        current_section.append (_("Default value"), action);
    }

    private void finalize_menu ()
        requires (menu.is_mutable ())  // should just "return;" then if function is made public
    {
        current_section.freeze ();
        menu.freeze ();
    }
}