diff --git a/Nasal/canvas/gui.nas b/Nasal/canvas/gui.nas index 72592b4d8..c1c228652 100644 --- a/Nasal/canvas/gui.nas +++ b/Nasal/canvas/gui.nas @@ -46,6 +46,7 @@ loadWidget("MenuBar"); loadWidget("PropertyTree"); loadWidget("PropertyWidgets"); loadWidget("ScrollArea"); +loadWidget("RadioButton"); loadWidget("Rule"); loadWidget("Slider"); loadWidget("TabWidget"); diff --git a/Nasal/canvas/gui/dialogs/WidgetsFactoryDialog.nas b/Nasal/canvas/gui/dialogs/WidgetsFactoryDialog.nas index fa3a9859e..f3762a98e 100644 --- a/Nasal/canvas/gui/dialogs/WidgetsFactoryDialog.nas +++ b/Nasal/canvas/gui/dialogs/WidgetsFactoryDialog.nas @@ -11,7 +11,6 @@ var WidgetsFactoryDialog = { .set("background", style.getColor("bg_color")) .createGroup(); m.vbox = VBoxLayout.new(); - #m.vbox.setContentsMargin(10); m.window.setLayout(m.vbox); m.menubar = canvas.gui.widgets.MenuBar.new(m.root, canvas.style, {}); @@ -22,12 +21,18 @@ var WidgetsFactoryDialog = { tabsMenu.createItem(text: "Select first tab", cb: func m.tabs.setCurrentTab("tab-1")); tabsMenu.createItem(text: "Select second tab", cb: func m.tabs.setCurrentTab("tab-2")); - m.menubar.createMenu("Widgets") - .createItem(text: "Benchmark label", cb: func { - m.benchmark_widget(canvas.gui.widgets.Label, func(w, i) { - w.setText("Label " ~ i); - }); - }); + var widgetsMenu = m.menubar.createMenu("Widgets"); + widgetsMenu.createItem(text: "Benchmark label", cb: func { + m.benchmark_widget(canvas.gui.widgets.Label, func(w, i) { + w.setText("Label " ~ i); + }); + }); + + widgetsMenu.createItem(text: "Benchmark radio button", cb: func { + m.benchmark_radio_button(func(w, i) { + w.setText("Radio button " ~ i); + }); + }); m.vbox.addItem(m.menubar); m.tabs = gui.widgets.TabWidget.new(m.root, style, {}); @@ -45,11 +50,11 @@ var WidgetsFactoryDialog = { r.setText("Checkboxes!"); m.tab_1.addItem(r); - m.checkbox_left = gui.widgets.CheckBox.new(m.tabsContent, style, {"label-position": "right"}) + m.checkbox_left = gui.widgets.CheckBox.new(m.tabsContent, style, {}) .setText("Wanna check something ?"); m.tab_1.addItem(m.checkbox_left); - m.checkbox_right = gui.widgets.CheckBox.new(m.tabsContent, style, {"label-position": "right"}) - .setText("Checkbox with text on the right side"); + m.checkbox_right = gui.widgets.CheckBox.new(m.tabsContent, style, {"label-position": "left"}) + .setText("Checkbox with text on the left side"); m.tab_1.addItem(m.checkbox_right); m.property_checkbox = gui.widgets.PropertyCheckBox.new(props.globals.getNode("/controls/lighting/nav-lights"), m.tabsContent, style, {}) .setText("Nav lights"); @@ -57,6 +62,19 @@ var WidgetsFactoryDialog = { var r2 = gui.widgets.HorizontalRule.new(m.tabsContent, style, {}); m.tab_1.addItem(r2); + + m.radio1 = gui.widgets.RadioButton.new(m.tabsContent) + .setText("Radio button 1"); + m.tab_1.addItem(m.radio1); + m.radio2 = gui.widgets.RadioButton.new(parent: m.tabsContent, cfg: {parentRadio: m.radio1}) + .setText("Radio button 2"); + m.tab_1.addItem(m.radio2); + m.radio3 = gui.widgets.RadioButton.new(parent: m.tabsContent, cfg: {parentRadio: m.radio1}) + .setText("Radio button 3"); + m.tab_1.addItem(m.radio3); + m.radio4 = gui.widgets.RadioButton.new(parent: m.tabsContent, cfg: {parentRadio: m.radio1}) + .setText("Radio button 4"); + m.tab_1.addItem(m.radio4); m.tab_2 = HBoxLayout.new(); m.tabs.addTab("tab-2", "Tab 2", m.tab_2); @@ -145,6 +163,7 @@ var WidgetsFactoryDialog = { m.benchmark_tab_scroll = canvas.gui.widgets.ScrollArea.new(m.tabsContent, canvas.style, {}); m.benchmark_tab_scroll.setSizeHint([m.list._MAX_SIZE, m.list._MAX_SIZE]); m.benchmark_tab_scroll_layout = VBoxLayout.new(); + m.benchmark_tab_scroll_layout.setSpacing(0); m.benchmark_tab_scroll.setLayout(m.benchmark_tab_scroll_layout); m.benchmark_tab.addItem(m.benchmark_tab_scroll); m.benchmark_statistics = canvas.gui.widgets.Label.new(m.tabsContent, canvas.style, {}); @@ -185,6 +204,21 @@ var WidgetsFactoryDialog = { me.benchmark_statistics.setText("Took " ~ time ~ " seconds to add " ~ amount ~ " widgets."); }, + benchmark_radio_button: func(proc_func=nil, amount=50) { + var start = systime(); + me.benchmark_tab_scroll_layout.clear(); + var r = canvas.gui.widgets.RadioButton.new(me.benchmark_tab_scroll.getContent()); + for (var i = 1; i < amount; i += 1) { + var w = canvas.gui.widgets.RadioButton.new(me.benchmark_tab_scroll.getContent(), canvas.style, {parentRadio: r}); + if (proc_func != nil) { + proc_func(w, i); + } + me.benchmark_tab_scroll_layout.addItem(w); + } + var time = systime() - start; + me.benchmark_statistics.setText("Took " ~ time ~ " seconds to add " ~ amount ~ " widgets."); + }, + del: func { me.property_checkbox.del(); me.window.del(); diff --git a/Nasal/canvas/gui/styles/DefaultStyle.nas b/Nasal/canvas/gui/styles/DefaultStyle.nas index 17640f479..ebd1f340e 100644 --- a/Nasal/canvas/gui/styles/DefaultStyle.nas +++ b/Nasal/canvas/gui/styles/DefaultStyle.nas @@ -176,6 +176,73 @@ DefaultStyle.widgets.checkbox = { } }; +# A checkbox +DefaultStyle.widgets["radio-button"] = { + new: func(parent, cfg) { + me._root = parent.createChild("group", "radio-button"); + me._bg = me._root.createChild("path"); + me._icon = me._root.createChild("group", "radio-button-icon"); + me._icon_background = me._icon.createChild("path", "radio-button-icon-border") + .circle(8.5, 9, 9); + me._icon_selected_indicator = me._icon.createChild("path", "radio-button-icon-selected-indicator") + .circle(6, 9, 9) + .set("stroke-width", 5); + me._icon_border = me._icon.createChild("path", "radio-button-icon-border") + .circle(8, 9, 9) + .set("stroke-width", 1); + me._label = me._root.createChild("text") + .set("font", "LiberationFonts/LiberationSans-Regular.ttf") + .set("character-size", 14) + .set("alignment", "left-center"); + }, + setSize: func(model, w, h) { + me._bg.reset().rect(0, 0, w, h); + me._icon.setTranslation(3, int((h - 18) / 2)); + me._label.setTranslation(24, int(h / 2) + 1); + + return me; + }, + setText: func(model, text) { + me._label.setText(text); + + var min_width = me._label.maxWidth() + 3 + 24; + model.setLayoutMinimumSize([min_width, 24]); + model.setLayoutSizeHint([min_width, 24]); + + return me; + }, + update: func(model) { + var backdrop = !model._windowFocus(); + + me._bg.set("fill", me._style.getColor("radio_button_bg_color" ~ (model._hover ? "_hovered" : ""))); + + me._icon_border.set("stroke", me._style.getColor("radio_button_selected_indicator_border_color")); + if (backdrop) { + me._label.set("fill", me._style.getColor("backdrop_fg_color")); + } else { + me._label.set("fill", me._style.getColor("fg_color")); + } + + if (model._checked) { + me._icon_selected_indicator.show(); + } else { + me._icon_selected_indicator.hide(); + } + + if (model._enabled) { + if (model._hover) { + me._icon_background.set("fill", me._style.getColor("radio_button_selected_indicator_bg_color_hovered")); + } else { + me._icon_background.set("fill", me._style.getColor("radio_button_selected_indicator_bg_color")); + } + me._icon_selected_indicator.set("stroke", me._style.getColor("radio_button_selected_indicator_color")); + } else { + me._icon_background.set("fill", me._style.getColor("radio_button_selected_indicator_bg_color_disabled")); + me._icon_selected_indicator.set("stroke", me._style.getColor("radio_button_selected_indicator_color_disabled")); + } + } +}; + # A label DefaultStyle.widgets.label = { new: func(parent, cfg) diff --git a/Nasal/canvas/gui/widgets/Button.nas b/Nasal/canvas/gui/widgets/Button.nas index ce70d27bd..ef9afd364 100644 --- a/Nasal/canvas/gui/widgets/Button.nas +++ b/Nasal/canvas/gui/widgets/Button.nas @@ -5,17 +5,17 @@ gui.widgets.Button = { new: func(parent, style, cfg) { - var cfg = Config.new(cfg); var m = gui.Widget.new(gui.widgets.Button); + m._cfg = Config.new(cfg); m._focus_policy = m.StrongFocus; m._down = 0; m._checkable = 0; - m._flat = cfg.get("flat", 0); - m._isDefault = cfg.get("default", 0); - m._destructive = cfg.get("destructive", 0); + m._flat = m._cfg.get("flat", 0); + m._isDefault = m._cfg.get("default", 0); + m._destructive = m._cfg.get("destructive", 0); if( style != nil and !m._flat ) - m._setView( style.createWidget(parent, cfg.get("type", "button"), cfg) ); + m._setView( style.createWidget(parent, m._cfg.get("type", "button"), m._cfg) ); return m; }, diff --git a/Nasal/canvas/gui/widgets/RadioButton.nas b/Nasal/canvas/gui/widgets/RadioButton.nas index 18d17840f..371317fa3 100644 --- a/Nasal/canvas/gui/widgets/RadioButton.nas +++ b/Nasal/canvas/gui/widgets/RadioButton.nas @@ -1,95 +1,113 @@ # RadioButton.nas : radio button, and group helper # to manage updating checked state conherently -# SPDX-FileCopyrightText: (C) 2022 James Turner +# SPDX-FileCopyrightText: (C) 2022 James Turner , Frederic Croix # SPDX-License-Identifier: GPL-2.0-or-later gui.widgets.RadioButton = { - new: func(parent, style, cfg) - { - cfg["type"] = "radio"; - var m = gui.widgets.Button.new(parent, style, cfg); - m._checkable = 1; + new: func(parent, style = nil, cfg = nil) { + var m = gui.Widget.new(gui.widgets.RadioButton); + style = style or canvas.style; + m._cfg = Config.new(cfg or {}); + m._focus_policy = m.StrongFocus; + m._checked = 0; + m.radioGroup = nil; - append(m.parents, gui.widgets.RadioButton); + m._setView( style.createWidget(parent, m._cfg.get("type", "radio-button"), m._cfg) ); - if (contains(pr, "radioGroup") { - pr.radioGroup.addRadio(m); + var parentRadio = m._cfg.get("parentRadio", nil); + if (parentRadio != nil) { + m.radioGroup = parentRadio.radioGroup; + } else { + m.radioGroup = gui.widgets.RadioButtonsGroup.new(); } + m.radioGroup.addRadio(m); return m; }, - del: func() - { - var pr = getParent(); - if (contains(pr, "radioGroup") { - pr.radioGroup.removeRadio(me); - } + setText: func(text) { + me._view.setText(me, text); + return me; }, - setCheckable: nil, - setChecked: func(checked = 1) - { - # call our base version - me.parents[0].setChecked(checked); - if (checked) { - me._setRadioGroupSiblingsUnchecked(); + setChecked: func(checked = 1) { + if (me._checked == checked) { + return me; } + + me._setRadioGroupSiblingsUnchecked(); + me._trigger("toggled", {checked: checked}); + me._checked = checked; + me._onStateChange(); + return me; + }, + + _setRadioGroupSiblingsUnchecked: func { + me.radioGroup._updateChecked(me); + }, + + toggle: func { + me.setChecked(!me._checked); + return me; + }, + + _setView: func(view) { + call(gui.Widget._setView, [view], me); + + var el = view._root; + el.addEventListener("click", func { + if (me._enabled) { + me.setChecked() + } + }); + + el.addEventListener("drag", func(e) e.stopPropagation()); }, -# protected members - _setRadioGroupSiblingsUnchecked: func - { - var pr = getParent(); - if (contains(pr, "radioGroup") { - pr.radioGroup._updateChecked(me); - } else { - # todo, remove me, it's okay to manually manage RadioButtons - logprint(LOG_DEV, "No radio group defined"); - } - } }; # auto manage radio-button checked state -gui.widgets.RadioGroup = { - new: func(nm = 'unnamed') +gui.widgets.RadioButtonsGroup = { + new: func(name = "unnamed") { - var m = {parents:[gui.widgets.RadioGroup, name:nm, radios:[]]}; + var m = { + parents: [gui.widgets.RadioButtonsGroup], + name: name, + radios: [], + }; return m; }, addRadio: func(r) { - if (r.parents[0] != RadioButton) { - logprint(LOG_ALERT, "Adding non-RadioButton to RadioGroup") - return; - } + r.listen("toggled", func(e) { + me._updateChecked(r); + }); append(me.radios, r); }, removeRadio: func(r) { - if (r.parents[0] != RadioButton) { - logprint(LOG_ALERT, "Remove non-RadioButton from RadioGroup") - return; + # XXX should we update some other item to be checked ? + if (contains(me.radios, r)) { + var index = find(r, me.radios); + me.radios = subvec(me.radios, 0, index) ~ subvec(me.radios, index + 1); } - - # should we update some other item to be checked? - me.radios.remove(r); }, - setEnabled: func(doEnable = 1) + setEnabled: func(enabled = 1) { - foreach (var r : me.radios) { - r.setEnabled(doEnable); + foreach (var r; me.radios) { + r.setEnabled(enabled); } }, -# protected methods # update check state of all radios in the group _updateChecked: func(active) { - foreach (var r : me.radios) { - r.setChecked(r == active); + foreach (var r; me.radios) { + if (r != active) { + r.setChecked(0); + } } - } -}; \ No newline at end of file + }, +}; diff --git a/gui/styles/AmbianceClassic/style.xml b/gui/styles/AmbianceClassic/style.xml index bb8d92c34..f4d4993f4 100644 --- a/gui/styles/AmbianceClassic/style.xml +++ b/gui/styles/AmbianceClassic/style.xml @@ -209,5 +209,53 @@ 0.15 1 + + + 0.949 + 0.945 + 0.941 + + + + 0.989 + 0.985 + 0.981 + + + + 1 + 1 + 1 + + + + 0.95 + 0.95 + 0.95 + + + + 0.9 + 0.9 + 0.9 + + + + 0.5 + 0.5 + 0.5 + + + + 0.25 + 0.25 + 1 + + + + 0.5 + 0.5 + 1 +