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

[WIP] Feature/advanced tuple editor #182

Open
wants to merge 17 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 16 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
110 changes: 49 additions & 61 deletions traitsui/editors/tuple_editor.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,40 +16,25 @@
""" Defines the tuple editor factory for all traits user interface toolkits.
"""

#-------------------------------------------------------------------------
# Imports:
#-------------------------------------------------------------------------

from __future__ import absolute_import

from traits.trait_base import SequenceTypes

from traits.api import Bool, HasTraits, List, Tuple, Unicode, Int, Any, TraitType
from traits.api import (
Bool, Callable, HasTraits, List, BaseTuple, Unicode, Int, Any, TraitType)

# CIRCULAR IMPORT FIXME: Importing from the source rather than traits.ui.api
# to avoid circular imports, as this EditorFactory will be part of
# traits.ui.api as well.
from ..view import View

from ..group import Group

from ..item import Item

from ..editor_factory import EditorFactory

from ..editor import Editor

#-------------------------------------------------------------------------
# 'ToolkitEditorFactory' class:
#-------------------------------------------------------------------------


class ToolkitEditorFactory(EditorFactory):
""" Editor factory for tuple editors.
"""
#-------------------------------------------------------------------------
# Trait definitions:
#-------------------------------------------------------------------------

# Trait definitions for each tuple field
types = Any
Expand All @@ -73,9 +58,10 @@ class ToolkitEditorFactory(EditorFactory):
# 'enter_set' metadata or an editor defined.
enter_set = Bool(False)

#-------------------------------------------------------------------------
# 'SimpleEditor' class:
#-------------------------------------------------------------------------
# The validation function to use for the Tuple. If the edited trait offers
# already a validation function then the value of this trait will be
Copy link
Contributor

Choose a reason for hiding this comment

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

This should be the other way around (unless you have a particular use-case in mind). Because one HasTraits class can potentially have many different views, you generally want thing specified in the editor to override things specified in traits.

Although not exactly analogous, compare with (note that the label in the Item wins):

from traits.api import HasTraits, Float
from traitsui.api import View, Item


class Test(HasTraits):

    x = Float(label='Variable x')

    view = View(Item('x', label='Var x'))

t = Test()
t.edit_traits()

Copy link
Member Author

@itziakos itziakos Oct 13, 2016

Choose a reason for hiding this comment

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

As you say it is not analogous. Since there is no restriction on the label. For example if the Editor validation overrides the trait validation it can potentially result in an invalid value been forced into a trait which will throw an exception.

Copy link
Member Author

@itziakos itziakos Oct 13, 2016

Choose a reason for hiding this comment

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

Thinking about it a little more, maybe a compromise would be the union of the trait validation and editor validation.

At the end the problem is about who is responsible for ultimate constrains in the values (not the representation) the traits model or the traits ui view

# ignored.
fvalidate = Callable
Copy link
Contributor

@corranwebster corranwebster Oct 13, 2016

Choose a reason for hiding this comment

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

Any reason why fvalidate instead of just validate or validator?

Copy link
Member Author

@itziakos itziakos Oct 13, 2016

Choose a reason for hiding this comment

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

It has been a while... So initially I had it as validator but then I changed it to fvalidate to be similar to the Property fget, fset, fvalidate



class SimpleEditor(Editor):
Expand All @@ -84,53 +70,37 @@ class SimpleEditor(Editor):
The editor displays an editor for each of the fields in the tuple, based on
the type of each field.
"""
#-------------------------------------------------------------------------
# Finishes initializing the editor by creating the underlying toolkit
# widget:
#-------------------------------------------------------------------------

def init(self, parent):
""" Finishes initializing the editor by creating the underlying toolkit
widget.
"""
self._ts = ts = TupleStructure(self)
self._ui = ui = ts.view.ui(ts, parent, kind='subpanel').set(
parent=self.ui)
self._ui = ui = ts.view.ui(
ts, parent, kind='subpanel').set(parent=self.ui)
self.control = ui.control
self.set_tooltip()

#-------------------------------------------------------------------------
# Updates the editor when the object trait changes external to the editor:
#-------------------------------------------------------------------------

def update_editor(self):
""" Updates the editor when the object trait changes external to the
editor.
"""
ts = self._ts
for i, value in enumerate(self.value):
setattr(ts, 'f%d' % i, value)

#-------------------------------------------------------------------------
# Returns the editor's control for indicating error status:
#-------------------------------------------------------------------------
for i, value in enumerate(self.value):
setattr(ts, 'f{0}'.format(i), value)
if ts.fvalidate is not None:
setattr(ts, 'invalid{0}'.format(i), False)

def get_error_control(self):
""" Returns the editor's control for indicating error status.
"""
return self._ui.get_error_controls()

#-------------------------------------------------------------------------
# 'TupleStructure' class:
#-------------------------------------------------------------------------


class TupleStructure(HasTraits):
""" Creates a view containing items for each field in a tuple.
"""
#-------------------------------------------------------------------------
# Trait definitions:
#-------------------------------------------------------------------------

# Editor this structure is linked to
editor = Any
Expand All @@ -141,9 +111,8 @@ class TupleStructure(HasTraits):
# Number of tuple fields
fields = Int

#-------------------------------------------------------------------------
# Initializes the object:
#-------------------------------------------------------------------------
# The validation function to use for the Tuple.
fvalidate = Callable

def __init__(self, editor):
""" Initializes the object.
Expand All @@ -157,7 +126,7 @@ def __init__(self, editor):
# Save the reference to the editor:
self.editor = editor

# Get the tuple we are mirroring:
# Get the tuple we are mirroring.
object = editor.value

# For each tuple field, add a trait with the appropriate trait
Expand All @@ -167,9 +136,16 @@ def __init__(self, editor):
len_labels = len(labels)
len_editors = len(editors)

# Get global validation function.
type = editor.value_trait.handler
fvalidate = getattr(type, 'fvalidate', None)
if fvalidate is None:
fvalidate = factory.fvalidate
self.fvalidate = fvalidate

# Get field types.
if types is None:
type = editor.value_trait.handler
if isinstance(type, Tuple):
if isinstance(type, BaseTuple):
types = type.types

if not isinstance(types, SequenceTypes):
Expand Down Expand Up @@ -200,11 +176,17 @@ def __init__(self, editor):
if i < len_editors:
field_editor = editors[i]

name = 'f%d' % i
self.add_trait(name, type(value, event='field',
auto_set=auto_set,
enter_set=enter_set))
item = Item(name=name, label=label, editor=field_editor)
name = 'f{0}'.format(i)
self.add_trait(name, type(
value, event='field', auto_set=auto_set, enter_set=enter_set))
if fvalidate is not None:
invalid = 'invalid{0}'.format(i)
self.add_trait(invalid, Bool)
else:
invalid = ''

item = Item(
name, label=label, editor=field_editor, invalid=invalid)
if cols <= 1:
content.append(item)
else:
Expand All @@ -216,21 +198,27 @@ def __init__(self, editor):

self.view = View(Group(show_labels=(len_labels != 0), *content))

#-------------------------------------------------------------------------
# Updates the underlying tuple when any field changes value:
#-------------------------------------------------------------------------

def _field_changed(self, name, old, new):
""" Updates the underlying tuple when any field changes value.
"""
editor = self.editor
value = editor.value
index = int(name[1:])
value = self.editor.value
if new != value[index]:
self.editor.value = tuple(
[getattr(self, 'f%d' % i) for i in range(self.fields)])
new_value = tuple(
getattr(self, 'f{0}'.format(i)) for i in range(self.fields))
if self.fvalidate is not None:
if self.fvalidate(new_value):
Copy link
Contributor

Choose a reason for hiding this comment

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

A more powerful way of validating is to have the validator have a chance to modify the values to something that might work (see the way that Traits validators work). So something like:

try:
    editor.value = self.fvalidate(new_value)
except TraitError:  # or something else appropriate
    for i in range(self.fields):
        setattr(self, 'invalid{0}'.format(i), False)

Copy link
Member Author

@itziakos itziakos Oct 13, 2016

Choose a reason for hiding this comment

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

I am not convinced. If you want the validation to change the value then use a custom trait that preforms the coersion. Normal traits like Float, Range and Int do not covert the values. A number of editors already provide a format function to support conversions.

Copy link
Member Author

@itziakos itziakos Oct 13, 2016

Choose a reason for hiding this comment

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

Also the fact that the validate function of some traits changes the actual value is an internal implementation/optimization. For example the coercing traits CFloat and other will do the validation at the same time as coercion. So by returning the coerced value one does not have to perform the coercion again. But this does not happen in all the traits just those that their Name/Definition imply a coercion.

The generic trait behavior e.g. a Property has separate functions for get, set and validation. Where conversion does not have to happen in validation but in set

editor.value = new_value
for i in range(self.fields):
setattr(self, 'invalid{0}'.format(i), False)
else:
for i in range(self.fields):
setattr(self, 'invalid{0}'.format(i), True)
else:
editor.value = new_value


# Define the TupleEditor class.
TupleEditor = ToolkitEditorFactory

### EOF #######################################################################
1 change: 0 additions & 1 deletion traitsui/qt4/text_editor.py
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,6 @@ def _get_user_value(self):
value = self.control.text()
except AttributeError:
value = self.control.toPlainText()

value = unicode(value)

try:
Expand Down
92 changes: 89 additions & 3 deletions traitsui/tests/_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,16 @@

from functools import partial
from contextlib import contextmanager
import nose

import sys
import threading
import traceback

import nose

from traits.etsconfig.api import ETSConfig
import traits.trait_notifiers


# ######### Testing tools


Expand Down Expand Up @@ -113,7 +115,7 @@ def skip_if_null(test_func):
# preserve original name so that it appears in the report
orig_name = test_func.__name__

def test_func():
def test_func(self=None):
raise nose.SkipTest
test_func.__name__ = orig_name

Expand Down Expand Up @@ -179,6 +181,90 @@ def get_dialog_size(ui_control):
return ui_control.size().width(), ui_control.size().height()


def set_value(editor, value):
""" Set the value on the control managed by the editor.

"""
if is_current_backend_wx():
editor.control.SetValue(value)

elif is_current_backend_qt4():
editor.control.setText(value)
editor.update_object()


@contextmanager
def dispose_ui_after(function, timeout, *args, **kwargs):
""" A context manager that will create a ui and dispose it on exit.

"""
ui = function(*args, **kwargs)

from pyface.gui import GUI

timeout_event = threading.Event()

def on_timeout(timeout_event):
timeout_event.set()
dispose_ui(ui)

gui = GUI()
gui.invoke_after(timeout * 1000, on_timeout, timeout_event)

try:
yield ui
finally:
if timeout_event.is_set():
message = 'UI was forcibly destroyed after {0} sec'
raise AssertionError(message.format(timeout))
else:
dispose_ui(ui)


def dispose_ui(ui):
""" Dispose the ui, by killing the application object.

"""
from pyface.gui import GUI
if ui is not None or ui.control is None:
ui.dispose()
gui = GUI()
if is_current_backend_qt4():
from pyface.qt import QtGui
app = QtGui.QApplication.instance()
gui.invoke_later(app.closeAllWindows)
gui.invoke_after(2, app.quit)
app.exec_()
elif is_current_backend_wx():
import wx
for w in wx.GetTopLevelWindows():
wx.CallAfter(w.Close)
app = wx.GetApp()
gui.invoke_later(app.Exit)
app.MainLoop()


def get_traitsui_editor(ui, path):
""" Get an editor from a UI using a '/' separated list of trait names.

'/' is used to access the editor of a trait in a sub-element of the
view.
"""

names = path.split('/')

while True:
name = names.pop(0)
editor = ui.get_editors(name)[0]

if len(names) > 0:
ui = editor._ui
else:
break

return editor


# ######### Debug tools

def apply_on_children(func, node, _level=0):
Expand Down
Loading