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
Changes from 4 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
131 changes: 122 additions & 9 deletions traitsui/qt4/ui_panel.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,7 @@

from pyface.qt import QtCore, QtGui

from traits.api import Any, HasPrivateTraits, Instance, Undefined
from traits.observation.api import match
from traits.api import Any, HasPrivateTraits, Instance, List, Undefined

from traitsui.api import Group

Expand Down Expand Up @@ -293,7 +292,14 @@ def panel(ui):
return panel


def _fill_panel(panel, content, ui, item_handler=None):
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
Expand All @@ -306,7 +312,6 @@ def _fill_panel(panel, content, ui, item_handler=None):
if isinstance(item, Group):
if item.selected:
active = index

gp = _GroupPanel(item, ui, suppress_label=True)
page = gp.control
sub_page = gp.sub_control
Expand Down Expand Up @@ -337,9 +342,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 @@ -582,16 +592,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 @@ -1300,6 +1324,95 @@ 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 the visible and enabled states of all tabs in the editor 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

for when, widget, idx 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

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 ---------------------------------

Expand Down