diff --git a/docs/releases/upcoming/1705.bugfix.rst b/docs/releases/upcoming/1705.bugfix.rst new file mode 100644 index 000000000..35b02242e --- /dev/null +++ b/docs/releases/upcoming/1705.bugfix.rst @@ -0,0 +1 @@ +Add support for {enabled/visible}_when on Tabbed and VFold groups (#1705) \ No newline at end of file diff --git a/traitsui/qt4/ui_panel.py b/traitsui/qt4/ui_panel.py index eef8cf91b..ca8658928 100644 --- a/traitsui/qt4/ui_panel.py +++ b/traitsui/qt4/ui_panel.py @@ -28,7 +28,7 @@ from pyface.qt import QtCore, QtGui -from traits.api import Any, HasPrivateTraits, Instance, Undefined +from traits.api import Any, HasPrivateTraits, Instance, List, Undefined from traits.observation.api import match from traitsui.api import Group @@ -289,8 +289,16 @@ def panel(ui): return panel -def _fill_panel(panel, content, ui, item_handler=None): - """Fill a page based container panel with content.""" +def _fill_panel( + panel, + content, + ui, + item_handler=None, + _visible_when_groups=None, + _enabled_when_groups=None +): + """Fill a page based container panel with content. + """ active = 0 for index, item in enumerate(content): @@ -332,9 +340,14 @@ def _fill_panel(panel, content, ui, item_handler=None): # Add the content. if isinstance(panel, QtGui.QTabWidget): - panel.addTab(new, page_name) + idx = panel.addTab(new, page_name) else: - panel.addItem(new, page_name) + idx = panel.addItem(new, page_name) + + if item.visible_when and (_visible_when_groups is not None): + _visible_when_groups.append((item.visible_when, idx, new, page_name)) + if item.enabled_when and (_enabled_when_groups is not None): + _enabled_when_groups.append((item.enabled_when, idx, new, page_name)) panel.setCurrentIndex(active) @@ -570,8 +583,16 @@ def __init__(self, group, ui, suppress_label=False): policy.setHorizontalStretch(50) policy.setVerticalStretch(50) sub.setSizePolicy(policy) - - _fill_panel(sub, content, self.ui, self._add_page_item) + _visible_when_groups = [] + _enabled_when_groups = [] + _fill_panel( + sub, + content, + self.ui, + self._add_page_item, + _visible_when_groups, + _enabled_when_groups + ) if outer is None: outer = sub @@ -579,7 +600,13 @@ def __init__(self, group, ui, suppress_label=False): inner.addWidget(sub) # Create an editor. - editor = TabbedFoldGroupEditor(container=sub, control=outer, ui=ui) + editor = TabbedFoldGroupEditor( + container=sub, + control=outer, + ui=ui, + _visible_when_groups=_visible_when_groups, + _enabled_when_groups=_enabled_when_groups + ) self._setup_editor(group, editor) else: @@ -1287,6 +1314,101 @@ class TabbedFoldGroupEditor(GroupEditor): #: The QTabWidget or QToolBox for the group container = Any() + _visible_when_groups = List() + _enabled_when_groups = List() + + def __init__(self, **traits): + """ Initialise the object. + """ + super().__init__(**traits) + num_enabled_or_visible_whens = ( + len(self._visible_when_groups) + len(self._enabled_when_groups) + ) + if num_enabled_or_visible_whens > 0: + for object in self.ui.context.values(): + object.on_trait_change( + lambda: self._when(), dispatch="ui" + ) + self._when() + + def _when(self): + """Set all tabs in the editor to be enabled/visible as + controlled by a 'visible_when' or 'enabled_when' expression. + """ + self._evaluate_enabled_condition(self._enabled_when_groups) + self._evaluate_visible_condition(self._visible_when_groups) + + def _evaluate_enabled_condition(self, conditions): + """Evaluates a list of (eval, widget) pairs and calls the + appropriate method on the widget to toggle whether it is + enabled as needed. + """ + context = self.ui._get_context(self.ui.context) + + if isinstance(self.container, QtGui.QTabWidget): + method_to_call_name = "setTabEnabled" + elif isinstance(self.container, QtGui.QToolBox): + method_to_call_name = "setItemEnabled" + else: + raise TypeError( + "container of a TabbedFoldGroupEditor must be either a " + "QTabWidget or a QToolBox" + ) + + for when, idx, widget, label in conditions: + method_to_call = getattr(self.container, method_to_call_name) + try: + cond_value = eval(when, globals(), context) + method_to_call(idx, cond_value) + except Exception: + # catch errors in the validate_when expression + from traitsui.api import raise_to_debug + + raise_to_debug() + + def _evaluate_visible_condition(self, conditions): + """Evaluates a list of (eval, widget) pairs and calls the + appropriate method on the tab widget to toggle whether it is + visible as needed. + """ + context = self.ui._get_context(self.ui.context) + + if isinstance(self.container, QtGui.QTabWidget): + tab_or_item = "Tab" + elif isinstance(self.container, QtGui.QToolBox): + tab_or_item = "Item" + else: + raise TypeError( + "container of a TabbedFoldGroupEditor must be either a " + "QTabWidget or a QToolBox" + ) + + for when, idx, widget, label in conditions: + + try: + cond_value = eval(when, globals(), context) + if cond_value: + method_to_call_name = "insert" + tab_or_item + method_to_call = getattr( + self.container, method_to_call_name + ) + # check that the tab for this widget is not already showing + if self.container.indexOf(widget) == -1: + method_to_call(idx, widget, label) + else: + method_to_call_name = "remove" + tab_or_item + method_to_call = getattr( + self.container, method_to_call_name + ) + # check that the tab for this widget is already showing + if self.container.indexOf(widget) != -1: + method_to_call(idx) + except Exception: + # catch errors in the validate_when expression + from traitsui.api import raise_to_debug + + raise_to_debug() + # -- UI preference save/restore interface --------------------------------- def restore_prefs(self, prefs): diff --git a/traitsui/tests/test_group.py b/traitsui/tests/test_group.py new file mode 100644 index 000000000..496063729 --- /dev/null +++ b/traitsui/tests/test_group.py @@ -0,0 +1,159 @@ +# (C) Copyright 2004-2021 Enthought, Inc., Austin, TX +# All rights reserved. +# +# This software is provided without warranty under the terms of the BSD +# license included in LICENSE.txt and may be redistributed only under +# the conditions described in the aforementioned license. The license +# is also available online at http://www.enthought.com/licenses/BSD.txt +# +# Thanks for using Enthought open source! + +import unittest + +from traits.api import Float, HasTraits, Int + +from traitsui.api import Item, Tabbed, VFold, VGroup, View +from traitsui.testing.api import KeyClick, UITester +from traitsui.tests._tools import requires_toolkit, ToolkitName + + +class Foo(HasTraits): + a = Int() + b = Float() + + +def get_view(group_type, enabled_visible): + if enabled_visible == "enabled": + return View( + group_type( + VGroup( + Item('a'), + label='Fold #1', + enabled_when='object.b != 0.0', + id="first_fold" + ), + VGroup( + Item('b'), + label='Fold #2', + id="second_fold" + ), + id="interesting_group" + ) + ) + else: + return View( + group_type( + VGroup( + Item('a'), + label='Fold #1', + visible_when='object.b != 0.0', + id="first_fold" + ), + VGroup( + Item('b'), + label='Fold #2', + id="second_fold" + ), + id="interesting_group" + ) + ) + + +class TestTabbed(unittest.TestCase): + + # regression test for enthought/tratsui#758 + @requires_toolkit([ToolkitName.qt]) + def test_visible_when(self): + tabbed_visible = Foo() + view = get_view(Tabbed, "visible") + tester = UITester() + + with tester.create_ui(tabbed_visible, dict(view=view)) as ui: + tabbed_fold_group_editor = tester.find_by_id( + ui, "interesting_group" + )._target + q_tab_widget = tabbed_fold_group_editor.container + # only Tab#2 is available at first + self.assertEqual(q_tab_widget.count(), 1) + + # change b to != 0.0 so Tab #1 is visible + b_field = tester.find_by_name(ui, 'b') + b_field.perform(KeyClick("1")) + b_field.perform(KeyClick("Enter")) + + self.assertEqual(q_tab_widget.count(), 2) + + # regression test for enthought/tratsui#758 + @requires_toolkit([ToolkitName.qt]) + def test_enabled_when(self): + tabbed_enabled = Foo() + view = get_view(Tabbed, "enabled") + tester = UITester() + + with tester.create_ui(tabbed_enabled, dict(view=view)) as ui: + tabbed_fold_group_editor = tester.find_by_id( + ui, "interesting_group" + )._target + q_tab_widget = tabbed_fold_group_editor.container + # both tabs exist + self.assertEqual(q_tab_widget.count(), 2) + # but first is disabled + self.assertFalse(q_tab_widget.isTabEnabled(0)) + + # change b to != 0.0 so Tab #1 is enabled + b_field = tester.find_by_name(ui, 'b') + b_field.perform(KeyClick("1")) + b_field.perform(KeyClick("Enter")) + + self.assertEqual(q_tab_widget.count(), 2) + self.assertTrue(q_tab_widget.isTabEnabled(0)) + + +class TestVFold(unittest.TestCase): + + # regression test for enthought/tratsui#758 + @requires_toolkit([ToolkitName.qt]) + def test_visible_when(self): + fold_visible = Foo() + view = get_view(VFold, "visible") + tester = UITester() + + with tester.create_ui(fold_visible, dict(view=view)) as ui: + tabbed_fold_group_editor = tester.find_by_id( + ui, "interesting_group" + )._target + q_tool_box = tabbed_fold_group_editor.container + # only Fold #2 is available at first + self.assertEqual(q_tool_box.count(), 1) + + # change b to != 0.0 so Fold #1 is visible + b_field = tester.find_by_name(ui, 'b') + b_field.perform(KeyClick("1")) + b_field.perform(KeyClick("Enter")) + + self.assertEqual(q_tool_box.count(), 2) + + # regression test for enthought/tratsui#758 + @requires_toolkit([ToolkitName.qt]) + def test_enabled_when(self): + fold_enabled = Foo() + view = get_view(VFold, "enabled") + tester = UITester() + + with tester.create_ui(fold_enabled, dict(view=view)) as ui: + tabbed_fold_group_editor = tester.find_by_id( + ui, "interesting_group" + )._target + q_tool_box = tabbed_fold_group_editor.container + # both folds exist + self.assertEqual(q_tool_box.count(), 2) + # but first is disabled + self.assertFalse(q_tool_box.isItemEnabled(0)) + + # change b to != 0.0 so Fold #1 is enabled + b_field = tester.find_by_name(ui, 'b') + b_field.perform(KeyClick("1")) + b_field.perform(KeyClick("Enter")) + + self.assertEqual(q_tool_box.count(), 2) + self.assertTrue(q_tool_box.isItemEnabled(0))