Skip to content

Commit

Permalink
Merge pull request enthought#49 from enthought/new_csv_list_editr
Browse files Browse the repository at this point in the history
FIX: fix issue with run-away change notification in the CSVListEditor
  • Loading branch information
pberkes committed Jan 16, 2012
2 parents 4ecb2fa + a59ac8d commit 490027e
Show file tree
Hide file tree
Showing 8 changed files with 389 additions and 100 deletions.
150 changes: 78 additions & 72 deletions traitsui/editors/csv_list_editor.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@
from traits.trait_handlers import RangeTypes

from .text_editor import TextEditor
from ..editor_factory import EditorFactory
from ..helper import enum_values_changed


Expand Down Expand Up @@ -153,17 +152,46 @@ def _validate_range_value(range_object, object, name, value):
range_object.error(object, name, value)


class CSVListEditor(EditorFactory):
def _prepare_method(cls, parent):
""" Unbound implementation of the prepare editor method to add a
change notification hook in the items of the list before calling
the parent prepare method of the parent class.
"""
name = cls.extended_name
if name != 'None':
cls.context_object.on_trait_change(cls._update_editor,
name + '[]',
dispatch='ui')
super(cls.__class__, cls).prepare(parent)

def _dispose_method(cls):
""" Unbound implementation of the dispose editor method to remove
the change notification hook in the items of the list before calling
the parent dispose method of the parent class.
"""
if cls.ui is None:
return

name = cls.extended_name
if name != 'None':
cls.context_object.on_trait_change(cls._update_editor,
name + '[]',
remove=True)
super(cls.__class__, cls).dispose()

class CSVListEditor(TextEditor):
"""A text editor for a List.
This editor provides a single line of input text of comma separated
values. (Actually, the default separator is a comma, but this can
changed.) The editor can only be used with List traits whose inner
trait is one of Int, Float, Str, Enum, or Range.
The 'simple', 'text' and 'custom' styles are all the same. The
'readonly' style provides the same formatting in the text field as
the other editors, but the user can not change the value.
The 'simple', 'text', 'custom' and readonly styles are based on
TextEditor. The 'readonly' style provides the same formatting in the
text field as the other editors, but the user cannot change the value.
Like other Traits editors, the background of the text field will turn
red if the user enters an incorrectly formatted list or if the values
Expand Down Expand Up @@ -309,73 +337,51 @@ def _funcs(self, object, name):

return evaluate, fmt_func

def _make_text_editor(self, ui, object, name, description, parent,
readonly=False):
"""Create the actual text editor for this list.
Parameters
----------
ui : instance of traitsui.ui.UI
Passed on to factory functions of the TextEditor instance.
object : instance of HasTraits
The HasTraits instance with the trait `name`.
name : str
The name of the trait on `object`.
description : str
This is a description of the trait. If, for example, the Item
holding the trait defines a tooltip, that string will end up
in `description`.
It is passed on to the factory functions of the TextEditor instance.

parent : object
The parent of the backend-dependent control that will be used
to implement the editor.
#---------------------------------------------------------------------------
# Methods that generate backend toolkit-specific editors.
#---------------------------------------------------------------------------

readonly : bool
If True, create a read-only editor. Otherwise, the editor field
will be editable by the user.
Returns
-------
ed : object
An editor generated by either TextEditor.simple_editor(...)
or TextEditor.readonly_editor(...), depending on the value
of `readonly`.
def simple_editor ( self, ui, object, name, description, parent ):
""" Generates an editor using the "simple" style.
"""
evaluate, fmt_func = self._funcs(object, name)
# Create a TextEditor with the appropriate evaluation and formatting
# functions.
editor_factory = TextEditor(evaluate=evaluate, format_func=fmt_func,
auto_set=self.auto_set, enter_set=self.enter_set)

# Call the appropriate factory function to create the actual editor.
if readonly:
ed = editor_factory.readonly_editor(ui, object, name, description,
parent)
else:
ed = editor_factory.simple_editor(ui, object, name, description,
parent)

# Hook up a listener on `object` so that the display is updated if
# the list changes external to the editor.
object.on_trait_change(ed.update_editor, name + '[]')

return ed

def simple_editor(self, ui, object, name, description, parent):
e = self._make_text_editor(ui, object, name, description, parent)
return e

def custom_editor(self, ui, object, name, description, parent):
return self.simple_editor(ui, object, name, description, parent)

def text_editor(self, ui, object, name, description, parent):
return self.simple_editor(ui, object, name, description, parent)

def readonly_editor(self, ui, object, name, description, parent):
e = self._make_text_editor(ui, object, name, description, parent,
readonly=True)
return e
self.evaluate, self.format_func = self._funcs(object, name)
return self.simple_editor_class( parent,
factory = self,
ui = ui,
object = object,
name = name,
description = description )

def custom_editor ( self, ui, object, name, description, parent ):
""" Generates an editor using the "custom" style.
"""
self.evaluate, self.format_func = self._funcs(object, name)
return self.custom_editor_class( parent,
factory = self,
ui = ui,
object = object,
name = name,
description = description )

def text_editor ( self, ui, object, name, description, parent ):
""" Generates an editor using the "text" style.
"""
self.evaluate, self.format_func = self._funcs(object, name)
return self.text_editor_class( parent,
factory = self,
ui = ui,
object = object,
name = name,
description = description )

def readonly_editor ( self, ui, object, name, description, parent ):
""" Generates an "editor" that is read-only.
"""
self.evaluate, self.format_func = self._funcs(object, name)
return self.readonly_editor_class( parent,
factory = self,
ui = ui,
object = object,
name = name,
description = description )
47 changes: 47 additions & 0 deletions traitsui/qt4/csv_list_editor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
#------------------------------------------------------------------------------
#
# Copyright (c) 2012, Enthought, Inc.
# All rights reserved.
#
# This software is provided without warranty under the terms of the BSD
# license included in enthought/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!
#
# Author: Ioannis Tziakos
# Date: 11 Jan 2012
#
#------------------------------------------------------------------------------

""" Defines the various text editors for the Qt user interface toolkit.
The module is mainly a place-folder for TextEditor factories that have
been augmented to also listen to changes in the items of the list object.
"""

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

from .text_editor import SimpleEditor as QtSimpleEditor
from .text_editor import CustomEditor as QtCustomEditor
from .text_editor import ReadonlyEditor as QtReadonlyEditor
from ..editors.csv_list_editor import _prepare_method, _dispose_method

class SimpleEditor(QtSimpleEditor):
""" Simple Editor style for CSVListEditor. """
prepare = _prepare_method
dispose = _dispose_method

class CustomEditor(QtCustomEditor):
""" Custom Editor style for CSVListEditor. """
prepare = _prepare_method
dispose = _dispose_method

class ReadonlyEditor(QtReadonlyEditor):
""" Readonly Editor style for CSVListEditor. """
prepare = _prepare_method
dispose = _dispose_method

TextEditor = SimpleEditor
76 changes: 73 additions & 3 deletions traitsui/tests/_tools.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,18 @@
#------------------------------------------------------------------------------
#
# Copyright (c) 2012, Enthought, Inc.
# All rights reserved.
#
# This software is provided without warranty under the terms of the BSD
# license included in enthought/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
#
# Author: Pietro Berkes
# Date: Jan 2012
#
#------------------------------------------------------------------------------

from functools import partial
from contextlib import contextmanager
import nose
Expand All @@ -6,36 +21,56 @@
import traceback

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

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

@contextmanager
def store_exceptions_on_all_threads():
"""Context manager that captures all exceptions, even those coming from
the UI thread. On exit, the first exception is raised (if any).
It also temporarily overwrites the global function
traits.trait_notifier.handle_exception , which logs exceptions to
console without re-raising them by default.
"""

exceptions = []

def excepthook(type, value, tb):
exceptions.append(value)
def _print_uncaught_exception(type, value, tb):
message = 'Uncaught exception:\n'
message += ''.join(traceback.format_exception(type, value, tb))
print message

def excepthook(type, value, tb):
exceptions.append(value)
_print_uncaught_exception(type, value, tb)

def handle_exception(object, trait_name, old, new):
type, value, tb = sys.exc_info()
exceptions.append(value)
_print_uncaught_exception(type, value, tb)

_original_handle_exception = traits.trait_notifiers.handle_exception
try:
sys.excepthook = excepthook
traits.trait_notifiers.handle_exception = handle_exception
yield
finally:
if len(exceptions) > 0:
raise exceptions[0]
sys.excepthook = sys.__excepthook__
traits.trait_notifiers.handle_exception = _original_handle_exception


def _is_current_backend(backend_name=''):
return ETSConfig.toolkit == backend_name


def skip_if_not_backend(test_func, backend_name=''):
"""Decorator that skip tests if the backend is not the desired one."""

if ETSConfig.toolkit != backend_name:
if not _is_current_backend(backend_name):
# preserve original name so that it appears in the report
orig_name = test_func.__name__
def test_func():
Expand All @@ -45,6 +80,12 @@ def test_func():
return test_func


#: Return True if current backend is 'wx'
is_current_backend_wx = partial(_is_current_backend, backend_name='wx')

#: Return True if current backend is 'qt4'
is_current_backend_qt4 = partial(_is_current_backend, backend_name='qt4')

#: Test decorator: Skip test if backend is not 'wx'
skip_if_not_wx = partial(skip_if_not_backend, backend_name='wx')

Expand Down Expand Up @@ -76,6 +117,25 @@ def get_children(node):
return node.children()


def press_ok_button(ui):
"""Press the OK button in a wx or qt dialog."""

if is_current_backend_wx():
import wx

ok_button = ui.control.FindWindowByName('button')
click_event = wx.CommandEvent(wx.wxEVT_COMMAND_BUTTON_CLICKED,
ok_button.GetId())
ok_button.ProcessEvent(click_event)

elif is_current_backend_qt4():
from pyface import qt

# press the OK button and close the dialog
ok_button = ui.control.findChild(qt.QtGui.QPushButton)
ok_button.click()


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

def apply_on_children(func, node, _level=0):
Expand All @@ -89,12 +149,22 @@ def apply_on_children(func, node, _level=0):

def wx_print_names(node):
"""Print the name and id of `node` and its children.
Use as::
>>> ui = xxx.edit_traits()
>>> wx_print_names(ui.control)
"""
apply_on_children(lambda n: (n.GetName(), n.GetId()), node)


def qt_print_names(node):
"""Print the name of `node` and its children.
Use as::
>>> ui = xxx.edit_traits()
>>> wx_print_names(ui.control)
"""
apply_on_children(lambda n: n.objectName(), node)

Expand Down
Loading

0 comments on commit 490027e

Please sign in to comment.