Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for {enabled/visible}_when for Tabbed and VFold #1705

Open
wants to merge 21 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/releases/upcoming/1705.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add support for {enabled/visible}_when on Tabbed and VFold groups (#1705)
138 changes: 130 additions & 8 deletions traitsui/qt4/ui_panel.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -570,16 +583,30 @@ 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
else:
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:
Expand Down Expand Up @@ -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:
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

theoretically, if the widget is visible, idx == self.container.indexOf(widget). However, perhaps the tab could change its index during the lifetime of the tab widget, in which case we should call method_to_call(self.container.indexOf(widget)) instead?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that this comment raises a significant issue that needs to be considered: the index of the widget in the Tabbed group doesn't necessarily match the index of the Qt tab, because visibility is controlled by adding/removing the widget from the container.

I think things will go wrong in situations like the following:

  • have tabs T0, T1, T2, T3, all initially visible
  • T1 is hidden, so we get T0, T2, T3, but now the indices of T2 and T3 at the Qt level are 1 and 2, but at the TraitsUI level are 2 and 3.
  • If we hide T2 in this state, the current code will actually hide T3 (and hiding T3 will result in an error?)
  • After hiding T2 we have T0 and T2 at the Qt level. If we un-hide T2, then it will try to insert it at Qt index 2 with the current code, and so we would get no change as it is already showing

I think there are situations where you can get things showing up in pretty much arbitrary order (eg. I think hide T2, hide T1, show T2 ends up with T0, T3, T2 as the layout).

I could be missing something about indices are being computed, but it looks like you can't assume that TraitsUI and Qt-level indices match, and computing the right Qt level index for re-insertion needs some thought (I think we need to work out how many TraitsUI tabs before the tab of interest are visible).

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):
Expand Down
159 changes: 159 additions & 0 deletions traitsui/tests/test_group.py
Original file line number Diff line number Diff line change
@@ -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))