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;

[GtkTemplate (ui = "/ca/desrt/dconf-editor/ui/registry-info.ui")]
class RegistryInfo : Grid, BrowsableView
{
    [GtkChild] private Revealer conflicting_key_warning_revealer;
    [GtkChild] private Revealer hard_conflicting_key_error_revealer;
    [GtkChild] private Revealer no_schema_warning;
    [GtkChild] private Revealer one_choice_warning_revealer;
    [GtkChild] private Label one_choice_enum_warning;
    [GtkChild] private Label one_choice_integer_warning;
    [GtkChild] private ListBox properties_list_box;
    [GtkChild] private Button erase_button;

    public ModificationsHandler modifications_handler { private get; set; }

    private Variant? current_key_info = null;
    public string full_name { get; private set; default = ""; }

    /*\
    * * Cleaning
    \*/

    private ulong erase_button_handler = 0;
    private ulong revealer_reload_1_handler = 0;
    private ulong revealer_reload_2_handler = 0;

    public void clean ()
    {
        disconnect_handler (erase_button, ref erase_button_handler);
        disconnect_handler (modifications_handler, ref revealer_reload_1_handler);
        disconnect_handler (modifications_handler, ref revealer_reload_2_handler);
        properties_list_box.@foreach ((widget) => widget.destroy ());
    }

    private static void disconnect_handler (Object object, ref ulong handler)
    {
        if (handler == 0)   // erase_button_handler & revealer_reload_1_handler depend of the key's type
            return;
        object.disconnect (handler);
        handler = 0;
    }

    /*\
    * * Populating
    \*/

    public void populate_properties_list_box (Key key)
    {
        SettingsModel model = modifications_handler.model;
        if (key is DConfKey && model.is_key_ghost ((DConfKey) key))   // TODO place in "requires"
            assert_not_reached ();
        clean ();   // for when switching between two keys, for example with a search (maybe also bookmarks)

        bool has_schema;
        unowned Variant [] dict_container;
        current_key_info = key.properties;
        full_name = key.full_name;
        key.properties.get ("(ba{ss})", out has_schema, out dict_container);

        if (key is GSettingsKey)
        {
            if (((GSettingsKey) key).error_hard_conflicting_key)
            {
                conflicting_key_warning_revealer.set_reveal_child (false);
                hard_conflicting_key_error_revealer.set_reveal_child (true);
            }
            else if (((GSettingsKey) key).warning_conflicting_key)
            {
                conflicting_key_warning_revealer.set_reveal_child (true);
                hard_conflicting_key_error_revealer.set_reveal_child (false);
            }
            else
            {
                conflicting_key_warning_revealer.set_reveal_child (false);
                hard_conflicting_key_error_revealer.set_reveal_child (false);
            }
        }
        else
        {
            conflicting_key_warning_revealer.set_reveal_child (false);
            hard_conflicting_key_error_revealer.set_reveal_child (false);
        }
        no_schema_warning.set_reveal_child (!has_schema);

        properties_list_box.@foreach ((widget) => widget.destroy ());

        Variant dict = dict_container [0];

        // TODO use VariantDict
        string key_name, parent_path, tmp_string;
        bool test;

        if (!dict.lookup ("key-name",     "s", out key_name))    assert_not_reached ();
        if (!dict.lookup ("parent-path",  "s", out parent_path)) assert_not_reached ();

        if (dict.lookup ("defined-by",    "s", out tmp_string))  add_row_from_label (_("Defined by"),  tmp_string);
        else assert_not_reached ();
        if (dict.lookup ("schema-id",     "s", out tmp_string))  add_row_from_label (_("Schema"),      tmp_string);
        add_separator ();
        if (dict.lookup ("summary",       "s", out tmp_string))
        {
            test = tmp_string == "";
            add_row_from_label (_("Summary"),                    test ? _("No summary provided")     : tmp_string, test);
        }
        if (dict.lookup ("description",   "s", out tmp_string))
        {
            test = tmp_string == "";
            add_row_from_label (_("Description"),                test ? _("No description provided") : tmp_string, test);
        }
        /* Translators: as in datatype (integer, boolean, string, etc.) */
        if (dict.lookup ("type-name",     "s", out tmp_string))  add_row_from_label (_("Type"),        tmp_string);
        else assert_not_reached ();
        if (dict.lookup ("minimum",       "s", out tmp_string))  add_row_from_label (_("Minimum"),     tmp_string);
        if (dict.lookup ("maximum",       "s", out tmp_string))  add_row_from_label (_("Maximum"),     tmp_string);
        if (dict.lookup ("default-value", "s", out tmp_string))  add_row_from_label (_("Default"),     tmp_string);

        if (!dict.lookup ("type-code",    "s", out tmp_string))  assert_not_reached ();

        ulong key_value_changed_handler = 0;
        Label label;
        if (key is GSettingsKey && ((GSettingsKey) key).error_hard_conflicting_key)
        {
            label = new Label (_("There are conflicting definitions of this key, getting value would be either problematic or meaningless."));
            label.get_style_context ().add_class ("italic-label");
        }
        else
        {
            label = new Label (get_current_value_text (has_schema && model.is_key_default ((GSettingsKey) key), key, modifications_handler.model));
            key_value_changed_handler = key.value_changed.connect (() => {
                    if (!has_schema && model.is_key_ghost ((DConfKey) key))
                        label.set_text (_("Key erased."));
                    else
                        label.set_text (get_current_value_text (has_schema && model.is_key_default ((GSettingsKey) key), key, modifications_handler.model));
                });
        }
        label.halign = Align.START;
        label.valign = Align.START;
        label.xalign = 0;
        label.yalign = 0;
        label.wrap = true;
        label.hexpand = true;
        label.show ();
        add_row_from_widget (_("Current value"), label, null);

        add_separator ();

        KeyEditorChild key_editor_child = create_child (key, has_schema, modifications_handler);
        bool is_key_editor_child_single = key_editor_child is KeyEditorChildSingle;
        if (is_key_editor_child_single)
        {
            bool is_enum = tmp_string == "<enum>";
            one_choice_integer_warning.visible = !is_enum;
            one_choice_enum_warning.visible = is_enum;
        }
        one_choice_warning_revealer.set_reveal_child (is_key_editor_child_single);

        if (key is GSettingsKey && ((GSettingsKey) key).error_hard_conflicting_key)
            return;

        ulong value_has_changed_handler = key_editor_child.value_has_changed.connect ((is_valid) => {
                if (modifications_handler.should_delay_apply (tmp_string))
                {
                    if (is_valid)
                        modifications_handler.add_delayed_setting (key.full_name, key_editor_child.get_variant ());
                    else
                        modifications_handler.dismiss_change (key.full_name);
                }
                else
                    model.set_key_value (key, key_editor_child.get_variant ());
            });

        if (has_schema)
        {
            Switch custom_value_switch = new Switch ();
            custom_value_switch.set_can_focus (false);
            custom_value_switch.halign = Align.START;
            custom_value_switch.hexpand = true;
            custom_value_switch.show ();
            add_switch_row (_("Use default value"), custom_value_switch);

            custom_value_switch.bind_property ("active", key_editor_child, "sensitive", BindingFlags.SYNC_CREATE | BindingFlags.INVERT_BOOLEAN);

            GSettingsKey gkey = (GSettingsKey) key;
            custom_value_switch.set_active (modifications_handler.key_value_is_default (gkey));
            ulong switch_active_handler = custom_value_switch.notify ["active"].connect (() => {
                    if (modifications_handler.should_delay_apply (tmp_string))
                    {
                        if (custom_value_switch.get_active ())
                            modifications_handler.add_delayed_setting (key.full_name, null);
                        else
                        {
                            Variant tmp_variant = modifications_handler.get_key_custom_value (key);
                            modifications_handler.add_delayed_setting (key.full_name, tmp_variant);
                            key_editor_child.reload (tmp_variant);
                        }
                    }
                    else
                    {
                        if (custom_value_switch.get_active ())
                        {
                            model.set_key_to_default (((GSettingsKey) key).full_name, ((GSettingsKey) key).schema_id);
                            SignalHandler.block (key_editor_child, value_has_changed_handler);
                            key_editor_child.reload (model.get_key_value (key));
                            //if (tmp_string == "<flags>")                      let's try to live without this...
                            //    key.planned_value = key.value;
                            SignalHandler.unblock (key_editor_child, value_has_changed_handler);
                        }
                        else
                            model.set_key_value (key, model.get_key_value (key));  // TODO that hurts...
                    }
                });
            revealer_reload_1_handler = modifications_handler.leave_delay_mode.connect (() => {
                    SignalHandler.block (custom_value_switch, switch_active_handler);
                    custom_value_switch.set_active (model.is_key_default (gkey));
                    SignalHandler.unblock (custom_value_switch, switch_active_handler);
                });
            custom_value_switch.destroy.connect (() => custom_value_switch.disconnect (switch_active_handler));
        }
        else
        {
            erase_button_handler = erase_button.clicked.connect (() => {
                    modifications_handler.enter_delay_mode ();
                    modifications_handler.add_delayed_setting (key.full_name, null);
                });
        }

        ulong child_activated_handler = key_editor_child.child_activated.connect (() => modifications_handler.apply_delayed_settings ());  // TODO "only" used for string-based and spin widgets
        revealer_reload_2_handler = modifications_handler.leave_delay_mode.connect (() => {
                if (key is DConfKey && model.is_key_ghost ((DConfKey) key))
                    return;
                SignalHandler.block (key_editor_child, value_has_changed_handler);
                key_editor_child.reload (model.get_key_value (key));
                //if (tmp_string == "<flags>")                      let's try to live without this...
                //    key.planned_value = key.value;
                SignalHandler.unblock (key_editor_child, value_has_changed_handler);
            });
        add_row_from_widget (_("Custom value"), key_editor_child, tmp_string);

        key_editor_child.destroy.connect (() => {
                if (key_value_changed_handler == 0)
                    assert_not_reached ();
                key.disconnect (key_value_changed_handler);
                key_editor_child.disconnect (value_has_changed_handler);
                key_editor_child.disconnect (child_activated_handler);
            });
    }

    private static KeyEditorChild create_child (Key key, bool has_schema, ModificationsHandler modifications_handler)
    {
        SettingsModel model = modifications_handler.model;
        Variant initial_value = modifications_handler.get_key_custom_value (key);
        switch (key.type_string)
        {
            case "<enum>":
                switch (((GSettingsKey) key).range_content.n_children ())
                {
                    case 0: assert_not_reached ();
                    case 1:
                        return (KeyEditorChild) new KeyEditorChildSingle (model.get_key_value (key), model.get_key_value (key).get_string ());
                    default:
                        bool delay_mode = modifications_handler.get_current_delay_mode ();
                        bool has_planned_change = modifications_handler.key_has_planned_change (key.full_name);
                        Variant range_content = ((GSettingsKey) key).range_content;
                        return (KeyEditorChild) new KeyEditorChildEnum (initial_value, delay_mode, has_planned_change, range_content);
                }

            case "<flags>":
                string [] all_flags = ((GSettingsKey) key).range_content.get_strv ();
                string [] active_flags = ((GSettingsKey) key).settings.get_strv (key.name);
                KeyEditorChildFlags key_editor_child_flags = new KeyEditorChildFlags (initial_value, all_flags, active_flags);

                ulong delayed_modifications_changed_handler = modifications_handler.delayed_changes_changed.connect (() => {
                        active_flags = modifications_handler.get_key_custom_value (key).get_strv ();
                        key_editor_child_flags.update_flags (active_flags);
                    });
                key_editor_child_flags.destroy.connect (() => modifications_handler.disconnect (delayed_modifications_changed_handler));
                return (KeyEditorChild) key_editor_child_flags;

            case "b":
                return (KeyEditorChild) new KeyEditorChildBool (initial_value.get_boolean ());

            case "n":
            case "i":
            case "h":
            // TODO "x" is not working in spinbuttons (double-based)
                Variant? range = null;
                if (has_schema && (((GSettingsKey) key).range_type == "range"))
                {
                    range = ((GSettingsKey) key).range_content;
                    if (Key.get_variant_as_int64 (((!) range).get_child_value (0)) == Key.get_variant_as_int64 (((!) range).get_child_value (1)))
                        return (KeyEditorChild) new KeyEditorChildSingle (model.get_key_value (key), model.get_key_value (key).print (false));
                }
                return (KeyEditorChild) new KeyEditorChildNumberInt (initial_value, key.type_string, range);

            case "y":
            case "q":
            case "u":
            // TODO "t" is not working in spinbuttons (double-based)
                Variant? range = null;
                if (has_schema && (((GSettingsKey) key).range_type == "range"))
                {
                    range = ((GSettingsKey) key).range_content;
                    if (Key.get_variant_as_uint64 (((!) range).get_child_value (0)) == Key.get_variant_as_uint64 (((!) range).get_child_value (1)))
                        return (KeyEditorChild) new KeyEditorChildSingle (model.get_key_value (key), model.get_key_value (key).print (false));
                }
                return (KeyEditorChild) new KeyEditorChildNumberInt (initial_value, key.type_string, range);

            case "d":
                return (KeyEditorChild) new KeyEditorChildNumberDouble (initial_value);

            case "mb":
                bool delay_mode = modifications_handler.get_current_delay_mode ();
                bool has_planned_change = modifications_handler.key_has_planned_change (key.full_name);
                Variant? range_content_or_null = null;
                if (key is GSettingsKey)
                    range_content_or_null = ((GSettingsKey) key).range_content;
                return (KeyEditorChild) new KeyEditorChildNullableBool (initial_value, delay_mode, has_planned_change, range_content_or_null);

            default:
                if ("a" in key.type_string)
                    return (KeyEditorChild) new KeyEditorChildArray (key.type_string, initial_value);
                else
                    return (KeyEditorChild) new KeyEditorChildDefault (key.type_string, initial_value);
        }
    }

    private static string get_current_value_text (bool is_default, Key key, SettingsModel model)
    {
        if (is_default)
            return _("Default value");
        else
            return Key.cool_text_value_from_variant (model.get_key_value (key), key.type_string);
    }

    public string? get_copy_text ()
    {
        Widget? focused_row = properties_list_box.get_focus_child ();
        if (focused_row == null)
            return null;
        else if ((!) focused_row is PropertyRow)
            return ((PropertyRow) (!) focused_row).get_copy_text ();
        else    // separator
            return null;
    }

    /*\
    * * Rows creation
    \*/

    private void add_row_from_label (string property_name, string property_value, bool use_italic = false)
    {
        properties_list_box.add (new PropertyRow.from_label (property_name, property_value, use_italic));
    }

    private void add_switch_row (string property_name, Switch custom_value_switch)
    {
        PropertyRow row = new PropertyRow.from_widgets (property_name, custom_value_switch, null);
        ulong default_value_row_activate_handler = row.activate.connect (() => custom_value_switch.set_active (!custom_value_switch.get_active ()));
        row.destroy.connect (() => row.disconnect (default_value_row_activate_handler));
        properties_list_box.add (row);
    }

    private void add_row_from_widget (string property_name, Widget widget, string? type)
    {
        PropertyRow row = new PropertyRow.from_widgets (property_name, widget, type != null ? add_warning ((!) type) : null);
        widget.bind_property ("sensitive", row, "sensitive", BindingFlags.SYNC_CREATE);
        properties_list_box.add (row);
    }

    private void add_separator ()
    {
        Separator separator = new Separator (Orientation.HORIZONTAL);
        separator.halign = Align.FILL;
        separator.margin_bottom = 5;
        separator.margin_top = 5;
        separator.show ();

        ListBoxRowWrapper row = new ListBoxRowWrapper ();
        row.halign = Align.CENTER;
        row.add (separator);
        row.set_sensitive (false);
/* TODO could be selected by down arrow        row.focus.connect ((direction) => { row.move_focus (direction); return false; }); */
        row.show ();
        properties_list_box.add (row);
    }

    private static Widget? add_warning (string type)
    {
        if (type == "d")    // TODO if type contains "d"; on Intl.get_language_names ()[0] != "C"?
            return warning_label (_("Use a dot as decimal mark and no thousands separator. You can use the X.Ye+Z notation."));

        if (type != "<flags>" && ((type != "s" && "s" in type) || (type != "g" && "g" in type)) || (type != "o" && "o" in type))
        {
            if ("m" in type)
                /* Translators: neither the "nothing" keyword nor the "m" type should be translated; a "maybe type" is a type of variant that is nullable. */
                return warning_label (_("Use the keyword “nothing” to set a maybe type (beginning with “m”) to its empty value. Strings, signatures and object paths should be surrounded by quotation marks."));
            else
                return warning_label (_("Strings, signatures and object paths should be surrounded by quotation marks."));
        }
        else if (type != "m" && type != "mb" && type != "<enum>" && "m" in type)
            /* Translators: neither the "nothing" keyword nor the "m" type should be translated; a "maybe type" is a type of variant that is nullable. */
            return warning_label (_("Use the keyword “nothing” to set a maybe type (beginning with “m”) to its empty value."));
        return null;
    }
    private static Widget warning_label (string text)
    {
        Label label = new Label (text);
        label.wrap = true;
        StyleContext context = label.get_style_context ();
        context.add_class ("italic-label");
        context.add_class ("greyed-label");
        context.add_class ("warning-label");
        return (Widget) label;
    }

    public bool check_reload (Variant properties)
    {
        if (current_key_info == null) // should not happen?
            return true;
        return !((!) current_key_info).equal (properties); // TODO compare key value with editor value?
    }
}

[GtkTemplate (ui = "/ca/desrt/dconf-editor/ui/property-row.ui")]
private class PropertyRow : ListBoxRowWrapper
{
    [GtkChild] private Grid grid;
    [GtkChild] private Label name_label;

    private Widget? value_widget = null;

    public PropertyRow.from_label (string property_name, string property_value, bool use_italic)
    {
        name_label.set_text (property_name);

        Label value_label = new Label (property_value);
        value_widget = value_label;
        value_label.valign = Align.START;
        value_label.xalign = 0;
        value_label.yalign = 0;
        value_label.wrap = true;
        if (use_italic)
            value_label.get_style_context ().add_class ("italic-label");
        value_label.show ();
        grid.attach (value_label, 1, 0, 1, 1);
    }

    public PropertyRow.from_widgets (string property_name, Widget widget, Widget? warning)
    {
        name_label.set_text (property_name);

        if (widget is Label)    // TODO handle other rows
            value_widget = widget;

        grid.attach (widget, 1, 0, 1, 1);
        widget.valign = Align.CENTER;

        if (warning != null)
        {
            ((!) warning).hexpand = true;
            ((!) warning).halign = Align.CENTER;
            ((!) warning).show ();
            grid.row_spacing = 4;
            grid.attach ((!) warning, 0, 1, 2, 1);
        }
    }

    public string? get_copy_text ()
    {
        if (value_widget != null)
            return ((Label) (!) value_widget).get_label ();
        else
            return null;
    }
}