From edd78d1c6afbf6dd33ff0791189632481b4fd861 Mon Sep 17 00:00:00 2001 From: Corran Webster Date: Sat, 14 Jul 2018 17:02:11 -0500 Subject: [PATCH 01/22] Initial commit of ipywidgets toolkit code. --- traitsui/ipywidgets/__init__.py | 0 traitsui/ipywidgets/toolkit.py | 14 +++++ traitsui/ipywidgets/ui_panel.py | 96 +++++++++++++++++++++++++++++++++ 3 files changed, 110 insertions(+) create mode 100644 traitsui/ipywidgets/__init__.py create mode 100644 traitsui/ipywidgets/toolkit.py create mode 100644 traitsui/ipywidgets/ui_panel.py diff --git a/traitsui/ipywidgets/__init__.py b/traitsui/ipywidgets/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/traitsui/ipywidgets/toolkit.py b/traitsui/ipywidgets/toolkit.py new file mode 100644 index 000000000..1872dea92 --- /dev/null +++ b/traitsui/ipywidgets/toolkit.py @@ -0,0 +1,14 @@ +from traitsui.toolkit import assert_toolkit_import +assert_toolkit_import(['ipywidgets']) + +from traitsui.toolkit import Toolkit + +import ipywidgets + + +class GUIToolkit(Toolkit): + """ Implementation class for ipywidgets toolkit """ + + def ui_panel(self, ui, parent): + from .ui_panel import ui_panel + ui_panel(ui, parent) diff --git a/traitsui/ipywidgets/ui_panel.py b/traitsui/ipywidgets/ui_panel.py new file mode 100644 index 000000000..dfdf9e091 --- /dev/null +++ b/traitsui/ipywidgets/ui_panel.py @@ -0,0 +1,96 @@ +from traitsui.base_panel import BasePanel +from traitsui.group import Group + +import ipywidgets + + +def ui_panel(ui, parent): + _ui_panel_for(ui, parent, False) + + +def ui_subpanel(ui, parent): + _ui_panel_for(ui, parent, True) + + +def _ui_panel_for(ui, parent, is_subpanel): + ui.control = control = Panel(ui, parent, is_subpanel).control + + +class Panel(BasePanel): + + def __init__(self, ui, parent, is_subpanel): + self.ui = ui + history = ui.history + view = ui.view + + # Reset any existing history listeners. + if history is not None: + history.on_trait_change(self._on_undoable, 'undoable', remove=True) + history.on_trait_change(self._on_redoable, 'redoable', remove=True) + history.on_trait_change(self._on_revertable, 'undoable', + remove=True) + + # no buttons for now + + self.control = panel(ui) + + +def panel(ui): + ui.info.bind_context() + + content = ui._grousp + n_groups = len(content) + + if n_groups == 0: + panel = None + elif n_groups == 1: + panel = GroupPanel(content[0], ui).control + elif n_groups > 1: + panel = ipywidgets.Tab() + _fill_panel(panel, content, ui) + panel.ui = ui + + # not handling scrollable for now + + return panel + + +def _fill_panel(panel, content, ui, item_handler=None): + """ Fill a page-based container panel with content. """ + + active = 0 + + for index, item in enumeratex(content): + page_name = item.get_label(ui) + if page_name == "": + page_name = "Page {}".format(index) + + if isinstance(item, Group): + if item.selected: + active = index + + gp = GroupPanel(item, ui, suppress_label=True) + page = gp.control + sub_page = gp.sub_control + + if isinstance(sub_page, type(panel)) and len(sub_page.children) == 1: + new = sub_page.children[0] + else: + new = page + + else: + new = item_handler(item) + + panel.children.append(new) + panel.set_title(index, page_name) + + panel.selected_index = active + + +class GroupPanel(object): + + def __init__(self, group, ui, suppress_label=False): + content = group.get_content() + + self.group = group + self.ui = ui From be0f87750b65ea6cf9c1bed20cb4c7358afa618a Mon Sep 17 00:00:00 2001 From: Prabhu Ramachandran Date: Sun, 15 Jul 2018 09:18:40 -0500 Subject: [PATCH 02/22] WIP: First cut of bool and text editors. --- traitsui/ipywidgets/boolean_editor.py | 52 ++++++ traitsui/ipywidgets/constants.py | 31 ++++ traitsui/ipywidgets/editor.py | 257 ++++++++++++++++++++++++++ traitsui/ipywidgets/text_editor.py | 154 +++++++++++++++ 4 files changed, 494 insertions(+) create mode 100644 traitsui/ipywidgets/boolean_editor.py create mode 100644 traitsui/ipywidgets/constants.py create mode 100644 traitsui/ipywidgets/editor.py create mode 100644 traitsui/ipywidgets/text_editor.py diff --git a/traitsui/ipywidgets/boolean_editor.py b/traitsui/ipywidgets/boolean_editor.py new file mode 100644 index 000000000..a00492123 --- /dev/null +++ b/traitsui/ipywidgets/boolean_editor.py @@ -0,0 +1,52 @@ +""" Defines the various Boolean editors for the PyQt user interface toolkit. +""" + +import ipywidgets as widgets + +from editor import Editor + +# This needs to be imported in here for use by the editor factory for boolean +# editors (declared in traitsui). The editor factory's text_editor +# method will use the TextEditor in the ui. +from text_editor import SimpleEditor as TextEditor + + +class SimpleEditor(Editor): + """ Simple style of editor for Boolean values, which displays a check box. + """ + def init(self, parent): + """ Finishes initializing the editor by creating the underlying toolkit + widget. + """ + self.control = widgets.Checkbox(value=True, description='') + self.control.stateChanged.connect(self.update_object) + self.set_tooltip() + + def update_object(self, state): + """ Handles the user clicking the checkbox. + """ + self.value = bool(state) + + def update_editor(self): + """ Updates the editor when the object trait changes externally to the + editor. + """ + self.control.value = self.value + + +class ReadonlyEditor(Editor): + """ Read-only style of editor for Boolean values, which displays static text + of either "True" or "False". + """ + + def init(self, parent): + """ Finishes initializing the editor by creating the underlying toolkit + widget. + """ + self.control = widgets.Valid(value=self.value, readout='') + + def update_editor(self): + """ Updates the editor when the object trait changes externally to the + editor. + """ + self.control.value = self.value diff --git a/traitsui/ipywidgets/constants.py b/traitsui/ipywidgets/constants.py new file mode 100644 index 000000000..de4fb7883 --- /dev/null +++ b/traitsui/ipywidgets/constants.py @@ -0,0 +1,31 @@ +""" Defines constants used by the ipywidgets implementation of the various text +editors and text editor factories. +""" + +# import ipywidgets as widgets + +# Default dialog title +DefaultTitle = 'Edit properties' + +# Color of valid input +OKColor = '' + +# Color to highlight input errors +ErrorColor = 'danger' + +# Color for background of read-only fields +ReadonlyColor = '' # QtGui.QColor(244, 243, 238) + +# Color for background of fields where objects can be dropped +DropColor = '' # QtGui.QColor(215, 242, 255) + +# Color for an editable field +EditableColor = '' # _palette.color(QtGui.QPalette.Base) + +# Color for background of windows (like dialog background color) +WindowColor = '' # _palette.color(QtGui.QPalette.Window) + +# Screen size values: + +screen_dx = '' +screen_dy = '' diff --git a/traitsui/ipywidgets/editor.py b/traitsui/ipywidgets/editor.py new file mode 100644 index 000000000..4f87d635c --- /dev/null +++ b/traitsui/ipywidgets/editor.py @@ -0,0 +1,257 @@ +""" Defines the base class for ipywidgets editors. +""" +from __future__ import print_function + +from traits.api import HasTraits, Instance, Str, Callable + +from traitsui.api import Editor as UIEditor + +from constants import OKColor, ErrorColor + + +class Editor(UIEditor): + """ Base class for ipywidgets editors for Traits-based UIs. + """ + + def clear_layout(self): + """ Delete the contents of a control's layout. + """ + # FIXME? + pass + + def _control_changed(self, control): + """ Handles the **control** trait being set. + """ + # FIXME: Check we actually make use of this. + if control is not None: + control._editor = self + + def set_focus(self): + """ Assigns focus to the editor's underlying toolkit widget. + """ + # FIXME? + pass + + def update_editor(self): + """ Updates the editor when the object trait changes externally to the + editor. + """ + new_value = self.value + if self.control.value != new_value: + self.control.value = new_value + + def error(self, excp): + """ Handles an error that occurs while setting the object's trait value. + """ + # Make sure the control is a widget rather than a layout. + # FIXME? + print(self.control, self.description, 'value error', str(excp)) + + def set_tooltip(self, control=None): + """ Sets the tooltip for a specified control. + """ + desc = self.description + if desc == '': + desc = self.object.base_trait(self.name).desc + if desc is None: + return False + + desc = 'Specifies ' + desc + + if control is None: + control = self.control + + if hasattr(control, 'tooltip'): + control.tooltip = desc + + return True + + def _enabled_changed(self, enabled): + """Handles the **enabled** state of the editor being changed. + """ + if self.control is not None: + self._enabled_changed_helper(self.control, enabled) + if self.label_control is not None: + self.label_control.disabled = not enabled + + def _enabled_changed_helper(self, control, enabled): + """A helper that allows the control to be a layout and recursively + manages all its widgets. + """ + if hasattr(control, 'disabled'): + control.disabled = not enabled + elif hasattr(control, 'children'): + for child in control.children: + child.disabled = not enabled + + def _visible_changed(self, visible): + """Handles the **visible** state of the editor being changed. + """ + visibility = 'visible' if visible else 'hidden' + if self.label_control is not None: + self.label_control.layout.visibility = visibility + if self.control is None: + # We are being called after the editor has already gone away. + return + + self._visible_changed_helper(self.control, visibility) + + def _visible_changed_helper(self, control, visibility): + """A helper that allows the control to be a layout and recursively + manages all its widgets. + """ + if hasattr(control, 'layout'): + control.layout.visibility = visibility + if hasattr(control, 'children'): + for child in control.children: + self._visible_changed_helper(child, visibility) + + def get_error_control(self): + """ Returns the editor's control for indicating error status. + """ + return self.control + + def in_error_state(self): + """ Returns whether or not the editor is in an error state. + """ + return False + + def set_error_state(self, state=None, control=None): + """ Sets the editor's current error state. + """ + if state is None: + state = self.invalid + state = state or self.in_error_state() + + if control is None: + control = self.get_error_control() + + if not isinstance(control, list): + control = [control] + + for item in control: + if item is None: + continue + + if state: + color = ErrorColor + else: + color = OKColor + + try: + if hasattr(item, 'box_style'): + item.box_style = color + # FIXME! + except Exception: + pass + + def _invalid_changed(self, state): + """ Handles the editor's invalid state changing. + """ + self.set_error_state() + + def eval_when(self, condition, object, trait): + """ Evaluates a condition within a defined context, and sets a + specified object trait based on the result, which is assumed to be a + Boolean. + """ + if condition != '': + value = True + try: + if not eval(condition, globals(), self._menu_context): + value = False + except: + from traitsui.api import raise_to_debug + raise_to_debug() + setattr(object, trait, value) + + +class EditorWithList(Editor): + """ Editor for an object that contains a list. + """ + # Object containing the list being monitored + list_object = Instance(HasTraits) + + # Name of the monitored trait + list_name = Str + + # Function used to evaluate the current list object value: + list_value = Callable + + def init(self, parent): + """ Initializes the object. + """ + factory = self.factory + name = factory.name + if name != '': + self.list_object, self.list_name, self.list_value = \ + self.parse_extended_name(name) + else: + self.list_object, self.list_name = factory, 'values' + self.list_value = lambda: factory.values + + self.list_object.on_trait_change(self._list_updated, + self.list_name, dispatch='ui') + self.list_object.on_trait_change( + self._list_updated, + self.list_name + '_items', + dispatch='ui') + + self._list_updated() + + def dispose(self): + """ Disconnects the listeners set up by the constructor. + """ + self.list_object.on_trait_change(self._list_updated, + self.list_name, remove=True) + self.list_object.on_trait_change( + self._list_updated, + self.list_name + '_items', + remove=True) + + super(EditorWithList, self).dispose() + + def _list_updated(self): + """ Handles the monitored trait being updated. + """ + self.list_updated(self.list_value()) + + def list_updated(self, values): + """ Handles the monitored list being updated. + """ + raise NotImplementedError + + +class EditorFromView(Editor): + """ An editor generated from a View object. + """ + + def init(self, parent): + """ Initializes the object. + """ + self._ui = ui = self.init_ui(parent) + if ui.history is None: + ui.history = self.ui.history + + self.control = ui.control + + def init_ui(self, parent): + """ Creates and returns the traits UI defined by this editor. + (Must be overridden by a subclass). + """ + raise NotImplementedError + + def update_editor(self): + """ Updates the editor when the object trait changes externally to the + editor. + """ + # Normally nothing needs to be done here, since it should all be + # handled by the editor's internally created traits UI: + pass + + def dispose(self): + """ Disposes of the editor. + """ + self._ui.dispose() + + super(EditorFromView, self).dispose() diff --git a/traitsui/ipywidgets/text_editor.py b/traitsui/ipywidgets/text_editor.py new file mode 100644 index 000000000..33cbbc3ca --- /dev/null +++ b/traitsui/ipywidgets/text_editor.py @@ -0,0 +1,154 @@ +""" Defines the various text editors for the ipywidgets user interface toolkit. +""" + +import ipywidgets as widgets + +from traits.api import TraitError + +# FIXME: ToolkitEditorFactory is a proxy class defined here just for backward +# compatibility. The class has been moved to the +# traitsui.editors.text_editor file. +from traitsui.editors.text_editor import evaluate_trait, ToolkitEditorFactory + +from editor import Editor + +# FIXME +# from editor_factory import ReadonlyEditor as BaseReadonlyEditor + +from constants import OKColor + + +class SimpleEditor(Editor): + """ Simple style text editor, which displays a text field. + """ + + # Flag for window styles: + base_style = widgets.Text + + # Background color when input is OK: + ok_color = OKColor + + # *** Trait definitions *** + # Function used to evaluate textual user input: + evaluate = evaluate_trait + + def init(self, parent): + """ Finishes initializing the editor by creating the underlying toolkit + widget. + """ + factory = self.factory + wtype = self.base_style + self.evaluate = factory.evaluate + self.sync_value(factory.evaluate_name, 'evaluate', 'from') + + if not factory.multi_line or factory.is_grid_cell: + wtype = widgets.Text + + if factory.password: + wtype = widgets.Password + + multi_line = (wtype is not widgets.Text) + if multi_line: + self.scrollable = True + + control = wtype(value=self.str_value, description='') + + if factory.read_only: + control.disabled = True + + if factory.auto_set and not factory.is_grid_cell: + control.continous_update = True + else: + # Assume enter_set is set, otherwise the value will never get + # updated. + control.continous_update = False + + self.control = control + self.set_error_state(False) + self.set_tooltip() + + def update_object(self): + """ Handles the user entering input data in the edit control. + """ + if (not self._no_update) and (self.control is not None): + try: + self.value = self._get_user_value() + + if self._error is not None: + self._error = None + self.ui.errors -= 1 + + self.set_error_state(False) + + except TraitError as excp: + pass + + def update_editor(self): + """ Updates the editor when the object trait changes externally to the + editor. + """ + user_value = self._get_user_value() + try: + unequal = bool(user_value != self.value) + except ValueError: + # This might be a numpy array. + unequal = True + + if unequal: + self._no_update = True + self.control.value = self.str_value + self._no_update = False + + if self._error is not None: + self._error = None + self.ui.errors -= 1 + self.set_error_state(False) + + def _get_user_value(self): + """ Gets the actual value corresponding to what the user typed. + """ + value = self.control.value + + try: + value = self.evaluate(value) + except: + pass + + try: + ret = self.factory.mapping.get(value, value) + except TypeError: + # The value is probably not hashable. + ret = value + + return ret + + def error(self, excp): + """ Handles an error that occurs while setting the object's trait value. + """ + if self._error is None: + self._error = True + self.ui.errors += 1 + + self.set_error_state(True) + + def in_error_state(self): + """ Returns whether or not the editor is in an error state. + """ + return (self.invalid or self._error) + + +class CustomEditor(SimpleEditor): + """ Custom style of text editor, which displays a multi-line text field. + """ + + base_style = widgets.Textarea + + +class ReadonlyEditor(SimpleEditor): + """ Read-only style of text editor, which displays a read-only text field. + """ + + def init(self, parent): + super(ReadonlyEditor, self).init(parent) + + self.control.disabled = True From 1e12dcf3ec1b54345033a34930d5affb4da0f94c Mon Sep 17 00:00:00 2001 From: Corran Webster Date: Sun, 15 Jul 2018 11:41:20 -0500 Subject: [PATCH 03/22] First working widgets. --- setup.py | 4 + traitsui/api.py | 2 +- traitsui/base_panel.py | 3 +- traitsui/ipywidgets/__init__.py | 5 + traitsui/ipywidgets/boolean_editor.py | 6 +- traitsui/ipywidgets/text_editor.py | 4 +- traitsui/ipywidgets/toolkit.py | 58 ++++++ traitsui/ipywidgets/ui_panel.py | 250 +++++++++++++++++++++++++- traitsui/item.py | 14 ++ traitsui/menu.py | 20 +-- traitsui/qt4/color_trait.py | 2 +- traitsui/qt4/ui_panel.py | 4 +- 12 files changed, 349 insertions(+), 23 deletions(-) diff --git a/setup.py b/setup.py index aa84bb77d..1eed88fed 100644 --- a/setup.py +++ b/setup.py @@ -148,6 +148,10 @@ def additional_commands(): 'wx = traitsui.wx:toolkit', 'qt = traitsui.qt4:toolkit', 'null = traitsui.null:toolkit', + 'ipywidgets = traitsui.ipywidgets:toolkit', + ], + 'pyface.toolkits': [ + 'ipywidgets = traitsui.ipywidgets:toolkit', ], }, platforms=["Windows", "Linux", "Mac OS-X", "Unix", "Solaris"], diff --git a/traitsui/api.py b/traitsui/api.py index 53946fb43..6f6b1393f 100644 --- a/traitsui/api.py +++ b/traitsui/api.py @@ -117,7 +117,7 @@ RevertAction, RevertButton, Separator, - StandardMenuBar, + #StandardMenuBar, ToolBar, UndoAction, UndoButton) diff --git a/traitsui/base_panel.py b/traitsui/base_panel.py index 53f91e272..425616061 100644 --- a/traitsui/base_panel.py +++ b/traitsui/base_panel.py @@ -13,7 +13,6 @@ from pyface.action.api import ActionController from traits.api import Any, Instance -from traitsui.menu import Action # Set of all predefined system button names: @@ -59,6 +58,8 @@ def is_button(self, action, name): def coerce_button(self, action): """ Coerces a string to an Action if necessary. """ + from traitsui.menu import Action + if isinstance(action, basestring): return Action( name=action, diff --git a/traitsui/ipywidgets/__init__.py b/traitsui/ipywidgets/__init__.py index e69de29bb..7137da338 100644 --- a/traitsui/ipywidgets/__init__.py +++ b/traitsui/ipywidgets/__init__.py @@ -0,0 +1,5 @@ + +from .toolkit import GUIToolkit + +# Reference to the GUIToolkit object for IPyWidgets. +toolkit = GUIToolkit('traitsui', 'ipywidgets', 'traitsui.ipywidgets') diff --git a/traitsui/ipywidgets/boolean_editor.py b/traitsui/ipywidgets/boolean_editor.py index a00492123..bf6b00b24 100644 --- a/traitsui/ipywidgets/boolean_editor.py +++ b/traitsui/ipywidgets/boolean_editor.py @@ -19,13 +19,13 @@ def init(self, parent): widget. """ self.control = widgets.Checkbox(value=True, description='') - self.control.stateChanged.connect(self.update_object) + self.control.observe(self.update_object, 'value') self.set_tooltip() - def update_object(self, state): + def update_object(self, event=None): """ Handles the user clicking the checkbox. """ - self.value = bool(state) + self.value = bool(self.control.value) def update_editor(self): """ Updates the editor when the object trait changes externally to the diff --git a/traitsui/ipywidgets/text_editor.py b/traitsui/ipywidgets/text_editor.py index 33cbbc3ca..07152082d 100644 --- a/traitsui/ipywidgets/text_editor.py +++ b/traitsui/ipywidgets/text_editor.py @@ -63,11 +63,13 @@ def init(self, parent): # updated. control.continous_update = False + control.observe(self.update_object, 'value') + self.control = control self.set_error_state(False) self.set_tooltip() - def update_object(self): + def update_object(self, event=None): """ Handles the user entering input data in the edit control. """ if (not self._no_update) and (self.control is not None): diff --git a/traitsui/ipywidgets/toolkit.py b/traitsui/ipywidgets/toolkit.py index 1872dea92..034bf1339 100644 --- a/traitsui/ipywidgets/toolkit.py +++ b/traitsui/ipywidgets/toolkit.py @@ -1,14 +1,72 @@ from traitsui.toolkit import assert_toolkit_import assert_toolkit_import(['ipywidgets']) +from traits.trait_notifiers import set_ui_handler +from pyface.base_toolkit import Toolkit as PyfaceToolkit from traitsui.toolkit import Toolkit import ipywidgets +toolkit = PyfaceToolkit( + 'pyface', + 'ipywidgets', + 'traitsui.ipywidgets' +) + +def ui_handler(handler, *args, **kwds): + """ Handles UI notification handler requests that occur on a thread other + than the UI thread. + """ + handler(*args, **kwds) + +set_ui_handler(ui_handler) + + class GUIToolkit(Toolkit): """ Implementation class for ipywidgets toolkit """ def ui_panel(self, ui, parent): from .ui_panel import ui_panel ui_panel(ui, parent) + + def ui_live(self, ui, parent): + from .ui_panel import ui_panel + ui_panel(ui, parent) + + def rebuild_ui(self, ui): + """ Rebuilds a UI after a change to the content of the UI. + """ + if ui.control is not None: + ui.recycle() + ui.info.ui = ui + ui.rebuild(ui, ui.parent) + + def constants(self): + """ Returns a dictionary of useful constants. + + Currently, the dictionary should have the following key/value pairs: + + - 'WindowColor': the standard window background color in the toolkit + specific color format. + """ + return {'WindowColor': None} + + + def color_trait(self, *args, **traits): + #import color_trait as ct + #return ct.PyQtColor(*args, **traits) + from traits.api import Unicode + return Unicode + + def rgb_color_trait(self, *args, **traits): + #import rgb_color_trait as rgbct + #return rgbct.RGBColor(*args, **traits) + from traits.api import Unicode + return Unicode + + def font_trait(self, *args, **traits): + #import font_trait as ft + #return ft.PyQtFont(*args, **traits) + from traits.api import Unicode + return Unicode diff --git a/traitsui/ipywidgets/ui_panel.py b/traitsui/ipywidgets/ui_panel.py index dfdf9e091..6e7265482 100644 --- a/traitsui/ipywidgets/ui_panel.py +++ b/traitsui/ipywidgets/ui_panel.py @@ -1,8 +1,13 @@ +import re + from traitsui.base_panel import BasePanel from traitsui.group import Group import ipywidgets +# Pattern of all digits +all_digits = re.compile(r'\d+') + def ui_panel(ui, parent): _ui_panel_for(ui, parent, False) @@ -38,7 +43,7 @@ def __init__(self, ui, parent, is_subpanel): def panel(ui): ui.info.bind_context() - content = ui._grousp + content = ui._groups n_groups = len(content) if n_groups == 0: @@ -60,7 +65,7 @@ def _fill_panel(panel, content, ui, item_handler=None): active = 0 - for index, item in enumeratex(content): + for index, item in enumerate(content): page_name = item.get_label(ui) if page_name == "": page_name = "Page {}".format(index) @@ -81,7 +86,7 @@ def _fill_panel(panel, content, ui, item_handler=None): else: new = item_handler(item) - panel.children.append(new) + panel.children += (new,) panel.set_title(index, page_name) panel.selected_index = active @@ -94,3 +99,242 @@ def __init__(self, group, ui, suppress_label=False): self.group = group self.ui = ui + + outer = sub = inner = None + if group.label != "": + if group.orientation == 'horizontal': + outer = inner = ipywidgets.HBox() + else: + outer = inner = ipywidgets.VBox() + inner.children += (ipywidgets.Label(value=group.label),) + + if len(content) == 0: + pass + elif group.layout == 'tabbed': + sub = ipywidgets.Tab() + _fill_panel(sub, content, self.ui, self._add_page_item) + if outer is None: + outer = sub + else: + inner.children += (sub,) + editor = PagedGroupEditor(container=sub, control=sub, ui=ui) + self._setup_editor(group, editor) + elif group.layout == 'fold': + sub = ipywidgets.Accordion() + _fill_panel(sub, content, self.ui, self._add_page_item) + if outer is None: + outer = sub + else: + inner.children += (sub,) + editor = PagedGroupEditor(container=sub, control=sub, ui=ui) + self._setup_editor(group, editor) + elif group.layout in {'split', 'flow'}: + raise NotImplementedError("IPyWidgets backend does not have Split or Flow") + else: + if isinstance(content[0], Group): + layout = self._add_groups(content, inner) + else: + layout = self._add_items(content, inner) + + if outer is None: + outer = layout + elif layout is not inner: + inner.children += (layout,) + + self.control = outer + self.sub_control = sub + + def _setup_editor(self, group, editor): + if group.id != '': + self.ui.info.bind(group.id, editor) + if group.visible_when != '': + self.ui.info.bind(group.visible_when, editor) + if group.enabled_when != '': + self.ui.info.bind(group.enabled_when, editor) + + def _add_page_item(self, item, layout): + """Adds a single Item to a page based panel. + """ + layout.children += (item,) + + def _add_groups(self, content, outer): + if outer is None: + if self.group.orientation == 'horizontal': + outer = ipywidgets.HBox() + else: + outer = ipywidgets.VBox() + + for subgroup in content: + panel = GroupPanel(subgroup, self.ui).control + + if panel is not None: + outer.children += (panel,) + else: + # add some space + outer.children += (ipywidgets.Label(value=' '),) + + return outer + + def _add_items(self, content, outer=None): + ui = self.ui + info = ui.info + handler = ui.handler + + group = self.group + show_left = group.show_left + columns = group.columns + + show_labels = any(item.show_label for item in content) + + if show_labels or columns > 1: + if self.group.orientation == 'horizontal': + inner = ipywidgets.HBox() + else: + inner = ipywidgets.VBox() + #inner = ipywidgets.GridBox() + if outer is None: + outer = inner + else: + outer.children += (inner,) + + row = 0 + + else: + if self.group.orientation == 'horizontal': + outer = ipywidgets.HBox() + else: + outer = ipywidgets.VBox() + inner = outer + row = -1 + show_left = None + + col = -1 + for item in content: + col += 1 + if row > 0 and col > columns: + col = 0 + row += 1 + + name = item.name + if name == '': + label = item.label + if label != "": + label = ipywidgets.Label(value=label) + self._add_widget(inner, label, row, col, show_labels) + continue + + if name == '_': + # separator + # XXX do nothing for now + continue + + if name == ' ': + name = '5' + + if all_digits.match(name): + # spacer + # XXX do nothing for now + continue + + # XXX can we avoid evals for dots? + print(item.object_, ui.context) + obj = eval(item.object_, globals(), ui.context) + print(obj) + trait = obj.base_trait(name) + desc = trait.desc if trait.desc is not None else '' + + editor_factory = item.editor + if editor_factory is None: + editor_factory = trait.get_editor().trait_set( + **item.editor_args) + + if editor_factory is None: + # FIXME grab from traitsui.editors instead + from .text_editor import ToolkitEditorFactory + editor_factory = ToolkitEditorFactory() + + if item.format_func is not None: + editor_factory.format_func = item.format_func + + if item.format_str != '': + editor_factory.format_str = item.format_str + + if item.invalid != '': + editor_factory.invalid = item.invalid + + factory_method = getattr(editor_factory, item.style + '_editor') + editor = factory_method( + ui, obj, name, item.tooltip, None + ).trait_set(item=item, object_name=item.object) + + # Tell the editor to actually build the editing widget. Note that + # "inner" is a layout. This shouldn't matter as individual editors + # shouldn't be using it as a parent anyway. The important thing is + # that it is not None (otherwise the main TraitsUI code can change + # the "kind" of the created UI object). + editor.prepare(inner) + control = editor.control + + editor.enabled = editor_factory.enabled + if item.show_label: + label = self._create_label(item, ui, desc) + self._add_widget(inner, label, row, col, show_labels, + show_left) + else: + label = None + + editor.label_control = label + + self._add_widget(inner, control, row, col, show_labels) + + # bind to the UIinfo + id = item.id or name + info.bind(id, editor, item.id) + + # add to the list of editors + ui._editors.append(editor) + + # handler may want to know when the editor is defined + defined = getattr(handler, id + '_defined', None) + if defined is not None: + ui.add_defined(defined) + + # add visible_when and enabled_when hooks + if item.visible_when != '': + ui.add_visible(item.visible_when, editor) + if item.enabled_when != '': + ui.add_enabled(item.enabled_when, editor) + + return outer + + def _add_widget(self, layout, w, row, column, show_labels, + label_alignment='left'): + #print(layout) + if row < 0: + # we have an HBox or VBox + layout.children += (w,) + else: + if self.group.orientation == 'vertical': + row, column = column, row + + if show_labels: + column *= 2 + +# if __name__ == '__main__': +# from traitsui.api import VGroup, Item, View, UI, default_handler +# +# test_view = View( +# VGroup( +# Item(name='', label='test'), +# show_labels=False, +# ), +# ) +# ui = UI(view=test_view, +# context={}, +# handler=default_handler(), +# view_elements=None, +# title=test_view.title, +# id='', +# scrollable=False) +# +# #print(panel(ui)) diff --git a/traitsui/item.py b/traitsui/item.py index c6b49a101..b99a33112 100644 --- a/traitsui/item.py +++ b/traitsui/item.py @@ -312,6 +312,20 @@ def is_spacer(self): return ((name == '') or (name == '_') or (all_digits.match(name) is not None)) + def is_label(self): + return not self.name and self.label + + def is_separator(self): + return self.name == '_' + + def get_spacing(self): + if self.name == ' ': + return 5 + elif all_digits.match(self.name): + return int(self.name) + else: + return None + #------------------------------------------------------------------------- # Gets the help text associated with the Item in a specified UI: #------------------------------------------------------------------------- diff --git a/traitsui/menu.py b/traitsui/menu.py index eef5a661b..9d2f47924 100644 --- a/traitsui/menu.py +++ b/traitsui/menu.py @@ -124,16 +124,16 @@ class Action(PyFaceAction): ) #: The standard Traits UI menu bar -StandardMenuBar = MenuBar( - Menu(CloseAction, - name='File'), - Menu(UndoAction, - RedoAction, - RevertAction, - name='Edit'), - Menu(HelpAction, - name='Help') -) +# StandardMenuBar = MenuBar( +# Menu(CloseAction, +# name='File'), +# Menu(UndoAction, +# RedoAction, +# RevertAction, +# name='Edit'), +# Menu(HelpAction, +# name='Help') +# ) #------------------------------------------------------------------------- # Standard buttons (i.e. actions): diff --git a/traitsui/qt4/color_trait.py b/traitsui/qt4/color_trait.py index 507df39bc..5cd03c1d2 100644 --- a/traitsui/qt4/color_trait.py +++ b/traitsui/qt4/color_trait.py @@ -28,7 +28,7 @@ def convert_to_color(object, name, value): - """ Converts a number into a QColor object. + """ Converts a number into a CSV color string. """ # Try the toolkit agnostic format. try: diff --git a/traitsui/qt4/ui_panel.py b/traitsui/qt4/ui_panel.py index 7be91330a..8ca408a49 100644 --- a/traitsui/qt4/ui_panel.py +++ b/traitsui/qt4/ui_panel.py @@ -705,9 +705,7 @@ def _add_items(self, content, outer=None): columns = group.columns # See if a label is needed. - show_labels = False - for item in content: - show_labels |= item.show_label + show_labels = any(item.show_label for item in content) # See if a grid layout is needed. if show_labels or columns > 1: From 90176385a7bbc5b1f38855870b1ff3cceb1e9cba Mon Sep 17 00:00:00 2001 From: Corran Webster Date: Sun, 15 Jul 2018 11:48:50 -0500 Subject: [PATCH 04/22] Test Jupyter notebook. --- traitsui/ipywidgets/TestIPyWidgets.ipynb | 182 +++++++++++++++++++++++ 1 file changed, 182 insertions(+) create mode 100644 traitsui/ipywidgets/TestIPyWidgets.ipynb diff --git a/traitsui/ipywidgets/TestIPyWidgets.ipynb b/traitsui/ipywidgets/TestIPyWidgets.ipynb new file mode 100644 index 000000000..b78420f72 --- /dev/null +++ b/traitsui/ipywidgets/TestIPyWidgets.ipynb @@ -0,0 +1,182 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "from traits.etsconfig.api import ETSConfig\n", + "\n", + "ETSConfig.toolkit = 'ipywidgets'" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "( at 0x108ea8810, file \"\", line 1>, {'object': <__main__.TestObj object at 0x109137468>, 'handler': })\n", + "<__main__.TestObj object at 0x109137468>\n", + "( at 0x108ea8810, file \"\", line 1>, {'object': <__main__.TestObj object at 0x109137468>, 'handler': })\n", + "<__main__.TestObj object at 0x109137468>\n" + ] + } + ], + "source": [ + "from traits.api import HasTraits, Unicode, Bool\n", + "\n", + "from traitsui.group import VGroup\n", + "from traitsui.item import Item\n", + "from traitsui.view import View\n", + "from traitsui.ui import UI\n", + "\n", + "class TestObj(HasTraits):\n", + " text = Unicode\n", + " boolean = Bool\n", + "\n", + "test_obj = TestObj()\n", + " \n", + "test_view = View(\n", + " VGroup(\n", + " Item(name='', label='test'),\n", + " Item('text'),\n", + " Item('boolean'),\n", + " show_labels=False,\n", + " ),\n", + " kind='panel'\n", + ")\n", + "#test_view.edit_traits(context={'object': test_obj}, kind='live')\n", + "ui = test_obj.edit_traits(view=test_view, kind='live')\n", + "\n", + "#ui = test_view.ui(context={'object': test_obj}, kind='live')\n", + "\n", + "# ui = UI(view=test_view,\n", + "# context={},\n", + "# handler=default_handler(),\n", + "# view_elements=None,\n", + "# title=test_view.title,\n", + "# id='',\n", + "# scrollable=False)\n" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "50f1263c423e4725bfd662574c9895fc", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "VBox(children=(Label(value='test'), Text(value=''), Checkbox(value=False)))" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "from IPython.display import display\n", + "#from traitsui.ipywidgets.ui_panel import panel\n", + "\n", + "t = ui.control\n", + "display(t)" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "''" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "test_obj.text" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "test_obj.text = \"Other way\"" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "False" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "test_obj.boolean" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [], + "source": [ + "test_obj.boolean = True" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.5.2" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} From a73b3f222d66e13b438b814c010a52ffb38e03f8 Mon Sep 17 00:00:00 2001 From: Corran Webster Date: Sun, 15 Jul 2018 12:50:32 -0500 Subject: [PATCH 05/22] Get grid layouts working. --- traitsui/ipywidgets/ui_panel.py | 99 +++++++++++++++++++++++++++++---- 1 file changed, 89 insertions(+), 10 deletions(-) diff --git a/traitsui/ipywidgets/ui_panel.py b/traitsui/ipywidgets/ui_panel.py index 6e7265482..4136a95ca 100644 --- a/traitsui/ipywidgets/ui_panel.py +++ b/traitsui/ipywidgets/ui_panel.py @@ -5,7 +5,11 @@ import ipywidgets -# Pattern of all digits +#: Characters that are considered punctuation symbols at the end of a label. +#: If a label ends with one of these charactes, we do not append a colon. +LABEL_PUNCTUATION_CHARS = '?=:;,.<>/\\"\'-+#|' + +#: Pattern of all digits all_digits = re.compile(r'\d+') @@ -187,11 +191,16 @@ def _add_items(self, content, outer=None): show_labels = any(item.show_label for item in content) if show_labels or columns > 1: - if self.group.orientation == 'horizontal': - inner = ipywidgets.HBox() - else: - inner = ipywidgets.VBox() - #inner = ipywidgets.GridBox() + # if self.group.orientation == 'horizontal': + # inner = ipywidgets.HBox() + # else: + # inner = ipywidgets.VBox() + inner = ipywidgets.GridBox() + layout = ipywidgets.Layout( + grid_template_columns=' '.join(['auto']*(2*columns)) + ) + inner.layout = layout + print(inner) if outer is None: outer = inner else: @@ -199,6 +208,11 @@ def _add_items(self, content, outer=None): row = 0 + if show_left: + label_alignment = 'left' + else: + label_alignment = 'right' + else: if self.group.orientation == 'horizontal': outer = ipywidgets.HBox() @@ -206,7 +220,7 @@ def _add_items(self, content, outer=None): outer = ipywidgets.VBox() inner = outer row = -1 - show_left = None + label_alignment = None col = -1 for item in content: @@ -237,9 +251,7 @@ def _add_items(self, content, outer=None): continue # XXX can we avoid evals for dots? - print(item.object_, ui.context) obj = eval(item.object_, globals(), ui.context) - print(obj) trait = obj.base_trait(name) desc = trait.desc if trait.desc is not None else '' @@ -279,7 +291,7 @@ def _add_items(self, content, outer=None): if item.show_label: label = self._create_label(item, ui, desc) self._add_widget(inner, label, row, col, show_labels, - show_left) + label_alignment) else: label = None @@ -320,6 +332,73 @@ def _add_widget(self, layout, w, row, column, show_labels, if show_labels: column *= 2 + # Determine whether to place widget on left or right of + # "logical" column. + if (label_alignment == 'left' and not self.group.show_left) or \ + (label_alignment == 'right' and self.group.show_left): + column += 1 + + layout.children += (w,) + print(layout) + + + def _create_label(self, item, ui, desc, suffix=':'): + """Creates an item label. + + When the label is on the left of its component, + it is not empty, and it does not end with a + punctuation character (see :attr:`LABEL_PUNCTUATION_CHARS`), + we append a suffix (by default a colon ':') at the end of the + label text. + + We also set the help on the QLabel control (from item.help) and + the tooltip (it item.desc exists; we add "Specifies " at the start + of the item.desc string). + + Parameters + ---------- + item : Item + The item for which we want to create a label + ui : UI + Current ui object + desc : string + Description of the item, to create an appropriate tooltip + suffix : string + Characters to at the end of the label + + Returns + ------- + label_control : QLabel + The control for the label + """ + + label = item.get_label(ui) + + # append a suffix if the label is on the left and it does + # not already end with a punctuation character + if (label != '' + and label[-1] not in LABEL_PUNCTUATION_CHARS + and self.group.show_left): + label = label + suffix + + # create label controller + label_control = ipywidgets.Label(value=label) + + # if item.emphasized: + # self._add_emphasis(label_control) + + # FIXME: Decide what to do about the help. (The non-standard wx way, + # What's This style help, both?) + #wx.EVT_LEFT_UP( control, show_help_popup ) + label_control.help = item.get_help(ui) + + # FIXME: do people rely on traitsui adding 'Specifies ' to the start + # of every tooltip? It's not flexible at all + # if desc != '': + # label_control.setToolTip('Specifies ' + desc) + + return label_control + # if __name__ == '__main__': # from traitsui.api import VGroup, Item, View, UI, default_handler # From 60ca4a937af6368590ea486480d41666b34ebad5 Mon Sep 17 00:00:00 2001 From: Kuya Takami Date: Sun, 15 Jul 2018 14:36:43 -0500 Subject: [PATCH 06/22] Add button_editor file --- traitsui/ipywidgets/button_editor.py | 104 +++++++++++++++++++++++++++ 1 file changed, 104 insertions(+) create mode 100644 traitsui/ipywidgets/button_editor.py diff --git a/traitsui/ipywidgets/button_editor.py b/traitsui/ipywidgets/button_editor.py new file mode 100644 index 000000000..6a0f3d261 --- /dev/null +++ b/traitsui/ipywidgets/button_editor.py @@ -0,0 +1,104 @@ +""" Defines the various Boolean editors for the PyQt user interface toolkit. +""" + +import ipywidgets as widgets + +from editor import Editor + +from button_editor import SimpleEditor as button_editor + +class SimpleEditor(Editor): + """ Simple style editor for a button. + """ + + #------------------------------------------------------------------------- + # Trait definitions: + #------------------------------------------------------------------------- + + # The button label + label = widgets.Text + + # The list of items in a drop-down menu, if any + #menu_items = List + + # The selected item in the drop-down menu. + selected_item = Str + + #------------------------------------------------------------------------- + # Finishes initializing the editor by creating the underlying toolkit + # widget: + #------------------------------------------------------------------------- + + def init(self, parent): + """ Finishes initializing the editor by creating the underlying toolkit + widget. + """ + label = self.factory.label or self.item.get_label(self.ui) + + if self.factory.values_trait: + self.control = QtGui.QToolButton() + self.control.toolButtonStyle = QtCore.Qt.ToolButtonTextOnly + self.control.setText(self.string_value(label)) + self.object.on_trait_change( + self._update_menu, self.factory.values_trait) + self.object.on_trait_change( + self._update_menu, + self.factory.values_trait + "_items") + self._menu = QtGui.QMenu() + self._update_menu() + self.control.setMenu(self._menu) + + else: + self.control = QtGui.QPushButton(self.string_value(label)) + self._menu = None + self.control.setAutoDefault(False) + + self.sync_value(self.factory.label_value, 'label', 'from') + self.control.clicked.connect(self.update_object) + self.set_tooltip() + + def dispose(self): + """ Disposes of the contents of an editor. + """ + if self.control is not None: + self.control.clicked.disconnect(self.update_object) + super(SimpleEditor, self).dispose() + + def _label_changed(self, label): + self.control.setText(self.string_value(label)) + + def _update_menu(self): + self._menu.blockSignals(True) + self._menu.clear() + for item in getattr(self.object, self.factory.values_trait): + action = self._menu.addAction(item) + action.triggered.connect( + lambda event, name=item: self._menu_selected(name)) + self.selected_item = "" + self._menu.blockSignals(False) + + def _menu_selected(self, item_name): + self.selected_item = item_name + self.label = item_name + + def update_object(self): + """ Handles the user clicking the button by setting the factory value + on the object. + """ + if self.control is None: + return + if self.selected_item != "": + self.value = self.selected_item + else: + self.value = self.factory.value + + # If there is an associated view, then display it: + if (self.factory is not None) and (self.factory.view is not None): + self.object.edit_traits(view=self.factory.view, + parent=self.control) + + def update_editor(self): + """ Updates the editor when the object trait changes externally to the + editor. + """ + pass From 2ffcfb4d3c0e2b63abbd7584cf7967992b97ef58 Mon Sep 17 00:00:00 2001 From: Corran Webster Date: Sun, 15 Jul 2018 16:26:08 -0500 Subject: [PATCH 07/22] More work on getting things up and running. Tabbed and VFold now work. --- traitsui/ipywidgets/TestIPyWidgets.ipynb | 170 +++++++++++++++++------ traitsui/ipywidgets/toolkit.py | 47 +++++++ traitsui/ipywidgets/ui_panel.py | 73 ++++++++-- 3 files changed, 234 insertions(+), 56 deletions(-) diff --git a/traitsui/ipywidgets/TestIPyWidgets.ipynb b/traitsui/ipywidgets/TestIPyWidgets.ipynb index b78420f72..349ec9db8 100644 --- a/traitsui/ipywidgets/TestIPyWidgets.ipynb +++ b/traitsui/ipywidgets/TestIPyWidgets.ipynb @@ -15,22 +15,11 @@ "cell_type": "code", "execution_count": 2, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "( at 0x108ea8810, file \"\", line 1>, {'object': <__main__.TestObj object at 0x109137468>, 'handler': })\n", - "<__main__.TestObj object at 0x109137468>\n", - "( at 0x108ea8810, file \"\", line 1>, {'object': <__main__.TestObj object at 0x109137468>, 'handler': })\n", - "<__main__.TestObj object at 0x109137468>\n" - ] - } - ], + "outputs": [], "source": [ - "from traits.api import HasTraits, Unicode, Bool\n", + "from traits.api import HasTraits, Unicode, Bool, Int\n", "\n", - "from traitsui.group import VGroup\n", + "from traitsui.group import VGroup, Tabbed, VFold, VGrid\n", "from traitsui.item import Item\n", "from traitsui.view import View\n", "from traitsui.ui import UI\n", @@ -38,46 +27,58 @@ "class TestObj(HasTraits):\n", " text = Unicode\n", " boolean = Bool\n", + " integer = Int\n", "\n", "test_obj = TestObj()\n", " \n", "test_view = View(\n", - " VGroup(\n", - " Item(name='', label='test'),\n", - " Item('text'),\n", - " Item('boolean'),\n", - " show_labels=False,\n", + " Tabbed(\n", + " VGroup(\n", + " Item(label='test'),\n", + " Item('text'),\n", + " label=\"Tab 1\",\n", + " ),\n", + " VGroup(\n", + " Item('boolean'),\n", + " Item('integer'),\n", + " label=\"Tab 2\"\n", + " ),\n", " ),\n", - " kind='panel'\n", ")\n", - "#test_view.edit_traits(context={'object': test_obj}, kind='live')\n", - "ui = test_obj.edit_traits(view=test_view, kind='live')\n", - "\n", - "#ui = test_view.ui(context={'object': test_obj}, kind='live')\n", - "\n", - "# ui = UI(view=test_view,\n", - "# context={},\n", - "# handler=default_handler(),\n", - "# view_elements=None,\n", - "# title=test_view.title,\n", - "# id='',\n", - "# scrollable=False)\n" + "ui = test_obj.edit_traits(view=test_view, kind='live')\n" ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Tab(children=(GridBox(children=(Label(value='test'), Label(value=''), Label(value='Text:'), Text(value='')), layout=Layout(grid_template_columns='auto auto')), GridBox(children=(Label(value='Boolean:'), Checkbox(value=False), Label(value='Integer:'), Text(value='0')), layout=Layout(grid_template_columns='auto auto'))), _titles={'0': 'Tab 1', '1': 'Tab 2'})\n" + ] + } + ], + "source": [ + "print(ui.control)" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, "outputs": [ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "50f1263c423e4725bfd662574c9895fc", + "model_id": "ad6fbfa88e5d47088de44c5fb1d77511", "version_major": 2, "version_minor": 0 }, "text/plain": [ - "VBox(children=(Label(value='test'), Text(value=''), Checkbox(value=False)))" + "Tab(children=(GridBox(children=(Label(value='test'), Label(value=''), Label(value='Text:'), Text(value='')), l…" ] }, "metadata": {}, @@ -89,12 +90,36 @@ "#from traitsui.ipywidgets.ui_panel import panel\n", "\n", "t = ui.control\n", - "display(t)" + "t" ] }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "d861bee9d0db4fff9da86ce049d860c2", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Tab(children=(GridBox(children=(Label(value='test'), Label(value=''), Label(value='Text:'), Text(value='')), l…" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "test_obj.configure_traits(view=test_view, kind='live')\n" + ] + }, + { + "cell_type": "code", + "execution_count": 6, "metadata": {}, "outputs": [ { @@ -103,7 +128,7 @@ "''" ] }, - "execution_count": 4, + "execution_count": 6, "metadata": {}, "output_type": "execute_result" } @@ -114,7 +139,7 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 7, "metadata": {}, "outputs": [], "source": [ @@ -123,7 +148,7 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 8, "metadata": {}, "outputs": [ { @@ -132,7 +157,7 @@ "False" ] }, - "execution_count": 9, + "execution_count": 8, "metadata": {}, "output_type": "execute_result" } @@ -141,6 +166,35 @@ "test_obj.boolean" ] }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "0" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "test_obj.integer" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [], + "source": [ + "test_obj.integer = 100" + ] + }, { "cell_type": "code", "execution_count": 11, @@ -150,6 +204,40 @@ "test_obj.boolean = True" ] }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "5ca16f2ce9a747f1ac13b2e41bf034e5", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "GridBox(children=(Button(layout=Layout(height='auto', width='auto'), style=ButtonStyle(button_color='darkseagr…" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "from ipywidgets import GridBox, Button, ButtonStyle, Layout\n", + "gb = GridBox(children=[Button(layout=Layout(width='auto', height='auto'),\n", + " style=ButtonStyle(button_color='darkseagreen')) for i in range(9)\n", + " ],\n", + " layout=Layout(\n", + " width='50%',\n", + " grid_template_columns='100px 50px 100px',\n", + " grid_template_rows='80px auto 80px',\n", + " grid_gap='5px 10px')\n", + " )\n", + "display(gb)" + ] + }, { "cell_type": "code", "execution_count": null, @@ -174,7 +262,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.5.2" + "version": "3.6.0" } }, "nbformat": 4, diff --git a/traitsui/ipywidgets/toolkit.py b/traitsui/ipywidgets/toolkit.py index 034bf1339..c43d9b5d8 100644 --- a/traitsui/ipywidgets/toolkit.py +++ b/traitsui/ipywidgets/toolkit.py @@ -5,6 +5,7 @@ from pyface.base_toolkit import Toolkit as PyfaceToolkit from traitsui.toolkit import Toolkit +from IPython.display import display import ipywidgets @@ -18,6 +19,7 @@ def ui_handler(handler, *args, **kwds): """ Handles UI notification handler requests that occur on a thread other than the UI thread. """ + # XXX should really have some sort of queue based system handler(*args, **kwds) set_ui_handler(ui_handler) @@ -34,6 +36,51 @@ def ui_live(self, ui, parent): from .ui_panel import ui_panel ui_panel(ui, parent) + def view_application(self, context, view, kind=None, handler=None, + id='', scrollable=None, args=None): + """ Creates a PyQt modal dialog user interface that + runs as a complete application, using information from the + specified View object. + + Parameters + ---------- + context : object or dictionary + A single object or a dictionary of string/object pairs, whose trait + attributes are to be edited. If not specified, the current object is + used. + view : view or string + A View object that defines a user interface for editing trait + attribute values. + kind : string + The type of user interface window to create. See the + **traitsui.view.kind_trait** trait for values and + their meanings. If *kind* is unspecified or None, the **kind** + attribute of the View object is used. + handler : Handler object + A handler object used for event handling in the dialog box. If + None, the default handler for Traits UI is used. + id : string + A unique ID for persisting preferences about this user interface, + such as size and position. If not specified, no user preferences + are saved. + scrollable : Boolean + Indicates whether the dialog box should be scrollable. When set to + True, scroll bars appear on the dialog box if it is not large enough + to display all of the items in the view at one time. + + """ + ui = view.ui( + context, + kind=kind, + handler=handler, + id=id, + scrollable=scrollable, + args=args + ) + display(ui.control) + # XXX ideally this would spawn a web server that has enough support of + # IPyWidgets to interface + def rebuild_ui(self, ui): """ Rebuilds a UI after a change to the content of the UI. """ diff --git a/traitsui/ipywidgets/ui_panel.py b/traitsui/ipywidgets/ui_panel.py index 4136a95ca..1c40430b8 100644 --- a/traitsui/ipywidgets/ui_panel.py +++ b/traitsui/ipywidgets/ui_panel.py @@ -1,9 +1,12 @@ import re +import ipywidgets + +from traits.api import Any from traitsui.base_panel import BasePanel from traitsui.group import Group +from traitsui.ipywidgets.editor import Editor -import ipywidgets #: Characters that are considered punctuation symbols at the end of a label. #: If a label ends with one of these charactes, we do not append a colon. @@ -105,12 +108,19 @@ def __init__(self, group, ui, suppress_label=False): self.ui = ui outer = sub = inner = None - if group.label != "": + + # Get the group label. + if suppress_label: + label = "" + else: + label = group.label + + if label != "": if group.orientation == 'horizontal': outer = inner = ipywidgets.HBox() else: outer = inner = ipywidgets.VBox() - inner.children += (ipywidgets.Label(value=group.label),) + inner.children += (ipywidgets.Label(value=label),) if len(content) == 0: pass @@ -191,16 +201,11 @@ def _add_items(self, content, outer=None): show_labels = any(item.show_label for item in content) if show_labels or columns > 1: - # if self.group.orientation == 'horizontal': - # inner = ipywidgets.HBox() - # else: - # inner = ipywidgets.VBox() inner = ipywidgets.GridBox() layout = ipywidgets.Layout( grid_template_columns=' '.join(['auto']*(2*columns)) ) inner.layout = layout - print(inner) if outer is None: outer = inner else: @@ -209,9 +214,9 @@ def _add_items(self, content, outer=None): row = 0 if show_left: - label_alignment = 'left' - else: label_alignment = 'right' + else: + label_alignment = 'left' else: if self.group.orientation == 'horizontal': @@ -235,6 +240,8 @@ def _add_items(self, content, outer=None): if label != "": label = ipywidgets.Label(value=label) self._add_widget(inner, label, row, col, show_labels) + if show_labels: + inner.children += (ipywidgets.Label(value=''),) continue if name == '_': @@ -321,7 +328,6 @@ def _add_items(self, content, outer=None): def _add_widget(self, layout, w, row, column, show_labels, label_alignment='left'): - #print(layout) if row < 0: # we have an HBox or VBox layout.children += (w,) @@ -334,12 +340,11 @@ def _add_widget(self, layout, w, row, column, show_labels, # Determine whether to place widget on left or right of # "logical" column. - if (label_alignment == 'left' and not self.group.show_left) or \ - (label_alignment == 'right' and self.group.show_left): + if (label_alignment is not None and not self.group.show_left) or \ + (label_alignment is None and self.group.show_left): column += 1 layout.children += (w,) - print(layout) def _create_label(self, item, ui, desc, suffix=':'): @@ -399,6 +404,45 @@ def _create_label(self, item, ui, desc, suffix=':'): return label_control + +class GroupEditor(Editor): + """ A pseudo-editor that allows a group to be managed. + """ + + def __init__(self, **traits): + """ Initialise the object. + """ + self.trait_set(**traits) + + + +class PagedGroupEditor(GroupEditor): + """ A pseudo-editor that allows a group with a 'tabbed' or 'fold' layout to + be managed. + """ + + # The QTabWidget or QToolBox for the group + container = Any + + #-- UI preference save/restore interface --------------------------------- + + def restore_prefs(self, prefs): + """ Restores any saved user preference information associated with the + editor. + """ + if isinstance(prefs, dict): + current_index = prefs.get('current_index') + else: + current_index = prefs + + self.container.setCurrentIndex(int(current_index)) + + def save_prefs(self): + """ Returns any user preference information associated with the editor. + """ + return {'current_index': str(self.container.currentIndex())} + + # if __name__ == '__main__': # from traitsui.api import VGroup, Item, View, UI, default_handler # @@ -416,4 +460,3 @@ def _create_label(self, item, ui, desc, suffix=':'): # id='', # scrollable=False) # -# #print(panel(ui)) From 1c4c74d277ac815b7bd8b422cdac7f8c6e717b95 Mon Sep 17 00:00:00 2001 From: Kuya Takami Date: Sun, 15 Jul 2018 16:40:21 -0500 Subject: [PATCH 08/22] ENH: add html editor for ipywidgets --- traitsui/ipywidgets/html_editor.py | 64 ++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 traitsui/ipywidgets/html_editor.py diff --git a/traitsui/ipywidgets/html_editor.py b/traitsui/ipywidgets/html_editor.py new file mode 100644 index 000000000..0b7831043 --- /dev/null +++ b/traitsui/ipywidgets/html_editor.py @@ -0,0 +1,64 @@ +""" Defines the various html editors for the ipywidgets user interface toolkit. +""" +import ipywidgets as widgets + +from traits.api import TraitError, Str + +from editor import Editor + + +class SimpleEditor(Editor): + """ Simple style editor for HTML. + """ + + #------------------------------------------------------------------------- + # Trait definitions: + #------------------------------------------------------------------------- + + # Flag for window styles: + base_style = widgets.HTML + + # Is the HTML editor scrollable? This values override the default. + scrollable = True + + # External objects referenced in the HTML are relative to this URL + base_url = Str + + #------------------------------------------------------------------------- + # Finishes initializing the editor by creating the underlying toolkit + # widget: + #------------------------------------------------------------------------- + + def init(self, parent): + """ Finishes initializing the editor by creating the underlying toolkit + widget. + """ + factory = self.factory + wtype = self.base_style + + control = wtype(velue=self.str_value, description='') + + self.control = control + self.base_url = factory.base_url + self.sync_value(factory.base_url_name, 'base_url', 'from') + + #------------------------------------------------------------------------- + # 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. + """ + text = self.str_value + if self.factory.format_text: + text = self.factory.parse_text(text) + + self.control.value = text + + #-- Event Handlers ------------------------------------------------------- + + def _base_url_changed(self): + self.update_editor() + +#-EOF-------------------------------------------------------------------------- From 23b85355fe03bf91baa46ef51c4279b4ee812488 Mon Sep 17 00:00:00 2001 From: Kuya Takami Date: Sun, 15 Jul 2018 16:43:31 -0500 Subject: [PATCH 09/22] CLN: remove button editor from ipywidgets --- traitsui/ipywidgets/button_editor.py | 104 --------------------------- 1 file changed, 104 deletions(-) delete mode 100644 traitsui/ipywidgets/button_editor.py diff --git a/traitsui/ipywidgets/button_editor.py b/traitsui/ipywidgets/button_editor.py deleted file mode 100644 index 6a0f3d261..000000000 --- a/traitsui/ipywidgets/button_editor.py +++ /dev/null @@ -1,104 +0,0 @@ -""" Defines the various Boolean editors for the PyQt user interface toolkit. -""" - -import ipywidgets as widgets - -from editor import Editor - -from button_editor import SimpleEditor as button_editor - -class SimpleEditor(Editor): - """ Simple style editor for a button. - """ - - #------------------------------------------------------------------------- - # Trait definitions: - #------------------------------------------------------------------------- - - # The button label - label = widgets.Text - - # The list of items in a drop-down menu, if any - #menu_items = List - - # The selected item in the drop-down menu. - selected_item = Str - - #------------------------------------------------------------------------- - # Finishes initializing the editor by creating the underlying toolkit - # widget: - #------------------------------------------------------------------------- - - def init(self, parent): - """ Finishes initializing the editor by creating the underlying toolkit - widget. - """ - label = self.factory.label or self.item.get_label(self.ui) - - if self.factory.values_trait: - self.control = QtGui.QToolButton() - self.control.toolButtonStyle = QtCore.Qt.ToolButtonTextOnly - self.control.setText(self.string_value(label)) - self.object.on_trait_change( - self._update_menu, self.factory.values_trait) - self.object.on_trait_change( - self._update_menu, - self.factory.values_trait + "_items") - self._menu = QtGui.QMenu() - self._update_menu() - self.control.setMenu(self._menu) - - else: - self.control = QtGui.QPushButton(self.string_value(label)) - self._menu = None - self.control.setAutoDefault(False) - - self.sync_value(self.factory.label_value, 'label', 'from') - self.control.clicked.connect(self.update_object) - self.set_tooltip() - - def dispose(self): - """ Disposes of the contents of an editor. - """ - if self.control is not None: - self.control.clicked.disconnect(self.update_object) - super(SimpleEditor, self).dispose() - - def _label_changed(self, label): - self.control.setText(self.string_value(label)) - - def _update_menu(self): - self._menu.blockSignals(True) - self._menu.clear() - for item in getattr(self.object, self.factory.values_trait): - action = self._menu.addAction(item) - action.triggered.connect( - lambda event, name=item: self._menu_selected(name)) - self.selected_item = "" - self._menu.blockSignals(False) - - def _menu_selected(self, item_name): - self.selected_item = item_name - self.label = item_name - - def update_object(self): - """ Handles the user clicking the button by setting the factory value - on the object. - """ - if self.control is None: - return - if self.selected_item != "": - self.value = self.selected_item - else: - self.value = self.factory.value - - # If there is an associated view, then display it: - if (self.factory is not None) and (self.factory.view is not None): - self.object.edit_traits(view=self.factory.view, - parent=self.control) - - def update_editor(self): - """ Updates the editor when the object trait changes externally to the - editor. - """ - pass From 285e6341357eaca3576ade1c3be3d3b57cbf0db7 Mon Sep 17 00:00:00 2001 From: Kuya Takami Date: Sun, 15 Jul 2018 16:46:30 -0500 Subject: [PATCH 10/22] FLK: remove unused import --- traitsui/ipywidgets/html_editor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/traitsui/ipywidgets/html_editor.py b/traitsui/ipywidgets/html_editor.py index 0b7831043..e8d93c665 100644 --- a/traitsui/ipywidgets/html_editor.py +++ b/traitsui/ipywidgets/html_editor.py @@ -2,7 +2,7 @@ """ import ipywidgets as widgets -from traits.api import TraitError, Str +from traits.api import Str from editor import Editor From d5b6d628905b3661afff41832d22e297fd9c0c68 Mon Sep 17 00:00:00 2001 From: Prabhu Ramachandran Date: Sat, 21 Jul 2018 02:54:30 -0500 Subject: [PATCH 11/22] Adding a range editor. --- traitsui/ipywidgets/range_editor.py | 593 ++++++++++++++++++++++++++++ 1 file changed, 593 insertions(+) create mode 100644 traitsui/ipywidgets/range_editor.py diff --git a/traitsui/ipywidgets/range_editor.py b/traitsui/ipywidgets/range_editor.py new file mode 100644 index 000000000..ce32ad3ce --- /dev/null +++ b/traitsui/ipywidgets/range_editor.py @@ -0,0 +1,593 @@ +""" Defines the various range editors and the range editor factory, for the +ipywidgets user interface toolkit. +""" + +from math import log10 + +import ipywidgets as widgets + +from traits.api import Str, Float, Any, Bool + +# FIXME: ToolkitEditorFactory is a proxy class defined here just for backward +# compatibility. The class has been moved to the +# traitsui.editors.range_editor file. +from traitsui.editors.range_editor import ToolkitEditorFactory + +from .text_editor import SimpleEditor as TextEditor + +from .editor import Editor + + +class BaseRangeEditor(Editor): + """ The base class for Range editors. Using an evaluate trait, if specified, + when assigning numbers the object trait. + """ + + # **Traits** + + # Function to evaluate floats/ints + evaluate = Any + + def _set_value(self, value): + """Sets the associated object trait's value""" + if self.evaluate is not None: + value = self.evaluate(value) + Editor._set_value(self, value) + + +class SimpleSliderEditor(BaseRangeEditor): + """ Simple style of range editor that displays a slider and a text field. + + The user can set a value either by moving the slider or by typing a value + in the text field. + """ + + # Low value for the slider range + low = Any + + # High value for the slider range + high = Any + + # Formatting string used to format value and labels + format = Str + + def init(self, parent): + """ Finishes initializing the editor by creating the underlying toolkit + widget. + """ + factory = self.factory + if not factory.low_name: + self.low = factory.low + + if not factory.high_name: + self.high = factory.high + + self.format = factory.format + + self.evaluate = factory.evaluate + self.sync_value(factory.evaluate_name, 'evaluate', 'from') + + self.sync_value(factory.low_name, 'low', 'from') + self.sync_value(factory.high_name, 'high', 'from') + + if self.factory.is_float: + self.control = widgets.FloatSlider() + step = (self.high - self.low)/1000 + else: + self.control = widgets.IntSlider() + step = 1 + + fvalue = self.value + + try: + if not (self.low <= fvalue <= self.high): + fvalue = self.low + except: + fvalue = self.low + + slider = self.control + slider.min = self.low + slider.max = self.high + slider.step = step + slider.value = fvalue + slider.readout = True + if not self.factory.is_float: + slider.readout_format = 'd' + elif self.format: + slider.readout_format = self.format[1:] + + slider.observe(self.update_object_on_scroll, 'value') + + self.set_tooltip(slider) + + def update_object_on_scroll(self, event=None): + """ Handles the user changing the current slider value. + """ + try: + self.value = self.control.value + except Exception as exc: + from traitsui.api import raise_to_debug + raise_to_debug() + + def update_editor(self): + """ Updates the editor when the object trait changes externally to the + editor. + """ + value = self.value + low = self.low + high = self.high + self.control.min = low + self.control.max = high + value = min(max(value, low), high) + self.control.value = value + + def get_error_control(self): + """ Returns the editor's control for indicating error status. + """ + return self.control + + def _low_changed(self, low): + if self.value < low: + if self.factory.is_float: + self.value = float(low) + else: + self.value = int(low) + + if self.control is not None: + self.control.min = low + + def _high_changed(self, high): + if self.value > high: + if self.factory.is_float: + self.value = float(high) + else: + self.value = int(high) + + if self.control is not None: + self.control.max = high + + +class LogRangeSliderEditor(SimpleSliderEditor): + + """ A slider editor for log-spaced values + """ + def init(self, parent): + """ Finishes initializing the editor by creating the underlying toolkit + widget. + """ + factory = self.factory + if not factory.low_name: + self.low = factory.low + + if not factory.high_name: + self.high = factory.high + + self.format = factory.format + + self.evaluate = factory.evaluate + self.sync_value(factory.evaluate_name, 'evaluate', 'from') + + self.sync_value(factory.low_name, 'low', 'from') + self.sync_value(factory.high_name, 'high', 'from') + + self.control = widgets.FloatLogSlider() + fvalue = self.value + + try: + if not (self.low <= fvalue <= self.high): + fvalue = self.low + except: + fvalue = self.low + + slider = self.control + slider.base = 10 + mn, mx = log10(self.low), log10(self.high) + slider.min = mn + slider.max = mx + slider.value = self.value + slider.step = (mx - mn)/1000 + slider.observe(self.update_object_on_scroll, 'value') + + self.set_tooltip(slider) + + def update_editor(self): + """ Updates the editor when the object trait changes externally to the + editor. + """ + value = self.value + low = log10(self.low) + high = log10(self.high) + self.control.min = low + self.control.max = high + value = min(max(value, self.low), self.high) + self.control.value = value + + def _low_changed(self, low): + if self.value < low: + if self.factory.is_float: + self.value = float(low) + else: + self.value = int(low) + + if self.control is not None: + self.control.min = log10(low) + + def _high_changed(self, high): + if self.value > high: + if self.factory.is_float: + self.value = float(high) + else: + self.value = int(high) + + if self.control is not None: + self.control.max = log10(high) + + +class LargeRangeSliderEditor(BaseRangeEditor): + """ A slider editor for large ranges. + + The editor displays a slider and a text field. A subset of the total range + is displayed in the slider; arrow buttons at each end of the slider let + the user move the displayed range higher or lower. + """ + + # Low value for the slider range + low = Any(0) + + # High value for the slider range + high = Any(1) + + # Low end of displayed range + cur_low = Float + + # High end of displayed range + cur_high = Float + + # Flag indicating that the UI is in the process of being updated + ui_changing = Bool(False) + + left_control = Any + right_control = Any + slider = Any + + def init(self, parent): + """ Finishes initializing the editor by creating the underlying toolkit + widget. + """ + factory = self.factory + + # Initialize using the factory range defaults: + self.low = factory.low + self.high = factory.high + self.evaluate = factory.evaluate + + # Hook up the traits to listen to the object. + self.sync_value(factory.low_name, 'low', 'from') + self.sync_value(factory.high_name, 'high', 'from') + self.sync_value(factory.evaluate_name, 'evaluate', 'from') + + self.init_range() + low = self.cur_low + high = self.cur_high + + self._set_format() + + layout = widgets.Layout(width='40px') + self.left_control = widgets.Button(icon='arrow-left', layout=layout) + self.right_control = widgets.Button(icon='arrow-right', layout=layout) + + if self.factory.is_float: + self.slider = widgets.FloatSlider() + step = (self.high - self.low)/1000 + else: + self.slider = widgets.IntSlider() + step = 1 + + self.control = widgets.HBox( + [self.left_control, self.slider, self.right_control] + ) + + fvalue = self.value + + try: + 1 / (low <= fvalue <= high) + except: + fvalue = low + + slider = self.slider + slider.min = 0 + slider.max = 10000 + slider.step = step + slider.value = fvalue + + slider.readout = True + if not self.factory.is_float: + slider.readout_format = 'd' + + slider.observe(self.update_object_on_scroll, 'value') + self.left_control.on_click(self.reduce_range) + self.right_control.on_click(self.increase_range) + + # Text entry: + self.set_tooltip(slider) + self.set_tooltip(self.left_control) + self.set_tooltip(self.right_control) + + # Update the ranges and button just in case. + self.update_range_ui() + + def update_object_on_scroll(self, event=None): + """ Handles the user changing the current slider value. + """ + if not self.ui_changing: + try: + self.value = self.slider.value + except Exception as exc: + from traitsui.api import raise_to_debug + raise_to_debug() + + def update_editor(self): + """ Updates the editor when the object trait changes externally to the + editor. + """ + value = self.value + low = self.low + high = self.high + try: + 1 / (low <= value <= high) + except: + value = low + + if self.factory.is_float: + self.value = float(value) + else: + self.value = int(value) + + self.init_range() + self.ui_changing = True + self.update_range_ui() + self.slider.value = self.value + self.ui_changing = False + + def update_range_ui(self): + """ Updates the slider range controls. + """ + low, high = self.cur_low, self.cur_high + self.slider.min = low + self.slider.max = high + self.slider.step = (high - low)/1000 + + self._set_format() + + def init_range(self): + """ Initializes the slider range controls. + """ + value = self.value + low, high = self.low, self.high + if (high is None) and (low is not None): + high = -low + + mag = abs(value) + if mag <= 10.0: + cur_low = max(value - 10, low) + cur_high = min(value + 10, high) + else: + d = 0.5 * (10**int(log10(mag) + 1)) + cur_low = max(low, value - d) + cur_high = min(high, value + d) + + self.cur_low, self.cur_high = cur_low, cur_high + + def reduce_range(self, event=None): + """ Reduces the extent of the displayed range. + """ + low, high = self.low, self.high + if abs(self.cur_low) < 10: + self.cur_low = max(-10, low) + self.cur_high = min(10, high) + elif self.cur_low > 0: + self.cur_high = self.cur_low + self.cur_low = max(low, self.cur_low / 10) + else: + self.cur_high = self.cur_low + self.cur_low = max(low, self.cur_low * 10) + + self.update_range_ui() + self.value = min(max(self.value, self.cur_low), self.cur_high) + + def increase_range(self, event=None): + """ Increased the extent of the displayed range. + """ + low, high = self.low, self.high + if abs(self.cur_high) < 10: + self.cur_low = max(-10, low) + self.cur_high = min(10, high) + elif self.cur_high > 0: + self.cur_low = self.cur_high + self.cur_high = min(high, self.cur_high * 10) + else: + self.cur_low = self.cur_high + self.cur_high = min(high, self.cur_high / 10) + + self.update_range_ui() + self.value = min(max(self.value, self.cur_low), self.cur_high) + + def _set_format(self): + self._format = '%d' + factory = self.factory + low, high = self.cur_low, self.cur_high + diff = high - low + if factory.is_float: + if diff > 99999: + self._format = '.2g' + elif diff > 1: + self._format = '.%df' % max(0, 4 - + int(log10(high - low))) + else: + self._format = '.3f' + + def get_error_control(self): + """ Returns the editor's control for indicating error status. + """ + return self.control + + def _low_changed(self, low): + if self.control is not None: + if self.value < low: + if self.factory.is_float: + self.value = float(low) + else: + self.value = int(low) + + self.update_editor() + + def _high_changed(self, high): + if self.control is not None: + if self.value > high: + if self.factory.is_float: + self.value = float(high) + else: + self.value = int(high) + + self.update_editor() + + +class SimpleSpinEditor(BaseRangeEditor): + """ A simple style of range editor that displays a spin box control. + """ + + # Low value for the slider range + low = Any + + # High value for the slider range + high = Any + + def init(self, parent): + """ Finishes initializing the editor by creating the underlying toolkit + widget. + """ + factory = self.factory + if not factory.low_name: + self.low = factory.low + + if not factory.high_name: + self.high = factory.high + + self.sync_value(factory.low_name, 'low', 'from') + self.sync_value(factory.high_name, 'high', 'from') + + if self.factory.is_float: + self.control = widgets.FloatText() + else: + self.control = widgets.IntText() + self.control.value = self.value + self.control.observe(self.update_object, 'value') + self.set_tooltip() + + def update_object(self, event=None): + """ Handles the user selecting a new value in the spin box. + """ + val = self.control.value + self.value = min(max(val, self.low), self.high) + if self.value != val: + self.control.value = self.value + + def update_editor(self): + """ Updates the editor when the object trait changes externally to the + editor. + """ + self.control.value = self.value + + def _low_changed(self, low): + if self.value < low: + if self.factory.is_float: + self.value = float(low) + else: + self.value = int(low) + + if self.control: + self.control.value = self.value + + def _high_changed(self, high): + if self.value > high: + if self.factory.is_float: + self.value = float(high) + else: + self.value = int(high) + + if self.control: + self.control.value = self.value + + +class RangeTextEditor(TextEditor): + """ Editor for ranges that displays a text field. If the user enters a + value that is outside the allowed range, the background of the field + changes color to indicate an error. + """ + + # Function to evaluate floats/ints + evaluate = Any + + def init(self, parent): + """ Finishes initializing the editor by creating the underlying toolkit + widget. + """ + TextEditor.init(self, parent) + self.evaluate = self.factory.evaluate + self.sync_value(self.factory.evaluate_name, 'evaluate', 'from') + + def update_object(self): + """ Handles the user entering input data in the edit control. + """ + if (not self._no_update) and (self.control is not None): + try: + value = eval(self.control.value) + if self.evaluate is not None: + value = self.evaluate(value) + self.value = value + self.set_error_state(False) + except: + self.set_error_state(True) + + +def SimpleEnumEditor(parent, factory, ui, object, name, description): + return CustomEnumEditor(parent, factory, ui, object, name, description, + 'simple') + + +def CustomEnumEditor(parent, factory, ui, object, name, description, + style='custom'): + """ Factory adapter that returns a enumeration editor of the specified + style. + """ + if factory._enum is None: + import traitsui.editors.enum_editor as enum_editor + factory._enum = enum_editor.ToolkitEditorFactory( + values=range(factory.low, factory.high + 1), + cols=factory.cols) + + if style == 'simple': + return factory._enum.simple_editor(ui, object, name, description, + parent) + + return factory._enum.custom_editor(ui, object, name, description, parent) + + +# Defines the mapping between editor factory 'mode's and Editor classes: +SimpleEditorMap = { + 'slider': SimpleSliderEditor, + 'xslider': LargeRangeSliderEditor, + 'spinner': SimpleSpinEditor, + 'enum': SimpleEnumEditor, + 'text': RangeTextEditor, + 'logslider': LogRangeSliderEditor +} +# Mapping between editor factory modes and custom editor classes +CustomEditorMap = { + 'slider': SimpleSliderEditor, + 'xslider': LargeRangeSliderEditor, + 'spinner': SimpleSpinEditor, + 'enum': CustomEnumEditor, + 'text': RangeTextEditor, + 'logslider': LogRangeSliderEditor +} From e622c7b18d8506d5b1087822f9fb1c8cb2e01caa Mon Sep 17 00:00:00 2001 From: Prabhu Ramachandran Date: Sun, 22 Jul 2018 13:43:26 -0400 Subject: [PATCH 12/22] Adding a very simple button editor. This does not quite work yet as the base traitsui/editors/button_editor uses resources from pyface. pyface doesn't implement resource_manager:PyfaceResourceFactory for the ipywidgets backend. --- traitsui/ipywidgets/button_editor.py | 97 ++++++++++++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 traitsui/ipywidgets/button_editor.py diff --git a/traitsui/ipywidgets/button_editor.py b/traitsui/ipywidgets/button_editor.py new file mode 100644 index 000000000..6333264ed --- /dev/null +++ b/traitsui/ipywidgets/button_editor.py @@ -0,0 +1,97 @@ +"""Defines the various button editors for the ipywidgets user interface +toolkit. +""" + +import ipywidgets as widgets + +from traits.api import Unicode, Str + +# FIXME: ToolkitEditorFactory is a proxy class defined here just for backward +# compatibility. The class has been moved to the +# traitsui.editors.button_editor file. +from traitsui.editors.button_editor import ToolkitEditorFactory + +from editor import Editor + + +class SimpleEditor(Editor): + """ Simple style editor for a button. + """ + + # The button label + label = Unicode + + # The selected item in the drop-down menu. + selected_item = Str + + def init(self, parent): + """ Finishes initializing the editor by creating the underlying toolkit + widget. + """ + label = self.factory.label or self.item.get_label(self.ui) + + # FIXME: the button widget does not support images apparently. + # FIXME: Menus are not supported currently so ... + if self.factory.values_trait: + raise RuntimeError('ipywidgets does not yet support this feature.') + else: + self.control = widgets.Button(description=self.string_value(label)) + + self.sync_value(self.factory.label_value, 'label', 'from') + self.control.on_click(self.update_object) + self.set_tooltip() + + def dispose(self): + """ Disposes of the contents of an editor. + """ + if self.control is not None: + self.control.on_click(self.update_object, remove=False) + super(SimpleEditor, self).dispose() + + def _label_changed(self, label): + self.control.description = self.string_value(label) + + def update_object(self, event=None): + """ Handles the user clicking the button by setting the factory value + on the object. + """ + if self.control is None: + return + if self.selected_item != "": + self.value = self.selected_item + else: + self.value = self.factory.value + + # If there is an associated view, then display it: + if (self.factory is not None) and (self.factory.view is not None): + self.object.edit_traits(view=self.factory.view, + parent=self.control) + + def update_editor(self): + """ Updates the editor when the object trait changes externally to the + editor. + """ + pass + + +class CustomEditor(SimpleEditor): + """ Custom style editor for a button, which can contain an image. + """ + + def init(self, parent): + """ Finishes initializing the editor by creating the underlying toolkit + widget. + """ + # FIXME: We ignore orientation, width_padding, the icon, + # and height_padding + + factory = self.factory + if factory.label: + label = factory.label + else: + label = self.item.get_label(self.ui) + self.control = widgets.Button(description=self.string_value(label)) + + self.sync_value(self.factory.label_value, 'label', 'from') + self.control.on_click(self.update_object) + self.set_tooltip() From 21e752919c416a3f4d3ea53a2e71fbabd3da3ad5 Mon Sep 17 00:00:00 2001 From: Prabhu Ramachandran Date: Sun, 22 Jul 2018 13:50:43 -0400 Subject: [PATCH 13/22] Update notebook with more traits. --- traitsui/ipywidgets/TestIPyWidgets.ipynb | 91 ++++++++++++++++++++---- 1 file changed, 77 insertions(+), 14 deletions(-) diff --git a/traitsui/ipywidgets/TestIPyWidgets.ipynb b/traitsui/ipywidgets/TestIPyWidgets.ipynb index 349ec9db8..ae131780c 100644 --- a/traitsui/ipywidgets/TestIPyWidgets.ipynb +++ b/traitsui/ipywidgets/TestIPyWidgets.ipynb @@ -2,7 +2,7 @@ "cells": [ { "cell_type": "code", - "execution_count": 1, + "execution_count": 2, "metadata": {}, "outputs": [], "source": [ @@ -13,17 +13,83 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": 5, "metadata": {}, "outputs": [], "source": [ - "from traits.api import HasTraits, Unicode, Bool, Int\n", + "from traits.api import HasTraits, Unicode, Bool, Int, Range, Button\n", "\n", "from traitsui.group import VGroup, Tabbed, VFold, VGrid\n", "from traitsui.item import Item\n", "from traitsui.view import View\n", - "from traitsui.ui import UI\n", + "from traitsui.ui import UI" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [], + "source": [ + "class TestObjInline(HasTraits):\n", + " text = Unicode\n", + " boolean = Bool\n", + " integer = Int\n", + " rng = Range(0, 100, 10)\n", + " large_range = Range(0.0, 10000000.0, 1.0)\n", + " button = Button('Click me!')\n", "\n", + " view = View(\n", + " Tabbed(\n", + " VGroup(\n", + " Item(label='test'),\n", + " Item('text'),\n", + " Item('rng'),\n", + " Item('large_range'),\n", + "# Item('button'),\n", + " label=\"Tab 1\",\n", + " ),\n", + " VGroup(\n", + " Item('boolean'),\n", + " Item('integer'),\n", + " label=\"Tab 2\"\n", + " ),\n", + " ),\n", + " )\n", + "\n", + " def _button_fired(self):\n", + " print(\"Clicked\")\n", + " self.integer += 1\n", + " self.large_range += 1.0" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "<__main__.TestObjInline at 0x10a1478e0>" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "t1 = TestObjInline()\n", + "t1" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [], + "source": [ "class TestObj(HasTraits):\n", " text = Unicode\n", " boolean = Bool\n", @@ -45,12 +111,12 @@ " ),\n", " ),\n", ")\n", - "ui = test_obj.edit_traits(view=test_view, kind='live')\n" + "ui = test_obj.edit_traits(view=test_view, kind='live')" ] }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 9, "metadata": {}, "outputs": [ { @@ -67,13 +133,13 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 10, "metadata": {}, "outputs": [ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "ad6fbfa88e5d47088de44c5fb1d77511", + "model_id": "4503d7dc6a3a4d1fbc05b8cc727265f9", "version_major": 2, "version_minor": 0 }, @@ -86,9 +152,6 @@ } ], "source": [ - "from IPython.display import display\n", - "#from traitsui.ipywidgets.ui_panel import panel\n", - "\n", "t = ui.control\n", "t" ] @@ -206,13 +269,13 @@ }, { "cell_type": "code", - "execution_count": 12, + "execution_count": 1, "metadata": {}, "outputs": [ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "5ca16f2ce9a747f1ac13b2e41bf034e5", + "model_id": "e8bf7960786f436890895053c2b427e4", "version_major": 2, "version_minor": 0 }, @@ -235,7 +298,7 @@ " grid_template_rows='80px auto 80px',\n", " grid_gap='5px 10px')\n", " )\n", - "display(gb)" + "gb" ] }, { From c9bc71cb0b47fd89973140e1e3a544ce73df082c Mon Sep 17 00:00:00 2001 From: Prabhu Ramachandran Date: Sun, 22 Jul 2018 14:07:53 -0400 Subject: [PATCH 14/22] Allow traits object to be immediately displayed. This is convenient but the injected method definitely belongs elsewhere. --- traitsui/ipywidgets/TestIPyWidgets.ipynb | 143 ++++------------------- traitsui/toolkit.py | 8 ++ 2 files changed, 31 insertions(+), 120 deletions(-) diff --git a/traitsui/ipywidgets/TestIPyWidgets.ipynb b/traitsui/ipywidgets/TestIPyWidgets.ipynb index ae131780c..1b627660a 100644 --- a/traitsui/ipywidgets/TestIPyWidgets.ipynb +++ b/traitsui/ipywidgets/TestIPyWidgets.ipynb @@ -2,7 +2,7 @@ "cells": [ { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -13,7 +13,7 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -27,7 +27,7 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -65,20 +65,9 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "<__main__.TestObjInline at 0x10a1478e0>" - ] - }, - "execution_count": 7, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "t1 = TestObjInline()\n", "t1" @@ -86,7 +75,7 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -116,41 +105,18 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Tab(children=(GridBox(children=(Label(value='test'), Label(value=''), Label(value='Text:'), Text(value='')), layout=Layout(grid_template_columns='auto auto')), GridBox(children=(Label(value='Boolean:'), Checkbox(value=False), Label(value='Integer:'), Text(value='0')), layout=Layout(grid_template_columns='auto auto'))), _titles={'0': 'Tab 1', '1': 'Tab 2'})\n" - ] - } - ], + "outputs": [], "source": [ "print(ui.control)" ] }, { "cell_type": "code", - "execution_count": 10, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "4503d7dc6a3a4d1fbc05b8cc727265f9", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "Tab(children=(GridBox(children=(Label(value='test'), Label(value=''), Label(value='Text:'), Text(value='')), l…" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "t = ui.control\n", "t" @@ -158,51 +124,25 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "d861bee9d0db4fff9da86ce049d860c2", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "Tab(children=(GridBox(children=(Label(value='test'), Label(value=''), Label(value='Text:'), Text(value='')), l…" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "test_obj.configure_traits(view=test_view, kind='live')\n" ] }, { "cell_type": "code", - "execution_count": 6, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "''" - ] - }, - "execution_count": 6, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "test_obj.text" ] }, { "cell_type": "code", - "execution_count": 7, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -211,47 +151,25 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "False" - ] - }, - "execution_count": 8, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "test_obj.boolean" ] }, { "cell_type": "code", - "execution_count": 9, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "0" - ] - }, - "execution_count": 9, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "test_obj.integer" ] }, { "cell_type": "code", - "execution_count": 10, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -260,7 +178,7 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -269,24 +187,9 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "e8bf7960786f436890895053c2b427e4", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "GridBox(children=(Button(layout=Layout(height='auto', width='auto'), style=ButtonStyle(button_color='darkseagr…" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "from ipywidgets import GridBox, Button, ButtonStyle, Layout\n", "gb = GridBox(children=[Button(layout=Layout(width='auto', height='auto'),\n", diff --git a/traitsui/toolkit.py b/traitsui/toolkit.py index e91865990..1d0ff9145 100644 --- a/traitsui/toolkit.py +++ b/traitsui/toolkit.py @@ -24,6 +24,7 @@ import logging from traits.trait_base import ETSConfig +from traits.has_traits import HasTraits from pyface.base_toolkit import Toolkit, find_toolkit @@ -35,6 +36,13 @@ _toolkit = None +# FIXME: this definitely belongs elsewhere. +# An injected method to render the object in a Jupyter notebook. +def _repr_html_(self): + self.configure_traits(kind='live') +HasTraits._repr_html_ = _repr_html_ + + def assert_toolkit_import(names): """ Raise an error if a toolkit with the given name should not be allowed to be imported. From 9627db23d691326a8b21529b8fa596210e278acd Mon Sep 17 00:00:00 2001 From: Prabhu Ramachandran Date: Tue, 24 Jul 2018 22:42:16 -0400 Subject: [PATCH 15/22] Mirror changes in #473. --- traitsui/ipywidgets/editor.py | 8 +++++--- traitsui/ipywidgets/ui_panel.py | 19 +++++++++++-------- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/traitsui/ipywidgets/editor.py b/traitsui/ipywidgets/editor.py index 4f87d635c..2012c8a1f 100644 --- a/traitsui/ipywidgets/editor.py +++ b/traitsui/ipywidgets/editor.py @@ -52,11 +52,13 @@ def set_tooltip(self, control=None): """ desc = self.description if desc == '': - desc = self.object.base_trait(self.name).desc + desc = self.object.base_trait(self.name).tooltip if desc is None: - return False + desc = self.object.base_trait(self.name).desc + if desc is None: + return False - desc = 'Specifies ' + desc + desc = 'Specifies ' + desc if control is None: control = self.control diff --git a/traitsui/ipywidgets/ui_panel.py b/traitsui/ipywidgets/ui_panel.py index 1c40430b8..562c65e24 100644 --- a/traitsui/ipywidgets/ui_panel.py +++ b/traitsui/ipywidgets/ui_panel.py @@ -260,7 +260,9 @@ def _add_items(self, content, outer=None): # XXX can we avoid evals for dots? obj = eval(item.object_, globals(), ui.context) trait = obj.base_trait(name) - desc = trait.desc if trait.desc is not None else '' + desc = trait.tooltip + if desc is None: + desc = 'Specifies ' + trait.desc if trait.desc else '' editor_factory = item.editor if editor_factory is None: @@ -346,7 +348,6 @@ def _add_widget(self, layout, w, row, column, show_labels, layout.children += (w,) - def _create_label(self, item, ui, desc, suffix=':'): """Creates an item label. @@ -356,9 +357,11 @@ def _create_label(self, item, ui, desc, suffix=':'): we append a suffix (by default a colon ':') at the end of the label text. - We also set the help on the QLabel control (from item.help) and - the tooltip (it item.desc exists; we add "Specifies " at the start - of the item.desc string). + We also set the help on the Label control (from item.help) and the + tooltip (if the ``tooltip`` metadata on the edited trait exists, then + it will be used as-is; otherwise, if the ``desc`` metadata exists, the + string "Specifies " will be prepended to the start of ``desc``). + Parameters ---------- @@ -375,6 +378,7 @@ def _create_label(self, item, ui, desc, suffix=':'): ------- label_control : QLabel The control for the label + """ label = item.get_label(ui) @@ -397,10 +401,9 @@ def _create_label(self, item, ui, desc, suffix=':'): #wx.EVT_LEFT_UP( control, show_help_popup ) label_control.help = item.get_help(ui) - # FIXME: do people rely on traitsui adding 'Specifies ' to the start - # of every tooltip? It's not flexible at all # if desc != '': - # label_control.setToolTip('Specifies ' + desc) + # # ipywidgets.Label's do not support tooltips. + # label_control.tooltip = desc return label_control From a37da631f4d00e3938fa592329d36a26f9329df3 Mon Sep 17 00:00:00 2001 From: Corran Webster Date: Mon, 30 Jul 2018 14:46:26 +0100 Subject: [PATCH 16/22] First cut at image support for IPyWidgets (untested). --- traitsui/ipywidgets/image_editor.py | 42 +++++++++++++++ traitsui/ipywidgets/image_resource.py | 72 +++++++++++++++++++++++++ traitsui/ipywidgets/resource_manager.py | 36 +++++++++++++ 3 files changed, 150 insertions(+) create mode 100644 traitsui/ipywidgets/image_editor.py create mode 100644 traitsui/ipywidgets/image_resource.py create mode 100644 traitsui/ipywidgets/resource_manager.py diff --git a/traitsui/ipywidgets/image_editor.py b/traitsui/ipywidgets/image_editor.py new file mode 100644 index 000000000..70910e855 --- /dev/null +++ b/traitsui/ipywidgets/image_editor.py @@ -0,0 +1,42 @@ +""" Defines the various html editors for the ipywidgets user interface toolkit. +""" +import ipywidgets as widgets + +from pyface.ui_traits import convert_bitmap + +from .editor import Editor + + +class _ImageEditor(Editor): + """ Simple 'display only' for Image Editor. + """ + + #------------------------------------------------------------------------- + # 'Editor' interface + #------------------------------------------------------------------------- + + def init(self, parent): + """ Finishes initializing the editor by creating the underlying toolkit + widget. + """ + factory = self.factory + image = factory.image + if image is None: + image = self.value + value = convert_bitmap(image) + + self.control = widgets.Image(value=value, description='') + + self.set_tooltip() + + def update_editor(self): + """ Updates the editor when the object trait changes external to the + editor. + """ + if self.factory.image is not None: + return + + image = self.value + value = convert_bitmap(image) + + self.control.value = value diff --git a/traitsui/ipywidgets/image_resource.py b/traitsui/ipywidgets/image_resource.py new file mode 100644 index 000000000..dd89ed4b8 --- /dev/null +++ b/traitsui/ipywidgets/image_resource.py @@ -0,0 +1,72 @@ +# Standard library imports. +import os + +# Enthought library imports. +from traits.api import Any, HasTraits, List, Property, provides +from traits.api import Unicode + +# Local imports. +from pyface.i_image_resource import IImageResource, MImageResource + + +@provides(IImageResource) +class ImageResource(MImageResource, HasTraits): + """ The toolkit specific implementation of an ImageResource. See the + IImageResource interface for the API documentation. + """ + + # 'ImageResource' interface ---------------------------------------------- + + #: The absolute path to the image resource. + absolute_path = Property(Unicode) + + #: The name of the image resource for the resource manager. + name = Unicode + + #: The search path to use when looking up the image. + search_path = List + + # Private interface ------------------------------------------------------ + + #: The resource manager reference for the image. + _ref = Any + + # ------------------------------------------------------------------------ + # 'ImageResource' interface. + # ------------------------------------------------------------------------ + + def create_bitmap(self, size=None): + return self.create_image(size) + + def create_icon(self, size=None): + return self.create_image(size) + + def image_size(cls, image): + """ Get the size of a toolkit image + + Parameters + ---------- + image : toolkit image + A toolkit image to compute the size of. + + Returns + ------- + size : tuple + The (width, height) tuple giving the size of the image. + """ + # FIXME: can't do this without PIL or similar. + return (0, 0) + + # ------------------------------------------------------------------------ + # Private interface. + # ------------------------------------------------------------------------ + + def _get_absolute_path(self): + ref = self._get_ref() + if ref is not None: + absolute_path = os.path.abspath(self._ref.filename) + + else: + absolute_path = self._get_image_not_found().absolute_path + + return absolute_path diff --git a/traitsui/ipywidgets/resource_manager.py b/traitsui/ipywidgets/resource_manager.py new file mode 100644 index 000000000..00900cb89 --- /dev/null +++ b/traitsui/ipywidgets/resource_manager.py @@ -0,0 +1,36 @@ +import base64 +import imghdr +import mimetypes +import os + +# Enthought library imports. +from pyface.resource.api import ResourceFactory + +mimetypes.init() + + +class PyfaceResourceFactory(ResourceFactory): + """ The implementation of a shared resource manager. """ + + # ------------------------------------------------------------------------- + # 'ResourceFactory' interface. + # ------------------------------------------------------------------------- + + def image_from_file(self, filename): + """ Creates an image from the data in the specified filename. """ + + path = os.path.relpath(filename) + return path + + def image_from_data(self, data, filename=None): + """ Creates an image from the specified data. """ + + suffix = imghdr.what(filename, data) + mimetype = mimetypes.suffix_map['.' + suffix] + + data_url = u"data:{};base64,{}".format( + mimetype, + base64.b64encode(data), + ) + + return data_url From 913c5c8dfb12fda9518960e118d9331d7e06c54a Mon Sep 17 00:00:00 2001 From: Corran Webster Date: Sat, 13 Jul 2019 12:02:25 -0500 Subject: [PATCH 17/22] Get image editor working. --- traitsui/ipywidgets/resource_manager.py | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/traitsui/ipywidgets/resource_manager.py b/traitsui/ipywidgets/resource_manager.py index 00900cb89..5be5af89c 100644 --- a/traitsui/ipywidgets/resource_manager.py +++ b/traitsui/ipywidgets/resource_manager.py @@ -25,12 +25,4 @@ def image_from_file(self, filename): def image_from_data(self, data, filename=None): """ Creates an image from the specified data. """ - suffix = imghdr.what(filename, data) - mimetype = mimetypes.suffix_map['.' + suffix] - - data_url = u"data:{};base64,{}".format( - mimetype, - base64.b64encode(data), - ) - - return data_url + return data \ No newline at end of file From b1c60693d2d0f35dc1c00f372eff79d2aebcdfd4 Mon Sep 17 00:00:00 2001 From: Corran Webster Date: Sat, 13 Jul 2019 12:02:46 -0500 Subject: [PATCH 18/22] First attempt at a date editor. --- traitsui/ipywidgets/date_editor.py | 94 ++++++++++++++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 traitsui/ipywidgets/date_editor.py diff --git a/traitsui/ipywidgets/date_editor.py b/traitsui/ipywidgets/date_editor.py new file mode 100644 index 000000000..3b269de57 --- /dev/null +++ b/traitsui/ipywidgets/date_editor.py @@ -0,0 +1,94 @@ +""" Defines the various text editors for the ipywidgets user interface toolkit. +""" + +import ipywidgets as widgets + +from traits.api import TraitError + +# FIXME: ToolkitEditorFactory is a proxy class defined here just for backward +# compatibility. The class has been moved to the +# traitsui.editors.text_editor file. +from traitsui.editors.text_editor import evaluate_trait, ToolkitEditorFactory + +from .editor import Editor + +# FIXME +# from editor_factory import ReadonlyEditor as BaseReadonlyEditor + +from .constants import OKColor + + +class SimpleEditor(Editor): + """ Simple style text editor, which displays a text field. + """ + + # Flag for window styles: + base_style = widgets.DatePicker + + def init(self, parent): + """ Finishes initializing the editor by creating the underlying toolkit + widget. + """ + factory = self.factory + wtype = self.base_style + + control = wtype(value=self.value, description='') + + control.observe(self.update_object, 'value') + + self.control = control + self.set_error_state(False) + self.set_tooltip() + + def update_object(self, event=None): + """ Handles the user entering input data in the edit control. + """ + if (not self._no_update) and (self.control is not None): + try: + self.value = self.control.value + + if self._error is not None: + self._error = None + self.ui.errors -= 1 + + self.set_error_state(False) + + except TraitError as excp: + pass + + def update_editor(self): + """ Updates the editor when the object trait changes externally to the + editor. + """ + self.control.value = self.value + + def error(self, excp): + """ Handles an error that occurs while setting the object's trait value. + """ + if self._error is None: + self._error = True + self.ui.errors += 1 + + self.set_error_state(True) + + def in_error_state(self): + """ Returns whether or not the editor is in an error state. + """ + return (self.invalid or self._error) + + +class CustomEditor(SimpleEditor): + """ Custom style of text editor, which displays a multi-line text field. + """ + + pass + + +class ReadonlyEditor(SimpleEditor): + """ Read-only style of text editor, which displays a read-only text field. + """ + + def init(self, parent): + super(ReadonlyEditor, self).init(parent) + + self.control.disabled = True From 8d212fcb73ecb7c73cdebf9b3fdc5dd1cb87102e Mon Sep 17 00:00:00 2001 From: Corran Webster Date: Sat, 13 Jul 2019 14:40:44 -0500 Subject: [PATCH 19/22] Add stubbed-out pyface action API to avoid import errors. --- traitsui/base_panel.py | 3 +- traitsui/ipywidgets/action/__init__.py | 0 .../ipywidgets/action/menu_bar_manager.py | 14 +++++++ traitsui/ipywidgets/action/menu_manager.py | 41 +++++++++++++++++++ .../ipywidgets/action/tool_bar_manager.py | 33 +++++++++++++++ traitsui/menu.py | 20 ++++----- 6 files changed, 100 insertions(+), 11 deletions(-) create mode 100644 traitsui/ipywidgets/action/__init__.py create mode 100644 traitsui/ipywidgets/action/menu_bar_manager.py create mode 100644 traitsui/ipywidgets/action/menu_manager.py create mode 100644 traitsui/ipywidgets/action/tool_bar_manager.py diff --git a/traitsui/base_panel.py b/traitsui/base_panel.py index 8f696a1f7..1a8f6eada 100644 --- a/traitsui/base_panel.py +++ b/traitsui/base_panel.py @@ -12,8 +12,10 @@ # Date: Aug 2017 from __future__ import absolute_import + from pyface.action.api import ActionController from traits.api import Any, Instance +from traitsui.menu import Action import six @@ -60,7 +62,6 @@ def is_button(self, action, name): def coerce_button(self, action): """ Coerces a string to an Action if necessary. """ - from traitsui.menu import Action if isinstance(action, six.string_types): return Action( diff --git a/traitsui/ipywidgets/action/__init__.py b/traitsui/ipywidgets/action/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/traitsui/ipywidgets/action/menu_bar_manager.py b/traitsui/ipywidgets/action/menu_bar_manager.py new file mode 100644 index 000000000..10c6a04c7 --- /dev/null +++ b/traitsui/ipywidgets/action/menu_bar_manager.py @@ -0,0 +1,14 @@ +from pyface.action.action_manager import ActionManager + + +class MenuBarManager(ActionManager): + """ A menu bar manager realizes itself in a menu bar control. """ + + # ------------------------------------------------------------------------ + # 'MenuBarManager' interface. + # ------------------------------------------------------------------------ + + def create_menu_bar(self, parent, controller=None): + """ Creates a menu bar representation of the manager. """ + # IPyWidgets doesn't currently support menus. + pass \ No newline at end of file diff --git a/traitsui/ipywidgets/action/menu_manager.py b/traitsui/ipywidgets/action/menu_manager.py new file mode 100644 index 000000000..09a38a4b0 --- /dev/null +++ b/traitsui/ipywidgets/action/menu_manager.py @@ -0,0 +1,41 @@ +from pyface.action.action import Action +from pyface.action.action_manager import ActionManager +from pyface.action.action_manager_item import ActionManagerItem +from traits.api import Unicode, Instance + + +class MenuManager(ActionManager, ActionManagerItem): + """ A menu manager realizes itself in a menu control. + This could be a sub-menu or a context (popup) menu. + """ + + # 'MenuManager' interface ----------------------------------------------- + + #: The menu manager's name + name = Unicode + + #: The default action for tool button when shown in a toolbar (Qt only) + action = Instance(Action) + + # ------------------------------------------------------------------------ + # 'MenuManager' interface. + # ------------------------------------------------------------------------ + + def create_menu(self, parent, controller=None): + """ Creates a menu representation of the manager. """ + # IPyWidgets doesn't currently support menus. + pass + + # ------------------------------------------------------------------------ + # 'ActionManagerItem' interface. + # ------------------------------------------------------------------------ + + def add_to_menu(self, parent, menu, controller): + """ Adds the item to a menu. """ + # IPyWidgets doesn't currently support menus. + pass + + def add_to_toolbar(self, parent, tool_bar, image_cache, controller, + show_labels=True): + """ Adds the item to a tool bar. """ + pass diff --git a/traitsui/ipywidgets/action/tool_bar_manager.py b/traitsui/ipywidgets/action/tool_bar_manager.py new file mode 100644 index 000000000..1d4a9b837 --- /dev/null +++ b/traitsui/ipywidgets/action/tool_bar_manager.py @@ -0,0 +1,33 @@ +from pyface.action.action_manager import ActionManager +from traits.api import Bool, Enum, Str, Tuple + + +class ToolBarManager(ActionManager): + """ A tool bar manager realizes itself as a tool bar widget. + """ + + # 'ToolBarManager' interface ----------------------------------------------- + + #: The size of tool images (width, height). + image_size = Tuple((16, 16)) + + #: The toolbar name (used to distinguish multiple toolbars). + name = Str('ToolBar') + + #: The orientation of the toolbar. + orientation = Enum('horizontal', 'vertical') + + #: Should we display the name of each tool bar tool under its image? + show_tool_names = Bool(True) + + #: Should we display the horizontal divider? + show_divider = Bool(True) + + # ------------------------------------------------------------------------ + # 'ToolBarManager' interface. + # ------------------------------------------------------------------------ + + def create_tool_bar(self, parent, controller=None): + """ Creates a tool bar. """ + # IPyWidgets doesn't currently support toolbars. + pass \ No newline at end of file diff --git a/traitsui/menu.py b/traitsui/menu.py index 9d2f47924..eef5a661b 100644 --- a/traitsui/menu.py +++ b/traitsui/menu.py @@ -124,16 +124,16 @@ class Action(PyFaceAction): ) #: The standard Traits UI menu bar -# StandardMenuBar = MenuBar( -# Menu(CloseAction, -# name='File'), -# Menu(UndoAction, -# RedoAction, -# RevertAction, -# name='Edit'), -# Menu(HelpAction, -# name='Help') -# ) +StandardMenuBar = MenuBar( + Menu(CloseAction, + name='File'), + Menu(UndoAction, + RedoAction, + RevertAction, + name='Edit'), + Menu(HelpAction, + name='Help') +) #------------------------------------------------------------------------- # Standard buttons (i.e. actions): From 398f41b3c61dfcc169f3653178b82d24d4b09809 Mon Sep 17 00:00:00 2001 From: Corran Webster Date: Sat, 13 Jul 2019 14:58:03 -0500 Subject: [PATCH 20/22] Clean up HasTraits patching. --- traitsui/api.py | 2 +- traitsui/ipywidgets/toolkit.py | 14 +++++++++----- traitsui/toolkit.py | 8 -------- 3 files changed, 10 insertions(+), 14 deletions(-) diff --git a/traitsui/api.py b/traitsui/api.py index 1566a72f5..1a5ebe8b8 100644 --- a/traitsui/api.py +++ b/traitsui/api.py @@ -118,7 +118,7 @@ RevertAction, RevertButton, Separator, - #StandardMenuBar, + StandardMenuBar, ToolBar, UndoAction, UndoButton) diff --git a/traitsui/ipywidgets/toolkit.py b/traitsui/ipywidgets/toolkit.py index c43d9b5d8..bac51a085 100644 --- a/traitsui/ipywidgets/toolkit.py +++ b/traitsui/ipywidgets/toolkit.py @@ -1,6 +1,7 @@ from traitsui.toolkit import assert_toolkit_import assert_toolkit_import(['ipywidgets']) +from traits.has_traits import HasTraits from traits.trait_notifiers import set_ui_handler from pyface.base_toolkit import Toolkit as PyfaceToolkit from traitsui.toolkit import Toolkit @@ -9,11 +10,14 @@ import ipywidgets -toolkit = PyfaceToolkit( - 'pyface', - 'ipywidgets', - 'traitsui.ipywidgets' -) +toolkit = PyfaceToolkit('pyface', 'ipywidgets', 'traitsui.ipywidgets') + +# FIXME: this definitely belongs elsewhere. +# An injected method to render the object in a Jupyter notebook. +def _repr_html_(self): + self.configure_traits(kind='live') +HasTraits._repr_html_ = _repr_html_ + def ui_handler(handler, *args, **kwds): """ Handles UI notification handler requests that occur on a thread other diff --git a/traitsui/toolkit.py b/traitsui/toolkit.py index 1d0ff9145..e91865990 100644 --- a/traitsui/toolkit.py +++ b/traitsui/toolkit.py @@ -24,7 +24,6 @@ import logging from traits.trait_base import ETSConfig -from traits.has_traits import HasTraits from pyface.base_toolkit import Toolkit, find_toolkit @@ -36,13 +35,6 @@ _toolkit = None -# FIXME: this definitely belongs elsewhere. -# An injected method to render the object in a Jupyter notebook. -def _repr_html_(self): - self.configure_traits(kind='live') -HasTraits._repr_html_ = _repr_html_ - - def assert_toolkit_import(names): """ Raise an error if a toolkit with the given name should not be allowed to be imported. From 1f90ac925a149cc96f9782773e2bb285e2a1d563 Mon Sep 17 00:00:00 2001 From: Corran Webster Date: Sat, 13 Jul 2019 15:09:11 -0500 Subject: [PATCH 21/22] Better way of adding Jupyter HTML rendering of traits. --- traitsui/ipywidgets/toolkit.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/traitsui/ipywidgets/toolkit.py b/traitsui/ipywidgets/toolkit.py index bac51a085..4b773c0e1 100644 --- a/traitsui/ipywidgets/toolkit.py +++ b/traitsui/ipywidgets/toolkit.py @@ -12,11 +12,12 @@ toolkit = PyfaceToolkit('pyface', 'ipywidgets', 'traitsui.ipywidgets') -# FIXME: this definitely belongs elsewhere. -# An injected method to render the object in a Jupyter notebook. -def _repr_html_(self): +def has_traits_html(self): + """ Jupyter HasTraits HTML formatter. """ self.configure_traits(kind='live') -HasTraits._repr_html_ = _repr_html_ + +html_formatter = get_ipython().display_formatter.formatters['text/html'] +html_formatter.for_type(HasTraits, has_traits_html) def ui_handler(handler, *args, **kwds): From a166c26f5744b9400fec698d04431323ae078d49 Mon Sep 17 00:00:00 2001 From: Poruri Sai Rahul Date: Tue, 27 Apr 2021 15:08:35 +0530 Subject: [PATCH 22/22] FIX : Ignore flake8 errors in traitsui.ipywidgets for now modified: setup.cfg --- setup.cfg | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.cfg b/setup.cfg index f27ca6796..882426da9 100644 --- a/setup.cfg +++ b/setup.cfg @@ -27,6 +27,7 @@ exclude = traitsui/list_str_adapter.py, traitsui/menu.py, traitsui/null, + traitsui/ipywidgets, traitsui/qt4/__init__.py, traitsui/qt4/array_editor.py, traitsui/qt4/basic_editor_factory.py,