/* 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 . */ using Gtk; public interface KeyEditorChild : Widget { public signal void value_has_changed (bool is_valid = true); public abstract Variant get_variant (); public signal void child_activated (); public abstract void reload (Variant gvariant); } private class KeyEditorChildSingle : Label, KeyEditorChild { private Variant variant; public KeyEditorChildSingle (Variant key_value, string text) { variant = key_value; set_label (text); show (); } public Variant get_variant () { return variant; } public void reload (Variant gvariant) {} } private class KeyEditorChildEnum : MenuButton, KeyEditorChild { private Variant variant; private GLib.Action action; public KeyEditorChildEnum (Variant initial_value, bool delay_mode, bool has_planned_change, Variant range_content) { this.visible = true; this.hexpand = true; this.halign = Align.START; this.use_popover = true; this.width_request = 100; ContextPopover popover = new ContextPopover (); action = popover.create_buttons_list (false, delay_mode, has_planned_change, "", initial_value, range_content); popover.set_relative_to (this); popover.value_changed.connect ((gvariant) => { if (gvariant == null) // TODO better (1/3) assert_not_reached (); reload ((!) gvariant); popover.closed (); value_has_changed (); }); reload (initial_value); this.set_popover ((Popover) popover); } public Variant get_variant () { return variant; } public void reload (Variant gvariant) { variant = gvariant; VariantType type = gvariant.get_type (); label = type == VariantType.STRING ? gvariant.get_string () : gvariant.print (false); action.change_state (new Variant.maybe (null, new Variant.maybe (type, gvariant))); } } private class KeyEditorChildFlags : Grid, KeyEditorChild { private string [] all_flags; private ContextPopover popover = new ContextPopover (); private Variant variant; private Label label = new Label (""); public KeyEditorChildFlags (Variant initial_value, string [] _all_flags, string [] active_flags) { all_flags = _all_flags; this.visible = true; this.hexpand = true; this.orientation = Orientation.HORIZONTAL; this.column_spacing = 8; MenuButton button = new MenuButton (); button.visible = true; button.use_popover = true; button.halign = Align.START; button.get_style_context ().add_class ("image-button"); this.add (button); label.visible = true; label.halign = Align.START; label.hexpand = true; this.add (label); popover.create_flags_list (active_flags, all_flags); popover.set_relative_to (button); popover.value_changed.connect ((gvariant) => { if (gvariant == null) // TODO better (2/3) assert_not_reached (); reload ((!) gvariant); value_has_changed (); }); reload (initial_value); button.set_popover ((Popover) popover); } public void update_flags (string [] active_flags) { foreach (string flag in all_flags) popover.update_flag_status (flag, flag in active_flags); } public Variant get_variant () { return variant; } public void reload (Variant gvariant) { this.variant = gvariant; label.label = gvariant.print (false); } } private class KeyEditorChildNullableBool : MenuButton, KeyEditorChild { private Variant variant; private Variant? maybe_variant; private GLib.Action action; public KeyEditorChildNullableBool (Variant initial_value, bool delay_mode, bool has_planned_change, Variant? range_content_or_null) { this.visible = true; this.hexpand = true; this.halign = Align.START; this.use_popover = true; this.width_request = 100; ContextPopover popover = new ContextPopover (); action = popover.create_buttons_list (false, delay_mode, has_planned_change, "mb", initial_value, range_content_or_null); popover.set_relative_to (this); popover.value_changed.connect ((gvariant) => { if (gvariant == null) // TODO better (3/3) assert_not_reached (); reload ((!) gvariant); popover.closed (); value_has_changed (); }); reload (initial_value); this.set_popover ((Popover) popover); } public Variant get_variant () { return variant; } public void reload (Variant gvariant) { variant = gvariant; maybe_variant = variant.get_maybe (); if (maybe_variant == null) label = Key.cool_boolean_text_value (null); else label = Key.cool_boolean_text_value (((!) maybe_variant).get_boolean ()); action.change_state (new Variant.maybe (null, new Variant.maybe (new VariantType ("mb"), gvariant))); } } private class KeyEditorChildBool : Box, KeyEditorChild // might be managed by action, but can't find a way to ensure one-and-only-one button is active // https://bugzilla.gnome.org/show_bug.cgi?id=769876 { private ToggleButton button_true; public KeyEditorChildBool (bool initial_value) { this.visible = true; this.hexpand = true; this.orientation = Orientation.HORIZONTAL; this.halign = Align.START; this.homogeneous = true; this.width_request = 100; this.get_style_context ().add_class ("linked"); ToggleButton button_false = new ToggleButton (); button_false.visible = true; button_false.label = Key.cool_boolean_text_value (false); this.add (button_false); button_true = new ToggleButton (); button_true.visible = true; button_true.label = Key.cool_boolean_text_value (true); this.add (button_true); button_true.active = initial_value; button_true.bind_property ("active", button_false, "active", BindingFlags.INVERT_BOOLEAN|BindingFlags.SYNC_CREATE|BindingFlags.BIDIRECTIONAL); button_true.toggled.connect (() => value_has_changed ()); } public Variant get_variant () { return new Variant.boolean (button_true.active); } public void reload (Variant gvariant) { button_true.active = gvariant.get_boolean (); } } private class KeyEditorChildNumberDouble : Entry, KeyEditorChild { private Variant variant; private ulong deleted_text_handler = 0; private ulong inserted_text_handler = 0; construct { get_style_context ().add_class ("key-editor-child-entry"); } public KeyEditorChildNumberDouble (Variant initial_value) { this.variant = initial_value; this.visible = true; this.hexpand = true; this.secondary_icon_activatable = false; this.set_icon_tooltip_text (EntryIconPosition.SECONDARY, _("Failed to parse as double.")); this.text = initial_value.print (false); EntryBuffer ref_buffer = buffer; // an EntryBuffer doesn't emit a "destroy" signal deleted_text_handler = ref_buffer.deleted_text.connect (() => value_has_changed (test_value ())); inserted_text_handler = ref_buffer.inserted_text.connect (() => value_has_changed (test_value ())); ulong entry_activate_handler = activate.connect (() => { if (test_value ()) child_activated (); }); destroy.connect (() => { ref_buffer.disconnect (deleted_text_handler); ref_buffer.disconnect (inserted_text_handler); disconnect (entry_activate_handler); }); } private bool test_value () { string tmp_text = this.text; // don't put in the try{} for correct C code try { Variant? tmp_variant = Variant.parse (VariantType.DOUBLE, tmp_text); variant = (!) tmp_variant; StyleContext context = get_style_context (); if (context.has_class ("error")) context.remove_class ("error"); set_icon_from_icon_name (EntryIconPosition.SECONDARY, null); return true; } catch (VariantParseError e) { StyleContext context = get_style_context (); if (!context.has_class ("error")) context.add_class ("error"); secondary_icon_name = "dialog-error-symbolic"; return false; } } public Variant get_variant () { return variant; } private void set_lock (bool state) requires (deleted_text_handler != 0 && inserted_text_handler != 0) { if (state) { SignalHandler.block (buffer, deleted_text_handler); SignalHandler.block (buffer, inserted_text_handler); } else { SignalHandler.unblock (buffer, deleted_text_handler); SignalHandler.unblock (buffer, inserted_text_handler); } } public void reload (Variant gvariant) { set_lock (true); this.text = gvariant.print (false); if (!test_value ()) assert_not_reached (); set_lock (false); } } private class KeyEditorChildNumberInt : SpinButton, KeyEditorChild { private string key_type; private ulong deleted_text_handler = 0; private ulong inserted_text_handler = 0; public KeyEditorChildNumberInt (Variant initial_value, string type_string, Variant? range_content_or_null) requires (type_string == "y" || type_string == "n" || type_string == "q" || type_string == "i" || type_string == "u" || type_string == "h") // TODO type_string == "x" || type_string == "t" || { this.key_type = type_string; this.visible = true; this.hexpand = true; this.halign = Align.START; double min, max; if (range_content_or_null != null) { min = get_variant_as_double (((!) range_content_or_null).get_child_value (0)); max = get_variant_as_double (((!) range_content_or_null).get_child_value (1)); } else get_min_and_max_double (out min, out max, type_string); Adjustment adjustment = new Adjustment (get_variant_as_double (initial_value), min, max, 1.0, 5.0, 0.0); this.configure (adjustment, 1.0, 0); this.update_policy = SpinButtonUpdatePolicy.IF_VALID; this.snap_to_ticks = true; this.numeric = true; this.input_purpose = InputPurpose.NUMBER; // TODO could be DIGITS for UnsignedInt this.width_chars = 30; EntryBuffer ref_buffer = buffer; // an EntryBuffer doesn't emit a "destroy" signal deleted_text_handler = ref_buffer.deleted_text.connect (() => value_has_changed ()); inserted_text_handler = ref_buffer.inserted_text.connect (() => value_has_changed ()); ulong entry_activate_handler = activate.connect (() => { update (); child_activated (); }); destroy.connect (() => { ref_buffer.disconnect (deleted_text_handler); ref_buffer.disconnect (inserted_text_handler); disconnect (entry_activate_handler); }); } private static void get_min_and_max_double (out double min, out double max, string variant_type) { switch (variant_type) { case "y": min = (double) uint8.MIN; max = (double) uint8.MAX; break; case "n": min = (double) int16.MIN; max = (double) int16.MAX; break; case "q": min = (double) uint16.MIN; max = (double) uint16.MAX; break; case "i": min = (double) int32.MIN; max = (double) int32.MAX; break; case "u": min = (double) uint32.MIN; max = (double) uint32.MAX; break; case "h": min = (double) int32.MIN; max = (double) int32.MAX; break; default: assert_not_reached (); } } private static double get_variant_as_double (Variant variant) { switch (variant.classify ()) { case Variant.Class.BYTE: return (double) variant.get_byte (); case Variant.Class.INT16: return (double) variant.get_int16 (); case Variant.Class.UINT16: return (double) variant.get_uint16 (); case Variant.Class.INT32: return (double) variant.get_int32 (); case Variant.Class.UINT32: return (double) variant.get_uint32 (); case Variant.Class.HANDLE: return (double) variant.get_handle (); default: assert_not_reached (); } } public Variant get_variant () // TODO test_value against range { switch (key_type) { case "y": return new Variant.byte ((uchar) get_int64_from_entry ()); // TODO uchar or uint8? case "n": return new Variant.int16 ((int16) get_int64_from_entry ()); case "q": return new Variant.uint16 ((uint16) get_int64_from_entry ()); case "i": return new Variant.int32 ((int32) get_int64_from_entry ()); case "u": return new Variant.uint32 ((uint32) get_int64_from_entry ()); // TODO also use get_value_as_int? case "h": return new Variant.handle ((int32) get_int64_from_entry ()); default: assert_not_reached (); } } private int64 get_int64_from_entry () { return int64.parse (this.get_text ()); } private void set_lock (bool state) requires (deleted_text_handler != 0 && inserted_text_handler != 0) { if (state) { SignalHandler.block (buffer, deleted_text_handler); SignalHandler.block (buffer, inserted_text_handler); } else { SignalHandler.unblock (buffer, deleted_text_handler); SignalHandler.unblock (buffer, inserted_text_handler); } } public void reload (Variant gvariant) // TODO "key_editor_child_number_int_real_reload: assertion 'gvariant != NULL' failed" two times when ghosting a key { set_lock (true); this.set_value (get_variant_as_double (gvariant)); set_lock (false); } } private class KeyEditorChildArray : Grid, KeyEditorChild { private TextView text_view; private Revealer error_revealer; private string key_type; private Variant variant; private ulong deleted_text_handler = 0; private ulong inserted_text_handler = 0; construct { get_style_context ().add_class ("key-editor-child-array"); } public KeyEditorChildArray (string type_string, Variant initial_value) { this.visible = true; this.hexpand = true; this.vexpand = false; orientation = Orientation.VERTICAL; get_style_context ().add_class ("frame"); this.key_type = type_string; this.variant = initial_value; ScrolledWindow scrolled_window = new ScrolledWindow (null, null); scrolled_window.visible = true; text_view = new TextView (); text_view.visible = true; text_view.expand = true; text_view.wrap_mode = WrapMode.WORD; text_view.monospace = true; text_view.key_press_event.connect ((event) => { string keyval_name = (!) (Gdk.keyval_name (event.keyval) ?? ""); if ((keyval_name == "Return" || keyval_name == "KP_Enter") && ((event.state & Gdk.ModifierType.MODIFIER_MASK) == 0) && (test_value ())) { child_activated (); return true; } return base.key_press_event (event); }); // https://bugzilla.gnome.org/show_bug.cgi?id=789676 text_view.button_press_event.connect_after (() => Gdk.EVENT_STOP); text_view.button_release_event.connect_after (() => Gdk.EVENT_STOP); scrolled_window.add (text_view); add (scrolled_window); error_revealer = new Revealer (); error_revealer.visible = true; error_revealer.transition_type = RevealerTransitionType.SLIDE_UP; error_revealer.reveal_child = false; add (error_revealer); ActionBar error_bar = new ActionBar (); error_bar.visible = true; error_revealer.add (error_bar); Image error_icon = new Image.from_icon_name ("dialog-error-symbolic", IconSize.BUTTON); error_icon.visible = true; error_bar.pack_start (error_icon); Label error_label = new Label (_("This value is invalid for the key type.")); error_label.visible = true; error_bar.pack_start (error_label); text_view.buffer.text = initial_value.print (false); TextBuffer ref_buffer = text_view.buffer; // an TextBuffer doesn't emit a "destroy" signal deleted_text_handler = ref_buffer.delete_range.connect_after (() => value_has_changed (test_value ())); inserted_text_handler = ref_buffer.insert_text.connect_after (() => value_has_changed (test_value ())); destroy.connect (() => { ref_buffer.disconnect (deleted_text_handler); ref_buffer.disconnect (inserted_text_handler); }); } private bool test_value () { string tmp_text = text_view.buffer.text; // don't put in the try{} for correct C code try { Variant? tmp_variant = Variant.parse (new VariantType (key_type), tmp_text); variant = (!) tmp_variant; StyleContext context = get_style_context (); if (context.has_class ("error")) context.remove_class ("error"); error_revealer.reveal_child = false; return true; } catch (VariantParseError e) { StyleContext context = get_style_context (); if (!context.has_class ("error")) context.add_class ("error"); error_revealer.reveal_child = true; return false; } } public Variant get_variant () { return variant; } private void set_lock (bool state) requires (deleted_text_handler != 0 && inserted_text_handler != 0) { if (state) { SignalHandler.block (text_view.buffer, deleted_text_handler); SignalHandler.block (text_view.buffer, inserted_text_handler); } else { SignalHandler.unblock (text_view.buffer, deleted_text_handler); SignalHandler.unblock (text_view.buffer, inserted_text_handler); } } public void reload (Variant gvariant) { set_lock (true); text_view.buffer.text = gvariant.print (false); if (!test_value ()) assert_not_reached (); set_lock (false); } } private class KeyEditorChildDefault : Entry, KeyEditorChild { private string key_type; private Variant variant; private bool is_string; private ulong deleted_text_handler = 0; private ulong inserted_text_handler = 0; construct { get_style_context ().add_class ("key-editor-child-entry"); } public KeyEditorChildDefault (string type_string, Variant initial_value) { this.key_type = type_string; this.variant = initial_value; this.visible = true; this.hexpand = true; this.secondary_icon_activatable = false; this.set_icon_tooltip_text (EntryIconPosition.SECONDARY, _("This value is invalid for the key type.")); this.is_string = type_string == "s" || type_string == "o" || type_string == "g"; this.text = is_string ? initial_value.get_string () : initial_value.print (false); EntryBuffer ref_buffer = buffer; // an EntryBuffer doesn't emit a "destroy" signal deleted_text_handler = ref_buffer.deleted_text.connect (() => value_has_changed (test_value ())); inserted_text_handler = ref_buffer.inserted_text.connect (() => value_has_changed (test_value ())); ulong entry_activate_handler = activate.connect (() => { if (test_value ()) child_activated (); }); destroy.connect (() => { ref_buffer.disconnect (deleted_text_handler); ref_buffer.disconnect (inserted_text_handler); disconnect (entry_activate_handler); }); } private bool test_value () { if (key_type == "s") { variant = new Variant.string (this.text); return true; } string tmp_text = is_string ? @"'$text'" : this.text; // don't put in the try{} for correct C code try { Variant? tmp_variant = Variant.parse (new VariantType (key_type), tmp_text); variant = (!) tmp_variant; StyleContext context = get_style_context (); if (context.has_class ("error")) context.remove_class ("error"); set_icon_from_icon_name (EntryIconPosition.SECONDARY, null); return true; } catch (VariantParseError e) { StyleContext context = get_style_context (); if (!context.has_class ("error")) context.add_class ("error"); secondary_icon_name = "dialog-error-symbolic"; return false; } } public Variant get_variant () { return variant; } private void set_lock (bool state) requires (deleted_text_handler != 0 && inserted_text_handler != 0) { if (state) { SignalHandler.block (buffer, deleted_text_handler); SignalHandler.block (buffer, inserted_text_handler); } else { SignalHandler.unblock (buffer, deleted_text_handler); SignalHandler.unblock (buffer, inserted_text_handler); } } public void reload (Variant gvariant) { set_lock (true); this.text = is_string ? gvariant.get_string () : gvariant.print (false); if (!test_value ()) assert_not_reached (); set_lock (false); } }