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, diff --git a/setup.py b/setup.py index 2ab7b58f8..38c06f8cf 100644 --- a/setup.py +++ b/setup.py @@ -356,6 +356,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', ], 'etsdemo_data': [ 'demo = traitsui.extras._demo_info:info', diff --git a/traitsui/ipywidgets/TestIPyWidgets.ipynb b/traitsui/ipywidgets/TestIPyWidgets.ipynb new file mode 100644 index 000000000..1b627660a --- /dev/null +++ b/traitsui/ipywidgets/TestIPyWidgets.ipynb @@ -0,0 +1,236 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from traits.etsconfig.api import ETSConfig\n", + "\n", + "ETSConfig.toolkit = 'ipywidgets'" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "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" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "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": null, + "metadata": {}, + "outputs": [], + "source": [ + "t1 = TestObjInline()\n", + "t1" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "class TestObj(HasTraits):\n", + " text = Unicode\n", + " boolean = Bool\n", + " integer = Int\n", + "\n", + "test_obj = TestObj()\n", + " \n", + "test_view = View(\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", + ")\n", + "ui = test_obj.edit_traits(view=test_view, kind='live')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "print(ui.control)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "t = ui.control\n", + "t" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "test_obj.configure_traits(view=test_view, kind='live')\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "test_obj.text" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "test_obj.text = \"Other way\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "test_obj.boolean" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "test_obj.integer" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "test_obj.integer = 100" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "test_obj.boolean = True" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "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", + "gb" + ] + }, + { + "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.6.0" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/traitsui/ipywidgets/__init__.py b/traitsui/ipywidgets/__init__.py new file mode 100644 index 000000000..7137da338 --- /dev/null +++ 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/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/ipywidgets/boolean_editor.py b/traitsui/ipywidgets/boolean_editor.py new file mode 100644 index 000000000..bf6b00b24 --- /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.observe(self.update_object, 'value') + self.set_tooltip() + + def update_object(self, event=None): + """ Handles the user clicking the checkbox. + """ + self.value = bool(self.control.value) + + 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/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() 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/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 diff --git a/traitsui/ipywidgets/editor.py b/traitsui/ipywidgets/editor.py new file mode 100644 index 000000000..2012c8a1f --- /dev/null +++ b/traitsui/ipywidgets/editor.py @@ -0,0 +1,259 @@ +""" 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).tooltip + if desc is None: + 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/html_editor.py b/traitsui/ipywidgets/html_editor.py new file mode 100644 index 000000000..e8d93c665 --- /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 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-------------------------------------------------------------------------- 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/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 +} diff --git a/traitsui/ipywidgets/resource_manager.py b/traitsui/ipywidgets/resource_manager.py new file mode 100644 index 000000000..5be5af89c --- /dev/null +++ b/traitsui/ipywidgets/resource_manager.py @@ -0,0 +1,28 @@ +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. """ + + return data \ No newline at end of file diff --git a/traitsui/ipywidgets/text_editor.py b/traitsui/ipywidgets/text_editor.py new file mode 100644 index 000000000..07152082d --- /dev/null +++ b/traitsui/ipywidgets/text_editor.py @@ -0,0 +1,156 @@ +""" 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 + + 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._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 diff --git a/traitsui/ipywidgets/toolkit.py b/traitsui/ipywidgets/toolkit.py new file mode 100644 index 000000000..4b773c0e1 --- /dev/null +++ b/traitsui/ipywidgets/toolkit.py @@ -0,0 +1,124 @@ +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 + +from IPython.display import display +import ipywidgets + + +toolkit = PyfaceToolkit('pyface', 'ipywidgets', 'traitsui.ipywidgets') + +def has_traits_html(self): + """ Jupyter HasTraits HTML formatter. """ + self.configure_traits(kind='live') + +html_formatter = get_ipython().display_formatter.formatters['text/html'] +html_formatter.for_type(HasTraits, has_traits_html) + + +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) + + +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 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. + """ + 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 new file mode 100644 index 000000000..562c65e24 --- /dev/null +++ b/traitsui/ipywidgets/ui_panel.py @@ -0,0 +1,465 @@ +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 + + +#: 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+') + + +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._groups + 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 enumerate(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 += (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 + + outer = sub = inner = None + + # 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=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: + inner = ipywidgets.GridBox() + layout = ipywidgets.Layout( + grid_template_columns=' '.join(['auto']*(2*columns)) + ) + inner.layout = layout + if outer is None: + outer = inner + else: + outer.children += (inner,) + + row = 0 + + if show_left: + label_alignment = 'right' + else: + label_alignment = 'left' + + else: + if self.group.orientation == 'horizontal': + outer = ipywidgets.HBox() + else: + outer = ipywidgets.VBox() + inner = outer + row = -1 + label_alignment = 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) + if show_labels: + inner.children += (ipywidgets.Label(value=''),) + 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? + obj = eval(item.object_, globals(), ui.context) + trait = obj.base_trait(name) + desc = trait.tooltip + if desc is None: + desc = 'Specifies ' + trait.desc if trait.desc 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, + label_alignment) + 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'): + 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 + + # Determine whether to place widget on left or right of + # "logical" column. + 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,) + + 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 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 + ---------- + 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) + + # if desc != '': + # # ipywidgets.Label's do not support tooltips. + # label_control.tooltip = desc + + 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 +# +# 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) +# diff --git a/traitsui/item.py b/traitsui/item.py index fde56606d..032b7144f 100644 --- a/traitsui/item.py +++ b/traitsui/item.py @@ -312,6 +312,24 @@ def is_spacer(self): 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: + #------------------------------------------------------------------------- + def get_help(self, ui): """ Gets the help text associated with the Item in a specified UI. """ diff --git a/traitsui/qt4/color_trait.py b/traitsui/qt4/color_trait.py index 06c9d5ebc..409c732cb 100644 --- a/traitsui/qt4/color_trait.py +++ b/traitsui/qt4/color_trait.py @@ -31,7 +31,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 683b0fbe4..5d71de09c 100644 --- a/traitsui/qt4/ui_panel.py +++ b/traitsui/qt4/ui_panel.py @@ -765,9 +765,7 @@ def _add_items(self, content, outer=None): self._label_visible_whens = [] # 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: