././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1684768028.1278067 traitsui-8.0.0/0000755000175100001730000000000000000000000014165 5ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/CHANGES.txt0000644000175100001730000012573400000000000016012 0ustar00runnerdocker00000000000000Traits UI Changelog =================== Release 8.0.0 ------------- This is a major release that provides support for PySide 6.4 and 6.5 as well as Python 3.11. It includes a number of backwards-incompatible changes following the example of Pyface 8.0.0, most notable is moving the `traitsui.qt4.*` modules to `traitsui.qt.*`. As with Pyface 8.0 we include a backwards-compatibility mode which allows importing from the 'qt4' namespace via: - setting the ETS toolkit to `"qt4"` - setting the ETS_QT4_IMPORTS environment variable - manually adding appropriate finders to sys.meta_path This release also removes a number of features deprecated since TraitsUI 7.0. Thanks To --------- * David Baddeley * Mark Dickinson * Dominik Gresch * hopeful0 * Chengyu Liu * Orion Poplawski * Corran Webster Features -------- * Be strict about 'handler.init()' return values (#2008) * Move 'traitsui.qt4.*' to 'traitsui.qt.*' (#2004) * Remove deprecated 'format' trait (#2002) * Deprecate imports from traitsui.editors (use traitsui.editors.api) (#2000) * Remove backwards-compatibility mode in undo/redo code (#1999) * Support for Python 3.11 and PySide 6.4+ (#1994) Fixes ----- * Fix uses of None in date range editor (#2019) * Add comments for Wx-only examples (#2011, #2020) * Replace uses of "fast_ui" dispatch with "ui" dispatch (#2009) * Fix FileDialog selection actions (#2003) * Fix regression in Wx version of FileEditor (#1993) * Fix missing attribute of InstanceFactoryChocie (#1989) Test suite ---------- * Replace some relative imports in tests (#1985) Release 7.4.3 ------------- This is a small bugfix release which resolves some bugs that have come to light as we get more experience with PySide 6, together with changes to run CI on more recent Python versions and current Github infrastructure. Thanks To --------- * Mark Dickinson * Matt Reay * Corran Webster * John Wiggins Fixes ----- * Fix rendering of dragged tree nodes on Qt6 (#1960) * Add explicit allow_none in Datetime traits (#1964) * Generate extra TableEditor menus dynamically (#1966) * Display the selected date in the DateEditor in custom style (#1967) * Guard Qt TreeEditor against destroyed QTreeViewItems (#1973) Test suite ---------- * Update CI for EDM Python 3.8 (#1972) Release 7.4.2 ------------- This is a small bug fix release which resolves a couple of bugs in the RangeEditor and TreeEditor. It also pins the PySide6 version to < 6.4.0 due to incompatibilities with the new enum system, which should improve the install experience. Thanks To --------- * Aaron Ayres * hopeful0 * Corran Webster Fixes ----- * Fix RangeTextEditor handling of None for low/high (#1950) * Pin PySide6 to < 6.4 (#1951) * Fix issues with shared "New" context menu in TreeEditor (#1953) Release 7.4.1 ------------- This is a bug fix release which resolves an issue with RangeEditor not handling None for either the high or low value, as well as an issue with ProgressColumn rendering. Thanks To --------- * Steve Allen * Chris Angell * Aaron Ayres * Maxwell Grady * Johannes Loibl * Rahul Poruri * Corran Webster * Hai Yan Fixes ----- * Fix issue with progress column rendering on Linux and Windows. (#1937) * Fix typos in docstrings (#1935) * Fix range text editor bug (#1931) * Fix issue where file editor filters were not used in simple editor (#1930) Release 7.4.0 ------------- This is a minor release which fixes a number of bugs and adds a couple of small features. The most significant changes are the ability to add separators to EnumEditor comboboxes, the ability to use Pyface action Schemas for menu bars and toolbars in Views, and getting the VideoEditor working against the new QtMultimedia APIs in Qt6. Thanks To ~~~~~~~~~ * Aaron Ayres * Mark Dickinson * Rahul Poruri * PyHannes * Prabhu Ramachandran * Ioannis Tziakos * Corran Webster * John Wiggins * Hai Yan Features ~~~~~~~~ * Separators in Combobox EnumEditors. (#1885) * Allow the use of Pyface Schemas for View menus and toolbars. (#1827) Fixes ~~~~~ * Delay imports which force toolkit selection (#1883) * Fix ImageEditor paintEvent when image is None. (#1907) * Fix VideoEditor for Qt6 (#1908) * Fix the ImageEnumEditor on Qt5+ (#1910) * Fix some height and width calls for Qt and Python 3.10+ (#1911) * Fix a crash on PyQt5 when a Group has no content (#1914) * Numerous fixes for tests and CI (#1897, #1893, #1889, #1894, #1898, #1899, #1990, #1902, #1903) Documentation changes ~~~~~~~~~~~~~~~~~~~~~ * Add a copy button to code blocks in documentation (#1904) Release 7.3.1 ------------- TraitsUI 7.3.1 is a bugfix release that resolves a couple of critical errors in some Editors. Thanks To ~~~~~~~~~ * Mark Dickinson * Steven Kern * Orion Poplawski * Rahul Poruri * Corran Webster Fixes ~~~~~ * Fix an issue with KeyBindingsEditor double-click and dark mode colors (#1864) * Fix an issue with integer division in ImageEnumEditor (#1862) * Fix an attribute name and type error in the FileEditor (#1860) Release 7.3.0 ------------- TraitsUI 7.3.0 is a minor release which includes numerous bug fixes, documentation improvements, code maintenance changes, and enhancements. Highlights of this release ~~~~~~~~~~~~~~~~~~~~~~~~~~ * The most important new features are experimental support for Qt6, both for PySide6 and PyQt6 (although the latter is less complete). * Qt4 is deprecated and is no longer being tested in CI. Support will be removed in the next major release. * The new Pyface Font and Color classes can now be used with the TraitsUI Color and Font traits. * The image editor can now use any Pyface IImage instance, allowing simple dynamic image editing. * Numerous bugfixes and small improvements to existing editors. Thanks To ~~~~~~~~~ * Chris Angell * Aaron Ayres * Per A. Brodtkorb * Mark Dickinson * Petr Kungurtsev * Eric Larson * Nicola De Mitri * Rahul Poruri * PyHannes * Diego Ramirez * Pedro Rivotti * Corran Webster Features ~~~~~~~~ * Expose TreeEditor actions and IconSize in traitsui.editors.api (#1690) * Add UITester support for qt TableEditor (#1707) * Add UITester DirectoryEditor support (#1710) * Add an expand_all method to TreeEditor (#1726) * Used Black to ensure a uniform codestyle for TraitsUI (#1760) * Allow per-row tooltips in the ListStrEditor's adapter (#1766) * PySide6 support (#1803) * Add support for Pyface Color and standaize color names (#1812) * Pyface Fonts can be used with TraitsUI Font traits (#1819) * Add ListStrAdapter to traitsui.api (#1823) * Remove uses of the archaic `property_depends_on` decorator (#1832) * Require Pyface 7.4.1 (#1840) Fixes ~~~~~ * Call HasPrivateTraits.__init__() in GroupEditor.__init__ (#1674) * Add RangeEditor support for format_func and deprecate ``format`` trait on RangeEditor factory / toolkit specific Editor implementations (#1684) * Fix Dynamic EnumEditor on qt (#1719) * Fix ProgressColumn bars overlapping with PyQt5 and PySide2 (#1721) * Fix selectable InstanceEditor combobox updates (#1725) * Fix Qt InstanceEditor appearance when None selected (#1728) * Prevent RangeTextEditor from allowing values outside range (#1731) * Fix droppable InstanceEditor (#1733) * Fix double error dialogs (#1734) * Convert ``traitsui.instance_choice.InstanceChoiceItem`` into an instance of ``traits.api.ABCHasStrictTraits`` to avoid users from instantiating the object directly (#1738) * Fix issue with incorrect items added via context menu in a TreeEditor (#1745) * Fix name errors and refactor flake8 config to reduce the chance of future errors. (#1754) * Fix NotebookEditor initial `selected` (#1791) * Convert Qt Enums to be compatible with PyQt6 (#1798) * Fix SimpleSpinEditor "enter set" on qt (#1804) * Fix issue with ImageEditor not updating for all IImage implementations. (#1810) * Fix issues with KeyBindings. (#1817) * Fix the KeyBindingEditor. (#1820) * Improve error colors for dark mode on Qt. (#1821) Documentation changes ~~~~~~~~~~~~~~~~~~~~~ * Move "array_editor" to "StandardEditors" contributed examples (#1691) Test suite ~~~~~~~~~~ * Stop testing against pyqt4 on CI (#1686) * Remove tests and examples that depend on Chaco and Enable. (#1833) Release 7.2.1 ------------- TraitsUI 7.2.1 is a bugfix release which updates TraitsUI to explicitly require Traits 6.2+ and Pyface 7.3+. Build and continuous integration ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ * Explicitly require traits 6.2 and pyface 7.3 (#1666, #1668) Release 7.2.0 ------------- TraitsUI 7.2.0 is a minor release which includes numerous bug fixes, documentation improvements, code maintenance changes, and enhancements. Highlights of this release ~~~~~~~~~~~~~~~~~~~~~~~~~~ * The migration from ``on_trait_change`` to ``observe`` is underway. As a result, TraitsUI now requires Traits >= 6.2. * New display-only VideoEditor (currently only on Qt backend). * Exapnsion of features for the new ``UITester`` including the ability to inspect UI object visibility / enabledness. Also documentation for testing has been updated. Notes on upgrading ~~~~~~~~~~~~~~~~~~ * This release of TraitsUI now depends on Traits 6.2+ and pyface 7.2+. Also, deprecated code / modules have been removed. Namely, the :mod:`traitsui.image ` module which was moved to ``pyface.image``, ``editors_gen`` modules, Editor and EditorFactory factory methods on Toolkit objects, and more. For a complete list, see PRs in the "Removals" section below. These were all generally unused / deprecated for sometime. Also, importing directly from ``traitsui.editors`` has been deprecated. Please update imports to import directly from :mod:`traitsui.api` or :mod:`traitsui.editors.api`. Detailed changes ~~~~~~~~~~~~~~~~ Thanks to: * Aaron Ayres * Kit Yan Choi * Mark Dickinson * Rahul Poruri * Corran Webster Enhancements ~~~~~~~~~~~~ * Open links externally instead of in the html editor in etsdemo application (#1446) * Hide demo tab when the demo is None (#1456) * Extract an interface from TargetRegistry to allow wider testing support for IsEnabled (#1490) * Expose clearButtonEnabled for Qt TextEditor (#1516) * Extend / document secret TreeNode api for passing tuples into add list trait (#1527) * Add 'IsVisible` query class for UI Tester (#1552) * Extend button editor to allow dynamically changing button image (#1566) * Support testing with simple FileEditor (without dialog) (#1571) * Qt VideoEditor (#745, #1609, #1621) * Add item_factory callable for specifying creation of new list items (#1634) Fixes ~~~~~ * Support HTMLEditor.open_externally on QtWebEngine (#1451) * Qt InstanceEditor button should never be the default (#1498) * Propagate UI errors to the UI's parent (#1503) * Fix resizable readonly enum editor (#1532) * StartStyling API changed for new Wx version (#1536) * Don't use html for a variable name when using html module from standard library (#1540) * Label {visible/enabled}_when (#1544) * manually set the text format to plain text in error (#1546) * Fix RGBColor hex int to tuple color conversion (#1554) * Make Scrollable group respect visible_when (#1555) * Use correct source_path in example (#1433) * Move EnumEditor import in table_filter into the methods that need it (#1616) Documentation ~~~~~~~~~~~~~ * Add a module docstring to the api modules (#1441) * Use viewcode sphinx extension (#1443) * Add links to API docs in a etsdemo examples for traitsui standard editors (#1445) * Clarify which of traitsui.api and traitsui.editors.api is recommended (#1471) * traitsui.testing documentation updates (#1482, #1483, #1485, #1486, #1487, #1488) * Fix link label to the documentation home page (#1489) * Document enabled_when / visible_when better (#1537) * Document entries parameter not supported qt for FileEditor/DirectoryEditor (#1557 * Add script to regenerate screenshots of editors for documentation (#1574) * Remove unused images in docs (#1584) * replace # with #: to document traits in traitsui.editors.* (#1596) * User facing docs for VideoEditor (#1630) * Format code examples in the user documentation (#1640) Build and continuous integration ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ * Fix cron job not installing additional dependencies (#1427) * Remove job on Travis for testing against Traits 6.0 (#1430) * Drop Python 3.5 support in TraitsUI (#1436) * Declare Python 3.5 not supported for ets-demo (#1437) * Fix link for installing wxPython on CI (for etsdemo) (#1491) * Add GitHub Actions to test against EDM (#1492) * Add GitHub Actions workflow for testing ets-demo (#1496) * Use EDM 3.2.3 instead of EDM 3.2.1 (#1548) * Drop support for PyQt < 4.3.2 (#1607) * Remove CI test against Traits 6.0 (#1637) * explicitly install swig 3.0.12 for cron job (#1652) Test suite ~~~~~~~~~~ * Refactor and extend tests for Qt HTMLEditor handling of opening links (#1465) * Update new test_editor_error_msg (#1553) * Unskip "EnumEditor" tests that were failing earlier on windows (#1615) * skip video editor test without numpy / move numpy import to when it is needed / list numpy as test dependency (#1639) * skip test if no QtWebkit or QtWebEngine (#1649) * skip a couple wx test failures (#1656) Maintenance and code organization ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ * Add pygments to etsdemo's etstool.py (#1453) * Add help aliases to etsdemo etstool module (#1457) * Fix/Update copyright headers (#1467, #1486) * work towards flake8 clean codebase (#1469, #1562, #1563) * Remove meaningless comments from an outdated coding style. (#1472) * Remove clause that deviates from PEP8 backward compatibility convention in the testing package (#1481) * Formal editor interface for tooltips (#1493 * Add instance choice classes to traitsui.api (#1495) * Undo/Redo cleanup (#1510) * start on_trait_change to observe migration (#1519, #1520, #1523, #1525, #1545, #1622, #1644) * Refactor TreeEditor _new_actions and _menu_new_node to avoid hacky eval (#1524) * Refactor _add_items method of _GroupPanel object (#1549) * Add new trait to eventually replace scroll_to_row_hint (#1560) * Add 'default': True to the etsdemo eam metadata (#1568) * update "super" usage (#1583, #1587, #1588, #1589, #1604) * Use "str.capitalize" directly instead of an alias (#1598) * Use "PrefixList" from traits >= 6.1 (#1599) * Update use of deprecated "Thread.setDaemon" (#1601) * Replace deprecated "wx.ListItemAttr" with "wx.ItemAttr" (#1602) * Adds "traitsui.toolkit.toolkit_object" to "traitsui.api" (#1603) * Import "TraitFactory" from "traits.api" (#1606) * Rename editor factory classes (#1610) * Cleanup imports in "traitsui.editors.*" (#1619) Removals ~~~~~~~~ * Remove deprecated classes/modules (#1594) * remove traitsui.image submodule (#1595) * Remove Editor and EditorFactory factory methods on Toolkit objects (#1600) * Remove backwards compatibility toolkit factory imports (#1608) * Remove unused "Item.full_size" trait (#1613) * Remove code handling old/outdated versions of wx (#1614) * Remove unused editors_gen modules (#1626) Release 7.1.1 ------------- This is a bugfix release that fixes a number of issues since the 7.1.0 release. Thanks to Corran Webster and Kit Choi for the patches. Fixes ~~~~~ - Fix ``scrollable`` trait of a Group not being implemented on Qt (#1406) - Fix icon button's clickable area too small for FileEditor and RangeEditor on Qt and OSX (#1383) - Fix missing minimize and maximize buttons for dialogs opened on certain Linux platforms (#1409) Release 7.1.0 ------------- TraitsUI 7.1.0 is a minor feature release which introduces a new testing library and a number of significant fixes. Demo examples are also distributed as package data such that they are visible via the "etsdemo" GUI application (to be installed separately). This release should be compatible with Traits 6.0+. Users are encouraged to upgrade to Traits 6.1+ to stay current as future releases of TraitsUI will stop supporting Traits 6.0. Highlights of this release ~~~~~~~~~~~~~~~~~~~~~~~~~~ * A new :mod:`traitsui.testing.api` module has been introduced for testing GUI applications built using TraitsUI. See :ref:`testing-traitsui-applications` for an introduction. Builtin support has been added for testing several TraitsUI editors. More support will be added in the future. * On OSX and Qt, there have been reports of missing UI view updates after a push button is clicked. While this is suspected to be a Qt issue, changes have been made to ButtonEditor, SetEditor and ImageEnumEditor to mitigate the situation. * The internal logic for disposing an instance of :class:`~traitsui.ui.UI` is changed as an attempt to resolve AttributeError that occurs while a nested UI is removed. Notes on upgrading ~~~~~~~~~~~~~~~~~~ * On the issue about Qt button not causing views to update on OSX, projects that have been working around the issue by adding ``GUI().process_events()`` (or similar) in their applications may now try to remove those workarounds. However, the change that mitigates the issue in a production environment has implications for tests: The button's click slot is no longer invoked immediately but always invoked by the Qt GUI event loop. Tests that used to call the Qt button ``click`` method directly and rely on the event to happen immediately will now need to update their tests to ensure that the click slot is processed by the Qt GUI event loop in the same order as before. Consider using the new testing library which automatically runs the GUI event loop after each interaction (e.g. mouse click). Future removals ~~~~~~~~~~~~~~~ * :mod:`traitsui.image ` has been moved to ``pyface.image`` more than 3 years ago and has since been deprecated. Previously it was scheduled to be removed in TraitsUI 6.0. This planned removal is now deferred to TraitsUI 7.2. Detailed changes ~~~~~~~~~~~~~~~~ More than 100 PRs went into this release. Thanks to: * Aaron Ayres * Ieva Cerny * Kit Yan Choi * Mark Dickinson * James Johnson * Eric Larson * Rahul Poruri * Jonathan Rocher * Kuya Takami * Ioannis Tziakos * Corran Webster Note that the following list is not exhaustive. Many more PRs references have been omitted. Features ~~~~~~~~ * Add :class:`~traitsui.testing.ui_tester.UITester` for testing TraitsUI applications (#1107, #1157, #1171, #1175, #1179, #1201, #1207, #1269) Fixes ~~~~~ * Fix AttributeError when a nested UI is disposed (#1286) * Fix wx error due to use of alignment flag wxEXPAND (#1095) * Fix ButtonEditor not causing other widgets to update on OSX and Qt (#1303) * Fix ImageEnumEditor button not causing other updates on OSX (#1326) * Fix SetEditor on Qt and OSX not updating view after button clicks (#1325) * Fix for menu actions with on_perform being performed twice (#1199) * Fix deprecation warnings from Traits 6.1 due to the use of PrefixList (#1053) * Fix deprecation warnings from using HasTraits.trait_get (#1062) * Fix deprecation warnings from using QDesktopWidget.availableGeometry (#1311) * Fix deprecation warnings from using logging.warn (#1165) * Fix deprecation warnings from using ABC in collections (#1103, #1129) Documentation ~~~~~~~~~~~~~ * Make demo examples as package data (#1088) * Add UITester documentation in User Manual (#1263) * Add Developer Guide with internals on the testing package (#1314) * Fix warnings in demo examples documentation (#1378) * Generate API documentation automatically (#1368) Build and continuous integration ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ * Add a CI job for testing against traits 6.0 (#1108) * Move MacOS CI build from Travis to Appveyor (#1160) * Add flake8 task to CI with exclusions (#1222) * Explicitly declare additional dependencies for demo examples (#1147) * Explicitly require a minimum version for Traits (#1323) Maintenance and code organization ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ * Relax constraint on PySide2 version (#1146) * Update edm version in travis and appveyor config files (#1049) * Resolve warnings in tests from QItemSelectionModel with Qt5 (#1041) * Improve how we capture and re-raise errors in GUI tests (#1099) * Replace screen metrics code with Pyface implementation (#1322) * Fix deprecation warnings from certain usage of QDateTime in tests (#1310) * Add a new shell command to etstool.py (#1244) Release 7.0.1 ------------- This is a bugfix release that fixes a number of issues since the 7.0.0 release. It includes fixes to various editors, improvements to tests and fixes to the demo application. Thanks to Ieva Cerny, Kit Choi, Mark Dickinson, Robert Kern, Eric Larson, Federico Miorelli, Joris Vankerschaver, Corran Webster. Fixes ~~~~~ * Fix error from true division of integers for Qt RangeEditor and BoundsEditor (#999, #1000) * Fix handling of minimum and maximum datetimes in Qt DatetimeEditor (#803) * Fix error in wx TabularEditor (#969) * Fix wx panel error due to alignments (#829) * Fix various issues in the demo application (#799, #808) * Fix format_func not used when EnumEditor is initialized with static values (#848) * Fix demo application description (#850) * Fix auto-add functionality in ListStrModel (#860) * Fix segmentation fault for TabularModel (#871, #873) * Fix event handling in ListStrEditor and TabularEditor for adding and removing items (#875) * Fix TabularAdapter crashes when column number reduces (#897) * Fix theme pickling issue (#915) * Fix error from example's tutor.py script (#813) Documentation ~~~~~~~~~~~~~ * Add links from TabularEditor and TreeEditor to adapter documentation (#917) Tests ~~~~~ * Rewrite nose tests to unittest (#809) * Use unittest discover instead of nose.core (#810) * Add more tests for EnumEditor (#836) * Add CheckListEditor tests (#837) * Add SetEditor tests (#838) * Add ImageEnumEditor tests (#845) * Add more ListStrEditor tests (#869) * Add more TabularEditor tests (#874) * Make sure all UI in tests are disposed (#865) * Add tests for layout and labels using any non-null toolkits (#846) Release 7.0.0 ------------- TraitsUI 7.0.0 is a major release which focuses on modernization of the codebase. This release includes updating the WxPython backend to support WxPython 4, an effort led by Rob McMullen's first cut at support, with help from a number of other people, particularly Per Brodtkorb. It also updates the code to use new features of Traits 6, and just like Traits 6, this release drops support for Python 2. In addition to the modernization, this code fixes many bugs and adds some useful new features, including: - a DatetimeEditor to go with the new Datetime trait in Traits 6 - an API to add placeholder text to a TextEditor - support for the LEDEditor in Qt Finally, the documentation has been improved, adding sections about some of the internals of the TraitsUI mechanisms. Additionally, the Examples, Demos and Tutorials have been improved and cleaned-up, including fixing and unifying the demo viewer codebase. Thanks to Per Brodtkorb, Christian Brodbeck, Kit Choi, Mark Dickinson, Matt Hancock, Sandhya Govindaraju, Midhun Madhusoodanan, Rob McMullen, Shoeb Mohammed, Eric Olsen, Roberto Preste, reckoner, Jonathan Rocher, Scott Talbert, Corran Webster. Enhancements ~~~~~~~~~~~~ * Documentation improvements (#720, #724, #723, #778) * WxPython 4 compatibility and support (#708, #736, #762, #772, #775, #776) * Traits 6 compatibility and enhancements (#689, #690, #693, #695, #713, #727, #770) * Style improvements (#648, #771) * Add placeholder text option for TextEditor (#735) * Examples clean-up and unification (#697, #702, #703, #705, #711, #712, #777) * Add a DatetimeEditor (#719, #730) * Refactor and clean-up of base Editor class (#685, #686, #688) * Remove use of Category classes from TraitsUI (#654) * Add LEDEditor for Qt (#635) * Add 'docs' command to etstool.py (#624) * Add TabularAdapter to traitsui.api (#627) * Enhancements to CI and testing (#683, #714, #740) Fixes ~~~~~ * Documentation fixes (#546, #766) * Fix cgi.escape for Python 3.8 (#780) * Log exceptions in table sorting (#751) * Use TupleEditor for traits derived from BaseTuple (#747) * Fixes for Undo/Redo (#733) * Add explicit redraw for TableEditor selection (#721) * Fixes for Qt5 support (#709) * Fix bugs in color functions (#707, #744) * Fix importing of ProgressColumn and EditColumn (#691) * Set initial focus item correctly in Qt (#602) * Fix text elision test on some platforms (#644) * Fix a bug with TableEditor column clicking (#669) * Fix typo in progress column exception (#662) Release 6.1.3 ------------- A bugfix release that fixes a number of issues discovered since the 6.1.2 release. In particular this fixes a number of issues around the TableEditor and TabularEditor for the Qt toolkit which didn't match the advertised behaviour in the documentation. Thanks to Qi Chen, Vladimir Chukharev, Mark Dickinson, Sandhya Govindaraju, Maxwell Grady, Scott Maddox, Sean Parsons, Rahul Poruri, Corran Webster. Fixes ~~~~~ * Add "bool" to allowed types for TableColumn (#656) * Fix tabular editor column widths (#652) * Fix setting valus in DataFrameEditor (#651) * Allow '...' in addition to ellipsis in text elision (#644) * Handle invalid values in RangeEditor better (#637) * Fix multi-select in Qt TabularEditor (#633) * Fix call to Bind in Wx FileEditor (#628) * Fixes to doc links in tutorial (#619) * Remove 3.5 from test matrix (#615) * Ensure etstool.py has consistent default Python versions (#614) * Fix typos in comments for table editor (#610) * Comprehensive tests for the base Editor object (#609) * Fix ContextValue __init__ method (#607) * Fix tutor.py in examples for Python 3 (#603) * Set initial item focus correctly on Qt (#602) * Make View and UI icons an Image trait (#600) * Fix TableEditor styling for Qt (#597) Release 6.1.2 ------------- A bugfix release that fixes a number of issues discovered since the 6.1.1 release. In particular this fixes a couple of issues that impacted the usability of Mayavi. Thanks to Mark Dickinson, Maxwell Grady, Prabhu Ramachandran, Matt Reay, Ajeet Vivekanandan, Corran Webster, John Wiggins. Fixes ~~~~~ * Fix tree node copy failure to copy (#590) * Fix/scroll to row preserve column (#588) * Call correct object on label change for TreeNodeObject (#586) * Remove uses of etsdevtools (#577) * Fix modal view application (#574) * Remove unicode usage (#572) * Update Travis CI configuration to be compatible with Ubuntu Xenial. (#569) * Build CI on maintenance branches (#567) * Fix an erroneous handler removal (#566) * Fix the sizeHint() default for TreeItemDelegate (#565). * Update unittest import (#564) Release 6.1.1 ------------- A bugfix release to correct some critical issues with the TreeEditor. Fixes ~~~~~ * Fix TreeNodeObject listener cache (#558). * Fix tree node insertion (#561). Release 6.1.0 ------------- This is an incremental release without many new features, but with significant improvements in the code base and numerous small bugfixes. Probably the most significant change, but one not obvious to most users is that we have moved away from using 2to3 for Python 3 support to using a single codebase with the six library to patch over the differences. This significantly enhances maintainability and development speed, and gives a better path forward for future Python versions. Additionally, this release introduces experimental support for PySide2 (also known as "Qt for Python"). Pyside2 was enabled as a potential backend in 6.0.0 but there was no testing being performed. As of this release PySide2 is now tested as part of the CI system with tests passing on Linux and OS X using the 5.11 release. There are currently issues with the 5.12 release which have yet to be fully investigated. The one significant new feature that has been added to this release is the ability to completely replace the rendering of TreeEditorNodes with custom rendering code on Qt (due to the limitations of the Wx tree control, this is unlikely to be extended to Wx). An example showing the drawing of sparkline plots in TreeEditor cells has been added to the code demos. There has been a long-standing issue with the way that the Qt TableEditor handled selections, and this release fixes this, matching the behaviour of the Wx backend and the documentation. It is possible that this may break code that was written expecting the buggy behaviour. Finally, there are a large number of minor bug fixes Thanks to: Martin Bergtholdt, Stefano Borini, Alex Chabot, Kit Choi, Mark Dickinson, Kevin Duff, Matthew Evans, Matt Hancock, Robert Kern, Fede Miorelli, Rahul Poruri, Jenni Portman, Prabhu Ramachandran, @ransonr, Jonathan Rocher, Roger Serwy, Ajeet Vivekanandan, Corran Webster, John Wiggins. Enhancemenrs ~~~~~~~~~~~~ * Switch to using six instead of 2to3 (#482, #484, #486, #498, #531). * Experimental Pyside2 support (#451, #500, #504). * Allow arbitrary rendering of tree nodes in Qt backend (#499, #502, #527). * Use tooltip metadata on a trait instead of desc, if available (#473). * Enable scroll to column for TabularEditor (#547) * Add demo for tabular editor with context menu (#460). * Allow the use of extended trait names in TreeNodes where possible (#500, #528). * Allow drag move and drag copy modes on Qt TabularEditor (#363). Fixes ~~~~~ * Fixes to README (#478). * Documentation fixes (#471, #508). * Fixes to setup.py (#549). * Fix an attribute error on Qt TextRangeEditor (#540). * Use SimpleEditor for Qt TextEditor's "text" style (#535). * Fix handling of Range traits with a mix of constant and named bounds (#533). * Fix display selection on Wx backend (#534). * Fixes for Qt TableEditor selections (#275). * Fix deprecation warnings for inspect.getargspec (#530). * Fixes to etstool (#526). * Fix TreeEditor unhashable type errors (#525). * Fix crash when TabularEditor has no columns (#521). * Clone editor factories so they don't share traits (#519). * Avoid creation of dummy traits by springy items (#515). * Fix drag and drop crash in Python 3 (#516). * Fix Undo/Redo for readonly items and error handling (#510). * Fix source parsing in demo app (#501). * Fix signature of close() method of Qt modal dialog (#506). * Fix incorrect code in Wx ColorEditor (#479). * Fix incorrect code in Wx ProgressEditor (#513). * Remove uses of deprecated HasTraits.set() calls (#457). * Fix rendering of CheckboxColumn on OS X and Qt (#456). Release 6.0.0 ------------- This release introduces preliminary support for Qt5 via PyQt5, thanks to the work of Gregor Thalhammer which got the ball rolling. Qt5 support is not yet robustly tested in deployed applications, so there may yet be bugs to find. As part of this effort all occurences of old-style signals and slots have been removed; and this has greatly improved stability under Qt. This release also features a great deal of work at the API level to disentangle the two-way dependencies between Pyface and TraitsUI. This has involved moving a number of sub-packages between the two libraries, most notably the zipped image resource support and a number of trait definitions. We have endeavored to keep backwards compatibility via stub modules in the original locations, but we can't guarantee that there will be no issues with third party code caused by the change in locations. We haven't been able to remove all dependencies, but as of this release on the dock and workbench submodules have required dependencies on TraitsUI. As part of the latter work, support for TraitsUI Themes have been removed. This was a feature that was only available under WxPython, was slow, was never used in production code, and was not supported for over a decade. Some of the codebase remains as it is still used by the Pyface Dock infrastructure and several editors, but ther long-term intention is to remove this completely. Another long-desired feature was the ability to write toolkit backends for Pyface and TraitsUI that are not part of the main codebase. This is now possible by contributing new toolkit backends to the "traitsui.toolkit" pkg_resources entry point in a setup.py. This work was accompanied by an overhaul of the toolkit discovery and loading infrastructure; in particular Pyface and TraitsUI now share the same code for performing these searches and loading the backends. The entire TraitsUI codebase has been run through the AutoPEP8, assisted with some customized fixups and occasional drive-by cleanups of code, which means that the codebase is generally easier to read and follows modern Python conventions. Finally, the testing infrastructure has been overhauled to provide a one-stop location to run tests in self-contained environments using Enthought's EDM package management tool. Tests can be run from any python environment with the "edm" command available and the "click" library installed with the "etstool.py" script at the top level of the repository. In particular:: python etstool.py test_all will run all relevant tests for all available toolkits in all supported python versions. The TravisCI and Appveyor continuous integration tools have been updated to make use of these facilities as well. Thanks to Martin Bergtholdt, Alex Chabot, Kit Choi, Mark Dickinson, Robin Dunn, Pradyun Gedam, Robert Kern, Marika Murphy, Pankaj Pandey, Steve Peak, Prabhu Ramachandran, Jonathan Rocher, John Thacker, Gregor Thalhammer, Senganal Thirunavukkarasu, John Tyree, Ioannis Tziakos, Alona Varshal, Corran Webster, John Wiggins Enhancements ~~~~~~~~~~~~ * Support for Qt5 (#347, #352, #350, #433) * Remove TraitsUI Themes (#342) * Improve Toolkit selection and handling (#425, #429) * API Documentation (#438) * Adapter documentation (#340) * Support multi-selection in DataFrameEditor (#413) * DataFrameEditor demo (#444) * Common BasePanel class for toolkits (#392) * Labels honor enable_when values (#401) * Better error messages when toolkit doesn't implement methods (#391) * Improve TraitsUI Action handling (#384) * ListEditor UI improvements (#338, #396, #395) * Remove old style signals and slots for Qt backend (#330, #346, #347, #403) * Expose a "refresh" trait for the DataFrameEditor (#288) * Use Enthought Deployment Manager to automate CI and testing (#321, #357) * Continuous integration on OS X (#322) * Reduce circular dependencies of Pyface on TraitsUI (#304) * PEP8-compliant formatting of source (#290) * Add progress bar column for TableEditor (#287) * Add codecov coverage reports (#285, #328) Fixes ~~~~~ * Fix some issues for newer WxPython (#418) * Fix Wx simple FileEditor (#426) * Fixes for DataFrameEditor (#415) * Fixes for preferences state saving under Qt (#410, #447) * Fix panel state after setting preferences (#253) * Fix TableEditor ColorColumn (#399) * Prevent loopback from slider in Qt RangeEditor (#400) * Fix Action buttons under Qt (#393, #394) * Fix ValueEditor icons (#386) * Fix bug in update_object (#379) * Avoid reading Event trait values in sync_value (#375) * Fix raise_to_debug calls (#362, #372) * Fix errors during garbage collection (#359) * Remove unused argument in wx.hook_events (#360) * Fix button label updates (#358) * Fix TreeEditor label updates (#335) * Proper InstanceEditor dialog lifecycle (#332) * Don't explicitly destroy child widgets under Qt (#283) * Test fixes and improvements (#329, #369, #371, #327) * Fixes for demos and examples (#320, #445) * Fix CheckListEditor string comparison (#318) * Remove some spurious print statements (#305) * Documentation fixes (#301, #326, #380, #438, #443) * Fixes for Python 3 compatibility (#295, #300, #165, #311, #410) * Fix error with Qt table model mimetype (#296) * Fixes for continuous integration (#299, #345, #365, #397, #420, #427) * Fix offset issue when dragging from Qt TreeEditor (#293) * Fix Wx kill-focus event issues (#291) * Fix readthedocs build (#281) Release 5.2.0 ------------- Enhancements ~~~~~~~~~~~~ * Add support for multi-select in the DataFrameEditor (#413). Release 5.1.0 ------------- Enhancements ~~~~~~~~~~~~ * Enthought Sphinx Theme Support (#219) * Allow hiding groups in split layouts on Qt (#246) * Allow subclass of Controller to set a default model (#255) * Add toolbar in Qt UI panel (#263) Fixes ~~~~~ * Fix Qt TableEditor segfault on editing close (#277) * Update tree nodes when adding children to am empty tree (#251) * Change default backend from Wx to Qt (#254, #256) * Improve toolkit selection (#259) * Fix capturing the mouse and click events on Wx (#265, #266) * Remove duplicated traits in NotebookEditor (#268) * Fix exception during disposal of ListStrEditor (#270) * Version number in documentation (#273) Release 5.0.0 ------------- This release features experimental support for Python 3 with the Qt toolkit! This is based in large part on the work of Yves Delley and Pradyun Gedam, but also owes a lot to Ioannis Tziakos for implementing container-based continuous integration and Prabhu Ramachandran and Corran Webster for tracking down last-minute bugs. Python 3 support is probably not yet ready for production use, but feedback and bug reports are welcome, and all future pull requests will be expected to work with Python 3.4 and later. Python 3 support requires Traits 4.5 or greater, and Pyface 5.0 or greater. In addition, this release includes fixes to support wxPython 3.0 and deprecates wxPython 2.6. Thanks to Robin Dunn for providing these improvements. This release also introduces a DataFrameEditor which provides a tabular view of a Pandas DataFrame, similar to the existing ArrayViewEditor. There are also a number of bug fixes and minor improvements detailed below. Finally, this release changes the default GUI toolkit from Wx to Qt. This changes the behaviour of the library in the case where both WxPython and PyQt/PySide are installed and the user or code doesn't specify the toolkit to use explicitly. New Features ~~~~~~~~~~~~ * Experimental Python 3 support (#230) * A DataFrameEditor for Pandas DataFrames, similar to the ArrayViewEditor (#196) Enhancements ~~~~~~~~~~~~ * Change the default backend from Wx to Qt (#212) * Add a Qt version of the ProgressEditor (#217) * Links to demos in the documentation (#159) * Add minimal support for the dock_styles option to the Qt tabbed List Editor. (#143) Fixes ~~~~~ * Fix failure to disconnect selection listeners for ListStrEditor in Qt (#223) * Fix layout of TextEditors in some situations (#216) * Fix removal of _popEventHandlers not owned by TraitsUI in Wx (#215) * Remove some old (TraitsUI 3.0-era) documentation (#214) * Help button now works on Qt (#160) Release 4.5.1 ------------- Fixes ~~~~~ * Fix pypi installation problem (#206) Release 4.5.0 ------------- Fixes ~~~~~ * Application-modal Traits UI dialogs are correctly styled as application-modal under Qt. On Macs, they will now appear as independent windows rather than drop-down sheets. (#164) * Qt CodeEditor now honors 'show_line_numbers' and the 'readonly' style (#137) * Deprecated `implements` declaration removed, use `provides` instead (#152) * Fix TableEditor so that Qt.QSplitter honors 'orientation' trait (#171) * Show row labels in Qt TableEditor when requested (#176) * Fix TupleEditor so that multiple change events are not fired (#179) * Numpy dependency is now optional. `ArrayEditor` will not be available if numpy cannot be imported (#181) * Add development versioning (#200) Release 4.4.0 ------------- The biggest change in this release is support for the new adaptation mechanism in Traits 4.4.0. Other than that, there are a number of other minor changes, improvements and bugfixes. Corran Webster (corranwebster on GitHub) is now maintainer of TraitsUI. Change summary since 4.3.0 New Features ~~~~~~~~~~~~ * Changes for new Traits adaptation mechanism support (#113) Enhancements ~~~~~~~~~~~~ * Add Travis-CI support. * Remove the use of the deprecated PySimpleApp under Wx and several other improvements. (#107) * Improvements to Qt TabularEditor, TableEditor and TreeEditor drag and drop support. Should be roughly on par with Wx support. No API changes. (#124, #126, #129, #135) * Improvements to PyMimeData coercion to better handle lists of items. (#127) Fixes ~~~~~ * Fixes item selection issue #133 in ListStrEditor under Wx 2.9 (#137) * Fixes to avoid asking for value of a Delegated Event (#123 and #136) * Fix drag image location for Qt TreeEditor (#132) * Qt TreeEditor supports bg and fg colors and column labels correctly. (#131) * Fix ListEditor under PySide (#125) * remove event handlers before window destruction in Wx. Required for Wx 2.9. (#108) There are currently some other unlisted changes going back some time before this file was created. Traits 3.5.0 (Oct 15, 2010) --------------------------- Enhancements ~~~~~~~~~~~~ * adding support for drop-down menu in Button traits, but only for qt backend * adding 'show_notebook_menu' option to ListEditor so that the user can right-click and show or hide the context menu (Qt) * added selection range traits to make it possible for users to replace selected text Fixes ~~~~~ * fixed null color editor to work with tuples * bug when opening a view with the ToolbarButton Traits 3.4.0 (May 26, 2010) --------------------------- Enhancements ~~~~~~~~~~~~ * adding new example to make testing rgb color editor easier Fixes ~~~~~ * fixed NumericColumn to not expect object to have model_selection attribute, and removed more dead theming code * fixed API bugs with the NumericColumn where its function signatures differed from its base class, but the calling code expected them to all be the same * fixed bug which was related to type name errors caused when running Sphinx * when using File(exists=True), be sure to validate the type of the value first before using os.path.isfile() Traits 3.3.0 (Feb 24, 2010) --------------------------- Enhancements ~~~~~~~~~~~~ The major enhancement this release is that the entire Traits package has been changed to use relative imports so that it can be installed as a sub-package inside another larger library or package. This was not previously possible, since the various modules inside Traits would import each other directly through "traits.[module]". Many thanks to Darren Dale for the patch. Fixes ~~~~~ There have been numerous minor bugfixes since the last release. The most notable ones are: * Many fixes involve making Traits UI more robust if wxPython is not installed on a system. In the past, we have been able to use Qt if it was also installed, but removing Wx would lead to a variety of little bugs in various places. We've squashed a number of these. We've also added better checks to make sure we're selecting the right toolkit at import and at runtime. * A nasty cyclic reference was discovered and eliminated in DelegatesTo traits. * The Undefined and Uninitialized Traits were made into true singletons. * Much of the inconsistent formatting across the entire Traits source has been eliminated and normalized (tabs/spaces, line endings). Traits 3.2.0 (July 15, 2009) ---------------------------- Enhancements ~~~~~~~~~~~~ * Implemented editable_labels attribute in the TabularEditor for enabling editing of the labels (i.e. the first column) * Saving/restoring window positions works with multiple displays of different sizes * New ProgressEditor * Changed default colors for TableEditor * Added support for HTMLEditor for QT backend using QtWebKit * Improved support for opening links in external browser from HTMLEditor * Added support for TabularEditor for QT backend * Added support for marking up the CodeEditor, including adding squiggles and dimming lines * Added SearchEditor * Improved unicode support * Changed behavior of RangeEditor text box to not auto-set * Added support in RangeEditor for specifying the method to evaluate new values. * Add DefaultOverride editor factory courtesy Stéfan van der Walt * Removed sys.exit() call from SaveHandler.exit() Fixes ~~~~~ ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/LICENSE.txt0000644000175100001730000000312500000000000016011 0ustar00runnerdocker00000000000000This software is OSI Certified Open Source Software. OSI Certified is a certification mark of the Open Source Initiative. Copyright (c) 2004-2023, Enthought, Inc. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Enthought, Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/MANIFEST.in0000644000175100001730000000063000000000000015722 0ustar00runnerdocker00000000000000include CHANGES.txt include LICENSE.txt include MANIFEST.in include README.rst include README_example.png include image_LICENSE.txt include image_LICENSE_Eclipse.txt include image_LICENSE_Nuvola.txt include image_LICENSE_OOo.txt include TODO.txt include edm.yaml graft docs prune docs/build graft examples recursive-include traitsui *.py *.zip *.png *.txt *.ico recursive-include integrationtests *.py *.gif ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1684768028.1278067 traitsui-8.0.0/PKG-INFO0000644000175100001730000001275100000000000015270 0ustar00runnerdocker00000000000000Metadata-Version: 2.1 Name: traitsui Version: 8.0.0 Summary: Traits-capable user interfaces Author-email: Enthought License: This software is OSI Certified Open Source Software. OSI Certified is a certification mark of the Open Source Initiative. Copyright (c) 2004-2023, Enthought, Inc. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Enthought, Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. Project-URL: source, https://github.com/enthought/traitsui Project-URL: docs, https://docs.enthought.com/traitsui Keywords: gui,traits,traitsui,pyqt,pyside,wxpython Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: BSD License Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python :: 3 Requires-Python: >=3.7 Description-Content-Type: text/x-rst Provides-Extra: docs Provides-Extra: editors Provides-Extra: examples Provides-Extra: pyqt5 Provides-Extra: pyqt6 Provides-Extra: pyside2 Provides-Extra: pyside6 Provides-Extra: test Provides-Extra: wx License-File: LICENSE.txt ============================================ TraitsUI: Traits-capable windowing framework ============================================ The TraitsUI project provides a toolkit-independent GUI abstraction layer, which is used to support the "visualization" features of the `Traits `__ package. You can write a model using the Traits API and specify a GUI using the TraitsUI API (views, items, editors, etc.), and let TraitsUI and your selected toolkit back-end (Qt or Wx) take care of the details of displaying them. Example ------- Given a Traits model like the following:: from traits.api import HasTraits, Str, Range, Enum class Person(HasTraits): name = Str('Jane Doe') age = Range(low=0) gender = Enum('female', 'male') person = Person(age=30) And using TraitsUI to specify and display a GUI view:: from traitsui.api import Item, RangeEditor, View person_view = View( Item('name'), Item('gender'), Item('age', editor=RangeEditor(mode='spinner', low=0, high=150)), buttons=['OK', 'Cancel'], resizable=True, ) person.configure_traits(view=person_view) It creates a GUI which looks like this: .. image:: https://raw.github.com/enthought/traitsui/main/README_example.png Important Links --------------- - Website and Documentation: ``__ * User Manual ``__ * Tutorial ``__ * API Documentation ``__ - Source code repository: ``__ * Issue tracker: ``__ - Download releases: ``__ - Mailing list: ``__ Installation ------------ If you want to run traitsui, you must also install: - Traits ``__ - Pyface ``__ You will also need one of the following backends: - wxPython - PySide2 - PyQt5 Backends have additional dependencies and there are optional dependencies on NumPy and Pandas for some editors. TraitsUI along with all dependencies can be installed in a straightforward way using the `Enthought Deployment Manager `__, ``pip`` or other package managers. .. end_of_long_description Running the Test Suite ---------------------- To run the test suite, you will need to install Git and `EDM `__ as well as have a Python environment which has install `Click `__ available. You can then follow the instructions in ``etstool.py``. In particular:: > python etstool.py test_all will run tests in all supported environments automatically. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/README.rst0000644000175100001730000000554500000000000015665 0ustar00runnerdocker00000000000000============================================ TraitsUI: Traits-capable windowing framework ============================================ The TraitsUI project provides a toolkit-independent GUI abstraction layer, which is used to support the "visualization" features of the `Traits `__ package. You can write a model using the Traits API and specify a GUI using the TraitsUI API (views, items, editors, etc.), and let TraitsUI and your selected toolkit back-end (Qt or Wx) take care of the details of displaying them. Example ------- Given a Traits model like the following:: from traits.api import HasTraits, Str, Range, Enum class Person(HasTraits): name = Str('Jane Doe') age = Range(low=0) gender = Enum('female', 'male') person = Person(age=30) And using TraitsUI to specify and display a GUI view:: from traitsui.api import Item, RangeEditor, View person_view = View( Item('name'), Item('gender'), Item('age', editor=RangeEditor(mode='spinner', low=0, high=150)), buttons=['OK', 'Cancel'], resizable=True, ) person.configure_traits(view=person_view) It creates a GUI which looks like this: .. image:: https://raw.github.com/enthought/traitsui/main/README_example.png Important Links --------------- - Website and Documentation: ``__ * User Manual ``__ * Tutorial ``__ * API Documentation ``__ - Source code repository: ``__ * Issue tracker: ``__ - Download releases: ``__ - Mailing list: ``__ Installation ------------ If you want to run traitsui, you must also install: - Traits ``__ - Pyface ``__ You will also need one of the following backends: - wxPython - PySide2 - PyQt5 Backends have additional dependencies and there are optional dependencies on NumPy and Pandas for some editors. TraitsUI along with all dependencies can be installed in a straightforward way using the `Enthought Deployment Manager `__, ``pip`` or other package managers. .. end_of_long_description Running the Test Suite ---------------------- To run the test suite, you will need to install Git and `EDM `__ as well as have a Python environment which has install `Click `__ available. You can then follow the instructions in ``etstool.py``. In particular:: > python etstool.py test_all will run tests in all supported environments automatically. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/README_example.png0000644000175100001730000006352100000000000017352 0ustar00runnerdocker00000000000000PNG  IHDRi iCCPICC ProfileHWXS[R -)7Az;FHC ؑEׂ@".lIͽΜs?Lfe Y\a3>!Id2P`s!-ű"8΂:[  zsrbbe!$,ƩR!Rl)2 'c8r-X!5by*?I1Yq,E"d^ 5,LQӄadci'CyGb/D1oX3O0İ(C9YB/Gxѣ8Y8;r4>iј+ac*1i8)O=ĝ9QӼlH1g}ߥ"6jVX^% GnZtćqp}|03+rԷH1jUq3#uE^˅LZq:+(B{/ȍrq`l`6HKX@Rj<$=|/ gKyPu\+}Io#<8 Wp<>=`q1?بD_1G43a{ 7 ݄ rabbI߳xŸ3A(~~%C1ýpWr:0ma&;jd(g?'cz9S9Qǭ~C8%;Na0Xv;.3d&)l,,-QBܹ=[0OKMez -&1--Co#u٭8CewKcO{ZwE<?C 51|A ̄UOY,EAv=`?8A 8΂K/ x!!4!ڈbX#⋄ H"|D,@!%H)R@jߑc)҅Ez~ P*jdDhtfh!-G}hz Dї0Y`#捅cX &aXV᷾uc'q&ngl| x;~oA`Fp& 9"Ba7( \Q}D"A4":@L''"n%[]^DR#\I$)TDLG:IF#}$˒d?r"O. Oed dee82di*'3LQQ\)єtRJ9 孬TYrكe{d?QTotZCmޥh4Z"-VK;M{D(G -k&J^F@S~||a 2 ,E n+ )W)U\dQ*TکtZt6}} Ol\_CyPEIV%VeJqn0d22k'hNr &|PU-VWY櫖NQ:n>U}z]&'O<4a1_ce!M-MMfӚZ -t Z'n< '_0ULf99#١ӡ3k[[P稗AMoP_[?T~=G4M >.7l4|njhoTgfnm\m|Ähhaդ53M34j̶ٛuM"LrğT=9<ϼμǂabQ`hj&2r}+% f7֦lJ64?6M6mlUwvv; ;;$9lqiS'g{\Cduy>h wʮ),nL$n:,jz+zMQL[|q>qq_JPO%4%cw'MqZtEo01wƅ33g%?5p!).ioV85%y~ls]g)))S]Sק y?dgddegZ 9HΌ\exԹ,2"s˫8'vṊss/3r޳|,XgEɋ-.\ܷɞKX[PpIa/ n/wYmc+s/X|Y^uW_YcĵֹSX_ڻ>t} 6x̶l&&ѦnRVqҫ~Ɩ[>llVQu`涒mΰڰl'qgΧbwKvt^P[Wc:NT׿o>QϨ/9=[v<|-GGy iM M]ǂ54⏚*ל(<1r2PuT޶YmOǟ>LgΞ>yy-/xv^rþզN)]'_;u7. u+֝owy~7{y/y@xPPa#GYm}ǧ{ٽ/ 314 396 1 !@IDATx]`UE^I{HޛHEł(`w-wUt+VĎ EN ^_3%$!^S̜9gΙvPZZ˕6 NgEPwo{ڊUf]=W{ rlW[Ǔw 1ȳ WxTX^÷mH≿*+TژMOWS-uq~0ܹ)U{Jc0A5k`YEGH(J c@@FPk3 C+F[3a?Fos^E_3@!t/+ VbIHwdQp7 Cۜ@³^mha̟}맿.IPw d!.#njATTT#1hND:p*. aWM 搎-G,5`UyA ii&m9*~霩a{{5 ~қn12:) òE߅f0jqyo ?g*J;Oѿ v&Ԣa"$~v8N\x[8f_^P !e%tuZoRH9Kr%< O/|{&z%E% dU˚hejf(ROjK Kkx$\ R z[6w6 vcUW9k*_C`A PO6J a0u U'@ɻIđVxTelexaJP{ OO$u6x&1?7t~.~&q${o=IcK0?߭:?#(4g;)>=߇פ*` ˰aWJ!""!ȟ*K*gPs2NnOs?FzN1'|-(8 eDQrTk;!tE0|١yMJފr  (&楤t 1Js:[XB$%$_*UyXM|7v|j`Stj2ҳ(=dDT|KT(.v|Q&B@h(^ y߁<Zn # #;^G9F!><|%eBDD(L:Υ)թuD۹O$2 wK2L1n!J/ww ^Y1 ()|K5xM %L1nA0  "[R:tGsc + "$;3 ^Ga |۳ajH<wF&]FRc&=aIj*ɂOqVm U ?H޾{BJm۵ǡ=l%s7vœϼEMwށa:&-/O~Ald^޸69[$?bd8|X9qH=Kngq:+30,x Ln.>&2n^CJ9EL:$c%X<IVl-:4<.C0XY_lǃ_`6Z^} >QHOއ/S'v 0DPaso"mOHp! "9$g=w)-8W!Ug4!f rL!݃;SH#4{y.epm1zi qI =91+iI<eRJzw'{o罋Gg[AzoTLH+2Lxe.:3!*\ '?(OۋM^0o!ľu}hx.NK[zg߂P-`gKmqo?/{dMK=v!J޼Bv,^Jހy7>" 9)wn0$ʊYo|GCbe92?z Aծ: J!Mg9Z ȫT$&4zZ|?E8WO0 +Z:,=;Fszs4*w!g^mތWE1p B *麾*|ؚK/>>Eb%hP%a!ġaa𥾤OpNݎqo#B+áU_0,2.)1o7՘qdtMNuvRҙP[).B1{9x֮زo?:?S9> \t /Xt]&vH^&N@qe8 c-j\0r|c2ذwc "۶pLhD]oPƲS,_3֬ nT#1ҏL!v5N H\jI119LO%_;XiD:0>EZa?(k}oUi)M-WهJP䀨 .&WKx;K6>)*M9A F\NEFQ #DjZF࿸E$:aIJ58omۆ۷cǎغe3HEH\~ӐAj~He#哅dى\ᣩh? j͡=[\L "|C1bYIǒE#ؽ @#C-Dώ&'HNFnF >y%z A\sL: \'"$؍LvQsV':tw"ev0l>7,^LDžAҊLI+URK&>Mr:ȯ.̆`W2rS>woُddehNAv|r Nԫ>'+T~Gԯ;DŹG,}02eFNi:5jg`+[=,& ztCv~|Uԭl(4bm6L({nCwʹz{l$s\<P$$ :;e\|"#>"3(g R` mسco݂uk`ݺuذq#|Lг[w$v?UkTzC`/l^I}>:DZ^n2tm'c#q&Nۑ!N,Z˖/_pPc#0d |ǴODl,X[rA.9)C^ݱ%X5?6'ؽ]/+_ip/s%To|z`7K1O'r0۞t#4k?߃C8Yx۸3x`r ҵg7,ro_s >56`ɏKeԕ}@ukS6S6ʂjLw*ߥ%֯C eW +%=%;} Jٵ1Ee))%Z߳Wjd:-+P 3!^uo r˶HL#!=*[¹"G7(+PߖY&9ضsk#未t}Ty*daMTQȔW`?rD沲L".g5>b/,+f!NNk7s[q.E!mJ+{l}rH&|_Qʌ4 ?O rz6΃/adAVi2Na4ɏiFU$8Y@oBȚiWpyZ8~)ZLI@1,G"5BX:DQȞ@IQ>t,kA$# v0 #vzUxfOiu|5Ofkĉ4I*#<"Ó^z8>7|2}@IK!Y㦕ԁRՈ@#q MyA08PJzL1E^Guu3l?ݾwp1\JkT0` +cAFJ's QʩI݄1E5v Ra"3HKb cv~fK D/ԦS;҃1N•+8ƢnƧaڵNuJ Y5"om|%BJfqAcDbj } '!,]G P/ ЩC>s0Wb#T'0>SR ^*ZoIa+Ě7pu;8%kSkk2֬YCԩ^E&qhHj2tX.M(ܙB~=0Y" 8v T".mEeҤr(xysZXѢVA@}aTNf*!Q НQ gNڽTQE 9.OIuQ5\4$ʱt.7&?99X~m,p>Vj][V6jG_ zzǀYFuJ%G?fQt#F}n˫,8RfF9^jYI(P €8 N`- >65!0MUvT SwQo2nA^6W,U bEo"M櫋`M16 zg pguѧb!iq`T9j0/]bh:%2e&?9f:݋ pu\|9j*ƭW$VA"75[%Hiȶ^Aµ,[my VD+h uJQ 7p/6+8ݻwAld+?E\7Gtƨ3R-I8ʱЩ?T@SAB 4p sAR*wB!ʲJ{ʐQZ 1{J ;z_CzzJUŏݿԂeJ7EQZ$_W>*_W9ܪPƸRY*Cbz9yֆIBܬVSoWMU7;+δko@*l{v2 |YiַEHƓ>kV2FH"T  ]e.KK6j݅HQ~.yJtyzA=ERGSuص wn+.6jɈ̢GEf^zY%LҸ\ Yx\01FQ4$<۴a7!)ϋ#¼LlsڋbU\r8?EFn"lغmy>Xk&lj:`++۱{A%jlo .직!#ʊw6g'rY YQa: SV:"ń9TfYSnOᩧf~p(l?RvH &| ?Y*O);/9{419q @#%7`|JNs ÚF]B'{uA&zpSpri^6cб}lx @U /'GZ_}y-uG-@S/l|p/c5#LoZ~Gם_bٟPZdž8}:lyZ O?m.mʆ:I$3uM|?̹ $=,rJw|c#p xi0=<]:icƽع 6\0wf(HzJ.ZڴTF(.?Ř»h-# _2Z؂Ϙ3(#vLq}7VPvi9G*E\zk|Q6EVeZ]UI?ly&r(pbD(wvq3pPx 𱓙Ocϡ/}7KFl ڭ[SjNc }d. p'g^/W 1ƏCȳ0o#%pD=?OHu Y S i~A<8^t1Xs-V)w=Ў0? ΖqxY,<`Ŀqi<Mp\D ΋ԘF5SR׋)0%.gvEU:W}\.efjCj΄-(e^ZxBL4lV*&9[9PWv:MJqIp: DBs(:r[~ wF ڛ~|NمI}̷8.[UN?@Pn񌴔ѣK7|P9c0a(- W(vd3ymxg*0D!:wD/9‹+-SQ~8ϽRVJ YFWbՖdI'2D.TL\[d\HW@^v0/!4 ƒeâئH@74Lu g!(<dLD'J#0ѠL]-wm7h+"s2SXrL?k)dн=M|BUH̶H}$&`wllK҄Δr]\M߲'?IbM ,+⯗E:Ae!w\[#F 3 2-"]!զ"ƾڧP'FŶ7u(Uo/:՜~WI7+ n~USB?+ q$ܯq @ylf^PE~2=e%Q+'~=L.j5ھ%0~HEpe~tY*KU㛋;pğФ*z\wxz\r˥e{ u0^=ن=ah2cȯm! UTIR-O[3lטBPs< ŔL:Dv_ BH Ԛd8Q1"ZSUۻHhu!} Wlr_qq ZvYVIӵ`ZΛO'kri;WYtS+OI€zg(./tPkC#|8؁&k1{ kY(OR 8ԤJ h!xq!4[m׎E%,kR$E'2ť-4jC*oqv6=Ê 5R$b CML!IZC!d J1 CmBGIz9!=lR*jMAy0P6Z6H lh ҋw,դ10Of*cRXZԘ/iCSS"!<ךWt <p UJNd"1V/xj=:ũ#L!g; h]Wk͌!$SrM#V^~:9)І1x ȩЖU$FmЉFLCSvV?)W[,NڦYH(xN(k-eňTVZ@J{!6Rgy02LKc2K6s۲uZ,ڱ7MZ,ef2V3mZ3H5k{SL_S*F]jJP?~V4wݏUQ_T%O]%)@g Kqvȡ}S˦\g5[ J|^g͏1%o9g$C;1oҢ4 [:ṿ硿KH X4ٴ3n ]5*zhtڗ+PDxR=`?&hwVhs6U#>^p>CAyz+k!P i 1ds-&t[q͓cOp5g9 7lTW͉weveu播`hI[oD[@^i%Xu"(6#ojTC}K'ʺJy0sPiA׉o=,m.B,|kpZ:2+nspe[.i%4BIFU3ű>hh@ 3vcO_|+U)!ZgFbț@BL?ǎIΔhԋʺ6z'&e b9!d zxt "BhωJ fط?1-el)a@EZH'J\AN:? z,OJrAJAnjo Z WVRrWP$ڲ9HNIG`X4b"i@1f<9943K8!.sA^ ԞlˠgpZlƒʈ^02HfR43v4صc˱#׉poGTK<5`-;c#f-͢G!1.y 8gnė 2k_ӧE+&WNxb<ڰt7ZWgxi4q]%9x1oH&*0~!_].if$^=]]vElTU"'WVjlv*iTxT:16!@4vQRc;~QԦ'F^KrXQ:Fbt5RSi)'[{EZFs'AZ.p{ s//Y=bꣷ(-[wVfwc7-_ x燵'LJKf.,{G@~>F3t?f&,z%9|lܝ? 6!|]k~BE_h<j l(qTG@dI?n~1m sH.cxY[coݫ Pq-8o\ٛ9KTA~z$YO,׷*^HV@Ӟth>^X݈b a}-oJzrϒ%$ d#J jVfa{N~,?_lFؖb3i0GR)0_LK$'nG THK1٩\Yxzt!Iⶇ܋^%(%Xet~&Ci/{:?`8jѰr1#kbS&}JA ~xTbx}{!eIر?kh;l8h?z4jfocsSYO?LZO?#uIPYʰkZO|6*dl(5/8|ʆG6G۲/*CdQ8㘢*rqCs,,Fd 썖( EH4FH6qE1 9 _2 x#c*As?$RKL+@O&IbeHdoma!%݈!t[/9$>M[b??^oZ1X-SEhydzX 1F%!"(6SĎފY*U#cHG+m*DӞr_b\lvD5X7t9( UDbS/;,\yn)e`Ɋ CVRtz BQwם5zr7W~KjgKk~Տ;ӯZO) Y2[f6+X!ųRx6˧0BQ\"&$Yڸe"2;mxKNj8Ò#)cTs;L||t-jqbleu7CՔ Nks;&vd"e&͌,8⧰౛f=+&OFgILyTʠY!؄_ycR26eHsɻ^*X 0b6e㢋R1+qߤtZ)$W='z?8aWy@}Itp;Uc?IO` @7sjUN]p;/6LDf]Fu\mpY4|0~+>GSh7p-q76s?gcpn42P#ٿX`X%_{&@o}0j C,t0xUxfo>&t`eM#/ {~wRܴ( kƐ޽.OHjtEvNlm_!}XywtO@k8 Ú'ߠ[WBxt: 5q x織:7NpC[ȘAL oDz3р~v#hF<0ڃ;j Ci[gZ= ,p:Tg%,Edf -#F/_F0ߗpc-{q@VG HNrjr8r 8a'Ś,"eswMD8Ui H!Ȱ eVfȴ1otr]XeaxUnJEڞ|('& c0QU7EIo%"6'*\3Y}+M#t9(&c2~j6gM߄~u^20C+~=CYmĂӋx5A9Jd :IkZqt#Am)=4 A +SR.E CVb՜07SYIxbهƓx* ,CL|bp|QFZ: )N%}s櫬kU f u)&[n>nBW>ݲj+P'X.#y;q";ȯjƥIP}_9(nҧɅ9d )sixha]mpWT˲+ݢēr9$E3LwfN OeUu?-DuQJRwKUw!wyj58:i~*q#Qğ>)g/́o~Z7l)O7 M.AP"c(:n<ހzTh4{IKV󭶆;_-e [G$;wBf=;\[^%[Gw]=ΎkI͇ݳ=g-Ï*T1Ao8M%7r"%rdA7c6wBx2ΐCJb"_ﭛ;OvZǡB_$|VF;Lrk\ү9+(8XނkU!L! [: OpVkwDr޲!P g+ʸI?*+¯}iFLjS˅;-ٵ*ƐJ$Epp0~oa%0GS^ :iTuZw1ɫyjL=rl%n^f]jcC磸U3S27ꯎj&~qܻRV'1vJ$G̢QrL"6.)ʶZsTi}ƍ TVoՌȞ)*;+Qio-^*:t x7"v%gky+WC%/`mQ'aZ_H]T+G"'OKxuz w'1Hh4#Ŀ{>~O!IaXW\UTp?ui~DM.fs^#{ne8qNz3ē>2J:,X;]|>KSiU2Kbx(Fغu;2s <bɤ\N4i hoB+clddŀXg_$˄IDAT|gy&^xwO9iX%\z`+ÖoCOqG :ߌ̃[1c 0#.73~&]x2f?[後߮|h;Eُ)2v{ag~ĔN J,i%\6:c ٳv¾}0}tu u+䅲 ,<:~*~oñXVX]ɅC<U8{*G+ʭw_E xx9a!V*=pf Xu/B5]a/Nީ)RGwnvݗ]P5?ef阜$L S$>&pRbD"/܅{3g}KRڭ5Av@UBFv";+ʽ5s3fc}mcy箹6b"EeJ5jgSKW4uB\u1ʲ;YQ瞍ۡCH ͺ,opYؼ;W{y/Oxܗ1L^eȍ.ěsi.쩫y?!paK qm5a!"EżaEFt5}YGp` =xݰb"SW?\後 ~qjzm#WټF 年fp ǀ-c }LQ=9`AeW=Yޟ : JwGpɾ6nfùK(OνepBL֏2{7hq̬?;:*7Nsh"꣏> 7ܠټqUW]ۅ4g WiH}oԌ{!xM=ο}<ޱ_o;l{p{1 H&r%P'8 `q0w,ox1<1it %?~Zmqzꄌ1=5%G߈nꂧ/=3ъ6ܗ!8Ȗ +n=gc.yܩv'&&SNHHHPk/NW]然{᪗Ed!$rtJhzZr^%R ǍIoȷ.qjͧ!3%Ou[N*UI_q]w}6&>{ј4'Iz}QpFЉ8E^y x>BҝRC ;x dypӦMjJ?!Ҷ*įo*C#=Lot z STOCF˙ a2$r_!l!ҙ!=搙)]zRD2zo! B'ŀC;*VX߭1Ox @#Zrӵ5ƀ1N4= xE4'8"0aLBh xDcܓ_1ZD3y y1aqO~-h)ƀ1N4= xE4'8"0aLBh xDcܓ_1ZD3y y1rcqFvk2BB."֤0m ؒb@C֬G0 t#_&:0*d[cHeAw9ۘ3-e/~~~Mw&g;p0WorfmChfu9g !\e!&&6D.a{ zYVnX5a7I!Lrsqo\,6V+ջyzKեSʠT ?zs]kے>ɽ$,LЫVu=Z=F!n1ʽo4-YcJ{QJ4#R,Amh{4'e,sכFi&H}drUs8ek1I*jRD=7݅ 7 |^8|A=:b" LN'I~*!x?-\bk錊4?1u'nߖpe`_QoCr/.TfBè1ull?N^Y|p@,{sĝ}3zr7/eɜfbKS+e,1 ښp2'>sɝN\ԉ NJH~`KwcGs.{D)ڀ6~$")(%b"$ЕQGKIfm 3;#)q(=aQh%!OEC$WʤqPmV <WD=AYzxx▸XOJCvoHfK]w0h5T('y5eRyBewJ"C ٍ_=A]VDW.}|:'&@b޽ BQlUٯ^!_pv5H;ds݇/#*>cƏE629)x֋߾#c|A rp@ӻ #a^_r8f ñ;N.`jG;ǰ̦uށrx')]1 %edQNWX^5?P r٪]۶ӪNQm%PxlG?n4k~C*)E? _Y`+ע?:^* e8o$,_ zO7s"H.E1{zG{Y`vV?9%$!Ʃ0&¶E~jJ&H9Ɍꋝx 5+_(8w~:^88)z_x lgF*Z/`u?"S{6ƒq!#c.[/9JUsA0MTmHY YT䍲GȤ XťhWށNE. òVR&O3FY%qAv NrrC2YM@$F򲬎.D#־yBz#WN"R=I5!P5oYr !mqSQqb C?Z  O~[bG;꺺g^Fgؾl?3)볱@d3+~z|櫸qXĆ"ԫkS$/WRmω2+@D/U?i/;8 [رdm[|u8ЏF HlFoC8,eKJieJ/'1xʶwnn.; Fx"/>m$b12bL[,L&r\GP]dA7'cpW;# ;W=6N}a:xvrsq9OcIps6຀,"[sxB&f?u˱~lEiE8;TjE) 4E͸Hk3!HOM=)wtQIչȉX ?xp>J/xνP3O*nN|uhQ3RӣZKWd[DlE+ 1" ѡΉ-X+\r8.Xj G-f/޳5Wa,;)8;b/TYPsQ!z9r5]lsMwN՚9Ц V^cD$9^Gs &.y'| wQ3|'38C rS Kz Y@BB+ՠ wwtiQҳ1EE#,HjL!*A\oNOW@0Ļ޼!)8g6J#-*C8эmH>fe,v^tB mndq߾}ԶQ'`ǁVyU@C'64L[|$qh# Ht+k[ji՘Ck WϠo-DU%":*WU  zdWqm,r  &%tA6w2;ḿVG+7_yT:_rTƩͽ,5ǮWt 25Ɛ:A5hk Ϳz dj[q- N2d?Ղ[ !)=". dj6)$LO[Mt#iHv:-x8 U"jB+kqec ' jS1^QoNII;R[ŗ3)lJ\\\4,%lL.T2kmn1`laTx1H~*+::ZMnٲES^SM){;Ȼ<49~̐r,:*MQ)yi^6$[G .Cͣ*CeOJINEEE \BZ[^O&FfJQf⨭L-_0rL۹QᣖTaZB@Ϩ)Qf/_3t*-'GTVKa뚃+YQs7 gQO _:nuq΍2!Y5G@ÀzcqR%9љh-0Dk$"'Bw}}}Ռ0|‘߈D '͎ODzz6)I{nkSD_en==81  'PRf3P;YLK"qodtџWOtww'0r&CKkkw5=o#w@A *cv}Q%RIENDB`././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1684768028.0118074 traitsui-8.0.0/docs/0000755000175100001730000000000000000000000015115 5ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/docs/Makefile0000644000175100001730000001077200000000000016564 0ustar00runnerdocker00000000000000# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = build # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " devhelp to make HTML files and a Devhelp project" @echo " epub to make an epub" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run them through pdflatex" @echo " text to make text files" @echo " man to make manual pages" @echo " changes to make an overview of all changed/added/deprecated items" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" clean: -rm -rf $(BUILDDIR)/* html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." singlehtml: $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/traitsui.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/traitsui.qhc" devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" @echo "# mkdir -p $$HOME/.local/share/devhelp/traitsui" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/traitsui" @echo "# devhelp" epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." latexpdf: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." make -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." man: $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/docs/README.rst0000644000175100001730000000100000000000000016573 0ustar00runnerdocker00000000000000Building the TraitsUI Docs ============================ Dependency: Sphinx Optional Dependency: `Enthought-sphinx-theme`_ The docs can be built by running $ make clean $ make html from ``docs/``, which contains the ``Makefile``. Refer to ``docs/Makefile`` and ``docs/source/conf.py`` for further information on building the docs. NOTE : ``docs/source/conf.py`` calls ``_version.py`` which is autogenerated when the user installs/develops the library. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/docs/api.css0000644000175100001730000001103200000000000016375 0ustar00runnerdocker00000000000000 /* Body color */ body {background: #ffffff; color: #000000; font-family: "Lucida Sans", Verdana, Arial, sans-serif;} /* Tables */ table.summary, table.details, table.index { background: #e8f0f8; color: #000000; } tr.summary, tr.details, tr.index { background: #70b0f0; color: #000000; text-align: left; font-size: 120%; } tr.group { background: #c0e0f8; color: #000000; text-align: left; font-size: 120%; font-style: italic; } /* Documentation page titles */ h2.module { margin-top: 0.2em; } h2.class { margin-top: 0.2em; } /* Headings */ h1.heading { font-size: +125%; font-style: italic; font-weight: bold; } h2.heading { font-size: +110%; font-style: italic; font-weight: bold; } h3.heading { font-style: italic; font-weight: normal; } /* Base tree */ pre.base-tree { font-size: 80%; margin: 0; } /* Details Sections */ table.func-details { background: #e8f0f8; color: #000000; border: 2px groove #c0d0d0; padding: 0 1em 0 1em; margin: 0.4em 0 0 0; } h3.func-detail { background: transparent; color: #000000; margin: 0 0 1em 0; } table.var-details { background: #e8f0f8; color: #000000; border: 2px groove #c0d0d0; padding: 0 1em 0 1em; margin: 0.4em 0 0 0; } h3.var-details { background: transparent; color: #000000; margin: 0 0 1em 0; } /* Function signatures */ .sig { background: transparent; color: #000000; font-weight: bold; font-size: +125%; } .sig-name { background: transparent; color: #006080; } .sig-arg, .sig-kwarg, .sig-vararg { background: transparent; color: #008060; } .sig-default { background: transparent; color: #602000; } .summary-sig { background: transparent; color: #000000; } .summary-sig-name { background: transparent; color: #204080; } .summary-sig-arg, .summary-sig-kwarg, .summary-sig-vararg { background: transparent; color: #008060; } /* Doctest blocks */ .py-src { background: transparent; color: #000000; } .py-prompt { background: transparent; color: #005050; font-weight: bold;} .py-string { background: transparent; color: #006030; } .py-comment { background: transparent; color: #003060; } .py-keyword { background: transparent; color: #600000; } .py-output { background: transparent; color: #404040; } pre.doctest-block { background: #f4faff; color: #000000; padding: .5em; margin: 1em; border: 1px solid #708890; } table pre.doctest-block { background: #dce4ec; color: #000000; padding: .5em; margin: 1em; border: 1px solid #708890; } /* Variable values */ pre.variable { background: #dce4ec; color: #000000; padding: .5em; margin: 0; border: 1px solid #708890; } .variable-linewrap { background: transparent; color: #604000; } .variable-ellipsis { background: transparent; color: #604000; } .variable-quote { background: transparent; color: #604000; } .re { background: transparent; color: #000000; } .re-char { background: transparent; color: #006030; } .re-op { background: transparent; color: #600000; } .re-group { background: transparent; color: #003060; } .re-ref { background: transparent; color: #404040; } /* Navigation bar */ table.navbar { background: #a0c0ff; color: #0000ff; border: 2px groove #c0d0d0; } th.navbar { background: #a0c0ff; color: #0000ff; } th.navselect { background: #70b0ff; color: #000000; } .nomargin { margin: 0; } /* Links */ a:link { background: transparent; color: #0000ff; } a:visited { background: transparent; color: #204080; } a.navbar:link { background: transparent; color: #0000ff; text-decoration: none; } a.navbar:visited { background: transparent; color: #204080; text-decoration: none; } code { font-family: "Lucida Console", "Courier New", Courier, monospace; } pre { font-family: "Lucida Console", "Courier New", Courier, monospace; } .pre { font-family: "Lucida Console", "Courier New", Courier, monospace; } pre.literal-block { margin-left: 1em; }././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/docs/make.bat0000644000175100001730000001556400000000000016535 0ustar00runnerdocker00000000000000@ECHO OFF REM Command file for Sphinx documentation if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=sphinx-build ) set BUILDDIR=build set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% source set I18NSPHINXOPTS=%SPHINXOPTS% source if NOT "%PAPER%" == "" ( set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% ) if "%1" == "" goto help if "%1" == "help" ( :help echo.Please use `make ^` where ^ is one of echo. html to make standalone HTML files echo. dirhtml to make HTML files named index.html in directories echo. singlehtml to make a single large HTML file echo. pickle to make pickle files echo. json to make JSON files echo. htmlhelp to make HTML files and a HTML help project echo. qthelp to make HTML files and a qthelp project echo. devhelp to make HTML files and a Devhelp project echo. epub to make an epub echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter echo. text to make text files echo. man to make manual pages echo. texinfo to make Texinfo files echo. gettext to make PO message catalogs echo. changes to make an overview over all changed/added/deprecated items echo. xml to make Docutils-native XML files echo. pseudoxml to make pseudoxml-XML files for display purposes echo. linkcheck to check all external links for integrity echo. doctest to run all doctests embedded in the documentation if enabled echo. coverage to run coverage check of the documentation if enabled goto end ) if "%1" == "clean" ( for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i del /q /s %BUILDDIR%\* rmdir /q /s source\api goto end ) REM Check if sphinx-build is available and fallback to Python version if any %SPHINXBUILD% 2> nul if errorlevel 9009 goto sphinx_python goto sphinx_ok :sphinx_python set SPHINXBUILD=python -m sphinx.__init__ %SPHINXBUILD% 2> nul if errorlevel 9009 ( echo. echo.The 'sphinx-build' command was not found. Make sure you have Sphinx echo.installed, then set the SPHINXBUILD environment variable to point echo.to the full path of the 'sphinx-build' executable. Alternatively you echo.may add the Sphinx directory to PATH. echo. echo.If you don't have Sphinx installed, grab it from echo.http://sphinx-doc.org/ exit /b 1 ) :sphinx_ok if "%1" == "html" ( %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/html. goto end ) if "%1" == "dirhtml" ( %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. goto end ) if "%1" == "singlehtml" ( %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. goto end ) if "%1" == "pickle" ( %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can process the pickle files. goto end ) if "%1" == "json" ( %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can process the JSON files. goto end ) if "%1" == "htmlhelp" ( %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can run HTML Help Workshop with the ^ .hhp project file in %BUILDDIR%/htmlhelp. goto end ) if "%1" == "qthelp" ( %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can run "qcollectiongenerator" with the ^ .qhcp project file in %BUILDDIR%/qthelp, like this: echo.^> qcollectiongenerator %BUILDDIR%\qthelp\PyWin32ctypes.qhcp echo.To view the help file: echo.^> assistant -collectionFile %BUILDDIR%\qthelp\PyWin32ctypes.ghc goto end ) if "%1" == "devhelp" ( %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp if errorlevel 1 exit /b 1 echo. echo.Build finished. goto end ) if "%1" == "epub" ( %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub if errorlevel 1 exit /b 1 echo. echo.Build finished. The epub file is in %BUILDDIR%/epub. goto end ) if "%1" == "latex" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex if errorlevel 1 exit /b 1 echo. echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. goto end ) if "%1" == "latexpdf" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex cd %BUILDDIR%/latex make all-pdf cd %~dp0 echo. echo.Build finished; the PDF files are in %BUILDDIR%/latex. goto end ) if "%1" == "latexpdfja" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex cd %BUILDDIR%/latex make all-pdf-ja cd %~dp0 echo. echo.Build finished; the PDF files are in %BUILDDIR%/latex. goto end ) if "%1" == "text" ( %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text if errorlevel 1 exit /b 1 echo. echo.Build finished. The text files are in %BUILDDIR%/text. goto end ) if "%1" == "man" ( %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man if errorlevel 1 exit /b 1 echo. echo.Build finished. The manual pages are in %BUILDDIR%/man. goto end ) if "%1" == "texinfo" ( %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo if errorlevel 1 exit /b 1 echo. echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. goto end ) if "%1" == "gettext" ( %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale if errorlevel 1 exit /b 1 echo. echo.Build finished. The message catalogs are in %BUILDDIR%/locale. goto end ) if "%1" == "changes" ( %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes if errorlevel 1 exit /b 1 echo. echo.The overview file is in %BUILDDIR%/changes. goto end ) if "%1" == "linkcheck" ( %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck if errorlevel 1 exit /b 1 echo. echo.Link check complete; look for any errors in the above output ^ or in %BUILDDIR%/linkcheck/output.txt. goto end ) if "%1" == "doctest" ( %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest if errorlevel 1 exit /b 1 echo. echo.Testing of doctests in the sources finished, look at the ^ results in %BUILDDIR%/doctest/output.txt. goto end ) if "%1" == "coverage" ( %SPHINXBUILD% -b coverage %ALLSPHINXOPTS% %BUILDDIR%/coverage if errorlevel 1 exit /b 1 echo. echo.Testing of coverage in the sources finished, look at the ^ results in %BUILDDIR%/coverage/python.txt. goto end ) if "%1" == "xml" ( %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml if errorlevel 1 exit /b 1 echo. echo.Build finished. The XML files are in %BUILDDIR%/xml. goto end ) if "%1" == "pseudoxml" ( %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml if errorlevel 1 exit /b 1 echo. echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. goto end ) :end ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1684768028.0118074 traitsui-8.0.0/docs/releases/0000755000175100001730000000000000000000000016720 5ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/docs/releases/README.rst0000644000175100001730000000201600000000000020406 0ustar00runnerdocker00000000000000The `upcoming` directory contains news fragments that will be added to the changelog for the NEXT release. Changes that are not of interest to the end-user can skip adding news fragment. Add a news fragment ------------------- Create a new file with a name like ``..rst``, where ```` is a pull request number, and ```` is one of: - ``feature``: New feature - ``bugfix``: Bug fixes - ``deprecation``: Deprecations of public API - ``removal``: Removal of public API - ``doc``: Documentation changes - ``test``: Changes to test suite ('end users' are distribution packagers) - ``build``: Build system changes that affect how the distribution is installed Then write a short sentence in the file that describes the changes for the end users, e.g. in ``123.removal.rst``:: Remove package xyz. Alternatively, use the following command, run from the project root directory and answer the questions:: python etstool.py changelog create (This command requires ``click`` in the environment.) ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1684768028.0118074 traitsui-8.0.0/docs/source/0000755000175100001730000000000000000000000016415 5ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1684768028.0118074 traitsui-8.0.0/docs/source/_static/0000755000175100001730000000000000000000000020043 5ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/docs/source/_static/default.css0000644000175100001730000003231200000000000022202 0ustar00runnerdocker00000000000000/** * Sphinx Doc Design */ body { font-family: 'Verdana', 'Helvetica', 'Arial', sans-serif; font-size: 100%; background-color: #333333; color: #000; margin: 0; padding: 0; } /* :::: LAYOUT :::: */ div.document { background-color: #24326e; } div.documentwrapper { float: left; width: 100%; } div.bodywrapper { margin: 0 0 0 230px; } div.body { background-color: white; padding: 0 20px 30px 20px; } div.sphinxsidebarwrapper { padding: 10px 5px 0 10px; } div.sphinxsidebar { float: left; width: 230px; margin-left: -100%; font-size: 90%; } p.logo { text-align: center; } div.clearer { clear: both; } div.footer { color: #fff; width: 100%; padding: 9px 0 9px 0; text-align: center; font-size: 75%; } div.footer a { color: #fff; text-decoration: underline; } div.related { background-color: #24326e; color: #fff; width: 100%; height: 30px; line-height: 30px; font-size: 90%; } div.related h3 { display: none; } div.related ul { margin: 0; padding: 0 0 0 10px; list-style: none; } div.related li { display: inline; } div.related li.right { float: right; margin-right: 5px; } div.related a { color: white; } /* ::: TOC :::: */ div.sphinxsidebar h3 { font-family: 'Verdana', 'Helvetica', 'Arial', sans-serif; color: #acafb3; font-size: 1.4em; font-weight: normal; margin: 0; padding: 0; } div.sphinxsidebar h4 { font-family: 'Verdana', 'Helvetica', 'Arial', sans-serif; color: #acafb3; font-size: 1.3em; font-weight: normal; margin: 5px 0 0 0; padding: 0; } div.sphinxsidebar p { color: white; } div.sphinxsidebar p.topless { margin: 5px 10px 10px 10px; } div.sphinxsidebar ul { margin: 10px; padding: 0; list-style: none; color: white; } div.sphinxsidebar ul ul, div.sphinxsidebar ul.want-points { margin-left: 20px; list-style: square; } div.sphinxsidebar ul ul { margin-top: 0; margin-bottom: 0; } div.sphinxsidebar a { color: #fff; } div.sphinxsidebar form { margin-top: 10px; } div.sphinxsidebar input { border: 1px solid #9bbde2; font-family: 'Verdana', 'Helvetica', 'Arial', sans-serif; font-size: 1em; } /* :::: MODULE CLOUD :::: */ div.modulecloud { margin: -5px 10px 5px 10px; padding: 10px; line-height: 160%; border: 1px solid #666666; background-color: #dddddd; } div.modulecloud a { padding: 0 5px 0 5px; } /* :::: SEARCH :::: */ ul.search { margin: 10px 0 0 20px; padding: 0; } ul.search li { padding: 5px 0 5px 20px; background-image: url(file.png); background-repeat: no-repeat; background-position: 0 7px; } ul.search li a { font-weight: bold; } ul.search li div.context { color: #666; margin: 2px 0 0 30px; text-align: left; } ul.keywordmatches li.goodmatch a { font-weight: bold; } /* :::: COMMON FORM STYLES :::: */ div.actions { padding: 5px 10px 5px 10px; border-top: 1px solid #598ec0; border-bottom: 1px solid #598ec0; background-color: #9bbde2; } form dl { color: #333; } form dt { clear: both; float: left; min-width: 110px; margin-right: 10px; padding-top: 2px; } input#homepage { display: none; } div.error { margin: 5px 20px 0 0; padding: 5px; border: 1px solid #db7d46; font-weight: bold; } /* :::: INLINE COMMENTS :::: */ div.inlinecomments { position: absolute; right: 20px; } div.inlinecomments a.bubble { display: block; float: right; background-image: url(style/comment.png); background-repeat: no-repeat; width: 25px; height: 25px; text-align: center; padding-top: 3px; font-size: 0.9em; line-height: 14px; font-weight: bold; color: black; } div.inlinecomments a.bubble span { display: none; } div.inlinecomments a.emptybubble { background-image: url(style/nocomment.png); } div.inlinecomments a.bubble:hover { background-image: url(style/hovercomment.png); text-decoration: none; color: #598ec0; } div.inlinecomments div.comments { float: right; margin: 25px 5px 0 0; max-width: 50em; min-width: 30em; border: 1px solid #598ec0; background-color: #9bbde2; z-index: 150; } div#comments { border: 1px solid #598ec0; margin-top: 20px; } div#comments div.nocomments { padding: 10px; font-weight: bold; } div.inlinecomments div.comments h3, div#comments h3 { margin: 0; padding: 0; background-color: #598ec0; color: white; border: none; padding: 3px; } div.inlinecomments div.comments div.actions { padding: 4px; margin: 0; border-top: none; } div#comments div.comment { margin: 10px; border: 1px solid #598ec0; } div.inlinecomments div.comment h4, div.commentwindow div.comment h4, div#comments div.comment h4 { margin: 10px 0 0 0; background-color: #2eabb0; color: white; border: none; padding: 1px 4px 1px 4px; } div#comments div.comment h4 { margin: 0; } div#comments div.comment h4 a { color: #9bbde2; } div.inlinecomments div.comment div.text, div.commentwindow div.comment div.text, div#comments div.comment div.text { margin: -5px 0 -5px 0; padding: 0 10px 0 10px; } div.inlinecomments div.comment div.meta, div.commentwindow div.comment div.meta, div#comments div.comment div.meta { text-align: right; padding: 2px 10px 2px 0; font-size: 95%; color: #598ec0; border-top: 1px solid #598ec0; background-color: #9bbde2; } div.commentwindow { position: absolute; width: 500px; border: 1px solid #598ec0; background-color: #9bbde2; display: none; z-index: 130; } div.commentwindow h3 { margin: 0; background-color: #598ec0; color: white; border: none; padding: 5px; font-size: 1.5em; cursor: pointer; } div.commentwindow div.actions { margin: 10px -10px 0 -10px; padding: 4px 10px 4px 10px; color: #598ec0; } div.commentwindow div.actions input { border: 1px solid #598ec0; background-color: white; color: #073d61; cursor: pointer; } div.commentwindow div.form { padding: 0 10px 0 10px; } div.commentwindow div.form input, div.commentwindow div.form textarea { border: 1px solid #598ec0; background-color: white; color: black; } div.commentwindow div.error { margin: 10px 5px 10px 5px; background-color: #fff2b0; display: none; } div.commentwindow div.form textarea { width: 99%; } div.commentwindow div.preview { margin: 10px 0 10px 0; background-color: ##9bbde2; padding: 0 1px 1px 25px; } div.commentwindow div.preview h4 { margin: 0 0 -5px -20px; padding: 4px 0 0 4px; color: white; font-size: 1.3em; } div.commentwindow div.preview div.comment { background-color: #f2fbfd; } div.commentwindow div.preview div.comment h4 { margin: 10px 0 0 0!important; padding: 1px 4px 1px 4px!important; font-size: 1.2em; } /* :::: SUGGEST CHANGES :::: */ div#suggest-changes-box input, div#suggest-changes-box textarea { border: 1px solid #666; background-color: white; color: black; } div#suggest-changes-box textarea { width: 99%; height: 400px; } /* :::: PREVIEW :::: */ div.preview { background-image: url(style/preview.png); padding: 0 20px 20px 20px; margin-bottom: 30px; } /* :::: INDEX PAGE :::: */ table.contentstable { width: 90%; } table.contentstable p.biglink { line-height: 150%; } a.biglink { font-size: 1.3em; } span.linkdescr { font-style: italic; padding-top: 5px; font-size: 90%; } /* :::: INDEX STYLES :::: */ table.indextable td { text-align: left; vertical-align: top; } table.indextable dl, table.indextable dd { margin-top: 0; margin-bottom: 0; } table.indextable tr.pcap { height: 10px; } table.indextable tr.cap { margin-top: 10px; background-color: #dddddd; } img.toggler { margin-right: 3px; margin-top: 3px; cursor: pointer; } form.pfform { margin: 10px 0 20px 0; } /* :::: GLOBAL STYLES :::: */ .docwarning { background-color: #fff2b0; padding: 10px; margin: 0 -20px 0 -20px; border-bottom: 1px solid #db7d46; } p.subhead { font-weight: bold; margin-top: 20px; } a { color: #24326e; text-decoration: none; } a:hover { text-decoration: underline; } div.body h1, div.body h2, div.body h3, div.body h4, div.body h5, div.body h6 { font-family: 'Verdana', 'Helvetica', 'Arial', sans-serif; background-color: #dddddd; font-weight: normal; color: #073d61; border-bottom: 1px solid #666; margin: 20px -20px 10px -20px; padding: 3px 0 3px 10px; } div.body h1 { margin-top: 0; font-size: 200%; } div.body h2 { font-size: 160%; } div.body h3 { font-size: 140%; } div.body h4 { font-size: 120%; } div.body h5 { font-size: 110%; } div.body h6 { font-size: 100%; } a.headerlink { color: #edaa1e; font-size: 0.8em; padding: 0 4px 0 4px; text-decoration: none; visibility: hidden; } h1:hover > a.headerlink, h2:hover > a.headerlink, h3:hover > a.headerlink, h4:hover > a.headerlink, h5:hover > a.headerlink, h6:hover > a.headerlink, dt:hover > a.headerlink { visibility: visible; } a.headerlink:hover { background-color: #edaa1e; color: white; } div.body p, div.body dd, div.body li { text-align: left; line-height: 130%; } div.body p.caption { text-align: inherit; } div.body td { text-align: left; } ul.fakelist { list-style: none; margin: 10px 0 10px 20px; padding: 0; } .field-list ul { padding-left: 1em; } .first { margin-top: 0 !important; } /* "Footnotes" heading */ p.rubric { margin-top: 30px; font-weight: bold; } /* "Topics" */ div.topic { background-color: #ddd; border: 1px solid #666; padding: 0 7px 0 7px; margin: 10px 0 10px 0; } p.topic-title { font-size: 1.1em; font-weight: bold; margin-top: 10px; } /* Admonitions */ div.admonition { margin-top: 10px; margin-bottom: 10px; padding: 7px; } div.admonition dt { font-weight: bold; } div.admonition dl { margin-bottom: 0; } div.admonition p { display: inline; } div.seealso { background-color: #fff2b0; border: 1px solid #edaa1e; } div.warning { background-color: #fff2b0; border: 1px solid ##db7d46; } div.note { background-color: #eee; border: 1px solid #666; } p.admonition-title { margin: 0px 10px 5px 0px; font-weight: bold; display: inline; } p.admonition-title:after { content: ":"; } div.body p.centered { text-align: center; margin-top: 25px; } table.docutils { border: 0; } table.docutils td, table.docutils th { padding: 1px 8px 1px 0; border-top: 0; border-left: 0; border-right: 0; border-bottom: 1px solid #a9a6a2; } table.field-list td, table.field-list th { border: 0 !important; } table.footnote td, table.footnote th { border: 0 !important; } .field-list ul { margin: 0; padding-left: 1em; } .field-list p { margin: 0; } dl { margin-bottom: 15px; clear: both; } dd p { margin-top: 0px; } dd ul, dd table { margin-bottom: 10px; } dd { margin-top: 3px; margin-bottom: 10px; margin-left: 30px; } .refcount { color: #24326e; } dt:target, .highlight { background-color: #edaa1e1; } dl.glossary dt { font-weight: bold; font-size: 1.1em; } th { text-align: left; padding-right: 5px; } pre { padding: 5px; background-color: #e6f3ff; color: #333; border: 1px solid #24326e; border-left: none; border-right: none; overflow: auto; } td.linenos pre { padding: 5px 0px; border: 0; background-color: transparent; color: #aaa; } table.highlighttable { margin-left: 0.5em; } table.highlighttable td { padding: 0 0.5em 0 0.5em; } tt { background-color: #ddd; padding: 0 1px 0 1px; font-size: 1.2em; } tt.descname { background-color: transparent; font-weight: bold; font-size: 1.2em; } tt.descclassname { background-color: transparent; } tt.xref, a tt { background-color: transparent; font-weight: bold; } .footnote:target { background-color: #fff2b0 } h1 tt, h2 tt, h3 tt, h4 tt, h5 tt, h6 tt { background-color: transparent; } .optional { font-size: 1.3em; } .versionmodified { font-style: italic; } form.comment { margin: 0; padding: 10px 30px 10px 30px; background-color: #ddd; } form.comment h3 { background-color: #598ec0; color: white; margin: -10px -30px 10px -30px; padding: 5px; font-size: 1.4em; } form.comment input, form.comment textarea { border: 1px solid #ddd; padding: 2px; font-family: 'Verdana', 'Helvetica', 'Arial', sans-serif; font-size: 100%; } form.comment input[type="text"] { width: 240px; } form.comment textarea { width: 100%; height: 200px; margin-bottom: 10px; } .system-message { background-color: #edaa1e; padding: 5px; border: 3px solid red; } /* :::: PRINT :::: */ @media print { div.document, div.documentwrapper, div.bodywrapper { margin: 0; width : 100%; } div.sphinxsidebar, div.related, div.footer, div#comments div.new-comment-box, #top-link { display: none; } } ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/docs/source/_static/e-logo-rev.jpg0000644000175100001730000003121200000000000022520 0ustar00runnerdocker00000000000000JFIFddDuckyd&Adobed $>2  014@25P36 !1aAQq"2 0BRr#bdt@3c$4PCsDT%u  1Q"0!AR#2@PaqBs!1AQa q0𑡱@P W~\*=mlIkګ"Pesֽ#- Pqio8F$:}yv5bwl6e;猏߈Ydבvo -Mig wsR|,+ϟ߀'=adڦ5Sw8+ʨ,fpi;]dt . ^gvx*h427zؓpg 56RՃ}i>`{Z3ުfic/EI']vQb96R<~}KyԻa[I1[c?A.fes`it45mpp6R<C,;n1Mwmk81:ǬO9C}81:ǬO9 On~6k l;1Ӌ5;|Ձ:ҹwb5Lئq0xZ %)=mt}a@vS`.# /h ~2#jؔPplZU(d'jHf7Fާu}~ZBo[oY1&19_ S'\b\7Iz=]}XVAW.־ؐ^@(D&`ި?\ 'Vfp껺?DW.k{|iѹ}qC˥_jk_@ӪH㕛O+jkJ2TNJkZ,ʯe핧Ġٓ:ۑvO&Mק i]&{Kh)2{p94--E3O|Wؙ\꽬C8v=bN^mj 0JqᆗbGJoۗ{U )N\lV ǀHx_j5.^[!knn|}Gii5:ͣw:W~MmH./jh}ggsoۛ5VrHw~M _ ss7n[߷-+b^^q+oPX4tbn$|!W['N5*VtKk]UպgǢx!&;]5S%X$ ZfVe%AE W8lUmeu6¬+8TՕ 0Pq6jRuOc)bȸK˻}|3Zb1mڽaaI$}eK?^Icl* Ao9;ڊjQh`, gh_>yszGM vƹN Ʋ$RD99<.^C(WY`ީbl#@ 5 'IgN$|^&vbkZ,@ö;HA 1oj𪈇_T忐9A.ohc"3cEjSO[z [D(bߓNo<#G>ñ΂H ~ʪ5"'"b_TT^`-&!^8P"dO aQ>,jF/ $%XIX"1I0,<*8`mۛZgDDC VX_#8Yɷ IWd1,+dQJ 8`ےJ"LpTËM$|OQ ;绘w@Z7n ,OcDUYɖCZgmSU9a JNvOuBD,7CפحV 8*Q{MD$g 6X"9@i!v1t8Q*b{$NXlIA`c 6YqUU~e4Ґ??$a#w6BFݠӝRo斎zEu.krSF0$'7p ɞk C؝ra0QhF(bIQJm^64I/I=wĚJhFm;tEMt#ӝ1D,CnXjm )+M ۑSvMΤC(zYӑ 9@e;a(^hC1ƍe41'- 7;}ReNjbOQQaҹ\Z'G:ӻ;;S˳_ZH$$'w~eNj8Uz+g |aHJ>wg1]Vgwg1'%-]G6 (Xg38g 2Sawn-=EJbt!yY(}su )_ x M D#$SզmƄ+-h[P G2[_(>YlIhVhG;'iB\m|s54R4/H:_1ϴ I<aIR2\Y>Ğ嵰$%9?To>MENG''o:rB]u{uGOU>esֳ"wYMƦ5pl9)Vn y7lB%hKawͫbDCaCrBw˯օZ3ovnP7X7ꛖjPM(uEuK5[r+#khrNEFu4s/(G0 J=;K%TyE^- B I)7VW%ٯA!./h_ o&.Xh;WztPnK_y= Lt߮ĜJ=ng7 4}rJt&13yҋs.}YA%!)??FT"u{(m;2]\~t3ЅZCE#V)noRn}&\kML 8ć B =IܴSfm "։//+h}lE,3fOؓugÐ֡籱'(9?Jy}YZ t.a4(;5K޶MYw\4ǀͤyupNՍkML}H*1FnPt[ŝec +'!Tf!UTfc*Nv6^pa V*+Wže4NՉS|ǰ#34#ꈣnO7}~{K#ꆒwY>NWǵ\VXJGKMтmX!|W_wuSyI)Q$WEhelAI.|[ $" na9cnge>+KLnv5mD9_Vub$0 Ei$4 VABj8pWѧch__85y:GW72q]X?U_i_1 )Ƌj +N=̪d.=L}VN.UJZEt~=2x8'=EMt=Sl vI:cu6(ӏ41F%6 ^i%Zd[bw>&kZ yUBЩ[c(y#\Nڼq. &oV8*}fż-޳ J ^yFV8b{LytʱcNmi?]JF[fkve%w29'yEq#vcM@;4ʶ*®SktXb^% 0(UyI$$ 2d(eOӧͮ>Uëazn@z>[vc-&-USk׻G;,'vPf߫V],9]9nrWU[Hʋ8("Й*zǥtIa%A0xyԕ#mbqcI) oG{xL2Tq޺F6Tkf2'ǞUog[v="vz!#cAt ;C-O(Y; +śVXq{ZJo{^q] N Ruu9:ѥE;z툝ڴџUAa%O2u޶4kcAk n3 Imfn_|plXxQ`vF;|jW ~Ϩw)uwRQeQ 6"Im tPwz4S gJzɱѼR!E*'sSyPjON,pM#9OkVoޕ>ѨnØydr5k I4,qUmhskH{ƝcJy^Qţ-Orԝr_ >cGr7A)NcVQ^̝)7^gC0IW4~usF涥PN^?tFrqS y8!Z!FƃrO :IS,s5JWqdbS}\iQ[jGtwPa7o$nȇtbs cE4S2l݌jo]{V&d8cʤsySV<^]+[W[sq.!`C9Qğem*;>g$؍8Sq3vsHE1;l$տ@C)89>kF݆P2C$e%?ʇMxNa2%GLwGWye,I#1IAV\Z/~#&wҭVxG 8ێ{kɮo&lB/ #1g?!Fa-;/+Edj266nBA{ʩ戠&>f9NGB;"_(f\VrEVhdZ>!<unvQg_/H /3#YbZl؁PEEQol"IJn˴vJ"iUz*G|8hx4:HLBZiFbiǕFόvmUxh KREu2/\Y,erػG] ZU`lJ3F! *nak5vᐒNQ# IAQiWmR&rlky,B(Jx5z27cM}W*W33C)4(7SVƁXJjZCDvc.Gq웭cjLg 2bi& \Pb*CT]ӘS2f&\ŅL8lFZW ۽Y eAE Bs|;:|J-/'h5``<8/!ʜ#bdzW z@ܚ#qmq:X}"e8' 'ZE;.SW.r ;:I"qb8ڡH*!a ,!m_?o1[v{蜄cm Ay. /ք~zcjPk: lĐ,KAK;U1t $h*CVeA6a?<#ej(.ıZ`Y'dX1[H ae'3XS7*_3Q ~۫чl{{.N1ձ M 5,I&R)J8̈́DP CXp 䃐*mD`fMſD~RI0 OF)9K2V'[t5FjTc4_ϊcJ4' x@䠝9D'W[Ԁd&rK6B1XO!a@ EȷH&pj>"Z$ jfX1:ȞAFZt휔=TT',.?!ݰΪ?J@6KYJZ;}O pT[%Ih`Oػq]<ǐ~|qO0S5 yߚrw2ySH91S_VY=@ܘWP)ϻB)}ٟ؆߽ʀ&}' H}|ߥ.%gx˿ PUNǮHqR;$ }NG*r+'oߨSN~v>EAZuv6niݩl[6-acB@:ӟBɈ(cwK|hejTߥBvw>s P*TO|xcG$CӟOϫV1U᧠Fϧ؝><3sEb?F/F\_ q]k:;~xf6ǫu+`Rñ%j|?C]BD[5*ܬ;kK'c _cE4~bJƽ\\B?٨}_y׳j$1 ~+?mԅC/ t_[b|2ߡURW=N+&qN[4?{bZ ,z%KD?Ow_Z͆cqX=>w UP wB'{z!U/M+8;V,9Q O#{jPyQk;x(qk$j\VOǼYizVOǸjnQ2AcGD!3&%fY^_b^4O?ʒ=v}˳_w^pƸH;*{?.!+nHIXwYo;m]ߧB<HH|_n#U{?E'g rt_S/? nZA9mbjZ4cH 5Hh̵wbg=w|2k_X _$ce{T;i={rcx˨?/j $I$I$I$I%I$I$ORI$I$JbI$I7ILI$H-I $JYZW -I$ēJnI'$8I?N%1$I"HI$OoI$HI$l$DI$$I4I$o .I$TCI$HI$Q.$I$I$I$`$I$I$I$I$I$I$I??`B`2?u!`3"BqVq;̦aFsZC\97,IV*le&t6'n` Aeqs* UlV8\HHVݟ E3T@Q!f7K$"m 0ix20'+C:eJus!y51=\\l X_ЗR%yEHB-,h g1(0DQDvFIY bxf$*4+D/PZN` l_,([t }ZWRBfU,6jrS-cP4庌"%.@hc54Ds-lI=2F@$P`-)t6dd#qIY'ر tVREQ \Њ A0SKh?~QD.k`I7F;nA29JUBAK]D>|ZFiDA 8A3!bĦk^H (a"ǫAz1+V1pXeJmM&@UgAN>̏q#i2M`5_+ -K.N%J7T$Z$7^LPf%*(xƹ!|8C2^`C %*XcZ3Du:JUzaD Zjta 8Չd; ^ڈ4s"zytХgN/5ef]e Tr̴xUƣ\82Y83V_Eu?sƩ+A{Y8I˨5erbȫE7078[][j7FhfEt8Ĺe~XyU- 7-b{9z.e塠-֕D@DePchSVewg'E/N''v&(S3MʻTe6 WS{aC&pu UEG{7s(0cXyѯIj"5lY#pQٳ~ A8 ?Ĭ8-wN̨R.`"7Ь !r)ze&n5'=<#d[3HFC )1hfmX/V$K2EC)Fߊ'',mGpBbIm$#lx ;ixLk}^ABɤ`2;l}}CD`;yoՃg}L( !ɍ~n.l!k3yBoE5Uou JU f?˽춰0wϦOP*r%"g+KOd{b,J@\fn޳>=@#؊2s>JY`b vF!owrGc2.ɖb(S4g}2th ,pxre'Fqjlj'_wzd1vQp(\ N?w n]Sdw'H6Sj2wzb$`vNJb) ,#.YB^}8;ӄ"B_gQ,Pe+N+Pc.=C "vo qrC/A@N%ٷ\ZSxP+XO.ӆJ&[pPC)G4(&&ʁzHv/1{Ue/Sm&!]Gw]Vнe:<0I7wa䇚OefA@rVi5wMX\:NfT;!0'< Tk™#ɹY=R0HIC[+p/@PE& UeϿ)Ȼ7heƞ0ёa#_'DR aY &NaxA(.%Df$>GE1-?G ܻn 5J߯,Vpq*CnLe,97J X%WW@S}#bχ5>בwyNʆhRieqm3\ 8jXqYOW$IV3";g9cl~r Ԓ-)]06"l->Q $"qwZT IɥJ(.nN* HXuxpȽkCW yAu6-m &W裰o@Ϲ]D[W,UӌM6IV1-5yw4"aqz:&"Lz̏nCLH}PkZ*^+ұ@^1%*J(4e{eDѶ}$T; )?ZU1ra>oF-`]IENDB`././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1684768028.0118074 traitsui-8.0.0/docs/source/_templates/0000755000175100001730000000000000000000000020552 5ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/docs/source/_templates/layout.html0000644000175100001730000000036300000000000022757 0ustar00runnerdocker00000000000000{# Filename: _templates/layout.html #} {% extends '!layout.html' %} {% block relbaritems %} {% if current_page_name != 'index' %}
  • {{ title }}
  • {% endif %} {% endblock %} ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/docs/source/_templates/search.html0000644000175100001730000000174500000000000022714 0ustar00runnerdocker00000000000000{% extends "!search.html" %} {% block body %}

    Search

    Enter your search words into the box below and click search. Note that the search function automatically searches for all of the words. Pages containing some but not all of them won't appear in the result list.

    {% if search_performed %}

    Search Results

    {% if not search_results %}

    Your search did not match any results.

    {% endif %} {% endif %}
    {% if search_results %}
      {% for href, caption, context in search_results %}
    • {{ caption }}
      {{ context|e }}
    • {% endfor %}
    {% endif %}
    {% endblock %} ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1684768028.0118074 traitsui-8.0.0/docs/source/api/0000755000175100001730000000000000000000000017166 5ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/docs/source/api/index.rst0000644000175100001730000000050600000000000021030 0ustar00runnerdocker00000000000000.. _traitsui-api-reference: TraitsUI |version| API Reference ================================ This document contains the auto-generated API reference documentation for TraitsUI. For user documentation, please read the :doc:`TraitsUI User Manual <../traitsui_user_manual/index>` .. toctree:: :maxdepth: 3 traitsui ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/docs/source/changelog.rst0000644000175100001730000000004000000000000021070 0ustar00runnerdocker00000000000000 .. include:: ../../CHANGES.txt ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/docs/source/conf.py0000644000175100001730000001662100000000000017722 0ustar00runnerdocker00000000000000# -*- coding: utf-8 -*- # # Traits documentation build configuration file, created by # sphinx-quickstart on Tue Jul 22 10:52:03 2008. # # This file is execfile()d with the current directory set to its containing dir. # # The contents of this file are pickled, so don't put values in the namespace # that aren't pickleable (module imports are okay, they're removed automatically). # # All configuration values have a default value; values that are commented out # serve to show the default value. import os from traitsui import __version__ as version # If your extensions are in another directory, add it here. If the directory # is relative to the documentation root, use os.path.abspath to make it # absolute, like shown here. # sys.path.append(os.path.abspath('some/directory')) # General configuration # --------------------- # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = [ 'sphinx.ext.autodoc', 'sphinx.ext.extlinks', 'sphinx.ext.intersphinx', 'sphinx.ext.napoleon', # Link to code in sphinx generated API docs "sphinx.ext.viewcode", "sphinx_copybutton", 'traits.util.trait_documenter', ] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix of source filenames. source_suffix = '.rst' # The master toctree document. master_doc = 'index' # General substitutions. project = 'traitsui' copyright = '2008-2023, Enthought' # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: # today = '' # Else, today_fmt is used as the format for a strftime call. today_fmt = '%B %d, %Y' # List of documents that shouldn't be included in the build. # unused_docs = [] # List of directories, relative to source directories, that shouldn't be searched # for source files. # exclude_dirs = [] # The reST default role (used for this markup: `text`) to use for all documents. # default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. # add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). # add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. # show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # Options for Sphinx copybutton extension # --------------------------------------- # Matches prompts - "$ ", ">>>" and "..." copybutton_prompt_text = r">>> |\.\.\. |\$ " copybutton_prompt_is_regexp = True # Options for HTML output # ----------------------- # The style sheet to use for HTML and HTML Help pages. A file of that name # must exist either in Sphinx' static/ path, or in one of the custom paths # given in html_static_path. # html_style = 'default.css' # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". html_title = "TraitsUI {} Documentation".format(version.split('.')[0]) # A shorter title for the navigation bar. Default is the same as html_title. # html_short_title = None # The name of an image file (within the static path) to place at the top of # the sidebar. # html_logo = "e-logo-rev.png" # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. # html_favicon = "favicon.ico" # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static', os.path.join('tutorials', 'code_snippets')] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. html_use_smartypants = True # Custom sidebar templates, maps document names to template names. # html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. # html_additional_pages = {} # If false, no module index is generated. html_use_modindex = False # If false, no index is generated. # html_use_index = False # If true, the index is split into individual pages for each letter. # html_split_index = False # If true, the reST sources are included in the HTML build as _sources/. # html_copy_source = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. # html_use_opensearch = '' # If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml"). # html_file_suffix = '' # Output file base name for HTML help builder. htmlhelp_basename = 'Traitsuidoc' # html theme information try: import enthought_sphinx_theme html_theme_path = [enthought_sphinx_theme.theme_path] html_theme = 'enthought' except ImportError as exc: import warnings msg = '''Can't find Enthought Sphinx Theme, using default. Exception was: {} Enthought Sphinx Theme can be downloaded from https://github.com/enthought/enthought-sphinx-theme''' warnings.warn(RuntimeWarning(msg.format(exc))) # old defaults html_logo = "e-logo-rev.jpg" html_favicon = "et.png" html_style = 'default.css' html_theme = 'classic' # #html_theme = 'bizstyle' # html_logo = "e-logo-rev.png" # #html_style = 'default.css' # #html_theme = 'classic' # Useful aliases to avoid repeating long URLs. extlinks = { 'github-demo': ( f'https://github.com/enthought/traitsui/tree/{version}/traitsui/examples/demo/%s', 'github-demo', ) } # Options for LaTeX output # ------------------------ # The paper size ('letter' or 'a4'). # latex_paper_size = 'letter' # The font size ('10pt', '11pt' or '12pt'). # latex_font_size = '10pt' # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, document class [howto/manual]). latex_documents = [ ( 'index', 'TraitsUI.tex', 'TraitsUI 4 User Manual', 'Enthought, Inc.', 'manual', ), ] # The name of an image file (relative to this directory) to place at the top of # the title page. # latex_logo = "enthought_logo.jpg" latex_logo = "e-logo-rev.png" # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. # latex_use_parts = False # Additional stuff for the LaTeX preamble. # latex_preamble = '' # Documents to append as an appendix to all manuals. # latex_appendices = [] # If false, no module index is generated. # latex_use_modindex = True # Autodoc options # Intersphinx configuration intersphinx_mapping = { 'traits': ('http://docs.enthought.com/traits', None), 'pyface': ('http://docs.enthought.com/pyface', None), } def autodoc_skip_member(app, what, name, obj, skip, options): # Skip load_tests targeting unittest discover is_load_tests = what == "module" and name == "load_tests" return skip or is_load_tests def setup(app): app.connect('autodoc-skip-member', autodoc_skip_member) ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1684768028.0118074 traitsui-8.0.0/docs/source/demos/0000755000175100001730000000000000000000000017524 5ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/docs/source/demos/index.rst0000644000175100001730000001051700000000000021371 0ustar00runnerdocker00000000000000.. _traitsui-demo: TraitsUI |version| Demos ========================== .. toctree:: :maxdepth: 3 This section contains links to a number of TraitsUI demos. .. warning:: Some of the examples in this section may be out of date. We are in the process of updating them to the latest version of TraitsUI. Standard Editors ------------------ - :github-demo:`BooleanEditor ` - :github-demo:`ButtonEditor ` - :github-demo:`CSVListEditor ` - :github-demo:`CheckListEditor ` - :github-demo:`CheckListEditor (simple) ` - :github-demo:`CodeEditor ` - :github-demo:`ColorEditor ` - :github-demo:`CompoundEditor ` - :github-demo:`DirectoryEditor ` - :github-demo:`EnumEditor ` - :github-demo:`FileEditor ` - :github-demo:`FontEditor ` - :github-demo:`HTMLEditor ` - :github-demo:`ImageEnumEditor ` - :github-demo:`InstanceEditor ` - :github-demo:`ListEditor ` - :github-demo:`RGBColorEditor ` - :github-demo:`RangeEditor ` - :github-demo:`SetEditor ` - :github-demo:`TableEditor ` - :github-demo:`TextEditor ` - :github-demo:`TitleEditor ` - :github-demo:`TreeEditor ` - :github-demo:`TupleEditor ` - :github-demo:`VideoEditor ` Advanced Demos -------------- - :github-demo:`Adapted TreeEditor ` - :github-demo:`Apply/Revert Handler ` - :github-demo:`Table (read-only, auto-edit table column) ` - :github-demo:`TabularEditor (auto-update) ` - :github-demo:`DateEditor ` - :github-demo:`Dynamic EnumEditor ` - :github-demo:`Dynamic Range Editor ` - :github-demo:`Dynamic Views ` - :github-demo:`HDF5 Tree ` - :github-demo:`History ` - :github-demo:`Invalid state handling ` - :github-demo:`ListStrEditor ` - :github-demo:`ListEditor ` - :github-demo:`ListEditor (notebook tabs) ` - :github-demo:`MVC ` - :github-demo:`Multi-selection string list ` - :github-demo:`Multithread ` - :github-demo:`Multithread 2 ` - :github-demo:`NumPy array TabularEditor ` - :github-demo:`NumPy ArrayViewEditor ` - :github-demo:`Popup Dialog ` - :github-demo:`Property List ` - :github-demo:`ScrubberEditor` - :github-demo:`Settable cached property ` - :github-demo:`Statusbar ` - :github-demo:`StringListEditor ` - :github-demo:`TableEditor (with checkbox column) ` - :github-demo:`TableEditor (with context menu) ` - :github-demo:`TableEditor (with live search and cell) ` - :github-demo:`TabularEditor ` - :github-demo:`TimeEditor ` ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/docs/source/e-logo-rev.png0000644000175100001730000000751100000000000021103 0ustar00runnerdocker00000000000000PNG  IHDRoi*sRGB pHYs  tIMELIDATx]ktT>z\$m ȥ!TD jV|_CWph PT\"B$$6aIf9qfB%= By}; vo`!4ChVv[12 !HGiyg):QVd8SmsQAj2~~AH3qU:?!4[a6SRu ǎ7H'1"_Qq!JbBwx$I-S^QO>AO~( HAPU)=Ojjm+ v$~ئ"33zviJn[*.\v(/E1U`Ycֿ&y3g>=x$;GS@]d1YÓo"۾X6n8o2 ,c_܊U?y" "cdL5HfFj~}Q]H錩/Oxcq'~lӕ_ ţeW\| &cLMhdȶ9-՗ $ Θڳ9i˗>xa6>#E _h2$}앿"a\l߰0/"ޑҦ.*:UQyٕ~`:oYfxu? b)<̜>җ'rYgԾ6ngeSMkm>uv" Snhj ̌ry_ݚLM01@$(]vƏ{_{#&>4l|c.8~rK05bjԈm;14*:Ο3yK|ީT\> 8nd٤B]j맻]8#&[5TEUlu#u\/kk^6t=Zo`Ӌ-,R'*EP1#EQ DfsnlOYYYҨ!${G2yZ~\pN|olӋnϯBu-\$5˘TYgNR^\8gF{@|4Ņ0ov2֊^:j)D"zM En1]WfN@wǛ뿨k B|c!>8T'JԉaZxubOW~;c%dLynظedNSt~WX\f-pO',9UI21`xĥd  ,{ER"Z G 4PLq@$#15! G}\.-2kEfV=G15Q&ph!9Ce Cvj(# 5#GX:InHJZmڞU__(h݆' H7cHκ})"Db-&`i\eU?*YJ05 D S[GabDěrqEʪ9կm"4LwtGTدr{OPۿhj?:}"i b:/7yA@eK#$t13mj51K &^w !%PSSSֆlr{s^#w4DmQI S#3a@57Q; S#:į v4yR+A&P0j/))-&Z4S.[Z2d^!j8J01-j(T!05Q)"jԌ+@vpd"'4LuyC͉cv,@A1i_qLq|s4bvGz!U !KIQD1E3[1vI $00h6FL̙dnu˞?SScw\LGaʃcf-N]y/4u: c c PM18_h>4~h޽f l%&N^>?2=iC)9v!˜j>hN'N~(aİ}Wx+' u0?1sL _/>_nH ! x9zq@bzlLؘO_6Ac6~t=F&מc2\汋rh3.婓Jx`x^_>_mqKkj+-++Y.zw3TU+qܹ~M\_:pBI" D5 JcTubd!P%+~fz*EP]6R2;/uz] g,'Nd=C^n188D,dZ}W/)~ǎ/z~*0P]g*ݐ[{s]b76 $?`[퍘JTDDKŽ t "((}qqwZΦO11fZ XSXk71E~;{GbN#"k" r@4˗mrN"srLڀ?Vh?݁nw'?0l۶`bF4]2UU ;llgL bkx'ۄ&%QU#c*B{awE|DǶBhZ-f/wIENDB`././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/docs/source/index.rst0000644000175100001730000000047600000000000020265 0ustar00runnerdocker00000000000000TraitsUI Documentation ====================== .. toctree:: :maxdepth: 2 :glob: traitsui_user_manual/index traitsui_dev_guide/index api/index tutorials/index demos/index changelog .. include:: ../../README.rst Indices and tables ================== * :ref:`genindex` * :ref:`search` ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1684768028.0118074 traitsui-8.0.0/docs/source/traitsui_dev_guide/0000755000175100001730000000000000000000000022274 5ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/docs/source/traitsui_dev_guide/index.rst0000644000175100001730000000102000000000000024126 0ustar00runnerdocker00000000000000 ================================== TraitsUI |version| Developer Guide ================================== This *internal* documentation describes how various features of TraitsUI are implemented, along with the reasoning, history and decision information. This is intended for current/future developers and maintainers of TraitsUI, those who want to dive into the code to troubleshoot issues and those who just want a better understanding of TraitsUI code base. Contents ======== .. toctree:: :maxdepth: 3 testing.rst ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/docs/source/traitsui_dev_guide/testing.rst0000644000175100001730000003121400000000000024504 0ustar00runnerdocker00000000000000 .. _testing-internals: ============================== Developing The Testing Package ============================== This document describes the context and design decisions surrounding the development of the |traitsui.testing| package. Readers are assumed to be familiar with the content of the :ref:`testing-traitsui-applications` Section. Background Motivation --------------------- There had been proposals for introducing a public API to make it easier and safer to write self-checking tests for TraitsUI applications. At the same time, we noticed that a lot of what TraitsUI needs for testing are common in downstream projects as well: - test needs to modify a trait and then checks the GUI state has changed. - test needs to modify the GUI state and then checks a trait has changed. - test needs to create a UI and makes sure it is disposed of properly at the end of the test. - test needs to ensure uncaught exceptions that occur in the GUI event loop cause the test to fail. The step that is the least trivial to get right or the most often-overlooked is the need to process pending events on the GUI event loop. Pyface (a dependency of TraitsUI) has provided a base class called GuiTestAssistant which helps running the GUI event loops in tests. However, test code manipulating a TraitsUI editor will still need to know which methods on the editor it should call and what to call it with. These methods don't have a common interface, and their signatures vary from toolkit to toolkit, editor to editor. Often one needs to read the source code in order to call them correctly. For example, in order to update the date trait edited by a DateEditor, in Qt, one needs to call the Qt DateEditor like this:: editor.update_object(QDate(...)) In Wx, one will need to create an wx event with the date value following wx API, and then call a method only wx DateEditor has:: event = wx.adv.DateEvent(...) event.SetDate(...) editor.day_selected(event) In addition to the lack of a common interface, this approach also has the following drawbacks: - The test code is toolkit specific. - Advanced technical knowledge on the low level toolkit control is required to write these tests. - The test provides less confidence as it bypasses GUI event hooking logic (aka signals and slots). - There is a strong coupling between the test code and TraitsUI 's internal implementation details. The last point is important for both TraitsUI and downstream projects. Strong coupling between the application code structure and test code means any small refactoring or internal changes could cause a lot of test code to fail. This would make changing TraitsUI more difficult. Downstream projects realizing the fragility of such tests may decide not to write the tests that would otherwise be written. In fact, TraitsUI 's test suite already makes use of a number of internal helper functions for testing its own UI editors. Therefore we noticed that there was an opportunity to wrap these helper functions and re-expose them in a simpler public API for testing. By implementing a testing API motivated and tested by TraitsUI 's own test code, the API is likely to be more useful and has fewer bugs. Goals ----- - The API should be simple, intuitive to use and to understand. - The API should be toolkit agnostic. - Test code using the API should not need to depend on implementation details of a UI editor. - Easy-to-forget logic such as processing GUI events should be provided by default. - Despite the rich and varying features supported by TraitsUI editors, the API should still be self-explanatory and self-documenting so that it is easy to find what is supported. - The testing helper can be used in collaboration with Pyface's ModalDialogTester and GuiTestAssistant. - Unexpected exceptions from the GUI event loop should cause a test to fail. - Functionalities provided should be immediately useful for TraitsUI's own tests. - The API should be extensible so that projects can add their own test logic and TraitsUI test code can optionally defer publishing less mature test logic to the public API. Non-Goals --------- The API does NOT aim to: - Replace Pyface's ModalDialogTester and GuiTestAssistant. - Reproduce the full visual experience compared to manual testing, as long as the test objective is fulfilled. System Context -------------- In a software system, tests are the most isolated system component, which are not necessary for production use. Since the testing library supports tests, it should only be used by tests and nothing else in a software system. The fact that tests and production use require different exception handling further justifies disallowing the testing library to be used in a production environment. API --- As mentioned in :ref:`testing-traitsui-applications`, there are broadly speaking three types of actions when one manipulates a GUI in test: - Locate a GUI target. - Perform an action that mutates GUI state like a user would. - Inspect a GUI state like a user would. (Terminologies are defined in :ref:`testing-target-interaction-location`). Each of these three types of actions should have a public method in the API. Technically speaking, the behavior of "inspect" is very similar to "perform", they only differ in their intent, and therefore whether they should have a returned value. They are separate so that test code using these methods will read more naturally, and communicate intent more clearly. Likewise, "locate" is similar to "inspect"; both are making queries about GUI states. However, "locate" is likely used in conjunction with "perform" and "inspect", whereas "perform" and "inspect" could also occur as a standalone command. In summary, test code should read something like the following:: perform(...) inspect(...) locate(...).perform(...) locate(...).inspect(...) locate(...).locate(...).locate(...).inspect(...) Each of these functions provides the natural place for GUI event processing to occur automatically so that users do not have to worry about that any more. Different types of interactions and locations should be supported based on the GUI target type. For example, if the current GUI target being handled is a button, then "perform" should support an interaction type "MouseClick". Public API objects ------------------ All objects exposed in the ``traitsui.testing.api`` are part of the public API. Objects accessible via publicly named attributes through this API are also part of the public API. The less obvious part of the public API are the supported interactions and locations exposed via the registry pattern. Separating |UITester| from |UIWrapper| -------------------------------------- |UITester| is designed to be a top-level object to provide the first point of use for developers testing a TraitsUI application. It puts together two other types of objects: - |UIWrapper| - |TargetRegistry| |UITester| is specific to TraitsUI, whereas |UIWrapper| and |TargetRegistry| are more generic and can be used for testing any types of objects. |TargetRegistry| collects the information required for resolving an interaction and/or a location for a given GUI target. |UIWrapper| depends on one or many registries. If the |TargetRegistry| is empty, the |UIWrapper| would not be very useful at all. The |UITester| supports testing of TraitsUI objects by providing an instance of |TargetRegistry| that knows how to locate, inspect and perform actions on TraitsUI objects. This abstraction covers the existing use cases and it also allows TraitsUI to extend the API in its internal GUI tests without necessarily exposing the logic to the public. Likewise, external projects can extend testing support for their custom UI editors. Registry VS Inheritance: Registry wins -------------------------------------- The types of locations and interactions that should be supported depends on the type of TraitsUI editor being used. e.g. A TextEditor that only wraps a text box does not need to support further location logic, and it should support modifying the text via key events. However a CheckListEditor will require support for locating an item in the list, but not support for modifying the item by key events. Since there are more than one axis of variables, class inheritance is not enough to achieve polymorphism. If we had used subclasses, we will end up with an API where there are a lot of optional methods that are not implemented by a subclass, making it difficult for users to find what can be used. At the end, the registry pattern wins, and that becomes |TargetRegistry|. Rejected design: Default location --------------------------------- Early in the development there was a proposal to support a special location type called the "DefaultTarget". The idea was that if an interaction is not supported by a given GUI target, the "default target" is tried. This was motivated by the use case of testing simple UI editors such as TextEditor where there is one obvious toolkit widget to interact with. With this design, one would register interaction handlers with the low level toolkit specific object as the target type, allowing those handlers to be reused in other contexts. The pseudo-code for this patterns looked like this:: def perform(interaction, target): try: _perform(interaction, target): except InteractionNotSupported: try: default_target = locate(DefaultTarget, target) except LocationNotSupported: raise InteractionNotSupported else: _perform(interaction, default_target) The default target for a TextEditor can be resolved using this function:: def resolve_default_target(target: TextEditor): return target.control # would be a QLineEdit for simple editor in Qt. And one would have registered an interaction (e.g. KeySequence) on a low level toolkit widget type:: registry.register_interaction( target_type=QLineEdit, interaction_type=KeySequence, handler=..., # some callable ) The design was rejected for two reasons: #. The control flow resulting from trying to resolve an interaction or location on the default target adds complexity to the code. #. The resulting code is obscure because it is hard to see which UI editors are using the registered interaction handlers. This makes it difficult to perform impact analysis when one needs to change the code. At the end we simply register the interaction types on those simple UI editors directly but we refactor the registration logic so that it is easy to reuse. Where are the tests? -------------------- When a functionality is added to support testing TraitsUI editors, the functionality should be used in TraitsUI 's own tests for the editor. e.g. The MouseClick support for ButtonEditor is tested in the tests for ButtonEditor, in ``traitsui.editors.tests.test_button_editor``. By dog-fooding the implementations back to TraitsUI 's own tests with the goal of writing toolkit independent test code, this allows us to capture inconsistencies among different toolkits early and to make sure the new functionality is indeed useful. Tests for functionality provided by the tester API can be found in the ``testing`` package (or subpackages). e.g. API of |UITester| is tested in ``traitsui.testing.tester.tests``. Package structures ------------------ The ``traitsui.testing`` package adopts a naming convention that is in fact not new: Preceding underscores in names are used to indicate an object is intended to be used within the namespace it is defined in. If a module has a name with a preceding underscore, this means it is intended to be used by objects that live in the same package (including subpackages), but not objects that are defined in a package outside of that package. If a module is intended to be used by objects defined outside of the package, the module should have a "public" name. For example, ``traitsui.testing.tester._ui_tester.default_registry`` can be imported by ``traitsui.testing.tester.something`` but should not be imported by ``traitsui.testing.api`` or anything outside of ``traitsui.testing.tester``. .. # substitutions .. |Editor| replace:: :class:`~traitsui.editor.Editor` .. |UI| replace:: :class:`~traitsui.ui.UI` .. |traitsui.testing| replace:: :mod:`~traitsui.testing` .. |UITester| replace:: :class:`~traitsui.testing.tester.ui_tester.UITester` .. |UIWrapper| replace:: :class:`~traitsui.testing.tester.ui_wrapper.UIWrapper` .. |UIWrapper.inspect| replace:: :func:`~traitsui.testing.tester.ui_wrapper.UIWrapper.inspect` .. |UIWrapper.locate| replace:: :func:`~traitsui.testing.tester.ui_wrapper.UIWrapper.locate` .. |UIWrapper.perform| replace:: :func:`~traitsui.testing.tester.ui_wrapper.UIWrapper.perform` .. |TargetRegistry| replace:: :class:`~traitsui.testing.tester.target_registry.TargetRegistry` ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1684768028.0118074 traitsui-8.0.0/docs/source/traitsui_user_manual/0000755000175100001730000000000000000000000022654 5ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/docs/source/traitsui_user_manual/adapters.rst0000644000175100001730000011650500000000000025221 0ustar00runnerdocker00000000000000 .. _advanced-editor-adapters: ======================== Advanced Editor Adapters ======================== A number of trait editors provide a way for code to adapt objects to the expected API for the editor, and this can be used by Traits UI code to provide strongly customized views of the data. The editors which provide this facility are the ListStrEditor, the TabularEditor and the TreeEditor. In this section we will look more closely at each of these and discuss how they can be customized as needed. .. _advanced-tree-node: The TreeEditor and TreeNodes ============================ .. _tree-nodes: .. module:: traitsui.tree_node :noindex: The :py:class:`~traitsui.editors.tree_editor.TreeEditor` internally associates with each node in the tree a pair consisting of the object that is associated with the node and something that to adheres to the :py:class:`TreeNode` interface. The :py:class:`TreeNode` interface is not explicitly laid out, but it corresponds to the "overridable" public methods of the :py:class:`TreeNode` class, such as :py:meth:`~TreeNode.get_label` and :py:meth:`~TreeNode.get_children`. This means that the tree editor expects one of the following three things to be offered as values associated with a node, such as the value of the root node trait or values that might be returned by the :py:meth:`~TreeNode.get_children` method: - an explicit pair of the object and a :py:class:`TreeNode` instance for that object - an object that has :py:meth:`~TreeNode.is_node_for` return ``True`` for at least one of the factory's :py:attr:`~traitsui.editors.tree_editor.TreeEditor.nodes` items. - an object that provides or can be adapted to the :py:class:`ITreeNode` interface using Traits adaptation. There is a crucial distinction between the way that :py:class:`TreeNode` and :py:class:`ITreeNode` work. :py:class:`TreeNode` is generic---it is designed to work with certain types of objects, but doesn't hold references to those objects---instead they rely on the :py:class:`TreeEditor` to keep track of the association between the objects and the :py:class:`TreeNode` to use with that object. :py:class:`ITreeNode`, on the other hand, is an interface and uses adapters associated with individual objects rather than types of objects. This means that :py:class:`ITreeNode`-based approaches are generally more heavyweight: you end up with at least one additional class instance for each displayed node (and most likely two additional instances) vs. a tuple. On the other hand, because :py:class:`ITreeNode` uses Traits adaptation, you can extend the set of classes that are supported by adding more :py:class:`ITreeNode` adapters, for example via Envisage extension points. Specializing TreeNode Behaviour ------------------------------- In general using :py:class:`TreeNode` s works well when you have a hierarchy of :py:class:`~traits.api.HasTraits` objects, which is probably the most common situation. And while the :py:class:`TreeNode` is fairly generic, there are times when you want to override the default behaviour of one or more aspects of the object. In this case it may be that the best way to do this is to simply subclass :py:class:`TreeNode` and adjust it to behave the way that you want. For example, the default behaviour of the :py:class:`TreeNode` is to show one of 3 different icons depending on whether the node has children or not and whether it has been expanded. But you might want to display a different icon based on some attribute of the object being viewed, and that would require a new :py:class:`TreeNode` subclass to override that behaviour. Concretely, if we had different document types, identified by file extension:: class DocumentTreeNode(TreeNode): icons = Dict({ '.npy': ImageResource('document-table'), '.txt': ImageResource('document-text'), '.rst': ImageResource('document-text'), '.png': ImageResource('document-image'), '.jpg': ImageResource('document-image'), }) def get_icon(self, object, is_expanded): icon = self.icons.get(object.extension, self.icon_item) return icon This :py:class:`TreeNode` subclass can now be used with any compatible class to give a richer set of icons. Common use cases for this approach would include: - more customized icon display, as above. - having the label built from multiple traits, which requires overriding :py:meth:`~TreeNode.get_label`, :py:meth:`~TreeNode.when_label_changed` and possibly :py:meth:`~TreeNode.set_label`. - having the children come from multiple traits, which requires overriding :py:meth:`~TreeNode.allows_children`, :py:meth:`~TreeNode.get_children`, :py:meth:`~TreeNode.when_children_replaced`, :py:meth:`~TreeNode.when_children_changed` and possibly :py:meth:`~TreeNode.append_child`, :py:meth:`~TreeNode.insert_child` and :py:meth:`~TreeNode.delete_child` (although there may be better ways to handle this situation by using multiple :py:class:`TreeNodes` for the class). - being more selective about what objects to use for the node. For example, requiring not only that an object be of a certain class, but that it also have an attribute with a cetain value. This requires overriding :py:meth:`~TreeNode.is_node_for`. - customization of menus on a per-object basis, or other UI behaviour like drag and drop, selection and clicking. This has the advantage that most of the time the behaviour that you want is built into the :py:class:`TreeNode` class, and you only need to change the things which are not to your requirements. Where :py:class:`TreeNode` classes are generally weak is when the object you are trying to view is not a :py:class:`~traits.api.HasTraits` instance, or where you don't know the full set of classes that you need to display in the tree when writing the UI. You can overcome these obstacles by careful subclassing, taking particular care to avoid things like trying to set traits listeners on non-:py:class:`~traits.api.HasTraits` objects or adapting the object to a desired interface before using it. But in these cases it may be better to use a different approach. ITreeNodes and ITreeNodeAdapters -------------------------------- These are most useful for situations where you don't know the full set of classes that may be displayed in a tree. This is a common situation when writing complex applications using libraries like Envisage that allow new functionality to be added to the application via plug-ins (potentially during run-time!). It is also useful in situations where the model object that is being viewed isn't a :py:class:`~traits.api.HasTraits` object, or where you may need some UI state in the node that doesn't belong on the underlying model object (for example, caching quantities which are expensive to compute). Before using this approach, you should make sure that you understand the way that traits adaptation works. To make writing code which satisfies the :py:class:`ITreeNode` interface easier, there is an :py:class:`ITreeNodeAdapter` class which provides basic functionality and which can be subclassed to provide an adapter class for your own nodes. This adapter is minimalistic and not complete. You will at a minimum need to override the :py:meth:`~ITreeNodeAdapter.get_label` method, and probably many others to get the desired behaviour. Since the :py:class:`ITreeNodeAdapter` is an :py:class:`Adapter` subclass, the object being adapted is available as the :py:attr:`adaptee` attribute. This means that the methods might look similar to the ones for :py:class:`TreeNode`, but they don't expect to be passed the object as a parameter. Once you have written the :py:class:`ITreeNodeAdapter` subclass, you have to register the adapter with traits using the Traits :py:func:`regsiter_factory` function. You are not required to use :py:class:`ITreeNodeAdapter` if you don't wish to. You can instead write a class which ``@provides`` the :py:class:`ITreeNode` interface directly, or create an alternative adapter class. Note that currently the tree editor infrastructure uses the deprecated Traits :py:func:`adapts` class advisor and the default traits adapter registry which means that you can't have mulitple different :py:class:`ITreeNode` adapters for a given object to use in different editors within a given application. This is likely to be fixed in a future release of TraitsUI. In the mean-time you can work around this somewhat by having the trait being edited and/or the :py:meth:`~ITreeNodeAdapter.get_children` method return pre-adapted objects, rather than relying on traits adaptation machinery to find and adapt the object. ObjectTreeNodes and TreeNodeObjects ----------------------------------- Another approach to adapting objects, particularly non-:py:class:`HasTraits` objects is used by the :py:class:`ValueEditor`, but is available for general tree editors to use as well. In this approach you write one or more :py:class:`TreeNodeObject` classes that wrap the model objects that you want to display, and then use instances of the :py:class:`TreeNodeObject` classes within the tree editor, both as the root node being edited, and the objects returned by the :py:meth:`tno_get_children` methods. To fit these with the expected :py:class:`TreeNode` classes used by the :py:class:`TreeEditor`, there is the :py:class:`ObjectTreeNode` class which knows how to call the appropriate :py:class:`TreeNodeObjects` and which can be given a list of :py:class:`TreeNodeObject` classes that it understands. For example, it is possible to represent a tree structure in Python using nested dictionaries with strings as keys. A :py:class:`TreeNodeObject` for such a structure might look like this:: class DictNode(TreeNodeObject): #: The parent of the node parent = Instance('DictNode') #: The label for the node label = Str() #: The value for this node value = Any() def tno_get_label(self, node): return self.label def tno_allows_children(self, node): return isinstance(self.value, dict) def tno_has_children(self, node): return bool(self.value) def tno_get_children(self, node): return [ DictNode(parent=self, label=key, value=value) for key, value in sorted(self.value.items()) ] and so forth. There is additional work if you want to be able to modify the structure of the tree, for example. In addition to defining the :py:class:`TreeNodeObject` subclass, you also need provide the nodes for the editor something like this:: dict_tree_editor = TreeEditor( editable=False, nodes=[ ObjectTreeNode( node_for=[DictNode], rename=False, rename_me=False, copy=False, delete=False, delete_me=False, ) ] ) The :py:class:`ObjectTreeNode` is a :py:class:`TreeNode` subclass that delegates operations to the :py:class:`TreeNodeObject`, but the default :py:class:`TreeNodeObject` methods try to behave in the same way as the base :py:class:`TreeNode`, so you can specify global behaviour on the :py:class:`ObjectTreeNode` in the same way that you can for a :py:class:`TreeNode`. The last piece to make this approach work is that the root node when editing has to be a :py:class:`DictNode` instance, so you may need to provide a property that wraps the raw tree structure in a :py:class:`DictNode` to get started: unlike the :py:class:`ITreeNodeAdapter` approaches this wrapping not automatically provided for you. Custom Renderers ---------------- The Qt backend allows users to completely override the rendering of cells in a TreeEditor. To do this, the TreeNode should override the :py:meth:`TreeNode.get_renderer` method to return an instance of a subclass of :py:class:`~traitsui.tree_node_renderer.AbstractTreeNodeRenderer`. A :py:class:`~traitsui.qt.tree_node_renderers.WordWrapRenderer` is available to provide basic word-wrapped layout in a cell, but user-defined subclasses can do any rendering that they want by implementing their own :py:class:`~traitsui.tree_node_renderer.AbstractTreeNodeRenderer` subclass. :py:class:`~traitsui.tree_node_renderer.AbstractTreeNodeRenderer` is an abstract base class, and subclasses must implement two methods: :py:meth:`~traitsui.tree_node_renderer.AbstractTreeNodeRenderer.size` A method which should return a (height, width) tuple giving the preferred size for the cell. Depending on other constraints and user interactions, this may not be the actual size that the cell will have available. The toolkit will provide a ``size_context`` object that provides useful parameters to help with sizing operations. In the Qt backend, this is a tuple containing the arguments passed to the Qt :py:meth:`sizeHint` method of a :py:class:`QStyledItemDelegate`. :py:meth:`~traitsui.tree_node_renderer.AbstractTreeNodeRenderer.paint` Toolkit-dependent code that renders the cell The toolkit will provide a ```paint_context``` object that provides useful parameters to help with painting operations. In the Qt backend, this is a tuple containing the arguments passed to the Qt :py:meth:`paint` method of a :py:class:`QStyledItemDelegate`. In particular, the first argument is always a :py:class:`QPainter` instance and the second a :py:class:`QStyleOptionViewItem` from which you can get the rectangle of the cell being rendered as well as style and font information. The renderer can choose to not handle all of the rendering, and instead let the tree editor handle rendering the icon or the text of the cell, by setting the :py:meth:`~traitsui.tree_node_renderer.AbstractTreeNodeRenderer.handles_icon`, :py:meth:`~traitsui.tree_node_renderer.AbstractTreeNodeRenderer.handles_text`, and :py:meth:`~traitsui.tree_node_renderer.AbstractTreeNodeRenderer.handles_all` traits appropriately. Lastly there is a convenience method :py:meth:`~traitsui.tree_node_renderer.AbstractTreeNodeRenderer.get_label` that gets the label text given the tree node, the underlying object, and the column, smoothing over the TreeNode columns label API. Examples -------- There are a number of examples of use of the :py:class:`~traitsui.editors.tree_editor.TreeEditor` in the TraitsUI demos: - :github-demo:`TreeEditor ` - :github-demo:`Adapted TreeEditor ` - :github-demo:`HDF5 Tree ` - :github-demo:`Tree Editor with Renderer ` .. _advanced-tabular-adapter: The TabularAdapter Class ======================== .. module:: traitsui.tabular_adapter :noindex: The power and flexibility of the tabular editor is mostly a result of the :py:class:`TabularAdapter` class, which is the base class from which all tabular editor adapters must be derived. The :py:class:`~traitsui.editors.tabular_editor.TabularEditor` object interfaces between the underlying toolkit widget and your program, while the :py:class:`TabularAdapter` object associated with the editor interfaces between the editor and your data. The design of the :py:class:`TabularAdapter` base class is such that it tries to make simple cases simple and complex cases possible. How it accomplishes this is what we'll be discussing in the following sections. The TabularAdapter *columns* Trait ---------------------------------- First up is the :py:class:`TabularAdapter` :py:attr:`columns` trait, which is a list of values which define, in presentation order, the set of columns to be displayed by the associated :py:class:`~traitsui.editors.tabular_editor.TabularEditor`. Each entry in the :py:attr:`~TabularAdapter.columns` list can have one of two forms: - ``string`` - ``(string, id)`` where ``string`` is the user interface name of the column (which will appear in the table column header) and ``id`` is any value that you want to use to identify that column to your adapter. Normally this value is either a trait name or an integer index value, but it can be any value you want. If only ``string`` is specified, then ``id`` is the index of the ``string`` within ``columns``. For example, say you want to display a table containing a list of tuples, each of which has three values: a name, an age, and a weight. You could then use the following value for the :py:attr:`~TabularAdapter.columns` trait:: columns = ['Name', 'Age', 'Weight'] By default, the ``id`` values (also referred to in later sections as the *column ids*) for the columns will be the corresponding tuple index values. Say instead that you have a list of :py:class:`Person` objects, with :py:attr:`name`, :py:attr:`age` and :py:attr:`weight` traits that you want to display in the table. Then you could use the following :py:attr:`~TabularAdapter.columns` value instead:: columns = [ ('Name', 'name'), ('Age', 'age'), ('Weight', 'weight'), ] In this case, the *column ids* are the names of the traits you want to display in each column. Note that it is possible to dynamically modify the contents of the :py:attr:`~TabularAdapter.columns` trait while the :py:class:`~traitsui.editors.tabular_editor.TabularEditor` is active. The :py:class:`~traitsui.editors.tabular_editor.TabularEditor` will automatically modify the table to show the new set of defined columns. The Core TabularAdapter Interface --------------------------------- In this section, we'll describe the core interface to the :py:class:`TabularAdapter` class. This is the actual interface used by the :py:class:`~traitsui.editors.tabular_editor.TabularEditor` to access your data and display attributes. In the most complex data representation cases, these are the methods that you must override in order to have the greatest control over what the editor sees and does. However, the base :py:class:`TabularAdapter` class provides default implementations for all of these methods. In subsequent sections, we'll look at how these default implementations provide simple means of customizing the adapter to your needs. But for now, let's start by covering the details of the core interface itself. To reduce the amount of repetition, we'll use the following definitions in all of the method argument lists that follow in this section: object The object whose trait is being edited by the :py:class:`~traitsui.editors.tabular_editor.TabularEditor`. trait The name of the trait the :py:class:`~traitsui.editors.tabular_editor.TabularEditor` is editing. row The row index (starting with 0) of a table item. column The column index (starting with 0) of a table column. The adapter interface consists of a number of methods which can be divided into two main categories: those which are sensitive to the type of a particular table item, and those which are not. We'll begin with the methods that are sensitive to an item's type: :py:meth:`~TabularAdapter.get_alignment` Returns the alignment style to use for a specified column. The possible values that can be returned are: ``'left'``, ``'center'`` or ``'right'``. All table items share the same alignment for a specified column. :py:meth:`~TabularAdapter.get_width` Returns the width to use for a specified column. If the value is <= 0, the column will have a *default* width, which is the same as specifying a width of 0.1. If the value is > 1.0, it is converted to an integer and the result is the width of the column in pixels. This is referred to as a *fixed width* column. If the value is a float such that 0.0 < value <= 1.0, it is treated as the *unnormalized fraction of the available space* that is to be assigned to the column. What this means requires a little explanation. To arrive at the size in pixels of the column at any given time, the editor adds together all of the *unnormalized fraction* values returned for all columns in the table to arrive at a total value. Each *unnormalized fraction* is then divided by the total to create a *normalized fraction*. Each column is then assigned an amount of space in pixels equal to the maximum of 30 or its *normalized fraction* multiplied by the *available space*. The *available space* is defined as the actual width of the table minus the width of all *fixed width* columns. Note that this calculation is performed each time the table is resized in the user interface, thus allowing columns of this type to increase or decrease their width dynamically, while leaving *fixed width* columns unchanged. :py:meth:`~TabularAdapter.get_can_edit` Returns whether the user can edit a specified row. A ``True`` result indicates that the value can be edited, while a ``False`` result indicates that it cannot. :py:meth:`~TabularAdapter.get_drag` Returns the value to be *dragged* for a specified row. A result of ``None`` means that the item cannot be dragged. Note that the value returned does not have to be the actual row item. It can be any value that you want to drag in its place. In particular, if you want the drag target to receive a copy of the row item, you should return a copy or clone of the item in its place. Also note that if multiple items are being dragged, and this method returns ``None`` for any item in the set, no drag operation is performed. :py:meth:`~TabularAdapter.get_can_drop` Returns whether the specified ``value`` can be dropped on the specified row. A value of ``True`` means the ``value`` can be dropped; and a value of ``False`` indicates that it cannot be dropped. The result is used to provide the user positive or negative drag feedback while dragging items over the table. ``value`` will always be a single value, even if multiple items are being dragged. The editor handles multiple drag items by making a separate call to :py:meth:`get_can_drop` for each item being dragged. :py:meth:`~TabularAdapter.get_dropped` Returns how to handle a specified ``value`` being dropped on a specified row. The possible return values are: - ``'before'``: Insert the specified ``value`` before the dropped on item. - ``'after'``: Insert the specified ``value`` after the dropped on item. Note there is no result indicating *do not drop* since you will have already indicated that the ``object`` can be dropped by the result returned from a previous call to :py:meth:`get_can_drop`. :py:meth:`~TabularAdapter.get_font` Returns the font to use for displaying a specified row or cell. A result of ``None`` means use the default font; otherwise a toolkit font object should be returned. Note that all columns for the specified table row will use the font value returned. :py:meth:`~TabularAdapter.get_text_color` Returns the text color to use for a specified row or cell. A result of ``None`` means use the default text color; otherwise a toolkit-compatible color should be returned. Note that all columns for the specified table row will use the text color value returned. :py:meth:`~TabularAdapter.get_bg_color` Returns the background color to use for a specified row or cell. A result of ``None`` means use the default background color; otherwise a toolkit-compatible color should be returned. Note that all columns for the specified table row will use the background color value returned. :py:meth:`~TabularAdapter.get_image` Returns the image to display for a specified cell. A result of ``None`` means no image will be displayed in the specified table cell. Otherwise the result should either be the name of the image, or an :py:class:`~pyface.image_resource.ImageResource` object specifying the image to display. A name is allowed in the case where the image is specified in the :py:class:`~traitsui.editors.tabular_editor.TabularEditor` :py:attr:`~traitsui.editors.tabular_editor.TabularEditor.images` trait. In that case, the name should be the same as the string specified in the :py:class:`~pyface.image_resource.ImageResource` constructor. :py:meth:`~TabularAdapter.get_format` Returns the Python formatting string to apply to the specified cell. The resulting of formatting with this string will be used as the text to display it in the table. The return can be any Python string containing exactly one old-style Python formatting sequence, such as ``'%.4f'`` or ``'(%5.2f)'``. :py:meth:`~TabularAdapter.get_text` Returns a string containing the text to display for a specified cell. If the underlying data representation for a specified item is not a string, then it is your responsibility to convert it to one before returning it as the result. :py:meth:`~TabularAdapter.set_text` Sets the value for the specified cell. This method is called when the user completes an editing operation on a table cell. The string specified by ``text`` is the value that the user has entered in the table cell. If the underlying data does not store the value as text, it is your responsibility to convert ``text`` to the correct representation used. :py:meth:`~TabularAdapter.get_tooltip` Returns a string containing the tooltip to display for a specified cell. You should return the empty string if you do not wish to display a tooltip. The following are the remaining adapter methods, which are not sensitive to the type of item or column data: :py:meth:`~TabularAdapter.get_item` Returns the specified row item. The value returned should be the value that exists (or *logically* exists) at the specified ``row`` in your data. If your data is not really a list or array, then you can just use ``row`` as an integer *key* or *token* that can be used to retrieve a corresponding item. The value of ``row`` will always be in the range: 0 <= row < ``len(object, trait)`` (i.e. the result returned by the adapter :py:meth:`len` method). :py:meth:`~TabularAdapter.len` Returns the number of row items in the specified ``object.trait``. The result should be an integer greater than or equal to 0. :py:meth:`~TabularAdapter.delete` Deletes the specified row item. This method is only called if the *delete* operation is specified in the :py:class:`~traitsui.editors.tabular_editor.TabularEditor` :py:attr:`~traitsui.editors.tabular_editor.TabularEditor.operation` trait, and the user requests that the item be deleted from the table. The adapter can still choose not to delete the specified item if desired, although that may prove confusing to the user. :py:meth:`~TabularAdapter.insert` Inserts ``value`` at the specified ``object.trait[row]`` index. The specified ``value`` can be: - An item being moved from one location in the data to another. - A new item created by a previous call to :py:meth:`~TabularAdapter.get_default_value`. - An item the adapter previously approved via a call to :py:meth:`~TabularAdapter.get_can_drop`. The adapter can still choose not to insert the item into the data, although that may prove confusing to the user. :py:meth:`~TabularAdapter.get_default_value` Returns a new default value for the specified ``object.trait`` list. This method is called when *insert* or *append* operations are allowed and the user requests that a new item be added to the table. The result should be a new instance of whatever underlying representation is being used for table items. Creating a Custom TabularAdapter -------------------------------- Having just taken a look at the core :py:class:`TabularAdapter` interface, you might now be thinking that there are an awful lot of methods that need to be specified to get an adapter up and running. But as we mentioned earlier :py:class:`TabularAdapter` is not an abstract base class. It is a concrete base class with implementations for each of the methods in its interface. And the implementations are written in such a way that you will hopefully hardly ever need to override them. In this section, we'll explain the general implementation style used by these methods, and how you can take advantage of them in creating your own adapters. One of the things you probably noticed as you read through the core adapter interface section is that most of the methods have names of the form: ``get_xxx`` or ``set_xxx``, which is similar to the familiar *getter/setter* pattern used when defining trait properties. The adapter interface is purposely defined this way so that it can expose and leverage a simple set of design rules. The design rules are followed consistently in the implementations of all of the adapter methods described in the first section of the core adapter interface, so that once you understand how they work, you can easily apply the design pattern to all items in that section. Then, only in the case where the design rules will not work for your application will you ever have to override any of those :py:class:`TabularAdapter` base class method implementations. So the first thing to understand is that if an adapter method name has the form: ``get_xxx`` or ``set_xxx`` it really is dealing with some kind of trait called ``xxx``, or which contains ``xxx`` in its name. For example, the :py:meth`~TabularAdapter.get_alignment` method retrieves the value of some :py:attr:`~TabularAdapter.alignment` trait defined on the adapter. In the following discussion we'll simply refer to an attribute name generically as *attribute*, but you will need to replace it by an actual attribute name (e.g. :py:attr:`~TabularAdapter.alignment`) in your adapter. The next thing to keep in mind is that the adapter interface is designed to easily deal with items that are not all of the same type. As we just said, the design rules apply to all adapter methods in the first group, which were defined as methods which are sensitive to an item's type. Item type sensitivity plays an important part in the design rules, as we will see shortly. With this in mind, we now describe the simple design rules used by the first group of methods in the :py:class:`TabularAdapter` class: - When getting or setting an adapter attribute, the method first retrieves the underlying item for the specified data row. The item, and type (i.e. class) of the item, are then used in the next rule. - The method gets or sets the first trait it finds on the adapter that matches one of the following names: - *classname_columnid_attribute* - *classsname_attribute* - *columnid_attribute* - *attribute* where: - *classname* is the name of the class of the item found in the first step, or one of its base class names, searched in the order defined by the *mro* (**method resolution order**) for the item's class. - *columnid* is the column id specified by the developer in the adapter's *column* trait for the specified table column. - *attribute* is the attribute name as described previously (e.g. *alignment*). Note that this last rule always finds a matching trait, since the :py:class:`TabularAdapter` base class provides traits that match the simple *attribute* form for all attributes these rules apply to. Some of these are simple traits, while others are properties. We'll describe the behavior of all these *default* traits shortly. The basic idea is that rather than override the first group of core adapter methods, you simply define one or more simple traits or trait properties on your :py:class:`TabularAdapter` subclass that provide or accept the specified information. All of the adapter methods in the first group provide a number of arguments, such as ``object``, ``trait``, ``row`` and ``column``. In order to define a trait property, which cannot be passed this information directly, the adapter always stores the arguments and values it computes in the following adapter traits, where they can be easily accessed by a trait getter or setter method: - ``row``: The table row being accessed. - ``column``: The column id of the table column being accessed (not its index). - ``item``: The data item for the specified table row (i.e. the item determined in the first step described above). - ``value``: In the case of a *set_xxx* method, the value to be set; otherwise it is ``None``. As mentioned previously, the :py:class:`TabularAdapter` class provides trait definitions for all of the attributes these rules apply to. You can either use the default values as they are, override the default, set a new value, or completely replace the trait definition in a subclass. A description of the default trait implementation for each attribute is as follows: :py:attr:`~TabularAdapter.default_value` = ``Any('')`` The default value for a new row. The default value is the empty string, but you will normally need to assign a different (default) value. :py:attr:`~TabularAdapter.format` = ``Str('%s')`` The default Python formatting string for a column item. The default value is ``'%s'`` which will simply convert the column item to a displayable string value. :py:attr:`~TabularAdapter.text` = ``Property`` The text to display for the column item. The implementation of the property checks the type of the column's *column id*: - If it is an integer, it returns ``format % item[column_id]``. - Otherwise, it returns ``format % item.column_id``. Note that ``format`` refers to the value returned by a call to :py:meth:`~TabularAdapter.get_format` for the current column item. :py:attr:`~TabularAdapter.text_color` = ``Property`` The text color for a row item. The property implementation checks to see if the current table row is even or odd, and based on the result returns the value of the :py:attr:`~TabularAdapter.even_text_color` or :py:attr:`~TabularAdapter.odd_text_color` trait if the value is not ``None``, and the value of the :py:attr:`~TabularAdapter.default_text_color` trait if it is. The definition of these additional traits are as follows: - :py:attr:`~TabularAdapter.odd_text_color` = ``Color(None)`` - :py:attr:`~TabularAdapter.even_text_color` = ``Color(None)`` - :py:attr:`~TabularAdapter.default_text_color` = ``Color(None)`` Remember that a ``None`` value means use the default text color. :py:attr:`~TabularAdapter.bg_color` = ``Property`` The background color for a row item. The property implementation checks to see if the current table row is even or odd, and based on the result returns the value of the :py:attr:`~TabularAdapter.even_bg_color` or :py:attr:`~TabularAdapter.odd_bg_color` trait if the value is not ``None``, and the value of the :py:attr:`~TabularAdapter.default_bg_color` trait if it is. The definition of these additional traits are as follows: - :py:attr:`~TabularAdapter.odd_bg_color` = ``Color(None)`` - :py:attr:`~TabularAdapter.even_bg_color` = ``Color(None)`` - :py:attr:`~TabularAdapter.default_bg_color` = ``Color(None)`` Remember that a ``None`` value means use the default background color. :py:attr:`~TabularAdapter.alignment` = ``Enum('left', 'center', 'right')`` The alignment to use for a specified column. The default value is ``'left'``. :py:attr:`~TabularAdapter.width` = ``Float(-1)`` The width of a specified column. The default value is -1, which means a dynamically sized column with an *unnormalized fractional* value of 0.1. :py:attr:`~TabularAdapter.can_edit` = ``Bool(True)`` Specifies whether the text value of the current item can be edited. The default value is ``True``, which means that the user can edit the value. :py:attr:`~TabularAdapter.drag` = ``Property`` A property which returns the value to be dragged for a specified row item. The property implementation simply returns the current row item. :py:attr:`~TabularAdapter.can_drop` = ``Bool(False)`` Specifies whether the specified value be dropped on the current item. The default value is ``False``, meaning that the value cannot be dropped. :py:attr:`~TabularAdapter.dropped` = ``Enum('after', 'before')`` Specifies where a dropped item should be placed in the table relative to the item it is dropped on. The default value is ``'after'``. :py:attr:`~TabularAdapter.font` = ``Font`` The font to use for the current item. The default value is the standard default Traits font value. :py:attr:`~TabularAdapter.image` = ``Str(None)`` The name of the default image to use for a column. The default value is ``None``, which means that no image will be displayed for the column. :py:attr:`~TabularAdapter.tooltip` = ``Str`` The tooltip information for a column item. The default value is the empty string, which means no tooltip information will be displayed for the column. The preceding discussion applies to all of the methods defined in the first group of :py:attr:`TabularAdapter` interface methods. However, the design rules do not apply to the remaining five adapter methods, although they all provide a useful default implementation: :py:meth:`~TabularAdapter.get_item` The default implementation assumes the trait defined by ``object.trait`` is a *sequence* and attempts to return the value at index ``row``. If an error occurs, it returns ``None`` instead. This definition should work correctly for lists, tuples and arrays, or any other object that is indexable, but will have to be overridden for all other cases. Note that this method is the one called in the first design rule described previously to retrieve the item at the current table row. :py:meth:`~TabularAdapter.len` Again, the default implementation assumes the trait defined by ``object.trait`` is a *sequence* and attempts to return the result of calling ``len(object.trait)``. It will need to be overridden for any type of data which for which :py:func:`len` will not work. :py:meth:`~TabularAdapter.delete` The default implementation assumes the trait defined by ``object.trait`` is a mutable sequence and attempts to perform a ``del object.trait[row]`` operation. :py:meth:`~TabularAdapter.insert` The default implementation assumes the trait defined by ``object.trait`` is a mutable sequence and attempts to perform an ``object.trait[row:row] = [value]`` operation. :py:meth:`~TabularAdapter.get_default_value` The default implementation simply returns the value of the adapter's :py:attr:`~TabularAdapter.default_value` trait. Examples -------- There are a number of examples of use of the :py:class:`TabularAdapter` in the TraitsUI demos: - :github-demo:`TabularEditor ` - :github-demo:`TabularEditor (auto-update) ` - :github-demo:`NumPy array TabularEditor ` The ListStrAdapter Class ======================== .. module:: traitsui.list_str_adapter :noindex: Although the :py:class:`~traitsui.editors.list_str_editor.ListStrEditor` editor is frequently used, as might be expected, with lists of strings, it also provides facilities to edit lists of other object types that can be adapted to produce strings for display and editing via :py:class:`ListStrAdapter` subclasses The design of the :py:class:`ListStrAdapter` base class follows the same design as the :py:class:`~traitsui.tabular_adapter.TabularAdapter`, simplified by the fact that there are only rows, no columns. However, the names and intents of the various methods and traits are the same as the :py:class:`~traitsui.tabular_adapter.TabularAdapter`, and so the approaches discussed in the previous section work for the :py:class:`ListStrAdapter` as well. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/docs/source/traitsui_user_manual/advanced_view.rst0000644000175100001730000004121500000000000026210 0ustar00runnerdocker00000000000000.. index:: View; internal, View; external, View; ways of displaying pair: View; context .. _advanced-view-concepts: ====================== Advanced View Concepts ====================== The preceding chapters of this Manual give an overview of how to use the View class to quickly construct a simple window for a single HasTraits object. This chapter explores a number of more complex techniques that significantly increase the power and versatility of the View object. * *Internal Views:* Views can be defined as attributes of a HasTraits class; one class can have multiple views. View attributes can be inherited by subclasses. * *External Views:* A view can be defined as a module variable, inline as a function or method argument, or as an attribute of a :term:`Handler`. * *Ways of displaying Views:* You can display a View by calling configure_traits() or edit_traits() on a HasTraits object, or by calling the ui() method on the View object. * *View context:* You can pass a context to any of the methods for displaying views, which is a dictionary of labels and objects. In the default case, this dictionary contains only one object, referenced as 'object', but you can define contexts that contain multiple objects. * *Include objects:* You can use an Include object as a placeholder for view items defined elsewhere. .. index: View; internal .. _internal-views: Internal Views -------------- In the examples thus far, the View objects have been external. That is to say, they have been defined outside the model (HasTraits object or objects) that they are used to display. This approach is in keeping with the separation of the two concepts prescribed by the :term:`MVC` design pattern. There are cases in which it is useful to define a View within a HasTraits class. In particular, it can be useful to associate one or more Views with a particular type of object so that they can be incorporated into other parts of the application with little or no additional programming. Further, a View that is defined within a model class is inherited by any subclasses of that class, a phenomenon called *visual inheritance*. .. _defining-a-default-view: Defining a Default View ``````````````````````` .. index:: default view, View; default It is easy to define a default view for a HasTraits class: simply create a View attribute called **traits_view** for that class. Consider the following variation on Example 3: .. index:: configure_traits(); default view example, default view; example .. index:: examples; default view .. _example-5-using-configure-traits-with-a-default-view-object: .. rubric:: Example 5: Using configure_traits() with a default View object .. literalinclude:: examples/default_traits_view.py :start-at: default_traits_view.py In this example, configure_traits() no longer requires a *view* keyword argument; the **traits_view** attribute is used by default, resulting in the same display as in Figure 3: .. figure:: images/ui_for_ex3.jpg :alt: User interface showing three fields enclosed in a border Figure 5: User interface for Example 5 It is not strictly necessary to call this View attribute **traits_view**. If exactly one View attribute is defined for a HasTraits class, that View is always treated as the default display template for the class. However, if there are multiple View attributes for the class (as discussed in the next section), if one is named 'traits_view', it is always used as the default. .. index:: default_traits_view(); default view method, default view Sometimes, it is necessary to build a view based on the state of the object when it is being built. In such cases, defining the view statically is limiting, so one can override the default_traits_view() method of a HasTraits object. The example above would be implemented as follows: .. _example-5b-using-default-traits-view: .. rubric:: Example 5b: Building a default View object with default_traits_view() .. literalinclude:: examples/default_traits_view2.py :start-at: default_traits_view2.py This pattern can be useful for situations where the layout of GUI elements depends on the state of the object. For instance, to populate the values of a :ref:`checklisteditor` with items read in from a file, it would be useful to build the default view this way. .. index:: View; multiple, multiple Views .. _defining-multiple-views-within-the-model: Defining Multiple Views Within the Model ```````````````````````````````````````` Sometimes it is useful to have more than one pre-defined view for a model class. In the case of the SimpleEmployee class, one might want to have both a "public information" view like the one above and an "all information" view. One can do this by simply adding a second View attribute: .. index:: pair: examples; multiple Views .. _example-6-defining-multiple-view-objects-in-a-hastraits-class: .. rubric:: Example 6: Defining multiple View objects in a HasTraits class .. literalinclude:: examples/multiple_views.py :start-at: multiple_views.py .. index:: traits_view attribute, configure_traits(); view parameter As before, a simple call to configure_traits() for an object of this class produces a window based on the default View (**traits_view**). In order to use the alternate View, use the same syntax as for an external view, except that the View name is specified in single quotes to indicate that it is associated with the object rather than being a module-level variable:: configure_traits(view='all_view'). Note that if more than one View is defined for a model class, you must indicate which one is to be used as the default by naming it ``traits_view``. Otherwise, TraitsUI gives preference to none of them, and instead tries to construct a default View, resulting in a simple alphabetized display as described in :ref:`the-view-and-its-building-blocks`. For this reason, it is usually preferable to name a model's default View traits_view even if there are no other Views; otherwise, simply defining additional Views, even if they are never used, can unexpectedly change the behavior of the GUI. .. index:: View; external .. _separating-model-and-view-external-views: Separating Model and View: External Views ----------------------------------------- In all the preceding examples in this guide, the concepts of model and view have remained closely coupled. In some cases the view has been defined in the model class, as in :ref:`internal-views`; in other cases the configure_traits() method that produces a window from a View has been called from a HasTraits object. However, these strategies are simply conveniences; they are not an intrinsic part of the relationship between model and view in TraitsUI. This section begins to explore how the TraitsUI package truly supports the separation of model and view prescribed by the :term:`MVC` design pattern. An *external* view is one that is defined outside the model classes. In Traits UI, you can define a named View wherever you can define a variable or class attribute. [7]_ A View can even be defined in-line as a function or method argument, for example:: object.configure_traits( view=View( Group( Item(name='a'), Item(name='b'), Item(name='c'), ), ), ) However, this approach is apt to obfuscate the code unless the View is very simple. :ref:`Example 2 ` through :ref:`Example 4 ` demonstrate external Views defined as variables. One advantage of this convention is that the variable name provides an easily accessible "handle" for re-using the View. This technique does not, however, support visual inheritance. A powerful alternative is to define a View within the :term:`controller` (Handler) class that controls the window for that View. [8]_ This technique is described in :ref:`controlling-the-interface-the-handler`. .. index:: View; methods for displaying .. _displaying-a-view: Displaying a View ----------------- TraitsUI provides three methods for creating a window or panel from a View object. The first two, configure_traits() and edit_traits(), are defined on the HasTraits class, which is a superclass of all Traits-based model classes, as well as of Handler and its subclasses. The third method, ui(), is defined on the View class itself. .. index:: configure_traits(); method .. _configure-traits: configure_traits() `````````````````` The configure_traits() method creates a standalone window for a given View object, i.e., it does not require an existing GUI to run in. It is therefore suitable for building command-line functions, as well as providing an accessible tool for the beginning TraitsUI programmer. The configure_traits() method also provides options for saving :term:`trait attribute` values to and restoring them from a file. Refer to the *Traits API Reference* for details. .. index:: edit_traits() .. _edit-traits: edit_traits() ````````````` The edit_traits() method is very similar to configure_traits(), with two major exceptions. First, it is designed to run from within a larger application whose GUI is already defined. Second, it does not provide options for saving data to and restoring data from a file, as it is assumed that these operations are handled elsewhere in the application. .. index:: ui() .. _ui: ui() ```` The View object includes a method called ui(), which performs the actual generation of the window or panel from the View for both edit_traits() and configure_traits(). The ui() method is also available directly through the TraitsUI API; however, using one of the other two methods is usually preferable. [9]_ The ui() method has five keyword parameters: * *kind* * *context* * *handler* * *parent* * *view_elements* The first four are identical in form and function to the corresponding arguments of edit_traits(), except that *context* is not optional; the following section explains why. The fifth argument, *view_elements*, is used only in the context of a call to ui() from a model object method, i.e., from configure_traits() or edit_traits(), Therefore it is irrelevant in the rare cases when ui() is used directly by client code. It contains a dictionary of the named :term:`ViewElement` objects defined for the object whose configure_traits() (or edit_traits()) method was called.. .. index:: context .. _the-view-context: The View Context ---------------- All three of the methods described in :ref:`displaying-a-view` have a *context* parameter. This parameter can be a single object or a dictionary of string/object pairs; the object or objects are the model objects whose traits attributes are to be edited. In general a "context" is a Python dictionary whose keys are strings; the key strings are used to look up the values. In the case of the *context* parameter to the ui() method, the dictionary values are objects. In the special case where only one object is relevant, it can be passed directly instead of wrapping it in a dictionary. When the ui() method is called from configure_traits() or edit_traits() on a HasTraits object, the relevant object is the HasTraits object whose method was called. .. NOTE:: There are some situations in which you may want to override the default context used for a particular HasTraits class. To do this, simpy override the :py:meth:`~.traits.has_traits.HasTraits.trait_context` method on the object. For this reason, you do not need to specify the *context* argument in most calls to configure_traits() or edit_traits(). However, when you call the ui() method on a View object, you *must* specify the *context* parameter, so that the ui() method receives references to the objects whose trait attributes you want to modify. So, if configure_traits() figures out the relevant context for you, why call ui() at all? One answer lies in *multi-object Views*. .. index:: multi-object Views, View; multi-object .. _multi-object-views: Multi-Object Views `````````````````` A multi-object view is any view whose contents depend on multiple "independent" model objects, i.e., objects that are not attributes of one another. For example, suppose you are building a real estate listing application, and want to display a window that shows two properties side by side for a comparison of price and features. This is straightforward in TraitsUI, as the following example shows: .. index:: examples; context, context; examples, examples; multi-object Views .. index:: multi-object Views; examples .. _example-7-using-a-multi-object-view-with-a-context: .. rubric:: Example 7: Using a multi-object view with a context .. literalinclude:: examples/multi_object_view.py :start-at: multi_object_view.py .. FIXME: This is a bit assymmetrical. Can we clean it up without complicating the example overly? The resulting window has the desired appearance: [10]_ .. figure:: images/ui_for_ex7.jpg :alt: UI showing side-by-side groups. Figure 6: User interface for Example 7 For the purposes of this particular example, it makes sense to create a separate Group for each model object, and to use two model objects of the same class. Note, however, that neither is a requirement. .. index:: extended trait names; Item name attribute Notice that the Item definitions in Example 7 use the same type of extended trait attribute syntax as is supported for the on_trait_change() dynamic trait change notification method. In fact, Item **name** attributes can reference any trait attribute that is reachable from an object in the context. This is true regardless of whether the context contains a single object or multiple objects. For example:: Item('object.axle.chassis.serial_number') where `"object"` is the literal name which refers to the top-level object being viewed. (Note that `"object"` is **not** some user-defined attribute name like `"axle"` in this example.) More precisely, `"object"` is the default name, in the view's `context` dictionary, of this top-level viewed object (see :ref:`advanced-view-concepts`). Because an Item can refer only to a single trait, do not use extended trait references that refer to multiple traits, since the behavior of such references is not defined. Also, avoid extended trait references where one of the intermediate objects could be None, because there is no way to obtain a valid reference from None. Refer to the `Traits User Manual `_, in the chapter on trait notification, for details of the extended trait name syntax. .. index:: object: Include .. _include-objects: Include Objects --------------- In addition to the Item and Group class, a third building block class for Views exists in TraitsUI: the Include class. For the sake of completeness, this section gives a brief description of Include objects and their purpose and usage. However, they are not commonly used as of this writing, and should be considered unsupported pending redesign. In essence, an Include object is a placeholder for a named Group or Item object that is specified outside the Group or View in which it appears. For example, the following two definitions, taken together, are equivalent to the third: .. index:: pair: examples; Include .. _example-8-using-an-include-object: .. rubric:: Example 8: Using an Include object :: # This fragment... my_view = View( Group(Item('a'), Item('b')), Include('my_group'), ) # ...plus this fragment... my_group = Group( Item('c'), Item('d'), Item('e'), ) #...are equivalent to this: my_view = View( Group(Item('a'), Item('b')), Group(Item('c'), Item('d'), Item('e')), ) This opens an interesting possibility when a View is part of a model class: any Include objects belonging to that View can be defined differently for different instances or subclasses of that class. This technique is called *view parameterization*. .. rubric:: Footnotes .. [7] Note that although the definition of a View within a HasTraits class has the syntax of a trait attribute definition, the resulting View is not stored as an attribute of the class. .. [8] Assuming there is one; not all GUIs require an explicitly defined Handler. .. [9] One possible exception is the case where a View object is defined as a variable (i.e., outside any class) or within a custom Handler, and is associated more or less equally with multiple model objects; see :ref:`multi-object-views`. .. [10] If the script were designed to run within an existing GUI, it would make sense to replace the last line with ``comp_view.ui(context={'h1': house1, 'h2': house2})``, since neither object particularly dominates the view. However, the examples in this Manual are designed to be fully executable from the Python command line, which is why configure_traits() was used instead. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/docs/source/traitsui_user_manual/custom_view.rst0000644000175100001730000003655700000000000025772 0ustar00runnerdocker00000000000000.. index:: View; customizing .. _customizing-a-view: ================== Customizing a View ================== As shown in the preceding two chapters, it is possible to specify a window in TraitsUI simply by creating a View object with the appropriate contents. In designing real-life applications, however, you usually need to be able to control the appearance and behavior of the windows themselves, not merely their content. This chapter covers a variety of options for tailoring the appearance of a window that is created using a View, including the type of window that a View appears in, the :term:`command button`\ s that appear in the window, and the physical properties of the window. .. index:: kind attribute .. _specifying-window-type-the-kind-attribute: Specifying Window Type: the **kind** Attribute ---------------------------------------------- Many types of windows can be used to display the same data content. A form can appear in a window, a wizard, or an embedded panel; windows can be *modal* (i.e., stop all other program processing until the box is dismissed) or not, and can interact with live data or with a buffered copy. In TraitsUI, a single View can be used to implement any of these options simply by modifying its **kind** attribute. There are seven possible values of **kind**: .. index:: modal; window kind, live; window kind, livemodal window kind .. index:: nonmodal window kind, wizard; window kind, panel; window kind .. index:: subpanel; window kind * 'modal' * 'live' * 'livemodal' * 'nonmodal' * 'wizard' (wx backend only) * 'panel' * 'subpanel' These alternatives are described below. If the **kind** attribute of a View object is not specified, the default value is 'modal'. .. index:: windows; stand-alone, modal; definition, live; definition .. _stand-alone-windows: Stand-alone Windows ``````````````````` The behavior of a stand-alone TraitsUI window can vary over two significant degrees of freedom. First, it can be :term:`modal`, meaning that when the window appears, all other GUI interaction is suspended until the window is closed; if it is not modal, then both the window and the rest of the GUI remain active and responsive. Second, it can be :term:`live`, meaning that any changes that the user makes to data in the window is applied directly and immediately to the underlying model object or objects; otherwise the changes are made to a copy of the model data, and are only copied to the model when the user commits them (usually by clicking an :guilabel:`OK` or :guilabel:`Apply` button; see :ref:`command-buttons-the-buttons-attribute`). The four possible combinations of these behaviors correspond to four of the possible values of the 'kind ' attribute of the View object, as shown in the following table. .. _matrix-of-traits-ui-windows-table: .. rubric:: Matrix of TraitsUI windows +-------------+----------------+-----------------+ | |not modal |modal | +=============+================+=================+ |**not live** |:term:`nonmodal`|:term:`modal` | +-------------+----------------+-----------------+ |**live** |:term:`live` |:term:`livemodal`| +-------------+----------------+-----------------+ All of these window types are identical in appearance. Also, all types support the **buttons** attribute, which is described in :ref:`command-buttons-the-buttons-attribute`. Usually, a window with command buttons is called a :term:`dialog box`. .. TODO: Add diagrams and/or examples to clarify. .. index:: wizard, windows; wizard .. _wizards: Wizards ``````` .. note:: Wizard views are only supported with the WX backend! Unlike a standalone window, whose contents appear as a single page or a tabbed display, a :term:`wizard` window presents the view's Groups as a series of pages that a user must navigate sequentially. (For more information about breaking up a view into groups, see :ref:`the-group-object`.) .. _example-3.1-displaying-a-view-the-wizard-style: .. rubric:: Example 3.1: Displaying a view the "wizard" style .. literalinclude:: examples/wizard.py :start-at: wizard.py leads to the following 2 modal dialogs: .. figure:: images/wizard_dialog_1.png :alt: First dialog from wizard view. .. figure:: images/wizard_dialog_2.png :alt: Second dialog from wizard view. TraitsUI Wizards are always modal and live. They always display a standard wizard button set; i.e., they ignore the **buttons** View attribute. In short, wizards are considerably less flexible than windows, and are primarily suitable for highly controlled user interactions such as software installation. .. index:: panel, subpanel, windows; panel, windows; subpanel .. _panels-and-subpanels: Panels and Subpanels ```````````````````` Both dialog boxes and wizards are secondary windows that appear separately from the main program display, if any. Often, however, you might need to create a window element that is embedded in a larger display. For such cases, the **kind** of the corresponding View object should be 'panel' or 'subpanel '. A :term:`panel` is very similar to a window, except that it is embedded in a larger window, which need not be a TraitsUI window. Like windows, panels support the **buttons** View attribute, as well as any menus and toolbars that are specified for the View (see :ref:`menus-and-menu-bars`). Panels are always live and nonmodal. A :term:`subpanel` is almost identical to a panel. The only difference is that subpanels do not display :term:`command button`\ s even if the View specifies them. .. Do subpanels support menus and toolbars? If not, add this to the documentation. (If so, why do they?) .. index:: buttons; attribute .. _command-buttons-the-buttons-attribute: Command Buttons: the **buttons** Attribute ------------------------------------------ A common feature of many windows is a row of command buttons along the bottom of the frame. These buttons have a fixed position outside any scrolled panels in the window, and are thus always visible while the window is displayed. They are usually used for window-level commands such as committing or cancelling the changes made to the form data, or displaying a help window. In TraitsUI, these command buttons are specified by means of the View object's **buttons** attribute, whose value is a list of buttons to display. [6]_ Consider the following variation on Example 3: .. index:: pair: examples; buttons .. _example-4-using-a-view-object-with-buttons: .. rubric:: Example 4: Using a View object with buttons .. literalinclude:: examples/configure_traits_view_buttons.py :start-at: configure_traits_view_buttons.py The resulting window has the same content as before, but now two buttons are displayed at the bottom: :guilabel:`OK` and :guilabel:`Cancel`: .. figure:: images/ui_for_ex4.jpg :alt: Dialog box with three fields and OK and Cancel buttons. Figure 4: User interface for Example 4 There are six standard buttons defined by TraitsUI. Each of the standard buttons has matching a string alias. You can either import and use the button names, or simply use their aliases: .. index:: buttons; standard, UndoButton, ApplyButton, RevertButton, OKButton .. index:: CancelButton .. _command-button-aliases-table: .. rubric:: Command button aliases +--------------+---------------------------+ |Button Name |Button Alias | +==============+===========================+ |UndoButton |'Undo' | +--------------+---------------------------+ |ApplyButton |'Apply' | +--------------+---------------------------+ |RevertButton |'Revert' | +--------------+---------------------------+ |OKButton |'OK' (case sensitive!) | +--------------+---------------------------+ |CancelButton |'Cancel' | +--------------+---------------------------+ Alternatively, there are several pre-defined button lists that can be imported from traitsui.menu and assigned to the buttons attribute: .. index:: OKCancelsButtons, ModalButtons, LiveButtons * OKCancelButtons = ``[OKButton, CancelButton]`` * ModalButtons = ``[ApplyButton, RevertButton, OKButton, CancelButton, HelpButton]`` * LiveButtons = ``[UndoButton, RevertButton, OKButton, CancelButton, HelpButton]`` Thus, one could rewrite the lines in Example 4 as follows, and the effect would be exactly the same:: from traitsui.menu import OKCancelButtons ... buttons=OKCancelButtons, ) .. index:: NoButtons The special constant NoButtons can be used to create a window or panel without command buttons. While this is the default behavior, NoButtons can be useful for overriding an explicit value for **buttons**. You can also specify ``buttons = []`` to achieve the same effect. Setting the **buttons** attribute to an empty list has the same effect as not defining it at all. It is also possible to define custom buttons and add them to the **buttons** list; see :ref:`custom-command-buttons` for details. .. index:: View; attributes, attributes; View .. _other-view-attributes: Other View Attributes --------------------- .. index:: dock attribute; View, height attribute; View, icon attribute .. index:: image attribute; View, resizable attribute; View .. index:: scrollable attribute, statusbar attribute, style attribute; View .. index:: title attribute, width attribute; View, x attribute, y attribute .. _attributes-of-view-by-category-table: .. rubric:: Attributes of View, by category Window display These attributes control the visual properties of the window itself, regardless of its content. dock: {'fixed', 'horizontal', 'vertical', 'tabbed'} The default docking style to use for sub-groups of the view. The following values are possible: * 'fixed': No rearrangement of sub-groups is allowed. * 'horizontal': Moveable elements have a visual "handle" to the left by which the element can be dragged. * 'vertical': Moveable elements have a visual "handle" above them by which the element can be dragged. * 'tabbed': Moveable elements appear as tabbed pages, which can be arranged within the window or "stacked" so that only one appears at at a time. height: int or float Requested height for the view window, as an (integer) number of pixels, or as a (floating point) fraction of the screen height. icon: str The name of the icon to display in the dialog window title bar. image: Image The image to display on notebook tabs. resizable: bool Can the user resize the window? scrollable: bool Can the user scroll the view? If set to True, window-level scroll bars appear whenever the window is too small to show all of its contents at one time. If set to False, the window does not scroll, but individual widgets might still contain scroll bars. statusbar: Status bar items to add to the view's status bar. The value can be: - **None**: No status bar for the view (the default). - string: Same as ``[StatusItem(name=string)]``. - StatusItem: Same as ``[StatusItem]``. - ``[[StatusItem|string], ... ]``: Create a status bar with one field for each StatusItem in the list (or tuple). The status bar fields are defined from left to right in the order specified. A string value is converted to: ``StatusItem(name=string)``: style: The default editor style of elements in the view. title: str Title for the view, displayed in the title bar when the view appears as a secondary window (i.e., dialog or wizard). If not specified, "Edit properties" is used as the title. width: int or float Requested width for the view window, as an (integer) number of pixels, or as a (floating point) fraction of the screen width. x, y: int or float The requested x and y coordinates for the window (positive for top/left, negative for bottom/right, either pixels or proportions) .. index:: close_result attribute .. index:: handler attribute .. index:: key_bindings attribute .. index:: menubar attribute .. index:: model_view attribute .. index:: on_apply attribute .. index:: toolbar attribute .. index:: updated attribute .. index:: content attribute; View .. index:: drop_class attribute Command TraitsUI menus and toolbars are generally implemented in conjunction with custom :term:`Handler`\ s; see :ref:`menus-and-menu-bars` for details. buttons: List of button actions to add to the view. The **traitsui.menu** module defines standard buttons, such as **OKButton**, and standard sets of buttons, such as **ModalButtons**, which can be used to define a value for this attribute. This value can also be a list of button name strings, such as ``['OK', 'Cancel', 'Help']``. If set to the empty list, the view contains a default set of buttons (equivalent to **LiveButtons**: Undo/Redo, Revert, OK, Cancel, Help). To suppress buttons in the view, use the **NoButtons** variable, defined in **traitsui.menu**. close_result: What result should be returned if the user clicks the window or dialog close button or icon? handler: The Handler object that provides GUI logic for handling events in the window. Set this attribute only if you are using a custom Handler. If not set, the default Traits UI Handler is used. key_bindings: The set of global key bindings for the view. Each time a key is pressed while the view has keyboard focus, the key is checked to see if it is one of the keys recognized by the KeyBindings object. menubar: The menu bar for the view. Usually requires a custom **handler**. model_view: The factory function for converting a model into a model/view object. on_apply: Called when modal changes are applied or reverted. toolbar: The toolbar for the view. Usually requires a custom **handler**. updated: Event Event when the view has been updated. .. index:: export attribute; View .. index:: imports attribute .. index:: object attribute; View Content The **imports** and **drop_class** attributes control what objects can be dragged and dropped on the view. content: The top-level Group object for the view. drop_class: Class of dropped objects that can be added. export: The category of exported elements. imports: The valid categories of imported elements. object: The default object being edited. .. index:: help attribute; View .. index:: help_id attribute; View User help help: (deprecated) The **help** attribute is a deprecated way to specify that the View has a Help button. Use the buttons attribute instead (see :ref:`command-buttons-the-buttons-attribute` for details). help_id: The **help_id** attribute is not used by Traits, but can be used by a custom help handler. .. index:: id attribute; View Unique id: The **id** attribute is used as a key to save user preferences about a view, such as customized size and position, so that they are restored the next time the view is opened. The value of **id** must be unique across all Traits-based applications on a system. If no value is specified, no user preferences are saved for the view. .. rubric:: Footnotes .. [6] Actually, the value of the **buttons** attribute is really a list of Action objects, from which GUI buttons are generated by TraitsUI. The Action class is described in :ref:`actions`. ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1684768028.0158074 traitsui-8.0.0/docs/source/traitsui_user_manual/examples/0000755000175100001730000000000000000000000024472 5ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/docs/source/traitsui_user_manual/examples/configure_traits_view.py0000644000175100001730000000155300000000000031451 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # configure_traits_view.py -- Sample code to demonstrate configure_traits() from traits.api import HasTraits, Int, Str from traitsui.api import Item, View class SimpleEmployee(HasTraits): first_name = Str() last_name = Str() department = Str() employee_number = Str() salary = Int() view1 = View( Item(name='first_name'), Item(name='last_name'), Item(name='department'), ) sam = SimpleEmployee() sam.configure_traits(view=view1) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/docs/source/traitsui_user_manual/examples/configure_traits_view_buttons.py0000644000175100001730000000166400000000000033232 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # configure_traits_view_buttons.py -- Sample code to demonstrate # configure_traits() from traits.api import HasTraits, Str, Int from traitsui.api import CancelButton, Item, OKButton, View class SimpleEmployee(HasTraits): first_name = Str() last_name = Str() department = Str() employee_number = Str() salary = Int() view1 = View( Item(name='first_name'), Item(name='last_name'), Item(name='department'), buttons=[OKButton, CancelButton], ) sam = SimpleEmployee() sam.configure_traits(view=view1) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/docs/source/traitsui_user_manual/examples/configure_traits_view_group.py0000644000175100001730000000172600000000000032667 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # configure_traits_view_group.py -- Sample code to demonstrate # configure_traits() from traits.api import HasTraits, Int, Str from traitsui.api import Group, Item, View class SimpleEmployee(HasTraits): first_name = Str() last_name = Str() department = Str() employee_number = Str() salary = Int() view1 = View( Group( Item(name='first_name'), Item(name='last_name'), Item(name='department'), label='Personnel profile', show_border=True, ), ) sam = SimpleEmployee() sam.configure_traits(view=view1) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/docs/source/traitsui_user_manual/examples/default_trait_editors.py0000644000175100001730000000172600000000000031432 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # default_trait_editors.py -- Example of using default trait editors from traits.api import Bool, HasTraits, Range, Str from traitsui.api import Item, View class Adult(HasTraits): first_name = Str() last_name = Str() age = Range(21, 99) registered_voter = Bool() traits_view = View( Item(name='first_name'), Item(name='last_name'), Item(name='age'), Item(name='registered_voter'), ) alice = Adult( first_name='Alice', last_name='Smith', age=42, registered_voter=True, ) alice.configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/docs/source/traitsui_user_manual/examples/default_traits_view.py0000644000175100001730000000176400000000000031120 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # default_traits_view.py -- Sample code to demonstrate the use of 'traits_view' from traits.api import HasTraits, Int, Str from traitsui.api import Group, Item, View class SimpleEmployee2(HasTraits): first_name = Str() last_name = Str() department = Str() employee_number = Str() salary = Int() traits_view = View( Group( Item(name='first_name'), Item(name='last_name'), Item(name='department'), label='Personnel profile', show_border=True, ), ) sam = SimpleEmployee2() sam.configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/docs/source/traitsui_user_manual/examples/default_traits_view2.py0000644000175100001730000000207700000000000031200 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # default_traits_view2.py -- Sample code to demonstrate the use of # 'default_traits_view' from traits.api import HasTraits, Str, Int from traitsui.api import View, Item, Group class SimpleEmployee2(HasTraits): first_name = Str() last_name = Str() department = Str() employee_number = Str() salary = Int() def default_traits_view(self): return View( Group( Item(name='first_name'), Item(name='last_name'), Item(name='department'), label='Personnel profile', show_border=True, ), ) sam = SimpleEmployee2() sam.configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/docs/source/traitsui_user_manual/examples/enum_editor.py0000644000175100001730000000204100000000000027353 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # enum_editor.py -- Example of using an enumeration editor from traits.api import Enum, HasTraits from traitsui.api import EnumEditor, Item, View class EnumExample(HasTraits): priority = Enum('Medium', 'Highest', 'High', 'Low', 'Lowest') view = View( Item( name='priority', editor=EnumEditor( values={ 'Highest': '1:Highest', 'High': '2:High', 'Medium': '3:Medium', 'Low': '4:Low', 'Lowest': '5:Lowest', } ), ), ) EnumExample().configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/docs/source/traitsui_user_manual/examples/handler_override.py0000644000175100001730000000222700000000000030363 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # handler_override.py -- Example of a Handler that overrides setattr(), and # that has a user interface notification method from traits.api import Bool, HasTraits from traitsui.api import Handler, View class TC_Handler(Handler): def setattr(self, info, object, name, value): Handler.setattr(self, info, object, name, value) info.object._updated = True def object__updated_changed(self, info): if info.initialized: info.ui.title += "*" class TestClass(HasTraits): b1 = Bool() b2 = Bool() b3 = Bool() _updated = Bool(False) view1 = View( 'b1', 'b2', 'b3', title="Alter Title", handler=TC_Handler(), buttons=['OK', 'Cancel'], ) tc = TestClass() tc.configure_traits(view=view1) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/docs/source/traitsui_user_manual/examples/instance_editor_selection.py0000644000175100001730000000327700000000000032274 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # instance_editor_selection.py -- Example of an instance editor with instance # selection from traits.api import HasStrictTraits, Instance, Int, List, Regex, Str from traitsui.api import InstanceEditor, Item, View class Person(HasStrictTraits): name = Str() age = Int() phone = Regex(value='000-0000', regex=r'\d\d\d[-]\d\d\d\d') traits_view = View('name', 'age', 'phone') people = [ Person(name='Dave', age=39, phone='555-1212'), Person(name='Mike', age=28, phone='555-3526'), Person(name='Joe', age=34, phone='555-6943'), Person(name='Tom', age=22, phone='555-7586'), Person(name='Dick', age=63, phone='555-3895'), Person(name='Harry', age=46, phone='555-3285'), Person(name='Sally', age=43, phone='555-8797'), Person(name='Fields', age=31, phone='555-3547'), ] class Team(HasStrictTraits): name = Str() captain = Instance(Person) roster = List(Person) traits_view = View( Item('name'), Item('_'), Item( 'captain', label='Team Captain', editor=InstanceEditor(name='roster', editable=True), style='custom', ), buttons=['OK'], ) if __name__ == '__main__': team = Team(name='Vultures', captain=people[0], roster=people) team.configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/docs/source/traitsui_user_manual/examples/key_bindings.py0000644000175100001730000000373300000000000027517 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # key_bindings.py -- Example of a code editor with a key bindings editor from traits.api import Button, Code, HasPrivateTraits, observe, Str from traitsui.api import Group, Handler, Item, View from traitsui.key_bindings import KeyBinding, KeyBindings key_bindings = KeyBindings( KeyBinding( binding1='Ctrl-s', description='Save to a file', method_name='save_file', ), KeyBinding( binding1='Ctrl-r', description='Run script', method_name='run_script', ), KeyBinding( binding1='Ctrl-k', description='Edit key bindings', # special method name handled internally method_name='edit_bindings', ), ) class CodeHandler(Handler): """Handler class for bound methods.""" def save_file(self, info): info.object.status = "save file" def run_script(self, info): info.object.status = "run script" class KBCodeExample(HasPrivateTraits): code = Code() status = Str() kb = Button(label='Edit Key Bindings') view = View( Group( Item('code', style='custom', resizable=True), Item('status', style='readonly'), 'kb', orientation='vertical', show_labels=False, ), id='KBCodeExample', key_bindings=key_bindings, title='Code Editor With Key Bindings', resizable=True, handler=CodeHandler(), ) @observe('kb') def _edit_key_bindings(self, event): key_bindings.edit() if __name__ == '__main__': KBCodeExample().configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/docs/source/traitsui_user_manual/examples/mixed_styles.py0000644000175100001730000000225100000000000027555 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # mixed_styles.py -- Example of using editor styles at various levels from traits.api import Enum, HasTraits, Str from traitsui.api import Group, Item, View class MixedStyles(HasTraits): first_name = Str() last_name = Str() department = Enum("Business", "Research", "Admin") position_type = Enum("Full-Time", "Part-Time", "Contract") traits_view = View( Group( Item(name='first_name'), Item(name='last_name'), Group( Item(name='department'), Item(name='position_type', style='custom'), style='simple', ), ), title='Mixed Styles', style='readonly', ) ms = MixedStyles(first_name='Sam', last_name='Smith') ms.configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/docs/source/traitsui_user_manual/examples/multi_object_view.py0000644000175100001730000000310000000000000030550 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # multi_object_view.py -- Sample code to show multi-object view with context from traits.api import Bool, HasTraits, Int, Str from traitsui.api import Group, Item, View class House(HasTraits): address = Str() bedrooms = Int() pool = Bool() price = Int() # View object designed to display two objects of class 'House' comp_view = View( Group( Group( Item('h1.address', resizable=True), Item('h1.bedrooms'), Item('h1.pool'), Item('h1.price'), show_border=True, ), Group( Item('h2.address', resizable=True), Item('h2.bedrooms'), Item('h2.pool'), Item('h2.price'), show_border=True, ), orientation='horizontal', ), title='House Comparison', ) # A pair of houses to demonstrate the View house1 = House( address='4743 Dudley Lane', bedrooms=3, pool=False, price=150000, ) house2 = House( address='11604 Autumn Ridge', bedrooms=3, pool=True, price=200000, ) # ...And the actual display command house1.configure_traits(view=comp_view, context={'h1': house1, 'h2': house2}) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/docs/source/traitsui_user_manual/examples/multiple_views.py0000644000175100001730000000252100000000000030114 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # multiple_views.py -- Sample code to demonstrate the use of multiple views from traits.api import HasTraits, Str, Int from traitsui.api import View, Item, Group class SimpleEmployee3(HasTraits): first_name = Str() last_name = Str() department = Str() employee_number = Str() salary = Int() traits_view = View( Group( Item(name='first_name'), Item(name='last_name'), Item(name='department'), label='Personnel profile', show_border=True, ), ) all_view = View( Group( Item(name='first_name'), Item(name='last_name'), Item(name='department'), Item(name='employee_number'), Item(name='salary'), label='Personnel database entry', show_border=True, ), ) sam = SimpleEmployee3() sam.configure_traits() sam.configure_traits(view='all_view') ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/docs/source/traitsui_user_manual/examples/tree_editor.py0000644000175100001730000001156100000000000027355 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # tree_editor.py -- Example of a tree editor from traits.api import HasTraits, Instance, List, Str, Regex from traitsui.api import ( Action, Group, Handler, HGroup, Item, Menu, Separator, TreeEditor, TreeNode, View, VSplit, ) from traitsui.editors.tree_editor import ( NewAction, CopyAction, CutAction, PasteAction, DeleteAction, RenameAction, ) class Employee(HasTraits): name = Str('') title = Str() phone = Regex(regex=r'\d\d\d-\d\d\d\d') def default_title(self): self.title = 'Senior Engineer' class Department(HasTraits): name = Str('') employees = List(Employee) class Company(HasTraits): name = Str('') departments = List(Department) employees = List(Employee) class Owner(HasTraits): name = Str('') company = Instance(Company) jason = Employee(name='Jason', title='Engineer', phone='536-1057') mike = Employee(name='Mike', title='Sr. Marketing Analyst', phone='536-1057') dave = Employee(name='Dave', title='Sr. Engineer', phone='536-1057') susan = Employee(name='Susan', title='Engineer', phone='536-1057') betty = Employee(name='Betty', title='Marketing Analyst') owner = Owner( name='wile', company=Company( name='Acme Labs, Inc.', departments=[ Department(name='Marketing', employees=[mike, betty]), Department(name='Engineering', employees=[dave, susan, jason]), ], employees=[dave, susan, mike, betty, jason], ), ) # View for objects that aren't edited no_view = View() # Actions used by tree editor context menu def_title_action = Action(name='Default title', action='object.default') dept_action = Action( name='Department', action='handler.employee_department(editor,object)', ) # View used by tree editor employee_view = View( VSplit( HGroup('3', 'name'), HGroup('9', 'title'), HGroup('phone'), id='vsplit', ), id='traits.doc.example.treeeditor', dock='vertical', ) class TreeHandler(Handler): def employee_department(self, editor, object): dept = editor.get_parent(object) print(f'{object.name} works in the {dept.name} department.') tree_editor = TreeEditor( nodes=[ TreeNode( node_for=[Company], auto_open=True, children='', label='name', view=View(Group('name', orientation='vertical', show_left=True)), ), TreeNode( node_for=[Company], auto_open=True, children='departments', label='=Departments', view=no_view, add=[Department], ), TreeNode( node_for=[Company], auto_open=True, children='employees', label='=Employees', view=no_view, add=[Employee], ), TreeNode( node_for=[Department], auto_open=True, children='employees', label='name', menu=Menu( NewAction, Separator(), DeleteAction, Separator(), RenameAction, Separator(), CopyAction, CutAction, PasteAction, ), view=View(Group('name', orientation='vertical', show_left=True)), add=[Employee], ), TreeNode( node_for=[Employee], auto_open=True, label='name', menu=Menu( NewAction, Separator(), def_title_action, dept_action, Separator(), CopyAction, CutAction, PasteAction, Separator(), DeleteAction, Separator(), RenameAction, ), view=employee_view, ), ], ) # The main view view = View( Group( Item(name='company', id='company', editor=tree_editor, resizable=True), orientation='vertical', show_labels=True, show_left=True, ), title='Company Structure', id='traitsui.tests.tree_editor_test', dock='horizontal', drop_class=HasTraits, handler=TreeHandler(), buttons=['Undo', 'OK', 'Cancel'], resizable=True, width=0.3, height=0.3, ) if __name__ == '__main__': owner.configure_traits(view=view) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/docs/source/traitsui_user_manual/examples/wizard.py0000644000175100001730000000211000000000000026336 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # wizard.py -- Example of a traits-based wizard UI from traits.api import HasTraits, Str from traits.etsconfig.api import ETSConfig from traitsui.api import Item, View, VGroup class Person(HasTraits): first_name = Str() last_name = Str() company = Str() position = Str() view = View( VGroup(Item("first_name"), Item("last_name")), VGroup(Item("company"), Item("position")), ) person = Person( first_name='Postman', last_name='Pat', company="Enthought", position="Software Developer", ) if ETSConfig.toolkit == "wx": # Wizard window is currently only available for wx backend. person.configure_traits(kind='wizard') ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/docs/source/traitsui_user_manual/factories_advanced_extra.rst0000644000175100001730000012700200000000000030417 0ustar00runnerdocker00000000000000 .. _advanced-trait-editors: Advanced Trait Editors ---------------------- The editor factories described in the following sections are more advanced than those in the previous section. In some cases, they require writing additional code; in others, the editors they generate are intended for use in complex user interfaces, in conjunction with other editors. CustomEditor() `````````````` :Suitable for: Special cases :Default for: (none) :Required parameters: *factory* :Optional parameters: *args* Use CustomEditor() to create an "editor" that is a non-Traits-based custom control. The *factory* parameter must be a function that generates the custom control. The function must have the following signature: factory_function(*window_parent*, *editor*[, \*\ *args*, \*\*\ *kwargs*]) - *window_parent*: The parent window for the control - *editor*: The editor object created by CustomEditor() Additional arguments, if any, can be passed as a tuple in the *args* parameter of CustomEditor(). DropEditor() ```````````` :Suitable for: Instance traits :Default for: (none) :Optional parameters: *binding*, *klass*, *readonly* DropEditor() generates an editor that is a text field containing a string representation of the trait attribute's value. The user can change the value assigned to the attribute by dragging and dropping an object on the text field, for example, a node from a tree editor (See :ref:`treeeditor`). If the *readonly* parameter is True (the default), the user cannot modify the value by typing in the text field. You can restrict the class of objects that can be dropped on the editor by specifying the *klass* parameter. You can specify that the dropped object must be a binding (enthought.naming.api.Binding) by setting the *binding* parameter to True. If so, the bound object is retrieved and checked to see if it can be assigned to the trait attribute. If the dropped object (or the bound object associated with it) has a method named drop_editor_value(), it is called to obtain the value to assign to the trait attribute. Similarly, if the object has a method named drop_editor_update(), it is called to update the value displayed in the text editor. This method requires one parameter, which is the GUI control for the text editor. DNDEditor() ``````````` :Suitable for: Instance traits :Default for: (none) :Optional parameters: *drag_target, drop_target, image* DNDEditor() generates an editor that represents a file or a HasTraits instance as an image that supports dragging and dropping. Depending on the editor style, the editor can be a *drag source* (the user can set the value of the trait attribute by dragging a file or object onto the editor, for example, from a tree editor), or *drop target* (the user can drag from the editor onto another target). .. _drag-and-drop-editor-style-variations-table: .. rubric:: Table 9: Drag-and-drop editor style variations ============ ============ ============ Editor Style Drag Source? Drop Target? ============ ============ ============ Simple Yes Yes Custom No Yes Read-only Yes No ============ ============ ============ KeyBindingEditor() `````````````````` The KeyBindingEditor() factory differs from other trait editor factories because it generates an editor, not for a single attribute, but for an object of a particular class, traitsui.key_bindings.KeyBindings. A KeyBindings object is a list of bindings between key codes and handler methods. You can specify a KeyBindings object as an attribute of a View. When the user presses a key while a View has input focus, the user interface searches the View for a KeyBindings that contains a binding that corresponds to the key press; if such a binding does not exist on the View, it searches enclosing Views in order, and uses the first matching binding, if any. If it does not find any matching bindings, it ignores the key press. A key binding editor is a separate :term:`dialog box` that displays the string representation of each key code and a description of the corresponding method. The user can click a text box, and then press a key or key combination to associate that key press with a method. .. figure:: images/key_binding_editor.jpg :alt: Dialog box with fields for key presses corresponding to operations Figure 50: Key binding editor dialog box The following code example creates a user interface containing a code editor with associated key bindings, and a button that invokes the key binding editor. .. _example-17-code-editor-with-key-binding-editor: .. rubric:: Example 17: Code editor with key binding editor .. literalinclude:: examples/key_bindings.py :start-at: key_bindings.py .. _tableeditor: TableEditor() ````````````` :Suitable for: List(*InstanceType*) :Default for: (none) :Required parameters: *columns* or *columns_name* :Optional parameters: See *Traits API Reference*, traitsui.wx.table_editor.ToolkitEditorFactory attributes. TableEditor() generates an editor that displays instances in a list as rows in a table, with attributes of the instances as values in columns. You must specify the columns in the table. Optionally, you can provide filters for filtering the set of displayed items, and you can specify a wide variety of options for interacting with and formatting the table. .. figure:: images/TableEditor_demo.png :alt: Table editor with toolbar and instance editor Figure 51: Table editor To see the code that results in Figure 51, refer to :file:`TableEditor_demo.py` in the :file:`demos/TraitsUI Demo/Standard Editors` subdirectory of the Traits UI package. This example demonstrates object columns, expression columns, filters, searching, and adding and deleting rows. The parameters for TableEditor() can be grouped in several broad categories, described in the following sections. - :ref:`specifying-columns` - :ref:`managing-items` - :ref:`editing-the-table` - :ref:`defining-the-layout` - :ref:`table-defining-the-format` - :ref:`other-user-interactions` .. _specifying-columns: Specifying Columns :::::::::::::::::: You must provide the TableEditor() factory with a list of columns for the table. You can specify this list directly, as the value of the *columns* parameter, or indirectly, in an extended context attribute referenced by the *columns_name* parameter. The items in the list must be instances of traitsui.api.TableColumn, or of a subclass of TableColumn. Some subclasses of TableColumn that are provided by the TraitsUI package include ObjectColumn, ListColumn, NumericColumn, ExpressionColumn, CheckboxColumn and ProgressColumn. (See the *Traits API Reference* for details about these classes.) In practice, most columns are derived from one of these subclasses, rather than from TableColumn. For the usual case of editing trait attributes on objects in the list, use ObjectColumn. You must specify the *name* parameter to the ObjectColumn() constructor, referencing the name of the trait attribute to be edited. You can specify additional columns that are not initially displayed using the *other_columns* parameter. If the *configurable* parameter is True (the default), a :guilabel:`Set user preferences for table` icon (|preferences_icon|) appears on the table's toolbar. When the user clicks this icon, a dialog box opens that enables the user to select and order the columns displayed in the table, as shown in Figure 52. (The dialog box is implemented using a set editor; see :ref:`seteditor`.) Any columns that were specified in the *other_columns* parameter are listed in the left list box of this dialog box, and can be displayed by moving them into the right list box. .. |preferences_icon| image:: images/table_prefs.png .. figure:: images/table_column_selection.jpg :alt: Dialog box with two list boxes for selecting column names Figure 52: Column selection dialog box for a table editor .. _managing-items: Managing Items :::::::::::::: Table editors support several mechanisms to help users locate items of interest. .. _organizing-items: Organizing Items ~~~~~~~~~~~~~~~~ Table editors provide two mechanisms for the user to organize the contents of a table: sorting and reordering. The user can sort the items based on the values in a column, or the user can manually order the items. Usually, only one of these mechanisms is used in any particular table, although the TraitsUI package does not enforce a separation. If the user has manually ordered the items, sorting them would throw away that effort. If the *reorderable* parameter is True, :guilabel:`Move up` (|move_up_icon|) and :guilabel:`Move down` (|move_down_icon|) icons appear in the table toolbar. Clicking one of these icons changes the position of the selected item. .. |move_up_icon| image:: images/move_up_icon.png .. |move_down_icon| image:: images/move_down_icon.png If the *sortable* parameter is True (the default), then the user can sort the items in the table based on the values in a column by Control-clicking the header of that column. - On the first click, the items are sorted in ascending order. The characters :guilabel:`>>` appear in the column header to indicate that the table is sorted ascending on this column's values. - On the second click, the items are sorted descending order. The characters :guilabel:`<<` appear in the column header to indicate that the table is sorted descending on this column's values. - On the third click, the items are restored to their original order, and the column header is undecorated. If the *sort_model* parameter is true, the items in the list being edited are sorted when the table is sorted. The default value is False, in which case, the list order is not affected by sorting the table. If *sortable* is True and *sort_model* is False, then a :guilabel:`Do not sort columns` icon (|no_sort_icon|) appears in the table toolbar. Clicking this icon restores the original sort order. .. |no_sort_icon| image:: images/no_sort_icon.png If the *reverse* parameter is True, then the items in the underlying list are maintained in the reverse order of the items in the table (regardless of whether the table is sortable or reorderable). .. _filtering-and-searching: Filtering and Searching ~~~~~~~~~~~~~~~~~~~~~~~ You can provide an option for the user to apply a filter to a table, so that only items that pass the filter are displayed. This feature can be very useful when dealing with lengthy lists. You can specify a filter to apply to the table either directly, or via another trait. Table filters must be instances of traitsui.api.TableFilter, or of a subclass of TableFilter. Some subclasses of TableFilter that are provided by the TraitsUI package include EvalTableFilter, RuleTableFilter, and MenuTableFilter. (See the *Traits API Reference* for details about these classes.) The TraitsUI package also provides instances of these filter classes as "templates", which cannot be edited or deleted, but which can be used as models for creating new filters. .. TODO: Provide more detail on how these filters work. The *filter* parameter specifies a filter that is applied to the table when it is first displayed. The *filter_name* parameter specifies an extended trait name for a trait that is either a table filter object or a callable that accepts an object and returns True if the object passes the filter criteria, or false if it does not. You can use *filter_name* to embed a view of a table filter in the same view as its table. You can specify use the *filters* parameter to specify a list of table filters that are available to apply to a table. When *filters* is specified, a drop-down list box appears in the table toolbar, containing the filters that are available for the user to apply. When the user selects a filter, it is automatically applied to the table. A status message to the right of the filters list indicates what subset of the items in the table is currently displayed. A special item in the filter list, named :guilabel:`Customize`, is always provided; clicking this item opens a dialog box that enables the user to create new filters, or to edit or delete existing filters (except templates). You can also provide an option for the user to use filters to search the table. If you set the *search* parameter to an instance of TableFilter (or of a subclass), a :guilabel:`Search table` icon (|search_table_icon|) appears on the table toolbar. Clicking this icon opens a :guilabel:`Search for` dialog box, which enables the user to specify filter criteria, to browse through matching items, or select all matching items. .. |search_table_icon| image:: images/search_table_icon.png .. TODO: Add a screenshot of the dialog when it actually works .. _interacting-with-items: Interacting with Items ~~~~~~~~~~~~~~~~~~~~~~ As the user clicks in the table, you may wish to enable certain program behavior. The value of the *selection_mode* parameter specifies how the user can make selections in the grid: - ``cell``: A single cell at a time - ``cells``: Multiple cells - ``column``: A single column at a time - ``columns``: Multiple columns - ``row``: A single row at a time - ``rows``: Multiple rows You can use the *selected* parameter to specify the name of a trait attribute in the current context to synchronize with the user's current selection. For example, you can enable or disable menu items or toolbar icons depending on which item is selected. The synchronization is two-way; you can set the attribute referenced by *selected* to force the table to select a particular item. You can use the *selected_indices* parameter to specify the name of a trait attribute in the current context to synchronize with the indices of the table editor selection. The content of the selection depends on the *selection_mode* value: - ``cell``: The selection is a tuple of the form (*object*, *column_name*), where *object* is the object contains the selected cell, and *column_name* is the name of the column the cell is in. If there is no selection, the tuple is (None, ''). - ``cells``: The selection is a list of tuples of the form (*object*, *column_name*), with one tuple for each selected cell, in order from top to bottom and left to right. If there is no selection, the list is empty. - ``column``: The selection is the name of the selected column, or the empty string if there is no selection. - ``columns``: The selection is a list containing the names of the selected columns, in order from left to right. If there is no selection, the list is empty. - ``row``: The selection is either the selected object or None if nothing is selected in the table. - ``rows``: The selection is a list of the selected objects, in ascending row order. If there is no selection, the list is empty. The *on_select* and *on_dclick* parameters are callables to invoke when the user selects or double-clicks an item, respectively. You can define a shortcut menu that opens when the user right-clicks an item. Use the *menu* parameter to specify a TraitsUI or Pyface Menu, containing Action objects for the menu commands. .. _editing-the-table: Editing the Table ::::::::::::::::: The Boolean *editable* parameter controls whether the table or its items can be modified in any way. This parameter defaults to True, except when the style is 'readonly'. Even when the table as a whole is editable, you can control whether individual columns are editable through the **editable** attribute of TableColumn. .. _adding-items: Adding Items ~~~~~~~~~~~~ To enable users to add items to the table, specify as the *row_factory* parameter a callable that generates an object that can be added to the list in the table; for example, the class of the objects in the table. When *row_factory* is specified, an :guilabel:`Insert new item` icon (|insert_item_icon|) appears in the table toolbar, which generates a new row in the table. Optionally, you can use *row_factory_args* and *row_factory_kw* to specify positional and keyword arguments to the row factory callable. .. |insert_item_icon| image:: images/insert_item_icon.png To save users the trouble of mousing to the toolbar, you can enable them to add an item by selecting the last row in the table. To do this, set *auto_add* to True. In this case, the last row is blank until the user sets values. Pressing Enter creates the new item and generates a new, blank last row. .. deleting-items: Deleting Items ~~~~~~~~~~~~~~ The *deletable* parameter controls whether items can be deleted from the table. This parameter can be a Boolean (defaulting to False) or a callable; the callable must take an item as an argument and handle deleting it. If *deletable* is not False, a :guilabel:`Delete current item` icon (|delete_item_icon|) appears on the table toolbar; clicking it deletes the item corresponding to the row that is selected in the table. .. |delete_item_icon| image:: images/delete_item_icon.png .. _modifying-items: Modifying Items ~~~~~~~~~~~~~~~ The user can modify items in two ways. - For columns that are editable, the user can change an item's value directly in the table. The editor used for each attribute in the table is the simple style of editor for the corresponding trait. - Alternatively, you can specify a View for editing instances, using the *edit_view* parameter. The resulting user interface appears in a :term:`subpanel` to the right or below the table (depending on the *orientation* parameter). You can specify a handler to use with the view, using *edit_view_handler*. You can also specify the subpanel's height and width, with *edit_view_height* and *edit_view_width*. .. _defining-the-layout: Defining the Layout ::::::::::::::::::: Some of the parameters for the TableEditor() factory affect global aspects of the display of the table. - *auto_size*: If True, the cells of the table automatically adjust to the optimal size based on their contents. - *orientation*: The layout of the table relative to its associated editor pane. Can be 'horizontal' or 'vertical'. - *rows*: The number of visible rows in the table. - *show_column_labels*: If True (the default), displays labels for the columns. You can specify the labels to use in the column definitions; otherwise, a "user friendly" version of the trait attribute name is used. - *show_toolbar*: If False, the table toolbar is not displayed, regardless of whether other settings would normally create a toolbar. The default is True. .. _table-defining-the-format: Defining the Format ::::::::::::::::::: The TableEditor() factory supports a variety of parameters to control the visual formatting of the table, such as colors, fonts, and sizes for lines, cells, and labels. For details, refer to the *Traits API Reference*, traitsui.wx.table_editor.ToolkitEditorFactory attributes. You can also specify formatting options for individual table columns when you define them. .. _other-user-interactions: Other User Interactions ::::::::::::::::::::::: The table editor supports additional types of user interaction besides those controlled by the factory parameters. - Column dragging: The user can reorganize the column layout of a table editor by clicking and dragging a column label to its new location. If you have enabled user preferences for the view and table editor (by specifying view and item IDs), the new column layout is persisted across user sessions. - Column resizing: The user can resize a column by dragging the column separator (in one of the data rows) to a new position. Because of the column-dragging support, clicking the column separator in the column label row does not work. - Data dragging: The user can drag the contents of any cell by clicking and dragging. TabularEditor() ``````````````` :Suitable for: lists, arrays, and other large sequences of objects :Default for: (none) :Required parameters: *adapter* :Optional parameters: *activated, clicked, column_clicked, dclicked, drag_move, editable,* *horizontal_lines, images, multi_select, operations, right_clicked,* *right_dclicked, scroll_to_position_hint, selected, selected_row, *show_titles, vertical_lines* The TabularEditor() factory can be used for many of the same purposes as the TableEditor() factory, that is, for displaying a table of attributes of lists or arrays of objects. While similar in function, the tabular editor has advantages and disadvantages relative to the table editor. .. _tabular-advantages: Advantages :::::::::: - **Very fast**: The tabular editor uses a virtual model, which accesses data from the underlying model only as needed. For example, if you have a million-element array, but can display only 50 rows at a time, the editor requests only 50 elements of data at a time. - **Very flexible data model**: The editor uses an adapter model to interface with the underlying data. This strategy allows it to easily deal with many types of data representation, from list of objects, to arrays of numbers, to tuples of tuples, and many other formats. - **Supports useful data operations**, including: - Moving the selection up and down using the keyboard arrow keys. - Moving rows up and down using the keyboard. - Inserting and deleting items using the keyboard. - Initiating editing of items using the keyboard. - Dragging and dropping of table items to and from the editor, including support for both copy and move operations for single and multiple items. - **Visually appealing**: The tabular editor, in general, uses the underlying operating system's native table or grid control, and as a result often looks better than the control used by the table editor. - **Supports displaying text and images in any cell**. However, the images displayed must be all the same size for optimal results. .. _tabular-disadvantages: Disadvantages ::::::::::::: - **Not as full-featured**: The table editor includes support for arbitrary data filters, searches, and different types of sorting. These differences may narrow as features are added to the tabular editor. - **Limited data editing capabilities**: The tabular editor supports editing only textual values, whereas the table editor supports a wide variety of column editors, and can be extended with more as needed. This is due to limitations of the underlying native control used by the tabular editor. .. _tabularadapter: TabularAdapter :::::::::::::: The tabular editor works in conjunction with an adapter class, derived from TabularAdapter. The tabular adapter interfaces between the tabular editor and the data being displayed. The tabular adapter is the reason for the flexibility and power of the tabular editor to display a wide variety of data. For more detailed information about the TabularAdapter class please see :ref:`advanced-tabular-adapter`. The most important attribute of TabularAdapter is **columns**, which is list of columns to be displayed. Each entry in the **columns** list can be either a string, or a tuple consisting of a string and another value, which can be of any type. The string is used as the label for the column. The second value in the tuple, called the *column ID*, identifies the column to the adapter. It is typically a trait attribute name or an integer index, but it can be any value appropriate to the adapter. If only a string is specified for an entry, then the index of the entry within the **columns** list is used as that entry's column ID. Attributes on TabularAdapter control the appearance of items, and aspects of interaction with items, such as whether they can be edited, and how they respond to dragging and dropping. Setting any of these attributes on the adapter subclass sets the global behavior for the editor. Refer to the *Traits API Reference* for details of the available attributes. You can also specify these attributes for a specific class or column ID, or combination of class and column ID. When the TabularAdapter needs to look up the value of one of its attributes for a specific item in the table, it looks for attributes with the following naming conventions in the following order: #. *classname_columnid_attribute* #. *classname_attribute* #. *columnid_attribute* #. *attribute* For example, to find the **text_color** value for an item whose class is Person and whose column ID is 'age', the get_text_color() method looks for the following attributes in sequence, and returns the first value it finds: #. **Person_age_text_color** #. **Person_text_color** #. **age_text_color** #. **text_color** Note that the *classname* can be the name of a base class, searched in the method resolution order (MRO) for the item's class. So for example, if the item were a direct instance of Employee, which is a subclass of Person, then the **Person_age_text_color** attribute would apply to that item (as long as there were no **Employee_age_text_color** attribute). .. _the-tabular-editor-user-interface: The Tabular Editor User Interface ::::::::::::::::::::::::::::::::: Figure 53 shows an example of a tabular editor on Microsoft Windows, displaying information about source files in the Traits package. This example includes a column that contains an image for files that meet certain conditions. .. figure:: images/tabular_editor.jpg :alt: Tabular editor with columns for file name, size, an icon, time, and date Figure 53: Tabular editor on MS Windows Depending on how the tabular editor is configured, certain keyboard interactions may be available. For some interactions, you must specify that the corresponding operation is allowed by including the operation name in the *operations* list parameter of TabularEditor(). - :kbd:`Up arrow`: Selects the row above the currently selected row. - :kbd:`Down arrow`: Selects the row below the currently selected row. - :kbd:`Page down`: Appends a new item to the end of the list ('append' operation). - :kbd:`Left arrow`: Moves the currently selected row up one line ('move' operation). - :kbd:`Right arrow`: Moves the currently selected row down one line ('move' operation). - :kbd:`Backspace, Delete`: Deletes from the list all items in the current selection ('delete' operation). - :kbd:`Enter, Escape`: Initiates editing on the current selection ('edit' operation). - :kbd:`Insert:`: Inserts a new item before the current selection ('insert' operation). The 'append', 'move', 'edit', and 'insert' operations can occur only when a single item is selected. The 'delete' operation works for one or more items selected. Depending on how the editor and adapter are specified, drag and drop operations may be available. If the user selects multiple items and drags one of them, all selected items are included in the drag operation. If the user drags a non-selected item, only that item is dragged. The editor supports both "drag-move" and "drag-copy" semantics. A drag-move operation means that the dragged items are sent to the target and are removed from the list displayed in the editor. A drag-copy operation means that the dragged items are sent to the target, but are not deleted from the list data. .. _treeeditor: TreeEditor() ```````````` :Suitable for: Instance :Default for: (none) :Required parameters: *nodes* (required except for shared editors; see :ref:`editing-objects`) :Optional parameters: *auto_open, editable, editor, hide_root, icon_size, lines_mode,* *on_dclick, on_select, orientation, selected, shared_editor, show_icons* TreeEditor() generates a hierarchical tree control, consisting of nodes. It is useful for cases where objects contain lists of other objects. The tree control is displayed in one pane of the editor, and a user interface for the selected object is displayed in the other pane. The layout orientation of the tree and the object editor is determined by the *orientation* parameter of TreeEditor(), which can be 'horizontal' or 'vertical'. You must specify the types of nodes that can appear in the tree using the *nodes* parameter, which must be a list of instances of TreeNode (or of subclasses of TreeNode). .. figure:: images/TreeEditor_demo.png :alt: Tree control with instance editor pane Figure 54: Tree editor The following example shows the code that produces the editor shown in Figure 54. .. _example-18-code-for-example-tree-editor: .. rubric:: Example 18: Code for example tree editor .. literalinclude:: examples/tree_editor.py :start-at: tree_editor.py .. _defining-nodes: Defining Nodes :::::::::::::: For details on the attributes of the TreeNode class, refer to the *Traits API Reference*. More information about the TreeNode class is also available in :ref:`advanced-tree-node`. You must specify the classes whose instances the node type applies to. Use the **node_for** attribute of TreeNode to specify a list of classes; often, this list contains only one class. You can have more than one node type that applies to a particular class; in this case, each object of that class is represented by multiple nodes, one for each applicable node type. In Figure 54, one Company object is represented by the nodes labeled "Acme Labs, Inc.", "Departments", and "Employees". .. _a-node-type-without-children: A Node Type without Children ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ To define a node type without children, set the **children** attribute of TreeNode to the empty string. In Example 16, the following lines define the node type for the node that displays the company name, with no children:: TreeNode( node_for=[Company], auto_open=True, children='', label='name', view=View( Group( 'name', orientation='vertical', show_left=True, ), ), ) .. _a-node-type-with-children: A Node Type with Children ~~~~~~~~~~~~~~~~~~~~~~~~~ To define a node type that has children, set the **children** attribute of TreeNode to the (extended) name of a trait on the object that it is a node for; the named trait contains a list of the node's children. In Example 16, the following lines define the node type for the node that contains the departments of a company. The node type is for instances of Company, and 'departments' is a trait attribute of Company. :: TreeNode( node_for=[Company], auto_open=True, children='departments', label='=Departments', view=no_view, add=[Department], ) .. _setting-the-label-of-a-tree-node: Setting the Label of a Tree Node ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The **label** attribute of Tree Node can work in either of two ways: as a trait attribute name, or as a literal string. If the value is a simple string, it is interpreted as the extended trait name of an attribute on the object that the node is for, whose value is used as the label. This approach is used in the code snippet in :ref:`a-node-type-without-children`. If the value is a string that begins with an equals sign ('='), the rest of the string is used as the literal label. This approach is used in the code snippet in :ref:`a-node-type-with-children`. You can also specify a callable to format the label of the node, using the **formatter** attribute of TreeNode. .. _defining-operations-on-nodes: Defining Operations on Nodes :::::::::::::::::::::::::::: You can use various attributes of TreeNode to define operations or behavior of nodes. .. _shortcut-menus-on-nodes: Shortcut Menus on Nodes ~~~~~~~~~~~~~~~~~~~~~~~ Use the **menu** attribute of TreeNode to define a shortcut menu that opens when the user right-clicks on a node. The value is a TraitsUI or Pyface menu containing Action objects for the menu commands. In Example 16, the following lines define the node type for employees, including a shortcut menu for employee nodes:: TreeNode( node_for=[Department], auto_open=True, children='employees', label='name', menu=Menu( NewAction, Separator(), DeleteAction, Separator(), RenameAction, Separator(), CopyAction, CutAction, PasteAction, ), view=View( Group( 'name', orientation='vertical', show_left=True, ), ), add=[Employee], ), .. _allowing-the-hierarchy-to-be-modified: Allowing the Hierarchy to Be Modified ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ If a node contains children, you can allow objects to be added to its set of children, through operations such as dragging and dropping, copying and pasting, or creating new objects. Two attributes control these operations: **add** and **move**. Both are lists of classes. The **add** attribute contains classes that can be added by any means, including creation. The code snippet in the preceding section includes an example of the **add** attribute. The **move** attribute contains classes that can be dragged and dropped, but not created. The **move** attribute need not be specified if all classes that can be moved can also be created (and therefore are specified in the **add** value). .. NOTE:: The **add** attribute alone is not enough to create objects. Specifying the **add** attribute makes it possible for objects of the specified classes to be created, but by itself, it does not provide a way for the user to do so. In the code snippet in the preceding section (:ref:`shortcut-menus-on-nodes`), 'NewAction' in the Menu constructor call defines a :menuselection:`New > Employee` menu item that creates Employee objects. In the example tree editor, users can create new employees using the :menuselection:`New > Employee` shortcut menu item, and they can drag an employee node and drop it on a department node. The corresponding object becomes a member of the appropriate list. You can specify the label that appears on the :menuselection:`New` submenu when adding a particular type of object, using the **name** attribute of TreeNode. Note that you set this attribute on the tree node type that will be *added* by the menu item, not the node type that *contains* the menu item. For example, to change :menuselection:`New > Employee` to :menuselection:`New > Worker`, set ``name = 'Worker'`` on the tree node whose **node_for** value contains Employee. If this attribute is not set, the class name is used. You can determine whether a node or its children can be copied, renamed, or deleted, by setting the following attributes on TreeNode: ============= ================= ============ Attribute If True, the ... can be\ ... ============= ================= ============ **copy** object's children copied. **delete** object's children deleted. **delete_me** object deleted. **rename** object's children renamed. **rename_me** object renamed. ============= ================= ============ All of these attributes default to True. As with **add**, you must also define actions to perform these operations. .. _behavior-on-nodes: Behavior on Nodes ~~~~~~~~~~~~~~~~~ As the user clicks in the tree, you may wish to enable certain program behavior. You can use the *selected* parameter to specify the name of a trait attribute on the current context object to synchronize with the user's current selection. For example, you can enable or disable menu items or toolbar icons depending on which node is selected. The synchronization is two-way; you can set the attribute referenced by *selected* to force the tree to select a particular node. The *on_select* and *on_dclick* parameters are callables to invoke when the user selects or double-clicks a node, respectively. .. _expanding-and-collapsing-nodes: Expanding and Collapsing Nodes ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ You can control some aspects of expanding and collapsing of nodes in the tree. The integer *auto_open* parameter of TreeEditor() determines how many levels are expanded below the root node, when the tree is first displayed. For example, if *auto_open* is 2, then two levels below the root node are displayed (whether or not the root node itself is displayed, which is determined by *hide_root*). The Boolean **auto_open** attribute of TreeNode determines whether nodes of that type are expanded when they are displayed (at any time, not just on initial display of the tree). For example, suppose that a tree editor has *auto_open* setting of 2, and contains a tree node at level 3 whose **auto_open** attribute is True. The nodes at level 3 are not displayed initially, but when the user expands a level 2 node, displaying the level 3 node, that's nodes children are automatically displayed also. Similarly, the number of levels of nodes initially displayed can be greater than specified by the tree editor's *auto_open* setting, if some of the nodes have **auto_open** set to True. If the **auto_close** attribute of TreeNode is set to True, then when a node is expanded, any siblings of that node are automatically closed. In other words, only one node of this type can be expanded at a time. .. _editing-objects: Editing Objects ~~~~~~~~~~~~~~~ One pane of the tree editor displays a user interface for editing the object that is selected in the tree. You can specify a View to use for each node type using the **view** attribute of TreeNode. If you do not specify a view, then the default view for the object is displayed. To suppress the editor pane, set the *editable* parameter of TreeEditor() to False; in this case, the objects represented by the nodes can still be modified by other means, such as shortcut menu commands. You can define multiple tree editors that share a single editor pane. Each tree editor has its own tree pane. Each time the user selects a different node in any of the sharing tree controls, the editor pane updates to display the user interface for the selected object. To establish this relationship, do the following: #. Call TreeEditor() with the *shared_editor* parameter set to True, without defining any tree nodes. The object this call returns defines the shared editor pane. For example:: my_shared_editor_pane = TreeEditor(shared_editor=True) #. For each editor that uses the shared editor pane: - Set the *shared_editor* parameter of TreeEditor() to True. - Set the editor parameter of TreeEditor() to the object returned in Step 1. For example:: shared_tree_1 = TreeEditor( shared_editor=True, editor=my_shared_editor_pane, nodes=[ TreeNode(...), ], ) shared_tree_2 = TreeEditor( shared_editor=True, editor=my_shared_editor_pane, nodes=[ TreeNode(...), ], ) .. _tree-defining-the-format: Defining the Format ::::::::::::::::::: Several parameters to TreeEditor() affect the formatting of the tree control: - *show_icons*: If True (the default), icons are displayed for the nodes in the tree. - *icon_size*: A two-integer tuple indicating the size of the icons for the nodes. - *lines_mode*: Determines whether lines are displayed between related nodes. The valid values are 'on', 'off', and 'appearance' (the default). When set to 'appearance', lines are displayed except on Posix-based platforms. - *hide_root*: If True, the root node in the hierarchy is not displayed. If this parameter were specified as True in Example 16, the node in Figure 54 that is labeled "Acme Labs, Inc." would not appear. Additionally, several attributes of TreeNode also affect the display of the tree: - **icon_path**: A directory path to search for icon files. This path can be relative to the module it is used in. - **icon_item**: The icon for a leaf node. - **icon_open**: The icon for a node with children whose children are displayed. - **icon_group**: The icon for a node with children whose children are not displayed. The wxWidgets implementation automatically detects the bitmap format of the icon. ArrayViewEditor() ````````````````` :Suitable for: 2-D Array, 2-D CArray :Default for: (none) :Optional parameters: *format, show_index, titles, transpose* ArrayViewEditor() generates a tabular display for an array. It is suitable for use with large arrays, which do not work well with the editors generated by ArrayEditor(). All styles of the editor have the same appearance. .. figure:: images/array_view_editor.jpg :alt: Tabular display of numeric data, with columns Index, x, y, and z Figure 55: Array view editor DataFrameEditor() ````````````````` :Suitable for: Pandas DataFrames :Default for: (none) :Optional parameters: *formats, show_index, show_titles, columns, fonts, selected, selected_row, selectable, activated, activated_row, clicked, dclicked, right_clicked, right_dclicked, column_clicked, column_right_clicked, editable, operations* DataFrameEditor() generates a tabular display for a DataFrame. It is suitable for use with large DataFrames. All styles of the editor have the same appearance. Many of the optional parameters are identical to those of the TabularEditor(). The following have special meaning for the DataFrameEditor(): - **formats**: either a %-style formatting string for all entries, or a dictonary mapping DataFrame columns to formatting strings. - **show_index**: whether or not to show the index as a column in the table. - **show_titles**: whether or not to show column headers on the table. - **fonts**: either a font for all entries, or a mapping of column id to fonts. DefaultOverride() ````````````````` :Suitable for: (any) :Default for: (none) The DefaultOverride() is a factory that takes the trait's default editor and customizes it with the specified parameters. This is useful when a trait defines a default editor using some of its data, e.g. Range or Enum, and you want to tweak some of the other parameters without having recreate that data. For example, the default editor for Range(low=0, high=1500) has '1500' as the upper label. To change it to 'Max' instead, use:: View(Item('my_range', editor=DefaultOverride(high_label='Max')) VideoEditor() ````````````` VideoEditor() is a display-only video editor. Note that this editor is only available on Qt at the moment. Please see :ref:`TraitsUI Demos ` for a demo. .. note:: Depending on your operating system, you might have to install external codecs to get the VideoEditor working. On Windows, you will need to install DirectShow filters (such as those available in the K-Lite Codec Pack) in order to play most video formats when using EDM to install Qt5. This is because the EDM build of Qt5 is built to use `DirectShow `_, not `WMF `_. On MacOS, the video editor will work as expected as long as the video format is `supported `_ by the `AVFoundation `_. On Linux, `GStreamer `_ needs to be installed. See the `Qt Multimedia Backends Wiki `_ page for full information. .. note:: The VideoEditor is new and still under active development. As such, the API of the editor might change in future releases. .. _extra-trait-editor-factories: "Extra" Trait Editor Factories ------------------------------ The traitsui.wx package defines a few editor factories that are specific to the wxWidgets toolkit, some of which are also specific to the Microsoft Windows platform. These editor factories are not necessarily implemented for other GUI toolkits or other operating system platforms. AnimatedGIFEditor() ``````````````````` :Suitable for: File :Default for: (none) :Optional parameters: *playing* AnimatedGIFEditor() generates a display of the contents of an animated GIF image file. The Boolean *playing* parameter determines whether the image is animated or static. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/docs/source/traitsui_user_manual/factories_basic.rst0000644000175100001730000013726400000000000026543 0ustar00runnerdocker00000000000000 .. _the-predefined-trait-editor-factories: ===================================== The Predefined Trait Editor Factories ===================================== This chapter contains individual descriptions of the predefined trait editor factories provided by TraitsUI. Most of these editor factories are straightforward and can be used easily with little or no expertise on the part of the programmer or end user; these are described in :ref:`basic-trait-editor-factories`. The section :ref:`advanced-trait-editors` covers a smaller set of specialized editors that have more complex interfaces or that are designed to be used along with complex editors. .. NOTE:: Examples are toolkit-specific. The exact appearance of the editors depends on the underlying GUI toolkit. The screenshots and descriptions in this chapter contain a mix of both wx and Qt. Rather than trying to memorize all the information in this chapter, you might skim it to get a general idea of the available trait editors and their capabilities, and use it as a reference thereafter. .. _basic-trait-editor-factories: Basic Trait Editor Factories ---------------------------- The editor factories described in the following sections are straightforward to use. You can pass the editor object returned by the editor factory as the value of the *editor* keyword parameter when defining a trait. .. _arrayeditor: ArrayEditor() ````````````` :Suitable for: 2-D Array, 2-D CArray :Default for: Array, CArray (if 2-D) :Optional parameter: *width* The editors generated by ArrayEditor() provide text fields (or static text for the read-only style) for each cell of a two-dimensional NumPy array. Only the simple and read-only styles are supported by the wxWidgets implementation. You can specify the width of the text fields with the *width* parameter. This editor is only suitable for small arrays. For editing large two-dimensional arrays use the ArrayViewEditor instead .. figure:: images/ArrayEditor_demo.png :alt: 3x3 integer; integer read-only; 4x4 float; float read-only Figure 21: Array editors The following code generates the editors shown in Figure 21. .. _example-14-demonstration-of-array-editors: .. rubric:: Example 14: Demonstration of array editors .. literalinclude:: ../../../traitsui/examples/demo/Standard_Editors/ArrayEditor_demo.py :start-at: ArrayEditor BooleanEditor() ``````````````` :Suitable for: Bool, CBool :Default for: Bool, CBool :Optional parameters: *mapping* BooleanEditor is one of the simplest of the built-in editor factories in the TraitsUI package. It is used exclusively to edit and display Boolean (i.e, True/False) traits. In the simple and custom styles, it generates a checkbox. In the text style, the editor displays the trait value (as one would expect) as the strings True or False. However, several variations are accepted as input: - :samp:`'True'` - ``T`` - ``Yes`` - ``y`` - :samp:`'False'` - ``F`` - ``No`` - ``n`` The set of acceptable text inputs can be changed by setting the BooleanEditor() parameter *mapping* to a dictionary whose entries are of the form *str*: *val*, where *val* is either True or False and *str* is a string that is acceptable as text input in place of that value. For example, to create a Boolean editor that accepts only yes and no as appropriate text values, you might use the following expression:: editor=BooleanEditor(mapping={"yes": True, "no": False}) Note that in this case, the strings True and False would *not* be acceptable as text input. Figure 22 shows the four styles generated by BooleanEditor(). .. figure:: images/BooleanEditor_demo.png :alt: simple: checkbox; custom: checkbox; text: text field; read-only: read-only Figure 22: Boolean editor styles ButtonEditor() `````````````` :Suitable for: Button, Event, ToolbarButton :Default for: Button, ToolbarButton :Optional parameters: *height_padding*, *image*, *label*, *label_value*, *orientation*, *style*, *value*, *values_trait*, *view*, *width_padding* The ButtonEditor() factory is designed to be used with an Event or Button [16]_ trait. When a user clicks a button editor, the associated event is fired. Because events are not printable objects, the text and read-only styles are not implemented for this editor. The simple and custom styles of this editor are identical. .. figure:: images/ButtonEditor_demo.png :alt: simple: button; custom: button; text style unavailable; read-only style unavailable Figure 23: Button editor styles By default, the label of the button is the name of the Button or Event trait to which it is linked. [17]_ However, this label can be set to any string by specifying the *label* parameter of ButtonEditor() as that string. Alternatively, use *label_value* to specify the name of the trait to use as the button label. You can specify a value for the trait to be set to, using the *value* parameter. If the trait is an Event, then the value is not stored, but might be useful to an event listener. Use *values_trait* to specify the name of the trait on the object that contains the list of possible values. If this is set, then the *value*, *label*, and *label_value* traits are ignored; instead, they will be set from this list. When this button is clicked, the value set will be the one selected from the drop-down. .. _checklisteditor: CheckListEditor() ````````````````` :Suitable for: List :Default for: (none) :Optional parameters: *cols*, *name*, *values* The editors generated by the CheckListEditor() factory are designed to enable the user to edit a List trait by selecting elements from a "master list", i.e., a list of possible values. The list of values can be supplied by the trait being edited, or by the *values* parameter. The *values* parameter can take either of two forms: - A list of strings - A list of tuples of the form (*element*, *label*), where *element* can be of any type and *label* is a string. In the latter case, the user selects from the labels, but the underlying trait is a List of the corresponding *element* values. Alternatively, you can use the *name* parameter to specify a trait attribute containing the label strings for the values. The custom style of editor from this factory is displayed as a set of checkboxes. By default, these checkboxes are displayed in a single column; however, you can initialize the *cols* parameter of the editor factory to any value between 1 and 20, in which case the corresponding number of columns is used. The simple style generated by CheckListEditor() appears as a drop-down list; in this style, only one list element can be selected, so it returns a list with a single item. The text and read-only styles represent the current contents of the attribute in Python-style text format; in these cases the user cannot see the master list values that have not been selected. The four styles generated by CheckListEditor() are shown in Figure 24. Note that in this case the *cols* parameter has been set to 4. .. figure:: images/CheckListEditor_demo.png :alt: simple: drop-list; custom: checkboxes; text and read-only: str() of the list Figure 24: Checklist editor styles .. TODO: Change the demo and update the figure accordingly. The (value, label) option should also be demonstrated. CodeEditor() ```````````` :Suitable for: Code, Str, String :Default for: Code :Optional parameters: *auto_set* The purpose of a code editor is to display and edit Code traits, though it can be used with the Str and String trait types as well. In the simple, text, and custom styles (which are identical for this editor), the text is displayed in numbered, non-wrapping lines with a horizontal scrollbar. The read-only style is similar to the other styles except that the text is not editable. .. figure:: images/CodeEditor_demo.png :alt: simple, custom, and read-only: multi-line, numbered, text field; text: single line text field Figure 25: Code editor styles The *auto_set* keyword parameter is a Boolean value indicating whether the trait being edited should be updated with every keystroke (True) or only when the editor loses focus, i.e., when the user tabs away from it or closes the window (False). The default value of this parameter is True. .. _coloreditor: ColorEditor() ````````````` :Suitable for: Color :Default for: Color :Optional parameters: *mapped* The editors generated by ColorEditor() are designed to enable the user to display a Color trait or edit it by selecting a color from the palette available in the underlying GUI toolkit. The four styles of color editor are shown in Figure 26. .. figure:: images/ColorEditor_demo.png :alt: simple and text: colored text field; custom: color picker; read-only: colored field Figure 26: Color editor styles In the simple style, the editor appears as a text box. The text in the box is either a color name or a tuple of the form (*r*, *g*, *b*) where *r*, *g*, and *b* are the numeric values of the red, green and blue color components respectively. (Which representation is used depends on how the value was entered.) The text value is not directly editable in this style of editor; instead, clicking on the text box displays a pop-up panel similar in appearance and function to the custom style. The custom style includes a labeled color swatch on the left, representing the current value of the Color trait, and a palette of common color choices on the right. Clicking on any tile of the palette changes the color selection, causing the swatch to update accordingly. Clicking on the swatch itself causes a more detailed, platform-specific interface to appear in a dialog box, such as is shown in Figure 27. .. figure:: images/color_picker_windows.jpg :alt: MS Windows color selection dialog box Figure 27: Custom color selection dialog box for Microsoft Windows XP The text style of editor looks exactly like the simple style, but the text box is editable (and clicking on it does not open a pop-up panel). The user must enter a recognized color name or a properly formatted (*r*, *g*, *b*) tuple. The read-only style displays the text representation of the currently selected Color value (name or tuple) on a minimally-sized background of the corresponding color. **For advanced users:** The *mapped* keyword parameter of ColorEditor() is a Boolean value indicating whether the trait being edited has a built-in mapping of user-oriented representations (e.g., strings) to internal representations. Since ColorEditor() is generally used only for Color traits, which are mapped (e.g., 'cyan' to wx.Colour(0,255,255) ), this parameter defaults to True and is not of interest to most programmers. However, it is possible to define a custom color trait that uses ColorEditor() but is not mapped (i.e., uses only one representation), which is why the attribute is available. CompoundEditor() ```````````````` :Suitable for: special :Default for: "compound" traits :Optional parameters: *auto_set* An editor generated by CompoundEditor() consists of a combination of the editors for trait types that compose the compound trait. The widgets for the compound editor are of the style specified for the compound editor (simple, custom, etc.). The editors shown in Figure 28 are for the following trait, whose value can be an integer between 1 and 6, or any of the letters 'a' through 'f':: compound_trait = Trait(1, Range(1, 6), 'a', 'b', 'c', 'd', 'e', 'f') .. figure:: images/CompoundEditor_demo.png :alt: simple: slider for numbers, drop-list for letters; custom: radio buttons for both Figure 28: Example compound editor styles The *auto_set* keyword parameter is a Boolean value indicating whether the trait being edited should be updated with every keystroke (True) or only when the editor loses focus, i.e., when the user tabs away from it or closes the window (False). The default value of this parameter is True. CSVListEditor() ```````````````` :Suitable for: lists of simple data types :Default for: none :Optional parameters: *auto_set*, *enter_set*, *ignore_trailing_sep*, *sep* This editor provides a line of text for editing a list of certain simple data types. The following List traits can be edited by a CSVListEditor: * List(Int) * List(Float) * List(Str) * List(Enum('string1', 'string2', `etc`)) * List(Range(low= `low value or trait name`, high= `high value or trait name`)) The 'text', 'simple' and 'custom' styles are all the same. They provide a single line of text in which the user can enter the list. The 'readonly' style provides a line of text that can not be edited by the user. The default separator of items in the list is a comma. This can be overridden with the *sep* keyword parameter. .. figure:: images/CSVListEditor_demo.png Figure 29: Example CSV list editor styles Parameters :::::::::: *auto_set* : bool If *auto_set* is True, each key pressed by the user triggers validation of the input, and if it is valid, the value of the object being edited is updated. `Default:` True *enter_set* : bool If *enter_set* is True, the input is updated when the user presses the `Enter` key. `Default:` False *sep* : str or None The separator of the list item in the text field. If `sep` is None, each contiguous span of whitespace is a separator. (Note: After the text field is split at the occurrences of `sep`, leading and trailing whitespace is removed from each item before converting to the underlying data type.) `Default:` ',' (a comma) *ignore_trailing_sep* : bool If *ignore_trailing_sep* is True, the user may enter a trailing separator (e.g. '1, 2, 3,') and it will be ignored. If this is False, a trailing separator is an error. `Default:` True See Also :::::::: ListEditor, TextEditor DateEditor() ```````````` :Suitable for: Date, List(Date) (custom style only) :Default for: Date :Optional parameters: *allow_future*, *message*, *months*, *multi_select*, *on_mixed_select*, *padding*, *shift_to_select*, *selected_style*, *strftime*, *view* The DateEditor() displays a Python datetime.date object, usually supplied via a Date trait. The simple style shows a date spin control, while the custom style shows one (or potentially more in the wx backend) months in a calendar view. Dates can be restricted to past-only by setting *allow_future* to False. The custom style can also be used for the selection of multiple dates as an ordered list of dates by setting *multi_select* to True. The styling of the selected dates can be controlled via setting the *selected_style* to a CellFormat() instance. For readonly style, the user can set the message to display if the date value is None, and a date format string for use with strftime. DatetimeEditor() ```````````````` :Suitable for: Datetime :Default for: Datetime :Optional parameters: *maximum_datetime*, *message*, *minimum_datetime*, *strftime* The DatetimeEditor() displays a Python datetime.datetime object, usually supplied via a Datetime trait. The simple style shows a datetime spin control, and maximum and minimum selectable datetimes can be supplied to restrict the range of values. For readonly style, the user can set the message to display if the datetime value is None, and a datetime format string for use with strftime. The DatetimeEditor is not yet available for the wxPython backend. .. figure:: images/DatetimeEditor_demo.png Figure 30: Example Datetime editor styles DirectoryEditor() ````````````````` :Suitable for: Directory :Default for: Directory :Optional parameters: *entries*, *filter*, *filter_name*, *reload_name*, *truncate_ext*, *dclick_name* A directory editor enables the user to display a Directory trait or set it to some directory in the local system hierarchy. The four styles of this editor are shown in Figure 29. .. figure:: images/DirectoryEditor_demo.png :alt: simple: combo box with '...' button; custom: folder tree Figure 31: Directory editor styles In the simple style, the current value of the trait is displayed in a combo box to the left of a button labeled '...'. The user can type a new path directly into the text box, select a previous value from the droplist of the combo box, or use the button to bring up a directory browser panel similar to the custom style of editor. When the user selects a directory in this browser, the panel collapses, and control is returned to the original editor widget, which is automatically populated with the new path string. The user can also drag and drop a directory object onto the simple style editor. The custom style displays a directory browser panel, in which the user can expand or collapse directory structures, and click a folder icon to select a directory. The text style of editor is simply a text box into which the user can type a directory path. The 'readonly' style is identical to the text style, except that the text box is not editable. The optional parameters are the same as the FileEditor. No validation is performed on Directory traits; the user must ensure that a typed-in value is in fact an actual directory on the system. .. _enumeditor: EnumEditor() ```````````` :Suitable for: Enum, Any :Default for: Enum :Required parameters: for non-Enum traits: *values* or *name* :Optional parameters: *cols*, *evaluate*, *mode*, *completion_mode* (Qt only), *use_separator* (Qt only), *separator* (Qt only) The editors generated by EnumEditor() enable the user to pick a single value from a closed set of values. .. figure:: images/EnumEditor_demo.png :alt: simple: drop-list; custom: radio buttons; text: text; read-only: read-only Figure 32: Enumeration editor styles The simple style of editor is a drop-down list box. If *evaluate* is True then the user can also enter text. The *completion_mode* parameter controls how to display partially matching values, either as inline text when there is only one matching enumeration, or as a popup menu of all possible matches. The custom style is a set of radio buttons. Use the *cols* parameter to specify the number of columns of radio buttons. The text style is an editable text field; if the user enters a value that is not in enumerated set, the background of the field turns red, to indicate an error. You can specify a function to evaluate text input, using the *evaluate* parameter. The read-only style is the value of the trait as static text. If the trait attribute that is being edited is not an enumeration, you must specify either the trait attribute (with the *name* parameter), or the set of values to display (with the *values* parameter). The *name* parameter can be an extended trait name. The *values* parameter can be a list, tuple, or dictionary, or a "mapped" trait. When using *names* or *values*, the *use_separator* flag replaces any occurences of the value specified by *separator* with a separator in the pop-up menu, allowing grouping of entries (this is currently only supported in the Qt backend). By default, an enumeration editor sorts its values alphabetically. To specify a different order for the items, give it a mapping from the normal values to ones with a numeric tag. The enumeration editor sorts the values based on the numeric tags, and then strips out the tags. .. _example-15-enumeration-editor-with-mapped-values: .. rubric:: Example 15: Enumeration editor with mapped values .. literalinclude:: examples/enum_editor.py :start-at: enum_editor.py The enumeration editor strips the characters up to and including the colon. It assumes that all the items have the colon in the same position; therefore, if some of your tags have multiple digits, you should use zeros to pad the items that have fewer digits. .. _fileeditor: FileEditor() ```````````` :Suitable for: File :Default for: File :Optional parameters: *entries*, *filter*, *filter_name*, *reload_name*, *truncate_ext*, *dclick_name*, *dialog_style* A file editor enables the user to display a File trait or set it to some file in the local system hierarchy. The styles of this editor are shown in the following Figure. .. figure:: images/FileEditor_demo.png :alt: simple: text box with 'Browse' or '...' button; custom: file tree; text: text box; read-only: read-only Figure 33: File editor styles The default version of the simply style displays a text box and a :guilabel:`Browse` button. Clicking :guilabel:`Browse` opens a platform-specific file selection dialog box. On the wx backend, if you specify the *entries* keyword parameter with an integer value to the factory function, the simple style is a combo box and a button labeled :guilabel:`...`. The user can type a file path in the combo box, or select one of *entries* previous values. Support for the *entries* parameter is yet to be implemented on the qt backend. Clicking the :guilabel:`...` button opens a browser panel similar to the custom style of editor. When the user selects a file in this browser, the panel collapses, and control is returned to the original editor widget, which is automatically populated with the new path string. For either version of the simple style, the user can drag and drop a file object onto the control. The custom style displays a file system browser panel, in which the user can expand or collapse directory structures, and click an icon to select a file. You can specify a list of filters to apply to the file names displayed, using the *filter* keyword parameter of the factory function. For example, you could use a *filter* value of ``['*.py']`` to display only Python source files. You can also specify this parameter for the simple style, and it will be used in the file selection dialog box or pop-up file system browser panel. Alternatively, you can specify *filter_name*, whose value is an extended trait name of a trait attribute that contains the list of filters. The *reload_name* parameter is an extended trait name of a trait attribute that is used to notify the editor when the view of the file system needs to be reloaded. The *truncate_ext* parameter is a Boolean that indicates whether the file extension is removed from the returned filename. It is False by default, meaning that the filename is not modified before it is returned. The *dclick_name* parameter is an extended trait name of a trait event which is fired when the user double-clicks on a file name when using the custom style. When using the simple style, the *dialog_style* parameter controls the type of file dialog that will open when the user clicks on the folder icon. Setting the value of ``open`` makes the control pop up an "Open File" dialog; setting the value of `save` will result in a "Save As" dialog. FontEditor() ```````````` :Suitable for: Font :Default for: Font A font editor enables the user to display a Font trait or edit it by selecting one of the fonts provided by the underlying GUI toolkit. The four styles of this editor are shown in Figure 32. .. figure:: images/FontEditor_demo.png :alt: simple: text box; custom: text box with drop-lists for typeface and size Figure 34: Font editor styles In the simple style, the currently selected font appears in a display similar to a text box, except that when the user clicks on it, a platform-specific dialog box appears with a detailed interface, such as is shown in Figure 33. When the user clicks :guilabel:`OK`, control returns to the editor, which then displays the newly selected font. .. figure:: images/font_dialog_windows.jpg :alt: MS Windows font selection dialog box Figure 33: Example font dialog box for Microsoft Windows In the custom style, an abbreviated version of the font dialog box is displayed in-line. The user can either type the name of the font in the text box or use the two drop-down lists to select a typeface and size. In the text style, the user *must* type the name of a font in the text box provided. No validation is performed; the user must enter the correct name of an available font. The read-only style is identical except that the text is not editable. HistoryEditor() ``````````````` :Suitable for: string traits :Default for: (none) :Optional parameters: *entries* HistoryEditor() generates a combo box, which allows the user to either enter a text string or select a value from a list of previously-entered values. The same control is used for all editor styles. The *entries* parameter determines how many entries are preserved in the history list. This type of control is used as part of the simple style of file editor; see :ref:`fileeditor`. HTMLEditor() ```````````` :Suitable for: HTML, string traits :Default for: HTML :Optional parameters: *format_text* The "editor" generated by HTMLEditor() interprets and displays text as HTML. It does not support the user editing the text that it displays. It generates the same type of editor, regardless of the style specified. Figure 34 shows an HTML editor in the upper pane, with a code editor in the lower pane, displaying the uninterpreted text. .. figure:: images/html_code_editor.png :alt: formatted and unformatted HTML text Figure 34: Example HTML editor, with code editor showing original text .. NOTE:: HTML support is limited in the wxWidgets toolkit. The set of tags supported by the wxWidgets implementation of the HTML editor is a subset of the HTML standard. It does not support style sheets or complex formatting. Refer to the `wxWidgets documentation `_ for details. If the *format_text* argument is True, then the HTML editor supports basic implicit formatting, which it converts to HTML before passing the text to the HTML interpreter. The implicit formatting follows these rules: - Indented lines that start with a dash ('-') are converted to unordered lists. - Indented lines that start with an asterisk ('*') are converted to ordered lists. - Indented lines that start with any other character are converted to code blocks. - Blank lines are converted to paragraph separators. The following text produces the same displayed HTML as in Figure 34, when *format_text* is True:: This is a code block: def foo ( bar ): print 'bar:', bar This is an unordered list: - An - unordered - list This is an ordered list: * One * Two * Three ImageEditor() ````````````` :Suitable for: (any) :Default for: (none) :Optional parameters: *image*, *scale*, *preserve_aspect_ratio*, *allow_upscaling*, *allow_clipping* ImageEditor() generates a read-only display of an image. The image to be displayed is determined by the *image* parameter, or by the value of the trait attribute being edited, if *image* is not specified. In either case, the value must be a Pyface ImageResource (pyface.api.ImageResource), or a string that can be converted to one. If *image* is specified, then the type and value of the trait attribute being edited are irrelevant and are ignored. For the Qt backend *scale*, *preserve_aspect_ratio*, *allow_upscaling*, and *allow_clipping* control whether the image should be scaled or not, and how to perform that scaling. ImageEnumEditor() ````````````````` :Suitable for: Enum, Any :Default for: (none) :Required parameters: for non-Enum traits: *values* or *name* :Optional parameters: *path*, *klass* or *module*, *cols*, *evaluate*, *suffix* The editors generated by ImageEnumEditor() enable the user to select an item in an enumeration by selecting an image that represents the item. .. figure:: images/ImageEnumEditor_demo.png :alt: simple: single image button; custom: multiple images: text: "top right"; read-only: image Figure 35: Editor styles for image enumeration The custom style of editor displays a set of images; the user selects one by clicking it, and it becomes highlighted to indicate that it is selected. The simple style displays a button with an image for the currently selected item. When the user clicks the button, a pop-up panel displays a set of images, similar to the custom style. The user clicks an image, which becomes the new image on the button. The text style does not display images; it displays the text representation of the currently selected item. The user must type the text representation of another item to select it. The read-only style displays the image for the currently selected item, which the user cannot change. The ImageEnumEditor() function accepts the same parameters as the EnumEditor() function (see :ref:`enumeditor`), as well as some additional parameters. .. NOTE:: Image enumeration editors do not use ImageResource. Unlike most other images in the Traits and TraitsUI packages, images in the wxWindows implementation of image enumeration editors do not use the Pyface ImageResource class. In the wxWidgets implementation, image enumeration editors use the following rules to locate images to use: #. Only GIF (.gif) images are currently supported. #. The base file name of the image is the string representation of the value, with spaces replaced by underscores and the suffix argument, if any, appended. Note that suffix is not a file extension, but rather a string appended to the base file name. For example, if *suffix* is ``_origin`` and the *value* is 'top left', the image file name is :file:`top_left_origin.gif`. #. If the *path* parameter is defined, it is used to locate the file. It can be absolute or relative to the file where the image enumeration editor is defined. #. If *path* is not defined and the *klass* parameter is defined, it is used to locate the file. The *klass* parameter must be a reference to a class. The editor searches for an images subdirectory in the following locations: #. The directory that contains the module that defines the class. #. If the class was executed directly, the current working directory. #. If *path* and *klass* are not defined, and the *module* parameter is defined, it is used to locate the file. The *module* parameter must be a reference to a module. The editor searches for an images subdirectory of the directory that contains the module. #. If *path*, *klass*, and *module* are not defined, the editor searches for an images subdirectory of the traitsui.wx package. #. If none of the above paths are defined, the editor searches for an :file:`images` directory that is a sibling of the directory from which the application was run. InstanceEditor() ```````````````` :Suitable for: Instance, Property, self, This :Default for: Instance, self, This :Optional parameters: *cachable*, *editable*, *id*, *kind*, *label*, *name*, *object*, *orientation*, *values*, *view* The editors generated by InstanceEditor() enable the user to select an instance, or edit an instance, or both. Editing a Single Instance ::::::::::::::::::::::::: In the simplest case, the user can modify the trait attributes of an instance assigned to a trait attribute, but cannot modify which instance is assigned. .. figure:: images/InstanceEditor_demo.png :alt: simple: button; custom: editors for instance traits; text and custom: str() of instance Figure 36: Editor styles for instances The custom style displays a user interface panel for editing the trait attributes of the instance. The simple style displays a button, which when clicked, opens a window containing a user interface for the instance. The *kind* parameter specifies the kind of window to open (see :ref:`stand-alone-windows`). The *label* parameter specifies a label for the button in the simple interface. The *view* parameter specifies a view to use for the referenced instance's user interface; if this is not specified, the default view for the instance is used (see :ref:`defining-a-default-view`). The text and read-only styles display the string representation of the instance. They therefore cannot be used to modify the attributes of the instance. A user could modify the assigned instance if they happened to know the memory address of another instance of the same type, which is unlikely. These styles can useful for prototyping and debugging, but not for real applications. Selecting Instances ::::::::::::::::::: You can add an option to select a different instance to edit. Use the *name* parameter to specify the extended name of a trait attribute in the context that contains a list of instances that can be selected or edited. (See :ref:`the-view-context` for an explanation of contexts.) Using these parameters results in a drop-drown list box containing a list of text representations of the available instances. If the instances have a **name** trait attribute, it is used for the string in the list; otherwise, a user-friendly version of the class name is used. For example, the following code defines a Team class and a Person class. A Team has a roster of Persons, and a captain. In the view for a team, the user can pick a captain and edit that person's information. .. _example-16-instance-editor-with-instance-selection: Example 16: Instance editor with instance selection .. literalinclude:: examples/instance_editor_selection.py :start-at: instance_editor_selection.py .. figure:: images/ui_for_ex16.png :alt: Dialog box for a "team", with drop-list selection for "Team Captain" Figure 37: User interface for Example 16 If you want the user to be able to select instances, but not modify their contents, set the *editable* parameter to False. In that case, only the selection list for the instances appears, without the user interface for modifying instances. Allowing Instances :::::::::::::::::: You can specify what types of instances can be edited in an instance editor, using the *values* parameter. This parameter is a list of items describing the type of selectable or editable instances. These items must be instances of subclasses of traitsui.api.InstanceChoiceItem. If you want to generate new instances, put an InstanceFactoryChoice instance in the *values* list that describes the instance to create. If you want certain types of instances to be dropped on the editor, use an InstanceDropChoice instance in the values list. .. TODO: Need an example here. ListEditor() ```````````` :Suitable for: List :Default for: List [18]_ :Optional parameters: *editor*, *rows*, *style*, *scrollable*, *trait_handler*, *use_notebook* The following parameters are used only if *use_notebook* is True: *deletable*, *dock_style*, *export*, *page_name*, *select*, *view* The editors generated by ListEditor() enable the user to modify the contents of a list, both by editing the individual items and by adding, deleting, and reordering items within the list. .. figure:: images/ListEditor_demo.png :alt: simple: single text box; custom and text: multiple text boxes; read-only: read-only list Figure 38: List editor styles The simple style displays a single item at a time, with small arrows on the right side to scroll the display. The custom style shows multiple items. The number of items displayed is controlled by the *rows* parameter; if the number of items in the list exceeds this value, then the list display scrolls. If the *scrollable* parameter is False, the editor displays all objects in the list and does not render the vertical scrollbar. The editor used for each item in the list is determined by the *editor* and *style* parameters. The text style of list editor is identical to the custom style, except that the editors for the items are text editors. The read-only style displays the contents of the list as static text. By default, the items use the trait handler appropriate to the type of items in the list. You can specify a different handler to use for the items using the *trait_handler* parameter. .. TODO: Add an example of a trait handler. For the simple, custom, and text list editors, a button appears to the left of each item editor; clicking this button opens a context menu for modifying the list, as shown in Figure 39. .. figure:: images/list_with_context_menu.png :alt: list editor with context menu Figure 39: List editor showing context menu In addition to the four standard styles for list editors, a fifth list editor user interface option is available. If *use_notebook* is True, then the list editor displays the list as a "notebook" of tabbed pages, one for each item in the list, as shown in Figure 40. This style can be useful in cases where the list items are instances with their own views. If the *deletable* parameter is True, a close box appears on each tab, allowing the user to delete the item; the user cannot add items interactively through this style of editor. .. figure:: images/notebook_list_editor.jpg :alt: tabbed instance editors Figure 40: Notebook list editor LEDEditor() ``````````` :Suitable for: numeric traits :Default for: (none) :Optional parameters: *alignment, format_str* LEDEditor() generates a display that resembles a "digital" display using light-emitting diodes. All styles of this editor are the same, and are read-only. The *alignment* parameter can be 'left', 'center', or 'right' to indicate how the value should be aligned within the display. The default is right-alignment. .. figure:: images/led_editor.png :alt: LED-like display of 90452 Figure 56: LED Editor with right alignment ListStrEditor() ``````````````` :Suitable for: ListStr or List of values mapped to strings :Default for: (none) :Optional parameters: *activated, activated_index, adapter, adapter_name, auto_add, drag_move*, *editable, horizontal_lines, images, multi_select, operations*, *right_clicked, right_clicked_index, selected, selected_index, title*, *title_name* ListStrEditor() generates a list of selectable items corresponding to items in the underlying trait attribute. All styles of the editor are the same. The parameters to ListStrEditor() control aspects of the behavior of the editor, such as what operations it allows on list items, whether items are editable, and whether more than one can be selected at a time. You can also specify extended references for trait attributes to synchronize with user actions, such as the item that is currently selected, activated for editing, or right-clicked. .. figure:: images/ListStrEditor_demo.png :alt: list box displaying strings Figure 41: List string editor NullEditor() ```````````` :Suitable for: controlling layout :Default for: (none) The NullEditor() factory generates a completely empty panel. It is used by the Spring subclass of Item, to generate a blank space that uses all available extra space along its layout orientation. You can also use it to create a blank area of a fixed height and width. RangeEditor() ````````````` :Suitable for: Range :Default for: Range :Optional parameters: *auto_set*, *cols*, *enter_set*, *format*, *high_label*, *high_name*, *label_width*, *low_label*, *low_name*, *mode* The editors generated by RangeEditor() enable the user to specify numeric values within a range. The widgets used to display the range vary depending on both the numeric type and the size of the range, as described in Table 8 and shown in Figure 42. If one limit of the range is unspecified, then a text editor is used. .. _range-editor-widgets-table: .. rubric:: Table 8: Range editor widgets +-----------------------------+-----------+-------------+----------+-----------+ |Data type/range size |Simple |Custom |Text |Read-only | +=============================+===========+=============+==========+===========+ |Integer: Small Range (Size |Slider with|Radio buttons|Text field|Static text| |0-16) |text box | | | | +-----------------------------+-----------+-------------+----------+-----------+ |Integer: Medium Range (Size |Slider with|Slider with |Text field|Static text| |17-101) |text box |text box | | | +-----------------------------+-----------+-------------+----------+-----------+ |Integer: Large Range (Size > |Spin box |Spin box |Text field|Static text| |101) | | | | | +-----------------------------+-----------+-------------+----------+-----------+ |Floating Point: Small Range |Slider with|Slider with |Text field|Static text| |(Size <= 100.0) |text box |text box | | | +-----------------------------+-----------+-------------+----------+-----------+ |Floating Point: Large Range |Large-range|Large-range |Text field|Static text| |(Size > 100.0) |slider |slider | | | +-----------------------------+-----------+-------------+----------+-----------+ .. figure:: images/RangeEditor_demo.png :alt: slider with text box; radio buttons; text box; static text; spin box; large-range slider Figure 42: Range editor widgets In the large-range slider, the arrows on either side of the slider move the editable range, so that the user can move the slider more precisely to the desired value. You can override the default widget for each type of editor using the *mode* parameter, which can have the following values: - 'auto': The default widget, as described in Table 8 - 'slider': Simple slider with text field - 'xslider': Large-range slider with text field - 'spinner': Spin box with increment/decrement buttons - 'enum': Radio buttons - 'text': Text field You can set the limits of the range dynamically, using the *low_name* and *high_name* parameters to specify trait attributes that contain the low and high limit values; use *low_label*, *high_label* and *label_width* to specify labels for the limits. RGBColorEditor() ```````````````` :Suitable for: RGBColor :Default for: RGBColor :Optional parameters: *mapped* Editors generated by RGBColorEditor() are identical in appearance to those generated by ColorEditor(), but they are used for RGBColor traits. See :ref:`coloreditor` for details. .. _seteditor: SetEditor() ``````````` :Suitable for: List :Default for: (none) :Required parameters: Either *values* or *name* :Optional parameters: *can_move_all*, *left_column_title*, *object*, *ordered*, *right_column_title* In the editors generated by SetEditor(), the user can select a subset of items from a larger set. The two lists are displayed in list boxes, with the candidate set on the left and the selected set on the right. The user moves an item from one set to the other by selecting the item and clicking a direction button (:guilabel:`>` for left-to-right and :guilabel:`<` for right-to-left). Additional buttons can be displayed, depending on two Boolean parameters: - If *can_move_all* is True, additional buttons appear, whose function is to move all items from one side to the other (:guilabel:`>>` for left-to-right and :guilabel:`<<` for right-to-left). - If *ordered* is True, additional buttons appear, labeled :guilabel:`Move up` and :guilabel:`Move down`, which affect the position of the selected item within the set in the right list box. .. figure:: images/SetEditor_demo.png :alt: set editor list boxes with buttons Figure 43: Set editor showing all possible buttons You can specify the set of candidate items in either of two ways: - Set the *values* parameter to a list, tuple, dictionary, or mapped trait. - Set the *name* parameter to the extended name of a trait attribute that contains the list. ShellEditor() ````````````` :Suitable for: special :Default for: PythonValue The editor generated by ShellEditor() displays an interactive Python shell. .. figure:: images/shell_editor.jpg :alt: interactive shell pane Figure 44: Python shell editor TextEditor() ```````````` :Suitable for: all :Default for: Str, String, Password, Unicode, Int, Float, Dict, CStr, CUnicode, and any trait that does not have a specialized TraitHandler :Optional parameters: *auto_set*, *enter_set*, *evaluate*, *evaluate_name*, *mapping*, *multi_line*, *password* The editor generated by TextEditor() displays a text box. For the custom style, it is a multi-line field; for the read-only style, it is static text. If *password* is True, the text that the user types in the text box is obscured. .. figure:: images/text_editor_integers.png :alt: simple: text box; custom: multi-line text box; text: text box; read-only: static text Figure 45: Text editor styles for integers .. figure:: images/text_editor_strings.png :alt: simple: text box; custom: multi-line text box; text: text box; read-only: static text Figure 46: Text editor styles for strings .. figure:: images/text_editors_passwords.png :alt: same as above, but with value obscured by asterisks Figure 47: Text editor styles for passwords You can specify whether the trait being edited is updated on every keystroke (``auto_set=True``) or when the user presses the Enter key (``enter_set=True``). If *auto_set* and *enter_set* are False, the trait is updated when the user shifts the input focus to another widget. You can specify a mapping from user input values to other values with the *mapping* parameter. You can specify a function to evaluate user input, either by passing a reference to it in the *evaluate* parameter, or by passing the extended name of a trait that references it in the *evaluate_name* parameter. TimeEditor() ```````````` :Suitable for: Time :Default for: Time :Optional parameters: *minimum_datetime*, *strftime* The TimeEditor() displays a Python datetime.time object, usually supplied via a Time trait. The simple style shows a time spin control. For readonly style, the user can set the message to display if the time value is None, and a time format string for use with strftime. TitleEditor() ````````````` :Suitable for: string traits :Default for: (none) TitleEditor() generates a read-only display of a string value, formatted as a heading. All styles of the editor are identical. Visually, it is similar to a Heading item, but because it is an editor, you can change the text of the heading by modifying the underlying attribute. TupleEditor() ````````````` :Suitable for: Tuple :Default for: Tuple :Optional parameters: *cols*, *editors*, *labels*, *traits* The simple and custom editors generated by TupleEditor() provide a widget for each slot of the tuple being edited, based on the type of data in the slot. The text and read-only editors edit or display the text representation of the tuple. .. figure:: images/TupleEditor_demo.png :alt: simple and custom: color editor, range editor, text box Figure 48: Tuple editor styles You can specify the number of columns to use to lay out the widgets with the *cols* parameter. You can specify labels for the widgets with the *labels* parameter. You can also specify trait definitions for the slots of the tuple; however, this is usually implicit in the tuple being edited. You can supply a list of editors to be used for each corresponding tuple slot. If the *editors* list is missing, or is shorter than the length of the tuple, default editors are used for any tuple slots not defined in the list. This feature allows you to substitute editors, or to supply non-default parameters for editors. ValueEditor() ````````````` :Suitable for: (any) :Default for: (none) :Optional parameters: *auto_open* ValueEditor() generates a tree editor that displays Python values and objects, including all the objects' members. For example, Figure 49 shows a value editor that is displayed by the "pickle viewer" utility in enthought.debug. .. figure:: images/value_editor.png :alt: tree of Python values, including dictionaries, lists, and tuples Figure 49: Value editor from Pickle Viewer .. rubric:: Footnotes .. [16] In Traits, a Button and an Event are essentially the same thing, except that Buttons are automatically associated with button editors. .. [17] TraitsUI makes minor modifications to the name, capitalizing the first letter and replacing underscores with spaces, as in the case of a default Item label (see :ref:`the-view-object`). .. [18] If a List is made up of HasTraits objects, a table editor is used instead; see :ref:`tableeditor`. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/docs/source/traitsui_user_manual/factory_intro.rst0000644000175100001730000003401500000000000026273 0ustar00runnerdocker00000000000000.. _introduction-to-trait-editor-factories: ====================================== Introduction to Trait Editor Factories ====================================== The preceding code samples in this User Manual have been surprisingly simple considering the sophistication of the interfaces that they produce. In particular, no code at all has been required to produce appropriate widgets for the Traits to be viewed or edited in a given window. This is one of the strengths of TraitsUI: usable interfaces can be produced simply and with a relatively low level of UI programming expertise. An even greater strength lies in the fact that this simplicity does not have to be paid for in lack of flexibility. Where a novice TraitsUI programmer can ignore the question of widgets altogether, a more advanced one can select from a variety of predefined interface components for displaying any given Trait. Furthermore, a programmer who is comfortable both with TraitsUI and with UI programming in general can harness the full power and flexibility of the underlying GUI toolkit from within TraitsUI. The secret behind this combination of simplicity and flexibility is a TraitsUI construct called a trait :term:`editor factory`. A trait editor factory encapsulates a set of display instructions for a given :term:`trait type`, hiding GUI-toolkit-specific code inside an abstraction with a relatively straightforward interface. Furthermore, every :term:`predefined trait type` in the Traits package has a predefined trait editor factory that is automatically used whenever the trait is displayed, unless you specify otherwise. Consider the following script and the window it creates: .. _example-12-using-default-trait-editors: .. rubric:: Example 12: Using default trait editors .. literalinclude:: examples/default_trait_editors.py :start-at: default_trait_editors.py .. figure:: images/ui_for_ex12.jpg :alt: UI showing text boxes for names, slider for Age, and checkbox for voter Figure 12: User interface for Example 12 Notice that each trait is displayed in an appropriate widget, even though the code does not explicitly specify any widgets at all. The two Str traits appear in text boxes, the Range is displayed using a combination of a text box and a slider, and the Bool is represented by a checkbox. Each implementation is generated by the default trait editor factory (TextEditor, RangeEditor and BooleanEditor respectively) associated with the trait type. TraitsUI is by no means limited to these defaults. There are two ways to override the default representation of a :term:`trait attribute` in a TraitsUI window: - Explicitly specifying an alternate trait editor factory - Specifying an alternate style for the editor generated by the factory The remainder of this chapter examines these alternatives more closely. .. _specifying-an-alternate-trait-editor-factory: Specifying an Alternate Trait Editor Factory -------------------------------------------- As of this writing the TraitsUI package includes a wide variety of predefined trait editor factories, which are described in :ref:`basic-trait-editor-factories` and :ref:`advanced-trait-editors`. Some additional editor factories are specific to the wxWidgets toolkit and are defined in one of the following packages: - traitsui.wx - traitsui.wx.extra - traitsui.wx.extra.windows (specific to Microsoft Windows) These editor factories are described in :ref:`extra-trait-editor-factories`. For a current complete list of editor factories, refer to the *Traits API Reference*. Other packages can define their own editor factories for their own traits. For example, enthought.kiva.api.KivaFont uses a KivaFontEditor() and enthought.enable2.traits.api.RGBAColor uses an RGBAColorEditor(). For most :term:`predefined trait type`\ s (see `Traits User Manual `_), there is exactly one predefined trait editor factory suitable for displaying it: the editor factory that is assigned as its default. [15]_ There are exceptions, however; for example, a Str trait defaults to using a TextEditor, but can also use a CodeEditor or an HTMLEditor. A List trait can be edited by means of ListEditor, TableEditor (if the List elements are HasTraits objects), CheckListEditor or SetEditor. Furthermore, the TraitsUI package includes tools for building additional trait editors and factories for them as needed. To use an alternate editor factory for a trait in a TraitsUI window, you must specify it in the View for that window. This is done at the Item level, using the *editor* keyword parameter. The syntax of the specification is :samp:`editor = {editor_factory}()`. (Use the same syntax for specifying that the default editor should be used, but with certain keyword parameters explicitly specified; see :ref:`initializing-editors`). For example, to display a Str trait called **my_string** using the default editor factory (TextEditor()), the View might contain the following Item:: Item(name='my_string') The resulting widget would have the following appearance: .. figure:: images/default_text_editor.png :alt: Text field showing text that contains HTML markup Figure 13: Default editor for a Str trait To use the HTMLEditor factory instead, add the appropriate specification to the Item:: Item( name='my_string', editor=HTMLEditor() ) The resulting widget appears as in Figure 14: .. figure:: images/HTML_editor.png :alt: Same text as Figure 13, styled as HTML Figure 14: Editor generated by HTMLEditor() .. NOTE:: TraitsUI does not check editors for appropriateness. TraitsUI does not police the *editor* argument to ensure that the specified editor is appropriate for the trait being displayed. Thus there is nothing to prevent you from trying to, say, display a Float trait using ColorEditor(). The results of such a mismatch are unlikely to be helpful, and can even crash the application; it is up to the programmer to choose an editor sensibly. :ref:`the-predefined-trait-editor-factories` is a useful reference for selecting an appropriate editor for a given task. It is possible to specify the trait editor for a trait in other ways: - You can specify a trait editor when you define a trait, by passing the result of a trait editor factory as the *editor* keyword parameter of the callable that creates the trait. However, this approach commingles the :term:`view` of a trait with its :term:`model`. - You can specify the **editor** attribute of a TraitHandler object. This approach commingles the :term:`view` of a trait with its :term:`controller`. Use these approaches very carefully, if at all, as they muddle the :term:`MVC` design pattern. .. _initializing-editors: Initializing Editors ```````````````````` Many of the TraitsUI trait editors can be used "straight from the box" as in the example above. There are some editors, however, that must be initialized in order to be useful. For example, a checklist editor (from CheckListEditor()) and a set editor (from SetEditor()) both enable the user to edit a List attribute by selecting elements from a specified set; the contents of this set must, of course, be known to the editor. This sort of initialization is usually performed by means of one or more keyword arguments to the editor factory, for example:: Item( name='my_list', editor=CheckListEditor( values=["opt1","opt2","opt3"], ), ) The descriptions of trait editor factories in :ref:`the-predefined-trait-editor-factories` include a list of required and optional initialization keywords for each editor. .. _specifying-an-editor-style: Specifying an Editor Style -------------------------- In TraitsUI, any given trait editor can be generated in one or more of four different styles: *simple*, *custom*, *text* or *readonly*. These styles, which are described in general terms below, represent different "flavors" of data display, so that a given trait editor can look completely different in one style than in another. However, different trait editors displayed in the same style (usually) have noticeable characteristics in common. This is useful because editor style, unlike individual editors, can be set at the Group or View level, not just at the Item level. This point is discussed further in :ref:`using-editor-styles`. .. _the-simple-style: The 'simple' Style `````````````````` The *simple* editor style is designed to be as functional as possible while requiring minimal space within the window. In simple style, most of the Traits UI editors take up only a single line of space in the window in which they are embedded. In some cases, such as the text editor and Boolean editor (see :ref:`basic-trait-editor-factories`), the single line is fully sufficient. In others, such as the (plain) color editor and the enumeration editor, a more detailed interface is required; pop-up panels, drop-down lists, or dialog boxes are often used in such cases. For example, the simple version of the enumeration editor for the wxWidgets toolkit looks like this: .. figure:: images/simple_enum_editor_closed.jpg :alt: Closed drop-list editor Figure 15: Simple style of enumeration editor However, when the user clicks on the widget, a drop-down list appears: .. figure:: images/simple_enum_editor_open.jpg :alt: Expanded drop-list editor Figure 16: Simple enumeration editor with expanded list The simple editor style is most suitable for windows that must be kept small and concise. The 'custom' Style `````````````````` The *custom* editor style generally generates the most detailed version of any given editor. It is intended to provide maximal functionality and information without regard to the amount of window space used. For example, in the wxWindows toolkit, the custom style the enumeration editor appears as a set of radio buttons rather than a drop-down list: .. figure:: images/custom_enum_editor.jpg :alt: Radio buttons for a set of values Figure 17: Custom style of enumeration editor In general, the custom editor style can be very useful when there is no need to conserve window space, as it enables the user to see as much information as possible without having to interact with the widget. It also usually provides the most intuitive interface of the four. Note that this style is not defined explicitly for all trait editor implementations. If the custom style is requested for an editor for which it is not defined, the simple style is generated instead. The 'text' Style ```````````````` The *text* editor style is the simplest of the editor styles. When applied to a given trait attribute, it generates a text representation of the trait value in an editable box. Thus the enumeration editor in text style looks like the following: .. figure:: images/text_editor.jpg :alt: Text field Figure 18: Text style of enumeration editor For this type of editor, the end user must type in a valid value for the attribute. If the user types an invalid value, the validation method for the attribute (see `Traits User Manual `_) notifies the user of the error (for example, by shading the background of the text box red). The text representation of an attribute to be edited in a text style editor is created in one of the following ways, listed in order of priority: #. The function specified in the **format_func** attribute of the Item (see :ref:`the-item-object`), if any, is called on the attribute value. #. Otherwise, the function specified in the *format_func* parameter of the trait editor factory, if any, is called on the attribute value. #. Otherwise, the Python-style formatting string specified in the **format_str** attribute of the Item (see :ref:`the-item-object`), if any, is used to format the attribute value. #. The Python-style formatting string specified in the *format_str* parameter of the trait editor factory, if any, is used to format the attribute value. #. Otherwise, the Python str() function is called on the attribute value. The 'readonly' style ```````````````````` The *readonly* editor style is usually identical in appearance to the text style, except that the value appears as static text rather than in an editable box: .. figure:: images/read_only_editor.jpg :alt: Read-only text field Figure 19: Read-only style of enumeration editor This editor style is used to display data values without allowing the user to change them. .. _using-editor-styles: Using Editor Styles ``````````````````` As discussed in :ref:`contents-of-a-view` and :ref:`customizing-a-view`, the Item, Group and View objects of TraitsUI all have a **style** attribute. The style of editor used to display the Items in a View is determined as follows: #. The editor style used to display a given Item is the value of its **style** attribute if specifically assigned. Otherwise the editor style of the Group or View that contains the Item is used. #. The editor style of a Group is the value of its **style** attribute if assigned. Otherwise, it is the editor style of the Group or View that contains the Group. #. The editor style of a View is the value of its **style** attribute if specified, and 'simple' otherwise. In other words, editor style can be specified at the Item, Group or View level, and in case of conflicts the style of the smaller scope takes precedence. For example, consider the following script: .. _example-13-using-editor-styles-at-various-levels: .. rubric:: Example 13: Using editor styles at various levels .. literalinclude:: examples/mixed_styles.py :start-at: mixed_styles.py Notice how the editor styles are set for each attribute: - **position_type** at the Item level (lines 19-20) - **department** at the Group level (lines 18 and 21) - **first_name** and **last_name** at the View level (lines 16, 17, and 23) The resulting window demonstrates these precedence rules: .. figure:: images/ui_for_ex13.jpg :alt: UI showing read-only text, closed drop-list, and radio buttons Figure 20: User interface for Example 13 .. rubric:: Footnotes .. [15] Appendix II contains a table of the predefined trait types in the Traits package and their default trait editor types. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/docs/source/traitsui_user_manual/glossary.rst0000644000175100001730000001733400000000000025261 0ustar00runnerdocker00000000000000 .. _glossary-of-terms: ============================= Appendix I: Glossary of Terms ============================= .. glossary:: attribute An element of data that is associated with all instances of a given class, and is named at the class level. [19]_ In most cases, attributes are stored and assigned separately for each instance (for the exception, see :term:`class attribute`). Synonyms include "data member" and "instance variable". class attribute An element of data that is associated with a class, and is named at the class level. There is only one value for a class attribute, associated with the class itself. In contrast, for an instance :term:`attribute`, there is a value associated with every instance of a class. command button A button on a window that globally controls the window. Examples include :guilabel:`OK`, :guilabel:`Cancel`, :guilabel:`Apply`, :guilabel:`Revert`, and :guilabel:`Help`. controller The element of the :term:`MVC` ("model-view-controller") design pattern that manages the transfer of information between the data :term:`model` and the :term:`view` used to observe and edit it. dialog box A secondary window whose purpose is for a user to specify additional information when entering a command. editor A user interface component for editing the value of a trait attribute. Each type of trait has a default editor, but you can override this selection with one of a number of editor factories provided by the TraitsUI package. In some cases an editor can include multiple widgets, e.g., a slider and a text box for a Range trait attribute. editor factory An instance of the Traits class EditorFactory. Editor factories generate the actual widgets used in a user interface. You can use an editor factory without knowing what the underlying GUI toolkit is. factory An object used to produce other objects at run time without necessarily assigning them to named variables or attributes. A single factory is often parameterized to produce instances of different classes as needed. Group An object that specifies an ordered set of Items and other Groups for display in a TraitsUI View. Various display options can be specified by means of attributes of this class, including a border, a group label, and the orientation of elements within the Group. An instance of the TraitsUI class Group. Handler A TraitsUI object that implements GUI logic (data manipulation and dynamic window behavior) for one or more user interface windows. A Handler instance fills the role of :term:`controller` in the MVC design pattern. An instance of the TraitsUI class :term:`Handler`. HasTraits A class defined in the Traits package to specify objects whose attributes are typed. That is, any attribute of a HasTraits subclass can be a :term:`trait attribute`. instance A concrete entity belonging to an abstract category such as a class. In object-oriented programming terminology, an entity with allocated memory storage whose structure and behavior are defined by the class to which it belongs. Often called an :term:`object`. Item A non-subdividable element of a Traits user interface specification (View), usually specifying the display options to be used for a single trait attribute. An instance of the TraitsUI class Item. live A term used to describe a window that is linked directly to the underlying model data, so that changes to data in the interface are reflected immediately in the model. A window that is not live displays and manipulates a copy of the model data until the user confirms any changes. livemodal A term used to describe a window that is both :term:`live` and :term:`modal`. MVC A design pattern for interactive software applications. The initials stand for "Model-View-Controller", the three distinct entities prescribed for designing such applications. (See the glossary entries for :term:`model`, :term:`view`, and :term:`controller`.) modal A term used to describe a window that causes the remainder of the application to be suspended, so that the user can interact only with the window until it is closed. model A component of the :term:`MVC` design pattern for interactive software applications. The model consists of the set of classes and objects that define the underlying data of the application, as well as any internal (i.e., non-GUI-related) methods or functions on that data. nonmodal A term used to describe a window that is neither :term:`live` nor :term:`modal`. object Synonym for :term:`instance`. panel A user interface region similar to a window except that it is embedded in a larger window rather than existing independently. predefined trait type Any trait type that is built into the Traits package. subpanel A variation on a :term:`panel` that ignores (i.e., does not display) any command buttons. trait A term used loosely to refer to either a :term:`trait type` or a :term:`trait attribute`. trait attribute An :term:`attribute` whose type is specified and checked by means of the Traits package. trait type A type-checked data type, either built into or implemented by means of the Traits package. Traits An open source package engineered by Enthought, Inc. to perform explicit typing in Python. TraitsUI A high-level user interface toolkit designed to be used with the Traits package. View A template object for constructing a GUI window or panel for editing a set of traits. The structure of a View is defined by one or more Group or Item objects; a number of attributes are defined for specifying display options including height and width, menu bar (if any), and the set of buttons (if any) that are displayed. A member of the TraitsUI class View. view A component of the :term:`MVC` design pattern for interactive software applications. The view component encompasses the visual aspect of the application, as opposed to the underlying data (the :term:`model`) and the application's behavior (the :term:`controller`). ViewElement A View, Group or Item object. The ViewElement class is the parent of all three of these subclasses. widget An interactive element in a graphical user interface, e.g., a scrollbar, button, pull-down menu or text box. wizard An interface composed of a series of :term:`dialog box` windows, usually used to guide a user through an interactive task such as software installation. wx A shorthand term for the low-level GUI toolkit on which TraitsUI and Pyface are currently based (`wxWidgets `_) and its Python wrapper (`wxPython `_). .. rubric:: Footnotes .. [19] This is not always the case in Python, where attributes can be added to individual objects. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/docs/source/traitsui_user_manual/handler.rst0000644000175100001730000006370400000000000025035 0ustar00runnerdocker00000000000000.. _controlling-the-interface-the-handler: ====================================== Controlling the Interface: the Handler ====================================== Most of the material in the preceding chapters is concerned with the relationship between the model and view aspects of the :term:`MVC` design pattern as supported by TraitsUI. This chapter examines the third aspect: the :term:`controller`, implemented in TraitsUI as an :term:`instance` of the :term:`Handler` class. [11]_ A controller for an MVC-based application is essentially an event handler for GUI events, i.e., for events that are generated through or by the program interface. Such events can require changes to one or more model objects (e.g., because a data value has been updated) or manipulation of the interface itself (e.g., window closure, dynamic interface behavior). In TraitsUI, such actions are performed by a Handler object. In the preceding examples in this guide, the Handler object has been implicit: TraitsUI provides a default Handler that takes care of a common set of GUI events including window initialization and closure, data value updates, and button press events for the standard TraitsUI window buttons (see :ref:`command-buttons-the-buttons-attribute`). This chapter explains the features of the TraitsUI Handler, and shows how to implement custom GUI behaviors by building and instantiating custom subclasses of the Handler class. The final section of the chapter describes several techniques for linking a custom Handler to the window or windows it is designed to control. .. _backstage-introducing-the-uiinfo-object: Backstage: Introducing the UIInfo Object ---------------------------------------- TraitsUI supports the MVC design pattern by maintaining the model, view, and controller as separate entities. A single View object can be used to construct windows for multiple model objects; likewise a single Handler can handle GUI events for windows created using different Views. Thus there is no static link between a Handler and any particular window or model object. However, in order to be useful, a Handler must be able to observe and manipulate both its corresponding window and model objects. In TraitsUI, this is accomplished by means of the UIInfo object. Whenever TraitsUI creates a window or panel from a View, a UIInfo object is created to act as the Handler's reference to that window and to the objects whose :term:`trait attribute`\ s are displayed in it. This object holds a reference to the UI instance in its **ui** trait, and whether or not the UI has been initialized in the **initialized** trait. Additionally, this object is dynamically assigned trait attributes which correspond to: - each entry in the View's context (see :ref:`the-view-context`). - each item's and group's editor in the view, by id (or name, if no id is available for an item). Where there is conflict between ids, the editors take precedence over context values, and if two editors have the same name then the last editor with that name will be referenced. For example, the UIInfo object created in :ref:`Example 7 ` has attributes **h1** and **h2** whose values are the objects **house1** and **house2** respectively. Additionally it has attributes **address**, **bedroom**, **pool**, and **price** that reference the editors in the second group. In :ref:`Example 1 ` through :ref:`Example 6 `, the created UIInfo object has an attribute **object** whose value is the object **sam**, together with attributes that corrspond to the items in the views, such as **first_name**, **last_name** and **department**. Whenever a window event causes a Handler method to be called, TraitsUI passes the corresponding UIInfo object as one of the method arguments. This gives the Handler the information necessary to perform its tasks. Additionally, traits on objects in the context can be synchronized with traits on editors via the **sync_to_view**, **sync_from_view** and **sync_with_view** trait metadata. Note that not every trait on every editor can react to changes: some values are only used at editor creation time; however all editors support dynamically changing the **enabled**, **visible** and **invalid** traits. This feature can sometimes allow developers to avoid having to create a custom Handler subclass. See the :github-demo:`Invalid state handling ` example which demonstrates how to use this mechanism to control the invalid state of a dialog based on the value of multiple editors. .. _backstage-the-ui-object: Backstage: The UI Object ------------------------ As opposed to the very dynamic UIInfo object, the UI class provides an object which ties together the various objects that are involved in a TraitsUI GUI: the View, the context, the Handler, the underlying toolkit controls and the shared state of the GUI. It also has some life-cycle and useful utility methods that can be useful when working from a handler. The UI object is returned as the result of a call to edit_traits(), and as noted in the previous section, is available as the **ui** attribute of the UIInfo object that is passed to most handler methods. .. rubric:: Attributes of UI, by category TraitsUI core view: View template used to construct the user interface. handler: Handler object used for event handling. context: Dictionary of objects that the UI is editing. info: UIInfo object containing context or editor objects parent: The parent UI (if any) of this UI. Toolkit control: Panel or dialog associated with the user interface. owner: Toolkit-specific object that "owns" **control** GUI state id: The unique ID for this UI for persistence. title: Title of the dialog, if any. icon: The ImageResource of the dialog icon, if any. key_bindings: The KeyBindings object (if any) for this UI. result: Result from a modal or wizard dialog. modified: Have any modifications been made to UI contents? updated: Event when the user interface has changed. history: Undo and Redo history. errors: The number of currently pending editor error conditions. Note that changing this must be done very carefully to avoid permanent error states. destroyed: Set to True when the UI has finished being destroyed. .. rubric:: Useful UI methods +---------------------------+--------------------------------------------------+ |Method |Purpose | +===========================+==================================================+ |dispose(result, abort) |Disposes of the UI. This can be called to close a| | |TraitsUI dialog programatically from a handler. | +---------------------------+--------------------------------------------------+ |get_editors(name) |Returns a list of all editors matching the name. | +---------------------------+--------------------------------------------------+ .. _assigning-handlers-to-views: Assigning Handlers to Views --------------------------- In accordance with the MVC design pattern, Handlers and Views are separate entities belonging to distinct classes. In order for a custom Handler to provide the control logic for a window, it must be explicitly associated with the View for that window. The TraitsUI package provides three ways to accomplish this: - Make the Handler an attribute of the View. - Provide the Handler as an argument to a display method such as edit_traits(). - Define the View as part of the Handler. .. _binding-a-singleton-handler-to-a-view: Binding a Singleton Handler to a View ````````````````````````````````````` To associate a given custom Handler with all windows produced from a given View, assign an instance of the custom Handler class to the View's **handler** attribute. The result of this technique, as shown in :ref:`Example 9 `, is that the window created by the View object is automatically controlled by the specified handler instance. .. _linking-handler-and-view-at-edit-time: Linking Handler and View at Edit Time ````````````````````````````````````` It is also possible to associate a custom Handler with a specific window without assigning it permanently to the View. Each of the three TraitsUI window-building methods (the configure_traits() and edit_traits() methods of the HasTraits class and the ui() method of the View class) has a *handler* keyword argument. Assigning an instance of Handler to this argument gives that handler instance control *only of the specific window being created by the method call*. This assignment overrides the View's **handler** attribute. .. _creating-a-default-view-within-a-handler: Creating a Default View Within a Handler ```````````````````````````````````````` You seldom need to associate a single custom Handler with several different Views or vice versa, although you can in theory and there are cases where it is useful to be able to do so. In most real-life scenarios, a custom Handler is tailored to a particular View with which it is always used. One way to reflect this usage in the program design is to define the View as part of the Handler. The same rules apply as for defining Views within HasTraits objects; for example, a view named 'trait_view' is used as the default view. The Handler class, which is a subclass of HasTraits, overrides the standard configure_traits() and edit_traits() methods; the subclass versions are identical to the originals except that the Handler object on which they are called becomes the default Handler for the resulting windows. Note that for these versions of the display methods, the *context* keyword parameter is not optional. .. _handler-subclasses: Handler Subclasses ------------------ TraitsUI provides two Handler subclasses: ModelView and Controller. Both of these classes are designed to simplify the process of creating an MVC-based application. Both ModelView and Controller extend the Handler class by adding the following trait attributes: - **model**: The model object for which this handler defines a view and controller. - **info**: The UIInfo object associated with the actual user interface window or panel for the model object. The **model** attribute provides convenient access to the model object associated with either subclass. Normally, the **model** attribute is set in the constructor when an instance of ModelView or Controller is created. The **info** attribute provides convenient access to the UIInfo object associated with the active user interface view for the handler object. The **info** attribute is automatically set when the handler object's view is created. Both classes' constructors accept an optional *model* parameter, which is the model object. They also can accept metadata as keyword parameters. .. class:: ModelView( [model = None, **metadata] ) .. class:: Controller( [model = None, **metadata] ) The difference between the ModelView and Controller classes lies in the context dictionary that each one passes to its associated user interface, as described in the following sections. .. _controller-class: Controller Class ```````````````` The Controller class is normally used when implementing a standard MVC-based design, and plays the "controller" role in the MVC design pattern. The "model" role is played by the object referenced by the Controller's **model** attribute; and the "view" role is played by the View object associated with the model object. The context dictionary that a Controller object passes to the View's ui() method contains the following entries: - ``object``: The Controller's model object. - ``controller``: The Controller object itself. Using a Controller as the handler class assumes that the model object contains most, if not all, of the data to be viewed. Therefore, the model object is used for the object key in the context dictionary, so that its attributes can be easily referenced with unqualified names (such as Item('name')). .. _modelview-class: ModelView Class ``````````````` The ModelView class is useful when creating a variant of the standard MVC design pattern. In this variant, the ModelView subclass reformulates a number of trait attributes on its model object as properties on the ModelView, usually to convert the model's data into a format that is more suited to a user interface. The context dictionary that a ModelView object passes to the View's ui() method contains the following entries: - ``object``: The ModelView object itself. - ``model``: The ModelView's model object. In effect, the ModelView object substitutes itself for the model object in relation to the View object, serving both the "controller" role and the "model" role (as a set of properties wrapped around the original model). Because the ModelView object is passed as the context's object, its attributes can be referenced by unqualified names in the View definition. .. _writing-handler-methods: Writing Handler Methods ----------------------- If you create a custom Handler subclass, depending on the behavior you want to implement, you might override the standard methods of Handler, or you might create methods that respond to changes to specific trait attributes. .. _overriding-standard-methods: Overriding Standard Methods ``````````````````````````` The Handler class provides methods that are automatically executed at certain points in the lifespan of the window controlled by a given Handler. By overriding these methods, you can implement a variety of custom window behaviors. The following sequence shows the points at which the Handler methods are called. 1. A UIInfo object is created 2. The Handler's init_info() method is called. Override this method if the handler needs access to viewable traits on the UIInfo object whose values are properties that depend on items in the context being edited. 3. The UI object is created, and generates the actual window. 4. The init() method is called. Override this method if you need to initialize or customize the window. .. TODO: Add a non-trivial example here. 5. The position() method is called. Override this method to modify the position of the window (if setting the x and y attributes of the View is insufficient). 6. The window is displayed. .. _when-handler-methods-are-called-and-when-to-override-them-table: .. rubric:: When Handler methods are called, and when to override them +---------------------------+--------------------------+-----------------------+ |Method |Called When |Override When? | +===========================+==========================+=======================+ |apply(info) |The user clicks the |To perform additional | | |:guilabel:`Apply` button, |processing at this | | |and after the changes have|point. | | |been applied to the | | | |context objects. | | +---------------------------+--------------------------+-----------------------+ |close(info, is_ok) |The user requests to close|To perform additional | | |the window, clicking |checks before | | |:guilabel:`OK`, |destroying the window. | | |:guilabel:`Cancel`, or the| | | |window close button, menu,| | | |or icon. | | +---------------------------+--------------------------+-----------------------+ |closed(info, is_ok) |The window has been |To perform additional | | |destroyed. |clean-up tasks. | +---------------------------+--------------------------+-----------------------+ |revert(info) |The user clicks the |To perform additional | | |:guilabel:`Revert` button,|processing. | | |or clicks | | | |:guilabel:`Cancel` in a | | | |live window. | | +---------------------------+--------------------------+-----------------------+ |setattr(info, object, name,|The user changes a trait |To perform additional | |value) |attribute value through |processing, such as | | |the user interface. |keeping a change | | | |history. Make sure that| | | |the overriding method | | | |actually sets the | | | |attribute. | +---------------------------+--------------------------+-----------------------+ |show_help(info, |The user clicks the |To call a custom help | |control=None) |:guilabel:`Help` button. |handler in addition to | | | |or instead of the | | | |global help handler, | | | |for this window. | +---------------------------+--------------------------+-----------------------+ |perform(info, action, |The user clicks a button |To change the way that | |event) |or toolbar item, or |actions are handled, | | |selects a menu item. |eg. to pass more info | | | |to a method. | +---------------------------+--------------------------+-----------------------+ .. _reacting-to-trait-changes: Reacting to Trait Changes ````````````````````````` The setattr() method described above is called whenever any trait value is changed in the UI. However, TraitsUI also provides a mechanism for calling methods that are automatically executed whenever the user edits a *particular* trait. While you can use static notification handler methods on the HasTraits object, you might want to implement behavior that concerns only the user interface. In that case, following the MVC pattern dictates that such behavior should not be implemented in the "model" part of the code. In keeping with this pattern, TraitsUI supports "user interface notification" methods, which must have a signature with the following format: .. method:: extended_traitname_changed(info) This method is called whenever a change is made to the attribute specified by *extended_traitname* in the **context** of the View used to create the window (see :ref:`multi-object-views`), where the dots in the extended trait reference have been replaced by underscores. For example, for a method to handle changes on the **salary** attribute of the object whose context key is 'object' (the default object), the method name should be object_salary_changed(). By contrast, a subclass of Handler for :ref:`Example 7 ` might include a method called h2_price_changed() to be called whenever the price of the second house is edited. .. note:: These methods are called on window creation. User interface notification methods are called when the window is first created. To differentiate between code that should be executed when the window is first initialized and code that should be executed when the trait actually changes, use the **initialized** attribute of the UIInfo object (i.e., of the *info* argument):: def object_foo_changed(self, info): if not info.initialized: #code to be executed only when the window is #created else: #code to be executed only when 'foo' changes after #window initialization #code to be executed in either case The following script, which annotates its window's title with an asterisk ('*') the first time a data element is updated, demonstrates a simple use of both an overridden setattr() method and user interface notification method. .. _example-9-using-a-handler-that-reacts-to-trait-changes: .. rubric:: Example 9: Using a Handler that reacts to trait changes .. literalinclude:: examples/handler_override.py :start-at: handler_override.py .. image:: images/alter_title_before.png :alt: Dialog box with empty checkboxes and a title of "Alter Title" .. figure:: images/alter_title_after.png :alt: Dialog box with one filled checkbox and a title of "Alter Title*" Figure 7: Before and after views of Example 9 .. _implementing-custom-window-commands: Implementing Custom Window Commands ``````````````````````````````````` Another use of a Handler is to define custom window actions, which can be presented as buttons, menu items, or toolbar buttons. .. _actions: Actions ::::::: In TraitsUI, window commands are implemented as instances of the Action class. Actions can be used in :term:`command button`\ s, menus, and toolbars. Suppose you want to build a window with a custom **Recalculate** action. Suppose further that you have defined a subclass of Handler called MyHandler to provide the logic for the window. To create the action: #. Add a method to MyHandler that implements the command logic. This method can have any name (e.g., do_recalc()), but must accept exactly one argument: a UIInfo object. #. Create an Action instance using the name of the new method, e.g.:: recalc = Action(name="Recalculate", action="do_recalc") .. _custom-command-buttons: Custom Command Buttons :::::::::::::::::::::: The simplest way to turn an Action into a window command is to add it to the **buttons** attribute for the View. It appears in the button area of the window, along with any standard buttons you specify. #. Define the handler method and action, as described in :ref:`actions`. #. Include the new Action in the **buttons** attribute for the View:: View( # view contents, # ..., buttons=[OKButton, CancelButton, recalc], ) .. _menus-and-menu-bars: Menus and Menu Bars ::::::::::::::::::: Another way to install an Action such as **recalc** as a window command is to make it into a menu option. #. Define the handler method and action, as described in :ref:`actions`. #. If the View does not already include a MenuBar, create one and assign it to the View's **menubar** attribute. #. If the appropriate Menu does not yet exist, create it and add it to the MenuBar. #. Add the Action to the Menu. These steps can be executed all at once when the View is created, as in the following code:: View( # view contents, # ..., menubar=MenuBar( Menu(my_action, name='My Special Menu'), ), ) .. _toolbars: Toolbars :::::::: A third way to add an action to a Traits View is to make it a button on a toolbar. Adding a toolbar to a Traits View is similar to adding a menu bar, except that toolbars do not contain menus; they directly contain actions. 1. Define the handler method and the action, as in :ref:`actions`, including a tooltip and an image to display on the toolbar. The image must be a Pyface ImageResource instance; if a path to the image file is not specified, it is assumed to be in an images subdirectory of the directory where ImageResource is used:: From pyface.api import ImageResource recalc = Action( name="Recalculate", action="do_recalc", toolip="Recalculate the results", image=ImageResource("recalc.png"), ) 2. If the View does not already include a ToolBar, create one and assign it to the View's **toolbar** attribute. 3. Add the Action to the ToolBar. As with a MenuBar, these steps can be executed all at once when the View is created, as in the following code:: View( # view contents, # ..., toolbar=ToolBar(my_action), ) Schemas ::::::: Pyface also provides action Schemas as a way to specify MenuBars, Toolbars and their components without instantiating them. These are particularly used in the Tasks framework. TraitsUI supports their use in Views, permitting better interoperability. The primary advantage of Schemas is that the deferred instantiation makes it less likely to have unintentionally shared state between declarative Views. .. _undo_redo: Undo and Redo ------------- TraitsUI provides basic undo/redo functionality via the |UI| object's history trait. This is created automatically for |View| objects other than subpanels whenever the |View| has a menubar or an "Undo" or "Revert" button. This system is largely independent of the `Pyface undo/redo functionality `_, although that may change in the future. The primary hook into the undo/redo system is via the |do_undoable| method of the |UI| object, which calls the supplied callable while capturing all changes to traits that are being viewed while the callable is running. Undo and redo actions then simply reset the values of all of those traits to the appropriate before or after values. This works well for simple cases where traits are not heavily interdependent, but may break down in situations where there are complex dependencies. The |do_undoable| method is called when the value property of an |Editor| is set or when any |Action| is performed, including actions associated with complex editors that provide context menus. .. rubric:: Footnotes .. [11] Except those implemented via the **enabled_when**, **visible_when**, and **defined_when** attributes of Items and Groups. .. |Action| replace:: :py:class:`~traitsui.menu.Action` .. |Editor| replace:: :py:class:`~traitsui.editor.Editor` .. |UI| replace:: :py:class:`~traitsui.ui.UI` .. |View| replace:: :py:class:`~traitsui.view.View` .. |do_undoable| replace:: :py:meth:`~traitsui.ui.UI.do_undoable`././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1684768028.0238073 traitsui-8.0.0/docs/source/traitsui_user_manual/images/0000755000175100001730000000000000000000000024121 5ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/docs/source/traitsui_user_manual/images/ArrayEditor_demo.png0000644000175100001730000010314400000000000030063 0ustar00runnerdocker00000000000000PNG  IHDRPm. pHYs   IDATx{\ekB 𬉚eyP[yȏJf$iVf)C}DWm=$&*! O(sq}wnagto՛缏<8G\ FGSK#^;zhPM?χjZ o{EEEԆqssf'ԆSA*Zm˖-= ԰,!,н6RVVVdmr0C!h4ѣ$P(C!Ba0JKK= < Bm<@PN[dI===ݛ5k6f̘+W8z\pzZ{;99Cx׫>Ⱥ:PVxeeeVj)5zhk޼~jժ!Daa._a Bm8 f!Dpppjjرc_zO4Iqڵ9s8zp jP΄E}ra̙毌NLLlذbҥEEEڀ-Ԇ3!AQǎBDFF1¼ASNBhL EmjÙ"p!Dtt{#ؑ)ڀ-Ԇ!AEZVb688X.:uʞƒڀ-Ԇ!AEh}9___OOO79%Qp2 ܹsr!00 di3ڀ-Ԇ!AE~~~r͛n MAl6 *jժ\8ZVkAmj-Zț8fgg_#S[ 'Cի$Ȱ^kjdG jPNEɝԖ-[-Vfߺuk{ jP΄E >\ׯ_!!DϞ=7n Bm85jԨP!֭[F;&L 7HJJr8l6 ]-9sW:u~+W!z11 Bm8ծ]Ν; !zNB-^xڀ-ԆpuGСSSS322JKK=.8[ @DvڵkQADmja)0P(E@ ΄#@@9 NAnnn?='OPj@mNFvA3oj>:mXv# fGtBm8Y 8!/7QP\N{ >8=HPP}ᬸN(>FN(QGGQѣ# v]C.Q/cVRGѷQ/&//c2HR+斶jio DP(C!r@@9 P(C!r\=NZGQN DmY Bնlѣ@ ʪ9j)QEr@@9 P(C!!0 Dl6j Vrʸ>}DFFΞ=СC5jjy^z޽{O8񫯾***ϛ7o„ -t:ݒ%KڷoެY1c\rQp3~:u_}zmݻwGNLLB[T\\yBZ1שSh|n߾]ᏗXsݻw[WRˋu(SN۶m-1%ׯ[t~5pرۛ===eiLj.{yfvfP[ 'bj֬<#={vĈgΜ>}zǎifԩO>\F2-tO>d֬Y'O8qڵk6f///SHBuVM礏?>trvիW5j% D!D-,)9N8a1WSLw3j#ovӣ6` dT @{챥K`0[#...rAݸqCz WWI&رcΝ4{ߴe~$F:zh``#<"zK.YoݺgyGhBFy]ly;;2Qp2 V{E!DӦMGFFzyyݺuܫ;.eeeƍ?#.]*..4ޤG;vB:u{5ݓpWWWN'Vq۷_zuiiiFFFdd*^o&m&''nݺ^zAAAGFFZS#;2Qp2\ҹsΝ;n 7wuy*N7vؒ ?b;w~7 .\ nnn]*nnnI:wѮ];0e !Mf[b{Ngq''d˖-hϙ3G.g @5l6*k׮?֭[kKyeϗ<Ν;bĈ} !.\oo/ !~ 6Xp%y[y|+''G6veqN9rd׮]MOٳȑ##Gܾ}{~~/o.[M)..bnvڵkJ>==_J>~Z[lԨl_x,1c0m_~⸋ĉMVO:u22[bõ&Lzƍ{_ bgf~W|ĉ{|߾}k zdIݻϞ={ڵ}.Fرc([ի+uV^mډU[KMMouqqh4SIIIӧO~ʪԆS6` U !ԩ3?}UpBGz#x3l,Yfjر]yw/xw*s㷼tzlٲ]Z~ݺuO>AAAsεH?BkמyMڴi#o>B)-8quɦs_ h4#FSv%''C_z^/^^Lql6BGt–-[ޏ󳲲coo.++~OaaaNN" ~kܹ# ~%E9ңGr/'.]~O``}揓ALRSS322JKKp5>}76^lm\]]}qM5qooeee111EEE;w4oɒ%O=T?t?}kt)7`ڀ-N5jۣG:DFF/ǏB4@(GBBV]|yAAArrrrriUxxʕ+s#:t={.\РAȈ޽{׫WZ@SN:ur(y  r@@9> ***՛^EDDxxxTC[V\ Uxxx8za ' 'EmY P(C!r@@9 P(C!r@@9 P(vRTTj= 0777jvBm8%j AZe˖jXVVVQNڀ-68C!r@@9 P(!`(--u( 6` P#Ai:nɒ%۷twwo֬٘1c\q Bm8M^^C~~#Zmkwrrr8:TQVVo5;6[dmp=z܋5oO?]jULLp/_v0l6K||Cqq &!O8ѽ{aÆr Bm8Tj!!!k©S9*< Bm8Tdfg|S[ 'CΝ;'@6: Bm8T'n޼YݴAmjVZɅ[jAl6 *jѢ#;;zmvvhȔDmjz !222ךّ)ڀ-Ԇ!AQr'e˖tUs̑ [6` L@PÅz>!!}BٳqƎڀ-Ԇ3!AQF Blݺ511h4رCPq Bm8M^^C~~~˖-[RSS ...FUIIIӧOw(q_]5ԆS6` A]ڵKNNܹB˽ŋً)ڀ-ԆpuGСSSS322JKK=.8[ @DvڵkQADmja)0P(C!rxTQTTtĉ{x[4=mQ6 lZm D# ѣ@ _8Am8)jPr@@9 P(C!r@@9 P(C!r@@9 P'=|5u0FG(_~~~Mu[S]v9z v P(C!r@@9 P(C!r@@9 P(C!r@@9 P(C!.PZZQAA=@PN[|y^6mo^zcP0G=C1SRRsv//7k!P0G=(ocƌ7o,ݻw`zPK||IjjԩSAAAԩS6m|֭bNSO9zp do׫WנA!Ċ+ܹTCBӅ111ן8q̙3z9A5 `0;wNѭ[7Fc' A= TSRR" ^(N>maAD*L_5lC OQ "ArI&n|b3 IDAT ϟ?o!D*6l( @oLp͛˅%%%׮]3 ΍z9AA 7w12D*YYYkMA= "w^;v8uŪ ʅ-Z{Xp!C!z37mt!D׮]y١:TP 6={VweqqqEEEBŋ7mÄP0G=F16p[n !\\\4N>p jy7n%rwSsԃR8Eeeem##Q0G=)0P(C!r@@9 P(C!r@@9 P(C!r@@9 P+++oFcn=!HKK;tF_T`0t:u8z x P0G=8=M^^b޼y999͛70aHKK[tu{@@@TTTTTT ?*s7nhѢb̙c#-ZhƌB?^zNt:ʕ+/_~̙Ҡ޽{7jCP0G=N:m۶jY̙3~~~cƌ:̶mFekF0aBbb=dT%%%111hڸqcv2*8 sԃRj؆ 4$22ҔKJJΟ?c4.\xƍ?V-.zhr&kܼy[w^GC=rWw]_7n} >^ˤW^T4--mРA:t ݻu-[t%<<_~?xDDĪU C/8zaBBBwSOu/ج`-##Cփ0իڝ;w?~^w.1o_ĉ%%%gΜO sԃjl2^_RR2f̘k^vMtM6=Ӧ#rKz^o~SN`-,,,44ToڴzMzO>)[OGb0Ν;'֭FXk;8EP0G=(S`ׯ_߼y?aÆǏ_uBϛl߾:tB4iÇ؂ڵkO;v숏wuu-((/4ƍK:wx_uŊ?ŋ7nh1lrͽ{ve޼y۷o>QyVHHȽ+++[ٳڵߖ׽7n0}kzqmڴi󎟟p ]\\Ə?۷ B̚5k͚5?RJ(<̖j.B 4""K.C tU/_,>}رcecXXX߾}[XX0|p*r :76m2t۶mx>sqqy{ݝ[; zxx OQ "cǚ!lڴOo߮R͛W!ǎյm۶h۶yLyj~ذa߿wz4lP.قlOz9AA ///7بЩSLJZ6׮]zjFe{Ez6mjݺ鿍// of|||SPP0cƌ;w'O ˍmڴhrόBsO" Zh4Fرc'S =ܔ)Sܹc: }2FSUBNNڒy}i387ߗߴվ}{G |G?CCC8pҥ2Tj9sf^LڵkYYʕ+'LV?>&&=#~~~>>>>>>;v\&M^xr73*c~͛O.7u988>B=vӽ;8eP0G=(pWWWN7uԻoܾ}իW4z=Ds _lٲK.}g'Ox~^u#eee&mڴqqq#LN3335֛oV yGpGHFvp`zPP $777yI#[0ŝ2eţeMfqO=\|MOOO!ĢELŹڵKRСC[RRR[8ԩ#O~xFl޽`hӦM۷o/--[:cgzȽ\!sԃjj,ɓV999ˮ]֮]kޮFٵkW)Ȟ={ !92r۷/oeˬ?=,???y۷̙S'8t:WVVVs}dGO`]v5xܐ!Czԛ6mqk׮PQԴiS0eʔ]vn3qٳg8p '''99y۷o?}.he˖B#GjjȐ!˗/oР8&ƍ7N^гrJwʏO>B7oN4iyyy;w6lO?dY?f̘qŋ=o>>k֬۷ݻwߴiU^=y ^OJJK2?mܸqРA[7?֟լY/E-ZիWRRիW oe ޽{BB|o-oY͛77cߨQ޽{[oc:W)]+}Qpppƍ vU!D||;ܨTKlkU=zXlY׮]?LDDĺuLRΝ_[\vړ'On۶m'OܴiS6m+,KT:6.F٬Y3!Ν;M+9-Z=z0M4ibاOKʏ5j=^R6m֯_߫W/6m62 5ML)7xnݺFߨ7(Uk|s h6*}vYYcc ׷X{2ހgqȑ#=z0e=_%SZZz9 Om4/_|5__߀*<Ш\n<򈿿Mu[sΝ;WRIIIZHKK*-- j۶ʢ`zPA*+((,**߹s K,ywӧOx#;w}vttw}vtO|||FѣC%%%|˳Pضm[FFw}'_1eG())iwA\צMybp+I={.\РAȈ޽{׫WCCU۷CCC3p@G2W5;(C!r@@9 P(C!r@@9 P(C!r@P`(--u(`zpz (G-_W^M6 z7^q1ԡs)))9pEƍ۵kQQBZ}B1c㏛7oBݻcP0G=(%>>c$55uԩB}uԩM6uVJJJqqN{ꩧ=L sԃj Yd\xիgjkРbŊwq`wQ!A!BO8QQRRr vG=Ta0Ν;'֭FXOvp`zP)))BY O Q "An6l! lФIr7ϟې@Q "A 6 n }}}7&8sԃ@PEBNNڒk׮oF= Tcɛ;FNQ "AB,뵦Fvp`zP ;;v:ub… B-=,8sԃj@PȐ!Cz~ƌ6m:rk׮PQePHXX؆  Ξ=;v{"!ŋ6maNTsIKK8p[...FU|P sԃR8<7nBzwػ)z9A)ҲJKKڶmT@P(C!r@@9 P(C!r@@9 P(C!r@@9 P(ĉP:DDDhF/??|Nh=P(C!r@@9 P(C!r@@9 P(C!r@@9 PIQQVu(hnnnyl̳}<;j-[t(hYYYߑ1bygP(C!r@@9  F⹱w<lCt%KoݽYfcƌrJʊ tww֭w}W~H1<4yyy`j߻wŖ:t|7o)++h1b7|h6f;f'sg`F!8u=ZŚ7o駟Z*&&FQXX8`˗/WcǎƖi4k׮7o^FVzw߯P`yip&ұcG!Dpppzze~'O^`b„ .LܺuO>?~lxb֭o߾v&Ms?܏o̳5>g̙3_eذaC!ҥK*r/֪U_0e!Vkv>g` :v"22rĈ 4:uBfffV¬Y\]]W<l̳3!AE̙3Bh+ #*#3mӻwoU;wd?Ny>g'CS7BBB˅SNUؕIyyyvu9=T<l̳!AEV}9___OOOq/GwGΞ=[>Ęg`yv2 ܹsr!00 di {?0<d@P\yfvfvu~*ٕayVZɅ[jUUW+<l̳!AE-Z7qdgg[6^vd%%%kǧzC~(1<d@PQzkMߑro])5y>g'Ce˖-̙#Zn]~g϶X}v(㬘g`yv& (jB^`޾~C !zٸqcUwYb?,tK:u B[.%%n0z-{~l̳}0΄E5JnݚX\\l4w1aARRŏ+/r/_njtuu6mh49B7n'NBa`yٙ6xBB!FtrURR-z?߾ Sh|嗿kϺu떖o{AAAx{`0<6xxGbJ}~?~ŋF`0!Ν+_lN:vzG>&M5)))eeez^?k׮-zUB̳}0Q#*əfddGEEyyyDպԮ]Go999>>>5 ,1<{UeG~<lo"hP(C!rxTQTT$_Sy//|i4{>""ã2vQ#85@@9 P(C!r@@9 P(C IDAT!r@@9 P(줨H:z4777jvjdUhږ-[:zȘ 1˜or#tgϞ`b̳}0<;Tt9XݴY]ݽJv|g`yv2 O.ܼy di {?0<d@PQVj\*~+ӕay-Zț8fggFq/;ܒrGS!?g`yv2 ^zAAAB 뵦ȌFc~Ȯ<l̳!AQre˖tUs̑ [d?Bٳg[ھ}|YeqV̳}0<;5|p!^OHH0o_~C={lܸ;wXN: !֭[bj7 o\oǃy>ggBF%w@[nMLL,..6;v0a ))G^y啗_~˗/75N6Ma4Gya!č7^}'N!ziWu0<Lxh~?~Eh0~~~sΕ/dPN]v=#~a&MLfB2^/۟}ٵkזS~~~z \!>gyvALRSS322JKKluj]\\j׮m#~لyg`=D*#t`@by7q4P(C!r@@9<(**/٩<>4=mQe+B ///$$?<<͛v?q{~;zaBBBwSOuAw!:::$$w֭3mvZg}V_?`֭[@`0L|/?tÇ0@ܹHqK.Wի3gBswxѢEO>7&Oo߾3g|B#FA\\w#F48UTz֭޽{7j& ̨رc9?_$^pFYwr͛DWhfVVVVUUUVVFRi""" "P5\._d jժ߿O>`&.[n4h~{=Ϯa۵kWDh^*\LKK;ydIIɋP5͛o߾MDsaՌ[SS_,,,/eeeM5l]v!a· cbb ُ:tpsssvvvvv>vX4@B."Zx}!?|ĉB{RR?jԨ1cƼM{ ֭['J0ֲsNV̚5+!!}]Pxzz1CίAD˖-cOJLLBذ_YYY{ٗI:}4w#!K.*X*5Fjjy Z`;wּ `A3gΌ9rdEEł vaЫWׯgffnذA`ke]tajJ\.a bӦM# Ə7''ҥKG+MPcծ];"rttΑ0 .|葕UzzEDDɓnzر~wmܰbXp'JKK=z4tPWW׳g~UUUDrJ 2$'' ,:uK/W_ =jc ԩ#;77wD~La|1ό)ٳD4gΜݻƔ\rrrrݻJux㍗_~yҥD5f;}xqƝ={vر#/+Ge+nwmijb2ld"OLLڝVX:$%%= v6?#HOtm޽+X]tQQQ孶m.X`VVVsؼyY\\\؏F H$&L0m4ѣG=h}h&O @Eֶmz4rnr6 lMsMڗ^zgѢ 979r6&ɹY         ""Z-˛d([l4sT*7mԫW/[[N:͞=*//oܸq>>>NNN 駟|­r6 lD#}ڶm|//};b3rևM92_|XlSRR\\\믿np۷oX׮]g͚%{{{/^jkk7nشoEi g@pիDԣG &ho?$ǹvX|vBi g@HV߹s!C؆12+޾_~Fci g@f𨨨ߟmܺuAё78TAA=xM9r63(GG+ᘥ瓜qJRɶb4i g3xTPP6||| v`B#2?4i g3x6?~lk58T9AΦM9@]B|ԩS/( &ܹS"4n& !g@Φ$97 ~M6ł]vQUUȑ#KKKիƍS(Xzuǎh׮]}xoU@ΦM9 ^|ӧ[pND_v1#=СCD_dzƒOm6??˫iߎČ!g@Φ3@_|6-[(㔔"믫(ֵkYf ދ/&ڍ76[l49Az*c„ ۷677q]6/_nee+!!f'l49AD4h ^M9r6'xݡC5kxyy dDP(T*k322 2ٽ ȹA4i4Ig7oޔᎎu R[[kiiimm]Kݻwou0Yl4Z @zdl4Zo;(;(;(;(;(;(;XxQ]]c<Fۿ"!AΦ,97 {{{7,Z&9!!g@ΦJ X]~`XhhhS  44Th{7PM5vܑ|866qRill\.H$'OJϞ=qZ8poAAA HvvСCܹsvvv=%%eƍD4s+V3?ND+W>}:k,--߿uuu6m<<~xɒ%O&ȥK5'd+d+d3< "JG"H$JJLL<v="4iRzzЮhw~Q(#GZČ![ [ [n cǎ999FVkjjy[[[={---ChH$Ç'+W(JJڇ e˖5oV|866qRill\.H$'OJϞ=qZ8poAAA HvvСCܹsvvv=%%eƍD4s+V3?ND+W>}:k,--߿uuu6m<<~xɒ%O&ȥK5'd+d+d3< "JG"H$JJLL<v="4iRzzЮhw~Q(#GZČ![ [ [n cǎ999FVkjjy[[[={---ChH$Ç'+W(JJڇ e˖5oVlukkkssssrrd2Y#mPuuunnիWʴWdgg7E+JU6_~[}Ro1xW^yeϞ=}7"믿ݣ=hv׶NT*W^k+$$dŊce bժUaaajnݺu6mڴ ?~``/>>{ٿ [9ak0R+-Zj*" o{޽{ذaϟa-rsϵ]LJRT/rH"XYYΩK~gG***uVSSkZ=Ny/2b)iӦ} BFiy#nܸo>}ZOQQч~7+JuũSܰ]3fعsg=/믫T#G***RT:kIII2dH޽O<ٸ`U\#?ŋ?ĉ8..=d2?OBѦMO>_ӧ݉hŊ7yȑ˗/GDD˗/_FO2'' 6l۶y/~7}%;v|WB۷_~ ?deemٲeԨQDi&JII?gϞ,ovȑ?p@#&޽*KK={vŊD|~Ackk~… .,X-ݴl27rHGGG"ҟ*ka-_DR:t ่rtEEήO>uփ֬YCD~~~{qqq!aÆYYYmݺ5''g۶m7nx饗^ziӦl=zڦh4/1ޞڴi7ڽ{l,55wgW_}uĈ˖-1c _QQQnnn_~-9244\Zaa!,gWWW6QƏ߸3%饥Dϙ35 :tСUUUK.v[۶m322z~LNN~Ν;ziڶm;f̘;wo®Xرc7l`iijAQRݹsozj]6֬Yêˏ9r}O^ ʼnc+\~@/bӦM\^\\|%vΣ0sZMD~RRRGPՋ!cnٲƍjBVk֬acaaiӦnݺ5B 0""B~N:%''/Y]v%T?[oŮUQ\\[ȑ#ƍcqW虖:;; %xL( DR^_C&44TVRRʪ*~[[Y 'oʕ+l̙3ڝիWY$-((`sbjbWʈ]֮].]9?aDm6\~M2vcvvNSϫӧO߿BĮ g(&$JO?x1^J!!!'N(**ٓ(*++N<)WsQ%%%EEEDh"S5&Mk(!#G|WgpR)yxxkNont .deeJP䰛 .GٵkWDh^}nt:y H$qqqVҾ Ʈ==!Jdccc1Fg111:9;;;;;;vq/oٲeB ロ?~۶mn&s<̕+Wz~OW^Ͷmllڵkl);W_}`Okk:ޢ\HqqqW]r}_0f{'O0ܺu韝;wg֬Y I Pj4ɓ'oٲ?φ .\(ٳS:ԦMzy_|ADޛ7o۷p+ӵ;`<~X$Ѝ7tZtK޽B`aeOXXX<~]x]c׿_~&|҂/տ#˯]DGi?ҥK h"vu넛{Y)J\"Ԝ9se .t e:?~󷱱a ^)^i v]zw5hF-菐wwѢE*>Lrr2[QþuT*oQ(:54znnns%O\R JE̩S"#####Waڤwŋ/_r=z`/^$Z̚5qk9t6ln/((X|9wҥ#7l۶\.O}T׿jjj222Μ9Ӵ jAGBBݽ{766v׮]NZp֭[{SLa BD?^`=ztأGϏm,^W&9w:w}'\H(''w(**qg}6u۷o߿=|cN͛7ܹ?͛77lo럝b.ӦMkr .dΟ?e˖qƱW\fFID[l!޽{X͋w дZ=@D޿׮]W\[޽,LDǏ(++۷oCƎkUoƷ~KD믿کSߍ]bbŋU*UjjjFFkONN.))ٻwoff6ml۶ɉ{͛7WTT,X@ckk;|p۷/_~֭sݧOFgb5jTUUպu2IIIiY 9~x1 h ƜB`=%N>lѢE߻=zO?!4vQQQ۶m`׳S,l|ATTԖ-["##ُL]A'vԉN<)ٖH$֭W,z7O:5x`qiiifXXXho%HFuA֯[n{~!??+Vh]wmSfcǎ0o޼6mڸ׳@S}P(\yT?>>>,}zyyiLuyYaaD" `xqO<)..VT:tpww7rFV=|_~=]˺LQQqYϠAP@KD}|}}hF(@Dk߾}HH￯LVJe &_7V&hL&}1 G:u*{7 ييr˲'h9s8qbbb,,,rssrÇccc$JcccrD"QQQ:c݌l [ [ [Ρ^d2"6n߾P@N`\![ [ [Ρ^ \\\y>HT6v  EQQ2؁sb}S ۨ4؁68S8UUU:hxx-P/Fqq^LVVVݭ޽kpnPييrsv`WVV&]^pέ 7?i4z ;;v֭[:֮]6tb8ڿ%8y$[͘q  Ș1cHRi8p ++"##ݵwddd9s gzODj0[o%hxx-4xHHHȏ?XYYRciiy̙j"Z~=[C0{{F {{Gх ===?~dɒӧOQddҥKM|JG~ YZZJ$Rv%&& wM4)==]hh4wf?( ȑ#ubƐxx-pұc蜜RFV555u޼yϞ=斖!K$Çѕ+WJJbÆ ۲eK] š7d+d+d-NIҼ<\۳gOzd2 kkk{+**]VRR ي يpK%IDAT~j\$Cd&l [ [ [(m۶EGG/ZÇ*??ƌ#F8x`OAAA|`8R466V.K$ɓ'GDDHgϞݸqCV8P [ [ [n p$;;{СD{9;;;֞qF"9s+jǏ'+WN>5߿M6YYY يي G6m6#%$$oߞoٳ)((`G)S힞D$ɶnڤso鐭xx-PGrrr{111ڵH&ݹsq(99J{ܹs܈H*6մ[d+d+d3@ Z]PP@D пQݸCx15>;wLIIquu裏|嗭ZڴiSS\Α#G?u3g w<|* I twwQF)}izz!iObuُ&&&"(#####ڵkWnvͷ~[\\ܤqرx3fgϞ=}Ebcc[z/-諪 \~L&d&JUg5+>^pIIID+|m۶%"ss'Ο?$rƆ@?MiiiiiiqqqӦMk8ӧqqq'O|P(tqq iժ 8w\LL̐!Cᇴ4"ׯ…  =zuܙmm#GRRRLMMv@}o&"GGGcDzZn9ezzݻ***ܦM֧OWW\\[y鴴o(66Ȉ밶ߗH$˖-hYӄ}=<==m\_L&.]DDZH$YYY_犊\KK#G BTt#Gn߾BaYYYRRRvv?`kklٲX,>uԩS***&Nt_~y7,,,vK_>ol\p뿶… )X?X1<x  ~~~~~~ƬcǎDTWW@?:uRH򲷷JIIa-.]ڵ+Vs9|7}O㰰?SH$bjwNDDԣG@nիW輱+Dtff]j|'S6m:t@DOfK$u4EhҥLǏgϞ9rQ|ssssrr">֭[\G"""ryv\\\[nDt6٬o/lddt… .i"SnkjjbŊYf9#GܹscΟ?ߩS\HԹsFYrCD+V wɓ;w,,///$$C~ӧOmmmx㍖@OK=]&38`cccaachn(@i^%7z =CAP1hXhLII!5z =CA{SmˎCߝ={Bz=@c( z=@c(aK@KR?\.2dHϞ=[z84Hdbb~\.MX@+W~ӧOuDꩩ;w.mذ9(諫,X 9""B!`w}~~~Bcff_|\XXhff6m4@2bx֭۶m?E"aÖ/_ޥKFo\??~,Wg](*++E7llm@bbbbb`ii;?m4,_}պ:+++}||MLLΞ={|cWyJ^%7fff*O=3-"))i TWWWTTr@>}?^xwޑdB044tС=>hLBDnnnǏ߽{H$7n\zz. 44hP~Ps3C/܈C'U3c/6--M> cccrWWW"244wYKMM ׾e>sL]+J3ͱM]]]ZZZFFBT*KII͕H$ wѣW70[ʕ+(&&JeLaa!;pppΝ;GD\eLL I$M6S"##uΙ3gDwޒu4I ;kر\D"tԶmEB:tqttl߾-[d2YSedd|D4uW_}0V vk `ڵ uƏ!"T>N0AP"H$,Fx>]r@&)Ri~~/|ر*ٳgZ?[QQ~z2sɓ'߹sk)..;w>0zG&J$[[ۍ76#" DTVV2… _,[7UZZZnܸ"SVsjϧK.ԄKn;;{*orU<t͚5{"۴ixb"H$7odiiiDj_N@@… 53eFՆ^*lܸ޽{>ܷo_^(!!;vT^^^Ώt4BBcjj*;(++;u>--ǧm2p@|3KHH#Kk|QQYZZ^xm=IDӦM6l#GTVV56\鬀k>M*Ru<.ӧUtdWq1cč7֭[Ry^U޽E,ϛ7\\\>SuRN8QSSciiپ}{~%K}]Tző#GuQQQ}йvkkk)}?u<.Q}ۥ^^^tЁ$Usz-**mڳ`} }GQ]]͞$ЦM-|W6h v mQAAT*eq4 , *tmɍohhx9S={ BTz-ʏw|ʀ,[֭[CBBryNNk¢ne*p{hK/6vrrb-2]Ee<q4%Tj'ZM,_r֭[*#bxϠݻ,X@,{*Э|J-Sт 8gwwwww'O}ѕ+W~ŋfooփ^WWWĉʊm|~W}066a#F'b~+^o}7ZݻwwEDllWWWڵԩS*M">>r54_#g͚ED;vTyvɒ%?ӣG.__bEfggBpѢE׮][v- 63c'AAADоuVb۷oъn3gO;}MOO啕| kJO`ڵ555r<99Q'..Nɓ';vhw-PUo^&M:U ++ӧO_#̛7"R4<<{ w O0/H^{5.Ą{322/}yPhddĥus󆆆j}-S(5XrfЕ7N4`Ν1wBkkI&effѣG߿Y{v6mxE}H DEEEGG.|($$$..NaPPx ;eE"L&#Dۀ,--upp`[UG.m۶sFgϞ%C6\;;;__߆wx,))IOO733swwgOm\.OOOήuttjxRuE"P(Tx`<?Ӓ zx)ͽ 4"z =CAP1z =CA AGxa@w~С-: =;U!c( z=@c( z=@c$87n$%% ٳgXXcs\..D" wؑVXXصkWooo__߹s皚j1'N,^BBB ;0H/;{q?J;v?>99o+2],oݺu۶mH$rtt6l˻t(D_$''M6M |tE?̙3*ӧOnn[PWW5ۈ-:l[ZZ6K6m)^ua֭SG9ٳ,_}U/s@Ϋd*R*rg>]r>q:QUU՚5kzյkT~~FgϞ9sslٌ\.~=sJaȑ#uuu__>##c̘1qƉD%{W]]m``a@733&2⹔ܹ3;4J.Gk׮3g ۷O4CaaaIw__Ǐ?~|NO֙Ν;\ IDATя?o?|09;;\4bT*ٸq{>|o߾^zQBBo@9s!!!ζ$"52^^^]%F~w)++;s M>]Z\QQYZZ^x1,,ٹSNӦM;}4ȑ#_z0Vڶm sJ]k&iϥ4(P z|{-,,LMM- BCCt3bĈɓ'hZ3ǏoӦ VuuuކN8qΝ;wcood"J/^TN?s"ӧ?Ȓ 9}3iS%MAaÆTvcBB[ӷoXK]Ϟ=ƍ݅֊ܪ^U4Ν;o^ԠAARR©Λ7o-m۶-;PY4ras~J@}-諪o3z5k,v|"/XpxxxIIIqqqFFǭzjyy9{M믿b[FGaɓ'O.:w|"U~999go'T*UX[[;99)%nAaoooggWͬh0mޮ]?ze/ʞtaɤI뭥TTTteg/_K.?CG}XYYݛ=ZSSp}ADZ<_TTľUgI(%nA?uTv\ODdjjB[ ]WW'k?V?r^Ժaeev9|p~~>Tyy~KD>>>֭[Juu]BBs믿߽{w׮]Dѷo_GaooODQQQ+V#z-r-[pa?~,WCnXC޽[[[{֭‚?> OXɓϞ=[\\?l0,rݺu[ppzy3HLLLLLT3x֭l۷?zhEEEMMMBBY̙3YdNNO?ݫ׹>}ʊf@vښ\}r} }J!Jryff&kڵ+kQ?Uנ}G f͚U[[˂ܹ!Cǵ+r/W K$ߎƢQA/JùDaɄ "447Y`sQ B<M%kŘDdmmvh P(5{)))VVVvnݺp堛ە+W;WV>Kq_ײF.[1m4>@>rnddghhZYP&mڴ155ҥKppRffɓ;vhjjjnnw^LX _||Ǐ%}r-zb=_xQnNbbbbb7oԩʕ+㓒͕۶m{JxgϪ^}պ:+Uo%7{5k֭[? f̘u={̝;ؘ>(~GUYYIDFZ~w}7~VZ=~xȑΝSĔ)SRRRm˖-qqq3f "H4nܸ/;2L(Ι3QQQ;w&~?1r@Qп4`oC &&&D5J$;H~O?$ ( @a0,ťk߲e k9s Nr宮Ddhhx=]+|^EEE7nPS3???--M;8ÇwҤI@"zjyy9k}͟?Z.o߾C^^^n= d2YXXئM֯_|ޞrks;xW={T*f__ &(ćD"IHH`k׮UXح[7~|2FﱮN*bE*JR":~͛1I$貲=z,]ŗ/_ JNNڵke s<%Y-aiizgggwwwvnܸ"Sӧ;pĉcvﯜyݬR-teͺo߾7n]mo߾v[ne۳۷9ײ1c<|ED]uUvp}vz .](s.YʍtX\.z4O\P3 9uꔋ ݻcǎlPx̙޽{ǰal4W>233,Y"ssso߾-,--W\9uT._ d}͜9UVv%aii)kQYЛY[[q,Ee<gK4O\P֬3 I&iZoooV3NNND6_qqogffg*=KKKйsLR]]bqxx]؏RGkVp) S4%5kA#kkk"ѣBdϞ=nh׮]?裏>h̙ 2008p_~;xK/DD?_@@զM] ٲ{SSS#W+`oXJMteZW|h)22222rΝɩNNN2lٲeͣD蘘Ȗ!/&%%űq̌}FJ#H aܱx~;i<.YCl۶- ;եKwfdd$&&^tήgϞSLꫯEmۖ, .C\w\PP Jٓ~MteN4\.m=|||Hy~޽{8p`ӦMlׯ#''GnwK///ڗJ*?kkk'''tejYH͋S8O{#!!gKKK+++ͦ <<<<<=_Vg*Y"bw=zF!=2VXyHEEEIIID}l4O\PVA/NZx1 gZ7oD &l߾h޽Æ c[UFDDnݚٳYѼru։D""xرchٲe)pSSӏ?G}u"OǤI;w؞E^ E||Ǐ&;;{菂'#-ZDD\ˣGXdllBϐv6G{F$&&&&&\RR­!"P/쒒O<9rd}PLpsW BnrD>f0&&&Cر)((ZUWwŋl~%hѢݻw+4r(4[TyҶmSN-ZƆR){\T֭Ǐ777۷Q^qpIF*=+WQP(|P>QDD-+11111Qp72СCVVV,Y9GWlbbro\*OVc }KJJڼysUUU'O.//o lʔ))))De˖3fH$7n\NNN37V.4ZC}e2Y:vxUɓ'߻wzU[[*scǎwSL0`|ɓUVܹuYgRi^^^JJJnnD"iܪԒLV\MD111܊XVϝ;7%%K.*d֭c33gΜ޽{oES26P_3?wppҥS۶m-Z$+ԩS?oggשS6ً^FF_MDSN}W k۶ѣG0T*=wN0Alhh(I$ 4LٳWZşX~}pppcJ̙3o֕+W)Sl۶^Mdvƍ3ƍ Dtƍ'O>} .4O.c4GAz۷QPPЯw@"ڷo7o177333oݺdPHDK.n˗/Qdddv ;vlÇ@1//yuhDboݺ5nܸ޹sGhٲeDx1h1((((((%%e͚5K,QD6mڜ'd2YNN\v}.jx]im+Y&\Ҝ/++޽{ѣGkjj>|Dz6C.c4SAOD ,de~F}f}7Wѷo_"ruue?N8QnONDѳwڵ<<111qppѫ@M>>>p1DRM%={ ٵ^^^VVV/BaVt(*Ǐ4kA{v zx)M 4z =CAP1z =CA AGxa@w~С-: =;U!c( z=@c( z=@cuuu2QDM/kkk.r@^"8p_ԩۇ^;ճg^zT3p@sssSSΝ;O0aΝ t9y;ݻߑX,ӧO6mLMMtƊo\?~,W۷uV_oÇ/))QFbbbbb{PWW7ydiӞ>}~@W_}N9G9ٳ7V. <˴DnnnDԡCƺ=zp IDATkg)C a\cyy9 `hhx=x}˖-}̙7V.(kPЯ[[]v/Ϗdff*H$\eA?`pҒk׮xOOOTʏ?ة0~GK <}￵Z}AAD&&&$''ZmΜ9e˖k׎fr…ʝ9 vrrڴiB- Bɓ'7n}{c.&..}V(FGGk=H7o444$S z+++"Pرch߿Zkސ=zp-‚|}}z;|v|\7$ܹs'++ ԧO +Jg޾};bEEE\ ddd >ߘVYYgT*JDk׮+ѣoSyeeebTZWW L& e.6nX_T*$"gggݻw߉(77K.Dtv[yݬR"qƓ'Ou_paĉZ Խ)6##4P3]vuvvZV^ 堠_5//ѣDo߾˗syyyG ,8u###Y-?(_q&&&-:rHbbQ֭[oL&kժ'|xիWoٲ|\|"##_ ( }%"n!Tvp}vC"VXŰ_.0.;2%;C_TT<==5@QQѲeˈ(00رc&&&Df͚%KիWڵk?ccƌyg̘c6mkΪUXվaÆsFooaÆ 2DIF Qo 6xi!CRSS O:5b۷oO+KKK^7DTVVƏ/ںLx>]r@3%%%[Ytiv655U.њ5kq6m/^LD͛\0r<TUUu3Fgp[A.**Z`Gط#"6mpg/_~ܹĿ=((,mۖ R)ɊU,.,,d iϧK.(S󳰰x˗E"Q}+"/ we~ ϝ;<׻wVZeff*bew57ԴFׯ+?Hq|*V[n ៲z^{5~#G.\Jyyy/Bl}kׯ@ dl x Խ)Ȉ=3//oΜ9:t 2L,_rKՋTFr:ZP՜8qV{QW-b۶m}}"|A$"eeeޮG(>|X[MtRlVV1Rx*j֭[FneE߾}+++w޽{O8ZBCCY~ˏfkx̤R)k֫*{Ո#$ gq}Æ Z R%{`ͻ[;Jj{{{"/**y@Uu@pA5穜&M4-,Vj}2xGc)O6Y*** * "(ᜳڿsAd~q{k?߰ak绶;wt:kQoX>~A^kۓbkEYpbFqڴikaaɓ'?cFU?R|e? Mרw1L-Zؿ]-lү_?u ;sLjj3gz}Vt^u~˗/h⪻@9snh5QRR/sݺu'O U۷oB 6y%ԩ^߱cӧVvigI&M4fl:5z'w.]{@3/ } ܝuķ z@~6lXC68g*z@ #F4@h0=az@t:m6[Vkί(Jyy0Vtn]tٳg^|}}sh7YfɌޮ]_~Ҳe˖m޼ԩS#&&wޓ&M9rd)))ݻcbb}I&l޿T1bĈW_}522^/-[)н{*ӺuGV5UzڱcG!D˖-+0C?ydXdk{rrr@@"::u~СW :t 0'Ot.++SW\)~W_uލ^[֯_"駟*-+**JJJ:|p~~~'`'N!VZTiMjj꧟~*7n܋/zK.=\PPP^^ޡCdCv!޽#<ZJaz-,[LXxdRg̘1d!ĺurssk]_k ˍln߾Я:rnݺٳiӦwi׮h/Fh4vUvZѨ.;v3!}gI&v[}|tN>nXJ5k&䔕:toɩ~ΝŠA--+((5kڞ}w,]ѣIIIK..))=zsdeBBBZZڈ#;vLKKKKK_iii_~|wq !䝔pR~~ 'Nt]q]Yj޽{˖-k֬YҀ g͚ػwѣG7o޼K.gϮp?{"88I&gB*"zPB{v}KIII~|]ZZ(Jv~mNFZ`Affhܴiѣec׮]###xUV-\P!7wFGG'󋎎.-- w=W^Q>ӧ_ʌ+u… r̈#:K. !6m:bĈ:vرcN:g4)ޯFϠNW?88X]]v>}Z.ׯtܿbĈj&Lp7 !ԛ8kuÇBlڴɵ}ƍBvUz0k,مK N@vڃFFF~Gq۶mi.BVZUzB]VjZ׻K_x }XXؔ)SkEQ._|̙ׯ_~޼yf%%% !~G\=/"WI|j_^~b޼y8TJFyGATTԶmۢ)S 2dȐ!~ʕ+fPnvu1lv;TiZ׻K_xqo׮ҥKϞ=owܸqG4.....S䄅bܪ'X,ꪛB]ml6O?-3gUu:]dddZZbܹj"##̙SO#G !/\pej{ppۋJԻK_xqTDDĦMZj(?n8uMDDDͯQ.ש Ǜ6m[@6 jӦMO~-,_\:k֬lC%%%Bғ'O !Zh!gܣd۴Q^>`0իIZ}ʯ~=ϟ%7u B-Zlr|n݌Fn,XP_WC=߿}VBݐ'رcr{BLv{gZE߾}wp8233=դ !{!D^z<}ieB羦gz!DYY]DԥK!3J.ԩaÆ}r6n(1L&Lug=`RQ=;]pa׮]BAt:ԭ[7!_]VVE~zښֻK_x@t:gΜ)$k||u\ GlTooo!oe<$HHHk6mpرտK={ !:t x.r&B#^wQF;/_˿ɓ'o`AA<*--mڵBΝ;մtڵ?@UlْT|httؼy5kϟߩS'y6__3g z7HLLܴiӀd~zٳg;v={V=dXtrW\o]|Eh۶m۶mICmyH&]!Dddƍ/]tҥM6>olٳgbb(wsõbz~ѢEeee޽CkW?qD^ )|}"##evT8p@iرnrϞ="!D]q?_j޼nŷ.u߮6m7n;V-0Lz'%%y^">>^S`0!{Z׫ϱ>}z[RdcNbcc-Zt=zݻ-[Vv/RҀ6nܨ~̙3 ݎ 5$&Mt?9+}-['4k̵ܹ7`0lذ矗m@_p,XPӯY?{lԢE yo+mٲ_~!!!2F1::ǧ⒒rJ^RRt:6mv^?W0U JOO?x`YYY- _Eqnnnbbscbb3z(Jbb'#""z-Vzj0x-wׯӾ}nn4d->eϞ=ɫW...B+="h>w}.\N:|А4CBB CG7p@CM<)@ F4@h7QG|4aÆ503 #F4@h0=az@ #F4@h0=az@ #F4@h0=az@ #F4@h0=az@ #F4@h0=az@ #F4@h0=az@ #F4@h0=az@ #F4@h0=az@ #F4@h0=az@ #F4@h0=az@ #F4@h0=az@ #F4@h0=az@ #/IDATF4@h^o߾0|w;k1%F4@h0=az@ #F4@h0=az@ #F4@h0=az@ #F4@h0=az@ #F4@h0ccSt*5zNE_=޽۳4 8.vn+ n:NFe4,`С=ڱcG*b d2 5u @aZKKKKKK}||?[O4pM֥fl~~~M4!g0KKK>>>K^rL[,P͵7rp0 \&M CEEE5 EQvln쁠fy?tu YQQa2{ h|&tV@p]tB)?@p`?J@ph@u%%%iii.\h/L=VFzsM/`ʲ V0LM4 u+NMMX,.\~|51Ctg}j̜1c#] 4/4:wܽ{H߻wީS'^۷qizz@cJLL8qSSS=͛'jժG}T6>KMM]z7 ۷BÇ x'YhHC}{٦Mf͚yyy%%% !Ο?ߩSzjUd2nwNEQ,Ns=myy(ՙv;aiiN3Lz}U3us}+Nm۶b={tvС:?/\pܹ蠠 bypCnJKKϞ=k۷o]EeEEEjjhlݺuՏ/SO=os=СC;vBtGQWZumzkŊe˖/v}vƌ6l_nݒ%K5kVzWu 4|njժU5AAAyyy6M8wh0`b$&& !""""##ΐ})!D׮]&gΜWڴiPO:СCӧO8~ ! 233 e Z馛3 ^UPPaXԅ|7z %7s熅}eƌ;w߿#""MVGG4hА!C͛7\t),,,,,l۶mޭڵk;uǏߦM!C 6,::ڳgo>f̘֭[8pСQQQÇ<+G9tPUՋ#oG_;sk~gbѢEnShÇo߾kp7?~٦O.h`ϱ-ZTQӧO޽{sȇ^?;Ec^^(FQOb$''b!DQQQrrrIIIcW*//OJJtϟd5q8qٳ]=.o0z]EE(--6mƍ/^(vW_}uwl6'N?~֭iVu߾}?kpaÆ ~>~ ߾}bccߪݻwƺFm͖Ot\z :do>v]=;;k׮cƌ=zp]w9rd̘1UܹS4hݻ !;veryo_w%5wU@X,zOl6*)RfYffKIIzM6zիW6mt:HII)//ȭVj PlǏ+룣{ճgϨ(clFFnٜNgzz(f[n ۷\t:]oz7=[{qqq6lH322}… <@AAN{G7mO?͞=;""Bo7ng޼ySO=_}Ո#999>`QQks^{bhԨQQxG/\G)LΑwoܹݻ;BڵLoi֭[?vBB#G~ɩJBu#PҨSvKk]9(rQŕ>HEg'<2x-[五M!44b_Idd'M4ҥgM7ti_I088Xmoժۭ5k@y_Z0`@|||zz(o֭ۋ/7&&fǎ;vȑni^=ϐ!C<-;vٲe)))G-{ĉ_~eÑ$}ii< ?x'xvWB4o<99~ի0)9v{vu:3WEZ{v}zź*++4^) ! G]o`-0 ȟYn4=߇+**P/}&Moٳg/_ܼy dqkYM]^^~q!D=ט3n,!P!D``m܎9ys1k2V+n:eʔ!Com>}>>;vشiӱcǜNKz>|i޸gP:Ũ4dgeeɿZ{v}ִiSQaaa+Xԏ2k,up{~l6~駟~pϞ=k֬-˜9sQ7}ܹpdffi!88m۶W^rsnB޽{׮U] ,$$DLOOtJ."K>>>yyyފ [Qz_IIG̓W6m\OQ\\\iشiӦM9s&33S.Fwl[ 5%wW_V"رc￿f͚GB$$$\Xքꫮ/8P;#..BTTT:tWo\C /4JmTW훞~ᤤ$OY[bk4zGڐ;;vlѢE'O 6^xrsٱcGrrիLbZSNyn&EQ}˗'''?o8q-.(,,9sfBBB^^֭[xo]tILLܽ{)S?t:KKKoBf͚z5ĉC={5e6999wyÇ%%%sYzs&LpO``ŋ˿ѣG9]g_?uԇ~o裏jw-yyyuMNE߿?!!ѣ_ohhhLLDfZzsإK(JFFFbb}?.ױz0sss=w. ,((8tЮ]>z^]w_P͚5 V>?~a^~崴4!Ĕ)S'""b .6lXݧNz!ĝw++'O,?/N:zaq;w=J=~sݺuϟKtzu5b?oM4mOztaÆW^ywߵZrQ1x~k׮ 72Pͻo N%ubb@c+**SWq*((izh.**"߰ ă\wzdr:󩨨(---))l&I}NVu`ǎn- Bj2<77n{/ZoFF???O ,pQp8Np87Ng0FFZi0E!tJzT`P #F4@h0=az@ #F4@h0=a .BH<IENDB`././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/docs/source/traitsui_user_manual/images/BooleanEditor_demo.png0000644000175100001730000004351200000000000030366 0ustar00runnerdocker00000000000000PNG  IHDRXW pHYs   IDATxy\TUߝa]@TvpW,sIɭ47W,ܢ\}׬$MͥQPYguA>Ow{?p=^.##Jf $ A IC!@ ifCaaaZZZfffNNZҶ Ҳj`*&yٷnR( JlZݴiS{{'I ׯۻr333㪴A4Vw^vvv֭.]u\zzBdU> AyZQ\\Uu0AT*RYK_BBߴiS#We2ѣ2,+++ \ND"$" 6LNNF@]RK0))rʕ+WRSS===۵k0f q_~e֬YDt]vǏ3g9̬}777NQmqyccc ]GSu?Dtԩc>x@,IJJJJJ:x]6me2Lx:KKK\^*qrguAx1chZ" ܹk||cbcc_|ſɉz=wܤqƙⵔ\.7 2.͛je2ŋG)Ϙ1O?ݼysVV֞={&L@Dfff*/FjoǡuLQɹy&uA7HT~DTӁ&:::55U(::ݻI~~իWoݺUZΝ;/^LLLh4Uvavv6[hժUW-,,vرiӦ?9;;;99]V\mNNN}5jT-zשSgQRRѣ==={FNNNNNN'N iܸq```NZh_TF i׮/вeYf677ܹs7vu)JJuȑɓ'߾}{ݧ<ϳfnCMVkڂwyb[s &ܻwO,OMM]hVUղX\tiaar^^srr.]ZL:u˖-%>\rezz?(t%??cǎGx`pEhffLD< ?kמmWQQQO3fo?kDRBBBݻ7dȐÇ9rwe#!WZUr#Gܾ}9g͛7o^n]d=zزe˗nݻqͰ|" ׍g0"$ \t)555??ҥK. ƍ_^zuۂ 8pʕlIΝwYTTtu???{g~~~/rAA̙3~r'}onnnNDnnn=z8pŋ.]:ydVޫW/N:V]-B"<}o)NONN޲e;geeeUrWǏ9>|8[>|@~6NGWf;+ݒ֭[O>oܸQ^t]/`iXYY}DhbbbXsttzJP B"rpp_r)Df͚~zB֭u˽K/];v,ر#[./]O<ѕ+Wmll:ti &QFE666C:t(]|y׮]֭ST)))'O޻woś6T>r|||9ͪ'22-OB.Ç'//ԩS٦1حZ[.y U0??8GGDz5jŊD'vQ֜W-iӦܭ:{pjynݺUVQQQC`eeeSqPv)**""FW_g /**{511 0FƲXXXlٲܭ/Bܹk׮]v=~XnݺP +;t.,,,(( *֭ٳ';1rHhժUcbbV^=|pms}&OLDQQQÆ ;|pzzzDDĒ%K&MV{V 6m+q D!mݺuѱcƌ)¨QZi5'$$7nܸqnȑ#Twϟ?^%;;ŋ+ T>MJJJOO7 BRķ& ]-B"jѢűcFagg[q\ӦM.]xbR { $-mX7}B|ZvZE``;|}}kk &^#VnG-_|ĉC[ p̙ _~Ǎ1&ZYY<1$󹹹9; `4\FFDIIIљZ6K.ew̼wÕh޽TAUq|-̞ބ,5M\\L&/qPԺQ]c3:˫m4P CbbbJJL&{<|JJJ^^SߥV!TRtqqa`kk[-Tܬ<nP!>//B{7VBj0666.WS*NNN8PV,Pj cB!@ iB4!H$ A IC!@ iB4!H$ A IC!@ iB4!HYeVz6mjW0bY\FFSWrtt@Y; XF@*""IC!@ iB4!H$ A IC!@ iB4!H$ A IC!@ iB4!H$ A IC` iii999jJ* ;;;GGG'''KK!pԬ[n) {{{[[[BQjunnnvvZnڴ} $u\aa]]]rqUڃ F޻w/;;uh@]:.==BPd2* Z(..NOO:郰Ե<<<,,,r9=CX6l099Aui0//o˖-{MLLlРA@@@@@@޽}||LR~)ʰ8377_~ll`J&8GxQFeee}IT.ZhFFffʕ+h͚53;ٳg`uhڢǏw u " {U*999 4#99ŋϟWT|˗曚IjjOD;vA(e2dTZŨA?믿?|O?700ИuC8*@m`.--{12}ݾ};[^n]5KV>NxOII/**&,uݻwʕ+e'"++ח;SN/"=z4%%"##Ξ=rTT{ԩSNz嗽w1xpq۷;99oh[??6mիsM43f̭[t+33m{ĉ۷ǧgϞӧO/,,a˖-m۶ nܸq޽o޼w,ΝjUD5DDݺuk޼V[rerr2կ_Z-F&իg͚9cCϳgn߾uS©SnٲEwZ}/=zYܐmYF܃FYv-~>oqƶmBBBy̙#G^vÇϙ36l{ wwwvoϞ=ׯy&kA&'':tqqqǎ裏6leXbccM+&f0͙3hƍūVZj ͚5[n8͛RPP~׋Æ k8~8}t=JZfZ,--,X;?288?HOOOhՃ ">,55u֭.]қ)c[[[}"Dl!)))==]~A( JP( 7 05\xO%GGǷz3Љ( `ƍڵ33+m[[Yf}>قRuZK@w.AloѬSbR/_E1/amZˀ?w:%50 aDRL&ŧ &U@mq4J 81 A( AAA3:ydA(vvv@[!H$ AǕ{ iB4!H$ ֬YR:wܡCSʤӦM+*ԕ ңG ͙3 /9;>s!I%mVXXX5}}}k";|U˗+XA.W)դAAASJuy"ruum޼5Q9rM6&BƦsgTp˖-O7nLD5kidRm۶ 3u-LO*Alrssywww7uuLCwzxx8;;Wu󴴴$777 j޽ڰaC33#EDD 4I&={l޼y>}Ν;'ѪUƍwÇ_7nܸ)S( ȑ#Dϋ;wN*+;;>stttssԩː!C233mAA XvvͭQFs){V̚5˫sM6mРԩSU*Ucvڐ!C?QQQi &Ow9s戫O2%//OTN>jniZV+ʧ߿_~5~`UaBCCݻww-11mݻץKUVݹsJo1{AjݕǏ?w%999/%'SCKKK{7>|T*SN>}R*o{ؚǏg 77Ddɒ[nѢE5jDDG  $͛߿ODk'~=zۻw2dݸqcҥlf͚(MRPPp =z駟(88III|h֭gϮc #,k…w133۰a/ ǎaÆ3fq˗/ |G}t[n-[km܈֖,--4ivzԩ3332]v%q͚5Dk>}:55F>|:u7߰={ܹs)))055u̙D /9r‚<<< |g EFFQ``̀,Ys=44t„ qqq/>}Zvuuੜ_޾}{SիlGRRe_U(#FXx+WxQ`3U_ =څ A  覝_uFsʕN:=AT1*ڵkDdggw WիGDzчzC-^8[l]Ʀ\΂RD|A|՜&M1bРAݺuc ., +c /^D@B>իl˾}W:߿~Æ ŒE? 5jTϞ=RjСñcʖAعsE": IDAT.]hyݽ{w߾}k׮]v\.޽3^yq51]U|C>y?i5֡'077glKT*+9u%Kesss{{۷oUs;;{^po?]]q7sׯ{5U*mzTP_vܹsv-v:|p0++?$mN8qɒ%eddL:UoJⲳ^Ynrspp` ٳNx7ntԩSN٧ { $"v ݕ7ooU8Aoԩ5>޽{СCqqq6mZZZ\._t\.ҥˈ#СC{ݭ;iO>ܹsؠiӦ+J{بn͚5K.}z-ZdeeO2eʕ $$$92!!ψ(22O>{MMMϟj:88Ac۶mVVVjz޼y={Æ8o[SY:n8V8{lOۗLطo_#""BRRRzziի7i$"z٦M)SaÆU-q377/(([^=KK˦MDtAt/j?BjrbIǎ]tKry-VXyf6y.77wڴiDΚ5üyby֭['Sׯ_?oooF vx=p]K,YdXҦMGߟd2xew8l0vU9+++"*..dZ7oޱctGr~O>D%"!CļK?^qC핝.ִiSvOWQQL&+U~~~qqKyyy666կ [SAAAU+--ݻnnn5b*͛[[[WFINN~K),tfj7n(Bnݼ+^܀yfJЮ];_aMjC3u] ApdBRкuk̔ˢiӦѼy [nONJNNϷrww֭رc[jUw>}zqq)Sj~mR+07V~ɧEE>AFUÇ\r|-cǎﳳzCU1cʼn%ߟ6mСClwީ⫺AVZmAA1cv%6R5́z]/u߾}ZmQQѣG^HLLj}e%3g n߾}XXX42%~)ii ~񕤤ƝkuwޣlfffΟ:⭷~8%X<|pٲO(2X;w-4i҄-,Yd˖-DԣG-[\|y֭?<޽o]p'_ޱcGdd @D?۷Ǎje2{キgϞ~>kРڵk*66-VPVZ)JEQQQaaa!!!b۷ooݺ5ɚ5k>}с^b%2ldDjuc?8AD~e]Wc.oݲ1$dݼ|Ū ]acLU>A`݈Îy oܴwzZΝ? ;v͟Z@P/9BD^^^D_?}v6{ͭGxҥK'Oʳ^|E‚<<pX3ӳ^zBhѢELLLFF #zW^ =<<;vRΜ9ZXX 8p'Oѽ).kՕ\R.)#ݕt2a„e˖jZ֭ײe˧WVbbb4MNNK&MҍF77.]:u*!!6]nѣo+x"6D9svvvb4A\-www~-/ bذ!˖ (F\zFFۻKwɗ_M;k+)O>mF!3$@%T9333駲7 }K.l)g W\aA(*,,LHHxaNNNNN۷y[TTcǎz6"ڵ^r ʌAױ-({777"˫`;wJLL6tȀu҉늼X$+uÊJ\tA5Ahee{[ZYY>׫W7]ey*++K՞9s̙;uܵk[H$=$.U>6`ӧٳx…CǤ:tyDWԩx .ZڵkeSnK/.v ɓoxyyu7P77ݻ]jDnn~_./o+9}6oZ/µk:zO_]l)cfff=_uODd5U*UDDD{8{,[xv{Oa"KKKvQr2#3gNTTTqƌ37;uTvv62dioQI۽{7/sUq>>?$666Dt\7$.\0Lȓ'Ovpp J qt sή]vD$[&ڴiCD˗/[yŊeּyAQQQѨQ^Ç#Gd^^y3 `aaR,X~۷8q⋐?6 9 y3>A=_i}6pt$"GGGV\#ٶmG:ɓHR͝;¨aÆ>|8===""bɒ%&MS=z  Xz͛޽{ڵUVt˴iؙsΞ=̙3W\GK.X&%%kƍ**))i֭zbgիpzիקO"Zno߾EzL4iĉC ٶm+l{/eŊotݫW-__OYfMr"""?5k~vbqo'&Ѡ5ݦ~]|%JKKߟxFZ Q Rg;;v͚5wٿ?OJJڻw-/^̮fkk;nܸUVeee!!!=vf͚^zZvŊcn߾GG#GoNLLi|Æ ۻw\vc… Ç:T/_5id\.gOE8n}oBAAGS>h'ՠaÆ+ϞEbRҮ]{Ξ=jgg%jHZDQͧOND [8[|ĉu0̙3AAAbܹsΝ;bgǎ2LwK/zjBnD_ϟ?հ=7oOjozKw8hPȩtʒe2YgU9qNG2B^xpE 4ݻw)~JJKÆ +ާ )))߯_{%'T_v-99>>>jܶmgӦMOL&.?/6OO=WWW1|bbb^^~fM+:hܽ S&mrrrNw͵WM] Vhl |] *meddPCUZIJRISOFIB vߩlI"0Uk#9X VJ8Axcc}ౚ3sD2sdnF n텆D;77w ,4oM cGq%)/$pDx.J 0zܪy:f8TAP]goq Pr :ħxcÎ,Z8{PZ TG£P+H Ǒ ǚ RNѩ!2HբjmlY PT rsK/ #XAfrJ6j 5NGOt?4xN=Jt~bO|d~nPP2jU΃r5RrhrG)˂H(XU~d ayyWfZƣJO#$2J(5VԬ![ϓ^M~ܔ#O謈 Ghq\՟˹ CƑQBPAsI52PRnQi[ו|]K-ё\ Z\GVҢE`4Ǯ50'D27VSOշ*Y/.g*zDY)il]h,t>ONB[O?TL˓'ix +)]ħF{|ODԤ! 57( %%bD$ D$hkQGPj*]YЖ@".^ -$6q\%YnFzL!;Rݞ5q,RrtL͡vR~|kT:^ܴWJV;vQxS,)PsSv *"Q])ɺFu?jrG"K˓X7s7>>̾-ϡE`< FK>AsZCVCO<σh{8}7Sȍ}D-k)}O8-_r L]׽9.EKo֎"@s{Bi&ضLH 9ߝ]3Kk:eJW WnEP]Z^(NVJzEUZhǸ4`i+wlKS$ T4g/&"@/xN瑴 ~g+Z8UyZ=P7{Y [s S!E.t,o'ѣvo6!U{x-l/+!@dM St磉7hG*GG-uk\ =}(!vi"dzsUC x4#tr%D%krYk 6ɉuMnCiM`qkIo7sz%>6Ɉ+m&⣩ox#IImgQ+űw ]QKwAb+%!v檖]=sUr277S?0xW*JRjV+` Qp' RT* )H0Lfnnnfffnn.KA,D.Aql`B H$ A IC!@ iB4!H$ A I?%?IENDB`././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/docs/source/traitsui_user_manual/images/ButtonEditor_demo.png0000644000175100001730000006127500000000000030270 0ustar00runnerdocker00000000000000PNG  IHDRvJ& pHYs   IDATxy\0IܗF.QxH/IQ֊'ި*ޠEZA(m /,@@Byu$$ $dvvvBB Ap (;Pw ήh;X,$Tl #h4Aw\{{|l|@ 8kjjhhht@ w1NCCdt̉͵UUUzzzzzzE.гg>}tv]J+//WiArJz . ylJ/aLWР.O> *-{!y_]]UBĉ_^tT]>~Nϟ?_NAW֭m.]$ SWGGGeK ^RR믿fgggggy…fƍBo޼SEM>֭[[nE͛7OM ={,&&&;;L,[[[?$%%Y,źxb\\ܡCgYsiiիWB cڴir6NNN|2^ Na|h.\Gibb7nN0L&!m۶D?Fh4C2 㪪3gFFFgϞ=.\W^D"Q\\ܷ~+g .tX}@j.t}͛G_ᅬrʲeBjjjK.m 3gȮy!C\unݺU^^}f˖-*\qqq˗/g0-eX\%YBŋaÆ#;BH]]}ΝݻwG)ս. sss˥y<^nnwɤbzJ (^4BH$fff B]BMM?7oL!}}#G3!QPP_xQXX1)))U.ʲKKKatV֞/X[[ˮԌ_~Gd2O:Eln:&9eʔ/jر#Fx!Bd};v,]k.rAd2۷oM>_~#Fڼy3{ vsrrtvv4hƍe{| ۷ox<ٳd7hDG~{{{E9;;L69(((11ucX...#G,)):::r*YWW śDrss7nĉG{neDў2|>?))iʕR :R,0"q#_z5k֬K.-[DzyyyDDH$"~3D"YYYhjj"6?|pmm俗sΑS\ndd$>vXVV6jԨÇ'''|n޼fϞ6C vP(?P(DRŋ_}%Rx<^VVVVVW^mVOOɓ09̙sɴׯ__jm|||SSF <}6m"buuuUUU{`SӞ-w55#ww۷-{-\?_MӧOٳ-Z:zlɎ;͛w;wlݺDEE)}8;wٳgϟwuuE]|yĖ)))xtO*^|hVVVr6qpp077W*s6ri4ܹsBBBLLLBN믿Z8GEE "]S#pA-_neeo߾˗5JjBrdW&\NNǿ"8dիWy---\Ç-?}/ܾ}9;;;l0o| Fss3n/1tPٙ-<===== /]T]]mool rvvȸpŒ3p"?- < jll ,K!PixgϞEEE+W3߆ L|@Wgdd"XܾிA)˭[iʝ3gNFFFNNN^^y|P(TSSk1q>/Zl[xxȐ!C 3gw}}}jjjMMa-hC6&NRC࿐(d㕖*2"EqURȀ$W\yGȈ\pO#4iRϞ=[܅8\+[ٚ՞jq;;/ȑ#"{DuX,l"1TC IIIQSS~:y3@IZn-K$+WD"777s4hxoɓ'CCC>|w^|H)ݺu9s'׮]qwdݽ{`9Oy}vkkk|I1YQt!1O< ݪ v>Oĉݻ[{P(11qÆ 6mjn~/##CsIqssѣq̙3g/Hy<^||<.brS.`r*iNپ}H,Pqq1R1|RjLAA~USSP7fÆ R`{%o!UYYε?QWWb EEECijji߁,,, _^lYxxx||^-谰0bhLLL<<<BNO8@ڵ{{{{{{߿_j՞}ذa8jxxxǿ~?<**jرxyT1EGnݺw9sxm۶&;555]r%B(''' 11fgdd߿o)**ĵyAAڵk'O 믿2333g⩍f͚썢e˖!N8m۶ח/_[?\[[۷#CBBh+\QQ!|}}lllloXEݸqٳgǎ[~WBMkϟ?~AA˗/.\(_~a3M>=!!ٳgϞ%ϛ7﫯?XRRrմ4*}Ctݔl={vbNKтSJ122:yd```mm&2eJkN2%777::lŊgΜQ檫۷oBSNŏɱjժw%%%=y7o 3w9Ս7Ν+5 F0`GNcWg-^=jKql:::˖-#%jB!!!䖚/ckk.u988\|900Pv"9qիI|ɞRa܈LMMn֭[n%"lnѣG#LWNcyz4M߹s… D}}}//D\ еЪTuIIInn.ñom.vf9555-,,ZC w0L9-󋊊U]-**zX,4hG.R__{:naa!Y;򷶶W^0QRSSHJQV߾}wp}?ZfaaDKdAwvvvvvnuuu1 333|ti/@A(x\WWjüK2`0:l )@E rH7TAmR x]YYENWG2]N҂GaJ---Evkill,..ڮQգ!w%zzzo޼͛7zzz?7T)`C(y󦪪dup*** zц7TXrkjj뛛E"?$!5 u544Pi;b@X,hFtuMՑALWD544444$ %5)w6#@e Ap (;Pw @A Ap (;Pw @A Ap (;P"䨺T訊l(@1*pT-(FEn ZhAp (;Pw @A Ap (;Pw @A Ap (;Pw @A Ap (H+N,D"D".H4M6`מ%577744x<>/!й;Ꚛ:::P]D"`2s>FsssmmmuuuUUl}ѪTZh_\.gϞ}V^^^UU҂ޕ477";]l ^vhA]]";]Z>}TZ .C,x<ήcxR[{}}s^Z\\pzeggUc<{,&&&;;L,[[[=eww,*p555Ϟ=;db]nݭ[˷o~y9lٲEՕ+..n ŵ,+;;:?SEEBRQLrEyyyMMMmD,eee|) ?`d߿ΖM]]MM?7oL!}}#G3!QPP_xQXX1)))U.ʲKKKatVDpvBH[[;::ZKKKv#FL0!\VVb2L&ÇRUTOOO 1cƘϚ5+==ŋL&q>zP(ܳgIFٿ z\޾}ŋnnnvvvcǎ۷u뚚$ɹs<~~yyyxB=z ]۷ٳd7hDG~{{{E9;;L69(((11ucX...#G,)):::r*YWW śDrss7nĉG{n@'Hn\|z=zm[~R$'  ?qFN;w<|ŋؑ_CCCϝ;GF gff&''sջyɓ'BSjjjlllÉL|'O~}IJQF544 ><99C$ܼy/̞=m r5P(|fJ/^|W\.HxYYYYYYϟ?_zukaZ=='Os9ydZZׯ/6>>F>}ZvM6Z說{~tQ^TTlmmlfddԆMMM"2>>ݻwv|>x3f{IOOOOO)v‘}<8wۀ:;fffvw ._}v##Ç?xCE)))x+Az%BFYYY\lvpp0˥hs΍ 'ةSw|9QQQViӦV9AJGmaa!رc5jTdd۷=:rH׉+Ѕ(r ~}þcٷo߯Z(z]]]mm766LLLȍY\\fmm奦/9so!ݽ{˗/|퍍-[b0W^Ǚ;;;#}'M[__?b]A9'/_'O}"""&ڵ w&MDe999ȎYXX'77?P|RD"00`*A-_neeo߾˗5Jj¶Ev,((!rwr:7667n^ tDs3t E[Ms555y<SRR͛neeԟQ{ذaRe;o!hhhHmikk'5Qlq`0444K;jnn.,,D :TJ,,,tRuu^tt4aA9;;gdd\paƌ8GIWM.`D({X,ĉUQÇf IDAT_f[!ddd4w˗@ >}ʮqFqqqMM y֮7U'H,#xA֭[xAGGԴmΙ3'###'''//<\|Y(vӘ-j-ϟ>(z64X,Ueddl޼8ΡCg ShZ3m.d"x<^iiiL<ٯ2vFyyʕ+6pWX,EM4gϞ-t:ݱu{nCe@'R>j(--t>f999!r[[.BBBBBBjkk=z}͆~ܸq/%%VvZ~~>B¢{pCDrHdbb`΃ RWWV۟3g$$$(^"!00Nxx\F TD'UpWɩS~7Bp۶mx8_|GXZZjkk#_N޸_*mڴiҤItb Rt ?H;44Tjˑ {^^^TT1Zt!ָRWWbŊf ۇTvS|4$~zٲe{>|*!dbb>uԽ{пhZCܹݵkqKE!BBB󛚚^~}>ȑ#!MMMbRi:ϛ[nY&77… ӧO/..>++ō[nEkfffʕ+/^3LLLLQQэ7VZu\Ycڵ'OV Ã󃂂믪̟y̙M0k֬˱l2333Љ'mCqMuVwG=zTjKK˨(ӧOoll&~~~Oȸw:ZZZ;v f?~͛7l5kBǏ9s&B~(//?ӧOF=) %%%l6tgϞ]x1aXSh+V(ɓ'kkkϟ?/5)SZwʔ)eee+V8sLks=OǍ:uYjջw]C-YhL-۷/44<6@MMmԩW\!_hٯ N!wQ/_ `0lllN8zjϙGGG秚֭[nJ|)dsCt9{遁xY7 oO_s΅ ^^^fUPRaa7olvϞ='^|gnnuuuzzz-΃A& b_TT-**zX,4hP{=s___{:naa!Y; ?TjjUk#?^ۃ;`UG*({A `0T:{.Oa؆!O?VeAp2t< TVVjiiDR w%:::@pJJKK߼yuś7oJKKT Rvй F=B7oLOkllp8=zPi;Xrkjj뛛E"_йn*pGܻ"X,h4:nݺ:#tD"!й&eC`0TCUuYr` Pw @A Ap (;Pw @A Ap (;Pw @A Ap RSdU׃JU-(FEUUU}p####Հz8*Ũ(V`-P@-w @A Ap (;Pw @A Ap (;Pw @A Ap (;Pw @A Ap J?KKK4/PSCrss?~LϟO::ӧO/Ę>lcxrرc*Ѫ:@9< | :t֭[B'BÇljͫW^~*oQqq39̙3VLLL&LիWLMM,XuPQq'22!4gKKvHӻ^Ǭyp:>,]Ӂ㓒;&"]"##f{ę_TT5rppطo_goߊb333CCNCeeeii1"fv@|CCCQQ߾}+ ۥS"TTT[ i4u1Zd2B999L&dȘ9sfǎ;p@ooGTUUY[[oذa\.ׯ_իW+X5k ?''gٲe*>>>"CZZZEEH$"y)l6;!!>{ӧO'>j>,u|e))"999==!4ptEUTTq\uu-[޻wo˖- AAA8",YgϞMKKÉB(""_/E Ţh| *--wqqAx=cƌt"ٳCKeStYZϝ;rss;wܳgΟ?˗wޭÇFDD3{=}T" 6l@+V@ ¼BٳgDׯqr4СC[AA7{l#F඿FiaaQ\\O2)#Ȟ>}jjj>dvvСC|RRxBۗVVVr\ɽOHH>}:No˫ y*\.gX,uuu%gggx߻SAp>o-@UVVVVVՋH _~ݱ~$`0Fjժ+ F سggwJL:詐]y++77WIBFnٓ@O}2~~~䑎Drڵ#G*`kk+d2w^]])Lj Sq,l2"&hjjƲc@__ٳO>SSSE"ѣG=z4|C|݋{fJJJ>ﻣR$d}Q=bOOObpHϞ={e```hhn~SS_c6li&+HvG>gŋW^M<$Q2'$$>KKaÆ9Oqd7119~#p=Bh޽-މaXl6wpph:F$O>Ӛ_~!6h ի[b F MKKÝ?ݬ׸ZSS;%fϞ-,!Do(++Zن mڴ'OZ`q#ƍKc!!tR"#Rdo*))6Aɵ=zGNrfrqq3gX]]' <8$$d4*44Td|rҤI&M:{,9Gr wbn|>ǎɠBhÆ RL.]rqqqqquVk[ZZ}L lB/@ Ad^QQA?2O99*>Y]]]HH9ˎ-HN)fff!Hf͚G&["|?Snnw_Ν"8kVTT00QF͝;!t+W(Rdeemذ!**^"/]!Gw^r%::ŋ>#B :J'cjjrJPNNN@@@bb"ؿ7|STT$Z۝NY&77… ӧOoq!jkkW\ê>^aoܸQNYy)eذaQQQF/Ƀ KB1 ++#G뺺k"~bݻ߃rdM>=%%ϧ\~+W:thɒ%Dlcǎ uF&5N' !υihhي5gϞurr"f(۸q?H.qcX5p͍xsᛊ <<<mmSJUɟ3ޫ_~ G>w'NH}{^׃Otooo|ϳE ͺ  w0 &^EeS,X\RRR__?`9#BBA,w]jU]]eݻ&نuMMM/_377o2xXakvϳؘHbիW_~)Bc㓖"po/F۷Zw4443!hrV)NNׯ"[X}B˖-S\riii6ctMw;7^z)1|qֆ#D#i,]ս{;fsrrn߾o;AII@W^ U/c[u&Q#+AJ&mmm######{{ug 3@A0(;Pw @A Ap (;Pw @A Ap (;Pw @A )QNNA%XѪ>j@=GQbT+0 Rk;Pw @A Ap (;Pw @A Ap (;Pw @A Ap (;Pw @A Ap R 知K_}rɓ'Ϟ=Ԝ;w}1N?>FSuqKW'N~zҥ )oLL wvvvrrRSf͚e``T&mCb@;}9˗s(eƍ^RuYںu+BKW$$$,X!4nܸ81R677^zJlٲK.SRSSʤmLLLL&L:2eW^355š%544B޽uVG rrr:p@gmj IDAT$GGG*m۶ήB݃Ԗ.]ٵJ S@ WSSDeeel6TNoD"(++355ݻx<^qq@ T㙘Sa EEEFFFܸB55}vJW]]۷oba[Phee*u)BrW^([z#Etn,Тu1)S 8ps!6 vsrrtvv4hƍ|TV۶mrpp4i݂ rWX1`1cƌ;o߾˗/[gVV~ƌ3a[[aÆ}0SN}` L0a'N3fLrrKAq:w"3f̸qFCCHr~-y3X_qUq___"$\s߿H$<>>^$꺹Vٳg߽{ȩ9BH(D"""H HU[srr-[FA_vohh>b1~_ʷH1.999==!4pt] >}Jμ>>xKuI$> !sNX@듘x R|S5$8;;wp8駟Z,SRNA?9#>}A6l@n-jkkX!$ p]bcc;iXnpSbWϞ= 8ct:݉j Ȏ 8Dʓ]*Ϝ9/y< N>Om19!؈>|HN_dɵk׮]+gw _BBBB_~#Glm߬,;50___(?Xe)9;;;q|;.J]<666DYoP+W Gr?N9FJ;tZ*k>;;{СUMMM_ro޼AbX׹x#FHhhhhccC!/6l03nz٬#GZXXDYtttR<2o}*XQ F سgg7N-g_Ç_f[!ddd4w˗+>g͚5DLrr@ ukm/o ի5QsYYٞ={?8sԩS׮]А:a0o RnKt1R 'C[6mMMM JJJZi=+N1݋{fJJJЇd0??ɓ'>}˸g:t۶m/ԩSccc_yfhm/Bp<BŞĀ={ߍ ֞:uŋ{ N6֎5CUڧ| vubM3119~#pO%Bh޽A5!g7IwM <\܂M, #G]v-00c!?HKK+$$$$$ѣG7olhhǍgnnH&YYY }2]ԄBaPPhhfD@6سgaūW&.bD" ŀ/>HOFM5D!OdE]]}РA1R)#&Ȑ%q qҥ...7 !G988'OH_:mDSʇhjjjMM ={^^^}BH dffJm1cxO6o\%##M6!mlld;Q{?97C&P*R ݢf8ufYgmm[G UXJٲu);THRD,ec,1 &!kz9w BYYي+:tz g'NAsu]gFy7nXWWwȑ^xaʼnPYYy]w8sҥK7nxm۶u];vH\+WkIVZcݻOK/t뭷0 SN\~=k/ɓ'8pw߽;9ؾ}Kh¦dj~}%^\d/..k׮Om۶t]v8q>&,_|w~ᇓ/_=+V{MMM;w6c0$eرcb&~ކh _ڻw#xm FUWWoٲyܗ,ZW3g M@ff5kGoyݺu"b:NTVV~O?~i~qqIޕ꫷rKGGOUVZzׯeeQ >L}M)nݺϼSԸqzEnQRm.**3f;_j[[ۥڜ^:/]]]GN]zꩧڵifs{ee%[[[^^^nI,`Se_vQϟ?}tQQQIII~}c] J7Q̲IO駟6s%=''q8Ѵ͙3 //o C勾>/B.a ^wQ|GϟommEKCEgzypСwy4Κ5+*=ztÆ ׍IIzqǜ6:OR୷r´`թ_;S<^lG.+q}UE)Ų[̊Dv]%goO6l.3ʣ3KvU2ka5BS*T(l$25 Fycx4+$sj88^d'}/KD-H{j~{T~Q^(@P)2Fp:i|WۥC6Vt"iw;2yK&= >\f p+h84BI B]Q FfϜiQIjfDrQh-S[fH( "J)dԵI#NDZsd(-aR䎫}T 92Թv9պt2܉#uٞlQV2(Ns:i9r QJy,p!!2h'?2܉(919H% "IWiqDTWԽWͩRniÝp?h%RQiCuSIa<#bM.mjgd}qNDi pL7JĦRJy?J2| _p8|dζ֏$9Ԥ+R8,[J(8/ኯ7k;%g$]%#91R8U[Lx%Ç|YJ [vEk­0h1܉(9 [7<,kVUxE FR`$UcyC1;cҕoYJ+o蚦>IU0܉(98]ey*tIIvyd U`Z\VnjvCٮUx&灭ɑz yƙ§K-J^NDə{'x$ͣnb(Cfq}<~>w඙ja~T,H(҃8'oÚ,. IhϾfQr"*R-WCtD>E`}\~˼Tw^UzP\ STZ;cݬRNS)h I2Mr w"JNhBZ \3dRRpNzP̱'钯|E]3ߩ iGodǒʭHt>"\"Y/(V TW'\XiDȆ׃Wˁ <@]S _^y[Pc@KD40Ц+QiR#? [bF*B^f @c5 Z; T]KaHDK!'jI/vIBmH}|eT[g#"2E=<#|p5_G?NDpoOzTH aYʖdwwlV)UY~qX;.\su*uKDəxڢBΟ-̉<iʦE\d @tsl_+?܄o KRIw-r,l^d8KT\SZsl"0s6ieZ0*hqW9|m)ejb U{׫g tg}*ZW8u22O]q8?k1ɗWbo3i@ptb2x,̝8l2] tƖaQr㝭ͨ&af-)l(F 3~Q3W4ѭ|b*KE1f|,@ ]YGr͇U 7a?%ע4WY %kش\]Um֘5t [?}9X[|Toٷ[Sװ]U^s'ݵ(3@Ao1l+6NT<?]>kq9N:tgt;5r lI *G?w܉(9ѦOհMbodLO4bsx~W҉p>{HnkwarʷK+g'k([CD  "!u8(pFqK~ ϼ%+Fv9L~_~d(ִߜ6{D4Lo ;h|эpp`RU'ok-CDfFq#FƤy$${:q d_,ᝣB@i  ՖXSbL q0KIDAT4RZk,+c/p'"+T)5O[2CJ*?ƼPo8)Dr[DBFUaRp'^-jqPQp#ڸ]Z8)•05GDlE`ۘZp/eQ䣺9Źe_kϏPDJY~{Lx*hZ4ӚsG-?NDDI9J)tyʌtSʽ'@IɿP;M+o4yީ0&32>k zzz; L_hL S%~7>=W555 Ƙض l۶m[kplJ)˲<z'>XfVxp܉(jFnUΘd1fėJ2dKD1N#MI@Y֐q_"[,P[.`ɝzr]X9JVffia;gZTAYOGe$'26WzÝ( Ν( 1܉Ý( 1܉Ý( 1܉Ý( 1܉2E+IENDB`././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/docs/source/traitsui_user_manual/images/CSVListEditor_demo.png0000644000175100001730000041235500000000000030303 0ustar00runnerdocker00000000000000PNG  IHDRi pHYs   IDATxy@? ðo^eQ3 ^RZ7/V꽗̮424LS^-4E&("$̰za7gs^I&ޯ9uxⲕZc ,GOGCtҞձQA"N<ccMN^Ǜ;4J?F5eeeb@,7'''#D \<Ӓ:X<$8!O```),,ܳgѣ,X`!bccM4'##rȑcǎ5XNr{jhȚ۵[|}}-ZR7Tz;;;77~ lܸ*$$_Ģ= {XխX… G7nXn]{{{gn'Ne˖EEE7\駟~g%%%mmm?ի8{} wf///޹s2ZvF!";;>vhn޼YWWҢVmlllkkǞ{}LVYY)r9 T DHpC?.ƍM_-~"…3fX}ݛq5k9r?xlǍ7{9D~|'J?ŋOII/{so.ccc3h vP(JJJُJp\~;991RY"jookhh\]]{׭>믿jG"Hnݺͅ츱QP.$C ;w>3[l􍴴[߿Wsss}S w%''q ۷o_t{7"X\SScW;koo&P¶^߹sx<_DDDHH[Ԥy(w޾ yfuk4__߉'hL"? 6z+W̛7= `B![[[ V.gddќ9szsBc=FD'Odݻ/xE#׈hܸq젏>v~zZ?|\\8p֭[bhG(Jxokk;r@#v^YYBDaaa:>z 2DsŅ XR)Be7Dh;Q` @`ڵQzG?qsp'DGG{{{ |A3gΰC'Oc+W~Ge2?qq-ZeѤI6//Og DryJ׻of} W^߬ceeT*Uff&O<Ϋ?HTs nhwxzzFGGd2;rww{e===u^> =۷!Ct^ruue7L?P(*J.k7ڵ/Knn\.ҝ>}ҾDRsI"xG3E9r+ʴiӦq=8.3F;NmmmTT̙3+gΜiDoq&??mrQF3gԭ>oaee5|̰VRRhCBBҡL&cN_us}vqttd)J]VR_UVN<䤦rZPP1{Ç_rСCÇ'ݻw9.zxxp1p222d2ܼy7N3f`_QKKˋ/HDÇ>bTQH7nԭ>ouv957o[ppQ v:kloo׿O=o):JjGpw@QٳgYōd{PPŋJezz:IRGGG"buJ*49svŦlLhhhccNY)S8::Jq=+aDNEEO'>AYGGG77۷osݍȌCtJ.'NmR44 Vףeb\;fp]38;wN=11155555v[fb^z[CDǏ'"4AQ}}vOaaazz ?D-[… ccc-7e:bXoơ:UVVT*|uMe`KcW(Dpw@zþۺuk||g}Vԉ\.gnnn_ekkȞ~RAGǍ9ǎۋ]]?P(<<<-=dWYq뤚}hn߾үAAA6[Uո\ݢ o^^^D$V\9a„Ç'%%555ىRd1 zjussXHwk47u}Xh4uZBT$%:4 θ;i F!Z*gW%6vS `FHp@f͚u5kDEEom6f̘}Ӄ=g l\n*ij_L4BKuu5;ڵk\NE͛;^4#2:*\ަ;TTL+ViC.pƪiqhn\<-Ydɒ%b)))?L&[bɓӳƼ OOO;UUUhȵkׂ;='"??:3f Wׯ_*W$::ڰ:$ ;(--􄖖/QXXAsXf\e $5M{{{ysl@ 0 0˴i>kB9 uuuq6)vP#F oב#G? hjjR:r%\]]>47[UDm իϝ;gooAo Y7Fa.JKKuj!Wj4QqAH$p̙3МN_tiӦLHHknnްaի?D1z^>p6tЮ&HR;;;VwRRϲ7 A B\^SSmBDl pAS__N_ c"JHHHNN6lX[[۩Ss„ DR^ <Ϗ.HJKKŒ{S݌KDlΤx"ھ}KmC fffZYYDn[[[;ӗÇ9GΉ(11qÆ III666K.=ws=6y뭷X**??Μ9DK/-]S\\bbbdkkU~NNNRT$?醟_UU\.x|>Ν;EEECzm|~```YY\./((6lJ%"{{#$898k֬={\|̙3BPP/쒓>5ͱcǎ;6tP|Oj*"1zq[[[O2E˗/7n444ta"O.//1E?;?rԩS[[[x7|ʊm@Dߟz)ve?CNsss[[3e2հd>qUZ]^^^QQAD\O___A_,766J˗/7%@aQXYYugh) O8b ,I222fϞرc?dz- (v_f\"JKK# &91cFxx8Ǜ?oD&S[>yll?DRsRR'|ƾX4iR/CI^RdA_6Wfv̏d2"zpqq1b[hXvOvn;V%<<!".2rH^0X DRYY䤽S2Q{;V^meeUXXh-))ahmOwD*NՇgYҥK111Dz]ʕ+큁:X3f8qC=?7fmnu X,,(jUUUppzfHBH$Q^Juȑ](J[ZZj[n{P(Ri{{}mzmܺ?{Ye %*lll?H4={ /C=\&UVVi=jnn;R%,YfS[=q0 `~5uYf988۷KII4hЂ ~Nl6#JJJb࣏>j)'|6L;}888|wwwCD<+_LԭlllLb(^d "銌EF zB x`b!X>^$7Ꜹ_~N<ً_-.\ K.=p@llc wGy$//oɒ%k֬]KKK[lX,&?Oƚ9R]]JR_~9eʔ^&fϞnccsȑ &otC]*88EFP,00KTv3lٲw7ҶnݺN_=x -[OQ=.Wh׮]׮]3d2/h"xo\*ŋCHHƍwOQ{{+++{ѧo$86oޜ`?cF;qDSߩ)S(O?DCL4/ `#v_OD?egg.C |fϞcǎ7D"ٴi}y@PDVCD4g>bBaB#'Oj4C/Ǿ}ˉhѢE?Iܺu&1cƼ}pΝ߶ڟ{x9dPz޾)[SS?2ѯpRm\J>߾}?c0?XvG}Z\\ݞvBnI&X[]]]RۛBCCϞ=N8sD"!ɓ'ʕ+SRRƎw噙 D$}CqYﯭ=z@pܸqVVV[?wwm۶M:/tjժ۷o[[[oݺ{}RΝ;GD3gyg9Rf)!xb-^^^ts~ZvKCFsͺZmcc6x`[[>dJX,˭$GPjxxx16 BR߹k׮/rӧO?YYYrT*~NU*D")***,,Ԟ #wں!ЃֵkתTഴ|\j*vɓ'srr($$$''''''55매]\\ ##cهrʡCNDws\" d3>X LFD9997oͻCDD<򈏏O/Tkk/" euRt|[ZR]]mPܾ{S۷/]db1(~Wݳ g(juaaa[[[/ܹSTTDD</"""$$kjj.,w޾ X=8{,̊-^XTT*utt'"6GnȐ!:ܺuBCC }Μ9vbFcbbryvv6ILhhhcc#3eGGGT:n8???3 6ܸqmٲE(On]}e2\]]2x<jABMׅ5772vvv,A\ZZR*++ ~yy9sss[[[kkkx fp@Ϭ'11155555Q~2wwwF饗E1D?~x"bU-7h "n),,LOO?~AZQQ'|BDgkl>qO;:X)v5pQ*vȑFKD/,,LGp Ϗe7v/JYQM "Fc܉*/c IDATЃV o֭}YII{rjP:[[ێ'ؔ TjP,3ǎۋ]]Lj }FK7`}/|8gtt!XTl4 28;;{zzfqLu~1a >ozoED"hʕ&L>|xRRz*J6à'X77_j0?CT~vaVk6FP.Je]]qP w++ÇGDDe6 1J2@vvv;0h⒞iMd?$8gfͺ|5kdm۶3f߾}`ooϊs+36q5Tf"2dȃ>xC W˹/;2ձ16Ÿ84e|.FqM6(88X;pD;Mr>A`siޕ8Pdbggdɒ%Kϧ2lŊ'OggMMyg4555lMyyyttt,_~/ bP#Fj7n`#G4,=4@r9+f 7?ygVVV@TbՌ938.`iӦ>|^#"BsN5XrD0njqMůqqq ܦ &MCLT|Cy^ Y6%q fp@V^}9{{joծӮn.--mnn֩CDW^h4E7QKK ŠM$]pa̙drСC322:})//HJJ0aBj̛7P,o۶_~CѰa^>p۷OAAA6[U1볫qYDCGvR"##wAD Æ kkk;uTRRyzzj@D*__pND.HJKK>F͸DӾ8>>o~%/ggg[YYeggwzԨQM| 9GΉ裏nݺi&D/K2/_O< OLL4v44@h4׮]#";;;8KD\Nlh4\}q]M14lʮW:V5k֞={._|̙XPP(6vvvyG}tرcǎ:tŋKӧO_j=͸DĎL}˗7:0ѣG~a:KII;wn[[ۻロleeŞ(!!aX$zQBMuuuKK pUTD$ ;- \.gL`}!: xL14aj"=ǿ|>;GLPxĉ+V1;;;O4)##CtرُX1NNt C;Ȏ/u3.^a„ :̘1#<<͟?߈ٍ:=P&v>~G *=999\riEEED$ 'NhPsh~#HAii?b\xŋlA5},Ek^g,NNNDh:ͭKr̎hbH$J''wL&koowttю;V^meeUXXh-))ahmOwD*C[^^ǹ,y;h4׮]koo9rN?{1po fOL~~~ssGdddWWݹsʕ+D4|^T_ؾE= 5˗/KR@&KKKѽE" 2$((H\?k4ww(bq9@#HD N9888884.^?/^yɸ''|KLݠgC899;vtFt6CCvUzD*مQ&;x{{KRRYUUnmm'"{{{S| 4H(DsEE[s*w%,QcccLD~ߚQyy={7tdAll)J󖖖*"Ač\./((JDR_^[[KDd(# WSN5k?o߾^{å 4GM=V7$ % LyIIF4uPmmmNNNFߚG&=#""^V+**˹zyy[}FTze>+D$"""2 >JJJbٚWdd.s>?{ѣGcVVU*K 4v`|WWW}qqq1bĵkפR)ڰ ^bhF^^^~MZe7\\\BBB:.! `roeu>bĈݻwvvid;[{L Yhh7\\\RiKKZ^nࠠ TneeeooŐKTDdkk%,Y`eelxT^ qtt즞h푵ufA,0p577_pO8Qb,Yf5V◾ nƽ 7xz&, S$ XBи{[=xx .?7VUU]XTTԴ`Xw^7o:u*))̘\prr+**Z[[_x "hܸq$8>/E"… ;=wű3f nXuz;Bޛ?ƍΝ@A"ܹ3%%eԨQvĉGnnhٲe_uppÇ"vN)֬YsN yfuk4__߉'v|522믿6F7w\Htwy@_н@$8B D t\. 9sq!LWju}}}]]m7'TUU)PPhXO/WZN;ydNNN\\䤦rQppq#\~hĈ6m>pO?MDUUUO>$yu֢EJƍN>[oQrr۷=!+**322d2ܼyӸ7 KTgϞmmm%dV4((hJ2==R?> nȐ!:ܺuBCC~noo͖.6m"#GѰaæM&'K/э7FID 􊨨(+Wj43x;n3eGGGT:n8??"][qܹ~kOLLLLLԿ~% :uڵ'xc~~~WW屃M6t~;'::;7o޿oJUPPpsscKcǧM]"&&ֶm֭ qqq\1}rXLZIu?;vKBNphuX,wa(J4h;ЙADK]oVjhhD+W$"wwЦT*yOXյÇ*++ 6l8ydcc>w\֠X{̚5k֬DԴm۶1c۷OّP(t錃C7=k4n^f*++'M~?~i eS` ݒ%K,Y"ϟ??d+VL<900<==kjjIpѣ322bqDïJDfKc6lDD?-[M?QT  7vǻޮ+u|~#eڴiӦM[n͛ EnnNLKp7QFK.i@%"\^PP}ιs}vm7ٍ^˫7xyyk7 %*wիWO2JΩe8X֠T၈|||ի/*1X|97YjAp)JR.s);wvy7CZM٭iDy0p绹c~0o<GvRHHH;vQBBBrraN:DD&LN "J/\p~~~쥸tDRZZf~6o\UU5o޼gyf}Y>,;yɅbxҥ . ۹sgVVVws;Dte"2d;S\\OD۷otZg7577dKUYYK :fڳg˗Ϝ9+  agg裏n߾];vرcCx"{iV"#&87ި;x`^^Ύvrrb?Ο? i&L(//鹛Q(lӧ\Dtƍ;Kʪ|>;GLPxĉ+V\.h4Γ&MȘ={vcǎǏ~61!#F7|i`t6o޼|rE ̜9󫯾/|666>½I,gffN8TUUs3(??#22D4vD")++ STTT "rpp4hN`ŃVT_|)³ 9rdXXX7 "6lذanP۹uVzz:%%%u5 ]666DZ#k׮mkk4iҌ3 ЉSΚ5a߾}接n޼O?[ܱag}fի fpC,`P8bCw!`:-,3w@&Nhu xHpC,T*رRT*Dnָ`A .'^v޼! DBxE"… ;=wű3fnZxoƵܸqƍ|͑#Gd@Пjի?c=:x`. {oܸqܹ^^^f `6;wLII5jTo8q4{A"Zlv-ZR(..+++ ~gz{{9˗۷ OUVV~ӧO|27 gԩ"h͚5;w4k`ZHpDEEm޼Dwĉk׮UT|> \UVX۷oWX00IÇ_>___s̝;W$=zw^MUUC  P D t\. 9sh/׮]#cjg7H([o>rH/ _^444444=yfccû)hkkkMCR644SLT:ں|Dl2.ADBp=effj'8x≤$Z`s0hڵ}kqqv{VVڵk r9k4iҊ+,lIAA[NzYv™3g$ M|#t?ʕ+SRRƎsΕ+W;wN&sMMM,rС;vD$}իWqDD"),,7nU_cqrr2w`a!fP(T*`voh4Dts:t(..ksIvѣ; rĉK.Qrƌ1®b޼ysΜ9޾Jb㏻wR*}Yss׮]˝//]4k֬+V/^O?i7߰uDtҷzK*&%%mڴ޾l2T:hР+Wv8rΝ;w644|'\#{OZZZ-Z&p7ܹl;fF~;UYYY---\{mme2YLL 7OXWkk+VSS~z>/WZN;ydNNKv䤦rQpp*"=oqqq4ѕ"qƭ_OzOvڕ[oѣG{=ww۷gggoٲ==zvƍ,#>|ʕ+)))?8i5~ׯ_'M6u>`ٍx!Κ5+77w˖-K.e9sڵ㱐cbbryvv/ijj5kVo]Wu{_~5??_,ф vm%)S8::JqƱX.;wN=11155555U'VݽKAAAgΜ_=WUU8p`…o۷ 7K)ShNqvFb fGV>Y~Ç?zkks/VQii͛-[P(:NЖ Y {{W^yJH$ҹ꥗^b DT^^5SsΟ?/++e7f̘L?~H` @bbblmmڶnݚ#\Ξx;}e}ѺuSN9sUdw>~'"=rssdtJظJRC[xxH$YpZ[[ܹ#b1[Vj>2;y~6x۶mYȣܬS샭 Wj߅mxxN?l:T*Z<<<{O<ѣJeuu˗VZr!*\UDرfX"$8`۾}UD"+{>_|)JVRm͛7o9r?嵵K.QOZرcGaaRgM6Qg4iRs+bzꩮ),,ͭ]f=sq{{ nZZZꫯʚ%BYfM:uϞ=Ge+,m۶gϞw}w=`ooomm-_l2rȑ#G>ӧOJYYYͮ}z;wcPr V=#[[[.M[G9>>>nT666oH$ʪԞm\hHpcggdɒ%Kϧ2lŊ'OfvӳcC&xN'DDD<3;v Ҙ܎TVV솟߮]ƍUظqcW5zjv|ȑ3gN>QF!۾OW&ϟ9~T*lM"D4yd7k״;4B_۵[|}}b1W35.iӦM6mݺu7oV(: BBy\SI ",ZE* "bARm\vQR^*֥Z6 wԭ.`jAhAE@QY,l!!97 0g3v>~ҤIaijj2\͝\WWnj p@*,,>uuuPxcccVTTܺu+ @ HzB55;v9::Ι3_~tN**˖-۹sӧOg̘,{=ggf_KKkժU gOOuyxxVTT}GHOO=z4!dϞ={8tuuB68 egg٥KBww#G$$$ܹsٙ$ZZZAAA.觟~gXѷoM|ڵk !>do1!ĉOOOZpt͛G\HCitZVV|r\MM#""3*s! bcc%JbRj8 -5j(eW98@!*PypCQ踄Bamm-ܩS'P( !uuuJt8p@U^^O?5煅ʭtLp@G?9Ñ($po  -,,,,,ڿ2GPypCTrp@GQYY*B:,eWTDuh<8@!*PypCT+ bqp8QFMhGVv&66VU@) J]BpCT<8@˿krr#G:w6M\\\ZZs]# )?.Xx.;|ֶ}BBrm62422zzzK=d{{t ׯҥK'---<>>11Q]]˫k׮-إKbbbtuuϟodd죫իEI׷G:::-tLź|rJJC0{…SS ߟ;;;O8)kⴖy Pz'OV(qvqkLh}gUUSr֭[nEDD={̬Ujhhظqchh(SRVV믿;ww޴Ι3'$$dڴiݻwggaaa? ٴiSSTw1[[ ߸x+W $3x BȊ+<-Q M&j5k@ Қ5kË\kkk7+W !}1cevvK_|'$$$tԉ<~xkk디M6۷}.|y8p_W~Zx1)ఱٶm[#44T,sԨQ[/\FnOM&j ڧNb^/r…W\Ͽsq7oޜ9s2bĈ'Ojׯ_/,,CMrܹ~=榧yD(Bzѣ &B"""yq!gϞuqqk֬9v옽/lٲ۷o wΝNNN999@ѣǿ/ &s}YݻUUUfff?;­[;vt_y<._I422Zjiؖؼy/ݻwoϟoee󃂂.\HeffeddBv5{lzlBBBPPPll,sr 69RrB/L[ {)}g ~?CfϞ}͑#GFFFJl;wp%v|CCCCCñcJl߿?999;{キyfBѣ%65 211LOOrDCJlMJJZ*vBzHD1|4xpb1!֭[w=}4 Cgy^JxO$3}ҧz+**؅<9s˗ .ezaZݡC6l.IOO/Ο?%rJ&j7EܔOs;aÆ &sӍ7e_ ~jٮ^*ܰaChhډ'hpTQD" ۷>k׮k׮c4^*۷oԩSMMM|X |rӑt={Р3n8=z?^SSѱK.4daaY***x<޺uƌpf͚uufҍ-Ði5ܰaCݿѣGWTT\~СCBpQQQ_1 [3 ?*'bŋׯ_O4hG}$ðaN:%^z%1ܼ[n%%%q=:F$]|բǟYSSC ?;K(^v͛Ν;ӁtЩV>}$S\\L߿cJKK;wM-rttdhkk={vȐ!|:a!##)oooo:r6|W^bww}ѹ#F(//_kkkҘDhѢƮ8qCihh0%EEE|ӧi=11qǎ˖-eee5k0Kٍ5]z, ;"'o۶͛7:bkk2i$q@ mm3gH̞ SWW cVVVfff^^^aaak׮Uei5x@G.--ݶmΝ;4ۍ4scǎB\]][%^ZSS>Oqrrڷo r֒MSSSc *E IDAT=釻w}||"#####;w,y!Mdvғ]\\$SNd|p8Lٳg32=ϟ3oM;vtc:h쾺oBP(LIIIII[[˗O؊}Zi4W#ݠ-[Qnnnҹ!=zDqqq~WzzDa Әe˖I|rJkkkºZQZZttt[?n߾Mݻw8qL!Iڵ+!PΝ;= ֢ÇԩSmmO?ҿ)"B볗K`"o$0阂fڵk޽%**>ʊ~m"mJ300b-//ao FWu! >\9r׮]WF~4otBCcJ !gϞB$nҥKBȓ'O {S۽0MScggV__/RBZgΜYQQZ!L:uСB0///!!hڵǎ;}0H##8V/,8LLLٳvڢh9s9*BTz9r֪[n%MY!Mdx벯ܦ4;_ !3gldBȳghM7$(55Fv$> ,2*.....622eiFI ddd4JPZr!iӦBvْ$|纺~\| {2J8!Ǐ?rȹsRSSJKKwugikkkhh*]ՊIc\.|"th 3~D15-dQX*//T",J{LԊIvk3g7n8cƌV94MM 6DEEݾ};;;=yd+_Vd PK--ŋ/^߻wرc7oެ3fx`޽-F/lB{-!޽{gϞeWTTL6mȑZ6 rܹBرmib~ }|coo7Bȗ_~qƖ0a!$33Sm)))|2!DMMM" eVz"RuuuFFM555Oi@!=L^at;wN~.dOOϠFGGB!"hժU ,ݻ rڵL󛨘~3wr>^jռyx<^BBݻm᭷)'SSe˖ܹӧ3fqpp͍ Dի|qqG}si#x{{ǕӪUݻwjjjegg(4Ogʕ7n(**ZtׯǍg``w^gD(2ͶtҸ855fu"<==+WWW>B WXQ4A4hPvv7BCCwzᄐ޽{:TKp7g}?B<==׭[ajjZQQUVT3gΔoBӇkhdɒHBȮ]Z%KWWMccc:@$ٍui<==ҒlGIHHs玳3ǫjiiO#}}']C=&*fiiٌ!$ ãرcǎcg̘F8z[n]NN΅ ߿/1AOOo۶mLgD{e:}tc+X[xǏ䔖?M4IS:ujҤI1{[hfС>-[~ڍՏ=DښѣGGFFΟ?}¡CJ vP?Ÿ~~e6~Ν;7]޽CCC/_^[[iӦM6TWW3,fϞ>OOHpss 믿P]`~bmt?.ޓ]rԔ"bq.]\]]<<ٳgŋB}}ɓ'ō=q8"n޼900Bx<ިQZ[ ܹ8i${{O?tݺu> }Մ2dȐ3g0/ݳ{ g7- CUWWBƎi+Wnٲښ"ѣ֖~:\.w߾}gf\5jTDDJTXvcY0WQO~Aћэ޽{ڵY:/BWQYd<>@)))iUVVfgg5s޽{7nܨdeeꚛVMPrcc@]v533{kt֬Y7npvvhy=RSS4O6Mc+ѿ (t˗فf+++522ѣBfggWUUugۭ{&%%ٳgvvv>l^^^z믿$t\cP\nKF֠&>h8#77‚oړ'O˻uFWK(// !^GGWUYYennj˪ȳxyy=z4;;ɓ[VҒ-귩޻w!Ą{V餹!;wr پ}D8qGb*\.cС epС &lݺ˗Bo߾NNN~~~ںs+qZ7$))~~~tjxZQ/^[XXhkk+:IF IF@Z?(@cx<￯Zhh|>_UBJbEbCZ^Z98@!*PypCQ踄Bamm-ܩS'P( !uuuJt8p@U^^O?5煅ʭtLp@G?9Ñ($po  -,,,,,ڿ2GPypCTrp@GQYY*B:,eWTDuh<8@!*PypCT+ bqp8QFMhGVv&66VU@) J]BpCT<8@+-|ȑCΝ;.fiiFjjjN پ};lll Zf͚+VDC YYYuuu...V277ocEGGBd%VZ-CB &CZZZHHH|||AA_~9uԷ~Ml]]W^,ZH$=z􈏏:x'u˗SRRp0p…SS ߟ;;;O8Qbܹs-X˽{Bl٢Q*D$//ɓϟ?{Sr*ڥ˖-z*Y:qgUUգGǏ7|y<^``9sBBBMֽ{wyfϞ㐐M6RNT7o޼;v4oŋp[O8AYbΝ;chffF?֭[ı 㭭+++C\.w儐<9 CͶm䡡bgϞF:pӧOKh%%%7n<|U}<ƍ?\ui8Z>}Vܺuݻ[VѦ/v D"??}b552 'lݺӓΚ5-++_{'}`3ڴi)))Ν؛|Vhhs?gnngddKKKKcATT!dʔ)rݺukZZZ||DZDX\XXojj*11YYYBo߾ÜG={紕 fff2VgsN*//Ĥ[nJpo>g/.\p޽gnڴаu_iii^^EΝ?גN\277߿?ÑHJJ{.!zΜ9Ly.]m. <$}l3ԩ~~~ ׮]c)}ï^qwcv',ؽ{~zz:2D"!ӧƄt;wTVVBƌ#}8΄ kjj=z4bZwurZҽ{wGGǭ[vڕ9|͚5ǎ_W\TDQWWOu&qň͛7x9~;|+++>pB AAALÆ F)q+WBlmmHx͛cbbb1-ݻ7ӧƍ#߿Ã9Ν;ӦM#cʓ!R(BDwmgg7i$ͻwP(>sKӶm.\!9ΰav1p@f^lE|ׯ|k׮c4^*۷oԩS` BEӽ8iLO>CHHHLL !䣏>ZpǏ###/^@>}S3g7mڔիC}7tgϞ)444|||ƍsGJWp֬Y<oݺucƌp8сUUUfͺ~ 933vzyyy3f̨PSSꫯƎۭ[;w9rիW=z5kѣ/]$ܹS__D ݻw o (3}ׯ_4fѼ<-9V? VVVu?C lذɓZzeNm+_~m`` } Q v)@+!*ݺu+))kHBH$|2a ptDPPMګW////Px5Bț7o:wLӡZZZL0Q\\L߿ǎ3; һwoZ^VVܹSNss &ܻw(..NTSL9p~bnnnee5|p@8mBH@@31eҤI?BBBB>#ZheeeffUPPvZZ oߞOٰaŋi[ee5kf̘rǎ{ҥR@ 55x7o<|*i?$7] ---f8 3.뗗0Zr<믿5w{p4fzL>nhʉ Kljv3*Jo-))o>555RrT~4ft0H9,~Y$|k#G\z5!_s˖-gΜٿ?nP4DjjjCCɗ,YNhjjJ?%ScBD;;e˖IWѣG&AM<ܝ$qvК1 _~t)ЪLBرc&sf4dzghG8Z >Cci8itﺅoN:R]]MD2)忖m+'ڌM!Ռnf$7PΝ;= >|xNjkk駨(OOO+t@ 'lG.l"35iMM˗/+**|>!SNVVV=` IDAT!'7|BHEEEvv6!^Bw3B/֥KBȓ'O83˜Tɓ' 0550`@zzݻwiHZ77={^r͛46D# ;wyak,%rvȩUtC1C+}EBRmԩS &ߏm¨f?A$u8LLLٳvڢ5kB ̙7ȹP(p8bN-g||H$֎a)###ݛ, zuuuG#LMM92}3X[[KY 2[\\\\\lddDa2Jt+-55涔K0`mǏ焐c!֭[?HNNu;8;;7>n)23e‚BbZr|>Μ9MYGG̙3\nCC!!''~`j֒cۈ[! v}H$eжחC?H[! pSiii]]gϞ|>_b)ݻK{w&L0a„͛7o۶>11Q"AgXH:^&&&_~ݻKKKo߾i&ZdG>ks6AMM:!!͛eee\!bX/9`ܫa}mbOgee )dhjj~QQQw믿!Lrqܹ֭[#F XXX0_iLB]V[[+1ŋ]EBKm#>n(ڶr9r$ EFFfo***'1B:9N۟D+z#,yyy#Ժw^PP -XOO dcƍw>q{f!Xx:-?33\bJ=!IIIbX˖-_<`^zV*}e˖(mmmSRRݻwӧ3 ,xu>}N}2`ABjjj.^hllJǶEѶ3fQqqgϞ:LsaЅ!)))&M5B32322dn~Сhjj2 \WWWnj *(,,{EbEWW_accc:`SGvvvcFOOO&ȑ# wqvvx4 VPP駟ٳG,ӵEO>Yv-!Çr8!^^^ˋ񱳳 -++[|9Gc ȃ?~-''0,,.fnn~ԩI&UWWб?x̙M0ĉ>|kMΜ93qĊ={ٳuڵfbׯW^4Dq6xFz륛uرiӦ1A%OOOwŕؔnkE>૯wիW>}J#24߭Ç闑ǔSTTԣG۟Q__OGp|'5k 777MOT( \.݇'ǻr劯)!D .]FEEI=Hd&lff6x`B{v؁&M)>Ϝ93~xzҷþYMw 4i~nݺҚK2dș3g'߿޽{%v_~-\Cf%xYAz ZbtMi)?ɩy$8::;weDf͚m۶1-YDSSؘŽԪcFzӘV 5qB55Ç/ZM[WWG̺'Np8sa7oސF4s**2>]]UYY+RΝ;Ӭ޽{7nܨ*/ٳrHMMCϤ9`+//SWW۷/\ZZe9/]YY+;BRXϟՙ2D[WWr544=v֬Y7npvvn`zVҤ[GGry4Q^^^z\؛222Ĵ&47zhA L|>?66c!Lɓ'O˻ufmm-yy'O!:::-:ʬ,sss666쩣#G^^^G>y2cddԊ;w^rl߾]6O8Ap844}}}455ׯ_|򈈈 62Jpب1hƱӅ=EE455!;w,..Vvu$988EU>|XUhԡC&Lu֗/_ BB۷1KNNnyI\.cСj*211IJJjl]"mpSN:U x񢾾B[[[ٕ>>>iiiVVVÇo:Ν;燅ITLbO7d;;}wl߾}ǎ666_|-YfM]]݊+ޥG^^Ν;u޺@ óLMM]\\VZennތGGGBcb?fIS)G\Iv%l&lf-k uuZueۮBVbiLek}|3} {~c'55))**:vٳgO>=jԨ6?>v옶6_;_ZLLLaa-Mpp8??M6͘1D@ۻw>x𠓓So;wŋl0zQBȲed1Յ&M:s挣c3BPBCC|#$3oF~>nݺǟ}555 !>>>Cꫯ***6o޼cǎ Mۼr/\P1̘1#++̙3}B{#C ?KK:^}}}nnBiӦs^OOo>u۷|ܓb˗/^{^^@ ۷Fc0533ѣ<(,,D={֭[sNȨ}l_~f7ϟn:???yO~-nP|ork$6.[TTdmm+]W œ+++GMMMAAA}}}%֭k֭Dqqq sG ߱cGnrrr$˯]iʕ+^...EEEBajjJ߿IIIquVMMMZZi9۾}ÇhɈ#6oܽ{w!!!:tSVXXRRBQWW4iƍjz s˗ϛ7ΎEFFΟ?_wFFF&&&2%Æ [fȑ#|EBSc)iiiQQQW\ŴW^p OOOB޽{}||&%%͘1ϝ;)OOO"DGG+d'N(Bƍɓ'K;wnJJ@ HNNnv[lrssb1rvvkyj]=Zfͭ[蝥dɒ/봶wb8"""00yׯۘzB͞0aڵk\zjٷBcnnnccsEF{ -,,~k);P1فYB!Š|g۫WN2ڵk/d^" BX,֐!C jQSSC7D"Q``?d7!%%%+ٙFU]]=ӧO!D ;w[J>S晖vʕ|  ?P2A}̙3322$ KJJ4\uvv/_d7!|?@ar.3dP(h//D,ԕ$f֕7o6/[FEEзQ,߽{SkIm^і g]7>>֨F$ h|9sN:'N|a&O]Mmڵk]fT3ҁԄ Bkk[tPG?˗/vZ~?NKW(ѧOiӦ+WB&L0|{ƞ;w.//o޽g9s&xڵ>|YttEa<D:::W\ٿ?,˗/g͚U^^pVZ5n8u刈YfG^7^rQQG}T^^駟zxx%%%8pٳgffffRWW;v/_IIIL`bb"ޘ5PWWW(2a?N7z)Y#H3S#KKK7o_5k41-A8/Br1 1cƘ%%%xǏ4o߾Mqvv>|xvvvLLŋ>K+((TvVggǏgϞgiloll,!D(^pTUԁׯ_]'"##VVV ..RYYK.ZZZL2+BHZ,Օ'''3 -[TVVBÙNNNnnn'OiӦ"uuC1vvv={ ,..>tPhh(-g:lݺŋ5k|˫"$$䣏>bϟrtlp*++ܹCYz.;uԦjJKKːӭ[2ɮ: quu=uꔶ6!wGqss{!3rAjAl6{̸^zׯ7ͥ>}>}fmllL2o3m~ ȹgjjjKJQYLqnrΤH*2//o믃F_Blի#7lpɽ{A'DR'_x䜈Gr:)&u=BȈ#VpqqYtliii777 SL?DK]>!DrH\\\կ_UVB=zD𠻘Ovprr?z39m%RtttV{p4WӁ;w -)) !-ZH;M}?d?ާOcǎBz{q)==ٽ{қyyyfǍlknݸ\޽{ҵkו+W6FlBMM͹sLMMi3x۷oPO<9q!vIm S2dneeedd ]Ν;WvVAW!w<{e t(LD6m``@;1=H&8^|y̙wOa׮]H[[˗/[bll,޳gOBP(ꫯztwssx葜6/]4,,_>::;б!355(//zjxx8پ꫔7o^r]SS3999??@vp͊+.]TRRdɒϟ{zzڵXx1s!MMM}c$˖-tҋ/z芊 BȆ $?~U(WWWjjjƍ3kB[M-^866}?'رիW7o Լ}ŋikŊ633sܹ Rі ?eݻ߿iȐ!?NIIݺukIFh>}96P$5755555%!0á?3cԩ{nRRҘ1c8N}}=BKK+22RI{ҤI;wtm}22{;w?-e```tttQQQlllpp^PPО={JKKKH55549|WAA={,-744/v<|RCGAX޳gO:_r$% MMM:X,f&]~i<ɓ''NH+{9+0)))""bC4iҪUܹ\ e'Odv:rGϟKM`Ooeel2Ha)^^^Ǐg5jT;̩!Zx;#F8s " ivCOO/$$d˖-atUy25L#얠P]⍭ixLTRyF?\|4hboMTyTTT1sU6NWWAڵ+,,LMM-33UVLxՋ/z/dggB;c ʊ+yɒ r7l gBԴLFPWWf544bqFFǏ,,,,nY.]4f̘i76o޼ .G~Fr d!Mm/99yرXN</11} 988_VVv}BСCuttZX;XAM6m֬Ynnn999$FZ:8.%qaak׈Do{U\\!dݺuƛ h!MMHBȶm^zp 6,---55uΜ9111|>?77wŊ# 8???'''.yVmQ]]]XXHZ1P[[;--~ yfMMS^^NdqiZww_ѽ9t!*"00͛|˕Ɔ?=zY޽;w\sU)]1466n,۳gnp6z%KJE7!ٳ˳` 0D҆(O?yO B۷Q֭[*\\\d>l!C8vh 2lذЫW2KvqѢEtVann޼itG7}ӧ'O[[[kkk+;(  n;qK066oׯ XP23`eG׹[άsN2 oh<O!hR%bX)@QHpT<$8@aQAmm-ҥ@ :qݺun=/_TnH1!adX,Kfc-tL t|T98Pv!//O! c~Z1<$8@!* PyHpCTPyɲb#h,Kp $$8Zرc$&&*;$8ZCP * PyHpCTdff8p`Ȑ!si*>|hcc3f̘6#9v!d̙:::#G|'''eGՊ<33֭[l6{޼y,FPV!`O>]pa߾}QWK>w.fեК IǿiӦ~-Yc>|x;;;WWזT .;gw!p8vqq0`ylݺ~pttOhIHHH]]ݲe:x`ݺu_6m;-Qb˗׭[Gṅ7Bu5Ý;w9qFEk?[BȳgN>݌ܥB۳gϳgjjj>3 I.\5lذ&-L%oYYY ~z7f̘_*iiiykkA<}t׮] 6\&&&V?Ǐ;vL[[Ipȯ/M*TiӦ3fH^Lڼݻ<ԼΝxLp=zl2ٽ#F`,))yquuuaaIΜ9،:P>?b+7BG~ 0qDk޽[iyq˖-mt={=z=Zv'$K***֮]{ׯ_={k?t!Wٱ鍠\ .Tn 3f:sw}gll^ dǖRɳ檫[YYiii)Z>O6m6o[nݾ}s8b˗/_xaaa!go<@зo_ ƎaNkff֣GyN[QQQXX(z٭[?!>TUUUQQQmmQX*:~\.ZWWWW5y4h 999:::VVVr~ӧO_nH$Wh/tc(<=8wѭ[k׮gff|ZrJKQQP($dddBuz@RRREE!dܸqboݺUSS6|pZo~Ჲ2Zbbb2bĈ͛7wޝyyHHzԩ+V$&&B'MqF###cbb>B|vzPd1}}ᇩ|>իSLv B矄5dBbZ H?2 BHIIJvQUWWϟ?4AΝҸO?y]r7|Ba}}<%۷oϜ93##C$55,UgggO>L7O;;;Px &$$va$'' BHԦ3tȷnC#wIDA!g]5D"@ WW\\|=UUUEEE58Î,z#\vZk֬pvvj!r0D&<<\(Z[[oݺյe>'_|kwqB&s\BBЍ>}ЍM6]r2a„ܻw/66ܹsyyy{p񒋊>r55O?())Ϟ=0335kرcϟ?/&|2HJJg2D0@pͦPWWL(6=f;D.0G\ZZyYرcJ!],_J_uǎ 3fYRRRXX;~x@@lc:;;?~?{L?Kc{ccc !B… (%$8_NNDFFAA\\!RWW]UdիW_X,>x 1x^zl۶K.KKKoodSM6m߾}+ONNf[l$3]񝜜&O,c>!dӦMEEE0a-ٳg```qqCBCCi9yDw`֭/^ Y?^^^^^^!!!}8|NN544$gggX,SYYyΝQF7>E?rNtZZZa>쳎4ԩSڄ޽{9rÇp)<䯫cc ձO<٫W/]]@X|u&X̍@{n555u@YLqۃcccccc>G?_uPPȑ#kB^z5s NSl6{鄐jioպE]݀5e: <p skȳwݺuIIIiiiЃյK.?c||[Zx<""8p@w6lhpjҚOx<3BH$D tN굴 ՚R^^O:tlEÆ BxI]v%ܿ)a~~c9sY߿okkkaaakks Iѣŋhntuu##ׯ_7' Ғn0ACZhbYXX<}$17B=8+{/*RB|Ν%%%YYY!!!CCC??E5#C@ `XbXυmmm[;!֭[BP[[;11QGGGHXk׮L@ du ;;Α1`3888Hdgg~111111 իW^BXVMgslA֖%xxx4!D$]z2~x:Gff&0f̘&m!U~"Bͣ%uP]lE&np)̍-^0D2uԻw]ёr۷owvv_96}:c >R]]Ml IDAT+t=@p3dww}6OeJ.B1`,,,4YIhTR$둝]ZZVZZjhh8dȐ=z888b3 ZkGT77Ry\sgnz/tJAiii}9KII9x`BBBUUʕ+ǍoϛHpH 9{l]]ݦM|}}A~~ݻ !6l3=M6nج#666t$NvvLuuu@0k,ګieeeMRqppШ^fӒQFo޼Iצy(//''':YGGɓov\)m iB!mzuF=A ˭,ѣǓZD[%}}}ooooo﨨-[קJ%88)4Q\\,gE ,رcݺuڵki93'… G)7555w&$$J"e-555mmm^m (..n ñKOO{^Σ;߸q!0/zzzn۶իÇ'X[[3+дB1kiisiR(<B|0{c***ebbR\\KB}}}$8(%v mmGJ.,+9 9ѣnݺǏ.圠tҥkii!i$}vJJJ3.v/^,XĉLǐ 6䔕r?)///xy>};F BݻGW?mSzz?{nP !:::͎7.>>?(**bLgWWnݺqܽ{?ʣk׮+Wl: ShȕE!6,vWn 5[SSs9SSSwww9vtTdk^]]9 J˗gΜipW~d vE|u!ƒ={$¯* W^tHB[\\\EEţGlll U__ҥaaa|>фwy ٷoGyyիW鬟W_ܼyʕ+kVXqҥ%K< 55u׮]?ŋ \njj#Yl٥K^xwߕ3'''GGGWTTB6l LWZEpuueg6nܸ'OY !ZZZ_}U+%F.%33sܹV"C~mY(1GO>}藆{/^Kپ}Q@I+ojjjjjJ)++C$B#??@f ԩS8pݤ1cp8z:VddIv)"}}6{Bܹ#g]TT⢧gϞ/9RMMǧMp>UPPrϞ=K :D2diiyɓ'WWW w}w̙y{{=zΝ;ogϞ'O|wܹsNɽf͒,ׯUAAI3z7VByVV͏4kE6WW__Oh{ CJk**LFfc$p8/^\r!]HaС?#蟒}{9h B``&S,3_~4ɓ'Naꕽɋehh1yCN4iժUwܡWgnn޼ЙtG7}ӧ'O[[[kkk+;(hؠA۳<:@G;p8 PvAuѹ:՞d@kx@+HLL*J4BFvT/ąo~OK.::: 8hmÝ9A;vChCA%:7|3⊊ MMMSSS}}|P:WZZk=====SwN +;6 7tVf TTTTUU);ibbbbbR\\kBH׮]۴FqкZ҆;uuux@IIfumm@QUUp@133p8UUUmW8hSa$8DׯDmqrqjHp@'! kjjw@:ݻ¶898h a$8XY__F⎃vPF:6@uM;ځ t/.. p8ʎH _~`޼yzRblN|!=vsB---gggWWWD%iڵ'N`={ %8 >>>~ƍ(/:QWWL;N`o߾};;;ѣGl6wuvvVvhڰocӦM[drjk|>?SnnnmmIYeee}wݒ%K>c6#}@ p9BHxxx׮] !Ƿ~KyӧQoHHH]]ݲe:B **Go<_oK.=|PѣaÆٳGGG_IIIb cyG7q…aÆuGeeo߾͔sLݰ/ _~>HKKk7Ν+,Xŋ[?~ҥ;wlKP>oo);櫬\d r666k^~=55u˖-k֬QJ`taÔ@K4ڼyS:l`_Eޱc(0??Eqܓ'O.^XCCCya/ᎠFW^]w޽ Μ9ûw&_׆Bݻ{n__/"==Ɔo߾B]]}…QQQ͛#HKK2dn 8o/ҳg۷уr-D"˗/322={V[[+9sssaMMM˲岕>~8++ʳ(P(|AIITymm^x!g /r%%%mHjӔ[{p=_HqDx$*ژI INhA{pTUU֚XV -lݺnlذArF 8qڵkoll,DBr}}{zxx۷m*r+ srrttt+%::DŽEm޼>m%BikkO8СC?.--500BpϞ='Nx5-QWW/%ƌCٿQTTԭ[C̙3.]oÇW}}7nHɓ''NiӦ3dddܹ̓sgϞ-YO81x={^:%%ҫWpWWϟoذ!!!4111cFӣ rjkk7nxʕbZٳϟObccWZ,|ӥ.X"1RFTTT6nܸqfcx{֚O_;zhwwwKKٳg?~8::VT=9sf~\\\zeZk222w)J(BƘ ɮ|aaa;wd@'Hv|DBP(fdd|G7odͪ8D_cBH}}=A %O!0777 뒿۷OH@  kkkK.!$??nt3MUIIݻf/__Ǐg3Iii;,YBwABUDRdkLݫٳgOPDCjMB8mXӧO<7TVV~嗄;;;7n0%$$k;w.S, Y,3˚5k٣vQOO]$ѣe2+8$%%͘1Cj/wޑ}=zu޽ HD$|9s$Dqqq)))SNm߆b__B****??La-B{իW|!@c!l6o߾̙3{{{__ߡCo ~W]]]ɓl۶zzz=zԩS hl .>}9I/\⢧קOBȋ/***̙3f̘ݻ߼yرc? !ل??>4&&毿|/?O>][[{%Kx|/SNXϛ7OOO;vsεk&N8iҤaÆ=zhѢEo~[{sNHtٳgkii5v|33i!9^M6&piÊ%8>r55O?())Ϟ=0335kرcϟ?O0._L7^v0% .ʓ rLaAAAc500kb3˗/ gggÇggg\xQرc!!!cƌ133KJJ xǏ`@y} A%%%ϟ'{|#x"RHUA,Q(j,AckQc,QƮ1Ƃ5k+,TТTwy}8 ٝٹzψ":כMLL N8QfS~kww#GVWW^:22Rai^a&w^uւBȬY&L788xȑ˖- 8nܸ  ɑJի鯾]v}Չ'jjj>|0W3p!;w={6]ңG7FEED"f/4A۶mT{=qD:oBBBBB@ ^eVX*uuê86l@ oMFz{{GEEEFFSNeddXYYBSSSy<@ Tݻ7!D*ۗ÷l٢B/^/8LMM---KKKKKK[Wኄ, Cأddff68pҡC#Glْұc?X >=tPĉRkט֛A7o\r%/ܧO>w1SrP~m\\\Dh IDATaa!7̘߰1cկ_~1) cRR:tꫯ-[VUU<ߧ}OFH+,,ĉ=:'OB{@}iڽ7Kfrssۺu+7M$ݺu֭[6l:ujhh(Xʬ"ZZNcD7( DEEUVVfff1ƍvi׮ݙ3g/HKKwҥKGݲeK+Ockk0mii˗/[WɊutHyys4½{ 6lYYY);ݠçN*#, ܿK( )':e ^0zF-<== SRRdK_i֭HD'ܹ|.]))){B ;;;vJ#̭#[iBzѣ}:2OTl̜93,,RSSsȑI&s׮]{Yjڽ7o3f8p͛7WZIGGG߹s[PNtv@x1nݚPx[Ogfcj$]ve~O>/cg$]IEt˗/i(4 pUZU& ݻ1t:i@ dgP>:㹺N:ٳ{ibb|2g77/_ 5kt1i,TJ]êR!!h .E>ƍ3}GG8;;Hm^'Odg8r}$.СC2oC` LtQn߾0s G}ŋe& {kײ))ի!ҥKnݒyW*25VӧOd7da ߹sg˖-SNe?~711aҧO?#//34wnaaQRRm6h|Juu'BaHHƏݗ/_Yf={B<== ߿vڈ#B!!B`ƍ| ]H-Ydt劊+VϞ=裏4>1. zLugϞM8122/''ŋ577QFɯ{갰0kk[nݻ7?`EsSL|rAAu늋,,,{پ79u&rrr233%Ixx?iӦ}wޱd{~5T'?wuvvH$w煉<DFFڭtHDwߍ5ήm۶r,])Yf]p}]aaa~,--_cǎrBȪU5o߾ ,&t޽UVt^>}~w.ӏ?NٴiӘ1cr'O^fMAAҥK fΜihhxƍO></55YvǏ|ќ9sfϞ]PPܹǧbٲe۷o'xzz9R+iZ7nX,>|GE"&OmfAh@_~eaao,;v?|qРAΝ;t/GnLҜ;<&&&&&̙3m <o̙QQQ555K.]t@ `;'\z#[nȐ!:fiȐ!7o۳gYM66mbR#JϜ9ssixD B _eggOL۷?t萹D"ټyaÂϟOϟ?_uqqei0U3/>BsG1227o?}ZIhO_Ϝ93<<kddԲe5kB,--:+QmֿEBcbb._,ƎaT1A6«ԍbҥSNSQzzzߵksW֫W;v3K?oc?%cǎ?s޽}$mmm,X0ydvJ%y]7yGslA6 tݻ_E"С̙3w)+V|GB]DeJ_>c'7uԟEnرc `ҥ?` uG t}넄+W^|yɓ?3kx"3(!C*wYhe,wׯ_ի_~%{ٳ,X@˻, %%O>ajjj.\O?g Ɣ]|Y>>;O/͏?HC)))t\FMMT*]b埘믿>y򤶶eĈ-ڲe93}UU՜9s1v ZjH=-;vdfG:G-.D gUkZŋڵ322IP\\Lg[l XZZsCC 7ϊgϞ֭[[[[ +j˫rpp?lR**z{qWWƨEo򊫮~aFF{O-V檪*HdllƮD u7\Dbmm- ~33Tk;;;???go()))//ãSNjg@C:w>}CoVo 7 Di>h@\]omAmi7^UdSd``H[x]vڵ4 of!o|輒:oժUrss\B4{&t5 ʡAxaeeell|$>{uҥʕ+˗/OؒjmZ`q^x~/\pM7hx<^uuo!Bܚ:/ЬTWW5t*{ŁQ#ԣGׯϟ??...477/Fg`` [[ۦ΋j,XԹfŋ*=;ݽ@T-ޱc!8//3 |>Ȩ[TQQQ#=W*F֭[ oIUUUvvvSgeggWUU4.pAR#͇Y^^^VVVS)eee噙7^pAQ7!*eeeMxehZj%B!}WS *)))((hٲeVi Wh&u8._,D*6IN4H_ e-<@`mmm``˴ׯ_K$-?42&mڴ177\q a^qqsДjkkESWW+7x|>_?-ZhEemѰ#͓D"${-y`M_x<xLQWAuQ&MOQ<8@!:ypCt<8@!:ypCt<8@!:ypCt<8@!:ypCt<8@!:ypCt<8@!:ypCt B̙#fܹҥKXB\]]KKKM63F&]ii+}- SRR-[3g:99)`ٳs#׮]### )jҤI"@ zpB߿|r&ڨ<ӧO-,,¬ibqEE$''m۶GGG===v;11ٳgڵ tssSkHԴ4Xѻwo=7xÇӇ|P |gK,al?77wܸq|o݆ }YvvLb77 6ٳ#fJҩSr{?Lt"6oެVJJJFp؏?YA=zÇU;"hڴiRT;ؘYiӦ~633cEiѢ>!zԨQ= ˆaÆ%fbV߳gX,{lٲ/ё.yΝ;: Q9{lxx83'OpT*ˣ}}};fff'={.744LOO7556yʉ'߿/O8AgHNN~^zEYYY@$uׄKKˌ >O?~<' zpx{{'Xtirr•膞iW ~‚USSs.9uz޼y#FE"Qjj*!D,3Qm۶B^z͛7o"ox>,,,D&MOc浣oV|||}NO?՗@(2٣KL) #G\i555 ׯ\RWWGݿ+МۃZ`A.]{sgϞ2>x ))SN SVQQэ'?JѣLT@5prrb?O~+W|gA֭[0LLL111/_~T*%3o-[ܹs/FvvtB{ϰaØ2j!...,^qq1!Օ 3f` 4̺%/ZWWg?~YnaaahhHlP={fIpm<8@!:ypCt<8@!:ypCt<8@!:ypCt<8@!:ypCtܹs:и{>߶m[$99yʕ]tiڼC#mvaKKK|ԩ{x< ;w={6--st&l$4JɓOx"wpphL43ڵksss]\\f̘Q_ҋ/8q|rss7AXTTׯg͚CL<9==}޽ݻwo%_L?-ZD:rJ{믿.]J?~ڹɓ'lbnn>vXDj6pGϧ6M>}:%%GJ<_ !QQQp4>_IǎKIIO#=(aWIIɖ-[!cƌqvv~j,Ws-\ݢZW_QΞ=F7Zj35OTlٲk.[8q̙3:uP5=(7pe˖L4M +??~ t޽98;ZE+NaQVUU%&&B=ڴ|7^~}SԤ?eʔ΅5R NB(sssR)!dСnh־`盛jMDB+++[[֦{zzr~eee^^^MMMvZn9sssڷooaa`z,J ?nkkۦMs[_>|ooooddH{aYB%TT-JH$B(M IDAT&&&* Jj0}III^^~jjjkkkRZJ999m۶mժhzǏbS-$xɫŮ| 8 +% [[[VTX͉{5kx{{w),,gϞ:t0a“'O4~P(Y]"xyy !Cj*OOOuqѢE"nTTP(۷o3 B3g!< B0 qIϥ,S]]=gΜΝ;{xxy{{]F&%Eq,JW^^~}ɓ噙Frqq stt\~=s_Di۶P(+J$u1WԩSKJJfx}СÀRSS#)*--/CBBǍc2uAPL7n=]Ǐgű4j[޾H$ڼy_PPPǎxM&㪯p4292egϞ=|iii2ݰaʕ+322虗J SzO?tLK?sРAdf'{iԶiӦ-Y}޹sg>dΉ㪯p4TUUM0ȑ#*bĉd_*]D2_>j(vB$=x`ԩ*|8KEqi:dR~#t ;vp#>x`DRSSsY%$$dggK$zoMVڷo!s֭~}Os>mРAfffY6D__>K6l@o߾{߿իWl!777;;GV:}m۔d8///22իWzzzSN=zh\\ܲe !hJy.((=zWw}wʕW~wrr*yڵ.]"СCIIIv '<~XUWW/_\"8;;8qILL\jDϧΞ={ B۷o߾M鹗BKjj*Ǜ>}zjjj^^މ'zEew^(J>yV&_W^e1d^^^k~~~TTԙ3g6nزeKBȁ޽l_XzիWi%Ζr͚5Ntt;w.\@D~z-ZtUz֭|;XRRO۷d֟A%jԟFj+IIIǎsrrZ|+WI)//7nDZe{AlliqAwwwBHVV HFQQф bڵk\BVXQZZkr Q:} q [㴴c֗433SfIAAu!۷?r6p@}}_~%%%eӧO7446l޽{cccN:E mݺ5!pʕBlmm g̙t9cȐ!;v2xÆ ϟ?'|ӦM i߰򨨨H>=k׮߽{wi2O81??ڪ湴o߾7n_XXxРk׮UWWBVXO8qX,? !t8rlddԡCU{Y(8Yӵkŋ%;w6mׯ BjzE =uTFFFII  @ }v޽ !R[۷/{ |>?::zС_SSӉ'Jk׮(/sBѣBf;vXzzz||O<5kV%{3+￿}vکã{"_|M"ӥKB㙎l&&&QQQz2kɟ| 8 F@T-7XG˗hmڵAiyyyf~R^ntVj۶-; %ܹsL7oBFl_p!;all_BbqJJLN͛q5???掅rqqY`! psRR!$((NY߿/y^jաCmƞSEGԺ:.Q-`Ǐ?~3P6HGol233iqPև:~8gA^Q2BCC 4>>>#iii'n;`vM'nT2xfn:`Zjܹs̑6/`bv>?bBHUUW/.޽{Lht5XHm￧ s+ln,مnkkKC=}Y"wСCB"""! Gȟ| 8 z+o5.N?D"Nbb"BEG˖-;4y%̷nݺ$:ug!;;ɓÇ iX l˗k.}]2---TH$e޽{t\le˖2&֝yf~Wʲ!uuuuuuus{555?Cllヂ\]]dT- y^9r!CzA{qQ%[FFƍ7hn',,̙3/^\p!i[?<ӧ pSVZ͝;W~Jv)JNN/z!]ԫ?V0d(N:]tˌTjf2 i*vr+**ҫWh!?ps,wĉL~t p)yzƍ/_^B5mV#/\K ff3cذa[5[<ƉSpjj*A+ !͍IһwFDDB955N<>QTTTTTdmmF !R?Srr֮]͛7ϟ?0%%%**bee5v/FUjYț?Ϝ9SSSw޽{㫯b(RFhh( pB!}]4A`` `maDO$ߑ̰/T:E~^(ZZZ'$^i233OqҥrU*5rKKK1NjHϟYٳOfQHk~qlXyKs,w棍=J~ :ݷ @~R Иdk׮>anݺEgzg餉|>항 +`¶"55c[lE˖-srr mذa 9z(\RRiӦe˖} sʕ+$>>>>>{wkV244t֭O<)--^^^))).])jxЃ8EuҰso_*YUYpeggߟy"I֭[liaaA'B+WQ?VU5R5jTRRRNNq8|}}ccce&*hc[={޼yɓÆ b]t)GjѢEmmmzz#3_̳X,=z4Pvm߾GnvZghhڴiӦM+++ߵkŋ+++>}0CBP'&&F*9s...}7l@TQZ)޽{>7oޤ]BCCi򰰰/^d;w=ۅ֩ti ӹJMM0'Z?~yyyNN|'1.Tj3/"yk֬э?|֬YL_ DҾ}{ϗehljԏӨ?vQFѧҹZtF70ts["nhhX]]|'3… ߨÇիW^̣(;w~Ǐ #&&&*n3>>ȑ#^z5f3-s6o[)-^8,,,<<\fIA{:Hff˗/lj; }Z={-,,hV*(%$&&|F-,,JJJmF7>K*|72@?y͛'s)ܹ3}1{lf,!D*N2yn߾0'.… iWܹsi(?. 4qѾ$b9W p½tUc7M$X))));wd?dmQC(rss#""ݛy…j݄//? !?3!$ Nΰ9s&!O.,,LHHذa333kkkYlll!}ݒ%K]=jԨrBȪUd~j0ϳgnӦT*1cƍSNEDDeffj2)}@@4n{Ҷn:|p-啔tǧUUU>}zҥkk޽{3۷oOH$s̉ח^`899|2))i…;wJSL& 6TFi(i";;["1wzzz4A78#wVVV-"9rΝϟWBHBBB/O>!ÇNOΟ?qw۷~m׮]=b"#.~iiiDDĎ;.\0cƌB'Ndr"GkrYY̙3oܸQ\\yDZRcEI$''0ej|РA;wBv*;w49y߿ݻ2Dh߾Co _`ANNαcn޼)e˖ׯyJh ի͛7o޼G-<8p`СUUU˗/y̻ᆱdM4i֭_}\OOo=z@UÆ IHHzj``@ X}#7x͛7KR''[K^YP<o_|EuuܹsΝ+/|{JQ_Dei׏@ Xӧܹ3==]湰mڴٺu)ZbEzzz||{ص2<<ޟ+gddz9sBo>bYB[[[{xx\re޼yfffcOrE$cر*,,<~8ݻӧOe)ǽRcEI餤菋 cTh###;ƼyѓSXXv /tp_;Ϟ==СC7߇rccC'x7o6m{bB ׯdI&? :M+{åR#@utY.JE*7*;#$$V-Zȴ!Cxxxxc":GkN"<Ȩ}ƚoS,?/_ <77W"X[[ B˼<}}}'''MV=Z[f+//633kp ׯ_r}zSE ,YB^>}݉@Gi:AUUUnn.aAQ|(J:/9{,}4!UV}@[THH{Kh~|}}}>fllleeeee9:3/fpCt<8@!:ypCt<8@!:ypCt<8@!:ypӛ;w$'':u޽{>>><1ud݉FFFtarrʕ+ tҴS[EEɓ'O>}E>F{>߶m[>N:uԝ;wu릧|۶m;|eAo^~… U6^t]v[ri4WW]6oɒm+..;wnMM #G ܴiҥK !C`ǖ/_>yd0 ==rݻ7eѣG?jt# IDAT+V[[ׯ_Ϛ5kZ`fϞ{nGYXX%!_O<駟Bsȑڵksss]\\f̘Q_>%%%=z(++۽{!CgL=9sf۶moݺebbݍ됷hO[:V(I}m@a%M- Krj.9Ko`αcRRRlmmٷٳitUV!!! ~%vExݺuر!8d˖-1c8;;pӧOCIAì6XC-[VVVD78fLNј1cv}ݵk.YD7wi m]D q9(mzzz*8@mݥO bD7oBڵkNmooרlٲk.vhĉgΜԩ[ਪJLL$=zˏWoɡ]z8v;^8"??~ t].[,''gҤIo~**!'̚5Kӌ">W_}G۷o7nLsxhԋհ6???o5WMZJt>!d߾}E>`ѢEM7{M 5JRBСCuk.ܺuk8LԹQ֭[RL5-[EGGY^ӫCرcM hD,lo3.9;;*IV^^[WW׾}{ U"H?^XXheeekkٳgB1'˫i׮]֭ml_,_9Dabbboo+Ps5:@*<>׷722Ru+**˳ oDw(HKy(L;;;+++V޽{Ϟ=z7z9J+T*-((xy۶mmllcXԢE %Tmjkk===U:?o"SdkkۦMF/\ɫsrrjkk]]] 4ocTQ2;;Ny=ic{{.]x{{ZJ>j_GjFӪK['O7og 6;K҄~]z=L:--mȑ^b5ӝ:ujpGgϖGի-[n۶<..n„ YRTT4w܇oV"0_̻5+]jj#d$;;{޼y_}{]~L*qe0!)+++W\i&vJ T$gIv۷/Z&xtN\\܍7<Dωs%P:zn.&G3PRRzꄄ{hB+{W9$NT5ٳtu__˗yI***ƏOP׮]wJ~!x<[233=<EM3_\SwcZS1%zɼy-mmm麺#F0;;_%#bŊek/_} j`֭FY#/LVh^ԍL)n]] _2طoĉO<}3g"x<ۉ}vڕk.55fooor-Qxx8A.\ѣ/KիUV 6__z~۲eˀ(F7n޼+h33,䊊 //__߸@<_IppKďy՞={]6:::!!ɩYYY}.]TS~窪*C!t%/d{ģqH֠ @`jjP6UΝ+1f͚e~駢"~;;;Prr2P\L.K"455ťMMgddܻwoÆ *** D'  @ġ\~Dt^GׯOHHHNN޺unTTTSSo'''{zz9_zE!E]W^^^ZZjkkH<6Oмī0ܘvqq{YwwwЋ/e!Ǿ;ܻ1nܸ矧O={v}}"J imm]lYLLLjjݻ"QQQn|>^WLԬ\竫n߾}ݻξKChP5G… Gr///aaaΝsqq |||Ν;GQ-r={ &OXwpp=x {ߟXÃb`ӧBH]]Ąixxxee%Bh֭@;ggF/// 444bccqkjjjjj Dal6ɓ@}cccMMMPx=+++] ōhH$ow<祑ϿuBISSSCCĄQZ#ƍsN>**jݺu߿VPݻwNBuYBjjjBCeƕ+Wƌݶm[}}ٳgkkk>}:fI4tзo8t:N4IӁp=((hʕxs΍35xk}̙s)F%DxCKц\.7##"PF,O^!+W^aÆMFQDɾ`ȑ#OҲ}˗ jkk%EN544z eee8ݖ-[FH=r޽{ CY!'# \'>|O?TXXxܹw,%N]999R Stj*A.͛7cǎusssrrxCtrРA!k׮YZZByyy\jB666^ ǎI4a„˗/9vID'EqSJ$ťMVI} 2Xv-b͛jiicёe0www<Ǐ)x!m6r#LCCoABJdĉMEx5@< ȷnH}|91b4x,/2ftēx/V^O=߭mٳgn]]g-Iy%E1Q̱͗#Hf2[`pu1qDYY[[oذׯ'C?qD˗/Eδ.ڲeHQH =qyʷ=z̟?!TTT$qb&))=@Xcǎݛi(SS]v1!{b 5}?>Oulllll,T-\0??x ь1.r"jhh(N=P7pZ|a~M%E5He`ћ7o&o777ܼy3tL1ixь5WڨQhN.&0FTRW20aFGz쩥Ԅ߸!377/,,߿?~A@ r"p\t.m;ݍQyr$SEEEx/555555ˆ{-,b|8lÇ .$Ds7/G@$gtēQOO/""" _utt,YvZ)c* 2))-:::::ZEEo>}CO. q d~!;02P(?zhAABKBOP` sss(Kx86pVVV۷͛LPhxь~8pĥ+GqҺֶTt xQK/Ь >.:{~|tz_4'(ҵ),#ʃtdM޵fGl6u;ܟMDSP)//gyxx̜9̙3qqqx|cmm#GΜ9|r޷o?|LKK{@ ̴x"t1=444z5S8\Fʂfү"DHA/;w;v կ_2FGnJ/QD7VK\ZZR[[0`n~x щvpEd>:ɞ_O%usۊk4S<KKKUUU>z3fLrr7>UTT&BF,--{^RR"'Op-FшǗ3T׭[nݺ̳gϦ477ӦMX$O^Μ9# KJJN>۬pEN !ߋo$"\c1l0܌x999{"$t:_MM ؖxpb#S>>,3TAw!sssw3X,M߿'m3B'$ܹPUUuD8\#q"u"sNI "$ x$c]$uPyԚ5kS]JpF_2++K0^Dǐggg? ]NsNljnu "pU&N?I/ѣ>_/޽{ԩge̯BE݅BK[(@333\B0|;;;bFh Da/_?RNNNDDy=zfϞ=O$;;ex"((!ԻwohvQ&EqPVV\B^2ZZZ8\r)Jb ?~!I҂nu\9TUU4GN8!L?sN ʯ<:44499ѡɫ.bT{Scz=L:Gc⮇o޼׿t EQ㋬ɓPP^ 7Ko2#|>loo)ѯt/gӦM7yπܥK$δI K( X{%KlreE _d\!wC3U.塵5&&Ν;& rWTB6m}63fhkk=z?iP,߰aáC󽼼V^mkk[^^~ݽ{ x%''ԸΝ; B`+V066#7nx]vq\|"NjllD2cOqjȘbii;-[2|𶶶?c!]]I&;^:dY2˗%%%`…{.++ {xx _BhժU(TWWodPx֭gϞO8BA=Wܜ%ĸUV5Ǐٓ~ү:6oޜy;wXXXL:gϞ2I$QM%!555(( $ IDAT߿ɓ'.]===ƎLŨYET[ZZfϞpݻwIdH\.L6~Æ +V6lÇ;wxep8}݁.\kĉ^_qBK g$%_AjpӦMɥjhh5kNFFƹsB2!bԜ r)ׯG9r{~C.]rsskii ?>sݱ[]v y%ԯ_kJFFF.^X =z}zJJ -HbAu)Xk׶Wyxi.C/,,$&$*--yl2(!f @eeeɽ:pz,E 8errCGGLKla4k'FǤCMMeeeĚ:::Ν#n(9EkojL/>}Z޽o***MP...;v:::rssK,ׯTQD<==_~y"˖-#jɒ%\.7>>?`&MzfdkYQ_H˥3mȒ/ {5矿{̙3gΜ!l2OOO_Y냐{~1jNus=T14;;:8/ 3_#lz%W3fLll,yiC=z4::_Y,VDDĺuȋ-iiigddL23"##r!={(#&!4~("srԩSBBB6ne:t S<1C; ZRRExBo߾SNMNN&7J#G^(BKoor4<==qlY@@իWE&,t<}gOΰl4dP#pu#;E|DA I&OE?lc/^L_~)#]ү/`77Ϟ={֭YYYt]HuNF75%%N*66v̙^<ԕ|ݺu_~hgglٲÇO"(166f޽}}}e Pwpp TSS/\$>9xeF8pMa]z70m0#i1U]zɉ8=z=_Cg ~~I$csB Yb:ZѼw7xٓ(z?X ]\D]]]EE!Cw_p8~PXYYYSSӿ{掎9NmQVV4d F{{{߾}{~RPKѣGwܩRPP &'ӧOWc8KfCC.`a4k'ǔNQQ~ĉLW]D?}<KrРA */MLLGEZXX rȔR+СCIII,+<ݴiSdd$BhԨQJѬ}kWė,U::::yyyO~kkЀrrrB .O?}2 |>1cnQQBR֎;].&ZÆ x"y*"=x`ҥ" bl6{۶m?Vd03c q*-Smhhpwwqa/l586m/L&|O\KKA~W!!!D (5UХKZ[[mii(2vtڵB}}}Kа?RܻbƏ?lذ^zQ_[[{1ŋMMM'mڴ nԩDⲃQ!;+.REM2߿333#mddLZJ'$$$%%5s±cΞ=kee(u!IOO\vNnUUÇB666%77!4y丸8eC1tiPE]piرD HMUUu͚5ȑ#8=a%ԅ!4`{{˅B!BM䍘6;>ڈ%"kkk+**LMM555)vkll,//000=O%ꪪ*###477WTT 0d/^C ѣno޼p8zzz]S% +++?3C/..VUU o_6EC9NAu@ r:::4F3XgNMMMEEŠA8N-ݝP^8`oo?b'''3g3o<GGǩS͚5+33f(iii...SL744?~VV, 9rԨQÇ;vommyyy ./ۉtG@ OS+"/>|r_̛7oΝ{fh("WfϞ-n޼)QNNNii@ ={wPBB@ ҲC:ann޻wo@wKII399=##C tttL8F_kJ[CHDmڴiֆcǎ?8;bLxvP\8UUU3g<{7oBrݻw_~IU8+> 2up۷oĉ'Oξ}6m߾اۻAMMm׮]iiivRSSknnMδ SSӄׯ_憆lG^Q@?ݻ7==ɓR^^^ ***qqqxK.9???993666///&&fĈW^%~ժU믯^omٲe"/dD3 o޼233ʒ+{Xݹs!ₓٳ8S^xq :(///-- MLLbT6GC9&b֬Y}A%$$|wu///EFp\,~sٹKsFFF mllx<^FFڵk>{}Bƍxh!~]?(.,}Cb\.7##f̙s)5BH?Aŕ7z{n}}}~nܸrppOd…{~:yDz΢E:.>}7|m6r/7|r={VSSClՍn!jFpX[[l?{"B<gѯ_?<쟬o߾GQaccӫWÇ'''/[qС"3 0aȞ?"Əxyn^zfɂjkk{9>A% 9"thVkk˗/_zr1 mmmy .ѐ">G(I4acc҄bݻ9s(ZhQzzgϊqjjj,7eßb ‡.\خ-H$YdFLP(?zhAAo萇]`L !hHoN&ڷo-~yDnnn~~~ׯ_裡PL4;O;8B!'''LAA@>t+_]֙KOTPP ߈_8ʤh[[o\8YqOL]"SG^@̙3Ϝ9_=rș3g/_4 ZRDLđH:iڹscjjjZZZ+++믿BH3Rĉ{"B{9f o,Z(// u~ ߣ2e;>}Nj޽{_{NGGgرl6ҲΝ; .tJǓfSCrԷo?|LKK{@ ̴xb}2Ί |aii󽽽oQWW_nݺu333Ϟ=O6АQ@VVV[MM۷o @=zJHsss@ $-&qLVii)VӋP0e#ĨlЏ9f͚.cc &ܿ?>>~ѢEx b~.yyy<}DGGS:aҤI=dOTrvv.,,LIIz-BhڴiAwsJg̘1ɪ7nܐ8YYY9sF($%%>}۷YYYAAA;+.ddK={u'~~w\;!0GSSөSN:gPSSãsrrďClbz"ꦦYL YWTtXqk֬#̩lЏ"rPqVWWOY`As`'O600@]~=))I GdaTL넞={QOU شig}&> IDAT :t7nXpajjgϨkXlYqqqGGGKKKbbݻB&MbΎ;Bo߾]`ATTԳg~FL:qA!v?ܻwɓ-jllD[100pΝ?>|_~)!!B /6oޜŅE6yd yIzz:i{.JKK5\;;(tTtӫW={mmmLaP~~Wbb" _~g$MC ۶m[TTTSSP(,--]fMii)BÃU8T\O<2e̙3i3 b@^tͭ%((LJϩ<<<Μ9ࠦގ_WWW !F~򨨨>̑sihh> ۛq 255\x@ 8z(75>食ٳgGDDBŐ!C O, ?ޓ' lW3fLllĉɻ :ѣԿ%%%}!' ;udOOOΌڳg?y>/?ƏOʕ+SN%QHHƍO_bE>1cFdd$1!gϞ`CH o⻌pQQQDR/"IFa _,,,bccgΜ:)oԑ!_6ECFԩSl=z#//p k)NNN8I&Ӊ-,Ԏ9zj]'HdX֭#rwwȘ2e u$38GOOd;;;<f\z_"XuXt9sY,֒%Kw«t UU!C/XZZڧO$*++kkk9Nr@p«P(߿?1]Aass555)a4htAS_6GCA9&1ѣG766Κ5맟~RP(X'B󛛛;::e<ׯTPQVV4d e:Xq555Q[;8~HUJDsp>N\.7??Ν;xzQ;;;(/͛}6> JCCCGGGGGCsp@Q- J:8J:8J:8J:8J:8J:8J:8J:8J:8J:8J:8J:8JOϏw nܸ#+++hɝ@ 8w\nn.XPPg.;f̘=555]~=111%%fKql6=p@P:ݐ5onnn">KҥK=5j =Ƚ` rx7|'._lffo޼Y\\37_h +//AOO߿O#~+BӦMrJGرcDZc_):é".]JgoKKO&wk׮jci&ܻѿSv1ZYY?;w.E,Cjkk;Zx}p9bl¨'Zpp%Kϟgus'c1N@?$1=ДXXXHðaäP׈̓g:f)Sx<Bzzzfff位9~ uرgZYYFZZZrssB'O{$kh:|0BF= 77ŋ7jii#L##;ә9seaa?o{ȑttY3i*)-ɊvZGҽ}kDrsuu ,++[jU7i@7SE]piرD{9:R*// !77OoBBpРAڵkԩ Zϟ_XXk.xiOrKKk׮}XRUU]f͇@<VTTjjjRX^^a``4@PYYruttꫫ߼ypԥ把6==ЙňΉ9>3?5@ɓ޽{Q*Aq8s=xdܹs4Qhii)//CUSScּyvq֭e˖1ܳp\.;l0߼y#FtyPXVV}jddTF5$ >?dȐ=z0 4ۋUUU-,,.Ź@LA"OYfi+ +++;)nd 1{DgM5:\gQ]]]UUշo_}}^zr4 2Dҹ%V{w{{#F899̜9H|y晘8::N:l֬Y4CVVV...Æ ۱c911yQQQ{q8Ç9NRRB(??p8ɓ'p;99?ԩS"G_sɓ''M4rHWW_L?22L2tRӧ;;,SC={lѢEfffփ>pѓB&cvI$k͛7x744{޽}ϟ?'ߟ̞=EDD :qذa5OU\\\Lbooohh8,:YCܼf &V\[bO:5d$---633>}ŋϨˋѹӹ"pdOo)F(f_Zo :tȑ#-,,,,,VXw[x1?9W.ɷҼk+͌H566fff .CgtTYOYtmϴKZ9rQ'L`bbr.'\b122jnnH XZZr8b 6NM6p@w!CFҥK_xq'F} œ3f_+..^`c҃-Z8jԨ.ڴiȐcǎq'N\|{aqO䪡h޼y"'RZZe˖o]WvyO\${"hBj…9SZZzghD bBamm޽{srr{% y͛7ǎ+{<PFFƢEY}}}_~ws:q=y$--ECCxɾlذaMMMw޿? OԿb23:H6%%ԩSYӧOՍ1"((|l_Ȁ(֭oYNK4>)ܱcN\BgLL#u|h֐8ZZZV\1a|>?!!ѣG'~2Cyfܹ'ѹ;KIq(.+p""ѿ!_&W;w7rׯ 8<'gA|*m -qrOPeeĉmll{DM5:QȢk{^:ղx iooOHHͽy&s̹~zKKIx~(pyX"==" oݺ%qiye/iǾ}&Nx۷oϜ9!oNS]]Рk׮]v577{{{y:p|:88\pѣG/^Ľqqq{%|ժU믯^omٲe7o!33,˫AEE7...55500z|%c^zgϞk׮NHHprr,t_|yVVVtt4w߾}8K.԰?)))''СCC]tÇngt~駢"~;;;Prr2n̝;Wb̚58nmⳓFţfʕ|>_]]=,,,---55u B{ ƍx^2o*;HmOeKѣߟq<𭼼 ƕh֬Yx=UU#jEj;v{S\]]kkk ʫ5(}OL#8lll._4KKA"TTT]fii>|8~O !'~"ZZZW\[XXK._gΜ>ӧF꣟2ko||||[BMMM$̝;722sCCCsssvZU!]2Ι3ԩSt~s3:./~?@\RI)VD oػw:_744tuur !ۺDr/]޵.]dBYSS֖.j+:u8PRR|rN8Qdc4۽ruu5:j``p]]]UUUsέ_^{Lq  ;ի'O" tUwJJri x_$kגijy!ZZZ É#Q3bx~ǏSC1F2fpٳgx]]xktq\Pg!I%a}ֱW@B .TQQA<6Ԩ1>Pгg8qv===R3[~VVVw366J|s588XdIE WSO"2 ի)f0eTC֯_O~/_9}q˖-4i^ҝD8GeNb ,]C{ $=z?1΃9vvy%#,((uVbbbL>MM5:\LCCï(++߿k{^ 4BCC)𘝶~x}-[{m\06)>=\Bc'Ckqof655!x<^qq1B_~"s"HՁDDUWW'rbǏ7 Ghcc#넝D'"q?=z$2/xo|/GAX,}}/_DP988ǦQLr.KUT?<uNiAeEHmJ0LÛ}>Fc)i%QFQdmF%VI˩(%۩q=s:g+Ͻ\}]}_u_WYUUU]]]]]]}}=> 12|=8>ww[nY{i4ڱcǺb\'EEEUUFuL__d RTT466NNN@goU)cff믿&%%yzzZ[[RWz I,WRR~'0+=^3?+**7pL6MKKbݼy̿csssTUUYKV!Q}Pxׯ_~]NON'Z[ ˗x!$l|x`UUUHjkk[SRR"g\n\ ̑>{EJWWW''gǎ;{W_\#QjHidX{>N(˨JgϞ'N444TUU\RRbu^$Ɉ>]]#D(R1x'|DU#|5{K^rC,jŮjBR9Pm|GGǒ%KI%E2iҤ$yy[n oחp8C 344ׯ_{{{aa!EEE~RE ٳg\naa;w~?fff9r;QN;tYf֭IoO>?w ^`^BEq.ZZZ,>{EMee7nܸ.==ojjڱcǬYz )-=v)ɓw$GP⮗ aeTZreXZ]]=<<ܜc"88X,mmm +~''A dbTtpdzF/M<9))=GkiiM6ӧ7otuuߧ?{nhdd-k 1=t|9@Ul6;;;[:::!k\RS(((^wN,$O&3*4M__ӧx쮿5V )Gé7t={;fee Ay}IIIx~??#nJ~/.KOOYֆ{ ܤ^Sf111۶mCt2Rl,avˮJ~%v+##I)ԩSe}YI+-V!tx2:\;+=Z&/n/~Cc޼yJJJ--->|h o ^ؘb%&&|+'UUUO^Q2ضmÇKKKݗ/_>}o޽{722!dhhbŊn2wܸ!dii'!HV;vϗXbڴiEEEqqq4b4mڴ)>>!t1Ys QF]|ť9 !|BF~駒k׮.|ajtRsq#//h*b͝;7,,QǎSUׇ]dI;BG]GM$g(&̙3yh4__ 6(((7[VVVnnnvvvܾ߯};B(<<?#̙̔bHٷlٲx|f̘~£ގ{pz^x?gϞ?Ǐ;v찴d0"EZk.r(!WK쥂zʸ={6;;;55JAAZQVV EUW5LQ.R܅^0ϟ?X߉C]i⫢z'OlٲX.''7|lll\]]KKK9Cş\֗uٝRjD!tHKKcX'OX} ^QEJXk ib7pJmV˻vtҳgx&%qqqoJV ,,,LOO# 55yEDD}t!i%Ri":eeeAR ohx_dx9N؄Ibbb3c?~<22#4-,,lƍ{TUU͛fccC^yz ƿ!4us%ի䅣G ܺuk-]EM`:`8wxH Z999 ͟??))BYY!ftq|}}xƠ:vڵk͛7b_}ƍw1;š5kd0΃D-|||BD555|S[[S&Sr#""֭[Gmʔ)/_&'g/Ūz(((ܹsgǎr MJJ? !QjnK:ʐz2 9hWvHhQŋfW^pĄQEE駟ț)d"<-"]|'_500qrrBrGO8tR3gμ~:\udt _g$Wj܌?6>%H!iB^t'ãیJ^ۋwߋVˇ6lD^^%.. 666x*= OE_/""ݝi4ݻwG^YZw"*6mRTTd0B&hI>3ueeecǎ%_u(x]mm-^q򪪪!C>]bmAmhh(--p8Çg0RV5H,))ill;v,v+MMM=砭mƌ,GUUUM<5""oWз}wa0NI뢣7nܨR\\L \#|>7GVUUvpjkk/i}i8"8i{]QQBʪ |\]]>}zE8A8A8AHl6_~2:4Ǐ}||Z[[h"++cǎ߿!TQQƍZҲ!`ff&0TWWEDD|7JJJϟ?b&&&VRRRl6{ƌo߾ݿ%GCCm֭7o\!4k֬WmnݺSXXbjjjjjbŊÇK(pύ|2z3ghkkϚ5ʕ+ q쒓 lmmGsΖ.ehh8qD;;;mmmgggZD fꚚZ[[?B%yd0|ɓhD7$67spx 55Ç3gĸRSS8d)6dG=8Hvww[nvi4ڱcx!UUU$培g8EEEUUF'L__d |@YhllߤI~]tӧOC )...--UTTΞ=Tggg.8d} iֆ.)cI?~b@l8 ;::,YB6ړ7nܸqƺ߿iǎf">b޽ohjjjiih_,_b$>}DI%{!tU9s:t?iYf֭D9fEPRR߉}yH嬳LKKkkkX:88 lllX,ׯqǬYd;zIGA}MФIoݺCM bp놺zxx9!,p24y䤤:Gt%&yįI!4k,<=?UeGFl=|82YdDvKQQ*3[1FNMy*zl8nӯ^J^^__faaA/]{qpp7oqļ'FydMM͖-[rrrC&]vUTT\tٳg|UG4D-|||p4ɍjjj/7nܽ{V֬YzgGDD[SL|%<:P c 'Cy9Q(ȁDF=͉oĉ'=kh7gRUU7o^ZZscbb3 0`C )wqq bbb_bN?b B˳Æ #˻őnU5 hѢccc&쭥13Y eGiLJK~8b8A8A8A>>T^hձcߏqx Xv-^hiiYXXJHH033ŋK,bGCCm֭R OFmڴIWWd5uuu| 9`BxyyM8QꁑhJfϘ1۷_~=ϯQQQ7o1b=` ӓ HpppiiA''''pG! Գ{FF<B/_niis?#]d2544V\A7n2dmeCJJJЂ 4pO8Ztuꬬ~"`BhJNAA˖- vssꫯȿ.]4""ٳg{Aܾ}d"TUUd2qsξ!IU܇/dĿ7}RWW7nyѣGpdĉϟ711!7 ܸqΝ;Ɵ9''!diiK+M_"k(_Bfff/999.]BmݺWCCáC s@x4%dhhd2݋Et:}˖-˗/x*0!!!g611Ȋ$Uqҧox޿(Fmmm?+FFFP"\\\ʇB4kN\j!TUUm{{;!._W7^PPpBX]v566nٲ-<::|Uۏnpww/--%455}:+++11 !fu>|dɒz}۷OAAiɒ%~Er\YYYEEE]t 7/Ěo߾]z5ák֬믿v5l0! 1nܸL񸬬ã^NNn>T|2>7ly {{N}}fffdd$СC8}xQ㏊;w޹s';;ѣF]|^ ŋ h4ڦM nܸaaaJJJ ,2s!tttp$6pǏo޼@qf:uJSS3<<ѣG~~~ :4,,,---447%TTTƒ7n4‡fD ¨Q F[A~7Ҳm6Ǐxcc۷v1&&&77CzJm ͽvرcRRR<<<B }]]]]W2Ea+^;}bV^-F Zミu[nu<ԃ3Gptttnܸݻ :fɳˮzZjUGGrpppJJÇF 3M/DMʓ8 #Q33+Wڑօ?& .++ptt 555***"""55ʃ",--BVVV9997o\ܹ9t̙~މPQQA)++3#G=ƍBaΝ<?&&_ 8F]]'F O&zkii 8ˋ>~&@ ۽{BSLٳg^2ssmܸÇ3FxTUU!P{A}$aĈCfի&M޽bPSS7449rBrrr׮]344DM0L)#vǏC2neef͚SN=|0&&㧟~QPP8q&^MMѣxQF9;;WVVCTUU^H t˗_~}ٮ2E!3>% xID1;~/ O{2߻wɓ'7odW;CHHBHSSիx &8;;_xdFDDlڴJF@__ȑO==;w"ڈ>qԫ8)6$$n|||oND@jĉD;L__?<<|C;¼}ZnBh~---o޼_tvvvvv~d%t[ ;0aٳg-ֲؿ Kb-//sNkkkddddd-[hсSRHl۶ OB~AF1ǻU7`A~T)9ܻu1-ʴ8aĉ޸7yn\n||)N=(Ic***q^ %%% =\(#2EIe[HۢEݻwǏ#TUUKLLdXT2b"]lll8NzzܹsqO:KKKeee{{8;; A?FET2-Uf||@#κnG<)d o$y'`zXXoee%Ľxlٲ 6d]=:t(!!ǏT_;I*O>g5ptxɩW_uOt:]ps$ԉ:ilڴ6lP=;#qS"5"\훜|-z!wwha~)yA@4vsP.0¤V*#[l(d#G:~8^QRR}iӦ!aF<1KO>144y@X8 [[[MM͆ɓ'c3[zyվXkkkGQPx9ZZZYYYتrN*){{(\uܹ={~:999((h֭L`hhwoHN34^ad5:NJϿx"1iөN:EDD888I6m$vo߾ˣG SPN*`MG7U?;w;wNf͚5~xe&L J͛W_Q '>>#*ae^[rrrbbb]]NI2dD"}vp :T/b;R2_ů_Gl浲H$RT-ya8XcD܅΂ ,XP^^o߾+V :s΂_"sYd 6"LLL+C]ñp^IFD[[猠$Ib~JwU]RGGi۷Yݗb% ^!!;>>b 0~!&O>8GχȼVZZZ8'3vZdd}(UO]XYcDE!zzzF[_`͚5Ԟ))) g@ g< gq;F>^QQ1aGGG2{i0GutttttěơwK޹s>T"ܐ"Ӡ .]9>R4@S]K%rC$''/#ltSػw/=555Gr :DB/zruu5$(# 1c Bh֭D^4b .jwܡO?G>|rtW^~S+,,vE#2#nR`[n"b8F)fP???@'t[ʬBJ%vo`ZbX}JE%;;;$$!ԪUnݺ!&O>8eŊ7ߪUVZ{#^ĪV8Siff&~)SVZuQ0jE!k֬qww7n%.qP+W(Y_RXXXdeed2///]]ݼ\9z،pvv611!n]\\.^'O"999qX,MNN|e˦O.H޽ ffggwG);p¸8)$+2NjA5t<##~-[ <'!!A" <FDDUTT\z5((HaP)--9s_xq̙?!ԵkYf)O"͞=;00gϞ.]ڹs'$s=bPX&p1|pw5bzg322Zt-[󽼼O>pϟ?>::!dkkwq)+%BO>!4{s粺B[ʬ2ɓx>a{VFLcΝFFF7oܽ{7~9 X8k׾~zĉ3fpvv믿:l{Vݶm[EEѣ ߏs*}üs/sݻw|uBNNN02CMOO///_hь3u떖cǎׯ7YnBP6ST1CpGΝ>rI PL"8//Pv HII!OqѣwJKKU u\*H6mkw͗.]ѣmΞ={Ν/&.,< _j*mVפDqnժUUV:t(--!8o|Dedd3KKK7Rf+W@T0l0:::ESTiӧ8@ &ٚ ݻ7++/,BO?ݹs'U/V111_|EiiiTTTTT>>yU. ?~|TTݻwoܸ,Hq聎Npp0T~8`ooO$l۶m@@ի_;)5s̘1"hʔ) e.\mdd\| :8N:rrrb baaa89fxrxx8.hllL9r$K]Gijj߿޼yD-Bo߾&/.Ss>:%wߒS4ɓXe>nW)ŶlKN9vX52JZ"нMT`^eWP+ ~#":({v ...111`tuu}||(8̆ V\IΗ駟~嗗/_߿?v)U,--ccc FիWhhhhh(ٓA<̫}ڶmΝ;\p$*YYYQPxN,S>͚5+..K.ǏW;Qu|֦(qJ"S\\\PPбcGAҥK8бcG>77AUm:[UUչs&e#5IqL&+((())111a+.0y^'Ot钳3&e6aT_}Cň.(**Az{{#JJJ^zժU+SSS#z]e.XܤS XUm(۷m۶555U]R X__sg<ghV999O:B!\3S[[v| IDAT1cPܹ.!!US˅2e-8o߾yyyk֬a2 #m۶1LAAAtFNN^MqƏĻ-|DZcdȑǏ?u]Dfzʕ+:ulٷo_M矫ǏrCCCrwQ={*4jofN}Ԟ L6m~xAKTh88h88h88h88h88h88h88h88h88h88h88h88h&==̙3߷DBTLtttJJX,τY8ICCî]=jiiپ}{!nQSSܻw{D[+W466vԉYEs<[kӲwPUOMM=s̙3gRRRﯡ"BHL]7]6l(**ݻV[TVV>}ٳ bԴn*-0\pѣGzD]F l;zE-~z嵵LJO8y֭Cjjj \Cdll\WWd???şfشiS~~?ӧg̘:tcDŽCCC._,Bw聯466ׯ_~g<Лϟ.Z>tuuYZ:cZtZ5L|$N#5p;xବ,Ѕ  ^Ֆ!>|+Wᔚ=𑣉2dHuu5qH*޾}!ԩS'KKKr.]4s~ !4`&F ˛={6:uܹs={T'J8p@^z}ﺺϟ~lӦM-ݤn>gbŋO1uTF> [T#2ޟp lj'|jUcǎ}۫/М_]]>>[3dW^^{_Yx'jm@#>A(@8U۲ezٜ Ǝ۲G0>P!,E=KJJ߿_YY۷o333r쌌 Td{bl6 SXXBsd/^644p4{322.wpyD"/߼y^U@UUU.VSS^WWL3 _Yը]ryaa{ohhHOO'֦|E233={V__ϪtMö7ZC=Rd\Pe& UUU/..f >}d䩉*Ar<;;MZNQ!CMpرڣhϞ=ׯ_1b!C Թs &$''gddt9ÇqL>koo*7B֭[LYO9rd׮]޽{={spKAAAZZ۳ aҴu֙r|ĉfff]v}9>X^^heeeggnccccc3cƌ7oސrJ###ӧ[YY988>wuյwvvv!!!+{FF/D_ZG!M sHA.OV" ɽ(ֆ}7|ciiү_?33-[' D3޽{^^^fff 6lu###)ň]UU5o<?ՙ3g~~ec+ÊCQQ̙3{1h '''ssŋs`)} Ps5vc7rss'Olii2&((^r?9r䈳k.]V\YSS#}B(P6G~Of^Qy[n%Ǵw>ƍ5=8qbEEwoΞ=۳gOMLLo(J>|ŋ .ryIIƍ޽ $`Xpd2Ց#Gbccb%:___j;vڵKa[X؈eN[ŋz[9Gzpd/**k˪J"HKKe=_vm̞=ۛ|iӦݹs,/&%%?^&Q2v.\ӧt6mѢE'Nxņ <<<+W ,YW566Κ5 $(**:}tFF;wT]]=c b;Bٳg&L8zϫWㅅ7odWG~޽Prϩ +// U-(jan)ŋ:t!4q׳mz^kb% 6^ۤfBff_MїUVUVVѽgΜOohh8uoݺE@ʼb|P\\陝MJ/_<|0y;UQ 5Jt=UnNW^}W% |$!!!22hWCCÞ={zDJ)))Ǐ?}J$>|xƍ3g_'^b#J===_z5qijgϞ;w~>;w>rիW׭[]0{U!(Dp\d\s//p"^-փqC\ܹswݶmB$uPhŋI&UTThhh?~իd_Ô<|0>>‘#Gzz9ϱCŸ!:::;;{̘1{~͛mmmB/_򱭏C07ܼy׮]Svwލ):88={Vov211v횿Np֯_o``}녅ǏWxc83m4:u*MX eB7ԑׯ|2q<33/s ~EG9a@@~嶵ݼyk"##GURR,\P=z4wF-- :u*>>~Ն{UFP}X󼋌~?_t)66vΜ9bY >P)455q %PP7nV--G =zu!33h,rBM6xBSSs#FMLLf͚UXX~V!=f|NWWwʕr_|>#f|dРA{]`A]]jݺ9LNUTay&!_3&&k smذ!4x#Ghkk#Ǐh"|\ܼy`ҥˬY.^lӦ ^׶m[]\6+++w… e֭[;v !?۷Ǐ177?vu6nܸ(a5-VVV_...p 9;;ϙ3g׮]W^4iիKKK%Ɏ;?ziiaڷom6bΝGTTTHٳqG0r|;vD?ѣYYYg9sQ vvvnnn#F6m´stQ Br|ҥxF2iҤnz0#bhubݻǎkjjڦMYf7osB̈́[ ,q۷oW\9i$J*""wΝ;[[[0@*&&&Ο?9r79=z߾}9666_xx9s/1s ,S1QxݘΘ1c"##|dE?[gkk۱cGƉ'{8*!t=Q-13e۴iӦM&Mq2`1 lSGBرcAA[y~ѩIHH vTs--- ?{nPǎccc'"[[qM<9!!|=z4eeg:2D__ƍ%%%[lٶm>̙̕3%%%BđH$NNN!ܟB̿|?>Zb_&rqŅӘq |Hăߓ'Oon111qqqnD8)#sѥK[駟B jja(Źs)[!;t蠬̮]VŋO0K`Xu 1aB={ q}֭[w#vءK&nϤWfU%VRLcǎիW[[[eBBBbbbvڅ-- & 233!sD"b~'ycvvZ=z\!TWW, a8p\lٲ?!7ߐl[&z$ѣ1WÌ7 rB_~ ciiЫׅ 17![UڶmKa O[DwVɨ&lYjڽ!www;ϸqd{70k [?`Io#+p 3;wTUU8}-p##""3&}/7U  _~-R&F-؊Z-EQB}}}Rכs5a)ѣG8" g=X*RѣG!==={Pvrŋx 9;;gggWTTgĉcƌqpp[Iᐡb%:"R 3' ( h۶q+]0˗/DZ|)e=Ж^HdllóܖpY'ckժ$ZslV1IBJa}}=z0OَSъLr#TTTd:<+abjsb+j̻/뗑U__G}X)qݳoݺRv;vb?B=/_bS|rJICv cKs` 53i>ŕU}FjՊ#ә찥BԨAL7ӽX@m'у.uY&z$R8Pla233X!Խ{w,--ڷoOwwlV1I oaddԾ}R,jQ!@EnjDzk 3C-[:ڐ!C444d2YRR^0x`aÆEGG_|yԨQ8b1X-Ĝ}`;F +G[l۶W!)F!"aSd؊Z-.X*>~֖U 777@566^z!4l03==Kpvv".$WXEV#FS'zzzx%v8p --~zii)6(IDATIII 8t萠Jt_XL #C|1P/Ǐ9rdTTR۷GEEN6E!TVVLBϟ?wtt?x?H6Ǭs\l=ǭ0?KY ycc#}5k[J$ccc}}}==<ꂘ+3JtMá͆Ri```Ϟ=8'R:=ϱUQNK>ޜQSռ>sZJ`H"B۷orr[<<#VSSaعΟ_9s,Ypd2;GEE򬬬sٳAAA[nUK+;>>^SS̙3}Q΂ ,XP^^o߾+V :TaᛙM>}Ϟ=^ڱcǒ%KSzu̙W^a)Nݞm]BT%999oT:/ߨssswSNΑ6mh 4Ri]S:v8qDL6k֬xrtsZwkZZZYYYxbz|V*FGy!3)6g}TD5*'0t4O/ 477ĺ:I!4dD3.7tPvvv񅅅eeeFOPu놗k)9Zq5yWFF}iō8%2ǧcǎ+B(---44(@\j޼yw!A Drr2}9LJJ CY{κС[=믿vqqKKKMF$"^|nsW"T"*KVcpA_M(6|!9%Rj19C^Ahtffkkk-,,ޙ89BBBv|4Z9!1C LAyy9, oryLLL3ׇ ъ9ϻO\j=>V)n"4\zoԵkWJƟa'))رcx# "4Cp VCC٬!O>8p|\__9r"Arr2j-#ogE=zёyXfq(W"}:y8{ =XbjO>%VVVǙwV`򮥯^>}:=An{9rȕ+W(qn # T4PXX֑3f`֭[%Ow)ӧ]Tq_jժakB SgddjxK-V%֗z)^Vmhcp[Jׯ_XMJpCВiݢLbf G5-}q.sb⒎@sB⣗/!$͛G_ UAA?@dvC(LߜX m6ա8V)p\SSSb11`}}]v!OǀKTB.]***_|9|ۧco… Y]pС-1cFnvquJI ,L奫ST?(lbbsZXX8;;:b[EgllhѢm۶=|pҤIsuppϿvƍe2} ?zҒgkk޽{mm˗׭[244;3cLLLB2lٲe3f055%z2|2ٳgJ̻+:t{S644xzzMJJJ nx KII!-\0..!}vb`PV!t}<у!4{lt,Yrҥka}ILL|-B($$[Acpsٙ3g~WVVV޽{F , FnZ2O7n)(hNQ{ՋTO {.N|xرAAAT_| 2eJLLLQQ8䔓C^$͟?f˗/H$+ :::nnn EEE˖-CEDD{_a[Dz꼼'Nܾ}[e&9usssMֽׯ_O98~w޸qY"c7Npp0Y9=<uԩS8G!Xhɼ?ݸLbf G5U/R=u aÆ_GGgРAq+++SSSl~$SnNt}ԥ}ƍۿ?+sC"8p=//$66700ؿ?^]sb.j.ٳCBB֮]Kܻw/9a]tC?|p,,HG/pbx2X,!~Bm޽cbbȻ khhXYYGGG79K￝-$ɜ9s<[{zz;:: RX,󋍍}sMAVXp sщD s7.11(>e Dܹs+VKR\ޮ];WWxOOOw^-=!Kʂ4"@c5f=T\,Ԃ ;..~S(1V^Ynjcmm-LBvX,\P[[Ȉ%^Ya~prrRO곜#gf]`^ [#lllbbbF6Q`팋KLL 99Bufk*öS*ߥK={hhh466Ι3|lJ+D5YKeÉ ;vՕ rʶb \ӣG&-HL{nJ VxjhnP8ԙ.XETHd\Y@~& 0`QKo#vuE>Et0S9xOH4bĈLK^ jb``pƍ׏;WNNN&VyP03v`V3"jbkkKTj޽\"JA9l0|'''j/  "B/^hjjZXXP q@yWkll˫hݺ UUU \_E˲2##%۷m۶m2MUUU]]]6m իWIIɬY958ê{;{&Nu+++qOb1=L/c0DMYըnZ}}=^Ml$ y-...((رcp!*))yUVLMMYu(DÛz𹗺.:L<ҥKά"޾}/ Ը ϱUill|Y}}eƧ>Jw 1Qմ^&ܜ"Ԕ dffȻvb (|F+V9”~g**OT),|4TxbT]]w]CII ׯ?dȐ3f]B!\wigg@>Awp[훗f9@l۶ܹs"h֭mV^!233zgBL2޾d.T Zn}޽={ݾ}͛7qqqrP(ƍ gS]]`y޽{ӦM;ydnnT*}oDDBgϞ> x{%իW۵ksԩS߼yC?%g֛'N1b֭[ٞ 'iiiÇ744|'A@aVMvСC`JqqիW ڵkggg7|xIB&L۷ﻮxާOw]n޼9''!$-,,֭[GO0ּ~ŋ۷o|fT3:w{ B{3P]IENDB`././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/docs/source/traitsui_user_manual/images/CheckListEditor_demo.png0000644000175100001730000007373600000000000030673 0ustar00runnerdocker00000000000000PNG  IHDR pHYs   IDATxw\s a#KqwWjj[[Zm=֍XZ]UATP *O8 `de'O': DQRBN 5S4pP(8슘8eeW$>\z3m5ʮ2mWvK! W3 !DP[XXXYYYXXTvL FC)z_?kXjbkڷo_U@/W||<WvEPy?}ɓ'iiiFa`4W7]wqV*B؏?io8sssWWWWWׄPdggWٕ2A.GW]Ҍ;D+)J*a4BՅT*0: ba*"R 5sM("U{ܞ>}a`(NT+A,@{aTi<U3P5""Ε0. @ا5AN)6"€.yDGPBՉÇAp0\0JiFHHl LEfІx[eoumEB/‚yQ+"ED(h<(hA֜]%:"!(΍@`0\0H hdU[P_ Cu!Rp @ѳQC ѩ*ejJҟܹsu A DĢ-״OZRN فS*2J4Sg}7hT2 yIna됱 wƍ?ٳgn^ӕÿ|r4hB˙_~EV׬Y/Eeqh.srw}nl~ j")&JTJVW'NR)S7ψ*ѱcfϞuԩ"Beq@J}O e )(tdUXd%^S|'.]-Z-$$d޽999˗/WTFzYύ*PNNWA0\0ֺVY0P` 1S@z6eQoWBBUi]`E:U2:6|F3b={ZJT@~|qyXhذZJ!T~q""eCxtCewJ܆pjxoU t$9E ֤~MV(bF"n !9{+tDlJUNPہ0B}DJJx=vuvg̙,V`ڴiӽ{wHKK+_(&%%̜V#SSSܹVuܾ}4/*سgg\Ҵ/DEE<*^. R^$퇠RNw8s^O`c% /*ᶡ ّ{qe=)p([%G%}}9kEx^9YHARq%p0}WׄC>zv5' !\:J):![ ױ&-ݦA k+0s\+O*,p>ixS$оL*;FUfMv9F=zRc''2+/sNvE>pwwoӦ7|ࠓ944t.\"F1bD O1a„?00PI&>}uAAA,%==saÆ͛7GFFB2dԩSݻzꔔ033kҤŋ֭_BVV֖-[n߾ӻN>O>eEa…, |Ap|CCC ۶mkӦ>6mԡC0y޽E6m.^^Bի׬Y)@/ QLS gB78yeL~ leWZ1WB7hF:E~rLIz7+RI%]+|Ud/P`= ^od`Ef!>=,G%8Yvl@uJi[]IdL1-/e__P\\Bׯ_bΝoS} 8~QQQK,ݱcѣX["._<Ͽ ijJ]y!T^MJoo903lK‡퉋 tkOig-D @ )]| p?o kBd— .H[b ޠNhák<pwXQksx)1WBGL.R‰:OmOid2=3S@2. )X"ς ̜93//2cƌ2mzjY-[j֬ }2e_u5ּ~$ꫯƌJhԨQ׮]?y 0QƖ-[ԩ>>>8ۺu4hРCzP,--7oܴiSW_}k׮K/wٲe췖[oV^Zp!;;;##E^i8NPT_]Ji^^F8UB\3yRB0;6?L9Z"p76[  \6t^rL۪!Y~vOmǵHU,Q7E"ۺP讫T[t| ;N߅xؓ;T^ŽT3<^Þ/tI*;RԸ?|A֭˴?sd֬Yu֥J۷o@-s Sn/r޼y999\0 cƌ3wsst+ZOyFT*---_d ҜQ !U^F8Am,uL{۶.Մ aꈔmXX8#iQm/B @HhnK9AO)L=vqTErz7O-‹WPXN*tCY.ĦURAOkdr:(XA*!Ile3&fXtkR[v^;x;v={9s:t秓~ҤIҟj޽{мys[ht\vvvvΝ;*&FfggCu,,,SV-x(&RJ)!;;ں' (!aD (?h!_e(5S$(^^(|T(Z8iF:i?Esڜ>DK:HɄ9<=XHFz /P"]MEj,u_‹D^傚׭mFNMx+^]vY7nvZJ8sLk׎a?(]\\K~{Ql_РA0ݾ}{e~%jpttI1Ff<PF f9U{^\++Spy  @?y` 2u(:R`w{L\HyYZ[ ij)MGF/Ģ :/VwF**JѨ(K Pz;e餪 _(5 쬭V.muC` DD*dKe yA;PHuoK,ToݪyʓUȣ6t6%94p1kK |MimӂUDZ^SgJEGz TwS"ėsiI&<"K5k֌ݹvC;vرcGXXT @HH~9RT;;;`4͝;w]T ;OqP…ҴTB64Ug]:D"P" Dv1xw=}bnbRm !q ]aR"nهoM ~}?'6E7qG%Qɐդȳ8XE.tYCJ']a^MY+P"GrLa5(22ryyyNNN۶m,/___ϛ7O~ϟ?ӧOY ;UlݺU3&&fʕ`eeU^bqs6ڶm| %d\v|,--J3Lp&)X{;@X ]< ǦãtB޲%2!,t=]|lpxCօamW 9'eunG]WCo6!$¯W/-r'ZՁkI O Psض6K)t(tJ,y%HK8g|EeC_BD\\ѣ}ݰ0PۻQF,qʔ)˖-KKK3f̰aڷo{iv-:utؑe8qٳg-[ڥK{{WeeeY7 I&Η_~9cƌFŝ={v۶me *r|9s 4HRݺukӦMlQp8 T{I  V*|X_YMʄQ[aDaD"} @0|=oOaVx']@;R20 V*+p x5@I+b뽢"CiaPhӟW4Bz7S$ƍ>\cܹ3**jܹ򇜝.]*mܸqᙙ7o޼y<ɓKXC{>zh||g}&;884oٳ0*90cƌ>(77w׮]le wR"F㻠H=PR޲n%F&pa^k䟾VTٵ!xЍHNVr! [q+e1ퟚ(foht5_XKV}%粁 P0hJ3fΜ;;;K)o־}t-hذM:v(̙3pdɒw}W>m۶l|:9Xm:q]B뛕IFF΢j,-yH E*vQPԲ'U㧐SjYAqO [ #]R!999==f͚/n󼳳s͚54rѣG666/V*!>>r闪Žo^^^tt{u?~xq}`lrYa`jz\J(ju LCj*Du P)a{. .Jӂz =fq}jUeW ݻ+ B8vvv^\ɘDQh4FVk4A}/TEB/(JRTfff* OL%e 1E!db0 C!. B B!d !2BB! p!B`B!0\@!. B B!d !2BB! p!B`B!0\@!. B B!d !2BB! p!B`B!0\@!. B B!d !2BB! p!B`B!0\@!. B B!d !2BB! PVv 2YZ8!83_.JVAX!TtVJ:Q|:s5WJiVVVvvyeW !1o,S?GӏZEZ$/HJg"V*kV/0\@@żHz4ͦm)S. PmGJc[ >TpLjJ*!n=nܸuVpŘk׮q7bXd<{NNN֬Y^8p`>}p'P) Td¾m u36V`h̩y12d߲Aqqqw MJJ]v-Zj5j(y=w\x᯿Z)U=s̢E`С&;;22rĉqqqĤW\rݺuU=P5"Pn] p&aHs"B٘#zrjAyJc#M ڰ_gE`Pߓ'Ok6m]6KC^7o;6;;S^==rHFFFrr~cǎ]YPU' IDATnnn'O ݻɓ']\\W^g~\7qyyyӧOgB6m֭[ggg'=:u)S9s&//o믿lmm+j@v6A; i N"#"&T4j# p? -^xСRԩS]v=}ǏR9nܸʫ+dͱzjy˖-4hЃ׭[7<罼JʊCB&@ J` v8apmgw]Rv.[-H*D6/##кuky*jܹҗ|XXXRRNz^^^XXX|||i ξsNtt4ϗmrA?~ѣn[`nnO?Гͺu묭bjkڴarrrMׯE'ONKK/*$$do۶m;tk׮ׇh"%@* aYri-ED ‘PXwZ 3='xp=}iذAAA6m:u*K˫[׶mۤlfzw>}:n8__߀.]\|ƏЮ];e˖ɟ(--ܹsC iܸq^t`Fc$<<ܨ/ !Tx "%HJDJJ (/š⟷AAE HyF QTA^lh3S–3xfffѣ&MвeKlB)77w„ .\ң4y䄄)=))iբ(N6M*CBB֮]+e322~ǒ_ˌ3v-OܰaCJJʚ5knݺegg8p2+KTTӸqs6jĉl֭[KLߧO֯_?**jɒ%QQQ;v=z4˖2~LJ5y_\2''g„ {Z^ʋDU,A20]Z68"Ku)nݺ'Ozdɒw۷o_paԨQںuk߾}@V2$!!wݻw}Fmii 7n,XdInnСCٳgfΝ۷o/׮]bN:mٲʕ+[la߿|r)ӧׯ˃*EjhԨQ9 &}._s]tٸq#[NիRu%$$(5k֌=aÆޣG^t)!$99Y'CU_R' kD(P$^o+iC^".s޺u+)));;{ڵk׮]믿ޭ[]ؔ?oFΝ>|H)߿Uب6m<{l޽yyywmڴN!cƌ&Mtڵ999s6lXRRR.] ;v d+wi?ӤIXz=lllԯ_ 4`w`1cȗrsstAɄ@X( X~ݻw?! t*)[ʉB!J غ`j׮}ĉ>@Z-4aٳgK ;vt2x`vV޽ٝP^NҰaï&z!!!:}t*VVV~)<]]]]v{uU<6k3БXYY-,,|||t2nָ޽{`ggwA/!T5X Y(`H#)IQ/)mELb%.~ŊsϞ={…d޺uѣG/<uԑ;:(Ә.boݺ<{ܹsSchh4F_9;;;w޽{ 9#iF B'ΊwecP9rȑbOKKKKKsrr*KBU/l&^vR0>ѹ//Wu|Uؼ{۷/00P$&&N:u׮]%o^ Jd#",,UO.9y%K Ղ4PucE{mș%B"RE)2"Bu%d}ig7±pB>> B~/ZiP<^dďȏן}?w.\l]0eٹb[5j4|_~^v}m~a돋d5kv)B&L(_ ]))HcWf%I f|5 VV'e0\(ހ؝%K<ʮ|^^^P,\P~[[XXHuH}sΕ62{޽{Ϟ=+%FDDl߾ःՠATlٲӧ߼y3###333$$d̙c5װO)~ """?y|oi׃lҒ%|"ljædmY}I¹}l1.d `Zn]nӧO[SNm֬Gtt˗-ZCek+U7\xq˖-,={>iҤ~#G;v\\/_.k֩S'922ׯ_…|IO?T p㏥%_~1bDnn+VX!7xw]8BJ(ZdbEJW%G>@:YpsՔ)51KLa\[l?~|dd&NaĈfͪȑ#;vرc<}ذa~a N6ѣGz|B[hjHqqq)))Uy$!/hݺ͛7ڴiC'͟,6 b:5mtÆ Wf&Y 1V@Ȕb \tDJ'v%eF+? ?oR8E`&נAÇϞ=ѣR:!kĉ+UrǦӏ,鴤s~r:w\~uft(LYbۮ]={kԨѹs~A{Æ 4hPU$;wܹsBBBLLL||(GgϞ={2=ԦM;vddd+:uB-(:Cҹ`\g kͳ<,<"ߤQi,DQP(r{qŅ5lذqڂ/[JJ [7iɒ%,:IOO(멋O deec*AYꨨ(GU**WwÂ/Ì}hdGN1%*`-dKӖ.Jڵ X1˱RԿ BJE KЏ(@>.C RY' Aa BU{ڱbR ->_tx-\ . T>) WB#sno[g dʖm"Y4OlTpJ=z4<q**--t ^e u&Hm:#n€C6Ss 岲3gNe8LJJp!TLtԉz ;c BȈ ^u"G n&OZ>ԕ[P尴y!^z]Ы.hЛ')߄=Je Q}u17n+ r( A\\\p#BiP .MH1l+E"PDD/Wa*JrrrRTϞ=uV~~ 4!!eݤNѮ>Ms-LF:$Fh4FE B!DPT*33333*uUQuqR477bB^!8LB| >W!dbP# B!& B!d !2BB! p!B`B!0\@!. B B!d !2BB! p!B`B!0\@!. B B!d !2BB! p!B`B!0\@!. BeeWl*  (}4??%ճ=V>**NEAʷc1\@ // BTBTyAիgڿEp{{p;#P%HKK0338RoJ)TEF陟R\p{۴aP(??ܼkJx왗Br Tf͘r|@)U*j﷨P~V {jEReee޽έZjժU^5jTٵ3YyyyhdVsssGG;wrCϞ={ȑGRJj׮УGJUT cV/Mzjݺu9S1~z _~ZU:00011 Zz7Q._wKq N;vFxzz_zUVO2֭[5INN^n"qrW_foߞ6mZNNXZZv^Ǐ:u*+++--/X`f,?yuԬYSVZƪˀ"U,--7n\Cu5V}+.dffN:  X||±cǾ_jj(cF1%pB+hb666ң&L7oޥK-Z(*=pB999}^z?Xը*RzXXϋ O?%''ҥKuF8l߾ݗ.DĐ %%%<<\V'$$꧛ݻw9st++N:XBQqqq111ן~AdddVV W bjj;wJ٤oz={v=ז  BɅ)͍wޓ'OJ#zq1h¡C_~߭[3g?~<11VZ!!! ݻwo^;vț"Ο??ٝ;w2esϞ=SN>Gfok׮߿?Ky~͚5;vHIIa)J2 `ڴiv4Vۓ~嗻w#Fo-,,~K8wŊח0x%?J?~kk |'999'O8q"ez6idҥ+V~zzz:pץK/A͛7_~]Jiڴ[h_a9sKԩm6y_|gNk˖-,EC} scǎ]vEJtɇRJ !7当[o]vmpppnn2x!iٺudoŔ)S ewcvvw߽Hf)S|6׬Y3b)b[C#[Rq1b<ɩ|O? P^ŋ'%%8qWɓ'n߾{ƌ ,ҥ HAeX;t;v9r޽{u7m9m4SN&$$߿4s̖-[^vm׮]IIIضOfׯ'$$W5111N J٠AϳMHѣG@@k~} }zvvرc۴iCzƍsss曟f͚/5Vmۮ_BBBuo޼)"5)\>iӦ Kٲe ;oy{{߿y?>ѣGOfkk}Mm۶mNF233o~EFnݺEs&%%@ӦM{Y~}&((֭[{QQQ{y'<<<>c2e6 ӦM8o۶͛7i&gg瀀rbJ^pAZMx"0gΜ:@ڵ?#YxVV4K*$99wyxxر{eff믿M8Q;wd4nܸVZ'OBk.6~_ԩK&++db Rkӓ'OtѣǬYطkݺuhp!00099㸹sue&<8vXc*nݺΩpĘ_zuذa,=$$ FƆ,[FթS'Bq:thС*]keel L2 ɓ'=) :t y̘1:礻wqO,PEQZLG}]9sHyػI&Rxzz7rssccc_Ęk{a|* ??? u֝:ujذa:ujРAZP@933f͚%%% -[inn^F  lccczǰ3ȚG %u.lcnn$ Hyg-;Y{Qm۶GȰMJJRTRRRnܸѩS'J){J=R( (ʆ ^tTwR, [[[Fڍ:=)PdR̦AѰf/p{R*W1\:ܹӣGnnn+VSRR"""f͚NNNOt '44\]]u~u1 6|ڵk:uz!kaP?999uҥxyW/%A.6]Ol)s|rt363~r/*\oU Jr߿Ϟ=8!bZZYf >` %O*>oH&V[4ť$MFÃ;w~,\z*V8::J&7 !>>>#M0)SjATM(^pM6ׯ_h4Yprofn{O_Hjaii9a„ &ddd\regΜvjpByO:xue?<==]Eɱ򜜜;0ztuӮ]Zvcv_Tűt6ams崴b͍u|BXzelׯP(A&IİxBV>[R"ЫUlڵ+Ν;<<g3fXp!DI'l IcήW^AAA9+\>(L4oޜݹqNNFsm0Օ̌MII/>cʕlܢ9fT*6$?>??Ã]zҒ^ROH?pu yuU#1<5nf`ffNcᩴ2Mc\b}߶m۟(/foÇ=c#t6INN޳gN ׯߠAtzR|@3fЙ߳iRHa,Ҍ#S2rH֑yԩbW[;t_>|¾nnݺŦJL2r3fΎRyf(`{>y"| yk֬av.$u,6#G9RX&p\\ %[ &ͥDM+.4o\Z/;wݻwBGyw~` (qlTٳg۰={ <ѣG:7n8$$ҥKcǎ{(999Ge+8;;˿L=< r5ёRx_522299ܹsӦM~zll,[$qB.t\'''v\NzK.-]^^~b_J†CEFFN>MOOg,^866VZ#9r$?7o޼޽{~xpiU9sׯ__n݆ *U8v5kV~~;7nLz~C b'l??00͛.\ٳ{rΜ9y ]v=sLJJʷ~ k׮}w`ԩIIIۭ[M&  քKIIrRT5RVj4ӝGŮM Ǐ?gРA&L(wM\]]/^鿩:u[oJceծ];6%\>N:nnnl}\yO3f̘#Gܽ{Wg%KȒbرcN:u-666_}e\zUyog}{nnj#_ϯ{ONJJbژ5j4nXg5W+ں bѢEk֬џ4dȐ#GLmժƍ7o.mllf̘!ȥ @R۷oʔ)h(;w믿--_|رү. //Je>}v)?KO?ߏ=?唧LJ2x+Eff(/8bR*bff2 "3fɒ%cdW?$Xvmy'quԙ1cƂ =Rޓ%/"2nZ!56 zj VXC~cbu>{ Bf̘1dOnݺm۶MT̥W{R^^zK.Zj}g:ʹ͜9wd˖-W\ɦYɏBŔRgdd( A>LMMurrj֬sss>XMUVpoW}/.T~8d̜j}eZ|IFF+o iTi4MFFFfff~~~Y')Jsss[[[;;W;X~H2@!/.!B`B!0\@!. B B!d !2BB! p!B`B!0\@!. B B!d !2BB! p!B`B!0\@!. B B!d !2BB! p!B`y|TEOuw &D (. ܫ.q; 0:?ʢ ^YDQ/DE@AdEAR1$x=̙'>rީS/ um&YZn]SW v |Ds"333? SSW v:۶-JMMkul0"*q 5.zqqqqqqqqqBt),Qvk 6x$4C3 E =w1bػof)v[d`jˁBg P|-@ac}BZ+.ڭa݊P+6үUTMnEe 9yBڌơ*,^551k+#P+NxGv@LYr%1~9q5`h7?v~5"----Mˋv cRJa8qqiѢEvr*ƍiӦjGnРA[(E\ɓ'W^۷onOt=S׿KFꫯΝ;7=qiѣGRC,Xy挌o^=Tz.҄ٽ{ҥK:q!.V^-"Z y9F/^Gu1vBg͚>3f̘1cDdٲeąH{Wo^D v}EQTP7X-ƅnڵFj߾_>p;6lժUno /,((_~پ}O4ID훕Gƍ.R~aaU,ˊԛ ąe˖ѣGggg~cƌٸqI&~{aaΝ;MV+))4iRaaa\\ĉE$ G˲, w}ĈÇ7S cƌ)((>O?+**馛Լ#"[fSرCDMvYg￟}HVz-g}VTT$"|{d˞~={|3gzڵk۶*8.Nzꩧׯ~^xAD vUW9222RRRD^z`Dzz%\<ս{w!.IJu։H߾}/kNDi޼ԩSEd۶m<̽ }*f͚Yfϯ8-peD ?Xܳ"aÆÇK,ygED)O:5(%%k׮ti6oLB\hѢENX&M%ӦM[z?Lk7Bmp&*:aBB|88uąվ}{ {:u={{oРAW\qEp`cJHHhӦT>_I&n9t +j)G\]:uUV{F߿رcÇ;:u?~?C'"۷o+̙͛L}E\]wuYg8qs=ݻwɒ%mvW'O|A{ޞ={9RD,Y633SD,˺WZȴuL/tذa΀,\ 4hPQQQ HNNv ׯ駟:KA". iiiiii'{611O%''Y K)u!g{Q( e˖l_.YX@\@45m4##}|7/JH@\@4[ O>}7p2\ ]@SZwлHx @#. >h7P+d޽CԨQV^ujÆ KMMx'P'O?{r ;vڵk6m|y~Fyio>yx_tE5/V^-"sݵkٳ^oT[:wO뮻ND Ě:ڵk7gΜ{l޼y~{7նz7x^xᅫ}^|رcg͚5~VWFu`0XeY{YnݻCP%Z8p`Ɵ{۷:>?iҤ袋N?~O[n-PSN "J/\D=n:< ֭[޽ ҧOΝ;O:b֩SݻۧO-ٰaCe D$..)..v gyvK ^z饉'ֶ &}̞=Y)##o߾#G\ns=ww86lO$&&HffVZW_?멧:|s=wySإK޽{9K 33~wymڴZ+Ÿkf̘fffm۶_~@૯[Ox!C+V_۷ozzSrWK^wРAp&qaǎ<_6mڴ}vx<&MrZ~3{ _?1))o[B͛/q'|Æ {饗6ol۶Ԇ DswqGx={7^/,p*ąP(DFFFϞ=?ߕpĈof0\ts[[>|[SSS?Ϟ=b;eʔs\EEEΆ :zΝ;w)મlIDAT"m;q~nݺ={ ^711*Yu۵kWs^/,,=zhѢŮ]>7.8#W^yexe3P IIIYƲK&''n\pgVyfgGkx_|dlbYھ}M4ٲeK(|Fgʴ4wΦA_茘TN)5|pgZ;Le4hPP@Eu8.twqN8>r;sM7HԩլY3ggԩfr322RSSsrrvQ"Rkd222rrr;*1޽{)͛7⑫bĈO?;~#L s= ,())>};v\l}w+[|NVHOO1cFݕ~ (ڶmz-ڲeK ,󜝝]I\_E=zXzŋ: w5SBC=n3` ׮][ n^zߵ9ӻիFDNx5kT,tL6mƍ'l֭[~rkʈ#D$;;;//k{N@]qZ- +.AL2ŝ?~Ƚ ȑ#վK(;vk׾*+qjjС"R\\|-T\#ov&-^~~w <8111 >'8z;SbŊ.L 8cVq!55չ1L6)p 7nx7~ᇹk׮1cĉm +"}qo޽{l2k֬nZӟ9}ѕ+Wnذ7nH>#7ϛ7o݁@ ''筷ѪUEq5*TZZ/^\qqM.?a~5iDD|>_m\&LHHHhڴQ*> 8cgx2^M B;|pӦM݋'O۷/==9VBk~߰a͛Wʒ-[Feffk׮*(=z^q(0[_r_p "'TLNjm۲T6Q9w9s9ƚM41R*==$$$t9|xg!qƝN:ٵp"<|sG;™)77_|fv^zF_|TDGydmd K*6l.-M4  RJyކ X?̚pfz4jԨ}sO˖-"@gS qqqqqqqqqqqqqqqqqqqT3.hkUM0 .0 .0 .0 .0 .0 .0 .0 .0 .0 .0 .0 .0 .0 .0 .0 .0 .0 .0 .0 .0 .0 .0 .0 .0 .0 .0 .0 .0 .0 .0 .0 .0 .0 .0 .0 .0 .0 .0 .0 .0 .0 .0 .0 .0 .0 .0 .0 .0 .0 .0 .0 .0 .0 .0 .0f\Zl;@m黚qDKOՌ ^׶mֶm{꽼q!)))ց@ ))z/f\x<`Q bm`Uտ2Ae0*@lr JJJ,JKKq?F啔|jgYV0 B 4PJUP`(@PJ98vS ƪ Dm[.W^գXz=ppO`žEAosI,ػ>Aܯz,iWoux/^2$A-KD۳U|w!^i$RZk[((ılOq{Ȏx$AIO_%۫uB; |quFR/NZs&SJETiVPbk-Z=m8-eL$'iU ."˟Tb$+[kN7]z+?-JٞҶI*)I VIc Zg >ccn繈J)Z+th:+'UVue:\a}7!|]k-<Tܥh[;,+'XQ &h FJ$R6mk%TlOi[ 'V(q --eeJ 6Susܮ U_[Ec*}z,(e(ob%:gٶTZ}!̆""}}#.*nŹDv?s]8_eWe*s6:Z4*[JC;r%d+-);c9""~}幍SvJѻ-.2l*\4 DDIʪY<$ΒIbR.uRHnOL>v@LZlQJ+GQR?QDdϺnuz$1^ E~!(9g՗z1лVk:T bwψM#}GJK$$ >iJwJN7;ut/7SowF=1S۩z:˖-V![VNIَvFl{HD7gCM`i{f[-"%SV ".Ntki)ۗY[V HXq;[T U"ݟ'i'Ik6KUDD{#N7-GxöUrqeWFCC4}MwiDGl=xj=(?lߗNU۳K I`xz8.0ظ;!8z#i/Umι՟(ɦc*D HP. \6juO?gW`bڲ%dc!KBgDz#ݞ'"'GJju$9AD->س] K;F5owT=;Xw[l'hVVnt(+V>tc85)S [TVcbydq- #q\NFD~<=Ry#ֲ>[븠-V]z/,Blt-|@nxI^#-9 e5gr9|kݕ#BvcNY,gJtiw|H -HH؍#beTYUh@SJՏqֻbc4vS3w:rO974".jm4WDP%3w HĶM"[#͑5|ﻷV.[V*G3%!Kdx̗EqCηQ)⏳f[C[)zP%"ZD[ ʐ- Z?w,+ΐD ·+$ wx ^U]-{ [ylQQe RlOy+mѶh[lKf\ 8qAX1ml9軸;Sή~ODdNQ-J^q̴H&iykACVaaaaYѩ;]FV<s̚ծP״iرUu@۷oJ569}ӏv%h|pŧJՋ vew\JToXl6 ]6deϙbc~?Žcl9s ]֖U'9wݾ{l/Àȱ8q*01;#&Ҟg,ܵ*A[%嗻9 ׯ8† W?,"M}8;v4G:87bct@rSd8t[ .8hXxhk[hDXPlL-ȍI`VD3R|E@D5.,.NNE1j3,ҭlm{GS.H pw{SEd= nݮ%fuK?ŭHJ~-S6~+" 3 $d1]h"2mjk o #V}^XRokG[)s}Zx:Fؚas;8*׆bbTzpժƥ3gyۗFmRˮ]S iS /><ζsZ~"ү߅֭:8j;zǥ,ԥ]QK~?N·;y(lfZKg,zc:%M}wWnߐk4L'w /[VYrt)I]zMKn6{ꅜ[B  uʅ+y<ۖim("oqذ8۽. +*҉SOS2 {ʅSqjKDιtJTx]zReȱJ>Ի9Sj4"{B9݉ M zZ>ܓ\̘񥗢/]h֮W˷ݖ$"'NxZn6k>4BD7<8zNjZm\+opvJ/tuVM=>رI#+2R˞)Cl[f|(Ly2QKmdHQw;4o>{Xh,1M;\f'7iSٳ"2qIo [zA+e@Hd+K}\\E$.#U2ëAvCIR/k@g]VOrյDDL&ͺuz_`#~Q&ʠAt&"GxY4*89{TM@!)B2R eS| QVѠ\xIwYv%&.}}AAWKMOԳ_#i7$"n?\D|_a @!5u17+6=YD{HxK?c=!+CDyx4䭕Zb6d x{Mvo֯+nەy}畅4Z&FDzx@!5y8r05~p\FD4{ "qujj-wǦM?we˖h4KW$|. Hǎi=z$|p!P#ܶ(f;Gilw"?Zv~Fi歞cLڧ}GZ5ș3 yw\*{Ngy&\9#8bD\>u@>}K5nXՉ{B+O)9I0-&TRT1SՉH٬KniRql98?t]w%WuTNgy3[mҤݻ'y䣵;8j&-]qsػwotttVV 5!!fX,}3"67yx;T"fQǿ_S%.CI?N%y9F5 /&%9{xlӨ[<"#*.֎`[{]]]51\ݻ+Kc3aXD{֖\ٳgOtttej`(P5 A }tkuTB=e8KI۶mfT"/нXY; ,P fc`?TV9 !j5]hhŋF"Ү]ޥRE9s"m[""ns!ҡqW{-"jt.ӳ}dg//cXXV&L8`w],!P)Q~Mػ]ÇWٻ T7;pY(3 .]@q <UÒQP)!T@*Ŧ2={\x1))l6n:,,,<<\ٻ4&G ݬ[n͚5۷o7^㏽R,|ᇛ7o6...K{:th^^}+nnnt:]~ޭ[7^/"Fq/BVVŋw9`{ ܴ!ݬ]v޼y{VҠ899Wܱcn~B؍s7@wnݪ\m־77!}}"=x`{G.\SOfyw BƍI&79!ꅂ{ٳ"2lذ{7?!쯰p111"rN>@ O4i߾}"2t_ {X,SNUgΜIn>|Þ"ҧO3f߰'OΛ7ODBCCgϞMn0>nOn2Dd̙.P!#''GH߿]v.P#{:srѠAehY 4;v , P!h4*|A}z=;,}taN1Cw1*E "J@RBP)!T@*E "J@RB@uf5YL|YYY(dJJJw2{UKIIٵkŋҜ[jգG'''{vsX,C Ywz]Kǟ}w}Ksם^'"g38bE# Ky;v\N9rDh"R4)չܹ#G4l&Z1j+sVװX~ib2Ho Vn*Hvvݻw_7nر 4^BM\\t&M )S4mڴcǎfc`=]v[|"1;m߿ĉ"xwwڵk׮}==u..ig4hܸ6 96l2e˄K+w@:_~&cF `AAA/>x௿zȑw;ws]}1!C=zԩSnݺl6[Vo7G޶~mݜjwdloeܹ'4 "һw_~9((ܞ&MNN^^"JՋ@~yiŊLootW:ZE TTXӷsv6)t/NSbĉ֭{RRRv=nܸ>ͭ2;֚Ew..."rر"Uwit={zɓ'+ipذa~Ҡ\]g !ߑ5\\*V_/_< @D:oJxB/g,\XXhrPW}ݜ2dkVq+׭ ^ Пz3R_&M,ZGD}ռ`n<~IHH6mxzzVo .,]ܹsɅAAA-ZhѢE۶m###> ,(..jǏt_UNw"2fLI~~8)7yiտ-hWtty'Ϭ_;iUֺ͙u˫-%yy'^z)-gϦlpnΜӛD\Z2;&+:2iZq 0@i ӳ^H^ƌݻkS6l85}zQn@pz`{w߷o_t.9!d2%''+=;wn"ҥKqUc#G|EDDDt1000===..&MRy Ֆ^>7߈F>|xU1ߏQ]x„YhPAâ7&ItK[ܝK1i::'v]}S`D&M~O\Ċz3"m]g2 fHROuU !{˫[7nJŷozg,&ųSg .b&Nwŋ4||ҷmڿ?٘o5&kd߃ƊH??߾};ys? 옘dQE$mfk ?{V\b,&صKD=ڶ9z…jmʕ'N"sqr*LL̏3Wp3jYi4#G:tHD֬YC aO-I&=eZelڴbs=7~xۗҬ_m[oݱcRfffnܸQDz\m)"ޅB,.>XDmVn |ns"=}v!B@Z^}9{G)ϛ)"z9w5\D?B't{CA({޽NgߔdhjO}|-'__ɴwМcNJgoN."͛wk}Ӧ"⩧NLziʂ˫W7?RXX ` ۷gN))6}(͛5kVJJJvڹsg۴il6/_\D4ib(cC IDATsW/WϠ+5k('޽{ܹU[nrFI&mذY' ̙c$ܱc!"Æ ]Uˮ U2$啳Ƈ?lgw|13̧鹔!aC^RrЂ* Ҩ+Ki?o"m@(=* ΝD(%R*~mTP}`Jٳ)sirJK7k5)%88ddۖW^WZMbaêSɁ:^xO/sR_""">zÆ U]-_Guvvɴe˖ɓ'{օHϞ=۴i#"7oVdx ^ JR2 q)j&jЭlV⽺;gRӕ=^=ZM%'+¬J1d2ӡ+HHP.ט5mФͦhZז-E$?>>'&$?ݽM߾}E$QcjjCBjX[UZy]bڪJɐu^Ta]OR+PwzEp̙O]ԩSSaرc۬l' E1 R".#NNYnu9oР^ 8z{WZrDFFvСܫ_5"pÚ:~HFaFݬnc^VYF;Vb]iSGӷn-a<~V"b1S74>zճ".뺘*Q3glbcOuowk:ࡇ :s̹sD$22RYQRRrX{=,"{Ҙeҥ999e_RG}lu]͛7'N( =ڃHVVֲefΜm۶~3"",w'g=S{o={H\\%sqqyzڄ7'֭kyQk.Jcn@'*s2ŒkWabbU("_9{7.a[l?l{7.]*"։J2of^^fd\JHH1crݭ[9t{Gv\DV^o={vɒ%ϟ[r~˶mߝnaN0["_?;Q+c*TX2pRd*ڞ}aض!Cٳg[W[K/9HO?},%%梢S~=>SQ/ر%Qk缫{*ZrHƩ^;7w1#b6>}`CD+:;m+^TsoWΐ:- ~~*Aqq<}ք vXqݝf}`}$][[Ֆxbjmga7~駟~ݲeKL9::Κ5+(( e˖-[LD\]]-ƍzZ)C3geСC8z>}:nVv͚kVƙfN\l|]0ĭɥiL\}j{bc;u0[Jm߸m#F,Y$==}ݺu?p׮]k5k֑l0\^բh4lS GZհc `^:}K7Pa{h2\[|ݪWht:-Μ=^^ ;v:p@봘*1:?DH֭[*?$a!e.]رCD,fU{w'"?o]uB؇u;vLI߸qc5tӧOݻu|%t!C|Wؾ}{qpp3fLn}jNtҥߎa`^0@iʾZC)%l/^1qQDJ%Jظ-nٰa_|Ql]uMVGleBw]vF//-[T W!111999--߿Yf^=Ļu hm n6>|Ů]6nܸl%G~,o7;;:7h[J J;U_ĉ>ϟ~c,\'oo}f.͛[Y`YYǏgehֺʨWgff:d*(|66\tMsPm1͟YDD_kq|H\FT { hP9 ?}Ȟ={sBtKJJϟܹ'xe˖70űc٣j}z d=P@XUgu:O?l0s#Yki}) O?}vtÆ {'k,P,y'NN1c}gvwt!11믿>rȑ#G7oޡCΝ;??ϯX.n޼y"2j(C>l6?iPz9sN~zɴtҥKlrȐ!&Mwu@iӦM;tPvvoY{p?c]E$11111q͚5+VꫯٵU}^/2(((99yϞ=Ǐ_~ޥӬYFG)8={vBC[lQ3ϰ u֍?d2H^uִi966رc}ݾ} 47LLL|]8Nkۖ\us_}U||'NhoGEECǏ:488W^ѡhRRRFFFva;wnݺڵKY% 7L prr2?ɓ)lթS'/fsII[M&SAAqmfm?{<>䓗.]'%%}&ɚ3M&:4{Bk缼?8;;{/=ܒ%Kl[rrr̙2o}HFFF^f̘q um۶?~ƍ_!CDh4:ҥK>u~ &(;X̝;Wɖ~QF-[l˖-ӧOW2ŋ.\XϚ5KI={\dILLҥKo6Yr̙3=|ٷomLVfE>|pRRR~~ٳgϞݬY޽{ӧO>nnnww3grcnݲ[pԩqM8qƌuTTT>} PPPꫯ=gSRRۖ-[֠A ٳ}wٳgOzdɒqƵi楗^̬PGi4#F(#FPҠbʅ7xö%<<|ԩ"RTTtrED^y%)\\\HIIr4nرc6lXn]%/.SN͙3P>W^ۻ#]Fm#"l8[]v-;صkWرcÇJY*"GwssҥV[.ݒQ+77z衇+V,\h4^|y}]ŷN͕RYњ';WʡCaÆ]kkIao վ}ۏ1⮻߳<==RuiEvV۾}k RPDW /,,h4>>>e_3f'|""gΜ.ݬ;'N(hm۶վ}_~aڵ'!_ϭ͘1#<<<,,ѣvQ.\\\n@=+{[*Ax:-j~_1c`(jBB^oٲ '>>.RC֭˽˺RW^"o{ѣG͛7[?pZ*Ta.]/~8whJڻ@-oJeffڻ@-oJ {EM UoOez{E ]B8P{}eW<Bw ZۏBrqqwU@^yxxۻkBUX,嶇'$$$%%z5S>P4Mpp_jjjfff=h4z׷ޮ"B\\\]g@RBP)!T\{!ۭ[7{W/[Pc;4{6"J@RBP)!T@*E "J@RBP)!T@*E "J@RBP)!Ce:=z뀭mP*TLIKKn'׃*HOOT%R!|!"J@RBP)!T@*E "J@RBP)!T@*E "J@RBP)!T@*`P+{F=zteor ww&BVVK5!!!.">>j4OOOF#"˶b)jnnnnSxT@*E "J@eꩂԬ, "zťV'@cXl ˗+yREi̷wPu1b9ydNNε: fB!AENNNBBB ߈@ IDATHAAArrrez&%%PQi37uԹ,! bXjk |Z[c7rD]t.@KOO2eJhhhhhke51Efekel,fsi vrʁ]Vd2ʰGtj4"ҿeD X?eO{%!ݜ?W^ٽ{w] ~k)f enBT3S*iP?qss=o40x5k֌=Zػ@]X2 y۶mW[f~RB"6 .cfvjVQ[Nccr^K=#|~p]>ή~ODdNQ-J^q̴H&iykACVaaaaYѩ;]FV<s̚ծP״iرUu@۷oJ569}ӏv%h|pŧJՋ vew\JToXl6 ]6deϙbc~?Žcl9s ]֖U'9wݾ{l/Àȱ8q*01;#&Ҟg,ܵ*A[%嗻9 ׯ8† ;!XD6-=tqvhwɓVup5!nNuNTZuo2LXXD\amYcA1 7&uXH oԸ8!;]DԪͰJb鶅M("M.uYEzaޔʖd2|3gS"ӥiK ҭV: mo92OJ. k^_fF#:rKr))'NxmĶi@8cFG9Ֆٻ󸨫㟙VY\TAqW4MLmQgeY2--۽nfjM*r}_7viD!Gjޜ=g޼"sϝ.kjB=껺M8ڥEugivZ폣^yG"n}ĭ_>4` 4nݲȔ,pn% 5h~ n;>vqNAmxUBjgŰ^:_/9RAgWs KNSk+ Μ׎Y2qqN:%e6k^8jTu$5Eo+AA78j3 EZFw9Y2("YB8;lRk_?i?3GSRs=NwlTή>: n.}fڒaȝ/*%Fb-;=;PΙ|%=kO^ZNN9:`ϴ.2ȶm~"2mZ##yy:ykUUFv|@9pq%"vZD֘[O Aީ{@SbjL!4,j.}SCw"Ziе]Trc۽fصk%q{DikXM&͂!"9xpL@"B:7hܧqrAiŰ^y]ݕt<خ^â-8*#S)^CK x,9'(;z]k2UDt3fP1L9|A{ڶ/:MD&L868j!ޅW1T%[-L͹9YD"S~n6_IO-tOZ:嶳3ݻn"b4j6n챾~~O,XR$;tH=ׯo*88~lE@!):C"Se]屮T(*ӐkIWٳƮ6-" s}})YL9|A{u'yh 6*B *jFڹx ."u*.zLZuqsȭZ`2VxzMr$:M6wsIB5L=tΦM ySoU@T;4 DdDhGeczK P-2 "R;Hʭ UD5m6-ZRcGBu4y͛ON%KHvI_PE@,:Dzc{EArJaSOQËiIﲮo0sr|*xwg7JI|ʥd6_LMU򠠈dg3sȨt9|ϟ.T܍* 6s?T1 "{֭["w9K8ԧA+ƎK>nէ*FEdGh6ܬr:s?w%ZDz4{E v_Ϛ<`g}Oq~׿aѦѨ}ny̘O<+".~J^P3gG# )w BԩS4ݸ]#}:Pz_N^IK 6 ,};L 1hP^s {%t'cdžzjԥKۛ^yD6JIJ*~D8fT圅Uz?×"rnY}C7ORڔcSr|[W[-?wlRsO@mvs\[_V&upId[kzܒ%;+q(*Ouvvh4„[c+*Րl.]XJ222DdaaaSFqUK<6 (v]tkRm:9jҪUJ)]:ٲhe5QdC}غ5K)Gu/P~,**E b(Y[wl݁ !Axʆ)RBP)!TEe`czj\\d j޼yppNu6qڵ`0:{zzڤcJ0e6駟n߾`0ۇ999)80lذL1Bt}5jTΝz ]viiiW^ݳgOmM!lfÆ ٳEwޯrݶp#f-wuuD DsNUV pw#fDsp7#r /`2D䣏><[*QS>S)))"ާO[Q#dgg?"2rI&ٺGݏ@?~cD䡇:u{6!"Æ OZ~,7oؒl:sL Pm [?H޽ON߿a3gΜ?4klΜ9A63uT("3gtvvuw!6ӕd|}}[nmjdg@.\ԪUK8ZTZƍWԅ@0 AllY @a(lCݱUBFXXXdd{#0RBP)!T@*E "J@RBP)!T@*E T':5h6ںKKKu/bƸ8[Jfg@{դ$GGGOOϦMs=l6Y5dM=5"Boa6qXՊHAFRWPӏUٌnXhn3r.]*Z??-;R֏dYQQǏkKPh~`bkLDnܸo߾}-]7n\Zj!l&22RDt:]z233###sssoܸ1qk}۸r8`kVw{+Ɵ5{77WDz[o[S_^VTCDj,uP tNNM^zwߕdw_YyZcEohSJV0Nb„ 7nSO-Zťz lf…ƍA3g>>Ç/Seu&gQx43-?]L»ţ-jtK]_EdB u$$/|x̙"2vز6^Vz{"ڷu)تag(.\܏?G5_ܮ\28hGj9--ۓ~s;:;.^Miz\$|5ly#ވDLC$f%n8Ah6h6VR^},٥K^\=lP; +[_VƩS˖_#,P}heŋ ۶]_ܹ.AAw^e)+ϿqX-__%3f |od}aʾ} [:5Z Ֆ8BX0C=z#GDEEEDD#–Fc||ŋ,Y%";v|ꩧg͚e65MHHHv### mq2D-<}oF3jԨ6^nQ)QcD:9917bDdٱeZ̺DoFC׆]]\ 1 kp{Jr܂ܡ*i0NH=z^`*w#;[i}Յ CRlwuu2pbytѹ_o|IΝ'^~l4ꜜ۷ϊνv͔wl„h-5//_A;zÇ;xy%GY&L/ W.sAAģf;'"||~d쓓&/sf7ˊu'2<("I۷[aVtR"1Gy&e^qsk*ĉ+Wmk֬9=yd[hGNllVd)? !,1Bь=ȑ#"~z!o@[:|%nYEZil۶l6ȤIƏo}*???))UVݺu۽{K6o\hԭ[=ӨQ6^nN |fu_."SO)6N6IDujWͽ_IR4w{|NotaSV}sG0|p|ats r4f~MYKwV >)?߻_Ӧ9x{Æ<FZDg׮Jͤ~;7u8w}"NO|m͚옘d!/WX6m.ZPRmۉW^1FϙSoKy97m*"t;7 ʩ;l&/! #RܝIWtAopNwz_>LAQA9u䩧v(R.*SAzUV^^֭[MAfbQ8|ͼy󲳳q4D txxr0o<%Y]V:zrsw4zS">3g{gNj_WJ5FGA)ek:X_ذ\\uR믖SJt`g}:;77.{8x{7yŲ^~s`P ZW. !l޾q-Zիȑ#-Z~zeg}͛7 vܹ2A`Xpa^÷lْ,sε ޽"2rH_em^ {-md IDATP%H\f1+j|:z.&+_g7RSH}1ߞ |^/TL/"ֺ u>46[mq)"ZsAr`Y2ňG-'7.NDeDYYs-nZ//e#edUFc"Ro(V3NuIuh[O-Umb6ȑ@^/U9`sB,!!!fRlRu:ի8::hܱc+ҳgիW*"ݻwoٲ;wnJ#@))aF0argYFDZhѶmۊ7^ql`4Ȉ*~jkHJNJ,Y/caaRi 22a۶,&Y%VÔMEl4*;*nhz[{E8xz*O]VugDYGD.\.ղʕ+E2pWJ&>ѣ{wᔔCU111ӧOW;w.fNO[^bWEdݺu[tt+.]T[qiK>GgMf| h%˻+Me ,p,l35r|n]sȐ!9s*7휝E$~S&FD LyygFݻx{#Jʙ8UIw߽8o!%l2e?hGGXm:5jl<(|[CuOCB3E1 eEFy;c,3`@NlW94f̡ѣwaCe˗VlL1bwU*!lײBJJO<~mݺˍtSӲMVVt:ݐ!C}coӦٍ;r<5h:vXʷc8q PDtڛ$4gKP)A ]{:!"R`*P`6j]vor+(_UFD,^>[V/"uZ2k6+ãO[wB-Y^l"~?ieeQ#׭+G|X537{| J16~f'[zy:nݔϰn}zN~٥K:V:K,Y~ԩSW_[P4IUlaaaiiiHΜ9wdѤIf͚8 䳊$__߆ zuBbcc TPP[VV&ѣW^ԩSݺuVXq|œ?<)"+-ːhЭo9[{4٤>M0Ag…²/]ʊ,p7lofY2NOKsj%(VݨQOMM;rĘZl{~׫Bd2-^x"hׯD~9;;k4wwwe@ +*X3##CDo(a^^^@i@X ee6_~Ym޼YY`:YQ R˺/]DDӍ9矯າ@U3裏N>-":nCu6w297o޼AG={W:L~~Ӡjs4HDFʕ+u6hРۺk@1ûtO*rtt5kif?{;vljj$))i֭[n5k֬YjFrrܹsEA'd_}U@@@||O:%"͛7뭷l5uqpp={ѣ?3e]vۺk@a7nرcrlgg7lذ_~dw1c }6lxÇ۷`0ǎ9sfU$..>N:&>>o. {EQ:t_GEE>}̙3P 77~9;;ۺGTj ?48|OO/Rzz%Kx=zTgߠ*nnnIIIknܸq׮]GՠmݶxGDBت6ިԩֽUk /"!!!}Ydxooo偢/0??vvvp0Lqqqnnn 4(QBBBBBBPPukג &7on:[O>>TZQ;99-]֧ؐOټyEȑ#{)Tĉʩݺ߿@@@Ϟ=uְaGy$""RaժU={T^>J; ? mժU~:wܸqGGG[+99Y~[jU[hq}O<9''l6Xe˖[ݻwFYdddݻh4“'OƊH׮]udksν|xzzhTbRAAAEO-X_RheXwرgϞUV) -օ&MZbuÛ7ol%gޯh"K /NKK 6mpCnڴɲ׻t钕թS͛7Q}#.\PBBJzJ˫m۶m۶-999ӦM3?իW><""bˏ?8"""""B*"3fP`֭g͚{+V(k^|yĈϟ`;w;5kL>?߽{9sڷo/"qqqk֬\/deeHDDĵk|o011Q9hѢEb׮]999"vMD~邂-[HffSƍׯ߸qcK#f??N&yO:W_KַXfN˖-իϋN[l4ңG:ȑ#G,%333,,LIP o099Y9h֬Y’ =pnݺuֹܱG*fRҠŌ3ɢ:N=rm*iP֭[ɓ[lٸq$T -:uRzx+%uر)VZIqP jQV-www .TS1k2 ]\\:vV?Trp*E?s9uɓv2 v)ezk.k8pիOD󋽤Pl۶rp5O8a]u'*8/^?:[PP(3FY300ID6l`]9>>~ՅZxwCeffZ[^5A9l {&MJII>[oISN}嗖9Tk l۶3<Ι3grrr.^aÆ/^?a„j}ok U6WL:577wٲeyyy͛7o^ _~e6mڰaò.]tRK# :tɒ%?wZػw_5!!^ <"oŭ\ѣv[zk}""RbccUw:O??~hhhS^^^O<ۭwr:,[m۶vv7㫫)SXT~7xA"b0f}/ 6["O?4iR:u,%vvv>ڵk֖Bn-[jg]{uFrqXh@TEf̙3鷫\L3P\t4HOOPdggǗf\\\vvvEE $11*E ]+ӐyqvIMMEQSf\Y9_wk f>jUQ(!l/99yĉ͚5k֬+ek&l4L08 IDAT*Bؚ5k ahfj4"үIH4X35-j;%!\t޷o_U4ޭa`)9Y|TE526!ʇBɓ4Tn \4Tdgg7x׏3Fغ;0e63ϴjǧ lѩ~=ؾMCwƜx̌7|8,>kٱ^`ۺ ELQaM< CSZx{eѭ:۫ugsH\> E /ߢ5?ٳ`,h]oXp;~ң{$er=C};BP]-ϝsr9;ۮi͛h<-,,]}̓&3٭srtgwLa3}2cZ:bCtmf ~@)nCصnrWg4/djޙJ ) ?;sԙL}f:h)9<ǴRKS~]o2 n?Ÿu˾;}.Sa썔cWLC$fel8ܺ)vO6oթΟiȀWl٤㏍D~1cmg]DWNqT5!a )ygxIZn> ?ÐX}v!1;XK"8"\nN~~̍dYvl-GvR囌~񉄫"Rϥ[7pu?t=vYyc-Mɜإo.M!~4rX?൘Kiɖ ֗\HIIxm?b8fhmۤ]7HHp<}cǎz5L9=6F{tY@U#Q}o*.-莗;{TD>s&L6jB'΋?tK&n4pu[ͦq-?ODl_g s#v(i0A^uvs m,XV;#/iOzɑݖ :;r[XJ,t]XYtvs)+۶?_{Q"+'%׬i,"}^ QPPAϴ.">n2D$cAB1@7aZNߟ9w4使crv֩ohpv3cז Cԝ?}1hT.i5Ǐhٱwޱrp+]~ ~rrtAn)uɔ)Gmiڍi6З_6Ӊ /._R2 {ʁC-jt$"܊.~d% *L7î]+iԮG'"O{]Rn2i,c8ֹA>c% j]ץ4Nk/ evmUXtOf>b]2gco̹ J^A4v\<.raVQPTRs/3Ҭ<.)ףLm;,O)6ijiV. (;u!=\5ǼN{\fx3>[("g66fKe/-g2^^G)զڝ.a7sݿǏ>={OO<$htqtlPXQwrBFA^hCv~jx]%Ň.JKvqk[>bc=ݒ\<=ZnԨKbb<۶}X7q˷nPlj_(0rmbϞ=\ iX,ٳLNNݻ7""250egCPUmp:Geppزg`ZյC2*bYzzuu)j٬L͚`4TV7!j\cWݶn 8pt[]*@ݖ,ik{&<<6(!P)7{"ިF"J:uJONv6e rqʔSf{"/H{WQtqj{WcQ(!hSF[S] ڻ!Ax)QB(!h4Ee`g{MLLLJJ2͡aaamڴ. a76lXnݎ;Fc"""}]hSFa7o-[FcxxxDDԾ}Fk [#^?xc`0hܱcǬYw5tP{ ܲ!ݬ_~ٲeWӠ899 8gQܹ~>!ť7@wo߮t6!ꗨ5kֈψ#]p+#xO>i6E^[.Q_dffN81##CD"## d[B~~ɓDd̘13gδwE@+((4iґ#GD{;w+4@;+..QF-X@c |=Y,9sЏ=zA7魷Ed# 7߿a7˖-֭[/^4d|̝;d2]9BGvvǎ]E.uYY8Z nbQaFQ=HHHXpam ;L}aN1Bw1E "F@QB(!h4@E "F@QB@s2L(_VVVrra2]@RRRvޝӪU^z999ٻ[b g6ny' {Wtݻ7mڴcǎnݺ}֧ DFm_{#<<6l_yŶokɩWgf0Iҥ)?TdAε[_KU;3o^fTťW_bz(Ç7h <G5^!ny}=ߠAs}7[^1gÙ "1f6jsl"Nԃ^{mΝk7UNDJrr3&MJ5?w.Qše3Z˰R3 Ο/۾8+ kꏱbyG KhdO1cmU\reϞ={O&N8agg@}@ Ĉ^oҤI```nnnLLLaa+Wf̘ХK{xt2؟ ^n-np%>J^S剈wݽ{ݻ۶qһE_qnܸ6@410J*hw]@8ر_~"cEQ@X* ZbףGfgg[{YtK] vӹs^zw}9sl߾l6ꫫWo/q_N۳b2vPDsФI^l[Dvp]][Y^曊L;htW^M TUXӗsq14L׫:u x㍔={L8?tw9XTvL0EwŮ"r"Uw+^j= VUzz48f̘ziPܳ~M&Ț4lĈVjڴ:t?O G D~2.u_#GK7}:ځ@ o4B(U5i>kذ|wQQQ5/)_E]v^^^ŋ+W)**yUEEm?w"Sъ#+9͹sm}ێn7z\q.sqϯ~]6rm^(lm66pfÑ#8H= !"~~~w_~<8|?~|U;߱\?w.e˫Ww\=4Ֆܓ>3yqq)6[$|&6K~{k LӤEĭukCՓ-r䈳ZL…C{b֬0cϞ”MN͝[Tm *!LuZСCQQQe>]sBؓdJNNV{>wcccE[n'NFG]pbQ%<W_(cǎj;~x5ڵiԦ J|v;[9}9g,&'Sf=\Xxq#+ϭU+1dg;xz9cLOWJ۶M,Qkj"=9_~$WF?ǎ {%ԡӶmJ.*SC pvv.**駟^~MA~bQ;_~ҥKq4D MtddztR5Y^Z:n7Ujsutq܏CC^ѿ}nn"g;)V??sXðZ)^o4("ڌjR-F=-Ognzg쵡>}zVB^:-Y+$ ԧ ̛)mjEk=g:xoH+S~~;u_6h oryG?V6x z+se{{;6zG/ձD3?CZ= "y;ҥKAmEr:m&.bsNPIzmյN>>.͚H uWUWF&MRt:ɬdRp.icɴ_>F4eJ#N] 0))}NqmSU GklxWA =9::hѢm۶ 3ḟ~n:ug={,]ѣ:Ah iӦhd$\dupΝ.\1cjTSe煪LDrYQao6q#Yg̜TD=22l>JTWgVtm]FUXYi2G-.EDXJJE]OaRXQBN !ǫ3'g12&bq IDATޫԭ[7֤mτ€Ç^kR7oEDnjƴO5 ?*.{Q/\P=޴iSU/V""&i۶mӧO߿UJ }kNDN>e>XΫA)ZJfՙ ]}&uգi'uNDLg$knIE/k^*JNVLYcdC+3jܤ̦(:[˖"}HI^GvHc|5԰*M!k1jmUf:/'w˛ B;S"8{lٻnmΜ9jT vWXFFK/4a6+AB|ĝwީn9]kELFz`*KhmYsߝwvrtYǧ~jS=0?zZ*QƦ%"=z(z}#u ŒmѺ.J}H Tr=nua~`Q;L\k3ASLٱcǧ~k.ٳg˟|IۖnժULL̉'o~!Edy%$N?-"A =N\(HfM6Ubccd"#fDvh(LL'__wj޺g-"{ݻ;xxn\xY]S=2JZ>3*| rjбkL靱FQ婷Nׯ_?W^Qϔ0PQSo~+"m۶ܹs;h4E+u|T 504Y7j].#"ԥD7v:2e沷,o^'6TVDb2;n( k{E8w]u1U.#"ygڞ=}rЦ=wuR5gϞ=wmV_QRRrXx ,"ReʕeRo[}l >y"rIu;#VsϟW͈ȇ?g"1Ǵ*]{HlFl o߾E"rȑD n>GooEĘq|ƌKXJJU\+".$~Sk<>hw oN Ƕ6u]L\)z/SSOj4K uWՍ=:}!m۶o}l}W\)"ցJ2ͯJ~/ddd߿j2dO?TFzܹso]&//O lz~ȑ9::vIDƏ_[S[n|;:a!~[EDA^ї}e[RK%*o?~GK4إqz۶lРٳӧ_EןKZn";dHZ,jqnc]//{R=qeN>HoUWv cXUb{ V@Xbrďzۓ vO>ϰnC&~ӳg ˗[ND5jOKPהb vo߾,{RݻwFoo-[n:i`?*,,LHHHNNNKKswwo֬)/%!!a%%%ؽ{ƍm/[󘈬e}y..<9c%J;ޯ}weSNUwڴiss,-'Cf͛[YdYY9'Ngehj2U1řY=۷|6.t5&MG]@>hѢE"(;3dȐZ{*7(vu={TpBV=m~#"{IT@x灰*bL67nTpuje]§zjǎ"njub߿^;yyw} `7@ȔQhl~~gyn~,dɒÇdZre>}l2{#===22gϞ=]\\.\H%'UìY%@)G۷o]JHHO{  {]899-Zhܸqoc\\܎;"##]Pڕ+Wmۦ;885jڴi,$J ꫯ *Ӳ]vu'N4mtĉ9*99/=zNu?=ydtt+9dȐ777{W@-J ߿VFQkь~С>lӦMZZZ Zhѻw~X%"o΍;E on.$$$$$Qo5ji%~sssս|_ vZ{W~wɹpl w9PXe|xZhѯ_;UVÇ߽{AZZZXXXpppnݲmݱcGpppppOq׮]9v옿nm{nt=zhul}3رcFM>=77sε6+**zsss̙#"Ŷad2L&Ѩ>|r#FҒǎ/o/䔗7vK.-###Ձ>̺wEDd…~~~"qƨ~HV7/yyy"en!,m/^tppO  ~M6lPk*?1"ҠA[zSD9b{ڸq|MQwiРA_ݽ[n5-@''OT{Yf͚5kmk=p={㎛R,OfzдiSuIrY,ۇY=VW@'ڵspp());viDe/^T{guV#gg簰0ٵkW%/_>syCCCE駟NOO/qqqq-U 5E ,s"{o|vv>ثWGyz233t񩧞Zh(iii3g,խ={6++S'N[!6sL???2mڴz㉉ׯ衇mv#FXϚ5+99Y/^Xsܸq"~իWv("&gٽ{uӧO֬YÆ 7f͚}WůwѩS &߿_D1cƨ-+>:uROK5ٳg'''[EQ{>ԃ&@PQ5viժU={[~VXD$''g֬Y"Z[z{{"e{[nOEdĈኢ7qƵNz4{Peee]xe˖M ED;::N+{U^^^QQSDQſ{\ERF{*k3PVNNݻ7""25DE`SnnnnnnNQ ht(@E "F@QB(!h4@E "F@QB(!h4@E "F@QB(!h4@E "F@QB(!h4@E "F@QB(!h4@E "F@QB(!h4@E "F@QB(!h4@E "F@QBjP 7vvvk|c//__:j\WWWʴwuuk~ gppp _@(7^??6m(RrZ(Jpp_jjjfffaa(W)jXj*B\]]gj>*h)QB(!h4@*fjMRRK7lrJ#"[BMݥiԊZ@Ҽ]?j j#bQ(F5Be1BE b($???55533bػ/LQjrn@i%>>bX ._߉BX,.5UoTE];>>))&h(Jpp_jjjfffP`0xyyۙVBkPPPPP pSq!h4@=c;[߾}za*Illlll}k j޽{]zGIKKw ;`Q(!h4@E "F@QB(!h4@E "F@QB(!h4@E "F9TѱcСCmuAJZZ 5lذ k+ d`(hTFF@QB(!h4@E "F@QB(!h4@E "F@QB(!h4@E "F9ػԭ?%4#ڴia*$|c&deeٻZbbcc}(^^^mfXJ=ӣG{V!"F@QB(Vz*???555++PD kO zb'%%ٞ,(((,,|_ppp%w]4hP]tjX %%M65̄C+HʴLJJϯkIMMeq5%`o233qYBbVo4^LE^[}7D]4.@KOO1cF֭[n=iҤZs#yEfejl,fsih v6lCT+~NQDdHZ~jssӅ.IC9 /gϞOgukZ #Fa7sQӠ`׿^7 PCBؓÈ#֭[+brma('С_mux}[KҶ{@p_LIZ捝?%<o.n΍Ht-JV26ݮ=ʭjÿş=p9J{5i`Bm`foQmȖs>9+:5)XޯTEy^ӧ.^twhJXؕ.]ҮּѨ-3gup0ϢE &RkB͠AjÔ|_ꤳޡ@߶?ν߼{:cӋ[B܅_w;v?lj)ߟ>sl'.*|bo=s:=yuqݏ<ީgumt_CCNX7'^i5&\8|Q=k,Լgv٪S,sKJ̙?("2lM~TO"gsgzǪ97GqvQᩴ^U?e 188 ?>mQ\󋍩9G.Z،k>,6_Iώ쾳U1͊ͦ?~XJ4qopOXǦ^.'l8s4hO rg\$<%[Ν`_?nmnZe{ٌ_Wgz'P}faHi{'hrmMl[>!5ΛGtY('9U.^aGGvlxN1@&|Nzz\XRXX^B]Vi IDATt8H/"O>yzN(wN"=w\s+򑏫iPmpJ=>&"S<92Skҫ9X("[|97݉3ڦAgk-oQ{IP.U4jׯoO'Wni=o6+."͛_PдM[ jF=V߈ЎKOTP/ )7[D/viҬl~njԲ{ʷ{yH3=9{WՊ.k)"EE_ܼ{oZԃW^6oS?fU;@4-ܷ*&B+dhH۲'3 /m&"1@Or;Q_,T;gr;8w^;c171 ?G?L;еk=n]zR]NT(+XxIEv'$|ը"#>E?(.8dl Xcw^z@l&""O>yzqFS<ܾ15>\b6j)DPDt۽ɸ6aty~zf۶RmE}Ꝅ/m0%Ww:B jÃvdx8ljx.+;lM%/oߠ5|u?7OH~ƕd{t.z3s̘ؐl9x?qTjw/ɓ'33XRh4HzzΝlҸq㚼ʩ_~M+^A2?eps<`VMNzˣݶm}&ߵd3YDӧP딴WAĭa߾}YYY.ք\/|PѽXDV?O<ѱC%bmFA y vNp*-ŭ_@owLLtKJr, k2Q .lbĉ/^C}(^^^„ʵ={Tpb3aXDgϞ3999"wވQj< A V-g`˞-kOWג2:tȨd{EfY\-Gksf2yr5kEdҤSZeۻ"#)6sѢ[_ILtۺ5F"ұc[owi!jJLt[Ro@tn~wHxx+*)=95d)SN98]*@TJ{~#]Eұcë]Ee@QLPoMw7kj@[F@QB(ݻ7111))l6iFۻ4G lذaݺu;v0xw}||RLݼ[l1S5jTnn}+nmnzǎۣG "FqǎfJLLܵkСC]&pbv~e˖_M"4pgyF}sNUr{xxJm"پ}zСCV_֬Y#">>>#Fw9@zŋO>l^{zo!@ D}9qČ 4h+nqB 'O1c̜9>!쯠`ҤIG{wܹ!쬸8222**JDF`%p3dX̙n@?z/߰zΛ74L|DGG/[LDZnxb pv3w\$"wssw9aB2;vw99ػhٳgggguhY&LEB }F !!a…1 B0eo؆ef:!#"""&&U h4@E "F@QB(!h4@E "F͉ˌ3YL|YYY(o(f7uC:I $tP@AP 걠(z{kAAEx((xPHQM@$H<L2,iot?W'^;{dofϟ?VPܜ-[9s&55;((uֽzpv4Mp+ZW9дmjvľve֪LD233nݺu?'1bg@C@ NjltR|||~~~ff?ߴi.]85~~[_*+[|")37:J>eΝFwwAu֭[nu=7WDw`vQP2%+++11QD:tP>}z'NpB^^^TTT-Zhq5u̙SXXh2|I\u>󂂂{キJé;<- з'OĆ>၇ySEdTQ-f횵ĺČ vۍnd}+{aӽݼ_BIKd'"r[zF,:(!=ڰk8po͘ڱcE$88_"MCGƊ٬W!MLL3's^MU .ˬo鯾_ިMN=O[%^}{֡Cdm:v NKվ 32krzI=7o#F]gɫVwhYB֭C[ۜx{nwڵk~ :?^:r$i1j"##/..NDx+VT{@nVUDn.UUs̙:u~y'OnܸQDnO?T/W!Cݻ}]yǪzuvk^kî-T .z8бǾ?5NӟrWۻv=|tmgu`¯|_ݧ@x&~eVڰkkR"1Gӎ.>x展_7&)3ɈD$%'eTDzm.\ Y?gςz*l99w$Zo}`ܹ͝ؽ{Gy'ql'W>dɵӧ7jͫ-K~(9~ܻ3,LUmLk6[w{./nZ@OLPPmHMV{-ZgϞ;vt/#™v O81gΜxj\p߾}it%222--->>G'|2x`鯿ZDEyzjKӣ] 3Ed޹Zޱu_ѻYo_yLDQ{ꡔCzr|[C48&7_lCwl":vT%XB\\f{wk?%m=f%r?{V-(;jԍ۷ݍӠ[F;w?wԨk*t .fАn :p u:{n_fdDU? 6m2IH(~%Mu@s^&ߟz-"w5Y>]f;:x8MUEluV=j| <{e˖:B8ݻe=z /P:Uի5M_|'t|055kӧϦMNxQ^y>[5-͖wQkZP i(ɶlr"m_y٣*f󡸸3|S[R$'!i0ۍo6TJ.*SCrgAAU|0 jƌx4D Wn3=zhdƌz3,YD×'UfqwCD̊;?q:s#k82+oFO"2Ж-%j)"6J ]U}^:M" #Μ14IS4("&.NEbDmyM0zI4X\T|||:u$")))uB!sk֬Ylٳz[nEQiӦ=ӎS*[oO>䓩S&''WP[n]vիW媪.ZHD2V6gk.7h*z~7;c w{W@WŽD޾~-94m*"9;ٳImE2y5mzG9fc8g< AA͚HީSuHå#G 33E$-=҆'TL&IF%"Gԟ6ңc MܨGu?>m& i9fLU^1(fֵ0o,p&ww-Zr-Ç={e n:cƌ^GrVYnѣGOL42: OntnڴԩS"2|p_Ux5>P=KeA]NVa>a/>{{.ap/yD>4:4RN|ť<=fO5*sO%ϟd*coY z!&&#'+2R_[%qnO߶MDG{) '֤1V9{[~oŪJYZ4MD"ưO=4>竸 NG D?SUn6-Z{{{n߰aøqnE*"}С9rdݺuz_W>Rҏ^~]_L="z.4)&HŃnIl.5ZR.']J|ssEDۭ1;bW9"TuMQL&-E$'!!k^[No!&"Y[SRr@تU Өm* *1z۪Jϐu:^Tg'dE ΍7ިoEpرҳ'..NjMg]xq#F0~o38vȹsm$ o9]׊Ufܷ:s[yFN0󧟖eSs~~=챸F[q*ǂ&$m,"=z(fsc}l8dS% ZR}:]QFg|72GQl~%~U?z觟~~/6o,"[n3gwǚ~{֭6u:3@!} _XXX>*1L7xg}6qD􆁊5J?/^,";wku[VqRj_D.]QB>NؽhΝĖ!W.=e1mF[N8,Sn-"ݮﺡxϺT;X~ȑCUިmۈqŎ;v G7'B slbyTUnJ4mYYY' TGl޼:tHNվddd,\w_+fDDds_zn8}UP1zlzQw~-\hj|=H9s~^ĕ/YTaCu[X_EDfݛKK_%F''@X2|*q[=,-n)RPϞUkX̙3gٲe"Ҹq^x.^[p۷w=## )ÇlْfZ[l٦MVQ]~~~RR҅ RSS5j֬Y+)/!))i63UUϜ9ӭ[&M0G,wA}r9}GށC; ,G6j(}Yfo߾~YRɓ9l f,͛,YaFF-mf4gcر8sWxxw UU?өS(W$?\|||E 4Vܺukr\hP ß>Ed۶mFD:nϝ;WӴ޽{T\zg޼yVk׮W]5@X Uis=fټrJ}de.K?f̘~MDfß}+ 5Mv;:tHDfI v<<<:uԵkۛ:XVK"[o?>gΜ_~ԩS999%22w#Gl׮] _=..^hhW믿f? ,//ӧv} ,hٲG%{K~;ko}_+|nN_DEE]pa۶m]v򊳛Z<<"b <...,,lРA9991j:tfʽS@/皦߿?>>^DL&k?kzY,=f;xu]'"'O.B:}Ds?c={> v>m۶W}_ڵ;xf p|hر1""gϞˉ'*a͛gZW\9|ppaÆ5'LW:Ժ*´9s.=N8yIQUUz֭[N<իW@h60X,r//;DDDȥK*`={#E}||Jt~ kUsƷ۶meƍkY'-]?>p@aÇv)'>Ɩ. MJJ?l6[]E$ ĞYEQ {9Nj}%b'Կ*Ž;{*77믿9rQ5L:u*RM4OƏ?}t#""" ?))%)"ӴiӤ$՚Ю]9sFwՖaÆM25gϞ$r(8KM[dIAA)SXSNk׮uss[|NLy4شiә3gvl6M2q1Ҙl>\:e~[n7V7nhԯVITTT=nݺtÇ뫉FDDӧ_N4m駟SzǠjݵkWWؼy~3H"g9wVDk3M0ae6ѣJE _222<@8W-$qȬY!RE^3V}7zի׺uDXҳD۷{ٵkG}T\m[D$??xb YYY<S3fHOOw]l6:[j5ṡznǎ#G\pa ÇTAoq">tƎ裏~ꈒZׯaΜ9fl5Qs΅T|MMΝ;Yɍ( >|ԩf͚Vjegg_{/kdһLkۻwQWv}a!!'U(z1BMe IϞ=lٶm[+ӆ2zpsj͐FA(M6 OOOΝ;wܹJϪ>@<թNP#_^_NW^@XWV\i,`4i$J ܡC8fhhux (W-lL3"""""""""""""""""""""""""""""""""""""""""""""""""""""""""TLG ˫===kZBh@+_9 &E $$$*F bUfXXbka󫸎_ttt _@ (111M4)BhhhE Z(JttthhhJJJzzz~~(^^^!!!HQMj*Bh,KTTTTTQR^AG E E E E *.M5ϟwv"5M;++KD233k9"" L t 4-ZѶmZ&%8 P}@ t (EeEC!P=2 MIIIOO/((4E  X,nU]ilMΝ;m25܁BץiÇ)999???&&p*ɓ'I@JLLtv+E tQ.\pv+:e#g7p oJOOwvW`&|g7p o!ԭDmꚗ OOOg7lCJo{t94p5 s+Bz]THH7EY,0g X,nE+::٭݊r1%hVfyϟ?_\Ahh.MQД; g(W@@@HHH)j B,KTTTTT^1\\\s]Bvv!m߾Gn+$$$$$$8 Bm6g7@: '`QpQBpQBpQBpQBpQBpQBpQBpQBpQBpQBpQBpQBpQBpQBpQBpQBpQBpQBpQBpQBpQBpQBpQBpQBpQBpQBpQBpQBpQBpQBpQBpQBpQBpQBpQBpQBpQBpQBpQBpQnnUUvi%KzE)]b6MԂ[d3پ9fg7Dž#Ɔl5 ps:jWi묝>!f135U%*9R(݋Xz맻B-CQE4UQXoI<tOwӝ!")=Z{VgG P} ?#gEMSE4i)"k~tIqr?oeၕ;J P}+^塨&">XQ-(DQo9X<^P|\k.#&UUt8vy1QDViwjif9pkj̺Bdۓ2|}Aǩn>PQ)d( %)n#C]*t RU32jRWj"M*T;8X z.[T!Vɍ OUŮJq`'EV 4w]ջd LB P&&vyҠ}/S''Rho-Qǒw[f\!S5PDaI^/1B7RܥP.ɡsCeԿҸr.S[VѾmئȹ m^QnCcCW!>*vr""Z+N/NɼeJQlv]Bէx9 ѥ^<ֻh#_l̍o1ȃݔԌܢAيg 2[S*fW% |nWzTD$%[oqymW!P4M(OlTlrB2)%"r"M{Pw"UkM!"}JEI6WwS5Bx!P)B^4dqҔzvxIEꄺ KINA[Jb;# 58\>|UUb*Ɨ]SގsDDZ&W:T|+,*1ԮDdҏ0X (h׊n]s)ɚ>!P}&vM(E_jr.yt<)axj5?os߈>|Pc5ØZPQdmJ۰D+//屯ƄH$Py[-*"X/ӆ:j1E_yWu%>*vUb)U5]BĮjVDŽm>Ow6LK#GD -*?*u uPxŗ~B!jFfW᰸\RI?-- VN"M!eSJtHvӗe_/0\Hv=دsUS쪦(&h"&""i"?&gYc%55> \jvŢ0at \qtnҮDJFr1GN%ͪ]=JȁWyxx\ P}^AQDSP&̵ʞ$ٓv#{kj"bt!@Z.ŁDBA#|;(X|+?@w"ry;_T,%ouC8@5)QXC0ƈ)Pъ?MQ*IH PMfmpYD.S)֋+ڥmhld2i㼿~vEW4*k(N÷"65Ĩݝ*HWp UKD4.B]*@Z5c0E8Zbs<Ǝ&k"]6hVM,@A]-~G/e;Q.68W컯xzzJ-@MZXXhZVkaanz^(b6=<<<<<+!jd2yzzyzzjALH>- 犢豰#E BzNTD_ \\\\\\\\\\\R"$vIENDB`././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/docs/source/traitsui_user_manual/images/ColorEditor_demo.png0000644000175100001730000005715300000000000030073 0ustar00runnerdocker00000000000000PNG  IHDRf$N pHYs   IDATxy@T3Kn,**K*BaQRf|2s![>Xj~*)sI@h.i %B( 8?w> WΜ=w4^9\MffXQQaJr y-an#::DEF]- DX *Ca2DX *}aKKK|쫯:w+b¤#GXXXUd6JLLܹgJJJlllL\(JVV*;j鯷 .lڴԩSNpvvիW>}lmmnw~iiif)522r…Bɓ'+.\v;;;{֭Z6$$$&&&666--uȐ!^^^p[ 1x`??_ݶ RRRfϞ]}&335苉6m?Snݺ}ǝ;wB{\vժU2fddQ\ӦM[vgDDXRRn۶qϟظqjqƻ鯇 cǎq5l0))e˖ @-=x`C:׺ȵsN???V+pww߿VRRRۗ0lذ !FhѢ .p9zhHHwm򒉶NNNG 5kҹP&}6iҤ(zQǎۣGSN͛7￯lT0ٳ˗/ڭ[~:tHq…_|Gٳgnuʲ_۴iޯ_:|}/eee]tի׈#W~m^^Ç ӈB1B=##I%={BTTT)2TYXX4oܐqعsg!DksPaYv:|{=z?.,///++j7njSNݵkW^^lOMM8qnjsrٞlٲŋ+Gtɑ#G8pB>\̙lٲ,%77/|u]|[ncǎu}v!Fqssm_tiRRRttt嗜;wND"B-ZXZZ !_+{Vz('(((0] +ZYY Jwwŋ>}??{nڴiܸqBRooK.=;wܵk׿u !V^]?hɛ7oZpa֭ׯBg}a!СC7llܸqȐ!B-[,]T~/;|S|ɳڸqcC?~ӦMB}6h@ilrϞ=...M42dKvfΜYXXwcnU%%%wV<S?322 <<<<===== ?%KÇUTT?/!߿vvvXXXqq3guw_~Y֭# {^x뷮^*2d͛C?~|\\\PPЬYdתU+rSNt.+++,,h4AAAOP?zheZ|;wklʻʕ+R3kV|I&)7l0uԮ]ׯ_7PӧOWk4___X^<|/$ծ]?PSNB*exwuoT^7xCQVV(gϞ;wYft~WFQz Pj4irʳgϦoܸQnS]״iSZB_~K.jmڴQf˒<<<Ǐ7R1lذ~)77̙3:w܈#e/҉'*O$wMO?}rssܾ+kkCr]ws4cO:5::211Q^ EyFB9rDѾ}d2رc5O2ҥK dE#L2EyժUsBBncd_-[%fz&O\^u.P](iԨ֭[+WU} {׮]`fϞjr[Iw]!SO=u տm۶B777ʻ29sTPeggo߾xS!:to߾{QF%((hrw-!E4SiћT~<ڵkW~baJjg̘{%xƍ|X9*3s3قEggg+mxxn^*XbEpp٢~Л6AAAZZBz3={Ǐma&335҅ :vصkfܫWa&O,ʺt钭m֭m;;;޾oԽs"C? -[6w\KK˿F.Wx#Grss[hѿǏ;N *ʫ3<֦M,PnDGGVjZX=εd _ӦM UeVVV[e{_oeƌWNMMo-scM7mذᄊglZ^ʕ+;vB,_ ekkb !Ē%KjGrdOO[]3g^ 3y1+˴)fvě#g%rXx ,Ӟhe@iN9pu Y7J״溅dn!KV$zܛٔ h.a2DX *Ca2DX *Ca2DX *Ca2DX *CXs+**L_ P#4Ma.]F1w Sc-,T !@eP",T !@eعsgOO*;ؘ*Q^233CCC-,,;[-O!k׮y{{gggoݺU]ՆƦ2˫A^z!w7+oРAPPPJJJQQٳ/52O߰aÏ?xf͚ӧO>#GҥYJzٞ&LPKJJ}m۶)-IIIIIIk׮ ?A֬YSͽJJJB+w 7n܇~۲ejj@a:4eʔׯ+-{ݻwe˖-[k2K!ovL0brѐ!;C!߿ӑ#GBBBlllf͚t.,,o߾M4[nݻw>|x۶m痚;VVV|yCvGuvv(**ذaC׮]{٦M#G={V>|XyFۅM}ҥIIIѕ_r9$hRqXٳJCyƖׯ_?!DDDDAAU0]MHHpbРA۷U/r{ho*++YVVV?O?t\\\iiFQQQO>t u̙3O>ƍ۷oꩧ222*0""78}27|o\r֬YrYQZZzS!]|[ncǎ_7յqƆ?M}mР.[lg&M 2ť]v3g,,,; Dy*OFؒzv믿t'*kڴiӦMl>Hնk>sss_,XPZZ` !&L0x3gL...##c˖-ʆdǎtRVNFN:ҹϯP>%CU||ѣ+**ӧOܹS]|W\޽Q? 6yFNL':L'&w;>>""߲e,OנWP1۸yMy5Ӊ^c/6y}Wݸf:3t}2bf<YYYAJ*=t\S+M>}-[7/ddd(}ÇO>y{%Ժu]&&&(-[LHH8sL޽ ,ndgg |SE}7B<88XY5jt!WWW;l0wwm۶)3,O,eX+ Qfkǝ oWk ~wpg.Y ??ߚMp}}}~>ɟs~5mMa8φfEh777[[+W߿:tPB}{ں{UFXicck IDATq_w911\ήpHYYFwW~zu7STTԨQ#?ۿ/V=tO{@e{ҥUv˗/W\1n8y>O=_ZGEE;wM 9 ;~*@L: +Xp)S%%%W:tK/$kvݫ{G}$'bG}tРA3gLNN~g>~xyHںu_|իuuaݺu†WΝ;wvO=78i$_CCC 'N.\{K{Y[WbŊB7kkk~dx4!QgT={ ",~dYX!TӦMGxbeYO>֭[xqBB޲AoĉZ%X[[ڵ+((hÆ \ڰa޽{/^cǎzŬZ?߼y!K7,_]lF^ Mt[*OvcƌYz3g|}}MvͤIwr5kl^{^{ŋGmѢE!O>-#ڈ#F3Ir6mhlUrĉ'Nvf͚ѣ+N{{(%%ANNN2VKܹsΝw p!G}n2`ee0w˗/geeխ[QNjڴԠ̭/jEÆ 8 o~{Rk܌3V^kyÆ k*~=P !\"/_KS01DXUO!ۭ֭[7;7hVVVʾz,--w̔UakkbŊq-Y䥗^m̓===%jyyB  >|„ ̃kر>>>vvvzrSkZw_~qժU{!4*oLVK܅Th]<P/",j?%T}P",TsP3h{bȍ7"o.&*Ca2DX *Ca2DX *Ca2DX *Ca2DX *CX0(hq3yޮJ4yN4y2˰"LpTγyQf:fz÷4w `53]4n \ܜlgزzm0 INsnqׄc\8ky .^9b8>7y8l9γOX9γOrX9γ0sgCam۶ ?^cƍƍ樂3;ƍװaä-[VS3DNNNtt zxx*?oBVZo^(`v޽#l="xwt۽d߿ӑ#GBBBlllf͚t.,,o߾M4,,N:=&&fԨQ{>qrGy䶕_O>?ťK};QHP?3!!!;;ܵԤݻw !&MW!ľ}UV) L>f!ɓ'ub^tI>x z(Ϩ~2?*Bډ'zꩶmۺ?۷=zÇ;vlӦM߾}sssu_{6mڴiF^IӧO{{]v ! ;|V5՛#Fgdd;99齤gϞB4Q* ͛2PQ;w,rm.Fӧ~hƣG>f͚͚5+???--m…Jٳg[[[!nܸOZV---?^|[ncǎu}v!Fqssm_tiRRΝ;'_"Ste!D-,--ׯ_=dl=glyBDDD.P"+WL4)77?9p~hmm]PP0i$ӧuɋÄ}Yjjbٲe-ZBǎswwBoرcǎS&/űcǔ#kOB6nؐϟߴio߾ 4Pe-[ܳgK&M Ү]3g^?wg Dy*OFؒgVߧ~zE++^ҥKΝ_{իWk4Nh4 6x7KKKϜ9j*!ĉǍ'988mVfum۶m۶Z>Nѯ_?^rSNt.+++,,h4AAAOP?zhqklʻX~7N<)pww['OvEq)100P_-_|7nhժҥK e˖ {ٹsgB4k̐ί(_"AZm&MV\y7 #""OywY^ӦMpxP5PiiQFwjذ?mwر|r!FYjUTήo߾w[JJJ JƢE!? F5:t萫l6l{rrmoύOywY3 0 ?Oyߧ+;\vڵkZlr)S}Qn2F#ի妹zu7s̙3g(JZСCwܟ<PfaGyy| /ƪREEJV~ׯ_N:O5BCCg̘!pqqٽ{wSlРAx;OywYxjA]v*++4iŋcǎ=Xӫ{;" @e jfa2DX *Ca2DX *Ca2DX *Ca2DX *Ca2DX *Ca2DX *Ca2V.\lllƊ Wh4lksp$>(5jdn)''άa2DX *Ca2DX/V]n]EEŠA:w\}璒}}իW ^JLLܹgjD,ʫRfffhhw>tFkk={ӧSN-..7o⣏>2¦]6&&&==^z 6mZǎr􀀀ٳg(^v;;;{֭Z6$$$&&&666--uȐ!^^^p[ 1x`??_ݶ RRRfϞ]}&33zi˖-ڵŒVPPкuk!G}4}tݧ.\_TTTT_|100nd]`|wiӦ]3""Bi,))ygmV8klܸq5ݸqCy߱cǸq6lԲejjɉ#M4ɉ߄}݅ ~KKKk:G B;^^^2ȑ#!!!666fR:Dշo&MTEoNQǎۣGSN͛7￯lTY~޽[=//>Xnjvoo7n"`MIDAT##R!|0cƌ;]u&MڼyÅ  ݻwB?>,,N:=&&fԨQ{>qr !yVbl=gTO>d.]7`bF ,[BG8K^x1..eee"##ɓ,S\rɓ.]O^^޿o˯BOO@800ڵk-%%%))ƍ+--4i }VR}̘1BF9?!_Wє+_Apߨ 4ȑ#EEE'OTtҫW#F߯ssrr-ZԡCnݺ >K.]t:u?Sy;w8k׮ÇѣGΝCBB+ܷo_nn{̙U / B;w*^^^J.]?x`'''??g\اO{{'xBi9u]kVdd<-#Fmpuuuwwwrr{IϞ=iiiJ U͛77d\c<;::\ ʴSQQ8gΜe˖eee)-_~믿iӦ\2;;[izO?4j(m/ڵk[xqv!L:gu#7Zmaa_xx2VVV}#GV ///V[\\?=uĉkZ{ӳwﯦ}v!Fqssm_tiRRRttt嗜;wNDw˗/ !Zh!_~=66VPOQPP`@j,>|X>h۶|gmذA1t 67n2db˖-K.U^駟B#Fڵk5wZ">>~޲͛7wIV_ꀣGnРu '9Iju=ui!kƍ M6 !+ߗ$[lg&M 2ť]v3g,,,; Dy*OFؒ;+ک"lEEEHHȮ]={T]zuɒ%B!Cl޼yȑO<ĦMz- *))G~a&N2l0''cǮYEBXeK^~]xb>}[}][bɩSgg VԩӡC!Dfff卷&LW_Pwu ckk;~x!DttZ91㷭Fddd!5kfHW^yEnw UZI&+W<{lzzƍ?,iӦo/lVVڵk+i&00?Avv\$P:uJ*Ν;#})/////(..>{M7SJ\jnmqq!(}իnkk[^_Bjؿ֭[?~>lׯ7z;֠|SE?xpp޳25jСCʿ|}} m۶ضm_|Pͮ[OjK.q…3gΔU-9%ܸqcH@CU~!7h4<'|"mY*ceS*++h4sUZz/k=n$!)0"U+H㭣U^*E(Ax -i+hǽh^Q PEEXmI ) }dw6<5yٳs\{{(cƍ---1??-ڱcBwӽsnGثڽW_}yŊӧOwΛ*gJxܹ?JJJrsskjjP*!ԄBjS*Pmm)5ëɓ-Zdj LgϞXE훙 .XXjaȐ!of¡Dz~M7۞c8y{xE#l9sYuѢEw}3kjĈ[lX~}ף}6xҥ7xE  :[e&HxrԨQf m۶uao5e]6r7ޘ2eYy$m'.#Gta7mtڶ]VVuքkkv1[y۾ t/9ٱ;"s+?"rɧ~9o_C{G̈́van})//pJN'~ {0۷oohh0U&Mro&t{w]w[%&566^uUgώy;n־3t/9oqL^wԬY  "˖-3_K4Šȣ>իn*8cܹYka&_޽;frΝ;{UTTL8QDΜ93uqSN{fbwܑ2a„P(`ΪZZZV\oǯu>7yРAo̐P^^k=z}WCCSO=%"`,M'Dս]v n\C͝5kBy晓%%%'OaÆ'N޽駟9sg}͈7M3_jҥ/rmm_x;3~~̋7oܹs}}=3ӦM `3/f˗/?|p(yWF]]]-"999-:#''gر"FDz̙3g̘1iҤ+V$tȩS>C3Ɣw~%޽{&M2SYY~SN9sf֭cǎ5csn_^^~W8,V{D:Z<>ifo1RQJmڴɜ뮻5k8衇~濈?~irʄ_O>_~"OtojB6{leYf_#fĉlVYYIZP(d?z k׮M2tcT&`pΜ9"v&`),Y2c l輼 &rsr޼ys0lذUV>Q.]lc+" .?须֨ؼyYWB˛1cƖ-[ٜྫƷzD 33 n3g w_BYYY^sOfk|/^dyt,!<+VY,O6ӽn߸qcSS ;jkk VRWWwرvZcǎ痖&Vkk9RWWWPPp饗6,9碩kijj;vK/Y|>_ҡ` 0O=O~_[[{^=z>8uԠAFy֭nk׮Jپ}YgPѽ'OzWWW|!gظm۶$n{13lÆ fg0ž9sf… z|iӦzzg@GyeWE{ T}U",Hk}.OkSݝ47*<0!n=V_nfZաe HIJY,i V`eݍ0 ЯvJ)2 8CU$$!L]EFV| [ŝJLekQ&])%Z"*_ZV"%!A$+Crv*+/TEL ([k+l;Ӣ#s:v91.+zj ]jGiǶ폏bG3o-HpA(Dk֢u/Ϻ%F˲joF]%Zk1uJ)WI,ΕRmmblӂd!hY-DXg+ek n-ѵKm[(Ǫ`!s:Ƽb>D8" BI.KȶŲ%:ktM=e}UͰ3mFDXl-ۋ_hfvErI4{闰+zՎzFW4 tdk3 k('o|^=z,ʓ|WLi ɿˁcz OA}:֨Y{?aJ)9֠=bfuϝ#tdbK菺L=xT"27~D2d Pp@׻; :Eeig ЩPD(y)_6+D+Gƪ \{7w5re2|~lyr>x\R] 8Td;e'; KȌšU`=W#ddMQﰷہ{*ՖvCs.S nN_~IR9j2Y=KیtʲLrM#r*mloLDDWJwt@U C1D?:zuZ>?&<. k"i^)]:ro#mJxQZD)## ̒=r"նڽW+\۫U$G lz>ݓ"(f-ڹ:h$&tڠa–G~g'CufI_\"X5HDv߾[tE\NUq""TKW,[)%/[l[b"Qj%"uM{+ub.(,@Vm()Li rOI,C'uCơz]>P}3"28)^0 ;V;b V~%GeHxtZZ%z#Cb%5GTdxJ/yN[樜>""_xji ƏK{7H~_]߿Vw|=|Ԯ[{a -EW$pl*W}]ىNUv^~q}*آM%K2N%? K!HGv{̀SE ]tDBޏnWW&nsYzH?hp {3a55}:$"2Jn2eK܈1_ ґeek˖6Klf]mnĜ47,[ۺGuz>`|m7(LA"{*R&9^残 >1A+;1\gXb+vBɬڲgŲUU'T߻^rO%9ʪ3غ=t6yuϒœU_ftÇM-,@Wl,[+̦ZZn)XO4ɔeR/'ώk^ lf>dD -.9\Ĕ=JD'; Yd2mtwˌbFOYLxNcd8֭EI?''xD9{UN|3_\Gl\d[T ٷOן  ^P]FDwrN`\y/Oѿ'L8:75bO>-?r,($ȵ.lJ{QmɞS~Χm'8/"gݐAb:$:"{E~85^?ZX",HGN娈[7H\gl ڙUr"U57n-mJ%5Ki_Qм.@lhoؖ\1O |E##qt+(q7Yrf#_*fVb+89Q"e}FDhK2~%!tT~I94c>rJ(E֮Jm&Kd'253՝J#MUeM ]Y }U"ev8BP([YXG(~fff W3 322Ww%ў/_m 8 }ƀ-a1DXx Ca1DXx Ca1DXx LQ3tIIENDB`././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/docs/source/traitsui_user_manual/images/CompoundEditor_demo.png0000644000175100001730000007401400000000000030574 0ustar00runnerdocker00000000000000PNG  IHDR2xm pHYs   IDATxyx8wOFӴM˾@Y |QWrݗ+Wp*"ן\ (UDpC-;-66MI۬SblK)mv3̜90y,sUSS!EXȷN۩pйQH3FN)) K@-9!m:P knBH(jg$9Y 6Pm<B 򱰷DdoǶH,6Poz8ȷ)`XBuAmm2BB%;V,Hž={}.%.#cB! ˲Ǒ}d2\~)S " K(xzaaXeYr\.WT*JѨ;Q?DD%!<ϻ FtnRSS̙3eeen;>>d2EEE4 !"a"ǓyW79 55555l8q"??_+::ZRE<!`XBarl6[aa.z|~뗗w>}k!$Px9S^҉YYYYYY[l9w\Ϟ=u:]r rn})tMnr6oH^Y@ڒԝ9s歷b&))g a lv$۲efS($?ҥK=ZUU?hРaÆ=z>LWX,MNN~WtF:mٲy%;##c߾}JyvСiӦ$gggؼ.'|r͚5?ӧܳg6&v_ u0v& 1顇C2 -IT||o}5ׄ#qc&Ldw!ǣhB2q}\}+qqq!hBBʕ+oƐ:kKR4a„={)&04M#e GHXyyyΝLEE~R 뮻DkKR4q%β6H<11֭LOTP(ڥwIgϞg}ĉd^5jc=6r^naj(F!q7LH<--,Q~UUUyyyXR[[K6~[n%p|SN_Ú$P<ÑxbbGʨu~/3gܺukIIɶmf͚.kڴiaBbx("%)J0SO=SOG9rȸܼysGx" 1luuu8^%Ael6nݺbRܹsIl 1;L&S(a8}\.I [n)))aJѣGxe]x`!OST6mt-t0M6^D]U||unÇKJJzݷop_wǎj# K(,(IIIΝۺuIڝ֭[e2YRR^%i2AF<.W^~ƍKdƍ~W^ثD}HT@eeի yyyONNNNN;$ #T*U||BiСCG#G9rjaÆ͚5Kc۷ PZZaAk׮_~f̘!Pk~{kkk{,bٺu ֬Yc6~ !BK]Xڶm۬YX1c\ve&o-((˻ꪫ~駤$k_yr} qB! 4|ee2٢Ef̘3̝;?>wܦM|AP(XQQoZx eюeY$'\qq߁#wt: 7778X"4nVruw;vǏ\.2XW^ }x'W^ wkFv}޽;++  @>ꫯxĉ'\.Z]VVP(~mޒj%-UUUeff<W_^Vr;y(2j9p1b!DG\%0?xmk׮{233~sε1?MQӃcmgϞ/R~͛7^oaaa=tsbh{ ~~~>i4m۶B.Lta bcc.]zĉ{[o%(ƌcX.H\\\)H6dĒ`999'n!yyy-^СCdng98^>_ F!ې`z~ԩSNÇoذaʕ TVV>7onjJm`G_ |B'ްl>}u]p8~n0Ly*xL&ǎk308p-5x;w*/2xbYB%W_}_~}=zhד V65o>A5G*I ߿?CG\ao&iiYYh"RqqqxdJFӧO =s1g}6jԨQFڵ+3??ʕ!=BuB KÇ'fΝcǎݲeKII 0'NXrq3fw`#`ٲeO>]/_>}ti^y ѣGMm۶曏<ɓ'}>ߕW^I.((;vSO=u5p_ !DK\}K'u]ųfj~̙3`gʔ)[n]z5y?7`ƌw}w+'>eͿ믿QLL̢EJ%1gXp(BHU[޽{wqGLLL~zxEPdd\\.If?68>f̘gfft|0x l 'ܾ;?ppApM7cǎ OQw܁1 !D7P0Œg훙"sC/&kl Fmv{RRR+Qp4W !Itxfs`}taǷ8f EzzzЛBBIkK(8cYLTqi,'-(8 Bٳg@`.:N0 LK U:%w H ʐ,Qy@|CCCCCZ5 4 rfgT w !X[Bt:u<@Q:|$EQ9 @&5jԊ}՗,!†xz k'x!;YHYIjFEiU)lC$iy<VHx'b9qgc9::|ElTrȳjpA:[4enwll W?T?NQdOぁ RO^+`Y֍C\0,I>ÊxhgpƏǭѫ U?"Y "cBaIX$*AcL?jK PE5ܧq|ם##1  |cPIT(|~2)_x?? Fb1*! aXB!}K@>Q<%:![Ba9Y򳼟IЌQ3v)i(}cMΑ@OI߸ꌰ'LE彨rd,G>%IK_Eu.`X& Plhȭaa-!aXBi2q.xg:8-! aXBs|s7PyQA EO $bm NLPm &7 l`m  +jqw*d;8 S}1PE j ^IK.ae [BgQ,GY /B459ݦSO{LcB֖`Xη5B]O죂O]O#=B%$RxǓI}ZmcyzaGR o 9oI"$'B=?n(Tj7)z@ghx%:' KH0, b7(x8ja뷀XM<T[N  ˵8_?0Qm0[KuFTDZmۏ?zYy⼮)}Ǩ> 7bjU*UBSjjjq>9Ƥp##ݕJJRTJR&~S:%-!LV ZԓN*WHEmwuj;!$8l@!$"B%B"a !`XB!$"B%B"a !`XB!$"B%B"a !`XB!$"B%B"a !`XB!$"B%B"a !`XB!$"B%B"a !`XB!$"B%B"a !`XB!$"B%B"a !`XB!$"tp8p`²CD٨K`BBH$͆x!DM%B(2BHD0,! K!DB!BHD0,! K!DB!BHD0,! K!DB!BHD0,! K!DB!BHD0,! K!DB!BHD0,! K!DB!BHD0,! K!DB!Q$<93ge*@\z??$5DzBgSrGuunw\4V6 z^lJNP555B fE2עe 9(Q@Q<~ 4cf 7β$TUUUTTTVVz^j5Ja<r8#99d2uM,KNaX:]'nL)G<>h%79}:1ð$ s\Jy*0vHlN:H@Q䖲,''ȤCEe9Ǵ2DAA>{ٳX,!arrr:"BAӴdu82c ϿꪫkvܸqO>ݹsÒS]]M6͛׷oߎ_9$-kA$eW-SWoM7 @!g7 dZ`x箸]ል շAbccZ9㸩So߾}!F,8bccaCCCIIIBB^Ű$ Uم&8.+H_\S|hkZ0(r|_bf%a[y+Hj4M3LkU {Ξ= 'l鐰믿dJJʨQyZ\ %I)<=kz&򿗵зKm e5M(h4,WJJJ}]+**8gϞ0aBh3ֹ[:dܳ[^^qƲEuw1,I݅38ǠvxSl?SѣG0 L-R[bY6&&f֬YGVTZzuqqqnn/k#{ð$ gؖo +/щ ?+[@cj'Jz5MhyQ:99ozŒ7ܹCNDұl?LvN8qb\.yK$ rL&;PY-7~;i-w qYQQQ )9-3gΜ9sLq!]u06IT@s =d2Rj+{bTOEYE+rG:.(̟?ꫯni}}Nש X:cڴi,N4gii^^<{켼}ee2JR*k15뇉6..$&&&!!j ڲix~,sS}ѳg߿{/YGO.\ڎtΤIJJJ֯_o̙CQTp\{]֖$,w'Vy YF̓/D[쩒,hۭVoS9y'zvӴ ~; ,KbZ1bʕ+ t9z*LO̙3'11T*'O~Wp>Ioej O(:RbF.KǪRhY,=z$&&*Kz*}) {Bl6FBz #O%%%-RaD-Iθ^>Zɯ?ɳ* q<2r֡=҉$-ITy|a宓jOE]|xÆ @#l,]$^7f}G---.٬p8l6Dt-R(4ML&p8Z??'C/^k>}eƍx:#a%0 ,8.!!aI l锕kjj|kfɓv׮]ϕaXs%$$ I`HHH8w\+GrG΄ /~wގg3\`޼y8e!l\eYX`A|||`\.~ǎJOGlll.cuuuV]];r&+JWQQ\u.Kkĉ>,T,ȳѣӘm۶*K"aInwZZZ] _Vk+%%%]k ѣGrչX.ǽk,<F `ٳggBHr|-3f]L61)*caIhv8GÒ?Az!\֭[WZZ ӧOlAQ̙3gΜ!ˁKKC iAǽ^o&؈' :`544q:d7w|{ -'N_JJJH7ƼyȴR#`e9 UkKc۝Ng1rw9Nޖ)9{w1cFv i\P+,8zСy4\Rzg%Ih4Fӑ5`: Y|`0O_'wM:ջ G;[h…f^Ɏt(_z%ݾlٲ|0˗zW\qE$N"..(11Q.wu,-{=2d0`&PuUWuw[$TFGQQQ@&hwf'~gΜYf{UTTm62{TTTWm $LVw]Te<}>_IIMOOoxv_4]vueZ'H޷oDX:O=TEGG/]o߾ȌaߒFnZ~?˲h.'O|~j =n_*.r!e!ksN>}>ѣG7)~]5&֖b#11QP\yէN2ة*X.b&xx'N9s,޽{KMs$9fh4*4D'>ŒB^C"fX:C$'..N.&FVU(k2UArH+>aD֖$Zٜ֚@cw鬩X, FQV . Ḛt"Òvj5^u8 䀸8|܋ ,1 7 K<O}}tT*5 M:.:::(r3,BHDB!°BHD0,!R\ !P:Xv͛l6[BB°aÆ v0@,!F/3gk֬SN_fsu]y 7nl7oy~ڵ ?~|FFƵ^[TTwihhػw/˲/B]M, ={ӧOYeYeIl_x(ݻu֑sYvm1>o˖-رcG`}@> _bnׯ0 o߾ɓ'o߾l6#Gt:999;v?$Bue-˲={ܲeKyy.\(yga7|snnL~|ssssss:g…$& 47׮]K>}zڴi M.罹._|Ϟ=?d7?xCjݸqcܝ;w:Nͭ!ՖFL駟HO҂ .r0{pzV۽{wE2L={7 6@߾}ZBf͚>G tllƍI:> ͛7gffDƌ3l08x` &z1bBH"W[ldwaD 4鈺/B_4C7xĤ 1&Lm7tVvv6IDzz:8h4}۶m/B]Z’N#{7'',pdɒcǮXt/] >G*jRX"sj`0@~I09 ީ. ?B +r߃dرcadzwu=zt~^~@]H1$4ALYYn02B6\XJNN& K0y_"o:t͚5mIU \j% !P[D.,9[G]zիor M?ݻOK/fy6؁ZbLL̵^~'||>_4 xdc߾}͏\宻 ?KAAرczk=H/l/{<իW{e˖-[={\re`|̟?ʔ).kժUV 6mZ ɓ?W^yJ|dlM ,M|[n~i':tXGEE_`dbTUUf6B!  IDAT2r|ѢE~VVV]v Æ [zuvvBD^xg}6&PT_}ܹsSRRa磣Ǎs)S4o}^hќ9s{ ō7޸iӦt`G_E"OQw܁1 !$qNPXXXZZZUU0hРλv.iCCCYYYTTTzzzG~q\u,VVVl6SSSI/W—>Bu B%B !`XB!$"DpTWWzEk4Vm0+\ K'|oIҪ****++z=YJѐx<.p8dDE"fX:aINSN>m4u:Rd2 y8|>鬮Ziii=zQBḚt"ÒUUUT*ɤjJR(r Ʋg: az"fX:}KSQQQPP`4M&M4MZ aD% !-)))iii:iBqsɽ'rB ǝ:uJaylj\ L@d#$Tӥzrw]E)Jr{<&_|<@R#%x\[~v=..nYYYW^yeϞ=/j*?"su6{KLgCСC999ZܲmDz媮ж-p ΝGy;hw6sέoJzꩧnvgtf̘Auֵ;?b%Z#wrFaZ/),ݻ$ؾ};%a_~;w'&''WVV;vСC ̟?ĉsH://@t$plvBHP4m2rss/i d2qŅ}x~:)ap,XĤ'>sJw?ظqرcsrr:E wM^.]Kw.8'$;w.!!`0f'HZ CBBBsQ ?3~4M;KrYf iѣdz>døq/^L7mu:"k$Ò$8P]bccGOٹs'00iҤq7|òl[.~-h4^{MV7?`F:T9,DrH%IpсwCvm<7==}ĉP[[믿*KrI2kȐ!, /ZjŊ111ag!F0,I04M_;h&X,d\8 H#F KOr)--%zj_/3ᮑ& ,j4]EQp;v L0r7m?644DEE6o7.D wMIIܹs[oiÒ$)Rl񰂂cǎqI2 KSQQt: u0Arw9NݞOV/_CU/_^YYyС!Ct0cVG{%K8˲/bii^a)$A q\$o cǎmႌI$`̙3e2lܸqΝM>eY#7M<90DB:-)ڒT%&&>yLjiڝNM!jkkQttuv"&&&!!j۲fx~L9iH(::.1&Ls\{Pꪄ*Ge?z69l6/X 0>Bj-(//[(##%lēhۭVgY䉏]vh4~|iiiII ?BpWlvuY"_.r|ܹ/REbcc'Mz?wIBi_N+-uuuG ⒞O|էN23LP.6-66o߾8JGR0,INmmmyyl6 -dQbD!"fX:}K'+++nhjM##\.i'pr3,HڒDy^ZSSc6t:]{,r:555%!!h4Jm\$aNd`XZn0zZ&z{"fX:a 㩯w:n|>P*iNoEa%B"eG#0,! K!DB!BHDpn׻\.FFGG D^/t6%ḚtKZUUUEEEee^ZVѨT*`\.p8M&NX.bn$t:uF111Q)JLLqs:V5--G:NwYX.bHRL&2uJj2/r Ð)U***ի>aD -INEEEAAh4L4M4M#'BfFt|o2-,1҉$ KRUUUXXh6M&ViB4y>MJJz7{پ<*o$]Җ.Pl,X*"#^0&㋸0(€ࠎȢ2*( V(Z@A״M{ioӴ@4yIɓs'-e)))7m0izlٱ]^7 7} ~~~}늢>}rrrLzΝ;7V-ӏOϞ=źlQ%''gG?Oc*rIutltر1b-^vo߾ Q/#Gs5{ǎ.]TUԩS֭۷o_YY_톱$q?a]r;v{|wK.;wnytBAAAXXXPPnEyPPPXXXAAmAs'9Z8" 8qK/$k׮4n-G Biiipp]xNcbbh4:b-Dž~ieGT6Ē[(//ЮEw15 wRQQIDwq*ւH{\?[&~ ?F%?:. fOW7|{{t?_RRb0&Lਊ rkݺu#ʋ/6~o-GUaȃ[P`08|/c`0(b3f<:: +j ǥaݺu 'ϔ>|nFĒ[p7sYY?P<>>^UUG׫e4,..NnE9}۶mstdXr ^^^&`08vs$~iBBB8iii_}W_}5qħzʱՓ ǥaׯ_+jP"qU%c2Ǝ(f22**jڴi6999/rzzڵk 䐺2/b%44{kYd8:ݻww;:*iaȃ[)--o GS쭴1-C9n-ER۶mE_מwJJJTUuLUՒFN*&b9uꔃb|\… DԩS'7 ɏ C,BxwB1Mm3LFTTT+ֲ|\rrrf̘aXhذa[#qm%`0-C j9aUUz-1Xn|ZiK^^ތ3rss})))[#qy| ;wuzsE)++TAA~4W^ݳgN;))鶫r9\~/b3g|BV^yA,Xraaau{~a^sbXE\|yΜ9=<<69\re޼yչs7x#::6~tbɍDDD\p!;;m۶1^1O ;t`ϣS^ӥK~8((jJ:.mڴԩSrr]w݅ui

    l ++70Lm۶]p@>qr{ѣGSҖ-[N<;|aOͯ>|xܸqfXX̙3KKK/^8|m34k֬R//9sQeeu((l7^ڳgϑ#G1ɟQrrcF  6 l?W_ݷow}ꫯzyyob'|R4z֮]+PѲeΟ?ODK.mӦ ޽ȑ#w9rȑ#[nٳh4ё#G=7#FDFF6_m2~K,|G}4tPQؽ{ɓ'gggG/"1VXq}?쳩ϟwhر?xlTTOܐ!CZjUZZzw#0ZFDwI¨QZattEٳoY*++###/^l矋8q;w:9Xh-b6O>MD{7 ;f]c޽{ǎo&1y[VZu]:K@,r)1//&/////uZҥK:GD'NYw_ڷoߥKFott=3y]6GEELYf⋍U|fsxxK/غΝ;ON:9VW\9 F3gΜ9f͚?ϋ-pcm6k֬""E,nUVn;wҥKÇz6|۷o7s۷СCDGeeemܸQ;Mh4[@mIDAT |+pK6m.)))yW֮]?o޼?qs 6Lt|?5{of7|N:y5m*? (>>d2edd8Rs K׏>l6m6P?Sffbi`W촴ܛݜ~mJJJ͛'ΝkI߿h"hѢq̙ʛM*֭4iҤ:&&5! R`=DT^^[,E{С]tV7,Xйs={2$>>>>>~z?sΤ=z 2;޽~j-b"3f̴i?ODTQQsN|Μ9Ç7O=T|||~ 3iҤL4)<<<::Zȱ>l0W\i`/^رac<==JyyVs-]ڵkZIqqUOnXUU'Ooj۷o衇l}&M:{V7{ ֭Վ;?@{cRQI&m޼YkY,m۶=uU3|pEQ***vmsѣG333E>|(;wwٳJ޶!CZ K+:t+˖-۰a 8pÆ ǎ^"/^=vɒ%{%C~iiik֬5jeddԶx)SEt_>/^ ̙3bG Tk׮^^^k3fQM6uM߿h۶m6wN7vXQuV"R:hСMgsh5kvED 7xM6=~z뭷fΜ) ~/_`0QLL̃>xkkD4gΜYf=+i׮]@@@Oӳs'O3ƬիW˜ݻl6߿gwaׯOMM-**  FFF1cƬZJ׏1&5Xx:缠 ==ܹsD^~eq?,.z嗭gΘ1cĉɓwy'iyzz;vժUNRUU\{q"ݻ>kq~wyǺPQ+إK>]( C<>>na6m~$b qI\gc=x#G ?n9z͘@DӧOO6n8yd\tJ;?o޼UVu//쓈6n$222++l6gdd40ݕ+W)+ r%KhxiiiYYYT5vÜ9s>sɴdɒ{LYg;vQ 7)2)22rw}6В%Ku]+r 6K ?[Ҿ}~:th֭ƍ# J;@<22' k׮-[L+$|цpSO%%%YJ'E#w6 )秧[_UcDh߾}GuT2s̙3TTKD/kcIIIIII| il6;|v-W^Dt+Vlrʺu{Gbĉu(..(/ 6O>v?o5`0_F=x6mڻw8sL"2 ,QQQ0===%%eΝG]lٴiΞ=[YY9p@"{ŘիW_ʕ+O~wnjSwٳg ,7o?oTuzhh(eee 2dڵf9++?2d4 `ɒ%5l$''ч~HDwygΝm6mԩS}э7:-c~ah˗nO&&&K/e˖Cb")SW#1c|ԩի'L(ʕ+[H'O޴iSAۡv7nŋ333׿֭tt'|D lٲE[?L0)RBkI9ss R`XbԩW5j ,X`Qp~MY{V^MqDDo… E fR\\W_}5sLl4u={_~i/] l}ҥKyyy111 l;$99yݺu7NӦp[KnW^4viþPc-A]{I7Xj*wֆWsP`T|}}CCCCCC{1gq!04 @"$X @"%b $X @"%b $X @"%b $X @"%b $X @"%b $X @"%b $X @"%b $X @"%b $X @"%b $X @"%+дTUUsnS^ncn^nX]Xy1FW cMJ| b \ #"7yMIf\2 t#jM{`` fC_k*}(5q}Ȟc&zf^5}UH݉iE&Kj-DVP9 #5Tԩ"rϗy^QԽ-#!񬵿]5CI綟l:d vJF͐5ڑ^t{C%p5*1k_ص;V}S7$L✖~dԴ6iG/&3at[ӬsI4bJsyIQilrJZ XU\B,%qPM a _/xOՅ;k~ |u*{w:"ߗ9U%!9miH8g8pd=,h-R9STN[gbf'=yuwjɼ<Gmg9a҅S8%Q-V{W=zÇXWܢ2"Έq$Z'5򞸪ǸocĶ*_:,*7xy#uDD;|u$6rkΫS8jocr;R{6,7>r!, y]Sj0+2dֽ-gGCNPR%p_  nun?) kDDET\fJשSk %U%ؙבQ^ V_~#nh-YTbUZ7N,17]h* yL#TUzDRȪԏfDc4a۬Ϣޥ«^XS:U\spZWrRZ͈%cz"Q}UM3-m y):Ͽ,jQcE쿩L9A}bhl"Ѻlൟr3/Zfv7KrGiə?cD7pK佢Xӎ{k'>A딪%tQkImQDݛ>bCJ*jh5x}n6!"7og$^[E~mE+J%sKTj6&&Q i0b%4=`" `?˅]yeQm9'%DS-9[!!ճR0b[%p5ʫF**)KB,Ѩ۪frz;NeX$˦LOBFDڣٟʬ\{Ubz>l6|/|zuLa躑./TQj5N/.4ċWntnƈ%SҲ(-pW)' Lk;?3\Lr%WL 3YM'5.u&ۗ%p)1JIV%&G Wމ8{[Ą7X뻄DT󝽹' wW5S"U.mzޞ= tʎdzX_M/KbE{bt# ʛ1\)VSr,.;f]BĉBJ}Kj:Ex SwIt\L޼ ^dn )Z1bߛ<b \^KQ3mSU7R,$*qJBx8cxo'<KRUd2\̀_<])7mZԮdѾ:B,kRUl6JEQ87 Ƙ^?xt:7\du2!$dߝ> HA,DK HA,DK HA,D擶".{IENDB`././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/docs/source/traitsui_user_manual/images/DataFrameEditor_demo.png0000644000175100001730000011653400000000000030640 0ustar00runnerdocker00000000000000PNG  IHDR`5|: pHYs   IDATxy\8sJJ+J*e: c0cf`_[le1/YhDH6mSi()V=[{n}x=,Yilssskk r3cd C]]` RODbE1.t=Az KNNNNNH@> ]A 7H2УA`K}𡬬۷uuu!%%% }*++K,t.L$x<ںׯ_o C tU"(==ݻw-PZZZWWgnn. ]U^^1ݻwN`-]T__M&Eii)BHGGG_^~}ܹZK{ H|ы/UTT::,ÇfffxxIII@@@qq)>tZ]"WTT|Z~~~PPPxx7o $xW#Y5-GBCC?]N9Nq(/dxJ!Gt:JEE)WB`())Q`X>)MMMCGw)eeev;طo_KK˱cr…ȧxgϦM6gΜV|;vLAAӎ9憏fӤ!CƎ;lذ6.L}}}9`rNu_>pBfO\SS)Si G,8pɓoݺ׼ի׈#,Xg5=988xϞ=!dee#643x`KKK[[N3#)))߿?j.CуJJJ:{,BM[[[BЁ\ʕ+;:,-Cΰq7=?LLL;wԩS)={rL-Y,CN<!9|uOæMB}5W׮]8x 9ԯ޾}}v8qB[/44433۷]r%𔕕tQJd MX;v899WWTTOl6upСr{햛`iiٺ"BBBO9\.Ν;"^j=6]HKi2$$b,X@j .|7bf9:---fjG4*b?qƱc[__ X''۷_ N>-:{ll*g>}{;.Qȿxp?|pٲe΅#^zd = Ugؽ{w`ggg'$$'<$2d]1~TTT~G8|/FFFM;&NsDׯZj_7ۚ:ZjY}}{@@R]]]rrrrrr\\܁444JNd3gZϏ xnnnjjj bI&m<᧻}vK ۷oD"}FlWtYЬyYXXXaaGeffѡ#RRR[QQW))((ܼy|%~+> L6mѢE!HӧOfdd;vƽcvvvSLD111AAAϞ= PPPضmu~uu5uVsssG:u… yyy!!!ϟ?OMMݰaŋ{9#]xm[??t<|P]]5iҤ rss 6tl0m*m2d]4~lllByyyCllG#ڰ~R۰ڌZZZx"V^^sN{2N҃~5ܿ? `| .֮ <}t||7 ndV3fo HWS~iiiICHB`0999++7Vcjk+C?7nO?P:}D"P؆I:Y+?B~RFׯNrÆ ={Ç)c?~IKKK[[{̙cǎEݽ{7))ȇBL&;&?P(l"F X~CJOO 0Ǘ6/GdP(l'^ccLǏ 2DUU5'''##C$0<"$H$o񸌖R]'0d»vڋ/)`ih6`R \C*J2ejv70uDabh6SKf 8!$6{uaa!u7>I]U+$Zl(j##G}⓪=2]y Bhƌ֭UUU_~!l$]udbX,̙3SSSqܹs/^fXFFF͛={}[CYY‚*14{iZZ㝝gϞ}tٲeܹsپ}|߳g{5vS]~~zpp0B˫W^~i{͛7?!_ĭُ3fʔ)egϞ'M㓐PUUe``0w܅  NȚK={ ֬YcooO}%Bݻ!777w,+Vxw9rdCC-w?~+w4G{BҥKUI[>LSS">>/GURRp=>|XzP(ܻw/$5`)Ν;7nܘΝDШQƌ3w\{rʿ[YYimmj*CCC74sl??!CC;޿ׯ_XA3fb  u!Ο?OxF!))XSSs͋[n-((pqqh9Ǐuuuu=== ѣGoݺqoF8DEGG]|oU!Iʧ&}٥Kp}xћ6m |r~~~>},,,֯_Oe/y(5+τs7IF7999C]l؄*!C,YdԨQs$yQ%S)SKejBQQQ=y+lq͛q\EE#F<<66J~8 -ZB5c-կ$ی$@:-!R7@sUj{MjÇ<6|BM̙3+PB~>>>$Vd!ԑH$yYUU啖:u*88`07njԕuBչjjjbW cXB9rݻww)))aX 33333qժUg_|//RPPHIIIII pႄ$̚5֭[>_Ć޾}!4{7oҏtR$[Xب^n]VV}Mff{NNίRG}}WB#F-[y/11iO|``ݻl6;==}ǎ'Oͥ/) DDD냂kPMMMqq1nVWW:;qF~$))i8 W^zԩS#GWWW Ξ'N[)ПR4 ׮]ålHH"fmllom˩M:'22*..o߾m8o?P(ܵkWXXBdD:*5yK@k6lǫ}9}'OdggkiiIF+**rss<_\\XW)Gj@sHnGFFFFFŹSu,{{#GJ߾}KKKHr7IFO8X,Vaaaaaaxx}>s|Nbb?KR%%Ry>>>>>|>b1лwooo?W}^Z]]OT)R[[.|||;d2}vi&3g =<<B?3~yHXPt߾}.]]][:5O:f]]]qJۺu+É}1 yK Z|(Ǝ9jԨ}VVVJ3$wܺu+::ZQQ344GxnaL L֊Ċ0˦LB+.No:W)Q8xqΟ?Ǐq˜0)GDؿ[n޽bqܿkĉ޵k,++CSRJ$E^y $Smڴ[[[_t)""?ĭↆ;vI&?aaa>>>! 5ugjiE&+iۻwH$pã>;zh6]^^NM+8k߂زIҴ~E[znHxTLOVH}Jh7|HGx#..Ɉ"SMdYiiiYYYYYYQQѝ;w֭[VoV\٧O  fX!`( NNNN{m!4Ј#XlٲǏ &&&355eXbs;wBe٪@ hڛFNǏd2fddT]]t'N 2bYYYݻWQQ!??S4ʭ[p1c%1Mлwo\)))gΜy͸qW@Tq(a3שּׂ ~~~ 6̚5 煹sI)k򮩩)(!!رc7nDVVV1cB(<{Nb2x$w ƍwq8ӧ=}ŀd,tX+*B8DFFRl>(xv_쌌 jBhرl6;++!$={ ROMMe2ƍ#+Ӗ-[zꥠ@tǏ{9rᲦ|u1%%ӧƹv<;F%Kfj18pܜb9r߾}:::uuud?~|Ϟ=x;Pzz:ՉF e*Z35I"SE,S#=zT[[WyWK^z >}xyA]]]ccdЛ7o Y,^UIW2tBVZ݀5(?JX'.ܹs\wݸq㧟~2eʯ*փLX]޺dق ꫯjg2//[wAM糴jC =YYY&)>%_~%?ߐP%y'NC@$cbƘ񡡡gΜmB'NuGOg```dddddd``'9;;tF6={lqjkJff&Bё>]CCClZAAA]].POYUNBSNOSVVU~!zBHEE/ AKJiUUULwnMCG^xP(tuuSQQٹs'UKߚi5l>+Vt=y߹s竏~[n}`޽f /8zh*j(>?xA0)P+G|W_яϜ9ЏKMMM ן8q"}R|GIIIWTT8q">O<կ-'6Б)"4f4>][[;p Q/߻wÇ BD322*++mmm?|[qE4<ŸTWW߿ueMfffBcƌAYZZ҇͒P)$ $SRݽ{7<<OdXTq_YLEU+Ra&Vdj4 fgThii5;5?pYjjjMM1~c#o &ujI]jJ֜NHz`",?JX'.ܰaח,Y OnڴiݺuL 79kٻw̙3bSʄ^@-]I6YhJ+@bX,}!,;vر)%gٳfͺx'>x𠶶v̘1FFFx,mcbc~>>>>>>^xqg~+WRSS?aÆ+W#USS.//~n҉MÑYYYlzP(lݖOu]7|D͹ibҩReΜ9xyZÇo>sk׮ƣ0KWׯ^ji^wuҖIg IDAT'kVVV!dccW c.] bڍz%.S@B]!ombR6mhhnllL~/˗/wssKKK۶mteC?tttyC"HQQ?zhܹ,w`AAAEEE.H^ȔZQUHH&o޼-mC[Vvvv 8&l{pKކWH8gϞ5k<{ƍe0YSXF;B\zKHJ%YO$} E@ٳxݻwM5SUH]$O+2ԸjBٳ88lg={n:>~֭>>> ,ruV~:(//?>ϏW366nFM'3gǏOx^L;o`Xϟ}6ٿ\C܄ѣGgΜիf7*j~lU /}c Ll8>AII! F;d"TG-Μ(˖-+..}vpp0 )tuuqػ;}%E%tl sY=|Wĉ׮]KKK2.]oGGGX,һkݸfkWo-]<巕O.1>wfjqԢaӦM FQQqРA}ݻǏ߈dZTI#kFq%AKlĚӇx<܉Om >bUUՌovIթU;=VݒӋ!~jEGKKk֬Yf/_jeeEX]iۆۧdmmEy{{Ν;_~b'uHxױ wnqppMmӶZ2Ƅ &L ]vK+!ZZZꕕ2q=kkk_~M?ܜegg o)x_jƜ3!ͣrrr6AWuڔFߴD2ƃttR]]ݡCFAxGE5j BmB'Z*Y?%y\߿… 6m ⪪¢w>P@ [#$FPt[555kϊ˗/?~iϋ RUD<~dԈ,$033mZnz2^,$$$,,,--MUU;=z҆W*S_Ln͜~s/StIh+**6}^~ʊB~k-_\]]={~w%)j n+F:¤KMMuA{;wU̙#ZRbKYYYiiix*5j;@_TVV"Z&A1L>/Qpp`oWX*@2>>/xAG(qA_K S%SNNN8:rȑ#+**9B2 N<>+&LMM [nݻɛN]]};wP;OtA1Sp%PhccC/h2///H4f̘6)+;aR4[5;.4܊JX{O]R>o޼ٲeņe 1\g _n|Y޽W\z})>\+WnKIѣO pϞ=;s N"߰a묭 Y,*m0X8rȪUBxŎ]h:22r!---jB5I())u݋$Dϟ??~ط̘1CSS͛7k```VVߦMRCR}sk׮]nX˸ĴS/R$''{zz"jkk===SRR8 :mJ.{ĉ DO"NNN۶mc!!!23f=|||5;\oܸjN)Ml^~ɒh"mm_~Bhhhtt*ȧL ~tݺu WJJJv%vZ+G(Trݻw/U{E9233WZl2ܫxw]۷O>EQD"ѻwoJpp0Nbm'BG> gH%%%iN>|"2!>}|exxݻwǖ*>H=* %qCCC6"}%$$=$wL6ŋߏ000ͭ0`@fBG> 5S#MJt/ &j7d Bmmmjccc]]]h lj aJ0w'O4}؆) Fan2Ah``PTTb\ڪUUU-Zh \.WWWѠL ۰jIKZA2fj¸jȑ#׭[߿?8nz. ClDMM͡C9--*Ζ4["-=7Z:.4Әl{C]$ ܹ`zzza/|H+mpt]i`0`g͚5/^PǏm6[\\ظpu֡I`g%u=w^&YRRRZZJֺsrrڹs1ckjj ?]3g*H 7?j.b ccҤIO611)// KHHczNR9nzUUU++ ^x^Ɖǔ)STUU+**FyiG͛7SVV5k^AAA^^^^NzU|&Mf͚}644deeQtT4=AGBSLٳg垗bك7 _i;CJ BFFFSLWRgΜAmذZjw?o1UUեK;wNDr6=􏨨x0\MMmʔ).]V0!LMS#W_ݩ.Q}b{r^0XǏ];r9B!H^<<<̙chhX^^^^^nhh8gzW>ɽ0ϛ7OYY9!!!,,|.\{/gx_|~q9IXfZuuuzz:Y~=^Auyk]EfjӳU\\\EEŚ.]9r$,..~fnn~yzIMR]!|fUg6!>|p|ݛ7ojjj&qr-?VnE-E,Ils\MMMy/Ι&9MMM )ׯ{zz:884;bџGbBamm-lcX__r)xDn$hJ|~nn.~[7p@9=?t mÖ'헆ŴӘ*F]$ .k```bb"yfdHdhh]zu9j?իWs_U;YuŔ M|o)++sqqYhQG wԝpеwYwX h?رc_|K@wtR'NHLL|ܹs;:DOA]?ߟ@ݽbI@[DÇ`z:Ϝ9r,uq"2dȵk45525+20ahhhhhZOAAoy S"y$@dG2УA:`ÇK$HitW w R8t0 hAz4 =t.ү% CM8Q>_T&|@J @wtoA0 hAz4 =t :@dG2УAѠ hAz4 =䤚C)((l۶M #oߎ:tBh۶mPХ566?|`8::nݺ?cHDQQQٺuk޽e:!qС/_"o>hР1?ׯ_e޽'Nꪢ"vI*zǏtH$97!!=%%e3g^Gj=qDAAA}}M6Ǐ}H$:rرcHɈ˗/3;;;;;ӳ@׮]Ejjjaa]zz'4Tdaayf__3f&qsK}}޽{KJJG-VH͹ʞY%''X &>ի\.˵CX# t~$51xTD#322l B(++`E"La\p!t%%%~yGGGi G|)$&&X5k۵kn۶-))St l6 .422B9:::&&~I*255̃xD$9zڼy?|ĉSRI͹***޽cXԑׯ n}]LLLNN)_#a[la2{]jUVY~wn2$x=}>}>}̢dQFijj6{5瘛Ʀ⁚|>Wʖ-[ՈB*sE"65!&w[!oooeee''VQB5R"(@"=yݽo߾%rڗ@ (--500@e~fYY'O~z8;;3%Kxxx?~|ɒ%e˖-ÇC άٜkhh\XX@"\XY#;U;wn„ ̙35R(/bdd1mL(jhhjhh²2]]]|D]]_~wppwwwvZS$Z*%%ӓZrӦM=H$ZrW,X0j( isLիO<9{lluꔕҥKA#~7oy{{R䜻NR[[/&:>>>zBtfÆ x;vy IDATXfMYYَ;ktP~Zڹs͢Elllv8b ڗjyy9`yy*:BE;wFqq1$:t!44'44rokm3gΘݻwW^b'@*sUTTݻӳg^!dffoLJJJJJrttݻ' G[hݻwB7nܠHBjMÇ<oԩSLquuE޽ظDgݘIRR500@q܂#GJ۷oϩJII7n>bmmm``\YY)6L"~4hУGNT@'$5RD"њ5kB/n ,X+:1yeCC[Q#+ t 51STTTTTk6piiիWB-ёSaƹŋBb޿Oݻw_z5x>}L.ۧO556+5O^^^Æ 0`Bʕ+TD蔤\1{ɱk+WII ʕ+aÆQcF @W ͘1{!^aΜ9...... ._a+V566":rJׯ/^^[[ݻxcǎtBhժUǏwrrZr%ԬXSi۷P}}/_Z:$=yƍ;vhjjڵK@@s߼yfddt9Kܠ 7{lUUv7@[]lg}6nܸOrH;"jkklvccW^zEFbccO>M!==m۶I hœkaaQRRRRRZpٳgΉ&ϹwA0?.bҤI˗CBBBBBB#)_#aUUUgϞŻDFFjkku1̭[g;乄?# ZKKK>8q|t1cưX'$%% 2^sB%%%! 3!MfɔH@J]%%%盘zER$ uuu͕=Gjr7rmkbFb2x<7ǤȌp)"!dkkKFZ[[2 'l6Z ***/BrBHGGG+n R]ȑ#p~ׯCCCCCCHХA:JP1@dG2УAѠ hDX:tHAAa۶m!}vСCB۶mki+e:3˗/ "ɹPv344!d@nHPE4r!all d !Ь7oؔx<ׯϱؼy3HccgG,k„ Ӷl======[j{$''1[,*++^:11r\[[ۼ}jookooߧOOzz!D?hnn.RSS|ekzrHMOZZO#B@H$t?Of1իW:::vt@"SJҫW"-ZӖAFvWJx"Bhҥ@ URRj:/FMMMCC㥧#\.O>jjj =ݛcggG_W\)--444 6 jt@we7f̘Q__ѽ{B^^^45ۆɜ9s\\\\\\.\ضa Fvb www777<СC+Wɓ7nFFF"vءihhk.xJ?\h^mŨ ЈbTR$2&t4dcRр:q`hdV&Rcq"ب2jE-# fqgz ?{O=x_f X/3,ZHQ;w[DzjuuuMMMNNNaaadddVVs M6.**RۭO<}>p2wCܼy3--JDu:]tzPk(|/'NXVM+dpCWZl͛{Я]rEDΝ}vg=oFnn7|7߈HHHȦM/^;5ЕyD|iev/;v`0 {tWY^^^j`nRPPȌ3s8?VSDd.bX,cvO\ߋH\\\O,&Nyy?`4;nf"RWWWYY;600biih#]Bn} Qss8'fEQՏc:>Y\\AQ_Ҵ"2uTGE$>>;&:.>> 8СC&?3'NWvZ-\AJv1Lh G#@+MMM&7--feffڵkEd2HZZZICd"Ғ."FGݎ\dK+ YTT(ҢnFEQE.%"dIIIgϞtFQDjjjEQvQ\[M54Ddюmp&(b F M)((w0vXEQE_)..DnԩS)ׯ_x GKg̘p@hpZ_ѻލ>l6| G#@F <2x4dh G#@F dkkkpppuuuAAAIIɎ;\rb觺Ԋlra"@"-=wv3fLiiw}_kz2**JDEVFȑ#E5ٳgʔ)l6O2etz꼼3gޱe˖>}s֍7^rr„ ;wlw,c[˗7mJhӞkZ?#F}ZD]6n8^c܂ q9pWb8L b=_)"^^^K.ٳgo۶mժU-~~~:eeeL&??^+o-ZT[[ /O8ϝ;'"999fyРAG6mvwr%'ap----Ή͎'''' `֬Y7m^p!%%0`Ĩ_]v7͔)S͛ϰp?n_r={vbQ'9s\x… Vun;@`\+>1 kFƠn˓^pEEE۷o?tƎށ:mE_}Տ?h6z'|rݺu"H_CBB\r^y׼N8@F'x\+>1k}I?mF><<\D/^8qn#"/2$$$$$$˗HrrrA+RK.]tID.))j @o_zQF}ݟ>~РAjJhhZ[[Yi}^s\uiX,vy :m2E$;;[OD_^i6-//+ٸqڽ h̙&??_D zW^y%;;{ܸqG9r|駎Iϊ޽{?Oׯ_d unoZ8=v%}~RRRRRRΝ: .pt M&i-rs?رc"~GDDlذ]i4ͳg XW^),,rՉފn޼VUU%":n,ݻ>9s樉/vdKMMݱcGFFƙ3gF,"Vrp z71s:ZPᶶjuĉVUsS\nСLJJJOOp!C8)))yjPyذaiii9?ݟr劈ϝ;w> tJ{+v՗M;v`0 zK=YYZG㝳EFF9rSg"EQ~_n9 .DF?q\GJ8啚F֯_蘈(񡡡9^aa̘1=j:uJD&Or,b;vIyy?`4vIӍV- p6l6O0aذagw}pi8777H/f(1,..4"2uTGE$>>;&:.>> ۯ'NB+ @DW>j{<2x4dh IDAT4}Ųd2lLYvL&IKK tiEW ؖt1>>>vdd%K\ZQ4ȢDDQ薖uh4*(nvi- KJJ:{N3"RSS(JLLmGj)@&"Gvl;0GKx4dhJAAA|||eeeoc*(Hqqq&"v]DNH~s<__?c Н:E{Ҁ Wnop9f٥s<2x4dh G#@F <2x4dhtZ2555L&__ߴ4͖)"k׮$"iii.(4՚W_)ӵo{_~ݻ+** 0cƌunO?9rСCeee uቡ:}tfffUUUTTԬY^~lmm}v͛z!wV(Zv-yzjpQ?qt _9rn;Rz}jj[ӻt!"---"b4}||%KϟgϞ //ǏWVVyy ߵk1cJKK/ɓ΁W-ϙ3j L 誣G>䓭%%%;vc-[>|XD.elג%-?-km۶}p֬Yh4]EEE(h9r("&wt={L2b)S޽ V>1bɓ'-[t[zu^^̙3tzwRRRZ[[7nxʕ &ܹ]KS]|yӦM$QGF E OjРANΝ;wDDc,))ٳ ?xMMMMM}Qu+?͛7o޼YD>#GN;|&Me˖HqqqɔCtƍիWũq֭g^fƍ^Mh;Ӄs M}5NFcoScюQF9cbbzFwā9Yf󮷷 >n #WǏ3gδy񼼼!Ci_CȥK*++}}}'M4x`V3Pe,Kxx :.<|d2:2|W?l~ꩧDʕ+"7߬zЀueHH˗ {{grF><<\D/^8qD!!w$Μ9HvvvAAA@k\k(q}jkkG=pv%@DZ[[̙&VTT.^X{9"r?رc"~GDDlذէ~ի555999YYYwyʹ*t ,poeel4O57=2J-Z8vޝh0WTT$"nڪ'NErC=x`RRRzz  2đYYZGT{ݽ{Ȱano8bsݾ}{O9\vm;f0el4O57=2J-Z8{GyoUS¶m6k֬vEyyyWbׯwgДPPDf̘aZO:%"'Orhh~0cǎᘖ!]k@EFF GҸjjj*//oll4cƌ z~Ff(1,..n_Ҵ"2uTGE$>>;&:.>>)'Bn1hq y- /G#@F <2x4M_ljj2Liii6-33SD֮]+"&ID]ZQ4rss322D$66%==]DFdVpM(Q%::E6(bۣ]ZKE4ȒΞ=F(nQFkh ѣ05LQ|jfff~W$%%twn{O?n߼yC=t<.(((--|?#_ݻwWTT 0`ƌ֭=J:hE[nxb#Fx_~#Gv#Eק05<әUUUQQQfzo/ȑ#_uQ՚aЍcs@O?9rСCeee uZמWh fddHlllKKKzzFu;22rɒ%.(ٳ'((㕕yyy^^wq˖-dVuݺunǏ?yd ߵk1cJKK/ɓpq+jiiynaÆ9_@o۶믿ng֬Y.6ɴG}'[[[ JJJv\֭[>bĈ>}:88Kh߁+??Μ9VUmhh @7t1O/eTT(nƑ#G*""j"pGسgϔ)S,l2eݻNV|,";w |gnyҥJ__I& < @D4퍍v`08?~9sbO:%"J:KKW,Z6b;ʯ-fsꫯFFFܭӧOȵkƍ{챒?["r43u>3~!<<駟~'i&wp;uuujʁDyqʚ>}~8k֬. @?-묮܏\-$$91$$UVVf2Xn?wA=ztڴiwQv}ѢE/¤IzЇuH^^ {sss~퀀7|?t ncҥKEd۶m[j֭[Eœ9sfŊǎ  #@?v ZYX2hiiqNlnnv$"׮]o~3eʔy9gs|ED,ZȜ9s.^x…TպzjW\gϞ;w9R+R~'|r_\n]VV_"6mzg/\{ 0@Dbbbԯ 97nl޵kh\|. @_w ZYX2^ jlltNlll rȾl6?SO>uD䷿QCBBԻV\}||^{5//'N{|F]ռ.?I[<))I]i;w; |rt:۷:tH,66͛7o߾}"wXw@7hYguu-+6X^^^__."/^8q#=.]t%QG]]g.}}}^__?h 5%44tMMMՋn饗G_ &[CNN O?xhvE$99b}||***nݺc裴-X,vy :m'dpHvv'ȋ/0s[/((p.g{ӟ_ܗ^yq=ztȑ"駟: hiE*͖/nܸἻo߾ѣG߷d7n<|BB nܸ駟Z,GJNNέ[ƍ爎uXw@hYgi_=s))))))suG;r .LOOW)f2hѢs޼y3--JDu:݂ ر###̙3F?UV92ݻ>9s樉/v9~xםN[G=#>kرcE VEDErd."SNu\~]DJJJ㹃 /1c{WXXM=hi@E+z77Z8 <2x4dh G#@F <2x4dhdMMMׯ߸q͛7 6lذY]]QiVuӦMӦM{G~mz{~˿$&&^z>z|̘1K.mӧ)..n̙|AO/'NnݺhiZ)Z-R}>з9r7ޘ:uC=jժޮГp?xjmm}'~_'OѣO>dkkkpppuuuAAAIIɎ;\y~Kϟk.oo1cƔ~w_|ɓ'yX-y-s^e8snkȈ#KKKޮ:];lٲ"RUUCѴEQm8rHEQDDM{2eb1SLٽ{ŭ^://o̙jժw&M\ZZ?aŊRRRZ[[7nxʕ &ܹݱ౬Vg}6bĈ'O~uuu [lqΦjh#:G>'00pҥeeeSNpBvvvo _uU/_޴iSDDk)Mg&$$<555555ӧOG_(?XD6oyf裏ĘL䰰S]]e0 njs{7n^7..nٲe"u>%M:wo'M$"aaaj )..vΦjh#:G--[,++kĉ"b0~_{^@`Btf//7nizRDF5jc;&&k~((((11QMLL8p`QQQWȰlcFn0ˌ?^DΜ9ݺYf󮷷 >9QKV hZFԩS"#vE?t:~x^^^ff!C_IDATV7x]\fY,pXNnXl6[:}\vmܸqz+))qXWWWQQ8p@Dz,x_~ّ`#+8YYYӧOg͚պlW_}5222%%79dpĐsΉHNNl4hѣGM뼼d̞={۶mVRhn/Z^PRii=ժʻ3gάXرc"pB^5>Oj]DL&_/մD ZeGKKsbss',ZȜ9s.^x…Tպzjl6mzg/\{ 0@DbbbԯI*ݾr={$$$ܹ'--Z5ҥ^7nl޵kh\|yFFFo<-k׮72eʼy2^ jlltNlll R(!$$D lʕz^:q3z>??hRo(A_؞jz^͛7o߾}"vuӲZꫯ~GSO=֭ѽY2hxb}}[__EإB|}}(4HM 0`@kkkkkk ˗/'v-"z/vK/5믿6 b{UAZz񩨨uVo6-u5pvҥª* X,۶m۶mە+Wu"luO>_|<"w^uO###7n<|BB?߽yWǍwё#Gȧ~h*--Z5WjK7nO-#%''֭[ƍŊ@j}̙&??_D n/KIIIII;w@DD΃3-\0333==]}d2 >>44=+,,3fpXSNɓ]f2&L6lX_8vX6;bԪii\%n|Ǝqfz77Z8z\ǫ.Q?G}JfYQ`EQDDWD;(ʑMDvL:Ցru)))2Nr'N8q;O@hi=ժz%EDDDDDv-x%q^F <2x4dhbd2|}}l6[ff]VDL&ud>)@!"---"b4}||%K+h EEE(---hTEQZ.)@tYNg4EFQun5ʵ\CSLDFva2+hєޮ pcǎU%88XQQʑMDvL:Ցru)))2xƌ9^aa;t6 hsfK2x4dh G#@F <2x4dhdjjj2Liii6-33SD֮]+"&ID]ZQiV533󫯾R%)))55Uk~#G:tlw+/_>}G}ԽrݻwWTT 0`ƌ֭tڊnzv#Fx]Xu:}tfffUUUTTԬY^~v#GvG^OMM0`sgϚL k׎5 jGGZ 6 ޮcѲ>x%֋4u܌ miiIOOnGFF.YĥE6={yyy?~2//n`ϟ3gjUwزZ֭{wv>8~ɓ';gXdkkkpppuuuAAAIIɎ;l۶믿nf͊w8qGyfttdgg?~tٺu:}1tO=@˚ p#-WbnA[DDQhzQQ8rHEQDDM{2eb1SLٽ{ŭ^://o̙jժw&M\ZZ?aŊ(j~g#F8y_WWP]]el"e˖>}[nܸʕ+&LعsgFD Tܹs֬Ys-[;wܹs[l5%00pҥeeeSNpBvvvo Z\h_uz%=3'Mw%%%={VFQ%&&Fݶ3X=h qNjș3gn|ҥJ__I& <ݯcǎ-))z!y&WϩSDG=\@h_u|%=C  2d~dfX,7DtpbٺTӧEڵkƍ=XIII~뭷D7ywl>x3<÷JXWWY_D8 "暆~':tM+"˖-Seo~"`w ʚ>}~8k֬ޮzpܻG^uO``СC:th6jjhhhkk qN ikkkhhx4cϝ;'"999!!! :ziӎ?>eʔ7;ݾhѢ^xaҤIRT^^Wkҥ۶m={vJJʹs'M6gΜGy~|7 ,/^\UUe;e_{ 9sfŊzl…zj􈻭\u[TTTss_ 8P} k---Ή͎4X,j!sٱcߛo;^رc=Zev}ʕ{IHHعs=AkӦMϟ߿JJ 昘UbO>fVј|Yzv޽{^|E7W7nl>|/_а~ޮpzj􈻭\u(w?;f r2Xµz}PPPccsbcccPPP됐.+Wz^{ĉ6.Qc~lll~~~@@V^^^^^4`!nEEE۷o?tҥKE$66] |$e2nݺu;w>|֭[N͛7o߾}"vuSk.u>r%v/E0a°aԯU(0lذ &F ;rF><<\D/^8q.;|A) hjjjmmJKIIIII;w { -\0333==jdx%2LovBfsKK͛7o޼٪-'zիW1 >>ё\PDf̘aZO:%"'Ov96l6޻&_{wdհFY-"YF# *A/ zѫ`^>>>>Wii{Oə{=}X,ر5/|dbѣGQm۶'|lLHFMYaoa՚ V)O dZ444T*FFFޣ/\.EQuE qpTrfx7oެ`E$aArvCrvpVrA@)H5  d TSj 2RMA@)H5  2 Xvm`!B6M4(+W(:tбc2sٳg7nܸz/rҥ>h|/{cH˗/w|Z4ƳCCCw?8&H^W:,䳴(Ν;uTR1{yw,k@C Ts  2 믿8N: EQGGG.f 8r}pqxŋ?B?\A2>>^Tvx\ߵc)755566 Z }I=МlS$eɂlbb9hf-\-YMNN3ͬˢ% JR4.Z |(mm-#-utt3lݺuL(yǏ=hӦM6Ha;ȺfeђY6gSwww6mt,ZPظqcݢЄ6nX("Ao /:FYj;T}2˿EQPJ>h(:::r\WWW  l6ӓt   dڛ5Ȧ"B}}cttttt4lxx8c@ӊ>} 0C6!mIENDB`././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/docs/source/traitsui_user_manual/images/DatetimeEditor_demo.png0000644000175100001730000007105500000000000030546 0ustar00runnerdocker00000000000000PNG  IHDRP= pHYs   IDATxy\/}^ .Re˚-% ]"QnQsKDhQiU9{c=lfiޯ^3,yfy@ @ b@@ h@  R1ڵka|PYY),,--- 4FJJJ%^ggǏ gd~BIIw'߫drO& رc7nv4M[[D"u/9w7P( EGGEj===ޒN666~YQQuQ ׯ_+++UUU@{VVtJԩS&MӳИ>}zNNw͛7OCCZKKkƌ)))|B||| LLLttt8@R=?|vԨQ #Gd.ibbB&=z$d2y &M755 b٥K&Nhdddoo=$%%٩YYYM4IEEeo޼dtR˗ӓL&Ϝ9xѢEZZZSN]x1wlmm VZU[[Dikk۹s',q[[[###=='Oi ii[nAt2ࠪ:i$+++uu)Sܺu α޽zYAxxg[[ɼ~QNnoo??|I0--iӦL4,~+t:hȑcƌqww|*q'N`?LwM Ǐ,X؈p‡9˄v2s@P.^wOLLtssKUUծ]>}mgg'N^6 bNNμyX2R\\gϞm۶QQQ,8pz p'11˗,qH洼|ܹ <`0V^3#B~vvvdd ^H>KZSSswޅt;;Æ ݻF .Ğ?'33˗/?FXjxG~7xXABBBPPF]|^OOȑ#x߾}_߿rʨ(E{~u׮]׮]û~V~G'NL0ҥKo߾} \@REI$ҡC^xq!ҲhѢrީ>}$ׯ_7N<~8注h͚5t:H$[.***66vϞ=C w7oVXKK͛7o޼zmeee...BBB^^^pѣ'{QQQ7n wM[[ۑ#Gt#ݻ˗4"HRMe˖y&,, {P¥KvOq<|E ?y$vvv!!!2.Мy朜{ٻbll /EUUF?ydRRRbbǎ̝;c͘1Y/^TVV |iddѣGϝ;7vX@eeedd$/o7 o߾'ODEE[H$hUVHK QO:u_xq%X ׬Y}*ןh4`ffv- zXXenn9ydYYիW젣ի+++^ʣP(ɓ'H%%% ''4???~qyzzn߾`bb2i$'''|#p]ZZ ..{ihM6AG8akkb'!!+ɓ'<رczj_9.HIIIIIa6]x 'ެYž:A9sFLL booBPEŻtcǎ=x t4iRppM:::~Ìs*EURRjΚ5 :IJJzzz2W^/J; 4(22z644TPPpww EGGtuu---MLLXݨ$),, J;~xMMͩSΜ9--EŞ9L$ܹs3MII177U?h`ƍP8o<@kk+,~Zeii .HLLۇHغu+FeggCG1`s„ V!ZZZ%??%-`o˗/ׯ{ݻwy/x{ӡ+{}||""".^uDDDd8]5k~~~UU8tЈwbn#"" abb 33h~9:: fll uDMM ,(({뤇 ;v쀂NQ1= uϮ]v2o_WG`*G,t0y$k%xÇcǶ.333R4777cl`a2^ej„ ,Ϟ=jii7h Eޅ=mmm EEE`0"//q̘1 ,5kָq`?P(III[eeeUUU0#(}Awf +***''ܬS__?;;S=|yW'+ @011kHTL ??\\#5΃bBWTܹsTUUUUUGl…Āӵ?~k8pyBtvvr\WW.edd8;;oEEsݻBdgg{zz,YqYuO*ן#g6y"<>|8>!a7prr>}+W"##4HMM+W-[mS<H$lIIϟ̵k222㏤::bffvƍX_).. 2tPYYY999W/o/.hDSv,fWݠ\ҷjPXXF-Zv{e<{pxGG144xk;oڴiӦM )))!!! ---w2e 6#PTT 7n|''O}ucc+W0G]|͛7G9}tObC~xq ֭[}vlGӕaGKO_lviH$n[866zȐ![l}T=JTTN,%T*ݻw} }S5&Hpd1r3+YYY{{_ w 1, ލ,@߱cǫWrׯ%?~QBGyy޾}C<Sw7xݫFq3gHTljv栠 5~rLL9%%x盛޼uۇ-AnݺennnnnSw{ܹsxϟS`>??=#o߾4 p$4&mmm*:fѽOvejj*R9NAAAd$''fL&ŋ'O?vYYYF÷ȝ,/6e@՛G<}D_n3g-V~~~puXQnjgvލ4BݻgϞ={7ʵ?{컝v e˖/_L6mРAiiiϟ-`<+))yxx9s&33eƍ+--}t:v]YUUUvvvϞ= sPVV;wZJMM پ}'O***:DP`Fp XBz1]]OևڽC{K>epƘ:ujcccbb#Gr,MM\:,))YRRH;99gb/lKv&t$ KN\\<((x9|u[|rx ]߰axBBBśXt'-,, x%44C޿?Mxȉ'۷o{ Uon?+E5WWWwuCKK ×: yyy3G˱n$*S _}Xlbm-N$W G[ ikk?>,,.*@8wܦM9::&''cmAM/#//}Q(^0vipp0&->o߶;;v9@H$ңGv (RT&)##cmmq+_~Uj``O"W*JgΜ9V*J$'p9H$555ss~a֭,9ҊH$Һu_,__ínsrtg×:%cCCC̿vpp0?HToooOOO"6lٳ>}im޼YTTL&w Tw ߚ8F/dVtL&z<!mKKKGG$[:>tО-T\\,--ݻ- n[Fxít FIIIss&a M]5444` 4՛w<}D_^Ad]]]iiСCyǎW^hd̿poxkϼ'UH$~@ cnnmD SCsikkkxf!h4@ ~ᦼ|||-駏"DϱYtzw Ā Ā 5@ b@@ h@  R1Aj@ 4H Ā 5@ b@@ h@  R1Aj@ 4H Ā 5@ b@OBYY_\\ֲ T.oj|||BBBiii}}Ծ}.9 wwi}***:;; @MM͵kr}PsZx}1iii b@oMMM,D!CƎ;f̘JrŊ>ĻO6g'77wZ__ŋ}7ܹs;W.\܌;6..=F 33SAAP;uV]];v3gNӻskMMo%IQQӧᵮƍ0aԴ%Mmۆ?>>o_GSUUܲeСCH r~*A ())IHHb:JKK͛7ϛ7Q!^zZ ())5448 #~rA 48k׮^~}p#Gf̘*555--\XXXQQBWWNW DDD BBBбWTT(++$v@Ă޽{'N(&&14 ?#QRR2|py8BP>~XZZ:x#FL]TTW| !C;xkذap.==:1-%%%99YLLLOOFBB̩ٵ&LPPP`XUUU\\d#^~]^^NY"dٳg?~X`7I ]gϞKJJL8c_ZZZ TUUmmmYRIKKw!'''11IMM zx0mܵk׮]K.fzڴiW^Zah2΋/i4ډ'\5s7:::aF~~A[9999.BJJ6[T*ÁL&N"-[vQq___0dee3fAAA{ܜ9s8|2eʔ۷o/^ WZeggl2*USSt钉 >šEa.۶m9r%Kǻ@ ~~~G#xMP.]2bĈP|?%:::LMM2¸y޽{Yִ/^ľḎ\x.]:zhKK DZv- y=~e˰yu ?y}캦f:b nM?;6l8}4DGGϚ5Ϩ/N*z;w9ҾV^ݟKpinnnJV^vtt\|c=7ơ7?.^z믿b IDATi-_ڵk{e*z9[neG2Bp/L[[… N&4 >>~ZZZe BDDDBBBBBBDDwn=zܹKFFƫWE.GXyFF6۸q#GSaaa555Ν;{a˗/9r$tRK.# >|ZPqqq~aSϞ=#***!!A"s >*~VYY9gΜ_GΧ࿌ eժUn DG?~mhˊw|L"LLLAϞ=1rwe8k)4j(N :4::ӻwl5fϞ:uD%!!qԩS/(`ҥzڵ:11ʪlDFFN4G=׭[/x'sss&yauĉ+WL͛ZKKK˃]]]]]]SRR'\]zӧHHH؛M1LԟիWGQQQ}'O䁰twwwwwӡ) -WJJܹsa[\RRqLJJbȐ!!  &&B`w.**j*>1Ca[nj---ѱzǏC'O(uQssL@@vK2B =LeZ<\ك0 *54#d\\\8F)FFF)Sfeeattt30VVVX  ֔)SN< L&@ISFٳ~̙[֮\ O0x`ccDX{? |W@ 2#c ay{A1k,{{{EEEŃA!ɘ???(.. edhh+//oĉحBy>2_}TFM^rssӱv?|)>TUUQYY; 隆>}ZRR1_+٭O>})??_MM7##cҥS))),w'm[7x=tCCCݯnEGGse͛7&(KK~~~sT^^`2B4p ܼyŋt:=++˗,SX3Դ% ,"##=/vddd~f۷W^M$߿1رcQHHF1cF~~GCer劊VX O>l2@{zzzp#BYr  DR/^ 9nܸvI|4h 8K=DLLٳg?~8vؼo߾cǎDGb^SSS@@>bccuff&svҥKOl߾=$$DYYӧOxYf s¡I||jEE~#FlaѣGgE\xqڴipdbGBB@pڵk׮6m˻9@e4{`lVbb"Z xT?G H.]+#O .7u#j6l$BƳd[455-,,I|rnǫ`?^'aa&&&aaa:,.."yf|$%%]]]yޥ²]0zhG@ԩSsvwĉW^eU *#aa`3~G3005 Ғτ?D"L&%$$ܼyGDDիk֬a>|ƍ?nhhwEݙc>|pܸq;)''o?D~9s+&L]0> KN555cbb\]] p;LBFF0$%%'Lyf͛7Ǎw555}!~w$ r<*7 b8qׯ7oXD8@$tAJ!ψAJ!kOBv%  -T]I4VbT*N3 & D"QHHHTTTLLL\\D"dž'7wNi=X 0Faaa)))III|[OR[ZZedd@$4k ~hz*:l0EEEH$D4hSNNNX? mC5:x>!8k E-Y\ f}}=FSWW'fEEEii钒cc?9VVVZG<ݱJ ҪEEEMMMVvG:>x`tDZZzUUU؊1Rr J%JJJ T[[F NQQў !!YR#d2;;;{\[[NH ďbbb<|HKKwvv"5@ ?$t:o'?? Bl߾ӧOH$*|sNʺrJHHH؀}ʕ+7n致 ڶm[hh7껂`?>z|M+"YdI_M`sss̾x}GG{eN*@Ae߸|ƍI$Ҳe0gϦy󦦦FWWl̙<=tǏ===mmm NNJJJNN.**љ|cߺu믿̚5kܹ=(dffzzz6l@ Z[[w=j(nMJJZzumm-RRRRRRr۷oBw(]RSSXx#8|Č&o_ZȔL<wpp|ǏIIIgΜAikkKLL̘1ttt,\ݻ+W޽=۷o]KFHHHprrjnn\| .XZZ?^QQɓ'yyyYYY6667T^^^%%%k֬,TVV®ُz?bFxǏ :am_޿Ų?ybbb[[9t9sfBB`sQQQyupppssy˖-cddtԩo-EI߿` (OV|sUU۷oN Cqix̬YV\imm~> T'ӦMc,nݺ:/^%HKKKXXƍyA={6J500`1cСC#D@CCXx Ht:h4ZVVVee%{{{{VV֗/_%''ϝ' eeeiiij81P[[[Lfeeezz:B4!<=YePP FEEEzzzAA?7o߾GREIi9ޞccc !ty@ ݻ+**\PYY;6#----a_ૺqT555>3`0>} pqqt@XXPs吨'N|s!,}a: **~2L55ϟܹOiy@$܎;h/^GC_RR'CRRrV\)%%-,~ bbbi{{d˗1od2y̙uuu˗/ֶ6227n˗/%%%+VPUU=z>2L&={=guuuKKqikk<0h4΁ ۺuIUUU.]ZPPsCCU+d2;lҥ0nQe(8%%B$''CoВ74,ׯ_}}}ʷo߆F躺¡W^ݼy3KXcΜ9BBBC/RW]0zTe]k-,,/lff͛υ Dqqq H$Қ5k[[[]50>>/^l999l4EβNa~0lmm9~Ezzz_] Bx 7ˢ.FCCg0P;+Ϟ=koo4hPmІ b^fMΪΞ7o:w_x>w\BB_PQQm6琑a())y@kGc X:hР?;&&ӧ/^---AAAp2 &fYZfsv;6ɾ+̌V8nZz){:FVV`co mmm p5`0.Pfw;555eE"FYYY EUGUP'B?,]qƎ;a>$%%,X%<<*lmmٍxyyAiӦuu/_̘1ADD$**JGGG-ZdjjJJJJ^~]YYu .(]TT_aNgtVJJn۷TjEEGtt4xc|8 @OOɓ'x= zk@AUkj[s9d2d::: :u ga… cch#z;;˗ ~P{{C|||rss׮][VUUUUU]ȑ#/_㌓S]pVaٲe,N=zѣ/^>^XXfWVVrTu,"C8n8l.{zKf AA3f }NMM@'-ppiAM/M_@x5`7YYY=z(--FN83f bcc]]] ӧO1͊ GLR9n,Z,! Z!asssTWx²2 \ v KFENRa@~VTTo* p%8jm2wNEEŧO=X  ƒ%Kͮ]~wn>ݻݿZTT[W GsY!''h"+W;ZZZ] 3Q޼y¾;z(bh-&&֋+?<(((`˃bbbֲ}a [nNaݱc\0 6<7͒Pjj*gΜo-T_qW޽eMLLݻCCC:?ڻwQ>K///յ,!.\hmm9s&L/]9///n_PRR8sLff <ӼǏ +W )++svv^b,'O,,,LdԩS9eeed2455-,,z(@ 7eL&믧N*--uvv^| bbb3CCC.ټy3܁aj9d$dcmQիoN8… &&&bbbp#;ٳg?~w^^^^^^񲁁ADD_ C .P(N:c4aaa('7͛7dnF,(H;djjjr| 믿¬h4Ǐ1i*ʏŭ @Ǫ{ҥcƌ677Cq`nGuY988(++wfȑp… 1ũ$++ ~Qаll) \󃽕r111555h_|']ZTTD Ժ\PPP`?@`--- f2 \ rK[FtzEEEMMr  ᯿>kt$YYY>XxxP^^;ʌ=%˹rذaÇLSݠ煢 N$3;w#=qԤ$mmm!UUU>?G TZ-$$$*:t(/0}=Ye[r2"$$QTTq211t@BBb„ <#@8n>}={z#?"y_57։ ?U"($$B#innsbH 1x4=+d2 T@  3jjjDEE;5 %%?ց@33ox,}@ @/644`VaߑE H$RSSSQQz7o ßߑ@ 8/h///r999OϨH^sPnn.77WHHhРA؞ ? D`5xkkgBDXhwPm_3ø34r"*\* эJW7骗nJJx Չ E# c.?ݟcϸtαY{YyСuuuD|+//:t(~&2*W13"YQQqذa?~Ǐt ~Mgg'SSSׯ_Շ >ޫ #h||WVVr\. =$BP(yyy%%%]]]**@'2fZF K? %*LƠ7>0GF@`7/wk!3砥w2)I!7C yZ*}E&+"k\LbvB I@EfZ>@@ A r !Ġ 3@ dbP@ j@ A 21Af@ 5 Ġ 3@ dbP@ j@ A 21Af@ 5 Ġ 3@ dbP@ j(ZFEE*((|w+wvv٣srrdV6KD͛7 ۊׇp8D"RwuuQ(hT $fKT/_BH?C!ë TO&۷OByyŋ322Y,ĉ}}}{yvM6رM!"uPRRHNN7nܷ'$$dee،?WCCC2;rJtttYYYggɓnݪ!K;V*)) ʪWRR=zʕ+̙Cw^JJ ^zt2Hܼ&CC1c8;;ttt`޼y&M—m{{]6m"rFH}}+Wrss{kkk3f9Jرcl6NܹSdgϞ8q`hjjZXXX[[[NYY) L2zzzqqqF3J3񂃃O:EtҰFf`"˗+i%%##ۛb ݺuKWW_b<<W\>vD޷2TVV~2-..ܼ?+5|>ݻ1cNҒ KӧOGFFZYYIII? /Xf+((,X`ܸq IIIYYY޽1cFVV"V WZERSSccc;;;-Zt]ӧO/_]Q(ŋ̙3555>ܹsqFtprr1cưa޼yNj/H0Xyʔ)_~Ŏe6440|p!n333ϟ?c5.+͍dHDGG찰0eee%Aلbo~uAtRSSSq޽oߺPT055qdݻ˟? }vWj^ mLp? xf?~|̙u};IssÇk)zKTTVTT+WIJJ{+,9t1"==]NNaccuV&yС'NH8 >}4ϧP(}AWơCa ._zj}:Qb2/^7wo5jԩSvarUU,rjmmO? _d <\!iii'$5pc`#Gcǎ&YYǏ&SCR]`J ǒӄC,!b8L_<\=&F<oʕ‡ b=&\MMk׮psnnn\.799 իWa u %'n-Mqq9s5Ũܾ}{[[ƍoܸ!eXƍ%rW]]sEEEp/qŞL8Xq%%%xncox9KKKRSS&&&/_ZIun߾ٙ&~!@FF{JDx6mp8bjZXX主co-2#>͆3uey.gg͛7, ta*ciiD!Mn#F#GؤI^5͌á pqqˋC2+Ç+Vrd2yժUo~ۇ raJKKᆙፍ QXXq?| &5sL&XB&Ν+F$ړT'$''gggO<`hh}ӳBY&...---$$t#۷֮]8m4@jj*fgg pq èq>rh4Px>]N222›$ŋ7!!Fkkk7lJ3=IDATGHۨ(il"ɗ/_Ze>K޽{7ƍ`aa1o 0`@p 8љ4i{nnѣGaySSӴiՏ;&//ӛ>}/ ~zAh$]uc Ç⑕522***_~zzz&&&ƍcϟ?+//҂wSʤIG"Ittth)((`Vrȑ:ݻ1(t9;;3@OOOGbll,t%c8;;+++?^̵H,%%%cCd}@_.fߪH$ѣG{L)t+Vdyyyh TwHHH/o>ɓ==={ZIIIHHϯz-SUU _BXB`𬬬۷w<͛"hlXb쏐]vj_Q]v_׿{Yh \\\ VVV[[5kr:z 97xà<oG6w)xoV'N$8]_nhh, `8;; ֛7o|ѣ6mژ1cz2q0{ 3\,Yr݊Cׯ_===L@ccȸAXX؏?Yŋ fܵWTT|x<LiƍoggK(|GG#> ///<hO0A__211 @ҬYK\{D"l kcc7awJRVVƎ[꼼Z[[eeej@x 'E╕1ע|kk+tw'$-زeKWW&'[ܵtRSBWΰjaaaccCӋ˗{xxI䗚xƍƲ1|իWʲm۶y޷O,felmmmmm544Y,V||xw7'3heerJJJX9/$ ܹsԩ7oވqb0)rpСCJJJ8Юh4H$OO~ ~I@\{R80;Vd$~z]b^/_Ν ǎ"#'pD$77[> @ Fw! k M˞={"iii):$ B -[ٳ>fo۶ 0b8P=xGVAAe*++CCCd#칡C / m۶ϟ?cY1077f6lp_bЧA&Ŭ_Ile*CԪ,򥭭]UUf,T[[ GaWxzzCW^^L4}I\{R8'|[X,  |C;*r'^t^N[[޽{#Fppp'bڊ @210z-\[E;zhOf ""$׬YM7CNc}/SUUsp{...hÌDb3Kw@&L!5!@.>|,kii*##s=RVVVB1~xaMLL( ˅ D:99Szz3?a„;wxyy$מ:!lWW\NRh'Jl6{p僀SC,,,d2ǃy`/P 8AA88Po߾փﲗhnny8Љ&HST8Vw$<wt=x@xyK,WE> cٺ Ne0aBw-wPwdֆ30hjj@eߛsaС~Cw~@)7]Ν/ڵK`͛7?~ p~Fj/_|W^''A`]tIټy3O\R|.ZZZp,cccF)+~G8[ٳ*:x bsMF uSSSoݺ% >Ō!7##~p޽{rԨQ'6)zҤIPѣGc QvvvГqEYВInsD"ptcc#|WY+7fCBB`Vc ===Ə_]]~A.k``S222'""NO:5--m߾}G@@@jj*\yEEEɓ'-sƪ*gg 68::6ӧ;\IUU5<<ϕ:sK.ƎKI\{@*ҹ\֭[/_S7mڔRWW`899?0%,,SL>O^wk_zR3f̙3CCC-,,TjYYYTTʥR^sdΜ9ǏgX .433+..~:|h4"ʁ[L555wʝ;wVUUgff x'> GX***+V8sLSS>BxxxFpB.{)|oƍIIII^^^>|򥫫{~ T:ɓ|>?!!!!!aȑpM]]Yfaz+++󼻺Tȹsvtt8pB` oooCVZŋBGGG99.N.\0_vHWk׮<{MFFFAAK{P(AAAk-::: Ϟ=&BPTli!C\|YT"BpDfD>"oD99@ǂ$ɓ֭çhٳg?v !!!!!!!xe!LԜ"""fhh}4CCG 2h֭KMMέ5L ]b*;::2 +++:)ͥK,MMMoݺ/>|넵)Rf211!H-:I$^lI%@9GR4!^=BP.\f m;w~J LjA\WMM͸5k :p P(&&&b0H$m"TbI Yv-tqs\hSRR,!כ]橩NNN{---ܘ_S[[L? uuuÆ c|>ACCCWW`ܬ?nhh355$w&iaad2gΜ)CB!=t"TVVN`2\.ᆪҵh+ZZZv񷥱Ǐ#!R誾>??˗/dMMM&&&***FFF}4b9{|c 6a0OW_:9sgΜd>lKOO'BRsn.@{ .vKjᓸJQkDPȫ.+*z*2u!JUh6lؐ`L\\ܥKx9rɇ츢0\ذ0288؉F{1cƌŋќ9s 6LX ݛ-ٻw͛7;w\J_"ڰaÛ=JD:t޿Q 42eʤI*ջk׮77qƙzғ'OV\IDFjڴ*)ݾ36@TAdd?񏌌 iIrrrrrC6hЀ-E/5j 2uY^"ի bJ%ݶqaaaJJ5Mrr)SΜ9sN[[[S{=p@֭MG-Y X}]rebbĉu3}׷m۶7V(5J8"Jl*诊XV%_["}ÇZ:uX^wxի샼L۾Rw8.44Tρvvv~~~-[ٳoF3gNݺu===kժ'T*՞}ԬYm۶}))"qƱ瀀WW:u̟????_or#F`_DdXqҥ=z888nݺf͚ovdd~/k׮ծ][T:99ݽ{QFJ)vyx{{njoo߲eKƍYz T*ղe˜<==}}}kժt{-m۶MTNJgzlEoffT*KgS6<{ gOl̙3JeڵKX+`IO^^^ZZZ~~YؒggM6I͘19,,ٳg#FhҤIΝ.\@D#GlРAΝӧgΜ~<<<:uФI߆u4͒%KZhѪU={m۶iӦs1k}mX}FV֭[Y&$$˅7'OvEc͌;/x䉴$++kŊG]M[d۶m&M?w8A, _~w޺i@R]vm-|Ȑ!K,y왴055u߾}oֽ{ؒhꝙK.%&&j%jݻ9r$''Go#OÇ۷o~;s8p7ӧOwuu%ǏgeeIBBB/\vڵrRWtttmmm#""5jRtGۣGǏIHHVT#GN>]ͽxbXX]KFw}*6B:K#OT0{pOI'lT( q<<<ǿs&sq S9ϙ8X8%)8Nq/eDP1+`#GHhtiӦwݻwg? ? yyyG>r򄄄oϟӧÇE]`jj.\(AtڵsIsrrO?O>Y|Wu}Ǻ=zwѭx d7sLV۴i3g$&&<_XX8i$iͅ n޼vzA6pK.DsιsᄈO~3g΄8"|ӧOͻvʖ<~xȐ!zݺu111ы/fIbٺ+͛w "ӧODDDRR4x۫Vbޞ[eKxu߻w/11 ܹsgYOƍccc9>}GΜ9өS'"Ћz~۶mKD*J= ʲXlYLLLllesrr>QHѽ{5jgXw-{RڵkYﯻlEe3PS NꃥT*vzرN:|w7o^]ч~8xO޽СC*j(4hĈ>>qqqׯ?}6l(ESNQTTÇիW[Vyyy@u̙<"ZfMΝc„ 4=.55?'.]9rMֵk׮]^|yѢE}4DdccsQ99rĘñYر4.))M6D4f̘w}-󳵵8q(Νׯ[qF+++"rww۷sRSSF++oڴɓO۷vZ~}޽2ÇQ@@ҥK;w޳gȑ# =z$"ow۶mYv8dRo>[!C>|W_IY_HILLLnݞ>}ҕ4 kmmݸqcݭ.\Ț/^|޸cGGǏ7lؐ|||֭[׮]+Ք3zגMƍ٥HD+cxyQ* TT DdƿXkxFi"Y-AEQmˋr`ѢEXUVuرu֓'O>tacJƌtҶmsD$b׭[׾}E;DTPPpMÝ|GV޽qv:S%JKKc9vaݡ!!!}h_jUa.y=z`^O""V(k3FaɧO>}4;W\a-MQ6663f "F޽{~88ưgw```DDDDDaÆ) "m رc dXuTLLT*?T*nU˗YE,{V_|zyyǏ5#""N>;"o/{ 5YFEEQn\ _zUZh|!bK}RbXtNN/իY_N:z)ߠ LzU\5XDԠAs͙3Dt;vرfСӧOx%fرcj;O>_;w6jh޼yK5k6cƌ9s޾}EǍfg϶[[[O4iĈ,lݼyz=z`,Yr1ct֭Yfz×G ={5H|U$رcGDiii;\s̬,bss^^ݻw3ܽ{AUt3!!a޽#F`[A[[[0GD5k4xtvzƍ |뭷X5@R5j蕐-&bcc7lؐp):iBV*iB/]]E"lժka]k$UTT sTTc;Ջ?yIGlcViŽRXJ1<<|G=}sw܍7>|ȑ#\%Yi tftKM3kpx|v؃{yݧXk H5߂Mމ7ׯuI&İʵk=z'|RV-J0X\[~GD^+sJJʼy|Y+#؝m,VN\jAAA?ҩRbYDj*}&黊XZZ&9r ;`֭[:]uԹqF\\ww=+ծ][/Z[[O6mڴiׯ?vXNN΄ BBB۴isQ6O7eiZֆ8o<&M4{lKZYYGMLLd߹so-}ϟ?p̣gΝ;gϞ#Fvg7778E{E/~3g~TRb "WWTj4#G[pRHm۶C=uӧM&jvgbW~%bkvvvRRa%{ɞKʡUYyH*ѝ*rurMOOם@W--g7n.ƫM-aJTO'K'bfjZVeSrV+ӤI_F}FDD̞=j˗IgK**^zK.eddXXXH40k|ڵ.]۞xEiNSNԩnmʹs- :uԳgX|`|iOqޟ͛7YirvNZZZI̋S܅dBCCk׮{nzVZņқ‚2{ޤ 񆯢5jJw^K$帨qRNpe6| # $ )I9O<Oē#%GGGJ582BAEGđRIJ=Wh+`-\Yf^^^˪l۰(**ʰQTT*ԼRybѣf$IDӦM ҥ4# #g9F?:uހ۷7o޼yl"y8VsөS'OlZR+V;4B.\()^ 4JR͞=TSt퀀j GGGٿݥHII a3Zyyy. ~zټys6,Bqrr:p3f/'͛zM,<]xQ}}̙3V%M2MdzyNJqQ{D3`nڴ){?=ڶm1#U% Zp!B޽{lKKKOOPw"uҒZgϞ-jg$MHDׯ_߲e멟?sLaaa͚5oձcQFQFFF,Xp錌/\288D,U%oTT/ҿ8Arssfpvv&"77>]ֻw^ti…FyZ.+gwϞ=(33ٳg"""z@K.lի7nxUVu֭6u( CƟ&M<{,**jԩ֭{(Æ KLL$";IMMYd˖-_|uuppЛP`Μ9...(=zBCCO}:KW#FЫUzu P(m6xYfy# jzJ2k,=ŋ#GlѢEHHȢE48qիWm۶G՝IP4kl۶m?c Y9Yt){gϞo~޽:tN{衻c͚5zs3J~2e #G;r-1[lGGGWW@NLLכ [F~FFFݺu+2}I{qƭ]mj5*ѨQR&`?~ұxnnn7lv4HĜƍv*]>Ϟ=ciܸq簤B[NNNaaqݻh֭kǎ_5ZjxHPfM#_UJ:Ɵ꼼۷o;88Sw䫘g&ZKDk L"vto%`Ste8%ƹDiŪKS//-[w幧O>|]Vh4)))ΥL͐#,"JX+W׿_ iڵk?{Rf}*22Y_jv^*i ;"@D@T4';qHELDDZA$"^//͉hR7!YK;{ugϞYZZv4555::{{NLڼݯI%*uQEӻGo<JtrrZjk;tJZ50hD,ҹDs3 8Ν;.FQԪUvھ/i צУG c:KZ<>&;f4u)@WtڵM]b `zѢE.@%eRxӡНT ^)jٳzKPVs/&Un&W>XUKdP)T,*ŭ P?.M`6bKy&^35,f SCj\^Ɯ{ .vU}tST~&qϑ%[UAd 3,!`  @fX2Cd 3,!`  @fX2Cd 3,!`  @fJcV}]>>>r _SeSv@FF~F@fF`P 3,!`  @fX2Cd 3,!`  @fX2Cd 3,!`  @fX2Cd4uoM]cƬU5k, ԥx]|'''S֫W#WF,!`  @fX2CF\ "}8DȌHE"׭Y, Lfߩ;DԤac" "9 DR- c2@e?cr,l  LF9"@D\H|p/DDk版ShH扈E""'"{D̟??))IoaZ\]]}||:w1رc3f 1c(x8߿?( L2eҤIVɓ'+W$QF5m}X`2 DdҀ4 y""@Dd"Z "R*DHk?疅%$aT[[@0%%&99yʔ)gΜٹsmy^=zd" B/sN6iܸq~^Qy^3__SN.IKK۴iܹs5͡C&N7ߘxAt9))IYΝ}8'"BADB*}"lKD)ωXGsb϶ODNDGDں "DyLթSg̙FEq֭ ݻjSVJ𕓓_~:u꼴SŤz999ҲTm # Օ1bk#JK.׷u5k|### ?lƦgϞ[GE?T*J?޶mRc׏=gݭBCC[lҸqpO8QTvAR-[׷VZAAAϟ7,.,5j0f5jWX8%q֖֖666666V6V666666v66vl' P>}+?~<++KZװx…k׮.\fMHH˗ 'Ov.鸢(3f˖-Zի5V]GjZz=z?~\/!!?^hjZժT#GN>ٳgS/^ ?8= Rҥ1kvҥZM%жm["RTRxQXXXVVŲebbbbcc-[fnnӧO5fΜj6mz̙p 'MTEQ裏6oLDÆ ۶m^||?Yvm||||||ْ^zeee)S8q"::zʕ5"YfmݺU@QQQvjҤɪUbbb<8|p"ׯ_ff'ZF`$OOFݽ{u7nY9"iTHDbCDul\IDTQ,Yj-'"JN#"Ȭm۶"G%ONNV*ӧ[>dȐ~W , 3gњ5kX5 &Ds;;;É8v؍7ÿ6Mƍވ~7p…)))DxiӦ~~~{Ξ8qfrtt<~xÆ ǧo߾>~ڵf͒DT b^;T*+ҷ mƍa"1bq" pB"ɹyVWfJ~:u 0jZ쳁mx""OT4zVjz)\ "RUUDDJ͕֯+55=`SaTX"QƉ'VfؤhdeedɒG3[n͚5+iXYՈ#lb|*,,d*vBكEOuIoeKKVZF#`A Wog֭KD111*lǏ?~LDߺu pĉ-Zpvv1cFzzX"-[.=j mѢtKbooa>IbbbvveB DTvz˻u&[v_7J(Q+=Blb#VqHEa~므Ų ƒSq|ӧڵkA~_~ g}z ?~|.]ݻ?Z7mt„ 8W &2._P&$$콯p7mJD6<ѱ(U]*A XNY̵T" iBDDDt+f"(EVRV۷oޔD~ׯ_(SXX,Йjz-]E.Z-&%%?}#Gz:vt;wJ͗;vԽd;믿vvv6cK at{EA'VT(dc"b81.Vޚl%Q(ɩ~>>>Ç֭[%mѣg>w&M|gn3-ZiӦﳎSuҔT<ΏfjCuVڵh˖-mn ?SNI=<|Rv풚t$`ӧ>:z(eddϟ?m۶zoɓ'(;;{oֻ4a„Lil۷ogKtu "q{W^&sСC}ݷz+ <<@Z'&&mxeW$… l˽ڵk'PՎF#B;9"6zK˖p-#"'"b}Tl}mEFjZ%DhtݿCtW?>'O;v֭[[nuuu%"QuwHDjZZf?5֭[GDQ|ٚ5kkRɎ[FaOIGaKCT+X&ЧOcѼy:t@Du]СCÆ kذaxx8KW]v5j{ȑofq޽*?pႧƍ‚={[nq7f̘QFGGG^:**ٳN }'AjG-DdS;e t)5aRZ""QADdNDΊu.4G{mٲE-33SN .$"WWݻ_rɓ'.]*u%"[[[";ܙ3gwlI```xxU*UZZѯ *Ν;a;wjJo߾7ou 嘀EZjjbt@Y!`ADEQW8qfWJ!I=\Y밌RJϟggg''' lcYݹs;w۷ohhh6mXu@uxM6ݸqC|EL:'N(((سgϞ={ E6mƍױcGP} `Akn'HDbuQRgUZDDdb?(X-[K b,^Xj377[*r_g;;unذ~jQQQFjݺƍkԨ! 0,]ڵ+66޿֭[WGEE=ztNNNDtڵ˗뭩RdddTUjѰ>U JaE3Z`~Kޅ +EI?CYW\p5mڴiӦÆ ԩSnns;;;z[_*5X&cfп1K]~UéK9bBBBVV011w}wwڥw8GGG"bQÆ رck߿ߘTjGHdٌVOT^=Îi IDAT+ Vuڵkwˍckڹsnnn...YYY݋)))Vzyo֬ٵkΝ;7w{<::zÆ lM`P`2VVVlޓ'OΝ۳gOv>233g͚ձc޽{/\PHt={do>|>8:u9_˖-;w|A" ۷ٳgJ;w;!!!~ݻw PP `A# @@G)S QAHH )ĉĉ$p$p+W9G""q܋D/P&K,5jҏBb/SΚ5k֬YlZvk]v]8+=j*<<_pDvvڱRx:um=_G*i镙 Pp.B/_4uA*WȣMX Ζ _ϲ)XZx1)H刈_I `l?vvv&^/D&"X ,@d 2L`D&"X ,@d 2L`D&"X ,@d 2L`D&"X ,@d 2L`D&"X ,@d 2L`D&"X ,@d 2L`D&"X ,@d 2L`D&"X ,@d 2L`D&"X ,@d 2L`D&"X ,@d 2L`D&"X ,@d 2L`D&"X ,@d 2L`D&"X ,@d 2L`D&"X ,@d 2L`D&"X ,@d 2L`D&"X ,@d 2L`D&"X ,@d 2L`D&"X ,@d 2L`D&"X ,@d 2L`D&"X ,@d 2L`DVqƊ+dk֬yg:we˖5hРk׮]vӧOv*db?>5dҥ#F1b]wu]wuY0˖-{CguV˖-a/Dyȑ#;\BFN>f͚͟?ĉcǎ;s{}o?DTjժ+"_<@ժUS>/_j?c9.y\Or{-ZBh۶}ݗ^W!c9O??pZӧO9sM.\8iҤzÆ eyŋMDdnʔ)%??XZFO=TKЭ[;,p¤I5jԨQ?8cS&O3&}1c8͛ٳG͚5;SƏoԨQϞ==sIf/QPPC;vݻww}gNֲe˒׎=կ~ծ]C9dvk֯__\\3ϴo߾SNzjѢE>}꫌e՟|IaaV.QǨN6m޼y!>9zaJgy&}aÆM81''g]vI0yջ㏧ޡ'XbE6m z &xon \pv/''gK +=XfJm۶ׯߥK.]d@,0e˖Æ [`ĉXRk6׿~?~G}t2dI]u鮻裏y~WΘ>ڴi{߿vu/|뭷֯_裏}'/^;rȵk׆ƏwmO ؔ%K$ߝ>䌫nG!v.*((x뭷Bk֬UV5v}u%u]w}ԛ,Z讻 !4m^_~u}׿5}_~铮W/O7n|饗*W_7o^^-T~UZ^zΝ{7nr|7'֭}O>VեK/8y|ep _|̙uQ=PZjU`ԨQW_}iӾ{N;wno~ҤI}ѹ_[7߿aÆx`jM&'T+O=W_}|`F=^?Ӟ~Yf;}WU*UѫW~mzzzw/^|W{챓O>9׿577gK̛7oũ~K+W}:d^yJM: 㩒JǶm6++VW3nE_~7|x ti~v͚5=9W^=wڵk7o޼1 ֮][TTT… .[zM6MNƍ֭[-mӭ[YE9XKwD&"X ,@d 2L`D&"X ,@d 2L`D&"X ,@d 2L`D&"X ,@d 2L`D&"X ,@d 2L`D&"X ,@d 2L`D&"X ,@d 2L`D&"X ,@d 2L`D&"X ,@d 2L`D&"X ,@d 2L`D&"X ,@d 2L`D&"X ,@d 2L`D&"X ,@d 2L`D&"X ,@d 2L`D&"X ,@d 2L`D&"X ,@d 2L`D&"X ,@d 2L`D&"X ,@d 2L`D&"X ,@d 2L`D&"X ,@d 2L`D&"X ,@d 2L`D&"X ,@d 2L`D&"X ,@d 2L`D&"X ,@d 2L`D&"X ,@d 2L`D&"X ,@d 2L`D&"X ,@d 2L`D&"X ,@d 2L`D&"X ,@d 2L`D&"X ,@d 2L`D&"X ,@d 2L`D&"X ,@d 2L`D&"X ,@d 2L`D&"X ,@d 2L`D&"X ,@d 2L`D&"X ,@d 2L`D&"X ,@d 2L`D&"X ,@d 2L`D&"X ,@d 2L`D&"X ,@d 2L`D&"X ,@d 2L`D&"X ,@d 2L`D&"X ,@d 2L`D&"X ,@d 2L`D&"X ,@d 2L`D&"X ,@d 2L`D&"X ,@d 2L`D&"X ,@d 2L`D&"X ,@d 2L`D&"X ,@d 27n\EdW Prrr*z$kҥ=?+D&"X ,@d 2L`D&"X ,@d 2L`D&"X ,@d 첌4u=رc 2~Fg-]t#կ_[aٲeʺ!@deڃ@كL`D&"X ,@d 2L`D&"X ,@d 2L`D&"X ,@d 2YvEG}Tr`qqq deeأGV`"ٳg~2)~)֭[ѳ?a+W,L`D&"X ,@d 2EO'L^{ݻe;***zG>wܱghW?׽{}ݷe m[*((꫾}oB蜜ko!x~m?oݞ~RJ?&CGhܸqݻwRҥKk׮]ѳSa2Vw曌q4hФI;r!*իF IDAToذ0`@Lq۶/z!#8bĈoBСCP!?*+W|}Usϭ_,co~{СCOޤI /0o׿{!>:K/͘1C)%>裧z*Xof᠃%U=tv۷W\Z=ܔ)S5kv饗V dlKK,4hP7^{qB\VK ={[.yyycǎ !뮭ZJsv30lذÇwXwqǟߦ999!:g"mnuתU{7.X`۷ѣ}ZjU,oK/ꫯX… B8?hР9s\~s <ӏdOO}>C9UVG}'|aҥ[nѢžjժ~-ZhѢŕW^BڵkFB:ujF5jtA_z'|RXXX^?\!̚5믿ӧO7|ww^vΝ;o߾f͚GqDKx㍍7n޼yc \uUyyy-Y$;;;;;߾۵kפI8^۪Ʌsc;={UV;w\v}駟.+V_~&Muָqƍz˖-KsieggרQcŊ%a=˳:ٳg{;S׮]=ح]D!u]zsnݚ4i;߿۰?WnO?wuԩ{N;c̘1%ߧ3aҤI 6ή__ݲeW_}5y*<:tӧN:۷w}weVZ=ZyyywygcN:5h77KҐ!C;wmuׯI&!#F\25|ڴiGydƁŏ?c4i6dɓ'~K.UVNNN˖-J= !L:w#F(..N ={nj՚ ._~+wرsLؖ 2(Jf q***J&ZJ/2.Ԍ%.\SUri[$2-Z3\jU*Un1c|7|s*U֮]{g~wɘ\rI#jȐ!)!A͞=;p]w!|!VZ?~=1#G\vmaw.^nݵ^[XX^{=zÆ sy*Uq+"5歷3ЫWaÆ͛7?ϡBxgo馒oZjW]uk6z>}}r!q-[J*zJ,ZO/((Q?w9sx<1c}٭]D?ԩSpu֍=:rrr2RoWw׮]CyyyDXp1rʪUySL:uwYJ5k{6d?|;;ӭ[#m۶mڴE];xk6|V۷?{?p)kOիݻwZ֬Yӭ[]wݵ|~u%ѣ+4C9$ТE.,9E:zڵkp !C=tժU !4k֬W^z7nm׿5Rf7|3H >,KFNWU:of} !\|ſ;wU_^\\p ˗ӧ~?xC͛7??>իviO<;SbŊ^~B^6m~_?;CjV-7x#Э[;#y!/^p7n\pamڵkTSoؿyegg˩ݮ:th޼駟w=裩Y}&SLra-[,ԭG5kBHBFz ,!~W_}u2sG}tnVZu嗟wyWވ#_Ɏ;*U;C;jʔ)EEEdG^^^q%G|g3r-u(Y3fXhQjF5*;u<0D3՝!SXtiݺuG,7&sW}Y2HBjպtaLmisJk7U(ϓ3:_}j,Ydɒ+k뮱c&q7lڴ)S$+7l /ꪫ4hRx_[5Brs-ڶm ,曇 x_|5kn [ `W_}uO<O%<Ǐ|&nV-u:t„ }˗3f̘1p믿ҩ]JZ$ RR eɔjժmذo=~2r}8m7-mڨnk5 }gyfrKYp +^z;.;Jg>t;>mڴ3|G! fŨQW_}WXb̘1<[of͚.#l޼>o&7&ǰ2>o椮믿>ǫz73gNAشig}Gq=8蠃>_|OԬY܆EӧOW|ŋ'^{c=J][l !;&*u) Ng2ѵkל:kȑFS!RN:7)TUV}7%wb%?{:пmilFʸC7nxmAIGժU[nB#G_ !p ɗ]y7 LWʍM)&_XXXW_}s=7;sׇǍ.OO}˗/Zj qǎvR'_y|pޔ>`39r6/:p 3fHO-s?#<2Y&guV2ZjM7yOmD>}6l's=d|UJƝV_XLǒ?ņ tܖJنj&WʢN]\t.|E`eJaO2ijժSN9H}؄K:u tҫ*mob5kVL>',?׮]|los9'ypf12ws1cnݺz衫WN?uy1ɍX|۶m۶mF+=7|pOD'(3`7zj=>f}ݺu֭[nz%?~Ww),XpG&e[n~wd&njQ?+W<#ڶm{q%C8ׯꫯ&_|tQ&ߣ3fdNiر/LnRV6mdLo߾jJ#m)5?mF)f,/+)^y1.\8dȐPF+UW];{M`_N{f͚~uz~-ZTr{rr믿Wr«O>ISt̙={ׯߑGmgoѣGoܸqvVկ~߄/_~0`ԨQ˗/;vAz_z왺P.ЩSǿ{'xiӊ֮];tkFzBh֬_¤I>C~駷zo~3f痲`k'3oX⢋.z/^sQGCM{}ϟ?mڴ{u&|Cݺu+^/=?~W^^xΜ9{9sBvZN>|[n夓NjѢErڌ x㍻Kqq^xO}y3f.,LԥK'x"aÆN:)o͛ /1cƤ_wu oSL9䛬[fL(%<0mڴÇ_xO>daRrn\|s=#3g,+aʰ%PE]R#FK'O_ <KYcg}}sQGxd<x%^;k֬wqoV}cdxrcĴiӶ;lذ+V$'!qśQTTRRr/lذ!J*F=\y晛|G1SE|3,[pa2pƌ/1c&ٳg }uץF۷o1+W|'3?0N<%~B&M6˸ԥ!ǽzSߣ9;w5kVN0侜 'PXXU3Y\\+}Nٳgj9'G{gj)Sl( ( 5j=C:uȥlKw|gK݀˸Qvq 4([e\cǎKm}뭷1=u@+V6lX?|{6yRΝ_xssχz駟N^zu~BM6M:$ի:>}'Lgc=m۶YYYg}v)eŲ+xҺ:u\veÆ 8jժ~7ߜi://nݺ{?~gO=W_:PBWީ:}?<50^JL.~=HΨK׺uO?W^;yVR+H\S4hРA %:v옓TRɛo;tvH3~]WEt'_1qJ*nzNrM{P~Ν;s9oĉ7yw]wx\r6m k(*LnnqoK/RӗǎGѻw-Z2ɞ~W"ݺu>|x~RēO>?)2a)˸Qd@u{Cw}}i!:u 80#kər<ZtiEÏ׊+c=R'[lذ!PrqTRW]vƍjJj͚5r>ٳgݺug̘\nnn)7HN0a޼y/W^&Mw=6%ZjΜ9uɸAv/_q? 6738^Kzͽ6???*e˖hѢ 4idZYfV:7w+("***3gΚ5kZjgbViժUps3֬YUΝ[PPШQƍK.kҤIr|-??֭Yʲ- ֬YSTTN;G(FI[e_֭/֭ۼym)\ѣGYFve(6Jyf͚%~u.k׮e\rK9wgԩO!w߽ɥ./ҹvء7QFe0`@rϕW^Y7ߤ/J*%_iaVIV177۬VZSNrQa;Um)]ʿTj6ʾkԨ~${c~ժU;Cm'O1bDrz\w_ޮ*d[~/Eqq9svu׌YjU-*z^zjĉ-[VZENѮЯLmK>/تUzm bڴi~ŋ;w\!3?YK.-wy3VRs]vmӦMJ?lڵ͛7! 0K.)9ٳ3fk׮QFӦM<.u?pM4ٸqW^y8㌑#G^q7pCa}]6m.ͽW_=ztJ瞬rm7q#8J*_~eZ*zv*LoN;Ըqvѣ~B2-? CzK/&T:hƌ!ۯB~b/o1}=۬Y_~e˖1*?Cũk׮9s̙3z .`qi8mذ? !;2lذYfѣO S #GwKM^r%~TkyСӧOoҤI/c[Zl?B8묳-%P'V'EZ.^Zn9昗_~CQ*..袋${kvZr1cƎBsח2Z7 &Xbk'7jԨf͚i&u͜9s%7 [5n)S|÷j-YdҤI3}u7hРo߾{orpKv۔z?|ڴieg2%KeZsϗ/_^lK[Tr~U-jnӧOa&MJ /((8p`vtrGtuK~\[ns=;tлwvڵk /?oо}޽{wԩm۶O>dQQQ1yUVN:/|3ywy! 6\sMF9昵ko׮]{ѬY_QFM6]regvڨQ#<25dʔ) ,( ={7|o1cq͛7ٳgdN9gV`ٲe5jԨѣx:tӧϛoU {LW^{;nvѣGϞ=[hq衇%_^qVqj[;wg٪U޽EQ>E"DceGscM=-he24%/w~IEBjܖ; T3gݙ}q;=SO=eoQIIɔ)Sx߿ǎ$$$鎏7իWMNN~'ڴiӧO}8ȑ# UΞ=ۡC_r;Sz[ÇnݺW^ /_zgL%Ko>22O>sπP*)҆ ׿y~H[n*JUZZڼy󀀀+V55NII/<|u̝;7 ~ae«޼yS^SPPGM4IZQGxbO۷?cIO{5jԥK5999SN7oQ} \QFmܸQi6m6`M  tΝ322A4h_?C]vQx{Fǎn˖-=ydDݻwȐ!56l1cŋk;QCDtСxe4`2Ξ=/kvb^~aÆڵHEtG}tռᖗ͛7y睿襁ڝ'ў={.---66v߾}ʍ;Zҹs vMww֭[r7C.55'6ӧOWVV&i„ o8qbĈƩlV)*-٬+(|r).E-u(ߝ)KKtiӆ?Xhڵk(::zڵO^niӦwyG~mBB{GRRRV^=d"JOOO-\2fAt:رc7ooM6)]p?q:t0 egϞݽ{wLL ? 6+WXJ 8ÃmۦztO>$_ >"!%%%s]v۶mvɓ',XL&rj ۼyh?~|bbm}wDt~`0DGG5999F2ͮ ߻w믿#/X:/ѽ{ |7[i/?<5516qԫWn۶-**vޭ j}y$$7n1bDAA`x뭷z`01BSoAxty]Ν;?ާO" =~"zj\\\AA^7mڴw9s_[~j/)))[li۶ܹs߿vڸ8"*,,|g#UuiذaǏOLL.\ڿTl@F:KzjވݩS'*;;&޽{oذh4QpppttСCOLsÆ xtչsg>88x *x-ZED<=w߿pڴiqqq7n_S tׯyoNUڴiS\\̟ Rg]ũ.Z5[_:u1cx:xN7sL٩Sx̙3O77_Dd6|< ,HJJOYy@jj<̙3DԹs^zIyT=zN$o{j\|́l;qDeU\|_&I;ɓ;?͞=)))w5ʼ2ZƨFu7n֭[n]@ֿI8ݑIIIIIIv<1K{m~eӧOWFW2/]uVnW+#GWܐ!CރRSSj'EFF&&"Xn޼y"m2yʙ3gxGnݺu֕+WHEQu:]iiO?DDݺuN*zR-vmܔ]\\T󟕷o߶<@V222mW#wwwՕzx!uvŻw9rd>}ڷo:/Dkv펈rssO>M_^+XAA*ٮd)&"vQtt  twޝ7otxdJKK#"///oei?H"JMM]f͛7===l٢,Q _OV*!"xٳG5ƺT ,RŮzfw?o>efBy.|v9@[.]ܹs6fU#Jf233Gl;o$惱d>>>/TuiU1pBe<o:0$I:u-[>,,lϞ=rR]]^bWQ@=;ر&M__~zl$o:k,~ "2 ^^^?=2YFLɔnck.4(qqq )>>>%%%33<~駌 ƘoqvN*:t裏>rM6>7o~+W3g|aW5Mҧ ,222yëiӦ^^^^6ru?v5kN:gyyy 9rȑ#ݺu[n/<;}lXsǎjժ`HHjEa޽du.\ȣcǾr -ZP&^ZQΝW\)Iҏ?cǎ+V?~|ܹ-"F2Qxx8d6Gr/}8p7ߜ3gpvv.//Gk)x [ 333GVoR-[쥱:238V]4=DtM&Ld [~_U\]]|*S`fϞ`!x<xKyQlllN_qydHAwve*ck^^^ HJJzWɓdO5KuKNNS&hx5k?ĉLkE {W=)۶k׮2y'JXAr[|9z>7VV3e0x`&2o9{w.)iLUJU%QW>RWɓ'ѧ~ʯĤ8sLUZ/2*****'"ooo^ٱcǬ 7`rr%KT/]BCCu瞳 _PP0rH>\Gҥϭɐ!C\\\L&<*))ٰaÞ={=.W|I&YǠׯ_g͚տ!CFyئqw|R}Ϟ=Ul6+r̮i96h/t`͚5c>6deeVcǎaʑ#GTOAAO>%gx*+M2WVW!*"`=J@{Qk׎ 92wƍ$-kDd2`ٳqqq|MvvvrrE&NxҥrӰw|e]X@:u*XΙ3g֬Y:sŋmd?>_F&)33sݺuOOO'"OOτ'OBj*"ڵ7ĉ'Lk!|877wqqq3UBː"f>2-KkwuIVGFFS%;ָjP ۞:l#[ڨkt:]v-m6TwfQe8;;W:.H 핪JՕvuu>?gٳ}q?'N޽{N)VA [v)7o^FFFFU9w\{ԏFre|ٟtU R1m4Y:*yۨQFU;m$XZ۷hҹ u xc; f8,C` !p0X `8,C` !p0X `8,C` !p0X `8,C` !p0X `8,C` !p0X f( IjƘ^զ5 @v=ϔmVQToqwh+5v,77.ԅ$IMhn WОz$dDEer3XrU-XԱtӊf>n1$I%""+;[NGFFF'2:'r5jJ9TH$cXPWH1DYXoXK$.weL+,DQ"bT1(YZRuGݳ4&C" "Y,#+T?RYPnl=+,HHH*]1V'nNȠz*őnӲT6L 1%ނ19R> Vэ uMuiLet6fIߦWb'"Y&,떴dlt{|ii?YKΌ!>@c$$$˟R;=np;R8S]C(nY+?vmMT@[B_cTTFo*z\=#܉ yk 1*!WaD/?jWq ?eK~R @FDÙnD$G2:R b1FefCD@wԔH 1 ܈" 2˟Y&ֿGWD~+NlI[Oԥ%Ȝt9 ۚxJk?YOHv$TL*ױfC˟ @c[x+*C+ h(nzgE"5plFtcS⊧x'$Ifpfz7gc%RD`D,SFE"wҗ'f4#38Qg-CO'I+{kםhi߅MU Z[Kk<}#A"#1*R$~gHh Eȧ˲ghE$Jn4##+͢f ֵ!">?L$%K%? @cdI4jɤa`l2W?$rrSK_&yj6%Kk6;1Ђ&H4 1X[M0>nDDoQA2~vFHj쁶r 飽?eUh]$f1Vq*Q 囒+rΟubR"KUDEUDߍ%6,f7ZT1$-Xk".`f Def2:q@[Ń*Rt&gHa@ٙkt2c,IQL,]8>+A$H" k,$z[VDDƟ$af Kb$gHJyHhߘ[~*TQDd)dIʏjZ#Q"A# _2,";4- "Zxs2yeݪ -zK*1.!d$Ӽk;?r=/%Rr3 X%,B2jHdDԭ5]ʮvn ?7 *'O7uhN]Z*㭪ioB!@c$V$H:ҾMAՏP6G+6ەZ)(7JE&"PYԊ>يc6`4FH( "%,_$JLϑ9CDdtUǓh:}ي$/Ҭ Ws`D2O 2`BJD&HL,IY=/b{7%=ϞJ^LǨ'ڙư~DD;PN4zhq"" 9DK-;B[ c~D b=UXq#⊛4܍e )SZ=z8Oc>wVe9Ԃ/A4mK*G1ݒy<3Z#A*@" REϚ[,+dy),V4e)*O॔zג%ŗj}^ʈG"sL$ABY: >`2:ts Ɖ,Ещ:4>Դ VDWTZЇU7,EEtnF`E)`݁1Fc(%R28Ae%DTcz,HH,Y/-xF,QDW$r0*B+Kk 1G,)nHXAʙE=E0< e3)ZXճd)sXȟhfLS#,FGWQe; CUeY"Q{z- ţTѺbՇŰtĒ(YFfz.jBH1ҩrVHKX:hILH"2Ni 1j]NOP }w/dI1̴dv4,HbDb:b,eU :/IzEDDHHH@[.`24Z(]Z#U'e4Wղp}-܌FN_[-X(&d2 !UC`zggg`0GW.BLF'''(I1-GQ&c<3(C Ȏ.!p0X `8,C` !p0X;Gԣ%IENDB`././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/docs/source/traitsui_user_manual/images/Dynamic_EnumEditor_demo.png0000644000175100001730000014521700000000000031364 0ustar00runnerdocker00000000000000PNG  IHDRR pHYs   IDATxwXW.,A F#D׆Fc?5`/{XWQE#QR#Ut0Er>̝;.9s,P04Ms RD"X/ktxGw-u;8+...++D",DA*8$!EQ5w"#Ȼ JBeE"EQ5wAA4qq,R%j↌|y^[@ ۉ (r78&N|y^B4M4AL3}BByii):A\ZZZ=A34+T>~zG h 3 S=A34+T˾7 A Cݽgr  4P! 4P! 4P! 4P! 4P! 4P! 4P! 4P! 4P! 4P! 4P! 4P! 4P! 4P! 4P! 4P! 4P! 4P! 4P! 4P! 4P! 4P! 4P! 4$um@ y/IKfG׵!uI\|- ARDGD:u/vkUlmԠҐ@W<48q$C.3޾k H-@i( D"UY"QVR~ b -m)} Ĥ\K(ХzC ʾHum?Ij 8}QPZ2Q_6D@*zsoF|nPWZZshn߾}v4w0̙3gRRRƎkoo111"hȑ>*2gJJJiiKn4P)>|T*6lX\kh+PS>䓺6A.10n\}2~)//gl9(hsirt rDz,˲@QX,8ܱJARE /\gnnԧO-[*4ޱcGii_|Q͛7w)))7nihÇ(Z뛝MޖT*:TO:%]٧7@WiԨQ] W\E}JT ҍ(J舢D"R T_1@v&΋wvt88 E/&wFɩHJ$*W" ^k߿l2\o ?r䈷СCukxc'lB4K+=䝻W^MLL5jT]R/@ه {Q,Q|@EqE߮| $`eJd563ndB7.:I]hݝ(* m:}eTN ؤFGG[ɓ'sm0陖6f̘*^)..~tuƍZ!߹p6mڠ#C!N5r\YcIqU*Q ^- N>uSƐ!72Rafj,63gu+^WȲyׯ_O4pV_XXAAAPPG}Խ{w]cǎLzz:>]>As8b9 >K;׺"|Gy(2#~?ӗ@AǕַ q,&65@Rf msrrqѢE }577={6} $''Q,ecccff,633F=i{{{D_v)/mee%MO>ʓ#)((`XC˒45k&JYmbֶʝho3qYYYVVVU;]݂AD$z-o/Qp,GQBQ1dY^Mدݡ?krLLtʟgg=zDDDYYYM4}Ν;cbb Ã@KJJȼ@//ѣG2 LD'X[[wSl6lp%5jԜ9s>/|ld~3zn۶m!!!۷߰a͛###"_~s577WwӧOڵdҥKI<%Kvq޽"++SNDFF[:t0cƌΝ;+ w}e❥(w޳fp駟Kyw}ڴi:t o=<<0y?sl?qDHHH~~>baaѱcGSSSͦ͟?_.d={4e 9, [.見|;9>[=y`4iZ)^h*eFnНF %tܹsf5[dIFF(Nggg/X"8p 55uѢEd I42[n=wpKFF˗cbb֮]~W4hqC6mD:u*''gٲeFY-))Yr;w,^~ɓ'SgL F:⅏OZZ*ӧO,XPPFn߾M6FaVXq]a>ڵJ*Wo0222::ߟxsʄ$y,ˮ\?6mdmmԸ8ooo֯__P!r|it 5UL [poZΡp.2@jN r##N(ŋbiӺvڸq㨨ݻw_tiԨQ*q=zh>GGǑ#Gv%111,,iiiV:'|ѣd{n>6mFEE]rnԩ®}Ȑ!-ZHJJڽ{wRRRjjٳgǏÇӧDGΛ7?!:t8p`V4EM>?(*""bEEE{챴$wE4_V>:$''_r%<<ڵkN!d,ϟ?9++֭[۷o/++۱c;w,++۴iݻw5k|_'>L4_޽GݢEǏ^~NRij||\.766޸q|k}߿gϞ...iz}YG߿MH4soӽ{vΘ1z{ff޽}:K4pV&U {#*U;)/BWȱ J>5H.˲2}@DQ0 QKzHsYYzV L>S+V׏mڴѪU8R'rrr87nlܸ18::0/444)) BΟ?^h?؜@׮]@֭w}ѣGN4IaڢqҤIeeeS'[KKKu.}XɂD~~~{&WZ2}tHII9s 4idƍDhjժOD{nII ̙3K.`cc3zhȈYd```0FmW^td ktttNNUڙ+ccM6ՓJ~(UrN8HٳgϞ#GرB9s/]R N&]t-~~~III}嗕_'N(|IJرݻwSRRj믵C&{ 9r$11x%= BH$?s4÷~=cǎ>X{@eD2pSN%$$,Y'&&C nݪlmmϟ_fgcƌg@ffsΝ;zݻw++Y)gϞU(FȾzre O~ FRvΝ֭#ŋ7oSm۶Vm70 ǰeLZͯ%"|RL[MXK/z[4]|fAq7"{ d 64,[PeiiZTT(^uIaW_}|֭[i>x1y!/.ތ8RTI\$7m011qppMӉd#//O&m޼^{k\]xÇnݺj/;;{0nܸ9Q~ѣGĉ#""JJJM?V#Gn޼ӧf*S.-lGݽ{am0LXZZ3ٹ$**ɓ4Mϟ??;;{ڵ!iiidFK޽խ6a9z*PvDq)p J-R˲Gv3ү3 \{:zHmf?ϓV/ثW/]WZi5"P7n\i"'|KKK__;v$&&n۶ ̆ 6~xmf8ڵk'OSȆQ 5MkZd2u:4sN޽{Ñ}yyyӧO'oĉ :uj^^Ν;G=`]H$}]uZ\?ԩӕ+WަΝ;ٳO&gϞgϞmڴ)sԨQ{]reuhD,'Wo&1_u{ڔge1hlΌ3L˕Ѐw埊(36iT.B\8[ڈ$IYYپ}Zn0(T;SீRv\MunnnAAAW\IHH8˗Ǐ 5kֈ#fؽ{Sk===KKKSSSL._ZZŋ+Wnذ%B_NO?)dr=ښLڵk:0ÇG]גiii|0LJJJzzefʹD۴i6A5ٳviEi iiiϟ?ɞ[nWj>ݻϢErrr.]4rHxRRR5kVa5rUy#ǧ R:X8}H$ED^gC=1bLL^ɕȤRiRtؑD>|HӴp\\*k.<<\__&M,XeUV۷֖ۦM۷om۶UpӪU+HIJ!CMqܹ֭\Y'.J'L0a„DDDmݺG*jsZZ|˖-svv5Ç:|H۶mׯ_zȻwٳLԋJ(((& E 4M0xccc4 R ]mی7sB]ttCzaffb x0ɾl7n\paȑΟ~i.]>Ca=@O>6m9o|r1c<{쫯rvv6lpmTTӠA H,K~¼ߧYfabLx,Mޒ &\zUǏ'M&O2EYhoذa懾=Q΁bii>tPjj͛7~'+?ݻǘǏ/^v*W$1Ц8s]vcƌ!wSwy OOO*J2$%3snn.]<{֭[6mx"4mT]ͬ ׵k׎?N6ZYYM4 -ZãG?{iױMLbaaq@@@\\\ff7.\3>g} ,8|rr7{~Kj*66޽{˖-{)˲aaaw\X+D{Yݼy\mڵ+~;w.33>祤 IDATӧ K(ӦM @iieJCuH})%dϞ= ,իqAAAwffݺuF2449rd^t[;tsspѦ͚5swwwwwXvŋ4ĉȇآE gg6mڔdgggLdʙ2_U+@V\IB~~~{gϞ{… If͛7o޼)J]]] 駟*Td[柆 2q'2Zh1rȰ?C rG)3v cg~,Ddžܯ@QԈNfciTߊ,zHYTbX*rۃsQ)Eb]is!f͚UZZzܹ@VƬ'fdd'%%)x-,,-[n\R~1cƫWߺuk>}zZZZhhhttBۻbmmO ߿g[///Rɓ'/R܂>|xݩSJ$aߣT*={VӷoǑ];;ldy``\.ߴiF$W0`ǏO:bŊ͛7mzG-}?~WV`ƌr`dd+E=z4;;;>>cǎFFFV]mCڵKwu֔Dwށڵ5kVFFѣG6׵ȑ#'  1j tЁ|cN2e ovZdcƌi:::Z[` Yf}իWǃ RެY32lذJ5!,YDI߾}}||ܹիI]k׮]vmɒ%...˗/2C'UT&MiӦhR żVآ(zW ?,-}Ҟ7#+_u z8=͕.D"!1/}}}R6ٻ,eX,ԩɓ' fff^^^|?9ʟa+(XeaaOر/I fŊ$رcfffE-^|PeBJTyJ*?j:WwڶmnݺjU$O4I@"Zj׮]W\իU:*\OOoǎǎpBff&yld|h߾UcbbOA{===yK˖-g͚*S7oMHHCɡڡd\.WСC~=zѣG ))I'Gׂ ݻGN8÷o#aV˗/7n6FUKeP.\GQTfrxG)jܸʕ+$ON*rnn. 𷹻w ʟ;)rmP,޷o_m~ EEEU޽طo_eK.]z5"">lɒ%k֬Fㅻ ^V@Jpe>e߁Zb_ƅlw^EOE,܁Z)E˱ܞ?@X*=OHwRz@tT*%ڄwKhn_ϠA cffֶm[u.]29G*\1V,Ovvvff7))B֭ۯ*H-jB{2 ؍"82Dtbx˖-bYVDSiӮ  (7_;HFr~IOeVyJ"$,Aq&RѼ}[b|R]4,$Hh;vl:5 ҁJӴGlllF<<<|Ajv\]]N#D~zQFdow߿[XWh?##m>zHeǛ7ojsM2 3rȐ^"*PqP{D]oooLFB4j%^<)<q{PV>)F{1CkfB ]ԖBGuT"JItuܸXd )?2TBh^x1q̘1CZ׺ٳ;w9s̙rooo;y0YѣNNNNNN: XsΆ -U^VI&$Tz9)S/K_\W, Õ*;Pː5H>| d1ߋTUWYTSǒ<8+Az;,tH"w9{p\.pss:uj޸qFþ...^^^W_}?ǟ9sՕhCCùsV---Iڵ+663g$knZem駟tNU5kFGEE 6ٳwYfԩScccja@VXA&.^xׯ_{ C 8ᄏuV^^^vvvhh#Hdo?yD8qbݏ?&7͍ܺug˖- KwX]b)6+gX\sźmE;Q!R offfPP]fff|5cggGr肃]]]*M7o^hh/|||RSSݻw^RYLJd~lڴiܸqUlڴx޽[lQ^#m۶|۳p]v=x@a=iGT044 rqq^dIΝɊk֬IJJ:qDXXBlϞ=Rm۶Ǐ>|807nzfϞ}am*xxx$&&\e٭[*/tִiӧO b|% ƛzyymٲ%''GX,?~Yu߁ڄa#e9fg8R=+/\[P@e8\AO-y={6G ~~~U?/^,Y*0>|Ȉu 2gϞ~-q|hNFB,ٳرcĉ"믿T B૬_ ]Ȫ?zvɓ'b1˲$)C-X@ onn>v>5XΪcWR]ch?:tDže,--o߾uV-(j.]j߾BUSS#FDFF*/:ry ji͛7oL %:vx#F߁ڄaa9jg{|7anC^Ua! 4zJ,WWaÆiNTl׮Y5+;;eo]w $+:dK9r*OzΝדtWŎ* EjëWnԨѡC{R[]OIѣG ݺu!IIIyⅭmXiɩ q\JJJFFF&Ml(((iܸq.]5wEt~~>˲U6>###%%Ne HQ^-F-2l\q7235@UJ(((TVE流]NV{G>^Q㲯ieY] 呵CѩqIIX,~ķ}.R˲vN%Pހ"oW]>/RXJGه H}@ݽ}("HvlԨJdIA É8~WSVrP wqt+& >A?CVc[N{s;%.l)οù}{P!ò@} QZ>m{--N)8>d5 H5A(Vl^}Jыk(2Ҭ!RAه HmS+Y˒ {B |KJU )7+(}kP!rh")l-'le4ֆUY P!r}pUy ^S 7SjK]Azސ/5 o Qj|"OM~^bYXX`Yv\ǔvj5 ZAs\=yH{'`"e˺*6ajJiH쫧\|M@Rn,U@ M7ofVֺLOCvKnMLމ{&1R (03p=͕ƾFI'vw-3a>lؿM@nOCD"YG,b [ːO3yi`~;(Q ևG'qAW(dQxTM컖`^uCK &ZBɄ.c4`P%RU1!HR5GL|n *+l"TAjϝ;Gt۵k+aΜ92vX{{{̈D#Go5 ϔRnݺ@\\\pp|0bĈ6fP%؇⺶A%cc]}CpDz,˲@QX,8$I),,pBhhhjjj^^S>}Zlxǎ_|E͛v74}ztϏ&oKJJ[fMRR҅ Zl١C:fP%\qAi]ہ Huc׈ҴL]yH8PaNzn|@QXR_l\.憇9r{С5,[l!ť}d{9X.ˮk ;A&t;ZTyN#'ّ `j_鉈RЦ۷o/[i8pMzzzllltttYYٺu Xmf|V!xeus8_h>www___ᬾ0>{)ǎQtrznݺOم,(kcv3D%";E 8Kko61bpDZPI;~N?~<''-Zѷo_ssٳg@pp:0LrrN,deeؘd:K̬s5TT8yaeeU 9qYYYM4iҤ6ddd0 ckkkll\i͛ 4,mmmR6m8HD\'[m]ہ Rݡ?krLLt_zgg=zDDDD>{lΝ111`hh뭒2/k  ֝;wlHr8nÆ .]QF͙3ÿ˗/L駟Θ1C(mҾ} 6l޼922 7w\sssu>}]H ,]߿ĉ&&&!!!U>իW___~G%ۻu矷i&>>>000**ʕ+vvvSNvC iѢERRݻRSSϞ=;~xf>O>O>%:r޼y~!hptOY8X)zM6:::Z[[Z*;;;$$dC ֭['n*))9stlllFMtxx8i9̓ ,,=?WeTJ}7P"Rԫ(V&I$*˾͛WpttT)/<eoMLLUr-BRe:KV$$$PLnnnnncR;rrrmfffÆ ?~>A[7)Lg+ =?QflҨ ]_j"Rb5CQD")++۷o_֭]\\{Z(JC|u2겂]r%!!㸗/_?~<((h֬Y#Fv 5(AE"ޢz Ԋ12yw&Wj JU^ cǎ$C#پ}}Qvڅ+;4ۤI ,jժ}ھڦM۷om۶E7oN> ij^VD"˲C 6m[]*N0a„  (**ںuk=߲L@AjD"a)}w$y+Qn5;9W@$UyT*ѣܹSeaV^iӦ={TK)@aaٳgϞ=0)mݻw9s&Kжm[i,"I$7""Bq^__/`llܧO0;}U} H"􌌌y,,)_xVL&ӫ_>>'O^xq'ݽ{̙3͛booO^>yAjFyX,חdQ/>ڵkmڴ!Y&MhѢcv!===22СC,6mڔ(zԩScbb1cƸ߻w/--Dyzߔ)Sn߾٣GF{!L8ғjɓl2//-Z_{sss/ } H5c"eYN$z3f+R)q=(8BQX,e90V91k֬sΕ*4_jU5fEL81###888))i֭]˖-S7.T?̘1իWoݺ5 RO>=---444:::::Zxi Dz3g5joܹ ̙S\\ g-88Çwޝ:uD"a$Hٳg;! eOs"%#HH>EQRT]Gsť>>>:u:yB333WWW///Ze@m" zyyYXX-;v%_AجXw;v̌ŋ[YY?τ566vqq={P>U^:ҫʏZr纞СC.\jܸqvF(c۶m׭[w^D"{{I& 5~vqر .dff02>rppt-ŒxX,F] R`Ya iCMwOH]d6eI6R5oB HNNNMM133k۶Š0/_o"0r:'$&&tݥK4HA~~~zzD"# T4L&LnhWAfZGIeKEQbX˥88R:*-ZhBCXleeE֫mmmQFQO֭.\EhllpHdo AjڄEdU\X 4 lva9Y(,']&L ggG-[,444--͛7֭[kYv0ȋ y rIz~jLyYwbVŨ9ԭ֢EkղI u } C2W\9rkEwy֬YR7 >Aw}ȻMϟ?/++k֬A]ӀAAjDSx   @ه   @ه   @ه   @ه   @ه   @ه   @ه   @ه   @ه   @ه   5yA!C.` )WO^">0eZ[05h%4$P! OLމ{&1R (03p=͕ƾFI'vw-3aA^AɐO3yi`@DDF(= D>A`n/PI7`P=!H H5@8N(|n0Kfkhp9۷o߮];]1 s̙cdF|||LLH$9r$o}?\'9wq]tqttƞ (Aj&)WO.q,˲, EbHQe?.\ MMM377wrrrrrӧO˖-ر/T:ܼys׮]qFo>>|XrkNnݺ<==Q! HKD:ppq w;pT$%OCQX)I_l\.憇9r{С51HAه T3:yiztwvd7CWz"TcMo^lM`aa1p@貲u=ydܹ \]]===ƌSÑ/~5A3|>e3ߺ{/+dY<ׯ'W8/,,߿ ((裏>޽vczR-ůFq %q,&65@RfsrrqѢE }]nyaK*e٬ؗ/_j:K233=z0ZU~yRR5}ǏW:X.K/2Ǒ󒯄fccc_xQ=fee! uځq`L6 -31+ ~~~RTs="""Ϭ&M>{lΝ111`hhg`y^^^Gd& 88֝;wlԨoذҥK0jԨ9s>|_~ᥒL&Og̘!޶m[HHH7lذyHbH$ׯܹs+hZ˗$nݺ“'Oƒ&M̚5]g/K/2'N <.rvfeS@Q%ED$15MIfW, fanhqo7/&4rIM͈ԶͯK*h 8lx2s9vz=) _d]SNeddܼy|1cƘ1ce9sK%==='N2LB=0 tZ;s"S"뤴"""v\~ N,["vQQQHJBcNKMM=rmIUU?pk6|| 37mܸsydyy.\Hj޺uk P5q!CHҜ/233~Z>zhYYEQqqq&M充~i~~?|駞z;j !jCd$ai0S,(" V˸ww7h Bvpc-}͡(w1b JW^ͲlnnnsGmm;gÆ :tয়~:999;;[RGDD؞ErG37)!!aIIIS RTr%ݻil[߿\\l4ƍZ~M66>Bma'h*L|Yyeٹ1@TY?H$"ﱐHN [>}ؕP[QQY\~CFt .())iN??? #ӊk@߾}\},{-3n~u&G20$in]RFSOdH[!Pbf)Q KϮ2l;i /EsMMyCҺ1eeeO<񄫧7.IԡC%!9;)qK~U(Yr˯!nqf\},{MbYĉ())q\~mrMhhh[ٳgk4SNcǎ;vㅇO6mРAߓaڇBͥ}_ŋ3GHZAb2{HYl.­|$EQl6o߾G?QcDQT iV+lݺP(dCޭEɖ@.Yիt 7%%$LB-@ˆ!zw?~ka--4W4m4 -Ks{d!Ç7%{>sVjl+VرeYz;wٳs|~ZZZ[ovjcq ZM23__ߤ~qvO!!!<aJ؎oӻwUV,RN:j o߾tR7}%!EQ<O"@nMyojnhb9Qb˖-MֱZk֬ٸqmۜ'v!'a0/ڵkG?>twy;JVKX,emwV<./GDD؎}֤il_|-ۡ(*88x{J/8Gm>Bm BTZ^'\`;xYL& #MFL[Vud"c+i4C7o޼ys]]/O? eee+&aڲej~aӧO[~jmwV?$v~GPXXU+qϟ?nj\NM[8ȋB'O|H$r/j$;Z@ջw'>|RRR{FT*Gwi~饗ZyW5o޼>H.]t„ u֙3g[Y|7JKKO8ѳgOgԩS[RR8iҤʜO?aHv׺r?"##C\__Ō w{̜9۷ojC dϟ?|[oK.wܩ~j;vD"Oj597*LBf-ϧieɽEQD"˥|>]}%&ȑ#&СCzjwɓ'WUU}W**55ORRRsEyݹs'##GdzΜ9j:;;nQ./^؍jkL/:tHmܸ+x111v?VX0́8mqe̙h4 BL8p!u63Mu e2\.Rԙջ ݽ(_d;Clyyy=svsK7S 9%("\9g…fvzj.w}\SN.cY6998MQʕ+⸁E#Gܹsm'iGG Tהn3> ,X` _~Ȑ!# +z7xi9&&棏> ef3EQ]v={ lB ;w|}N@!v XV?ϓW93k0ȺpƍZ//^zxZ555oqKojtj\IZVtKYh4Zݐη0Znhh Ů݀;4>zh<,i_Eby,}WOB5!r3u]F , 492 k<@9BnK:B_xgҘS.Viyw 6G6 mĜ!wA^zh<, yB!5LB!LB!LB!LB!LB!LB!LB!LB!LB!LB!LB!LB!!УJ?甋UZȣ>PLwosT`F D=!]V ).Wi(v:k5=y.mD}A^BnVSU"(@*j˲$Q l`0;v,;;ۻO>}6lXHH]t4mڴG,=zT ?LBsi ˲, (˲x#wd֭[䫏ϫ / ƺ.r}!a:H Fx)d3JKK+++ ""k׮U{öfٲeаcǎDۚׯ__lY}rʊ+6oܳgO7~7%MJJ-/>Cn3k)((HIIv)۷o/^XRq%رc)))\F-&C䶬V+9Dk[;~888}/ڴ7nX,aSu0C!Զ3eYX*jRH$]qOhXYfEFFv!77w֭zロ0aJMMMbbb}}P(3gΠA(:|FFFCCÊ+mK*ٳd3|{YZZzСK.3g$5FcFF0K.ׯF9{͛fszzΝ;[=z>z]/YYYJ… ۷oםŽ?.bccr9ɶ_{,_~K$ .>|ɱuREô!P"# DLbGQӲZOX;Fczx]l9kEQ#׀Tzjessso׮]</99yȐ!088555YYYsO>"##SRRDC??[EEE{:u*)to0:ukXΜ9A*'LNN&!!!Ç={vmm^z%Ubxݺuv»w&o7ۻw&$$pׄKO"Bma Wx\d~kD;YL&NZ^pp0#G 믿rť\\`())!%W^%sεZb)SaRI N4iӦM6m"9KR߶+~뭷gʮz'sW@mg@߾}NXu!Pb& Z$ eV0cYvn /Gu$H$r\EqOݩZǮ(__ߊRbX@P\pBk׮$NgWk\7,,iѸw޳gώ?~AAAvS]  RFN߾}#ەzZm{ [+q)p]LB-a*öRTHN!q!ʞx WOv,[T*ZljؑVj\Bc"Vii)bŊڲ4z^yOOV@69ݡCBZeeeet= +q)p]LBx"=`Z8HLwO7%XHX[qY#R+qmw5jСC333GRɲ۷ۗ?~xW [ iGhr/=7>%!j[%B>Ļ[ɇXFtr>'㤅qW|xKz[7nܬYZܳgs&7tDt\\\\\^:|CCCjjjttKΝ;УGCvK.fҤ3O:N%lSBt jCEx}ʕ+mk-[ ?0}ӧsn:E٥h7۶DՒ`b駟l+|->{hV^mO>ܹsv]zJ]p!9E"L&UU|Xjy33{8qÇ %% h4Jr߾}㊦^zu75cƌs֦TWWGGG{zz煅0ydRoԩ{-))ILL4iRXXXeeeNNΧ~0L@@ɐ 44pnݺL .|GW|SNZ駟 *((KrMx<^HHHaa 6mϋDΛ9sfAA˗/^8q\$jĶKO }!LA3 q̖4ͲWޢ(J"R>.ÂfyxttСCU \zuW 曍G 3p_̙V lkŋsGW_^ti̙j4M\NR0y䓿-4hмylKxŋ766feeeeeqdɒ8pn//C.\ nr@r.ի׺uv(</00pԩv)EQ+W;z(U.?om) ?3\KL&{.\ؽ{V@HKKۼys^^A ;6>>w͚5qII Y2,JM6fqρ;>cǎh4:{„ Mw)Iu>,B0VÍmd{s="p#/jc,pfx`uqƍ^^^z5IWVV .]y4jVWWt:{v4׫jLֹsgw ܺubfh4޼yS.2w3fQrFs+0CÒUiYT- ,ǯ1:'j޽N(jɒ%vĤ>|/{5s;Bn毰Nh0+20,X0_: +...**JJJVƍ|YmӣG,CGT'rJ+g5PLwosT`F"ͽ;[nk׮ҥ˟R!{>>㏻w.//'C( oݎ0C}aaXfs׮]v !Bn&vA;Bv>Bv>Bv>Bv>Bv>Bv>Bv>Bv>Bv>Bv>BvɋBMT'rJ+y' m l诰a'!rj'27LBYNv=cRr1{9r$33ի:v!r3V.˲ 0 E|e)*wUYYiW(ѱcGעAm,==d2M6w:>Bn3\ZAes};~2̡xQ< n7cl꧟~R*噙0`kJRbjg?^VV;a„ jC8ȋB\#9by!ya !N[شREEEu҅yyy˗/ohhp%ɓ{=v؃-C!9| t6nyiMDppulKj|p…_~e 3B/LB0͛7jNBѹs砠 n/eY.@UmJ^s4M߳}FȲlMMFiP[!F1''bhhh())j >f Fh TDSO:tHRyzM( wؑ 7o^DDWr'u|||lْKz(R /0g/---++o߾_|g}ɡ}͛77n@bb>|ǥf-f͚FN2,)));w!ߟݸOxx%K,]Tdm۶uҥC!PXXq5.==ݱ&}˖-+**2YhwEl #:!9wժU{UUU-ZRRx⺺:D~z!Уd2) B!eF( 򠠠*5QZ<PQkpL'D"{Gj5P555BpΜ9 ( +Vضm);v>|xǎ/]V=ڣGկ]t0aBTTZ駟{oGi+"9~H$ ]vmFcFF0K.ׯF9{͛fszz:۲el޸qKv횒"ڵŋ0dȐnݺ]z5;;ɓ\pa.YD 6bnFczxݛi4?гgOoڵ%''2T _zuMMMVV֜9syGollǏq$}ܹ3 6֭[*֊HbuGW_x7 :uX,gΜ J@&\Ν;˖-# ZPPP[[dJrwܑ7nĝ"Уh44-|>Ix<,h&0LSz+<WEI| ˽d2@ழb?~dqС<9rdpp0؍]@```||mIg͚&qu…$,^]"'x™]5iҤM6mڴə ]0!!aIIIIѣ@T:4(++Üτ}!( zxf̘ ƕJb&jZtպwN>;+J:urrEZ]__/Z 7|}}WX^[[[VV^^^=+­hn,{ĉ85CO }!(СCUU0d")侲,ۡC^]q0sDM$&ûϛ %_~!!!r-[b6sm&9F:thff?T*Y}}233ǏFuփBC&UVV޺usE"dVZO_/r}!(}6٧C dW???++ -Rj|IIIr]v}駎kk֬ɹtҶm,XEN%!(x!!!4M[,d6fdX,4MxBDb[55&ۣb21OG$-,mҕ+W ɇ=zrld2ܑ/n]$&ˇ ~z2jձݮ:/rDDm]AAADGG{yy$E>tYs}! =z o"Keiܹs=z JRiypjRgg2hA%z~ɒ%ӧO_r)۶m-QT| 4ݭ[7[YYi[jvB+"q֭[ϟh"`[H>N#ܼyve"6#lhh/5zxxY,}\#yBAQT4>pE"L&UU|Xjy33~v[Sf̘qܹڔhOO?t59R|С#F~$w-[YYYO?}{8)44pnݺL .|Gmܘ2 o  022:t{/^ BZгg˗Yd2%%%m߾˫7Z@i4;wp[}"`jݦ.it(ab466כL&$\.gW2^lٳJeXXؖ-[ڵkofccaÆYT[[KֈĜ?99yҤIc}YnR= 3gpX,ee2YTTG}d@߾}W^NγtY-X`rDHHV϶m6zע ??% aoB=4޾d{s="p\A6!HW `0Ⱥn+++A.]g@:UWWIIo_ǎ X,jcǎ{"T__Ve2ݻC544fDjZF]6 ۇBF6B IDATjH`QE^L;,G61rqn <==]vmH\"ɜl\"H$&97&|MإBt} &0o, Lǯq }!/O^wBi)ۍ <(h&8*s>>Bm_aPFsR_fhРA"HPq\.@ò!`.!B}!B}!B}!B}!B}!B}!B}!B}!B}!B}!B}!BABTPs*PgQN(h9*qThچQ"Ԟ`ڇB.+ŴxxI\;ay׵tot<@6a"Ծ /B!7)RxBGxx AQxJo0jw0C!f'˕b\ rRjp!唋is>emR-rq܀z ӧoݺe2>!۩BWvYea(,K2?*wUYYiW({٫W/] >X|5LBsi ˲, (˲x#wd0C!| t6nyi͊kr_)**ڷoӧ É'Ԉ#3J^B˲,c]0!ڔ--xp&ח{yVͥZFJT*-=+744T2.k^vMڭqdXJJ2.b!Ѓ >f µmPp[MӁk wؑÕ͛7/""±{V/>C*y2Np&c2eJee%9$&&vI*\xqϞ=+Y2iҤc:vڵ;v?p4M#66vv5wqE. x饗&NlNN\ZZj64;**j֬YaaaM+ô!0Luû?/b==%,7opׯ/[.rʊ+6oܳgOW+s+mZ3Ͷ\v|r dڵӦMmJR-^ضh4^rʕ+Jrܹ\ɲ%KKEE͛ ],EQ$M)((HIIj_}!Vs,k]@Ey1T*D9Gyyyjj^/2W^SSX__/ ̙3h Ο?аbŊm۶T6lד`ѢE oo۲el޸qKv횒"Zիi:>>><C*,OF3fN {챧zjܹ%++}wh40o޼8'x^3 iiicǎ%?K.|o&Y"ݩSXr0 R !Pb&)fR4]_Y$퓋o|{'+TV}u=kqq1DEEqi1rݻwE.}]~c1c)-->aϞ=g̘a[988Rz*i]Ν;7--͛ݻwnF~~~tt4WyҤI&MrӍ?}!0qO"58T,˲scx>T'HD"lzͭu`YVUUUegggggO:uܹ`X@P\p2rz5եmV;.0d2'D1bĈl&5xn[k׮/,,iѸw޳gώ?~AAAv|!j[ 9=J1}Um'-hMMyCrS^uիW_re޽=ùPH:dSZVz{{T~whvyiVB5RIjn tޝ^zdϾ+VHOO-++KKK//{W^l흡>Bm..^H9B&SEr /99^bYÇ||||;JnTQQmKPr2 3V9jԨCfffJe۷o۷/333>>~6,LB-@ˆ!zw9k&bMm:tСF!sBCCy<0ƍ5kVTٍvEr_|q\ךj;vVBl6Vb"44[VJxTRW^4M744FGGߍ?!P(I$Hڭ)1M P,x1f4kkk D"AnK]^^^ϷN-,,{@  ˗/ۭ\/~YYY  CBB5Naa!лw&#Æ [~ Vv_}! RNxwx75PjY& ¶c,}̦?me^dɒӧ\+tr+ByfdbM-[ K4va[믿;7om殺Ю H$AAAp֭_h`H>"r3d|>_$d_?Z5ɷ%頖';9SҴZS-4M<1cƹsjkkSRR===?s5yd*'e>;hРR^O0!((ڵktc ӧO?wZ޷o`6lW^^_M6a0amMFm۶{xx>|v ܏#44pnݺL .|G[_Emu>vB!: XV7'DQ<Øbill7LEI$\~Ϯ>eZBٳg<_+׮]or+6lؚ5klrR={6{Æ k!$Naa ]tٻwﯿ@ƦmEDDܺuKL6P~xN?h"nGYY… P g&lNHHj^|~ڙD~W/_nWd4tB!oiڍm cG.MD(s>+ ަZsQ u5**jK,!8:t?o<|;w|W0f̘]|koHHF!o8qQJeMM 'bcc,Y7tHOOAUUUUTT>ߣG3gΜ96tNfΜi37nϯ'2k֬8p3w~W`oB=4޾d{s="p%ɋ!3ûLD[J@ХK2]]`6I6Z,rpUUU</00kjСC˩}}Zd;wv LB}U:~yUE&2?& kLx_a !hwv!_a<@ǯ` ;3,X0_:r \ɋB;u'ƜrJ+8ӯY,fQB1C]p! /BA^Bv >Bv>Bv>Bv>Bv>Bv>Bv>Bv>Bv>Bv>Bv>Bv>BvA@!hO(9bV3(Od48*_am(jO0C!~y˕bZ< $ΰZ7@MO ln0j_p!UEbeY(PiNc0;]QQQWWݧO>} 6,$$Įrzzd6mZRv!z`|>az[_u<ø?ׯ_Or+V;uTJJJ}}}ffO>;@֣G%K<(E!`,2My}@pppbbJÇ[|ꫯuXUT7n0LkXJJ2-_BuFyyJ" q jjj7g#vaMݾ}VkYYZ[}! nx0kLh k ivЯ_ϟ>}ZtرvV\?GDDlڴPRRҙ3gn޼9773<{Fw5kx^߿VV^'%>>>K,થeeerrrHFmS222n޼Iz{{Ϙ1c̘1vy睳gܹӶ>>"KplwO>ؿBʲ=jݻX xzzp-[\|`0D"2eʔ)S|!z` .}wx7ED -+--]6W߮<Ɏbaɾ1rK8o^xJyرꔔ/?/n0aUV]xJrƍAUV\eaNRZ-;˗/Ҟ={rg&<|ѣv/dee) .l߾_oDј0L``ҥKhΞ=yfٜڵ|C ֭իWO2;w}JN4߿?|~~~O>dxxxeeennӧm?sHx'6ŋ;vhmm/~bm! \e.lY@ #[VuWnVBj%nnҾ?8 ӧo޼OGGG/YDeddݻ.vx{6\v7|pVk"0F1??wm$"""vNB<<<}aÆC<裛7o>uTUUUQQQXX/6nC}||d2Ybb"q/^DstbYvgnEb^+WH@cY\.F=gLkٖjUg:-~]n?֭[nJ3!dŊIII -3gB4]D#,XhopBm'L!|˹s!-"\tBCCKSVVF?^f>ʕ+?zGhwn!bY6AE09@ !ZVjCCLijťO軺|4P;! k ľ8x⬬jTFceeeqq3g˺:rk<ն0]>!t>1JWUUu Bȵk]\\舦jp=bǾ &}\\9utϚ y`p,?uI2#Yؾey*JN;~\"vss<<<|pNIV T* CEEEOzzz_>--MUVV *j֬YBN׈pa&O|sM6h1[W\! 6[]' B a]ɶ/.ޠrMQZ-m=od(Rj,uiƱ33cbb:tұcׯ_?x`ll;vl޼ʕ+,J$?22rȑ>~{P(~rK+4gO<cر=튦Rg", ,D~Z6U"l{sa[CCC鬂"wW[yy9-FGG;ubFӵQJC1 ZTTtر(ooÇB<==ƏϏ۷oϞ=:tg =y䒒"Dࣁuuu555}ҤIo``3gzr0>D@(lՇښ柼bgzT* /gaZO۾}{}VosNr-RI SV{_ӤW bڴiIIItRj---%6>~鰰0-}kңعtR[Y|7FqȑtՕ>OGTx !ۮӐۏ9rȑ~B% %L&ixkr\"{Zp!MYYYVkzz:0{잦[s%%%fom^ رcҥ+WK}Շ܅˗/wtŖ|v{kqG_}~Oz@ x^Ps IDATZWiii۶m۶m}@b OD"\~ߢ5joXJj:b33Z``ܹs-[l߾իF_W|||FF!az~[WWWPP}t[nڴ)77:;;{ڵ4h!$ 0!!Nݵk!DVU&NH즦Wfff^7:G3ezz; 8jvP(y.5oWR^|EB^W=zڵ?88B>`Je95[H0 q'o˾X+\]] H$r\e9dzٲe&);;d2efffffuMLLmTdddLLLNNNcc vww& ,8}mIhILLѣGKJJ.\'V+i0K/DӞ\.7o^ff`HNN7 3f:<__ 6_eٌ 9s>>u!~.""f.tV/ŋw!?pzz:aO Ov5uBȍ7D"|˲VɥΜ4 .\H׿{`B3vyvbKlx'P]]]WWT*ոqTZgYbuIz\>bzV>|~8Nzwww//pZׯ_p< ^p׸[b_A{iEMsGB:WLoVvtӽҺ n"t[u刅%&չ ܀0 0^%Gc,V%;q(Om D((5"/^( i}Cb0$  }Cb0$  }Cb0$  }hh5*` ސ#DɰcHΘNou`(Aw^rPyAʈP@T}ۜWLُLq= :<8 0  :T,m\XH0wP@B"s!MCFhN`&q?pƀߢ㞟^jIOqK t` `my`Hg>!D@#&5𶾞drqqVЭK. '| ՚q\xxBVq˲,BH$8 T%NĉNkiiQAAAAAAӦMuOOO'׿n2RRR!} (8 ,>$y_cHBԓEEE ogΜ9ṡ~z'x/KO#}4Y,9hP}NKHHX,3gs= eeefwVXѷAۆ>Dz(17G?/lgپ-4ښD3ߌ3֯_oT_nn-[ڲO9pWp7.p;qDZRdJ;iu+;t??u䈎~wGvj655[YUUhhtbh4*qZ^B,Kyyyee3 !zL:um~Ǿ~>wQƷRٷQSNBR͛aQQQ~V>|8mZO_N[rb_ϟ߿QQQ,͟?YPPltddŋCBBl:tرc#44t͚5nnn|cǎkiii^^^v{̙3|rww;wnllWuuէmsssw}5Z~{^neɬ3owζo)*_%6jԨkDgvkkk;|NKHHm/,,\v-kLQzi…|?{L l—YݴimkFeq͚5|'Zvڵ =={|uӶϟ߸qmg^R]]@Sm8YgD:Lb;W|npDJRT-慄/""b޼yw_EEEffŋ꫑#Gўz>11eYa-[ʲٳgZFTBFݻY}WƏjmf6Ko>L2gΜ{ʕ+N:}tMMÇ_~e۳5$$d̙c+++All EIIɞ={Ξ=>}mmm-,Ν;eLVPPpȑ, ,+wa;|Z-pnV.qs)BlVt|!dĉ[lzyyEDDZt>,m!gώ{;vL&KMM8h鄐 .'/_N{̙cXΜ9Ciood7nDEEꫴ=ubNǗMmEGGoܸkmw^ŋ{9rJ!@T}0X]0;?)/G--".Ur/.8vuwW~ǾѣG;Օ+W0) Rw݆ Xh4AAAत;yXQTT?mv)X[MMM:` ah48p //oGvP5ȭQRnJr̘1N^ĉbccgΜr8.ok44Chh?b .e.QBf3KwL#OEL!5״\:Gyxx>[oݺu=?kڽ{~åt:]eeejj*!DR͚5gqww{rQ^^L=YdV5'N8qP( ]pIl{:y]rr۫W k}=.b ԓm#Uܯ&BrNnni|Bޏ.ůRvD@-[WjJR* >g+&&fԩYYY_}F8Zlٳi;v+H$OOO777\P[[tKPW\9tPAA``Y(88x˖-t:sˎ:^/`p"YwH?B- * 6νú=#3 ӿG uՒoeeehJu}}w"(55ׁ}|3o޼EvV30EEEǎHII3!!a|u{߾}{Wa'00pӦMUUUfeeW^yտ_(,[UUu?} 3-0P(tuu5٪5&nZ/JB0 C'Om߾>V7HNN޹s'1n8Bb)--JtRBmIIlP(MDZVz8~O?fDcqqq'oѢE陸/_3O2 CStR P(H$2EævUgC}… ijʢoeZ鬂ٳgvoᠾE-ZAןX,[7]ߓcǎK\ݶח~ Ilݡ/_-}RSStҥKfgg۶+ zt>9]]i۱cBJJJ>c˗/۵BX'D"\.ֵ5ntÒpX/KYGΝ'BlRZZ:k,Vhy䑾^WW}6..ٳz~ʔ)ruXb 0%òPӚH$b㸓߷e_ L$9.Q2Ot]ld6Lv|}}Y^xԩSvUBzj1YfD˲܅0 8"tjNj͊ZmMڧG~:Ч?CB׻N3f̴iD"5ͧ~`ۭbx77={۷oڵOyf:ldNSOyzzv{b.\O-_ s=s̱X,gΜ!d2{hرce2Yjj*qEEEݖ7nDEEꫴ,=ubNW !{k,^瞣aaa+W;tbYvgnEb^+WH@cY\.FVW\֬YC3oʕÆ [p!EBme2kf[VիV:gof/BnݺuVLF NJJJJJ3c/CBVX`(X,9s&!Dyj``]x„ w bY6AE, YO,,rB_1!WWW^߁ѕGz[n!D,7.//~Uhh]@  m-X|/րl񑱼|ԩݞXHH0Fyyygώ=zt:]-YF߶8BBB@op}0Xݟۺ$FI,ily2ُ<uϚkZnn}<<<|ɭh6lXjnjWWWjہ١e\]]w--MUVVBT*լYyہIVwoOy( IDAT8.'''##Aիݾ{od}0ROTqF{ȵ^o;IJ˻ {?ʻ?߶zhB#[uASfee}Wׯ|~H$nnnr]X, u#Ԓ1`ܛK:?3R90|PZ-))-)///++#DGGTnK.ٵpWZZJMݮO?IP(ӦMKJJhV+=\aa!!DR-]Զ[RR8"O?tXXŶ=K!҃mP(H$2Eæv'UgC}… ijʢoeZԄٳgә|KNN#pm۶577b߁!>7ζ߶mA=0رcҥ+WK[ChȣÐ/:nvttض_|n050YRRuk;n!Bh9ABJtA)Ex5jC[XתM fH$r|0ыzn0T^^ލ7<==e2Ycccaa;_Bٰa}2j]pA HRnJVC.]*(( ,G]uB{BBSm׮]cikjj!?pOע볲JKKǍn4wtt˗ ˗/L:Z0LqqqJJٳg~&LA?S[ sWQQT*juSSW_}o!矧Y< /4L.\hooH$/x`uB4F=+l 0%òP '8mkB!5Q2 l2ɔm2233333:&&&ھomɒ%͟}ٕ+WJJJ{EJ;w|k׮]vB011 Ŷ}ҤI/ 9zhIIɅ bjS+y饗h5kɓ'u:]NNNNNyXXXmmVup\>o޼Ll{!3f̰.ݰaY5gΜ'Ove E^`cf ba\P( L&sf.ˑ1j>Hf͚_l ݻ.8f͚EjE(>Cl" -Zb?UGhB^zE>>>ӧO+bx֬Y7nt|$---..ˋbX8呑vzGiQFܹ3""Gϛ77ߤ-O#ڶCPIIISL!]O>>_ ڏtAKKKgaYbؽRg{Dže. =Ҳ]qȳ[bۜ?꺺:NRƍkxV x{{;SF7 jzذaVlmmb}%3r#zcXhxȑn=Zvٷ[iZ^兇d=+wKk4Rq/mI~1YHW-^f{ 0ouA!ȫD,, KM$ȫsA`@`J f/EKXP#Kw Q2Љ0PPk-E^P>!`H@>!`H@>!`H@>!`H@>!`H@>!q<~=},@ -|_n&D"e?; q,ˊD-0>Lf2p pg2d2|_nbD" f#wefH$H$g~I+ɫRVh?E5Fj!pp^!j=mb4bD"IWVl6[,J%@՜W(f2O@0@@+5r|k=n xK0$  }Cbu,IENDB`././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/docs/source/traitsui_user_manual/images/EnumEditor_demo.png0000644000175100001730000020730100000000000027711 0ustar00runnerdocker00000000000000PNG  IHDR{Z pHYs   IDATxwXSMBPA*uW։Zjٺj[ZֽZV:TD{\I" _O9s?KII!EY7@D+AD+AD+AUY7&( qT* e 򲲲rss5 KWV0VsϼD֋[6Sv#DO2(*Q322222lmm]]]]\\juY796~֊G𔣡%8NY)dˑlmU#JnSǶ^ #M^^^FFFժUEIHIIdDmayAtSDQzXp(Q$;UVu(_"@&++KV#WY @@ R >oewgt& Z ...eݐ E`AJ&DF]Htq,rN/vA(W / ̵(srr\]]˺! zDQ丂iTkqlnGL#Q96KKJIi*8^i34]ׅEh:J q{;vlڵk˖-֭aqT8vٱc?~zDFFDԱcǗ_~ԩS==z/(ퟛrU@@6lXfsқmךrշGc Jך+DQXZ?K*.쮏XѪڳgOhh(CԦM//؃^z5::k׮Ǐ ^z͙3'>>w)놿@oND Bk׮]rEL6Mwww(}xKJJb7fϞݨQ6$^OSWD@z.W-=$NDt'nJ[(yaق5r3u(j=ܹsyW(K,ٶmg}2s"zvVTcǎ]`AYMz=~ѣD4rH"ڱcGbŊ[nѬY*UdrSN=x^u)W1ݻw7n;wv)Z& V_3Z~ /PUnQ]fJZs8A8W]qgrcVMzz7UV,HIj X"MjщsssݻgA._|M6r<߽{jEzlo>FCD ޽;ٳ5]?Q>}͛7ٍΝ;+mܹsE+B iyD&-/jyŬvϨžKt_yEъp]zogG&D&--ݨ_vvvaaa֭:u*ےN̔)S<<<ѣ#Fԭ[s[>y$Ǐ9ǧs͚5 /Oӿ??֭[׭[w̙,[jF5o޼G-[Wތ3qԩ lԨ>}(555""a<ϻHY?~nHj׮nܽ{WژLDnnn@Ǐ_%I+ rZAEN9^xN}{X} OK ju=,t ċV YXIRҁVZh4{8q/[AXW3)??QFO7o8p͛Ǎ mOLL\p!yytΟ?dɒi˗/Yk#Fhژ<==?rHWcaiѢWBBŽ; 6޿4diSKDSƖ,Yr݃(\͛GFFtaJJ vӦM,[Q߾}۵k7|۷o>|8++ѱ?=YION*@E֟KjXɋ8@&D[`}9.@SnnnS ȷK5 4..U49㇈ 0D.K!~%)K999j)v"]jMW:|||\& ا)7.Wb+ Y@iBUEӪUZjݼyСC:u/6m}͓'OΙ3'##BBB󚟩UVeggk֬aiiΜ92'N\tiTTԐ!C{֭[߹sꫯxر#ի:u"˗;w)rvv63ݻO>/^:tPa ~ǏN6˷ 8ƍ֭KII;v,.ѣ@aQ"q3 fH " Av^_k})8"¿D}_qn/3@B6l0rȫWƆ0bKKZEw믿\oyԩSmvi6&qvv^hTt3㓒XfvϞ=fݻ7 g ˗l?k֬Yb?.$dV:f̘ /]f̓\eccӿ3f|"J ]QۉSpprժ\*C&D nݺ#&"Ǐ"mT(B~dO![ÇG!/ae-v^^^ׯ֜vqqԩӼyo߾Va"ڵkSojժO/"P(Ə߼yڵk ԬYAf>|K.eff5k³OŮ{MERNS)蝎(n}򇘘q$ A(E+ U|||tttjjj7n\E*ZD`ggV-?Vw^ZZ䔙i:h4ׯ='zxJT(R(n˺6@dF-^Xw@תpgJ*&A { 3YzY%BVL888̟?[a} >99ʊ(`L, r#pels噣cvvv\\\Y7?hxZӫ7JkP4*U{(~~~eݜZ=eϨ!^QvHo,2D+Q*nnnZ)))UV-U*$(QWd*%'?V"$ $ $]>0 J@jժڦ]r%//yQE\o Tpԫ'զ5P)J =AVFVj\7T`(BakkRlmm*YٕbG8,@@(> j j j j j j j j j j j jFeNQQQϺդIk P#\JJSwRJPVRSSu(^@>`@j,K j j j j j j j j j j jFU (yITI2rI˺5P*8T&w'XQ串n񱳃|9VtL)DQ`N)}8]'m!;bYU;5DĽg)gs ȉO>1zPdVV%"7gJ|b" W.uY+ 䤣HVP,%h4j]Zv͚5n;vlZJ,}B1rg1ĉ_z:vh$55uǎ "$$yI ?éSh:lvvI4 ծ]{ԩ&wYllll7o޺uɓ'Ut#Z@DqK.]t)11ǧy-[ v۷oߌ3[lf-vȑٳgѳ8%[u *Ujժ۷o7tZvԩDԨQɓ'[_|njd:q+;s̙3g~M6u`իWyyy1Z"/TRA_#ccB {bO.RXP /*cǎ=ÇҖ]vmٲeݺuҟ š,̙ޱcǾ}JmTa, IDAT=jԨRhCjjʕ+(88vڥy/\Vϛ7o|ݭֵ~hݺu֊VǏsر>}dffQ^wnggwݻwzYjժ7on[ V͜2(?$"h}-P}jϞ=l6mxyy88ŋGlqm6x`O~X"g7֬YӢE6hR"6VTAJ &t>oyVPE[;w. bɒ%۶mBBBfΜBD=?*jر ,(VԪUDQ^z˺-/֭[ѣG{!I&999>iٳ^JDWvqq1ϡCݻGD#Fr3`6@qi{BBQz7"' g7H9^ -"'m8-/Dz?( h~ "jժ RZ`WWW"*Ҵ*Vh=777::$*++7oϷwqqqB* i4Cр1q贴>ZцثWl ??ҥK/_.cx A8p@I𬅅ѣOD7n,XTT_MDÆ {W c7c|~6hP(UV2)iyDIO d!LcqL@:ׯ_^;;uIjsss====<<~Gi)SxxxѣG#F[nΝZn}I"9rOΝ5k_ʟ(55#<<<&&~~~AAA[[̙3Si/QF͛7ѣG˖-ի7c KKDl?ic˖-=<;;{ԨQ{.by?޷o={) .7ot^z +gff._>{k|4o޼+W)"""BCCύ7׏4M gϞ{3ޞVZe<2M6=ztپDo]̳/^xDԱc_xqÆ e֭_}S5"]qAAADTNHÆ {Z5kֱc"""f͚V &My\w}q\NN6̜94i'р"##=c޽ kmDEE:taaaϟߴiS ?%/sN\\\֭={^m2d]pAZZjkJYh4p@Db-22U&8nСDR DDv*obb_|AD]v}뭷ܡC6Ba۟ݸ{z-ךּy뗛[f?k`O1Bه]}V ^z7>~j'zX0UH*~dɒ;hbĉvb%YwꫯZl٣G~ϏDQ|W\ٶm6m̟?M+͕&ȽK,֭[@@q6l@DӧO/ljKRR҂ C6mիwϞ=7nNK,ae`Ө֭+_R%"b~7wUTߨQ V8.11Q^cǎ.=z4,,N#ZrJ"rppJ]xyygdaԤ"A2`5ktڵf͚ݺu c'NKsѣGm۶m۶lA%]Ql߾},%~3ٻw&nK(,ZըQmdWyyyWYT&MJOOmڴ!{СRF|7,F5-OlR rR>R_R_M,D,ykRh1l04;w֯_?jԨƍS{m釈eӥ6^vڳf͒oiР)S(//u,p@6m;?$"VcltJ*w;<C^kԨ]tI}lNӷl®:u4]f߈7 FE石P(WF _{n]v vOOKp3ˬ?U>&߷Çw"/ԩcIh۶m۷ر˷~VyR+ɓ&L]v5|8vX"j۶-Q]|M޾}M,ܴiɓ'K;;+V}j_ cgշoO>$;;{ǎ,Z߿???8@"A k< RQKgݺurH7776˘e.]'M$dXfիWwtt~zTTTxxSZre6P^B/2qx㸂2K+.DܷGHooKQϊK+<n+Z@tef͚5k,88wޙǎKKK+& ]rɷ+f͚v:88h4G? ,bf6݆gW_ ۽{7.lӦA&Ɋ / ('oΚ7oz墣>|o͚55jl޼ϝ;nl˗5kyq\ӦM6mʆw+.EQy&UV͸(5@dإ=yCdk#jv?7W17 VPO sMgѼ}-X  H(KqԩSRɓ'8a„Kw=xG^~]^F<&&M7~Wغo-7nFlz?߸q̔" {jemLȸT5zjN>Ӟ={ eq< tss33ݛu\eddvU W\=ztիW{3ߗc%&&޾k׮l7xC [UAI*}+rVЯ'ȖE(-z#+(* 򡼈jPLElÆ lP)66644400˫C~);bgJ_!Zխ[Ç7>Zj-YdѢEҩEPq SŠq݁ƍK[Ǎ'_Vj˗]\\^{'NX2ŸFM6%}eKVZ]V6.f͚Iwխ[wŊ/_j~|˵+V5o{=8 X2{fU 7s҄ȑ#q۷oÆ 9>| < (5kjEQ|ju믿DQP(fJOOٴiӆ N>}֭?;wlݺuڵ{wޚ5kgFY"A45df  4hD,v@P(.%%NMM_~ƍeu$Vjɒ%!!!D`ggkVw^ZZGQ+V?Rmjp͞=ҩUPI#]]O$G{ZDYהlg\+(~Ο?.]ZVWU СC`Rflܹ;w.\{0xl O$'^fY-6J$^m煂٨MKzTOy\ٳ믿~uF!M<f5HHHb^+Ye8G~)`!i #JEE8=d[:mYrrr3f 'PTnnnJVZ={,lE//K.rà<a;F'pʴp&/T-$ w 9Vl$*Ǐu"d;#4[+/l)2_iyq'$ @d*>z ?[Q p)))OݩJ*;:@YIMM֡x<A ,^+AD+AD+AD+AD+AD+AD+AD+AD+AD+AD+AD+AD+AD+AUY7"<Wy*2)S$ =-j>|Y7JUfffrrrZZǏsssr...ժUsrr*fB)ucUGDƫr*%)8RpqDDHHHZ\셌DY IDAT@M|edx撒߿`ggVHfgggfffffV^ݽ /D+bvuۜ|8BDy,=XSVV͛7ܹYZ5GGGBP(8#"QA!???+++99911f͚jrtt,惕bB(3qmlU(,RDRgrIJJqZrrrU*JTJyj&///333!!AԩS /,̵(7l֟)$Gv6t%SuU%$$\zޞGIGQ MbbbZW ^dVEs&Nrhlt/]!))ڵk>>>^^^l$*'JRն?CV_vMRy  ++vbt] wDT*˺!eͧYJ* *agYBT*U*'͛gz^<ϳg!;`EDc*:fE"G;vDGGرcTR[ti~~>ըQcȑ&wuVXXɓ'SRRT*Uz6lozxxrʇM6ׯ_fn߾ΣEz8q666뛛{ƍ7|g{ժU"5қRkϟ?w^T(_|S~AFRرc 믿.]teRдiA9;;JJJoݱcǘ1cFe%<3gY8ZUD+Kn-? Ul:Ql7y"Qp8rݦHԩS,WѾ}.^e?~ҤIs]vǏOKK8q[h!.w" V\ճJPbb] FJ_bbSi֭YI&??… D+߳z%igΞ=K˽ڴiSc4),ZL={ɓ5m߾}4iãG^tۣG޲e\\ܘ1c8{W6mjggk.FfCtϗrI "񏔑jB>*k|=9"Dy^Pstexu#ـB!66ڵk%h4jժ/_6އ3f\6m7=:uT֝zj!Ɣ)Sׯ_FC1IR{yyEFFfff7ȿҥ kܸq%o1W\qFQJeo}^~_~ÓHDݺu]Ve~zԩS޽KDkϏ7g ŋKl!CNzsuAYYYD4wܮ]zܹwС¢޽{Ϝ9CDAAAR Pzwi9 gxo {Oݛt`-ckhyg?(-Gqfdd Æ '}UOqqqD4eʔ>###Ga޽\tܙ \pܹsvo±ΣGV&ǔlKժUIkj7nܸu떅#7l U|"=_~h4 (s ;wdj? ݻwr){>u!G!)W1ZbG|2y4e˖Qƍ ks|@ȕ*UW ȓ~}*O$/W5y(GRҕ{!FCDܹ3_Jb]~=C7}J/md ť$-,233]]]ue ҥ˛o9lذN:M0MB/nݺoM6-ңwW}.e…'O6y_rrO۶m !i 'L`|4777"E+]dǏU*ԩS͌V X$du]wxMm4RA3ZE ZkFׯۣG?|&%AX`}=?OkԨa|?!tf5##֭[UV0Usrrj֬)Uֶ v=|ʕrooXٳg/\h"+ 8s^z k׮W(x ۷5j͛7+99],fP(|g}Ƀш#jժUX*E2-h#B@u޿}arUTSL()ӢxУG"jݺGWh}vv?̞ 6d7\b<1Bu}i#Rӧk)uy{{kn񶶶kv9hԁ,v4{{{=YByyy+Wygʔ)M4III9qŋ5ŋw ekk[\u]'A W0e~('[hٲSwNOO6mڃxl7//1bCUD+p7Eѿ>Ij8sH7ʢb6q\݉HTv?ȨT%KMM]j͚5csϾ}e=D< ln~[>޽{[n[hs]y;;;O8ή̹s̪I&1իW8pV lvO?D`,22r۶mDԺu.Au֊+DQLHHi̘1H>qSLy V%Uu9ASE͛K _ٓ4͡Cqe˖eff>;;99Eϟ6m4:??lz?~nJy\Inݺk׮ٳg׮w@SW02H#@D2EAqQk >ζjZ'ZjU@\Rܶ e#@~8s%\$W99I~۷"ۺ83@ׯϝ;700000㪼(Є :999\.gX%OXRR~'9s&1P=.._-ZqݻwZѣG>>>}KKKDt:=>>!D SN & :tʕh5i? C$)/D B!H_DaaaO>1cСCՋrXX؛7o```k׮Vgh _mذA h{6O /_xb"پ}R)Yf6Vh~цCCCx-Zk.L>e`X"7[ը&mԣGm۶>|gff8p!djj:u+JIIXlra#'Nhkkt_֭CnB^z_3==}֭!!! TxիWk\S] @+fJ /oGU 4T&WlX7$W {fFQDǗ.]Ҙ&22rŊ!cccuh>d8p@y1NNN#&MMPg(̞={M4h@ =zJՍ7𭔳3BVy4^nݺAP]~ ms&h4ڬYD"і-[ϟW);x ^}>nܸغŋ 鉉!!!YYY!__Nn ,33‚JDY__DJUjӦM7nݻX,~y`` |;5s:u !ԭ0].AAAׯ_GY[[ݻ* (qDHG͛+V|...t:=??ڵkx'N3h4@boo Zvq΢[,&]fzzz Nx6B")_4ԮVx(cc&f&%%?~>ׯ_?<77wϞ=*Θ1CeŴirrr|~@@ 6vXNĄx< 7FkmJ<嚘_r%))˗ϧR ms^z~~]رCc#Gx WRSS-[FRi4*ݻ-M]~%ChOJܭ$jl0H6}Ϫ@H"CVO6d'7NxRkZ}ZtJ͛U>@@B7t3v]`eeUUURL&kf>UUUD*? Y|9%H ׹sWn;?"ht<q3?9o_d2Ξ=.DP/']~PZzG;̠q6p\DiMrQVVmggGV"6q<KKKquuurr"=bVأLEWD\`pS3MV(,,h ^z}<;V' LiHv $"w+ɜ 4̌J B+++bʐ;_GG');z\>JG"= ң |GP H*C|x p8FFFJ)D"@ ̠qk@h +Z^TW% 8,ECR:Fihh !DәL&bƤ :(VuHi  V 4;$eeeUUU555 !&i```llp,,, ;:RAy>`ڤd⽊bqCCC}}@ ={ѣGGg;(@B+Z.;;VVVl6N+rD"+++xl6 CQZZ`0 Ce{}b1΢H,;99i(mX+Z(==ښbX,\c)7L&f%''KRkk\P*2(VLiiiFF-*ե2BA ޓ`0 w] C7C+H˗/߼yd2 |$))̙3 /;6oM#'zekkfY,Fk$TOOJh4# ˳lvQ!BT*Ub2dȐ!~~~u 7o 266&B+W?mذa&l{{{CCC\c N:޾!77-דBV277o#9"HB666_}d999O>-//h...nnn ,Ԙ6<<<%%%55ή_~#F!+CJů*9_{\_B8sL}٣GRRR ;{l.KzftJ7oݻwpZi‘#GAiƌ3rHK.444̟?o]hEUVVG޽{8p|>?((!4|GG:NO^[^^^\\DxvAAA׮]Ï4V6l⤤{ڵK=,~}@@@^^q$777777222!!߿ߪvT믿 ??~={S 666666ۿk߾}ȈNхZ"88vСcƌQ>Mfݹs4#Gd06l8sĉIQX+/b1B>}ׯ?.׭[͛}wNx<ѣGBÆ !MV<wަL&Je2{x- ={FDFFZ. IDATy&""47n ?xذaqqqO<ڰa DDK.GգGw]zRQ:Txyy5'1Dѣrʞ={%W*bccw򰰰'O"f͚շoߚب:""Z"99֭[![#m~8mh˫ZS!tȑNNNǏ߾}{ZGtЪf8O;<^vmMM͙3g5rg=z~i'N ^㪹s_W|ɔ)SVZ~wN_Uq!曶DL&  J9rǏ5>B,88LrqHɓO?=2ٱtW(Gqsƞ:p@FFx'fff޽{wڵfff~_ zarȑѣGUF?t~|6D"INNNOOס֑YYYʷM(--MJJᣲDZx/_TnU(</>>ٗ744'%%hWAJ.p0p8\.Wi111yX,Pddd3sge˖ƪ︸2ɓ 3f̒%KB /_$R(E)i\.wԩ!>w)ZJ999Z$xB ^`d|>?==!4n8sB FA"v„ ( m~8-6*))qBǧw'R(\NtDvv(~:B 44{?~Q8xm `0Tқmp:R*tq-X`޼yG^nR>>>ϟ0`@c qx4X9MիWuuu>Hf8-ӴEhhD"[t=zH BN&)))??!4b''ƒEGG߽{W J*)RS!!!fzX,&=z4}t*xH@@$ɍ7>3_>55xӧO=ztÆ !$_|9c U48Kn݊1\lٲG*`޼ysҤI߿W,YrELPܹsgEEE*o*Hd2z_Bh۶mƍ}:B(++Ky B觟~B?pǏO:ТS&^҉bĊb- 8m?mv>뉖$"WF -،31::W%%%w#9O8](MDAAA2.((vZ@@:MFRꤤM6 bg֯_ϣҡetxyA)GHpZmqTJRxX,n ccDB|Wg9bggl2Tzh``ЧO=Z!<ϏF%'';wnڵomjjze|cRW^EuԨQC Ak ?cHeeMMM9d2B~~~Ϟ=+--U?ʕ+ڿӧϛ7rmZK3gQ)ShZJ&1L\S(&ekP(!*:nܸ+W￵aÆ}c߽{wʔ)*t$PYYL&s /^xQOO`NO>}ݺu-ͳNхRѴ/_U7n7W={ϥR)󩯯ǓI` }ȝ;:( Q~ycZUܹo(ɽmř <=='NDu=zT 0͛77 -H:Dd2<+xv\N 򹹹%''PӆSB0'':77!$r9BRy-۷Pъ6b0"HE H$fx< SNj9s&>XRR~'9s&1P=...** !h"ç޽{ժUEEE=۷^ZZH$A(**Zd ^ݝffflݺw׮]ڼnR,}}}H3f :^^ #7:v222N )Bеm:O>-˛hBJogήVDƵ2cƌ'9s˸wogΜٵk6BhD6l߾σb0zp8&&&WChrѲg?Wb%V"O @ \bB/#8tnk?>]! x2U"""^zp}}}l6AӧOƇ8qP(ԡU -x{{GDDDEEeffʰϘ1'8pٖfP(R-(( )))yzvgױdh4DJݻw<@M8qb'Z >b ¸8XXP[<"aʔ)ʕ bYf͚5Ϟ= ~]]͛ǎkkkt> 񪫫u,{{T*kk됐///"_UvrwwDc6}l6V.) \^[[=ƒ\\\geeܾ}{ GK'$l0''۲e q(rHTZZ鉏$''{ cbb222aNǖ /X` W^}P("VPͷuVBj*mkW! /]v"d2\*j9ir\*s8ǽr;Z % \]6x|[455}sNܙHHOOcǎj ޾6Y-Z:vؒ%KЉt҇*5+**CpDBi&ث;jЈSN!T*@o;nZyzz._?^fͶmRSSL[nM4)00!d2kLOO6w}̜9п  oTiaa1b" rJhhhff&6,--q+gϞ}wݻwܹs]T3`F2 9|aaajjjpp̙3;LRAA nZx1֬~k׮]fͬYr>33zLƚKPdfܹiR:Lx}-rk^f+hfBD-[ܼy;wYOMZx11*55uٲe.|>ʕ+ĊVs&:JRRR^~iӦlţGpugff6h Rވ@tOΝ˯K$k0kkkooorWGtly6lBN EݻwgfҦ9)) uLNAK:bII`,_7=CCC//s]ɭ@zQ8qbHHq 8266:uSZ;,&MMpMihhÎ#"L^kqu (3---r h YYYFFF5T*cgdb>blllcV++++..ٳ6 ޾uB˯B$5:??‚F讔-++ζӵ$򲲲rKK锗UWW;;;bN])JE]]]QQn#X,eX]xvX.^x1*z-rgۥ'N;1wE#Ct V(,,hT*ٽLN4??W^]c1TeUby r###wfHzRQQ1sLHw1cƐuڎ5{hWfffT*X(ZYYh4\ol<ӧw*(@YGwʔ)$FxdСCɝ,L\nڴ@d2T*J/i>1TZ[[[\\okkKj@w@:<|===g~Μ9$ZZZfwڥJt&xy###CCC}}}Ng2,f$DTePHf9Hi  V @ (++ohh@1Lccccaaahh C/&EEEņL&U,@ ٳu=::ˠAʠӣR4 l6݉FTd2AL/XvJ#y*<h\6mooohhkBt\744߿9sX pqqӧFׯ_OJJB͜9S=?YYYQQQ)))T*}f211Q?L&uVBBBbbbqqG'4^pBFFp8_`A[>SBِ!C&MԢ̀`hhvZ*1MlllTTBhÆ C m6 OnjFA]\\%m%=z]cZYYovvvLUU՞={СCU*񸸸M6 BHllllllttѣG---m۶=~;F IDAT8wիW/\P ž}_/^xo݇tT T*V.cc%KhL_!VXU mx޽{2VטJe2{xV?+**x<ʕ+9Ңlܹi±bX__ʔ)=JLL]lYDD$qqq!wwѣG[ZZ&%%ݺuK(s*رc8244O|Ç߾}rׯxa )BЙ3g>c77;h Њ@ Æ kE F4bY[[ m >|׮]8~od]PPԑ#G*75],:th9s~G|rȑxvv6F/oi„ cǎݰaH$}rh 177'믿^~~~~Z~OJӱc֠t< d2َ;۞ Zrh r+++[w6iӦ ޼y#H|auuŋ Upp /_"|}} !DRW\?{8 `ƍוAy{{#222***gϞd={q>U\ѿ'Y*& srr*** ].*֭['M #Õ~Ǻӧ7}f@YOAhVb1jbſVgcXb-IHHzK2),, ?2dHy<ޝ;wBnnnz yyyGllÇsrrA-\ӳ90tT(+))ٰa@ Rs>|˗/\RPPr"1P#믿>~xyy}t'Ouƌ?믿x<'Tֺpႉʕ+?۷/^(N8G:w>СC̙ݻ?իWw޵Y|yKNB+"ɘL&5&FPLf(W>z!Էo_Cz*BˋhsjL&_D"BٰaqHLL>ŋ! p؄:Q!&&V9JBH=#B+rvVhb .Kz)4?w_544ߟrʁjd׊ 7n#W^ ,::]!Z 4H ѣTH+Bh/'lѻkЅbӦM eee'OԸCӿ8)ZQ0KTi|ZaٵrkB嵵~ŊDOS^y޼yde!$H6oޜZl{_eϟ?߱c\. T^`mm6HR\ڌ;,ꜜh4T*%`*ƙ׿%*t<m۶md'NTIrv) [[[]]]>}JRubbbRUUUWWGVk̺*mQףGٳg#Ν;,i$˷oߎ-\PNERR͛R) ll𬙙}* hbbꖶ@K: knQbB7˃ל9sB.\Py)qDZs["h,K]Vhdr8rQrT*-//p8_?R fϞ؊lHdffhccXJ xbPUUr/'&,1))W>|eooߊ@ΖoO;UyݫÇk$^uQb:;w[qfh,33‚JqeB!2hѢEǎ;wl4 ~:Bں_~wUI@PƏu[SLIKKSXO0̙3aaaSNurr믿"""k`Px☘?'@LkQbB7عs'^])ssŋx 6K?xzӧ~ڊ7/ΝQ\\SNd2Cs,Ж xxeֺzSPHRT\n}f͚ܻwoꓢ[رCc#GQaaa)-׮bDhERٳi&weu%;ֻwo>I{Rȑ#_@ pJʕ+WWoQbBg˃+՟oݺJLpahUVxbhӦM* x}ڿ TNs΍1B}ڢt,93sƍ_}U[:[ bBJ-[,YoN$;vcǔ'P(? ??suF.| ֢RN,;;ΎƉK$l{{Ne'*uuuonnnnnt>[:QyP&+++Ld2YIIIMM .( (,,h 3};^z)#JP;ݜ+333*Z\\, h47URS__[RAyΠ VD<ΎlzSƬ+//rVVVmJP{ 6p8FFFND"H fffp@ʠVZB+@h@Hi  ,@H$![[[CCCR)ɈŽ{UOO&JT{AJ vqЪU[Rr!ĉ>>>* W\Y\\|O>9z(Q>XXX8uh޼yv"%@ ի EOqر&}/qӔ)SB111ӧOWJJJ&t: Z _iiӧOϟm9L&۲eD"quuMOOWO ˗.]Z]]bN:5|p|6l(,,<~/B/++Cݺu-Z ͛7e2YϞ=GvZOHۧ1AJJJEEBh߾}D\4i͛BW\)(( V+ 6f   2eBw >[AAbrJ777i^xƍSy!$JoܸA,--EXu V,%%O6mڄ ޽n߾](:::~7FYXX0Lz쩯z=qV\.!T]]3HL d͍f;O~1B_ Fc ****OI=R˖-2dȼy.\8x௾uY \2͛!biiӦ!={Zz6@Ǟϙ3gСM6l~p rɓr̄CGX,&sٳgK.}:""֭[?FcX}iTD۷oߪ'x͛7oB!sssBnnn qqq)b)tE޽{mllO;ܢ޲!d͍G͜9Q9sDDDTUU=|׷ٳ[ JB'Ovvv&Ⳃ> !tR<d87>hooò˗/oذAx̻ H*<==mmm߿5mB^z]VX,ơҴi|||裏^xڻwɓ'm߾ɓ'KcLرc###9… TP(xSBްC3 c VGnl6^ÇUUU푇M6m۶yyy5?By{{+‰e2ݻ8yݻ!4h b" $&&͏$ZP(xd?o޼9l0 "ɊB,k*+DDD0 DblloiiX$555//U=bn(##˫uVt5df%!ܡB+@h@ٻ*5NO z@J)AiHfPDP{?K%. ;}1N)‘L3f TCP @5D+հ+Y :w\jV!ZhjVaPdff_"Rf͒%KҧN;}]Vŋg[vܹG_nذUzaTTTddG<==kԨPE Ŏ;Ddܸq3gTϹsN0A޽EALLL=_nخ]٢x#Ft捽{ްa*WBaecӦMF>}zڴi9DEEkNU߿===-ɓ?Nҷo!C4jHD~VZ|j"Z}ׯ:t?033aÆ Ν;?|gΜ pۭ^:}tSN[tj*!!a۶m}ݩSN*"111.EԷe^_Jۋƍ e"##mmm׭[mAll{DdݺuoO>_rEilgΜ~zPPP͵ZmŊ۵kVt:֭[EwUZ%>>ʚR"boo;? v|`]r,\t=}7n,"+Wtttݾ}/)[RyҥΝ;;vtK, OHHزeKӦM? W PÇCBBD$""Ō"##g-"jj7Æ ~~~ ˔)ᅬ57h4"RX?0:::--M̢ѣ ۷-:\D6nܨ7W`;uرpIII߿`0(g޼y_mh"INN~iժU+" 4PDwީ]G{̨(-@(O[jzj] ,Xvm``Elmm^2E(eA 'N8qDJJ>|x9𰷷wvv6}4ѣϟe)ѴH vkjyf/`Bq?SD T^=C Yvmrr={rjTڵXj3f}˗5k&"G^`^).YdǎEDuѐ &XY׬]vF1_{D+@6m;;;es ͚5YK6nܘhUZI&emPU@@@MM4iӦÇ/^\v})S&))iذa=Mnݺ=z4**[n7nT`t?'Nlii F:uٓ\c;wnr '|kUVbŊz;v4'GLr֭۸q%J|zf͚}a^D+@ʂ嬲RveffDFn OD|||bbbZnhzkDXb_~Ν;W-Vg͚UR%xٓ'O+VgϞ 6~QxEDDf?~}]WW &VRR:{:u&6x*0s%K~z tRW BP @5D+TCP @5D+TCP @5D+TCP @56E=#>>^כXYYժUɩ='&&-[jժY޻wK϶^ԩWzzzfۡ{=mƍr((Vj4i,mll5jԱciӦO\JJ?H^vaW:t͚5e˖ iѢEW]twy{キvZ[[[F8qą 嗳g϶Uht(##CĜ>}z*::: ܹs߿Ν;EN:׮] Avvվ}{SCubŊhѢ\raaa׮]ۼy{cFrJeӳZjnݚ? ;Ws\2IKK;uT=D$$$dɹh4Ο?yΝ+YdJJ^ӧ[^xO>J?ʃ*}W\ڱc}]ٹsg|||>5 ZήQF;vh޼߿?~WZiӦ'OZ=eS履~*"GMKKScccEdŊիWWmll֭[<ܷo'HR֭[WZ5iʕ%Ku7P謬'"'O|Q7nܱcjԨmARR[-7n,"W^UZk֬YV-[W+V_g*-w?EdΜ9ťK~V0\+E^Tֻ8pM69l۶-+W(gtssSZ|}}/\RݸqCDի~o0 * Pĉ/^lݸq`0*}q"2s̺uPyծ]V^|ÇEʕ+W\ܹ_R%ݳg7n <~>>>$ݻwbYNYxȑ#5;....\0͑ʓ˗ϙ3GD_Py}e۷|]F-vܩLh4^^^HKK۷o h?ҥ~;wj4֭[*Qfgmݺ>={.[,'Ot)111$$N:{MK3J2yכ6mz#Gc Z*kҤInݔmOO}=СCe{1;eYK-`޽hl߾}``3UѣGTT]PPP֏nzgМÇ:BxuB:r9s>|_~1E.R* ݻV ~ևED׿پ}@sZ111m۶ث;%C5vXeW_}׵k״͛޽)?xΝ[lɺШVZ]vM6)2DGGGFFHΫm+hRJ7NDΞ=zj{xbǎoy]|9)+)(ꫵk׊_|Q^=ʄS"C|||.].^("z:tj;wV*FsR1:[[[NjժCgddԩSիʕpBnVcХK{kǎFe%g={رcE$<<\ۧ~2eʴiӔ *THKKS/( ?Xhhh_exYӧO/]T333ssT*SL ROJJz葭mKq xߵ|SJ(Ѽy'r?IAZ gQF>WTCP @5D+TCP @5D+TCP @5D+TCPMQ3 -SG^٘2exyyyzz-[ #ILLLOO/[lժU{ݼy3Clll4hPD+@iiioFn*>lfĉSLh4yFJJ?H^vءXbԩ9"Z*h4666FԢ kkkJӏwׯ_jjF]vO:iӦEGGڵ؜EGG;w..[[[;;l)\+@ZVgxzz[˖-COzٝ;w=zʕ+z={yYVPV@kРȑ#{C`/z>:NIT:ujڴi~ o/ggUVݻw/((ԩS:ٳYK.Slڴ,+(D+L2%&&N:ϺI&AAAFQ899몜WN>]D:wJs<@CBB,XpI]a׮]"ҲeKGGGuϻvZkܲBB@@@if͚5O<јخ];8;wV3g]z{{9@A@JV^mooѣ>dɒUT)UTz:$"6l'_.ܲD rSJl}Gaaa77n(*W>>΅ZZZ|||bbGK~ׇ7"zATCP @5D+TCP @5D+TCP @5D+TcS! sԩhQן={6&&&..L2^^^e˖-H˖-[jլ{>|#OOOoo5jtsD+@iiioFn*>lfĉSLh4yFJJ?H^vaQz#Ft:޽{oذ)dEThlllFE+敦wݯ_TFSv''':uӦM޵kű98wܳ &O.]Թscǎ.]zɒ% [liڴibbFD^ Fw۷o:TD _|}<=7=zʗ/1rȦM֭[7 ȑ#]vÇRU KyժU :uN?{lҥK簰0ywj׮mnoo?jԨݻwgffFEEnݺ8PLSNgۤI Ѩjmll^.--"RB{M7o,YAH/^<$$d'Oe0v%"-[tttTt"b0,*h4o*W (VVV"ҦM5kdɒUT)UTz:$"6lgO#"{)W\ݺu7n\D/B׬YWt"ZB)o>裰ƍ[[[ȍ7{W+W7o^``.Vg͚UR%xٓ'O+VgϞ 64/1/ggB?--->>>11ãNd^NBCC}||w,oE'B ]tyaW ZP @5D+TCP @5D+TCP @5D+TCP @56E=#>>^?ko *T"xVj4i鞵?_x#Z곶hc7Ȱ/Qt6mڔᡴ>}z͚574hPюZjСE= @ ZӧO7otryzzzzzׯ_GR;vlFF_|aV|IbbMի]{Iܾ}PTot>?~q&LINN믳fj׮֭[_0O>}/3PcAPP׮]xO?4pʕ+WT\r={\|ys_ (rܵʏ4(33SD\\\y*U\~=&&&<<<33>;uܹs {$IIIK.ooo77>]\rݺuek׮={4l?T}l/ҷ~;l0ӏ> >s̷~JˀVy#G*W^111ӦM2dȴiv޽aÆ%KȚ5k=Zԃ}4h`…,#F_HHHrrÇDdժU3gG%%]<[dɝ;wD^zK.ޥK^{s"v֭[\Zܹs666UVutt_'Ν;III%Ktuuuppx!w޽{믿nggg~֭׭[ע]-\o۳g\ҥM7fϞm0܂K.-"~~~-[{gϞ7(ONs_ xI< ''ue׿O۶mE$44"rqxe-GvСZj~~~UTݻwddtqqS~|~)t:ݼy4hаa7mڴFtc:?jժ~mڴZرcҌF͛5j[oU^… ͘1ťN:Ͻ^OOO={Zli*xɟ2@ ŬYZ6..ND&N%yD$%%%111Ow׮]-ZԪUYe+W~-[VDzW~$u:]]W4iҮhF>|?r'ty1c6ol^jwjZ4|t:?׭[wƌN233zwުUf>^}SL/r^vaÆ滴ZY}%\1MOG-d11O>Q޺/^s;9y򤲱`gsQljLڷooYwﮜCUjժ)?*[BӧO۷oRjԨ[:{V//JKh7+VT6 /ZH=LҠA6Lrre˚4iq]ST͡P2h ]vԯ_ٲe"ү_?eaV^Di֬Udd 7lذa.Yy֍GG#F>|7oVJII믕erָqcIJJzQֽ "RZ2e<PranJ?~Ŋ:t(VhmѢŤI6nhq0*oz-zItѣN|6ul}%lԨQm~aΝ{u:ӕ $"!!!wٶmE'On߾}ݟ>}jnzw?߯ Yl3f̃w7.))I^h=,19sڵkMcuww7J%t[bĈ'NLHHHKK|rHHHNVX!"u#7<_>}ܹs={zEaaaLHH0 w6mk-Z0W\Yy*cǎu]pAYEs}nڴQ֯_/_T.zED[g}W;vTE׋cڴi6lo[77kך漋Ȍ3z왚nݺu֙gqGk֬?Zjeggj掎f2[o8pݻ_~^wo&))i˖-'OXgr۶mXzt|rѸsΝ;w֬Y3""BDk׮ݽ{״$=/wz…VjР.gg{ yzznذ|%&M4~xSʆݞ={keLXd6m߿gϞg\l'|bZd|j… njczؼ;v0S6,vegnY֖<]ڵk5kfq]vWF0` P4+ܹsW\{nr5jܯ]xDUTydO}ZٮUz}||-_s? P١CxMkذi^:t5k˖-ҢE ~._ܥK={>PsN0A޽+W4nY:tժU9] G<Tjǎۮ];%WYӧԩ{nڵ͋5k`gg6km۶=zpiӦ0[n)y-1@θknttԭ[711Ѣ_ݹsL)-[ikknݺgSkUB\$yknݺY&%%Y|eSzzիW֭+")))|󍈼{UTLhʕ+cǾ*VNj Pǐ!Cڶmm۶e~sssSZʖ-D+ a߾}"eccwذa?~|4kӧOk-Z888p x !W=Ǐ,Y""m۶͡… } >}m۶j4YK9(V]t)[l֭۶m[L:\z5XPdFСCoݺekkhѢlk>~ɓ'< ̛7ϼǣF!Cl2*wf̘!"NNNUTtRzz[o=zUWw"3nܸQDfΜ̲W^UrGFFÿ́ n߾2wTV666-|hccֳgϜh4$skԨv>--әKӧϾ}J(d~QxEDD8;;ӕLnnnM4qttTSN]p^z5Ӌ㓿cy |||'<vvvު 2 jV!ZhjV!ZhjV!Zhjlz?PXXXnjԨeѣG/_e=99T\\rqqqofʕ#""t @ΈVz# 4w\e\p›o9pΝ;'@) \DDO~ի7vuu-1+%44'r xUVZjE= @0 @5D+TCP @5D+TCP @5D+@5zJ*۷ܹSD 77<6]+@5M4iܸqZmsiܸq_/ PYLLɓzA7o޼A=y$޾}`0Rݼyh4\vͼoD+@e}K.}<䧟~8p`ʕ+UT\={._\כeժUرc2e*W\xݻ_~=k6lhРSʕ]]]wނ_ gD+@e:u>|]688~m߾}7oެ^zapp~:bw;v{ӧOE$55_~yyU3g(?޺ukݺu۳gZ  P֭+";wr V"_HHHrrÇDdժU3gδ߲eKR~嗇޼yO?Ejݫ٣Gȴ#Gxyyũ}hqͶݰaC\4@hOOOe ={^:VC8qbҥMDnԩc[o˗u:ݩSDdᶶ^{[n"؁2vаѣG[k׶(8sLzz4jbWz}TTTM*T"OV\n]PPybccFF)"ZzƍO<8p`֕Lwj֬iڵkw,*޳>$$$Q=z(99\ry@nBTfͥK~'N:'%k׮5l|V}yMϨl_RJY 4M2e' V@BBBv9}t///]5jٳ3>CQF666:}? OB :.<<ܼ^ITsMMM5ߥLwtttwwӹCV^m1c̝;7W Pʕ+vlw9RD"## CBBȑ#ÇeeC_pႈܻwo Xp2PHx :u6lʕ+- t &ڵk׮]Y~)K03f.]Vv5lp̘1@θkFyY=+KRY?~Ŋ:t(VhmѢŤI6nhD³:϶}-z7\UbGEE+VW xz K-""ٹOjϝ;WzuOZZZBBB5x+r/44'@xYus9::6iD>9 jV!ZhjV!ZhjV!ZhjV!ZhjV!ZhjV!ZhjV!ZhjV!ZhjV!ZhjV!ZhjV!ZhjV!ZhjV!ZhjV!ZhjV!ZhjV!ZhjV!ZhjV!ZhjV!ZhjV!Zh|E=!44~Q!ZhjV!ZhjV!ZhjV!ZhjV!ZhjV!Z&7E=@] 6T+|=s;PT+(@@5kjV!ZhjV!ZhjV!ZhjV!ZhjV!ZhjV!ZhjlzFŏ:F7\+!^#GX[J*! ݣG r8sTCP @5D+TCP @5D+BtX2W_禲O>Z*}ZzuݻIfL/ 6`?ydJFFG}d?y\T3 %i₊fjh$jj7%[f^Ջ[4WL+%R `930a=33/>y=uYcA6lUK,yyw 0s̘Gի[hQN@/fggWw_w(" UV%͞=DrJ''29t۷hԨQr cǎ9"߹sxxxTP&D+xVbccX~;v߿w|7հaC__VZ*_~ ' wwQ||{N<)BU}REEED4f̘P"ڴiS%oӋKDC )K.>}+7_^nJԩS2=3HLL(<<:nڴiDT\\M o#&M1-Z4lpСDq>(ƍ+))quu)ۨQ#s OiZ"Ǖ@!Z޽;t\N駟;vO?tCgy>n:(,Yru"Zh8pٳ;v$"gϞ={vCٳg3W+W>6wG>ʕ+9,Yb~lUV|'66]yy͛7F~q֭{ťC?ߤI(SCEDDZYY]v„ 50a—_~KOO_v-?1|rI}^ҥ? ԯ_?~W퉨Fׯ_'{|S6m*caaaDdee5x`"ڻw-,KOO>}:uuȑСF!7TyG޽{+_|~Unhꋋ#;C޿"p`"zŋË===.\X[ŋ`GGǶmQtttXX<'O,|G+A\\\>k׮ݺukƍM6%ÇOmPBP^OLL$"GGǣG@DϟW6طoŋ1;::VCkժ%OUd2)}KNN~mޘ2~2Oo >yf">}QDFFvƍ;wjժF-((t||%'N4hЀ76k׮;vrݻ <D+PYBB_`k׮]vyLfffffr-ѢEN:ko5sU٧' %&&Ν;WnQ8qڵk=zk׮IIIzĉ/#gjtFOqwwpĉ={V` T&"$I򥭭 k-Çw-[V1}Ņ2%Ƿl2^L!<<\.>vyxxԬYS~^zW^?zɓ'ܚ4igT%To߾=_* Tָqc++C 1sTm۶޽{?>V֮]c7oΧ8.^￯_O?ey{{oݺ ϝ;wʛ:LHHxhcǎUk֬Yf&O,7;9JﻬSie a;ƦaÆD$O=ѡCx3g5@+`ӻ ;vX ^͛ʇ̨ۓ'OҬY'.Y1ecfի YF~L3uÇQ~x#v(WgנArРAGv矏97 F֭;`cֶmB.;;{ѢEDdccӦM^( >pss$[lŋo߾o߾_zlԩS޽j.]j۵k7|p"ڷo5AO<)W|rNNڳgOy׳qF)ʀ"l۶"W^r8;;%Y[[o7lеkW^p_)|Wnnn֭ۗ[XXxȑ޽{Cń @%!Z|||6olggW\\m>}cBBB^{5.#OIIxߎYʻGR&L}HLL3fLj#x7n܄ Ǐ=ɩF]vv8pW_}z 7hO1ƃ v-Z^Xby񼼼S,dgg+[jfӉo߾5b >ܨhrQSC eD~2/h^nݺ˖-[r|8Flٲ˗{yy&MN7v͛7[YbYYY+yMV.-FcSEEEʊMD2W>:uCUL(.]/00zOussk۶-&"ɉ~饗Rrrrrrr*]3 [5kTv]|& ?D0!D+ Z @5VAP /TTNNNuw,@DGGO=kThD+ Z @5VAP jThD+ Z @5VAP jThD+ Z @5VAϺjڴZ¿COweee= WWק;;@uwZ¿COwШTFThD+ Z @5VAP jThD+ Z @5VAP jThD+ Z @5VAP jThD+ Z @5Vaݺu$o߾QF.**Qׯ_냂7oiJJJ C,++7q-FN/j49_^=";w;cz׿曘[ny{{o~̘1 6{yyO>ׯ?{,͘1M̘1|}}<ҥKV:|pzzUӦM[l9aoooӃgϞmmڴy7?V}v??R8{+VH$7_|~o`IIIܹsݻGD 60aZg;wu~ˌVO2dȃK^sίcǎFǯ]677O,**BPUVv׮]{[}&MTcLI4f̘{-ZҥKNNNLL̩Shڵ)))7oj٪pa(22Rhuԩ1'O ']v6&&~wwվ}{7olz¦MyA@@-[-yyy|ɺu벲f͚cǎ[6lsF'5u#Gzȑ#_~ĉU4( /^|SLEnݺqqq0n8̙z+**jт ̟??**JwK"ET!h-ZԶm[":s^7:@_599\.$IqqqOܻwݹs1yyyf3gT*K. , ,4s˗/DR?5k$CҪk׮ŋ<&:::--\> SN;vLnOOO;$<`Ջ #%%% ,h޼y=4h4~̙3^hҤIz㢢7nܽ{f͚5jh͚5(O?I&#G9 C&ӦMswwӧO~~жmۗ^zgo=zwNNAAA={[.\pm3'Tݻ1bD>}h%$$ 0@\zt=z[ʍXhrss{O(3>Ζ322ꫯݖ?=Z;MDSN7oiwz-3UFbA>|8zm۶i%%%{իi@G,O6D4s.]lСCf: >FմiSQVV֑#G(?1s2ׯon Nrr'k׮egg>}Zy ʂڵk9̙c~lE|ʃ$'>ߧOiӦ)C<"Hf͚Q`` /.0:te˖^zyyysӦM-[$K3ܿk׮ ZfM׮]}||BBBV\K.?~\ ޿M6o޼N:\vmPs=`+X[[ D, 0`ռcݺu9L1#EGG 򁴗_~ӓV۷NVkcOeN\>QFFܹscǎC 1p۶mym۶W۴i߹s|Ѫ$%%}d0uԱcO:4͌3a(3fP= p5oW]vǏ7 |Ql2W -))1 &Mb/x7n9!z^9'믿Qe={cu~>5Ix Sy'5cƌ\Nx'DD'N;va-%%ҥK%%%拭!4'''>'svv6<16dȐ|N$ು!!!F BW\Q23gܸq'~̓|GGDT~}> Y^^^w y^?s oSNDD9Y+غukaaxԩ3eʔI& p̙.]T`U5hҤ\ٜ{M6=|pcƌlF ,Tu;f/NrrrrttLII~9tȪ<)))z>))̃o߾͗p( 2$"" ť㳁U@YRy[Rddiӈٙ2%OZߤ9~xG%$+sٳg~sծ]QF|QYª̧ rmڴ;OFP U6mڎ;"""^uyx``Cۧ\Ae*99*OOUViF~LDDFFiZAMSfcnlDhxW^۶mO:{0>*K/)~>}٤IyMvҥoݺ>byzz9$O7ux֭[ҥKG|1ָqƍr;wE$tմϲ @a{7nݻwoɒ%r;5}`Sx*ըQ/)FsٳgǗف+WL>xl>n4x`~\Uⳁ5jؿ>|1xJJ\AYHKK>}M Rц l//ADڵkժ̙3MH,”)SsW^'hƌun֭Ro7:̙3bcc/_nt+Lo+,,5jim#F={ʵҿ[[[^?f lrQӺHKK!^z)%?RO>9T;/CBBԮ]{߾}W?~+Wg_z5ёǬ={=(''u:ѝPVqʔ)D̙xc||!C222bcc,Y2iҤW5:tWZ~۷o'&&~嗡ΧN׃ϙ3g֬YǏpg}ohR)))ݻw_n]rr^OIIٸqcݓ!""Bлwo"Zf lْR4iĉyf;@D۷o+*{),,9}||ge{{{K"񊊊| 6ddddddl۶-44Q)|W^^^n~ᇼcǎ%$$1!"cƌꫯRSSw;׿RRRvy):GGŋgۏ;/ZV ݵkVZ5l0AVX3f̖-[L "߿?,,͛?Mi&ړ ‹P9 幭s 6L@ggg4>x` F}#G\r'o#G=Z2|K.Z*##c1^HHҥKQW0jU*2 $I3Ɩ/_>qDy܂N:ɍs̙3gH^BI(o ۪֭U\]]̛7PZLb$''':tȴvCyg/-3wܹvDdmmˠ4i;J\Ÿӷo_J?KQV-[|Fnnn[xѽf޼y . -:k-KIDATnĈW6_P(U}'+))}vvv\pLiiiu1NI233]\\+X(11֭[>>>o`f͚+cF~_(W^u}}}4iy;wթSe˖WO >>**?!2a.,WFFF||ѣG`*pV`8 /wqq7o^rٹ6nxڴiӅ,ZThD+ Z @5VAP jThD+ Z @5VAP jThD+ Z @5VAP jThD+ Z @5VAP jThD+ Z @5VAP j`( HdnO1fڢj5BP'Sb1$1>\($t16TWAD+ "BJ=R. $+ƘbT22F(xeHdlA(@$A c$OTId$W0mF⹵t2qlݘ(4UөZңA^ bE, UQzx=kjѨ֝cw iU4f쇥o)MIJ if_45x5d,RwIĨ(1A$]|Z9㚫TP=#""m=W1/H~u^MΊ=Ͼ7 )$I5ا5.5V/Wki+-V:hXnX.__eDDdD#&Q#@w[d޿4^=/RG9/jˆ{^btBDlج3Ҿ RU~ -.%k.DW TC,fIgs.{5J_zt#Js.LxCd9F[Ҳ!Z[߈_[A4\[>%X J >ێ.}M=&#Vף9qMWӝlf?re(ЛyЋuiftXnψa,(V~Dy4e{5/~.R)(d%d(ePO7Q~Ѡ Ѹʿ[DI"1aE},++KRz}vvH8rXQúLkz.Saquwr -4N3`B(Z=1D JWBq)5)Vh''!ZXQ"Q2ԯ4igd,"W1VJCW|eW&)$h)uC3_YD+ ֤b1̒BD6%Uh h25Z"z4v0bU%XyH[V,FyóVH#*&3V [5dJjPy큄h`iBLjiOgHKت?l!$J)$PA,~uuĽ5ZӂFŖ03,,)#Q vӯ]σh`YZm:ALO#3 _)3֣RLVzK+""EDD~I}Vp;Er}O&;\!Ⱥ;7ho\^H3}Co;l,(z^ C`iZkkkN鬭+IXYYHs2]!iEY3x< r=_wF` CP jThD+ Z @5VAP jT({}IENDB`././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/docs/source/traitsui_user_manual/images/FileEditor_demo.png0000644000175100001730000006433100000000000027670 0ustar00runnerdocker00000000000000PNG  IHDR ӹ pHYs   IDATxyxU񯺛B hV ;đEAaQD:+ʈȠ "("*a- !tw?=I{6ՕxSN9;w}μ5> `\9ob%7o7pZv'7P'qq:͓S'QehuZ1/b43>\ *V(3_X>,h` otb>P$ ;R_X U"tرOPB}WE} E*)bbb%Ξ={s,#,#Jq {",#,#,cpFڞ4oO윀$2? )."X$egVNNNDDDoW_}uÆ WNS|bFJJxne~I |u$t:… ޽;%55555O>y|x'%Tu%e JfTJcƟIiNY2YA)lW? 3ftM@@Rڶm[ju}k֬IIIҥˢEbcc%uJMM曋9駟gԨQ mG~zovI>d@Ju F?h'+s9,R}n%={믒Zn^W"""y晊+J:V~?%%%---lٲ0ׯ_[~˗oڴg "!!!ޛo9x`qqq&L6hР=z 8s;uԤIŋKJMM;uԼy&M1"v`իWӧVZ:tHLL1|;w{: 6짟~:C%''7tܹsy/\RNNN>}nmƌ3gμ[ʔ)#i/ ><;;_~SN/jﭷޚ8qQ7uվ})So^(>xgC{Λ7/++KRRRRxst,Q)|ץ*OI<^yyCxq8y zZ4|+Weee=z5jرcΝ;w]C|[f>ܠNl ,I+V;v?}?>tGSQF7,%%Ŝ^+V0/#Hq )%Cݺu Ot@N9X~na͚5֭馛0`}>ɓ'O<9|{~x<855u}w}VLL̨QJ:et&PH>Ǖu@so$z}avbU%sϯԩSgѣF0=kfI[·+2 ?_C^ƍC[ʖ-{?6t&}[+TЫWocǎ={lذ8]wu@ܹRSSSRRvڕиqe˞MOO7+`=_~vڵuȚ5k0~-[222bccO#4oVJ}paǎcbbLiu ۳gW_}to]:p5/߂A{q]Iu@Е: E`+5*pp(aXJ`cxR"El8-X)UegvRޢ>*nʹ'08 r$gΝE}/7iξWqT.R5*~ ,!eeeeeeeeeeeeeeeeeeeee씜|6mjPBܹOwT ]lwXTȿD`YFPx`XF`XF`XF`XF`XF`XF`XF`XF`XF`XF`XF`XF`XF`XF`XF`XF`XF`XF`XF`XF`XF`XF`XF`X+o)P|߿0X\ ,8-Y;X%BJ8]z饳g.QdK>A$WRG뺒RWRTQ"Dž,B`ޯέ]O_8t%)BAIþO49Et 8]-xJ;vx% 8~E}:,}r%EK^I=JR)$$%$7(I^[݀f,kt>m۶5_>R^^z9Ś5k6o޼-&L2dO7xNp!N:mܸrRJu鸿Hỳ¥%Ue>L:&|H%)}~I!IWRе3esyGU6p@u'M.˿޽{7mj֬Ybţ377~|jՊ:+|efffggW^sKnܸ133jժ*U:`111kds41`s=N/k׮111͚5;:묿/ ./m۶ʕk޼yƍ˖-{%|Gvuo&ɓ'|͛w{eޚ6mZw^|ƍn:..^zcǎs;]x999+UTNf͚U\]v-*o hݺu  g Zn}"?OONQ#GG-[lٲeF)U6le˕-[l.s8r9'rny@ ;&''wuܹ#ׯ뮻@ s 7I򺒔$GRKU8eZO$m۶l:thjjzi66iҤf͚\s֭[_~姟~Z҂ /i̘1fZZ3w^Ҿ}ʕ+uvmf-e֫WMRիaÆmٲE3<͛7{w?^+V;wnڵ%5m/x<&MZvK/4x`{H(B>:NdnYfŎ;̋$I]t ՕiӦ/_nz諯 {?ꫯJ0`@ cҥڶm+#!!aذa233gϞ/;Sjժ`0cǎg}^5j-)q"""<#wy_|q!O2a'uݓqX(,Qu=g1%%ERӦM\f͚|> 7 :0z| ?~Μ9wqGnj֬sҤIժU֭[ k.]gԑ4k֬TRW.0d ٻwƍbN:%a3K8_=*uJy_]7u"qsVߊ0//^S&QFo_}QI } ޻q-?%l2Kmڴ1כڴicȁ@PkQT柮$yv3:4Ych8>qu;w76x3gnٲ޾}9/rرfY4޽{?/M6}׿; %SML(6,Yx]~$볯pK*둤9˷I֢/vd+I VI"NAI5k%MV"8-ZG١y|A%=%-[cǎ^իMPRի7mڴ]t)pVZ͚5C:sytҋ->| 6ol&N˯/BhI*s'|}Zj @߾}׬YsgK8qs=7yd4mtܹ?yB7֪U{뮻>?|'ǍwiT%&&5V1ge$)jO哤Ht]K`yGΑt ~Ž`<6^zK>ʞ{ݰaC yc`0xYg]6mUTɿVd~WV,U+FJ]ΣrAB,H_Z_d.q2O+VXba,_|m}3MTT%wXL:$OZ.ZIr|aP:<>G3@PI+uZ4;a#k׮K.pDX(27t]#>ED Xᮼ+!`Xx',,,˘U"[O8Nu-SceeeeeEg}\ߦKʕTHr]W=-ϒt`;M,B3mέ]O_8t%)BAI~)5H. IDAT@ 0q!Clܸ᧟~zGNZT'E,8AבnzI~9"\W{p$y\W?<I#$z$y<$9Iހ$]XIMm;?M>ÇKڴiӤIᄉkN6-!!+sB`Ef'NtW֮]s3O?t͚5UT,~]<\J}r%EK^I=JR)$$%$7(I^[݀OJjҠT!$==_ty /x7o߿ٳgg5:5rwyI& _(,z,shذSO=UEBz$^If'(d%Hϑ$3ˬû/[^IAd]KOOߺukڵcbbt|vؑVJ>wܹs֭qqqg}.5I@>}zBBBϞ=oքYfIڽ{E&Oܭ[ .ફ%ݻwȑ[ vڵk;orE%$$|g~G^Hvv!C.?+ ǎ{>V2\dI(tŋ-#GLHHhӦB%y̸HXc+GR#IfU㳮RG@-~ӧOOw wuעE7رcΜ9k֬4iRj$~@Ih^?8p͚5揎㸮ᅬ3fʕ3ߞ!~߼%PX@ٳgbbڵko6IO>^(s O>)]t~Zn]\ڵk;UΝu&''Ϝ9s6lx7{y/^\N^{MRҥͻSN]f87|˗/r^x!))髯7o^O/ %Nn0()*lt*‘KhFr` H땤I:4fee>2eԬY3F|\\\͚5 mĉW222:tPbaÆEFFJV_t;v|G*UH,X Yf=Ҷm۱c>C999:ugӐ!C._}͚5`82~;wV\l\Y p,8G:|7_x?t>RfVO:}'tA'(Gr$+++׫+W^W+G<>y|purCJpLF1pе?S?^_2t6˲ zΝw*Zh1v֭[=zmӦSNo˗yǏoڴi$|?9B@ܹ'ђ%K322DN#gKU?Ae0UA3]lx,`jТ I:׵F4+}`WCعscccC DEEEDDo[VVVZLlIvvƍ˕+WjX;֭{dىٓ9X(qVT\XCfrYPe&e.zG rǗVFTTTTTT'>>0G(SLÆ x!e`ĩ\I?Hrs%1$7HX$'L0$)Q,BsYjI}ˉ+YTgF,c %NB0```sJ6eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeEX,SW$ߚ9eʔӧoڴi׮]+WnժUVwިQ"9%[ /^<`=eΝsΝ;wȑ#GٷoSpv7n}֭[|#(!Nu`͛7999bcc5jl޼y}]NN]w?<'Lƌ#M6޽{nSWW\qرcK.zw̙wy޽{_=ztT-t /}vI 63fLx]IѣԩS'w^z͚5}`0m۶+V[HzzzJJp[n]jU9GIz7ʔ).]H={m$X"66666vyvNNN6o-\0|… /䒚5kv/K5ꪤSNر_ocN=\&M6mڵk׶m֮]nZ~}wڵ|vSNm߾}F:u?hРluLҸqf͚uܹVZݻw_,o1F]"LIIIMMtԫWH7n͒:,I@dϳ+O㸮+/\xԩSeC7>SL '77?^|ٳB4/^{~ 2224hOt޽{Ϛ5+>>lܶm矟զMٳg/Nڵk͋ eJ*hѢEy. FvvO?֭oٲe#Fx<999>+HJJz뭷{.l1bf͚9o2eq\s;}|/\zW^y嫯zǫV*>6lXJ^|of-[>;o޼,IIII[n=֟nN֎;̋բĚÇ/ϙ3GRffftttTTTڵo>Ujڵk}#GJ^_R%I ݻw|oի'MtwuŊ?sƍWRozO޸qcs:jJҊ+Bڵktttfffbb)3pF;u#Xv2/=ܓf3an裏>?=ʕ+͋#G 1b8'k׮֫W/]-Z0ueԬYqݺuqqq)))s̙1cƟwlٲΝ;OWi&22RҘ1c:v8a„_~L*P~[n|Is-&&Ƽe |cLLL{_Rnnnɓn-Z0/.]};^sn„ z)\V]xu֍駟}{/ ҵk^zeffo~W`DDyȻؽ{w[>hZZNB`^zĉ,V-nf;x 6|駗^zK/$)22[o=t~Y`>`JJ/gӦMy߸q+V|7駟g̘1tPIg}\ڹzfBՇ~oifkW_}[ov?{4ine͚5;v|衇uBp&:u C=pɓ<8~١nݺ'N ͈Ogoo~5ݻ믿lٲ}f6z2eB2e:w_?^y+R#_?0dɒs;Sf͚Uء}=V\Y?ӷo߾}?>0dȐ={>S!yٳgϞG}taUV͚5+5׋/ ,83g̘Q^;vy/wuK/toUW]UmÆ Ǐ_zuYYĉC555r\uuu9SO/ h?^reYY~g}~5k֜}_=/l Φ̇n7|3p!iӦ{l_~//ceO>}͚5!_|&Oi殻: ݻq>/!$Ir7{+Wַ5cƌ7|n !=N˾"ЩS.>Çwq՟z՜N`54gΜ±[駟>pܹs {=iҤ… okc=^{SNek ,!tgihΝCjƳ:kڴiO> 7BH䦛nҥKhǎ;m=tXx׳>裏>}-[lٲvۭ-[B8s?f9XXٝlfiZev˳ٌu`g&61hР6m֞}" MqW|pȑrv;F3}!`+W=zذasNaG}o~3pA]tE7xc$]vY-++ !,\pŊ ?wܑM>VC]v%\^{|ɳ:… \0a‡~XZZtС_CO>?\{!r5kVaM7x㎛0aI'TVC}ڷo_SSs5^z)/~qرٞO^p6^y啻k~Xx#F$I}ȑ#8lٳ;K,Yti3H`{I+I,wJKK?:xx`С-ߔ)S瞒ªU&LBݻwžݺukB+V(~aq~zSO0`@$_ch~IeeeKCbŊ9gIT֯_B(--m۶f*))i]kٰ֬aCǎZzuǎ㿀B3g<>ڧ@UUU3f8ꨣek׮]vңڵСCl~u4!#"X ,@d 24,Z`g!v 3fh%M9Q.|2 2L`D&"X ,@d 2L`D&"X ,@d 2L`D&"XiNA<0S@DMTVV~N=zX|y7)BȚ4@L`D&"X ,@d 2L`D&"X ,@d 2L`D&"X ,@d 2L`D&"X ,@dmZrw}wGy #{zȐ!|pħvDIeeew3gҥK?~s_}VZuWfʜp &MO4iٲe[yE555{l,δ$IN>ºu̙S^[[;iҤ|'x!C}{#ꫯo8Ç8pw_?nԩÆ 4h:q|W\B5j_كW|%~SO8gϞ#FXf׿qGuT>}?8ٳg޽?:dȐ={tI-s}$ڶmYn]ae]v/_eʕr%\R|~ܸq?OWXQظt'x_b.8.\Xزlٲ &\{'wy[9£ŁUSS֮]{?AOn#Frׯ6mZfϞx\.7bĈlW\q 'zӧOA;h5k֬>ݹ1s꫿կ>C=tuwu]v!>}|ɳfZt̙3 ?裏B'N?~|qȐ!GugxW6^{uy+/m۶+++4Mѿ۲}0`^]]=so|}vڝqsό3tRx(T;czm5j-RZZzꩧ~{ >q`-Zvv?Mӏ>h޼yC%%%w^ylѩ~ko[[[;C=4Pf^m۶G[^|>b:wo}xC˿M7T1e}]?~mmmUUU׮]⋋bС>[o'3fLiƎm|嗳3cƌ)yUW}]t)$Cā|_wҤI/_yΊ+k̝֬;7 uֽ[+WzC|> g%w7d ! 6A`knMLbaoo׮] TTTV^'<#{ŋ?vС`^؁|j߾}7˵o~ƌ:t(l/|kٝ4M{)Sk[Ya\.l!GxdɒB"]v&ct֭76:I3??fG OOXp@aU%\_zڵw߸q ۳%%%Sٝ+[nUTTtڵK.K,y7:}ĉ? &O|Yg#{lر٧+**:ꨈ?hmu^z}k_ !,_o,lgϞgx;_ׇ V|5 vebқ};g Sym?]~ %zgWX<3 U^zinB?ϲS`>Bw 3o 6lذ~:P^^xZx`a7|sL/})~s=+WlI't!4u7駟ޮ][:?nݺg)vhK.^ziꫯ6VTTd͛7f̘S.]t7x_pšc9&pGgo{[`:jԨKO0!-~W_y3gΜ;wOӯ~[D=z,Y2|{K,կ~5|E:wxuQF=߸vmsN.2eJոqK'㩧;voxm|{3f#s;ꫯ꫋?7px rIII/vm zk͎xW_~.l(X׮]/ӧ7^aKVxBh۶Qp34K*++Ϩ}VXѳg lٲ=mݶi˖-޽{޽pÆ ,X;,[G}8p?VZuAZSN/+)))\0ء5G`gSN:tЖ>`3Ɩ.]:o޼gy&>l0u; L60{^{mlҾ}=zcРA'N̖Hv`D- ,@d 2L`D&"X ,@d 2L`D&"X ,@d 2L`D&"X ,@d 2L`D&"X ,@d 2L`D&"k||>4m6I4RZZZRF ,h>XQ3Ͱ`i{:)mىthv`ϚI}oRYY ig/ޡ,RJC=(չ:ِ~e۷(זVEշ޽[}Y$I4ٹ_Mη&!$ m_-eG+oN`@kG5;]&4$$ i!$4InX6vmB][_}{vkҹBso&چveI>MCivr*_7Ґ$/nV?.Ю,i_>f![k#N>탲6!__T!xFpJ+4v$IH4I4iYixvM\V'-Yѩ[lĪxP!$IT,Uq&IHiY-ڔ%+su vHii=IiUjP7)+hqM ~!׺ӳu;nąF|CU  ٭bҧ]͆ W;4}YWQ> 4IWIRe6߽~{/m[jrHmng|>V`@kOLRȩ;!;UTzg0|`a׎IaCMX:~u3iHkWƏ_2W$ kV(h' F|7~\0/Ӎ[6ӌIQ2r&Kk*n=sf>n]M؜sǗ$IX!|մ..um<#[7=dgYC`@kT7+=Ir$' ۷@4}坰j}ڡ<9" 3 ! ֩Om\h"J&mX*{%P$aCm[M)>6~ lIʹr[$5ʇ$Q g/P7|`U>hH}!WMI8O2bpcs +KrEҷGŒ!6]Ah,Paa[R7lXes~gIDAT.!,Q6z6ƴRk}Y8Ⱥ~.oHه'_ϯX[Pv3Mw WZھ,koUCU<>Z>MrMo/\]<ʒdn%:³Jyjnn$em$5~|եiytBSIgѢKaeԝ^?={>Q.$$iKI<'I׍}0aٯ.2x5ͧ[WӓHBG³74Sri2 EsBF0lڇUa庭ˇ |hP~!93Ia٪p"mZ&$IU7s$tjBo-O2|y2w$+ ևP?v5Wԫ w FF7FUC9#XCmr--G̙ aCm(oO>vBRuwlLڗq$s //nJϤh;M*l\>C.Mj/&ٖ;ifBgٳIanK!Of/N@BI:-ھY_fosiKCP&Fn,hi7Y)MRaUgPVzwMެ=;'w !6U[ o_ µg/oӢbwkVSřՒ!{%!K} 7p ;="G8d'?} ڼ`9EQ~KK/a5߽^ dgɄvu&I?!s)ⷺlݿ~?)5C.6i-^Kks!ۘ|ڬSBy1. P~:6t(!gѺENm2\D5']n/{ϳ9,رBBeEI.M U=I3^_uS+>4%)I3\Bև?mO1: ?-mW|{lCnM[>Mrk%٥Zu"8N]Xf0gs^CnY.exw&[Xɫ69 #\1"L|/S4ɽ~$$$bF5Ӻ@Kά-rpY8}JuCYŪօ1^`3j}mu_5Bapf]6iM·aS}IeeeۧE PzW&#v&Q>mS FÍkք.c)BhD4ߊkmu$YCef>vQ> ~?IJ[E]}bҪo,QaRE$l-~J5xC÷Q+ISY͆|P4sb/ֿ`ap:&I:X;gYqUز3!saה6:%%%G)^Ont9mNq&6컶?BN#ncI(ifevĩH`R%44jk4 Z}(uDař+79-$! poG&>֨t!eط$eƥnm(L !χ4|;mш5qV;Z|~Æ oɹmg-vжj'ҥzjA{////ixv^|&˥jhIm۶m۶M!f%%%mڴ)//O4[/pO$ˬ,XmiA` X ,@d 2L`D&"X ,@dd>(TIENDB`././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/docs/source/traitsui_user_manual/images/FontEditor_demo.png0000644000175100001730000010422700000000000027716 0ustar00runnerdocker00000000000000PNG  IHDR܏ pHYs   IDATxy\NsԄ(4HHP?k2%a1cیe0 5Ec2*Beh,!-(EEsz<-*OϹ{os.Ed|cW]hhhD&u0LD++lpEn*Hٕ+WjNKUܚg_=; C28; k***VJ(ٳѣG .޽{Ws̩tGh\M4IMM=zݻw޽kjjjff6o[PPкu!?i^|yƍ'''y&z>Z0ի#FpqqpBFF!$55̙3ׯMMMs MBNNNHHHJJJ}s}U}慨Xĉ0//B>*…>˗C._mѢEz"><33fݴiӗ_~o5f~knذa]vӋmذW^aǏ_zn[~=0fͪ(((ضm[PPP7۷o֭ bjjhѢRO{q'Xqպux"yfa LZZZҪ[};Ȟ&fYV :99?ʕ+rrrkB… O\\Mvvٳ{ׯ>w[n5wcddT}qM9y]ZjU (((ptt8p`nn#G<==?~,ه8N$U_#]nӦV}*2i;>$_>>4ܩ PxvuA<Çmڴyo!)))ʝ;wVPPe={sMMMF陕tR2f̘ѣG]600>8rȑ#GG&''' 'OOAA֭[/_NS---n [nhZkZѕee?#M)))С}lV7n\NNٳ{icccdddaaFIMM3gM>}~WeggkkkkkkM4k׮CٳO?TB_5000555j'|nݺ燇,[SUG!!!JJJ_}2j('OT̟hhhxooocccuuRSSѣGZ(//~tvv633{}:kddDvvvUs+))Z[zz̙3۷oߥK߻w=N8QCCdٲe/^WBLLLee8p@NNbmmmaa!ޡVrɒ%|ի{=1cƨСCB~ڵСCk{릚 E-S wtttJ{oر#FE"P(dYSVVƲlQQܹsizrr)S_>~>##e| o߶v]\PPsΕ+WX{l>%//?XxxgϞM0a58C˗JJJ[nMIJJ8>>~ǎNNN 8ƍ/vqqں :4""yŊtmZZZ\\JIIsθq,,,rss;6c BAv!zzzjjj5?SS;O>ՁDGG=̙3 {3fLVVV{ܳgٳ 777{{ׯ_ӵ&L _Ν;&}ϻvJ׏|27>>̚5+==ѱ}[n:thNNN5'*ѓoggnbbO8q֭ZZZ׮]x E-V w FyСC7o\ՈvڼyΟ?щ'BJKK'MgΟ?_EEk׮glR\\twƍ]t!xyyUPN$C >r !BBHdddzzz欬\§<}֭[ǏWUttkll ۴ijժd޽{VJHH LKKwqqtRD"SSgddhiijĉ'￟8qBiΞ=+zS>}H$:rH²lAAAJJJPPÇ,Y?7߹sgaaa4äI"""keee>kSTUU'OL?:tHYYΝ;ŋ嗏?UV|={pw .;w.99>//ƍ5?U)((.&L=]$b5peɦmȐ!}]t3g jUeff6jԨ?9?,-- eWxII+`77#F}GQUU%]qϟ?_!>~ѣ=Jpwss9r$¢SN::6m!dʔ)ǎKNN 6l *elll2ܭ[իW,{Mru--~TWWwuu%VU+$[=z4!ѣGxh^^^ 0aWruum۶-]޶mFxxx݅ UVoߞAaZttEڵk?XZZ=VVVݻw1=zt-{{{m۶%%%OVXI{D}wtbbbBqBȤI4~Z&^vmƌtiiis544\zuNNN 0 r'888O?EmذA}!8<ŋ;wNNN qㆁAll۷kx QPP022JLL,**#= 6}clj֡C+VhhhDDDX[[nR 8::x6BͿ[ٳgL>՟Tҭ[7*1>>$FSg:::;emmMjsH"i̢ڴi/]|ڵkbaa޽{C(]] i۶-}Gf닧ݛ.T7778ܜ.Ɗ#˫WBBBW-(whI܆'O|޽{O:~ |'Um"KJJ!_8N" UiJI}~Ǐ T///ӈ\D=+Dotҗ={M)CCÊ,,,,,,֬Y3lذ(gggBƍ}}} 뫭->bر=ھ}ٳgO8ڵk׭J nQPPt~H'GCۊ5`0(HQS w(55SN:sgϞ-]ɓo.}*_F3|K'oQ>}zU%ְҕobb_eTTT ޽+6M?;u7VQi"J>|X xyy_|ѣGm7$Ts wmݺ5?%E('''^YY˵h(Iϙ3'**΢IٳSV\y%KyBlٲe˖G}7~~~3gά$T_:033qFVVmᅇ |-MO>??C5\z_J?b.VMbcǎ?۱cǓ'OyixxӧWmF133))))Y駟X3XЮQwQ>y$?޽{QQQ666+33… 5<jڵ|[GffD6yxxsO4iӦMQ TYYƆOtrrXlqϘ1ccnT|'*}W_wSLMM544V_05H5֝ba455+500={'!$11nwZWU>}yygVܡ,Yd߾}|-///==TTT촵.\ȏqqq 믃._eoo/1n |w7oެE_#.tL3f߿cƌɁPFJJJ Ш%[hΝ; dgg'Ndff.XfXdرcOnff۷o2/gڴi'O400oPWWOKK;~xbbgLRՉzojkٲeAAA~~~fff#G裏.\}Iq"д͛7ٳRi0VGoBP&U޺uʪ+Y+={~}}}hÇW?y󋊊ϲւ v=zXfMpppQQμy<<LII ..r{1~6ƦCO[;?LJ#,--7l@;q]AAԩSG{챵kڶm;G=z|ԩHSS̙3@-\0 O 9sjV鉪JO>0v%~XtܹsCBB)yyy333OOO B*[McaLE )j%**~51⯿hIII1bDNNrbbJQQqi&Y}ݡC:vH=zA#Pw^:| ? xQ ?~,//cǎ7B222h`Sw=qℛ!d|ڸ &0PkkkGiiiIIIڽz3ݻw|[w֭;Yii/jR*---))-K Zͣ&駟:uPUUٳgJJJꥥihhTUϟjiiW++GuܹZeSRR:tPi_ՉJ%bccq{ff̙M0֡o$- KM#>>aBӧOGEEIմHHMMի{gK"ܥKZ §OzJ[[htXifݑٓfcK^^K.ou?Mat!פf555B;)@3p[liZta8; 0m*4;-•+W$R8kMU/ѶMyVe; C28; C28; C28; C28; C28; I366VQ zo&MMBvvdX x2F;Zw@!pdqw@!pdqw@!pdqw@!pdqw@!pdqw@!pdqw@7vޅVL8kPg TLɶwZkkƮ]r9]ͭyfpdqw@!pdqw@!p IIIv=xgVgcc={޽{aaa=ݻ֭[Fa;wv_y{{BN|Dku n3gl߾}.]ϟ޽8qD e˖x_C111Sqĭ[kii]v㕖/R XwZ+++W)O>u!Um{…6mڬZ*99uvvwުU]\\.]Tii"ZZZ!!!Z:qĉ !'ěFٳ^7ӧD#G@!,=|pɒ%)))(q|󍾾~||;w/^N3L4IKK+""o߾VVV[lh(|}}???UUɓ'ӏ3 IDATRVVsΟy_~?}vU'U'&:::44tϞ=ǝ?… ΝKNN˻qFO{U nogSO XwZiӦBLrرaÆѿ xٲet[nWfY͛4ZZZ?3J +VBH֭GMy0'LU\]]۶mKmۦ^qwBpժU۷)tPXDDhѢvڅ|cUYg֭[|sE۶mKJJN>M3X"22rذaUV肃!d4eҤID? BHBB€wckxE-eڴi[n}򥃃㋋/^LٳgU[_c߾} !իdccc255%FJ}!|/^w9999$$ƍo߮TGAA(11Hb6lÇs'^Z<<<222"""VXammMI1 Gqtt7oUTTfϞ3}WjR%BHn肪pHMx5#Z2jq/_>y˗/'&&{ԩ?!O>j%%KJJ!_8N"2Rv):~P(LMMh1x%>JԳbHTFK'}ٳ'ߔB1448]NNb͚5Æ qvv&8::nܸwذa#ǎѣ۷={ĉw]FOoE5a*2''PM~m_0RH%CӲ$$$盘|4eY???]]ݪ{Ǹ8nmmN:^ll,!&Çx(_=zt۶m|CB5»{8]]֭[ӑA<E999ʖ/_ECIJ]]}Μ9QQQ/_)={p@Po`O-ˎ;a'O8::2 SVO˯^ڶm!̌߿~dY~u*!'Ogw^TTç̼pB Zv-ߖᑙ)ϼ<<<}'Mi&BBxx?(Nlcc':99edd,[8g̘zj[nthcuU}>~ꫯG{zzu+/\$кӲ,Yd߾}|-///==TTT촵.\ȏqqq 믃._eoo/1n nڴᄏy… w.ֹvvv%7cƌ>|x̘159*66|ԨQIII[zoѢE;w4h˗/O8`aɒ%NNNcǎ>}Y~~ӧo߾mhh8diӦ9;; pႺV&ދ@gϞׯ_>}/m``p'/?~QQYZ`ݮGk֬ .**љ7o{!? ޒ$2p@GGGÇW)))]n;t|kGBx h'nZC񚻻+((:ucϞ=tC۶mϟѣGҚO:u߾}ijjڞ9s(??  CC3gݻU}*|aĻHs熄SfffT,/JYۋ@SSA xݫWj޽{O>˗/ߺukIIIzzznݪxBGG\ZZZRRR[8 wPOO:UXX3%%%~Dz444ccc+*""ѣG;weٔ:'8:QWD"Qlll^^T$|Tͽr劕UM2uܹs7j(++WgTbZ蠡:!tرV&,'%%;,,ɩB䪩mU'? //qwGAA}DܛjT@brsTؼysόꃲٱc!CiӦav 2!܁(**}cՄ|L@`qw@!pdqw@!ywZƮ@@"\rE"F @Tm7lCqw@!pdqw@!pdqw@!ׯ 'OLIIn׮AT dU#=,,l999W)**8884@5B^EC?f*--%hkkOS%%%~m``~q :!A*gffBz}vX2nܸǏ||8!$88ٳg۷okkkkkkId^*~QFuʪsSL3?~\[[ښ~tttQBp֭FFF#G0`@n͛,lmHHLJ b```ccjժb㼽 MLL ֵkW[[ۇJK~~~xx8˲<~ 0+666552hР=zT?HKK#mۖ², BDNPXqݻ׭[G8?~> 7яxӧO:t7[]ti޽| Bp߾}^BJKK탂tuuig,-- ̓w"vu'11.ݻljfYVOOO>~W@PZZ?l~idduȱcҔ_:&&&...tXZZڴi%v~ǻwrڵk;uDݼyΝ;CCCG !=j^õx.:ׯ_=ulbeeE/Bs!jjjݺu+**[uԩ[n|!...?GSSҫW/[[[yyÇ_~۴iK144ر_M;y򤡡!-dСfff۷oێ9RMM‚I E ׺MzYO#>_}{ s]pqq_<#GLvvvt_4֡tB?&%%:t=w\@@jVZх\YY}vkk}%$$;5LJ2XhllL* wS*R֭ !9+WCܫ׷o_BHFFFnnnŵ#tҥM6- w FIIIYn]yB 6CtuPW544lmmRVVV&)]3&&F<4} 7ٙ>`ڷoߩS$ M6yf͚EG6驪BΞ=+933(a#G+((OLRTT .P~!##Cw~>HQ;_~%]^hڵkݻW\\ѣgώ3ӓ` t˗/_pرI&HohhxP''{D7Bڷo?h >L;***[l魢2lذK.=|儐ݻwO<zꌌ#GܹsGbKr-s B$,KOc@T߃^kAڷqs? ɹ8pΝ455njyf~СC7oRWWwvv2e whNEE@777ooON9}Q~6oܫW/رtTյcǎ?K1c֬Y#$VUtǏߵk1yuaaaIIIii)k `UяHvJQ+2.uh rki~7sߤϟkO(..NJJRWWܹ{uu.]TS(DX}YvvL{++))iӦu|&fۣ替SlIԚ׷q:@WXXخ]}Pg C80-!̛BN'aZ!*b@Z׷q:xkAKP)GD"qD#ch MdE;9E"qDY|캩,}7sp+,,TTTl.]qDDaDbZ{'.IVUOYQU$a0}7spED%%% qʰ"<&ʔ/XM~Coɴ(Gb3umܼyĈ&eb; ]C/>a'Mfy9Jeots6n^Nq\YYYUsh.XG;K9Kl77w Sohmܼ9eƮw^R~$o[Ii! 0͙.qSpo1o!L|쾍9GnӧOo+aEB`'G ü31Q9+7ç•?TaVlWS4l8wԬ9'Nn,[]v⫮]J:ujO>HTPPٳg޽ ]q '˷M)o--]jxoAPuUBWY&0{<|W^}|޽͛'R&j^H˲=z;wnʹxZ۱cݻw'u%.+w }v;B0L4?D#z 1϶ف[8-i/ }u\z::;9]fY6&&&''ٳ3f9rD__+ 2Ǜ%cgZ~qYt5Ogcbg*87=M)f ~s n"##,XPTTD2dȠACBBrrr⋿Ru:u"D"QBB½{z]zk.H${޽{/]TXXp$xRީf9[~uGt5BM`Dt2Vtw!i[Iw>˲=uVZ5gpťK,}voooYsL3*//%̙3E_ SSSWWW:WXXXي>|2;^2bcc.h0"N:%i7?B!˱"S?/3eX5U'<<<33boo:ȑ#kBHTTԍ7u *--%|g#G$H}D2}}AAĪ0''}?ή_~~ӧ,]4//b#F0446mZ|򥡡ahhMfnn>eʔAY[[I}XD1*d9!۰;g(.+*R~uoi 7&$''kD/h^|($[nƍ#dgg_vM{)..~!_~7o/###+W޽O9r$˲%%%/_(9::ɓ',ˎ1VeD"eY  /33sٲe>>>9xxc"ǰr$!'DgD""qB9Mv\$!NJ1?19NV\G]CC2|p55sۋ~9B@ 0aB߿uK.߿?!̙3gYm„ <X{Yx)#gy3Lb:ɶхX111tys;Ah)0cǎ%=zyyy}QLHH7o].))y'O8]fMϞ=O{lٲeo޽{1߭833ҥK0Ν;şg'YFRQQsּLm۶)2@(S+4yKeB;v<|9sRSSn߾M555':JTUUUAAAm^QIIɅ C*xbcǎ;v:t(q׮]NZZڴiyn455۶mGUEϳcbbh=˵-]zJ wYNDx{޲o_rلw!SNϟOHHsN۶m?䓱cǪݻҾ}{ys;PG/^Ojz&6!!!77~ؾ}{--L tN IDAT~'Kf͚p6mteSSΝ̢i;ZXXyj[&4wzgoao8[UbFVu(ˑ7S$߭=7#/ k1N!ɚq )|ӤMrff}!fffk}}}|` 9,L:%+:: %D"і-[ﭕMB<ƒVUrYY{'Yu+}c,qo$pX~7];̬<%opoj455/\rʤ$U&k׮ys;PgϞctuuxs%VŜ:ujΝǏ?q!DYYyȐ!4'{֬Y}/ŋUNg9z(!Ĥ{P&4:,߼@!x[m8f,7΄Y7 s|4-G%P\#//?sLBׯ/^뛕u̙9s9.\X6sYPFCCcU7n\tttaaaʼnjZUN:yzzü'O,>͏yjj*}m^EtΫ[иX0;=`/y`(au2ќ]fd"`!dUrfq ai%̜9͛ׯ_OJJZfکS:88~9GZRRÇ !Gfdرc|1U4\Ǎl2??޽{uciiVPP5kAS*BtРAyyy[k[q/PKa UaE ?U K0u#dǸ|V~u, Ǘ&"̛G-|hժ՞={ڵ6l 1[Z9ghOhJKKPBDŽ]]]ŕ*(()((1bΝ;R&4+ !%?B֍'_y'euluBó*ʷH$JNNNHHD;wׯ99,=za]vB̙#2qJnt(+'738 B牑؄d߈.N ѣ9G-EVVֽ{_N;)D("IB8]o8ܗȺ3DĽ F[)MNci^.]Z~=]nݺի> E "Sw'?zw:_WP * ?;a5caȸ4_hpdqw@!pdqw@!pdqw@!pdqw@!pdqw@!pdqw@!'zZ1㸆 @1 S1ʪ&"i RvʕDRhhh4v&77wdqw@!pdqw@!R\bEIIIMr~gC @i)αcNJkа>'O̝;WANNΝ;wwޥKz]￷nݺ^wT 7lпSjüw磌˷8&<̲*ҒwituuO޾}W}^+qqqk׮ݰaúvU۰a^Z?zj cӊ֯_0̬Yy5 mTMۺu!C/ZT*^ V\n:a.^H?n޼a>SV-k׮)S[qlllgϞ=xݻw<8,,_~ǽ{޺ukԨQ5̿sNWWט##8>y]ZjU (((ptt8p`nn#G<==?~,ه8N$;)>I m MdM&D#c"0ded$["0v(*bd)Eu=?>uݴz={|s~Sd,תUC5jW*@˼/λLLLT*_oaRSSW\9m4ү_}Ν;ȑ#{ݻwE칰/wٿȑ#+swޗ/_.[l̙,e;w>rH\\\&MlfӦMU] |KcVi<ƦgϞM6uss|!55ήQF۷Ȑ… 5jԨь3Ғ7o޴޽?33}=޽ۢEM6رUV~5kT=x`鬽o߾VϞ=e˖)>}cllÓ(==e˖9p-Z\tTGm6g:VǏgcǎuOMLLZn=cƌq͛7ub͛7m$ˉɩcǎ BSNSoޭ[7##mΙ3ݻ%(5Ebx~7mt֭ b޼y5222ٳgiQsP_$~BS[nyzz={VaDdff6mڴ/_޿b3f|ROOoD' exy^ɓ-[0zzzDVqW8WbbbV^=rHAtrʕ)Sy^xjժ={L>}֬Ylkbbbttt^^{ѿ;ڵkذaDcoooffFDM4iѢ{m۶SNnnnO~Q$!!!22o߾:t?뗚Z'nذaԨQ/_e2ʕ+=<>/㏗-[֬Y3 ϫ9HཅpGUrra222~sΝ?6lǏYdzJ[^r%X">>|}}Yర={QӦM<:u*++=WnݺeddH#G{WddˣN8SVٳg$ӧߺukw9rHbbbf|}}"T*۶mdaaq̙T^?&+VKFJtᴴ4vSR*WxeBBѣGߩS&$$QR'Nl֬YLL#""LrҥUV   qpp֭ŋU*X[[@@o>CC[n500X~ɓ'_~Ubwvvx A8~';ᑑ!^eзL\0t{ *lٲeĉ͛78qoq\RRҖ-[XN֮][F AO{իWѧ~"1UVƦ^zlk޽ӎ;gСxv튏?r䈋 Ϣ"jՊ5͜9sx e).\Xp!{ill|r"xbq;5kLkۗݻWC۾};qݍ S k׮}||LLL˗/+ٳg6(,$$4224i٥K~{{Ν;=ݺukܸY߽{=<<ڵk~СC,ìY\\\;%oÉubA< "sNM^0q{ ᎪׯQϞ=U 8ޞnܸ!&֯_ɒ%Dt˗Ϙ1#//^zK.ԩuرrSC.[ÇoҤɀ^z5e"-]۷tpp "vNKKoժ´mۖXQؙdڴiCDeܠA3g\r>**}Hytuu[lylOdG?;ի ҽթSg͚5III!!!f211 qrrbݤ81bDJJ l۷o1B|ɓU6j(+++//Ǐ? lll؊pH$FάVɉJsE3zKnn.kq0119sV6622R8dȐÇ_ǭ^D522R W3g>~„ __K.}M49}իW=<}]v&MZvm׮]sqƱ SN9rcffC_PCN>} ccݻw߽{ɓ'VO;Q%f̘q}9::ݻF.]:qℱn`JHT y]tts:uh7w={ܹsgƌ:u255-Y~^p+ ߶mKƎqF-,,ƍ'5m4((;888;;ꫯZf {!]+Қ$.]1b޽z*|'׷ڵKWWW-rvvSΡCݻ#:w?NܬҒZJWWiӦ6lpuue[^vcܹsΝC ?nڷo߄ 'Zhؼys5'J} |]dT,6/}4**¢YfE>[n!!!kРA<'$$ԩS(D/Ri)ʨ GGjժ>^zzٳguIfoO<)SUֵkؓåMf͚,W\);wgر7ԯ_x9s_~immfىe=$֮]{JҠA"ojW.ەRl;!ujCg¢rvqҥ#G\ŝ~dͯ@ ]U5h`׮]yyy .tvvnӦ_|b~yyy1cƈ{~'6モ߿?K8pMAkO>VݸqwM xOoʸ_M6{ܹ"mmmo*`233S~ׯ/!ZjbҤ۷ߴiOe͛78>SNi'-Ț5kbbbl3ׯ?tիW+COi J4nܸp9M.nXad2YweeeI7|tXdiSnj֬ҭXmo^zYnTg6fh9;CZh9 D_WuٳgURAP{12h7-prw@!-prw@!-prw@!-prw@!-prw@!-p&n޼YVZ׮ɥԴRxYy h1 h-Q.CZh9;p IDATCZh9;CZh9;CZh9;CZh9;CZh9;CZh9;CZN Pnxߺu ]vm޼999۷outtl۶m9ʎKMM0ObbJ^6m5k&Ue]QVV5-\p3oܸܹsYYYڵѣVVV9993f˸+(_ .nk 4iR*gJr~~~ YYY7o%KhO]J}722ܹӧovvvbbbZlY%,+AF^:88Ԯ];==ܹsW\!-[>>;w|%"NW_}]AÇm۶=~8KqaYlܸٳgܽz\xݻ++bυ$$$4i$...((hȐ!E:tPVVּy&MR&LЧO'O޿QFUXJаaիWWu)Pnc W^~QFFƺuL"}R=z*:D~ +1!Yf-Zp>VK5yyy};wg}fccӳgϹsJ/[nvիWq{V(*wsskذӂ KtPi m۶'^z{o#G߾}ҥKķjj 6$"[[ׯ?^+*nڴ믿NJJ433[f̀Ԝ(Eb'ȑIII-ZHHHhϞ=,6Lst&VԣGѠAH.0@M!YN233/]$K,ٳg Ĝ#&&F;y "b-RuܹY|Pq!$ B%:!ɓ's'bCݻfC%\i{ֵkX̟?ׯ_裏J,;`_|'Ozxx_Ž^<ӦMyڵk,ʕ+fffbт ($$N<5蘘`KCDZtuu^:붬0ϯU[7o^5 \XXB:u)K9s&qbPzs΍=444tѢE]v۷XөS'kkG/_TCvcKHH8t萗Kd-YիWWȩ& zѼyƎRƎaÆƍ.ajՊn޼9hР􄄄=zH)b-EFFC-[qxgo++vv͛7YiK<"6o<,,L aG#;AteaadɒEEFF>|x֭/^WqC tR=X󥴗1c.^8qyu}ذa_MDDΕUqt! Wjutڵ+i~E:144BBBx744<{AlE&E~~~QQQ^*nݺ~H,,,|24Ѥ*_R,|ePƍŪ"j֬YoS.k׮]v|󍇇GDDġC&L@D>>>=z 477裏_?~h۶#G4/Tq q+8UܜQS-Vpe˖ҞSLٹsgvv]F-2M6N:l姟~Zn[ӳYɃذ>pNqիܸ853=zu *ߡCaY^^^ׯ_g5ҖJK{{#G>l``P~#}ym*mnn^Nn,CMS={d~M L6- `޼ybEEGGKG߾}~&&&ldݐ!C //o޼yfffҖV##aÆEDD7nܾ}#GL2ڵkƍS;!!;uÇŎ*iիϞ=OD&\0>+oٳgaQÇOo%ԫWÉaaa'O P ޼ysvc`}U(9{okkk6VVVݺu+O)g#My1,,Luf*Z!11#"P;w(!P?;***((HuΝnݺ ;w ҉^|9f֩Yd,+|oVZϺq7nlR}5~hr ֫Woرk֬yي+,n.0aB.]DժUkҤɝ;wE0`k?~.]m;w=(}lСC\rܹ4֒Y5ƍ۾}v)22߿QF`ر8qDBB_|!V̟??44t֬YwFΟ?2\\\|}}k׮}bGQnn}~'Ovرg1rU ̭[z|'NԨQǡnzۋ/<BDƍ0aנAڴi7o6kL9oo ;;;iǏ8⒜舳>Q%kVԫW/'''cc㰰3g-^IqxQiӦQ-6lQ?žɌ.]t~׬Yd -%]V%t2QӦM(ׯ_5*222F:*'8pAnnŋkzݻϜ9S.E kܸѣG4h?mܸQ%F~>}9sVI@DϞ=ڵ+zʔ)uMJJ:ի∨F.\>~oٺukݺuo޼r~-[rrrf̘!;vzvOUdFڹV^=|p;wѣ7mTM SttӤI͛԰adϞ=[nًSSAP(zZ&2jԨ#G$&&VV-99YOOOޣGjԨQ\iSSS1Ewss vZx𡅅Ej o-D/Ri)[neffmV:CDQ|5z?Ç?~#<ؿ+WTZLLL/_n]cƌ^x!) t4i>y???iѣw]xp#G߿7.wܩQe1tP1)իWŕ wJ[___}^:lW*wؠwyBDP433+nĽ{2r5-DoG{LVQYpg&"AħCpvI&UDTf́^xIL\` æd_}.>LMMEs!M8qbڴi*MXy&Mtԩ؋ۛxdvvvfՕ"]K]9ےςfx[L) q .t.֤VdfRN={I1O1ʇϋ?ؑ33➤ B=l9zѓ4!0R#R4pdx!XDDBKM[+䈆wyka]EV1Jֻ:4M;t𺠲N 2)z~\o)kb) @Kpd}w7ZpD3Ⱥ4/ȿOD|-TZYQ̍eJOb\! .2-:q-Jx&NFX,;%S b؆`VmϱXGp)M-."# mdKV9񒸡)'ӕDŽ[IT%dW䕚V (gIDATaЛMn̚*/z4k3[/)Q-@=eD4w*F5 (kaLs X W:l9 + iwfAr8^Ic )jѹ;բ!41\[rz:Թ1wfA݉AQyvu""3*zo*\]Yׇ߮pdRP(9"#Nz=H_S?2 ݪeצHA)Pڒ#MHˏJ@]VіKBeE%n 9 W+5|BPiT'TMǝ?SRߕ^ )xґSMYl׼.Q@JM~Wh%jw/ D!РQː()2^+)951TJ83#ScR2i?IXtӮD@9N$UY Fqdl@Dtj*4122УD:-Qzq1yujw߄8jWq;YEDt(GA:Tې+#Km"& O ht#P 9$͊;%?3x%) BI?^XJ ,""sM-8"|MS`!DžVV]rhH?VF?EӼvY%Si[d쥤}NS{qXڔ7?ɘ7rbG$1%;W%"Յ쪬1Kz%غ#Wx%)xR dY X"[ᕂRe\tH_VTTק_>љX|fqi{S`~U>[9g}C@yR b@WrOA:JwXI{r&:58g Ȕ(5 |'FDdl@r>"vnyee>J 4(OJrC lD=ri&ym?GQԠX/=N1a[gR(U9?ԧuY۟fHH*W+#BQP2^)䏈fH m@<)HGɯJEΓ$^*RH%=;IY9DD:R~\JN$^)'YI\A_Z~m 엨`Vbz!:T,zn'뼪.VWMʢ镸4fL2[8(Υ.Gy#KAYD:K)R(vQAMuZJc@IB$ypeA-rITkb"(qzy9J]p+-B9gil@ !(\.4zLNDo*7؆H,\@&uK2ёߒ_Pò|յQF `t(^~ٳg2hs`t2+ɳgL>,(;;;%%Ő٦L#3eVG &z䉙2#):Xh``E#fceffKutt5άI Y} =dddXYP~,Z+@ժUJ?ʩT*={VjTbcǎLrqq (0=ή$i߾}ԨQ9qĐIkNNNV3hР0ca3g~{ԩgHk׮lcmܸqڴiJeŊK.Wݻ52Iw{W!00?0G+Vu&;շn=z"m۶%(dׯ2eJxI0ЂDDD5?=`I&̙3k ,DZZZllΤF1GՃXaÆLZx%Kʶ9Z[n%A˲>*xI, $*Jᆔξ|NGGRkJ+3^ze^,?k׮|Crn:--MgR|aÆ 6޽K8 :TRMxС(?NLLLLLLOOU[>}^{UrtQ>}3^{޽C9s&))ӧ...umԨр:udeU>T*Sλ899eff[N/BVV֓'ON8jժ5k6l;v,!FT* FFFe RRRz~w|{_ݩS'99bBBBK}(v֭qu%2 wի NקOpR:eOO#G0aB~d2|<ЗqFU}ѫYf5mᅲ^=|ȑ#CۼysǎuFT߿?nܸ }ߢEtF$+Wl֬م JݻW棑tܹ͛G,(ׯoҤŋW6n3fqB"Un*Fjԩ,cw70իW;wcVѣG;utQӊ_xGK[oM6pR9rs&;wٲe2(7 ӧ-9u-b^{mժUG;vl^^ %[n4i=?`i:tXBW_}&S ̙3so~f2ƍ7uG[v۶m:t5GGk`izyy8k Tz왐`TCp 80xTT*ǍiӦTT ?8P}n/pµkаaÕ+W԰uV}>saÇ ԙs+A /|Ǧ&OlTg޼y iii#F7eNϞ=0` ?|rcK-^bXR$Q?|[(`;jJ#JIIuNQ08p+ɝ;wRo?~ذa%t"]vV`ر2JhhɓK>+̙3:/uҤI}/ xjժ~z}?K.d8qRoo_~~'|2v{y[J+W̜9ӄ |7n4 Q'NVz߿믳Z(^B<Ν;oܸal4]v/^TydÈ ._jժ%aŊ%e,77w)StM _5iRRRF]*#jJe`` ZhP(6mosoojϼyd@ׯwVoݺf#/~ݻQEtnm۶VP͛7/]AFڳgGa̙^^^%DúuRŋAAA!!!R(֟YZ_\,A"""e`?5LV}u)(ϻw:svAca2kkk<-+ ݻ׫WO䈈SN=z4}tONΜ9Ua\\/yȐ!ڵsqqȈ?߾}ӻuVZ-,_9 ի5v `i.UVƍո諯վ}3g߿VZJ2>>ܹsK.+9::nذA={ diի׆ 5k]pWp^.]d*/7oޔ+l۶M˗/߰aüyMk׮vL:t$,ww>G򊭭mrrrXX?piR'NiݺUs&L1e˦M~}/^l8PXsss'7i={L{wqƹ* kk&M111/.vktzw;QFv{)D` Z~/^sْ.Ӫʕ+jݬN榯ҥK%)QcW_}]?ԨQC_j\\\RR$WWן~IgG*++>SN罹ϧ~c)z%]```xx|ٱc߿_}<ͨQ9=ʝ;wLhdʔ)_OOO''J*3&,,lwUV|\u?1clڴI{77{~7 ՗n:>/++uKU* ڿݺuM(+ܹs"3f̐/ޣG} )pn^ٳGtT//xK2a9ȡC|@^}Uov?#K JFտK>V ,З3&3oɒ%!( K^*,gpј2DQԞ/_%((H++++^#,,L_RǎeFѣ ЧiӦk׮5ӠW^uv0l0}I+++lڴE>{wGΝe ^z6I2䣷m۶I L {bmllT;wjP*:WސRi߾}Æ u&ݾ}[f~!etxEQA-Xe=z! <յe.2ѥ#G^L戈BZ;8::Up2={RL8ԩSeݿKC' "E1..n;vLII)Ś t]&^&UA2bNj2nݸqɓ':7n/2NP\SI9B; ֭[ўmkk1ٳJR5`MKK(X!'ruu-aTNc*NNNLBѵkW,'On޼/W}Iϟ?շ>>>>֭[7nзb iT9þ)ӐUV֭۱ct\&TbJ2rYC9 0j(cK;w켽5ddd\vKk֭"Ç%nZ:ZxK~~n^TW ЦM+==]_Ғ%KJXyZZZQQE" $lӦB*7o$#55~;{˗L~Fdiښ|\c[>>>yp2+WfϞ]Kb^ !Ŏ"9Je>2ˬO.2ispp's m5k4W 噩rADQ F4idŊf:uj>}LhFhhh``` fϞ}^@:jhܸJ^G~r:R/|ͭAmVŒ/ɨ0իW/昑L\J*FTbըQèŐR 8״i';/=z^^ԩS{}vz{VV2gϞu\m,xq >~X{Aw_Kie:BE}I&)"s mf 䘯rIQQ!n߾}5 7n4Йݺu[~Qj׮-L؋ d²}YxM,KQ~}޽+Mۤs`ˬdf22 NFE"&HI%gT(b?ذł df6}H3;v֭w1treVnneY5V's_\@PΓC߽{4vFDD1 JL •h+fff>d277W&`gQFtSV*~OܠӧOtݻg *U޽{׮]ڷo/B%`sFѥKߤI(`Y??RK{,vڕU*cJ\Obc5jlvqq9YaaYʠ ׯ_O |ssڵ,XG_vX2j՗T2֭[e T ! :{Tk1뀬 N:ܹcx/R̥VZ2*>MK.KkݺQ 3+}Ѵ'O<G/,,?~Q+!.X@{n; ;y$BO>D˕W+Μ9c:Qϟ?/Өjj i۶˗/kOX&Iݷo!>>ŀJW_}u+q/cX?r!9/\ H ԱcG}I'OA˂j?kgf,׭[W_ƍ yܰa tL+"""&&ƐJnݺu1}ݺu3efӳgO}IFLxb}2;wo#*Qϝ;'o ɶj*T\]]usNttt ^,ΰ_X&P(2ag _~eiD_]Cʌ3։8sLXF*eI&o2sِ!CzL4I_qL?zLW ^~]_LAQQQy*_~%**J>ϙ3g~w}Z*Xe`b?7oެTRf/awb`Xãl& +vy666ŎrP3L9;;k҉'jϔtܹf͚L|e+cKժUk̘16mҙ6gΜoFfpŋ}z7o,\]fΜ/%·~UVծ][#)==>ؼyLB?~PPs۶mEEE?+Vڰat&]|>WA~ ۼyΟ?rJ)P>S}O.3S#j9L ` с IDATO6Apǎ3f̐YvܸqWÀڵkoɓ>#JMv }5O0ؠ'"eYj׮ݸq;wgc,)ĉ2<~800m 6z#GdyL>}Ύyyyرcg}5۷=z[BHNN?p}={ (SJƌlٲ۷oL|rV 6|pwwڵk߸qc2- tR ƍ +W^bLLdѢE5mT_WN:u!$EEE!!!~ib*B_~e6mZZÇ=zt5J bɒ%C՗aҥׯ6m 7n܈\fMvvRkמ={vx!8~~~jtF1GGG}I([N}ONNFUV?~<))I_%999vڵkM\O?CrجX7ԗAEc_O3O?K.ӄ3fݺuV*?~|DDs[ӧ6o|СwyaÆqqqݻwU/NX*iiiG=zk֬Uw2,֭[f 9`!,%WU[ ڲe\H.kܸqIȌ"47|ӨQҭ1bٲeU͛uP"##WXBXjzA+++5.\rJժU8pL>}ܹs{W | suu-I  ݻ7o^ZΚ5^+,SlΝ;3I 5lp۶m.⦓֭[KI4iW^)I a>YB̟?ԩ%z{՞#LqL#ԡCo-Z=ي+L{lmm&$$Pg3!պuw}״͞=[jԨqu떼aÆDNiݺ6?X* g`˰ݱcG͚5*m ?.a%Ǐ4ƦoY`AɇtBXvի}չL]x̴hUTѷ+d?^cp5k kUrN6M_СCiiiVԩ.\ 4j(22UY5;v(Ʌ `Y Jˀ9bh uٜa .,a(00044cǎؿ 4iB1cŋAbĈgϞ՞R]rٯZ… ԩ3A}LHʧNrJû^vʕ+}TR֭uyAMxo9L=ڽ{wWVmӦM~Y?rʕ+B5j[nˉ% Q)T`=z􈍍;wL6++7x#**J_R(~iXXȑ#KK.ϟ_WuժU 2+22]v5H׮]Ϝ9cǎ̐=n8} ФI{OsmذLŋ_zUc9s""" Alr'OT З޽{2[#JUV=vؗ_~YJl&Ly⤥jΜ9 K,^!E͛wSZ~GL !)gϞ;w޼y3)))''~߿;S^n Bvv͛7]‚[[[GGG7xÐ(^vÇzjZZ5++5kzxxx{{[悃ƺvڱcǎ?~ҥGKkժնmۀ^{ͭ|)OSN޽ԩSo~ ֍58qb_GnnΤΝ;;wNCBBBCCҲ]]]֭>xG';;ǎ;qD|||zzƦf͚իW@@@׮]˥dB"=t=~kSBEQ!`DQ͵I(>}4--VZ...#b%CgϞeeeըQㅞN(???77r-fxEQPPc[[ۗ3QJ:3P( }trqq)B\ ))[[[ѯxTTIi?^X4Xh``E#F ,,X4Xh6fcc~zIeF ⱱ2eJy@a!,,X4Xh``E#F ,,X4Xh``E#F ,,X4Xh``E#F ,,X4Xh``E#F ,,X4Xh``E#F ,tR|| իWի|؛7o ^XݺusuuMMM=}jG6m-{tizȐ! Bٳ<~&4 8q"..N}6iDyʕ[n b֊+v)B-_.yݺu6lf͚=R/n;w>#FP={|ϟ80''G}=PA{QV֬YgA6mڤ `}[laҤI7n,eBxHJT*hѢ͛|rim۶ǎYܹsCBBz-R9cƌ]viׯT*]\\֭[7x*Uw^EKTW|9s戢xڵXqw.((˫lXgϖy{{oܸZr&M<~7o }gܸqz~\\_†}A;;#GW>Hݻw_zU{x[[ۈOO@Rԯ_k׮vHHzjȑ#˦1>/_8;;Gߥ={<{LkkO>DWTm;t萴1|pi=A 'O]aڴiD@)*XuUM_}a6#00ӂ |gǏ?~\ PU?ӡC)P(⤤6mxxxhiذacccMn(oߖG34͛^}U,Xٹs1bĈ3g]|} ڵKM6-Z0w$qMgvrrR^y}6lXÇU6iD_:uH:X2L !v={U~ PA{̺~SVΝ;mTi PerJo߾bŊG>>F唕T HۻvR$yyy3fP 3fѣG޽{K˖-믿Kرc ;zҥKEI)))cƌјokƍܹsUa8y9 `߾}=v:{l l۶ޒOqqqJ2((ýzrqqIHH_cbbl'NTr:(ʜaÆ;;wvpp ݲe3SV^*5&44e˖g9r4rǎw}74im6x͚5NjLA T 9KݶmåiCBBBBB4ta͚5{ڵk>ãm۶mڴю /99YPH]T^Ν;'SJs4^W'33S0`@ *K !^,``E#F ,,X4Xh``E#F ,,X4Xh``E#F ,,X4Xh``E#F ,,X4Xh``E#F ^t)>>ބݺusuuǧgC(B.]ԩ#pJR  %> OVhӦMeO<.m[[[2DPH={ ԯ_ׄ'N зo&M_P(\RnѴ_ W\u`;[ZDQܻwoQQB8p]YUtƍW ФIvڙX111aaa7oLOO:toiAEM@zz{MAc!Ck!999""BgVZh!mgdd?~\۷ m{ѕ=d%J]9EQܷo_aa mڴ\PP~7+X7nܸԛ]i'FaZcv)?g͚eKA򙓓U׮]+,IعsN__bۜ^Ԟ={J;[c^Ci&ioLD|ҤIe٘ϟ[ȒsJ 3fYzj罹spP& CѢEb3O2E\fMƒ}DQRb`"Xhhjxx!g){J~rdWeV&XM}4 Çɹyyyyyy)YH{<~džĦBb8|BB|M{]NNN.]eٳ ʻ!ejݺu~~~~~~}IRl۷AK$ci80BOOOΝ;'mT^Yfj׮m= (n߾>9bY>?3gHۙEFFJVA6mZ=^%nݒ52<(={lذV\XS|@?*3KkҥK._Z AF~zsrI `={&5h <<\ڮ[n6-ZhҤI ԯ_RUԫpaիW_vm6V>O< /!]^r/=q'7 w``.]} ۧ>GCammݥKnEswwwww/V,*Ud8i A4hP- *$$D&R5 Nn4@DQ<}kRRR6mکSM)''rJAAA:uvNtrrj޼yTT !!!יMеkWQ{EEEݻwɩE~~~VVLVPP}ƍʕ+7k֬UVr狢P(,O>UexAʕW^ڵk/_~C6mڴi/\TTdcc#]*IIINJHHׯUTT$gdcccmm}֭'O4k˫e˖ qabǎj۶iKAݸq#666>>>77nݺҲ...yӥ kԨUGGjժ;w...ݻUVrss3WqI>>OdeeIoSJӧ {grS5^SrrrTJשSG EEEk׶u!)Ζ#i^zaJJtWŪ z2eL6U###UKqԬY̙3:˦ 8PcoaÆBJ >*_~ݦMB>>>ku%&&<|P?SܺukݻWEݻѤ,)UPj*UuzQ(N8Q'j贵]fAU'O WFQF={L*w=Oøq[޽+JDxU;o߮3&뛐`ȩ6*HEUrss?ʕ+qyQcbb4zQ(ofAAA UM|iӧ)ᣏ>RV Ϗ?;wVfJRTjժi?P=yf̭[Oyco2J >~j.YDUj׮]:3RCh̴ oThkkHÅ I=_IYn]lll5իW?~QȨO T3,]ݻ]v7wrCR)UHݻUyΞ=+u!1ζw^UjTTƩPZlQ\\`RR XP .7n؟py'Ol׮݁R),,ܽ{wΝo޼iAAAA``?h A+zMò'L0vT1c,\PE??Ǐk xٳ˗5fVmk)Q/,,3gΐ!CTuСCZڱcβ(~3gy}QKwT)ϟ1cF`` aÆ G^?6߲eK@@@XXvRNN֭[{ժΞ=]ҥKwޕ|MU;G)upܹ۫?3*'MB5k… -A>|ݻu1A(;v3fjhPoi/mג~s|^T]֫W/{uaaڵk-[o\RSɓjyiϞ==yD&L믵A՗$m͝EP( &mJuy9RVZ}c~ ԨQcvQQф WΞ8qT֭[ 9>::u:u-E-d'|T/znBSM=P_:eoذa~dH#lݻwW5. .rׯ_OIIQO=qℴѢE ,zj~cǎΝ S*o t+-xb<񃁁?gʔ)ݴi!CԫW/::ɓ?.[,%%ESSSuٽ{wH)Zq[l9k֬lUoj8իmww7x,((̜8q)N.\(m(ڵkk V*/J2s;w`ooЫWׯ_aS`igg׫W=zܸqȑ#R֭[SL9y333gϞ-=;M>ۻjժ u׬Y#ȑ#gΜ)=ر_B3|YfI;[hѿ?? .lڴ(==}ĉ7n lذAww8p@߿7ސt֭Caaa  z4j+!iԨQڵxYn]) ޫW/)Y?/[YY 4uwپ}Xh")O5 dggw{ YfF׬Y-mwjܸqVV֙3gn*}>׬YScǎI}i7n вeK$|),Maaa``O?$_1922R U;88 Zj"(?A٣J?>ҥKGW*FR;t/='=qRo-Q nmժޙEqlfY n"h]bA1I#%,FMn|hLP([ ۷ggASOuMuu-NWHL?~\iVDDEGG[)wܹ3KyT!</t)ݣGsΑ[XB3f 8\$ч4$$qr3//,>LE]QtKkQ,>&XhLjIpHHEQbXjL244vEQ$rPP])}ؑccu֑2aUįVDfԩwܡ#*(j::9BHQI2ٳDǁr[ɶfؽ{wz1ϛ7ܒJDVzhGB6V^=zhS,Z$_GO:[2̞=x޻w X!G4Ghh(|ܼy\C Z;?h"B+Saﶶ;w𚚚͛79s^nc񳚬Y222:qmXXw 333g͚EI,Rwyfԣ… ?̉ݻw}}zHD&,;;\ ;"@3>#1IқL 3`͛7:u"F-\0$$% KBC e͚5 Bh$fN~7Cϟ/(ƘuQQ裏 4x`|-JdJOAAC(8d"!EQzuV|˨(#<11Q ?4"ʹST"""UG 3i$A!#GׯK:<$Bɓ7o떖zիxiNUxO^+,--yZ@qLJ˥C֟555ǎHv5Y2d@ŭQ=zYfffBxnI",_\6e˖TnHH\ד|1xOdt8;;s|/EOȏ7nV95o~Ϟ=:@da;vرcGnkrr̘1;&k ̙3س*lRxwBL_TWW˦fu_tݻ71SH$z'N$RV"h۶m;vHLL1buuuBgɦ[rѣt}ŋOSSSI ϙ3_ggg;;;w7oւ:>B\Q@k=E%LLLdni'А>|0vy9i|l'vEtxjjj 9?8z.Em`Ynnn *k:;;˝YXXt RՔҥK"\.?9sJi ,, #HJJ"fɌAk!CfΝ;ED"^/666e*AQZF===A1-R={tttӧO+88xʔ)#FDt 4:d'׳gϞ={&Ņn8##cݲ166>}B- #s#GbSSMMMӧOwtt|w MMMl &qpp`"~FC^aqfu_rbG E 'e+Y*++qw֭[*XyW^l"{qM`iiYYYp!R[oŰԙ lذ!55㈷p- !777q~p's\;=E%*ǵUG 0gdzdcԁB:u"{L8q"6a>o}…mmmyyyxZwww BٱGnll755%K}*lccѣG\٘={ r?"Km߾}ܸq acc޽{!cΘ1~1`̀***"v.qƍkfdd`J3X999^^^|r6~oeRAAʕ+W\ijj:bĈɓ'r+,`Rڵ+RԬ~Kb_X !~NKK֭[ٓPdP"d"HBCC%$z(ԇHَ;8bmmeMS=z 籍E =E%XD*Hahhh``I:C:88ඝLtCf̘>=L0+,b;,00lcLKK1cFkk+ ,@sBGqOjwXZZC"mR'f&X]Bo&K-Avw= ¾v[o16ܾ}{ƍ>Oձ<1::c׮]  Xz"h8p ]G#Hv2uuuǎ? ˎIaXvܤfu_zE%jlmРA, ^∑q r$ ۷vǏ[T`ѧK.e,Hfbe]UUN:_|hI@t@qG8|pyyѣGO>N{NNĉϟ?OL{`ͧ+**d*KTGGQF;88xzz޺u(O3gÃk׮E4}F",YdѢEǏOMMmkkww;dUG׿x۷oK^^^nnn...cǎJ6}`MMM3g|2ecccf 1ú9LJJʡCU"xZV)w;Ht]uDDʕ+RӧOSSSnj_aMc Ŏ={z}ݽG?f9 ~hWA%찇իW:Gߞ?~kkgbaw˖-ܵN&% jV~x+֯_ðOwJ6}@$b'IIIg&.5g=u):ߧoh*HaaaY\|=f~~>_/666#GĊ1cƐJ_!Ɣ'禧$E"Q`` Vd>s^a!wL&3hkkv&$ݺu#$]/'-J5:'Nj*ƿrrrH:gбcGbԩSr?3QA.KOX|˗!ٳ&ML;|z*93$[,İ,nnn:99l6l=ڨȹs綴p33dNZFDRB׿xf̘`BVQ)J6= 4O>x އbff6n8{&cUUUBV)Ȗ{a: %j@dc[Tb.dA1l:{,GѣGϟ?ڵ /*( /N<)wCbbMw)I]A*oܸL&ޞX ]nl'&&&&&>xzzz⋦&l?B*lBw8MHLLÇWqݻwq}w{A<4!LMM v]`C6٭| Ga;9*L6e6x݉۶m#;CR~Ii5_⇖ ?jjj݈!%%EGC%>ӯ_?PKK N{&Cl޼9>>߿RGzzh!z޼yr8!4uTU~~qll,ָ5Jn2PgLqrr½0`y(&;w#ȸqT2$ (,XiS3 \xq:((][ *[ZZ[%4uuu!w)K011Yb޿?Ybbccɱ# UV1t"J W?_TVVFDD0dw_;`ܽqƥK𵯯//g.jkkÉ)Y-,BTVV޺u z֬Y1;wNcccR_o޼Q/Eoۛ}+`RB׿=s # s%?H|.A9d#T[QQ>pqf~w544tʔ)"(!!cǎ8066~Tsx#lVG www|}ƍ 1/_"HMLcǎo6>p_58[[["ƏGyl`!Ϳ[<_p+::ή… )N133?~CHۯΟ?O<)bQ"/^cǎBTy@ss󒒒]v]֭2dT*mhh6mٳ}||LLLU,cjj͝,^8--iѢE~~~3f+0ɓ'w޽?^*((O".\8bĈ>}\v-===%%O۷oݺ2nܸAUUUݻl;w.fAu +\})S<}ܹsӧOI~3 Z[[SzzsrrBG^hCssW IDATs?͜9SiY P:c*yyygϞ%Z]Lrrr޽G7א,'Ntpp7o#-K6=',,EK.cƌQ)ӧG@Q ƒ![vG}zyTTɓ'uEJ xUEAbΝX{{ukkkTsm->>~С*2̙C hff6m4UgL8q"] ~0z脄|-(&ȡuPAִ111,шMʸ8(J͛b?U都{G;۴i{d@b!Z[[Ȫ!$JJJw˗/I\/rصk[P;w}Ȑ!mذAѧN###'5[\\L"ӓ?xfKI_H"KQF>w\xIl# H|Gl4hPCC7 }(m۶rIRJJ%M4KVDa<|޽l>S|7T:(Ƿ""""6mڄ5zh-MvNyJFӧq`}}=)[Y溏9X]]ҎdchyF&FU:G\]]q+V0nOGEE1$P%wrvvvJ_T HR6y۶mHR~ƍ䖢ч2_l|.-wOQ HdS'ڃa߾}dk NSSsڵ*=?MnKSDŽ>|HgrWnH~ѣƦ998Fuuuuu5ݔVm<ɍ 6o|AƎ;=e"$ 4@O3e|K,(S: W^{~?OOO'$,XԩSudggJrKuL޽333:&0aBNNYBceRAaT})njsQba233nJHHXp!c,HbbbN:Ew9M###Ν;3RvvvȈ^b~a$om,W_}EljR%)q˽:X׿aMޥ'H&&&Gcll9sf޼yDBk׮+W^,#8LY-%.Y`>f#֑FHj۶mDdɒgϞkE %lG{J2UEJQ{p![:tiǎvbXǨ3} > cDJ^x1#"nqYh_V^zb###++''']땥477777[nvvv&Mb7ؘUSSXTzڵjSSӁv҅~*///??]sхz/c[]tuttgtrr*((@ݻ7,,رcyСf*==͛VVV>>>9EEEyyydРA)\b?MÇq@G_rAff+W߿;ń  .TVVϏ%sNCCJ[H {iz&>~=g=hb>qDyyG---{P!ZSW Hdnna/č\蟚(Bk[8XxXИ0'X֡C΅T&ЭB!&BBB^OQ8:|)++޿?9@  >8q"<<2a„!C*?+ !@7lؠ\& +&MŽ[[[{zz|o߾K.+uGj^%BP(X^ ,@׀ k@5P`z (X^ ,@׀ k@5P`z (X^ ,@׀ k@5P`z (X^ ,@׀ k@5%MQBmnn޿AA{b/bhhAׯ_Gyzzz{{ׄ#GP%&OܡCaollܹs'EQb8**J">7n  0h  BG0`W\)**Bۻ_}رcE!޳gOȭJ!oooU[n#GDUWWS(V䧋#y"|}}U͘ntWU+)c._\\\ڵk`` {ׯ߾}!djj:~xU¢ܹs$dKMM})H$SND'v\okk;x`eERt8I`G[)NJyo΃'Mĸ+T!hsiѤޭKr4447n(,,,**J߿˻ۥK]gPSvI2/^|IvZ2///z6^|)E{88މЗ=MFEDDpWpp0WBB͝;oNx~ٻw/{䊊 yӦM+NՕ~qy^b/X/=z4RJy%eq6Fw$''SLU/^Eu^sO_VO)EÍt vTvR~sudll̸%`!hsiѤޭKrvkcchرŋ+**tM%(yQEEEcccSSSSSSe LnJ7GWZ_7l0WytIIIJ۷O 9Iw駟kSӓW4ŋKJJ>|A;>)}Rɣ輑yeҥᥥ"|rݺuFeD҇*xՇ4py|akkl2 ]Y/4?yt tY顲k^ 7,g\rU=nEEE.\@O䥄|):rn]8P6l EQOXiMh֚x}Jy廆Jjm6HE\!'N;toccS__~ q̙3>,M'hZ\R&Ѝ9Ikk댌 |ݫW/f]ϝ;!dee5}!Sa:6l'N˳gN<[o)eV*// ҥKMM Bhƌ[l`RR<ɑ;b'F 7& A?ѤWZx1EQݻgdd?ߎ;!GDDܽ{!tѣǴ#04D">|sўuה$[tU@k\|ݻL:tQYߧ`&0)EP&ד'On޼7n(Ç>WLcV`555!0)B,ZZZ?NB>|رcG]Ƨ(ܹs7oެ:t$W^rgϞo&!f477Ϗ)矜Сl"b@ inn(J$ɖKK˅  ߿߹sgWWWWW>}'NeeejjjII!C ҩS'S5tZZZ :v8p@" NJ/^ݻw722!.]ի槈YYY+C {(… ՞JZ?1bٳgѿ+gccc8#K4|ڵT*=w\~~H$Z`=)U>}Ѐz>iaa ^XZ^ qSSS 0))iٲerMoI{E=>zͼ[n+RUTTgϖ;ӓE7gϞ 87PZ>H; tORLb-!Ϟ=CuԩsΊ2/g\#]Uj\ωgnݺNEkkk缫O3s|*qy_?ء\N:Á]} +((~zqqqccc^?bsssه28U&hAFq4N|ѦBn:"^Q:t.]rttd޽{VV+++'Md`?<iӦq|q#ܹSVէO]vѣ=y ߕH$.]bF' 0(++#=zDo>q__ߒ.B길8`,#""Z[[]auRVV6j(l몺1"?Cׯ11w̙3 SSSds]]]oΈO.ѣe͚5 q/CBB8f@M/X~=Wx!/ȷ~,Y#'hBW_~|MInnnr_i⥴&cl})M̵9iaf.cT,7odeH޴LAeaO@vɀDLLL̙#;>28ދY~/%&։ko(G]cƌ)((`?yd/^d4:`BkkkJJ۷JGEEUUUheee+W$u?붶zR8oƽ|Ы~?Ϙ1oiap…,Kdd7=T*ݵk׻+[T Bڵk~~~gΜa{tȑ# R)紴C={:O>mڴ}bN.NNNqqq 5kY`رÆ kiiIMMŁQQQ7n o߾=vX 򪩩pBzzT*{oFFV.ً񵝝ԩS{Z^^ZjUee%IwMII9pBڵkk֬e˖lծbcp̶ ^x肂6:t!diiҷo;w$''㝢Gppphh(=TMUU?ُ5km]]]VVo_O>ywq111u֑#G۷w荵AkkkXX؎;BGgtB&&&'O޸qF]ԔXT~ eeeO{lmmLїҚU%k襐]@QT>}|}}c!6"Wڧ~~_|aFFѣG[ZZ^x=l0&iW\/D"#JYp{BǏ *..޶mۭ[B;v,n=SI  y̹gOPUi#Wܹ%66vԨQKKKɶ>}O*413͙SbaE9>EFFNӳs%%%nTEEE|  (uF:։B=BH ;##cj+B~lE:ujkk-**"&M-,, ܹc?%{DEFFՑ[Ϟ=Ʒ$Ƀȭ*MڵkEeddIR.P}dyy9$C\\\Ǐ>Bh֬Y555nnn.9)]̣jnTO?쳶6r￧xs"׭[G2aUlm iii#ÇpFQݾ}p.y!pdnQ#+Vhii!c߾}CBB(?5tH~0SNsYREfKiM ޒ)~rP,w4GGǎ;Ȃ!D3#ӧOڵkt_!D`!USϞ=#P<<<?~Lw^ҞMF ݻkkkpLLr0455C*\ׇwT4)!*!DȾ):l%!$+ҕk_|pqpH$ ks\nq >xv~g$Ō36lp%S(jZJ믿&Aikjju#Y~+%%9t/^ 8qX(֜9spHpp_|||-[~߾}OE@~U#;L>{G3gl:~!yP ,YMNN{1J(@jjlY~= u>}d[,]M ,Є>Z*T*nKXvԯ_+4*co¾#c-KKK.CWw;)'AE .]5r͵ˋD0"/^̱|X\f! IDATJXL$^eo1e. ,ٚ5k݀dlܜ? ,M/Q]' ,MciiIl|1 vb/NHH!B^x!djjJ &&&rWhSj%s)uyʊ|ey!g̘lQOhHH\7C|OC<~%"B(>>>G7rCWs*~u*xlZO8Ӧt\ vIٳg/\_}7Mu2if.DBC>qƼy.:4fWWWބ#r#w Cl;?J>}zxx޽{ς+W\tĈ'O aTn;۷8666=zkRKK֭[ٓkK65x`hY*++y'Di&PRRIPE "jtQ1ԯnѦ%+K!^GlEOiӧ/\0//ocn7ސY-G~ү{/*hXȝ۷7ŋƽ{aر3f AWs*~u~ŵqQYk\\ܡC^VZ/3\\'M$4* cccB%%%ǎۿSݺ)Sܻw]GdoܸvڌJhgEkKT׉+unhhxdY*E[E6"y/D"ٵk׷~۫W/zx]]ݱcϟoccCIrGU=!w8B ɓ'bccc$"lDEu $++ kԩ fmvddr@}yXZZ;VޞDZwފ9zW$Tܹsoc۷7nC՜_j{jB\G[֤U$C4m@3s |\%s\b浬AF666FGG{xxڵ-U]WjGp6l?{|@ƺtR"wCaWB",YdѢEǏOMMmkkwwJX5$YҪ*^ٔtn߾B<zyyں;OCCnmm]^^t9sE)Sdm'o@-Y}y. ۷vǏ[z҂&x9() .//?zӧo3q9Z@9:{jH\3IW۷׮]K3gNVV1<\ymgt?>aGY}/_&ֻKn߾ttt5j[WvCQ%&; `՚{9X+ Uهݣ)ڈ(:t O޳g1eM V=^1!f"h11100y DBdRRٳq*=\T+5_mXmx `ff6n8K^՚&vDn݈#))luCKsH+cǎľ1JpX;nZ劉{xx,_x˗5TO\ӿ˞7innRmJWu5k.]+b믿V|f.`!$H)ɣGG.))!!VjrUrf0))Iv# Jl)r8y\c6mJLLܹs:}suFhnn?~bbbbbbbby͛7WꯊƻwNLLܶmӧe#RTC&OOO|믿FYb.U7%% ׯB%<<N:Us_XmN.wMoۣG|}1"lWwO~vyyy_}X6+暛K|zyy-[l۶mXewfi‰gV 1nnnʕ+입Cl#Gȑ#͑y%gv-W ,,,BE/^$X*͞Ɋ+KDDD[;:eHбcGKg+HG*7ofQ(~2(TUV1Qg!+3g0?!!aܹ䧆v9dYC+2.Pn/F~~>#/빌凰/[;G斖"VɴҢ_'O+)r82""yW'=a!'M322woܸA6*MM;:{ʺujkkɭCzEȏ?Kxڇ%{ 骨kga $I@@q7o2 R>: X503,^l4io/_\`z,--]]]]iMI!rSYYx-Si-Z[[UzlԎl`ࣧ/NKKsrrZhͿHЅ Z[[/\x;vJǏڵ0&mʰ/UP.]Hvڏ>!󨨨'O?B￟:r䈷Eϝ;wq}̙fݺuC J ӦM={IZZZbb~:y<<<ÝϞ=K'޽{=񇅅G.]3FΟ?ommgݳfb [>>>`ĉ[ݻw|B/%e,?})KGƏ!+~I0}!VTߌ3i? xIffݻwNVdkk .1bD>}]RWW۷Sa'?qݡC' ._5nܸLaX|ff&Cηj@p骨k˗/y&O9kC?0eʔaG3sfAZjɒ%!T駟&$$xxxnή DŽz9hРN:a+`nrr)S>}zܹӧ$fmm?I:,,S[[r] [Pz2B (^ ko{8$..N#PBB Jc)X=;% 2dn~u۶mHRz߸q# ȅ/Ov#+bҤI\ ~MMlښVM||<A߰aQFFF Ν+A),ߟȑ#޻wܢV}g޽{#ӇM6\~=YQQ|hZb}rTT.zOWʮ]_R5EK*EQbMNNƷ>Su_Jk2V,KQ*sL>Qidb{1hB~(eB"~~˗$_G hz}vG}TTT^e 7'Od/,ygͯ{ʝE*ʃ~ɳ , ^A\ E=VR5--O9s }֯_ZaA3sm|Z-.'E mmm\l޼wEM} ʎ4dt lllH1Rgs =eNuѣƦ998Fuuuuu5ݔbheORjr#D͛7Ek_G~^MQϣO;333>>^ &YRfu̘1G% 6,33+;;o~S8Ν#fn>>>!/T㑠JW\cbb#H/0<KDR}\ fHoQX,NHHx"˞nݺ-^֭[]Ǩ(MYddO?ԹsgFsFFFtt+gϒi2J%*,"bH9$J]V]]mjj:p.]fCCիW/_\\\ldddee䤉앖bݺu4i&,ҴzCCC >\ՅƜggaÆi͹dff^r]t0aU__…ʞ={i4:MsNCC#myyϹ\C/4/UJԳ)PUUU^^^~~~qqܹhjjxbzz:eI]vOɓ'掎Ē7H X}\ Ó3sfׯ߹sO>6669r${:zӬJOOyfcc}rQQQ^^D"4hҊf;w3gΊ+; :꜊ HdnnDKH ȅL( N|p).oСc 9sݶm۱cƏ뼼V_q)..3gEQڔ8l߾=11Q,s?) Uc֭iii nn=MQԦMMX/NOOu^'$,,N(l`B@5(ȑ#EDɓ'wЁ~ڵko~ippphhvrظsNbqTTD"s\|!Եk@ׯ_}6Bmhh8v u6rHPZZZuu5 :uRQQI~8::뚚3gѣGsxцG"hĉ\"?8K W\)**Bۻs4PPPp Ѐ )OUaOss ݻ'CCC3 .9'Zٳ!dnn)4~hOUjoaaQUUu9ȱ}%ԩSA?|!deeE?Ky t }U^^N?ҧ:UMM֞`8J#ݻw!ׯ_':\]]q Ǐ+Ɗ+ZZZ ^;vT 8&@{=8w\(EA)OUQ2///zk|&2 "%Cq {Y[[@A Sxĵ>$''SLU/^(wKBh|*=hllljjjjjjCiTWWWWWsMBS^^hѢ>姟~5W$qۧ脆Ç 6 UtVʪUrrrt ?zبUJT/9|J8Bc.]ֆhh@QQvЯ_?9;wiÃ! 7,\rUY=|7zsssVV4ry|akkl2 .3zبUy=+YO\Guu,--e)ڷo'|W}@322u^HxEEڵMtO>}ZΆ '\ٳ'O["p٢+W\xFΝ^y)O}E!+...::Z9k?( Ph M2// ҥKMM Bhƌ[l`RR< rH$Ç JZ9HJJbQ`2c߿]AԷJQ]Q$dx|ݻw{jllrW(>}w֍=oѥK֛7oݺu]X[[w}FhkkD{R%Hz3~޽7x~ȑtj<*--fwV__U\\333+++{{{*|maa!m=RwogϞ 8ITkk+>gddjnn>}taaa]]~G *-----555uttL566^r%''ݻݺuh677S%4UbC!ul4M! |NQTVV 2dȐ!:ubywzq3#F8{,w*_nnݻwBFFF.]v.Ғ_PPбcǁ(ڊ200H$o>DxaǎMLLvǏrss00!թ!,RMMMDXvڐJڀX,mٳgN:uܙś?~{M|Ç%%%ӦMa;xI,Y)3f|Dg'#44tȑ 'EpWZjÿoj9VHȮL2eE O0AQ! wqww(;ܸqDd1%11[nrNAAA~)EիWϛ7DG)UӚ7oi&iӦ{L>F600طoۉN~qqqݺu?g>|XK7a;aNzya8R,?NƲo%$$@+{ANNMvvvT*w"l߾}„ D'k׮u֍- gTwG-]燄dffO3g?Lsrr oߎ1bɒ%b~C* BCCʩؔ} 0Q$WWϏo߾ʏs :M  &̛7.~=ڻwoN4ÊwӦM6l:7իWqqqEEEӧOݻwܾ};M бŋƍq<<z|o:uĉWBHLLLnn.G[ݻwϟ>֯7oӧO !C򲴴|ݻ}z 6225j͛'<^~$322qΝ;@zHqqѣ>>>}533KHH8{,=xtDLֲeK-@ٚGDD?~rΝ5k,]`zjRDNC$ƍh2gϞÆ y={ZtƌVVVk-j9oߞFزe Uh;vˇ8g.]'ϊ0aMÇxÇvvvÆ sww/**JHHصkWuuu^^SSSgJu|4,^6lp]tIOO?{lAAxElə kk#G6m4**ڵk ì\rРAr#e{6EEEϧNťqukj*TZ 0m۶իu_?rADU4B0 ]BڶmWyw?3z%??޼yÎa02vXz=!!!4OR֚f!dQXX&kРAxx8<2g;aQ0LNN˩SEjM2ؿ?}TTTu PyyСC^zu;;;xӦM?Arqjp+??Օե ++mHYYY~f͚.j!xHf&::j "z1 /N ӻݻw0U!$TUUޞ3//qwa"DBAP2i$xyBBVRR=Ďf```p9R`*q= %ر֭[bBHF ym[X[[6 0iii4W_q,:;;áw8Э[7{{{mv777OxpOן7o ۷c[k@Bv&68ӠA8"d4ǯ_~ nJSLYz5ۺ7|Cׯ;w_|kbbr⪫c Դ֢E;vpEE߿?m4z444ci$;;VGl9#!dذa P;a5!p۷\-!.۷oӎ5x9H7lmm9Fr8ݵTo6nH uuuϞ=;tp T4ggUV 0&%%Uw:^LǓL&I"hàKGӳ@DD=7joo ДkԨQp(3iG˖-?S>s ̙3aSaͱ`Τ}ӪpQ&޽{޽p(..+hB80/F>o`N#!dȐ!T)lA߾}: ٳg0@$deeU4i .@^` :O>V?Yp!߫#LTdd${|ujxĈ8y?~>uAn@;bӬY'Ұ./U+4oo)SpBB=ۺu"x\6ΝˏpBQ@S ȇ8:::r"q-S___zm  $`{̙3|SSt6mYݵ4To@Դk.00PdTɴu%e- ~CeԨQ{ݻw/ٰ伖ciiٷo_Zh@Ah!p{!Æ$aM貊aSĎ մcXE:uĹ]LXryٳgϞ>}cE1 SYYy ;y$ޠA1X,,,|***UVޣGׯL&S䎝Va{hM6eeݻỵGdP܎2LE_JU?JI*-DUV`H֭c-[)--=y$4ˎ9zT# L l;::ck׮SNeeݻw[[!u5--S?X6nC_QL4[~-ME tH>|Ν; !f"VC y=,.lҿj|vvv}ـz衯ollo 쮥zKgS^^ 8|pjRəf``ЧOu==N:уҵ6tH888UBHYYYFFQ %vO?ы 2BA>b4%"L4ζ={أGBHJJ L`,++ XXX@4kL.;v:t(%%E1+++_36X LMZti&L}3|#---Օ]/+++)))**|(U ⫟Z4 !lM4?eQ"wNjDWWח' ]s١C~7úKշkN],1]\EEųg81bMkҤ޽{EzM6۶mxNP4[5vjCR FXp ,xyyPŋ'LPUUj`?4.Ydɒ%5j/Gq]KC&==vuPpV$gZ ڹw?P* Ǐ7o^uuǏ6cǨvGGGDAQMB<== O`%;!m\@ՊhrX,?C9|Ν̙օ]>R… VTQQ졉P'P#GÇ7opuu廽'hl!jCYrvqk,DN=%d3 :wW\5j0;"O҂T{1]WJTt!~wL{)HJŤ&`VVǏfkv DcY tY ]]]UVgΜ={vvr6՛t6lHrUA2#9>Z[wNHYYӝ83JR]-[©g"DAADChĉ9fi{lӪl@"wSC0allգGkk]:88 ի`66&;;Ԓ4;6jԈ}~MѦꈯ~up4B>}ڴiŋǏO2沣G\K2Y`%n5Ҁ^N.[ܾ}{U~NB'O/<>; @_QuF!(?R[T1ҥKTEqttlѢś7o,P8EWWw…Ϗ;{lLLLRRRuu5[ZZbffA-d z'iuB~pֆ鐺YHmmm;u:Y]ՅmG߼yCo AQ/`B&MDgOc?n݊p"m)RLFƇβeĬ3s=ԤkN %CD&Raaa'O#Z83ojI.w&PzfTi'Oܻw/3)4(Kƍ ;Swj~T"! > bccP7`d2///*ݾx"r򠧧E_ݻCm۶qoޖ.LF$ɓ2$N{C*yfvtt4PV]ŸqfϞ]UUnݺɵ,  Ѡ !!ѱk׮4ӎ޽{|R:W^ʢax4/DŽ Ν˙(zϟ?+ cǎ;99-Z͛p͛gI >]RO(sє\ҸqcP{(lrrrF9rHjcX"_]"!>P@0**ĉA&M RJbb\d I-ՠ^?Uio޼nBo~upp_irZvsQbPV;u6yjEEŕ+WM3twIe͚5sss~gaai*++z&M|||áJ'''sg֭4 SU[[[]~=***fϞMSKzݝ[ZZy_\DV:CZv} Jzq̘1J9@\\\hajV+,,dmܸq**5-88Ԃnݺ`hǎSJ|!^z@yyݻ9w߾} 4aXMNX͐J䍪+VcxCΝiWAE;v12s4C ܛCCCwŏIjjjj5^ҡڻw/?srr'&a*f\G6m::::¾_w:F1G lQiiӧ97o j-DkMUVVR1xzb6!ѣdzg4A YX6mxzzpcƌ{qww0a=u|t__ߎ;}6>>4ٳٯtaNѡCGƲg5ꑜooop%Q[n=h I8A::4`kkkF7o^~ڶm{Ν؈:Pl|Wmf}7{}qMMM``ٳgLMM3228EӧO}~GGI&߿rqww#Ft۶m ӓ^DV?a@%55u:u5kdϏ}iӦ~Ro?~p[[[իW9w6m!dݻw)--;vɓ܌.^Ԗý{!|ŋ;w<|BȺu뢢߽{7tЙ3gS__Ν;ǎe[ jAiӦp}?W_}EyݴiΟ?*Ϛ5kN8QUUÇO8յرc/_qF%lKPQR͛7{1dȐx{>|8[ `B k.e~XQ/h_|ٺï^b=tիqMM (^)bĈr_Tw-[0/^D>F\-va.pСCmS>]+Bf(`׮]@ѣFC>2!SA2Nd`{.|)8GIII5A5]VVV^^N=S. rR"77777Wl.T!|||yM÷n:>>~}BȰasիW\Ȉ}aÆK, eWMr_%Qdd$ԉ_=k,Xdt>c/O?422sݻw||Wv7og=|+"WW۷oU377_till,6mgɒ%|Hݻw;w.\aܰ@~4klɒ%j)\a'! "CL& N7WbBnd?2w .]CL V/r劍 VF~vnT9r 8Md2ݻAJp|v: |_* .\xM#e EE +zR͖FT˗/x-&&mJvÆ aکS'ΐ4`H\AD&?Qt xŊ`ݵjlXZZ^re4ϝ;Uf`ggW=q?VM$t׊ >`oo7}ŋs䜗}pWWWGW^lAQ * ȇLk'С駟2"^~3[[['''GGGE)ENrrruuuv휝UOuVzzzӦM ֤Izڵk999e޾}aƍ_zell,˯_KM54n:wL>痙y̙L]]]{{={2)333)))))y666#F]YYyĢ;vYK2JU?E0-qS|F喺`QY^ff&4h51gΜM6i8kQM\݀A4GiNX ا!uGU–  LWU*`yd2]D)!" 성OCihgKDAєJAA۴is1=nݺeii VF!?~HF ;aӐ ZA_4%:wǮaúwﮡg;Ɩd6c d> -AA.T !mڴٸqcGƷ~Vߩ d> -AA:FS'ڴi{+++GGVGwބCCC{{{ooo__:#FP?|oXnh: 6 +4С !VVV-l /uAAAA"    dP    h5(BAAAA`!    Z AAAAX    V,AAAADA    ՠ AAAAjP    h5(BAAAA`!    Z AAAAX    V,AAAADA    ՠ AAAAjP    h5(BAAAA`!    Z AAAAX    V,AAAADA    ՠ AAAAjP    h5(BAAAA`!    Z AAAAX    V,AAAADA    4Ko޼3 zxx߽{!,"wrcǎy2vXssNN}RQQqرԧOKKKϜ90 !O>N:USSCquum׮qh޼y !/^ͅcƌiР.1;;;>>uppႂh4hP&MMRgߏ?& 4cǎ"sNllÇ򼽽}}}5FLJvuFYYљ6mn$a'OVUUBmmm"WVV:u^t8[#1BéV Ü8qZ&1R\\|YBHƍ?Z_RQQqiad2٨QԘ3gfgg=:((t.aii٫W/yҥK4lcc$7Zff7hͭ"99kbb2dTL&S޽;~ӧ333 7onnn޾}aÆ >̌*K3g΄[ZZn۶M&*G֭[iii[[[GGG ^Y4HpQV?^Zb:u*7$$DٟF>bRRRhhӦM}򋨨NK}=zÇ#gggC-[pW.]wv҅}ٳ~ŋُ,Xn]x*%k.lڴ=Wc=R/H(ď ZQVVXlp/^p3`&ӫf޿_F/*[3E 5&?xbyyyÆ Y~=$CQEAG^v-B)E,[ bѣR$eee?d Νo, hX|,Q᩠ u}~5Hp^+K\fAT#ja?唖ݽwޯ^ \~wwwwww~IӿrDMF 5Α#G %l_Ë/ϟ_YYYi>gT~OOO}}}[ϗԹs |Bh@&yyyߢ̜9So߾j*622?k=^_dff^B ZUUզM:ut-u ! .|^\n ѐ C׮]f͚}'-[Dz QQQq.** |hk߾ի4`mm?i7n,y˗/iɉN:}Ƿo.x֭GIM`nܸQ]]M_~gk`}@`WG3*d{j+`ɒ%ʀΟ?Oaʕ+|UR$TUUp#B͛7',]4222))o>΃UUU#f8˯m :pGnnn]vСCzzzJJիWAh3hРX{{{zE%e"##۽{EDu@!˗/_ο޴iӂBȄ mۦF^ӧ/mfl6nBΝ??ȑ#EsD (ݬY-[obA>>ڴiZjwڵvK.uB !Ν KKK9ۿ=˙W]J! ;w YL+ 6q0 aÆ_T$)Si0C 9wܾ}uҥ4,--:FA$*jҦMM*R*> ?e|Ŋ B͛]\\6mJl,[Pγ "{VXAM4 Uyrtiu_zEd_u|||-Hu%㫯"0 ?uLh(i"T 0/_~NNMϞ=mll۷oߺu¢o߾O~>{,%%%---??O>ҥKΝ$PXXHOnߓMn.VUUQ걥"**NNNrI{At=hnР0 RԽ<Ņepi="`.5CWfddXXXvҥV4"GȨޖ+~?A|HɉhРAݻw^k.qX(3o޼INNNJJz -;+5]T2 ]SEE02Tܺu33quGDDB***8<?YaǏsKQ}}}'OB͛~BHuuu`` ڴi(hi6kȨ)))2֓_k֬/bmm-uժU3gΤXRÇ[YYedd\t͛ݻwWkՂ%juu5uQڠATDk.ɍn*H;G' /Iypp@47n5̮\"ٜ#Fp 4;vիӧ;}}͛7s";ޝ?>U>0 c˶(I{r aÆeffr"ӻ2a7BӚ4i%]r^~@m۶P۫4!QFK,J98rܮW^lÓ޽ lsN8.|Aȑ#߼yR ]v[@?#2DV˗/!LVRR"*z!To1be˖At1.\H#x{{ߟ5*}?ӓhʔ)M9x GaaaqƧO6jӓFT+ʦ900F޽ۯ_?ΜYfr#]]]޽+ށ[لw}ǎ&aAZ%нǕålVJѣGiOOcrfffRTR%jlUpg{ #ttt&MTUU%4֋B#EAA?@Tq+077[nȶmy9\bT̘19ؚW^ē[=rvvM6q^R~===`>y$0?3\7nT|},55l޽$p5"p=e1)F`X/`_Wph~%MQ)9^Ez!jV޽#hܸ1g4r;v옚*2[ &L`χ`&-ׯ*{fNt|~~~$666dK{r_ԨQ#GaHǡbV.]be555_E|\]]i@KXYf111&[D7_?8'jƍW)[OpnQ6޿/C#Z|9䭟:t/kՐ+<<\n޽{wlll3gFgggSJ5"a$___E{S 6x"n߾Hʊk`% =Jep}q t ЇKVÇ-;;;L&[x1'yGZ}62J?Hn|?[5zhE) Yz`)nܸ\ U.*z捓߰a./^0dMgΜb[%_nΝ;ukkk1--- ް|r سgiժU|||8x`zDSGGǧG׮]yI^h̛7ӧCCáCzyyYZZ>{lt;reZ100LMM=w]ĨԦM^h IDAT6l@:t7n˫W"##+++O޻wo&{L&kٲeǎCBBJJJz= ܜ}:88?a1cƴn:%%%&&V\o ,^ 68p`.]Ϟ=[PPÇ6l{QQQBB®]O*^N!FFF&Lڵ˗/Ϟ=KkKVV_|*R(*$3Ν;!JUL޲e m}cǎѩU+M{nN>]RRYqݸq#==bdd4jԨ͛7k4=;wn_EEEϧj_}Kƍ322/IZu={|itttiiS)Qc#fsQIIIPPǏ!ׯ=<<@޾_~FFFqqq7nq;;;'&&RmVZB z͊Z냊pw<^>\CBTe&J#GlڴiTTԵkרcʕ b?RYȨOR_*ɓ-[XYY=z(<>>}533KHH8{,K_^tQ7o 8Ν;Ly/RQdUf2ݣEd2뜝]֭u\\7|Ր!C_zlŋleC$mǏj6@sN33Z*E9BqeG{r _\l>T]OO۷7oѣ,rQJ\6Һ?JZ"wOGO`B4irU]]; < ÀsssΞa[rN:ի\W!iӦϟ?gUy*??NE:99q}> 3vXR#Geee[0ٳ'!|())a$IOa`N0Ҫ 00}NO`-^>A>>> lXC'( 6uttn޼ɾUSSOvyھ~nݹs4}KF$43=_QTVV6LnAS%|֭cܮM Ĺ%a葖']B#g@Z!J(8Ea7ja)M'KFF5\i+>@ `&M[&MbPFN`QF &ɶoξUTTԳgO>]8mooIڱc 3a֭[GaPbUUw?q0`) 5T0mL6MҊje̘1=s eOdUUb*ua777ʕ#.Q';\ٚKFZǕVGIKdϑT!n;.{?wxyO?D NII=s ղeK+,,TVV(س%<{ .׮] {.Ȟ߻w 6\\\! 7oN#P )]]ǏsȀ/)S0R ,===3JYYG=E LB~2}Ҫ K)4!bR)PSSTU,0ʷW0 555p: 99ݻwaf mDHH3.(烨N/RQ%m葖`%9z6EH+Di`9;;s!H%gi#[AA— #Feee^EPo;\T螺JVNNH^͛077TtǏO/>]T$z̗ttte`}g)g)=X^QQ  9vaѮ];EfjV4lY | [#U &l9KYٳi!C(hn`qdpͥl#J+%~H@ f˖-9---a^ m777%PEu`2dwn:}0a碅lu} O'3sLhG/a0,.j*&MiCuB <Ү];aMc>FFF4phZӦMS굊9s& `֬Y4 V0i) իʇ U #DFFׯ_V]aӾ~5j޽{ gddd/FGG_~ކ888k56" ifӮ];0 Z?~?]hU?ZԡGŬP;]ZơSZ!>Xn}E"h'FF5\_: .ۂ 3FIvL 4klĉ4 9˗/O۾}) "M=>}NFݼ<`+rpVIѬb41ЀV;r%\8\jɚK.u\i}D j#ϕ9jH kVZM8СC3L&k޼ykuT/8!OKxa'O+0Y`jj *rO?bkk+D@On|o}Ri wyʕW\i9JIqww{t%޽{G`S'HヒDE<==UZZJVP(ȑ#ǭԩSNJmN=|ܹs;vG]CBTq&о}{CCCi*POYӥj [.2>ioР5 k1KLUTT<{HCsϾ'Nܾ}Q1"!dTVNXyyy`N]B:vرc'O0 s?^押Qf͚V+믣GwbP%*5'Rg NܨWV޾}[SS]DUK-YsJ+mz#!Z}K亨|8/_9r$Xh<|pOl';T)֭[+J&妖}Ӂ={<`k)7κ؈M3v;:],H%=*fNݻ>G]CBT}& [pĪ"Q]-WAA|Uz4d2hz,T^!\sg*++O^]]-Y @):-6KMM͍prrbDŽCX0ױ9t]\\@fʔ)O¨^4JM kY;NE+J5Y"5\jɚOWZi%->GBH hݻw{\''SNx"222***66}l511qW^+EzV }.}$4ՇO|w‘uuu5jBuplXWWw…Ϗ;{lLLLRRLJKKCBBا0Ȟa(Me*VpTmO>mڴyEyyǧLGVt: 8ֶSN...<" {e^RR"wɤ㵺i H_`qCY,@|Ts2tJ(Dgr8Jz,>22>(ou&fff,j]~L9@ JIm߾=w`bbrر ./ 6p| ҃t/J.^^^2n!ǃ9vС[n%\p 9߷o_.]!$==믿?D~NOJΟ?OϤ' !]vU1 lZKYsqJ+%H ѱw!sj|y6mfΜIM޺u;vJn݊~R")2RSUwFF\s Z8:J:::˖-g[9̔Tk6*BOOˋA߽{G *VضmD ޣGujbhhȗ`SRmd2___jZ;,,l`0޼y3 ;::FDDp "!!HuttL!M 99YGUlD,[[[jɝ iCO]fEǪ8tJ(DgO<)++[t^di#-44i477}o޼)9\-N/hYXX4lPCmdd;,,6K;VY64mڔm޼3@xyyUVVyfΝ%cfԩy|riiӦ(jkΝÇ9uAdɒ5ѣ#GjݻwޥK`AsMPqԶ5_J+%HVjRRRs ,?iL[''Eݼy46KEEI]voӉ|.\իW!j`԰'jlΝ;G3yʕ-[H(J\ M>lܤIpdgo˱+qӟPIOi}UB@0**ĉtץI&l۷A,,,_ .]\ׇQr_vlOЈԘf@{ILL(33S]$ =j ;.Hs$ &@BTq&P]]-׫Znn.XơO62~z'8˗/ viN ׯS+|҂Pq7oޜp566}ii3<. /sGvI-=KҀ5X= 64ҰI caGK}On߾g˖-{쉋֭[llܹsAb84PY M KWZi%->GB>x?sI[l ݿ}T 6Kv֭RB#Ro=IyyΝ;6l ֲ\$ =j ;.Hs$ Vvi&jJGGjwe,adTc%頠I#³f͒+x%\vnj\ dffeܸq4TܣGٵko Tra00OHu&T?x~ CCC٦|&L0vX5Z|rH/,!''0ݵ^T(G|)j-j.aͥ:>JB"}|$s3 ̘1zEQ_BBXjݻzk@䊊 __Z[d`ViĈ<E999&MXٹs',XmV4*կ8 >v|Jyy9s4&xS^b 7mݻw'NT\>ٺu+Yasuu9)֯_϶iuZ Ə϶P\\,d~)MWE}.T(999^^^< /^`gΜ7o?"Y&Yr%Gtm۶GrV(*v\rH:!U wf_Xn Sfd Hr 4;i8::{ 0c s2LźAQ3nnn s9#Gˤ-Z̚5^޼y3؝ok5/a&CE)|AE ***>3ȷ+Wqlݺ|#9rZŪo:u2e ̘1clٳ/\1wa{oz1tPwwwwww薵5ڰRHJ( 9BފQ`H888X CBBF#v킋lGm۶]lفBCC]?xt9 |7'NHJJڷoߴi`Ա*))P_]]q}7Fӌ@jj*gTk۶mPPЖ-[.]^')=z]6<<|'O+fff߿G؆?敃`aa1cƌ_^///g.۾}Ç׮]68}t`WҵkץKZmڴawuϞ=111+Vݻ7ںu~W3g΄oGƍnjO?͟?D鄐 R'ۚ!CΝ+K* e\\a}al[[loAlzMVTT#в`xE"[[[m_EEE`?{>9* (""bA }P]ccKЈXP"G1ݲ7wu>̳3ۺuرc׮]Oe!++'N0W\\\ H!y;@VL{#GL2㚗J1p@-UP& Q&K$ ~./oo5klٲ%00Ȉ}d2y&5r1YIQ(B~*ZpF4 l?A~ҤI&Lԩok/iD0PΞ=ѣaaa/ԩdÆ ijkc60 #Fٯŋl${L [l #k$I^^?DիZ3&M|1D$*00P_pȈ_СCy1sy9%Ĕی܅ A)*f:s4:Z$A&}Tdggd_An!,ئ>\./H _EfҥK’NpBeg+(4` X]\\D˗`Uu\\;*tnݽ{Wh[n& XI,\a'IKKSg]]ݯ _+4`>>&MX@>9#'OXo|\,aԩS9w7` })ۿ? Eu>F_׀U[{3A4$ɮ]Μ9н###Ǐ/2 :wܰ0\Xa$ 9~![[7nuww-- #G+[jÇ9k >6mtRUO1'$u+L"&T*T Dxw@̯s \|dekʔ);w/tpp6mڒ%K<"" #G|2x` 2L Ν{e~w`kk s{/B ]YU @ĨwC x###'yٝ9sCP?dL3j"N P!9[fqݻ7|pNtСCǏWhW"$85kVtt-qxxƍ9e޽{7<d#r {' \1cZ~j q#""8;R-FLLL._7?>͛7_`?޽3fΝ;R`g}F0NCNQj9A#c%HM|})#yMD:l =xzzz}e/Oyy;w_5iKY|Q\\\uu@u)55!痕u…,TУG'2322윝tΡr<111??аC/YYY͚5vq֭$ccc777uFgee]v-66TWWӫW/8xϛ7obcc322LMMH<{ڵkfff={tvvI `H,,,|qYYY-:tp wd2KKKwww8T ϟ?722=?cHHBϏ 5"gd2ٽ{ <==k4UַĊKUufSK:ñq>%%ŋIII_n۶C>}#0}=Z.;hp&???""ŋ2E-[twwX5SYY^x~Xﯛ HVVV2˗B}OFDDdff啕iֶo߾x7nܸ'O4jƍT[***ZnkD;ڇfKTs.FZBKV(LɑH$?8D!l!0~aLMgˠҀ1Ej* )j׮;Bhر phhƍ:J=1`wv(/q޼yOw͜9sƍAEb֩Q(:~ X) B`ΝÇ>|#޾}… Zᙧ B|X,^oٹs'=n011_4eBm޼!VFP>eBP>~ydzgϖ,YRQQ[n"ZjaaP( E!m۶]t)Bɓ'ǏPT&"""&&!M C( gРAp۶m;h &Mxzz}!$H8 BP(P5itǪUBk֬P(5`Q(Ǐ˗ᄵ+Wܾ},"##o޼LMM;; @~(E}|||w!OEP>qZnrĉ>~8//aFkn̘1SN(رcBm۶P(wtCC Erw}gllRSSB۷wqq L&;z(0ZZZSNޅ0 s왢k׮ĉ!SSS??Z~}炢)))>׃ jܸhׯ_S#FU-""!ccp;;;1r^ZXXhbb¾[VVvر䴴t\nmmmmm8q&M(|&_!0 si\duؼ~׶ eeea!wwr͖!TQQqs=x 77W&5mڴUV־j(矴4۷oӑ Q5Bbb=zѣGvͤ$FHQPVTT矩O>ڻwNY蘜B0ʇ@hh('##Zaaa}g!Ϙduֵ_m18UUUlw!W^UDGGqtt~:|xpNn߾ 5kwaaaVVVFFF!!!999ByyyF Zxyy)xbvj Ü]CTAm6hPGAk0|,'(ϟ?޽;[K3@.9992W,pH|1PzիGΝ;=<<<<<س&B__Ґď",$: {s]X"PۧOS999+VȆ\.5k˼7mԼysgee)K͛70@WWwÇ y-eO,\.;wرc>6n܈uttMeeeUZZѣ#GǏ?w\[꼽/]b&::LJ "X^TVV޽{_`)5BhΜ9v킟;v޽{vrrr>WR0 o8|RJ &&(Bh֬Y}qkdïO5^?tx&[n EojUYg6`P( S@[[O>7B(>>ǀ N40`I~!4ie˖#fϞ}5~-Zþ(EEEun <!4cƌ@Lϟ>|p9W&C"""235R{Z"] !tcǎ͘1/ٸqԩS^,[l!KKڈM6ݱcGfB*ރMVP(bR( *XO}0 ^F.gg777pgׯc-Z[|tR[[[1}v ٳsqll@a1]I6I'ija9s`s6nȱ^!&Mt Xqv;ֽzի/̪oB[ߜP(چw 僣W`za]]]$a۷o绺 븸7ovҥK.FFF8yyy޽cAӦMdiiiϞ=kܸqΝ;wܦMN4aV[---[SZZsLa T*Fݺu+33ͭs8MUU^rJZZZIIpT&''Z[[E/_￙n׮]YY8R)..~%Bܜ?.WHFFF|||zzzAAA:wܩS'ŋ_q^^T89nѢ&ehhhooAv Hᓛݭ[nݺ 7PYYك*++-,,zߗ7_yξ{nqqqVޫo>T-baza\\\JJ.):" ԔH$Z!d:,BRRRŸiӦrƍ$00LˑϾ{<733 """&NȎ ~ Bh׮]%%%Ç)&&foau:t"t8bw)ݻw+**8GD]x_,Zhݺu <{,--Ύ 6]zyyiGsO_ wРAؾvر5kABeee|||jjQŬBikkK'>S\\ݐLqqqLLL\\\˖-lmmid䉙c۶mEVBĨ;0~uפdƍ>͵ѣGUA6TOk`<Q8"V!.R}%RTQŁX FJH H6ٜ >(*//G,D"F:sB&R)^ׯ?}QFvvvc?^|'Co٤&%%eddd-[C{' 3/ML)\&Q߽{4k֌*wiiiQQc޽Sb"ݻɓf͚ZY茌/_XZZٹ(N!o޼yeeu8⥥[^^O[XXϪ)arrr~MaԳgLv̄𔢅 Ǐ۷`̘1offvyΣ`XH|1 G!D0֭[aJ~z'''uX^6m_]]۷C4fgrat ѣG#6m?~q/9c󃃃9V```UU?̥nj#0E|DJuPx :ٲe 4ccKlJ͇%^~PG2 s]aò :V%3f ;o<=zt,Y*B+W>{  \.C O͛7h !?p>я}Zuuu=p6W_!ÇJ?O2XRN.?~v۶m)q9)}p&M6Uv>ׯGH$3f]xQ|Tw p~5XQ33h'pjDvdo޼YT*ݵk@ىZ5]E!\BT+6ٜKAэ7փD")--jgg!$VZűݹsa:p26nܸJ^to߾LL4INdS%SJcr5H!\d 5j!r={VѣGnT:y/^p".Xܹ֞9՜P֖ 0z芊 v˜K$Bhpɓ'ܹs9d4m422"1H}aaa8޾k׮ H",Y(-}MMMAcwfv+b{IϷ_|{YWUU_] m-Qf*))B,Y™ 䰇#GT* GOOe@T56RWŏa`eyvssR!-Gb[YY9o<<o8::VUR(<=&ӱd"!4c +}}}uGnܸ1Gu&#G3 2nݺ:::`ڷo"|||Oeߴi; Ɔau>>>h+W%%%0]_`U NŘL fӧOgD(N|Nq-mԨѵk8{.gXZZӧO10A䬆7n7eԨjT[_]LII sDC 3TU#m9"1Џkʠ lݺ8oСC lmmqd((%191SI:u{nQob"ɰHeABeˬ7xR@Gn f̘b ???m}=aOǎ3g{#q/no޼Y8{xM;B~޼yہ+X,`ccXM`cc3w˗Ջ*=|:tu&Mۯ_?vT: $ݻ7o &88888899Yna^?z[>“pD"KիO/P 盙KKK r9٭]611Qe,a`=ѣcǎ_50P쏍E7oTXt ^!((h޼y0g X5*aУGnߟ0޽F?1c[i`DTRw ޽;88Mlʿ[_`+VXx'*AX$9/P!(KUQaH XBN\( $WF#m5 X(##ژ1cϟW9iWIkkYf(<+88xzEEEa=w܎;k8@{)%1BhU0&&JԼyׯ_LmצM,֭[033 ܳgڵk {ĬZ ݷnzܹs;dXhI<<< h>ʕ+qD"ٽ{7Ű_*޿n~Tʡ[Oׯ_Јի]]]a|VaM4y9$Aձ !SS_~3:9\TTT@紐rpҳgOW؞}VGݴ4Ѐu]<>eʔUPPxT pB@@[~bccAqnEܜѣ 9AAAjRV 0̛7o@Mo>11n=}AYiRU~ ӱٙ1 B2f'бK%KE&$Q?~̶90?Ν;!`]v!k׮!^^^ ;np_SN)Kn [nQP06577*Xca***`S{ڌ3p@T-uxx8D{@Ԅ@!pBwM6m,рyfaÆ)|ںu+ŋ! IDAT1`;_u5A\|nUWW獵-1T )//66** 2VtYq]dBf"r$AfG1`1 Ĩ(vxmtLN6 +7ikk~V6m0 /`C;v,&M9r1?C7`._"\t  X$U{ЄjC`.\oh^XyԈ#8(1` 6k GQUr m۶l}5>2L l󦦦иR)WIf XK.IR)X6LYkRU~ б ,X)tֳax,|F#бK%HE,$챬©a-!0Ɓ}>g"())Ӄ!ggڵ^~5j4rHB8tz1@ {Cl-T/ K֭WO?$@gҤI$_};۷ ?bI `iO߾}MLMM,2D0&F dBXYYٽ{wxs8 lHWW;8csyar9TTgϞ-[ X_kxboXC/`:sLs6m:~x|vA.\໅z*>6mjxbd;/ZXX@@n֬YuĀv@ BhȐ!bSSS< ]z8)**M q 禯YL)<::`+ׯ_C |QqwwWceeO|D4H_*<2/{!бSc 6m$,bոp^YYٿwqڵ+^(QYY?p4iiԨllPƍp䯿ZgϞ4hPΝc/q~^z|w߾} Q7FR?}_SիWK!POJŋ,ZHC]ɰbo8::u֭2T*U+pٲe5ƦԝHZhzcii vFh7@1ѣݻw?~ڵKCuk:\|Dj|>EʨN?443`$#:tС~ljBd ѣGAZ&L8.#z C/H$8tÇa}PRRT_>ual`|ݺu㜼𥧧WWWg t;SNW6k֬Yf zzz`fӱcG|ad YG4hP+Wlr'N;V$ Xe 9̙3~Y|6kN!? gIQj*  lnn3GJ}uCyy5uC&ZyDX ^LMMQPCcŧR_:999dZ"EAW`H$|`8yɓ'id2٣G"""3ae~ PJdcr5TN:Bl;~8K.$**ŋ/^M&O5GQ8 @CP1;v?l$IMM]tҥK 3bĈqƱ,IvPcbb"b;t$p~$2!$GfPǎ_{i۶^SpIX P8o JKKGQGrep|UtThU Ϟ=~NE52Ѫ%#Rfggk1UsFWCcŧjP:qM???0x{{!C?˝޿ Elٲ寿{lPbpppppp3gB(33… '|,))9rӧO9gr+c. Xo߾`B}۷ 0.]8q"͕%F|'UgffG7n.^}T|w4+S!+++n@YWPaeEP۶mo;1(;Ό? xo 5H&&&-qH֭OڭKDRTr#I5!W PfnڴJGEEeff֒}@)"gϲ\zB{zQ違{9qD||<|8gGo+O/._?nænHњ aj*u$GaLLJ;:ٳgp-52Ҹcªc.u,?Uұ ӁmA <Oݹs c277Թsgpqh۶mPPХKbcc 7E>2U‚}b-[A|5R_{>uKd;S(t<6?'C*.X`޼yQQQ/^%eeefff`@0 'v՘ o`㪫ggsνxW\~z^^ܽwg}v-$qΝÝ;w.00ܹsX5J89 ‘R)-(Aᣧ%\T5WY__QQucȚhՆcB߿WAa}.u,?UCӱ`͛7?n8؛B]vFDD}nZ;޽{7l0<޶mçk׮+,>K$7o逡Cڵ !teL^T^@]t 0J8;d5L6'Խԛ+D1Rpp0{SΞ=;rH,=$* , y6F*jG ua h9K:-,,^ ߿?ni޽rʉ'?,r |LPp!!_)[I$ ^Y/UW#[/քkϞ=8 7oެ BI&}> ?Ƃ"m--+VbДҲҾ}H%:%++K}~S%k>jVɏ̴aF666_OmX0`mB<С~5d0`akL9FFFx{)BgϞ:::!4k%..!lB_޷o8}и޽!^w+V|WܻwOd{RGY1pe8''ORwS95;g7͘1CؽCDEaҬ7jo:P\ 琓}v|ݥKSNqݻ#>ŏPG&LOWbbb֮]l253ƮϟcWeܹwb۶mz%Hlq|B;s玲w uEEESN2ecbb{իW㐸8}m䯢R]]|wَ= tNNNxeU];vlʔ)SL8;;/^s w'xҥW^?XM.+s ]Cd9TWWϙ3g,2jU6-Z iـVɏ2 0n P WGFF X6}śW\ZkR)x ULh*9l۷{;5220wrmmm8//ꫯR_;$w³ɲx$$$(2 PNh7Gcׯ_?Ӡ:XT, yA#mK#4NÃ``xx8?p0(%1#uᅦɪUV<70bۿÇ>ۈ־⋹srWEEE⏾i@,|QTT^ dee)9/>p+W`_\.ǪAe5uϞ=m۶϶ZZZE׮]c),,1jɓ'>| w7o;V"]d [ZZ}W͛7#TTT̜9&/ҥK >|xǎ>z('Y*eƫC[enn#G8d͇@L~ϰÇ񅹹DB ? |K̨ +>UѱlLLLR? e2l(Ӹ !/cBXO&ݺuSGbFB ,5k׮=v@UV%F;(}O !:u=!!!*=ԝlo.:c jk׮Ph_p+hh Aq!ohv41Rsp0f/MR"A,Vݻw_hс R>|q!:/Џ?utt  5pԩ5/˗/|(((>|8~g͛7g ޡ*f1.k.{a9氋uEEEٳbRݻSNmڴ _{xx-..=z4_TT戫Fn, =#w@rr2{ 7_;w_L0A3W%K? 9stHsŎ1cg;w/^$Y*eO*BCjWU!?ݻwǏ练!k>Ug#,E a߾}0 р:xNKKça***ƍW+>Uѱ8===v:4{{{ߴĄBϝ;DE="8(\t0 Xg2lĉiprrAGz۷/իWsf2J(UիW߾}… a(BϏIO2uGf뇬7C!#;v 7nȑdFFF8pΜ9ASUUAMɁLT, 91@͎&\S{ao7'%%bR" ,{dB(&Lkkk߿_*w̙8Ç\ o8+W3ւv}: oDۥEaa!?$ߺukLLLiٳ=xN:7ly|1cƏ?(=Ww''޼y322[v$cƌp;;ٳg2b׬Ye˖@X"kdd ` M6ӧO߱cDzeتjT1azzŠS!C̝;7%%EbKKKa vHHș3gbcc92uT@m۶T㯺TP|700%vaX,6m@U%ƆJakӦ͊+?~ᠠ Оz K^% ^G->׳o*~vSurrZlYxx5k8-K}C ZGd:a޽ N<{nQTXIHHI?<$$dĈ#00t,Y*2!Qؐ 9YʇϳWgkٳ J=y#N"wߎ9ȑ# (r9#DWKgg'.Y$00[n7m.2?=..)Sv}ȑ)Sp߿RM<rʔ8eJ={6d:}J#Sw;vGYUVvU(S5YVVLnҤINNp^ ~䠬8ă.Q!"!'.AfG ќxP x9]Imt™<}4effOwỽ{!?v֭[ǎ?vgɓ'ҥKy@ikkGGGr9vWTʺw*{wnݺ0*9\.IGKK_g 'x5`0/^|0hUE@(XǾ|Rػu\\iXH~.\,7BxK! XdBnڴIYL!ӧN/*rM&mmmVkԔ0 &@LgggEV8硡5ZiӦ֭[-b Xʔ X 9r?nj9R9sFL;2VEu jTr9$E. #k2DE> ׆KY+ё6C4ҸN_,ano߿ "w un)%19WI]MCiիWaӮ]"N6f{☭[BRug_|zBc8---[ g7k֬h8166߸q#4gVDO4I]CCmdE޸qmŸGFFB}W{{HPvvvgΜWV*]|‚U~5M6]tJq4࡙͛79_k|}$ ~OQ&Tnnn<'177_l 2ΉJC$ɮ]Μ9(GFF?=TU*+rH ~!KKh>wܰ0 4yT-ͣ bjժ͛7CCCVaݻmc]v՜بQKKU5"Ȅg!@ΥRJGkԮY% IDATB͚5 IIIᏆtL은oAUƍcccGbjjO#*ܹs/_Obkk YegCY)k=e&Nܾ?vLِ; X?5F {:~ ~0`ԩuZYH$hb\C{F>KU!'.5V-W#mD4"%"kΤȴSLٹs'5mڴ%K(iA@S%PJdcrrAm{Lᐊ ٚ4i/]ݺurܹs!iϟկx6DCF͛#!ʇ'$$t^rybbb~~aDn]Zkeeվ}^zq8yMlllFF=8@)))/^LJJzu۶m_3ܩSTPXX_VVօ RC=O ),,|qYYY-:t fQ^^~ΝׯcWyM4Rw]qƯ^U*zo֬_|CFYYك߿giiٷo_eU_!?LFFƭ[=<Lǒ HJJzqZZZzzzYYY6mWG#o߾MLLLJJJIIkٲ%>xQNA&ݻw/::ӓ,2%@LNNNiii6mp{dI&޽C|棪#6l aU1Bѣ&MZd 8TV8d>ڨ@J9WQf˻~Çe2;{9azzz\\T*uqqak~(Ȕ\5jVe 2))޽{۷ԩ2s͛7={GÆ ۥo͵UNND"155R.,0=)cߌc3Mc}P 8ի9sl۶CP2KB*尴gРA;IIIxEVAAMXӧ8p… m>tS(5`51"@JJ ^I$8BP(۷o8ٛ&W\cl*BNP(S|$$$yѣGǧTS( BcZl V^>⭰p̙'O?y|svd>VS(X?MH$K,P( B%ΝW={յk׮M4yǏaOA޽g̘Q٤hT7q>RP!P(5,7|^߹P( yMl2ar9Ws')pr BQjEǎj۶T>>>tLxx}}}ѣG7sDPd:,B,YuȬ,Leffֳgπq}4gtR>YS(:S) BP( a޽{gddpP4 r !qBuP6m>mEEE'N@aD2bz?8p`#7< ۻ?瓪@Cnn B=฾sA"T) EUjkVxx8>Aw7n ZC%!![@Znsٮ];| KKzۘ1cN>ڿiӄ#7< |ͦMBǎ#~'Ui ( BP( 哢~W`]ֵΝ;=<<<<}pM6zjgSXX`&M+s(0EFFjՊ@"NP|700sҥK}g`ҤIYYYu\i"%GTyߕJ111vX\]]$&vfaÆq^40| rgϞ_ BP( ѐ#+TcSccc{?Rݻw_~%'tԩ'Nzy``ҥK+...wUU;ԩS=$ bZg^ľVI'+ibbիWe2;<&&_~0?Ϝ^vG\.g'|#_.޽{ׯ_~7))iԩ6mR5ۻw?~<0WWWkUÇ,eeeG;JQr*RݬYm۶ANZYY KKKMw1.TMs?Q޾}ˏvGG]a/^nQ( BP( -!WW!CĜ9sϢ3j(t1|mkk;jԨVZGFFx!zܽ{G <O򵴴|}}w^XXxׯ'O3**oɒ%[l׍50`@Ν={v˜1=eʔϟ_r%)) !dcc3rHPNKۻc``Mb&ۻի ӧy񵻻{@@MIIItt#Gpqɓ'^xx8BHOO}MMMƦH'ϛ7[fϞڸq_×f͚۷̕V*'NTMs۶mO??sWWW^EEE?xڴi۷ ( BP( RBD"ٰaCuu5ܽxbF]{{J~]4eʔHRPPoIҗ/_ɆNbnn~5G& 捡!o߾}bb"z)a{OSFFxƒq?Jw,\]6mJpWBey n!_r?ۭ[4eIu^^Rr9gR3Iu49kݰ01c@8 fԨQ?fBP( B|R|[B&LX`%А!C֮]|=oqD k9___|QXXx| S뀀[[[N+++pM YIO>ϹJ/?vѢESʊ2_C$Ɂ:t>}po{ݻ_| 9s&NII~ˆ#:t!\&33 W4Ȥ!4zI&w:88<~ܵkIu47ԃ³GK=|s@6 BP( Bm۶-[ O,>}4BN<[n0jx§7k Nဳc*|A`1 d%B]t'ر#1}}vEIG`x={D!nA6@чGGG3L/JS n}?^,q2DKuEEE47a` &( BP( RgԺZ-+++| Xy#-LVZZ sīlϞ=ڶm+mbJ peyӠy yUVVٳĉةnlTzz:^RFrr룢233UT+M!R :thP֭[Ϛ-ϟ?斝 "׾}{e,,,߀%BP( BP(uCΕ-HuOCl?5*BLMMT?olLLL4K.͜9Sa6mػ%edYf9rZSyT)Lt޽qp8TMsqLVE #!) BP( RԺ mRHnn.h׮ߙ~+XTjll,AOOOOOE===ؿEEE(M^^j7[ycՏ=}=ihhؿݻ8999::<ڵkO.((PBTWלTSUO([݀rދ!dcc3f _+:::@1___r!;ʈHtȮ=vvvW^ErSl*+@O?2ΕkiiᗳŒr˪={ఋɓ'q;$L}kՄ3gH:\zѪ5RZZjee%(h6S޽+uSMMMJJ 3 Xb8''Gjn/^ ޶mܿ&feeIMӳvZa^^B‚,l<Ի._,9 ГnݺU[[+yKYY=΍7p`ɒ%֭cXe\pI, rssVPђf "VUcjkkVVV? U`[f풕[jj*QS`BnB}ǒD!ccӧ#뇯ܹSHFFFFFFVTT_WWWtToȽ{FFF>|xРA8 :$xuu)DH޵k׮n%eS98@ߚpIOq/8,BLyy9 K~9~~!J/SL?O8q%SU!ZX0ssW_}UT& XQQQǜ9sf֭8fSSS͛q'+%sڵdϊ+uBE^͛77mڄooƅ wojjZlT"͜r'%m۶1+ǏWqreر8pUFEDDZ$Ñ W 677/]lr;}?CT]]WPPO0 ȑ#'Nŋϛ7O DDDȵkGeffr_@wCmٲWX"Np\9syv@},̚5k?mڴ>} nذ$۸qCbqppp\\Iii_ECp011Biii&L NKK 2B>>>'OD]x+ ѱȑ# ='Ξ={ر|='ݹsX,noox뭷<== _Ii>>>x/]!==ƍC:u*oGG3fDEEUVM:^WWWccclzccc͛W__x5ͻvrssUٕAVѪ?~vZ655%#Gܾ}5kBMMM+Wtބoqׯ7U£Ukyyy-Y$&&!tG-^xO>MNN>z(?ToRwTSPwvvu엙Yrr2tkmmm|Kn:::ǎcU^^NI2w\&$NGbccyݻw B&""BnȒwőaaaRo/mmm$$>>>ϝ;Ǟ!ɍSյ~Wxx8b I?P0#""Nb&LBX[BZ, yX,[C.trr"6VVQQ"بQIm3 ۂ KeeeUUUGGGgggggJ" #QWWWWW6%d- $EvvvNKK͛73N3X[B:&&ʕ+8}ʸ}vqq1ܼN>#dE . 6{lf"p޽ ,^T 7B$((Q\k55ֿ{ڦ "ĉ>˒Ν;ѣGdԍvC@$N:::>LQʕ+Bf,Z0EgXX*DEE'"݋#,XzIb``@ٳg믿#ND `!#""p X^^޻UV,\yi,*$** g_~gd+< D2eKKKss3B 5Ғ=dBK"D+W?ܜ^nMtww#tuu]R[[S[[[,˧-%%bVVVvvv$  Ğ_~ 㔔#F:;;] PS^O<(o߾FQUTTVWWfllLOeWW٧OHP feeظ:ʭJ&:D 7Yhmm.400ʼn999`ݺu\---C`;)R9Cr\JJJrrrƌ4nܸ~1ITD瘚[sssI]DYYYg#p ZZZzxx899/_CnJ@(>|د_?;;_^)))eeex0`psY:1AMᛣNىh,@BYr{"wMűSs$MёKe49FI2}=O:#Y3=K)L})FFF N777g,Hq\R]]]wwwwww}L(ɓ'!ammm YxP) +44% {{{fff)))RﭮgtuuzO!B ~aȔbD BxaI=>lذK-?2e>|S?~LTVVR?Hm9}T 5k֬2H@ (jdؾ};r:u=zT2K|}}%;/z*Qy12GGGKntf^nM>%511!mC%駟J999O!|7O+++X,ydU~ aEEz `QS<:) 6u_G)8((Hɭ)Yjgdd5:u*$o~(р/dt"~l tuRmV% lI,X?R4 <<}&H$ZvΝ",@CV^=rH>1r􄅅ɺ<*~W,ذd["ǎB|7˖-zȈ>!}ޞ07o?KU!Ej읂gCCCimm} Yà?*4t ~ቇbGNtuYvv,%ý'-yE;5;Nx\Uir4ecKQTzz:ÖG"]N"dy$W4ݍ7ĦI7a*EO?o>G677\Ν;${O] &466%$$L4)))eBQ԰a&M/׿HCGa[[:4'''>>!m۶RSSC9^{55cܸqaaammm$˗6 wڅãFZpۓ'OΟ?2yd \? [&N8k,KK?;!^ ZZZ6l؀֬Yֿcǎ]\\wٳGȇ'"իD[[۪U䖡-,,x"6#5%yUVV⟁fffqqq\e˖;w*Y>{!#mllZ[[SRROP?w}̌K,Y~z'O%!qK[[[[[Bmƌ&&&Ot<:) 6ueԩS!+VXYY;w=uTLL rtRWW[nC B<ˎ;H 'N|իWϞ=˥@$lhhHk?oʔ)NNN=kll(NdJjuׯ!BH__̙~~~VVV%%%,((@L>էOF̙3Ç111;lٲ4hМ9sBϞ= #G qfΜ9W\IKKóm۶M:USNCV oQF͞=[__p/\qwwOHH0:::88xƌ8c.K7TD#GĆ{0b%0喼 ;24#FUIϋd̙<{l"`nn>mڴ~%%%TTTTTT<,}LO$(;{lc)">0 X{ ӧK͙;s B"00pĈߏ{ر ,^$V혮s6{`! per~G3>d>|X@277gQXn)뉫m^^,By`RCCj$ +**%bG}t{;޳,XC@QǏ_[[K;**O2119z,Gk6m"7~Kϟ'vލ#IdffӋKKfsNRz]]]f͒ZWJtwhSSǏK===U8x"tQuAm[1`S*$][ELy<}"9o֧~#j@6CH`ܷo;wp,Jb;X`d2'Z1R gbbBw 6F_vb+%. blL/_d~Dr7`Hc<33Sܾے '\йsp$NʂM_Ua$u9sUOOOzB,⸮|2yR>oPXTTĸ,)ieOr~J=''q… k`M%QsmI㮮{=/]#jO(w`>;zhFQzƍ{w~c X'~:ޝH},=QVȊ ȥrMoyΙyYYYI΢ n$ۀ%O$W6]OW۶mC qȬL1`1j*|ˋDSG`6z׀* `aa!,!b0H4s٘{{%K4#Leeerr2w0SQBA/cTn``Գ{=2'{d>ߥ%]!3B~is=tСCWR>|{Ljttt$PWW'Wl%K[YZZ~8LCS$21"8p SFjq/@pCEFFk[Z[[rXܹsb,' I,,,M=>}l߾]2~ʕ"~EUi&v. QG}$5#]Uرo:|0,_֖q7>Ȁku:SA[Ќ3܈,СCb Bٳ{Qwxڌw!yۦMH$tH!S g}y5"c ><\GDvj% K}\I+u#T|.P(_%lٲNDə.W6ٳg*!!H["b1 {O?㏴ H+ܡ{`J<BG〃A{)$g}Y[[+kVRtt4ZWWG&:j6BNjc)ds;;;[>vX޾o!|~%_l-Z bcc$iZbaĈR7f666~WoM]tww|Y "8EoN<Β5#Wgg'9'hѢERkڴiW^f+?UXZZ+W 2d{׬ R'*d/y81t$>};6//wgD!:*QR^cǎeĐǓ䘫BOQ*D*A=QZqR&WhJ3ȓ4QwwwGKKK$23=~]o*ldauuuaa!Poemm׶UVVY[[K)Q:"xjm$iϞ_yY'Icʐ-Z~}OOOQQQvv6>ĉxM[]H~5bfhhH9jPÇ+c$N" rbTr$䡆 Ν;۷oOJJ*--(yMR7n[bwD"QII #յ;''Xb6`ܺuk„ +VP(CӾ6`,i0I: IDATmjJ4޺YtoѣG\NqR+?U3fhiiAuttDEE|cǎ>}%K3[ QR9rHHE!:7-K8Py*rNTU۩'i#Z59IeiQ IB\q,(5\ܡnՒT,F.HBWT+?Uxyy%&&Ι3ux޽={xxxv{.c$,#Q&,U!囇іlc TщT { $M\I+ȓ4{eefz U .1EᕃO(WH|U,O>uj,;ydǫk3AAAXl߾I}x B-ԖE(#݅ٳgd$`g,뛹$}O2nرnnndC(_J RoG cHuqƍTB#Gd\^> $dhh7agggGGӧ3Ve˖4x*bR !ϥB@q,(5Y AI}CO8غ F.zM577tT+?դǏ_r%!!׷nݚ={vjj*TXcҮYz&CmC1Wpi*QqG=QUYifVM$MGٳxHoKGUMwxy D )S:u !t'%%!+kP:1 _^;wB{["up?ѣGYYY===W^yERj|_hLV#?PZZjee%(2(w%y}}}jj*B>>>UUUZ'O2dHOOWR$(_JyBaXv% V,--%7?4য়~ e=dŊ؀lܛohno HůhSdU*ȑ#uttڍ2wTBvv]F.z-++BIE*_*D~U[[[{x_۷o;wnX۷o'''kxfzAGGK q:0*uFždVk0j'ix^~y4f#(9StW{^'X^^~}|ȑ#A6É'[orԧT5 z !wʒgڵxAMjlv )mXL?֔ŋqi3\]nݢFA(++SrueiRSS gi޽+(nΜ9sYv-B(77RGGGK ?KIV"G#;;;s}'͛7e*N8!K0 Edɒu14ͲڶB8;;pll,qwڅ-wd7n^WXXx1᭎辍0HD b" _tI<22r޽V!bҥ8g!;;;)uΝ D"ч~YQQ#pwڵ oH %"H2Add$K=I!&Md\})9Af֬YrrUiܹsJ%KC0 _qN(++v/\߿`'OC ƒy~}ǒhll/:) 6uMVv%3E2pH>TTT o"СC-O°J'jp$<4UiEz?wZa_ѣ6qOGTU&49I:_jkk%Ohllܸq gzbT> B~:qE4oB|}}7*? f^ֺuBE^n͛6m7xe8gBWWІP7o'NE\v-q$oᄒDo۶1?~_WLn˖-8P]]b ~[ Z;<<\ 8m62YGQ_BYUWWǫ%KeOK.%;4KjJ*3f_ 9sQ||dL ^(UVJΣ>QQQǜ9sf֭8f|3NʂM]U(_~%/_>pE***믿HH$Zx1J\| 466.\lSSӲeTV'Uy%\Z7sDg؈O>񬋢w}ɓF~BB|||N<x񢗗W@@cqq#G IZo`뭟_{aÆ%$$g%33s„ 3fDX0 HhO9@ʒw¤& ;FDDHXOGGرcE;GEE'o+w^ ,5Buww/da6Bٳg$^cD/(*==]G.ݽqݻetGFR%%%$Ayy9=0NNN8|ӐʨQYQ*.. ?ndd$lllHbK-H DDDGЄ ]r;=QJ/X^E\]]%CZ~޽} 3:ikk#0;wT;K ebff̸_'eAN)^R֌?SNR d}+ܾ Y2g [XXHn Ǒ+VZFr/ܹs :)ū?OrK.dMII!j*vCQrJ@ʕ+84lMJQtݿ(^̓S+|BL?_&\S(4(u"ORR?ZRKFўH.yYɂG=rVx^sޝ?$[17oyӧO[ ¦ꫯȽ*qIC1*%EEE}}qxҥr%S`%ѨbBGLj@/>}zL>=>>~ٲe*WZP($r+Z1#gN4> o 8 &5<<}?ޞ۷o?zf֬Y˩---K5jTߒݩs¦&WWWOOOpoHUHc###2c `9q===ÇwuuUo*))IMM766VIBÿ́|d &){{#F888kĊM#c.K1FQŎF(YJ^ZÓ4hre&ix^%󬪪jkk6lV}E655! XLOVGP㧎>}URRbbbbooyɁ*@`bbrdIQcÞB/ u{[牤$< ӧOYY9BB#GtttLh"lxx?izQIZgg+Wpxڴi 덎NCC*.I kҖc/*d@^:ۿ?BhԨQEEEiii.\a%(e~Μ98|+WүbrڲZWh? %1//L(Ћ̘1=zyfHT|hСC{QƐ!CBm۶ݾ}\jll< /!z.^Șf͚5ݽӦM oٳw6774&Pu֭Z !#77W^y'KW^ݫb @X[[޽B˯ڣGBUUUUUU{9sf/ K (%$$$r˖-ELF7xرc` Xp6,y; ۳gFg1Okk낂?~MM EQ9rd@@ʕ+}'5jԢEB#FmYt(/$ḿw/++1334i/^o^8x266mA: x٠(HW-@xaO!,))A 8Ϗ=q~~{B)ܮ(>>>555$ΎB|||}}= 瓂NMM} Bʊ^;eeeaÆ͞="D"ѹs(sӧOoKĉ|GoJ]VTT:uѣq۷g&222ÇLyyy ݫ_`ŋUڃ~oK9ڜ… \  믫A"@^6ݸ@Qk{{Bfff%66600011;c&M~g18_^~!4u˗/$55544! ,:8\^^neeջp$"""22!s)PDD1`OyWZuޒx"Bh׮]aaa|Ϟ=7n?^6ڜÇdQ''<M%n޼YZZʞٳtZiooW&O ݷo׷~[) Ov4_>gԚyyy e@u띈E9zcx1uRWꑥ3fd Hbbb>%)D"QJJ s KF;rH hh|4l>׷Ojiw", !ԿsǏWTT  Ǝv[nݺU2Դ!dɒ_{,游8r?pxȐ!+ -[Z !Իk< 5srdݫz6'@;6lԍvM^xQ'@-A{ X*'33dkgΜаHˌP(|W{[ @F҆s5%qZ:<,CCñcfee!KAooojjjH̓'O H\XX_RR1d|S9:::qĈrEJKK+**zQ 㔔#F:;;,asssgXXXp"?>޽]PP```NV h`` srruB---!SSS%sssI!D{zz۳XYYٹ2P6/2z={Q IDATà  Ԋpvv't钯d2wyLj浵sΥ}qqիH}4iRii2Oz{{WTTH1`(ƍOdeeɕɓ'!!!B}ݳg>lnnθeذa=K:;;K?PcSYYs#G }Zqg֬Yډ޺uGO}}} 88|n8rn(~#|M5%9ɂ(p{{W^yEj`͌ybrۉBՊY2``mm} Y,((~1р%Oɀ&y;݀x 5`K)6lؤIyL׿i \H}7.,,1˗/777'~ZZZ6l؀[f[KKK;O2*..Cmnn={ӧOOssiӦ/))3Dwٺ~֬Y^^^---===!!!R)J[[[[[Bmƌ&&&OƋ8?|Plٲ?Aijjtrw>DϜ9ʪӧGPhh#GpvCɉ///Gm۶ {>~ʕ+!y[t)+ Ν;?:u*&&;v A(zxxL8ÇW^moo?{mr'O#W\yzzJU.ݛ>}:+m„ iii b&MJJJ"kjj|||Z^{ )))##-JOkky*++@ooo33"Е+WlٲsN+/ksTy/]֭[x%!CB~~~8?k.5j… ܞ%3gÇcbby[li 4g==2гgTٳ'''===/_nccښ6'fffWlڴx`ŒHzƒCE!B~͎#b aFrÇբ"Dd(JΙ3gK&M"餸[[[ɥ|I(W߰T(14,]򢯶vYJϭecggWSSC.18Y!4`˗/K===J* ssׯ/>|~еNd/pǎ,Xrˇ*~7''%ڀ2119zhss3ަD6S9EQAkhh +Ǐ_[[KEJ> ߿Ow7'&&K;m4C@@ R`` c Xvv6ٳ$N!KQ_M2\|9$@p5zzYV㲠+pDs\zH&v(&VF ikk×k-uuuEQ< Juڵ$^Ecp?灥u/!T5$)//m[nq튈x&&&$$y;#c,X y ن_UI?odܹs8̜9øz|‚DK )jll$a==^V6` 6!Y>I|KOO/;;[[j Xc܈?c.^H, Bɓ ;)B]i&I6ȞhȒ0 21ƌ>1ȾF$IHѢ}9wvvsys,sܜ÷o߲iӦIgm `X 0?߿#P77|î4ﺬABȽ{Ln`Oz-EŋYWUU)퐛(D:S,~dd=mܸXKKUVVٓ{I",Z,q nݺIǭ|:SSLRu"rKN:Ͻ\ {nr}@ `ܩz Pwd=†r&Ltd9rݻwjqqen޼YZZJՕ3Ί+b1gJw4{%z̘1C֠A_~Ez3Xd6"l}%.,,dYoO>~G:˗7nܠ?tfffԘU,]Tz1''':f۾ >b|||Ԟ-ZH)ciiWڏ* O)uOÔǎ.]TzN5oܹ 4i:rJNNN_y9>ٳY%A/hddn:oժ7]]]=y]q{cwH$ڵkݻEqqk%ڊW/w^V&L2etk׮nºuRK.e3Ybȳn:nէ0qׯBBCCٿyyy[M988p(++HOOOOOܿ*O<=z9<}˖-Y>>2Flnj׮ǵk!Ga,v 2Yf?O!eee_fȐ!&L`U!XYY)rYYYIII]D (_PPNN\S#y'T:6Hky( })bCѶm[uC- e"==R\y/_q}A UHm*++o~mm ͛FFFܘŋoVWWu/]|X|Iy5FG^ {PA϶{UjFbR|mi楸u޽{'/ʾJuÇ{zzٳcǎ]vupp2d& z>|LpڴiQQQzzu4E pv xHN6e)^V=paƍ7wܪ׽{wh^vvJ:vM#K)ovڠAOOO㸠 <<g[UƲ7>|xXX!~~~ƍ.Q^֭gϞ={lBHbbSo3ƍ bMttt{yMfJutth/-nxXYYݿү_իW+5 ^˻G!bP+FtÚYlRẹi$ .ټy3 sD7Ι3u7mP\/]LUUՔ)S۷W^b ufTʓ'Od޼ơYaOiiՆ=%t}P>&M^CCCYA-龝r]"44T v Ò%&&rfbccys g̘?sJ6ȑ#Vs㹽# ,DnnnTTP ^x!}=ʝt;wXw!b8!!A6ϟ?OQg[nL#}hڏ*`\\ͣUbcc\2Hu떼iv) hLLLXV/^͛6+$'L0|^PE^}6YWD+W5u9nf^^ҀЧXjCQXazV۝}|@X___:믿~0++MKW +--=uZlpW^Iwh"LCCڵ+<<\zksX\wgڵbjj:d"///>lΜ946giiY̓'O׬Y#YfѺx֬RW;w.ϕF]cbÆ `r4o̟?yD2k,^UnZt)0`n_~z!dժUWÇoݺUo$~'t_۷oGA_5k+G.a7Zvj999~~~]رTo ) ^tÝ;wΜ9}ޏlPK)++2e psN]]~͝;&HNNf+2?صkwNXXغu贛-=j$촪.F~QHH_QQ#Aêׇ!lС'N`sj[͛l%y?i$ h-r劝… ufjjJM:ȑ#Fz/_ 68;;Ֆ-[w^UUUPPikkmd5k,ڲԩS... .~sXO?h:MG/r633{icc J՗6m0`@ǎ/`OO ۷M6IIIW^ uڵkFWYhݻ=z$ϝ;innqp+;WWW&%%e666ߢF$߷o߿D"ֆP#*5k !={ ޹s'_GbtرSLquu522rJpppj*)8>֭ .((:tٳ?c}}GWw;n^pss0am|}ᴴ4N:~ƍ,,,X`E<<~Xɯ_ۛVlB72h ޢϟ2u!>>JPP]'sw/*j˖-EFstfII0SNSamW+WXҹsR^.]ĺj߾}aa!/VBx5d8]KSLu"*.Q;Fw}WB@Q&P`ƍlk)%K#F`USRRk 8Q˗/+B9 F.͠VWWWm۶[nJTHvvH$277G%Y^J$N)DN]'(//g rttӫVJLLdΝ˺TR[۷orJnݾUSׯ_?|uzޑI>:lٲ Q,߼y˫ҪU+M&]핗wЄb׮],֣ӽ{ƍ?{,55ӧϕ+WZǯXBj\ʰKIIaE"͛7٨;vZl٥Kw%''E"љ3gZ |rŊD,߹s`*+Bɓ'ORŋ}MKK#dgggggsoٲ+xO!ϱl2ooM6EDDdffXXXkҤI>>>u:zݛbhhhoo?fΑkݺ8J$ o~ر3f6|:t0~xBHv;/C&DRPP`bb凨Դ3hBG/CCCCCz7mÄh5@!Z ,j`VC Xh5@!Z ,j`VC Xh5@!Z ,j`VC Xh5@!Z ,j`VC ^m:---..NAΝ;kNGGH$ǏD^^^uPRRիW>|͛1cwǍ7ݻGqvvvqq|BBB~':={ J)--={D"!lRN<) !...m۶^+M6߿?!ʕ+yyylѣj(7oܸ>:88ڲ7o|!ҲW^J̙3JgDÇ*ss'ҙ?&X[[;::*zT\\|9:mll>7--jժ5kh&K&ĬI&[lebbrپ}&''/ZWNR6mڰM6ϷhB }… #F@*ZX|EtԊ+fΜITgޜ丸8ggםeX4o|ժU !%%% ߹SkON#~ `QZrww?u!hһw&&&VVVlٲO>5")))JOOunRYYѣFuҥK.mڴQ{x !M4122ׯ_OHHDjfA===Q 333233mmmܤ6̛7oJKK銄"ږy79:::::*?0YeeennnAA ##&MHH$ׯ_ONNɱ㏭D"]~ hyo߾׮]#8qLqqqOyiӦ;vիږs 'x!hk[㓝]]]GKϟ_v-##cȐ! sRXXHy۩H$jժ29i߾7T:~QD$q׋/$>m-H=z+ZI]]]:7o^ӆ Z[[ߟ;ϣ233\\\{}GtPZsssnҥCQC3)H_t<ţxyyq6BƎ+/|Mƍ@.]Ok:tHO^zedd\E@d?CZZZ>}Gh_N;ŋۼys^6mڄ$O׮]e&;~0Ӱaj[ek׮4w۷oRZXXDEEܸNtTP7 RQ{vqQ`̘1lABn:ٸqc۶mt#wB<>st󽽽ӘVOM$mҤItݻK$m۶]9sFZ~ 6^RR¶=G!|y1P{)PXw^HӥPkND"QII KVS\\6"H6m_ θpB*>T&'o:UpFwE !/_3322f͚UVVϼs]^^䑧[LBV\ X[[ߺuK"$%%uܙ1J^p_~ҙ4226mZ!.]J ;=}BBHnn.h֬l"""uviSUUC*M*ܻwoƌpm۶ &$<ݻwT̛2d?z왟}UXɓ^zEFFr7믿&$$iWWI&uر8**jϞ=v_=}t #ŵa܊bbbvY]]͛֊AXjc6mllz*|}}uK~G'N$xzz Ι3ZYY=UV YYYUVgR&ziݢ;5bggGl޼yÆ tCƍsvv~Edd3g*++zxGvvv%%%,ϓ&Mj޼tk ڐ!Cz]YYAgΘ1۸N8qݻ !N*)) wm: ȑ#ɨ,˖-c6l8p.];w.??_ɦ/QF|&ӧELL̹s=zD _b7^'44KbYXX 4ܹsǏtJū:77à ۷Qdd۷*k +6lذsN:maa1tPdBHBB7|yf"טӼmٲ4\5n8ãGݻz* 􄆆'4AQQ… iH/tvvnԨQFF_EǏ'P!ΥʡPt(;ԭmBx%GGGlӦMt&^^^tfͯ\޽{Ys.bmy'O6m3xsx9&kC=:55U,;6 VLL (߿-z ]s3{W-+҃6vXev:O!p'33/Eܥ_q6oV!n7!\lYee% :t]W^z믿.,,d~UԩSRR[Sn/x&9s&7'.~D۶m.***Mt&&bÆ c3fwޭkBkS XZZr+ hB߲-Y-Ś+^~ͺiӦMnn.[TVVַo_I&h (5}lëJcnn~]# ' ^[ߍDM' [Z$6ˬg+Wwe2 ֝zzzGa}IFyG3uttِH$bח.ѣw2qn)-P>mBְaF/777'(cggǢK2X.\`3¤rJܼŬkƍiӦљcƌ^Օ.ݺu*ySϏTIUUU7mڔ&矕̤b^HX'J2+k֬a;w^;R2oVSϟ3~k `J]$ kmGTX,fKVe!]]G񶜑^NrrrX,X <6j|Un"ll~~~>8P,ڵC Uٿ?w޾}^kM&>cb,aeW_}EnzΜ9VILLԾ̯2Ž!}s"Q9%/',%jӦÂʝ/QdNx=@qE֥UMg:_~ҙ5F ! ` ;Oٳ͛7No>$$%6ՕJbv={ND]v޽;88bȈ\K.7c U ###Ի/_޸qNOC>8d&a9ٳYR]-̙Ûiii^_zE'D/kə3gonݢKJjZb޽tbҤIVVVm۶Wqh̝;W:A&M>S:v IDAT-|V5=z4off6l0:Fɓ+/\L>͛Ǻرc]]ݥKJ' +.^H'z\0Cuww^ն_|񅽽}Pys|U;jҥ҃:99щ'((7!̙3y9rݻwf-^} Pt@ݩ>6m:z 6.Beh޼yӦM"n/ڼOn߾]oǏokk;yS__ؘټըk׮$UkkkѣǡChbD\f&H$e9::H*`ee%stsss*݉Vŋ W7uׯ_-rrrJKKO8n.zyBFK p))QƏ/3o߮J?~L'z8pڵkijޛu3~x|}}i.bbUujEOOfڵ,&}hmm-m۶666Z:#xU׶j)3*$8'pR;gycȤƆ7"Hzl}n'N222+ȀT?; `=)C)NUZYY}$$$(3}}}lT8L^'MʄFW^^%''sĈ#FPk m+3Y#""tОKS[we=^JgZS{q(@)\p/e˖{XLXXt_ ޵k>} XL ˛`ݝ5r ***Νz#mݺB:{io޼{fFOruu-/*++}}}ѣG]qvvw!''gϞ=*]62Lff˗qT!$99֭[tzʔ) Rr Q}ׂQ^^c6lV&щݻwVCg6h`|Ue&+y?*#zީh?tuSNP`Sk͛7'H$YfJuҥKXlK.:^ٹs'w->%H뮋 m#,owwFFF˖-Gv M)//7okb)=|5!![aŊt"''Ϗww{vDs玊f,,ޙk;`qqHyƵxt"??ܸqܱ >Zu/uuuej}vĈ4VެY?\J!HDǂήk׮t:,,5_~ ӫV⽯>|x֭k +~:۷o'M]ɓ'lG}155Pk`z>z-ٜk/6fmr)Z##!CqaJOOWqkP,B5k !={ ޹s'ؘ7u%>>>&&ڵkonȑ#Z4hPӦMg͚K9uꔋ… mll***_~ܹ(ʧ~J4|(Xhݻ=z$ϝ;innš|WWW:2|p?m„ SÇt7nؿ?]bܹv\hѢ+W-\PX5|ĉ׬Y>6nx͛7[n8yљիW+خ]f̘A;vlXX!nnncǎuppx}hPʓw[.<<`Сg=D޷o߿\&LwqqqѣGتSׯѣX,.--;v)S\]]\,r2o޽cǎyyyu[nQ keP{ѣGӀD">}zXXXRSS^ZXX(xn:a[N4Yӭ[7SSSڬoԩG5jԛ7o_~e6lpvvׯC\"333::N5k%HYnKYYYc6_,+LGG篿nD\SXy;7Z~~~2ݖ-[A(~ ѣGIII2@^^9r.zt!>>^}rWҥ ?}t:'00Pl|;w=56TͶk׮ӧfQo߻t _kӦM*1 4H +޼y3tPkmڴ%PkIdd$w#!bN$ɒ%KecĈlĔs"VK4Vʒ/Rn$Qx^($Bzo.ӧ)Me``sNVҳgObq^)]C!tp&$$H첲u:#/////OT6!duuuk;,ʭ=.~Ǐ7kLz!CDDD!722:stCCïҥK9{EӦM=ccüUj7ط~}w`...wޕ y+VzQ2@&M/_.hժՍ7d.6lXll`9s5L]#k<{MqbT$)n(q+GފZTƸXZZFEEI<ӛ?AyyVp~QQQVVVҋLMMCCC׮];¾ MF144?~|魭H|/_fU9W?ŋo++EnSXиqӧO/_\k=zϟ?Pk.{ȪBիWZ{X4lp塡l?29w +ÉfwKWfw Gq&\~cj0˖-y !^C^uADWL .--{;w ,--ggg`7n$&&5nܸcǎÆ 333JJJsrrZlW;33ʕ+qqq%%%m۶ԩ;wu .333.....iӦVVV^^^u9w~~~jjjiii-:wᘛnmm(3lT#Xgllܹsƍy hP7o޻wEX+neeerrrBBBbbb lmmzUVVooo߻woeFV&TVV޻w/66SNvvvVPk5*NNNn۶mnT!y!&wOnnիW,--]]]?~ۭ[7n !}-i5["BX{Bd@XIk 4~X @mNx*oʕ~ ,)--vL ?XjS}#(C Xh5@!Z ,j`VC Xh5@!Z ,j`VC Xh5@!Z ,j`VC Xh5@!Z ,j`VC X;uΝ;鄐&Mxzz*N|޽BJ)--={D"~6mڿBȕ+WѣGp̳oܸ>:88K.A)dtر{J&|Gh:uDg&&&>~bmmXח۷ogffim*H\Q511ܹsvttdSRR߿O144SXXxBĉU ~#GBlmm.]$%%B͛w=6ܹs|IXjvZ:8~x:Lf=TP2~cǎBvX7n &̜9sǎ{Ǐg;w*HQ522矛6m[k׮uB KKKf͙3ҺukM5*444@NEii[޽_xQ,zE9rHYY---ݱcGΝ_>LX*jԨQ^?{tȈF;m6}}}y{LLLLMMߚYYY)uu(:]TTTl܏'OQfAq[J$J:͛7SLIJJ255Uc>`)ҦM7oJߴiS`` >|-۷o/\0by % JVX1sLBf£Gf۷O֩S x3+++}}}!VZfZ2 AABMSǴС{;/5;tuܹׯ_ f >ƍ9 j`iH߾}]F9qDYYt'OB o߾\֤2!!!%% /euuuUU!DOOOWW277%xⅉQ&Mxz*>>>..VVV&&&{ήӣ?~ڵ!C8;;+X#F|4XYYy/RHkժS!tAuyeLLLQQQV\\\iXUU###z`Y<{,666!!C֞LOOOHHx۷o;wܥK;;  0ݻgϞ;vtV9kذa` O~ n...>l׮]tb۶m'O8`BH׮]޽f߼[. ;䣏>"D"Xy%K9RSSYݨSzxxjՊ1o>XV۷*npӦM]Λ7NZ'N oaöoަMUrss'OΛoee?L4;˗˗/_~a|D"!,^X `hB!""<|00a/ P.]^N84ݻKG!3g%3fӦMGYnMpС`>|>&]Z d_...>}4o;}MLLUYREqq_]c2777nc 6,Yj*\~ŋ,fooO9+Va ֯_KBH$ںug}F{n111M6M}k&z'P}}},=@U*$IvvvA[[{C IDAT̙'<<^EQ" Ԏ;w* dRU]]o>x⯿rƍ+//ڵkIfddtUWWgϞn߾]n\\j,iĈd}HHȎ;BqqqFb9Ȍ3H$2??4hЪUݞ={pJPx5󎎎͌oߎk_Nj_g>x`Ȑ!Vpp0 !4iҤ#F۷o]ѣGuuux+Stuu׭[+H:ӧS@]rJ}}=YG52442eJDNs"B'|"?TKRCf-O^ϟ>|(`}M6zhkkk@@x*X=z?~<{ӧOu4B9s~G~!DQvBݺuيׯ00`0xeG EQ&W9K.ѫlO-CCAlC~~D"򮅭-cMII iK%mXVVV>;655R9BBBpkjjz7664iRGGqXc !dee%/ H$ble4";;7;~7 )Sk})yb$,▶8a^X|駗/_?w^OOϳ`c`#GD"ѹsi;2rRȌ~uFDyybhh1<,_oDW\\rR/>"L BT)]]]/^XTT/̙3ܜ599yɍP0<<\"sxFƽcˤ\r[ ! ---U&6y|9~gF,@&ZH_^Җ(ޞ+˗/_|9&&feeeiii_}U߾}8AV ]EFF?ѣG```K2zrr2r RcQv+YTt (44 .$e 'hSd+W s ###2VΝ;%%%x->e۶mAAAAAAk֬Aoa|ɽ{ܻwOy@VƝb1;|m=xA$8p@:ݻ[ZZT777P[[{aEEEn3gTBG =z:u`AAAx9!!Z9r N{ݿsstҕ|ȑ!F$:tul{ѣG=7H /" [V`:uM'N?Ӷ&#HI,Bv7cƌ7n^ꫯ.]D?S=7i17i}GEE ufff+W2UUU?/ϙ3ސ~]?Wb֭xz̙^"jjjϟ/s> /-\^2,, 77sss##W!DQ_|AW`yyyIIIQz߰7>)Ŀ_~ iǏT?{OKKCedddddзN:ŋ{޽;??7JNNNNNf;s }BGG'N׋D"CcB@ڵk׮]>q℁SХaBz12#c}ŁqLPieTrȌ]֮]{ 333F0QFDq+V񿤭^:>>Azqxxw}GOm------¢h{뭷p ;չpB!Hq[ikk]ɓ8} I&%''ٳg'&&:^__ӦM311!+'Lp2X;_\\R!c2gcݻw;DhP"dddTVV8gϞbÇiii:::...PAWPPpΝLccc___///.=+SSS ]]]]\\%ʒdP NЪW+/\~}[NP؈S ftjԩSapBTX@Ahm5VSSӱc(zwBa}:|KK?M nE hvqqq!www//z'vrss߿z7zݞ_|%ԩSuttIOOC9:: !1j%ڙ'N LLLBBBJ L.UhMvvB pss0uL\7oVWWw}„ xٳg%{;iׄXɼS4W(4ܻw!ԫWcDzAN8cbbJxTT˗/P(>}@ o޼|=zpL~~~JJR񷷷>|824dS (ѳWuu5#tRCM:&i8&]Œ%KpJ_6I9sw^;5$|Y;<$,[L^I4.ɥTAv\iHKKKq!+50T/6aaa8z .3qE"QnBnnnx !bŊ)f^zM;ECzuy=I3ٳq^bljj]YO(>>> PWWg``@߫l}6Y1vܩl͛:l%%%MMM"H$5oV14xM3Meeeee%)B8rH___??ϟsܤQΞ=f̘}PQgkRSSիWBswwאD駟|}}}}}ˎ Futt ՄԐLՉhUkO]|բ'FΝ; !꯿ d Ҍ8Cq,MjZ@ɔXXXŋy۶BOsss||<^I9s!$ M}PQgkҍ7B IP;*,Q&dNDZ{j=4*xTW]~!$ ǍGiB8U&\W-^DCoSiofU`uvSѣưl`g̙666xO>>e˖!eDS\^*Ly 2I;XxF<IGJ3/f ivQ4kIlٲe={F͝;wRU[[{5V4d*ܹsxA &@immx"BlȑP!NL4){uc&4IZMMݻwBFG455?4kff@C@&Qֽ{?~,o .5%?WUVVUXXallLFFF6 !ڊC577GFFz\^bqBBBnnn~~~݇:tо};GlA &Xmdd4p!Cp/^TTT888䴴;;;GGDZcjkiiiyyyUUU:tA8<466޿?==],[ZZ5Jf{N)533CQPYY1@QTLLÇF@cRSkiiߣ !dllܽ{w߅444ۻ,“R6djjjJOOONN~q޽}||XJÖfeet](ZZZ"^|ɓnݺ9::\Z\\_TTopY?bbb***BӦMH$,+N^ $ILLLZZ@ Xv- =zvtt7IMss3EQ@WWZcc#"P]]]qq1Bܜ>_ Ǣ_kmmmiiAikkKOCO4ٙMMM}>\ TaJjkk㢦.,, ` IXV&&&'%8&;%3]IIIRRR]]Li?RUcQxuuu={̐t/_z{{ 攔ݻ{{{;;;?Ԩd.:E}6.YT^RVJ)R} vPTTRTTdhh뫥`LΘI4n F54m(xHΣΈvrk--  6eޗ\ps7o^mmt$#""dޙ>>>7nhccccccgg'?<==9nzbWff&,ճf͒ ˔eeeo^;#ϗ.]*ݻw`)S0ɴg̘!pzfffbxIKKk֬Y"W=ݽ{ٙ4>>^f̕JtFh}:|魿;rϞ=O<1666223f I)L%;&' ,Y"sˬ4iRQQ#0} Ŧ/ 111 Fv@0gX,3Qz%@Zoٲ%??ԨQ>9`uuMz)}C>|O%%%E 6Lf4O_[t)^{'}_5fX,^X(QWG@@QԞ={ϛ;v>)Fq*NI˲ 6H_?999\R%3w166޼ysjj*HI:q>ܹ>eRF}QVV7ZD"lܸгghF >mV$2uҋ^RRr}O>kcĉx[=]8Gp{|hY;&]߾}?.oNIQqByH9dB]]o/ \UUE={HqB###HSGyK>j;vb|ĖСCY6"ݐyuwիWTT.\HHm۶qt}yC/3^޽7ߤZXLLG'N,ΝKuK70 9=z`@:88ɠ0{???yc=t=!!sehL oeLܼfy2uT@fqR SESS5-[cc# /7l8""BEG͙3glHH.IMMM q.#Z^3+モwjoС2wSRJMrdD#+9G>4 eOI<= O@~q8)F)T[~"oleKfZ[[ׯ_/o///@i ?g;^&e_l/IӧƧҶn:|O.RDNzw!/21~!3uSRSxNxT:)RzT駟J$.7А ,@@O,G[xMX*x!([R[]/\5kV~=ztih?3gΠA֯_`ss}lb|Qxx8BHOOoرcƌξ~zii)B(//oŊQQQ8dNN)'M[WWt֗/_.]4;;˰қI˽= 66ݻrQʫWMVRR?Κ5kԨQIII׮]EFF~gv"{Μ9ŋ!@0bĈI&YZZ޻wøasu>_ŷ~dSSSyL=~u4hjI)D+6l HBzzz{ mFr2 eOI͛7@`aa_tT<)LaRcqFJO?ݺu?~СC]V]]QXʼsb|# ɓtp޽{]v!T__~;wp6w]*pԉ.zII n#SQQchbb'(,,ė7/SjcP׊+~70}t++gϞ!ꫲ2Q$EQ}0`HH5:-GmGjX [akۤ.C7[>ӧdSkk+}s)!C (&]F.gomm-'],si裏GvIoM=%3/-uuu#F[f IDATB{ȦO>m\B6ٳfҳѢU$M8o82e onn~mcǎ?匜Ko}-Ҟޢ%{*gP=nܸA6ҷ婒¼xLX^^N61lI<V۷/=zMMMGƛz8"]]]_xA?ɓ'E1cYϸK.mhhjkkퟵɿFE=~ޚ5k'}O<%J=zDC5k,FTRsEFoC^-Kf?je EQd޼yx}12V^Jқ LLL~wzE~'%3e*iQ2WTTN ũm۶IOAУG'NΛI:kkk2=#__;:eqyԪ*j1a}RHǟ={xH4{lggzkܸq2'ܕG$]|!dccp8(}@[[['''MӦM=z@ C{^,EQ$a2MLLqsɉI7U; &A`Dz'O/jyGWoѤ,y6dMMMcccwr/LOIqTx"m$ &/DLWFz_ld蓞~⌢O-Z&]| B@@R%8;E1(NՅd|kL=zDQTsss$Ӈ &Ν;MBHHHLL B(<<$ H#ֶuV?!(ˣcG'+GzIX={yN Iّmzst@=C5@1QyDS1(NՅ Hԩ3IbaaoeEc"uXTUU\k.BpӦM6lvZTTTJJ f!ظ~zSSӷzK% wތqm"q`y|3֐v`O#HX^xٳgW\'99yw!gƱcJl /b ـihhХ?`Co:^__/$có!^ə5kcǎ6lؐ!C9򦮢 u$ڿÇǏwtttrrrww"*PS8%_MyS[[K~f) ^{53kwn̘1){ uAq,z_b, ;{&\BBBp;vx"** o ,~uXUr#@ `;v,~DFF8q L~Ep\\(SNeTEo"y%qX!M,--qo/_޹s!Խ{wf˗/G_t_~IOO7lBHKK1c|Bj ~I%߿n'\TTDoBr9x[w~)nXː//,,OQWsX+~ zфɻKҽ{eg2.\RRyt,e,oT)N WS~%3.**YQKzuLػw]BW^ſ>?!e<IRLP" SPۼAqZZZ_|_LY̜9sժU---IIIBv2Xx| ^Pm= pÆ 355}wBCC-[&I=z̚5_}^p9Kڶmۂ֬YBCCCCCɡ0WWO>޽{{dA4SRRdhmm]f :y׮CKjz{1V񌕺䁑(pѽ{wt9::Z:@YYرcqIHH022" ܹC7BhC!(!4wܵk2jjkkT E\'Kݿ j$eX2۽-#U?).xBT‚Llz{ɻWSR5kdddddDM4 SCoiޝcPC2D"xuIpvLݛTym:|;oQ۱^ ۷ozۇdXѣG:'f @J$PWSD"с{n>>:إa1i$Ǐ=zQ2t$4Dѣ?ѣG;!/ lj֭2&{f})(lSxȑ#J<#IKBѣx|ȑm/G@fCg*#Q4uXٳgdYϨK.c,K˛'\PY\\L9_L[GTeee"TNKe_ѣ5jzgiޝ} S~ISP:yժU8{:i&͛KoР7|JHH\tCKKKBBq qHjɓ'~=OO游81ƎK$?޽oPEWS-SIEtqqYpcBΝ2e]VVqf!%soO:?gΜTTTőَLMMWZ޳^zvvtt2dw;OMMuuu7ooތ>}7diW'OrJggge&~=܌qo}iӦ|2&&֭[/_$!kkkw.=*0ǒYRrRLţ8>}:~(jɒ%gϞ{Qtt4v#53f뾾3f2dH^^oT]5~8v<EEEY_SSԩ)utL^N:M0/B!D~HE+N} S~6nxȑ\Dzڵcǚ?~ tRʩfcĉx GٳgpttdtGD666\޷o__L,8>j;LH }:tMbyYqsskll$H$)KE|ĉYaϞ=RRSS黰_sMgo5jEQO7m$3eX!,, Yp^ ,oHN.]bڳg*++q6G#gٲe\MfaLOOn'ދG,\ Ry)I9%//O]'H:GqG DΖhKf={FZK:u*^KUxw.{ t_l/{INRdljjj"JRz.:B;''0`Bw SXuxq} SI$yjGCC}NI((R0VX|#X%yE1!$ [ɘz^Ӈ#]AR-)))--mjjD"ki*+++++MUB&MKKq0[!VX!ocM׭[ǸBpŊ7nܠw?/_^xnݺԩSq.//=Ǎǘ|ev7n1;88DEE'#}V^ }@ccN:OϞ=;11ߴiӽ{ȝ0a•+Wp ~~~qqqWH;i"ԞsWGR;88 6;**d|:ꪣWZ[[O6ƍߘ={~R,7]zy͛7jGddڵk+&MJNNfyʕ+o1˽zڼyt(x:e-44~"C]t駟~*rcKfF$Rd^P~'ţ8믿+4ݺuۼysxx8I.1aJ%3B:>>~ѢEk׮=y$,9wqOKRޏ'yT8)\.IE uڋ?}/^x1B( %VS2YUǠ8Ucqq?hFF%\cHU(2wڴi$}||uH~vU*oCÐQ @t }jddDqFPHHi} ީ$333 8h WԴGGGWWWoxOoI&2KKK___%WWW?zb"w㨼<::ÇMMMRSSB.T)Œ=),GqV[[[77x+ ܹillmx IDATo<;ǢsiRD:ESPAq[QQQJJJJJH$ݻÔ)S!뤙5~u{VTTKd@HQ1qWtC/HMRgRGG%ddd^9fٳaaa}]{d5SZZځX?]O?/ rss^Yfj~m2 w_~%cP 8@s@)00W`[lI$ĠΛF)5;;tP9 4agggjǎG:th}}Ç4СCIE(NP@G222qѣB?ĉ;(@{*Lg4'N>|ѣGEu֭3fxw8YJf:ijj,ѱ4VGG6P4T`X@AhP4T`X@AhP4T`X@AhP4T`X@AhP4T`X@AhP4T`X@AhP4T`X@AhP4T`ntƍZ;{ŋ!+++__bnݺן2e ^YVV2448qbGƏܚ~;00cUDDDTUU!f͚ejjóֹsBƍsttWTT˗/Bkvjii nhh@>|= BSNpB{Dt1׮][`BGEDD]!Էoߢ555+W}iN޽!1 ZZ PWR+Cbkaa' s ֘rڻ`ذa //bqJJ ^W`#T.Ҥ^W\!ӧOiҎUOOo4YB-YK!yC+8111!CCC.tvک|>|/%hbbҶ1]RFF%?T?xZZZss3^n6 B߿^!'͹Dd-ŊT)8z5jh e?$Irr2^fi,P\\գG=ZfϟSmaa*--}Ϙ1cE"Qfffjjjuuȑ#&C^^^FFFaaaUU=<MOO+))1225jՆ|77777ݻKMLLL B)))򥅅ň#HW^1@~;88/"B!c'OD";;üx񢹹͛H$*..677!x>>"H 2s^P" B%B&::ɓ'Bq,--MHHζ{ũVVV޺uSNe$EZ\n/_lllD2hjj"o1!dddԣGƎ O>(vС¤ Ȩ'/g0zxRUhu'N`x޽9,lmm?D/[nx7|Cѣ7|4L(Ξ=\󍈈Nj%Ktx$.\(]Gc``ӧAAA'ebbrYSL>8FWPPSа|r띰HHH u%Bhh(ޒubOãYAx"g8u̯$RKa&$c!ikkׇ䔛KQ۷3iii-ZHpS/U #ZcpY<}tswrr? MBx?PC }fŸ~ 9rcΘ1c۶mus_7*{ G0vi;<{l2 www//0@$%&&?}ꌍ_QVVsNrJ2~^^˗/~ڡ"x$Ј#аaÒ߲e 9JӇW~D"!~G?ojjW+츜fee%?lmmox" R;Gr޼y8ӡCHL^|Wő^WSNe.;v O 'K1dIOO[(>#cJvq=z4Yn:%@j=n݊WʦBСCd%0VWWj SSSz= cr3/ǝBQTss3Ȉ4ݻw\zKKK _KKV 2aFQn1z#/}jjjZz"U?Į]>}ػseo AĚZCi)UKܔjMnUNQ%Z-*Y؅DA,?﹯%BGs͙sΜ3>*HWC^Lwj:*/V[eV3Ҁ GXBٳGҥKw:o޼'^oS,Sz{.:vرC7nh]өW]|YΞ=L>Gz{M6BW_}U=MiX@&`BW\r~M/Grf /޺Uĝ{m޼YY]'n߾-3X"..NQT B}MڬKB\zU]tI]]] cӾ肩Yxʿ,YDWj8O;ݿ_K(ěPNx ԬYS5**J*MQ_ԩSj.c. t$''Dz?ϫeΝe~z Ќ2zkƍkJڅuO\v-5>rbuQ3z'R_Z\C^ fggPAd3֬Y#OdN3gʕ+WZt)7nPSѵ/i@+X֎;M4^bZYsAnnn Zj:zuppNv<(C 1zLl ݖ)ܹsڵk׮F&EImvիW۷oذa]vq *hۯ[N&N /=zT,!hz .˲ZU˂dTߔa@Agÿx{{wرRJ&e7E?ț *G(e&6ptt4LƤƭ]v^_ /Aٶm?mllf̘aB%O.$-*$Ю4!QKիgΜzᡂt9Eܺu!}r9sӲV̙37k.|z[6m*}nܸ!]Y?~Æ 2BukC[{'RԼys:ߗP_כ0jۺu z"fטY~ܹJԥGMDO譥޽^jTT7-/yѣax7777$$DMi&t:݇~(+{eNR׭[wرWZeʔ >CYxWU6p憆ʲ| c\QPRI_vO .~ӧO߿3 <XEI!Ċ+ƍ7#--mkV]RqCOOOGRO*˾:Tԃ (s S899)'xvRӦMw٨Q#\tiΜ9M63z2ehF'2\xMm`T֭k׮ɲ\Tp`4+INN?eMUSi[KU֮] .6?'k|{ \|Y ZmLЛuAygS;K͛'8l0Z3gδm۶k׮k׮r6zҁ !ZhUS;wB++~!W( K.'VHLLTّTcN6}B;wya_~͛>veM^&O\^:ԭ[ͭqƱڵBXZZ}' ,1cƮ]޾}[m}СCϜ9cee)KyVWIfQLlMOOWs o5ZԩөSBCCwfgg;vT髵9g|/A\(ě0..N} TO6mUU_"99Y 5sܹOzBBBbbbC`k:U^7nܼyġԉcK jc5k,>BhXM6kIǏ4yVBTP7~I4ik.qiii .Ի"dvvرc333 .TN׼y͛{{{GVʴ?991ϟ?/.4IO~JEWiʕyѬYdMN>y IDATW'OT?իW(,Y׵Ǐ^gɏ}SN@Q*V^Fkߢ! ONIIQRO._訦h~l{NLAAAoܸѯ_?>0ǏٳGnjcSIf)N)S kR);;;`sB;vٓ&M݇.䳷=kժUreڵ`Wq.\0|U}tСCr6PzzaNt Q### q 0T+FCTRLt蘋9**Ϩޅ=Z37譥&ӻغuьk|LN2l`*4 '&&7~WVڽ{,j"00PW޳gO(! _PvmKKK9ޓoׯ?nܸt@($FbY_,%YYY}̙3iӦ6GG:ڵk{٤sC>VREg޽o)99y_|Imz㠠 ׯ_/eyԨQ22vGDD]V>--wBXYY[NN7|XwYfժU*U@XDDkf͚%S)Sf?`aaax%x2h'6RӾĻRfl˗/o^;梠w!~r^>r;vCZQFf1 !N:Bݫ?'Rsdpʔ)jSҥ-_ 6{n///oQ w) s5:YoŊ>:]!PBʕ+qQ۸]vnnnQQQB3g^z_vppٰa=tPn8 yfvF斞~+Weٳ9*Udkk+3gt%))ѣJLL6>x`vvv&MK]̑?z訨vUZɓAAA;w _>77/$$&$$dǎ>N2V) h;ĩp7/nzKII 믿T(jƌ*뜜^zիWoʔ)UV}ڑ7!;;;,,̰@osΩa5eʔ1Yf`Qm]r$2c.g)]tW!ŋ۷oo^vmϟWݫTѷ5O%Q`t|Ο?obD+VTsy/?'XJ<گ_?IBkb |ԩGۿ[=^Zn]/!DϞ=;wl;w BXZZ(N[ Sg9w֭So-[:thVVj}vY蘝mtiii}yTѣGyÆ 8r2bĈbzW^jԨeر&;88,Z(''GɽrJh#z_qnnZ\&}v! Zwy޼y2==|˖-kp傍|^TV%%%m}7+d^xp֎;nUqƩR5jh7 4ΉЛ# ݺu[Z6EMRqĉ%G__nnʶWPAUDjvb…Mj]ڵkz޼y~mmM\\+Ϋ*ƍ'>>>!!ѣGokȚLw޽{7?B\B&}-[p5Rz:xw5lpVfVmѢE^k#Fn*Uȑ#ϟ?VhΜ9ڇ1nĈgΜy뭷3)ZjU,8qCɣz.\8sLNgK"##yԸ8Y6a+quuTkr7m_ݮ]2C=-ZسgW_}իvGDF>/jիg8i'ބs֟׾H~vV:f^c. z4nСC߻,,3zku)gbkk==1iϥ퉪UUXQ*Udkk^)⢷oj֭[g ^zw^bE߾}-8ܹsM4y|8+ uիW~!D͚5gϞ]_V W޽_>ZZZZM3%Y||6zeggtR__b ,@Iv޽-Z$''׬YaÆƍW^qw !(6lRܽLXBF f,5X0k``Y#F f,5X0k``Y#F f,5X0k``Y#F f,5X0k``Y*C;v͛ܬY3WWW\!(_olr!D^*W\y˗/ !^~:un}lNk !233wڕzamm]<(͛7?~e^7l'!<<<Ο?_x*iӦ]pA֩S7m۶ !/_VZZZ^d9**JӧQFa° ͹ϵpBEOoܸQd?QTTTppppps犻/\B=Ǐ߲eKE9((HUV%L[nݹsg=([.^(X;w JOOB?رC~v͕10:'--BGG2eԔ!DrJ*%8qӧݻҲeK333O8QL-[zxxt:ӽ ;zhzzzƍ6mZfMӻ>}ԩS?Zj6mTbeBBBvvlpƍڵk]ti֬^X ' }qDDą J.]n݆ n-666' |rttt2e<=====޽{IIIcjj\pZrei\Qԩ/O2Eg}122qƲBTʌ3fΜ)0`/"+}ݕ+W !N:tз~j++1c̛7O,\ѣGjkrm֮]hѢ{/##C[ٽ{+WVXѰ۷ۗC=-[VBm㴴4t999͛:ujZZ֬Y߾}[{͚5gΜ9h ={9^ooo9V(..NV6mڴiԩW\ۥUV7ntqq;t޽Yo[V7nBX[[_zٻ ({ァBQ?cDDD~5Z64ܹs"++k3f4hٳ!_{5]qƍ7N/z%صkWhh^4i{nmJa֭-[xb^7߼2z?tPO *22?Իސ:}lSBݻ !}h")))88СC999W\iժUPPu|~,t*U899ɏcƌYf,ٳzB̘1֭[?:۷۶m,?6hР]vAAA!!!2yxĉeãk׮>>>'NX|yvv{~kkk??ׯ߿̙3BڵkBF?H%q?~ܹs'NXxq *UڲeK6m.];w_ҿ &̟?_qw4iaÆzɏ_u~v%_ ɓ'Uʕ۫YIJJZ`ŋ Op=z8::ʐkʲ߂ d/!DRRR.]BBB?^zuiΜ9*z5u3fٳL3۷OVkذaXX8pW_{'4i `s;`ʕB+"}(^EP1cygΜWB!C`UƎ+ 2kk7~*#?( ݻeyҥ_|!6l['9rdBI&%u͚5[l^ !t^zOJJZh,6oN|>>FOԵkWYHNNwҥKAw\Z]ܹ^:a7o˹Z*nx"휲CѷoϘ1ѣB *TPƦ0J` !ΝR7E{ /`bpuu5ZuUI\b$/ k8cN2E6xX@sԷo߷zK}p§~ںuwmŅ<0Zw}'111*Ty1]Q ?m,ejT齌^ AzêtQ*hh(zFk׮ob>ܻwرc]\\dRmn#FXfÇ? vگ'ɤ %$$rZlmmmmme襗^j۶0iKKK]7}v~:P~I8^z>qĊ+W7*N駟4i5zhF\~̈́-Zʂ]u:ٳg۷oUx ?.?{N*aq_^u_x1,`B1,!<<m4Yرcsv{{O>Dlݚ1~xD!J+־};3vڥkӦM/ٟ,/ZH/WnnUd[e״FDDNo||||||ڵkW}xr`)mڴ5jҥKn֭\֢EW^y%33ȑ#2Tѻr劋o-ݻgnرj.ӷ~'nѢܲ/_.'=988̟??g}lܸqƍzm7okҤɜ9s{쬬+W\Rmm۶U~hȑk֬ BDFFyycǎrվ}׮]"PBL*[y >ܨQ#Zly8Nت ײi˫V=zj#~WzG[h+Ud؍.]8p@;HaaaaaaaBooӧOpSʕO~!{{{M&L? {v7eFK޽{ذaF_nڴI[Y|O?4U@ KLL|b|f/T111NNN*Rы>x`LLLŊ[jոqcO>ckk[F۫QB77^{M 3ѣGGMJJjРK/TvgF>z4:unݺjժF'%%]t)==J*uU2P\t:wf"jJդz1, `1f,5X0k``Y#F f,5X0k``Y#F f,5X0k``Y#F f,5X0k``Y#F f,5X0k``YwJC]x޽{oƛoyԩ(!WqwZy IDAT]G !׫W9'Oczo? 1bIJeˊ'%F `]vÆ B77 `M4);;e(==_t7o^zt+ԕ+WICBBTjܸq۷/G";/@a+1c\rϛ ˗_`>}#5j(.%$ ^xԩS҇YpvvֻGJ{L^,UvkkL!EvvvVVhii)<|0))IQ|y{{ÇGDDt &huuzzz֯_TRǏo߾jn޼Yti{{:@IꫯM4I~ ㏅G7n/pvvvvv5kVLLL;t0q)S6#FI?onѢErKwܰaCUӱcGgg:2… SN7o,AYj{޽6m\z𰙙Ǐ?|/biijYJHKիWO[]dWz&M !Uֿ!]l*;88D^ٽ꫾5jԈYbk.]^~'Or߁V\f͚BA `ѣG !.\(X 63gSgϞfrsstBǏo߾]nZ|AT300Pn>|x ̙XӧO s ܽ{wKx۶m}"#@q"e7nsإK曕+Wn ֭[zzzquh2wΣF_oݺ1cƌ5kMN:rHv2Gk֬۲eqӦM V +rJ@@ܹs{ZRz !{E_:kӦM>'&&[N8agφrVd"J `YNNN.رcڵ^*}w۵kWfCmݺÇB^xGаaóg !&O|O4̜%Kt"ʚ={ٳXYY_^tY-[I %v ZR][g5FMwOѹs={dz^z#GWҘ1cjժU1]bbUPCǏ?tЍ7ʕ]G۶mɉ{Cݺu˕+W%SBBNsrrtBO!dj&tBTUoK͚mvڵk7..%,5X0k``Y*_!%%eB''ww?II `( K322V\)·Uغui̙3B5kּ^^^U<߿/˛7oV+lٲK,iӦxرB `=~888eʔYO>}ԩǏWZM6UT1CCC#""\]]}}}BܹsD777/////ҥKLMMMIIBTXVք>>BӧO^O6m4u+Wշjjƍ...ܾ}߾}YYYʪG˖-P] ~z77/bzÇꝜÇտrF… ۷o?vؠՠ|6mرaΝ?ƪGGlj'۰1@~Xׯ_m`aa!g !qkɒ%3^ !6mzA4idzںuk˖-/^KPPPӦM WBAmذA[ڴiS"))iĈ;w~>77W>>/Çz||ܽO>mڴX'/_,ؿgϞmxQ6m_|cǎL6byO>eTR:u􌎎 LJJ y EJBw&Vegg3F}lѢEaa߾}B  _l߾ի+"7M#K^u&x'L ={^tŋ=Z6}BRRtѴ4Y)'XkԨQF 9K+((rرc ۗ/_~|9...6lhii)wޕիW gx|(˗/_Wݽ*UTHgС,ddd۷~3f8zhffB *TPԛ4hpטʕ+WT^^^}ӧO\\\\\ÇQQQyu5N:BTTLըQ#KU,˩ajkD;w6ZD%->s׮]> 6R/\駟nݺ|ݺu[xqbbzʕ++Ao^mXuɫMժUe!333&&Fok^>VttzMLo0Dš5kyOBW^y3gTK ڵkjժi>|wޱcǺ̝;WV7LUQaվzyцYʖ-LXB*UmSb PIx /ۏ?4h0gΜ.]Y,--?I&8p <<\KOO8qbŊ dkkkkk+F/R۶mMWr?2ay*NωU9%%EҺ}v~`erʔ)}YW߿~z}Ń pss;{}|M~. *ᔞ{;vLQLm۪׮]jaժUK*n"66h D%a afff&GGÇEJII1b&([l>}6o>>E;;uXb ڴi# 999B&MȚ̙3,X `2e{V䦌+VOLL\t,wU{,;ߗkDfdaʕiiiz[oݺjժg9>7+ _~EtҾ} M6÷=-///Y^h^6\BL0rrȑ#RD?~|ڴiܱcGI&}3fP'y?Ç `lW^5}ف~eJ*;2_~,$%%[D<` Tr`9rDSL,k#G Bڵ{ҤI<|p``ѣGeȂӷ~'nѢܲ/_%ppp?:̙3oߞnݺ 0;33s˖-mz衲NNN~WG/DFFnٲ_XR۶m{uV!ľ}|||zհaè5k\pYGݸq#k#GY&((Ho^Nt6l؉'/^,~bٲe}~~~<RvիWzzzΝ;\`aɓ'󣣣#""2oңG;w>Y?~ [dV|R<#N{aÆ_nڴIoEm߾RJtjƖҷoǏ{zz}aaaNNNq=z7n5k\ըkd6QѣG bx„ 6lgy. UtFgG&G~}:,,,&&ֶF۷_ݺu+44ٳ*UrwwoذarjsEDD:u˫qN9vؙ3g}||q}"(v :I.R˿T&T3!^R'HMMUo%,5X0k``Y*|G9sYfԩSQQQBwww//ғ !\ PJT+33Ǐ?qĕ+WWZ~~*Uz|!ĉ^#FXlsItt1c?E `v!::pԩS?j.\vZ!Dn>B=G `ݚ0a!Dưj*۔VVV/WݻwW5jnݺ͛7 !_~~|>}#5j(ڻw+Wd?oզ={.[lԨQBSX WX[n-ZhWȑ#.]&8uTat޽{IIIcjj7+Wk{sݺu_tsstzͲVVVÇ,˗9|pDDNk$O8ZzuoooGG}E$]& ={4ڠv2u@"##ݻw;;; !~޽{k[2… ʊ+رG[9z+V’%Kd_|1k,Y:t!CvvvܹsXU8qD#Vzzh5ҫ[O?qD@@$&&꫿{˖-Uevv^A{8Isrr&O?է~W"QXG55 ҥK7y~Ϝ9#]믿.˷٥K^z)))gϞ-[*'|WJԩgttt```RRRHH3]'@q( ,=׮]{ADDDPP?,7nol۶:Sbu(d^Ѫ790:Pd{:tСCǎ˫ѣ{QQQEյ")))Fܾ}Bw !Ҭ ۔.]jժiLrlllݺu ۨ$Y_{w_EusnBn@] PojC#ZAU_hk(UP"ȦD$!Dr9?&.7!\!=sfĿޏ\F.XVRSQQo߾۸\.kQϞ=Գv3klv?.+!!*[5Yp6} DC¢E?~7xZju뭷ٳgk^ /_??fk׮{)Uғ;w6 3fǟ;w:u̙4~ ,G|pٲe ֭?nݺ 6Gzz ̙3~FrrYؿѣ~DÆ WX!"k֬III{yŋ߿?`=`%%%K<9O>ϼ Æ kn:s՚5k֬Y3|@X"2gΜnΝ"gϞ={x3f̪U~߮_K.5:t۟~iƚU6mdɒZ.!UPPpFm۶ @W꯴t_k&>>{y.+:::>>h0++/ܻwo˖-SRR (=M@^^R*""ch@ <ت)**kפ,4 B4UX5,l F@p8D`1=` IDATBCC}ot:,Q֭}oQ`"##',Qxxxtt/-,]lllVjoӪU:? uJJJj׮DEE%&&*:_ R*666**ĉO.))QJn:22Ҝ9 ;wܹUSQWB[#``kX5,l F[#``kX5,l F[#``kX5,l pO} .٨m۶uu޽[{-Z/u]WGx9w\-Çħ{… 'MT{㋽iZZZzz ,h&*//O)aEq@g3٢k)?cFFȑ#Ν۸=ѼyRRRRRR^xK@\뮻.%%eȐ!ǎk+HN!tsܹs'Nt\"R^^>u;3ÇgddHlllc%6ol| 0`tMjz5yz/?=s4v_~@Xx✜/BDvUVV҈]jtҥK@5feJMM5,0rss;w,"Zks%Ѿ}W>}ZDZl٪Uzr޽{׮]ڵ:thttWSN\BgQQ? "QQQ͚5jӼyĔˍܹs-Zٳg=_v+**D$888((lVVΝ;>|^z%%%{v,??̙3VͱcZh֦Mub?*//7 ]vfyϞ=}5gΜQ͜9u]ov}YƌxLcƌY`_={WkDw?~Ux'|2??);v_>=z{ݴiSIIU9hРE<=?w>77gMOOwݞCBBf͚5eU&L 55UDz{n'\t4koKL`kךݻ;N칁b)Zlx1֭/U_QQbŊݻw^:!!YSL1u_߿ƌ^LٳgGd۶m_}^^|3:uСCk ʦNaÆ~;((/."Ǐ ~јٳgҶnj|g5xp8&L0pŒqȑoܸќ͗{];VD>C/6qqqv[vܹn:3ܙ9s_{5Æ fcaaa7nܶm[AAAAAAc=fW7|ȑ#cbb۷OD-[6jԨI&%%%7)..z~wGEEuر)ڵk+"K,={vhhh76m9 ,\+=""bڵݺu;y~7߉Haa+b{^z%>?~}_&,**2˽z<cw} g͚5k,vtcǎIII֘xwBrrq7-/,,裏j[nnM/X}?Y׿e{ウgϊHPPӫg_x.NOZ[+n&s?I F`|O>gMIIÇ߿uLm۶Y6=zw׮]6m.Vʉ"9t͛7Vd.!_:ujZZZZZڞ={|a;w͚5k̻͜9۷8q%999\?n/^lvf}>};ڷooر!:V\i-ާOsb.x_}%3f(..Cg>WIOOO;wnzzoy-wng͚UAYYٯ~߿Y(--]`AfϞ]QQ 4G1;/W?`nwCtw***J^z)Sƍg4褨(ig͚uYn+WG}4**JD}YNMMСu*8rΝ; 0aaa=VZZ:uo_,1"55,Ϝ9>Z׼A[nܸ_m6|x\… .\xf>1crfFb ٱco馲M6kWSDD/&"4iR\\\EEEFF… Lys099,߿ ?pbbO󤤤iӦgۗ@W8[,~UW-\;}vs5C:t:6<{ٺu98 krYRSS9j͚5k֬>|xbbbHH? &^tҥKn8`8_~g~nwEEŢE-Zdu:Æ O5 >j(5kV׿< +//oz,ӀS='UעEgw_ׯ+ 6۫>99yݺu;ϧ8U_c(^y?22zFnݺ˫M6>A޽;~***[~gdzG׮][qqq֭:th_*{Vx~X!Clڴ~ez衇bcckl_#B\%W1oPYYY}].ݻwؑt:cbbFT%t\Vld,--m۶m\\-b-U۷o޼GC ڵ^V[lY~|֭v횘8lذgϞ'O6o<>>u$htyyyJsXO{lV}k,4  !PgX5,l F[#``kX5,l F[#``kX5,l F[#``kX5,l F[#``kX5,l F[#`ւ\. 7`RwСCc`]~6ol3 )5,Sڽ{]۵k7t/ٿ޽{JJJڷo3x6gΜj;֢E6m4ț46,?Ͽ֬YSQQaU3fm۶~ڵk_|{Շx:vh֬Zj„ mRSSEw޻wSiݺu[zgz%"+VHNN>p%7|sJD\.oghk|UPPpF5ˌDŽ XXX~z0D$""bƍ={4/)**ԩSaa8)S߿UVo)S̙#"|͂ _{5wձc< yyyJKlV}MQQQ5܇_nիWHTTԲeFaZxkfX=Ыjֿwq8m۶ߺDk}w/YDD }vԙ3gߟl`]YS333'L3a„Lyڵfz%"LDdĉ%KJJJC°a<+QJ{fȑ#'e ϟ|cǎ;vl\zzwBrrq7_/,,裏1c-ZhѢٳgW$;;/}])fffN2v{VGydРA^{m=AУG'OVoնm[󔵔{Ϟ=D$;;;+++++kƍ?+&J ^|EEDk]QQ1sw}2)sDU퍽OƍO˕`edd\Ԗ-[ys˕{RPRR#^CLmڴ9uT=pRt:NK 2dذa9rYxꩧ^u>11?yo߾믿z pyR/_SላEdĈ//W͙3,gŊ]vluzv Rv!>}zppRʳR)7 56pSNMKKKKK۳g޽Zjҥ^镈|J ?o޼ ʠy BׯYO[Azzܹs|ͶmۊѣGݻwjr>w rwX"2y-[?>&&&&&f[n|pʴ4O={ֳGo x!z{/++4 gΜj_4ekǟ}_RMf栥^)<$MX&eUQ<Gȍ=b*\7S50@]B+wguW=:($PܥF^òt$W'Mq~A]S;1g#$ĴV7Z^0W U۪彯=w(%J]2 {Y3/ 4}k`H(G9RUvpN;F+lxiaxZLwԣPJJ+w>tjɗ vzOQ,dTU?TmJx7Pfz`<3Z;_GuK_{_y&X2HbY yc/O%ؔЪSު~ _``ueJUX̨}%PT\|INH[Se>""h^DF^BZ#skq k`gRmh."wFκjk6$tp1 ~sJDDy%UkbY?E F`XV(0j1O#>%-CED;k~e@wTA W=;H[dKAj F`)s+Jy/f]9rEZXn qKpuڄWΕr: ?HfvgX{*G`96VX2k s""]"E%ZE))*Wye֏-?"P2c[xtRUI>bh -nc O[z;# &B}[pіѭT03_ĜwF-W|W(w 6qr|3``+@.1m!nC pې pYi܆6t@GNv8;UwN-͝""ȮϚ YY]ʁW3oS1~m(F`gh+2+6[++̪||ʥc"dduGir(iJ'KPmEDJrʝdHP۝YPnۚ# q_j8mhQZ(1(%"(Q_$,G:emDcrkuUTaxsIj/[w^,^ODUQ"` !mhVn-ns1&CܺrTU(C'yCCۨašR eXeU|++F "yh -\I%U-ҫ =ӫhD*4';=4$MReHyLmoL\zzʪ Y2>¹Xת"f0b=7O1q)Hc70ac1M>Mz#g_nap >9X2_rGI!oK_q(ahƯiljpaܿ#B);KmjnNyv\"aS#r|||||Ut#6mJGj7yx4"P?lժU~緈JMN|||bbbŊ ˖'O tJ{w͛X+C>z@ޫe@,C@~&G%$IJ33XK @TZ\/n(]RRR]\\ 3f30!0ʐ$ZD({fOQ{O%˰@2Ҫ3|@Ν4jH5$$&ۊW(Kdc&1٢!mb&s=3޸鴜UJ ~ MDDѰ)<<|֭W\rJDDD*U6m=|peC͜9;wvY(]=zٳ :T}-(䴲-%q]iK`pkgc35czftֿde f$Sz6W?FjyyΝ?c*UD;"-voqerU;;?6oH"zyy!}RjŒ0`d#{xx }lS2e/CVWc!mMbonW>l.ILO~R٩*ODD"6!@:uܫY@ Y&+0 Vx|||-DRy&1R82TvNZ=ǍBN8"YY-u=ĭP"d#MDD\qqq7-Zc&VVV_}X5GØz}PPPDDI{rrrPPн{$11իnR_!`0?.\ j}}ヂZjenn㗬nnĜ**TaÆHNe\.16_₯}? )ƒR)\oZcCAid@2 FNޔ FqC IH/IDD+ZaN?~|ժUMvh޼W(R8$Pjj`x|p ֭[駟F}}=""bѢE?-A|^xqҥIII ˗/[tieĉ7oV<~xʕWVVg9eTԷ|?_6flW0DDEѺHgaaѩS'ov͛wڵ*00ĉÇ?|֭[x :o߾0`}}}?C1jժU/͟??))iС۷ogϞ-"M6_>G_dڶmy˗/oٲW^k׮Z?11ٳg\iࠤ*TpqUvڵ]]]]]]*VK☕*UTV?Vq\\3ɒKrϴȒ>ý;տadHm0K.EDD$&&.]tҥ۷ԩSNrteȑ_}ڵk˖-ܹ#r>}V\)̶n:66~JNN~0G͛7Ol7lذSN'Of̘fZ)22R<+}v{۶ms…K?^w>!!UVZ:R7"s鵿%e:@VWPĔ!=]AEr@MDDl*U8qVZ޽{w|A N#5j-I{%6ht5k~ꖺuQP)))7nq/]$>O^7vܸqz}pphtss _Iú)?Co)}d0*ʕ{I?@DE6(WСCG=qÇ$&&]V\\S*^f|&CFݺuW3mٲe|R˖-FPP9nҥKb#66رc8W\Qi۷h"Cި^ izI1R2ld>iQ&~p;w_^=x`{4 3+((Htŋ[oՑ4T4Y1ʒ\ il$Ib:WeU9H𧥛8H)դI&M .8$֐yխ[+000?~,6lmm ?gϞXܹdtϟtC7((KG"=`pAcFϴYDGhMoؘ7o^Et35j@n޼i2.$$du^ʵӧ+c~ѣJcpp룢T(KdI ҊvSܺbU],姳8H6hBC۷߿۷u:ׯ_ߡCQ}`СFeXjg}v5k qŖWB^xxxdd$S L'=+?eEТV˴iq|.Oԥϼm""ҢheԮ]ȑ#󎣣]5j,]tRfffb^z\qY\Ťkڵ۱cG ;;ѣGWGUzvcƌQOsrrݻپ}{W^Փ$waDYI[E~f'LVi1`!]>*-EDD%EGGvՠA;;yHQiҥCs}U*Az޽{DE 9Z7&wt:]hhhvKb8..Ni дiSz!<> YԻxJ 6i-?=\9zNiw-OצGMDT tjUTyn5iM粰ZI fbMoi2FB2$?_'1ge9UJ)펢8H"6QIi&"3 4@jj*JGgG()-3{$46i$<99E81-1&UBR$?MS=&+QF 2aooP/ͭcbb 'C 0k,nPre7FDZʕ+~\dSܒiK~р:3΢ "" 2akk;E333+SLTTTMS02]WpNRm AoK՞q<8s0l*]߿Q`̘1&LW^ G;9(MELG&EUV}*^obp""2ŰtuppdYVZaw٦\P_@}qږ$ 0豼ϝn#Qqǰt177/_^sNttKTcbذa݋lZ̕^hiQӊ3eZ$Rb#Wa4B6B6h޸ٳ 6iԱPullkRRR ,rAر=ٓ=)j2NjKy7ɲsOF{CDDTIphi;J+*ҩfQfgF7 ^+i"%LDDDϗYDqM%JnfM{#D `(gu1l*QjM3B(æ%>{@DDT ie%ZS262Q'In[3iXaSо}Qp\ߗaSiX] ""*dqqq/rwm""""҄a& $)ӱDDDZ0l""""҄a& 4aDDDD 6QYvNkݺu-r}944499W_ԩSHM&MJǸִڶm!|Irrŋz5))a6m۶-))I˞ 4ȏiϞ=|A rʵjRYJ}}}5jTaӃ,XǧXMgΜбc_7Ν;۬Y3MDDWJKشyf ժUп3gNńɲ _~&1Q~Mv޽k4=<< ;h4>xeʔy]n߾mccSZ5KK`w^DDD <==3z|#""҂2w~U^]v:tUV=N:UZ-Z<~X}?ZjժU0aoooWWW___||S C~?7ZXX4iDڻwo ~IGZrtt8p`hhPQQQ⾿+W:w\lƍשSyҤI:.w3gάTRժU[n]F 'lٲLԬYb޽.^(njذaCDD`ؔk׮ 03gμ[W'$$ܹsgn)))&LHHH2e TuHd0 >>G/\p̘1Q̙-^X=&,'yraۏ?/N8_ZYY%&&J0j(4ڸq``ɒ%nhѢ+;{lvԪUٳgϞݷo?11ٳgqI~x뭷BCC^bEhhhhhh>}D̙3׭[yV ޿ ,,G&y5ӧOɈ#|}}/]xիXvʕ+syti?sǎlٲe֬Yb?0'2|p'O ܹ3///qѣGs"""dj…󏅅?ꫯzyxx1"""~6mI/_ޮ]ǏǏuַ~ o񆸯;eʔQk׮ Z\r>5;;Zj)3 =<<Գ5x"""|˖dTҸ9]s"""z>;+t29h"EGGkPav7VOdnQif&Nr7) {cYlmmz<;Idq䓥%1}!<Q6m""""҄&c]ٺ5}λۡJ*\s:w QFʭ!!! WWW0DDDy&""""MJL:qFY_zzesJJu>Mt:wӦMǏ`dw2;([:uBXTeu)'''CiooU"""ʩ\[pݻwM4i]n]EaINN4isf6ݺukݺuwMLLx饗F!>_Ĕ)SRRR&LAt`Q:3vYf^*ٳgXBe1111$$$$$dÆ Æ [`z2嚝/0>!@:uܫY@ QIY"++L[bccH_Ns>3Q~q`ooߦMȿɓ'wٳ]6l=|Q,16k֬SN˗ 8}4~!<<|۶mDLDDDYqT~۷[⋍7FGGϚ5ky׽@ a1qA tڵq[~`eȑ#bbW߾}ǎi}w}@rr)S311?_~֭_~eOOÇg?qᮮqqqovuu}ה+Wܻw/SbKZkת]|ΝJL߿[n|H`0$''t0гgO2cƌN:5o?NQʳԩSbzbcɒ%7oжm͛7_|y˖- ]v}}.\x1;.^aÆ޽{yի=ܹ3rH`ffG޽SNuqq3JTРAl:%f`)++:}vQgΝ;k׮=zY$Z/Z`0AĶh4GHDDT4{Y7l I&UVW_xWo.jKm۶O>.\XtEG:w\\ognN:*_?z)S&L _~>}d8%RDUT)[l6OҲvѲ,K7\fhW^˖-u:ݟƦO>6m:~x\\rHmVYNo߾+W477իs^k"""*<9n޼9yd-GofffӧO7]tII>}q{=^ܼysʠl5KK\իFQTr M~۴i3zoVh0U:u䓭[>yd۶m#FP:33&Mdu(7771k֬+Wm+++www'''GG[n@TSV*Wn޼sCD) 4h…u/+tDDDT\@)S޽;%%e…TF^7ibwVDTr5kִjJpBzꙛ k׮eo߾zK̘swwQ`hʕ+bbb,Y$N;|G8y}||ԕEģ(SX5c,(CfϞinܸ1m4H,Ɗ+t P/@KDDDE|~?\r;q aӧ+c~ѣH_b ™3gZPF8Mv^bEƾժU_~{l5?~={>} ŋz,#G\jգG՘w^k֬f͚ +V3L#Fؾ}{Ƣξo֝;w>쳌غuk>M4hО={;;wNIxz`HX`HMDDDD0l""""҄a& 4aDDDD &""""M6i°HMDDDD0l""""҄a& 4aDDDD &""""M6i°HMDDDD0l""""҄a& 4вS```~5jW#""2Y)::;9;;;111yu(;"""E:""""M4e&""""M6i°HMDDDD0l""""҄a& 4aDDDD &""""M6i°HMDDDD0l""""҄a& 4aDDDD &""""M6i°HMDDDD0l""""҄a& 4aDDDDEaw׉' m۶@>YH7@eXX<}^v-#I] >v駟$L(j4>>v؁\K/ٹzW<sv\r8::ؽ{75k*$6ilSPt蓒𼷝%c_# SK;%ҿ9b4FXXX:_s&F,5oYp}O,1% VZʖ-E휪s >Dߑ: (+E>9B H[3.E&8MDDDD0T*o<]ٺ5}λۡJ*!NLDÇܹ3h,9|FժUYJ޽_ `>3e};yU"=(-7Td%WX1î$({oЯ_?-Z@{Cmn.m""""ҤM)))օ݋doqerU;;?6o:5j@zp1VI]sVR~} @fd<<<>L2(JI}^{ ;(WWWMTsnnDDhu +&n+sKXt)"= ԹKQBN<{=zHi>|Ç-ZhѢ@7bbbV\ `bJ;;;_|`|B(??vuW&& AlQIq:ud `fjܿ?_]A*.bŨPygv&sN" )S<:r@ZDxy]@ >2͞" r \B5KyNOmR_,FAM9گ_?OOϻw^p:O>|F"""-[e˖ 4lzqDoS_ ;vǏ׭[׳gvdJ?c77bN]KGG  U%KV-"{! @b6\t^F|YCQK]*?!EJT ss˕P@Ğy-,r V1)+:oAիl2={ܾ} Qk+wF.^xM+VFFFe{+Wp.QU٦}ݰadUV;ws1K]ŘBIDATcطo+VÆ 5k͖-[+/ZYY5nx˱">>>((UVYP~AE3"| \\Ԫ]sw;w 'բR4_)EՋY9^٪bŊU9'G]_88 GJ+PpaSPP(0K/ j+W{.0 ŒI`^5k̜9SSLO<}v])wT_Ս'NT/` 55u.\ ֮]Au֝;wrNwܹ>}:tJ*mڴILLlٲ^H"$ckR_\W&e0Jz9Kj'%%͝;`0ԬYsɒ%-[_MMx7_~ׯ:_ -X@L7~w}||n߾}С[޽{wРATgJ^jժӦM[u߿k.If͚9sfӦM.\صkׄ }Eݳg޿_KE*o\O\ܐ#`qEEJPzO.l&ťb%>dT^ FUf?_~eUT1b^_$$$V^]TPrի+yExxxܹSխ[7 188~;v˕+k.q TTi#fG{yykŋv>!!UV3QĀRdY>$>%(!111b#cDB'OT5j߾} w%h"ׂ W[eE׮]ձW޽c5mTLBժUů7oT܂~嗃>IhannnnnwKa+OYTq* .l3ɑ^.?lR̆]lY7n4CD@X6cԼys TݺuM(F޾EyK*cV>n""'ApU1.U\yӦM 2e ggwy4!z䍢nݺG UbLE䣱8^].}.HEU(LtY>d\3mTظzj=J>}Ο?_4lP$obbb͛Z(^ꅗONqs*ODDyMmڴ)SLRRٳgu:U^p@^"R&ʔ)3f̘1cŝ:ujÆ [bbɓ;v^,S͚5SODYU}8 F>7L$%*ng J{I^p&N: 9sf?>/e/%py"ʆcnvgHMM.&YӦMƹs2އJ 駟u}Uߪ̙#;b&Z͚5mmm(ffbѣGݸqc۶m}4hp?sС׮]3O<9xٳTPA<8wްa-Vsuu٩w8pӦMGO(Æ ×%$$}&MzLfQQ!ٳ7nܘjժUVPf+̝;o߾O<ٰaÆ A$nݺϟ8qm۶VVVbv2eϟ_LN:oY_~Ne˖K.رcIty+CE-uqV}fXJ8Bx˖-cǎVJ0ȟ~>$j0[I-Z }Δ/ɓ'>}:!Ca){kk׮EzV\HGg#wl{۶y@M/^z ׍7ܹҸq+tMOOV spp WeC'&&ƌ Ãbbbʔ)akk'kbC͚5p޽]1qN;w/}*zv܉wZ >iˆ~ 0u,pfmSf9MII_ ӋXn0(ǵpYn̪a9 _:uԩSGeʔɘʊƝ-,8Iw`F668ؔq$! ?Ӥ&?:f2 dvNJ2̤m2LHqm `j 6 82l$>OxvVw k,_cm bqҥSm*28 9hMb I{j\p!%}wiζV;S禛nt=H:po֭-Խ%Vʱc&+Po>.i8_8_7ߔFGLiu?7pN=]nݪQxsWwltTo佑|{l)jӌ xz뭒I^{o6GQ3|GiGn6I׫ZI=#&}]%ߪOjS I`YP/pB%k&i۶mJݵkϜ5Vp7Jڼy^IwW ަ ]M vwJp_5=PmȄjӌК՚0k#<\ aJiyNN1Dx1pẊSȴΞ饒޿I~ݒ~O)gj*IWnG*pN 20 cuin&ꪫC6TBqK%='O>Y]'<0\h6dB M +Jh u&[rxJ #1y]${)Ig͒{"I-+)L wJ/aKzbS⠞%p[fKxp;O'4Ӓ%8l%w Pǚ6dBiFh0xptqa0oSxL:" mڵ^sN(9*BSˡΑ3:9G5jEB eQ~`zISvT*JjZk֬Y|4#6TgFlޅ݅:& -Z_hOPR [l;M,Ԑ6nXmgbӌ@cH8@&TfMtN?j@&Tf0y&D~8t!6`M3Ž; 8ɶm]I9~8C2pjLj@&&LM2!6dBlȄ b@&&LM2!6dBlȄ b@&&LM2!6dBlȄ b@&&LM2!6dBlȄ b@&&LM2!6dBlȄI[w9En}|4fָX, rDl FWW~{phٻ3tGKN;f'VEs?)-{작*iVI Q4؈6RcEjZgYG@~զ!) ;R5KtiѮQy?ӽu8";4;xoy7>߰5g-}U*{b"WdN|֌Yȱ5663mvGՒv;wvΏ]۲]E_37?`aݵǑ1Uǃ+ͅ '@l ?&}fcTtg wl[ՋMҕkl7Mj>c͙vo4\ѽ}:S6^cVb@~,ՂGoΒ^Ʌur1̞Z҅sVuMXfQ*,^]h/*rm>5%.ė<\r jSIT L>]'KgI\O\ϸ߁Ҿ邤`  ݌J2gvm{n_3LaO6&Z9੡銈[vJRl"rt9Ţۯ+LҡA}hM$=AawThA[{MIK?A)W.bA% ~ %ugyolݼ&KfE%QmEcI7^S$ WTnӼN;@yq&|a_:KOo7=9ԚBjSa _TqU"oaMskI u-1Ih:^%Lw_gs:s}m#+YVr}0K61̒jB9}GtZ:n%I^H;?h]xnHt|Ϗ}9>զ6\l"6Jtxj$mXw ˫i}6lZ {O7o L^mZ\N/^;\n.<6۟\oZocuVuQ_Ј$r.]մNNƿr+M'r*0ix%HaeXw>v!%ܮo~Aݳ*oVWY~Kߪ=OwA["=ے9y+p)Qm9_ F!'Y޻5dZ|Lmz=E[>_?][au˺%s4Ck/±\rlC)X=chE[{Yo3Ny"yoP7߯nQ<5Ww_8Z SW3-:ovojnP@~"+ }[Q$+Ҿ~Hߧo?ҎOi}w߫dJGGGjhXnH9?,*rz,zFFF~cqgOM^-%lܦUt8M֯Z>mnMN2Cʡ!`T:OmL޵Pr::ޏP;zI'@~܋Ƕ)\mJHC/ўV jDdBėщ@6)T91ܟħn9iORKk$/HQmZ)bYݺBQn2!zB8>l&)o6rQgƟCl ?b Ksۖ'+ȒS:?զt&IsNEu{Yߑq`%9熇wu/|GۛS3s^;邑UK;rt\&3 A.̬X,JRޞ=3N:W(r[[[\އ̔NN%=!SX6Uhɍ&) b@&&LM2!6dBlȄ b@&JǮXIENDB`././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/docs/source/traitsui_user_manual/images/InstanceEditor_demo.png0000644000175100001730000015554200000000000030562 0ustar00runnerdocker00000000000000PNG  IHDR6p[c pHYs   IDATxg\SI6 E"*(UQ,KYwq䶣kY]u-vQo{W+*({JP@iyϓ'ADM^;L&s&g5^̙)(( &J:h:h:h6DbX\߀Ȗ(+++))j  *>_YY)dS";IE4TUU555~FD4bTMM*Çڲ#m_@URR[hahh}fhjjȱY!TUUl :y<'pG'BWUUE>|>_m"m|tuu+++kjj "HTQQѼyy"H^ "ոUWWqw*D4 9~'c;:藝rcw䳝:uCri011ĉri ŋO|5kVclذɓB߻w/!  q}M P(ܻw7oLҺuc%%c\*׮]C۶mkٲ%ȑ#iӦ5iҤ8lll/_ؽ҈qĉ/_|2//yL˗//YӧO7JWo߾b BѣUTXz=௿g''= ޽{z|U,{ȨE9WAAAAAA֭~{~-dgee|_|H7ondd$Ǜ!޾}+ ---OaaaNN500*j~ظkjjy<'i,++D&&&}*"{1RDDDL0LIFFFFF?sG[V\\Le_>|8wk۶-SXRRZҢE gg˗1޿Jٻwo~~RRR!Ç;wڙ3g|BH&M֭[תU+ZhvǏݏ455>cƌOD;v3_nC 5k'߻y<|\288C_lك \xd ׯsN^^-i֬٨Q{ϟ?h"f???eWqqqY.;|///+gϞ'NЏ@166ҥfͤ*x~SұcYfuҥka]D ?~<֭[7cc㴴7n$''Or>}\2##?hW?ԫZZZ?dIMM̙3###% ]|Accc&J bRH$:~xII͛F_x1f̘'OH{J2?p@ii)MҥKCCC%KJKK:TPPqƺ+ E"QuuTIee3"""rHtՄ0f,M0իWG#?|m۶/^ܹSEE6(yR#\=zT޽5k:td;+V ,ɹpӧOw%SRR&LPZZ*Y9&&fҤIAAAvvvu_Fo FVZ%Ϟ=;ѣG/[Ν;G&|̙3ʔ)S֮]X @^CFyʕ:ر&}ݾ}{֭}!}ޖ(ȑ#FFF.\5k!!ŋ͛7_n]XX؊+ڷoOx/^ja˖-CݳgϟojjJ9uԉ'ݻi>sqqٹs;wvխ[7ځm۶BIJHHW`޽VVV کS^zp&Lpϟ=z;EDDܺu};vз,Yׯ_ 455-[6qD:,(qwE󙵵ŋ/^i&rVV̙3'N,--UUU7o?sy5iҤ|!]hũΝ;@PUU]vmXX؇N>W6BaRRR-1255Yftuuu33Z,rssyUUUرcϞ=۵k'UUOOoʕtظW^QQQ=x ])Ѯ];:FQRR:t=U=== !o .666CX~aj][VPPBug5###1cƼ|r'Ny{{Ueֿ@ xɸqhwBڷo?gZҵk-[ :annbCCCfI q{{{YJKKO<4Z\BgϞ}ҥԨ(&wؑuVڬ̙3y<^pp3Fъ1!D]]=$$ѣ-%\.wL5.Ç1cX[[{xx888tڕ._;v{ݺu'*,,r\.Ν;tssڵe'?P(\nS޽۶md>|`5k֦M8!D <~x2d۷%+/^x߾}7nHEELJ\SS#վdӧ&!A;Ɔ[,--&aONJKKsхkf͒atH$JNN%W9;;B222B:jBP__߾}G5j'W{L>G-[FN2kiݻ3ӧ] aנ@ tҌ3) kjjB!!DrCuuH$*//7nܽ{ׯ_2ԩS~~~999Ly^^^@@H$bH$)&&&00PrlŁu9sHN4BJJJvv溸.]\r6{111{˗/_Ƹ8??I&I|Q%%%%%%هLϗA:))I6v899Ɏuؑ$&&JBsӃՇ/tƀ3%ݺuKMM---駟|||zݱcG--=ݡC###zh4VJӛ:u*cuu5]K#u i!!$>> >k+ի߿wss8qAlmm)7񾾾@ 8pX,:t1c8΅ ;VQQk׮KMDY2zhoooCCÈ;vёDyf\]]'Mk׮jՊE͛****''Yr oֱcǎ;N>֭[G${uuuܹ3!D,_rV7~f/))6qďT>HՕ3fx7nܨ<}ӧ;u4iҤ~>'-qliٲ=JNN_|˵),,,,,ŮFYf|~`````{^zYk׮Ǟ]tyX,0`Ν;@n݊N:UYY$@I&^;88իw/j][y`dbb:`gϞΘ1{zzjiiu @|||bڵk4mذСCvvv630yFّ$r%%%?IR[ɗmbH{O&$]m/ͺco"{,Yr9~"333(((((HSSsĈ͓)V'Of9Έ#؈#$nԩST֦Mmll,YRUUիZ~9+[cӦM1cx:ikhh ="@b1-LSSSr`gԼ~Eƍ;u &رoZW]!򷳳 WVV>vk0[lũ7n8~xaa7o\!wܩWv픕E"?n5km۶;w2nFsss\1͛7ZI3oU_t";NlL^Ha6I/**1+0^|ܹ3"77={~eeetiӦ±cvErӧe|ht𬺺Z/~cp8~~~W\wK<{L_]mb{B|>圮0SSSKk{X{[nM<#ͭmuܾ^0T >bbbÇ_^!{/SfͺwNsǏ^,]v .7&Nwބ'3cǎ>ܶmH$277wqqQP-,,322RRRD"Q6mu֬,B'266 pqq122Rn޼ٳ Ǐ?b;w={bnnޣGZ? /((Xpa^^ݧ-66/^ L0k:6h|>Ժ(n̘1t디x"[$&&GGZաC7o\xם|[뗙}#G9rDB˖-nJkii=K,a(++{yy}쎿uڵ)___7Μ93;;ҥKђ/,[LqZs83fWVVXbŊto# یRWWݻWPP@WOqW{„ 999'NHMMZעE72ٳgԨQ7o|f!䧟~0`m5ѹzjvպ7!y|/]hQQQt5I׫< ]p8~~~ﷲZ?={ɉ)g"mv߾}?#cQ.Z"ydlݻ۷Or=kӦMŋKVg|;YvICBN>}˜MI~(KڥKK@ PRRjժՌ3<(%BȚ5kƌҋ#\g-Z4}t4[h/={Vj.~޽^֭[[nǎX NAAAcEGG!RCP?SRR444^+W2e 6{l$>w zP\\<7oܾ}Hx*)S\ܻwo>[[k1,,.ȥN:E]zu^ha||Ǐ}||6n0 %%ͭQ իW͚5155MQwq\mUPP@[r%}rssMMM?ZCኋ Fjjj-,,444)|X,Օ,WD<2%%%999***u*""Z^&5ٹs֭[~͛ .l߾ׯ֢RRRRSSSQQQSS4I4$6wڠq Ih Mq " " " " " " " " " " " " " " " " J}** Q^M_yNAA'+u3ʫ)`z+N֩(4$" " " " " " " " "4v_ZD28Fؽ:JN5M8@M&_7MUҽU<D'4Rd3>fR^)Gm"U77J"/MP</ yrKVhpE`-9~!•V6v(\k-S579n'ݽ>"4&]]g+..V) uX éu." " "`Zڸqcff˕k~BȪUtttRh Oӛ3gNuO8AGD  ! f 4P8y|Hjj%K>]>y<^\\l圜/_~҄*"u֖r[n]w177ٳ?x):ƿ IDAT\.޹s'88߿B,۷o߾W^}IMM=ӧOܪU+777+++//|evv_ h k.UUUB߿ɷDFF>\2! X??@Pݺu믿JLLBp3gg̘;dddH.11qСᒅO<>|xlldŋ{թS7o~Uρ.\Hynd?^(jhhlܸ1""ݻ/622"YÇRoٽ{={/^lllL ]z###;uD eݻ_DUUu[|*_srrϟ'D0\ 6h ~~~ׯ_?2lذС!dbѣG}|KfBCC[jE722#(++={ޞҮ];777gggBHLL ލ7fee>|w޴t„ yyy2p;w*++3E(@QRRڹs'?33c5CBBBBBb/VjIտ---Bg1--)q͍gL#vvv/_2+Vw^LL?\ h tSL)--p႒'~M/......))p8P(U`2lllj]{4PxB|Xh jСׯ_?wÇw1mڴZnذʕ+iVMMrꐐ@w8wܹsjo``P\ 47>z(77wڵ?l޽{[ha``wUvN311iٲǪbꆈo>d@0eʔAIUذag&M5k>-Dr쌽P(_2| .'MDILLܱcԫ< /]gh3BZv혓K 4ŋӵZ%B5UWWܹS=qrr"<|Ӓ%%%C ޽#Š;w`@ݻ#j3fxAAA͛7 v%EdΜ9-[ӧO߲eK\\\vvŋ}}}޽"ǴiӦN:tГ'O*3@!4 Ȗ9?ÇGDDу>`@N<ٴiUVyxxt&wLeZ(y@tYZ=zL~+I%kHZvߵZԡCj;v8vd#ӦMSSSrۧ??|)N=w]W]]kBHVj"0֭[UUUJJJM4AѵVUt6,noHqqqxx57ٗFut4iBol0zzzzzzu6 G`D4AD`D4p/',F17v MxxT &=&  NCjdWr|Ӕ N[{ u gKW"(afC<1i k"(\eҽܾة{+qe P[s;ISܮP[:8rl"Y)Ooh3=a(D4D' " " " " " " Jcw"##e bq8laϞ=wFD4h4/16v>[qqOh:h:h:hHT]]ؽ`#Psιs~rk/^xԼ}V\4b@рu*++OzqQ===e˖M8땕SN}Nu񩨨L2{r)`m۶ybffv!|FڳgT۸qcPXjjjrsscbb X,NOOOHHx}5˓|󓓓_x/󖴴dܟЈрuBCC !jjjGח}1---Bș3gjjj$_}---777KKK//3gȶ#===۵kױcG???6b.;`~7.Ôs\ooo@}vkkkggg77mѣGܽ{ܼgϞ?S$8;;sK.Bbcc\.!\.wR-ZZZ~ܹ}1}SL֭[Ϟ=Ə_PP UÇ"H`%111--ҥKGGǏUrss|2Nj֭-ONNКW\rss#XYYEEEEEE?c6lXII_hhݻwW\iiiIYzɓ'{́B޾}+9vM>Ou( ǣud"ڻwƍ!sΝ7o-رO?$:DC[v-!円r\BAϞ=ݻ;}!ήO>JJJ'O|f͚EС!dc>X,~Q߾} !&&&mmmBFV>͛sss !/:u*Ӳgiiaä64hО={8!ֶK. 22?u<==ʺvjllꆈ¬k׮]5 JBHΝ+YvϞ= ***5 !41֭[gee%iRbWpƍ/_gg琐.ZH$҈?Ngeeeh">"uM&MLL\\\"""a\\\RRRNcPOh.&"11LMMMBHUUUbb"![nGrQ?јEl͛3#p_CvNMMΝ;󋋋KJJGR8;;˾ڹsgz Ɉ,c; CKKi /uԌ|K||<]/oggɳA&CC͛ekmnn.[nkk{ΝR:kaÆ+W8襐\vq8Xs___Yf(#Oh,h.t؆PwM&BxQ YSb1ݟ,==wn- tuu^5˧xpG'Kn\gTTTEEǪ BTUU]]] !tcOɉ_*v[ZZ![N-,,6l@[4iRrrrRRҽ{Ž9ҤIzvL=}{RR4;р]TUU=== !999ӧOX~Fg 4iBo}f}:t<=22RfYYپ} ZK7HJuA %UUUUh~"< /]Tr:vtӧ24;!̞=;wnΝ>|1BrzaBBP:r 0[wؑǟ?TZp ,X(JU^^^ttdݻwRiͣv۷%4 BvkԄ:L"7ohj*--mժUMMM//{;֢̜9sÆ ˗ō;m۶JJJ)))Ge6|8p$_=zMy$]6Zlk.fww?>33sرL5}};J>1I' ,, !#G xϟ|T@=޼y#;}vX|ܹsεnɓ'g'ׯd۷o|u…ko$f4##~Y;P7 lp,XBoҽ{+VXYY9s槟~bV&Mڷo(ڵ-[dJZlے~)))ܹsĈ̆ ={;wJr)S0$tɓӋVVVׯ_wuuePUU4iRPPl˝;w>x𠋋 shTe;;ӧO{xxHgnnfl}짠Kԯ_?[[[3rH3q7&EzufffMMY֭krLJiiiffR֭%s,H[XXr?9|GAٳ9bddD NOO022jѢǚw?jժR|>JKK>D\.+@1 Hwww R\\޳gOŝvt g֮eeBNԤI++T$ijjg*G/E}}>auXᡦo"<4v/NAD`Lt@c*..n."4pX(=\3lh:h:h:Ѫ 8[ז={6==E}k.G#l]1c|A%UUՀ#F4@7 wI1bD6mУh7o!<,33ٳg=ӧOݓҥKD4`h%%%g ڶmKMVRRrooo77{4[ly!goo`z|DyEpqqq=zXYY}Ν;333 !͛7'D"\BTMP(Ҟ={,YB9X,&ܽ{ty#(Y8gΜ :.\xٕ+W 7wݺuk߾}L BpEEE666Vb |9-uqq]trʧ.$|n-%%QM__Ij>***VZ%ڴis…gϞ[NIII ,\V4hPTTԱc6l%֭}AAA>aÆJzݦ{ _x1!$44t۷o ԩ!$///44y͛7|>!$***''s?5|n-??(nӕgk֬ٳ'!|„ Bի2--MjժظUVL#޽  >}Z__Ү]>}=z4>>ӦM#hFFF@q2`O.[JnݺSNGO u1s" -**\ccc9rq/ٷ|l`LCCcԩwMII <ֱcGBH^^^qq쫉 f͚})/pM]]W^%KZG(N8q˗/S%gϞ}F]]>}̞=R]]][Ÿ=+Y@t}̙3ϝ;'P(\r%>~wDiӦiӦ/JV~]HHT K.߿YYd9s@D"~͙3/-\0//( o޼_~ENOwjk+VTVV9rj׮]v풪ЦM2BVZ5pC:t)6l@ڀ8{VWWuk֬BCCW^nxs%ٳg ?~RkhkkSzKVCoĥiӦݻw;88H?jԨ۷oKJqvv>r䈓Jmm%K,Zi^ti&&&@ utt<<"U `|HTSSCWp8%%%"'⪪J@ D"H$"(+++++պ |X,(--ɡlԔnXPP^QQoll_ v>`jjjjjJխttt|D4ZzNi(llllll^xѶm[}}}nv =D4i|>?--p…>iFSSSA=6R^^VQQѿ/ku_UUUcw.B{Q4hd"ĉ.\PXXui߶m[ll,o߾ԩSebŊ:صkQFɽ{O|ذari0$$I__x~bٳ=*R۶m;֦M֭[|ٳk.t[n 0L͵kͥ &LpcuxF'/PjժBBHvW>|xDD!ӧǏ+VPUU:p@&M!8ubccǍB]UUUZZ;#KKK+++sf=988$''bUJJ t˥&M3gαc .\0tPZ;z(4VݻwWUUijjyxxӧ;w.;;>>>Hi&Xs!| nB! qqqݣ/w]zvZϨݻ/Xf1ѣGi>?~7ۙl9Q,_B_lӦMQf!DYYy\.WWWիLy^^=8555o& 6gիWc{b"<<|ܹ C"ڵk)Sй >F,sB$eee={,==](ʥuxmڴ޽K4*⌌ P__ׯWSS{g*++ mll۬Maaaee|6u!|m޼y|||ZZځB7QIIurm``жm>}HdccC_o> __ϟ+t"t[ѣG{{{BBCC?Ν;׭[7OOOGGG;;Dmr\6!BpݺuvvvNNN{vvvn۶%Kj˗_q.]/ʾDi۶-- k 444Ѳ6!8''Ν;O>CkDg- !EEE?NOOWP̒VFvv6cɌkii;v,$$DOOOAςh:fhee5hРSNܾ}wR5^:i$ x<󓓓%ϙ3'((HdΝ<oݒ/޵ke$'''f(G"PU,Qnj>off6gGΝK@oD/ SRR<!$))I  Jrd.U-۷WZ#(((""~Tmll|~nݪO>իW3_B4@qq1L!9qįJaSz|WWPMKk׮M:U*؜={dr JFFF֭#հaC깆eee4L&=ܦկ"hnŊ}~|gggoǏɓsΥtuuKJJ0WW+W O,,,/++8{lM2:::4:~tƙ5n!!!2L.L&Cuԡڵk'GqFڄOOϪmT*9r+W!s߫vPMzzzAAA%`o޼f^z8d2by&?p%$$ >nݺ5m v ,|J<_~!dgg+&Yr%-\6ʹ۶m#hkkӕUh43>*Etiii4 E޽鵸ooo5ĉtNLΝ&Nxqǜj*z%7,XpKؼy#>f6mDCԩS?M& }obcc鰲P/&@pѣG'%%ݻw޽GMLLΜ9Ml1rǏڵ+33sȑ<_~|rJO8)ttĄݧE !AAAmmm??? ;;ѣ_655믿>|бc:::~W+VH$FFF֭`3fN'\y !E^ٱ%/""?~mW\ѣ-ի--- 'aѣw]Ѽ:1qF:j /x)=zLDҷo_???ZXXXxд4ss-[?^ eh˗/1D+V8|0[ill-ܷoߢE9Ҳe*$rssiZojjZ~}##jS$h⨨/_4mڴy맦׫Wu~nݻ^^^le7Tܛ7ovС^!%>>s΄;w*└sss6JKK|L-Ɉw=~X |ιs$}:u0 BOhڸqcϞ={꥘bNx^^^O 6tuuUMGG?g5iD,EppiXܤIBJCѣ1c~Z$=}tٻv"T-LMM6mjhhW;+6m';0 ͛7&L{Yf}xή}K.w(XWW '''&&VVVfffԔ<}}}KKKccc###]]]---g1gy{{{{{D/^5nX__٠Q]a``PN’oLuuԱUVZ,=?8mmm{{nJ( Pkhh(H-Y&ؿBgP%%JeQ4V5X"g͛r%LA|r8ȋqhh!Aqhh!Aqhh!Aqhh!Aqhh!Aqhh!Aqhh!AqhaM7\ٹeeez^5޾}[m/QNN΋/ *2;;;ccjj'@@5VVV@(x mA&bDcoo4/@'ԀL==jii|~&d2T*-++_~iiifffÆ B4Dbݻ_|9e;;nN5zaTTȑ#k-JBhxW^$h_티f͚$BmmmWWW777{{{>^xqѢEW^:u_;wn톆T(**SN%8),SN'OTujB/… {~Onܸq#۷BFQMܝ9s&..zܸq5UA3 Bh_ڵkk׎733ٳgEEEIII}=}sz޽-[͛ &T6m"m!jAAA... *t?!ܒ۷/W SL煮xO0X)!D]H$VH$iiiBa'L&HKKDc---+~k&MpjUh;%%%_.++kܸquIҌtCCCkkk]]Jo*???))I*بQL/ݻ7!8""UV9::lٲW^nnn͚5[hH$RH@@sϞ=]\\'H5kfnno>BHII޽{-..mѢCϞ=׬YSRRB+?~ˋwԨQgϞhks_{;;;ŋuСK. 45j9 %VR|t߾}[n...={𰵵?~/Otss377 "Ѝtؑ['<<ֶs]tiҤI>}BBB*t!))),TzрŬpهV۾}{ffΝ;.]4iT*e%s}H$H$>RT,BIOO2dH||zj܄ɓ'KR--ӧ^reۿAA믿y<㓓i\rʕ`B:Dӧeutt~Cv޽YhϜ֮]{={;;;۽СC !Ϟ=c*JwFϫV kѢڵk޽{#FBOֹsgBH&Mb>|x^^%Knݺu%Khkk><55UE۔H$.\PggЋEdb4gffʕ+ !;v<~!ںSN}ݣG6l0sLZn:-_|t-[ܹs~r?֭[/^taӦM+--  VVV,hk}}'N>nիWw&XZZ׭[ԿÇ_zU?dȐ+WJs9:: ..aÆB222֮]K9uꔩ)!y{ W\\ONL100 q!d͚5Bz666?czz͛@Q ؾ}@ oY(}A?>_s̙0a9s!|>Zdd$۾`ndC,Zdd$!]v,>fΜ&ѡĬ,VX^'N={Vj-ӳgOBۉ]gߵk;t@ ۻ0}-1VCjtaΝY|FߟƈrOQB/sg̘qѢcǎ㏬F]|>)E׌VR999l'R IDAT_dd-[nݺ{DҶm#GԩSGomom'uׯΝKbEVZBؒ[kkk33}p;i Drk֬2d"^rE(?^u'JftuuKJJ$R}Wiٲ}d2Ǐ۰˗_^somnʕ+J :O>}('!UVrssYLбˆ *rrr bxi>@`jĉln$DD"Qxx-:ysd'NP{x<ٳCCCk׮Mb5UV mÇrҰIM;v!;w.((H"|"66Z"rǯn9BN !SyPS}fΜI{_vZF,XfS'Ov-)&NMjժ+W~ϟٳgϞ~~~rccct;Lƭ\֪Phт1{lVS&M2A<oȐ!@:ٿn7 fϞ;s͛NB4zƚiSNq Isׯ_(@5AE3221D"Ѳeh5-:t 233ׯ_?}IJ2@ח.~If̘nݺDDD,X湕d_2e ](w߱666tޕ={F# V m~ $''2СCO>=?@3Ua Wn/BIJJxYf}i|ر/ۚnT;//8www=Rdw%777BL&4iҙ3g7edd~vLBHvvvϞ=Y'ejjz~B/_>y䢢 B-[6eZ- য়~bcN:T\ps'\tInÇkֿeggwTTT~X`Ϗ(BH˖- !cƌ|A"##۶m[V-@PsI$°0)} C'd2v#uiӦqݻHbjjz?_~mڴ۷h iI0`+W<==!"7id޼yr7o}1cM.}}|TC2L*v4 z@XcnnN x:SmΝ,WyR7o T֊BT81v^z6lXQD"IKKӳ֮]crrr͠UGii)ϧw TڴiSPX,NLLk BeƍAAA<ol#5]vxxn6TBH$z%WART*7lذr&fB޽{~-!A .lݺ%8ޢE uT --v&&& D"Q~~ ,--i h&LP 5kv[[O$\D%r ---===###B4k׮}X,&|;;/]} H$zEYYYƍc D8ȋqhh!Aqhh!Aqhh!Aqhh!Aqhh!Aqhh!Aqhh!AqTvKUm FQWV255@dggWզh51 qEO h!Aqhh!Aqhh!Aqhh!Aqhh!Aqhh!AqhhGX jw]BL[Ex:tPѾ^^^5 7oT&B/QM7~eE84B4 @ D84B4h_m۶mN8a:::5݊ ۳gH$hӦMUmDWWWE;wɓ'+MJ[ly'hd">,< MNN6003gNFM7nܸ B 烟b ixo߾6| s)))Q;uT޽央uvv#ܻwӳīWѣFi㧟~ӫ֩EOOd˗/۷رc/^xǷ~"Wyտ+!L(//+tϟ?BnܸQ$"h{}iIIIwx Vnf`׏wʔ);wd}IFRQ֭322Q%I$ߺuݻ^j֬Yǎ зo_ʿkfffyС˯_FFF׭[uJ۳g??؂mokB,Yrڵk;88̘1cĈ|~͏޼y7ǎ+..VSuhgΜ!(&|UzСCYɫW^zu#G4jԨZIݼys޽c%ϟ?evء+:i>AaVV !?Cfjvݻw+ý;vҥ온_> .]D*׮];JTWZZ:lذgϲ'O~ //"_HJjҤ f AAA...ѪۡCv榁]hgΜ2dD"!t޽C666AAA񑑑nnn5jwD"B5jܾ}tܸq[nּ>}lڴӧӧOJKK[j!ӳjCJlyذa4>k޼Ipp_URRFDDU=z^peffFFF̻vʕ+ݺuqU׷o߫WB<<< P~߿_љ9s&\TTD6mԩSGqk-[+񙑑ɓ]\\CBB:u6ZVV6lذ3gB;wwO=z4##ʕ+lOw/Jĉt钖~7oޜ?~̙Wʗ>|߂W1hРELzr4ʼy$ ߵk׏?ʗ-[6mڴ{{ѣK 6i|6|s~m۶ 0{5Rg̘QӭnܸDiҤIdd$=C ֮]S-[c{hڴi|>Ȑ!իW33.]T|'GnJM4>NǏoܸҸq۷ok˙3gN2eϞ=N5j-߿?7o~ Zx޽{߹sի׮]ckEB͛WVVFٹs';vӧOw5k֬ƍW`q񱱱3TȤ$O/,,PPPì=rrrr#l޼^9rDRG)+d/_ɪ)///&&&22J+\8;;ݻWng/^o߾]UEGGs{֕ KMMdj6J)))aaaO>UgL&OHHPs,D"y̓^|)lXCGE !V!SLܹ3!UIII{! :Cjmm>LOO/7U\\3[e͛i|Fuo!DDDpgd5;/_NN>}@ XrsX,"ԪU+((N+]ѣG7o$ruX,޼y:!)mkk۹s.]4iҤO>!!!۷o7oN'zq{ΝF5j&sssss;!!!DqcǎN>](vݻw455mժuf'իW޵knժU n^|˫vNNN[>tμyBazXIVVP( tzDvLLLڶmkfffccsĉ'''G􂃃揫sqqlٲeڵO^TT$geeU~}777CCCOOO/2=Dk֬155kѢEݺu===ܹ>޿gϞFFF-ZhժU:u:tp-jתUӊSI!۷ݻ7!$000%%EsWzxx:99ժUW^5UTT4u[XX[[[-]T`K,666͛77222dHbbC 133srrrtt4220a5aqqP(ŋ-lذ]ݺugϞ4HH$˖-c 333kԨј1c,???PJۿzRq)_-K$uDkbZ믿d_}UE"g==Yf;w_!hEC4>,T* &Gq455wDŽGGG[[[t۷_~]ːrJ8k׮M6!8qɓ/;)g̘۷ UZ_Ϊ-[;uԉhZjBڶm[M;#+WEB_2i$BHVg`\vF(͛7ׯ7?`fϞM144d%W[[[{U/yvp̙@ȑ#WX++/_m۶ÇWlhh*^s[tpڵ=zĪ>CYfffXXXXXXqq1-3!eee=D2lƍp\rUKII.&E"Lz7BHÆ SRR +WbU.((ׯWF0*d{UZȑ#*W_-:?⣱3fВ:X__}hMsU΋/h'ծ][FFFQv6P(~*++S:5^ztȕd*+E;y<^^^+ܰa!‚ݻww)ˊG[n%IIIwhh(eb1OW7!֗dlwA]Φ%lȢQF7nWBU+'''00P͟oh!Z\\ ,Yr֭۷o/YZXXDGGӚYYY3]pYD%QQQaaagI&Wm~$(VXTheee^iii-\jOJO?zy! 5֭[wܙ1cYc?S__ё+Wԩ!;v숋W@z v!kמ7oi,A[[#GDEE\<^z۽{;-_|9{9{Դi 6DGG3VV RSSoΚ5kcbb֬YC?8VVVIII& 4gΜ =MCTXXHOf͚]~˗[lNNNU Ǜ3gNZZZqqr)-\p!ֺukzuH 8ԩS;v`]MgfUnݺyܹs]v-6]O:;;oݺ5&&ѣt ҿm۶%&&&&&hy+eVyӦMG;VxUMҳgOZA'jJul=vXB@ x1+|Ubb" ټysW^GHXbŃ._COuʪ7&ر#**컂(hzz8mۖ[Kiٲepp0nڴiro/_000۷[^fR*6EOLLLgΑ޽{s^JUh"DUPxQn}fiӦwܡb ,@"WK/ݫnGy!Znnڵk/3o>}:+Jl!CR){hѴ<""ݻw-oٲ<{,-QժUÇܧO2>$W^M^|)׫ڰaǟ:uJO]f;XNDVB0**B]7t!ɓ ?ܓ'Oi&CU)h .В`n 6tڵk׮_n޽{wu5iРAVV!Eyմh}OOOgszXPѣڵkWHk[(rvzXZZ}Y͛7iITT- ,oOE~-ptb^^^B6O&1Zɓ'RmԨQ0ҪU+rv6jkkrZd }hԳghekk댌 V.Hذ7D[TYY y<ޝ;w9"w޽;wlRWyjժ% kU||d__Ϟ=o߾ѣG߼yWk!:ƍR8p fQG_S{nnn%Na_ժQF111cǎe)j_~o߾[YY͘1ܷo_nԑmڴiѢ+5J'lܸQnǎ;hgɣG۵0BH4dn!{.?Dl5D͜9ڵk׮]c96iǏgddj׮]SÇ;w֭[Ж-[hhP777gTb n\Zh>UX̾[Cٱcܧ{˖-/^hQ VɧR)v)rV\IÇ=z|rgggdD"X'h̛7Ã[pBRg}(>Q\zHV IDAT땚2e 5a2C"ԩSgӦMϞ=KJJ:r_z$ɵmҤI7nͽuO?D),,ӧ1Wu9aQQQ]vekeWDEF9C#""_޿>_VV_Qq$?/%醚D"QBB!r3v-H 2$88֭#x͛7W(ڵ.:4cƌ;weɺÆ >}z֭?K֥$7}\o޼ͥp\F&%ND6tRԩs͛7={v(((زe˙3grgl511W\A=vZl?idrD"QLL !XKؘp~ؼI+A!+..^jŋ'Lн{w{{{ނnݺڎ9ۻ}J'aWvnժUjj*ChB1:q޿/wYۣGB4*%Ԕ cTS MKJ:t]vٴ'SNtVy?N9rd)UWVV]J}͛899Ʌz-[N ѣb0޽{h1{;w~ٳgItDԩSNLMM.]ZPPp1v}ثWe˖-\G666NNN/^=b?! եK.]<|Ͽ|˗]\\.\PM S?!+bEYYYYYY/k׮ rTΥKh|֪UO%UE*mu.SRR,YXvX5u6ѣG_Ço߾]$,cEwQP 矗/_)+,‰'[!AW4Ӓ>}ٰylll<㓞M;ի7nܸٳgs;/_P\\w޽{ :̛7﫯R4RzK''/^UP!ҲN:޽+/ &i]LVVV*_9T7o(>*inݺEhiN+ׅFO3"4!!AiMW|ťM^Ύ;zՖ-[9sF1vtRqq7!~$ɽ{}QXXX(K,ٹsgff܂ H$KJdZKC.Z&&&J9Ttqƛ6mZt+OBHLLLzz:B4:27I.˞\XXX}ͫ쌈h۶O?4i$ŋڵcffffff&&&&&&ܼ;#77777qu!??իW\wΥP,gB gv} !C|7۶m;zhttT*Zzm֭[Ǧ9sÇիW߽{'Hnݺu-OOϗ(=2 V+Nϗb]teuSJnSSS (SB"ΓuiBȨQn3ԿU}+?*s8qbڴi;;/*{@};wȅhTlŋ}||n޼yLnݺuЁ YԯOχ:r-v.)=moٲԩSJ~P(ÇXp! O:կ_>w9~u뢢RRR͛lٲ1cp6j%K_ ϘD" ?kuj)(((**+uqq4iڵk !?]{bGMs`ggtE3vإK~;D"iP!?,/KSEoNNέ[vqҥiӦ}W;siɓ2,... `֭!!!۵kƴiŋ999tl@[[[9C4]b)u/^ 7oTذgdddff;E[nR)ҒKbٳ~-҂iB]hh`з[[[_t$M!ػxSG-J4hp5'2zg]lƪ첲2ccߟ[Ȗ*&[m۶}6諘J6&&&JVZZzĉ-[~ʺuN:uڴid&X.?ttt7oNwOr ]7pB:x?]" --ѣGGFF^|*..޵kS޽™ntwzV^ͽ Մ盛7Mjj3͖]]]:MU/ʻՕX,1bɓΝb0҇o ^`!{n9;;/\ǴAE _6K$7]nitt$HtwࡡH$~yu&Te˖-[M+wٲ1#LHH B޿CiѢŴi֯_޾}R1 11Q^Rqqq7#"#88.0֖dw޽]˲2{YY]i7Kp0|_/^BKMY !?ibY upsss{>o<:Ch֭_b~'NtDRwU.$b[4x\W bEB'OHsB<Є̤QD?IԡCoZҶm[4m4[5̘1cƌSS 2!ɡSD???B!Zf WĖ-[NJ;/_hńPHJ[r%[AX^Ҩ ɓ{BL&=zEzBHlll߾}̮^"566N:ƅ"xXzŠA5jb ܾ}3g}m۶!==dżh""[n111ߟ\"d2TJ6}}m۶ѱ6fԕb?mڴٳgOBB… X[h4uϜ9Ͻw/۬Եl8SNiii˗/Sp7nM999m۶-!!oҤ VXXH9 ۷?~@L)ELӟ[>rJݻl!rhȲ%&&\ZZʭqƩhLhhѣG׮]_SתgϞӧOsȥUӜ;waeXWL{nZܹsW^eǎmX,f9ӦM ŏ=bw&&&ܻӧ߾}$44tذaƆze2 %qi5ىH?˾x='C@S}W*o !믿f.JLLqqqҥKUv(nҿY5o% 6olgg!ǏC5*Dcn'N-OHHPzfҤID]\qb = @=|*߿?Q&nID5kL.{b!*))\fG}}}Bnݺ+{f (jΜ93f!_}p) ѤR[uB/_,0ngEHH&˘JMǿ[nݺz2wڢ)]ZZJS2N"srу" ?x+6!ZzNMD"3ԩC3B˅hl [lI$vD+x{{sFEDD7rZNߨ3gΔ7T1alEb5]]]D.#GN]ݐx<=}B\]]O8Mn$6mm۶C666욆bbbBrrrmڴٿ?&;7rHK>ZjM>ӧ, sUYhѼyw֍u?hkk/qp_Kqk {]gB͙3GWWr„ r{{7Nn:9kڴ={v~*J(( f&AAALJvET_@`oowYŷxΝr7ի7~pll_GCsΝ%Ki+ȨgϞaaa좜2tPz6zVZZ7o|˗/+k׮ rg Boo˗/sцݼycƍ;vxr瘛 <8.. \7o^t)KbРAr?sLY?2թS,J$:jhhtҝ;wrkYfX|9hڵlqƣGh^c* Twl[liѢ{{SN)f_*oSZZZO=z4[曻wSYX8߸q֭[i-w ;e˖>ӧk p}!oۊ J*Z_ ?~_~dII ]oީSׯ%1իWRRҶm۪v=o ~dPhgg6BC|>_Yk>TPPP}z޽Ul5nܘFhիȬ,''-Z8>.OD*J@QDDğyEl߃ uƍn'/h۷o;HBfϞ1ೃ !gϞ:::+/ݐ!C5jԧO//n|R[.\/4&Cq0Kn !͛r%2:T.{. hE84B4 @ D84B4 @ D84B4 @ D84B4 @ D84B4 @ D84PJ111rqqM(j޾}J;<:4Zh) @ D84B4 @ D84B4ڻ?gDPȭ"ц+~΢ݵmnc(u&Zr jyzLLR3y9^B! !BH`B!q0DC!8!Bi B!4h!BC4B!!B! !BH`B!q0DC!8!Bi B!4h!BC4B!!B! !BH`B!q0DC!8!Bi B!4VM/@J8GVVVTb{{{;;*-RQ||]:w7TL,D^ }31>>ƍ`„ T6 ˲;vx葧g6ma4q%%%5uܽ{iii՛5kH$(?+%o߾W_}J| G멦dLvvI?.3Q$u޾}AM4o/_w'xɮ]?^PPgaaѣG)Sk#ܼ~X`G U8s挃CM/h֬ɓuuukzTRU۷o8p񔔔 8Z&tRwRϞ=K=z8uԧ_: "\\\lnnޫW~iӦ… quu0`{޽ǏUhT8+%/^e'OR^zVZuE>ҥKD]v_|yƍqqq&&&^^^S<NLL,((077ܹs_??7ndddԭ[m۶~Qj62Hjdy }ڴicǎjժ*,t͛7sG'$%%%%%ٳg„ QQQSLSN=zt͚5ūNW^?~ׯgϞ={_IB i0''g˖-_Vm^91ݻtʋ/<|=zTb1=<9 Ο?O*a>쳶mV> g:X_:v(3eɒ%$>3000auffӧoݺCWWXZZRR2uPhܸt钘x̙ବǏәn޼9c eB7|ի[xٳ?7KQ;D>|0J~~/o߾%KWUL ܹ… _X,p֭[jnIQuaڵ5-_\* k׎7D IDATN_`ܹs_~<}\jߓlȑ7n 9sf^^ޮ]\]]{UsK~[KQݮ\r9hѢEdd$9BFgO7nTw;v(s ÇӉIO *uꔩ)~K.U|yKrr}MfiiyUP=߭[7y&ɂR/޽"H̊㸌 իWiiiJ/Y_h?>#Kfee)[rrrRR9$I|||FF/_*w0ܻwcDUV֣GWǓOL8KII>NVEߏ/bb`eeao*ꛨ$!!!))Pyʬ茌 ~U,\TTTaz㒒>|X\\:*Rŋ󏐉':::jU{e`` >m۶t:٤7E^^޽{\*UQQ)M$*ͪUH|F8::ܸ8~3'XvZR'w^WBE /:СCMPP@OOСC/R7}llիWƆܷK$۷jjZ0 3p@(,,%]۶mxb977wٲemڴСI^*BCCmll\\\lmmvM+Ν;GpG1sL 7ߐnEEE$x'͛gffZPP0m4kknݺ9995mtʻYL<"77WS{{{333~ cO'11k„ U;[_~=~6m8;;wСk׮JMM0aBf͜;uԡC.EDD :YfNNN{ӧϑ#Gi/_nffcfffffFG 0UVk׮]ǎO8Q+[(쒢?]UÇ{immܬYyrw[[۾}ZZZ8ċGbxӦMmڴիW۶mHTΝ;FjѢE^[n=xk׮$#-K#ߔv_3gӕ*߿{MѣGߺuK&YaaO?D:oO`K"YC;vtqq֭[-&O\^sɓ'oɩG-Z3gL^QTTԨQ#33;w*HR`ڵܹisa33޽{?krP)TĜR)*_rLYt% ,--[j3%kvZX,~'2M6fffaaaGY_nݺ_~NNNgΜ'pV_[lDܯ:99Uqgffj\f666d҉\&&&J_~򍚌wE'>zڵk'_COM]~vFT7=)2Ey˖-iĉ...'O$F4Z x{{ozl"\e)SlذyԩAt#I" 8PɍT*-**9iΝ;)))RՕLYhQ߾}td!?1cê\B7I'O=C uFF~rرc{X7}uHDFF~wwܡwSN%wϟ9i"K.ׯ*<HK$;wΙ3gÆ gϦbÇtyyy/>Tnnn7o;vl\\O~G[.on[l ?{Uq@@@ݻwb1ml1l0~1`={Ry2''g&L{͚5QFgee1"$$fb800pԩk˲D**_ϟ9r!? =*ۿsNHH Az0{l##o߮Zr|-[Ȇ֭ۛ7o9RTTr듚کS'7n8:t[y8~AѣG|`…{y!\޽{֭[n]ӦM{ݷo߾}d+X;v4nܘ< Ǐ'vM(##GzСCd4Az{⅖޽{O&ZYYYXXL2%##c޽$SrJXYfSLH$۷oK.骓ݻiIW^ѣGIUQv{ʴ|x!:gƍ۷Νrʑ#GZ=z ŋÇo߾MVxP[MǏo۶->>>77ߺuȑ#O4]<ԩS}||@ ,\$w .7zzz=H$:ʚoPPжmhkkrX3gw.!N*/-i" H fΜS%3$b ~]r`OK^t)::_UҖ,Ybkkxbuu111>>>t~!aW_~%잶 ^II*~|7H ' tIHѣGC 6 :ya/=LS__F΄-QrH@_駟*7Z ѡH<2]&^hɽ-ZtQ2?v͚5RTMugϦM/% EYF Ofܸq2$DJ_r۷ccc)ifEnfƏԩOݻwcƌ/ Əo]?l0W6V7=9l3a#Ǝ2h @PRRԻwѣG:= v)ZNN% eiiK7.͕޼y#´766TPO>%-=YeYV |B]]ݢ"U4zzz麺:iIӭ[͛;L$u֕*|bG?޽{I3#GN:(all,s'- U {ҽx"++yyy/P=XׯqUV.\t))((رcVU9tttH VVV ;֐J=tŋ]qb1544H5խ[Qq&PHaÆqիM6GϞ=:w6dȐ]hFg|$:v옑!bcc** uWR!ډaTtFGGg֭ Mݻw'oʓD: =r7{b>!!%%%J ]vȨ}2L宧?qݻ|(9snQFkȐ!'-o%}||Խ{ݻ{ӧWZݻ#G6iҤ}ɤ[>}њ.u4'豤ɉj>^靘H$* hV>x@"(_DFFF22_Uy0cƌYf r:T&Ο?OⳎ;VJ*HOO_f͙3g*}iߤ5z깻@LLѣGw-gϞMcTwª+|ŋSSS'N(lVVVVVVÆ I/-Vq5idӦM ,LHH7o|W3fz-X ===,,(00000P(vuΜ9)ayȵQF Ӿ}sΥy&P}կ_4WVJLyNjR! 6H$G~Sz{{@]]͛7+[֬YC9\ӸYXCe䶇tOeB4v={:tطo|HG Ξ={̙R͛D|?w={dggtIbehաCzyy(33S1Jeiiry[we֡Cf͚uw:thʔ)t:)JҔ%K^"055_E WT4i$55U,'''+y˗/I|/1f???R9vh‘_Y=ׯ_ɓ'?^aQRRRO4hаaCCCC###;F:uԩӗ_~9xoFEE2q 5iiL.uU7ܿ#GM4|ڵرcDNNk׮e˖v`߾}۸qcTTׯRk׮](;Tde_fkjeS +Q4+fddQ/-˲;ǓK[ԢnűGZĉs͛)iP-q h3խ[袢"ͰիW/_նm۞={v֍c Spzzrq '׏t0T^6Uyݺu~ɓ'ǎK阛W0TXX؉'6oޜ|r1cxzzPY\ <==zIR  z7oG988*秦sy {hiiI$R@DԩSXXx-X,SVMő 2,UMTN/////k׮ٳsӧeݮ]8{AXXΝ;oݺ|~Mt9<<<##CWA7 vHaIb+y*Ygfeeegg$Ow e@{ɇ)O>]ly/g̘_Wmzj㴴*]  [$UvЁ6UJQkS.\>}:˲'OTXyBF%5ZQ^MFhhh8h NAu*ҥɈC322R0X|ĉ;vbKI&M2EQѤI򔒜~.F"X|s-yMȏe֩SԔ+a6iKפÇ}$frU3###EEEsrrrwwpႺWrl,Yܾ}_+ڷo߮]?T}*ˏ4[\\Lg ꒻𔔔ŋ+L#H_UR?^Mdhh8p 2@CII Y0VVVׯ_'7n(ohFiFJZ@Ν~x۷;vرc<$r_9*|IHF*N>qk֬{ Gݻwrww'_n8#}/?lrssĭ[Ə_RRҨQ'O*$ yyyݻw-Z$)]Eqܴi3 v=$&&zxx6hĉJQCCC2ZHH̐*hGaȦɑVZE*~۷o۶Mfe,X@V?Cn۶MW&}tRHII8qbΝ׭[G6~׮]wƍ'TKTAfhh8{lŴܜL3fLhhhff;w~3g>z<?'%/_LLLܺu#o|||Hlٲ%K\r%66vÆ JF_r%gSSSI."SSSU000! H;إKAfΜFMYYYmܸ1:: duՑ¹gϾzjvvvxx1c |F~H޻wO޽ٙ47nVSkww;v$&&;wn֬YjՊT)qfZ~=yӧ#""=z/Wxyy-Z(11t>}A@WWSɢlll\2nܸDe߽{JFmذ!)iٲ7o.\{oߒڟiӦZe+=ztܹ9sݻ,aaa> tss#QWy:*l77ݻw?z<-Zpvv&-,,H<gϞǏ+iΥ!֜`ĉra2͛7=<<ȵۛ_@ 4{ǎdCѣi~N78CX__΍Xnz>FyڵkJA]e%+Hdܸq2ɭd֭6l(߰lk׮ ϗ=yРAį^"XVZEFFBkEi&&JKK"սN:0ޱcy}Ϟ=lÛ,,,9ـ3gTۗheD3Uߙ_Pʪ՝3JK2B!_iiiT^X:6\5k?=y`֝;w/:v(3&&& _jii8tn~b``O`>}:Mɿ):t(KDDDWsZ~/ʤ߷o_yeNJɚ .]OLF4iҭ[5a#i%0zyy"AL%<<<*,BSw:|}}mllif]ʛ޽{iaN+zU:4@bŊիWDƍ۾}| ݡ_~'+]]]o& DBpΝӧO'dqq1o޽{'O sf͚0U:B˗o޼133S1JOOoܸ1E<ǥgeeӲ '&&><++ĤiӦ֟9??6??/oALJJJ+-Z(3&p?kFIYq߾}7& wJJJRRRHkuy< e˖,Ç={٠A[[[%|Qk秤+<,ۖ-[;xT*MOOɩSE_gYɓ'%%%tV*++\Z/_ޝP=S/X%IAA˲ȣWkq\\\Ǐ;u$(n:::2H$X_~yÆ 5jT5{ٳg 4o\CN];w7lذK./,,y~6mW7obbb6lخ]*>'''666--nݺm۶ո~ҹ!44T_qssd'JYUM)`*\B---j*%CJj2 i'N{;wT!TUcԑBqqq/^$5?>bLEFp}IH|}+a@ S9s mllbŊ]__[_]F%1c ߅WS3111117o^֋'L^ ޳>|xӦM]\\{Bۢ!Bi !B C4B!!B! !BH`B!q0DC!8!Bi B!4h!BC4B!!B! !BH`B!q0DC!8!Bi B!4h!BC4B!!B! !BH`B!q0DC!8!Bi B!4h!BC4B!!B! !BH`B!q0DC!8!Bi B!4h!BC4B!!B! !BH`B!q0DC!8!Bi B!4VM/B!˲R8SP0 #?E( ) !2ޔ\{:/ J58!uYto52VLvv'Z,B,n&6+:" AM PP}յHClYLzMZ1$_rq"!g`Ū !RiԌd8 5IC2 p,G´hFG*- !8m1ðl.(mƔG oi%si|aBN, R ڲkD3Zߪؒ%P}` Bډ7>c?IρP[J${%۳UvkdRk1DC!j'#hC2`J+xE66kAshPՆy ӹ/Q~=Ӡ˝ڳ c݄aHÝd8'}30DC!j') R}MOT+e7N >b-> >jCcy{E\]9X5fŚi/X~'X7af0 K`E4[ҪO}w ,';m0DC!j')G rh&S'e<5C3Ktn 3ZkʸvhbK7H-) 3FP^O:)-,SʪZ!B!T;R42 g$PaUF=][OHfFx807J?"UIޛâ!B=[OkABXߥc !c9F(k/acիy1PW;r[qXUCn`F[2ƕQ֎8:̯u6_"jth4XLi gY7.\*> BIr`8(-gh]Xܔ!1 hŜXSrZ̒{rc\ W d96"6.aBNeC*)h͌KQ&Ho4 $RB3cFI<`0c!FM),EC!\٠h:{VzW,UC0lv?[KaKD7S?fg !8F0Vk <OV tE_PV~flƖ ahL2FKB8,/a9PK@G f땾)(V0Ϊ1')=ؗp7ƣ4=ޔoT/E|C!P͓ aA1?)ǐ)eo8gSТ!pPA֦ AaIIr ``#j] RtJ9({eʍ!Bˁ`P.0LCQ5#HF̓rS0us߯Bȅ9GP C0Š܇\ͯ}2 0DC!j?PIs1` 2M`I>{,ќi:7}ujpbaE'B!T;4i-'H9WW/ bik8Msg@is҆utʻ懣\`#nWcMͫY.GޫރC4Bv e9) )C6 r)䍔XZ_XmX7 ȦB]IVN KN-ƘU~ls. !Bu,G0qRr ʦeuş̍ [fthlhd @~r%C=8| @_֏aսFdJٲmKJX~s !c9Fʖ>;! bK]ȧ{\Rf>;C3chZo}io`~xґ$H7].mݵKE)]kVK:m' BD R9)1c\7Cyt5BHQRR"X`V߳>fXXXG ˰4[K~XВk׮)**.]Tf]سgwtt1bDgϥ}eff޸qǽzZfM[~7@?xo533@wս,_͛7?斜vZB/bBСC[L:uٳ{nYr۷oׯ_qe˖EFF2od-[B&Oh~~~BҲ=6Oa>|xyx'?[{G©Srss !ʻwngSSڵk!}ĀEp\CCC???ONNJAAaԨQjjj2{:ueeeΟ?w^B͛Y-acm?'OKK# 8\'|swѰ0MM:?+?~i[[X/_.]rVSS7oޞ={\nAAnфgK>}ZTTbŊΞ XKBB?_~MC̢E~N uuu^|ys""":r>=oۯ_cǾϞ={!!!!Μ9C=zt;x߶rB:/^dee~n^ɩn݁ZP}P^^~ εyyy¼"HB;wp\>/!߿s-1̖'Oܼy-lΣo@So...XΜ9CQ/^,. KKK333I>Ӷ{BymݺҲ_~#F5jT^^t &hiiY[[:;;^zB KiQQQBPUUUEEEfysp.\:)>>铕ᡩieeemmLOoI{֭[~_xUCCƦW^vvvb R[[onnnii>o޼gϞ1=MMM9ӧ !MYYYɜ$C lڴ`ZZZ+,,ֻwoބGݸqCYcܹdMLL/>L޼ypv)qtӶ;)BKHHȦMD#GrO?iϟ4imzz)SRSSmmm9֌3._\^^1zhgG$:9PHo;uA_ܹ3au7oޤ憆/甗0-7n|ƫWzxx.>SVV 6Ba}}y󒒒v@p۷o|>}#HҥK(~Ǐgff0^+޼yH$5jSUUU3g2{'+**9ҊϤ~[cmׅ,˦M\\\yjo… AAAƄɓ8p<88ڵk>ܻw6!?uSYY%.j``@ٰaCUUܲرIOO/,,?~ĉSNVVV^^^呑[np!!!|>Ͳqrr~a+гg1cHC bƏ9rܹ|>?""B bLL̜9s?MLLz%\BKdwԕXbH$JKK1c;Z`ѣGiWccc+++33B!!<ӧTC:tӕrss###׬YC^3|||*++/_{&z*ifusHW8vvvVVVс ,--t>mqttLJJJJJDΖ~G?x }CCC8;1+j4]1vAO2}Zӓ"ӯ?{-ZD[۶mOQ={Z p\[Kn +X GGGAW|][[… wԩdH]jjj.ZH%6oqo4g,Ed}> >>imC=ǡ7G曒.wɓ͛7'$$H-IWWWEUQ O ё~ܹs=A~>>><ܹst.&&bkk\,&CKպD_2tOXDaru(q ˖-[zubbbIIqLLLMM !dٲe:6zѨQ?Nٯ_~›ϬM5gĉ={{x+zEEE{{a.il SP>fG |x }ر9 6LUUAzO[l޼UVmذ$ {n1"11O4hnhѢsνznp8 ,`%&&r8D/!lrvѫYĨM۷VsW,AYYښr5-ԥKvъ{BO?.q[JFz\ӍL6|SAwww:tgddr/!,` c*^&NhaaAg*++i߿Y:`tWu^^-"wW\)]{薦F6-jwх OK.}SV[ XÇ'a ~w̮v0i/"&&&VTT,Ylɽ{_b -^233!07&&&x3gի}-,5gĉ999555_}EVVVxx_CIu B??e˖ 8PfO}} 6lݺ_ÇK^tiۧDT:::pݺuWJJJ.]$ MMM%*bi{tJXccgϺ2c 9effB뎙^RRg}VUUk׮]v?;|dɒ%c]]]qĈ=nOrrr\\!dRtʕo߾]~=|9uԁΜ9B8p61=|ݺu֭~g#`Gӧ?#!O?@; )Ȁ,a []]]II@ 2d;WTTTVV2D$Ǐ={fiir_zURR" utto+X.^ꪡaiiiccӫW/;;Çy9p8.\8xabŊzHo߾kjj:99ݻwO dddL0AKKVWW955pAcccC9c :gc` uU@l( 휒vABΝ;---t7n]~O>ia&MxSLIMMGȧ@۾ +X J///@pܜPBȆ $^o=z477744tG_o߾gAAATT!ѣ-LӧSLxaaa\.7///,,LEEvԩ?&̟?0>>j׮]3gl@&`uSӧOoٻwJB|}}gϞMmll?H6c ^pakkkBҥK !C?~)!͛-L8888qbԩ˫<22r֭={G}BA:thlllsφC{lϞ=lmmΝKՌ3/^tVWWE(*** r$())1mss 777ʪUʐKA[4HOI$G-mȐ!vWg6 !X@PxEg]]]Xbjjʘ﫺6g-A 655vx<^JJ/隧 ޏСC;w<=9nFb[~!˰$Z1,,}o! Б?,a >H[aUD..\2jX`VԺ? S.+&:z Tr`6  eX,C`X2,!`  eX,nUTTd>%-[fbb322⋮yׯK7f3aW X\.VTT >/TSSc%&&B?)S|rpp0!ۛ颟kgO@.)))=t@W_UUU1-gΜ9~Ch"NqKKK<.Ξ=_ BBȘ1c Ë/޻w/??׮]#oٲ׷'rk֭BPQQ1<<ۛi}'OB8βe:o2tSl<#F+BJhh!佶[  o?yDAܹѣFM ?}vII@ x@5iOcccccc}}}}}}CCCCCC[۵VMM }`nn.ڱc:~zݻvZ==)STWW/^lرVVV鄐/hر666VVV?^|wʕYf}'cƌqpp033۴i{?СC>qD{{O?400P_zV>] ?wի%:ى. /,566 %K1=;wn\\_yy9^QQ/B& BwosmmΝ;y<[~/GoyٳHӧF9rdRRRc18BZp8nnn1cƄ߭*///--/<|tt !|>֬Y垞gϞ=w_ݣGBHDD)m۶y;66ի4>|x-_jG=z !ĉ?ҥKuuu7o>pu,Bȶmrrr***o߾}vCo4 IDATCCWWW777777 'L0rbH4s?V6stt{ݻw$׿B[YYM8~ƍ>>>2 _={%66VUUb``0z3g޾}{W&LШupp0`{^ׁZ+X## 0Eʎ9dKKuUWW9ҥK ..\(^7vԩ_/t-C Yv-!2Coذ(J]]}ժU@PPP@|Y9+77w׮]nlr+X;v&&&^|9--RWWgzj5]]]-\ "naaAH}9rȑ#CJ:''>rSl !2444FaQpppqq1!_%''BN:{쎙8KKK[[[;;;uu8nrr2 ;]?|X y͛7{O^ԩS-\|H~2$dggon|9SNBwBϦkBLFFFgϞ6lXǏr@K… =<Mp8Gf>޺ue~F5jԨ>@ )#Foo'ReJ/3fѢEAAAL]О={.^xÆ ҙvΝ 8rskmmmWWאKNq݅ v𥅾!!!|>Ͳqrr~aUU{޽COVQQ .--%1qgȑsε41&MDחD"SS;v׫ظq#!dܸqΝҢ۶m[n!!^/>x`SrrrϞ= !ީ,B?/_477ĩIo]gaۍi!Ǝr555ccc=I˗/y!ʦM6lɓ===ISS3-- ӧ:ujk֬a}U!dǎҍʋ- rMMMt7oB;[YYEGGs\f%kCSSx˭[hEm۶_ٳgϵkΞ=[ p\?oLQQq…ׯ{䉁tŀ022zgѻwo#މKԔ;uTqqqrrm$hjj.Z?|=|D۷oqggg=jΜ9s̑SOOYDUUU%%%111111̭!9%2Ymm-}pmD Bx/]:`A{sss377w^DD X"H/R"(.._~i&mll$ 4,,Ll۶m9r̙3XbJKӓhe+C-[zĒcc㘘ilٲeͽ_UUU utt nUW𵎡' D"Qrr2 XL|{{^8`68Tr`hhh|ׄKKΞK[xq@@@}}}TTTHH>v]TTD=\=d' 6LUU} x EKLLp8)))V@X2o۶gA,Xo߾}yxxв-,_1 ~wcƌJf͛7]l{g\tp8-Kܛ7o*++ !LYp322Ǝ+ DV+u:Xhĩ\`!fj}l$SSS%: yyy޽{ӧO_rÇaSSի~ &Fw}'1Ç[XXXXX?i޽{W)@{ {{{̲2BtƸq8@ .^ 8qĢP7s̾}^z522͛7DRٳg=m!"իW%%%;}pݺuWJJJ.]$ MMMܘ%Boٲe&&&lGkVrGB$())g\b۫{*))yyy;vL^RRg}VUUk׮]v?;|| OOς?\Q?¼VӖ̹4G"q~#WVVѓ}fffN UUUы [\[[Р4x<^JJ/)B(MM8^8ΠA $Og *,hE3)Ba >Hlk[@W(z۲+X, |ZD׫뉬]\t%LMMs ˰]Gרrru n!%%EE זZS]* X2,!`  eX,C`X2,!`  eX,C`X2,!`  eX,C`X2,!`  eX,C`X2,!`  eX,C`X2,!`  eX,C`X2,!`  eX,C`X2,!`  eX,C`X2,!`  eX,C`X2,!`  eX,C`X2,!`  eX,C`X2,!`  eX,C`X2,!`  eX,C`X2,!`  eX,)// PX$h/^S޽<x/_dk(|w,o4NL,VX2,!`  eX,C`X2,!`  eX,C`X2,!`  eX,C`X2,!`  eΞׯK7D Fgg玟 WWΞ\RRR:{ ,@.:{ :{ ,!` qĈ=yƍǏk͚5mo̬V|7o[rrڵk !NZ !Cms\\\rr2!dԩgn5gʕo߾]~-[olB:|>?--2p@sss|IMѣG455py:~ӧmmm#`]|>tR˱[MMm޼y{r2EGGB|||X-!dkQQъ+c|R;{ 2 `u/ |5 1-;iR2>|xutsm~Ǝ>>>{!DGGHw;s !d9η~# ~^x떻z*///''uj!B&&&sCyy7; w" 򊊊D"Q ܹr|tP''xOVV3=%ݻ&&&[n}EWWW KKK^z>|Xۊ+8Kmmy={455p8O&dgg7eee%s @i&:88kii͛7P[޽ׯ_˃{{{B=zt gأGs2 766vtt411ӧD3y捲2ٹsNK A.!!!6mb)222Fr?S=??ҤI<OSLIMMX3f̸|ryyyDDѣ%mllܷo!ӓv B!e}s΄ -޼y*S^^^RR´nܸѣGg^!|~vvOYYY@@ KJJbo߾]PP@06J#qK.?>`fz{"(::zԨQOUUUfΜ챓 8rH+>&!K,nK#}_V@.6mrqq~Affi! ?ON2㩪qܼ0کS>~Xc)++BN<),A>}"s7w+wwwBHyyɓ .dffB~'Ork׮=|p޽ڄ?֭[Oee@ PWW?\nNNNhh!dÆ UUUsNJJJJJ*..NLL>|c'==pssBf[L4EDD$$$$xxxzϗ^BH\\x>#8q`HH[BBBii3gƍG9zhPPP+>N%@w,SrrrϞ= !W\ar8'NL:6ZYY{yyGFFnݺUB\ 7ICϞ=nj#=!$((?rȹs2111s̡411իH$r -U\\lggGSWFCCcŊ"(--mƌhGU^RRR !>x?ۧORUFFFϟӧ!dСӧOWVV͍\f ={ IKK| vXB***6nH7nܹshCCC77777m۶[N*;?NY%@,?MW… !uuuO<7o$?Dyzz6rm9g``0}tBݻիC"]1[YYEGGJT+dsDz1)))))Ij!$;;[5 Ѓd~ĬEFFtرc=9i5OOOO>~aZh}˷nݢWl۶Mx@555555<EzG|=_tqtr6 O!***YYY5sӧOGFFҬKs~LuuWV[[Kܾ}iog"w/',DYcǎ;&sJdrss377w^DD X"/q=zCoJ'O6oޜ L&]]];5WE]N4< 0@G$? IDATGGYKKs=zZfxsEB-s# -MbW>S=!`;yo[ա)((,[lՉ%%%111555e˖ѣGFz9g~맣C ov>#7՜'߿"::zݣ ?Q-NY%@{C%wY[[s8@_2ŋ룢BBBc6wh0l0UUՆ?myfVZafP(ѣD[0bĈzK\~~>!dРAm<ҢEϝ;+,XcggpRRRY$wd膰ءFg6co߾ׯs[-_8eeekkkBȵkķS.]ڱcG+ՓB۷O?$ƍl+yKq_N73}ڈj۷N҅+>#wd5ejjxqHBǛ8q^hIf9]yyy޽{ӧO_rtw[cPcc#+۴ݻwyG&<-]˗OZne+`Y[[>DkK93w1کÇ[XXXXX¤m'wdnpXXQQdɒɓ';33_~%{wpp,++#*++w6nܘXTTfΜٷo߫WFFF{֜'|W~~~YYY%^) -[fbb2p@=7lذu֒ww_...>߻w/!zҥm㓓C PHDCCuօfgg{xx^>+))tRPPP(455jK5+#`kϞ=ZWW ~BȌ3$V/痙IQRR;fzzzIII}YUUծ]v%}%K:t".....iwuu}SNuf͚&bccccc̘[ҶlR^^[nI222JJJbnbv .\f @1oK8?W,((9 τJt;%m%(x뮮ZZZyjzK/Z|XsxUIIFtC)NmmmCC<# 'O<\]]Hfu#ʪY)ӮKVx<^JJ/)BFhycWrs&NCCCCCCJJJFFFFFF4p 4hР8iW%@s/ f uγ-B7˰$ZD0,,^Ly߉A tl ˰֭0z"k] {- +X, |Zw]A.x<^gO@RRR$Zxm5Օ;˰eX,C`X2,!`  eX,C`n~t#n ]̛=;;;wL؅-v䒒S`VwSxS``  e8E&汢u%G(B !JJJUUV+X, |{ kX`Vvk ={|GG#Ft\V\\\lnn>qqq۷wJҕ+W}vX!nԩS8sL }8pׯ_wXu֭[=莰O㏄''O?(<< VX,xouuu%%%`Ȑ!***_QQQYY9d?~왥}UIIP(466y?`{x񢫫M^>,\pÆ >|Ɗ+E"Ѿ} `hhhoot=y&1a---kkk[[[]]]ggT9 3d},իW=<<󳳳}||hP(sJJJڱc}L;wd:AnܸvO> O47?=. $VeR* Er\X7KKtv7ٽZ봤kJb%uS&9Nj:f?g*>z=𘮹f&^s}WZZĉϝ;ko[W{Tro.`Bf͚%~WPxʕP"o>}*~<222;;;44Ԕ"##曞={޽;77wDTRR}:ktttdSRRN*ݻ'&&Z[[HEE%)) 0f+++"haAAAEEEÓ&Mbf*))ֶeLMM:VUYY_)С[lD"ʒ X'N7>>>}YUUYb,--[Yט1ct688X(^|YЮ(}}7lNNεkך{B$)))QSص?AUULO{{솆XWWÆ(^ ^X]Ԁ{644Tz:s>|PShhh4ފ4Ba]]8pM)+++++322zՃ r]77G{իW/w7effֻw566HuzVrG_K~=z`DSS] bԮh;i$''QϞ={.]х ;][%]ST"@Ci#K}}}M̺ѹs'.ƍgkk;yd'--c A!ƍrJEE'|gkk~ڵkJKK.\X\\ |̙,]M:u\g---VϯǎA0X] o}\xӓ+tX63$S2 Cjr...qqq#Fֿ={vΝ˖-cW6J?1@JMMhyyyDdee1_Uyy96ں1ꆆݻwD"Qrr/y=/b UUtX-Ez V>-lFb[~!0o$V1,,^e6m?$ 5;2W03`u3LlZf¸[[ 3X< Zw{8D=KHNNic㽭;m;ϰgXyɓ'7nܸqƏ>ɓ_>#++8#tϟ_WWGDFFF3f033+..| GRZZ <ЀUYYtROyfW'NXdIee;&N8rȎ}MO`}$  exi駟ʈ̸'Nܹ窯ͽygZ}deeݹs犼ÇW^e!RZIIP(oO`֮]455;3<066"cǎ{ҝsrr}":tt׹s炃SΨQ!C.]ʂ*`Au۶mӦMcbxӦMw~!kQUUo,--s=yʏ.++۲e׉H |ǁ,Y988/索իl$c^ZTTDDL믿Q݉H"$$ezbm۶vZ֮Ī=={6---::0t}^|9>>w ٫N>oqGſ{EEE]tiڴiqqqnnn555C  DڶpРA jŅgϞK$++_|SVV `ݦOw^?Z;6nܘo>vocqqYdNajjm۶5kӇ>bhh/DTZZzaIII555DQRR܂-u ֣G؃tuyjÆ '|"ZGGGKKo߾U}۷/w7C K w^d >̎cgggllGD***Gc9r eeeq;vNuu+KfБϟ?hѢ&  XOѣ7&?~|>~TTP(433cVϞ=كk׮xקO_~% ÇlZpܹ}tTsXxݻ?uTaaaEEtVΗ"##tuupI:.sX,`=%%ѱuG>tбcǜ&=&t"wcccڵkwiӦeff[ޞ-O{?(rvasr7BMM ҿI[npSQUUՁ:~H}B͜={ٳ[ȿDGSS??{l^^޾}bh6$+seaaa``CZPEEEqqq-$##窮ɑeeaÆEEE SN5M"L{EqKCCӓ ׮]dX駟~WׯgS<|ϨuVɗDA.]#խ[7"MIIi%W644411quu566666i2fffz{{;88꺻߿_jjj{#*++ek׮566:te=V+++@p1"^Dvvvzc?7ozxx\r2wUUU]9rs1%\~z&M_PPpa"u֏?,))0aByy?pȑ5//o˗/~*$$+OOآ?sDȺ屋666yyyyyygΜ9Ѻu۷oO<^xuU?gϞ/^lݺU~ Ν;Dڵk׮]\Yd&Mcǎ̔#F׳蚚6l,<}ÇWXAD۶m1c^422ʕ+2LMM8%33=(**z!O{ ۹s'þGGaÆɿӧ^^^gaa1eʔdgPP)00;C 9sK]]֭[Y2?=-))ollڼyu5kѣO8ADfff6lXz%۩iذa ULLCnnJ,@`JxxxDD CCgΜID...{4hW[nk׮ocjjj'NMLLQWWwԨQIII2W$^hWmABxx=@0y#GH 4_]IlmmΝa銈LLLصm۶IϜ]~ٳm޼9>>~߾};vΝ;B!w "rvvYugookײeyҪweUfzҥK 6thkk(B?WH555W8Sz̙3gμuVAAÇ{.1G=zgܹӭ[7333\k +VX[nd[A@@jժ+V,]FFFsɂD4p@uu/^Bl]9;; d`F__Μ9DcǎTt+&˗9RzLRUUU":|͛7+ϳgʈҒ/*xjǩ2z-%[HΤ;wN3[`)pn޼9eʔ/9 ˖-cƎ+˗/޽{mmmmmmO<5Ҳ7nܐz p5==뫪\ѣG XO?K$:u* @~rh͚5qqq"hڴi={<{lDDijgϨ=m0::UrbϚ^:444++{ٲe^aaaRRR``D".n H$~~~/]/==TTT>z.Y|tOүFFFӧOSΞ=[oߞ>}z177ZBBB RRRd.PGDDH'> ,,1:::::_~nj.v9Nh֬Ylɓ'ߴiӦMXytfloeeeW666NUUUo߾}v$_5١.]RIIi׮]+Vaf``㓛Kwvss;tùJWf2CjrlQjN&5RSS=<<eee711 %]SVVׯ6_bqqqӧO͆-w~ⅎ:6DmcddHbtuu_+}۷"utt:+KuRW6K< ؆ll5I"n>r[` g7Rf|Umm-5̈́ihhqlfԺU{NUD@W%$''˴1RkuJ<"w!`  gX{"=xzz&$$Z|}}vm *(( " D4iҤӧؚ_<oyŋ#""?&V~~/"0aOsGH$vvvx"&&Ν;'O/յ޽V=G&"UUm۶гaժUDԣGN X(PhffXRRRrsshѣGE"Qfff ɓ;_smյ555܏uuu)))Ddjjjcc#wI mɓ'YСCǎsrrju ))حoB077ήn'"%%>Z"2eʦM,YJxxxg X]KllUUU,̝;A5f޽}Ysnڑu^z5꥝ϟoCBB;D4bĈv/l#(j?~YUUrʜ+WD-DUPRRrչ999bH$999-tvP(SdlDt֭k׮|ihh~FFƭ[ZX޸"ކneeEDMv?4 %IQQQzzz~~"53Q;mo2tMX`;;^z uꔇcnݜ+mɒ%`366>|ǎ#,$G,[FOO?˓fhh(^z|yݻw/\ ,555gΜ fڵCѣLT3y왪@ _dήwdpn:Ƌ/2D(\իWǏ/_6qs999)rS9sd֭#Fy~ǎDútPp m;gϞ&^vmرϞ=KKKKKK )))*,,Z׬Ysݝ;wrgϞN]uuuYYY/..~?bC._㫫co{_QI[hxС>}pk͛~I?Sv[ߴiӸ5vDᥥkg>d-ſS^~cm_ׅ,PȺu۷oO<^x!k'D"uu0PV]]=iҤ{)r.UUU___":rsرR"jr5w+ư~ ˋJJJ&LP^^'&&ZXX?pڵ$((whݗ.]b}f͚%~WPxʕP"o>}*3Yfűewټy3듖7f"㖥7iڵ,8;;oݺ577766'z{{WVVW^?8ӳÇ'J3":|0 =6OOآ?sD؊ϤI|&@W,PȰa*&&!77 ÇO45[XX̚5$"""88XܹⰵYÆ a+Ơ7rHQ`` 7~Ȑ!3gtqqۺutee娨3f---u6k֬ƿ*((pvvf+訣dɒƔSʼ9sDFF*{Err? Ҳn}+Uenn~=zрL}숈+W39OII)++;sرcv6YhllQii5kh'N`<=====7lذzj/Lwo2tYt(++GDTSSs}֘ADcƌ&1>>>$˗/+x:)SѶmׯ={jewggkJ߿ڵ2 tŝ]uk:th|||||Lծy橨QVV;Zbt }33369t&?f"""Xl޼]KEҗ_ݻ0͝;K.6l YBPLNyM. 3XCʴ "bwձ52Wwтooo]Ν;W%2ϟ㸻o4c ,EM}>JJJfffonˢ H""^SSEƦ^z#gs$:ԜJJJ233[wp޴iӢ;ftt4SAn02_Yuu5{peWWWu)5!`BdZdp8p&RVVVVV&&yzzܼys֭,`D4|6ASSS8l4˝߿k˚Խ{w c`` ݉'޽[^^d͟??::Z$8qMEEEw# -OfW>ſSkB~pz\CISRRZxe -,,***h:6{ۣG؏zի+1Z~Tsƍ׻wO>͛lG;Ksnil ſS;X,㏃x9 jkko–5}_x!-֯_ҥK[nD"䷂h WRRRQQ.xIz*۷{m<ܹsO8QYYc 9sp}ArrT"N7 ,r~hhhyĨر#55]jn1:88祗3III7on^=DԳg^zu JWԅg4ʿ-jGe딲/r(o2tAXܹs!H$7֖Uz%,Nջw>VU"7oN2/RlIS}}t erqKwtӢE}U%)H/^liiijjdOcco688~Ν-Z!1Ͽr +@e`` MV^lٲ{0)))00P"XYYTh5ſMzVrGXXX?~ã& @z":u셂ӉHEEO?1ǿ{O>ݲe˖-[={+ZpQZZzroʉ|ݯ_?_%%%;wtL+ssxn裏V\*P|ᇬ^'#""ZԊS;mdRpKSRRbwX| %үuqq,ҿ={Hod3k,z22MWp M_ͩS}۷sCO\xӓ)tX[ -:m̀ٶmۚ5kC 3f:uJRFFFǏg%JJJvZbtU\VÐLX ~dǏw}zxxuIf#o*++ ǖ:Q}}=[deeS]] E,H߿#---ss&uX\\\Sccc^J3@uMRD"Qrr/%BBXyc WUjs&NGGGGGG***4W"۷o8 ͑)/K< X0ZLEHvwߛ-B03`u3LlZf^Ka g7RcsTp:X]H$!tX]BrrL-^g3,rx3,!`  gXHEE[}^` 3Xꫯx?fXXX,a 4ZKqX::x +Cr lll$$$;;3a'&&ʿ"BV5` ^ܿ>TMbMPXWWGD8p@}ʌ^Cܽ{ѣG^z]wxMnݻn=xݼ@!ׯgjҥ~m=XD"Ԭo׳;88Xe@!DԳgKWDt…NWDVnIU@!lGzR__a...Dtܹ}IDqN}cvڸ҅ O00==}ƍ.\ +Wr͕inn1CJ[p![]~QFy{{>}ÃsooǏ֮prr2779s&KWSN]`YKK(--߿c:` Vr666/^/-]466 ɿDELK\\܈#Ϟ=1112ݹseؕM"bź:Ǐ;{ оRSS=<=c'N8p`g,0]%`۷OwyfΜvJjↆSSS}}rXDBDDBD>/44-33sƌ}9rQp?~lcc; {Ǐ8zlٲ5kTWWmܸu{˫wj*"Oq]]nnn555C 7ꫯx?fXXX,Yeees̩TSS[~sRRR֯_VSS3gΜsѢEl"jϞ=l<mڴݻDq^zQ|||FFȑ#:######&&OJJ! F { ݻw75ښ~'w "%%_~eȑ_~errݻwg"9s)SkMLL[nDٷo_Ӎ;VGGյO>NIͭz#2IVV9KWԩS @DBk455(///<<|}<]޽^pq-[lݺ/uuuׯ_'"==[Y]]]"Ζnp"RRR_:t?-8x`BBM4i30ijjjvvvNNNZZZqބZW k׮ǎ;vX}=zѣ={r-7np£GhFѣGYFTUUݶm[ =VZOD=zh I% Z}E `Z [$kI\ bƦtI:.sX,x18::ȇ:v옓ۄ]Az `;;;@ ̙.)b͚5c:4ydoov#ojjjg5a֭9?~lhhxUuV,EC]]Ɔ|IRRADk֬ywhOBeeaÆEEEѩS&H_MzqfffUUU]ACIWVVΜ9sذa}X^^tR"rpp7mڤcêQ^^^EES;wdK: +d۷K$uuu6rW644411quu566666i2Yfffz{{;88꺻߿_jjj׏dX,^vСC---{ϕr%"+++@eee @`oo/`;;^z /߿\|y˖-.]"%KpW\YVV?͝;w߾}ב#Gf̘455%"DbŊ ZXXW7odK~K.Nz̙[1B;v&lCCìYKKK>, -,,k׮;kyYZZZZZZvvvhh"oƍD.ԢEP9"(<<Ữbuq?666^xqȐ!BMk=*}]fdEEEiii5'KW&L={6i- ѣ}eeea'N^5yiӦLÇ&TUU}}}ȑ#=vz׳t5iҤÇѭ[~GgIIɄ ˕>@~#GtyyyӧOf;rBBBX􌍍-**?GMD[ZZZ^^ޘ1c&/////̙32'Znɓ'ы/.ݺhRRRbqm-nnn\J~lٲw^6RUUrJ"255]z5 88{k;w,,,f̘Ɵ ***"͛7;vȐ!֭;~8(,''g1&11СCJKK׬YCDG>qĔ)S&O_ц ?NDfff~I---kkkkkk6(mذa s̱2dHLLWDhYDaÆ?~mcii)[nluS3gΜ4iڟ}ܹsQQQQQQ!!!Yvіe2Yuu5{peСCeZd^ Viccsͭ[U[[˪N̟?_>2ܸq+WZ  YYW*nee&X\\<{촴 6 2`V UZ( /H .MIIi˖-+,,b$/^ܫ7mqݸ}k33Ç466&$$Ů*++4Bv&P^G ,ݾ}{HH[u>jԨg422ݝQ ((q Pql]9;; dMMͶ/].Bϙ3vؑF0}$|#GJ߆)STUUΟ?/>6oެn<Ϟ=ce/,--Y x_ *++cIw|As n!9v9lyNNy)S⋽{*2†e˖^cǎeܕ˗ f޽'O}7nܐz p5==|}}'pF駟z%N: ?9f͚PH4mڴ={={6""ٳgTÇYV"jll,,,fܵسfffW ^l{WXX(H<==#[%ŋ---Ka䎀DD~~~D駟SWWwɒ%?c,a8 Y)ECBm-2}L8qΝRv͚5߶/\H@@@޽SRRx mڴYdIu|]a; бP=][lرGP%]+HR}&ANeb9qą Ɇw|*m0w(;;;>>}?%BԨQÝhR[b6||Н*T(nUjue},,p_RdSX3ܝ3RSֈcdC 3Xc ܗnoIWJaL,1KwۧʊeLwjq=cw'eTr0 F0 `, F0 `, F0 `, F0 `, F0 `, F0 `, F0 `, F0 `, F0 `, F0 `, F0 `, F0 `, F0 `, F0 `, F0 `, F0 `, F0 `, F0 `, F0 `, F0 `, F0 `, F0 `, F0 `, F0 `, F0 `, F0 `, F0 `, F0ٝNw{pԴiSw?Tr-_:ro4Ksk c `, F0 `, F0 `, F0 `, F0 `, F0 `, F0 `-[6^#>ikcy.Lرci^||= XeiXvvك`0X#``0XgϞ~d2 0Ђ/ZnXm۶>>U˳wULK;vJly'rrr?m޼YDԩ(97o^ll_Q;,YomѢXqc?_$%%m"iZ߾} t}ҤIǏlYr㏗.]R!楗^zKiPə3gkVTSx8n[zG}_cbb\;ZJDy䑻fqdp5ϟߵkץKvݻwgff ׮]{#N>m6Η/_NIIINNX,7=jMNN>~tٷo_RRR^^;cC۷/??lNڱcǡCٜx``;Ko:t  aժUjW^qlZ Ǐw磖;LN7@DƏVz~߿M6ɮݶoީS'f͚hѢr۷ߴi/TbEIIIQ˗f̘aZ Ν;g6f?ƍmvŊ>v튊kҤIfڷo\8p **r͚5W]~oذcǎ*T k޼yŊ̙mfC/_~kժϟ={3$$l6"T&M bywԩӪUPÎݪTzի_~YD;m6GX\{>ѣGתU+88uիZСCҧɕ+W<==fg}w ,#`-111Fڷou}-[S7ntlܺuk.]|=zԮ][ϟ9sݻjժٳ|;g߾}:uE]re֭/?Oӝ;wxj|Q9vH%&&w„ Noj>s=sjX,K,i׮ݵkGZNc ;v,YҩSӧO;6/… 괾={ 0;w[vvG}ꫯgb,jur;57@YF[}:̟?ȑ# ݺuk׮9c/t%;;;666)))99966]v=y;ٿYl㴄ߪ澍1̟?gС+V:w,"O~3335M{ׯ_0f̘`l5o~ IDAT2ǃ̚5ƍ駟=_VTID;w>?OJJڽ{ĉEdȑ.\p[bbb\\\ttt\\\jjڵkնGNƍKOO7K.ڵjlҤIpppttӧM6~xw^111yyy_}*e7o޶m['ʗ/v?1cƷl瞋˛:ucɴ`g}VX^+FGG?YJ4o޼B u}=zpzG/¼yԄPppp&M4hpڵCHPP __/TUݺu;5 ظqݻ{zzΘ1cϞ=ӦM{w+y挌SNv5UV'xBDΜ93j(y֬Y?EFFFFF&$$L0aNU!nde3XpСCURL&Ӌ/("999NR;v7IݻwӦMEvw."ӧOw\ٿ?(EL_ҕ8GvloҤGT ,,̞쯨JqWQ[nT_Bמ~mAAAjrȑ#~7eQ6mɓ⬽mݻJE'OTˣ/z;wTu&LpLQ˗Wb$%%9>SY`-[vjQs"(S{+U~zj, 4h婩֭RܥK/#}z׮]wp;={.\o6mʚ .Tqʾ>hLffWveunժ>SM,FN-N6-ZhQp=T"##CCC<8uTrss۷t\rQ\7ZԩScǎ]r޲BU\ EEUQw Okp}4,,l͚5ǎ,p5k֨I H-'9=Cru{ߩ&a_ YfQ݊HӴ2dڵiii ,1~;mоCǎkӦ\իW^= @,ſ}TQ|ɚ5k9sf<ďc+*&)""U;wjo2c4kl6[,?ƍ3䘯ʈ#rssg̘?E]ڨ14mڵkرcUz7GidZ˕+glbڣG 7 4(!!AD<<<}ԨQۡf>a׳MըQ㩧R %͚5v лwUpL쿱NN@;|iwז-[:v_/.6R~}Ik /^LKK3L 4P[P~~ڐbsk׮UP#[SN;w׷nݺV+1ĉ.\U!wz/&GdggǷoޝ߿R XRb`, F0 `,Qɽ.!PVʄx]Ke$/nzUKQc `,mٲŵ%BW!` ;v,!ps\VYŞt>>{ F0 `, F0 `½/kݺ?\cʜqƥ,Ϻu/^,"qT(}7^DX,_|֬YV*ObbYf͚uҥΝ;7|Ç:t(/H۶m?pe3Xc ,'''--b4lϜ9ѰaCooo'O={6,,̩.^fZnm}`lذcǎ*T k޼yŊ̙ܹsfl6_~M6UC=TB>sڵkEDDm `:uo֬Y-*Wܾ}M6;̞=l67o\أG5g ?FEEY,{K^^^bbb߾}O81bhZVM䓺uΛ7oϞ='NS̛7_Wjվ디3fjJDN>=o޼b/t%;;;666)))99966]v=y+VPϚ2eÇ>ܳgϻP(fʨ{v޽G8Ԓ."gTcŸ7oѣS*W~Hf+"7nl֬4n ;v3qƥKvU56i$888::ӦM?~|ׯ:ԩSG D*.^[sm嗇 bZV.]M޽_{K.=*])k޼={Y?nOWÎ?>))vpWʨJ*=SE=o߾MOO4M |||.]ԳI&{lEb^^^rr]ߋVոq ĉ+N;vʕ+Ϟ=Kn2ο<YhѢE 퓑QF[=8#`-ǎkӦ͹sԏիW^z@@@@@@W1}r+((f͚EuunwcǪto9jժj+W.??zffbDd۶mw;]ڹXSjaF[i#K~~G}T2M6͝;ױ=;;'lԨQn lݺd#OHVVV~Ϟ=O%3ѣG׬YSW_}uĉw>qIJe:waÆثsHݺuչ ,:u\R2@Xpӫv/^GQFTTcǎz1Eu}m&"=zxW}}}U3g 4aÆ%Va-۷GFFڋ/xyy+W,W\Oq} ZuH2""bڵ}駟4__~}BB˜1cEen:Ç?3YYY˗뭷QA022rʕVzDd޼ycƌQݶnz\DBCC>|}:̟?ȑ# ݺuk׮ pD111yyy_}*e7o޶m['^psUT/ʕ+'"ݻw߼y3g=Ǎ."cƌǝ-[>syyySNU.99RsssO>}q]CBB&O츮w̙QFc=f LHH0a}||DOD|}}ׯ_Ҷmu֕/_^DBBBVXѬY~>X*w."ӧOw\ۿ?(EL_ɓΝҕK/HRRh;vѣG;I&=ztdd/]O?t1`תU+ĝ;w&LҕR|y-KRRСCURL&Ӌ/("999Nr80U 4h婩֭RӦM???{ѬGfL&իWۻ^g}g]YF_~Yu… iii6mZ` Fݹs,._ϭZrhݺSZ(VzS7߈H߾}s麾x?pDHJJ[*xĉ>}lݺu„ -[ٳ8,{4tuKIkԨ81Xei2dڵk҂,X%",YC4iPRÇۻٯx Zti``֭SKLΔ~j={ڵkԨQ]mϙ3'///""⡇2%W^1bDnn3bbbG}@?~\:ư7}qwHӦM]vKRVڵk:uJkךx`[X'Npjj޼yDDDÆ M2wyGDƏ_h:v̙37mtĉ__:uk׮_~wÆ v[ouJ* 3gΜ9sfTTԮ]+IoGLJjzzz6klǎ?ڕƍo6y-GW\z5q}G}ԝnBZzuJJJQE#Fe06nܸ)Sk]HNN<8k֬?}I8p̙3322^xYf^7+[nݴiSVZر#99yԨQ"rݻ_rm۶,6d߫SNѾ2[omڴB s̙0a|'$233#E ڴicٳG=qD.].]ZHJ[jE+W޴iӶmD믿NOO_`}LhժUBB߿gQ{1lX>䓚5kv9;;{Æ #Fp5jڵk?>q={VV6mڕ+WgϪkڈ/^LKK[p۹sghPP'N5dUG~ƍcƌZ!!!e )VuРAW^:uP7npBǖK.g>1cܩ]Νҕd/U'w?}SN}KoiРA "~~~/\#::zѢEkԨ׮] .L2eʔ)߿O>N?r3]LK.ٷ]51]);^ѣG>^>ܥK>*ȁj#Dի===SRR^.""ɘpY!IDAT=w}ZTXĖ-[:v:;qԩ@ RbdjРӷ,ˉ'.\N6,˗]VBc$eggǷoޝΆgP{kC~t%"/^x l~?+9{U~iSǿΝ;;8 իV577%Kjeʕ;w.|.]X֫W9=k׮4ڥK2jԨ73{P5IW"״iӰ0ӕ|0""W*TR p;jID&M4w\yGΝgϞyK~ƪ>䓋-JLL5kV=Dѣ-JjjjVde˖}wÇZ_~:;aaa >44TշKNN޸qc^.\ذaC5bĢ*V("+WtzHL{NXBDVc/ >k֬5kֈHչZgϞ8qta…j 00Gٳ?2Dgff>|8IPPPΝ={v˖-233Edذaojh߾}Ϟ=]7xiu<==4hry]ם<3ӧOWAAA5jٲe^^ޖ-[^{Bӳg9sggg;.ϩIGy>ҫW?ãk׮7ѣGgff&''9rDDL&ȑ#CwVEFo+%%%%<<\Dyzz>s}e*UMzc6m 4O?ulZjMWhhhJJbήTCv\mڴٴiNj9`tt*g?WV+ƍ{ ~י3g?c=~ܽ{rrrԝ$\r/fgggggfSի*ɵlm:,WsNck]xݩҦ֭[\`&nzmpxۭVo|||퉉k#{;Xbʔ){-e #^;zX,jWE+U; 4-::*Z֭S'4ib."osss,XЯ_?{Z4L͛7/P5kTwƌ矫^^^*UOOO?v1šMծ];===//ѣ\pԩSjkcccAs܇ lٲk׮>͛7߸ql%--MڵkO>UV'6jj߿86>b8,JSޡ֭[o۶mŊ}Qg YBX%Mwj׮mɿI}]ծ&vU|غu3pm:>]rBBB:k.8uVv:~x7n\۶m6mСC:_PoCttm6mڔ{6${s5p+4 2dܹ3fxWե+#G\zŋ"i?eϾ]IHHֲkڴ X}usԯ_g]lիW_y啍7:Յx/?S-Zݏp=z>|իW'LP+WV\YFG}NˢʄeS)C {O5իW={v׮]&M}9s:ujSNիkwyGƌe˖?󟋺D T"":u={vZZZ^^^zzy:utQ󋍍OÉ_TT|W"ޠA>~{`J3X"ү_/ɓ+V0`@DD_JOO_|mۜ?#uu+ԩխ<<1$$d/j2eU~.\Z:J*k֬ӧOjjjZZ?שSgw Utt];ܹ~_c%fBy{{6LDt]oK4M-9TR={x_ƍ-ZSOWt\8{'On TReĉ111j=v_~!CTJ*7ntPCt~Ge==={a5j۷ྣ?nb9uTVVV5 uܹ_~VZժU+/Ν\r:uܺtڵ8qܹsUT jܸq'9޹K.5kҥKQQQ|MQ3L fZI2wDdmڴ)Θ=Xpu~Amoo۶- 2u7W\9&&tJ nRJ*U† R#%=X+k F0 `, F0 `, F0 `, F0 `, F0 `, F0 `, F0 `, F0 `, F0 `, F0 `, F0 `, F0 `, F0 `, F0 `, F0 `, F0 `, F0 `, F0 `, F0 `, F0 `, F0 `, F0 `, F0 `, F0 `, F0 `, F0 `, F0 `, F0 `, F0 `, F0 `, F0 `, F0 `, F0 `, F0 `, F0 `, F0nfYV]ם][p{4Msm0ng6=LVާ.U(!=roSOY󖞫?. ]Y/`kO $*W$Zxr,Q GU=W4Muݦhjmo[5I56j"Zv#`p/%3ZB奜fES5Mt&]OWt]tM;uKA1_9mjBU pkh1S|4l.f;-t/7_zL{6^7lgb+HTX/iYwi뺦C]tCesB՚U1r*뺨X9lrL&MW! ˊCҳ+,K_7W4n_ҥVͦhr}SV0:}.,kͦY-Ђh2dEt4Mrɻ{׍;E yo,_w7h"o=ijr} GOȥzyoi4HZ kn{ѝJH5Ĭ?Lq$͙s9"DIʠV.'N5t]>\o=zc&mH2"J3 KsӊDDŽ&4CRy {!V|NFJ%C:ThoӮOh;E^fT+nD+?(^J녠fom:<kD䅖}EN]-ūWQ]=|TV8^sW Iq#nc M׬6Q6W;βh%\2*"X{l:/Yx5IzH5s,mik ޝ~崱=LˋLA?X԰k 6w#`p/tM5t> 5oEXTJ8fYEY,}nH{NεBJtJ|#Z)93yj-7?e wjMfUTKc^yrk"r\ɿb'7IZ]DLU-wQUZu)/nuZ ft~SIjVp)bm=-CTҎ/g ?ͯ/7ވZ|;-_,Wd^i$EHs߾n {RYv14tDoQWEvkOcӛj5E]y1 Vܟb{j}}PI谆UKi] o>F}h(,ݦȾ3`ݽ{ƍ7|KKK066=z+ܺu+44tذa_|߿_SSӣGAʾG_paiiotĉ\ccA}}!u>|{]^Zv={\z޽{FFF=zXhleeeaaa};vН999%%%GtX{ jjj۵kG@ڳgOtt4`0|>޽{ϟ.]KryyyXXX||Qvvvmmm/l^^^nn@ ̟??33do߾˗[lZt)666]2vXxɓwܙ@rLSLJŭ_Ɔ򂂂|ĉ۷o8qb AAA=x𠼼|QQQׯ_ b2< ;Kx۷o>|]"\TE͋Xlammӧ 7n|%^~_TTT :nݺiccsK.ݼy?\xL>}[Hٳoݺu֭3#(L;m4>w}t{)ߧO+Vo^OO0WSSӡC׻ׯСCMMMll\K.;vɓgΜ ׮]aÆΝ;Xݻo޼¢!hرcLLL̾ 'I644xVXmhhhff6yoVKtpww'/tuuOcǎM67*L&sѢEΝsrr/[lr @ :/5j5oooȐmfȖL8***䖈 ?-,, {ohvLDfG-[ܵ@lllHHX.f!>L5C .<{lDDʕ+ɛ+V+N!H *[(7 2wN6:˭CnPɣ|ǎb022Ql 4M$8qbĉ'NܲeBH}Ҁӧ@NNNZZlwD"(v;[iJ{ɠ} ŚJiDʑXn۶m0,,o۶m{ =0#Ԥ;w<~Xq?ɩM- %WNJ:w,[kj555 ]n W^>Hd`x<\:(++kq* :t׮]7o >|D"t҃űzjRyypBŎz Iڵ+ܾ}[vD"uV[[+7z366$mpǠ;I$cǎɖ-n)dٳ-[ r"ziL5sjժT觟~(JjMII_B!_555UWWwr׮]KPsN dر$Ț7x<6-7N#, 8rHHHȳgϞ;BM* 8vSSS .եK+Wʾ'ʖ{{{KȤ6ݻwqƑrMsΜ0770`@`` ͖]r%77b7nԨQMF> W,d06lAjjjԨӧO|~nn.ܹs[ǂBu_|yȑ$،5j믾}&%%0=<&+Wȑ#׬YXaƌ=zӅRnnnFFF}ݺu\.ťjƌ666~MD~~~pp!CLMM{dɒdÇ\%Kr\BnnIlmm\ŋKJJԼ,!ЇIAv7nhll9s0Jn  _U\\OΜ9 G%yʔ)ѲmFz9]|{ud5Lcc㺺ݻwGGG߽{L͋B}p"##ʨlذOKKk2q}obb@ kt0ᙙ<O&P8x`ݾ}{zz:;t营%8::R%|]|>/ ɡ}||5!!={m۶MD!>($_\\ zzzjd2uuuH{//.]\zU,ߺu[nO~ ؔ⦸{;,X@ g͚ձcQFx'NXBGGX, EQ7oy,Xf!P7[[[D"IOO'?rD"Q)##+솆67n֭ڵk>|Í7v}͚5'O4BKirógeH$nnn˗/p͛7Լ?Px<\Wdgddܼy388xРA6eʔݻw}!P[Q71P۾};YF$^p<==={uɓ'9weeeow߾} %%Eqy@*CWWwȑ֭w^bb>|D!ڊ ^OO/ ӕC;{-JI x/~=ݚ";33ԩS׮]M𤳽Xvk׮]VW^9Լ}!P[`ӧ HynnŋN>6 N<'B˗Ǎ*q{yyvs=ydYYYmmk&M`eeE!HddY055ݴi?7Ѕw?LG!\HJJ"ɒYꚜLW Աl]]]011!yҥMuuTl(ёTghhcٚ%%%tL&s„ bfufggGtppx!̮zxxs̄yk>>> A$ 0͛-AqԺM`kkGQEJ"##'[… nnn,K*5w`Ϟ=GuvvnllLOO/(( O>ҥFW!0"##===4wUUU={:99lmm:BAAAcccǎUWŕ;VWWimm-?B]~SyrLMMRjV655mفDceeXnhhسg !j3u#BJyyyYYYw(PXPPl|*\t N+[N۷o~SzM]]oS$sV"\rE@BߪF;wd/\ooookkkiiw^DBWڵ+%nɒ%\.>{l 011̤w?|0]d I$@;~x= ###ݻo߾ P'TTT3zhMmllmDSUUUsرQ=.^ڔJpsswtt422۷֭[[Żv5jupppmmù\.%"LQHB"##(57ǡKZ$%!!!J7oR)EQ~)p83gxP/"ڵ \.ٝNj@QTDD & [nIO}WZ% Rirrr޽ԴAť#Ü7ၫV}7흓T'/^_TTt {{{h׮]FFF*=:m޿TMLL4O>$22ƍƍ$)MJEGGeS~ "Լt\N_Cl۶M6{c0eM[BGGgΝ_~!x 3_~ffffffs̡_@ɇ54Dh B i5 IDAT+RRR/S,ޔ?#==Ν;7f#uu5k֬Y?m6=iҤv!9Iq-EÇIE>}ԉJV% I~GѣGνk׮ߗKr)JKKɇw(Bid$nݺUQl͚5p8 t nJΝS<ٽ1\3gZBCC׮]{egpSSS8tqqqG? ᩾jH$ZzlIzzٳg_rWVVy,..޽{<ODGGΚ5kxbGOII! !*?~4cDN@PPle2-cǎ#3EeggOCCXH$.]DN =h߾ 9h";;;2TFѯ[,--bhB_5Q$rSNGNQԏ?HoILLJd;A7d_RRBf2&LhA VҟjkkI/_mllwNׁ-"HD^%-GteTg}Fˮ~/˗-[F ewWqu +fj&&&dl(r/  ofnnxzBM8ģ΄ub߾}F"k4hՊ+ǏSUWWollL WZEw-VFƎۂ(ڹs=A槟~YXX8a:*77ϫs5OJ&/6;sLd…<oرt۷߿ &&&r+6uuڤ(***Օ x19};2 ģl$'<AQǫҥlVVƍcƌ+**"Or_zյkWww֚ &_|poy)Ϋnx7~4j盙QuuzC̙cmmKw R랞<#ѣoֱ3fQQQܳaÆ`6]QQA^Sկ_ÇB*PS~z 矟>>-- ĉ,Y{LYRR8o޼MG'GmÇ/_~y~~M^矷u\!] 0@n%;B!BB!- !BB!- !BB!- !BB!- !BۆX,nlltB!.UĜ##{zz*D"[b:w+u;vdeeC͚5KM!F4N_paBB\QN>?ZӻwoX,Wp8uԹs綸sl6[TlB!4Y|_}}=ظ$%% ¿o߾/_ٳ }c2l6( %%%VjYWPO<9tP&&BHSd___d2񢢢߿_]]?3̜ѣGDw"P(111ӧOW^ɓ.UlB!4A" UV͛7OGuttV^}(..xAUU:*--TfQQT*mZyyyYY: 6dĈ111ֲv[nm" BM555W\#GYFŒ3z-2dyϞ=,YBaaٳ]\\k^pw޽r 333 '33SSVdllwܡ rIM6q܉'?\naa!,Yrܸ8՛{A__@!YVVF5ٳgSXXϟ%]vU]v??S՛Μ9VNNNݻ78]!P|_`D" !#[b`˖-֭[ek^tOOOpqq)ş| J$RiӦਨ(ϟ\.WuM=zfΜIOt֭[]vR.|+VXQRRDB ̮4jh߾==YO<'''rs@1bĸq@"ѣG$DgϞ@pp4ڶm[޽2228Q1$__ߐ⚚ccc&%%o޼߱cG]]P"%r??l~@!n&+뫳 7+@Oى42_nݺX,Dف999.\UUUyyyDNl޼yrl6'WVV._|˖-cǎ:tq>B!u5H$tY쬸fgg744lb[iMdvvv<ѣݻE"z`Xz6l*lll޽`SN:u`=z߾};v|o gQ7{xxϞ=S3ùx)n{{{A]]]_xlǓ'Ȑ=RG,Ϝ93== ݟ3g:@HHvlo#ZknݺGEEݾ};""ÇOII111i 2ꎢ1b%۷o'(bXtz߾} %%Efjj*] qJ'Ǘ=󺺺J|I?#==Ν;Λ7oܸq z+đQz-foo?k֬Ǐx<2REGG!BiuYVΞ=KRSN֌v$xo5ܹcmmrql65w|Rh"g\.Ԑpĉ555W^upp++,RS ~]]QW^%-,,6?Ny1pBRD (Ξ??ܲɤzyy}% 666ݻw0w3!,V#<<<=z4{lfJJJxss3g&$$M` 裏("%~~~;***LJdR%H ?;w|嗲5}5А(]]A^ĉtg0qqqd{mmUJII0aqQQQZZX,vss; !п#22ӓ~LTSUUUZZZ~~>ٳS4666;677bN"˗NNNd5RiVVD"qvv&ޝtkkk[[[7-@Qǫҥ =y!RtuOO&xB}Hoic!jsB!- !BB!- !BB!- !BB!- !BB!- !BB!- !BRC@ $%%UVVhT:} /^|СTV>>>.\ %5-99 )/_tׯ_'%%eggSTT1*7zh{{.YhٳUٳg .BɊ,++ԠTn] ˖-344 ?x't]$ 6*6SSS5wĉ`kk+H'ۅ)b@v€Q\/LK򲲲N>š!Mg666677wqqS:$=z000lӧOKR&yYk֬/^|2e =uԠA;t萙IolȐ!k֬QLRnnnFFF}ݺulx." ̙CϏ k8z=wѣGS_UUUՌ3lll[:Wqqq~O^^A㏞={r8GGv :t޽JۿMMM\ʕ+eF+++oܸ!׏=y8Hx.]XYY;99:tH7|rWX:BE&&&VVV>>>?BY PkzAvZ۷eݿb5u5֭tҥ R4))ӧdĈЧOAEUTTtE1C6446+;pZV@@]p#!ClܸQqիW7{OԩSV5_}b|8Z6AvJF|rh)uV`ܹt:슋[;kkk!&[ ,kƍ陙;wGGGXLjFDD&L n۷/]JIRG_b$9ٞ={D"QCC ĉyyy۷o'3f̠[>i711Q ´4R bݺuyyyo&C w9rcǎ`nnl$WQxEkhoo^YYYPP@5k5]F܁EDDpAJ&wܩ{ِ!CHݻwUyK- ONl`h2rظlYO9sㅆ(gIT_R&ׯ+>PE@xx8)߿?9VRRK.mtTHrbϞ=--'Ƀ`>|n:@@W+,,$W $$ybɒ%tJCQII ]cRիW3g\r$lT>Y:ӧKҖ]fddׯ3.!_cii)]({d쿁Br~$??8u]D믿H_c=EQѣGe ~ӦMQQQϟ?\ܩ-\ HfxpppX|lG;ub899;!cVVVǏ߷o_bbbS&5 oȐ!nnn%O<www5k?@QTRRy߿?[lll޽W^m/Ϧ "ϲeEE`aat@ 駟˭700P%hr>}tY#1"|W&c k׮zJOOOvw???666=z􈏏ғٍ92::_})$&R':$~GB__ߐ⚚&_:BM={T$?z8z(pO<{ >}JQ\*"$f˕VVV|D}R>_UUtGTnH yժ2ޭK+(4Pr۷o"B^z)$e)V#WnP3fDGGGGG[XXPuY>}qr'pڵkՉY999.\%1(/!ZQ^6f̘~͙3G B|H+&WWW!++woj'O>vӧ͛GKmbx̙fffdtEsBߣ _~W^%`唗K$}}}wwwX/;qKM~nnѣG qqqd)+WMӧ߼yl]<==f'X׿cf|CCgXMkh3f;vݻ;[m8?HOOs΍3.䔖Bcsxl;w\nXX듹\3g!W_ߘo&ꂂRL"322n޼)[^ZZ| zT6$>w$x,۲W@Mqƪ*?<5?d:f͚UVVF/rGk/o&dÐJΝSZ~'xPky>|8LlٲVk IDATk׮}R‚xbp_ڱc{{kP(tX@p xb2[#G&MtP(H>}:jԨ۷9rL2ɓqqqbX(^|yܸq-V0`TVV`hj_} _|%IzzzPPPtt4h# Γ_G >|  ''gԩbeHSPBomV+))o3 :9sFvUVG455?֪>D"!s 6mggGJb0+Wա;[zG300 Y,Çjaaaol2ĄK[SSNBzʟD"p rMJ+5ܽ{7S@vСCɚw*~.v„ 0{l'E{)-8ȕ?y$R,###^G\5B5K㟋%9UvUTTɤ(J"~wܑׯ߹s&}666?ӓ'O]w>uPPP JY,V=_qFw8iQQQfrrrjlll6{=9s&]'""Օ`444Dܼy eٛ !՚/C:f8/jQit:K޽U@8;;/_<&&,ߦx4=)+077Wr_b<5[`0dZCCUVr?wrBo#jkksssY,Y ϟwQy?UUUA?@VVH$ԩ*ȫt߻sƍ1cy:W@}R͛7-r[Q[}qRiVVD"qvvVEu!//oǎK.}"===['V7z訨۷u,蝸z;uꔖ&B qyԞ={֯_j%HDnvލ!.|"ݻ濊92duVxxx[GBo5dĈ111XG(4۔H$SWVh[onܸaccm˫| @(zcccsl{a```ooogg)T*=pQ߾}n*['>>庸DŋwСC5/_rv*w.8|0]d r\.KNASnnnPRRҊaanUUU3f̰qtto[p!q4dy}MUU)SN9sʕ+ƍgΜyqφG9sV͝3gNEEI@ ())ill O*y4uuu|n٣P(LKKAхoFsyu2eJ[u0 j3eeettt 44Tq… Ņb`„ B֭[}##dR$__D@K H۷o.X %%.::C`ll\]]Mj:u LMMdXD"ߵkp\>BF.?~ NNNPWgffx<BH իW0sLRҿ9rdcc#].]JJ>|n:@@W+,,$ !%t =nDD))Q3[[[ښX/233e+VMmRmRTD!Hf|nnnݸqc}c モnee5~xHLL$%_n]pp0áؐ+CE>dggkz-S[[[TTTTTTRRB{sΝ} `0Ox!>߼y͛ l˗?cHMM%=z%jϞ=OREw獵Tٳ]v|>_$iz-?L2|ϿrŋwڕF)M//B}h4NHL&f,W^Æ ݻ7) ^>_^^nii)ϟU`B֑ F߆S^dKO>d7o=zAK;B iCBBUTpvv&;fkkX`ի<nnncƌׯٜ9sGQ\;}ğ9s%Kjjjݻ'Q_^Bfߥ# \p<===z$[^VVֲCwՍ?ҲD4 //B޽;rϬgΜ ױ)..owk١uE=~X\]5Arqq[.vܹjy\S8'AbXlq*իZWkjjU,Zk*Ru(A+be7@X’=oBZ'ϙ3s9sf&!!!϶j"z=w+W+W,XLꫯ͛sN2G ƍGrL&ŧNbn~aii)TUUmٲe׮]d|[.766jS'O۷VXA;6!^idUׯOA#|||H sڹ1ٶ"Us#G%7oEO9r$Y\^WWgeeE Om߻윓Ì&6WuC!t^Ok9q<777cLḼ[Ȯ8q9///%HRȑ#/]D<}"lp&ĸD"aXf:y$ɲH{{ǏlBꉔL6-&&͛d#g _isy5\7Bzlmm bqii|>ɉ)|Piff9R&555Y[[3j!^)<'xBH[7A!c[[YUѩ_+] 0~{ AV ^&|߾;$3%''/JR7hiiill ϪB]=N餼P+¯AccX,~ AV pUcccNNH$W*}KH &/!!h;D?yd(,,lnnf+ 9--M('Nb!Қn ^Pdee1/\3XD"Q]]]gDJ7Z=8ih)-&Jd2ٳh'OkYUUԤTHQTmmm[[[744V__M{Bim0`@߾}Ae<ϛ7JJJ=zDIQL4I5ښgD)ѣGAAA666V*((Ц2dsdddJJ,.CE 8*M>صk3fL:e Ł|||LLLkaa1lذ;v0c rkjj`ժU\.v{q/^haa1dl999\.ӳy}?ITUUEEEYXX888p87666*"**%""B[lqppӧ]llw!KII-Z<.)--%s\8x   K>|HWXڒ~yAq8:,**JfϞ=UZ[[ĉt[ȱ`2&h"];},XEp8ՀHդItsH2Ȉys6{lmccUO=vXDBWR[[fgghհiӦY[[@bbbW=B!dv/dE'y̚5'>c΅0}  RE"b1y>իW|||II o߾555$R5;wfS⌌aÆEAAAW >.Y,V]])Jpq:}ۮ_N1cFnnH$|3H۷IX[[P($Vw BPPtAr+W<~811z]VVF"%g̘q̙ >OQD"7n'&&WUUǓiϧO7e055=vXmm-OHHwy;z(9QPPЙ3gAjj*9B:9 8<) #(2V.SLυt. kjjLMM/G"lCzz:IqqqD51BBBsuuիWwA2GIIIƍftmqFll͛E"BCCCطo٤y_4:/YY~ 2zAߍ wUcHMM%_3ȝPss3)ٶm[lllZZ3lٲerpsskhh I9&x:dgkk;tP H$L6 &Olhh@ yS2w^zttt2dܻwJܔƽ'L@N_mL&#//_<}xx8vAXTTDf矽{ϓ02 aĈdYNi߶#Fl޼966"wvmڈ#fϞ yyyJK.UZA4&L`78E瓒um޼Yi%mmmDB^lذƆ333/!3)zɷn"ڵk"bرлw1cddd\p㑘ѣG֣,bHU=̙3q%O?_JpqqQ'8ty9b|N 044 䃖;;@];}6lOhh9YxW Z"PmE>}Eyy?3nnn .\XRRbii9iҤI&/(!c%x##ǟ;w_%ùݯ_?\&{`ذa)))EEE_aj2\L&311ŤI 󳳳+++0{XXX\\\ZZyL'aV}ێ?</;;Y^1*--]rUs000055ɓ'?m~rD((B(..VJ,G!u(SZL}BBL8Y Bi8''ٳu711&-TcǎYFi%Ç山}!\~kCѵkiLI-TafIIIɿo7(++t>::z͚5d}/B -ifff^^^}v6!NtN乍U%ڙH vZ &sK,9r@ hkk;{̙3%ܹs:vʕpʕ  _}ռyvIi@_x p2?`X6L˶IjGrL&ŧNR?}5fڱcG\\HKK{뭷MMM%ِ:`̙X,yyy'O׉`ڵdΞ=[i?cX|9ϙ3'''G.GEE]vM ROu4/ \vMvz^2^\\ߧKz߼ySxb66ͽ7p82.?t)qlӧɒw066&kꬬMRuѕv5rx 7sѣGHz|KKj=|>C`dddffF>lz+CB@@ìޣ#&xJKKSzj;|pXXسkۋ!bTH>]?d2YxxxQQksɓ'Ϝ9=c P./]HѣGkP:=W788ĉׯ~'BnO׮]5j)p8Ǐ4i#7xcz(͛$Zݻ"W^ [n547v xwǎkdd8qċ/>N<8}v>ۂBtH3glhh_=rʅ jjj_IQ֭[?֢޽{fׯ_!c:u 6nXVVvȑ˗/[XX/9̘1"(..E!׮][[[ ˖X IDAT-۰aըQ\\\`]rW?~2 h Oy.DRSS(mkkBX\]]շrmlllɓ'`ٽz /]Çߴi]8f̘+VիW5ІWY}]8}41D!U&xX|13fnii rrrIY\nBBBRRѣmllWQQAuH''';;3fݻW`̘1\.W%1l۶[t }+WL2ḸXXXDDDTUU)P(l`ggקO;;v-/ &Lvvv677.TUUEEEYXX888p87J>}`ݺu#+dvm| ?ɸpٓ{mTJQԩSǍ̝;t%&xpqq9sLSSSuu5m7ґGSΙ3gAjj)STe//G^~'K.%ɸq<11*>>̟?N ɸƅJ_x<ljj e2ĉI.Poll\VVƌ>|R! .loo''x+++ W3hjj*)IOOgVBfXfR2b a^人:WWWXz5]xprrHŋT(?W^XKW4$nB077]v1 g9fmBA2B!yddNUVS2cIHB%C&eeeZMI``%$?xS" ٵ2bvI>n___zXhPE"e2YAA,_y 77W1ّF~gd֭[վVRWbC%K͙3G!bv;;;Duuu U }6l\.ulR YHFoR\\,ϯcu=ekkk~~~pp0' H7\~x<GfuK/_'D4mWXX3rȐLJhGrv"BM$G*b2Nlذaɒ%B H$J~:Pٙ DDiRsUk`XYHGjjjVBMNNV& ȝмѣGɈ3w=͕ٺu7mt7nܸqc֭7n|I󝝝E!'xn ~Hlqq1j?W1dJes7Cuww'*++N+*++TH4{gj+qssc:tqSb8mc"gd"RRR(4iұcǞ>ejse "##\z5###==oߞ5kW_}rnBzo! 6?N Ϛ5wު1CC뾾zJEEEJ dn2{ i@T ٫|}} hR'+((`="зDO<4iVd2͑=@R5$]oKqƝ>}Z˝5z ټys^^ٴ$x^˽B蕢^t)TTT̛7Oyڵk7nooo:7wؘ;vD"WSSSoooRBnnܸ &fff^^^}v٥R tj߾}J[5/lG^f tfbbBw) N;vl͚5;vЩmL$J_bxԨQgϞeNx^M6mڴ9c# 7V]\!'xRCerEb`dB(--ݻw/I YYY!d8s}ä7o֬YCG~pΝYYYoerO|77od~С.]H$ݣ̙C 2N>-d2Ynn.s^PO?чWTTPUWWk.6zӦMt.-ܹ,{.yr*5\5_Ǐ,$[GYM>>>I 6|Z^zwsafkH#s7{lBA=z,L%x{=4Gk;¶vf<ϧ_Ol6H揟2a&xTJRJb۵ ^+CAdll}Zi5--mѢEnnn"x̘1 .dFnٲe׮]䍵cLḼ[Hô s<2 ]jV6WfϞ=?GgggIIIuuܹsHKK{bom'NСCMBJIIxl$n߾CH4h Sr=#Jܹa$IIII>}z\UGGǃ ݍz\Omm@ pqqQ;RdWڮRwwϜWrP##ǏktOO+^QQA](AQT~vI~!zhԩiiik֬ymyZ}ܙ!! wyٳh˖-/ޮRܣ|טBH-m#=o߾۷oK޽ۂBp!+8DB-L!B!L!B!L!B!L!B!L!B!~lH.3Kl缞?^tsB4tHCdJF={%Ki>_~{~mQ&JɇB/ LMMMMMMLL@,_|yҥsha„ ~~~nB!'D"X, ÇأG>"BHgO5j„ }ӧO+HҪ*Ձ} œ'O:::4QU[[֦M|>_i@D槬+:ѣGZ^^Bg0222233IB8pI߾}-,, cg͚rݻ7sLsssGGG33#G***mܸV3))i666PAqܚXjrtGbccmllWZUPP@deeq\@@R5p@.b׮]\.w̘1:H{6dȑ{G!zJ ޽{ImMMME-X@GM0>[[[H[n1Zرc%R,)--U#ZaNNaРAĴE'Nu dnh"{CRX\\L3gRf-,,iBjHf6ܹÇ`ƌ"3fC(Դyʄ++ӧ֒3f'&&WUUdzXW.^XοzϞ=o߿fWUU B. w B000:::KKK{_RR;;S>~,,,yFF]ɇzY&]~Nڃ;ׯokkkii)((:t(]?BW?߽{.\HQԍ7bcc7o,蘚r}B 9SN;wTΌ̬ظP;'''8p0%%qϞ=t6 qqqzc^u &%%7n&ZH w >޽{~駟~ZWWeB>WVVlnll#Fؼysll)}#ݻJ:99}g̒ӧ;H  EEQ ?L3XY>a„iӦ_EJLEEEMMM;&&OĈ=K /R,B8{{5B!F7۷o߮Thll|)J߃zئ踸cN6  B ؘ|fƍ1cy 2lٲe?Ծ 2IJJR[ɽ{Tc9W\)S:t(###<<<eee##cVV֊+\ؘbNߺ?eB}.]!`Æ =>p8ŋӯ*++_=<<Ǐ4Osssӭ,//]Ņѡ<@hh(?;;;2){4xG]zܹs Ǐ/**z#:'n?~x<^vv6RIf2@ɓI&=vaذa)))EEE]mذatɤI 󳳳+++ ,,,...--lK!O#E-ZH$_/|ŋgΜٳ Bgsdm=bO\vM0}1K\re'%ҥK5kt5%MdyBlto˗RH$:y$CmW# zJHHmii9y$ nooIdKKː!C"H&;Ȭynݯ]F=kmm%3LMM:dWWWҶ3+ٰa}IYfBc\.#MMMIH$"l6.C!D2OO&`llܫW/HTM<0gj|>=Q̌|fTSn:ysssIyuu5c_m7oTlbCrL&:DѦG Ν;d֬Y}B+[DDD;wˋbI$T:rK.zxPPйs BQ)H5--mѢEnnn"x̘1 .ԵL111k׮ӧd2[.^t^{iܹ)))䡟㑍o'OLv!NJn^+**>}Ǐo߾-|||~x{BHRRRxEh=EQ|>y|~B(55=Y4kkkk܈Mf58;;?32G,_~{A!{B!aG!s7B! z !c8DB!L!B!L!B!L!B!L!B!L!yZ\^^^[XXp hcc<DDDdee%$$;[nݼy9E5Y7o.\ۂBo"(+b6lؠP(:qx{{KRfmX,Rϟgvvv/-!$0DQF=xbyyyM>_(jo\.ʻM,^njh??3fϞo;::@bb3^Xs??s B#y<ڞ?.lll|뭷HU.\xN7#7|vvvt;wH/^LD0R^\\l\t lvMM͋n BN'ڜX~)Srs>}?XHܬG7BaCCNO)!s]]]3f>|8)Yn͛pfi[kkRse388|_훛P(/qqqqqqd&w-" {mUkeeqƊݻwizVV #WRH@@@: ??_Á)VCɹyfJJʶmlmm/]4vk$xկx<^PPÇ~addof͚jVԴw JI[op ::%22￯QbY[[.B!VĐ!C 744|w b7o2P=>x@"ں^Ѯ0*aÆϡK,:t(裏4LRY%ٟ|IVVEQ1++kŊW\illLJJJJJbXSNoT!z&$$n :lOOO񕕕 OC~RIKK Yg9˒'iڒm*** o@&-\033⭷ڿٳg]kN0Ο?֖ǷaR`@!ǴM6m O~KfnSE3y.**R,..𷴴Kc qxR'g"Mu{> 22R8|瓩|>ŋt}!6XXX$''嗪UPULMM?L={ ~@QۧSUm۶M(2ϾqFB{&ɾr>矫 06@>>}p\ }cAn/ڬYBB¦MN:Es7% >#ЫBerYw?YP(JKK$L|355MLLommMNNvuu{{HTJ4 p|͛]?iҤk׮dEO?TsȖpfO?DgϞDw'?ZHDvի|d ֶbD(o6)zu;SF9r,R055MNNCVl29rIE!{sKKK?}?jnn~MftN?rH2ǎzv4EIR "ax^{mРAyhhP(\s]]v@:KɱcHB \PPP;wE5k0  jB!=s(֭[t3_|\^^t^{999͝;7##C52&&ŅNfͪ!˺CGX.ٻ ӧOW*..lGӧϧ~#ݻw#""ss_-EQ푑]KKK?MdՆʒt?%555ӧO444_G555;'zBzdvVJJ Ӱ:\X,.***++svvNU[|P(433S633\i>mUVVl//f)r1|~ssU/ӧOee;Bz&55d)c8kVTC0228pԩSL?aՕL :d``y2`X~P۷oBWnۓ={mٲٴ5111b{ۂBÝ^^^^^ɹ}fnҥ|s#B!דw/D{{{qq1>|8*B+=BE!P^w!ЫV7! C/ &wWqBBS&UWWS뵯kJUUU9EQmmmwYHKi'C! =_QQeeeegggooQQQ3f =xұ۶mro]2k,.pʕ)Sp8 * Ŗ-[cggޮHڤѣGׯ";;rܫW*RXXHɓ'<((]>|xȐ!fff...Α)))̀AqܚXji@aa!PUUeaap6nبMgɷ2lݓ'OӧOlll[[\ɓ'Ut:nBRRRCCCHHHV[[z";;:{nã`رtɄ `޼yJu:88BÕL6 dXtdiiT*rSm111`cc#H~eeRz5k,`߿_l6ٳt QdccUƎldW%~ᇪ5kJMu'5B=C2OSLSScǎ+++xw0]<9szŊpƍtѣGIaPPЙ3gAjj*iٳ'77RիW˕̶yyyOkooCCøһw޽Ԕϝ;f`999b8##cذa`aaQPP@ڄB!ؽ{P( (D2n8077OLL'Y|t䫉'444?~ڴi'5B=mbccҘ˖-#).51#t544Ѕ\m766.,,TjC~~>|jUO8Abv,OLLTM#FNV^ͬ 8,LMM%զ3b577wYD>hРP,˼h=8]BHf׭[yɓ'3 g̘=600pE<~xeeP(lnnV +J2 Hn|,Wm V^}ɽ{|ddFmiCvv6⎎駟fYfyyyE1_"W tժ\2Nv@PJznd!gೳ?䓬,.211Q*!yFDS)\\2ܹskkk322 ܹb4'xկ+ ZYY*P`ggt$cSH333Hu'C!</.\XRRbii9iҤI&r8ŋƫݝ|Tzj\.wʔ)Ν;~xpp0y|7n\~4Ν;|>_2I:tVbN2W|}7775x{{eeeJ*++!'~P~`Ϫ۪Ծ X)li Ν;wo$k>?--X+6d2I&yɓ0h ׯ+%g~:Bg={ !y%==}׮]D"\vmĉ=f˗/9sk׮6ccٳgUmQ2e>HLLlhh ˻ᆱ2d+W,XݻP__W_͛7oΝd  >}tU!!!C 3gbX.}G/{|G?ʕ+ǎܬLNJmB=-]weʕE) O[XXAAAѣGd7ЕuJ)ӓY{gx<^ujL-fVw000 [ٰX,;٭_G1#׭[GkhhK|> ̌|fԲ;7ߐՆ̋S'k!zFu@oY,֕+W"##Ikkk377_~Og9--mFFF B&999%%%EDD(|V=/[ߖJK,III177ײZQFpNNN!!!"iާO& \.:}4IJ۲eˮ]cLḼ[Hi111k׮ӧd2 \]]-Z)njpB-;;ܻwo߾}AAA/믿>CJy)O!zgkklU(ߗ̍\TI$>}899i^=M=nnnJ/ŻɓZ.Kk&KKK5Ϝ͆{VRd) BCQTjjԩS4w !RSSy<@iwWgdРAO_ϗ_~Y[[kddD=mR;ajjʜ6VLn͟?? +o(266;v3<]W4w !Wuvvڵ+;;ܹs0|ݨ7>~߾}M_2oo_d2͘1ɓÇoҤmʏ4&''wG9rHjjjzZjպuA999i~Njș3gVZen߾}„ "2`R1tмc1®]͛7ۤwy#׮]x㍬,$99999~Zjշ~[^=UDMiӦl)x" d6E$000 N:III[n=~x||ݻwHhhĉ|M{w矟6mӧ ?~"Ү];",\4ilvpp:u>?\dŋ׬Y;h|Yܜh>|{Z֍'Ou~+^մWZ>*A^zSNi1gLLٳgM&]}g6N>mXJ?}C6\=XҥKjiӦ:99Xo;v*˫UyjGvwwڵŋ_}&Mtرym۶ݻw$''kر_ nMff;z٠Am6i_W~~d2M<ǧUV>l֭Ə_W\_܉2k4F+˷o߮Νk]W^m]~ر5jlٲQF<ȤIgnnn F̙3EdFO޽:ʕ+L׮]~5rss9rdI7V[Z?kÆ gϞ-^' {gBd6/k׮6lr*?uT޽wݭ[)SkG0]hə9s~xs9r)S233˗/Ϟ={СΟ?߼y{[n%ҢE e]i&u7n.2ͅO?Vq˗/qƽ[Z"ڶ>j:ׯ߰a[o,"s)~:"""77wΝ;'LP~}Ydɂ niӖ.]*":tXtÇ-[ի?3mۮ^*"HII-!Ѩ6onUϯVZZ… SRR&L{yUVMD-ZtۿĵkתfJLLLLLѣ* ?[N>ѩS'Yl'|R:\JMMzӧORCvΝ۴isңGٳg\tiʕyyyǎk޼y.6o<88gvڸq^y啛.}駿J*Hݺu;tУGӧ6Lw%''m۶uԹK!!!k֬9zhzz>*")))qqqRJW\ٻwoPPX,h >{>6jյ_~eǎ7]2J*7vѸqcmojjqDSN6lPzzz_|cƌ^sUXW^tt/Es-]_3fŋKxkᥗ^R/k;I2k?~j]ҬYѣGO8qv:&|**+W~Dd2%$$Zjoڴi%<]v .]hE$>>^-[$jUӧ:[ |>nK/ڵҥK)"&!j=TU-#UR6:}Ĭ,776mڨ>|x˖- P3 "( pnVul0Kȃa^k0j֬Y|ϫ:k,ILLn藝G/IhѢMm6??!:;;FGG9tXXÇ7m&"!!!eݟ'x_~1QQQju{†7k֬iӦqqq7EZreWn|_@Q_2޽qrrرDEEA.q+֭;v// z@]|߷o_JV}Fxxx^^^gϞU;995jIJJ*"իn-m=\]vڵ۾}V`U˚nݺ;88hZ۵kWz/RJ{ziwi#FОR,Y ZWt۶mAAA֭;}t~~c,XбcGրlvyΜ9ΝK/T:qě. +"u6loiiiiӆ XPPСCUAAAF ^VT6=ulЦm8::>"(=^zj̙s\3fƆ 8pc /k.,[^;~xRRҠAWxW\-;={駟/^xb 86_;vlrr?ooֻV:uT1mArrrZZmjڴi Μ9#V+VHJ|Rr吐M6;"l2Xoxx3g/_ާVZdddU@9`]&Mlݺ_Zu`hԨӧNݒvppPjۺĺ\-},V``+|}}*U;oUzv̙ ժU޽={믔nݺy{{ _~ٖhWX,eN$00Ȅ[ l]7]DdÆ ^`Xp|ի'!!A@cȰwn)999>>>33iӦUTMiiijEӧ0@D233SRRׯ0LץKoPsrrݹwE"%#ɔSXXXFΝ;Uvm\Ύj߾}I*?p իW^zH͚5oyOXqe_@FVkƆ 2aM$n y'؋[o%"rAʕ+GDDػ9&@g&@ – QQQEJ,]zX (",t !@g",t !@g",t !@g",t !@g",t !@g",tXJqqqeXkѢEiH S!##㎕j֬y],C)a b"tDWaWa3DX :Ca3DX :Ca3DX :Ca3DX :Ca3DX :Ca3DX :C777;=}'NWپ]lqlq+?c̷8a_n^+0'Fz81ga2m\'l۬U9]L6pcm}7mycmOxqεi? 8۴UU% 3DX :Ca3DX :Ca3DX :Ca3DX :Ca3DX :Ca3DX :Ca3DX :Ca3DX :Ca3DX :C(*4kMS viVviwiԬ8 ߁!##};pssS˾vj>lemq9sXʩh`lv#{C'Jd GÑcaof%r oq9k{󐾃WzپȾ'Ʈkv]iq۷$ՈʯL6̴m 7Z}zȐ!Z'O,"ڵ#am۶W^y%??_D{yܹ~-??Сʺ'_}iӆ[F вi|{ァ /_[ςݰaÐ!C._<]ڲo[fѣ... 40 u̿+55G4?W*((HHH8~xnn=󱱱IIIyyy%JZZZ||RRR9RܖbccFh\buUܹscbbTիˏ;VF-[6jG4id*VPh4Μ9SD/^l4ݻî\Rd?~|ڵׯШQ#77#Gwɦvڵ"Rr :;;жmۿ"qϋHll޽{TSve]k׮g}~AAA۷ݻ {PPCGuO1Ly[hѹs瀀 4ԩSmeffرСOǎի7z\ŲtR__ߖ-[7h 44ɓEʕ+l;-Zpqq1QQQ6m2fyƍQQQf駟 SRRBCC7nxeU3nܸzKSXXh2fsAAm#S'NdggO:_/3Edyꩧ7n|jg޶m͛kԨ!7N`Tdܹs޹sgݵ{De]8r?<--M+)((Xn]^RSS?{hM̛7o_}հaԵg?x`=P(ϟo޼s=׭[;}1<jc*t*_Z… L0aIIIͫV,Z7mf͚أGU>| ^n]rrO?ԩS'Yl'|R:k6oSf͚5kּ&rss'Md6iӦi&55u˖-}Q~~G}{nyڷoرҒ&OtRiٲ+Ү]ӧO/}ݹs~zWWW###ׯG5kl۶mOIIYz`Y槟~mK,IMM]z#wmvU9p@JJJ:uK"$$d͚5GMOOGE$%%%..`0TTʕ+{Uק-KttZazR5jگ_?Ųcǎ.[Jƍ_vM};LjjqDSN6lprrOOGDD3F+nڧYev4 vj8!N.JN;N;}UvUv:;]ʯwV3_#""ڷo/"{7L&ИRr jN: 6r…)SǪUTnڴihhho-Zd\T^}8k~wD~U lݺjܹKNNN۶m4Uݵk?͛UʒbW˯J>}^yׯ8qtA""9J*G~L&ӑ#Gڶm{n#nڧ}Ӯ:N i٥Ydqbq{/}Ԯ}ƹ$la333FٽqTK{Usj<9tژ2eJ'O޲eKjjVGӹsgܽ{QFjJW~ IIIZaZ;OѣQQQ6,,LEM6EDDȍYO=`0xzz+UTZf͚흐PXXh]rǷaEp֭"ҥK9tЅ U *Xݽ1.anU||6ٶnm~Ƽs6=]nk˴8ߘiqqcm@)7ڵ8+6mX.ivo-[Yy {KͼGQuԙ9sc=&" G~ꩧ5k6a„LZjPٳ.]./@L-Ejb@\\\VVցڴiSn]???Ţ.ߊ"( jC[+@_lwvj"#$$dW>zhaaaff3ϟ?qW^yGPVHzj@gghCCCD$,,Û6mW1ux_~h4FEEt5"lw'TY>..nŋ/.rݴWnuawݹsgbbҥKդثW(AD IDAT~Jpofgg߫ͪ_~xSǎE$**JMҥڥnoݺuǎ"e=)m+Cv)88XDΞ=;~1Lo駟[UVUԛܾŪUX~Hjժ(i~~~\\uRV׭[~RkvW_Ji"X( aEdĈ[ʒ%KUPlvÇ5L'NTǾ+..."W\YD~g.\(TO:wܽ{"IHh€_%VtȑjU)G}kP6!!a*S*:ul6hʍE|KwAz4˗ϙ3G{1cD$666,,~HMMݷo_xx;VPP4¶j7TqӧO]t5k89998Eoǟ8qb={<{lٳgGaak֯_?ayGׇPj͚5 .s窥RnjlٲCY?cŊE^u~wf6ײ2w\)Uhh`X,Q"rppP)Ryŋ/^(V zxx̛7Oի?3_~%"lzVDNټy"j֬c֭[/^UVFnWWǏ;V;ڨX >úuH~~byG:vm۶={iqƌϲ^kԩ#GtssJFs=f눩5]<ةl]n][n_M~HbXc=VXX-e-''b|ŏv!Ųl2YdСj׮=}/fff+JJ>󴴴SNڷoo.&@g",t !@g",t !@g",t !@g",t !@g;vn&ةUi>+;4 vviVv޹Nlfe*;*;222nnvjNاY_{󣌳M06zΜ;*,rj{Jh7rJ{w`{s䉕cڡc1}W~fqʯ1CW~gq~ʕ'V8 _ܷoIac'Nزzs"Ӷ޸hӕUgvۭ6}4ڼ\۞[6}wiom_m>Vxs@g",t !@g",t !@g",t !@g",t !@g",t !@g",t !@g",t !@g",t !@g",t 1ڻ`7nڵʪ;sy]U;4;4v*;^'|' {m77;kv}ӬWQ&g3ga3̅a3DX :Ca3DX :Ca3DX :Ca3DX :Ca3DX :CKR)..k-Z(C)a 2dddܱR͚5? u(~;#%LAL$Δ*,*,t !@g",t !@g",t !@g",t !@g",t !@g",t !@g",thٳxb}O4xaK]"ld.ATTT kaˋUڻ ]̅a3DX :Ca3DX ¢4L3f8yÛ4ibDQF啤f>}:tPѣC;vڵ$%%m޼&Hy˗/-IM__߲?CBBBݺu_R?&TӧM&"yãDؠk׮i7SNƍk֫W,:nݺ 6hѢ}zxÇۻU^"콹rʹs =<i4jޫW vرqaaaV!##iӦ 4|wwݠA 1BDZnaswwwww駵W\_fsYTllh4+V.߾}*;wuyLL*_zucjԨѲeF=#&M2LZ *ƙ3gŋF۽{wuؕ+Wj_1LǏ]v5j6rRwD؛?e]ũnnnÆ 9s̄ jׯ_1bDNNNŊG-"l6f-?y=\nZhb6ڦMT6nh]e6 wJJJhhƍ3nܸzKSXXh2fsAAm#S'NdggO:#1 3g |ãN:5c ݻ?[n]quugg Z7׹sgm֩S.$$d͚5GMOOGE$%%%..`0TTʕ+{ -"GpppX|y^FbرKVRqڊ֫@7ND:ua''' ޿DDĘ1cT9pX ҽ{w9rVJLL:u# ԩgZjoڴiv]v .]hE$>>>++KEX___-*}T8qtA""9J*j&ds?CDVcǎ"{y9|ua߾}7n?O:UD Ì3VZF]\\sѨ(DUf 7m!7f>SG(rLy+WC/^ܲeӶm{88x(aѣGՃV??x:]xeʔ)zW_رM:{BBBT­[H.]<<=...+++111++ͭM6~~~޲eˀL"ʂL֭[ߪ f !___h2^|E5$ƍUV=shwιчP ;|MD$$$O/F1**ٹ/*Ujڴݻ_ٶm۷~+"ƍ#Fdff޴U䤦:DEEA.q+֭jWJE+C[TVD_]ju˗{ݮ]^zI+x{'"-[|wMf0222FY+VK.ٕ`U˚nݺ;88hZ۵kWz/RJ{)mz%%Kx{{{{{k$#G>ceС_~e||_?wΝ֯5jԅ OO"?Ya)[ ?4jԨm&nYSԩSf9 f͚Q-%(=^zAD/_>gΜǏ抈1cD$666,,~HMMݷo_xx;VPP\=zG-s+W.((4iRǎҥKUMmՂ7|O~'nnn"^pA;l׮]U{9THNNVOmiӦ 4P,E[R]Zr*;͚5|E$::W^k~'?c\Z""rFXc~~~+Vx'GG&M̚5kɒ%ɧ+W5JD<<@%"իWӧOBBz).Ɛa><.]^6ۨQ#m*&//OD_#Tիׯ_wqqޕR' ""{ -)ɔSXXXFΝ;Uvm\Ύj߾}I*TVZj{oJ*UT)RXvd4o5FaÆe xӉ/",t !@g",tEʋl{wta˅"%.=Ptwy;􅹰",t !@g",t !@g",t !@g",t !@g",t !@g",tXJqqqeXkѢEiH S!##㎕j֬y],C)a b"tDWaWa3DX :Ca3DX :Ca3DX :Ca3DX :Ca3DX :Ca3DX :Ca3F{wenϞ= -{1 ۷o_a˅ {wJX[^TZ]Wf.,t !@g",t !@guafŋ-SO=}ׯ_TR)dɒ֭[jժ@oo_d2͘1ɓÇoҤmʏ_|ŹsVXϯu͚5sp5ݼQFȤInaO:5]v;wի+Wxꩧx㍦MgG~#;tмc1®]͛7ۤw~[\z?^*e&L5kb ^zǏ/\^=d'Ova[\r_ŋ322>5k֔^JҥKU~upp7o?5jo߾}Μ9z0Çw/CfN2% @Dߟ_l?cbbΞ=k2ns(Œvv/\r:W\Oq. ɓosǏܱol;}?frrO>}?*<+ Chhj&i>>>Zzg[n5~17;;{ĉM4i޼yΝ}|||||^ͭ_]v;wnٲ ܺu˗EgϞ7+_^+=z{׮]^oosРAA{xxdggۺukwwȑ#mX*bccFh\buUܹscbbTիˏ;VF-[6jG4iuͭPh9s,^h4ݻwW]r4~ڵkׯ_? QFnnn#G,GP G9eʔL˳g:tw x㍯ҥKZaZZO?ԥK" ܹsРAZIzzQËVm:fڵk Zjvad2[.446k׮f9//oƍEvgϞ5]vU%ƍ ~'mvN޿-Z樨(M6f\QQQf駟 SRRBCC7nܨn "999ƍ{뭷:&l6+jȪ9;x'Z_Ξ:u8P>Z_FÆ ƴiӖ.]*":tXtÇ-[ի?3_|Ŏ;Dg]bEll… w."IIIZ3gμf5k6o쳋/ѣG [nݾ}=z=.֫WG)TPI& `0X}Ν ===۴igϞo{HhIDATMԣG%KDEEeggWZUۥ. wСN:gϞgvtt֭Yѣ> J*]re޽AAA"bXEDM888,_W^cF\]]gXvq%cTҸqk׮77ND:ua@ppppp#""ƌ&%%}xèQ|vکw 888|ǪڡCS>{*W{dV;lV3Jzd*>{uȐ!֡nݺjYӧO|111j^ګL0!:::66V]"-SZݵkPҥQDD"W˯J>}O|ĉ{?UT=zL&(w}633S-A'OV7EСCjҥKjիjȑ#O<ӧO_|9;;;;;̙3"RXXXXXwIiӦM "Ү]3fX8:::99d}Nʕ˝nݺ"sԯ_ٳ֭߿*TTR߈[Z<==}||=ʬaaa?M""",WWקzj kɓW\.i.^e]ĴmJwa+Wl}ʕ+GEEURE+%[qjb]v֬YYJ?PO4/|ǒ;f2n-uIZjjRzſh}]V C~>sjs=W$(RHH"RXXuVҥ:t… Bpp^s_aaas{>>xum޼+:tw}wڵ˗Z5-T'|2{l]bźuVVjժɧN*rL)~ Vԩ _ت˫_~_|KпdY:}Ĭ,776mڨ[l0`iPdAYnVuyrs=z͚5ׯ_/=5m6?ٳgU~Sܹs۶m/^l6ŏsgyFB6**6V{Btg%Nk_=_nۗb+w+009777::Z uppÇoڴ_ͬ^<hrvv.C~ժSZ433sڴiZ{Uovv*"*}jq1TV)&LiN8G_Z]v]tI"ӧʋԱcGRaՃ\rV֭[e///mRTd=]u5??߾}wARUwǟ==0/2+Z.qq  Ĥ TXS[ ;\V$I*Q"J4qB\AY"% L 2L{}N02ͥc;;4YjѢEyA2Vҷ`O766666WVUhiӦ`e]&inۧ˖-}m'NtN8qwg? ͘1cԩy̚5"J-Yd]Ǐ_zp>iӦX,Zkkk[[[x tAdiq+gʕ+'OR+VXrvر|ٳgv/^|nܸq֭>?aK,<}|޽T'>}Ν;%UWWw}7rTWW棏>*骫袋rYp ̙jժ>lk.Mx<~7Kr lرc]U/_o?뒶l2s̵k׶wkjj[ouuuxx% `kIя~gyK_}lnn^v)СC.]ꦺWUUy˗/«ٳg?  VX1o<-[Ο?սo={ݻ_j3fSO=5HsΝvq)yrL4inͲ9s5k$%Io0P*++g̘ /E||Izٳjժ_~ysVE%CTa]j}dҭBo &`cz jjjf͚qFxŋW2eʚ5k\0 馛VXѬ0ML8_\hQPn _ς ^zkr7!T?'p wٳg>a…drĈz?;H kRVrsV5<rN~GV0p#GF,էփ644Oa=x`kkk]]ݘ1c\fsǎkmmwΥ_~yGG̙3x≓]X,Z{䶸?z?o߾Ç744%}Æ y.40;ƍ7nܸS9lذSW3jԨrW^ޚ.]궭n eee|S\p`,&СC۶m[~xD_":VWWT8kaKeee}}}}}%\r=MpeΜ9s)U)a14b_ – 6J<{ ;0@a1DXD C@a1DXD C@a1DXD C@a1DXD C@a1DXD C@a1DXD C@a1DXD C@a1DXD C@a1DXD C@a1DXD SV (>=ϳ}1x<}*#]ݥ:vŋ}9%dH7cSFt]{YS״ eɬvy}I(W|<Oh59!ѻR{2Ta@ڴ3k+mec$s/OsѕaXLIdǯZML#Rt𽮇7 sʍoe\2FIW#ZY#1fy U|m yua@)ze(WEJ#l?22&~'U$Le}eE%7[2*>X`Pcd5ƕMfKl?DXPrRU[媮Mk\_1&~/`o]ͼ[PY\Gw k]eu6*v[(k2Bs~rM:Rey.KY3sEԣ3wՕl7 R[Vf_Ifver)<WFw?3cvyywma@):&2C٫cCO]:Zk*٥?8W5^2G7l_QNi6X,9",(E/.= I6񠀣MvE*5Vk)_nm}㻮o՛wg؜9B7ČN} ]پ\.2Co*N JQd#98!}!zGg_]›z>sꆨS <=-",̊p-:Hf=tӮ[٩`l4RS˟0e_`8-*,ٞ@/CtCW\+hrb}Y[ fCӹ23FE"Ϸ՝\S/Ϧ?\3s(㻭L?u~\ۖi{K߫̒9ǏOOh$}j|I_oB<7ٗɴ3mmm RԥK*+mh()Uq%4isuZtؗuz2k? JN, 2,hBn8z=]0tcة#ۮ,rVw{lvp2tۀ d=uw+< Jф|rur9Q0MuO02ϟC(:5;eb2rl8f2euyZ@h;t:$ !$DzβˬI{Ƿg !c|DQuUC""I&I&Y> j:]Gu >eB-x3` "dpJuCqDgo5*9fs^&tꖏɩ:VB9F5<[wӁt:ĉْÇgɓ&MmPff޽{y2e ~C DDDtu%gݩwDz(1 JT*Hmu!HDӆDcXeq}rVaۄpn|2GDNo3ޓ'QBBwyesIqܢET*/|)))J;wnVVڵk{1`B,]ɓfu]ZB~_̙3t4;nzXaС7x#ﯾ*M4I@sB"O0iy㻝&q惝d `nXn勒JָєDɹH]=9"(w)=]ߟ'_2& Y\{+"n`7d_~%O{=j/޺ukff 7nxȑ;N6ͻuk|[3krnCo({7=  - v!D(sʀu qXa]c8#":/gbChE\0([rPtJeN<;4D,lgmuF ӄe'N_hfu,K:Q׮]CyX'55y+Qϒ%KRSS۪ߺuk>}}>74 n޽{ Wis!E@TF3Le n2%:{in>Z719DYie$rx̶T(ʞlCRKpJc?Qk 8x`ݭ^z-Zw z왒ZVzzZ$sD9ڰ(Je!Qfw\nSQf}9?j{J+=۲[:pUЁ~+V-P\GsgfУ5 6PBݾK8[s\UZZ?6hcEQ,,,l2λZ$IŇ:~9-KVV֑#Gv{T-k8"#Nڞ2'qIՎ r6zj93?wVzj~@|m!'C\T0/Ү]0}jSrɝj+)-|̚JwD\-k 3KޞJQmKS@ 7q0=XE~=*m]E\I@D/nխsDީh6SSSnjS 6(_AťKYԩSlIppG}TV\QQdɒo}r7jԨy/^[b`عs'[rС)SG}4d322n"Z|ÉԩS#F ?tŊǎ#"AL2w\Vnݺ7|ju>}^|x툈g]fM/_p??AFOwЁ9z肂#!##o߾D^vd2-_|͚5J qHxC=E[f GR]}sIHHpY?xvVw>}tn~ љ2<* H*.}\TBzssUѓ㹁gJJ?<,|ftP#ZL9;ӳ7q @]h.">vMI29<62˵:9Sg zb=55SNt>XUV#} ߻8.<<|޼yIII/}&Mt_}UAAٳW^DDeeesEQ͟?$o~iqqo1eʔ| !XEp805Ked9!q&"ѕ7\Dt0a\ʡ]9ruJ0]O~!p\P~ΔP['@-iUNJ"HLDmGk;R,k葫9o'n1y_ h Tַѵґfȁi @qatjTB(s&=i_͙kAAA7pCjj_=gww"S*F7$C~Zbbb 6}gfVZED+V`'{9vػogrx)))={$nݺ >|DtС&k}WX޽1cl6ۮ]XfbбYڍ#F>(+c4sF;?|D|r@D]v;vC=uV.]ZXX([o5rH{s)))Yf#<Ҵg^r$L@@XJKK}]"YbEXXu+TT_|Eff5kfΜID ~ҤI/zʲgϞf>py^FsU_Pejv[pN Եzk術҃WqW*"w]?ظ &+%RRwtAy8"Y#4/QP.H&C'in*UQHv\!tVi}\(8krDev8GvzGԩSSSSM&ӦMnfe9$.&&fԨQ֭s*--G}GϚ5Eȑ#v5"ZpҥxW5Er#GtZ;vŋfsRRKLΝ{ꕙy&뮻r[0&&f;wkrmO?]JSRR9"Iw?2tzz:xb^z;vTW5"2|p%3cǎ}9xM^М]8wˮ/--YhK~_-))Q߿er 7E,{KKڣepvJ/d¦`Y%I8Lv_xC]([u%%%n_Bvs z[,˲,l lrmzX,G<v@IVm0q,zɲJ}w14>e0 )[c5P!qr UIYwE.H MJJZregd7**ʹ^ѭ[_ĉ&P\\tҟ~K?JdX~84FCp8 kW(Di')>N.PC$RX٩YhX0ZuVw؟2)3dȞڌ!+=-LD\sMRSS{9IEHGp kyf=DK%K)4MǎCBBOCL;Y^-aj?锹vM>I*5%qtub%%AyoZ8q~aÆy۷]a>}l۶MUVms֫W~Di(rVOlQ=ha,w_4hҌtҷ~w˖-&JgϞ 8a~ÅKJJڶm[Cl8vgҥ;{>Ģ(ӎ@Ӱ,jٚ؎:B D$ eQvl8bwDIdۇ=FzDyT)lauMd\֔Y\%'2+[ecՙ5;l^eM].5t+)k6nۺ)Aϱݞ?u4eV7|.֭Xno̤ʽ{fwdbߠ׎&]\\Ѕ +ޝf̘1d{m|9ʰ}CwiQjoۡ'ZX;{CKJJbw8y]vQxxc=|݁<$vb!1jznچ:Jr(q)n9r]]yJN->]Ӈjcr~+rK*,DD*x̚MΝenLzKSorK:7ߊ2'Juy+bcc "Z|9lԩWzA.7Ǐ?~%<3OV֔ey޼y⢋.D.S_l"CJӠˑWBMNN $7x)e={|+++7m.3ϔ;?ϗ8ڃEjC0O^Cp.LD;;O$GR]7ډwO./:пz赭\z!s hh"7w iC5tĵufYX<{Bcn lǨw t_\V %w.<5O|: N\>R*T9y<ͱ$&S*,0nd ZSԩSԪg7/&&f~aFFٳO>p'OܹE1>>~ذal億6[aa̙3L2hР_S7{۷/ZhҤI˗/?Xk&NEqѢESLܹsfshIF~,4tP3?1bDUUՎ;^+44W_DQ1cFRRRrrrNNΞ={l6`p ImیF:b `ɆmVm m{+((9sɓO?]z&6'.&驧6mZ.]SRRY]k DD$2YM9庇Y7WO[{%F ?DT2ݞu|fl{h@ k3;+:PGCVLJ3T&~M4;=*vA"G>ܣzƵ0_~yΝ٘V_=ރ9st-$%%~}QEEł GA?~|CC75r/$IT:L8`04%~vbyל&ꦛnj-M[.--e8e˖)'N\~hܼy|C ˫ߣ^C!DD̉k1`͜u 87EH]6}Lb)Hb;k+"7e:ų+Ўi?~֚L69s4t{>א/hv{e_IϷgwؼF< J[%ql}kd9{. e رc7lp饗:R|oQ5q#GLMMҥKM:.#wIJJRJ L ܩ?ʕD.?HdVy7dȐI&}?ە^|w_\t+hoh! /hJ6 IDATkIIIQ!ĵk6LTNVq.[R6B&lf20>ZaEw Ig7{9vtƛUO-Pb3YR%s TWj,DQ,,,t.tb8q㸸8c\VkNNNppplllhl f|^h촲oGGG7at^|[ǻ gL&SAAJKzw-QKJJN:bcc=;6 c#ߔ y lҵk134D$Q0;0L\wr]%LDOJtᢖ]"\X%nmTA^ѣDžN)M]M5СCӧ+s?$/i_^AqnoJ?/BC>^1hz]:رckZw믳2Fj@ۃ |f45$.s7̙sZjժUN"\?.7^[r o\5v?rrr*++1x DDQWy@S2z8O8.sss0x DD '~ߛ㉫kvNM ^%BW`7KJJ*** Chh(:gPKь/w<)]׉cCL6-1 {<#"" ȀsŒqp$Ivnl6.VV8AT7Fh4jZ Vůgh; B0`;w A ~!B0`;w A ~!B0`;w A ~!B0`;w A ~!e |U 'ɾ?9 q!RhQŷ!ډ*~Wlu=$eX% 5:t% 6%K]\Cfk99$nW׵3vZx m^F6Vlb׵!ڼ DV)rQUӪϼV`th܎`r#]!ݺk|]e_W!@{*sBSO0EQeY8U^ ~A嚚fDQEAAh4Vj9U_ ̈́}&ɲlX#""h4Ў;zDa !3f2rrrDQ۷o޽]Vc>.Bbb`h4-^_h!nUWW>}:;;;999))ݻwiiiҥKXXX@@@ ZB0OfeԩaRRRRR/f!Hv㦛njZ 7t8~xuuwnId}] #h >6~#%%%''g֬Y-\1&##c}b493^{O>R?iio2>M6Jԭ[ &1j /<ͷ#h8؄L& <׬Y~С⸸8p:NYmƍs_O}Rw}^ UٰaM&%$$l߾[%˲\VVvѐ޽{wСhmi1uSN,yG}衇ٻf˯\sˌqk׮={vyy9u5++-iMVQQe˖\MPӬ$IEЄm[uT۷w}OVoܸ믿^|2? ټ`/ʷX,999 p@5>??_IÇW^mZ_y啀YfyVUUU=PJJ˄ !A7o{ٔ _~_O?;v,==}ܸq?STT=z'N늷^iii{ljW^6-;;ۋ˲\UUp8UfrrrfffUUhF~/Y%'|M%SN>䓙3gz4ikMNN9zWJ6 z饗DQy~ɒ%MO~T{yy*j֬Y/U6>cӧO߰aCN[~MMMaaš=n4>;;}3FIL=.pԩ4ojٲe,vJHHVF!g2"4hKRFxP"_HOO/..vYnZ Slq85x8q|+W>sZ[wwO_~}?c{EDD|U==_zQQNjժbZ͖X޽{˲p3fO?U rʹsn߾,Y2lذŋ$Qtt)S{1eGeee&O>$22rѢE ͛ԡ!7XzuYY[wAD:t߿?YcǎߟMkuǏꩧbccph݋Uv}<@Ν]V0`?%Ibͫc:Q-=ڙ[ouժU}~rf2eJaa7߼nݺ;Dlٲ,Y&? \g}'o|e\Zr套^JD_/f"ڻws4o+dY^gtYEm())idfF^ x`qql~~/|ȑW\qEPPPxbv1bDnn,_o~2bݺuVȑ#}u)䮻Zp!ߧO+믯^xi\.--}嗉K/YǬ.lwx+ b`s,;(<22p @PVVLM$I;wkyyy7o;w믿zB㏷r}Mq=\| ,P ̙3M<ݟt8'N8:W_}vhܹ70XaМΪB wqGC%+-mkW8yd/\^^ ֱ Z qqq999D4t2?GѢ5|~'OPm{2`E蘤Y/jJ 3L 駟Ν;{O,Y2p44acee%L3ػwob4GDvnz>((<>X»ロЉ'{="yM w7;K,Z.mڴt:E]رcl_N׭[7[) /^\ Ǭ[nԨQFڶmß|hr[NڿwݿTT?4Az!"*** |7hʕg>vXvv[n駟n\6mZzիO6oyN87ڵk׮] ^xAvJ`   {V޻wo.]Rj^ o…eee3f̟?Yi h[ԭ[ 6,X`Η[qwE{SNUAdeYvsbFVVe#F:us=z뭏?.rkĤVTT!!!#FXps7n܇~xI&̐Vzl_Ҿp44On馛n:qVkTTԠAV7on]@+5d-1o=zݻ8R6#ƒ%KX>uTAANh4/pGEEyȸUUU5#t+p8N>}1Vno{aaaܚnFOz54r$SքAb˖-2y$IEЄm(qqqd&lR:w|+ z&"JͦlB!k׮$K.!!!HݡRBQQQJJʐ!CE8--m;v WYS `h8h4/^߽{ ЧO[edd߿s:t0 S%KZ``wID{u]CBBt:]PPPyy}N:u)**JJJNѢk B0y*^$Z;T3.T sS=Q9"\U8;;;&&&66Vz$X`IeE 't:]```AAѣGtg-@}OhXV 痀H-Paz`JB{0 ֖dLeJhZ-k{h4999* j2p >P`R}u QrW;T04ܸΝ;zJPP *}$)77Wף%4r܊ >m>D{ c98Dmᕍ߰dv=<<|֬YV !!!!((%ڜ8ZͲEBBjѣyy#F\zU>9ny駟233Aݻwrr7ܴ.!Zھ|ͶSeeICVE۲.\q~W^ybcc1N:URRr%!AhZ9*ڵ+"""<<[n\fTTB0Vv޽O=bq^wޟW^jZm !Zڶl!h8Xee( dn0ϑ!l֝3[,zkM9İ0N$KKK=L&geeQ?|uR#>oOvV;nܸ}>}~KKK˻V^.!C:QwRnX*'qHܳ#GDY%(JCW Dy;3Q.]l6[~~W{fGM8Jb#Uڵl6{k =ofeeq7jԨ ?>fТj*v+EQ=o xjWߘ_{f@CENm9᭷ IDAT*B]oذ}"O0@?x&)22244uY?Ȇz=+))hԨQ ^%h|xܞV}ѕW^$`"a̙].%ZTfZ$(~0CZ%6LXz{snqD*2KiWuuuxxDXXةS'|p881co[ϟF144444K.qy(>>^R9BTТN W+ nԸ$`Fi [޳ɲN~p8zyEQy~ު?>cwB}EUX>eYN?Vk>T-34 It:a8N9[l!sޭ'm{_av\NѸM2C^Q,z}eew+++&RRR J[SO=ua"1cԩSR,@c`4hQbl!:Ft3ij: Pl𮪤x Gq1;'hjL!:_C.(((;;l6T 8/,IӧOwҥ19rdǎD4f4Cc3I}ٽ{ԩS1 0`hQC:Y~ Һ;A`̬V՟96"8N tZDdixsy Z688h4((Hղr555555UUUO k.[hO8Q:X,Q(W +OD!:)2PjecÌԘfbXl6 "Jhz^ ^`;b Aq1ͧN2L&jVt`0gq m[\h,...))at:6,fZlԞ&L^u8n-q F6:// &&&22200PV<͆m$nxeO" C[UVVcccـl{vt9(p8l6Ƚb$&&vO[hpBt!!}gcbbbccYSYy"{ͦ =ڥKh> 78n-q PB0kHHH 쬜DZ3YlFlOFhrrrT*'eฅ-[չqqq;w *by^JI_ p4z CBB0YVqj: (((!!`07(JԄ ?㖈v{6?zsr8,7S%ZکSJJJ.,BQT5">>~׮]0;;~JKKw7|sӺՃq[UU_ffffffVTTkذaFjZM%իWYf?{wTYn5mB)lɠl" }qDQDA(,*: (hYԮIҬv<&|Y_Ͽ{Ͻ97>-G9{liiD")(((((曕Jh'T`;wT*ٳT*0L&)//틊/_nZڵ5kRRR:VۊK_-OM<_ D-!k֬1LZz8_߾}{ j/>CHuŋfy*l.//իB !|>… J{otR&&M4eʔ>}XVVظoax ml;SB r|ٳFqСDƭj]v[oeL6e6/;!jn[,^7[1&oH$Z`HNNNHHuim M>K!_x~уSLyꩧ<=:| Lh7SΟ?v`ذa7xC=TYY?,XL㶤c[$X,{%^JԩSWX/ן8qb9@b(@Jbь㸣GBFɀ !|>ܹ#G+&![B/BϞ=ۻ*j„ @u Fv4g H 6n͛g0!W]uURR҅ N8QVVVXX8jԨ+VWMjns\gΜ߸qΝ;_xZ/$eYɤR>(L&e`xGZ-!d\sM@:Q)qv-[f0xSzN+V;vl߾}ާH8u-x>[~k֬YvmAP,k49s݁8h4PZK,)++#w}ƍ Tg *0n5Mii)!dĉ\Lzjz1a2^)Px2rQT΃$J*644@tD6 -t._̙3kƌD- TBHf BKIIx xr:mŭ_/h `*H$h]^+y8tzL֞5jǭ\2cƌ}¸:tIe C xC=4zᄏճ555I0Bh4ruprv\.Ţh E{.Yv?@2e}בoɡw.\' G!oAB8^_oB-[v5*%%d2k׮8Ə xzޏ GqJnLOO|>Je:'p8魄BKLr;ٳ'>>:1"$qKill|ꩧ;FyMvĈPŭEjԷ~fFdY3c}7.Za0[prr" )))F9yE蜄t&%%=#r{}A[BBf͚iӦ͓9p\\\n^|EdC}rt3f̦Mrss)J>%K N`FV0 (ߩ*}7S\SSCG䀬Nhƭ^/..6 ]tѣ6v f]p… **???!!3RGfC(L&V0 mLnbKIIx+H$"n YYY*J"4Gd٬5RTT~<-D"-D$FccchKRHDl6f2FcCCL&S(y7MClfjZVN3 # YeYV"`$-D"-D$xˆg6 n!!n0 1I0$s&ˆl7 )b\.剉$?aAjZZ'>>^,ӲvbЪ=)))**]@BDBB4A4`*===99Y"8ϖl;ql6tjZݵkPwb"`Tuuu/^dYVVӂBپvZȽjv-)))Կ-D"-D% T[[[ZZVimv:"{nbDRUUU\\b "%$tN"++KVdz,K5n. Bŋ"UH!,FҥD"aYab:^|aS<4 ˲X q q ,YYYd@pR4++K.WTT߅$n].BMaa3lZvtD݇a:ѵk#GT\w}ܹ3g466v֭`С#F' Beee~Çz=0zSRR Ă-!Dn۶p$&&{>ZlM69NBZ3gh'T`;wT*ٳT*0L&)//KKK,YZ{UhԩSK,X,͎++V 8hڸ%8p`͚5&V?C7ngyyyoF; ! bK.ٓK 9b6}o__X,<oر{ŧO޽{xwX:+2'Nc֭;+2[պaÆS>wCP 䄄+]^-!!!99`0:VXXxӃcƌ# B.kʕ4?'>oփrooذ㽂(¸-))),,"v+Nj/vϞ=\3@PY,RRlB <05`BΜ9Cx a=z.Z7n\o; !'O"5j*ea9dYGv?л,K)nK\\ɓHU1![ϣg@u FvСsQT!qk֬8N./X PhCPq'8"S<O,sǵaZ<ЪƭVWIzoBÆ kOL/O>M?~BBB`> 위w.++{7nwuuFq{:oB}xnᄏgAӹ\.BT և-[B 0nܸ`&J l6ۺn*_hg ҫW FV*BN8Ol6ztnܸo?rg4=ta2i&,-Z\)CPD":(@MQлl6HKF:}}=`Eڸ}ꩧztMyyyqqq6aĉ,gfdmKQQѾ}!gnu;@gC AŲdRTM&˲DVϟ?󣧤o}W7 ڸMIIyW.\XUUt:Oz\T>#4nMdm\~=!DVϜ93 }R4!X5 ςsss|IZ:B ̈́}:!![nFH$t@u Fև۷BO^WW}_l$Xܑh `*TZZZj6 HJ:"憆-].! |Tj2hvo![׽{ݻ{tΝ#`dh)-4n !֭kAYY٬Y!-4iRG -x1J$d2^t:h qNSd2kԊF=zhH7@RVoܸqƍtf+!-@DL0Bh4|>trbh4]nܯ_?Db6⋑#G6޵kKG~X6!!~v֭gw BHIIɧ~JILL9r(ƌZfMIIIffO>I& )`| IDATCd2RYSSC2bƿqv;NYSST*e2 Ì1o9u,X+***vI aNo.q;y-[+VL6n\TT~zܜ9s0- U^Vjjjjjj%gN$B@RUTT,|tNuuujǎ;whMzVT>3u*n'O|~MѼKN8[on@L U?  )))F9yE//9 á4MJJP(lυ b͚5ӧO3  X$ 8p}7X_zw}_-] :*nw-ڕrx<^e.0 x v466jڬ4aڳd왓#Bkkk CϞ=srr0B;0n9+///--8NVÏtbSh[CT.q-`%ɤj=u|mLnbORRRK"``v]744deeT*Dl\f^h4JRRa Bq q Ip`d3thlll4cT*fl&h4644d2Bw:q q IL&;w׫T̘1z ubf3VjvIaF(,˲D"?T n!!n!: ӯz^t)PjժiӦuuu[l!L6QĎ$['x߾}su8[nK.Ǐ?rn_hɓ'}V}W !D 1{fr/믿~GF֭[Ǐ?bĈ/3mӦMZK/5[4~{~|<ZRRRRR_aZW^rrrr^xA~wv'|C-..B{#GB<5khܧO3f :믿O***>>oV.]/^o߾[VWW</))iٲe>Ǐ~> vfRTTT]]߯7\__o0 CSSf#D"X,rybbD" r|CB$Bb y:DW?Æ #dffu]No%L&TY+V]~=!$##c۶m1c> ~!O~~~ZZ?L}Q~~>Ɉ#ZB=׎5J*LA9ZVnt"dXjkkϟ?R=V!n!!nXL=ztWx_-Ν;wvĉêUhrɓͮ3'._d2ۗfT׮] N>]ZZ9oׯ8R^^^UUާOD"f'ql6t'O]b1 k6{st_{HD0Bŋ"U_s5,Z֢"!ZmЗnF[n3Yd ˲͛7o`8|{wټ|뮻S-}ݷo_mm`h)s!]tIHH}0dX4Mfff.]$ ˲ \v|>0)qeYWAqk{NAIX,gyfʕ-8|2[o%xPw7lmcƌ3fK/ayIpo߾ѣG߮#8?rWTQQ!˳R)r'TQQrs{lM6jzΜ9~bDVn۶p$&&{-NcY6++kʔ)7pC =}P-͛7I0!ܻw/q'M}t Yh!ޜxŲgϞ3gzk۷7>믿ǿ;u|=u{n&qOt={=;Z(ʙ3g~ &t^MzV;x`Z$walD׮]9RڹۛoC[BXfd"IzGYֳg>s_rqqYC$o߾w}oMYpcǦO޵kӧOktrW,m,!qqqEEE?O<1k,Px^{jv7x2wܧ~:77~Xj!DR :8##n)cǎnݺ >VmLIIY` ***fϞ={kF|7}&m%%%t_#N׭[7R);3J[n:JSN}Ďƭjݰa\?xnw\\ܤIWWW'hCmڴ顇H!ByBRi[e:U&'|fm۶fmٲe˖-l޼!oX,9>uO?I&mݺرc?ѣ34,>%˲]w݁t:O݅b|>ժU6lꪫJJJ㽻![l۷g4JK.V(~-RՄve2on/ܹs=}_Yxz! L0?NR=_Ti]7ޘOIup CrrrBB]dj t ѢE| a޽f'O޼yscЇWݻw0aT*]h!8… kjjbqFFF'DIII-@:t֭[nLw-jY,@J%}tIɨQg3B bɒ%ÇoAQQ?wQYYi#o&I0J'Tnnnnnn۳,r-R=-iY yy0!jrDA8n͚5 BC3gU=;Gӧ=zTVVj4(:g,a\K.`*ɲ ~wcYV:/ӄGbea.{+VKm߾2}tJj%l6sRRRN 0nۃ.epOrgcMǭH$Q9=wnG$H!X5vsg4}ʞeͪ#{͚5hѢI&{BѫW'Orj[NUWWBR)ݻbGǭ`0|>w: ! ô P[TRl6j\#lnhh^$a}wor߾};)[a۶m5k֌3<+v9sRPP Z؂D2LK$nqNSd2k3fzj͚5%%%O>$!A3!kJeCCç~:aR9=zt"Nȑ#?۽}{ꫯ ?:`6Bh|~2r.bh4]ntDא-D3fx/^x\{ڵ}w s^uU:nVu陙555ܼy3!eY-t*$l2LTЭo\vNt(Jl*vʔ)'NKJJ^Pp8pTZXX)Zjҥ:nݻwxebxݺu"Jq:.ˏjtNuuuݡ7BǟBt Q2ӦMvڽ}+$bY8m=^Ѕedd^k!<[\@C#L&Suuuݓ ϜNpBzz:^tRIINcY6+++'''_'ph4PѫWD+\.\.Z$2Z6+++--a,Y&{$4MJJ B0!n!!n!Zu$ ƚ`B+**VkZZZ||<- OKw- }*9`>-D"-@K z!++KRR>l6zFQ** A n!!n!`9D>eYTpv݌hwP( /^0 ' 8B !`X4Mfff.]$ ˲ ô5{:..3 CqhXz5l[D[`Q/@EE\.ʢ/& ˎx<@ JYYYrz @!n!EP5ts0 V__jLGd>}]v=rJJLLUofCCC[g{}M7n[Jn۶p$&&{ュٵkӧ !7|s~~qk2>reggsу $MuMTbGdbR٭[7NמAyΝfvI0*qK9p5kL&!DVL&_|ş8`$q{E566z~7w}9sh 2͗.]ٳ']2 C+9rl6f 8//O&l.A mZ 6;7555 _~_=˲%%%vl˲v[{*$T!999!!JץWKHHHNN6 eOUG}._㶤cۛt\o'|vO?u9nzfͻqM8X,_|ԩS1 /APY,RRX,[(W$q{nIL ;'qw!JKK,YBQUUU_|FyBagt"Zh?a"ybSeeeeeeO=T+$J l6Xۺn&|73;sСBĉ~aiiѣG}gy&}(ڸO8-qx7|CIMM{?VUUÉ':EmA A%Jfse:"憆Teeeeee%9-x1Z a-SSUv_$T"H&zyيfq] IDAT25CDzGuuG}DxCw I0L&S*555Nt=.nzR).{_OX,wߡCfn?zŋKKK !7pC@B~ wq!D/\~0F\p!8q"v΃:*etZ(J9 :"5Жn>'x<_ #G|Ǯ7XpѣG?~N0a….C”F0LrrfgNBi4vN=|^d Ì?DC Ѹ6yNh1%ϟ??//oӦM.]gYܹs"t*HJ >A:Qfee1 C#GMIh4Vt3gΘ愄,̈́v m'㶺f%&&^uU(DT.q-`%ɤjrCMX,\JJ KA!n!!n!*! fz}CCCVVJH$el6zFT*U*^bPAB$BBA h4666FޱT*Dfl&h4644d2Bw! n!!n! (afjZvtB ,˲,+H-D"-D$a3A$b`9H $b`9H $b`9H Ä?\__o0 CSSf#D"X,rybbD" u7 ! kkkZL&JbX(Bv{SSb=|JJJjjJ u ! X,UUU}H$ .....Bn7q9٬N?.KC.^ȲZJ"H(2 C; v\Nnl6TUUeZu떔_Bfr܏k1 !S[[[ZZVYeYf40vi*L`X,HsrrRSSC[@$B A瀳ju||<˲tDכfF(D":g, /^0  X,&33K.eYa}=h~&8h4,b}0BTTT,T/.{x >>^*fee zb"f!Zi0tk׮GQTرc} EnnnAAߎdlΝJ={Jws8&d2]~wTVVs=.@pt:X*|%Za aZaҥKf_uK$3g.[((( h4|ptW`0$'''$$\: ~)!DVoܸSSbʔ)֭۽{wCCÁƌ@8L0bQ*ʀ)+J{q!駟&&&Jҟ9P`**=&+++}48ÄC6;+?@u `*ɲ1@Ʋ… F2dzDӕI$ V*ǏcCP,k49 lqhlg|Wڠwބgl@ `*T`6 l6744HR-ߕ\.*CPD"LNg@rMN^d'&&fffBN<2v'N r3C) FcX\.W'nX,FPnBHcc[ooMG >#]$M&)ʚt:΃n7CMMRdnPՄ>`%%%.dÆ ۶m#H3f <^e!t@\v8VB=,_)Lv+ q.K.q-XPht:`:p8t:FIIIigL}7p={\n2``FV0 (S´墫 h|lŕK.~" F d2ZmRRRZZZ||<04rT/rӦMb88@! kΝ5kwyATT7LfժUf67L`ѢE\.p8555 :u)S:#d„ 'N4iR $KyFQ(._o/_.f]wU]]M[Ν;wᄐm۶o .^HYjUrr2!dǎ1b!{ίKMMvgwK(ٳg !rlvV.BN:}pʔ){ݳgφ !<_-I*0]{Hԙ3g+kvڵkWmz^WT#V:r^'̞=o[pz Y)ZM_kkQ$D"-! S 8nGmU+W? &7b\D={$x?B?ޣGv=@=<~&͞Cgӧ!ßq8s̿w}Kq>}͛zj.]r…;09sޣKŸI)f?v-ZiӦ?jϞ=g>xoFOe˖iZ>zj>?xӧBٳcfffB\.O>}{=zXnoG1LO<!$##+VBj7ok<nƼ<7}Ԁ_ohcc#04 hll[(ggg fg! 0LrT\\\˫fnH$ާL&<3/cǎU(tƽ{6,_q/@ȠP(|b?NI$D`eSs@A 1I0$s@A4Pwb`{ H!B @KX 1I0$s@A _{wUusf&$MHAVYd1<(ůQpGE[Db]jEbmUZ,E"WlPdI!a Jdf=?0Ye |;g9s?s8!C8`8!C8`8!C8`8!C8`8!w.gT|w4a|1\=pu*;z gf0LMMᵔCq.r@F8`8!C8`8!C8`8!C8`8!C8`8!C8`8!C8`8!C8`8'h xC ֭['%%/_<ׯO>xZ f*((jLJJիWvrsg{x͛79rfdd 2ҥK}ԩ߁Μ9SXX8nܸe˖]s5q^eZzhڵþ}><---[Ddҥ^xkgJiiԩS}xw8%wdɒȖӧOϘ1cٲe>oƌ˗/Wߪb +\_[nz6lp 6,X`ҤIiCٵkWnn޽?x`;IѼy_|/@a9rd`Si;v};v;vcN>=}tkLÇ +j8WG-_J&Lk׮]{I`QJ9RDΞ=}v= 0`!Crrr /T%%%/R޽ҤI}[n݈# =hР~-Y$ԩS"[e{GD֭[g?;vKKK}>nҥKnnnq977cǎ]t pdeeuq̘1vΝ;=Z (--mѢEӧO91.HS "`픕ٍO?'N-N?)S"_kfnnܹsƢk׎;0M6߿n|ϵ]!8??7w}q\O=Ԏ;"'|2>'N {kEľ5-ϟg48v)"}F'''w=`+>}8}^vZf͚?<<4pz+oݺ0 ׻~f͚v9lϞ=֎://7ްb;~޽aH5G۴iSXXw`0Xpbx~-[V~UQ3Ըqrg"*=ztTpK8sʕ)SgΜYr}g[.w՝ʾ[^X`RXXuN< \BߟߥK;zUj\s?~֬YvEĎ;*B8%0͛7o޼>{-kݺ?a„C>|駟vڽ[ 4MWZeW>sw6D.HU[e=kRRҔ)SDDkmʦzW'MDjjjvv 777))m۶?O*?[j*@R>/{رcVj>UVڵi{-,,|iiiݻwͤuq>}zԨQ .cn晌.ivkY!f̘aݴ4(`_6lV|uDqqqyyIHH&8/0@0LLLlٲR*S)[@iigH D)e?4k,*=V8`8!C8`8!C8`8!C8`8!C8`8!C8`8!C8`8!C8`8!C8`8!C8`8!C8`8!Cx4M0Q[Tr2K'=&9բ4wwYѮWtWL*@hڴ[ٶY\(nqSjڈ SKi~p`ѠΉGH0@/GV^MTJ)ED?:nϩ-Q".$)IHRZ"ENwGűoliL.KPe3DkSDSkJmݷf${$2mڗުVu`X|zP%'Hr2m! iQ~ȶNo1LNTހ7*.i'&y ^E0;DPJZ)k^։nu<6_"0'[jaFjŪVJE"M(%V[L2`>)ejZsٚQ*VKuF}b8rbfB0@,LS SỶB7q]MbyUw{8|@&!bjѺXCMqNoq&%`\=mk]6j[=!HEّ7rGTOQ-edOWvҦJN8-vUqMsuXޮ+?;ꙡc'?Q=+w!bab@C]rN#nOrZrk%Z=ݭis^5Z}6uT\JIi 'V1#~! `U,8Q"亾s# TnzJDFTpb7ӦN=3#7Xkj=0*nMѾ1ND~=ZHyzq\N# )"j%2U8q/5fr;|iJT7 k9KZe7 hPZ)J)mVfoKIda`KOw\](Dcd5~y {ZzWEߘh0 РP9DrF†HyPjH0@,L-y$V)@˹6xJ[2[j"E\&"rXNjY|K@f2c:bZbi~ݯe߷6}|7%7nNuRGV`!baVT>صbh0]6PL|T+>CkĨa]Qa8-g걕/"2z-n:$گl`X6L bje- 4t1LmF(y_$%kwIe4K?-͒DD>+; + -*ա鷪v-u0# v̵R6LehepjBǵk)+7I\}Jx 3~yh 7]T[1Lm_dST@_|#^/{:b!3kqnWJf+i\+ǥ,nMxRc='J6/|/˶G}ohv9"y6B0@,L-aJ£HM#2o׾&  VD*׽XVhD_ \0T@ #hT8*֠IAj5L`nk3ۊR]nT7viH׶n6g \053[z" RV{b[[CIАg\O !`5^$򰮈l븕Pl"ڐ`@rR;`Xt2qɫr*`]dA3?<`X[%GVwv\C‘ibj[uފ] ,"")mi1n6Šq13M|o5_%|v(R)IMwm}rez\ѵ)"4@ ~ 0 C3MZPJĄ'`.\.WRRIJJZ[ 82K־Rʊµ*8Aci qpB0 !qpB0 !qpB0΅)LJIENDB`././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/docs/source/traitsui_user_manual/images/SetEditor_demo.png0000644000175100001730000007441000000000000027543 0ustar00runnerdocker00000000000000PNG  IHDR(`>^%fYVUUʳE+<;2^9, d6M&Siw*4T,dyRƹu-\ݩƵ]]@lnoE1...#U@ QUuUp7g)UѮtر[H9,+W\|9!!+(@q#T%dyUvWݝEQTU""_';<M߹kRKSPprƜժx[%Ŧ9+MDɌS(bSUQшq#00PD.^L(a*$l;VWgŦ"jÉmwY<#H``+W_NJ (v6g'e&D1C 7ŎYnCʕ/^hXJ (vV5We/m1PŐEQQmd{JJ (v&g8UQOFP%}MQ$3yEf؂"|c@YjXl6[iw*BPl6$kD U^aM~XT8*$T7S(J殖:!+dHXdڹz$WCj^(y*$TmTF QD>6PKU~=49ؿC\]]zKZ:9ujUUm߾} %A  PMYꍒ6 MYv!1:.fyIDvV:XUPGy|RKاuo̽)a~ 4h0|DDDlݺd2M4ɑu[n^^^y;vo#FԫW/BGfX֯_tSNũZjڵk0W^...EBiii'N7|3P07dٳϝ;Wngy&B##T%!3WP ռ[#ED~9)N|8#7R׮RO,SdoF Ӣnݺ'Oo>PŞ={-[&"'NtPE>={V{;}s,tXϞ=k,ݹs̙3gϞݼy6nxر֭[9 ( 6Q/?5CU%kAϊoT9Wr:@}Nڠoǰ HaAw#F\vMD{֬Y7t>SNY9+BPQȊMRh7ҹ%oDkXmb\ǒަZ26EFU¿mR-1cǴiӴqqqgΣ'%%ٕ_x.\(;> 0bTMUE̹(2Gd-bx\X̻uD:wS0=/o/xҦvBxݾFʰNJXm>[|,߫wS߼]%YI ,[jUQxXTU]]{""Ȏ`.Mͻ-WQE5Rwҥ.]ܹsCYV x7zewj''{wѵk6֜4iRxxx˖-?ɓ'ܹ끁V!11?\reFF(rwUDe֬YUT^s?kV^믛L&Ov'NȔ)Sqƙ3gl~;za7 _DCBBr yF\xQpww{Jcz)iժU+(sȟLS!jZ]Q@AMIIl?) o6U6bƒY*VhOhܑk=;jHL "cU+N&X5{e.ڸYnrj""rZgW{̜zT-VzϾBqZZjvBjxDtt݊z cǎHժU}||?۰azmh!!HmG'QF*sX?Pn(E TUMII)p;UaX,l& No}K!e;jJNs(֞{1Ff}%-Vޥ ;B* [;ˆA~qhurPzumC΍#>%9L(g#-n`75-әgSXlbUbmê]Gcm\ άs񪪗۲cd[i%G/dDU͡~7cD/|kb_^Tux{ܲfޏn=/ii""##sgfB͚5C"vL5ut iӦ6a߾}y>믿DRJ;ve 6h ;9L(Azzz/`BAjM&zTŦj>TU MM9<նC]9X/N'(JTɡV!cEDא775s|skط"!Uoz<%k-kr]ԩ#"Vuǎ9V:wԮ]`/*DdvOedd Ӕ}JJJN5y丸8w"44Toʸ.z^C[`×,Y""&_gڝϟo,߻wW_}UN;8|2Q T|+MU"*(bSE"=N^+ _7y8LxȺ3%w5Pi+"a +%rӱzԿ7,̒ٛ[# *x]i_WCtywXl*b:^UdA|dw;e%|֭[7i{YxqTTkn{UcbbF裏EEE aF{4Uv/^#FxڴisĉUVm޼`+[BBBF9k,6mڱc{+r~᧟~j>͛7g/4cƌnݺyzznٲoX,fJIqC>#BP6ժ*!ϥhwϙ02G((jΏc_ʢS7_""-ߎ+վܰFfIeyr,yN\.LڟQ~F 7mZ'դ}]iVCnϯ\2vNzRQVإT<ɓܾO>${.]qI|z.]z!U׌7ٵkÇǏ<dY /X?jXbŊ >^uԙ6mȑ#VW_}eI1dȐ~I_z|(s'c( Xm6#jzcQ9>>'}.Dޘ֡;pV/??Jck֬cd|#2a|馚YgXrh?-C](Ka LJ~3;w|<ޜԕ"(_}ժU=9dȐ>.N1ڵS'N|ܣG TVXS;0NNN|I~E֭ŋիǁv̱1)/9{l׮g}oؽ!!' E...>v^"..ҥKAAAn9~'%%%::ZQ´SLbbbD$((X_Ol5jԨ]v5nyTTUMHH\r``` />6l_~*fZ p, (v魧ss˚xuÜ>6]Joio"A 0 rU@IDCQO@؝s80BPL>FU((AJVB!Zv0J (v&AkMZNˆGmwvv&%'0BPL&wUv2,+ͳTC E(9m@>ƺ1J ( "d1Xď"f)U N:uիvG!T%n5=f]#Yi ™nv9ɓgΜtww/@CJlj;&&(&QWc6E(?ׯ_pBbb/*ܫڠ5]wDF^IJw.NUPHQRڝH/aIIIfedddddgddXVK(v9REQgggggJ*9;;s f٬VweTot@B8BPp *!T @UB8BPp *!T @UB8BPp *!T @UB8BPp *!T @UB8BPp *!TQRRR_^ڽXJ 1(UiӦiٳQY222 xrrH|||1RkժU^44TDׯWo?uٳgkaaa?Sqiii :99=VҶ{9o޼bz!r/DJ*@n)+SUڵkViE{y߷X/r Xl6j./l̘1[R{d"##o}9|pQ};ڵƎ /S?o>|^< qРAӧO/R]lݺu|ĉ"yAJ*5hРI&]ty衇 B;1,VXaXvm ׶zU(aƫW+{L߶6>}z,l` 1<ANE^ی#G,]W^3fLzzz Ȯ̏ػwoTT]+_T%RJׯGyã$_bzꩧׯ/u]ݻw7>4N"{_%K>"l*2Xrem۶]xjժ%ٓjժ5$_Ao>ǧ5jT|k2ᄏ^ u%ܙ<wQrwyмy^DTU=pMٍ7(J.TLe;TaP(ժUWZSO5O<{n}56?_tIۮ^z.]Μ9aÆsxyy5iҤI&-Z뤤^ZիW>X,6l8zs^Z~} }ǎۺukddd[jխ[\n]ttt|||*B6 IDATUƍMXX_|Gk׮YF6L=f۽{/rҥ;0 ..~31p1=#_u)N:ڵDGG^reΝ"ڢE}w-6oެvIߍ׿ EE 4w-˦MsԀz=l-oyŒnݺC]pAD5kּy-ZnRw}7,e ((O>v߾}Ǎ7q/R+?rHӦM펊^zutttllGܹ_܇[Pc&kf͚4iweEUl߾=..Nnժ՝w9k,mwŊPŹsƌN>}РAbyO<{뭷6nh^޼yaaan\\VZ//r>cƌgj'O-[+W~XبQyթS疯{ѣGvӧOѣǜ9s6KyLLtڵo߾gϞv ~ck5k̜9Sn޼v-pe˖i}ѮEFc<ZZ={}p['O|Emu˖-G}wifY_BD֭[of_mҤI~w 믿:ujrr^&39.oX.\INlj'8.==}ԨQz\8cܚ5k^tw9&挊4hPDDk\8رCߎ1~$Á|(á Ţ￿UVOrs l6cDd˖-ƻ]С̞=8P;,,8#==J-ӦMwM&S 2;ڵ+=[?>>~yرc+(({zի~<0}PkLV裏,VXf?Lbln.޽{Nm{~Tܐ3f.\ٺ<8{?n8gqr1Z_?}ʕ+L7.->bszyBB1&D$>>^ώahԬYVZx͛7_rE _c…Z+^x~~O۾|[lC}վH~G裏EX,SLѿsڵd?SRRm//e˖iI"Ξ={C|#GGgOdO)S߯qVZ;LΝ;k+Ȋ{%PEJD۷o2d0rNIIIٸq*:u驗*EђPh jjժ_#Kjjɓ'ڵmdB)\\\}||Ο?ѝ;wnܸ111SLK/d=vXnk.~3X'NʧaÆӅe³>/>r&LЫ+!C@ `yС'2281vg&68˃ tA.7-5jdk#qqqZU- qE=22tPmC ^hj%6ʴ:bӦM, gggEQ!!!ӶW\kG>Ӷwޝi}ۉ'#ڵk-Zo߾o]_J}pf cͽ{ݻ7z~"Xʺ@c #NNNSN0`] &hkReks]ի_2vիg|N( ǿ_EOqKm۶q,hn_[ʓ-Z|ְaClv#˓IN_ʷ]7裏VD./*Wl\;|G~a}yPoQrevZe??ÇիWO:*:uԴiS-<}vpf(dիk>+X8%͛7׫WO_aÆh#G 6l߾}ʕ}||-̚5;k׮m۶[qSl61jwuUTѧv{ͭf>sj_ruwwwnѶ@Zzu&L3gk72**ʮ̙3qQەV@-BCC وG*~w]} 6_dIǎ턄‡*6lؠowuz길C)))6<|Rg֭[>}WsDF6mHx֭[Ǎ{أGC/ֶ]\\uH8( .T^]sq_캭.]|Gԩرc~O)i52;v2'|Rp…;wumѢ}i} yqҦG6mr|3&СHNN޴i-3f>#--͸>4i$~wHw}7>1Rƈ_}Νʿ#GN)SWM6ԩƱ3f̸|;aժ`hhqo|eڵ|wE/KijԨ!7t?_뻭Zo^OpO?iYBBB%ܙ3gqڵsuu-TFUo۶M߿k`Ŋ=mǵ-[֮][c\ڵk!C׬Y3cƌ¿&MڵKoիתUgʔ)<;vW^={TeÆ իW }N:U%!11q&L pBDD?1pSԩSre ,<<<--UVǏ_rehvÇzQ`ǎ?xO}<󌶽sNw}[ӳgN>][Ml6޼yZ!C <~[dވq trrݻ͙3g~_]t^.yqs˃3g|vrrSVXawlR{>}L4I/^ءCEb@Α}k׮MHHv٣tswwӧOn-ǹ<8vرcs{ֶǎ;qDmb|Wv/DEO<񄱤sDDq(wܱj*3eoq폰ܒHC/_6HZz++ҷo_c˄ fHjuVÆ 3i?k}l^r歷СCއ5jyTh׮]> $^.../m5չsgWǬYDiܸq믿m[ֱcj7^^^cl7nvΝk_(((hIY0eVZvcxG1|<iܸ_|a 0IZxB5NXDJK U\|y<[Mv˂ڍh߾]*:tj6^z/g^X("z?د_?аpX>''^jԨ(Za@@ᆱ.;=''O>d̙v_o&nݺ&Lk֬)yk0vsҥqŋ۱kd2}W&MjڴDc .Z:TZ{?ݬYk׎1nK|=|f=o~͚5?NPPP߾}#""^îqyťE?ɓW^ݰaC㳊L8q޼yC |WV\}f͚kذ1xdV{9 6v%%%?׮];uꔗWpppq锔5jygϞRJ```ϟ?wܥK4h ,ˡC._c ,%%EQyw,##C߶RUո,E ӧO_pfURf͚yLKK;s挶fӦMK~$EA|||tt+W\\\k׮]A.fY P   @UB8BPp *!T @UB8BPp *!T @UB8BPp *!T @UB8BPp *q*ܞ%KvȯGy5 )s$e,TѸq͛v/vP& e*45ߵv[5.>vKppڵkK\Pp *!THU<\iPE9Oݍ]Oq~z5 ZEdVxx*sгjKa^"렃rokw5tI;B;@*r ޏuv @ 7bgZUB*ά;FU/tn;ƅ5=oEc Y>>ƦYc"b(rwfZ?ZF.na.Y\j^j^f'""}LGܨzj^݌lV|4&CDQ!Bnv|5v̋Nen'MD62mj9&L)EDJ-PPi+U;sIJ%NB]Ul&"1W"=fXm( X9UhqN3SEysewxb(]ȎSiWۊ PP# q>~28N?h5)ҥ$ܗ""=*H uR@+硊ZU]=S}WF`g3y[r6sf4X9U:h5y(}\o[_8EMl*"kȚ)"r3V5LɶgҋTL0) ɶ]&WDLH牴+mB\tfPlyBsm ӋU\zBD! M6Nu2IF*SbQ!B"Ls /HJ Gikyhݡ06!zb b+X UT5kF7ۮ}Um}SvY'vJ|&"Zm7JZcv+qC}ɪ^//Xj*f/MɡP>GU2Pp *!T @UB8B8v BW-]{P&18BPp *!TH U?~nSiwXddd\tI(8;--v>>>R>GUN[l?vGB(jWM<åK? ><==ٳ@TPEbbbnBBBDdM4j%v _~JJ3<\F OO<>>^U ԫWϯaÆ&MkfM81 7((?LOO;-[Jϟo”)Sh,׿_j~EFF4i#"G8p^a֬Ym۶YkǎWXQ3 @ϴ%%%sǎ^^^˗/U\~=...==ݮfpႈj%qqqW^}/^.^8::7n~xdd;cX{=pر}{7x㯿vT\\\\\\rr]5s%Ν;wܶm֮]۶m[|k״?陜cǎϙ3TQ8źu:uTvRRR/^ߵ1"a8q("ҢE%K̝;WDw@I**8Ŷmۼׯ__8e\tiǎ `ƍnnn"h,VqСC-ZkMmQHO^də3g\8~˗/SNz;((`yBSȜ9s7M-Ǐ6 F5jpuuՓwޮ;vk[lQUnٲ套^_/]hѢE)rϞ=[˸@ +@5kOҥK'OcE_bccsSܲW婧?<==矯^z֭_~e?GEE͛7oذa~~~][nIIIoRC|͂ u&"NDDJ*HRRj,>hd2ȡC:p@Zzj)9K9wޝ:u^?_uTT믿."QQQ7nv(rpvvvrrjժeن OPQUuΝƣ-[V}pwwoРL:U^)"NZon,vwm֦MۋըQ˫nݺ?cΝw@K)zԩ;y޽"K/7((YfUTR]=J|*| Æ 5jXB71~qHZZ( ZlKحۥPEf,/F5+"fy+VKt5jX~}Nm6b ZhQ~z믿:KD=<<~m=ƍ;vbOmРuÇ[,͛}Q>O5EKɞ1?fs287oEVTTTbbb݋q]JJʩSl6[^^^|СC*9rZjAAAy,jN{ӧO_` 70pǧ~zgΜ9sg}?qeodɒ[ny}򓟼=W_}5ݸz~C=N;U^+<*.\xϘ1o>ꨣ*̙|dxC=餓,Y2hH?O$I*ۗ,Yra ` <~?oԪUN=k+rꩧ.[*O>tww?c=?W:;;ք~(~=__3VZ[[;;;/_wu<]9"5}8ছnZto~;..6s̯~!C9[o}7N8!pW^xᅃ^g?{{w…W\qŨQB^{/^{GN=‚ fΜYymݖ~c=G]f}~\.3yꩧV\yG/_~/9s~644\uU<{lĈ?g}v]w?`3f,^xW^ye[[ҥKy׬YsמyO?tgg]w'}oժU>=P);뷿m~^s5g!~_~ya̘1/6C^zevm{lʕK.~B={s=19b„ r˘1cB~)S>?^zEQ/xҤI3g1bDC2eʒ%K/s50aBN?%\B89>/-[#TxS? !|v~ꫯ^{]vYoCov)W:nܸO}SNTw}{We!rL2#LSO=uWw[N:4>\qGuԪU{8 ;Z>1pazkoo}ݷ{ B>okk-xI}!|+o3Ϭ;w;8?zQ<"o;bf+WΝ;7P*wyiHtAggg*Lvԧ?δi* ?… -f !qѣ9?nInN'>1eʔ )Sv¹{T:E*x\tIaޙ7o^z ;TSƍw̙U (J5\3T|%#u5>b}eN<䓓&Mzg׮];p{{=أ}w[qna>|gotWtM'x"I('=9/x㍯JGGNJ+*KioɃjty=g~7X?p)\r%O=^{uAM:C:ujUj[]wuЖ#G1bŊ9*3&veA{O0afU sEn ^NuOgl6xmxKOw C^x{qϝ;~/=;'?Ixx]0bʕ+VmbҤI:T*r5l0CDQu=|^xyz{{O>ٳgqGqN;>{ta$^{}cO=ʹocBr'xg>wwo~|g?=}:kx6SœO>9 ޻T*=ӃR /P{ BoooCCG6{7tO NX_%WHq| 7 {/!gzŋ/_^8??g>:ԘOsԧ>gy_rz!38׾SO=u 'wwF#\s5\sMKKիCSLy7SN9;<8.Jmmm{wz ߿mʕ_|^z1W;+_Ûoy&MX`Aav:ӆ7~5>, f̘17yI}}u DAϭ'9rw{9[[[CWnnnַ\1/1蕧M_zvT*tAw}wzԁ=z7޸^{8{zz6\}ջKOOٳ,X0zN8'tXjO4eYre> y+Hq~!\gܹRi]vx}R_y~Mf͚{} o|ӧO?~ܹs.RWW7Ŋ+ϟ?~¢ow#FX[;;;_zva ȸo}ЂԒ!LUWI8.mmmxn*Cm|]]fYD566Z"E!]}ɒ%?BGyK.KW݌F1bĈY(F/9Hul `͚5; !rS~K_餓&NX5E6__{#F@$UCj * * *rBUl)]]]B! @ rbqٲeȖlٲb(U@M<Ԡ\.ظxjdKYxqccT5_PW\9o޼jd7oʕ+=`*6566+sέX6s+--- E=`ryΜ9K,7nѣ=az7^+Wj " YbqԨQb㩧.IT{\-|>___Ҳ۷j R Y\>555uwwJ8T, 5LZ֊BP___V(׊\.yRԾ|>>0yȐBl Iq$I2LQr(=`*Jrۛ$ɶX+(P(B0PMjE&IDAT5+I|>_A S\^fMWWך5kbX4jT5cT|eժUW!W{PaYMM===---5)jmmmiiI+LlRԦRTWWWc"ZWWW*=`*%IR.=-\.oKHqTTQ*8@OΪz *VI@HPj~A kI@H@H@H@H@HfEիWW{,PT*-Zh…RcURYf…/8X(Ih~6ɪU^{7|}U*az7,XraP3fرc;0`&U_fMyر;C> U)vuױcVw<@-qR` =1gΜٳgwttTq<@-1h#Go*XlٲeZZZƍ7z\B*hll4iĉ-ZpRz^z_7nرcbl `vi &,Yd…]]]Riѣwy纺jHrcǎ;ve.\bŊ8/^g#U[D}}}l\K jPEQV{ [JooozIPr\P@BIjEQT(V^]l)W. fU@M*6Ş˗W{ {zzblRԦBP,;::jV,_X, j")544qovvv655U{\T*VZvچ U@*f\.ݽhѢr$I$0#-B^#Ŧ|>_[T5+xP,{{{8^?Ll"ݒ^$y j@ *8oJl+"ϭp<j6.@H@H@H@H@H@H@H@H@H@H@H@H@H@H@H@H@H@H@H@H@H@H@H@H@H@H@H@H@H@H@H@H@H@H@H@H@H@H@H@H@H@H@H@H@H@H@H@H@H@H@H@H@H@H@H@H@H@ 3U$IyԒa* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * RO?]!B0~{g"KiB!&e[Zǽm05"U"U"U"U"U"U"U"U"U"U"U"U"U"U"U"U"U"U"U"U"U"U"U"U"U"U"U"U"U"U"U2T$PK*8waa|>DZ Iqa$I=}.˕J%gqJ|>_WW7W@FQ. }twwnد-]tS](} M+˥RX,1"a&TT1ޅ(Jhnny^mS2*6;ȐB[Vr9IADQ|> gT@ZܒZ;Kj]=' QTyvnv҆bTJ}bk3;ߣrBIp-"$IY$|xfQqqP "UPS,K*$Iu+(dj`󉢐I+P(w$IV #(N*'&$!]HZDon7v@H;P7};)y"R&C9&|%% 3CM|Wi,); S'!NB["+l/?cW.JLdx;Nw+lZ'鬊TT $=r1!l5ԅRxsuzreE۷Dw$kBx(pE^wPi{<<E7\x!^2-[rQZT~_9# l}G"Iwg>?~<*䝢>VN ><*\]>psBUj̀ TE}SYN*5鬊$mb]H?TKS1|ùƿ{q}BtxEWߗSX$_rp_囊iW5I$,.fU'Q9邚s(ft{Bs1~b?nx_?L|/b!Kts]$mwF6I{anaE}g~/ٿf!IԚrQI$~? !JBPHzr]>2+?ͳEBYI\N鍓Bs;5&>lͣX6TIBRyϓru҇T@[VVmBG}ߗ?tü7Bo9aȨ f?\![EcV^b֪?=wq)uknr^Y&#BA)⬏DXw/v^tUVDq(IPz>ֆBf6S*R&FG\h(UkCO25|f20"J'˚ͪ]wH9fh+rݽF6EpȦ;鱼c$5)'zjAx׊d زgUqC9zPD;IV]B{CBX6)m|k7 oKB(\pL4ioIB&')9CdV&NB9N_~Xí;lEav;.{iCuZ#|aMO㙰Ncs{}Cʷɀہ6IR5 E5=0'gbBҒ?;W<auKr~&D8,~Wp`ذϪp&^wGPNrg(l%akSlxd,QUqcB_P8}U;d[=CT@)ǡ'8CDq[Nz!ݘ)Il?ߧB.L?>5ާ>gBs}!Bں";o;M޴hˆ}Do'}9#(ǑY{I%Fm")Q9*٢{(V5'7|9avQ. cۢc'NvBֆoyfU$_' !ֆpQ]~kQޖ6m["Qb ޽$*IEIQ8 QBI(ֿ]*|?O& ; JeX"z]xm[7|`qaἣ97mz˲}L% !V9N.rwD9п*/(\qo:ք?0{BTn O!OsW 8qO _t?TYOO_\4) ﺤ!~Ɔ-aYgxmyx~QX[6M`o b_ Ԛ8 xDQH ՊjcsT$By* jM8[>!TB $TN)U!UPk*+;Q[?d>oT@MX€YQ6aQ<~9 Q4R5%67C~QUBnBa:P^Ar|oף+lzg(Dn7mX,r];_#HԞ?P˽ I|[nwG4pKII9(*5x̼\>T.r&ȖɀQr(;Ic:R&O[&B?b`|v鷡DE!8I8ct!.TB.]_l㸻׺n}W^\]WAlW߳ۨU{޷cS}}}n8T@mT*Jr`VEQ>+bn"8Z B}}}$iX+%A(J*eLm -M2D2D2D2D2D2D2D2D2D2D2Xl3(gIENDB`././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/docs/source/traitsui_user_manual/images/TableEditor_demo.png0000644000175100001730000021524300000000000030040 0ustar00runnerdocker00000000000000PNG  IHDR@i pHYs   IDATxwXWgv Ŋ Xł&?`D5 QbM41bJ5ШQAD l җ}yy93svgggN㒒dxWZVn 2drT.pS,bvvFd2EdDQjFPsWM?E133S\q\I PY("/^,v\j,AX;w|qbgQbZVբ<@aEBVSh6Px,V[ۂn%Z C A."ǽ^"Rr2JZq$ "zfV)&LR11Bo` hZ333C*^)g8:݈\Y? 2Y=Cc xyE}xյq}ԎT~ⶰ O;=Kω9Q)gUzz}"""{ݽ{w"vښ5kڵk7qD-,,ٳ2/I EssD4׽z7eyQs'Hi>I[ckF["# o޼iӦVZ}'.!͟?ÇK,9~mۈh֥P//86++Fy \\\:vHDoh„ Q/ ,~B(=!'zgz4QG,*}ne2٨QRe۶mM6-[, Pn PĬX<==^M<h:)oSz9>h-EaKHn/܎$ fOȗCݺuuKnZ~2{ػw۷Gm貼B.^ٳgDԸqI&Ư^s2|^={~z">|x?FR0`mn߾}޽rDqСChʕ7ovqqAmjGjnn^f W{٪Uu֕C*+^ԭkgs:qD+ DDA8A)8L .IOqre< UvIII<̌2dȖ-[7o^(-ZhSXND'&&uV`eeU '&&Y\]]÷:t(8s̼233;FDDԬY3d$wޝ1cc.<""V\P(>@P(( >6mڔtjڹsխ[}ׯ^3j B{EFFzyyں(iyDNˋiyQˋ@@v9[לNZx!^TN2>7mA:ϏU_|Ye( `Z۷#""gǏ' &XZZRn<_ZX_VRf!Zjo޼ +;;͛Ҙ^>'!CѣG^w)k?offֿ?:99mj,YڡCtԩe˖Rp '''''x9ep~7)'33-[Ѹq㤦Fk׮^zZެ<>>mڴrsskݺ+>}Q```)>dpHk.''޽{ƍ{-䡟3gSRR&LҧO77.];qDgg>}omɒ%yKXcPUf "zە+WF4ibaa tW baStCBBxݝ̟?_~:u:yd>tijժՂ ɓ'o?wڶmvڛ7oݻwرDݯ_|W8qbDDt֬YE裏‡~R.\ֽ{{>zh߾}DuNoQQQ:uZzupppiW|@Vs:u?b¾0ԽK +Di--"_Ϙ!OX&!!a7o~ QΠK.8q|.)zu}ԨQ׮]STmgΜ>|Cx77CշhѢA|%%%b`|͚5*[л ~))鬪f͚fO>駟SN6mի}^6nغuk"Yr#""ʟ(ʌ˗/tQdiݺuv4p@"+^:d2?/~]N[z꺋V^moo]|m333---"jׯ_[W5vܶRԭ _r/6 { (uK:u(',[QRرuu֩S'6z ;996>ɷ[zykرØ1c̙өSիK5ݻwgqssV/ sǮ< G}]3"O>= @U*ՙ3g4¥>Yt]C/;v ڴiӳgϵkײ@(0`Ŋڵe9;;ݻl;PQx///RID{}۷r77/)kvvv˖->}zm6L;3bĈI&ږ~w^ܿժUR:]r:99d={dO0 $ǏcccYORCVЈPTдK.5jxi```߾}=d^QVڟٻwoZ=jԨ?X/RRRlll޸qիg[۶m}U19?7~<,\ym4}٩ׯi]5͚5N">}Ν;333w5f)H'l*U֯_up*%%/^xuƍTd={ܴiS``۷Ax/iӦ9s|'Ǭ,i 6' `ƌ%ًy ^566*(HtKE;Sx\>hР~ĉl:BfN*X-N<)t?WYK.ݼyujrw~֭-[FQϞ=NbŊ7nY¦P>ŋ .;v;w4 {RY!Ϧr'-/n8˭ 6?[rsr`EN_T_笧d^^^^^^[lfmmZK}=ׯoaaAD͙۪l2/XkzMn߾;`~1ǟ;wz;vDjPFDQm_111D^-K3;MiQku T|+'!CիWaֲωƍG>tӧOCCCW^=}k4)?=z|6m>r "kӦMO PV|}}]E/_s͈]v1|#֔y/>y7m4,,… Ǐ{ bzҌ{5kր^7caٳ;u8j#WWW6xwl?KKK//blv!]``͛(33s͛7o҇Z>}[N}۷^zllڴ)44ܹs={411j /.5 }iccmll  > ȸ{nBDcƌyɶm۶mfdK-,,7qy׮]>}CDWf#t.;P]W.{|q˖-N<ٹsgHwzMuƂ}Jf͚#G,'+}W\E ɂDQEճI ⢣/_|֭yռys-Zh(Rʌ3DQy^x-_<11Ϟ=-_T.^811122ˏ?NNN޹sޓ5kֈVs.9/rtgor8o8K~uWy?|N~]T*Xk(.]%0`QVd ̜9s۶mEڴi~-[Jշ~;sLV*^ϕڶmnݺvI%d/LMMw=}t h4(VRs8p}:;;s7bĈbGDԯ_ڛ 8} }?j֬{=/)zWަM ~uZt[M)x}5B>y$%%f͚;RryiݻTƮLz ,Z4>mBƙכ>g)^U-' 111/^prrb_'+++22jժk.eŋj2ѣ*U0eFFFq+>Z(J''VZfZmff y',k;r \ xt՚AL>Uev?}$+ҫ9uV23ڌ UJW>o>«=٥Y4kϫVU  L&)-$9ѷ~x"ձ/s)sc|"ėUlT3F &LMM={f@$ċ$$$c)"^ eIIIS# F 5&{e7zM<\~ousWԡxc[yóLKK2tAe]z?!bUr/VKZ k6GbB ᙛ[YYEGGX~}C <\YtWaKEGOWt jժ<D;;5kbLS(bdNKQ:+xZv\^feB055}wLMMSSS_Rx1GQQWɷBjjd!_FfqIIIX---M.c4R$Fh4*JJ8\nbbbjjjbbbbbAyjժX5L&S* BTJ;bx(2L&<@%b刵 /40x# 0x# 0x# 0x# 0x# 0x# 0x# 0x#0weưa [M4qqq1l n"}4%%%;;̪UVF C [AxĬ'OԬYɩ3xDQ}vZZ2$&&fgg7iҤÇ ޙ)O1 J.333!!09333˺<Ń*OQ*ϟQX=ydҥ'N0tAJSBBΝ;;V@ XTlƸ\0 }ӹsnihÆ  l߾U9I'ŪULLL ]G!xFTTԲe4hЫWץPqy3׮]4hP޽ Y [nV^ŁqK.(-Zm۶UVr\J/̵*NVVVhh떶nݚhڴ,,,J 68qbĈ~aɷPBr]rr_Byr@P?^PnӦMznݺ]zzz:88 4h9^x}#G::zŊɟ}&ٳ/\@D&&&Z͛7o޼uֲeˤG˖- .^8--mϞ= lݻwV7n9sBPG=zhjjСC.?8޾y@rڵ?zBBB{='==>dgݻhѢiӦQ\\Jb(55u{dUV;v8RSSϝ;sN[[ا]~/`655qƍ7N}tڇZZZnڴ)88xڵD}t! … Jr'Od`8|k_>|8#F1Y6#hf͚󼧧?VlO믿v}?={FD'N￉H&Ӂe(cǼ9rӧ[ZZfddl۶Hcq:ST}ݿ?ѣo_,ѱ!G0M>,m֭kԨsV/"wuu֭OްaCVN0\ IKKKJJ"f͚?~866vժUN믿 DD/^oLĽ3T*ppJ1cFllx{jccnJ Я_x"߿?kbggrʈzVX۴iӨQTh]vK.Qk ibbҷoʕ+׮]sww԰a؅AĉY퇳~zCD .dR󟘘Ç8qյ؟ܰjZϯK.z}||XLִ &ڌ5nxر.]}4$ĭ[e˖ɨQիh=A]~ƌ^6,))iӦME*zN":qDjjj֭gϞͮ˗/>|+WKrCxΝ;ᇁ/nժU͚5 k?;dzڶm!Dt JU~p--Z4f̘)[k"RՋ-ں~zK6mӴi>u]v|O:^n]___6}Uƍ}}}ǎ}I?fpΝ;Ͽq-OOOݷܹs4 ծ][n-[,^uu[YYծ]&1<QJJtgqvvfĴiӆ ,AD=ήVZvtݽ{xծ]OP^^7zCR/J~xUi$6]Fbbb=zT3-{lڴiG >}OR5AQyxx/_<++/_~ j7ЄswwoԨQDDĤIZlimm}޽8aÆIشsrr͋ƍvppήSN4'yM8ÇSL&EfĉץK;wR@W׮]7n܄ :t`mmG999i4w}TCȑ#}||?޴i7nh4֭[ׯ?ÇO]Bؚ7o3|uƲ'q#GdT 6dI.YdիWe5!KݠAΞ={9DP;vLں]ڳgիWLJm۶VZ/޽w߱<|֭[uWwMP$M6;wܹsbccZ-qs-j <.n %q\]Ro<6z^pq3,N!HrS IDATyG wqq)Ieo!BD...e/))`a "Ad2YiFcgg=\\\\.UVm߈wdggǿ;UT) Zy]JV-,,J\Ȃ j׮]A/ WZѣG666zc˕?@ڲcǎ˗{xx,]4 uTishj׮]uU؋F8Fi4;ct^*:H)ŋKG+sQ [z?jdlR!Pe z̜ޘԴ~Q*+ RPA( sŹ ɓs577ի?k."j߾}k LLL֭[Z[ Ν;$%%M<ͭvׯ_~:իWڵ e<1111_,X;gJ--Zx#Sv_~%66ÇIIIvvv76P ](?F 0x# 0[1P;~Ppm)5F<@`b;/ ]2tySWWWn#C weזΓ.BB <@`F<@`F<@`.TtM(oE]wB+S9w1Ju,VT+]fi?Ұci "JAם2*`(6 p"*8 /ZJ`5?T(_1wSu$)‡jf:@sFHX;2&;Scͅ"kL*H?|z睊YJ\ϋz~bjDԠEWfpk@)lD5SGD5 `Q;KJzZA Td [ YU~?*ѡrbW6s$/ oEfL[6M.FB"տHHKF>U;'Szwd7kB"UgƋ*,# ೲDQ(nkuK%kK_y<"K[gT GRz^"(6ryM?ar^z53eO%IN= z%o`:òp0 CђվUEJu1biG%`rpjMjr ]W懊8_5<<ÇDԦMɓ'[Y2TȜjF]MhPȨbjfOP [zp6euzrwM,\wgbr?nVWE~0Js=/ ZUW>n@{U г?Sj(\Z܋ T5hlok9ώ{a ,L9_~N~O }-FZND3o-$avb+"?M!LTVob;krZUeH/8{_;40JhV{'3>::#UT+VږǪ{.}a*],F!WYeHLkl9òylJIK=TeTV^ϴ^u,asUDԩ?cG9֨\F.&+qdo#odk;2sm뚺8襛rMZ9Z곘ȹ&M Q KDխduM',;WVxe8a^gkwM{sqԽVY{rZGiϵ(u<\ō#/Vݟs8z53/vanDt1R-E;٧dQz6:آ '&&޸q/^looOD W^c4ydJ 8C%x(mTm,d_:tgʟޜ;&wUWlf%kΌcl-e? E5HòwaM]mߛAEn**jZ:jVu =P8@Dd|$`ǧO997&缧쨾i^*jʵq@K2R*U5пp(z, 0zjwފdeP@qFRڨFlem&0OdP}}9U_ftjQ;Lؚi1"*ZRsp@ܖ(ώYMQ)>E୬f͚%йs^#LPeQU0fxAAQ:m;e$F Ǚ-l _{!q;n>KZu`_i&֞tI[WڟG`ez;ew+-T`8Xu^IC/\TWY`ۥ skG`RT?!<綪z}MYk/x H f} H{adWQ4_ÂJmd@|6c#h오ƢvL=´yS4XX7JۥIU#uGvPcQ['Y3GT ]5+BJW_gU6X4,>U[Zkyp;NYe@V hg_osyW+)d3ޕsG&+p .Q"yE3{}xRuS#{L(CVyy#š(󓒒BBB455M-2jT7 xֈ4^}EjUWd}>vjd}Ŋ*'}uTD 4ˊR>2RnUߑKR *B*C~4uV]9\wz "$EZcz 2m./u ?d[~^9?I.lV*}۫݋[Lup4lJ.O++ Zrn],Ujl,LTv}OS@j1V!9rBXyog/K2dA|V5E}j:z'N?.TcQMY"2嵚/<VdNKU%bzXۿ9"B?/O ]uP:=3xJt= ^*ܔ@'7T,|W]!6Xmo-V*g1(( PdHd@~`Q\o{t-U<;;pܞ<b娊߮eV7U]c:,>g7-/>}*{)(u\Mu#~jteDU]lng@\:Tf 4ǃ\1ǙM#- 9zm?5b jJxڌHI˛ƫ9E$/ >+,IP|[lU KO53Vb@A FbF_5u [!G*w%r+a:^nVeUc6% lr~Gm5G?%elpn"Tώ\rIBN]sdg^Yh w+VW7gr2VleMY_vgӣo ;e+>LfBwfm'eQW8q_v'X hcrV5e?{RGNU]>f7cQߓ|i-𦦦gΜӧO_|9,,ԩSl&v}Bv&ڟH)±GFsuɠ9+W׶REHv\hwro;5&ʹn2%1VQϗZq*Fr0Exg_tk*"[Ruk|2WYLtx!TK/*pw{pAV>?DwVEʂ|Ms~B|J%WCR==Ƙ뗺w_WJ[)& i#Fpww˻}?g|x`czҌ^(S"( djc׎eRûhS#=GE6!DJ[(j.Ӌd;MM5[-ѥ"k1{?} S AG֐eg*ŊӬvP"8^T0P{+5f,@%I Jt=K Ҷ7Sm7J>O$wت 2iQWtjoBI)uwz۩SU Sz*+C E%- '-Td_*TV W-j2QóD+4jUf0'ry#|ZZZNNvkk똘YNT:[@tZ5iBkG_{;nnTLw3P CRA3T=~=;sm=AbM0dBt-𞞞{_*++UTTzyjbϼR wܪ{#w TNI-"%q JV65e pG葖&Y)4c7n0A|qh2 u Fu ωx%#[w}oоwN9IPKKZjнZrRj)F"u(8>P,5&bA8isl۱f颞#&>G<"/{sb[Ե8)nU3+t*s@Qp{z-Xqy 7Sr~e|LG{\2j@mo+bf_-2a|ĉx񢽽}UUULLLqq1\xqSd3a~I( =f+]5H >,xUe֪zlƁo v)9U]2D۳c?J*8]5ۯ+S >ӘZ|dN6Wl.}ȵ2dh=&sy!q8M5"`?|XzKIJ -}6"ݷKCb&]5^p|)snbY%k/+t5;o'}S7ټ=wfjcdnV*]#eJ k0&ZCA7o=.}ܹZ,6XԂ%?EK*b m֛W xIU&B@D9_ N?  sxBvj:Z_B-7o\4X[Kz4>!kL-+<-!oy+8=k*?aYv x*kS ]n:߯YJ'lo2醨 wܬ雖d[NYbj׍!qt+Z|i&nV\H)zB!|w(k%I} 9O8k2oy!Q*/q"+XT,KV1L$,: Fѧ.(V=l{YR[ ג*I^ۓ{Q9 F(KQ㲪Ŏ-kb,BSsC|Hq빼 V I.h[F6`bDaEH.R \ N1пpB>躊'W6iXUnVmt7ټ;s$[^>.-YJ?V'N;/RNye!r^3s1.C>呂jtW&`44vttА2L-kU4TY8'1Ǖ~X hm2ddq8T's)} ck7)jôgeFBH˰ڈUR!{Ԍ>yނ`C[3]mtԩRA[ni/XT6*U* 9w-oyYU.*E$nn'F@[ʐJ@4*:JjWRT)#!`Ǐ*JdhY͍RZZ aaa @ ::HC+ W|aL|ld@R/>,}|R s% r0:MVYR!xI\PcE)B~NRqy°UuL/j`yYprEm<ΔС#BwoN:6wuB!Txx<ޙ3g8ޭ[=zM4 ѣGi&33&}3glݺUWWxx GVWWstt֭KOgo%>r,<,֭[mjjҤUYYhR# Ξ={exŝ;w+|򜜜^1}&_S?~:e##zESA5 ै5jԛ7o-Ç>ܵkѣGO<8jx<ުUrssSN?csgDz So~6c e~eC^^wX`ns!Y|wOooo~޽ŵ*o/ס=~umŊb\euPcDE N!C3q.22r3i${{ݻ_tk׮}~W^%;;v xԒ<{|䨱EEE=ǻ}{/_ݽ{ $䎻G3+7I'1Gf̙t.!!dnݺ5U嚘 ={Cmm톔ESA5%߾}3Ou\)SHyY]]]iӦ]r%--~~z;BѣG`ϟ+v}:ڴ9vh&&g <[nŽ u?YiilD6mZ=%éHII! A]]RTTt 7o΁O1OHHx٨Q,++;==¢j'99iZbSE-[vv%Keɒ%`$BssWBC#e-uPcDE LMM;ꁁ͊ܲe yk.,bݸqcΝ:uիWPPvҥ{ݓNNԩSu릩9zׯ_,,Y@@3f('OxbD3gθ!X&&&{.((>}:}kii=:>>^d4/b988,R(n'tBop8siݺ+**d09yKr 6BKߺu{Ϟ}Nnv; 8"D5Ƽ[ow~m< }(ܙmaֹg^}ML-&L͛{n/㱜9HL5j:uF<|YluܳŔeD]p]~=;wi@ +(ĉ_;%F TIyWbI=kߩuͿmDH,X@EEL&b$Bc0u+#y=/p(A!P\\\LL q=)))!!aݺud G.X/͛>|8|pUUUcƌ9~xuu5".\+>_UUE?߽{:L4 rrr]&V>o޼W_ёCzzĉ8 Z:t>]B zZ|zcbb2š|>?==ŋv@p=>߻wMTgϞ4 w֮]M7FDŽWW:Tz<:*red / '䉓~KZAዊ=zOx"JtcUUISD痯\?`pll\'k.vv> 777-ɒj>_QQ1ewk6x=ZHȝ)SHښhuls|2z99JJJ?/AC>ْ߫5`gqo_}=*j\B:WX!NgnbbBФS&%n3uhhhee|򖗽MWE8F\ 'C荌^xq #EGGBܶmvI ?y򤺺?tC@ffË㏨?CUUlĈ?|\\\|}}cbbƌSTTo߾vШ#:"$$$G<>`$Q0b ՋQ7nxt̫Ξ%%%}t>`0̙uBW466޷Ϸ7<ؓD;wzVW$AɖGzCQԢrs2^7nܼ|*BJ`oorʺ ӷX]fccc``лwov͛7OkZuht;MMoFKK޾wC zUB!oyY4uQ3R9d_HHȁk֬iYYYAkjj^r_>%%b9sfĈdcΝ&Lw 6@ll#Gd`ǎ}||{dIqq(jĉb|||v}…"3W555_DܴiSQQў={rsscccĝ;wō7H?W.]|||.y{_tn>;wHOO{)Fv6p]lY-ֻw5QIᅮ-?uԷ~r9Hc1aUM6ܹS$8ۖ4G ԩc6~3-33k֬ׯ=qlllΟɇڡC1cF7.^&~~@QԸqcb3+׊r||OQiӺS]9sB 7N:~yOK=>Z~2:ul<##1cFb7oTZhA(WsY z:OǏ~.G_޽{GQ>AD^^^Q޾}k׮K.)iZ%0w߉^*++޽%s^ᦫ"#jF~vޭ?l0MMŋ|{=9P2ʕ+IXjX1///"""ӓ?GGGx)I۷O4|(رcZZZ 9:D BҒl$\iIG/>#:J`}|< |;RSSO8A.oݺ_7u\F<o 駟(~I^ᦫ"#jF~^v̘1-RUUٳg'''do"'%gڴknƍ[$/UUUddYC<(En]t!jw&''HVVVǎ%_*u85qXa}}}ھ!|zСW^%''ϟ?IKK 5jP($<=~S\\lmmW_y{{%-GES&B{̙s6Z[[]wDkUUUdd[Ş%Q: }JQXIMMΝOz 4%%ƏGotuuiӦuJJɓӧ}+* u6'͛pq)ݣ׶vHR?;~"66 >MJJY =={ý B2̞?{]RRй>GrssQK5{j###:}r ]]݇o_#sɞ} *--U&#סуqf͚g|ѢENrvvݹHVRKK߿~TH)zn:&9p@:񦁁5kO͛roeԍN }Æ @%.FEEa655%ylwtt5+66 9Ä[[[WI}Gxsssyk | d2|{ƌCߟf6F!݂yذaCzzz```EEŁ8d2=<|TPPdGS8!_|ENȳgr;''7''8:iZ7TgN2'N|:-~:}FU, v11|>:ut%t}2#MAQ#G>}zC:|͛!ýŽ&)lwvͪ̌ /UTT9zL&u鏋E|Zcǎݿ~sڵ ]]]L㦦+V|Ç{MC1bD\\H^.\}d|2Hi:4uy=MWSSrnnn^Ĩ(?۴i/ :z%M'OPSSL=cP($xAZ#Ch:(1f<ɜ;w.ɼsѣ.\8sL|ėZ䧑tꔝ#:MhyQ=\JY/fe$EڵWϟغmW_MkW2@G扉>h'H0OwU4&oΦ82ي|} w3f" xB HEEŹL;;;w!Iŋ%%%!!!^^^dڤIq=yhz"_&_۶m#+hii֯,pQdWRRd2юu, MotxUU.]^To_~!wҺt,sss1cK"##Gm+jΝ$DԴimۦTbKc``Iecǎ '&O\Mˉ>U]]=h qǏ߻wiܸѣ~-삂r!իf|7 <8pٳgeg@BoM3{X0;;=ڵo zС#?Eu1ryXS:::js^qqbkkwiJ8tp?,X|БV_g[pɁŞq8#~~eeefCc2'N}?`5RSފ6Ν|b3Ϟ s6~~+xܹs/_믿s]veڵ׮]8P|M pMtʇr?899]rEa0L{{G,]4,,l֬Ydq˙3g.5|9zdM{,i8AA5L&Қ(C!C>+џM̏wոg7l}Ś# =[lovvk֬xF迠o۷ڵއ:sL???"u5,,ӓ\?vvv/_T-/"#j.qQcv\rrr|||rr2ppp%Sq SRRX,OO>w-fCRޓٔ}(kkkxҐ##@TVVfkk xZJJjBBbJj*henޱdrxX,kyO_}sER}~B0##3''аuk7䓑@ HNN.++igf7[e/WWWLR-r}yLLmϞ=޹euPcT@-< $//$tuuR %''wڵxΜ9R!>>=zuʕ+B)CCC6[ 1vZYYqν~-))CB!bٸqQwܹsNѧ ƦMDB!֮y{۳E  ƺua,B!I;_z5azn~-[֌uC!$;7W'Lj.ӻ-hƺ!BHIxcjj999)))])Br311>z 榦jJ!BHaLLLLLL!ظkB!Y5<_{Qj!B!BE]]]MjaB!BJIzzzMW!B!P 't6}:Ǐ_޶lbddTr}̚5ťˠO;|XXX1G-ƭ[&LOoyݻwΜ9sĉm6{պu9s4{!T˗D<ڵKK 9?}X:,,<,,|_7v %ug_|eii1stB򊎎JKK~zߥn/-ׯ_oӦgwػwoqqqmr\umŊb\e7cƌϓ?(T2P>>>~\.wȑ^^^]عsƏ͛7/_y˗/Fm۶yn?}t```1G|a#>}FFFGԩ#["*@rꂂuko7KN=k.^ڵ 5ׯ_ݻ)S[CCrk֬^p!]͛#Frl6oիWNNιsBCC_~ٌGToDu&gff&SR^| ٳ"A-BoϞ=5BH\3}͘1믿Ν;'O$b-ZQvtD=Ny=Ȍ|XQQ)S5˟>}'c#ȃիWFғ /,, y=dgDGlϟKi _y->}\]]8p ""l!!!0a:zqFڵk⍏ @R^[~~) ޽t[ jy=_|yxx֭[k,맥еkWmm=z?~\$Ù3gN֭\\\,,,LLL֯_/xuYXXXZZtAWWwo޼-bX,֍7Μ9FXZZ4a#WPP@888H>q 6-ѣGͻu릥5o<# <تU֭[;99Ջ***,k׮]dKvX,V`` <{sb200ҥΆ x<^&?.$CGZ:֫y\K9~`٦]~=wjk~o[$siv;Яg=[YXM]bbh<]=c]=[n]0`u~m۷pl'+_XXHtQӿ;w;8־{_SK~\T£s>[Y 8͛zz&z-=t/\/^DtqL|-[۸ܹy߷-!wܹuVgMMM@(RTMNtcc:T=ĵn*$f0Ʋ733d? ePKC諪zn:CCCr///џUUUϞ=:ujjjʕ+C&R% ֮]ϫЅg͚uA7>}txxÇ[jE6|2{͛;vY>qR;ڴiZUUuܹ+VX[[zBoW^cxv*,,tpp=竪?~˗-m x= Jʼx"%%-jeK;}ÇK>7pƍxkLԄڰaOs nh̘ѢI{ʔɪEcCCss3BHZ[11˦?$~RPPpD@@;vlsfJLLtvv44҈flawɓgϞĵv.;;N%"znȑIIIeee=z1c!Chǣ({yy>x@lX $ J[[gϞ=/^z?CǃTVVݻ/""^6^QQQKK5ufffqqqG;pv/" IDATyC[xyaaQU^^|ذ)f\&MhggSJXzF|E^^~lLئL˲!olO"_ ~ߛ<-0uƏ?嗵GYYYibWf~& Z4P r8cp޹sg--;155 JOO/))'V홚̙3gΜIy'8P[[v5ʍuo[bW ¡m۶#0(+q~M^n^ǎ=v\FFV|HZLL,!D_AI+WC323KKKE+ԟZlmlm֮u/// +..ǟF92XDܹCu 6O>/_=:;;=z6pʕ* ;v>gaܹCZ?>99Rre˖ڵ+++֭[lֿ~PPPF6v"@CkvvvO=======-----~~~ѿ׋ }S+++!;vHnݺ5iȐ+VFDDlllDTWWo[O>m @ѫ7+ⰿ6nHҥK׮]^0P;wN(&&&۷ݻW>x +'CI{ADee%p8 .jnn`?}ZlωȀޛ7oBtuuw!:ܽ{p8fff֭KNNlKhjݻ'(¼hXfMǎDg[ES!-r^rM999=g""nBttt6oEt¹7~gO?<.w`z.'=BŽzad4X^>>hVrugZ\\L-3f̠WZjÆ 辰mo߾zZbE/gv$¶mzeddD hҽm}C^zu6m]Խ{mRWW'3Cl666666E?pp—/_V^t)Z̚5kחNOOp!DIIZUHNN]j\k޶7Q޽{:⩮N4ٳ_lO<6qpǿ-ЁWD[l؂VBZkմ:1r%tvvv4ɓ'0`nnnmڒݻBWh#GBRSӾrVB£k;gQ-]TTzmivسQIIك1kR(ffe[Eq:ԩO~ dg?<㩩iW.^/B7sZ~1ݕ" Ok"G߼ž}EEsn#6[׮a-;99m}DI!wޭ )))˗/iKqww?|0!G :YϩSDiJBHyyA._\^^^]]?899%$$B vtB?ϟB~ܹsUUUw qt <&%,Yb}4BΝ;wܹslaÆ=yϤ8Ζ-[fϞ-Zh"uȑ3f`+o޼977ȑ#bMv-$$~J@@sRRRjj*{I{|2e. Ϝ9s̙޽{2]h-} $ʳgσCĦ~%߻vۊOyFf֭;{Ut1|0q{B~3.6 xٍ7 "##V===???v\^^mW>'L 7wϟ8q"ٙ8N``?~ÇWQQ 633{'M=;vwܹsh}s222?͖GFF9=Gπ^tIII>5tPڙYSSrl&6_׭['֎sXXXΝE$vŞݨ շoϝ;W/^^^9Mђh9[9`gg7gbk,]!/w^޸6lPv=3yy[xIEEsg)7QVV"}޻w\nϞ?Gѩ"eee'Mp9_4Kpn\NcDq8}>jjjNcDfPs(m'~͠'<?'m脆tcahzWVV5jMp8m ֦\]g``@DV񐗗7oӧŦttt?f{ ?m뿕&~GI|89o\FF 45 V\\ݫW/ssW%եBz쩠lAFFFeee^a"eenݺkk7_EEEYFUU#w&DE=xyO\|RWWi:!G|geeUV2i& xBYYK.MWnW<^bbRbbRm-s sݛUMM > ES&++ÇVVV .c&uw###lmmE瓮&666))IQQW^ j;ԇv+??hjjhҙ####[B@0̻F@m<@sAh;vʁ4x) <@R@ Hx) ?Zqo歸7 j_M=x@Mh[#-kmZMh[+i^G#-knWxW%n`;) <@R@ Hx) Դ~bcc͛wСV3 *OXxc7YI7}ٴiSffO?$#G<|077p-RRRz}~WnmzֻO˗J-Y["pͭ8OlggmG75gk3[ Dž +VկS[[{!oo]:::/ݻwme?saaac[{Kttt޹i'Xҳg5kH>/..nȐ!a~˗/ْϟ}ʕaÆ6//ƶԈx&.ߪ;h'fƀ@100Xv }MF:::w%---((()))66ѣGoǏ{zzZ[[vm߾2x`xh1 3fc!:::&Ϥ6.>>zͺ ֽν{y{?hˁy1ӧOxҥZZZ!!!wuuĉcbbKc[ǣ}+_n^^!DKKkРG$;h'*""b޽>+R ~wGi<4a p܃7o^xwqqSV\{j| .^ }@Swb7͟6}WEEE9zOosssݻwfs=9zhhh(!dҤI.]jοKGh+66VQQ2}t;;|M3[4`B:4`7%|'V-qss;w600l}hol[xԤ:I7>^)));;;NQPPسg=}PYYd=y499![F4Blm%%%a׮7pPT\\܂V)((mll8NV#l߾wo:t(!ĉEEEm6A6!ci$|ooM6џj^^^4HuuE*6mriiiFFFvvv/++PYY9sL}}}cccSSSeeiӦ51@aߪeZRTT$+++++訮naaѧO:ZNB^ZVVVWW=v올%;i$sB|||dtԆ64s M]] 23;b(zw?ih~:jW-٫og0s֜&.))739O?}i?#ڽkfflu5rw>|Æ {ӻV !qqlЭO%;yts^@x?.]Z\\,''_4Xa7oBlmmŶΟ?iC6e`4~NX꿕}~7B_|1aׇvHj|IIݻ !˖-С1ea`fʔ)a7mڴ'Nr>;d,_n]??lll&OD2/**jP(***~ŋlJg+7Vh#'g:00XlST̬,a&LOKuu9gΜ+(x}_8={t.;TSS0~܄ hzĺ ]YQS]SRRْ:|jjE?zU[z@] VZZ.88ٳww&=+//'^"~֭ifۢCh.))}vaDuaRRRn߾]]]ZoK ̟?wЁևIjݻlmm\!'OVWW'm%\.wƌ111!!!!!!ӧ=Zusʎ3K 5kQҹ=0jW<A={k佈gO~tBHEE4sÇBziwnߤ][]v?niVӾ}^Ϟ=7\.-n6l+WzNEEe٪}upp=ztHH߶~5cyyyݻws 7h>q^x+//ipݚ`nn.++es@;DOB[+@0 ~["9s8I7>mt1 .fR] o+***)) n(**jjj*++K9ZB0+++??_ ª<===CCC.÷%xPrI7UXXX]]mbb%DDz,IkxnBH~~>Ǔt+>v4I 4kDM I\; Ւn1Miv+I\;}%k_NP[[;//O"dx]]]I7&q4+++wQҭBر[kbhh.V| % h 훕M7xoEo 144+***))QTT]xJYY{ݻwtC$^,x) 5n@&`kk+V{ӧO%݊"Ot5 xsss6@*`;).@ gϞa:uԵkW.>W^***B{C^^Gt^zjٳ"//%v=<H)x3N IDATveF(J-p8222222nKi t2k":+ǓQRRj!=xPB!Ez]իW***09iB!á^hts x_ iӦLBssÇs8!<&''7Qpڨ ))):u1cF#nݺ-_|ҥmVH)eeeKK7Ӈ?N:eff!!!aaaCovMMMNNγg|~vvoܸqIUUU6}o߾ǎ{cM6@KXXX\vM_~Fpps)))Oy7QG(&$$5g <~Pyyyo|xnnn||i۔իGׯի:40h=h!<<ٳg8p)SϟV{޿ &899YZZΙ3'&&hbbFIJJ2111117n\`~G{{ѣG;99/]˗b=<0h%gyǏB \]]|cccmFsڵkٓ'OVWW'(~嗴_믿!#GtRvvv@@#!'O_ϓG1]IIɝ;w+))5gѣGڵKV*22^vZ:૯!իW***:#Rݛ{xxо}޽{5| SZZ:tPMM-[(**ҚEEEtl3g[>|QBoFgeeU^^~TBHQQѮ]!vvvVPP kn9kk뀀BieeE7onii [nM44m4oo… 1cBk8bĈ wuȑ#G֭[i9@k(|FF_]\__9-[& !|uQQQE gomm}ݬ,p(++;a„#G.Y68;;矄gggÍ=ŋ =z$ !+V`S:!DIIi-b&%%‚SNUWWԩSK^6n _m̙˖-c&&&xBȬY]]]i[ !gϦ%غuhJWQQϚشՓG#YѫW/(**VWW;;YS;88Қ0a-7JJJ.`'x!<"տ "֯ޠBwikQQQJJ ]@^[[?oZ` B~oJ(x{{?~9˹Kzͭ:olC[{AwBrrr6nxҥp8fڰa;?ʾzlwrrjlWqqq-|"b>///O'kENNN#F8y˗`C8qݝmS۷osrr233[X+嚚6Vϗ 6Bعbf͚qF4mڴڥώ'"/0A;wn@H|QRRrsssss+//:}txx8۸q<N:yxxXYYLMMo޼)##sv;=[RRonnޣGBƍiz_tڵkutthMa/ףG[nN6ӧl y&=2r@]]#94;`'3gΠADܳc[!g}F_gv7:I;Nw^clE߉JumL.vv5+))YFtRQFy{{   CCCލY|||֮]˖899bVX-6J_]]}ӦMƲ7[_}U,--ϟ?/ܹssΝ;w-6lؓ'OrrrϬYN:Eo~ӧOߺuKt!DSSˋhEx<~ilM+뗰Ϝ9dVY]]P(TSSp¸qD+Ϟ=MXt3XYnrwwwwwgK#ۛv>v˖`ϖl߾MCC-;vlpp!CD0o<]]VC_}UhhÇE;!822rȑ{K.top~)F^EEVBKK%11O?mhŋrUQQrbm窪]ti&ǫUVVo6x񢠠cǎaJKKlU[&L=_!={dWto9>ٳb}}}, Yeee/_m@ PSSkaw[C[JUUo߾ͩmy?ѝҥOXbNn999~neee{AhSR 1CR@ Hx)e䠽+++t$ڵ˗/K <_n@{k<@R@ Hx) <@R@ Hx) <@R@ Jt@J mڵ &H 5._,&!xh444$eeenB5R@ Hx) <:ݿAϟ=|pmmۢmo޽{x:֒%K׬YN=<<444>Zx MNNH/..!8;;|`ڽ{>}%by@}^zjǏ{zzZ[[m;@;R<88X|ժU,,,h$0XP(,,,LHH(**zc/^$$$ Vo {Qqqqժ/VݽC666.../_:88?^[[zloFSSԴ&MJMMm_~VMMTEEew}п&-[!ZXTTtʕdZXXX8w\6Sq8PXRRgϞX///YYY>/_&2-0t;w;&ڝ^PPlٲW^nnn;KJJZjU~~… v~q>0'N(//ݻNbccgΜyY)BRSSΝ[QQ!Λ7ԩSmg} pƞo7^躺:aw/ZM:~xDDD@@[;f̘,rݺuGa khh>ϟ?ڽ{w&=O?绸?lP( {#?ݻw\=۷#Fx{{߸qc޽cǎ%dff=zyT3o޼۷o?~|РAk׮BƏv_~%,,,,,O?}cKUTT6n`BGUUŋe˖1 aÆ+Wvؑ~Q}}}˗/\SN;wjkkر#88x˖-€EEEϯ駟'9997|4B4s$y7zD-[nffo߾SN3#ӧO㓛iӦOz{{kjjB=V۸q#MǏ <|rZ>l0W^YYYmM(zzzBn:B~hh>!//O>>tPZ>}ٙ522RSSsuu 7n`/..3fL>D޽ĉoݺ-:u3gؒOٳb``Gpqq7nܕ+Wp zdÇ'.^ӓ/jjjor@;N0ȑ#)))+Ncyzz/ttthMooViɐ!CDGqB&Lp)BHvv6dii&M믿2 c?bѓcǎݸqWizvڷod42Dl رc8EDDVxbz[(|2666%%rnc===izgݻO>BPtxSSS6S...fͪ.}޽%''7c xyfNy!6).{)v^)p1cư˖-[lYcwMi ЖHYJQA(@AqaP\7: TEd}N"Msy8!mrs͛I9y2lŋǏOw14~SNѣGlll^ڵkWZ@6mZq̂'N8{ .wY#,,,33y^ѿi\#W^k {z6Yflo֬2I5kojjQaÆ?׌GI IDAT:x|MӼ|DD۷w=ըQ㦛n/"zkժU/^8q+V :;E)8|]W\o߾O5jӧWZdɒ%Kv=3ݺuԬYӫp,;}39B/""W{u+V.iiiUHH9w]D֭[jHHۯ3իWK.c']/^l߾O'>222""೭[ 4OGyԩ;w "Rv?/R-T$|'~'Д#GxVV^}֬Yw7oN7o޼ys۶mΝksV?G׬Yf͚aaae^P|dddP6m?'~9rȳJ祗^|9s;ֻwgyK.{/ DdӦMkٲŋ^{MDnD/RO=NjO>t3 vO>[+2jԨW^yŋƍ7n\PP:uddWCժUuf͚T㈲I&yڠA-[3|ǎ=z Oן;wNj/N4iҤIu]wpǎƝZj^~ƌǎ[hы/X7+rٳgٳ|nݺyIa }LoygzERӦM۳gϚ5klHDDă>z?fڸqߺu렠\c'SVO'*5{.H.`̘1cƌ4ѣxf>},]Ca`.f5idԨQ~90a„'|Yph`2[;M4YdINx9s#ծx˵{}uZ}֭kԭ[wРA[lԩ߫S'OR~B/C)ҤI| mҤռ/`iN3>> ŧiiիWK^CD*sxr2@X `x, t {xx_w+&&]x,<@X `x,<@X `x,<@|6n._wbcc}.#<]XXNPX<Pzʠԁx,<*_wJ/@XxmDdѵkuwKVVѣ].4k7*PUuk֬9ݵko߾/ɓ72d{T^^s۷osrr~e˖-IIIIIIĉ_} +\_V^mwΔu͘1}=&&fĉ ,1bDjD^:ujudv&"߷o{Trrrz)MիסCwNLL]W_}GM|`+p?Z gΜٵkӧs}UUO8sdUUˤϥ0~$ Zʸ3}@{m۶9sB: eYbۃ>سgO\^|y^v:pnݺuevo\`gӦMҥ>hР=ztqĉgϞ'Oٵk{'"}g+DDU?~a%}2dHVV(SL)۾@"_;t]ꫯDCu5xev-"۷ަM~ʻwu]DFp8UŪ[^{~{et8~a)^>|pckCvҥl*@ٴiSJJvC۶mׯlٲ~h8~|Ӯ];#Hzzu<2'ٳ7DGG_)(11q…"GEEov\\u]3g,@"_;<8{l3ߺuѣG7n,"IIIƈz ^E^-v2|+sss<[olٲVZ& #Tǎ|򔔔_|f͚M:'kʔ)&MjڴfkҤC=rg|J-66666677ȑ#M4Z;(ؼys_P`9N_w|._  /X<@X `x,<@X `x,<@X@;Mu_wbƍ)a8xuXF||Pwaaa p:Bb <@X `x,p_[xqnnn6mZj뾠 \@EQʩ>''p?%C 5s̔ƪUk׮cǎuz>|8k9srveȐ! 6|GE\ޫUVw}iӦݻwt駟뮾ĉۼyMΞ={M7uԩo߾{o/i=?eN3 f+v</\٘?0$$liݺGU!/7n 3[ÝN} NZ͛84M{饗LR𩨨I&WSk׮>}$''?~_Wƒt:cbbRz*xesx [?~\UՍ72d2{#"" |͞O<)"5jСC^ÍQo;vό-"}Q۶mX ֭W_}U뗴222>3ҥKBBBxx>rA%$${С[npQceǏ_pa6mϟ[o_>55UDv{Ϟ=ǎ[fM+رcʔ)ׯ7[ڵk .۷]vw5mj׮]uůf͚M>]D. }k6bMڵkWRvw;ؗ!sol?s̐!C<ʚ:u'Ly?Ǐ333yv=bĈ5k֘\rϞ=+V g+oݺuС_|E˖-ӧO+++m۶-*WQ(c }:uΝ;wzyUoz[nJ*eRE;vرcO-_ܸs뭷f=s\Diٲ7FIǏfkѢgӧO9/LLL|w.]{nĉC ȰO1c4jHD&OtRwܻw5k?;v?7RSS ꫯ&$$$$$꫁YYYÆ ;uYf͚,ٶmg3Gȿ曦MFDDtڵiӦ͚5{75;ޅ<ժUvZF={\%/5]/^p .?~^GoҤI7ov\6m2T9s'v3"]^_|̙3 .4`BBB233۵kW%?y.?t̙3ۗ` 칰?;;{ժUWӧOx]veeemٲXg~m}g[З~Ϟ=cƌu4-,,lvƍO:5r9s,_iӦWY3 Ν;~/\xq|͂/i=@UUbcc|M)"RVq8oL6f͚)ݻ 7rRRRnnXbŊ~BY曋/y̿ƌ3x`UUׯ_oVׯ/""/8uԣG]l,iMܼ̍=nܸ'/Æ {M688Gnٲ%;;X6_nݺ?jժ]TRe„ ;vXrebbÇ/~۵kp˜b /i= @zZlѢE@@_ >{A#`;ޕv]UȑeRuX_vmc~MΞ={y~_8 PQF]4f{elndň<ԱcD ]#;v|l,Ew8r7o>^y 7DEEyٰaq=0aBTTTӦM;vN:8nvaWБ#G;5KTp8ǢE o/i=Cv|l?rȤID$88Ҋy͛7WݻO?ٞ`R儇{/Yk"p8:vh4"ԩS.{/i}q_D~嗂{9sfҥ"b_UJT߽{09s7oaIc]wކ >쳡C^pADƎ[e=\ڵu]3fܹswovȐ!?Czi8p 66vܸq<~L<"ޡCO>Aw<9[omРL2eذaIIIƱ'O6~nVq"2`"t:W\ٱcGc'0j% {Edݺu]t駟u]OJJ5jkfԘJZ߰Q^?GȈ|vԨQ s)ԯ_ܹ/^8i$c|IOӏ?ضm[pq&ML88r> uG]ln+mnx/"aaanriP<躮irݡN!Q%"""===333 00fMrssnwPPPDD(Q!1(8|fffvv6Cr(n Ù&홈S<@X `x,<@X `x,<@X `x,^Au)i{!Yu)n4qxIuMv{^^r.+88t//e l̥4M͵큁BwWU5''\1s>''GUհR_GIKKN8ΜRjnn Wԗot\ `RŘ3Rvү<@X `x,<@X `x,<@X `x,<@X `x,<@X `x,<@|oh^[P:l6[iF P:!wƑQ=3T"!jjY-TX*iii-u}AG? Ad;K+K.чuH,(0Ȧ-ujA(kb1wnwM8\[$5&Q̿*r?T'"D*.-Et]Qһ"p{tDJVUlӨfZdbͥ'@eR%P)nL2e(Cn)C[iO9DO_1oNх]˅(뺢St >U>U} P)z|Սwϥں^QZ(nJ$.;T"G n]*4]DEJ@1Wuב ygڛPYhﰖ7 _,&n=Ucʃ9_P.x<4]4]tҢwE{hg'O%W>omL-z<Tn3{%oGpU}UM(9r=WK()'vk(<CO5,og*x,TMTv""zCRT"h-g*RԯҾjɏUWTetk4UnS̑~uiBvb̜7>O*W@"o r)=~}Dvk8o?.zCi]_nTDJ,tIYN-)/)btKv(x,4QT6g+uߔR1һ^ Pm#y`*}ؖmLEHהEo}'QxlJo)yZq PY#Ʃr)_F,e%8HwswDj2Nuk?{p[/ލJFw<+&uy++$"fkԛ{RSSeŮmOzhUeܽY?諓|z-%o|vyGBt(".(fBolS_!B;tMlW;ޭLz7ޭUbm j|^ߩW(x+Ʀwy]?w*MZ "3éV%."3؉}#EDEZ{J;yxScH6EW9|V/bp8MoV[ۤJdd䏽TOnCps?W]DPeE͛B72UJxU$-V <"8Nf"~c$OuSv֣>x\Sx[9TMܚ+FK|Xae4-\2""r17Ō[#;+t)2\[UbU]Kx,4]TwmO)SN 4W]P%IbLN?QK-mʠ[$"D`{?r.{F.&Km)"ze:\o{\N+-JFʔU>ypg =TڥyQucwEO5no^y}]5k*+[ZsAْ^gDDUnk)_ǚ?* UUUMܪh銪[ݪU5Bo ;ED2! ]iKCD$ّ|Y#y [_ ^~xE΍<H캪)a>]TMwVmeW?\ S0ŦHPeͲhr}Ml׿a}ͯ˗EDW)Dwj߭12Ntij(.h("(W y#{BEH2ނ{ɉ23I>ך'ٻ5kO6 H?6맮"kLxQ5=<3c1&7\5F}rk;Kf/o(ޓ|VbgSVjrɳ_HfȠ[M4VTM7dMI)iiie\.WD"y'ƙC"siQWԐZLI>'\_wݘ)_?t+0* sGQD(gd1v( o%VH'@ey˲%?@*?Ez?x <* sŸ\:?]x2=W]MP)(R=(7G x?_Ô߹1xZkn-5fK o%M:v8W @`ٺ4|*,<snV D*ϲٮmNZ+'tɛQmJބyųEDUqJlUEA3~&6'g.}y0d㾢ꖙ4\}nfD?/]Uߊ]DD4MtMtM4Uf?*\/"JZZZ[7Mrrrwt4toΜ@_w skf(# {=er4-77r\\UUuwa =T66p8]׍ew㾢(F/y*eHP <@X `x,<@^IENDB`././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/docs/source/traitsui_user_manual/images/TextEditor_demo.png0000644000175100001730000011226600000000000027736 0ustar00runnerdocker00000000000000PNG  IHDR\tx pHYs   IDATxy|LW$BAE-Jҧ*ZJmm-jI-TKUZ-E,?AP$*͒DB!23wss$& g9Q9˥qZ- !Jn/+L&2+/0EuW^ KwFjl\]R0by!UryZLMMMMMFBB0g!(Vj} %)֒ᗱ UUu}"jذauWUQXXZYYYYY!(V6悹8N@Dıg?^Ky}j<ZTZj?xuY[[Ww`yX5pZ8q$Z"NLEi @qpp 4R15(#(?w8S2UrZA "uJk?W2ByyyFP&FP"V mQ"ș4QV>]#{D&rjfG[prM&0~$"6a| KxlltG. h/1]SܤVWر#{hy ٱcy%^^Ǐ?uTV AiX[$'}͗4 Rѭda1'-,mFZq^jnhk ol0t 0gݶe% uuuݶmnIbb_}…k׮]vѢEq]ּhגV4ZrV.nխr\짶F=)y6IRZ şSzqض}t_k)f8+g쩐 ĖSȌ=֫7ndr[ngÇᩩ:#w{yyy111>4*&**z1117o,((VFS3<0Xʞ T%ׯ_2=FP^+h'8&riũ]i5WZybH%<ܹeaCdli$qov܍em93Jͦ;J9HqpgmJ~SD(sOr1P_gsΧ'&SȈ eШWb7 $g0778p?c"l111qqqywxAAUΜ9JlllƏ>ꙻEEE9֬Y3h ˗/DxѣGKYaxx .^(!''u^lǪU/_pCffի>!q{?~y۸Iwǎ_?Þ-/hѢu=}GZnMDGM=zJ}ٳg]ѣ7n{.ݴiƌӽ{ 7g~QϞ=h۶mw󋍍%+V > hRKvOx,сzN:#=}2Ghn#GvpYOvhI!=a{8pON!]KZ035cOӢ u>G^͸6G YT,({{{&66SNlʑ/j:**>KNN+N99>HLLk gΜyeDЮ]iky f͚(<񨨨#G(J"Rպ?w^𫯾?uKwڕf򀀀y.-lٲXv!.+#ϬjY<nיcD_7|G'ݎ[5!q߼ D+&vq{k"t&Ф"JFuizqϝۅq4;w2J(,KE7Б0!liI=]iOek|駟OŦ o7èWZ F\:U_JjjMD,jՊFտO˖--,,-[&իWϞ=KDڵ;w.+ҥ?sJVzqɘGڲDT*sssCBB:wLDDĆTh˖-l ٳȑ#srr-[6tPDžݺuR"{ٳwƘxRJ{A[} 6tqq8pJ b6$"33ƍa"ڵ?XV-vu)S͛7_|!ik„ *gm۶(ܬ,G&rR)˟w+ PPPVe2]SJ++%m>+NsvDD&rИИn&ӮhD~§gj 1JㅨO|"RxǓ#ϪBfS;/[823SP=FN[UQZ|ʕ+٬޽{ w~WX7`f%߹sÇAAy>77¢ܱ(??_/M+:8c_l=#J|v%"Fuߍ.p!=EizX)/?JZ1=G'N҃ .(R02n_\ckk!C|)))6m:}ÇK?YRSSO:UPP:ѣGYw#=zİܣjwdϞ=7XSPPyyy۷obX-Z{WMs>xzyT\]]_`Tvmî2"I}ʔ)%֭[b"3Oj^̶Hh4\Tr/)3ׅEaiɱyTPZ&!-Dk0&d Y?X&-:CiUcԠA;tjBBѣ֭kkk[vmkk뀀ZYYFDDرҥK<o~֭u1~7"ٳ?|Lmd-[޼y… C՛`TJ_f#mbݓbM -ߙat#3T* زE[O>~  y*@UVIQӺ$ `=8/F!-y$^)յl!0ֳUuJ#spp#|gxN,/6 G.xBRQ9ZN5='uWDV&&&[OP =8sj 韍zfn SE[T@DD3r?΂6e'ڢvyAvThvڥg||ȑ#G[[[D^MN>.ɤn[[[?z_~!q4ׯ3>>~ݺuDdnnެYg~|T}W?A? }K2qqqzِܾ[iN4sСA 4Y""ٳg~ mܸܹszW,ә=Ce4Z8'@iأ.J}B qr;NLFpzPo7"B "! Z?4-ťX.) ՜ <hdql+_acz~Or2 ODιun)O͊n 3Z޽7.\{5k,22r׮].]۳I&݋y~СIII7ndSx wij߾4H.رczMvܹ.==W^u ڳgONN-ZȘYM4;v}}իWߋ/ѣ{J2(((11vzHe:3@5B0ZGI8i&lQ(ZPujdCϟEF3SHJhDD3I,k?n`(X~*V/$G!W)&`Q'5`uޞV#FKOO]s}(qf .]tRR)xyy 2$΍5JJJ"q4W^ǎ#"Rt۷7.;;{Ν;w:k,eK_;"##-Z$׮]73ӤG^zMX[[Ҽ __ߑ#G&%%=~Xzvƍ'LwQ PpOF8^+hxn/R4If)Gzj ?SnЩF ?jI -n%*j^njhZSLMSҐheCW r[)Ԙק'=(Ę윝% mbb2q͛7ݓ 4?ܹ30Rd2Y~'i򲑻I 6HҳgO6y;uǎݺuӝبQ/rԩ{PRBذaСCOq믿o>6W5YyY& 6HmOV\Ԭx#Gt7  ѿO?ɓ΂7Rm0#T==pKORuY1z3Jrw qEާ{qBFHQF^iUiP[OnTOw 5:i4x"jܸӲj4ibV_rrr߿hիWA8sttڲq)//ORy+o~h͚5zy:ydͱҫ=W2cJH+ HwSQ*MED]HAo=穛I+вt,Fܹ[ouѤ$;]͛7_~}TLYPVFP"Lb{RƦ?!-]C^ҷé ,dPx"|5_«fڵ{޼yݻwRF2I&;w(==]Tb7 H&$TAOG}a_3?? TϚУ<^WzTxуرёd*o߾0''>tF%6mDԣGN:988ŝ>}::::227<}t/NHHJؿ===ٳe˖;XF%Zj2o;vT>o޼ ۷/##P(|||FغukuBz񲲲bccCJҥKԩCD4&222%%E 22Ɯ$77͛qqqKIII e=갰Rv͍ STƜVф6l~~>kcNV_{"Ę\v-<ܜ֭[g .˛wÇ{MD{ꫯMhܢu ݽ{7..[n\B"$$ǎkݺ5JEDD?^՚̟?̙3˗/߼ysvvvWcTK:::_g:Ɵ>`Q߾}{y]A ~z6`Ν333믂[n%]~_޷j{Ȑ!yyyK.}]G'--m͚5Dԭ[_~߰aݻ=:44~>}:+ӧeNNUVgΜa16l`iӦӧOh4&l++:ٱ3fC;3ΰz%66VQF޽5x?~gϞ.@"9s+8k׮s̙2eJ h""ݻѣGJWNNN}ӧOPPЊ+ &,[[[y-=LLLJ;v޽{Y4iҤM6...Ν;w.gٲe,[?f:u۷waacT"GG9r5{:ujN/^,Czɓ'K9=z4{?zh|MΝ;zgh֬… uK\]]UaaaLLL׽~:tGCٷFq+>t_ez(#5kV@@@@@u_JĒ r9|֬Y{5&M9o߾wWWW^[f鶠@j4=W^e b caah4aaaKF-[f|*"y6S=Ho׿KD={Rӵk/kX1*M:uK8qܹs/^dsQssswAiǒ4nXޞћް/W];v4`])DٲeKKKبK%9+kfff;vǧo߾-[,KIId{խ[Yf% 6do ˺3f6Ohhh۶m kxU'Oݔ\vK.~rIIlmm[jefffuMId,{yyrĈ#F /j:99y޼y+pݮ=/CnX(HV==R08qbIg.9::3gLNN 1cիWoҤIsεLJJZdɡCCPIUTo׮aBCC'L` Tҙ_^&|5A cpJh1Qٴm۶m۶G6lXNN΅ 233k׮]*dvo0RU5j7moiڴUVmڴiڵl;wxyyCׯ_~}6*I=7]z2cǎ%D*1mR]lll؛LíOxq!/777??㸺unmٲq~G")[ۯ#(WoDԦM\ҏR͛7o޼-[?~<''glҤɒ%KX*9s… ^*bgEDD7աCcǎ)sΕF6)i;+qBCCJzjOOK߅zNv{SR0bj5J:u~m6\VQ4zZJw,˕^ΥK -IXIXDVTW\y:&\4Br7z$`&xW.((z޽cǎiӦM>wYbnItt4[ԴKJw޽{ֽƍwNOOg޼y]tݻ7ga1 ߨj5 uW` oߖd2fffN`DDgϖf[3{qwwwww?qDI5MRID7oެ1,xRRRرcu\x1/E`T:Μ93p#Gǫ۷o޽{^cǖ{Ĥo_;wƍ;wòŋ]S6l8}t"8qZZZHH9sbbbju / bxVk׮]ppٳgy睈V{v[7 0222&O|ܹT7|LO+uM:ĉaaa6l2d:k׮-]!!! :p@rr+W/_ܼySVӧ+& Sy> ]ѻ|W'O6h`ܸq&Mj֬7|ckk[lcT~iʔ)wܙ6mǏgU!C;v_{{WQ'$$:t(((Ho zʕw4ܖVٓGiӦ+WmVTj6y|I&޽;99??ýcbb* G?G6LS_wnS֭[KY[6a fA\\\ WcpJ]tOOOX_^P1*Q-92zhİ8ggիW\Rd%TW[NwK{U޳g={Lcaa1eʔoFp&RmS:WvS*|7UpZZ.\dXJv3f ʕ+}&+ʙ3g:t6/{o%5TnxH~=<<KK9sz6]͛'ED666#Gׯ ~C:w̕%m-eb׫W/<<|#F6lcbbl,JŕoBIVV\.7~U]bbbddÇ]]]-,,iiifcǎ%GݿԴql4̓222ׯ_J)NJZMM0pu8kkk6|Y,ZMD͛7/ei甜}v^yzzzBBsh=zP-2MBKKkcٿq(233Osakk[ ɉuEBRQYY[[ĤLK9W;;2(Q(ό5%1MKK*\m۶MzѹǕ+Pý*]>u;qqq*ƍӦM۰ayzzV㒡=F`,oo1cy""wwZ*PQj4 I&Q J gee'U} 2dٲe111l LLwYYYUw &_C(kxThJZ| kx(ʒE ]>"AU̬*ɓ' %B0J!Hk0ä)xJqާaT Zx Ũ_FP)kk P/#y2<^ J=_ݵ( T LX]ݵKcB4x%[j{PBI[-k84T7[R䉙B>5x8%8ֶkx_`K!9h9Tw=4@` B0!DF"#@` B0!D@Tw^--ZSg 㰬,\.Uuc_XQT#j<[[[X ^j P!=F"#@` B0!DF"#@` B0!DF"#@` B0!D*_U6rB Aiٲe۶mPEë `UѱU߿kQfc B0!DF"#@` B0!DF6mڲe ]cl۶+P#(/ǏDPq?>22rǎm۶}ת:i߾}aaaNNN}Gd6x%zzz)ٳg˖-;vDw!<<|RRRrrrz: 61<&&&m۶5۷ok4R> }k׮VVV[0`Kv;uꔷe֭=<<:tgf̘P(zѣÇ׭[ӳQFΝ##FXZZzzz6nܸQF/=CZZBP('O ۷uv\]]֭;|Jh/^ФI]6knܹkL4o\Puԝ;wdff]699_}6QTlάPw~U7I6j(\߿ڵkjՒ>=**ULVjAM`]]]Ϝ9SPP+ gΜvKII=zF177߼ysXXXhhʕ+… =zw搐?KD}MLL?~|`` >6Dn:,\0//oG ]v3m߾}ͥ|˗TԧOCݻwý{&{~W&Ott4 InnnѺA޽׭[7__{ӠѣG;vuD+1~xVkbb23g/_~٥_AgΜa16lx׉iӦӧOh4&l++:ٱ3fC;36n޿[... 5jQ?~gϞd̙ׯ8p]Ι3gʔ)JNN^hѣDԧO>}X/`n65ښ[hQنgcݻ5N&MڴiRXXxܹs}-[Ϻu>cVةS}z{{y-=FPSHTld֬YVVVDԵkW)1&LDbxYfI9I&/~F:۷w閴nzҥDTPPpƍb?իW׊+tӏ[ Rф{,&FZlYfϛ7OqzAD111RKD={R}4idɒ4"9s… ^*bgEDD7աCcǎ)sΕF6)i;+qBCCJ =FPCթS_p!ꠠ *\^UV%]|R˹t˗/71;v$"JuʕCImE#+\.geǏ[%A6j>#)͛ץK޽{-~#|bQzk]RwDEEe݋=#"={4ۚٳg'Jm°ܼy*ǰIIIIcǎ]drǎjT#)ڵk|wy'""B8pnooMD ɓ';w.557dĨT֭:u'6l0du֮][҃Ϝ]!!! :p@rr+W/_ܼySVӧ+& Sy> ]ѻ|W'O6h`ܸq&Mj֬7|ckk[Xc5ŨQ6mtʕm*JZ&/_M=4iݻ?C:;&&&))7z?u}||MVʁ˗/߷oϟ?N:[n-em!#ۄ6lؚ5kAsqq1\)t%>>>==~cvvvnnx!jZj]paɒ%lJ%Bڵyd"JOOOHH033svv.u5MBB£GJEXƴ SXXhiiYybp65_7n\5V6V<{PRYa( j"kkkݾbi)bggW% $ƴ ciiiiiYʕ+FOO*پ} lٲL&sqqשּׁePE4^D**::ZRZXXTwua5Je֭]>"#@` B0!DF"#@` B0!DF"#@` B0!DF"#@` B0!DF"#@` B0!DF"#@` B0!DF"#@` B0!DF"#@` B0!DF"#@` B0!DF"#@` B0!DF"#@` B0!DF"#@` B0!DF"#@` B0!w1ǟLjK$-Z/tAQEEi{\V,VQR݈*Ujj D H238;y^7sjMJJ:xɓ'oܸ!Gʲ+?Çs4^Z>o<77;4h੧Blڴ)))Iq]v+ڱcm;"""۵kתU5jc߾}˖- h׮o߾#'Oe˖/RΕKLL|?5k֦MF+>oo?x˖-SN}'.\cnݚ.طoUp{[RRRFT/EޓO>)TR޽f͛iii#__߀$/^9sѢEʕBT^m۶...|ѣGheʔYxZV-!ɓ'Š+ݻwժU˗/mx]Cݻw_~튈B׭[dRsȑFd1N%VX1tP???!Dvv^^^-ZXn3p@?ܣX౨=ƍ٩Sf͚v}o6l1b&OW^Gσu5,[,##Ñ=kժUhժUqqq~~~/rEo=tPʕ QNN4@^FEy(++kϞ=B__ߪUYr¨ڵk7lPN}mٲe?CHH?קrE0*j_~iiiAAAB~ȑT)<,ի7wܻFDRR&qqq&) V>}l6׬YD=lPmyFFƱcǼiKKKKHHpuu pqqqHܹsIII˗\Br8rÇ;xa6WuΝw^À{{Զm:ԩS߽{CJJO<Լyk׮/1͚5 ذa"666 uiiiwX,Eď?ظqc//zծ]ã]v;wmaaak׮WWhhhTTnC 1L͛7|rNCBBkԨm6!ĩS:w裏7 &d2mڴСC[.UTݺukԨ=|𬬬۾(:j(T|---^ ]{׮]/^K, oԨQJ|}}gϞmX|||L&ٳypo"ӎ;ֳgϘ_W^iiiO2e[ff#\\\*ζ =bdgg_4hХKΝ; B1sٳ'33S4Yy-[n?Sǎondee߿_ec*k>u 0 22299(IMM6mZ߾} dee|%/Bbbb6m֭[F4uuѢEIIIl6/[iӦgXzuϞ=㍒b' G{ŋ_yk׮(Qb̘17o;f޽{?^ٯ_M !-Zw^Y'$$$!L"[V\Ӽys!DժUcbbbbb.]*ߺukzzb߾}ƙ GeXjԨu7n$$$̚5)337ߔ]p[nf}Μ9:p)Sd͘1c._lw<[ IDAToٲ_ߵkך5kt"lݺubbb^o߾cǎ7xCvQM>l6۝d̘1ׯ_ׯߺu80mڴ\19s&M_!Zjjժ3gά^e˖BofwM~xeS>}:!!iӦfھ}7|!ݭ[SN_vB'N̚5-66W^Ve[nݻwI*V8gK15cƌsΙLyiF.A]p!**jBM>m^vm7n-] 7n܈E믲mɶC2͇X5qPdd6lũ\l\<~Qo>!D-T$5nܘ9 ݣw!DRlHö;w޴ieciӦM{:S6'|-##_~}[n]fMf;gΜp•+W\* ]JcV2QG&ds\'yr㯿ڴiCX~QF|\[)W\Z-+MSFYjjɓ'M4yٙ ;=*..Nb]f͚5kr'%%%%%(2eʞ={RRRzjѢE6*U_III2d|}}wl_Թs{UV:C*[uT;Ynݺ9 \u^z)F0ر|[=5-777wB+W.gavR}`t20l'W۱}iɒ%K,)H{Y׮]gϞxCYSΞ={ڴiɓM41fxUPB e˖-[vS[5V;99կ_V'[TɎ#פ-[Vn|*@ ݣL&l~y9bŊ;o߾X݇ 6lذ+Wl߾?φ ܾ}{ޓ73fjbX ofx#A]!!!~zɴm۶;m1w/((Hzq81Ud5j!lr֭[#FșJ#F9]rp"SLgy&::z̘1Bly 99 |SNKڵkWsΜv%7rNadCQVVZnuMl,6l`u=zp?"ݻ`ݻwX¶ڵk=zhٲ1Jqٰ<`Sjstĉ"qqq .C ۰a5jԲeKo2l5 MV?RGer2z~m*7nՓ:xM$yS(IΝ{m7n달PxFz|:O?=rȟ~^z?~駟6v=zŋNܨQnݺ !֯_rJVTIaXF{nc-vڍ=:""",Wnݽ{O=\lljMOO_b^bŰ0!DvW\ׯ߶m.^Obӧ0`ƍ9sfDD:ӦMՍ*W,;vbŊݻwO4O>{vvvVn^IY,Am߾=11`_~Ə/oڴGٳg߾}T2qrA]*UZ`l:uj7n~BkN,XfܧOcѣeØ1clsuA"^zu.]d~6 %&&΢tڵqB-[yyyu9))}ƌrY߾}y!w}SOUXcǎ?cXXX޻[nСCzz-9SI&BΝ;6id̘1L2scm!qɒ%aaayTD+VȥASRR/^W_%$$AT"@0*fɯ7\ .{\Zӧk2--mԨQBaeʔ]?J ;wnÆ ȍ~:00PӴnݺUX_%Kرޓdee^tmݻ{rݻwjʘ:^D7|sժUrtu9Qb7](yH6mիgxzzʦ;Ýl46l1KQl^xȑ#vkuHM4Yll6.­cy*_Çg̘ѹs&M<&M:~xJ߀T0K rp7n!]s})QYYY{;wk׮tvW^MHH(UYKVj,IPPϟ߯_?!DJJٳg;z^|˗/=E?W\)---33ӳ.#>,f/ZgϞXM6K%j6s~8ҥK籤ayxxxxx^*CRlfrrGK9;ZY`2ƚ[qHEM2~4>uiy=&jڴ޽{s-]4!!!+++..n3gBA?,Z8*,,{K,9uݘ'!DPPҥKbPPF߼x no<~\ɩzaaa}WqW BzѣG5j)(QDڵP| bm#F(R6m**pK#bY1@!(#`B0PF @!(#`B0PF @!(#`b* ܙǏw.ժU+SCGRSSM&SQ78UT!XPP~8jҥKX+X' GB0PF @!(#`B0PF @!(#`B0PF @!(#`B0PF @!(#TxY,oF&Msfffɒ% /^Rn<-GݻɩW^wurȑ ԫWgϞ]i)))8,55d299Oӌ3Ξ=kWXd:uꄆ֨Q*&׬YS1~~!!!a;v8w\zz_&MSz|jժeff2dwy*4gΜ)S!L֭[B+W֯_jҥKv=zG+U7TR( :eʔynGEEK&L_qQVZ/;~T ӳaÆƯ)))'N~zbbbΝ-ZTvb^N4hݺuzꅅ+W.55޳g"**̙3 ,pvv.^v 6ԩS玂̙3i.ۺQ͚5.\h[6iҤEL4iU\}2999͙3om۶>}dggo۶m (RV-sɲ_ζb;wgϞ5yJ .}&N(GaH=mڴ~>>p3qqqyvN{ȑ .ؕ߸qȑ##'IOO ;˕đ>|wG>Q?`$4m۶Bf>_~&M"""6mZnȜ_W^}4h:`+EGGlٲ~5 Yp!Ï?x5!DDDs|Ϟ=弡7nDGGcǎ ԩSzzo֪Uի805pի_z5M6 xgÇ;mǎڵkժU5zo>bbbtتUv>˗/͸8W\߿ppp4hvB$&&k:thܸq >#3\t) `qqqݻw j۶O4ɑL,פYf6lB n"9swuرc޽SN W^΋'jaaaEÆ CCC,X`X֭`G^&`$pqq7n0 G9sK.%׮]φj{j8pٳSSSu=s; 8FIJJѣNVׯJ7Ǝ#lX222b l6]ʺ ;t`XnܸqFYҡCY2a„;6mt˖-yTn|z:p@VV1=&&k׮;v0v۹s/{n$;;;66ޚ5k 3` 6򄄄={/:uZ… 3g3XVbX_|q7n7n\ʕ/fXʕ+cbb7o.ZjLLLLLҥKmOx DFF._.A'رcoj5L֭[7|p I t] >"99?B4mtr5E??f͚u~e+Wzꩲe~BJ*knɲkF;3dYҤInݺҕWT+`2W~K.n>;k,YXR-Zdeeڵg'tuuoo~RJFf͚ʒgy>svvرmu/r{OWѻwoټyf!DZZٳgԩ#ѣO?-Uرcu]߳gOvNާO &֭[hӺGDD̘1C^ ._ƍǎ9oW_}wg ~ z2 w5BȌٞ~9s8܃#3gum޼y.]h}b0ѣG4e˖GB8991BvA3_7GȨO>D"d268GBu<ضV5zjXdߜc&L?Ϻ"55Uv~'l8}իWSSS^zi!jZNNN7nܐ_y_F͙3Ƕƍq3qww-wuu jے=ywg4lG=s̺u^xY(<<^[)S'ڵkgΜ6V pkX7nagAڵkΝ{<.;vL5F,ILL999..nرBoonݺ,WgRR7m-kߜ>o#Gl[ wkr[|l9򉊋 2\`TVe˖:ᄏ~eˌfÀSppNe.DFFΟ?_n(QϯtҥJJLLc468g_bbbVVVBBBr蒿gvD.]f̘!{^xᅃG+mڴY`ʕ+eK̙`c Μ933|||K.]ti9rG\9rMq!؄~w#;ΪU233O޹sgcs:ul'ٳge*5kV r̘1vnQ``b9vX亰^˖-M?bbbF裏6lpϞ=k׮}3???9ڷ 0`W^ݽ{… 1cƄUTi2ojbXUVxZڮcŷÌu۷o_!˗?\vegg߿?3_߸qcۻLcpss{Džs޽9 >ޘm7!111SN{{RJm6**7Bf&|#Geۗoby~Mn*9sĭ,~'!]s ' !\L!ė_~i,htpL0n˗lٲe˖۶m6ׯ__1$?w\JU>sB7n?:׮]ׯڶm[u Dxxkvv|p~e˖m߾=Żĉûvj7Zܘ|.c|#$ V'ON<ٶ$>>^VV-ףk"ag'N]œ^uE&?[J*%geew󓅱/rtttrrg͚5tǏggg7kLѴiS9?_xyرw=¾׿(ĉ;w<|ٳ_{[jBE={6+++11qҥrפI xyyo^!*|_8IDAT'r~:ٳt;wرcViҤB(zwڕu֗^zIPUϟ?rm۶}{Ygܸq2,W^IY,QF޽;_۷o 2dСC6m'ȿCA1zW_;wn͚5 B >ٳVڳg]XRL"<==3+W6qvvr UTO{mX͛7o<:|9{{{/_W^9}3gF_bҥ˪U;F[׮]  ,ؿ?ܦM,[{=ٵ׭[>99y͚5I&N*;FDD_~ѢE--ѣK/ǁ~n\Csu}իW~( ...OLL|ʕ+eyr>\?Hp/[\ǔ,YRޝJuci2!Dҥ÷lҢE pܸqƍ K5k֌7urrֲeO?v boo &nUYf9a.]zk׮͹̭&nN}Hns-|||&Ʊ,YB =z+QĊ+*pvv^^^-ZXnQ%y5kfLwqqyWK9ωgO-lkM8pO|_)44tܹFL"rH'Jwرchhhc١ ub줦LfW\PB3SRRΟ?+Dt]OJJJII)[333;R\J*լY3sְaôͮbwzr+s///y?\fJc=VDBIrr\>x/_?]]]};z^?`rHYYYwY˿>{PVlcqZWZL&Sʕo#m$4Mί%K֭[v"`*MK yX<==󾟮y縢rʊ?`rHO?tÆ M:ծzֹ*+=9m6yƍahРԩSQxGyԩS_}UTTN:>уoƍ@eK%hҤ3j߁-A%jn)[Vgg`E0]׳wx>Svv6}jaZ]\\wJ+Sb̤O /كiXJ.h)))wSL<|X,fDeʔ4-ߧ`$egggeeti#=hB0PF @!(#`B0PF @!(#`B0PF @!(#`B0PF @!(#`B0PF @!(#`B0PF @1wxYVŢ]y䏦i9KC0P$]yR],yWzsqW!b^bv*#e\X-%%I='>G VBp ])BȲ,0Q9[n# ؞YP]w/iUBMO ?KjB89(i%]-/Drj%|FeSpsѬw ] T ]wӖ.nFO!\M=N-PIEЬ.e'U}W?]h+?g]Kh/'o# jM*Q$793~ iBuMMu"6yk#FrWY/Bd;Fu!if3w6ajЭ G73093WJZ,#i٦rfՍ.]\kjՅЄtl]{LSp-Q# *,VqA`Հa3P@h.i(HV]Xu*4}s൸^?]E垨I~yՖY$Pl14#nMu|O2m-:SsuR8z^tԚx9;|<zG[Tji&_Wԋ5#+$N0 Yb{B7." ѣSDY Ee7տj稆c9W>i"=S{PW_=j䵐=heD P#oeR1фxӓUUE_n%` !{9M^H }¨ ii",&&82S8.gn -(HVYtAnԊ^ZLE.>ڤroyTꨙsZ6/b0ň\*nDd?IjFnj:dD0 #j; W { JSς_}6NBj=jr]=$;u]7\K wv/!3uoBqY2LHu+-F YBk۪ɛ?%bwϺCzz:Z hRE[f{ͫu޳N<bV}۱b]HVKS}h7e|F$U7[5!tMhPm} j}Q䴵"ٴ V{:w !VV5TW٪N !׿kE*rr.t7s.[`:ڠE0 \1&`h9Cn"Lr}+SȨZB|O_G[/-džBâߜo3H~W瘰V4ʺ !DRn8}YT-/ʸ UhhBkbOE?P*/3.#kf4M4QCp҄B$\h.R5g'ZB\!Ͷ~JtDZ=b\0~b@Ͻn"G!2͢Isn;জHei} _ͽB;tNvسns=57 8;蚻ꖮۊE/EDPꅠw^Y RADX]HW]l"]HX"䦋-3sygfv3]G}Y<7'?68&IK4W3HL+~<^R`cԻ>X+do'ե%e]M5[Nڡ-T!sWrA)IF7NR to6ּYڹ1xc_{Mn}M^LJKQ+}1^7~ ?ȬV_{:L礩5>bTYrYdMᏩfO9}s<۟0L}]姢 & 3e~$I[-JKwuj2zǍ `@u:U7a!dx`w ~QI*k:f_3hVQ=&;ا?=77Es}Gq[>aM4y Zh=m6=&00Wi6%IW@;L <ֽO$͞w7|wuq߆#IN&z=L%686iGe>ܪyZ2W;6L^ˬ?/iGTI%r[{:EI-+usM2b@.NG3P6qO=yA{H{t|hU\xI~ IڴZOr&:t'JZSrtmZѭsu8ː[O\ ؘzP(L LTȻ(t䬎mBA⼤i}'Fdy9W?#S[IBө(Dq7ƈ`@9Rjô-UՍQ1.'\^#ss̝ª[iŊ1 L.[5~BgT05E5&3Z>,5 #2KuDz m&CQ +i"CQPW+0Uç6;&L%oU)UcFdiYwa, r '0v#SYيDz_`@rܺu^X8@&:J'jn,-$9'䝜֭#Q9&&&N y4Ù_/vL[n#ѵ=V,n/AUȞs\.JRT.71&BP(|HLp#AP,ڊŢ>LElDNJ@Qxl Q3h 7lF ]`!DF@`!DF@`!DF@v=IENDB`././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/docs/source/traitsui_user_manual/images/TitleEditor_demo.png0000644000175100001730000014166100000000000030074 0ustar00runnerdocker00000000000000PNG  IHDR@&n pHYs   IDATxwX3KĂE)PQ(hb7WKbIb/5"DC4M  (Tewdy||fgϔ={fߜeggrT*dy xqJbHTQ}AAD"1~WYYYK.nnnӦM3~P͛oB^MnzMcSNOO͵={vfL{J$̽>Ax2111;wAAA ē1w} pB{{=h4 31 B.WREi h]deeڵ+11111ѣG^^^>>>ڵ{ ˗/C4i-}rIIɟ9znu…#Gݻ{ܸq5cƌyxa 8r]LÇ3giӰ+"Ą* {|ll'|H;v8qbʕoLLbƍwܡ˓&MW^[HSqpe˖ݤBJѻBrTxĉ'JRM ㏻w߿ѱO$N7鲿?ʛL֭IR) P=k(>>˗ƒĠr*˅4'''..Q~g޽{slSNfggWZݽ[nC~n݊.((ѣ+ /_j޼y֭]]]|r///յYfvnxƍ/_֭[M6jRL&$$$\vɓ'rVZ~~~m۶}/]\٥K-?BgϞMMM 8N&-ϟ?OLLZK.ǝuԉONNȠ...ի.]nܸq.] 뗤z ʺu֝;wӝի׾}5j_TZZڅ ^xQNΝ;gⵅ"~* SǏٲR999wKݰaCMg(Z |s&$$嘘SƌW_)g_lܰaC߾}ƍ7mD f?rHAճ}m-Xڵkt UbD9ҥL&8q"]8yd&M]6ffэ5Zpaii)!^z,X|yaapWy++WƏޭW^DDԩSO>-pɒ%FAͽ+VYFP5={\b0`^^G}$,֟yjTYYYLׯi&a̦޽{Ϙ1C$%<DB'NRILL̬Y;)H&:V7~}իXxKK6m(?99>Sʴ'hz, Z,,,Zl)u֮]QQQV1114P^5͛>|8>>GlUVfVtaaa/_~֭ׯ_ה󓒒]& oxg5_[neQQQoߎ ѴRVSFDk׮tRtt޽{e;^#Goܸ!y>))IXS*tر,rڴi w܉PMoyu)\? ۳܌$|"3{;vݺu&LhРA ׯOv9 Y[[oݺΝ;c !$33ƗR UETbxBٳ5ݸ߿?<oc```II M*Uf͚ 'ܹ={貦>jժ[{kժժU-[vڵI&&9 %|%`ߋ񗿾|** 0`7nܸqҥK111`~ʕ}媖ZV{c.ЪB"6/_~w6~L5A]\\š>}pJ*#G\|W=za;޷r;Dxt6Tp vvcI8Ŧ W.|v^7#3VةXu66Ԓ2˗JqjVH+VZJ=#-uЁm@۷/..\2p@$ϰ`/Mw!!!t*33ѣG]dIϞ=MFZՕ-&e?*wmG:w_dggϜ9߿[xZnbcc:#*UeaKQd#GVB8g ^znnno\\܍7hضm[++֭[9srvWb 7_N ;%:ڿp'& UGk4~ڣ3lCVڡjnQxk)Ύ]J{; Yunv\"rM6mݺu۶mJx?v_u b_Be?ŋO~]8a pAc ֭[.\xڵcǎx// UETb>|͚5:J#;ʊҧO{? HPGGGkkka]J֭Wn#F}G<<<233m_4lAAA?|0}iӦM6)_~l6 G~!=p%FxÀϟ\BNqqqiڴ[ؚ@z*e&0ȰMLRN?2>3fRKKptǀ o>#Pwݻᇬs۶mZYk׮#GTL&[bŊ+cv_  ;K gS?p]vAooKj!Ԏ_$lPڷo/睝op6ㄓyOLL}jjَ֬;:wzD]Z$ZjJs9885_'doosc޽{oڴI&Tշn* ìƍ'|:tzzgڵcD"a%|ӦM>L%?悡v޽{ϟ4]6m80qDz~$GZ~ѣ:uo)1τ?ⲳVb-!={vΝ{UViӦ 6BRijjjffuƍ ,+++===''S{RzD+++ooV"d2\niihOo0cx3 0x3 0x3 0x3 0x3 0x3 0x3 0̀O](GyO@oU9D"j^|())yUqqT*2=1]Po+ն+^P#nK_JyʪO d>porEt7QuZ`XAAAQQQڵk׮]`z ẂW4, leY(Ε-+ Wh`O+,3[: (A73Q(EEEݦ\\\ܣG`ͫ˗_nccZa=suB?%_~48<!cs^"BHoXD6_y|9e97;\vvҪ1l666 >109$Bŋ۵k=?{lBHiiDbn7oׯ_FF}9eʔ0SRTPF`?cNNN%ׅ̉f[3F9!eA8'?쑁\_KD&(^Vib_𛇤YZZ̄BUTQZ_+..%Lr޽;wzyyk䯿Zt)!dĈ{nN6lٚ{n߾u֭Ν;=<<>>EVV֏?H3fLƍz4% 'N{jzQ ehi"繲nem9jmYmE"#}ERbzWVOm9O TlܻgϞ"]R6k,((H֡Cn߾]vѣG:t(B߭[ ww#Gܺu+66?11յT={8pe˖e}KigϞy{{dFƍ;88hIfd(**zaiiiFlmmu*###--I&J|JJJAAAڵUVg~=<< CGݓd5޾}["ԭ[W/NwjҒgh>>!**s͛7wpphժ֭[_{YYY'Nػwo``S֭kԨ{nMtɮ_"~ ]tH$J ~~~jOr˖-y9sJ$A{bM +aaajffC<ׯ~bbbzUJooZj5l0<<\@¯i˖--ZUz{{ӏ6`{ի5k֮]꛱ J+VVZ\\\ڵkwyӞ9˨o4iqС,W\FruumҤСC={Rt1Nj۶Cͽzq%4fӦMέ[vuu>tj2#k ;s_ӧO?p7{j?\Adr9+8ydTъ?LA䊲 r4.]({-]5!AAA?=(n 48uꔖdUVm۶ 0bĈٳg+U$%% DST*ݿ9sիM6[M0_~233mۦ={xb߾}Ϟ=۲eK>L&/55u֭ϟ?|0Jk{пO>g+R͛G~ܹsl+!N<&<ǏԖAСáCnݺӚ5kd.\σZhA+g:=IL&-#rvkbXx蹥wÇJ'$$(L2%??_ű)222<<T&Y˗R.\4dxoyʕ+[W3ou$Ɇ ri>|ҥ]*%fe˖;vMe磏>ڹs0˗{e$gNfTii={^zڵk ^z5tHٳƍIIIPШiӦѯ8X!**ٳG֭17 /knݺ5xਨ.]St)0gΜ6lKv1vm??s\.8N#18O8B3+_5E+& qϝ';G .r}&Gsv+lї]z\ZZ}'s 瞟?+? k݊+֯_8g,fܹ^7@D8u ?ǿ|ҳgO6|dVvZ-PƍX0YjjÇ۷o~۷Bz+'NtHym޼YVS.]NOr޼yիW߼ysRR?LTeddc|MvqW{c#Bȴih!o޼vտ,-M'vamm=}FGGRg@ܹQF:DG 0`I=fTVvС1cBRSS铗GLtݼy322rذa)))GUjcz9ҸqDSd }嗵jZdիWN*CT̘cH$ݻw'9sFcӧRi͛koO6ʫ9A;O*8"m"Wqwbџ䓁ҕ͛7y?w܀^ovvvǏgC.Y$''gYYYoߦu.77Ǐ9s> !W;6k,ZXXk: ZEz}(mt9w%lxǏB.]:c y}iӦM^^ɓG4'Qh)O|o۶mK_vyً-ڴi1^`zo߾[neeeѮ YYY_xs΄KbPpl>%mE7d+hj4xjԮ]nݺu֭[WKb BHlRv퐐_\fMII MLPӳխ[7!!ᣏ>bB/C ])S5׮]:K,͞="5FGݺuj& 2Rv aJB땲(<<|_a,m622222EԨQh͛7uOn݄B>Cp}Wv@am ?  zZ}t bdѢEz\vҶm[3PM4Yx1)MPi?}?C  A~s:>}:)HD Faa! {y.kǎZFwӧRN UV)a'N3Fٰaޙpx14c HlE7(UJO>}WܹsYN}Wl_6#FYFzM"]xyNsyN eoޒ [Y׺+Q[]8zK4k,aEg}'d۷o`f͚111c}2+z?~OnѢmP9{΄lٲeFF Hu;;ckƦhٲeGOueU09k۶m\\\RRRii1OѬY[n9s{>}8pر%K'۷gsl2:u?+[ :hN"77Wi@uZ"v{{{ڐ0jԨQFB_m۶uIǏ;ɓDp?ݧOMC ޽[h턐O1 J?o:dҔRmqWvu],g~9q.]lmm{qǏ֭[t~]*1e@G5 ڱ5kF^v`FS3`Úo肍C:;;)7UVREP>>>'Nxa^^mnvjg>rȃ^|d:"dww͛7O:5333>>~ɄիN>]aLAܒ۷oS-0={7̘2`AFayV܊X5V3~z,=MfWСCv;v+~ڵ?øq*JkLӷ_BB‹/_xҺukHԼy󸸸'N5>񡤂,V8^:XP-/T -j LFjz8Ɋ !Ǐ_r%!Ν;ڵkժѣG%ə3gtUO"d>h…O>}j]1[O:uK.ؼv]zGqq1 3hѽ{wKK$''=z0a[S gaaQZZ[% y-ZȠՔU;&el^^^JJj%<â~&9󀀀Gfdd^6W7+vƌ3f9{;VPP0iҤ={ ь33Wԩ5p9W^K>};@C-H纜pA\1~ui=EwY5Ճ鋶'{S6Kc޼y5k֬QLt\.z[&J\ﱬic.jў!+TtիWmV/_l]UTTԨQ#:4=|1zzqiS feeE=([ɊW*12YO˗/+ڙ XիFhZqUKR%< *HժU9|BHiiիW[Ο?y=?} 5o̙3b< IDATM?[44nXiXDM|0ur|7_ g+9'tkZ6dpsc; &<,--q Ĉ#¼y󊊊}}!666t(oV_Ꭸ[6mڴiӦϞ=m6=z4mڔNMiݺ54i:u)SLsҒr:ޯT74PZ5BAPO>D!!!=:BŋM2`0\|Y雺wܹs !^^^&?rU#e˖ta„ ;7QT/d /"%%E>ZFY8aN:ujFFwo+f̘ѦM]* &J>R7MhҥJS!.Y>XaߋOC:tU:]߮];''UV}<'O;_//}P+U߲|Ic 7ք_p!ڷo_׮]v*L}o&5"DFFhbϞ=ߗJIII֭kٲ%gر9sBn޼٧Ogff^recƌsNii !_uZx ]tillG׫W;wݪWl2Bȳgznݺ;w&L_ ! 6ނvrO?={lzzsѣ!$''gرgΜyYddd޽8` 7}D]A2U:uC;w_ݻZB;Y 6o<777BȬYfΜz}B+nh:F\dÆ ?? &L/&&毿z Eaam5k֤S ˗/yfttӿkB Ibtӷrm۶l\RXL闢t~8yڿʒ}՗[>}ݯ,z_|_AU֭[]X?+V2AUQZaaaIIIgϞ~'|;;;::2 ck֬sԩ)SWRСC:v-Ȉv̕7drC]v+WN>kiiYZZJ;ڮZJ@4z'Og͚5k,^"ٳGػޘoiҤIݺui{ ~ӧʊׅ'== ^Y^4Fq5ڸq Xa_1cp *''!C$%%uޝѣbז-[<,c .Y_V_~]i^={9 OդI+W١,--Nz!: +a&c'/|KBHpp-V{石ClQhO,YR%5{ԭ[7,,l޼yzBC QZN<ػwQد qpm,<ܠtXJ"۷/6m9r䵽:s꧟~ꫯYJ$AEEEnۧEC~-[^*<3|puM뫯 c齼٣3oaJױ:ut!Wלe _I֟ !"B V)ן!K#lo •7DT⊨.,,lڴ)'O9sjK[Qi Gnڴ*_>mO?}w";TbŊ;w&;voC[Xm0Tz}vjج,ooo???{{{-e2٣G^xja/_LKKH$ 6TV"?~ˁ JJJRZZJn6h൉gdvy肂Bvm#WʇKMMd5ktuu(1&  "kw; X*WxDPBBB` ,VnOOsp?Ugc{ҷɈD"KKϟy* A Tgyšztſq5y N!'>.Bo2"ӧiޔbt&, %sD&'u?dggG%( :m;?^e<ɈVGxKޔlllSSSy_~e4W7;wy2e>հ2 3<5X,vrr왫k50Ke])gQ:)ߖQ( (^{Z ;w60G YZZVV2777>>D.3J^ wT˒Fymsx5mmiiilULP* 8 ( L訴5'$qGD"-^ |EŸSAC53 0x3 0x3 0x3 0x3 0x3 0ػ󸨪gf@P\Q7Tp)45ri-sɲOnTb斕} EEyt736^χܹ˹gspx\<.`#7E m&3lu:d"f6M&Nd#!朜OOOM6xwwwVK@1Lz^ӹ[o= }Fcvv6}(l|vvhFΙnnn6'F^o0J*U|yFc3YݻCS<N>^^^6{_J<( lJx\<.@ px\<.@ px\<.@ px\<.@ px\<.fјc0fsdVJ*$57>ZVBa6 ^wss+SFvo6߿o2t:FmgRRR"""c2;/(jFq׮]_}׋;/"R@ɧIKKXuNېn0o֤I;G?8qB.ۏ=XdJNN裏6l8qUV !<<<=ZbżV]C iܸ#w1c۷iӼ uٳr5ƌS@ͲC}ԯ_Ĉ6J d2ʕXV>&8qb>¡"e@)ظq/"Kaڹsz7n(?fgg}Wzdu?䓎۶mSǍg'~zDbeiժ#"ٚ|n*(b]Vy{QFur-9Q֭TΊypB8lذ"(۷[DF ݿؼy'QxURS`p=-Z>}M}]ggϞ?S.߿C>~[N.p>СCrppȑ#ׯ/w:u… ߤIB3D]fX ffRɭv B999[neffeOOի.]P3SP^_sBtUeyĈ>/#SNl+~׮] .;w\ڵE,߼yٳrYvAۧNaaa'Okj֬Y^:tҥKFv;v-{111׮]swwk׮]ѝ;wA*WܨQ##h4ڵt.]?֭ۢE %իz>++KY|%!Dڵu:/:u*::FZj!!!mڴ<_9Ç5lذe˖ccc߀|||BBBg}eٲe|CRRhT%666==]njٲ,JIII^^^ 4 h4nÇ'N(_|ƍtd?ҽ{~wE^'+Vn.W%deeկ_?,,RJꢳyzϝ;g0jժd۷O= ڵkg͚%7ns5j=ƎRr刈[!C(?(:ul2;}N<_z\0a±cFBh4O4Mѳgό a} oݺ5dȐ<ԫWoŊv@y`0(:l٢rE9v؇~ޅ111C}A>m۶I&wϯKJ,9XvSLޅz~ɒ%<[-[T/\?z:KGQcccBRԣi޼y_}z~ܸq 9X}݈]u~egg[oJHHx-sʕ#F #GZxvvy;['Z5]zѻ"##w޹p}###k+KHHxϡVJVԑ2 BvU>ZO ԓgddJW_}ŋ򣯯o~zIFEE7Ιؽ{lٲ9 mS6߿РAΝS[oܸ /ܼyfy|͛76oެ<;ShWfL2ۧL"5M۶m Lo>}r[9sFϛ7OY'/_> -[68Y>ҭ[ e?sY4:_~1LƢ}_}Ͷm۔6v͛q/+]M&ҝ^ !>,7&BCCmvmB|W=<}RJZǀ7oޔ/X:tܹsm۶U6YܹsΝ۶mS_}աC6lؠ~47JLLΝ矕5aaa23ϗh֬YJ̙3׮];{;v(w޽;vnݮ>t IDAT)Of~zj%N:)P^A{ܹ;*k;9~[n)njsԩ+WżիϞ=cǎU* y-|kJ;sӦMcccܭ[7%_|!_Sl\Q)U!0Ra&!: |'N:/ny=)jh_.߿رcrY^37""BjVR9sٔZ3رcF)z=H4bĈN:i45k*XJ{_+ۦMQF) W^Æ T;C"))x*8+rYL0AIO%;g驌P/]{^^^nnn]v?~|rUNWREѤIeիWe|#N>|w^keSlMIIRqN&e2!_|!^˖-;y5j[HzW( ??:W_}UrzȕիW72ܖ,Y"GV… [GŲedn̿+jA%7##cŊڵkٳg߾}믝Ϟ3V)Y !4ͳ> P~`th4/EŊ۵k|,]tΝ)'Nwbɒ%ꁑ^PIEQ~`BSwӦMGv0^b۷o !f7o·Vk׮UB !!!J#A+WmǏ=²eÇ_˕+dmS^ߚ#5K2L 'OLII{nffzslÆ ]xe&s%A``rʵo^EQGbŊ/Lzh̭OgPPR_O>g9BhhhnJW+ܔ^ѧOu?~E+IH*ۆǏ-41gBBT\YB8BI Czzl>ZEj M};w۷f4%|zׯ_m]Ĝ/uoq%WVMNg\Y P^>w\T67ٹV%/k4oٲEmÇ2#G$߮];ydmR Pӵ\rʲR 7tPѨ^斿ߦ2BŰVZڵkwFn޼9y3g(S @nJb/xg?3!l>p2Kɤ~rR7@5h@.TZUi\z#?=___eZ/5Vԕ)SzBPt&Ot̛/T}}V}!dR5"Oo3Uk=*_tiذd ###d`'kz|542vwwGX;w\%z޽ڢE //?_,4jHY>{htʚSNYG!Dݺu#"">|xĉCʛk׮:uŀE)4GA$ {UnժUnM* ٘v7npBJR k׮U7ЩJU&%` b$?9bx yheeBB2Fcǎ={NCn/V\nWT^'ɫ[ΥoF)RJ(| QQQǸ8(tJNnߴi`0L۷o/6ҥK+wQ!W_}5dȐ 5J=ZqСx*h4˗AB^Ojڅ^ѧOKiZOaܹo?߿aÆrgϞ{6 2ǧt6VZ9έԏqqq/"*** '|׬Y#?:u4hP׬:_hSFB|g}Y޽/^,?K/$_x Yfjj2!իVƍh4ZJ6̛7oɅQq|~UV)#cc ߦ  0@(iii)RJEDDowbu%ݑXiʔ)/,dvNg4 j۶mN:w\V sp^zBiJatuԩJ*ǝsεo߾Eرcۭ[!㤜V6x!D޽OB0;[,hԨٳZv…:uRdffOիW4 #ٳX [lG?O/4///lSWX !f6\]vM6G&MiLG/4h~m8yjm۶U+*c7jH.hCPkSf;o7zСC-ɓѻ,Yb}jM4eG~MwĉK.d/**7=z#㆐rZTz9Z*U,&Rmy~ӧWZm=VXb/ eӬYC7o|]ve˖U*jՍ̳Uص]v}z.; )geeLeHc"GLMM^zݺuJ $sA-'Jpx\<.@ px\<.3 &͙LmVd.JFx{}L ݴ.ٍ,+r;dwj<%ZW6B77QQoddM5n0i hyp憎w37tu4!!~{177͛7 !N8!l=y|||Ϟ=+VR^r͘1C9ҵkW͛WX]vp><͛7}}}˕+7**\rʕ6lgTuz___˕}H6m ^z֭exbȾVXZbVZ5jȢoKdرcݻw/W\FBCCU$PN}y !+/]T7l`/:=jHFquyW+f̘ѨQ#__PSNY?~|JjԨѺuk?????d"7o|W˗/ߨQf͚yyyܹs+VQxՑȭܿȑoݺu5|}}Mfg7I?զ(JVF-?*[Fh-?/T?\޽{wٲecƌQ'KMM ߿~ qFsغuFcٲe;v,<<|޽wޝ;wQ2|ӧߺuKY9!Cϛd2 V2gϞ9rDSRR^|+VȏrGySNMMMUwڵkv2c}IXl׮]޽Ν;ܹ#deeM4魷޲8Z\\SO=g?駟>q3pȾǏ{ŋ?<|pذaw-UԩS}t\\u:b„ r_|1!!a˖-2W_}5'OkѢ<֭[,HJJٳx[?d/ yde6ovڴi~/X|BN]ׯ_333=<<Ν{ԩs*U*++W^<pF.]˕3gΔͭK,}9k8'O-pjx/h4Zѣ###><}a IDATt___JGժU5kѣG###Gj À3rJ=233u:￿{ ȑ5'Nw !ܞ|I!u/ݫdMߣ3UC1c )qƋ-:uԚ5kd/EO>}k֬pѣG{-V9uݻw !zs˗/+??#oҰaO!D֭/_~̙k>s]Da<˗:uJь7ߏzDž;wsD8pwb tiV.]t={Vё#GL ; {{{Wv]Ѹqc0xr_%@ڶm{%s5k&pssSGGG{{{ !BCC7[.##lL$3|,۷[Nĉ<9))GѠA kl~_-է0h y3fЁ7?sUFy,UÇ՛d7!oQ V4`6娓l6/[LyL"h4M&SŊ}3{qj(5ڵkM/ܴ~ódFQF++Gս{޿_Y#h֬uE9sF HOOWRL&Y>Be}VKúP!D6mLMM޽{N.\h'K$mm]<XnͰSt%hDɓ'f?P{zzBPjcǎmݺuƍŒ[!DZdKׯB@'iZYݻ'ܹT>p@!Dll0"?..]*;HfժUU+ڪp4>7nPVVZuΝ{-@q]yd̴GԩS5^ݢE̡^{m޽lIOO/.+í 111LyOOO=`J_%+"BԬYS.ܽ{+ TT͛_vMyj45k^pAaNN|#W>ȯ!iƌ2+WTG5͙ΝB[ݡCO?4O~\ѴiɓOd8;;[~A׉P]Ǐoذa͚56lx̙_d޳gO͛7ylٲ|pj(uVTO>źj׮-/^/ !L&d][; «m4;w>sLfffݺuد_m~ťh ^[nF]Bwȑ[n nݪ5ͳ ^)z !bbbdد={O):7CC;2BF իW9QF~K.{ƣUZdlljڵpƍ7n<;%K~wو7lذ]:ə%..NesXo}6w>{lHH3>u|qf6lhfsttJO= &)22RѣG9O^LL*eΝ;۟',իWTh}m[eټnݺy5M-B~;k׶nlB4iD^mUs4͘1#%%e)|`Qf */_lg bDEFFcVlڴ)::>n߾m48p۷;{JA)?7nx޽:nÆ yj.^ڪU;V~}ė['o*D5o\.hbǎnnnWZBCCwqڵ ^D֭++sN9ٳe:lذ4G\pj BWqֻfyƍdCбRK.ݱcG! r:OȨ(!D``fG'b;!ݻw#""""":rWw!t( #Wt!«K4nxҤIe˖_y#`qx~[*ӦMSF6nةSN:nϞ=jժ""j۶6l#vqƩל9sfB2eO}.?Cppppp2va(U\P*TaoѣG˙˃ǧ;8?gffv-88XNlȷo~7-Z;vҥK5͛7DP99y~G;wzi1c#fϞ&͚5K6*QÇϟ^y9w ]}֭GjJ3{xxx nݺ_=:Y5Zj%ُ5bѣGaIyޢO'VZ?޷o>99o߾#^mu[nݺukf *y,f%aut>J,eK8a„#GgDPzQF !^}՝;w޺uĉcƌp^W&fKHH֭ĉ{mXd;v!^{5;?Çڵ+66/ݻ|?2@}͚5Ǐ/8qDϞ=7mڔzȑ3g<8>>^wܹΨVZ2駟s=x@ !wbŊ_~eСK,BXǮFqȑHNNG'O\Z5g8q"**>OU\Y^9gxĉ5jB|ƍ]pO?}!DDDzdu_th4iFyYuM![?dըR͛O=ŋ>oBupN:[_||W}]tq)RJk֬jK.mڴ5jT׮]RSSG+맧;v_xݻwfsbb+(xM(tRRRR^'w2ȑ#m&6l[r֭!֭+'x!Dbbʕ+}yʕ[fRHnioݺuf[d0 m&PS&1V`تl /;*jdz饗lE###dFip[`E/_n855&իW+djVZ))),C/r'Z"+3?h4/+{=e߿[\Bk׮9lz͛7Xor,;vX%&5J6~s+*U|k-**J eƌ٫S:m4^wsNV#GܽZjidQ !z6lΞ=+?>N{,[!C}!:q!VKf矕'Ν;ϕ͛?[bs.W#%?!ȓu ;'%%]t:Z/xF#lz-ZXdIVkZcFY`O>>>zڻwo=z h4/B! MU8p\x'wٴiSe1c-Zd׿۱cǪ*T0`ӧO?v2ubV\{)]5-]tҤI꡶aaaׯW滶$lM-[ܱct 4lb3"u$Ν;2ܪUӕ+V#9r+ /Wjy=zY& @YSJ ~v4iRDDDHHr 4X~|2ܤI]vuU]u/'Nh=ubݻ#<㞞Q=:Y5.\8m4ժU߿Ǖm9՛ ,X`2I&;w}-bرG>|x.]CݻoymRmu4lV^xرcʷjf̘i=7I(ĢpnnXVw^NN6v,su-BĨs)55O|7|Sv2eԭ[Wc`0\z~~~E?u`2L֣gƔ[nyzz֪U+";#_r-  G`ܬ'Z^u69s攔7nT\Yoo߾|rժUWnPN^wIJJ2 UV+,XoܸRF"_~6m 9S]VgLĬ~_vnN&vK^fdff޽;LGM/6ӡL&`3])/v߸q~wܹof:&2i&""B)))iӦ#Gꫯ) "wE[tݻw0]r^^^;vV0`@-azg0Lb h5w;ҿCI<1wI;;h4LIKKml777wwwF^7 J*_F,^ɡ)hdy///=/߁% <.@ px\<.@ px\<.@ px\<.@ px\<.w5^]BV 2R[Ґ(0JA ck}P iXXJ*ZiDKIP%hF"wq~}~{s&[_=y= |sspzק-KIIll!*ZVUVMӹVjl&Ij P!l6l6LzK8pE?~MFԱc1cƄ/Rw!Ć 7?HkWr]ߨQ#%cw}g2z!?mvƍp_.]?< 9rdERRѣ ?hW:ujРA7ot<f͚@kZ>SS_]pg}Vsٲe]U;afZ-K5ʫz@-;;?QQQ<=Yf5rqŋ3guV3ƧM4 !l6[BBW\\Zn=}76mW_uQTb|hhhII1''`0coo+{zz}=P.._5瞻%?|{ݾ}:%%Efdd,Ydر'(6l8`ߺuK?O.z/w=:wN۷?Ο?ޓ={\p]vh46k,**vN`0ܹyyy7Xur7k IDATrrN>}٬Zj5jԨCAAAN+vթS`0;vLmРAӦM?,|G7np͓'O:uh4;vtgϞo"4lPyȑcǎ]zã^z;vl֬]#G\vbԭ[e˖O=Zh4/t]XXx!D5:pݗ.] ѣGZu.]dXFR!"$$D9m'N3:z褤$YG1͛_v۷(ۿ^zS7nXb&[h>k2~駳gfaÆaaaQQQpTgee]rE) 9sf߾}j.\p޼y5j̙3wޮd̙l6Zj ?r/COO7o|Ȑ!db۶mb;C\:byOʩǏۇg@@E:v쨔$$$(O?c d6lؙ3gZ^x_, 6LyxhܸҥKz-u'OvU˖-{w ՅÇ2eRҭ[7Ew={(u6lqĉ[nmΝ;;`2ۧ||W谰Xe={Ǐ]tQOYOKK;vlff%͚5={v۶mKkGUwx/ӻ"//oܸqΝS._|v]?z-]. !JJJVX|6^Sw!DqqYJ{^&%%M6Mk4:GDDȒÇ+8qB9riӦ(֬Y,=o޼;w/Wfe۽,pfΜymyгgϮ]*l^b 3QQѐ!CΟ?/?(#ɓ&M*cۿ}v|| Hׯ__zuooofx{{{{{۽GyfHooo$11>~~~SB^/{Գ4h`wVfi-ԬYӮ,˵kJIi>SL6mz1cƄȒ .V5xU)M6]dI&M<S})y`f̘ +Sl &ɺ[l)mFݻɓ'>\7h@rQ|}e˖5kڵk'OV=-._\N!D5VZ%CԹsw.o3--h4:]gϞ#FhѢhۼy Nׯ\n/={GY'}3<#HMM0`̐YYY+W8q4hРAZQQQk?VZ}gw=z;dܹs+;3x{S+V]PaGKnv֭j(Ҕ߿_lذA9pÇq2𐫺#Bŋ7mT+JyffL'Nʒ&!!AXGpbӦM?<2e_~ƍyV&&&ի7u}9rȑ#{Q@6m)f\hNtt%Kիeĉ-ZPZ7LN'ؗoF9'Ӹ⩧R۬t^߫cǎ)/\t իW~b6ճ8_<[JkE}.]* ٵkk;_V-hFvdXz2?\q%q[nsխ[u֑qӧO+?߿K.ʎ忝\ٳn/((tru}!DLLgѢEՋ?~e˖wcݺuSڵܹsŋm6Fwu!!!k֬ٲeK/k^V7MPvIBwZt5RooﰰgʏӧO5kVDDDddd6mzb_wC?ꏁ:n5eH\̅t>}Sf{vv-[l2cƌ޽{O0A~yqqҦ߫[n͞={ݺue_^ef2n_CI&ʱluVZ~YzU%+߇5k^~]_zf͚ാt %Bhą3fKEEEdJKKKKK[lYPPرc_zvw<̪n/ ~uOtUV^Zl6۶mݻcǎF(%5]s:n6/ !4`!kS^=G5enᡤDW%RP_RPP=rZ~uŢ`[iжm۷ϙ3gǎvP嗷zk3r{Q˽iڡC:433?Ν;R\\f͚)Sqxq#.n%||رCI~~~'O֭[ t:]lluwUNO?qoL>zwq}///.ֿ8mбk׮W9UTTh (m6m( Y,Co^9{AGQ /F-X8--ȑ#֭[o;w<"vwթviii))))))G)̌s5l066vƌ))) X(tnnu gϞ^N]r|!%N[Vt2dHÆ u:]QQݰ|Y rssS~+*֭X,Gʖ~Z]W_xڵdczzz-Znʅ.]()))}?zw;wRSS)5kTgVF˝K;2}vǍf͚defS~trCCC圜#GuM&Sbb3<ӯ_~_*#Gv޽{Jh4O>GL[o.Y$--?t O<!Clܸq߾}ƍKIIqܿ7n(}Ϛ5kΝr\433^~^[eG:u?|_R^^ի ,xz)5F]v7NYS 00CwB.]{7wUXX矫'[ٳuqqq[ngO}ٿoj楗^pB ?S=qXXخ]g5jİY !XXX(5#<^+k͚5;wvڥ+WN:UQ~sΩ4?x`ppb׮]Æ s}]ybm駟㏅ϟ[=00ƍv_jJ^Lx޲e: !VرcOOO{۶m7nc.Bk֬qqk˖-_XXnw111N/?{l~\lNVZ+WvI)Qz o۶/ z~2$0ᅲmڴٴij>ӧNrH~̙¾} $t"?v)nl6>|XyԿ>0ZFv=/\t,fTһV5jTi] /MOO߱cz}BBLB?ƍslC=^mΜ9M4Qvhrssm6}rjϟ uzoܸ e1sg}QQQ/V}Q2jY%GDDk.Zw<*5G6/KDժCNVXѦMuիnzÆ ._L:nݺrvmڴiԨQ &?^ 䐸͚5S:[osNϟ?n8ug:unݺPD:1/^+-5_N#Gn߾=<J^^^ 6ƪX!|||F'w=p,۶mԩh4 65j͛]DswU r0ФI`Ǎfʕ+!$$DfիWz ֭['O lҤIڙk׮7hdSRRrҥlOO2nWvf9333;;[ު^K%%%G7o2 >>>eX,/ȿlݾ}ĉN׮];u_GmٲeyuVkVV?v;VU(;< ݫ 5k+P.x*wpcx7@ px7@ px7@ px7@ px7@ px܀Aw!r@j߾}5NTzz]T;vTh(gwpx7@ px7>}Z6>>^<VRRRzӳܻ:*p&L0a_l4i҄ /^\Ɩ;uԄR=}JJJ.\Oxyy 6ҥKeԩSoР_vV^mZ+W#AAA[n-((8yd߾}Kw޵k !&LPqFc=:dff._|ڵ۶mڵk׮]ux7nrn48_z!ę3g~Ҫ}BFWq  {XXXbbڵk "(..~g.\Pڅ:tjtFZnܹs6l(HJJ?~|rT"vJ&)۷w)h׮]pppvٻw7|#hڴcF5`+W&&& !O^ڵ &IhѢk'N B|ϟT m۶ B|WN+ܹB~o߾qS+brʱc._l6+̙3G̚5KݨQt"XzuNNGoZ6lRh"!l?~vP*|X!DffѣGʑyOO%틋 ޽{^arG}GMII;.O߿_]n6'|2***&&C-[>}#јZ Y,Ÿz)1BPֱ{ny0sL͚5S>TF;9΢ۻww޾B2$55Uc2Ǐ`b(EE]ơ7ƍJIAAŋnӦM\\c-?/ر'xB۷RةS'dzZB>}͛[WaÆm۶B$%%EoFm9>77wfkƌ;wܾ}ɓ֭+xʫW ,τ;v\lYjje:t شi|LNN.,,B׮]+\|YW (][fM??ҮlW~*<_=p\ɑ˗/GDDtm޼y~ǚ7o>f̘1c!l6z"''Btaʕ={_~Ϟ=/_..\xY{rv@6mիW.B(No͚5n*Z !Z] pS[zu!ĦMk׮ɩ?NBDFFZjժU}Q_++K?nل-3g:u>|{`0ȃ 8 ˝N:׺knJ_ ׻w͛7o۶m֬YժUBl޼Yh+W }y y`0;)9[^qIEt__֭[W+>8 իӫ5 /,Z/ۼysAAݻX.""",,Lر#77:!CVG r(-w|Uu5k^n7UIK.rrr>}?ɓgyFY>00v;w,ȑV㏗VN:~UR+WX,rҠknN{/^{n(>R?}᯾jZdbiڴiϗؕ7"""٣5ϼuZj^p?,|Idzm۶ݷoblԨkk֬ٸq5E$9~Ν[ݺuS!j׮w!ѣGkԨ!N?~ܮe˖Bɔvnl޼A.]U ׭[gw*;;{BN:Ʌ[h!(z_a2ԣfywmqB-[% +jaÆoV)TJy]vvM4IJJرce×-[&SV\ZVwdd%KZldo__7xcҤI:j49s%}ٳsJa>}4͋/XKwyrbFfl{SIDAT΢Ed^x!""B 6Lw |g&LLqqL;wNNN(~8A?!\,_ / .ԨQAw |ڵ h4kOza|=>x>3yx=ͥ')k5v7SJe}PN:!4AG(BGpWwކ<֠q%whl>1c;&(((Cx&>|5HֶCh ,8!0aMjMow)=Dđ %L'H iCPP0aMBϑz-jhu\9i9Y PP0aMBO@DR#"L`?lhFB" Ov`JBJ.NjX e1c*{L1f0!M+8\*Z&ь`+"2-KDZNa/ncu<]58#_O7q)4cgJxx[lll,Y"VÇ" i"7oެkhhh>9s߿ׄcSLHJnj:\ά0.w;ȆjLD=ō <vޝDDvvvϯORRO?DD .|;w^p:t0./7h׮m۶%$$͜9cФzN{:{VqagmwíP 'oWy+%\t6Dtz3Џ \ =4?|;wn@vVVVݻw򖧧qcfff~DԳgg0矣t還 Q },]k]F̩HEmՀ/4vPj:dffر<~ƎUVWRT%WaTybwˉ-3ӮնRpɝk!P'W#_i#$K='ơ˫ZCGݞz 14k׮-^*--ᅮT* ݹsŋ;gdd$$$>>==3>OU/)41FE1QN:w:A>f 8"ZYz;e|7 ^uג{8>LNV&O[~0[_//lpSV߸FNwl]os߼rQ/pk;U.>hq;% YU}V?|}i+:Q]--{sH```hƍ񇁏:t{s/22RݻwY-۷o:vطo_ww  e??{nȐ!׮]tgΜ3fg>}5tǏW`xYrrrxy~;vѣ}nݚ7o!xg1=Pm$+W :ϯu666aaaj~RV*Jooo[[q%''>|85jAAAz۷ovqqQ*-[\rFqtty~ʕ5M6AT9s9r=zt„ əJJLL9sfzz;5F!?c욈j ۷o;ӧG._|yرgԩS&Lؽ{wNE^Z*nݺuF##}Ŋ_;O0hԩDVD|,km2dHjjR\\hѢ7nlܸQӿ;ݖԩSǎkժnFJh4zߖ믓&MbG3YYYoKX =PU<¼͢=~ųe{Cl 3Y8siLFKDh; 9GDO 탯#gm6$mHƮs۲t?n~j^NH[͟MU'mGKȣ3=ȶLm&B#|_naY.gemVŰkiVې?.'*k<۸ j7$$ݍ(''jyxxx\\ܡC-ZԲeK"O˸fWW׵k>|xѢE,ܱcܬYoѣ˗/ڵ+eeeرC|lvvĉ r%K9d\^RR2qČ ]H޽nޚZ^zuRRŋ}SEHHHii)M0!99LD+WLNNNNN9r8}FFFhh_u 6ѦMN>-v[x1vʞ4::7 ԡC}"2ŋ_}UV+,Xp?yժUEEEbHǎKNN߿?y{{$<#k= e/{sucϗIIl5Uyzs@D D$Ui%T/I~dra;gR|ٙog!ZҡL\vGmp<͵;ULO 4q_k,ؿ*3$f777"b{ZXXTK~Bxǰ*i5b^Nq3?,aLX%W1r@DN7l^4<6F_#ɉHr3'oD"Yjˮ.\x֭ KǍ*Љz8p#F"Ν;l`^~]ld}>}l^cǎD 69rܹs4ߦ`cT>vb6ϼ+u#fرc ]ze5kְl^7߰Dt5r"zl޽otF)%WɞБUdӲVwVOjjcڡrdwV&*>>{qF^^^~"3:f#oBB-oMl{6 nkf۪[MAxx'233.]گ_RSS t޽{6TR9?ao:hzU$vl?ckaMov2oe'WȈ(%W}[ Y #F]ά(S rUVsg*Lݷ~KR͘1cz>3Ϳ[nfXFquur'DC1'ӧOm6OD [|Ҧ6[}cĉe >DĒ'pT*5 Rͫٻw{to yfc{:HSr՟>1iӲքami1Y_3١#+&طң 4hȓ3кrnͶwnQP#lvnRC4DD[0{ݢE؊|[DVuUVG$;w&ǏϺ텅׿z)nCDeee۷o?x M‘jEL֧O.ΆbΝ˶MtBDz/_^9f[ŧO8Q(ŋ3^+W0{ @.-˒]4366I=I+y8K8HήJy=uf(ghv|0֣w^󵇋J.oӜ| VDpbb?Kh[ލ?Kȿg@ʃ/O;m|̲tݟTV OG^Vl_SwD$ЪWV*yBXf ;>VV~.^XPP0o޼7|]vϟ_jU\\\}D2ܹsgΜ9 ?{ʕ+.+wgϞiuEz m۶m׬HԲe> ,,,55uȐ!o?]vmذO.g =M0/ܹsJJʞ={/_^y?0..ڿ- P(:W)"h43gΜ1cF֭]]]k3 4m ߯f=ۘvLmV˙%*a_!f-l^u I;nѮVv7 |N#_;‰q,]p ZK)vN$4-.?fޜ"m3mLQgK~T"VMc>q|ܿ?S~Vi֬YFb7// frڲe ۗPYB+s_eHKҶmۮ\rͺ̞=[P8;;kO~5QͻסW-wkƍ7/[k׮]hv@<Ϗ3l)H6oo/ٷo?m7SbbW_}5f̘=z5?v FܣG{U +ԩSJո$`8ACD D4w}>=&"{?R D‘^45%d-)U i%OH[; .FoN$W7zVRp W|_`>ԩS#GHIIQT/_1cƊ+K.U PJnaZ | Jo9ݽY1~A &l߾͛K:DDD4J` Bu߽U~8'H+Y⊻}f۶m[nm߾=?J$oo'OjD*qDž<"ZsggpD$HnOSɒNyms^sW++JJNNVTڵ{r[:DzxzBRFr4>>>4YHMΝ;/\ܹs9kȨDNNΗ_~IDoFv;Ch`IDJD)Y&%Y;+ֳ#"T"" vQ>>##U5VsbRikk[<%L¦њ5kT\TTn:A:4tPZ-Tsνkn ~駟tGP얈7߈=j~$ IDAT#> {'N=zk+V>a„S>1l"2dHbb"8Arss?'NDGGd8jT艈~ҳ_~5cHR]|dffK "<<?wFٳo֬Y}FhfBOUmӦMšK.]x:uy^Vl!*7Hqɒ%,;w|5Ϣjƽu%00022RoΝ;Ǐ Yvm&Dԯ_T6c- d/(̌8#"LfkuFcyV˷ީ[n < N333 .蝠TǏj8N8.:w.>LDNNN˖-S(= x`ܹs[l-[>`РA:t>|hW^]`n˥K/^LD:t`DTQQVTT|5_.cǎOd;`Dž}URT*{ 5 =u-))]-efUi#:wy6-OM*//OOO?{ݻ[l]{x☘7|֭[/bfN:石^,_tԨQ-[>>O>DT^^.H8.77N:T*߮5=\8zt =ض%}St$:Sqy opөx]deeׯ_?uT"ͽu떹gd"jӦM5 VZCmyUVMII)..򲴬;iQꏫ_%"FKH._DXNq}#" ]eѐ2cǎ>>77z}ؼyիPgڊvZ5bĈzZɓ˖-.**͚ׄ57>3jo߾<_E2o߾F?  @$''G7^͑aO#{UI$ˍH$={ܶm[f͈322 _ի.][aaabbYQQpҥ{2rC -[[ݻ_T_{lRRB/^8xx;K/uEl\bF#CD۳gOk9@]A<߯_?CzׯFsLQrrѣ---}]Y~}bb"q ,,--=x`>}(666::;w.66v񱱱7oތ!ׯ7b̗^z@P'$$$&&aÆݾ},-- h4ڵ;xSRRDR^^>wܺxr`ZnݦMׯW˫uֵ|yL "g78NUy턣kz nXf(ҌAڴi72wMDJrٲeo߾zyyyffG8q֭[9#"__߶mۖ>|xOhhhZZ;v6lk?~|FFƚ5kˆl+{VlMpQQuDP#HL~RSSjuwyZاVQQ++JVk,{rJvv@κh{lqss{-㉨b6ό;6,,,!!ٳÇ "7o޼y6rAB_gأGVy{uʊJ5.h"8K+'zt7Y.kX^ "kkK#qvveEC,,,{sݽ{7::M/..~SSS,r]Hޔhi {`/X ^n.fr\!+xW( 3ŒW 33L+-, \񠿙\a/%{iYVVMD6+===]JpB5CqhѢ+WdGhff ٽ˱|UTT:urz@gkkT*p.leZv޼ylßFvHSFFƉ't;/_\d("zRT*k׮mguF@@mٲE`РA:t>|8kyJe~5`!5X",ݺuKJJbu;FKDr=r+q5:{:]#z^[jDscND#G\bEǎ۷pB"rvv jDH좱c_׮]=zt<<88xҤIuݻJJJBBB&1bʔ)zܸq+W!qS+Dh+YnF"ȭc B"mZO iccn 9<0Bob:ʉ=ot:%6SFV'@CBBobzMݍbY = CB`0$& = CB`0$& = IPcDRH2H"\.o0T4 iZ"rttϝ;wʊ,,,̬xvB`P`39#"BQ]ɉ޽KDDTVVFD 1@S = CjU6UKRz8󾤤ҥK===-ZT#

    QEE?|ԩcǎjՊ5j4b J%OOO0aBQQԩSu3zhǏV-,,VZpK|p=9[`AfffiiCDbŋlk׮WNJJ~7(55uСlBpkkkŋ>`RZZZ4}CӭֳkV87oڵ+M6mѬjق 9rdĈ}DT*-[Z믗gff̏>HD;v|e2ٺu.\f͚ Fbiio߾'|zꜜ˗/FCݻƊ<3ydV ?w*W\cccٳgӧO5kְl^7߰6bQW^y]\vW*@t60{ݤegg~E_tsҤIcƌի8bޣG[rK.gΜѻս{w777vQTTTW@ =4U=<|-[ĉ<q-r劵5_K zb;h0HdH$}\244'Ndee7ka+**tOt/ƼzRT*k׮mgb>}K5u IDATtq3w܌ BB =Ԙ )n$+W=… 7:t(4hO:w܉}l۶m^^^||ᄏjժ"ARRR&OBDƍc=[lQjj!C6lpʕ={L6m͚5D7}t#^@m`J0%s~_Uֿ/ћo?feeEFFFFF}].p6eʔYf͚5KP&M;G7n<}ޖ7oЃ)i׮ݱc6wJoo'O{DsFGGǏߧO "*//H$aaa}k.ZHw3JnjsvޅH"<'v:uRl֬YO3;@e˖FIDNNND|Mt޽'OyyyuԩvLEEErr2iFPT?VMII)..򲴴FIOOɱpww34XRQ/L/>L&رcJ$6mS*82@ =;I<TL*Pupixa5BP{0TX}E z!0aHLz!0a؇j,;;T* wW&==ܷ^.7t TL*PZ-9::VΝ;DdeeEDDdff`q<;P0aC B.DDw%"KKK"*++#"ssC9ϟ6mںuĖ .\rՕ"$PcKѭ߳j7a;lݺ5<YhQǎy2eʊ+Ξ=gϞ#F:tǏo}D"y뭷?}h?38pRVϟ?W^]t?yI";vɓk #HOADD==s E,$N{WKJJ((((>>=z?Th[n=Srrrpp0V“L m]\\((mRݱի~@?f̘5kdggGEEw>z(;8|phh裚3>wU.;vl+̟ͧ?_Պ7BG}~wٱc{g԰kZ fΜY=''8XX*\8#IRp(<ruuܹsppGdd8`LFh"wwOOOGGٳgT*nQQQ߿|ҤI...޾7ozv֍}QZZqQUUT*8bjwvvvnx΍y^f’ЧO҇ӧOs[ED[e"*pSH$&"x/NI$"V<I:4~]td*j޽ڵA:JL>}ƍ{m۶8qb,VZ L'~{-eeeW.**ھ}. W*BFMMMȐDR ;ft,/̈́ } 8nРA9h <81bݾ}; `.\h?E~~~_}wCfgg \l2͇߿cbb/^l?--MTFDD(ʜxKµk֮]&"ooLW.\ܹsǁgnݮ]@///OOfDd&O "DVW^yWwܙqk tAV)-44444499ycW^y%&&F$_ݫ;6{l6W|^ CbξsלwaCϟׅ rss7o֩S3f`,׮]xE3g&Y˗/׏---ΝKD&==9shqss߿?[iFAŰc:φȻ6VVDT:㉈fX}:߫\y"4v2[l/DŽGQyyڵk{ ನZÇ~]Μ9޽7777߿o36Ms([R>}Ο?=vf 84iҤI(%%e֭SToÇ&(ԩ ȑ#c޾%krZUs([qf <ޅWpDT2#"/""aw -Z^DDbVF$"" 'BuhM2瞻#G޽P(i|ݺu/_>poe":thrss7o9r޽EEENZloqeZڴ'+0jMvׯ7my^` }8c_:߿„ /^rJxxx2z]n]FF{vq۶me ,[,''gΝIIIIII6l Jƍ[j߽{+W4m(y^Cxzzʻ5i8H+#- : trQ1:)֜5D:dNB:RInDz葜|X͛~ŋ5lfC+#"[[[O=zXiϟoCUjH$ήRӱ7-wuu%2믿9B7J|rqZ&%%j oG}TRR"^zʔ)-\uB@PPPTK.QqqqDDFXn]zzٳgWX ޹s]K/QllW_},[lDAQLLŋa Z~^,bFhxoTQaa!{Ĉ۶m{wΝ;w\"y>))777'"R0:nϞ=D4bggg"***hРA3fѣ`"Z|Z!v( 0!!!JRTꯖ!I&iiiBcdd$UUU鯺IJJ*..&ɓ'3gΰo#˗/ׯiiiɾ'h4Vy<=(mD' RzARyyyťeeelVFпOOϬ؈(ѣG3gΰw&&&Bx655eZzhRRR؁ ;(((XdI\\"(22?dn,--6ruڕ1 h RC;$Dկ_?BP(}ydd%K*++<~ P bXX_[NZꁀڛ/^pvJDK,a|tt… YOVknnV Fڵkbcczz_~B޽{sw1ssy4R,+Çg1P_رcD'<((hԨQDԭ[7GDiiiqGDUUUR㸯P&Mrqqg= Q-D"}eeeeeNco`ر:uzjzDV{G[Nrʻw EEEw~gsssjׯ_2d˅S*hZVRFgP۷o~h4>luuuC@&I*N:S;o>1뿫*XdIbb"5JT޽;,,\g~g׮]{"333LDޙGmxiiiJ2""7>>ח]v&%Ibձ7V-硦Nl2JgܹX>00g};wF7߰knjTTTtرڗ?ᅲJB;^^^+~~~ݻw>vٳ9z0Ucƌ!7/ݹtүJhڵJr,gRkFD}G|s̙3G[ի-2>ӳgϲ? O7=lK_37'܀ Ddz4;w,--%"^ٳggΜh4r˫o߾C޽{ 6yd Ni&yv-^f uرcɉ'Nl˙`^y"ڼyoB "6fz%p=x$Ѓc{qq1;v8nܸ:+ vKǏ7l˗/QЖЃxof揠/(((88hԩ 4h{-?o/\g < vwww"jӦM;~כMD"y뭷iccEDwy뭷t?k,Za}Ǎq~  m=];EDDѣ;wp5k֬YUgJѣH, %뼗~Ʋ־0 j7pnݺNm0!x%%%Dq ɉjeo3:thPVVVrE-BT%y6'W3VVVVVV-2Q!CF`KÅ%|@!CF`9uVKDΝ#"Ng#!C0aЃX~UG z!0aLz!0aCF+..&"JED<JDP(nL&k)<10aЃZ-t:"rttl͛7ʊ,,,̬ @!CF`+E":ϲO'''"}6YZZQUU;dL2`4gljŗH$T򾢢jrMtҜFGGGWWW71v ={;w]t9sftt4F!ծ=O?qTay{{{7UHH;.((h4yyy3g_bbbX =Çܼyo]xFٿTTԖ-[ VC+رcǎ=<<<<%%%q{~)}:;;w~h%*DH$Dr:iY+%$$FZjLT*V)g+++÷o^TT5Mll>[]]?ŋT*~^{_9r|rQ[ïکSAA/6/ zhHR]tɃ)ʈRKD׮][vƍ/ݻwE"ѻロxbwww"gֳ8""BXXX[.==ٳ+V`_?.\x9Zz.\7o{xM~4hOR,: [| ((hDTXX؜q^y啘6gwww??ݻWWW;vl٬ҥKhŋ/f}?R֯_FD999{&S? y>))iرLHH`Oh奥ׯ/))tO2d `|tΜ9FODW^O>MD{^h~~~;vXhp?$$DT*J!g&Miii|8 GΎ Ts5vR4 'Ntb2?|K8{wرcD'rɓ'@ Ç~:y{{GGG zED7n8y%}RMLDϟ1c̘1CVPZ6j͙<ᐡ5:MG(**a`blmmonnn.g=k֬y駳w믿kSN})S]vڵK3`W^Xr&d!00pĉ<m۶:txQV]Xv\*ySއ1bP^*{ӦM6mJBSN 'e2Yttt\\1ArZڲ Q(/Bcw);wN=?obffOU4uΤDTc)))={$ɓ'=zaIw꜓&MbU*.11`L;;;ҋэ3ϘWUU\2>>~ԩѣGTh)0'>}?޽{666|S{{}bccc#&/111;;޽{666gϟ?VվgϞ3gΰx}ӦMVũjH$IOOWTD?uζCFuܹ֭[ӣiʔ)gvttl π۷Ϡ}-.#2xiED5uY WBUHDĉj8(L? }РA{ٲe =Sn}W-XW\\ /888k׮e/1/\|yvvvvv6M<`EM2%??駟޸q׉HVgeeUVVmb޽{C... lشdɒ?Ξ=6ݻw8ζ1sJ`+=/Di=Dt-"juDkӋE">X,""v6'Jv +zӰbmhhh<) B 4Co H .lܸՙa"@&i+#9?LlEFkƏ_TTĎx㍖I}@@:t\.o{988H$ݻ5k;MޡСãH[0a„ ^.\h_C@`0& = C@`0& = N&T*7o$"+++" "336'6dL2&DD$<>DdiiIDUUUDdnnަ3ց = Cބ =`S[,D"ToK4:::8@ B@MwswK.3gΌf( 7aksG5oXnޞjrͩ{ceeŽ 5M^^̙3嗘VcZzhÇܼyo]xFٿTTԖ-[=O;;;"ݣyo[n 9rA\FӣGV999%7b+Wd6l?uСXYYZ[[{۶m}8*//7Yvԉ^hh4-rqqqww ttt={Jjx\ oDH$DbgggggWYYYYYX雖ﻺQBBBYYk_GyqJJKK\|8~xV[QQqa ZƷ~>*))ZV^=eʔ. zhEAAADR.]DDbݺugϞ]b.\xvK/dccCDc^}Uֲlٲ͛7Qhhh\\\^^ DD111/nx$?AbX,k4F|k, (''w#Fضm;8wܹsIIIyxx8)JVt{!#F8;;QQQ|@D :x1cFO?B}cǎ'|"DDt<ÒgmWԩS$7 ~u"f jjz fnn^YY9BH4i$JYfl+m6z2ܘWiEEE111츺 55^ۅ0Æ ;{liio9m4իW9rmllƍcǎ m޼y+VHKK9rg}677Ë/jݺu mc<CdeeM8SwZLwڵk׮]B^ZPPP8;v`me˖ܹ3))Iؚ۰aT*5L@ 888N8111155U?'"ooSN ;@d踸8ůs!C25&LA$mٲeΜ9lIP(222 Ҍx܉nݺNm0!h UJ7Aaa!999ǵ5j:33u&To>F"R*W=,G@*ٳŇ8k׮D&KnL2&I$ 8 C`7I,jܹsDA,ۮzӅ = C$ O&dLz!0aLz +..&"JED<JDP(nL&k)hzذa{nm3H@qwܹ\._y~9vd̙8"vioʕgϞ6mԩS/l3H@ѣGN:)"?O=X<_ibs~S*Uh?]lx"0cy={,XzK}g^{w\ |l3fر/FbR\uU/OIEMT*EČ3"\.#]*)S'4`B  a=$L 0&@Oŋ#%"<ﰷ>"z})@TT*"\.GD>}3_]FDΝ#bvj B SORb>˲hll\b۷o߈xW"K.jժԩV=c zH }Š }fmkWkkk}ʕ+fq&N /tӧϠAFu݉ l[ouƌowȐ!_9[@{WE{Ǜ6߫WhoJߛ]>իWϟ?_lkk7o׿?7xcc-AgsO/|W_pmmmwqgl[Uۅ=zc ywyb<7B~&LpW??2e:_קzuVXOΚ5m3W\SO͜9;Zbţ>dɒ =c-Lg8x|WƏ<ȺbŊSO=uÇ1bDΝO8bQ=޽{#Gݻw?_rw}ߵk#F=[n{__=gɒ%uuuuuuSN[wߞ={in馍p6#}.[KmmmmmmQonnnnn.EƠA"⮻Z|y1Z*Yboii)JՕbrss 'p 7,ZokkoիWWᩧ:*?;Z}i2Xjw=r)?p?O<ꫯބO`3قƌ---O?FdMMMƍkjjzL2bĈx.ʴ vaZe_WN:mڴ .`Сo/7n\[[[ΝӟΜ9袋_}pwmҤIO>Oҥ]wU|?˖-4iҒ%K~8֧B{6*62xO|Vy䑈kT?wȑ/T>MMMMMM4_8S;A!|ŃsT;ɸo }eԩӨQ: <8"xϿof͊k;yy-^xٲe˖-[|y*;@9l=[c=V<8pFW^ōu̙3 m׎4 /o_go'lA@_|TTz?[Yԯ_~ٳgϞŊl)O=O<Fe]wuh7YؔuFjll\z̙3q^X9oO>xTԩ;޵ =Egx?rhD#"ϟa~KKˌ36cGyc\ysO?]WW|%"ネ۷ʴzH ?ϼK1|s9k= aޝfӏhѢoxzo[4qoN:U>"N<G̙K.39L2gΜ.h{^~V<r?lٲ/}KgyG>{G?o`$?u=z-2lذO'pɓΝ[JO>cǎ2eFIoW_}.˪?O,_-Z4yɓ'W>ˁ,a#`wޣG0u?|D\ؿ^}:_X%aC=Ҋ~+ʴ~lhh8snDŽa~lҥ8iڴicǎݻV8!6Dhղ)-.\}Ώ[?rʡCv}_;wnMMnvZ[[gϞkձFSSSq %7l3Бw!w}ݷ9l@TT`[Q'J(nZ.&-} z@TT T}R 0&@zH@  a'lyw[__={ [T a*I*JQ.#O>/GD׮]#s;l`KS'X1eYD466so۷oD+ѥKXjUDti1[ =$L>aEm IDAT`Ѿ~ʕ^ߌ.ٳg.=2Btg?۶l/TvjhxS{5{}oҥB S.#"vqhO;7+WΝ;;޳v4h֩\./\p;7uϊ+y晡CRBww]v1bѣu^{]klnn>3<`c4_~'N,ZmVkkkANcǎ>|;x '̞=zڒ%KNz-={{N;tM[ml*skuE~ٲel{okkL>N{'LP_`:k֬ʙyt .ࡇ+/_s5/z7O6>P J{??[ZZ*ϟ'_җJTق/^1w?XPT/֭[1'ַU|0aBss$Hux\ηJ^xa{ѣvzgy}oۈ;vb}D~QG_y3f̸˿oTI.]1c?Xl٤I,YOWB>455555U|SO- ӧO gyfŕ755?쳋G}xpiK/-TTtAR'\tzu]ӊ|gϞk܈#"תai=[?{^uUMj7rO~Guړwq[oG9ٖJCo^ybچolNEJ ,YsΒ%K/^q?"/^wu-=֞wy]w]ed?Xp9~:u*R [xqDDDGDϞ=o}CC>E0zH }JRDӧzѵk׈ܹsD[<T a*I*VgY[lJDt%"VZ:uڪg B6㏏?+̘1.4iN* +*E]ů+Wƙ8q /wɓ'O;##8c/| O>W_=jԨ}cqw~ߌ?';Hz3"+Xro~sΜ9ѧOJ"l8>a_R]/EǛ6߫Whobߛo/޽M뮻4_?iҤ1cy6)jʕ_z&L4iGǏe `B]ѣGDޟ_woI&uY5o޼)SlWY_}ܹRiСmq+*lnݺEēO>s•W^Y*:0aB]]]Xs=G=zhnnnnn.E뛍pGYWz5Dm[[[KRKKz㯽RtI''?)Ɵx|3sO|7HgV-ZgyF<*zꩿ/k|.\~v卍_|̙3g͚u744X#x饗6$BߎTkjjjjj<ߔEkîl?zc嫮*"N;6'N7o^]]-ry5jȑw 7ܐeق .~;iT :ꨣ"+^O{アQ<<&~~hР|3Riܸq>.Rc=aĉ=$El=z8餓"kӟcƖ#O>蠃="^}}s+W,v#Fͨ/.9M9{'G̙3/Vnj 7P=rG>#7 гjkk+ۊq+"<ȝvinY;|M7֖O>yɒ%1f̘)S|򓟬~?_ݡ%Xtw4mڴcUR ",\0"uuI mH }% ͷqVT a*I*jR)"f̘r]ӧOPH =$L>IE}}РA-'zH@  a=$L 0&@zH@  a=$L 0&@zH@  a=$L 0&@zH@  a=$L 0&@zH@  a=$L 0&@zH@  a=$L 0&@zH@  a=$L 0&@zH@  a=$L 0&@zH@  a=$L 0&@zH@  a=$L 0&@zH@  a=$L 0&@zH@  a=$L 0&@zH@  a=$L 0&@zH@  a=$L 0&@zH@  a=$L 0&@zH@  a=$L 0&@zH@  a=$L 0&@zH@  a=$L 0&@zH@  a=$L 0&@zH@  a=$L 0&@zH@  a=$L 0&@zH@  a=$L 0&@zH@  a=$L 0&@zH@  a=$L 0&@zH@  a=$L 0&@zH@  a=$L 0&@zH@  a=$L 0&@zH@  a=$L 0&@zH@  a=$L 0&@zH@  a=$L 0&@zH@  a=$L 0&@zH@  a=$L 0&@zH@  a=$L 0&@zH@  a=$L 0&@zH@  a=$L 0&@zH@  a=$L 0&@zH@  a=$L 0&@zH@  a=$L ӦMۢl ԴҥK9zH@  a=$L 0&@zH@  a=$L 0&@zH@  a=$L 0&@zH@  a=$L 0&@zH@  a=$L 0&@ R.KRadYHmmmMTzX_nFt#]K6޿uzԿfK.B@*<\˕6j-ފJh)ŪX:?c%cwmXvTiϵ\X Yey󈈬o?l7qKYDMM4fXՏXϰ w _k|Z^]S}V#+fE#5i>rGEnZ_"vI t` Z{#l|>ۡ>vhyyأ&cOYVv뽼xskqc3M=v\.?. >7WTDeYyeO#yyCmao%ly7gٚ?c/$ZzOmϳ}.6x}@+EU{, J[u;G{AѧkC}nWVS O_zu-\xO,bGN~@+T~MDDg[lmq>5G~˭SwA=cPk~*ٟ-{lh쯋㿧Peru|͗\(Vo+;wkgo["6E{Hv]s"Ͼ?bKc6jP|d`V߽x3*9w@v5Y+Y-͋-򎷟z[=Y)++m?hoz{V:8^RU2޹w|(wO/m򝥪Me,[Hz]Q/Ǜ (_D ﻲtn[ӈkxdN~}Y_'TyY]Ţ20"WUփ5TPmeΘk5:zvX<^_ir}5v=:G?:YD,y#~zֿ`};Wv*l<ʑeY^,lػ RpMvJ|a|׾YmMo-6?1k']gz3g/J.JkThmVmX:g︠W5V^-d`ֹ!lm3yWkR9QʳrTSʳbA^sm_]KfEUkF*q97QģX綔`Ky?9/u?Pޕ(fYJo풾6v=mgupoX̲hy|}rj;=D. [VUjoN@krSQ뷥?>9$w/wX^^|W>jP6|@9$쒻zKnw7WTD))ZL}GIDATjM=C=ghY3\;T 2ܚ{s%"÷ْꏺjӚ7@+TKh+E9ϊǶRŃR9/[uܒ3#"qѽS9]?O.KxͽknºfAњ9K;wt6~G۷̋Ϲ'(3z6T9D"rVʳJoR9OYsiq;f5Y }qlo-aRyG""?nwT|rg[TQzWz6T9J<˲<"#,ydYDDGYdnoĉWէǐ^1gLOʗ4&ʷ^UVGě+wz9GǏzÿD ۵,˺5.GU>{oy|ŏ!<5ezZmmz7O_cMmDY'T緊7;UV:EK~+kkk7=v4?hǺ~kk l76zP)RssM; #G~@D5QG[Fl76,ɪG"mqRl`{뀆K;vbMl ŇW=β(vy#ljkk?gCwZS&R}uu&ot>""ˑ\KzaЇ;ryyf}#;6|Ƒ{ ߩscccMOoK "\.J\ʭ"˲ Oa ƺ<ϋ4_ͥ|8˲"oJ 7m\dz|F 0&@zH@  a=$L 0V%roIENDB`././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/docs/source/traitsui_user_manual/images/TupleEditor_demo.png0000644000175100001730000013270100000000000030077 0ustar00runnerdocker00000000000000PNG  IHDR<=mX pHYs   IDATxy\Tek6YA5IE-ܵҴnV+5SKo.f&n_rɥ0qT@A@ٗaq~|g#>\s ȼ9QTo.VQW~%p xc׮]My.F @ BCCMIh)_Mהּ\0B d 6  @XF?[hѳg;TrU(ǫk{Сiii;w4nt֭;~xXX۷7oޭ[C߿ ӧOONN~.v:nܸ'_1888...77wɥ 7ꅅ#ovڞ={=zИk޽8 ,6mZFRVs5j͛gb} 4)66~ B顡{9{u3f0n߿:w}Z޽{?^[=)@h<==zCQQQɩWѣG}}}7?!ݻgPZZŋ666Ra_"+Wyϟ68@8>^:lvڱc}իW>ɓ,Y"mN}(b<)2dH?fڞ~gڄ/T*UBBY.-wޙ3g222ի׹s2oPzK7?~xS^Gm۶F)yQf80aʕ+o޼~ӧ?^^^f-prrz矼2wWռ>~ͳ^Pjlr˗/'%%waܸqn5k۷oR;6{l!ѣj~J3ydiF1#F333//ϔkEMԺuLoi4'֯_#""컈>|7mڴϿv2Ust:Rk׮iӦ=/?F-xΝ;jߟ?~U& 7n۸q>m8&iժUBBB~~y_-ũ%KXk֬v={aÆ TB[zzcѱcG)X[[ϟ?vBrjEEE%$$2Hvv+Wn޼)i:Nw޽;whr=׌6o޼pBgaRSS;,DC{RRŋu:! kk蜕bŊ_˔)S6mdܒ'''?s(}̊ݻ%t[bu={^z\q@jه駟Ο?/3g70`gZٳÇͻzjņ믿ƍwС-[ 4Hhꫯ믿;#\˯*77w[n?gϞ-ȟyݺuo[7mtҥ͛7wMcǎ z9r$;;[q9()Rn:@1sBe%TkMW_]x1)));;;88888;((gϞ={tpp0}{pm^?dȐ^&siii۶mˋ)~kXu={ݻwNNο7|ąْvm֭id 4޽!CÃ'N(!++SN> ֭k֭['@u6š kk%KTu-$kM_9pݻw7m4vVZ}=2q{ϰP(F%m5x ìt 1__-~~~ӧOBrŋzb̙'תUO>Bhh~QQQ_M|]Սb88w%4iRƍTjڄkocbbW^yEZMf͚2quu-rJἫ"9hB(ϥHQQQ%ŋFZZM:*|CǎezږBP*%eh4iӦ !7n,vG >|p!ĥKo߾n:Fs'Ja-L-[,h|QQQRyE6|7(JZ`߄wUT6rgޛZB>ȂV@nn<;yTf]vڵ5jT~?R%>ȝԮT*۵kA,teSTo{A-@ s>;J,z Y^-;;;77WPxr˖-njw !_n8Li9W\)hM6%>]vGQ deeYb䬬,K U6o޼իW !#QFFQ9ΝjEEqզ4h.\еkWKY윜^j8;,^de{@ˏ>ć&Mj*!DF*2XX:[hذaƼy߿Ν;%sѸ"WJOm޼y29sɶm۞رcuY\J`gga|iY\|ÃEx{{?t␣xjTֱcG)9r$((hϞ=nh4111֭ѣѣ+|ʕ~cǢWZ5j()͙3q'6h`ĉB#F.\o>ׯt]4u>}tYZ-^0P...={v7o~뭷bccƍWØ1c~YСCݻaÆ 6=g)O1cF||]N>}i㇜,Ybee%}yi#>>>99.PPTѱI&;d\^~~~f jK5&h֬Ç_ugggvBѤI%KX[[KJRGq?L"KH,D```HHHV -|}a ]bń ]\\dh0`@- 믿^99sƌcZYYz{{ܹG۹s!LcǎCYt/ STu 3ϴj޾r,fX:55511ֶaÆh V^J&*!VØJ%bݫ\]]96 UEb(2KOO 5jSU![IV7lذȽ{b9iӦׯ_߱c+RAvءT*5kl"J߰aCL?-**… nnnBFPX[[j;;/FFFk׮Ļ\rҥK:I&uqvv6>AB[y!%0\\\lmm"##Ϝ9ԠAOOz !222jժlkkkeeEb8B[ jժW_Uur%6Rieeꚝ"E'fʊx>#ajZRj[[['''NWPP MRYYYULP B,EPXGaa?$RzJ%KOOm8JE2 -"'$TX8B d 6  @m2@hB d 6  @m2@hB d 6  @m2@hB d 6PWuz+S(vjs m5BPPPUJjbOB[M\%nzgiB d 6  @m2@hB d 6  @m2@hB d 6  @m2@hB d 6  @m2@hB d@]Wu m5Bhhh^_%P)?7%%Ō8 @m2@hB d 6  @m2@hB d 6  @m2@hB d jS:EFFZkӦ{@5g"%%NnnnO\!55\C3sã2`L3m2@hB d 6  @m2@hB d 6  @m2@hB d 6  @m2`qaaaz}W@ P(7vՔj. 44ĞٹK%==Μ 6  @m2@hB N[FGGhѢgϞ%vϷLGy%JII Q*ǏW?O ~\СCviܮ֭[w񰰰۷o7o޼[nC߿AO]tuܸqOҿ8+cppp\\\nnɓK/ Toڵk{yQCc~~k{,X`ڴiE]\\JYyԨQ7o~EP7h ''R3 rQٳ֭B̘1øRܹСCϜ9nݺӧL89''G;vtuu-/ۿ83۶m{SnܸLӯę#GnݺAJ%**M6B!Cl۶Jj?~K/Ӿ} .kڴ"< +qԩaÆxzz;vaÆ%vKOOkڴɓ?zii{8gzyo1&B"EEE?''^zG} ...?۷owCiii/BHKU(\?^ڐYhM /ݻ7###&&x[nѣGB.\0uMÇ322V^dmm}ə3gFFF !Fi|\ǎR(\gΜB4mǧğӯȒBxڵkǎkhwW^]h'O^d]XX8ye˖I_* 㩻_~y֭jaPM4uV@@ٳg) 233L_^JӾ}{!ۍۥ@J'RT*.]bŊ ! zq(Wl}?x`ffbȐ!ei{iB,^>ST f޽{gΜȨW^Ν˼Bϟ?/eOy%1bĶm5jTTGi㜶j„ +WyOzyyItt]yW<طobɒ%$6x:ۼ]tbnW\Bٳ!j'/rss{5lذ`kÇwppXfMUR4ջw.dռ{^^^˗/ZfSM P9BBBmU]Hɪyy^^^wޭ*fFhCuW_Uu JB pxHOO@j"-o,͔<6  @m2@hB d 6  @m2@hB d 6  @m2@hB d 6  @U]vŠ7ʯ)P(7vՔ@ !44Ğ6P2gg.)nzgifPIt:]aat>BP**@6m ^h4NtBJRmmmmllJ<1`z>77733311155ή^z^^^uBܻwΝ;nnnvvvD7h47nkժՋ/Xmkk۴iS'''kkJ rrrRSS]ֲe֭[/**ҥK͛7wssUV% `NqqqÇ/[nݺu={c 3hRSS322*$۳gOjjZ60Qnn̙3 5x@ѢEe˖[n׭[*((ͭ ߏ3)QQQR05xm .5yyy~~~/55ķj+W|BaÆr;S޽{WXakkktv-Uwԩ / 8p'O,922Rzȴ{ݰaà]z{{+Ν3tغu{PPo!#'j-jݺu6mzչsƍ7͛JMMlݺ{-[ѣssszMZjնm۞={6jԨo߾+233O:%ݗzj擳 _aaɓZ-~]tiLQyEEE !tҴiu޽+puuBn1jjZjլYvBɓ'nhDK)SlڴɸOAA={8P~}g=zt͚5Z?7w\ C ٿx{.;;; eֻwoirss oyխ[ٳS*YPjO??^1g777o0` Ugץ-Z߿rssΝ|}}ٓ`Rh ۰aΝ;?K_.ZܹsΝׯԲ`)mvaaa6mi{#233~ZjUhh/OOO!Ď;͛bŊ !vax#GΝKLL,﫮ZZVzf -18TO.99Y 4rK*o6,ۉ'3ؾꫮ] !|||~mV{A!DVVCZ7nlXsӳqƆAB999]z\իNNNZ/,I"##}}}WZ%hԨQV Գ&M$?oGZ9sotoZ3"#O^\dpeՠ҆Q 2eÇ/B[ttu,t:yxx\|ټ^|ÃEx{{?t6xjTjh?~=a„_W^ΗܷoK/w !lmm}_R)qر>,**ڵkСCܹSdVZEDD=ի999ٳuҥtΝ;ׯ_ 6~ݻÇ矯_~I&O^z-3-AAASNӧU(s) b޳wD={v^^ކ W\r"|}}׭[gjA1wܡC_~#Fl2dڵk/\_uں@b2ήgϞGMNN\jtL?OJJڼyŋhwD .H%J2k.M4hݺY3 O=K^LAR-Y~(o;vx5Z!D6lotqtt5k֌3 cJֿ۴i4h h4zɩGG:th=._3/ϱdɒ)SԩSТVsNPeu]r ТE B[: !BBBRRRΜ9c1\]]wܹGcǎ)))^8ccc-Rev۷ԩӶm2]]wqttlذa=Zmvvvaaa Bu:SSS켼,DŖƷ4VØJ5hР w^NʱQ*aaaAAAa!顡ҽ7T OFrW^y#رCT6k֬N:+EV9ژɀXYYI޿Æ :t0 .xxxxxxq`"m0BammV.^ٮ]-[+W\tI5iҤN:O(/BIm...iiigΜqrrjРtgĄZjyxx8;;ZYY(3rRrtttuuKKKKIIn3iooooooccceeŕ<ZRjN+((tЦR j&( P(PC)UuWu ?6XJ"섆iƓxJ }+T @m2@hB d 6  @m2@hB d 6  @m2@hB d 6  @m2@hB X\XXXF^_P)]v5幄!((K% 5'pvvIOO73 @m2@hB d 6  @m2@hB d 6  @m2@hB d 6  @m2@hB d 6  @m2PIӫPq!44H^J EşbR`  6  @m2@hB d 6  @m2@hB d 6  @m2@h)"##-]i\C3s_Rf'77'吚jP͙Q0i U6  @m2@hB d 6  @m2@hB d 6  @m2@hB d 6PWuz+S(vjs m5BPPPUJjbOB[M\%nzgiB d 6  @X)_)湹vvvU]jz#ښe(V|7nL4Yf}zBVkmm]fFӹs;e@3t҉'J{Wj ۘO>ϯr*^^{~z^^^޽{)*VPC !*6UkZRiF*!%%%$$DT?޼ڦNgJW_}{Ҥ׮]bϝ={۷4w%((IBw9rΝ; 6 0aB˖-;ӧW^]JEթSA vܹnݺD^ѬY1cƌ9ƦU}G}vVVVF:tо}_~gwO:db%~Ϝ9|giӦs?ȑ#$~yyy3f̨ӧOB+~8qիg%WDGG KLLtpphѢ}zrrڵq4FGG/[VVV:u2eOv- ݰaC}J?hWuְgϪ{k׮&Lpss+qڵܹs;v 0a>A+>>~ͫNZfMZVXah#Gի1888...77wf^R3>0odѢEt{IKKӗ_vJyz葑Qa MV^~4e,99yӧO->fpppk׮^UttޠAVUI/vƌܲ߼yio[ v)ӻw2;Wm9qO֭[RRR_4+3cƌ `9 ,^cAA IDATA'uѣG%Զe˖ӧwa-[C>p@S޺K1k,tYjwss9r/"WqC[ь5H'N8::YN#Gwz8XДW}OsNϞ=_5Eji۶msqqj ׯB76lP_Z+prr~ٳ2cRќ>}ZٴiSkL{ڴi3vXKo9ݺuZ۷oݺjϗ^z%6ȑ#CBB;G=qᄏu־}?߿/}>͛7/2kx)ӧO'##CaooklRT/ +wU !ƌ?K]tի۵kΟ?_ԩS~m)=ΠA[n}Gev.j/!P(ڵZ?N8ѱcǽ{>xO~-[ڵk|;gv'O3PR;=z7|o߾' >,/ !PϞ=+??ɑ'*''G&رk_?~YYYB}ѣKIIӧѣG{#ƍq:u*337ȑ# ϯU[o'''ڵ+,,,&&K. ,5kv&LСÇ[&$$tڵk^^^z9Kyr 8m۶/_:uƍ6%x/Q?rؿ!D6m*gwƞpK.Eo޼٧O 0aBFxRu"om|C.]S|7xC1hР c8!{ƏKң^^^WeRv]M6?yd*7]v^h"Zj:u4Hz[nidCB!OYИ&Voܸa>C'ʕ+h+V_R2э7j4hP;wT*R*)ݻw=^{5Cc)_BƂDw^o111$_N) 3mڴ~ -=Ӧ.\(~\3m9EJdff^r%***--kF7nciJرc!33sʔ)yFQC~.]*mϜ96qd<  Txׯ !wQ/c''-[H޽{s5qI&Iۋ-))eddDFF^xѣG4i0Qvv+W._h2`xxxhh]ve̲/!ݻw׬Y#1bD- BooGW\zNw6eHCgcLtȑ!Ę1c 1$Æ tޙո|RBi=YW* 7\B!KH5;ٳeR"Z$:6h9>g鴸v3<3|>ml߾dBဤpS>X,l#^XXruuz{{̃dj39y$Ћ-/` C Vi@6$&& :tFFFo޼}֫W/}}}kkkMüzJ_____wiiiiii Gϟ?߼y9qD޽e߶m[Xueff~ !b ` >x}( .Ν;ϳX,li7i$b_ONҥK6mܹsGSSS4Zhh( 7/_Z}!."$[n~ĉ,KɓY,Vl˳X,OMkv޽{``9sL2-[ǎ^x7oڵM^ڷo"rǏ;VUUZ[[Htyɒ%,k?~0aB&LRcN:]ܹihh(5о}{ }v.**J.|>֭455gΜ н{wumPRRt<333>lذmۚZZZk׮_~x%?}رcmc}ĈyJJJ455UUUh~ϟ?WUUUUU]`m۶REBQQI͛Gd)*11QUUBUUd2;u$Y,ehDڛx7nܹʕ+l3gB J/_tԩgϞcǎջwop!NM^-<<k׮\~Mw=|_ѬmKK ݻ)SUUUyyyxBխXJAAzj/|>Qfff'Nev|}֭zzz ,Ζw!&OkkkiB(66ӧO|>kѱt[@ݺu !hՃƤ, ֵ|>?;;;::Zጌl')#B_|ˏk%Æ|!++ ˋsE3s\j/2c~͓C[[UVQӽJ,J"--TǏ.]O QF=x:HC͛( %33Օ%*u3&""ǃNʢE6nHm46lcc) p"/^8::RD.4sL6/_)DL0P\vϟ?sss`?*((IקO`jڴi8,B>$$\.ԩS Cmjkk_~=cƌիWÑ&lnEGGS%*Xj{yymݺسgO[nIҟZWW7;;;;;{ʔ)|}'GXX䂂^?uT##ʈcJJ /!)mtMVYY FTT˗/M///Ο?n {~!Ç222rwDD'Ç-ݸq#8ɲ"``2!'''UUU@ ɜ1cdffTMMM 򻻻;rHNNNNN`_Yfm۶wރVWW=zÇ Wf, ‚UUU޽;h8W4 u\lܸ~;v,###44GlGGGJ~~q***VXɓ{¬~s~f?}4hРCEEE_b\\\xxe˘L&Ǜ2e ͖rWTTifݩiiiwVPP0aϟqm۶!>ve,?}^~cooիt<) SNxGMMMMNNޱc,6l؀Mauuu>!% !B?AAAھ};_?.E_e˖z999___8|_"-ɓ'333CBBRSS;Ǐ(_7`,^ɓ'qqq6mg #葡alhhؽ{%KP}B8xiq92444??ݻЪ.]ڴiͅ8 ӦM{26(n X#bbbrssBÇD600\^^5kZjN0aB F۷uV^MM\tu D B[]]-/3G#a!uejӧuyŊ8199$=z|իW`J+V#Bzz:lش_vvv[ cܸqaaaTodGPp‰!2Q QF0ӓb ЦM"o{\ڵkډ<feew׶m[>x7ٙR=ӒBLL 53V͝;Rľ///ݣ0 Bs&DFFDbbbƎ;v؛7oR^0KQ`ڧOW8ڰlPݛᤧ9c )7o&zUʗŜc]QQ9+((h(6oLMOKK]#F@ ,SĆ?///1yذax֬Y+999),,bGA- EME(S%JDDTA[s211;wl9sfJJ,5dGF:GfCIO4 Rutt`4ٳgϊ+t>#ѣ3H@ ܾ}nРAn۷~}XXظqdUr=zdoo|.:q\i\\h !χ,sAz`?˗>>> 庸$$$-_hVX$m8~X4;G4|>76?nǏn:t־dkkKoի>WUUaK ֭[eۄ׭[X{{{!Ĕ)SBo߾D;vf~СM6mܸijmmm>|CgggjAA:V4p:t[n /feem۶m:::M3|>M&;.\uZZZ~QBfff/_޸q#Cw>|(((tO;о}{ZXr-[!vZ***2xb]nbNFF@ h۶[ ء  Ƙ1ci۴i3k,!4i$&Y__KKѣG?y9U1$8m2ra} ,Bpss{{كb0Pnn۷.^ÃVG,qqqxׯbcƌq4?~ BKmrrrjc:Y# khh 4s˗/p8ׯ_ڸrUSRR-6;;[lР#GBeiNJ.,ѼMB }$ߗ:Y Ibhd}t֭[[[3Z5/*++u*}`LGGNJ yy7o+Wݻ!>lذ>}%$$DGG۷^k@<|ÇxǏeee`].iĄ`„d?i' (ݻwb3׮]ÑikiidܥKI5/_6Vnuu|Wq苆 ֕$6o {,pFΝ;t+T6bˡѣ󴴴֮]ׯ_㰗ϟ?r0 @GBi۶-lb %BS1 ӡ3f4bG ԩS{oܸ[߾}CCCD\2gΜZ''LIٻw~-[Ch%c"B됺v*6 0NŴHӾ}{& TQIPm\=s-[Y|$BGG'//.;;[a{UUXg*++K9sРAl6;11Q9800GtJWWӧOM!CPO=s Ѡ4ٻvfkjj>~H;DD BϰBl6m>-X,GRJ O?~0`TWWWWWo~) mq`MX*)g0%%읐%Ax# 2D`W^a !7ohjj 1d2$E~ӦMBb5GVUUJvb10Z6mZnnoAA7o鱱fĊM>;H7 "7M&)(+$$dĉR>Ǐڻwkkk޽{]KKKooo釠wGƻ<˗ׯ_5k|4رc_Xz ֭x򾾾JJJ` 5j,w{yybUeٵm۶*&&+l>} #GrBrrr!aŋmOD37-js}̘1 SFFFJ/B3gx<s +ּRbO!ک@|K:M+?55|*EDAAAeAbw^qq13zβe6l؀'>$=^L˗vZ?t/^tqqj!̫)6dȐ7ol۶ ݻS㽁mZb!"\tAf|)ܶm uuu xl``k`3Lggׯ_K$lhhhhho߾3SɥIw###)ymt۷oO>aܸqXB^^^9990)eddGQ !zrzzxX)B&!!ݝbF={Ò{5(vu|ONNr_b(͟?' #FJ(^hHX>/*ivP/2`]I't˗/8& SSS%ٶm5g6SNb-X,5RX "L4&H:jL[[͛7Ԝo޼=zHzج,{4Æ  xI&.B]]܁Ɖ߿5:50WX?JCBBh̘1W ,ySh[...@cBF抷q6TVV\x*(cWO>x# /]J5uREҤrarJ)Hp݆a0Iv5\=9 ^ABH]]SӭϜ9_  ƌ3Zjϻ XYY]rǏ8M@QzvZQK|SA3g:ZZZ `=|gb5B^^^U 3/a|}}6,%<?~80gϮZHE]]}ʔ)TG"ӾQ$=,޵xf̘իCTUU|̙ G.((8r`"ܦMƆ;V44)˗Reenݺ5ՙFee%xZZZ;w1@ï+ǏyyyZZZ]te...ak߾}ޢ-++gXFFFR:wܠMLUU-DCݻ7Q%III?n۶-t<!%?ɔvMC ([,mLf=DG{*?Դ }B[VVVZZ޽C~CͻwTTT'ŻQL0#G|?4`ԩׯ_͕޲"22N2|||vލEzzMmm <<<Ë-3uq5P0_vM7@h>yyyv=(_VB͛h4Jh#qeNǏ=z!4t&Kl!33bp:gϞ3NZf 9TWWéRҀu:88H \pA_Dhh(ر}Y;,KL&Zx%_res‰){N(jii=8- !'OP} ["?qT_FN,,,lҴ ֋aZLԩSqPTi!)i۴@Fٴ? A к B@ B+ش@ CDFFRpln@ qw @ bF  B@ B+m@ "@ F  B@ B+m@ "@ F  B@ B+m@ "@ F  B@ B+m@ "@ F  B@ B+m@ "@ F  B@ B+%K_]ss*;@ 83}`;6>F*;@ 8Q@ VL6@  Ѵ@ F  B@ B+m@ "@ F  B@ B+m@ "@ F  B@ B+m@ "@ F  B@ B+m@ "@ F  B@ B+m@ "@ F  B@ B+m@ "@ F  B@ B+m@ "@ F  2sν}VIISN3ձX{pppBBnbzzzw]dj><ɵx-9y<ǯ_ndd؟7A}}}K5?JOOw^JJJ>} ĉW^500ر_S ->NUUݻw߿S& c`:آx<@ `2,OB ~.d2eߪ+WRRR͛޽`iiieeջw¥a8۶m366ӧOGEETWW+++ 4ӳW^ͼ5kVXѧOfK̙3+**Ν;BeeeA̚wȂ?*++6}%$$\pAJM658߿ҥK999EEEBPKKEAA!Ԯ]cǎ.\ wiv IWuuuoܸѽ{&TW#lrPϞ=;{;wt9[VTT 2d„ M.DRO8uBҥKm۶mrBp֭ΝOS|ѣӧÄ˗/Kt-;;{ƌyyyh/ZZZ~%V;vؿ@ ۷o߾}\\\TL ]x[iiiiiiΝ >{l=X(nݺѵk9s%ߺu wꤤ?Ν;ϟowyvJV\ G޿޴:w&N: rn:yСCI}*婣<<\|YYY*ݾ}W^ )((l߾}ƌwvuul]mvߒ>?ƍfff-" ===o߾#GСCEEETTTLL BܹsW\]޾}{%Њ+_'x<3x<^ttԩS_ҴϟmO>ڥ}>}f_|*8p!ԿQ񨸸!ַo_I#J SL'BHYYٹgϞL&Çnݪ,,,0a˗i nB-Z𷍍sssjjj,XsN)Փرcnݚ?o޼37B<իW!1j([[[ի{{K.7)55?77W|BCCS'9:thyyK㚚o"|ƍ,ћ_Tmm-Hl}k@R޼y"a?~)Sܾ}{РAވ#$]~R:22rڴi\.WIIiڴi/--}AlllNNθqbccqiiiYVV۷o?>..K.z&S]]vZYזb̘17o%}hto311TymӦM7oll/d2O:_yor={vرŋ}5;vL(vή6jժgϞnݺirŋAt`2AAA'OƗ|||"""<<<#"".^*q8 6``8iӦyEDD.Z(::]v2|uΝ;7n87nTVV:uť y%c5ُ?۷ŋ?zׯk׮~cɜ?]m۶#=<< qڴiϟ?Uɓ'%]Zzuzz:trr‰י8qb37U_!Bkp%ϟ֭[7gΜϝ;75jԨQ^+|œ&M‚>wUVVr \ϟ?w|sBSNӧOܼy3yިiiiisBĤ.77/kFFƍ7Vhڵ Eq\Z>o߲l0QP(,**JJJ*))iIII_~ϟ6m}}}0rH,[ܹTJi0:\nDDBY={4/_lB UUU}_*:Fھ};6--&?~Duܹsqv킂 B_~ݳglذ޴iMbC9s jBe9& Jl!55`XĈUIII;RƮ$SSS333Qɲ`3333==] MO@F`C<##Ct$o_FΘ1N8"B}%88!ԳgO]J/A(fggNlY;a0YFB;m4a„+W"bbbOT V‰Z 'OLUX,S^\\C3]`OE"`fΜ!pPMkvǵ!S`Y>{, 033377wpp500;w. 榯ogggooolllmm-2͞6mȑ#ORc퍌FebbbggCj)#11`СÇ722rtt|]|~`` n^zw…T > Ҡ rHZQ=z|D5Ljjj<۷o;88ܹsKyرKvզM_QXtm Rh1 Xre`` u4_SS3wܐtxcǎǏ9mF%&&l>?~xH9rd~@0 IF]4~hiibŊ>twԨ}͝;7~m|>ÇVT>|߿l={,?lqr̙,/Bb\.ce@ MkvV%Ջ/"""h +WPR...XBDff˳gϨ/6v}Qs‡Wgggv#ƖXXXܹ +c411Al W޽;;;5\ wttlhѢnݺ 0`񆆆ee\MO@#cm׮(~wsӚ~`0໐Y>}Qv ヅE```ttŋAVPP0uTP SN[p7^xuVھ};T lΝ;߿OUZ|___MM 6߼ysL&͙3G4>iӦUVV*((EEE|OAAzڴiT ={?!dllk׮/_8qJ6>|FFFgsxJJJwzⅯ/,AjB!5P([ ^^^0M?<`B{k``@̙3~4 &9s&***00>եNZ^^`0<==oܸn«#Xhjj>}hVVVVVVk֬*>Bx-vomQ/TsTI㉚^-Y*t`( rmozje˖/_&%%IZh*Ǎw}@wQF"qܧON<p)|>H˕`988ܿ?;;;,, ,˝5kVrrkUM؆v][4;Ĥ/DH;w7/YYYܽjA]VM(Klʕ+aFQK):t(؀I&Ti֙;wYjU"YYY]vڵku߉( Nxutt?,''gϞ=+Vҥ pH U&;ڵ;z(;u(K J<!cc㠠UVaH|>_]]?!!!555((xTTUդ ]z8.n: 5R;f\ʚ={@ PQQ}kB;4p8 ȑ#E_x:n8&Y__ڵaÆ6];5iњ߿>}Z4]__Ν6{ҩ!~Ԇp8غHtl8p 0rrrGFyh EEE8bc١***Q-˅Y4&Pjj'6ݻ[oY֮')cSɵiӦN$ٳAرֆ ܹ#6:nCJJJJ V wNb!BO B^Z4;tQ@6E)((Iڸ\.oф$Z)))T1T]]]ƃ4Vʪ+/{o^t]5BSСCKHR+++aPEEJI+m@S=|޽{a`0<(E 1k\]]&pn]TOuuu*mmmE'b FUUÇ...C0aBnnnXXXUUbI>}Lfrr2QPP\Ѐ6mE͖z1Ν}6mdjjj 233HNN=vٳg8qUUUM;XM[6eee󕕕###T'iQ !BH(޹sȑ#RFwށݢX(4455󳲲@o,`塦F;H]]]􇲸0SPaCpĉKj><`BQ'''Kl{GZUSSsuuZZZ2/so^ɘ1c8Fl jjj%%%o( 6e}с1110͚5 K" 6Yl% 6bg"ׯ_p84N:y{{/Ya̮]޹sgyyy\.ȿ{n)TQQɫQ/ݻwP+6?~޿hwfnn~Ej[n-Yxxx~ZRxv}}}5k۷СC̒%aI+F mfffS.]z嚚+WxzztP2L)Q`@,w#v־}|?} K.\.777W:˗/C`F1uݻwö{RRx>KQVVrؠ !02ҥ 5ؙ[ƦÇR ۫:[ƯTIII /((HIIDZ30ɗV]taΑ?`ҥK0F:ӷ4{Ν jkkl,.gM`i͡˗yk׮RJ l6{x[_CCSN۷WSSkB) b'D:{3Ҕ)Sd4$,ڑI"lllXj ]]W^%$$hhhO<]vńw5bP)5[uu;8*mڴIL{, ڵk>ӧ;v|%>>^xI,iӦ]]]i4a͚57oެ۽{`Xݓe u%((ػwn|NElQ@)Eh-kUgkkswwwXtڵE$h,:u²)G^z 64ׯ_8p={( lqqq!VVbbÇi9"Z7###pf͚%zBee|cƌ1L4IQQKzSSS%ݥcǎVJIIٷo߾} \dأ),MѳgO_[[իWÎŋ@_FX)O+P(zŋaՋHױ|rto޼ &!mm#G0'''zjjRje``FQ555n˥&M3f"sqY}4)RYY9{lQ߻w ̕"ދ%+WҺֺu`8"uq˹۶mC4֡߿z Bh߾} K޼yBMtuu8p >o߾ ٬^&[nڵޝ7U}[(ha(EAeQg)cEtgRH]^2`rrУfj.^(~ <8>>~ٲe1"M3mN!XVE};7D]ԩSӧO?HұcG`mܸiƊ Ef4Ձ\sMzzʕ+y#fPzy+bڨD/h|9},koDN.OC=Umm&kcǎ DdH2''gԩ7o.--Zlٳ󫫫nMHMM]fMqqI&~+%11q…~wfΜYϾ%K00..?.((l~:to64tATEI$ ]2{ɓ'flAjB2Lg}&m6) "ZtxƎ(dܮ];u%֯_:uJrk/:uJPXB|зmVr 8P\䘰СC~8ϋ߅SNujpTDxb}v̙>+**tҾ}RRRƍ'lƎzj{Y`6n&;%''anƍKOO/--_:|wo3f=t"Jhʔ)k֬߹s/G29sf}0g*__hwʕӦM.]ڜ&f͘1_{5Lf.͝;7""sIӧL{|l6kDdX&Oz;vKbq޽-J,/>^ܹs1_3fw: 8_z]wm߾jVUUݻwڴi-CͣA9sڵkKKKKKKI&yΜ9u|G"n^bccr~zLm}g-[&Z?l6۱c|AjK$#Gw֭[lrzxLŞsh{e{n DM7PqqÌ۷eee>?ڱc׫OwWtΝS7NKKsj=cNwA? uzDx͙3uu֩(99u5{\m)j\;x,zMرC]J8t'RTݻwk?vm17oVtr5vmi/Tobdffzu}hRZZvQ,:exbK;11 6-KFF;A7IDAT=K;}VTTTnn7מ={˘ꫯjF(l6푦7nܨnX,N~=j7om۶iܶm:ӧO}'{Am4#GS{g"7`2𰰰{j7VgygՕFXXȂ.z>++1׮]fϞ6;^:xێD4c \ZZ4m=uܫ0hРGJÇ{>B얖S'::EAIIWf͚6Ǩm6`2L3scx}i2'&&&&&jYĬ[Nt$I{Mu]Βf7HJJG:@_~;vHHHpm p]B>aѣGZg6L]t>FDEEn 2dȿ,xxWg п;w}vQxw}Yz^6 ڱcG頶 '--M;Z?u\;3DQ?~m#}<bUV9$iŊ>6N[k&RKI}h&; :4##c̙N9z;z뭹sj &_^d_=Z[={\dS͖^_ܹs.]:p@u^ڛQGC Yn"t_|5khoƼy1VDD:%W^(H 9j(h4>3k׮U5mZFagjݡ>DԩS'0]^^#<`5z WXiӦǫoU8}+W:vկ_k1 NIIIIIQȒ$%%%%''X823fXrSvڥNc$#""z-/_1`08wUY{3V~-[Ɗj:u>}֭[uGhl?Mޛ~U򸪩)...//ҥ۲nݺ58s^RRRVVֹsgA"aQQQYYlѣGLLnZZǏW]L$/ӵ7.\rss;bExSZ6-//k׮]wl;q$I7nT;iTVV*'~ĉ޽{(..>}tqq`۷6m$zÌ3/p{5z>*G /\`6###|=zhTB'شb***[LQk6&&,\pM6QQQMahZdYҥKӦh_x(<}< &M5JDUUUڵO"Yzƍڳp…g϶mV\ۈDNz}$I5(?cuuutt7$I gϞʲZÆ ܧ^Q|1KPttA< Qs{(<Ν;GU_C?Mx'7lгg#G4A[[h6yf״TUU5rȂ hBL\McxDG?>:=|"U6p@ 6p…'ވ,66ʕ+}QV hZJKKsrrvڵzjv-<}ӧo>33gm-^.]Qi"m":P#cqND]\ikfDD&F&= lՑp|dFtnKm LD<q1{H8#²K-N(DOڰ3#z-:yN X(Ή9G8lj1.Zzbd|oͣڢִGޏE,dd Ȥp&&HyLb!+\>].㛳LJJ8oDL&"]'ϵ'7ןM 0_cY(sQ%6pp5,Q&sp$+ˤ-ai0֑Iv`ҺXzmsjr]}n&"jBoOeH8V)$ 933Yω4J}2sUJVҪǩggщk~:[NO}B?yiFq7Ӑ(]וKXi"8fp`FM6`!+ܞ{BtRHVEYT9Ve_F,UnZʽ4a9s\rsZFFUDD@cYpbSo_>fXZbVf Z-4k9f;tNtM;~HwTǑQyee%Zb4G&O[WZQvFG h%1Wܬ''P!h ';2:FZ7*"5@FlpQiC,`D H[K)r&>PFcU45muNe.*5Üڛj A@Ptך/-(鈨'jcUHڈJNIn}zm/{-Kò%rC 3ideIjx(!ho%b$9i3AqZ1iq".SM5M"w]&~SoIG)yIQ\s1Rdk&ԷkA,t:ݽFY9={S_I!&sTibX6{IjW6""R q޻CCF (RUUuنNҡ "Mk[zZ' 5Ls[uEQm6fe#O0t:`0F`0xG$I&IכL&ιشqbM&n3D} A@0jZ~ԈJ9mA@@mA@@mA@@ $k~IENDB`././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/docs/source/traitsui_user_manual/images/alter_title_after.png0000644000175100001730000000747500000000000030335 0ustar00runnerdocker00000000000000PNG  IHDR|FIDATx] T3;,첼\@* lcbmZUFc؂V,Ŧ-6)Ɛ(mA .~wΞ{;s{gL6?KKRI|C%jwlK`wNn焆2=3;L/%Gw)IDペd|eGu۩ 0NTt$iz31$O=: M(4pFU;N0{OJw%=mW <|e.,wkjj. C#{AKL*W,`r& vO )[I0{d7= zhz>Gh>yzh}<u(PBy?qFmy0d=DUaX0F̿ܙnTgGϧ_yԖO>{Tum0}^-t%3?63g]'*6nhxׅKcb8hJLQ=MP`šUaa("ä K( {C~ŷ OfO$vf~~Ü֦=&}mϾ疿G[ J+=;.e$lDᇌ$@4L+,#)]iILjZ J$+n"`@sF/<sM7fP,}\|O/3.3E9'. Z@GNzcaAS_]s(Kj H%wigo yUruI?!ph'au5 9N;99,v/3zݾwo#us+bϋ8cǴzOүTۯXK<D].:I2q㔁!̸J̒c^ !ACny>nI4g?qZ4y_0k6mk„ .%yC8MbBLRlf3MVDy(qZ:]W WDڼ1'f6pտ.^~mWV,F냞)ko!;Ĵ4K˒Y|Hɢt1%fJ0L6s8+\/~2dcMom\=j1&y^̘Ûw[ }}q!KsJl 7g -AQ;!I.B  "v8~"-,O`fAX;uݚ-]-_je_2u޴Ύ `0֗.CqCLwu_0\-0fd\yh@bobX7$*8 CD4)bg 8 *} Lnxװ;mٳ̋=G{+  PH7<_JR~|Z>Z|1 R'oJUlon]`W ?}rQjN"&3Daj7{÷q0 ##dh)+4(3MPҤe@aoaU#ZIxř7M]: <[f_+ў4wo4o*ӫ 'pS aLX>΀F[uw(|$Oz]Ld^121Gi%+,. 6 !2ode5=uHésGR?:xv;/HbnŹA.`kYm?> yWPlM_I$45% _Jx*ϓp\S,ԈpʰiC^ٍwn|n1㤑<O<@ iKKV|w~ҍ43>&)4@^gOxb}p@cߩO+5c0?O<1{>|( o!2{j#-\v?)E3vsؙqnSIZ2+hV%^ee Sn^Xă5|)Ľ*-YRЀ(rzF{KJMe_ :AkswPi-P 8 !졟83 ϼ`2:_ ugG bexG܏Ғˀ0υg(?!zT2C"ܠ ҬAQˀ Ŷ h`4"^^2αKh4و8ÒG$| yC OeWFC"edpy] bF]f"WA zleZ9,H}("B w 2 V9n^kZ4 VрQ3O JTU`>@Ɣ94(@ &T׶|o |LkTnԀp?att#Ӟ sF^(fvbKPCO`i25Kܔ|0p % +5 .\l^gp0PZo+`V5 0KbiMilJ!$k[By,eX~s=;ٙٙl?=?1J%J+vlKts2,j/&x)1{|4 b *FSm/${Ocx0,2P\bCcD W&T@ #ȍ@ Q^60LSQ J?Te|,>mupHMZp̈GǗ1!+;-]*Ϲc`,Ymh$0Y!EB어E\{v-D2Y}6flxn&\*Y<&^woM;K^쨙į/](݀vL)+>r"ɉMɥW.j;}&G FKm0[dVhw* @@~+\%RrE="KO'Ԥ'c;8 P K>Y:imxRUkgeщn}^3 >%v) اI^-fҫAZA+FJb9@^x4h쓩ؤ^V3t^}uۯ~C8j?X3(io}eثeF{ ~O4LYXD3X OęiiP4ЕaUXzd 3 -mזIKn}S\7H]S{.!84)5M?;OՆO}fֽfQM`Er7XS/N.fկKbL` Sދ, p;ύO,qSqS|U_,WZ;?w6{ ^ -of$So=qhv=v< pYWϠ8e` }dJԔc_ !A#S1ۇԅ-$Q~Mh%4_Ӟ7Ukk„ 6$yZ+G|2SE%+etР2oxН%-eũ'r9>RLJ 2iGqm>]zõ3{Nf.iR]ADB `yhy{3PN>ɱkUͣdL$/ag@F~R.o04#͌+IaIM0hPCp$,"~:g% w*Sr˚GwfM `%=J7_,!Gx D\u!ԫя%fziR8}W*?*H֖-Ù[ŗi@TF\Xȝُ5|%ľ*)SȩtwF} sJҩN|[?n};_miϖĢWv^~ 3`ƀX6-;cPp,#1 'A $gh"a"!LWN9tDS@0xf}D4z(vWmєR8028ټ/b 60@Ԣg9,Hyшa w " T:모t?H@-##p6D2=򍩀bz_{S.PSA&sﭡ#ɜN*Dž7ƀz&-R2 W^34l 4zg) "D6wHMG35hp9? C4URk`028ټ/0230D|9ְoL/ߵ) p/oLQS-*]N#G%QT7)M4QT7KQ)ߘ0/)*loLuG40/pց$E:嬾E)!ZkȔt#`BpE(SDyi>ଋ12z2"4iwqC( EtdӾ9Wv"` @sxJ"Z1 \ ߘrTu9ݢ4rxT" LuL!">!Y@ȣ ph9v䗬!+% r?ǎ\=]䛿 ewCj$ $:D@C8dCBbC$ :# ~'S2|Բ&8fxZ)< @X^F I"GK`L xn0x@hhj"䤲k))j@vPTSwa)DPߞZ}f^B3L6QSb!'0>'+>W׈):p e4C4-3xiiXz54us2w~տ>_m8߼ZԮy/ƍQ$e/Ow_GC`I?ghޟ35\ 7_y/ƣ&Kz%tQp$ޟ34oO俙.y/ƳЯnmh%=34C;8EcŹ28,TܩƱxIDEYn >i7w @ ,LUQ2잠_zmmX^)J<Ou9MWgfQs| {gɤjnwi*40%b܀?R}8Q%s9k\#q$HhƬΕ6Nu]FtU_IgnquoeۙepB d8 9R0}tֿ쟏Sqq+x%YIk0f@#Qk`kTtsWQG5EtSB+Q_G"QG* "+Q_]r\B9emgU6RZX# $HVirSqlڤ\^,}B-BeEM^H=k;'MKYcmBz͠`en:711NT+'"_G;}CWoEyyۿn۬0rU=J5^Z}6[f<)mte\D`* ð[A;:Fgs*$ z<; \=T3+bx*!I2*OrG/+9 qeGUD&9ߔ Je/7o;^Ӯ-m< %ї*`qhArHbnuCyD iC.w s0kDxMΩF=Hmܧeh~!÷,u.[$hCecBCyb ?ҕ`+Q_G"QO ?G5ESԴ7F[Sŗ03Y.!K`p DZ}rH񖏨WpYX>ss 1#Ilʛq+z9P\G5EG7a@T yPY*SbK<د$Oݕ BF,BN:_Y[=*UM-N$dYESn@ ۯr&gWl<k-qFqUY7g0d `,cF:ՍzNIԵ/M6d%E'!R3.@( >д- ڽiE".`ylvsG* tax V.."E(L)!w|!6) ,d|Vgu//\XD͵i"0kn3ko&t+;ۋI,縶Id;fPJr AӠʇs=0HN$%7_j?(ʅsWO"E9+T9E9*i5j,"uy J[P3N=uzG}GRiqKfTG u![ʂE9*i5j,"uy J[P3N=\K<د$Oݕ BF,BN:Ԟ"Ԭty7KdhwDMېex'npB.Wð yED<,G)ԱU-3MѭTe KbR'qkiaylaMI\[oT|]fvvVҬf-N?2 QC@w(qn!ʂ~gWl<k-qFqT{]KM޺uique7ILF"]F}խ<:9v"1 W_o&t+;ۋI,縶Id;fPJr AӠʇs6WtP3zz֠e\JZQ玢0"+@ؙ\#oՔ֤J5͎q?PTjF.2.CFPTcF(Q\sth[:s{ ^ׅZ%b Wyi&B> H:߶ еds}Kk^Bnym`A[HV!|[-:l?ч?zun|y:n|y:m^^?z$VCNp_Xv'TKAmrMq-M<{.)pNWvqvFZIa"Aa^Mzd*M6+ʣ|`vX/׬;s%H& D=djyR8'+hH0QXJJbIʎsN@0 wtV۵H!ܯz|5N wnfRR۔C^޸C48/w7w/{x%y]#! i"$i$??l/J@/ף)?^qoi Ρyp"[ZyhT;#AFsW!u]:}B8-.m^ gdW~v%\2@Zmg2kuM #r0N# OU[MPG-Da*ɑp s !ƃ-M?\Y[0UUm(p8T~%M6\_5Dr^UuqUwxSC?WmhdX+Α158b^WbX NkVnܬnv5woڝ;R_KLp6 Ǜ(X$$$ 7W𮛣_w$Vd1&r!S˂n፸^N .icZ;Vd"@q,ѴcEŝC eUQ[N!kG_ģOӡ!rCa8l ߧX7w *CW/N㓀B |AN,o5X-"U-+_'lQ{]FZ^qO2ZE=:HDd}R>Qv׹^Ӽ=i Othj3i ,ȃAyU\(Rz WNmȱW"c>jqדmG$YD3 HIV##Bktf.#ݡLE4dpUv}g4s lu,c p8-} AVVEUAhJ/ 6r{>Mg7hDYxuUGfco[ly_n[,O Z&.dXZyg@(]kektFտG_Ώ1?=FտG_Ώ1?l}6Vb}:SbQı4M8oq1?sv{}RFԢ}>PADyɼOˀH2q&Ѯ.ҿ-7cKcYYgr_GX!qv4Fd%ہNxϓN{nȤ28(Qqv(CMPADyɼOˀH2vskp^E%Vǰ!B(@r*h~85z KO j@t&I崯 .ӐQ DPyW;k᷷-mJ'/n5 hM7|d,pwtEz=hVF1?b}:4=[5^j?6:F565O:]䙁ݻ`#sOXsujZnmk}mgݘv1A4-@k~5o)uW>W[f Сnr+oi:[Zfڛ7ps6Ѡalt"G^EEQIbmwef.e4+;Il縶IY r tYƕ_oZ~xiaaF` XmF,n&I`Hf$)$84hFտG_Ώ1?l}6Vb}:O{ٮ`)Q$N6 rq{L7Vrꐬ#]EeRRzO:#&}Kx I0:¿0Duٌam;u{Ѡ*h ٘z(=MF{'bM Iie{0ZD',@j_b-' \ssj&o'FvX؄y|ʡI75xm|Q:[Mjo Ϳ '9`tU7?ښVo.{vtkh}UG̪͞F .EB 0?1'v_NDڼ;{!|[.>iEo‚|@0tE5Fyk%ܖ#q"yi&O3B=$Ch5VI u >3DzS"EJ-<5zkhWtd/y(V6mPO?9+-oQ:.-ZPvF7eFՌt+a-(>ݤzzλx2pcP+ :k?ao|b^d4zOw@9C^{.60|{RJ)774uszGQVqwWw.2[I۪\ ?ưJ u]8l,2PHh%s|xNSny'uu3sjXp2IY~+yt%RK{^L6ӻ+BO3̌lP ȧkmB( xvぼ*i^Ў Y~%]m׮ODsWGP6M($WS>Qvd5m;—G3]/6w$$\۾ i4ۯ kVzŪZ*[\[}1G9b3<-/kӬέ4ZM-Kc](ƄeZqᰕ #HŠ9 (M:sswڝ1SQ '}q7C̺5,p8$oⷉEvznyo;pҪ2Inf13d- K8m"`86  _\ W ki&c9d3@>$St{;mWSrGŶs ޻D=zsV/It3DaXf@c,C€YYo^56]Mrp]q@NHIh:,b1di[V"Ȣ1:Yc}@3uy}sZ-AaKg4\[H&I¨=:-6m;B{{dK3fU'$=z~VegwJ[n+أD ;J˖'%#t>AVVEUAhJ4-?^sKoèYk][]^F9FY& F$,O 23.7c޲<=)CohźCsumXy؃zn|y0E7a^E7a^ ?տӪ7Yzzv|2x_TѴ6-S`b#pFj_Yg_*O  dDgj $bp~8 >,4ۇ&E*J| %$-¨2gi=J02 dU͋ F=5[\\Ki"ySO25˂zA hۛ[vF81'Am`Sp_F|=#Z>]eb4 ̂8č%*nl #:kyei\%7C2:.t~a@$UK<د$Oݕ BF,BN:Ԟ"Ԭty7KdhwDMېex'npBiX^ũs,Q$о7#T2T]eۋ{Hlu ˄yFBܙ g?0 9]>@.Ro?-:< Gߌv1~8Ol;_##[yw >|t<&MMQ7vZ#4&d>kyppMxM.ms>nnDFhL|@#'w8fOZk6Y71ܤlh$PnXE i l/#t!75 288 <&MMQ7vZ#4&d>kypqi{Ai2ku +rGQH Sp_F|)/ף=#Z>]eb4 ̂8č%*nl #:kyei\%7C2:.t~a@$UK<د$Oݕ BF,BN:7o]@KyeY)LA6/bk9K[h_a*pyuzmŽ6W:ta{WD,U1F 3\8Ol;_##[yw >|t\*Ͱ~|&3y< Uz[?BeM0".噤XS") 0|Syz燵(5*M62%mHOy [vv]Vy6YZIg=ŲK% 2PN:g,7fgź\# _S,cҹܪ>G?.`o'g3jrKoNMGyi7(Eǘ>c`uBF7@RWkNTsr =b>]ޛpo=*2qg*.QK3/uzkmSQe<|v+}Vܷ(Z0 ͷ6x y/$NErs+39U; z&i!p_E.n^}JG&C@DII~}7Qn6Ʋ+xG." of4?n lށ8Sڥ4>ifPmD8$'xP Ѽ/m5igخI4n@eGw *v]NTؾvlV6QʹTSHCx}+N-uk3F C4_E`ۤT83n;BkRP.PN:fYJdc{f7H=T t]1,+>;(k R"uRʻ vQLΟwlv߱N3&sƲ|45ZI3 a30-BF7ف»'@#n[#H (}6V0E7`oѰzh1^xMmtഹx-5^ E<YpFGfH涷{P4K"0;qMXh>ѴcEŝC eUQ[NWbisj[LW\jiZi Ѯ0E-V"v!D_2cw6XgCM@`;l6`c1In5h{XEIU<<bA-\Si3ҌwuĂi.KʾT- G]ۄk1 jJ`mֱ.ks[X6Ğq1ۃԍkvWS\=ͬ"$@YxL "{Y".ťڛIi8/+Ƌ qgAmOHvkh&p,yLmP.ِyIn5h{XEIU<<bA-]7;F$V#hTM/-xnUFT768Syvm6QԴB)mG!WC1&cw =*]?~jkkQyD&ĉ(CerqT|U4iZVnu - wQu KO`cXԱ6z}%e@deb@ `gw(o k^Iɬ-&Y"(X"9#  V]* i'Yy ~vfcI<='/H'Kkv(3Qs,]˵}g4s lu,c p8-} .yӨ|tyP|ui7ZַKn x9 l*ى0wc9mW7uiZLqewrks%4/+` _A 0&iV*#tyѦ9_#=9lTorow2Ō^Y9q :xcPNSny'uu3sjXp2I\o6Y5HBwTeHslݍc;gaUMSQʭq3yyd\Ă7iWմۛFRu k[hh6pbP*A٪mm {mE]2I':םŷ_ 4M~5}OJ3EXC$ݢ v(+;p!ҒxnXX$l]H # Z.$6fSEbQ~و_T4{7Eʹmq#C $n%Irw+B=utH>~%ĚnM.NO&bFߧѸqOQL?ohߧӨ?ohߧӨݏ#t>Rm6(-xwNB\|ҋ}cf˅Uzd𾩣ik,mZxR m#UFb&>y˽P!+$.pr@96_ Qolp1~s<-p,ftEw0ܪą$u`}Ҹ][A|YockiWLqquTYAJI#s[Pdҵt&; AqJ-.6Os%iȖbSś2ɳp d^XFqi{(Xn[6ݯ< b,|oݓ(cVcOj/,n|gX\Z8Y"-1\g>S1sJ#~RߧѸqOQL?okīZcɲ%pvW8;1\gxGu+,F{GeḾrR o HHh-# axoB8 EgM9|Guw ןfg)# _,bD=>IvVB= 8U=#Z>]eb4 ̂8č%*nl #:hoi:<\DjC.ƑăsT!rj`iu6'iu-dt;7Ui>d' 2y>kBEDd]ba˷U\#609`lj;;RwRië4vȦ<NX!~S m;KԵ=MbɳْI{?y[ǡ隭>E. d/崐w);3{:,fӠO"Hp0x 6774n?oiS774(*+oֽvz#:lA"qoW/_2J +nnd$i-Ss`n!ԅoC@( xvぼ*i^Ў U]?PmupɦMn F9mά\pFA5G_,bD=>IvVB= 8\6uI.mU$ 꿸Vped|I1qy쵩#Xgd()p<׭?kYx u7yEv 9 -YTήXIeq2j[y`S(mes1Ikpìg^ {/dH֮!fm~8'cqSxjMmu*$gDG#r2@d d3W[٤0d` rIcI'y?"X]ӋD78R !GF*.[1խ/PҠb;Ur>d%O^H=l]YZAVV3SE(l1QY?.!;kK2&۸\ț\78ᱱk:to Sk(`Ym(C(U]T[uX/ף)?ч?zu?ч?zu,o;IH^O Ψ [H/Yʚx\K(S4N=SNDXԚT;QmW;G'_XGa5s[sʪʣ8If@&A a# EEa)+'*9F9|nk]F-OnRd{X||ef̈Kyj@.\o vX[<*<̪7d Bd 8kx෻%XUF0ṕHLA]fo_arde,KY-ˑWˏvVlȄ"e+4|,e7nР I)45}GY4RgHVV(ٻbo#"2=ؾ s.}Ťv۔/pňT˟P?Ґ|y:`7-[Ces^\#Ȗ֢0Zc9\tfNP KWY`߼t]`~la{[_̲ZDB܌S#W+?[}6QL.mQ: 7n9$cqKFevy 1UF`[o9o\8xA}o/>[NT/AO emuGmݴV dc^+473 Jc~r Oyj0[)ç[[",20xq(Lga ɪk д5w{0Eyaw*@ڮmŽ6W:6Ʋ+xG. Z-ONd&GZzuż6f4pO7s,ȥGs\9:ZϨ%-Ա+oaOAt]YZAVV3SE(l۩GuGRXT[Z2d,s!s{gQnޱ4+U5=NW㿔\cLhx s^RZ7g袯O0Q=[4l}gJ [0,U('@/l}6Vb}:n.DUUgx9 SZ&VGO`B$GʯߘSq^F }[Pµ-.Qa}<ДݙH6ƶ kZv4?m lށ8f62蚔POY>' w*g~aNAy,m} K;d`X(PN03FIAVVEUAhJ4-?X>q^xR E76U@I'vو_Y.iWz .>#z#x$gn;lAW#ԮoG5MMUP2ǀxͶΡv-.fKIy^4XVf F wc=-] \)$as~cO0m6(-xwNB\|ҋ}cf˅QI;MGUHF}=%`:n#Jf[+[[-٤ x[J.pr q~X~%]m׮ODsWGP6M($WS>Q '}q7C̺5,p8$oⷉEvznyo;pҪ2Inf13d- K8m"`86  eKk;Kd)=3(:9z sVZ=3ZRk[ŝFb=I3kn[!KK6;KHx-"[#A${ %亝Y6Km_P=$gR1Ю@}~cg9Ԛ65i@Ƴlǚ *`nn|Io5+˽ܛ[i'Qj|qE{O:-e֝>7;F$WA[x. eJ pvcZ8Kk{ujQ@(?ohߧSw74n?ohqGZV5vr]ܺn$onKWsH$ t[*+oֽvz#:lA"qM;Oe֟o4ϴ ѩc'sH/g#u yQ"Mͳv7;10n񝆻!iYi ;qP@Q/mGH^x{-_Y_#!IF7ԑhRGZ[-vK3FV'hxgזͪV/ٵ?!!>Q)eS] tN!aa{k2tf%s+7Ϲd#,NwgQt^]=q]@6Xd]NJ`8F3|ȵ7}w4M.wk0+~ T Y^M -RhX 7p[' <3koaqNLmmdQkE` r #)+z8Ž@)HI ,Ė>{8 Z.$6fSEbQk:|_ޕh,XI/Ϊÿp$q=뱶 u.o ִKťVL|0ul؃-f +PDvVR[Xi!m٢$ T*>#+O۵e%唒~ CLyoqy0E7a^E7a^ ?տӪ7Yzzv|2x_TѴ6-Vu[mUf"\]m%>VryHT;,?[\\Ki"ySO25˂}2n |1S#5I_nvg]Q]Z[yynK.Uyinտ[\\Ki"ySO25˂e{}=%?ч?z`:}rH񖏨WpYX>ss 1#Ilʛq+z_YzmŽ6W:HH'Y " b.:S\:^u7Gx-< #W-)#SӭY-nI}nFd֩l.\GoUDqˏFyOZxWWri/FHmA>^,LQkpìg^ {/dH֮!fm~8'cq֭Kj6iR̩i 1&YuzgIy]ڴz6uq:o6Ь[ vF,y+ <>5Ʒ,zdxrKYda 1FŰ tk_h6I iv$r)VF( &<&MMQ7vZ#4&d>kypqi{Ai2ku +rGQH.V+I XhZDz~?ι >I{_XC}vP<0 -G+ٍLcxS߹:.mmo=T]dv XC#;*閺NjNsObi$}ΒHZ- JGw|e$|Es:|UB-ŧbG)#B alºMտLQM=[4l}u݃տFѠOo4ۖӶV \]Ǫi֒XHXk^#1J}j xJr-տ\ͪY/ٿ;6(KAC;|`ߙ [`8'x_+ }ڛB3VZEsyo@sqgmRY}ko4]Kp(R6Hm<(tۋGO[Xb9"nw1a'wJh+Ƈ2xWEӤ-m/]zeUR#;@_zy*ma UFwNvV;SQo iO -Oxq],X`Nh8o[{{HeO5P3"aNӜIHl4 WuogSt u@b\+>[Z:MgWSY%/]e lvWKLPЬ-%&F;fPJ'ӥ 4(}6V0E7`oѰzh1^xMmtഹx-5^ E<YpFGfH涷{P4K"0;qMXh>ѴcEŝC eUQ[NS&ӭ-"P9HT"q2E䪰#u- P߇[h~iϕٽmq 1qiqyntN@U$` vTl?u 4-VdХ[7bTG\\$F%%{p֙,1]UAT=طμRRKpi$g CqflAqyPADyɼOˀH2wM\ssj&o'FvX؄y|ʡI`v4Fd%ہNxi`ƣIrwFm~X;cҮ>=$Ch5VI u >3DzS"EJ-<4MƷkuyE=-J , anV{;+[]k Y\O $nNcԿӮ$Oqr^Uan:F#\g.Yjz$Oak6W:شSi-'xaY. P6u݌iZmKyRKۍB%M&?.E#t޽p=1.ԷjwIvnKoO,Q>g,#RLoķt/J@b}:ܪw̱c>wEA\H#|6>I]]iL@8 zU?Z\\Y4vڕ枉(L,RS}dy@PW%8ʀMwńZڹx*crD9B 2Ccr/!䷵6zhg2ٙ 2A|s2Hu?u+JkæBVK%D1 1#K:+/iVMo[*ܼ_6QÖ!\ 3>Nд'VIcڛ dɴw GE8E)'#ƻ{d/%ޣZm*y^Ko63lQOr I]]iM3sjXp2IH 1?b}:uo_Ώ1?:o_αm6QԴB)mG!WC1&cw +I;9l]mndf7T%el+$44; *DvQCn8Z4ڧ8+g'8 `ZSZsmF 1 'aƧO2OghԱdҲW}wWvVǦ\$)UcIT̤oFF]R #}^#W)8/##ҥ=FE4Bi H~^B81VPG(Gˍ TuGEi w͙0.ٖ"F'gG#eM@6vygxɠ -_Oi>eYؽ*Ol}FFHv5mg*GI,tI& 8^zԓV+4TMזȱ̦THȁ*csq~Y1gLI$fcI$$I$V>7AO775CS}ot/?ohߧMQ 9X|n?ok—SXqO~khWtd/y(V6mPO?9Ώڦ+>=:/lf/KonΓ|,mk-D}'̐7*[b3'̜`δ>?w۷;~9},tF k/T p[.n<.Cͷk2H#؋)[d ''Xnڰӭ"6ȱkob4f|8< i9RAcCq~F?ƨ}oMSAb?ưJ u]8l,2PHh%s|~7AU5QnHc,1ӓ~^t/HHh-# axoB8 Eeo;kH%rY2#eNFAl#]3N]I )j(FO'!m p$(7*8;sz3YYW6gjbRчݸ)e g OEuCs_ ɹ$Mi֖iC#"lбaF,0aY5of79B91hAbo]:ư]˩S+X( -"GqUE:; ʲӭ젵4K+qklJhN>D<O46O]+Q;Q@P2y<: 775CS}o: 74n?ojڦ(T,_ߧWA[x. eJ pvbڦ*Ʋj0߰1I or:rcO΂iYi ;qP@Q/mGHuB-:O[ڑ]]X%s$d aumbHaNQ@ 2y8uY5of79B91hAb+;AѭnmioڬPm'\ET;~U?1&Dnk]>"kk<@ JDžRW;~E$|kzuyZ/XϷ̋qMX0H#oN>En)kr΂mtIZ2e+I\iGVimonb8B(V6ZxGIբE|ȷݵT9j }.t9pjSOJ/b(ǵ)߱G_m~fcڌ{Qȃҋ|أJ/b1F=AiiE>vQ߱Yr 4;o(ҋ|ج{Qj9rQϝiE>vVf=ǵ9M?(?;o+3bD_m~qϝ1G"SON/B8 )ߡG_m~fbQȃӋ|УN/B1F(Aii>vQߡYr 4;o(Ӌ|ЬQ9rqϝi>vVf(9M?8 ?;o+3bD_k~Qϥ1G" QϥiE>Vf(,iiE>QZߡYr ZߡG_k~fbQȂƟ_k~Qϥ1G" QϥiE>Vf(,iiE>QZߡYr k-((qK,Gh'8($&Ђ5k5PxBcmj.CXXJRCDF}:+իGeBK8var}2>LӴKHڭ7( r23w[ZC$YWlr(+n;o>?Mp |'-Wkw;09},F%Q2/ ywUR)ΑZ54uK[hW~m2[=>{ |_ hmµ>* kOڬH,n+}~xG}O"*'4<`T|Z3hjvI?Z_? S==aOES==aOES==_OEI?Z__?}G{{g ğ??W~'G ~ 'N3?K¯'iO"|AO_G]*ޗ$(_OE/88""?U$/IQ ğ??_q?qEEvI?z_?Kk<<W~'G*ޗ$(Q)yy¯'O"U$/IQ>S(+_OEI?z__?}GQWg ğ??W~'G ~ 'L}5'wpKy(KwkI9f aW'ǁV>ɜyvKDqQV oJ/W_g/e*]+1ɵ35jir@ jFxX2INUⲍOaA|_h,w3$L[^23qf(cΒ-m6;2<"5\Jxl܇e|׌2$ӯP;+ĵFT}'倔0 (Q@Q@Q@Q@Q@Q@Q@Q@K[.7yh[p3\_:?5uŌ:I8A1v׽|s c ?¯366Zu;** _q'*SodW!4q[h QX>n^_ǎ|. DmnDjWy}l)NV]]<-X_h؛ŊzY֮M+f5zqOt>a-Үn|~v=sp4[-r5mS6sW.{؎i-̺-[(7dqXtzW:ig.IZltrCGFOr>/,03?+-#WJd('d&o$я!0i❀u*f/GKlRZd1e–`3q_?d[1k &>s=`B4vkľEwg5/ukOmnkc.S<$ADC>ʛS]?KUsϋQubIE'(褢 6__ ?SuK<0c͖8{~ȧ7|(CH>e|pfʏCdt_o?KWuD^{+_խ~!hXTט&{2n[+E DIYg?pY#%DWGYXd0.2:?l?°#i5{jW4iFԱ hTr,HC+, Ѓ?bV"-kV.}~W=υ_+N _Yv='VtG6ݞ19~g?pRju\^giilp3[II%߱^d(+:8 U{W0Lb9!^(,g V/ L@&x :V8ugx4\*wXw:oSGqʸ/k[x5)(cF00 zWULTp26::( C?ξurk,>םuW.ѭnKc*l3c*Obt$t'c4&Cq^/'|?c>s^ՙpz59ۃQ>#)75*T-J#5#,}ꪚM}KZU^?A를Jr>t'-){.*jq_!u=8ȻnC*ы^_; A^`MERHrMGXgE7x&|wJ._)(]CؿRQ_q?G'3־ȧ7|(sZmޡ5@ 㿹?)_?J:vB9o$$Q}3[}r(*XsV6jlV5''p8ƪ׫zo}[PW{NpAz#_F{UqxAPk2m=ؤwvUl oʲ B6fdegbOA ?? FFK<:Ė&Dj#]?R (j2}I}M_.mf>\\ѓxJTŪR6Oߘ2x7*%V_+Q U Տ?'? _mjHmp ($m5i 5 ֻ) i]Hq xAg3ak/44?u<6VHtUЊMWi"զm"5±UV&7 ʩ32f9fYR7&ֺ"}LңX|v#"y5p-I(%1 r3nsx)v6ݟ;޸զsI!%7)Tշ hˆkچywk!*r>ZZڅ"ZuXRPʷDF~СNߺ^$׼񦑧Kkp1V3O aJ koZMOo@a x'O7#XdbI<1qcniu`00*sӕߪ1mB^ٚ&H_ dsqI>}/G~z֗5'd{J u~MyҲGQԥO빣]Xtn"ےp}^koxأ6@?CWjY3}f h?P#&+"JGZQU$e:+>} NI(пn9JG>RU>Q3-9{*e@W|6?ѿuVhZ#>WJOMj\'Fp*φ m#wm(??W=OE/]CؿRQG(褢 )|Og|(NoQk]O>ȧ7|(s>+cz[y'1޾K8M⪥GT*jM&L@Ҿ觮ĿtkdO>WيnʕWO V3Mc쾮8Tqii-7E+J,B+ Ӝ~5wOɯV2%B7I;4 _Z?E/]bRe]g+p)Ƽl]S&[_+?~+?~xXcg}N(Ǖp? yy\Wf$e9v⹸Biءsp>3|](Fnjjv2fvL3S7k'֨x)[am.['1SGN8oM~G~[Vq0Q&yv_ ?c)?%UASVφԥ EBS9Jik# [ :pZh袊Ox%}{urkqcV,5~_g}O3 NnٟWъfC'#mC&#y}汛f#Bc*ǯ]|1O5"TGzWZ?8XJM@S__s$}O*~a]?O_Kgn'=|>f= 0}Mo^zĸS-+XXAhB*+_S#0EqȫTr*^uyhϪ"zN¡a_1;"Uz?C޼'Ã*ы^^wgqHz~{\'FS:?5GqC>;/g6OiO,?떯___(褢QubIE}ZQ"_ Aq (X ?Stϊ_W=NN3kO@5Mkg#N.2p=+s#EyzEzƿͤqi#V=Hڰ?+?V |떮 f"tM%*n} _7@j$Pf8q޺n2%Ќn4 YtЅu~.{Sp:ZUb܌s4(N*Nw O| ?0o:+q^_qŎӮepC/a75i,8V8bFI8 c^C]¿O +`>0Ȱ0shϊ+:?5}?{#^mLZG;*/ڵCKnlZW"ojpf=)ǁjlǧ5r{b*JxRjԇNSןZ1H~?xԊkDK RjU>XN Su?m\ŹWvaa^zŸQqsX;(OCp`F*J3Ubv!EIG-9o(Uh5kGҸ+KF}.^ N [|8? k+|8?? WWgg!Wurk/w_j_:?5{ש|S$/͔ԕ ͅdMSWoK.ZoG>.E?݋E%oS $sK~=k?sҎSEӵ=FݧMڏGooG'~"6ϵoϨ+xUGFQa`XNrO@y6Sdӓϯx5J5 0)QS.z KT[ pvpɯ gdy=#[Hv'$ nr܌-\4gcҚYss ,}p´_u%58/u]'ϭfO/ƾfiF7=$UFimAi hKByMUi$Y#v30>ÊsNodu9<퐨Ӏ+|ŷV67{`0VIsӊiaq?Z?}]={o⢟Cؿ]l.[c| kş{~ĞԤ\- c[0xT(7x7йlao=J _o O/ GF \ҸX&rלѣMZ't{m\nۜ}qԼ=2,\2 @+YK#XmJs)XD| ųuKC:}$\0ɥ 5ib3ZЯk}dsDn^O0y)$ OOG?aڦ+{(TƽNKJU*5eMi\6zuQ^s\a^ ׆[? ĭeʧ55lz~oksM6mI)Smێ{Dž+HF*Vd`ٺYS/9kף-w 0qW#Z59?>^!Vq-B&T*6|8?? W׈xtS?o!u<,ޤ} Osϵt7Oszb7?6r{m:.w #/QubIE\7)?1_zQ"?^O"KmZI!!Lg?JL_QyBf|]o)?_WIJsn T}̍+;97V?G_1:Ӥ(6+ƀԶo+nJ\ַTeV.PH{ujic28]_9mXz CJKo/!v TkĿY*>[Lg<ywEWʜE{\'F6{#^+_okR+.i[y5iZFRHM21RHEpjxUI Xr*D"xXSgSVگ!^l[G1_(褢QubIE}K~=kGsҎ4$Hxed$E9iGKGگ<=f7 b_+T;Vp?_ƻoOѣD&lܿloS=T]u x@jzCAg ̓ω'Q±=H\ ;ISv pG^#紟ѯxyxzK;kaxyw+o_UqbQs<+O>)7?.{Sp: (uqC RHtgjA^Kit{kx̓H 5xU5F)r*hoF.?uo@6?Sxm0t?3ٟEju\_,^ݥ‹VV d3\")ghH I;zb[9og4V*3)D\ޯBubW)~3;Af%I'` Qo׽vxkG˸)S eP㨧ZЌ*5<]G qxTA ^_3E{{g<8dcq0;~"?Nx PujUF N7էs$ ti%Bn2ēk>,_ $* X*3Vۛ{LdeVaʒ:޼/ox:^ڲ\g ݙ+ߡ_}O7ƾs=׀yƦ4XG5|BT]{_? mݎdmndeC">Pƀ#*naF8W''-s5ӿ^C#. Α1V5/M3W o 8*j= N;7)?>ye-c=hv#?M7Ed׵<+Jnz_+gz\K"6R#;T9L?]w? ? W ! S^.xwF\_i7Wc( ׈@0? \'FJeUhzY{/1Vt=j'^mHYp‘ȦBƹ95Q%54k٠d2~gM+BcgҽZLތLہɬkNqɪ(סNGEqVPT** rPdъ MڹjKC֣-ȦRagJw*}'!{UxTW~Cf.^A?1_ߥzu#^EOz,ۛ_zI>'эZus9_h\5/S0 JK>k%Pv/QwJ._)(c#__ ?Su!Ӽ9l(\y'\d|uUЖ[3W: [_G;jiuMw9!#V\I W4/"JF>;}"W;[H&{K#ENޡuk5嶌пa/ WEOL%׋mZ’ ֍q~h޷ךG_`I@Һ7#\ΎFGɮU+'Wtb7=ɮu&A"=+iQIfFFpH#iߟי_uoj 4W<\&{}{nA!` 8*Λp'(8 ~%Ya?#+X;#.hw5?)MCZmx4WIx#AoM%q|idn 2gW_2#=r?k뢖N W:AJ7}/Ħ.q0 e .p:Z{x́m2|~u?mT?IMF|5#& Sj)l/–/qnѯ翵t 'gs:(+:?5q \'FR1DۃvRb؏N}*}j} 2G}鱑HW7.TϽ@b*cͪZp]TH 8Sz-Q֩I^6mN%'wQm9{SR=D*^&8'-JiTSH>{*m+!{5xހ?ҿ/ WW k^A?׶:?5~{z s|ϡ>`ӈ5n|ż?kq$Og|wJ._)(]CؿRQ_m?G,'3־ȧ7|(K> F]0 by#)A܏C޳~ȧ7|(뺯=N/bx3޿-?rYYj_ L&߳i]6eauf. 3jtӤ`wg ?2 ?\'W]>ߘ 0Hsr Blh++9⫭m,mmVO3ȅc33}968y>y_XTgbE 3o+)⫺UV1xjmݢ[iK0!\lj> 5vL gJ֢)+#;?٫ֶgh,>#)Pp^5dH_%tWgV#濋QubIEl7)?8?WE9iG]p ?SuouORMO:u٧o*9r$W0D~OS sxBn$P 0d|y ~u kp]! eh#la(1F6޵fntVxHHՙGFq1boDom7<k6/w)]nFlJ_^"Jt#]o"԰PavKysQ]gl&6a,$*=~Y7ۓoQfIEc'vxMNy)"MR$'zQMb\GQL.k"cQ5oTbW~ eDW27P8 T9R}&+Lb#_qb~AQ+kf5]+:?5_U_:?5?ނû\ [wý ,>J꫒`žvo%uOy5]J(%Pv/W῁O1/)_?J:ޗFbofuHm`fZy2c}Wq rуE9iG]m#q+HD2H<z*\ׅ LSK[ZAioyqyQۯu9glU%>Pნ0:mn tˎ;y!RSi gnP P_d T77 u(oՑܱWd^[5 Bo! 4vb7Kg*I/Lϑ|O:vSӿN%%p,j&,z;U KVd>FJmY J]&&B J,_J_KsxڅΤ٦vج^p{f.HAH.[samdPiZ,@@jQ鶳\ϟ..[hw`axbx5IuY?YF[tWXݫl6'KmKȤw*+sG ]")-R) GB( J)I@Һ7#Yq)IbӵY#e NCЃE+05謿j'VLd9LoAv>Y+"?E2oOL;Ge$B(RsslBt(w*yg4Qd1Aj(a#؏Εբ Wl//n`U`8fVmƳ-@g:ʛP_d ,Ԣ^&, &9=z7Vwijm2vp=ҰV_B*{-@O-8R-5f(OxwOx諊vqLC$ A4h.RXIIO͠$O(њmh\L4Hj iQRLJ"SsE74RQRL1^^KŦ_Z&Kw+:?5M?"A?|ZN0kX);h9g$ƸMNq(j4H]CؿRQG(褢 :__ ?Su\͠YKW/E9iG]ٕQmYi,Y ۩?|gS'AL~u_𦛔8rK[0blZ5c9$8zV_Ij6K']"uе2YjX]Y1?˼v);:*_i꿯VV35_AkJ8Pf$e9vӂXi{ EnڠgD^fg\CYZzVwWpTSD"XDi:LVo"#;rv? ZzIB<-1h\: Ԟ3OEӅyOې9jS\mUKϻOZz,򑜎}Ɯ#+C[VJ+욥J],J=ZzEmwĭGFq}GgAi]@[c',9ܦ0t7o_GB7CTJ((B0](^K{ږLurk&<<99ֵsԷ?-/?Ƹ+/\Mڝo?5G_!ӏ;KRkCq7jvp t7FV46OѪZ_mo -[Qʳw[ݷlzk˩I_f} ַ2Nު旛'=5kwZt"H LX0{0<:7S9D."m!5-pȅb$kB?Ơ)tE#EVl݃pp{{իn{{JN3+E1qY6;b/}nmg!h׳?RY>]j_~a+$bI1*$.T ۡjwzޛCk{mW[\h2*̈wf'یm\.#3F$}z2AilUl_3֩9h)jGlLwSI\LqI)KK*F `@lG'%(Xk8#2!s ] ` &jRk|vN8'n}( FdB1UQK`T?m#b a>u.E?݋E%8oS $yZQ"Q;wG̪6n=\u?3h?o򹚾6׆ G}H-Ԛj[2HϏ`T󮊘?듕\{ۋ+x DMq %$.\Gf??EG361M KobiK~bFO)izeJ 5IȽI f??Ef??EtU tok+wLv秥fٷmJXePI3洵_AkJ8Q̿xyi_"sG Bড়W/h9< lVu?ӔRBG? RnNΫK o_f??EYwYs5`ov $c!Yvޡqx1iUAub@8a] Sfx[c:?f??E[PHX lnVݙpmy\AmY[3F 1Som 򑶻++Ɗ IH,Kab8l $gTӧ4;,oPĂTS8ky>n/m:8DV"Nɽе]}F8E[E|R<$ 2~B0E]}"Rٽ8-縷mi,M+o.ȇ$Fݼm'8 8J\ì_3֩굆>n$ݻ{n:Y'2VW$XHIRqx9HUHբg#}1jVlV6-_>G'ap"O1rh8;@1sTҮl-Āy]݀Q0eE;96JDzm̳<20`<x{RԧR55tۇVFU3~+ '?))|_7w6q{_+Z'ftW?8J\ì_3֩굆>n$ݻ{n:Y'2_(褢QubIE}t'3־ȧ7|(6нӫ4Xl,0Oq_ ?SuW=N/bYao'ڏuc*E5&:Y̼j~c*h,Y7dlqF B `B~u h,Yaol>Saa.nсTupy1R۲Zm $ =*:)]?l>e`q_ze6X,:-mі%NI$&7?«QO%`}*+()B[;g? Z)]?l>#~96k 6W(OHۺhu?[aoV&O zc8=pB۶8 )9#@J( ~}H;Cz}*X(s\䫭urk_*/2QEzÅQ@Q@^ o[CjUtU|Gr*?qTb\gƮD۾q +>MQ-sgk-G$2E#2lIJf ’@ q?PuY5`68Qs*;Fvl*Ff%]z/&Y^;TgMk&DvIcc_3eѥ3Or+/hT9f#Gu? ^jKZ,[bc2@m $Z4aOw5ꑽЌ7 .qkT=c?;:ݣiֶ&64,E$ "+0ji%{;v4AJ$S~ ,9b]Č_ug-[Oeq!26s? \C|ZL.E@I?g c+K iW[ VkyNW@ {]˖?Xk7JlܾNn;XV mv!qBc#WG"Hil6LI//J|(==f8౱[hIʨ$w$=ӹoe̋m(;$0:\9RjvO$.>8Qs*;Fvl*FfS%Ѽ t.RtX#uJ!rFHc0'SQ엺pkZn-۬w$ brHݱ3ĽFV-,31qkFDg'ˌ8-z7M{syi}u5IS[Pºa(6tlᨮ5>PԦZI i¬:23@rw> 6{'Tna,Qp s$ W/|Z,_3֩Lq]CؿRQG(褢 :__ ?SuL~P@{zȧ7|(뽴?tV[3}uῃ'7 >M¬Q\66+dV(W'7 >M¬VDQGBxVwy0>nO3ڀ/dYZقv̈ nAݒr6ẕMpg[G9qH?{Ls ~My07 ncV^ Xt8s~,<s%Qʩ eD),zg7?鼿QOo/;[8#FʷC,[zC瓆'>{U"Rg 9Cw=Py_w*ny9ExC]O~,Z]%O,`=rN}sր&'7 >M«Zn2baq Qǧ^[1LT͏rsӧ<hdTNu+݂M¬QEC?K d*OU?tc #*,,PDBE2yq dzZ_n&I์HHWhIB*9 [k7 K]2(6 uB8>e$#/.D׶XAgmv/fyUyEǙ%73 >X —*qo1{䓁Am6S>縙<kg!s@:fc$sIEcym^7:F*JJÎ  lQT%ԼzKY<3,Z[xKfR\>w^ mdf=XI?g'tLjj_=^4Q,gb@Sv˷wuM&䳎IdyD.p0Hŋ,m pwBU8wV* ?r5zJf`ճMfht`hQiHBfUU,_XlCՓbI@+?nI?g#}aZG_hR0iSENe"|HbT8%z #Y7uͼG7=f72miUy=nj;Фtb8zU\?j]CؿRQG(褢 6__ ?Su* 6BJȧ7}(렗U&.m4'6Vvn**98 :_/RQtF774m}i~+'6>i7_cSL\?Qox(G?dbcS=M?De,p瓓G?dbxwM(5;~+L-ddy rМr)kU=XB(%8(䟶KЌY2A(Yr88e#>Q-YAF 'k+|]7[{^[VV-ʆ6j_/ c/BA΍Ѥ-8#z@Cg9;zywLuzPd.A'Ny)~&h(䟗ރ*HRxrj<], cN9ze,R1GF*e#u )j/k{[=r1&ok߶ >4ms'M?GEIo#Hf$衰 )IKwRN2o^7t<*09l'$6(/ c/Gt*΍ +?Gt(Q@2=V1Q? e{)M:4*-gxг#V#RH`C(9E? ey~(n_AΊm%q{< 39-m\ICi^4K[O-mYc u<tM? ey~(nBDM>GKx$m+8EX(Ž|sui,׶%h2x<0CGt(Q@2IZߊEX&ow$+m&+e,d !.q m$Cq5Ĭvh, 6x7 elO)Ps/2ŋH&*rVk;g_1F?GbTTRKT5="W~_:'H]k.CF~ޛ?c/g_1Kh9oIbkL1.E?݋E%|]J+H?WE9iG[}* ʟa|(NoQ;Q#ivDf*#yv+ו̗ZD[:wn٣UX DU2h# h5!,2jڂX,1yi$ʒ8c|>FbnRicnۍ;Ps {&YFG]Xe8,F%-Agv'72U9GR$܌B5 SmMomıO Bb12VRJJp#`3Q>jdYSYԢʓ. 1RS$ǔc PƟ/oɦ̑/ahH%Dz*9浙SdYvopKNeipzէ!&@\,dsArKZՏD?V= EO ((((7ҪI -j}VO/ tVP?u +X|//|CFz砣ANbz7?C=lz ..oSd?T ͯhw${0AhX<]:lXKXFM|1Q E{kuj1ojzs},nYYd(s8Vp2F/ 8kdE:İ}'r6d|ʧ7vmRQדxX 9[Aܭa!FKxuv3Ä7 xC2N-5D[[[m``TG|joyR8'+hI{AkA(gp2$;۷# -k}rq-lSʆi7>l3X][W=Ӯ#'OE.C)s9cpm1]lBG&.vI)iK{sqoQݸ{{fVv IbD%W(ˍ6I5{_ W\CmǗO2ȲTYX?p% [),n.eU)@FR^*8b_,Ge&,X!VI]_w" Gݕ&ΐmWW7&Du_:;<*e˜sЬԭR VE)IUdT"4U>)nrHQp]xGqyIʙpN6a9QxnZ9_^Zoh m=73AC >?\L]a U"Y:mRIm"EE l܊0J݉XQ,V(Cp_rf5̊13|x1mϤ6-dK*R!]^>#4'|KFV$#FDH(2-Yǩ[7Zs=|دm|4п _M|۟mOG_x{'bhc/?_&{I۟{o/}gLJ4qEQQPUQtvq=]dkm>ҴE; n I 7U4q=>s=tU_ }e _M{OkT߶w/_п _Mx{'bk߶w~q=/_п _MxxI7?:>s=#?m|EDUE@UFږ8mΗwO?_Y>Ƣ7mΗ?:_ؐ2_=nt}{G$?`̿}E|q۟/?缟ؐR_5?缟7?:?!?eθ4[{NW9i%?m{}SY,Vac/?G#I7?:Owq_#?F<=B141q=>s=#?l_#/F<=B141~s=?:??`_п _Mx{'bk߶wmΏ|=>c/?ZP Q c hT{s=?:O'Oz톱+v^GؓE{8F%+^././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/docs/source/traitsui_user_manual/images/custom_enum_editor.jpg0000644000175100001730000001244700000000000030537 0ustar00runnerdocker00000000000000JFIF``C    $.' ",#(7),01444'9=82<.342C  2!!22222222222222222222222222222222222222222222222222F)" }!1AQa"q2#BR$3br %&'()*456789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz w!1AQaq"2B #3Rbr $4%&'()*56789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz ?Aloou;ޱwKQ@T(T :TZD\K3%qRLldu5WR٪Ccn7ep1 OZo种j+>rr~o种j7ֵ]esOQOzx֫p6< OZuQ9?7ֵG?oZ=r~o种j7ֵ]es'Ozx֨|'=¤6O֪jVy!;qԒ)^2bmcPTMB+7t7Ҵ5L{Їpwc'|zΛ֊kkF'Ӹ>[^ͫ$p ̪as֎VœΗJԭ*Q,($NJ?t$yfhXB^@J"b8]FmB{}9Zy:JUv2;M.dk2 Űʮ|ܶ{TY\O4pk\щ`g0vɏC+LWDFGטޓ]6$-B t3*.TH7^+ӎ2vI(3;Uibo.I\|f[vK fgxS{TΖz[۴pAR}35?fWM]L_9ezJOΕ*] ^a02tR*槩h-y}' “f =I&-VPk7FG:4w6r],4ʈz?_I[`5_G4d vr[׶=+#Cj_F1>B"bUߠ'qP[K|ƖbhJP+F:cHG^R^ 1 bL21`0`Eg$sw ^DDV8TO$+ny>="qsk9c(`!G\zԕ.%Р{OA gx7)<)sZ }k K73}v9T[IfmeśrpB9ִՓ-!Pw8IWR=k>i6Mu)#k&J;/V۵zb'Kz D"Xq\ͷGZP?wm 'PH ph\hۮu9[ehƠd^`T.|QZY\x[`,cAzsTuff-Au`zÚ#_Cy@Jϖql5)"L2R A"2B#PʽdjaY^GsZO w4㸙{OU'}(IZip@K)-#ZIlq3:E{{A1m9eF0G{A?&dmOQy N}GQJ@d_-|jNI\FZm  o,GFьSp84"sE>RnREӮn~5}x}+ uJ1}k,B~h-<ر}JQNkD}™`2KVLZ>pjmc/krH\]=:UkivdƥYU@ 55O'=i+W &L] qx96:J٠(c&;W jc{[`˒XdE_j`;TY jzlBhgq#ѹ8#8CjH iT9 GXמ'f"宖lyam]=ԺvSٴЂ$l3Nv(qǧj7;w&l!1yaY" #goNZfXk->r*OD˸dp@4j9'QEƩiV:[ ǦGQjka4!;x(DFJ1֛E }M.nF4PEP0WF/ NO#zb;qL'%Uj*E5|A[}WR٪Ccn߃sa]KKf]S[tzecV^_ڮ-8Fql4ttV&E?6 /V6(ݢ6 /Q ?g>/*EamO_o Ϣ|_UvjwHy!e\u+7o Ϣ|_UxS}⩫q=Q]q,y yd }1\%υ#ۗVHmќ ̫ۓO _ѷ9u(݆֊HS*_鏽ZJDgBcADί]V&B1ƴЯti fEy6 ҏ3_6q݇@1]o&E?6 /Vwe$wκ%MoQ]Iyʕyǘy5- ?g>/*M)C>T;7h/M)C>TmO_!Xy?*1;~XtѧuHO*7_:UNj|w Aq=X}=UJ=˂dnO8Oa[Fi%u3qw2|_-CuX:5ķ\XO\il#$Vn;Jg~譯M)_‡f};ַ@"g4wˌ̭!c`*8?hdk?(0y@v961 >TmO_V&E?6 /R+ o Ϣ|_UxS}⨰V&E?6 /Q`7h/M)C>TmO_nX_xS}Ÿ3EݬWF/ NO#6 /U9{υMJ}Mཞ2jN]3(` ~c 5RTUP 5RTU_(KQE_(KQ 5RTUP 5RTU_(KQE_(KQ 5RTUP 5RTU_(KQE_(KQ 5RTUP 5RTU_(KQE_(KQ 5RTUP 5RTU_(KQE_(KQ 5RTUP 5RTU_(KQE_(KQ 5RTUP 5RTU_(KQE_(KQ 5RTUP 5RTU_(KQE_(KQ 5RTUP 5RTU_(KQEtO oj<3nONQ@././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/docs/source/traitsui_user_manual/images/default_text_editor.png0000644000175100001730000000117000000000000030664 0ustar00runnerdocker00000000000000PNG  IHDRaJȆ?IDAThYMP$8B p !@ *$J&lﭟ-pz|yp5F0Ϗ8y|~m/;u A~i+RhXhmETu~؟|yќ\Grz3|SV8?IENDB`././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/docs/source/traitsui_user_manual/images/delete_item_icon.png0000644000175100001730000000166700000000000030131 0ustar00runnerdocker00000000000000PNG  IHDR&u2~IDAT8T_hE{ۤkUisBm,y0}bZ }4T_Bxʞ;Wv}hů~kj5b U93 pdʊaIR\WE8?Bf$1xqC<_yATyk_::4Cley \ kW7 S0|#c ? e &]J/ CO*oƵOݤ-=B%=`}?wwWN]~+IENDB`././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/docs/source/traitsui_user_manual/images/font_dialog_windows.jpg0000644000175100001730000006766000000000000030701 0ustar00runnerdocker00000000000000JFIF``C    $.' ",#(7),01444'9=82<.342C  2!!22222222222222222222222222222222222222222222222222_" }!1AQa"q2#BR$3br %&'()*456789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz w!1AQaq"2B #3Rbr $4%&'()*56789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz ?S<;X ?ѭEJ'' H?V&8+WF\Xh, * ˀAW (QK}V3*ue)cz[ߡL:C\Ыwg͕&EQH$)pqRu,m.E*?ppx rjJdT(?;o* V/XӼЗFR^ QT[CqR̙AiiE>vQ߱Y\\_m~Qϝj1G"SOJ/b(ǵ)߱G_m~fcڌ{Qȃҋ|أJ/b1F=AiiE>vQ߱Yr 4;o(Ӌ|Ь{Q9rqϝi>vVf(9M?8 ?;o+3bD_m~qϝ1G"SON/B8 )ߡG_m~fbQȃӋ|УN/B1F(Aii>vQߡYr 4ΧߡIZߡY,iiE>QZߡYr ZߡG_k~fbQȂƟ_k~Qϥ1G" QϥiE>Vf(,iiE>QZߡYr ZߡG_k~fbQȂKZͬ8Ǚ,|NҴBp׺(#?A 8?k[}ac [ *+JYn!;k̯Vj5ks ros}..©eD3N/#scj\D*dzEmkI f^hQG̣ 7k5Ï*a]F^{?#o ȼ5?UKK:D(G)hggd!R+.m^/#lSsI%~jH-@ZF._Yqђ-MO`gt]t(xO"hCK ;$tSW_)=heyUmS==aOES(+_OEI?z__?}GQWg ğ??W~'G ~ 'N3ȣȮ?K¯'O"|AOk×N3}k+_EᲖQx֒"1Fr@®OZ|=]9G+"ttZ2>_>7 Q)_}T+B)8jW*cknfk'\?Y jd1e:P* odXfIs™dgZ*Pa%V1z[w>lvex,Dj6uMw4$}E-3QۏdI^xHL{h彶/ 1wm: xz_c#EX%߇, ﷴH0paǡB] Y(EQk}߹⺜af_ƊEd.'7D;0@=kR掩zi)Ksܾȧ7}(뺮?Ҏ_OxOYʸhu u?jz15hbV6~quingזQJ80x#xOS0Z X>xEX.۰J4.]źp fOEch֫{Eyw̥|X1 zQ٤EP0((((((}iڦkza=X*ڬ1M" J!ːNBăS֊$6GksjP*F{#Fޒ6QEaGVlu9KPMO?rAT[ K0: +|Z] :im-x,7UĠ6C.ɨ_gw2)"`y?{8烊,YEs/^=yvc ,,dz !(z|$:zMX(aEPEPEPEqVzն]jڦ:t7vmb+2*ȈI&Ps[ҿ񍮓u{Gkq$;7|M đttV6K?ۙcv㙖HXBXp`0A!+fQ@Š((((((>.E?݋E%7)?<|Og|'NoQ[Yj-+k d $<ȧ7|(볼W|V[2WT^gVڄ} Pip-΂ cd$ *ˡh eZij6f{sL ٮ$'(K\[[-od#"?W$g/;w&3XPҼ?%~$ioSΑ&ԗI݈t$5TXYf92`Aスa([vmWk u=CPMfATAym6 !#zw]rU?፩s29;4i_񮆊ϒ%ݜ[??CE ӰoG[h!vsvmWhӰo] rD.{NvmWkHi_Nt4Q9;4i_񮆊9"f:2F 29V*[hkir]5\4,'ww\ cZ:o"ljVB:k=E\ $a P̓JңmvK%WY%V9QPU]ϔݖ-IRO2o3 :aM,c?ͻiˢ&m\*cpp Q [tF >Uom5$n;WjjP]Nssw4Č1'xmQ&^U@Ve5,}:cuuu=wIߙI#bbI"RŹP^"X.~-0rFU#;|9iĚ%㶵*X8K7H*N~BXF(Avsmq[© <; YO_w>8`I'kNt4QȂ翴?a?ƺ(][??CE ӰoG[h!vsY;Kvf8J՚XFEQ8let{knٮ{9b3\Z@rOBylG<]-EtX&PYvRVɦtwV郴Ӯ|M5rc3li $N9Q) ieLwx;pp *Ă5Nt4RC9;4i_񮆊9"fw֓H# U ҟ+#ėxcUK#`Ը{.m[zx~ue\ZG$M9,d)$Ԓj!5C֎9޿Q#?H/ؿ-yG3;vyWwɍ(T:o1?b}:CTC:uI4B,S%b6p˴rTg,#?H=??4~? b}:f_ΨX$?MUwm4%`0-F8ePA9QvR?ֶ_J+7u&f/,p{@IiVVm'us濋QubIE7)?<|Og|(NoQgy/Fq ?Su>oWylߝq5uaCՅ-§&4i|̅0/&GB"Y|=o}&`("O `O 3]05M$?MF>"BpF~.0CwNj(yu0T$_̗˧B({F+,@U;Aģ\1޼ WI{&X+A]'婇9sr5*ӊB WI{&X+A]'̿oOTW_ ?s +=G++{z}B WI{&X+A]'>__zWO4_ ?sJޟsШ= WI{&WW0Ey,?MXe>}-ę؞MP:I᪥wjguEss!M D .v|*'*#zԬloltc0D%בj ?txŴaL__zWO4_ ?sJޟsШ= WI{&WW0Ey,?MWO4}R򿹇*+`th=>a?Vb/OUE)‰Pdu"z AJҜE)O p@ѫ$7 mha&v,s8I{O3] I5m$upx;wogzK?imsˍU/_F<}j}?ɍއ=]oc3[_+n#?*n7f/?E=??;-CZf;zmЅs_羋5^4<7HC|yC?8I=NmYCD}Z֪uiendKXb0X*VknmEZ)5]J(%Pv/WaO1K~=kGsҎ;q5kQ"\_Z|~V>xmVBX*sِXmP0= x5(_-cR|p6{ }O^xk>/[]jjy"]rst$d HY99>pCA [i7#L;jQm*d ?l,˄FP][CήNYGR;s\y~bƛ ?9ozkۥZe=rpMz6 ?jºEԺ\w:OVmPH 0#;q͏XS:ДNK;]N, q1)$hrH,*/LB҅.s}voXq\~`r?mxEcVIk2L~@T㏼jdž=5/+Īd9Ul62:C2BX/caި8G&1-~I^t|9gx3EvL/4j削YAڧlޅy;}>YGQŬk*UP ̠@= eZ2ķ@"ہnך:@x3+2jn vdS+T(Sޣُ*=E{*~Et?wUT+T(SޣJpSQ z`[+NkI(Ԍ=s߯09߆m#n?^DdҎȬDFWgVw[K EfiwsK?$/z6UдHƙO΢-&5.#mQN27HW GgR݂ ppTTR{t9K-N?zO"U:??ҡLNQ3[õF~i]\O%H9q n3d0$6/#M68m+(c8ܭ |$!27rW;oF?g+o3L×2eDnS>D>VlQ>_Nxڎa|m㘨] ȡ>EkV3VH3濋QubIE7)?<|Og|(NoQgy/Fq ?Su`ѫ_/O>č\Zۛ,5{ "[.Kmndh^B,Q iaQ v"M(}nƯ&Ӎ6,+&GZAq&{srm R&̹6!YXjMN_-WRL]{-#!B\3n0nJPzeψ4u[X-nq_ YQ+%H:UoxKMoI_}Ķy>eXa"rrx^ѼQmos. Ȋ"iw1'h'$ Żƶ,5p6-VЛ!xOΡv7}/ZЍWxIN"vW.9p|xi}OV>vuK5>|aReA^58H8&ŅY\qwHdNX[* 8z&s\][swy}=IJќ hmV/X[etzg]*^S'{*+aJC1̐;"#ޟ+r6. Ayy~]j[de2 oheXWqK_7T*0T_—^/.nl WROJڠ(((( cAK?Vl&}ݕm"ϧLF@Ge\Vs\It.>fB0L~p?UBi) kQK=_yZvZڅgEΒIm"5~y0} ѝYA!}s]MsǷ-.srhSo$KiZh9ɊFmL_+ZGФwMswf/4dQ.w=v"PAN~ea[#AҷHmo|<~V׌4ҷMq,gY؀dCϡU[I[6uHDWA5R3x;I7(BqY РKN{]۴qqAk ۄ'90CY}LvI-bp TУ$pR9[kZ<{uiwi)ei XML8gI`s+Rhtzǂۋ[{_[iKܼ+ -4Rohˎ&g* -'Y}^Omus+"D@lA:75E>f}:{fD" Vi\pSxE]6q{fJYsǎx]/]F eZȝĬȎ![>n[3> xR{WG2Z @qsڣXV:Z\9]%̬Zgr ,[xT]=#si]aj&$G<齺7˻p7ѭ^Hc6v!0l"%X ;Wr jAsEsy$̑Gf%@Yc("^!t;[}_KoFfm<Ď. Lf.yiw.I4JGZdt A$Y!DaNzoy$.(,G=H,5ۉP+aV$)#{>Zw|pkvWi>N$KvKiP'@Ǖ2=dko  i7Mt#*vH#,8zo}֩#Ȉ]GvFdU#Չu]:K]R}B-P.RQGRR ?:zo#ޣaΆ?]6YNQߌgo"Iy #?V:vJ]Ǜ+ 80䎽~]?ƼKծtY^=BաYK+ё#|`ڹYԮ.-CHhȁ<$Gn1Tk=/L[.w.s& HKDZVA$Upg:\Ij{*{s8|ca3:wv\+fzvwj-ⷲRgU+%=+^EgwmsSx^У#D.an%@AU0H4e5hw ,Y([Y,Vp$_99̎Ta^M׶w-fff {L"] `kѴKiL/ܧpK+1 r;o)4袊((?mѫQT?5j*Φ"VkkK i^`ZE}s"Iƹ?uUSHˢeڐAo3H݃sOyI*-F~jVB - BX;G$S5X+,y. bZ mwnZQ4RXjv\6%w0ɺ]~faJ`S8]Fo&êEKZ%ɾ+FD/?2rG܁V[xHoEtKĥdI cp缳hwDakuyQ˂dCo͗753NRGho%Y.sZȥZg(_#v@:Nè[Zr_-X2:-kfj2`20WEgGkO4A{8^t\UsJ [_vGշnE2ɓ$# }[}&si2\Q*%Bʰ8R ~MAz]褎vA3 Rk.-!s]ZoGtRQGRH -7AV?Zz?,??Vf ?=@([Q.(aEPEPEPEPEPn oZ?mѫQVu6A6miMDdgIDTx ;tjzƵukiG-[]JnvLȒe#+,I.I4SVI,n r@Ày*|tY~Uqm+vu2J۬k|R|gN>m^[QHei֏ӥ2o4Rv_iE|Y-5(((((.E?݋E%|]J+H%?WE9iG]|(NoQgy/F|c?SĘltdu+xH% B{p%oK0nJN )<r5ίe:87!$rEq: ""0[d)%#2 zM(4mKmv[߿6l {nR>+-ۿ?h*O_i.ifgXa"ky⻫,u Ì<]yssjmwWIrDhһwʥ @$|DrZqo T)<\F"yq.k gb.p8ۢk+M 1b>[=kxMҥy wٌpI#DN/pjv<ƊݳxYI:РQ?KL?ΓVmb)n/ 1#IJ{?#R_ψ:!bA{v, a^"4YEih\wR^*QEQEQEQEQEfը]cAgSd0w4mFGuBP,W%Kc#^'.YoGtmmq$p#:?v/f{*i6O.xY-G [jLmP~.<)u. K4 d#!_H (uQE!Z>ONORA nb@bI=W;7-+ ҐC+I8_8)s)ne]U6!ؒ$$هՅXRQEaEPEPEPEP>.E?݋E%XoS $yZQ"\_Z>ȧ7|(붞:=epˌWfN>+_L4i۾d2CsҴe#vJq4PAϮG^F/?'s|EW2]M'effx9MO7Ħuݳ˓̪ISuKu-ژ!"Ii2Clȭ*6pE%BؼF/?'s|ER]ʳ8 K×" nmBn&dO1NwJmgW\ao_\Mxi_dijMFg$a eWzĦ8:.d1yA;/"e,SKAt._B-6ݬ%y*{[&_,faPj6Ƒy$ZE}pZ}/lhEzk-le:p~|цcnćO4s~|e債+h|1(AB˞.-5lG qyA;/"-JN bw?_E˸ɵew5.mQ0?\t^U7\lS+,qH~l}1Mm5Yⅇ_/zXYLEG[1"!}vq*C74q1ddhQVtie`+"Š((((.E?݋E%|]J+H%?WE9iG]p ?SuW=N/bQY\Q[}yeJcwDvl㞝k4%tlVn5Tx6,gI|U2iTV0DdNЩ- 4;QHaEPEPEPEPEPEPEgo{T#uYU))xe9 @8Ъ\mq&E^;i!pŮ1&!o`nŊ(Š(((((((((((((.E?݋E%|]J+H%?WE9iG]|3铊>ȧ7|(뷗\ׅ EXt,N;5i>Jf ܚaH#۵$Q0+iV4^3YAIrvogVKeE@ ,~G8<%Lg}_WyK<>g}_WyKCΧMIE5jǩ"]Ȳ31f$t?#t*!QP#%Hu!dwr0ˇhYΗtH2,kXop$p륋6Kkk3-k#iqA yLAWml|olwn4+ܶdr#tqRy(fd,C#z` (NTF9 i'NXMb0X$(@ݒ 4+GRno"yK<>̡RƏ)}_?O/gh4(RƏ)}_?O/gh4(RƏ)}_?O/gh4(-9k15-1_COAEP0((((濋QubIE7)?<|Og|(NoQo//|(NoQo//OS %GkmP: lͯi[1# /+I(eT!l8<.e{$sk%Pv/QwJ._)(c#̗_zv1\Gv1_+T:ۛ&}$v'Ljl{E/yM׷88F.7f,K[e=dU&dQvOJTvu _q\"_cN)= 4y!f*m嶋/̀Fm+UUjePj7O,Z#IpϚ~Uz=o+m9+Cbtew T&](獋mյku[Eo.] rBNP;+֧yiYBȄfW.9& pņIj@(QEQEQE2?DD?#t*=YSVdj?묿)^ ( (f]%Dr P2€4C ( oB&Y(LQEQEQEQEQE_(褢QubIE}d'3־ȧ7|(뷗>ȧ7|(뷗\ׅ H#4)K,w4NP~jj |EemƫFmgQգʆ=.!4GwTF]d;YFXpI- S4-4綼k뛹"-8mT,@GWnmILmmVI09 TxONmͮ Z4 $01<> nm cO'2dF8.GcI-Ι`nlmL1v,hvJLJ-3~oGq('9IaC$l]H #ISp*Oi"Aqak0$[{NG$gt<+M#dsF;'ަ,4?Ik}:m+!@ EQEQEQEQEGTC?G?$P0G.,>ըܙ՝A%)\{u&O近 I?G(ڟ#&jETtPSz/=?Q@mO近 6_GEI?G(ڟ#&jETtPSz/=?Q@mO近 6_GEI?G)% ™E1_CO܏65rN=jJQEQEQEQEQE|wJ._)(]CؿRQ_u?G/)_?J:b)_?J:bW5|uCkrxXZc`si$ye|UB~v çeC&e8@8/x>#lcۚ5c ;%#ceU[#8C/14EL_?OfR̟:2x dpz24L_?GORCgOQ<(L_?GOSy14EL_?O<y14E>jPAuPEPEPEPEPEP>.E?݋E%XoS $yZQ"_?_+Q"_?_+s_ʞ^1 Կy\A5?w+Gf/ynwFqAax^f˻f@JV?rϏ(?Ux˿?G*ЙwSoOP\Akև{:G]zqogS:~i2t܌^Ún->K BS3' rG[@/.;:_=7>%6+--o "H_"8/*F֞Ieۡooi`7zK=*F-7̣nӽUQko?8?_]wR~qtko?84 ~qti J+/]y Ӹ׈#fm2˸B0Ќ$jsI6;B&Ef:D;ǰ^DAmxض6qQy=/9]M/e}~\?j!s$e #b   O^T&ta7~$[M|'~/u ޥ__͇2vI>#Yd>ett#R{Oo"#R{Oo")F>EF>E/g CyCyC{92E3ԿԿ9)F>EF>EȦL5/(5/(rd>gϽ7GϽ7Gs!Sb~tt_?^y>gOQE=)t_?GOPyDt_?@E=yD>gOQE=)t_?GOPyDt_?@E=xՏ@ ʳ$[|3|&Za,q̪#} W,5#bE< =#o IVM#ֵdIٙ>&Lk˨I<20DזּAגVwRb^>'B֟kaH\- p؄͐q EQ/-G "*1pdVdzOYYHZ+3 u ,eB{pQƖb3e֑dmCvgXQhь>(\FYO7YI4<Ws4 DBʹeL|nݭ y>H)f[0;0lL,& ߳,svrH0@@b04tbQErL$Ԧ@ (ܣ;gƎEQ/-G "*1pdU36Ƌ0/QT4?4,EQ?#ih?#ih GcL_cL_.U36Ə36Ƌ0/QT4?4,EQ?#ih?#ih lOkϮ|O{ ͧɧ0ՌmBZ9۝X[f̮ zCn$3ٗPT7#?!]t,M%.M=w9R8blqnec<+f1m:B(cP((((濋QubIE7)?<|Og|(NoQu\/wUOS Q^yQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQE|wJ._)(]CؿRQ_u?G/)__J:Q"W5|u+:(((((((((((((((((((((((((.E?݋E%|]J+H%?WE9iG]x)_Hkҥv,&ʫn ? W:}PSQ+|+ͯ_ /M6>#(k@_6 (/MS?=_6 (k@g0>#( m@ ?(??1G +|+ͯ_ ?1G`ŷ$R WaGF3h}bEy/oICŷ$Q_?XzmQ /_ɣa}GQ^? /_//oIC#4>O+|+ͯ_ ?1G`ͯ_ ?|+#4>O +?|[O E-'"g0>=~_6 (k@g0>#( m@ ?(mQ_?XgQ^? /_mQ_?X{mQ /_ɣa}GQ^? /_mQ_?X{mQ /_ɣa}GQ^? /_mQ_?X{mQ /_ɣa}GQ^? /_mQ_?X{mQ /_ɣa}GQ^A P)?|+#4>O`ͯ_ ?|+#4>O +|+ͯ_ ?1G`ͯ_ ?|+#4>O.E?݋E%_\jVv^WJ;48l././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/docs/source/traitsui_user_manual/images/html_code_editor.png0000644000175100001730000004774400000000000030153 0ustar00runnerdocker00000000000000PNG  IHDR IDATx mWQ}/$b0XraD)jT`@)gtuD`Q,?j,@̀(J_0"B^>k:s}~^ݫo{ɓ'_>wtd{y矺Vy= F@uGO{{yz\>vǍ}-Ͼ P},+.G?Bhl@;e{l,Q+*2dTt"[Y%jw|yF@ ^rƃݖhYڐfɀ\5'z* 4NF{{{/?X],t#ФM$:ÒdfTK@d;ǞyKN۪3w[tҳۭ굯x}nxlΒ+@IVW J@4_3y'SY}W] %Cy7ݝyM7}L?~ mtyx/hֽܱ+Z"+/~б]wM{큯/IDQMx앾:|hY`Ƚ~a>-|}Nُ~XgKմL4K<.{-@V{>t{?r; OҋNɟ "t{ߍ:ϰ,_vR7{y}Ww?kq:_=R@ %#M+XNѴUM |ԥU??[>vF$}7_?)O,i@`@IƯX_s=րI\e7wH䩻瓧?ؒË_?{k?JjO>:uUigXG?H:_?O_yOOOwOi}gYq}_ !"c *`4CkAq6i0]y5Wd8#j%o Jg?;+ kOzOnݗ=/7կ?Gb6aPϺ}px/Ӷ/`"937uWM==(id bv@ׂ۪ptiw|aȃw3ҜMFw~x嫾ǿ>qK3-ip ЛcEd%SP^ҿ% `3 []7O^N?q;ݯ~]M/"J|I le%AE;+PXcVC$_)>@'5Ds _4[b08u ['J2Ĩ4`Mb@`2wwfg홅<~;sy{ԸMg/k\G~w?3|>Nk߲z \b˟m5|RVm9:EVkhH@@T E|A K<7=1!bκ|2R g}ܳN\׊r}ӷ-яeu<\Zɡ׬O<{|a.h[B=w=:|Dng#׉Hf7W%`?zoLdjڛXh5s&1d"3YJnU$ފ[q<L@M~~]{A삨l~κ{g>Mkt?ɺϒ'yS>+!Xzp{xɔ/%{-'.|/s?_Cu+s}: 8Oe3E|3qR4OPC$`wqAKSeZV>hA57?cؐq=P'LO@?$maj%/O{ť硊λ (ǾOF`P>} !8a?\)\! 0n>ug>7|#9ȍm/=k ϩ'B?\ ww>O'ײwϽ7)rɫ!j%Y7?TW?[%8tS[ QoߴN|"i`C` ~=}}k/j1:=\{ȹvKmĐNw؉>3,u#!G@T>@ q҃oa8? 52DrE%Wܤ {~.4Z=UtKٍ#`s֩}sX3A%N<"X6mJ@`R֤ CC\@`RIa2@gzg̍ P3Cok@AZxG {A5'#GrlyrĮ5#-@ @!в79]0Q_@'u$ t%, äddҙ\IS8xe\Vx@@V(\WH+&1 202$;MFzH5lo@NW4`qJA`t%)Wa#˫%>YD8! FaV.5ΊN+Q52kI'VSGUIQj"W:{$%)֕cN)kC=XK֏MȦ>K"= ,IRF/!3'bSr- )>m5%l3f0V)C}n(WL`IIPM'o$Sb:,7OŻX` )̛)ZJqxVO`'>tvJknk,U|kX.!QO|TCS,}ģC\l{l"5|ȏ9ֹ,{Ɔ@ V|5[yr;YǪnNfG-L SZY3|wmĆtKWc59%zBV9t28%[O?ZhKa-4C XR5h@3 0k"L'`gH@`"DOϐ D@3  П՟!  Xf@??C*@@&4@VT&"`Mi  LD4@ XR5h@3 0k"L'`gH@`"DOϐ D@3  П@`ɿ ȬN:㰦ddU,eN3Z"O?}.MZ,󫁜@8Tv>˄"P`= [ }jCK'0`y)&Yr{?*RbK!X6Dft`HuF D/ H!X!k&a֘QZRǔX`T}zgk,+7|Ցf0 ]#P*XFl3iy/eYf`C'8L |3bF\6H_ ;N`[g! X#, 0<kxTF"`R5XB@gJE@`$H`)  Oў3o=%/eOC`M,/@i [ VL`[­'8W|X PN`tNK^հE'p( SKTI06ds8# zv׆mjJf0'G` Ŷk}KV$n EzVKK`VFj=':DHIpQoRBk"P-Ȳ@`pH)E,u! X#  0k,ԅ'` XR58R BcQ2ixA>F)q+0kڔ 8vABW8\T*'0-؋MǞ,au#I4Yx1) 0#Zȅ! L!XU' D VһBKM@~n9Y;UL ~m`@$0`<z%>8跄!B XJ Xf'`~ h(%`"5)@UJ8@`v지 R>8*]N!OiKP[M @`@kY>,PV G`\ovɏT<c? QņG`[B!¡F T\3f: ^!BLl*`C+#0`ML0%@K!JΥb~>'9cTN`qXؖGfgIgH`\E&CЬ0Y0 pq8hU١7@`@fVg  X5z6 X88j&`|v  `mU١7@`@fVg  X5z64~̭7_ Ú 0? PL*FE  07k3@1j IDATCbTBsh|Oc矸O:o{{f4ZsoK9z;?|].G^~B=<`C {k׽}]O|ȔW^r_\zF!! BQd'韟zӟ-{o}GǾK?ju",G65Yے_/¯g<0 D -XGzUZOGAE^H VaM=o C@^s4sXG, X ڃ XG, X ڃl|~A< P'Һ,9W`I]dN@#x^,I{ӟIlÏ{ds1 0'gX8 Au@:td X8 Au@:t3#g˟`V-0Ő|G{/Fӊ$ 3ڔRޗ:[4ݚT+X)h%R%?%ͪLp&FtG *FflHj)jUZa Z&sD+ %JJc`>uį8:VP [a! hU4?o`^9x R85hRR࠾/"vPF}:H4iF*fade}T$X:o){GNw- F}*C6&$ƣqx|o8-_whTy%,`]N22Ҳ3fa N`VЂ:ZsĠT$X:&59Ww6\MaV0\ٗ.' -2?,?~U"[DDv[`[sڹFa%PчDjoP+%*2PFuR2*^;UVAor3=.|KMIJNTòh a4g]SmC3)4ݜzINF[eK&~Ԗ>^fh ޗJ->"YjśVDR6QҰ:o2Ɯ6OdE0$w5.;gOv'^uokX/?{O?JdU c?qzz;n^cI%@`b-;RU(?-H=! 0%vX%+7CPsY\@:lI@#EP@vk &#P-d f"@`;:@v`,sG9Νr @{;GڹS΂!\rC`਀!/Ӛ'OKdQ<= N~I'>@@ ~pF2VSuPFҩS~o2i3Yhր0)%jMK{(JNd)y$b7ᤑdggS̯Nt6T1V-sGPր̃hNznY{Nd;$Vp"1*PaeeDltح)K_lC9n'Ŷ`Sn 5z${L3+Rb޸?K )?[~@E827f?SG7 *("NHMq[‘oU69pP+3$P-a*pqՈpWQ*N#n@v^cʛ\~ ,+LnTP5C^Jt"} 3CJⵚ`_Sx'P`y (l&&CW|xx @E7gyE!wH& Mf0`;%ge&*H*S!@`(~k= j%F0-L\@:@-%ˈTx/aBx$6A[1FM@`e*au#N7 D찖x;Jϲ!D=C`G X;zY6HZYg(kGO<ˆ TX=^3 Ё@]|IC@EՊg, n~pK@]e:nke3ATtK8z( 6 4z 0(Kea FA+!-JN$ˀ.@v,F2 ]8ˬ+!`D k2kJ X+9,@څ& {UwYGs<8jϸ"M37T_9̨7<|F[\oyL BHo%P`I^b+ɼ,,coPT*F&UP=,,?ٶO{Ĥ]'S`y#.v"t}2/OPGRϨv{3;,?$m3LJ%mD4CӳNPMu̯uuP0Ptkf/)\a-6}Vp D_~DG.<͖nGkO0#V 0&1UlԌ™~v=f`^#vүC[[b &}cZFuR8&S!/~7CQ YtXk9v%%[l^ԮM Pj[IJ0/a9z܂u26{kL0QasH]8+dRtsxeyIw~U"[DjԶC`/B+9 ӓaVKNQ:_~2=PHZb{{Ci} o+ ޟMdpCW9$ ?>{1騼 FPR+_?yHm+KqAq3R%u,rհߧ񾎯1WBԐ, kKm)ޯ՚^ɠTSp\?)q wIǟ||0jOMJl3$>1-26:$l+C>>M1Ii՚FG#\kc?'%٬no7_@m޷MǼ3# Бi3# Бi3# Б@E5*y|dJ*C ~98 xː<R@"4{dH~T5SY3) Й@-% (*!mNŀj&5z6 X88j&`|v  `mU١7@`@fVg aa=강$Pу֐9C ɇBe(OVIoߪm% P`_^$IAjLɚIgV} p!P`!\ j7Q"eF0 V}V& +#$jSbd(HS%3$,Of(/Eok@ouM^&n0F?MogSXZKhokm?viƦ>C`*,Ʉx73Ĕ~@-T~A%X}˫ۖh_ BjX|@` "Xy N]-8O:%Kl%/۔n` I<,K%PkEHp@`0CI!@`l؄ F % Xc> 0k0&`Mx1}|lnovO^oG d(鏋Lt;Hg*TshY 1 +805I$IAj)c{22uSړΦ uEA`IV 2 jKQF}q&mգMaa6 ȡE:>ױi?R0鷀Vu_uv f.`R/CA5HplZk6JxKTշC)jk'ɡ8X|aa)KĀV!X[]W_eLQ{N&^ӭ܂P<65X`q1siscpkM_e˯m:Crueה?q&nJLgbTAv53xy5tz,+ͮɏybۆ!bjkMJx_Dkʨ95'AL N E`j& ->O: 3Cbj *>t)NM27?YӜOfa *nOi}%2 0vXӱf&@''@! Xӱf&@''@! Xӱf&@''!(}LJŹ~y2E0i &Xy?`^!t3 N,OMqX^rA":UC~j jjM ^A:X.3,P/HeZN;^D!=3cfH[ b@`)!X[]υeva4a&}빮!Pea2'apj?Ja@6-arˏ^}Tέ6|a5O3yuY) TH`}ܮ} V?feWBp'A/VX3j[\q_V*bjx 3)Tɡ]:Z|ҟt['Z'MSI!N`IKgM@O3zvtvF+;,gTFKq@`H֐4J/!! XCҤ 0*kT$*g! Q leϵ y3R9,Qx='ݽGGQLIg2]ŶJo* ȖUjEYJxv2XZxSf%lZUf{%j$(◂^Țy/YT ; &(SmWeIHJ9L) Sj%j%vlɏ h8ŧ{["uĆ*!0H y4YbGIv,\f,S(xKU^h, m+t( PZn k`A@fa KѰ< ê A P3Cok@A @5Mţ?57@`F3<%/XH<a%ddc a@P32uxe1]&0+`'3Y A;B.,FbW"0gXM+'(N A_ pY  П՟!  Xf@??C*@@&4@VT&"`Mi  ` =BMCAdC}3RJ=̲VV=WQ^?9'0Ó=!;:MbPh!kj5x3_`ǁ'#)s@IDATgT;~@,-m3OI~Njۤ?8<>^luצ:R::m(ZSe( f j wpċ_~ěG.|[X+>@k>}}ooտOĆr %XMǸ8KbcĤMCZ,gr+Ŷ|jȡwz{M}(1}v9C3}PaCIG@<"$AoejۡU~cH)I:[kZq  ̐\E&Ig&{[RC+bܦM>k%0ۃkʺ ̰o15WfgT١`3EśX k1F! bNB y&K˚an XC,|TO>: CIM+Β aKE`IyjeQgL?8R4R̰nFrs5ID`Ra $ɫU J>LFUebAVH; a2OZ}ߪuq. Q,_z_vm3Pa5Rf*gMSSD!h5 V t=c4~TB:p19P#:T]j-3jM Xqɏ2\D+_%騼zMUS ; JYPp3-bRB`fBGd K|jVZ3|@Fh|ڤ b8Z Z, ̰Z!ł%5m R N`[X k1F! bNB@`1*yKZ@1 XC;QE2t A)3]wn CA02 -Lr5e "0P8R4.xT3lF?E9z,XKwOI*A&pAMY&4>>P%1 RC?* K#wp%롌ʫə$m ZPmj 'ALPC, @]5c%Z|ҟtfR||mj0?fb$I†NB` ̰ueggTƉ"k7 X>}4"`fX4kѧ![:߬&*gEXCҟ=/vCM Uxt OQF`d8 -L$AT{ҭ 0%KwOI*Y(KKRk/i@`X3Vd6AP =aSj !"Xb=md dk;l@6ppL A P3Cok@A @5@j>;l@6ppL A P3Cok@Z^摗[o6@`e6eOrzhݚN9;yeg_r{CkM`T꿼~_:\L]&Ъ^@wuad{о~g}Z! :HU`i#ϙK#]J %G=O&tJ3H :.kjdAjQ"ɦS/wT_GL7LIn;9l֭&#I9-˪| @PB:ϑQ@`Nh,yDjYuO`@0^@ijm]lb5fWGq[AbC},~P@V$dk5f:~#:_}L{;D vWYWbijms =?WW uP5vv^a\w[Uc4\k~-,%FoźToA_RҜ,_|Nt-%j%Mh;_B B)L[c=O}cN~mP&G]/z\҇[<wD<2?̣g@giQ4 0 FۥY:cR@% 8 0k. lM \Nu[w'VN~_2@cwhYk;=ﴱ`?3LFѩ.<=Iɸ3 ЁkΛ:d@`b|g&dIENDB`././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/docs/source/traitsui_user_manual/images/insert_item_icon.png0000644000175100001730000000046600000000000030167 0ustar00runnerdocker00000000000000PNG  IHDRbxIDAT(c|M#>c۷MMMДڵ Y ڀ) >s|WG;:ઑ؝T׃`Z߿hdH8$[X^]k*ʁR؝TW:č@Ew/j2TUUOUw \PVNB#ќ2qO6s IhYjP@5@POY@wwu`2Q &d$5@,0IENDB`././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/docs/source/traitsui_user_manual/images/key_binding_editor.jpg0000644000175100001730000004043400000000000030460 0ustar00runnerdocker00000000000000JFIF``C    $.' ",#(7),01444'9=82<.342C  2!!22222222222222222222222222222222222222222222222222h" }!1AQa"q2#BR$3br %&'()*456789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz w!1AQaq"2B #3Rbr $4%&'()*56789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz ?oE}]U r+ԯ!\\Oo 2s*f1ڼŧ?U'*ͱX_$.qVSRMuv}E=[V*KƠz+M|+⥅]'TX9I ϶k|e~<[xZH_kfX ^sE׈58 `Se-滱XW.H3ǡ()4cž'+Ec zpjfΏ4KձIZ_-ce #+d/?_bo&@?Amk޹W?iv$?ƽx-{6ZڤꭵZ0b,W?'5~5ƷE }A8|=h[UgA>T6pH#GʼnX?4;Ŀ'4#O_kVƲc>m;B WIp9GʼnX?4;'4#O_kVFຎ]{VIQpH3Qb?MD|I@+W\Gnְ&Y`p6 uim+ݴhX?&vx">$?ƏD|I@+{"[Z*ŭk>v6b,_W?'5?AmkޘwdMkWfOQbWGʼnX?4;O|K@I|r^ k_/z?,O1d KĿ|r^ k_/z?,O0ɡݞ|/^?ֿ=^ k_XaC<7/G"^%?ƽx-{?ֿ=X?&vxo"^%?ƏDK@ {"[Zx-{b?MDK@ x/EG"[Zb,x?//5_AmkޏEGʼnX?4;//5D`"X(5AmkޏEY>OxG2҃l jOD͌,lՆ|ErmƁA*i`;n#'X^ k_/zY~?_ihf9)c5CkUKt1v)Qx95?AmkޏEYVZR\_M֎Uiɩ.zv<:b~" NK93?SҢ7:=卜[$,8W/zf_W/9WgUUTSGNSwWO6UDqmY?U 1ԯM<_-j)‘'wi m K܏˻ ICUG;5G#??ttXDSӧ$I Je"1ұmmsqrC+ڨ -U~V7+zAT=sAc 9XAqeysKl9dn,'vT vY6Io%l GsoWDs ?\wX?jtɧntY@-+.n'P;1yMbX"xrZR!ci\wX?jt9ul] .Pq :4: *` WojRͮZh׍a,rA?$2:nd`?\wX?jt9u{ ;mIWyo8! 8PvkcOR,^-o$XMvF%h%#??Q΂p6p_O7" @~rv:Vo&mZ(ngs 9C`Oo,Rming&h4H6TH`sTluh- JJ^P"* eI$ǧJzAT=sAc 9XK^ZE)KVE`xn##4R=~o ZYK!M$dSϖ79;և#??Q΂! Iwf.k F20k|:H5=M]g{EvA3v윎yu\wX?jG;5G: tk6KeVV{nfLԡӬlm`K{-UAFcyn\wX?jt9\N̻8I~2]s9sdGI78ʪ Ns=sAc ?,VԡԦxqI%X<;v*F7d="H#e$r# _G;5G#??Z֮|= Qn.p.gww%vl!IFBTpF*#??1_ 9Xm&Y.<˪2qv]IK8+!r8[?8^29ϡ_?~p?gAbzz3bpOA]t ݳK_?"֭>w֗WW(<+4`'|Zvq[Ke]ܼRa,7K'Xo֞eWԌ7bK i'xdE.W(ÐNbzSX_۵^ƏIw.r^VF[u9eC)1R8? [=0s,mP#ʒ A#qnt?'Qv>rLU;#.(H ?4h?'Qv>rLU;#.(H ?4h?'Qv>rLU;#.(H ?4h?'Qv>rLU;#.(H ?4h?'Qv>rLU;#.(H ?4h?'P<<w%G]qP<[g@96_Uo'3yݝwuϿZtǜ/X?g|콾Vÿ;:mn/*!mFϜ/G!2TKt#.*Ce;A9?&_* n\[~ECe;A9?&_* n\[~ECe;A9?&_* n\[~ECe;A9?&_* n\[~ECe;A9?&_* n\[~ECe3^旦ikygGuṁ9H ?׵OLvky$\ەSc: $ ZڌD`vT)e zVBOb n hmH* <[zb+Xb $:q{[}1l=\\執j-Mڿ wo+D9Ϸj-Mڿ wo+D9Ϸj-Mڿ wo+D9Ϸj-Mڿ wo+D9Ϸj-Mڿ wo+D9Ϸj-Mڿ wo+E.s_P9K}>^c88vBQp51clݫз}6rj-MD+۵&Q_ G" ۵&Q_ G" ۵&Q_ G" ۵&Q_ G" ۵&Q_ G" ۵&TWk-}D3@K@HW'aWQX-}J#ŻIyue@@OzqsWZ؋[jM#ʃ° >o?I`4@o[RW8㯨30sFǭsW-八L2ZGc]%AЊ/ y ,^8 ʤy ,SG夞br 6z`oZuyݟcD7?hGBi{ס(0(h) (((((((((+ξ4ȏ}_GAj??q-tNeg Y$XͤM{#$dƹ'|[VVOBW{H&fp[afUr0]sY *Ok2KQU[x<;q >4r!*?҈4WM2FcMBqI(Uo¸.ɭ/G5%"?*EVUpЫMoI*>ɭ/V[sT-c?Z ⨴_k2K1QMoI*j3kտg?E4*[_k2K1UEVUX֭9-.W T}[_-c?Z Z~oQiw ddb Ukkտg?@V]pЩ#?~|W~`o13җ UVkLj;lۺ޷lsZ bA5%&C$ZZ~oQj3B5%&C$ZZ~oQj3~ɭ/G5%"?*EVUpЫMoI*>ɭ/V[sT-c?Z ⨴_k2K1QMoI*j3kտg?E4*[_k2K1UEVUX֭9-.W UƓ^ưkM91inX֭94Xt0^Zjk2\ہgRdR ҴY!.d64W,M#>B~5x}6x;@!)a<ɴQpGYg_E`[&,֭ex}^PN9'\TV521+t}h-`ܡ(/WT?p;. +?t_y_ QEظNaOU|/<⨢\'gN _*]>WTQG.}bp}|/G. +( >SOUQX?t_y_ Q _*(v{T|/<}|/Eb;?*w]>WT?p;. +?t_y_ QEظNaOU|/<⨢\'gN _*]>WTQG.}bp}|/G. +( >SOUQX?t_y_ Q _*(v{T|/<}|/Eb;?*w]>WT?p;. +?t_y_ QEظNaOU|/<⨢\'gN _*]>WTQG.}bp}|/G. +( >SOUQX?t_y_ Q _*(v{T|/<}|/Eb;?*w]>WUDxKa` 2$# p}uIm?7S8)7ˇ;S).D4 /U0^p)Hal yS(m0\qƁ0d.O%Mc[fnJjlqk-e{ma\pIJnwm\8Qc' *yb_]q O. ;J rp>Lx?C-TT3upS PM%@5SwSqSlW]8)8tAyR.ꃚaA2~\ Qa7UV\ uDU43 a238Li?%;~ͳŢ}neG'E0p*k*xSIENDB`././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/docs/source/traitsui_user_manual/images/list_with_context_menu.png0000644000175100001730000002011300000000000031422 0ustar00runnerdocker00000000000000PNG  IHDRP#I IDATxϫeW_B[Jb!Rh#! ؂=jp 68Q7@ΠGQG H2 ZIEi'b*mQ"{s=?=gϣxZk|}'q@X*;O|y}c^@N}Ʊx@`o@C`\ w1gyn0*#/~o|-> Fny44!1oL__?~7u3c/"uDǟj$@BuHu@TG_c9v箼u!٘7_2l;v͛=' {Do&!C _dƿ׭+}eN#R2*mr3x#W(2g Xl{%'T9 lâ3]0iɷ+ܷsI2mV~cmpy`x%av.# )JM t1*hǰiNR.[:^.^ˣtuI演 goߟ?(|SyJF#{tYX1ۻzͼ{D[X2+55&akd# Ollƚu+ǽG뿞ww!4I\|xRb(,jPmgK0.a~KGp.]iv P_ь]e1Ƙyt[udk& ͜F#3p{Twlg&OlSָj 7mJܰ.k\'ƺέGuںtr0W[~@`gSQF߃HMeB?4!LJA`JI`w{E2X s D v B PD=gXDjUd!PD=gXDjUd!Pfe>pwg3Ć/T,b@ݧe'È(@*R /sk21+Ih*j$Hո+"Hh*j$Hո+"Hh*j$Hո+"ނO8Un & 09T_u/3y@F NRGL{ɣ s7i!m R"92#O;vXXv{M{Nj5+!PI*W(Fgvǟ<㹊IaX}ZSP17GvסzK,{Seu8D%Rx'P:lEӎ i} KzͰ2NR5^x(%:7%YI'yM<@Ϥ.aM}䁃K (=yIkRt J<x oQ a*0xbd4&6z. )R&I?x+Ϥ7t/<~[i KcLEO<= 9x/e(SloL)ӛW5Ѧm/>M.{L4س>a(S<= /?\;u#PRbmp'98qce"PHEiwԯg!Fq^bh+ "o4'&Is@@`#D}7_{pu'~A< ]imSf~ydx|G3vɖKFǔcI"cmyd4։](Y. lvQJדۍ1K(,Tٛ$Ԍ]WM}ƚ$L"\"eDh$m<̿=bC{$0HddkۍЌb#O [)N#Y@ƌb<-f$@!0dOIǚj3.3{MRL"Li1[|\\OҴv?7/D@`aիe -ƪZW46 F(;6|S=-ƨIPdeD1 P" GkՆ,i1G&(MlaQxZv+%P=tJ @` ! Rs> 0"5  SEd@`n܄"HG2 07DjnԇF@F#"57aC R M0!QQH&HM((|$Cs@&L}@`Dj>! Rs> 0"5  SEd@`n܄"HG2 07DjnԇFH`3ύ7O==y29 {*R k*`2I,@*RɁeS_< A`-Lj-;e-)Q# n^%bt=Y)]2:x@t`L)go<_~(]iGQ{إkfx')y{FT׉EÒ,kjJEÃȼ& PgRKx=_z^m5v)%uO>,+o s͈HE*9(niZ4ng(?aEȂ0|p>Y R;000ndA;"PH뛂 v?KzEVAj/&_R*d؎Nb7[JuTkHDcLboӄ@"))HB1Qn LKvL[%H8ps/Y̰fa*#U&$V7!r=2鱟6ꈉ]»jj@$P۽\I%Τ]1H$,& P*Dj;1jPT˲ P Dd("UƲ,B*e'Y %H, @JIB {3m{:⃫+RK|QeX۽IbS?$jbM7[/ٷ"BbdyXݣ0$I*wS?1&1&1FjXmF?HxQ_ƔN=S@mV,R&.\D@`#T'eZ'4tO[Ƃn+3t]2kvn;F!pBN^ u8"clUh}R ,ZEj1L pk)"UFL"֝c"UFLZEj'u7 VcwX% pby[텐44b͎">~wD {~7,7wJeZL \&7ɚnR"YIbzrcIM5--qᩄ@"ո& Rh菶4a1@vc$&&PH HvZ11xj(u\D@Fi/#7:LD9RL@`BԶ_mKO~uĨ*( 4;(HE䴢TpLjȈ3iEvYJқ48 6*!-[jb"{X!êTHA)WHT=J(1^YVeA@/J![m3ɕ@BA$1ˑSG] jsV,@`Du!ѠD,\M|Iq0y@`iZoAM>TuDA?\[M 4IFWcDǀ Ї@*R)CEx,;{ԥq1 +̭e&M5N{X*}b'.sn2_XHI ~tlq"";3.zWkĘg Z.r JcpGc0 [,Rz){ՔC%PHٵzVL4{7ȚC< @" Ї@1Gnt2H')eyM:belfv2ͻ`yh&Є +Rv''8f[d[X?wFOmC+7İ!F[1gTDhY*HZ1gTDhY*HZ1gTDhY*H` <kΜ!-Ru|+b?;&\!nj\ŵugӨ%c%R?o/6F ?f"PǏU4 [#<_}*C@i9=sq8!5($ `@7D R{ R|L0< MC/ gԞ7!nT7z!=@ t@ " `x@"͇^@`=oCn>B{&Hy&Hu3Dj@7D R{ R|L0< MC/ gz<  vpHke8 ,7I|FIDAToIENDB`././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/docs/source/traitsui_user_manual/images/move_down_icon.png0000644000175100001730000000111600000000000027633 0ustar00runnerdocker00000000000000PNG  IHDR&u2IDAT8-0(f %,݂mj zK0h~#帩{%qbY3Νw]mkч7_$}K0pT*c|QO gY=׵ᄀ,Z=yfx_L.xمPi@"uԏ~FLuoG9zC~D -?*DT1 5> [UOd^w5X!98A/)KNhqHq\nˑ5M{ӡBr.nڬ-hcwoΪKN6zx)̛rJU4_ X ,8U.C\ڡEe 0@F CCE[MOdJHV QCyVo;k WHdZPGpPP/x~ &+O4y< rl;_{c8ofPЭCo#IENDB`././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/docs/source/traitsui_user_manual/images/move_up_icon.png0000644000175100001730000000113600000000000027312 0ustar00runnerdocker00000000000000PNG  IHDRh6%IDAT(c?)M1P{Oω͛211ٲ2!A%JKw?~i֠]BY5 tOMϺ.+_U@h8pࡻʿ?m?ۙ&V]jx[^}g?E?f3j}WZ-}3D#bܯ&_l_>~Y#ʫ;M g~%%QoA"߿~O [ag^?~ٽv+y/}7 Ԋ.̾͛O^ˁ7FI.0~UqsؘY^Yq _iX5ݛolMmG˷_Ϗ5_AQ ^VVƯ~|o_>s13Ie!%i?N?3L1F_ q"6~do<]PTFG(!8d>A6 rJLS~IENDB`././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/docs/source/traitsui_user_manual/images/no_sort_icon.png0000644000175100001730000000114700000000000027325 0ustar00runnerdocker00000000000000PNG  IHDR&u2.IDAT8!1E{S6\4plpa0Prb#X0Ʊö*]%%þ^ZKn_8 _ %PܠX4>Z+eAl }J6[ՓnA̘?ie׶kkr胁0.b5*ž`b ؉4F O+(y3,O:_80Ueʺv 'SFR2dY^?ŻcZ[VUHB>b [:#'`n)ي9՛6VnVƪ!2kHiL+7֫ ]8wpHHr_J ?:"L !}Nq9jO6^ dS "%ߢs̨ͪg2OB_%4ɷh8\j<8=R=Rp৚0q4Aq7mw)X(s<>juV),q{|z֐u *4IENDB`././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/docs/source/traitsui_user_manual/images/notebook_list_editor.jpg0000644000175100001730000002607500000000000031056 0ustar00runnerdocker00000000000000JFIF``C    $.' ",#(7),01444'9=82<.342C  2!!22222222222222222222222222222222222222222222222222" }!1AQa"q2#BR$3br %&'()*456789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz w!1AQaq"2B #3Rbr $4%&'()*56789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz ?{-T4mpI?ٿZ8ȑ3FjڴY1ވ%X&0Gx=qkJw,?߶ ?,?߶ a7o\)kW&n 8%93F@G?* dfbUz!?,?߶ ?,?߶ Ҙ٦ב ~bQ e춱%Ķ6FtPTێ8UEM}iӧQ^)"cltH*tL>P+t-6+Xn|gNK~po_/Fudž[x!w7f̘Uu=ZLg erGi!޿_΍n-oR$i7wӒ7d.eVNt4T̖F=sFuU!fcSj\BGxmR be%[7m8'Kegnդ"[hXc1]OڣoʏGߕW29[Y ʠ:ҲeuK BۇF* )T~Rf Ue/ͣXVA2_BKJe0DlI2Y8 7P<2 6]O~ddp?/QaisushUsyyOﭼSui(`kͩna<^Yr 9]ty)?7 h,3W头rY“es{ub֪fPF0JGy}+tWMfo"5K\Z^xDX-WuisFWn87'=;VKw摂qS~o?F CuMV#k.8r7+dg(O >}S5@D39Zoy}u3AIt2.oʒ$Ica#\)*ϒy}SX[\mem+k*4JOD*TsIZ>mOJ6h8Y r:-ORhwoB&(br13slaty)?7o̿<t}}oDCcظ@U: 繭M/cak2YsTJGy}UEZyN~WJm&կ/ne5 $9JƣgO(m[y>P(}Bvs~֯/+I/eK9g|e]Fqx}Nh9~В]%H 1cfp:S~o?<RD ZOⴒd[݃qkigF a~zz{Vגy}u͈Mh֖mJ@t :C݃L0CtL U ]V{kYB(P1 ty)?7cZ4>Fݯr {- Ȉ9#LzAu6Vn1ӂbNo isomokifbfX̛>QaA]3:Πb6 c)GU"M~Q7.8+S~o?<V=nS՞?7gD(*0u;9ncM2IWb:O<%?+UZwjݭ]*5kܫmk5 *&ݜg'j :][7z|%DK$X#W䶊X7o $V9Fp֔aV&Rrs:8jKu!2񝛛vA9j%Ruّ$9y``oVn2?<%?D7KI֬.eE-Jy0T͂;fV;wisml-Ep'ۊOS~o?δUhroӛlWk_]Y5a (lsJ}]7y}bkrS *iTʝ*ME7{=?v$u߲=?>XGaS~,?:oae{tXQMϻ^ٝo/`232(B'A<`,C׿j;k?+7H!losG!?f5#j+ vMХי) /0 M$qȐ"#s9è>*dҼ^*xM녹xnmUAŒV~ xL^-vHY }ŸqU?bmɥr<։G=|=ԦyK7+|ҩ01]k{;t <>Z,:i+ ӣ V,sKT֒wKt޺./zǼοB`KAs3,Rc qڿ?P/*} V^_ (_BUG+m_ף=#"?пk|Q?WzG}jHA@_E/ZrOz>ڿ?P/*} V^_ (_BUG+m_ף=#"?пk|Q?WzG}jHA@_E/ZrOz>ڿ?P/*} V^_ (_BUG+m_ף=#"?пk|Q?WzG}jHA@_E/ZrOz>ڿ?P/*} V^_ (_BUG+m_ף=#"?пk|Q?WzG}jHA@_E/ZrOz>ڿ?P/*} V^_ (_BUG+m_ף=#"?пk|Q?WzG}jHA@_E/ZrOz>ڿ?P/*} V^_ (_BUG+m_ף=#"?пk|QaeTARBuOUm+{XRUTA*?Oٍ:^1T!eך/=,/p+3gLW'x-nַ5/)"F#tIM_iv6ӤIoIgp'pVcY}$oӃZݭ,r¼zʹmwm}{cqqpw6)'v}WEiiUm&%7u sY_iiiζ&?)i}?&?)i}9t4W= 6AKOQ 6AKOQ΂CEsii,t4W= 6AKOQ 6AKOQ΂CEsii,t4W= 6AKOQ 6AKOQ΂CEsii,t4W= 6AKOQ 6AKOQ΂CEsii,t4W= 6AKOQ 6AKOQ΂CEsii,t4W= 6AKOQ 6AKOQ΂CEsii,t4W= 6AKOQ 6AKOQ΂CEsii,t4W= 6AKOQ 6AKOQ΂CEsii,t4W= 6AKOQ 6AKOQ΂CEsii,t4W= 6AKOQ 6AKOQ΂CEsii,t4W= 6AKOQ 6AKOQ΂CEsii,t4W= 6AKOQ 6AKOQ΂CEsii,k? ̓BuOU[R9eӡ皯uOn oe^t\lǜGN?LUԦ)."XV+斜ԭcMJ[cm$[gFܸy5e8C8rGZѤ҂_!80rh Q􋋣X3m;f Uug}gOxt_d۲ͼm1ީvȨߘ֖/ XD*a E8sI=pOjXL.EK6Z_Z}! pOljgqHln!U( r;pkGT6"ZL'E(qeO굟bxYԘ^g$lUٜ )=WNu(kxo>iyPm9\>M:HP5>D9"6{8z鷯TJ>})zx~tg1"ѽ?:g1"ѽ?:g1"ѽ?:g1"ѽ?:g1"ѽ?:g1"SE`*ki/o[KZ4d.W[K_K~[*O'u _K~[(_V}M>U_G?ߖ h4]v?ߖ ?USFOQĿ­d2}M]Ŀm/~%-k'ѓh_m/~%-ki/oY>SE`*ki/o[KZ4d.W[K_K~[*O'u _K~[(_V}M>U_G?ߖ h4]v?ߖ ?USFOQĿ­d2}M]Ŀm/~%-k'ѓh_m/~%-ki/oY>SE`*ki/o[KZ4d.W[K_K~[*O'u _K~[(_V}M>U_G?ߖ h4]v?ߖ ?USFOQĿ­d2}M]Ŀm/~%-k'ѓh_m/~%-ki/oY>SE`*ki/o[KZ4d.TjX`~'?*+Q"Lo"m%Hˎ'd././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/docs/source/traitsui_user_manual/images/read_only_editor.jpg0000644000175100001730000000605400000000000030152 0ustar00runnerdocker00000000000000JFIF``C    $.' ",#(7),01444'9=82<.342C  2!!222222222222222222222222222222222222222222222222221(" }!1AQa"q2#BR$3br %&'()*456789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz w!1AQaq"2B #3Rbr $4%&'()*56789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz ??/·^džiLy$&gcI+I5͏/m~A|lXe M4̑[ȼ?<RDWf;+oo$Roɥ'd4Q_IzoMխuX^Ks*mf)lo/-q A4g.{dY?;D$=K$'h,r`_/䞛ǫ5&QLDe+[%F~e9X;D$=I$'HPI8d9X?;D$=K$'4}^^`4K9cr3Ty*+L nTgI5E8Q#g(b߅lJrZ6(8nnAKgo9+F<ΘH)sa}F{UxK5/"9=`76jfMf(7\F kb!K&Ѽ/@Cw) (((((((((+#UŸHk^5_)a9?w/xIuF;4Vw7=_IO/$h'h__4r0Egcx/ ?WAM.hY+y&o{E;y_]覢~Ѿt<((((((((((((((((((qyVOQ@././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/docs/source/traitsui_user_manual/images/search_table_icon.png0000644000175100001730000000153000000000000030252 0ustar00runnerdocker00000000000000PNG  IHDRЭIDAT(-]OGggf?^8`bW 4 JEA/H~R7P6J@(‡X"6YXۻfwg6c;:gwC-}>cU@[ͭO<B8cUu򅪆_yNͰS q-~UkLFncO:|dB@ٶ?bqål6[TQJ:9 T89jm'4s:{$Q*˫xfN(qhFSi5%Osi{_QғQ Y׮yz'@pJL$vvH!jF-U1Xezق xpxr2;88bq!eQ0Y65:C԰C<'Xن\F /bhM|n=SiݨoS=T\z]R됾!^47.*cx]A?'&o_ss*Km˧ϕ(ˊglwo'GG&nmm[M(u(b$Dޫ^!Q?{vvv=gf'cJ2R#'w.%0wn~'Q=YUub0ϫq@ x3!̽ K e߆a~;L|Ѩ3=IkzIK6 Fa/֍c,&!XApIENDB`././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/docs/source/traitsui_user_manual/images/shell_editor.jpg0000644000175100001730000002750500000000000027311 0ustar00runnerdocker00000000000000JFIF``C    $.' ",#(7),01444'9=82<.342C  2!!22222222222222222222222222222222222222222222222222" }!1AQa"q2#BR$3br %&'()*456789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz w!1AQaq"2B #3Rbr $4%&'()*56789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz ? ԯ"XZ5H>]_Q(S_-YIXeo]_Q(5?¬:BQb֯NxIMokbdzf "hvpO"ȲȊP19'&]^ZA[[ U8!c$j9Uqn)cY#WGH Z3/\޻keZUF}Jt׉d7kx+<=uf;ny`8=b+y_M[]Kojr)B/j"A[\֣\ꩤjxp}0&o]V5F<<>R7 Fӓ߽(4ެ_ ?_ -5u }WYWȐ˕9P2ci;Lҽzʭp9 ;mP&EP̉.TѩhWZr[g1JHV$`p{Amy{yw˧g$LpWP9}#+uέڟu+Xm\ 3znVzH5 T mcŰzG!mafo{* 4HhKӴjٮtW0ْxaAx<ή :֪2p3p_YI]r*M,j4mqGN*|=lz4zv~zCas|FO\p{O]i_HD7ωFhAs]3h]3k;[( Ɠj2\m"E8\6PHK `2x5Gkm:7֭uоݭw-$*R7كlF[r7?_ ?_ 12:&nb/۾ ?x~l9zV|Ϥº^mr}K7rKקUu!7lO-yuHK;€u$tk=N;/js2%ݿ[Ωc3=(\C3HO ]dUI#ܪ.cq֕薚uw ෈ny$P4m;}wTT2:) Cjs\\^_h7IpO%NHžǭA][GKHHlW C$ZߠXH:&2psiz%iݦo(H A.׷:~9TkT!IR}{U \AO%/\Ip>7Dr=hQZ@"o FYT*g^TӬ![͏-`[ DŽuhx+Uk:6&[mF cOOv-kdcJH7~ :چ{-.-1EnwޗWncWkF @FkmlU/>5;l8mLI FF]uέ{7E<}b5POOZes[&Ę\NzwuέVqi؍1v@9'#n8=tVO.nbda099k5'Mh !$ gVuέdv>$XyI-ȗxz*fFwgV+=?M OsKw8}k=MsZ5ԃPo3%Ԫ䜏/ϧJœjŏE:_&E:_&E?uoM2_,H\I] V8##ME67˦?u?lʹYL%76`rچj7sɩa? ܷC]Ne)&w;W8!61ܬa[_ko?՚ES]VZm\UL(5v;.E(e1Ψm-j7F槦Z' 6'vsR6VKo+Xٕ2p70\vc~ܥ"'c _#g=)/K{Eý| O4u?sOӺխm.ټ'q.B[h8W)W o)=kf 빵ۛOs9$:`&"3MUNTf ~®V$4hKWEOynHcA5+I57Ӓ]12pצyu欇RXPJltK?h,+ Ao_JojgAVIB>q8Le&Z(Wu Z~o6py,dzgK6]q0'|- FneFP~uZ=(X8wk(`F)gjQWD)Kk`yQ)g+ҫGZ xY~4], I@ǩn]cixCi2Fw8ڟ4ܮi]hiEYi5e! JU# UCA<\ŵ.٘VH3>J府8[l[iJۤ%V>Spmo$#K1<j\Y@,r^hXY7cpq4`^ >SpOJŻ[0#Y5KDx?)JJV^_ CzsAFʦVDp;3 =G-`VA8&4g pN24bx}$gVt0bAZjpXIX`vW<Z)I>߃6`-RuF:Y~IdoWߞriNFYclx|_]x-\ q$c$䖈ɨjo+e ЃUSMM4]$x;leyHF9gR:i|1c'sH}3Cn┚^zm~#ɿ!YLQ 'ԑꖲRin#]6vv1G*I)IA!#Su#8fM7u#DfBo^1ҥJW׿o/.ypk6GPy<7ymv3~1W[SDPO;׌AN vT[mϷ֬nʹo&kLzb0{zQg]5/'e p RFgl <@$&5p0*玝Oc%YJYգsTSIϱwu3nR͵K;fMʌdU}^4k-M,,䑎aƉml"1k8m[ dg d4&b۳Hسl :4vЪ4"pֹۍ6Dhe{y#e`G,9ϥDt9V)4{W6׭,6c9azݤ?2U'myKO #tb^jVpL$ +V&esVo/$6( iQU2FYJ5ŎM6IJ2yjY6p[җ<-tt]EVؒPJ pqן^j;-F\qy6q? pUH#p~4Zdq'{s?/ȹMٳ[9LZXEVnN /70ҡiWo( o<֜sGIy~jo6J/Kԯvǘol*uÕ=mE j]Ry 2_~1v[ėW#E {$ɒA?1sz9;]VTo!%F dTup@,2ԫǵaz5͍hi#s8GVm#z@vǵJNfe w$L8ܹ#V`Qu=ίi:L3T@=kz-˧&Aw@ʼnIJ_ǜj觱#cd;`QT߅M%^͘=mڧ~ZUTKVk;QL z͎q$3Hw(aeI03]׋?dծ*"Ko{n–u$Z7W:4FHE@k4fMm9l 6?dubvHhX5&[H\:]s),q0TQt8 ~uh K{飶-эf6]ĖZMvwF@U!$tzkP;i5*-It5+DlmiHssW뀸L[_Zp n"ua('mkHhAݰl,y=1W4OԤ##ZTW>l&;qpwcϮ8ϥs zzy7pJFGZ.Gu66 }[Oi3.89ݼzd,l䪀1 F+EVjom*Z\3圠$uRMr-f{{9#hnٴ 曌/$/d56/ tUv Q|UeZ;H.+NLO<𶟨=-Vv$DF$C+] ie:ŀ`^?AJjܙ(+nZz #CnGX _òjhVk jG/$bx)Y#qGzQ-]_c/Mmyq-˕pNEX[ҥ=JUJմue;tg['#*JKŹY$V܅+2O9`U]T`4^)TXe cΡ\/\um9ٖ퀉F%>W#jvig=H"UFNѥ3Iɹlai'ޫMw)Ҏ{:i#]eQSetᶕo n$;&OǵdhEer|<_ޠ׶b%3[A۵z9Ywe>Ta-;G33rSSzZ#<4R+"# r Ma>6.bq7NA<~mkqg{e3NWl麴Q_ b]4nYG8ǿ Fњ6:ޗ[=ŕwI1}?qm*-*Q;m|FcT5I&` XaU<9<<Cmj+#Τ(- scMWw-5vF-kxN%P>l vn"9i%pRiA=($V Yw<<ڙ ?osnןj-wn(H=={С9)sE|[b@m7yqjߨE7n#Ϯ8T՜Rq 9o"m[lS 8p 4!ԭ%2$0:XN[:^.#= Phԭ/4aw&4Oj\evQH.z~$kvt~ ѵ UԺ @@k%ЭM2ԗ1n b\Fvh r՚rGtcgZgZ6a筿TKVjjjy (jԓza{|b$/!!psHZW%ɽMN8fбO+'3cNA3A0yFH¡BݱKLhuC#Sppwd[x;TY\ܙ,KPx6Fyj} TMrR-;N:󣨺eV,bʅ1eeԼO6ga#A幔I]njs݆,xF.^"0Ӏz$a5WNеkkBKR{KI!ͣpJo0QڙcJk?jAa%%FSj;@l}/<4*D*)/u*y8Ma-S$HRLBTsqbCet-KLXqx$"iHN P]x*Y:qK1Z jБK.џIԣM(H!P-t*mF<#/UnJ.4 Bxl~l{֥[DݴU!ceQԕ߹/2xCcZ 0%$c>1С?duy7+ɷp'dsTm<-áAa6m$֓,֓-P ?:E:_ sjnY,X*<̏2I4u;MZ]YJd\2AXUF`xcg ?ܰeY|`zVʹrksL˴.'})-Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@V9ojV9oit'jm3l-BxmuHTKVjjjy (6ZզtRZEb@8r]ho'KӔoGNz}+R7{qq FF'',/`8m 5ah/d&lTw| d{Q:nOmcwڢtӦU0HCI}ߩQs{[ 'PPh$X}ԗ~).&{(@0_ HOo5${h`8X# u*{lPV 29Rp(((((((((((((((((((((((_ǜjV9oit''*m7l-B8.t_[l_ko?՚QE0Ynjq?*(((((((((((((((((((((((((_ǜ5f_ǜit'LF9qЇ_Ǵ ,0qSickjjVS_-Y>;!ES]x\Wڻ+? 袊((((((((((((((((((((((((*yFkFwB{/~6?kn6?k G9o?՚mڧ~Z^|v:(ڻ+?7vVs_@EQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEUkFk89SS%a__l?9o?՚mڧ~Z^|v:(ڻ+?7vVs_@EQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEUkY6ԩzq `ToZ|NFY#ܤ= G9o?՚mڧ~Z^|v:(ڻ+?7vVs_@EQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQET71uʓȩ9/֓zUg17E6nAI'X~SrmjĒ8;o?՚mڧ~ZJ; (`?*{Weg05T=Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@G7EԕICMEgCtS_-Y%4QLvVs_\ojʀ'(((((((((((((((((((((((((n_˄A?MU6劐5œY&; +!p Y{дE`\c{}k<#nab ݼjBG#>JbE"Sn00qn0-%{ŗbH&ko΀5|ko>m>5:UB@(oMsh94yI<$7&(G(oMsh94yI<$7&(G(oMsh94yI<$7&(G(oMsh94yI<$7&(G(oMsh94yI<$7&(G(oMsh94yI<$7&(G(oMsh941JS(96pRVVR0({$././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/docs/source/traitsui_user_manual/images/simple_enum_editor_closed.jpg0000644000175100001730000001056200000000000032043 0ustar00runnerdocker00000000000000JFIF``C    $.' ",#(7),01444'9=82<.342C  2!!22222222222222222222222222222222222222222222222222+(" }!1AQa"q2#BR$3br %&'()*456789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz w!1AQaq"2B #3Rbr $4%&'()*56789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz ??/·^džiLy$&gcI+I5ŏ/m~A|lXe M4̑X/j $_ KTtx18ڦNh_Kzo:>Qc_Kzo9X;D$=G䞛ǫy8̒:(f8zNG?ÿIOMԟKzo5a"n^,56݄ÿIOMԿKzo:.-=Y Z2DpH?43?Ձĵu4$'?;D$=Y. :?|EOHusm P(}fsK?ÿIOMgw/IzbiX4b>K$'?;D$=Y4Xw?G1?Y_KzoXw?G,M;xk?hgw/Iz%=7Vw,M;xk?&<5Q Oiw4;D$=G䞛ǫ;&<5Q N('C4?Kzo?ÿIOM՝ N( cG}C]%=7Q$' cGӿ熱 >P.䞛Ǩ_gӿ熱 ?biXP(}fsG?ÿIOMgw/IzbiX4b>K$'?:䞛Ǫ 3ƶ:kajĂ0,xx|Em895.Y+2V2WG9$'u,lnAboehb{ MɃ>X۵m#2s8w)ݤ<Ř2eSuֳ' s; ?~&V4ŕc 4XC:y8*ǎxW_)0' *E-r &Kd]_?xkV)j#'Tat-|HZL03HygcR]WX4$Z } qʂr19=sڴڃze%RZ\\Q r++gG\JYv9ݑ?^3^s.x$Fި''C{OWeG'9ivi3y(eV9vxb:@MQ!5l`̃s?} p\̗B)W>W(G}Vx~RW>W(G}Q}RW>W(G}Q}RW>W(G}Q}RW>W(G}Q}RW>W(G}Q}RW>W(G}Q}RW>W(G}Q}RW>W(G}Q}RxKG #WzWQL,Ld| rҨx7Mu/`<HQۚ9${n+qUj)'4e٘?lXȦ;@8cޭ!FO$o2ʲYk99S|s.wUňەҋW'%Uj(*E5xI熿kΞ/`Ik`E1D6_6|_ޙ[XY]I )HPXtHj'k_(?Nֿ$W~!Vn]a+7Sȇs]ckYEں o?J  ?%oAs]ckYEں o?J  ?%oAs]ckYEں o?J  ?%o Bv"M_kE|  ?%otr Bv"M_kE|  ?%otr Bv"M_kE|  ?%otr Bv"M_kE|  ?%otr Bv"M_kE|  ?%otr Bv"M_kE|  ?%otr Bv"M_kE|  ?%otr Bv"M_kE|  ?%otr Bv"M_kE|  ?%otr Bv"M_kE|  ?%otr Bv"M_kE|  ?%otr Bv"M_kE|  ?%otr Bv"WC'5 (,yبSm4cf$־utCI[9P\ğ>7i= H9=x4W3[|GM_Vծ>}>d7m(pQT#././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/docs/source/traitsui_user_manual/images/simple_enum_editor_open.jpg0000644000175100001730000001327700000000000031541 0ustar00runnerdocker00000000000000JFIF``C    $.' ",#(7),01444'9=82<.342C  2!!22222222222222222222222222222222222222222222222222_(" }!1AQa"q2#BR$3br %&'()*456789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz w!1AQaq"2B #3Rbr $4%&'()*56789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz ??/·^džiLy$&gcI+I5ŏ/m~A|lXe M4̑X/j $_ KT4|VI N<ڦN$'?;D$=]jV =A.@:⣝c_Kzo9X;D$=G䞛ǫhybfw8UŽv9%=7Rgw/Ize #$jYzXSSmOEsO'䞛ǫʈlaˌMfUgw/Iz%=7WbtUxDR:m89Ta)hcO:Rj67_ 71M=&f,mڶ2q`8*j~gbu>cup- I^.mEm LY_P0cKh^1h瓂xIuC<R+-Ν bid{U@gk5xI熿k_1xӏsG;Yjz HFm6'UYI^OLOcBWc[# zz(r}O50sڴwojmgI-D~XrTrq¹ɼAHkL"GhMHҠy5~Hޕ=pkC-ڝnܬo% [\>\ftF#`;HTߍsuB㞔9iI;W/?.8&fDvXx/ՌV/av}K7Dm )Y!Kfb\ɭKo\Con$ ozs=*jO2#JQ `g𸯁zU:=>kڅ޷-$s姻EBbId0kwj/ 쪱b44FL @S\.;Ȱq _\wȯϽQJz/WG.K/QJ=nO\+_% (a=nO\+_% (a=nO\+_% (a=nO\+_% (a=nO\+_% (a=nO\+_% (a=nO\+_% (a=nO\+_% (a=n\av`c"$8=+B '饕o 8f;5֖]YgQA%9Oq") >I!FO$1>%Ȳ+ *AW+3i65.2\MNgE'%Uj(*E5nyZ襨4Kq.ݏF:ӕOO'UZ<=!V?"CzʮQKbT}CU(Ar؇?bUr9P\!=\_/"K~1]s697J횺`nc5PJ2Lڱ%p.,g ː (qAKDn{i#L[9gMb륚yS/ Zx]PҘ8ʉYLcm̛~E#;g<5:,cǾG'K*9` ”ef)a՛.? c~VZR?Ox?hէ>.? c~VZR?Ox?hէ>.? c~VZR?Ox?hէ>.? c~VZR?Ox?hէ>.ujW+\tA@^vc n.8[-t:Ww$Ŀ ME;y_]覢N' *E-?+ZO'UZ<=!V?"(((\־|+=wem'4.#-2OzF=urZXO(/W{M2{tny+1pUֹ",^7XHhˀZI6y ay#Yy׏5x=G뢴 X涒9 u AR#OEPEPEPEPEPEPEPEPEPEP^{ ~WWw_wTxIu9 J{ NoϨ+QvWws:G' ?k?cTWws:G' ?k?cTWws:G' ?k?cT/'LU(\T.;=uki1uGF-;%eG G G X*>VaӌIuη|@G$mefmNOvT +a\Z]yyn!֪xj-V5 m3dKF(|+K@ p qGB([KE_<XTt̡ߡG_m~V^hԆwL #돽uic&a $Ʋ1pG^:դMHFm]&Uҋ|УJ/Bu[ɧL[v2E3Fy`Ճ@x`x?b7Gm>Ll˜)%JOdiE>vQߡU촭GS>auw I9p8EwcɅ;qsz7{6?;o(ҋ|Хд+{R &8(Lc͎䎆o|Y/Ye.Yf Ǯr~UiFN-ߡJRKG_m~Qϝbm}V/-lX Cc9;FqW.g4闐KpaI`ei :z!VmBT'/_m~QϝsQagt$`:u9 ⳓEd4ƻw=ErW!V_ P]dQϝiE>vStU"eMl+۳#3-ͦ"4mFin^'qV<[5%|Q2_( ?;o*KG%Or! ͝im=u m|{Z\ܷekٖ;o(Ӌ|ШuYa/-Ȼ+TgNǮ)JV]&[z .W,z|{7XӋ|УN/BMUK/5q\dG*)WӋ|УN/B1F)rqϝi>vVf(9M?8 ?;o+3bT_m~qϝ1G*SON/B8 )ߡG_m~fbQʃӋ|УN/B1F(Aii>QZߡYrQϥiE>Vf=ǵ9M?( ?K_+3cڎT_k~Qϥj1G*SOJ/B( ǵ)ZߡG_k~fcڌ{Qʃ]A18D2Mn TP7\gsn=.>Ʊ*?x?b[==] +Ij0HBrl:=k}Nk{/ :e?%RUH<*k *@Ҵ&ښ{]P:Uy%M+۲oTz-!}G^(_9]ŢW:hogfD*=zO IMjw7vyr&iPXcj9r]<41&nDn J;gns8MeO:~[$~㶈q5ћ!iϧd!Kj@'b?,[.b){Zi^ߖy?Umƺ,G6!B@߀0x8Ȯ:|:U6;g0 Bp 'v?o\- X겼ڮe:zVQ[4p0^ۻߙ[2pA]_ay <\gZ'K@TQ1WB j_Uzy^5Z3Jۯ|4tڸxPuaW9C#I=qOso:ƙA5ƝvJkI4LPecxb@SxLqnB/Ű݇9^? yB<[@s1T T:_ p4"&uimo6 B0n r9#]~ INu* 3G# s=8#ſ?G!-9t_o&K4v՝ROt>r@e(8cFYW rwz/t[khM)'c7[G0F$ g#ſ?G!-9$ەdM=WTsbJ4kt}k&3FkZ-Q͵$t +;Xwv6|2U٬f3&2@ݍN!-9?oV*^~k~1{=[Um\ew˱p'篨W/._i#V!HᲑ"b9%OL_Uxb⫮\ F|՝koc1PZn}K<kh] `c1͎I{KsFS wƈ͋YckڲoQG&/*eK/Uכf-;?-t嵓O&LP|=Gq ,2 /{w!-9OA[@c1T,+{e!nE=(IZ|:V mk>$3`ARzp7SڌrT s> L_Uta`hI5h²Պ AL_Ub⫳C?c긟qy G? &/*A[@c1Th`~U#s4y?A[@c1T C?c'sA5 L_U?q? AL_Ub>Gh tbſ?G~A\O?y'ҺOA[@o&b>Gߒ}(Ot3G -7LQ}W9$QJ?of/*A[@oC?c's~I>_U3G~A\O?xn}/ы]'vҍrkm|([|y-$tauA |5b}9YOp=T-UnuVV4z$4[nm@0C}1۽7B}e}[HGu. "xwQIw|Kn'=5T^'2[{+ 5{ic̲,`OMVnUI(%>lmH0KƽxjE#xW]n%䊘0Z˝*_irmb#]Xi:_ڭKU>[e鑞X+%ȺB _>?+v}?6sRx̑|_;`^'Նdwo>*)a΍zsY-~oVk>VWlqOlۀnW}Xox>?"izNhrk h N@ø aq5*U%vƞ&:Tc$.xCR-/:9+ԫ;˃ l(򎠢(((((((((((((((((((((((((((#ZOu?INLlWE󒻝 GVcYwA9T( #(TwxjCMwrT~H'Wc?S_j?(;9E9(WO" (AsWO"E9+T9E9(WO" (AsWO"E9+T9E9(WO" (AsWO"E9+T9E9(WO" (AsWO"E9+T9E9(WO" (AsWO"E9+T2~-bIp^CnO$ԁ Ib9_ܠOSy| W`I(OSEpT"y|' ׵`YOSy| 7 :qå>$OSy| .͖fL6ؤ}0 oA4\ZJ( iq"U5yNg.$9.VF<;[w^8V8J vA:ҋYV-< g2Aǀ;qy|}iY*wLᧅ Q>+w3~t]Foε^8|G!q+|~bFO߮H%u#rNwJK-p\XV?.#7G%f8FY%f]ߝbGpȃ5]ߝkZ(}f_kw3~uEQ"ckw3~t]Foαh8Am]FoΏK-}G>WK vA:Ţj1 vA:?.#7XQ? _6.#7G%f>DYoj,-.ef,8BGQ:Ou?x~趩>:ǎmsҥ Y8YYR:<;rw~h_<5!rצ;E?z/7,[Ykq# /O௟(((((((((((ϺX>;t9c(dbX#q8ZW_3PVrJt? C NA(Zע̏Jt? Cׇ|t':?xwOӮY'O~h_<5!rצ;E?z/7,[Ykg0_?͛QEy@QEQEQEQEQEQEQEQEQEQEu}4 Gz}:`i=b(<~tyF:)]'/oΏ>_ߝGY:k&p]ns0RW Qž~R o>_ߝ|ѿ:VH^I9/3>,g˴.FңZ$= e_(O?~T?%ocF%?[g1BA_ UOl'[tT@̭˷1'zMOVԴma{Ζj)nZu^XT* !V]EnIz3z6oma|kHJVHlPĮѵv2Z\؈~s ohB9|<{RI@ ( ( ( ( ( ( ( ( (t}G:Ou/G:o^SI,'4Y/9k|"ſuFy?܇^-50_?͛QEy@QEQEQEQEQEQEQEQEQEQEu}5}bד1~ݜӒ;+J`j Q   H#nOE!G@W(?ZPG$7U' ? ?V   H#nOE`\5m.,2ַ0 [b+/[g1Mn#+rB((((((((((Qþ:ǎm{μ;x?iס_K0 ~ dž?Zȱo]gѯ^e_<5!rצ;E?zy*|$,'WfQ^qQEQEQEQEQEQEQEQEQEQEgCASCAX= (Š(((JCYz?%okq]Q[QEQEQEQEQEQEQEQEQEQE5^4Jua] zqK3I$ @OJZ#?oa1q3VnWEVQ@Q@Q@Q@Q@Q@Q@Q@Q@Q@ >ׇ|t':?xwOӮY'O~h_<5!rצ;E?z/7,[Ykg0_?͛QEy@QEQEQEQEQEQEQEQEQEQEu}5}9AgkC[Zw_3Q.skF/u C:!ؿ?z6=:!spol_ѿb|:!spol_ѿb|:!spol_ѿb|KTחrBa+ U/c]^==T~⦷0 [b]Q[QEQEQEQEQEQEQEQEQEQE5^`i=>`i=b(A ^aX:lM5ƭ arV=xfNmQcm$_5C#V ]-ޑi}y-f0 )e#j=0NsT'/j6<[H.Dzg#"xMΓ͖ @yuVIQIR y28ѼK%.Fxϛl#'_@tzÚmޟ=_}`N !$UުsO4 4} ˷c TU:4*^Ֆ6ԯZM594&(ː0\˵Ɩ.wEy5ErKBEUhA{O kr&{sk.㲛c2nP3a.Z5^ZK ̀JSEY˰19ɠ1"gH%ȚWa'7Dkignje9WFB_2\6āQ?"EvnVXmiesq9{#i1MMq3V?0=bSJC-袊ܐ((((((((((G:o^t}G:Oue?1?~E7,[Ykי| Cb^q# / ٻEWtQ@Q@Q@Q@Q@Q@Q@Q@Q@Q@_3Yw^o-G܁A>*OzԺX>{daIAWGE!RoUQ'_Ek@RoUQ'_Ek@RoUQ'_Ek@z2_}neCe8RASuJCYz?%okq]Q[QEQEQEQEQEQEQEQEQEQE5^`j `j PQEQEQEQE3VJa+ S[袊ܐ((((((((((G:o^t}G:Oue?1?~E7,[Ykי| Cb^q# / ٻEWtQ@Q@Q@Q@Q@Q@Q@Q@Q@Q@_3PT_3PVr(0(((oajV^0 [bGWEVQ@Q@Q@Q@Q@Q@Q@Q@Q@Q@ >ׇ|t':?xwOӯC)}a_, Cb^xjCMwT~HXO௟(((((((((((ϺX>X>{QE!Q@Q@Q@e +RJC:($((((((((((kμ;x?i׸Qþ:ǎmzO=% OgxjCMw_Pr-zoXק§B|6nEEPEPEPEPEPEPEPEPEPEP}?=?ܠ) (((+/[g1Z3VE!EPEPEPEPEPEPEPEPEPEP_u?IN^5*ףּV-U>=l^d-U>=l^d-U>=l^d-U>=l^d-U>=l^d-U>=l^d-U>=l^d-U>=l^d-U>=l^d-U>=l^d,Qþ:ǎm{AS:ǎmz9K6Yω_'5Iao pleDUy?܇^K;>_ Wd_eDUeDUKEyW:e>e._eDUeDUKE/"_*"_*`/G`/REv_v_hEX?/KQX?/KTQp",ݗ%,ݗ%Z(}T}U-K2I#BWv_v_>|ѿ:<~t\ eDUeDUX7R  p%$he>e]-#rGԜӼ~t[v_v_>|ѿ:<~t\ eDUeDUP {k_ۂ3oVl $U,ݗ%,ݗ%ϟ/oΏ>_ߝ`/G`/SKyinjN2́r;e# _v_v_7,}IM;ϗz7@e>e=ϗz7EX?/KQX?/KU)_1#=Fyl̰RA_"_*"_*Fp+}T}Ugϗz7U.K k{JkoӪ4͐0Q@,ݗ%,ݗ%[T<ױ.UL>In>wSz= eDUeDUY=V,ݗ%,ݗ%⪇gدZxe:u6zUMjW~t[v_v_>|ѿ:{as}w;GmmM+p9<Ҁv_v_>|ѿ:<~t\ eDUy?H%P39ُפyM7+:ǎmz7=%'4Y/9k> dž?Z:3/ QEAEPEPEPEPEPEPEPR/Q{F;.iamiRp8)(H#nOAu_3FlImX򌣁䊊8[ie^<@j$7U' ? ?SB,Gs4w9WګH(^xT&P0?i7`ɕ?x*$7U' ? ?Qp5謏H#nOAu_0[bCG_PPa+-2B$ܼcnfu'VG$7U' ? ?R2X+,K,ym$!VP Rnl lAu_1 ,s$hH;[hPPB^SL"a|n+<U/H#nOAu_kYG@W(?Ha6m\IPV[d-%2Iya# - !NE1(H#nOAu_3^ytSY]GMMQ[mZu!cąSt ?Q   ˜[HgZIee˸̊(_m??G$7U' 5謏H#nOAu_0[bCG_PPa+-2B$ܼcnfu'VG$7U' ? ?RKJ5ƟozחWL."hI$>YiUH]3?'c MW+Ɖm>l ;V7nbRq{H#nOAu_qZߝu?o< [=ԓ1[4yxs]rIPx= b7#HK;)7(2yyC]$7U' ? ?Qp5謏H#nOAu_0[bx?iqjRk(a IjCqp?INLlWx' m'FSN6mXt;Jy& (WTqBS匴 _- V_٘/^֯~_- - Qkߗ/K_aG/K_aEf`aj7A 5Q 5QE٘/{Z^ɯ^ɿŠ(ϵook?0_}0 k@o(k@o(_|=_?zZ& ?zZ& (3>/kW/^ɿ^ɿŠ(ϵokk?0_}0 k@o(k@o(_|=_?zZ& ?zZ& (/>/kW/^ɯ^ɯŠ(ϵoko?0_}0 k@o(k@o(_|=_?zZ& ?zZ& (/>/kW/^ɿ^ɿŠ(ϵooo?0_}0 k@o(k@o(_|=_?zZ& ?zZ& (3>/kW/^ɿ^ɿŠ(ϵooo?0_}0 k@o(k@o(_|=_?zZ& ?zZ& (3>/kW/^ɿ^ɯŠ(ϵooo?0_}0 k@o(k@o(_|=_?zZ& ?zZ& (/>/kW/^ɿ¸Z$vf6۶1Z()`g)yY2)]|././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/docs/source/traitsui_user_manual/images/table_prefs.png0000644000175100001730000000053100000000000027114 0ustar00runnerdocker00000000000000PNG  IHDRfE IDAT(c| m@-,>|@o655dTk.dA4.D j,F:s]E^i6LS8͟ÓKq&)X'"@ڻ1~5emy ࣒Fl֮,WĻZÎz|Y0hvkDG2]drZZj77GGɖ( ic4Y=͓z7Yf@OJi7n1xJk- `kLjKD2y $rh}w)O\NU2)*GUvbvrI=yt4!ȟW$~bqI>2OjiOeZurQat\0F~wVMǎ"V?WVrKVF/ŋ;tbNavQӻ#ڎW9rU#hݓKoG+˹roG_s.*_v~Tr̻:֡i* D叛|"Mtx{ xZIZ"24#7=7(Ƕ3֯<\ye;xCovܭ6wPj {k{w>U+eO%[CdeI1咬UPno'/T'i '=Ji欟 Pc8nQCXd[,XRHPǒ9_`]<76e'4whZ-!vqY(5;MoO wKK}рy0&Dvfgf=Y'ۈNR9ݲGPH8GaG+˹CEnObL yyʹOסCzJ.o%fYM `b`:Q2]iQoG+˹roG_s.|M_ZOikc:dHdI#X!3StQR6lp:*IiwZbHە\/ʃFAlUخaȆ?. 4z2gz0%ws&! qytJt1-n/ ˍYe譁]/Aqo^iwH9#<ջ1[(+Yc2ryeܩk6rsnn*(,wUTxTuXKUgԓGNkNHR9*x2`dEQ-H|䜕 @G+.egSby-o6R.py8O \_^h"q?K/~B@$sb*;{ +V!U,͔'*'V#@cjqC>eܻP^ǜԸo~"cdfX`RN(N_KIH)XDsK>}cYc,M6Rx"]+NҴ!vX¤j9?Aֶˑχ(/cy#"bYP#6OJ׵?\=M Qb. 6C4vnOEYW`!T+k iw>:=8{q@N"p- &wϠ=륭(+(fh ,]@=tD/f̊+_.?G\?ΏjȢte=+_.?G\?ΏjȢte=+_.?G\?ΏjȢte=XQM=k\鐠9G\?Ώj͙V\?Ώ\Nj2M*][+0Rv'һTԎ5AeBrT`C*rG%w^N ,ws"r9YY\x[o.4eFIG*#F3]]e7D񧘶b xEu%F1q1V KD|9Or)z t5yL21]dOҮiK#w0*=2XZmk6>Gpy:~\^͙V\?ΏEkeˇP{9{2 |7MKkx;DEY[)A2d$9#/Һ.A4'aÑ!l`1^q< ]jwzd7M%ݒ+RqK7$sItw/$,cN P 3Z]Yys9&B|w"e}9 DXvIgwcU@5BmWD ZLlNi.K\w{tϵrz,Vm (X%H@d-V$o=z8 e;yeys-W3 J8rF)$t&h|-7Ĥp"kP11?P-*!0I3N*m.0~2}kRǖI eBǽSԯ,4ȬQiiU-\ AY1GWvi/dN[&&q,s ^sq?tUocC*'=GC[ٰf^IV\?ΏWBr2(py:=g#"KkH<{ \@\kˇ N};FXķԅ 6gD SPƚ,w G m`8z֔kqypnn<{XyPd9ԓQ"S7RѨ=qg[XoK{ VMHѺMר*Gl-mSaop9۵4l 9:DŽ#8X&I0P`qV2᷎g,8;#ӱ#l㻴2 ;X پou#¯yN[}ǭY c] 3*l}o{O" c#̃}y-TuXqg 5%}I{7,NQl`s=֡.a5Գ!]QYpQ\@E(_$̰/m"fTB >sNՓTYaY CPho{O#:_t:~ɋڣ:_t:=j|tyGL=:}&#u3KNhwΗz?G/Οb論χiQuwee[ d":_f뚵`p4%#3 *Lx){&?hk f춑T@ݏ ׶:󢥽].Nۃln##jǺյu-w$h#t_U};ė;"Lb;_ GahZ%Fd$QG=I\?b+>V$aܹsz܌V vF>*mFW>]Aϭ7Ηz?Gwh|tyG&/j|tyGL="o|W!sJD#ٷݞ3jsGqcwre2v|{tzQ{O"Ƌkigf2Iq -2w,TTqy]6;+ّ-^if6._seci,w9ΰ;*o&(o")-rX 9J,<@Pj0a1 cGaYW:iio6h; zJ_]h77)(rYFҸ드tNWmE%tiL͂| ^#mYt_-* b{`(l=ho;ڋlmHYd0v9:tҴыcI'3O׵}FE;Qt]a%' $pxZ:{-ь34%Hg/f#=5^EEbqOEBqcLH|IuDG~K 8ޢH^Wg4q7*1P 8&⃧j1`ڕQ\%By`dqE[E8r+\gyc#W]xI-[Y#a9VA y۞>DCd4On|0%z|3MZLfa} -q}=A |gV-,~}@\+ln @́x/a{;5o^xo^`dX:1?#Lq[j2uk#sr{Q  5ԚK1$2*@#nۿ۱ۧoAu-PpݡEyV<b{}*!Y7qڭ[yVrnEӠ{c/Rы٣ńyKwtyW@#f rsڇLܤQDQY(>uq8\y E-VݕN_}e].$IigEW [IuQ{4jy>sRsV?OWv1FxKVd?k h+h4#֮[lċIu!T@C{+oj;[ܳCr\OLdbF\#[QyJy<ݣ~ϻ3ڟG{4a°&#ri>sF7җ/i+Fخ瑣W?٣ W?=GAQy.B!p2:gu˭Gƺܗ1hP<[2[ f7Zז#\Q4sP8N_%6.H(2!"6A'>V]3Ov!{[yJ-?U2A_:t>>mw"c$ X#i ,h=:ѧܬʮWhAF?SYxB(}IKm%; yvu#aRhxrxNN ,V+n.èO,29uI"F)< n)F~.071S$2mG(AVDqIn6PA`yڽV|%}x\Jn q]}Xݕ~@+;m3xIwyg-ma[[aӿ/h٢;B[ƞdxg!Y0+z[F<ݧ.nztw'*ʀ0* a[(~bF\#[SFخ瑣W?{YFخ瑪:_SD2FVHYA0@WW\.y-s#4s"((7R~\qGa "RxEZqA [ O#gjP@,XbOS~m_Q}WK/w2C`<4?t8pu Nͷs `d+\7#A܎8꽣fK? Oegunhdy:9;)UO8 Mihcq̏$l]]07P n{ ׇ5$$b&J 搠.G`tZ4-be}G̋+-/un1Ga\xr}Z-En/!4ql+RFz$~X;eBXyh ;3=I?TvSӬ#f:nŧFb( 9ڝ+bf!!*"/ |h٢կmq ov|HdA @۸y&-c(UUGTpahw"N[^[.u;ip|"Ft>@ w%<,w241ʡf19{Ga4l% 8-A{rP7m3Okgi#G35RVQ ޥGKH걨 g#"-fY+=3؀Č`3ۊIk}j9cC`Tp>Ăc nH2s"բ Gev*}Po 3%ьvtO'rFhki+6b@dGKyZ-Xڄ&_נ=$E#⼸N7/!`tP N&!4` deUڹk}DR ] LIf'}GSq#0F-NVPyR`sK0E$6V?tʊӝMѩۓ8Q{ֳA$f 4[2$$r;QCqm-ZD3sή%\a9#967I3X:7F8q2 CcqG&#m+YWbF}%oyyr0;F9xGnԞ-CU&M\;\siELxXC$*r#uAQtI:1jBxËs%MpxlpK+80wL )8uSj`: eymbOoE"[Ewe+iƮU7*8asRI%sghKAX;q=CHtk:̢g4QxγbE4qIxΐꠓi`F2.Ӕ0x9O^EëXv{P]Z* )y^V%!x I gzVjp悻:qZ%e`Ӆ嬋,Ќ ?`xOB4ۛ{G6ϔqr5f,Z*txΏaOf>9=1֡T{f fSj]n)+*km)dH$]?nq +#QЍLJᶰKymAcUe9;9K o=)hݝM ͸?*miMyx-~C t.}Ҫb &03}YOtTU>@V;pA$ jvr;^"*r/N:lwqy5XgPVtlCWIlkkb1F{bk{i7ɦj5"HrXb Q uto!{x.&yY +G'ȃ ֱlqY5ǿT}Z-sLmZh[chRf:JoZ3f<Ș%_(, szzulww>y#c,ɵ@;yzȻ;:XIGeys]i髑o$X`s#+#`u..>m $`7icqU5wEЭIYܡL+0\zJŭ :[{׾mIS#Fc2z!wQ['wݻ 0m ϵ #NlmTyr3>V4ZƞcΤ*䌞}A M_J)hf|c` @A/'+mvK}FKn# C/w p݁3ދ{B4~+motD:zv9/5/i׋i{}eorE,Xp Lt=5T<:lT;qy⳵}/T70zڥ_ O%m,AyEv8,Etv,C[bzz>Ǣ70;5  $n9おG]=i64 $jc Bj?[%aOZw4C+o}y-Mct\VD?bKGحl_Ώ6?Efլ?Ξ:Za̞`]HG>T뺞/2R>Fgn8M$ޢroʑchc9ڣ5 FQT;0F[qcn1hYu+++(g[A#x-5q{t[]T.Xl?VΧgn([7.qu'ӭ-PO,nD>kngAOZ.GD!gHк<?:õ饜dpͻzB%%G+F:WBASsڹ|+0Ϩy[DZ4ɖr8\,M,Y f1ؾ`rW%x۵fzҵ;[(t 2J$aQ#V?šp>V1.jpN2Ny4[CV/fKkX䍭-D$-}hY9l$MĖmuܬ{.|q]m1W7{F%SFk&m5M*WS'N=hYy-bKSy}:< "[%Vjo6?GdC+o䵑 qiI Opn}:4<|BGRc#x4\,xN[HwtRJ#n~>{/aL8J&ܚʓ–xEm4Br#f8ⶬvIˉؼAg=p:0dfjZ=DXk#*d}yp*M:O$'mRt{Tz֢?O}fI'yT 0*\RN1q<ӴZm^z+"P1 ddGwhQ^W!2F䜐H@-nM]Ou̖7H\mW/ۛv=J`@[l̤8 dqh7z]6K4Y%|y z.D6Zon`PfRB6Ÿ;-[N>$[|8ʰ}7@}:US;Yeo*a ;$[tVvV(cvN{~6E<vj;yxI8]J[l6XA2#2Gl`9eIqO"B'$8Һ)<3 mvBn3$`ˌ m0B6{9$۝w0}&!P]'?pUxNT7^1ڮjv#Y-gS Sƒ8=@zU|Ib֒mò4a$$}ZOr"aIۏsנQλ#a/Kk_ͧM5U~I;U_P:Nqg$p pOp; ۟|u7V462 v7@:/Z\L$SnMVdb0vִԴk׵Λ20,r9ieemg$6ҺN06Hʘ%5Շ#Re.%WqrKF?{HIv(WQ?i.*\J?Q#9%أE^˟G\J={$bE4MHbɁsy?:\b{.%sy)H$t;3ĭ"[;x%6ܩbl'={walVy5VXVgN8GڞŸʻ79p1C trMB!7hCqjoxݯL>CI?tϵcF{Jy&6ǖ5G;=zuCkiIoo76GAZv Hӵ_Ӯ4+WݬvAҫԌb*Ҝ1|9kևv,qܶs(bGh֥EQy$ۜGZ.% TS8ӌd[hsy(˟KG%أE^˟G\J={$Ta0lY^08ݸ1ڏItkvM kA&7F2tMfh^'vL3E-n+xs„83Sίbйm%d< ?)bZ禽n7e{oܻH6.U7nBcxxzlMDmݵ9{`-6-E8X` h]Ñ!Ĩ2~i8^y?s=ޠWj7WQݺ<v,rGӣeYrJ8I<-c췶/p.$V1iǸr>v%ܱ\4Ͷ[#TiW1€6ӎGZ]j6ZO,3[Rd;9+Ӽ;oa%Ƒ1 #8&u95 {FPNH8snL߈"fGqdOy8 <ܟ*?4eݦycG20̉ t۹3λ(QWQ?GW})[yTgX#sm<[ pĢ1iFޤgX:m# D<R ež`}52yI"i`9?esj9iua4׏!V?.QFT>!WG㵪s Au|{*P;r87de/3hq o<_\y"ʬѡq>G{H v3-J1a!#kKYWY"T&EQQ~XxHh7=~ϤOIl-Gl8C=ri2;-~_qoC=f6BvտkW7)g$JVL^BB=2 B=ɭiB&-:1 =Lm|WL^ʷQy;XO`;v/xixZhL4)T trH9OS_m52 ffE#vپ~XLjKl$R[ ˒Y%&d\FsV!o)]2V G@<[@i$I*Op3G#tE>k)u'_؜Ė[(v1'$-t*-uF'D)$̮Oul֦⁩ x3p=\rH9Ѿ19nA^Kc9>r֐[^n,[Kg), #lRv6=:O$Q];eTmPG$D_D>%e$m۵{Bv܂:G*c~Ck:/4ֺe  xr >j1ZHX)d*N*xU 6NGMrH92B^coy$WarȠyBdTOW<mIIq)f29 ҶΣ8:Ra΍z+RDԟ?*~A"lQXړ'GQ&s56e $}VXB2JQc2kcֵ]76No* Co&`H̶̜mu23Z{O#/gqgyYO30;:cPҧtP[]J %TONGC|YϠVгAO)aO{{Gg $O)I<Ǵny0#LJQ¼֥cYKPMCZv '+œ67+'x-mJ]؎@5_?ke_jdi3!VF0O9_Ϗ]#M-TI D$=r=NpGG+i4R y$eJ#N5A.;Kv|P_?36R\\Wׯ\(Up8ZiSArTyi_ ^g9Et~Zq*/a9>r*7 r2pFI@y0n"MJ;ylc0C.y'jS_z-q,rCm:HС@rrF:cW>%Vy :}|Ќ8îZ<qEm-6Yʗ$g=){O ~f?<)|  0A:M&%e8^07v~r Ȯe&u MkEJ 3Jsz"M<6r(*Hɑ@xWU5*ZV pz3?-?Zq*kirTyiAuD;SbnIߙ W{ڍ2QMFRKV S?**ry$w^!6oll;O]o8gǾ{T>Z-[;5ɜ2+v&gcjWw^9"c@.{럭S4GQ6_Xc:,rvЁVin=! THzdu]CY]%I$sFa#D6.ɻhAo.6 _0ӁC{ 4{ ԕ+02e|| 7g9Y Ɔ }r^C\&h4Oo F IF1Z={/2/tϳ_e#8!ݟU}_ET,D"+뀝 }-n|y Cw\ 6uj6c0J糖v܅~{Ngr~P[X*qjّQ 0Lg95؟*ڌZ:\Lq\Kfr 1˯w#jf~Hl m<دtqsp .2*WryϠPu;6JgtFy]m0Ej+֌/mm7T"*Ib gy1'Rӡڋk1S#%Qc}.4KkHeYcI'[T#NK,gFJ[qP@9 ֥vIRAE0pF~l~T*lI<vJf=ڜu)iuXsKe̲fB=SGUV ?fi2;W>\D^} TvUʂgryϢԷ-Ƒo% /wy_0 >nX~+=i6 mO͈$p-H6(e''8[n=sh(hpIB~d'TFۭuaQG]Yo?*>m=GXTQpM7IXM/e1>[Jh C5ۭ^r*(T :I'k7qGNu¸x:䌟ΙkwaUd6Y7s*򣁜>whβPKY&9̯8 HҩMhs"C$ Ȏqb2z~[.QΰHqo('=qu5XnƮ%C rAOyH=;sj%GY*!@F?SYP$2\KrEi!̊c~BvyzsW-35MNPoKRP@Ym|'-Av~&14Ӱi&;`q(m5?]G-4j,c_W-y{KA'PGxQJv;@&6j9$y5)-C#g,ylqK٫\~ꮴ*PVI!*CEp1[mݻ:W\j!Q^izZm$>C^u0un**/j߷[QQo?*¢dfۭ7Vuq4rE,1V :~RLf\4˳ L@ʨ cp#} 'M UIhw¼+QA[}mOh-c$םޝLϢܬehFvH{y@aRr[q5ܙ"N0ӞA<3rh+ #OnS$[ G'qe N}?*ŅM2MvJK9g9v=\W^`n;.nW ̿A*LJbwwqiKoEОekum})@t ߞpۥ%Q"*FO[gmsw"\i~lʹQ5xn[]27,o*Y!DybOI4{5`~UDі0@hL$egQ p}WDQv*QGz˃GZ賞huvTR9 `?t'\O]/2Ycr.NrO]cHܼ1(;?T aFGG\w"֦-J/K6i"L0;ᱪi7Zc;RF11[ vN8#s˸rG/49~' 2s0 1V>;G[kY5"AŚ,{{- ~fM'H-E}9_6l<8^Nrhr#:PFTtS P- ޡ%t\ݽqƑ",R;̀H| 99"ugj=\F/13 @a8ݟ &2[X'̹VŒ\AM-5]4I `:}zB6_imjF*񛴌K/da)#'qҎywH: Xߛo0[J&rT+riY. /o(r3B ddsxfGȼ;Z?H\. I |t_Z֑[',C1HI2Ǟ%_*?Wzʹ(ŚvO,$\5 x68pzd\r5k[ <2˖. PBgpt?+=OGJSVs˸rGTd?hG<${j7N9GJSVR]Ò=򦾙hQA,̀5L>Y?z99#2+6ukqܙ~l"*^$r~b7|ߠlOMBnd7+ we 35kQ%&zA pL<${$:LZr_/$_.8=0{gP^hw:&=Pgke<xj7e_O(p.ߗxIetھOnʌcR73g,EywH#Lwӯ#4l7{j"NMQ 1>rh7Xl&. FUrMgD/|0 /$ۖ I 2=jʵZp՛ҩUFf^ܙ">pAjU7w y溺tTSX4VQ3Wzʏ+Nyw1c;%_*ѢywH0YݻjkFx=~ui7^@~}3Cxĺ[ۍBh9nJh>I!&kf7m=cZكy8Im4oesbg)% dGG<$J+=OGJSVs˸rGTd?hG<${+=OUomA ^C"ͷr@ArGJ۬Cu=^ʗpM4jQRTv9v Q.=58}"HIaA*y4cǛrq݁F+<[^CL=x[q 8MOx.novvLvAqaS]Ò=P>57=21Ϸjaie{fܣ۷*cP{6]v챴8O1hG#Yt N )lgdyn.əv#ܠ ֎ywHcGym o9P󎽹z>3IKeFPLQvj6ZtZDF"vkvAe|f*`7x4`е!awwt. ) gvFy ]Ò=Kwu8bw 89w5;_X^V+F05ڦo4K&?}$>a '@+NV6r ˏ7,ߟOAڇ9wHWYGBCK}jE^XV1 RŊR=qhvQK-orghq\MW|^Pe.['=~B \&5Zve*,YAԓ[F<izFGVrqp~Iitsv pv0}x BҭZΑq2s Uj\7w?j+m’Qy_NӰUg Yn % #z|wG8Wѡ2q-m3L5otQJ~o0h_5\=Gz/Ki\xO[n];]F2kM奩Q5&4fe:qOj>.d[_,Nc39j}ݾlI{es+ec<j= b1sKY%%An@c#i!$S[]] o!H#zs֙yw}F#Clanv7n՟/ԡ{.6EQov4{A+IrTn=zt-'}^ A'#8s+# )eh [@W>rO\qV_ \s:\ʛZDݕI'|/)NJA#ֲc4Cow.pONF}+F;[Z2쑠P,F0r:ts\֗}U4W r$6HAdNTCa.cףhoQ*T$dg㚵ˏՙzv5܇̞V%6PZgy?Mry?٨ejgOQyT!{ċwpX+O4lH%ϥ7ț?h_lG.?S>7ʏV5Nju$Sī)̺(pi߸iuZ4̋yR8"-PܻH-Y$5nv vRj#|G4ig,x) *s0Pk˘'Eo-efVfXNݟzc۟ iu)/' faUn\M&#xQ E|g,>06qJ I4mzU[#Gt'#8$(UK[*y$~b  $w:^6234V^3V/X²1teԃ~Tzؚӗ\%:L*Rt*OPj.?RhֶW!+z;S~7ʉRNjT~\f٩gy?GOQhyˏ}{53'<*- ab&Id&6>w'R/0}H@u+`{{MCP[ $pц(|, їH\)끌1Y𖓨j֪.ii]; J(Fs*8W d}#ONB\Ef,ʧyr=K1)A4oͫ%*8>e0qcP(5 [D[fI ml* 'Y4FӖvd"# @ŽQ^^w˶6Z$?(Jѷ@eyA:\Ijczι nnG=Eu}s9w;^]ClL1>@ .yO(zzˏ}{53'<*Cȟx.?Qˏϳ<'<ˏGT׮ʚu8P<-yU跺Pr&I}IQE'?xm.l#l#y"Pjs:[N i%O-u;qzt5BÒ_PC, g=*:@ 3Hܻ1QhSEmXNEqBpC(% bvX\OD70C"ߔH96wZ'D՗W w+`dݍQōkw;Ji 0N:3 ճ *8Ѱ+%QX-fKK[g961ߓt5Z65Ǝ*l! 䏗O<3{ej-1u2lgϥei^u x-R;%Bsc9Y5φfԣӮ! ou-d/5ZmV5M, lhGNrn45? HјT 7$c,m&[Xbxh 򞞝lHYcb%TdgrI4FݬM:3m' `r@18eʆX 0 ⹝/H"Dbh~X~q;z}@ p>U`Ozڤ4hUt Y-Ӯ8ϥEy4δگ". OR:ҳ -"E#M97m;}~^GaГ"kiqf H< &h“" ^3KӬOuqS$J#@ɪV_:2a@wysZL!RJNpsGC} }c}JKtҵЏ 1*-]6Fީqֲ|Skow9.`e2$@~=hJfEh?3ۏF3h|{kcK1"tQEQEKHq(+IJAFO0ۼ)IOCj'7e$7 F3H i~{籭AwK1 E oԳkHׁLoր)jx_&U"bg#<`kNVU +99?)$lHT7ʪsrZYZ>5-2d1oo 5c_6Ra@vp(דZ=Ȏ yU Ň#3kO>KkK)V(CLĈ.*O>??Ju5S((T{2*@"c3ңd[%FnX9w<㞠E5x;ƴ 9ooMi4 ;=n 옣 OAZ%ƥI y%V%h'8OzFjmnuTxmV?!rNE\,n|8-XNbx)cDuF:VHQcގI\v)Fs>?ϵr^/7ѩHZ4lIJB&7@ ʊ;m㴁^HvX, 4/徟ϹOɰ&t ,ڊkuگ5$:(р:|p}8;KhmɸQ2p)u;;Y|Gm EN@=Er~)\Mǖ s<`~j'ˌ3mrp_z/?J寴:]Vi,-^WFvK3(;XujP(+sZYet!9dGyUei}onv*eQ%/ҠKf3@!uҖ}Z$ӄfp"GպqqFN^n$yHr4JY{dWNG(Lch~T֛w7r\\*_4^*ދ+ѶΓO5IK G*e@0Ӝ gagG #R0 wlZ[ 10@ھPjuϨD%w~X .~qnkwwm]*GݑIxZV2Z@Jт$sׂOjm`C2㞼 MQ,}bhenvG!r.602 <^ˠ n%,-2D~m Ȧuv)enK4",Jrkmh././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/docs/source/traitsui_user_manual/images/text_editor.jpg0000644000175100001730000000525500000000000027164 0ustar00runnerdocker00000000000000JFIF``C    $.' ",#(7),01444'9=82<.342C  2!!22222222222222222222222222222222222222222222222222!&" }!1AQa"q2#BR$3br %&'()*456789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz w!1AQaq"2B #3Rbr $4%&'()*56789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz ?+7%5}:jN ƥe-D#x R{J$-nZOBj+u PEF>EmQG,{_#R{Oo"#R{Oo"=^/Ͻ7GϽ7[TQsԿԿ(`׹j_i Qj_i Vrǰk5/(5/+j9c5bCyCE1|K}?!ڟ٢b8uF=륪ٖm~+jJ\܊ՙ|+kL#,NISS'{>Ҿ _%s`RoQn:Ia>6K Z17n?ZTGU9ʫܪrroSA7JZmۿb݌gMQ̰oGeߥ Ʀ&U~7/> fXύ_2|mgΆ翳,?/Qa>6Žx\eߥ ?,?/Q:+̰oGeߥ 9gCEsٖm~(̰oG6Ks,Ά翳,?/Qa>6Žx\eߥ ?,?/Qg_F$qG b8QE_ŠkMC袊c ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (?././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/docs/source/traitsui_user_manual/images/text_editor_integers.png0000644000175100001730000000563100000000000031066 0ustar00runnerdocker00000000000000PNG  IHDR `IDATx]=eE]EDE\L5VhdfhfALHпlb((*f5ݯn߾GuUuuթ3}?ڷ7C<ĭ'~x Bsc屚]=䇐Wj _>}9)&9,(rgF] rƏ{ϿXs;\ܸ!ysggw! u]Aft~zTog@Ywvt?ɹee2ˏGYFz*{GׇJ{'p60F:C`>2ELnXN4 eJJVOu/ᇔjr7mmhtUBNOD'|[!?y Ly>@ܓ\jÂWLNJ6(V ;Y]:XVMم%'3&cV 4d evL|94`&?Mы6+J3G)VȺW\#h0hdzD~wм8&dc5:jD J()n9\EyQaUDCLIQ"ƈO;hxOz刬{֖x<>q@$TXG'ZOkWPBT50ӳ1+P6hYw]#-Rܽvѕr~Zȏi[*㋼gUN(I^5oYo;]~(ݫ c/dLj<cT*N"p ))dD r9thKM~enYn8/w7v_h?z:wEfBBl)y~lh?]{xO1% ˸m}/Cyu%UÉ䨁nߏpNr4ARd;Ev?1TtG `:L`؋hj D Ͷ?%#[⇬oSDNȏZ ,m\ 64zrSTR&:7L"/OVˏ<?l5@D=& urn'en e{V+ػN/C`lϒ7Gzǎ0?d9!* 1T;C~TtP^ QҡCz16?Dϧ[c\Y{ qK lTϗ^zEgoa!?XuK~Xh~82?~|X*o1Æ<{غJ3,?@bC<,?RgЀz"%o1mr:v Eȏ =;2#CjKW4Ey  T<{[Yax><'Dbcf/WC A 9X*pX֒-"9:XuD s(̐ާ*)]yDR~Daq_q|& J ,GUVo0<3ϵǠCRDp**nc{gɛ=޼8Bj{)q(4ۿ1s*!޿'H>_quj3djUO@5~hbN`J`0#M;Α:S?Vx_0?ʲ'?pkqRDɱ"G. WJbK402= ""[rX=RĒ8׷Ipb,ECQ5 #u?]~}=Q;OK{^LQrqu=Aq6b m(~}ӓ/s[5EjN?cξ{&?HG~wo9\׷m>w6~ky6HT.~rߩ  sE!hLaU DygZσ.(A\c Lex 6DmIOb&tfCXw}+_lr Ԣ(u 'r;nNkFcR'"vAfGE !)FnjOl. >;s :?DK@#m뗛^ :D9CȡC[x|;km(A ?H"pmD@ ?H>v'Ь%?؝Y9p{/IENDB`././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/docs/source/traitsui_user_manual/images/text_editor_strings.png0000644000175100001730000000707700000000000030745 0ustar00runnerdocker00000000000000PNG  IHDRIDATx]=%] a6e(JہAȑ3ٿA8Q 俠(q(8eZ\{^鞚,Cuuu}:{?Ӄx^y| xw/_(~Bמ;geBg߹&E&'? &МKAI /Z~HOE.3g(G'gߞe^٬<{ⅩMY[ç*G9^^zI2 4%ק"]'mUk6~f>&/h{, #=s%+=TҬ,lY[-}RH_gZO=u\|/(06+02E <(!(sYqcs{uP>7_=C*7AKd YPrcX4 ~4ot]~<~1)oOu_d(/N?G?R6L_јSLb:j]S/yѠŏ^;g<ȥ9dB@#xPB QB':1pߖ4ǜݨ|}.]`ױS,}8%j\ϯzǂ,{KwT/3]g~TnlO9}i?ϢǿQ[/ق8.2~ %^=4J/)O!cXKr5t(N_B /F554zFiffJ{,hlLrnElȘ\tw1Q.OzT¹`Q-ƆB1!]:*W7k- 9!(:`mznS,, wlc0 R/BMVaڃcOKcK_ONZV~͞!5H{c4M L8PO j8%FWNA *xZ :$VRs`eML z?=\=НE"GvZ=ED1´uycPloLW_X.=I,é$Û6nwx{RAB!&/SRW4ۿ18G6M@l.C+!Sz1,,x.~bL,1C9LӍQƒ@>XWccn-94E@- ~u9.!-ݰHCC)f 9dyoB {n2$ED6~Է*&k߮N"VA@S 9$?d'aN?}F-nEzkOofG߻jk}lP}\gfCcÏ5-}V"uM5:Evk[uË5\ml#m RrIs^5A~2f[bPqb\H\YB  -Hrd3kKL37S!c!d=K?ڳ6kD>zH˖,YmRN&)l^2nmanSBLmufɍI4/FbbQ?`<nr8mB?tX+iw`xv IkOafXHSo֦T=2f4ӿ~F>ɏY,՘۹%GO@z "<~q;fpcC`= DڼSi5ˮO˾bv<fEh+{kjꊖ+v_= EXO6J[?"K1vQ +@ВK΅.-?7}0 A 6)IENDB`././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/docs/source/traitsui_user_manual/images/text_editors_passwords.png0000644000175100001730000000532400000000000031455 0ustar00runnerdocker00000000000000PNG  IHDR IDATx]3"hi`'dP"㍗eU!6_"huBM1AC隚w{x\N3w43/@~?|_R}jN _CXC.[}|ۄٟ.L(F )oGunoLB׶+o^DI o^X6uW/o7M|H{Q!{iipg* ?B0utss(2yd ZW WAu~ccAnWJo?hޢIjunJppo0nbEV=k9`Ť?u S2ONyXGpHdG O>YlXȈbX9 f2=D8[Z*6~Ȓq˫ :"@P}S՘ 2 BWn<:"'C7y3?"(>yъ%c֎ VW-/,Gܪ=IZ{2VqrЬΪ+W[UO2a9I2Q:[9qˇoJR/ud)~y>0mld9ɬ:aEyQ 4aE;QdD(!Hs L" uOΏ =Ƭ8*V/Ts~I yz({cK~ѧy(YT:_.7%Hng)GojI PǗPeO3'=|%d s#ѓ< ?umŏ<{e 6QMy=4 g^v|4 XB$lKrNgDV,#Ph@C~ʘl8~͋F!.)_E}]@+~ڪD & uRrF ?%Du}^qʹbV[QiL1Ǧg gbZwDGuH H~LŐ!* 1U;C~TtT^Lmϵg]03ďQ;gC(yvȃRJ<(!Cy!@~xF~䇇m9!~cnT>\7n |?OdUDxPF~3þsL1h06'?,Tn~eq[/"ΠvlѮ;r(!#9,n'E(=LO h3Ԫr;M)9䀂5AV+"kNi 4[#^8\JBɞ=¦qp93k5  EdhMU)Yg@CJha2%b;zu/K%dC <jx\Ez*~ۺhScӝsա2 ?lkȏjPNՊ"?A9e cʶV+唁Bo|5<;M])?F4JftcN!J) w䁇CAx:䀇@'QXp~.t|?GMWyAIAx1ŠxdۜG4e5'PXNO;M)C$8"^;Uڧ8@Hp| pJ-8~C֦C4jOC scAyX R<B'N~L ;¤|?T;OqU!@~xF~䇇m9!@~x~sC}!rJI1uQ,5C}\w%yPJ%dC <rCС <rCzVTOl !~DhH@v,:.5A>hl+fgIbJmN,Rê)\ Uª]Ʈ;?rl!k3*Ud5eDsjvn!@ r LYNI搇ͭ4iP'&vr!\1`)/Jb_ -imըUD!~$]hwS+|=&Kێ/`n-d8.M~䇇m9!@~xF~䇇m9!@~xF~䇇m9!@~xF~䇇m9!P'Oy*ɏȝc^z|y9 g!Bt9MC?B3t&ȏ3u{{wY? wŘ{+ń5x$IENDB`././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/docs/source/traitsui_user_manual/images/ui_for_ex1.jpg0000644000175100001730000002404700000000000026672 0ustar00runnerdocker00000000000000JFIF``C    #%$""!&+7/&)4)!"0A149;>>>%.DIC;C  ;("(;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;" }!1AQa"q2#BR$3br %&'()*456789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz w!1AQaq"2B #3Rbr $4%&'()*56789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz ?kY77VŠ6`nO#+]ޫ{8$]KhhO*.X-|)aԗk7*Lr7~#88֣_[pOes?ϬQ+CMc}w ť}oml8<5g:ybFk_+|ya3 gx;T#&aj4F7X?أϬT:vu^EgcyQC8 ZÚV[zr8m9uN.i=3Tdo}`bB/? ꒹TyĮnb #EF-%H8Rx#֒Ú^b[/~ˉ"f`;[ڧQ?cSk1ϭhE>Sޭ:+-VvcGTC#G|uӢQ߱Z];SluZXQ36Bszr37h24k$QR{dx"*Xz1ϭϭGGY++*XnRFA#T[GJ883{JVy<{H3INv>y` ww>wڭ95Mws!$59MqZ%G\)C6Dt)۴ǀ 2OL` Slt8!hTq8HdIU, oqW*IԅhӖYziCui.2yBH@3k<%iw5]<.pq%Ӥ>>o$mr?xbg%{5TXji2tRb; x[; 1. *25+x}Ho5ZZ-ȃBÂ`b26Ardqsڵ^~t'H~k^tE:p4Ɵ܊N&RLɠ$F06ppr26i\SU, U7 ԯz޿?:"Wz|veZ##Z{q'ڍĖV>1g+$4%?RqF.ZUӀ~?ʪ59N1SX["i-!(nB*w2'Z u>(k>̳(OӚ޿?:"8Yie*`$[Ԩ,RVp/?n漘%Vfrl1jV-WZ$RbJ키.{[»OUӫSq[~^ld՜}[Vcڟ$&33J~..et_lI";xU<8+HFӅ} ](wΟn>>E/ٮ]̭~TKK7WA!hK\/'e5»OQ k;JsbjK [WKmx0Y·ZmԓH 䏜w51L!I7H=Ր/Ի1䯉Tj䎚T)8 :yL]&0|/]Jd}7gPDYTV Eu[*EdLO23֮Xȵ'|4-K_M+ xoD(7 Ȗ+ދ"JHє¦ ( ( ("f5"99`sXSZKKSlҖhD!QU}cq_GS_}cq?7.iOUY_!n/]hj=W >ҞecG>At{_(Jzn/]!S_)꿯VW>AtcGrNdt>J7/k{}:_2R["ۈ\/V3^S~itzmr5HћH`29 WGqM6Y[nfx泂U2c+) N(jVq-IJ(o-pG\YZ"ӭ!mGXf%$OԬ7s'G\'ؖ-"渌Y%݆Ŏal5ch-ZS^_N n~E!ÈOC}gZ;jTK#/ +ӌ-!$Vd4ɤItIFhݛs2pKrHkž-FI%ӖT{uBmnpY$`MI8LC4Ѥ8Hmd1 $d\8&3LvAv=дx$ҬV%۠ێJ?!@k:"^{X>%Df! ;C$4n_v-ݠYE mHRhrTⶅ[D g6WL H/x4Ky@ A i^&5X]_OCn;ynkrx7x# 8 E^K;!%F i\wvFxӬm,t  08!>_*t-/ ҬB$nv \Z -o mgH&6dvqo62v[Ks׷O.&" Uᘩ'jo$7'Gi4i֑ċ,̰(2:f8<Ձ K3N#A+Fps($O,Zm#r׃ĭƷk-E ,OLֵ|&IEG'?Ə9=5WB$G.Oor{ChJ*?9=4y} ('?Ƌ |oC5+!5Goտ_ ׯ|sEiu4j_i]upf?u=K !EW QEq.RX-/7ؖieIO-Kx`>\qi\VwtRjJ AV;+G÷oz($Y2̩<.@-EsV~, {S{`o-.P`#ť$XxcvX&WH$K-^5f%f\28f]Er~0?^f:KM4(U Lgpvdkm)PBݺ6NTϻc`#UK홚)3(0xyO&oF%o֑hMf?Mf?MoOxyO&xyO&裕xyO&xyO&裕xyO&xyO&裕xyO&xyO&裕kk}Ye٬AOvbqP8_#~_f~̾/V3^R)j]i"ԿҺ~ȵ/|43῅+*VK Z^L{s^qj&IeidJ-m#GH]CbA#5fiWCp<)$N$N2NNMX364lPcB B;6fYEEpYDH:sZPu΁]K$,cđ9;U܌F\՛}>c5Dp4,QBonP{h%nVͣ/7R l|,l^F˅WQG51Ui=4o|F@&/*kb>cxoѼz7ke>Ѭ_ ǣ&ѿXh?/Qc2L_UϰX=4o|F@&/*kb}ɣxo51T}X G37%;57oտ_ v.۝1c NL)5oտ_ ׳z#C~ȵ/|4-K_M+ XoD(7 ˲MQDk! ) AHX;NzL )&C"+[M[5X eYgWCr~n+mj:wF]O KRWA-gp8rB[[Nɼ۴ ʊch5Mjɧq)iQm+3rfbNIl)mԖ7v%^.P!\oOW{Wǚ\Ьq** E"$B2̮C<*xVŬ-ۀ4Qmnres r8^ۙo61ǮsF2n2tdhi`H#?,'cbSQ+A%WNc*,<p}j樂Bh|@duazg#"-DɎU[$$}(nOX6K&b[]p3uvE7z}:7)ѽ?:ntoO@?OFtپ|oC5̬8`x=_#~_F|߆-K_M+GRJzB(p(׵o m_;q󯮾)k|9 BEhmWe\F]BHOJ7:f-x5 Xf,yQ0UIc dIڢiz 8\F6vn :+ &ﱸ1:ܭY<8?$ WwMKt%U0 dFqmWMkza{  \,p$ ZЮRܓ|5_z?$ W){rOܓ|5_zh.sےφG#CEsܓ|5_z?$ W( #'jG(\ĵԤ-oI-<,|oC5sFe~KriOszOWMyrGTNW[qI(I " z>wx'd#R[>oo&0XԢoo&[,jQYko7Gx5([m_,0 mnO(O)PKsMjUg qgjǗedg5g#\OZT<= <= eWNkUTT@7;Ef$$io?_?_լ#۴X-6N"S1PuPJs[JV,˙.#Gyv^XXe$\[_?ʙTn-YfgS$.p;sEfX_?>Ǭ^/U\{; 2Fa29ՋxnWRuec#-ž,pğŸenws@Z[J΀X20FC(8 9qU~Ǭ^/Uc?/**^躞d`oȲ$e>`2eS1U,M#SdCBsm;<Dc?/*NQ̷6Wzlv3\Jf~=v*q<M*{D1246ko2UKX[Ax T}X_?.=cAx T,`_#~_f{[u@Jųs}J?߫o(9".ais9 zg1'W8ŻrX?1'wƊ)rG&>$onE ?7}hğ4QG${cO ?1'QwƏL|IAE#n ?7}hHğ4cO (wƏL|IAErG&>$onE ?7}hğ4QG${cO ?1'S`L|IAY7N#I,''֊(Kd4././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/docs/source/traitsui_user_manual/images/ui_for_ex12.jpg0000644000175100001730000002244200000000000026751 0ustar00runnerdocker00000000000000JFIF``C    $.' ",#(7),01444'9=82<.342C  2!!22222222222222222222222222222222222222222222222222" }!1AQa"q2#BR$3br %&'()*456789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz w!1AQaq"2B #3Rbr $4%&'()*56789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz ?Wpm`V/oel8!Yr7any8ش mydU&CJc p3:5CNe=tODs־G-sɣFy.YSOews/J/B( Ҽ(iڕ 6m&ߺ3̀hcv6*.7'|,GoǎFzeFN-gY$ҋ|УJ/Bze޵ l:ǒ@ ]ڽ啭s[<7r+r3U)EM';*3j2/( ?;o*h+!DlP*I"DG-ة$ZKO jG@\\G Ok/z+H;o(ҋ|Щ`qXwiV8d#Ś#b \: YI.bL1c'  =ij?̻oװ1qϝi>vVKAc#~4rm4bH6q}ӏg* kV=gFd9Ib2 5%mr@Y]?八"Ӌ|УN/BhzI#,"U$eXFr;~+x8MsGTc(8=ߡG_m~fbUrrqϝi>vVf(9M/8 ?K_+7vSOJ/B( vSr8]6k'E$!;##>m5(k5[zi1s}'8:WnwRoYVmlrpq<o6FU&l~+$S|8w\ۚn=t54uibb& k:؊Z۵g,j[Z Kۨ,bȨvXC3=_鶏$*&TKp.: Ȯ 5'VWI?Dje? 'ZKZR5cϡӅ;|Q K5l]QBc2;bNMމa,wjnx9G 3Hqsk?KᏉiO"[/J4o?÷!GSub ObS7xlqc 7bB@pr26hYtko'WEET, E 0JoV$/Q ğ?~+S(k_OE'*ޗ$(P)y4y5¯'O"U$/IQ>Rj5u-Ÿ*DpCy8{^&Լ7tv[y͝6oN< $ó2]+:eO&BXUt_bbњZyΕYǑ;~q-m+viGy/,R+EC,;cVbHzT7`g^*x3Ť29Kݘ0s\Y]HnK| ӕw1BiY "5(%d>a^}r8s/1PIIs 4",|UϠiM^5w**FjF}bmĖUEQX|H uMEI`R2dcg>o_ďt_b>Y;q-OwEO{ ^}y+Es>3Z>=𦻣gT.Y`QnH]zʊ"0J\^G՛^F۷ SuW E9iG]|iS!EWWoź-ILjV?k- pLsU4u(i&խ-#LI"JÒ!B隷,Ʀl./@薚IsYk%{e12ȅf ω6Uy"fvGT1X1}J@vt 5  |c2:)Fi&kbMgndIa,cڀ@,.I "b<^}ZԮ5-nn-ǔE̚\a_/n_1;\@urhjyνwh#ZXyUD2F3e2yCs⋛@xuYY[B2W>N3Fj̺LMX\_Y#D8\+ 7dcQ[ m`h^n WpaN2>jhJ1n3ΩVn5 }i?~5o}^ZEs}f >X3WzR,O9I 6ϹPǗ,p7<R>6*v䍷m9_`WO"E9+9QsWO"E9+T9E9(WO" (AsWO"E9+T9E9(WO" (AsWO"E9+T9E9(WO" (AsWO"E9+T ʓzԿ6vke1gA۹Aw֬a[RљGc?ZooP8j+m+V@$.삂TrKH'AtKqlm=W?G3~]z_kOUV'AtaGkOUQ_baGAt{_kOUV'AtaGkOUQ_baGAt{_kOUV'AtaGkOUQ_baGAt{_kOUV'AtaGG7 Od 9eeiMwsOE}VSY[E9iG]p SuW=N'bQEp6wD}=TKi Br2t#}od0隡S%R7`6˅>_ @)GIj~Y[IulhTD|sU;E˦ǣa3omD%1~U^0D p!d*7*=@%W#EcU6ExZq吾iIڳDxdKv-sy"Ms*Cn񒷲yJL  &7;Iqkp>Z*%lT9PP@:T芉aj*TXTv騣g'ZG'ZG9\Qm7dt8ќ랧֭OW6[D*92<GjU#0L@x_Z\ZCi\KHKw(lFzVEmoI1(HB(vפThZ4kQmݖ;p0:ҮZFi!nFc`Hy16T`V#Fb9Z]ss [N"8Lh2CanC0EY;fm<WYm1f m'JKSW[WheYZgllT qoq2zm\,i}=G?Ə'ms>ОYQǞt!s2QQ|lO诬u¯VpV#3j~Ҿq5[<'ؙ2:C#KTqcZZ]]h?hKUs ܃L23\x |UƎ-p/XLWtw_6^~?.Up}RQYGcijܗloq F \nw,!َ2 ^.j-kIam~PKi7YJ|O (m@ ?+Og~13oS?65o Ϩ\Ii^ZQأI Y[FQS>ME,$142Iqne1*2G&s/oI_m'"o~10>#CSx~]mYOI䶑KrwHY!Vѐ[sk_'Rujئ¹#[1מQ P)d?)3x^KMUa]Z5"XCfFjόAl2Aawogl@\1 Ͷ_pjm@ ?+ͷ$ŸN7?}biL_iϨ %,(,ፗ8]у>-I KE,do_6 ?|O E/g~11f\|-կ~ݭ^ŠCo. Kq iQQpOc -H`l4M1_6 ?|O *Ujܟ_X{m'?g~11fWQ P('?=_6 ?|O ('?=_6 ?|O ('?=_6 ?|O ('?=_6 ?|O ('?=_6 ?|O ('?=_6 ?|O ('?=_6 ?|O ('?6>6ȏ'W㟉AM54Ƶ+:ͼͿ8VҊ,Ju ^9jIJm././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/docs/source/traitsui_user_manual/images/ui_for_ex13.jpg0000644000175100001730000002440200000000000026750 0ustar00runnerdocker00000000000000JFIF``C    $.' ",#(7),01444'9=82<.342C  2!!22222222222222222222222222222222222222222222222222 " }!1AQa"q2#BR$3br %&'()*456789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz w!1AQaq"2B #3Rbr $4%&'()*56789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz ?Wpm`V/oel8!Yr7any8ش mydU&CJc p3:5CNe=tODs־G-sɣvQߡZ3xr%O 34DY[n`DAM`<~Kmƛ +*ZB2r~on%r&wKTc,5Hg;o(ҋ|Ч uco kIcá+vQߡZ_ ƤCvyx YNX! g[ZK:4K$1ȒKf0,nZ̮TEߡG_m~3T[qxFYE%X Hʰ wVp推Pqvz3ON/B8 B4;o(Ӌ|ЬQ9Prqϝi>vVf(9M?8 ?;o+3bT_m~QϥLQʻ)ZߡG_k~fbQʻ)Zߡ]7#鴫Y?) bC\=F0varbe81[N$4kQnjYfǧ eX$o:8' R4[T^m~lE9T{'IҠ[j>iCiIb3 )8 kV.-.ݧ֦*%uznO h39^oז\u|6qm^_y-$P),'`;y#9ڻehOk"iXD/mc^~eqddLvvS[Ċ?KKZR5c¡Ӆ;|Q K5l]QBc2;b0mgecv N+٣u OU%GI ğ?Cć^'SI呢ʭw)}yu,[T~|S6WVݛŲ+J$sO8h[xKGQtIOEdfkgcf$ ۸moU_?K°'iO"sڳ%۲'u_VӋۛ].9T-Txo*(Awejw:8 Q\#sGaVGk{?K°'iO"J3U5_|ɜ1M8omM/Zi;_٘q[pN0;v@è^%ږV1Zlʱ\!2sS°'iO"x\EHI0o]UWwmU*2.yoDW!"RL+oY4;$1B2|߼ÐY!N|`%c^Y2 *[((I&9K ğ?2+u5I~#Vו1|Kae`" ME%,H_[+?K°'iO"h8 0PEcR\'{{g ğ??Xx~'Zk3+N3?K°'iO"|A WgG]+֗d(aOE8#ޏ#޻?V$/Q ğ???1_q~EMvI?z__OE8&&?U$/IQ ğ???1_qMwɪ1Ʒp ~ު¯'O"aDvfU %rLBI&+0KծlXl5zw2Mxnm&+;mj\Kq zo?? aW>-Xկ4X6ީn s׊87N{"凫]Y/umpA|~TЬi⽽Co4Hj xOp>l5 i$̎yg7f>9'IW_E]v-Ш^_L?> 9{ u\uxJhݪ7ς)?}AOKnDc?*`}X79ϛ+ͰcVxZ߫oMuMbu䍿) PȰ1Y֌I+晔q8eF'}Ck{oxO.WW;QaBN'zWacZju;~Nd=]8X>QKG\<1&@n $uo>iO{u+zjMw1.<8ʣu293K}rɅ N꠲l-$-bY|#:'xl=)/ϊޤS[j:KFJstZyKKngU,RD_ko܍a>a~S=C@:ޓe ާe|yndqڤ;/]6ȏ'ݖCƿw?sҎ~ȧ7}(뺧T' Q\@QEQEQEWO}TC-d,QY NFP__(N/YEem'W ? `jQY EV_Buo:QfP__(N/YEem'W ? `jQY EV_Buo:QfP__(N/YEem'W ? `jQYG\dZ/fO3^g][ZRM Id9CrLJzv[Seτ)__J:um[P6k&TX%Iv+O"Vu ^Romfhnmuul!s֫4}F1#_LrK$1!]@G؅; SxKk(%i-e72CHmnc½ycqrإgʞMB.9IHHNU;p. e]J"ԯmŪZG33Ϊv䙘W' ht(湚yD+ ŤJ̠F.Te-k427ȗXGebv qٲ8x*U5oReOM.aQZF? ZHwDrKEi7QW ? +rKEi7QW ? +rKEi7QW ? +rKEi7QW ? +rKEi7QW ? +rKEi7QW ? +rKEi7QW ? +rK+xB0z֏:y\U##dzewwڵōZf\:WA_G]K]VK+UcwrOA$WfNcjM}h>i,Z_[M$oeA9Ț zf|K/myXMO_J-zk_lE9iG]yu tޥnZ$ƲT[pAGoi/fyaiuZϣO+ drLQqOzwgiu=܊^xUVUbn L{.#ƷZZ8G lv+vQ\7vŚGqXEy#:~.$p88Č5+{WKf)1ʱyUxldX—C;_ij.V_,6]G!pAF:*M,b弳[VUFG2Tv~FTd/ pFy\ޏ];[vieXcde}wM O͐1lʊY I') G"%0Eco6W!..9fz[iyQe H;I:~B%Q??Ȭ]#zv ֍$O n*NrsF M9Kl1 fha$o`M@Q??Ȭ_]PIX"46r7`qp3V.㼌̪64Cц}A -(TtQvs|<g_4bn&qP!IKzwҒ#rp3F_Q??ȬSX ߿ɉ gẁzv- D|O AV`AHA.??ȣU./-fi6u)w8Fr8}>Yv}~f[dI ,<Z.Q??Ȫ:o.mYa8%GPs<ۋ Ǹ -w2+5|gy* -a0_|ҩj펙pI1pʤ ՊA8ppj"9EGP\^[k mS #pqF<}(ߜ"9EGE`I??ȯ4Kx""z'z +}FO2%' 8#yrzWOVyi7m$4d 2vpMm=tmd$ *<<;qN8%i(qx5w25ՙa1Ԕ ڿfcҊm\|!ffy"\!؜#*۴Yi+Hm [X d"I-[$c;\'5[o7o7/ܾFڗ/Qm/e[(ݐ X9DjBL1\ax NhVRCC2Z6l ) PFPwIo7o78/{j_̾B]I􇻹lV>l= I S`BTiഖɯ-; ʰ'yFC*,E!|A̟M|A̟MQϹ}Re$.:̭,Ry"#Ä *0Կ}KGm;UԦK2\DETc'߁\޹=JZ/cWh/$ӥfMpI '( 6##;|A̟M|A̟M R{j_̾Wui6ԆOKFQ|:p J8fBGƻ.[M"Gh`Hy&20P .B9, \,n?'G,n?'Gq__smKtZ Qe-AVR0;מ-…\,n?'G,n?'M`i[Kڗ/yhc4y{O̙@Wºحiut,w-3[6 pM.˨ܬlU72Kc! *hw24w24}K>0Կ})3so%@!i-IJD#!Ѷ/l籓XЦ#vb;nf`n SyXڢ?f;qߙ??f;qߙ?>R{j_̾fM:bXlEEVl$u`@cg[|A̟MqLV$ Rxߜ1yr^I?u@g/2ǿJ$ X՗,m5ꇱgM o8)$4ٳy쓘̫,4D󌀿W Je1U4Xyp/lpLYkH}V_+ҍʂWg,]8A?|Zt|]٢>Mq3Ӑ8w56 "ߍziw52>s.i:ټuD ~FѡCso7,֏ ?s ױ[kN'cΏv}FsǏpQ#&8r;W]D1xf-5? I99rg,]ɑ>)Nso1i|~ޱCECUV40whGz#` |)ΚO'yU]s_ČX vjk-ZNݥ;5>;p-zmk^ӆ{{B%G*z KÒ#|^9+;G_b05]}W>cfᎈgnHyivL|3G'zjXsLe_{_ $Ӛ~^7x b/]f=~5kμJ޶uǬW--fƯEـZ6r[,bL" —lZ7m'LK}>d03/|5[E ο?˷6=z'Ч^g13?|ݨֳL`~g Lǟal1#e0uj;h9m?+,:Q ˉɭO?/x뚗'}[n^yc1Bu%rBjZjҰ"-IcN+C;a [? } G9{w7.B6ejmOx'FDT.> ]J]76VQ=n7BDh&&YyL2 }#Nhi-kkOwnmw{e~+FO5{34Ԙc^zM̓xL Tv:Mh{[`ph;k[l^DG͗kgcdLECWm}4Ik}}}돯oR?2c;pBK}ņuOnypÔf&6LP4b`'s+gy2KdtYۘ5bF&i'? 3t'ƴH84@K)S2?S~dt0PaNySWlo߱k=ؑR3m;)¦Gn[vy6=Ck Mfzc((Mt_$kN0یdch4);6)n90݅ r븖&q"Cwg]:b:k xKvy,[ogpU$"Hblji6Od9qBP/^Ll0hg 5w|B&bmtN_xGXO+/ֺHd/!g|~p /K|GԘ;~iEA3_oXG}"Kv'캜hOnfn˳O3_~pX}ވ74Bk':Ӊb0{$)'=K7&}?8,]5N X _4tZN5#@ER/x.['v-+XnJĨPzT.҄vmkbT&a7mLk=͞îh7-Bw$tNr3ohx`%]GRuѕ TY՜R®T#n ? #_S{j%1!;z8PKbB? yC 0s&1Ap4_nm߸.J.J? Hy8;cEI e{Rkm{hRC G$^BkY@HlFg\-S0TdkI*D _GvSd"biVv'^r}~"kMg(@H8a0Rج%'Z 8<6I;/ Rq8zHL|rmT%c}M̄&.Бe։UK:%yB21 XhԮԧ&"7gN'Adی4d z0֖$k'e[aE21t@H&ؤ*BF4ʇ@H AI4~a J zR {֕ $2GފًYBYĚ8+obĉA GY!gq^Ufëd̊=)k#L (SWJ?lYrX 8K8yL1e@Gw#')<H2.y@1ǔbK9͎1~~?<9h4Z2L tno6}EXD"J 8vwm G"m> a@_@L C \Ð>w"vH1Xx,i-zj a gߩZ'sU1$!L5FT1W[ؐ q(8Q>؂JELr ` ѱBV.́~0JTβ -t4*A-9H,βm5̀=4".|DSƝrYH0c{H_3P䥡4A^z2q:~7-Q6x[$hF^*Jb-Wrۤ$NعJ݉JMw9 s^+5*R]JvxPY7(x[lm[XB8k  H\YCmmpPD@"bnC$n""qsцAvD=Vnk4Ro:H/DaDm 0I183$y!Ł7ś dn}}-z wЄA?u hjcH@EnZ*DtK)zz]dYu%Զԩ@fbȺkYTD;OP)lւԒG-w Ȃx՜U M4(HE(l[/@H`άL K;L&Qq<ʊ]Q`aѓP[Rn*c qD.TQɬWV60̸zdNhm鞒7FH[ V3/L&kĩ؈BLv&sG$26q0o rg7$z!EŒЈ^{?{P ݊h% ˇ+(<~F"DZ!RѽgU|$1F0p,v^hIjfx-gPwAb$Ѹ[?=AMB qnvĦ,>vxP] jfb?NZъ] 4bf,ˇ@_LJoݜă0⏌Uep]N P0(l @!oǍHaDm 0I18'^?ETDUiv2w 8Tӗ;cnl]QYJJ -zā)%ѲVg6;ܔ(8,ʰx$v"Nv]EmXFbX[8&Fc6YmMG'U6;ϾH) l85R!)@HFp7S{̦Z N!c#L<} czbg5-HRQ @ 'fcM&e qYf+C# qG`w@H"Z}~odϟ<>7rมAd@CmL .ɆR  b1:4@<$K>IENDB`././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/docs/source/traitsui_user_manual/images/ui_for_ex2.jpg0000644000175100001730000001371600000000000026674 0ustar00runnerdocker00000000000000JFIF``C    #%$""!&+7/&)4)!"0A149;>>>%.DIC;C  ;("(;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;o" }!1AQa"q2#BR$3br %&'()*456789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz w!1AQaq"2B #3Rbr $4%&'()*56789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz ?kY77VŠ6`nO#+]ޫ{8$]KhhO*.X-|)aԗk7*Lr7~#88֣_[pOes?ϬQ+CMc}w ť}oml8<5g:ybFk_+|ya3 gx;T#&aj4F7X?أϬT:vu^EgcyQC8 ZÚV[zr8m9uN.i=3Tc?"[(Ћ}mة#ީ+HǙJ ;tTbrT'=i-9]E%"츞x&o%}_}56߱G_o~R)!6Db0TƙƇo~n`Αm:9!C϶kwh^KJQqQN2Mm*5{wY3٣!=U9<CE̹?1[NQ)Ny۹О0<ۍ- <[A D\Ktnt0ijG45o`n<jFl\XON+ZŜLv 9"L!2=*rhs3v6 .V:˪FrJ`r ~F?#}G̳LFiӶzy~WB9T{''EA&Ho5UIjd d7"+| f|ʅ95EWmdtE!NJ)FQXo\X%&fdxBF7JFVrK-yb(uB 6;NwW :T䵾<$QSڀr ņ4}{WWn$gJ81Kn_r3)N$DlAھo;I1ƌVd!A=:Zq\UǶ_EmUq2#7QO[TUo:O"F3+j,o:O"F3+j,o:O"F3+j,o:O"F3+j,o:O" [Ber$ץל|eR_%u` RJ뫑i"ԿҺˆQEnQEQEG_JW$6-淝&U-ۡhUNT)鎔&a&dr-nrW' ⹹[Tl:{HdM&FNެLʊQ\ܪH$J~Ҫ6dڪt- ʅ8@=1Ҁ97ĶWbuXt%nA.pPĐw/ҼKIuqiieΜr]2F)϶<,NDT[+uUX@@ `qO#Gi}ӭVyw}p%ʭh_/*4ƃ,qkM_v2!@8&]GQQ}Oo@IEG'?Ə9=4]%<tT~r{ChEW|eR_%zό A-z^Ou~MEiu>NzE$3mՇT^շq⻭6M/쯨^A3%&$E܆)!ːj‰\(W&;{gif+p2T``̠cד+"]12F-piޡYY]okV-嵴I$m\iY{m|f<:n .#-0 <iPĮNpE[xTDDfwsKnc1rrʷ^ 4mRSUKET7G)fE@%Xc4XC(J~ӂMȡ ޭǍroV/@g_L4}_L5E_*xyO&xyO&裕xyO&xyO&裕xyO&xyO&裕xyO&xyO&裕xyO&i*x.NS7Ifvgzmy_!RW^ %_K|.K66w\BFa׽u2htKv!)>c̊drFk<1&ú[Y9XK*:ֿ.{?5׌b*'8GF2V1ggik mFehdt Ɗ< 0tzxWGIao)1edID-h@P:q\O.{?4#Oc\٘Wzg6aqt2Bݣw +3++i`Q>b38\5q d9z9R1\.?4#OcGf/aIac2O/"J;)s1%d䀠 *֓:[kI"Y ݂@]{ o] ho@GƏ_+="YDRѰ qVGd4upBG ?5Wgo|7FMy.?4#OcU_>={xoѼz7ks'=?mO^=4o|\ ho@GƏ_+S3׷FMǣ&=?s'?0 ѿFɯ!o@GƏ\ h/=>={xoם|eN_Jo@Gư|c/˧f2tapXV%̊a(&././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/docs/source/traitsui_user_manual/images/ui_for_ex3.jpg0000644000175100001730000001646400000000000026700 0ustar00runnerdocker00000000000000JFIF``C    #%$""!&+7/&)4)!"0A149;>>>%.DIC;C  ;("(;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;" }!1AQa"q2#BR$3br %&'()*456789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz w!1AQaq"2B #3Rbr $4%&'()*56789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz ?kY77VŠ6`nO#+]ޫ{8$]KhhO*.X-|)aԗk7*Lr7~#88֣_[pOes?ϬQ+CMc}w ť}oml8<5g:ybFk_+|ya3 gx;T#&aj4F7X?أϬT:vu^EgcyQC8 ZÚV[zr8m9uN.i=3Tc?"X?أB/bzU"dq+HQaRuy㈙m~eg_o~ϭ|7NfUX$>p3F%i+Hpe;Z~Җ˶Eً߱Go~gV6f.Xep U#'THEyw՛ BPno-P' kJ;}5d mƥ([h$oEsShịO(eYbrO V̗[K'}.h ͬ7h[RHpH[Un9jΝm| t^PyU[kg&,yW23ze<I?G?rN($.Ԋ&D]DJ[ont͒7x#VG1F}Qí?j.wsU,c{4m9o5[qZE dd6{奟c5q左߉j\~{hqxqQ Ek}Tvq|lЅB!ƛ7ۗ3f o~mO^ >cqױBK0µehy'FvVlw_CeD0 N;y#=?JDwĺd1ʉgPS\w<:om'DFDh=zW^tMtIFumѣͥB-x(m,Z^9YpL̊.J;ϴi>6Ardqsڵ^~t'H~k^tE:p4Ɵ܊N&RLɠ$F06ppr26i\SU, U7 ԯz޿?:"Wz|veZ##Z{q'ڍĖV>1g+$4%?RqF.~2ǘ/4EbI\T# UUQu4?ъ?6-~^G̼>aS»OUӫSq[~^ld՜r.(ulf& 9*5ۥ6y ysWE _??G+~tEt,~[b؇ [ڮ.Դ2*Y~.@+OV<%>*i]ynp93= o-X,<^pRz|g??K)p2[uFUWi 5fE9dy+^=FWArα>(@֪O[('M®0 aJF Ա?1‘Y1dW<^m5L(Յ+fdPDZ{+G4 "\F}{/Ի1^'n4x$Kx!靑ވ\Ni*S[E_2+ _j_gHf) moJ{x;si,<zz ¾hUV%dbLDIM)k䥖^:VJNTe{.OMN^|7 oz$ǘS^x;ćML=o1eGQMgN$)2Ē?VG`$am3NJݷ|vF>{|s:w< ?w]c+1{xoD!,5m&MVū[۬sˉoSbyy^[;>a PSX6Y%D#&Ԃ9Ia#n#ṹ"m`%ӣhdgF۷xR%O.5Xk4\\G{/G]pT|y# Ȗ+ދ"JHє‹隞[hua Q>25KN^inZ[Evqp#/r|?l\=ooRLIHʈݽݝnPm-.r=–ޫ9Љ/V`71RʹAuE.acG>AuE.acG>AuE.acG>AuE.acOKԢUI 7F ~;?*٦GgĿk%|xk ?D}{(C{]X a K]yYM0‰ 6[h"6Kdt9$Q\e尶ӭ'HeK$#%'֭\&?co~ڼ+7v󎙡lfh#JAnН͓>(Z>촫+V0ۢя>M#LIZ< DbhD9E1$: Vb򭢏ȏʋj*x|ҚKX\Hϖ ?UswݥB\( 0n}Oob}_L4}_L5w6'?Ʊ>/?>/?/ G7瞙dh7瞙dhCoOor{Ck"zg"zg 9=4y}OxyO&xyO&46UCk"zg R,9NEK +W-־/?DI_8?ZK9kg|0%u@θφוO (v:}\k˄K{υX3q!\v@aPJ]ҹB7yg9- t SPԵ GYG$b)a߂BW.999d&_Z[Iuqkڋ0hLgRv;8#VJ32ǀn@U3clZ$$e/ pyJ>n 4 77ǣ2 I4s8mB8Lr>n -buXeY #}̭ʪo$W^ze$w\#+,39v<,t5cm11AG 7u&Hq'Wd@7`7hj-nYb+HvFgdqrj>0eVV-!yY \+NrBrF]j\p<:Ko$A}ݳ B#6#0nzcˏ4/je>Ѭ_ D+51T}X ] r }X G51UG" 51T}X ] r }X G51UG" 51U%M=lp4_Mn_#ʂ3_^׵qo~wy^"Jž׿ ^_=nIeK붶Iqe&RS߂ryH8 ? w]c++1zQ9O qjvp]%[sypf7sq.{%d1޷$(pPIJTjṹg[[{%ohnL̠ȭ$7 Lc,dn.'(_PH>1^.K{ ! %.^䌙 &SFWn`9άu% p#' ~5v3/b>z?6OG'ѽ?Qd~ty}:9ޟ_Ψ}:loΏ6OG8X?#:`NGzd~ts ?D}K +W-־(Co54_Z5MێtrN((RTNRo-C?祧C _hE/w/iP4((E/ a _h/iP4QG_}?祧C _hcGܿ.,KO?祧CEƏ/]{X"G,KO=_r E=-? &X"E{?ȾAw?czZ?ME=-? &(4}(?czZ?MQh"w/iP4((E/ c& 4m촉IV\991VKX././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/docs/source/traitsui_user_manual/images/ui_for_ex4.jpg0000644000175100001730000001623200000000000026672 0ustar00runnerdocker00000000000000JFIF``C    #%$""!&+7/&)4)!"0A149;>>>%.DIC;C  ;("(;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;" }!1AQa"q2#BR$3br %&'()*456789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz w!1AQaq"2B #3Rbr $4%&'()*56789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz ?kY77VŠ6`nO#+]ޫ{8$]KhhO*.X-|)aԗk7*Lr7~#88֣_[pOes?ϬQ+CMc}w ť}oml8<5g:ybFk_+|ya3 gx;T#&aj4F7X?أϬT:vu^EgcyQC8 ZÚV[zr8m9uN.i=3Tc?"[(Ћ}mة#ީ+HǙJ ;tTbrT'=i-9]E%"츞x&o%}_}56߱G_o~R)!6Db0TƙƇo~n`Αm:9!C϶kwh^KJQqQN2Mm*5{wY3٣!=U9<CE̹?1[NQ)Ny۹О0<ۍ- <[A D\Ktnt0ijG45o`n<jFl\XON+ZŜLv 9"L!2=*rhs3v6 .V:˪FrJ`r ~F?#}G̳LFiӶzy~WB9T{''EA&Ho5UIjd d7"+| f|ʅ95EWmdtE!NJ)FQXo\X%&fdxBF7JFVrK-yb(uB 6;NwW :T䵾<$QSڀr ņ4}{WWn$gJ81Kn_r3)N$DlAn]ԚƢhRF "`|j2ȩ}򒺰I,D RJi"ԿҺˆQEnQEQEGW"X`gNLpʌA8v6mwy,~܀qQ@fS_)꿯VW>AtcU CW)꿯Q_++X_?? C _GS_}cq?7.x45~ҞiOU² CX_?=Jz}=W ?7.}cqCW)꿯W|c`^}cqh͐]&۷qu\ί ?Z?V֯4jZG#~ V :c2X ?Z?WM}Xp5 +{%C`'' (/m51\AkvR.IY@̀%#`s'nsj=Zvo9Ap; vRm Gy}*ɥUKw6ZS qzc hXIǥY%KuA ~BnV.?-;N>yKv4J<_{u{ieoGvO8Fx鑓0,42W*7* h5Y4n BrN>PLtsM>-؝eyV( [\ o7Zwz r-:2rEZWz]#b0HP?* h'?ƮDT~r{ChEQQ}Oo@IEG'?Ə9=4]%<tTI^'?Ƽ!pAK^h_|4j_i]uyτӭmoe^I,*E>amxMK+LvIc07!~ Hc-N`ڧ,7utW9.fm+ɱY+̱-3(?>F5HL{̆Ѡg phDD'Fe<W(VmGb_%Λm+KHKw d2ET1+),QEsV'/.dYۦ\hw>pׂM_ԵTom,U$Yb#n s8'm,(}R߼8F(iddgwq\U E;b8f?Mf?MoWʅs"zg"zg(As"zg"zg(As"zg"zg(As"zg"zg(As"zgGJ Moٝ~^^qEHׂIb`eWd>5_ ͍do%W!јu]L%0yR ݲHn$O"c'qyID}܃=[ENa?Qa&ڍ$A#yA a+>4𮎓Rc#\[jѡ€0to['ܫ6aqt2Bݣw +3++i`Q>b38\5q d9z9R1Wo['gX̓ ȮlnIc9 (<5k%vΖHy<{Br7`8(Io"@KgK3F2I[c{mde+x U_(Io"vWxoѼz7k+'?o[bz7h=5E?ݷQƮɣxoWOmvG3 ǣ&ѿY_?ݷQE,j|7FMevGOms0z7kξ2ȧ}v\Gů3̞X?lLbW^:.4j_i]*ų3q ?Z?WeԜ71u1+_Fbc)u%.o{-[[;ƺ;[3e z?\0q<%A+_.xKV2?]¯9}Mt>ȈC 1J$< ^s F<%A+_.aW_ttW9 JG Gx4|G"sdYJjA '~jɛW(NHKЙisZ1/ Ztc_{ sVF& ԰3($6]q'MVy,Oo+ <.eX"9  g,OS JG OW0sVEQ|xB۵20HbGi;8k][ `[^xDŽ%k#я JGW<;XYAgl\Q&Iڪ0O'ަs F<%A+_.x4|G"s|]B/OXYJjA '~z{Xⴿ]+ecفQb _eVqpvg>tQ_Xq:7SEn>Eq4Q@:7SEn>E}OEn>q?P΍tQ@h}OEn>q4Q@././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/docs/source/traitsui_user_manual/images/ui_for_ex7.jpg0000644000175100001730000003424400000000000026700 0ustar00runnerdocker00000000000000JFIF``C    #%$""!&+7/&)4)!"0A149;>>>%.DIC;C  ;("(;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;b" }!1AQa"q2#BR$3br %&'()*456789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz w!1AQaq"2B #3Rbr $4%&'()*56789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz ?kY77VŠ6`nO#+]ޫ{8$]KhhO*w*X˓ qkV=/⭃: [o}`b>߱V<1~oc/ \Cl$(y/ 9߱NU61^-R)r`;bFjmDe<3@(I1RvU)7dIta(|_m4:JӯEjR*yQFB1e;$aF oj,vL}exYr#K:RR%+OAo}`b>߱RW8@ K>dVn$l[$`ÚܽMmxf?xҼ{Z?̾{do}`b>߱I.yȂqo heynbH0傰9OCV!=ͲمDeTk9 =1zZ)]}3̃ϬRhE>߱VK[Ju(Q5V~(_*&Ƈ_o~ϭ1G* ϭhE>V~(,hhE>Q߱Yr߱Go~gQʂ?#[*(,_Џ}mأB?bbQʂ?#[*(,_Џ}mح}?K]qFOgX\:uk^ލ8Ot^PyWctۗKQ"2Hފ'8us.NVdrܟ]<}?ҧ{i紎Q 29b~TldsjJ?.h ͬ7h[RHpH[Un94ǃW7W+T浛_SrWmlD}ŏ0CCFzQW?Ntg#B}#>٨8ĐDR(g8Qu.wsU,c{,2=oDS/ib%.{?7G(@3ϼ}CÌg.[pʤ8 JUEP FCgo~mO\8,_ RV=NQzNWVou4!kHAy1B@|㹢}TGzwq6QhTI3C3tK gyKK#uqu9xsڮ{DDmJvOe~xO八RQ[thB-x(m,Z^9YpL̊ƹ-nnitdM莲qYn9>'+~tE^_ik򯌽?Km3JI4oG;3F`B Tzh-ݔ 0Y r8C޿?:"Wzˣ55SU-c\yw3wZw:G5#pa {7ֶ awEn yPHHV»OQ _??URiSGw:1pQԫ*}#+sZV7ڣ,Vx`ZJ)Ww# HϵWwΟUYOP]+K/qjY|ה??k R(⿲Ɵkeoy_~cuz%!58OEql18jWz?]*?չ-ҬD,/!H[e*"E[y>»OQ _??G~ #N[޿?:"Wz?rOMu?»OQ>Shkwӿ4»?rMMu?A?:w&W~ ;?G~ #N[ɣɮ߈?N hS)y5uoj R |ȪyeD8A?:w&A[!*i]ynp93=$j̊r>uWK]2OxH16 Ûx%)${W-Ygk^!f>kbO(p2HU@3_d+Tn׿wc3T΍:M^ݭcoB }Ixc>HSllDRC܎]oGW!@Q8ξK1zنH>7>7#7#7#J(=Ϭ?*Ϭ?*#7#7#⪮5Bo4墠P[0~Cq<-eoNIn͝Ϭ?*Ϭ?*'V]m+.sǹoGQoGV7$x[q<-eo97#7#q<-eo 82SVl}dTo}dU 82 o[t{)+x6w>7> o[u-q{!OV+NktÚ=MϬ?*+@%Ob{PɪK:$C #O$z +y4* 3u#ѾoGP?7)OFORQ@}dTo}dU%OFORQ@}dTo}dUQ M{vYQq8z_kb [Y?UY?UUF@&/*kb⨷>7>c2L_Uh?/Qo -o}dTo}dUWe>Ѭ_ @Z7#7#⪯51T}X EoGQoGU_kbF@&/*yk|'zBգh,$1SQПCWZ]FWtd֨k2ִWcꉟϜ[7?Z+O<Ivu|7N+ާu῅/_ )mΡy7lny!o8؍wl^kw$}]Dc7p[O:6]9#HdO-#`Yx&XegwwarC:[x}d!Bdn$cCr-cŦ{y⵻φn.&gE>vpFe|qRD, $~s,MEW(0T}ođIzӸCyEv|5  _ђG{whFN-W΍KRJV0h9|]aW-c%}w0pstH#OyB$D{YFdfmLb +3 W!opxa>9f4;m"+Xl8m@] `0 hm~/Ok^dw+F®`q{ ll7p -myv.@QA5jm{el E'F.B'"' <^E#n>}ץ q|ҷS9]/Nn%0y!%H""?"$JC@~lGt4S(,6eFv'[^-yU2`~VR}BjgĖٵـ5:)}ۈ`z $>oc;|?[̺MI&3F3:FF9ND0h/sz֑λ%RˀL Ǜמ9xlk + ުI$ݻqr:rx*;%WڤA zsLqpnN-^{)amaf#CCFW*fG }]lW;Czt_kk_XAXHP6o6-v[xuٱFW8SzUO"V]cbh q$vlF8$u-]apd_gz_xKI,<}B;x<=l/.RFl[zS5魯4B%;xvDdo1ldjwǩ\YKqhi7sfyjA@ ͿcTSoncd*yJT@"=A4o^$ɲ)$$XfmYA?w*fvn/5ܴѪ.# 8ݸqMkqKq,~D=.*=͌OYKa-W5o/Sǰkj &[\ՉH ]Nw|յMRFjWƅ[]] f<2xzey#Z]E nFxew)px5>]>4koJlmasr xX&sW^.V[,#u!*|ɐ@y 3Z5-M3^jEn\Ii4S8bn ͂RInyC$""J+0f ?1vc.7qr@ђ@bqɪe⏳4Jj'<:]@0r1EVߊ.MV\]@ٽ$Sn &1<'9Ȯt5tm1bQii1Z>ssLѴ3ƾtsn ASTCr8xR[n;(d,d{)3{n:z.شk->5ɉ>ucwlsb hYKNYK3#v c4ZxoG[r^;"]M'>leCKƲJ5&[ey42U0c&յPyf Ћ_,;>ߗ'+J5֓CnQGccI',zzo/m7ݜg͎q@xM;K[/,Rtv"hGޅ %DI$gs\֭Qsiuem-cKh;;9%b#9*wt]"姸VvJ;ym,II#Ϲmn>4M[[I ]~A [5)u]$n$i)CG3FHbJgY? iq,v ;NP#Igy 8$${@zxZ)>R}E}{G_m:> ?o'b^Ľ>yfA;8S_'O &%{x\A,)- c(|6YqhM=:-Ri<$SA"!>d)G +!NXdcix,nttVpsUy6 Tv+s1 /*'Kv"Se@bz 2 /%dI\k_P˰b!H qVmKK %rKs@1TeV%X9u#"w.!Iy!f$ $ W|e]gK0"<1Uf{ Z+ۛg|v-ّ٘ ;[q2?UGrO롢(\翷$ W?UG9B='jnI>=t4Q9?UGrO롢PnI>=ےφ] r+}V[l˜nݑGԞLo跭Wuo跤՘'|J?#oX͞$Imɸ yވ#ةĺL# KA" WP6nuQk Om,rʢ8Y3FVĺL# KA" WP6nucxGO+̸ m*mds0=hVY&!Y |R'tAw:xfp\KqOJ0F*@KRZ<@B=:tRnI>=WY/b,Zuy%ءd$i qWzDWS :6@m˻iBv|IS5ЮgnI>=ےφ] r{rOܓ|5_zh.sےφG#CEsܓ|5_z?$ W( #'jG(\Ƶ@ 7T5kEs? t?8?Z(o~WIoZmƧii,$Ol++g1^@Q8ξK1z׆N^v7G I-1""#i9,ABuRj5pڢڐh$y q\Q'[YuExL$e۵aWITH#/e 4ZjKr"Ia㽈 &😀y֓co>&pݘ-3LӢ)n#9\ك`P ' 2I:4Qp3Y..FIG(pAEjiH{onoTb*EU.88ui$8d+:?B%$u;؝؀O2GCEP28$O5E>pEg(_Q΂ƅ?ȣG: V"5E,hQYk|<gsy?e?|[-5ǧ*EKwc܏cKu+!cuy -Ϛ!$ߌf' ?7)YxɁ0bXl$c"9s9n2T36ܚWXmK󭺌x;c(xF_A&t)V܂E"1`;vdn鑴2݋n5770Lە@Ҥ(T *rI9+j>{뫙(M0eW N9vIn[5$4I/Tm|'SQH k&uDD[vISFX`ruZe3n݋`d1 :f{^5;KibҭJV7(5Ek||,hQYk|<gsEg(_Q΂ƅ?ȣG: V"5E,YEs? db8j;Տ& >qo~Pxh8po (Wg\gG_m:,~^Q (M̿M-; +؂Ayu[ ආ-Bd5I"4Hq+ {!x'%E(YHeݝ^[EstS ulL]Qc=Ҵl~ղY!>ago{sJ -Ou;#1YTtm=kVM#Lfhbw@1ҥ)Dʀ??0@?`뤿)}շTm ?$Lm#`x8Oշ?'d+K4ʉYy9X)>i?PP2j*?QO }~&mCm ~i?PGm?(joHv&f88$WE?#oRTi3z (9<(8fYI ɂIA3cvߥQm[Znfod&yqo AK3$e)^74vYA(##&Ӭof{+{m|,Jd#:z w&}/5'[occa$msa˴cx篵Yn/I-b$b&%|8ѲIt11g}:ѧ<-*V8 0xU(G4FU@.'9[]棪im o]Σ<;O9 Iz߈~߷`Jmn3>ͥ6#Lyq}T q)iamCm5m?(mCMECO >i?P@QP~?PT?m5ek2֌s3drHVkZŏ& >qo~Pxh<> ?o'@Q8ξK1z׆B(riCY՞4Yت qYzՁw[Gs˳c sO^x(mnIngEnF9zZWm%12mwc'Ms-0:+\ɡ[ٶ,WԥmYBa+'{tM2KӒK9IDNVb*/ mO/h$`[oI$6]"S= AɧYZxh۹m!lR1 H=8nF KCx}5'Ej kiaeGB3nvs{٣5υLF>xܛ8r;Dnq@$ڏ-W#61|ֱxC*Q8'+zip&bl +(B2*,,b% L[CeGt+5λLXAy؉ mP,[ۙe  "W:u-:E&FC+ A͛D֮utפ;!;Dp2`?i'hvo;!I渐!%U* @/gah t ?Y:_(ah t ?Y:_(ah t ?Y:_)G,}R)^8UXvU}[֫:}[r Gޤ>gr?%@Š( ԟOieG"m)U2 ,R^2a!2!+@9l`F9<ZKVc);\Gp`somlh4T̵gpےv$.)Q\.ᩯKm&[mƟb{rH+v;|#!EuPWZnf?Qۏ3ސ lO>ڤ`XHQ$9< Ո ᥅_,oK|V%Wtb#v)GÌrMqP^N8Ip1l "bvV^x{U]:5=5t٢YBg7!Nr5bͨb{8likG9B/IPÂr mP,[ۙe  "W1GPm%d/>;fyYub/;viMjWMzHlc٭ӴN / q-_ ,/2ydKx[ⶰ*J?wg+ GUW6VQjH.٥e&g Z??-V_W Z??-V_W Z??-V_W ?aG%Fvq5[] ?O+׿ׁeOBϜ[7?Z+> ?o' BSnKאbUXzʺ3 7_9_-RXӇU(J+/wKLQK+UWHw5(/ 3_9GC}/UWsR`?n0i=_p{Hw4|ZV2Adž:ߘ `?n0ije_.xw7>}Gڏ~c+wKLQK(Uڏ~c(Qoan0i?t4ʷ9Qoj>?/ 3_9GC}/VW<;j>Gѿ1K(`?{*ssGѿ1}7?°t4ۺ_ gre[_mT}[C}/u#G_Ә!$$wZ=npsù?7+,zGΚA *7L?GC}/*+=J+/wKLQK(jQYۺ_ gr/ 3_9GiK(`?{ iϴ;n8ǹڏ~c+wKLQK)sùߘ >}Xۺ_ gr/ 3_9Gxw7>}Gڏ~c+wKLQK(Uڏ~c(Qoan0i?t4ʷ9Qoj>?/ 3_9GC}/VW<;j>Gѿ1K(`?{*siޤy?j׿ׁe`?GZ45`5U%A+ե5}[t&s+C} ѸΊ*!΍tQ@:7SEq4Q@h}MP7SEn>tQ@:7SEn>q?P΍Eq4Q@h}MP΍tQ@:7SE}OEn>q?P΍tQ@:7SE}MP΍tQ@:7SE}OEQEAg././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/docs/source/traitsui_user_manual/images/value_editor.png0000644000175100001730000002332400000000000027315 0ustar00runnerdocker00000000000000PNG  IHDRoK IDATx]O%ՙM%m u Iٹk,f/Mqz!b/i,f'H"Qmt4>i{߻N:nսQ;[>^F:Չ;Y>/b!"@jm$Ry?V unٵ_{ߧwWg|{o~x/"@$S*ڤr\J=|1}ЙJ"@w?l3ЃھX&U(!#Q:CqFb!# ;nAb\k_<zg0=@ Y( XX_I  ;@݆"rݭ>>~ƞ{2\9p$g>p@ P\j|Zk,kR}7ǁC"@v]S'7O#A_?pMmBpp}"Jܹ^|'&q@ff"PV>gMqtNSZ@ v{^n[w :UE$JN"` 23tI[g\v >,C!?}籮[_!\[ Л#]ÕW<i%:f,kH'L[әi%Fmċ$g*u9cVcUe٬Q?IPL >6BA5%G+#!"Md3_ @''G"d &qÁ>[a~J_ 4%58%)3{ݤsI?I" ,nċ3~ : m$ KXD`4a~jjaD`T~M'[|8p ;"@6n,lǪ$2_ !)[u|1\H|3q($͕bv,1)uZdTmB`kĵ\y1n2Zy@$ƚ6I4uh'E km[b8ԠYmʩڈ"Є . ᤮Ihh[#!7MԂaR{z D`hnXMo}tK^y{l?@=H0 |D V&C: yFtRIv&wb_*k7xj*Hhb8Dq$%ce;uh+tZnJ["@9@vk}Л~1܉f 4EŲQO@vk~mRrEj9B~Bԍ!2 )O~hnCnzYm#Ѓlu_i51Y2 6s­3rR ;@vv||}1q]UHZ-b@v!Є݆݆AZpJ.ƥ<%#)-E . pu+w#;g8 hnMnA lJ"@@v~%5}BvB@]w N#z`~uݙ;ιޥdn6 Є3 >ߺ|U7ouOOy ҨNNh6hd8tDM-%Uk* ݼսUcE5#YD.}x'YЇϺizt|;g藃BSCh0dP` D5v v#C5:k$jV h="zdPIfVil]HMִROxP7\Q*9b%zٴ d<#p{cN]x?^^w w]v"gҾɞpν6$6'zGId5Vv9$#UkJ:Fi0St((ݽpjG$b"'ʦ<ւjA#PzSi6jdo6e"01Mmg;g\}|Î&1tLor4!m89$#s R! 9g km!Ks n"%ș[im9*|\LA@vUИbv4a7}]~+=PChrWaЊ7ķ؛B"0;ۀ;)j?u]z);LBhtJjlᵛ[\;~ Glz&h1gUlCrM+{kQhn2`9~uI֭ ~kϙlYW l-H洲NlMح*4hὃwHtG LA=F`(@sXײ UFEoMrhn%0 R[YcA:w(X+Ul^ oяSC"YU}7l1U9KڃÄUۍ4HA[sg Ģl1X-F ~{P 95}{I ЄⰦDtQú&6nwvxEO#Jp 2Sz/kzЁ,&6n4&4Pz}[ɦԢQY%*E+P KG MvjC#{%pw@R<}_.蝡k#ЄݦYT_>|ݏ?na׭k%O8⡮Ů\[[maV"0Mm0.wow}ڪJW64I?GBhn nDmQ*r7(É&6ϵ[J"@b4a-jRO@m՟7p{4x>m lL@ߺ|U7ouOO,9t&(OƭFGfA㧢M-Aq*v_?y y/>Ja6Pe_HHx۝gp-9D`hnDEU5߳WO0%"34a݈VAHM2elfiK|d"XuLvS/}Wp$%hi!=XU9(mmmG^wR-AZS}0J"@zhn[aAV;F cnA.O6I@v[ЂhACN"hn Z-E|$MYЂ(Um>ZvBrhn g. hd"*BӄQ9`**N!ЄݖBmr:nRĔ!Kj"h2Ӎl7V[@UO}i`*Pvw ܔwS#%mv;})&& 6F% 40nGKi1M'{kT]C`n:̪a6C_w+86wZVvn~!Ys4霴A xlA0n߂dD`۠&dm7ׂ/с#ne ^~ *C3AvC:'b`A< yABp`VD`l\jZ891XSA'|ޯ۴ܘVK&@]9 SHj;r\SǦQ7[#s:C63kL@k٨~Ֆ|'&k8&Tf+" Pݜ}'s$B4"պY9pPS0*SH 933"@,e<^uk>r~MR8 czC3N 2X!0Aek {݈n?&! 2`T *=B %[_FPBH8[ul%-[?d|{'QKSIK2lsG,:~SZ Lkj}|6icpUg* @@`u}|砏h-/5פbuB!ZTV`>PBX!:Cz+A$J+[l6CФ}!C!snaunVlgߡ&^oW25[ -:oϣSE'Ī suIDiz >1 4$N]y ~xP_tPZ[1!&#s1 ~bv;,djl9 3[vZ~W]aOB)TjK0ChSa,OOsSG:ʱbHK^[c!h;2ۄ'VPBԵ Cд_j Csʠl6ЗUc~8C5DLnn#[jF>A'[ue;pgVv B-CS+<˾6tr64pu97& AAcY9LJ{ins,  D j0"D`Gu ?[%xJZ~[=WVSgCDL|3-P[lrSF7q!斩!P-7mX|:aך22vUwLxyIκLu~kc=+4󒨘c55D`J4d9~uIV ~kLwv$O |M,6S [iFA& Eo ɦH*@D"m hO QO( 6`$$8!qV[g3ٜܯhs"P4d@c3m!+;&ǓC"0=*n~?{j] RZeh`¿bNI(488"(9$A-v%9w ^ͽݙsj^"CiVL7Ijb3A*[Fݾu;~md9qY9柣y<1=?&I0kT"0vsPh >XN?C9GclU~`o|QM{XE(8V$@6@Mv=#{#r9ad0p3itcC}ASA+l~~[儞-[z݇v}G-uaNYWt˖i -:)Tc7ۓ,n1Qxpvw-0 [YUichLn)hn&߾uÍ"ϻie$8 6LJhn9k hŁlT"4a%Xq.BΝ &zr  QEhn) ΀믿u׫7og^<1>9Й<w4y|M0J"@&F Q|PP[_"&d r"k4"0%s۝ԂZ )X${,;P|MLG@)s\>8S!5 Z˔wU*zٔ|MY-F"PS/~HPC^ˇDsݭniiKh$4:rLK@ĦyAJ4qwNc "Ӽ-XW }!D5K;wSd/9d' DF`^kֳ_xj?MmЭϑp o'n5G#IE#gp"@hrWackōn$@"P&6-B#* z3vr}IDATЁE 䛩t 6d*hn3%D`rln˂Dl Y\w[#_bG 3}o?uz);LD7np8ݻot3׺v.iAb֘~Pi: Pm햸֖Ivݍ5@ᵛ\;~264~:=$״ց;Md9~uIV ~kLwv$Z㹠E5[ ұ9\0e"P݆NEo YH*@DN~tbjBSA D`ql݂AɠrN?Aszze'aLJC"8*UnC=LXLC)-'&3sƊjYD9Sgi]38T/:C:FT^$g|Dsow=v-qXdsIu:+" P݊;~ocsۻh1YaVm+bV 2)("0On:>a"n5";BZNu &[ J멽FeqCȪ C,{##{#r9a$d0p3@} 2@8zg(C(!a"\[a4.݇v}G-uaNU9?zGӆw#-YD`#Tc];& aSP[JIb1}!R #E`cvK]'?܈ Mˮ܍,p"@y[Ż<* q62 Ʈ@m՟7p2x 5DIa'xxJp|MP$. <+B6<'3}I$ < @A:H"@ bd{֊fpOp9I"+r t139fDh. rbLmZ^@~lasm 0P P NӠ * z]! ` D2BƐd 9A?ECqAbh* R~Zuz @[3ɰ<K`[0΀\x'\W'F"| ï @P4&erGbP(j*UDաZQۨ~hf3Л;Хjt#26z=`T1{ ``0Řc]a6Xl46{[mb8Ngs8.wwwׇ}ēx >}aAK'؄u]V-a(K':È-b D"ّI\R6tt4@D#ɱd1y'N~H~GP(.RCDyF(E2bH6KI5JI&HJJΒ.>#}KzL '.Ô$S&"s_fB*k.(&Cu9[.W%A*Mu[GWCXy}y||Inq9  k)P4=JE;MG誘]NOqRiRRR]teO=MOU*F**kT\Q[$akQӋªF!UvNy]RS'Wՠj8ip54.h+]eXB[sJK_+\+G^6QV;QHC{\GCgZG][$zzzFYO (w ) {`#+$2[ư1qbbżŕM\M2MjMLi9M,YgIofVffG˙瘷0`YYܱXzYnl|xiKXQ[mjmc-ѱ9hsV6v5;f6O":88wY,aeZL ~'SӏNΚLJ..lc.îɮ'\_ &7{<===<=K=yiyqjƽ{`||g1XƸF~dPRFr{? 4@FAAAc˂_l =!-lWppqxGtDlDMdGdadԒQ7U1c1+ 292 364 ,5jr9IDATx `SE6)-"P-E@*+ mUPVEQAT@YXZ!("+_PQDkJ)^I&m  @3sf< +pmoƵ^H0 $~ _5V^ EEE9GQF!//z(b" I@ f ,aW=|mM$@>TpXA=&&999>A$@ EA Κ5k^ȶ #pI%''[rpC' ny9lEpW䈳o*ELKK2̙3VUBcZPJWP$+RV EVV\b -A,}Q&9L؉ϖQxj sfe!i0 $3Wpff&DƐaRG|dIʰ "<za\v&Wn6/VSoCvO}d,615nv<ߊEM&aѬufH[l6ZKEk|0o\.xA8Dρw!.=įX\S '_gC̳Cc`P/)̻q~Sb p!jԠ`f-a%׶tD@st"k%F*m5ZeSyU]/a)ǒ"C!YN/wgrǃޮ.~3j7ߥzL:ڹȷ5čca_⦑ᆦ `S8/sO-a;3xwe?H}* vUDV/^/rStDt~tWtk暴 ANQNT%9Y-v׶Czr6I"8Ng' Q+WfWʾpLO4T֨jil˕gd_#` E*TeԳ굻°h'O̮p^XcəC)zptI{njj׉w_SZAv~'xyν}n] Cj526MM`K6Y_;{hڡ&W4-*FڧʨeESymȤ;]Uħ1m;M"1> ծv&(! u-өm8s3zu⠺ά1[sRRLTY^HOC:5 =#G/#n_^W vldyW6nc+NJ]S$h}nj[)?iB*H5L@+v03;φPU &!٧qn|4=|Xƈ1]t=/9soSxQΑ[T*ʖTgDDɩ[kOOWUs\I]_I\6N9#IIkq!GNsGX7ױX[P'uʉy]%B% tN ]v)ض_#~]y:KtP͵zBJo%L*)a&ap4+MەVV$W&mJ7%e>auMϗJ&oó{}Z?(fVx,'| X2''vHc򾪠f":s!y*S[/I׷4.s~ثԥI]_iڮAv5S0R;aJ#WxUfe$[?mQOһ͔gΨrJ{|O9sE5$,߳UPr^K (>>kk@r3p<2D(s/\ d]_4:'{H笊$`GZb^L % a$`&}< t n @x_;s y @c3e$@e$@A*#8F$P(Hϔ5 x @ UQjx %K=,}_jOkaCZPeLI$PAr`"-ڠmJ/FY0i/ۃGګ=3 \*!H@v*0Eq?ѳ,u}@sY1$@Z9$/`E1a|L$"#cE.Fmy [h<7UVI1-9<R721Xĸ. :`֘W?|RkȈB۝/g ,(?}%-1r V{jmf& #P.ARg΀Ñe-e#]m9q`:x;[Ih7vيzhGOjxtrla֖a0)n#֭:K&ɏI,,y^]Uo5>g4U¨R6~˳b5rj źa=0udl3wcbė# Ý1K檸kZk܏M?#u8 C_&`\*uD8{{N GvsIT=!lT+v$Uעu뒧çD*5^6߁GcȧɸS8ْ݈3opZʫVO% Z/zC=ȕۿ옎 CcPK[>6Yзa4>erǿÔbOv<ʮ#tγX1O sxbFٳ;w۱bD_{ԨVX3 3hPKjw/FJ£ ^)} uΪ5DWAӆ ѰiS4mz%F/$0EG{!˽[ɣW.CkG6=ҋ e %7lKJDl-&GZMX=3d|[תHR$P.ARPk.xC!/@`=۱j@/Me؋JK9_z\RwJJ"KwKt0yFo{va1V=$vl|wտv $d*;iFDE!z NoEaMD6VMlFAqf"s25]XߛF n؂vH[ wa2^\DE\2ӽ$5?&CXD-Ԉ-G#\YD} 1M&sNJ]t)%cʿb̐<ܤ1ked Yhڢ>٥f W?C,'e#CzMq s"2r L_QMG@&PK=7b }e$p 92,Ql,[75 Wbڹ"6\ŨnƜ򫕘=+fy ;̟񘴛>m|kN&y!q.'|qcet1@;SbKt*b xV{#?XwTw=RmDُatYHyU]e<"'kt?,/zڞZ*_~?!sV9Li_z:Fu?{[bl68$;IQϓ9˙Gz6^3 M@^7&zt}hx1.PP/\ $RR EtȲ^ R8?}RaeT! श/n T"*>&%!/u VV~L!],@YM9d ,ː \,lr)$E/6B$!r 6_XAw bm'M&(rOjp2 @9 tɺfk}M6ܽ.y[ʭac&m@^mn?^ps9eSru":J.ZƏ[~[)+q q;L$Pj!buL[þ5az{,#[Ndu}hm??w̯*ψ=7+z{ ϺlY0xnLAJ{_a@K< wVj]6D͗uٞGe[Gɺl뇓0Ze1}K{`պ9% UwĮ}wt+jbP<j]`eH͆94gh D'%'[;6}wh,r@.sfYVcnAtvkn]"ekw_eQ$DLs=f<"wu.[uxGJH0E?FNŰ?M6÷"3hSj1JƵC& ![V"t7HMiatݒ$tʐm̴%|Y\8l8<~PTV{M.ugXOibej`)߮۷ng{uFy* oT{5Rq/Bپ?ޖ'߃qsc ]߬e<prI]ftVN%]c{bUy@+UhZM˖[%Vw]S[0`ݴӱ)I-<Љ@)Tv쎏G؆mݓH;'DZcW3m%r_Z}/o亵Czu 6KV IJGzZw~xI)y7Y;7S2o=HŬ!_NCd4VݥeK+ǮaBX|tZke5ǑExצ5bGhYO-1Yp0`ё߈w/?+縎!3niim!M=~]b1%no_jBɋeZ `t_=I1q,sQyN2"`La;*=r:ԉҍ"ζ.[6c3r<;q83Gmko{I@?#,k:DdeHC-[7@^ijdZv&)k ?uنϣӶ2KtzcoYqx.Y+/I"pQS۵XcI"%}+ ;VTR YVt*y]LyMĨl_?C7:D)ZEH{l6IY`Sqnjs¡e^3!]1©eqֳ;nm&ZקkOdx?O7H@g}Ig8e{9@jud}4,CK࢜C\$:.U:Ӡ.8KЂI0H4KS,*k pR[xZ0lFΑ^(Hzŋ֒(HFΑ^(Hzŋ֒(HFΑ^(Hzŋ֒(HFΑ^(Hzŋ֒(HFΑ^(Hzŋ֒(HFΑ^(Hzŋ֒(HFΑ^(Hzŋ֒(HFΑ^(Hzŋ֒(HFΑ^(Hzŋ֒(HFΑ^(Hzŋ֒(HFΑ^(Hzŋ֒(HFΑ^(Hzŋ֒(HFΑ^(Hzŋ֒(HFΑ^(Hzŋ֒(HFΑ^(Hzŋ֒(HFΑ^(Hzŋ֒(HFΑ^(Hzŋ֒(HFΑ^(Hzŋ֒(HFΑ^(Hzŋ֒(HFΑ^(Hzŋ֒(HFΑ^(Hzŋ֒(HFΑ^(Hzŋ֒(HFΑ^(Hzŋ֒(HFΑ^(Hzŋ֒(HFΑ^(Hzŋ֒(HFΑ^(Hzŋ֒(HFΑ^(Hzŋ֒(HFΑ^(Hzŋ֒(HFΑ^(Hzŋ֒(HFΑ^(Hzŋ֒(HFΑ^(Hzŋ֒(HFΑ^(Hzŋ֒(HFΑ^(Hzŋ֒(HFΑ^(Hzŋ֒(HFΑ^(Hzŋ֒(HFΑ^(Hzŋ֒(HFΑ^(Hzŋ֒(HFΑ^(Hzŋ֒(HFΑ^(Hzŋ֒(HFΑ^(Hzŋ֒(HFΑ^(Hzŋ֒(HFΑ^(Hzŋ֒(HFΑ^(Hzŋ֒(HFΑ^(Hzŋ֒(HFΑ^(Hzŋ֒(HFΑ^(Hzŋ֒(HFΑ^(Hzŋ֒(HFΑ^(Hzŋ֒(HFΑ^(Hzŋ֒(HFΑ^(Hzŋ֒(HFΑ^(Hzŋ֒(HFΑ^(Hzŋ֒(HFΑ^(Hzŋ֒(HFΑ^(Hzŋ֒(HFΑ^(Hzŋ֒(HFΑ^(Hzŋ֒(HFΑ^(Hzŋ֒(HFΑ^(Hzŋ֒(HFΑ^(Hzŋ֒(HFΑ^(Hzŋ֒(HFΑ^(Hzŋ֒(HFΑ^(Hzŋ֒(HFΑ^(Hzŋ֒(HFΑ^(Hzŋ֒(HFΑ^(Hzŋ֒(HFΑ^(Hzŋ֒(HFΑ^(Hzŋ֒(HFΑ^(Hzŋ֒(HFΑ^(Hzŋ֒(HFΑ^(Hzŋ֒(HFΑ^(Hzŋ֒(HFΑ^(Hzŋ֒(HFΑ^(Hzŋ֒(HFΑ^(Hzŋ֒(HFΑ^(Hzŋ֒(HFΑ^(Hzŋ֒(HFΑ^(Hzŋ֒(HFΑ^(Hzŋ֒(HFΑ^(Hzŋ֒(HFΑ^(Hzŋ֒(HFΑ^(Hzŋ֒(HFΑ^(Hzŋ֒(HFΑ^(Hzŋ֒(HFΑ^(Hzŋ֒(HFΑ^(Hzŋ֒(HFΑ^(Hzŋ֒(HFΑ^(Hzŋ֒(HFΑ^(Hzŋ֒(HFΑ^(Hzŋ֒(HFΑ^(Hzŋ֒(HFΑ^(Hzŋ֒(HFΑ^(Hzŋ֒(HFΑ^(Hzŋ֒(HFΑ^(Hzŋ֒(HFΑ^(Hzŋ֒(HFΑ^(Hzŋ֒(HFΑ^(Hzŋ֒(HFΑ^(Hzŋ֒(HFΑ^(Hzŋ֒(HFΑ^(Hzŋ֒(HFΑ^(Hzŋ֒(HFΑ^(Hzŋ֒(HFΑ^(Hzŋ֒(HFΑ^(Hzŋ֒(HFΑ^(Hzŋ֒(HFΑ^(Hzŋ֒(HFΑ^(Hzŋ֒(HFΑ^(Hzŋ֒(HFΑ^(Hzŋ֒(HFΑ^(Hzŋ֒(HFΑ^(Hzŋ֒(HFΑ^(Hzŋ֒(HFΑ^(Hzŋ֒(HFΑ^(Hzŋ֒(HFΑ^(Hzŋ֒(HFΑ^(Hzŋ֒(HFΑ^(Hzŋ֒(HFΑ^쁚r@ ߿vIA)))Β2׭[{IH `Ǐl=@+ U" ?8 w T Rpg$@~P@. !@AlH (E$P9(HÝ !@AH*rU ?(H~p @ UwJ$"VI ]$@CT9* ~t"//pJ"`b=l6J*ZlؔdC1g3zy8W?KI,~W >}999x.`TRժUCDD ڔeffB5Vzu14sPPotM%P)8>y6N= UFi#?grÐ\(:vѴiSԮ];vY8q4^z@%BjRJ4hH,SP#GX,o8#"TU<"tKtBm& zk׮ʹlۭ^f:uV /`AR5j(8/F@uaO:UT3y;{AsRήqBlϿ˟UF͚5 , LQF8t*\ԐM 5G@1LMM hV5# 9J1pCt$.;)`:s xabk׮ tq#Ḑ ["jDh5_#4\{}"YveĨp Wezٸtuɼi'j$K [\P_}/WG&qkXL,Ѷ. Kɉ}bofQfuѾU)eO ]Kqdi!zXq}lG;wy ~e6>#VE)q;46!&J2y,MJee>z;uws0ʚIGZN< piX,H^N!fmTtݍ8M?‰\ԬUHI7I[p")P',^~m8^"+J9E9[cy{=mN>V.!r8 ƽev|f#n(]1R}IӚ7p۸]=0[5%Loۀef^ΗRHDm4]NQ ?b#~QNc:pˎPXϼ׷lNP0^+:Pū ٧)^=X*]3uT>0+qg5a7V5똍5G`Exc+/*j,V(tʱc=yQ#7}?,r8zHy8z7$}&5K dF._ta O|ݹN6AHÞ F8ypEd5'~u+}=՜`'{͵PW,!U%N:sqUj| #ǒ'r"w99Q:7:_xyCjG]8>7_@;?3q\<;>nx}Oa)>3JaY)/X+,Ѽ5pgƊcs;4x<^lo7=w1z ?(%E}A}0bF"vP:6rT_W=%o|B0x`f+jO.f R߱yuu[ܔ@ a>%7\'uuԓWA2 N̗L>W]&u]mh O<82G|8 Wu^DUin_QcY(0썋بTS,u!x.Bjz>w_M_b{h:^n ǿ}xJ梲!]o BjϟL'E`|K9lYvY[rJOѠNP .vڤKz \cyS1s=s,2I\C[݇Z LudV 6C310dvuS5i:#u# hymwCrՙ/_ u~@anRU jծ|$})fqez*J,u)q*o+URC\691;+?NW*ApS>JU$_Dꓯ0;ns`a]{u'o FLd>;PYwY\oe&]qEq4z}ufnuզo^vu̘[:kqmT7jaGGz.G9[*|"B]u4G&~1jWsXp QU%W2ܓ)dv ,C&;4Cؼo]=;-7OMjy O7r<2<*Չ(_w< 6t$ކڧDokC,u}'z?ZNx sXz:kl mJ=soEj|Wgn>QKK*ol7t///2Ksρ{C"R$4N|t/BS$l>RWyDT[y\Rav[&6&GϞr"S2 uj"IS"yE̖BjFNa篮SDGGTMqkK8IUBc\O)6e3ug"[]Le'FVe)`ARwKKK Jrbh2> >=.g]B82Ф>xK3KbXNL*cYuCuZjY_u/%FꎛۯqY{*l꒩ 3y9LƷbhY&?M6UV_Y<={ʷ4nسOj^1!c|΃lǰkK06aDƅ\^toqwt QzᵺçzX]MfU)yc zf T)Z>y$5#±~K*zA3H4&.8MJJVsV/ ȈIENDB`././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/docs/source/traitsui_user_manual/images/wizard_dialog_2.png0000644000175100001730000004713300000000000027677 0ustar00runnerdocker00000000000000PNG  IHDR$le iCCPICC ProfileHTSY{EJM"]z JH B *6DGp,2# JTĂA^dP{v{/߻{w߽y2Do7zTt 7 PZL_وnU! (xYdt\$FğjH0g{f8~Gd&SFLCA،C؉d#\ⴴnG p3^HxY=B~*s[iwh!$ AzȚUI8\YN3K3l 5DIa'xxJp|MP$. <+B6<'3}I$ < @A:H"@ bd{֊fpOp9I"+r t139fDh. rbLmZ^@~lasm 0P P NӠ * z]! ` D2BƐd 9A?ECqAbh* R~Zuz @[3ɰ<K`[0΀\x'\W'F"| ï @P4&erGbP(j*UDաZQۨ~hf3Л;Хjt#26z=`T1{ ``0Řc]a6Xl46{[mb8Ngs8.wwwׇ}ēx >}aAK'؄u]V-a(K':È-b D"ّI\R6tt4@D#ɱd1y'N~H~GP(.RCDyF(E2bH6KI5JI&HJJΒ.>#}KzL '.Ô$S&"s_fB*k.(&Cu9[.W%A*Mu[GWCXy}y||Inq9  k)P4=JE;MG誘]NOqRiRRR]teO=MOU*F**kT\Q[$akQӋªF!UvNy]RS'Wՠj8ip54.h+]eXB[sJK_+\+G^6QV;QHC{\GCgZG][$zzzFYO (w ) {`#+$2[ư1qbbżŕM\M2MjMLi9M,YgIofVffG˙瘷0`YYܱXzYnl|xiKXQ[mjmc-ѱ9hsV6v5;f6O":88wY,aeZL ~'SӏNΚLJ..lc.îɮ'\_ &7{<===<=K=yiyqjƽ{`||g1XƸF~dPRFr{? 4@FAAAc˂_l =!-lWppqxGtDlDMdGdadԒQ7U1c1+ 292 364 ,5jr@IDATx ?camrwa!p9B(Cǭ:J84HBCh~'S)Tp8SpPČal3z3{{Yyy.k}k=<]k?Y^͚5˔޽{[nPjU0 IgϞŷ~/6l3GQFիK  $zEDD`ҥGAZZɺHrPn^J;[׮]qʕA$@ד E^*Uu. C… :y5'7׎̳gc;~-2dix+ކ2:ëj\o*E<Z]ƦoX&` j'$$9?Y"D:T\"L>y38%%"P}H?%*S5f͚/UjP:~2gsǎ hѺ1ʗq\M^{0[uD/ ;xf$~'_!yapMKQhUdtUĨfBÛb;-ag$>AV!PZ99ߛ,6dɵi*2-FV1#^Uh e7TO%N*_t,}.Ld⾧{Qj2(#SwyXS2-ΥfZzef:>iyZ:od[GPwMˊ.7|"J+w^f-#jrru-]%UwMSђ*[M%Վ]:D'3!&-ײO M gM]54#+)3+ GэKK؁NEےXUW *rAH͓?1/Uto߲>* *I-m2n4GEG_31+h%`u@ ȲmУ*VD~ ݏG`V=N|<{qE'Ѿ/2.ſWχ4J݆P'/λi+eS^jduӬF`XIñ-5]D֕ոRbbb(*. x gNtaGJqKK?A}B*b# WIGwMi@l{c5 QU722#ĝ避Oyge8TM+{gV-Cρ<W}?0DL;uˤ#6,wʿEl}g =i ɯ0m*1x WB1Y<,}oyJUQo ;7VȿGեM'q*j1/`segZ;;i<${Ovױ Evb撅p.T8ޤEFH=.׮7}m3} KwGM0;p`DaQ$xE/qoPh%ہ5zu.A8kz( ;WW' OtSVȑ'G_@fU,7}ON D;]J#iio>-mUjfs ϤE=mv`cipkmA 2`1zIk݁G):pϊ-(c!Y(D <4S)(2BIPخl,geS}){\(gAWUѪKWd*՝jC.3~6}'Bh\qhܩcj굫r*^(jK;Md՜+edfCU4}.V]Tʗ;k/˾jU$"vx*ʨe&ʨuC*h)/<1q\>u`V"V3S {>L,{qRhri~ogvKrpF Pt )`}$K3u2)C Xշi-Fڧʨ.rscp^j2-ug;]N'kbNJ{0[9/\y\Pwyy23S]1J7;c> TƤę TYd/$P?N<5˪=/*q/8U6R_3,H sޞuHn*Bx3/햱nL9 í"q3'KvpmMz]…3XQvcq-3wz `OJƋGN1Ry*[S}_F.8wARjSˮeI+[ϞǩȽb: gpE.JCcΟ#X}HPuʅZ/ BocbxJ<aگxg~ڃߍZ%N.^Uдs&6? j<l&U2Jֽ'98b3kɍ9K&Lzd6G"bU[vN:Ul}Ͼ©T${ON3~;&L"W"rjE\ Ţ."=oT=s7F[R&Sʊ׽Nl1RuTd:PJWYa^K_\2-9__稹%&04펱\aRe/Z Y] \]&4y+6|OuA97j1ZcZ ZmV7Z 2aNOm3!)鼣V[D1+}$M{bLlqv2l0|йށOKExAxG0kWcrwih[񼳲]f9; @~3΃t,FQ SEEZjJV$tIV¤rꆩ'J_q."&20-xNJ%S޷𖟜}MϐPYz<<:< :Wt'yڧU"^eO\d|/he|ǡI;29J2&ҪE!{z$-NX8R+eSC:Yc*:ȴS f+ 5*){/i*6E/_V6ǝrvUеږ6|NZ,P D "]t-MǯODí" x!nJ6Vh K,jnJmcIIvGA#n ~r{Q4~uh93/p|sڿ?B~s=bcڂ`5+<.~1n3ޔ.g˧K2WK8bLקޏM'xchް9WNF52|6˫ߓ7T2NƟ#rզسD3H5By}hӕTECӺ*`kwE!jp8_akep_a/ E9"xL }SzZYYUmk帗 #-H652W쬏nUn]\"<՜vוʫ\_)gJuvr=sɩx$ 5N:h3RθLeX9YTEa[$ JE2*6+ m0! HՓvxuO5bzTNvcoHmAk^b`ݹuԏ%ⰫOtpJ;%$B*sVQiaR|'QL>Ivׯn5ȭ9^Qƒm~s-6R]/a^F,ѭ/L;k\#`hxGlو_|us$˫Lp51diG(UXy teÝad i &<΋8 p T+c wno")js0IZ=/QY3jx BRmfVYG {V> fls<:`§G1tG%tvTmmVǿ9[ +[(Yp , S^cEUmkWHܯb8;`_LY (WNXWX"]S~U4 gJJ|tTGЙBO OKUٓHz y);ڰG ~9+akEOWӎҎD<ݱJdHTǝ]pt0VJ[t$ɲY@H*kݹVVZ[@;dTSCeFU%es/@HgSry.#މR0)Iulpl{-nUZ!LZGGqhn].~S>ۅU/yVyg%1i)-.WO8(Ɋ"H_#pWk)MlvD -ژ3e;ʹ_L,+y[cEHC_nE2綶5@$73y kf,^ Kw>G;#io!;msrzj:+{&ŮRyrX T2 7 IN:%ت]hhuzX6+wKaOݩ\aW Ih6^f:0c5A%zcPa{?zb҇/"d}qtM`U҆^`ki>Ϙ&"+tЬͪ:O;ry٥F3^O|CYdE*C[uH79'=ɗQB<;UFk83QgK).$_<yH[QhvH%3Td5,p= (!OT#l~C҉< xi%bW1,|~u=}Ů+6LD^mA9s~jCxH^L$PZ(H%L^9%#:5{J%59 ēAmO(RFD$P8OFHs,21EݚJi & +r*mTTG˜FvWnh{Aƛ˖zzV@[T~ (5)>dӠnϷtQq' @0X$915rG/9/]6:\&yX3.DÀp 6h־aa;<%k'Xj7PaPQ<.De;ދc0 ',EtlTe{N57PT:O ο//ɭ2[xQ<5/(dz㴧B:Xp*w}CQ?HakI0oky-<+gI%G.}|ʣFhQOb2aRH_[ 4<ߨo|KIǮOfa42Ϸ'4l83{!<* ? h 'ݾ1crUE?Oh&u($sup2L憛 zt<[&Ίعezb$3wt~c5E\R c $Ϩ%KJ[}`}\4i1˞>sg =Gw{I"׸#Õtޜz os]6y8؇0g,tWL g]wws?d#sU7ƠC)ݩ1/nb㢘g>7WTWmasCc߆ZY\ M2raƾ"21K{Vmj "uԱF<jdLe[S?]ׁonP,J2U34rG֭[%يO}}C h7Hf[@cGV{L$%J8D6~Y0O#cD,Qq+17s<UdF:o^X#`;^GY܇ꎴuRff]3.{vb`H0&<5<+dG;v,iLu/b,F,-IUOYؑ(#ƾw^kGKvg'aSkۙeB'ҷ8G gV$wY 2surLN;M94DX1Dyzk>p[[]\hdmqC|?$aHT8/TlMetk- r%,C8{>gums]l1+bل}qYC64t ш\k"g vt֖֑ ᡨRnD,:b5xwcT܀ LB VmQKkNs]"#5T!#)\WsR6DXFNGذyD<0ogD_E$-8"mQ]͂n2]cXtlݰP⹈5ؼaF'/EX8`HǹUUz!0թ EأuJ\5e>19s yẶl6]pVa.ns^telcxٌ]ɚA%D0V$:5 "b̗8FB i U7hlV-?%{U0ate{ Q¤X,=!(t _G[؝NR GS֨Wц7H7:vF5oV[?LjQ70$)qa߱#V&޺bvVa?b^(VKĈ.-PQ{T5Ϛƽ{*r8séq\e= !%4r>ɷǒeuQבJ^$[H۠d%T;ۿ?~".Cmc@.)0 %8u_{[FِwJؙbš)EfvLY{a?ICūqzIhkahHTFu*evdoYkfJu,ML>Eԣ|ϳYs{W1l%sgJK'!k` y?+2qPLEAEAz|~`L^i ~[5ҽHWEGP]ڒ{1:ݙ X?t F /^Qɬgp>G^JĒ\拫%Ox|x+txbJHHI.Ocߦ,<ߠ|BD t"PE3uݻ!CTTFbRS+b6`:#\".^HBhVXrk%>'cg$B!mba MƷqk0E.cɸb L LX۝"\m2t l:p r@!x6QpBjj:Uճ\WMϢ0<ӧ=gs^1 }9Fgz:ML,+NJoN]OZud-0IzH$뤎X: [;|q{'e\ ݢ1wOջǚptwf֥.}#v<>-dK231+*+.ǒԲ>닞Ӣ10~aɆ7v_}cnYA#uvb+ G=NS.l%ѣ<Ɏ$'!Jw\9jNKK[L~Fءꁷ|!7䶉4 æ`oi+]QxfwI垰!qφ i䍔4}g\z\&\O<gӲs  W.|t3c%r:_^%t.D˷ebŨ˒#PRr _ 1tCC w6 "F&@A*wGHpP[H }H%  7Z  @A2.IPъH tI$GǍV$@P @K =$=n"0@d*]  q $PH@IH  J$@z(HzhE$`T$#@AF+ (H%  7Z  @A2.IPъH tI$GǍV$@P @K =$=n"0@d*]  q $PH@IH  J$@z(HzhE$`T$#@AF+ (H%  7Z  @A2.IPъH tI$GǍV$@P @K =$=n"0@d*]  q $PH@IH  J$@z(HzhE$`T$#@AF+ (H%  7Z  @A2.IPъH tI$GǍV$@P @K =$=n"0@d*]  q $PH@IH  J$@z(HzhE$`T$#@AF+ (H%  7Z  @A2.IPъH tI$GǍV$@P @K =$=n"0@d*]  q $PH@IH  J$@z(HzhE$`T$#@AF+ (H%  7Z  @A2.IPъH tI$GǍV$@P @K =$=n"0@d*]  q $PH@IH  J$@z(HzhE$`T$#@AF+ (H%  7Z  @A2.IPъH tI$GǍV$@P @K =$=n"0@d*]  q $PH@IH  J$@z(HzhE$`T$#@AF+ (H%  7Z  @A2.IPъH tI$GǍV$@P @K =$=n"0@d*]  q $PH@IH  J$@z(HzhE$`T$#@AF+ (H%  7Z  @A2.IPъH tI$GǍV$@P @K =$=n"0@d*]  q $PH@IH  J$@z(HzhE$`T$#@AF+ (H%  7Z  @A2.IPъH tI$GǍV$@P @K =$=n"0@d*]  q $PH@IH  J$@z(HzhE$`T$#@AF+ (H%  7Z  @A2.IPъH tI$GǍV$@P @K =$=n"0@d*]  q $PH@IH  J$@z(HzhE$`T$#@AF+ (H%  7Z  @A2.IPъH tI$GǍV$@P @K =$=n"0@d*]  q $PH@IH  J$@z(HzhE$`T$#@AF+ (H%  7Z  @A2.IPъH tI$GǍV$@P @K =$=n"0@d*]  q $PH@IH  J$@z(HzhE$`T$#@AF+ (H%  7Z  @A2.IPъH tI$GǍV$@P @K =$=n"0@d*]  q $PH@IH  J$@z(HzhE$`T$#@AF+ (H%  7Z  @A2.IPъH tI$GǍV$@P @K =$=n"0@d*]  q $PH@S{ZHH }.hٳe[7N$@8wGe=PHtPJ;k%ȇ)(E$P:(HÝ C"(ZI!@A w  Rpg$@ HJtV |P]$@CT:Y+ @>(H@. !@A*H ?~$33x".]+W ###ܕ@2ePlY/_񁗗Wbnp v$xoKgHɸ/ͫ]Ehv+|`zZwЦ̙3օTNTR汘"믿">>DV%Pl|p)WlV /\Nτx4(|suw#$)kٲe (WΊ2 7p~oC݆_ W=EԘ< BIi*cAB RJJ ֭[H ZjTȨp`yUFE|}ȿ6B< qfqws=~ ,;2[ Y= Ȋ1iH*\w:ECPE+ ]zGv⢿D%@p+ڶG՟mnǚo5*%a!ؕegU'}ݮ״+HHzvAԫxa" mác8wlm%M&%HYK/dX]j,{GRiCl>x 6]B.-`sk@ոc-f^XT[ɎscfltNJ@@'پ"/9AC*KzYK^h0GYOsfe±tݞo][#c[t^h"H_峄 U=*V3`ZX%F-?,Hy]sb\+8.8, 6t.QvܼLZ F9ØkydlKDN(=BR *DF?nOóGh[:>ʻ;zn]QQ,]ʔJh&+ힵ\5OQ0n=h zwcpqxP_"'ہ$d;jN=;wb)Oއœ!88z}3$fJBTĴ~c_n/qKj%F *W[z՞)eEWDJeE_ j~>Rouj7F>/륖.2ҷ,9W-} _,_ɩ7~ge_&}꿶}#bSRuHKho9NF=GaFG3֎4l5"Vu8g=Q`LZ)W~_s˛f|XƈRҙ_8K֏B:e+C  ՞CQ˱eMú°pH_|PS,ØsK֠{c;V>|; v{lGhPauz成Tzj|"t*8TV mvr;jy#n :xd !cw);"No2K^ {>Z6t"#W)XהCpDY/WN,X{ ;k#N܉ Wc !|`s.pu1ފ#T5x/0C1af퓻=w7K]N^.C;hmTᗭD9JD9K}1_pyDX?`X ^F:x h%:,wyÏKHMO1W뽔qE/{tV6TQrI":d:WMIm*°[2(F"U6[ÎVeJ9UV MhO)8q&ޫa?0 b*TK sPz\u}tGJ]vR #dV [>/)zK.)h A\)%DRd\1xdsk9е1R/&%cH\$i X7>߬{'}K䖺RӭKJ9!4ܻ*rmV>hinXSL* a(+źxju/|6gʢA2h>wƲ6AVがLm㫴_noq}N%pVoi&ThT(jA簳ʧ39EMrnߖVo jd5%kbLNKEW8I>J75Gc٬P6qoZxbpB,^1<="66':Drh . r'[#@?vJ \ԶQ*RD\%Nf y,ʧt\I;nTvve"(٧Ƣ2\yəxbQ@6,i]*'KMn\_6 zᧇo6,Eݛ`LYʼnrrʻiŭH9}q`Ŵ=Rׅu ߼@}@ (vuv!rvL!pVcH|c8Z̍IHJ-3C!rSl_Ra]0pu#SEa>*c"vΪ&Lԑ)1ɯ+pnQ*n>~^k&Q#"r*Sv~~]m0G;xU&[#  b;QHHJBbBּ&ډFMkg5/s8+7d45aOG t G.we,k;[É'_ MjKw?%쾫miV.]r!VIRcHF=ag@_7]L;s#DQ$Nl#jrm_DnbqR*VE4 J-K6U 9*gp)cݬ,nkLOgmg&w_>yOH`z唛:_A˪"NMjB1X7՚Cor 9u'_l:wkew\XasxZa =WMUNֺo<.]ndOC\޾"Cy=MG/=h޼G~ͬud,GO]^9Q]ZːE'נ+:.].Ğ) ?R&+*V{q/M7&K+ʯN~q-ƛS;!lJJG6e ([c㹑Wd"sܲ"^tՒRRAJl6 r Nn' )rHkqruAY3/r]Yz^ 1 硓Ft#l IN>;>|: B 55e=l /2`M;C'(ej .V)GDI2nc" A@` /Ks؈{!IENDB`././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/docs/source/traitsui_user_manual/index.rst0000644000175100001730000000444100000000000024520 0ustar00runnerdocker00000000000000.. TraitsUI documentation master file, created by sphinx-quickstart on Mon Aug 18 11:27:34 2008. You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. =============================== TraitsUI |version| User Manual =============================== :Authors: Lyn Pierce, Janet Swisher, and Enthought Developers :Version: Document Version 4 :Copyright: 2005-2023 Enthought, Inc. All Rights Reserved. Contents ======== .. toctree:: :maxdepth: 3 intro.rst view.rst custom_view.rst advanced_view.rst handler.rst factory_intro.rst factories_basic.rst factories_advanced_extra.rst adapters.rst testing.rst tips.rst glossary.rst predefined_traits.rst License ======= Redistribution and use of this document in source and derived forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source or derived format (for example, Portable Document Format or Hypertext Markup Language) must retain the above copyright notice, this list of conditions and the following disclaimer. * Neither the name of Enthought, Inc., nor the names of contributors may be used to endorse or promote products derived from this document without specific prior written permission. THIS DOCUMENT IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS DOCUMENT, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. All trademarks and registered trademarks are the property of their respective owners. | Enthought, Inc. | 200 W Cesar Chavez, Suite 202 | Austin, TX 78701 | United States | 1.512.536.1057 (voice) | http://www.enthought.com | info@enthought.com ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/docs/source/traitsui_user_manual/intro.rst0000644000175100001730000002262500000000000024550 0ustar00runnerdocker00000000000000============ Introduction ============ This guide is designed to act as a conceptual guide to :term:`TraitsUI`, an open-source package built and maintained by Enthought, Inc. The TraitsUI package is a set of GUI (Graphical User Interface) tools designed to complement :term:`Traits`, another Enthought open-source package that provides explicit typing, validation, and change notification for Python. This guide is intended for readers who are already moderately familiar with Traits; those who are not may wish to refer to the `Traits User Manual `_ for an introduction. This guide discusses many but not all features of TraitsUI. For complete details of the TraitsUI API, refer to the :doc:`TraitsUI API Reference <../api/index>`. .. index:: MVC design pattern, Model-View-Controller, model, view (in MVC), controller .. _the-model-view-controller-mvc-design-pattern: The Model-View-Controller (MVC) Design Pattern ---------------------------------------------- A common and well-tested approach to building end-user applications is the :term:`MVC` ("Model-View-Controller") design pattern. In essence, the MVC pattern the idea that an application should consist of three separate entities: a :term:`model`, which manages the data, state, and internal ("business") logic of the application; one or more :term:`view`\ s, which format the model data into a graphical display with which the end user can interact; and a :term:`controller`, which manages the transfer of information between model and view so that neither needs to be directly linked to the other. In practice, particularly in simple applications, the view and controller are often so closely linked as to be almost indistinguishable, but it remains useful to think of them as distinct entities. The three parts of the MVC pattern correspond roughly to three classes in the Traits and TraitsUI packages. * Model: :term:`HasTraits` class (Traits package) * View: View class (TraitsUI package) * Controller: :term:`Handler` class (TraitsUI package) The remainder of this section gives an overview of these relationships. .. index:: HasTraits class; as MVC model .. _the-model-hastraits-subclasses-and-objects: The Model: HasTraits Subclasses and Objects ``````````````````````````````````````````` In the context of Traits, a model consists primarily of one or more subclasses or :term:`instance`\ s of the HasTraits class, whose :term:`trait attribute`\ s (typed attributes as defined in Traits) represent the model data. The specifics of building such a model are outside the scope of this manual; please see the `Traits User Manual `_ for further information. .. index:: View; as MVC view .. _the-view-view-objects: The View: View Objects `````````````````````` A view for a Traits-based application is an instance of a class called, conveniently enough, View. A View object is essentially a display specification for a GUI window or :term:`panel`. Its contents are defined in terms of instances of two other classes: :term:`Item` and :term:`Group`. [1]_ These three classes are described in detail in :ref:`the-view-and-its-building-blocks`; for the moment, it is important to note that they are all defined independently of the model they are used to display. Note that the terms :term:`view` and :term:`View` are distinct for the purposes of this document. The former refers to the component of the MVC design pattern; the latter is a TraitsUI construct. .. index:: Handler class; as MVC controller .. _the-controller-handler-subclasses-and-objects: The Controller: Handler Subclasses and Objects `````````````````````````````````````````````` The controller for a Traits-based application is defined in terms of the :term:`Handler` class. [2]_ Specifically, the relationship between any given View instance and the underlying model is managed by an instance of the Handler class. For simple interfaces, the Handler can be implicit. For example, none of the examples in the first four chapters includes or requires any specific Handler code; they are managed by a default Handler that performs the basic operations of window initialization, transfer of data between GUI and model, and window closing. Thus, a programmer new to TraitsUI need not be concerned with Handlers at all. Nonetheless, custom handlers can be a powerful tool for building sophisticated application interfaces, as discussed in :ref:`controlling-the-interface-the-handler`. .. index:: toolkit; selection .. _toolkit-selection: Toolkit Selection ----------------- The TraitsUI package is designed to be toolkit-independent. Programs that use TraitsUI do not need to explicitly import or call any particular GUI toolkit code unless they need some capability of the toolkit that is not provided by TraitsUI. However, *some* particular toolkit must be installed on the system in order to actually display GUI windows. TraitsUI uses a separate package, traits.etsconfig, to determine which GUI toolkit to use. This package is also used by other Enthought packages that need GUI capabilities, so that all such packages "agree" on a single GUI toolkit per application. The etsconfig package contains a singleton object, **ETSConfig** (importable from `traits.etsconfig.api`), which has a string attribute, **toolkit**, that signifies the GUI toolkit. .. index:: ETSConfig.toolkit The values of **ETSConfig.toolkit** that are supported by TraitsUI version |version| are: .. index:: wxPython toolkit, Qt toolkit, null toolkit * 'qt' or 'qt4': `PyQt5 `_, `PySide2 or PySide6 `_, which provides Python bindings for the `Qt `_ framework version 5 or 6. * 'wx': `wxPython `_, which provides Python bindings for the `wxWidgets `_ toolkit. * 'null': A do-nothing toolkit, for situations where neither of the other toolkits is installed, but Traits is needed for non-UI purposes. The default behavior of TraitsUI is to search for available toolkit-specific packages in the order listed, and uses the first one it finds. The programmer or the user can override this behavior in any of several ways, in the following order of precedence: .. index:: ETS_TOOLKIT, environment variable; ETS_TOOLKIT, toolkit; flag .. index:: toolkit; environment variable #. The program can explicitly set **ETSConfig.toolkit**. It must do this before importing from any other Enthought Tool Suite component, including traits. For example, at the beginning of a program:: from traits.etsconfig.api import ETSConfig ETSConfig.toolkit = 'wx' #. The user can define a value for the ETS_TOOLKIT environment variable. Toolkit selection is largely carried out in Pyface rather than TraitsUI, where further details can be found. The "qt4" Toolkit ````````````````` The "qt4" toolkit is the same as the "qt" toolkit in almost all respects: in older versions of TraitsUI it was the standard name for all the Qt-based toolkits whether or not they were actually using Qt4. However it does trigger some backwards-compatibility code that may be useful for legacy applications. In particular it installs import hooks that makes the ``traitsui.qt4.*`` package namespace an alias for ``traitsui.qt.*`` modules. This backwards-compatibility code can also be invoked by setting the ``ETS_QT4_IMPORTS`` environment variable to any non-empty value, or adding an instance of the :py:class:`pyface.ui.ShadowedModuleFinder` module finder to :py:attr:`sys.meta_path` list. .. warning:: Library code which imports from ``traitsui.qt4.*`` should not use this compatibility code. Instead it should be updated to import from ``traitsui.qt.*`` as soon as practical. Backwards-compatibility can be achieved fairly easily by using :py:attr:`traitsui.toolkit.toolkit` to access objects rather than direct imports. This backwards-compatibility code will be removed in TraitsUI 9, and applications which rely on the particulars of the implementation are encouraged to migrate to the newer import locations as soon as practical. .. _structure-of-this-guide: Structure of this Manual ------------------------ The intent of this guide is to present the capabilities of the TraitsUI package in usable increments, so that you can create and display gradually more sophisticated interfaces from one chapter to the next. * :ref:`the-view-and-its-building-blocks`, :ref:`customizing-a-view`, and :ref:`advanced-view-concepts` show how to construct and display views from the simple to the elaborate, while leaving such details as GUI logic and widget selection to system defaults. * :ref:`controlling-the-interface-the-handler` explains how to use the Handler class to implement custom GUI behaviors, as well as menus and toolbars. * :ref:`introduction-to-trait-editor-factories` and :ref:`the-predefined-trait-editor-factories` show how to control GUI widget selection by means of trait :term:`editor`\ s. * :ref:`tips-tricks-and-gotchas` covers miscellaneous additional topics. * Further reference materials, including a :ref:`glossary-of-terms` and an API summary for the TraitsUI classes covered in this Manual, are located in the Appendices. .. rubric:: Footnotes .. [1] A third type of content object, Include, is discussed briefly in :ref:`include-objects`, but presently is not commonly used. .. [2] Not to be confused with the TraitHandler class of the Traits package, which enforces type validation. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/docs/source/traitsui_user_manual/predefined_traits.rst0000644000175100001730000001562000000000000027105 0ustar00runnerdocker00000000000000.. _editor-factories-for-predefined-traits: =================================================== Appendix II: Editor Factories for Predefined Traits =================================================== Predefined traits that are not listed in this table use TextEditor() by default, and have no other appropriate editor factories. +-------------+----------------------------+-----------------------------------+ |Trait |Default Editor Factory |Other Possible Editor Factories | +=============+============================+===================================+ |Any |TextEditor |EnumEditor, ImageEnumEditor, | | | |ValueEditor | +-------------+----------------------------+-----------------------------------+ |Array |ArrayEditor (for 2-D arrays)| | +-------------+----------------------------+-----------------------------------+ |Bool |BooleanEditor |ThemedCheckboxEditor | +-------------+----------------------------+-----------------------------------+ |Button |ButtonEditor | | +-------------+----------------------------+-----------------------------------+ |CArray |ArrayEditor (for 2-D arrays)| | +-------------+----------------------------+-----------------------------------+ |CBool |BooleanEditor | | +-------------+----------------------------+-----------------------------------+ |CComplex |TextEditor | | +-------------+----------------------------+-----------------------------------+ |CFloat, CInt,|TextEditor |LEDEditor | |CLong | | | +-------------+----------------------------+-----------------------------------+ |Code |CodeEditor | | +-------------+----------------------------+-----------------------------------+ |Color |ColorEditor | | +-------------+----------------------------+-----------------------------------+ |Complex |TextEditor | | +-------------+----------------------------+-----------------------------------+ |CStr, |TextEditor (multi_line=True)|CodeEditor, HTMLEditor | |CUnicode | | | +-------------+----------------------------+-----------------------------------+ |Dict |TextEditor |ValueEditor | +-------------+----------------------------+-----------------------------------+ |Directory |DirectoryEditor | | +-------------+----------------------------+-----------------------------------+ |Enum |EnumEditor |ImageEnumEditor | +-------------+----------------------------+-----------------------------------+ |Event |(none) |ButtonEditor, ToolbarButtonEditor | +-------------+----------------------------+-----------------------------------+ |File |FileEditor |AnimatedGIFEditor | +-------------+----------------------------+-----------------------------------+ |Float |TextEditor |LEDEditor | +-------------+----------------------------+-----------------------------------+ |Font |FontEditor | | +-------------+----------------------------+-----------------------------------+ |HTML |HTMLEditor | | +-------------+----------------------------+-----------------------------------+ |Instance |InstanceEditor |TreeEditor, DropEditor, DNDEditor, | | | |ValueEditor | +-------------+----------------------------+-----------------------------------+ |List |TableEditor for lists of |CSVListEditor, CheckListEditor, | | |HasTraits objects; |SetEditor, ValueEditor, | | |ListEditor for all other |ThemedVerticalNotebookEditor | | |lists. | | +-------------+----------------------------+-----------------------------------+ |Long |TextEditor |LEDEditor | +-------------+----------------------------+-----------------------------------+ |Password |TextEditor(password=True) | | +-------------+----------------------------+-----------------------------------+ |PythonValue |ShellEditor | | +-------------+----------------------------+-----------------------------------+ |Range |RangeEditor |ThemedSliderEditor | +-------------+----------------------------+-----------------------------------+ |Regex |TextEditor |CodeEditor | +-------------+----------------------------+-----------------------------------+ |RGBColor |RGBColorEditor | | +-------------+----------------------------+-----------------------------------+ |Str |TextEditor(multi_line=True) |CodeEditor, HTMLEditor | +-------------+----------------------------+-----------------------------------+ |String |TextEditor |CodeEditor, ThemedTextEditor | +-------------+----------------------------+-----------------------------------+ |This |InstanceEditor | | +-------------+----------------------------+-----------------------------------+ |ToolbarButton|ButtonEditor | | +-------------+----------------------------+-----------------------------------+ |Tuple |TupleEditor | | +-------------+----------------------------+-----------------------------------+ |UIDebugger |ButtonEditor (button calls | | | |the UIDebugEditor factory) | | +-------------+----------------------------+-----------------------------------+ |Unicode |TextEditor(multi_line=True) |HTMLEditor | +-------------+----------------------------+-----------------------------------+ |WeakRef |InstanceEditor | | +-------------+----------------------------+-----------------------------------+ ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1684768028.0238073 traitsui-8.0.0/docs/source/traitsui_user_manual/scripts/0000755000175100001730000000000000000000000024343 5ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/docs/source/traitsui_user_manual/scripts/regenerate_example_screenshots.py0000644000175100001730000001052000000000000033167 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Run this script to regenerate the screenshots for the various editors used on the "The Predefined Trait Editor Factories" and "Advanced Trait Editors" pages of the documentation. Note: This script assumes it is being run from the traitsui root directory. ie. one should run `python docs/source/traitsui_user_manual/scripts/regenerate_example_screenshots.py` """ import contextlib import os from unittest import mock from functools import partialmethod import pkg_resources from traits.api import HasTraits from traitsui.testing.api import UITester # Demo files are part of the package data. DEMO = pkg_resources.resource_filename("traitsui", "examples/demo") def _is_python_file(path): """Return true if the given path is (public) non-test Python file.""" _, basename = os.path.split(path) _, ext = os.path.splitext(basename) return ( ext == ".py" and not basename.startswith("_") and not basename.startswith("test_") ) def get_python_files(directory): """Report Python files to be run or to be skipped. Returns ------- accepted_files : list of str Python file paths to be run and screenshotted. """ accepted_files = [] for root, _, files in os.walk(directory): for filename in files: path = os.path.abspath(os.path.join(root, filename)) if not _is_python_file(path): continue accepted_files.append(path) # Avoid arbitrary ordering from the OS return sorted(accepted_files) def get_editor_example_filees(python_files): Editor_demo_files = [] for filename in python_files: if filename.endswith('Editor_demo.py'): Editor_demo_files.append(filename) return Editor_demo_files def replaced_configure_traits( instance, filename=None, view=None, kind=None, edit=True, context=None, handler=None, id="", scrollable=None, screenshot_name=None, **args, ): """Mocked configure_traits to launch then close the GUI.""" ui_kwargs = dict( view=view, parent=None, kind="live", context=context, handler=handler, id=id, scrollable=scrollable, **args, ) with UITester().create_ui(instance, ui_kwargs) as ui: ui_pixmap = ui.control.grab() ui_pixmap.save( os.path.join( "docs", "source", "traitsui_user_manual", "images", screenshot_name, ) ) @contextlib.contextmanager def replace_configure_traits(screenshot_name): """Context manager to temporarily replace HasTraits.configure_traits with a mocked version such that GUI launched are closed soon after they are open. """ original_func = HasTraits.configure_traits HasTraits.configure_traits = partialmethod( replaced_configure_traits, screenshot_name=screenshot_name ) try: yield finally: HasTraits.configure_traits = original_func def run_file(file_path): """Execute a given Python file. Parameters ---------- file_path : str File path to be run to retrieve screenshot. """ demo_name = os.path.basename(file_path) screenshot_name = demo_name.split('.')[0] + '.png' with open(file_path, "r", encoding="utf-8") as f: content = f.read() globals = { "__name__": "__main__", "__file__": file_path, } with replace_configure_traits(screenshot_name), mock.patch( "sys.argv", [file_path] ): # Mock argv: Some example reads sys.argv to allow more arguments # But all examples should support being run without additional # arguments. exec(content, globals) def main(): py_files = get_python_files(DEMO) example_files = get_editor_example_filees(py_files) for filename in example_files: run_file(filename) if __name__ == '__main__': main() ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1684768028.0238073 traitsui-8.0.0/docs/source/traitsui_user_manual/testing/0000755000175100001730000000000000000000000024331 5ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1684768028.0278072 traitsui-8.0.0/docs/source/traitsui_user_manual/testing/discussions/0000755000175100001730000000000000000000000026677 5ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/docs/source/traitsui_user_manual/testing/discussions/automated_vs_manual_test.rst0000644000175100001730000000170100000000000034517 0ustar00runnerdocker00000000000000Limitations on automated GUI testing ==================================== The testing library allows more manual GUI tests to be rewritten as automated tests. However, automated tests cannot replace manual testing entirely, and there exists toolkit-dependent and platform-dependent limitations as to what can be achieved for programmatically imitating user interactions, e.g. the animation of a button being depressed when clicked may not be replicated in automated tests. The |UITester.delay| parameter allows playing back a test for visual confirmation, but it may not look identical to how the GUI looks when it is tested manually. If the GUI / trait states being asserted in tests are not consistent compared to manually testing, then that is likely a bug. Please report it. If the GUI / trait states being asserted in tests are consistent with manual testing, then such visual discrepancies may have to be tolerated. .. include:: ../substitutions.rst ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/docs/source/traitsui_user_manual/testing/discussions/compatibility_pyface_testing.rst0000644000175100001730000000503600000000000035372 0ustar00runnerdocker00000000000000Compatibility with Pyface test utilities ======================================== |UITester| is intended to be compatible with Pyface's |ModalDialogTester|, (for testing with modal dialogs) and |GuiTestAssistant| (for general GUI event loop handling in tests). .. _testing-with-modal-dialogs: Testing with modal dialogs -------------------------- When a test involves a modal dialog, |ModalDialogTester| will be needed. |UITester| can be used together, for example, to launch the modal dialog which then gets closed by |ModalDialogTester|:: from pyface.constant import OK from pyface.toolkit import toolkit_object from traitsui.testing.api import MouseClick, UITester ModalDialogTester = toolkit_object( "util.modal_dialog_tester:ModalDialogTester" ) tester = UITester() with tester.create_ui(demo) as ui: simple_button = tester.find_by_id(ui, "simple") def click_simple_button(): simple_button.perform(MouseClick()) modal_tester = ModalDialogTester(click_simple_button) modal_tester.open_and_run(lambda x: x.click_button(OK)) assert modal_tester.dialog_was_opened But if you try to modify or inspect GUI states using |UITester| while the dialog is opened, you should set the |UITester.auto_process_events| attribute to false for those operations. Otherwise the ModalDialogTester and UITester will enter a deadlock that blocks forever. Example:: def when_opened(modal_dialog_tester): ui_tester = UITester(auto_process_events=False) ui_tester.find_by_id(ui, "button").perform(MouseClick()) modal_dialog_tester = ModalDialogTester(callable_to_open_dialog) modal_dialog_tester.open_and_run(when_opened) In the above example, ``ui`` is an instance of |UI| that has been obtained elsewhere in the test. Note that you can instantiate as many |UITester| objects as you need. Using UITester and GuiTestAssistant ----------------------------------- |GuiTestAssistant| is a more general tool dealing with GUI processing in tests. |UITester|, on the other hand, is a more specific tool for testing GUI components managed by TraitsUI. The two can be used together in tests. |GuiTestAssistant| has been around before |UITester| is introduced. Since various methods on |UIWrapper| (such as |UIWrapper.perform| and |UIWrapper.inspect|) automatically request GUI events to be processed, where they are used entirely for modifying and inspecting GUI states, some previous usage of |GuiTestAssistant| features may no longer be necessary. .. include:: ../substitutions.rst ././@PaxHeader0000000000000000000000000000020600000000000011453 xustar0000000000000000112 path=traitsui-8.0.0/docs/source/traitsui_user_manual/testing/discussions/debugging-gui-tests-at-runtime.rst 22 mtime=1684768013.0 traitsui-8.0.0/docs/source/traitsui_user_manual/testing/discussions/debugging-gui-tests-at-runtime.r0000644000175100001730000000130400000000000035020 0ustar00runnerdocker00000000000000Debugging GUI tests at runtime ============================== To debug an issue with a GUI test, one can use the |UITester.delay| parameter to slow down the test, or they may use a Python debugger to inspect internal states while the test is being run. In both cases, developers should be aware that the GUI can react to any additional events that may be caused by these debugging activities, and this may cause the test to behave differently compared to running it in normal conditions. For example, if a test relies on a GUI widget having the keyboard input focus, a Python debugger may interfere with the test by stealing focus from the GUI when a break point occurs. .. include:: ../substitutions.rst ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/docs/source/traitsui_user_manual/testing/discussions/event_loop_and_exceptions.rst0000644000175100001730000000306100000000000034666 0ustar00runnerdocker00000000000000GUI event processing and exception handling =========================================== The testing API is designed such that GUI event processing is requested automatically wherever there may be pending GUI events. While events are being processed, the global exception handling is overridden temporarily in order to capture any unhandled exceptions. If any exceptions are caught, a test error is raised after all pending events are processed. For example, |UIWrapper.locate|, |UIWrapper.perform| and |UIWrapper.inspect| all request pending events to be processed before and/or after interacting with GUI states. This automatic behavior is enabled by default, but it can be disabled via the |UITester.auto_process_events| flag (see :ref:`testing-with-modal-dialogs` for example). Motivation ---------- In production environments, the GUI event loop typically blocks as it waits and processes new GUI events continuously, until the last window is closed. In tests, the GUI event loop needs to be run programmatically for a limited scope of commands and yield to test instructions every now and then while the GUI is still open. In production environments, some unhandled exceptions are tolerated and suppressed by the GUI event loop, but some exceptions could cause the entire runtime to abort abnormally. Neither of these conditions are desirable in tests. Unhandled exceptions typically indicate bugs and should cause a test to error. Abortion of the Python runtime prevents running and collecting results from multiple tests in a test suite. .. include:: ../substitutions.rst ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/docs/source/traitsui_user_manual/testing/discussions/target_interaction_location.rst0000644000175100001730000000452300000000000035212 0ustar00runnerdocker00000000000000.. _testing-target-interaction-location: Understanding Target, Interaction and Location ============================================== When one writes a test using |UITester|, three categories of objects are involved. * Target is an object on which we can perform an action (e.g. a mouse click) to modify an application state, or retrieve a GUI application state, or search for other contained targets (e.g. a table widget that contains buttons and text). An instance of |UIWrapper| wraps a target under the protected attribute |UIWrapper._target|. * Interaction is an object that wraps the information for performing an action or retrieving GUI state(s), but it does not necessarily contain information to a Target. For example, both |MouseClick| and |DisplayedText| are interactions that can be used against different targets. An interaction can also be specialized for a specific target if needed. |UIWrapper.perform| and |UIWrapper.inspect| handle an interaction. * Location is an object that wraps the information for searching a target from a container target, but it does not necessarily contain information specific to a Target. For example, both |TargetById| and |TargetByName| are locations for identifying a contained target via an id or a name, which can be used against different targets. A location can also be specialized for a specific target if needed. |UIWrapper.locate| resolves a location. .. _testing-target-is-protected: Why is Target protected? ------------------------ The |UIWrapper._target| attribute is protected because it often exposes implementation details of the GUI component being tested (e.g. it may refer to the Qt specific implementation of a ButtonEditor). Since it is best for tests to avoid dependencies on implementation details, its usage in test code is discouraged. However, this attribute is expected to be used while extending the testing API, which inevitably requires knowledge of the implementation details on the tested components. If the implementation details and the testing support code are not maintained together, tests may be subject to breakages caused by changes to implementation details that do not affect the overall behavior. Developers should evaluate based on context whether they should proceed. .. include:: ../substitutions.rst ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/docs/source/traitsui_user_manual/testing/discussions/working_of_extensions.rst0000644000175100001730000000342700000000000034062 0ustar00runnerdocker00000000000000.. _testing-how-extension-works: How extension works =================== |UITester| supports testing TraitsUI editors with various user interaction logic. However it is possible that projects' test code may require additional logic that is not supported by |UITester| by default. Furthermore, some projects may implement and maintain their own custom UI editors. Those custom UI editors are also by default not supported by |UITester|. The API allows extension such that * projects can test TraitsUI editors with user interactions that do not come supported by default. * projects can reuse the testing API for testing custom editors. As described in :ref:`testing-add-new-interaction` and :ref:`testing-add-new-location`, the testing API can be extended by providing one or many registry objects. Internally, |UITester| maintains a list of registries ordered in decreasing priority. For example, if you provide multiple registries:: tester = UITester(registries=[custom_registry, another_registry]) Interaction and location support registered in the first registry will supersede that of the second (if such implementation exists). This list of registries is passed on to all |UIWrapper| created from the |UITester|. When |UIWrapper.perform| or |UIWrapper.inspect| is called, the first registry that can support the given target and interaction will be used. Likewise, |UIWrapper.locate| follows the same rule for the given target and location. See :ref:`testing-target-interaction-location` for an explanation on these three types of objects. This is how |UITester| supports testing on TraitsUI editor: It extends the given list of registries with more built-in registries that know how to interact with TraitsUI editors, and let |UIWrapper| do most of the work. .. include:: ../substitutions.rst ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1684768028.0278072 traitsui-8.0.0/docs/source/traitsui_user_manual/testing/howtos/0000755000175100001730000000000000000000000025654 5ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/docs/source/traitsui_user_manual/testing/howtos/add_new_interaction.rst0000644000175100001730000000723700000000000032417 0ustar00runnerdocker00000000000000 .. _testing-add-new-interaction: Add support for performing actions or inspecting states ======================================================= Support for |UIWrapper.perform| and |UIWrapper.inspect| can be extended by registering additional interaction type and handling logic via |TargetRegistry.register_interaction| on a |TargetRegistry|. Suppose we want to perform many mouse clicks on a UI component in a test, but instead of calling ``perform(MouseClick())`` many times in a loop like this:: my_widget = UITester().find_by_id(ui, "some_id") for _ in range(10): my_widget.perform(MouseClick()) We want to exercise the mouse click many times by invoking |UIWrapper.perform| once only:: my_widget = UITester().find_by_id(ui, "some_id") my_widget.perform(ManyMouseClick(n_times=10)) Define the interaction ---------------------- We can define this ``ManyMouseClick`` object simply like this:: class ManyMouseClick: def __init__(self, n_times): self.n_times = n_times Identify the target ------------------- Next, we need to know which object implements the GUI component. This is where implementation details start to come in (see :ref:`testing-target-is-protected`). We can inspect the object being wrapped:: >>> my_widget >>> my_widget._target The target is an instance of a ``ShinyButton`` class (made up for this document). In this object, there is an instance of Qt QPushButton widget which we want to click with the mouse:: >>> my_widget._target.control Implement a handler ------------------- So now all we need to do, is to tell |UITester| how to perform ``ManyMouseClick`` on an instance of ``ShinyButton``. We define a function to perform the mouse clicks:: def many_mouse_click(wrapper, interaction): # wrapper is an instance of UIWrapper # interaction is an instance of ManyMouseClick for _ in range(interaction.n_times): wrapper._target.control.click() Then we need to register this function with an instance of |TargetRegistry|:: from traitsui.testing.api import TargetRegistry from package.ui.qt.shiny_button import ShinyButton custom_registry = TargetRegistry() custom_registry.register_interaction( target_class=ShinyButton, interaction_class=ManyMouseClick, handler=many_mouse_click, ) The signature of ``many_mouse_click`` is required by the |TargetRegistry.register_interaction| method on |TargetRegistry|. By setting the ``target_class`` and ``interaction_class``, we restrict the types of ``wrapper._target`` and ``interaction`` received by ``many_mouse_click`` respectively. Apply it -------- Finally, we can use this registry with the |UITester|:: tester = UITester(registries=[custom_registry]) my_widget = tester.find_by_id(ui, "some_id") my_widget.perform(ManyMouseClick(n_times=10)) All the builtin testing support for TraitsUI editors are still present, but now this tester can perform the additional, custom user interaction. Inspecting states ----------------- The distinction between |UIWrapper.perform| and |UIWrapper.inspect| is merely in their returned values. We can call |UIWrapper.inspect| with the ``ManyMouseClick`` object we just created:: value = my_widget.inspect(ManyMouseClick(n_times=10)) The returned value is the value returned by ``many_mouse_click``, the handler registered for ``ManyMouseClick`` and ``ShinyButton``. In this case, the value is ``None``. .. include:: ../substitutions.rst ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/docs/source/traitsui_user_manual/testing/howtos/add_new_location.rst0000644000175100001730000000574500000000000031712 0ustar00runnerdocker00000000000000 .. _testing-add-new-location: Add support for locating a nested GUI element ============================================= Support for |UIWrapper.locate| can be extended by registering additional location type and resolution logic via |TargetRegistry.register_location| on a |TargetRegistry|. Suppose we have a custom UI editor that contains some buttons. The objective of a test is to click a specific button with a given label. We will therefore need to locate the button with the given label before a mouse click can be performed. The test code we wanted to achieve looks like this:: container = UITester().find_by_id(ui, "some_container") button_wrapper = container.locate(LabelledButton("OK")) Define the location type ------------------------ We can define the new ``LabelledButton`` location type:: class LabelledButton: ''' Locator for locating a push button by label.''' def __init__(self, label): self.label = label Identify the target ------------------- Next, we need to know which object implements the GUI component. This is where implementation details start to come in (see :ref:`testing-target-is-protected`). We can inspect the object being wrapped:: >>> container._target >>> button_wrapper._target Implement a solver ------------------- Say ``ShinyPanel`` keeps track of the buttons with a dictionary called ``_buttons`` where the names of the buttons are the keys of the dictionary. Then the logic to retrieving a button from a label can be written like this:: def get_button(wrapper, location): """ Returns a ShinyButton from a UIWrapper wrapping ShinyPanel.""" # wrapper is an instance of UIWrapper # location is an instance of LabelledButton return wrapper.target._buttons[location.label] The solvers can then be registered for the container UI target:: registry = TargetRegistry() registry.register_location( target_class=ShinyPanel, locator_class=LabelledButton, solver=get_button, ) Similar to |TargetRegistry.register_interaction|, the signature of ``get_button`` is required by the |TargetRegistry.register_location| method. By setting the ``target_class`` and ``locator_class``, we restrict the types of ``wrapper._target`` and ``location`` received by ``get_button`` respectively. Apply it -------- Finally, we can use this registry with |UITester|:: tester = UITester(registries=[custom_registry]) If we have also added a custom ``ManyMouseClick`` interaction (see section :ref:`testing-add-new-interaction`), we can write test code like this:: container = UITester().find_by_id(ui, "some_container") button_wrapper = container.locate(LabelledButton("OK")) button_wrapper.perform(ManyMouseClick(n_times=10)) where both ``LabelledButton`` and ``ManyMouseClick`` are custom objects. .. include:: ../substitutions.rst ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1684768028.0278072 traitsui-8.0.0/docs/source/traitsui_user_manual/testing/references/0000755000175100001730000000000000000000000026452 5ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/docs/source/traitsui_user_manual/testing/references/examples.rst0000644000175100001730000000205200000000000031021 0ustar00runnerdocker00000000000000 Examples ======== Several test examples can be found for testing :ref:`TraitsUI\'s own demos`. Editors ------- - :github-demo:`BooleanEditor ` - :github-demo:`ButtonEditor ` - :github-demo:`CheckListEditor ` - :github-demo:`EnumEditor ` - :github-demo:`FileEditor ` - :github-demo:`InstanceEditor ` - :github-demo:`ListEditor ` - :github-demo:`ListEditor (notebook) ` - :github-demo:`RangeEditor ` - :github-demo:`TextEditor ` Applications ------------ - :github-demo:`Converter ` ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/docs/source/traitsui_user_manual/testing/substitutions.rst0000644000175100001730000000503500000000000030025 0ustar00runnerdocker00000000000000 .. # substitutions .. |testing.api| replace:: :mod:`traitsui.testing.api` .. |Group| replace:: :class:`~traitsui.group.Group` .. |Item| replace:: :class:`~traitsui.item.Item` .. |UI| replace:: :class:`~traitsui.ui.UI` .. |MouseClick| replace:: :class:`~traitsui.testing.tester.command.MouseClick` .. |KeySequence| replace:: :class:`~traitsui.testing.tester.command.KeySequence` .. |DisplayedText| replace:: :class:`~traitsui.testing.tester.query.DisplayedText` .. |Index| replace:: :class:`~traitsui.testing.tester.locator.Index` .. |TargetById| replace:: :class:`~traitsui.testing.tester.locator.TargetById` .. |TargetByName| replace:: :class:`~traitsui.testing.tester.locator.TargetByName` .. |TargetRegistry| replace:: :class:`~traitsui.testing.tester.target_registry.TargetRegistry` .. |TargetRegistry.register_interaction| replace:: :func:`~traitsui.testing.tester.target_registry.TargetRegistry.register_interaction` .. |TargetRegistry.register_location| replace:: :class:`~traitsui.testing.tester.target_registry.TargetRegistry.register_location` .. |UITester| replace:: :class:`~traitsui.testing.tester.ui_tester.UITester` .. |UITester.auto_process_events| replace:: :attr:`~traitsui.testing.tester.ui_tester.UITester.auto_process_events` .. |UITester.create_ui| replace:: :func:`~traitsui.testing.tester.ui_tester.UITester.create_ui` .. |UITester.delay| replace:: :attr:`~traitsui.testing.tester.ui_tester.UITester.delay` .. |UITester.find_by_id| replace:: :func:`~traitsui.testing.tester.ui_tester.UITester.find_by_id` .. |UITester.find_by_name| replace:: :func:`~traitsui.testing.tester.ui_tester.UITester.find_by_name` .. |UIWrapper| replace:: :class:`~traitsui.testing.tester.ui_wrapper.UIWrapper` .. |UIWrapper.find_by_id| replace:: :func:`~traitsui.testing.tester.ui_wrapper.UIWrapper.find_by_id` .. |UIWrapper.find_by_name| replace:: :func:`~traitsui.testing.tester.ui_wrapper.UIWrapper.find_by_name` .. |UIWrapper.help| replace:: :func:`~traitsui.testing.tester.ui_wrapper.UIWrapper.help` .. |UIWrapper.inspect| replace:: :func:`~traitsui.testing.tester.ui_wrapper.UIWrapper.inspect` .. |UIWrapper.locate| replace:: :func:`~traitsui.testing.tester.ui_wrapper.UIWrapper.locate` .. |UIWrapper.perform| replace:: :func:`~traitsui.testing.tester.ui_wrapper.UIWrapper.perform` .. |UIWrapper._target| replace:: :attr:`~traitsui.testing.tester.ui_wrapper.UIWrapper._target` .. |ModalDialogTester| replace:: :class:`~pyface.ui.qt.util.modal_dialog_tester.ModalDialogTester` .. |GuiTestAssistant| replace:: :class:`~pyface.ui.qt.util.gui_test_assistant.GuiTestAssistant` ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1684768028.0278072 traitsui-8.0.0/docs/source/traitsui_user_manual/testing/tutorials/0000755000175100001730000000000000000000000026357 5ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/docs/source/traitsui_user_manual/testing/tutorials/first_test.rst0000644000175100001730000001602000000000000031276 0ustar00runnerdocker00000000000000.. _testing-tutorial-first-test: First simple test ================= Suppose we have a TraitsUI GUI application like this:: from traits.api import HasTraits, observe, Str from traitsui.api import TextEditor, Item, View class Form(HasTraits): first_name = Str() last_name = Str() full_name = Str() @observe("first_name") @observe("last_name") def _full_name_updated(self, event): self.full_name = " ".join([self.first_name, self.last_name]) view = View( Item(name="first_name"), Item(name="last_name"), Item(name="full_name", style="readonly"), ) if __name__ == "__main__": form = Form() form.configure_traits() # GUI appears .. figure:: images/first_test/init-app.png .. figure:: images/first_test/modified-fields.png As a user types in the first two text boxes, the last read only text field will be updated. A test that demonstrates this behavior will need to do the following things: #. Open the GUI #. Find the text boxes and the read only display field #. Simulate the user typing in the text boxes #. Inspect and assert the content in the read only field Step 1: Creating a GUI ---------------------- Test code can open and close the GUI using |UITester.create_ui|:: from traitsui.testing.api import UITester form = Form() tester = UITester() with tester.create_ui(form) as ui: pass Running this test, you should see the GUI being opened and closed immediately. We will use the ``ui`` (an instance of |UI|) in the next step. Step 2: Locating a UI editor ---------------------------- We need to find the text boxes in order to modify them. |UITester.find_by_name| can be used to find the editors in the UI by names. The names are simply the names used in defining |Item| in the view:: from traitsui.testing.api import UITester form = Form() tester = UITester() with tester.create_ui(form) as ui: first_name_field = tester.find_by_name(ui, "first_name") last_name_field = tester.find_by_name(ui, "last_name") full_name_field = tester.find_by_name(ui, "full_name") The ``first_name_field``, ``last_name_field`` and ``full_name_field`` are objects we can use to interact with in the test. Step 3: Perform a user interaction to modify GUI state ------------------------------------------------------ We can find the interactions supported by ``first_name_field`` using |UIWrapper.help| method:: full_name_field.help() will print something like this (abbreviated for the purpose of this section):: Interactions ------------ An object representing the user clicking a key on the keyboard. ... An object representing the user typing a sequence of keys. ... An object representing the user clicking a mouse button. ... An object representing an interaction to obtain the displayed (echoed) plain text. ... Locations --------- No locations are supported. Objects in the "Interactions" section can be used with |UIWrapper.perform| and |UIWrapper.inspect|. To simulate the text boxes being edited, we want to |UIWrapper.perform| an action that represents the user typing a sequence of keys. |KeySequence| is exactly what we need:: from traitsui.testing.api import KeySequence # ... first_name_field.perform(KeySequence("Leia")) last_name_field.perform(KeySequence("Skywalker")) Adding this to the existing test, this is what we have:: from traitsui.testing.api import KeySequence, UITester form = Form() tester = UITester() with tester.create_ui(form) as ui: first_name_field = tester.find_by_name(ui, "first_name") first_name_field.perform(KeySequence("Leia")) last_name_field = tester.find_by_name(ui, "last_name") last_name_field.perform(KeySequence("Skywalker")) We can confirm this behavior visually by setting the |UITester.delay| parameter and run the test again:: tester = UITester(delay=50) # delay in milliseconds Step 4: Inspect the GUI states as the user sees it -------------------------------------------------- To extract the displayed text in the read only field, we can use |UIWrapper.inspect| with the |DisplayedText| object:: from traitsui.testing.api import DisplayedText # ... displayed = full_name_field.inspect(DisplayedText()) assert displayed == "Leia Skywalker" Our final test -------------- .. code-block:: from traitsui.testing.api import DisplayedText, KeySequence, UITester form = Form() tester = UITester() with tester.create_ui(form) as ui: first_name_field = tester.find_by_name(ui, "first_name") first_name_field.perform(KeySequence("Leia")) last_name_field = tester.find_by_name(ui, "last_name") last_name_field.perform(KeySequence("Skywalker")) full_name_field = tester.find_by_name(ui, "full_name") displayed = full_name_field.inspect(DisplayedText()) assert displayed == "Leia Skywalker" If the application is written correctly, the test should pass. See the test capturing a bug ---------------------------- If we forgot to add the :func:`~traits.has_traits.observe` decorators:: class Form(HasTraits): first_name = Str() last_name = Str() full_name = Str() # Let's suppose we left these out: # @observe("first_name") # @observe("last_name") def _full_name_updated(self, event): self.full_name = " ".join([self.first_name, self.last_name]) view = View( Item(name="first_name"), Item(name="last_name"), Item(name="full_name", style="readonly"), ) The ``full_name`` field won't be updated in the GUI, which would be a bug. If we run the test again, we will get an assertion error: .. code-block:: bash Traceback (most recent call last): File "first-test.py", line 35, in assert displayed == "Leia Skywalker" AssertionError (If the test was written with a testing framework, more failure information on the actual value will be available.) What did we learn? ------------------ - |UITester.create_ui| makes it easy to create and dispose a |UI|. Such a minimal test makes sure the view can be initialized without errors. - |UITester.find_by_name| can be used to find the TraitsUI editor inside a UI for further interactions. - |UIWrapper.perform| can be used to mutate GUI states. - |UIWrapper.inspect| can be used to inspect GUI states. - From the output of |UIWrapper.help|, objects listed in the "Interactions" section can be used with |UIWrapper.perform| and |UIWrapper.inspect|. - The |UITester.delay| parameter can be set to a nonzero value to help with visual confirmation. .. include:: ../substitutions.rst ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1684768027.9958074 traitsui-8.0.0/docs/source/traitsui_user_manual/testing/tutorials/images/0000755000175100001730000000000000000000000027624 5ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1684768028.0278072 traitsui-8.0.0/docs/source/traitsui_user_manual/testing/tutorials/images/first_test/0000755000175100001730000000000000000000000032012 5ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/docs/source/traitsui_user_manual/testing/tutorials/images/first_test/init-app.png0000644000175100001730000005020400000000000034242 0ustar00runnerdocker00000000000000PNG  IHDRGGiCCPICC Profile(c``I,(aa``+) rwRR` \\TQk@@pYdJs(m_p'?@\PTd+@HQ@;b'AGjB@@rF" @Nx:j/(Zp.$D;Teg(8C)U3/YOGȐ՟a(!DXm b*[*H,J;Kqcz N.@%{_A~nVeXIfMM*iDASCIIScreenshotMiTXtXML:com.adobe.xmp 270 Screenshot 140 ˞ @IDATx mYUu/XTt (H+"" &E(Qyh(&E#~DA$H(Ҩ@)~7w>)J5;swc֘guБ,;Z;thAg={Ç/^x1v6:6 .`91q+\ Wb4.9<^|⋛AO_^l_}/ uWtۃl;~|?C_ =^ŏ>-R>%o,.{??I|w z8ɾ0+}?Zs/1ŵ;/w/׹t^?)ns:|u;(>~qy=5xQEEK"z\߉A6BZtcy?⏣v9xwr[Q|,|~w]t">Cvŧ~8t۲6; NO`TKfο2Oa9>Ӝ].6g ]3ʍiŇ>=-ŋy~kߺN:'8k_1u6_Gu ;]ru^>lmxG 82ő]2ZKD"-OB3rh1ƃS_<{|ŧ3.AŐxSwvI;{u;l7\/~6sxkX]ae\fr _ܹ? t$VP=t|4hE /?$[ϱͺx_ŇgێOǦj(,'CŇ??ǒg]':]7v5~7/|ዹ}<]qħ,lp#gmJ?O_8ҙoً_]W1?7owW(>xR6?oXd׿닏sŘ{8sNxN&i_ v|1vŇ__>׿kDkfu??s?ˇ7[:l4o:dVh [a#xAHIϋgn1'=t-8gࣃ)~ŭǿarYZt럏koN貙GӜ8뚟3[y97vQ4wPvN,]ڼϾltٌ[[%_(~Ʒzѓxӛ?(~>kk]_N;cg>= 7}|o>cz;/7lqzomG'ِڍ>3ъU|:gWۘ|hr#q'r"°S|۸ym^ط㷖k"j [D_߼;-q,] l]K] ?x? 'Ŝφ71a˟-9Mp 9l矜-9v#wwt,X`/x͋޸,'m7kkⲴDk']sOώS:Ov+_y`ב3ybuٲn{G9\/61bBW2HlIɚwsy޷qկ~x-vlǣ_|}c!c.Ys]Cwk?|'~rk\c\Ut>| 57|kD_  їm,\_q=l d8R9(1_22v`?NCڭ+1jGS_o~rg.IՐS맚Tk۠6y5[̱mfM1ݘ/Y}8v$ۓ_c* |@]Wc)Lg\SS1\5Wy+0m88$^neۑ|Mz_3i]=ߠ_e]u.;'5h8묳F}WԛfVz8U߼Z&ZXsBILc׼qs]E_e]P'6 wFՔ:Zc?zOsV D(JΘ  d fg`nɮzիoH3</^.x-Or~9|]u1*>7oyr^yϹzꟹ\:w[p.|\m[?ozڿ{0o|5w_ vY jݸ^U7gH,xeWc6 \6J*!GI*|8v"5.Avk_[y 5嬫\e959y̓wˡ|v;p|ݯZ.xÓUYg>}m=Wr~r;c6o_W/p˕}d|h÷]}|Uo9w׽b 7}{7N~W~Wo}{77pOySr9\^yqe\P6 Npr0JlZ ؼH*)ĩ8Pil6y]{~h|UXN޲~לcsqfӸs+]皛؇rr3.o`;OxNæ+rQʫqrX^?7Sqnįۿ1w7믵I?IOZy{.~e̯/ 0Ruc9<+޹n FtՓ^5?>81];-_8B7ުP!GZp`BIޜNoszyKƝƱH$| G;HɨFc{0`=  ɸ>!)~oa޵C{~x{2x6~G.:̑Tl W^˗u9hmh[Yo_nWy=q{{ǯ^-_u_W򲗽l o88>>k/[WK^OOƦg>s1!}ڧ}oͿݕ~rwy ^=Yn|/_E_4삞{-oy˸ {;yWٟ=z~;sѾ{gy#1~m˯گ->#WW7Mg=6lp~qUE?!= 1'[=Nj܇SJP.RƵEP#8fLz Tq7B3 ׏r6xs`8=t2>:=;r+ze,oxBc˿L)p3m꫾jv[B|x^;;om7#vYzx`p؏xx=6~ {~__-3?3['?yj>??/xz_cS6OOkkoo.zիF<[[>sl,S~ΫZ!ts9N1Yz\[,} _N  0n^rjyAϟf <%ƿ_'_!t<7O|?4V3?m[۹׼5&>}؇>U6wmsذM}ߎ&ou[.__M㖷{uk^7r;a3Q#pgAzEıX a{׾vo=ri)'Q+s5޴60 G\ߜ_Oq @]tr[ JO>;$Wѧ/GAƸ;=xoXm n\\<oMR.cm#~ò3ϺzAu=]At#{"y>n+"s쟷6ql_!uGanCuwcֵOELOpėowy[ַn׻g<cp.w˸ qZjPjJKcQs|ԧ11 ѷv8 W|R $# KNRJ ts,#g>}ăЋ&kYq8rۓ9("_o6=r;szO7oAD2̷Z61ZD1݅m {|q;'3~/xn6@m>Ι0FZw4~N͝ţa_(f>ؼf>X8Ot^9g5o?<3f=y1C^g/=6Juס '6o{d1s[6pɟ<$6'>qys;[oW.Q}9GrATS>V]V|dqOR"H$%* v6|%7H6/Ɇb(W{z3O:w;nt vsR Õ63˵vn\pï\.`Ow+<_<-ɇ_{mޞӸ\q/]`{[{% OxӞ[n[=qэ?y^g'q/&C-:g??>η gd'~rkmΕNOO"5S{suÿoQ΍d[>.?css?vL*.ts+&<EHs9#ouw œ;$;|jRKGtqå;-ى`G#-yvMqW 6wˢ풣 ˴Kw].Oa;3׷q8ɗoA|'{.hr!:6 뫩Izkث?꯱?fG\@ZXD`oNMkQOTPZ4%ϙ=[e]u]χW׫ԕAmij^X:㄁ťwqd`L$>9{A ӇᏟGa}9f~H{x-<Q~|lzIy 3-}*0_VG:m;5&m18()P$i `yX:1kGk]6 Zƣsg+5v QW]Ee/%jn%ؼWD HnCOTdha؍ɤg8/ +0V ~i^Us-6X-j|4c_5z:pD c6 GBo^3'* +pP_jH54zkn'N;>']h"/c ï%_W`]8=SaW/tUO˦?{|ثlz >2dN4`Ag7>@!|Img-\@vooZ_,ooşH 85_5#|c$y|}r/>gQ$#|, -Y>bu.՟zҪ1=Ϧә9s2_Z|uJ`'5r=am6}ICN_B6c61V?Qkqٟ̅N{GJӧz/3Ho֞pػny%6bs|5&r6Y`W_Ϥ;|>gPOeyC'h? OX~G~pu]POOꌮ OmD~yAQfۥJ:y1קR[u*S]E;Y6 $,NsI}LOz~`6O9>|ȌsBMLn@_[[mnsYB%s0ܙN* +YZԥQzTAuo9wOO7c}DHN%9ۼt>/]a)zߟ]S}c~pU6y2t+fY@ӲQ7>6l@M}dM}suvEYa?5Eԥ:bWKrO_UqI>pնǝ ;3q_bI1 /4> ;m6Ov% WocX\y!~. 1jõƪyuUUcz,ɷZm>i|X`-9(S5co^"8$dz*B\;Vs}._>3m{-OtO}u_.+6,WY>w#cFz{bx5<ᗨU:4/Ac,s,ˇ^,޶Y:m*W/ {lg\FˇwV qeb5VMW9L`̇nu]~*h&!h+!d P5\8ru?_U_pU\V^$?מy@=:VosVq:U)p=RMKfND)*dD6®ٜU׻_37kw]V >RF6}?l'SJy3gsnj3CRi|茓qXWq޽:ZuҗW+vGWj=޺5էߘP̜S} Ɵdٳ߄͉Ze]u.s!BWѼ>?>bO7I@Pxt' J6 nگ+\ƚZ'5Igc'jnjzb?6h`q%)i(A:sI|~]u.IU^UM'%=BGaI|ov)",]+mс1Vgb @Nj֟ j;sҩiz8l<e$K0]$m%K3Kf]w/'wO/+җt6OGxԬ.W lAm$6:AHJd5/i|u8vKws\S->jmsUtw?;;@E/019s0sGE 3gaX_XW`\+ ElUcߩfف%f.Xm"%N EgOū+E+OUկs?6n?l^ DΆ͆_W/4z_m;KVYWjC-KpxO$)vD%]g/̀o?wy 1Wu)V`Ϊ!5nSl:WUy pP/8 "9`|5#wQϷ_t;U=ʧ|¤'=I7v_P~X_?xf7?~Տmo{&>vJ//Q{h/yV<9wa{Ѓ4p/| `]8QTa5VWV+rC`{®4[4o^w/ycƗ6,~z--^~/{=n|~fx,rL~Ʒ}};c96>Op$6˕|7|񴯊n76G}o|< Od}YW` ?0֟z?«a4zQBlH/ :v䰤qd1fve1Hk\c#/{떻.}{ߢ-w_)MozTۿ}q0(w?S?5|u G-/{˖^|u5JlnѝC*ޯʯo};v~]+Pիbve:T?8<_xB?89"VIɘ#2D@t`%g'ưw}w--`sOw5A%u׽uU׿~cn=1^ؠl կl6 Ǣ'6"׿coh6 k7y`>W/ ZS?4ߩi9݅@l*"ɦ/9Wx=^ ^nzyֳO?}xBb_m}s6|g"Ts83//Dtsr2hu\DMG:3&յZ]0^?(\܈ \IƉ1\_\⊗[Oi̓v=yϸK e\*WY|sg|#k_˫_0";6-^k^x<+տַ^^nUqfUuԥfqQQKɚ#+HkL%@ ow2??8fz}W~9{[x훿ٟ]?ŷx&2Kyg3 @ˏȏ8헿~w]4\8.UصjҪv?O7{]F,)tvĸA+44.;ܱ={b-gm<<9UKaضO[sVXwop,?qo'd~5vg4oNMMB5Mdm 8;_oE'dI9xdemWYWx+Q7e4_j뮹ᴹn 'bn7dg,YA0xͧҳ_ꭺi~iSMnn"u$tU(nj ^OMgn㨥/>qYe]u]8G*>r2yd$.gΘ&R01n(֗u_r*?7J|")_% p&mCK^K7 I[ ToQ7ηyx&U:>Fi0C`{%!r6 a D .n8I֗YgK^N~5NOޘ%꙾NSRdt#5/x6UXW+Pݰ'sMWWݡEmo֍8Ɓ`GAi>MDr37˺ \lS?{9 v/=`w $#1|t pt6Z [SGډ/;]gsЈ'SiȌkh3,{J{u]uP?d?cRM謹W=N6#%14)KP#Ayn̞nNHd}YW`]81'['ڕKXSmdšK!(UXW+pP\jmWJO})7Y (p'(AH#ɟ[e]u]IK(jg#sھF*lmm$txx'x~m] gk\Qjf֛TsӀL OG_P`O7>[Bc}ʉ6׼5mwcYbqZuxZ5l.jO_]URt\3׊_WKqN`*z\#p0P2`v8O/;mRu>nV@ݨ?@`̍AO‘04[|a->QH43D8@91An\;Ї 'Ї>tySw'3K?~~_/ }}^Q=˚||:ʯگrk\i]uZjR/jO:Г6~lzGGUk0G.s";`G/ْ6 VwE />y;߹x{]}z.x_vz31eT>O^~z|/YK|/˗|7|78g6sy{ޘ/ +pPUcU𫾪?5{P?m'Q"A:oa O{})tݱxjuv@\3SG՟šW\z\aVp0FNJRh ¤ek~//7uYKc0/?w(o}{eOtl/&Nޕtt7i~臘ޗQypחuv@םkF54 }:U}5̆;g}<>(H5p%PA#!Їnn[Z׺ֶzztM/>Yn'z׻>v/u[q טD!T}bSKa-sX}Ǧ[-哟\H   .g 8|ƛճY[ S˳oxS?MТm|'>˹瞻/}ZZHKVYW`^|`p纂ouIcf4g?g&Q VJn_ zƷ81?1r;1~l7g0;ї]nw~rl‡]]$0뺫ǸGdi(^wF]bϛuǹ3ZW_3/noq0@5vz#Bs$;aɦy/)=,5Ws'}17ƛ6ޜ=1mC4ƓN<ֹo]S|+z~o?sX_w|wLdγXam aTXUa #Ko܉_ qM$U|:sW^88qõ`qs5|?V#5&Z$OElL7LBZ9Nxa1?;Xzcǂc*7._$?' r3?;x ^LRlnǕoc ꭗsAZo97wlp3N/1s-\\+a+N}_s|c;~]om(hd#Cxomt37G ;7gY^+N8xϯHWa/~~XKjŀ 8\v8W"}V&~>>p-f'Y8m,Fx[q}ݚZwka_Q:q;pqvNƏ]? >^?ѻfℇå'sNq59n7߰ Ki@^e@±ENI1_-ƛ<%̧_M닿Ŗ_8scw,a/ ?C5.97_o5m٭tqKx=,ƣ8p3=?7=\f=NǭϯGgqRx{ā |os y:au|Ù+,xl,әKpu0q/q{kߚist4:ӧX!gW&>ʹystl9V"~_N3*JEGV,? 1íG5y 'Vø IDATiaێc^Oي=q??(>.m>~>8H0֍z‘taںYF3n7gz\?~ݐ_ @TPcґz9>Ӊ/~1[ş~ܼ\%b ɣ~b7w.ovqΙy<|:wtlpm]c^Ot{B;3o}q/ׄǹ蜆K"Zv/9IŗL9_AD9e`B ӱZ%Tg.~Ɲl΂^<:6s¦1w3> ?慮㩟egplcU$C}Qwaqc٧mnq;xf.(>'ū3cd#a'$a%㇍kr_aXWk7kښ*Hlz6翱~C'z-zxwS|GX= }cX٧黦#vBRl9?;[1g!Ŏ^|kVb7?\qvG}`O|8 IGn\dqP|zz|޺z[y؉1sNP|6m:z_pv> >[o޵Gç;.5q0̒s`ƈHAyzAӊ=Li\ [|i8X-jg>SϖOr[yvw=ֲ=hɮφom'n̶o8p=Y|6$\saʧ1ޝQ396 !i̞ lEanǼ`I=1`:x3?6͗/~Ҙ}cSnzs SY_L~g[`^kJ[gBsϘgWLR|c|/ }"Ϸ\ʓOs|ו‡-y3w97/^;}o~w-Y]]O9k/s<[9%!a ZIN#8` iٷyMOHm4'eoaKŧkpwş./nKrw;gkι$0j>pt56/'9ŁWe:h(IENDB`././@PaxHeader0000000000000000000000000000020700000000000011454 xustar0000000000000000113 path=traitsui-8.0.0/docs/source/traitsui_user_manual/testing/tutorials/images/first_test/modified-fields.png 22 mtime=1684768013.0 traitsui-8.0.0/docs/source/traitsui_user_manual/testing/tutorials/images/first_test/modified-fields.0000644000175100001730000005742200000000000035051 0ustar00runnerdocker00000000000000PNG  IHDR 荅GiCCPICC Profile(c``I,(aa``+) rwRR` \\TQk@@pYdJs(m_p'?@\PTd+@HQ@;b'AGjB@@rF" @Nx:j/(Zp.$D;Teg(8C)U3/YOGȐ՟a(!DXm b*[*H,J;Kqcz N.@%{_A~nVeXIfMM*iD ASCIIScreenshotxZLiTXtXML:com.adobe.xmp 265 Screenshot 157 U>}T@IDATx eI]̀ q@AYQPP㖠 [(E#C$DETM$&wQ\@AĀ # 3׿w[g虹O׭^s}}ȱ,WRqȑE[ѮW,7 /_9]G.j@';3>Yrgv6cM3]S7:O+k>27'3Am%h0s??XMMޒ6= x쳩6kĺZ7 LE-=j}6Mpd|x5%A*hs\6~zDl ᲗOA _νGVWƺi[;&oݭ3t}-fO 8+  M9'۩rv6@Е1KG>ȥ_85'W>zZ_]SJoakgޯ7gks}Wң__fEO:ճ}y='7קIyuə9nsH_“MN;po7qVSӯ54{ӱ~eo?C6|:~`|+^[;}o/Ƶ49l_}~9Z9|z}  r 1|Q2o5m4~ߜ']s~Coh)%~@ʑx/s<^oW4x}2׏v&3יO5k:Hy:޴ghhKݘ<~vΘ6)A*hFAJpY(pJя~|t̲g}2`_=٭ϓ>>e?tch[gWͩuצ\ߚh ٧ .˾FґN<:x%1+ش)`gz:rv2p~uO&mm<>~ǿ~/F[;kB@15k =|Mf^[7٧c+bn)U}m|<]xu+WRMЁV5]<}lS}[Н}z???yqK0jsS:i&ӣ?IKel?^: ׸)-Nm%0Zx5]H/}ħ_LxG+8O~e_$Ɵ>g|?8٣C?߸n>?k\3GoL5>g6yxhGoJSHϑ1@΄'W`loǍ/G'd }|fpيz&t~c/[96Žl9;KMui<):ƿnmX}1N$bH ,3 z?ӕ<|hPP͞xÓ͇پ1eW['61i^o[ܧ./xON>_{Y}XQu!UyN2O .磫jl~g?pǛ|~g ?;?siy矚?=?Il+>k(jroo>O>5rVh%ct4sjtdIF {mo{oPv\%76Ȟ><Wg M!l?;~H&}k쉿97`}ˍ_Hgbi}]?QZmY'tpxZgNh.[''!x XMF)%ggQ77-oGp.Qg?t곟A~jxCӑ['[ⲍշw\Pwy|5l3hkyZp7wǃGVȸ@B˺r(&LNO? ַ.Aޥ6חkphyk^3,o~RCr%ix :oݽ 8 Q]áW\C8_?p`g l#,n5|n{ -\-Ë]}7kjast%pLp(^ Ɩ+B l#9asx;9Cr/9_9=Q+(f5uUYN3 Ӄl7o"C`Ӈ+qۿxrq?80nr˹+V+ W6菖-VVIeZkviזU4+>AyQ-Wөt:(q iIN+4>oF~Nso[$ׂgWgo|><0a'<[/ѵ˱rj2#|>ub13\SO@q닶^= Vf9u!e FW9}A< &ٓ;ǃ }oH^WIWjy316I/9%ge_e~DMo??:%ݧ/oO^ov/~>ÞX˿|l`֘?³Xc!_cc?,\rɰgNyO~Ƴ6srH=:>rWf(`9ME2szƣ2tf8}dk)8ϙTNj^Mطl%^ny[ _'G<O/袱9J'/س!I{}-m6 $==w}w?r{kȺ)!Wd?3?s]:>Fj|ԣ5{my^|؇}F'9ۡ[ˡMƧFK\3N"\}ԟ7mL1%hCǎoweoX<\< '6_:x/_}-WU!%QZ/Wht,xw >tyջ^xHk]8=|ς }ɉ)<7 #af]խF*kO&7KFW i-o\WgG:Γz<~ uⶠ%'_5=ߓQ:\ؤ<WR ֠+r+&xcx#Pig1 pHbJA%9<'愶]ڠ~AX}>v_g_1<<1?OcK_>~%lѕOny}M6¯Yd}+]1S(ރcw =g$sϝL5]{ͥ_%ť<:2tY opt*!F=i}}~بf}xlVl?|B>#n+`K49=KB@_*= Kϸ@P0#6}j)A <~ p =x7՗.}Н߮T6t̸+>>Ö{V'"xphxβ}?vCߎ;6 M:6VC?W x?C>dc9yx6xv9)GQcl%>z2\]<ԡ0d DÓ3 ~~6McOp韹ųnW?z\bŻRA/Y^O] .=%+ރX}_Z~/;V >{,/x)d7{~ٟН~1ۯ.雾)u-o$zЃ<U{C?C ťV 8nl"?S?5=TξgGG_(֚tg> Ħ ³yy}vVo;Cy|oՍNaX}>~w~g/ m-(tnSHooY~DM/[G-߭u`MN;[my1v1mA^Q)74ux&YA߄9ѩt5UiˢlC.wpPJ[}(Ìq?ls i/unԦSR>9ɴϭDgk/ }yOR ʿ6Ƈ?>xWh^ j=FF#!bτay&6rMn<.6mx0}l ^Lu1K EWO)\:⣘|@&naHl?S9sp;yLFr+#埾|SkK\tr8~v#fJ2cbd 9GfǓLv=l#9"o䌫RݗpxWQRE]ݦg߆1 1, g83!kX;\v4m89G.rGr[*era0>ƕBIh*I&~D##QtΤ}{X73[(ʃdQ??As_o]3+7~B2X;~<)EWA5te ^16|o ׏c9 r-o<(">:CGO9("EJ M ; x3#ιA~l#" Js>矾yoWw,IkHxys]Q3[~~v[N8 F>> hI= 3j ϩhѓ×n8|tTջ P$O??K^ߟOEG>rKK-\;" q -?ϼ 5y/|Pxht$c%Z2! r%=>Cڻ{ ?#>b[Ϲxܿ7fgɟ^־Ǎш>ƭ;ͻl67\ +&}l;Tɵr|SW:f:>*1S$(xj@ 8m}%6y_n8@F^ |$֡G=Quk9ly;u>;x9  (Cz> 0_|}08 OxxO%>oׯ[8{"`+Dޔ\y/9^u:gpq%@Ȉ'At|xڔx /9/rE'4mW 6dzw |+MqA&g9^Dch^峮JAF|l^Uu{;LW68珍whs'rE$o5_k59П?Z@%l #)x8C ^?%/ђ9A "J`@ѣ8?E?ſNe3ߚ]؆Fp9mЧb٥|tYz^OB=tTo9ߙx].<3滿7"{S˿h?yS oMKz=wݘ H Gc 9svoҸRT~"W8s ֡z\zEN ~(Ʈę$ʱoO_0|H_d/* Ęм WŃs"h釛oamNE_-rC/*hHNd΀,H>e3d|Oi<(hm؉@'A99x hNf=G#2W] ~J{&OK x92͎[D@VWԞM <7ݹ|3dnW.Skp[F`#P庞G/?<@:vqiq۠nG&K843%\5lUcmv#P.A'O.r\ Dc@H@3HhNx+xӡ@מ0l#WBgBT^~9oUߔch=R g8At65yto9=l#9[c)ʽoyh3)*P7N WHh;g6] \Q_kWצ/BYճm428 SK>j@otgl?F`7r'@g,1}m&ӉF_ W6vN #߇:›Y#YGsabοѱ嶫ל/<^rn$ Ï^N^L Q9q 78 lNDzeۺGN3r_y mZzW) Цq Iv:7;# b Ymrڔ"R<;L'xpQf.0y#G XK>Dbݹڃ-\" ׂ67phOY~S˿>֍~}N$n-fm (>Lsl[_93c?c̴\'#d_@[rNNiow}cmr5Ym-AaG .qc&j8rLϱh+ 6#0ݜG}9{x&2o  v4> Gj4@j:読n6k ?j?MWrz҄1m"gD9'6tI@fl#p"՝h?$ %q,i%1|Ɨ2@_Oӟw$xF`՝r9Đ%I%n '^pv! F?]}Wzy>ߙep:/9ynL E/zn{ wXʙ̿rz? WvBxS^듃Smӈ_mW8|Cy{3AmrCϒx=?ySȄW_u#O.OaO6WJp6lwGb|tkg~0'tgC:|9OzғIN(?DF,xNv|˷331XϽ]29a{;~ǎoN $~Dv.FǧN=/Uoovʜ]3ʿrۘKj;U z`Fq<)!dx񣑙ӹo9dW;x~D:iTMߴ<^>>a鑠 ~z?A6q]v8{{dž>P'd9ɑ ȱs2 @|;i. ps  t@Ehc[rvw}w-N=Y x#vI6!kkGl'+ڠsv笸g:q__<Θ!A*thÅf?(^W&NpQ$+@ gD:j@a8t,#?}pl$ ?//,Ndc8|r}xT .`$^wY$\cq5 xa3pjv<`K.cvMΦ/z05v]ذ{Hjr5\Wfbb`g?{qq2>'ǎZY[6s$l(OXRWߥ@\{YWW l||Vr>NKEo~'=G9-EQz(?W7ji6Ո9󆿒-oycsFT0"xˡ4&gWsg#}8qO2楼#^ϳb(f @VS'NvE&97 i COGx5J:d7oފ]eo^r Ծ]=X.?:-o9NҦ'A8ot9{Ӂ=AC_}kqre/{P=is#s6¯/n$I!Sלڔ_}Sc̰̄> dLgs}aA~'{֒oMd,p T$ԙ>z)cBV@_L5G,{Rt5-0X.A8ۈ*_ o3*VoyLQIMis6GKsX_ ۏI`kW[J^~h{mt;DMϾdpyRm p^;OIG2m2&AAmv+M  U@qm xWWoֱm.~9Upa? ; 3qE•#zx\DNrJN/˿r|=؎l@ jx/K|6ƨ<`-\33 -0kW/ru%0_y%FsN?6jaNz2Y6xl&mpJu1n}ض-gG#*_gZ>9tr]ye|r7:<] hᄒCBj 0cN A8O> g|zbq]i{ӻnZSXx1:@Ρfm#p\ݝ8DɳZl?Lǣ+e)niGaH9=orvzǖ7^M=x-:Wǜx&A@?|%ucϠGπ6^;<>mzZb6'EʿM?n7J9דVBgm,hÆQApMMc l#3IH\*k뻅hc ~pm?lfc[o#ȏ++_9?=s]_<:+f S&6!_?x79x`?F`"PAJ3W(6 OF%,j_sMo7SY?ۏm؍(_'yY6Ƀtѵ9 zIC9TFrX“ 9>t%;l#pʿAߔ >e?OvsF %0@;<ޮ!xөNXl\# GʿxInʧJ 2t%7\3"3OJKc,m2oam+\%gqmʿ+Ua\I@%3ZGӦCR0l8ٙcۏm8)_yU 9QDN9-f%:yCH9kFfA?;;NG/c ^6~~`u^v]9\ٻ4tTi5n粃/T^U~gtܾz69M.}Aa ~CUky=ބə }zu [[w*:8Upֆ1w];(ͩY:[꾪ޓف@s=\Q˥|-E8tU|EEZ)/AwWqYXHd”YX )nЇ: ɡWg2tj:m?|tBZh/~!BޥNr?|rJ(pm h񄇓a@DJ(Kv5d$n_lrB x׫T{q~Agbcc˫"3G:+\ց;#!m >9:}É!km2\ٛIh6`wMwH_s9Hq~ [l^9@2_#nٰ5\ʑŤyt/ G +*M>Q Pb9 !LL:IgȕO{=uT [9CJ8Cʵ;#m(̫ gpvPN>![[f|`b¶o?z.8DOxO|~|[Mz&`>8}|sns5׾Gr6Ŋ1\x.l"=)6_tCxFW36+mn{\=8 ]lP6`8 ͇G卾vTirhy#J9& ÌH4R4>L |6IDATpn&C/9dg?9/H5m9*AVqN勾+vZ|:L7MmۥM`c{9 R<ȳ 3Wַr\Mt t\w3}sJ9g{Xt:7q~7/&=JlIy*wʌ+'/Gm?]vAZ) I۠8Axj%np}p^/}xHv0^jyf!:2N\tڡNT/CGbSl6>|4ۋg<C~"$ccq1 ،ɻsA1ϒSEfZ(m9kZS> }ۧb œ3x8oځ]m4uϸhEk^v$W=tP13^'y=wo <饗oW{u%W:u2\:vC'&ìo@ Hxm$.9KM!xCk6HW&(dά<ڠx$]8NNl>鳟 y}t=/<~JwT/sOGf}3MrtrH_[v2m2pGه˿OƟGg  ͷGL$__^tm>W슭X>tUx:*lYH^s?^uXt+; "Dm; x>ɿlV: oB'y5|GbF.]h $_1GeZPh &ѯoD,_S[N["\ЃY?deKO=pڳ~86:}nx&Y ^-0Uoҩvt'&W eGYmixal~b_١+ht裁g6]-٭u_,#?Rm.Q+ٍ.}@vrtjóKxwM?zGt3Yw\3HYhÓ XM⩞@ut~/=pjG=WN\zgx7ixUIAۏ _ 8hOun^hŹ OZ_7ЧG;WG _kjz Clp>$Z  GKq#̋\[_[OЏ__>=ڍK{Y:OVg&^|_?`ߥe6gȪǿcqYUG/sU7D]0ןi'V6htUS5^lGnL#%!r4p >j.ٜ.=o]Ms@Df]Й|PgOI6٧lr櫘[%&A Ғ]2$&$|2Ol E}uv9'6l{a .?=s0$g<pAzt7盲bǽ|//>tNIn> }%?V3.h[;w*˽os-%zzgsڜAs )>|l(ό3e?l?g?{'/ 'P[Či+0PB<5W=.G.yijh*f}_M׿-?C>Z?g+go>)ɧokW_|͝/JJmC<%fzf{xgydOzt6ZO =de?oÎFALRDI7hA}!x5Ю6Hrx7䣩vIVt_]ONoA`kx'μisFkPGݜJ Y%^ӕa|S/[k!8Zv 8mJG)\r29}m@&^L:9xfYe٧/M_OcM_O#>4}}x'}ɨo ujo1/7/^ۚҖ=tO}ᲁڧCI-]m ч6NƘZ FBEGQQ^_ԯAO;SɃ'6hddr>}6>gs)Ţ:}~Wӥbnelvo2'Xo=̓ZEK9W@h@mݕF#ў3ś+|4&o3.q%!`^3.#iQ,I}5S2MGӧSAç~%Xt[$i'=AzЮ/f%]ML5GKf}2[/n inf_.<Z2n7پ5o]_÷71" h)Vs-zr>};S҇7>`'Zzy^}mGGKG~4|pWG GxgiezFu)psP|67p34ldK ^5hSi<}g.[dG^I~G+6h/. m0`) (nPOns6>\ĆGWh}Ş .7qծO㈖}݁t4r`79om1>~:g[ɠ5}ǟ>7`[2Y\A1]?h^Y|k ;y~xgeOA̳NdZ:`4L_N>Y5^E; tЙMlͮ:Ѵ qO6ɪgdgɯ:L;s6_?^iHkf_6ǣ/~=5h[wǛN2k7 \ϊzsQjSM.3n N'ڳ}x T @7z|q8d Eg\FAv2oP}vg;:h-j:ۘmlƟ-4W[6lwkg]y[:[|[dl[~^tY|N_if٧g}hCJ:b JN[́ %M n_W6> ȥ;~UlzGVK7}@ Gf}+y_ܫJKl7F ȥ>YBqKWzӑh|=:T)(ހ8NK lKm|0+tf?x?3vW>e?] w/fdğL77g9ūd1W6sI&=ϮhT'ӆ[%Ov#yps$C3 u`.phSgx鎞d=L69oj =odtWo^y+w=Vy橶~<;/߲?Wg Yfnχ`R ph`\Lzr %A'#pڠhAzܧ9Ʀ#a\=m&sYs<lW+чt?YVe?jNE>;%n=iWǿK  @-9 G&y:y<P 69yMN< F??Zv?M'3ӵj/vZ79sL:s Gk5+ld_Mu<>zQ~zvo7.0FNq  O>=sn}x:g6¥#';g+MM&gkgq;Xsn_|Ͼ-FKr86fV4k!\vyk~G^:y W90hIgym&>z-3=y8c>Ww}J%x?~k?['AG?w Skӎn~%G\Ol)a}Q2!ZNih =<>PI>||g>^?ُN9<h`k3ǺhLo ?پ˾?7ه\}YT 2:G8~G;#.l_d6lMGhp?>l$dog}zg~~Ln[3J_>YkOư~~>73IENDB`././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1684768028.0278072 traitsui-8.0.0/docs/source/traitsui_user_manual/testing/tutorials/images/test_with_nested_object/0000755000175100001730000000000000000000000034526 5ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000021500000000000011453 xustar0000000000000000119 path=traitsui-8.0.0/docs/source/traitsui_user_manual/testing/tutorials/images/test_with_nested_object/init-app.png 22 mtime=1684768013.0 traitsui-8.0.0/docs/source/traitsui_user_manual/testing/tutorials/images/test_with_nested_object/ini0000644000175100001730000007065100000000000035241 0ustar00runnerdocker00000000000000PNG  IHDR|6GiCCPICC Profile(c``I,(aa``+) rwRR` Š`\\TQk@@pYdּx>uA haTRR NK.(*a``LK @ [( {ao #`5!A@ [ 9#h [' I< x\\}|B -]8tPZQ *23J`d`d sAdۇ_`񍁁y"B,i 6[1y - $%8g``g  '{Ϳp}_<=VeXIfMM*iD|ASCIIScreenshotiTXtXML:com.adobe.xmp 380 Screenshot 190  @IDATxeE6I`P feU1Y1+u͸kƜb0  "TfNvOLܪz u[T:ϙh@e.\lM͛7,˫R_c5jgYs5W_=?~#~"' k?SlOM;ο=W,Ov&7F˩[c>¶>8Ǧ|h#DI}D&`9SȲ1j.اCCd^C?)'mO6rl8Ǐ4Զ.j?ǿEd;Wtc/>iƵq'Q?6gKCMyt`{Ógc' QRh101#08iG iy8iVniN?twwk㯍S7`Ѳ-dKI[ ƥ?~0Bʥm92lNTD`2[A91~Vx('$QN@>h~c?l"ӂO=t|w=-4]3V ԅԱşn(zؑ7u9e6c)t=-X8R6wˡՌvlmǿr#e2vF?u}d>{<4oF$P`~N>8.Qȅ' _ݨe>_:Fk'(~"'m+Tg?>#ǿ0ߣhÿs 㟯 o(| FK#QnO5O> i?69=|MSt[z)k[|'VE/q'I%Zzoi/;/bbI%mf:Ę޸<NjO?ԶOY8ꃻҩޠrǣ xiqFse4raCTGh{_C/?꟯ؔ=Q'/ߪcogq_>9Z{l(% @q'U"q|ҶdrФlktl󙆶O~6bo:]ik[؊Nw.MOLƪb?W!639lٴ3fQ2(2itt#'?;=vxE8&b4+s#2EV9u9 DVKdk_JC^"s×w=uىq3{{[uEK4n㍎V>uWNM?EfH9YBi/ 9h`iG?H$69|IRPƆv?;[d[{Y#SoϿobcU?9K0`c;eKמ}ѕ CdՑG)>J&85p)'K`x #GF94ZYG)،qc+>ο:dž4~{bC|Pb>yuHO,/6=,}N}SxQM>.n*xta'|xbm0qV# K4Qy \rQ#YOGtNGmǿ4-WoӉ-?WǨk?ڿ쎿`t2 ^n eNG? uD*Ҿƫm[J5v\c0N964*^zirN 2H9mM`O|j:O[⿵%l,oՍt/XdʡǬ^e֫km|/O貗>0|To(Njɉ*sƦ9 6D~{K.)믿~tMkF^ڐLi[N:Gءi%^'c♴Bb;h?+,_W\Q6xrްA,RS+Mky|uK'CZMݨaq@ʎ/]tQMozӺUfg}v` B[Y8Q҄bֶ~|~G,U! JH9u~)[GVP@] NߞF6teA;lC̅܀XD'%GF=LUv$kpSJ>&<'`ǭ @oJ} ݰC~ b+lM: WՑCGV~x>倳J7^_4 4ơtZye_>ӪPָ=V[L~+\S]xrwۖ5GYc2OW[zwa9S6Uf{oWfSu==s0 }/{K#eஉ.xd88~Git:묲[ uG~}J*zJppӼO ȭBxC^tq-roWnxՉe -םˮ(}Dz-VmzkcpI=;e-kOڻj`˯(οCyv+n|qSN{{b:ڭn5w[^{U>O_}O;r 6Od#ݵsܤ3,`pRu%zkۖEN*( %O93F&)dqJ<xn|Q`YgUď2o~YwPǁ`ep?fL4`z떍7X{O;z+}+{`Z*nФ_夋~Ue^ J /}iKTЏ$r޷XBRP>ϕg?ٵr.+'0|VMK=meV*>ΐPk#Fz~IDEFUlA02ue?{&MLkma9' \~rt]` }Mq # v=X`N9D3,_~yw]ⷿn{WsCoʍ@ J?%bTGʑ"\]D6 >(qJdOwmȥqH˓D&zbcOO8Eb}[=d=SWԛy|{+Y߽غS M|7cWe>_|9Ɲ_:rym}">O{s׽uc9<\~rǖۮֳ뮻>p?-?:җTM$7ˁX6dz- oxCW>z;PzO>rG?Gn6s=kn|vۭnQ7~=yz-Ozғ /|ӟ.vZ'䐟ԧ~ 4uUø_`nzxQك+^5*(^:yI!e\{p3Kg$?8Sڙm쫹aK~L@[ڶoOB?7ҹ~#Hzhs:=n@9J֤h"|_O6M9餓ofC{&VF}Cޯր5L=yzxr/6eȁi1`t<]D:zQ+G>ey Ş}]OZ;WOg̰8۔W^/ҫGO;v ٥ػ;ӯ8@;58!MڼoWK7Իg|:9~9Tk[A^?R-6ڨzzM/wS+1FyOd8bK&/zEeK&K?y[s5:?_'z׻_;K(6pwҷZL.ΒK1D 3FZJiJZ+wWxmE;R.g8..FUR4:ޞIv:[4mgܓ8u߼5 g.jϥڻw.lոGc_Òl=ڧ9qxVmG yj]Ǩ :U4wm<6<6VO}Sk]m|ƶ4r|x+#c e;qf z} /RuIgRSWOڡAM>K'N1XĀ: er/|l^>ǥW{9;\z@}1yudȎny5o_~OlXxȐmueE;.boBڭ@?08#CvՍ7Bi5y򖷬{V.X~;)A&& >Tqᄑw[C߿ioVt8z3Mȡ~}~TZgcuXm}go++BF7Q~N\bߵ}t|ߨ?ݣ_ˋw%J}ځaiK>𷕋~06O :ig8I# / s/j+F?n8X=A/lyic/P,"5jaZ}O(-:o=,.V7ؼ@r;o}ݪ2!|rwt۶g`@my2fF]ro*&:+{{',tR_*#yX.Ȧ_N V+ci@;Tb >ZFϡ.&ϡVQ~b]Ϳ2Ǖ';*he{jhUyYl?ؒA BX][9- מ6S)?'WZnVe뵖3u|='M/tTGۑ'?w%/I2`fBw _>ueȝD3i{t3HkS6n_ڑ:Zڴ>7}K]2^?W}nq)ʔ2N]SFʙ Zjp)q4hV7y1NqY?fcek8/Tv^闑OY/2Slh|}>m}kkmNTlנq|MUW+:sQ OQ[) 'MI=]};x'3|<29! #k|Cip-F\lʴb@lڜaD fwᓅRexKL E*ZRd92Ñ"H^^[#0%1gwgLΥJ8ڂ4o2QJn!o) FWM)k2@ PQ4:c%|===s,4`-l F&Q!Je:$a1^@NY|wup(te5{+'=[CpN:&  Ӄ]e:dŮEʙEQ .pB^9 %ir{Rɳ٩GGG`FFg1x _3_zd؉)1!yE yf +9~W#p]@u}ן,\k2@l+⯺6|p$%D!(m|Kt4TeRP T###0G"0[N_@.| I%E59Nimnj8'sᥜDnuOoׇmryHʠw]WnGG`#b`p1tsim$RȒ?F8g2CΑ%!e6F立zb;d)GqN;irکG`6#;|KX~KLnOfQ>V|ޱZ+w;U杩ï!{g֣:|+_?A}p/ַmխnIn Pg '-߬L^WqK/1u~Ucr$.^.!/0}lz#@g?'Z}E D829 Wmg_7t£PN9 +;"[l;"Y>я^Բ6Bڈ9oq^ɠ`v W=Q{iO{ZP<:kh{s2w }< :{٢\mkW SlGx fƶyp~Xl&sGg]o$Ĉ m 'mrziÌr\9'V&*?*y Y9{+elkR>Xe%2_}ߪ>S_M@^[yd5yML A@ʀw~i`; nҞ}^X\+ofkWzf#Nj`3iV3! dXE| 5"mҸؠh: V.S;.@z[nYWvXm~p`OE@^M&/Fk3?S lK 2nh<Ğ[^p}G~EM7ݴkC'pBNx.{heو VO9uڠLaL>K2 "DɇҀ4,VC2䤎4>Υ(bguOq:f}C*#(y U{! 9cl%EMmxt#ڰ!`k l. yᶋ.2e%?w"9/5iG]NJ{]3X,ǥOyS}[SH.=ܳNf&'_.xy <~7lsZ  y ; nXgOX1#q &OF=J#rae U # Fƽ,[lGد_rJ_CǝBy=kV|+6 %S&N#O\_\i x# r۫-v}(Z~k,~ \ȭڢowXԧenpjbJx'[n^@=ÌyYPԓ(0F)AX1pEr%BrO/1|?]lx÷qgd؁/w%Yɻ@lb˥%AQr+B[%>/# n7u_V.%⭭̛7I^R\5Fխ.y"1@qYyDg"-zi$^UeÝ7ه_Y]u-!e;Mr6`v "yѨG`a!\`(ėY8ajqzOˌ8 '.jPa!lϯ#^"wՅz_]]ySQpLJdVZOC>dw%Gdbox@ fV PNg(K#\dz###0"s8w؉;";8E^&Ӈ?mUr@"!(ɨWe( "C4|2|TFX 28|w:\"ӦN{ bX\k \^C]G`G`憴?=ŋl `k( Mʰ^Hꪤ,c81iog4=3L|.F[,s1 `epUذu:%eg٩C E!i@^Ŷqxqb7iyiL"ƋʽwXX6ltD@om6^O2jrҜU3|Zf*)ꌐ Wcrȶ.2=iONnE`1i7K+2d :r؊=|>|B I}"vՏ3 G[ GGG`F:ZlSץoK;dOc姀2 Q| e01tw(w&mWL06Lj%.cg3H ^===s,0+_!!c:m،`x]3:4,i^\hb>~tbW#:B8j:89Vpx~f8χ4BJ,#EVis #Y'ٜ\;###0"[x(X)$/Q%|i@Jt*GG=Gd0[iȸ2y j/"Z>8/_ak72R_d}ʄ9@>cHCC#8V`( O_;FkFҐ䕁9ʖ ^fyG9#0K.o5WcPuLmrۭ^e $f iVp_|.Z G/c e~ sQQC윐tLq>b;2Q^>-XlƳ`u&nvj_^ _yYu+˭:Oմ.性p18 oahSwd@I7^)hP*F@= '/շ6jEXX뫷m]h~ˢA4c1Lկv)]f7耿8Zs*YlL+QdCձ1| ~^X-Ay|FU{'FD' >۝zV4_~yUMk[8X0 Ob`0jbgg{{ޮ ,xkߘl< FPS'=rz!zzՌ9K'j4#dҨTG#:(j\@X\qKgBvZ9w{Mo:T;E]4,r2`Ggb\M ~x8%\@@)+`gN]7գ{ `G(N5*J;TAEٖZ;ow]+^?OʯkWG?\Q3˭կ~<-|;Ʋ*O~Ԯ̨S3/~~p7Q>O>G?ZC9e/+ykw)M[}yyݷW>5k]]!LiW^J Q ]K0A1 u9">Uӈ4|f9-o.G[֬:r̊e5Α<)em-U}=2`X>6zVg|# _Bo_>U򗿔w]c{\=*x}:{Q|*tGug~ G4u˂PW>w٭K0)yfk5D'zf|m':/6V珟媫^gMЇ6wZ/[= `>@oRs57~҅|[TV.vUi?8z4}\^t֧eb2hvȬLAAnt ʭ}٧+^l~C;εlv]v)y{ʮZ~]nwZ~_}I'T}ڷ}^8Tmob?ŏp-3Tj2&Վ-}< OV)xiU?~-]Z?+Nʻ[@}XpYzVY`Ag>Sۏ׽u>OG/|s[xOۀ?蠃waG=ԧ>U7W17Lӟ'>/{՞=ګ򕯬)[խBCơ n=aU֖v_y_On}{߫r*֊ɏWUu~wjڪa=vMG}tMozSLN"vAu ˖h>ZkU_b2=䓫}1RR}y{^kkerslo}[??lmAg"_EXXWVinF cg/V>P429 $T3!Г7l,K^|ы^TYgGk%'=UAnyܥ;6~ԕU*fmK_:!½Wn~5L;\'v[[A'2x[޲lC=_ci/xD9#}{z!l_ /$V{~L8V.ZNF*׽y>!7 Yu/["{]zW5lcK?KW[rqVOFUVɶK[M=.+~(o}[Wuᙐ{l6ؒom+f&׋t?7 n@'$w&Ӑv se%'vI ̮_>VG\hdUo2r/JweɯqaۖXX_m{ח"n?a:X?R˼M_߼A(Gg g[؃`ߺQV$ y@?tu,xM?63!r~v&s)[wu}wMWv/}YligkZ 2M@"kḟh\3p.3% t@4GqDFaI<\Sfst"-Ⱥa>(l~%[kK~Y+_"]iɿ//.f\ZI&Cwނt62)hp`Ey?6ޏ͋>]*N_Oջθ ,9< ~&ŧC^}8R8~ EƄ)E8d &~5K_j툩tMA[{37%mU ي||~LI}yæĖǙ(g"?[27{`qIK~)/? S&ns%صl| Pv|D+" w#7jӘO 3@0k@+gb/W=U|, ,T U ژ;KW%~ӡF3q,+O:Ԣ?Si'|PeRyع6}^Q:<-X z:1z:y'?iFbXQ Vͷ пSo5?Vd1$Vr?d.:i}7r.?1m궮nV֫~jif" Bsh8ʻ [שG`E"`E<#ȭ=O<Ǭe&Iʍ6GWwG=7"`)^p[;%|l`w!iZ94RG_%cGpoIQV{5 ٶv~N&ݳ򧥉/QOaeYBDZ &*Kӡ-ӿL~;8(sξ F,ؠ#[+2B菳u,6Xa~2GQLǁG~>Z!')CA9*MS=>$)>;(vkBx +O r;F9Z' >D'' E===s-0Uu iyy!YӸȨGh0seo]rGguQ؉ ucw}|{zz`kp3xL9`|ViDǩm)C<}eiȲYyDS@@\\\;Ӏ?PQl#_G@@ v@YsR2J6rQ7zG&rzzzj` +wRэ0ONܩG`E"`_lG'?I=D ]ek[-AVL$ixX<,x`˗7|] =וce_Jgً}-U2AGGRO>`'r4v[=#"8묳_1u{Vi p+yK5 !(2^WnL䤜";fg||_ks)O{ʭnuz//?/~q~^;g=wC>7~WN8aXG;ΰ3=SEV:PUux9Ύ9zH[gB+ubzf+|ˣ>W+O}S5OVG]^{O|F7*|u׭@;ޱ e(uA<_jaʣrWrHy~ #0,ab\}1;_q ,>Et3)$%Kτ #0xP6p v|3);Sbm/ˡFmT|ԧ>?5/\p+_Je]*o6/}iV\ FGuT9kL``R-lq@,dK= 豃H_巑_=r߿nַN:w v Enx־:)E~~;ݩ|E{go^#0S2wY#ZkU3Qշ` Xog OZ~lPG, {#X.#fUe.xQשG`6"_O{{[bm2؟{Sn~׋=kTM@~ l\`=Y?z@m7x\M99;owQ c\[wW"/ţG.7ӟTs%UcG`%F@*lfŋ;s6t%&}iH]?{e!Yy<X\o;;,C8B3iZp7v!rl"DE:kq^0,lhi92AS<[`l-ף{i6Wo4{ZYƥtv9V===s0Y ;u*6y#@dy2GN} ~8j3C~ vp!ti"+%x]29!rzzzjbSW6AWv4JR|db2/Jyco2m\" -k,se-E&=K'(*5$y4 CNCI P& EGC~_N;Ӑ':N==?s h <Ґi\d;|(6tf*W~vMoZ_qq4yLJ5L=+|-nQyR wa!k_D[Oo7Y#|߸^*_ע:L/(=yw),`f0~Z fl+|@Sk(z&؉}];Ur:W\qE}L}^V>V/x jzew/}Ko { D^Wg2B7>= _y~ ?Ϯz?$q~.}ceɅ&X8RGh]#IQlhx^G\d"_cx:o~0_Ttw&vgX@oe~ǖ-.0}o\+cAd~WM,HnJbף[w}\x<{mOuX#[+XD>xJ??XNZoTb*SVGQu xtP摏4}J?wMnrя~5Y.^-MVdxwl喵ڞ~nWfz WB:[lQ5[@4y7b= sÇeurpLlU{#@iX1 4D Y]JӨ)?؞._+ i|g[88RfguV9+fYyqÞ|_)wsϭz׻>hG ?Ow gpBέSc% [,'0*^C'xx lɩCluYq.gXmN.Ⱥ8:L^M{^ɻ6߾˩u{]ݛo5ΖT{NrH~ip\`˦r=E20w:7rY[9 1蓗oON3+:Gvjsp{WMdM*.@X0#Ҳ\Wtع@j)ێEʗfUrd;}e~R^3`LP;H,]YأʑgN+wZx (o}[cSXᆹIbYYc.^(sn߆sG> 58,;d豛4=_sT$ecdCꢗ4illӑO}ltv#z~#,;\"s03tzNm9!壌GFF>i#BS7-Jg8|pHg*R/z Je4ɂm>21}tB8rCWfC蕝LwD`=ix;^җWcO(}݇c3>|"{I??Sem{_bҊ_8 ?_-,b_zKΣp-SJ4 H( CuE`6=ɣ~5xЕW^YI>;EqgV3mzl_=; FA0Svϯ(xH|0o傿uO0:r˷蚥Bm.[NSO~KcO{O `<HޣQi!B묳Ny8|j'z@0vȻ'|.xLƁZn~! U"FUDPVLA>eݱ]`.>#pmD`eFy^xa5\7<y:/"|V'=mO/C`d0&LT o[%w',"dU8a|N'Ð4cŏȡ&>دG:y>nl̄6iMm?~Cj}B 뮻n-ԤWyX^[z%M%Wxd|;\Y?u JfFZ[dھƼRI JkAeS {:*[zO}<4?7N;0w댒qvgՎO%o]nVuN@5.[ MNM2 DgŁ1l4TgGzkƓÏ{38/!TrLϯ>G^w;B9:Z\e#v4y1M>G"#AF>2XYm~Si۩G`Y#'a_<\ٳzHF/(+͡Nїﴭ0Gy ;ԙ]壯^ʬNG###E }_,~NW= , +#'guO F[~b-m q ~hLc7yeuNڿhr{+AL}dѕe#r? šv|ɶ}U}>&vr[ڋqz]@w3M\}I}##zeɫ7 `}LHB:D%OI] CF2HesDŏv[Ӷ/$ե-ҮA{C&h<ïGꢏ|':ĢǿOvgo9?I~Pd;eȻD&>ao<% '+b;elt('OA|r+ZCBDJ=AȲAJ;Pn~(udH0A&礎|ڨ9[ɷsa_rϘ1F!cx|ƒ40XƏ\ɒaWYJ{Zdtˣ dV+y+>f?'D(CK^qF(-oc+2l8!᭿Lt؊NB/a/AT=dFxH9mSwhcѕF7œh2JQ_RGƓ|Ɲ<(2ДإF*O#N'G>i<9G{ndGd|2R6pQ4N~{m i( /2>YkKҖ؍\-Uo߮ ybIG'yiL 2Yyr9T8o ?tr|Nxt"/Թƞ?/cttO3y,Եzx3F+':i>=4OW="/=c:oz^]:P'$(HPrLVA@ӑHx|,=uSg_-ˣd0|Dߑ/-@Svԅ@s3g_]1_Q(3bW]xz.qҴ8|&Rl %'ÿ4-6UOAj;Q +谇P(K`OJքxK0[9QuآwR2tl|[KzzǖV?\[ Ɲxvesql덿 ` >s(ǖFM`g+;N'tpGb?(*gq'-O(`OF>'n7zd݂E|;9 M`V爏%HSs%ʧ-]>ĒcS&ؗ:Loͥ\թ81L2ɓa/'rkڣO_32.RNqӎ=2#!|c-\m-GOꠗ6O>Gx(mO/#My-KGQ4j&Axr(rjt*>ȷx=?#R'' E7TŖT]ܴY]+/Ҝob>9Wҿz *bAƔ4clJd͘LH>~/m6 fXFVӍ^ʉs:)Nء>uU:9b#bȑIC^d?<Ŗmz)G>qi:vK^?uk-mzouo%]<:89|O2\]kL>dQ(uC#C7 [(u|bްvKJYF:|y2%U.GZ@,|鰝_ Ba'&r%./mI]&9Nm;}e)JF8{_Rb:.^=\[WW+g!29.R6"G/cT]DVE&$yzxyKo(1B! C)0瑓WT>v|գ4P9S&l]e~).d ɳ?qh}yҩ)w=Mlf-3mǟq _ї'~쥜s!^pG}uO}z_u6>8Xr)3A #ev|a?ne765Z}dح!+x6%rK~ICю6OFK8tco1H73#/G./c'5L|^SfͳݥI[ٜ_.6Zӆ9J%)c:1GdQO 'vHEM|9lʧPkS9r|IJ_Ҵ;_HKLMh}Θ5vħ# 0*g^hdRy:(Mѧ#zBWN=Y}JG}㡴_>)hdN1Zc uR؉ȗ@.yS]eirۮŎruэ<ٶ}œG'FMv+)%>:;^ߒO<owu "@wNO>;v%*JToZރ>׻~k9y_X9KqV?fbk_jtgVG9Kq];%YGMXĉ]:r3AXr/߮s78#=Sќy}0ۘn_G{sa^G[aX=\q_^|vl oz>ͽʭ)wH$ÈGun\˭ʫgx?q/+~'u\o|wOa]-cc^71Z{\q5#3y\ׯVhm"ZóĬy2199KMw'xm-vyIkâY7L_j>~ƟOoX=NՇx./'j߽"op0LOᮤ__<^V.\>֦ۜ`14/h]_<䏃T_stuXW3:|?~:N?98]?Qg?ϞJuIFV'\__q7XU.?(=;.V9՝"/83ׅ/KbHD'yE)x?.Y~4WM/?4fվ{,u)N!g/8xyƣߓy5Ss33\.ƽ=fx&?p7uL rfy" o ڄ9.O)./nCm W]ò;7Ly%6jG0+Gyzϣ?WwӟD ŷivȏGNGw 5UuWyz*Xds>ɫ6e94`_}9KƜzq4Y~ۛXZi_so>b2|=W11.az;ܵ9[w/-ziodp\9⮛  "gYF6_Vm>xo&_|uCHvq<8=/? c|yb?zU9b<8&'5{.^0҆>i/J?<ܕi Y38ރisD06ߜX8 k'mqߡ/W}|OO;?b/]9}soJMOY#Ub]OXΰ N}ƞ/< p#6lE*Ɓ! _<9lW5L~6፭^xV/ꯞnӗ|;zO'>g`2=a]P~=n\}|Kך+_`Ya։VAosŭYp:cwȫ!V;Zr1Qܘ>l]__|_ k{3GWEsOw.}a_V^ugãxk132cg] x+pabOy|e랳ÇQ|Fk1;5;W35Z~tZE?}U}??q\1/f^/vyE~asӤ퉧|9ׯVPN6C11>>Uqaڄu>$|/S ;.5TSakWu! cʉ?=80N#.G;s/ޛO z}꩗'^f?,J_L={'OY1kqz09}܁]p1Ecpwڠ|=~i~+~^q&LgX}E vpzI?]_wT[{ G19ϜR,IENDB`././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/docs/source/traitsui_user_manual/testing/tutorials/test_with_nested_object.rst0000644000175100001730000001753700000000000034030 0ustar00runnerdocker00000000000000 Testing with nested objects =========================== Suppose we have a GUI application like this:: from traits.api import Enum, HasStrictTraits, Instance, Float, observe, Str from traitsui.api import Item, InstanceEditor, View class PayCheck(HasStrictTraits): name = Str() pay = Float() class App(HasStrictTraits): min_hourly_wage = Float(10.0) n_hours = Enum([25, 40, 45]) pay_check = Instance(PayCheck) def _pay_check_default(self): return PayCheck(pay=self.min_hourly_wage*self.n_hours) @observe("min_hourly_wage") @observe("n_hours") def _pay_updated(self, event): self.pay_check.pay = self.min_hourly_wage * self.n_hours view = View( Item("min_hourly_wage", label="Hourly wage"), Item("n_hours", label="Number of work hours"), Item("pay_check", editor=InstanceEditor(), style="custom"), ) app = App() app.configure_traits() .. figure:: images/test_with_nested_object/init-app.png When the user selects a different number of work hours in the combo box, the pay is updated accordingly. This behavior is simple enough to be tested without GUI, but we will to write a GUI test for demonstration purposes. The test will need to: #. Find the combo box for the number of work hours field #. Locate a different item in the combo box #. Click that item to modify the number of work hours #. Find the nested 'pay' field inside 'PayCheck' #. Check the pay is updated to the expected value Step 1: Find the combo box for the number of work hours field ------------------------------------------------------------- First of all, find the combo box using the name defined in |Item|:: from traitsui.testing.api import UITester app = App() tester = UITester() with tester.create_ui(app) as ui: n_hours_combobox = tester.find_by_name(ui, "n_hours") Step 2: Locate a different item in the combo box ------------------------------------------------ Let's take a look at the functionality supported with this ``n_hours_combobox``. .. code-block:: n_hours_combobox.help() prints something like this (abbreviated for the purpose of this section):: Interactions ------------ An object representing the user clicking a key on the keyboard. ... An object representing the user typing a sequence of keys. ... An object representing an interaction to obtain the displayed (echoed) plain text. ... An object representing an interaction to obtain the displayed (echoed) plain text which is currently selected. E.g. For a Enum List, with one entry currently selected, the displayed selected text would be the label of that entry. ... Locations --------- A locator for locating a target that is uniquely specified by a single 0-based index. The |Index| object allows us to locate an indexed item on the combo box. Objects listed in the "Locations" section can be used with |UIWrapper.locate|. So we add this line:: second_item = n_hours_combobox.locate(Index(1)) Now our test looks like this:: from traitsui.testing.api import Index, UITester app = App() tester = UITester(delay=500) with tester.create_ui(app) as ui: n_hours_combobox = tester.find_by_name(ui, "n_hours") second_item = n_hours_combobox.locate(Index(1)) Step 3: Modify the number of work hours --------------------------------------- The returned value of |UIWrapper.locate| is another instance of |UIWrapper|. We can find what are supported on ``second_item`` using the |UIWrapper.help| method again. .. code-block:: second_item.help() prints something like this (abbreviated, again):: Interactions ------------ An object representing the user clicking a mouse button. Currently the left mouse button is assumed. ... Locations --------- No locations are supported. This help message indicates that we can click this second item. In our application context, this will select the second item in the combo box:: second_item.perform(MouseClick()) Now this is how the test looks like so far:: from traitsui.testing.api import Index, MouseClick, UITester app = App() tester = UITester() with tester.create_ui(app) as ui: n_hours_combobox = tester.find_by_name(ui, "n_hours") second_item = n_hours_combobox.locate(Index(1)) second_item.perform(MouseClick()) Step 4: Find the nested 'pay' field inside 'PayCheck' ----------------------------------------------------- This step is trickier. The ``pay`` field is nested inside the ``pay_check`` item, how do we get to it? Let's get the ``pay_check`` object via the tester and see what we have:: pay_check_pane = tester.find_by_name(ui, "pay_check") pay_check_pane.help() The last line prints something like this (abbreviated, again):: Interactions ------------ No interactions are supported. Locations --------- A locator for locating the next UI target using an id. ... A locator for locating the next UI target using a name. ... We can see that |TargetById| and |TargetByName| are available for locating the next GUI element. We can locate the text box for ``pay_check.pay`` using |TargetByName|:: pay_field = pay_check_pane.locate(TargetByName("pay")) In fact, |UIWrapper.find_by_id| and |UIWrapper.find_by_name| are simply aliases for |TargetById| and |TargetByName| respectively, so you can also write:: pay_field = pay_check_pane.find_by_name("pay") Now this is how the test looks like so far:: from traitsui.testing.api import Index, MouseClick, UITester app = App() tester = UITester() with tester.create_ui(app) as ui: n_hours_combobox = tester.find_by_name(ui, "n_hours") n_hours_combobox.locate(Index(1)).perform(MouseClick()) pay_field = tester.find_by_name(ui, "pay_check").find_by_name("pay") Step 5: Check the pay is updated to the expected value ------------------------------------------------------ ``pay_field`` wraps a text box. We have seen in :ref:`testing-tutorial-first-test` how to obtain the displayed text:: from traitsui.testing.api import DisplayedText displayed_pay = pay_field.inspect(DisplayedText()) assert displayed_pay == "400.0" Our final test -------------- .. code-block:: from traitsui.testing.api import Index, DisplayedText, MouseClick, UITester app = App() tester = UITester() with tester.create_ui(app) as ui: n_hours_combobox = tester.find_by_name(ui, "n_hours") n_hours_combobox.locate(Index(1)).perform(MouseClick()) pay_field = tester.find_by_name(ui, "pay_check").find_by_name("pay") displayed_pay = pay_field.inspect(DisplayedText()) assert displayed_pay == "400.0" What did we learn? ------------------ - From the output of |UIWrapper.help|, objects listed in the "Locations" can be used with |UIWrapper.locate| for locating nested GUI components. - |UIWrapper.find_by_name| and |UIWrapper.find_by_id| are aliases making use of the |UIWrapper.locate| method. - |UIWrapper.locate| returns an instance of |UIWrapper| on which |UIWrapper.locate| can be called again; |UIWrapper.perform| and |UIWrapper.inspect| can be used on the nested object located. .. include:: ../substitutions.rst ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/docs/source/traitsui_user_manual/testing.rst0000644000175100001730000000244300000000000025066 0ustar00runnerdocker00000000000000.. _testing-traitsui-applications: ============================= Testing TraitsUI Applications ============================= TraitsUI provides a toolkit independent API to help developers check the behavior of their applications in automated tests. Most of the testing functionality can be accessed via the |testing.api| module. .. note:: The testing library is relatively new compared to other features in TraitsUI. Built-in support for testing TraitsUI editors are continuously being added. Get started =========== .. toctree:: :maxdepth: 1 testing/tutorials/first_test testing/tutorials/test_with_nested_object How-to guides ============= .. toctree:: :maxdepth: 1 testing/howtos/add_new_interaction testing/howtos/add_new_location Discussions =========== .. toctree:: :maxdepth: 1 testing/discussions/automated_vs_manual_test testing/discussions/debugging-gui-tests-at-runtime testing/discussions/compatibility_pyface_testing testing/discussions/event_loop_and_exceptions testing/discussions/target_interaction_location testing/discussions/working_of_extensions Technical Reference =================== .. toctree:: :maxdepth: 1 testing/references/examples API Reference <../api/traitsui.testing> .. include:: testing/substitutions.rst ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/docs/source/traitsui_user_manual/tips.rst0000644000175100001730000000520300000000000024365 0ustar00runnerdocker00000000000000 .. _tips-tricks-and-gotchas: ======================== Tips, Tricks and Gotchas ======================== Getting and Setting Model View Elements --------------------------------------- For some applications, it can be necessary to retrieve or manipulate the View objects associated with a given model object. The HasTraits class defines two methods for this purpose: trait_views() and trait_view(). .. _trait-views: trait_views() ````````````` The trait_views() method, when called without arguments, returns a list containing the names of all Views defined in the object's class. For example, if **sam** is an object of type SimpleEmployee3 (from :ref:`Example 6 `), the method call ``sam.trait_views()`` returns the list ``['all_view', 'traits_view']``. Alternatively, a call to :samp:`trait_views({view_element_type})` returns a list of all named instances of class *view_element_type* defined in the object's class. The possible values of *view_element_type* are: - :term:`View` - :term:`Group` - :term:`Item` - :term:`ViewElement` - ViewSubElement Thus calling ``trait_views(View)`` is identical to calling ``trait_views()``. Note that the call ``sam.trait_views(Group)`` returns an empty list, even though both of the Views defined in SimpleEmployee contain Groups. This is because only *named* elements are returned by the method. Group and Item are both subclasses of ViewSubElement, while ViewSubElement and View are both subclasses of ViewElement. Thus, a call to ``trait_views(ViewSubElement)`` returns a list of named Items and Groups, while ``trait_views(ViewElement)`` returns a list of named Items, Groups and Views. trait_view() ```````````` The trait_view() method is used for three distinct purposes: - To retrieve the default View associated with an object - To retrieve a particular named ViewElement (i.e., Item, Group or View) - To define a new named ViewElement For example: - ``obj.trait_view()`` returns the default View associated with object *obj*. For example, ``sam.trait_view()`` returns the View object called ``traits_view``. Note that unlike trait_views(), trait_view() returns the View itself, not its name. - ``obj.trait_view('my_view')`` returns the view element named ``my_view`` (or None if ``my_view`` is not defined). - ``obj.trait_view('my_group', Group('a', 'b'))`` defines a Group with the name ``my_group``. Note that although this Group can be retrieved using ``trait_view()``, its name does not appear in the list returned by ``traits_view(Group)``. This is because ``my_group`` is associated with *obj* itself, rather than with its class. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/docs/source/traitsui_user_manual/view.rst0000644000175100001730000005676500000000000024403 0ustar00runnerdocker00000000000000.. _the-view-and-its-building-blocks: ================================ The View and Its Building Blocks ================================ A simple way to edit (or simply observe) the attribute values of a :term:`HasTraits` object in a GUI window is to call the object's configure_traits() [3]_ method. This method constructs and displays a window containing editable fields for each of the object's :term:`trait attribute`\ s. For example, the following sample code [4]_ defines the SimpleEmployee class, creates an object of that class, and constructs and displays a GUI for the object: .. index:: pair: examples; configure_traits() .. _example-1-using-configure-traits: .. rubric:: Example 1: Using configure_traits() :: # configure_traits.py -- Sample code to demonstrate configure_traits() from traits.api import HasTraits, Int, Str class SimpleEmployee(HasTraits): first_name = Str() last_name = Str() department = Str() employee_number = Str() salary = Int() sam = SimpleEmployee() sam.configure_traits() Unfortunately, the resulting form simply displays the attributes of the object **sam** in alphabetical order with little formatting, which is seldom what is wanted: .. figure:: images/ui_for_ex1.jpg :alt: Dialog box showing all attributes of SimpleEmployee in alphabetical order Figure 1: User interface for Example 1 .. index:: object: View .. _the-view-object: The View Object --------------- In order to control the layout of the interface, it is necessary to define a View object. A View object is a template for a GUI window or panel. In other words, a View specifies the content and appearance of a TraitsUI window or panel display. For example, suppose you want to construct a GUI window that shows only the first three attributes of a SimpleEmployee (e.g., because salary is confidential and the employee number should not be edited). Furthermore, you would like to specify the order in which those fields appear. You can do this by defining a View object and passing it to the configure_traits() method: .. index:: configure_traits(); view parameter, examples; View object .. _example-2-using-configure-traits-with-a-view-object: .. rubric:: Example 2: Using configure_traits() with a View object .. literalinclude:: examples/configure_traits_view.py :start-at: configure_traits_view.py The resulting window has the desired appearance: .. figure:: images/ui_for_ex2.jpg :alt: User interface showing only First name, Last name, and Department Figure 2: User interface for Example 2 A View object can have a variety of attributes, which are set in the View definition, following any Group or Item objects. The sections on :ref:`contents-of-a-view` through :ref:`advanced-view-concepts` explore the contents and capabilities of Views. Refer to the *Traits API Reference* for details of the View class. Except as noted, all example code uses the configure_traits() method; a detailed description of this and other techniques for creating GUI displays from Views can be found in :ref:`displaying-a-view`. .. index:: View; contents object: View .. _contents-of-a-view: Contents of a View ------------------ The contents of a View are specified primarily in terms of two basic building blocks: Item objects (which, as suggested by Example 2, correspond roughly to individual trait attributes), and Group objects. A given View definition can contain one or more objects of either of these types, which are specified as arguments to the View constructor, as in the case of the three Items in Example 2. The remainder of this chapter describes the Item and Group classes. .. index:: widget, control object: Item .. _the-item-object: The Item Object ``````````````` The simplest building block of a View is the :term:`Item` object. An Item specifies a single interface :term:`widget`, usually the display for a single trait attribute of a HasTraits object. The content, appearance, and behavior of the widget are controlled by means of the Item object's attributes, which are usually specified as keyword arguments to the Item constructor, as in the case of *name* in Example 2. The remainder of this section describes the attributes of the Item object, grouped by categories of functionality. It is not necessary to understand all of these attributes in order to create useful Items; many of them can usually be left unspecified, as their default values are adequate for most purposes. Indeed, as demonstrated by earlier examples, simply specifying the name of the trait attribute to be displayed is often enough to produce a usable result. The following table lists the attributes of the Item class, organized by functional categories. Refer to the *Traits API Reference* for details on the Item class. .. index:: attributes; Item, Item; attributes .. index:: name attribute, dock attribute; Item, emphasized attribute .. index:: export attribute; Item, height attribute; Item, image attribute; .. index:: Item, label attribute; Item, padding attribute; Item .. index:: resizable attribute, show_label attribute, springy attribute; Item .. index:: width attribute; Item, format_str attribute, format_func attribute .. index:: editor attribute, style attribute; Item, enabled_when attribute; Item .. index:: visible_when attribute; Item, defined_when attribute; Item .. index:: has_focus attribute, tooltip attribute, help attribute; Item .. index:: help_id attribute; Item, id attribute; Item .. _attributes-of-item-by-category-table: .. rubric:: Attributes of Item, by category Content These attributes specify the actual data to be displayed by an item. Because an Item is essentially a template for displaying a single trait, its **name** attribute is nearly always specified. name: str The name of the trait being edited. Display format In addition to specifying which trait attributes are to be displayed, you might need to adjust the format of one or more of the resulting widgets. If an Item's **label** attribute is specified but not its name, the value of **label** is displayed as a simple, non-editable string. (This feature can be useful for displaying comments or instructions in a TraitsUI window.) dock: Docking style for the item. emphasized: bool Should label text be emphasized? export: Category of elements dragged from view. height: Requested height as pixels (height > 1) or proportion of screen (0 < height < 1) image: Image to show on tabs. label: str The label to display on the item. padding: int Amount of extra space, in pixels, to add around the item. Values must be integers between -15 and 15. Use negative values to subtract from the default spacing. resizable: bool Can the item be resized to use extra space. The default is False. show_label: bool Whether to show the label or not (defaults to True). springy: bool Use extra space in the parent layout? The default is False. width: float Requested width as pixels (width > 1) or proportion of screen (0 < width < 1). Content format In some cases it can be desirable to apply special formatting to a widget's contents rather than to the widget itself. Examples of such formatting might include rounding a floating-point value to two decimal places, or capitalizing all letter characters in a license plate number. format_str: str '% format' string for text. format_func: func Format function for text. Widget override These attributes override the widget that is automatically selected by TraitsUI. These options are discussed in :ref:`introduction-to-trait-editor-factories` and :ref:`the-predefined-trait-editor-factories`. editor: ItemEditor Editor to use. style: {'simple', 'custom', 'text', 'readonly'} The editor style (see :ref:`specifying-an-editor-style`). Visibility and status Use these attributes to create a simple form of a dynamic GUI, which alters the display in response to changes in the data it contains. More sophisticated dynamic behavior can be implemented using a custom :term:`Handler` (see :ref:`controlling-the-interface-the-handler`). enabled_when: str Python expression that determines whether the group can be edited. The expression will be evaluated any time a trait on an object in the UI's context is changed. As a result, changes to nested traits that don't also change a trait on some object in the context may not trigger the expression to be evaluated. visible_when: str Python expression that determines visibility of group. The expression will be evaluated any time a trait on an object in the UI's context is changed. As a result, changes to nested traits that don't also change a trait on some object in the context may not trigger the expression to be evaluated. defined_when: str Expression that determines inclusion of group in parent. has_focus: bool Should this item get initial focus? User help These attributes provide guidance to the user in using the user interface. tooltip: str Tooltip to display on mouse-over. help: If the **help** attribute is not defined for an Item, a system-generated message is used instead. help_id: It is ignored by the default help handler, but can be used by a custom help handler. .. TODO: Add sample help screen Unique identifier id: Used as a key for saving user preferences about the widget. If **id** is not specified, the value of the **name** attribute is used. .. index:: Custom class, Label class, Heading class, Readonly class, Spring class, UCustom class, UItem class, UReadonly class pair: Item; subclasses .. _subclasses-of-item: Subclasses of Item `````````````````` The TraitsUI package defines the following subclasses of Item, which are helpful shorthands for defining certain types of items. Label, Heading and Spring are intended to help with the layout of a TraitsUI View, and need not have a trait attribute associated with them. For example, ``Spring()`` and ``Label("This is a label")`` are valid code. +-----------+------------------------------+-----------------------------------------+ | Subclass | Description | Equivalent To | +===========+==============================+=========================================+ | Label | An item that is just a label | | | | and doesn't require a trait | | | | name associated with it | | +-----------+------------------------------+-----------------------------------------+ | Heading | A fancy label | | +-----------+------------------------------+-----------------------------------------+ | Spring | A item that expands to take | :samp:`Item(name='spring', | | | as much space as necessary | springy=True, show_label=False)` | +-----------+------------------------------+-----------------------------------------+ | Custom | An item with a custom editor | :samp:`Item(style='custom')` | | | style | | +-----------+------------------------------+-----------------------------------------+ | Readonly | An item with a readonly | :samp:`Item(style='readonly')` | | | editor style | | +-----------+------------------------------+-----------------------------------------+ | UItem | An item with no label | :samp:`Item(show_label=False)` | +-----------+------------------------------+-----------------------------------------+ | UCustom | A Custom item with no label | :samp:`Item(style='custom', | | | | show_label=False)` | +-----------+------------------------------+-----------------------------------------+ | UReadonly | A Readonly item with no | :samp:`Item(style='readonly', | | | label | show_label=False)` | +-----------+------------------------------+-----------------------------------------+ .. index: object: Group .. _the-group-object: The Group Object ```````````````` The preceding sections have shown how to construct windows that display a simple vertical sequence of widgets using instances of the View and Item classes. For more sophisticated interfaces, though, it is often desirable to treat a group of data elements as a unit for reasons that might be visual (e.g., placing the widgets within a labeled border) or logical (activating or deactivating the widgets in response to a single condition, defining group-level help text). In TraitsUI, such grouping is accomplished by means of the :term:`Group` object. Consider the following enhancement to Example 2: .. index:: pair: configure_traits(); examples triple: View; Group; examples .. _example-3-using-configure-traits-with-a-view-and-a-group-object: .. rubric:: Example 3: Using configure_traits() with a View and a Group object .. literalinclude:: examples/configure_traits_view_group.py :start-at: configure_traits_view_group.py The resulting window shows the same widgets as before, but they are now enclosed in a visible border with a text label: .. figure:: images/ui_for_ex3.jpg :alt: User interface showing three fields enclosed in a border Figure 3: User interface for Example 3 .. indexx: pair: contents; Group .. _content-of-a-group: Content of a Group :::::::::::::::::: The content of a Group object is specified exactly like that of a View object. In other words, one or more Item or Group objects are given as arguments to the Group constructor, e.g., the three Items in Example 3. [5]_ The objects contained in a Group are called the *elements* of that Group. Groups can be nested to any level. .. index:: pair: attributes; Group .. _group-attributes: Group Attributes :::::::::::::::: The following table lists the attributes of the Group class, organized by functional categories. As with Item attributes, many of these attributes can be left unspecified for any given Group, as the default values usually lead to acceptable displays and behavior. See the *Traits API Reference* for details of the Group class. .. index:: object attribute; Group, content attribute; Group .. index:: label attribute; Group, show_border attribute, show_labels attribute .. index:: show_left attribute, padding attribute; Group, layout attribute .. index:: selected attribute, orientation attribute, style attribute; Group .. index:: columns attribute, dock attribute; Group, dock_theme attribute; .. index:: Group, image attribute; Group export attribute; Group, .. index:: springy attribute; Group .. _attributes-of-group-by-category-table: .. rubric:: Attributes of Group, by category Content object: References the object whose traits are being edited by members of the group; by default this is 'object', but could be another object in the current context. content: list List of elements in the group. Display format These attributes define display options for the group as a whole. columns: The number of columns in the group. dock: Dock style of sub-groups. dock_theme: The theme to use for the dock. export: Category of elements dragged from view. image: Image to show on tabs. label: The label to display on the group. layout: {'normal', 'flow', 'split', 'tabbed'} Layout style of the group, which can be one of the following: * 'normal' (default): Sub-groups are displayed sequentially in a single panel. * 'flow': Sub-groups are displayed sequentially, and then "wrap" when they exceed the available space in the **orientation** direction. * 'split': Sub-groups are displayed in a single panel, separated by "splitter bars", which the user can drag to adjust the amount of space for each sub-group. * 'tabbed': Each sub-group appears on a separate tab, labeled with the sub-group's *label* text, if any. This attribute is ignored for groups that contain only items, or contain only one sub-group. orientation: {'vertical', 'horizontal'} The orientation of the subgroups. padding: int Amount of extra space, in pixels, to add around the item. Values must be integers between -15 and 15. Use negative values to subtract from the default spacing. selected: In a tabbed layout, should this be the visible tab? show_border: bool Should a border be shown or not show_labels: Show the labels of items? show_left: bool Show labels on the left or the right. springy: bool Use extra space in the parent layout? The default is False. style: {'simple', 'custom', 'text', 'readonly'} Default editor style of items in the group. .. index:: enabled_when attribute; Group .. index:: visible_when attribute; Group .. index:: defined_when attribute; Group .. index:: help attribute; Group .. index:: help_id attribute; Group .. index:: id attribute; Group Visibility and status These attributes work similarly to the attributes of the same names on the Item class. enabled_when: str Python expression that determines whether the group can be edited. The expression will be evaluated any time a trait on an object in the UI's context is changed. As a result, changes to nested traits that don't also change a trait on some object in the context may not trigger the expression to be evaluated. visible_when: str Python expression that determines visibility of group. The expression will be evaluated any time a trait on an object in the UI's context is changed. As a result, changes to nested traits that don't also change a trait on some object in the context may not trigger the expression to be evaluated. defined_when: str Expression that determines inclusion of group in parent. .. TODO: Does Item-level or Group-level take precedence? Find out and document. User help The help text is used by the default help handler only if the group is the only top-level group for the current View. For example, suppose help text is defined for a Group called **group1**. The following View shows this text in its help window:: View(group1) The following two do not:: View(group1, group2) View(Group(group1)) help: str Help message. If the **help** attribute is not defined, a system-generated message is used instead. help_id: The **help_id** attribute is ignored by the default help handler, but can be used by a custom help handler. .. TODO: The document needs to include material on organizing Views via Groups, including the implied top-level group of every View. If we do thiss earlier in the document, it will probably simplify this. Unique identifier id: str The **id** attribute is used as a key for saving user preferences about the widget. If **id** is not specified, the **id** values of the elements of the group are concatenated and used as the group identifier. .. index:: pair: subclasses; Group .. _subclasses-of-group: Subclasses of Group ``````````````````` The TraitsUI package defines the following subclasses of Group, which are helpful shorthands for defining certain types of groups. Refer to the *Traits API Reference* for details. .. index:: HGroup, HFlow, HSplit, Tabbed, VGroup, VFlow, VGrid, VFold, VSplit .. _subclasses-of-group_table: .. rubric:: Subclasses of Group +-----------+------------------------------+-----------------------------------------+ |Subclass |Description |Equivalent To | +===========+==============================+=========================================+ |HGroup |A group whose items are laid |:samp:`Group(orientation='horizontal')` | | |out horizontally. | | +-----------+------------------------------+-----------------------------------------+ |HFlow |A horizontal group whose items|:samp:`Group(orientation='horizontal', | | |"wrap" when they exceed the |layout='flow', show_labels=False)` | | |available horizontal space. | | +-----------+------------------------------+-----------------------------------------+ |HSplit |A horizontal group with |:samp:`Group(orientation='horizontal', | | |splitter bars to separate it |layout='split')` | | |from other groups. | | +-----------+------------------------------+-----------------------------------------+ |Tabbed |A group that is shown as a tab|:samp:`Group(orientation='horizontal' | | |in a notebook. |layout='tabbed', springy=True)` | +-----------+------------------------------+-----------------------------------------+ |VGroup |A group whose items are laid |:samp:`Group(orientation='vertical')` | | |out vertically. | | +-----------+------------------------------+-----------------------------------------+ |VFlow |A vertical group whose items |:samp:`Group(orientation='vertical', | | |"wrap" when they exceed the |layout='flow', show_labels=False)` | | |available vertical space. | | +-----------+------------------------------+-----------------------------------------+ |VFold |A vertical group in which |:samp:`Group(orientation='vertical', | | |items can be collapsed (i.e., |layout='fold', show_labels=False)` | | |folded) by clicking their | | | |titles. | | +-----------+------------------------------+-----------------------------------------+ |VGrid |A vertical group whose items |:samp:`Group(orientation='vertical', | | |are laid out in two columns. |columns=2)` | +-----------+------------------------------+-----------------------------------------+ |VSplit |A vertical group with splitter|:samp:`Group(orientation='vertical', | | |bars to separate it from other|layout='split')` | | |groups. | | +-----------+------------------------------+-----------------------------------------+ .. rubric:: Footnotes .. [3] If the code is being run from a program that already has a GUI defined, then use edit_traits() instead of configure_traits(). These methods are discussed in more detail in :ref:`displaying-a-view`. .. [4] All code examples in this guide that include a file name are also available as examples in the :file:`tutorials/doc_examples/examples` subdirectory of the Traits docs directory. You can run them individually, or view them in a tutorial program by running: :program:`python` :file:`{Traits_dir}/tutorials/tutor.py` :file:`{Traits_dir}/docs/tutorials/doc_examples` .. [5] As with Views, it is possible for a Group to contain objects of more than one type, but it is not recommended. ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1684768028.0278072 traitsui-8.0.0/docs/source/tutorials/0000755000175100001730000000000000000000000020443 5ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1684768028.0278072 traitsui-8.0.0/docs/source/tutorials/code_snippets/0000755000175100001730000000000000000000000023302 5ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/docs/source/tutorials/code_snippets/code_block0.py0000644000175100001730000000057100000000000026023 0ustar00runnerdocker00000000000000# code_block0.py from numpy import cos, sin class Point(object): """3D Points objects""" x = 0.0 y = 0.0 z = 0.0 def rotate_z(self, theta): """rotate the point around the Z axis""" xtemp = cos(theta) * self.x + sin(theta) * self.y ytemp = -sin(theta) * self.x + cos(theta) * self.y self.x = xtemp self.y = ytemp ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/docs/source/tutorials/code_snippets/code_block1.py0000644000175100001730000000122200000000000026016 0ustar00runnerdocker00000000000000# code_block1.py from traits.api import CInt, Enum, HasTraits class Camera(HasTraits): """Camera object""" gain = Enum( 1, 2, 3, desc="the gain index of the camera", label="gain", ) exposure = CInt( 10, desc="the exposure time, in ms", label="Exposure", ) def capture(self): """Captures an image on the camera and returns it""" print( "capturing an image at %i ms exposure, gain: %i" % (self.exposure, self.gain) ) if __name__ == "__main__": camera = Camera() camera.configure_traits() camera.capture() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/docs/source/tutorials/code_snippets/container.py0000644000175100001730000000160000000000000025633 0ustar00runnerdocker00000000000000# container.py from traits.api import CInt, Enum, HasTraits, Instance, String from traitsui.api import Item, View class Camera(HasTraits): """Camera object""" gain = Enum( 1, 2, 3, desc="the gain index of the camera", label="gain", ) exposure = CInt( 10, desc="the exposure time, in ms", label="Exposure", ) class Display(HasTraits): string = String() view = View(Item('string', show_label=False, springy=True, style='custom')) class Container(HasTraits): camera = Instance(Camera, ()) display = Instance(Display, ()) view = View( Item( 'camera', style='custom', show_label=False, ), Item( 'display', style='custom', show_label=False, ), ) Container().configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/docs/source/tutorials/code_snippets/echo_box.py0000644000175100001730000000032500000000000025442 0ustar00runnerdocker00000000000000# echo_box.py from traits.api import HasTraits, Str class EchoBox(HasTraits): input = Str() output = Str() def _input_changed(self): self.output = self.input EchoBox().configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/docs/source/tutorials/code_snippets/event_loop.py0000644000175100001730000000026300000000000026027 0ustar00runnerdocker00000000000000# event_loop.py from traits.api import HasTraits, Int from pyface.api import GUI class Counter(HasTraits): value = Int() Counter().edit_traits() GUI().start_event_loop() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/docs/source/tutorials/code_snippets/event_loop_qt.py0000644000175100001730000000031400000000000026530 0ustar00runnerdocker00000000000000# event_loop_qt.py from pyface.qt.QtGui import QApplication from traits.api import HasTraits, Int class Counter(HasTraits): value = Int() Counter().edit_traits() QApplication.instance().exec_() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/docs/source/tutorials/code_snippets/event_loop_wx.py0000644000175100001730000000024100000000000026541 0ustar00runnerdocker00000000000000# event_loop_wx.py import wx from traits.api import HasTraits, Int class Counter(HasTraits): value = Int() Counter().edit_traits() wx.App().MainLoop() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/docs/source/tutorials/code_snippets/interactive.py0000644000175100001730000000056400000000000026176 0ustar00runnerdocker00000000000000# interactive.py from traits.api import HasTraits, Int, Button, observe from traitsui.api import View, Item, ButtonEditor class Counter(HasTraits): value = Int() add_one = Button() @observe('add_one') def _increment_value(self, event): self.value += 1 view = View('value', Item('add_one', show_label=False)) Counter().configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/docs/source/tutorials/code_snippets/mpl_figure_editor.py0000644000175100001730000000403200000000000027352 0ustar00runnerdocker00000000000000# mpl_figure_editor.py import wx import matplotlib # We want matplotlib to use a wxPython backend matplotlib.use('WXAgg') from matplotlib.backends.backend_wxagg import FigureCanvasWxAgg as FigureCanvas from matplotlib.figure import Figure from matplotlib.backends.backend_wx import NavigationToolbar2Wx from traits.api import Any, Instance from traitsui.wx.editor import Editor from traitsui.wx.basic_editor_factory import BasicEditorFactory class _MPLFigureEditor(Editor): scrollable = True def init(self, parent): self.control = self._create_canvas(parent) self.set_tooltip() def update_editor(self): pass def _create_canvas(self, parent): """Create the MPL canvas.""" # The panel lets us add additional controls. panel = wx.Panel(parent, -1, style=wx.CLIP_CHILDREN) sizer = wx.BoxSizer(wx.VERTICAL) panel.SetSizer(sizer) # matplotlib commands to create a canvas mpl_control = FigureCanvas(panel, -1, self.value) sizer.Add(mpl_control, 1, wx.LEFT | wx.TOP | wx.GROW) toolbar = NavigationToolbar2Wx(mpl_control) sizer.Add(toolbar, 0, wx.EXPAND) self.value.canvas.SetMinSize((10, 10)) return panel class MPLFigureEditor(BasicEditorFactory): klass = _MPLFigureEditor if __name__ == "__main__": # Create a window to demo the editor from traits.api import HasTraits from traitsui.api import View, Item from numpy import sin, cos, linspace, pi class Test(HasTraits): figure = Instance(Figure, ()) view = View( Item('figure', editor=MPLFigureEditor(), show_label=False), width=400, height=300, resizable=True, ) def __init__(self): super().__init__() axes = self.figure.add_subplot(111) t = linspace(0, 2 * pi, 200) axes.plot( sin(t) * (1 + 0.5 * cos(11 * t)), cos(t) * (1 + 0.5 * cos(11 * t)), ) Test().configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/docs/source/tutorials/code_snippets/thread_example.py0000644000175100001730000000035400000000000026640 0ustar00runnerdocker00000000000000# thread_example.py from threading import Thread from time import sleep class MyThread(Thread): def run(self): sleep(2) print("MyThread done") my_thread = MyThread() my_thread.start() print("Main thread done") ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/docs/source/tutorials/code_snippets/traits_thread.py0000644000175100001730000000315200000000000026512 0ustar00runnerdocker00000000000000# traits_thread.py from threading import Thread from time import sleep from traits.api import Button, HasTraits, Instance, observe, Str from traitsui.api import View, Item class TextDisplay(HasTraits): string = Str() view = View(Item('string', show_label=False, springy=True, style='custom')) class CaptureThread(Thread): def run(self): self.display.string = 'Camera started\n' + self.display.string n_img = 0 while not self.wants_abort: sleep(0.5) n_img += 1 self.display.string = ( '%d image captured\n' % n_img + self.display.string ) self.display.string = 'Camera stopped\n' + self.display.string class Camera(HasTraits): start_stop_capture = Button() display = Instance(TextDisplay) capture_thread = Instance(CaptureThread) view = View(Item('start_stop_capture', show_label=False)) @observe('start_stop_capture') def _on_start_stop_capture(self, event): if self.capture_thread and self.capture_thread.isAlive(): self.capture_thread.wants_abort = True else: self.capture_thread = CaptureThread() self.capture_thread.wants_abort = False self.capture_thread.display = self.display self.capture_thread.start() class MainWindow(HasTraits): display = Instance(TextDisplay, ()) camera = Instance(Camera) def _camera_default(self): return Camera(display=self.display) view = View('display', 'camera', style="custom", resizable=True) if __name__ == '__main__': MainWindow().configure_traits() ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1684768028.0318072 traitsui-8.0.0/docs/source/tutorials/images/0000755000175100001730000000000000000000000021710 5ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/docs/source/tutorials/images/application1.png0000644000175100001730000135071400000000000025015 0ustar00runnerdocker00000000000000PNG  IHDR~@usBIT|dtEXtCREATORgnome-panel-screenshot7w IDATxw$eO=yvf.afŬȡ"b:c= F#Bs:^p.oy1h&j^_"AVG[7,w~.}o{<w=W #/ C|ωa8-~w<VJܹǺ%y,3tsZ>%ܡ;p>g!&#G]Ce$!NNA{K~Ȃ7:5;w͟|D1wu~&Aݿ_{uoOg\7?}}Ssq>u6|8e=~mXWWGRBM7ދ|l255E&3#A8^tPzeWe<ܑ';╨V:A閫vvLNs{+7 "_oW(ʷtp)OBrW$?5d׼ JT%ś(zP1@"w d;-ً߮Pgn1~O @^C߳ނѳ ]n -{A}342=p!{ނo߇ΧN> O֝YHx 'K6,siB*uV#vtfMݕ,li 7 ,?Aħ>#sN6{4li8񢗳뀝{/r(~wo| ywN~7;mwK5o['!7s~ȅ'o&"I҂c<58糟ؓʛu Mlڭ{=[K'3Fr|-+W[.9x0( o=q{g0{O\˽w{=!;ukXgX}UO׭3WvN?# љ0EA@aBz7v -+FOa,5GV l ': ZUqUc^2o}\bKkn.q,noL%yKYyk~ĉGe.vm| \og#w羄:Lt VmRibnݼ=q ʣzWiz5'>mhv;$~np=$vG6As8,=)_EU9~n) <&y8Iƒҕ 1zAn!! R*xΊAl/r%KUzm[ÇTX`=|_Զ!nAuvw̻#}å \!sMgL"jAa<ϡ~r|<ϙ-N离ۺg:w]}vqwH$]s*5>Tg=cp] 2]2V _ 86`SsPBwA׿bucLMx[N U%T\CqY5M38^ȶzn}#3k;JyߤEhE;cǼKn/8Q3x)ΪێcEs) Goۚ6?o޺x?{̿~u fgv[~6pr,y6HҞ!|scqx9OЊ{2K[.8u Aw[6}ӕA?TJgg]B1,G|CS0Ժ=j;sY|+04W雮`=7_T)`ъyq'ϝFº =Xڳ9!{9^:`._EAB$$`OI$ϻ$Is}z.nݺswݧzn?Ƌ~ӎ_G{ Tg##{7Ls埧vC䯸qtL;s>sѷcb-)zs[ |fn&}y%өOM/#7 g^g\"fy\?SУ6{mws/=[E~]Ҽþ1ۆ:Oyy%Cz'W:ٻnoBy!1w^ LJ޻)p'gsHwNh) MjB2+=pMXH%3_+f/^p Kb4g "oot]X٧$ z4_dS٧EDhꃊ(qܡ :m7"A e,xw̻nX'k!AH4$ (mWQ/;B8 Yg_Hv8y/VB+ϰO8'܄"{chh4~lMA|AHVg$={k4g0{Cb6^I{oo12:Cwvw{~_1 g%6 _Zg[hq@ڱooC=Kdi CYP\ץh:Db'Ȉ!- O\w|XN߭s=! <ɒ]Ykk7 =,.:|~ĐA'N>b moyk/[\;Y7VAAAx]ױ۴GM   \?<AAAqeZ7AAAǟm)   O "AAAx  3(A%~i/Z% O"rʥ{H$ ð} tAAxɟ OAa$w_z{ۯ& “HAI.2N<ݖ~7L\.{^~_s  <,XQ@ @ e iATQ*(sijmM&\ň\tR)%1 Q Q ;2qBNh҉LTGG^GoV̨;"$9DC$%B3>fg7 &0F'0qCR')I) r }nLǤ81$9B5\4C3\$% w#dLPZ`8z#iv*XLB&V!yw"x&XayRy^"% vxfK|vjDHF8["w_\%@\tCWEs">NQ/*/Hd@Y헌Wq'?*߮2=#>ns2953Q9|>w-L\5>xڇ/R7 &-8h'[Od{߿.+aqkW. <c&ÐK/c=vcގK!R>\nufsf $MV՝nXUXU l @[BW61&kӛ+0:Eڌ⵸>k?ŭ˟=ϚgO[^滖宥l{1H6embZ4+~GGmGvd`? CY,`/Pf?IPR lob77f5gpiuK:? ke)P?Ku2&$$QW{rcDu3KM,63Qf}q +Y_X_1@ՇfvH 5H 7H 5Pu%C]P8IҬuKF\iQߘ1AcCVMƪjm+mbGwe;CNJQn稴sTYzPӕxrʹuacتAʄB(}Vr+R`q5'ȨU&IInP2lOQBI:nXA +h(O<!u0_Uqbs%DFʅDZwSیi+la(踘v1:. QL"4%l\ʺ-Yy5o^M5Vqi8n!+e:I4q OP*r%@)S)61&tXh=Fg:VK-=xjmA"E~ >_r[\<,)K9*R))J tMpLƦ49ZCktKjF=C3H3^3^?3^0F6E6M6M/SŢxkq Z=9|q_g~̹oq8#xO[vn ?l g .4[-N:U\z% shZ˸ #H{11r\$˖-e˖\/eVVZ.2|c>هc!ț6o$Ӎs,.ʗ;asv#/7p)˼Moe/{{@IDDV&bJA`񢭤.xImkVxIOm@ jVz՘SLY)Z4x !k*ZJ1xmclZ:n;@3$6MjF#i8pgM^- lC[I0jRT}kj~G25ЖhȩI9B@(,&L8BJEdZ$ Z*9 >Tn dDAd ΀N8%:d$$d3LhS]y2R-aQj}CZ!dDJ!ęQD1$\B>i-5NShb4$M;m1KLFCF#6!7R):H IԚA..)'[: #Ѷ궋qP܀'uHUh 12 dlJ6QB}tDb(lHfD=22L1$L3^RS136L3&fi-^dS)sDDfNF֑ŠbK)Rhy÷1M4C4C4M4GH:l2m)*&H 6ՖZVe#jJdXU {6i;MNo 43~&NDm$)Nh]Jcs!qs?^78'rՏG{%W_mro "ryp_~H|sϝמ]k&r.'E\srYr8CW?n1JC^V?|_u]9s|3֥_H;In=.WSN9SN9AP2'uNؖ**): ciJYk,;,W*rP~R-$i 恭C$A$C_bpp{X^侀([QF#̸؊N2{t#ʵ Q!M$9"-1hL{h]>ك6tdC7"M8lkĨl 2b4%fyG]NҐ$(ÌOL{C6V"Hvp3$ ܄ASMѮ[+  Zےp2h)QS&lJD-J=@(Ɏ^BKj O T W +F*UI {\4<4"i(H::yJyJXQR":%;P"OQ .16TsCAeX/6 HOf8; v2fH$XDN*VcYzVDYgSe)ʫYW^hKF-IIP 6WRbOƐ7c!򆐨 k*i$PR=K:ͬǖ(IFC>, gV"N; 4Z)4BǨǒʤ :fm$5?"t"t&aU-; L}"M}[:Ra&lL[XJXdj1̐t vL2UctT.ccwSQ2̴ũ&EbEԊ#M*R*Vrx RۢmhRTs+Ɏ/-W d xFQ#D1|TFr4a<|_'%9 >bZyMgdj M_ґ,e" EuõVZ1A^7qL| =X7i^\o,!ޘdZ&|-/_fo׏kL7<79I\~|͵w^6o²Kشy {%|(8*I4M;ߺc{q':~CӴ=3s}:s'?s❒]{:w ?7Hۿ> /8on_9s~!1JC^ws6MOq;;ms î~!dz]۳#$'@|$ޑJ8wB&-ӂ^ O<5<%6ER nQsl0 ?{[DJ/:$-lx n=cR/SP&f7%DEbEE!P 4 u[s Aiq"-=4kdQ#E45Fff }t (бhD6Т{/\ (infRYniNΫ0$V@ =Cq=o0@GV7UC7Td-$IK5Q=r!'0i~O5РMK5sNnE5$LN<5ja8R,@]D-T$L^-011#k<NNK'Fq;K693DuL8H n)!4$0"\G& BPZ&qim`$N9`K&H&vݤSqա2D6&I8 jU:<@<97P$59FTpeV'>* mRIM,$&fԡh9:u'F"U!I5 BIhK1Zr I Q]!V7l3j&aJh=/x1~ ~HBGƷk"a5ًCϗi8&'C"59"8Q@5#V%/;:[)hՑ5ԁ*`` 9A7č>^hPr؝#i.%$X1bR}Q7P2%UmrK4zק6Um߽V#|}_jLW@܊v]|_w҉'3>ŗ/aS9JYʓ9mBU4RI6cSn@&4JUf ۓG)e$@M# |";C`wٛ'㴗Zie&{4cRfeY&М͔*"&x+ tJ9&)nMLHrD1BF&=^TF*Q'g̐R% zAccAuA&pѱ&[TXF2bDd8݇(2ϑ&}i R*"Er}iSS'E&b3&Lٳo#v^$]vQ-$)IT2*~FƷRfU0#> >eRRzh)6Z_@Io_m0#p2mK%c82,cK3ۏ  K! t]lLZ7y6y`$:VUwq* NY(E/!ز 44P@`dWyA ICdA7ҹC4c$ihJ~N652:AV!P& DF3E!Ь'`FbAJ-qud#t `I|~ie,c4 :qNc\'*~Da[i[Ô˖ӫ3gYlի9O|䨧>N|G _zVk Í}=|?qǽ؝+yzcppksoҋwיE/$LjYa}(@CKljFb]jJKVpovZ5, bM_|`X"{ٴvQ8BnL{hnN2UT$l;cB :NEەhD)JvrJjc [,jOzNoo,:x` 5eBxpv͢^ )Fΰt O5qU;Ubl_TxkfFMr:YVd [9BO&M"(Ci!aF'R.1\SFF<};ݢ9"eK,mff4 %@~8} dFG(eyJ<55M )ԥ)J & L1 ]']e R5cͭ:d24Y޻XVcG+ѐ'k)ZaR"ɩeRyI$;C*)m\ iQHz$})Cg K2]g(1ȘRzQ#%)2>2:< Ab (UVc`d$WHpg:v6HK'hZ Xjl5B#&i*ю P<8˘TI)04NѤ56=ea;q<%c)|E7I1:tCkӼ[qF8nSU$-ʖf`HhJ@ưY(R7Ei{-= oÖ8Lh$ 8#1E&dH& %B0Yr?݇2% &Yi0J[e<.t wV>ȤyB0;֥yq<(MxN؝v2M 7)\GYYa0 BF{E$eVqXNdHi %k2R)Aawzt #hMӫIձeThX(a!1A Q*L͂}l2|XN.L4)JJòi2:`̥Υ:R@$&K4h41ITY6K*IƂd 9B6J~GouV~{i>ϗ/)qi~N>y(qv{|psi]ag/}('>_p9E:.E|_;`m~1#P Et:al޲OAx,y&~'7td R:mM5e1iS@,3TZZ&HKS aEo>f?euoy92ryU$eà " l/lAC6) &d0 ݬfU;ߜ#c<"YdCn,3Wba#OfNo-/n@/tښp  $s&i)4Ygc^_=}ޜG6p]!d0-9z~ HZ4EY[j h\p43}M<ݚq|;ﯙEsb#h1O)Iܙ$"B<0|K{`J(Qɾu)'|C~[TԺEYTM+Ed*Rc,%bŐu5M'h(S7qbszmcGDM %\Cgs[\ f%_32c֔¡l|b _e~:`b y&y40.]t_9DW#4MYl6"n)BI8 7xȰd%+ X?xO}cGxMs);jaQpD53.x%Cr<ģ8w?SxẨ{1%qQoxF%QC4i!mb.7LX$T;hӫ ~7 7 FK<-hC9CF`L͘8a(!UI9`SW3 01^ 8^Z[:K(5"p).ue&]:АEk4Skm&l0fƠث>F,K.rRKzQR?K#`={̓1//;WXj#3@)P}@2E#?wys!X vBVB89~nhNֈ![=$sm!bØ1,"w=lϢ5[rZT君c%ZJ#C1UmQ-V p(r ??KWnk~ MtS| @;<.q/"s].lxx֧^=Bp3|}?/k~Je/?Go\]]k?s]A"k9<8WӿU~:(w󄯭`nrMӇtF?^sP;{`=O}t:R*2ٱqǓgrM\s'`hL;3Ր{=Ҩc؞b3eo|Fr37(1 NJ$j_]%N#GFOljqS+<3C!h0)pIeifě %=g[`[%#cʜ XgKv8slmQ&Mӕ)R"!bKtNMW+e*ײ?̖CQ)XFlrj9`HuĢkFGZ1p'%h8Z1Vnv"~DZg? ͕8fNYb9ƶے>qsGUFJfvu&شc.*rM6okHߒfD.ɸ{׳#X{ R>8?:GFp>`$`eʁ@z7FCEZl!(G2inAlZ+Bw;R/ }R?@~j+#{DD:uP>['|:2`t-˚Kܢ,l=P42dW7ABrOkz΂BN=wv%szag]A"`j0w97Nbh`Tv:xZG7{lՄb<̂sgK76h:ؠMڢnvT!`A1p_@@A.`\(.+|?0r}S + Fj1kFEпQ)GYg\vz u4 4 ޱz7"?Y8 "]# -\ҡ0 ĀC(dt:EŌ.܉a?ވN4ޯ*~'"şK_k߷o퇹q_w|meWKMK x!f8kTb+p0lbϲgeEF{1S{s',O: ػ4CJsi+ IDAT[@dߞJJ!WJV5cjw/#V\f5z=Nc[3 %S=?kJCĖNsƒ/PPoS cR` cJ#%!\Ցj*6e( !i#GZ$EatfDOOx"'|.}..!KK̡ѩnY+j eCs]k`kЊ] nD 'C9{"6.z#otqxG/x=a*):Ѝ#'g&6[n>~a$NR,tQNg'\}ȇ/ x_ ) Ƣ}xhuP07<_@{%Q3̭)s{Ja =kC`55:+߂^*1H<LкR.*, R-z1b5R-F2;&S C:s -B UjUABKuFYm%qjtS`++ r gP6K_b50i456;yab[5*c ̌mV){5 W WY{t^JxM(t ~ |ұ5gS 6!֡=pv+cs'B=]77| uOٟ||y%vND\ V{I/闠}HK>xe ^3n>, 2#+LM"5BsI>8{~EN ׸9 AIGBĚcȸf6Yc%,C"bvK i0aJVx.;$M҂tԁ0V]t)bO]Q$xJٔac&%V w$*Y<+f^Z fjπ3tnRnM44vC h/#}l<$u2OI&hB[KkWA  >~LeUL5V&{;FNTp]~q^z';8VfVC-:7==n=+OJRZHinh*M뱭"L(!,X܊?#grcѷߦ,g'x /fqF;l]h@vC3 XZu􌌾"Ϧ-hF=,5o/oPJ{+>/lDHD"G޴dfiz}JP%T:hmF\8i(G|.W|# rP:(@!Yl'V! CBCA?g?vo-ň$ZjcQ޸\ H=Nb슉qttxBi:>{!/ b3KfG  1\#i]IЩ gW>Ӟؙ (((x- j:8@wk4 JG2e(W̌5,eooE|~vȫ=N ƎRkOIrMc&nCtuvA.PޖJKvtL$v+&b' e1URqR܉;w5h>>}i@lډ @az nX =֌dtFt$ޖZaY7~ CAj촡?Q?i]BYiT4[,FقByHt2Um@|Q)hUq013+X0dlcM$6!ݭOICj 1[{֚0rWh>8^E'K =6!Mn$[⽛#݌DxNHG1۲OQtA31ifPtT8ruP6!iS4%Uڎȶ!mlb1Q1n/{s^z9frB -`?q}WZ(K͌LʧPJ ԉ4:נ{:i 싚z^dޒHy}vgxsvL2wOk]2F{kf3+h{Q$a}VT P9P@A@]SLJeS76Mj-umS>n#U4`YGN9brCHD3眣K^pKܢDTJy+/γBE ӕA(kmFqjQ(NmS Zĕv3."4Pt",:E! yXc1"dAHdAHԩ${-IdP\}.b62 ƘW-1#VG$:I_0s<=K_7^<}unw\tr0{5ɆxpD Y_X]W;<QJZ'ۀts&`q_+jyʰI!qңL<y7yK[O^TLkyñQ~`QENl6ww% !v3J~EvC#YHKG/Ƌ~܇p;7~J}G|˜E&۽!Au$P] gmđ9ݾm]:X@HOG&lu(w[`/٧ea=kϲwМ,z3~0ih0v+}'>e5=QK#Ό1p ?1FrdA KGCFn2A''!v]X)"wYXMJ9\18Yt@tUw\>iY:#m U]7iLENG+V}gNS޹wG:e_Ͼl) N]{[?_8zcK+zъ޽5zMXkN7VyU2O+ZPL kP)ݍU 4ڑASZTҡj,@.5^qxG>?S||ONɫځA34h&+Mxqc]=+&-s?OF)a-Q#dh=R@#KEW$mDx,)l/SID~*IDȥ-r$.o:3ȟAmA4QNX{:fL EK)DR@1 Mq&.4-!7]n+q;}7N  \t[??S~WZf5%ЋF %K$|J~~{>ƌ !̀U:D-{Oy9gFcsVdېj/]s݁GWts>2?A;@ H6sm'+[QMMIZ#+ w(ԞRZs[Z<ȗ>e@qм./ y8!(FML%2ho,Efm! QBtNi|>7KqR.I{JrխKHҪ y[ ?܌p;/&/_x\}g$V2-:tT( -IivX4N 8[ W^KTwW& 3pOk%DZV%dζ葶aB#; hBv,{WBQ+-EU 4`A:ܙ-ŮѱtiHLC%ҶfQ>Blu>n" #lq0iT#R[)oSʑC1v0îqii/tRçpmv?;WOE̴2F0T#\ [Њ -!0>SAtUnm#"|;uKaJ;ʤtD*Ğ1+^Gөu jߤƠ%8>l=/utF~n\n||,:,*,A c-꘰' =IhJB%ymS9X^MԵMXzpSx.A^dC4iNHZq^ad5٢O*fiq3*]%ΧN|>rb|V8= ê1*j٭VJ 0%bj ^c U> (a)3ej^:AQydO5W6ܦa8s𪹏JڠitZMdӳRNl8"MdGhH$"xRV߻YV%nWQPP.͘J٘apQ$h6ר0^А7=R$5" NWMcr~u^Moʿ+q6yg>dy>)R@B BQ΀H\h01G5Kj:(,q(76OC@H!F wT;] l[f;kgv0nM?6!9)9:'^ċA큠uFy"blxANGF@jy6ڨC h 5s<3dzr\@6:K$l;=d!Y"sDl7b{5pz:ΉNٺTn[dǵأ3L_p_5.h }^uu@jImiCET fc ֎Y-TPYdVQLS58v{/'=xP:uSS~A/D qy?\'Xެ?%d9\bu-pX֐; Cq]2l6,W<͕M ohUmxnAP Zshp)|FQ2 ݅?Jq`XCU§X4(IG4i'px򈫗MLC 0孃/Hmc[6=ʥG5 jTJL{px&Ю KʠѥD{Oy/8[fu^/Oh"}֠ ,f`6yM[tbɚMS!,5<.СTCpd|szkmƀL%g l!q0dX=VҶ { `Y0i%bR[( RNhQhpp)pTtI$Ȝs<{g;N=uYaE!aτyԣz#Y,٭k^XX<~c͌<`lt8'9Qs GG=}-vzW `BA„ h!Ѐ#85H%LOJuMzݠ\I4AYQuTl4Mt#7!eMn7hAG=Po^xlؼckGkֈXtE˫)>;=KslhCEUy =seaӨ>[*0CDtd+>Yfq^d:&Eߴ2a/H؟%콕N3pꌡ1m'sܮvא+S rAwYrjX׈~/%x/nJtBx£e.8p|*Ubp[/|oO^>#WƠ- pǒ;Vd/_ rH^[s&Μ2紺GC*FVWb\9DJ2it/t9w/y ,;?I:뇴 az\ KN ŊmpN|cq搋7\9dRХ IDAT38^28Z1<^+u@z\oIS>^2v5=7,/^0fPO=hf-ŴEs[ jlvϠCɧĮזݜK|ވcZKHӶDbK$blUQ)D0CH&wfs>4^9{338QG(S*͡=V#VƬpܒ["_.W.WC.E6LGׄ9h+H-kz,"r<-n,1nNi;Y~Ƈ'DN]yOw"{k;ҨRJ(:'1IL$&:)rte">]mցُ;np;VehmhK1YdS]FZ3BՇ@KS, /bU6ȪAU-bxsb<Nj$q\h]I0+:!̎)ZuQrD$̉1jHޠLG2"7tjQP5J"r.c ä|*ͧ}0@SkjYJQzc Y# RZ)J'/=SYxCٕ>Z~;0!Zۡ f[cN ZXFutZ&8\`xVhD C(LC-z&V ΡޚԥF\wIk+ZKP'}٬=t]GuV;)A`P0JfT!¢tpָBC) $Ҥi 4v2l z^A][vxvkui6IVPiOQP*0צ!bh-]gnV:Uy٠_J+I8mNSRh"BbB"VWw])LGb$D7%~N ƛ*tN]Fe/Ieyggcf\H͖lY [6l o^yKԄDQ)2+r9G/n$IPe"" }H,4zKޞ0e2PVF bA_=gli)i.,Anܪ*T!JsDZR|BT|d͈j w"ZdOIuq%511mĮXy‘y,[.[Έil"de[G u*Ѭ\WU!UHC* A (G\g>yB ͨB%[%PӑTL. hq@l9 AvU{1cQ3q ޒzkEZSLIsŰiS ECS&D/rF!ݒ˛2)1J-FMDIvR~HEX^ڧhDd6Du8VHV)\0`c m_؃٩ ]tMeK & E:DvI:^`ѥA|epuvMff,%H (bde4[cJQќRw)OaxUq0Aȥ UNI/b-uXё; C.nKt Z)V-8weT6.͐p,|N j@n@  9$f YaDXGØa 3en_?~׏Kv'lW*cf>E)$V2E`Z\&]Ѥm=Nmy^UNX44%,鷆wըj\| ^#_0Ij V虎;2AG" F=>ϰd)%F+żrQjtCxsțo<9Ǔ{D)30Y(:qjJ2w^KyKiKi6i"c]bd XdØ2&/K  Te#zp`<~#)(jg+}o\aĺq4}-[ЈfԽ9wC0Yi2 -]f6W]QD(E,Zȁ50.Ӹ|PP=d';%I4-nźj]F!ՙro5?`z-A&h]$h1͈6.^\;IV 1_msl:f-Tn\uLIزG[lSeAyaLP J5AԼ~jЊ~H,/uSN}d ,u&z4R;<UN;>b uF|S υ_DB$YJ O!O<g>eҵLv&(pw f WzH}7([0l0^S JI` B6^j;xulO%W(w.yh=!stqǷz`#)nv 3%)TbAJ4FqZZZP1T%YZ"c)" R FŴD>H8H6&!%jAPn(upm}H kI%ueJEY"+KX!Z+7ԉ^ƈU-0%@A8 _@r&H ޙBlBgoD .0̐?rW?C6 7p XDn[~G7.) yH]P;v1}cLAT5SR}zIͨiR-(wVH DB UNk~q8}ܡ.IN4G:LZd " uS=@?*@3*lpJϬ͉*9wOrpg%G"5F|4 v9U?8lDS `" RKFHi!i4tg?vj|+)pֹ*R!)pѓ5bQb;&Y XAqR 9W %?!Br2" .d`U0 (U| ե4JSTRIeDV ZP_d? 1"qNB \lTѲBt bT3jdk2=+8mqbIJNKqFLQȀ@lb%&@<^/  ]-PҊ3'xͧ(d"EvXiW9xmN9YFQfyDKLVm&MřF2ZQqf8bI^:%eeE)[5Xo۬%XhB!R`!%),}̲O P.iUl!VbeW5, B1݅!Ta6=.gB TaS!`I*Y! DVLI l-]/RiDgm RYK6c6`^oEdvBQYҾK.XCAR_!, e {lMWY&倦9~[cXf=pۜ *"%.4bԘM IP| ˧-pXbk¦-(R#C #j*D9RgyēvTv)T-uI2/ЍHYJÛO3Β : R>acD!1GY*p$(Ռhp2鱚hV5N6J캨 b˹B@KꨒOi:s9Nc"Li1-&w3$3Glzcgmz\FkD]jq䍓YJEYPUT5w.#R{>G$TXbaEñrYE-RU'(Z{=vHW̏tL9Ji()ϑ ]>=Dm$M^q+:$9*Jbry|!1wp5QV88Ցf:CfvFHr߰2B! VY?HSH)p\P s*$V"tik,aK_{)MmLWwѯp~yw}/gcϤEe8bmr{ARrsBIc!* pI3 0Ya# k')R#0EDC i!9"]JįeN?Vf+њRf9 54@c+6_\*Nj>Y*3MFl[)R.Lf,eOpZ+֘5dD,{iԗ/ٮZ1&T e1eNg$z \pyt]VQhU $9P\YcO Qp.tq=ɪgn٬M}:# "\gU9y*j)BuB,B*e]P;1Ō`ՙԭ)-mDm Z#AM 1j9f9i_SoNqs_:Ul!1]4yO4e i!V`E>hdC6/\F)6+&tH7Hu\ʈ"̉ H*9q %ɹ'뛵`<^ƚfcNǘА), j,ʄKa1[6 =dDI[vD^;~3bt=9Քɏzg*֨q4lGlsA*Z=pwJ[|#W 91WBܷM{d)?/?tu qh ԝ 㒷% ~]RwCTu+j l~|){ã,Ѿlo<2 ;*U6l ϭ3}o8[L~"|YBŒ)o|>>={E$R6_N)oWy>,aU{5/s?qAD#%i諘Fv6_?g׎w/kǯIנ,o 8$5yåg}BPKf8_XPzE |ͷ[g? |/W_<< IDAT /lK,4d%$1|1USێ)b~6v 7sLjk* [\\;~.υ_k>28q|u]׷9ȶ]&u*'vo yX}Χf#>ͷd)'bϱT* bL#쉏9 '>Fi\`An221삤w爽9֙2ی6k?_f" vyfNc'mf:Ke A2!v|$Zi^ٻE{犾3gW5YeVR-5A/OwXD5*9ZkFӲF41xurקw///1FFJ5Pjo" 9&&O{\}y~Iΐ IIAddQ ̧0 K>Q0 RfAa)yz^[o~9Z/(Yi*m&Ʉ&d6X,UVʆn)9wH `i$B0By!Jś ś*[ TdX \"+S ojM-Br+BݏQnśQlS'Hw% H~H~N 2E"7uJZF7Up5Հz@w5ymqS"^Dŋ"E\c՘5Jf@9n(=DZq!Hr$W (4p/ˬʸe%y.QH\K&u$( mʁ!+ #`b7X &vYlz\|\zR'fP*<Ջ;|>|`*7q릃t(|:++:+d5f4i26Mdci>cGzw8pЎ.FtsjͥZs֨j_q*zȲ[{st|o{?o7߾n;~~6S7 :fiDI-^$l1A"UuX$|۬2f\A5ۧj(F 2cJQ@=sxI I,0D^RhnhK&v UB 6 42HtjS{V+Tj *wTwgHQBFANA$UgmN6+ }fQЉC 5̑I2Y1֘Ԙ Lç_9g}s8a\|( XÍeX[chP V{s6r҃͛yp:x/;=YeYN2\鐥1]M@ۄ -eu={6[ .o85јc.a-fB "\$*+lL $]u)-kjUerAQ<O/1D`CG&@Gw $LdT&k@PnM.3LI*Ah SG&P""ĐLgޮF&;Rj)HuF1B ,˔UfiIɨ2"U#4"E#Bbu悲ƱWb&d {3(6ySLC0'tt²NXё%B QXdYNÚڟ1n] ^qK2Ҕ&S1lwy^Y\hR# X>yE"m)$- I9 jRX> 3S03qt| 0&#bL>VcƘcaWwqWɮdԡZ]P3~wB]b[빏Q@ j=rekb@O\eH&IB% P+Uj`6ٲF6h]J=Rg_9bʌ(3l8xv_Giu )/в3 )]sLwTʱw\[\\[D:e@?Gr[(p%+׋vuhe4/eK}g %SRcR_yKdskn/~k,d@%~Gt {>eGϚ<%O {PBJJ"Z3AB Y  8. NlB#d]Һb6F'Fr/ "ftQP 7o+/IœڦD `J).3oYR.\ q[}^C!uye$\$ D6'&`*5\J) gNѪE?DB0"|\wAȘ-SAbA`d)ր4B.)LşYͫ% ױӄ !oD_I1sy+DFHJRFŇ |S|(fD?~ ">ҜꊕlfdߺdG?gqvrN&ɸ+ۜ)}VRl3.EOXDtusJ9/X(rReA\TWl9$OiƌW~H 5sU'*Ve8~9|av"KrU՚ZgA6vkN-+2_ _  _ ,s m&I e"Ey"jS7~5grǘxc.˫*ˁw0HiV됾vAsIs&bT)F#&dk9,ᘻ.>w?:Wǩ ,G*aNwܕ?bzLषYFܵ&%l@U$&qD9ȬaiW4._DD/t&-J6a9k}"q*F삧j,Ӽdd2l3>0:1(#yTEutAa)X)0{wo|Ws 7_Vc/lȷTVu* z שVЛ)vӣ^R*̽:aа[1[oΨ) [B2ADPaZ{M%m+bE) -欹c"Eg--.Ti2ULB+0SQ͜z$ebLHD.%@7HT\HB HBH(1Z R1"j0BBW"Q:Q *LǣP`ѽ̣ P+pJ[QIa-$G+/ps 7Sq]H͈V2@C$J3 .ʦd p"K5 [Jp?/Ƚ|l C2tEd=uOC)"HMrť\rH.EbDg78KRd9%d MKMm'eTjn6g6nbVE?1l rKS052@  ٔCR#@((l=t9\T-'tY!Rd9M3LiIcl!@6uY2#3Zp @28!U!Ǥ30HMev2`(wYJUJJ u"Y'S$Ha회*uuV&5R]%44)FCÈVHO`dv$qQ C[)J?FDrLOsK%µI83 \PlHmdDZHL+ıF~R"2dQ 3zc.#I8f,O9}whP1P,Ue\ @9X8aI"EІ@*#ccDH"12`-hf4˽sw߳9oO8w4b 4~~ut?_?}ok Q<`5v :βh8m32n/P0 ,-t E1){zadԌPI>23x5"F43<AdbYfCYsbBY\6Y1MbvD{0gL4 9+vcɬ[$SuL| kUF]-r2tEt&F[a٤&{K&~o<<Wr}?knEdwm`1G={42L]1ȅ0=1=2=$T/ 1s^I+q*-%jA)Et!mdLыbA71ZnҘ N`4ٟ] ;2uV'juƒey&YSjV+!ä2ɻLf]f݈!N3!"M2 Քn>WLS CA5I5ʢZV%ZdwfVtc2~]HXT>FĎWԃG)5!; G3W%~ A b:fN{?.R*NP qP6Hi`0OM o_M#9^RZYL  >-gA;י#6wy Mۧ7G 5X1Zr6 [KbYLg_#EmfAکw }'h0ɄIlD{F[ML'\XP: #7 j4-]$՗4li(>eu`:T-ҳZ&^R+¹x]=eO^PP%͖O i! f7H019(M2\d|8 v WLRj78h6&rSl9J'䑙cMP4NsMLduuN69 CN<;}gf0vͻtn/6h -UR:ncس#FuiBO>qB=Qzf/Mb^ ~jcK$ CZDcӚ]ӹs8Oyf~AbvuWSt] ԚDH,´K$ Yc!UUEA&iB^%ΚihJNSp6 ZY%ZSg)=eJ/rs|O?e>1_ 2,k&ǫm>1~`65Hc*1+jŔΧ_¸^`}Uڄb"s%ssһk.u(aXK$В8퐳ry1`ue&QGe$QǠQ%Qp,z^0ThdN>seJ@Bh8B *:܆[<@ԢpfֈkQ!Or'_M9UML]&An& 9J4CD3 !4 ,WCeJޓJiq\EAdj %\=Ia58YeiePT˪.3*ǐ9`A\K{ZJlmSi%cʜpҕx-^rws\ޣ%kRZޣǜ}4٧HVzSe<ٟeO {o{`{&y}<|%~BhMT% A* ٨d8O`#cPSQK-&En:b9Sf( =(G%r+:1Z+0')CG +ܾCACDPV 0f) :!J rrX&s¯<\'J(GTH$լ<*nd;p圦"mNjaBSТt\1 …B.+Fr2Wg4%V@Ztf 3H!$yMJb:EV3p=43hvW439Eed"М 9v0 JI^)݄moĶ;_#^z@+H1A$2vi62qF3^&TuAeBiɒqc{ .a0O0Y bi",J6|K3ZOglS6S6)c\3j2yUpXb`)JYQ*!ĴR NgB1O* \je$9ZZ՚ ^nc <WsLuM:4TP IdI8M$kY褮9qŜ5"U%*5S*yݥhVB%td$(reRP`T4eL1fbd9zc3%5,ff#cq'ĉ@)*D!MVKV,C`U#Zľ g\ t@on(X.+j'M<&bV#,-Zϣ&UDd*j J@Nht'=_7GtĜ6Ӝt*N:4z(uDVSjf#tM\!ZF.H)Ðr΅>q&lM?S#i8(Q(.@TD X[E2!tiR'(Bi((6tJ<3 VbjۈPҒ,\ ,h!Jx!+ f-| "2I8/I-DuWr_ u& IDATZ^)/6>oy[?8>>h|aXd_<1OyERE%ϷKJηZL~YOho.hoioΩu{%+ubu"]pr8ТZ$"09 4N4b7o"{&dt˅VyiK nlr dcC]OU R{QWH ġBa&Y:aUc5_\`t u' ^=9C\gV=2g,]'04*#T2!W" 7367cJy !4P5F=4U0.B -a^@_Nx0&iJη^o2^g36tFb2V>2y6j@VJO9=Ǹ5xzUJ:\u:]'U#̎QrZeA&i}*E݉hZ` B21-F4:`ud7165Frln'~\Y?)TӭO>e>{rϚ1N3'+]0W[X$РP4b– &}Ⱐ), aPX'~MGlIL/]2m2a Me,/'?z"  ux뿃׿;a@ܹs>9 ?g`p;S<'''{7w>;|wm@~$I뿾}w̻މiyϽ7 ?gs_M?/ʯPozӛxHӔo[~^?F~Gt_Wô_t3?#?&n޼?=oxQ~'<#c<^UKELP 6egx }JuY.?Xi !(XX$, `} -PPV[Ǟ=< Ndm D(*1p𴵛gQ¢( ђQ@* a _69v9Hv8w9@։Pi=i,##U ; QҊ<1+exL^( i#)%eaeQS={DsOvNy$epmZ}Ľe^_.z,Y 9[sS,;*C2fV&IH8yG" HSF<,0\M\L=l@O \^jpf3L6eS p$ 30q0vǩ>5 30G*BR"K@%S ܔ$::ѱz7ZipUWfObU(@ ]toHvّ s:1b+:bP R@",,ЊF\"f-WŊJ׉btYP-9 2΃pƈ J-FeUfj0?3/"Y󖷾qߏ}c!ڿɲ?GEJOprrӟz)%O-v(RJ=7孼mo}q\'Z[ěx<ƍgAJW/_j { |F#~'!oxyQ??w~o~%JηZⷾqͳO ɲkoQ{5HjтZ\6. V%U .˹$U`fAZ[5*Q A]HAj dLeI-}$tHvl"4+G"K )p &}csGrݔ8>&Gp4-GKj> +UpE]ZFzJⒾQha9rPGXTUph^4]hĂpnqlbA&ia&15-/:2l|r֢ u5bzgJM.n1Hu{b,4I0HlbT4VgJ(_8iI+_-*YYW˲EؤMbPeR68t;Ŷc43':ibqV'3(RIҐXn`+`'kSfRyΌz$f>:K|uWD*?v1\0hNh4TBi &4kDZ]ǧLYj邇OQKW1+[~]L1F;!MMOkh-b(b&7lm2+ZVyA(ETZV2ԢDLSp@5vF=g}X[9|E5pMFN荜fX ҞcY) ̇s4*ԁL2gO%c@S&^&Y7ZaD1 ipLe,%{f5P\0I]6Ii#4*0RȞTBIT):gڐ%k#ޑ9$tP**Y\~,C)#9uuUb$ޘhyC@j&km+*A$hgZKNɘ>qc}c}> j/I|ɢǘFiXF´lqnqn2I%d8Ԙk$v$m[#XdDPPb. J"7^07( _)=?yX~ܸqn;fe?vٶm7syOGqD@{XG7tD`X ZЂ`rYG)0ݻ<`MZ*L0Q`R*({*Ju\RdT1Dr2s(3&A:`zESKizs KKKs46I|ȯ3(ST{cErN}I\9ouE?nOZ-V+Ud:\nlFlwGl_?GW%MN'$,m`=ag&nf,:L6;M ^A&h V\Ϟ(k,KorƔ UWPv RLTA sP;d~ʅ!." v19TH&QeUHU拉 %b :MnyIŹҹ8gvL hl/cv9oqt6yذQ7J uj9x R7a)rDveKWKA||P b,4ɛ*ƥ7[ňMY`p087 h+fTĶCdH, \Vչ JهrX0"]꜎5G{żfiv-]!MA+TBA5$ͭ%McJs8.=0a\˙ڌ{]&.K\Iwt C^dļ #t{!˿xta??ď!7]oG3'Iv^S}~|7}_>шa͍E톃>/\wvv%???o ?KRgv,?o?sm(5*+9~cE頷Jܕ5V584<޳]. p%l>m,~r.10!`Rݓ @K:`H$B9c9krʏŐe1$ 3)Ȍ4wȂIqܣllkf-b P k jG4ٙ&4GhwPT_3|j {쮰>KvSy0~*' x=Ao# ^WN$9Vqǜ`1 P󌲀87H2iaq@yS O*Xb뷸xau,ϨTAJ)RaRW#&GOPX?FIiHkNWiEZi :%,Z,=r(xTO T;hK@@P:ibm:iӍZ#X]~kBc2=F FC3\y\mp|.\|퓘Vſ>`rgႝǔ_&is&QjvHM 9O/aLr-n޾j!eTa: Ҿ0)n#-“ZDpغvJ9q65)l[!ueV(>b&\pD IyZJp V;GR$ rQ5HC5⭂PP+ѠhiM좆.r#qun߱0>sтNnZLa*v!IG _aMt#I8rxKoM*[zH V=HS 5JQQ͊Φj+~IVwN3umWy^3aRH^[ U X";c4eel3Ih5hhtŧ.Hn֘>G<#HIDuod\ r#g$!"Ȁj6:هiq(EgKqi/'4 W[K0rxAb bQ/o;ok/.?Ļ^ /7ou Ð >/wü-o~ɶ}\twz/j/>`ݻ\rO;_[G/Y48ױy%i׽/?m2sK|*&~:@HDfx!zGʨP ,wSVu Yj֢ԔS1oU7PZ]Նj ]m$XcSK P8*Q :UTUa "̭ s 3]nHxvВyFU4i(ʌަ$v/ .ω뜊MBQ' NTNZ̒.JIOrζ9˶Y]Dd?f q31#ӂ3sȁ͡řIД:$[ e*99tHwQQS"1Ș-L&%d-eGn IDAT T.2*DetML]uFWUggW8Rw}0!1Bz=w%Y_ uq@sY8MT_Q M"Iڔ3vl$X |'XFC44:R_#t{OX8ɷXdrec]nt|.b1`1cLse&9%W[(u^.JG%lWN50eMc-I_vަ:! )cQ(H$r8Ijf REJ2 $HI|BR:{OwO{=Rujʇ{}za}픭ǨDl^8A_HIdc4xuj}d1&p4m(8^縿`XĐV&馸DJnNň^n V[UyĺpB*I#9#aa<"Uŗu|X%I|(fV^.0DJfI Q(ʫRe)A :iþ:M0fFĆamXx5Ήt# F {:sL).)# P\,!Xb:!f1R!9"yBn%F4b FR"Cb;R7$Lj$DcŲNP2 B3Che|f;Ur%rdt0@]Cr 1¹8Hk!r>)hQpmrhoz5M"|CxXcg~}}/j~|soZJ%vwwrAn[L ߢ ߹ty;Xߴ>O}Ư9O~O}g}z~O糿Oql_Ї>'?i>_'Wmoݺɯ7?0锏ۗ,_dkk of~P=Gh'WT]3=LQ$QXDE?_͕X͙@3egmK.,DV6Z!YsB-qZX[Xc8cʍ1 j,6d +*`_̰w"G8X(#*悚4.4GJ9<&s5DCjWT!:~CTp TlLL?1,,%ΦOJ]"}y\,9J01KLSlF8֜5ıDF?lџHzF!3}4b9DhdA"*x"2ˤNWINK {[yN?jq7q6|͋DMCoCo6'r1F.FQgT eiBEaasU2BW%~ΠYc`X&.kۧ ]Zq[!n)k_ :Ո5cDCmWv#E$)AE! !΄| Xdc!\%*)r 5a ؂'jFi(RDend˅M`ì[d82jh<_u]:?#?o|Z~#OG>?i?gmm }~/|g}CM+i~~'~?ӟԷggߋz|L>˿ħs_|c'? >s?OտX?/M>яGߵO4G\t~뻮[U[of~A'A 3A*9MsΖy:$".2[o73(y3v=m}g_a(U1wWtr#n/Plθ+=] 2)G:iGlf :}@3bL  JQºQyג{XҐ4MyH-S g1K%$;RF*Ce^3 nLn=5TΥ+ 1R#[-^J '̥>,wW+.z*Ɵ[[loqWQ:֫`Ù\oZ6mJ9[6ۇt:]֚3ȑ\-`#c `ZD!@7GW$'0#uR8& #A)sJƂ9G! b9$po`dwpid{ qCA6ƈS:f@u+FUT$SP@v\qsװsI^X+ ,* f0]@.2 P\ H] 1T-P"AjC1#NӘy()sv޹Ϻ5a>=\,u[|}n=_O[Vժc6_! %˫JbT`~jwK,tF@Q IMJkY  R]JzHHUPU"YcbTpuЉePOV]L`]Nf"ıcNkL0rCba  XI2'L-&huS wX`/3>2\ϛ6Y_6AduU$}qn "#Ȕ$aB,I$4P%MVe:K$UDg[ PdHaE*(1S*hVkg 8roP](Bf3ZYXg*ӴY܁PMlmΌRIXliF!Uhz4s?7Y~d2`0e1X)b wi.-K,1ESZb>"9~fd*Q&R; K[ulfJ4:>e&,XFc!3<%J$dK yTԴ!NyUs%q2:0;wVV,O\!虏i{IL,)2K%&S-~`1W L2CFlrUħ X׈J _נa5q@J/o1 &K3c״QˆZ'5)Hnz$L,X袏.芏J&L"CѴEJ!j;"O IL/H$OQ%1 -Cps`@!0Jɇ R( +EjJuCQ|Ü@"qywd2E y(p(iWԐ3́əĪ2vѥ WsUVQKZbSlJ0`vFNK".3¥F< 2 Ì$q*DdDr]X`;.vdNXXO&q9҂a+*2D$U89 },?gߴpu7} ͍C#"Ga0t\E̎LMZ, l"] h^=C#T_Z$/K-JZSE&G9hrho4XT`*f>EGaRTR< YōX XB&h%FAQ\a0JxMdjyhqr\' @i1GZ%т\I` i, j4t14JY&3Z9Cfq9B6qZ9O 7-jKw&e1U¢.G/ENSX&79^ě؟쐺iIyG9f ^b5d::DZT 5\']FT1UyD:TW:ֈ JQ5cmppatPUQAetX#U-a+#*#ҢG̢2 W![gI_%.$CYslهl; iQhYaXLuܙF0M`:*mU:e+<|1Fb1bLe&qh1h1a;oqk8/N;cqPDIc f;f'$Y稶ѓTgjGTgL*hEP7T;Ԍ! 3{f2ܭrö(nX-"-3҅LmP]? dv9Vb+Gs,GJs4& j.h*`9.uG+TYU"I'>fSr, &u؍`"7"R<MH;8E6MMM969r78o$#pC=gslo1uJ CȮ`UiDES5Fxpul\)}6+]JsJ[39JMTg ζ̻ S{slL^U9B-GM t.PF:iR3DSh7+q֘WL7+6^6R`7C~wq=;^}7k>c|oTi+49OjL&j,'@иv!.1RVRδ}J)F^O%}"C7 .;+D UZ\h A#R/#DD%K!.f6<,*!]H@nIDFF$x<12"@BOv`JI+ s!DP!]Jđc/(:Sjd/!H%BD0Ѷ8ƒbˣteI>Mi}90\U5_gM@jFTQ] 1]#7+^Kus\12 WLďM ~$21r bB̈́ׯq _߱)ssLbER Y?{=c< b77} ?qy&"Xg6-L DJJHILQ+!)8e96XLjcUPk SHmHR2r!ɑ 1Ԑ2nY+c Bș+bX.ҌJcNS6. Bk/K" FqIo I )9*Q:q::Y7R  Bp3{-]o_4ЋKriLL֕NerWF b 'X`.fࡧ>!j*I1O @YFzHXnآPXl"M¥E*((]Z/|EBB%)M ˸~߷D=_+K4kI5mMތ ))}%{ra1VA%(s!rD)2'_Kye!f!DJr=H2^&R#Di:=?#9P{!z\k $E6󸄗ؐ ,&4s6c.(!R!'9j :<9d dq*[#&2Cdȃ b@ɨsvR%E  #fQWf6-3$SX%YEI$Qд9r^$*aCESΐĔr9y. bO! E#4"'&r1!ܥI&# ᒙ"#X z1ªX5?ט6" :^C0I`@V"d-Acd=FteDWBXJDIY$ʒ6{"ޢ2Nť% .vŠe$ 9$ IDAT [#~~//Y111mo߲o*\d968mrtxXtt1n-: &IuuvF=l/ DHm#i źͼKt|AOUL XF٪>T^Y,-qBLHCn'1u5 %LhG5FgU'5CXb.ֶy)x2sWٞSߘ``0iH2FyQK*!!z$2e"XHm`I< ɂrDM_e("ӕ '<~_|k7} X|EpǷ7L:{UNۿ~~uS7j+\A@D@@@iw\puKs^iЫ9/787댨2BF/sa?ρ|eXW[E:g2!ݩ@0QR6rƮw`z t0z)fǺrĖz|̭"JmMDj%]1Sb̨L!(YWOw؋vS~dYBU Ƅ٧a1]@Ã[}j"H؂O0]<)f1WN55‘A@'mz:j:`7W\4ΈERk*˹IIEwoQ6m܆C H\OItPY:=acul_t)0g2>c財QGԳdHhjEj-{#.{Mi*"Q]%T5 ? O3RHnmAR@VOJAA1@iss.mN0GC0D"I^QUmJ M 2 MW¯d|HVNҀdrv6]>ak琭Gl9G1!)1,1K*qY"&TֆHvLcܗr_\l:8`h>Q1u+L uyȓ+ErE]g%$Tl'OؐOhd}D:27prZ^Q6OiEc$\lQdO, ,ΊI^RK\RVg5UuS (P}4GKѾ7Hj"z I2Q AlZ`>-0+p')(};je7HcaRcT(o23( rt C]ҙG:HƢX-+ H0RyrI疇蝙^$.?bR;61;U 3$FDa0iprl]g[g̃"ni<4z~Ec;QMp.Sl'׼wq'ztPfAdhFLI]Дԗ} 9qCx3;+r>nqئL2nn6aeFnNעwF~.QMq-G_Nmv{Wӓq?yaADgȣ{s([*wU&_֙]&Kf>|W`dEp.6ُ/^+3+u#2UM7}'afizzL;?N0c1V$$QHe|?|\>.7LIAw'τ? eޟRb:Go/vۿ͟,< `3>1گ2ޯE6ns7_^cwU΋mvy~y'kkי0a8nNvzReV*ejQ"[D5fu!CftF^#]̎fG #zy̵'x!O\?), |Y8,&5 :٦H)mkӽ22ӽ22vsm_o7-ܾɝ;7us7ϩ_?guEqh~y~nQn)GT#J1Mq@C4!5s@RT!üƃFJ%2S 35_3b"\WVIZ-iqLUZ^qX|"I3y᳓"3Zg<^橭i{U& YѴJߤޠd8wG(Ɯ1O)SJR0O)S,C3t3@3qÛtE*Sb'S`lL bTHV,&qHQ<gޛI}<+kzf\ iȔ#W< Yd(dɑ%O,)B=`1{n(KEp1cԕ?+6n4Z7tD̚!zY3dRq<9yu;k,|l.x>.^>OSG|g}`2uN1w=y7y>$lС8p^f/3o2ʑC>v)]=xyam'-?ɟ?9cwY]FRv:gf\Y.{,W}֐=df [C U*p%% H'[w_xѷ }~ O ~5y埿?{Wg[hny鱌,MxO6hk>~K~|vn|z>=1_M>`||+W4ISZ6.~1/}f*~b ._R }!0# #0%|ޱNm&ϔjTN;lMfQ--k^5ANN #T!FJ`{=x<-'Mnp+ELXn fU}VJT*e/KxU? of_ 1z.Ο???zxݿuo/1~}=s4n%Og~O?R]W1sLmT_q[h珙Zt!.]6^׋ z: ]&rc S𶆷Yq~sH^\ގqTw4TwE^J f/f4A@PG(32rK)+']&1lL 28-H16ا8w)SVP&^"p#XCC)tBI4[n٨KXm7hy.63Ew]a<kW_3l XA^qsʍ˥~ϥ'k ''yyY7c޼gcn#ѩ9oq1 TuUGvP ICTE*i6+<2{I[dcFZtY]V>myi+hK ĻZ*JAb&vg<9_P5NC^:'cb j ^Bb`/`D ~}*ޣ,"VbHGJ- +mu@P%U8NFY^3O\/:;${PtT݁lM; SCX.ڡBٴTejkB]͆QǨ4u!8SAKDDGzԵIԷ /CRP.R6 YhꂪB9M=2i?ؑN~ӄt~Xs=i/_G OuH9X"%ZnOXhCYS|f,W 䆞\X`dpmt.o<&{hEEĎ*dZߝ3ؙ1PsrF%lJPRsx]>Fۃ4q[HݢvW 2U|\<-~x) {z#iț#Թ@VPu-ǭct6жK ЭwƏ毭1}%wuy#{oWP ڗŒ^=C5S2j\#jeo5~)0t z$88``g!3Vt 0`m#E m Rc7Ǽ|o1/#^skňռ|u2vPLUqJ@*53>? Aڮ1oEO!k>T3KhB;Y:O[4:=AePj[i(DPȅqO y|ÝE[I7wQ+t{>(wh] n-o@ f(gtc5#$J YkA]Y@{$.]+5IHwoYl_)!o?[G %?!r!˷W3˫7M{ѣ>F;; ڇvjTKH-FCrиzgTs>~@28Mj8k`Q@64!!4=cdFc~j3d~RfU]GT:B:Yi6p-FHQBYdӆF >̢!>ހȕKE{ԒL] rD҆4h"_j Ri*{Mr.n Gΰ`-Ѡ  t'SLjZM-J\X5eH cMI ƃM Mq7Hvu gP, \DZ³%'ޯѽMkՆ]uHMؕLF I)g̺;G5Ak*ӤZTKbPMG6u*@wx;]{>W\-!mEhTrWnքn(Y%6M*zmp7%n3Rw<^, >.f6ԖIiѠH#LC^ݴ/=ν9[[V5ƨkq̄I#mĠV;Ό'o)U2;k3bmthwF|RbY&΢ Qb[+MrR8NF94#^? Ϋ#΋CEHU(h`gv6dʐPTEG!%0T*<1)iS6(m{ӣrl|'#7 400ݮ71,0oΌc$Oػ6_}.C&71 #1m|y]0@ctȫs|6mfNK )PRz-W5s/g'k,Diۉ IDATf()f-$Dކ`%=oskU=DȚ1Qt#~ k|& ܄cahNȍF12ft5 +E%#.l1`;b2{ÉKTXTXĤBIDt3[B`@ǀNf)zl0W5ٮnJw/CZ=}fւWf31cڮmf`p+XΡ:w.Mch 0X>4mGzpۇ,ieG4y;^1z|ɓok4΍C,:5-&%. C]Hvҥ꘴OSp۳mf6fLcO<(Lӫ+ 8+\1뚥cb]-_F}{X[09>QIt{&7>4Gfwhf70w崮w9=msXvE-L*,je*9Z@.']pxxe\^puO iJdCBA6vU cqbQ($Z@+ &?3ɾIV j v ql0ۄ}QXH^ʢX¬cDh'˫s= ;7WJ8mᢁS  &48tX.BwHE]Dl*eQ) DF<}X#7 Ö %7L/<Ӌ1!l6'^>裪>l3ܠ97ieƅim+h::Scq6w%sϝ?*3]w}di`Øx(loy|Q| Nx{Ye}y:0ox*"O_w[QIj+ՠ ZϠW3JZ+>/Cg0m{`SҮ1E2,V/x+ٌ.>zCٔEյoc c&]JvDS[G)>?h6&F34ft5g D B2b"Ƽ5b4[/kP\V&U1>kЫmd%wI"Kd2wI`@}sA0ܚR|=GoXƀImj5BGkȷ򍆚۫h(н>ɱOr:T_:~[K:yKK{d~GQ}zdY>D^=ʂvw PnM #kzjؘpb! !1Qyjd)Pxƣ7{oy99ni갱;ĖB46/-/-?aQBU}{Y^;GL@r#Ln5hBu*ᐮB6Y]mtpO+@:&BD:MR6>yIArħ E]{ۉv+=pmiꦁk[Z*67>Jj; sp׭NZŀl BmR =j,jˢn@6lŶ1!y7TNƫ6D@t0i  ƪ+Ʈk& (6euX 2w{ܴc}>关=fT]Bd0DvQIJ * Vaf0cfت&  gbPnBhZM*{lDHwevJvC*"3$uCNHV{d%ådGGzK(JЀ X:}&.toV $<[ML^B]3$!UcbV Fb 4ȅKūr l&clQ>i@ɻpR^Lȇ.ԑFP$BnN iRW&Mfbh5!1tEAhBCMqM/40bH Mk(]C:Jl͊u*A R5YVUk-BW苖0OH]E-$'떺ݲMKJb5JOZK=u^@a [,¦DS-JjT"qX\X^ h:zҠ zRZѵ1={NߜSsZ1FL]6.Uݨ1DN7TN{Ѽq]8['kaFZ4Eu϶4{j$6C< f~'+@"LZZMav"ꁁyTIVj{m0 1Újdx&KԾ Գ0,8#>,H$ EǼpY2{N"}έL=G\GdC,T|}Ii<_~þu{U,*ѦB-uRJ|%(/4TZ݀t=s=_)%4]%]ꞁXQ.Özhې|eiT]m=8hN(0RMi:I#ec)\ aBBB 3fplCwqAwS.%aG F,y1r:1g#Mѷ)z6%a&FkZ JE4THG:&3k; t5Q" 6DbCDfL&6=fӋf#6QH%.NWCw}KQԮII4M\1A0Lg䐩4a0*1x>R;dϺ>yEyiyii=}a? ?x^s(ʡ&ڈ[cE72zq]J2oUc# !ZG3Pa<|yyt3fgN9Oi%].#JGl4ƠMjâM:b$SdwP nncTr|r`SXЀmjh6&uN!.5W*4Xȩt׫VˣmtC}1+Oqw gT RB1n Kj>lH1w:6--o5D X9\?^C- gU=1y?M#o%=jeKXīr1e@zP\+AuX& "I'ZKKKDΟjݒhW&B5 u LZڦZ˒c{kVZP +B99mrvzLvm ߻:h c X:w<!0th(Ѐ[nN;I>Zg㌺PkruE(ְY %T6\-xd3 ;cUfBt';w\-\<̣.WzWl.V!FOhff4fkRH4s>7c<2:\ұWt%;jʎ1c7Ǽ==Iz>MѸ@PeʣW6F#w\j$Vjé $)_~3>о`fܹ;L!t;c+qT7nU34JpY#~jpE/`GϭoN#]vOS^$ \V]S2M)*eQ&Cߎ0x}ͳ7<^k'4K~|ʕ+ /ݧ\GlfcL,E[HSCZ[ 9n 4#ڼo?+迈eBЋyRv-OV{ KUTT E@VR:暎ZU!봇n3L:CfKf8G9:֚xƠ?g'2ܝSPk7/.$1kvS6EhK>5M33G^r`ѱtQi*{L]M`>4h:b=lѕN-˫cKҤ4/ri:mh09q쌎`l,~ųWXlrR\Z kjZjP A^64NCi7~53?gXf)j,F%#XL|z#3dyД֪DlҢ#Bt;HllY2PJt)l|ϥ\Q^t pKX5_ZE-tj]65jSZ4l3֧# yghDIjif#XNKRK T!ss={{iYq3=Bk 4:qd)FjSVlv!p{5q4Fci4K#kAUԹ@&$&2f˱w{GZnf*Se R8(D6i1vj|𵄁$vrhjRBє H02zhi 85ݦ{&@e&*0|°y1Wъ]i+VEeeYtX\5FkE{WT-ZBzkdp@+%^P6^ ([:Ss*ZvR+LmtQ #m}* ."ƤjM [–%D[@Z'|3< +m}>4 jh< IQK6*st.K c횑1a`:+"w;Թ&]VqfeѬM9!l5Z`ΐcuǝ9J9l b%p5rJ XXM )dI-jn ĖNNuOgKD)vXM&F=Zע6-j˦l-/+zC44 :iԹElm҄&E-ay:o&}0R1jR>Efq(]̠dxN47r#T8xIN0OU&%4IQ:XITT lKw4(5O,{eg]Q&iܐƒ&VMi=h0G ,12G\+V.=<ytGK7o8Pp3vri1#}K^6EV6E[wh+ v3{3ƽEf1-,)hD85G5^DH4%1JZIsєn_Q,]VKRu)Z`^ kc%qې&>onSrx?#6BpЋ)=9&|Mƴ}h{)Sn064AeCaH#D$qH7E\ko}z9; r20݌wsQB$' btk =JvT7d (`v y *ViKk%Fb4[h[ĠAۡ<.ܲGn\vLk9NVn#[wO+fClwFr)؈r7.d 6 2]IV1 {Q6R-&; E+"vX&@GRM'M&cA>Rj50ì1ݨЪ*ЪY3`NS!curc01@sA8:,#ƣ ;cLΔDK+zCv9%[/5B^泱flԄauYU}*Qn_/(͵<:xKnW7;)g߱/&wh17sUHү;M3ML{)awsLUb8ޛXu{^kSUt3A:MF?t [K ( &QƐU#i!بH:I9TO޻׺~rƆ~nMUXf]u%(+  Ѭ\t4+jʫ_n?/ZUtVdyAKpAh-0m(orUvj7V됗RU^GmDn(J %Z+9EYd :2)h]=c>(볟m$$MFT ( 3Ӝ:duDݙȕSdIRIMU)>[Ⱦ#}SNqc BZ$E@Q$HJu,UuvKþRQ[B8PNUH{fu`][GuD|{;[q[˨ 7U$]dv/yo*wG]msrã{mQyi :ۚRSg j̨sL%F5 #'4l(Ld AHHHZcirat(u9~9/E*j#BJD{cH0jً(IX0 w+D8~yϓR IDATvLsg(j3Ujl5S52 Su2UGK $H28#qu *dYUߢ3׫d53d+Qgs|A9W\t8_!*'\~9`h86y~dYPgv]'r`(aTu9yj7T{xjB?6HD\֤du8kdBXdB':IczSenJNV2ga1j)j[1]N.'rqjP,J֘v֞S;@L׫, .gO TlVǼP}ĻHRg.{\>yzb%u1&lOi1DV(Z}''\=oPDKE%YBȑ"Ca)g .}Mjkn[T'ldjިP(JF5W4#cڝw;xaH% 9WImiy\gZ2GjNfZ\ͺ :|;hU/./)ΨcCvC3U*PTł(4fn ~1a,"k>@TAcq.3!VHKeTB X>圚2pc,l-AIzK"prUnUdkzY/]4rv!6!v"PCD!VubM'V5~P6L{>GN~!\b kA1œSZc1P X|^^S<-|eA;P9'\- XxW^r|t5vLՄRPmEʷbV]ZC 2SIr S.PbRaa`ª Tָ=T ˃ o~V9 dF"LĩG4Npo/`![iׇ+1iMX)ۍ1fb*յɷ2&Ra)VyyZb0dRS13jΘ>F9Fa)bk!Tb'Fk !u$"81~} 25l87d,L^`rKcl48v%3|NpqՀ=VeWYU`\Pᑃ-" %wo%p 7/-~˿>w| ?ʲcya%Kk/O1vd*.ȫd,I(]ll֮('ѕÓۼz2B}3KOf\\qq'}*EI upO#<_ʐ.A\!x,}O˸f;N1wgkClIؖmMI*ˇ+iPDD!R Rv8xVKзc4-FkDdfV?TI`ҙxU$*徂x*1d@:3hF[V}Mɨsʜ>.b(g+>VEyp[sAJCDE\sYt]=Pga(Z$* JPPqju&T#^|Iw~FsVLf9}3*:YO'D~d&OhxC I|>M/Č6FP>_=|/U9dkS(.sQe&|fhJf8ID% V8\yʔ:&TlbI%]G,D*ɑ-A-ObkK4-J*ʜĂq 6a&:i3 qK$#ϪtTi0iL 'i̓9n~uuIv:b2ix'(*s%[< o1 L&sNĚ-"cƔGވ>s0d̗UApPHiTT U&qK%yNjj H*ZVsjJy~9^@LS4O|!R %KwO>cs"!HyƗ/gdn2rwdM$<\=xxAぢ"Usm49TwH :d Ut*2`O3z YˠtprъXp 7𯛷Lz=mk>SɟI>gM\˨Rowqxb,HDP$~mzus^>|[q n17'w1SkUX8HQ"P*@^ȧS6(h#YUYXv1Ż9x<$!e%*y"`P,wm6{7$F=%jijw=\y]DT)umJ)w3]3kyGOʕڈ{!T81968278V7(B \Ljkϑšá)/ӝ ٺ8e,M1 ]?@$K\,+´RYQCK\t[cޮQD*D iXcđKZ hv&zn8d딨g*N'W?&鋱HoeF3I &G[>{Х RvJ B Rԯ9 pӓ-Nk]^J7(6:gߔMuJu5Ϧ% #'c+tR匦zM:|B3Ќ'D$B#tbacJS2@+Sd!9^`8BHUK%-ʔ63r#IY=vM6E 1YD˥@dH=Z G٤ pKe@, BP$%M%[L٬ڛ⒑<}1%X<OCKZOxmy]߷'MMu֐k5ˇkH*P]AYHo31HiC]L 5&ԙK>)YN*C>\B Ƹ]oះ-ne~Ї>įp]>pzz֛;mW):]U!/]3%Ե1uo4Be8RqrѤp2Ww9yE4d0ӈiJ.};P_,6 T@JʖcC#k ιW\Θ&MD\t+6JDF3p* EFT)63f^ڄNr< \ 9X0 S:gE 41ueLCsl(ɫUbıMf༰]bCR rB)P#ͨ+BrJ0Bg!-sLа dRJG/5o7xC>#1uJ1S)g*Tcmm~RWPU]uTY9Z ^&..euJ>48MSX7kJDLM҉I:5I& jjmF6% bV]Rb:JŲt TEZK:^sCK $IbQ qY~ذ4 MYQ؝dogҰvifXfe%zJ[If6YX, .Rk]@.*YFDXeFhIFJEzٖ {nݵl4fS,-VR|%%&L1Ɣ:Yj',gVP܂%T,Y>Wz+͕f8RǤ,p%vg9j&IK(V#j:fh:TXZ($5tRS#1urEC%D]XU#zFE_]M 6a44P[9/YuNp U&Dùg;h5Z-[(]SԼ95epd \EXu`jxCZe})CGUJ*ňITGF*[@#*J$U4bz$R{BMB IR o딷u4j/;U&fݶhĂ3AtF[)mWVsD JQW/PDK;*Tc+D4nԼm\4CT%0S ?l%(|7nn. O}S擟$ԧ>ůʯ_߽Y>6IŰ}ꘆ?.: EGtisugq39o09m5(j GT J<*/Qt =!]NyRkr,8vmUθ~†qʮvH[b)9!SHXJBWGl8#ɱHi̫WM ޝ9;n5V씒aaaQ-"hi _i36sGT=Mش 6q$ HؤI\B(k'&:tk3ƐSC7Ϡl*pœ[usK4IL#dѩV]i cPEV%JV W>+ŕׂrN*gΗȵU}rm>HZ&+WΙ N7v8mF+lZB*umƗM =Pz1I Åsb~|Iv\X\C*`)f3 P]H*ᒮ}A׹aO1cVSL`)ӬUezJj2c}L7YFXk`MR 3ES22EO2BqкtFoB_Өi>7]Ѯi#ڳ1Ц$-br*=%k.xaAԲHQB2V2:5:a$<wy XMmmAV/15Xm..zX5jfD TR$"Q-2ik P̍ "5#"q%J>~`2V<emFJD ^˯sG<ޠ<0)UfY :AĆAikC5-uDq͚ y@D_ Ye4VYBA`3͐ NQ"ڧ,kcLJD*4ϙ YBp4bk/C'):cxte=B$l 'v^S® ,fQtM'YI]н n5 -nl6ETŶCl'^aTxo@+rfC>/~ x_~G~䭾7noҗ//a??{^9::ǯoxfYQ!}=44k#nOXN9{2&*YW}^#^B'_朊Pa5F>gj#Ch|厨HC< irǥ ٹk3n<+4)lfo ?N!Ҧ8q1JgmP .>U|R#|>޻T%nmsk%JgVy.{<^ܥQN nԍ9 ERyIgl, rn087L&yQ*,~9Vc RMA,T.\;̴:s =STDchUM\q;t_q)TP)$D$CA?U)U$v70m-A_&h2%IX$_Ib _W|x6m$z^MRKd ԼD7rL?(W\Yu<ߔ/=񫸳ݫ6NjW/ej†8aC)NhgCڋkڃ1k9b8Rzc N0ܔ( <r.33nszoFz^TXYdZr7&ԙ)5>ބ^V`)v`MS.(uP@j IDAT@b)}.q$G}~@$e[lmfmetkƨhY|YΈF9ahT%VR6CSsG-bK\8bI!Tz`,,RP a^!L+GoO+wջ|`7Ȫ:oUu<ĭ-it'Kby:c e"-,ȗ%(%+1j0P̄DZؤ +YKI`l$.EF/R٪T3'(x,~Id3)N4'5Aj [P%L2=,J%Q_Sӯwz3HOm Rة0uNA!4VtwCtT_ j 8iƸ1NS* oKϢ4cu<&- xߥ0ee谼}#6{tjWl68߾KF2B%۽oo!|?G=* 'U n eo}}??˿KWHy{?>Q%gy{G Ͼlz"/d/2T:D>0*Me&YQ(+9z@FoVT(lA:ӈjc5$Z#C1KH HEf.0Qj tC1}5BS2rR M Dz6BQ\͒Fi%cDt]EC+ epu n5Ʈ8^e4pL4 #Ѽqҥl"IIQ N4![h3ʸ>ӢܭP ag$m 28zD[kHj1'2W!6,CnaN<\K?(*bhxNf \hݳ2S%DJ0LSrfkYg .OԈ<+11f TIYjd$y1Ϫ\}ȥO[m.:lJ9:!N"ߚKZn2 -mve+B [Tr[%m&EYbEiPT/_ }n}vc<+gHu @ǬVصvMG *%k9j)Qrk%T]8o SA=<xU'݇0bf.b.jw9msXF\u\- kj֘6\d!F0O\X.5CsI)&Iq\gԉ^SՔ &ֈjsЫ Z#Cmg(ɗX~%X~2&6fxaz;5+DZ3l;rdFW# ̩Kƌ9ZJ:¯7MP}Nb`3l[X%^)9gC5U32%)]"yYeeY[g*$LcZPF%E*+!}=tdd!NBlIa,̀ZXp3,,%ѵFdjD*85(}q, ܨpj̽*);Y6Zrlp]z>я~y{z׻?O>ggHe|}GDVݳ|3|;w|C>qxx?K/PG?C }/yo$9;;,uS/~G//Moq>>c?co&i__SA??9I~~}sx}{oe#[şp}>xOO?;gG|KycAB-sOFl3ynSFBXy X$tNrSnpK"A1. +PpzV/حbvL"[(K \b ҚOl";]J$vD]PZ,o!-{u$ 1ʄ:5j1vLJH5aDu1V.SNIgkB{sJgk[YFVaZ9;q~IӤi6uYeubUexzMY!9j ˅R0Phk/AJ~Ah, [aikHC vCvC#,b^Xx!+ B'-WQ&j?sZ36S6zgl$gUA^]lr2: 7Iz/4r$#õ!,,׹2<П#T,[\k,sb ʉ6MnDA׺jO9n4<%R_9E:i3$Z{FUH"qqn7*tK:وe ׾׸(YS4$dK'C9;!`B[ ҃^L[L_L[# -\e;8ds~\ $E28\rp4! &:Ǖ M3%Z=Ch"Zp=NKI>5X*L .g}Z b6FT˱rmdN@u{FggHp%JC$ʈVeDYsǸS>')[(EI4&410+L+=3M}eY#~G{AaMzր5ePO tpkgح@E8ks#2ߠl(+\pY>'Q1 Q9Ped G*rPPoP8W@Ǩe[bmNHҙdj01 "Xc5]"B0.CdzIdzRT%j\(B:4=c{+g,RafDmjy:7~7_IӔ~8l6?_idY;~b_2rZz16. +̕@[ @ۖ۶le0H@i@a\v%32#3;(d~ңysN'2Sx==_FO}SO$'>?y{9* ??>~75/| 4ͯ{_ɏۿ|}}c]BUz~s-ȏG /B~~~|_?08>>Fx#^3wxxȯWS( ??o1g'kcubS KB,eSC;CvLkw~℠t&X4XPnXp"’ ڈ> t|$/}-*΂jkJ$IR5S>7M46uCN$ yARH&IeZ2SA$4Q UVTYk7 /Lni,nh?:X%3x1sإ͸9tiV,eⳔ=fia[? T:FQN?1nu&4 аMY.ъ ;Wmih}bbj$/(OH}pg+fY%+(20s6uJ%m^7-妾dXk2uD"X.Ӣh6O\s|EnsיkDKp. |i^҉'H@Qn, CM[Xsu$>aq̍軭Vt]圎2ͨKPv2rM&Q5B",i{;ܺʋ+g&Ijd؞pmzo}J9JbU58]FqofmX0,3i7/lG}:8@ $aT$/)OPOf9z@ Χ[|{&0ۯr٥̮&+9a&nK&r "%مFx0;n081ZȻ%N["(eS3 Q/ٮ߻z] LU5LU9Ӻ:J9; |ɞ8eW'NY^x ^8o^2I6og=/֝xnGzFnycY6\j=N3JM)SyRevgINfb( P#fP΁2"*_RHD3X%jhf0Qm%$M _uh4x?oo=U5>,㓟$o|_%>O=}_<˲ ׼??䳟7~7x[*O?iַJ~VkC~ל#Gf_JkBSH:L p JI)IBfhJT)VwHazj,1 ꠨9r\ /AH) 1^< {Y"DT WA4%Rl'ؒq5$|֩EI&Z:WTѱ)*nRWRwc.4L#$A Tµ\etKb$?BFȵf6\RCJM"uRbj%HffYI֒JL|f>؆Ae S#l42R 6sdF&iBD<1 ;XECj֚7ƖCJU %ȑ)I#<#ґMJqzb)Zy V]VGHKF#ԭ1[ZK A&,*R K|bh EQm;ZAi RO"*bt`JZɗ1fJ9!y&k&)42QfRP"EQ8Ȕ̮zC/"VmBK6.X>y=+PP4ͽ=یm TaJ$hq&T9^vAy• i30ŒJ .0D#H YQ/2MgUG-" -p*}E"P<Q^t^xע[GOi*̬:Ӹ:#2Qd =FK2$PU-IQ9i6]FH3)e Lp%L!SL ț , ]-KѢ]:@Y ȡ@K$[k3|iVG9,QI & @Wq{\rJGPK'̳ W([,ĜSSeJ<2;^e40޿[޿FhWs< =A5?\mggg<㯺??3OO>//aY|2U<||xx=z|qq#Gs~[Y",rabMcmeD^0tk@(5 v+&+kb )u2+ɝiXRmY_gIDCEU:ꐎ:hkC-G FZ $>.R]h~c5ClsMM0v>J~ vGUf*=8QLeR-vl,؝97̱*s&6b#R+$8fBS$/ؾ|AFR$ff,3 jýLkU K'$cABY(H er8571ZQSgU(b/:"HL#tT]ۺρz(%rC2$DP%JDT2,pӫμI bf1fK`˻`KRb 1uC!a1:ZǴ],=e7 " 7YXy¾ɮ}4C-l^eVW% 9e~^yJw9,d"&hv8GY y̾tB@d\㴺Gel}ZJTK8+mϐB PIYcү393!EO!L:lb%T#֚Z)T2Q6u!hVA]_S4v Y6Vhd>FK1z=eQ+, KB3c{;St7@Xe7C<YM#MT…B9TjkBӥ0tPRsGNwR;!XvmQ '9(٨|w}{y|w7^}_[[[Zx7_­CC#$R]ؖO8lm9")7}ZR(es.5țUv"4M\Q(K2bK%E4.(r#!DC6V^bb)%FfE_rvX.d9 99V%8d`u /6=jbN;%ZR4n{6'wI#4Ic |8+Âɋb_!L#{L#W5|gN3[ӻz}09wb95Bu2Ʇ;e^u_YU,ZLS_֡K8?e99`-H7a1.qz〈_cWUVCM6AmFlL&I6Ka[%L*sRIXQcFZ-&gmz#w9oo|cr-ˏ Kh3h@q/Ho>K@2”6}L9a;e_e%<&y~$.wYi`*1VA :pHu[͔-cȺwkS,"v3v3*yVl ?6\.L3ڂVgH3WsrnyJ{9d֙Xu&nF41M]FW ).MQ-&f3wIѢ%x~ǝ5_39N:l1{Vٍ T)JTehG9(# nk"JV Z!uc^|e=̩3I͑Y)ԂTYJw>gu\au*CiG^U9?0i4(hhÂLq !E*j9F/Yi)k=ΩI3d 2yc3[]6ىNzW`ۗT<|}TdI`OkLmxJ!6L]\Qd KZD-)UHu7M^ϩ]w (˱N6įhv4c2}jW:p%e IDAT8wy'P4ź*"%L~x׻ůύ7?}sx;G>(UZ͛7~}?C?{^}Y??k>Bw{~;{އ?ӯB>xk +9MFue㸍79{>n 6|.#") Š.ң M%@^ $#*BN*+d!-JSŵB׊۔ęcL#p܍ _(+&JBJN-t"g%jS{ΑCA8w-οpG ҅`p!A**/cle(Le`*L\nQUrJaTQ|_[ ol$K2DM)ɘjL]؛õ,sK22H#4r!!2b!Qq%ϛkqOpmJ`% Ed؄%ĩ*Q9\($APY<\P #)O])Ŗ¤.yA\P-)h[9ECʄ5Ǐ! P Rz2UoF;:ap?vr6&4$İ"L%f!*g>ͩ.VԺsRGP$zVcUU)+ !>|3駟~C._eַ~:_<{mcǞy򖷰^|g~g|2^uoo穧b^___w>{?|MG<~rr2ܡ8 ,rTPX,BqA f,w8/hV'4 d,bEZaWwqOeY/\&KuJ!Q"c᠐m7EzBTw\imż[d]JXۢώg#+IBY`bJoTNM+7O|@eTOQ̂'0dWT*YUcZ0$K0SY>f:-CMsyGe˨´\uįlihJZ"*gg4j27 +9mt/ьHՔsiT'!*և!ڀNs@ǽ@sSKe1%VR }#{IK,!lF %ϥl}_smo{o{^~_e}C_3kycww㫎}Yų>˳>533_M3 g{5x#^o\7d3UgB'.ũ|+,3@LiT\;8[Z7TY0W JEž4mVp#JQo7;sA{{d}T0P-JRtWgUԺVQ 8y㛇kk'\vLZTsFFcc!6oHПɎtR -0``"7H\aNsr Y!DŢK,O|HuuT.(-Ґ) ҐF6cGz7zi"̽N1U)c0y.m.Gw.q!w\Ν˔&BV ڕG^'Kj܀0ltXH>.hJcۗOٱh9{=Η==F 4TC2%2'?菧J9ls]C }r!]uj .: cdR4Un)6RK"9 -{ *j^:gG!\A>ʑN eBG\ۺEyy˴hab*cPv 5:"館n[ X>z nT$j8_l-Ixl"ne17[\!tj WY+\eHn1j)l<߳<# "Ӡon*pїX_~Ccv̬2kB=dowx#G7m3|;!JLB8BY0? E"#<]=<⿼e? @CD9و0[MmYī6gA.wB;NXK+\NeA䳠veH_A7/d*r4r4ӟl?1v5BǢ8h;FWf&XO-ʹJvL3 4HDDvcM#zo_}0 )5e,x| _Гzl Ҧ?[&^01֊zsLMvRC:3 _AkG}ۧW?q_Cʦņ)%?`X,.4ȄAX: #I9 i\RG%bH5" ]*  "klm2~`3. 4f=e;CXB}kPT Jy X<ȅh )M:갬W7U~lheXAA-ЍfeHrT9hatf1 hm85iun<'x3!&c$ŘRHUiJUQX\sMBR(P ab6Ov_ EǷ\*7?5C3$)'(]"9]\!6a+ PQ%vHe{JgpF[?Key޲ h*JU`K 0DH4 1 !2$5V̭:J&9j6e#)]4J@ %Gsd+G* ʺ weRC'  iu@4 oJ@33LδQgԫSj[Sj3bdmMhF'q5טV't2Ec-{r"%"r8vK*ü͢YE< [iF~C ђ*-WdAU@\"k}zeMA]G}v`ұA1PDR!5LXCWrPY)Q<Ә&ufii^gXvxQz#Z]h"P!TPk,h79Ccp1w)#emm#׊L :^9@)}J_qW:d(%Hh#:N1_[PQ`2jӰn (r٤A0q&ke䵸aAf0T':iS&$S0ӫ̴*3t>q%K&LVZ(tFT:kȹZ`%ƀ䁩Ŭ2ZO]MFr}FegFLEGcrU0rwSjzAƱ1|B|2ωM(,".O<)f*e$&RU<&QI`UH# Akd؍SKХ E*@$ЙOd3ԥL'4(i'({c/6ZB'^6 + "r,@J]wVx%:e\\ˈdH!QM2UP%$؊0C ћ!ΚBS` tLc׮ߠ{x]_!9(+Ҍ&crt"aIHX"Όm粸ML1*dhr5ߓ?#Gk +Xpg  fmD6ķoLnJSp ɰVuj&\_ƷL&cEVMM#j 0J$'G* W"u4"$&@لQF2N[dSIFJei{oYtW@68#X!P|Mz *T0R٠<|^~AɸɭUn6I$o$Գ{mÖwf1񗸝]Vy[ 3FÝpu{mns}kN5^]?uiE$I[%15晏Yd&4ҶԟGёE1( US9Bf!9]F7cNT9m%G' @iCb3יu&N2I:DG ed"5 E\#:68VH2B$IKismq+ w˜+[\]j\+\sEĩI$/#$ mt˜wTM}++!.y]팕f~Zc*ՙu I)sFݦ.MM?>.rl9إ?!raL8CZBflb%{f˻5#{Sos~ Zr;{0%͠p(e,Ld۠@,WUL' L'umBg\]\E2MYڷ[A)}z&*֜mw b`N [[z[JH4H|3v*mEXp'dB㸼IqqaIf&"iBA[!\8Kr!Hh ]P!KDkDgrt@+s woIswHѝ}+Fc!ͭ Jm"rZ Qc"ט*5 S %?Y%Y1ksgfeeV;-\!}cc Seɖ@1HEIu>vU2+3Z{b/Visp~O;Zo| _ OOOVũɩTnL}+RE^ȹ*:Z;9{sv VxzDk:Y 5.) z[#j;W1[1.Д O (cblrLBj@ÊNuŞ<|d!2* ׸5{9i蝯r>6B-hFs>GWsFNAFx::yQrPBu%H4 ٰqVj6z'Ԍr´BJ"}|51^-'ӌu¬0)^3lMH+Pzʣӿ!&L]N{l-.J^{5B^zvy$Ə_U9-oąM P WFl<Gߠ%ĥJXySu'M,Z @JTRRj82+(wXݠ+FnC?o}(2a*L6HJQzE($My kTy:ͅDa(9"8@ssg̝لnwmwx;>-e±˱M'9GDMdD5UXpX %;Wo󾫷xMC)ǪBAA뒸sbIʱ+j"Ee[+l-AWs!AXćWLV_tB6f)+ эiht1fcl IDATf9'b.JUŝ hWT'= ާDyRTn&>JC>ҐjT*vRz8vHkcJW^-G ۈq[:L==xgoN7zL.R<bLFϔp]9f:Y3DWr$ ɯ5qk\=,4&}ܲ =fգjMtM⮅Sq%U9w! KI(3$2IBh(J]pU/( A[( Nm LIa,jeFUJT?WSJ\ȱ@rl#6V溴oyV'J"FqCI"NZdBW{{CJSUPҵFj*m0OGs˺Om5g%]VJ: rF+gYi2MdA`U!x~U 1[94pJQ&0_ 6P͒f#6jپJ-N!JOUO\}.xAD/j x뒹*Uak1C$EMAtaf5Y#|B!Z |~P+ZrVM2ֺX+0Ì(Vg8J,L GY1llOe F>2UW>e:osئ\&r:LkCw"5-rG_&$EZ7r\W7,npE=i F`BTi49-cBc), *B*Y8z:u49gIjX43tgBX lf!G*RC"<\ eFSPf49J(BA4tɸlT']bD2a1l#uUg"\Lz@ X:KBԙM&f!v$Fhw3:+%: Mf -oBq]Zzk\{?ɑz#5Hu9621۠JBŦwc6nli_1F\jmfzi! ^soՔBSFK|aR. XHJY=`BKTO2)OT"K!)gqXcQf"H}*VXLM'477.hl,hId=􆉾ɗ'9zRߙwL69X 5ۘpSy̳[ &RiT%qlD%wx_ vx +21ˠUG1:iF6T*VLir)&V#`{; Z h[`3L03Hx0U䤎hc/CVHP upt̵rY$6in׎Pw B>aQt17IPBTIK`^Y*W:络d;S[KB%7G|BI%We U(p7n[$G&ѷ|$ R>0>+1cjfoVpB%o +Ay =\}J+9Vw=C* (`xO%.2r;>np8h> Z+DZ(V]ڞNz* NP*d eR҉gCF˝u'#4҅N>2YXúT[ޒa6@MK2aVyŝm>?2}һyDYۈ inOhmM(UE`6 5 &KwdF78.|bFcs96<ho*oMΜMIg-&+Y?;B *a(u`jXrף`k̈́Ym4P@ofZH:G7xxz4@IDIԙd#?cK?a;aKi)3e@=_R`7` 8O6`V{(ےZcVn(a]n<8_$o@y_dްxV(A(.p@(um-H[A9(Q `pJ"fP[EN pwwWO/"[[[skm\k\{iZTXIZC+d52TI*]IqihRR*ih\\).ã>` Ŋ8(7("&Etf#'*٥Ivc¢]ڟLJP3jƂ}Ii nV>0EE>2 !H+QQ8ZAQ!5/i{o4֥lKF |VWvwÆqN~ѿyA\0p.aV(Z*a1<*nMMYcy9-u\*K8-N\^l2谼hP HAXRJRSuZp:o29˨5٧&j2bǣ3\κdSЏ.A7WATgm0\̷oa4 p*LcԜ~#v;gнo_0(.Ѥn؝uORa-ջ JM&9Z?Gur"݋wSL'GJtMj&ZIخ `UȖ8:a1tԨ\A >o z3_l3|P㤿7Kه4H&=8hztKG QN`L6'=ޞe1=;V,'Pg4l7 Q+H] T1c-V$}y. gdXuDY VDlyB6!JȁxM1oҒk_΄uVb_@*D"PJʱ/in6a 678fۼ[20 Ԇ ;1ڔmgD8hmPՊ^4\a8131z :`R%¶W8U_g!]Zy+Wt)SԬ$sv(7(7<Vhiƴc ©3o4XDuv}J߸b-xeEJ2i\$я}1˲_ Ðy?qO('?ɯ_cB /a/|4B<k򯳀⋼K!w)MS~W~˿|zMqk\{7IKeVg5RTAU-PYM>H0ƱE-Y & EiZp'`u]f 0%QQb%z^5^XnnhcZ֘1*05/ѓArɽM_mT5C3ZFNiCH<`I-X2bv8^Eq[˞~mqE=rR p\zRTzc#B+XGFTdQfdvYumS礑N2b:,y} wqPW49uoR5\~p U/DA%j&Qb D׈2$C:21taSk.K|3]%XUa樍1Sw[y %5.c[11 >v &Kn((bIÞhJ(FSѱ&Gۏ+M϶G]KX5]fI6cv~[ &&e! Zꄞvf1P$4 . 5K"(q3^QĉK\%}A(' D]Ϸ@AF`]?e:ZBްN y-! EL%zGǹ+P_U>{ӟ4: Oo,_yשS/UU.׾7 :uNTU>5qx9g-$*}V KjjP<)Q/jf6p) t f5g /ؗapbmoUZUV%5eIiJakj(VUxJ&fLQ Q~M ,#i'x wS 2VUhaߍqvWI MVGZYHJj(aJM%LBcV6HK.S9 u&jW.K|l#,>Ű%0,:AR'):eR& P)fU+-ϑ[&H"l]_<.qO(;*usF2:A\֑*yvV`gB*+#kT:ۣxrM qE@dW;e8c:k0ۄYE/s#<u,g49[u}APz>VP.UԊʯ@Bt$N"2,sl5k孨3j9ڐ#(SsD+0 6sG܍UUEVTBIA]zUZ$\(\YIEdQ,c1gR 2 E*ڍ ~72 r.Q2/q M+TAHBc)|ԉ4 ljG \BL)TjDKbN pYB$jU Lq7i0QtMP2Z ,2L'h;9V/#ʂV2EsʂSBt%g[?6wPȉ&ӣ#M5hIZ-sM:+q,0IWq*P͒;F!r0;m=lVf^D׹b=g9bzng JJhj7_a)Z%Gjbz\\PW%*bk+ݺl IDATޔ9^%E\nq܄+x O|/| O}~뷞no}m|3aoo??_SRK/O_^7ߗ%^~evvvg?}_zO^x</~OO.w׸ gcU$( /%@!P2t%#9 ) ^Ld=fZCd,oxmb1ynqQ%YZXABѷ!â(1Ldqz&0;nba9FJYIߓ \lgG,"& ktR!Dǡ=HB,h.xs^ k5͡3@u%R*ꀥ#G\Z kIz نEd V., ?7BoW5'G@xHjj6aTÌu9ydNtw[̒&P ga  DD;D{9ўj!u([*nMV8 4CluP*&-s-&kswٴ1PV}Fa!}.glwQcfJ}W)\VǛVԘSVsnR)g 62>PJWXmfzIQ%9'1 ^k<ljE7Y04z&نMEں pq:b(F VS R*} {L HT]KhcG8U %=uH߹ q ޱob6Sʮňz Zx h\՘Vm&ͤ!n9xꂉ9 XV4vZ\^If$M$cuLۛbD$ uxE44&;fDo},:U ^9?&nćl4W ԩdSdB ͜ѴF[Liwh7ƴov`1R :".mF&ʊG'1jB?.hIV\[\=)P- _B8`tQu/,UpⱃtΈnc ̃EZK^HnI{ͽs6glg4K";Dcp\'Y`hM"u_HO먝v"n]LSĪNDLk rA* ೟, k!x|eE^ɯ|+|cW^!"ۤw쌻w~ײ_ₛ7o~uXnܸtX/666.//k9k\-wn!r[ *ȠBkgS@ɑBR:[/sg378RnpFz˦ascL=F\lig5^Mofx+KLSElM4 TBPvˈ T38+8-8+uZYK1ݵT4Cu[KUf9':W)c6K6!LII:ݘ=ii1F_dgArw:\:&Yl1MZ\+&OSX0ֺN[cZ)&h' Nē))VL-Sp{! &;%7Gt1`tHkD-b_hsN=N"2'Ƅܬ˭JV:`cW1QdE,-FUYJL:#1G]=^riY)[\>9[℁qΒ:3r" d"c*V j:L:rV;xv wr8#$I"V %F[ɯ-YoVݞ}4uwΎ.~b*T{5->>lai1cq#%cl<7-,B*Ī Q+ R bjPV:SmSIJ#m>~z?`7퍻|Db>k99 9t1qBsn Com3FZ<]qoK Y<^)F c t_rpr9.< nĻ Z>Vqj5[&54"D5T}jMhW p&H? |HX̟ZK9ژsn6q6/o5/!4.4S9]s$#l0{a2)i5'\<#7\j0T«eZe$*WypX4)ItӞ-|".S1ylWx%#q̆v+|w].DG:ߕ>W)S*P( Lu '>aT?7H=MZO\] DYb+7s)⒝ ʾ"8J$Z]DGBw'G=!>9{^{b( 5!)N9 @k BL2Op-.//y?ﶿ(0 0+_{*_7AUUυX~3 OO˿Kk_ڿ/*_+Wcglz\&?QJERP%4 65 HkѦ}EYg7 YbR=zm=ݛrzoF9;䥠M>TxZxޑIe`+`=|5`l>v(g͘Ve7cz%bIXP/EDXXETZD3ē Ih'h/ym2UE d@ R]-'6Ei89c*^K.6kTL2X^G49"QT2E"Q${ٝ~ˬM.KXDXA(iu_wHLbOsw $\Bżj23̛ RIb6WK-rnr,p3Zd>C^OQ:+A~AHCcUYԙ% fqAxi;/=P%o8 ΒcS z/&/vHKEL yj]l&Zd"h__~xwiZ<ﶿo~| _W_o׾g>677q]o̟ٟ?p_ꫯ/SU_W}'>K/ʯ{a/я~_׾5!ɏ;??泟s/~=p+\_ 83jm'm 7:*e[!6TP!/*r$+hLUI v}J˞ҩMe?aRy]'LN#b d9`8']`6BzȲ^7 R]Qr{x6; ga+%x6 sTr)0! 򦊦8V0?c3he32dx?vxtxl(NaMO1]I06O/ /)Qp}6֎ya>k 1W|PYPЬJPՔL($F* |aqY7L[Fs:%dL'bN &SS^r8wiISDO1!i:),9M3ϙ\UYf{2\0OF' Sj3cz3B ߰9N1T)gJB b-Jz1Ls 8 \Z} C2&7yXXycxU";` z'lb1ZR͐撢y..GL %z#bx.hrB$챍G “P T9Q5mDZñ"!/X749&s◡bT&UGUZ«3/]Q,7YWJ4rYa49bwyM?[#%9 *1:5=u1 :S4hrxIijr =F*DY &TB,&6:ڳsrE,PITYT&rY'$@$ 0Jb3 AQ *.=L9nt K^3+OQ E(jlOk;wȮ:gRЗ i3Rw7rL*uS^P;%:ilȻ;M X)qh=qQQjk2g:Eʜ QlHߠhs U* O}O}Ssh}䓟{}lY7$I|+_ٹUU׿׿۶X/}K?//=>og>?c2 o~||W_p-rh-]6qHC`gMM8`9/5j<#\d=RdAt)h8 ^pڷՎMFlFN)ιT+6."N/`ÅZ\B09\5Ti-%G;ZNxJ+ E"2oC<:>zqw2f+"VFc}cnc>P!ULҔLiEY5Zg,pIs,T("P,5þD2 2M#kE[VllU8U,"˸ǔ6Krlt E%SXqȆwH*iL*&9qn].EKeQ6S<= sۘA 7]D dDnsf xz2&STL%'5})q#̂&>sA?  މ_C"^?b  !QuV{$Q $D%;!%IW&D(|ɶLi2-f49a,(<q$)1܌6Xj8+/$: b˒&ӕΩKs,BT2`E"L<_"5RW滄\AR!0XEfgMܿ˽S:3S6ֽU#;Na58wz=,V:PQ+9b;G/4o^" %^&/|~iʫ//t+\ KoBEHP٨Y%HJUK4PlMYMUd arD׻u4c!\V#rҦv9WĪb2,b.P(Jz?BjDlĥARĥA^ԄS,%C+*GH `1#LّJn k]scx #55L-΃!zPW4Vr86x0a IDAT-MpiSr!s€y \c.5GhiahG: asm2M8z(NVODSSRSe48,FYFFfhL6y$>[V>(c*Irf5'-.b1ae!GʊE "a3>&,l EFZƔM24;(IN(&Cfe>eLiS\$ B"-$-J[,J9/$becRc!ה]jީPIEBxF~#yJbA(䎲j *gʀKLiT)1a>" ΔFma`@C B2::SpQMJiKADFԗ39}ZT#)X(7 McRψdoVc1q̦-VLȔNPVeUYTX.)5cvB" Ox:SeIa tSD2( 7 %ݜ2r'cF e)G AT&K4L_eDI 8Ylhy<.iMðc&<ȫ<)c}e:߲B΂)]v]{D)ijVBsdm Y wKDP %% $X*H` 9 "w&DATMP [i)PU[hפ\]w eC/+WÍ7sa)p+%~*~Ms% z c6t15ǭ![# S0e K0e&ٹŐҔX ]Lop*Ɠ!b@ZpJR3wͣ_NBK}:{m/`(XBBS"Bd@3ѥMJE]#l9HtJRB+2`jݭQy$s;Ӥ4m3MۄVvV\D.a4R>TB!_%݋tflw2tOi3|5Oh,X!igSV)LkMn4TN>[ע}}(!M5A=c#}PTPEN+]): AdU;/qAHw4S3u|&:^%.^y\А|v!U:(&*Y n)(;)!*A:I/ C<Ȗ*e)0刡8^9\z\λF^V^И./Oi/L[LGM0[hJbdN pӶ-nxFa.BM YEبdجWWxIku> sB$蘄=P MΥb̶v|3 KgܱEwC6]2W!+2.j 5|O]Iyd5\ϰ$/IWVBcX4g%5Mki5Vof'ljrP9E2I/K(bI"4=J4Y%04&4uma:ўNH.U'fT%!Zu}Z툛cnY \cc.Ada]^=SS]C (/d.68FJ$GՃLÖ1n1 z/z{*RPtt ת}6c՘@ _1 $DSQk Mi†Gf(y#G~!G"d6VY*CYP&vvaAkԜ%.-kbdWo5\*/W'}s)I9p+=2d3ƒ7x4"U)&T2Y%rY%x`ajՒZoI{/vC+ƴ:cj,[.'d6l\yt$JGI^?Bj#K0SH$Q='~IyUѪ%&=L9K'CM&CBE*KB*KT9EoEG_ 4#!uΌ.3 69,->Nhv+g$XǙ:ı}fԇr)S-$B&tg~G)J-E)>](N[;*'>o6z0HceU!!S !S-hfl>⃭f8ţϷ;$SgszEt%Z-10W̭&3m2Qˠ)b^(ﭢV XZ?%B>I+u;2 ]lKJraD6&yQ1 se&']OFX>va[5=8;H0 JjHNhm^/_* VpWzY|P5Őh ) yx,r1:3μrA3eā)#uH3SnĠymp8V8VG\ҡRW3lixhpm%r!e&SNeL50zz3ҋ2Q} # 2t2dT0(/(SyUI 81:GitWE2 f3iS4U1/dJa4 MyJ]SgI42{r'wIy"ku 1ece(4k1Vt截@R aߥZg>n_sǺmGgٽIg:"AV8K:\.XK*~Jy)f2 w)[씵Qv sAϫJ Msu3CKR$#:33wY;Ii"UWLz#ZMK-sVr3)^,i قf@2~+\ W?}K|jŴlq-BFT0N2fcrIj QRB(AːAYHET HqA%$Dg qQUYPU*P 𩱬\",l5m.\ ɡV"j%CT=C%:fJI# &8u0IP]K՘\ȔBzjRH!$ejBg y<+iS0ՐrIK\bJ܂CP uNu {j*4uXG3I#Z ,%Ci#1929qvFd$1͑Z$D&KTT'N LB)r2MIIMBn~C;Ez9b|BĹAE+g;3#Wb8 lFTf|ʎc=Ŝֳ9H#e`1RC04|Z x]*2]E(2J%!RU V9B85rrKn.hs 3ƶ|L犺&5Bj8URU] W޷߮>'9[zD#,X]X&Ԉ&&&PH*P3s pl!B.,=Z0CZDAQxCɸh#|{/ub^NAlI&[2F䩂_9H%D#v->Δ~w̨q&'U5)#W5fTisXu]c- ,L01KTxAANW#Li63d$ \"e]13XPZXX]#lMehr1#F1# @L)`^r7e])!˲ҩh~u Ulr^ EgwIUE-zRVT!ˠ*p*J-Z唵#c,b#P |^ET?E]V#M!r+g +*YcD9MG.Z R Tm2]c&Z+T@ Zژ^휙\Rd.tY]299֋!!FkA/?yL$P*H+:bf P~7 M&,/\vϯvz7`R<)YY !)C6񄗳-mm-zERg4 ,Q)FU֪1:љ&஭9u-ҥdXP^ CM۵tjSn֞b!8:IK )B1BM$xQrpmir6<—-|$&8RHM +K>x&E( g 1ⳭRT ҲuqX3rNXsՎg\}έk]iEZ' *9\]UYQ p _`R,*UpdŔ.>z9Mqq>_/1/V?xHȄDjqJiՠ%u'k23k.Q2*Q9gQ!si=6k$:zDS9ֈaj&6//$: Ol\(d##NG PUЍ;cCrL۝ЬOWʐtEg6fmRC'LaP\FyAԯSm*0*}@"?w]mAW+"=֪\gK;`i68}Npmz`|̷x>}a!Q2=ꤲJYPw渵vX1X*6qarx;[#Fr_)qaR90Q;ڍHq2~MArRj(68// %А)pX $Bg),'PXHW_p\ W[7M[={*TT˱E)T01YDuG.lYT *ft쌞Grcn3yE9,h'\?dsm6E&#I$H:~UcBP)&*c=0p"5N( y%:#d*C"+0BTf1eoS#2"yf2e<{~ŃēG`,dlS-1.2Ɩ@–,²mYlpnSEUQԩ39##2=OppԽݾ4>{ȵر4-T U(Z(ڢI|-L%,ϰ5J1C a"j(IN1Iz鮅|[3 ҬtGT&SVG,'bZ$$9(@8B1͈QXb!qɬ|r22Vq5b>As±MTB&G6R@ ͂cc^9~yceQ}A$Q 9;+QFX=bsUkoXD OA5 kNڧU/G@:UOu m_pFI9_b#/3+L c¸ eiNEsCľαW} 98c{pV2RQ*,UN\(i夑J)mpҫx )3}A?rs~iRYDrK cIR [%V'؇1΍!R r$! *H:^ePmTVnFm8z86>fȽ@JdPd2I$6,P =ޔ1d*a ]K1K1cVb@%v 3k2Jd I/0+O>ЙN g63ʙeWYg-gD1 EM@7*ŔNqtn|[ǔ#D0Kd4&:i'vmLsrU 2e8we $q~'~|4o}Mqw| bO"U =ꌩfTK3j֔x+9En/rFZX2wiIvcweQQ{.D% U8Y_%&"en3jU\"ɯ̮KFL iY'͂(U&AifK)K˧,> H[9+kx 4Ƃe1G)(3YfuUN4YE[抇'HHHXNڥVY,1eN!L**x拈g YM`[Wm cҞhLFw9.%.OQp;bJ{uيvHɟ1*|W#4-rK5FYQZcՉd)"^Ný8܌i,}RA*4* xeOh}VˇHv5GHrNͦLUmnzu Q0՘N4 l6~'6..q2db:!a08*:]zVg׈mjkL=cVC|B2 eYiGScit",fy#4 ["$NW-M 5gkfA NxxVo'hrđXA%8x3)-IdP9Α(ǝz,M(fuH3Xy(] hRF!@UR$P$ MAH2B0 d |L)iC:ab IDAT1G%$>G|B+\p\Z&3e2Uʔge*ԩ2k K F:RU(^H@N L'HrTG0PsŲiS 0m{qNmX(H>[3F3ъJᑈ>y2Hi-gnqح.2ũ2-v.uV;b99e%&˜anf ƹ%j(r05؈N6ADDy ?+;LELhc, sZ@nT bI`,"&'.H0jɔ*S*L|'Gsw{hqqO&?Tj@} {lhVF.#];eΒôP4M2hdܪqv)[LB  6qы5#^Pm⊊"p+)J@gv`2=0T[1 1e8 ##MXuW/zxk6:ll$(iN9[?[TX䫌:3l^a:}@Er{Y Pd}6{lب"孥E )T)C+243H1v먶 nzΧlp~hgП,o0[)1_u5J*Ό=,( ȫUyLK`iFb](nYAI/_{NٜRsGe$%)Nu;YIwJMQri0)FyJxrĞPj9-i@W;A33 ;ƨce})kn8 \,q+|^e($Lr[!r n[%%M sw.[cOrJjNY~!ս)=}zE,=x/bm(ĩ+m筳m0*qFX"ܕ9wqhۧĒB+pAXAʡY@v2m9^G@Ccc!`:~RI)Rd,) !KB^(B& "D. 5V#eę2٩Nj7\3w۬pJv@jhM^=b}; J"y ᧈRN 2ORP"Tbϼז3O5ܜ.㤎If&y-o HiJji*QϦʜ.}aRH* םֶ-p 6XqL1S.iXbG2SS([=^㊵Pr{o,8 NiaUX*zіOi)}#6,U`A)3V# 態WH|e7WNr@ *ɢ,$ PR "ӈ'6T)钦*]7_ݽeqx'v$GGG\p]^?q^WÛf4MQON@/2C6!<\{ "* ҠK//"~'.祗Dc͑#DRB`jE6SgԖԔ)v9XP"$_1VcO&vY_JsOFȦ-a%!5G|*ϷZ%,T?&&#,/HKľIpN|d"$qG¬+s3 qWg aw.saKaV,\rHѕdђ0ԫ.',sFbƅ<2*}0_S2`^0(}4ŀԧŀboɾ~X',CWrxVzEN%x$CFOkf{%XRDAR,yNX9fkGveo59*w9l,sY#HьWy q# 3)H%u+1F Q4wN̕G 2 If>K7ٔv'eQI\cU0 Ȗ4!ڄF}H$N0EА") 0 ȑ\PeJ@E~RP%mQ:Q}΂n2YVGdkyC"s$ U&I(@KLLD9'D%R,Өj:S"{!hHhHe8A,4>sQE*(r"(x@Z%Gh ("k$uҧ)LNYҜS[`)Pf!KcX!!Ϣ =: K5JP8UN6<,"6= "mꀺ9$-TNYBd4ICVa-y(K2)!POYxP}F*%rJ',N(3$UV0uά:jvx>J({spμp6sDRo yuDt"NK|mvZ#!{0E#jшxD:m8ȠH9 #Cp -DD^eG"J HD\&]dsRq޺I8F2F#ErZ!v9rER24 `9EK͗U\6L -ug9t622IdB$4@QR|\#KTF~ѤhZ' -ed\qKm"ӤhȨPkLp)&6Bu) AnXR)3QOaLc.o-a}!؏;L~8 Fa+W8n(3\mQs G }@#[JfPii^%)r`To0$FW-操UI26H"Q@|>2G3ַۿ}|h裏w|#=cν{˖eoU?O/~(?}|3OC}[󬭭}e{˯ykx'^x'_CZ?u=A@AJ E%t2M'teZ#ob{6n!l aJ$" MB$T,b42#9_:R@^yMv1˵V8F#E%E#C%%l<|3Cq NåK; &#FbkKKhK5rn9>U]eEzC_,q.K:ԋmO=9N͝bP =ke)ݲȫ*Ջ3kY* )cbRiDDE#a@,3WPH.sR38i@ 1ި0nf H.e!L -3b fJU=ev{R|q9(qe6МNk%K0f)`k>T,a7Pe26ٞݡe2[a^/1YT4FzFɘ&eѫvkC6ΐzF 1h2Ʉor>E-Mf*f]COGZ1dT m턹p`6\X,qd{Ld{Ǖ?7sq pf,ƌSԬ@eL'9"Tє5Su2p3Q h-X X f3pBݍiVN1Eeѣ[&7 _ 9ҙ?8̙ĕg-N&E&)HO>${Xje`ͻ j0 RGI ?(<VNq*/?oXS@Tv:& ϪϚO:EJМ}5!* ˜t9v5Ґ[mnNss<]j[)]֧6-m,(r$/| ӟ4/^|}Cض-eYv%O>^%kss%qʕk-};w^rv_;S{8}}ǿaᗲY ?HPH$T-!d+#._e~_Bh*Q2*S:ȫ*kȫZi X: R )+S9e=XOdn\DAX76#fIN U#S5$V,b"EOjaRJ/.;w-5pad7MFQ+wh\9RW0ceKpT": $6绸zmn,L0#J0& f?'dmLdLVq ctE9DcĺYYz<,=s<mY':ىF~[Y tZ=~y=z&P (eĆpJb)iY{lZ{ȦE V%$h6W ASuRg-4͎87C s%G)JXD! uin08k KvM^b:fcx7]\cuȕ:PIUxuI{Sk89 789 P%Bm4I`JSQ!EQ "K cڢJB!Tg(UɅ,1*T)^Uz+5^?4sBjJ>Oǘ)ffR<ȌQTDM" Np+|9!^H)ւl7"^䊸Ƌ^dP)T*c6brK-.7%`N928Vh-PSH|S ):)vݧ=ŕ=L%BS$C@j*VaB(/>E!3ü>Qxi 6A^M2!  D[R><>ReX+!V0UUN.dR^sͫ6ak×~grF.]xUѸ<ȘٲBЄPg'yky׾~؏r(/--/9Ϭ.;;;R~ܹs[F߷ApO×5:K}}ǷS0MR;g5`2gd5qa0E]aFvKMɤvH]0])F;cxAXqԡڜSCjrFY-D*SdV4_2DKıFđFNh䒌lDn*dJL*I; &wsṣ́=zK\3ɚ*4@q2挮}RM.1j!kfq6o Xvm؎$oibbd!F`wΫL^9N]5Ifk(5W4HnB)'#:FBm`g:aQ-[K laЕ7s֢}^B6KʀPKH* HXX>>}u@iܹs|B3b77??`cck׮O|߲__WW><Yc??Ff^Vx|u?0y[x>{Yԫ$e"KK9 Vֶ$mL4Q=pOn'6c͐MsBPPF4#F-FAqbjȺ랲%b;Mg|B`جq,wI}4ЙqiRaWg*D"HVn!eS))5iL#`SW IDATe G FMQ($!tCrK5Nkm$ ]{ z^iM*44M878L Ci{V-zNwMl*6>`y蘆80iV[U]"LF,IK $hC$[¬GQ(lLUI YYӏYuYYX>>9a)_,j. X4$̡_8]ZP( f2%V%n3$lꌦ2d8,M1∶?E'cZ'j%Z0ɪڛqαnRJ=6]w”9mr8ȩ11 ٿϪ(ĸ1ic 5 7i{CMe\aOJ N.i@䒂/9#$Tr 4DJH]=#ȫDɮQPh,DP@ ,34u*ؤ=VQ\y)μOJEϭ,'[FL*F,L5D(1~E7Z"V N|s|Ά8bEaV`)^~d QQƼxř&{G#mJP*STUK#։Sд1#VobRf)eyOjv_Mf |åw%]H@Wuu 9Ÿ8CkSf)VȤ2iVL+qG90Qʈ2.0"W4 P)ekrD4jcغF sC%)i1}($7"^Ry'£>OO6>O7u2g>{7~5y \|w߶=mo㡇B=<~g~%0oy_yL^>я7n;>O;oo++c=oaCBR.e< nq:tWiJ"0mu2w[e1=͕݅!FkDɜAkgeDq{\d[mnʼneG$mM4&6B y@r^8o "9~Ui-|LşArhg=L"äZѦ.Mh(CC=dXstFm}~/Q =4rvNѪ)S*ܒ/}<=e(ϓ2a8mr6/<_+ ""-b0"8OhJ}Sj P %qCcSƄɝ \abT jecNE[6尴ΰڤ4ؘӼj4>V`[Hd@wYK,ғ s4FiTNIPZ4bbPk|sMx5 -N/3ZnuA"lat(gugsC %ՌaDo .cc3m)kzC6dmAƴMuM~_R~??//}ز,?C}$Io0TU+_ _W~Ա^}K_e/sx _ǏQ0 /Я}~>O+)'h% $l2L,)'ִ@*2`%asYNv5v PUWA!-nK9iS Ez\dEɖҢA@BRY  rQ#9d2u(h2BЄlPvb3dS fHH0DAF/[!mkvw3In+,6}EGlE֌Y1gU{Km>NB9'-egQ6%uj.̀E6eR U#S\U(uog[IUe*C .Sʈ]! @ /9q=у-(@I3R|\lY}#u$8R\G-oUg걒܉#qb&GRZ] + "7JLS5.)1W1Z$>IfR-dJM!-u6S2"Krr٠%ZU!=u@[2RgZf(^I@)JI%3*Vi2b+Ђ -r,HDBi}lJb)f)f( RSӔ6+cM}, `ϦL% ҥMѐq45u1A.*B'm,aWl.dR-[0k`Җz1P bCX'ǠA%mizi'A$M MK٪rŌO͗Xjq<¨2w-EݐO z%N^[×L[FQ;㰝rNt&Dyd **)Д+wFFR伦Z0udGGU JC)R -U6.A6.!W R % :gƴ9C-͆XU6XzLUkEX{dUNmTH ,TH___)/~ܿ!=|" ?p!%Q:@NbS#žCu .A%eRhdVCF.z4dJTzdRrni rWv>RXjKv)[5`4h 4A Ye#4LҬQ0v" AN/+-c#F(eWv:"+5(PYJKȄLF~L- .9a H* r=Y18\@&J[zҖv8W pI劭[&gl&)} b)C+ˬ>GT;NKIJuىJR uPߪh1z7尻Ĺ6R.eV^o_~qsȫ-6b 9ϸ)OIsp- K}A-Jn}|B99i*:8f /FFi ĊS !Ԛ5vQ$l}D.E%5H NNz^8dZUŤ䨸e0c!b^!T!rαvz?K:bMqv1^oݗ݋o+PI pc.)$w` g]_"B(5PHbmtiFd$LcoapD1i}_& &-ӖQImr yF JNk -Ar.cZU+PHx{Q: swVNBE %OW }R(.)vf}FϾ憎BhMDE}nE/rWy_VBlٛ voc(~<6*uP/7*fӶL[[o8E&jB 9DRD @Jz+Ƃhm)]P6ۊ}fav4\3iTժG-ϕGP{L;[&억avYe-uYS'Y\w':@sʩN1)vG<|Yzʼnw7uIm"sF(GSyR"Üqc<#]P$n #qxF"Xp|3,##l=BfS;fSfwЇPdjUƔ2\'Z՜6t-OԊۆ8Y LCR[b.G\:g\PMh*&4NJĬBR[~5X0c)–"l)+v++.Wk@Q NZHZ{ǘ<E+/9:GWt~WHdJ.Bj%,RmKRZ(TB*6p7&lTh$ o+IT/,Q [% &4m q L?zpĥM̸8;q~NPvTBa|@:?0qKъc35?{k^;giRa(^E"[DC,;DF$Xm؄]ltm"wb'"}EV*4@3AAqaP:Rke0x澦koХM* ɖNA'ZE`ţ7<^1dMxuxfj/Q{ՖK o JYv֩K+opI1ڜ(Jٕ]ɈݫUD}UR'űFq2f<(~,G%7޿c/-Wx-~V?f3:R[цf>Cx!z"mbVh}BYyur,ib+,YP}[٪B ̤bX}Or5ҮAJ,;-@E l-t K=i^(KH/t!5ʧnjr%P InG ҠA}_2\O !i`qAh@*QaQP*Kiʴ6xy(S\}n#^rcU1-(W/oTF5(??]=۱:ρ eQEq[MN61Im(%h: ka $:bS#\+±"\-)"VUy;b#j[F( e\jO6 Q4DS&Y|{W[;u]{I{!*j&̻vEzx|b_LE֚.)eQ*UGEXˍİUDK;!Ia39~|œ轵fޒfPA:]-=Q%e8oqɌã[FGSvuHs`xR]ҳ6q7o;Ĺ.6CHƎ?grhI@g)q&~Pud:O8V2(*[CAKA46>jZ ƽ[r]Dzn"G3sn$f-q1Z\c9 P6 `JRSJ*R.EbuBoFF IDATmbfJԒ6lH51XMz@L_r|~|CX(C|K/p۰cn1kSKI:6XGFU;J+3d4UeZCeZ_FSt#CS24)!%!.!7Eu@PI\9Rkg3:hwşljgD>V5тނрR.h-cYPsT[l8ʦ)G;I(rR(| OMIi`glkǦ#4BA-i$TJ|D,l4RU0 Gz:ى7|¯6AfSOj[jMFUʏc2Xg!eUZI4ɯM&'< $!u E6$U`O݋h"ڐi&2!D˳.c&9$Ml.zQm΁7w U .SZI:׌9f_|emhz0CݔFD^ʼntz!2[ Y>ztVb:3߳O2>.gn*v{$B<,+]e%]0t@RAՍ-AJVH0{/f]: [qtrKs\IS*HJJw.G 2:O>7 |$#T9!'G)GєiĹ H^݋?Y4xzě%'o8 svKn4ۮ1EBI.4ZLҏepן%r` b~ ь5w]axDF"Ó y+rn1e2Yn"z( %X!d߹yz#Ӊ[N >3D'ѡItbuc̭sD8[6h;h7h;eb9Ip~qoHtzZkm93֜(%=-1}U/*n܎F!o6Oy={ƫs^5յl]۳1o\r]+?*e}-O?bq'|{|:w/ۼgXk]"A ߦ<fIlaUk;yLd8\J8"òCl#RCl+,?es:`WIR;l>Vb3gOѴEP1Ҧk,)AD:++Wmf[UPi2gYj\ϕuy ξW& ,1ԌJUX]bLrtBR3g%B:h$r 1fKʌFQ0I00I1Xgz/,f!p|1fv}HG."p{T)L(ˊxO??;|ӜO᧘#Cte]Xo/;F9n'`.WYҷ/% < .x\wO ѰVzПS9j7XP#͜>xq{}D贊{1xb~ĉzMpv~B8\7'.7)Yuto?rz|ZxJ(L峠?*)AĉiVbv%%rzK'rIf;ꇜW5漠2\J).m6n.]UqA/b ^b6r[S2ic+'Yax9RXa nqOC0ĵC,%nbvioWLTIxQ8cC&eLe*!ňMۡ2 tgR!cC@p'Mx)RĹ[uL°㰺Wp*H}0r =GjI& e5|A5e2oe߰j,t9jp6g2=g839 g9f&s˃g|źp - ,8`K}nȢEARZR$Mj[ 3K(s,NxsTyPN{mv \(]ѽ6hIxK~DA3[gDۅ@ \3F4U+X<`y1mN iɉ}ɱ}ʼnvԴLyLGu(a䣨8!` &C-/XeU mݜAM$BdJC"%(}'H}'l",$u RTQAV=X|k$A3 sbX=Gcв/kĪI,BeR ߭6-W5C{.9}Tlʠxc JQ_y)9 sţ,E 8=R=92=eICuڅ!E'*sMNu~ltsSe2? :-Si~':wOJwtd>DA+ ;n{'5sedeXjq$dT,3g.ja`-l0f%fogH F>o?P2hA/l:-RAtZj]&kLfKU^gMڤ:F 5z5OG#>]COwO*}75Gof<}}Ov D-u 'X]>~bΙt qD8C"٤kk:}(9C>['H&]`Ahz}VR:߆tf!u@HԙLԙv3,}<(jd8j&rjY%PZJqpW#^@䞵?O#K$RsL%Pr %D{$WxY*=3^(O dX2Hsz'qW Mퟆ.GK3?L8֜gzvV(&X$co6[:͆1Sox$HzTĆcSby/~xԞW4=㬹y{Y -B#.Oxuiϩs9'ǝ+)j\K8I5TF1rjjUAkBenF̸A t@*ZmzS!h)}1Uv暳+snCn117rWqN :M$+v2*^m[Vcy{@ #8;[FocY0f!t=P ) i4|w+SCgK&5\ 0O]M:c+F;F;}rr_ +V<3BwQhdz>;<_-PF:CVilya6¦׈4w|_~|_{??3 _+ʲg~g?HӔ__//q]__K_߳k~7??&"/|k_uzeYٟUU[[X.@E<GNi🞳i<ؼ\_hꪮ3<9g<Y}{~RD;Nk-UCI*)cbI# Hs *QQ(k:d@ 0?ퟲ3Uvǹhv<$$Bxɢ 9g#=Yx8ąA5aS,4B-Lqf>٩F:Ԩcj搚9f YGtn1G3+ƣI5*r MBlR&Ũ$VZJ+ "K9"-+P,dD")ydfY@%F ۓ}$@Z,ƪ!X;C`@aʔK:I )t>ue$e,4W8λ$E;x&1v:L%t[+9IVʃ}wD#i }Ј2?Q XK H>jb>z#+fkJcN&a!i9%yx|bPfҜF1d8aOIPN0e*Wa,C$$,VH aND9b@̴S У}S˦lGhd3jiB&dt9VT mL Xpw8:; F-aWV^7oefcT%{GHKCB3,v1g<v2}"(1WRgYh< (Cp3&%T.PCpRJ T@2L]"tY.'t7I5\KP14&)zz&8,ЉJ)6& )=oeBː T!JuTd*SMlЯ1P.}}o^я~ᵯ}-|#W??WWqSO=O<__[[s7/;>/3P._wؿgqv}s=GQ O=)T/}K|+_h|Gcz{)Eyg[kr/`_7Sk2tBπX!Ik!zbVM+$h z-Ž|#C%A"ajnfi V`!Dd {L';qNs鳾<]!ԕueDYY 4MдUFZB0Ɇ*-,+d:.l*tRLRQXlŲf30+wcS0 GJ+!r: b "̽k/Klexʒ3VP-P e|4@84Sm5T1 (ddxv҈Uq(G_Tf3I=R M+)b\ =Z/=hL}4m;>Ҝ>38Gw}VV;*bG2\@bF<.slpmrm5!PB3F_ZcA) )Wo5֎X?81@$I/uKi< :$P@QˑCV6*ĝy5ȪCɞ| yys]4%K}D2*m.07Ct$)HV*̤*}Gr$6>?2G(n PkG\7YY.Kez\f:r.3,d6"V>oET)vqBEb#nu>%rqد4ZS:怚"`w9v;xCBvQA0P)bXe'UWȝӋp%BMM0DBtܦ7^cvnȢ@.\ʭ)ruBYbef!,\/1\%Ӹ~M!i` yQiV@5g11c ?_' ԧO<~?mo{?M>/|(G?O~}slll~{[ O|gss[~yE>w2??i6|c3aaÔI0RVI] IpI_uEq ]Zpהnq(%KQlUK>oHjZK7#GU,HPAĠJ```WV^X8q#0˪ƛ5lf@d IDATqNͦ8`-y@0pW%=qq8iowBD#G4sD3iITg9)LtIHe٥tyAKu G(/YLUycyt{3Jv+ RNCb}ʃ9QHMеϴ( k3w+~&אcB6G<>FئFc1ڮ2ޮ*VS&i7y!xkCǞ4Fc$)Gs$)T.-91ivBͨ#CbWK蚄ޑH Pd,5M'6z{{{2~1_pxy56u8Ε[/pӻJ2R$gRW(K+ܳt3NXO8ߦPs;SjXQ;y۷.qt)uhY4nE, N0;!vXb~+ :@CѲ s#ZcKsl-z8xd_%9PHTӂP1t2ȻTDV ]2#FJ ȡ1,CӤG,8a  d{ OcxՕQz <WlKE3y9[^&vvv^v#xk'''\p};wz\)nenL^e38Y᧕C# \"HdNZJAFLCʌ|A)]f 4BJ3䞥 "M䲄jƠ bDdc<22a Ե3PmVPlleB'4H': IZIK&0L RQK*T Bdx: *3]8v |pb2.ꄱLt=$AIr,L%tT#Nu&y2L Bf i^ޥtMfrH2r%îzkc*1qB[Ѳz*}G-"4&F3ڋJD:R}2B%*7Gf K̂p;Xb1XrT &M6JMvf2vڣ16MU~`=) rBP@R!)j`. ?VK)^/J0,S.'yptX%֤cIE"*TRHeTRHd"ȑ„܅*ț̒K  9ϑDdȳ4@>MG%dTБz.}l1Ʀ̍ =\q* RpȐъ)ѳ%K dLkx5zɥUH=Ol]bU!u&T 6=Cc &Aj%C_I J$A.I.',$qO| DE9fPh3 Nu9kGuTS͌0ƢJ :Aj3Խ ! ) IH Ee!&P-%b_Z,.RBMSҔѩ( d5%2" DV/ct/ I j`*fPSCrZ sNJ!$AgH~؞fr'(i#2^w7@)V&, Icˀ&bHqED Juy]tB" -dpI2#i%//eDB_od(iXZ@<'YR\H2*z2鱂Xz@>vLUb26{?|q]rm8</{}駟f}};:_Ν;\|wr_V(Eh4鼬593V| 2"qYc:$ fp^0ˊ!RX,4&Ei"Rj1fӾY )4 JG0ٯ+*QvRL#E{8/p%=R`tdxdt`x$ fY"|2Ye|!SV1Qt o*uJUDx*.(08YxNDsDR7ԍucHccײzMN&CndkCJ[L*fQaXR=ER$-E"Ec|Dt ;D 8dxT)&յSMK J7-xlUq~rs]dD*!3ԚK/,9DJ*DJ9 =1f?d-9yzXS qp 658]Y @,Kd 58kt.",,#QX6 a!8q"B^dHzN IMbn85V RCA!accsu@nGbh# zYU/P2-eURRA:1emfl<7\\tPdenj -&y@6Fxir\Z礴AhK5M\gJK:ͬ:b"4+zc*c*Ƅ>FEn(10ew i%Fz#m*ʌfw@=G)%yšt%DɚnW/; 6CAb,d1/kؑGliԭ4cemPiOyu^ /GuV7(S68 QE#*g /"ozӛ=~:g>i~O~J%; ɟ EQַ7 UUnܸիW﷽U0d2o}뷽&nLox_x'{qgd/,km$ *=N2%p"fJb!N@ &M0@RI8N{eg.v =z]㊇,i3|FWZhm-n/YXpyqOy: B+,V5ƉF|ჼ\ųmd3FQcd)pF1s*Ճ)kc#^i&l)AUʐK\^k䣔JnjR($$±MmvmJ\%H=p5H]Ee 7u1a"65AjILM}yX%tL9%w#JG>:X A)ut=T+*F)ҔrcAy@SV12ThS]p8GLSQ3Fv2: 1ʄl~//r{t;Tc6Gw:esˤTJ3vrp)sY-CJVO AyyED)]<$rYY%$7M2 Ka79ҷm*))k#j!Bbʚ8 ~Eo,lRvH p7aWz#v5N]*Sc%\=,/ RLq]= ߪ0|Ν w:ef2Kޖ$u ji6uVO__7Mlmm.>w}c^ʻoyz'|{(x{~xY.Yr ?Fvy;g?|1o~k򶷽eqgqߊY3.X.R%'Jure+4 CԦ>eC*&LNR(oLQ#j!;W dS r*z;ǸӘV:e==*ząF,Cf*y JvhH7LDC_D.(g\j`{ksĎ,/ӟϷ}iU9HSY(7g)\dj)PHsrK)7ͻ -HAzyzVTo0 :d6K6jd ceK5P1eFyXSq5sG}SLL%9ҪdE9,g'-Q<ƹ/Q Fт'7Y,]4WיrIMalF~Zp>LЊO)E,#AjԬ1esc.ѵIuS"`8xyXRW] 2: Rs&{$#!g%'efe]bz7qD®nqADUe|̦u#%ܑ[WPv4S}/* &Yhfp803.7z!|*'[/sv^R5G6W4-*]! U$&#"a8Z :I EDm1ÎBj%8ʀ -19edYeH 4Sb +HqR yc3kW[ɀt+MvzFݪѰ 8IJtFҝǾ]^:^׽leggoܸ'>ے$ws|cck߷ W|w|Y۫_jy晗-oJۍ0 >OO}~ۓO>Ǟqg|3UOO;wh49q<{൯}-o~󛑤oQ*wDe䎆v׀=KLC5RJmDA^݂BNqZZ aM i0]\g#yQG%nj)ΰ_%äoangSXb͖ 8\GOcd1iQUU O1"kH6~=7~J,"/Q4j|~EΈ3z:E:oIRX٣mq*7QRVg""L|,|)w4S#y@;s%7WZ6Z%g=`Cg+\˖{S¸^cdVU<,wbZT&J֜6^@QR*r8ҟRt>Bػ̫0+L Fn:.3,1#L%T#Ε_bERlk%eOqXD%yNg6AndDnhJL+PRFl&:8$j8-FV ߰Xayb*:QZj:I=4)B3-f (Ȼ [CjDk蚏."@/BLg/ݡaEGwܧ=4NX҈J:ajKֵ}-*[ [ƽ)IJx:"-O1q yU %fe1r"RPMDhjfD9xFUtO``o},q3tQqbS6FDhi PUې:jҶ?*7McHYTMnxmFL+{ʳYy y&I2&[EY^*p_[s6Nw2v)I$IP+5BX($exmKH6Lpbq[4S=a͞fdG%(Z$hnaR~īx r\2. 2&HyZ QV\ϐo2<=!c|I~ݥ38IkK_oo??a??%s~׿_uW+L*w ԯYpK s"! @&G2rjAх> 40楻W՘3VEr2z6}El3Iˌ&,k X4ϛ̗ /e:PYZ֏k $C a43lȬ[I]NݡE^ѡ͌ Wh'}6C6#֣Cŝyn/p0&Ruv!kc: e( $H#6m_AdC9d]>$P-nxIsik >8gdg_dϹ@ryx01cRH\*VxS$S }eA&A#GB"G^U " =b0/X^| |1sm{hvdxIxM]S1l6t=vR)c7T*'[:2ǭ%Z+&afL*Sƴ1IjbJYұRHRB6yQD?p8^grB +ЅBW]Uº෨ IDATF(p's6C6^ƳYDdB!UUqe$ʌ$Wf8p]. 1ֹݽL38Ŝ6=yb$}}7_~MBgL %͠ :CNf R!H|O)JenFVRI)BF3x"W^}~~e+83 ~U~k^k^ߤRYES/Ї^Qq{BPjP`H ? \v/!9'5nt{ [_b{G.|Jku@ sQ7k,k QV 0ϹY=LYPi.X8enɐDIb4b4ERh0ʄ;yptTb>0Txpy7WXcGppC>j7Wѓ=NГYDWu/Ec/f\)jPks+M7_x8zX$KRa.Wy@U&!* U 2*79T6н7shM(74F*#ȭl xB Sn?xUg68npDT ѕ k*&K pDISj,q޷ɒqm@t6C܌/rst9C<~r!PSꌊ9f[aqH !^f&{QޤnACg`6ik5E^c|W_}|pǬqBth%g}g' wOHdDn (~c .2yyӔb &vz{? MHdx2zf -׸Z@.qDQH0±h3^VWO^P[fa9K1MNirJ!E.гM ER U, E QE,g[Mn?*38?y&O?4O=Q裏ַuں/{QofoCi f1egoy9y:5ڀ y0@Y'A`aɖ-eaɀ! \ %nlU3sĞq|p`]IvF掕{G}D(ķL6M-@Q>& b={fG17.pAoS Y56he2oS|I19ktC a6mdCәڙѴU$s٭ 'P Sb tLI |ط;(aҭ*:^eCB]ZgfxySgqQ5*ѧ9֣Hъ(Pr]mCD"PÿAqJ|Ya6.gN5f6ɡԯ/hηk)uZx~΋΂FoNC]v}콐`mBU2΋=.=UC 2Y31//ָm#-g{cT>XtmZ %=.cT=g6yMauVYROVԓ%ql ]ŷ…ˣƌΌ?hFsZ3if規iB&ādUYv+Ttv.Z@L<q3vF=r"~›WѴHZܜ>pQ4M3cSxMCXiܛӜ \T3q$b+qIm\j\Fyrv ;дLM*©6 B6YRgiUDJ)JL NfE׵Ӥn ,C1H4 4mB&T[UDxlhdK LȤ2 8,Ƹ':hPڴ%]Pw.k V<Չ M^P*|NVHc+%+PU8ZnnkJ#suJ] pzzCϿd_T {HWr4@Q?X|_o(cb~JfvƎ6d7>`ZcۋmBSl +iKJ]Rj)iALk27zUe}Qڤ7{e_%7kἉED4)eˠlLj=תH+VLΔfFӜR@8Ί"hϦW/pﳢJXSeEeƺQCm35* O&9^++W 8i4|sCJ__|O}S=DeTck0UxRvL֭*B3NJtY>87`xd[s[CzN+lI*,:ex^]BGJj|8j8Z,MUdiRWpr۹Sw)(U{6vunA۾LhKYGuV:j?GP7556ԍ5e1=;L6#.UFKI0d}mW¶L5نBHzNj[|=xѢϒFA9nvS. sUQ:xOY,x{mŵ"*͐ .yʿm$EQY"$YvD:QFp6a!szXbbr^}D1¬y iQ?q6v6C cZұn%kc2bx).Ȗm` *-bmyĖ|#~o4d=.*S|%Qɂ}GC,*mn7:3MkJ*MȢgeUQ TUhhiuKԹĴ9 3T7 qo"ma 5{9xZX-%Ye.E'6rWIb2VQ aKD9ЯLt<%D,lpIa8c` 3h)fJc&iژ[ P>j(v5<ë~ ]xƆg(*jFZ3DB۸ k}:[c:嘎3ӛqqrrb^E-ROdNԒ? 2Ú @( "U/N}u6xqYR57d=KKO/%$ɚ gR'BC%aRa젬 ("Zr#:ItE=cYDEkjzgF&Uꘞ2bcPB3t&'d#j%zhJs̬k*YDT6!j ueJTEM%DIhZI$I\j='p18ƘqV9Z`S`Za}Qg-.8gT 72~NXd)'Y$(3MyVT }wȁ8sã":t oUco2u XyfK5N%T嚶:}rXa$řFƪ%۵OD @zP8Bi)b+t$)@s CsvfMTg3v(jSLJ2O'혤Mn(fj*nbEyfILm0+n>Jvs҆Fi.R"VqT*hZFĺIm$?ZRM 61N}ldu = OPaQaiѐ2TS4Y dI)P}`JTVƙt3ڳm8J+G)o{):QD=Z |p1ʂj]iP 4e4.g.sA \>bX1H4nn %”(,BfP5 .Ӳ:XT^NѴt+K-*PEۉ:҆kI>׈68@Wst%GW HD g{mDvsk׳1 lnfhfnd\h}.zS#ja$(uZВ'M!lMSkD=AĊK֨[k<ͧiis:bB1*tYueUYGPe#"TQf:k_g>ә_8^%^ŴSZKք0\e2鑔&-++|ׄ߻.>Os?syΛ' Jg|?Ό2LC仰P):"YD Yߢ쫨VZ(uĩ0r[̂S {.~ƿ:6R_sc;'.f;\,u¥LVv R`dK x۳/NO88' eP8T߭q+]TZkܦS EDXy2P]h\O|0Gtt:]ɬ^v \hBNCiHEIN+dF8" KyoURhQ*턆g8pՌrEɤfbXz ]?;`vDc6GeD͸e2jn߆,U$*&M۝VL PgAGBSz^Af^{et1#]&4BwUBeYZ'%y*Ʌ}th2^if/RprУXпgpA΂ΜFsA9grZ6.~V!__}7M/Neخ2uW\q|ׄa}3UUm/^IiθwčZ )l*y`Pt3x ij"-[|CU8ԉ jZU%6s okdF!-g s^5:>֧uo,@'uTaUȄ΅6ksW=x;xx,l*n.Rsԧs5|c+e20d<˫<3k4чRr厠؁YI4EUKJEAsUH+NĺA\dbn{**@Q)Q"j+z1뚇g؝ўL}QAzh2[u0 Hmr>фM:nro&iSg`^0.9eDch0 2i.0:KL0ƢkLW/.j+팎3i5.>G!uCfn3yнד[~DS2jΆfoZs:b1osr=Em8W]U2py2gb^'m9%x )* GܽY<^s !7T`Ha(NlyC!*)|MKJD+Y"FRb9{n ѩ_rZ9{ Rtb lQdD6\LV8Ew%\|j*i .,(CHG)G>bwyE05Z| HZ ?3^sgjJ=T*կ}?+jB%Bd4BY(QDPdD%c x4qM90ωjZ5gfbdB#HKRS1)ˌZϩDT&ᩅ5ߧ:PiNȥ=\` DZL Hoa,mlD 6 dBŝSCb)HLdNJ$!(,H285Y%)ٜs8{Ht j8ZwC5PA%dZ0A aRj>GuJ=JC`h Q{*eR=e[KdKD %+Q%b#>$Zg5VxDMZJ!J NPnhAؔF`y̜ (]\[ftVztP3KFFqDՑFj@۶UA"r+R KtD*pK 0JG"P Q KYҗ#v Z\):b)YF .‚=pu͒ U^"aV5D- m%FrnUY=5̨\ӹ9wXĒh]M^?Aamo{Ç_/e?#?oUO}S|cc8rm>OqttG>xg?c^xg}w|3'c~ᐲ,Vo}ܽ{~w! y~1}}:οunIKK9W\q we*6j.kUjlK ؆QD-Njft7GRk*.QBWqʐ rIdL/ īx4lTGPiBӂma9Zj}溇v#8L:jartkVԊ |*>UfFFJɲ8.d0Uk8'N,&IɤC,M=o_,,gM`D/ыG;ꈲSDg ]0Xt co56G=Ћ&$T>mSȭ|aJahn4 -fB:%DلpBc8T){&$V%$tN}|Ѝ&t ^5ydQ v3v3S4tR;s6RE Fhm]RsA!5rD_sQ6Tw(jN4Qӄv~{`6#L dR2p? ț& Gxh'& VLWOggi?A} =VO|O$???yZ_E/я~/˼j5~__}%nfwOüFJ/h~;1K[L&R W\_i29Yzۄt|Tj!N혞3e>0Y^<˼\N#j~/4Em[.%Y R899V.d:<%MZ-}ENmfwvμ`Vi0 VFR AVDO[.sk/:g`(tt_],bv'#z)QgyU}ϱyZS/ќLqڠ`As9Y'Nh1Z@^0@JT@i- szspw_#c81F925/G%/Ƀ!7x`"uhsb'68b+$E sĺ$1\;F|JXu%1$XEJ0ݨaAU+KXNB9,5յ*ζ/slaibV&&2W(A5!bтJPn"wJL;d7?V?`vy4?(:Ojo oJnhS:%=D4og6Xaqs"G Ya e?d٨Tuۧq_n.xYy3f+/[Jէ-x&ts|-~1dzCL%x~fƣ䐣IQ'Bv]вjV+.ֻ]zZglfDśfn_Wm|ڵkoӟ4V =|~|}/Ї~ۿ}[[o~?D`}ض9񘃃>[_:??t:yW\_+@!g.˳:"2@q얘$H!@iJ)S<r[yPjFRאIԤ@-rM:Q :L1~BJtoُ1\&FO5]H9lTi9Uc/WT|.`H1/ g{ H.yͩVi_Tކ\DŽjYabYT䖊!b !M,t6tJJL)15˱"P)4Q!46xĆIfĞ=/B,$beq`(wrɡRT4t7Ê*O=_ Kaz3%]ex{IXHX$I(G)EzF3^ckqUmMMHb$)% J^RCg4SRS+,$0t֘6zFN)&+,Ba&SE*4ʰ@^(#{$AJH4A^yq 3O :+.*%VAT$l &Q]KɅDxh3&.l&O t;Gx"\?D9-)C(+↠Քzpv7pM\}np3,XSǑf9J&B|i]*0ThKJf75Ed6Xf "3ޔ}]1 ~YRlC5BoP2MC~747TBAS[pVԲ5s~³k"nW֕ Cky>uKUuSL%Ƭ 1wxN($~E)Uu^FUVTY5,I[8UC]Kgꆁ<.L5(Zu/[uXjLKp`<(R։EKlҀ"(FX*8JWt)IK1Z%AsbJ=g[FVd2a0/~exp3<1uo=o8˿0Їm]}NF#毯+| 2ɏl]Y=y{1R <3 2B1`b' Ġj 'mOdG߫;}3c(T͂(p:׽=eN}uvC['=,qVJXUSJ0;̔W 9{`иAݭ[L"\jg1#:7< M nK_Tkfd<0G1޳-FU]9jwguC&QI:--*_@E ]sL`5#d ҘqL?9_EٟKDaHF#|~+{;p/^-nq|tx"8LG , vAJD=x`G6qh62μ EdYXa-]ĥt[F&Nx@--49ʛ9z}gPܰWR{(cPt rߠfԙa.Pho F1xdnL6K'Ut|5Irt2 T#_& XK8IZ%:NK<ִ)r1ջXV1p <`ؼdؼwi |GyDx;ֆib4oZQ^/{&nj&0piV5bRQp-ؒx6>:L b\Kߜ%sLGj@۞㈐Rڈg%99?V0#"E*Rs4elBph9J[lQ""ٚ+lYlZ}&O԰-I<НbS4RH!p2 +F>Vy7rR](Ɛ_B2| /X4ANc L9+!<,*9z%9iqrrǥ'+4R%+5di\|0A3% Ps .@9+98{mCΜVAIAg/tHxDKl9xzH\ ؄.c^Ѝ8=[Jb,F<ĸ" AK9` `QReOlWrM 6( Rǘ ˬu9(U q#:Ŕ)%ttGcI[+RWF#TL ^&MeŐ F%ogx_ݠ9%O["FTIyH? e͆` &'wx- ;SSUd r)= Ź;dIZ,+jĸ *I:H;$Og.Mg^P%ٳ6g-I<UU@űj<ۿ__ _-nq[[~ ` NEc?cr zoMf+\8lȀ >> 9XpΈ%M _q1Z6Y?oNM-tk +tKO8sgp<5 :[zAum?ͻ &:a9mN}>(?xy\k2wL6+'CG~@-ngg{=&&K@RJTRದŔudl&R}|k#ogvze"L.tHZ BBhehJWeऱϻn&(P)(TUl1c*E[\8Kbł;)tazJ73-fTm1ךvj-)- ՚{kM ԎE nk`@@p'N#^w8TN9ksR]'utR]'L+sys?#/ppp@c߳[I~f)0Sl?1%**%h P-ijK\-"L|Hcy,˗m/\ұEP2s2Qⳡ!W1|LX[<{]ߐrϠͨտaHC" Ii)u0c9br|%Zy%geylCԠ\Hi^G7oogg?} _x 0q)WYUaq>9>//';wneկ~~җ#- sh^8$X7v,$%UV2Teyau${iMe:ieRe;\0W4dE+^ ŵxvxAY;7ީB(M99Sz |VJ":m./g!(MV87t}RʖXf +_O5 BnczJ^{\>)[yzLAI@b\!M,&)Xa*WHrnѫgԹ~BuW*RRh暖ӞPXsaY d/m+xÝK1CӟRl-9|pL\44%iU׻L{ Ms`6gFF)pUT6RBdҢ͚ 9)EE2v\(83Vw7C*S!VRݦLk i0yc -A,ypq#ס2Eי(|¡\`H -nq[~K%B+МM:,BKDMCلS qyy ADS Vam3MU~GL ݩ׶M9{瀳N9`[ :r$RAP++>7|G6?D 4RnFQOJ(]ܩ3Vĸ͐3C$ZSMIa|n}TveZvE1(&S ޥݴrRNnqn;9vc^%TR'F^Ŭ&siqlP$1e';diͨ0-Z6~eB1KMu5`6Xk J@vHBx- sI DOq cл9f+ Q4I0mw:'Nռ_ jN1f'3JӘIINA^*$}{OsFT곊ZO _v .u3H*8+5hEkF/#IG:}eg-=BYxk o. >xa]2R/i fJdVڌ"e\Y̹{R(E R(TW%JoV3%yU>~Ux*Tǒ!5S=: (E?O\e#O{ZZ[Ex-^O?_W> oum|HӬ5_lo*bYmdR 0eN)xI4C8y! HUqӝ鴧cSǥ2`"&ەyc~sM3ۛl-ٿ{B)4k5՚\=]ׯpk/8|1pIn\)ȭBMzE"\49'|3OÐz;_?}b6ڌ˰MX8dMaYTILjf1W&K3B׭*zDCj 5 DZl -n?7>o5%kl;k4Z6<+g#=Ć-8QH!mGl[$duOpM-HuD zBkmWCUtAS @YKH®dNi'X !1 xo@Pn A&k*!X6 ] 1c>&?A~a8eOX*,1vn>_Li' | QNKwo@Q+Z!2 A Eee 2m 1L!SsȌ.b+͵6bSU+/ MrJϹ?&B) UAWs,+x;`xĴjh˒p٬K65߰_RhjFhM\6rDhNL>W$qIB}V`g1/.3}#HC  vqy!/;\d]VwJҳ(z e5Y>EW)MmNw{M3Ȃڰ*),VjDqQ'>z2J.;ev%8y( 0 ݙ38ba)nFL5fT !a賜1)KA)J)Ȋ-nq[Q|tY?甍-}!wlIicXmg-g{<9[>[tґA22Iw *P)M0 c ]n!y^ެT߻RSTHqYP&.{]EiGt\",s,Jkr##9ucZ;'U u@c0tČ42o"& ڭ99uwKBr-"YXE>g=⅍4.?e'8gi9qGEx%/c81k'xw&WY*`٬|L & 1 WW:k"$ B)Uii8`:39{\#b ,6\kchB%74"5AD4 6FuvI ٖ3_ ҄NΜќ6g`^$Xm:e]zq)gt3s^KN{މH 3L<)ce-n6>W)9۰diXX6nIg3XNیdOsӌPP-D۞ԑ F 6$GQf!CyCo2_qޢ*UAZ*e + K*sg^Ҩ ×|j}sfN[ \. uNm"'ƿbJ]W` tΎz(8g?c?<`{^tNcL5 M TNpm'!Q  <7(b^oӻd8185v91范UoSf*IYչ1~H2|K+YQhBb[x XtPI 0I4Q4?}{tjVFbJcmfŴfra[bg~cZϛhMQ-BW)4\XJFCZ!ZQ' GmΫc;sJSUB -c66{L>оG%d$5B3`c45]e~IJC%{g{oݽO7*4$&QtP5Qgoa1Fg+6PiK5c)[+1%}cIʊqm"8zRCJ.42i`.+`.;L>HĆ\H*PD45J&zJl$MR$[ e "BGIpWh) Osg)e!lz&U)3"WP*ШDݚ TTJl&^;ȥ9 Rt@1ouHQBLTh,Mfn6vH"6S: Ťe mJ(HW__~/;ƌH0) (F*ol b1eS5mNkmY$,UPV*EKRVd"$J]ADGJȑ@-'5 vѹrdc"̰@hkJ X RPUIR!M M*b[yE/fXi+*E HLP)MpzDZ}j[_d`\2[DC*JlbjC%#1f8w̃/ vUKH.'O{4wEPHXڬeC&Dh0mg#|raD[#1oBmֽ9Sz'3ˬeu]f.EټljBOb C0)',I4Ď2H!\f!(Wq:tYw7!pFFPn1]VZ[m6$3THuEyw.ef͵gtٌ|ܨ<'%`Y)(${&$~l00]FeRԹDZy8a9k R!RJ}-?DvXG>edÔ)/{t9[n<1}~B Bh8L',a3 JhKr7}ʺ3j樱$^:[{Y{[{DK1i[\1]k΁yZٽd1ĦˢuZXXj*۞Ksv]2>!=/Fx7x:[% 4cxP=FK(\ q>t94zL(U-iKƖGFdF&YLU*Uk_oаBjXE6ddfv&*@ w `6%fVb eKz[cXb aAk,>?l5M֣3R_=KynPmtȠk'[{[8mB$^p/G'6zL"sSjT.ޥ a'#%fQEE.,֢4 54v8zCW'u_]mfɊ)&tKS6s>\f1Em",Iw@5vPss@k,rj`Ìwr<*&mC'x'~'bXN⸿S厵cX;=nOg\-/(YD2܂1dI~\o8QHyȧvp9ݘf_L1%$tQP bS=Vj%V]ϣPM/(TurOpL؈kFtX]axSNB* 1wi汋ڍB9t@jf؜R25GNxȏ$}l7vl3rRv̢Z"lOrz(WP;/RI9?69dHM[(t~g/`>st"8V/0<\zMѺKk.)ƿǸsBAೡ.?R:ii6Yics`L9aʽUIT;Ayŏʗl}w3Xhg|jt O*V'5\d@SjD* X6 R꠩5Ya89rl-)~ ~ sKrYh?ѬNP&+OTwqwKUMp^opИv6Xd4@Ì>pɗVΈ!Q-o|~O<Ot ?kՌk#zUm0 ~+Z0ڽpR@ Qe4U^iCڬ[Ոň#O6HAE%R,"`c.̕!sξcTMԆhmAZd"6 "u0[MX*!ucnXoR֔AleH1eɩr)tԉZtmt*j*ѵ *Eɶ(5 jgR/ h4Wi4( =-hqWYݤ_+.2kdGh.y÷W!#.%9!IJ10M~8ޣU>@EA[*GT.˴<,4:7[x'X*]'}-dVV0JhyUDqx$#.9_%?0w,>ק :K|)I! lsR Ji־tH["wtn?޾E= J=DGHՠn4ZnTzrX[4F-BO@ \@٬>7#/ :ש jn;&nAAϐ"Oءnϥmk GV1clKm;z톾\o6:U萘UCH !)6;E%2]g}IS740v_r]3V )ߨ3ގtcCSrβr5]T2vG*h:h:(z&8dz49r1i8l9l@Ī%*/IQ:7PK}I2A= 6SCbCQ6E Z4E9RIw~e|ڞig{)i%1j)i LeNub:pX!9{r|'xOV5M IKʿ3R!&Q=w֨:u4mS)~ٶ!0cUI񴔱@OϘ(36UH:DNHb8$KR;,1la6mdUفMlm:Dݐ\J*r#n@ڐ.;颶0S&UO0~ȝ p>Ф*212;-HtoqQwߑlvlE:I{6E"M"O,jUU_޼'\D }P#͒~a%mG0f^4W?l txnp#+8tC\{"ء: TcN,0)Xi|$K4L+ܠMܤJUݠLE 10_Ԙ &D*rЮ$tc5V)jgQ&RQ0JAnEa:L`%MҤUcG vbQok\gvt W`eU6XQan MKxgIy` a99q 0hM$D-Q2<L1KK$*`M hiBahH}jΌi69s#JFӨ͔l1尡uޤL1R*+bo8R*LOu6n\ q0Q'J֢mмB1Y]hhuMGlEDЛQ=z0nf~5mRfMU;`-j*ȕwz Vgs7LqIq Iq H6eȦ 죇zPFPBͻ'ɿ"fyDOxNlRȖmI+$*:4BY:5 bd=蠟v'Q)yP+LfS:l{'x~ﴹ IMxLx}ɲS ϥS:.W.H !I SSYeѭ8J~dlM ymK5SLs U1=S<dCaaȮ,xMq*%2RWT`cEi3($wL1,Ĉ#W@LyxLoبDžWI̪=dzSւrI XwIlH jq)qY_(I4 /TKrZ2bM8Yec]RGMD{32ԗ\XQ46M"#_s{ϻ(Y܍8׮90g|k~o̿qKFž㷓hcWO[ Ptr r}~5Ir^ƼZe2O?PTg!q,:15 >YnBҖ BjXEa|W74 vC%Wvʈw(B Rn6P){7{)H6Aɘ ,s,oqn2..xwqt:5漠?_M Z8@:0}/v~p+~woqw)/߿; %om%Pn Y"LQj؋xaξf c]˟uYOm{Cf5(7?h쫚dl^Y zU)vVbGS&) x`"uW/_=cvtwk±zˑvnqZ8|HTy)|4(ѩ:%NJƩ\`5%OBT\f7PJ^yyfL1ilS*3$ \j*hȏW_F?_#0O{rR4+XUQQ I9rnx&n묇[JKE!B5iQ?͖hD3NCm. 7*ef`%fՌv+`V)f **i5=ZRvkPPrؽySGְo2!?dK2-e(jYV,cu*m@`ݒ~ItT&d4al̙% MDJE/7%o'x'~N_N29g ZtJt@7K4chSliP/UJT*m iP46G d|ŕTQ>e_%N隔ETLC S0XMnR4bmuyc T6|OXd=F4-f[ೣ/VGx㮀|=$K,>3;fˡ{ĩM}N )[P&ahEK8F¸}~)}LC a#|[4j,ԩ]ZxJ…IY;Ȭi߮@N JMz@[O>O\oZ).i\ C _w_sֹn)*eqgr 5*B@!-zY&4dai*'jC;ȑB;ԺJdI1-Gl|^r|`#n@r6xml2ոIlt&X_XGSaj5۠jt`n'=Lcnc /=-УBI0$>Pv h$.SK账, Syў*`CQw{Iz$bPc x$1f&fYwd / ʄeN[1WS * Q~^Gң-[֣./OwbøaV)aW:ggĹǰ2S|"jU6FrTЫ8u"% #[,x{*@*Аbnϙ=6OSk)Ւj~_[{^n"~GDF x6`cfw<#`ϖ>>[&ngp>:|óTFPo -Aah% =)`dn[d%x˩2e(q6i IDAT+ r>I9~{4:Fp3~CxMpC)6ݒf0Z 6 'ј3~/1@b"4Œ*:qtgSaxXGث? l5a-/xۼ|]pȴ?bjP$fĚܼb)ŧ<Ƈ,-K{I:\R\jT4j\R"5{0 -%(#2e=v?g:9G_Ec{ر6CkΰcɜB1(\屲<6#"R`{N#,ЇCqϡqD+̦,K̼"jBn'Mp&75{{VT4FS*Na ͮmTTT9Euj[:I=O<'+XFi)T}Su#ZL`-e@X+~!4fАw!7`fP/ d: b en1ӘF좀/jЌA=* ՜QDdIG^QGkAk@P:S:_pHk[NW}F$bzSz ocޕ~Y>&gfʼns G^xoxὡn~mCڬ?._f5?f_.|Qy/[ <զU7%v v3awM{7`IEH)h ͢鳐OOPx菹|q"ᗏ_^ 1Qe\]XTV=sN&$ϒSa(SB$ߍߝ5dj3F [ͨFUXuҹ9x{&o8U B%Ařo6o&/WI99qo9vo9vn -R;uOì FZ8g^'~D<]aSJM+펙:>|-FS1]B~ՑX-xZ=}Dwߊ_Q:ԊFT<:1fsyEXsM؄ꄹ'U =/xbq}˛ox#^00BV>yf"cB5Y(6Շw݊_Ĩ_M 9߶[7/35.s6mD\Z-m.3^P `ixDt(Ш9q_ߥBqvj6$,,=V!k((.6 * . h5ة>a!`(gҖu:p9͏_׿d/9<%UR\( V15*Scy4FU<~}JXF5Jꏜ+BI/K!rbjW!Frʨ1S]M*玟\KMTMAUإXjL8Svx'x~¯(%EYgVG%Q*B-Ig  "u }vu MVDt4E{DMH 6^Hdyb l^𸝰}DW>vLqߏRuR:UnP: 3 t[ AEtQᱣŁHmےJU3Ga@C&3چ&>yex-/-x֖Zթ}b⃉`j9^2ފQ0g/ L& mfY:`6z$sxS Xc yI\\ϸ_;bR1c8b g1sd*!_+mG,Ym# 鰡Fߏ\]=wjEai.3m^ne-Ff]b1-M[Pde\ϘCfڐD- vvq_1=ڮB kf|~jO(PݚL-юg%yOTA) VZx*Y6 s'pq[1IwUiP`JHXi]fNŮ8 nz9+r;.hp>;:uJO_2L}WHܳjz,|Hкtw_p` #>W'x'xjeӢlZ33bI5I+da-(DI}7.k pw*ƯwUL:,D2S}U 5FdM54{JL '31DH|v4$x<0AVylS,;C0g%u#X/=/[gk`PQ `v|$p3|'&C9 ;<=,bI=O<',v_{5I]Wq=ȬT%%yټߝ fCC4.:2txkq'K6lɇ |ɊϿX;.yNYd[^ @dEXԡذW!W  T%-FAڴs*#7L[Nk֚}b꒮lw6WW*ګ {z?pf_O\|kv@0&[+ᇨjQ-4|ȡtmmFJO31yE(VbJT[íO|\r$7L m_0SQX%.Ss7Lѳhb2cR/G 3ԶPh̏fđyE05^cRwCƠ\[ԴOl#\+*]z+~ig}34q}EexadD=UNPrEN&zSS,%9#(`p2\tJH딴I@*3Aj5y.(sASQ_Vx;z5ްkp$Ȱhyhs/|z 4#NwGcn -IE1|6DI5DLKs8@$@*,og _Eqޞkā@FxeG}I-TJ fhrL V?Cs+䱤TcoDL;&b!F3^=֏Ư2$7M5~i}&j[:>PK0)jDwanу=yGGܘSTQ-l -uKGkW]tp$HY]]cO8gyD_2v<縸椼F%lug|mK̨|(1g%IOM > g^2Г-D#gHmԆJmT/v* QaZX W^upQ}B2`'ҿ5P{ E"4ٞ 5᳂q˅= [ g'x'(4;;^f]$<ʮ IY4 ՎvK'mR IiԞTjS1}{őyHcaP %lD{m A.vq- E.{t_66)1~<%YiPdҭ1.2aq}`TFmTkLBC4sJCr4,/Ű *]e'5~\J٠5$*UW&IIBREJƮQI}ᳫ:l>K9<K0EdF ԂVblislon,!y=dn =~1yC({yD8L;b٦y $[fH)MvUe9dwHS& L8[F^ћ0:VOd% cI^Me*(5, FU5jB[s4xq)J2K4Yz,1rBcъ=S߅l,.8{P 3CdPH\0{TJC1 5QO<O5~G>4g*q-QD^i! 5&Tw=N=שct@Zu"Q&5ZPa:)VATSv:ڎEF5S)n{ܼ|ƍxơS:&{+W.3ܘg|oyk~ۤ3&iϩɉw 4Ds^eJVs] D +ܱ'I99>a:)÷kgJfd@9f#N/>gɨ7FnMnd"!ل${rQ:3o7I]Mgozdc,4tKZE(% Vqqs)Ӡq:^)&3o@O7!Љ1?#MH '@QٟxG|<s7_`[G:e`qxmO,FcC-sJΎלo8;^3fgo{EJ[i6 )TH%ߩJ(<0/slY㤳Y锔=Rv5D].k%%h;PwڤK⤡njTBhP)j[C"M>MqHLmǭ<杽È2Xe)٭Nlhw9Rꫂb}LĂ`'d}lhj0/Ǭ䐀='5Ϫ78E48qg&{&=ފԶ;>s{D,>suB`@72ج15:&PЕd]pO̒#)wcg\/Bd yfzG׍Dg̬ p\#H#;lW)I(LyZ$}i@ؔ%>/P*`V EedjƦr936UEi􂡶B+bj[g0"r>*eFAI9#6edPD&$N<< \3;eW!ھnM2栏lm]BhzCm ;`{]kTBsE 2x'x{;\ b$G%;7CtԐZ@ibAyr2Wir)H]K;4nW1eҠ5V"kA7(kxPP .qeĜ.I[ |^Q,MF6s\tΰ;L5sh/}!Z4`h%EU:;:іgs&I3Np1$?eզiSZH$GhYMЯ+ s Z7/xK·Xg&5~xF$f'܇#6C,5 5fgJJ蔦FQ+؈OTH$-xp,!soB jFIV9lޏ +.Ot-{Ck!>l/礩C2)g̤SP!,#mUtn8C;6l 9I=ҡG20`(HȒ>9&dX$U'x'5~@hM{l&jnXܕGT{dcS}g B{bh6;CYL3|吏9o|A,]{K(6'c"Ө*A[Y8fpľ)]ZjdEv۶*㮃XUXaLhm)*ЧwaA J&: ]LtW&|#(7]lѲ%ݛ%Z24=`a #F=Uz]kA餁M4.<*QRQK$ۈ^]p֜e5y E'=H&NGWPSSKŒuBU ,-Cj B5*%:QEBC8T1R t0acoF]ħvϙCL3r(gȒ=8eC;/|8;Bxb+d1aDLuVz,b [2G  "QLV>9oWDɰ&h>>6 6 Q{:lqQf F5 Ow^ cA)TRg]YCՀMc*?0>2_`$f/yl@Xs-UԴ>dJrDؠgozΆSb2,Z|8VzkK2cU%yf]2,??YA͇ū6F2Ǡ lKMVTμU.z 5GڌHHZt.f o^4jc:R(5hwiꔍ!r+Y,;;ڒZQB QBgŔ{&bƄ~U}[D1`KX[U|\Bt G*1(D3b]n90{~qG5'\sJA|jQQ4ȁ[ ƑPyc1~7^3_Lx7/tq::gq@l\aHB icS:ۢV@-?sgFD%nu݋ UitzN(s|w3>_O3ad FƜ,4ѽg ]NŒ)fE|J""\*Rd$LVMo_|ϵ[zyq{|\@u^nXjs*6u*4y଍Otk O0W AB! U(ٔ=s9[#㞶mPBV$1YWJ]!,} k8)wUF]kXCż}cLBs~ɻ },w*jKFL?{s俧7$ >_4ʏPBJzᒡx`l3Pdż2?LxNY' ՚GR#BXJox|mr»|{ A>KF<0REkRG4S 9B|߽%WN}[moU~Vt$<?( aw N >ĥfT$|0&:g.X:%)4xW#v]a%F|H^3{|LnyO<O<'k4 :p!8A86a0`ٮɶ x_=3U~ʷkf'K<-x 'H*&w}je]ȗ"aenLh!>}\_N^[kɺTǘMJݨ̚ z¼&ڶDTۊzEԣhvEZ/WfMho -zKxrT K VրtGKsNOŨM# dy39bW!yeAU ]mKp,uDA"Vyд/qlPOc WP3}K"M[Ė죙rL ^[emvK\+y\Z"ssY`54Vx27bu( &'nl :|Qt4BQ|94ZeY{}Vvc8˄6?0.Ӫ/hMpa2cy)b+ z@ k#+qLt#0^3.Yw؝` vAg1xYSݡTʲJ~oYQIɵwpKp "{Fŕ Tij(4@jtbnY=jðgP{D@YEk0:%n@ Y}mxu-oKiۼZ#էm  9/w\gs(zCh-m3%v=RD}Vu oWw+z5"(4Q%bp!,0fAn{TiU5LTB 1qc1+iaSl ; y+H79۬Ã;fv:8JЉ8& .6.qs(}D*tv2|'Xuxwwƛ ֻ ݄Χ:aϨ*j*d+SAXl& mʡzV3J|UW;,;*3uդ;f1Θ1240H9oV#jQE_?ƤdȼsWs}OQP-\gX:ֆPْgy|hڊJQD8^J2P=As=GR8d09Դy`I5%GcqǑ}wvW1p'pA7uG(.h10,E -I3`{$x ɘw+>W5x/?2V %jV˸jp5>gإeR T@ k"e:C +DZl+FjFK VT`EtC/{d!X>ogm>m[>{xg{P*5WKrg񾠫l8~baWEyw /OKsa8GؒTMB1Is|cRjw$rO(4>Ub",&;/$BbE% b:sÑs0Y2H +1i#<#ʈK9Ie:ˊkF[|ǀbƯN[}z"⬹^gߒcr_OUS)xfeom" J)t撾uH:T;':hNqQ:נLjqb${'x'O?E*A=^H#Q)Qmm>̡R4*WꔆNϯ15/;ȅ| Qh,Q0=[b=`oNݛ_ːZ§ Yl{T`/Fc_QRL?H@KK敂s2=E3[;ATc'v oWɽ1eэd/6Kamkڿ? e昲6)*2ۊڿWj&*TBS*Y>`](ߵIԀ߶ IZ&ꌩrGܢ}k$wcjm԰x!RT¤g%S#g cwra60nK~Gޔ3bt9d#.%.?[яj>r^?+ul5T@_4@@OK"%p \ZhK=(MVM lWKu`bA! M09zV֨uVJy|?W;\_, "ί?!$נ*PtV {%$/?W~0~k+{{ʷK:@nB(/g*DU- /Wlʑm䴊¶ ]Fd+2t}{ Ɛq\B,rzBG`:Y;i-7%=cS䵅DPj[5`Mb\W٫]b:%[5 2{I]ZTXBEl,}8O^[uuvUc^چyGy'O]^ i{G\Gĺk "P-זl!nwaȹGHskJݦGShz?c8YQ>GյNār`JH>0@PP껲O)N %G¼hjlP5g:gz kφO m|Eg4>lN!(Z JGp! &uN$h>(sR|O/y~丸⨾f,(JCdזh3&TzYI.' 58Ei8g:SnjgނOL/m0%?JwOjz̊g.s7pWM@8K$;[oGzX 65+Bh5!ܟ***S2 ΋8^N{ ; ƶrZU`)?/JES2T,riSH\ZTŌ_35i*XjN]G|A|eS򵍡L7wL ܮ@kZEi;ZU~= +\1uC֊o,+ @A (9s>A}Yscu``p2`6%I"]sHXů[=cӵrĭ>ߣb7df^OȖ.~e3&qQ2/$EvH4̊IJiPY}主@U;BņP"jۏSe,㿘3 |7|w |gon-ز܌tw(2o#<#?Y~iLw̷wo>o'L;-sؾ{X+[?w 1'/W|y&B!WL2ņVHNQDh䉠a @R>BHr"xdC , Al`VaU3Nʈfj㔟u?BvЂ7 n<9$ZeZۥEL 1mը_=`uտ/zhxKEv@=rr3j* -O[z@+xV>+uJC<{Vї|od#4g;V,oww2mH[ |.GGgŗ&G% 1*=0|PWc[vTttnרFCh[Xx]ƨq}2>tO~k%t/?wો m[Ж*an ,6sB(JUjZꗕ.uMC=^iokĞVآdM+ZEe!t~ϵo18cxE$>w֧u"尼Ce+>4O/1P*T ̆lkm\}tOx/ѐhr4}oGJ"8"1ml%sl C}́rρvǾ1RM~wϨ >ݞ>O#/x>7rl;>=!ۺw!|?GyG'k4F#Zѵ5 Jՠ-h,9Ԯ9+K5X @5HG"-3lI,=*wTFÎ0Gwu$JOT{~ uLd# -hq7qnqG c%а WK[0?X1?>v1B[tBڸ,!W1N/ΟzkD+;Ҽ*԰MB:>wLJ kYuʏyaRBېI$.^!'3\d @HL =ucl"mKc8<>-jѠL'VumP7&umE>zΤ1MUuL|<&(sy"f-I( %BCMh$MmPDvNP*}Vʀ~fsPv:昫m05*ef %iReߤt^C(JL4KM"dIAODԚN[T 6Q=gNldGp\2rVJusDl0{)*HU 4Kʸ]V3/X#VeT#iQHMs}9*?c~[`ar4!)kbeQ^4Y>!MNkBJA.KXeT~K20OjjDEn.+4 rpџdF H8T9Rn8o8쮑P!CRxX:ňᐶ<RR gdb3kĴc QW&l5V +S*cтsU)zũՌKZz1YG3+Hf>O|Y? 5V'|пC|DQX&*lw8Uav"ͷJk9/UAתԝA-wjJuHstie.j+XmC 78m": QcQrB$aSǸ2fo7 WUL[^'A'RKŖ46nr;~F-uC2myYV v'f 9Hp;߮}\}JX_tu޳IT5#q±CD^Ek}D)-{LPYCdǍsȍss@* "C~Qo %ˇ#VHvg%Vg%lE׋W.6O52Lw' &D~5(mR30%Mg%iNg_H=:*+[oogXA7$y|#47 %>BCyØWFD&řA7Vxp[R. aKB :Ga<0d팟#h̔rͨI S)N-~:5-gRU&'y=SY==lgւuOi̓=OB86;6Ղ[mʽ7{ƒmJSiZ"u>˦BtNAo XV %5'NEK_pCW(1=+7# bW!BP񎰂3.qrY[!skn^XX}] ՠ`N2;`1qafӷږDt=! 4Qi\WTRE23ʌ#q4a fcb l AcjTA.-rvĬkBbVUgo(6 + Yauc>9?nO\$U@R Owsބ.񃔣z7V= Q<>_b5Ov{:}IsH6|*O"6PWcY"nP_(ߚT:wqd26qG3Ԧe #<#0q։ĭ$`Xsd9wLnδ3 v٥JڣM{$HGQBJmU,*effNXknvM!Q)B~0;JlPeG#5y*SQm0W_Sa5]>`}?`{IzTN<-hli K/3'DY-_ Ŗg-![%Vj4s3+:3w4Vi5<5"^JГ[;&qdNH%>iiS] p; sU#!DTۥe-HLUHUdIUx!['06h[kTسyfR4Y&VٝKzacD .%! lh*?$>GCTS>l nb : @ BBAh.:V; ߎ3A+XH:CaC>v'|+5!jPip{5AC* 5<;+&jTuZ8۔`O^`h.³SӉ 9J?di MiDCޚ#ƓEcn/\lcwƟUqG*Vؘ}G\Ԃ##=eٕZF6dEVtRAK=d٭@@4*}׼[Ec&uk4:yu6EgC Z nZ(1{5עN C4Y]pel=5J)4 ϟr9:&I|( }D+Q %QQ7*jS56*M5F'{lPh}ز.8Ri;$("vRGn޲kvs_V4F[k>lo̼U}ri6 ޡ5ZuԅA(favNQ8Gi: ÙSsSu!{g5,ݠw nRui*ֲ"B3u20WG a #|\# (1=X4W$=ĩ@L~b:f1c1C_[l:qpRf_Db7 0`x21,Ϸs SvDh[(z̼=nNN[Vr<#EsoPTޒW1K]x{vq XUX +yů#Sy=)-OZvI,>+k@dMn[J[qwVr1!|⠿hQPNvr1cܳLcy'R{Ns~^p/16&cSP 5W Tu0VzUOki6QÖ^?/߰XM,x@(j-6@ I7yGy'Oݮ?wHE-a}ҳ  o`-BH* 2@[4A+%hhmEz5 IDATlU ع #/׼pzog4N4FZzmExZd4FaCb_jWZʩuEAZBåyepƥ<8Ch-"/אpcwgL73ܼ*&&\-շ~rLymS%!h;=xR^56 zWR# m*LB;lvG JGO8Pn@Hᨹ ŏ"D+Q0V*7.w .J=Z996Z70Ơ*(gP>8缶_yU<oy޼e.1ˆrp9nBənf(ywIAyϧ֧|q/oB0+*[g vמ5eX6A@c8JX3TV ^R&fk|=ev1eaÔAd`t`4XQCfb=j&׋CLJsVe Y$K };Cׇ.Pzc>蝢U`9q +* ʕC,iR)&ZӢ%-+BwM]uGf r%^)oS=^p9]*r¼pWNHEE05&l-n?9x?И'SƠ-*`Y(]GGy䑟,?]>+~`p`Ŝ> -BJJŠR4b<̼)`LvO<_3rc}=]q4F|_p ZQkBsM#B Rr.Q).ŵ \0if~|y̏8,'o8x|WYFCdu{(lF-zS%҂ky@>9`vÔ D8|z0 ]/6WGSbS&UiR&zг\59`:dN`r؂FX1z FA1j~`Z>P7{G+7-S+.SF@[3V>dAGY'JZW-Xe;;e}=  ㇤GuDEQXJD{ uXjƌ[d XƬOXtc ߙLro~FЭQ K2Ǝb1sy0A9fWaWnn2w;odTlG[Td!%k\UDp1s* (ra oA6"~8Fi:,-=bz]VEQwQwQsE2Ej uC$gm*OBg [/񫘮UUT6|?)*c5#]C:eL/N Z:/_3gP`(g jE~Gu ZbUXA9.эeO":BbTab.&)ޔ6XڔIX3Uv(Tm&O c唭ŁGxS @}Qg[+UEsF:FdFo0{%4%8"ADڤZCT9w۶l\~Z}dF|$5lwҔG1Er*YkAdq~䢸d׼5kE_^c5 îua+|§C%H #RIgcBW]4 ۴?f,-/ %(IGZ({>˰]42[#z:mرHe\)t\j/m;xP*cs}6vJ![hԲ V Ba'DI4tbI "7~zĿ( W6&l@(H35v42DP y WXaFKfUlG]!|[킩dƴ~ UQ5ʪ>ATDrL) _h?'0}6Ѡ'߮ED),>A!aċn0zxH㜴c#0NJ!d2_;>k|LSR!ь N?`zwOW`)u".>`zS ?a3pkϨ1<5rڠd5rZ45^]jEbY~*C2\A,<3d6 !$zf} [ȅfh6ґsD#}5[2T )))Fx y~S`(y}(K>9H6O=6i#[U*cFۈxLxLxLXCAqmM$;a-MVCgɉ@]XnƗ\}m8stpop= uc^)BMUb{l>P [EZK)%i.1&zy0 -A`~nǙhʇNIn~_sfR `86FȄ䌌9/9䚡TcʗiHX1 dMe6 Í9׌Í?}{gy0~ą+՘_~7y|wiLEd 6g}8Yq5q<*)tҥ!tpkQbG^1:ܣ4r]4Vp.n8\CyRRnwHc?w"L42djl>Խk3O7Fn!uٖ^=(u<&yɸsλ(3F,CĭlOh>{r %3VebMs΢+Td"=2L#似[c?}{;^?icoP8dKM( D\ޚruzfJQ}+F+9uJ6lXiS<@ Y!Q BvRa_l@2ff#fOX}B*t\G[\!/;.D1$lRX&2ƉZ# ScKy,)lc?0yW|'oO(*U(&Îf}wX39rd`X2yZK|ԩJԙJWhwM(hc R6` wj>e,9ke}uCL\ ={us]ѥ\hAd^*ƽu{,-%UJܘAܚY%%`.˂y0!yko}7gc|\0hg[dF$Ahvt$+||,j jK2ejK7X3 ?k'w>Z'"dCU6 l@oJ9@>3<̏m'+ ]`[ҝQ)eF+T  T;TbߨttYn(Jd㰿Aڐ(։k}]¸C{t2H܀pOv0WwxMH!i<4ǔP*$+Den3ڪ_HrЙu7n!LmJ:$Z*TayN~f||f/%;N$X0A61؂7Y]Bc) XI}R=.W%^%1//xԎ LS*TMv}y*}j" &[cq{$x[ ["95a5 .`n BCZyuqĤ\0(פ^+jFm"kDs(Ǎb8@4 n0nww"Sv%LFdY:kP-ż!!j/WL[J]6ToiJR(t5)k6=̙T %bsTZ#UJE>VUz)Q}Q%ڶD\CzX>mW'r{q(fEfHF-u,PEy@p4R$\ReXnɸq1֗xz& tjIE/J{aLv]Vэp1Y b&cf刢U PV+B{>|CJ4DI4>03J0:^ghZJ 뱏;5wi#:NT j -"LIub,;apٖ>+DCL_l0nU '|GuOՑJag^TRRTJ[lTMBT$A-X5HFi.^iھ D;4&ˌ1]w0$E2$a13>Q]"ZIY9v/EO?2<1hm٦mB'}c FE8!C3<3~*{\J]3qq[$ޙ a.eGv( TM5r"=L4Hѩ- q! /;XO q=#jS)TA\.悻sq`23/P0;h$2W>TԢ`Z<0-xrǻ)<=LGż}i'fDkvEG@.t!Ӫ&}` aSKTS>_p_r_>~R U",;C)#8KFM3KCdb{<vGdc:E*6i+cJRR6nK׬4Q8@k괖Kc9ԖKSPw9*P.qP{4G5FU9R挃-RaJZԉLH+0 00<dI({lu,3}|b4cqT{X{Bt#r-eh8I#Ӝ+X} s djt N\\QvƬ?wN])B|D脹Ku e!D)P.V)kUī;ʑBS(*HAos2Ŝw)RZ%dˀ ]~=~uLR\pi]si]s?2̦#f?3vw]PflF&'l"zU Ib9fMH!rJUwlQfQB SZ*y i, tZH4kr%Q:ს6JRm]ԥ@eN% ZvGD*T5i熮b.y(NOO)"Fȸo;q4z@5m4m UW ]0r$4 MC8 N/@t[9Qۜgy+?¯D#x xyh9EʏfSZPlP%Zߠ4ԚNXDJ64.dVK4 DA!x1vKᩴPDE#Kƶ^r%z^EΗ|~Xv*k;!sheaQO)_՟W6(9)J!WW\߼߾~sz+z'+niudZ E@ȅN-$Z!L>oWWVK>OϿdtϯ"x^_P*ibŇ(P IdI'ۍ v@#otc6EkԪ<QJ*1F*Ф])b<(<АhP>Ʈd[(Vi&t;'sL7"UU:fZ?QTz9z#'-y__?W?",$B kTgWڎ3Q>2(EAK7H4HaCtZDMKTH^8d:8i*EP VMS%]є]/0Io~4͸O=^G4_mg_FFs֠58-@'s w_6GNf/)w9M:E:y|0ʈVHT`Uk:丞QHI_'$=M!UdAhBRJ42щL̴Y}DRmTjU[vKvC4)G(t-fqsC(`e E FTB,db=G}mHj / %8Ly0-Ô< LOVhiqPQ E6^$XԲr$:b<:ZCZ٪JژȢFjdFs]_xɛ#JY4T*u+ 5zF5BhjB 70@)@%U 4Tt'C2=4A u,xhW")ѓ^bV ՠY ՠ!)uQ& c5:avq(wsݑ̰N[-GzDGI(+:hBUeXX$BG14_E(XEc1&ku2( g5)LGAZ d>-V# :ш3sFRb[KhSiSJ1SJi{63H6-^7Gԥ:25b ,10J\Re8dNX6CVZMg_:4@j"Þ|̼yGUAar[8ed:szٴE:bCM,*c]Y}ـy3&u +e=M,{ωL2b/w(/CNQkFR4F帧{^{Nl@@#ġ{@(@IB Gu%X']Gh9bxxq˷zzfHflՇ1AppJBijFHvM晤{,2`͘tf ̈́ypt8n l3kmut7ĵö)ladԚLEwY.lNb8iE<AQMv8o͏N>FƮF9c剱ȑĨ]-sT+Ǹ"mcTFk#Yc|-2@+hZZj0{V>Ċ%7*+â?fQR>n*Ԧ=m\2C쑾7Ƙw%O>A7tϺ#H' QM6i}a`вmW̌1zR u_XVj1OvsG&Eq2gxi@tE9vڢD$r: =1( &Au)q>k'4?gvxk$,VŢ-F*0:GS@X#5װ0GleIor^wHˣie0*; 46yr+6e['[&'XeHA&sUd< >WPr/OU"$~d:;5 Myxʎx|8b^O(rZW[eĺM`yV@KtYJ]l6RVoJ'ۃp$.{<t JTFB52yA}].QI1c&Yۤ+tkQJ*Bny5{NmLh$MęC9TBk?Ugy]4 |ZT*:Ҝh {vf}-ҩ\ű#^zˏ޲;_0 %;.R]{]{Ʀ\UWHeMdI>0h~oby\_s~}ζ a!*Li1!.0 a]G-FW;͌ )k4eSʵ cb.2\9mG[.F)iV,$9Xp+1[#ku(|_Ki}hEK4toj!cm"R*[K^o37'|gdO#S6倻z/: ]}'d"}V1ta(NTy>a1ˏXC\yAdCr6Roe:axe_ݢ-- $P^J]Ї _yxH0+O:]7q 47GOPl!O ]Cjc L|xb5*"|LQM!=ҭ- [ě J~J2=P -O 5*2foWԺ^iF<>&;䕎f迓n[:GK3aiO[N;w-qE(Âҕ C 2dN8v_6IȻ-,PI~K`vz: ">/AZI*0#O58`LH=.{%ꔯ) 16- K-#}µ,2gjFYFYhMA_]`i{rMQAQKL-CH+Nx;r }ŇL95jv@=nV.Ϲ[qqsL_5۫.oXEWj=wLO<,O ޢ)%<ԿԿsZdk2E#u242I PQR:UR2m~Ȼ,J552)&{\Ժd~]לy=xgGǢ&e+63<3D_h˿ooiۖ??ϑ$釟+<6h'DI,#ud#SpF:> `1(Kyj8dڻco$ET+AH1Q8RPME b~w^r(䆀8a xʎIx? Z$Q2ukzcຆY $d]`1(Q -r\%qءƊK!^Tf;@*LPTRdqc:U@ZVbG m^-ےYcar[2C+DըfRU7V3 ^oQz<:l~Nj,R2E;L3EjJ0qk{:F` VvZNy:aNV;|{g,Yn$L9e`Ho4SJBe%ة8TqI5Q )C[S1UH-ֲٻ _5uQf}P5 i"FC1fJZH[qK{C :Bd j uܝ eX',emI=Sk)dS}Uhh}8'$A1CӠRTdQVs\pZ{QJ~mDsh"űt UIꊪDdIl>pHP=J/QS P;Bo|г O1! -v/fx>'[f?$ 80p$0W57o-MDӈK`4]0 猫%zŨZ1LVĚI Q6eeM$E%ùC/Q ĕMYiGIlC6>ti{[dԛW#TmI kʥBI #Uygy+~w??_knoo/I4_7ßٟ=3 ]9V#SL1pn-GsICZm˦y[e)&KmdA ZMQYm$J*lN[l'cl'ubAj3V&q_3WGrQ Qh4̙jGdr$O˷D>M䰊NhuFF^!emeYXUC倢q=b[F g 5'#~AŸ\ķtbI^0`U$At>pN }FHe !W*RdM5 NGԨ>&1Qp{r4Tc*hVN4LFzdtĕqI'3 :k`mj;ūs{a7}zUP2$St7b;# ܂ttIHy݈4WtK:y ,w=y-zΌ5Cw^qk2r-].w7C 8kbQy[FG uz%`ZnSupG-{jKmbm%L#\7ԃ= G->b??p1j1jU1dN'14tZ zZT#HRz&'s8{G-<^^qA}DLLq 5>%frJHNήۤ)֨JT1 bG!O+WZB]ڡA@d{s^St)hJ' SYpA zXV$5`k&/:3 IgԤi4梹f"JIު5bC˜> Xݥ2lC[6S$Sc_R6d:GPz$?Mhe)IC'nh$FƜ! ,z\\D7{7\ׄެeԈ5TJ; YAYT T"6KѾx8o ߚa4l5a40TYmvEpmmUѼdyQ5і9*C_bz{PeʆDѐɖX'c̮Fߐ>%dܔ t'܆̂SAG5G5k%79L'تJYF`a qZ" Z$4-g\]!kqhq_p]\p$"An(djR$I4n-[\V(yG~5WKfM+`[G)l:Qj{k%ȉeأ$ U&u9 5rM4к_G-ϗy5߼fR?Tä3nk\YiI~8M*uU ےlmpX5[r~FّzzPu}܂cW#7;|$MdZ SwcF[X#<7d!!ȠYQ ex!UP-mK! MfOW@ +l<,Ƕ<#,K%ގQ`Kߜ{ˇ)\{?=ew}Scʷ~o>g^zDzA|,Q"(2OBX`Oxx=5׾|1, IDATތ?\bO\vakÆ b4cԣY]b0`/A CB^z ˄(nQ _c>#QjfJ׿Q3w\e^˥! LDkhmY2b+LȢ@ QJ)o2ݜT6^P#KUܶǙqC3['6;q29Ma+zD]9qCG#y9 vfo5V]^2N8q& ( ,q\9ܷ+ߤPdtmyMGZѐwDAdDIdďuq['u3Ӽܣ=d6;cƓsgAÒ"^DL@b JZmNfM'[} ۷oF_{ֈ)}*P~_bZrDcbݯ!l@-:{ b{/B(J*/p=8i0{L,6Ih++ʜ29c85BDq"8H3>eȴ9d:1^7lfʐb/9x//?j# I?h)*KXl(n\q:7;d$u-nnOY &ql; sڽyϻ/Cy)=/VX16Fts C\Xl6J߅l05O(,xs%y\ԥ=adi4tG+A>Z EWE$λG9Vl}Ǡ{i!3ҧBny<<#woOL졇7X8]fʀ26:+:ƒ}ch;]+dukˀ  -š2*i@ OvХ{I۠h((yN^ҭsZł*/Y *MPRT(kSFWԣkx{Q@)Z \c7(谦!DsN5$ M_Q}go ks5L 4έ1??7& Ҡdgp'|xlVw˟ٟeɟ~kћ1 Hx"D_?n؊汗Zr!^/L/L.DIU6Zxevx7Gs Y%JIq>#/yxC_, ܧ,$V6/gk7xC._L ;,0NFDĉ:kɂCư~mZ ʳ p2 Y&50+YDpO? !VGGpYTjuTA)cHBsPn"QU⚆*CH1&)qfKo vRgG=MiOS0-P 遳5A%v=éh?mpwz MXҒV #+ņU}4>&- ű((ӟbQTĪFĩJFS6Yhw?ſ%*mO#HCK$@t,iy][d9f_ՐC}m||>߿6Jkfx_)aED80۝p;n|GW|\|#s^XxӺmWe8`-5uNbRP$2U#r !Fg&LҐu =ӂefKs~q5^ӌ4hO#<;v CXI+V(v >  Tu><?;W__[[l6~__77vMS SW*d ҹFvٛ9wn_8D}r~!Qilȝ <: _% OB D ueuBg4٪MJ@J ʈf{(yIy07,(屔@*=Pw  UM1ˎ5%=1z-+ Il,=YV#K0=i[ 2l7d#6zJEY,.%Fpe=fb:aC$/4Va?P5嘍#&rL2KA7"t30cz OoxFޗ[AqXl(iN[^qrڿt0,hEaUj55?M!P&+4]j'~a*mr,)YMnst7d_r`?c!" /j0 <rA]P7w:k^%BΜw<&?_B%'üa˦h:a5u慎4`}$~R#~?s)21c['.:,ԣwľjp6Nΐ9EheZFk5R]ٝF0vَ'x8&G|Kg^D;_dU1'g7Cy\鲐;,{QHl V}f%OνftλG3Ɣ6; ?P6dLZi8R"eW88{p= L6QǜGkz_r=΢9ī94 T#[ Ѭ-PQݥӜ|doLGh dDZJd,ŚTDD9Dk EF.>cO5$P%P$(%`$< OlBMb(!';rtʭ|J"Ju*/1䐖b$#$\" 7%eݢnbh$V4G*#R%GQsT%\8PAc EWMG'Ƿ'Luϸ|Ɵ!/+a#(8).^PIJN:e[VO۔:XD+tRjD3ͬ8UOSI+-sL1,6oOЂ?Ojw4MeMdwR-|Ϊ1ʣkbM#*6-*v>'xmy!gҘ:f%rr9MėHMqf*C ?fEIᤅ8+(?1'#&uQ~>;m-PdX=8z>= _~e\(- *Q=cIֽ %Je y?69m/i;k:qa';i^06Eªhj"2]g:wOJ|)cT5#.M^M1U" DD4 vۻFZGsGF'ƌޔr/1.NgߝodZ -9K^d! u <2״-sMl%(PI|{[3$ ʮcMUKrCz}պGQ9'k`4ǗͲt9J&эIt00uBҴPw-BA r';}w@̘w-"Z%PG2zx_D~qfo ;9I^{g4'A@@!GR;N.g ;N3yMyL'CFD¢ZWb!G|Œ{ƲJ;wpq1$8̔\/Tgw:g3.pB׮5?mnjo$pݽZY3t4kEZg⎷Vj{Fr_pfs=l2~A%t>4 KEA% P)QĮ릉}q3>q(/Nܞp8jzB\$XĚIlY[rR~~Ma nhvΆ`$wU%L!l^[s^?O◤]O/?COHFcnЊo[wKކ/2;cwx,E'9q{ϩs;-u"`+t,Ecq˂ 84psOch}__gG(aDlsI:) i(njZU(q+ ;eח"%5.:.-ֻ%x2ƭc"_Cv. ?#}هYw|n\,%74@Ⳋ,˨*BS34'CW367hOk9%g vbx9uS88Om AjRoP턚Q:)>_SjZ { ab&v:谾~aCWL"s:9O|7xwo(Ut&cucI:Ľ8wPTID kƭU(+y~<# Ec1Vj,tDqns͸p]1_ 8ěIS@d% 3zΔ=ŴK*WjԾ. ,72C,5 anlmmʎFߙ#N_)ijh=ݩva5hEB%a -bd9agX֠QP",Sdc5Qaj `* -ps[/)7*u~H:jv)Y=ڬp^gʾrÉZ*YNɞ~bkp>B.򅅥d ČgߙаY]>OX4KUq~WeYhuZW4 V%%"=(+S-(S iqxBiTJ^!eFX" ]x5܍/֥J)+hUS t d( i#̼=.Ϋ3}V27QNABAaL#[ӓ3RHPv# P)n'Bll:HEG "%*5,Њj'%T\Aq ;T#=[M&!w=6imPT*TQ̕1ܕ2do̶Cf+?_G.2QEڮ)~!L$"6IK,I#,rC.jE҃b(M+@ImfY&tEf1̷}cfj1sǖk$6  m70BEDvPu:9qdKH']Եf0ݣ;*֐Kq#P?JHkhIa4$ sH_uC>$20z08*24YF[ \r/XU-XKTQ/c~4!:zHFFfRUꙂ}:MK/$#`pCGi܀hFFH@via*u!` UcR 5UɌA~rcț+4;Gw$S !Ռ]^#<# ?Bq aG4 M*Tj]l{"W Eˋz >]ʤWxzAǍ":[R5[S֨VD?4|46s_*udMIH5RM/ lf~S֣&iXG1 C7rd*P9UQ}}e}e}B [`kWuS%uTP 0LAiLP鈕.h010&ooe}ѦQ}t֛o{st`etk68p32WsrYsq}Â[5bio,EauX\wpzb`xhxFܳǴV rS~XdX>i[Z.V4 opŖ qop99xJ[E&Yf'kbgɟyi+&b&mTe+:gnrȽ2BF=c¨@^-ET m1MGI,aV픞? ScIBd䜆 EF *`#_B[bkQ':>DUvtZ2oF1V?rc,b2!m:d=thvU]f= ۢĠFĮ*d.?aIhw|>)yM¸0a<Q(jJ7kC2Ymle@VD$%sIؒ8ąC9d6{[qH~2t {~D6iDfD$EhE'[,]BckxF!BiSOJKnT>~+e7?Z31߿{3oGyGW5<+Q4#Fφ Vm.j?h,FcTՒf8m8ϰ삺+NVƷ:>OQ{רuR9?:AI^Гsk*QS*Q)KwtnWhѶ5缮^!X|u͙qηminkclFL=Eg-oe3XfJa2t L[tY` )%0j:eReSL EVd Q!Ӗk[W9\׺CiV(^"=א*'^fY<$w-ʆNȆM J4m'hpw##a1vXȕ  xiˈy:`Y( s +ZIW Ka%/zYLc (CzEst=/># ;(yG~MxZ*%ɆBlò/D_D\K `0x_CF7D].8!eKrS_dby%"xT>arb ^&uRŪ %2e}B`N 2%Ab"`"PKQ">ukyS^ȵBQ݂C^X+2G\V -;1o67kW}n/9OG E ~SG~s2nGXNK&>qa7\O=)==u/[t7o9_#ē{acƉ{IYdvʃ1-(~yyo?q$F}ߩ8 *N ^Fx}XZ-zF ꊞp/0Xuxu/׼<' Pg =tT!jҩ e,_qϸapbo >fy>m-G0v0)$&5q;i|gwzXeg.nmMmqn(Q-hk׼ _g'l qBp4Njk-U)lb?TF^WhQE/QW0W:|[kN_U~+ GZ=vcSxlW>̾avooNEAJm!CrE^)%:D(dTNŅI:Ih%ba'~s/FeS3jtBUI_Q q/(=jS ?] t4 pyK 0fuMщbUB#gvY;l[7M MGxεDF55RPNa1rAw| 3d?d*LŒ3٥ j@G]SY q"6,ba8M|gM`E[.hl6ť梅(DY+l.\6_z/*\4࠽4iTNkh;K[{Gy|_.JiAf[$.K¹Gh]N[doLB%X 6X-0uB_r1=G41F#CwsFFhb)%{ &YɩS:,c) Wc4QO ` hkZu@[9= xN8|Ϙ1)mLM-56t-~+8(h*FP{ X^+%q)QըQsdEc^tY]nwQf*ןi֚V;F 炨@Zit9!Q]L3EsK/1 ۋwcY^ڭF P,°I2w(:UK jsʸ]/9r0hK3cP" 5hMN Y!hwєz(2J'jF_x|"4l6F'gs[zs!˂Doiw߳m;K:DM:DE})=[VyI+Z,cj'PD)%jZ@֐6&}i4ln#ʱBwoΫkd͝/W(ftXqMQ, >#,$asQ#-Qc\5H\q$.QD/4J3$#$"d]$(s\cNϞ3yeND!4DHS_ݰltXVvMFuZTafN!,A6)R[XubaQ7;3"s'CBi|:Ƕzca. E'[w;*_%-f43+z6<ԥ,u  ETu E':'zDO7vUp/ u\eȴ=ڭ#<#SFmh S-AjIORNTY|| LC&"_5po3FL0!"3t6]:3z Jhk||>Z/gXGs+kk bQyLE9a&H`h%>bý;C$=H&eOLvUUSj B \=mҴ|H^6QrܧH5W}>3OO>dW й2.̱#+43NzFk;Ρ"P}UhIJ!,~wbKapOkb@b7z֌]G@_!][Uvc|mM@3.{kZ5Ō5sqa1F UP1T&]∘vC"kipsPjeݙere3LQ)t,vZ ]F`51~ƠVv/w8@ 0٨3qWwGvUf~0rfJ_Ny~FKbb,ٯ/% uA\%r٨I=bR##sϵqьik+$~%Lk) ,P Ի5żc1=x>gru1b!dɺjmY%'OL)7՘L0 0HuQ_Phb'|a |W}DmlV\2oya3P.W 4W\ic8 %%pQi?SZ=64':O<[b+ zƓ7|_&U,>3>O(p2C HES+k-w|{e3jl.ymPHɥAXWX1n޳oݳߺgdpܯL  r<usWa VtkƓ{\WBN 0!]ӊ.vG_e"EfO2E"gwo۔1LKʀ5]⦇,sAWfM=9xv͆&೒mb( o,<:~Qzp;'\v<#<򿆟V쯝g#ΡH9連Φ*D*ҴC?ĴSr c4J4Ja k':Ί2K,r<-MuCG[UtX2瘋K CcPwU kuB9Gőe5n4@odxF5p?Psq~:=eeQS*uK`vOV ]9M3x[%\"!1-R$L u.V BꤶM`)ѳp"b˩: R*AqC7]Hn/ѕzWRM+e.kId貤X-%"; 9%j21lKCO]sszDZXEoo-݋vUD ]m$FEA. UyY&لC!w1NWfegNLRHpvIc--pJk1Z QIi8!O[~ЈÝH! -u;uSLWDJH"l6xD8di"t7+%~s8Ho:&Ymn-]DQ#t(,,ՔEPt/N,iJJEJu[PC5(HHkT)UevjT_V b2.;`;&wj9fc6=`%;hI ;9F?NJRF1:gϿgOܑUˬHWn5p,PJ +ewE[I =E3psh}SoZwbm9fy(n;:e_ aY{ϛ4߷)WQ1m?s6妇9/a%hZI JUPNJAku=#<?_+ aKnFBPd 4ښ R)9/KCrt -_Orah6ԩAa䊆ʚ=垁2e_1施j]49|_?VOC!5]{{Q 4:8T 'Gw2 #C':DV~ꢠP4ѤuO1Rp 5ʺ$شE-Ca=j2:.SD 0oUռC0iQj:iS*ѴA{[ZAwip" AĆF4YW-e'sJS4u>}q kY].Z'\(ľ肺-hk,W>и_/͌!~eГst7לQ[~yNpljImvONBMKi.1 !8jےF;[SUT˂*7(S2ձ)՚rE{K8ZҰ)N@1c߸v%f1hud1kw[%vVV#6`K\mYF&y!DKxa;n:lE](7i7:HU"=K[eetB i!ޓp gs646}@ڬ6r+yffcF8JfA_mi'M@{Њ<=۰sYJ챑YmF!#H@8D%Y0 G| _.zIأ{c1! & }jIYq~whiŮQeM-6ВihM<6S퀪U/QD( sH76>[2 zWoh%8TI*Yʆ(I YUi6Z[ iD캉nmE K*YsGs \};m̰UtDi$fm}dpwg0ۍpN#ocXo4NI* 8^<# UR"ۅE+jBYҠB_]ي'59+z]K P?&ɐ? 9= CԊW kr5{#8/_xʏ V-/ܷ\#5AAUH6KGz^׊EYԕ4~ٮ* $h(Mbץ4TΖq~&7czsNWHku4z~MO{Ko1!2FCfd݌l s ?G(yBM^j$-X-˺W>wŘ '+G1*WQ yQ11n &G+Kq:Q{rS }wVo0]Q{~i"$_bRYcz5jQcFQ%UP'iEh-6F{ʄ}už6!LVR h`2aM,3y73"3P`QU[ݽv2 ɺMJ$  c||vWmD gw'G5zozW,eӴ#+))69cMNY<'/ =Ǯ̜U*\ZJqR  ==#P[W=0_ΆDT$ٳ>30v X SuNCOy>B}ħ%ߤ.;kC3+0SڤhnLM:"Z'H`J?r^spp&nYˀ>-6hh f> `$`{proj h:ը}`#3^<{K-lm3[;G5#nkbl0qs_O%\v <w`~QbO|"rMICFȿh*O< Dm $U7I)v!H~@n@.-<=sng|yy~ëVȺѶ}ŠAkfNgI "wH }2/{L3cތh ٕL~<9>5Xrtu;-Q0 xaIj }dOecXnIoNIEA^:ŮXK!,BCTt%-kCZҷ sC9zPscf cE-kb);eE=@SVR8:4Q{hEKن  rrl͈[㜅# )KH]C)AzP"d1┑8b*S Zl]S>Ýe2%e[]30t%^ n;/8 j6zTv"aB6~qۻsm$3$ZEkMyY5 ,+{:ћCԘFeVeVCP' 3;SQ^aX5EbiE>uGVK4E;<]58*r3GD:(L+G)2Z%G{.׬;6pB{\Q6ﰏ}&U.g *(-4R;,[BGY`G)*&}Zk1R}G]ʞWDWfU;6.tY 12D3#o a:N$8c%Óqo(Od>cubB2zԍNXrTK|/4B#=ViQ~Bw+m1zkS%5UnSW !] -yXA 4]1tjà&myRЇ5ձ3 l[+"]a`臟nk Y%]m4 @nuMjߠ $:Jh ݭؗ&E%{ V [2 +sWbKHmduXfA /l&>E{aaO<ϗ[7cNd3CD50?я<9 OpYpg8{-K;E1QƊD[3 Wv{2NL' "alØm B]bfsEo`v% EaApqk)pU!IqDǠF! HSK4WwvAgYgyJ56-6ɤ $|>qņ-1 c֡J0^QqQ!4UXh![ޔ9fYqv8?fc564VMk]!hJ~IQû ~1jkx=n{{t6 2`z NBhVŰ;vƥD6jC(7rC/\;[=z(aftnQ挍fqedlwXh].QxڄyKmn8oz{,3nS%F"1 vU tf=/hΒ䨘QfPjQ @OK88"4WXeuhӳ\R1ΘŘv5즠s8_ fuÇ"YLؚVs I0u>3 aFh?nZH]yEi3`G( 3ь3펁9!rwaNJW0U'SbsI4xpss~*j>zd`At.돜~cd8MfBg ٿم!FSsҿ{)Ɇj3Dmc^1LgV%WMOK-3[vBM87L?r9@hR0h&udP&eˤlE|'YV|3}IefhGV$IO}ƧM-NNXɘ6qsb1#FℱvLA.9]5ucSj`v!4F?)REd+iHޞAМE(G/"ĥҥ@" ^CxfL8׮QaBϫ<տM'x'u M ?d46IՊIZ1A'$w c~o䳀c:WK^ gį΂0;ܪDbbC.+EU4=CPuEٷ(Uߤ[fGLrq36.]~R6@q~+¯ Q3VG6%ΗqN&: 4$I3st5v\0<1cT v)2)RgT}ft(p䜃N2[cFi&94sQٖMa+k _|n0%Є++S뜴ptGK)ђ;7؆!HOvG7jjC'o\RK8o` CĐ%nPxQv.lk *eCA#( 9 H>Yw &:"~`WᏸaWUFRLK~p>}BRdJ+XC}³oКl'bicv+cu+6igilYf4kyEaz4Q چWx%~zUbkQAx-$֋yiw!Q[Yt]^kA./='M5cI|SV_q%so.n x6ĩvOZy[̰Bo|'0J+Xc!s]6oM2ҽ]r2jCe>̼YseMi05I!F$6=/,s~e;[i|gp\::cPoIH#EiI2?`|]sY-Sf9c>wK{"^cU tq(윈1X9KWc/G##Sw, ܂W .5k:|~g=vUщ.Zf цa8B`GD0)msO<OFW|/ϘG )]}@קdGW.cwXT]̄{X|kaC HN©rV'Vm^tXri2iujfڶyuLlcfQЂ#ź11rA+,8# m̀;N#dWf `"G ƻ܏ϩmil-t[sBk254O@SPyd&ncn.ItT+6B"jB; ٽ θSg-OݞQK;R&[;fc81i#*UCP8pQ_sV(m<јb֒W)\cxp54 3 S9.x:kEd˰Ғ[DA@@!]$}ciK>s@3}"p$l0t‘[a%mִSVzAh ΘZNY,>5~xABQ[̗}funyߠE7AJdo81yeDzIϙQIk1y_%3e]k+|=!'kYMzN2MkТ=h0;%wxcFN ;4j4*4U?^ LyUCZi>Ws?RQ'9μMn䏱1vc5ݐ!htC*޳,ܘ:V-lW-ʥKq'|_|E\WIof0Ma@BC!@3!p$ypHSPl.ۥvR& M2д50mrjm,MhX&ީ= 9"{M,ɽvʃ~ʽ:cZ+bsÕ/̠Y4c92x~/ak2ޜ|ԙ@8iGg KK;,u ">g1wJr\O5ԕ}~<ǨP  h@ 1oN ۫elr֢}KۘQUX>Vɀwsnw-ι^pВ;zjG 9d=,pPU ;-6͚hS%]ҁCyLp$VV0|G{-( <c(yܺN\4\48Qk 0%}5&-|ҧ|f!zVM긌Cz朞9k- ✻ܽ@+$?_I[Nj]vJ9&8azꔇIn=Xq>w`vf`%FPԜctxXqظ&SnEI\jiPIvɪ he_[aŰ?u%w;Bv Y-Injas-MuaQw6? 0ku~C5vN)W lG/GzGGc"yƖ1w; -~O<O| 9̦"R$_y#w\xyߤ8')o.UbR.^,6ӣ>Ǽھ\>kF`8%_x,HZkQmR* ˫(KVtKٔ߻uDl!d Qt; >뼥 O\veңuz=|~J !|KyOмo忇6?W͟Ė5/G Eo{P6Ρ2<]-{L,т r ł~W-Y]fVFLC~xK?dz@KQH0 9yL"?}8b6ƒfu8 y-=ohȟZEo(qւ 7=O<Ŀ?[:o{sO|EHWQ%c%ZAv #",>X1e#u).EbS&IW\#~v{hs~ZF7&1868Nqr`PTav jˤ z(FasиYV+D~z- @<. Y{zNS b ffRNuJe+SGnp#B={~B 1 Mt8nEaaY.,^(}F݀Jwmo,k^t+Ba[hq3얳;;[ N|#b_XraXĪ+pyI(?~[%ww;jL_Vȗ5'W`XЪ Um5{T#5#|z@lEĸ _Ψ[:MӴ4Xg֏\;:ن`aJy6W(S XM!taKA/ㄾ;NK.;qsn242Fh%X]Ee%Iõh14Kvj&>EYk(hHKrMraQ&P]!7]Vu1 raW zKQMК͡%Һ9=rQț=Y2êj|2uiPLv{>Z0 {Мphk؛>W\@23L!S12 S0kE2NU-(9aYu)n6ԆAidYm&ڐ2e@C93|b=i#mjkPt ҷh:j(*ڥi[h+3#C: -ʠh UEY?VDb(lkS'[`C:|2.SaTf6)60Ӻ\s wTVn5vg-h#7]H"nW)xL|!ZlZɚf7@h ^׿`\Sb'x'~lh}`agKX"R|?8aM`.w UQ?+.%e}ZQwUP &O.evꮋ2,IvR,']dp܌Mf'!d|<N hO+ӊԑA{8st)4 aJl2*4PMʽDE~zL3y{'5qkvq:zumދ(v$G葛Qy{Zњ솯oox.$1|6Th6Vww9 ̰ R-qX\UW[X{‘{N[$rjQmMXT+نN b`X UӷYw2#إ.2'8~x"- ňWPJ.zLŐ0T7=ʿSF㢍6D9#:ob|j0>6Oe tƠ ,JߦMYvCTȶWqB[h 2qGfO -mI_PYmc5ƮƜ(^F~i3<Z@ (|BXM>ȕK2K5i RT¤0lRvZ=VE<3k>i5{N䄢q5IpҾ# -œQK:n(pUU2c}C9y[cݸVH`\׸^e(_Kf,܊sbSYdꠍ%ӨenRzu8uԍ)#D]jhD0}nKK3A3b \0FT2 j鬌c=rˁԸvJj;|21f3pDq~M$Lv~k%KaX:|vmzMma-p-; )kl&?&`0 T2TuX$x'~ +,Qoz]D >w5]$VcT8] iTZM6Y>.,Hq,']-tyh$iJs\/L̀^*^E-v #*bCMH4Ji#]kp{5V}jࣤ BğD$Z*[XS]]unoa89 #~ fUGt=oA^x uZ]+[J=ꖿRh{,-B>-(=Jag$\\qE=Mq]cNjXzQDߋ^ٷO~6At%p"ni9ϵh-fzgYyy?O>xW+h}K/w{tC[lhlP5Yc5FƳZ=&m2ёF(MИ:i{gIp9o9f9|[diM//Ҩ&şߟ֣㇆ ܠ^( ˥[(]C)TW0=^7_o ǭ %inNuăqăYP%;!qp-A76A8XaVv..x]}ߒ &kȡ2KL+ǵS<+Ag6B[;hKߝz/O'ŏ/w([jzrx'~ m*S*&݀i¢ :ԛz!C6HKL᭴jpj -cNlV(b84XK&΀HBX-F#>]c\ȩzbv|HҊ״5-kM(TEYZTIUY<ΐv1mpA5)MP{Je}k Ȭ*/,`qf}, ]gk8^=R: jy8_'9`(\ Y q-çmFޒiY*[C:?Uc3n3VوBQеIhT KDĩfLcZOw@U;oy\WMM%MDMl8D̓qg튝A v$vd1ZPvqBgj"Rgv٤JL`9x-aku0򌾻ޡvwFDr4-!Vce fTƳxf8 zz+ˈ-~6^cF+hPJwrHu0sWeea G޾c<.xV1P)P9Y9^)`ّ IDAT3<3O ?S|!ն&( JQ#G 4GB+$N4US)MA՗%%|Åĕ@i\+W\/"$:θS(Rs pedSl%-<@.j\#[A$Ѽ/i(tQ  EQ*u$#9tWj!im/7DESDTt Uaԛ2MOX}6;VO;<:[k?Z>s<~;s.4&ģ=:+&u!.ic0cʿ5)*&a_vh0RSgVz|_9~Y4Nv]/\/1'?񷲉.LkN?ԿD?1X혬\loQ%,QÒQ⬺"㟟_($/ zO7TlyRyi$62Vl.[O3.llll]v|]dAZÍ{Ƈ[>ChNѴN{t>{km>eYd.KʒcJmc˯x/%JS{\FH0q+ܻ=rb2N+ި{njq>?'XaB_Q**Re#HJ}HMHcn|7|Z3M%pc)j#oJxj]On0T(^h_hw|oG5G136朻7-=MTЏ,PUB"7UZ<3R* n9q޽Խcd,KL"ښ=U&.e?x[#MHW%g-R?0 Vb%R(uR$]<_#H#A:&Ш].d-u8"[X$3ѠoQ c_y0 O ɫ#RDž8\}'z|ҚBs3Jgyg~yb_Gq3y"4A۪Ǯ-zlycXh{Q $j&|+*PL*($v-$$/u$ԺnbTrlFFXJPYsh]_K ;HmPe ]%tī:zإ.rb,7clh5-3I Zj&ZfF5@TөMH,BWQWMɑՊFX]Vs[>$L8Ɗc:Wie{vіOL6 dn,:y/mM(ĘDAl.bQ2|CӥMJE]GiO0<2t5Gw2t3GvIabHB@ }6Mn9Ɛr" )z22¿c{N[l7D)%Aatk#RD%rY#^1ӑBĪEZ$Ş>nCz̮쐣*9f!cZVP52fh"PKD(R`7,5ؙ]ʄ옉DIc 6p"< ÿVi%*>-9999!UF5Ab%SE=zEIJy ~EPPd E(aŠ2Wt=';NFwTd(jA[3EA5rnKjs{}N/}R$p-(%TK^DeWHyC.MEtC*dN3sbcQw)G-=eC/dԥ,Q.dľD*+U)w 5vmo1߶aہM&A KpnVl3<3l~y?t2F:f-ɓh[RݗdVA712ftOvtG;~ЪrM-BM m#2nSV0w^pj6S6-ulvV kN'ƚc{bHj,WE]%zrnt/qZs:u,[ j)]p>vvQ.{u{]:;b򤎙OFT#"V<@x1K{6{\kOkx+MMBS*6mY&1XWᗻQ$j[m_=nt@7AJkc|f&a2'7̞N䊦/7z'34VNIs,r\<wLsF̝ 8au,>i7e48⤼笸㴼g1sc#\g\1^.u(mΈ[_$NhCr$Q"Rg[(nGqF9rR"5Z3sE~BÌ1IK/2/"*4)c$o9TCu-E$iyrGk0x0֘U0n5 \m靭(22S a+w}p'm'c>o G#03N?k0UŎŒSG-. &-4'r"uܭpHbʆygy/V⿰*|WtJɒjӐ>Ԥjưfښz3o? AcY"U I#tƮnp{}w>%p{2,ql?pvv;ScR%m;~c?;+yK8s~֩$psohK>P9,cI1Q2!5:,mu੍?ocsŻx z ʖ0_sr!2;<[D$!cZl>=sGGoCNkȀkiekٵ;l[)C ?n܀DZzo?;7[}Cf>e=}ˋO G+$e%*v+Ҕ)MTD6 TcFyQp> Woo{KhiS,5tŹo##Xy<QKO3qXYcnK~Rߡ_鿞-ɣ#mvl lc5Ajj"GI+DРFۍ8;%{z>ij́[hnA SԣwgY0džbinxyT;MIu-2~1>Hѯr_帧=w]pҺU˪s>qC^o3Zo=: ݷ[2 E(\إz^{gy/Vٱ:.41!hl3 9;98V2UP2EQ4*ys+<3y|f7 tjWqt:D5G}gM:=莠ƌ5g-R]-2Ug =KvS,aXz$F5+yp05 ̑X)U\ֈ?v7.ރEoLv\9ge}EBՑ0F}k=^6w=>I)..+CVqۜq\JkF8-V&jR%h84Z<#Q!W$HjJʹqk'7Rm&":&S)!3& KsZ&l&$^ R 9rcND"{bKyBG1V77e@c`rI#?h1 jG;uP]͸丕O h%ZQ m>/3Ϩ= +A~Y}̵ôS@V+T@+sԨiC1VbZ$RP'^%[$K(u- (PI F\Ej?oQ=HeKl)-qTӪ}G?uv,!=3E*QE*Q. D rQV§oovy|[G~']CU=ILy*l;lQPA?qODwE-2ĢGt- Lxe0^iF85RB>P]ͿWUZip7Vj)(CVZHoeBAHPWa^gyg~bC?_O|sGaLP0fƗI]nsaAI2P(sPBU#;:ˊ &ԺD*JhCBSJ}`-ս*uY,{CV^[$I]N%Hf3Ԭ$dmj&-}R?Z3YMD A*|]ᰦ"$7(R&e(z>JQ_He {A>\WVՈ꘦U;Zbl,KF,Kd&yGeŀ[79jLXv]`vc12+cy"XZF(ZfqMc{G:֞W0\} WX},7r]E/=v^->ZA* KeYDU;^RIpW0VxM+8j'&GӒda1$+>9WTD|jbC./=fw(=M+Kџ6zS 9)fTǜL/OL9^tAU+A0qٸ]ySgZHDBKE(z/6^XY0cGs>p5 #m&=D"b0\1Z+z(neOAZ:m6VbԠ cnb:@]qdV O%@(R.2LU3D85M TPO%jWl+ڬ!ℐ m̵vIx}=Kv4ZIjDI5#ng_޲z8qVϣl[~l;fbfFnt#)s"Sz-JϝwO;V̀ [ꞎ5ֳ1ِ).;lBŭs+jJd48~#W1?{߿5 ~[_Apy!5L7dQRi a/j= IDAT90%i0;ȯG-=E 08\D Rw6r:l5/, Y"\`7Ŗn^)}ŧV1mgޚ9RbO'USN1Éi;.6_x}MlcqCycs\(MKhvxh5lUcNc[q>2I]*I#:cg%2h^xgOwO#&o?w8ݐo|G[Aqھu3-3oX]q}Ơb^0|`Yzp&2ro7%7 ʷtnHݰ3xorst0x0ngKŠJU&񐟑I_BZy(ϸWH] sIިlhliQ {ar10cX'Yď6ɃMXڄCxP\4-jP]P ]|%"fW_--N(2ޢrUOR7#< 9E+B+D0{~<3 ._AA\ME1R:5\{hւf-Oė:OJ}bnP\ @  "{Z=Z T)5Z(%BQ({ )Lu(}g b_!aV~EJ4L)ԹDUɤ9lg,!7N0h$e d[][EJ9ҞhepDP^,y @3Kz*%B :_WZH 6jgGR {oAu#(!'ٌ&oaa56Fn*ՊG'?PEF-A#CLp=›ED% fqEºgMTseQbc@ rPS]r.JP8o ]WFlmI27<ԧ'cĤ1Uf\7\_a{LcO)6T2՟oʫ.)BwrL=vBJM½/\Tw_k7 Yb- X5-v7vtӚl' :c. KA- ] pe>)Q@[?oٕ]fLVlyD2-fLM)ESuAvxmz-g;Uf/=bCO߽Aut0co{5l?8tR`Q W|ڽkH㪾T3zƞr?Ï?9>E..IwXcf)V1bIO_|T[h VZkJ kF$챈y=E! d0i b .>)FtW1Wgҝ10)Bwe\s,'ƕ§Jn=3<b6xا:Y}v~$kdG: |"jRb>jDmaEXaL5O  ,$1,,U= h rvzrQf-[5g2+.)̓NޠKH4(䘤Y~Jj/.O$O;2.9?]1!=3bݙYTEe IlW(orR0H@n`Tor7\ǯw7%B A MM=`S N%K6Jh{:g.8kbMB"YgrU!%E)}{ťж?18^ v8+G'lY&ܵ.(Fs>FKZVhU[E -HlEk;x^o6 5d`FVmmw([=k{R*(N<IdWtL7>3r%]go埑􆡵/0̔!MR 2CF&m-N&9ʯ sR8S[!s)QP BhG>0okə,={Vz !E[PF:.B[ܤ1=[<s- #x#OJ3/tIDkq슡慴1ӆI% R#&D54$=FҐ1::TMѡ;RafT+FŚ!yG%j!mA7{ )QnK!❇9o*UG%B-=XFԠt_žk\ ,Ysɬd^YOms$2^/\<2;LDz!Q'Cd2rY%CRϣV1j`k+T͙t+Tr7da~&9٧1* yb&Fg`A;vK`PMymdqYs)f\R3,1e2n~fΊY=vv¶IV=vumPјah`“>h4bՍf'G2%](NqjjŮN;~8fOS^}='xzF:m4j/fx1lY GFc$'̏'`U:9Zl>S)\t;A#-y:)tBu8 OX >CA V\]-x-缶4χ)>40/pa`[+橕9vD:)QΙp!D2Fgygy#xXs>r~f YuCV݀M9ۘw:'>DN}W܈;;n;^w+̮aN0W5)WD]!ƊJ# ICD"h56ݗ⧔IC ~[5+ںm ڮU[:%:no?2:&nZlF=/oyH콈t!эt4tV2ԖA-qF|{o~iȽix+莀,1[c{ Bj@}tFӑ O?|us](T2cAlG}\2GMIt΢nZTwc6EOmqDJqR=7?n#:QG ('Sn_sȢ,!S.(p1TŒN@\gL92ES $A{fs|l'/wSNu/g kc>ۛK ~_?6j+[,ߝL)~m$Z3NN_t@kA$Îx~bW:D!%}[=sC~6Y Kcx2LX0ݕ~iiNJ'>n~2~GB/ ~dy xmԡ ;^ᴘbCORD?pb Kњa*~8aOo}!2kxΘSƆ }ιy<4_ t_cm&|?G~AsU{.{.~k]DO[1~j+.#R'7 A> G= FF?TdJ.3obpR>ғ;F^X#FĦ병}6ϦSH:sVս J:Zl>Z.AB7ӑ:LC-5E{4. T$AC6 k(!o:W{~OaB:T逮/VŚX[gGd JB#g/S`;_5-:3<3?.~/>u@P* Jة /϶ć1}5cz>l0cψXnG }`h/+w1 <sdž!\c"Y#Vvy!Tbctʠ Dj` RXbG^?0}H"4HƂ:t 6pCl'F;19p'#,c}ʝKw@("b&G;`#TG^0X;GtkZ&Aڄ1Sc|ڼ$S6{{R>S=容&=2'u|v  |sBZDjabz-fGZ[opԅIԩIӡC >}? "E.n'A#aayS56!w$;%`P_Qd>EOXdΕA|iuc15c|Re_c_/s=#Gç"H+^轒\͸<>RY #ܳ#0kjYErgs orήS$a|Q@k7dBCR#DK:Muq]{>06tɫ# @A+>,_%gۨLLs(Θg〉rbs?% $aĤar_ܐ;|QIiTCR.=e@m8Bqses϶sRumq ȝ!P:>'%C c#ux<j2eՍk_I:jTK\>r=r>|$SMMKWTPX.ug\M3 %δvjAa00&*Sr'#nqj3| 'cޘi9hCuHZa(LƆ햷ڷ@I 3uٶDMʠu +kBg z-*b?=3<#k #M Y_0A#M֢&B z^{b{O|K_ =/ξlRx;):Tx0Dl2{R>p-o%qsĒ Fn~* [zRp8W3.=Oј3f1T|ZxV{`-ya0Fvy(+7P6őKj`c%Yb%= Ra-u[je Պ~w2~*9rј6S,~·-uoh?Y-Gūy+Mm 0)t_D#o_ros~ V%_ǿ|7㐽{/d`o%+tq*NFٰ2uń0S*N]HH]և!t&=&()X3|;,R-$BZh٘r˚u{2="쳫4, G;FD||ߌI2gXD#y;fY}7bsL\HWsu&ofMJ[s>} Αp6Gz״׬58u0S^\W$_WR)]X/Ư=?]a5}cõ~/.x9S6 hnyY~榼%tŸ~ IDATR:m>dc+7aogygwGk\ Ρ!BD BM(N+;KК:i.`P.ۇƣ4L4k4!k*)[*iJ ٘4EEe_;6Q(pFT]AޔDETqD{W W9O|`opbs_cW5Z ڗ/]ufԘafvϠL=M9 D# ڦ̢ZW#i㝲؜") J(~ګQhAh&eSS;-`9-vT/Pa%5h-mpB6 X)`v jfaz8e%&И&] ]*Ns8`q:_*1EfH;BRFo[9][5pL+E*(;,&;Ōs1gũ+reCl>{-&$%aπL<=-b9pX}i|!:#%q#G*ߤغ[s,thDZ4M")Рĥl=ʣGY~14IC&$3|GXFMRь-֨_ZEf#bSTt]+c_a z'htHawYS ҐbS= 4Ka5~x$?x w 쪤m@jEbh ֢i~1LCQFؚS ޕ+Sv.vycڸe*eŐ 6G,ewOdf@iz44 ʢ&MAi?W3<3x_&T1Q}iZCٹKә(ѣn`=AI>2thd&#vm Ǘ>8}9V@&QѽӐKhԉ>1OYBenj i~x~Sѝe;-0IэÐ]F\ ~:_yH7?U7&ĉ 5Jy%1{t!񴜽govtIi,1ξ"xpR'ox|qA&(7 ޤ tFlukHԞX"/[?+6CQjE "7#1)txMS-%CI|^1HI[9;9q|}H! O^w'$J*Ġ;%Sd c6Vs4Q) u-#!>xcs\5-ޖ^#c^2̖C0KϘ<<#Q3Gv\1<.KFǓMk̶$+^S6;8C1h F+ K#0Y1fƬ:\ۀYl(Nϲ`-f`Z<:fQT=|69UAlnWL%/>=ɀtUǪ!t.b</49$(]C343I &l-,p,q'G8eR.rmЭuʠ[ IG%J~ ʙ3&#^b|~:R&jBzY.0AԦQUQʧc%tRC[(ABfxw7/qBg5vB*"($ .@[tFdR7չᢇu^c iPA$^řur߾j6Gс}=|I2gmt@R)L9'zzoZӀ7 +p 3Т?mEWH14(l yj[ ~Z2VŒyr9 F5ÂN$s+f‚Z0eNOla*u6&,Is9lS.mOg6ё텆h1ӭ0ìiRi.3G/Sp VBIzjEOߞ̅oY|2{~ZϬ^w + PW[*%S&}=k?9b>^kpw%ҙ=ƄV\u)남N P6BHn)JPNqlBd+{~Z,bS A4Pw:4 {sd;͘#9uP_4M~kܼ]K ͣk4ˈ͔h'3^kyc󁱵pjtjJ,Z2J0;\"W$BR? BocY^RDCk)ג<3<̏šf4ZD&aRKՐNMHڄ\G[~Pokƃ;,+OWS)ɬ8Z/I-~j dIm@::Eq~*K٠蝅AU2E:eW<,.ЕDN渱HF"UÈ-3ut6`ՌXGl}vۧ>f!8\_#nC(+Q*slS:Qcy-F0~Z8>X9#9c{KxbBH͠3uۤC%P8AAl1EŰyb, rPdxKEfH *0K8 mLu碤A ;̤1 H:{"zLFDŠ<$l(z]6s625JhF:i ,0KٵD5us. c}ߕ StF&|Vƀ{Mۧ.SngA~Ge\3퐛i=ՐY5徺ssW8z8ME[Y;Vوy5徻X3K.#MM9`f_Ѻ,dmU` ): "R)ʰis4jl`P`5v2hM[[M^2\.^=J}5ys^ϐe/yo[gpX<1u,6`mىCbJsfҮ0-͞0huV7NvY6 IbHs5&+]C>:~z,tK FD!2{([+/3ZSQ[dV5TGKXu1s9FaBǢXNKʤ4ʤl4Ejj"TBl!3&cR -h ,j|HhH:5&rBh`h0@%VHdya~|y+czl{-& %=m/@zXb)N4jtz=3<̿}~ru:tֵ)V*<-5FI>p[9&caȯثfþT# Lz-xKo! S4CtaCSMk4%fs0B!K~El UJRctA%߸1pd`P6Ym,3\Tpp41ge r;Œa<v)Qݫ>[cuvRz|=,>A9vɕϡkTk 8qq }aI'9V")Uؽo{(j)E峊#aC%⨱%& 1gGm>17, CtQ&;#fXC6g7I]u3sSP]ak17Șdu@4xGϓ7bXz#Vƀ'Z(]Jr*i0TE3<3;5~ۀ?WDvQ=k.vh45:NۙE, Ծk(욖m^O?d#}Zf>*̦5-g&sDZJ"jXxޔ{O{()h ڲ9O;=QeʜH3/ ~1~eՂ\`(k1`{8w>hb.pÂQ{k\{?Њ Qr#'/xqxɋS̠MIi'|Epkk T`F 5M'&QT?Exr\Vr9)d{ɚ d0r@V)}kŽɚ!M4i##2lra30' vܩ]jul*GPقԴXr̭eY gWoq9Ѫl=D L{=4Ś ٫xSCn]}*j\G2f}xus;,.2h.ⰢMx}063üncޒng-  sg1=}%?oG]qEܡ$a+agԧ?_4c^ "M1(-Euc>7.:Zj؞) lt47daltvjdθqkGWF@EW69htnewXՠ`lBhAXCX.w&s|MÈW%\WpU" o>bxFłp뀨e~1?k63v1֜J("m,6Є}itOPwu5 7K DXR9Q4( ;cL6x\ysѥҊCl9O!锘ܐj;=]j-)/s92\qa8"`|M[Q gI맺1+ {{?FPUIC0 Re hj*;cOluh6z*YO:=}jҡdnr93_C}[jSۧv:r\)sfUFs%ۄip"Z6.Pҟg]nen`>r^#m2szl,Fc"f%3LY" CxrCXDʚG=W|V{Cܼ0!OTA({xZLC.r >QۧטGDW ٪ R1}>^%;f;R@ZK&9ѕɔMe(U)]kFҋg83Ym*OXf#l:Csc IDATB.S31FcP|i|L*j$`?7cv 5I0".1t3:C5qdSnv_t:P([|t?C۹S,QոŲ$\.f椡&ւX+bmk1P}~2 >KѦPӤ||ռgMvz@5~+,"؋Ȼ#-5/Psk4zgy晟?_gZ`Z#5fP`1 /әKsλeZ`Y[3)G-3ip~AueBu3jbr; G<+c߾g`Ͱ휶bǾgEᘔʤ-V J:z],M) m \ DRvѲE*å4,rӠH%DP '+^9;\V,]'D4،:l&~l+Vwk<ٚO1qww=937o;n;o'-]M‹AX?h">  ;}Ǜwb2^N38Y/K:'hU+3|B\ xL1:ޔޜ5a43*:S\{\\= k5~ՌS&$ xsp{GyLxUB'\2=r8FQ=5UBP[WǸFej0dKB[HLY"m石[73pol sUV%m㯴 } y7rѦ)R! m͐H4M3{~ٲGwwNgg= ]]5iKg|;6s:tïL޲۝`*>OƀV47ks^hagy+^gqppO+0 6 oQab:GF )ߍύEg}j뀘CXuS8ue"d?zkFG VNͨA2h3sߜscdW}:\ywiBv8ZkEImK J|/E.s!pu}m&sqHll KbA1{`?2< < YDIHd86~<3|? .`mTfҨje؍J~ 6ef3]V q冑+?Rv3#o;̺JiE[-=ëzs:ފ\+ (,VlGHDl-L.PU@kAPA5Vgxm矍?>^a VI1 ]VmB]tJ굤%Mox/_y\;;RvUE̡w\̎n+ezʑuۋ{Fzh>|[|-2eJ@G )/[^ȰJ􃤾y/bA۹S֘V'V i ?҄ ' ?mvM2lsRlmցfȮ 1>xo><=b~g~gͥOWMc'ݏ|^#bMl<1+#\ z^wsrno͇ c>E+]6}' ϛܟ3|X}qwE_O6$V_^Ꞷ\Kr aMԍsK_)VQ"R@&1tvV^w:\͙:r:{-iMܒDaJxkNSؒBc_lKlx#.n.7>Gv_Gr;]]rS소^>)GGg,d2`H3<3gk'{ "D; aea2*G/ UM$$ٵRS<%<ɣ^P n['|¢LP&ڣhYTm 횤h2UhT Y`0Թu4Kzpjv$i;h~NpMa~)xC~X%hs~Ig"#V[|<{MyȤGj@bh_7425HMrHMڕ4FҔk\RVJXmVuH< 3\=(& B񆽯nȮFuj*SC`xMu &]fFaftXMaP խukVIA;&Hɶ#~d7zXWN@(VΠ;r `9n719NQiCɂ7u-)[dfkdW#6, +Um"l Wh\9&WSg&hy-f'1%كMh=ڤM%qt\B$\KBw}k 3R%{K[dZ2D#Yz`3Mp/#.L,&"_'L!R4VlkS0Bjvъiw uaVM6Ej(jgyg?5~{{7l4iht 1nSWgumQO') cZUa%fuY%5l~L9lM7a3`m^VX{#KV'&u_R$+X&S5.rnԱqs&yoާW#:d2ktFAI6$bMFJKdrejJ%I D9D2`E&k\64YowШXspsjrbAn=hl#E{{, 5Ft l)ִ\AC9.aɖ6V]oLkܰ׸a=b RP "n4w^MDHrm=Cݐ$>~d3{$:$˓wKv^@J9^ޢ wP_R2ۦ H6UAbW8˘3״%ҩw  ZڷmZlpꀸ&t&bă3dA'mX=17u1i1{bE=40>(,663 .c. }ǁ:qmVKL",01pIc6+ռzdh^[h")R h7Ь["eSqb3}ba}!* q@7*$o<3<3gm_/ LYO  ?ZT鶀4: Iы5b&$]bf>s9j A~DkH֠V Oj-U}>oC[PߎhX\mܪi$ EBswosvsQ I v_y9s8:ht=.o_ryw1瑏$*F_* H1F1iKs~WߠҊU^A5֘YI`Eh_Nl#fk̃=Mpi`c93a#8"Q IFzi''k F4HUcȒɰMưfEXS ICbВL$%>+Zl0)hbjEikQi}B`DÝZ4a 5~vרQEZ:?IUK,&˄i+Q|![3eRl K)SG~1oֿWo_yG~e֏-Ab%ƺX6!w;,m򩅙ńG6cp=;ܳ%<0 E%A3"؍h!/'Bd*<ˤIDv@eH,+EʊǤ3)LcRL*i@gj$Ic pG/ _7UTKZ BaTx* ɨ<Ʋ[u)%Ea`xb'UPy5|Oo[g-ڷіE 6sSs[HKf 3-V~=Z.z\ގ~ǘꦴOR6k4if1.?8n3{X߳o\r79rڸD8ǜq̓at)n +ʠ򡢚Ti E %n@~A6оI*fyao5L0h9enS6EpЩu<3<ߋ3p VV 2(HikR"35 4ҨQf4J))3A>1CWIJӞ |'}W'\xvD3[tV4%fhl5o %qm||!%cS CxyB7>bl9rS|wE$f"T ЙBG =(QaDFQаִ}0d9`ftY R&%Lg-UNʐr\rԼsq\ZEK kϑvE+XT-VFڦ#BU5FU2l<´"ex2E:%:CMT-$ۢk+ ENU+t)!OQ^^ib3}LUQH a 5div+YPvبB:a5a-+,K2lc?hے{c;$MHh7xNB_rydbRl#c욷Vl"*ȁ\6jES#uSQU[R$n58FFQǣropi%;Y( UTȢBʊdtA M{Լ86N9rC,F9Ld¨`[)0(PJs$TI[.i.)*ZP!)0ɅVTh[@%ȅMT4(#E\LQftz ,(&Kí&rp>~V+fU-1k*u&Ȗ%bM6-qņ-;[vwXv}>}>pr4>btЉ0:S6 -]2סY4!':7ć-6yT*R)X/ne̴?V/~˰{)8h \% ;i-gtkҰ#+ts6!O1n6g/y 3:,=k (5T%z%)sl#A2ig :֌΂vwAH.u`4NHBh%0)0eP (JYu=u6)&QyN4NQОp0# ܐhl13{D^=̱Dq^5j\|zEBc`Q#m/-ƇE.$JPZ$k2yԉi\2Κ?E*S"JJC9TR>)&kdg|ۓ+> wQ]‡Ub0TLm~lb^ aOl"٠IBQyXO|_;F9?=&وalV,ܦmj/̖6.#Il6eZBS7 VL 0$SJ <5~憆h|2Qw IDAT~^q_\Q&7oޤ⎗_症SZX܄L<~w/?~Ez|꯼}Wsтy2MR$O$qy^`|(1? TBȃ_c6rL/ŲS,S HIAD :8lp>% {={=B?`!, Vq] " ]y+ϱTkE6B7%-4&F "gcos~|^g۩]"v~1&A O?HԺ5G{gtnYmE阘rֻ52 ƔӧHħM8y/o6>'M;4yxj5q~yǠ5%ު2$eCQjIiH!0+/3UU=S&u,Og=ZZ"e,8/9/9.\vxCF4c/{ɲ^lfs1<#"ʬ*|[AZH%ԈZCXX 5* =u?L2#2ffvթhDQ?fWn~e"*C2Eb-8,h!M@JN)Q%&6"LE R (;L8>=B\Ki .}ҞK"6)wDp+D@NNAْ)Le3QA1op9h %. .ʥ-#֘>B.0N~r-/f??c4q=~~7|?o_e~7Qq)m?s8ɤw$Щ*kTQSM!0Ĕ"/;/3E@h(H=g $!V4Ɯnck7BH+*e(.q1 (^JPɦD&HJZeiE-gpwޜݽ+p"@*ZL6[.&(uC@hFdH͒"sl\nv$R(eE5_#j*R"U)Q"BU#5bX!5(R2.#':Jcb~h//2/.>Q)Cu2 {{XbǸTDMs4!C +,L#+DʅBy BdJUX(=ʣUϑFc"S6%`%FcV1jI/ɮ/Jj)A`7Y^,A 5:BQ E"@jBjdD*DHTB*YC4+,+gL)Mʄ(M DF +U4rE@6#`4aL:(r5h+l5Ч)v%\sxŻGlۗ̌SLh#Uu -.]j*jF rA©p"b!#Z5f+6W$D t5ƒ7 "&uT"CEd S٭._)&1KB'ɊlAqBS6d lWӖ qaGJ*TDJ[%meAw:z|/mX`M YX$HdԀVhFjQPԏ@)$Jh$JIU4Me~9qbiS5Z!Rڒ45ū\]B&1lhjл5r]%M@mUJ[P2B)k*^lFh/ڄNui0Uhȹe85\6~2H L*P2!9zщv [WT@8L rd: m pT':@Y F(:ZhV Vl lV(ui1BL> W!e#.\qW)!kRIóܭ9 wo ,. Bl[ dTD0H&ܠET56t+ÑC˱|`9iE){'zDٖ9c1ao7XTM$'csA"I#w" *M{ǔ(BfSBc -1!;w=2ƭָZS#4O K d lPĂs>/#B7-,ZŬ#V9*^Jی!#oz?v6 sh@)dE,&?Ikb,p* :95$AOREL9*_X3Q{>afH^iB A |na';\8\:F#jQ6Ef_wr_Nz)QBsn!K9sPsWyM,LӲϴ+ڜO+\r}9d|Mp0,B,uhO0 0Zqi`YY-u{oo|4t~y>zku_)W"eT:e]f译)Q5y E*&f^!ѼxEkMŏT\HԂL)(TiDTYiDG9ovM˨Kf [\\wJ=K(f;%{9UKeMll'l猦;M+ꉅ7k'hxT3eL0 mcg<{yHwgAow1+˄m{LF;(fXl,q]9ndS\]q|q1 zք6ST 4JT T D[@^8ic#v+޶N#+2E/6JPI. / .F\h;\\Z;\jCRY#SrE&:jv9bfvMټ?$A1UX'.Z ^#Qc|V>)|r{Kʧ|)f#EJIE^L.߿ .B]"%BcPSp(sÕI@9;#NW¾(\VHg2JOh,L(25L%m K$3`AHEfgg&O||?}oΗo2p9:WS[iJ1kNpfSCsȂlqCCNLJDW_p?Dۊ9^sT­|%}V@+9*/c,b|sM~hin*})G'ggYmrZ/8{ MO9S'|9EܡDVe{7,eHCtnjKMO9[@)R-E$(Ü0Ü;3W/O9_3ԵȪn񺺇dh{NHsoJdKx ~F'>mBk|X[6c{6L;g98{ObV6iW<9~m o{AF؜]_XŔr/9WP2sb+b*o6^]1~* Hj **K(s3J0a /eY/4Üǘ%fQ2u⤄AF-s*Qnc1#j@H%3(w ws 1b!Yå%ʛ #$1))eutp+6)uuӛzȑ' HR˴z>MʈYRfU]䚂NP .7ٍ2_aTvduDuq6.Hb[Kz, Z.Oķ8JAUbJdrITHr+ȗ(:ŵV]i="+@' ք`e u'[!i;c"Ťۚ!  PKP5ʆ@]Q>QaZ \ &nn3'G,]rբN{|9PpӳMa SffwݛC j"* k%,Xm:hF҈ш#61„vt>bڳ9^X!T-rEgWT}fS裵cM v7EU1jɿsk1UY# ~\<zpEHA7e*@,.WG.,%vlebwf=fyFfq~!uICDT vd\ۼq(ɑ(Hk1.d|Sꛝ!jjj7c\<(kO㇌-b͠%Bf&v(q>SPYSt~uBѕW=VMTiB'UM(*xd(9|Qi$TmªMKuRkutOkr2^n,m@bxn Q542S!-R@k2Qc\l*+%+vpLǙ2t/E_~ARj.}ҧ*6c(zS&j0QTr$-w R3GnfByĩI웬›x+IHyZU]dNPrD)+H UǴP1e5oy= 7 MfҐ(*E${ Ff[ `n3wBY$Jmqs>}߶"xb.I7`j 9!V .],;Dؘ ,5b{v璝qy2d˓mƗ{s{sVtXL.E0;,f=>J)RcWތ?2~ 3TfvJ$:)RMNI Z'khEFK[1u_7; w8NcUZ)}k®=oM8p|*FMչm:IVYA;P9PO Qr4:BnTURYl1~Ytmb;_o ikv+_ |Q1E>dTl#ʛ%i-gPE G$G lJ0Pw`ADBE*D-mGM3irn[epsok:E%63!D7ILMg&͘W(oȔ-qcUZĹpcRҺ&TVƨ={9uk(a%5X.I r* 䎀ڗ%'8#}ٴlcw)% ER̈)şԔx-r-?Q~j??}_OO=[_W?w)n<<k ?̱w_8(K@)d~AX(WiP ,g(\Px#>aO8gY7Y՛0EƊ@%xQ-r"5f~#~p~pJMdkK d.Gg' ''pizjlvS(/R"R^}g]@.-{LWO}_yc޼}7ܽϳ6#VZ@I]Azc9)Zw ,b8_K)*3O-Nm2×Ny9ϋ=͈ IDAT!Ͻ7y~Ԥ$D$D%lL4)s1?l}pE/\p`0a~_}Wg?>+N{{hlk6l#R<S!LZ΂ک:mR"u(R"B- +%6.WW]zŒ;H2S~Eu(qXVw֚ֈGηwqv}݊pbQ#\J۬jQy~ 2_~{H/? [n[nS~rɯ7}k_cX/kL.? R Fr_r S) EE(SX-QdS@INZ Q rA!4MB$-RY@kd@ _!fAk4Gi$@tU^rL&ma6}RHԗ1LdjIi,& pYaIP-Mfa ;a&Q`B ^)RDned-DwŬK0)2L*.`\uN,(Vt)d-c3)LlV]d_OœzU H@I 2nɘY@Rj\D;|~%y>~lpm!%%NsM^y[[+̚ ,N'STJ? s C sL&t 4# 34#Ɣfct9{;,EGןR%.]jh ,y* }(4)% I.1Ud4D4PUP\k-|!5Dl5ĪBl1VB:s;.-f.hV\ۼq,8I]bPSpUR*a8Ii$>J\0z29;x&U%b ?8l{+ix:1T-m vE+xU2USwOJQQHE _M>n9jV9u)RD2B!$kb]ҧ/SF6b>EEC5g}>7͢0S ( 0: b!nTfy`&iS:Ƽz![u.b7|F8+W#W;%T.Kv¨U^ :2j-rȧ:[Y!f eu@ڢk^+1wK"!꼜c<PNe.;\wrQГ'Oؿ{9r3Cvs+'"֨ZP; >S,]RFJZ9~®{!  "亠7 (`/;!u2Yw1K&Kb1 aSD|W=^wgykf+2f@(+ ++\Lum3lrÍL&M.m`e8FYyOPٛMv՛ (Xm^ t_iJܥi.i W  ^B0j0m>u%nhEsZ^ fY- kR)R1YJ.cǹMTj4 r3t*] NZh9I4{x.ޡK,n]`Xc>S ѡEZDCpfH_1uF+exQ˔D̡efĢ8i#>C܇%%VGdOP*Q+e}KMa6L1BjY`U7R"-Lq5rNK! acs'8wIVOBzέ[n)nVN?[1yJǜ)&u,3>^4nCHGliF$X”tɑrőzI\8S_& 56~5t2A'Sub`90:#?{HdAFgԁ; Cv9{s.vYeAA2L-m(lhl{4mg2ZY>oz& [!jGD 47[":%5J~!SH|]+ г11~7s|;M6'Xlt@945oOx~rL>s]ġ ]d"Ba9FE61d~69tWLLdF ~V']^]w GQ =&LZS"(Q*`L <ʛR^b]Cter1 z<Г1Ncv:||6_?<~0[!5q5 ɣ!) ߶JuS&2Y(ԖJ[IrHgj^1T& ( 8l+FJJKRnKRrYw9)Bm𔐡5EEdAlSb^bF/<7\;.ql%qn3NX^'8lim G(b/)nq}9)Y.UI0oB8S6E.-.hpފΘp RAd5zcg֎x{gVhq`ShS9ѩJM(4BhSaM#qf "!]Bc/.)mIїT=,<ڀʠʡ.Yqmvh䨔hhHB8 81(ԕ@">A4ÚuK cMBZX}}@Nb%rD9j#4mVB[Vhn)mQ)Q,V*ӂr1`)eLkdX#6 R#] K2Ci7JN%?h䩠l""LdYO<OFbG'%R]-mcq=rUALǂ5dddD2^yŽrA(PXVJ"¬2+=B_G5[.[.l3;?e|s8f m0]B® Bc7dq\.۔{74;R$Lҏ"hApj*))#I}/h>j@ ^~RkF 䖓#S^pܽ3ҙpL89Yh;J)ΦQ )Ԡ*RLZTaAEXdE,;b)b+1q+x2[e8u_mmKYWws(g}2;NdYyrទ2aTL{%\t%C{H2z+4k~ˉӄW5B+ ѩH-L1P_n1@ۢi*p-gP^4\w\w\hV ؘ-Jm>b#fq.$bUYyʹetY:,[]<5+sDB,c>~ݬ62B ?'xKrS~Ἴܺ|x*JQ=>!>Z@-X>73s}UnL|3K(MotyD("x}Vw}wE\rF^IVheI^sq2Smкcq˜ℭ"97 5; *@rla22wNAowwruJ鳔}pFΔ 2CK QTPVDPJ\_**lXkQS[ZIɎ]p`<;%v{ A8AMvhvIU.+ڬAP"I9]21ҦZF!$MBw!evY^hVA97Dw ,چ_rE/5gbLuPiuYRMCqF.HP;aLؖ>vⱽUNA8 BM]CO<O]@zĘ` N`baZr?//5C̾&Ot|mϥg$8Z̅@1Ki;5W sNx2DSR ! 1 EDQ.P UZjTr.3*Ư5?|oAŵsɭqɍr+D":ԁ%؆j̱ _M7K{W wq:{Bj39<<,,0I40IՏ}_r  r*S!-m"lZ {̫{~|Cj7|k6!1ubGg #8{.7ߎx%ա²R>oqD oB8<ܟۜݏJ_x5KFkN;[δ1#gbL;=&A]VT3Mlz9؂ݙ3^ г!t;8Q=@W 9O<OOgk45vhvcDj*J VYȼA%"֠6@s32U QPԕJS4ZG Ղd-6*/-N:`\rjMyf?}{Ťz[%JPV*E&qcS JW%n}TBu8 1űR$-r#YeM]ES4(uPv5\?(N@|{> owgxa/c]N^svu;v?X]feYQRZP|_S+**Q?mV*Ԡ K{4@IJRD5^B!(V&(nMn^cftKuSljilLܤKwѸ)}}Ț"*|ci'_"mj)P_4(1]HYlPe)Rl=\h(@65jS5zc4ZS%1dt{+jyzϳOw|VGBiJaК5,QYTȪFT5e( 5C8 $]}e њS|^|SZ$ES2056juѡ)u$Bz/Qn1gH?Lw$+ \P*UGjV*;EVH"o;}fvK=Ci )s>pP ma/ 6w]^RL/Ęq.N5Q6}\\2[EGqʄ!+,KmAa F祜g\y* }N]8) IDATnP VtA[ !`CPm ,G9hX6FBrP+ġB5hU 18ncVΟ='Y$dg! 12EB#e[wr#>V s(5"&TVl* 76Ce'r\4TۣvUAŖdog:%*IeN=Ne~Y(%_z{4CAA1k'U r\did!M )cW[ՠ[5Yi8 )NO<O;Ίkn6W'ex{ kNjz[f;5]#:u!ChC@9&XcN XuY}K3BC9cĔ8uH8v$=\5FftW=(dx^ݱ[!ۄ9a~y9".2@<ѮRT@1+Py8`0IO'L!͐〩=DUʎby:g8&Qlbi3%-%cdUPkZԚ&$E+1kZ1-cOl )jL5'#<7r#{TQ#{df:¼?c;BMldAWBP Vo*xÐ~oI_ӲvxM:obTh5RY+95$;ncEŋޒ_0U)5b,$Yv~+ :]#J媺*VV!n+Fepaأ 0=3u§ E)khԂC,$B';Ds|jPAh '9`AW]<G&vCnl.Y23۬[iM)?ّ=E)nPu[" !BW9_PJ U{,f It(=~}{ t͑iEw>^s8JLVLgmw-6- t-]sKGۢ;O<OòcQX%YRjgާ,k63F)4; yZ"KeʵJ5ը*bo-AG.Kϝ~ţ~JO]WIztl!h;)qvG\;ҡ1`#Ymf9sƓsc5Z+E9RQS5 I//6"qW N GYm6Za#˂A9kB%͠vߡK}%iMqݘۓ Bet䚁V:, OȪ'Ĕ9)p𘝟P+ŖZbK_@9ԱBөw w:ID%~O{c[1#/ߐ&==(+O^mr2|19g5/z\׈ACrwb%.Ġث+Iv0rB*-&J4[5`y]O|1dlx'<',>KaУ 06-xIϣHsf.)~0H~p*U9Vy*7xCNN\\7LgWw;nPt,.9U%9&31QG6tI,)6Q$bXc&tNtOtOmR m?r>r~LJMTdNN0qN;'Lftcn-v$y4&1&ob!g˅yǕz$܋-p{=O3猗gL;cwh)UVdȢ*$ij2S>N%M&/,.+6#f͈i3"m 4 tQe1C@@%-yQe @=(q2= W9[t5#;2Bb 팪D+\^XQ)zgt[w :\WEV;Pb+T{jQhTU~g[\C[wƽƎ.aDFC䐹:O}a V…6M#di٢$\#{opFW85b)f}݇b&!Cc S3Ac)-mZSQ t+"}yfjHD#.бs;Wg3~U\s#D_z|5ө 9sNzW>_}>(/e .G>?>@бBε)pE#Ox'x?HrtLw;h2A5 |ւuCX3=sIiُ1mA.4D%O2S4\JtWDaf:{#sMl7xޢYt ~?!c^%5*U嶾b_^2Y0Y0^X݀M \AH)3&$ao4a>xz)Fj-z CͱɀS jKX?UJEA65ZU%jUBZQUX U a,ҾJk0GIz"zL Ak,CZ]h`Ȱ H6b-ꪁFKrMq~|`8#&œ&L'LL'̶'ў^6[Y"Z1PIhOZLJrggԧ57 )ϸYyónx,}'4]l#1Bf( cod9Ȳ9(6 I<`)oИ0՜n%J]NseW~kCZ$EZIgB^:ުo:mզr#LTY$!JP J*4Bɂ yANo ]ȷ }czLCe礆VR4Łn,Ҏ=W3.]e'EʎJfXꆞ@A[h}QBg2x7hQ%b}b q=zl.eqέ]>H 甯_8:TXD}a>u]ݝJ~#%'P풶ВkvS|^7+>QEt%+y*2.O_p9n{B1Griw,5 eS9*oJWPZU#[5 66nMhjW-=mI1̹U{|മ9|W$ctDc5LSr4hҮK;r9HCv-P(Gx6. $XۄM|Ϲu=O< ˀ}Ej./9;AjHk{o1G1@J9C2rtVVta?41E̎іxQefAonKP'ʠBKae%T;\/}w՛,x.p !L8AFR5 Q>s\{߻?߻ .6|v{n6:ERGDXr` sN1'Θɒ2)ZP?VUDdNiǙHs2HsKG4>|"OKKY]ViSKZ׉h zy4}-&M;c*iPq>v. +]p?eB6oGўX}!5cVVUhZM<3L|6&WרzG9+5!(D uY<.CcvZLCt#a YKG3.Q$S5R]A;ܩ3pv ufBrKa$h# Gu27<+nyVp>ilUO#%咵zZ6e[h]ATk8<])߅s~vf[Mk P@sdN,&1kpNCl8jVf.sn$+  9[FƌMb}0G&C!KrAR+`_lf.gK:͂ 0nC?JNK~ \I*!&#D"BFbᲨ,>2Q?6@ _lk]#IHTGGSxU}O& 43Wdq4~yki}%vUh4u4Q;˰;IyX0@TQUH -B[!k_%vs*ǜ*cN1Bs4@ İ@U1 $!]X,!ҮN;sO<O4q:Vą823vd!thmQbRxȐg]X9;xJӈpo!&⧽u)Cfhu5k6҃K-`^¾$ Vyꄖ3/ᦤHV+&J嘆*䂙,-tsymmQ5EuI f`1jGH Uget:֐C6$OzKxAZB5؉6)YxPT)Si aLLs~zϠ^P;]8zG\cΘi!)MӀ F&)61M".Zfb20Om;L3CHW>U*h/#Wڐ{IQ(x{*pPۈf#ح zrX]3o״ Y7.VbabSqW2^CfcP A, fd]:L#{kv0 IDATU+N͜g תXmVID:Fif< ?b ۆ4Ϸ_p ʃ|sǩ䴾UɤJ`yЇ\|d&)vz+(v#,-c}a}& .j@e)l>TȥIXdt5ceJ;\cG "J*,%e[UVhzM(\hj̟By. pG*(c7>ӎA V}>:Yem[,:U!:j pI~鳹Y߷^{y# OO#nKX;z¶ARHAu;oT):  :ШTRQq8`D`*9G<|d?̿6pMX{H\19ؖ+>]?cGL#6\<%_LGL-gK N +*Л jT)& FgR&mxy"Ғa1U7g\9|T`S*q堟VX ׉h BxVP}jt}nբ:P mv77<]dp fuȬj1[l4+O@ԴO5 ,jy,{>]oxJ|mǩ5̛rcȂb0لLS1qiC@T@A!d(:Q(ԋdQR+EzGΑfHjg.imREize٣#V(Tj**pcrߚ |8k>pn\|`UyFL!zn"^x+z50 "tIȯLEщ{'ȻVJʧYs^1n U'{Σ{Gyʯ5ԕF[MhqEU82Ŭ3DMte<}۫!u?h^Eyy|On{8(By,hM#^X+j2q嘊tTڿU~^dD?f-;kkaE$Cb9lņSFL]7-{6m$AcI wGik|i9gOx4Iyu똕aiuIR{x-TB j|NGp(!~m1֨֘[ƭ{O|OQ׈Aī:ū}AndAf$MܵVA'__nl}s[~s )N)ܔ=y:kP5b!):֠{Fk޻aUk$Ceq1g[NXκIZuШ@$Z#1dCbs5/j:LE)5`z`ɌIpW?p^_+~ŏ~KpLVZR-K< _~=/~Ƈ/XI7a~>yHLOA-kb[~1~@jyPuI1،h"-bX,(rԾb`M1u2Ijw3|;-xzHRZ:em ucHѡh zHUt2NF7n-yTxGq񔻭TM>ɽGUO1׈z-_{>.4`\tNw|[s~圇ȍ 1h)ʉjh#NċO5O jEU;[ECj.(UkBEq7X$Tdku؄mq* V ?yS#cRZ 1dcGx=Biy)($҉&a=v^jȩeN%'WL~{MY״5m}.Jٙ&{#<#G|%XNk%xVtJvVunvH: :,=S4gZ||d:`9BS*3nUhRdUNv! UX^TS"VI/ Ȅx ڱ1lF2 RJiPb" {B'WIBkę-oWpFCq OFPK\CO5DC!]( &P7WVQwʙrGā}gl /6HrKgnIJ &Z$ޓ̰-8-:RTAl?mר0R]FN (`33:P[T X!N<ا)T"=Gp>f5n?5ҁ*# Yq6cjT#3A"JtƠCJjm"iD44J șHl;#%32{ƣ;̤ T@c`fK~3L0 -o],S@dXM%_!AAnlOؓ}#e[>Q֔R 5GRV:aiܗG\r^ S"]P> PEڮP,7ޞqoʹ\*M'UD%vrWD  5?.@Qkh@ A*U&Ea&j,i;[*{'!֝C jeCSJrߡlMPNJf>ۄL0tCkĞwɻ!LLkb f.vuaA=bnTdR5) S O."@ko.KEB(UIAUE4QуAt@``ȚNwA({|;pk^D~h,:t5!I}OgZ0)V/ L?0n)(FU4htAm44UMe4yY˜†b`O Ĝ XJ7yGy?ʯh MvS57M#mAna܅ƺa[Y€)C˄ <]ˣێ QqE\ܬBV2;c&ԎzaV T(5|N)nXCV65hu7m眹ayu/X6f-CֲMfJZa~7d [L}cH F9=xN?1q˳';~F#\OڜmjUxvOSd)}ʒÒ3zTG4X6zeu ݟ:Z?Zpc oz^4dsb}"7LAJaz<̫/5-vפ;E(%avx,{F\;L˳>LuM}QS9^qڽwIobblȺ5dt l{}@.PGw(#u7Zidg{ )Ն:RaZے'xj|bx@dK_<Ûsnc.32ѱ~8ߛ?a277-ʮAz b 8aNQf(ꜤY ugu[Rĕ;#<#Wk!/!x% _o3n:|z;7'DG8 ! Pgp~BѨdNjT72jP]Xt"%{3(2#2sٽ`A[RmRe)\>pl,7S6ɡtQjTRZ`T9FU`9^ED|0`vrq݆k家ɚcL,6 :B\Sf>酅`Y9Aufܻs:O+zCjԮJԮBrcsy{CKڃ5xNϜ:k-`G<+lV_)hrwf1n1niyBkgXUXۂ8%EX>Ra3G<0bY]MШ 4R!-2"Im vIT2 ~1~Uzw0~Za9]ugK)EW#-P5jR56.屎7'IvZ&ΫpMжVF+%vp].N67:l@ `e F{W8WTBe_q##AkdžnBnn\Oϸy8W^w ͧƔ,djqe@u+Ml?51&XIƧ-Hua21(张4|fR(-bR0Pvşhj3_s_Փ˟8;re [!i3ړDAw-+n8 :ǬW.J:ᡁkSF77/Q(D/^9.ۨ>R2i~9ٳ<{aJb;T~#BҎCt*5b'цv?DnDwQTIRgmɀibHP]P*Z①گx|GyzeADj\!lXc>=0 cy 1aΤь g{'ad8j^VVԴG =( BKpW*P*f&iCܓ$CR)6GY-ٚ0]s +tB+!Yip0?}CnH00L9}v=HgZ$ǮF-+$JATAnT~ю$Mx}.s5dԢH lo=#< ^YG I3h0記(蠘ච@OtK l I&-c&!_,5# aoD3KtD3K4֩ntk21 4@N Kmuؼlq}:!" tn#"P <90HpP@JY\tPtg9jk;at 8mӧR\t (nh5f[eXeUeV;`GޓHtj,6h*r`jwTs(E5gt%}ժruX`EIxYR5شBm6v6i frPVMβKZ;e27[kkGxaqLq.nAQi9bw4;.uww{[κ75-GQ=1G)dNɌîvt;;ly8r1z3f*Ѩb*0^gh؝N{( WjL0 :Cm/IpQg(eTJ6%{}g8+ȩ:A}Sޚ[9MVU%VP+; -?(X JCxAn8\rҿĺf"oq􌁳Yxɢ19|č3[sqah8c_{lQ (o)dNS)N;sowfXByi>JKb FTLd+lGwMw7t%!tS滅2uL!sGdl6sOؠk]?T걎:r0)CY1y^J) LDz.qp@tA'*ض6vv0qp.BzEc < }򝉲i 6LkN  m-lݍͧ1x>i>^'ճ7=ճ]i#uKQ1Ay-/w(q>PX--i鈋)q񌏫s>-Ι/4+nsDyӕ-Cf;8f}%h_lIn=V Mh8UWՏM7&{|#pvnJ2YgmVYu֦rRs#@T˚UnM,߄Xi}uԹĹľ椹fɜ !vrr:[!0$ ]֎3/G?(a1W1˦CG IDATבk\!{'(M"`&cwxk(V1&*\$WyR]ؔDx;Ơ`p˳mGyGʍ)WTP PC0CÉ߉ρ{XV[I4Woٗyn0]1:"W z jp[W9jAђhv^|1y ͹ڦJR^>?dHlxQj:zSV3h覍cL1</xPw(6$đ:%]f()HiwCl;a=ioHWX`z,ݬ ZBS 26PI] FM-R˧b:yR{ЛB¥D\~-<[ *ﳺGzm.} a5( mmuéyo-nnߟookzx\w~6;9Es?ÈY3D- 4D * Ԡt&n9 nx9/@'I7t hCd:l€u|[g|?}=guCH bқq *TVktb4|`TLt: 'kcF۳s>=47Am7^eeyեZj0R+?N>6sqɥH <ނOڀT)s  ={c6Kt7v>v>a;+{[,=#,VEK2+Ax 1.9n OC`)9uy(Q JAv5'55up+"ܰD?S@1% װP}P|28g4}t P #׬3z|cc:V` agT/X۬q:=g5lP JGy%ВZvY\mx~ft8! akFRbk%OyȎztFTkn1?VmeE\)!*)TkZf[ wkD(1;Y/BA4I`xS-5x ?ĨQ٣'Kde\4!&:sn-ARzaץ< + BjᎣޏˤ.f6͎Ov%qaW1ie5/t5Oѯj+|m!t ;Yc<׭Y:0! hA%r]'ML$*htвuM #JuN H$x^Job(25fuEGكlb~6ӡ[to$ tn-O6 &'dO ϲڪ&_ۼ_xoDFR|ϹkpJQ|wȼgp5òY&]XstnGyG~R$ۆ$h}A$I€/LU5y90 LQTFI%b+aMC֠5#dQEE<4YkĄ =eNIslIv7q 6JbHVLE۠Gk`X,M̖ `0ܗ{W|*^x Uj`OI䍸7Fk6-6- >X:󊄠i[b=`c4O(Ϙ,{,~xEZ  t{ t9pn}vfavXB;|ٯ[[th) \;%5afE&%lKXVB{%_ 1xN/ыmh="ˣ&;2QEk4z2e3e?|ܭF=#1 v3~rWOqRОIMQN=W42 Ѱ3~@h +i}`>p2nӍq\+*$:j`aa,>,>Hö2b^qK/KŅ[=g pi*&md&W)7W}n;h]im]s3F՜QyQ"¼>xL# eˮU G7]ۯX>V'X^<[1oOd=0܍'6BR:c)MVȢk?rl]qb]rsyŧ֒#g3~;1 :Y,:lbm}{ Q!3&C~1bG>S|PuF^pg\plcc]Σ.*JduY=bm_ɧG{+=;^w]^vYX~P4ſ>3.QuJTf _O&zŇzt:DBPPМJST*~S U{^ɏ|_h/KZbry03<3G5~a5[`v vEق(7ݨ6 JM5U ~ClI5FLC9kPzA=k(gFX M3T\ \@Ib̲6dH*+8ܴi<(O#&`lJool q@r T& \q/ d(SC nkbٜ֗f` h=Ƕ X[?5P@Ԗ@mQδgUv@VD`E %]cw=|7 Gڭ9ka۬u,ATBDZ5 : ;䡍6hEC(V&U' ní?ʧiiJa4mиe-Q[/7 J਌V*A*\GD OA56(R8M^8ԩdivOhUk*qePWʈ{Dp{'`q98f:اel)JMcdȪ3e"*S82AV jdf:5/$GјʤP.QӨ$Eajܭy`-+VMo" u"YD2Ĩj̪3Ѥ-E>ؘ!wkVF(;uepXm⸅)f쓾 Ў$ eawbuܦq$W1WnϦLI~>?'(EQhQmdsAͥgcm͆P[:Fa>Vz@*thbZѬqcѝŰMB"Z+ޘh߰:|eyjc4JMCHU @6I#z9#|79ϹeH ,鱶Bz#t):4Gmb?eE\$f "BbݪZT59_PZBTȰSl*45r+p)*r^;kpk Ӈ>-mxcj-xOtGs6'e  (`ۀana,Ffq, mf`sC|ȗ MհU=wgygo@z5~Bo1h!EP%T>>T&~{KjKc%;ԅG~g X edԏ̓Ye4I2 bfS܌X{}K Sa+<@HjSR:%}n#n#DU0<0n88#f~evWbVYv}bf"WwLhxjs',oCV!ˏ!"x9zy'U^E,&{Rd,.he4`pQ끽=xFLP/s9#ݚ %5J LU7T\   n@/k^ixMSȽGKi-#wDz@BeH>'s6U'uW{jG17M.'`,z,z,]FvK"=*#2E9Hiզ5zeAybx,[E++Q)EO&;Mb{F9GYon[դE4ԣ%Ry66XO r9sl3 CC~?GV^Ն bܝp5b޷H]T}cTEvMl0*Bsxtn{[QF8{Uգބ,z>#6 =k]kR߽]. f5Ȉf5 šn1+ŏ[@cr<&1,ZM.Vzo׿69<`k0^--fkU]4-}q.jHr=;bv9R:vHIj<S1iYHc`VS~4ed435eTD4)I"\CЯSy|_xy߼G+JaPh&fp=mb1@ם_xK)t&bds uD+ǠǕ{je0 f=yǗ)+nse3ux=G/xaXUXiZz7UU~X~1gGy~$vbO+/Qk[~|{gme4y5xW'E.jS4qq O?M#GڰhcW4jK%'--˚NFRhZ#2j!qe[=3en[sB'O\ҍ˃=d|NpHy~|ɻ|,.E)c%ϊ͌3lW-#"?@)ga1܂\<#){`F'& '#G'w2{L>r5''5Yɞ¦կYoyx=s=oͫ |sAxl5`u`B =]pX]b>Ĵss?/7:oȟ63<`÷b2£%%ˊ)}źX89 NJT%>jf"ĄDѨ @4$I:: U`~id$-6sF=rOt#,М4)*$wI\P1( )akbr6)JL\XJ⮇M+2FY)]wAXʃ1!<0:Ȧ䴹EfþР?餐sH IDAT)Xc7qq3f슺(24nC!Vi6lkZ[f]cIx VAYE[De4x^Ah+ʩΡY[#tԤ[Mbku,r,̴K9N55kJV9}洴QBX=.t=\cm1CzaȬC.lmsȥ~FPh0> 3.8?a;G=들Ɋ5ee4xl4E ԥ(LҤJ N33@ *Wtydhc)+~ۊiK;LBK錖on]&\UZ;ZEhBj, ".>ѳ%c!z ()")e6xV~k'xz"ʛZ3*;Ba<xygyow/_ߐv1{9U _R.DI\m3fPQELs뀷'(< |6=:ԡF h)xk K{??g̲%[,w!-QmUaxar! V1Ó BoEZ\o xX@@.H J,2\/#E*4h+~Lڱ3$3I$c<.8AiiuB Q7M#VUJa {$jL8e{ʂ.Rbbrerc5NGP5*1a ^؟VZUHDTdp}1KeQ,ЦlYThJLU!)b“"1;V젇ۛߟs?2b{>gm\qUud-ypp LP~.wo[- 2gW%buʕ:fk&ZO2Ax ~J`lqǧ;XfEee2[ Y_D$ }Q=jGR4)t9_o'a~Pvl*1얄 )~raa=b٣֬]E|0킮+v=ٞlC\6DːK&c.Ǭ;4KAj> Q"hu;V-fKPh5[Oqm 0Q1j0 ¬H-=GN]lRmCaV)4Az㒸.Rh"- !1TF8\7=F3cglG @ BY:0`NuHRGlj46r( d׽ a@mDFoi[EbAU$'KEZd4(,J 0A[pb#_`t*.Ѽ${2GTD[i{OڄK nš&Y?@>:ͭ`}Ryp‘v0h?rҾ8ؿ9h̘zѼs"lYSa؍}gyYwԹ@S9:)H:?XI#"D?Do /Kap$PdXj0PkгCE]~a% /iK9N0) flk"ThSޒNſ\bDZ3Hˀ};9 W=5^3~ۢ$=~5~=fnS SJЎ$zOaYF] [』K>Roҩdqb^̖UIiGaqOq62i0޿$)n{ع ߮5qIyg0 H]ʹAԆ3dˆ TKи6B(tٯ-"=̳H]!Ccބ/'<-՝K[0 :cϊG#G6C[ErL\ZhJ=HeNW/9oH#u0MaQ!)d}Pb +*%IM<\z4>T &JkhԨAC̗ѧeР ::U Wt^n98q^adAeKGq!|4nl(خ$')v]I9 e>I5aܺ/|̰|̱uc}|֙ǦYӰAXwY~Ccd&r <]PGskÔ ^#WDG*->YB:ۍr;bQwB<<@< SmE{s+yR3t|rFmVTÚuMU,UKfgyg#?KR=!Zdz VG5 UDgSeUR/SDi^d ݀4S̀lЃo0Nub:v=o8*B)h%wʀUnu۫#+j k[9/Ic/m W|Xb>To ĸz| 4ۅmvȺ]~HJTVbQ(BTR8?HSmbZ%š@]evծsR{?t(cn.[3""ˣ1%XRC9тzD|.h m@4G2sI'.R Rph f_Ś}S>/XE.&Y{ˈNuО`T)gQ.mxgs4itzfԺ[qH(&ܢXX$F$IbX;Q+Vl"ShuVÄ бf4s( \Sz-yy{CӇۥ$U!jAX U[5HgygM5~7KhAg\T42%mufڏČTrt&l% I$\VMVlMf3o~l-׶ޖrxCg`ɞwǾsdl6?z0BkN+Nk]%2fc+~}Eyvv8A8ԺX_s>?fqdYw̓Ϝ)Q!)BYU545ȌQ 86WEu>v.0ٲZ>qЧ.Ia8cw (~ IkJlF\V|[xգߩxqrQe;S6[?䊓Β1@q-OGCޤԻ:NPٷ%4cF*x㜎UɼlDa ^~ ¤Md-tz%#<#V͘O(On9ǐù92CU!sFEIIJ 4!}Fv $9$1 L{Fƽ/ !o~ ֠?@BAYB 0kW } 7= 9Qi65PZQ@")iKdEc X .j;qqW FQb-CS-79|΍<^ S ׼R"iAu7O'ې bb9 s\r Il1iڥnà]qchi.a'+×_Qmw , \5onT"kÕ#I1grSC(zǖU]_ Y㐪_(DM$[ұLΌKDROrZ0fN} bX`zHOxncDB*dȺidg_`֌wrF"'Zs0Ha}*L-=gijs]EW75 :11ֈCbAu.6=V T)"7/4[tvB B#!5hg6zUhx.8a#;R w{veȼsU|A2 |^t^<,eJ;nO!E6tOf\O0g!FTK}RPE/)%PcݢCЉ@H0fT2:D7ffP<8K"a)R4D%&jkb;>WǽcGy䑟1?[7fΗԥECulPyTC{h]Az&Cv㳘o#>)LYpwQ \XG\~oR jSN xPXm%*Xg?dۜ.d]DrҖ˫p9ys32̀ل=E-$gCQVgvUC«pHr"|?`2ؐ}`Ŝkr}ʅ"[4$'1k`<+SS=T]cꐨDȥrmGzG![ص!k5\ f2q#)Gݘ"^; _FCc;dCiW;EHM+r rЭDJ?t,ч&ָ%d|'6TC\嘷eL:b؟6ԊN/T;ގ첓vY@$g0-dvu7:.s:nB褄nB1xm}Ր4}V$ˀSGZÚ=㎢y PeETM"qW* S!厮̉%k` 5*]d/9[eo̿Q+k|H=1ڜLO aY5 7._7L!Su9,:ĖAd.FKF^_?Ÿ3*sAt\;R= !]CtX^>لDuh#l mÒPdAN8^` g Xfl賥φ>qCkԼA//&nNdFxsѭQ}W+ 0zEzn1iirI\$j3ji)hdn{B2sWM5\kɜ>L:GyG~lDIf=iH@ (oB,Gs4I  |r̹a-eHbgP/]m)!0 y1in9*ؼΩ66, ӌ_fFsjl9d8H!VL^fL^ƌ^$`Vפ_ ? >e)~7@R'CÊ5֚~b• A"eCEsba}nazCcLxoTC~2|~0_gH86`xdW Fk.bG%Ƥ&:d~Տn~4QEYK:Q2 F+e{LĦ)=tCnA@_X/[sF4|*KN="vKtLﹿ9n! Q@jc=1df}LqxX00Dgkg\Kcrs?>?bS-z9S"oCH~s{sLxlK糘Aw0 L<)j7bz#vuHz#n魠At{ZtjvE]a+:9xr'K6c_?A麬'd;lwL}vgqTȥF,5anM[ceIzsgޏJϡ W+KEY^(pYFK:IUH7RP6P֐%`t?] +x i#G}I}IKC-o9xUq%i `}/b DA\SMA(hBk! 2ֱ" ^wWᜯɛ5A;4PCUlx"M[#WaCg7\ .[ś񉯎~ WL U E eKt(XF[ZIT$FQy io{N9z@OmԚHAkdBɼhhBtLN\Z~>`gO>eÅ#h}1]'6Em,*mrm#676jnwq؁eߞ):J#`i8R:~(iM9@fuꀡ&HRːԶV=D 5@_Ubc(V 6@@` ~asΌi}O_pt<#<򿏟[φ|/dD|?gF# :1 }FZI+~Ak@I,>.>7)OZ.ӅQ>4dw0>9bQOHMm]h]7P.6!P}&hҚR L[MZ[y@Cu |6ֈPl؏ tF hƞ8/lҧwb9oZ..N^h%;:4`ӏqC|1&;Fd%$;03'uQ6ʦ^BAa:|<<:p5lUu*`N>^Kn̜s{1_gc$}9bԔ0k˪ǮQ=t c#MY8Ks^~JJ$)nUTn]VU lQtwnKcML8 i~옕;>' /p+h&Хt\ `TXn A6X}l˧ln1bLbREF,*%tAtdr#nukl!M-,'l.֥L\tPW ֜c)LJ!s|vK\tV!6@%.VD%8'dLf<=xGB4R}W%Q?JvR6M7_;y8̪ 7C}cWzf>eZscawz[! j\& TFp%DoSYxC8gdMpXvPa%TTj*US+cG~gS+olLcn?ò XEG CIUDO{7HAm8E[6$, F|LF$wN2eӿeIOi氋]G&lJ"J<œENnЬmP>RwfN!К1(ОNtEʆLn{<#n9!|3O(z¼RV~D0siEeNW5lm'."P Fb%IC}eP_JDYz#`' -LTGI,BD5c IX6c<бD<0(i;7Lpa6S25fLe9ݒ B~&2v@4`OK->+Iϕ8M;d$vHnKJ>lZfsdhDyfDlmuI ]Q7l\3z!n{Mwt)0#2:eJXuY7Z#tZq=TΒK$W'WLK>7 6dCw2&"n" ^OR|; N)],˖;MzI4{ͱr|ٖhCo%6D1e%*LYJ 5H]ܞ[dEݚH6/̙)NTac'|θ)YMʚ+.wS8.YcPM1͙F7h}uTC4sY:u,F ÄAHE)+vĶY[EeX|% I>lAaSMdzjnLƸa%ʓ^© *a\\}_wjΜ 1l1Wo|wC S3i[:[_xco 4Z1jVGǬ,';VV ! Iŵ4K,DBߏGy䑟-?[w{ocPg6չ8T8aIuք~LCm*l*m_^rS 41#M\ gGBԁZ|\+\ݾF9F r Cж܏@.hT44B@Jvhe [@MBDo/>7dMV@P{_IMZxg#|4s13>054Im[ԎEOAئ`_1+5o%_ZKlOF51!X#cߌI}A/+89\q|a-a{ՇaY I뀫*QqLZǠ c ];GEGK[4‚`ft//9̌ j m^Qh4Cc vWj'Γ+P?%,^[4E[CsR{3~6crqO[k&SM2'+QAiC a-F%|w{s{/ r,6k, FH&C`'673 E_0 JU -ecw&tzgd-7Ch9џ/2ظ=nډC6Ovͻ ?[~s_ֿkvMw^qHijm@F-Ki5 쒣K~uq,|7x9(i{/Nr*-sAYݺE? }*aS$-Z";lԡ]ʭ4*54)FivK.}rz6 Os qd3*Vb%kӈqˆ&ZAЕB *u, ѯ%_ {+I!*_8}ص~oVUcKV8Vʀ@&*Еo1I ,\75hLbL*~zW S424°.Fw5VZhӠ  ˣ]P₀i;`;"jnh,4*jQRP.t0Ė W`*6ti(Ys%CCJWh=mkBd"=3$퀬R,Ke( |fcnD5q.pKzx2)vV",PDKU R]t՚Z[SIVMe` ɱ EM OQ[T94errYlUP%:%$هq+DWT4MaYUVeU9`Y#'ABlMlS%I̶m,ѵ9%>0 ΉwG){I_GK&'3|D(J#4TҢaZCgICnب!Yl<{m ;`zI Y1f󰙰4 "o=( 0F䘍}ԁQ( <'|dͤ笼d]J0i2(onC^5XI8$kIw$ͱ sN8eEQCUxMnM푧EK<Јgc}/3Ž1+ԂdP=kK1iA4}وwޥqhH1MAxE,!ELlKQBT u ڑ92F Q? CfEN*HeڇpLLhA"B2 /.I0oY0&-Ot (]ұ:h-ԡGʂ(ts D^iZlX(ۢ6`} gG]6 qP%q(g'aKGyG~T@M0ZP,K H7.O+@AKo=_}@qe谓D[EZ"2FNwӵS~BH8 ~'gE>+ϊ/23#rǜ1d~j}`cD8%fDeFW2i&;[6]SLQ"eKD$EKC&CC*Yr=;fvz6&YPcvX4CE؍ʊQ`. :֎TҜ{}rD:\Ζ{ڤ Ƥ ʙ ATB.r%PCbSH3|'g/siM}`q6;M~_0m )F3}еqřya 7(k, >tq>afh-9s8+_azHoT;$ )kjJ}*mYdzVB`LNkSЇ6R})M.\b? v}/?r\qF#̈pYޛᐼr?'}ny9Bk1x:4wEa%|>vc䀕Na/";L.рh&-U767-/-trQoѯ+!W.hC!m,ll80穀EEw]jom\=ٲ0}_GAh&MaQ%& Ln2=v!TL֜?u|" +[p%ee΄. AɎe"gLvƠ]ּѥ80988y`fNy<}0ؘ}JOϤa `B IJPd{<.`3##rq EAKSO/eBR< ,I_GOOGi8Xámo{@P!G-VTrýdYD\@$7\g\g\g#At[/E,O'^`t5[6!e"VE4-UHEJ2[9Ɇ UE}ICACI;^p_G+~@3  ?\nSo}un<" I&b ^?=Pr!6;CgcY=x`91pvJOZj Vbx`?o鿌95oyÌ*OOӰ&6R ?pjEL3c\)mVǣkyyDb)G,L)6= IDATŚ5tlV}Ř|HvgRh[MsSR5,>WGjF=P!YHGYcn-R)#ulm)\VGҝ?g9@ h}g1rzƚÁ\{'+Ξ9=y۔_:bҨ@cy.¥Ճ|%9x!/ _ h#6uMDUlt/<ϯIKg#E%IO,l,ec*[B7&<AMpVr͹!vl#2[Q EZ+B?{%dt_ŨیMAKtЌBXN4N뒺.I߅#S)h J lɀUݧvLU슚5NØa0wi&Co1rzqəԿ`&9LU4Bjj" 㮁w<#<򿁟۷ȐJa5VUcvV;E(JaPKD>|AXihQ5qQ=)C)-*ib6MEVԺ* ]BJ Aؾ#[(!q10V5kzƆI{# A;B#i8D ,Ge){%I,nݫ$"3#2g_|HsduW΍rr`UxZRėȑbb׶{cv|TA);y} _YBFST]Fvx*&CWHM@~z[fBIÎc(MtYa9*did~ef M}GW_v< d7X.k%]Ed.RAunPQH=^Ȩa稖N8N[Dmj?1R;q?dSPmAž`!=B7 Vej)lo- ʻ6eEHlP66oð='sMZЈJH¬2<r[Qi_;g.Vk^ f3Ck(jtrnK!-ؙfzKCbq/&TJ#X*LA=c:5.Ob;b:EՂݮMR,y IYY&$Jfjb k6 8b*vh£wp!7& 9h$]0fݚꤢJFZhnIU,I޺#HF5m8eT;z1[NyX۶Y]3FgM>381tg`|0b^#\chqy}Njnfq;fcxiS eUe8*!&gC<(UszVbZsmGq'7O"P%VJR՗p!==Ү$ml9h93>tCO&JBBk *Kc )F;+XwHS|oYd}V7mvC^li"Z8bʐ~?Yj=ZeNu6Sb:ϊA_,i!L($YwY=4Sw?R;ⓩBh !p ALCD!w)r!)Q!;QҦxg$7(0}q袡m!f4̨ؕ/6 f|`2HdLv^!jE4.R~=Zz<&;|f}B6w;݇}eZGނt|IR:іhG?ےUTj42Cɾ j5[>eˤuʗ0cڻg}`ڻ' 5.FGUit ƛf -EX6>bb]ծGx O*aJ1P k%GG'dſP1(y0> }>F'pтhI.'z+1 赶[8d%Vqj5~g,uas4;]a7nv'{<7R8x'x߅_/9VwzW}z\X{GQ ba>N,6CUHŖtSꓚWg|.n5;+eɳ/$5 ӷT-vcrۄBAc]pY3H>GH8RT  0<.|ǻ .Gli~~zc1峧F"Jѯ "%ud&F\?ÕMU([ h3o2x|ϋcV1o"QFJWR I.zSu&$A j Tơr wp (G#*76MMqW&6Y|#BQipoAjL|>S8G5iQE+רSZ}Ar"mX}*KcxN%EN9רR'H@<,0=Ɗ+ZÀcǫH>#6_i2:3q̡8iA8vIUNp !&7 :hhOtup9 -,GOs, KX?EԑFI*Q5F5&ȑ~dLj3~h{> mm7? ج\ 4J$2Cnf\` w&H6'6$s |O4H^ZNgl􁳯/͜)3>9:'1&?v(c !`"h\.f=ߌgi%MWoBF*N~`vICAGLgGf)֢ qX>3EQiYy5$ =+qdCZ*B4L+"3]vf58 9]$T9Р%Fe/;||+ y7abv jP;l1eX2)ڮI~pm1a& ]q0 "(h!_h; i}>L#{p.]xXR':O@& + !5  O(:PD1H(  +d\#cb?ڗԤTԤ8^\?~=WaWz}0VBR#TEZZĞb?QJEI?_S&fR`i9:R*&H#CC]WԪ`&~C!s#BPc8uCe~\u?H8|~gˬ&MYdqpb"l1833gpqfÐz|>,^|}^ED 6o'|9? $%B*V rQ-Ѭd;A75!m1U(^[(S&[G~Jحܯ YuIv[ ^p]&*44Pqx)C{w|9>{>I>ژ`_dLb\z#Ldm7> }cIuQ$j,%v;BZJ$,"Mڤ&e"옺Q{X&u AI Fz/o5[FRm lk99޶X?8<\~cz9#Oa}C4%N+= +W4e'Cj2)#v٤>\{'x G~(qjq Wg\qzA?EĩGY L7;dr9 N#U |lrѰZVok."k/L=nFgv>.NZX3: o("9hHln#n#C0{e XY'33 "[c\weJY%>e0c_>ڠDhb6£GIkx;9 cDu J&aa #V+g3~LfcjGP $u ~8kGF=EoB]ME琧Tu6}1h g(hZ`)1:F@B?URѬ,-a?o)~8m}:3֖t.UͺzE%%韪 !AduQ5$  u찵&A0s3إp}?7F%bg % ,hA6;, jGmyQ=~{/\KaϙFB,mVuY5.8jwv62O/.ɇ"<6inq d}r2PIIb:9(f=RqfggD :KʞFYQ傤Ȱ)dEDD+螬h!RUU;b_xb/|j"WwM"h/q9o:>j,sreNMʳu)LoJpY٭0[[wKq+`j@ve"g_&G$=t:ܹɎ= C}ޙBzڀlr;s&qd\Y,ċ6:۔ S3֋SYiAwǠ0miRjrO5@5zuc4x;`pӘÐ>qs{Y@5/V V{RtQ Lêڜ^stGNo9Wj J]Rt4R݈۠kX͌_AR*ԬdF4-k}^j.: v(0z#I\\ݝ#*E5{ ucŇ|@ wmMyd=-/_;j3밟X8-GP++ [ s2PP=r|r2y5y .0v-1nwbND1F81J挓t9T#0L0 ʰHmuaNNjeBGZIQ QTHr b ,2$ ύpG ŕ5c6".t;cE%! &WA᷋[P4=yqsq]ێC6q#EODHh-sS0LKe4bCXg6f粝 LʓTcJ+Vsbc}el6L!BI?@M ۪9acxJ,scHsP ش;"m|K,rZHsli #^?~Ǘ^1/yQa.'į _e`3 ޿pM+0o9=St;-N2z9?guwS@}t>MU* ҫE&[aL\϶W;ٖ!ƹyb{eh1xvqk"x'Ó{'x/X]?~|?~{^O_9*nU/)66E#YxwCTK@KZhe>0ߒd.&89M%{47\Mk4%$=WqgM֤ OggӃsy!߱O}!Wt5jD=Qj j%%ʔHskj{m}7?rmG~(NV_i(swBMz/BW~E5kuou_&X$)-l>` B6k.?W/'X=99#3MbP.9*vD0Y0J:K{.&o] xK׏ +C}CL\n9chEp+ˌz`4}m w<?p^qB jCZĞM[Ğjaq;j'Oܺs dRI EI3>G9ZY`&~AIN2fsFN/biZ:ؤ5j":p+tj AVEn!2Ÿs V<~&w7)薈qJ|#'d…\1\  R4 D ',8V3kԯvYuL/s˜*X2jeP)Ih˯@ 򹃘+T-%öl 34U8<ÆFאMC>!}XgHsș}w+Rո"f؛sqvɷėҩZ#b7W 7HjssoGdx__'ql&u,bۡ [͕~>ho</ȺF%uVPu^R0߷X  z/%-*s#N!!"6Bt̙wG&a0jW< 'x'~b|{jwt,#%rd5Z>8cq[; ="ʣjjIR:2Q IDAT,]&Mb屜=CjZP5Th!;!meR4jSMH]L(i \oOѭyPSBO b@".Ԃzfgt,._u*JꮆHDW#ج&^C?_.t:@z]UQ.7W]xne Aآ5ǃs+%bslaةw.$ ~z!(IJԺ@h)Vvj+ )ԦY}ɶV L;g/iy[l+#|aԊM=$RԘ%cB [+AwE50 țFkDB9!?"- )Jh(ۤ>jRi-yݏ )RtwAhGH&MiY/E sڧ`kRWa%#5GSG{9y-LQiY؞b:ґvTi#3g?.Im&ɕIh UR!J00Lmc&8t4/YZ<^̗.e"zT/2j7fl&o,87܄2EUl26ɕ=AuϜ WO7g|;]op׎pLi\XD?ӈSvkg7|]쒰d'mITYEyDR91+Kݨ(Mc`wXke-quIPSgdCqJg1C v B`@MX4yNMؿ`-/CKc1$6duIX+zxF|(Ϲ/P^Pc;NY,=`J61xų{N'9|So=J l2,!VNXjS'8u%G$|6qU!1^chx kr,[ qٔlGS.GŨōx_7.ɍKiԮlǗ_J!?Wif[ZvQwy#Gۏ~.1{'5cw~&61x7!W\_#y~O<OR  eYfeZB+[D,x&y&%aRvO˚T Je}ʢ.%ElXݵ6N6u%JFLlNM[Gz8H տ ?bAzR P昭 bvfzs/Lأ 柾 ~n10]|Wm3MFYfvoKPu^ ۚ2Ȩ2L].-sȡJa1[z|h~Nd4vz1(An0WCl_~R;F]uA Z;!_iW>gσ1APb6 nx'$sOnhs $%=ŐԈ{wʪ!vlTށ"-ìU0U[[N7j#cxyo紾f\dD>0':\k@6Y})D\R $шw O~i~TO7_Wޞ)LYnN_]򛯾y< s"%5]Քj$(է  gKN8jxcݛ y0Q&Cq?] )lRJPj Ftgc39gؘ bI_,5|_}.p&RBc]Hiy)s֯EM! !1lݓ%ޒ%nQKq-Atw#n/m#F_>7 '3x٪bQ[9mq @!3gLzF_v|,g]E;rP*o dgIJ:. '{_399t(?}kR-t([0LoG?Wǔʠ^q_N/>MWttOO}6W\\&hs'X2糯/7\tUsCXK#'x' (ne& J@C^֙{*]efhed6¯tIJآnH:=7QBj5SY&nHD( k5^kYJHFjTD 7nD8b uXAB$KK$E9ΖB)*mءۨ44 ^?kJJ$$BI 8g՘'"iU$VEBM!ftܔ@,(*R .j$ F !SRSBy6PDZ! W%4d&v(Xeal$ Yj4B 4!| 5y:d#ِէNZ8R@JS[:0+0%ֶ eRU#jAh愍 1vbsE R%tl)}zuxč:&Q*F_ %meSMB岫&A$,[UWdz^StE^4r`AlSkúeNZT))TYǔ"t-%>fDDPFiUj-BEwarTǿuu3=L27+"!$ \V\0(;H rAdGO& fޜ7s$P[.P &0>bF`(BpF҂R% Hږ(1/^q lkպ 3NHCL*D*j|)p)S$6 0CrzP31DDR,t&8Y1lr-ԪbyU ׆xʵV8Vd4Ȍn͔B!!dC[q4 1 $@HɨMcUuJNictF JiBJW7RVe(CcT{aM`XxXxNP.5AA+بf;L'Hf*X LdXH"EL$B2d"$[T4rR*RB{_PѤsFGlJDQ^]Lh\~%(]E!LM9_G/ {=6CAhD %hZ$Խ|B>F#B "qL?xU^M n%<> 4hMUdœkEx  EmM4Vī%6iRNdb5 0{7 )aF˜3Ƌ( @ PQVmkCP)0?ɍ0-Oa8 ?Q6AzB(F>E)(]6^*y)( b{k ݫJ~Wk#Shv`&ZSf 2Thg`l8yD.L ^VѪwZkDU0&PQ?V!-Ke*A6g\*1A[[W@n"5[(-J}>Fz:Nȇ tnPu: Nm@Q1P&FgD[TCH$$E%oI]I 듔6&qa|F )B}T1Yꛇwp/2H&6BCl0_Fr%pe$x1/KR1_^8/ E]γu.nJJTS!N`0f%pbVK//˛ĩ1A?P$Vl$>a$NyD𿃔bQ)F k z >R'4"=bR;P[`>D%Awn<4EziѤѨdCTEG=϶SIE'X!l'D$S%1NS]G5:4M#=yp"$UrS69%Äf_/B7C_11+QJueDZur7S-%#F!LJ_ CFt"+1WX@RuLRx⮍_^m?Wl ) }_:>^.^^6ir!偍HaUV"=$JLkX_bq ]tL"yym'/dN6*Z!t]ib/zhK^ k=BarC(1_G0e)0%[p T-d Id!$0JDQbF o¶t!=qR ( ( #BAAhK]MfbHjI/HԆ&z*x*.DĤ2 m`*J'UJACRhp%.$Q">ab$I&QBD-*(H(GHR1XA5nU# L1V'w1r1z0ɦz66;X`7Bx! <1V,3R}+Maji|?Tbi!4עL$rlvlWGJHeP.?cZ13KDΣz'Sʨ!]6Rk"1ա\VBϓts讅_1rjj;=%.Y,n VA*R2>$&|aj TE4R%po ^'r!U" B9%!Q'EN2n@oJI-r<* z=7R!F4ylRJŌ8N8NEe\φ$Rhݕ0ݶN P%E&,Zm 3bf諄QUaXAvtlCDɶ$6)^|w~V0}91B Ie$3YRȚkZkvSq6_qUZVZeRAJMڧhA(+ϐ"mQ6YȓmD~ #jiCka IDAT4YDl:kk[-Kil[Žx$eCjCK'-"2tHGhP&\"V,-[Y"}a?jNe͆w(l 6" !$C(Jnb0~QMHuT02.D@U#h2z)#*RGρiz"iTj>~\M5!*C万9(-.C~̎cs5A xA줆 a%C i զ5Kmt;.dMk<-_hf TrD\"I)ÏH$9bJHdbcbGB.N0 (- &$T%uDZeZj&׆z'B!79)B1M_uT 5RR}2KP Q6LaRuäk 5Ґ*rZEtFb6 ˆyU)bnvP[\7q|\1MTV4FP1C8RdV(&%LIR(3[3ZHrO~:C눆K!#ӤRv!IUKKdxA֭qqZ|V{FU|@P54Fh0Fe 2AZ-6b-4ڂWQi Ũl08TEL4.8L6,) f )Sxx o eVj-';I=G]0a2{=;2Πb'F6_G.%3b4HF@ "iʕEbJ_Y"hlHvTH)#aqh^Ieygm8"8v*  7વϒteh6t=]E-/{YAn!$k!YCx ^$6L̬ab$'-}LW3lꁽYݿX h :\G;qtuӸ22R'aH(9FpJC3&i6H *E(砒jv JV;WjfhUEo 0'dӨQ2rQEĢ(D?BAava j.R797Q%fX_N(پC8<%@vjg45Pk2^dtJDExo>AAصvo͌vvvյIA<:yNm#40 "Qr$ѱHy3HN.aHJ@&Run&bc)Zr^ KPXH[Pq 2t1Ls:OOU(i 03 LkfZ.0[y:P6~2L>hfh:TѩJ30HsDeBStTvhcL3PV)TCgkKԪZl%Mt`~/%DY2F Wn#7fD"Ӡ"Z[ s^3^E]#Q(l%Fs ^l`2 ac28IF6')=d(| 0:%TI%~^Zm:RF!RBv37`}`#>H"&YmbS,TE$(TcJbZbF_AȱiE꓃4tӖ즍nL P$6hs@σ!A2 h D6F*5*`KAyLxɄ(AT29 DtJٟF5::UJ"Q}p||MUYBljk_'tya01e]YsK- ZMAR2|MyW9R'ƺI@&1^HSdC꟡d)406|~;:&aJ$L+:&CI8DbBk-i+GW8! ThcHB:6[6)F2!3zo`_P.XXDvR|ł, :q}:'벱Ib1'$ $5R;m6_UjM|AªLmXg?,x&xI|rxZ(| {K\^  l - [Vظq#'O``7Z{[=_pGG[Imb2Mmzy듶 u5: xk37[?? G~|{叵VV?E6Z~30ߠv]0r{ma~xmzYu=Ukɛ^mϣͭmoǢ.ڝ[`e?I=km{~7Vm}wmGK폚}A̫|w _Um2ߏ6j^cigZk=/P1eWZuxVAAx얉ԩS馛袋} 2uԱG}4O=|{ ATewq122‰'aƬYƎϚ5kAAA[&~pYgqYg;ZOxS[eҤI{タq뭷h"}6FO{'[XnLSN9}s/3<… Yv-SLN㠃}=u]<<#c߮D {=[km^G+;w.=Gq_~1ѯgʕ@l=[AaO%~+WdܹR)8Yr[GC% 1uT W;~inf⊭6wqg}6&MB4>OUOE_nf$Ik---w[n裏~?ѷ ž`v? /0}EnuqWrWа1їfժU<\zXAyG[[mtWrEڋ wJQ7v'VAq?aǍ|rN9:=ַm93g>0MZ%Nswr}q*3\s Xl˖-(\AAhK^?\s {.m/^̣>:րm1C4%1gV'r<b1b'pO>$ rG]A{\e\.ڦMꪫ8I&[UDmtI'q2g\K.r[]r%g?o`}/D_S-?[AaO TPKx: {1< ¿EQ8p)oy|km9ŬS{g _/5-/}Awпys:)   l   w  w o~ G{Aagإ"AAAx{AAAʼnOAA_HAAAʼnOAA_HAAAŽgm+9၇Vfw{D  k#*~]'? K,a̙osRO΂ۖ0uT{6mX‘GO9wYI>1{L:;ydkgsG3oųwkCϼpxn{N%}_"{2ߠx<zJtC{  ۭpsܹ${h|Ok{!W:/,: .'Յ3u>u7̡a7}tNޕ8> |wsroO?mSgrN?>/1:;S .Onr; ~WcH7pgK^p!ozlAA-Vȏ8k_#GD QT ${}rt3o3o΢O‡Yc{WSf^̍ߖxN?|'ep/ra)_g/x5S| O5vvwգXǤ&YX_n|W8xYr%RbH>s>Μys8q2444^~e^~e  oI}x3 >2$$J:_u\| dwk6/o|4=<3`|Yx -^1,8Tᏹ XtN9d˷+;%q~壹gvr>\gpK̓wZ<HcZw^%F2JD#[;XdkG=x^<"X_+HvZW/נ@kInB Ї>(§?iw('ts?3-X_(')AA]gK{_\3'~#HA0pqNK^{OW]9~878o='n:H_[SE04Nu)iw,y_ck{pq{a;w~8FYx쌱xκ{ |XN,<~ܺ'|qx~s}f}3@׿3w5NoYl6qF ْ$.H,=s:  qY+'ZHw-n }V^=_J|zb1"]8s PO޳8tfv<{^n.  v+9I s])^/.<}KTqn? rԵwеTܶ/}p_.Z?x8ţ,3>Ofʧ\z!,<~g079n_O?z0wKeP0u)xdIŧncq:W*_>]信6Nzb"YhVye{ j񇧟ǪzxhNS[~  ®[\} 0Ƿ3ky!Xe r̙3%|#rwr/rβ#Gç{s?i_:/c㌻%w,cG0\u1s͍uQ}s2n8G=|*i?r>{Nґܼ|9s ;0θ!n8~:ZzNՆ{{2/eH$#4%"E]v) `/"nE*^zp"y{?Yj*3i]6?p=OsZg鮟 =dYib U\,g BǬ~ezhi  l<:d>1CD1\8k>:($a6uuu},>|>\Չ:X#%Kfo]ʂy's}+Y8gg?;oO?_>MƍKY{n=\ s`m#8wsp͚9ο'ϪUbќЯ>k4=)_΢e_rXogZ^Klɼ72w|$7}($BL}TBsMlE}l:4$[g';VD)Uliؑz`.'p\}}Yzc1$3̽O]]ƚÎ帳#.;%MUPF3t]敁V}\WAAصoKɽ>B̈qw{t?d}Y A:fƏ~&xa/ClVyő_ܥ \0|4z7w\p6N>E79\?{Oi/o{ܶKtX<?M'\Og??1-^U_Xw<~E|ۗ34޶l,q7Hx2oXj'y'HU-d7%,&lC*F4Uq=ߣ8T]4.TN]ə>t\gGlڴ#L^4U_>ʕ+4W{dY.Ʒ8܋l~3` uŏXp֌A #UΞwmeɒ%t|338뾕\wa|s[%}_ir.?d~C\~ܸd^0#ϙoyO<_.~Oܾ߉3a9f5Yݧ='}E?N=EӸirN=n\^u ,8NR?X~u'=kqȠޖ{/*KOuݿf*0L:@}M!I&bKu%ݣRnh!65F8R&Zfii{2Mz\1IDAT,cbUq>d<Ͽx ?nw<XsG'u)t]ulq)U;  »=Mn9t>6Z)&tT&t0xK/"$3Wifߟ#"-tL[~IFvۯ+3gr-sͩ'&F=[Ƀ}E>̩G -qQGl;OUn:HμW.1 _ʦ{Tڤ'-ҼCz7yO#˓i *T\ wv($j b(:>z.kGKAAw{>y>4sX,o[$mdT,YI}#/3udk#vU[Gncr7M+38{Xd9'}>i~fM;`?nGx~ cI߯^ܣg/civrn8aZ;0&|2Qq,6j1Yd2y晬X#2-xg8C1=8M%BaVtiU RBcP.!h!j-vtk8V(҂ nvɒpI1)>3̙ew>͏} 0IKGms-nai?vsƷǿ5RS[KۍaCIEDDDD?f_slvDt!`ۑLYY91oSApBakǓ^PK'вr\e>T?9ƁC|1Mц=zpMknݺqw3覛1ObTX hϨ1X aQ0k[_\?ԥtt ""Ҏ:m v  Opg;HNv&%+NSxt[$YX?ϦT&g-WFvZvCm]PvW^XAќT< DҷHs1C^k'w~fjz;uw:zvaea[6^[c.~0 /.K/1St%!wac`e47âO}}y8y`-7xC^o}1Q6t;ig*v _lkn섹\.[ąKȚ<ŦcWsjb|6Nؔ695 v&E 6{0}~Nl 67ssUGksòqe@;hG[x∿`u9} :|zý}}:zt!w8y=L>gݡwð[ӳ[?xO vө_ >0c_s=~M^ E=Js&$}k)i-&;-Y%8'WJ$ë+ؿ9kmz 3'<=tKYa/3=EΌq,}mW깘ë0y~Ы#9͉I8U<&ѩw=z>#FP]]Muu5a6!5Ϯz΢27+'̡Ct#h{taEfߠtN6È,  W/ak]ɼLKwSԬP/$UPoɾٳg9ik/_ gsYSF1kK geIQy)vz.E㬿ז?[cǗ_y @᯺cQ'|T1~9xrʕ+yСC۹J֩0ӧsrYin`>?^'::@dNi.\.=۶y׻u6+W ~5Q1|XYŇ08]\.m )B-].mZ\.]ud==q)H[Ç SǏEW_ۏSǏQ\Rʸcy,"""{|ZDP\R ТUb':WODDDG/ ""rettuDDD ""W6uDDDDDD.s ~""""""KZeO"+ͷ%e.uDDDDDD:z\DDDDDD.sQCDDDR֋}vynjNwt=""""""'*3!{gz}IDDDDDD"=6H8S'pVwHa""""""v'*9} .b hX(l.%*:.]vD""""""Jr1@(YaC&?srMD~]IENDB`././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/docs/source/tutorials/images/application2.png0000644000175100001730000136261300000000000025017 0ustar00runnerdocker00000000000000PNG  IHDR~@usBIT|dtEXtCREATORgnome-panel-screenshot7w IDATxw$eO=yv6 gAAAT~=5qвZk_;e~ǭa2m7@}ѳ75K.zwSM}c^AA! B]?z]qB/99B݉T( p+[  ͒}o._ F#Bs:{^p6o g@r ܫA u9UۿzWЇV|iw>V / C|ˉa8.<+S5Kܽ˺yV/1tsZ>%ܮ;p.-gꁟ#&#]]e$!NA{+zм6Uξ>e]g>?pŎ5G ?QMAx àC/𽙺%ds |n=sǺ8yy;_5d1NwL߶-skY")!&ۋyGǍL>l6$p9pauwH S/> yeW=';V:Nk^ 鹜u_gpe'|BO+);&x%#OArW$?UdWj[%?(zD1@"wd9-ً߮Pn1>y|Og @{^MQoYD{Jw< žEᾹ52=p[0w'y7׶&WOz;38PN6̉. eZ)ә~/uWB6ܷogq7tS7HA[=2@1ogOMYfh:~w/}Oر7Kt+KNWy~zUzn}_}G[.8կ L4IoIq4Ǟfx`NflWۿݻ}5oK~c aT+W>{&X`w= ܶ۷z3c₝*ζ׬e]fno9ٯYgYmsGwgLnn,j+ h!/XK+_+?t]nwu Ͷuq?OЪrHgswݭ%mM'mzE)⶟]1/|)+y59z^.M`: p9ig߮׿=KM^c}ܗC[wO+>#;g;>o,G޺L#>;Ƙ6W5T="5K>r1r _p 9ᝧ;+O98 0iaK~~j89~8;n1?ҟKOX}9C׵OFv)d9N=ίE<{Cg\ ~ 6LsԑlWAط[?l-?b=?~bR)/y a18<>?w14{wZGt/|y}y=7\w &{1u5,|y.C,?*t_Xc<+^}'fwOgNlշ4 X(hH9 .q²9$iOqMo)~cwL8Tmxs 8ሇ';^ՙH?MS&VC䯹 qtL=x'>s鷎g|mo zsY j?ӷ^>=TF:19qUdշ;ֳdphnĵ߸oVLjQ6;=󢗞Mx"+wdI^n=Řǵm}gky7Br7wU:nk|y>1wˇ޽)X7>HwOx) Mjv|2n+=tMO%{3NXg/ĞwY% yO8)x,Lzdݸ}z =iq_ֲQ޼dT*7̿Y v8Ǔs.)O-vpp?{p}63cg/oͷ`j2,ɫ1{mB楌W=\?2$,dyǐ%s/?acɡhpFca^-kw=xW~-6v$b (, Rk4 eJBO8+8/?^7;58=~nzFV+Fa:-ڮpoHz[2qJbX '1.^3+nDHJpHWIC],, :DD`ZcՀNa++T絇wkV'XxQ"Geyb,jڂNۍB%H2Kz]6s'sZ*vH$ C. eOt]'rEa6׭O “ۛ^z~~q8еS:я{# aAAA'J x=~MY$   *5?AAAaQLg]ŹJAAAxں_]~      ~N$~ $x  <ũ{^ wy%'t^i;AAvocOd'1_Ax/H/G*AAxQUW,J"Ay'~A O$ £?AA"="w^D7a{˯&OAjD7/w^1'T$"?AAx*?kզW]w^n̓˛N~Hdfڕ7' OAF֫2g~>\xSt]/{A)Mo|<]P}:x=-{:. S̵K^2 o͚5uY/a~\~/9_WpAJ$} £$~>|3h\s 7oꫯ檫bÆ G?2AAx:I/;D7A]uU|_wd2r~/e t#);D& OA؟=.OTen… g_/^-[q  ou#^O9%OOhwo|}caȕW^ɱe1oE}`)B.D:zSy& IN׏/gJ֍`J܆m-+XiX8 612 k_y}<~[,=΁GMr,=Ė{lg ]1!Mli ŢffQ?$o7:!Pp0K0%Ku(K3I?3H5d=YԿE,ן$ߜ"ߘ&ל tA/8Lz)RX`DOMiI<5IuLj&Ymb<̺JVbB5Ujnjd#Jq4YXҢ!AcC jxUkaWĎ?&vDQi稴%g+ih&mäǰU0 C P=\w;]?c,8bGQ$;Mvݠ&eب/۟#.< t@  VF ⣪>|:EuKjX}-X#`:HjDΔ -?B} ͌Lݰ{~yޙGK_q1\M ˴Ei)FE*o>E&6}7RK_:U3EJmư$^H_P/Ҏ⬑bt0wKo&83U;̴CˊӶ,niii"v(O, lMJѧMϔ)v-ldm&İ4NIc%4ԱNd-Z~oD*=<WʇM/}ogw׎}z^}ɜ޹\GiW=_{7j\[0i|Wr/λ~h6}ޞS^=]~n| ^CO3gK>G.}yw\|=\Ϯ?Er%_xG둙9q&>O?ߌy 2 h 0 2;XuJ5O2mLѦ0{mɣdJFR6O!:F&[GG^a/?H] ;Ă64}4}4!_%㫸#7X[BVjZi)ђ-ZJbV-6b8Ao:=SL'FB:ip:N3c-r G$*,Qn7f IDAT2؍gPOfP!aR } F*T̀QYfD4A3vS!Z5dc hIHST_d?$nQ*$:V@&6H(MLX!鷈y%b^'S$ST$It6q7pJ&f"hc:D*FETh4'tĈ6HԫiTU=h=.j2Q K'2(HؒIJ3QǢ$]ȧZ4iwKeqM6M_v^&AeG&Znn&hEIqI.e,ɢtBva@iFJ,(Qa;Paqܽ}眽u=&&'GE} .S%馛v9_QN;4N;G @$~:a[ƫ46l0)MeI^Ki&7| QD|qqVecՊH ,-dK4´ۋ-GG<\ "D#z偩w`y&GhVt_>@22eOݵ jEѾZ6R$MH*!'A4- c4q{ er|=GPr:ٕ D]2:Ek< 4$"M!Tʹ>ZS-U3ߨtm(נR#Bl=hq>0 aU,PL^pxshzA'{&rr qu =^CU5t>IRe)tnP{JɎXn·40h)uv%A{a,қI/`Z<0B_aoH2[Veٲ)]Ca;5t~ ~ξNʕ =;%6;яzltS^CBhzvOýw||gsqDzxѢ6of%?.7'"˟~?i|sa}97vSS.ّsI>gwGdNEݪP?AkI˴EmBS4:iZaM2a\FW\ 4)}"=FԖ}H0 NBi$1hmQ[rDNfq )."$\& QRL%H.Θ88ؒI3JP  A?qw ,FiI5RHSChiZGGv+Ld-cj@Lt4cKe=DӻI{:9Bo0 XQ%P|f`ooZH*j$zBN`PL8kءA9 )j$( f;hE5$L,4ja8R,@]D-T$L^-0>>#k<%NNK'Fq;%bT &q` BlFHFHؔ#!rb(-bf4H60dKjIL9`K&H&vݤSq-ա2D6&I8 jU:<@<97P$59FTpeV'>* mRIM,$&fԡh9:u'F"U!I5qBIhK1Zr I Q]!V7l5j&aJh=/x1~ӽbxHBGƷk"a5ًCϗi8&'C"59"8Q@5#V%/һpP([ۇ*#m#'kU rn-4|Р;1WG"\qvͻ~ve5]1 8.⓼կmoy\x,Xkr/_>kcɤYv+V,+9'3yݴm`ϽH{>wn|!OeO/pTJ3J%rS[{zzhJ" 1uA>g%z(O(g)Ohe& =(VH%* [LCغNT0$*Uq-lO(M4T6.dtb21&[$.,Y"3Bgm]2J ,)2JW b1$0cfr43YL(3J8G=R/`  S3e,i&ZJ=3_j E e(D P>jb[29B._Fi@ffp"1d8l@UdfTZ4l%3R^h$룴nrNi"h?-iI(RB:IW$J$jӤ=fAiccgPgq\t{[YfuoFyMGO#1E2wd HtHt4)Yer"xI"~ j1jcj&^KCEBAFABOćʷ /.@~C;$*x?#[ E)OQOPL*L*=4d/ ۤ6QF]MJquDA~b%LGR[Bd{*NǏ. 6\<@Nau0 8TܢD՗Rl^D^``h(gD+T< $E[dA7yC4m$)hR~N652:AV!P& DF3E!Ь'`ZlAJ-qud#t gq|~hX&htc ƘNYHP7^v8$}/x?~˵qfz{wߣh<^q`1~g~o 7qG>Η/.p1/jʓNc9s3>.etkb3>쭘4^iZ{!/9w<~H&Z-FxLqMh <<?N"&2+YVJVbk ,YS]65ADruMqpzAV(ܔdLYHTQid4IN&T 0=MT(8mW(=={(z&nY>:W1-L Q!m5z567'Y2͒2Vȥw P'2!>$pڱ$u[YeqzY!I5RrB=Am4]=QJ!#a aȻ Z)0F(!;(π2N:QeyZ&A&Jr~5>wgihizx2 Eo*X-Pcur"1Gc,L,d,.twV>yB0;%}u<(-8 _QMN ;&~MV›i⬈,7q!#u"Ar2+rk9${73wcmL5Ƀ=wLɤX2*qL4,BtZhȰ[IPM(hBfdAV4>pT'o`&}%%a4ugC0^R)bbaF%4Ry$*,%T$"JYGf ZrÜWb't"q+Κ5/ea|"8S|Ex)w(=8/_y{ϻvx}NzX,|3>8]{18qv1s`iif>sG/!O&O/dA*^g{&P,&~ ExJKSQixAp@"-|ۧ5-S`R>ޛؖy>}d,٢ Igl<1ydl!@S &0"U/=}'$m*b+7pN=7]3ᐾ -$ &i)4Y^?C]W'쌈CP+ߖH)">["\|,ZS!WEUm;֢42Sa"ÎtE&fPMGy%>S/4H <AD"2RG L(>?ûܟ?xooy1?%S>#gsTQP&=[=Nx:{ɯ\)gv,:_C3s@C HF~cwvFQ[>gnTb~JԁNiԕKFɀ8+Z w%OxfB`Rgif[g[b[%#Šc΂ YgKv: lcQ&Mӕ)R"%bGtNW+e*ײ?ʖCQ)XFՈjz5bHsĢ[VGZU``ϛ :epzЋԤ,*AUQ^1L%S{Ie4n::B}|xCcJfъifq݊(bgEÈN? ͕8fNYb9Ʈor[fQFjl)t6246 #}CHzcK|2n퀛A~ ]`MQ=Z([\F͗s`"`mځ@z7FCEZl1(G2hnA[kFFw;R/ }R?@as55ь =! "?:/t>1r}vrHt2`t-k¢,l=P42dARsk[ΒB=wE4IĈsy SC}b4g=b ;[RM'140}*;bVxv!jy<ΎnjNuAc4E\!`A)P@A.` \*.kF|?0r)̅˦JAbb@e冹QQ)Ԅzr?@ZŀInjrA}Xj:C},IlYR.EC䱆Ӣ.7u(LpC03HF NѹlňOHOFOG7~&ٳg~qO8?/___UwǷZg sےl^x͋}e5)1On9]X65ds*y^e씃( d/.>ˋ& }o- 2zᷧ#Rȵ"dM5>4`;L"gK`Sܮq IDAT{GiF5qbn_g1gU"v {.+_PoS [X`Rd-SLJ#%!\Ցj*6e,7%i#b@Z$atvf@Ox"'|CCV1kK¡ѩX+j ec55hEy$?=`4,HbmbLfM܇X/tN1ڞ0zœ×VIin:GC<@0ᭁ=q=9s!9C7%١J-H4 yy*3;7!u9;4dN*8r[ؗ(!.ǜ9spiV=K,Bl+y@p(ր99!AXϲ1FX3y#5<_c5 {š2 -VS:- jllkܣ!kQ.:Ҁ.5jSn JeBk &@łc'd p\A%eS1l֋1ru NA=TzY`hʈᄬSoGz>b*DrsȢzʷ);l!rѵ́R``z' /lV4kah5&lzyab[5*c$7)~MhoXoO#n?JW‡%/hB([䳎Q>bD w], u^iF+S~B wOraQPfp/>q{|~U2-i dpohGG/8Ox|oVOx}|z~ _ߢ򽧯8%O>/sC,ы#8t $_)үA +xe A3m>) )#+LM"5BsI㈛gg8yE`8 NoqE+r<  #FƖqHm!fZY1f-D |5h0eNVx.8$MڂCuԡ0V]t)#fOݠP$xNٔakfL#֌M{/g,3kP?@Aр3`߂}tRL44zűp^oh-<"I,=nZ6x# +O?* l~Qn%jDG8?_9W|a8VfVC-:==n~^/JRZHiE_C`z*5,'>Jl1 w"<G\ g4C)F>A:G1+aEY\hGN([8X6+WSkp14ٖcvs_T?{8Fk![1$17-vT N, $x߰" _JkE!8$k}V *cz,Ph  /N9ْ>F$B{ˎecrDlu˭ǧk5JөcnxUhmk濲bx_8GL .ȅGFh)t[83P`^Ppୀ/01!޻#4 J_j2k ~ޚLl1?%7};=ݏVkOIrMc6B[tu (ZV=3 G al!jPJĽ=q{ sp3<Tʦ@ A0`DE1(PAkDǂ}bk̺;4O  275Pc ih0bqNS1 MhPXdy@Zy@hRle !ɔOVy,wF-*I8gsH`a.q%IlB;[Sv5c|pOtlcJ G DxNHǬ)rHQtA33iPtT8j/"E- -x9awL L t{}nu(!$a=`aga|l :S,v A\E쒈]sD p1gY̑V4Z:j6c-4C&hi3($﷨B ܌@eQ6(E,\CB--VUs9jwuka<~:qŨr][xl, N~^oS8a94P7T tl#v!m6Ĕ5QsW$TkXDhP!}KלZ8<MR:ʦtlAlb0M>8/)5)Z1L}!Q[*7VA;\y\ aSi]#@:%`SBZ9 /-2!EI[uԝF:Wwo[[–GC֤O^{.xy󈫛񍠽1.)!ijj ()q*wX&d66h( w=W;O=:d:B(:ݸu3|ySO#~9%}w|<_gk?/T?~V"?dk qvbhL:pDbS2n- HBTȧr tgJ-$ &}%<ҧ..쐫w\g>B*4%UZHU@B+8X^ȗpꢟx'>g5建_/jt<F4T2QFSpF2]%^oe|I<$w~}ke,֦Z8#d* W>PJN]ѹ̠ݻEJ@^/mjگX%{ъ+"ޞ? b֟O_.x㌽ %%m'xtM,Vc YZ#.逝63"@rڃă5@ʦnlԤ[p%یs< B?l}6݈wd!h:Fm-s*C[tG&ݱD?Ή4 9>~qQiZGkmk1oGvt:Ϣ ](PNWrh>BXIJ))y'#yA^O[Nܿw8KGu`QYK&qyeoV(ɇFB9߯Zi(Oi=g ooTӿ1Lv{c.cv!X} d`mđ9݁m]ۯSB,C MA ||v:U% ~a3eSMޱ ؈6dՎxќ,~': a`>IRԻzQӞŨc<_"~a'@|D~l|v!'tJ7!3hK'`gdOQ㜃z'?]crpSaQd?hqd}1?(߃MvڅwSoG9{kFy^p Z3xaPo(;kÙ*Ji Id *vZE(X@4E%ƢI JC]Beg|?;^ͧ~O_/fvdЌ I x#D\O5 Y삏3S}FXh@AsZG$ƀPRѕI7j.[kL}*IrE/ +ێZ('DC='l3f 6EK)DRld֌o]pe=?&BSR=R 1Q*| 6 ߽Da<3 M&.7>-1o]nA䕸[^tw +\t;k>|99Khd'XFEŌ%Ɋrɕ$dNaa5'.y5\\\\.\mnG)ޠ x:xγs;uZeBGr.H95|_ۂYŎ"a$쫄uQ&El,N! SZ 5N4p3?d"p)]%I=^0@~@2`z\u++iUK=ꑅ\y¯ڽ=q{ |qfHXZt6N٤cĕBYlA`%NJӛSc`bd:i2|}L,pv~f=.uP\KG 4̠ JB?alDi)Z!oWLb@XGS" < M ۡ쾅Hi)WH[R5Pi6M؛-lDߊX4$ޡiԍKSTdO6:F78YjA4u*w P`)-A9)Gġ:va8v^Qִ:S6Oɢ;f9'xӚj沞z$Ka ZQAC7F Ч}.|{ukI)[ C@$UQ&-M$R%@:ԃn(}rHD@c[0fAed@iɦr谼G-ц;,Fc$$) g+grOb|8 ê1*j+RL0tZXB&#CXL7$NPTY Mi.mh,\ib6h֩kY 3#8HӀ4) IDDAњW6kܪ < xteR)3l9fx͕U0f@jFDǮY6툝8!k5>() 舌nBWCK9YOVI&tMZˢmMaQMahk`:ð,=K@:f¬"Gt)T)fbrǜnZ&m>McҴ41ۖPK_+K.m6B8¡4]2!|j->>ea8n1'ԩIr&7M{= w5:ˠ03iL.9d82n7 7w%!o'.Ɯ]qy5\ h 4{!pk &ftEZXR9fY (DNJtGP5ԁK(]~ǬYzi] 8 q݄I܄I79 ^V huSu6XǗ$itdToWhmڡM:Tɐ0+\33s<+5 d mӼ(Et$a۱W}hg0.!In[ vgXs gj8锭K;%VA8aT`Xlm՞EyT0oPscr7)usQOd:&E] AI&SMM +n0.c~wLZ8k!J8Ү7[^mj&]PtP 3|'?;t#nM/pԿd[OxkS޵ZA Pp&EVBs(K[2K5f 悓7MlG ݤr*ͦ,E@zބh^BlVKuR. 8InQsjjLvxlajO[vq,C&D^p,9DzBԭWR'&vGaRf٤C ubc;i;iq&-"M]9T=Q=q{|k~ok8K&rdcggxx ٳnPRclvoSOVXdEQjpu3PG l#V5\j,IYc֌v}E1pNszZz `BA҄ l!ȀN"8 H%̣OKMMzӠ\I4AYQu\2mO4t,"w!eMn7hAG=Hn' -}Pߒ~1`:bEDi=a$ÓkK/1_&?!O+$ߕab St5 YDt j퐷>7>;;=bOܰ&"$ǥ+Zޜyo~0f4'퉎6V^ŞpsoP>S] ,iDD/ׄĠ+,&}2j6ө)% {A~{ NԽSgEXLb6>VTf\o_#6ܬi A[MN%;f6?^ Is1у̜'dY.7ؤ5T.PF,fs≔nmҾ0>7Q_%}_W|\p4 iE01vo98u2kf[vnջ#.rrdxdţ\C!p;$$e>M{'~ĢpӜۄIx|rC^FPak+2p휶 |R}/ԟ%_VE_ep֞ u)*թ4rÉy NcӒ*I:"&ӹ ۻ=q{ y^8Mv[e6'pque A@--P߼i DJٺ42j,vPeTբ9-'1-#FMLwU{/i&8zE'9%RK1v.JNrd94fXm[ "҃TFN- ꢦZK8]eaTOTOhcam=@)@#rb 6r![*A ;<=]Guf94}ӠE8 Bk;lkBQB_uԉA+Ne4x]G`JؑIEl9oSkheH404zۢgmI]jĥ yGmjΞb!ۍq(e8 QIl3*4TXnšVht(%RV4uNF4++p׮pnq m7q *)j*X#ڜ7&C tJRǹ0+v-;I)iIi-@DBHLH$x2jN+H=$xX=̉xssS.֩+}"BWxQ ɎO,""Y%f0P[s?e^O$ٙCWԙ{Apw_.@UjZ9nVu.8 3hZ~f",2{ڍq}u>OjWjE☘E5HW.}gikiɨ:F"ӎi!K ׏ib\m":ܖ\񂷸8jѡKKTxn`KZixu:\dIlmW#Y(a"BU"SՈǚ0(YNUK-&NQ,(ԲG4Ȥs$r򍉰cfcIw8#;0HMxG VH#VزDJ䦉+"<LZtlazYz\IoH`A ADC}TRa%UR%yU|l?۵'< ?lpg7ox1r}L=mѓK:bIW%sP-m@q'(2sZ uEufܫc)֌asJ[r3wmn6dYZR78j~3SMa3M &KeۚØÄV9xHd8HO޵0&W'I9$ϻm!A8©NMY)u9m }~x b Z6[zњ5% 2F,l9]}e}Į2 IFB&N(rE:uI&.؂fA͜)Xz]x`u ] oG%moZ MB(8TSg ML"\"p-6"{[;4!wv 푺 6Hu -Wx% r^A7XQ UDlըF D5r ʣ:ZQ!Y 52Pmy"9x(iR|a!;:?#V.쒡5+Hdc|gԉ.鳚do3jq1OiTHPP5iN47Mo'3#zU5_ Ox~eo8k_qҾy[G.G_J&zلdƉ+d[=X>SIAXK ˨iN')i b/wW;V5[|MԷHGd$~//S!I $fUChuRL/'NC~C&MkֆM=ksКp=G߳[c B5C<5CJPŠzsDu@L~0[E~^61/ ^z-'fveH>/OXga&J<|; ʕI4(&z (XITY:tH]ʫ^3 ib 0Krd;[mGEP&!jM/[ҍV Vcd7`q{gͺeead5Jh(K#j`L{/ 8) >\= Ymֻ6]hgy}^]`) 2#$*i$r40̡]<-! szm3]O &aP%UP/ʯ5/:mV_]` [{gj~CN7s|k.v.P&]v23F|}I>4IF6ňkYZSTPv> ?Ơ4 NY * WHOO|?2*J.AS;Eu#_i z3+P=K|8,{,ﻔw&‚W* {w.GunR_ۋ9 /?š˻|9_0^6HOd[RhD_mEU #(ΎvcMYӖ|3"fƨD83THTFnb UFedA%%QGWö.ho. xy~ɆelPFF|0Ov^MlstijiCHǂk(n 1=7O=_pw8n}I= Ox~ 0b?[zjBWi.C8٤lmLa4̘Cc4+bI亴K~&M &\] 2-_r9 I@9-)W?<a"@;~GKH,(kWG,'\Y5i2;~NjKdN=oshk7`kc=d/=E7RdU v,JĤ@jY#W5r,I|%)gɕ_?o7r79K$B$!2 C.uRP΀-+LjTqE#.Z'Zknv@+]Vc4^cAPJR$;}/1T _"rN RB|ִRƌsz.qn[&곸ip )KDP!KD^+FQ  9s\|; RҠNҐ>q6c-A log;w `뷙8Ԭ벅^f{#C(3&5i yePh6)I+ԈKSۂRڤIM QP٘>;6̠nj 4 ϶%*+NP,x0 Rl.W$ PF\p }w3R_QݼyR6@\[DǪ0h)hx13Di[k~U[ )TRbd52 r"AIHd HU3GA #rć L'㸨i MkB*)Y$2m.C6)!OW~EcP eHt( sf.0),53/{'< ~/2\snf,fM:3ɛ&7& >\buq L(B&I]wS۞2oFE0RZĥGY2C5Qax[j6_JϺ=xʈiE.qG.qt}wECuAXr]z$e |m=.ϹlX8]S &դVTɐFb9B!,,qMfZHR8qIRu*<&VU "nSp5Ӏ/m7KXQЪ7S\ҽ}P  HigCrl|cKC4 81iijD&edWߖ$%aJ{)}0)PT;Eu'\Alڨm>FeEeJ41/C-9l--⋀X<(d=g99oh`Rz"tO'< Osa5{7g,Lwcfy[ࠤ<0t I6^=Yw>2 V}=xĥMYI}`S  G76)BƋ:`_{L13{GX$9\3p'4 G%UJh1f=GesD 2GY:"\ѓ)C2RFW\{ߞ6Xg> v4B|789@ᰥR0rjU{G-?MT,Wu\"pr[c_)ym\E|iS&f# R*C6*7z건,Ehxkނ5eNW`5 {K. .s;beR%u c΄acМU ^xYB5qXr&D")MЖ 2ۡ jYgdYE&okʎ~.Yz;xAt5V>A:v8uovoGb5kŚ˺ZY]vYt=>i4Rǡ<4E\PT1P iB!P,2 A#pp]bn4ٮZ6E*^?dW쪀PhᱩG|-vɐi`YHD e#,AHh"a1Wb,>n[B(4ɮ[}nZ'( ?G8h%KMXdMmK NA޴ټhS[),/vS,R9͑O,=k)1scaVY3+ΒnAk-_h'< ]!7X`)|G(Z'%ќ`A@oiyI"|hQ1Y'=JejҧE3) ~m44^m}oIMq15S 4Q_P_GX' 5ጁ9u&kb,cOA#1u?)W 1ⷡ' Ɔ\!wHh]P圠9|Ival2%D`= σ75Uǃ ~zvNM_Io8ļwI~M *g/0Ś{<6ܒ`SsP޾]$Yt7w3fF&[FL1a^ *k!:_Mz~6WMiYk W^SE9{9=s}8 GHY!EM(|Lwtslq-=Lulev%Vs.93/Ip8i!xI|O2}UbcRjo_hR-kIJD_g`6ܴ/)2bnw(;';^5Qt3C!rp`s}sdqubR($RI!EJT`Ԃ LjG4"+*\YMz&@?H{[.lE_0@ԥA]`S{!i 5ԁD j_B)P?|3IDh^qqA9߰Ý<﫿!7JϢGbgwV5qa9ӀXШCZ buM8lU@_=&tMio#=I 'QFEC%eb<2q|H$7;֨)uP;Z9L>e^PTA}_ o[VX2é#*D"=tHz Nұά _n?C6(3&s9לs+#laKdHjL?ρ ?^t?v~|7_ďu-5/>/}|맳w!B X7y/}קysӼu_Um~RTE¨L12E2*c2XaLx}9=p!⊐E1.[6QBR}*`:yl !"D!Q; jDAM\)[9SyPN0uA(|3Ja S !1I63U IDAT5֙l]w:ώ(:00咿{'k ~yˈ)eT)?J-ƍ# 08dMڔ3Y4 .WbV_.yCdk<6cg1n'ua5]}&Ijd;Ws`ώEcCfm&+2 EPҼU=[^os"% C1WP*;b3k3F7Bt4N'3fN(j6ɵIV;&Qj9>E'B<m8.IA4PVlavZ8Ncz5Aa{|}P-@c%vnw]§FP}^ o̬~`d=ʜdgSed}IMaXC* ;!,O? ~~zOʹѵ=1zKG[>яG>2*@?lՏP<'"H"4Z ^H}{c 觷#k}ѯOm?]?^>zy;-~twq~<^cy}קy= ,"8灗[c[[MVsܻюiI#"JxO(8f$ Mmi` WCv+0Ï9I0(?8o3sYBXrRJO)j6?qC$; Itj2q[|Pt = jBYi\q>[" MMv\C̺MjաWΩET+`bayaKubZ 9yuK,7խMas ŢC4(#UnD+<)5FUi q3ԑ@_X>dRulŃ U u_k 0paQw cVY\=[WF*bӏFڡ7o% Y ;6(:ԁ7em3an0{d`e"BHPXG~)UaRJEՃ p,KdLv+g/eEQE1a0sɤIHdUa6FS =RWTD)Eӌ76 n"3-r"3,2,ZޖhE^iZ;K n#*[.rK(}A7)Yc΁;A5i`6m҆E>lMn*ꪦ\,9Sf#T ,; vd8bD#yJFG'{S$])kL ̼DkA.ObT.qX$GL|NbƸRhuӼ>돺w1 LGw/Q19?1/=._s^}vuc4HqDJ[S3GbЄI;M?4 /+_8)8 [6&GV5ۈ.gzi{w\)DXcAҭX4Y ?}VܯϙFX(lt m&mR 9uoy}/o IB25\/xo.WGF-BEv`SlM&Va41G{nSUI xsw|/b5|!w8`r(6vsjnR>T&D:=]@v;!fSNN);î)vǬƚ9ZNp$ "|둼U0 ?eH3氱⼹椽e+}xysjrһ夸P8b+l>;FooQr cbڬ6kZR;krE]%H旤ϭiSr\zͻ97َhMlE'_Q̲vaZKb ˼(o,ƛyYo7͗xƙD36" ߕ[3]ʑu=1eEN%:/ #FU%9Wەa  dfb5o3(ڴY2y+-kOp'Wo;ض~~fYɐCc꣦m_޳ОwVRx)?cOZ?Y?:dٿ-}|k~|SC#?&_"?ݏ}}޿K#۷?OK/y |AGXӋG*~ 1!c`O?E<=9C]Aw'ys'8?Ӽ>Ӽ{?~k wQLA])4CIM.б QUe3Qk s%]<]b40[ d%Av9H wS#1-D^"4[!FHK4;) U(QRxVB%q㖺obLCB7 }'<',|vuV7 ,,$ڵ<) ,&ʕԩA|oA j#lUX!USZ62mQ;*szւ.sł!5rHmaQy rFvjT@C*0҄rS.%")5}3L?3Q#ƪ"6h l0Tdʦ2$yf ]fanjRmRY̱'p-I|EkdP4RZF EE+MQJaC~_[fT ;t钄6|·(}E ZRjIEmY kLYb %4g9b9R֏s, 2,*|f 2jSS&IРlTm Ib q l*7%~;̈|PunU74a6HA0H ,Ȥ 4) ^'4&ku3sdCnu5?&!vi<_kVBd7P I:&Du(l:z,NL{+Ԣ¨&ƨ u"K'tJK'eU!a-#0 LE.x";yhQ?XQiC"'KuYX:%6KVtŒXRVX+|Cj9Kg cqXr6&)^Li 7Kd6wE*).Zkm݆u FcWVfkނǷ)aoyO# nAoO⁵\[swܱ{Yܵ{n= ;*`)Kx8A{ph׾ُ[voGO][&eso].6c=$݉Fm܊\Hݗ_[:)ېy\SremCD;2}R̃>!!QJþ״ׯћ [#߯ l;BZЭV "œp Ź;jkD6QEjzkM]_]9o&P V߬80X&}fY9`6`wbl/8 (04sa5]αuR'[( vCU,鱨IRqa!"`; iG1R!5b3ί܄^gTA Q R:D0">(-A7[!hl)GܤhL|h ='x eBUvWvΊ^l2Gmެy=:vȟ0L|eMwb uziyie.mĉrL9,bߠVGax,03 W֗1K Lf%;LhS*RtK=璽%{%{c(ZШ PFRAflN[1KPm'&>RH/ h85=5g/huJeL4X=bv9OpW_)u7mSI`;4]ڷh&^,tcX>RP$n4DZscfMTHz\>fz6&>wija=̃9c^! u.pGM6q)5%%=wP$ mt (4m5D1kR!i;#n:0ơv2kAlL4ET/}7#Nǜ̏Y4DHZF+֨I|tbx|ê?@ ZL]^fw/8\9gY-b$< SY*IMi+̜>xp KetYJiQJr#W~{I"J۬A䳊;$C[%{svkFkC/0ehw1,RT?iqOn>Y-d--ʼMԶR܏Aȇ~ ]w7otXvJٮv+}f}|t 7c>ӍEuv<|kf|ko延\Bɕ*ʯ7oR.[mj|eeLhL1/Oh<~k@ (5n2D J11'Rvs$Wxs#.%ħ0 3_jT]Ꮧ{0(:#>+l,%wVgN|9/Y, v*xS4Qas>\Q:nb7؋sEdP;qbΌcQ̾[KU:(Y8o';#yl_"˚%, >rC-MWsJ [ԖDK,k$+b%UUUEgyFdl4⧡)%=`יsV(uBIkd.sʜa4?7|~{ ݀/H3MXLuh3FJJHsӿW  f 1sv|6g__id_9Xj { ?x VGi=>˾7{s; pF-yכ f6h*Y™J6R Ua㰤+xq)Me=`/hyƪ\P ˼ [gaq>?BƤ Ơj4M]M!K:%xE8iD>O01[ss X)%,Q5+^}5W#qy^N(c>y0{\)ciɢ@$ABW{.wC0Bg| Xn>3[ "moCqDxĸf]֦~4;CйYٹG۹=@w^tms;"v^;@+nesu6hCNNE*~BhT%( A*$`S;s r#_ ߛ쪴 9Ў2882E n^5V(j`eV27ĂˆzR#%j> K`)|I+8JL qG1 5*BKEke"IymR#yB8e4>Uin%ZEdfQS/** `B\cdMҩ] 3ZOAR(eeG긤pu֊$Y1Ⴃo@7/V פBKʖN @†݈[F, :`Agu![B40(Q2Y7M Fd)w?v$qL+N# ԉNجSn#St5=mF4&Ԇ,.z\ eALwBt*H$v7 ,>c\+mؕWLqUBd ƌsU*CCix^@?cXM7WW sY(yQU@678z노 ^u`mΤCCH4JQ$C&ԕN*W,[RTb)Œ&R).U#AԡI:%V%i8-'b!S"(JĘDKnX,fDTzH@L!/MƥhtֱKHB40zKIp9D* D*%N 6P3Sn`~3{g@{\o]USٿa3z{+zKz{KZdK%u^J 7\<&24L4K:# B敇TUNC}JW˜z)#NFC b2>擾Ǣy_R$*s~Dfu:rX=V"TJ}`Pq$^s$߀ eKBPQ ᑆE_DeL:_TZh 3zszYL,g-.JXܣ^; xz1OQAF;n:[Iћ8v1A%fc) qnZﳜ8s;wگ6YנjPv*VXH^eds\ LLt'dEVd "m&cZ+s sѡ7)hn6*BUJƔ1ehLp5Kc!,d +v+vԾFFmkԦJ+TmXkq#FZZQiɘHw6a9>W“j4 $fp0L?' X#iD'*u-`?|_Z߶omM;ڒ2A[vr˭E]_n̰w妧D[>]lŮ[@vمl 4w-AAϽ{p]}X{wfM6H[:-b,fGwx;/mUލw+Wq'wr}'wr\W;1. `{dܟbbo|m1̿P}1G>Qhф$r#&b%+/R"k.ЦD/[D6Ņ ib]۬}V*$_P A A +iWV$hKi3dZCf0YO:|x%qhQrʇWhM2Pe:l̍>+C`Me *% J测%#nw96g߸hG:{abKFt7t O?1͹Q=rQt}}C74]6+ :jroPՊ2(5YY7~qIEk+k+:jDgYm+;]h4N]j4Xf׹`_?gW`( zQ M"jPխgeؽ3i^D:ыĢ]Ʀu@mk=ǣWSP)}_a%[:szޜn{VT'-<Կ f{gEۚwpq璣G-=^]XRRQRw>r6fKPRc NY)@=:Vt:+G}'/Ik ( jd]rd B+R A5QCӒnOheu!mm&GO5 k{Id 2eI*̒A{S_3v9Gp*; ]*jKsKkKuLTF[2#V6J_8bd]T4Vw_ p,^̾6'&u"׳;pXo@POݿϟ|}?_8=4 x`|7߸U}`>A>|'rvゼo[ͷ_¥oO;}Q};ZqwOߺߺ~%N 믢*~ťNqe(uߚsG"#T]Bex .'?y>RA؛EV,J12@-ZM6^) y"95uUqcRC{Bߘ}BuC}4#}ip=}uۤ_䅉Y F t핌{WXvBS$uʢгő(S#p#QLPp\|oɟbh9SЙuq؀W]4|+:Uc13{ԕNF(=ns. 8sI28uAhzK>>W.1+.QRZ"PtT4jnd/$ٍNN@mD_G>(tWٗ7O1&H cnc~ʏ)LWcf$'%R1(R1+) +eC=g+":at5n!5 JS"JIo貢%rD+Zq"-&uUD&c1鑿ޠ3CXG4E0n 4K;/,F|c^c:9sKHZRmf2ehto^?ϞQhF1f?r ?9m%#Vt _PVHѾ.+]T8ra5l}4q~vȗ?dY#0Ca8:v`dӴ4&PV5A\Khf->dg>1$j~H/'Zf5nJ{oMow^ĨsD UQ?ܞǖ&wnjnQgw~ן#Wxgom4 Ʉht:Ene*|o9 /q3-!owl=l }Eۘ&n=&w Qa6߽=ηCm~{wr}'wr.*~pLVܜ|E']yF L#.:жD#*.iI†fZ@_`st@!|o!76:G5I-J+J2b OKЬTY BD1]TbnEX^}Dv܈t]i,4mBUl#ft9kFnNP&%iA.yu2;L%:QrDⶱ2|>e!Y m0Zݰo\=neeU e=f{ VU5j RD V5:%:6N@jN> %GsL/ʐ;ݜhOC6ɽ-ѽeX^bY93% XAfm$}eÅ[) 3i;8c4/ VO\/l֚7I" {dy*(0Q(>UIbIT49:~L؜Z>!S,6NYVTl(XH%VWb*0N)?vבA|%f6ŁA(HARdP:C8{ͦk^N<{G\{\L1bQI8V#=42L322u||Yޡ!ł]ȝcE4(R$!C#&]gxuXuk $?t=B?9odBo ozH$ݿ?psvw?g][%v,G޲@>C%`bK۞+-}9Ll-6Bۉ=YXqok6d]%RNq^~N)Gno-p7-M<ѻ8wr}'wr\X񫀘<+p(W:W{ :#Mݱ`G1r?QlGP(h^n]>`Q*0S`ւ`r)(*JsQS4')F%$%Y 2اt%ז? (BA\ ^ӍqND5)8,  OB?aS"5c_Y)6 18"c8H{3"ey1 Ħ77 NMkhkZIͅzNҪ7}%\ Fc.z,F MKKFFS|e*+|k-%]cT0h[K@SQ6+e np]aT5ei.T2SA7cv8~zo8, [KԔS,IK:FѩwC:Oc85i[缽K։WmK0v CЉ i1  ܠW=._f`%G{>3T="%p:,xEŗG:B.2ꁂ(trJ63?a Zg췯x:3VVmk/vr.KTY "Z|[j]#Ppx'*%G <';X3pq9怋.a( nM3KL5mD;)({ Q@AqVwYסO2b>8gTt꒾X':kã-Bf4Cb, \(BAx#("(TGPo'!_\MvOZM Ǣ#XAFWhjH:k:ƜΜvFuhȞBYɢc:0iҶM~S k.43*Ê~Փ4"3 FmZm;d-fcNΟ%ZT"M![CSD8,6}nJx' LL2J2T"l<&ǫ"ńQ94yFOϻ_my[3~ܵ5L޺~pppdVE)]yWޕw]_(~ 76ޭqCk]}h>k's||zOl6?psa8^G+~O΋MJ9öSTTYuii<nn@~"`-RIu'ʙ/tlU\"6$3li\I`wt<ȑ[_FC@Q=+CK^",`GKHOu)P7 >oKSw7o8~֋mju{>G $/ߦLݱ%{ o'y}ۙ< H dkVU0 w]h%Ll#2a̩(TBFC6d4Rѻ`t+n1N0'X!50e}Ůrnq^v+9h}Pe7&8o(ʂឤ9 >W_{ĢMamZi4BXd AYp]dnIb-f^bdFVg>BSjꂎU&~n\WtK/+bux#0`Abc! IDAT 6gҖM5ȻE@**íc*Tk`!]d& ꒁ@|>B&` # i#G!G7_St1pHqni.+Wk*UuAg@"?) (}r M"V Ј1/&5'*C.}žG\&CNƇؽ$9-9aZ9b2CQ\,=C\5 ʪF]֨Ūc  Sj_pg=>z3y$Z$pdˀ04H7#yf,/g.L.i\ِ (ZV)eJĎE\QZdžCcKHlBS!h$4BBCIsT$ ,V~z M"M2KcW TOEěTl%" Ff%J"!RzAV)AisEhFnFܴ7Ұ64@JDD&v! ʆ\ Ba#Ӏ":+7n%heJ:,* {LSt#wojϹn\rFy+M*ԻԤγwXYH9oqMq' sB>C?O~r//Dj_G5ݍOlk[~6PKoɻuW)\oެ-wfA)G|Oz pPɗwzowY͚[o`|[{p]ޮ;~&K"9FOw{^mDF:봆欜`s6gG[<x\yZ1x]5  28-Ϊ[,:MoF;7.[.˺|Tk:QDPj jZ-i:ڄN2F)$PdeL/V:m;Lh?]EH./G˨'g%5sՍP9P3AzTE&2߫.Ag{#9dqS'@KDKB*:P7 PgB~^g2Ce**vT=}N9 jZ-Eis:3(JQydTt+:#lC9SɆPHmhi%bbP%Ljqǎq*1)F"Ʒ|V j˳ii"*\6 P̈R2 o?gn;,6CEi)_T䜖9+"fT3ɪ kPJAc; 1yS'ojdM  K(34!1HK0=QX)'! j.:aLdWn3T{3{'X 05|d>o,,Je7G+W啶9k)tPPjM :Z%U.)gLih0ya0*sQ4xU<+mFa#LHݤ3o}Ǯo72>vܮ?_O0 ?CO/??vɟ 7sӟ?70 9<<|l'l |m^=ķ$_{~ߚl݈g]Pfݾ_gwoU꿷+ޮ o'9G93ϑ{)ۜ:8:{ KE©lJɣCϩP?*Jΰ8cZz%cÕawF)4a"T*S?it =mLO.gԓ%k) ͡^RhP%_t:_8̿h$y]G]/rx:c:j# A(BTڬ:$0. FV05 qY%D!-;YESZ֧Vߒ #ɗ&&jT2I3~W}._ X]6N-tco1;aeTY5mVF[DׅQ|*URѷR$5 ّ3PuXe96m1>}eDNXQV5ug?A cz,MlWg'=^1=ot@'u)+`.p3Ŵr.İOjX | H&3 x<|oX2 Ca.N Ri\c8W \ @n d,1:M)1SfXJmDy>P  ,Y/2˜iQuWs(eȠ"CԢ$*VX.e(( KT7NZRXD4 @2v].Qj%, _EhaZ6'x5F(q&<7Dkx[ 0nէRΨ,e=2i`Yn}MOIIqI(7AfF9WIhEY\ʛשH I.L| -X3m\ݰt7"ޱ ހQD n4Cy ˛q+|S&͋ t/q ݝ :D+X}={n*7fH{']wo㷻s?5Xz4cx>k9cz.еC}'y=dDoW^:qlrxqI=.]]M4A,:g XVoVG (#:.I.7 Š#ÄmNmMXcsʚ4n2͚L&Nyl VIӓi3y2{>s# y{-B. L&fR)X+UMFc%8IJrEUn(pY PO<\e6"Xj07SXZ#6N]<$W9?UFcJnD0}&eY'!'ƐAR S(”:Mi;KFFqf-3iܢNi9R9`]bչ:0C0@EB0oaEĽa X$6˴(MIJ*)"KA~e5 V#3#49EYidY/,Ia%w٫] ]I E`9 JЍ HReрeԷ |5wы # NxTpos].ZKc9qa(bxt m{B5*R"XU6 V%Tq p1 5,Ѣ5,)a ,$ \#xuOf>Dۗh{FUJRŘ1n$b$1Lbaϧ#FNST=bI,K"LTӥ;۸`MUF;*u~ TX.DeR<2HpɮzĮ8.dz] Q NSZ!,N=^dⵘM#Ni3Z w9rjq\"5j:dyH{F}wE}oIB/3ҹh}3|vBYP~YǗ.(""T~WQ^|b~8 x u׻׀;p'䮣!{f7^7UvwߙUK]΄2oQ3x~o{~}7~D`8^Qef>00yK1J#۫f\]Y{&S/lo!~l?ԏ-İX5.ESWW9EPR+T0aS.˴Tr|mp %Pm@ݔ+ %ۈDy:w}oA.㤇ƈUbNY0!!(vOK~9=?O89>y9yAe8"F% %,9 |1]^|ȴl#)Q*%e7r`/UF '-j΂KE/yK1nLJmox7T2E2g{ 7|2]yRnT$$<狋8foP5xEYQ]6PVbJW(*E^?櫓'zy ;ϰf 6iiin/x`4PBQ)TRKϧk]SCjFD tP* *kv+<(XjvFmLcCBC2/LTKy$H<Ȥ*V K>0_嗸b/OAlEu`G`DH#f҈Z;6eK%$Us#^z >j~:Kʸ UϾCp7ąM63YmpyF: f>4@&YH4JSA}$AJ&$T!g|l|?k]s^^<';<~ţag'<ႜs )9G2Dh6H%91߁v$m_ n{D/?]Tt\u|Bo@.>:A{|[_G_uͮ [{?buܦ'ǬJ"W3墆 KHD$D5$'XusAZP7wciZ{@@@#FK%22}hiEyZz妸__ Dz)RAYB$]KJ46҅DG(r}(0HWQR`4l YA8Y3P1:Qm(\T5HK407;)DK4jsV'[#}[M6`W|%>씧l3)3TrU'6s$LbՐ@ T;Ǯ89\E]T5guXyURKf&if&ib+z eb&e,AˠϙYCXҨ/(4s kqF5^klj"BWSHUQƂ$5nqE%[sc)KW-$Pzzb!:B_gJ_CQ zj65*Q* "3]!8D3FduʠwΠuNӚR \WMZ " T5]yɃ쐏/ђ8nE P"C"6e2D2 e SJ1.ɞβZu1JV^ $*qXeu܅Bӳ/ٱNxhvZEKb?7RMQVVPP'LҠP ["Q%,Q%ڸ!¬%XS;\(#YjꃀZȔe`4X.u!rLB'SU,5BEFj(jixl$) t DU EUHdm@".вHdH-G*96 s+, q# * 0JcU= 0G!+ IDAT$FY?C1+DGQB!/6iϩtH $5I"T1W䥆TlQHQM^ɱj)ND&QlE&iyQX$0aFS43G24+C5_E*JR "7YP[s%]k[8,5}bp6 $7}0.1m ,Enw۵ݴ wwԹ}ns-y_^#7]|Ȗ׬z{7}i}?I\%cSiZ GBJB> rS\\o,]][(u^Y'G_24pCmgo1B^yaMڠ6X B:1E : HGAGE j2f=PMjcjyNPTHXDAYM@marQ4͐,F'#Uj c!TVCĶML/ZN۬'8>y@0 .5K`T_ݿ;\W.YKj؍a5B);ەcv#2$u4֭WCWPz,+!F/ BdK?2r\T>s9jJ#N۷4phVN-m)plUIp.˰h*j䅎E.+gxEK'Wt\)iJ>9xI9ۜ-ԑM$B/AV O#L=WlsRKbL }Lg0ݝftNfT%5mEM[RV(K+&S䅊V$yBұX$-i-8hq2~H. Pt6(&soC(Xu[ͷ8i UjuFaJ.rtg3}}*4.H4."`SRa D* Q.m-oAVUi H5 i 2GgYh5NqhQ SS/ .zb`9yd^PgŦ 'K͹;w!@漋[boje]W);gt;F yIqހB!Qq;\_1A+P5Dw@7wt=tMp|;}J7#oRn66-7?~+q*rޮOخ[͕/gs.'_q.óll5Oyq_])^PpP":1"Eyc}^e XB[1y2<٣3jfQe˥aJMJZƞΈH H-. VofuAg';{͑TJO!=&G N+؏COf44L66(OLj΂+ qWSx o`,.#ckoUUs@ 2"§iKZGԴ) +G|UyXG(U{Nǹk_[!#UY?5bAfqjnI6k K$ ذ fx}Am{\9;f|y=!{a@sU[- ]ѭ\R~y*6ukɶ}ƎuĶyLGNS L0L1/SKY((E7R i [&QicAce qq4t4 X\ysO?ᳳOK:K}CJ$JS3S>3>7\zSOpu>.6h!g5+P+,bѐ_49{{x̞wLÚ)Qj*sSns(%ܚݝ^R{K9+xij"6#d7Y̚,FM:ڄO(j uH]qT \/mNW(kH/- أ*+ƹM:b MQqYQeIUZgaAH|CØ1\̶ Zx`o8JB뤱tX#5 BD~RrEmrQeZǯW}S4QPbиMG$o67BT9 di7j6YrVV+bP(ïn+*,U@i ? ~d F-F/Mbա(k+ʅ5ZoiE$mJ[e:ou./{vCC:d׸bSCw%AcR0-ؓue$sqݩoƸRw 7 v7)w[J.9Zky=+;yq،q] V.tCmzm|7;z5ֻF1PvBܟޭ{{Ӵo:~˲3r;Ź΃PG<ѓ/?sq*[ĚCe']L}:,rjA@Bb_A { mFcBQ)"I`7F>yH9,^8&&[v?:*EKNj Q`Eݚi}<$hPzTn[>Q0 JmW]W9EEDxVuL}s8ecL%ܡM<a(@ KԨD7HRVT+KdZ4tK9jsoqz>-&mH4PThu\8ʈks\6oD AAPٔ?ID LOO1k6c6Jt-x&6}hJj榨jh b1SjNU)tFr`-c,V_1xR{%ͫŭ4Gnxp4HP@(%P"d(J K!kjd]T՘)MCC&8:iD %BS@ h·_=F>O~տ3函+duJe#*DKJK,mRTUe',yyaZcEc{Ns^-&]Ɵ]շŌV}9bW7UBQAJ =؍nqIkZ⯪d8ZcE*qnspGhiF-NĴ-bQ7+ ~11O"su.t UͱOȥʺX5ed9k5X5qs}-ugrDyZ='Tk`Ms$l&4@ Dg,>VhBjľ&CJPQHœTH>k9*oo??'^Zmjtˤ._NQ|qxePy=ZSmhS3P? \00+a& *'[EAWL&h2 j_R-{;-(#kb,:G>XW%YUAUX%{ZP*mAU8:8j8jp0.~QԈ C%;)m9Ә uQ .%xٺ 𭃯ԹT8PU&1dB+&*tBS"(5X "i1Zɰ Pa@A#jkT:+o*kX50| AUn)U|bhq걮Xlo ukj- %E;GE 0ˉ1"sH8wP;'Kz=&ۜ;NKMPQk9ZV&j%{Z]+ WTr %)Nɦ)!#MC0j1/0W-([y yEFYJJ)(@ /{Yi>~"uVbW7̟݋duLh:h&/ӽe*x'^S )8^gį2;^_gc)o'l:lu/t0X+-mEl 3Q<4We\ X.,WΈu8v.r3|F!}^dϽc]OPa3 ƃe BQ56%vhZ ]I(g nkx~mWdXn,}VUtQ}ZHBJ 9% W4ʦqjc(E0CvLT90+Dy#-dG>ظ]gm_aKDöscCZe/.e&fP-K|ȮN٘8t:zHusp7p7o}FLx~@$XG\ۇԇ\Rgũ(} b ,Q xb}tN=>8m7nY>dsϢ7>>$DqF{T3j YA~esʍϥ:9Rqoؚ 1y~`glD]iߴbpd &zڑe8ljy1Wc+0=3s8 0XL&5Mal:\O~ 3ax\W$deAܰ_|L_pz)q+q_ -q س:WN ZEІ^&, &=Okb+EX޽KGG>;95i̇is̹QPgϭ>`v6;SA ?U~?U4^c.$}@,q`] Ta-߬yўag4'CJ`2Nِ98bpo13N-8bk;E P+T,T, |׼gzŜو[2dqKJmsw/;ۋ/=MV1Y-e3)?pUW))`g&)gX8 aQ=T,ja#Wm?)k2g }]ܕ ΄;kB-@kL]CS?i%4 oѮd-}_Hw8/z޽݄mcZv?ŵ4 Bxu-8[=_Ͽ_+K~+]9+pV<ŲZ){{O(M&fLLyH-:y%.u4_׽/YeZY·({-vwc ZS*`rjOr.n ' *`Y-ՠzIWSljZ\Rb..eL.h7 ֓OOݜ4HIf'P,,^F*:2[^B jP)ba26w;lY##RFT\S6ʥZz+bCMFR ɚMx6@4]Em+*BS[{kg\-w5jB`hIEW8~Moe0RR*Я7l)˜|qCw](B2|*Z鰶ԎMHJHK]y̻my0_Q {WXqM{|rǘ ѵ@hpMŞ7UGCϔL+4ǂO|7 c ^mYg6YuhWFp6}ƽڧ O幄^Fn%Am`D`y5ٛAI\'7 gS>$s)\TX B.c& +]}}e7dzĻT~>G@ r/1V-dˌ% IDAT/ًkHF]kvqKޖN!6qE6IE[3KZK1CT2RQ.$P̡A] }:5pC<D ݐuD|y=r(T!k-Y50<#/pY r EGCHIrroG| وvM3nX70`AOp *z~KKݔVԲ}w0=wiO:>˻N`Yuh=6alEhff=a5XA͑s)ސz|3DvZ~}lG$eLZ!FF7۲<<}N7Y6 *j–g_|uWx(.ч<[QQ+q"6NU=a^7!mf(#O 掃I{ǰYpc ?eӵh:!3|n1.8wL:?ÏrdO#5nUY[B%R KN:X{,>K#dLjY X#SAuP9X3=(p'a{#n^L!II7MsH0ys?fQO%QaD)0^t^L_r6)̧]bOh,sܦPT4mMCW1 V>gp_ys=G%OS..cn%E5>˦O4E)ʢm䐮"u\UlP]W|ǫ_ٗx#ICقh@uK]aŠHy8>qL.Xϻ!' dƸs'^{NW _"U6`Gy+uo D߀kvv*FCc"!o׼?ẗ́| 9$瘲 YCj>ɯOyrpӣS^%EsW9XaO-[e3~w+~_fQ]Jǡ9;ᷱ[Eu[MBIt w)!q0h66Vedzkԁ-FTw>Z[k г` 7MUv^a7E !/%Nd3Ih7efZ D_i\~|_a5XXC}nCL+ >AbfA8/ 9շpѣ֧j꼥5MVZW\}W11rAAI,ڟ_WUϦ}!l=yipA{́}Ag; JMrէvizb0`&կXS&RgSűU4muM}$L|Љ7pv'C/3#C _piw? >iG-[>k5Kbw =jۅ@难0xឱ1 1wbLc,%? O| Xk{>3 g n{LNe.[CCrXP쾤GyG~NP ;A34}mPNVh_@Q tYt̺{o{h ʅĜx MS6!yAjAr+ ۋbg `+ aukM4pmZu YK.ah0-kGSjp ]("d.$nD:\TrM8ԎC݀n$Db7Xؐ<,( -qӮJ4Mk)uSWXu[הv3xUe11sNJYN{̶z.ɼe)V)}?8tfSeFkj";g bgK$h-ؔ1FdYQ/1vc: '7>>E%mU|EPH7ۧՒLȰ_/nHDF$yL5wv3- }A^f+bƮŪm \'rd[\QmC*$(ڧ<Uݑv&-i#Hz:hM]4%kj)XV Pc6]HL*ؤC~Rݝa1K'%6.kaD+Q]U[akЂ ȚZ`N-t ʠ-q"IP:uKhN&Wo ՒRuъ IB[5jq iZT!4f=*iPiZJǣ.J݄Hms#k43M٬zlV=Uc EKI{hO%gnm6xQ0^*h%u$ U]kԎ֎I]Lm]jNK+nסZ'dvMp:Y{4:E!u "U[Έ3HD[ː0o\'ސs wKg[w2bR;B$6 rsОU*ݡM--m))DO/r=#<?[)KjmdOZ} ;q:5CVzd]ۘ|o=˜"r "<#=@-]ڥ]*&D4CF)۹`;o(3X. :Z6~3\{(C<~rk <_0~doI?آ7v>,yN>p).EE9P7E!B6VviEcYX#hҠ; *4&!V6:8k:N#6tԖ%nV>Ř1NL'>N{W#K* '`j&oC$b|P/-Q0J<ϘS4cꏱ*xRs=}<|ii=Sa? :a$M#eQznskQVCGG}͐y=j\#eȟx/M 3˧mBO< O{i' }. m0k AXTʦ*e^AwB-!7#G'ch4]]q͓G ǣ ʡKu@ivnؤCHqaҳWDfK]ydiStOV˓/LW!U8(oÔX$x Kꐛ!lD?Isaɗ"Q.[e\NNNYLń< ȟԶEuZnl)eDzQ\+AX̆w&>]:ngIlm8n/9/9^"qw,ؤ=̍]٘ Ik[}]kijUb.Kok'OL- |Bh #xО=trq>)i|ux<(z$NĺQ@}TxxCۻG9AE55Dn&|=S{q՜u|:{^n Uc3Ā)66(XSN;N;{֌5'WMfS}MfD@/A/͵wF,g{d>#._~IiZIKڡU@1)\ڍ$|j& "Vfȴ: ܪ$)_y 30s;#=ΈiϽϕ8fZ7 ʩh#,zYnjEob '7P*h ^4guڣ>u)Jڣ2os9>f9ם:ownկ+ӘGs^ooo y u&l;Ρ,-.?Fk;i9m>_lX ?ꄨI|JsȆ!YBjSaSQ-<!& 80JkfEVŬ>A$=i1rBD #}r'} ]gM2c8Mثfϐ0FkNQ.E|{~u{6Q,1 >wɑ{OMPnKc,:`UVLȢ}fLby(SzSKҦv¯Nmx8{`ľ;>fgђccL*kVPk̲UYeh Ja5_]=3ǮEc(!簘_#4˳>7w]/(t6;fA*?L _n-|5ʟ*Qj)\p|q-k~GLjg\Jqj%VTf-WLԔRjHMY}si0WsiҠS%:WG#<Ϛ)&fNȭlݧ$jdpNj'~1䷒r갽kvK|p%A>4q$$u@9`TM DjCb1&aWw/۳p$rC5P-Uql墽k>8kj$4@(^)q|4UH0A5̰+4@f! B4W ld6&Br17;ΊN#WˢDz*O\q0kld"EQL);!xR.*0$+֣NTa[ANoDR9uiU Hm 26$"EZdAsK:dhjQF[LBu2 < + }54։IQk6&ksi͖K;&uЙVt Q6ÝgӬMSGT* jJ !SlJ n )dEI+jܦm `u}HwU%ԸbǕ%NS ֢,€wmqH&%hE_.DCE#,He6gSw]&iBvZT(qEġ@7͖#sR!Ep)}"f^YУ90 %BBj*!}ɹy4nGW߳X ,.Kӣ k}-%y.[2,i[-,WBT)Za6c~5QxVޯpjlU%?1+]+`p-޶lo>t `4 -fŧ@W8/ A++4OYg!S+јWUL.XVOU85޵o;C2'/pK%aC|yI] [+Q2p#n7Or@n\vOl3/+?AíoկiW͆NaS.RC>l_q<&[1ML[,>zN\OHeD-lwqh7+֋w[c. ]M=I:V~b-tЍ@e7Xve(BV9*U4 =wfꜰ=Z.j!R[FQ4c){YP;QП1w )tؖwG; hG>'J:vgG\UKBTlsFvYjRY}ʍt63Jr({%{?m6f杻;{4Ʉrp=)4]^2OM )rlSbyڒO#0Ih 9p0c9p=b6ghGy(՛{Y sg4]-u4BXy+M4V({S~1}j/ubC@Ɩ QԹC;)[wyw䬞>ꄪ"UAӖ~b<\ltʳ`4;'lݝvyExy g_6xM¥9+x'_KHlڶ jglziT/j䐭"u^)eMCe[;߻֪ygw7&-;X8!` y¶,d"ɘи/ct&֬;sϰkWծyÛuttJRTUEw׳_{fu`][7uzI=dsͽs6.%11w6``ב3t Zop|6qrCsogFM3/1/0(Pа3Ó+(YU|mm:&FRX rIZ8"ĩDFt!6r&։ n.qwVX#'b,4 IDAT<Glo3ڌ6:,> ŹCiB{!v7U$E!o ?HI4 Q3lN0cFi|9G.i[yLj {̊Lƃ.6aPNզԪS4;t_ew]}爻#g\fFdNhidIg$΢SaZǀX<_[V}zb!l-l/)*err;̽CFVSm=L6̯W$K.:'`\&jROgt6TQz }LWHLdB'|4f7;UdJ*kzQ1)jjQÜ=}Ts*9ʪ@](˒5a󐽍Q**a5e1wO8:Km!\ArBןY_RT\VKJp)uΖhIdN\.rk=8a;mF7.oQF1R5$e|{xUԢjexM:9 (Ω##3S*PTŒ(4dIs--;G*mpxt#aW"v!7uf5׶USuR b!wCZi1Z3&&sY7E@L[Ӗԫ&ܠT)(H+ TǴƴ1֘Ecbjp)UcAUYPE'J }^=}bbQ~.(^I{sH{!ˮ˲j9,UsIiR(BD!TfZ3HE.؞шu*<%a"lbo ?* r!-Z9h&tCpEi6l't،Ψ7Ԛ3j)H6gSVW~H==6YMYgfh`࣊U)DBE,?3gΊ0Uo\zYE'wȏuqtg3qh!4I'WYS&* N ѻ~ም옝;ۂUg hFhFDT^Nh)xފZeFC*6^PIѺ/ (DwrdK9:̰MK(`#_m /ZW\-gw<+&c푖m}.G8YDuk~ixy`6Y8uVOK"bG=ixzc}^gq*+6Ydpl9bU%  2)[dʨ$n$iXMͻ+$՜R^OpR2ZO<(f1ɶӤBpك{5^npEEJrKhr**tȄATcN`z *ɿmOQ7wp ߶m¯!@o ~|Z?A>;(fQy!̻N%F=\c!G>u/Vae/Q(*UeF]QcƦ &''PX Z}f4Zspq$@ t#v.ϸxeT\ M1 ]?@$.aZ)MyʼXf.IMTX ҿk(\R"4 UȀC)`H/ GuSwwScƺI)uǏ)-ovb,ast3G2tL6IGެ0t))6§Th4k|mAk"4tӯ2Jүu-.X>7eSR]ϳ)zŘV0?B'Zi״ʧ4)x.IN"4M'8:*~9*Whe,$^+LjR\kTe)c9&m8{p.g._~ m9b]d2O꜄{uE\/r~KWؕ'$AJVPRG ټ<'\bۢhjS=1c)w1|I:n3YL r%@(%3FiL6j86 r7YEA(rBp>O^w lj5`۵9;3\2g/bY:~G9ӐzN1/!C^v?onoR>ڠ|I>1npv(QJ@.Y dM ux< τDojcu1.ԘRgMg($VG.W}ąIoiGnᆿ}6}/"w8gggڽ=NOOr|ok̽LY) y鲜(M{S*.6ie2|g-4%㨅FHS2w ;(QbUzVR4YSt.*Gp,iR$=m3>BǸ‘!FId4'ҠZjD ќi3WlM$@-T*^}I^kabjQBsA2Lh(Sb-3 yTl=8I7 bKJiF]%3U Fy? iV @&U Mtyq}=*9SUJ9W)fCosUm3[uv 4hBp2þNGgqqXI:5Ig&DMRͩfAY,*㰋PJL'~V t f LUjYKq!xZ%ZZ%9NgR*=Uw TT{c9c,* {ifeXVĹ]vxr:DrMo]---NPrAX%KJp2 6CCT%n` zfPbm<hSJ04@)%+(D %.k3*zdk(mspHC-Yȯ%DO~ -2\jk;\h}A%ĮjD" Z5UkA[PSMf=pbQ,Tuw:wg1 u7M2tTBHkZFudD޺QQ$ɭ>E@Y 5 5&IuS!Ҡa_el20mE# 3FIwh;h%jGPUȷT~"JTXd]Q)B;X!fq0m *9b)f+AI%o0ws7p 7yۄ'?IO||3|~/ KKˤrT_>uBßPQ E!FCJ86W>sEY|W-Q,VE>ZvQ8E(}◠Ku{ɹ,3^Kq٨s-}툶6(RHsHCHg qq* v$ȫW+E#:DO*xwTnYVWjR2 =cu988NE5mI]_bvyX%B&m6m&$}a 0R"VT{ ^rVlqospmH6m95y3 ʦBiZ<޺EZ׸0dIzndG&Neª[au@:F Ud%J^d%`q峼Y^yp-8^/\ML9sDnkE"=4l_߿wpAr4npvxD70Z f;Rsh34I.m(CI" '% ;-||@1`97797607 XJ%B,"ٜr:hJе/:W4[`Tsj)L=e5 WQ"T)B2ZWlO8A5M& 40S4%C+S$# .ZK\is19=j+ڌD]~C,]Τ %%jX,EԲpLrMN6]{ڢ0)UfYL &:э +4$sFkZ꘶9*37YDX2Xg- Aς6#8C$O9Y^'&dmT$i3WUӈCK:Lt &zFIw=O^S® ,fQtC'I]н /5 -nl6ENLŶCl'^a׎Tx%/X9?Fs#n}yۄ~g~0a~~M}6lmm!|_6(0:/?K6 Cy!c22>ѹE|>A~/uzΩQYso62^J4ij0`h(w\X\j: 4nltιHg4z)D: K{ pNVBF)5$4X@ l5fC$tRr\Ӯ9LгH:EO%O4ƍi]5CCUbG] B Kz-A$@"!9O4*.!.-8E2t?AHЃM6cK6ɗl^zk4 ʌۯsos}fusx|c$ Wsh<1Vj^9bkwlxW |G y) VNjoT_{0Ԅ-qʖ8a[F״'T t'p2t'f7`)Qjsyǣ=B 9)[SF;S9!^64mk IDATʳ|ɴ:nL3Wj})/y4JS4H]P AYC }N>I4G}~@%e[lmfmetkƸh˿->_7Qic"ʒ $n3R<5xCEY8"%PM&ZlkKM@@+y0F]Wxn7Wx6_G߸˗n&yYUCZ@;0tHDZX&3ȮhDĪPi3'b6\a](=yqd+`:JgRτD`|S%pܧ8X lAٗsB~>/kH)!~7no6?گ;G>>& ?#?}-ǟo}f&-V |.>"8kk乁ΨoMi)qb+re^!LL'$Wz>VJ$Rcl`_ȥ[aB T,ҎI[u?ZyK% B/Ʌ "!&ӥTLj^1UjEֽ:V.HeaVLc/ӰRMGX%&1CgG^=g|)Liop1KҨ* &G=. :Mڝnq΢1z\%}g66l`v|Wl) zK@0Th AJ~AhvЭR}f%x钽+X<^žGP8dAZ?jDg#vzζ}V䜲* m}->@Rf2Udv6$e:Wfscy||| ,6Ьi3JdtK\KFCZ'[;'(EIӘ44 0+N*g*3FFÚҳ!Mkưl3*; =ֆٯ_?$q  TGh;dAPfQ9>%}.O%c 6& )*sT)@UXX6Pq 4Q(w$r;Ya:JM3żH0)2\$2 dzIdzRTv%z\(B&g%+gXKcӾ`þ`S9g#`60jWg?y/̛+_*eYoed?{75.^~4G?_7p m__>*ЇЇ>{\<}ڽ nml#}MVjioZ%KRT|fiawAC vAÓKo}._"I-"NMȠ={|y>j9NbU=8=Fqo fXl+_5贮WlGq1+UIRYZH*ԇ49 'xA>І}2ۯqRHf/WMgoWPsb$1,b$r"Z'(Nfd/EAfj P#gP΁#/W)L!=:c}2j[ IlKF(eXl{k@(!qr:)S/5*MZ/g+z\W48.T\̥]NhcN4Ou/,*woS_ ٿd:hOuw|;?~77CοY&)['RJ玟Uj=~S?ïg ~|wc=|#z-rfOJBWILKp jI):y/5RKLcBff蚉ZHAfPg%L?]AXJkl .8"ՂM×l+\eM):#Aolf\6厏 dסdEE~IËIP+-.(+R#HML(uA) J9ȑYU֢JL*̮*c!Z!$N;,5iTc5ȄN&t Q0 .vЩ9icz5tpbC5D rD idG:yGzRkj mQk sчٍb\'@)n#ޘ2XoQ,V+UUSi+x% {̖>)^CSBeI -(JAbV\%b#C&Xy|c1)Lȷ(/1hyhyN=RͦiVrArL5]byk((Jb(vNBu45':,2ǬXFiT[ V25d1R-.mǮvԴ9R#HS0 17:mF7: JD%' zc lN7f;]pPk|gĚ!Ü.#,4PSƸbMU,h(%GHtYVc!W45,F4kvܳ#tM,:芽1'\E]VR qu\!,HAs,k53=*J=I9Z&尶<fH(a%xYbMMxʦ`㴫g)zc(!@[Ke!+JDMKN>ȇ:٥Ynjؕ%2* )!dJ8qA  atʜ}q<2Kjq2.p_Ω9 9X#˓}&IkY:{-{?zZ|r-|=~[W|M"lJafM`DΈ:,M; Oڄqٔz$+o]b$3( 2+1,)7C%N6)DhhQTKWnj7t!#Do%eIrY_JFq FsIJcBkM]0N>^~D{G5QaթL!NTdE!2f)RÄb 꺄]㠈;9vuZcoV%V}"J* ^p xk5BFH%mEY` Be]n O!TbbL*yjQ!"gIBY YIk II\È"c%t Ku:vh'RP} ;L cF\P%%*А(Eb&κ5I bV1fKf˿f˸)eXiXA1luml//V*5J]ԒMgP("g¡TbdLwkduUcnVY |Vԙf^dD eq|,\'r ;ߘ\Acc"*&*l: [%{9 Eq#K 'E&2VcJ -UQ#ք]}yJL&lI֠%'ՎX>b1\E pHfXX)[uf^SX~EFDT9ʬ~l>|y&\dv~uzrDk0L&sBKǴ CK0EBCLѰkm&VKoIѦ-F*3u 7ڛWRM]zCTb]gnĂbԊI{䞎&%_\?jwxˤwU{1{JO@¨w2X)2RJG?a?a&_yy_;\\^=?|ӟ~qW_?믿ΫUk[nu0ݏ IDAT-]5V:w+'&v ]&Gu:{")UA,TPl{K"!6(eUb'+iF 54)m,T)VB!۔gl7<(R gdyL#pńoEU e%N# d%ZSH($9{z 6Wo \ JDG!]HV R j–=GD3Pe6Y#7kf bPWhVQMHP JJ`i1 }lfqm=hB$b B r!ZsG p٦Va JSUXA[r8&AÆVR, sQ mF"_=)bKeR6<~Z. \h 욭"!-uB͞S! W)<(}Jq?H 0q9Ovyܣlav,dy"0095ŊzoN:Ct%e[05jL:SԨMi IJB%,Ҡ ,1JsI`svחtVC)~0-kԑ@-rr(q/HU uB 7 ѵK!?p6C|n3K:\S ԣvB(Hs0wXUyŲJP2|kIw;1qwj Q-xDh:JȞ-U묤EX$xAĚZfv0,J|'fs%I鱤ci ND9wA&4a7 )+z+#שsJ#)]¬ͤ: d92,`#gRjHUݸJó?} iS4Mk@. qSz;g~3?|Ǿ'_>)%?rZptt\e]~O}SYNC}0,jU.{7*S"HSRhBۘ\L'M/.dZcV+ފ$>l¦"f׸jv8mrszIqOhJ*5Zg j c2t6Hp2GW3*%>(s,Ca ,4X.+骅nfh~^h:ZYADz+͇qcvZQfڂ@ =&IhEؠ AsXv+LM4#g7-IR\}sF4 я1Ι &&$l/ϸ#O)-g7$LIId48-yV,=2"j 挪VBr]e7N-VJaeU9 ` 8b;H EVN(,.>M9F-JԲD)J=eZ,w*(Ju'ڏw#투46NVԵM{J6՛2v:Df~8+.Rc˱qM6YЪOKiE\aVYdε]ε=δ]Z#Ԡ l3)RTY` Q%܃rSv=yƵcɺX>+*}u5@Q B.j߀Տ)36ym<<ʨ$ . c=iw5δc [DX"RbVJHwH-Q&"TX1굱O[14Htt5k/ XUc0Z'&նnQL0+L4pɥ-":Ɛ7Sۺ]{)V+*jccgIO9f֑BFR6Ce~3ķ|7y_?Y;#?|q~We: :m.}\>i`kJRYqM{LQ 8mZ` m\R+GniE!aa7a^{> DjIP`. ]E9R[WT0ی-mV4C:k49xV(?d\XRa%],`1uoJ͟Q38""34&J^R<ֺZeT%:Sx]`qnZ+)9BX\[D*!7;m.*‘X)Pr<J-w3qCM`tx>cv`hh6ÉvH2c9w/رi6\}>W>W> x^ftn:Y#KSi"ǔmn IbQ.;nAXMf{D֒Dj2ǒ59Tjdq|H1WQsi8Q'^? ٽ{̃\z\{\zL6&)a+1PwL0جx^yWcZ`8jR!Q 4?`UYK6&X'IA=bߠ^+g>z͗}r-|~mo2@f Y2"" qϑw~oxG)PgQs6hvG|o]Yī\P7t>e-|Vx\gǂ* QaA5^eɫ>XK1e`]~+op31 ꄮMq(7IT/M_ # i؈ʹFvr&6 .M`*+Јg|o9I:W2VXQcyZEQ›g |kOĦXwkE5k4'C-rFp,"fVqw"==n"6,x5"1knr:I%t154ńR4T+*+, Fk 6aXWY=k]c=V{ Y~]1;%Hy֠4LjUn.^"妊CHsY6L z`2"D8LhU!m5gtt=0~ܥ|Iŵ7ZO]>/aBMHX"R3bMH`u B@%+tQY>חۼ{Nyi!DNPzD=\)7?)騔MPk%vHu{JwpzƂ*Pk\Jߴ|oZa`=[ SUA Zs/x{;=Ygf^~^, @^`%˦ H-Hci3oָv{eAgۄJ|mIW (1A%іChvG{.4Go<{{duaɰpDDElC^tt6}m;RV9ajG9@vLmbi04)M4ń&Jhlc^ ޛ|{fu;<L?>Xim$UHH|Y]JRk\"ϯ[nVeN,rt &2$ ob0JԒY3FfL;0yỏ4tmJDDm.PQQ I)Ap hp GYL6g!ZQSקXnDnX48}FGdyD\RZ341*oܫV6mx#l^gh07HRD 1vڜ4jsԡNP1Ӽ43+L:ucF65~8#,ֶڔnfbg5neu`w,7 Su֊"( մ*Y4k V TJ'tTA0ȶ`9 +&yV9 p]*E/0ԔHЧ(U(\mJ J;L&'cF%Lz CI(JԴ(bCZk"CJD Lg4uyam*3t;C LeJJh䡆\i +9~F cp3(#um1}#תBK~y*ޠ5:)sŷ1CC,L"C TUmAE[,5AQe7-P",MlO<)uބ{'{ݗ2j\[ҷpk}+,;"9I:3*81P)Je"&[W6iv JI">mC6(/3rBa48ŰbAԬ9U}DM@Cbl0i4 &AH@b"Zd"\\;^`#79i$M -$yYq+ukTk Z+\kgHs8S$5(3z򘇃DC2qNۘtjb $c7d;+./Y.+\.mjuTiPO=A؛<>)rlfipy`GEBKBaja'{f:׃>ˣ:I{ 7rQ+9x -A.T$QTH3F$IUMQ6OAS`7Kvz%}u@_T՞tLI\1S1wqDB$NR1Hcɨak3 Ԋw8e??epJ&uN;w]qdfmN|+6cMt .ҡ\(z!1 U) Zħ'5ē̱v3%1f3pc~&4{Sic"f7"1D3UL;j@1JhJ|eŮ$J E2#yR^sPThIfwTTk8.+QAEYR}B=`6" ]))0YS$$!-9b<VHb,4n-r-_޿o%jj[ݻboNs RdfX-RB'-o0dݔj\W6H JJ0K( D 9+MAjhDMǚ Kz\<6L9"XVYFu(m&3jsFXD"CFW $I᫰ P1X|u ,#&\P3 \N ?|XV ngvn)~3q1|4P-4D%LR:Ԙm6^31Fsz=}HȅEF8-:NNi4[ PK u/B6Y"spz{KZfR5Elr]9S3]Y&RdD'j|im֌xtƫ ؙZl}PF;l7E$[W7byƯ/I+%c[^DoWĒH4IJ)KjmrO~{ 45FB-H:.p@+">:TČkp@j~P,E݇FCiPԙF9EXЪfk]KFV"bwpWbȰRgGzڂ cW+t9D\XHÅ8>ŭOi<_DV ՈFĞG٬.P%\^H\"Ux aM4 m3?ߚs=$R2iR m ՜*KqGy™:FSl}*F➧G^\_7_W>Q4tw:N"hBkt/u"z{K=zg$8fk8z"p)w x{F-4Rj sJt" 0)͜u΋54h|~?4M~<IBO4OV : NO~Mi;QwD:uWR E:'wY3LRDS2j'ZF]4$Y񨄎U8WH2a4w(*:YgͶjanA$ e`P:ulWc IDATSo%\G`LqڵmęK*$rtG5jH;> yS@r`O-ImHMF2g nW{myjIHUXߊ+z(j} ݢMZ->*W_#7W%h,ZǬK2# >tȤg2+HuD7! ⋐;c̝=_w|h]aQzoz'qlxMLd* <)t26(^[(!e@ !fQT.o! }~3U$]j s΁wAUair( HWu!"d*X3[6eS5"/ 3lIQ$ݳ >+=CSnh:vAʚ\X]ie&byt=a$V`c"1(c21cIQ !- kT;sU"R aAQIQyDBba>>ChADXԅFTx!my m۵nԅڑԮTOb7Z{Ϟ! #Gh R4Ԛ$. @XkzFiԕζ鰩z(B 7p7ޭKKXZE\cj=cE.-rӦÊ#풮Xa7:Ơܘ+f&Q[;!3C:ڊ\Vt5ZԠ"AȆHsPeʞ19`Σ9G{&jmhR-c͟2d]wCTYW:an]ʍE1bP%QlӜ q E',BWwSF 6g-i69<4?"f6fwKgoCg!-Y,MIyxS]Pъ?z.7ΝPjO^3Yܒ>˲ϕ<&salj^گG &=MM[m>YNt$ʄDXi]Ąd<>:U#f} z&Ê2: 3ΟaW%2ۛ i8Qw$!9GZQ43Iv^uX/z^yT5qe>Ub)9.8~tCF M4Ԗ$Mu_C4H݉_jqQsY.;u]YĸIk:TSy4 `'%өAhaf%6h*XuYq;=`47c3 &wc>L1秤T<ձ e r' [|sDrOve(8 ($r\"C5\!&u#fXY.l@} KJvKd3i{\Kn0D)s4 S9fc}#?hPCvT20nX"1).Y@x,"E_`;z{B ]H R|h.aZVGɈwc؝颢ǂ-_?vgH<?+޿Ͽ1d? n7cVU\ beB=Xጾ{OxK 9G=ͦOykq6s"za(ENil؄y@q)gIrS\l~ݯ1Nj̀h6*xqn7wlesSs7>˪zh)gאm(56Wy8qoy}┛tk_q|ts"#+jC930Isw 8; .c^ ~xq nc\74 t걙N{>j=vĆRv LJ7#>;ѢT(Ki=Β>ҐD7 j?:llъCNX~Ft93os~Y#f &MFkaBphJw)*ʍAX(3&(6ڢfk3 fv-G W:ƠYlT~ZՖ{wi1A50ˠy?/%^6W!DtRcuF]y Ͱ˖Iv7d{|8d[_쵈#M_7_b<+hk{v)zQ^X$~X fjj04'mnc1q[(tБ`tk,+GV mGzhq:ej}fNO[ѮCNk&K˂SmM=_8Z koy"R }g î:\ߵ=n~g"sy V9&|GLp&\6 x IhhkĞbPb>  m.G'ſo_2M뇶xh̟c ?]Wخf -Uj- dr<#1uAՒpܺ"!S*pɰ Ni!f=(hy7BtL2!r&ZDQ8 H`Z9ΆdY8aJt-G%csF' T̺p]"I+j6w8 ` ]dF[l{k6i6\4w:ȻAj(b1>E:5f`n a9y[&[wU BF *L?=]t8Bq# CYZU^p,h>q. Rd-鶖dmmu,'-j55fQ`FV%FZT!u%ћ6kG^ yiq!2]Gv^M4DVY1hC]C\/H4ef~t%棜Arϰ'&rn3cHx 5^23D'-"-؉=Si8]deCAUig:B4Jr ڣ-Px*d(% mWcζO0(0))?L@Q dP/)zM^ӫW1.򸪏8N[_8pw+Vw+ߊpK+ЋIVdzeٹj]oG0I*kȴk<4QG{e#-H `RU;qP I"CRc"f~"ɫ/Ϋ/~(<I*~l]y|JPT&ht-: >MuAi5cZMFIݜaw!Ҡ;dx\b%=aw}m@` Hh=l6odxعeRݡ7 m'ȓ|; }\"NR-1CoB ؊'tvIm(OSgg46>JO^R!/mM6sȯl'|X>vG j]P;-ikKj *Լ`q%wɄlȁ}́u͡sŁ}k:ǩczCmh*쑽w(shn|C׬Jҡ.kr'{e#qu)!--ظBXaݹCRr8fZMdS7o.6uĉqNe 1W!W!˦锆v@ n0TVeA3w' kcM2qƠO7^3ΐeL;+kjCmhk|67Dh81b% \OaƪrN "횦Ҩ?FM7XqWt2B*;.GCdתX3X)9~ԉ+M!`fH. Il#]{zĞLOE<xb}dz.3cwf@w QVԈT!BV7IeDYqΤsý [$i LBvgk-9L{C.`C )mgC[t j1ލލX]C# bW[0{98 n'FS4tɷ6V5 `k%Xe,jǏ/+zzNwM7ߝim6nu&nK=}įcl24P4&mG ǦPh&Ҫq UBX|0&+w4>P>%(fd`ZP 8`^ꦢQHc DbhqP$I x"bMy$>`)5rw;6T<~W'okmfSt\PRڂ@$۩EgSiXJb~곩Fl!eix)hʳ-X׼#bu#mW[!XEN t%6#V 9GDp{ul|tZ+Z>p\rsSsSQWГ F=Ք6:^ !oLe|l'&l B3k<;{5if٘4ui.5KRn`M : Δ5O[摃워\7M` gW:gPBh _ mg|<?Q~n9a_Kv%BJҤPE )ti#u]:4GmzԖ2V)]frڜQ:gc6.oa]UBujKR9:x:K*2R/ñrnV+DnӛYVfoRM,$"Vk\LqTPI+FGF5ZFػ|ZEdyQTY`pwiI\#AᾌEr'✢&iͦAV 1D4U3qmܧ6sLiTB4*TPWBm(Nsx~d~Rby;A}M QP4BLzhh1V1bY#I1R*wSflkc#{؞2rfDL1b4j'E.1j}2>~w˗ޯ8,-6EM&7,{`9hXogĺϙ~=""nk]!;7ͩ;Az Q;9a+[!X dE),' %4=nAQژMIwf [޷X{滙f*43i2:9:9$[XqM~.~*낄(-1}+[Rf:<2>2><&s1qwh2cha'ɩK|R$^e ntcn#whB;qгV<5R낶fx}þ}eDTE#&\s@ )JwYĻ (IH*OR;cuF5\Hlt:Śznc(CccY [ 7 ߢ2AZY&t93Zm/XK,}b!ǦzU[xug׼M0( IeH4}6h 9-;$ѓsN$ID+ )c5yͩ2xC6 IDAT>gX>Ϣ7 rBveG\عaZ^9S}LM2!>Ҫqz rJ-W.Kr D+ v:=E4o5Hda~9۴jA1%hG{ɔnA.;'4e: /[Œ~gN(<ʰLi$Tm̦@4v5#ޫ'4#fQ@s%dXүY^ܦ܇#K#@DBVѼM1A=MUQ~_ { ba~sGZ1VSJR7uޡ&x+KܿhP(XЄ )v%VV҈ 0<}v7xu\{Ĺ9 m{B:Eu,nm΁~K5Z3x%efz,*բNj'Tf *O6 {nr]pUp]mVʱ]Rt53ygz`.,iHZZ$, ħvٝ t%OxqО30XzƲs1ǯ>yŽ=@tDf؝rh3L dAؐ,2wkrn@i"07rq}d߽!hs;){cu tx)gEYs̸3)M+8܏̇.Ejzܦ{ ObiJ71H Hq%j~IO.pULħ9Vմ QDw`Rmps'ڜI8eo9"T<.k.*H>΂w_ϹJ;1fϿa/@\21of%z՞zʤ%M*Q$o1\8u׼8ߞ 7iY@bR#*q+<>Wk"%%:w%yko~d81#{⛀0~{W'k[加(m A=<',*ܿn@8*֨." K+iDE%?:{?g ߊ > D\OجGimC[7-9iʷT]25Vzul CF(bw[,_ydsɗGBostaP">Zh4,DP2 ?0~sL1/*/m{wvϘ* R,7n$"%Ay%TZZ’OgI-_#D!2nC",f on^WSX+ĆR%~[]R2سq~WnG."N3~^=^5-ceϕek8 o8ʮ9pMr1eКݜZ8xOHN1u)<9`t889?s+=a9.. JpUL f^ݼG+@m OXC֭H&m$Oe߸t⏭_[q[ͭ?nǍcq 1ɘLLH.c~3dO9ߞG-B^=9O<->? oGOx7zk&<#xx-cCސ o/x{9g NY  erܭ')lp0a\w{}`\8Ln'K$G|`i ?BQp1V?]E CgG%pK~Kz(^AgЙc/٬X,zO㏟-kÒKP/lו4={Fg.r{ȝ;+ʪ,}#tm%hjð7В-TsN{\ęy}]Y]vԪJc= .k1#}]!mvd]R yP+z9k>N̿UG-VjmOK&COK>ϙ~a2h6ͽȷF5Lk>֟]7>g nd ԣ Izo?ᷫ;1^;"O}>g] 4ޝ&Ri-^BTyO <'f\1.wt؝֞G4xu_~Vh`ʸ}aXjXZ*vz  pd}CYXΑ ޕNB#/ D(D* ʊ wX]/5b /feƭp+T,.o/ y^4 9Aq{r_R4:W քĶG#,KIt8a/ռ SD˭ǭǍ;&">H -wq-G̜^2LdMSEK/x)2ȰDM`73//82. E'QBhK@U ֕U5TTʶF3l<^c*6g"£U²KD ' 2cZ]T-e%EB[p`_256W¯D'!639lЛ(wلgAtG4(w@PkG|9{Akz9C{VJ90l`6q`:J *4jT$6]yu>~׼wWt+*4Jt2LZfț3srLZehg(XfpkӠ bľKבBtƻ.F#o[9=|23h j͹/F#zXfݨXܢ a/Ղ27)2W{|taNmE29Al# L[CCb__2i];,6 Chhv_^.>B:&T܉'!dcsE-h;nt-qh0Qu')-R M*?J>G3~lg__ҵV;:դ\}O2yk>||(mi+;/kgOQFș$}6ǹ8BjJà oV59 y~a3ˆ,>zQmaP,JPnpB1X*]Mq2fL 4m J ^|'v4}U6(jj5].w̧П2攚 ՝qu8 ~P~Ua6ی'yf?#~( Sd;G%2pAZ 3x;#7L:c/ $z྿%TR|5֗ WghXZƒKeE&:TWg_"g7Gp b!|tŒ}嚁rOqH)K O쪬 F4IVSufhUWև|9{里Ʒorp^5ىe{p]=$a瓹 +'Ǣ2݌#˿gϑ[7.bh9ėf{nmcl/8~LK Ԉ kM9Iea 'cOcxچ6&Rn Wq&xZ ,*)P)p6KsV>"K-/yn|c-jL-ǴwpCU ɄmQ@iFi@_rL{3%^~UZqZ=e/g\}HtҼ4kA_p){}ysp3$'2,f*w0 b)f+Ҳawx}gO{ȷw~oxW?`8? ? XeTG:H-(h[.K\:Ć eۤh[D`*l' ݫ ZF48fGl:H 9{~w`°$GxGyF]۰ r(M+ntXNTB(j nӉqFNUsȄ#3&Ů3ʐ]3I(U,NKxVҒ!F H\%34B8RNy gY!{g}}Y]1f#׻4v*Jyi!ڸ,_Ft%Pau"?g<00B<3ĖUy*!^y5F1 [g嵹'eIAi,dJxyqt(rX_ҭXMT gb}ea a) UQaorkڥTL#k-9/HG+\6jt%]kIG]-Yd7NPcI:ĮM:4E45jՠ<7 3ms pԄG)=}[w*Jt LQ"D/rL2LD{}k]Йn4v[K-֔Ν7XœWRj=k{EjXޒvkd`ASAS ZP*mP&aR&Kf&LӪGTNU锥_mqv-q[)tqJ]10֘{nt\VcЂT5 W-6 :`vJxNRA4SzSв6^(FM#Тqh }Yh}Z@:UA]ciΒؽab]3nkKl5\P*enP殛&2l2C!.[ܗcǨjf)Z1 ]Ұʥu]|IkoyX;zF(4B*hjj캡2(u %oads.+|ִI5[z#;[%K[4D3stc7Rb:WF]ny$$GѐSX}Ee#)\3N3_#,<7j(#B/굎;W82oi[+s['(5}~[;FVWmoqy#) M~Ǚ+DwHuDwPEE9N,AjM?$:$:Qx{ ɜZVF+nJDK!L'}ԧ T^U̡rɑqA[wJ^蔥A95Z4B3bnR,Gp(:M#Ք) 5jl=~5!]m zKzIzbaqe9 4DjJB8yYrB:XuEG+.+Wb/&E$e7sE -͆H o mDB9׎WsN3N3NgEHsL?H;5@߉qA+pj.Q۬C{CKԑ 4:)숎Btg8BSq-sÈc` cCS$lX6[dR(V&bAZM8ZXNEFG]QꚀ-b_ \mQeۦ h@y /y[8N/P !k&Nlۀh4e4 ޷*? _>%"nPwɔ1`rE\FT': W*mcIy~qˤc35 3wײMaJN^ zy*/,_,{(R:'˺Α`-5k/0]-wjnSnTtYy{x_[cMb:钄-ꭆg9'63z}DS!Q)T@Ă*SVQT9(Y 2TFVHPJ )ѨHph>)ѳ7HBAq**h5VC ,fF1T,vo=,5C1%QaQ F(j]Э[I אmL6Y@\*H't]1cd4DU4@ nCB9gSyd jSc6 j]b =uؽ{l: XlkQ * -1%F7$El2yz[%_x"^V ( em!:Va/S:kL`q;v͆E`Y9_t%\u$-RA+&C+(lE Sw2Ʉ*jFQv.P+q޶·\_S:`5fA.Qѫh1!(lFɆA8gcؚ+Oߢʆ4uG=^iXY qÄa28; jTM",-7<_sѩt߭ H5\\5} ˤ"VHJSG[h*TPE.=GHV.EEƲ3\'&Q&EBW&hFͯx<|mߌ!?#T3H6ܥQ+: FŔ;F{0 \a1BYInjqbPSKPzj$|qF?ChZ\cޖY=ʭc! *4"\4 wKF9 h(%n Z;} ܆%a}WaS*F>E. K Քd}/t ;|T ڠX~٭;rirsep+.}6yxͮPV }vwd߾䠼`q%Uɕ~4cx[<*? -lѠwKn-ݜuzʦÊ铙g$\ݝN9WrڙfzIuɶ z[5 Fd͢p/2b*G̫n<^hZBP;UDU$ Vb9꘥ҥ,ٟ_1ٿ! L 8#s4G MhFE5y# FV)wTqמPLn o\BMh\CF!xT_Dkjż3Bo7M˂>ސt{Kl#"gpzP.e5Bhya[ Yf֯FUD ԒsSܵ$m[ )+٦ߍV~fc_9s9jsj%=Rϥ~O0 :55_n9=sAZ2SMVdMԚNMLO6G8T)0i J&[w\wiwT}cWP>C73 -c) >[ߧΖshxG2gG3CbOd ƭ ]$cO8ָ 0,c4j-Gł[<^5SX:aL%yba}_l鱠P;,!`b{xOK]rbt;$f'Tw* E¥QuMCRpF)G|t)|JXx?ӳ=3|/iudf&h. oZ^-gw PRRL!yM?&D99IPsTHqph) Zs-ߜ:Q&+z_8I\%Qe ~7G\L+&k;f{gyxYbaȜ1\Cd#ib1/8 R+ ."btY2?[4k/oEAŎ3 H$BrˈlA%񒈪R땡 MH]Az g!^ }lOlu47Ε'Z$K)|kSDWQk2 )''^Nd- SzaZ]š z͜,wژiǴ-:,eJXl4fn `_DPk4 ՅAh=p|k/ ʌc ٗ7jıqnskG*ɩ>bfJ 7Fs-&7}ujHx<9/?&~ˢ˧o KDE:*ڰc4fl|—-⟸$?q Ʈo<[O:{ŋ3^hsbZĢETԥ4BDŶЩzS/t' Bv )s$HS =AJKݙi)Tk PYizM+H7zbԨ.r@**bKM-4[!]ֲZ4[N`Vs])v>(чfU-&:(3^DYr4sҒ!OW| VNX)&  \GJnc$ٜ'7 nj{"PlP2H=1W>6ߨ Za2%z̩J<lf?ۏiz{vu==MbdDY%nlNZQU*O#.JKZy|lTMnfuYx^MfaUD57N!5y/l5ʂzXEۊQܚ4Ig\;TCloL1EO,YrJUi̊7zYύchF tdC}k~o_0- Kt"MBcx$CRnU&iV*%ҡ GM8LfM hV}cRX&Ԉ#."%Zؘ2㢿Mүii+R&6`KTHSz+j!-mbM5MV(IL0X_;<~_4?w#^>}'ܿɯگ1_uo}_#[na lEntpt gssɘ7yyg.{Ȟ 3Azh췹4cuskK*р2cPl%*Ǣ~!i_`4pF-؜l,g&3V?wxhoIL},rs|G Coj#S3ԢQn#lUyMb4Y-+%>u(44%&464K,/FE Q׈8p9]yo-34D^BJ6jfnE{Y%t4/^roft9]n.]`'vV4 MgMJTuEK%}mʎΛ6x%Zd]Ht `k kM_4"'#5֋ y P> 5$1kۧ˜\`TU* mA+bXBJL'lg옆 =OΒg}Wwc =RaQ9*o EeN#NCN;Mm>׼&,m>7~ݳ95c}DShmJrp*8YwR݀ kX03ޚˍefu6\Ի y0mȫ-Qw l"tY\DKu!y36sFPHjRS!TZ Ii6qb%)Oح. R7*)S{zk\"g;G]^Ǽԏt-Qn*+FB#_#J (Xȹ 5|mt7FC&}yw7Uo](TuEalġE)LqO<׏q q͐YFш>ӚuB ^6?<}o]bu>bS6L()2-M8ZMRϼ)J k8r8?!zjst䯙魶sPI)RR2V l^Z\hSgi)*is||oG_:!wDv ;wKK*|o6~[n凁/;U/4Nvq?]_r̻3bJg!gGGKwO lcsAdy߿7a[$j`ؿ`%p@vƘ,#J$n*PjN @?e_/sÂ6KaA3vK/1+3Lf\̲!_w?BvR H!\t$8pY='C>7=yoy*[a#Dφ1WۊGk#;52Dlt x?EU˳–Hl7nS>n'UF/gN1wor(d.FExr[h2@Ks 1h BR@DGBD[R!P%l TB%5M&N]أ^\XѣB?(M~yb?y?kV>Y9SyKdR-mŏbRXIJ3w;_F:(:T'Izft[7]ۈ$XP&ΦjP;cLTMϧx|SʄS ufv1*Qypܡp4;/tpY)-&rij<0-Ƈ~g?F #A,%j] K tSJT_yb>c!րԲ Zƴ+|Vt@iH途F嬨5V-|-Qmp1͠?DHSrXD y6CJQJ2p_6T(qbPBNSz:n[tY(]g]Qca1qю("_K /H))W_ bVU7|ѣG?ÐF@}[na |+ILMA@#nPQ; >s?C:i j[ mA$EZ$MN5~T @4kP%87ZӾbKt 4 tJ4 Ft!G%o=Vژr=FLZCCŰ1l`tv%2$z'&rT.Lz@2SnD8$O/ 6Np UǬS q_b^]~&5ZS#Δ=}Δ=2 U pTD{9ڏ*FLXc\q+gr5cx 6G)"Vc锚NYdMfvU{Dس96&<X|ā8a ^ǼT91yiEcQX=rl ?j~ mNIpT.| A𵐃wy 2L4\XsSJP eC4J\b,S\,EN3k0'%-}rg7tg.(? AclEp8GȦ<(&Owا9Mua^$"BP'} 7:*L?Lp|@2(t7w0O8V^`O3O2ws9QL} 6QVmˉٷO8OP,#fBs8푴Lb3ѫ䂃/89yU3}"b7Y%Z6*޾)c)#}c#R$h"V݅>vQW7NN-i6rʃ)g<̞~O ^M\j;>hQEEBѺWB,5E3 )@SA3A@@Z([FD]KƛX/SJ,nh%$N'!R4!c"3T<4>Iw"f-[g|и2d¢1 ?ˇr~ĻSgc~9t_[y{Iib۔B|nAbmO4MO8u?xㄻ<7x8==W~Wٟ_EoۿH)7//k!>?2ϲ[n凍/T, b 2baCޟ5{3;)XcmS~H!p\ ,?dظ3{fTLHU^@i; -TufB]VjM=fSuI+4s2<5R27r*שbԖTJK#Me|S$ąǫ숗^edGisOֶ({tAuKֆs ,jL(zM}NNۉqǍ0rYU]¤-2bL`E.:h[p=nN,JGGmhFHW`9^ .i>qʘ2L4,(iyK\ő1CT,6Zg [93*S Hd, dutyHlPΨ]0+&9k~Sg%^5+mXL 2i.K`6PlZB+s҅EnInEԴJN0$Tl$蛒bJ{>]0BE.Al~l 0|gc5-  L5Uⷷa$Ւ/-n(]XUmd5c&ƈl!ڑm2UFLE 6u^kBai[,dX:h3aDBt/6ԥ ]#J\X tQ`4z²}RQ( "HP0YjmRH:D+b#ui6yy_ e'Cε]Vz0P.ˌiłQ1a8'+lҢm?cѬV4%rI%2t&Du.3~HC] 'ęuh^Ah4dc^,cD2~0|+-gl_18_H")-u>Cj{w#aDNDw٠" +BeZ@ۿS+FB!f(<9zƮvueF7s nj7 iF 4:OoS?ɟo3?3/}ᐟ{sŋDt-r_\gi7l榆"٘F+5{wOh !+ARINTDxGQ‘ z7>2LtC29Vq7yhyElY$js)s"2(""6dMYud56JPvՠ(FgxjH[,(uy:%EE|%m& ȡ8ԉ]u{SgtzL!!b@!uBeqq0>eotpÐ ǥϥHd\2fN\0I\#D8Y_hCmBXZS5~2ZL}MMüae:FƠ1ΐ3q\;1~pN3v줗t Օ˪bi>u|O !H}mpV@BjOP; QF\o\^]]syA/X`jTog.Hjt}. ԪJȍ`o}^pNx7~YGj; =uκ{IC1A4Z&W4fK4">mDVeW/;JwIAm*5yҼm[+*?QըP10 03Z;KL-HTt:sr]cYry> =Adu1"P˂RH']ra |q쑖CFV5ȚZ 治5 RFw"jlgnbOƇn\x> 5Q0D#f7cVYZ'g>2.f7e3mhysQ#՘ih PcɒA= U-;lpRMJEG4%?=^qxuq.G4ƼdsYaXݔ㽧$4ņXP6hqAՏQšIIdL1|L%~#n0zk {f\'~alte\Q%e)2Jc\_ӭVTRCF K!4鶗qroX t'D#E"[??NWUկ~Dž|Og-n7jf1YE %KzbFlĖÉy@Fk宅h޽h6hNڜUŬDNuj.+`u3.8JB?"!]:+<)b}kcjZ#'B^U[w x[yR SXȈ(k0݌V>#;3:7MR\+HZ-:bEWs vO{|CqO]V->wPʊ{*Xr}-^>>+/_wKj$MffŴ=1e]68V攨yN$),^RV*+EܶiM5xVor:˫w80O[&8FLĆr6c jR)kZhVnUH]Ųs:VJi䕎]*iϏ:sa v}n—Dt툈ϊ+U.ω5DpY5~oƸ{Ŧ듙UHLf+ WJz 5}ZV.oN6p;蛂Q045δ=8CZ悖,V\P}fxtgFP7Xy Qs^SW|Yrȋ.p@ؤMR[B!RtI01EUPHhz$8)#fisrNnM rb@&ǚyzѦ<ټh/Lu_rnOG[Dyț{K?aMYzM&Sqde4=&4gQW#̊VsT@*"exҜVNߚs~Aj[EV%jR%NRk'<=1*;ᅪH%ˋx C#g͔%;fvY8#5)dsjr- kf{?A6H!oڤKtT$,v٬T#|P06S<6l3M2,j,RR*YeCQۍuRmWjI!uT5"5wg8zra)dl+m85", r"sm;xƮyPNkFrY˩K98Ɏ !Qlehe =F*ʹ mz&fZ%NPlAXSJP$7'UJ6)v#%RVIT 2JnuQJUw-r-hŝQ&Y$HSP.Z*!xmJZ(䵊*%-RڄYA&'%Ţuj]b)䐚:7fWApݤ1o  y#GѨ1)A٪4#MSȶQ&])A yBilNz+Zs%cZS(TǓ,Q#oȎ ϣptT9RBTJ\kC^w)q@jQU%̰H:s{HmeEGYVW4k_\:FUKV ȱ.Q%J\~ބ>cͶ*HUetYa&u.:jE)5Ja&)f;aԞ]|gP*Fg+FL\>!zꐋ${Y<;6S*$!A7ENDwU]Y6fZxT)r`*vnf s_ق`xa>NԲb+7s.DFA[K4id|t4 O5JOc$\NURd|54.;.;&5FYbs-R&(AAfC‹!ŀ,1UwsFV!V5B{1Vkg }uOO:7 '8'H^({<' "$܉K%i\=!"*5Jk옢fYN$LsxhQ0/IT1c_. B#@tږ*AdAh9Y&?iw9kmj]|LPz-;wuVÒeok6>b ֔KǾ?}- ++K;,^?r]%1AB8 cU(~<*MlO*f39OX+||HPҎ0(ZdX3lR*c퐕c[ }T{'"L ͤUUT熜us)Vow+h[AU褝CcM^'؛}¾GiaE:IK / ?]~ﳿJQHLrM5hdC֟ƴ BT)kPs _QhH_tzgDl.7t6'>F}`!1 cW NFA*u &)2((kDc &&%XfoyOOza~?+>~zͧOYOƔ_jnjS ܠQHZiiTXqYcnk2s]+A[FkK. ^oyXyEEEHOЌU*ѢMHTm=d[c&?q&ϙ)O9;NN???pT zQP-tʅN0N{{.cK&RAS|?yK5XV\,HZ+pA3:Gp)N@ǘIZXFzE-LWO--`ͩD[4)N;е%ol^wip7ʆ,cvR n+wƝ{͝sŝ{Mh;D[3k V"5a %-H1cK ^rl=l92\P.+ԋE)Qz%B@+!S~-%jQs,{THJ!B$<3CdԙB#jOQ{o/x/IZ8lUh%+I4~m6Vj80:?q8e,xUtC z*z7"[jߚ]heɉ_sk;J85d}4IeXqHdX(K0=?]xE/<kc}]Bfx|R6 Ƣ4&l CBPQFy2<@կedYm2^x4M,8?$d5L"rp#rmЏ61ijAHtݱpj|b.=aNѴL1(NHr(ۭTJI!\ qc(&l!':ȇ)hQ -/IyjG0[Ki!}mOh; d:uR4IӴ D (F$wȓiԢ~^1nx3|ϟ)Yhq >!m-6<2؝Θw36݈pqSFRp0KM%WHgYk;FRѨNQYwS >uy߾9}@OK{W Л4JS4KR \9@?NzېÎ8N9Vr@Ew |~A F+Mě̒9b!ǐƔK<հ+Ȱ9>?ʀ: ]I?Ip(d'%r@';$zkGHtnvGaj$%mUdXiNsS 45Ck& YoY ^@8w!2U*GϫGDבJkCĚ͎)IpI4]oLn#RtY Q`af6)R`? I鄠T*I'|2ݡY}!$E$2ޞ½eo wcny6HERZd+KF F N::TƱcviR 2AsCPA҇'w-r"u "Od12FV_&~?NN,f? /'YguOAL:a+*tRըc|Z,o'vMVg;Q; vxJDq6Q(Q)1 =.P5}xW qeH[$Mc _#-UҞ>}! f YIQ%{QI`.\8g,N(Wg*?.9*a?Q8amYK4Qgn*영'm<CʑJSHuTԍoo(uC\XyaOhvtvΈOj!Q2rlwN9T`3YYQdEַȾ0Q3oyM- %P5ʆ~G!.pI8$uDYa)1l\KI6(|coy8](ٙέ}AfZDGԹD{7LKƇ{6m&lRa!harO3Q'2ɔpϳ\TFe(TBl8Oڳ0)mW}"!m"aǐ2qRl/C5Q#5Dش#'oMv g10Nsi@Gx$eQVUTv`O錎HKzkBFacTl⊸tITw3H_ 4BUn=$DlԄTt}>)WTգ2dtBu88K$ E'=v4E[,%JCJKĈLsLX*AK$Y. >1sAi EeюKOe]!ݵg3-9=39[ G,tSEq^I1#wNS&SϷ>盛/H 1CHF_1;pv֏A +ou{27;+FՔ#ѣF % 5+}m8x-}@ tQz?c<07-i0o3) O 1Yi$Huت}Zp .e %("m>lgc>!}yK_6-{ (_U-~"dtz˃1Ic9F]&G1":w|}6dlH:IiQGV?wE?NjY!$ ["tA:w{4bN<8\䴷*EaV ҡ{%a$}mFL:5΃eJ<'btºl^0;[0p['nV\sQjgorSd(TB |$ôOZW!-(ao鏼(P)Ș=<0'~5IGLih}!r<)ĐRш,9њRe*>fra#:6jH1Lo-8[1Pow27e,/,Sb?Ĕ{8[.,XrvX0 XQA9m1Qz/*:[bl.W*IA*DVdi˷|X-oWpp}WS>/=9;O:zITg0ì̀1xĆÃqN#<,;6b,5Ceڴ''+Ma @ p2]rr| hX{ـ~, >'S}D[0 yxl: Adb",b"\Ͻu[~_W&JFl0ԜZUv}L t"R;|`A:蠍% z-)@ƒVQ1I1102dlS,)hj=epJO aZ8cR&딦J M͉N|_ÿ9^̎f>7 ~s' a# {y*юu^0ўtop{!+1fQ!#g~mx*ux˫W2ڑP8:ѽA,lV@1fpFtGQx#o.߳XNiV2}4×W_ŏ})/ Yꪤ5~Ƀ__P4AU$2$2R#xuD$|K'ZA4rW.r6pMױ{p8gQ<~dtd%X/< ޱޝ~υzOqL?%R c}weL%tW+w\߲;᱕5FPEV}dX8w1&xќ ,+a(dB5(ѩ:J4w "=ŐG123D YcnɿfmH6S*Hyn{ Y uԶA)A"L$qLA?1q:rr$pzpj]*5UKӓG*UzEhc $;Ŗ@<6If&g҂ꞋꁋKǿhICQ\DKL?L+E |T3 IDATC*:AmF#l{,)!֢h)ur( rH[*$Z P;tĪC}>Tʾwψ]av$X2m7RFd:G[J&}u]=`_8|Bz'⫖Xk#?e#s*2t6hNZ}O{)ڌ|J. VcN,eH:HCq3#{}";@xBQS_fb,1ʰf {b'T²bDEKamOOơX$*vm4$Y42]( /~{? /{KTj jRo2/]/D\ "g>"8>%ExDCd;~(*K&غ[tEVO=>e$mi.gwfjG"dAx2ZW"zE:^y{59@>]T5kl;0rgg`^+BE 찌y:U4F,X*g|߱=mw1=pq1Ne݆Q`H2ycv.nHx~@\BFN3<~dۍش#و;mxba\198Lf&+'o]DZ?v׌Xs>[ȢCAR:2$MNGN9lHyC;ܕ|_~m&ygMf3n~^H@I*=dI"EF>BkfLPtCkƴX VԶt_Xj6t*ũIdl!TYw^0OYcɢ4*WFy, 픃h(O%rڢl;>J|Үd_2-d컌ݚw19ѵz%VR,; =r2!7 xɻ=N?(NXc1 }y'^7X]z -h "ꌻO3*FbY3 z;6mc,H"tǺ>urSd!,#}GQK\7q"%.S}2[De}sm`(ХU)QK2!V>Qf>a6YninR4:P&*M;g3jS Fg+FJV&l>S8jЎAY C*3ftx`:<Ǡ@J!L( OƱa ++QM)t(d*f=^(K\ftpyu=c{͝zm{]vIp"]և%C'.=vdmKpx6/ /'k{uE6UI[E[ߙ"#Yg|}?-3#`/TkܹS2E^51IQT$OoGd(54uGS7t]H6z=6\w t2aSս㖩ǰ//䀞W4J6hva]lFv㠟tl4K`l6/Czi Pae& yc0.֌ =ƲUU"R=E 47-M"3 !ZDe$jL5vDMvc4)R[w ^[F|; -~;~99[rѪ )UD:m,IAThۿRƈrNS{'e8eNYShgubH))m+ɠ{^3]ms?.WUz|;c~s[&'&'?t3dFyVŖtDʧ]a4 x;ףϵ(_C0#uvyw= mM /m/ /ƒi:  $ qIrUaV<2)W"rJU%L8̐z%832aPDGIXJ8AJ֨7=}00 3ȴ]u!ܠX6 }uǩ1'sLVo/XrO'75:E M*QVVȽ 9QwHIt(iIS VjIl26êRNF&ԅ{! -q]4+NVtTAKIjUSsnkF+;r̵42.Tߪ|[>rrZhv|!6U 1H[iHVRPGJQ;KV4>cX['drԶ$SxFU5!nÙW0d^R*A DA%hJfM2nH B/ d8W*&LuE#dtVbzڞv@&LVɘ0N<%7Ř]>N-9lR~+vHKCBM4B[W F2V2V8eJyDKW4E2^I[qmO9N$H~jRSFRAEj0EF{G{D݌) &Iv(C0 YoT ji2P?O+kVZ EՂQѵ 9l / ?;%8a*hg7C>LR: +F;m$5)tB -"' zlQ }P`)РR!Q0A5A7A34@hP* dW yϱC|Cuɢ2vxFY'b!2I\k'|7$}y}Ҁbd5A+*ԤDKxDv8-)T\I% LTuػ=vrl8񷌆DPį;N'ʜS Y1YP ʖW"g 7=MQ702|fDzrέGbDžr|BsKBiϠ  V&% OhJ#b.;Ĥctao'PykPun%-2雴+.;$gZQ =pg|j[m9F3+jKgi|.g<ṙqq6"Ui3LX :[b'-z!&}b`O\i5FzӢ%a&# ,epCc|ڡZ-z]c&#T CE݄Ohѭ v΀F$|?{ּyky;g2Mm;!{nA&RBH@DB/ ZhN[35 99)bz-!r qűO^SfcG[<-.!ΔV9MXSٔ3,E.s2Gr^vvWXƖ~-=^&x>4y@X|[Gt\f/(!khjN ĤBN_ {Қ/rI.rASU6OSi6M`+PEFkݧS%tMBIdN, "auF!C9vc6^'_ ޼7>s޼z W!:D9Y*F ^[G8l9(+~Edؠ˨RTHIH*Ralt]ά,џlLiSLYuQ|Aqʗo)}=Lystat' Lnic$cDHj'ԃ $+ټevfdgjW<rP\Q#>Y\%TuT*Oڋu䠛)~ ^zK[Ww|۬{^o̻yzƷF_B?O؝;_#Gabb Xq^A؏=.ъs"-ZbK2u>|B%TȮUiEN1)ԃ9LV]ŜsF+aXuv4k ;,n9`N #(se^~w =|ywɲB+-&yQgmֲGdY䞌o.[ڈ5㏙\wHИl<=ntd;| dJ⚈ZU.vrǖAtKk66٢R XrRNbl\if,C}LGfsJe6У49Q saF[@wo_YaX!RfMpؖ.LRы?\K ,K ~OU%RUQP֠ ʶdRW!uQURU %(ycL#iµjN!drE&7r[!wDQR %DI$ujDUDӚQ8e $BJ^N%$I\HaH˽^rf(iS" 3곿Jy\I+jLhUk"GJs,"$J8FI)=t9C 7Fo$A$[Dv[#Hhoң9V;PT ߒ)P5˱ې fT$LL~ 9Qo.nK+ oFw*7wwI?|\5OI=6p 28+@}`1`c"j,w 6:&G^H)fq=ŬTDP,/J.p-5}X ̧mf6iٴMR:U"e=BšP5QUiuD8/zVeeXV**Bg|Gq ʆdsdcP *McAӘ4fs,h3 ̂R $6c[\E\Ň\EG,:fS1#IˑQ1="4:a<-],7)Phon0Qj3h@[8Ŗ S/OUzEaId5ܗ[23(1N;!1UU!Td2pCگAvYC9z ip),c~'7 fW +jt εh-jF v.ͶIEIlh ᰕm(ކ8s o $:!1]zYEn("Ɛ cC|`bC449oL~MvK~Pb6A+L*JY!2-fe(vFVhVʑw=H,&P.x8x턜Oadan3ٴdThoi.&̝67ڐX .CjΊ>&uŘ/cD"i]Q/n1{+qS5@s-CfJ 5A9d ]Hvq ;y$rU I-2Sf8e'MrNRKi7/\G5),\CPH#θp *kxG Q>853ע:daB"id' VkEag9ɶ`dz BΙ-Zf:3 p7xޘlBw6fbvr\9\9xm6kjι<4L)ů-8Ԯxqxn3b [, b`:qb;\vlTHL9ɯ $;ʲ??A׍ϥ{d=#,&.6HF\ňuRƘvۋbhiMmb#n MNq䌓W;yE+ T_HRu"BA^WHk:QdUTbEOԦҒPn=>{>SV|p{D7&7Br+I5NS8se*+QՔllC. {oqG:<>rG9;ڇfBh&ap[r}o3V:hn6;-/x5hWZEadB\HZjO7$Fo6) G8ق7sjpq9,9!e G|rGc$JMr%Dns.s&3JTso<-vbRԸ lLoҎf{`LSVRP6%2.^9(:58bKMp^>^%!XCK^kjO_+BmW+Jk]z `*.4Sгoixb?tp3g,&8X RWgQsQ"Q-7I<HH*)}$J~\ I^o,Nsm#FfU2_fS+LîtT=îU;!76ëŦ#ňf/dm3ڼ0(ڔ^5 b/|6~wHk^__q_==;}P2yc󈗛GK5Д 0݈YEl<=ucӫQv%DzMT!6MN~RVs,iB(j kƵ5q `G- 0 pkς'm\9847t:|Oy{y*S2]B ttB .!/|{$clp NŎM0H&:H``v'I$TI$I2I0PÈjtI% E(5P),*}fEIe^5)$M4 hK$D s@&0Ik@,wu.(ۜ (QU4ӽL([Y& ai9ձJ8pyM\MIo=ĹAiDihJJ'*[lg:"3djJsAhXlu]䰻LҨK[('MNv+V̀6G,"&}z_,O 5в YPg2AMBJ@@'A9 %.61&Qek] wk'ФG#dz9QQzW8Lе]E^Ťy~(`EzD1;IK'v5W'5Lmǁv;- ;T?bT^hd6p &՘X5ڢ0ӴCIoЊ99(J&45F3jz.h)[88?8$\1wYC7bK#ZĒETf4 T}mc.ة&].C>鹺l,(uAYHLID剽ث; Wv# C\$IDuBP*ĕBiko4O5^j} Z`I5l2E3؜+Cn.OxmR\KHv2P j)cx1F'¦I0 , !W U dR]&RY%A'T  Y %(wq?Ȳn&˯ʯptt__|+__:eY}??g~g>;M}3S;Y>ffsD|_d;E2)@33'Ofcpkq.$+9h\1cB15ֈ*<6Ih$T |oEdf}:W6.y:k|fzm oԞ>ԣ`P$YWc^OF5vn岻qѭ6t[|V4hAcB$@:*LW#\N]:6xK}(zfmF7ͱ]&z*PUohԘ Tcx&5 ?&8 NGޢB?v=W> ;k*h2ӛHJI%* $ʯE텟'SVnzZ !y9Bwqw y_ _* ?3s;өZhvCJ * g"1w8f]5-)+р/qӳGR>&'W%&2OѺC0*o`T |1d:݇~᷷ G d $QkP!hf4sZ̨䴺)  fԢf!?=CpS\ޜ9tL>KFc,GO3,CFcD1U( ( 1U2=fً|YTMT9! o'" BlT|ΛG|;|ɻl.}g[a't{7<~AvMw<@`֭36M>>{ONz1OhcZ6coȟ7gȵUTlAks у5 Ϯ t6TmH[zYg^lZ%bIŒ'+^~s'ܬ Y yTQ#ўxi=fd "Ԁy1-#6"eED| e@kWį3~)~)&1&161ʧfEDlA>Z&#Oٌ>S~$z ?kBXH>ՀQ1<&Q`碯S[CrOeײ?XTS1q@Lk"L"LvXLiaCpow^#%۷x>%sK&?%y@U3LRScxwzQ C8r0-L^u zPt&f6`$ W}Fـ{~S~&[7 m>`4 b *[P֤}OHDi j +X@-rd\N)?ybJKu\{!|ߘnjn.tѻo7r#) ܈.Ox~@$ywfJU)ЋCvEVT W,*aZŨ"C;㇝M={o|گIKKpuug{rr=`rx6r% An!4B3ʷCmeNd8bNB2, %٨Q%-^cy!cڌ:˹n6rV(UDDIZD7Ұ4sC!BKIA$$W $dDWt1)%;nl9*BUXZosiX6\ G%,xb)JQR^k}tT]L>iyErOǐc%~hc6_[KuyaC:\Yʸ)&CnC 2]2{,s*CwQĒcNi&t[:՜ c^OɊ86XuǏJ[W9nASӈ4ohňQӨƪp2>0>h h:IiS+ް>g`X[r_^m%# ?$|ʀ?E- ks˰E]S_-?_И5\Vd2ljb/СDBWPW yQ>6HYn a,y$8&e.KɲjgePDJ)HҹζXYL+{IU'Z?ӷ(P7 Pޏ1HqRVF餑3Wo `XUB᰾Z4Y[-m&uEą6/(SU @ lD @J,%ӜlRTK?}ĨR.F<Ԩ eO&%VE S64ʦFИz]655͘Ɠޜ>&675'"Uhg[wc]ylcU=zI>ǒ !CynwqwMqL^??k_[O:z4Oe:c*=軈TS\b@gӬ!D:W!PIW6.W\?K5S6+,5mls}cVEqKZvXr)XJ%6 &udkۧ<%#ܞչ>sPǁ)h֔1g3Y}6#r^ko[<{_ې:[Zx,j1{mS6q:-Osm\}\ߦCDUs*d?L6||ѲNJ:z􁆣8\`xan.jD^qzɃ 5Nݠ1-<+6IE:6m h.Xcz31%*=њ fa+;^Ivt[-]1"4fU[GnjrɽqId\CGKۈp!N]Ox{ƣsiJ+ HEHY}Ooz ?XН`ޏT3^}ŨMI[#LP]Q˜1zޘ6]ޏ|qyzsي%-îi3ul @OaGRL.4|lGO5LС,y8Ϲ0x=aXMߵַ|4)BJ+4H\6XeRPB$B "nekdJ KȢD#D@!GN˽+B^TۘfZb!y:!^-+fAa0oU*QfS2qb,IlP2! )/[9Sv+H.\\-\^0 +5C횡wEKhm43N;蠹 ɂ8P mq-!-K+]$Pc_:5AiB rCP))2]j1M\||x@h*TVhJTT% q`wq;, o+dнaй_|(YQPf;=b lAxX"?(izsT%@f%|l$ϲwos}~?? WhVU}/2;~o?z;As~NF4~gp~~Çxr;=c]_!~DjQKM@%R%V.WoḀy5Jp0mv/Xog8&Ol؏ҋ$4Xx)?-kko^|\F=7Mxӂ‘3RO -m!n76W7 c嘶5]0T88R/9{XٲLӟ}{wdd# s@yPpHK@z( IDAT h, &8شCi$BMj`(/G+ ,UD,cǍ8?ky'[|C~7*{&gPxl_]`Or 42KQnފBPegc̋%S$;xkxvȷ[~IcQt44g&؛XXkBvѴv ާ0 |;b=~)P@ryJ3Gtaࡄ؞iKdT`%FY*A xN|-6u=q|R+qf՜`) $%2- M(;cHp;Ý szN|F'F HmvHl|(b0A(5 /,ڦݨl[.mG$lX:۞4,]t9 WrI$G긤dRtyV SImD:uI4~^n0˂0; B(CIA}\w(M )aTESg9r2O\Õ5d2ۜc>!Z]J$$#1DN;S:nR:C-DʂLZE&-\_N .Z6FV"+܊g7enrFz*5*' AJ49%ћmÝк%jP#Mr;KMZ cUQNL3덉+/0ԨNA^۲f\.sM譂^!cS038BT(Pbawѥ@5gbig]4HR{kciFr̷(Ih^Pc(QVsIq^bh%^I!O,m_NTۻWz$R>F#43Bo,s6[Ӕ1"F%PP&QMZ@t9m}N_L2&B'RŲn[誢V]U"Aqa 3AdAJo<ۺgO bQِ(CG?O?g?7^~}g7zoo3oo}O!nAL}ww9ʲ#_җg>|>i>O `ٙ1ؽb{ۋSϤ,tA%Ч#ݮ P jhdl^+AZ'bc9.=K:ȥ 'N9pjj-w,SgUգlXwb,/ڍ#p>rrB.X>OHc%p)ń|n!JeXVmehvEAJRe:ڬw޹Ƭ}+˱αimVY쯎ٿ:aBDW:c<&/ bF9+{<>g kl~ߊqO&r#`;^eI,|cF%DK䨤J!'! 68lO"]:ƊFcISL>>vl6_%~7~]ߎ}xw?j>1yn3g&_o=:>7p~Ngwsn?]4 &oPF&եv/#5w_K#57h8vQ6eORbO1]wmof.1(I'I76EdPuByz~wrz{#[%td*m.t!r%"³6xfkn\.G\c;o#^wޥ]-^> P:cw:WN9u9YRkFZbjC#4I (6DTԀ.k qSZf@`؝қL ^żrXfnG^N9s:Gj{w`y`l]0/[%u%62`h3{ e!0ݜK,0f`N5 ͩiGNA߽.rsg3GX<5\GPm?U#i:Zi\{wtۼwل;ᑮ -.>yץ-i.hKX1Xߦ0tJCpt #cH8MXɇ8t_p;xʝ -9i}af%[y6[@(Nihhg:gojDhYMQ{5+ǫRTi5Y;v[~9+^-~$'g&dqI,1JgZǭ"V-x_|oO){&iec%Da?i< 6Scƛ|Q*KLэfb+4X 1+ ʒN5!VČ6ة/ٿfiAfBrycV=EX$C츨 Ϭb8m-iQK6.N]GdM}"BR+r(¢ATDec[]g X&"+F9#y;'TVeE"Amix (DGmcA\ڠ@W%\&Y%1oW LUrU>*C A(s}}ζt4 C|>6] 7 ^((y J*Pԙ%TW)*Wu)񊄶\1 X$MX3<+Bj !)6ڤ:f# b UЬ 9~Oyl؄'.cG u^ D\,ʔF%c[)cʰ Dn S_mK唴(-[;egmhsB\,ZMVNj^Rߚ`DG$Rr(+D-~!; 3KbؤCGQc\ԺF. D.KE|^5 -SxNDj2$vl]d&ٵ)m* V=031Ch$جi0QXe_FȲ (`!Z$XZB0( ($ tH%ilQMnTdɆb&Ԕn=S-)VQشmщט6L=*!qmrLj$(g̠XxAl6bc6<&eBϘPC/fҘmhyIіJnW)"`]SgcE"|^oc. L 6 TIiTz-2nP:SB)*$-TAjv6͞\Sި909H7'J ]V +e1yxJ )0ehWNl;HۖJU#oC+ju(Dj{de,<ֳ&+"!MJMP I4 jC37gnjTt J[RH4XME/ZzPAA& 2@:HnǪzmj\Pd"W/Gc/2L'C)]+UJ%#uN}AgfPh&ASk;1/G,I*2X-.fUM$,BHkY݃Ra1ve+ 1Κ+>YHZH#]9IYmu~-'>ɿ~??_%|' C͆ >n~xP:Ky!a+]}Qܖ<A@ #.qf-;/3"rMPmu[hZMeuL-͂f^fDg.>0ló.ө͡eCǂ   BrbY*4v~4eMS.Â&*iVwgP4]|F)ŲxQa|t2-I3Ig219*sȹd\Б mN,g6ar0d__R7 3cޘ80Y& L>^lʀVaLB mh,CK伤5[Q[ 5al{jO6F'טP EtBlBlNtZS Zh*l?&\}B2H& ^M1âSUc9*Gu\ԥ <†Kp) X]2R`\_64vhzIԱ^y~wFfƔO".R銃~rνs_a+~/r\{}>Wԗ߮R/I+TB TAٔlD-qDc8NEFDJ*}I)@PhxiBb񮢑NJ[N(uIK[i:k$,S44vcN7зpŤWI0zDd9"X,/"ePN(f| uVߊطyzW{{_u9A#,W6EeQu Lp6)z ̠$ ,fAv & )rSrvgy̩5&{P3zDovI-rF]k|wMeR+ (*n ֈQ1|Aq+s9`e6X2g`]/Ne-pYm.1>2q׊eE,by"7-k`Y!hult6`2sZs0mtUaѨ^|n`Miu]CO|??Çn~x$|NLg 2oCs;D;e+b^ 7IiϹ .\Y*Gs&jJ]b))rr|5V*Qt>t582a%3D F/kSuR5EOy-~kSzŔnYCrE.iFw\v0xrCbI Gt1m$K]'R.%nߌq/S>g}Ňp☱~IЈwxb轜u&Kp Yj/>r!a1&B2D.J5s~f.ڬ&o»!4ag \/Kg+4Xt_2|pzP y̅-NoqH20Vt+\R%%Tx5'1Kpk}_D'#nUdDku7BqpͺpyT>~ٟWK_R/~/_Cdg 7p?¯2"hjz,O[EyjPc@eRZr2V z[*eQՂ2ר |3[<Ohk,~UHU(C\ TY^M~Y32 &t`g nOs'y+S IDATwW].|M;_ Sѣ-4m_鄸\0$Q fc/_pVp6\ z0%z#}ޫ/@Wsl464 Aqf 4:)R*!C* ͔jj+B0 (q҄*ҩ)mɆԴ(L4[dt0kagO2 5eu*Œ&+"=ń,`"p~ThiMjMbxvg!r ^'#ֈb(hW/oI]ԷIMYy<+Ȇ&gl]N>rxАkrkĢHQhheM3\Н;= ';''?^!4W@qII4ڡlbe&:.S%:P%<+(ޫ 2)([`,6%U:cH~x CW&)mB+ S &+,VtYVYP&S"A6Hz.Vb^1Ծ-F#-hf+T,7\}n󤺃gl ^gC0ZfF%k|B\`, EKZ3Ndi=FT,˸ͲhLsOp3P>jTiYH TMɟЎ܉҈BhkQr:]>/ )}sf9/TAumPD+ RVLW_~.#Dk( C45[Z5X`]717K|ƉYbO?79yu`RdAj|eh-^a)]c Yg.ef)(j_LC3up ){|'kʐz3R%2~~~G1g>×W*O>7xu%7p ? HQh^Hv֔{%^FQWA92(فd1jrAW$)\"[ZdI>nyCڵ] ;S ]qt*@ق3H?G_W5+f(ɚ6fhM֢&YC%(A֭{}c*ҳm᫐U33F@Ai͡XB}m1lA'9rT|V˼CI -Ǽ35Bgq]:y+֨i5BcK|;4 $jQcN}NH HKft'3/>L0YM/c\hgT f2Ӻ\1DQaHqNo[bud0/ `Gb|Oukk4sZa9Ob̺s|JKp1l 9 q*{4k¥ǻB*n'?C KtSV^k1˔aYwX]akUGH4ҐfL7jCvȰ=!fR96' N!Kk,(ޡVho -V2э})V+L?NEt)?}wD=&yO\ԁ N1(D݊P[cuQA?^_ӟB||+s_n^7Q8G o-iY#"k܀wHI䐄z3'm̛MNY-uj#:=JizkdH dsXҒs+5gjCr By~k֕cw|(&eפLJaRHs;3LJN/Ǘ%cx9[1q;,L3IBP`cRebaR\X&&Y$C&,dPRu/=0Hv Ba~SLɥΉN&bY#.gShk F F >z+b"+"Bg~pD`oZfk`orE2"3L2i s;<\Yd6:b-JpE5>w]T5 f`0fflGz3!3mwO*Ɠ3Ws|m]1J ^ 0AhkED[[WTl> &f@tHmb"n7H 1Ҩ@J\bF%qu 0vK(.79C{ HU./:<)ĸ>)*ḵygN^IT ߡyUqi)l  )s`_ 1_XڜވSwUEI/ HDGbFH ل/}^0]U2Sͥ15{(SP7auX^妘Thxlhh !6hc2,ՐrķʏP:!-NM/1fT f5!JME[ySoIF+ [6\-Nb9ꂇ;t&7^A+ BV4%'-C+y)|CXnhBKY[u&s1Ã)wS:Ea1ɇÔ>v# V6 ZrQZlҧ.cG+Xsp);ԩbIB;\#VŻxd᲎#5@A50j9T1IBy Fp 7C[K;5^O-9!&"bM,+tH9>[9c.x}4 br!aoGޟШ^.^ڰoD@Ϝ'r+}2Go2.y}Yżd.[̽J 1 ?`+^FD4 zO,;q <7h#l =2Bⱦ͔ublRfC|6I_m{.SbqKaHhzhAX67yn Jvѝݩ){: zt{5ݻov} U ^Ub; `AV1zL{QcKN_.;;dwC[>^14__Hqhl&@8xR_1bpA;AK(6 =4c 1p ;_d4u[FWt 7??bl~SsҤL@ ~y3` nύ0ꊠHqpN+ 8g|K&⎽>{(Hq'1TbFvffL2f80喩aSh4i?O;B6t Dā>xK5QB R|[~ 5._~am%+ޗKN?U, apt ZpQrMSd m SHmե]S?T[!fT+杤AKˣkW<9_,&oI>x3z#:Aޘ8Q[ՌrƠZ cFo^)9o24u0A TSQZnkhpp9`o}>ϐDiUc}ZX~[,66SwU=Iʤc,>Et>ۢ\z=VĵQ8FP,q/J?ٛ>+v{82!{2p,r^" 7p2cSwD6.[`Y9VX eUy%3lXU&ߗUhu:QN1( 4-x.&6!;W+5T A/Yr\q\1Hq7i<7oV+`[HgڷQAi\UWQ DPA'Oi\>g>%1]D.bGrQ77OH:(řf)$Vm%R,Cf5 D~b3|~v۰uB֜uȚh{^Fx b3e_ⅎ,c3{NMcI7xޛ0wZ!J([Qq|ó#6y|䌾f`//=vc΍46z}Sx2 e0JTBR"A!?A 9{Nɬ3OsMZdE4 s̠тӗh?Z=]VkKfpY΋7 '+L;* ؃ش MUN &#3b@-9ߝ"w ~kbtC/O.9}xAc,ƱE0<|Fq,y?65͖y}DZ+)Vehpʣ $MOi=)+l+G 5DB"Yj kOV\Jl,s:pO+4@ ;3<%l#<<> Rih,zջK f'cGيlEz,'=>KGHOiSbСt(`Dq@xxaۉ׊hIs(+1UD^ӵWt5r``_`JLzSM wlÀ}ԣChT6mƙFduLPyb_m7VHDGNt^xvmq %אnK:T!W U6oF;(]oMo~ `Pǒ'y=/y3~NnkN5jE$G++R$؝G$Fu(hu̗5'Mgts;<=A:-OfT#YHD+fѝ8n;1^L s/Ɯe5-mՊq<3BfS\mɨ^" H0 w~FWkZөNIz枎TDsJn1K4ҍCz\tƁ~K2ѩ6<䜮km[֋>X܏(M $e;yAZbU ?4F8(fQ(>;:* ^țl1=@HBwYxw- G4FCe[(ϤpN=Anx/y~Fm3FDZHzRoK+D?7N#<_+t];QP#dtKL/q I ? g9Ϣ<Ή}H:DLk$94L֭#h/^,{_ ^ɇx? bl혫= {BRy0 OK,0E ]@+(pq>dΈ`HC5aw;-$۹dK:W6xC5!Tb? : ٻIykN+ʕ*-HʉEZDXփ6'Nzٮ ?fa5㰘1MM]({!h-lg @dmZJa^8RjKzgGؠH aOjӹ^s~oϷ_oN36L-Sea4w족"[3Iv`ٍNIBQ hjw]YS63^{ Spo/F|E?ïѪz/+ܛS8enkbu󫏜_hsӿ|>AOԾ! G '.h jGgѽr{̫G{<GGhIDTPhѐ "?k~Ccn Fyq|}7n֚q B:TMY<:nYHe7=Ið.2Cl?:zLPyGW?0:YWJx~E2줇eX}U9d_؋ } jiP.lKnI*9ઈ̱:6 Sz+[[v[Ŋ> e4kD-]5$`9b[) wL됲1 #wX~B*J%Q,lȠJtFR|ICmHCEX Zk&rX̘4wMF5{l۠CxľK커B骝H5^7;H){Yc)cw2NNHv욐]vhH -cK }fUICR$lkFg.b~/%mh)l@j„ʑߦ)[&;&I doG}oAqK1d.PEflI3pC8$OdmO+Wj #FqHQ#$jS$UESӞT#6BAȹ 1Ba 4!@ЁN=a#;!ؚ>+;&EĩRV3PIyΓ䆃hY3r(g_{UqIQ*xo@h;*҉kq(1MjA22IM %CJ"3\Z{9e߄D7R5 IDATM.9oԞP] DNMX|dԖF-4*]Kl;~,|jMR=,ɐrSsM8_\)&F^{z dɒI摤.I쑮Т .k* 2;|̼q"cY)l80W$(SLHIl}noyCyʵu}L1 M5ҨLIjM첫Bhrr0akXU6[=dҏsv#ʙH& .q)ZzNFhVLޱή㨠pM @O )'KUEQYAXaDԵ,4R,4TC?yo?N''w# gݿ,mxu'!}^}a37"F,}7WWkA>5ɦI#4M|߱OCQe4` p͈-$j([1waҚ8HHQiqqyfA52n/"|sɞt`i+f2K#u="Uc'0Pmp؁e]w67L[mBZx$% ]vZɔ7OA!"3w/x\4 RKRZ@[3V IqY{N{xOJ|+w 5K1&7Z|ʝrY2 {>t9 z})ΚiԾhr0` Yv,!Q}Y}γ9gs* 3m2&wmtIJ2LbiU$r JkYݦg[Zp1eҽc4c4gh(4j, ՘l̕88n4TSI6+ǔTB4%m{3>%${ ٛ!fiXi'OΙnPYy`ezbzb*WL; /-ƇHbxb\\q9W<帧Yw4+ʠ2S ʒe/c1w!52c3hRf;vXAF/_q_1 98x\s_sI%uPgvn?DfqCP&Uj5w<W,11~$IrœͶ/ꯩ l̲< Jaf~˶ 5R(r+NN|o-pChlL̴MLZLe >w tA74KQ _YNpȍ}k$hAeꊗ|/W"BEө:Q:SIRZA)1HlA ɫ9Mǻ~S=i,scq[̳l"\"+dovضM~Y*S';w_㟷7*BH&] dK QrP͐v ,R͡:=}XK,+'"{OhGbln0my"..&8c˒$TU+}HhMbc#:g9>S+v ULDC:9_iD{ÏjpwMqpp{n_WFkPx&#g(8g 턛1Vܬ;$ tmlF=Sϧէhr.Zw\ KDg}gHKωy.H<sy0ߍ(o%̠{z?GoyckbdfF%MkE`-:K0 ]ґC5#t Vz]2(|QLt2f\B0OLjD6YilB[Ō'9W=;1￙p8ӟ1/1~7#} yvSҍOYW ߃-^}%{W&lfx:0?PyGTeAU;EpHP ]՘d8T*U r( $v)6fY} i$S㖾&,JDYZU-Vi!7۠9mNɀ%ZS{ܑcPW#Э 0şMV+JC#5L"" ݩѺ b uj ۠Y5zA64ݼՍNSj.Ma %>+Ja:*B'cK#$썀(V7+1ffsQ~7){^6o`wF8#g2\ ,*#uE{3L)-Ak&og݄.9ON*rvts&ǷL_޲]Eä?M'FwO/ Fږ=dCzj=`\X0RmDXݜ1 NU<> X#m<ֈS\?So8ry1ˬOp O"ƒc9JYaq73BkB,E{9fOl<   a94>)-c|-K-Lcm;Wtdmc޺9Θel}Aj<=3#jj̯Տ8`GX*^A@HvzBJ4"en pU#-l$yg[vxlԖg'؃i*ҍ˵}̕}̵}Ly:KيŒ-2W<Gн XB6M{PF,j2aF`Ђ hEw-0Vc 2A5(hlx嘹=fdDvdYdEYt=l 52ZcHGLmg66Ф+i,(@?qQYy9JRKIJ j̦j*̺䨹Ls`59nQ:2ȕEnsYV軚rOޑ:vqIц2&M rȹ0jJ ukX*sߦ?E0G0Tda؇l;٬5Xn'OH_lo I:j<:6;bpǍqMhU7[x_W>`2^ϩ\ BFa3۔:汰\\Ň,>IrAg|:ZIֵpkc>m;Ype {-Ⱦej_oQ&"ƟP$2r]pO =77kr"OQ`- D ܄Ǽq=Yu΋NW*Hw|HxuJ =A#՛2\P]:ˌIKR^-Q Z!~}Q5:rؼhR.Q⬁!m!~mR&mR&}c`dY28Y5FWtf ]c(4'.iPZe o0WG%mvNBTJDuDXń͎ѷXZu-)VE cI; FŚvKՑTl׎KV=$;&=Sq#Hbm:[=}w :6t5o,!N@12䖛юqgvlnO'tO1zj2H 4wH cF-ξh\LP&4J'}9;s}.lIF>ᣢ`@})wt HB$֨ ʔTD *1 Oa Gm~1A3v'K@0G+o1{'پd]p׍x׌(L\n&378S"lm=Z< GyG~`=T_W #jK?Y5Q`[)u}VcSIAȺ,KL*UZv11Ř1GI :[Rh$lE-2z1F6m,LGX*'S6ic )Ma)M>+ѭuo 4IEQPev 2Ztcnֻ.ޣt%׈k]S(sroQ-M[ڑRtjѸZ'@!ЪUQ8u"9tՈ1v0/_6 \E103P v8l {s;2ΦRv7vIJTC΢@g[vX=ɀy4^H;x#[ږDɀt-v3NȬCXrx$<+1N|h7q~b-eKU⢢"ۚmG ,:4N)%vsc4.rSIgˎ=GCD4FFhƘF7jI]IZ߬;ot*R#a^JAB39dayATAKʕAdCZI6)9 '}Es||RT|(eQ3 y} `7٥c|:jO_m4kQ+JaPV؈.R6"!2=0G$8 n[unFU-))7K &#/o$=^D؋ )rjJZ<#KK;>uGjKKHR:U,I1#yʹ尾᰾t%I'hL4UVTA,!a+,bC^:yͅ.%iϦ${?uT" Q 4ݬͪ1E%S,r U j4HmC&Pz%c7t]2G|LO;7w EʂJ HjPN'зX]znDt|GyV=/W_b4%^)~ޛeiz߶yg:}<~8]J*Aݯgt ʪ2c<񙣑F *趻ºqNۯH]sH\Dr[9E?ϒ Xs\L2 K/ Ҷ4}cIv]ah`w-1tS3k]g#sIz >?|M]딕N̙@l++E S.2I~!-F=ѩk L9gttHi͒AR0L9m(MdsP6ΖcF9FV5s;N/Y |eINJMM4M̴ ܤMTE[. W*t|Qc7Ui 2wIk-kZMEaqPKt.Je;-9Ni&>u}DD)W9:lnÌ ԍ-3ڟQ}wΕ81(q͒Ni̶DHɶfc+|*`ѡ #kMq eީx=^=l!0206t(PjKA [~ {23&TJTJX8N=Hu5U2 LաN:tظ 1)RxG( (Ds[4A dXCZECkbC t-zS1|ּCo&͜ogMmԲ CvȺ6hSv-HOx).)N6)N!M)}oAh~ᗐkDyDmEi%QpI+$[UtiDKfB $ĎI8좟]v'SQ)>yP?Q~N|,GyGg+N+C.))ﶟ*TCS键nv.,H, /.xBb\MP2(^QR&kx@JAX 1ĩGݴr U1=ѥ IDATS<yHm#Vrϡ|P>:\|(ι.NQۖ2V׼T_ccEi&(U81}vˤc),,/'%bK-/;xX~F"*SŬj:"CxvZX...99Svt;k^nP;#>؝{ 713&ӑYn2W\\\5%%<ƭS:E6 }"D:S~/qW2fltwWO< t{WF\puzbjc+:VH2bM8Yg#,Vhr%%/Tr׫?Ue\+K)0UmCt8*gG١5v^0̱%/ßh#ڝ̺Ĭ ̦ ;#>s1r("㈾1(7acxF)W3n#"խysvmVU>9g_}%g1t-fBz# U6֌wjFU~G_Z)ZR0횎w6Z0$Ⱕ:Vж%ӛ[报rMQ܊);ŧ }'$\O̞Ҧ OIz[dŎd^(KmآjMet]D'-' f]1֌%FV3ʖ|Ve Bgk YS&Gh+VSQA/X6c-'!$d^%u& ʴE ]q"S4BPR6*T@6yfg>4LP%4ۖt˒ޫJ+6ngbwٝ5*ZcܳggoѪ\(yG~g5s~mrZ5_K8P~r%l%^O v%1nhjm(+b@Y0 1TMꏹ'i&P3)ϿWhJ3',>Yn#) :5f,1:Ƹ-ϰ98.Ks@'&н R>d)r0 ͐Bh*0¡#vsF̀%S:kP OIxtRV.m&cm?qKJu9a=d t,2"CKZ$;0|}?-=g ^DS.^[+NQ r(oK2[VOYZU<)0w.GUXЎXP*j&y`t57?Yt=O2l~E񽁳`W&%W6N`*,j4A+n;HqG)zR&cC %C/KfheۜBI.( >Rv hZ6SK<5RR4Gl ?@}UugU XU=ܚ'z~<^J>S&|p':o{G[^μ7o3S6Q2=cDh*J7X*C.Z[ZgPUZ7 5I%8D ~$`:鳒nld)h.vP'*R.2n6t?ېa::CEǖ *t*NƠ[Ԡ Ce6gYNN f#n7G\lp OOC5Ѻ jP3/9^slp\38jF#ReVC~!xt}|͎{n QJ\6[;`;YUaS̓el8[R?ږ*m& FR ⦦^݋zGyG~Wg$Qf;G$(pԽ3c)/o4ꍊu{iA}jОhSl(*֧|gR7IbAGr 5Y/S$& b$lJqkQ&3fhy7|AnYLX ttp;sdHVY;rܧG{wS"IqwsbqhvD0Uʮtؕw1w11q1*grF-b cP9*WK:Ei[p$BEs <j*huŒn9Y}6Y73Bg+lxD;^Fox%HFx]ARH, tLT"z7!gWV!ccx<ÛN4vpkq9>"l1m5~K?AiuDPEC+i[c/uoi^}l0ff@$fԚ̼d$I~h [pCo,4LfL3f s`N5,>[U&MP+eH#ۺ0_28fL^5|"[sJ8x^ᏼ%o^i;X˒6ٛ5B 6n&,}Oj;Xxl①SQs"9!*9cr0tMٗRQVDxܩdI`E^_n˔pp3?s_|urK>ًD;ǴkmKFւQjs Šp "cmxhlۀs}=uڠA>*L;;^`6fYb&::zxBZ!A!؅t WQM'14;IuRQQR(OS1uvGy䑟1?[wyjh-jSupڽ!/rk4l4m޹07Wm!127YތY|;a1OKZ(4eXcK5gt5QZڒIEJѳY>iVH_P:S:셟7Z ',YbEY33ocޖn?Yu:|'/xdC,m´O/xzO+¢ \ty/8 ^k/Z`h.949wd)DU<3icB5Y*C6t9,n v;5}˸^_qM _?5K|ߢd }+]DJ9/?rj`{߭擪_ǖ.5]DqHUDqɰiPipI`v( >vN)Y%?}k8{u g_\\% ic*ƯvUL:<cnC1[| ]h@*5FP` 8e?Dxs>!&ūR,a!F|~ԮV3͜q;'CtBk4nįP&~mRnĦBl 3.RcaD_١ƿGyG~ER5v :VoJҹľ@CudO:$ MW,cHMCؘ)agRu(7MhІ*m(PjB Zc؃ МfAoJDc.k]Tܵ%lxyC4ew@}삙x).-QKԩr* >ϹLθ/C,=*tc۠˒JU0<_`ǜ[1!%@"Y3`'|n)5@5oEn_32LSC!s|jsD#ќK)p`e.rb,(c!~{2$U,-LqM6LcT/B:(i;̳ . kHANBKhTcX5iۍZKll}#&K1`e;.U! ~夺Y斱XAtYj@f4RAJzʚS#;p$;#L"LЊYsF݌SoY d@GIhK+>jUíb]A8CN'As*4Bsj:")3B)T 7-2bc]Tu|:[&!~؇ɫU' }gEژNҨ [.ylҬTY%2l=gX c Na 5. :q!O]6ajU ώ9/It#QxkPN9/Qv 6jIK= 3B-%k:&wGyGg+NzŨcz3}sTzwE=JA ݤtL 4f[ %M;A {?Z1Zթb-KCޤ`j9>MqxkQg_b?5t#|:,0v3NI\*Oh2*#hsE=R< >bH4OPhe#3l$dǏhVU(GtL$%T7HnMs&&xyph *Šk'{$CbMq#OKN渹Xc OJ>kG5V?I|v{.C ''XN+Sv˜!GugZ Y-Fbwsގ~g)8ɯ1]lh ^w=Dl$FR 1nTs4 zM[iy(I4.k pO*ST:,E2S;4E6EdRk1N ĨE [D";TbIOhޝ@` rݢr,BmةP(P(u>u mtIsFtB-kz֚bHXV6[EyHː3\r%Ok}@ 4WûzQ/4ꅎLbD\{\sENv6ya#80yY޾Ĕ H6M,v) 0)MGK زgLT T4Vi)?\|? Qxl>%~T\܉CFr͙{Cf[(t͐3׌84!CkMX & pw>T;} AL&=N<"exruƸ$oI6?_}ɕ~k%p#mW$[]Cf&]J @P P\\ex1F޸(X㈔CqGE;i5lg|=vm6t 8)6J!%f^V@h m/|%(\c'b|% 8eӉ"FT.谥OD` r4FI\7vtqGX19 IDATpLY>O¯6"(BdE9mq[ObC*4R.M&]"gwt永^417!7U4rCWneCW]]B6_l.\ޟO"%G/ kNkޮ+nc ly}GGasF30gCEZ8S9#vv3dجKYhiJD">y%kqe;|_bmyc<{KBĸ"1mPD^WP޶Ζ[/8Ti T?KDenj5fu7cD_o=w~m-~/&$E"4 `MQ8rluFy#<#k~0<-Nv F9F-tҷUO` HLehRjBG;'hlJ Ie4Tj {šyXccP#%l\0]B+ F0j[2A36Og i꒮X(E59ƨ@KԤF~n0j 6 8C&2נPk0͂Щ 0ZW Y;FA?uRheJ]I:Ң:T$VTaWv]fR72 \+t [GjvH kR߻lPPQEkUIi d`Wtu!єݪ1( bCAoC0"c3>ЕSi R i 3n I.IDrvZc*UJ=\y1ݛKZc0 يAb. IiawYFxFLk( )?BLԘ鎾XpT=XQa ]y1hYQzixFkĸzTdAyKj٬vL[)O5W0?\S&mR8T0IF"d5Z4$FXOG Q =eÁxo#J쿯JL:L{z1*DAxZWak>V3.gLj/",=#[ڴFN Mascu;bX ̛sc n 39%=!I0mHS >-2GJh%ec,ۢK982lg^џ0%J)ȥKG,9R>|%2hTCMQe$o9?@GGKG^YIQzgNW,Ąk".s]@x=`q| '؁R~#R*/Nc9y:6TR:,>F#<# nLR%=jٺH)=r-}ѦN A=i:Mސb?u"Q4Kd MSmFt-csEN5S*]nG!r:dNW\WJ%ɌpA8#ca4`gz<0uKdZǔ֒Q>Ah{)wޔТ:]ޏC/y/BBIY1qN#<2`v:c.N&|X`]PD:oH,F#d߭њӣ5B<{wS+ P5ʾF9؛!J["RiZxc; `ӭu_i![6Kw{o$Gr{~"﫲n f8Cr(=|kfa_d&HGq.`Fu_ygFYhq]1iqxDX\}U2@ŒJVF0ZB(EE4#}7 kRE . ~@?'|R;acP]ė%KU#&5-7]|aq1`l8JxV^c?~|ǰ}ǰ3bعc3#q]6^GL.cD3G;Y[̱yDo-H.q3]r5:jzjDE6#w%\Zk퀴qG%8z5^yź.i:A[ӷGL7ⲡ>EhQ QwH*hhsZՌ6304lPrdc̭6sm~ Wc6z[Uґ3B}KߚV!7.G\>02J^os z9V= mm s“a43иy1L$ X-:FsV‹_3H2m< [̽&Hxfo 9,8N92z1eXamr[/rm!/L Lܤo콙j%ǽ^q/U N% .GH#P=Agfa.̚d" !%u1iXFiKoRx5H^AV*GS`, \8e)}Y[}lK_bh6k7d:s[}Rn,KJa1ܹL: ѩT*P ef(WC5+AN@܀>,tKVK<$G[3f^#,=dе2TGTȈrMM#j5=oo\M)-~N7#ue@dyna99]gBЩlevBSᰵ}6vj0.ۧ|}ωO.ia""O R@s/ds;`#TlE R!}*v[%4Y8X8͈dO魀.u *CP.bY8'n#ַk6ь<$DAArO_/I84p| SߠP~UU0IB `}P J/]iڢmT9GIIZqU |ƐcS4ai#nM [u5ptEVI%)XS` ]ThH]Rqi .t3s5Ӏ \Yʮ`eF:'p Ofr+>+K%>K̟K LSNwloV1<7]QHuJt!i]J`hp/JLY@$F^G:kRz&e$֋7#g'U \(ɢjcGu^-\;l6ɕ)3tQPhk-da# WI^AUTA:l,lNA@ Ra2V};uJ*VoB YHe q/M?qS+ VJ *ɕŒ67iQnMI53BJP .<Ӑjn&Z&뻤]餔v뱝6ҧl!hĐ%V,;0K0AQ!kRo-GH#_wd^*Y\d%gz0$hУܝwk Cs2m1JfCFWC 0rL+]1i.!Ɣn8ט ZľqGe 4x~L;}0}2G->T@Y5p*LrARGvRԊ 4A+L1R<"vB54mX6—i#=C1 Ba&,K4_b2A!MB :\t*J_PYKT2.|U@QĥcUd$O45{5oO92ɤA&Mn!({B( 'Mٗcg\1FQ=;^_һ-Q$!S2]IIDG2Xyυw:ΊN{EZgvצ/. kyn=sPyǶ3,#^g 1jQ$?Gb{\-R4^Zi+<;&3-6z\0rlR\` -<(CҖNR2X5Efrz:W߳g(N  0w_,cf%3c\bC#[ m$HҜ,8ȺO@ @T&ˬ3-{,-~EgVS l z,v8ytvYuCƭ.p#^\DwA]t/!F%!L[I;᭷ ĔL~]US&?A }a.o'/7*R91tS3M吴YT=N;,Yѓs<#9rVxi?jvB,b!aakjsP4)%2cN`ZDkƛŌ{r|?ɻsֳ&"cQa iJc(Ff:-RasM\h(mEBuX[ƸeWy--]CsvIԑDA^n}o˪MڵXCҡK0)c߹+RJ>GH#JB.֥լw!߅C!,R׊0ؕ{14_c+0jtW_ feVEeVߏ : :Nfq:DK|MOK) Y-P=Je(\dKs^a* e}Q+A\{7 *W'qV^a qq_p_b%V?F@U#sl0̆^_nSWMiW)vXNyxC<=jòy<<|MItYp rz )ea1G>VUү昕Fd 4WdawݺNiAiFEG%+1@Nrx'o.új0yc{Z|E<?*-9)UzW?>XkTSPB X7I~׏Q}#V;6/o w&7{>\}=w/uڵd>l&*Abn{"?3ym@V|-9=V f ;w|-?9-UK#8B''<ͷ(W% ^{pCFESa%)%dKd ί_dL!LB֜ p!\n` {j+:sWIR&˼RuFZy{~ŏvC$FŐo QL UXb RZ;_I !(5L39o0zp:&<:ߊ1-Wxq5DkAqLc%i0!HuB0I ^:U$ܟ*\RnP㱨H\hjN\@Rjϙ=޵O募;˜ë1Mp FuK6_Yc=kavKڴ1c,0m\⨄6vE='^kLT>%+mf˥?E=H;7/yzo:_bMsNs{{3ĉ̱&nm9oǟ J(E9/8w)tC r'9Fhk=T8ZL^Z~M|g,D4Ē.SgO'P&B*l<7s[\K~ /|} nu~3-~k' bS֖q1-/xCTVI{2e9:sGǜaR`%*C UX)VqջU[= ]'V{OLv}DD\`۵ܽQ7 P j&w><|ڥB!P]X!v?WvMs{T=cuOAz)~PPCr7n^u;*ǧ}[;%nOKtCq_?}MJBvBx ph>d.9f IDAT[e|l1__0:K '+1'0Fxa)Mn٥}&Yl R,񂔆Ѵ67]f&.M\] Y^6:ia.eB%uFrȨ2CXsYR-uQP- [3匎tpytr$w,fNp” 33l-{RuJb,˦lmmI[6 Ϛ#ĆnNOLw]ɺͺˌDb'h81A93D#tuW;;^m̌l1.IVZ3kt]f]"i撗/IYJsԼOjL.N зh͊AKmld!3gMJ5ўb̖ĎbҠ E+EAΈ[&цD\ߏsJ*R:PRCJnTV07;4^5hb hjZ \Ho0WjH;z͑FHu]:7 1/گٴ|:͔Ҡm1*58[]luC@3LteVu 5jFg5G43!JFAYܚG0 z j/hu ]5V&c Q xVgxvT?n#r22"]EykngF 9}}bb6Y:Mfz3#+k{+5τ~<ГJiQ: Bj b^kܳ-qdE6qdút 2Kek7)Ry#ƭրQkQZv38j\׮9rUd'0+d,Vl۪A^Pt%\ch9 Z΂$K-eVCޮ;Sqn]f)t}lG tm&^>է\C Ɇ% 74 `ؠ|Cހ!CyK1$6@A A7 _)iGs7|^9_NS nq {X%!ESQ@oV `1J{4gpC,"|>1= ގi轝ns\F2Gvskz/G޲j7lV&xJ?+QJXn8nePݣ**˙a ay-j[lѷ ⧣79bϰ|;V힡6Mjp`g'HkI5_xNDFE) xKwY8N/i&T..ԧ ޿+D_kFPa bsk0s||N[[p>[~Xy YY˝x1=kL/o5*['l%[ؔ7 ^{\GkI鐔q+Ud41P5Zx7)xJ3*ȰwͽXBN\^I{&oL߆|Պ_NO>MD^y__asW ܕLeŲMtQ J$lbK-{@Z%Eox Dw2)B>r<}q}w jhPRKt(efQ&[˧~׏q_) y? 48B!i)Ƅ2olgQV=(~$O?ǍtϠ7bcZl vfiy>2 mN߹ E4W e|YX ?%(U#wJз`NAY+1/O}dA56('T FmY|Yj<j ׳K7Hk#z%p&liȵ:i/Y0X'3B*ޛJ~//7&ӫwH5g4-s˟kmsU+ꨬnZ9$|ԴYj-Rkd;߃CrD.j!Ԩ>Ө^ȁ^[>k~˧73zdF~QVHD}oShўK/ԟcVnWp-ie|@*}o@BX]q'I6h=Tjj`fPxYB౼k1񄟛_ IڵH<ijI=MĨ ?'wg;K؇Rw#S @z8}ēr^ݙG/*`뙔zKCѕ`R[@)}V/>as7G$f :64 dvcGVDZy$[d\nL" 5-kJ+ǘnX ʥA[l;'Va<AfD˺`nX;lMxaLY&䩅h5U0x^CznA`iKZ6guRcG94-w*[U3:ՌА,6[~z^Vfw||UeR2*OշhsL+PVU`fY*|~M|1otN2pCqazų욁vGZ4xfa9'%b%SQ $J1'Bl{lVa ke߳HU -ȤEĂXs4B C%kjY̪rHXmm$:))6ʮȣx[aDF$QiDO8.[+v)Zы]~ .,I[Q% ,# ]J+R\i{+Z-g U24̄@sCƶJ՗xбB P*ѓ 'JiL;GvP ʶIܥL)L $/, dN+_c`Ɋeǫ~89jiX-XLW52Uk cga)vlb(% zm4} ׊IpIpHqA: vbTɤC)uJi67y(+s*:8ח4ٰn5Y-\,6[buT_Q~k֝\d&q,ZxBh B}bzH>r) ڊ\:+\$Cd%뙸VBi&gk7)s,M*d7sgL.']6N!!L9Ҩ8;?Ȉ:>cFˢIR8(aқ.c%6Ͱ 1)HQd &+ g{İbnTu*@B ժbam?8I@=\ZA`m=#1j5jw5)|0#b}2ڔR{| kh^tMk#zPꃩ>u7d(.hq'q_?}נwyyjp<:y kW&u@>upIhk:1cuY,;p}j-99眵!JX-4Vs 0s (L2f ڌE&6QG!6dfmqJ?(D?'웜um]s {#Ga|e62v( ]V()3l7on +bF?|`FfMɡPr(o㘁>JtɚO7H]Gڮ 傦\ਔMbTa9bix8S4KFcvew$w짷)v#nXAJ Fȥualƨ2;t]bE[,i+V62r{>$|jB?1ϡ YOg||F"wnK~kowݻ~Ι̌ c%_AȀblHi02_G!ȉ X8f\o߫^}|f9KZ{nOu,F&'ϻu9y%^YLͥ]Sw/w@?뿯, ">o @RY\c .Wf/J^_ qiut,Y\!]k@^5y6}#[kxW H]4V\梡r"hu\&\?rk!75"! lV~)_: ~T8̞H)|g|B~Y=xGfܸ17^]y&C<, I1+~n`٨nUaZ {Be]K~2{O۔B)q;smmT+cVLPeV&=,$~;c>|^g5-NMmxw)D)S '쮞 tDI$ "i2JYhY&˂! 7=F41?+Q+s#so8cJeV-H}i#h˦E [pː ^YZf ݃a_/®m:fZ \g*%n7ҌVRMͨ=Q]APt Zcujl&#٠sx=' _zw +-ԑՔO|yxKFڂY]ܥاl&/ [ `Kt/öB-1HH1gaP* 9iE(XHnY)+wrv'ū}ᐏ&o>"]P$*UbNc,76#"E(JURh%J\,/z,Kjb]! cUM;)H:|^|BQՏyM{OjMO< vxVar^a̻,f>e! xoϒO20[B0sV3e0$g,f|ǜ5MFO*M7&t`[,MCshmGCUNj'l}R/ ى]Qޭ#np֛x6;&` % ?%R\u#gtM+/D~U*.Ž\\K_\ȅB!K"LAIs9^` =-ϖ<`+Gs|"ux9ґH j2]mRwTJ,͗1JEwҶƸVe@SC[HEQ9~u\b{` [YjK* 4g4ow";nM2X!]ym`v=Dx0y +t¨H`$̨)6R( 0kAR̀T}yBY*9fT0)Esr,g2EQ L3tLM:ʄz{DcBe=VCdhʘ:#d."asmMbŝztkwG.˺9p_\+L@eqE\8>׃]98.* `M(/M \s}—Yœ1'sGKBS^h Wy{ՓڗK^>PP_uuwkJ,N NfuNfu ]QXQ5BrȖrD/;W"WCIL'O{dxbi<MM^1ڭ6CBrbςmɆuJ+Pݜ :%J Se擘:BJc~h8 %q ޱ1ˌVmBAu$,RX}>.eUc\ؿS6O[EMʩ¤RRejcgJP%.f+VZF<ػM+B B%+ 2y,!94_PG41 D՘SmJ 8E"J abJO Q(jA+) BjQB=F}MolL%:EOt n;O͟XA3} qKgc*FϗLJ,VO$:'n-¶sj'sj3*gsUB-^Um69d|C8[989A de.9L GN#˱%eB>P|L͌oԻ nØ:'c\ZZ lGlF}dH-?ƄV(`I31Bt" [FRfcfZ^0K ]gLhI<':b d1VQsn&ؚbf2l -fՀİMĴ4-gHP1%ȨD6CVs!D3VڦSƮm<"«@s]_c{XKֶة,ݻy??OYEg>~~rwwKKx$>?S?œ'Ot:؏?= i4FK$)Zh4/˒F@V`4}p?c$K5U1e]`_ÒܑWͼ+4ENJ#;:J?u^xYEJY\PrZ[ůsocw߹,vf5 阞>'c Jgyxq%d%+8 OU)[14KN(M)g\,iPqgwX.;lW>S$Pߑ(O$+9J89fFQ~!bll*sd#X Ia Ψ3(:%eSuF&GV^2Ɨ1ÄNGPP Ay17Q};~gGh=}D1b@~A~NK/P,cO}iXCM{H:#8 =&xgς7fxƑɻK۸ǤVEh%^+s b.7F8rXUV1<'X_#?`vVv@ATsZY1=f, ˲RzHC!6 km-t@*H:OG%D}dYDj&AXiaI5#YK8JS4+aרGSMq#5}j?aJ] /&On[46Yd>4` ssHT(yO> k*CP[L͛ @Bõo#H&j\ݡiqnrxS R'tlB!P%//|? ?|G~Gx_7\>׻KlT%Vp0qʫ?7 |}{L.^Lƕ .gZ ]6*{^xCrm\W6^w}]/^W"(~nJcǞL '&($Zڀ-kMwH')tـz6R0AByaeu+ YC!Rf,Wa<2WfDмTTY*!BQURcUTDssjI2WXd>`rZg_%|TH=EC t7& K04,=&üE?ڠX(H]?qW1eۀ@biO0SAiL`pƘ$TYʜa f :`P6xVm H-PG9j丕]"o+leU$hC!<+UtIlr 45CSr4*UoBӫbI87 {}.8c7&r媸M`[pb+Lm,5f8`K? 0d}DVe*$$dL.99P),Oi,|u@6e]#4R}mqn&Ya:Q*m҆ 0A %V`V24@-/,4T3Z!]ABiLӍ6wytmK<`)DR MY^>'4\%_h,YfzP(|j<] R)J#[Sl-H9vSM͞R[V6}LM+(zgXj+BDYFC0s"BǴSƘZ|F}{4莁Q0 X!7H'QBS) A]?6cl7/eN$RXaQ.TQJdldҙdUVsX3 Ũ~V8:v}YellO_~~˲8>>?_xE/8,K %5A\?/O.5\oПCTy5uxN\p\so+;ߕCr]B@!@^ 눅(9oGߒfR嬯L|$׫?K9bx!iy 87c&+?2^+6tΘ[ NW$ݒ%]\}>բ7V8?\Y!z/C6- (P%GBKzc,&(6k5!_,h)g1@CL&7@KofZ#vO [6^uF}4> l4φF[ d֚,| <2K'$&e;cI0 ;c ZB3!"ې+*|8b8f;#l5fh493Mb/rNPΰd|~v]$9G`Z2v m+A1_PV0E=Sʅ;ƂnY0TXN}^[Gi)gtS:)OQ( vFY' 5l%3B4UyJ՞3"ܦ?QXCkpY gV,ePˆ>> Jβ*ZcȮ1ꄾ*l [ps3ĉS9=VIᠿTۉps_.v5AqBs'=}oZ,L&J 𼐆74#GQ TpvӤ>I1&)8EO3NI:jRss) ko2wꌌ:EeXY<`yRs~'~7xn)ԧ^+|s_Dz,~g|)Zy[ubPӇaZ[xi")W`,1F1BHR`nxdpf~VlT:ֻ~ԨUU'UB#AjXg1LКeR&*2ҐN4v)e6nLr!sAc ~g4Z>sxrY.іTlwة>yga[>;EK62zzs|D ( wo2>k!g r.(g#vm?%/9yоC.wYMr]'StrE#L<"xGYg)k!;bch1 9..&.h%B+Px9yKh&$á;˼D\Xt&}:>/Ob)'7Sw{GS!cu_&5E$wJ[e\A'Jc2';^])W)u- t*G $ӍsD!Q))GU6&;cC6G՗)VS:bBʻw'T1C bsjkw"(~AԟPJ"SR+3bE1aĻ6z=!9q;G]-yF*xeȁš6 %M0Z]LM/y KK+q/Da&9dŗ'a6RNuiDgGJu_.&<|.ϣ]v6RrLj댂guMFAi[*nLh(c uX34qYc00xizuDvFswH>dV N  pc9xvvP FK[oJ!k f^sog]y;feXf%"t5ц9ʁDs,/C)6R@q7掩j:'j M hFtV{ųm4qZcz7)lA $`9P ^JO:!0')~uƦr[[xq,;,r4X,A)KQ^<..I\ؾqSw-ۿvͿWCgsÿ_ח ϿC~O;4eX6Rd<9WQ~& ?7O8,)z^x,\A\־f(un.y\>úu8ozKR+E/s^e9PvmSX6(*ׯAU&=NXADzoH)9BJ U4x!{AZ_3~zOEW-zP29l"\yYZ`sY(TTʥJuyio Ɍ?ξ=uwWyﲨV͒@RN96y'z?]} JPKVTSt>`x<2Vl"l_]P .?$t9ͩ9b$I% _]Ti@0h3,[ I "|NS3be7 ]0nymlY w;;x|Ejm""`%r`<'[|ir*]d#8l@ic:'gt>>/MQaA\baUUs\"5X CIN$$#@ _S{Ud|>$6tA F S?頃7^9Jтb g{>$kS|D 4;c,qg^[Sn*S>7Apaÿh]q[042&+c~pn. ,ThhKaK,aH@i5`͛WRQ]Y&6%‘h~Jp\NjQs'u#c IDATl4!9*Cd*nU#nug?Po7Qz#\r_ l?qG=xxěs]ҵ){`+!ڜ>78`BܤXD2>@V\c\B]&]3<7ڭ >O/}??3?Cʯ H?`6eSŹb*?w.ٌ@^+e %A}%l{{X]Z<5{ (v.*\{n%H^/F9X;M(y˚ZyxJV~ d }aFwxaz/.D^/wޱ\G9]\qO__v5*( |/\߶6+ \i!<dI`uBSV']*QZg/1>+H1[ %bD %Fze@怖? f~@ $IH&r T mO1i_DZ"¢P lG#mj͂;#h-ƄOی6{.Q>>Ŭ$%ne#yI61IIrb-Sn-+WtMFzh6C R2V(]EKeF9a%ِ┭1>xXQeL#Qth#rTLe)>Qe"(odT.P%b2FM(% 1߆a &X&4Sd*$Hi oҒk2%\]cYB!tB!Rm$5 D蒪2EE'\*cIs>1#(TA[URL4X⡈eD(ӏl'x:˪R\ҙfrȦ8kP-(U<"uC]gOC" !^<}B$gҮUK5՗7v'tNȚ@;QGڨ@;+֊ TKt70leE!4M0>̮R*3r+dPm0>Ԣ2_rkGm61mm% 7΂:3L9EtsZ1|^vELPlO5ft)c(XUYY/^+ E2|m~%͂i=`hZF#F%Z\FZY3[G) e>̦5di"cquPH!Ǵ3ϸ`6M(ڞRۘPmL1̔0w P9 gg/0BȄNT8,*㰉mnTK{Zb!Qb(hyN$LL#bXÞEG*Iش*ڕ!ʐVe@'pmҤz.'ys?!d80 5zkJL%nm M~Y$\:S3`gv~ھcf (K᳟,;_{{ܾ}]eJ/A}6.@??/2v~^0YoWjųgπe^^ Xq:ĵK1.x/K²ӵwAx{+ٽQV&CkYt}_R{z|(&ׯ~U|k0v988L} T(mDC4+ K\jFII}.+O+9Iz(iƔ1cAsJecA`Oyބyt.Lc><ؕK O { VVBT1r -Na+ rm^[/`ǸbGbG\dE)4JEg&T4y"OoikM눷3ފg$UyW.ؑcv1N&0n8VҌ*<*b|x3dDTe-|C>̎[)Q]+iۈac/CB111bj sA']$k,F裍d9"̮t3zޔ}>%p pjwuYs;?S7L!h@t^sd ed2*_CK<=s6ˈ1: x&&,1,#(R]Yo-o{]{[^~6S@Qw_̌˭]XLTBtX15)_>M(G*Pl(8`3yy4E`n;e{P¿~)RWfo kɖa?77o>:wc;wm$_җz/-|sF?|v;;wcUzW@%[&7mAܣd(vY߭힔!SCm$!<`1}/ڵۣv} ʸ["w߾f DxϮƼ&ݢp`vf_;~_oLgC&ϛJj RjFv]8"bP# 75fZG,,Iohj{/h>۠%jU%9gs;-}uѕ ʶvfCnkEH4Rf* auRq_Ь7G8X ՠA2K:n1"Gg 7Z[F-& ń>Sƛ\a=R/t?SJW!g9`IϕjHf(k&V"P3ᘽk.BG|WfjΎzC^-.>?% y W9?Q^/)4OQ운6-:x,a'85o}~K'v ߨ~QqOԏ oxGg|I~G3>R_J--%UvZ.Kͥ׽fǜ3E+6)؝RDq SIR |R(T="UXXۖ!9i%C&(vވ]ܤMD]KLkXm#CX$0A3U}[w,jMraS? _S~K.0+)kfغ%jtJ|uR2-|D딉FԩZFo3ʘ2a F ɔ̈́$sz-~|US':USm4J^ d@%YZ&=FGOSk}i`)yWg^v(9TO=~s|i} Mw:śm0"Icdv&TRGK}9cTBujFr^=z6%TFiTF7gdW|]0׻,sˊ1 6 :\qRbi¶߁l-Sޥ"=;'A⹃?<; }Nyݰr{U?Ec#3<Cyqk74/@g{;8!{pˣ[O5$y" k_:~Vc9NN99'YXՆp(@@P(+hkaԔN$<3`>i5ya/<6( azRY EU&Q)=u`5 ʩ^{쉴ٞhwZf&W = CGh%R7*jѣ\QT(jE~HW -۞]4 D'=I`ϧs6OR4?$kA@8tIP9.Qe3ڬ&SL2Wl9Ь昷1'1j"W R\̢E%tf):.o[]\紲b)j@* 3ũc^`OXk "Aj G{ bBFkԺ D3t=o1v(@]oW)B[TBVTjU6!7$&&M+fo5)LiK悶JE#7 iST:2֙JMŨKzޔfsPԩ*IJuOUB3(0QcIUF4ȶJ'SZ!s%]&Uȑ! P[)AلOQXg2ZM )Ms¢PL*E ڳfamk keoY'<=9wטVNhYG̫J jJqpCniʐf{MyHX'/S*]vB+ی4\tW4MBP6) !jА! QxeL;}tg$ǍxJLR\dG,jl0DGDW,1:{5l(,&'JҍINNQԆZM9I 4|,30JMz"\y$CYemnrjF%]5z1?dw h!d3ްwhtà?fЙ`[&2Ql&E&˨KԆԅ@" mOs>}PnN `1t=>̚Ǿ. j /E(n-"^o_@uT~V>~므]kl)LhS96l.^[\d6ʆI YK9UUd_ĆBdC6)*"ZHj\EV4pnbܽw7 l[$JI-t9Y=c.Ώ Մ`̰;A3mӽ>A(*kDsv+vkv+ꚛ\_qsK4p6 'nkHKRZ84 ȄI-T~0bqE5E7 V78ɞp=m§ĉúh`ޝ"+VEܱ+}Td5G&yf.JQH:4Y1z.3K4(+e~9,>!rJegckOJ4KzcL?/pmSf<[.gSNUi '9_#ѯqVwEP\%Cr_[I{rC{zC=]3K!AWSE}K!@wK$˒\kc[ȶ0ZF;' KQ՘jkKbt|jǣr|ꪀ"C^dTo#d[P6V5dj-j]gG3 (94FۚuBU@!eeʾN۬&G^߰{s {5{-c4UJJGxj!ۂ==;F$gzͳ EC#vGvJ ;Ucu,}֪5K^), @#hC|a Ho]D% 6Kz8$8].g{u ] OS8ܺCnڐ7aOcFR i=<>2&0kb%Rh(B #3hkb\,5Vθc9iy` ':qc"!)bFP5BV ,tp&>9&2:m=d@ 4Dӷ6Yn+C.P*Ί5;+j]ʘ^ ,=N a«:-QvjԠB jD) ^\sc(#wiO|___ p /`wW뿢:~k`Asl<;~M83r#ͭ㗥@W-mStDۢ2L2!*T.URIPUj%4!MMԪB. uN??qǴvBGSq}~{8n :[ 6II1|_c?me9gO :_Ӷgtg#M(UJU D J(H p@!)m.}.g/j3z#21_=K7nJ&cN;Evu̚c3$#sy`&WzYl Qt\3(sJdA6,0PAWqkU_ɒS4c1ҾY!@Utn[ݐfaDfƒO_]?O$rD+Yr^5ZƊCA0T~0%ߤaTz_2QQmC4$ BRT$MJ3{g"wuL6H).ah9c Θ?^#RK5!OCOToSVwXxGg=1ψkndR#75F!5Bҵ@ۃZmrXMywѽ^' ,=,=EKT-USJ-64ۦD+>.9/M&O'|<1Ϧoxc&>Sy'WX2W WJF딹{ָ')Ny|,I&I$}n|Ĝ8' est-VqdmM.C.9dVI$yư?ާfJS^5׍gH8xK9/s_>W]$R"D .j\RrNZWݒ+q$X-y"k]@ߒ EG'M,"a @+N1"6 mvY9]D&)eQJLmC >a|%z^}FZp1yh0 FpWt!ƕ1T *A[DA1Mg| o~[UEJ;c߶G¡;@y.k{S#u>͍{|YZ<>qw/Uok(;$pQi w]BHI}B? r7qqw??co&^#~  -! M1 ӹ7]k14MM+(e%(e%cy)QY*m QWYg"E+Bb IDAT'L}Hla1WC=fE1goX8$&cSHjUqR=!(4VbAg=G*p7o9/_|(isʒglT차;,P0Akll 5%/M•ba-|F EWOhT:B& XvfjmU t;G-ZfJD"l b&fjLHJe"-m$jRJ4+ffUqTUC *E%UMBei4i ik)*QETZř8:MWK UŶ-TJ*,SBh%EX ؐ[c1U@}`bԲk^%^BhmN-AD}4qh v;5S;5Ub!L$P,>b91=S/ ɮ$}*Iv~oZ$M(J*V$u"&0,U#@cct̡έ5$&v^/9J99UQ%(E F-jT(Vp BeᵸX&f}Kud8]?aBE*5`+1&:xA7Z\w<ֻ>r#SQm#ueLӺh(eG400'Wg<ސuEw[b_xEUbT6TDkc !D@G&F %~2T&znh^AYwSAd-ʼ|d}l]_(;u^w=$cH{ w<w_=c܁Vx;( "=z&nGަ=Pm=#l 0[o~BHT7`v`_a"3iB m jQ|.r=D{XNdxv5Z!\[s7G;1a1LhԅғSL5]e=mX2ŮsirYzDXxdzďrn%NO9>e1mC V*8%,ąm2Zw l= ֵҨ8[uY:,f]R%w\uw0 $Ќ.y1}WX~bNۓc!A& 2Dq+ҦMIC 5' t`gp퐶Ӟ[U˼";*l  j3 Y$"rhYud7NUk%gKEPq˘AcPa^0@2O+j/B|kf 6hm0ӎ9Kky[y wE5P%ڳ|uV,֙ &!bHX.c .Ł2}ئOkY1ꨦکHG5YccH\5$[k=AԎ@*`imwWC|I$4*<48E4='ؓ_],i R#ۊ!l}p Ԛ҄њg??cvd HCR+ʖz_jTBVSr -Rע`4qEPBי}K33^_(% n9hsppƢqRmT6ac t'TD-f!м1X>lmm4|mn6*g*N3ny򊖾D%ɄnWHE`cj)f'`;b IO;~"| Y bi07>r&6ߟBo sw:-Rtrn-րW|dAw0b!S:2u6U#jE!lz!Q#+M +üW%_04v43wLވ;,9.8\p8k4@z$\VRd=XI,5JA]U̓㧴$ 혜g6h~![8R~+t7|wnvvY(6'5]}.=]n*Џra5 yUhyKZ{N^Qt &3tj46b DMqq+vX[Gém2HbeCrFs>=bv֌n{F8ۚzUX >Spx27Wä%6d钕&23{o\A> _A~%o_wl ۿEJooGG(pOUdlZHUEi;:a L]us aQ0*cxnkn炍a)$vÎv^,muM8,.<>ڨٞ@pub!P7g!og82sBRQ(+ֶ`04ᴂ (&#l3F^%j]mSr ujmxߵf3JoR$uhJ/8 LoR X#v5@:.&lUdIEWTe*u2q] Au$3$}&qHcx6WdE&a)$7fDe$dKAMxUQ{mz+ ;,icqhQRA\opfcy zP\jA0I1YrՂcqFhyĎC &Zzc*ӰTFnN}ΗZ0n(Qe'p>5ayf֋A&HZ8Jj4%(UAuQЪXuRJU+ԵX jHTdt嵊U]/09[z7bm-٦)Wij|>#z?MDgr XJ)h g[XXDǠ9a;a;f3, rF?6am[6(*r(Je&.Jn?;M+)Ѫ rATAP6bg1IM/tn'܊.;lc(%mD߃9y |_|gW9??k.|9)3R-4% PG%bp|LhT͚[v7 ⶠ ެl,8!S|[)YT-^Wa-/7| ,JFȖXJzCK/hpqV[r/0Ok|($ؤX$ sZd .2ulͰotn/Xvѫ)zEiR:UKEwVnڡ5?-.kao3xC$aQ/k_Uӛ_> +/.ɟٟ};iZ|[OOwVO mg+^ _:暞5%ULU,XFi$XfIkŘ mTB!V׫!ɨ;阣'Zb{ՙYj<žGڷdm7<Ϙ͇\"jY Yh+ h"-iUau$>q<'oyY(?( CDpA;Xs?@O31a۴ζ}:Y╬mFl,&im n|3-TVM``>hs=x|>xIK%kutuxMtDۘ`VejEϨٳKY%h s 笌הF(Tb[[o۷LIpz1D8l3deMvc۲soA]}NnR5nvRH-=2 'pG6 C"Kej$w0XuFPG7a\ aY?``"XcmդP_ei}~sA U~T "  3 ZhyFI1cȜs,#΋+kk+"dgzp){ˬ8?gĹE- rM% ac gCv/n9~[1g.'n'nY=hm$Zͪs0/iii+D Eї^g緹ݏx㿠ʆLՔ(2B#cj,>ߨ>紥,Qy)W.ix>l(D\ȾbTpss?ß0VZ: ux"!N*NJJKu{d&,ssܕc C YP44gE|cS eCO|z'[oyEY|{.M'w7<t0&l-'Q7EiI {o^<f;?3O^ ߻_xd;b1nIsG9."z̾[(m=h$(Y ,2/=O8iRx ^xPC tUF Mw'p#z'縍=E}?VXG(JA\,}VQHjݍVcNc>`w\'?ǒs5;+#^!7NY\Y^XE|\ ^vp;l7C0ɬب-uSe3z3b,v}!! )v?3IJ%U khV>c*I jV`3E1=Qu5MZ?4G%qbŝ=90tcS@*k 7Z jUP Afi!p0ʄ41~C eLm&h4ԭ5u jvV4؈&IN/-96!mִPCVFNG^X7e6wc}<|O2>U P,b bDX"u ?`UQp8{x_?̈́Z?~>z+d!||!Ϲ2lU;*F+A=?mOv2d*]&ϰL6#ƝS/3\%UC%03ҾudG5*W="NKT;+xe,Y[eFnI҇u IDATE,(D2v}BXGS㚣CndJl;x~$JlJ[c:G+:%ݣݣ}{FYpļ#_)V;cgylng7ؙ0'$ؤq |=2Q & ?I|(*K:򊮴)oM0M4Hď5N"8fHuC;`ev?ݛnj'#fϜ#<$ǒb|-jElwX9mVZmfA|)huWwML[C& 79+XJ'iV;fEߥ_b ¯B9KǞ5m &՞|C&8Om/;6>'q +ޱo:8̯ JY l-A B %4ANjnxV@Z %x cϱtHM]D~#uӰjN d]w<7Ș)msܹ\9O؛ mKCTxb8QH# ,6C}Vf)0ag 9 ɇJN7_4xo+IpVרr{(}doD6-kCҴ6-i:is[R&~bx`ey#>yf!GCU ޞأg&w!fnq.N[0kjGW4[VrauYN{p"3i->9GG۪v`4؅-ZO?DArLV}<4-,ȼ"{QMa_iTDֈ&{lDkqV6_ZBzD7u8[8|v3b-R!L$$4D+.:Eg\F] ˷L2Q=g]p3q3,';wH$Ȭ0qxL_/#ƈㄞ=s|LfUwIJMAI ncJKfax#\zs^#% iG5tMʿctsMSQ)46XU]>|}ɘS|Hpjg;;N(2ũjNShOyS qsqu7~~`:c}1U*#n3ҡk,KlJ3blSXr%X] .: רTȬi*QH;Lhՠ.ݻLeSA) KFE1ePhsbU65A *MVrRJM,Qw4{S|c7(鲢!Xs2}<8Y#ݾW=#NٴZMf!'lMZu`X4{mMB"?8M<>0FBWX~ B,5tRUClD%X$+syg̳vq]sUs]_0N=刷 ,.uRbBuY'|+)aC#8C ҙܖR#kQ2Y& H؄!lvtazfZ ݬi~`9Fs<{rv sl,f!iZ&& ҉3Yv̛`s.~Q[2u}j[e]kȮ=W?s45&Q' %w8UȶpP{Ku¬Ǵ5: ksyۋ!b(a "k+i8@&Yn|ۗ3JY%*Iqg#G_X0E@UJ,^;w~o_폹x}ɧ?1qI`-6wNi<`0 Ḏz9W 9`I`upԞS֘uaȊY{:" p$UAM>?Z^sECsWuJHG4>4qS5|I$k}>0&InMJ`l&M`K-iGK44,P5=+BA+F(Y_cw ˢC\Ж f5|MWv[sCЋ[r{j bb.S̛ Q$F$RZP|PIH6iḩO?#l.˧V#XC.*$D**Y:& FCTw(yz첞t vVa w?A wMM+>(_[|8QWcQÒ=?Ws^Yx>uG>^"Gf-7YIMrZ'b>)Aᐠ3Cncn!I/nK%һ Mw[W||rE+ӌ4hOsm f!ʪ ,U&Y-cDJئ< )*[,7=^]6Qס<^9h_t~L8_|x%~%wݯ׿X!5-y7H]=٬HgiegU2]ccb]SnwCG *9YAJ@I)fI넅:rZiQ[llWRUcU1j]EQa@UAYCYQC)ٞL։"%Jʽ0@6*T5c,;Fx+D[ z'h^v_:FǤ2鱘1BdfyQEBZ3P !;X[Pjb/0c& +K(6y"B.+RcuCUBS٘>C?jgra%ؖOjLc>^F dkP'4L'Ku,%",sf[ܸ'lU-Ѩ4jGX3H@*A4@PU⦁olD"QfNmSKXFHr.ZlH^ Y%w;"'H hH($_[kM+p>^(H5/5(PjoH #Oh;lO#E;{c[cdRV v•: UO&idG*QL3ڢ PH am:-CB2lݧkwhcLk*SDŽd4\71v]; 16)B֐[4-`J3;w]ncPlj.k@eMh;A D =nSFᘓt\2ʅv!k8M pƖ1w(zFnPWr %Ca{9:P2)Xe=iuA26jen0Ԑ[i$V)ȏUTO!Vu%BѬ IQTVFK=VoM]O0w-}QRC~PtґN>:2ݍXmB"MB"C#sT#B5Ve%\g{_v٨-aͫjƓVbS9B5!HiqMh5Wm-8nl,;kS*fs^3Oy>1-݈L8*8yy-B|3D : ' =eNS]* c嘍Qxkz"0r#V"U[*S2Z\;gnLpz}dwrw%$:%ݳ9s՜;aniwWt9G68u>ȺY'NLqT =5&:]1䎁:c(ݱgDg?sp?*L䒊+DBxᜤAD(ۘ`S|"*HO5gqLvGwGAG><&˂o#"d~e2c]6)Lؘܰ:R'[_?4`#x戙?`0{7`yy4gAfxԟy14--eE[=nGA5D(aB;Ϻ/y%o?T3Rld;1ns~= I:ِ@¯[ K47éZuL0+Z'3D6D~R|R"H~nLFL>8m-PdX>8z><˯+bpj"\-c(1 )&IjUy}eR%bW!H5PP%T@D&C9(P! /TX#ʭTHF\h"%9mit5$~q4ew*N!Y^- e!nl*H>rI_ђ7XjJDIP;슃[~iӠQbK>%NAKDq^)JP2u"C"QV(AAר,҈u LQ%/1d0 )!nXEHGZwhAN-KkD D]S!I:e.S Z*&qۢ+HQnt'B).KS(A @Bh-T TRnXeDh[DSuNYheHI}8w\X 8T1T$rQ5G2*DUSPD5yY]!P5brBw I$j*Q!C(BhdCz8O?h0焪M9,.csD^+VCU2jPHe _m~d60~8(\ %88Muz)Uu#}O*Q+!dPÊq=-{M{l/"aXaZZ"\UdYfwT*꣔Ej!TTY6۪ITL-+D dr0ZŘUL IdD DrKDR*etIˇ{Bk.[Jr\QQ=v!m*zۡ,JGP#UA-*p*j"oꄢ62 ,FVLڑ:ª@*$ EAIr,TNB jCC76uhCj! > }A@V' vK}Ҡ.<k+&gKKM7rG,P`K!Qfčt΍txrΠ FP$\4DjLi>8;kąI)z?F|pREsɩ9f1^ Cbu$CFQkT`IQq-[ KrfYqTP{DM2ne˒ԓٖ5qj,98{@S-7f#Wl{6i-ڴ #qKlG)a5nuC`9׸e+fE[򉊽o<]agMiCڰ !µKB N;qy( TphluE'YS}f-a>xJ;D!LK>j]9ZFIhC`;(.l, Ѵ-)-eE[)+LS ҦD w3F b\. VEqT^ Y^9Q%(V⦆kf @MAE )QI1ɱ QF,N.C䲢դ+VjD V 5525i5شZMBl'rOOUw}) IDAT 30wr'S6%(%A#q<,̶@ݏ0{)Z?W%Vf+5зtY=VI,H481IbLթ{rtXK]-R*Mxe : ч1uέ5`״)5V#@5L-DS:!isgЂ P'kN9Ie#. ]"ZUݵKDu0qd~g914n9>I kv1\# =y<$|ܫq=msEZ6Wx;8чB|NJ f}ﮞUOPeOn\Q W ,WGTk}P9xҿ") iqr6i*;,rVqF7MAIlP7ҖbC` bP82LSbLmhӦR@q?G)<}/?<+#~)Џc;9) %2% 1lq|1ehqG75<=3~}Rcp4$CFVA )!릈;-xi}͇DNADA+K^e.3uerw=mkŠ_x^Z_:ygFLxi}'/-~*E4-w|Of&,KR%=yE\rZR*_/2x1͋+W|I䲷\v1˰jciT{ q'[S+hrUʙ|M'YR]ՔeW6*xlpuptn!#I 'iS 3OCY}X+UGFxZzstS钫_b@i(v{ lZp-H4I}p(maԭMT y[N7$Tey{cg$SQm2k'9E{)ZRCʸ @9ӨLXͻTvEXSģ+u DRP]vգi͔VJ2 as yF41՘ k̎kuêylnBPYxh@i V$Ha2PRؑO<莖8'/9H9Bd0a^ )MC`j UA!CWt;@V 5`SI|D:U,.. ip25ةOhcEgSI%(G%eY?fE9frL2ٝlP %⡁!EE3sU;\3 Lj"e`J6Dg= Q9Ϩ$S(@ RW rT7KrBjAp)H!eRi/9͐E En&H圬X/LIK(YT)p@;#E IaLT7闗9ؑ7=Sv{{QR/Ƌ wRԹO),""\L -RvC{Pt52iI&QTEa B 29j$٢(ȅ =.TeDWG%[ k5\#d D3tEg!R`֡?П1S&8ǛM)#YdȵxSa0 0 S3 =ȳ:b A( IGPT$FAliC DY&%3T9D9QLiYɬNbQD#qfdFIQ&2.) "aHsu56\*̩#^<{TqBL+# /P-(s X8c_{HL\h$4f񩌦a$J.RڄcOp$gUzyxQ#d-zG7W9|dٴ@ r"}^)(fJ^(s/anؖGřQkMh#1sO@^S]K}YEbD HE&^"'X9Tsf&زK-ӎM5JqM4fbƨVj%(v b9b1/;ڇof~ }1yg'O"o{_˿Fwa/^~$J]Ϗćx( ^?o E/ ;_c#O3^?[򭘆_ɷ7ϣ/s|}B?;'ďJ8!Y z'r<* .29bl{nS%;ۛ<quwX'ǔՄӨz[j#%rFN#ڙu#cZrPS8is,x a2QN2*` Mu LN-&K<Si#톈+8\$vTgɷ7q*@$/]lnߠak4m55VQNLw8U8WiD2} OaUg;g k)SJZ,6af*Y"2~aTp\s6J7S %-<1k%YI^eqĊtr,F9~nn.aoWk +|L}6ϙjUr]i LH1.]'#Jw=HM[8;Ɖl8Y8Ո%H҉LԐiV^vÑıDI$Yֺ,c 4Vf %@ǏlFQPqcZH;0NRD4b)R A~k||$3d @8]A/PKdJpy^ochO[iZ +uY~Ir4Hȑ2*(so<%hY#pѠC[:s(ӥ l1^ϫLN<ji&A(%(Em}D9ZYEPN,`Ήd:xKOUGMQT*ySlƘz4Y06kefZV!HM @"&a]El0S0Q[d[N5<44A7t x2|xK>X[ 4M|m: uyoگMãOh'??{׻yECc>hcdz!t]o޶ï|o˿Kc?zE/yB?;'L'hQ\xxX;?GyUMJ()V>[>[>7ٖ EVappKU;s%fKKyFyA$unU :Kdҩ2(Azj \ #q'Kӗ0͔IXu  X$J ѫ!i{r %BKJXI*`^]d)2o0(irH4֢HlVo$ %uBI' BIG8.ʘ5nNNר3zJ {Uv O})Nz7Y8_zUj uvN1[sh#8AiG<"e?Yc9?b#/=5*5WouYX XԔHUDH@QEͱjLu?&ˌ&X˕##g<=ZDM*#kC$'GA]YQvXM9ݦ/8J;K%<,\'IX"+FUάpda}lҸ4ORz5cDr)5R {b'bomwseޛP) Kd%I~( &#Y p̆}G׿:766i6=J9Y 2Y 2IC%čE,$%43F3bL8 C$ED!BԱK[\skvzId:,*sLRt#jEr }0K46F4JC'mz%zT#L|0-&L0:f>'֏8vsW)s?7/}>wgϜ'_j:w1~"_?~wԧ=c$ o{Dx!ojv~??c/ފT{_wMֿ-E|%~q:CR4-R=ڌ SBeFuQ o;P!0VY2w9psR#"E!! $fFbi6OsRxy<~RB%rp3_9to&UBbhE@^ * Dw՜bx[ttm)<a*ҞLiX!;@Zqb ŪZ4>:fmY_ݼWz:k]VwY]c8>:A87A! a3ژjt}24l;t---Z]c vbM:)x0UΎ{Zr<]ƏKeRM{l!{xx.7-[ E)?%,+gShgl].*wWuQeJ5AF'srJog]b׋V=H@bNCeVΓZ2GY0r$Sin:-sgػkv,Ĝv\?U ʶ˪^ ; zT&.;à^UVV#jsZߥb28H6ΑJ) |dE0yStG#=qbs`̡)ĖIL#gZg(^F3g0\1N6K7y0/-ށz&=IOi2ML*Mf6XmRڧT;.!#Ԋ1bNS(RJJ( [t_[įq%>Q׾g^p4EKeu7^oS?ޗ?)y~L?N mkK7|!'D\zw׮= EQ\z1?ۿOe;;xrKbABjjDA E|Pf>0VDW4ҡR0$M0F5 .nPihՎJҢF",뇘G(4\*'Suʜ}VZm#¨rS ܥVLhbHqdqS/V + fiJ<é$'S ɖL$D*O$D\OYˑ+rkёїlyA`63 .K][Zuq5)zlZxڀFm@HSFC;E8aEZVGnddK" "q5P&O%E#ic!MC#qUi}ԛDBa< i$Ni·cONB|@Ql]fV6l&KJ &{5t VF95yT G9:0 @( 6 2 J&%U Դ 9Ew"Tg ~dr|DxBiI!v;H". )XhG'"B' Bg+H. F1ɀ9`8(kԳ s̨2UF8B29 P8: :uh8hՐYզ0>3r)2gT3:ӰXk/C"ԧ̔(2J7pi4Đ5$euyAWd}OrJ.?<ǴQf,3mh}#a̲e8d8$sdՠ/Z81WnI?ISViMDRIeDR \5[SNA--$HjZ%ƫ۔9!NOSI|}.'5y{ޗ|'?_GoۿS@/??HVH]/{ ˿Q<_e~?>x,˸z?f0{'<|DzǶmy'gxq IDAT/{ ߼n>%'wpwӘI*BZhDA[Y-1Rfvav!=I2P@.sA" gt&816PTRY.EԵ 2ʘyʖէٞrWab4cVQ dV>DFljtMĄF(\җܔ65BhJu[!:iS!  YE" D& l5R0b̘u$g#xh$T^=ř޼Z.̭m2]¨/YexXc8zRZ?M4:};%±E2C"2XC_RrD=:c>[`FMUEaAK&(No+Xci8ʄVbYhpblBu6=sv6aKl'K$ Y(2 #uj%|jӒqkFtG)EG/75hӣM6=dEث3\c1^5*dfhVDUS'NȥsO(DL2}<ʱs^*hy:qƗ).*z#)fZ 䓄8&NIcJub"sݴC7Y xEbO[îQ*SSXA@տER-@ %J/&V~Io[v|9F!K Q3ɪiITCrs]VG,0f!{*-:ZGzEx$8L>@ E[}Y )DS<(X58g^utc:rZ:P0‘ʮ.k؄؄"6:1~G1hFԴ9%dSk\Ns#8E(LgQT)AhOB]]5p~2ziMd.>]* .]HɤB&ˋW"6o2;Q&۔:!p3>M$7-F:Q <6:jXh, 0% _XxMYV7፫0ahAÎWٟtx=ޗ_"F1O}=0 7{O}!kk?O>0HSz/ Nnm~ɩS'?){,s׹sO$|G<,}JaO?ϣR._"+_ߍ$IӘ݊ Af"҂qم w:WHr$$n m :s#'HsySXJXiӥ# )IsAh*Im4-K[MFRUk bJϩ&r_rP2Vz{:}a@Pѕ.g='/0LJ:6Mk$FąF(򇤦YbR=,a>`op4^,Mm@p 9Sxt%Ѝ1@HB)GKNgkk'wI:$]Fu'Qd{u- А)3K-Uݢ=ZR emU鐖9\ E"C6l0 mNz79uK7ܸwEbvg)) hj\E) uL1m(:2Y806 䍌dCaa3^W@-EEAZfJ9*Wr(GuhM4±+j+6ra37l<" B]'t\#U\ppFi&^P+EEQ34>( X{Ζi#JK#G40q*\Ԍ"(lLhD6j(h,1GW1倞ޡUFjJQMo!GPFjJL2HЈQI.Q$n;e;VWS6 Ob!xLq'fmI"VSK'XZj|Zt@gޣ:r) :RY)Ys٥T)eeFYQ68E:o9\ajB@PPxBȰbՍ Sގ<ɷ|7rGGЏϹ}ϟC YTJ)ww/տ\PWbyG,@2Sc0J-jdEN>UԘR-\¹,(39+kd5 y9A^JIHib(^8^fW9f#O)S*e}u֛K{N?cD(,Y4k@.`t ĢiLW,hm(!cBJdB247`C*׉4_6uAchs,U%JW2nקc,tK*ƈ z AbL|?Uz*5,O`?//^_`6=/=!wpw v2h1,Z3D!Q$P`(P%A[YԮĭ-OkϽ`[?\=Au!ÇMzE"䒄fPBA1rVq u.l?w?s69"k{N7$³:FHu \tkLUS%nG%ۯ-$.\oۤ]dz L\jC\/&]ѦKQd5 ahb4c@&/$2/1ozHue>E6J;L:$U ~Lڝrbopiz+4lv<=Y,#9F-@=Hмm' ~(j̈9R)Mifj- pAINbuf8KJ 1#'9z,% HG9QAId:fpQenVUTT>R˲%PtƒH8qoZ_f,r)NN<}َlizkW̑{׮M7eʒ!AW @u0[Ё InU{1"cּ E@Xv>̵rooAwrP \M:R@ j)bc?+t9O<|ڱzl>1Y,Cʅk{Y ("0Eyx-~g+BcMYSb1g ݐæ2}ҡuڧX`ƶKy niOr>&=z*֖iM CL"6\])?S Gw-9/ğH?h_7Fmxv:o 8˲/??ߊG??>?vLs}#?sU?yx?y䑿p_"B P|;apr?  7 9{73S>whE++dUh>IybBPѠc5S3u$G;.=&ƀ1`gF4IUKVlˈ];)=5Xppq [ Q0 xaIj }dKeCXnIoNIEA^:ŶXK!,BCTt-kMך75sMzPunC cI +b!;dy=@SVR8:4Q;hAIٚ3 rrl̈k㔹# )JH]C)AzP"d!☑8`"S ZmS.Ýf2%vdk&m30t^ n;9 o9nj zDr"aB6~q;su$3 wZEkMyl Y5 ,-;:ћCԘFeVeVCP' 6Sv[ST^aX5EbaF>uGVK$y۟]58*r3GD:(L+)2Z[X;6pB{Q6ﰋ}&U.'*(-4RSBGY`)*&]Z׮c93u͹3E =ï C5&̪ m\2Pn1ogCe \%4fF$[|K#utNIp.KG# '.P |jddq^)iFZ{,bgoě ocƦJjܦ\?C!Zrb찂h4FcԆA%L!ykCmgtY6VD4nk Y%]m4 @ntMjߠ $:Jh ݭؕ&E%; V 2 +sWgbCHmduXfAm glv&>E곽an8GyGW.qk.am j_n8$lpit͓]r%WlNJٽdgJ+.0<0dPehOM c6a&I!uMFGv`ޠS`ᲭB1o!=ȉ4ACB^.>! .hl [OK /pjkIMICFI|5bC  zWa<1B&:uiм}.osL>q~X0Vj@!l|}Gi,fBJ %ф킢K7g|c>d;0vnMmdn<0aopsP ԚP ^w ;P] }jdk̢7 k# f0׺,6]p/${"5̺4HvӸN֎K #4U>l_ }AIc!vS6y#2MӜqȈq+2F∑8bR; W lܘ[3XDMg"[Jw 4$aoǠwCkhM"bF#{gAD~R C>iA<\1 9.QaB7<=yGyo9?\ᷮیƆ46Z1I+&褜e􎖜a{?ג|0pHb+~/ӿۙFB["ؼT̿S6SxXxإzaRjc+˼(eߢꛔ}wsvۆoO(O~%^!j^ȗ . DG!$I}'__ OG gdy`3msĢLmd?sLPyǡl)[ᘏoy~fxU[e$˽}ηǼ_kB(tT:Li v0g$)t<['ɜbacv+Cu+6igih6f4KyAaz4(bm͋ηR *15 MYAY_VTHO-VM4qs|tᜮsG5'ogӞ&Rifԡo|}_?\ȷ\x<| NL>w?A: 8?pҳD3ЛI0@n9G֘*<9a>d0 {TI_81o"C~YHPm4H[6Y쐶6nĻ3o2 NP1u7YtڤFvbZ(_cܪCveΘo /a,+eloK-['v NKŘD]7><`x~2G >gQmFtbi^'*$ufs&0ءcǹGyGk+lW#4ј($^ N',VƣMhw8@A],'7WNHڧ4 #փ}ϑHG}oP_4 X(XȇUX[wo#] &d6 &f+.]kMƸk,0( J-V6wǼyQ4&R(t3ǵR0w ~;D. IyTΤl\5gb[@h@̢M{ Ѡ Ul\t eSORtjFfiVd:`wy*Ɍt\vh0]jh*@8Sk=O *H0WQ5 MmY$-0K *R]H#n-$^Ha:{L;pWCw̙M*d,E[q̭8b!cqV4Ҥ9Q&tlc^.v )ew;As4Nk2jq'v)tZeMcԊ#&搬@Pߥ-=랝QV&Ʈ ζ/I> .=zU#vP4^kTA-M,m,9 2LVZL&'ŌJ4P:KZ\|XS+8W8YFsXl.h5p)ݮ`g\')CΎN:gO9H9^8LjPA%#"2)B"2Y[2{uw6J*#<4%:bH Qz!9pitŧ7t$sǢ꠭e7 ^"m i ;`kGaJ`[g)1; WÔV1̹,)Vn6!ѺdIVh$81vv8n8Hp6zK@?uXzGy䑿` ?whL cB+\`:b;ޑ \> fus[J:`YL 墿#LֳAhA+^ҎQea);x,MY4!Ċ06]$ci >os:G 3}6"p$l0t‘[a%mV SVzAnisΘZNY>̧5~xABQ[}U8y_EWAJd92oyaDzIϙRIK1y[>9Ve]kK|=!'6+iUzJ2MkТ=h0;%wwxCFN5/4j4*4U?|7TB2R i:OSsz_#xO[D8gV6e?qLC߆6] )OCxvHpeX`Y(.a\M9ErY=% 4%LJ1[!8l? ~myGy䯑;8OCIm*Y! WS.oruCqlvv${팹Skj_qNV\7\eE's_rֺ$n>z•zBe[[~$I rJ  8$ϝsBܾ?fp,6 2i*P&ІQ&iPh$nנo$,EnFR7%=nY+"gfInccn EZk.|DfҠȑ3]ʷNaM??c@c%Ibbcl:΂_G+[«{S>?% a(< n6Ŏ{iǼ}x{ʷ\ hԮ @lp܂.KmE40>+sr? D "Q.{~d|44:nY'd7Ek^I2͏[; d w@7 ۫e׬h=R?eE{\L1G5#B |&Gٞr=f~挳抖SK>ay!9d4tRh5iI^WD nF.Y>0܇ӹXƉ S`?XS!ovD[|'S'!y.&rS h66{=Qh } oh<f$G{L0[iػ޽暳G8jcƎƤOR4OUEêI`HϜ3gt9]sNtr7ghd{k I]BsÑyGA\9'\M]swC7,9\qڻCY\e|u9f`%FP7ללԌCtXx,X"K]nEI\jiPIvɪ he_[aŰ?u!l[ݖB6v Y-VIvvY\̢~aj/[w~yL_<q|ċrp0&whl-3Cp`I-lAނG#<;l*,%Jǿss{w_W?isbtn2_v(.C.|RQ|ZșoH*ZOXuIãt,3l;}GsG=0aCNd[ Jk?&4F%v~ڃWMgjԁ ~@ŵ*n4Oy>`V̡r_/0rRcq6{A kD>0҇\܎YU1/\ >Ek(~Er}p"f<;5b_2!1cu@*'tVZҶlRYX^EX[PO'>9{T;Q +DG]?|_7&֔S|7s'Sv;Dظ;#%5m@s Eby.WG'|xv7',θ<-J>o%W ҇F`OM D ̀0*5?v6gm 0d̐{іCwBsdcTWO̜54uRblh5s'[D@3z|֓hoX]Wgo'pV3Ƥ4}t(-msZuFyT+%k;ܷS=Gqn9lf1o_}ěcF$o;# (h c1Z,{,w]W=V/tw| Vůp',Qr,/O1_-UwOXԼ 8- ã4%ihN*=g'oGLdOWK4ZP&WrΠ/S<]^}dȷDR@h)`iQ7{7)Opp7[[?--̢QN7X8iA݅e yGy䯏[XzIh M"fFVHG[` ЄƑHMo \,2ӑu"J}˗\#~zsh}n췌MC3plpǝ8s>2 }oSԖI+P4INq>VF} Z&x@""vt;w9#^4Ԥ W6ç 0 E;v;Dc ̚p]nXlQ)Fq^q I§<o錗hL+\ 7т' 5'77״%A&|+ĮfᵱͱdUWX~P!snǡ'STOK;l*BkC_TFVۘi1ErNy%?z -RhE!,&Y%Srɥˑ|?D)tu*aR6rX9-fsǼc6uxԴGq5I^sԾ! -œAgO :n(pUU3c}MyᰝcVH`'^e(_ f,\Sb}Yd젍%Ө7enRzu uԕ)#DkhD0}s.s=f@9c@e* Y1S15{` /0qv`˛.,H,Z HdӔ:MG̟F3 IDAT2/=Jak$ǜp䚿O,Ƹ1kXzVD߳9;W׸~6At%p$iOXk-zgIyiv?OSyc~e}s 7[dM[iklP5Yc5FƳZ=&mՓb N\ v١'Dl@H ː&!@H(cR”W޻ӹg}zܽ{S'4pɠVMOw^k}He9lV;C&ř­,UB$WEr V8isP0aC>+_x]f7903km.qTDUY5Bj„wIg5-©{T%UkA#q/EgؓVbCk)AEgiW'GC*hj攊)tإf pȑʂ\X5@ 3}RCJ}8T/ş̃& ( 1cXFaiVNYCOb΂fuܘ YMPSXafbB<5fר&@O9;1u^Y:", @;I maJ&@5^/mWlt;fW:‘$D3 Mg}YYϐ IO !(V 2U&MܲXnc[9rJ"jsF.H$3oiWYA[D ܤ:[r$Oq$se5mҠKjxr\ 7 ieDHAD\ :`Cdh >3jCwP 5J<ԈT=~>F1^&hRDWXr`E lZ9%{cUPVi)c>mgN~J)g/ޞlɄWt-Ω1nqQ$TH$PPi KaC_ӾwvBw:bگ3יV̫[l#sPՉ5bi6Gw7ӹ koTPqD5D*2,}E]α=#7^ș7 =ʘm^)W)c`t988i\[Ǿ`=bBʜLVH*QlJ"y]$kĆJdDTxu>ƽuSa$BY%H .JGÕ =d!(K[ C:%mC{HL:_=%\ᇮ~p@|idg;1;\ShS4 &*i&2 dgO@w#v : vJUФQ/騗d!\<-)NKQO`Вwdp]gd]kDaYrY'413ֲ͏w~47~?97:>Gx+^}q=il.=_3>ˏ?j{y/?|,//{_Zg㧎{Ͽv>b_Wn66:/#<#,sj# %RT D&; 8ÐcKu+$H)Rgu\WH*i* E)$2)"ÒŢY * RLSDOH)1C0-5/hc 9@$B2SIf:)833vT* HTg]\)Q-Rj0,o0OAR@39["o:(rܓ)‘1 U$ Q PH`j%攒) ZY|dp fTj2yU"\# ܼF)\PvԌVR%]WTܲ(8#VDi)z) Q&y)()ĔT9ARrJdי VUn7]*o`fP 8ʒJ̩&.Uɣ9q>vAw:"T ΨA2&5i/2:3/䒄,ib*hBUXQ/cahJfhFRKA-)E1m2d<*Li2-$VE/[ t1A,rV.Flg1쯎[`-VSi3,g*DBN! ĥJuj3}H5 )+#F<L9&-@P1T1 eiEPaF< FbG+jKk6<8%/%| TU Y@RLGsT-$đܨ3}"b%x^ڌV'[ K*x埳㟲ISN\xQib*FSI41!> ٺP,L'MeX&dOY@ UN+[:'g+8,QHɑѐ'K,*z5_bgK,5B֟ :! ,GIrĤe i4BDP N4ҹA 3[gv ɧ!hSN4p/EМist *aPe0UaVi TA^eHFU]>x_&ۿ_7?o?~S%owmxgS̻~ 5|/,}>8O['g#~x;o{[ ,_w|⛿WQy;?o=/^PGxG/X]e6Ecz}&b$giFlԓ9}@}kN3Gl*Ti iE%5CE'Ia8W8ѶXQkΩ4bf[udaҔ&lc }kgJ$,U&>(خvzv눝qYrY2vYT9Yi1NsIтU')eܠ­uneCӼ2LpP6v;䲄-X|*-C\ *u_f(LkMF),S4)c`u֢_UjT FwE-PY6 bTвS3S 3}_`p(MN0Հ>izxqMhĉF,ˎkrfVZCim38o3\{Lfmle'lg c̡#.ٸqyuSDͥE\[Tq .5ED!CzhY.ӌg8~="M'uD#(t"BM0C\~BWrys~7t#@' ܤ0%1iƖx@Ifa'T)mwzZ1{1i,sl20Sj̤`I wV7Fsz9X0з}3qNA䲄 [glvJGIl(eTDiZF%oKlRZ'ܰƑQsP✆>g_?v4E( lQw̖H*uyC0 #Z)uw⦄:ZGl3DAnJ,* 4rtli 00E0Eb +^ӇTW pP)P3#G\]&.ٖؖq%ɐ :i:y$SP.֖dIf|EŢ6X5֔~-] QULPHw^EHaaIq:+{s6.vN% ^a  0Վ1m¼#sfۄYISyO$o]^oQ~WMD k=/Q:<|޿4t!MUy1MS~]~_c8O|۷W @7sŷ?a /}O^NGx?%~;_.ŇP0*qXOKHiW'*Fum?]JI$RQ%15ʼryp˧n1_6Y:u<1)+s;1U{ΩT+JK:rS Xl1ZLh16Gg{K7?˺Bmur׹bb 1."3%-rRûV>Ƙ[OqS]>rɩ`1Nظ:d]Y>]lcg#x%W;\m }laTo1L6ӴIØӬiؘ#E@T6`AP1W̫5f#ñ+eP9y'aEnE$Z6$rSwa)P%RTRRk¦lvcu(aa *@ҬpwhF 5r4Qɑ]ՏGx>\̊:G&:7!"v%6>S]2 .F$ $ "a!\D@"L'jaVL6y RF\IHM!ٕ-Qe)RõE,*cw3kՊQjƴ1y)")F#Q;)! SS-,\]ݣXuGdL), aWMʖPݞ[V/WXQA19F)c$&/%R]#idjFe!nxb'[.x)(C^@Cl$TKz>,5d8Ӎ:K*G%Pӌ3qUtc)r"0+iSs(}9 H)j)v0#Q9" X'I LS("%) ai"QRRId˴ha9Ih1"YE#daR,ԟnϹT"_Zb,b"iQTXҴflt.ٮrվ|Ae;5E EcZb*K=m:J#2O-/ؓ7sj6WW?g/Xw|V7>ͽYea#("sΑN1V Y,2Y"2a6SIdI&+2*Y[!&Sf"ESbjsGI~*375\6\6ڌ-ܤ2&E>%Ws jJD/)[9Yem*DLѺ) QB$g1˻9 `(&ٰNS@Jd1Ec d-CD(RĬ@@%;2aakyY)#gC }lHjAb'51-;8vb3:++ dW{Hؐ&bJT[CnvR3\keza$,hrq;Djb:> Zl`6 ^5gZIi:srP՜5JqĥfwLB1jpot1`B%X~WdZnf6e3+_) *₊R]L=@F=\ ``\ 13.2Z"%rYS' NJJCT9YO44,0uM&s,n0kda !mB,%Xq3hC̩ 39d?9r% %>UaN{53)49b!nQ7l$F[0U &b+P gzVPS]ZʘMT1 H0( P5 W8PɂS6 .C*ݏ8^21݇aKQOm s%r!n2tDӭh#F2QY\N3E)߸`}ҫ2_ՙM^GgLS̨r{ a #j搪WY+,'F*KnfyWe>B+3X+LHI[cn"PHTm$fXPRH OqrV pydQ(ndݒD-YI<'Mp+T)"]aQY^p/k*e Ja9+KEXĜ:y.Qe}g,IӔk?oz.DQk7>s}'D~&_oʕ.I7o~+W>G^|0 u}׿Wi?o?g}wW׾_xGx?[|o^D$י[̌:^[ ֪l.0"Yu;{sd+tD>Zy >X/ ,OF) ںt**J֒ɯJD+1Ýg#t=081" @qu@0 /X.=tqy iZRN#`H ->Z$YЄCxH6b,MV Oa >)&\pCu~ QĔ2v ׯPO\)ѽ9b5IbC!h31C[GG$W"KgL:ŪH8c""J)!MmJSf4)E׸_cmQ}z君C'%4psJmmD!Ie;;D;D=SE2TfTЈ)`pD@0}@#Y*XI6-tϠ%Y@f49 K>HՐ2n]YΑ,<"~wܻw~k,K7w1yYt/[?)yٟ{? |yO|ygⓅ~wO~Ͽ˲oF3?KN/꜎^UWjV!r"PN*ƅG$Ktjuʙ,1|*\P-p^@W3׉( R(LIe!S"eWXn7n" "G(2#Gh攑HKLD$j,U]2cFRwOnxt |zw<" Y@B8T)eQ2T, D@V24)B-CoGhf"9)2q'8p:(Z%zTh"$P ]%M\u. ^?z/r8(hL+ˆW!]bQct;ۧtǔ"kҁT`9q&i=T/ь!&Havxh4(du[HURe"5Sa")SwD~$b+e$m D 2y_R>SPJP0Pp}HBnqD/1 TLj-T|(!2a@y) x r$ Me J&~pHli6J0V-\%<=YMǸ`C`C[# |p XeH:.xQ^4^ϝB , `h"SEC^}W ^~`YX3=ʤ;1,Zk_d0̽7zBzrPc(1 }A6ԸG_ī_^g\ݿ_z@( jL 6ᒆ6q,Ă4;iT(/3YB%B"P'g1(mJ^.(e4Vp:P,4Z63aD6q5Nm$v#zԥUɥ’ . VQ56o|G~=\~|jۧ^ʕ7o^߳~6_eʿ:O<#v3_x?S?O?ΣGsK_?[_Eܿ|C?3?8\^_5sϟ.Y0O1C&8O_`הMydS>LJR<̼v/9~3@';s}[#]kpd?IΒ^`nBi3HWR+I󢡙IԕA\44 6.(]3bZC^4glP9N#NV WwnXBn1i!UgȮ3bљpՙsgI IKw*ԉ  &ܢ+K* ̷ 5Oly>Mmց TWCv 1%G:kcQd7.!n; yG2ppi{m֤Ns:% \r!\CwiBV1E^쭀 <{Jxrٞ3'8ӮJب.vRh~Ȳ!-8 7yyo'mn}FK4 sǣlFMBZ lQ<&z@*p]IzGTd%+B2+#}>mfa%j _0,MyHALt MW1 #hЇ+4_qwd7<~ ;+9?DCoI__},{ݥ9P1ʧ_7hs% 5CТ>7hC*ԞGT4=>%s"nc %Du_(i1ј:n \9DU9buXw'#6qD8|0G w}1;˴Rt%_.sΒkŞ`$`)۔'U Ԡ9䓐meai[].M2e㥺 2֬9_!NA{p; +H[JY< ri#Eg<[׫SEeʄu(fI)@Q%5F]o 9^g'c6uL! %Ö@K zqqD-;ڎ"8$1!>)O 9~G/s_&{oK+(~>KWǾ#?OG?˳{kGٟ?ߍ/\_Os?ubޡLmʗlo<]3 %l0_q:W`X5ư!4$5c.h)Kfb3)mUcMbT"P';[BΪxo&Πښ쳀uc](?H㔾TL9$f> G˨TNY+fcuuueSuYrc!'h 4 M"9Mx3yN\- )b`A5׌;ͮsKϭ>G}DwF!RjNoߔ& "kFHAP =͖t;Oo=8j])q +Xz]JbK-1bd z)~[cPZEϦ%!`Ƅ5ٜ쎸)JpJ%k:E`bn#Rӥ1uJ"\ ;gϹ^Q/mtA %f;h罏=xI7^ư\g\S.SiH2͊0fKzW9ήH͂yk2bNWa(DFQ.27s'3&7X=^ p)m4lx 1hHlӎ6NdDX=D ۻ]֥jiiBHñF+Q 4Vv#rrc{Fmb|Kq'(z+ւgRcgMo^i1;R(Mx*e L*w/O_7ӿWD_M<+W\KK.KgmJ (Jb5FBw+l+#bmCO[/Cљ"AC"n%@дo#~ӈR;9K|^^g4UN4m8UB_1Zq2a{L\7ئoߢ亳fE$c)̱DnԠ+c40#Lk7?rȋ'|+ȍjȍhjvH6<ן` f^ KA[k];(:!'9׼.uF\j'܈ G+L鷐6kÍ>bOqč&$uLYAukF7h,`ǫC 38lj2`_&8eQ.~AO/șёS?ʈ 5]*uH29Ů ]1&7 t='ĒXq6D0)4|"< lG=}AGys=kU :n0٨eb2$њxCUS&LL)QЖ|Gm 9cGIS#k]JbHŐŘ>BxwJ%QI/,YY:2(JX48'L.:qyE:t+8ɠp;> qh8ZПpSǾ ȶ>ƥ:k㳷ve&,6Mf&Ы7jO%yq{hsAI]3|@*#>֟}Lj+:j+,}It͑y˨^-$zHԦI9dxhA6*!Sl"dmf%a[R`Ɋ#3 ^Su-rӢm%Vb56uc ͡9ϰ^p^_R+=P8flÐӑAU١-xbu2:#7\q-v"F 6ɷ>Yj*q%\q_SbUroI:;melc Ǣ,4W G뀘@Z~_zT +`aB<ۿAa!ÿxUޟJrwޠ DuAؐiJMcLk)c+t$[bZ4JlvQNJeY0򖺰dvGs=s?|v] R*f (:n\>i듵>emWt dA?g/SmWo}K`Jgv*W ,.'SNpX2'a-\6KK5$w1l->%{B4lX{hClkFwDzN9犭YATBOK:')ylp}4&-a5Ubģ V2lO; hQqHbh[k@&*qes60VdΣ<<%A@W3^C6Q2ΘfH]6?b!#a9F4ծG|atvb0`/lWQIiWCqR=y@e8B6ݚ\̾ܽ!rsʥy8)ɍq̇Y}n;K<;!vD-Ɏs FQM+rSИ&}ZV щQ[bCbfhF&lGfMfָnл24$f.H&!`^HZ$|Y eJ]1Ewb5Jz…qux/Б1/u;& jdKe=>vj᰻͏xY: ݄³)< ̹fԽ#h<{NgϡQIȃ,by@;CtXj=vO j J^,yZt-G3rkF\L]A+1&:笐 ӥRi4&=vПTd z"\GS4AF9k ǃB;!MnM5wX2}}FyPX[pF9$?Y)NPL*lnc#jAiy[8Ct`-iۿG&>fmb-E B{S#Ƥ1Su4DuB^n#kBgl\"b6?jyto{5~~sO3nSt,O&McQK!`C_yAlo-%ַLj}oe/5{?ZsOb*\0 F<k%w3FΊ#qGt8-'߿||ÙuGlm)+o5.`o!M`eX=vM%1"u~%;p!og`+Z[lhmxo=8iFwNp)wg<>_2yv":.mU)SS̓F!I-CuȞ1fM1w)^/*zPb9֨yl}WJg+s~;נ [գ|f vPbJS!`FfN-s{,Yc-Qh!K#c5T]q;#nGCWsjjkd 93,3ѐE3d qf3o$bc%qK:%>svzMujQm2 ,C"W;+̦)vM ZkS91{.Kj&>χs|$qEڢ1i^Y [!kRh'Y|2Wy@e8a7y7&R8d"F kGZH9hV%lX^[H L#fb̭3*/]@,w7uьR&c:hG\\W,ɾ&:·r"B-d숸FHKH]7&C- G] {CbD^Cw:2̸ flxܾ7uu#]? =oN&t3<=fnƘ)p8HM_ THDKtb&N*`tW$K9fLXBAk.7VיkZ]g^^bl2E_=s=33k\''̠!BD BM(-[KИ:i.fg.>ʣ-5L4+4"+2)2 YԍEE_[&Q9qZmNVDIǔqDsg5Gw<9fp|scZ/]fTafϠLj9̛rj,QaGTMQ9TEX.C;e 90LJaa>VEl(l DZ3)-Н iНip~D S v 'w ZȊ>rBn;mh5` 4uM6:L]w٭cE  ̷feB3$-PJU Q* Y7 z]DY8&E-^Ŕ c1c$4J2eC[-&$Ö> RSy~ :#2ᰬz,.CZ{tKy;|'"cnՔIv.eYi\4hDh-BSAKxG)523rEX#ly= .%+VK,Edw m@ TF r4qHxUNTsf,>nuv+,v*B='W;NĔi;R'ik R{&(Ǡ1õ% a  R Jĭ37ZC)$V5Q9FՀA8oڊJX]gs*4?a ] f>I""b_(1 I@j:9q֔+m sHM|huVk9a+LFo),#}JᐵAo|G9wf),d{o3a]R޷@-Hй t ͷf3ԑ]j\C:*(.ksµzb4t%Ư{{ 583P&g MBw(Zu[%`%4Rcj {VchEW#'xwp&ZGa=94:vIϝCA![+bG]ae%f>"r\-"D$1sXat*LF >ci.c~aIPR&/e RmxNNSd\$IK%#buJAcmCX%!l-1UhT mkjE&}q#Ngk>ٯvkݗC{N@3vir49&9bKA=,~03:\-ϸ\q)e|Ce~mV&H̐$!yQ6)[MZBX%7{p*dWS:*7K>|EőL֎Y&}^]?C^_=.oCRR&WbkKIW'yȎ+VšBq Gynyl"Rp9Yp4<&z>ʉ5qI೿蠔5LPO%Mܛ BNjSK~P$.y"RG. ڥ`׉x=DieǯSZѷ?ĵ2&u4om9H<|kU^rιu&4$N1 a&[6(8s=sgϬ'|_Ik DmhP{W{)VjdJ Vڷڇ_<'̾o'GXaE?\Pk :wMa|RW>NQ` 7K钞R)fSc/)VqvĞ%}Gj vt̫7xCiQ&i ${E>rڂrR޸.80 îp92w1ZO;,"VfdtW|xYތ*rl&;f.ng.W)yjj8`{! >Tȥ @qC{dWE]֊ڦ4QIWzqLy=G F-S5VQ``9Do1)ra!oL/$/D6urіbh Ti{{5~VXѹآ44IcT҆Zm4dI![S%s#s _fZZڷI ~H ĝ:t[ttc=|UA @ [ -etr7p}{$z{6EH6R,*cT6zr?dYY0{5txGq EQB%a%q k0: R6w7cŨ1䖑1cd߲i;1&CfК:mRJ|a( '1EɠcX2n5B--!1*qݴî[ǔ.J!Ġ8FN` C4BgKZ #?]h4Ǟ+C 6LjISH@kCZԆlJj :9mqHD1gx>o r)V#> ϕuʪQ!u xj?#RvL̴Y.i9uAYB+jjZ.8UI|b "EhTP Π+ `UHhr =?39zcX=縚"[(ED\gJzG0z c\ZK1 c),EgiŸY`n-a:nZ4: #@clivjΎMX]HCcV'4 olFT"8 +ʱɱX0|{1:Y.%KFheM(5b`w`3(ZL)"RH`Yt-FŜ4iK4)SMoЭ 74Z㡇[z_K čI혠H D 9AIPb;! P ISx`$<0_rdް<Ǿ X]VZn:]7݃ުae$8ӨOw=s=|f9i6rm&_8X;PTK%uxÛ5 /CV$fO\5-$nD`Rt+0A[~8n)! d78D kiQ7&u}hZRi6;#Ɣ IDAT8!;r/ѷJUBp6hd 1F?%mT&BMCA]h`$2֧Ei |=%A8 5aͣkMytPrl9(vb)]U!*`\y~q#8CwcVwCD(U-o'G)>5>-iq9]0T\T\\TH*JKP 3&cd"ztf܋eS5e3s,S̏1(sw0ȇWY0R AZw}jk4sF(m3J:WCo ΐ~mzyIj$-t tlX;9f1^A^% 錞2`MS#(TFYˬ [gyQyaEVYb.,[49\l \- &zI3 nK{ԘZ>3$IQYBuƥϤl2)}Ħ5{Beڗin aʠMif}AA*'VA3LR 4sEEE\ɷ]O6sROp&?Fb\!%r O kS"- $ D^qR^`:CRb`[k.qހus An*T-V)(䓏dw493T3TDsKT7gKT-\1*LM&YI2 .#M|'l1Ò!rB4^p |5qk\޴oz_F6t䮍uUâS=XtʠTrb1cfy~Qc^>~ 1q;LD6W1S\DsZώra۸Dmi NKϥZ%d)(b0Yu.'&9&FFdJKOZ (yGA)"Հ LK`AT oTum Nopr\긇tS^N-ĴI;Iip|,/?G66\HG#AX&I+4Jqnx5NjXK& ._[#xAtd6\9bZ5* )HtBs&6>^_ro>/yu~Wϰ7/mH:6vu+6c&]gHg6!;QmruI}2 eY$6Q&lNI;r/{gO\g&G7s|JۘKH2iC\=BJu5{=Y^2Y-aj0]k7$h8Mj;csz؂A 4 CcZ9mrR`Yo$v՚@25+&c o746grxddiƪdK&1~{1\;fQX<e!ad,g.WN_Z fa} L9l5m8.ऄ"sJ 7iߘ XV.cv0ٷ;L&tјP&S4\k&P- UؔȶjLRU+W;r;C1 CA\5sO V ;OC8G:icVώ"䋄h!첼~׸5q7-޴o~2?݆c  +T ,?CR^cs画kbĪPqM^j{aʔprod$.j&j,u9w7yk~nd.׸|m>J)e(%P^ǖWu2Kfے#38xy3-Yn SB %Ѣ*KjO4r,@!$Uw3"&)TRp[= SuYqмŁvL]{_^p=.Vq4))q?̩MY ʻHGPX :K8 :w}w{CŰ]&3: y{&wG<4 W ήhn.1p1.o0E$ Y嬎(~#7yDũicjLn8HT sj6 B2z<]|({< W*$qD&.Sv}6u3v&Ij.1Hu丹+}^0L.' D CD9ݺ-?7S:ʱ$=6MDhvjJ߽ n|/sq|J*=9L}|Gm7uiJTRHdT*a|5GZ&C;{SCޱ2>3V<{^` 5c)ՉmgS yC w@lTHJ[Pj yRT*T)+R񴀭 ϵ^yZ̰LPPQpg78 Fk+Q}$ԣ_<#=O,$ԋ׸5q޴[iB"JP%3J-GWRR!QȦD#ɧ. P]@V"jbI\R ϼhDksܤzꀞ6^&T499XmTu ZΈ1!*-9%: 4QATE\i#UB[ُgFC/9 MgF1~>bX|"% =NK]Ǽh NWlGos/ͤ+^߸ǸljL eDYrL=& #|3lpyAihfb٢EZYNN>>W7t7;N;}RiRib 㥳ay1j@hLgjiȍeKcFMZҭFl3nT't1%Qa-NƭBZ~2frBIfY,:cMlظDԪzR!s%EW ")M8J͐BAQ*'M8+7I*j!8=ADzf(-Rٲ.b1-֘M[( 8)X.ڐ/QVU4JRR:U2,q9 X`URTȖ&с,iFj8msD.G1t.K,\DEĬRd c 8iFz!jL@E1̺Zγ]4Y_:EM#nMqG84<4_02RdS=R`_9=[\W:T jfN됥uNh3kC:rHb5B,z\q#P>YImk&FH8-, k\ŷxҬOiǴc1`wpS ˩Y^' qc43fJt1yL %,d!S*E*Բ.VɺcGxo h:sxn5xlEʆ)|F15uBX(CfAFHB)J(u2i"jNۉQ5L(998]WL'+@ UP Ľ\>$kb9Eu" X ~5qk\o yn[M4U$JBr[zb<2сBxA m c%&-zM 8ms kYTmQ qWq-143|sκyBkS:i 4b( FASV2G )F.5LLKѡPZHAzMdF(CAu Eιark}Ŭdva3ݳ8eI$[25fXǑJ=W5=n.h+c}5fvsNG-uŁF63X |JO#(VExii;9KoCo팴ޡѬf4V(ܮqe qUjew9n#=hmhmNh#jz@-Y/ eAxx[Mr~i`N\K8q9vkr3qʈf0?3:Adٮ30M^_bk)V 4M)(\T>gϰ\=iegm3 N߼d~F㜭9rslpXg_t ҆AꛫU1(JAgx5, ƳY ? *D +^ 7An63v}v=,g fŁ~;@srl3Bl" 6'kyMυ ΀Ԣg4)k3m9Mwy}z&M}JXdǧVhXqtLwX+^l2imLhOVkkEE$T]|w>Xriqvrl4Glh 1%zJW*x3'r"Xk\y<'oV9$ | G+PkJMKvH^gHVkvcblMHMUU%XU4%yf(!nIYл9enĽQglЕN}]}[794TKt?c/ɤ *:44;`-pŕ̄Di1M.&gMN^org8q -I* ' D.K6/YsX:.㡤!zDrHA)U. [[?k]޺Gk>{q)%o"BO|3{3fG?_rM_wg'_vl+K^oW_|7i4||7}=qk|q~6UfUH1koLwEOf/P)JxD Vbz%wGx< ]WfkulD`sW# zz6/JJicC:ΐ^2қ 1ObȴƓK=)KB}\zN~vNe$ksai4*UŭBjUD= !L)ʪT%-y4(+(+ bb`uQ)yڇwlu/ỵ&pEYn?n1WLU3S}yaaM9W)*D[ *[U򁁈$VIa'7S% ._y^Ȟ#]|2\{ɦy̖v̦8*2h%T/~D]2_&>hq,{KQ[& ecbZ nu;79Hn2900Tȥ%um}v[yC};_()ɇNIuL7c;y wyl摺T6h$ y>̑ Au_L (c[?'\8Z2KRpAso9f23|eAE-TrF_bU F^ PBAS\ӻ5ajro2R}]2٘:Oi8.HJ2HRX~H>1[RHLjSn&Ny DCmӤ'~a:u,{:MeU}J\mn]r7o3U|&Di2-Tcʱ@*翪|Qnw^?oF4<j%>xwnK|/?g_?|w__O#g|{?2?mމ(>ȟV??f66oz=qk|q~ݵ+nAXj b5bFƨ 9ILc k0)83VQ8>sjqu`1d)WyàUYu&DU-MԂ [+׹E%+95faUD~ЯR톴)Z0txem\"k /c6xi$%2<+1ch$MbX$Ae+ZPW$el0|U1PzifȨjq,bfjAͣ6V[ uADzDh;C+䮆4AWs,RLX5b%@S"*8o3w-M-\I ]fTMfU ƨZN%PUR 6K<'9q>&J<#H`dA% a,ڜmJx#>=ˁqcQI[—X3SdI'gjQiJLa%ES,5F%U-V3:(ZEÚUpmQr- Ey|  =͉tfqL[gPtΚiOP^w@ȧO"u}ɖw)o( }^ҞcLzEϸBQQ uR8g@otľMDM՘MZ K,!N:LWVyӣTFF5r݌bGco [*Y96C{*fQJAUrZΘۍ=n"E TBiID,P`XSC];9SILL&:MFVk }s 3ŕR I:0H&I"$6q&uK0X \4ẅ́9fQՂM C 4T& { TE%@AA%CaAΔor`q`qg=j &g: pe#@O`Le` mT~Bk$e8ط?0xo3+k|7~xmoo?n.c?SO>~0?gLU_pv^?O4?߷?#5q/4޴os__Զ8osZbZkb:1!v_I}IR! JZt fe$on1Txftl  K IDATXWS=Cl5"\L}M&MdPb`ĒV+]aXZ^qח1ESH. IG}H%\һwA)Tro1k\&W;>yGV"A@U_ ~%d'HNQ0f^~zEVglyG(],.eI3Y6qdDGY!Q%Y+ɸj1.[$ .{bA,B!{,4p*,ǜe(iGt+f:UGl+`Ѭ)U:'UN6dvf5g!224TPrx,ju̍[Q edMqR=J0Pv]e{5q(vtdM!vu:NN*u>MTKA\;rCmzx۳3T,;u; i`+!L..zeٙ`BlNhx3\Ͱ ` pT0 ThXHנ2u*U#fYyʡs׹=.5f]FsnX:O_k0~A=#P50M}FG>}d4}ҾXK9^5[5 f,1BGjDWϰ/om:INpb C<0 A r%T 8 a1w `𼀸X.k2QQޮ||/(g& #6vTU<~z̃/* k?ďտBQ.5q/86/)Ϣ(9`)CCG d@\%y8BCN|d.NdKD '-gDߓ7]TeHN>\RgFC`F8L?b1(D(DD7ls¦6acmB[`G!eu{W.;vz?j[pxv|ME ّJ~"E=дQwg$5ϫ/ +ށ-Eb٩7{1^ 3ϟ2bhdaMQ7<6KB%\\i( nzZKnGֆ]{isi}J{UXm+BX$c ֈC8H'O$h]R7UpIJ(zI][P `\*N$leN:s\,y,{.$,΍ 17&$J6wJzJ!WV/0Z"ZNv2ְW8Zkf,$i[mg>]Gyn)^\Oo^ 3|gWyKSxIhmj\{\}.kۜ3d#=!g&G"lR&ȶF7feŲY}#F+MLHT#{X*zeT/s[^qYOV71Ybca1f+g\uM~Q%尤H* (,+]}@kHWgx5tSAdI̦m>in!_{dܻw/yݻ |meYc~uF1{E<=v]0!o~'~ ͍moQvm/wC~~!?+O5EÛVnNm{ARiT"re-jj@*.Im, D*T@ PlQF,9YB ";#bQe&c(:7p׫<.rX1ukNݚLڊ"1`!'$ L [E9NSaxŰ\{N*˩dPcpOBxpTE.UDA%2Gsl"jߚqGe63n]ZV#L!mb0n`3EStCn7٧eO)fҠdg YfsL]#eZVheZA;IiƝG8'Mp*D6Z'aBbtzd4/%ĥD Td@&Ok*3$.cAWKrE,3FJAEיMWxBGaUejZ9dNisZh3RLx^BUmB\i%V_X wꖄ̀!`j Mmš>`C?c[22,: $j\(ՐPUʆJ+ Kh#,-%taqf ָ09[49rAj^%R27ꌭ1kT&񂀞{HnUL5)mbbtV)raH0DF*J$Î"a@ʌZ&AWs$(d@RWR e^XD @MgL=qB-}K;ciYc:1Z_UE%qKILB0e+֓%xDET@8%F#nD(ҭ݀1M,(Ƃ:cRn%NNՙMΔMN5u^| 2KR,adÙR /+ (qS)PU zQ,Qe1j\'ihA K*["k%2@+@MA@MQY4]Wd U+!e`Fpr]xSD"gs/_x?&!"< C\3B|ӿ ٷ=?ms0~gs=mi~|Wպ5ěVŤl3 ,>¡LuԆj$m{L 3Yo83Ydi cx&gc\T tT`u9YqH.RC ;T5a/$j"#A:]!MFRؤE3wWl7DŽ; uKc,QU)Ԃ A*@ 3Gq3MƓ.aЦyʩA.T(p:U9)K<%@2ʆ LNLѓa0glz6o\pvcj38[tM6WB%UL\Up{3 +^-00V89Q(R@ANNNi@DP h`-b V`G)eukՌYY eI YD "IVdlTBP > X99[%h[EXfҐ+C:C3޻ښ}~_ﺯ}93s3((! ƆjIT*B.)ZiiRbJE)( YBBU f̙s_W={گZ<!/R"ғ(GS\ښЬOѵA+]Vx)+@#HJ(14MZMUI7M`!qK#n$Mܖ0EVy3{Ses}eFBlLb0|V֊62WZ X M*Ub-N֍#6!y8z49rՌH kOh#%dVx%4]˯(I]Xa !9eE"Pz"R$OB=P4s9YChҖwf+\]Y6sBu .g.ajd6IjH[idQEW2'WU_*EJM$r"DC%e+d+ Ђdrz4<}@0Q"A* z("rܗlgo=߸`)+ɎK\r]AzMM*WHJFSpxtC$4%o.5cu,ҩq36x(;*;*ert>e̾uj{X>j+FVr$5Gs2,7@fldC~2ĊC\k[z8lVU''' [7ЦQ,JXЪfyYeTo W$"/QHErC!1 :ք=F1c']NpՃ-8þ_qǔHYErG"DrY~6~x&?wo>ď|{s> ??w1>4;˵kryDŽoo n޼?q~,K~__*>?Ki{w,/$:/QU%* j`1V7R6EE5#wLn$ &)sBŸt^~L]XߓnF_8GS)GDMZD bP* Z!o4{cz'Û, \RѝVgNWMQIL Ъ XT_+$5SebM% R%K%*2 n1q%<,*DTKd%GUSle])!ͩI+V˲t *e6aC]ޮk "(!hj.zRP![D6Xt&=0*.[ Nzz[Xci_CDd-iPvc^ H*hNFRmܖm [VD5߸T*EEPL$a4Ou:iIi:WȎ[ 0eRkl|𻿋ǴZM~1nہG ??jݏrzv_ȇ?O?UUOw}Gx1u~gKw}0wF5\ |3O$ )ͿY'+/2/eY|__Yט]6_8$E F^"V.}璾}Ih:3jE( a L&U䚂I~KѾc-uat=JddE.d'yM _䫐rêYXџYTźB,PE 3o'0,{l,䒛<#[HKݘLpgk[yu~fY5c#Ff$H<24г&Tn8rlr1tkG5Eέ)P6eěXF%Xbx5feIo- *NmZЈn gVl.dX0 f}o1g[pŽyBW+b[П6OK 襣%#:!-gZ'X䕊Y)p?a4}MMYߘ@ A+F @f~DPI\!(%CyiJKjL11zk(Ea[2 .#P&#(ZT0(O{(z;  "D*.bFtuΡIDA˞𞻿KKrHL<:Rܤ4dƊw_UѪMk0N:7'X>z3D֑)!)($R(%6+QK(k+ZM*,m׭{'#Nvm\RnFGPI&ø2.Ƃ&g]7.ζ3<Ŀp`T 4,u=ݍ1]1%J QaWc^5YT U,Qzc??nMK }A&JcDnNg+4BSNd-;vy#}ׂE눽6cB",B$*LJDޑWڳBZU'}~ Zbl^&JstѽMrYzE!90u6t,Zඖ.(l5KM\iTlZ6;E8 +'-2&3`f Kҵ,-FlBv#um-p.jX5NlKҙ{ F.I2 6xa['(lOdx$%ƥFPY2ˁ|-k+9~o=9svv9!z% ʩDaB.%bƮr`W؅DaaZQdL2a ͕iws^)Eǿ0<[O{bb Ve~I D@EAEps&U1k (RH(pt?w_s^n#T^ 4rl<\!105[\ 4U 8䘛c싒eqt*Kݘޜވ|.CVNT& Da"oC)/^~yy߷A}c}3ґ9^Z~m#bap+WJն@dJ =>1Cnu޽k!d-]g.Jrn|ʁwtpxTɑϗ&dV@p8F~0ZTbdǬhoM72$N2Y\5`%P⺧- bZBCbJKb1'L6U%<)o"NPߙ6˥b$Œ直u2Z\5^bjۄo؄_c!^c9}6>ыGw{&vٹ%E)g{|GiykSK6G%V,os5![EIYWt^!bE&c12fcHtFZ̓CRc#'NLN}I&]~"H4Im̮-E!'j$p;9}rO|S퀉")T%@&$e2)CNp%-Jg5V,iq4IDd!5#∞4/lk<roORixID* ʚ@Qh3Zdxe2h#+=;. LP7 tP7Qً̦m|Ѡ˻:@Ӟm 2n?-:2HQ]zhjBk;+'CXcf6iS-::qeb1#y 3hNcxTfF@mu@]L;..#֌ni }4. Qu\ѕ( cw\-Oz{x^ QI(j-]9$,nsVOiڳommqrrrGGGloomndLXĕEZϿ;=kB0,&_י467).FЭ`؆2dlB*B$6)7rE_8gG8cG8̈́ 7 83N S?jumR]exţ!fX5 UzzD˚Ҵ WM^R=CB-3Y,[D+e9kyQFؕOe'CK$0X)s}GYW ]Yg"jL69i2޲FHTljCϞvVMk2E5X ]5O6YSdmro2Xnqyhe1ۜhJ"K jٺ`}/8sq&Ë)){3Zu6y0Z&^| k҈t1RuW<_~ $[m޲oS: J$PerKGDr 49!5#'+l) mN˘r~DVW899 Y511ـt6_Z6Zcֈm{@q%npstKZEֹl:NZiN3^vˣS%R2`6\c3@+_@HE뢳~ZMf4`':g+ %#LWlU<'>:lAago"%)qY Ƨ=@E C$C lX1P~{F>@DDJD9 mfSɹ nҵ(2pK}HK_H ! |/~Zs6.uI'T s2ECbXuXQ&<~ IU * t#ԃ=p dt2<(lRM 0mzZÛ&6T uա^R4Dʆʰ =,Z_Fʞ}эyz+0f!X.Ka)˜ˌʑ[jW&9Kũ<íVx1i$uh#%[b!rl3?m瘌LF^B_#[)Q$^s5\s_(;==峟,q]||__SeY}|o_; Tv7L7GpC9f2\mr-ХB&m'I6KY[Bn&iT ^;c̷l|FA'g3}bt×ᅲ>yWn,~UDA%\[\y\7:dKT_d[>{Oa-pP0i83*b4ATH hVBP\D\z;\N^i}CtHBxEVy#o˾ =8y>}G'yK+ۜmnl{%rDPs!mHyA3]_`TNkEw S EAU"[9r=GstGKC4AJuޖ+GU>oR"jNyV_͗lr5`p%wG}$7y?Bnnh&gi~! Z@@PHrN{s:/n ()4)F"~BhQnX(&FnqjlQ"^.f S`úKy!ט߯-oHޯ EC<5EI~NďztvE3Lѿ Q,-V6'+o k=DϬ Ė}XPz Z1Z-"Uy@} {1;)pB7 '80;O} ymLL$YE(,$ S1 2f|>G?l6n>򑏼qs@IYby0+8qsA^0ʀt,P: c1V#F5r$D~j&bWE!, # 9˕gB$ L*Wj&,:ma6}BHVTLjAi: pYbI>PM&Aw bwXuB߄D@QVHI* m*Idep0j9*WGf H3dVN!"'^@T(C%Q!c>qqne?6W 85{EXѻmXI5|"j;2y)t CS HeR'. @*L30L3dЮXjAD˙uLl:1EW* &3b(#BS HXuq)2S9Xe-JYXSd3`etdf͒Ky'DR]Q?_Ȋb8* +o0Lz'b%I -:e)b _p\.q߃GuhhsKHaY)/qULՒD$\JK*CR2*DP&)$xso𤸁Sx4^P˖5ߠ حN`Vr^lQ-WN1Sk f?: B|&>][JdI$NIlT]pj>8-L/Zlc:d(L$H"TH$ATrIR!rZQuq8bU,6el{aqfݠ5Z:y q BBrJID&$vQzlU׮k+:UfS91I1q3[C,. +VCP\Z$='!1 gNJFJOB$dB"m*O$d8z@&*AhiOCKfNdi; IbS\+V8ekkByDŽ߯c$}}c1?ԅ2&hU:O[W8x7 2L`䂌%Is69 ѥÈk ~as'.i:Ym/npr2YAU;8VUˠ!Г+™r8M. qszl [ maQ .\,YRgI_)P MP͊Q]VKTYx. 1,#XDeВ@[hhTMV~'_H 1@7n5b;OV'\av N]`1X&qb,s1b7s@Bb%ΰŞlf6kQeDѝvGlg0 Dr:1woadN*ܗ2cIQgNPZL.Z F%Ĭ`pm8ɓӴ#pjK  6B.}M>c]ٌz`mE*&&0d#,/ V"7P 0f奸ޞ?tG7 ۘ82`{д+GngV'C@'tI xR5j_]yd޴nѭADk!]Y޵Hlbʓ+V>>n^s_NxPc ͤn4ƢecA锚AIj2$J=:ɖaIHD6{!zaX=&);<"a?vp{ʱK$MJBР;^pI=F]p ^ IDAT.u5,[p{֔abݍ16Ze4?+o}Fc˪bz Ԑ.]|C ^V^VfEk#<= @aB »]@ si4ig6e! >v%[wlq-umtS3y ɂG# f? Sԧ4uJK45bp1}>&ZDg_yUzt3t[ֽ_E$Ffڡ<'&+f+÷Oo OW 1FiA4RQk(X8lT6Ŗs**\-&;ϘGE_0Ps9 8_8xדWU㪘-svԖQtEw.yeR^?[+ㅋq %KOY2tݢ=c}7~>^ia&M-(2E uOi! KL\AW ^5m$ RG5L W%p)}rQ5-DsH)XfNx-0dɟ@jAۂ[F _xڣi%Jhdh5?q]VnN֦C2IH)~<]ZlfDY??ENIF, Vozܿ>emLqEKM8??)~ѫ Čܲ -`.>Wy8wslztfrJ׿yGyYV>aWF乆(+&"mZzt?bpFQA@GPh$FhFhNv}ciYg]vq9-XsfNi'|v 3!h(]t٩+Py1 K!kl{W(Ds t`TnI瘁j)nՍ1z)z K~Q=hE,jt;G9v6fH#!GZ,ћKKpψ_apܸi@jfFhs2lƺԥUaT9U(AjXĶM&-JHAKL=aH15jZ=v`˔'G}I#{e$3L0[1~0'tXC ݐ#%RN@v(Gl͑}OޠRFҘk#ʇӱ=k%&Veni[k暮dcv&MvY@YW1.)mIi C.u&K-ʝ:X8)BkUȞ4f&u-2$u ^-1 g.3yK;iI;10k4,>(g Kyr2!"rG1}ЈTE:kF/6""ҘgC/73L34S,3Q1.ϟaG.2:2^DG0p Ԝv=cVI8Io9Mn8p8*g|Όz0XF/6+.ZW cH)ۄ )G^";[&)2QFIKn@ߠ&moRk487979o튕ڬTM-ZPM\!I4eW 2Ըez,]W+?B}[l At&ï|t9Y<[sZ^sjtt&JPbGZ@-XWn[$IZi0ɗ$ PZ }|~.K~gwby=`wP9: J,9+t5z^g̽$&'#^$s5!J.ϗL~zLу@P#qR!Dc}F랒j=tXeDPSQ!.3z8f-bPmB^wYo\&!D?/ t ^ouC]H}MUvI?tGN=^lEI5Qr(,=T{`<)[-6>ҭ/J"gv =#=Ӡ0trSVP\#U2~`0?*Wkb{ݻ6O'}-(I Jz=DjĮfa6H]tJ_:qIZ}Cy,xr;_8n987tկ/ä@'yS:`My9|/o8i%8ۘ8s\ôm=ybw W93n bk1cW1mcK¿b_*>.P2'2MdhfEK1dF%,z_#h%;.c2̩,EZ:dM9-g=Ϗ;G+~ڂ2]{wD*N`8H+r7ߟSn.X+l;e؟N)Z6W3#wK XygܥgyQ&uI'#|=df\S8z8f^AEh?Ƃ%=f UYFo@| O*eIh6RFL8B19=8OR'{,~f%9V?p`]]rrE傣5 '='8vi;uk[&[$3ݝq?bd#ɐO1}B _J'GyG7+t-q7NkFZ&J0 UhXYȼA$"֡6Aw32UD((kJ4J#F6&H Y({m0{g O[p>nP=D%(+2HkqLI\Kq*RTCAJK:EҦ- q=Kz貤h 泱шRgD%=AoKŖ[.w7?d:sp9Ag5ۣfY}fZE1W[;Bj9[5Ҩ5*[{ojOLd $-Q[*py0>ˢ'[ZlnߎP^Mn}<  ںFkiH\~u}~n*b`,84BDEKlفl~ɗHZ 4ep%JLc)+ ,D##wL/u"uF6X!O/L *(҂}\imHv6yfPdq'_)1K\UJ<ư/3( r;zjjSú$I# iJ0I -J|Mhî1Oj7nþSpHqzGyYCZo0kO\;s oOx=asӡ(4DVSׂ<#"588ddzH^d+Anj =aScIe Fm U_rL}>{>Qb"Z ¨1 f_m[*n}HJX?쥃tO6NHNQߢ 0QN'"^\HpNtb϶l FVi2eN4o4 [^@uSi#Vk1CLb2,E5rAC"s>,؅]my N}xK|Pc~|ۡڪ_ީe[#;5[Q:Tq*cl?09Ě\psNPB#9 q$qNxZOj[.:Q0[rY]3i!-0:9vvYyD\dxR_hv*PY8!yLݐ3B:jK__8S.tHC,ՐU҂YԺ6$.jE8$C_QUɱg6w̴RZt]>#HM. w>tǼmj+fJ*vREMl:d-rLPbck7 2/t+Xm[*# ^R uRtY!ٺ̷#i (*3x9^5!K,N3d١{Z|M*l)WA it[K.O\T5zOs".`G?D{xRa V]$3h"I5R; gn|.˃Ӑ;GiL6yAW9-'qdƧxm`|' [ԡh5X'eNYk sja#tN' U%DY~UR}hNqnԖ@m)̴>#o-C1B^| HHuLYdɶ XfՀy9`C@aNaH*|vYgaFEE«w7\76^Nv2-QNIdi;-bɘ"(~9`Bs'Nx#^TBwJ*4 4(ϲ\fNyQZʼnF̲Ec *S;?`!AAB~>7sµϧ DKURj}r1W}Ԭ?{O: )uq8,!ݒ&S+jSM4=_D\Alx,w1}m@3s|"c&d2ی8&fCļOk%.!A͢ay8nv)rD9)Kż&zh%29ђx訋jֲZtX: ,[Ȳ`X7J䡲d=uvdvI%fS`s" əGu pIbQ/%b˜Tp|B!s3 Y\l kRUe?] Sd#vjSx:u["F5qBww<7SvUW( k>pqzͪχue?I0fz\3>3>~W =Z#ҵEڤ"YX4- 2t7Y\h*FlGyG?~moW?`[͆٢ (DZg|gOGL1B4HQ#D&Kl;;h!1"8qOb5Z)ׂW2h 'ބ^PTBs/F Ȅҫ7<~5RCQls)7qSpQ#"$`CYhMFm{>Ggz*##!7d'ZDi? _^L)? 6 ht@!k$5h; veo u2S6ֈTa35 cPQ4FjԢ&l wS@(~-QԕNըo5n &slab];zQjS= ~ų D9̿ IDAT GL)SM#_Z,C2A95r<`d9MMsx/Wniwd0d:p:vrO',7)q~b_DE,*Bh;*SIsg|ܣ$isĤ9"mLt Qc9C@@#mT^d<ө2/%*QS0 Ԟ`hؒ >p2F5.q&x*>`G).H 6]V3qéEа7] E`*'fOjjɋmhF-m~2?h; O+~%Xi]~F5թt؜tf 6]5UOr?u–aD6ZAL则6:QiUe rEy/I VOZ>)_%sMX6M&)g:{;GN9 wg0qK5v-V"⑇94<4n{ܴћTUE,| GG$}BUsDd\C߿d2 C:tƸɗ7|\|zuv*gR[LGL?>?@еCN _ȟDJv=#<ȿ߬~q=AleQVbQCIV =sIZ00΋kc1í\褉FdDԿ$iBG|UQ8͇ٯַy>gx1gmf3I\$Ø`{H@RCT_[s!U6ɽM4uC]5PINkR?_%8Ox& ٘jt3g3zՌ[%-#T mۿrw''5W Oē''z͊}㳷[z-B1C!4!E>QvF>plr[ETU #|`PO3SC4>GNC {sQ]ѷ}^b vHBHZzzN4IpeV5\>kv-|Ga7<ܶlH]\h(Ҟ3]J㌣rˎ;b/]Lc89Z@%B/N@-p/د=1CIڳ7 S{Gy}ᗛM2&aB_E/[oyzu#rX1XҹM~gdw)z`ufGI W!-q_n[P;'jX!G%jTb >S՜'WuB4Qc2Z*7"|b)˴2zNI1C}/xauYg]6i^J =fluz  ;K:ΆSo&?;"X6GgCTg xj~h1ͨ8~cN@Ph:-6Vh! 5mȦ0)GLn]#!*`kĞM0OR?1' Qܢ%7 ;o? >Ƞba{f{a{&MVZB5RR+A-%s|̹ĉyjav_wdE2|78xa\pfz ˤ5 ^_q0N'͎΄Gf\}d> Pjʧqsx=}kcUJpV nuY]~ٽ`鱸0{ m0a lĪ<8NӒLӹǤέ~L>MMd?=LЏV#0]͸,xR|Iqqzz1opCwѧ]U{|*./I6:%&d$g\8n92GXmV.#&GLFL&#g:3u8- HT lHWSav|Osvb*ǐs^ [%u-ad~D`3o u{Gy7oVU(r nqsn~ ]|yyϓBl0JPޫL} h2iʨv'9*J, ޭ@4}.'FQqDx 3g7 7-t5 Vuަo0/*cT3]6:ln#Ӛ@S~-t9x?YQkShŸ84أR`QR#H=Eycvo%P#(Y}dS7vuȰZ Ie4}=TjՒ+2Dr|mGfWscE RItX ز'p'IYt03/\W>^5)muP=C5ol7d4w1=gF=ͳ j;+)'>鍁\HXֈDX,^x5݂`ś j W?;GakqQث. NKΌՂA,pdk{$pFzw9R\ؚMPsacʧ7aXOcO\'g9'5aƀ)C\(]VJatG>]' ʥǖDoyҾs?+/_ôë }ϔt/r ;I5W^Ftk) mlVj?}{:?8hA3炮5'=pEs͎7/?s=R.T+']U]5DZbj%Uh5jp3 '!7r GH US&Iu[RzGkg衂*tOw|}Ww5*E>Ց@vrUSl 0+G޾OSB]Ԗr8TJ~COmoIYhW+l*h"T2n'$.GhE/ +jo-iɟ#^ټ y{#D &4BaoG+~4h;KR/%Q1GGGX;9VbP4;@K:F%n_xx$ZJꌸt&C.ujuز&vǑ1fP+ Pxi['UvK]d,iC%۠f!'[q& tCwz @CmL: >]vɳ^gF+uBe%&e=z]l%ʅBh. +cep[ĎYa%dMBӌ @Kh!^h%'+8PrZ]^^P:o!>$X2cj f4Lkk{eKh%VPh&[brC|RkDG.6ɩtH87Pݎ9>_ϩ]`P@lr\c-g[Μ;Li{|{QH)i}44Q6{frg[϶XVqZ4<|2ߍo ;8B9B=NU(d 5>71wcVaO %}{ؾ%ߚw%՝6 tVu ?1 F|ey#,<@%Hi;6a"4لmcu Mlh\MZA%t8D[`ȜoCY2VfzLq3>,s8Zolix=Cynqğ߳^`-d1r}QtPuW4=vZU(en#)c egw8u%R:ڒq͚M'`~eP5 +1}AfZUώLAdҠGQR`-3jG4iMJTR: D-5+0UOO#TLMi1>=]v't6;'+i [s۱\6K)Id91MdkAX¾} E)_A((]h]Ju=sXym,5G'G89[N|kA]*̵o'8R RS( HX8]O45!PKzD*F9eYDGk(HTD]8*S5KRX^ڨB4G]6q4v9Ͱ$3V~Qa\wxx_\[VXl' BM*Slb2;d5g:aRܛ=[EE@01g%;eըGOKF=F)7ٞeCc( ^ O6^/Bv2R׌zF5^{ Uv$ؤX$ؔh FLV ʍ:`v9 k dNΒkhHօ4/ ʻ{\4q^ybpubOidfʣ=F+GN}r%?~<#8VW4=4Ym1STSGNyZ[ͽu۬ZW ˬSZ ^J*d$-VR2%uVŊn*K4hjnpj̟B1lLnܽ:fz>]e$x0g6:>۲^8{m+yo*ڄGM>Q3/l&Itҷt[+[|,. 10d6ۍa y1l$(D)jF+)>cSz$BTU EAa+#S88I=#orؖa9߬pm8f"Cr_s|rc[ .p0 l-pxLΏ[VE=V%UCj/Ljʍ΢5 L6Aybc%d+"N+gθrNy: TA?-z)&𬐁zՐݪI!uj A-Z8ox4QQ̪YdV5TMrij a:[/湿OzwW}vZSμ)C uM&`Z2)FD Y aa a9PG}P-2EA5/ 5끇s`i8G!ը\f..饊R nt%ˢK[P(T(B>ep"l>4I1+mtP ZۨͯP~ܡlz>ZJ5Jc,n5'9uRUWxUWX2Hm2uc&3O _>nx_ ïn8z~㯮7)7Eռ:Pg5B,$V;b~ZTW^;g̣>w1Y7~EEY%D%)[r?QnPQ#T䙂<>89X2e%x ߽/x9}☴[N|~=.ox~»XmI3l0q1S~#jQ1SrSZPCDB$b3ꠍ P4A ZƚS53'cU2+S`wRvHK:tC^Hޑi oz-0-x'^N$3(((-A91Q-mh=שhQBF}|F:ja@pP J~]r8@l6~#N[$J,2IΛTKrRm4 /Ð&KAGJ'cکUʉF9ՐSKNί暎iikZ]]x5~J~N+%xVt vVvnv: 8,]S$c7x_yOKO_]3T3re(f,G5P':ҷI&YM)J4YԑBR) mIz\* AY ccPS5^"ENU$ҠDJ/rKWf3[߮rYySC5Z() XhH*D1BVJD+eMܦpΔ32u|I{l dC"m&Ŗ~I3HCbjcjBR"l&Xd{u(PJe+Ů1nBׇH/C7st!jyDH0'm{IT H!=AĪEfBuj"{lE91$}u#` cS.Gfc[Xڠ<s԰BX8AY-) D\#r%B@ r+3ՑvMrМ2 &wI體ƾ,5%P6XjJC sHMz#wG2::ê W,q%AndA{2_l+3,C d-BHR'^:l7MQIq%g5K0%ª5PCp* V`Iu+(vХt_ZTbGX*xyP|{"FOsۯ+R:Ya&YnFf㸺WDC9DshAu"{hJAp(=&j`rGKmLXN=抁`l;Nc#8Emև,ҥnN,12 :s\}Z`y{ʁOkPu-l7< IDATAkhl.KEB(Th ʒ: Y {΂vQ4í!ɂ8k_V֨ۚ>M>)BsRnJ Vc6JSP"URZTFM]VTaJ=юm-DD_sE$;xjN)u&lGB*er$>u\o6r ]0li[m,Y=)E`ܘCkzR`^!L=ޯ޹8ˀUle k$jGaG:SeIx̵>g7虇7K&4MD:)5AYvK;2X``:x[VHeBd\kfMy>b\(K=抑=c<^Xkg[B=y Fft:=sNϞc+g:j?l45jD JT*G5*Ka洜5k}Ērl 3AjxOy 3.3fO娇 R@B`˩TrL `j> ZXw|^ϙ[m- =I5l-2`-[Ԋ`w6Řڣ86D9*Q*ӏh<}߻X!a)ڦRU{`wPm&V?Y0 e&s'$;ESM:O td*Y|BT9L.˳tMuE5I)'T#:Wv8^X=:n(>^v;wMִ~Q3ȼ̔djM0-AamA|Σ<6>0r=S PR$o/971dHVL/0)ru0E 9sQї}a2 #/RMFnUFW,2*۔dKiĕ7<Wk꫔_! ^Sn^;|x=&h'KT; ! |@gjszD^NbcaU;Am)7 Oo$ "Ld ˠVr)Y&ĊBS0ϱdTOUSr[LJEQJ*QfeYdd{}nk}0~ O&ct,RaP[7hqI;6GZ+T?Cf(9 ޘgM3Eai}L+RC]QWٚ_<>)G\vq̸Ujޜ=5raI4tGقcnsz}[*P:װpH3Ϗޠ4$œ3^jFQy*b)(Eo&fθwьѣ '!{'f8j^VIéh kJhy񡒗*P*f&nAԕH,oo|YecgkdIW1C/%Z^3] H 1+̄g F)O.1{hZVĎǮB-J%JFm~n唍R6-36֥ѵ{+jBDE{xM~㰎@MΠDbbۄ]2@ v/⛇J刼",c&aſҨԔ J}0$YYi%ŵNyS^ m5u ?cp]o VFv,ʎJd -eCKlQma n[dIm({ɻ#p@񫃚:˼.n mMj +Z !ǍCR"yDK仸ayacY*_n9j9n9p *Ѓ sЍ>XozoL}iS4}'=a)c2(c ȶh68J 2a(ǪRւlo'A [K [x>)M0SQ{b,?T7յNqkf7=Z@RBMJZ@ro,Gv "G*5]I뚱S΂'%Ewsِg; 1)`jbc7pu.w1aHlM_+q11FLwD S7<-0͆XIA[eShJJ/^vDZ\LS40fbnhu*_騣-GN>A؟Lf]&=&.:+J.ia0SZF+)U]f!mxT!C5R(L HQ)iǬse[c7 El]jG!7t^EJw1bV)y4JKd'{x:b'>FT4LT,u8B;sFtVQժzզRU^qKK1j*$Wb6,/03m2FƂ^OD' (c7Ȯrv ;?m=3L6X-ccbTD`*9 Qk C4kKԃVcM`5?Ư=AZ;Μ0z]=''c~HTDՌXdXL ,>H6}Vg]OlOZ@ilS.t2A*kL%ZRδD/Ч9}1-Z//VNW|g~g~#^ZȤ4G>YQQ4SyT\~wψ/dQEdų|e Mc&1ꢎJQԦ |A3Qvi!P A%R={`w{ga2wO |RNxgox5gGxZHC!:,KQ0ʄ~1ygoYSX(}tTxx9̗}Q78}t|< h\m3>v4mo=V:Mp0E变u#&/GD{7;/vM W D?4X-ViuڢN(3(b(FRZ$Y qIlIh˩sɉsʼn}I}@:$C嘷O;bY7A@g P1׃ccu8sهMC 3)xT_` f|Τ0^"{D,ZXRKA)tT\4[Ɩϻ|v@[qq:c..cZ^ƞz[^ 9͚\1;qq!S_^?f3VT\&cʣC~r_n>F9kcxQٕT]AaH)h)}4BȅXHEP 4pT̖1V1&zŦ(y'ވ9[<;^{<<_Wn$H˚~P 8539TNtx L;`}8m%[pR_qf_dtyL13m^攨!kDSu vc{1>bJ sM))$oRlh>xQh:z]VSh覍c4Mr3gApXQ )8>:b )HiWl;fD]iw@SX4ZuY J\SH "2{PI&.Bb&&ՠb:YR~J@p)ִ' ,2Ye=V$2اOVؘ@R7|Vg{&}{Bs׽5vlt>kg4xk7S~|դŸ;|{APyƏH0' lJTRcBCcxNk8g={ܰ-q" tA*?1:!]T@8cp?򷇿CU02HjegX/hkֿlsa)\=e 4|^^6L=.%V!3Ƞ|hٖi+ew./Ϳ~7L!25CFN5F4Ϯ1슽{={+%}F߲s.V%Gɐs ,c)WmfEmf$U f;Ch5&ɰ)s= nT AUi5hZC&|U;i1ŷ)M&D3P`&3<3_P C짚l@Ȃ } i?OH6S)jdՠ b`&6Q!6K꓎X*4!4ׄ{@?oAv.~yc~գ -+m0 n UUmm:6'W&zKQLOϟqrIA-S:]oj>+f!;e>0 f m#5rh5!#zЩ~4$#u{)Vh(kkkZVDxD /Ve @ Z & 1A?*t)E/0^cQiIV0KqNpo8x v+:0-m\#: HhHJ r,Ҥ4uPu 4'ht L_2#͂tJ,<'_9XnJpvk^=c>^䴪ޭ`\%.U#ͪQ`uq7-Zn7\l^ZnޮB҆2iJCp.8Q] =0ٔL3n}~*hm4(}_ Z v mōyB 8qr& MҹEO&wL`1/+½ &@#"٢%ߘsda.pLU@4U?5$BR$1-W&|n#oS9dIVhB̠4E&d6w`f9k+k:5+^xC 5ӡey#.ik C}C>3<vd!;,fl"!(bXؘm.S>3!{,vVCZb)5ݮikk[fuaiYִN:_|n12-k]&lpXӄm Wh/R_ЋhN6#< i L+CU4FS'-S:Sz`9 IDATd32S\`;g8(Gz%Z' I3iQ}Es  ; h W 3&c>ݟqqqpY#ggTbeYh5|8{dO̔d!ٳyc%^j7W{>^23̒Ġ)VJIf{kaPIj0l{g1PA^۬6EcC;NKNKN^^av ұI61=9~yEv[tc/8 IN$DXDD&˘;0atX|Yy{gl;#M cUUEda[|jj^uϨ@_ad=#5eˮU 7]/Y>T'X^<[1oN`=0܍'6BR:c)MVȢk?rl]qb]rsE֒#g3~;1 :Y,:lbm}kp(!w?m1y#JF>U:/3.OOܽ;G]Tt{Ğ.HOُ;^38 ^?pw ѭ?= fE8J}5%%&%c )*j1g.cÐͧD! T cCSZVI.B`{BSIVZPi;:&o:x Yt2?;9fOo`=eo>%_%2 >)W3op[0\'v-L~G%PUj2Ly;?par7LC! ͩ>hN⧞>?B]Ͼ7Iov{Lp^b_\ӆar<3: Leg$DRqh]Mi4@y5&`P |{gd:L2@{O c,Ds./0X_gsƃ="+,G2`nK B@-vkR[!P[2G:bUkXA.HS$vIEe^ݘ/h nmFP}MiKz4$*쐇6Zؠ ZX+sWl+DA zOH-,Ӡ!h*bšqZ$o8(oZU#Q&BUT$]D OA56(R8M^8ԩdivdЪTJ< )fM;jp`(m4!Vuf LQj[52Bg(ߑEcR(BFMzͪr:rX5A?*Kd=l1L0kG"(bcc[գ#.#@El-M`<@;tNV'SIe%]qƑ^^A{>"3'S'e$GEm]f5Qt՚6BmMlXF`wXiqdke?GJǍ1Fw*6 7hW/ n{c}2}53uPjBҐdI9~ȩd5?yV;XP"k+dX=NbCs&SfYMi( "$&ܭEUCcmu+N +?ŦBCPc)r( "q"/\# fo[0}?ao Z3ќͦIYd¢3 6``(K}мY0fۀY$X_+eCS5މ3<3kT g?!AJD $(*j*mFXu,oV>BF nhVM)Hb7#^!`T'8ڔTNmIe25Q ;n٠ǼcnuY!,ݨX)i::qc]pc'ڭ%8\4 ېECH#+%6AiƂLRHF*O& #6 =k]kR.dh3b pdDZ3aM7Lp M19^Dg-&Vz_spW0z`嵂0^--fkU]4-}q.jHr=;bv9R:vHIj<S1iYHc`VS,yxgINHA;m8ޛ {RSQS`%$%C`k2zqQ"*~ywgH+L|drOTY$a /VݲXX=>Z/m[&oۚtR7gy_.\BOc/tq o wTe ~SKQ댦,mjQ]BuP>4ed435eTD4)I"\CЯSyW7?q~_C+JaPh&fp=mb1@W'o[RLN; q͉$VA+ղl9`icH28-_NYYmVv+w=أ~|o9]*1+-P Y寧juß_pxg~Wa^]2x%sT&j5쏸4P?lEy8N=NP[A!CO>BSx)kQ6l~6~+5Doj)j:bɉeM^# M)4-2Kh-㞙2̭Ne9 M'.>V'8$|~Wy~|?L ,.E)c%sz<^iH511ds&&?d9Ό? S g.C;;杁UI0<~`z uwQBQ" &CFk7_ 3AzӲ3Β{©`iqw{iwQ#5^kQFhS:R-qգ9S']ՐMd3 :Ka):,D%"*br"rjrlѷ6x vRwbDfEgf[$<.%[dM^YdFYR(5yd3XK"ho~3? ]IqhY0nA.L=T0bsHn- ֓BNgꑣ#_u;L=&r{Ϛ֓xYɞ¦կ^??C~W_~7ۈJV[)D+,Ƞۃ5i %|nCL;aNÜFQ uVSK6yg7^p;VFx}$sZZĨyd!f, ki.qݶ!zɰzdVY!Mi69R?e#B(4JIVU{g ]g\ܟ0#{$)kbMY40=Ae*u)) 4Rj%̢(-PBl]XvJJ_bSPaR:%Ƥ[+yգNVh%h%= !c<Ŷ$ Y^w\$<"6;̭ŧ8VX),Q(Vy;FXJ4$O[)4,l,dؤ!$ +ET[PPjҋ*6+fwq(ujD]?hD$B=5b Z5¨w1i2(3"}QBY̢l6NPY-2|7P*N;%"`:];L:<\9d*fxrATkz h%4IAEKeZ>RmwӏI;6wƘc&d|#\G'3 c0.YcY=FiӪq<}y2*Q);b$s^MsIC\lo|"rBYp^߿EJB,]LzL&C^Qi*ՠF%0# pzGVkR+":\9ɍCٲZehS, *LUa4%JIEcRKjvC9ܟx=d೶B8:r~Y2mvAW_Е z;lO|OLVQ eɧ1wcV \f(kWz+ZږV%W-֧6X^WCT{ 5aV͌#}tI.K6u)6ڡ ԛT0+Rh =qI\q)4 *#fx3`vg@_,E0P:$PVm)N65Q`lsI9{ a@M;"#`뷈" *E"|Rj2R%kqr^ -8q0:H _h^s=#O*ǭv=d}'mBT%7aM, ijF r`sg b <8vD;l9i_r^q_D4fL=hu9C0yF>3őP *;fu#jE$}z@wp6)n{ع i|1`>0޻s27 }g*v qw$ ͢Mdw &,2"umV: ){pϻ7Tw:˻.n@,#( +NK<+qP겎D-nnOnɝG2qI&.jR+IL{tg#f(:^䨾!l-n.EĒ%A5$U6sѤ`R7ޛ(TP YR3_VD+5B*wj4ZV1\Qj7r8q98(N ʖ4FKC5iQ]I: Rr @|,jB}øu냟̰|̱uc}|֙ǦYӰAXsY}cdW9NO.Tz5aJ~+#ai,lNgi`è;_~ ~z|> IDAT6ɢ9᥎<:S>s~?9k_͊jXSʚ3cɌ536.]0 ֻy$d/"JKR)r,)66EbRUcEm"$H(\S[I;M4>ŚثkQaKM6CVYKZ:ȉBWXиJש%e!(NOd < Tf^??eeHLDc@jr THA9MS/RM@DPad`ԉhf yXar,!`Ko6jȽaP.M-**EQ7TBI+l:raıYb)a%"=(w)ye/,y5idCXdK@]t'pCC/4TlKI;}A3hB WXvN`o3-QIIKJ)Sbk:h(gygkn_POyj؅57D.Q0 :t՝Ge0˞,rAa4;}nvmsP#5١X2U.0!ҡנBC;t UY=d;rbT?${cu:1iDxћF$L[k,3[9#g_bAr Lx`B)LP 7Ok+1CeMCk8o=1BF,wcNm*,%pGMLEɀ.?^ 2(6e"<ɘ,:)UjP'UjP]ȳ4EvR=5z(%(RNMsy<'KlDPF&oxM?~MBb"c*ED=0V՜lZCg X~ס$ 9VE`謐BZu Tmmct5ccN[}CIC_W6W 7-M>d\nT)[:ގoˌxv*0NkiDt4A3iṄSSSi񓜓awS뚗hK;k~6{ߥL%d:՟4W)I(41~\g1YrH:" SXp̞RASW=uGY\r{a΄9bbZˣ55ZS4 0K 9rAK#]d.A>)4=ykn4OxG4D$"jlusAf-zlۢw\pt?]Ob NĴF:tU)-?='3k0#)[wݧ>])sLƬ!Ao \2+ݩQ}A})l-AC{0@i4sY[}V>QCːx"_Fgß'TV@r@'wß}p\U e+Xy-K_OvoOؤ=ԹqSía摿IiHF uŚvW'Kh<8 uTE坃= Hi=<(>)Ys` qU Jh +܈&rO6[Z^QIq)n)Zzl)Zu=亼gD.՝M;zA{>heSݝEm~싒Pn8l>JDz[yN>T,.:TQL{8 QB< +dۢ[(#-2 ^ "Y^i,>?hK&i2&;k#X@{GycY#S膮jD: Srs=WH[{ hh)(H(H+27QC qq \n՗,p-_!iHqX1^qq O SlȎPhgC JH_a6u'v$PLLlxޣChXm 5Ol/>_~5;ǰ !Lֳ͏]6+ ueBb"*{pώeBִ˚FL$l>ݔq)<D˞H,>NYܜX ZH!+-PCAHT &oscsFϺoy*cJ}+K0ܠJpXv}iâ5B(zƖow,ra" VFe] zkӺ4̺Qxp%[z l=j:bqxa? ({6Fkca枱v~wBX WlЌc1=-H%v3")"ߍN;6NqKQ1Yg@UMFG4t%ߗtOLs.}cfT3CAo!X*@ 1RBN?6i.zBC-D z5!_*L;=n B M.* RtQS-LTJ$6OjqNC(Es2q_xI >x/y{Ok{DgfpQ5Ɏk LĐ2l-?ۨ vqbrL<0*\tV8ݔ@^h͵Ś~|5m:^4[O9s?'~<#K~%~KUۀxoҍGqP6TGقg!tZvYChJ5qLW_H:!h^e_*U>DepXbq_y.Ѿx.bh!Y[6}A}cF$P-ߒ+mbL4f |y(#Ls_b^`Ǟ;zDMvY.kW{NErw9< HA6ґhffhVCI#eچI\S])s^'OXuz@+FaԬ!nF' V8?F^P] j1厗:v)lS U[ ;`0+aႡQ&{rR yNd4rI6캤2XSjǛP nxdI]##<#?]~o,F+qp=EZҠj$f.!$LD) KA1w${ |q}TK.]e @/jڙNsdle{QLGiJ-&))_xlͼdXR'%SƟG _,.:gQU$w_d1~=B5(>sORy,)AIٖ=cK/0ၾ`˜AuKcǒ@i`t 7+7616)+)HqQTN,]$mMU9?}^fXk)]77:mstM֬!oz??:ol3S&uzm#Hоg agG"j>IǨB'*Cf>@l\99ٜl w|ZMlIk+F5e=#Y@xqsQ^! 8l:#g=O}݈aZmcj{=Fs=%C"$ V-,1KcJ״JrH3OAS/S>[R&; f>;cQ=3 7-d}'f{G6'i}F E EYY36LFkz ^{֐Tl>2&4?4?Ԫ%xٞJN~Q$yl sAe01#zoOy+TBZj(dar &e9bhL` 3T5͐|cq9eu1ãtdS)1)^񱮨 ]ևd }:hǚ<0.z[S {rfP%*Zԫ v.$T*UC UOVxbAY0>*K#<# ^+rP^Ap.K:I[u:B-jQkT^Ӫ1K_g6ڐ¶YC:?%6O߉D A{0~0i]lQg"ʡQF\ğ0-Bd{^i4IX44W)Oˁ˿^&#Y TMoK-uDmC~cn0N:#-Ox+q 9-j kE5?U_D,69jg{&R@)P5|j9Cf DؠY-?Wy_Hm{g½=֞r~p2O߼aYL1̴3f}Ds3PD$~a5^'tˉ7i ) )Q2+qh4~ d2bՌ_ox/D_<0E3= x)ƥpÇ)}g (X0z=X,NxMyXMt;FtG[:$6$]w<1L:k<7ƿ|ҎŲb߹~@'r󘛻'gvw]0j 3ޟa@A>vL5ٍȉ|Պ_=ngo f{h@tt;76-)oZ+^ܷWQ_J 8nia@ѐ-r6!Nm:Ee$f'{3n9}@[v J 6:ctEHOAikA#ZΞg_x+o {lߺ%1LxS z8y*Ty@U1I*U&528C&7շ&&h1>Raz ~.POC>$Ghv iN%&={`Heg`6}dh 44u A(MR{wQAWuS%* ߀I էP,Sm-jNOt<#<?Y] _>8bɆ.gyqVMjxCՒ44fdij;1/=8t6 LQKÅC4zU5&D9VXU4 \JL@)lDɆF( ܶI L8; X#l&3$M@Y㸂P5*AAwDzi.sW\@pd^BP`q l/~Čjg#鐴;$ՇBr }{!UdRnL/ZusP}>o<:N4d4,!ˍ26Ȼ&WҖ$y50Xy9v¢h&,!ˡ]T pΩ-&Omºq2<7S&q]Xe]emXSx8DtN"<@R_?>=c,ZXn4=r`SX6ejݮOJ psgtzKXa "t/UJ$J5dIxɌSwi3SEZgr@]X&f᫒Xrc IDATjV7ԹNZˡtƧ{ZpS& 7 tI+t-H?(0 9t]k:`Qm,{?8f8- q np Ok> '=q *4BV)vmRy4B>6kWzuJ2&00npKd-2hk kNlx`ƄgS8<;A7;|7-+1SHQQKe[Q^Tg.URb]IΤzSLta)v/0rnAG*i@*0q{ $"| ^y/NkHOIjqlCw¯ g='6}2SR մl0Bݩ!Qžٗ!*d!>1|Qe[cIp)~ ^aT zU#+j0F%f?#(w\w$u@ZG7p.5܉UW ĂD[R[:iPY). `~oXlmx۠um"cx4~2&vDr ,'L}?y?濢ԕA:/>=_0~6'M< A2AW8ŎcS6eYH8ks0Xd{w9,pՒ_7xGezWm, ![i棶.SngOh&4S,iዖ80f;6l+h6;t/YqZi_[r5ow6.[;dk,%]j7|uy=zVA*uLRd:#zQ~hW-ki:̴iy~k~e/9z*?9p X[Ea@opCO_pe=yÇmgF0*MĢlL[0ģLL]Ӯc <Ԙ37 |!+VYJ Y!"E-jl1$ީ kI7|}-Z$_{hA-W!1m6yzS^P_2O.0D˸z3σ5@$pVkhe-.lqs>5tBi=S>=Y/y<#rJ(>gO%2nS!iAԙ tbrٔ˦.uL`{1ӽһ\Eqc5zSW5qKJP,re)T9hZf/ sgĹtC{ݐ'z=5Y ZBPQJRT`/4m 5fa65`CL3cN&wAB,1}ݘA ^BYDZJ#vnHsiOtż*q2'O95ۢKZ8dNB1EyScr3!9ęGTt4l;c0YrdKyD h[)N8q[},''6JJDD!+Π,\"-smLG+[E!S:Z[54N-4jWN ߜ.[UsJ܌hTsءh X#QVfaZbʊlSld_4$=A韤Q1p %&r.5n?1S4&'J,ĘFh*2~}3f=eGمmRXePԠpjPy}VJTREQ)ZBYiPKJ@ODrFdaI({i)}/~C3ϜGyG 5~@109K8`4,P9kl6սKsȡͱ&|":dAd@ˆfUӮ4Y)- 7&+ 26N1zlq$S0#,,#8f['Q!mA+!ˡ,Y9J)ae\1tN"]$8HJb: GMN@\0ȰI9XXgnL(dzbH%uRKLa%fŰY3hVƁBR_hl=rHi֞{J4Z4 A>|&hvNSAn@ lM.+T8W)R3IEub0b?79K|p7Ҝ.@iǐ[MmGq˵k kcͥ}ÕR`d)eIl8Ü@ħ,ʥ.Mc䘓ɞOw ڋiva2.@MMbۣ RGLDx/D2`kzC_zdj0OBː=$msw?NJwI="b].{Hevc̎i2<"#"2_@*_K~AT!=\Gκd#k풵pmhL^"Jf-:,6OU o4Ipa9 KɠxwEɸgj8ol3]i!l_2trcHãr-J_ja斑H^>nb?mb;}G|f:`qW05Xܵ\P{qDV2өeKc#`% q!N]&Nv&w(:ݘq1UAER6ʒ[yWN|!eK>#7 '==l]vSbS%NrZቈF?CHY-TeZ44i OO dͺmg#e@5XR(TBh<nC6/FŐR:dBxa]bX%F!/EEd}1tfD^͸E%RQ9O#ZP=O'x_-^W0K0RUC,E}]P?YXhŒVdm|4)>o 4:y@1P 9jNQ[ѤGFsmejd蟅Fa hʃ`A"6!q?>.`l=&PM0UpDJE^ xDؤ3H? L5cIn>O>tknҫ[`Βms͹L^q.h)[SnSnS F'ǰ KZ'u'|#d T;Y!:BªQ EYfFa-֋-dNiS,DMvߦo3AꘇקRy{G\"uA~1dwwAG8Zg-yUWM$Q Aiٍf=W#CWm&uPW*U`|Q~D{wQ?>%uz+3MBQNYg!}2rޜmn1AjYL#(^nn9yyvf5\Y/S^=HlRj:LFZhFxnx RC5R 6KσCky{g$?\pa9:ۣ|{(pk|b;B*>R96ST|uH5woN99Dx'ǗL?F|WBج%+Ѻ%qEҢ0Lc4G<8FF7!ۢŶhA"/4L'|R,B#䶁+a0YV赁VmI#Y{g°q ӌ.aE{kRX]v…ب)DMTxNDl7ifOYW7W&CC(Y%Y5eYaۂQ7{%RZH!9 VMauBy 8@OQ35}&'לל8׌3^8Ĩ3+f5r^Pݥ^x'x߁_;\5zUzc>@[ԍL B^+-#Q{`5q'1F UL2E'W4T͠)Bfi{7{N[΍&#Ȩ bKohWM  bI)q[!{ t Ԟ#5E'q(BݎVc=SN^jD-ݓP]Є!.ɞ;\aɊZbO~E=thU[$}XMM0UJg^id OqʐRh&s1m05$C;ęLs::z`kt#FFvjEAjt;Rl;)"va8tX} ~;!#mhc]B VejI,4:}z $H mwà5's jPB)Ԫ¨2\.ʶW ڄ3U C+6 SQ5|E6KS'7]vFV`PdJtyc*kLV[JUVe.uڄY6my%ImELFwZ۵Hje'$$ "1D,Pmt>6im`6mS6i>;\ck)~/8e_.͸ 1V;1 v Q 2malDhT(YLy]\7YіT] ({hqW1&AjnhF%U!Rkb"Rzkb#DWTDX5>Vz0ceN䋒hnnhnXzM+ʤXVXV؁oZQkJK L+Gz]p.)U]氜9n 0ѠD%lkTGPtnC +st#0r!2[OlP'2))6:cIq ŭ'pZfeW 0I"W9UZR-kz=O<Ŀ^gm0ݓݓ%noOJ>^T*H-B'ds|{lz}H TYR U04gꤢJF\NIU,H;8CPAk~AGpS)Q4l9a8fm;*2x>2c@yȼ2l{ uҔ znfq;bEceYe2&&gCr00t ~`ȂZU suĵ8SAbUPKjP[ 9CCLLSbX_ܴx4'ȖJylz!sc6qp 1SK4Uz,0pϹwnL2Cfʐ]V!؄[gl; ,XUP xp4 Uלhs>utzM&RB,oWTPJ(W6f&yh&Yd{n[liVli#aDsjo1?X]jaN46b2NJ~W,WI]/P$Xw @E=⳩D C*XVcKQ6E+N(~UL?:g7]4$FEc,(]#*w%i0'Sm:GԒfѸHgjY9;6;9={i~oq G>ńUujRe RáJ_m*m;ZiߔRd]eeJBA7}7 otwLʃAPu*v?[Ǹ ^p!eh! \XĶ~azTbr$!g(-A*S\P!l@(7yh-FD %A0轂^@mmvHX EJx iEaq43Ya5ov#.<7pmGx'x߅_>^^KΊO]>u_X>y}c^DKh!*"<X4Ym4fsϖpRꓚړ|.ߩ5otCKW 5նTV<쏹==w>*Qm5@PYhӾx1Q\t?NyNAaGM|yg):n[;KvCk;@<$v-@qa8fO<O;~Saٜ݀Ӌk^zC1^ mQ+^T/jHN6aKB $zNNW1E5nAze. BK*(8ULZah ~"]Yޜ2{3[:YÚqs;j_Q4<¾:6mg\8EAvk0$3IiYyKkH$ZVb+ RJRK"M"+M2ag#cR`P]2G* T,c|e£`5$H|va+Ql CI1SIuWQi$?8Ma01c&9%b_(&锗|}GXL%ddP' unuJA $4Tw Z Hu"9bP N 8H\5J,QbI㗒aIMJEJM)_NiC.]>uc`BBiip,ٕ`^L Ts4*E"UAY-߶y6u]Qˊ;v %B;<7BH5]` n'NK%\¥C| 7zPow $kd V>Op?3<0x@80`3 ~Tqr~ˋnЪ(>!ABP$#}fM7H]4bKD oϸsǫTkݺJޖdwm 7%4á Mr3s,W=18`^d84HBh nQ+T'* G_bR<7al6*6s64VL]됺ßI-?w HA*w*N}Pmt,לbm}S饏Ci9֯x9a)|ZLmP0`L2yYT\%@(YmRF2EbVP7gcx'x~_8nDRsn~tyw^-O8EidqRd&\v݈qd-ܤ4=~oGnq̒rW%&I* E9l6 Wڥ QFa<0Qh:z*J;;^B24+V  ]7k}&5FdyղA\0jDl6G Ѯ;,!%HA^*+&#_g- u^(}+OG>n'ME琧TuگQz:@iB}Q0Ao]RƟ+hV{L5fSʲE|#g'9c-TcYuجWdRBRM7P5Pu V?X ufk}PgK6Bz==/; 0%b, %& ,hA4*;;ω/uj[MqQxEWxEcJFڜI=bbUU\.nXz/I>PY,Cm4p+@s,G+stY Q1H1Љ{k͵6rl:48V(*c!*$KEhCVʦ&p #5CHބPO!d;erw(MX^H%!f8miKvMl~*"qԚ6'Zg,[$׍6O<O,& nzĉs:{گִ_h9Tbf66K{ ӺYeT.f솣)gלr9omBVI-V#f>`633X:=HzΕ s2 :}x>#E 82y-y .noP|#2Akb=t03Jf 9@A7%ai [&,r)|Nj8; KL_=.۟w=fgGB!G'F$CAS:06S~B72\gCcCۆ<6(< T dNQY<? vO1DO =5=VQ7Imml_dc+tŞH-ePXsáқ=bCX 6asdM*T#yAVŬ d-B!0޸T}IT7 kN^g*lE4X}k#iNEY4.1iΖ1x7V]4b`yֻEE-x|a#g^ .Y"Lƃ=y9E瞼퓷d .79[M4tDu^N GCYTdW>SblNT$̶W;ٖ3A+-ǭ;׼GA4sDO'x_5bwsc黿fv7!;:#%Ƣ($ advϖ3eܻ#f Mi|țJB&=$;XNj̑TRA(FrU w  G}~~(=?cpb^ Y@BZA* PP,X&N|׍?q!M'~(PW]|RU}(qM/N+N+NP9oθsoԊB(ԊJ-yi.,(9vJoO-Qas/sFwdq,4y&ݐ⦢xMQnF#{R*h UкHPM!"!"Œ Lt)-Woߒo FG[~WǗI&r{K6630K d7 ?bqqisi}1g,92Wpaa Xs+y~6KX/iU/tDk>ϰI v 2cMN }yx&A; v-$v-VansR I EI30GIsԲ@'bLb59-{М13¾^/;%Ā8ĵ4AԚIh(M]iHQ+RdEkIFv@Bܤ'G&gS"F%,* ʊxJؔ^i=rJfgY@2GѾ̩뿷XХ{SF-u*PbzW5?4 \'ۈDQ4ؖ-Up*+gXWz*Jdj;sG@%]VX2T眷9nx6alFr)Xz(8"fНsq~7Zė֨j#W~w(K%wCe73 #" P@ث vl-C`)T)}=bLlDgMDd}rz&E Lh5CZvp٫jWeI&.QLZJv]%73zR?S+ю c%(\G$" U}uyg^YC"Ҷ|f̶&o vwyO%[ƊwVt9ߡ*EAN EdAF)F*-Dʩ&>oJՏ49h@$%IyHMBUDnNEnD #ԠT jbOaح_+H,ZK|weVZ'#ZDTX2"E!LP44MbﬨF&FR=ySGԈ^hTLv(_lQ-)*RHˠ>jR>Uc)?ڑ-LҥA9t*Oլ~)t/톸NKpפLM tJ,7GiG.uvѵ8/C3: ڈy-lLRiYunM V v&T߶P2s>As$ccrimyóRJD z`Faam }}J[UC7mD8g; c;CQ^67\u+jҰkV3C*\.y=O<[~_%k4 j2K4w F;J*6Ik E9_o SJNa0?‚TH=\5X>޶&ښsĹg<Ж&W:Wh՟FFʸ`mdfygx0_:(gE"CpۚIpk ;g:<3`".sLUfpe`1gUv΢QhqJ^̷#ZNJqNJȤníI>_qjjt[fo+~].{^k 'm5ZfZT6+q萼23f7Xkm֢ZVtsXw;$HG3!\dciJKѪ=A =I&11[ ?^|yX3Ro( QDQ$Ug5|J)tʾFkcY;,=^b@&,FΞnzĊXeŴ|e|Vqf`_L!Dc9",јR*ʢ Ԏn&JSa;p9T)>sC|:$R; _~H\n̶vQ|ܽrUAx^/\ `|Lj>6{~h>V;X7\ߠ ȓ{'xʯVt˷FdN)P{&ڗ_XߨtNĂSx$Kf$+C^ڻ]I "$lPcL0l|qi" 5b3WWuus~T3@ddFfz^]UwwABS,QaщxA=Ayvj(8B8*1%ʺ,0 d!zӠ'mN5)-sFE/8= )d4oQyV|x`IOX,%7_C)L;!k=XTYxZ" [Q'lyH1w@Zd|\9@n-{]-K3.T Fhd.l\jMMKofJE(@T6 2is׻SIJkCKC$JB5gqHHJg5?0i,y* 1gR+)bLp:&,'^f~LP3472'#,0L5,kqfmzmxXMU jlHF9RJߌ u7 -9Zva{s f)IK*%i )G#hzzlt$~C%BS$T%%g J)FbYM-GqJqJq]+R:$rstJy)sC 'CNO3+b>rk)lL JAeo n/K[ E VkS֤ ؄qœaJA+!x;SHH2^ kCbW"P:I, #)٤Z8;MsM҅*@(Z !#bzz!nt D#$DC<7w\dzfB DiA;O@x ߒJ=I+`Yv0{c1D3"#!&%NuXH.D=٢d0 fjwZ#Dyu$!FHQ8dEyt'QQ4UzEO/ ^َQRꀄ5ÚeK#QqYtC,yVFw$ %po!QB,ĒE =bzAQ"mݧ@ Вp-U0玳=t2'$#"„B:1Q GN,qfei9QDԘPSt nJMaj#Ce>¼ (3,fybE*)R(\]<Jɥi8r@1HdaWBUi+9zge}hf0PP[ 5&fj,jHZkTIC"+ 6"+$M|2duy#1`Xe`D#-uH!Fϵiv:HJGh*0iCU@Vqz&l&z衺!!1^Oe<Ȥ2K3K2J+Wh,!LixS5Jo M[˭Nf+Re%̅tU /஬ud I aP)I m$@kt7ЌB$'MuL<ϧ0fK:&Q#eͩJtiq`)Zn!ctL15F @#DIX=%GIQ:J%m(L@v% "?^ACmᇜ}X@YA $ }Vh`G" $N$tuO7\3O:ّf囘8uuHy GQ5!^hJXڨ6ꡍϜ7Njx;Iv49X>02RedtTY2̫Ck4<}tUBʙx Q5mqJ!ZԑQ+!{1$>A!B}@+4&b iM2XR 5#+8MA,B$N" b zadk`FF"-3ȓ:W( ^A[)i' JX'!MF(].fE_J҃4t G(DhT+ h-[ I@s2 #tjMrjcT1zHSzx?'@4 ]UM58&YG4blJDJZFt\kYܤn7i5ԕOvdw2fY+ol/wr|$aܞ, OodԘe6 CTPQw~iG6:3]z3P*!ZhIś5i=G\aeCCtG5DwB7il:6lɟ ²  s0dz-BIi(9Jؗi7ss+$DR`>$DAE&c i"lLF0G$W٤ @UNVX?MԖ):-S,*Z:!1fs gh<9W#/)iulFGR~п ~h,8k{%I=X.dY~E m,F? D_`>퓲|]S]ST9DQ#d=(ƀiz32Jv%KGyș%CbUÑM$‘L|Y#T$Pc4һ&%dƘf;e6mqN3fts y*KyZ52: %I!ŢOiMIT /lH&F* D*X5%xwD=RNҐC2C.iac"ڟ HH#&-9rKbK=)$V$9BW\z5 ]DOHXoge i^L,*4*i+ClL2R*KY‚Z0s2cUHH"$H" t;ZSGmxoXxXt4v^5Mo>ݟΥlH3Ӵr.B/DbRZcb>)ǔ}Lc&`.2R$CSE'4k &h#FDI8ie`P-GٸqO<YEE~1+kN`RH1y 7{d%jz%BI@z%vF"/h qɵ4-ӵ7R!AH~ꆴoǖI;u,yDL/IP 5a绨zxןZx`+婇E"j(|NhH*&zĮZ4<mu9F'@Őc) AO`{g-~Ggcb7h)\RxcN^ U HS6e?0M^`SQS7#8NH8Yf$aVeI8k,aTV&_+ɗ6!k aj li?&ģDiXWm$/7K+(0C*8fLUQvhM,<h0mk|Vo-[5,z#;|_Dk%eu69Kb:X]KzCtxuR,ml>4rJ3@OV]ݦSضOYT߁D&Gڣ)ZCiǐ9ϔ)i+fnKUo$#B3&!|Y% 'T"Wșu<@ PFB$y&3;УfU"LTt{8Ua_aiUzܟ!g@N!gE}5L'xKޜ?2U}'4XaQD!zDmY([ACr&YEBoYj,Rf%KuhO+.Y8|q8; ԃ#'GvI:5 fzwL6+/_ GYjh6ݠ (e* L,FRX2Z62,F&{HanydzU LgO2! 7!BX Nu$dStAhhttzžr5KGwDMCtXE_b=JYb1B^o00L[SA 4Qmۭ)\Ce$I$W3DIn Fbm9SN7U%nK ZUfv/zxrQuĖI Rp5}%+Y@644%.en7*؇؇yy a[Tty"DF5jJ5$#g4(kk*~_+!K[M<si"f3t^ 2&X=ŶA{Qe+ȍG&zt V;oF!mu&Z-ëU(*9a*&6 T'x+_l0|Qpk>0D+:![*ѠB4 ɨ!kJY3Iح]95*6UduVneyV[ 3 ]eX!4dTLlUd6byEwɍph#!UPehkC8XZU>ZR`Tx,YcR,>n[>e{̦M/mdlzY8BK_@Bϵ:)ɩC6#<#<#NGq T0lPq;soOk{HW ut:dSb1$Bwr5Y3LI(KyU!4yfh,%e(GD=8N8i.46Hbc9`Àf&ulB S9grtQmhT) U%s}f4h֡>+8עOx%OzHt,vN5K$f*uOZk3[bxd,c2 cRlѝ_Key-|5[ʀ0`JӡldS}7ۤѫH)!G41K2#ܡ[)F]I2medɏ׏!5PUd  j qQ&YPS>`D-ߕ۔ApZ-ZgM?sbM)f !F^O\v5:'vV& Y^53:~Vbl 38rLM&biaqMRԢ<H'y$8]-RB 2 H2yWƀL8C&RLZ֖s#Rs-= Y٤*Vp\xv`= FQfXVְ@8 I6r;rk_W+'l=J(k-d':iعg6 VC ҡJ9-g֥{d^ M-߭0sT*ey,'*A/rIQ@)o:&-7|=x>%StȬsd;  _ z IDATK^A!>AKHG AՅ|Q4L]_:'_ž voz>.2֭[O?W\ 7{^vg ugr( \VAwgvι}s ?q8gÆ ?餓x9#_(Aۈ ' N;epGsꩧzSO}ׂ    ' ?EQ ṗtƟK};ᦛnb~~krs"/7 7\:|An&nJTon~_ϖ-[X~={.GqKcqm~ 3◿W:_97 ?xns"xy\~/| |{s=W)]w]w''?I@ԧ>k^XDnA~Ww}s9y"ww+~稣Bu6l@yg"?17x#_~=r ]tk׮E4ַ==H"/M7I' XDnA>9ż3_ٺu+ԧ){Nrlܸ|O~xG%\ؘnԧ>?qo8+˗JQ_OVAw#~˷=gJ?3j>v nVp(\uUMoo|#W]u+  cǎ޶m`DE*.b ;w5`W/r{9Smt:gMCqgNIӼ=ᡇD._˟ȭ ? gk4,// ۹s'W\q^z)\y#T"{yƭJӡrs"kllnNC[oell |^," `[^%\B^~/<'{T_r7uY|33 CO}>OO/~|\veK{ȭ ?z4 /|iFuuNz牿}<@E{#vAA( \VAwgvιo   {D'  [nSn  F{ ;ƽ} AAد:{AAj'nd   U2Nr!GMǛWth|9{eq~O/#n|>?]E62]Qo[ވqY),9\<]s^C$'DrKL^z^ΞD!KOGoe-Gq8PN8ݻ6|_^'  /`*l~y䫱$^EdJ_c9 3.8 G=9X]sͅ_:t>v?k_翸Gc~sE_7}:?ΆKb"O%"kkpx_GHw~į?o4;w}t:6f/}K匷p _LZ՞z)z).rQ  /H}evyד z=) pS18p9k49G_G֎o.>v'q7 =u/ן~r׹{c{?洿{np˝gx?{Sӏ[3Oo?=KҮ~P8:wǯ_IJ+ܭD$`zӨoW#$g=Oykt\l-[,?o1W]s_+F Co5CcA^`ַǃgpaW<~Nnz9[5lIQ/$+G\y6 7v![$> I8fyyM6z\r%h9v=N y?3ϤR8:@q _r :UÚx?Oq|e]Nڃ..~ |z|+|"n7c{#my|ygq}/{9oN>~sJ?79?Ki a,Е֪)y_Dq u:W؇Kܛ%r|Fl w;XH(9}=KhAh+)hBmqذɀ<Y<=SCB浇cxCDŽaDDqf,[?YAA}f_c6=ikIK_ Lv8CCC19ɟ 4rh+G||w77է>{>1藿- w9]'rS/}_r7םy<v/W>ty;kxnk|s>s?oDn^C_Ͻhο\{q\p۽y=sj̓9(F MQQdHQ0䐖0?OWg:ɟzE>xv@~TG~j~W>ÃDM tMF=K\&JgAVPTnK٢Yojh5;[-q  }bo'?[ֽ;M /`X,b6$>b;.׻v*^ߝwɻyOr¯gE_׿λfn<߼w,nkO?sok8s\9č!_kވߩ'ї~>pjd83NWE6WVnoo㣼Wgf*bօ^me!, -!" (P  !1J(!m?&LBrz9 L>|2LLLѽ&B!h^-B߀.;?9=ɓ^:t@^PU %z hqo~-R7იSp;\r>ܝϛE7I`k>&FaqVΚĺ=E|f>ML/%mR7mnj<;KF3}>6'l~ǏdMQ)/GGbwR&~i^L+stFurv jb}RQ}몉YBU A_E3\`BZج}4*/fUPP?  \@‡9pmӭgOL߷o':ŤF{Y]%>{PD [E&GG䘨zǩx|}ypfpl{F;~e8 uvH!7ž?Hfb42s]O+ݕuJOEGx|M@7L<,s Uܺa8=yoO~إJkQ։-76weEE>aaa|q=zpqVUrGtj߉ʥSeŸ4M,j;a`S<hn ʣ{4?B!ТsսWsf۾\xs@1 }Pm1=yOggPTVGa eBF&icHLK~ܨZv;SDolfL1k6R|2exd.l-Mt6 !9yꙶ㓯5TĨ!d2E25ȯzN۪.((K]g`PZh(*(!1Out@3 \h64q:.Mík5桬5RUAY2T_[tמl2,y4S@ LӻU( xWIu{4\nM4.#B!E_9:7l7//44]b1=`mm } V;[XY7ed& \ }pڍ<3*5X\E2eh᝭SWW䄫Y&eQf6O΢lV7v& I'[\ڗw&_MĿ~EU Ё7j Щq{=qi .~ݷ9|+AYA}(z'w s-R"¯tp) _w<@@44]ąjhB!⋻߮\BbELV@zB$مd$Ek}mLzRr>."2Gj #^Ԙ(W:}sGj>mt=7Tj֦ޡSa ģWslV &޵ITCABmt}P ~uTUAUU{?UAUR(4U.rPtaݼ]uVV r:qޡZE]? !Bg*L ;z;aIz]q\ !BӪ.`jUX`x*1M OܻfynN*-"ml$eL) s|8\EzWLy;8r>컞?2!I{ٜ4"1s|4~Cf|#SbrvG|w~mtBs VyMabQ;[jfi}?3:w̠>}ҥ3tȏ~DpU;}|u=}Ͼ;¹m?{}Ρ=! B!ͦU?]?>`jZ h5<&wyz} =3HŦl/"312I.$sB4)9EdMƒ|;*9r񻲞icI]=t[a;3%N֤,~uoM>];sg1Nm}WjvRrsʋe~]ӏ?saGLB!]*UYs9;FOÊJ\sy Ε_d~BRҸxpK6ouv;sRd=׫9q'B!֟p|-p1"fUB|w/w#~-E7=ܧ3uB!DaA!wS.BVG}B!'B|HB6 !B!ķ?!B!۶d~kc7^Bsg-]Bo ~W yF&B!-Oz !B!ķ?!B!5-]B!z !ע t?5U-]B!B&Pv gЛo)Ӓ5 !B!h"cЛo)qGRy Gue&B!ePq9pͷYg/+-j 00%B!BH?W#̛TB!B&߹*r@IENDB`././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/docs/source/tutorials/images/application3.png0000644000175100001730000132631300000000000025015 0ustar00runnerdocker00000000000000PNG  IHDR~@usBIT|dtEXtCREATORgnome-panel-screenshot7w IDATxw$eO=yv6'`fŬ"b:<Ŭ(NOO(*<A<Č a6N+aw]_gj}kvf Qaoc {oL   Aa\}Za˞σq7BD*@-OAfѻec#E!9}Z{DQ8< @r ܧA sKYƛTǻv;WG|A_% p&o]9yK?87gCiumká Zcd۽K]wlj?CRMF?3`$IBS {ٳy7F|;<>esb}~7|&Aݷ_{uoKfg^={mε3q>gu0Ő8y=~nX}ײWoBRBM7x72>FdbbL&Yg!'_leV^~rG@נZyۯ}ĺ+>p-}s9ʿK_;k>~eG==߮P'#֛: qW_Dד]u,oK]QbDz0#>睂ѳ ؽ MwzA=342=p!ޜm۽sU3' g~p%<$~9~e4[!Lb;e:SpJFIOo ~w39#sn{4l8 RoqůbGnvM_6:~wѩ*|L~{x`kovXj3MwQCKyo_Nzۇx[Mo@9y?]wq?1'TrUvov@Ke1#Ncc>j?/~[8=r#(<Zȋ?C[|xݧ3NAC/:':ZUq˵_1k}VbSٶo+qYk_ߘY -nW7O̬]Uyc8K|jx;X9FW/\:Lz=T>D+y3c{J;Cbǽk7$z.cU dQA] [vg['>kǏRulf٬=qs#{׭an'>khv%~n~s=$tG6@ 84=Š)ݗ_KU9~f) <6]wkrh>&'y΢8I£e m1zD7F~mE) `i|\feD* @\by @O_?wnisؐsqh_600Z6ڎygbo˾#0AvY7&ȻZAsh9>妳OsfsrwٹΝnt?Ib!5;sW}pH*xCQ.yvSAxvu߯Q?0Y٧|}|}Sc8^jú*GBU$f =G.P\t$9[ ㎦R;i`s-a>%[+!N|>=9cIgDZWAf#MgwNͩ蛵g/}).ٺG̿gKv/;y6HҮ?!|s9ccAxѥгiW7pc)w AcW菘7y5?4#~p!huN ŰszUk~= MňS/xoz뻸fodsO1&o<}vRN_8vucUl pխMN{IKA$IH>/"I/u[Ifg=8?cǤwܶl|ds9P$ί<)/?OG_}|.O?sclo)zsZ bn;}{73WF|{W(anbLZ3^&^~M46Ý![+Gnk\y.1K?G߿ )_w{.Hl) Mjv\2n+=|NK%2G_«/MǞs]% yO8-xOzwTܴ}j=is_ְS޴dT*-??Z n?|.I\g~ߌ;~DRX߹/~?a3}-L}ō׳_vLMfe΢3(oߺGXĒY:$cn_ul(9MhܶCڽϒ7ܹ٦DLaPEyQa/u{fM^dϾ].D|К{Q}3Zs o'onsOy9n,Ӣ 5Gѻ"S*(Oa8 Mgyժ8Y:$ڂFDLE$K;%/_i1?g "ow+t]X֧0=۟h*ɆONj"E=*K{Gc^VC,tnD,AʔYԣr_mw^.10NRC iHyQa/mP/;BO8+,V҆N=~<t;gp:[Ph|9Ig$[Qd5l <ϟV|)?dO;dy ꚟ3{C_t6^I%Kzow12:Cwv̶ϻZO1 %6 [Z[jq@ھowC=+Kda CX\ץh:Db;Ȉ!- O]wN3{CAx%{oeճNܧ^u\@f7w]\& <6tCAxb;% o7)>wJG89oo!   p\ױ۴&AAAa?Pt;AAAD2}q_gN(AAAgr~yGw   SHAAAD' OpAIN*p+8'<Aav{__H {mN<4ד_|tAUU9h٢ݯ$?A1sⷷ|'FAD' Ox݄._&œOAlD'<}%Ggt3R$“HA/WզW^>n)'x:Wo;GgD'<)OAF]&FO%;aOW^#dN甥ns9@7B@|  < ]mzKtʉ{IOAxr  <\m  qIV^g^ae]k^^Wկ~0 LAD' %;sy3IE;̿ٴiW]uW^y%ׯk2AAx:I +O|_%ɐx/~=.A(;x њx;?<8Y$~r-[? y=.A=}< xkOGl]u@7Mv_zj AigժU{]%}k֮ϟoV!i>Ox>?;H0OfKTϹ_i y;g=G|s>t C/}D"1Sٟ }댏OPc;&~lܸK0::ʼy[n?!zPXb!lL&.F] ۍv,ڮEKLbj6DU!F.bZ1FnizyThS+ir #ژ93k+*NdD$p"[.!hiBڀޝ&+QȀ8t$p2$b :xBrlbK1ڝIh*8   tL)"T H$Ic<榩{|W風tE"@ Ud%5@BB[)-C+z4#xh=>!G *~bFa|X&"( ,2t!r1KTQ.g${$M L葋94$u)ASJbc >*>* !vd&Ф"(*{ ~[%B0TYMKm,ZXƌ:u_,H2H2vJ#K*XR"D"!Du Rݩ @g HS="="="Pԕ$ %tfJB@88t0p1pw(!!22AC!E۝~]Y.6bc!F6&61lU4hBL۱0^N{bFqk0D""Bt uRJ\'4Hu<4\i3qĐptph:-DP J2AA&jq0zKۡb52 XLJ$K3AG|Qxdcm|TJ)J9JixILT24Cu.!t@zqMr%Mr_Wԋ:FӪh $dd[2^'|@ezf[Eb%,g5_}=d#=ݮs[~|븮˹Oqߺu~?p בekW+_2jr,^5>яw_WRIgpg Λ;7|.7v“M9oO|0 +8c @:oL "CȅV^o>w>o@2$a5IZ麱[ڱe[ %t!k3mb670FF6x-k?OqŒq&8c,>ޅlw[1!Mlq ŢffQ?$k5:!Pp0K0%Ku)KIH5d=Xп,ן ߜ$ߘ"ל t&A.V^W3Y륰&KaMCY<5%$A<떕.rm Ս,T7@Xy+,cma~ńjU>!5 5 5@Fԕ u5CCMh&I-qE}}u X6Zª/!~M+FQig J63M Ecb.F%4!ID Xik6ࡍ+3*< gM3L0&aIJ%@(e`*61&ĔKeX> 毡K%= [҃D{I0;ye)GYSrT,5%M]MQSR4颛.ᢙ. iksTdA @ޣ&9j">~>:a~m>m~m_xE8;p{r8yܞL:7pfߧOwimYݩ9&>$> >D2> PPCX!TO.o7rMaiڝ v'MJhXI +c%5Ȣ[M"T{W3#$}\zg?) 91t|%_abbVK/搃]6ݖpeOqOpַ<3wu7]pA:S\viZtk ϴqozq_=]y,ʕ ===]/i3>ɡG51NbbkbPSzmpOGhI4TH3F!QhtZc&a|S!tIl"[2xIkj- EvTGg,DZe2y` g@'2Zui|Tdg|*G!G!-A2"vLƨLq\J.C!G4U)[4hIN۝&i#_`L^ e$ASjM!Ӕ-ȑhu8(n@SKغEk*t4@qDdI6Y%(>QۃE\B"BG$3"TdMr`&d\XbZRS136L3&fi-^(dS)sDDfNF֑ŠbK)Rhy÷1M4E4E4I4GH:1e*&H 6ZVe#jJdXU {:i;INo$S~NDm$)Nh \J<'&&".3>zG~7ɤq7Ja|֡;%v?f'Duk~4ΟWnDQ?;B:S:ltr,RY?n96$Xtv9<;\mK;qIn]WSO=SO=Q-P2Ӊ'uNؖ**): iJYҫ,>4W*2z)Ħ7! !/l188ƊXr_fi>lFr{e`(GkABHrD>[bGV<8yMBy<։|9$@@-Sw-nk(k>)I dI;M@K=j=Vgm| uں1uO21 #(9rrjEnwܢ5J*R\l`wcU.loPa6kP!c8RNxLtBӰɪe)Y"c8L9H45@c ]E#v(9nt\`Kcg*Fe}8AU0#*({Z}r!dJ)7 6&*cu(ȽPE؆AI) g^0\ i74$z!kIj,װ4Zh-ˢl,bMykY; qɨe22B*8"J^y4D^$|MTEJL(*=t( èSRCn& :؞䓛Yp-r߽0錚L$X4E#T$NoR+&۫F:O&m *ELTYdÖ%-x |/zgOwz<= ;$d_3pE_c{_g_u~8o]1ǽ~z#iz6wټ7$~{봳ӵ7z_ȏwYe/%LjY1W7'']۳#$'@|$^J8wB&-ӂ^ LLO<5<%6ER nQsl2 ?}[DJ/:$-lx n=eR~0SP&f7%DEbEE!P 4 uKs Aiq"-4ҽ3&b(:dҌT,ŠBOja] QZE5Pp:j>&ёb0@Ǣʃ.@p5 TMIzkKe=DӻI{:9Bo0 XQ%P| YT PTT'-HF ȅ꧙tq< װC61rS .HP@Ρw:ݫՐ^c09ƒtßԨi&CDHEwQt SmPkd2y Qa::-{-cy4&S0[A*DfltdM !:2a"G."f6MLDoCv&qĤ[2 $G2&;m햁@'Ұ1U\NaPi橷l!=" !4+: !mWݐXhNBnb$41U'Eѩ;1ɨ O*1J-?F[ђcHJzhuI4 gW3 TPBY|NjQӔhmCz:2] $(^xL1)8 NQ'g59 *| B15}2Җ:r:P,!A1;FB j^qu$!\D:F RU0JÂVu 0"MnE>Eo}1ߦ(Gs) ,\` (;ֳs.qp9]L͂@`>SS{봳ӵ710 _{q+rW:3`iq&>ssw>osfd<Zx1ffCH_88f!1K"&&2i2Д,BEW\]c6} KLm<4<ݖ/l\N#$(l11a84[PiTTm=z4UFP0:M ښ6"tLHf^̒z <[m[ ZQb`QQ"N&7c=ʂe$@Mm# |";C`w'Yie&{4"e YМΔ*Jk)wo,8sSܜb*jͧcxERDLF1EJmcƨ9 c c 2uu0 `V&jGt4# ]$|G@t@N7K)RxU&/KOҟ?>N^.נ65CmkiH((H艐=]:X62aEReȏsrD/gd|K(iV5 c1c [3Խ,0J 5E2F H6ַQyDn\mq?c_؟eS~&zfd)6cKqi39cvHHoMY7NՁ;K/]9:ض+iY竗|<,2<4i̵{>ŗ\kOz#,[n?!O$OOS.y2!' Ux ؚF@TGWM }Ц/>I0,\`lX=(^PiUƋ=47&٪'42I$v'f*"* uf+шRJS= =FT7YԶZ>:WleA[IP3Z&Dx~ m`,,N۠ߜ`d>[TW5S%Jϑ7IldVNg?,a+G$Iam43$V%F#`#>ڈ6s1t[4]wla,وxHo-Jɔ i&ӷԎ%3‚( ӣd )Nʨ jFatuR   DeNR @t A|1҉*K{0n 0f2n P u 'tON;EKԓ|-zc,MPmO4GhL܃(eT^DA BhҚHPО8 u vLdiާѸ_#p* eKeՃ~h$4% WUc؁E,)қݢ4н^ ?dæ8Th$ 8#1y&dH& %B0Y͍r?ݛ2'KLleQ|>&||C؅!8+M)Pӌn^=FYS8݉a'įjZx25qT\e1&R!dW$HB.YfYn faU R2&#6zpG0V1:T+QF%EN "c JErd,?8H߲&Ç6 ̤O#0,.qF]k\!EL 4LhD#9F*O#D%ed4X@V3+hS,B\x\KI' _DQ' p#IfZ-[k}n_R~5w}0 <,.KDQ'?il7Nc9X|W}۳.?c8cvw}]i,|2殶S;IБ5Hӷp~~ŸO!Piiq*J -O5HUo?{o#[^##wX"[-2 f ahѴA 6g$ )٬fU;ߜ#c<"ؔ,jBM+3yb8'o- >)]㢶&kq4.lIZʤ#MmVWxs~7Q \WuYg%fK޿⭟;v(Ҧ6Mnd֡Z;F'`_~mx_aS(#ni&q1kf%{/x;8Uy ڼjF!{_ 3z OK0ZyPuj>0%')S3f:N=JHFRؔC C4Lk l0k0;-EG,,Ҧh}Ϗ~i[#dyyW+R D37EdY`+B.[rvDKid7-jEn5AcMpӯR}O4HsG݁#x4"Ed˅>C oOy{+ѿO(ߗ?ƫܟ?~_֏g~o ~g>ƯdYm__/s~o|_ongosf?կƏRu{?s;ᛇo`nrMӇtF?^sP;{`=O}t:R*2ٱqǓgrM\s'`hL;3Ր{=Ҩc?؞b3eo|Fr37(1 NJ$j_]%N#GFOljqS+<3C!h0)pIeifě %=g[`[%#cʜ ,ѳ%qnҹM JvSw)kuFhҦz2nkgˡME]D,#Erz9`{$:mbQuFw#-02g잇]5UhW¶yF{lE_)彝NM̢ 8Q%#{^0TV@{CA wR2hVl[+"Gpv\cd5!fcl m-7{TeĮea7jHM;梂.dcƚf- mNn!9;p= ?k쾉5( -C~i|IFVX Dn` wzm8TŦRIBx$È͖b`Z+t#2'/]Q91GAQZ'X'xS*g+O'wQ&:bYe[4 ʁ@*=H ?YSn)t {xCY;Z \h<کCRήEynQ/L1L+TD__3LY =TI LʎX^fm|pV̐GYpNlX4IS[n6t 05< (, eŀ3GFCf sM%R 1hUM2rԨ(jB=9 V@yw}nPո;PF5 GRu|@VTKuvy>v`\˵+ zL 8BASt.1ZIY\Ӫ3?}-|Ei+?_SG9Ώ77'_/(Uޏ=?wj+,?XbnZMg~6YXs\1e[};h//2ˌܛ8axr._w:"9X IDATc't}dVV RdW?e wV{œHdT67)7{\uޚ`Šf.W?$]-s %o_vަ">&-61Ǥ&c,>ٙGKCvaѹ&#] TmPrCF$EIA8l͈DN\\6\,C3CqiSSݲV ; (˂.֠0g# gAORshE2!l]\GtK`ޏ^(z9URZ!u8OQGO 7PLxm`>8Oyyʠ>ZvydEwL`P1uZ2"j+@8!٫!/ <|FYm%qjtS`++ r gP6K_b50i456;yab[5*c ̌mV){5 W WY{t^JxM(tnIH>XE)dzPG| ^ifcAd ?O\@[q(y~wWXg[%H6W_nvxypϘ}x#^.x|G  ?~S,}Pv"j B H5|HK/@Zb(K`:qCaISAXajKG\9=,jކwzO8(PjL <" F5Ϛ+1dɐgu[JWM S#=wI!mpꚠK{E" XSjϦTǵ6a/bk$Q!Ϻ$ТhP4w0P{,9skrksP*W@ g%x9 eA$yM2AKZ^u]xPJ,bpWCԢZt8Qaz·Ɓ{XX)YaCtܬXvk@d%Z *EX46If#.4#f+y@>s9(~myV sH㐬YD@zy;|η͖DbD-d~(o\VI.mZ'܊ivĸf:b<]4ZHM=V]V?Z1%?wA.\r 4Ю$RTkݜiO\ ܉@ n a5rV;b`ӂ5 %můQ5L3cM8K[)u9{i'?ȱ~s$h\Xh6yېƥ-]pF -R=  ؉Bs}Lՠ-*2%_;w;wQ~3r线O1 M;1P}(L 6ǚvcQ5Ո^=1y>$.ׂzwqH NSEVM@لMӔTj;"ۆEChDaLƸytlٙNVk(# IG9'./r񣈿ڙP}?/// wq~H~#$18QEg}0 8dbL?l_S["taܯ+F1A8XPt[ H@& [HuaG)SG 2#x]mo<= |ThJSHDHW&ql-r]j1E?Џ}c1)Nk7n=qqp`PCpB) x>{%gc/'$q!wtjT@| 塔@Ns ډAaI0X]d_ִ_. %'%'DZ{>Ǜcdw}'$kco7f]0.i <‹V'+eYNVq'N{lI$,

    d`-f`-mi>y&S19 FEXԇ:N_d۵L+l( g!-_W/op;~? wk~w~h0&tvoEvvG p…)YD qdN/h[Aֱ!sQ [j؋3~bGoXOzEZl<4#|n_:L]Odҧ״1`itĺÙ1'ƈ# _..>[ ?_~h9M&$Į++?P.? Y g+'˝肮1JZ?ܧ3 :KtcMtng>SaQ%d߯hYd}|;S%cdO9]v_vA^S_\ыVkJZsj% ΫyZւBdYJn"H, ]dю ҢUc$r. 2S>zOW鋟G|zZ=4CoҹĈ711:إٳor89xoU)Z!rL?Ahlѣ):TtAFǢR!?tJgD\`;" 'S%MG|3@P-M>֞N5ӟu0@Q6Rg6[40׼ιaV! R)|(y@G>D넣.j{1gLehSܳɤ nKvC[ Jܮ ZN޿[h F.oytwVY x#,"bтhIR1?v>!q< cs} cƅbsf* ǽ3xGLmH.չ+9S\ȅCYyrA[Թ~O|yu-U娦$YB; iEjO)-ŹPAK2ph^ͼ v &aVuGx7O[(Vjt:M4^tx[K>p8r)j$xIÀyv%W~$iUK݃ꁅ\˄|Nw |q}OXZx@KaJ!R,P |'ٙSc`bd:i2luL,._szvzգ.uP\KG 4̠ JB?Di)Z!n9ۢGXS" , M ۡ] RHGjU)6 =mpgVTG%!1]Hۦn\G' }O4t4R:S)kK!lI ʾM9p(G[ Ʊ[ I µM\=1b1o\SM\Vc$r)lA+*hXOoNtv;WѺ[-Q^!tF R趒*(z&@{Ɯ{uO;Խ/}rHD@c[ f^h{@iɺr谼G-[,Fcž$$) <4N\cy5McS6ucaoñ{N]z9b Y#8Q: h%yf>EY4n55Ψ`:fwA(.::Y_U??X4 jDMgW `KLLƲ0)/|6PRfԼ&!@uZnkPlMsaZcq4"=Us[4A贵N]Rg"qDɎDID$ ؿw+lƭJܮZ; L]11Æኣ I7\I-mP5Qa: !ozIjDxlrcaeʎ:!iY{§0N?^?6["< ,ZrSЕ5yw=Vᐛv•apЍ2h-tA)=4i^ Y%jQ=7i-FX4E]Z4EÆZ]v`Ql\F4f9KJ1-~ӗ[l5t2liٶZZ0\rh^u9un¡ySk- v8@NM =4tp;o|s?YU߆>NWMcr~u^Moο+q6y>dy>)R@B BQ΀H\h01G5Kj:(,q(76B@H!F wT;] l[f;kgv0nM?6!9)9:'^ċA큠uFy"blxANGF@jy6ڨC h 5s<3dzr\@6:K$l;=d!Y"sDl7b{5pz:ΉNٺTn[dǵأ3L_p_5.h }^uu@jImiCET fc ֎Y-TPYdVQLS58v{/'=xP:uSS~A/D qy?\'Xެ?%d9\bu-pX֐; Cq]2l6,W<͕M ohUmxnASP Zshp)|FQ2 ݅?Jq`XCU§X4(IG4i'px򈫗MLC 0孃/Imc[6=ʥG5 jTJ\{px&Ю KʠѥD{Oy/9[fu^/Oh"}֠ ,f`6yM[tbɚMTdO}jPl*!{ a2>g9cM=6o kc@&\N3L؆C8l[Gi[uo [T41m-)]4l_(4B8LA*J$]dpd9b3k:Dut˩46uHzYo/Ûf{ ^.ՍKvNh'xW' y֩i1ⱡǚ1lŊ<-n؎zEc79#yƑ<'*פnmy:1i[ Xf6 &[ck[Q3j lʡz :p;o{u? ,}p3샒bb|1cٳ_SRtYoG_Yw$srgY`c1B-|k>œ}A1hK)ܱdN但ً>C~5$3xęsr|CfVru:ZzXChJk6gHV&3s;_ᗼs~ɓ{|(]sva=htM:K3ÀfSZ~`B(Co̶j+)h;)eGb LI/;2鴈M6uqWXb &PBo[LC5Kt!}V eO?Y{躎>갭w\ R`$i:i1S B*AAE6*qFR!HhIhZdD=P.p 7?mp'(*U0`MYyCb;ZLG:ܬD-t AhW+ɱ+qژ2  >JwD$ĄDr'3F$RHnJ%A7U8%Z+b^DC /gGlIµާEDLh8A9ሿ# UdUVFfO]ZIpFAذn޶֋-\0kYXĻ'0pbޱ7ϴFTC.4"m;4~11eO#}݀F6WCW4RauQgE]cG'x≟>?Y\9h ;f",#`xҌ`W劮XkzrhC!^yR Aq/̂v=i''1^ݵvQkư=gԚWN&]ۉͲ2Y4GcNͭ]s}m@­;bV-$Uh_lN=v ZS61$5,fe}RDJZN ㊎3Jd;DGyco+Ĺ T&11> >NW|E5 ;m; 6Mrm,dՀ8>8 XDKg|?}z-ZYŰb) A7?jo`Z%_hLh{ѥQpHXRQ~g W6jea pp8c7k^@"3subuoX%͑ ַE*" ]R%G)88DZJ^z-/X]vcI''/c@aWI}{ I2V&z(YKLX>:lH7.ND3Gt Ybi]GO6w ^"q)lhnk^FpA{3|1`;bux{v0rA%4Ji fw7Ň >^?û<>:lv%Ϯ8-;%V%31cOD\l9iQk`T@yZ#>g/lqp $F?-(Y( K8U_ol^tY;fVٴKI9 ]>rrEƹBX`:u%$ۤc{ Po(VEKMYCH JL(YZ%" ٩AA2DŽYNMEك1jo+ 5ğ`%2z/3 Guo,-x% 8zȿH*$ԗ&͕>x/*jp_=3&cs%?w0R"l'Z(/apM[zƒE5[<'7ɿ/ qFטǐMY-h6P ʥA|kPQ`0=/q֓{'xOV@K)=8ga,KC5YD]D iĊ+Z5A! LʹՉ\|[a{WDӐ0p^ eMKwm754xԟ9D<{{ /wJZZi!U7(OXθJd8w'/W&e'OCB-Qm68^є|dwz'Y,)LA=c`gȺDd kXVi*ԪAFQ[ [pz5!S.՗j17 >C9_A1"i]цV>hBj@_kdڠbLjܤ3RjRRhEvV@ jvcFmI Je6.F?H75?d8:T0ΉACDV]B&fAK:Nh̤7.!o.B5B5VhZQpX)Ynng4BwGqr>_|?%*O=>vutMq"qy9Qof Z3t#YF,*jv{IŖe ]oa9-cǀ%#؟O!,*aPIJ8E+Ӄq-]A lv:) :)k6eMF//` xjYLA60H_٤8ڢlL*mRkEI446yS?EM6įlf^M97h>rQw>2lGpY ܂w ӭUbitx_Ŀ H/]PƗlL]6k{39?qla?.lO3Ox*5ѿHo*ڣ(5 3,cRQ^? ³ٞtP'5ʼnԵ;6ɗmj\Yt8~⸽v,G/B"d9_—5'W\膁5%Ť'x'OX>Nf>w9QEud .̲1|L9qszEm;]6[o,I*ʷBȄOobR^GLM<~:anhO.kgYBkaMi5J!f˜N6B d!P*dY DN_fTXT[I^q {n6AqO"9;@㰣R0vj{Ǐ>MR,>ߠ uZ!ӊhauv /=^OLjWѴ%zp0{8뽧ZcbRMM}NjuM{^;&|Wbw-X+X}X@l$[ ϗX1ގzzYL<;Ff +v(1 Αn>s\۬XZ}VVBC_2f )%EI0h1[{+D(M8se7Xr9%Ž`17fkF3eNS:?O#D7Ai>Cn;TA#k\O\ùwg7t{5cgA_4nXm6tY5]6ê[d[dbuBZ{ʯKʤ+'bXv5*;BX䄬qF~fm[Y/lVvul/%1>`ľ !gیXX+e Xjbex";huVkwYt',_PCuo@}b-$4ɾ YXng(>?GŴy˦-Ц%U^]u<;ȃ O\wBX5Ji=@.-?`-ԋ"#M٥5;jpK!¯owPl/:Nqϰ KeԎ`{H`&Qxgc6 Ś!kbM7XYOO'x≟.?ag<'YEphU 9G#a}{EئycIT`pɶ{,1B ~iAmCoiCy3 3"4\\/XgK9 ќ97M !f"էd@b Y:~*㇎_O-}\n2w3F;L3eQ*1_-A>_h[`,^]ҬѪM5}sɅ{'8GFM *!Y{z.X;8ɎfdeyXkeK>pa~ ~UD g4W1kʭI C>Sa+Mj ~ѠiėeGrm`_R6¦xPewt*1Pz(-I(f#9fr?2jgXIDgp9c8h5:~~KBY qjz'hQ< Q !򸢩 Ơg_dц %T*ӝY_";D⇎U뚫 9/V{yտ0*ߢ4'`և`2oqc̰`wR &Iha٬;FêIKO}2j\-:1.bDov{6MP{ڣ̰p^QԑAshXN>X2ib\:1®ɎҾK3vʅ5 ;>S=UrKKnd-slY`IIg?,~'xGil;F׊vLW)&[eu^,M$* );/'xg10#<l|&UI!hSBfB@~ حC&zh2g7Qd`T ?Aĝ] Ip^G5#9%i.A"'qxE"&>縻ukqux/W!OLI2qjCyKf0-B4&gg#fوy6bwHܯj(jv'1T;U8b{ގWقX:ĩKt( OF =0ahRQQ/(gY}"#1|#J\caS |kO6Ia;Z+0 Cg6nYajcFV#,R,RL2̰Au(9D.45n|0$jE ; H!66ؘeb|1h7<=GL1I0YҊsZaN8]yuѥyIsL3S8ݜ7lLƤ&y[-E2&1eFOlH]qQVɮjq~ZW8N"` =aew~}Pm@c4vSI8 .NnРXQ?d_Dw!f^28Zr<2QUl1`0[sA9E:t:6IF^5I\kmt#:݈nwiV?ďi^oX̦'Rh̯??Ywxt{.y%$`6`6`sۡ=h'h.q; n`ኄk=>10!a):Yد^R /L'Š0+<&EiQd)rY}+=. R4li!B]r*3X\ZL a99686Ȏ}ÆH6/ל{wt.VȼFӠic'ވ}&G'cS-8^wW PQ֚0ܱ\vY]wY}|t𜄓g^Q%b}b8DET(BB`{.h_D:JzX U!cny:+ j?[L]rҥ|cC>M }fo,yx~dbL2'8&+&XG?Cnaq?.G `m{"FHQ$X'~)]P4 PC`4\Ҏ"VƐQR&sRN*졨pOE Q(ȥIJd]cal!sbf6u&RИ(h1w0ukrӢ,r"Ǣt ' vT68%+XzD"8 C`lVEG5dMֲ\ %;> mMa4j/^;>P#GcfT % %v'g:3L1Op)n?űRqr&/Cg eWEYTh-(CeؤGRy,7# c>7u-ƻgD$]@vB>ll~l~LU,6;pl%g/h_ُt=o?KT?#=?AGl϶OX)$$92.|5d &χo|W=_=KJH ZPP:ְcbr6'1W5er/mM=t1}^fw梅DXgI%ҫٜX[l>Y$6Cחܟh;tKhK8/l#OVǸ7-G{^򖡘BF} =oE\xd[y'dO{LY7vG=ld\ָ Aç\ ϓxg 9OQWzFrSg/+$Yح;*8 \+kb֣f,F*[)N; AAP~ʐf±沵ᬳc'CP8xAw8+逸"R 5N؉[qpg܊6? 0* =o`Bٰ1lTaCF*r3$}vCaT4l*p 3mJa^wlxW?}QW f=nwEٚn1eS|[Ms-IG&R\VEqY.k7CEwb~z_WxYdsۉ" YW s}Ɖu(0eEA-/>GkZrG^=xoWC3]fm1| ƆUŽU~H1;;lHN8oiWrAhF&Qd5N'd6 vn% 8ۇPD].^M K² ,ԤH4MC"C(ƶ"v}`^ZB%D2/i#ZnD[F;/(Ku綬PRVʀ{G30+f 퀨 Hb8 ʀ}-#Om+h| ,j_c EI$Mfiu`6!] ͜~9cP9v\.YdKhu"N]5 ǒ.G:[ hx%* b`҄;GΌ'WyBB@BC,`!8y1[cWjz 5p粊wȖylMxķDCuB֠D+[V(ΊNHYM­s.oжCa{((k:{>WN#NmdEdL!9:g쳠K ^~P-5ł63ZbF!uֲBö3`ngiլdP!1"UA$-u.u/U/-.C)ՂFUd)dv\2KPm&>TH6 酇p+jn>YiT2pg eVHK cX<(O$setLhzMlHӕQ#zKAݗ;!:C~aRL3CWK4Q Ggr|q̧V].v13Vjy',dk(*0%ƺRuǡ +vZ. "ɰȰ㱤CVЄJ@l{ kZŬILLnDk ɬ1!Q] {g>[/87x8DC|VQ5p̪G \&Ok1hY-$: 6SY*NMi+̜G4oDê鰔ƥkZ"㭈SզM6!-ԃ;~g`8F%Ŕ\*՛.oT- kg&kiQgq1yYV,ws#jnj_^hR.ϵ{:;"oi2h A$߽w߾芞b7r,e,$r>uıb)9zYC7$G\HI@aL!4BѨ::eW#-w- aPx:C6!+l6XJFO)[:q5[bc7UD[_oMDE{\1f.u<ȯ9gٙKug@=qN#QyfKS\cg1~Z㘃Όg3~Y.`V BݾBc+{j0J Jȓ<(r ? M)i!;Μ֜Q֨JZe%u@3P 9x5Okڝ7 ?:'XLΣ}08L`1.1цJJ{}IW*#_ lN??>pԄc޸~Ֆ []r϶9f6a(UT lg*&K%V1.1AI3WS> jn6LS ü#6e b-سNz]]:jЎ+ `;=gîɅ853"%h)^woxacmp XWc&1wlGzX"&\cG( :(:UЌ MQWzc$K/EEa>szĪpZ A:҆'<@s`ӡrքj\^!Y~/?A=W̆}ftC%f,Έ6GyL1:NJ2fb2ZrPm&ZWW$n(Y#ڴ8 EcRycP5릃. Đ%ђhEF$ ZN mo1Pi5Rbʒxҗ__>r59 \C49=GuTF,ӒEI2,p7p 7'W@k -Ai UDKbl 4Ybg5u糀'#|x=w>ބfG 2s3$Oqd4_n5V(jpaV27h'zR#%j K`):QpϏ! jTch ,cC,) 6g.8Zq(Il&*ͭQB=UD&i5YM񨢢@~/̶)]0Xm6ӆfJ٠2uTR:k$ѲGN[OH7 e )]J:E6lgm+4+(h#*I#lE(i_gBo荂8O&S ~&)Vc$bF'جSn#S5]mF jC,λzHd2k#ca:JZDXwtWKvKv%S|+H5zay;7֘1XuR74Jeh!1V+כ1&kvKsº sY(y*J m78A ҆zi^iԡI J%iR6dѺMKc+W0G Xҳ& ĊR:%MRz>U#7*V#*rKJvB\'ϔS"(JĘĪOnX,.g>fH\H3jh24Y'>q䲉lЄ%WIV譊(MaJan[}Bp6: aXؠfPQkPczМKڂ^{kETԚJX{£Ysfa#MRav'>D(xIH{Lڷ66&=vHsZJBT[QTgk4 }Ѷ(LDMRĕC"m2ŠT2f#}6MYz wc5E a3t'=`qJ'$³ T+ ^o0\ccNIp BHJE#Rԕ%xYCZq(U|RPmH2Ai$"<4 mkr xjsƷ찌dKTfjC1y{F5X`҉!,ٰ|%) eƩb4h zG3s:,L,֧.q%ox aH C!rMRx~~Eң B#BgNwqxǃ䨢VR$I|y:msXA(E7rB͓,];gvO9잠j&  vN(EJ4#{ Uhb Iц,*la[ltIW_sipn2TvI+z{F P@nIϘ20 f),d첒Agsعd,.4j4j[6U@TZ 1!%ʄXwÜKYOA  ? ' D#i*u-`MnᆯLr߱W;W0MHIkTe7шxGM$dHqNXId %{O\i.]۬=V*M v - %5 JS3(O%#f2->QYT^m>@ky*әgLdnXmB%1oT)YX0}M'h/t:+uc'}NlCdpP.bFElRh,\&}uB'6g~SrCC.a[bA[ 9TNo5>ZQZfP8:+uercwP%KjLkZ˲hjhr~ŎsΞ~Ǝv@N4 !DԠ/aw0yT_ď\EQMQ눾ҏh&KĘygYYed=O#@6]V=czU҄r#ÌvEv\Ά>ۃἽE{>kFYhUAVA%mLrR(j|wG o]^,Ϻ<; ^jX"îSI>+PjVAH֊v{Es^Yvrg {.ĞM1)*M&P]VhEڂu!& udbZmpGͺl!fx6íFsn~ Z ?)TYfssʼn<#TyHN`-[9XVTvޭX..].2BhP)naˌDيⰢ%;!c tP٠z)~ؓAfd<6I?4c_M?#?{ޗLl#xX\h|~/gB؏zn?)b_qS\J]sϹ58&V[DO"`f#(Q*E@2 H;t ugoOs2f۾Vi#at ڶ̳aUEPrihY ȅI(ۜd\cR [#m^c/SZQtƼ` JPf&%R}J+bE!oRd1.tJ#i,\E0')c5.pt騂VF*ewkE0_ Y  k^鼇-u/ MSu(Do7Np$iD1%m‘zCrJU45J3k8S=s7z)*du]ffҩЈdkF}'|6O|]<;֒[o-I(RZ"PtT[|5CrGZ'{SxS z6@;+v +O-I2D!]:AB/HX eiftl:LŕH)'?mmҧy 7+ pBV\ټ昢x~pf@V6x6%Y%Uh.+yiZ+ظ"pPql@9imoQBRAU1)-Z*mZ & \+@_^ZyS>eyߡcnޥzKIJTq#>i4 o9"CL=urRAU״AC+ u g[>ymW&ymRhs6Ec ;\.ǬDQSl-2 6`dq}DϜ#N/fXha"ؤjCm(D˕2`Sn{ԼSV :મ)u!mئuH`5;f-یkz7ϱ,_DENYCZxl>\h$Nu>+QD628} B}?MlfMq2 krޟֈ;,ZK:VkMc*Ԧ 3meDGH.~an1e X\wX;d*QƌsC6)F7#MoHN]Upu͞qvAT)Wv2tDjԲA "TFk*tJt}bjU#PtMh!N]r=t*VKr,J$o~*\ʐ1 ga>,hPG 2E>w;,c MQCw$ZLiqNsKe+s>{6k7I"dl T=W`PI$MEcr[J$lg.͘L/jE6zk)zZĺ/:S`J.{:6W$L6MoPo )2(sxKfV7C`x'~qs}s}>"\5dl5SL#4s,#c^w88wm \ZE)&[3"H2*24lBuF>rf^*P>wcE~S?| _c/;|G>?$[?)L( >#/~?/3cG_^c?r}=R5?__"N¿Mg~u~?gw 7|Y ~g%e1fz@{:3Vf@[71`])1 r[[PaL HE9TPT#漦>)hSIJPdb||@[B# +HA\ nIp78jRp>qX~ Dܾ.[Z+F\CޯRlұ`c #pDƠ;ke]Hmk ^O5^jM͹Oŭ7='| Fc., sGܧ U%ã ý)$PVtXұ撎@*\xD9 ?E[x\r4m W.FUSC/BCfU,:l<)Gw91K˰KM)K<5BM^ shT{'}7!xuMjI=3oIVa!uO:DETąNTxv`~+rlŃ8ؿpcwsKNVaJ>g|8{*;jDa>h`0j mX-=[)n"EphmtY h ֠X}1ԺFL~=rHwT9KsZNLkMߟwxt}O ;S4;5M_,!P0ψ)(ѻQPA.Q)TE)*Ɲ XDŽ]Elp28*Ѣ-"f4r, |("At-(b8RPo:1ɱܰX,z]򾅰)htF(mix9С! _`Rw %NlgI=@24pY!.ȋ5\hfU=JPeCʍn>kEjyLdg.gwͫWlr0v#0rhʂ%fPuԝUG%Z LL2J2TblBZXMN<"9p{c-CJݟ=>W3oگßg7~WR}O,OH)kO ] 7lÛoͯ/tgyw_}'|_/w^w 7˿¯fr9FQz?*62'VY][͗2~EOFfE: p!A[%הh)Q(?BQ pB,@ ҡ,f}VWinդ0g NUZ-id[%+]nOϹ<=To5{C>W=K=q#l#*vŚC{^9LqF|d0Zssc6+-xfΰ.)mUeA]AZd<1>k|$o +XR 'ܾqsŸn+UШ*R \$/_ؔ/ [gdĹ˰ 642Tʭ%:O8..wICf\opNpe¨S]S_S]ƬduWXw&b*Q& 9[3:c&,~Crav\2|嚏?]ڪm,oۯa pi%QsS:ۙ|O G:8$߂ieNYrw}+͇"hO:Tm6ܗpD Y}L7dzcQ-5(h4S 1ѐStb1TE$ZTmReFW8`x, \E\4-"SA* @ lI .XAslDw oܐ?2IcNOOVi^ȇ r i (* 9s3R6b/t, %'+&g6gCA<ʐq5``+p:>h>/'"ޓ$aVyEY WNv3B:-8ϑQ_dr4B 2b?~a fcNoI,E*_Q}~Om>λ?_GO^<_})~:_~e2LO=/El{#oֿߗ5c'kFʰ)2PR  ڐ4 wTcܛ`G{WwɎrNq^v#!h_ecpE5Hh(ʂ8 !W$£ tOuMP-Y%,sg%\>frX ⼍Yyŕ9%]4.hk!Yeo>G eIg$Ȓv~-KH|hp },]a;,쓺6E U N:SY}u0򑙠.K[~sK[#Gx^w1uwM1Թ2G!a;<|VN5WTFar׀`S2,3,*K tʱAu\+j$~+GM ]|p*QE}&{G݄'onsRq6?Ĵr,323,3e&LrwXx]W~ȰsQ5Fjx]~CZ 3(߻S oaʙ"K:Qiu2 z$`G%7y*o! idVCeZW+twoB}$=ku\5^NC0kTjTF5ב@/f)=mξ8VUZjIT-kG&i&AY5R̢${|F8 o3O53%PD%*~qPɂ\=> lUcXf]PKr=ORXBG)*ԸL,q _&e&q(NZ"ٶ[ɰȱDєeKHdatAԅK*X<.QR+ Q;RYYF" l5qmTCNANGC&+i9k /]hMA '=zjUkUުzxur-ߚ| 4A~R9(s\gܭ,>\qo+U > 5  2;1w78Qu+N[.h-+˶mj_1A!vUzMǽ[KqZmER4-muFGI(tsul,If[mG4[ZQ4T3r \BP 6m&Ų΢uʎ<2޴9{-ۭ5-XKҩќ^U6dc >'t As'ɨWA r=<=~~?E 'MɌda^ج.\k77荂3uT݅_lG CgՍP9P3Aj$*S VZ#* 򐽸 IDATW\%4ڝ Ǽ`rrI .- DJ߀@_#;Tl|"%M**WwT΂#΂cNg\Gh RzJM[T4%Mu $kmx0#)S5&^&{Wlu. ^7&مIzib Vju`rj1j>d H1 ]Kы 28zn' (wXX5T5GQDQѭp\BBA$Bj@Ks,3*q8Fc&8vS=O1}6[|BU.M=֣yy"*\vC@5eԻqvX5lVCn)*:rS>ѩ%-sA e3WdU5Im@u( u©tX똼75 HdtFiXFFE(Dl -jHÀKMخS$m&xjo~a&f\X,M]rX(G2A9('nAq@:4hVd8xA71tkuYzh{{}2"u7>~_w_/}]\^r|tvOa/.i[{n߿WO48ƲvN_^Uoկ~}s|k_7c[n[OV fHzΆ#} mVc_W5ԃ5 U*ߙ-gZF{]Vw;~Zo5hQ {/+oj&zavPH^SMi~gH{ j3zڔ6].'k/U*CРLK0wwXߠΔ;<|g./b:c:j# A(BTl:$6n FV07 qY%D!-;YESPoisdBHr #w1{Ki7w81I~Ol2!R, N?n{xN2q@ ɔZe?"ۜx;C\+½b VEUdz% 0[̍&SOH@2V2 ;ePW\1/Z,&I\Z C|5T 2tTQlhI&:DD&ft`zqӋ B3H@g=h h}D؍(X[\ͧX3:)wT va;$ΥDzlpc6>C>=~k.!B 'ɻQkύg\7>>7l&)%’nሽZq1"zSǯ䖊RM0(Ր)dg,@iJaD W19:ucCPԝ V%+ٺ֥ZX϶z<>b~"_dN 2(S(7%]h1-/fЋi\7adb#W JAٙ@kh pvd@RVwN Ri\S8g LrO 2hJNG]pW9ESJ ;ǔ`f'9oBB$s&gdT >ʒ{_0|4W\|Q!P\x\-QVbn~r?G=(cbF9 @-A\**TJJ^r> S|IMF5F&_.Ez cmE(/o}u~-7&_)%_/zu?ŗ_2RJ_W_O)˒(^m{g?o^oҗ?/7_{W*"xUGGٟY~)%_y-5~ւw)ۂunjKpb%\9L:k\ލBKD5 ٟ4jNjd82n%H@mi$j"LvmL3H4HJeWh7ׂpG٨\+PzfFuMOK 6[BiSSڨ)6qdn,UVe=in1vlzWHs@a Ԃ<"ecRP*% :(%$*e)UJ 2E/][f$E;A[Q56E<ֈijP* aW"5As[DE E";>I(T{kvS 1E)JUQ:B.g1y2i."SEHPSz1hmBi % DdP!jQd+lC2WpG qITĥAZNZZ*Hc`I(\J]8 D(@|m*e9^kKۜ5T>l M7&+ (l%*#"*b*23\cU)ЈK wXM~ Ro0sL]Evuΰ>P’$4C pF:鴤X.a#Y)) \P*2Wд|ܪOY,Xd=2i`Yn}KOIIqI(wcM3r e7[k?|w} ~?K_;?+CA|/OzW~ܿ _|/pqyɝ;G+◿_NO~}_#UsL&S޹O[_~ߺ{O'''[n及O;<8%tևCM#4b}rrg65~L5bT愼 WAQa7+hwo86y1$nnI˶&5VgĀTas9S؞DgyIrbPa>~Jxfsu'8)3csvNYSMYyd "1M&py!cf*KTT v Hqb?HZn](Z7ŀqnK ̣Ua-&*eg}*ѵKKp4.ʋ= _kӺ3DA0KJcgVvxqra *E 15"Li ZڜfkNf< 3Ztq;T儭rv}!+ma9a9B03 S(3)`z:G%[ }$:m`1fi[)Z%ERd)'Y l5bqq0W ¯]YkxjMK\q\qT(&23 JЍ HRMMԷ|8NEg+N9or!x5A1̜SebM<:Ό=W*l}aiUB\\|k)d= Wg6y-nhsƂ=']g\GH W^p|A;~~V۠`ud4>f^E5l4ʙg$-QJOPQl4NhXe̐+33te Nx-YbsGdʒ+Qu|H"r)B[.~>4_+~6y;?ÿu+O~>>u?Lho!+_ _W~#n[)7~p{=jrZaa6ۧ6ccGDZ),6LY{&So&6'!oFbX7)I@Q@7MN@ U|:8d>u}}Yuԇ呂jdE%L &IlG֑ XSH5Gb(*+R(Ndny+2jįO]̣CLqCWN2"8#2_g淉#yƏ= 5L$`pA1~N&/?#?v fUNGa$د]p8T*;(,6J8&&d*F ҉% { zWo>}S1H3c[zdytvg8h6Nx!Gۏ<Kڀ+^ %gmD"<{oEe.ӤƈMbNY0!+|QT5,^M:=woqbӋc4'Ks4lE:a wOpD"KJYrM.|zyF8%STJX%zntbUo =jΊSEOS>nLJ}ox wAFNY*D*lr 3#{sˡCLR ׼9:xztqa-LV FGlb4+Hӡ4{zW"uT5!UD ~u7yz{\\p!]pokr( l۔chPr-r˧Os>o)=\!,֫*G*Pn$b&KDՐhbukE\]fQ- UM9.M D)K]_Dz{h)Ȋ,!@6*IdM öt!ёF\ A᫨U)0 6Ţ \lmamKAAR r(yi$ g^'"}[M6`W|5>WQj* ld&qajHUYcB&QU͙z6^3IYdIXxƆ^D9(ıI($uP&3DU 1k!rQ^iH_È3/>N`4I0UaHR+*ٖQҧNinl.I\!BjIGIX}!|(} MTDD]ndDKhX2CJvx-%MkNE p[a1kD zJEҕ7^fZR+iaJdP.&CHY(lj_UFvItօܪS_loTqՏpo !O46Ik&.'Д}Áu]9wԢD-J\Dmrl$l*NdAjDKD]h`bZLp}٢./w wj#SiM`j T1I LUMdh"G%A+]gNn.>T8Z4B'dN D\e)E"ɐZTrR)CmAVX\GUa؛{`CYJ~b&(VC^=!-L$LRŤcOK M˩>`Yh%^ɱj)ND&QlE&erQcrdza2j9hzfVfe(⫈PETRAȊzH\ҵnZcNbpz6mfIn``%i ڬ|ˍr-/>oyex\8r~}b7 b.˼ڠ6 B:)UH,W$QQ(HLh#{)xl]6-ߩ6j 0(K,مSFZUU0.|v*7ܱ{*ȡFb[,m-m3qq8 7N0oOde2]vU b7#FH6=crΡ~UƶUc2TqurByq}+=ԑM$B/AV O#L=Wy>Wsͮ p5 1>3X.hg : Ꚛjt9BE+ʙy9#.l^(62\@qi^' Ҹ$ HgpX6{K"Ȩ WV U*OjMN4vkϵ{,n8(tC nNzbc9ydP.Ļ[nZTj<=.LJ\?ᣏk^dN6OLT_"_WpNwJJQ[DU+6:@6 S /ytoDq2nvitq:iII0Rz.z wG Y]9ts =Sb6e~HƝ9IXR=Xqp}]s')v ;GZ%H,вp / x?~=l:qiS#3s:rNgtfa$fa$xAH's?>/\BA) DS+bDE`W, Ε#>~Dž{ :h.6hy>{omDNGSn;I?x|w ^\ݧ/|6D|Mfj@EL1>r1Gw9iX 4#E3JMeq]J4fnF`k'<T{Fm2x6x]fD&EոIG.EM TvOR֒n9ABzc>iT5}1>#4-#GeC556iͪgT!b9 c8rz1d) 7p$IcF6 [`Z zOKyC'}ӜfUevwƯW⽞S{;FPg44&<${"SmD1yVQF"{.#leC)& 2Z _) Va:Nç}?`v k01vX`eGATxU0Z\x;-"iS*el3]vy|ѣ=~ \keм-ex-(rTU#=e.?9`W:w>sAqop> ~1\,kk6v1%FdU܂BRBh߾۽VT=,m#.T%4Rt'Ez a|r,Y=qX:OLzc9 w?{N{K!UR#*Y<VW ³ 5zsFQQI _Ѡ%S~q0 Jm.bu"rM:׾f{\r9rePb0TR%jTƻ k5ԭ ʚ=YV9G'.G849놾uZ\{\^ |ً6$(*TTDVqNeqKLDѦB~aoo&2Tv5)Z)on9qi4HFCNUD DSV347Euw% B:)V= CfѲZ}ð6A94'WjLG 8hf+PJDQBȺ:PȻȻhq1;juҾJ 4P4oob{}>o>6ѳq03;3?uIen#*DKJ"mRT Ue'eUՋƆ|Ec"ZNL1j;SbA3wNҫ;i5BբPTEϩ>v#[ܐ:dq*vk}F}}fԊ%MILܲ(+JaucKO'/CŬyǐh[5w9|$RL7Yr (H[H0-&n,NaC&NZH_+i~fƽDE9HPl-  I,mJMaXtJTn11,q%)w\w[n<;k&u>ǏiZ'3)mk{Fh4|̉gm664䊹](Ue]XJXQ= +O|fF`f4YuҎҎhokStuޝQV{s1^mVuUUߣlddmkl*۠&,=Bmx?Σ甇 e[eΫD/l8TuEɸ3;;5ckWΒE[42G'C/20G]<ߝkmÍ X&Ե%od,l6uti\Awjm\-JMGtlQEX:sUӐwhp_E+ΖRSܜv:%xJW)"z,e4!'AaBȳıȺ:2n{Z{{#ْ=fvp:#u^UuzH1 9 P`0 H$]V]y33r~0£nu/T|U 80; ~P<ȟm8}[_Qݵ="2R^pwG,wYy QMWt6|ciEw89"/B.oTyd%U۷ѧ-'9#'!5S"kB4}:" uK݉펹%}1@P($^[s7nO1ax8GpcrmpSpsq@qSy>棠my6F1GF]>Z(A5[ SV a9\=g>ސa"OoyS}˨3d )7. K}Ƚac:$TAeMbIw5т;`4"EhG"[ ⠡+~֒G\]1Ѯ8‼w|NȾgߺfߺaߺ)lu1?a?ܜꈛl6,kv)s.c>T/8Ux%N\KS%nS;ք)qUA+ЫVE~~ϓJ!<ZdP"j4r"[2fJiۣjZLSaC:V`wTo}3>Ϲ:yϟE=g>T/9gPN=ʩN$/Z AcIj[RJR:§0Blg> ݃%|>q+eb-$VqKqoit4QW5\Q5CXZ"JCO/xfE""VDz豜XnKUH]9T~.H] L%0)@L5ނ77kP`\QJN#zQ Vb5GfŧS;=-eo (eD}nC*!|/2zނ=Y:zqLjj0)يD8#ydc е,L l$H%fh[x0 [:t4›ijMxc")2tA dt~U (zl6 ?ّȿ/_+wc GG)h_'?¯K7o0t<!)_XG6k&!:zM_`_tC#2Hs֊˛=hbƠq;Ǜ%`xY0؝1ܙ243zJ%\JQRz/_ 1Z9Ÿ1Y&%.^P?;O?8韱|_?Ü p*0U!y1=V^o+i#5CCO#5B <{/_B%sh2 ͊'̽>˶ǥ966p2~ PfLM=Ĵt!;s2sro^Kݱ[5O*nC,R0W}$/_Mg 3"f-:lDDː O/xK=l>/ |6w~ @ZdiLo\L &|_:e_~C(p(0ŢzYFCZg%~R>"2d40 tY+whSukhʭ3!'1F`A-ڕd4xnq+?]ٻܿgŘl4Z gE!\rf@@le7:[  C #F-0yی]ġ7)bVJtQG2(ҧO}b30:zKӿ=?Wax=Og?şOyGBSW4G+L,hn|r EHԃ@TJo*'9\jSi)Ur_QW.\g 6t\-T` J? Bm}2-tOSG>u'CNu~z@r ,Ek1F2!44Z0!!V`q !7>}c%;E#&嘤i!_,>fZ*IΠBY bY j}=]_oyώ=u(6fϲYdˈz"&8HQ`=i(H ICdWdinᾆz/dn "Tё ;oy~p 1A l͚]sܱe I)L{;5nPqMeTKjQ,=eR6IaB&$kB6itȫEKOm8 v2z)or8 ީWܫ6 M$eb%_59J\VKK!BCjC^#5/s林= ^vHhP;6U +a a,wnZ Q y+k\a5E:1Ik Dco)Vaeui|3c ^nXeYUhWFp6yʽڥ$/sw)]TX B.#& +ۧdnh#Ոw7lVi_pic#@ zmfXo =ui;>N`Quh֕aĬEhfj=a9XA͡s)ސjl=DvZ~}lG$eLZ!DF7,<}F7Y6 *j–UWx(.ےчulݳnسnqDEl6NډX;1W>բGi3OnJ\oko6sn!t-N2Y 7sG9n* 5RjUeY{=acĴZ{,WwP`tV qn!tȚs3u CU߯du5jגc 5 eL KCђ KP)%Zgya,A]#**QbSa"]}!kHZ9nJ:LC"b/el 6SeޛW\lh>D]ΜWwDv3 ڞvK38ޣx>FaKmCXĉHzof46#=8Sy̤1;4yd/' %hơŦG"Hl4^-Ivgڧڴ/W2MdzXկz m(d3,;*  )#Ί,Vf!̃SKT36ξW8_0델yKQmnI8}nw;lַSޜїs7q+jaSP9Z@떌w_pttNɸZr<:FL d)(;@,uFaTקY{4g͹Esn MF`}(*Vж͡A}pWy3=%OS.c.#n%E5>O4E)ʢm.#U\ehP5/?{/>G"0)zvqu+lSSX2 ܇#}&V.y.-h`ʸs{^-=Ǜ+}9o/2#Vլ+O; 9o5ATaZeEX4cTu^~M8\}Y@.$b2dm'8)' rZÎY0ʦzs~L?Csom7f 6zwj52klQo=4;RB]T aѬmtȚVԁFTw|6H@(gHAnH+0>ľoPU\C8QI_[ NɴeRѶ/<@{N4ڊҸ|yͯ_ a9[C]nL+s>AbA8 99_qѣڧh꼥5MV\S\}ϗ01rAmAI,_WUϦ}.Ҭg=Yip^{͞}^g먇n; JMݏ/ IDATb٧vi7z`TbCӋJ?HwSMxGVUT67!0 oA'򓇽o8lO_`g )Le-+jiS`o3|l^`gxWo˯vW|/oF86Ю1]0lkf;usIcc.MkEg`>fLy<>psGMtY]6BA\:$_;w#<#?>~ JZAs'h/k i s(*.c.i}mm}@X6BЗc3t1I}&$:H-H}sasQl~7l!,=s \kn!km%>FiQ]=2߯TVB!31$q#*ׁܠrkơvt#ѕ $ƢƆaD!HMH\l5vI$7TʥlZKakªkܺL\6!ڣX,.a=f~vd@ymlR Y]fbUNaj0Q並&" sb?!v6Drւu,ELfCC]1A(HEĎI>). l:, (ʀp@E,w.d*@-]qC$2"clՆ(Hs4S56v`U-Vh\'>A#+peM'lV!DYs!i+Y XKX ƢR6P)X %nNYr9>Cue<|p<СT4`ffMz=DTwP)={Id6ԕGFX=L;|lp8Oqp 4[x`8n`!Ypvq,Ѭm!A}~sA/nɥE\6F9ؿA Țeg/ɣi@mXyxQOZTE:q,;L\,N.#ˈ\ |:S$71:t; bkQ{Q~W[di:an$\HZB_7}]K۔TsY|]Q]͎Ļ#λYgcXx=˗ԵMÔ>S1`u@mJ㠍'J =CkѓK&)V>ɺr=]\Z\ڻm#=2yGyG,%pYTҧٳ%NA ڣ=&i6%`Z}ZBVdP%4z[E;UjXB&.劺\C@gVuP`=% (":!v ab5Xv`|;\͏?C.q_⩇^<k-+d4[ՠVcD!hfs1"KCnn 莗t􂮻k-1v)#3aLч|\s>~ҴƗCA9ˀbS^kIԾMD,͐IWUIS>gxSw3a⌘[\#&źuϝ_}6V_/8Kn{_d|6GX^*+/Ёfs,O{ԧ.eRiR{Tơm.G,}d4-{]__b3>fx8U?wKPǬڡsBlcnb'C39_n}ф Nħ=li 16c>bBAi{Ѐ3kdU*cT2!\_"A=2'}2;α?[uV 3v );|w)BՈz)¥swɗ{aLӰ43pt4"eIˤأ,ڧ;@ D܏wrs^o:Ǜ;XyKΎ0@ZA1 f"]4ˆk(݆/V~35UF: |2W<,o|~4Exp6(D RiaJbv˶UQR{O{>c tnY_kMb!\FkAUԹ@6$6c2mvqw wGR-T5Pe ZxX.kvjCTF}Q 2aE -YjȷM#i[4 SDQ H2JdB D^F,Lfc"*),350, :rɲ(z,.ˢd!7fHٺȦA-LZdmz2x K *cI2l=AOV%KL)JwZ+L݀1 # mC*bStkO5TKPFi,tY!phȳq 1PCXl4 fmbfƑ9Gl`Ȕc_^3:Szޒfv3,7}Ci9-Z[I+-f{}V.*Mc>gՓmgjd؝ް7Ft Nt:KZBniuBh+ŕ5ȭ1+HQA֚Nz匞ѷg9p΀9]E\rFШE&hUA ~+)GVh}>W7d":ZX۠aSC$e9듭B*[ťj{+%)"2,S#S۞㱙p\ 8oa#pm5rknv a)6%NSb7̈́n$LRܤ5nS6^] qMw>\Qj\)k^Pa@;ԶCԎK$^/XƢPu"RE泮ԮM۴R!;-*8"mffácOI"s.vT3RB/E[,`Š 5A͒$h HZcZASIL l8ͶxA C9پx?T~56>~<ȿ=R?'Q~ٙktӐn4 AJqKF]户+xg҇U2q@o`t 7w.]쒗.-;1UjT٠ Y6C[wi+2Sg3d0I\hMcްftٚhE-hlUSi =_vbc9,Lb&H˪lIʄ`0u4nQR_ZaSihJxXLg#+Հ#D4sGWX{٩Uņ+1WG,=g{.ۚ-+W셷x|S̉<3h Bv[z͒OZr_k2ǃV i%6 x2\ѧV6n''n19/E͘:^];.+ 'pj\-'^wCdN[8'%aM|yA](ss`C8|[}!7'\Yhlj0Ga0.4Eʣ\AMCv,٬;$ͺfݡ|+I1?8g|xpN}&&1y Jy-V=Cdw<9ó 5!O$JEXmlœiO[İAۡ<>ܲGn\v l3/+?Aí˯Ϲi͚Nf]ֲǥ8f-:\|ؼbqD6`m [LSi}2v ʈZ ) ooƆ]Qz`xun5s,+r[JPcn˪QVrdU Yi2} }sb kaHm3FєayЗ}ϓ%ِ"d쿸@qлl. (7CB~΄n/6%W-:3v01 ̀?ȟӏ˿x~P<ȟiz>'OѮݵ35ۖii!Hc[ֺ@]j7~uNn‹)?|Oz_\ N0}Cݺ#1(e-k5uP&cʖa/g|}g9y_}uLU Ui?X2x::~@FSl"?!#g{yoW ^Q43$+ kIնmBۖ$ZmX/,BeDKE[mJ\$C??hvw9|v+Љ7vA`2])jKX7`g|I+^{o9;p~'kbV%=bϽNJƕiNC,M+=I:>z:9rѶt57dʣUD9;GƝ 'Lv aqѧnIx!|E9ɻ9w{[1zvلsOL0]YvX_Ǭ:lcN&{""Dmhk #/rSZv,8<+,_,l6,i|1 tη,,$]7;dm@[.˴cjk;0FoVkeQKZƪZܲ)_4ƶkX3$DV[ (hd!5/ Aq+XEB:H $Z@k_s=Cͻvkk|QcvGoiWUK*Z38#quV &dyMߺVe*fR6PWVxr2Zw3[Wӣay;._I6wk83?#8<ؽ˭&Cw0l: P 7G4|\|q Grpx뽾z6iܺ{;G~?uoR_s&.6[D"N LkRt$b Q+0 ȑKri/-Svk3^J,䩇[艊4DDטc\rxF GQԒu^O|O^c9`3=YP\Bu (3|-Z:o!sط)yB*9Z#jd45"S:1w^{HǝRB0 mRbl2LYQ%jBCKr攛|P7PeE/<ژ^0a8Ta,* RA u{MMkB3؆jz[ AHG&T$(H$&9*3c9OVәM]bi}܁B3rgGx!*}3^zM|~6_Y|!$D}ZbTXvm JH#ld)(-AN*ZmJSh9 =#A61H/ Uœ+ } ۷ Q% ~)[i@2* /зΫPf͌ѣڀZ#<Bn~+Ao%jB)2Qۨ@՘Me֑BT@Ƃ]>_B RJ"l"lMX5:pX:#G 1 D"i4ʚ1*R[*Ƅk4M,idzq`Q*qa*=,!.%(BE]>?n.j[UeF:!O\m}2-]nsw9o63%! YEجY^gW H]Bi .-qED78I`+OrP缿EV+ ){|Yu/-1Ug [* 4ĖJ$l/b}xRGu}E_Зcb-YK-xpٜ!1*ivauVAf 4fFc[ XR\ W9Xk5zY7 e4B]'.|+/o@T |z8 Cv(\~g3_~}C)??;;[[e//~|# S,sᾜ⾴8M^iY 2QBīٮk٭_Q +'[|e>.)nWˆ\ Hi|EReC \(#NHeHT.~\{UYq͎7v1wr-Y!$K>Qƶ~8SwQ9`#MZ14YV+Uu }2o:sFRKR| IL#k4wgӭMLhS:)tʹH?DS2jƊ/=ًO>cVy{ĖŪ~ \PQ{˺֪&MR P(Q`.,RQ#z+v49iC#kjFn(} %ck\4PeA.4@[6^dlw[]萗:]99֜zkA1 svaJܚ[ w4vg+bcѭ0L`PERbXxn#(ϝSajb$ NokȏuIt6Q8$7'WYXf*N{}섽1{_qYKQeEЌM)Ќ'>^gk<ϧ^YTgWTMy!J"ѝdvi VRQVp'VViVlž Y8g'Y }^dF9xFlNU8K]γ}6#Kze'd ĊjgFGkb-1NQ:bS܏< o ,+T q5쩧w8<`~/+LYdpnVsVA_#  j+;dUIEVIiX-;>IEJZNRK)/7?>;UϧrJM)'VvTi@`s/^&)oKL]ƀl_%-ޥ(*R P[:FcTQK& RE"}S&Z^P:.[U8@+2^w^4^腾{=~!??zɟ䓟_? hVFwYxnWY_א HZԾ]ޞM+5>[;p'13ٛ5X;HQ"PP-|qFQNH.ŏ=Y%]̳z!޼ɣ_؃W%l "gP P jKpJp3y)N=3Vu+LiiMVƱ0JPg&J'T)Hiq~̡v1yhpTW)-`[^p+9b79v"pf!kqamʫ_Src5|D)mNNNJ Z{n4ZQDX;X-']swe]۠AEnO"P(ɅFeEVJ5f[,h]֙K6V3bwkxIERJ5CJfL;czᄥ^ئDUtR`xcN黝DT%yH&$dLJVBA NsUQThkڊ&XEYg_g_;W]RŦgSI)``vNJ\R]iesZj&:H4X@f!kTV飕)x \sP JA[,+KcyRP ĎADIk8gcOIYOG|)7O [7^gR2ipT7QIs-l-1d_(j*ee`M@smqnjyHR3fqlF732H_ PJjӘmq]*mS*}bQP"!u\?ޥV cn}9X w> i F쵟 [+b wza?? V]7 >O}__೟,ԧ_o1Q岺euˤz@:YQQ׌E1]&KJ8F6W*ay|,Q,VXDUD mQ8E(R-AEiw+Z =%XgUf ;97c:H!! !]@2Rzbʾ>eϽƩ$-N&'J ^iF6wVnqk>of Cg=Nlq[{L][WTŊVd."V $z^KR/K ԼD7rjQnolx|We̍3vC|uWjyjŽ8cG+d:k:kjb8Rzc N0ܔ( <^.r%KK\S9;3ߦL\TydZr7.4X*u.~s+~+M;M)RBi(HCPs^*Y,G}~@,%eGlV}etkgƴS|^~f9iOi'T+K6Hbftxf~XpDK# 2[̴&3b-=62W aZ!<"5狼f['"Oy;|`7j:Y y[h f0veg]5 Lg 'A)=ьUa)|,K>O뎏 Y_%^atԧڨ2C\dFYtDY[EKR#z. fՔh ,)#W(" Tt T'GNXƂ4Q%v5Q1p/i2Q#4%#G!5 rۤpMg#4U,idV1K@U4$)!|qk1v%R/cU ȴHS0&=>!NNќP\HF4Y+\k̊*ʢr+BC;DE81O%#BL&%Q&ܰHEXy k! f{Xt2O'5RGfb\%XE7Q, 2߸{Bfd=QefiJ 0Ռvv,YjpatN(^щ1JR#MD&SUV2I.wyVLs)ig ꬨ>rlaT%.' lL?ɧtk:))lRmȶ =ŀlzuߠT2/_kO9,8OqRnI#EF@Dz^hvMWL)c-mZJԲܬErK:&243:tҫ^{X :=QEWVYc8vmXHcJ#{GC7s*f@XR3x SKI5`4YW+-"TI g~D 4% cpqٸjF$P$4  L {nTg*<LcQPF%E*^@:p' p7 (5Zk8{a/#R]kDFdH 8jH[COY3RVkkʫcop| -8 Yr1>&?_ja~3 ^o?o)zOm;B/B%3~>g~|k?3V\_ ~g__3 Gf|;d?>L`V66v OeeUrE0Meias]ؙ4g4s+:g&qt+}3 IDATr%XF62ȯtZXcCE5ItaҨN5xG 0DKr!5sHUV Q*&I-ķ}´dyQkcm qHpqhT&Lթ41M+!tUbQ5ӌ[7t]{s: { Zڬ6* d>nNIdm*댲>WɀmbU2c<^SOʐeHsT{.c̱TDK`W !F9P99OUcUX 0H XGܽsbI/ص_\Pyrqqzbh< YJuic&] IfYdΕ~Px"o2w\[efD-!7T"۠g]RT^4s<ŧP͜PHL4H R,&OGG7uXEF;==fsSFlJnµ/-.mt&${:]}rT}=lJz m } m$(s}2`Id9s?du@ N+;8kֻ~Q)/dO52F^IDFX!z"I0&倅Vh"A0 fFt&Wt@1$J]ҮLiW:}f*=I[wR-cNӘA ˸;iEYca։_Ѵ1}kLZ0.;L.g&O1{ nԏ8$q  TVGh;dUl̢r6s}ke}.O%c 6& )*6rT)@NUT؜Pq 5Q($r71 ՗K`l`uN1W f-j}u>0F!(B&)^j%^%}Y-JP9}qT[zlC![uZeuDQ0 >K=^蛠OگwG{?_ZQo[NWs{=~mz޸}__O3Os?ڛodk_{}/BnI\.kgwxxȯگ~OUU>}_{6yC'sjEEeeuaɐp s0uRˉ:a8!(+Leif*[<6<DS$yd!;VPs;3"-IZ4kjb$* b![-nBX$5^1זI0hZfԠʚ:kTKÂգ:ˇ / w4R4BŜט=Ox>lxȡ;Ɛv}JRyZb].آ&7鵚L:Mwy=i.[RNS]t5 |E}@ >V+A@% gMԬ4)@Yx́/u7#^78o σ=c7V%]TŒ_S D$a,cK\(l{I0Է =jW+%=f4Kv9Z#GeB鄺ML&;<Wy.IjqjBk:_[S+I''qI+xC8’w˶"%=n{Dd;[LNyoL*ϴ}R_-%ɂCc*ޛض]]vMtݴ8 D! $$!F("@ϖCOV C 8ݷ޾瞩NM{p.]wUqs@xTvCZXiWW V7љeۗC&FyX Ay!).$ť#okq׌qn:a:WƀEN@p :^9RK%[/>yd>dGs?eo8qyhȡ~oEE@|gY}C.! F l,02OJxae ?~6 &Vw.њ9Z9RTKD@+ ^H: O]c/%M\Cr\:h 9rw>(ю^D.i<l#*S~A:ҩu=pP\fFzOXrn5[NVSzg<[5y~wWd]ԙO6/O?_,g_??5~7kZ{]{czoӄ?/`F6j+FnN)7;wHeH3AX1t ] BdrP^+ PO!P=aQ::Wxzv=q'TB5C dEi" TB7Q}6c-?!UkR,ISD'̯u&Ou*#Abd#F6c,, A&M24rjr j"Y[6V:F4uan9J@jZ &z[cI\ EG.˨8GU;[n0eP*WFjZX Jؤ Xu,T7/i v@kd91o-V&DZ!֜vr-ln76e@ #1kkZΔ=cJKp2RcM0uHYRef|tj(EhX"(T)x%_%e. JK6RXڮ rePUhKd`UJJsywpxWZw`SԸ.|XZQ|N=d1{ ')lΪh )@%)H5Pd\kk0U]$4`)PȀu1l}֙ϡ~f4%Q 2(Y.J)Tɭ}&=&++dT! ]+0#IpFܰ8/9^k[wnŖ1=Ƣǒ> 2n<.V ) 0Y q %uc%^=ݘqX<6 ksŧo1cav|Cҡ~2wgBOPwWL=g}&]3Pӹ*Zou6AMatnTIW^WT# TbA]H:u2ᢇ)R')û7߻"4cF10IeYX-a08_a,s֋x3Q}PFakpu]a0lvYc3 (FlW`Y6k$$lHJ W>]ԙ`isՍ;&,i`1G)aYng- NJ-n!C.PFu`)̊ J($B**e7V iièܧSN3n n3oDFb[Ijv`omBo;#߼13cު@,,%-19 @+U֯)_eo7659uQu`=*%v|j[zZqe(D9${\!xQwe#~103<#ٙ:ќ\qptŁ{ cKX2A%0Xc\ lf56Y5|&F sE1.CyЏKt:Z1Vw49Z$3-,2Zrc{]yW:N1F +zW4BT QAtfU٨Ō.^*ʡF2q׬Qc(u*vsNIҼ3 [DNxf5"#k:)f+chYunf+~rrAz#c5qu:\_ U@D+jc4DOihve7mNG]pyFޔ8^sIgfeةzh0[6]~q|U6oėEmE5-ïxFTU?oW!uw1NJҰAc3a_q}Ơ7t~"~f.[QIq-YC;CiGׂ"Ӓ*4&*,PB'[ 4 uk}X3ی=Fbg/ /FOx'?b1N|k%biI1->x&79{~,d$KI^2,%JԱFAAԼ%A},dv0?8?벞FҬQ±\7d䒽AcC%h|Eu[t;Ysb}kF>brgE'ܹwʝ܋3TWkMs`4tW4$,0 t)mĶXyS2,:kFI& L0ʜx1v1.)/$噆zD (10Ӛ`ތ/\ $%L.G~f0)k2fwa)|I_ŗbydvC;eN. 0׸FLtXwM*hݙqt aj&E94\׆{Ʈl-Ʒ5gj4B3jM6i"(גd,jQSRXzNrcq~߻û?"OߟQBr'>aF{?vݟ"!|'"w|}7_ֿkƷ6|O&O/G_D8o7j->¯[{3nn &ְ^#^sx^4؃_ɤf,KSʤ %rSoYNt<(7:J-*[w"[ϨJ21|[cI4 _$D_6ZAs* W*CzR|ʖH?Cy/ <>)DOo![)6+P+72I,1!6Ҙ=qKi5(@Ԍ 3\qh.$YH3JHl=eeЗ'XLD!Pj-Q+A]Г;%O7?ǹ:}*`#|1&9.Р$sD5UPkTEZX:tce h=Y".rEZѨV9Љ-231mFYRK;!ւ2(}"ЩigxnH#XЋ'dIz\Of"ZCO1{ "d;;$=B BXr4*-ѮK D ^~3AdAAo1\pgq_FeWƸyB^I M# a%d9RضNjll?abVVf$ZExDC+Ս؅1p!z-'`Mњf‰"Ya.μZ7s+QN`r;;x*B!>qn3u_-vTdcc-Dw5Qfse])-^I_*E4PŴsGB'\E aоA ^,`Irxqq(q;+OkyJXR:JvJg= ~_|HsGDCV蔱@JiJ~O|E_co0 2~峟uib[eqqyOsߍkr_xֆxۏ>~Gt~Gx7l>z13kS6|ـKE9Cqz!Pi7wuY57 qOko[nc×wbBΖpC˜Ksȯ:ԾXè n NmVh]Cs0b1&&ONk0hܲ׼&V* A_hխFqQ^.v"H3iA GS}[qX]V:#ԻYVW!]p+J]ߙ\;eD l#D8{1"V$CR:T$Av!1gǼ 9)NkFile $,i/0cOz`O4C˩ܿ *8j_> :D-H8z]'ٸ7 +G2ZN'ѫ&46K0$.1_kέgl=&U&zBE8mn=VQ26)j&eݯ1͂yftmFvTva1F{AZȈИ͝wRNaddYuΣ;8QㄸnD8n)2t-G9.͕6m¤heVUȲb+x9a}PC N}X1ye+2i ΜNcFg0hp M6`uZc՘}LYj.SԼӘn4gԂ5*!!sv哄.yfR)A C#o MfJ򈎠T=A|a9hpY|6UFl}5@oZIZP8UHb @\)譯L*8;p)ÐNvO'o&pqD-l5b%M*WB]Dcټ3t_0H -Q-Xu.8b1t:/-ԓ;a>fӨ-1J1hy IDATkB(#bz昞?׼߹`WP{F<]S2.YdM+ HRn(5?|>'|>O?Ѻk?Ɵ`]}7{?zukk|#*Bx/~/3~|7v{>o>ҭrRg׉Eb (/=>!ZcԜvcÓ o=xʥ!>K$-ص-L)Gb̛gy28"HÐ 1kڐVV1պlO1v |=Doi-iay.w$%3P;F/0O$&gP.u($ܲkԲ61uGnʗdR#mjj@-WCDKAK!Z ZʑT4*KO\6/٩;[Ɲ(YMVerS%ȲYun=!./{T*{ǢQtq?j+'LmZvk[Κ3#s;䢹ϥĥ1( jvt p%vr_h sT|妘Z_oX>:vp َ}n=elI10:f!a }ҭe!XԖ2]tLG7]6#q cڜ'>9HrJ"X^xJx*~>!]_Y=],ى0WO9ܟ3o9joclz\mRn A1rasɐ >+Duc5od!P>t8{.O~!I9w=+~ /0SwB?DR';wTj1W5*}j {NJ#ɵ:J$ww+@ ٵ bvn%x{Z)Oݠ9ZYPt&>F/Ň'<_yej [#0NANWht.@UVա Z{M6v5?w E>qiͳ: N3ptwz[7%np_r\_e @+BC M@!b+ZX5uuM}n&:B *L+S t[Igrgq rrh31ZrY(@x,PHQuRGkT89k:{Fudк||rmc̿c033z:[Og?!<Xˇ<lv+bv{1X@B౫xz(SRtRy%Y7ހJ2*!bHzE#R%6! fHU ]5&^,p8>.<+/pg>w--2w~R4BG2Q+t6XSok '5M$UND*Hu\7(upK+QhCnn0drn&YnN'Ɔ>`p AibA)&XSG%XFSψY& @_ kނ򛽅x&c5 | |)ƚNsB9̙> ܪqt MN<(>欘^njDtV DUR3 +a9a>mfqIVYzo|sC(-N>c|  2b[,FtXJ:F&-+I1h+KNZ/92vx:z%Lrg Y2 z݅7P*ft L>OINSr&uKAL>d=L;!i$I^R782 ?9dVBjbPuʩɬbl1m&[35$LtKU$nxH<'Ya3-v9i"KM=xyykm}\o<{oo $I24u[đK:ϻXljr%is^_8Wl4 W>&sb.[Bë kx?%Hg'ݒ&W7CT!>bK# =A.N'=R6 YhRZpԺAZO -H ҃(P$v] -֛YnYn&>ei2-<+`9َ́QZRCmPXw=o,4qE1gC\&kLҦo3L.r8.8+;L>GI J4š$5%\{D+hR 8)ex~#L %!93=m U}N`%$7=SoTA 5a> ddN%Ҭa+.Kqe: P uQ]kTK+);tz;?Kj{+jo-[!9ljhqIM폩>M8Z1!e/7t-5,R@Q3ެ>`|n` F!_k~ؾ||&+p W`hhhM蚣#DIupkwT 2z[@9l ,.䬲!>[jpC[d6b^#p]g79N0.[ , E '#A5=ʢ 4؇rS5+Vao}2l3!=.VP3.k>A)w/N$3~9nD-o7~ko!O.yG'<:~;Ɲ6cCJEHRʲbz3p[^iufl~GjL&jQ5J_Gifxhv}J+v&8FD*,lVNR9avZc{]<) j-QKKDld6зɞѪ]P3WT =,Bdӷy$nhKa6SfsJ5#f|P>Ւ䆓K=&].ǘlgkADwvnNV8WNWyW{IWۈ  *)QB!G'|h9qiIȲsbihr>L@UQҠD C*% }f [xb~%dQ|e\4m$JjTL$tȤ&!jʏpϴ SP 81%X1棄nC1o'sscMXsWDù8eyUEo9cޞ|;/ }d)=2`Vj# G,&Ү(TR4ޛFyzϹKw&32URi6t? 6FխJUY+\#z`6H7@=.D|.0/=Ð%hAڤ6/,brI5{IS8eӤfЩ9-VUb%sup'Al{oy!{W4h6j^yӼQwFS]{9[2l3<#1S\3SɀtoNg_1<飯9]2Hu0DOH_)щX2@J6sN6/Y5] A#{'x2*;+';"P+)"ڈ&)){?hl-.LLe۬MxTBG*v+cQQ԰DE;IFgͶjanA$ e`P:uSo%\G`LqڕmęK*$rtG5jH;> wyS@rcO-ImHMF2g nW{mybIHUXߊ+z(j} ݢMZ-gGP_i[9(n(J*EQY4Y2dؽGZ5QA8}RIadWW0!ZoB<!wƘ;{ Ю1Z5F*TK 2$< Oz! GO22bUe76ydS6EWߙ7elP <Ʒ"Q4B B*zX%]^2ׇ;,8MOO&LHKVEY25$ ]y,}7t["zd`(cgnMτ̰&EiPt& 1N]nNV״ RŪL,.7'L.IajY`9(26):IB࠺N >5>gQ:~cJD*r4C!,(N4)j pQHG"^Va4[K67`**D (-l,cT. %3' 2ﱡMR{]pP;ڕ B-v}U] A=="| ECIrB !̜g.iF]l@(Dp 7Iqݺ䁅Ut5V3V"7m:8.(I zSm ʍIifi_ Y[<35mEG[E *S8tYfm# ,4 6a>Ia `#l6fUuY/d8jp4F^h$ң . ]V ?#H]R%RnxM&Y a3ٻ Wr!r!K.u=ijb>O3&yt0D͸ Mb̛SVFkt4YcȜJNX[rcQoLⵇ4kI4'B6HaQ ˀ۰}8F46yۡH Һ܉ +8;E-f} z&Ê2: 3^ϟbW%2ۛ i?;MVn&GZQ43Iv^uX/z^yT5qe>Ub).8>p R(4P[6 m~U i0 #vCyeqĺցg;>Q/N#PM-EER*1̊(muIoX>KgҀh(1Շo2m  і-/x:/U1kɥmmzhri:!V>cV{GrMrcPi6bwL;&Sk;F?Ή"4(&Ed4n?$DTmUm&"`8.8 /8]^p[qޜrޜ56HgEx'na}tuO9>%څI`oMt@)%iҨ"At4#fwg5mŪr9`pfXbn["@4C2G0cY==sfa k0hG5- ~%rLQ`x>"ZdYh V۫ W A.$х &Ds h5aE|0xc.ӧ jDz2;ӳ?[ypuydr=L [_+f_"vYF,ԘJldɡКsd3ilc^_<#=GBZ ]TVMrfQߙ$W>ŕa9G -of yj"׉vq' IDATV69%㳬:'橢y x Mu?O ~e;^ y#{ĥGRxܾ!Sn^9s],^,5VZ0h9 &_ϸ☯/ xz zlf=dgu[CpY.S1!4<wsvxYl7S-M8ty{}yP ?u(%Q`VT#aeM'+g1%ٖbb^S-tO'Xt~_*CAk69__Q)A+u%I*wܝ;O ?S|ίqŒͫc&2 c\+/W1da~؋F "g'(_Η_iA+^;;ߵOѳz%ܝ'tR`6US S}O,{}298z#b#U1w+:neȪamsqWOZ,.z z@UlV rհW^s`\_ri+uH)BrditYaMcG+ހuaC}5k*Ȱ+W>7-n~){&3о0"W S&\ Mmw( j]" a4HF%L+7<ǭ+R2eFNb7v)w>9-=lc\%^-Q(I8DN@ۄQ(jGiVL+00'L NȽdlָv!1~1YWCdѐW6i6YmS.[I KfŖ簺fhoCEIs q4fЛa"&[Som@Kv;'Oq˄qޢ*򖁖Y(!R;ܼ哫7<;E51ԄMա,- /E]xrhrxrGoZ4 I|LѸrgm@)}gt[KI6:aК(0#+#-MI5Oy3-/|VՈNegx#qa Fkrݦr Bi ,Әn!.h!J͊$2q3?:Q gܓK{9dm1$u<R/uycecĞ}2tb!E*״3!Ta9](<v261G\'nYUєm0P4t) jJ^ӫCK<#.лRлaoE8VECU$+mfse=즚j]oG0I*kȴk<4QG{e#-H `RE5T(2$56.)9'/WtzK]|d4?coq` '_-wR)aBIP5l6\$7]ڧՌi5&vs»'ܭK2x|ŤK{ị̃.WaY5җ+&r#{:m:ޚsˤCoN3ȑ' *臵7ZAf%D%~ᝤ89[b5 u7]!Z *(MOPtv1 Q 3,ΘoOh\E[mxu n4+zr|?yN=6!nyr22 4 Ժv[6T;ig%!5551 [&G֡ O)^`(I>Y>Q셍8Vȳ RBZZıqCRr8fZMdS7o.6uĉqAe cCCMcv@ n0TVeA3w?' kcM2qƠO7^3ΐeL;+kjCmhk|67Dh83b% \OfƪrN_ԘJRcUM5bS )KKGSfȼop9$J}'I=A닱|=5(K[fxvgǸeD٘+0VͪW#{zw"\'s,ČtAZ3!nȡB U_L2!U6aR;[wSN03~\֟`6h",ڄ-7-66xA,"UJ k]P9)ihCsI[b7)0cǗyyz_ {W2c|}Ck|㰼Ҩ̠X?N|2鐍m@*A;וko8+,lfqmY~.*6Ue}pk"utt 31]섟ү' qw[szDYY$E5,Н*)czjP_::ni[ slрYu7<5_3ӭ%&+c"S>f+EVN\{3gf I­ kؠLLg!5z߲My}'kچoT F.*T#E6Jt͸22b.pz9G37%B$V9U";5bS5[ c% kP|{ 'D_Zx@}~`+  Ė]Q&q.ڊ8yHnss}<iK>j5Ҭ:5kպ$-"gUwk4PBgw{LKR呬=7m7tDL[CBV 6M53Ii4D~1%VRJC/KJLuꘗs&S~ ȻHt5c֊9e1 6a޵i$M~]a[ԒDe{4 g;%pC|7s"<38֯869,>Q*"EsETn\s^66Ϭq^b Ʈ|W (T+ĠU 6*0.qDoĴ-niuVzkF)=fQP+q Q^zY|^(TPuB hɮ'Q PjI)1ҬVMi s sXqc,!^nM6hI %nH4&=3ϐIzc}gFBlvw7 `-̴(  TTFS`9vT)n5 ZӏMexAL[˖FIGВ[|-JKoxnD5ZUnL]jiX&gX2гYHjrE AMڡ-f9`Ҫy  l5VW]¹oOZ?_ { NNPNUnUdM#0TA}hE!vO=x~I!*ShB~l7C+(D B 0"9qp[=ƈ>fe'G>X^J TOCfc{ș1̊l̪{VuYUV[~MbSmr9H' J{J';ۃ,/"ڊfIb9u'H!u=jSr''lep+\#ıc+1hZ:EXFg-H5[)׌?atKy|ד#z |L&yf3M&\'\'Dq +.vɷ3OXe]Pe:&oc{[L\G\'\{.0NMfM1!9u]+, +c@UdkfA*t5eE3.:%˒ܷ(rl[JpN0/ܲiUfـVS! |͂3ed3zrΉ|$ he!#9eޒ&:9PC5c1o7>'lKgY=_nX.uxΏ4L y?c)uIf8GZ5N/aTNi[--SwYDF!]sxpJFcQ->FyL&a_c{J GϚE_,w̱2̣ aɔFIBl DHkY3zLZ.;bd k9g]2J%jumz}8880 DD)$3mVz J*ϸ](eX f ;3-蔕N|:dX=ȆA5Y8ܳr~N"BEh%6%D6Zf O#4jTGO~F*a7чjIPXMM* J$=VzB<+KܿhP(XЄ )v%VV҈ 0<'|;W3]MN=iRɽ2DejCtR"4co}Cklvη3l$]A?d<_1QŨ|ԣ p`$HC"&gQ` >@ZyMYqŋc13euDAtj)d0^0Ht i"w\u>7jMNIjKV wQ`޲ݲ7ɀ/3-fﰞtiU[jAY 7hn7=fWg$Gs={Ͼ{C& -'91LweS)X E gn C*@KJ 5l"ǵ^3 G%=U2RWr~?1ڭU":Xl{tWWyb.{֦ o}K9i]ʷT]25Vzu|3CF(bZ,9{`s~.g*#àD|*1(hX e@;ycf'%dMzb~vqى>cRb+HMW-dR!kjI KRn{=|݆ ) |~<&yϮ'V ޥ TK. 6$e9&gL3Z-MAL[{ߊ8qs)-ceϕek8 o8ʮ9pMr1eК]njN]n9 Ҵ"R,4E\$'": VN#cs;8^_qx}8' \"#\"%S ̫hMq 1`ȺOȤMA~}c&|>9z?2pmn1w=n椘@KID0`'Br?U]S1 |pk8.xI';Fy=z̛\p& IDAT~Y+7KA[L7d0 9z)x?mډ>@̝o$m1^oA_-3'~+>AHן3 n?fM|>+>`8H^ ttRQat 9ےŢ]x?ɓ:,UI1Thf[KK!uE9g刺/_3ܖ1?.ƾȼ fbk_j?$l4uN3N zւ5|}S97 Em׏QtW2K' B\$/9WYRS5NCt'D"-?U<Ӑp3 }~['z`@^-rI&q"{~J@wk[5e%.[KHV^p^pKK "t[$]eTͮʻcitZI\CE!./Ow^>p[!n nY_GX(̨GGx$*UUuUEV% $8SPȩuLI'rFEM}[[ϥ(Hvi,_`UO-/O$0:'5Rx,>w֌{sƝ2#)m)qlu_)/kF횞43p!mB!4;ZFj-;~XCT@U 6͐S1^P*"oʄk,R,VREۃӡW*_j6.0G8%AJEu"蟬jr]j攵f@ \2LH $ʾE jLT4un1bl?jcn*K4Y W k:z)-+Tr bDdn_-&snsČX3̶m "a'VzU{ʺP \ KKMrxS5RTHB&)aPZ*4RҊGYilv}6CE~CogliBԱM9!NJ$k#,#q**ҁI⛤V -3*m)H{&[ˣ9R9':vjK_.IjjI,D! {acNH,^64sIUhʱNU ߷oIS葷&#2tPIck{l.^B,3  ~˵w+#:e[FtM@7ՂPH^P$a"]n%YڸN_}$.T*Uc.W;(f|_kZ= tg3/p#Rr{&.k(N-̺Fjl|Z,iP#NGdIIn09iPK*Qv 戲bKk zSzkb(_1,XeC;=b}A{lN*xW;aԬ=/OXAʉvChhnn:sQMxR&MYb&%FPf=Yz4|RhL{ʃ9^!3t8ӂY#S儇rceeq=M"ʃi.TNq)?.q i7(V͠blj\oa)`,!ĆE[ĵI[R  AQ_פkta,LS\ o(:Xg{c@[Ɯ.{sxJsk5t&(5B(J[{gyg~bߌo(@ 'dƦHLښC `UUW~ݟCG"NHk;Vemu[ѭ '[~Og? 8nohIl [ܼ~%x}o= &?\UὍ9}{ǯxӾm?PZ֒|v)f'h-js/\)Ҭ)ui2k8k8kpW L=٘ q%Q& bM*;K8 o(F]4eM'gRX!K1bt *թBk_/ҟ@iR Bk Gs i_0RW挕Yu,{unc۸r;agtkγk̤;mCdG)2p5%`bpIu4Z }cLp#?BF %F.߫ĭ@As镴o8'w2#uBko&vh> 9'~wqssF$|vt%|&:k ?VI1pN ssgy_.XRaZY#2Ij+"@%* q!:ĥM;],Ę`(\ҿݲh(=cfSHasw]Ke5̙N4Gٖfށ!\B.R*umABdSf!EV@*HAUJ4 cbID(ӊ8OLXmݤXuB m/ m!0&)WS;41!6DsԶBȚVGƘZjVqСՈ`o\3ddp͐fqqa49S<Èsᦻ(~ n>+:V:+5hC%z\gZGdS(1 }0'6nm[xE[E~ǸfrS1ʒ2(BYV#΢k&ق~l2Z)9> 9ŲL3ôR,+%UQbsN;ڡV =on8Il5D+\!m }sCO,2q'(qKĎE48D45Jՠ-ynf.A.+g@`J <;%P)ԟE߲r 2 DyxsW[TU;DQ+6qRqԈȹ%M|wCŴ30U-j) \7(l,j@9TFUiW8MFN c[Fcz{Q̂[d$-_*~l7}vO)EӶՈ[Ѵ5Qњrc4@5I `ztYCvO:{:U@[cކw̹gn1W,% M. 2)JM!QWϵԇɘx ҰCpP!to'=/OP.x^BFϫDU* m٫QF "ye O[*qⱣK:D5)=Yh}a<%펌q qqRs$|gy_XW.%zrEd2ww w#9wwgsjKj[R[ Ia1$G9UO%xj3&A WE /B&͚yuڱA31`dPq;jt $2PAx HTG*5na]J**; yKk Wp+OyQ)%iaw vC rIN:2x\&)lIp_r_r^ax/Ѣ+m넘9VT^R4qp|x肙@>V vO C  l:=>t jܻk^WWiNQ.O)Knc&,FD:w+0iW|NcMF蝌FNVDC! #=ԣ u_pyé~MWJR5R\uX SRҤXKx풭m@i2жtb;f`VeOwǿ m=}6X}t@5K~Kh\\4/LΉCN1IF~ tM,^EFIaxgӎ՞SItxh-DBROySs%V0||%Jk1.zhZ Wt"!*!1†}{jhdKxWV?5g3+u,<(3s6Q:F/GN ڽީw-fx})P;(po^p'>ԸN˯&x`9"3Z'f\/QOLTLeG|_1z?l|cYGnÔXwR5;5lk9bUYcKd|pګ^8-֓CA%7*ʠhM, f b|a^8qU6KLykvVWܧs-OsS qOuÛ?pv`B20ͪn/sm~Ϸiux~[1 G܉;ѥ`8P9+7y맏H9Qٍ|n`£:Z/ -0ܭ2ݱis#9KAæeYLx̎xHxHCR IUIR(v0d;׌GNm/F f06 B9LS)b]xm}fӧАfpbNή5vuC-0l>zÿvDt|kQ6b !P^?[;^mgkW!>E[ LL&O|en._q+.޽E 2iq_0oMT,4 zo9o?qf\2/8*L 2㰯xv)tM5¯aihyIzdGɥ`@Aﳩݜt5z])azB)Qe_=rOڣ%gW^ygy_pS1aIub|y}@ŒWDT4(d]b m2iU:m+)[ȶF"&Cz8Zz&iWViNb Q"C[`ԩ,v\L%C-^aQ Fu.hf%j!1>U~iS/F&;yBhz4W+LЂMRdl9OXN/X)#I*iPԼWC 7r^ҫw? MKͪmAmeF VPVd%U%:l{V=]sie8vE򳣮EJ .1N5J\ygyXd̿D(]cҹ60:>] J!_khP ZR*bͤX0=yd2zb!l!dŜw/p~c1qu`wb$5u.)zkc~x}sT@ss3>/Y@;1h*TE^7kb IDAT̳QGW &=Ҩ˒J٪:u6e/VzI+L`+ n#nc#bp]>ULJ2oP 6{ G [,AJnS6Sz/Q%j@srv[[#3LFO;_s3:v:JL`r# A>⣏sNk&5S;eE;aU(4UTmUFԊý"Z٢(%>fyt6-e-*]\%&/IүG*Ucmx)GŜOK#BR!K+)jw:ѪRMк edfb0͌`Q䌮rX eW.WJba+}6ˮu ?08+nF+ƎHX |E"7Pǣ=QxLJK5yFl.ey,%7gd7 {&:j)U(vI]K( v;ڽ<+ƚa UB3HUHuX#lў`0Q`"a)*>4$ꏹkj:DRcX3ZN3<3o+!#4 .~0mBN'`shFUiVU')G|sW9+aa,.H7~/h\IJ &l]dې&z@ܯ0o ̫;ˍĕdH.TJ( GeՊ//Z(j`c( sjթ+Ɲ'a'eF@JroRyDZp8(@w+oo q[ЪP[T(ZbnWL n~? r@:' =A??ה ;4?}s.s3Vƈ}˻$i99jZ'}? s ċGBg N:Uh2A47S7w ;|6rM'w  2BV?/xCWOfׄMD&"#\QV+,L(-zͱzFkW$LkdT#\k'\Ϗi{ϑ{˼sTydMx\/*^G }%&zv\ՈkqB۴t1ُ MV7qCybol4tGQ"? %' 9R b}iEnQub%!i=VC !ZD$`Jh.aduI@&m'D([`7XF\'\{'ܗsWsnGCش ߔ Vo,wc w#Q*l͋{NQP R4RKuR5굆mG8~ 2Ib45ѲږT3B5%N5T4T4'{F5v0)hBvX1TR<`k1~ $vص= tGB$ae!PsYdHSv[]% \}HXԼ&ڔN[ $5l9k =̛{^U{>ɗ+l>XVC\\ŪF%جȚh!K-\?`M#z%+6V{kƥz@ġpڻL=rZDT"!T"a- 6#gTONJm >e{ )ڜSIp'F x1V dS JVSf*kr9=:WN8' %  qS,%vMuLS ط9ч5Ri!6"lstBʮN,Cz!.+z=ۀ^-c2C-ϝQPASIR')l'|iD7sqߓT1H 5еcZ9# D"KqFkA`Z3b͢*P YGO);5i`.+k}wƥrʲGW>CT'ߢ~ge&q`.Yc6D{!ݖv&h[A})* t7>JuǕ^FQ[|56)\iekĪ5dP)Ԅawɹofb>*5Zf5TwME5)l )Q uK*%bJ }snOsNmIV46jmuLW8o/Cd$bF̹6u&{;{'>N!Nq=zEz<1f'&3<3 b_s%(/C/:[N@г K s߽5+0`.K]9Wl>|LT;"UUGX$916ppHtxf# ნ 5MRSPFgaCV.>9dD}b\/xYsytB40=!Z[b {JxTjPch(B $@ 77{üFHvJ,kLE$' @>0nCXru#ʗ\w~c8"ĿbHMc3?܈豩l۪O& {`p78~:<̗i([ ?GK~m$ P -bPs7 vP*!.K9d+Meh-/ԟ8Wp=AC/h{S͙fKfO^W973'&aSuљ{>[GntG[hM- a6x9Lv9?R?b9c>D#bGZ,c keD 4%k+$D ) $EkIJAԸ~kv 5zU٠QUJhhA)B*iuAI"-&]*ELGzO[k|eQo4VA|}gNeJ*UeDhxG]gt΀MϦ3@msB5"medE@QkʶIb} TJ^^+EYf84AjۇspoS:p$N|eQ!iV嵉hZJth9q .z$cEB3z@\yL1;٢Ip>\gkSNXz(%Sm2a4*,3GyLגq!-vbK ф'O<OWlaϗ\{Mf?Kl ɄcE_1꒺Ј:Z;evX0ѭ 1;9a.$0B|=$#46=c^x_Ci3a%{, Q|XmjCp]_pSs]=#TCl ֌DP@bz+;ceSD?Q`붹\Gg\GlNb 8dv=CgFF#d߰Tz,VQCPš@^(nzTB,$jR"](: yS]F*"QhJC@*FSF*IR$KRzԊg[|q}\pP 5G sȱuˉv1WNwC=|4._:ZZ%":?UAJPuC-71%r%#<~9 Q)KspA:mJ[gɦmB&)E#4K~v_Aıc\+5#̰f}7 :a|Rm.'|75qa){̚1r̬Z)C u dہQ {$EEfX5ee6>QQw7Glc9 4UTB*U) 3"|P_6ާ e}NͰQIO'x_/^wPk(G5|e=7 ; ~Äo֐S~t=zt-'n!|R|䒏{bK(|tJ 86yd#G,鱕r4L0\?cU?G>\??/vĂ b΀w97/?w? -R |o+Ayo38.E-R Ec09ܣ8 @ `n&au`lO]kD\r-/X6g Syˀ%k:ld5n9${ |(]𐝲G|WjkEJD ve}&l~^k%UJQgawnQ:Cþa2+%܇'<Ǽ^ l >vSwWZ0~d|`<%ߴf(d/f" z\!Uַ[3&#Ίn{bTFU`F]m"5 4YUQECJǂ┦RYGܬ[mPeMs(&uN_og35xH,u:.k|"Qrd-Y0eUסi$ff8/q$wi ֊ј1BVLp˔I:eu.yҚ`$uȩk;TpR4JOEjF###4I,}betY]c]D!~v\~(4B]Xn,ϽGk a+HmeXt5lM{Ú1H]ıI\PEJ*j"1 2`N&-%Ym>*d LUʭɣ1A^;&Xj=:Gd J)Na5JMS%:uh dyTFL<ҞDUTBq{LR9^FO,dFxjFT:vYﺤ̓r_߀՗h'53kHfY4=W:-c+)V"t4ut@J@v4OXTAI ){u>/a^g? FiJn1˽E<ϑ i4mO= FIj ʯtSu0z^(uZ鱾{CM0.#NL(ƤؘB}d>O<į)V1 IF! *ݠ vGk^<|t4@ڂ5IԵHU `@A0 h5(gJ30OsIB1hTh]Co{܍<ڄvY}FL#k(~.9ndJX\glb*Y9bZ6k?cY2cO%pH#0eyW{-'-qe"Dd`J}%ȖD@$ rpB]HCTWwn:LhHD+6-6bCCNqv x[\/I6w)t16s˩(!~/#4Fk:yN<_Q/1e_PH X03ܖX%]'#bM(=,TnK>01QĶ3Jxf]I}ƻK޵_mFv:mڐF*eL+}-JQE1cHNwtdG+ =~+B9jeׄ\NEKLY+ҖE-SqLpU4$M"k} h Q ۓEK'KKA uePɚZhHMErdY/3ֲ钘f'a0rs6{>KquL(sᨙr$L)]wFUXj}|eϬ0k,P/%ƫULS"l-E+0%T*(f4&d*J`16:ºQ43g3<;x\OA:4\D`9Qn8Uohw[`dn188^yλ ݼ`N1=~wGzW?XTMSk 'O<O+6~%BAm4Qy-~_h SṷrzʭrBA??]GE @yQ~UM&0zƚ# 4w i"&QBcTN[Mnڔ3& &)o|)~L\s=K>\|:.>opi|3k{HqxPO}{Ayh\WZ8`9&fV`n y9/ T *Pp bIh? >w;~g|8N9=­}=c4_t0~t.kTUzY\X79cX"b u/ SC5S0g g̋!֠zԩ?%_+[/dkT;?ޟx?|v͡~ i{<ǚXIVI(UJ2kNVA3oxDf@x޼7G~CwstlhyZ x=wġsǏ }kTQSF*EPF* ĠƴKЌaJh4TL$m=4V֧ ۊZ!*=$C%"Vij y!E  ]9԰2N}b ;[4ƣ9!(ߟ#!Y)(Ŏ6oۦJ; RÂO<O:?Mˢtjvz)Nbv10x|QN4V痚2R(cq)}Q0;r9f[U\T ~H8kz@TdT0G, GVdCATA]ԅN-RYn̊ NWg\\YEOޱ@u+kĹ@ X$C0I4EoHue8IpSωkmݣ(M¬R:)&)q ݬC6]"]xiDX364@d r 7 r$L4m G&'q;j 6K|-4lLe.M;LB[ =@lluN :ܣ{Ӹ*a(6aQ&f] V]0.Og ]k ]Z5X!,Cᴈ+ ت ZFhMM8XVIyG2P=vQ)6DM }_1\V-:DA@be 0 `G M86QXS&9&jШ *6 nwa$j zm^Ω\dxOxƘl#ƑTm1WE ϧ/PV XmֲFE"t2`-B{J'.q.BwAOSش:|Q) U@,p)jܐ&CO<į_3M)Q>!"pĽ5bq=gd[Z%HUaw_\X[Ŋ?\\X'6M?Ry#eZÃ2* ؠu`>o5ފéAys<5#6xMD|L3'QjcX{dMV:Ftu}uϹq˲gꍘCJ.Kv?p '&ݐ%# ) cX3w7g^;bMecDV8y_o Ė6mlp)QXevYulMS,Sw£Dٚ!ϢO\Wnn9q=Г[TW.~&4HЌ@A h+#Z%xQ410cS{(Qйx&aĿN,ĕ˼p/'3udOQU_E'[N;BcY=Avhk{]@0aZg |>I@61mʹhxBOKSuʕ~'knzTFIIOI_>`j MW%0]QC\!q{ rL"j 5]ORTSڒbqUW9JRC dC#+;;D 62 E|c1))%R?o6cXnf9)6SuO4/ˎmʙN.-nSd*Xe=^Ƽ5`_VA Bk0p10\aɊaU;I }5t+Zꆿ7~tg$m-m `&]pF^<6)-';e4[o;L(MФLbgOB|.1o]W,1;W{ {ycڱl%AaWuO3VUUt ]e)rZc[j zGQB:XuB'B#%ech'O<į_3ӢVf@Ąc=} l<#Ѿ jD4?D85]k!bEoŊmBueZH/l{{9QQ !8d[")`z 5ʹF>w 0yRS;Ș8'[xߚݙw&u"Y ʭA,\6!]8pU[oh fِe{݇xiJU3tg-+/V[QVgjӧW*鑙6a&a zK_i3:JМ sM6ŧsZekI:6R?W6|{?qf` LqBI =vtTQ7*uR5*TѬ*(e8֎Jid+|m|:qVb')vtx7KD =PyB q\ ĖGZM6?y#/v[eQv5#Onj:c8] %<(}K0Ճ:m>c^%pJejj'|SgD\״5_0`GD@cuRD5J;gzCSk!9,!Yj6icD8H#TFU #6C8<5ZvSN0(CkuFgM6y睏9H}kοw7 %|kk<&܈sGle7ˬϻ%F#D^0kڭ5R@NmcEDkb[k #m!ZWiUqo|WH_~uG^+?alsdseL`$`3g r@Zz~hU/Sd h E&3\2l%+HE*fRgf@iDsJ#QE*NPL(LӠa$~H7!vt5=>( Y[yFwH7.![:D PhP!SCAU/&fݸ,}!M*0VDC%2,2 e[{ iS&}b>Xaa Xt#aseꡯ*:5+ '7$s*(R'eE1 ] J^{zꜾ/GG%4R*UT*nliC[lqvC6Ug8IANk-v6im36cSURC[-5 M?iGH] 8c6yeal o{}/0% TI6y@)_$JRyh"qE3Ps>zKyQhc59 M5DmuUSSRi&#|2g8''Ԫ&id6B44ba]C "uT`ղ(J$Cdϰ KЂRhN. ragUlUV`+8$HBh@ R Mڔ&ŮS*#ihJSӔ.+eM=|M ԙNH.@ jkgYcԢ"/L!=vuMaGdEaԶDkVA*,c󑥂Yd9}A lc eQkZjVnr-np:MޔHJ3g< očvUes0v$L"^7Slj^)ȩZ=#9e$gHs// !QlAA0aYGOPj#5aY蕁Vj@I%U6>& l|<5K`_ ĕ1fEu]leRL1?)4bMG(*UmfaU9U*]lO<OO5~g'jGo MASXm{;舰v|ŽϾ*YP4*RTaC Rב>MifP@f,W qp@kQ.MAԐKqe7dg &j,d?oUbF[v䄱G(qq;gXL{MT5O?~= r c.q%N]9ނ`/*HUe.Q푗&M.w#7d:zK%5Z~a!:f'eYp5R.UVA pA—fU}}yF{sO/`2\,.(y0 mpxus cT u0f A-"0g- !7ԆJKߑvU$l[,]Kw)>ewccwe1r*9N"B/w [Uov1&[x#vωP< 7qF{knk&*48ܾ NR4;"LdMv\GxKNwXFaܴNX]t@K"=Aʗ:a˧Z)T;/hK[xo.X=fc4B<}- CM)s"^e?ɶEγ#5AlĖCQDkUjSRcˌ~3Ŏ0DZOAP0RhFIwo-菖-SX:&`c:1{Plvmm̰ FѝH>R{Dž~P.5iA$c';!eXĸˈ};`iD%(]U2(!p'x'~߫'JM#]b#]Ƣ~TYX|LUSL4JKR@lڟ_=gWi:2լ0Z)Mi:K͵zd*-JY,D _ k %-c*īBGuN%Vm !TST EPl*Jyi}o1%>] )%هHw)u U1єP~5ԺN!W0,j P0[=%#NqZsZqZ[ @W RY&mbaUXU=U}p>r>p=r=۬+:)wcrOg 0rjRLM'e--!%DP*oG/(GSuR#>9 eWˀ$ASxp{YJ󻦵 ᡞJU$䰻_(2nn" EcY >n2-V;;;vf3J+$D,k$: K[ 3bl)Ɩ*$2zrreء\T%RJԁ8Vb7B>?f[5jQs,{THJ!B$<3CdԙB#jOQ>xW<$Iko*DAwL$??KAԂVImжL+yU|Hxxr81koxU*׆T=wX-5&"h2ZYrblx|{̙GOzF>AԤE8$Ch]M%iMė垿.S" `h;dF3KT?GUm|T =sG]*Ф]Kl\A8x< , N\M?Zf̒茇~uX>(7œAS<TB~|]B3_x|R6 Ƣ4&l uz]^WNqQig:!c̓ / dQQ}']^x^xȯYÄ_Q>#u5]QnzD=O.mq&&-]-;.TOR ٥',V)bInE=vUJ))R:!n,ńm1$[ -J\1)O]bk)=-QcVnZF#}(W%(y"Z/+ o迃ou IDAT?4b?DtA#b%G+$jyRAK:H\w9FcT9FQj1tI",yw5_DEH[K-%`#5$&=vz3be):Q+f<|^}!rҁ8З 579"i9ir*?m$!q:g !ԝrOn W~E E+Měkb!ǐƔK<հ+Ȱ9>?ʀ: ]Y$8}2@U kԝ @Z #$: 705s6*24Gu;_ƿ%MMPIB[1;>9[*Q݁#_#H%vСRUbfGDKC'$8$2MW-lȢ9#jT+]'@HznfmFf|MԤ80nR:!%J҉;Lw(Gm_GImImg.8woiO1NgmP4&"SbJz\ka1"܋DHxkFG [ >}% f YNQ%{QI`.\8{YP72}7 P(OUL/\" r %U~>r5q2tDhD7 NFw5U1kObx"#-0I1ѩyPꆸ(:b[~C7BeJrn jo}e5ЛQ[J^5j^# 3|UD!.pA8$uDYa)1l\L%$EKHӃy8](٩έ}NfZDGԹD{7LKƇ{6m&lRa!hMfPOd1ϓia _f84P ptgS4x-aR2#ٯDCDÎ!eq^k$FRk$Q?뱝iGOޚk g109,~G |#H<l(+DS*PA茎HKzkBFacTl⊸tITwSH_ 4BUn=$DlԄTt}>)Tգ2dtBu88K$ E'=v4C[,%JCJKĈLsLX*AK$vJZ,3^5/v' z19] G,S8p`xR0Ip`ӗpI$4}I+'NO71~ސmuOfc(rr0z丄DSa?f%o|6~δ^KV. 4@g̓Fw% _b=aI!#+ [VSV6ޥ >aEds|̧|5/o[݆~QcϠ^z"In,IkHFK<31FcqmrD#_o} Zؐ6O y-}~`Pc bTXJF Ȑ䆱qww3c9Va9f*!;:71rƲ:e2-f߬[Vg坰ml!fDwqvsj)'1~q`{6֐'ƣ?cbjiϦH¨+!`w=XvYIc:U3d:[e4;C7r4%G2b\"\Y'OV$õ"9#>klC!0QEo?-.21s*=Y4Y`)eS)")^"!16JJml+fT!8zB=-9DP(ⳕOXJ3L=h4UTy~JuHpVEdRnL y~ ޒ5:ʶa4N׷icƐi2!>73& &myM3b[I ը쐑["T;Nt!;;vŤ]ru|P:vƀmPsyhuA_,',ޏY1ͫ۫);-Ä=x&u9#  /¯_;N"=>}0@RZQOGяZVH1IBhS.Hͭ9=A)8\䴷*EaV ҡ{%a$'~-ی\.xuzo΃eJ<'b4rns:[pj/.FFRܭ^dMg'OO{ »'nV\qQjgorSd(TB |$Ӵ9ZW!-(ao돼ffhdƟ'~5IGLih}!r<)ĐRш,9њQe*3>frrwb~3Fɍu|ȗ|rƘNnf(M;pmc,-I|nxfƓ){89]ai`,4rxNQ x}oGb.^:;Tzu4\;.k*IA*DV뚧: o^-o>R?(> <]Կ✺g@Yy7E_m>|>%oSjFK b@6Y#4zNۈ[D Ŗ?iӞOJ4I2D1JlْVN& )"㏲X@}\OS}D[0 rssy@NCĞElYĪE l{댛%\M`9$D¥B9vi B-+$>렃6(do)2JIAIAΖ!0bYMYF3Vˇ)=)o}jEAKS*+P65'b;q߅?|>0?W|7 nfaVф=Ohu^0ўpop{!+1fQ!#gmx*ux˫2ڑP8:ѽA,lV@1__^pJtSz'(fmK'ZA4rW.I 6pMױ{p8cQ<~dtd%X/5FPEV}dX8s1&xL0vdIԎ`|{ce%;bbbȣcdM_6OH$W\f$v҈uD c Bj[ԠOA"L$qLA?1q*rr$pzpj]*5UKGf3U8HE@1Hv -xl-b"6LNg==HhICQ\@KL?L+E СC*:AmF#l{,!֢h)ur( rH[*$Z P;tĪC}>Tʾʏ/]av$X2m7RFd:G[J&}u]=`_8|Bz'⛖Xk#>z<"c˺ dd \ݐ\ؑ:i(*<+ s(m7bӎXg#wۀr|qxbrX1Lf&+/'o]DZ.>} y YITT6idʏL'3O8w+/;L]/ڟQ (Ϗ{g ](]+нT {ɎxyDLEs'gBׇD \3VȴUF-~c%L]SٖCt Q-6caNYcɢ4*WFy, mAi a4Lg9mQC%>i|/ԿbN2]nۻÜc}5a+)bSF҆s3sm X OI]w^Y',1>~Uy.=Ybu) #fZkN1Nc$ZT:qc]OimYm*(b(Q qG cߨ׸s)4dT{8ǘ' FQo=h4>am.7ٍ~Z52` r,5Ip !4@ik 007X37 ˊVUKO[S| ߴ_hqfH"z.&Q{e#rmѤHm%x힓n3_Z{%k)[w|O9[?pѪ (oe06H+'Ip)cv]A Q.dSX.+ U@3c++RՑ w9Pښq7_^9k?e f!$@t n+~~?<¹bzùuYt N-u>bH:8sF=`ns>pbtQhxxds}s69e2xf2z[L7CkJ2Zh]6&oǟv\A u9Y{ƾQYhmxxisx^xW̯5L3ӡh h P kڈ􂁷u!#2)C4]ۍ0UdXtEG~6 HHt40!`kXI"SLrG'Q,ccUZHZF-uHYsqEWDeJb)7vlv/ؗJMCTe(I\guYeԝ)JzRD#$4A+TB Z!Ng[2[6ʈT_`D9fa9=@#+,)oRl?rUC*zRRJ+zR!"*SHd#)ӐI}b}岸UqüxdRpED(JDcq!gNRv NɄBi&a)Z^)Zhs26#vׅ(rbU$S1icX#~Hb-=qLc*4D:n[%[!*FZD!%-Rҡ%M)hZJR%0 Ji:n^P ;N[},Ʊwѐs,8/[љ2S3 -)uθθ-H*%;<n^&u2צH\ʸXSW{.?rrZhv|!6U 1H[iHVRPGJQ;KV4>eX['drԶ$sxJU5!néW0d^R*A DA%hJfM2nH B/ d8W*&LuE#dtVbzڞv@&LVɘMyj<'S6SVQb.Rqe6)F?•\;@k p$%! VHY IDAT+ʏa#hkJT+2%<%L; iѵ WV2y\!osk1 pߡ5T)(EIHhP(A``h 4(Yb+iO  V&%f3W OhJ#b.;Ĥctao'PykPun%-eTG7iW\twHRI*{_pMH.rfVԖB:0R̼y =Vqqx9l=k6"Uizb=S~9+^tN[إCMցb`Glydc#iQΰ  eY#nc:!@vV^טI49wz7a;n{Ip#I-2~/0 [dSHS0]uy]sIga mѴuAg3 3g^ / V~lcS#V9 JNоNS3']:FkJɬ{@kFij9?h鵍*ST%&B@"Q*amC:NK "R]1࡞#Q4}ss)9QלڏҮ9 ͨB$n:":b)fY${C~}spfԮj+֚Ia^3Y3~AJFikFh%UjN\2"[r]| ig{~cZ0OaP;d3 MRXugvGW$Hr$P X9*RB%D[ TQaiP2 BISɅA&LvϢ<1?af!q~{K}+ABop_?|~<$=HeMUJBERC6I .2]FZC*:DQ KcBwlpf`& o{4d>Y!-'Κٻ=Q/Rh7,S9~0n\pO-4 Sɐ=/''H):, 8Rļy|,}[~NڪJwTtt4gՠ%|ϼ~U{L}fN ѷ[?㹚Nc:];~[:]"g ]aNs&R%FvPS`$T M@[Ku%`J)xBX7Tw*սBҭ;rŠbc'[&3 9 '`Ė[F;aOĹú:Am b,eDCLQw{ob]zk>ODg*3VcVQ Ǟ{`<1F# MT.dYRuw58Ruꊄx`{pNxkߢ*rv>W_n?)/wwq|a'O A D(eM  5.n-xH)E,^)BJMRFuXa&) -LdFS1Y T7p&3;l$JRN-JB EJl%+N3$Y,IcCi^(yQfUL^NZhdβj r YfRUFqYa-ĖEA ւZ#uɶóc9fMo{KS( b,Jܪ*WjE5>[-oEeI%&L6ظ.Ө&pY|G>>3h g#ʕ*jru-A%Qk|D]#%rV&9.ƈbxJIj+ǰ" L٥&ﳭ\ )A YrY+6jd2W!uRQR]"W(y]`LcicH¡DFSB/S@P26M.yzZθ'TY V{>ic,"gfARąI; }d.)$a;ܲk%dsYTqLcDŽ|b2L]^l#*8ЮA̢qa*}Z]ck BДGTF$6KBQu(wY,j' iQpIi#\{BPd C WA5.C3XB\IJIb@Q?ƘJYǴ]eS(i@Rk$BBBUUڋU*Y2+6">G?Q5cB^#RV`#Qa% "I M a z3! b"M[b##y"ޤG- 2-vLs}%S%j^`i1!ΨK\)BFArj,=Z0АVn|qwq/5|6t Ks8+D}b b&j,wM6Ҧ@^H'fs{H5#X^4+)Q][<}WX ̧fiٴCZu*Ge}"šT=QUiuL8/VeXV*jRꇼkǏOP*ƠUZƂ1eh 40 vD9tFFmrqqIB_ 8ǰRVp $@@3"V imRddĸpF`~avnFrd7GЄp-nʗy|zkJK"@h̴80J\TIUTR ʼ9Z` ob!gdB+d¥8di4lDQ`rpv0b_f9 @\fvHmnpow_"E.K|yZV$ΰ[ƉCm 1)D+{7%6˴f0k) ]$R!uYl0W[qfa8G[)ӯyt;[f TFʪbbm6K=VJb|mfeÐ2n!ϰK|iA|d2^wl:IJI&47״N[XM.#yi>`vM}"(d'qd';DE1O\ mS!L6jF+юgeㆸGƂ^LhBo6731{\9\9G\9GdҚdm6GkhzYs]6M)#iW<->^t0}fvYMVżG ź0tQ]b1nwq_TuJT[L=$ um%rJJ0'6" nkĨ@ܖ?''JWxyċ#.M/DDOCLgGW^ӓtjGy=$T_I3u*#IP<(=d3}"|)1z墧)jKBiK- q>^7~]woLn&kVP(*j?0w''^ U(v},>gđ3(< N9V9>4B5 [cS9;9+1W ikd%>X颓 0G <ٞ2XWkx_ZMiBHjO74o6ӏF8ق!7؄㜹={Yz%`JА2ToHVռ ǘiDJl\Ǚt1=VR@{"KQPSٺLoщg{bL3VQP5%2.^x? Ut<8b'v-$W)uu!H=+~{j%4__+FmT+J k=BC ˍ[!pdɪ.4Sҷoi fQ x1#ł+o8Zt[|x]Ľe$su^ , )GϦQ$ʡDv5LT!1MNqRsZ,iJ({qm y>Q7!`!nc,8^dg5i"r<}ʽ3/u3^QE~s)Q Vw 7NY>u] E"%aes(S̀٦K*V@!_gm&RY"lBfgڄfh`jVo2QE$`8 orh[lwNh%Ko>GLYʔ6p\щLH7y< O%5tc#op-vnlA:ѩFC$Ō;!(Ly-R uMI4W#MLPdJS R*`V=uRRDFK,fN#Ԩe ܤ8lT-Ț yW!LGbÎѕd:yF[#{‘ 8nQVN1UH6`OwPi2b1|ULSq-Msΐk|KQ&Fc͎ i?ϠU&eW|2,;#l8&cP9nYqJV#K ,G (G[ԙJu1wдܡk)^Q&u"C;6w埍p4<Q@NL >UxytB)B%B(uU.d@&t_xp3ܒz gg}&yU}##7[R%*OP#8K{+:cgS&IG6ZsUkx iz3:Մ .o9~ޫN@AjTޘHr,?(IHU"9Ue$HoS5vP5T+iliKx;wPw]=mAXBYǪBՈ^&T}Ϭڗw f&Nդm5?dNqX!TLgwqw/U?x%v&UU?u]!J.[e$/5W dJ}g6!Q6/5ÊzZY[UyA`yxΚw3ҾJ%l rlaܼ8UMhS[bxQw?^d8p])  4FD=lKQĻ V'd[*m鲭\6]>a=Q䒬TiK^4I"82Nk=k~ʉ _]#L "1EJDEYDG':ՔwxcT}E㘰kCCvMJEl0MYkbU7XÏP@_'+V41ElUGÀuRQ:k,1SL%TSybTwm"m* &!) *C4$*CFS2WْxcM苔xu7*q̭&av&QtrI!mxX6*.;a YU 7C[# |`1]+(GsVIetBV!69 *g4THD$ĵAv)."حik"DR_[>ݧH d(p)kNJQ1Ċc1bLo>!u5ҶNjNkڎwt@[bvKQ~m'*%\Vc~}Dk _avbYy? frQ PMhjfxF!)[89?8%N\{,E[ܡ)o-"n3ZIϾ 1WTOGLCٹl,tAUJLITD틽ث; ;pt{6I%D1>RHj5k:̵=c6!yFG9[lΕCnm@tmR^KHv2QѼ H0PQ$lkj*J2. Sx2HU&8GnHjD ;/MG?.G1/Ÿo~}{TUwo ?7 XTn^YL v"!>~EB2*FM=*T K5*WoMgNӝs28CrJFnelX>qc 6|Zݶl:lv_~ Ы99pk9nn d !ZSZ{U򖊯!dT}uQݧ&'k IDATǚg15/\#Uu_1Aa>X##O!1?&m H5)C!yuy_bMfhvlNJ; wy<hK݆j'tZǼm챋v+ݍnX^m蹷h% ZzGHZt\;m&\O1IzT l4Y3P& -oF?ͱ]&:pmj*1 '7(iIK V!i%32.(BǢdX-fiyaj jLsc j0!-}A}x Y>~Ĺ>O1f$D*VEuj+G_B"/UZgYѨx=poAD|- 6& gљ,5)"( N)) 0v뗼c|dL(dB&dQ͇,6ycZΦdƶm=qw5 S9/E)lR *,$.,"`%*Yu=[i q1ʐE*0[b['+W"&dm! 1yusݡu3*֖Ro&˼'Vڊ}cd@jgce˗*wC`$ AmK,SmTs@x GoeJQBPU6͢1>sc&GxM ŋ^Sqzp-Ql.n?~R;wct'vv~̌3EiTԢ !KZ)^2QG)9_ ;qwq_k~o|o|?#L)~w?? ?3kp;^Zh?uC} sg;3ĮχTDhȋ珹|~˳RW+ySOxlh%YD+DBCe?0 ,~.k0rANrp8?_8^ S"jߓdhdhEA=՚fF%/:ռxe3 L/i'f\ޜ;lL1kƿF@rQl~ǛY-ꎀ&pCϜy&ŧ/zqH;жǴ !ƛ)o^j@ k p1;\)a+h]uƼ`n6{ M{Y,i=_tŋ/gn5ty7CnVLy\>Qc>Dya=adΜ{7ԆPC!ox>f3ϯqԏk'Pz -A,^~J:W"Q#01H0IH0Xp4q.b:g 5 Dw2Bf xXW9/|f,dp]C|$\upq sH674$&$dŔ&;6x{<|/>9]|}/π[0C/E.JDcO] g|;ǼZNG98OO&zDK=Ku:H2|Ȼcpː22_cmgdT R,>{yOa -t>z\g%@pp:.}BCٍ"L'&F=ny)P( [aADW{'KnYXΎFwA)QW<,aWX JM% E2 )*))P uJ,rdi߉;;҄)??4_2WWW'''\^^ףC5V+!W!mf#tvN ymiGIVzᲞ$;5NxPweE zX~Ff6yrnM]jA&h&fM4ᜦv(10:OBr'![KǤpgLF!RTVOG@$Z{VOf|hͥuDhHrWUUf(eZHEM!bVA*v2k2fV\{m:,i,`FG]~\~rH`-i4%vwyxRMymzM([(E]EKbB`"eAmT5Gw;_, &ޔN{BϼY-M>2V5+tMHe:9Y򘺒5x՘i9xAv6KPUߡ)IX3=V] qAg,%$js\_}Ê܊w럢(9qmp'shV4 +@349[;,6 uNclAs6$ Y%5nRS j^3K$Dnbm1`0@ݒBXxA:qK+ 6M""2]JedEPDF%HSŊֹηXyL({IU'T ׷(H7Hޯ(0pWdFd3_1h ᰦDa}`)iY+T$6,.c"5EۢdVł(rBm/ )/nkF<#U*UP/!i݄A1A~gܿ:[Q%-Px+LeA>H^gÁP .wlEG?B]ڬԚJk6 `4y6}fwJmSiԇ U_%VEQ ˘S55FԘ=6<5譄>@$`͉8;X㳮}6xY5|6 Qj̱R,i|#>;;_KF}wßztOILb;a"<S\rPgB}d+ gORNuM?j%TʃMloL5XnUBJ TĒSL9RR,9ciXX[+X~cON)Y }ݒ9cMiCNX68`3R@O|چ.ʐSfqgd m 'DYosgs}WʾL5݇D`AA),v^i%F>+,M`x v{+f]zCDQzs7_rXĘ&rZnӚ0rmhwҺ]2 f`$)J\R2[R"Vv^5iq[-=1"4fu[Oajqqil\'<U64#:dw3iF;ED Xu^@a` \Л`HO3OP1FdBp9q5ſD Gc?R11mmwWy=&h+ʶL8m1\C7Goi3{LӤ:}mB4AFqw$9 ^ Nj( 2P숃5owb;ޏ{ӯ0-:|7򦄭oטJHONiS 5Q0H5qVdȈN5|TW= ]3o0zF@OJa"/Q0-tG9WW_aaca!Sa_f_g透~6u-QՂhF5'4Ϲ_~?x-k_W;/"4|6BOo}~!UU|[S x/Pab~I1z:ee5'tjKBJРpUcg.îiQx sHqYqU+'PɸÄ^oN=,SAHԩ*i]{dk\NHB2,"5S:!;%@|KYsmj!:I@-PwJjQPzB"5n갹 V:*cxMg=M4nEWbP$rR"ڐL4ʔ!-S_0iFs0vޛHeg+<9B ^__ Kߨم.9DFDz6*po/3 FH0}]f*"sS-W)lZA]KʡP.ʲ01}_#bgҬvk\?,9v.qoScbW2YOL'lmM$ Mn]W]YY :) HRJӣ tI^WTmﳡ2rΘYr]Oyb UcjʮEY%|IENLBsGhDFeUC. !U~ݚnN2t?wnEqEV8q7̂!kef ,t %kа`&8"h ^|ѯchkc]~6MC=Y.|v6jhځ(@ I}[3]ň+%lNE62F%[!iqM/_ O#*LՠcZR\Cjȶj>X1XV%U50F4(=DVS/-2Of4jPX.)3cH*m?~O4ܸ>^ ҩ iWZԐUckʱ1VQ%}hJo`"Қf,h"h6wKd%]sP%$a:lTS7(ma_d!$X\H? *tN3ǔ<#1rYGyGfo뚟'Wroo /}Rfր2SC8ITkbUCliSPcS!' i\pE}Ľ1"x/}JZk[_˓Ⓕ唛 6*@;)esL5$Ͽd[^p?nXl=֢! 8!jٚ%Od* [OхQ)9n8.bNhFE-,jaA0O )HEM# *iQ~TָM`?|ZS0)Mt cH8\_1,hcf;tY z̆fnu:8/ 1Nj+NtK"w1"{y;2)2$ =VA DUaAGA%Qàny~U {d3=D.zf^QPԥ%Ix:]ѿ[2[0]k+YDkbp5ǶuY8 *܌sks;b^SH 62m Xru:;\:\oh b̙fKQk;q \pJ_Rʷȵzy#9>gOن!NQ*q EtI.v'îJ7_2Yh1=AsiR֒ڧQ.o$eȵq'oxv.5%AE!mQ(ƛ eɏ=g{$X/w7= mqO!+>'lDjzQʘHe9/юطz힕USD)(KK}̥uB$(6nbv<"tR|X_Po@$h"RsY/KƜXkRS-.)ꔕ<6Z=O(CzֆVkMF!2-z_)h|d"Gd2eۑnE6+z@$4X<0"S>zY=f qIdNwɒ'K+K\'f#_UHa]<#<fl/??3M? Kz NyzvŻD E4wJ#>*}R50F Ưc™$!֐v-`/5e)ʷ,2x_S|ï?e{!l%ΥJ$u,QF֚O/쯩ɯjUl&>|%Ƨ55YCbCPOx([>k^ɯn!A,zDK$6]QkM%-\C43sS hi,S! 6lpv`6'c5g˺υw;p%O0qp}Gj%+b3ʞd2uo:51)2dYuY-v@ lqs& IDAT"-kÙW1[k¹q{E0 ގN] Mi"hoVXFEt7+.8oo82?p)}=ٯMap>f<5| %\g%ۧTҤ&g"epOqqig[Zֆn]xWu]~}u$l.:zeRl*}_zL]rocgͿ M?YеV{g7~6jqO1vؘ-6^Q-a  9}ca_m#:=+$K5`QYVUTdG^bv 널"uQWͿ?~!Z$uDR흿7~h.M0g0n7:",b^ ̈́+W-0F #Nɵ]즦91/>uEp_O/SPsZ" .VXiI1=5ϯOy'W--gKMJֳ7g7qZ0: >#<2.>^ie1dTTa{ x oa> a9qn9 'p^Q* +͖Q$2̹LKg}3 h0j rc@̠*MZъhl RXƱK\'sRNY`ShWSOgzGOs:֒%=IwZB%RW#"M)lV6ZIJl0܇`$f"zڷ ~p3'.y.( J$)DHiUenVjl$ &4w (Ls=#ХvM̀Tc.j̬JDR( 2\XGN] V]T2'1s#v{"v eYH̀ E,- nhc=s!0S.Kך"z)1{D{dKªjE@mIUX }l 12rF%-YV18ѾeMյ]6B M&~I>}b=dMU!(R; 䴭ָf+ PP IeJjiҸ&/hއlAW=BqlG_ V3.)юV#\&xI[H*LA8=vm VF{1f'#-m#ؘoZ!j(2*UF@7M6*`h""+ @RʐTMT#yMb5kƥn$m}鳫۔4rY[ܸ-֝@b-gNc(bcic;eMgvc!4@I+f(! f gt4I3gܾӃ SBkQ6cVWSJ+GQ8~DFֶ*T-H哴|*i1wLn;ZюìIzfY0oy,HK](lJs(ap(pwt6g kȽ?k-ik"c69,Td !ADx&[%Xkk6 nQw[#*8ks5;IM$ubw, A91Odx2<# MnјNj&RcH0 U^N AKkԦEXQmd^-ZK3*-T-vZҏf F3S##k<̀d]w'Dj.W9zF%$M S:CB'ع3Pseq]q9byiD~bݳɺܥSVYUH:|*"rXD`ܟ3:1XgSnS)Y9@L􃁞 Xm0{5fԒ~5gP/ Z(;BsKjT >)rIu={{ă=b#;1})7_M 6&5! 8a#k :`cJέrP4='9Ow|j/feqjՎIyϳtXZ}n)CYAQjb%vWq Y2~'\?l`n7d[Z,t"-~+c{oOyk%䒎?(1Uf#!mѽ}o%6`lhl n? v&"אȿX7+qEϺfFlN[(~/e# lTeʉuHs*?p^ AiT$!rn3}q[S8|e1ᮘPz!XUӲzo CnO |ź&{c& Dx{Gy?\4A2ɯWDf) hS@;`Y5Q.puF[m?F{4. %K% :rld-O8oi["ꌛ~JP:Q1ڢ--h0k`ۂɌ31= *Y7ep@谦FtHTQֶ!Z`gD6_.#32,dPfaөWh%8\rQ^q#'\n Ka,}.3 N %;C*aV>e۸Egk G5SèyldD!m  %6MEB@Vx93̀Mܥ4ldT Eq(ɇ6*XINls~Ƽ 52% 8vP#5-v6΀&N:1r 6mVG]VK밲fMkB1 -@ 1g (# 'RI' BNgTRpGm6 #ό$BY> =.-oo -> dɄ;hHxw,h "%;nFiJ]y%/L;·)lߑ~my6zCkY\XdykZ-:)۲B뀾Z?1gQtiԍ trC:gmX6gbVbxtr9&a7&ƌ3r9j\fVu4Nlx6TtYQk[uwjL sEJۘRql_p8irtz1iˆ~0ħ%f倇jHڸHBD ,A*v2E6z?ַNz<#1 |#OOC] J9 dְ]r>6[%\yx0]ӽ_wW+XsQ E`Pfeh愧iVsV yPSase y`ă1c#Gv꒺E(#g 7Xh!I;&isۺ/PAu Twwa7D+:Ul>zVX)8-θqӛpqvm8-Ƥl,t ?Z}p ѸXcpOA.A܀#LX \<ń+MCMW<Hd@"B>180H` TcqS s{-巌k0 r?ǦDa#eadd"X1_?1M|7wc>q_ӯ..o )vRӏcՌ[V,);<, SiqM)_%&0mnyU}E_.- *ȺDX |ͥ*w}4~a#hw71]+y~ɜg4IU91e1g!4r&l4]VTVMk).;O688䊃TY1wP'|q5vէ|lMڨDOM.XBKv{@s\f):k0#<#?`~o:PKG1gbŌ-H-vDXDTbEkp{}6w]`5`:#>"9g<D3׼5/1 1` َ ^=??Z`0>>ưDDLȒ1֐K?~ɴk~e :̼#Db7~$}TV$ˈ*"yQ~q?X( e6H&mvI6վ7JşGe"Z =wM8[b5FV1m}oZ0¢ƤƢfZTwDܡ _xkˊ.Bqസd/. cnvYZLɜ.Ŏi}M~K-֮uA]!-A L3p C0leo3Yd]f07z\q%?nnAĮh11oyf`ЌoDsqUDq\q\1d8CP!_B[P!tgx=$CλOW'SnrԷ&ӻ[.ikn)_OgTMYTDd~բoπ7ƶF65JPsY fy9gs%cqNF2d% 08 lQ")1PO[Zy -/oe- o s)x}|,~O/[ӏoSy%QMՂ'ÏUJ!LJ:zEr ?E5Z3SuM!%/)BaTu#Y3B1mLk~vu?hFg|b+Ř2#B,0W}̀y=^ڨ$_-^~ˑq?#?NcK8W11 B+}nW8vg'Z$T&5S'y; IDATGy?;ԈW F>.Od4ؘ4ZkV %f0jE\Ӷt5Mb&!C> Cا,@_agE[Os ODװZ|x}|@ ٔEetI}<'SbqJ'qY]~ųR"#P#$!ƜJ;1rMufS8ES%δyQnX9q ;Kխ1&2ўI9t(KҶ)cdꑄ>%6.9-x@n`^1IxFF+QMڸNИ57FрX7VK``?"Aꣁ53N3p<ߢcI;v /Lf(2d4$OZp5pQNH0Û渓 w6Nc  ~K(|VZ C灡3#2vXw,d+u˦ET= A~-f]t !<cfiTHQajo ;8Jnx|%'!ѳW2 ^z'^>CIMfuהV]Ac%¢¢RSH<%R0}&w튻JYo$Ycgs #"#3+nR ЋO 4խ{kʈ`G'}vyԃE?BB't:lg-EdIJ ?r PȦe0^;nUz[́[,1`UΛ%gW fk +*u8v M#*}6j1K,dخܟ qczlKq:G%Zʢ2-$o,aqQ{}mk%jN;XP~06@k7=3<M~ﴁBz%<ͱ i0(7 ìҬV+Y%B>l{MI5J'oLj4H\>Idȍ|ɟ[{$&Sk}IP23IhGS@T4ˀ"y\Oi_s<׉KmI}h/< PL[W&OjTY,|0IvZr \[`4*fĂAe8_3ājg5RGT\.r/f E>#} "0 b3ͱtj[k#BkCZӳ6 a/1F5ө|ܶ8|O4Sj_R!h2$ v1~ ruúb:!rPS 505Z1U + 54PBzZ%n0o*a!@/ss7gk7p}}6uo7钗>rl\ޟ/_;~o !¼jMI4}Dm*kR 0 c>D${IQx{x[S}Hzn'^kZ4/=vm> w_S5nN9:f3MPZEwᶫ[ĄG1xͣp\wdr݉q) z-Y[GΜsaTY'Xi[(IZ \k.#;E.y}o@ƛp')ICTIeL&꘻'x,ͧ>¯XSd@zPJ*-D 3<3+ZB9%a;]Y_ W%ϼ.=HDJk"䲽 i$\n:G:Ӟ&~^/+J C7~Gnwg&7/ ʅ!01mVbf)`)Bk|h!7KW|ꢻARHΠ=2~㤸BZT^,Ye|S4Au:Tsꃂ=rRuNnqk;%R=١7@ ]x( {s{>Kg ![3fq4d~ɇò곲,'}ڈcb̒6)S4vZ^ ٫ Y.\E0!0NH4 GSrP uxTQNM$r\VƐ`Ȫv9qDʮS]O%՜b,{@,S5ٟ+j4k0A52hg ~Cd5@d jbJxϫ3eSN>l>#jjn='~W?o`i=GzGdOՙXLG54* I)$%45^l:ѧKrĥ7|'~"ࡘhxoFS^6 o>s}~/;>}|=+NL&󮲃ŲfL1ے K^Z@_V8NJh5K`8Z?Z3,y,{9e!F#쒼p(w=xIM!=Þ?>S~|?~wr%/˯>qbpr3BD'M$64Li?q^揤?,r!}F\32lR&,5)4E0 %^X*Uacⷰ}~RnuSV:VAZ[Zȟ=yg~>ibP {b}U9d]8ֲB[A)![ w$e pڈ̶B|ba)5G=G=G{&jrrdM6(ՠQ v*@k0KOKh34u@rO` ="aTHYQVTޙ(Q-]HSE SjD̙6MV}vN.{gjm#"ŵ6(dIZdXafXaN(움}iVv?@S+eƎ6[mhtEtd B6vV 3qp@?.Mf2%+/r 4AP Y4T9ac{`I撤I쒮bD+MTN, E11E6.R@rԿcHh AktD"H*dqwʻ-sn?Z-2aq0\6Ǿ ԚQH96(ΰ*,vZBs0>z3oU'GW8rC\} SdZf{5ӗlMexmB3J7ĄXq;%pP[2-)|hryD?/_mq1&k^R{˓!ᐥ=bGl>'5%\.t̰tܱЬ0T)2r9&Z1ʭH,QJsUާ ;Fp5:c?q;~𔡾b 4؜q-ΨudX22~uAA56F3n 6Aװ}z4ИC0f_Sp JVf쁋'.O\Pb9uD8TB'7QG*S̼Լfbp%ׯ5=x`a0Xl0?ѯ蛈ɟ) ڿ=QI,]vlpG1b9/\ҾK,=̺*r+Mpژ3nwZ/18 Ŋ<6bӦ 5esMO+f[ȶ@}ç9͐7SҍwL9\s2sBr||>cXnĹ HHL^7<1ۜ~ȟ3{uϴxxy!E hԭ2, 7krSC,WXӘ=y8!#``m h]ݰ|räxqt}Gg\~r ';fkfkfj|0 Ȱ?ۑ8el=8 5c횛Or%ڵ<½pc$1ot:P/<3&OkiIl k: X r}P`|(_JsIԙd4Xt;f;8©"{޶6y;^4$!ꖓ⎣RXk<+rd^}淧ӟY9}n3PMJ.g~Ng rxkf- g /[N[:WTJ4Nӷ-88ĕOYTIzNJsermpe1/^$7ox&ӎ!Z+َAD܋3~`%wTFfILf:fK,hff4H,1`Yw1 ʆ o{YZFӨIiiTF4JaɂP?Z)I,IM Ղ y^6/͏Ԧĕ*k!昅>aa9j=EQƠ%7"3`R_ E {m[C#Mx8NOTZj:h'?j'gJZh-fN`Fցp̖P-ㅸƋ2¿K|[QTS #P(Ae+Bژ+>\^9 Z]-mfIkj/ g=FȖ17~4$4$ǏԀչeOp '|,.Ubq {)wS>Ϲۜ_$Ktmm-#3疯g/Mt'Sީ]%6!o?6"i]k~Ly:b=-oo^/?w&FFgfStX4O&Y"CV7DG2rH6PQ5ұYk45 StjXe̤aNIKZd@P <>' s^̊{n)Mݔق=y3zԢHN3?3Hespz_Lwܠth7#́+z[Tl`S-̢k]ygy~O+m Mqڄ) $v(FY 3ۢ*JDѢ5leULi1w d D8T؝0\ hfuk5.IuH9(Y5 &-tnCkf6^ChJ.`-kc.i;JWltJ]mĘͮhmj* YP1=(PtA?Dg#Yu]vKwSPBF)Eka.6dY4JS!Sd4@)&j]b\z,Z4bvx`_Z6e6Yb!obEpd2/7)?*|kJt7Na]DQU) ( ѭffE{@&CPD6ޡJuApe4u髅BB.v mHR,( Di=I34B]/ -!i]_J[@EׇƊCaR Ѡ c]B|gDFJ4eX$}2ikASKFjeS EPPS.D 5iA;A;A"m\Gl*mPiZ4CVH;,D JM1Cmͩ4!N(8Č%z]`g11(ZN !g!D%yn6&EDheø|䨚;&Q?K>pĭG258uY}bPDMhZItw&ל{|gZI}MK Xt+TNkjuf߆mFFˎ t)Cڗ #3i/qyySV,?9EG{-F7r5&HuS![>Yah:Ih{9b XjCVb Ff>Eitk-,XVe嘳 <86N-MɆؐ&001Q(`f]=>84/glW@5u mE;d,cyT|^3Tk[2_^w %PWpXCSL;8f]I jw{^׼>*>O]V5_hiKuNNN]~b_8)#k sx-bْ.~™'Zi$cx쒌\ ⸜SXz܆?t)|%k~/XJTL*:傪q+z2J`y:<{A8-fK_ ew,Nܧ>Egx'^҅bW$g=үE'(z, sa saAcI!˷ք1ىAvb&Yfz=# wm,Wؠ5P)S |E8_̢PEȕ:5FSa6F]rq^r"0L3kg&(7 IDAT[XsXXC5YhpuWIUJh;NB);P*̸el?5f]`&G5ZK?AUccs*]bdtlV]B$~:%Ď/Kĥ87(:Πonxs vNjP(Xxw;qupfsHLiF.ާ 3ϡd"l2e (-eێGqvd8=[@9 {`)Z9W\x,䕉%JTAL(或{FlȨ-xs9QHXX.6q)zgy晟 ?_wf cj JNK?B޲Հ3``8{* cewiv[I"m1/i}˘)}wc_g( pT.rP5)IAXk=VXYC\3i|Al;E8fV[cYCOiCIo02?q I=v+w&n*Q15LM6F,]+U2 il\vX\ǿX̆|x]d=J09n%67?Z ~}/~~䤚3I12F,[ffI c|MfԶ8_2Ζ%"(wy7?Uy6Q)A HiD{Ŭgl=ů=#ƎcYi6Yi:qkį CK{vƑ31rT"vCX=$4D\4P4r͙Ca蔦NaȲw' q&>Vic/=cx[M 4{Bm)sJP׊_$=56 /K ݎ*TTzhM+o Ԟ̾c2x${$u1/(k>OjKXm _FBbl-7[ -6G!Sz/q-v7I 4I ##}N\ h hZXU~>覛IBsKIAY$KB63PB$m-uPTAiYb8RJ"#"c,fdVT KeU60YCvUK>n-8N`u8FDa(ƈCpψ-)5!7qɗǖ/TȣѠY^wx~<3| ?k[1C-eE dzeVJXCL? N ZS, #У }_!tB꡻9*ϙ 6;RH,v"+mBY1 l^71J`4HudEXd&o,Jài4@-r[K4D^?›aJ Zg߄T)x!؛>nP:p؈b}uf#9ԙ4Ĵrlym=$oMܠ, Brhˍ}; I7DzCrٚqb)˫_jv`lO!CCTꦹq4I됧p~ǞpMOh4Vԕ5͆zw(B-DOjX  2gv\Ox߼uLZ'=R&sLr j AKs|2>ujT:j_{%!h#݇c }aaXouK)tJ'.[#6ZH 1 6Elzw2}Xt$NU+K28ꊩ~;#@؏w1q9>gU[+KrbcʔQc;XQE:֨bE>0MCN&8{N-DKy/Q$xq[@5NMUc%Gkǝ`3jX-yi4WT}bn3Ǎc$Mb̤$ $LDC-(w6c00G[Lri4kIW50+|`&G@7 8yH~N N{XNS8""}E<ʾ@ lkmےo,82z0smq#qČ%~1fS ږ]PMWGk'FӪ́1y\SY}sW?bo)(5Rjdq5'U!x&N:a,'OUl cE ʭQ^M.Mz= UUĖPU;dR-^LG[gMlIJ0dݎ4#֕AH[h=xqNq'OqöAכ!zX*(12>sc 6{3|vM4Ȧeܧ',!ybRGxghH-цts1cLwQ2 sɬ 2D/y ?=3<~E}ĹnvƇݗ!H=NOvaQC,`!W"sbjvTA*G&L9S끩1mcrѶ}(h67 r5LObyhϠy &1+縝s9> hM%L5oɰYSd;fo6w,ń%Մ)đx|ڤ ־a%l0 _H{g'L%;ಹzŲ2r#ow( <[bTNu\sN|_<|4serr2o& #c^1Vµb9^sGY%!NE@oSz",i}/?pJPA Yc3: A9Z=`xS/e /&īG^m~|sј3^q=9Wtɱ83dEFKJ8wal iԻΌ& %FSmX D|I`?ג,Kzyhn} |fa8l}kNZ9/-3v3o*R}-l'tjjW%wl=$qT|g;/lfثjWh#ڝ̺Ĭ ̦ !̎1r #z/7~nX > zGE8 -[sysvmVUsϾ+27ޖq3Ek#$U6֔$pjFUc~Kb-,.+٣hmKk|;laHaKumK&787)g 1VpNvJ@]U65n?czevA*UW':l;za,@3[A*[T656Faw T7uEZ1Y0[YM'd Y+[;F`Ț54]D]:bٔ6%kv׫W^b ٤}AaWt+Nx]EL#Q*eJY dJg6qKC̤Y[fےn5aUrk% %3h1NL;NjnA{r-Zp9O'x≟+?[w\?7&GkϾc e ꣕{,a+a%O>n7D Mp%,}eP9`Vcq/3tOMv?xnAAs@7܏ka))VbXjAHįO$ 7Sf TBP_ zbE̚7 h^q{)Y܎x]q`#5O޲6|P.]5M u K' ﳿ%\. ٘/Vog|3[,d3X|a,7AJd Z 9;~;{NO7DCϸlq(߫b`p% L8LK[]mRp$?Кkt?`B3d/ uK^;\5;>he9/9 ę3 Z7=D G?_fKy ~&{S}ގ^|c+t@GB˒)J-{Q`5 XaM.˿ӑzoӬѶ 5(O  :&ٹͶ2;S4ZAj:Jt wهUgoΙ]ǿ PXHnp%qTy)|4(ѩ:%^JƩAjJ-AC>f|ʖ/"q3$G"çT 6fHB1T!?m?~Y(@8ӫ!(G*Hhui}iX=F1"W,l'ȹ9{,5YKJKE!B5֣~0PoݢlP; R\'\sBS zV3.E d$hc0S3TTɶ-e*+uXZHB5{-'{{ڵ}]ͪ/HJ(jU ,Q -iLJC*2S GvlK>$I7YӚ0epdLp- MDWJEςo?ſO<vlߧSXU}Oc(AHtJt@7K4c)`JJR*REA@Iѐ>lMn}LZ\*ܫ2h13̌QyBꔮIZNE@i<'#1 Vƀ ͨX[nC^[/ Y,Ĕ>;bŘ)|1n xks^N$[[6HDטaJ\ׇ씀EnbfaRHM1[L1 %G8b17hY+S =wh3Ii3Yvʰ^PW:YђnE?YO?"s-Ij9KsYG=:~Y#n7Eǭu'Ԩ=Vl97tf-K7`X {OyR` ]6ȑ)ȑU2 Mnp5: k7?z{˟p"l e{gMJf#om0: S}Pîp;)n?IE|2G X0P ľ,9zZos &1;ÐC1iO-KIza/7rn]ٓcXU=VeegYy{GskfN Ɯ^x)VLcqykT*nP\g4 ȷԵN^V1j0hV$ßTq!:%_#![VI_Ӫ> l%#\6NTJ](XeNo|!fy%CEǖ%} *t*NƠ[Ԡ n l: 1xР(A> f#Bn7G|>3U;]g)PӠ5ނ5 5jf?$U\mS46i55}7"p7@ݠJ($mvv!BP' ֫p55Fb-UL@ ͍MM璲#)v71zSsx'xgV]ݟ 0xi'1D1-7E@N@~!ިdo5xQoTdHӠ- SDC*`CQķ;Iz$bPc x$1f&fYd / i?+ 71) dѓKx>= Nuy <>gnfWxN/? jYJ]pssrʰ~`$lUHTQqMZ)Jۂ‘]"G17YwTAk4BUKb9f?gd>M1jWKVt'%^Dxe0Rx5`uXCڀy:bY#JѽYsvNfdLxՉuYx}n#>rN@OD->n[gK*O##*"ZI)J) r|v݀'[4yDGo~8U 2Ct[B">nRܱ0,1?on5Ex"[2,nMKh+F8PleXx8l-VVD:agSap=xXc֣^Տc>[c/uX]޶/yӼmi%H̄5yP)g貤MAZ! G[7H0  ,b<6tHЩѨ9לqp)9S|C|ˆXVn tMٗRQVDxܩdIhE^DPn˔v6wS&Sf3:o_1G 5~ůi{`<#c)s. > z+~u-ĨF1jqMvaS]jKq![,9N"eJ], ygK92jsڌ>V3jEQUjE#o,Vtn2y-egUqKG8Fo#):' x*Q4̪ mN1^>pqVG/8Vю:+޿OGнauZD [ <_}Dw߉߰Q:ԊFT:1fΛ  sO8 zbA7_rp⒋?r 㗼^r?x\phs(ȥB*<3icB5Y(6և݊}Ǩϝ^g|#7_g~W%kX]޷pROѵ Jd.Yșj# Vwjxl`QQsþC_#0;d5 5HTuH  ZhH vOal3J.Yo;nxb/?rxz GIۘ aS25F,cmuC;bK@vP1HF)CgG^oR*bwW8^L*lQ;eSrWzԭ*׮[MTMAyإXjL8);o1=O<~¯([FZ^XXGªPMI:S P؏Aא=աc@k"B("46!Rlm? :B XA0ln$ :}m ~L%JYb İRd!ɅQ[rƩRD`DIS&lRԶFW& kK\/9nl.x9q2њj;$ PéGg`+"# |^WWϙ)RbdÄ;n1d0TT(TF5Ҩu\CԸ &‘\<4)B<4HB+jfhs: aC1@Gг:.Ј93W|Ԫ[X8~Thn")3B)T 7-2bcvTuFy<[&!~؇ɫU'%=gxmƨ47aH*lFY![.yl,Ui%2l=eX c ?0  SͺA[ĪGgǜ$QN5'p7F 86jYK=Y}>gDxO<O<;\bIQjZyr\< ݤtLЄ 4f[ %M|ʠZ^9\;Z/Zթ}b%{ Cޤ`j9^_d`_0 z/& |,0 Ye=G<)MH?#|.sXV1c8b ƣTd*!_+(X#̀D’916h1շNVjj}Kޮ_ҵּՌK 3FT .Ke'E;#gG(^K\Hm \s˹sɹD5L ʮŻM[S^.?YuFDǍ>S\b-S 3\^+32TpGقAo#Vq\NI*L0MhcfAm ]Ũo0 WBȴYCQ@U JA 4*4{yq6vb/zʱvXkcG;m #CF#ұO̦>>1IM%,F%FV[2R 6W.3>3̴ׇ!fi Lqd-3嗌wmWѪ :o_BxE4gllvwA`%z.P5aL\Lij9nn9VnIwUiP`[=`u9}(x`;T)#!e(E1 NnĄc('!gHò*O'x≟-?cc/r'K>bANhޝ2@z` rݢr,BlةP(P(uu mtIsFk&-5]kB X> 1`]wɖ6[[[eՎWkk.K.k}@ 4H#wQLksŐlelFJOq`} V2$ķ>ɍOIa%{^l ^bUQx˴ç>~?k)Qxl>%~PF\܉Crř{Cf[(t5gG\3мoX+z Eo0iPDKڠZT̴HNl#\K*]ezK~igSW\駼v_R:^q0^*'8f|]H5Dkvk҅  UsN_)Y)hCyR|"h TSU?x[M};i7FD G;r,YM3i1|y_ЎT:-!T7'5fjitg0e$B),+~o5ߺ4cޞklDL #DoGQ~_ Դم>[:}&B5HR*7"&~1#9^=V¯6",<.68j;QӪ ԨKIuXfF=9,n9+> S}čqȍyĭ1A Б:PQ7tˊk6_߅l>?9 Ec7@/ kNkޮ^kn|3Q90*¡}΀A$/d Z*QxJ֢%vdw)xWOĴB{^Ay8[&웏4Jc4J-P]O$*;,vC˻!~kK:svr!Ҷݘp ~47%ãqBi E O<O[~0<-NvV9F-tҷUW` PLڤ-ZG;'hlJ Ie4Tj }{ɡyHacP %l\0]B+ F0j[2A3Vev;%&g i.XnQdm0saI}n0j 6 8C&2נPk0͂Щ 0ZWيi;BA_FRheJ]I:Ң:T$VTaWlB72s\+t [Gj: kR]6((J]*Ieya42Q0+:q:hJnU @cvLs1AH)ѴMDFYe[ִU7pP$C"9B;1WZ*UW@}.< iɜ%mo9lI?[O9diawYFxFLk( )KFP I&@kjtGO9ݨ0Šf`.,WwւIzixFkĸzT2 %lFK;q@5S0`MJۤp,`:8 D:8-Nq+hHH_ Q ]eÁx h#J zI}C&&=]o&jL ,Q&;l-jrFq9e\M D瘢Rrd+PAۨԉA|6]C`23Lp_$Lʖ8$ä#{7OD{\̱eJd[wXTCE,shs -nqdF0L9>7YbtJ {=RKX&s|{+be"Ѩ,Y7)F] jJ[q811 .yeQ&EeRV&my:fV0cpndGUvw!>Ocf.Nd8ՃER^IgslŔPIqH?_y{'xr|ߡSRIZn"RvJ\ yȟAPtN7?}H-1YCT` jѩq;)mȜbeń;J7/=1YJsc%(I;|ܜis.w{TΉwhz(-Ǐl>u 7E#IQEq`9>S:v:|uuJ.J QrL9X8XO 1a1bQg,pP:&(0tgSm4L;{+R. t@ t m啄n8$b>muggzc<4uKdZMǔւQن~ո{;oBd(Ƀw@O!$I,ƘŸg"<2`vzLO|cSD:U`iX$ƚ=8=Zsvbot42TF({3"R#bRD*M`3e<#3l8rrnq:UOT] 4d{IN~l3X%IҴ V#4u߭C"a|ᐚ.N[y[{4OG?g~rLqp6D&P&QhqNOpO$xfƒ5bdCRj}j e`R)T[/`vX. S*$6^{ErXcC6J-rl-&VsmO<>c]QK/j "uvQHQd.1 yfSEedRnM#*ϥ9SȰQ QJnIڶD]nަY;yczčt ssU32x'x;?#r$IF[7G#V:jHFBY.elGqA6ZlUҢ$b FSj)wÈ]TI[ |R.LڅFae av^I}|lw!qJ6:ϰ/q!.?\p^ُ>OFh׈ k`X}RftQk[ͱd]:?K" MqS;F)ģiN|5w`6F>xf[j? 9\W'l(KN4˪x;$Rٙ+pWrq;zwNQJP5vI %i_p? WGL1HBvC{_*_ ]bwblI-S+UN/0с&T|h^c6}e璉B8:AE{~ 5K>+8MsΔA%qh[PlR*y$=IYNQjNY4W9Rs:(9JO$}OBh)߷l((64mUA4J!LҠEt CvѯVL~G?}"ȶt(KwiFWy,Y/37%?SLZF"f(01: ? .sTMX#Lt {U4qpq3NpR}[m6aD"x,-ohuMp ũN! r`= ެ>r>`>$"7U{qg$8Jk&~}8b=3iHJ_# lFdNejTvو UcdMhTNN?9Sw2F̕jnTúB2 s咎agwvrD+G5vs9$f&G|,s&ՃA`R )]zw!&*:CYq ]J`4uAl  ,/k9YЧdMO<?W~_i۔Ǧ)u>жsf@VahV[\^P(*ؖ(h# 1v>&jn (7[r$,yl>D#X$sN$|  zo=duH~0u@s݈p}u@KH.+G5SˤFiHJXfr5d2w;O\[.$ Iҋ?(^2pQM깻$뀞uȍqȵ:b" yCl l kQ3Ɇ͌zs-d v\+Zi1.ҧNuˣ,Vv)9SzՔnQ>Q-{dk+dR Oy +ZbI"(s֨$‹ȐRUiAn:$OV?55Î[8X8bO鯁u4r]rtNX'1dv] $;"S #ѹ`5Fy 4*oP&YⳀZ44JGx)*v۔e 'yiрEצ. RiFܛ``k9(jt1r!.UADW hАԟK[tAߴhcI[;N@yR>(Sm@u 'pOr+>+K%>+ÿ?v+gfl)"?bv):5̵m C[riS(  BC=H]qMjϤM!^-q;?ٍ2 H IDATDALM<ڥj)UEtLYJ3 CR4tP6&eeRWMEMV6G޷1ѯȅD X\8mEh %tdm 4ĭrOb|3BZ2jQ LĴ2|kӨts3zь~8}[+}VceNoA`Ad[|哨6v7.E^HT)*c"a@ t+##d^4@v bIټ^c##1(+"a2,k4_b2hA%MJ \ /h,%*iXT#M$ub,G,:]a̞=e`M3zjAL iPH;y%PhJs儢TxeQ58Uhququq|5;ے$h8㠰UM]TIԙtd!3L5RnF]5+圎Ǝ]d7K:h toj2veq& aF6=C>yCj9 8ik<;0-6zB2Jlr\a-(<#N:QALյz\m^6={t```~߷|]OtPP2Y=ՀYgYvWz/8Of/[OawЁFlu/bI=x*K:k/ }%ɖ1>*<'exUo&/23 kfL?]yhQ./w7R%1tS7g.M&Y6}N%l1ƘXo0Kn n'!R!FE )MJZX 0vJ%1p>K`󀥕kk efOu9޿ fwR}r`45F]a65M?Y;4sbj"l"}쌟O)1ט yLum׌GԿK["bzٜ0b5a%b}OZCE\Gy"#pm)]{FWLy9`L85b%b/%X/[Bb}-1'-tw$':)Ns|7ǫ2<#Ê4tY. %րieiϰx$8f8ao7HQ a\q1l6-}Mة[,8\>pczQ6Oo̐.KL[ѕ dvyN({gs]3T{i!h{AQ¬fjh4% %=H1n1KJ 8+%X=h 2X=S.sn#^+o-gּA_]fdKuk/bEQiqV^qR^cT5rgFH%@M#KX,Qwۜ G[ړ5bE3^H`z BsS}MItkoUVA9R b OXf,0IG_)|@d)x_oƐĂ$qJyL ?$C<*[U0b mW 7Δx,vF=.^[/)5~L>~|1}}]r0R S0Bw3.gMS =y *:ފ,6FBtxTCL*r2zrNmuO$cB.3T9mǵ<+6yHqG88ܽ>Π>ydMǔ 05ujHt-VXs"gӄ\|f|4NQ 0xQש-ӣ}!yb r٪OhY&MMɦ97W#V!J'8,!hX7#&rDz cYcjNIj`a5zNӫt傖"WDS:&c1wQ9Xϱ/FE4R߭4Pm&r<`!{lE@Vt%cE_ru3熑6T̷ؓQ*^5xaJ 5mBZc%%:k;dkʣغTC,]8:[> bEj,6]ƯwR~V;ϴ|gKٳ==s"W8Ħ!tR(!P %, DY"tE'ko{4XC9br)љ=Ayw(;Y暗 V3̬LrԺ忴ժE= 2\-%Էh gl>k#[ ?,~m ;3vٖI^%Ahh sHjmȔMp9?HU64RW)ґR tj&֐٥U/1@XU-hsPw]Rhp!?`%^gY~3Cs[cgzZ:O 2 ;ش}V~뎮@3%4-FqG/_g+(`sz̛Q[t DK= +J<@8ygȔvgI3X7ܩ;3 <;ų߃7LBJriQ;A>a9ot[s}I<$}Ĺ"< ք0eNlZlT[PZFZ[̼>u{%Ml&Ml+ȷ w(PRi1ꞃJdE,;u*9eo]@C I٤CuBGuИg[4=Ni4Ghe0>w=O6Oxsb=bZ4FȻX3%h΢3/̫>El.$E\i-R ,lg^fxq3*Yd82c3n̬>e`2̄vJX_ЫgXUkv1kb"7lmR.T:]}Ź,ii+!osrWQ}R; ^qV C@^b2X2 -=KUl=H.cS(OO `qtAs(9p[ f>߭_q-inh 6DޖB{Km aE_n ZE,*g7d~zpPpݡZ%FZ-TJthȻ%ﺯxn,8uGA=cP0YApܾ'cYDUT u+[ 1E`1!ba %D yHYJqBcAw[d8/c/?nwؒ67kA:cK8'762aQ@Uιe y!QJJ\ĭ[Q؛UXYX Io|~zf=2 /r#/8 IЍZ^ĉK8-+ZU [oW.xW@k F`+Rso0w -9\p#ڠ,j޽ _ҷ& *j[uJ&+\M}g <4,_KjvHkTyq" |Pcwo93fN3G5P`S~~Zm+vѽE {;Cf#ED9vƯvuD̉[^Q`Ќ<̒>UCrQ) jͤlRcpȰ?f3ZvLdi11h^-c<1%9Al"U c:( LLOg;5G9-}--a8556lwv:5IJh,6+="< yTA'-?,JL%r,(1W O5h49p5v=lHlnfiKq Vh﮳BJϙw{LԈ3ZQ5 \'/iנo@%VRbf^fbPO U᮲rVC $ڝnyDkќMRh(]i?[2\MMsAozo+~]W~]_̾5cNo. -{5VURj&ss@mst~G3p6Dκ5T+~ABߜr]r< 7mVZdŧߟ;b \ JC.5d|ѼԑC}WKnܦArؾ N>2Snȶ5O5Dbñ՟ſ ʀ7U/8QtN33F3'ݔVwMԍ5]F[K\ѫ9fNP'hHV-*Jikk>[Ch1ij٘4+'%=cij*̪¬kՊwG#@4mK97<+nj5-sg&fEZpb_S.[SY:Qk5xN49 S iB(5^TH̾gFBZPH%Ph60ACE}텬ϸ5b;INjMʤ!6IHTS+>#Qu0.1E3z)D_~ .("Iۥ,#]J«rR-xkڽ-g dъ!5!xhmbl&`VCȝ%z$9aKdOU۠TO+S*4)+6hd.W_`L2,ٰyk&!V[j k"#1*u1l6IIs4٤IKtttv|JppqA:vc4)C-uji630yϩ J8w״߶-ⴍ+39&tkt+OClX?0IKUƫDS1Js #vs֖^丸!yBPld06:,6)9{=~2'4N4y|@:J>:\ҒKESE+̼S9gVoRtmCѱA=s0 r u㕌Gް̽6 \13@1kqB))u(nq{ ^{ ~C;(KZ N\"ͤN %(Rksg#4I+ji0SX , EYL8ο'6|\p=}% IDATeh9GkKؙ@ʖŲf10? |d2HK`/13i07\xS.u;9{½qJLۿb/dhIQ@W`To3Yxz'p{$Ol`w6NuȦ PX)o9YPyc9?4hhZYvY~*w\t(7aaNgHvL J}v' htK%Ծ4 M5iy!S.rȔC@R66uir]Q>3"&b(KȖ.^1Z=0=02ǨAk$}R>l@7m΂c ,k,7].3.3.32堺紾@293=3=3owmrPۈNTn9PU挋z[戇h[*I5$Dٚ.= ?G B.&ŀQR]u\jtV)8躤͚X6֨}9b|;bD ԔA4e7X@w.[k"cM$W}_pu@:'Ok* 6b;sO}67{Zcw=I̍sčsTlW&X@W}ުW6߬~ =??IL>oH!mmGZYizE.ld&SYF6:F涡ֈVoɱyIll̀XZ-%6F |R e)y˾;Lj>>KIl5<8#E̫;qO{d\:PF^}\#5R\#× |CktY*N .[]n/}o7|ߣc{trNBP32{St+:wk:WkEBs h5f;, B2WO3W=Fc^>~G^ |>i8@)% 2+~ z'{o+X"x7'/\s<-PRA֕A.((v鰐VVM4dž˔~ȉ) C~2M rASV=4HUvՔM!t+]R?V§JMIssș@ tĊ[z]J\QFFәR802~˙}).'\'lMYG$$:kc}οůjSt@5%]L5[k~{f~yLJυ~DIqm=ckڻDGeKzḿHo,OYe]xvv×s/`g-[q=9W?wk _=O<?Y㧄 j(l*4l,Qc ]phL\LkA. )Uj.|Vrұ<~ORfUN@ AAgt bEy706 cSbljMM^AR|cKhNwA`FlBtaEycY!s=n#0%o+dx =h ,DUF*.kd4tM#=uF(K@)PP,ҐR1#]Z%-MVl91(l6@Ф^clkҙIp$DA(1=LS放=b>uaRm0Q{hBb%֘2ҩ*5&sނ&6xdتĤ@aeA3<;W+<Sq0q ߁*jOQT /anh~hI(Ԯ/iKePz6Y$e2B+R9q \7!)F%kmRS;nvЩId\4[b0r kΏ>NĪZ>J!'"lmmzl|i2sY& V3vH;ԘԦEylG-.UeQ6Uej0d i:6(Wc2wC uw XX=17 ҧ/>; 46IS]ã4ZݤU MVjU[]I({fOm$z5%xEol)d7Z?[{*>%M oW$Eh(Ԫ̆k1`/|Ra #<#?Q~$þ}>'t1n9wL[Lc;HEV&oYD'1x@:Sw=AL,"s x26 :G[Ĥav(yG”2,#b"o-PرS2W#;3ћawD4D>o(\h<™n{NQt{|q>y*$nԑ"aha+ނ"-BoEϫ U)Im+>8i*h*AS˔FT*Qmq`(З }oMGk;|eI4h$%YCXV݉ ;zs1y$r8^^D*iN/ 9ihR:sV Is#l$AquaGXflp?i" yc)-'؈ q" Ktt՞*ɩꛚ+hx±6}`ڟqf&n!X ֒$䗸閣do& ,"xBc vFsV@>+/tf1ߩ?'R/12WxwDYQw5pas1Gc*+1 K ohe7*Ń̽1{kbQ'2{ġGyG~zd쎿O67?"4⍷ꖩaZ3fWLILT.)Ր7~Fu$k4C]58eo{l"[X;Ê> it0~c:l}(ZIYԎLvfu /Vx^NQsGOpףy)y>qCǮ]`v hď(=e,x~GL9cLxp\Q72u+S72UgdE)J(kfr&]q&jѤCJԴއ/xv̠`T iz kl#"dƗN I$UHhfS_0fs^Ľ}~@ZAE"}Y!U!մ.SIKC$#i@ܑ9dpp.V?t~ 4/jp/;=Ιs54P?{kU{ѩ|]1Tk$⾝r<k>jKƊg=73xFQ;7k_vJ6q^Ui0F,7v n1|B@H$r8F=%ZAd~J)5.Qj1On#$RD̙HsNtGuN&W(-(g *]5 P},QPeYQ(fC*$VKwd[z"fC6?26|Ak p1 )^=DXzDOTz x|uKZkHUW8n},42;uivz+)KgLx~̉{ K` y3$+U-oyҿEᰧQ>JlĎE5{U~C8bwCIoPȰQ44ZEAT7D͕yFyskoe]נ6%WbB#"m֌ uP2 VQ5 ]:%m5`Hp!{gѾSH^4؎:X~4#W5_EyGyO_|~F:b-> ʂcS{$3iLrAz#J:v2eZ $Q%2H"eJ%"TL=m0:DHr($!7(7;R'ۆUDFBh*(ۇuG|R8*AmvFejԆJ)jF!4Րy:%ZQ\HGK&b% $3>3Vj?2))FUZ_["Bj*6ਹǩ"@mK6{>NLΉsThjP13,7%B Z Q`HH5l[ZA˴D4uxI+09uAȎ4D'4%($6񥉶aUav}}YtV]wmZM$)Ox+39qf+X*4 `ԜC%(>MhV ̆V>)3d,LdV3gח<{A.v:eQEPDE!it-%i5J'C.\h ohpZaޔ]nSnO0@oCDa':于lWΆ.!6  iZ %D%7Au0B(y75J#uZT"iLƄ8G[,GwKF[2H7涏dtCcjejGrvx޿z Q}C-S JFJjO(ԕL* R U@PkF閺BloPdʽzltet=/ˏTBԟ3[Mc>ȆGښԕ6j!g"F4 e&ҦZl-,u1L.ي^ӟ;LTKÞH i#QeaR+2 Za)^Cm*rU'W5"!i,ܡ euA>nC9&*۲CO0,qZ1Gy䑟.?Yw?':~4FyNUDrYcHU?$vi{5gY2ltdɘFF{ZǼ8.Ҁj,Q%L3Y4B)d.^d M.InLgŚlN6(RM2 ]W6cnd'rG/kE"U-I$MMl[ʒadp$:zȲtl.a(eۏ=y_sB~qiBXJD;} CiD`<0sZb{쟹G.sg1ħ2L)bYjѪ3Ry|;6 ఼>IPvePvIM{/ M#Ny茨Bj;1z.G$IjR]a,Vr)MFC:P_HH`Z)٘y!yHy|/Ⱥ._ƗL5{oH oG/pEwrBkB68!?FhmS%D(Sv<=Hwl&=6k&>R-(jQۊ(sGyG 5~ wVjfZc>q͌^W!Z Yc9B, 7 h>Όg'w|}򆯟eu]JqJIP OQ 7F}9ʠe\MX%4lcl%{A ڽ?^a}W%}$7dSv{>\I ĴIOL$s g N+CP=/|aM؇ҹI[dF*d5{<1 %Jps8c>?b^-.ybxn~;ގ^{KNJ%)Ĺ^xGTvt/os)1g e))99SsPR#)5ByZ^E"请ŭ/x& &9ݜ/vКјKطxy+=bbYޚ_A iCe5O4Տt* I1S,9R&QLhWs,݁h'Ռt#m[@-B#ҚtP?5zOYYnPo8J/Vȝqnq?lpllh1=ci{Xb򇫭Lf'\P9$n?]H C4ϐIo~E]'5YCvnV65EC]KhCҹ@ υkZ1= 5z^o-w'n3]fbR&9RMC/ 9Zs=*<*=KV~^0U~cT%%-}iCO41):bi,cz5AoN0cS 4Թ]NVOijgyaK곖+4D\9O9C #S b >CYVH7- Za8V#c\T5- IDAT`]{Cҵ7؝=eGaPF'r1F nGܼ` SO7L2R2|"se|U#_6hIW[|}u{nuA}{&FQϺ̑8#<#?Y~o;SϗK8ThԈ%4 I!A3a:Cozų/f|Y#䛆m߯WJkƃkP?ѷGD[4LdK&Wz*Ws4t <<{O[< NwǓ _?=A͠ϊ>k)|nѣWV=&T38b~1aw݃KK 0蒿ޟadBzNZUF-ǘ"E-$̋~F8ㄳ @ITR)s7[WAkjUHZ/79 6nq7:77WHe_+]W\g|ŅwNq_2G~ɯe8n!~K<+G} 6 =P2.8Tz{GK13:NQZ0Z ēCҊ2KwtIJE3I9~EVt_I sb^mCsО'} ]qp;*4gP7{7!ѡ{@} 5؉1W&׻M)FAI3㤹㤺kmL@(\ۊ! @=P<_!M!+B&S~gp? % : #(Et~}sK$k^Uzz‡g>9:52 gODלr)[θ)NX\YvXߤݔ;^[(}밿ys*hj F-0[ hq0Z_\w]+~C_Zq̲COYVa(^>̱cT5 ,;ko3k}u.9V<^%a~ M"tt/!uL~[Yqmpn o@|ע3[[)R̤ISnx ( Uu&Q̀7y=#<dߴ{?[YPk2R& mf?kum4vk ,Tq17$O  (N|;EtfR\R/Ї9V!ZyXhE ޒNa!, swBMF' NH(d4Y6c>F\2RػsonJ4Ndeug X._ԙL`x\OCz>(%r6(Y(MzuZX֣?h v]½Ođ|3A^Ԥ3 ꌡbo{lF}E>:NĕMĞ[K3Ds*im6SϨ5Up fil. -JU ]KsO)AJ}']0r %CwZ08u|h鶁;61uN>g3,3УywxyݍHX3@|đE;x-?Omu<#2fZh;]gK[3'g`o׼W_R 9d Z;B_"لK$9DG㷹l}4FOmp}BI2|*E Sn1bFϞOŘ;iL9NXBԤκAJz _4aEmJW;:b/ﱊX6yhS )QB?F[td`A,|7d6!$z{G,$D9D Tm#+5rD#0zXrUކp1ϏF6/$'6o05G3(H1D.Wpq$q$Q JRY>u-#"' 8wڰwθ0s!#hqDıJR.jX1^qSg*d+7!L2eP ja'!H-cHftEЩ 2ב-R.=.D1$lRX&2ƉZ# }tcKy,)lc?1XRB- rVӮ yx/lSϽrDĵː% Okg%۸pV>}b٨pc?ş2bϥx% e$-1(0Ғ=qGW9wtke9K~W7/)*U(&KfqqvJ]1挤ciN0 K& "8Xv^oxM4JԡBh6 8I٫y]a,j3q%< -諬 % 7ĚO3.s..g-se%U{ VEU+'1C,-%UJܘAܚY%0Re<}[ǯ|- [5 Y B\IVBg9,j jK2ejK7X3z:;y- kFV`b]U6 l@񯁫J9@>#w k%L|7[^;>및$Xy[dѠ]ilA1Y]R>!.uZUS.s.s>̴#D!Ӕ Usr[GrZ!ȄV8>08 k:[ ["95a5 .`n GZYuôgR.kR"T}B#T=U~(Z2G';䞎kDs(Ǎb8@4 1w7"Sv%LFdY:kP-ż!}ېe5+kJ]6ToiJR(t5)k6=̙T %HPԵF^":䕁h!/6LS*r`T G <\" uL,U*EUF$$AU LܢuXthbVmhyh˻<0R?fdHfd6,04|~ZqJʱ{)z`uppÔ{;;|}UE8rFĸJy#:KDžsƅsʅ{BA ʖJՙ\vO+%S$#xIjJ Sa)}r]FX]rQ U c)4źI;5LbBXlab-ܮ]4>,[sKK{Έ7G9#o؛1ԦL[,!> 42φDy,I9f͎SI1DM船DBik])0HhRM]&HDR)2Ua>*&#J= jCavyC_PUPR͢1+:<86=4I;>Ҟl tuxf#b fMT\ψ-vA9S:v@4(A4H <'x'~[5~%:;Z`B{nTm.Wg;4 62:(jok1cco#m"uM*(NꔲZp%r!N)<БiPꃶIs[2خKTIJZ-yXcGڲDYt5NsP?PT:F9F$g/om٪58VnDe,o/.202i YW9@A5! @n S@UJIpTbBĒR2bM>yb' I{*Hg{N,gM9X P)|R4Jd; eNdC:*iSڬ>6ww_ݷSKSд EpJ htۏ)Q+KK~l}˳ξ=οdc._X]D!C r&V( Ш KIЇ(p/2N^_bmS ҮA )m}LIfkr'r²&i3`[/xzΛsW\W[^kN9qӜ3VY,2}T&58d qXO)dgvMdYmCh!MjdI,rt$h&(&ҭDDJf9lR.6*UR vq99Bs دrAi6Wt5vPD:`HNv ?p8eӗzM#A%$Z\|.9x?{)e,uO<}}¢@Uj 'prL2Waf=5jJ$jIjjS% S;a9[da ՠ5Fc9!75UeuP @Xe]x}pr3dnt%aԞUH6dH6տ(c.sMkkB7g{7W/xs#o n.ы )P h!:'˦D[N;= (wUػMa v+L+EWrt@W*@mIHjXrL*YJR`+ IDAT`?#Lϰ6YiV',ثqJQ$CN;߱:$Ԋ血P$jY!S "ai]JJQicH5\5W)N^Q* PhB@5FE5ڲBhnjvNЂTQ#J }T% "(f 7È2$[G<0+))1-$ŪT4A40+,%B)uQ&Irxb, WJ4+@z Ȟ IkymvOZYꖲҩQ M#V,b& TSG5z*XiL0ʌI>p]s\s^s# r$dP\nEZkg.Ew} ]fܝ`HD QKNKNiwCnN?YӋ7w;z5C 25ҦAُqM}sm̢{[>a !Ccĺĺ}Gш&Aek p%2U&;auic [,> Q Eț6Ǣ̚b`)2rXCBEHaTRAN$CS8fxg;qɑ$h$i{@Rk&S4c,3AjCFq+TdDuH%He [[ȑ+#FE+- jWBC4O<[k |H}fI1|؍CL;ǵs\+ŵDx'L@:!d`<#Mc$/ł L=%9mt-|vMSdXM^l.kGwϥ}cMqmqMqL¶LϸNJb3֋d`~a_̴_/N4 Hy'k>OI*UustVbu0/îjYՖQ3K8&Jnv9KnΆ)%)&Erd퍟UaQ O){M#6qz` cu 5/MTs$@Y!d C0uΨK0+̷!փAqˑI9HWՠ(3>~B[FQPw}3E|õ?_$Ѳ76'&]:*BGid23F=2㗒3Z -cCkp&,1 gL?p\ppœv^:((=E:2 e#=5iov[e[Jt#O"\Glx?~!_ۈJIBh1qO3ܛܙܙ.NP ;;[;@Kӂ,mgj(5w6)QESVzSzzCk4Z'!dJmF9d*pT,5D5[ӣ˂)7gܯKz%ќbsa՛>fSttN5;:-ь!yސ6M)n˽%bEdAN&*0*P~eQĵ.Z(Xxhu:z賾X]V6\0Q ŐH1(Ѣ [ r,w-~}\r~+QTm,G+LjE:lEkQ=+q&`,0Hi0@x\dII&LQgz ^Y%jUq]R{7yQAMjJrﲾ39\ydz̳!Z Ej+[,+EkJcq.!-g-U/-0K1t2vv=vYi 2wV|}%'JTl6Ҹ2/h*B}^a}Tak*C{`~Ɓt:vȯӚD(TF%Mvрt}99 -d eyy!7ڤ6I5Ad3N! &,[( BJ#Sr١!z4y̶\ :Xd3L*UCl73.s^or$?1#o'ZYܦU) rm򖉞5g6&,K2s3ϐKATNt5ǝk>`>P0e )=m/TJTn9cn9#dPsL7ǨsҝnǖSlp퐖RҤ`6 r]Q5 u#4$Rl 6in@lQ+(j+ѴU`X8 RcvC[l1!̈́[M38 Tc~&L$K00[ұ-MLՉKa{``p0cT-KՒA$-"c!F5ڦ46I$e0JTDBk($ʡtdyl@Td؝L14ғe)*!-,T'x'~[G=k~4 k~sn7'gt5oE[hkqL6dmhZ( )HxNt%*vI=Ҵd힑&1lDY/r6&>@ֵϛuLX}Nȧi&&!tFj ?(,<:11v?f|&˟ LJ93.Kv~6[Mo5 2˫!ŕNŒ1K1$BQ$7[s>p\],c2Vj+aq=O 6!eMaQYVe4ʐbWFt5'%G=~rv{|'Owt%bAg]$xAlǷ_6G Zm*: R*&t@=U4]b9ѢgccI\q{ru6>\c2vNZ4B9N:2< WG+me@3%vрxn2@m!obYtIY!_xC1EFK cK`Z Ⲭ,c^ѩk7P \%% bab!!$8$($tY;z-4ǚnNotmPMʠ5j_AtmRRY*$cb+BM*s&?t*aPC)t9aj.j.f,"-²ņ[V2&bQ=_9CJ4DD(tYJC: G7 ćb=\kz[z;42ԨBK̨ĈK|?>?M Ӡ4 S4GxX422N)fEV /L8J4Ԭf(-8g(%u-]f>T  *LE:( Ubj987I@SKvnvڑ&iy`oM6QE>$8֎s8й㠸ÜelV6+ͪGXuq}K=v+w ,I"cGk? 9O 7~|'x.~g_kG?'go{JbxzH7ddִ a)a +(~;"Rߵ?qc}N(\qˋs60|~K!M7.oV;qӉFQeZ rwc2aT6 } PF, %UI:,C~9gK&>m~HTW$D}l`az΋+7C47h%[Zh%kL6YmeS$?*R&՜{^U9-h~Q vhYVdhu.2z`J95(L.!HÚϢg-H%HB*T؅m |ygm^Ru <*`+[t*1,&2WA4{ =-o هհox;|EZ{>>>:!>%,cV!-7`2+š,c.t5yEhΊJQidZVh6X8ďU.kblBO<3?S3 ~{|쌛_{_'&{BN[حC`>Fz܏1f;C-*$&Vx_k)Gj/ASTeߪi=+~ R[@!vnneΛw|29Δ}QABeQՊTX,cUQI d+7vo~sSgܼ9zW{P7w,F,,8>H- q„vd;$:_):E6Zx1tbƠ1`FZ,!h"la=4)K]|Vi)` J&QK@kdNVdhE+MAxxLǬI RRf&aڢ8w)'w|'O@.!z N%4kto7f":ˀMtDcw/\:#O $cΑڠi@|+^>8r/gTؿc311Z+~eN9SoT?Ftֈ Vj|6'ǠF!C1!˚*i߅Sb1e̮iQ,9wx:HbdiTmFSP%*M%mL&8$Xd4bT:31+ S;N;H*؏RX62k!j\dH#djM!W b?-*6tX}\[^--޽}HJ.w1a钮-QTF %і5MvPLv!W/i2b(@p[pqw?ȘqfẄ́𸎟1 U ߱϶m,I9 H KHf (p#M]Z%ӖzΆZ'<K]P )[p[dM?w.xȷK~aΘxIߢ,HqP 0+wx'x⷏ߘ?cVp>Y`J!v4i= l.lfHk ¸Kl ҬKm||1ɿu0>+Oq*GCóD-c&:Ws޽y3iĜ!-hdZi-Ô5cGIJcvY=VtE#ʥ{2j!C:KFx|͹vѶ GޔW t#'vgp>gg-m-׶'qvXl&x.90`3&u}Ϣ@B_xYaoCє vP:!K:ŷ7t-0ȄA. 1Ud#޴>beuy#}ͺGVmi^MHuw+ƆY{ eX="9 IDATI]zoHu VG|umml6A&;t-ߋLYC.Q(rU&|Z|_n%NJpT2eW'ZD\IcW8|#vW2|MkJ!^!w#M]d- nhtݪ҆$u&|"VEaZyܵθmr0fnJ Sd2"V(x*j'x'~;6}\]] ...8::~F7AYE&ѢQOGUR qMcZ#̑6/QD(I;r3DWbzL7.3&ꌶZ{-@y@~P@~`qNL?`8R& u ^_^eMxԢ:z9ZSR-=ҥ|=F*V~fMq[e;Yx=d>-̢rU<=pp˫^ f8=v$iB>j77!gΡsÉ{͉rJjMCͫ)&^J~+^-6}ug{&Yohѳ &uxƗϸPrdX4A 9Sƃ);D$V_QJ{rvy.0p&!fxPܫGܦ'K\zz<S&~doJZ g P kĪ006($t T\ 1e1}vWvàѭ爪aU]1$]FhJV5&q1-9 Րw J >+:l$8Pn8!-[pЎCn.;Cwl:v}YoE9OYѱ}L)UaoSn ^W>7t?#~m LZÔ=_{ٶ\6#"vnźUnO [|M ]bys;wfy7ѥuQf'Hc[&8z[!_x;.c'&`G7A7TJ%:Y1&?ўR{35#uBP,!['+>_YOW ]/OJEU.j!tXSL:<)hK=J;j^Vkh퓾t_ >3dNga#_ݡ*rmf]/~}|= R*fhʯWA>|O ? ?|zFk{N.w9ǝ7Ă1QXO__p?oPb&~찿pL1 en48=bN'bnrw9׎9͑g-voO`xMɰG,<"(<GWt\Z[j;HJ9^#iAxѺ-jI䁁h[}9/nKb[thT:l!dKȎX{BbMx=Ia7#)G'Ǽ=eeҽӟ|=0>v\-pápm8`55fǎﰗMhZr u`i `-?:.Y5?)3GߑIwS)efhPTHlQo4l7Aw=`Vլo}ַeb|!̜/|O_˫5on+cQuprcѓ6|O_H} ^8xӻ]OW|ZV atX!ogIA* -#d*&))VmrP5/7:5y'f{:ٞNlcʭl3A[դCB-;}s4mAK҄NURe]͐4>QpBw50/甹//x7߅˿ooX-W__t __QPc{Yk4]P ή*]ϰv.)w :@AujAz^MSIA#FP&IW Ucֺl.KT@- n}VKZ')is+ye;S&GjGDKl3q3DBH`SnD]zL[,!jDj'; Ī sΚ>J0N!)kKa%ِ%V^y̭sMNQI,x^Ԇv+=nfXNDmtWsԡ 6 rMAQÕ~q 7 [lvO(wrG^3掑:1^B@-v79xa-Q53 mTST}ӨlA H0J\P@4 B#cY>ѥ.T;CvZHCHMAWkL@7*TA Z(%jjʡ*hX[k4è=<٠i%ȡj0=nhs0jkn(4 A',w]ΰo1+(} 9T呎y1y!pVԊFҺIБ5#:暑qωq-2Cgd1:lғkzrM $E*@j\[%oaX\WԱΚ;;/DlPL͔䚓-k4 O7:ubpК;4Th%{Z0ꔞѧcVrȲ.dXKOS&&ܖ,1hB]֓ qd/YUK(sƌ,-pAP°=-Qe`RrZ=#VlnMx%`}PBoizhjK eR%:e%:Ue ,x8Xwxcr~]3醠;2xx_ſ[ק*?K&tV'cHcﴥ=uXwtrC.-wмsΝsLTԣ1 ΐ 5UJHVV'vpvHvmSeJ ;xmIr OD555mpqq[Dw2T(4 xCܳs+hz5A_TAHGטiAm7mA) Qn~Bl{^nTsxrEo7dV>q0+y?#nnc :~" Y7t-3؛_0 9Ղ3qK{zQ5g; y5[\ܖdX~YqͣsҀhىHɝ&TYKŮs+(Ɵ0=c9(9JHbP[4T #t CCP;lNzǼQ'ՊP+e[1+sLb;Vn8VF 8Fi(~iKU]JRs:# h =ȫ0AtZ[( ZP[Z!i5plkc2}eYaJFJ% Z ՑF}jRT&e_<^c)kGb9$M:NuR rbi =}Hu-]tNTetټ23Top)j d:9XEJL+3c\kO}E*UnҨV?X|J+,g.wg&<تס4!P! ku1ނN&_FM#b@dKKz8 ݣ5ɚ j-g[NgwTW<|=f}]%z݉sfqʫu)&&cMlY4A+4 $, ޖFEXlp˙yE۰tF̬#oQ[-Ow ڹ«'̞J{ϙ#'#3mg5?O_2QiCꀹ2`'BB}OԡZzկ; /089 <-fbM[wLAjUS4Tt٤SJC~YfxyvjϮ_r%G]ywBxt53BeN7oiKzZPUԟD MGGAϒx!T@kQ܃SXS$- v{l1V4:kPgp.{t:P pR<1IO\P#7XZʱV9F=PNNeԡB[K,5,9U?P) VH*>Muna;ﱝ{<_'/ #N~i M]Ait$C90FSuvkg[V]m97)23\d_.ZcyghA=^+]UBr& 'w蛒ͪYdKr#a="hN4DE1$k{bا̢1ٛ1wGDq3.0ϞWA:b,MY{#y~|MnB' jMC Nsko/8S9կ!X;k" ,Lgb=A2j0=b9>IiuHo{IEYNGۯhR5=u(Gh*,=x~uf|rբmoT,M!CĠ&/,@6;i*i;XKPZu)KҠR ]JP:efDVE$ՠ6 (+E}UvAO0]MȷvsN=ҽNva`6]-T ~m$/ ad$=E5j[0LR&n=vo n:_RaDHG@Q"Q5?[5dNKhv7MQ6|kiԾB Z] U$U$Հl8;JD8-h5D4 *3jOmr m%h- , L;ri aYXy{c&Ii[$o{Hǀr<<+n7/_T:eGhT1BYEU%̰ -81ܠ AUQguDFvIv~. :d/0G(jq!SZxǙ}ͬ;z1ڜp-&DT4EWhPD- Ŏcx$^s,oPXFChw#∅>$\MyҾk(m@ݒÒ#n3 ̶ fß5x [!o8կ9 o8_Z.711jސi0S긛v{2l8v}? $)=#YĺOj4$g5ѾX,.[WKuzXS1^%yvcOI2:RZԕ¼11[ xѠ* b0j抡`"t=s9bj1θ'(uW38[.X5ɲ&BHũQMEVhq@@A%A:;r$ٵ+HWZDN --*-l]֝b2ød}MZ$GRyG5Ti:4 vϦh{-Ra1t#h*\l*PEnRen6yP&Ha?Vjĵb=Q".H[y6&9]⌾ O0'{3fqi:-N`9(LބBc$,y"_r5^KWwnѓ(ѕnvcsU;sOEkIxrpݜp]pzҫ\{P]J -g;Kzlpr}hdd=7<_xXy=VÒ|*v Ɩ]]dķJ?A*vZ d&H`IvH,U1`NMۊ d.8xZgxnkl#³+{ųw/y~S8!Mj%?uCk(njVZTM(KmW Ǘ"5 ËR6ŕ)JPnx+%#P-  {SrOP񪘝~_[%dY'ɀM҇EhNozXJ jgvc%2P7Ny' hHEF MvBM֨6켐w#Vj|]0)#qG([Z3ޭ!:W4SN>%nl>foxH3VU,UՔ8=FUNvS^bV%1nqb3tc۫=6h*`51VVzK -%PĵR9~&C$-٢A5-5.8sӮi\SVXneXjAƾE]UK:sn-w1`m к;!,ZVU(yHDZi1nR. f1n>?; l%-"(uEhu S0 S8nK-zu./9MFO.۷3eB n-C+7\XM($:5c*2Y=])+ K)8 <7+ aҟ4+&R6uYhB A*2F$-j$w5FT+`uǼPR+WjݠVp ӮP|Ie" R8MNs/9((B=?ajr^yQe1QEڕXÜ0Yk6YkEb'6EP&b @]ID@5Bo))6k#\*WAWsD+)I:Dҧ* d2zTY5\,6 6 5a`B5HlX?tm p^fHBKӠl(iRv nr L-1VĚX,vGLߝp >E$a|^J4F2V }$aDwecvzw^wTaLP a<5 JEVZp$t$ P3\uG7[pTްxe!AKtEw*47C1Zp yx?XG"5(N9q„AEbۥKQؽr?S^_)I#7zEMwz_# IۗGj0 {/DS$}˓MM vo oMV$_7dkIna ~$cE8M8@}Tb1yqT%m.P%ʇ2ZÐWcP++yɮ l P]}V+Ш ԦJ[;ioХ<2i48+82f|@Q$-mKW՗dYf]pa{vEȷ7_}ߥQCt_2 %SCz֊ NkA?㪺#? WzAn ^_2X=Iuw"gVU1XF}g̽5fʄyuDx>6-l m!"s6y)7cQH9,V/.xx_W(f8AB`ٓIݦˇ-z4Or,xYh+d_<48oB81e̋٘c1ŘZ6RR˖V¶y_ף1َ:l2-+q+H1ݨhb*P3ķ)u )M9ƶk!@*ԕF@kȤE FML'\L'8`#sHƚG[YWnEvne-r u9ei+&j+&z$<ѹ:Ζg-yj&#^2)ם>θZ]VOx,irdtY{=aۄgk߭qʒ~(:& X_xMuDN0Kkb΍}up bYzivYzTԶB+TBArJ4]4ņ͖fܨ/9`ĞE 5 OW(D,\:SՌ+N=_޳PLʈ=&!&U!KHON>;рc{c/p͂}cQyQwZ7||t|gׯ& n\V Wg vG]-xKo;23`.(4JӨ ũΓ+$רfXty{~-b.9]bLE*MR*U_îID%Z$y~ /o5jFime3M!:ì,떁92s*٧Sa SBPQ=G)Ԩw:ܠY[Y vYgkΎtPůQokJ;hOU(Oo_HGEHWh`Pi]yNܭqռzcn Nv̏XoWBj{va4[<6s(69s͎#OW|"8ulκl.۳.]ؿ b*_eti#H|}t5?_V,/7G̣1jo6$ZE\klr1dZhDʈ2"҇)ӳWxveYz)wrF۠P$8m8g1=~snsYxtt4ngg? N)Cu7ovNOy׿?1:$-=ҙGq4e-8w)|y4xmǙ+GR8/"-_&RKij.+J顷uP078mK-,C6g={oOGo9Uo}}xD"<ï}/y1FP=W\V&*cnm4 mK[9ğo sk2׆DOuٞv|#`A@ӓ<ӓzMɅE9H'EƯ~4LMz̓D* -W;~o¯/JӨ5ZݧljF7=k]g|5ALoLtg .@SF8t7]575qO<O<FmE&^īUݼKI֠^ K N<4.|HQnF͈9%Rsh>` 0`_\%Q}ʽ`jEqV`u3,5#!vK -Cd(Vdؼ/O ☜1 #TsB>h=v+[F}VkTB5ZԎDSeYVx/ t!AwKq&rιsE2{K%{IDdihn$[`-OQ ́B)T8-РUZ!om6`!GLCA9kOt5V!KY .OBȾTIe6Kο8&Q5 #q`#eLp) tL1eE:iJ^PYk%M|%,Sbާ!y$(i0ޟ~SQcum&7,IݟQr's-z]K`>.iQȻ#c jP&a$ _-[ĸ3Kf̙lsZ0 + \sa$nj$vtG?9ĺK:$M;$r;T{>mklq)C7տ"QjU [Upliy6cᒯ-Ȗ9埡gUYԧZ=NqX sU`JGM 1bXTC6U(|OԵΨ^CʡAԪNR'%toր 4 NIwy/=I4E(-r:S0;0fΘcTD}~Vq٪_ZGpK m??c\*{{wW BnD[8Ǭ8k&/7?!!3s^P԰8S)G#lEhKDX=o $Iqkb4"t|[z'覟ߴ~hYBZ,GCސوhAkDQd56]ºǶ衄-G)ws]m}]s(0,0DuQԖ@h{47~)0A;s%f9D]J{^?y˥Dѫ7 vt*hpBKK.rZ5zP B t {aeDxz=K.|^1{NiDE&5fQ).6G{WP>~˫OO_.xAznk6`fQ%&T0`=ƛ1+J-T꾺9v/Vp~3\jO% ֛>"g&zS38X0,q IT8$e>82_H6]X2`7~p\]S̶`^_?'x'~犟Q)i56ď>v ZdB_Gj٬E]k{&b+<%ԩ2/q/R E1`Tfnfdϛ_Uwsމ*,Ą%cr*k~u75Kt౛.ekPI)AX36wYSSfvǬ11?E/Ko}$~IXsZG`Z9=kP$BH"QӽskD\l8ZӸ_lEB8TA+BQa =X1h<>}\cO3@JʚTь _55gaJml*tf G¯_k%~ͿvwɛWAm+8M8rG8"8¤UE-F!m>vx9Wg\pEBϖx#cڽ٘sFJ3g5cc偑0FRS{60g]uBZDRp\2o>KJ^& IDAT ~ld".䲸n@UjW!fKd1F;%\!3-rӤlL֠,u!>fH6Zur&4XZEX񜄎q}b.D ih3xy _1 5R :W/PY y`!LRekvHL] 55mF$-v?c, [͘f 5CgŰdX.ɤU.|d*ahkt&m"G%PTICrhO|O':64tDw*tdMXES(R"-h{@@[ZChJj~gFCWn dL+t nR(6 +܇0}fUbf%F^D> '7MDWb)1F%VsNSEc.zFU&2"жHS̺B%lJ@2T~&1 ,0C+(NQt 12Z棸 F#*1Hju3]vԣNB@E߉H#u4\ְ(я*4* Y Qx'x?(AۅhQh ?Y|ϱB\@|'>jCuHt >'04J괊2TJECSKeˡ2e9sa;j$W>~Eu1΋S疁=IШA[4}X|SvU Dw @DV7TTJE @5"(ۚ0]¨K+Sa{aY: qT>q鑔>MͲO8k6ф*ܣ =^x0"d{gJP9:ۦ`9_|TM7~p&`%E46p=:x"Z+Ϯh{nŒ)#UvnAٳ &!PR/uyW\,>v=.18 )6z9) -. GC,.\ϸιܯNNxpK;RQ+Ҡu\t7[ ?hjZKt!CoHY%Xp`?o}Ŭ[c15~b zZh-J`%"cY\h"F}l.Iqxg'zDLCEHY&/'&K.O\F>E>AlyfUfc&8J BQHoMB|=?e\OVD|0R@q#lI;ůx|I 8Sb}jM[cųnUhynQeK]"zDteH[2hp FkѢl$ fZX3;({~!ל\s:T%kLUd)4վZFo%`!`.13&Z쫵CgBY+4Z28[2 BIWtfJsxqYkj3j`8%\$8H)RAidsFS XTp6x'7?D_ B6Y#dK #~ve'>t\?grsm<|F?#C ÿ5~K JiEo@BC%[Ŕ3nl?r>nG^?rzr'@!i{4a<#Խs].jd󇪤ՈB!u]jC8.o12dK[=,> 1` 13&!b:[riP=4*Thcy7Eȑ{W9!=}ĴJiCtgno{; >"}M@XFG  stgxcMMqpx%nwwxAnB>)57nn$⛿Ƒ ~%1ņ-镂I ΠUp=O<KZhQ򐖀\3a|둻푷Xj|/{?ݷl mD#FAPk֐5baH4"H 8YCژ6&;4NUMV7f?p9tC+X#?AD6.mr@kvIGJNAA^Z.4DLkFXU#zZH_)agl{c #VvY}U=U#mR!KAcT:a3VjN)+uH*$ ,EЪ R D J.Q:>f"Qr/b%Kt.Em"kS,$1eOcG=gVB;zP+#V?P \%xUq?wCpL{ԙKQ@Y1RѨ j);|6() 괠FרM&jt0JL0 Tтx+mQS Z]ǥh Ϗ0dI1#w_**Ђ*,a)9 #LېDW%Udt5{lM_[&h9%M "\$6' aP ;}J> ,̺Gʠ&\|czm!}}1[ۏ8,?hnEGGB}=qBY>sCv&4zGx?Ѵ*V;1˒є` #H2҉hY*CF/b[GI%PHkMgQnCU8M#*rj݌Vh {1Wz h*Q VzNjCe >!Smǰ Plh lM3h#ө]ը=)b_ݭKd"MUxfDfideN!yp&"*1M1nC/;;%GXͥH]v.49<OT$`{3rN 7Z$G,,P&h5mGRkL w)w) uҪKg'͘tik8~ƨ,Dc;tBkj\+e*iS=3xsrrss u'jr^|@s+,-j24*OghM=$!cL؈Z1IW+]fxʵK 'Χ;T4NѨSv5 .Il\6>UJhԮάw@G Ӓ<f]شcLĔCl1GL!}%p嬨wGtFdIJMKaiѵ3cGawv "wC1\\-oƩr>1) /'x'kd4&N H^?(cxLz{%^ !5/ogŸ\ ;D.$[Idqel:Ki*^uE92(GH,r 1' mv;xS&B7~+1Ư NQ|,*=hHPhIRw77AtD<"}p /OGKFgS&ejR$P>bfU#)>7Y{+qR(S`>+0˜fS,XBPXQhdQ~w7|/i}7WKh|p=%-,tp= "GڂQp=sŨZM䍍VY*hnęa-;<4Y%,+pNrNr[ٶ TR'}=j#]%[IYa?yK'l?éR*#lfƐO9oWU_7>*2UӦt5=M[1Qg5Xz=UǺcetV8jB{ o12W ykA}G\i׈Nt9xr^~,ųTFm5!g)[z>[D2W&ڧ~ɵx/y:E,15jUtu(_FKzL!K5ʌW|e@7_m*r3ڷkP[ϵuɋ)ޔhp]0_-yqB@=|AŐ]CQAğq&n#Yಟlk'x'Ogk6íf"TG+ZnA`NYo5l]둴Ʀ>M7g7e: M6Tv^bu3FUϱNMR!k 5cИe hv$ϜU[ZNuPuc(%=m)7\(WRG(-U2aaY)zD uMq/h%˲Z^:nʪF A f4À4fgʗt諕kuU690iٲ3\ss{׿J77)~悮 w$O!t &hwH4rB3F7[tT6kzxXiITI3.+zo!9R&>DuBTD5nȽs[ݞsys}Lbܘg(CNB?[1.L&*XQvLФ*k2A[A$v:,( G8|$RG$$zTN* 9\U{,itһ9d]Ѷe m&0c469C}hM;AuB5QFs\c 6^9Va'l!tC^h$$G)ǃ{N[ Rp<\F?=yGyl974q8MK5ZW/y؞0u&Nұ˧^˲b5lvżtO].G a[HYӍ65n?gsc>;|$C IDAT/>l$ 6&a`( ""; ϰc5f'd'lR*VKEiGש}:TmYJ 1}/[(cM= )~h>΄wsvLNq'qTNm|\ʵ|"0 c}Hƣظ[ugUXU3!FG-.>w%N]p9aQllϚ'v>pZ߲$}6q&uE8b9Z\}ܱ]ujl]%V;9| _3H7DŎ7ޛps* P"} c#]HDhtⓐEuƭ:v} Z3R#a#vvQ6:|wEsY@J!6YY4f*к2CV[@SNL+ssƛ%9.Gтm@퐻.G͜Z 0KE G0CmKa +%l11b/B GbC[!YѓjKOm1enLO_1v4~XОT˪|1lH\S|? RbYnE'5Z?;N;^ b,qN嘼.ym5@Z_O F)}Qs.|<=CZN MH8zFFϗ-A%$W[Ւ5>OVq;G1 'E' ܓgY,K ۦlkc!ۅCvC:J`\:V#G7]Ks'|_~MY\Ho"x '''9%+ z?A#<#Ō7 %qqd,Mnvg\oϹ}}zLw婅OH5+mWh+ 7Apζ\Www9Ț~gytE mFy ).T>48{,xU~_;.U T(SLmOLJӦQHJOƯEK(d; )nrJ3{gGqhN;^?NM ȗkdnn ک|0x}ao2ݝw}ԙ@8nhOҚK91-}g} ?~vΧ3 `F,<vOX&<+>,?&_&i|ַ@>h\%O\WqMgg x}w|mn 2Bdp> HC_9]U:&k|Β)wr\=Lhj o/7Q@*А`/y3bȜ9WcKoiC{ʕ~V.nc>4X@wN^ϹϹ]sࢽ+cj×G:Cbh.K֊ظܬι} R_~~)kBwOh91it_Tsup)3>Ίx5&:?4TAEsnpfp0TKgs>RౡÆCmM1ɕK# jiR4.yU>Mi9T9uWFXs4z{'DqL'Y!{+do쬐meXc6>q'kS_X4O/05F߬KuPO]fSlsB?_3y^d2c~1KOpbI-BхG#<ϗ;_?ǿlk<#(A%?q{wՏd_丿NH.MrRn ~ŵjAg Z`;9b̲*ƔC+T/($VOƕ刺fșEu h bxЏFi#ꕁ5+tW\?[~UzppFSNdD1o{.>k'ȕtuJXu 4֥-jۤRWS6$kŜ;o_K:ß{GKD!m6 /()9Cz$Շo=#k_O*|v?%n@zdhZDH-e4O/vWeC+hM@jCSSKB_c.=1)G8"szg%XWuE˰]>P%&Ʃ+4SG WhCNMhbKryyw)ӞY!ȕG~)LjjG Y2b@m+YiGì;16gpϩ{˻wiKޚG{&QL S{”֛!dz醞3[[//Xk}SX^(KXsRU1.*h> ZЎ\<]#Lт; 0VjŰ^,sO_0_agthK<,:i`4-#- #WJ+/^ S hIFu?yGy?[w9_c І&ߏ설 *ꥄjw5Du$RSq꓋S:R7x:eu3Lmd=pYhDow/x؝C\8<)q%B ؠ9965AcB%Y6=$w:-;hO/`zӢ9$ ={ΰS " ZvaRu*eP(t 0eҹC A!QADFvHa:wn*곲{=Vw>[n@;xUW\\|q5/{ Z0WG{Κ=g- gwW77 SN7|4֓r ,Ya55Vs8" /yk>#uFΔ:Xm}+JļJ Sq@ݘxmj%q1SCR'Rg/:L1^MWtڮF\yiȅ{Qa.[B9h&آ mJqLW Z ZTO F?JKZM9#ºea8/+jC#wm+D(ɅǺ.iE5b? 'UVuNen{AҨ]BXI8#t(LM̽kL;A1gLC97M b;!mzoPt) ʷh{:H֫ڂ +g:,Ԑ ɚ j5pT S5Um#zޥV0:' v') >5O(4ܴɰI56JsU_p^p+. G4ưtŰl?lEB{1D;:=jG72z@4$_y >yϘ6TXj#<#?c~a{_c%aODGi+BR"o[AR=O[ӚÖn5I,?T1ʰا$YBgqsvim%IC&xSNB7LvAq):,JӥJ4)˨TKTD%qѪE%"WRZsHs n䏼:yM݁k@_vڜs9jsƊyb?T_C ӣpI-h_4AXTM%> J06= 1񣔡 kV|Z{z5)NF~y{tgh]B-JaQf6ҢP.rȕK!]NթIidatY!e6bюY4#1FmN䌲u5IwK8Q@[cIkA(pUvF!K!^E4SxE+%Npq0.+\/r-!Z"mÅtronϦϭĶJ,%( 8R'N;q͜ԩ tHL1G܈sLI=6UI6}Dzޠ*3]*Ϣ:Uǥ4QO54SlX7SWgrQ5TA1"3da),(_`x”lA#ʃ3lr&5lrug_}va]ԇ.Co1%wДQ|R@4 !OG-Bi]o͋pv09PosXފDx, ؈;]b?"|qn;8xiUQC"r'7'˟Ec^7W AM55Ns=ϗ;|-!-"N97Ēgڏ. }\GV笺<rR:_g,p< Mیc5gGJl4S'-hDkP=2ɐ6pA=THEluL{eb5??o6g>aM8Cӧ5J)KԘo"y~bwD񑩎5ZMGi)MpH 5=fb qKr`[_Gٛ2~u?WАK\4+Ei45B K9u%﫯up>MvI^M7&܋#rABTaXġjt`GL_ՀW} H`U*b:;!bn3r{Ey7w8euYywg=VyG5~Ԧ@}C.)E+Zl/{*ivJ҄&"m@ Wi5Ԁg;ƒȬQ\hjjVBmy i@H~I3dKZ2j̦SP MbPe)DB*P07@"H AxSfΘXhFYl. 'v#:rGdI@DW[&noZ[BPWUeQW&umqpY}v5t⑱̆zT{ %5ZK!QjM~A7R+ 17xk<r.ܖgx"zx{B+,Z!]Ъv^1猓x]քqD~bsR-]$_x{v˪[s,&R;0;Ipp u.I8,\ゴ X+qC qG֔ S &m53SpSMuY8voDb-赤,C ouwC3Cvh \c&1P JR;+btQW7)[;tR>sUkS,[[t: 3O%b$s^Jb ̸ۯ0]m=)yP'$99hC+Ƀu'd Μbl;b;s6T*qs O+l$U>w)y0 &xED JKr#O7NMF Nވ;{;E9fX}VMLIXC5ξd^qlj+NÐË<)~)Z u4Xut1Ci3ԗ<>jtfuun\3X/W[{Rç2J˦kkK8⪹Cg\?_/Vt*c"VPyL?{B&<\-ht-v^? 4tF* FȖbR>kQYcȦp,,BCbТ=Gj ¢~0tD7j̲ırAf '܇휰 CԈ$=1[sge<GyG~eZ4kI}#~oj9DܡI|ȨujjjZW$[FK~DHk|#GLjBX[A:EF_5vo$DbK-MJ6I!YȻNm`u_T}% %4t?>Ѣ[!!q29ecwiN']4Ak$ySl=;h|=rzt"`? yr;z=by%ɮWhWMݣPUК:;.4CP6)]wKTP6Ƣڔ,ʧ.!KIWkRwp0~?__A..6Ls0#i0ZZ(\p^)y^:3ȟ;g6sWn!UnRgUfb%%>{'_1z5ՌiiUcq\ΘT*vP K B~XVAWnwt{7{yI\E+_o5ghsƜS3f[}@G#RN)l𝄞bb;>'oEl={'_?[~gk)G2r$g`nh 4i#y#5ڞF30(]µ(?`]ė/A{p\zK 'a« ]IKu[chnu=&X>m,(&Ģ2zAd[s֜,X&cʙG~9QTV3/`7syfBڠx1!yL0{f2J񞁶d,’qm~Gy䑟?[׵6<~O"-ÜTT%fwGtӈ }|:袦uLE4qkFT2h?,ִۯˬЋjUSUTNE3\sx{`h-pFSTjmQ [;/)܀ (ܔhVfIl@Y.+>ǺTEyxE,jH'NH#CC4dGnT )k|aRj(]#c$"B78 E:Mנtjۤlmev0K=J^uxf B (ʎNueG\OqD,b-fY4%1D t=ZQ`Bj4kBmgXFn(2cX{}V~?_&~ŻEe^ͶN%6DՎHo8[pzd5#7]ì;c}eWuqQ.kGjxZ"×)~msy=pۭ۬05/Bi# b:RƖ5T=*4ْhVpޢJ̮LxgL 7ӵa% 4"5Al1zL1bh_qEgdt-.Qᱥö:q[iTXF+s"ׄqUִJ'\rBCDXv 15&\'ܔL4"9q#}ɈYr$8Lt,"IvÓVmVFR9EU1>?!#2So9.)>&aPeH8Ե,. =I˰]3t-gkלnu1i)1̚rT\"S(ǿ4{|@F3)CB%mZ̪E*=m!T„7.URomО9mqo|>Mr{Ѿhf:b۠5-k F_ȫ<;LBυA[ ~`{>!GyG~l߹?6+qp%OO)K-uC{Pz5jعw7ޠ $b(#MET%W d=>#|̠;k\'al1ЗY Μ%i]oي.GP~5]y}Ofü3aMEeT]!l2e=.]GwY0xf y0L.ƴN RN)*FZn$w!tGdzdɲ+:"uU]`I -4+uWw:edwjvch|!,?m1Ed:{TYw1=\L?Ո}طH|P=|]g6ـ2Uo\5 %'=X%L2U璘>!5m_004ȱ p\ఘtL 2f΄)r9rraƂrUB\Ĕ|dtjPf]ܛ7iJI&%>S!ĘĆ~y9e~?f{ܙd KKΰ#ƌXKޣPpCFN 5 YH#89o8opS:8b1ekPuUvGq-v=vA#\5';N[N[ -EK4@@>?K0-qG{[Ɨ ;) ,.l&sМˉOXbnĆCP$uz'x_ Xw;jwͷ'OuKvߐ9Cű7g^? AkZYUJItƶYtن}BKػʰ|ˉ}Ϲvls3NToX?;,Ky۸~A1{h ޻/Q9R,`I S2!1:,%>C|<36|{~=?<|Cqgp?=%+/fb ?Z^Xx1|8b{!Oo鍷Y9P|)ؐx&[gwx )jHaˋ܂DZz7rMr 5Mg0d-/.?#ϥ݀RW(IRa_;TLe*TB#$$R8mk._H¦\h8ْ q_{J|ljp0KdBfb)f2{ai1I]bO#l[~C%7t# m%QLz3l7CB@c jYes>/8Rq 5%JYf7럹\q|gAYeͱW @6<.8Chi䖖XYuƈ#%N;b"PS~j+SmZF'sF's&{9##UI:I$ jSbN垕2sQQ79}}å~˷$-pG vT2!Q&颦"TH/Пh.pbz;ԻëBBe9A8P`B79ޛ fc/\ĥNzZ{'x/VIjm|&-xi.e /]q~q}qeJ˔F٪%tl76i\qk+Wtz= ѷAGM7+&P=rUg $=K~,aX9 _zu5<8JZhUVMETE!kĭMk%[:-WO︹`=y{#arֆgwz~밾zEHIEXsU"PU6j\3".7XqiAWкx4F̌19F5S4Hj4Jƅq+L RM:yFqLfg/z#2&L sJ&l&$^ J 9rNL*w6cNDKT6EJG2Vf\\7UPE\Pqc&a hv\ U-g.[ -1 _8w=OSJJtۊq z_\#kt69icY|/GJX]F}T:tZ ^|RUttw;&*U*PLطh(?{%V̫>OGt+[jѴysBr,G}VQWo֧ %x(Y%J^ ٍQ5G##y/vxOH'AyFJ$g&xϕ6z*P0M `%iQچrJ]jEY4qU\IJf^/ KF%fI/"R]4>?+bӡ)Ȣ4cY[=VR;OP wEv#RIHI&LSP!ϴ481*95}ƑĘ!Fq`6K&_.' [9n̝(1 &%11S͜bS4vՄf#PՆ׏  #6](UFK:lLiyL& 按5cp1].=•G8mgo9syq͋9ۊ:׈kU=>gthJ F֜tU]vj:H ‘R &M|C"LDgw.e9Q&Ҽn&-ֲ/]Ra[q=iz1ƄC0 8vR'-9wA]Q]'v'x'o/Wuïɔ.%0khrft>x`B(g:!ƠdDIQx)W*R~.%RѨ ֑i/dV&VXݍxHtK 5M$/L )dF& e=88N w.?_ ~愇+_-k+ rS3(P6z[-2A*tc/oihhͱDJTBR[ϲ2-O1|Ү݀G~վzVL5&d􂷟_6y*½ 9цAm7t fK٠6 Q)̐nř~`ކCg0s\S5v ]uGGRt$tT-6)Cus3{9c3*d6;4$y= ^=-Aw`jͫ_Khrv/S32 !LwHJ%U_6A-91 %9YV2=oGIzb*` p\ZdA:.;ƼZ\qS^2'fHrIJ__sK>/fxuq; ?|ˑHGl;:QH*raNn\;312//m t1uڹN?`[rS2H]jI-#:{""W5*dD׼2=_?:<G<.3rvd:0kt\$7gP}$Et-g9knߞswswK9ΜÀgw A)C.wQs\qvsIU|N75>g}8L9Ly}~wM`PRg*qGС>ess2 ]c`# |"tr e'L9!&SqBs&W6{aP^D=nF.J]6Slt#zS.L'LTs`AyVO27# 9CkFkD2}~O<O .w_ b"u] ڕ`w\d4A UJE)4D;4{>}T;TFkLe\)$Z&tBat͐nAH25Y|)?9'I̱&3 IDATq=o)ӂhӰʈO2}E[ VVtESz1ЭD)k %,։ ]ex̏Y#TD d-4REUsb"\1s+o2g{z5'W3N3&rE T)i]V PF:@_X&d򒟦_*uƑxG3g,#Ӏۛ+{ 4ۢR!TVy)5ZY}xDFHsk GsOi3qxV jfP/yUgD9 Su§3 +Qq\oWX>΀>YSdy=Kh6#uPZi\}~ehU'5ފooGIXr^ֿ̕51@*G5?P^v m)5)0Si'߉Ჾ66?yk4c_ib:~Rt7h K>l_nTszƎj??r6o#\..ZItX%cV3bAO_|ZԐX)VJv#&T1˔ݷeҝrlc2B0 F-ʸBXN+Et RO<O/V1KTW>q#OuVDXi +N)vcLrfIn6&aQdd¨SI?7L=Y9 砎6kʶTQj{A;3hQ)0ɠ,*f~,TM1?0,8=]1!EgHL,Y}ʢ$EkIKe)dL#0jąPK|l^rW96$H:@ە t,1o7V%ɣI4ߚ,17, k"{5N'f`9]-][NYg㔇p4,6HkM*K+y=G~͍|^s(X#>&/iw9KH}):[~1HQ)]e X׃O+ՈZwsɞX֙(U\UE٪vF^redo[0Nf8AHZ v8kGg, %%xCrxhuVu̠6X^ ]!:O?_3(V  Y#:`jXa}$z W0U.P yX#ɮ!'#ϯ,ș3tS2jF[֚ff{$ 8Dm>¦mw4*̤;CuI|a]i 'a: JR8E[F[a9ceF%^am)>dQ25G̰bs |pG!j~Vo4!}'=qmmu=\9W{m'ѫ>f@9m/A:q .+uHȄOTLȬ 8`O#UaWvإ>' FDv)kzcn:N`!M%<jvƘ9B(=xO<O<_/Xyw/PQ(bt4 K#!N #:[afkvFOK>Wl>K -1ۍ=sàZ2_yX]] u֤:sҒyM4K ,%u͗,iv,1/CИ__D'2Z/9?2 'xUw):Έeg}gͪ3 ,@{h%ʰ!TLfEL,wVv/Y}6QM#|HОH'>[VQlHbR EOem#sLl'cƚS1~ R]\&j2D3i>;N-;l.6bQQJa3gBWVc SͮS[cѸbjRѲkz|_~ Mc<;D\p}bp=lM䴭@RڂK#5ϧ8i@ BIV,&N[9:Y+.k.F҂v҉Nط]pι>oy"[S (Fm_ߔʕ+<-SdAqjAh[dƯN)|}'{GhZɸ] K06> 5YkKhtSg26pb1wAAd=P{SJTt'Kl߄8J+Q59}}ڔ-,E!l}#bդRerY#L-C'):V!5|Nӑp_+?zkƇ9=9e8mBpFCv]1{bJ|0 K pF@=iJ)G.eLP_(Tºq^ 䊅'?֑ 㯦X1DEG}㰨U4kl`.:dsf'(kTzIeTe8hT-Z}FAa#Vy^x(UO3>ICVPN1i+za7xG h61&.Y#s'x'+  ǃ)W5fjl>|?dlox1y7\;;.;.;n3Ԧdr]E1KnlZ#GҎ_-=.ʿ`Za]הiC* jKǯKǯmzW!kn3"R&lE0,8=2 #.yl ,hlQND ԊRՇvKR;VwC>ݽP$@I楄x)h@-X#J gASJ4]w(otCOrPՒҖҖX>sΏ(T4OH0sTjِ՘<>qxs9QNh2M "RШ0_{Ξ?0yHoUCdj&ʜиp $P%&ې7/FDR5Ëk5}6Τ %I[ľM T^w}3L#֫ɣ/v掎; 8cAT{0 by_c!2A"1l&6l TIIܛtľqdZRժ;v+Eiv{hRAJ͢~\A\{_N~ɣ<&M[2Ak 49ܼ{H3*WtHv$RUI%-5 pǍayԥrƋC[!#ev"]8a4HAԨjNAY)[`3,Ww<d̰kiTa61]iDQvI !85ժYCΊ[b&Ɩh A3hr KM[ʐLJmp=m!a9A]/yLNu\W)tT>r(aUϪ #X]㬂CdJS|ٞ2r\8׬.1ԖHJNr+.h)*,`HZ~‡KRiݤ9#Ԗp r'q_X0&ԕZ}!p]*K6*K!UM'l!;%kI; Wq9s&§ U6mzc9s#;5j'G34-Gj.P*A%}#|B'34 KC2[\35H*c}kZEXs8hkst/-϶0 B&f#ʌ~#nW9R} HS#=64HM^džAeOP҆2Ӱ L{Fϸ'x'o/W-JW:2 h~NčMNo;:;%Ɋ~@^6yg](g*$~).RaQe}p\pWDʦf4Y#&|>=h{3>-q}{Ez+9qoF-#fuzDLwLje;vNt}zA''_x9*^$[Z$KB1[tK:Ǝs3s^CPwSC#nԚTq)gn>css疑qA5 *U%etCFq1GT#{ܢ6%0\]9'R]B#t*D_ȫ H͗]aƷ9|mpʱh iC](ZDJUb1fq0h7mڔBƗ#.{f/SY ?^}+LG̸`<|d21̨MD5)T!Zb!C((VlyĻkĦRdZ>D),[<+5#^p͈,PΙ?fdisl_ws F0"rn̔?*F V/SՕYA28m=vtaQӀ43B|?kMM_ 8{E% YvA_ʻD;CtXj]vOw3j J_!Zt-G3hxԆgLc]r:묐 2%ikD2q_Gc^A,9KTQ{|n1LIЎI6i&lʦ;,? ~:%s,rIs8op'pS`rv!tDnAn}-h ^HS]hEf\>Jc^PPkP;hCHJBa(LJƊ owqOI##h1ro ^Q@ cp;W$1VQ\īg3<̯_O{lu,ݞr<3<__sv] @&JBhB̯֬-Aeꔦ!u!ST;4uY8<\4sL@S5r6ef!K(H>Okz]pT-R\=yr(3v"'Q :>-:09{={H>3@@<㥫,3,ԞIUMޔSbK8"(l¡H,r1`4%0Lra$zU-vȅ DR3-НݩН˩[YVT8= ZȊrLj;u*6Ya4D$3JӤE3slv6i"hvG3ò2LQe(D ,Ы ,( *[DPֈ "a"LČS` 8V.2ewjmBb"XtT;u nqˢ2pXxBknIZI@آ[%o]ҵv,hD*4M") %<#E~0RE/[#l @qՊK˭=Kq\O 8ǀ%PB2pHD*da&퐥.hE<8]"Zː;e``9V 4 HMG3j7` JM񹅹[$M:0H:ie-ѵ0YF#)}rp=8$dK| RXv9Ma;8 R<*Aj"SCxC0CE4?PGvtTR ꨃFtXc9jPҹ2t^83n:YЧF#gz(t-i#M2I)jtS!lPگ3<3^yx 585P/LT@.YR&JJtƢa=Xz(d^D׼ќ͓<*SuM> hZ>V@&^QАsi60&l,Q@W!u$t^L'b;kpv&9 ne'ړ5Ɇ0э ÐuB;#2y{!a|ۦW61N+;Hqi Z]b JЙ}~悃A]* K21Y6HI^9 ;|'aې>$C3WoOQ唑AeuEXĘicSct6Zt ɕBkZq>j -~|{WЉ:k:+zƊ=Y(`1'< ؃sF*ny┻fUm-E z^68BЫ F% K\,PCA쫀bLuևHlFYػ ľOA EX5mꚗ5r vuwrfP͂-vYHZzdM.mFS51Þ1'6)|&ŔI@Tn^m3Gv`½6aK>ݽ)CR&Wbka瓬kt!# QJm=LUA6H5,tMC6Zlm6O-\p;8Lh]Ra='6:q[rS5:#=-S$ -)gr*)Jބ:NwbhNȺ.!]E. ꥎ\K.jqDie/jѳN\؝2 2yGd?-'kGgLXJD24Y.E}gy_Z|?]Q[:0A^U^O-5VB߃BIŋ~k! . ./xFĢE IAIPuQ=C>w&^A2_8#s-?]ޏ|~b6GQJ9͔#=hF^ѫWQ%5=f\4ot>uvDTV0Gk~9:_%DԦAj$VS9$];-9=oAW̙E^Lm@w֠v &Dњ_`#G5X LmވGh&lM|ݢdSE:D:FTb9W`0=eOp j*֠&[JQ :1Ә1ť򣋪 <3g`87o sPW8TɌuc2.,54QS`ͨ~bT%jΕ|Ń:Gk~4b[(W&ebPLƽG\g&{ İ vVܤ'9ү,.=A{|?ur#ddis{won["Mƫ9#6Q !~*bB=fd>*!$7dEؗ!d7E,a93VDx GS/:SګՎh7Ưlqk%;ⰍmP IylꈽIT^8Y/ןl%]%Srͦ/y/D8dl.{c3Oyŧ?ӟ^^w)M0L/ً(ҡUg;%u)6IYvKi-#S&qYyi>.?Sy8zg8eNIgtɀY>>Z?' 䜓<"H`'\Y/5 YXh9XUItEUi_%FTv4 lrg`uUkTҦ*,&guvNOx.##bM"q.?:yG אָ0xbS[KmRY ?`'j謶TATR.42 .[.$!26Ԑ$sn## .]@vLG@:إJie%YX;M[K)I rcZ^=:bQ;Lw}t73T Z i,ِl S&uQ&ybUJ"u<%tLP e,wz9AA9hv404@C" S+87hY[+>J$z@*}ݼv4~Y9F3,# Fn=ygy3Z㷹zru꾍;=ʵI/lJiRIQ)2ja/ت}Ta[C,&%њh3Z1_jĤF$]ҢLʲYZRh6;#scs8r9T6=o BV1RB$\^BfԳQ] z͡𛪀pa{?iI|=MX= Z!v'uyӹ$-\R'zϢX xz状3{8ZJEY96V^j@h^ɾrb;=[8NASKK*]ϥu)+MƚROŐy0$9wAEϛ393FOP($,rDsj^.K<w 7G~JgQc#17ol.ׯ[jߥmnqTsN!M'"'Nۤm:k'bY;[)R,kN# $[,eJvXɈ4sR`C˛]ަhYB,(kz5=Q/| *hMN~z|||챘٪-2͒<T7|R%Be$*'3<3WWk7NQmu.<ԅGv>5:ՍNUTE, ԶKHlʊ*d+X}VG͂50p0˒7KNO&3D+\" xF̼139OvPRP&e3[<nc*[蓂Η Zٚ13Ɵ[ Cݴt>CŚt  ڧTeۢniaʠ;epŅ32ѹq{fQH5'-2[ OXZ<>OsJe Oxwǯxa(Y!6[ZQ!*.>pqvً;.>"s6)&mѼkT!X< 8{3&%GA͎gQ#kN} xm\Kk턝/y Ѽufn X m@.mz5HŎ;BbBD<2"Ǧ6cz{QTC-YTdIG mAfZ6ژ{1a##$vwv_߶Yd}EMȘ/9>b}9lK8,=]txz5u&zespP.^>mo?ӏRXVuiz嚞!(Xf84|Rz3{Z|7X|jetY'z5ˇ΍O\y]ञ2V^6 6cPY06p>a ؅/0l $:e~&Z+<`lBlA\C\S.S%-L7o =⦂[ 7CI5/W /靭IKRk {.]Vn+g7k&R=IYm'gpP=AՑo-J1^zW]K,2JС4 JdjǏ/Q}2)?5f.:j;J(öevDQ{gyW˯mo}w00a@@u ]YoUڡ)M,Gc45i…RC/ʦI4̴p7;GMhJ5לhX*bK275X1)F&)=p8皗\sƼ@_aSnsqac 5yt.d=2_)} zK{S_?&<ӵ-9BjUNO eS >KL<8g|A3y͙wE4^cZِٻ#n霧hR!d Gh}modjf{?*ˢχ["bǑK\D X.R $~0DS~^+HМ sP`U)r1wLF__Ҹ~EhJ?~ɕqN,H5yh= >ڿĘJ^s;ORg.oPr4J M3ԧIJa#b7pC.j18zkl;aQS>,euDlf쒈8ko7-BJjgO\sâkuDT_g?Zsb]w'`lTĆFG<] "P_!| DcG:@ ĤF ]44RuґN]FIo[t,p[lisZ;c)>n3~fؘ(qB܀I2@~e\JiA>YmVm<3<ߌ_ðtAP ZO}Y% NM>P urm4\@( ՞؅vĮQ&+Mv\6yt#X5VԺet-cJdl=BkG[е"A¢aPb!*v)0XSj]#P429P=9]%u]|! Mufhm~꽥v5ޫ7n{i,~ [  ڂp٘hK8d;k3 î? 䵃`ޜ9/_\28{"?342<%6.ɕM(b"$q RVު0Z3. ⎽ ՜% Ꭓa$LX$}feuYz=:gghH aX:=R@X'P$lSBj:ؠ<߲-;̋!?ʯdCݝަ as/ T`%߄3:4?4QFEgOD:֒ܰ95Ι2z9׷/yG([ek"$-E ᜾w8(?\G "缸Ծh}Rw-RƪsoH*T"Ӑt% :5֒^{IY>QF| K5VP:~Bp)f<`=0Tr:G/jtYV|XGt-*iUA74d[N!هlFm6^svi&.bEIIlT@/)n8D|ZYYEL>'maR&;,o St'_-UP ߞF<3ʵ3)t\39hSj.<.::$ZZReĶ "{ˑN) 1tҶB$6MXJJ`'B6"b%EJ뢴6 ʰ(L2ӐsA}%_u̽%Te{BQtlb"u}6OɈC zڒ7{fyE|ݦ2(6F ͩI8 6W2>_bc e݆NmpW g↧|~p]B=^=^ѵfL1־aJ K IDATuncnn15j\r*yS}$ma퓬|nS>^A{Ex@'0^>bqdkxvCLW'FeTjM$."ajE9uŗ6Orc=$mnJ~o 3&)JG<vG!ZTG^4g=-nN͢&ŘE>B۵Yn,6@uQ$6ZP>Y u jڤgL= l6|ソJ\;1\UHp(]n~6Q]$*7t5_o!.K[ޭ`F:a+|M)Ga}0M=NVtV] '<H^ .w>qGdYpҽgҝc$N'c@Y6\U`?͏0wGܓ)d;iygTy%ts 5 ʚS.j딄%*p7i{񞮷ctfD b醘ZM螁%7 7Snfzx ,X_* %(NIlK5o*|/CҺDYnGW>pM$0es T=C{{'<<1%AC:3<3^~ϰ Ѭ,AHmܬve˔"*.:(WU^PZȘxϗOHnò;`:,B[H"}3 fx[ Sj:trQ`/VkmLʦqJ5 T'5I s֧:MnꔙN5ר/lx1_x\ W߽pA=xĉ:oOHs"'A) u*@uP35~w??ֈoj0?hMY}V@Шwuvf|oG{sqK lr|K⋄09??Ɏd[^[kwq$wT"  11Y PJHL 9}4᭹uW ,d"+%I]&3Z߷ެ$Y9 f.dL%+-K$=|1yTn 64u'if/{ቘ7cfofsN/0Tź03tz!51&]ĴWeZWK1TGL gu]#/lFjtFTq0 т#qhhĿcXc-K<}ٓWVvj]6e1dؤCij_7OʹHR1~3t29aQxGg|7#}΍m<'ȺXH\UT7~ir3;O[' 7K>X_9;~$۰˫Oj"=֧]~J?_(Sb+J۸vߊ++o?3H{NKVc|{Ze .!)=edX,5ȱЌ< \OGF ę ZFb_nk)^4\$!G :%fGZH2$ 6[e';l.nJXWꄞ3=_>nanKmK|Bu.`\34l[mHjGmtB) ,X;]2!%\2#>"ݷB;ͪg#JTiBv*Zf)r|`JdIDgG6)mΦF+ZzOx?%l e2ĄL qSqS)zЀ6.De vD2PtAd{̝1L䘓slV.M, g ͟svN$Ǘ{=Lv3׼~OHZR4Nd{l.wՔm%ZMɰuQ뚱;:/pFoI:L8{>u$2'\g<7Ժ%jȲ3C@$ɉp`QOYԣpb CAr2loЌgT ۜp:eQ:wֈ]EwqObNw1x[]ϰb+#2Aנc<2h4}BgP wm{!,) A䲺yf/tɻ_t -W uf:%2p8PhO,>l1X0ba@$C/sv6n&2 _  @AB,,]̗&XOM'`=)q-Z'x~ 6~)KBj. 1R'R#Nc:7B5$j#%IߠtYO\LRP\E]Iv KQU=E?ǏYmt ;.UIFB0{k55A7:iw-fwDwIלN^xknG\ܜqq{ʛ'DK1XCB|!.%R* tD-k'93VՀ鳍jQ^:ŵɣ{^{?g^`7ʰ242YL|̹q5R"c&.1 )kՀe5dY 4A=0#Ah5zh)d?el!fø^rՎ>[zƖs }ɽBV4C PXvetk\;mмt32,=sNf@B@ҚN4Qw4ꮤH?oc9ec{s1ffph[[.QCZY#)kvf'}G~2fH8y|!ɽj6K9ܷ0}ER,39:%1'Ih!ulЕ[ZMC(%Di:PZPlQE@*0DE]lpQN+akU+l(\Z"Ė4)tRT}wګE-HR\4@5f'$>J^HpGopN@6@@NSt:-F[Ǎ<szuțSv4IYԵA;8DZhpGhfg'XBtQ}*FbFV QjZԄf1hz}J}1Lsc 2iNwHt]U +&fjϰ|_mfmV@ iYWz5C9JÜi=#6]" c:4D$Ua&T;ngl7I8/\C_; iRG3|;T_ &0.ѭͨP DT)ћj M'sˠڄKYH+BT0JĈ ֒%F<6.Dy>ָ%V~TOiEG,cLFEINIMPԃ o$NP$KY}!׫GY=Hb8ˆc$bYzCKRHFTIZ)k" S{KK XgoWsjS8-pg]T-B)w<<ǯm>:d && 0aW {սMuchm{h\aF5/?/! (`zɇO/yK|1sf}»]t>ܷ|~ƺ{@ifJRL5|'x'++8bOh-}b%>-(v[b;ex퓧.I"M]DtAh᪽3RZ25:$MTe4i?~=n?7<|!ib~m~'$Ͼg_xͰY":7 }LaY$Ox$OKƋy֫NIv` 7ô2L-!@ =6pH&%GٌhapGlDa#:ܨ#G$O\ö`f^"SEhTNj*EhFX7 !&G-GxQ|U!~Ἥ1&A P/$ZPcGni$م6bG60~[A_QjyU;9;>[Χ[sG8FVE8ybB2I[YlD_|݋~wp""a70Q^aPubL;i9砚s񲄎iBZ2eF[N ҫ3Β7< .5N NtH(\"f-R&TAԌ{'WyX^x~ƛm+' -Β4jZT-JI*]tQUEyYm]CR6.*pLˌĺԺf_~.;emIߎb  M IDATH͇D] $q\vzb7avs)/!!r#&$u{d.NlUhUvPt*Of-Z `4xw&_p6xU5y)F9!R!mfb{s& [˸<ʌ??f3>}ʟɟ4 ???cevXx;#Z,cv]k&h RL53qwj 7V76G.B1,Ex1+{1KF_7ԱƮf˨)&"_effiUfվbikÓ'k\z,ńꐩ2P%-"b#r2V֭bIZ~"Q6Uf?K2THA4F#%Bh"WȸA A-%S:zV`}QW,Ei3KƱ˸a^s0욾`&ZPAaڝb8[[j5f^bp6 "m!k t#4)Z&*3ZՖvhHhDg+ Jq~aT3-7vT9,Ikn]y.)R(iSu`jhlIb:HA.jAdUn 25dt[&;:3KϽ56O+ՠUBJ ⥏x#qe%=nۋpž~E5O͵7J"OmTQ`ݮpH5/ cPl˴sZ+vd T蛂k+F͂It+јi L>kN{oSO= ;c  c힢6)@5!\,Ub{UE/d@-u2$Ƥ,4(zSZ5]Kk\/esZmS a6HA7*L7I=mC[ T]!V>C,|2LJ3{ lI#5%0{5~ӷNyeP$e`a;걸18!Х , z)ѭ0kv90&DBX¦Ģ L*ߠu*BvJ쁢=Hni 2L-6f\M~Q3\=h<g+3~??3??oo;.//?~u~Ydi" :[[~H]i++:KQYVDI6Zm1O~J#<.DlAu%):ܢj  7r02=0m1TQ*[f"?.??%|tÎCny+Fr'ȱ",v~tEz 첉l!C4Þ8H%[9ԡ$[8VFi-L{c`{-=fp3ﰚ,wt#Qqs%=sR<Y7]V5AIԎ)g`K"&]Gϩ1Q-} Km:`>`:-'\p^06tNimy<3){ FQ)R{3b%t="#2qU{sUl>YpH7hC-t¦~Jl.c>I a4#.}͠q3veYNP1g :yE`-}r%Ma}ּŁv'ݐ[yHtOqsb2F, fk==xv?)ԚFTN$Ą,S DX i6K[M⎻!ٕد:QdtҐAa} U"F~Zc!=fYv@ٴ-oG∄Xi, %4]KaHAmr,tC4`Y9ahZ5S\FDU1׭SnZN;i >Ua-pp5ʱe6w]+ݔ5OsReٌXc͘"jscn_ݴ``WC\B1螂?p;jʒ$CvI(lQ}F)sb{MncĨA ъ7q>Wd.)KD.gWxWf雯~COnwOGG_bv_$5o%uR6u4";GC{k5֚V$ FP"V2r*rnse|3>ܢˊd5`s~'إ]vi0mrAPB <)bz]̽K>]cY4oo6qwC~~ڈ͸v!2| =%' )2i;CيeZx:z;hhy](MܥV:iJP}_; ח^??Oã5jCran{CE IC;.8 V6յ ӆX0 ,110gF*xga 7}U‘# 5-vcW{Xff5( ' 7!77nQSZs9gUwov.a~6Aiijid"C86(^ߋ{!^7BZUH hU)y}g,ˏB~V5Nb'Lby{ͩxͩCr1K$;KGG --͙sdj3~ O/yLs>VpV| i>Эrh>;b]YGm1(jc4%FSb6վ3/Ѫ.1ȑAS Vh5txde X,[C[ V#K+dD[n9n(W軆xby5eAےx#pҔN17,^bri}Kϋ4Hx9-y͐%DwIkF %QoזփJ( +6p얏/1G_q !pH5Wx~̺lڤ:i-G[j_,ҮЖCcơwKehEHVzKɈ>'^boTgL:)ǓG)A@ұt-ʖ6&\\Mc&xʋ=N]r'%K!s9F~ѐd"V FCg7<|G}\]]/n[_>AJ]$IZyJǪGWuI`:J'_ٰi+ξHɫh1=ՋwX^NRTЛ  HAQHAm +N7/WZJjM˻!?mcvUt1Qs7o8 +wgő7Y␭4†{A4mS%&Z`'?> 9|~3r3;ǷGyd^0tt-xybaqgz|Siitγ&B:Dtd>(Szu,\(Rf‹S&w/ӟȻBUχ>鳒(,re){*x)@SyZ_zo?aPdqI~s:~'|h-yHٮl}._y<|~O1(;Smjnr%۲GܭxunJRq1c\1iBF@Q:n2b /O uS}+#<_ M]n'E Pf]5/{o#[z=81GF7\3)V 땤vF   uð(RUu[ޜęg/"Yl SD|VmxYU&hF44Aݑ]Ҕds#X ( 0EJ,Q52]]`[Y*h5 BA.c g;P5Wn?׏iv- c }!gTԪCDzCuJadcp?[9e׺>1$DU..SyDtgIJ4]ө丂2"&5¶ KFK,yK5 ͜ZXmֽ6k\E%<2ˇ>܂I'^v]eC2U#2 j MŖy=s=Uwr^ufb1l,BT/,A!ûG;C} T6k T*.vD@L6-j "U"sqQPudv\Ӓטkb͗$͗'01FLjxlMC<{*ssuإZ{v<^Hs3zd&T(L෨SZU0 OS`%(7í^a 17S4B5nhkŀ>dxw':71>~mt[v7 A:.y0R~ƻfa# bT! !1ּ re~1+%;Y=fy'm+ox|1ЋMe!ϐ I-UrU6x!FLDLI8ܲY\Id%O_EMK:%f7FtHk\ |5x*)g;$ID\s)( pUXd3l}UJW֥CRHYJUduC$lbLXw.a S3-n]0v5f!\94kAj^UtrK%4l2D2rIgR.j5/j#nɑ89a3$TuP۵8~O!i1U5 a56XEkrRڧ n2;KPmM.Peb%(*曧x }s=5Q7Lk ]׿ggg<~嵇3)HOsFf]tXN׏tfqeyeq٥:bY[ct)4岇b"ڷwva,.9 ̉J*,-fyYކM{)^bǛ,fMբ$hta$]{AϙI9j`0m! v#"<',bH66_vXmTF*j&$5nzP )5$yrSUt?'=2I-net%@ib*U(m\ ٭ЕԹL4=h?]n/~IG_`1B&,.]*|Ϧ*T.CݠBa.0ED|7/9"zeڮJ\L&)ĝȚPhUKЎKΊLJ5ӋɅLu_j tAd9cE`";4 A[rOgjCHjr1v HǾs`SZAx3NY-֒/r.ꂲ.'І ezD"k6+)d4-uevaǘFLj\D;|s>EЧe)i9'gJNw][=s=Zs˿<~ۺ-A] ?Zg/JnRRSZ w||!NGy*.Օj. dJtY=$Fȣ[b;cq3qiniF˺< NM.}Ct(Й=lS;{׌G]ۺbO\/pnsr(N94Oコ1Rmd@$6gi)v\@.:/WCK~-n?W]^KYmB!sU&A~i&٥4g׋}bz? ~q{UoglfSd9{OϿ?ٰ߹_w}_K"ʘ+yuA++az$ [±Aӫܔ;V;L!]KdPC^S5ng;,^a_j|Y~ďCJ +F&ݘ<Ӊk.TujADHw2;9\2^ӻP qyEZ`1/:V  jqe ss&"ul_DvJ3Ja=kz?ȾRT*UG~[*dk-ѪwcXm6>w2Eh9LZ#$Jߪ Rܚ>Cj5 NдK2SVp#q03 9n21Lڧ@٪QQHdHlRbc!3 :nJ:;QnPs?9`u#mV/:$ * IDAT)s$>)?nO7$s=s=v??eZcrOO?W3+sX3QlXFBY,>̊g2:-g A] 2l-vRNfZrBDEZMU)$AQJQR*a2cfq bqF׸xC4+ڄ+*3\% k!L*ar4+y4MZPdTZܫP:9yGcQ}Tc9NʙnKp[ЬJʺtTKO Z AޠAi2O]bI\cfwk{o1Yя~ď~s#Rb{C] M]: *p%%Z!U,R(҆bGY/qCN1Vqqi:`Zv!g#MhED>c(XhzHa/UBffqUٸWKGat3ĺ!Y\i&5*м IHm1dSx#8O4a:C:UGgf Ǽ{>faJ j@.cVkPa* LgoFT3W=R )GSx]bS8*4=i* >ej pp8e߻w)g<|rR(bRy'XJ-"͊C,CJjH6'arY6~Ļ!lp[>N{.yC&(%<%`l_k]3tf޸Ѷ1ڪbRE%kE1KΫC.C^ލ6ݝ@9RtyBTڜg\1S̕>mkE[\s,N8'~Jxbr'4۝Ӊt^"Z!zC"42P*Ra=&bK\鴪 j{57M.}Tb7s 'v" U/H3l=9Ѵ+;vcF#J|lh6ئjorP'h^ K2 W.uC4ؼns|tBg|4)IEwOX\\){c EnƠeFRQ)F]\0u h $>x΃䌽P6 M""|bX,=n}68E4rly6jaR!#hPq nIYpyK08p!q.Fq5lS}Cc°?ap4h]m2odW.e=Y]t'!iujr=h=0Fz W 0r\{W+@n+q(u"LPA(6S&-Up9]쓮kn_0Dv޽{緔ڼ0vBOѳf)F0χ!`t̻;fؼduc utJS'1x^Xoh:ـOG:pq[05D=nNxc^zFaN4ƗTzl-.c st^]~ͧAeDٱPv,]iu}UczeKᨔJ4l^51wBjU& 6G^+R|R~U iizCv\xSYLjex"'7(?Yd?#KĴ1(jD0.$&g )Oz3_i-rWF8G>vѫ*Nri!Kݽ9~S+ 36vfXvȞ}ƴy|k)C?/?({S~{= F DK۰_\{3kW?a.R k3I(ULȕ+!)+|~ oo'D0SfՈ=-h"FJS(XX6Se@ \T$jn!^O3>:/{K/n?䫋缸}]X[]7ʖJtglJvyK6ꗂ ,.dRG-cuGl޽y̗/>aӴHuD35LWꞴNەDN!U(?5/HMdO'y_IB|ȭi~L,lJW+TBn(6y"Q,Rb )]v Yw5rq5~0>eȸ㣷حE)E.#k+Ov(Y]oX,&~,~8]XhbDxgwOF@'57˗=_{XZ;Qi['#Jq^` In۴Nv\}$#|{o * y-乂(+&BmRcUƚbܢ[meQ72U Hd'PQQH ,Q(2Q+`e256QqvzcY]fFKv3mJ%\6 \9`NUf#wX2ʁ܅EaXj4#LBvK@郲]R9'Sdnэz)j ϶CIrnyTԨfډ0f6̦HZB쎈Q-rY69` *!"ncӀm;ɄM3{ K*jʩjBH54$RVh$AQKt5"eQhYLE2jZކ5f`J)vN892f]O[17#XwXCn5-ݧ# jY&<ҶE9Rf+vkZ,(VH6.+\HӎX]R#Sj6t zIkMQ*a+#%)QH%2Rr#o/FJP\9`dNxc=j+CFaƣ#8PӒ8,D7Ek5La\A3q 3"%sG(Jt6s.+&)*i6=w{M㐎tRFd5Zce DDFR-!5BiaM"TęIoH Psdp\WM)@ Y])0ƔtT3!X"ƕ|J9IBf@ID1RѠfА:T:Uu!*iPRR nnRA] 4KͯP^M3iPªQZ$nЖM ̐ȑ43Ym,>mr@/P[-4sIiTBi˔BdLAUD$EcL%CL5ArԦD*kF jĪsx U&9ΊRQ}| y%a8 {HlhPs, Mp5}P^%(ڑGTI^ jt6G00% 1H6)W&r&8̤1)]߬9l8lyd\G\'C֢D9Z:;S#;```4VCfaRR#p 9iW#J(CBмHQm\Ъħ$gS<\~Esd ts:{E)dl'-^+T4B(WC(]eT1Ie,4)BY6uطX)b)c}3*a#˰c:!u1g )zzL]I0K/O.؋.ؑo)v z朑uÎ|˰ahj,9jbGc MK)d 4Z"u*D"5uR42YGJZCM5Q)!iuC& 99Rbi{6Kͺb%&ڶ.eI4٬0y%qʾvɼcX=\%//K%G\bXX}ֹ߬e5xK}.&\NYOHΣ9sQ" YTlhB4Q si>FR_*5nFxɡƛJCFdOW/+_"Kesbq>`sQx ![@aN^dZ%k Wp-\3ZYm}/ C#.7=n^hCr4Ҁ<`. iرo908d >?cDQAY!AE J_DX4mAu6e'%ɢ"hH0I0k!ד]^^Ո ɫVm>cA%]VJ55)P`3ǔ]&"F;j@o6uo$`vajī!^hװYLx|wk>y-(=dv#UquJk#|R ՞@r 5 `#>rÞsn}ŮtAG_~k2t T#ZCe³Kx6xXJ,c4?#&͈OP`GL՘풀1Sik>#hγ\)@S !a#d)';9$DQ*@WS%ФJ ŝFJ67'|n)Wqbrr*""Tkֆ9f bvK>~iCNG 6g<؜+8"ݦlG/U:͘0p99B[WѵCx r?Cs6ˊ.+aMi=6uM"\⁹MSE2SfoDsC? Os2 tR .VG, tORed(r:̩ r4h>5wݣ%_xNm@bhĶƵ="e&Vo6J|N_C4S ޟrɄer>BD.0'<3^9c'#NG\hyV&huN#j@rgfkzIO9]@} zLnUY /A UHKJrOa\a-{"BPSҐc3 {\ሔBR sG^gdI<㝑~7m^@>-YZKv-?ffOf_bO\6`JPD&pZ䆎v0~wʠ7ۦ`w\φhوv+@~*IG >~3=s=7JeQ[ѕEHBS2$B@4 Vad)j^ b)!bjT<3(SeM])4BSਈTkH:HJA2M G&JAoGX욷<.yjnsS=D%(+Hkt'a%^ITLm Sb)LzY\ǒ. z*Fh0z*_v,xO}v/9\$8)Ufk#x%{G2S Ѱ,-TB5R U B)u#.b>"Y$T)GFºn䒆RNTEf*Н5qCW-VEZ295$4J,$"yN}"9a-xݠ1DyŤAZ<Ϗjgi;mK X=u{[GKtƜ/ E,5ʕ8g1An +Zjtǝ=MeOSS(P{jVP 3s.%/^֯ELIj&Ϣ,hZlb8C 98onO܌aɎ[)#C#G(zck dwbk?_QqqV)YP?rcʼnJa*4ImJC=?!Jjy|3PEQMcx2>w:';cΘ9$ٚ$;E3LIIXqf^s^c)17bLxgkZ/E'Yi;(B 41=1Q#E5,4uLdn;!*gќlQP] ҆3qgnxٺD )bȖ&PD+t9';>+)TʱrR+lq)7qs8TH\B<|h[VuUM^kYI65J=K" ]+Q̒(`ԘH wFh508tGwpaWJA<[ &@Y!dB%^u`T0c旮=t ȤBP':亊g8JE:9y+BYUĩEp=>DD$Ԩ*խ 5A}=@3S"]d-oL?GRvVTB({g=Z4D6 1O2ƭsr#9cĜ"]z9 miL)zQgPkqZ;}fÄ́) &. Iv`]hyK\$&`[9WDR@"SA遥=gyc9RY=b^#@#G9]yh$ԩ K5TAs԰@*9iUJHbՔؓ*6@SZXp&.y">` !#uv6>ۺjp9CRN-gSq&4N=(ʭ sA5Sg ڤy>U@ UZP}οea?s'%߁"ب~mʃJ9ר.5*-&g=eWrݛpMO\"jT HP9JF%L#W,(.Eo_p\ 6Gx}꼁a(tZ1gڜW;mb#le: I'cN9ji3 Ȳ.bdPۂs"-Rm2BKJCngO}:6='.l X^ũdΗG.A5/\H, Y0ߜ<Ͼ|[>(O|z$XnM9f? 7{=<ddNxpO{SAT) ^0> G-t5=sEX3Wf?"vuw32*YKhIL讠wZCuַ48 Rv#=/ZG,4~ q^>:T4Uux_>e3݌_Xm&ۦǾrhڄEN*Ծ N,fSE  @CqH,Q qhsv2mx!D] P(ȺB+ Ԣ@- HJJ+־J. EWz5(1RQDT\PfÐC׷8ZH[bb')v6|}D-ʭjB3Mp.<~:cff1/8Kt=D3 DK Jh+|;JGp7PT\2g&Nl6%=kі2m'$) .^q=?'X4(cI4،[;C-DKQ}sk+h$ƭsŸ/B*,>K}Z% !P킶ҒՖvڄج˥KS6z o$ySw%+(6T|s}8f{# B1.41'Į3(.RWPZ%U![16ۨ.jhjNK+zڊg4Xp^iSwψ;&I$ϩq BZ'zcv&PM;P+$];b!2t;C(y Pɣ eN F7:<jririQ=ekdady1B$myWkQAFJj,->%KТ`%3(c}P{Tͷ2(eX`Y1[,HCpjɣWKK\'&FaIȩBP1U&978w߹޻ Λly쟻zn:yRXX1jTcsHޣɂ2[P7+_VYXNiǙH2"H2+'|" KP|,-atY'N.h9;\'b-i4}G|ctئvI^u^gͰbtb^Ӷwj; a9߯bb7 % #JD*v걜y; 5ԨBKW8ZĤ)@o{&N[ _vEY1bC2MBX5B&{IZ#KS^?Ja5cu&]G'w4;_UYnuةM"fbf" Z$TRR)JJ 5¾̾¾bbܳYٮ:Y_VD$N0H:Ə g=kvyct5Y!pI8chF[Yd}.W\=c>&ӊJԶpw3gaoMI5un@pB"kb:1t&.Ep@m(qIt[bR`n,%B/Cf-0A8Q~ţ\cQe8bU٨liӦhk+:#2~tǼ ^9vmvEt[`-@rCAǪa!5Ή턜Vjhf:r?2{L(j\XԽed6[ltl6x?Ub "dMjwv&PGCnܟv{̵G';v (Lќ h9+* @nfb P<ϔ+Q 2)ιdmEgl%I=3Anwޏz$&g uƶTޥb O So 7mV}ݡnrOkMAъNk/|,>%_WҹpȰhPcRP!H0 pYV}VyegjcMvU}9I@'GSyV!R8<-O?ZbS>P fIH7L5+HU}_>g͐CkLQ֌c;d.{A,#ֻ!QK-o/(*Ӽq(8!?HnuU fOk&<<~u`:갢5."b3B*ir/#sLM,|q%g-rŰX1WYl552>}wSWO8FF(GehNJG3>3^_dTMc|+~z_{o_`Ȍxd첖]VzD \¯k4:ZVw)ZgG By`c/Gf,-ۄKN)ѝݎIm=\# 7]AԾO-HG< /* )yRCJѣ~Q8+P6%ES!kh$}1%>{>,Q%UP69V ~cO%wM^m,+h"Ő)w3mJcԼ>#mcgDI4 mk7Dz‡Q=wǠDEoy{'*~*Uh:$^ŝW0 ."N='='=F345Gr0GT{@%!YZ,!ҮN:>xxK~/] aX=&eQK"Ǯ#*EJ(ҐJш2ZC'iԴ97x콚}b P 4U{f:?a3Jx>pW 1:'7 % s]\\6zX2E;R8c`cQrL3W(v1\JnjؗPghiqmn3 \uNq?Q [ VK `Dmn3S #Qz;C<|%΁缾⼼$F0̵!71Sf)6 [dPdIEjsSMt^ p E10%~AZd%:-c9b". L3mw7 ĆaRZITɜ"t K9? B)xLo'ܿ9evdDL;:ac_X{`mc}:Uyz2>'nb;mLmڞ^{%گI.#ak,;8͐iy}9a_ȼB-+$ZݟsvKFNF&T E!z"s_%2Ļ=Oĭw_w +GޞSӳk&Oo9r7Oت- VIW %%^ 2@tAiCu=Yla9x#B΍kY7\|Oyo>l)v^`;-vUCe=͈æI^kTBRKA%$mV<]hp(3eEdQ6ٕM -t7 *JAF@=|yo_ "!l"fO-4ѢB2bƈCXek2>6o 9*kZ(@Crs;?f}w9|YCQCӚ/ķW<=b7OAy6zv|(R@yFH"F#Mn=1JQD1nNh}3~_/e*dY9n1naX:ia"tJKUg$?_|_~uɋ;ps,{(eQ,KĪ&k{접gQU()s9~v,荷<_bC2p^tq+ID1IU@Q?l_y}>'tJT 4 BR #Nxw|1]|y뚿o_ Ocܤs޼Uw|>l cXv.'|L Xd61"j5d7s@41ӂa"Kus뚁sn)J cuV@[lq2#75Rg(ےÇҩS+p6ף?P1cP3H7.N9GW3=r5O71~hquS望?+-j%;^>Oyǧ̗#R#՛ Tf34PG 2W9%cY%( HdSF^*p:& @"sf]?`Ë; %x4>elRUBSN#dE5 k`Ll.J+SbR_tXs/okږmDo{x,?[no8DsUn\ԢM͍M4`QfYC9eHIUH#jX bڳH&EC TTBIQ(f Q$%$BP\z+J jQ 1ycPi{z+h؋56P`(@WJl5f*"Ԡ E)AS2p㰻j:5qҝq}$1f g3 QS[P{:fDɝ7a6mpn'W$IhZE85I}e$PjҒ( iUX] cHU}DeX2kMKOF50rU)&v9 )qfԏ!l-fr}=<:?2}ndF=+.U`BS D)QҭZEf< l0rP5bO-Mqh[,`ѕ8g$GhITJSuY+FdA(aM3=pZށPePTP-F*s҆MZ0PZ+ʩw>ڲ&G$$&t CsZSn &zXV!iҬYkcwXgfXZ}RN(GbV8S =~۠h;;0"T\0)(Ғ*H A0 nlt]VҧaN HVdE犎ܢ+.~GMÏ<@jK01f/lE`rM'6HA(*T+ EE}UR(̂l#W Ē w82=xx?7 _ UeK/L)"7Ըٟs{[ [ʶa_,Zg o/"V[hMXVHK1}jqiElFRUx*D$ I]eU\A8fuL3?ħ]k-rG AQk.ǭ &6`dlx.?|ǺTP6IbUHRQ1j!yb' SxOal-XK״-=DRMQ$\Y\ݞS*o(^AH#{7%D{^QLQ1} )V:+0}bmkO:oi=]6ۺŶnSIBn$!]Մ'?v<)PNޣ9,گk DDb=v_(+Y#ЀõZ{ؤ̖ `}*FFSzuuѺ 'h ^~G6n]>"^  lW)u.e]g҂ł2K:VDy(&밪,mjB tr>ko}1QnpRm)Xыi[t'Sm`fvIxkr_0\٠}j:+kcai1:,1V%SBRN$,dԾ}QNUqUZjcv19=G<" ,NkFTzM*W  v5캇$IZDM~"areq~y=44PdG'ıד|52d6@|ofy 9pn9rXRi7~٣J U!q vUsr??nrowY-Go'7TzB&o:.~#$:`1ޖ3a0~ƈs ,VQY%I i"uR󌶟RuVN 7L~,COd*KtȽ'?W|u ׍hS)Ќ(`&ƨZڄ IW hʩzzt$Ft_ _b4.A*%/'}W1iV[q,m0V/@Mx 22izLsrs"R/ /_Ȩ[ר[+qlZJM7:m-Sxg6Nb'fa(r(uN(Řv}+AHZ#5jZ [9tRlU Mtc_l)jV(j(yYA֨ZlQ[k[F$KF%CqGxmA@騤m 3FRl֢ATԝT@&s@K +Ʊ2O#g}c\7w3KCS:9V7͜Qsș@澉`FS"CURt;Ŵ":ĩ:+f9[DvaNOQ`Z Tԕ!^:N>^:d&^b߰QjRY*:1"7JAH+]k)j3'WT6OrP* #f4Ь(f" #G ,݉1ԘI=mLl5*B I=M5ÍcE|ȗяûOߢj9U="dtww 'v VIj+lXI*m:2% ez`L0HfJvHqi,, IdJCefՋL?}MS]O)4JXw%ء55JgZ4!vcrvLW-@JK"B+S*GKeajP*(-L3F6JʥL\N8i] E9RQ9S[ nk[K)v*Jc[xwҕNϝo+j,ۨAՐ6 }M-iKf8; H6gcd9Wۧ$fbe)=pܾ8(Uj@D6rT?EtOESV.ϛMkh||<3I[c>8g<8Ď$g)Nݒ+mfk:TD<i+!VGeē6dqhoffIhI̖=fw=fw]QND|7O\D%pBH\q{WD.镅6,.yu<>O1O1l# aT L=%HےFoػwa_^?؞lmL|p琨.(.N+[Oys5*;56Rd?G)S"?:ǫW%~\|sGְ5  g|\s5?gQ-OY=uҮXJ2dsknq_mWmtc1OxMzx7!O wޯxY[L;&o]o\]VIeb4 pnTKPPKy2I&=o}~~44F[;;ʌĺ&-l<,Ƽqjj|طi[{|ϗ#%,Qj  zsR]sZ]sR_SS~ŷMbOᒿuzB,.JPPՂBTDTiچ/;|1_Y (u@G`0L9\;.|@mF-J!G8ʆ~y}o]5o矓>L'#gH-͠` R1Co,:0?52HWuj%[Wƻo< ^%Xxbpyߔ@VNl>t·?a9/?m01i6oZlb?$2ݫ+V+][)&ba 5en-_"I"K?|UBġe? Dtg;[ƚkroo ԈFJ*6eUNQԔbGOIޡ3l¸95\ji *e*Lzx^lZn1 N X>gTQ(C߂'d TE'&!@IJ+gfKAQi%HRE5uIUҲZd_BU7U> ^x^x?_scʴ"|+) c(X=_.4mנ#~B YEWm $7b%1(ĪF;ɰN"چBn?WuܬX-MP@a vn7XJ\wɐxC2$54Bϡ̹RN(=bD%1n;vZYMrvllbŕT]w6jBja^p*P+BlRO  AEtgS%D! ;;;RA~z,퇗 x_*AvArX`5"aua"yiYS j3:Ω:.Ph{%:[KgLax1AV(VˑF%Y 4(v LPeEBHԶR$jkop2<-S-Sҵjtm[ acPYt~bm)^n?QK]0kWHzIR K y#Ҩ4v[ ,w]n\+$>lOހ:1Q3c玺%".0NOe~ƃ3 eXDm\[Gk2YǘdɆDFsEwkjD3LY}FЉIc~R12,-vCH_gIrLTHq0JH:AP e%!2(Zbk!~gMGt5k67MXYF`^{^!oJۉ٥.mSFQ2cNh` _5T%GUry"" oU-mc,;̃.h,RrkPG5Qaґ挔 C㎑L؂  Yfo7>p7d [AZlJٖ # Ǎ/nZH#5]F@rp8l嘭 _P SP~Z5"4t4BG{1E#X,V&k &76atYQe<hpDjMʒkM3N ń~>0@ޢMJ63^/Zؗ1wUKろ8F3 ysyO#'B&SS#ruRY!$*F2Zƌ#cU&W-֌9auS^+uM64ٹҹD[){-2O}o=޶I2T"e3g7|5z嘫c*9(Ube mg]T$~4P.y%2k5y~dh|b})d$TٟO _x^x/SN!a>Tg\eS7+~zo+~_*~b^S2NNjRA 6(Qnk4-4Nt;w}}{ۚM `C`YF{DK[DNBM3po}>?|'AM?jAHTD+̺j0r>g2Ǜ-C5 "5*6%gZ :|ϛy^pgŧۗ"&+ꏜri}ſSs6e)JHl>QdțTırǙq_;‰t?Lt=3w}WNY"M D#HRBPTU(Lʮ>Doy-C#tD*o On <ⲓ]avOݞp;?%lni4V ?]OaG?t˜,Jr%?!pq/7L=!j5uCPȔ'Չ՟>\+~Ƅ _'|{c2-,ȷ&O˽ /$\gITzP[Z3C rB?AGikHuKnԩ(;Tӊf2鲝Z(\XCMÄ:,I P&2ISED-! *[-K&A岫\v 1v8;*AUЬ/YCD@"iD!Sks6g<,,.]K/3UWd:&]=?g?J_+p^í=Osi6dqW ıEZD2m(8VނjV 62uABˊ6*% cn4IRB*Dھ:B+TtQ4TGS*$D]VXs>R2ow{(*.Z6Ź+U3b*bifR2+ŵqWl(j[Uu#}&~?>r={Ĵ;SwdT&E k&Ť#* MrU(^^%ᜳ(*ju(I2YfZ<\mW몉 pP4\'Z[3m@-J""aI6BlNuZGwĥ K(f2׏xܸ % =mSwH|"Xg4 {O)*lЧ2eT@B ;m)OI-ǘfL)qmA&tM~Q||\rcY`9Ư7liH斥d6Y|Z&k% UV!\a ,fsP̂eϧ8o&PyhɷoOYOm2\:n0QAшI^<”yZǯR* ZЉd`XmflФ: yp{i1 _k\9" f(0 AJRIYdEX؍5`k,`C0}?aknU) 4Xf%@ T TZ3 ɮ.esO]M}h_ /¿_slwPݘ`){'72.ҰD)RXMLa&}TY='VʙD,QQ(.*# k ﳱ;]nxr,:kS2&Sh .;<F]@Ƚg|z̻m6 :lJڏ_K5͜6mn͘1Oۅt9ËGO {Rn#CUՇN=pCoP% J>)Eai&2 XzH{fM7TYgĉn'8YWyG#.lwP\ VCV4y#8OdD kFQCg[|O0p]+[J2Ӹy:`9nYZ)kH)dSxjmdD dDRı ?mV^صG-p j]C "`<oA)i`2 'Fڏ4/a<9&LE$-ng.ۅv S >g΃92 g,tiwȢ`uۄMtqLgfz;Ķf7'ɐ AbmPgC'ĦIG8lm*Mml;|lٖM'iKV>Rn>StlP)SvEQjDJZAyCJ<Ts$u㘵7T%D%SNe{n_ / \~N؇hg+Z3LkkAz)~P@]CB"Dh U&X|6J"%P5 V!uQ+Dc{! >AQb7z{>}B%4rIvwU,B]ʫ;>o~kox+ސ '1a=+,߽ #+zz %iQY"2"C Žx,q;34gv؄~9d KKsX=c֫.Հ!O-{viv\9g,o{LУ ;'+^_T%v^%N0}?r/[1OO?H{3>e/X>Ggo-_^8?hKVW{M!*LQ*McMSuGooWyw*R>u(.eB`w71&}{b}CXgAE;X[mtkޚ%bV7YcN=fC? &6[l$?M"'o(lnkE<ȿuQ-K=g,6ǚ2nl0p-4%BD`Kȶz*~+p^4݌؝D_6ߞtg515A#eo#|QRR9&m:k$>7{'}ˢ4וƝ [wZw CÊ19959w[mHZ4yI\ddOrȄv[(5y2tX[M99/ `CSoMS!?u@kX~A.l#VG#RЍ Q4ɜ_8m}[q88dC여dWZ;f;\{C|@o?g^3q"&΃] &K2tmTᱴNȭ{ԟoToo[M+BÃ|Gyb`43Z+g+ZK'3 MGi1qRQ).A%I@$6T6´0YW>G-H^PY"F4H N8Pq`3c7CV]-n;,קxN8P(-(4뼋D%V IÆۤ6k͚l$ 'Et[P(5EE6klPQՈ~8V kS؎D4z߱8Ch'cE]YE8+?B]̣l.;1 Wd~LF{VWP1RU$sIEEdAD7RTA r &p ̇T`FK,RZ/ђW;) TKtbr<# BZnFٱ1WTp IDATiӄNES Tl$a&fL#1G;st0G7TnLEEIII!y xyGy/u/_1\˱{9S`U|YR'\-Z$Tct1^_tAAlm#d lJn$Hᵅac 1n<1[ȝ.di"*,ֻzevar#p-N1'05jCz{¼^j¢!' 2ژQ,v\y0IL#zK(x*)꒥h{,eݣVruʇ٪AyUSXtcj7H(mځ}đFЈUUIԣ^s+ʱOPiÌVk ON>İǔ0d4>?'(UӡlM [WXb")pZA6@t;K1Orkڬiʺ7f!uh C*WmT*A7gPaSƠ"|eS[X'qIq[zbhOKVtv 撮g.q9yȮkurhr#ƗG: u|v$ʚk^5Mv45M#څ)(]:.sz첀tN}{lS.\Ga^)4$>SHH HrHr,n8fg0?3_CюnX0'&$PV]iGlj`rr(~{1`*Zcy;Y vaH6MҮ&9פ_i*$&Cq(و#VsZ ́sq\cRKx yE4LC6OnRpӪQY1Q0LWk۱fܢ8̇Syg/9k]q^#S#똸 bȎ 6}#<#%ۯO))]3\%"d37oj&N\MP'abxX܅I2j=XIX %\1>~Iɳ% gX>s&caS!k"T_H{m,%ފN#\YaY;Hӂvr<4jVq\c[{+LcGmXoڔ¤왔*䩁8NUW:O<׷ZT`ydQTW%U&Yi: s0FT߻|O_`$o9o]p0 "Wo3饦\LQNy"0^jEJkؿ߸q\RRĕfzc>>8%ma*F#ov:.Ճz/&<3/91n(EXT.fW8hzsbwn5kD̿$f8Xf]>f&2Bvf;=hÄn{ɑ7 ~5qEEg Xw>¢-jdž tSd! W=",pH}5-F{֔à _M;5d5 ϲ{əuŹ}I܅##tx@l6}s&$d⣗ZDyɒٜ1tS+NeuoSXV-qVT .sI5>T,6Z*Ѝ=PdIbU}_ UܫmM 6 ~=77r2syExBVXT$(G"Bx>[AdkP;d5-sQs̫rBsɔ˦9 Xg!,`r6(h@~>G³2~'#'o*LM=4~s҈7aVDpZMxs Y< xx˨;_odMlsNMxab )_xo+~ڮ5˚Y>sV0gKOw<#<߂_󃄣[TxңM hHfMs28^TyA2Z ӂlc i -Hd5ܗRa6jPa+󊮛b:v T]<adI)%ؿN;'''^^WxsEIqg1U6$ `JH TVSP&pa2$xDumbwYlDq,a'g)FWel]fGO*<jDXJ2V^A=RfWQN2C$#Kٔ4\E|wɎFTlr$YZw]~ЅJlBmhhhhQ19nCj.졌.pֻ&x ܪyR1݊paMK}"d lC}L#j$"L]!`5J:%@Rz&gQzX3ܟ4RHdS-6iM&OltM tCP63E(G`6K,`b锎N vJ3{jEE|(z}pZ!%@Ý [BZ 4z`.!j aB&K6 Ɇ&ÚeR͈ііᖐ\˴ͪ#瞡sйcMsTsFLxHgNpψ{Fj []0 is^4*l2JLYQEBԸv[m6c[.qGV``a;siۗnQDv(Z.E!q!Ӄ!:4ĢKĢ,il 464.h,)R߫;>Ic/_[(6/yr`@ġBk8Q#CݐyH3 MRe(jd=aMФv$S9-x͑Ftg8UtlkkgS `tGdfDm6q]҄PW6Ân#դC56Xf}nSuҵpƊf ,%|>)sCzdRKa3r?~zȤ{d[3:oilW[xm26;a 6Pͨ}:5)5~r(Km*eWG*qG.nDƄ@djJ:7MqG; PMa`cFqV"~%0CaO:ImZd LȥEKl"6deQ~:l50&ZEt1O' VRw.9hIQK첑-n ơX bĒ8dIJbx zvid0 wȷt9j.ԥz}^0ԕAUTI B:5SccW#<#H~F)RJP-m6>L _^q}mt߷)IYE͛dK5`S6v,cCW8s֜VߞEx9:#-Z#U+DKeo*@}0~7{~ZdP^kxd6qS ?f)Ƈ;_ꈣ-M+bLD+Ɂ3UGh(#";ܰ}+zz{GdfN-׈BTNFJ )A3AXI\8¥n; Rr+N7/ikΜPxu Lje?^gN='W3ɻRd+:@M$Y8]QU!۸vf3k-aG(wri| [=m8zXIM#d\s5__${hD6@$oMT]h`2hy|\\r!?L:}$Sv7!ۀCH|QKZiN Oܳ0"s*#]!`R;22{LAL L~!_m8-4.ڐl7rP^۔:EۡsQB{42H0op7#6x 5PS|2;Sk] LԤLނa {cPy=#<~oʐDa>nB/$!ېùw _oHנl'PɉɁJ9$OX(h Ӟc{=VƏ寘Nz~w?Y?3歛a9nmmZ,&s}nCl#JeBVYEYV0#, Wb%a+a`l9 B!fwlز^2:`8`sӆK1-ƒ%@5$P Fzح sc53)Zmx*/p]dGUd7kλi{tMw CtȄ+J# -+ dhQ;mm[D&ItfT4-UE{,Gqѯ ei=g%)fLj%0#hǜ}W]cXbzHOy 1"PHW! 2P~D_rdyZ_,ޏy_+UA#UB0GK  Lw gug6P5 1ЈCl3"p`m/{h RbDCt*Dd@n^b !4RLQb`rjP6֥\bly^8685: t&PS`KVBC-dÂ60bx4Qژ'Pop̈́cz8{Gy/ ;"Z5V!ѪA 'EPhW̽!g-f6Q@i! F9`NfܹG5(.VW\~>ߤ0 JSQ  5zk6!z+Q[]&!ߤēmK ,5ιݙ5]?M nlv.Ҭ 9 !x[TDcjf{ˉ+*w- +yE4/Zs R<13N *ة TH9Wꘉ GSN_ÙsVy\L\ml,`>ӾWPɠA:hu|jjVI~Pt-QcI@o%Ä`$tԚvȕ<1хɶlQ6ԊFkakN6m=D TS}XD'SrIFNLF W\ȌX-{Թ&l ZܟV>e4Փ:"qڝaIN:-L;2.Mӯ0(!ybosgcl]Mf~٦MNXCʡWԵdt2K̢{&n6~[?C& +d7X?_SyG t95%&;gL2t1~̟SDA6gl' v"xBhk5vvaqCLlnZ̧C"ՠn`+5(q"!RAn9Vw}6Eš:鰭[YUDLiL 74ί3OIhC.ғvaWNMJy@T :AI}&C6W|NNx?axT*1kba:)yww•4P7$I}7}M3 |Rxg^rkcožR\RB` ukRԷ*2RVw)6aSJ̥.v+'%^&N K껂tc9H" _' _o鿊-Uƴ,_ ?y.6~[Y] )f!^AŊ5kEd=]ĕ A"yW{WPXX_X,EXzV6T6EP)/OC;Qz& >&X]~J^K cXPq>dnpqC&*8,zѮX}Aegit\l=6W.[jgRE6U;0z]|nZ+DŃlME/>i4O7L883:CC| v%Dw $z Hlp:k.h{-^Uzeuvrl7kNU!0bCŖnsH03L/<(Jgx}veH|+HƂx,v><-h4JvY]`l<̓%]=c{i.a@90 tƄ=;<3akٚ&3mPȅF,4`f YVeIZ33 IDATsqs;QZKZ&|IN0ٌ"^ZXVW=6is= /m4\)X(ҥKj'ۺijX,yy ih`, tFKtI]@\+?pfHVos* fx_1,O՝tCQugXRMk:ҚI=6\_\%(`F79GX ul'[:,ϨwYb*ZD-l;9&R)z ,L 9UI۬. A`z`kN {obma֢X坤jq9%ܖ7%֓9nN;s#<#SN b1~jsnFg}  bE2@D =P U(]1d3. ߱4ˢ*K:ACa-&~_&~kQMN')MfPMtmjWKBsOkZ٘^Mc32?85eRˊjX+Hkߊ.y^-CMv>`ja4_W<~%?;lqDBR {I"ǒKYĬԣrL:cXxQ]0*х@ 4le/(--,=Ȣ7?.oqrpq7h ϟ~ወ[y̭<86:fңtYkXvND,c_XEEL<]q9^9!ۼm~Wf{g_ [-_˟xy5!9S_W^ !}'>D^(+{)!CM [[Zi͢6pr̓Ƃ;:FuDTf:2ydc=kh6Vj~r;>П2l,{O|8n.;5=dJ=3?&|}7-pau@]D\;ML#~̾_# O<~%=Z'@ Ǘj;XPdkM1`T<4sV&ʖȩªL}h5GF !(aS 5LIZyGSCPN5ґ60=1&m{T;#hyu FP#h4HZtGa4L%85pO{cA+F{z6 AELƳNSxyoؿɂ1>Xz,)x &ƤޘEnqs|Judj[^!f@t; XS. iM5fiYcebP&ڗҖOy8kid ݌E7eXcU@\e*}8x3WEMZX/ MW)$Ů*®+6 A) ~L{tOOxVLZ|X!;a -Lt ,t ځN5)-*An7G]c+A6sxLåtL~Y*E_4֌ӈKsy{GAZMєQ9`8%ZdMبBa6{|U1vKbg&hMKS蔮Efāvg_6>U%6kIN8͘ѫ9(j(/BFEKӆZԞNgXX@"t(**rbZ-K'0Lz5RSM+$'aLؖCA$0Y7_FAuL$ Owg!d&SfڂL;1h/p)tȏՄ:ӗ } yq₻I˝IɎ+qRa&I#\)W!c) gup  ƪ 0T\s*S'1}b'1}rSR-qsb&}%6 >I76hb-wۀmNv?eaFLnY+DV109jyrEr͋Y`0`6rȮ}0۟E;J7!&t)45@;jPGB.Ed:#Ox{=K(szPPi1i3e`lɌ뿢:wgsO;1Wb_o.[zZy` ({-'.'Dw#Sw[1UiRUnS%.]cкݼ>]sufc[㣄qOEBO$"A$1 >Y}y>Ӯ$ZЭ$$q;ojӆ:Lw.Q z{=_B]IH|'̹d0Jh7.חC~9f`-918i-4-yz ~7Q>tŴ8#潈0N嘻ERjɖ4fcDNAO,;n}G,yg~=ܤ>7^/wTXʤ¤R&u:;>cIHg;TX&X|f-uMJIʬ:C5tsAX^N9C[42arA!?\TG+}0l$:sGFAE`{)1)}\Ý9_xXZ\KfڒҩM2pih>4h bhej;owAAbO 61 '!ʠ>N{ל0iK#iq]aW%.h,hL M!Fm{K)q)J"w)hI#4hrBr͑]Mt6iS.ů2,G BSh^zڎ_x\kX|OS{ASԮ:X%= R5H|4cpMaYFΚc[#6%>Wy9`5F B4EF4Qm#p KZc b,gۥ] -fgysjzkC:-y1y__'̰agl퐭(F.43̾/Oʿ}guMx_"/6=1e_xyTVt4>wZq}_fr ~~>,ig89Nc9zԋwx |TŮH[.[1_LOXaA]TE)#)8TX3 i{β{r2k90&\gn<2.6 b!kVIJ Y! {ZM;԰Sy@0dAYhlW?_|_qw|yr%cRm :|81=O'>-8 z1 qˌNkd.q81nd@7({m@,nDHs#"tѰ?gygW+%|68jŐQ`9Ҡ:]f*b:`*"e&eĘ*"~ס[Ќ*T@ #op(], ámFaj(A@r=p-0@Nuj;Pvh,6*jQRP6RХĔ(AЧE1"e–cDL==J,RBi::CI>ڣZTIkV `C]Xu]NT(jC6 34h1hHZ`xIa?0[FACS6tYN)9w JIaܦB߁hrAQە˦)6u9ozf?H||K%5m^74Jq3,YjA[&W.rд^:{N;f? B  f,N!:hШA% jaɀVӰ&\r"LrL! vO}p( r L8R*dQLaFvSأI@-6A ,Z6f!'ܐ|hEy_[#2 +rMɠEn0V&į$kI$͉ؗmfKN/=yaD~p4E"N xK""QR"z !h 꽅zEǣ6/pVgɕUFDiF4ى1Sާp.XsvBBdSP'ek%Ұr #Sfb#:!p 8!\XUc X50ks3CmSn RY^谨BLiF"|2W|X.`ٜ3"O2^Y(OP&ePYubt :jۡh\' D^)Zљ2LFԪ ĞN:ygw!^XTɾ(A"lggyg~T)`rK9`,P MH=6K^K0k?]~Dӳe-[UCnde0o݄ҳ^ 86ƀ !exz%Vɔ|81ٖFIThT%"/0')^}>괆N;IߛLbL8D)naIp(TH 6:5u0p &ţ``Q`S`IG KV0J;|jj?{7[\a)Fƚssyu*tB z"foHR}H՘TR&Q`ZG{^13O+ ڙ;ulf :Tȅ^챵?ql]<.CY?sL^Y= ]2ϡG|>:RWޢy(( r;7C6rF YY!=/uyd>du<᾿E5i}? ,a^+_(6׏#\=P2@>gm]X;G` ?D!>N78ۇ)ۚō6@ ' 4Ai *Y>zrDv? #n9\\#+%^R?Qr#K-;ǀݣGP?Ԟ 3u![|қĘYFӉ>=4a) L'lԗ#vNMq?[:Q Ggl#zFJI.\pv+h[a)RZ4y*75Z1a"fjDL備`.[xbG'NX3'i4vi8o"}upS6e#6EhZ!)Ca#?D2$b~?~,ToJډI)_]ڽkGZ PAun9c7sJɘd:"bl} H,r"&ӣ76DGOcG,g/}Vҵm+0?m|L3=zX}}5'7I,ZVHqSu X &,ńRZz_p2%q,~ZΑWs}ή~ɫcg?K϶n g;>d^Y&l#NQ+rް\ϏիKN':Q>E4n@ Xyu̺{-a^oMLg{~3?4v(JtG:ԁA;K -G5\%!xF_.9;}ûw6Uv<>j0OKO T)V6y5tq{WF8p~'p'[I®!4!U{ۣz|՜o6;=*DT-OeZYXE=8Yx%QEm9QݔrOwXH10F1FHê1ݪfGbۤM2an{iry_" Ij{H,Pce1m|@KST8`mgG'vqٕO>uEM{[P-RDY=4{gy晿^wh!10w ^G)4GIty߮L8 m\cL<Ңt+jUb^me72187o0;!Cm%v}N3y6xatHmD *nY9]";Y1W{jàICWh<хGqWgOC1 IDATBOؔ誥g#GQN T@vEsoT%+LK,XA&"Fe2X!%\E.J }2`a@bGOqwbJZy9DSa)Q%.dc:{|w%! zEIOČݨY7#Ol"]4w! ۠l0mdL3RޖitMvZH%$Zb%Hk47ɓZ8~/z5mi>"[ 2,𤷸X<%{#1 F+[u;HI9m,ñSGNE!ygJ'TXG$`2.؛gK Chf;R#ڝA{cj5f JrATˆtYLQ0-t0-l#̷ՖK.pdm~ۘZnL7hh sJ˟z4`tc,O\# @5}z.+L0+L"\R+r~*QyC75o>Gfhri Jj] @&}3<3~`JFgkFg+gk|s0i5Pz30&5ҠZ jal7=4ZӮYOݎ-^4hnC,ߺ3Lffm9hNxZyXB6ݐB7a LΞO`b2cQX3vջJ2諜nOwGg>x[y*d{hQFVz佒ݺ$А=ҠO4I(4/'b>x|tOkC!툶LRI7/^)q#]l1hDxW 3(0(Q 1BS~4P(y0>4=|>VJlNӔlh"|Ĩa;GQZ䬳 :4'xesr|p(!9~D+{6Upe=ngyg~60sP1zgy{#n?_Y=zU*!]yKԨ6dz;[v[НutPT Vc`L~crCa+FָKچ)w7<=S*ۄB]@o1.y1 H}Z#:d}詜n=m~FF{EzR^+ ;)âB(Ÿ0ju J:I̸/~_M([ =%_-M ~X~byZHNBOOy{bJZGиVȏ0\q0N7Ow}VNKFjP4$$;~ߍ^bˈSؤ7 㮡mGuCGk@O/؋>Kv'!y@tkJ:T1 961t'X}AWHZڝNhJJ$%U\cz ao ǿ]LW>WR}J&lrirn~*d f%YAP8EM8vITG;E5&( @WwP[L2z^`jzQޠܹ[̓KhIjTjC^~bLk!I=&&̓I`" IQF:f+ _O|+YXkIFL&8rPɀ17R-ę8+.)Jl.<mlb0q1oߣ_$)TdFRTCsjA"QJza=_;嫣o=5~l6;OVr[zg 0 􉂀Ql-76{q]u`$ ִ 2;A3d¡%)&&&迖/OxPNvSƂ&)SWwl>jQ,YAZ/!30:mPj@1~37[&_If(*>7|A߃#2c}gyg~bF1 _]?2-h^Y ?|T-Ƹr[O:*ڱA9u`Q)ZwBĊQĠb$ -='BL@B*҅?y/]&ťŦ1Ilz&%mƨ]c9AX\whg5xCxa -\D@ ZP''XȠ3)\0p=OkYS8~Q<tOn+:z%"c",ZƢ4]"3:8e]5ƤT tJ -_98jm`EDఛfHѫw]DOEaK+d- ɿsc\!A;V芃^!5:f^iiBiJ %^*柵e'cl);&V#c:{Hӽ\dϹygy~lr?o'z)Z7{K$Qݒjc} nyrr^R]M纸g qD0Z ͼ6H]/4OLOF.0LauWZ 7}Q=6bi;L!7&sE>Ԙo;w{I t{k C92Cjj$9& oƇPlBi}OD t8=ԵIaSJ jmFk򱉈W}acШFdr} HȔЫ\IDoq5^gȾg/>YHCgs5R)L$.DF3̬V?)ѿ;y%䕱@6es>۰%1Iϡt4֝ Bh!#"c;S=WHr\0X,x1bơ}A82Ymym 1Hh۔EjEt7 [v/:ZR>P}:zU#6xxb3o?_ul;Mܥ4 Q®Yا|oZz[kOZ}zȻp 7})-Vt>[vhZfa}=R*AV*js~v󳏜q}w/mhAs=`53NyHyLF.ŴBM iJ$ȗ5⥱OoXd]EIºLA&N$8N=,r,r$wp}+:{ڑy ?3Iwu/TroȂ!-o=/{jLSkX8-C9eSg;lA& 5`r˧c^<ce)fy7<5$x6Ii=-Nщh8frN%3)t9T#0L0Cmezc$Ķ*0FEO>i8; ^?#{,:vjz$OƏBc`P`!Qxn;Jpw ,lDTvu:1)|YTܠlre퍟7~E6ى&;[:+'[4rtcn3"8/C;RĖH%m03%sdfTFNW z0N]`:$3*ORHUZo3&c 2IB`VQ3PDuP5~ ڼ=eW4;#m1$9[6 rK,rڎ^7ssÈ7x=y XNzxiКqѿYC:$~S0ycބϦu,iW-/ny􎳳E"4mg\Sn1Q1:"JC0q3TCe)ҫD&0[&UzC< X|ø0] C {|v]{D-hyGA6~7?1?)7G\3^@ew-uE MQHՒ&r2Z1;e|d{SIȑs.юj[ilDHDz\ddM&gR{g{prx+fo#fDqSQIߤ TF)ޭ[bGSpIUR7=^%_r^}I9g|c[<;X7:|4.*A$`py':WW9W9i23rPPĵ#ʆɪ7C K`ž.v^jAz[~^~O׏ +C}CL\8g6oY@\b/20ft4yt55RWWDim{6n{6˰t3nSs3Q:eݮMU(!aCkvɺ!vmfz}XgHqȹ}Ë)-Q bG1ތ+~&*N Fy-o8 eq8϶߽_'ql&u,bۡZ#Ɗks_nkQEIyA%7QZB2' &ē)ˍBd5Cn{?pܞܙ{>7Mv#7%Q{Gyͦ>Y}N0(#u+ıp-q)US#L!>CtI|g+l9rPӪve¢bMPv I6u܄ԅ̄B&݀ *I+:b5P[|{MP F80 zc0t, ?bRUʕ] ՑF|hr;[ YVL~=dv"i?V: U9DAuygWg<ZTF;ǼV#BPa [NF5_tYM%fI D DO['lymuRhF)5D֦Yw=,ìb*Ƭ67!SKp҄& ԈNj 6XwD.yn[Tݖw1Ubmj֣.SSQbK>9fp><!ޔvxMgwgslzKw@j7 ZT>NpVPT PШ+ O :ĮKt )< W$8" L,08++ifM%Q@f@\4%mohS`be~FtUPbVJ* KDG-/J. ENt0$R"|.u$JS@6*j]@ľ$hy8>U'Hl Z^m8*=WFXC"aXp6@ |-f]RX3 IDATIIKmɛI׈F1h| > %4mR7 )RtwNxC6H&z_MiY>/E s^xXa [[7M*,rdfwQ#zE]X?`V{C)8"- Gئt$ A62_}vj$&IlarBLZT QQ&XEYDXEDf!|@q@hxZExlL1)F0CF~A[q4&H Wy=#<.?Xw{E>Tg"АBG`<9}}KX׳Ms۬ re}ʢ.5fRI֍NU6im6R9|= cJdJAko R"A[GR P&gB8t9r9r5.]ȗ!׿<` M*ŝep)KӐg Y|8uhw6f5_fWj`7وIxפVYEyDR9, KKݨ(.LmJVJtY=f`\8$fj3f;3g@ͮh2 wZ@_xZ>|W'Fore$ )R*RCgG10(:Sj$be1{L!ٚMZ;hKzbI%jć%UqMxCn|۴aU ='L#o=J l2&VFXjS'8u%1Hx>r\3*&M$ (]A)Q؏| 5*J 2Cv|*爭YD,i$4 (E Aa+li-24_M6uޡp Je[O!oe-Vz wׁ~1phsvObc1Bipd{'< L ' Cq!ja>I{qaYmfbȔOԈeCبnJ+vg-=>Vx͑ 8nсcuȄ8I=.؟;@*z-&#CnCri1>)Ŏ|Y}A1Ó ?)tWڗW'\le>#<7rK`xW<|1m}ޯ-+^;W_qwL٤Xdts y7_/:bb2). mVtyPGͼT:<֜\q63UoM/lN}P2A OS԰.`:%`+N|s=px1/ Ă#jM!~3JN%4JVaR _#B$Cb$)3Ot{ ED-~[i)ލY<}>N983ٲbQZ=SBS)jO7p5]lhs]G,f1̂ҙ=O@ EID<|처vUA}V+%)s,>Os?znkRa:gsfw#W%RtKk={n.v)A{g}}w%ka6Kx km; ^ I(]ʒV؍Ӓ.D; quP%.Ғ 5:}ə6wBEMmA0oRx +~INACNƾ[ְ=wɩC_ؤEYĩ&l N'R.af[!cjc9IbT:$,3D*dZ~Sf CvD1gИoI[cw1G9+ꎤ0 "Y8@ ga$9b6aͶ|ݦHH4`N9hOH?&a)}6EEY4(>Q8Hec=10HD$& 2鐷mvӊ;mJIH daʌبZ:qߥu0pglS_W9c`ZTDZJ%+zMEVI lve(#<#?)@PH iT(;$KVXT rLFQ~ F5(EJ"*MC9c9 n и1.GD>8=yTK6k6Q)(c{9EP B%jU!e})ۥb4.=6KSs V1% ;8.0)1ڥ;X2j/9𖴭 SeLKD!0XH #A*tY[im T3jCR& E4Ōj^TAgnM֗D<]]9# 5U5ȬYK2y{ 4:1ª fՐUe賚X/GTdݏ~඄i  ?iɂOslJ:'=a.wc??yو"䱤hm5MDŽf'?C)̀愲e6\θθON.UidM)s:jtN&yPo"sB^R}&7ZtNv-Pn aBZ5=mIn}}ݸ-^&y^F Z C}ל<:Ɗ#4)I~'2K$Y,#,l} {d̩sC92Hp؊&n/812yN{l>3>:gl-ZU6y{ >p0G>GWDCPn|4֨&2ٟZIjj!QR^CCejA [$Fåsԣs\9Ȝ,6H674?q)qU*,j,bbՔWN5)+.QyCF&<<#<k4'GBFcPd#joTGPE`R~B{Q!_ tX[j"vHeIŭ}ʆ1 瘸كOgO>;| T6V1DK;$Mل6?sH>W7EZ1g Er{{6ۍB2}&`ߋ8y*xsGl-~q5 HcΘ[{7ڈD#1~߈ rKj[T a834IW!ԤQe=vE5Ⱥ@&!СpZ qL{DYh-vOKv-- iO(&Rƚysb.TDJɎ&/y/ Z$IwJR,.i 얔meMJ$> t 'gcvM S!߽~ o^TZdB;r"G ߩFv蝴#t\S Q8'=n6{[ hֈVޡOvr[ z/^]N<[;LE1.jd[…I2'.dR/"\-FhZM[IHa:>F6m( w\~rV^c Wʆ mr*~~rI0Pt@4bS#5E"G^R" Els)-8rO\S-Oߢ5ߟꜛM5c/IJ1ç.nj.ǴlI+_WK'?+Bd#<#?@4~u]gg_5J)??AJ D5f81_ /rl7X1]hz4@oX1(,nHYݬIhflTh/u(tD:U\0s\3en[ S8!;&oC&,;CŤlfX"1а\!Jݜe' dE6GLُ&V: *) ]"m\:ZQI5jfIC&~Q(Z"1MԚ@'搖YPTꆢ^ {Y$S`3s3q70͛_B mT%P@UIitC}4L uIRʡMp "V tF7ف ITqSJcߑW8Fm)%E,IW>ۅAMH6a*dj_'T4\q0nP@+] &9c#9)iuBHo1 0sVSѬ|6@[8luB4'GK6Өχ)tpI2MD&4ƴw9]E{!{,ޕ T1@GyGJu͟o_Tm ,鋄0fAfdITl2:j<µLϏ[.2\/u1ŒpIOJ[MFبSnmh6w8S""OR6V5+{kD7̳(EkW"}>ˇ>ڡkvɓ?!IqYVf@ ]dgXvXG9"TAAE|1z)֧5Wĉ)svQ"YF=çɭw}ls[lJEMFԹAج>F l'p|@ؐ,"w,c}b=l*Vբ7grq4Tɥ{`7%VM=(kES\^:#E}HRQ{>&D@85 MG| .;>bKpG9( )K!C z*@JMhVkFxI"!l[-mv4yw>0$\LLdmNuB#O$闄MHcwd͌35w<vyQ|VAE&F. RlIDJ% I~\2+')wi*)0 E2Z&w{E?~53[d IHH(w_<ϳ"T)'bW<)zx T5P ϟyIM!uwmS~l@ xc;;o>l2y)$R.%սT~ش&Ey0,[ןP'FhXDĝ8((% *|薍*O2U$NGz}T*TV'R`kR$ 4d{B6:֕Ů {{B0;֙./@qB&RK#>5/lB!15oƍL6dƏό3 &M U$E)(Sà ᥒ,k؊ϕLIj&mkQtG|O/6^22<ŴNCv^Z+{ |0*v/IDmU*Q%O+?h)Y( l%e4KRp*u+[lE$}?&uChÖ3Rr: L8ŵ=jcoukaoqk*M/znjN%ѪyCePj2;m(;- l*"P뇚*lɱ!p8*ع6{lT ZiafYۻLbsGjMnj>wS&BhFmM8ұkqA-"S/"3ZDB0Ѣn*=`hIV?~5 jE)(55ğQ^_z\A $+ =S'>3'ɏתjnr?sZ"NkWLu m{lݓHٞn|ZGMOmxlfh6I+JW t^M׵$:ЙnI.~TkFB,={ЦwUm׮w>$8 )j)9'([9?nHI$I+oJ%pEpDHӢUr[N o6NlH&lsR5=t ݧn[;YCvTFMT\;T92Ө2 UEiV){h&TGўV"}Fuf)/u] iI )TI8B\$UN*! u0" h^*YZ+Wqe[cXUBeo]K`1D,*qC%TE%VAqMU^-mOV);Hs/I/EwbagfD5] iaZXQLSMuv&&\4@x?tT Bxjb5$*$(*;qI\lKغz!bI_\Zfq=?h )u!a0 -"$Ʀ)$ejbb ^ٮ8~Z$'#s/YI{b/!oߧ{[mQpVK$CKw[Ԗ*łotZ[+5@bfS. P2j2¸F)ŗ+pG2j-%b` B&]AiDU;aDv 3Ej ⊫ETmI&~"n;ؓ=.hMW8HtbGHI LԺz_gE(}UYl/W;Uw2[|J H/41q $InZP b5VF2$YˊBj+`)@KZK`U tQ=%8 ;IBMnzZܱTbG+]YN+Оg;H$H5' "V_1[8$WNP7gjw%LT(g):q !" 0|&j_Jv%\py)Ż#D*(-M0L奢c*&A9Rh-'ߦ6U1 ]BɄCq.aw>>+DbJSC$zj04w6!RqBc!?BP:_r*+>IɩTSz }{ݻヒa[oiӦqꩧ3f` ƾ}=B2dƍk4s:v{+*X~6Ϯ]ta}ݘI^^]t?޷o_>c;B5rBqj`tڕ0`!B!BYL4McҤIL4ǎj?qXo-:0e:uH6ŋY`6m-͛Yh۷o'55QFqUWI[O?}:cǎeݺuٓGyٸq#!I[?svZƎ[ٔ<6> rrrxiˣcq`Yc'm+dp%~7ndر$''ٸq뤰i&:wK.|߭w&z>.\y^,]ɓ'ӡCv;W\qE}Tddd(J H[Eѷo# BBB :>lk?i[!'.;_UU?XOٮ]1cSN%)) k㑶<6W^y%˖-#PSSʕ+93ic -[FVV mP?~ҶB!N'ZO-Bee%1iwpWM6I[#G2x`t]sy>y^z%:uĴiiˣu|SҶB!NJK.y3UeG<)95b Vo?xc{w1 )bB!~4MN~| !h V8%{e줩'_WO!B!F?!B!k1~6I7P!YɽR!DSk;oۦBqRqB!DchOndB!Bq?!B!O!B!~$B!B_8IB!N?!B!;a_0j5vTG?';iO!B_۹/659~٪z%t҅.\IY4޽{:Y^[nMKm1ɮ|HLnqB!√UO-TmA=&Lb&>}ׅ+93f4Nk%oc$.p+ц;YH.]XVcӻwx|W}K4.gx&Iу=JVI kOĽo|Ȃ7?#>%ٚMwKףGF1iпĘaƇ̻y,^{ƏddK\)M/oxF۩ZhB۩Mrd:>22/)fo?k ޴L:z_qp=vyB!BAJ~3(/pY&Iu֍.7ńW{bܶmrt3_W&G r\xΩ{+~rCZ]W_cAyAtzoSo.LCj;uN_l"-t8 RkH%JBY8ظq#@Ouu5+VmV7OL2e귯ӧK'B!v8;]}\7x'!=J0ĥ99gxX͙CjvrWֺv?;7ǂa}e[,Ă Řkҋ 6;;Vn` | OK5dˌ6J<˭{>=|-sdܲ ̞xcs4LL@Œ? T>T<'$ӥ/Vp#p(+Uz0öm(..FU[ܙhi2ZRm 'o6sӹ袋p:hW\Azzzؼu/OOU0LoSI.oK0P5 0 P0-(l~{Z6tU/B!NKJ>gVpiV$y<].J*N#9.^FK7y,دw p[1`on|sxKF dΨθ̞pY2޷gs.㙴"E7g /77CyGXr>_P $.μ1u& kxKô5Н 67W6ZF/^sGp3 Xo 8˗(crrrXr>W]|S.ck ݅zgu_eY[7Zˬ~]iX򷴐Cz2af 3ѧOte]< 3Aóݺpǟ0wwqf qfM1gH&,lP|>`lAWB!hΚEůԋp%l˻\0iw>)))nE!B'dԩ&' s]^;F&5b,?oݙWPbc ek3'caО~ 'U1*~rzu}̽q@87so<}͕L^:3^ s}3+~of*|jXGLܖbrZ*I˪Zz$bDuaQҒZXy o`K@ͮ ok :CSױ2M>ٳgj t9 [%))M=/o. ]3p=LejQb4qcKoiQ+B! O$}~1 VNogDǎPU8\.^^>;R%FghЄ/UN} S ތu\2e :%oYv=ʴ xu <^g2W=4ugu8q{%w]x&Z}tOY3f>_oz>Oՠ1~m`a`CЈUTj`UPUw̥/?n*q.NeciDuiF :`5ABgݜwy gt>=~4Į]]zv\#=_?f=ܡ=Bŗ7W<sq1@͌U;04SUUT0Mݺ3 LEH[_9D70H؂D@}#ETy2I@VZnut\n_ @.rWcTmq~[?^IEiZDuoJ0&uuuQ@(B!?-4K:\[pt©Gbb" _\=rhFm[Ec^%>#sބg :z'7,yba֢W9zhlbA=F2k3OORu؜kӹ#{3q΋|hqWf-z#/p "0{q/o}!ox/&->x<&%tOSQ9SQcf*(Ջ,@Wb_21Li: LLLBQ+ji(ͩF T{*YZ_(۰b]-}>_~%iii~v׿fS)6NqB'~iT?/bYB]0MBT70 =c:FTo`!B M~|U>N?? 7RUUEΝٸ>CCU)x̎;{G ͥ>X5;܉7'źջa֝zcf =E+~guT=>Ĝ!=ų K5HT){$PFHM.0O7 D7M¦I(%q嘖E0:C'(q_)T~VDN7GyrT>ChJ^no,w1L,+T(ufIDu(5,tG!ByռS v!^/^XQX6q9$zl߅נXrrrX4F aB^[lulx߁ꦙ_/<,꒾9Cz0jA>sG%Sw GAP0LXQp:b_Q:.ζH6zB!q'weͺ㜎2#i a(>b̢5$O90 |7ˁjviT:5$%B7DiO$Νڂibᐎiiu@i*Qr$܎R>˝{siŒ.pkX[weD3 bpu[5B!4/zpAQ(XQ̨6ɵTӔq^r2doFX@ް^-fΐ;On]מ|7|R;8Gᆅ2Do,^S֋~}%}O4,Fu*Wo{]$3([WΰL4BS2ܴNʢ׎]k~@zz:לw*~?~EQoO?4Lw^.r\Dǎٹs'AIh<5Kݣ7(B!Nf²0~(f?X2n rW]-._ǒy3)^{OC+~_gxJrtg y>ܻ|/SkxƁ$ XQVnux珜+TnßK'c3w>?8/n(B!ɬY%~B$aۿauZVIi_v"F"r(??V]T.Kn}/`Xnfۛ)|þ*~/[;'qsX2n .gMѪ٥6Q ۸'n睋AۭG!BY%~z#~]n,I1dP}w|ޞl}.??)cFw"9ZM1:IB!DC)/zT ʊ)X}zB!΁,k!e(X~6B!Ɓ_BojUЅBHO!TB !6 !B!/$~B!B wT]=_x>G֯>=B1yA!/Q%~pF&B!'tB!B_8IB! ՞8BqG;^!8+/֮@mwB!BK2[ywz4z"cB!BH!UzG:Uk'$0!B! W^_U9SnzG\s;q !B!8N.)&!B!>AzDIENDB`././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/docs/source/tutorials/images/code_block1.png0000644000175100001730000002543100000000000024570 0ustar00runnerdocker00000000000000PNG  IHDR0/2CbKGD pHYs  tIME  8 IDATxwxSeIҦm]J[F)P@L!8py|P( dqQQ@q eљҖte(Rh՜uI79I!Bmإ\{jռoG!㑛ۧ>= _/lXdIzB!FqCCf1^oGo|DZdQn/BQ,[o,[^v̹x0fpkGv)< ͊Ř&ŋBQOa/MVMt㝵+qsUkǔN"d@ӧslLǔ^bȇ?O}=B!D YmmUܶͻՆbX-ݚ3مv ft6RYdIя +(?+!DvVsaA.bn9W3\6sʃJ#f#;:ɲmK?lgS,o/JqQG͆bTŅbf9d:{NṯwLqr6Qzl|mk*V{̃Gq()߄ [TnY,k_ycֳlq[džsh Uܵ)(`NTmBrw/[prwwv(sދ)- Պ=gd̽c7k,M7^㎗~B$DuǠ4}&f&؝mޤs]ĕѻzG:;Ka19Joޢg4y=:%h͙tmn-ʢ7Q,$]Z?o{ ,V0|ͺ?ណ~lsELxm\ܱ;OL޹tx2P䥑WԿZ=0V8vnmȋw`ׇJ 9.n(VWE݉ $,: =hM\P`ŷ蘯e Qw ̜[q LhBZt/<;߆XϢ6Bd.՜5= =yP@FF:M<+E)ANȣmvN3}}u c\چj]b.N2BzcJ<a_r,5/Jv\QͅZ0VruB3ZV<P`*V( %/f < 3t;'`]:jj ú8hB"k`~8b,Z0CBqs&6Ho`hDAEm$Qhr:VbZFyF`6DLDQjl#!tT 'g3/ϲn3mD>Bƴ؅!Ω}wD}o4ߝ^T/<'o|[K+5|cش6.nF#+F )9%"mrmqp:N:k,S!!k^g6>ooW{˲ z~ƗEM~gx\z8l$ǃc"{+Ѻ(FRmZIygٱu4F|X rmvwfߧ ^_FF#@'W/rfqEEqc6-w^EQɊYd2v`ѧηd1lAlhB~g̗Sokkma7tj'u(Пr)D}W50Ϲwb*v'[z}tkgIp>$W7e5dLg{Y3bqהq9#X C>9'V>^Fb^ԿWDZl:pV9{$->宇Li{6IƍCI>sbgO1ZqEQU}3)w^sv lʠsʸMYtqb*;3f{C6$M^k/8݄`/wٶmC-wBW;_ó_;~!~ʹi,v:5+fQcJs_{)ͱl\;'l!y&+n.*b s%Z)Ն9kL| IfL;:BWi8݆JQvj#䙭}]hweGU9Q6ޗj;}̳j%+D)L]kCq;߭jX[\v(Z55Ar!B!jB+O~z ?\c"B:ArfIB!u1LK4BQNqBQH#B)`B!|%e2X, B!DE BVR XV E!$ZM@P0I;I?ů?}O~ DRK(B!D%lVOyZ7#"N6oй!B\F3Q摋x/әd%! 4LJKsV6B!ZR!ΑF!R!BԚ&y2o$EQv&N v M!J}đ%/H*`-[Ɩ-[ؿ?v!C3|p N!*94ytV|İ+d@Q[K=i٦=Dd2¤hӾ3mwfKJu Vmٻ}1^â>ui (ҩKba斻3sLBCC c֬Y|G !5QL"_}o׭oLc? ְ0dq ^]L?~Ǧ6p:>K^sى4{v͊={Vjݳuk2Ϸkbj̙޷o:urر#퓽P!k`\\\x}"/\+%tQd:Ez:e_Yu4rkݴNXX(sfͨFNyq"~~W~ɴ) F1gKSˌF#^^^^(BTR]sא;xuRG1R\DFG4"--q?==#HOp[,}MnZM̀^ T8İYjFKB!cԇ/ky9_b||BB.8z ڶwdǟۙ3s^vh(((p?{6Ӊ&߷ʉ#󧗪S֭wA֭e/B!jg3X`oӧq]WH -=3pmnffΞ?Yv 1<DZ'0l6lžjUX L.Vܔ):}¡Ç=v|i"އzɓ'LRR&MGP!k`LO.pC6VLpJNi`}z=cG?qzPƏ߯_Zy xu<Ǹ3g?Ҧ}g^:T?ONУjہ1c$s9={ҪU+Zn_?.{BQuݻ7o̴jk$yz2K̈q H!3&b)NcH8ɈQBQH#B)`"?A B:WGj*A!UjVn!%! $:I`2՗?|ñZ-BqV `͊Oѫi$IqWZtBQYj`zGxD0MJiն!BTلl,B%!B !B)`B!B!0B!W| >`HjB!*KQJ^}m%Lu;u?lC%isR!,‰cGXj9Z7w"ON!]M?n䮡ЬEK)^Bˤhh֢%w }M?n$77ש{Rhn0B4Hp+MF$'`4tRTŊZB0%%%be5J]O*!r0R!uJ79}$BybXxxKSY[rȁ~B^Tx<;^0U 4j| OȎ..g;:05]I8ul"!2obܸ%7⊛3_dWXX3Me\:y}t?uĶnÏ '--1[ٻ12;9xPO_~MnrVnZĶHNVfHIEl Uz`.Zј˶͛lL|arG/M捥0L,Z:g \޶ͻ/NaUsE !)v+>?2y6cqj8z[fΘ/O (yaQu\3#BV(<ݻaݷ|E)wy777,5ȅ:BQ l5r|3^'1)$$&cx uwM] *^!8:|nѩΏ"y)`Ȭ9s3ev;3g-17W/XtE91 +H*;BQѭ-6&sg3j8ǰsgS%mיf1 lul^ }z,!!8߮pu* 4m߱q;Oz!Q?lW2޻z=Ur)`Xp>P'|J~}Яo )e\<̙x9#t+B!dysc9Uz%E"gaS0Mze749!!W08{Vyuπ;]J%Bq&uyykOݩR+R(J巐聓SHB!`,9Js5F"B0PX_%˲Z,526"!$HB!J?IHHR\7ȚU+xDF5T(B"VS'p3:)`SdT4n_6}bPBJRkބ5] ۊHF!B\v N_s$FMpp(={!&,97:α{iT*qf#+#wqqO/9K "S'O >^6Gߕubı#^;Qђ r.J?~d?q JcUDl\'Onn,977>@-žX54kђ>7JΒsɹ{`r/'7,FN'9K>EDb)("Yrn09WycQ@\Z YrO9[-VyZ\/u,9קꐜ%g ZHΒF!*z/\,jj=E,1 Yrr m[Уuua%gy*$犄GDRrYժ~MK_2R,ܰrGm[^8qyoۻ< dZ[3ZC<4mEV<ʹ/moXVvů.$00FM3{&oTZİl63j5`Z7!_XIn3!C_r\ejل}@2{ Zmݳwұst a6??fL IDATЬiSNQbnWV]8?Mx1~9sкE%M;sNAn6#HNJó.߮۶bmX, "7' ))x%j444S'x1n Sa> VqpБu6t7&#,?8slfa}j4S_L4tgf6~;w늛V'>mKL3g˄ȑ#ػocܪU_i/9}|3M%4$N 'ƍ qjZ歸/`ԗjԗ/׫kuUr b6WJ*n/ՎnwZ"08㘦w7-Z7WyqʴRzv1kTFW/pv=L2X9U캘Ǚq `+8AFF:3j\z`pĉsnG~ѩ4 '!1];vR %3¹FHLJf׮]vz=QPЮN<5:ρ),,MND>&WC{aKmXE( %=l^,$4bkm=0Mɇ1wBf]@N.s];w}IzFOd%u14o\OVQ[sd֜$yАF<(6|tQxen<ӣt9b8>234fԓ#Xv끩lV:u[$77[nOVTI\OިnΰZ-5( ]t)>Ň)j5nvbj5#h{G 3f_m 9_y"0@OdT4n:j5zqu|%O?28+ReN-FQ$\L m-pc-ԝJrnڴ)o_>% h^<Յ)^`ʤJ6Pg?WU6#3r:kשo&5^v;9pѼܜ2ʠ!++GSa¢E3f9d\+{h޲ڽT[ ~i$gɹn|^.%YrmF*Uǒ\rVI\K8rέ0r`sP*9K΢ 9zՎ\b|YrWR4JB!kFRlׄݎJHΒsYQc1Pksj֨%gɹ\ۏx#pr΢p痜%sHh#N8vN.9K &RcdDi>>WwYi=a㋋,9KAQ)]nNܬ '_q]~KΒsLY]훷FfZep&sarsJ$J}P#%ٺg dNwBt撳ܠr0$dĨqU)$}P(n:oF<~4$gɹAۊHYr%gs.ʮ8tn\HΒ,9KΒ1j!BH#B!B!DY OB!D)`l]txxzKB! <=~"[~-f$#BZ\XP M{tss$%!BD3~شYJ~SzLMRB!DzzDGR9]ӅBQ+l{zeY$G IENDB`././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/docs/source/tutorials/images/container.png0000644000175100001730000002656300000000000024414 0ustar00runnerdocker00000000000000PNG  IHDR/&/bKGD pHYs  tIME8! IDATxwxSd4mӝRR( Ȟ"C@Oe Y2( (*PY.:INдM/r|ss9' <~W'3OJߗܨfΈe/> TΊQ0 |dvSH$d8xTca.LE A s9ɞガ$yy"Eea,dh!""j 6V}6 n>6eP7d2Bd259!#DmxVL+IR!Aـn7_y MYV+=MucshBV͡xvh ?M0-(` v&#ג`2.=NO~h\vbf Fȡ:&qVYVCE32Prf.}]mL|6NP~IӘi1D0 " Qd+,Z|fQV} 1*)2xdHƖĿ>wX: /4 ަ>֮>x3s'{n(Ͽk]{p#0GD /Fpm~N\Jü,+.fV[+$[J:Om!R4/psЛ1sLh4 Gǎ G^)hrӶsHt2 dVnlg%zbgZ,[s;)?h8yA,;K;CٷΣ32 6 9BoZ}|n !wXp ۍc0s;,)~1VهkH {M7F։&JT;sfSFc /,x`Wǭn~J%Tv';hM'6߄B]6Og Qx0`_-C;DM٨.AG@߉|w,Y4"f]`m?QF̴]Fu6kQiۚg۹Lx~syPZX )bڗk<kX?>x ve4>3?`Erc?YOɃWjvrLy\l޷!3!S/bѪu;<$?;rg^_bq:~y`SB {0 pݲg vF*9œ<<]d2.rh|xprrxڈ[3m$൅Km v;Nm? FuȎ6{oJjt Woh|feYCaAnЋs!׾OADґѪHzmw>,]N&o&=G홟.66sLz>$rN_JD3<H12‹G^&q~Wkeߝ}1[S-/~83s#W>h|2@.D^H[֫M5/NKEW hQ>V:+Ɍh>qm(5+3}Zz@*Ao?@vv[A%DR:>ڻ'm -:ln\܎><݈SǛa2|W~~G .pǥF`A9]NӏV'V8uEE`5 \>f  /`4zxaY6}}-W w0,C^~AnߞחU}@g0"!dB&TA'Z2LH3݇>/ ײ< 1ls;}Vй6G.=0u|?1 -wܨ[}"ݮoI~rフi#+l?ËPBz})!9'09&@4b1>WѫsJ%jʆv*?7h>d*6 Z'4\muZHJȳUQ|)r) +8,/:A}Gٟ>BX߱]۷B!POSϿ߂L0Y1|z: facV,H׉s(JxԪUat)*5*Mis]A,}:L^mフ#1O4_҂AR'7UcB|s8 yπjN wSP_g=T?cP|jMƮk^Y~Gm8*G=(C~=j95R^e؏'mDPp35Y*l%\"`dk6'8&b=f e˻ӍP\jݪHg\C; wF=O_#Z&>f{<!)1f^>e˗'SE RpǥF9"zתCʌX{=HhNg݀a}rO#)E?YnGvB 3f6cߦٜ@wX!.z R-%ͶڂUo~iٲwv$ It(, :NPay͚M#޴)9E3 Nh@>${p5Kr/9p4+&' S`E`WH%_Qyl wGVuR١~~Ft12ezTҮ~_FG^B{00EZE7+i鉨) g qw+B師.e8Po f8 o"ÍO$E+G"`^eDDD:̟*n~ص?U2ځ:=7euHS?b=HHLºoZl̘]ZwO^IDDT[5/romXUkxkZ?4ޫU;-v| }|Bpp/]Tlx{[o ̛(JzV)2F777}wwws$""D]Ru,Af,n YYYYhj<YYٖᄉo.6 ,7C{4tմQq4777h4xxxνU.˯776!!1j~bRտ}}oJe5?!1 *~_|"ym4&rڵ;Œ/<;.JN)զv?z(ڵkǽVBLa֮^Æbxn K]txFfV-Yiwwaɲ /Ž̟K|%z& R?/ܶM46~Vcn`cˣc9QqL>jڸ`w„ ;w.RSS9s^IDDT[5/,GǢg; Ľ#7Mֵ  _ ӧ>gPCPai܁Sv;cxcJ˼eK}?}n-P]`܄'жCgL !./eeʔ)۷/ڶmv;ĉWUvlwO?v:hզF̆1y\q"""âYbyZGt"""bx 5IRLFFTRNIf!~5 Ë7}hY"" "._8[>C~kԆet08?j|}\]]w^4k~I q|Q-|W˯,vK~V/IWCLX ^#CDD 8Wܹ1cմ3fKNnh?RKѦ}'tu;`"ZOHLĤ)C]ݮ#{r233-K/Ѫ j30qyJ/o;oVn_[ClMѭ+mIYd2a+sO?,|m.ްzk׿Kuk*l//6}_۶8|妖'""ӑ㷍jf7`ޜ, ^>loɢ<5IEFq 5𒙙`f!!VOV©gPXXf{%!৏uZ].t0Lu~jER!9%CCIVy~^}ynnn(((@m]kw* GY.*8~pw2`...y.j#G +|bf,YjNB'IIXzMg^DDDu],Gǯ(^ Naz2DGE9|;lx9c^3owwu ,L#i}y͘ΟǣChNx ΟZ&y.Kxo:˗0gЮmh#F~im>>lRQԸ\Mf?v toHǏCΝ!J+}>"<<))6~ w>MvzuxjD72mcú7XL6[i@o3S Zut6,XhDݱvj;AQQX* ,{}}(bш-[j0d`Z5F*UVZg?Vȑ`ْEP(rnt^XCn=0d ո8Her@b|< jz?  Ɍ4Xo5("91J%2g>#GTONƤO?G22 -v8flf:;w{vჿ!)92ͷSgN87 ,{ݪP窶Ǟyթ9ZƚhK:\| ' ##m@Bm.7Z73#mu ymԔ5op86GAZ:v(PՈnW7whsuZ\q-4((qWlyQz]!ii>6#\\Ƚi)*}ξ_U?Rڵi{n*6cڡv-*/~D^=PgCYt1)S&3y۶}  8(X85˼7o!(0J̞mrChxdҪ[_ ~~X85|zrH4ju#Al.׏l|,̴$L&22O/d(V*n̗ z.ɮ'\Z'? Nܬ zyrⵆ_ڬsMj*ˡP8!''r ^L320u?UDW<txyyZm0k+pW[. :{yy"-%~~69~_XϜEaa}:;YKh@U۞VcC#J/[֭*޲M˶TVޖ~>̬i&ˑWnzaa!M/ .ڑQ+Q4"erH_?Ԥ$pҵ+r'x']X[T\| 1; - ZGGàh\0a5Ta=c.<5I;vB=O?; +/Š^:?OLos秿g;n 77% бk2"V?__$&%#"<~fBg[B=OQRyJA6ljcHg97j ]|U8 P<`5; AD|B"||#HtsCF{$_:9_-e5BYYJ% F&zt ?He!U۴?W@B[O1g,˕l띃ws;|L6כ ANs_,⬀ IxumhJOXt9jh4je޸Gœy Qą1}K\3:Dldgga5rD۟Kpp0\rrM>]\j5v,$Iɩ8~$ svfHNIa6R6#٭狄qs$%&spAhPxx"Z&k\ 5-}9wwP͡ de_tݲq_ "wdG^ZDg`يn];㳏?DDDDڭKߧS&>F^gcXv= ЪU#X}XK@=?=2gո{hf<3yS&O |OBzF:ZDDgvynv팻Zw5 S_yqHxn7E|Ҭ],a4(*РP ѣG)=MJv:@ لȖQRaC.._@wKc}sٚ@ \U!,<ήJHRFQڪ 7OofgX PV #5^Xttk|J|tu8[IɵPH AXM7v͚&wcޜW0o+OHxzd<=e]Wcܟ+{UVS&O”ɓnݠ~uPr7f3shf#?7MYEu늠@M^Ǝ)hPXP_ٺ[U_M$4 32ꭒ%`Yg> /-JŬ3ܘ,aXgֹxAm/ : U֙u: /+DDDDF:y9Y,  / 3+FDDDuZ:単IENDB`././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/docs/source/tutorials/images/interactive.png0000644000175100001730000002523700000000000024744 0ustar00runnerdocker00000000000000PNG  IHDR0. bKGD pHYs  tIME IDATxw|%i6IR "eQ2dld *'[䇨,{+{J'mWf(3Ru=wsswORmj=N؎Q#""G¨BY*1%2L+7 l`(b􈈈$Rݐś1H&/ }AiJق;?zi^mhUipM34 zrSوv]D/UX*(|GD>^mh2 1$ۅ?zehsӠI3['`udS4m=ɗ/A \k%̺ ӦMCnn.E;IL[y?K>l-A"CyiX-;xwdGiޓWW>~̶LpՐ{7?2~rICy0Qm6M!Qf2QgEsì9P:^€5}>yGѝ WnQ쿚oEncp5!r_f˝Δܡu62 ߇iN=`1/ }s X܆@tG~ò%1b\ ](RUry,gX=x_aA"LǞ?axy\;zMY6;ϦhFcwkvm당/cK$.0p<1uS[%0Frnd!; F :`g ϯAF2p<7eIDο4@޲?`wU&8ȖOЮSeַTKv* [޺5AD`N]D./ٛ1i֭T{ O } ;ǚټ`ɪޠm hy;[ƣ@^,63_ #tZ-.݌C#ocic"xHqCL o?K^3s1]2+NanQ$Q WCwz jhlV4x <<؉~zIdzfY?OعxI_R*Fφ m\؆oV!G;% zΐ )>8u$haa͜[76_:8Bs?IQ>ggAn÷8|f d%\ÿ~SDTr\"v\9Z**WAU?RJW:MF`6$\&|2= {tGvR@o5p^yu|S30УeR:]6Bpt_fmxtje $b4ػzSXF BjjFd'C #=NqY2}G_}S oqlo4ムo4 W~u #v" UQʿDPIt1$ C!شj 2}/,E 8y* ^`߹ Wd @3PQZj)\W1Y^;;Ϙ'/S" nkvFaJD}D򮰝™{ O6Sb>h P^?+nm$|cNCWxy"00:l]iщS[p6QOA絻P#rL<- 7&[_y܋W箇_hrӰRF8 I:tGB~X߬GE֭DT6<z5ȉ~VvXϖzaqF$F]e :.puuő#G0x2$Uw,_/@tu+n@3B.#8|3̺O}+I ٱ G|y=DqD{%%0FY_L-4:#dR"Cbq"A=X=}+a'n =Ni!*c;Ҷ6 DZp6.yzHP٧~뗽P$w(F~2S;}[e4̢v "#0B9#0>>~JW-.̇/@ y iiiHMM5˒j-ܑ.>| wiꌛ)aP)h]H@P<(FM}[F؋4 XnWb{vwAtZ#FN, D!A/IC2HG ! R! 2  ?ٹHױl7AP\2c;NhBK:KA(%G1AKE[O#>HQV|\$P+cC,BR; ''~~~ܹjjnd-a@J)Egou<@VqGwJoGY\!w3\jU STyw6]竫@(:S-Ŭ7FANNl0dzaqX6M@xUZŨܡ,|"'>%zdʺ%S 7[0>ݽ==ݸ)zZ3tr=wZZ G` fTF;2DDDTs7|{ IY5e|4~0"""q"""fRB&"")ID;;ڝ!.>'9T """+#P}֨lycOk׮SD"-#""6oƁÇaoo:;9r$u֭[wY1x{{c]4вSadWHNNxyM`zobRϫ ODDD5N%0! 0DDDT"""jM`u FSjYvvzyYYV;vO<r < DDDo?se=Zl77W?ӦMRDtt4/b3˯a0?hw9fb0fhڢ Bˣ 55J{<^>@!4%&MJUa?ѿ3f3g l1 o_ہ-^H0ƌ{_}O~XڨO֭ǥK/?ԉ#JXު*qJ5k֠GDDD5;~X,o<4/4_*bx'k6s~oOenݸr;-Ga/ ""cD";~:vC᎖-Ξ=+V7K*KLF#JM:hDNN֬YQF DDD5HzkcnϿ Eƿ1#ʼn#˸xҳ&f^^ ?|n\1M^\;iӦɓDDDx;JlݶiixkSZT {q2 k(ɘ3oY1Nõ1i4e!ÇtHLLĜ9sЩS'"""[N`A;9ڬlX=h#GceֳE?ѬU;<˾66۶"ykL:z=;bȐ!pttD˖-͛70BCfjMaܸqLDDT,XS&V2hDNʟ "" 1!"""bsbQ> """![l RADDTۛ c^g ""BzQQQضm$:xK'B$"""k#PkFbgkADDT贚ڝT&DDDTϨ """"&0DDDIly_AL^/#""6b>>~xw4 c} م!~C%"""+p+m #XB0x84n䅈J%4h/ Jh='%# .F#{ xB&U͎uzbRϫ/DDD6ɦ#1y 1i 3?0 q]NDL`l-!"[9^y<QmI`8Ch])Xٳf`kxLD& lذ gqD~x&"l8qi+/;8`VV3аqjk}jvVT^|R՘9{6B6ChfxwV`ƍع B4Br\ahѺ-4߀JG21)<8qo7`0xyp|ͷf>XԴ4;rډcǏ[ IIJڿX`>K^=1clSG /b׎8{oHRt9KNX;_@\\i[aἹP(X0oYX0o. 9X`>nf̲% GGG.ʾ-?__d25k&v730|aj -- 7)1Tx+ Iaoo\ۨ[7hvVT^\i|\|䞞Ŗaqĩ: 7a :6~@I.z"@kx4}dxzz{t~xs"˔y"Ǡ]6jgWϧrHJ\vh>CDDDL`1!"""&0DDDDլJ~ 8|p1||hViɉu, F,]$@↠F{?[Ifgƙ**M`bo=0` oDwBzn`ۖ͐:8npHfe!E4 k 7OD"Y`0 +=/nrGܟgƙqƹćm|{hи ߴJg4h/ J*so\Fhx[x(TzF7Bˍ3sgƙqfqxօ[+ .㑛 LV2 {2Xr\=qfgl<8 螩`H$rD"g؟?3Ό3Ls'0fՂafgbgQ38\;ow88G`q|09|uOR836{NqI׿ }<>HMbqfmsUuU?̒c-19l;K/"//V}b}}04 7I_{c5wW]/ahaȗqڵruui֯獇ͨ|m<6oѷ?~79zk{UY0'gك];z o:y3 bd0[8u8:u숉oL~t}#>oEcАaի'N}>^zbАa]jN>ACboc\dd8oTq11xxS'ڿW.E s.\UDkuP``ƍع B4Br\.ga&hն֮l}Zg#i36m~juռsFZS \`0'Nѣv*t:i]oѣ8y$̶ߠ8~8;77}Is..xmܸyz˖@mѠq(^T\dffy64+33-۴CVVf=&9uz[5uCf0cnjƈaC, # 15%޻Ccq1~5_mC7/wHhݪ%֯i:z 5iB1lHX|j5Xx1jf-Meyf0X ׮E6аqL1T-jXR[~gt1s|cG];qq[ IIJڿX`]X^CB\d2%$~SbNgptrۉNǻ=z!/Xh4ty|zIIb!l]aaM`4ǟŋصc;ΞR.]777D<Y]m܄յu{y+boD\tTGڟ+i-k:|{{Y:|l_vڍ3f/@:?mq>|[2Ęѣp?8 4_.\q5ڱGG|B̒>okbeb'NwȡP*x5VןXd1|}q"qeS֭۰p\r(r,7lmlǂysPuZo~#Gp $%)ѼE+t:(JLyC'+ %wj IDAT%M!up@* AAfIpppD*AH}NN6 f-ọXȯ/d2ޚ5~|a#ot0f̨ ׽kޜwɟ(.== yT4I?o7qȄѹSG8HpqqY3pQ[l PnnnX8̒>_=~X8űZ8p^׾r=Ng` OsgNA +-Ǐ.T ]ZnLO^ȊN{T.h4jKnq~X+a4s3{ ID=͖|}Q/.~W؅V-Cuursq*OЧ6vB|+f}YoEvmԨ ]U I(scp/;W'[^[Eߪ6o^9_M%IDzYG/OZ%0Q ml6[}S刏u frYy\|Oj۱ຸx ڴm ;;{JU-$AxӰ0{<;Ο[scL˕-E KWDFf3ұd{f}#+4,^E<>8w$DA8zp)ّ+t, 0B"6&֬.OOO\:FZ1qPx)`tߢ}}ѮMc' y ]ظ8:\qS0-x7C&iX{F떶?n}8ʛ&O/6JJM_}ƗS=ӵa;s_?8O8~6mYYh58&NyӴZR{'$ ̶sǢ%ˠLV"77Wz>okU&Vd{߻V`@Vןk̸1脷.@ttiT`K/bİ!gS&MĜХ[O`#wS1tQx%7ޜ<֎}R 6Zn8\zju G|B"Μ: H???df{ 0 qFr@U S Rbk!_b+cz!!0~iyAXx zE)w9+5NNθ~4\`?KVz*n^=:A ^fٟ?3Ό3̩qK_wyoW/}P*yŴ4{Vm: ~Cxx|Zr"bo]*' H$rz(*3sgƙqfq}o$0Rwod3883&0UđL&qbgbgƙ1!"""*-1DDDTsCG }<'gWFV2oLCGoVUҪ ^^x9iW0JDDDd5Ҕ L7g⏽HǍ|;C*u`Ԉzc,0%/%v'"""p02%IENDB`././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/docs/source/tutorials/images/mpl_figure_editor.png0000644000175100001730000007243700000000000026132 0ustar00runnerdocker00000000000000PNG  IHDRIsBIT|dtEXtCREATORgnome-panel-screenshot7w IDATxw\S?7a)֪TW-ւZ֍Xw-Tv(Z V;jRl+jj P@A Ȕ@ޛ_(InynnyyP j/:0 Ô'gY|2ݿ0Keþ-t1 0f,jjs*Oʠ١*tӠLq,3 0`e *Nxp T %"dSm&+a\7I~r%kMO VNa+alahߢQ={ ~܄ :Ofǡ/#a2ӓ }˟V럾\3 ȍNC&FmMQV~ Ľ, DjڱcqɇAujrبhr qUOUltZ<~hN@ˊFq4C{miP" jRmh;@i\bNFl#̶ t:]8YR?_Ǡ6GmZ~.^KƅkŶ+Ck/7r+t@l X#57$ ;BTB)MF>Ҽ#^mazr)I nR(zGn{%SAt:-h'm*h59X{]~~w'w#{g+^m_j?ړMn%^AÇ_C޸lsÇq8;;ch۶-hͳ͜zmѥ\ k`e|많u­'^?_<ӿy:S-h2#ܷHs8 n^+Z;uV~wQ2IwX>`X;T&>߇3!bUO6V[jU=[8غ{A["ܷxt1=4&&I~hoX2=`ɗg cH?@S4%nYt£t2Rlw iT.9hsb֬YY(O/[F^p!"gjwP[O.@R%EW";o-Bǝy[^B[x+C8vCxWpA0#Px\:–^&[ipNF㗓J<S>!\ +O߹wڥ4nOy%~it}OXЪضyjbQ@+" &5]E#jЕ`w/ii50rV{:i\GXQ+ lll>WR r~0[o\ ,06,$=vVvt+GA sbd>TUţm7 V`GԚpE卓%hw.<4 k_oƲz\[:MEzw6=eZ^{+>xUzpJj}>\aHs]TEMNǀ&j%rpѬQ6N^{5u''CƎpB]RRѬa5 5p.!mj[}|BլYgo޲YUMǮ<M_X D\wP>'>]kx߹0޶3) ~<}W0spΥh?E ?[JԡGd#,Stuuff7 5?b\E.lmz-];Sg/6ozmiAmkZutcחZb,vִbHZ=ͨ%=;akB Yi}$5Ӹ /?9OߐrzΆe 7df>ƶbF_$ @PBA 7YA~3tVla%3k0E}W0kp# n)HՓ!}qk`ױ;2n;CUay: Vo[mSu&#?hN=5֠h7p34]bnsyJxuQjծza/дFT*f7u'pG1b8'ᗯ"Qhf -+i?"m6/qcL/WCِ>.D' } 5xm'cDSp%~V-OIJ͘0B=V>~*=3?堞Cvp NjqhU舣GbԨQzd>BMn,3%=FF;ڍ#_uo_w2מ>b3p? ZYЬFvA7M:،76ƭ9ֈ\lQ>T vcjn\KBF*֨f9PC9'wi}tN~Lt&Vr~JނR4x#BkVh\ݪL}q 2EhuJThn]oH] :huV@6|?D%+Ji^%Ӭ=i EhPԮVb i5jUHOOGZУG' LmZ(u]lPiNImsE=ײxz7/>yhT_I}@mgvw_ZnT^Ø[}v8iiBMI3:X@]2G%yDvv6accjժ8>bWyM[Q z:݋Wn{-P`ҰΘg2>f\_/;RefsB oWO[aEж+yG䖉a@`>0 0X;{`x?aaJb5]l'0  V4 0IaE0 ØRKW0 TN \ 0мIҏ3AF30  haŠa1)haŠa1)haŠa1)haŠa1)haŠa1)haŠa1)habpfx"oߎcǎ%ka1+0ѣϊaFY(tuV-0 SNhju 0m4řH*aPo:++={Ē%Kw^!** +N>-h 0bРA[rcQfȐ!>}~\b1fd?yMWhdD4iŠF)`ٲeر#ߏ z?>>2IgBCC&Fm|` 77#Fys]ytjkX0 rK\6%55Փ[ xES0d9X̘1ᅎ Axx8B;::1s%#X Xpv"c z\,]j+AObnפ$,\DQ[xE/\???Z$odJE R8}̝kzF(>aҥ@FRؽtr19aJρzg)gMΞ5\ c.13#FE௿_Liy™s&bIb13,J9˦Mo|LiaeL|3pM ŒaE0z*VNӀ1L`E0zW3G=1X0L1tl,2^{ c.abؿ5nvv pqeÊ#@z:ޙ|-owx`fkhGrK W`L(o ?˥" {J6ԡh1c$$99hP~8xw+KM #cY*.]?&#f@͚1@)|CUL%qm>(IfP`*@6S+V;v#GJ+'cZXTB/vZ\Zѣ7/t''䕚ׁ L~._[IHk|- o9w.:b2X6G'4;=}~eeQy3"DLYq~}RfȡC_񥗀";uo_6F>XTB5졻@+N(`*ׁM#$'Ü 0˗ЩfM[g:O6F~XTB:tΜXCnޤZ^jFՊ͙SΝMߏ)577eJȔ@joE`Ԩ(V4֭]Ӗ'r%p<0s&ecjLl~:u"f'edLf\vm㵝 ۙ)3`gG1ij&L0nRqa~:w&fnhfCP1Vk+JJÆɓoظSЖxxR3'~8VQQ@V#* >2Š2}:h4o[^}1w)AQܼiٹBxDo~nߦ FPx".\g}t:֭[{Cڵk*zN%;.|?4]_y];^]ڏ'4&-ҥ{a,+0t b)yܹ7nm۰uV\zPunn0m?۷SRT\<<$+ Qkd2J/&!M#W4[n5hL4 puuŔ)S#͂ljȄ֡ \f>JJsh J}*cǨД)0s>3μiaCj W^s///ǛJ,سb޽id в% .J2&;&ժ֦4hc|aLȑ޽/A~Di-t$rI /Id.]"Ӽ41xzzhp\\ԩSPzO>,A kJ7 J~E< /HoIH9VŠF/%%mT52R>Ξ={pСYYY2Jc<,Fbƍ NCTT|T !%d+!^^+oE?R[|90{6n̽ȭhn|V2={Jwv6]߯< M|SSS!DA3<3 ubر7n LnEDc r>: N)T <|(OǎѤbJ镌(yn_zxxxjB,epʭ*8 Mשb߾/5r97l3dRl)}ߌ<(~EHkQy9qp Sɓ*%}RdeQLWWZȡdl!3SI`E{QիRQ6zT13Sz,[*Ȱ?E+O|aE&o2jMZy61`b`jUDG|aR*mj5g<24iB&eˀٰ !vvGRr|IV4LxxPA3/~~~cqs<%91p~VBL S*ޔl Eϧ}tfϦ< xޥ0 MqYx8mT^X0Ѽ9%ii1x0%IW2ǜrWZOȶk`+`6 ӧ˛޲% & C:E~2şRD= S&>Ž,Y.dBCiWΏ?~?kR\>M qi&V4LBNʅ3͚WNO`[s(BJZG~JEj2V4LpuE ;;N>9ʃNWp_Q$!! Tv2aEÔܰ0௿C&ظQ>9wk\Zl9{!*)Y0jUZ٬^ 9#@UPFsȵIJW/23) c '۷9+ڒjv2ɘ1@.?YiRݣxBZvϙC)HnÊ1 VVd:IH>Hŀ4۟9R:2+Zɼ摋,n۷P) v(+p͔sPxP-zlQN/CFb">=7(ǵktג{Q$-й33@ڒ`ESii4`VJr= eFSqqK4LxT)?"2<)Sh`+y}{6mv_R: (Rqa+x`ru5\te~XvE 9"s'ԪR5k}ش)K[։ǔDu+m`cC]eM ܹC[OZvT*%{zz=FC&8Jx۵(R!-hl°hݚ̡5ѢlJTwwk ɥ++Wڵfiz TyY.s- gj0~Ĝmߜ`ESegSٳ_;6ڵR ֨AӼyuo~ѨQ~U T*ڒsN.]eɛ <|Hߝͽw}r\~i"MϏ~r_Tj!U4:(4hs|666B2)ߏhۖt?{TzݝV>I $v4I9%̙X89rs, ɷh PU1(g9PCzfM*$={cy' rXEsNܸq۶mNҥKk.LEKQ:uɔQ$*h6,-ݻt}DzPXjO $۱X#Ox4+3vlРNNUڵe߿O+Ѡ }%12)@44H;ϟ TN%(X`p~-)S|)Ӈ&@JÃ+(TjbJ=wIĤI‘CqΙ9[gcH'Kq+ݻi5#PY IDATr|+)D=,&*R"#dMɡv={6J5(Ҫ-2SIHH@z{yy! !!!8}44i_ NYˉ;֮A{-V-sI2$%m[rX6jdcG?Nn89 АMh;ŋ@T4VK)GWkhyZVvhCϞ=dݻaaa2Zۂ@gɔ6t(/ߟػ7?*Q#۵3͍K>ewkkʯYf?ɴR|IٴQYִ1іGW&g/`)X=>үb'_ƍPN 2O "#I>}o\3&Bf[&磽M( ++ZetHEB|mN'o5,LzJE믁\ysJ~LKߐ;̫t ߩݻڕ&kJy۷ꑒJG{CgIYɄ(Vbƍ NCTT|}}e˖cǎؿ?6l1aaap4B&JENAhF2;e`!?`8R$$HNN7BCC%];2$$#=0ٜZMa!tv͚ɜ+yHLn I5믿BJ(Xj[eES͔l7b-9A+#$77J֭Q$M&%K@F)˷@z:?s0E2֬I-{ӳ, ?|MpvvƼy0D=!AVusaRlhwF#FϪU T'%@Cށz`,|P%V*=G܈#[o^SD ܸTKnsMFƒYUY}ljXј++d  75jPئVuw *֫W>K` %N?%FD(Tr9B+VJC8~U23 V[2RT@;,e߼@+6myZ ^4xlIŝz; %3l[Hj5e"%jFGё&o,eA))G`"^Q V4Sю ݬ)h2&T\sp/]"kA};VTh0ANQX?ܩ\n&4oӆv(h9q2WT}{ k[ Kxy/AwzwHPtV4-[ 2Ӽ>|W])_i2r@#ƚ5@ޤd>wgd`(vɄGfىyc2SS~^ʦE r"oHlKBPVT{((`|z.w.P!ǀZّ^t%IQ`4bRFa%Dv@ϓ 4vB,{͛#իS#_[J>y֭̑)2r*E]L ǧrD)V4 ݝcPSzUk8ʸ[rÔFVL ҖP?`EcFڕ'&jf֍LOd*ׯӪ?)\wo H1ƌQꑍGe'O3x=zT.{ZNUaEc!XYP={(kR2%89QpϞx|ŒE~>}wIsL!ҹ"*G|æF ܽK'NPB)cb$ULϹфV 2XE aʔ)X|yʣ,2œC[xGvvmXY&wrszl͛..(V2 @\23' ))q2(J+܆(nΎ]\ ?F&!!{x=kXKGOL:ܡiQpP$VVOJ2R h<=yQ:e'/wj*r RwL55jpsB)Lf&) VMٶ-ݨNN}җiWWf}vv7N͚y͚<8zTjsd- V?ZMa P Mg:t(yVV*___lܸ!!!to<6,, 2UΦrɵk}..?^Ha GWJ[?N>RQoo 0L> ξBwٺoրп嫸KQ(Lr`Xv3 Sv<o3 H V :x?Ogl*h?߼3ui(h"[7`(2pI(e|V2 ( `*Z̜jT63EnI*7o?)d`ߜ ("Ř!CM^kkvmʊ/?V#KC,c0w*S~rrԭ[Kyƙ[iS Qjm=2U*`NN[6)W!99*^!RCjJ? TFVN%YTN×Φ/o.@ GX[SZҦ =/K~@\nPN02w.p<J AAckK钜ҢHȟ?L#Epu-{URX!哓C嫎XMgf#mm[{ E"Ͳ{:wz(RrR)k,Vuu!!40Ѧ ,x=,) ASɕE*g) 9B5VB N.K MJ'HX[S 瞣 K9O*fQm#;w/[F~3-''0YYyGzzݬ@@Pfܤt@L mM1Sq7BBlz8cNJ+K-,llhPGlRۖ}<|orx ˯6R)MqԬ Ntt\9C߿OLjbf ?_)xs8;uYO?qk*[^~cG֭f\*UȇZV?jv6\tRN]k']eRR4[5`_~L1yaoO>ؿ>V=&(3ƆJÊ1+c9{g̨u,VRe f䕍1X0F9~ݛJnIfTY}`&J.9۷򚅘aEÔtl  S6C+I/yI0eM ONKƏf͒["F.i+1c_~!_\dVB&V4\@+GG`gVy* Hќ(Rt:DFF"::(bРA JO SƚZJ(LuNggΐ2|e˭iWtAA,'ߦ nnޤ4Zn-d\(Rܹ7nm۠tRڵ z?ÊŸh48@zΨ11ke]t_`^`r%&R9=y'ߗf 0z40 EVI WWWL2111rU)h>#Ә;ELN I۷,O lժdZ6?rW2Rpp ?ޚ5ĉ@l2eeL"MBBի %~&$$ igj-N hV,/$Ҽ9& _uN2W^ Az: *xz&fd$(9ieYX>T4gϞѯ_?-٠PSȆ d*SEh0uq!Y{*|TM~Үb VU(ez 6y2T"VVTC-2V'? ǒ RБgѸqc@\\ԩ!C 22BCCaSĞҧO[~s%'LdGRx/-Q:D+AV":|YRA q2{6ҥ_{X7n UHѣI#={СCyϳdxȮhsbƍ NCTT|}}e˖cǎؿ?6lXqaaap2Ct̩Sd֍f%lAJid&%y3L֬ƍiv,@Ύ"S3VSΨQ}NH ~~O"""BF슦8c1c‹p$''ˬtIf؎4u/L/-YY4 '${2鉁):uHQRe4+хicC ))$sժ a)[WXXT#5H?wVVmye]|z;q]VV@ӦK@JUYhЀo~}ZۛF֊Jݿoo3/QY4^\J&> mLWk(ؐ}荅(< (MJƎzt卆??`|ZWFcM4OO["4Xј9ǎC8\T?dظ1?V4(JɡC;he$\@bbeDTŋɜyQD4:쯿iӀ+W䖈̔^ݲ%)%"(c& ,^ 8;ݟ~:w6^{cJc$oߞU }WiyS*SQD5VVT'5E6dsr[2 }tqNRdkْVFBv6 6MC/iRQ#33K ,xGHȇӴq7NNp*)^F̌f4%#TeD*gd1ȴ3mN(=+P9)qs#U*&FF YSb- :%$N><5+ٳp2P2 \\!G/lij6oYԬXA草JEϩE-^,mP. Y&@]P)SUBh+e5+LfD)YG76ml -fLfHߟ|@X *)*}+I"E`Qb0d3[[2/N |i(V\x۷oDZcpt:DFF"::(bРA J!Rث)x;*lBɓ5mh.5Vg*i1o(3h[[&]Phs~_^Ɔ]|غu_@aǍ/L[:DJ>"7J9m~t3kk=ݻT.MR!ɭb!(wfNfEsA'2ׯ_G'Tqqqg* "F>}߿j(R,f\ᆪ‡RTE xyMJS͘1T^hlAct:srRƽg*k]m )r)/6n܈t:DEEWO°08[NL|6'ߧYR&h V4a2dP36o\I3~% rR)+Q߿7552Jd㓷)"5AQn];ƍ$1_~Ie"- ⋲mll,])wBF d_CJ'<;nqrFF%Ө}/'OfnwS}tTw(V\x .ij>[>>>O=,={-(z.[ڵS@rrMCBN#kboO@P:ppUxU .RXҹ; C@@lll%Rb%8˗hrvki5iPL–ҮJ J?zJԬ 4higv.Qٺu+j5V^-(TTW/% ڶ #'5h 0m4řH*a(vESVz%K`޽ CTTT{'N̙3666rh=iӦ-Qs\Fvv"MA}y,?7dDFFzUVHLLĦM'8" K9K9EQs ȮheбcG߿ 6, 0{ 0 #UͼpFW:bƌ/tlHHÑ oooXp!;@ۗO.J`x"oߞwtDtt4DQĠAB6)|J&e9b)(EFߗVT3"B ޹s'nܸm۶Aaҥصk%d#ҮIY΁X}b)PFf֭?oy`ҤIpvv+LSm2_.!~JX}b)VqXhC#!!{xSUf#ҮIY΁X}b)VqX)+={~!,,Lnq5Q|=9]h Ah)yxzzhܸ1 ..NpΥ),砤Qp=ʃүKY0kb+ؼGyǑ]lhT<|}}qFz=ٽ&~WhKaTjnD 0)xyaÊa1)haŠa1)haŠa1)haŠa1)habp ++V0 ÔCڣ{a|ܹv|Aa1lذR?ojyn4<u殃Mև8y88-X&Q4߾+#vb|hq2r;6ėAӐo?7&W){cQoǏ?ŊWFwF4W*jgG/c'YB6pb}XU'6H*GVp1ÇHOOGjj*v؁^zaځ4 ~k0sL$%%=._˗/cҥlä2]@F]("#1M233QEmi"M1pe>} yc`w Ǵ=ш9Q"hg4"!h>~ `1]ayB:Mc1ǃyD ?`!=iMCA@[ѐwN1zEiw_ 6?'šy؍qBm^z ԙ.ӌGI~FkiyD fO| /dB7A:Zp / "t:RRR.<p[[yUV U㑓SѣGݻ㷫3MkBu\4h9ѐjEhu:R8Z'abf> aI6 =u w ,A`kkbYhVU*Om:u+wLgB> c_XD8 D !xۗbtln"!hg4FKa¶of8 y'6WxES1s=SwC!X3eS!y L 4:w*vpu `F#8fj~3x׳}Ae܄1bR4T<I&{.(B"''l^oX[hf OVJ ZW{k[wQT{l6ɦJ" (E *WNM(w"(*(6\BBi ElWޫ"* IH2;dI&&g?93̳9g΄r%~mߋ7b2_0PTEbUAF\V\h.=o۸;r>7Lƍ1 x4oJԢEdfSv\Zib'<6w؂ͺE0qN۷ohmg} K-%}z2mǗL߇m;bTny|v ;,3בi-,~^4 ^Ntx fr>#kafs W|ު 3YlM±9ˏ[n"mzb='MVG#3+,f]GljmkN*]U--QQQ,^҇b]$?IɊ7<0֞,݇IKWqpu\3ū0`{`DOfYRe)v]_$]^|L_Sm=~MU@9DF6MmJJHj5@lXU jfo"V+b(KG[8T}~wŋt҅];#7"׵ eTw09~8F;d#ccmҒA=<~*؃͠yغ}z9 2'"5w)wg\BK gO8q;O"qш>}KbI]0u8HH*h\~9Xe(u(* T j*&Uh`}aZ1) fYbR,dU{{ P:ned6DٳgW9sd.(P4]jUEl]%IQ֋lQ0Y,-VkPv@G\TJ@@VΊf;G~]s쮑(VKdaLXB{d̦d_bрnNCs\vΞY?EQ< lj>zͣCs,e3a6F1/=Ǣ]AfՎ]mO&vhO&|{XOIjr#fMLIdjP`Rl8GTX1^'$iQM4i;w$ȲLxk9x re2@u$''T&ůfɥN̆Ef=ѵTY21k5S$7lzYiJ"Sh*E"‘3/r05OS%M}} 9]_U1 qhY';Iz۫ d"ҵkZQd2c40,&d]q{Dn~EEFF&gz% C͊f)DShB4 l,5&TmP|,?X67?݄9nVEs/!#z+C{2$ŸčOwk<2@56=kY:,5ďdGX<7q)Or96;s7N@%g%KY{`棘mm z7lYu*M|}tsMP<_)F%dY]]xat,Kt&wU_Dф͚ KwIDAT&l:SJw68MҡY @ҡPM`5ڪӜ@bD뷳tX1붕x|pƮ+U=5~U q"j{Ͷm.YVͱJ6UI1mgtxEGD:TЫ =Wgŝ9eP@O E r1kH-ۃ.횃&Ff\~!:YBɤt$ۇYqC[ѐPKC\:]E_m+\@hjdw7\ځH h,%$ MIՒ)Z_A (9Z&0p,bⱕ[X>cW%?">5,ֻld=LOgΗ_~Yjh\g<&?"^EA\MBqEg٢ "}TmuX5%i߄zWsmPvHXm/ 䦐hقcE?w?!sXz?rrjӝxq%2*,4g`j;k.@ (sմo74BTK`6rMɓ'ߤ30i87dHW'?2q Ičd\VGG*e{D۷'uV3)g]PFon0LpUE'$~іZs5NϟHbKOS%RߟZq^#OXh롨 t{~aC$F 9 Q_UIӧ#;m۶vj'z`dKjG ..hP-yH"f4tpQ--SÔW&~Tod'k' fD{ڰ1yoY>-kєڻD찞p++ˬ Y5a0 ?U=^`*6uՊ`gϧӴEky@[B{ֳw޹HΝR p=N 4$lOpr5ٲh^(^lVgG'D>x9֬ȊSxjA,k‹ndݤݘ},z,kodL_B6LT=*k3 ꮍȻYu>qy!h##44.ww־ٳVkE+^\f!ϙy7gd_dO\1ݠZurOʵ^ o8:~j? ᧠>q'?U@##Kn?h@TDSF NE@ 8JGsAgAu{Ǐ(KMeW&AYDՙ@ 4@ p*"8l9!W߀[:vb@.F /aP@ #h@TDSF NE@ 8h@T:泭,Kxsm js<y9sw>q0?22ұ*V({sѸq8 7m!|v U$=׊*9Y*lNv&߆`0,RSO];7`-[]έR p؟$mވ-Z[V\}Ϯ*>^eY& 87u?SPPPnٿ;ke)' U\TUC%ZkC2>[!:7k5T8t#v g |v9" &F |v gS/*OC6k>Qu&* 5|nҬiN| ]?sCM=>gnk}/֙V;#G?ms}(G~T&׶(?;bi||+;'rc[hzE!e۶:g>esW5*"8 @" kHBîctuo" 8N[ n_'`Ȱt֕{{nݺ2dN?ql ?#C Y?.|N=C_ۯ_ګIN^~ǪhqiV+G~=UU4%fO'(UUI=q`޽~`1[n,KuY;`^޲eM3*mIg:,dB8F7>>ތ1 㘿%ڴ~{7bx]+ˬmΛ11<:z$p[o!~b{ԓ n m1j4ϟoڼ%k>wK7УW$>b13p#˖Y o=uuLW|_ xմyKrwp}v< L&cW)7I=y̽?]cU5ΦIF*VE!)ѹvv7lC|8s&`r ˖fd%$$MHKKsTObwӫO~,\uqQk=DEbzTT_-vgɇ+Wp=w׹pڽg}DVXfDO?w֭ZʜWK޽{[6oC?ҽ[W}{ rܞotiE,_~ad~_<<t{x.I󴻱4gS44%}={8pgfС(BFFm߄/y %Y#_{z"l-[l*"r-M1xy}YK}MpЕ}ԴIS/rM7J^%;"Ar:ၯ/3g>={Ky `03_~=lys"͙53畗6lī_!<, ooo9Owݝl4k$6nb+/Wګ9Lpp!!ye5aÃb`XoŬsX$a6p0{1:X,NYoq6QXO?~d&'"$I*YE>wy?K֌ΝٶSƌUn)Ӟp{񦰰:HmZHp0NѲE+JƏ@pmV>Vūӧмmm(,00_D4,ԓ 'B硓e}|ϧqP~##-cGكFMF.dfɄ4<T#tnd?+B5mDV$bMPPPXi'k}]s/Ėu͚-n oDNnfC?̔'˘L& xxs:-Y/.őG7"\<K9yVɓO?[WSyU !+;L^k.Dֻ$f4M5%L:?#D:b陳 r>uߎd2aٵ &7`j;tDTp2+\,aaϗ(wjYu-cur{7N:a9-[,SڦogCfVƍS>hтu߼;!"#_Rfhܷ^獷6m:aa7;?-ő.۴'3MxbDHJF=͹縮eK&OĺYUNѳw كS&]e;u@#I:6ƛ:aTTHa~.RuBpu7jԈaxyD-Fӡ( y=iܸ1-Z?$U6i҄۴I8~Rԃm6$,̱b}~DU|nժ_zzԴÇ/iizw=/Ug Pt6p$t g |v=q@ 8M5MMTMC+VdYBUUdTsӡDbUtn!|9gPQε"dۨƒ/lUCG%\܋* :ʶ-tyaBC U)ȽHʎ]3_mpag!^>~5CPѺ͜<;rPUͅJ6Uyt_}FFF:V5]Mй .~MS᳓>\Mε&0xz}ղAxz.|ATڶIj |v gPK04nܸVDoo*ﴠ]5],"@ 4@ p*2XX:@7Ev~)2'^>~uE 23x|w.5 oC">i]̤ N ᓙFLM_}#AǴu}> 9/wó.t 3Ayh;\@PeݻU'IENDB`././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/docs/source/tutorials/images/traits_thread.png0000644000175100001730000004702000000000000025256 0ustar00runnerdocker00000000000000PNG  IHDR0y٩sBIT|dtEXtCREATORgnome-panel-screenshot7w IDATxy\Tϰ8 K"g.Y 4"K4nfY^ʲŮͬ%f.h҂" °"̜ "8}:yy0_suxJ]B!ZG;joV~vG5*[֋N!E_IIY9⬶mcwWbF å"e0\& !BԨut|v*}8cUkKft-2I\BkiǮI'ک,)7τP>qDePZa`У+ΗE! c'ӿgP{Tr+QwpTBB-Feq Lv:jߧ5k!Fd͏┄Ot7d;zɽ+*WL z]6 a-fFZ\!c#ŀ^WIeE1?ovԶPz9PRRq6 FX-fNEu|'Q}z]]sFf0tTZ`0?k',sʨj痌2 ꬫ:7TJ*N5Ԙ1™h΁.}ֵFezT*666&4T[B\azOSLMM_?UU6csLfm`WXTlU^:0ߎ^WɲUbv_K9z2ΪGoᡑUlIH5Y&MT<Sp}{r@q1$w-'S8`ɡCҷoߪ Azպ?f1/: c{ӸSq,]m'>'ũCx~~ogЧlD;Wt;~r6>`"R>`naӸ+)ߑSPF*: ǭx]:+-7$PƁ7Ʊ0 9 ϡSQ{BE_)8%G!'r*Q 5ӎ^WiQ}mJ;qSGL[d'Mq"J.(+?ǥ$oUQ{Qi̛7U K#n zzCx Iݼ2.X d/p =5`:zOEk'ge 5"ܵ5.;g=̎#g9_f_/s1{>Ń?b\O}J >n^`i<:+g萹U]>|F/6:]#o,}O6c&ǩqzP}HKB_>gD_ܔکN,MU[w_K۹foMc_ĥCXT cpO^xh0(w ǡJZwӮ];~)1kp/Byz{94|.毋ٝm {LOwE-5$~yݦw֣<ϐ@g*tc,6*?mېpfqLWJ;C9ڃ`V4@7qT5bMo {Gtz =h_r]jB\5Su?ϮmΌX;=Xq=Ѓdvt5K?wЉS ;jBxJ󷁽-\}oܯ:΁yގػxfp.*z5;gpi\ՆMQBO/[O-;IE{N;W@H3xxzq4>76z;{gM9]B;qsmxz7m`0p!8iqrRsKqj!唔8W\ޢjuTVNIXƶSۧv;*j$0*&ȵU=CmAgOP١dqi`I  C`~)5])z]Тv /oC'Fo{Ť Ϫ6rsR^yɶj;[҆mǔ;g`}b,ǏEp'͵<}e=̗/oԫ֯&gӗ\q9A/ eSNx >OW?%d͜gﵴoO5X[]"+uޖ::Wp$}ϭGfwu|m+j{jQPڃ[if}N7¿_ǥʫyUai-iۃ w&s)g.<= 7J +Woa_ӼAfUTWCT?4,kn;1M^捯RuʼAuSUg.ʦ?-SGz>t}8w]n'~wׁS:ҥKu aM:˷{3f2n 6Ra{[z޻1&>gKEqm[Ew/G;#rt~x&ELb>dPc߆=?1p̟a+x&qUxî>[F~:1RSXˠRHqGwu)DRU}8וj[Yᆰ+;Kک/YjvbqD%f|_nvF_CUR]<ˎLjH K)dƹӁx8Ҿ}{KDD:γ1[Dol?gIz%*t [B>=']WOX=3ۯq:;2 )`oC#7#6?@YzΞBQh.nvaԷdSZFEJlauPTٹ=SR=*і=W5`fvC'OUL1sπ+, 430zf`xy Wr o*R ::wvgӧ䯡c' ϯjXf` ہr3t+cB칻3'(P0`b|oW.]FeRF ~\X_}|\ݎK`P٪iU|z3>̅s:.U*( ۪#]u^RQZ7 ;a_g_s-icn;.UN~^!~TP ԋgNMBH%1njPR K͚q<y%zbGv"{~gk\FkT5g`;T.nL5ӡA=zQQQŋi׮&g 4wձx_FB+lT*~M"l-Z/Y&7Z;6XxoCڵkW(9$DK1wHBT:ʟww~޴m~R g\L4+BrJ<ԪKeuLB!MB[!Ba1;MO#_B!z:/|'Bq]F!VG!BXI`Bau$B!ՑF!VG!BXv}q[ B\/lN 66*4.tӧIIc3uB!uiop!(;z`SMa s(vpuCHB!h}-l<:՛@ LNt Ԟ$0↡RBq899ڱQ8vɡII#BfSlyFJմTDF:~8> j ֭[:kJf6B4]ee+ L#2j(nvN8AQQo&[liЄB` ߸PXH-Z$0… 6 /_<2228vf&eQxd]63-Rjİ!J9o?]_|󰷷 ;;/SZZJ׮]fqww7k4ֹRyx?;w$$$VK߾}3**gcb@Ϗ r}gRRLΝ;On݀sFMffq8qG̙37g___н{Bqm Ԏ {zg888]K/Ykh33pww7Tۿ?#GJF4NT\錯 5&77פɋ%qgk.۶mZ@렠 Z6灁Wª>_BCqq9W-KsHcƌiɓ'MVVf i<(PQK1h Kxwy'ˋt봴4LY dzBqcbccYf Ν)++ә5kV۝3gg&55NGRRR46H?NEEd͍dz!ϟO^^<3L2dcy󈌌>GFF2|˫!7I`[n||wѾ}{̙ɓlڴ ̘1c5jT]d Çg899ĉݧqL0{Fŋ?i,[h 2K/DN "88___VZeR# # OOOVXaqyC}^r%ѻwoFYoBX$^ T+,W׉)Ba=Om+wB!D=z^z`|yRk}_׼ШBB!ՑF\ GB!C!BXI`Bau$^qÓo< !DHIE1 PJU/' 5$B!H1 s=`|^.rkn6pww'"",KrE͋ !% L#,XVKFF={4 B!Z$0Gxx8pB9bv* 6Z&,,6oL=Pߟ$>iii₃ƍ3csYYSNEڵkM,]OOOŋf㭬dt 777֭[gq,*5kFaƌCscn:t邍E}*//g&c"# L~zFm>qqq޽BL„ ؾ};qqq2qDۇCnn.ZP.["N:űcHHH0io>|DZ-,^l|+V ))D8uű$$$DZZW]9irȑ#;Q7ԧ˗GFFǎ#>>Q !r/$ ԾR,'{%88jīRR4U\]]RvJ^^$$$ɓ'֭MvIHHZ}̜ԥKѣGcR;JEJJq,N8ѣ9sUcQgc,oOHII{r>B\C[?xߏ0 'R0? rI8C)\O 6.O/Zfq[B&P"Νˌ3,ޯ:QprrsN3޿?#GJFXC׮]M"44xۛ\eggTgYCMf )''$υBy޿E\\\X`fmLy{{Q{LBsV^/\YΞ&73]-" L#<#ot:y:t5k HOOg֬Y&,X|?I9s={6t:0޴iӘ;w.\pg}Xbbb#77yi,sss#99d~ꫯRZZJVVsipLSdd$7ItB'3hVu!Y:z<}?;. L#w}L2GGGGAA[lfmڴ ̘1c5jIUh4ѻwo|ɒ% >ѣGDdd$'N4^ll,ۗ|}}-`Ĉ'+V0-ZA|hƍl۶ WWWZg5ԧ+Wn#G6XBw!I}*99Ǜ= t-U⏫x-Caa!Z 2iҤI!CH~LL!mZB9t %=Mttt[!B(.:A_- z=P-j30B[)!Bk}30ՉIr̜;Y!⏣갑b|^=S?`@ZbҾE!U''eeeg3۹sK4!D7Y!$GGGzd?yƲv'ҽgͿ>d Ǡ! ݟEKLyEE>sY?I ՗i3f<1>;;[ŋrH- !V-?Mf,bbKC|I$o\:̜>8AA|e76_|ťI};v,xxx0uTcT Ij΅ ,S;ͥ n^0)]t vٟ/obj\\\Xl U%Mɉ@ƩS'$0}ڣ@y׈!??:0eʔF#B^HyyYͿÞ}{$vפKKu%?=3= [(Sx^~|ILugxinN/G3Y 0塇߱F1..ݻwSXXȔ)S0a۷o'..B&NHTTqpbbbEjrse˖QTTĩS8v &^ÇVёŋoŊ$%%HZZɝ !V IDAT!$&66r(эq!9b<P/_N^^;vF'e\CH6DQE)nҡC3U==W V~NEuZ֏&>prJJ/+K/S:x@y)&y>P:t蠼+9rD R?~UY5y~~uIIIWRRxxx_wYIMM5NII1ik׮n|xyyWINN6[^_,b|}qפٳgMSΝM8qD!h9rJiqR|P)*WhsJ~N{s&M>uRJ?9rDrᓍ;6DiQt ERHW??FoNun?ۈzIdfϺbR>5>7~z־U/“sQ3~j3[޽{O?ݻ[$'':t:h")))LеkWk 5U}'fggTgYC\ 49T>>>&SNNI5 !q4;dؚۅ{cv9g1g}팾vȇ#L^/y)L mٲ _0p&aɓ'ꫯr]wѾ}{i߾ˋSN@FF>>>tZ<>>>ՙ5 @zz:gggGii1+(\$c"p`0?3LwԹn:-ZĮ]yU':88@zz:f&u,X|?I9s={6t:0Mj6ms%33 .Z T;//\͛Gdd͍dǫJii)YYY̙31iO̟?8&BqcaF־ Z`OgϞoԨT*[6mĂ pvvf̘15ʤ|ժUUow :{{{c%K>|8GɉH&NhXBCC۷/6XC1043E4hឍ7m6\]]:thuPV\qLF`B!jİ!J9nT3u JNNf񤥥zr~!~<ɏBQ _ͨzn~VW--mfQ1-s/$vbbbX|9,\I&uHB!B1.'(c(RkrӴ+߰( !88:B!nT2c墣n0BV%30B!:!!$qÓoH !DXֻ$0א|p !#3f vxoդR#ռ]C$y’B!,% L#]B!ڊ$0PYJņ  DVFBB7oGjORRq4qqqqƙṬShfڵ&mz.]'NNNDDDpEVVV2|:uꄛ֭8JŚ5kB0c MơU]2׭[G.]hOL>dLBx$iEqqq޽BL„ ؾ};qqq2qDۇCnn.ZP.["N:űcHHH0io>|DZ-,^l|+V ))D8uű$$$DZZW5eСC9r`Q/_N^^;vF':Ƚ,P׽,9A6*|(--E\Օ:+--k׮KBB>GJJ ݻw~Bq U '>hW If`ZQuT:Ng|~F3* FCAA<'']_EhhЍ7f&((βb0> 4(5FSNNI5 !qHsyyy̟?ߤ|Μ9̞=Tt:IIIDDDmoڴi̝;L.\>kq,Pug켼EFF2|>OG!čAFMKT6mb8;;3fFeRj*4 ~~~ݛCbooo,_d Çg899ĉͶKhh(}% ___c1baaairgE1h ڸq#۶mՕCYgm iʕdȑ )I$Qrr2Ǐ7{Z !+-&&BZ- .dҤImBqɽ\pp0!!!rB!ZS\\\% &::ÐGB!X ŀb0(rprƿk:!$!BXI`Bau$VZ~u]!uF!VGF:~8> j ֭[:kf>Bqppp0rp{-ZndooOYYvv_]T@n,h4WsuuvIHHZ}gc׮] 8???m~֗PYڷk>Bq}u oTor[4˜1c>>dff(b y;Cz6I`,t*#]I [;:!:)ll jG49dG*ɋBDvvG>[JJJT$0ķKWEAQ:MQQQۥ+9ٙ7sY5b%aiˋKyq:16 H!r/KԓxmfQ1rL( ?d|.BM}lzh3I UIf8!! Lc\͛>^!iII`:cLmBa}2.7} 30B!DL>sں7(--%GfGdw5;kP1>6}& .^ȫw";'gggiS6l(~dNov[ͭh+Gosi""2t`n'ǎbj(UG~`|.,ִ<\*++H={X+B5k_YW^7 Q3yqs뀺];o7nsf=$$W_͘E~~<[(yՇC/=nzÄ8~q^ϚWq[ \/[(CG"{OޯF3iqYdieߏ?sK⋭ gx>o@k%}\~^ߩӧ>sC{=Gg,gt4[OqX3dpGyy9,!WoBzfsIom@# ^*++Xj}gl|F\[k,jGPu!<\JJgIY|5^7 ̾} g|zIfNF}ܿ V6{c˿χp߽0}|xM|w?/7n;8^j5/U:%_~Nډ,)EAQ6},Xr1?|8Urr= /NuoOصVk7kArqv|%~'3+Q}6זkk]qT6IŽშj^\J,"KSi3f7Wڛߓ`׈]t[GY?XڻUzDI 5O``_4yLԩq4ŋKY5G亄h+EEE=>T]{?)t!FsA>ВzΜg3v:tIY]|Pn7#iq߿+iiD<ﭳ[ʿ>V]7qzH#cyy?ă?5׻-WuЁzGfoRL빜888ԹN] Tq3?%q4EyYաw|\k9<=EQHMK61{|j|]ǯn򞪹U5>ysk]XWj5˟_0~A^:{{_P`W盍#7/oO:-sCmUoCjcV˨wl_;!IOކ7] ̰aCxlm -YĨ#pvv~77+N<|<-ZQ* j-T*Xy2˥&>,zF YCii)}oR>]ѧwo6]^?qrjک՜ds˚#G8u4:'NcIM9WZMsYd%rtٹ =[7mƵ}{3M+//AڡYYo>IسFI7}86k_YK/yd ysfGq:}LBƎS>"**@EԜeŚgFNϾ7PTt7׿nfvLAPHo5/5k3`0SG1ATڪF\z5k_^wgccǎ~xn;oo?I7V] v{?%if8!:͈z{  L#.'0gFI+B4ZK}~Z5jq$/B!DL"Hc![;[t(gFɣ<ʣ<ʣa8"#.Igqn2έDu8V')M(2*d[skFZsqn2̭Cƹu8?!{~#: X8[X[m;w'LFkVcRdp0?ð1X8[G賵ƍJ`=M*Mw_(--m밬4{FK>~GN[HO䑩H>~d~&8[ƍm[ԓDy^}ܝm>cr-꼞~kLZZc՜*7&{'Ssz=풑CS";ND;NIFzƕmkĚx<=QQmއSxO:t(?c +~e޽Ohӷ?:VKH0](xO998i2iʘxs*=v@/m`*3/N.gj6?s p7YSOyzj[qX6k'ÆAŅŋg>m^~i>8::QoƲ>U+Wp]n,x'"oo4 K/b߶l'@:> +c[,߹|y|:7kYeAjvuA gۛyVLQg`~eR'$9kYe  ص5\|n?>2-^@nakZ扠Nguׯ\ >@-V+,{2hz{C^{zw$T( nGOAo^rd_O(..gΡ E_ JeFe(Ԡh9:;/cvYqk%5$cU8v^}>x #ӫ xu,]̴։Il {cwsˋL5\.VV%\]<f JC? <í-}F^de6BQѴ#{z_z=Οo\@kk+&|>!rqy 4=hzNN.]<Q14$<]e2331p`vVKnɏ)ĒiV ׃^ yp9>/y9v;팄j\#p:p:8t7@cú|wk]Vo[ G9ll2r{qcph?_"Cte ~ܴ=pul[鎝vYp8kg 5mG{G|>W]K~qRyzKVc׫ݱf+;wk+JH83uTd."eFL&#f́j=0[nB&d2޹Xnƹs "rss!4Vthي/̫WZZzL@BhuC LQ˟aC;&zmw".;dA mo8X 4d:6w̯PRR2jn+*oau]=zm6y\\\?l_{^#_Vt:bWOr%d3tQNP2x\pzFqp_d LN KJ~[w㸈ˆ7??3g?UǡvG·7*).+}8ύ^ YSĪlr.--f5Kf钠gϣ3ݦHSiV4m2籶knL033𙠜٠٣ !"9>#qNoaQL9?y(g6ə>x<)牣و&&Z {h܋)ڈ~y(g6h*܆iEVv1`4SΓrf#G ^S ϗJB4enJaQD[.`diP(K9Ǐrf#srrqdqDzE Zws@7PΔd_|s_-ZX-'錸@$]zN{?Ÿ825@Cv6s|(g6bje||#Xš^C7U()I9OʙXs&~r&(8yr92Jq祜G9KγF~aʙ ʙXr)aW;'٠٠٠٠٢ۨ !"94!BB!#A+uB!DMvˡ$B!1Z-ذi N>sdD!2!' >$7V Ӟ!BZ0׋ƍ'9OJb"6iHOW&NB! @` W,ZXMO'BHJ8uLxx %OIENDB`././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/docs/source/tutorials/index.rst0000644000175100001730000000020700000000000022303 0ustar00runnerdocker00000000000000TraitsUI |version| Tutorials =================================== .. toctree:: :maxdepth: 3 traits_ui_scientific_app.rst ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/docs/source/tutorials/traits_ui_scientific_app.rst0000644000175100001730000011453400000000000026250 0ustar00runnerdocker00000000000000.. _writing-a-graphical-application-for-scientific-programming-using-traitsui: ==================================================================================== Writing a graphical application for scientific programming using TraitsUI |version| ==================================================================================== **A step by step guide for a non-programmer** :Author: Gael Varoquaux :License: BSD Building interactive Graphical User Interfaces (GUIs) is a hard problem, especially for somebody who has not had training in IT. TraitsUI is a python module that provides a great answer to this problem. I have found that I am incredibly productive when creating graphical application using traitsUI. However I had to learn a few new concepts and would like to lay them down together in order to make it easier for others to follow my footsteps. This document is intended to help a non-programmer to use traits and traitsUI to write an interactive graphical application. The reader is assumed to have some basic python scripting knowledge (see ref [#]_ for a basic introduction). Knowledge of numpy/scipy [#]_ helps understanding the data processing aspects of the examples, but may not be paramount. Some examples rely on matplotlib [#]_ . This document is **not** a replacement for user manuals and references of the different packages (traitsUI [#]_, scipy, matplotlib). It provides a "cookbook" approach, and not a reference. This tutorial provides step-by-step guide to building a medium-size application. The example chosen is an application used to do control of a camera, analysis of the retrieved data and display of the results. This tutorial focuses on building the general structure and flow-control of the application, and on the aspects specific to traitsUI programming. Interfacing with the hardware or processing the data is left aside. The tutorial progressively introduces the tools used, and in the end presents the skeleton of a real application that has been developed for real-time controlling of an experiment, monitoring through a camera, and processing the data. The tutorial goes into more and more intricate details that are necessary to build the final application. Each section is in itself independent of the following ones. The complete beginner trying to use this as an introduction should not expect to understand all the details in a first pass. The author's experience while working on several projects in various physics labs is that code tends to be created in an 'organic' way, by different people with various levels of qualification in computer development, and that it rapidly decays to a disorganized and hard-to-maintain code base. This tutorial tries to prevent this by building an application shaped for modularity and readability. From objects to dialogs using traitsUI -------------------------------------- Creating user interfaces directly through a toolkit is a time-consuming process. It is also a process that does not integrate well in the scientific-computing work-flow, as, during the elaboration of algorithms and data-flow, the objects that are represented in the GUI are likely to change often. Visual computing, where the programmer creates first a graphical interface and then writes the callbacks of the graphical objects, gives rise to a slow development cycle, as the work-flow is centered on the GUI, and not on the code. TraitsUI provides a beautiful answer to this problem by building graphical representations of an object. Traits and TraitsUI have their own manuals (`http://docs.enthought.com/traits/ `_) and the reader is encouraged to refer to these for more information. We will use TraitsUI for *all* our GUIs. This forces us to store all the data and parameters in objects, which is good programming style. The GUI thus reflects the structure of the code, which makes it easier to understand and extend. In this section we will focus on creating dialogs that allow the user to input parameters graphically in the program. Object-oriented programming ``````````````````````````` Software engineering is a difficult field. As programs grow they become harder and harder to grasp for the developer. This problem is not new and has sometimes been know as the "tar pit". Many attempts have been made to mitigate the difficulties. Most often they consist in finding useful abstractions that allow the developer to manipulate larger ideas, rather than their software implementation. Code re-use is paramount for good software development. It reduces the number of code-lines required to read and understand and allows to identify large operations in the code. Functions and procedures have been invented to avoid copy-and-pasting code, and hide the low-level details of an operation. Object-oriented programming allows yet more modularity and abstraction. Objects, attributes and methods ::::::::::::::::::::::::::::::: Suppose you want your program to manipulate geometric objects. You can teach the computer that a point is a set of 3 numbers, you can teach it how to rotate that point along a given axis. Now you want to use spheres too. With a bit more work your program has functions to create points, spheres, etc. It knows how to rotate them, to mirror them, to scale them. So in pure procedural programming you will have procedures to rotate, scale, mirror, each one of your objects. If you want to rotate an object you will first have to find its type, then apply the right procedure to rotate it. Object-oriented programming introduces a new abstraction: the `object`. It consists of both data (our 3 numbers, in the case of a point), and procedures that use and modify this data (e.g., rotations). The data entries are called "`attributes`" of the object and the procedures "`methods`". Thus with object oriented programming an object "knows" how to be rotated. A point object could be implemented in python with: .. literalinclude:: code_snippets/code_block0.py :language: python This code creates a *Point* class. Points objects can be created as `instances` of the Point class: .. code-block:: python >>> from numpy import pi >>> p = Point() >>> p.x = 1 >>> p.rotate_z(pi) >>> p.x -1.0 >>> p.y 1.2246467991473532e-16 When manipulating objects, the developer does not need to know the internal details of their procedures. As long as the object has a *rotate* method, the developer knows how to rotate it. **Note**: Beginners often use objects as structures: entities with several data fields useful to pass data around in a program. Objects are much more then that: they have methods. They are 'active' data structures that know how to modify themselves. Part of the point of object-oriented programming is that the object is responsible for modifying itself through its methods. The object therefore takes care of its internal logic and the consistency between its attributes. In python, dictionaries make great structures and are more suited for such a use than objects. Classes and inheritance ::::::::::::::::::::::: Suppose you have already created a *Point* class that tells your program what a point is, but that you also want some points to have a color. Instead of copy-and-pasting the *Point* class and adding a color attribute, you can define a new class *ColoredPoint* that inherits all of the *Point* class's methods and attributes: .. code-block:: python class ColoredPoint(Point): """ Colored 3D point """ color = "white" You do not have to implement rotation for the *ColoredPoint* class as it has been inherited from the *Point* class. This is one of the huge gains of object-oriented programming: objects are organized in classes and sub-classes, and method to manipulate objects are derived from the objects parent-ship: a *ColoredPoint* is only a special case of *Point*. This proves very handy on large projects. **Note**: To stress the differences between classes and their instances (objects), classes are usually named with capital letters, and objects only with lower case letters. An object and its representation ```````````````````````````````` Objects are code entities that can be easily pictured by the developer. The `TraitsUI` python module allows the user to edit objects attributes with dialogs that form a graphical representation of the object. In our example application, each process or experimental device is represented in the code as an object. These objects all inherit from the *HasTraits*, class which supports creating graphical representations of attributes. To be able to build the dialog, the *HasTraits* class enforces that the types of all the attributes are specified in the class definition. The *HasTraits* objects have a *configure_traits()* method that brings up a dialog to edit the objects' attributes specified in its class definition. Here we define a camera object (which, in our real world example, is a camera interfaced to python through the ctypes [#]_ module), and show how to open a dialog to edit its properties : .. literalinclude:: code_snippets/code_block1.py :language: python The *camera.configure_traits()* call in the above example opens a dialog that allows the user to modify the camera object's attributes: .. image:: images/code_block1.png This dialog forms a graphical representation of our camera object. We will see that it can be embedded in GUI panels to build more complex GUIs that allow us to control many objects. We will build our application around objects and their graphical representation, as this mapping of the code to the GUI helps the developer to understand the code. Displaying several objects in the same panel ```````````````````````````````````````````` We now know how to build a dialog from objects. If we want to build a complex application we are likely to have several objects, for instance one corresponding to the camera we want to control, and one describing the experiment that the camera monitors. We do not want to have to open a new dialog per object: this would force us to describe the GUI in terms of graphical objects, and not structural objects. We want the GUI to be a natural representation of our objects, and we want the Traits module to take care of that. The solution is to create a container object, that has as attributes the objects we want to represent. Playing with the `View` attribute of the object, we can control how the representation generated by Traits looks like (see the TraitsUI manual): .. literalinclude:: code_snippets/container.py :language: python The call to *configure_traits()* creates the following dialog, with the representation of the *Camera* object created is the last example on top, and the *Display* object below it: .. image:: images/container.png The *View* attribute of the *container* object has been tweaked to get the representation we are interested in: traitsUI is told to display the *camera* item with a *'custom'* style, which instructs it to display the representation of the object inside the current panel. The *'show_label'* argument is set to *False* as we do not want the name of the displayed object ('camera', for instance) to appear in the dialog. See the traitsUI manual for more details on this powerful feature. The *camera* and *display* objects are created during the call to the creator of the *container* object, and passed as its attributes immediately: *"container = Container(camera=Camera(), display=TextDisplay())"* Writing a "graphical script" ```````````````````````````` If you want to create an application that has a very linear flow, popping up dialogs when user input is required, like a "setup wizard" often used to install programs, you already have all the tools to do it. You can use object oriented programming to write your program, and call the objects *configure_traits* method each time you need user input. This might be an easy way to modify an existing script to make it more user friendly. ____ The following section will focus on making interactive programs, where the user uses the graphical interface to interact with it in a continuous way. From graphical to interactive ----------------------------- In an interactive application, the program responds to user interaction. This requires a slight paradigm shift in our programming methods. Object-oriented GUIs and event loops ```````````````````````````````````` In a GUI application, the order in which the different parts of the program are executed is imposed by the user, unlike in a numerical algorithm, for instance, where the developer chooses the order of execution of his program. An event loop allows the programmer to develop an application in which each user action triggers an event, by stacking the user created events on a queue, and processing them in the order in which the appeared. A complex GUI is made of a large numbers of graphical elements, called widgets (e.g., text boxes, check boxes, buttons, menus). Each of these widgets has specific behaviors associated with user interaction (modifying the content of a text box, clicking on a button, opening a menu). It is natural to use objects to represent the widgets, with their behavior being set in the object's methods. Dialogs populated with widgets are automatically created by `traitsUI` in the *configure_traits()* call. `traitsUI` allow the developer to not worry about widgets, but to deal only with objects and their attributes. This is a fabulous gain as the widgets no longer appear in the code, but only the attributes they are associated to. A *HasTraits* object has an *edit_traits()* method that creates a graphical panel to edit its attributes. This method creates and returns the panel, but does not start its event loop. The panel is not yet "alive", unlike with the *configure_traits()* method. Traits uses the Qt toolkit by default to create its widget. They can be turned live and displayed by starting a Qt application, and its main loop (ie event loop in Qt speech). .. literalinclude:: code_snippets/event_loop_qt.py :language: python There is a `similar example for wxPython <../_static/event_loop_wx.py>`_ and a `toolkit-independent example that uses Pyface <../_static/event_loop.py>`_ as well. The *Counter().edit_traits()* line creates a counter object and its representation, a dialog with one integer represented. However it does not display it until a Qt application is created, and its main loop is started. Usually it is not necessary to create the Qt application yourself, and to start its main loop, traits will do all this for you when the *.configure_traits()* method is called. Reactive programming ```````````````````` When the event loop is started, the program flow is no longer simply controlled by the code: the control is passed on to the event loop, and it processes events, until the user closes the GUI, and the event loop returns to the code. Interactions with objects generate events, and these events can be associated to callbacks, ie functions or methods processing the event. In a GUI, callbacks created by user-generated events are placed on an "event stack". The event loop processes each call on the event queue one after the other, thus emptying the event queue. The flow of the program is still sequential (two code blocks never run at the same time in an event loop), but the execution order is chosen by the user, and not by the developer. Defining callbacks for the modification of an attribute `foo` of a `HasTraits` object can be done be creating a method called `_foo_changed()`. Here is an example of a dialog with two textboxes, `input` and `output`. Each time `input` is modified, its content is duplicated to output. .. literalinclude:: code_snippets/echo_box.py :language: python Events that do not correspond to a modification of an attribute can be generated with a *Button* trait. The callback is then called *_foo_fired()*. Here is an example of an interactive `traitsUI` application using a button: .. literalinclude:: code_snippets/interactive.py :language: python Clicking on the button adds the *_add_one_fired()* method to the event queue, and this method gets executed as soon as the GUI is ready to handle it. Most of the time that is almost immediately. .. image:: images/interactive.png This programming pattern is called `reactive programming`: the objects react to the changes made to their attributes. In complex programs where the order of execution is hard to figure out, and bound to change, like some interactive data processing application, this pattern is extremely efficient. ____ Using *Button* traits and a clever set of objects interacting with each others, complex interactive applications can be built. These applications are governed by the events generated by the user, in contrast to script-like applications (batch programming). Executing a long operation in the event loop blocks the reactions of the user-interface, as other events callbacks are not processed as long as the long operation is not finished. In the next section we will see how we can execute several operations in the same time. Breaking the flow in multiple threads ------------------------------------- What are threads ? `````````````````` A standard python program executes in a sequential way. Consider the following code snippet : .. code-block:: python do_a() do_b() do_c() *do_b()* is not called until *do_a()* is finished. Even in event loops everything is sequential. In some situation this can be very limiting. Suppose we want to capture an image from a camera and that it is a very lengthy operation. Suppose also that no other operation in our program requires the capture to be complete. We would like to have a different "timeline" in which the camera capture instructions can happen in a sequential way, while the rest of the program continues in parallel. Threads are the solution to this problem: a thread is a portion of a program that can run concurrently with other portions of the program. Programming with threads is difficult as instructions are no longer executed in the order they are specified and the output of a program can vary from a run to another, depending on subtle timing issues. These problems are known as "race conditions" and to minimize them you should avoid accessing the same objects in different threads. Indeed if two different threads are modifying the same object at the same time, unexpected things can happen. Threads in python ````````````````` In python a thread can be implemented with a *Thread* object, from the threading [#]_ module. To create your own execution thread, subclass the *Thread* object and put the code that you want to run in a separate thread in its *run* method. You can start your thread using its *start* method: .. literalinclude:: code_snippets/thread_example.py :language: python The above code yields the following output:: Main thread done MyThread done Getting threads and the GUI event loop to play nice ``````````````````````````````````````````````````` Suppose you have a long-running job in a TraitsUI application. If you implement this job as an event placed on the event loop stack, it is going to freeze the event loop while running, and thus freeze the UI, as events will accumulate on the stack, but will not be processed as long as the long-running job is not done (remember, the event loop is sequential). To keep the UI responsive, a thread is the natural answer. Most likely you will want to display the results of your long-running job on the GUI. However, as usual with threads, one has to be careful not to trigger race-conditions. Naively manipulating the GUI objects in your thread will lead to race conditions, and unpredictable crash: suppose the GUI was repainting itself (due to a window move, for instance) when you modify it. In an application, if you start a thread, GUI event will still be processed by the GUI event loop. To avoid collisions between your thread and the event loop, the proper way of modifying a GUI object is to insert the modifications in the event loop, using the *GUI.invoke_later()* call. That way the GUI will apply your instructions when it has time. Recent versions of the TraitsUI module (post October 2006) propagate the changes you make to a *HasTraits* object to its representation in a thread-safe way. However it is important to have in mind that modifying an object with a graphical representation is likely to trigger race-conditions as it might be modified by the graphical toolkit while you are accessing it. Here is an example of code inserting the modification to traits objects by hand in the event loop: .. literalinclude:: code_snippets/traits_thread.py :language: python This creates an application with a button that starts or stop a continuous camera acquisition loop. .. image:: images/traits_thread.png When the "Start stop capture" button is pressed the *_start_stop_capture_fired* method is called. It checks to see if a *CaptureThread* is running or not. If none is running, it starts a new one. If one is running, it sets its *wants_abort* attribute to true. The thread checks every half a second to see if its attribute *wants_abort* has been set to true. If this is the case, it aborts. This is a simple way of ending the thread through a GUI event. ____ Using different threads lets the operations avoid blocking the user interface, while also staying responsive to other events. In the real-world application that serves as the basis of this tutorial, there are 2 threads and a GUI event loop. The first thread is an acquisition loop, during which the program loops, waiting for a image to be captured on the camera (the camera is controlled by external signals). Once the image is captured and transfered to the computer, the acquisition thread saves it to the disk and spawns a thread to process the data, then returns to waiting for new data while the processing thread processes the data. Once the processing thread is done, it displays its results (by inserting the display events in the GUI event loop) and dies. The acquisition thread refuses to spawn a new processing thread if there still is one running. This makes sure that data is never lost, no matter how long the processing might be. There are thus up to 3 set of instructions running concurrently: the GUI event loop, responding to user-generated events, the acquisition loop, responding to hardware-generated events, and the processing jobs, doing the numerical intensive work. In the next section we are going to see how to add a home-made element to traits, in order to add new possibilities to our application. Extending TraitsUI: Adding a matplotlib figure to our application ----------------------------------------------------------------- This section gives a few guidelines on how to build your own traits editor. A traits editor is the view associated with a trait that allows the user to graphically edit its value. We can twist a bit the notion and simply use it to graphically represent the attribute. This section involves a bit of `wxPython` code that may be hard to understand if you do not know `wxPython`, but it will bring a lot of power and flexibility to how you use traits. The reason it appears in this tutorial is that I wanted to insert a matplotlib in my `traitsUI` application. It is not necessary to fully understand the code of this section to be able to read on. I should stress that there already exists a plotting module that provides traits editors for plotting, and that is very well integrated with traits: chaco [#]_. Making a `traits` editor from a MatPlotLib plot ``````````````````````````````````````````````` To use traits, the developer does not need to know its internals. However traits does not provide an editor for every need. If we want to insert a powerful tool for plotting we have to get our hands a bit dirty and create our own traits editor. This involves some `wxPython` coding, as we need to translate a `wxPython` object to a traits editor by providing the corresponding API (i.e. the standard way of building a `traits` editor), so that the `traits` framework will know how to create the editor. Traits editor are created by an editor factory that instantiates an editor class and passes it the object that the editor represents in its *value* attribute. It calls the editor *init()* method to create the *wx* widget. Here we create a wx figure canvas from a matplotlib figure using the matplotlib wx backend. Instead of displaying this widget, we set its control as the *control* attribute of the editor. TraitsUI takes care of displaying and positioning the editor. .. literalinclude:: code_snippets/mpl_figure_editor.py :language: python This code first creates a traitsUI editor for a matplotlib figure, and then a small dialog to illustrate how it works: .. image:: images/mpl_figure_editor.png The matplotlib figure traits editor created in the above example can be imported in a traitsUI application and combined with the power of traits. This editor allows you to insert a matplotlib figure in a traitsUI dialog. It can be modified using reactive programming, as demonstrated in section 3 of this tutorial. However, once the dialog is up and running, you have to call *self.figure.canvas.draw()* to update the canvas if you made modifications to the figure. The matplotlib user guide [3]_ details how this object can be used for plotting. Putting it all together: a sample application --------------------------------------------- The real world problem that motivated the writing of this tutorial is an application that retrieves data from a camera, processes it and displays results and controls to the user. We now have all the tools to build such an application. This section gives the code of a skeleton of this application. This application actually controls a camera on a physics experiment (Bose-Einstein condensation), at the university of Toronto. The reason I am providing this code is to give an example to study of how a full-blown application can be built. For more information on the example, see the `archived page `_. * The camera will be built as an object. Its real attributes (exposure time, gain...) will be represented as the object's attributes, and exposed through traitsUI. * The continuous acquisition/processing/user-interaction will be handled by appropriate threads, as discussed in section 2.3. * The plotting of the results will be done through the MPLWidget object. The imports ``````````` The MPLFigureEditor is imported from the last example. .. code-block:: python from threading import Thread from time import sleep from traits.api import * from traitsui.api import View, Item, Group, HSplit, Handler from traitsui.menu import NoButtons from mpl_figure_editor import MPLFigureEditor from matplotlib.figure import Figure from scipy import * import wx User interface objects `````````````````````` These objects store information for the program to interact with the user via traitsUI. .. code-block:: python class Experiment(HasTraits): """ Object that contains the parameters that control the experiment, modified by the user. """ width = Float(30, label="Width", desc="width of the cloud") x = Float(50, label="X", desc="X position of the center") y = Float(50, label="Y", desc="Y position of the center") class Results(HasTraits): """ Object used to display the results. """ width = Float(30, label="Width", desc="width of the cloud") x = Float(50, label="X", desc="X position of the center") y = Float(50, label="Y", desc="Y position of the center") view = View( Item('width', style='readonly'), Item('x', style='readonly'), Item('y', style='readonly'), ) The camera object also is a real object, and not only a data structure: it has a method to acquire an image (or in our case simulate acquiring), using its attributes as parameters for the acquisition. .. code-block:: python class Camera(HasTraits): """ Camera objects. Implements both the camera parameters controls, and the picture acquisition. """ exposure = Float(1, label="Exposure", desc="exposure, in ms") gain = Enum(1, 2, 3, label="Gain", desc="gain") def acquire(self, experiment): X, Y = indices((100, 100)) Z = exp(-((X-experiment.x)**2+(Y-experiment.y)**2)/experiment.width**2) Z += 1-2*rand(100,100) Z *= self.exposure Z[Z>2] = 2 Z = Z**self.gain return(Z) Threads and flow control ```````````````````````` There are three threads in this application: * The GUI event loop, the only thread running at the start of the program. * The acquisition thread, started through the GUI. This thread is an infinite loop that waits for the camera to be triggered, retrieves the images, displays them, and spawns the processing thread for each image received. * The processing thread, started by the acquisition thread. This thread is responsible for the numerical intensive work of the application. It processes the data and displays the results. It dies when it is done. One processing thread runs per shot acquired on the camera, but to avoid accumulation of threads in the case that the processing takes longer than the time lapse between two images, the acquisition thread checks that the processing thread is done before spawning a new one. .. code-block:: python def process(image, results_obj): """ Function called to do the processing """ X, Y = indices(image.shape) x = sum(X*image)/sum(image) y = sum(Y*image)/sum(image) width = sqrt(abs(sum(((X-x)**2+(Y-y)**2)*image)/sum(image))) results_obj.x = x results_obj.y = y results_obj.width = width class AcquisitionThread(Thread): """ Acquisition loop. This is the worker thread that retrieves images from the camera, displays them, and spawns the processing job. """ wants_abort = False def process(self, image): """ Spawns the processing job. """ try: if self.processing_job.isAlive(): self.display("Processing too slow") return except AttributeError: pass self.processing_job = Thread(target=process, args=(image, self.results)) self.processing_job.start() def run(self): """ Runs the acquisition loop. """ self.display('Camera started') n_img = 0 while not self.wants_abort: n_img += 1 img =self.acquire(self.experiment) self.display('%d image captured' % n_img) self.image_show(img) self.process(img) sleep(1) self.display('Camera stopped') The GUI elements ```````````````` The GUI of this application is separated in two (and thus created by a sub-class of SplitApplicationWindow). On the left a plotting area, made of an MPL figure and its editor, displays the images acquired by the camera. On the right a panel hosts the TraitsUI representation of a ControlPanel object. This object is mainly a container for our other objects, but it also has an Button for starting or stopping the acquisition, and a string (represented by a textbox) to display information on the acquisition process. The view attribute is tweaked to produce a pleasant and usable dialog. Tabs are used to help the display to be light and clear. .. code-block:: python class ControlPanel(HasTraits): """ This object is the core of the traitsUI interface. Its view is the right panel of the application, and it hosts the method for interaction between the objects and the GUI. """ experiment = Instance(Experiment, ()) camera = Instance(Camera, ()) figure = Instance(Figure) results = Instance(Results, ()) start_stop_acquisition = Button("Start/Stop acquisition") results_string = String() acquisition_thread = Instance(AcquisitionThread) view = View(Group( Group( Item('start_stop_acquisition', show_label=False ), Item('results_string',show_label=False, springy=True, style='custom' ), label="Control", dock='tab',), Group( Group( Item('experiment', style='custom', show_label=False), label="Input",), Group( Item('results', style='custom', show_label=False), label="Results",), label='Experiment', dock="tab"), Item('camera', style='custom', show_label=False, dock="tab"), layout='tabbed'), ) def _start_stop_acquisition_fired(self): """ Callback of the "start stop acquisition" button. This starts the acquisition thread, or kills it. """ if self.acquisition_thread and self.acquisition_thread.isAlive(): self.acquisition_thread.wants_abort = True else: self.acquisition_thread = AcquisitionThread() self.acquisition_thread.display = self.add_line self.acquisition_thread.acquire = self.camera.acquire self.acquisition_thread.experiment = self.experiment self.acquisition_thread.image_show = self.image_show self.acquisition_thread.results = self.results self.acquisition_thread.start() def add_line(self, string): """ Adds a line to the textbox display. """ self.results_string = (string + "\n" + self.results_string)[0:1000] def image_show(self, image): """ Plots an image on the canvas in a thread safe way. """ self.figure.axes[0].images=[] self.figure.axes[0].imshow(image, aspect='auto') wx.CallAfter(self.figure.canvas.draw) class MainWindowHandler(Handler): def close(self, info, is_OK): if ( info.object.panel.acquisition_thread and info.object.panel.acquisition_thread.isAlive() ): info.object.panel.acquisition_thread.wants_abort = True while info.object.panel.acquisition_thread.isAlive(): sleep(0.1) wx.Yield() return True class MainWindow(HasTraits): """ The main window, here go the instructions to create and destroy the application. """ figure = Instance(Figure) panel = Instance(ControlPanel) def _figure_default(self): figure = Figure() figure.add_axes([0.05, 0.04, 0.9, 0.92]) return figure def _panel_default(self): return ControlPanel(figure=self.figure) view = View(HSplit(Item('figure', editor=MPLFigureEditor(), dock='vertical'), Item('panel', style="custom"), show_labels=False, ), resizable=True, height=0.75, width=0.75, handler=MainWindowHandler(), buttons=NoButtons) if __name__ == '__main__': MainWindow().configure_traits() When the acquisition loop is created and running, the mock camera object produces noisy gaussian images, and the processing code estimates the parameters of the gaussian. Here are screenshots of the three different tabs of the application: .. image:: images/application1.png .. image:: images/application2.png .. image:: images/application3.png ____ **Conclusion** I have summarized here all what most scientists need to learn in order to be able to start building applications with traitsUI. Using the traitsUI module to its full power requires you to move away from the procedural type of programming most scientists are used to, and think more in terms of objects and flow of information and control between them. I have found that this paradigm shift, although a bit hard, has been incredibly rewarding in terms of my own productivity and my ability to write compact and readable code. Good luck! ____ **Acknowledgments** I would like to thank the people on the enthought-dev mailing-list, especially Prabhu Ramachandran and David Morrill, for all the help they gave me, and Janet Swisher for reviewing this document. Big thanks go to enthought for developing the traits and traitsUI modules, and making them open-source. Finally the python, the numpy, and the matplotlib community deserve many thanks for both writing such great software, and being so helpful on the mailing lists. ____ **References** .. [#] python tutorial: `http://docs.python.org/tut/tut.html `_ .. [#] The scipy website: `http://www.scipy.org `_ .. [#] The matplotlib website: `https://matplotlib.org `_ .. [#] The traits and traitsUI user guide: `http://docs.enthought.com/traits `_ .. [#] ctypes: `https://docs.python.org/3/library/ctypes.html `_ .. [#] threading: `http://docs.python.org/lib/module-threading.html `_ .. [#] chaco: `http://docs.enthought.com/chaco/ `_ .. vim:spell:spelllang=en_us ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/docs/traits_ui.ppt0000644000175100001730000721600000000000000017653 0ustar00runnerdocker00000000000000ࡱ>    {F؛ Rz 8ݎJFIF``ExifMM*bj(1r2i``Adobe Photoshop 7.02004:08:31 17:31:47jS(&HHJFIFHH Adobe_CMAdobed            `"?   3!1AQa"q2B#$Rb34rC%Scs5&DTdE£t6UeuF'Vfv7GWgw5!1AQaq"2B#R3$brCScs4%&5DTdEU6teuFVfv'7GWgw ?o _yE/k~b+ؒRca|w߭_mW$?{0;o _yE/k~b+ؒKߟv_g/"?Ňֱw^?x=};b([}h% #b-_*o9#Q?IzH'شffi?vO^?}_oܰXGlt%!?d_}TI%)$IJI$RI$I%)$IJI$RI$TI%)$IJI$RI$I%)$IJI$RI$TI%)$IJI$RI$I%)$IJI$RI$TI%)$IJI$RI$I%)$IJI$RI$T?=x2.}7DIy}|2.?s{2C?7eЇr}#$o(ޔ߻dW}s>s5ޕ6>|nC؟}}^G֏mIvR^>Kqitײuc:l-k63{l~Neme6Xv}5Kr,czP/$~ϒFH9`4I n6Q$~!>a Photoshop 3.08BIM8BIM%F &Vڰw8BIM``8BIM&?8BIM 8BIM8BIM 8BIM 8BIM' 8BIMH/fflff/ff2Z5-8BIMp8BIM8BIM8BIM@@8BIM8BIMYSjblue_blue_gradientjSnullboundsObjcRct1Top longLeftlongBtomlongSRghtlongjslicesVlLsObjcslicesliceIDlonggroupIDlongoriginenum ESliceOrigin autoGeneratedTypeenum ESliceTypeImg boundsObjcRct1Top longLeftlongBtomlongSRghtlongjurlTEXTnullTEXTMsgeTEXTaltTagTEXTcellTextIsHTMLboolcellTextTEXT horzAlignenumESliceHorzAligndefault vertAlignenumESliceVertAligndefault bgColorTypeenumESliceBGColorTypeNone topOutsetlong leftOutsetlong bottomOutsetlong rightOutsetlong8BIM8BIM8BIM `JFIFHH Adobe_CMAdobed            `"?   3!1AQa"q2B#$Rb34rC%Scs5&DTdE£t6UeuF'Vfv7GWgw5!1AQaq"2B#R3$brCScs4%&5DTdEU6teuFVfv'7GWgw ?o _yE/k~b+ؒRca|w߭_mW$?{0;o _yE/k~b+ؒKߟv_g/"?Ňֱw^?x=};b([}h% #b-_*o9#Q?IzH'شffi?vO^?}_oܰXGlt%!?d_}TI%)$IJI$RI$I%)$IJI$RI$TI%)$IJI$RI$I%)$IJI$RI$TI%)$IJI$RI$I%)$IJI$RI$TI%)$IJI$RI$I%)$IJI$RI$T?=x2.}7DIy}|2.?s{2C?7eЇr}#$o(ޔ߻dW}s>s5ޕ6>|nC؟}}^G֏mIvR^>Kqitײuc:l-k63{l~Neme6Xv}5Kr,czP/$~ϒFH9`4I n6Q$~!>a8BIM!UAdobe PhotoshopAdobe Photoshop 7.08BIMHhttp://ns.adobe.com/xap/1.0/ adobe:docid:photoshop:7ede65d5-fb9d-11d8-b5b7-f5f4d0bff19a Adobed@Sj     u!"1A2# QBa$3Rqb%C&4r 5'S6DTsEF7Gc(UVWdte)8fu*9:HIJXYZghijvwxyzm!1"AQ2aqB#Rb3 $Cr4%ScD&5T6Ed' sFtUeuV7)(GWf8vgwHXhx9IYiy*:JZjz ?eu{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ]~s=s{z~?d5}OY}}:?؟ݺAǻ~^]e⟏{>}T笋#ݺYGk=|YGu u_uvy(=k:̿ꇬ>:0}}u_N/k݇T=eSz?[Gê/?Cꇬ:2}=uCeOY=PΨzο_v:3ePaP:e}zοYk}=ھ~}PӬz:aMuo:q}}pꆹ2{q:a:ο[Mu~::wPOT=g_Osu}M׏_wT=H_w6zο゚?vP~{}~u}wBǻCua}Gxyoqg׬:8{uzξ:l=fQ˪u~?T>H_vꧬ-|:_Ce{oY_qꇬo>z̿~T>}f_ϻuC_>T=g?U=e_uC}}۪p*Ӫ=۪/3AuS}ꦝe?ᄒNz?݇T }OqQ_OuSAh }g=Sb{Z=uSA??zYS~kuW\Oݺ\ueu{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ]~rO_{]kY>:?ȇ^׏{Ӭ)z?Ͻz?}1o?zʿTovX=e_OvT"}?O{0ažGYcU8YT*}nP-ֽTq}Pz?^:z̿}>z̿ϻz̿6}ϻy|0}ǗU>}fê/OӪYΨzξ:eaYqN:οv6z̿zο[Tx{t:aYOYE:lǻzο݇T4:}ߦzοO˪<:̿{ꇩ Ϭ:ꇩ #qY}ǻ:covuCuqOY~?ߦBqg?ꇬ_U>H_o{6|οnz̶`zY}ǻCu?q>:lgQS݇T>2|:[T9:mw]Pf_3ͽuCe}a:uva _x<ꧬ{#ݺ0}?޹q Ϭ?_݇T'=fw}PG_vTxnNg'/ϻzovu[|9a^e_~]T~=uSE>: }ݺ([qaOꧬz?MOv1֏Y˪1m]dcU?ˮc{Z뗽eu{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ]!G߇z?=u_Yu^ aG?uR8_aOY[v/V<ּʿ{z(6oaOϬz̿w{aYGﯻz?_uC^/OxuqYWqOYGǻzΣ${NoYݽ=z:?{T=f_vꇬǻ|:?e_ovum~γ˪o݇MN+Ywꇩ/ꇎzο/T=f_wz:[m݇Mԅ}'{{]P:݇T=g_kzο~>Ba"{ꇬqzο[݇MY{}P#ǷT>}g_ſ}a:η}!6_ǗT?>(xfg[??ORqgm>:e?zlumcêYn=fSczu=ߦY?qYuCu:̿::agU5U?޽ߪOagϬz?\݇T=f_aOY:YGêlut yoNqUogU?> vꧬ}=Píu}?Sc7תeYG}u^#_ݺ?[ݺu}>_゙kS1O۞ꧮb}kreu{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ]r>O\kS_}|}uSAa֏YݳOY׻PyS?{U }W*|??T=d힪z?=uSEa=۪oϻuOZu?:YGlm?pu~~1Y)}z2?vSe^>?Ǫ{vP?އS\텯uCa.z-P׬U=f_Ev6YSYuCeo)uquCua[݅=z:ǻ Cuc6z̿￯)Nv::ǻiu?O݇T8<:̿?PxooCuo:Y{nuÇ{]TӬ{5SPuovPQꧬßyYk݇T9zʿ}ϻc꧇Y$T>}e_OYGߏwS)cR:?s~{z*aYG{u${kb{ꇇYG]dz?{Yn=۪WcǻuY=T.}:?{Zeu{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ]߽1|?s}kcUz爵dnz?U#1oϽӪ}>: #]WYGu^s_{U#YW)Sa?:Y݇T4f8CQ݇Z*{.-sP㬣aƝPҝf_quSe=uCeaY:2^=|U=g_tWYvU52ߟAϻSeya::>qgYê/T=g_v˪M޽pꇬ{ꇬcP~:ou#PG޽SCqꇬ=uCu}eꇩ :ο?wS^xCq^˩ ϻ2ۃ˦x!x|~݆zYzοæYǻzΧ=z@z}=zl>::+_>ov6YT=g_P:e~~=H_}uCe__vT:?=e[::ο_vSfO}Ps݇T=e_~zο8OvꇬuCU_wCe}nz?~?:*'ݩ0?OU=e_?wT[#YݫOYW?}uSa펩E}ov}T=dA?sEa݆~ޫ0}=s}=nAv>z/I o=}??}G`[ݺ]wo}>׺ԡm_3x_I[??>^|F/׿%n߿|G/׿|F/׿%n߿|G/׿|F/׿%n߿|G/׿|F/׿%n߿|G/׿|F/׿%n߿|G/׿|F/׿%n߿|G/׿|F/׿%n߿|G/׿|F/׿%n߿|G/׿|F/׿%n߿|G/׿|F/׿%n߿|G/׿|F/׿%n߿|G/׿|F/׿%n߿|G/׿|F/׿%n߿|G/׿|F/׿%n߿|G/׿|F/׿%n߿|G/׿|F/׿%n߿|G/׿|F/׿%n߿|G/׿|F/׿%n߿|G/׿|F/׿%n߿|G/׿|F/׿%n߿|G/׿|F/׿%n߿|G/׿|F/׿%n߿|G/׿|F/׿%n߿|G/׿|F/׿%n߿|G/׿|F/׿%n߿|G/׿|F/׿%n߿|G/׿|F/׿%n߿|G/׿|F/׿%n߿|G/׿|F/׿%n߿|G/׿|F/׿%n߿|G/׿|F/׿%n߿|G/׿|F/׿%n߿|G/׿|F/׿%n߿|G/׿|F/׿%n߿|G/׿|F/׿%n߿|G/׿|F/׿%n߿|G/׿|F/׿%n߿|G/׿|F/׿%n߿|G/׿|F/׿%n߿|G/׿|F/׿%n߿|G/׿|F/׿%n߿|G/׿|F/׿%n߿|G/׿|F/׿%n߿|G/׿|F/׿%n߿|G/׿|F/׿%n߿|G/׿|F/׿%n߿|G/׿|F/׿%n߿|G/׿|F/׿%n߿|G/׿|F/׿%n߿|G/׿|F/cj1?J'X{_/ V7_Ck1?J'X{_/ V@=s[WAd?Yx.Z/0^ k1qr T݇2r<_Zs[[yxO{˿]h1l_l1ooK=˿~]T1?{8%z?c_{r?e9c~pK@-+x SIgo/.e9g?韾skq8OIg/.gx.YOgYb4'b}˟Yx.ycc>yS?雿y¿f/}oO,Gae}T47<YGa?2;a<MEAO47<YGa_2[v_cOvD_UO3[@uG?{r8oW+Iae}T53<Yt0oOL=Gkoi,?/S.Y?o/́ݟ;#ݿ|M&e}T5Uɦ:̿˯_L]?Io_+KD?U<nM7U] [7vvr4CAC<Lde˷~~ d=sg*ȗo>yC?ow0L}so*ML?U<Ϳ4i/x3n<{UQ?Ldf_܍|ÿ+/tovʟS'w2ە?o |gWn?i)꧓#Sy:ʿ0}s)OL?T<\4Yx;I? 9ۿSyMg:ο?{'eۿL?Mʿ?e^>_4?ߝ9Kalg8),YGx?I o(SL?Uy+e_=?]ePm>y+GsY/?I>]6/a9˔?I?dw?%+|O'ves'WTT<·ug}g_wr?ϛ6*ꇒ9 g}e__xwUlꇑ9.?_YOΏ?Nw]/ʠTy3v\־/?,?{w_Wl꧐?_YoQexÞ9+ P<k* ?LW;v@v_mWo[:zo#}g_8 VoSs$epΨy|xk^;wǻz;epΪy[~6WnT?93#gMo?_YoOǞCÞ#-gU>1־7~|y{l+P}ϹFdwÏ#-a|Mevo?_YoGm׾Gi??~ '*?ow?Wo[:?|xwIs}s#g]lx+U3`ܞ|}}eo9mȮ_YW͏ߔ}~9k=nˑ"}f_5?R?vb^~_m6,Tw0\־ߚ'lK݇?r&kelۿp|y+aw@G?;`ßϝv,Tu0\֮4~Q^9??uS׸?o?EsZ̿i a!mel۟pu| x~?o?m,Ts0{\֮/?CN=>?}-gU[p =ȮWY?'k'Yo[:{Ld7?/ o!;em zAw/Oݿ?uSo_CsZ>F; ڿ{ uSo_CsZȿ f#'j?T>xuSzAg?ɿ= ڿ{?uSCuZ>|H9?{ ?u_ nWY__R~MnP[Ǹ|Met ֮?2Wɯn5}{[m[:o`7!]d d%| ;Wnv-Ti?0֮2??Lx |~nvo-hi?0֮1L?}q=[7Ϊ} nW\Ϙ&|_^G[7δ}?!]s >c&_ݿno-Wl?/!]s ~b&VW߿no-kl j]7hGiO5q}fo֏~>?u|.x_ҽmoZ>{|/Kb'|?{}MeO ]ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^ӹ "/y_coan]q?;ܟ|d9u|c:u‰r}o?|c9u‰/B>ocoo o_n]q?ܞ|b:rf G޿Nˮ"/Bco{ ?_n]p6!'f?wg 1:ك_(f?wg 3'zG1ηs?.Q'?0/?ܺF8م_)f_Շ/?ܺG67ن_*f_׿yg ;ɿ7zS2s?.QMa{ }ٗ.? a{ ̿wϮ'oOA߿W̿ﭏs?>R-Lb?uc2s>>R=Lb ?}ٗ >>RULb{ _}ٗ >>RmL?b_-f_տn}p?ܗf/O^`”r_#ޏw2?}n}p?kܖ/{cc_K,ٌ/f ϿϮo@޿a]۟\)z%1/=t/=NS Jc{fՇ޳ ?=NSJ/@'go]o 0:L'ِ _2fտӮoo@7go]o 1?:M%ّ_3fՇޛ O=NSI/%Oݢٟu{np?+ܑjgo]X}:7ۧX)$_e)޿kE7ۧX)$?2[ <NSH?eu?4ynp?o@~?=GۯX*#2_io=X}+:OۯX*#3_io=l}:Oۯ\*$W_9Sw4?ynp?܍w4_ync?܌x٧o%*_ۯ\*J#3_io=[ _2zRٛ{_Oy'_zS ܋_.s޿xÿ#gۯ\*r"3^~?w٧o$?uO3>ko=oΛg3 =ǽ?n_y_ݺT\ǽwVxΝg ȇo[/ ;nUeCh?]_yDg w ݽ`?_xDg3 g`;gV :nUBGG?;?:Wu:nUͿBh]= O:hu®rI _[x*Σg_3 G/޿6_unp?/oW#_WV_4X٦r>?uٷuOο߮o_WuX:: ?:~VUA?j?\}boxۿX+6 5Ϳ~whX*VzX:?:g? ӷ [coՇW߮oVzX9UwlW_!?~kFٷwۿ\O+~OVzY/9_߬googo]g?î'ow_8/~=c\+OVzYo9c\O+OV~Yo9o9p?>kZo]gîo͇Wo]gîo͇Oo]gî'o͏Go]gîٲhÏV^6uz{Ǐoy f՟߿s/7{y f՟޿{/7l{_u¿mtٳgËnnpi_?nnp[i_AP{x k_APox f՗߿և]gǮ?7 f՗CPox??{;՗֋]U\0wͷ'Cu{?;m8Ãnogk¿]6p{ \zٷdÃno׿0wͿ'Cu{A໿mY=hۯ_ +oGP{wǯa_.VO~Z?8?vW={ w{T?^]7qW fՓ߿֏]U^¿]6p׿{ \zٷdÃno׿0wͿ'Cu{A໿mY=hۯ_ +oGP{wǯa_.VO~Z?8?vW={ w{T?^]7qW fՓ߿֏]U^¿]6p׿{ \zٷdÃno׿0wͿ'Cu{A໿mY=hۯ_ +oGP{wǯa_.VO~Z?8?vW={ w{T?^]7qW fՓ߿֏]U^¿]6p׿{ \zٷdÃno׿0wͿ'Cu{E *LҢ7l|e+abY7C^XGCǻϗ=e*?}ip^Lˣ ?%5=wraFGx7 4K<ԟ5Feo$xv!,wd }۶։/^CE ٸ|^w%yFiR1V+)4ytT¿k~Vzϔ+K[C]K#CUE[IQ:JYH$n`R2*EAHݯ ?.af=b*?}ip^L˯`oDξG8?^]|׿0|wJ_~ZFׯ>?r>;[Qs#׿u?u zҨ߿֑?u뺿ϟ\|VTAH_g]{޾+ uo?]z3s.A_U:7{wW9^*?}ip^L˯`oDξG8?^]|׿0|wJ_~ZFׯ>?r>;[Qs#׿u?u zҨ߿֑?u뺿ϟ\|VTAH_g]{޾+ uo?]z3s.A_U:7{wW9^*?}ip^L˯`oDξG8?^]|׿0|wJ_~ZFׯ>?r޿k[ʎ?OW?֝8i~m\(gc'?~Oz?oVޱ8<ՇXuìg}}nz6?﾿zX>zoX3ՇX߽}XcMӫub?lz ^:zXO{uqìGGۏu=Xu\yzu}yOu4Wak[5{Vao_q}ǺVao`?_Ϻ?X[5Nz }ӃuSՀ }>G`s^:[Wxu_OW~}Ǻ>ްqS Ӄuq}ot=\u޿9zȿl}Ӭ }\uaXg}}qCӃ Ou=Yz[ X[a u"m\u[:puG9ˬ-zqӟWՇQ۟ Suq?aoN Waoďu=О#1GQ7gW3_#OVbks.1a•Xaպo|Wc?Oz=l|)unzX׏Ϻaz=Xp}|=:돽u{{^׺u{{^׺u{{^׺u{{^׺u{{^׺u{{^׺u{{^R鞐=ꮉM.qKd37 Z GR -=mdR@(o{딊xzOK,vti&nA?q=nG?+ivPaSk\~+fN?rZeb&IOMGBb`=Ķ![?LaO/鎮O?gWzpͽ.)Ar]]ecw/1 sh̴8 txڪ69yb^hUr1TTzɇkGܵ>%h$LhQ9G?A^{{^׺u{{^׺u{{^׺u{{^׺u{a(gc-zx=~~=׫>վ}p?޿u3?Sպ}u{Vb?[ޏVcn/[g?&ìG޺Ni?ՇX:{'޸u&>uq[Wa?>u=X|_ϫXSX[WWaouˬ-EyCX[ou=\7t=8Xz{_9SQ׺zSӃuN}n1?Ў1_uzpp8XC_ zmXOϫ>y?s">zpzX[{C }qN7CӋvCX[D}Qx{W`o<﾿q?A\zu'OW;}?ߺ=8:>}[-l}mX{Waot=\WaoO˫u>ì \u}{aou?>:ߟ5uq}?OtVaoǎ=.:xo'u>Xt=XuW=b?~z iX5׫0}#zߏuXǽua6}ޱՇXXu{Vy[kX[_ޫՇucpî/z=[Ӯu~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~{ߺ[sx>L yogp4۳]hq Z(7T)kh Yfvws>]1DX6,8T!v`Lo]uS$ ƀ@$U?w(ʜFrrnt« t%8 >E(0qҿ'@@v@e 5ŭytp7[n,lOZ+:O{q|Tkߚu񛨷_jn9'כPĴ[Kl\ߙ)6ڀ5jIb$FU#=x6h vGL~J'o@e7;}c@?2=8K?ʶp|ﯖ9nCoTM/.`o+H3ޢG bJ@A`l,,vv.t梤;r'XVܡ1MT9'99tn΍3]ÐnNL,)CvrW lujΒm^^L^R`F|a~Uoe*fn$"V:?E@a֊y\Fc)AVW![c!h|]=&3A 54Q`$j T"h2I8'$OP[wg T3:o7_Jw_se~G˚퓹?1}AEW9 "dmYY",y%/kasG5I̛˳ŷm4frHu xANy\rѼn_:F)k!jJl^ݤ)gxGx7IdIz#T.j,tIuTu$gl$bEjHJtB8M̠8 Pժz18?.^Lw{utj; 5]MeQmuJijY3$_oݢH7"I!%QQ=FFQ Ӑ?.m1c[H8`:BTO[߹7jcz1U[Cg>Kt͵%:T*׭5I)\u6 X^sx2AP1e'~8a^PqLP+v`v@i~uf󬪢mGƫ#-(ή.^awkmv2Jֵ:{N:|6s%E F5M?)<( Z}(rzg5fޓi㲸-E&lg5Q<4s"kܱ.ѵl7 nnUHtQ/;maqrPڴIMe& *̀+=wG}!z{"so;hdpb6ܸ[ev/sXx:HQl0El7IuT Հ8 '͋'7M ݃2$ ЎʲO?5~Z?# wiwlro,e7m肆4A"}Ϛ?vlUٶw;AKQA>PH9VֿSmvn$(@ N :_>ѐiuzwt~I 619\G?|}d*bwN~ͪKE8ټEQC@ts7.ivN@1P`+릞g͋y-Yӷnnu[6͏6~Y; B*%1D{.s҈# @M*X2O;G($sjK)~t_l%h7%sn|%}^#A墥O54,l8#/eS4P3kBUI7{ 2̈HhA?궗L|r ԟ:lbp5-͹[M1vG0tP #f1S<<-9_o/`ypXң[4 PTT:Sv[9AGҋM8 :A_|^~j-FRdy֔QCp Ijl|TL55b{E,XvJqէW˥ ;!<0kLt5=ˋl|{7 to^n޶힕?f>8 j`ܙZ>tKQ,h +ȿypv}pK4y$:V$UX 軗onݷokז wtxu*Iqdžz/˳K;:^vGEAvz]Ż6fSobl*KA*2T l.gKRKjpO=wTfZE q8Q}__1z[jڵ]=o~GGIⶮzL]lɘI{5L۩EE Hll˛ea5&FeFU`zwZ6,{n'4{=+Džd|s$G. [ϣcK(j=sV.r~ƷV[xw%TPThj+O8uE'O"c7q@u(gc(oϿuǮ{}p#޺ϬGOzߟXu8c7c6Xqc?½b?s]Xu??zì,>sX.z Qu?z t4}a>zȿ~ou~}׫}u'7~z[kuq}8>ް_u=o-"C uza^゙zsoϫ ]\u_׺:7^ t=Xu>iӞ|:3Ǭ-?Gnt>}8>}aoW}q\Ot=\|?q>7q8:zXӃ p>7{:=q?v{ՇX}sq:pyu?_t=\uǺN8?t=\|Ot=8Xt=Xu=aaOW`o߁N7/u=[-ÏN ?~z?.C^>ΰ8Sӝ`?zX[CXxWXaaoᄒ~ޮ::[gս:z\oG}=ua3{V:މ}u]o{^׺u{{^׺u{{^׺u{{^׺u{{^׺u{{^׺u{{^׺O92oO}Tx?eʵy=V]=u#!/>uob /L{OvԷ[`:{xAZX>ǒ]IzxNìj̾þ^}}N-?R k/CZ].6Syj,F2O[?|cyژ}X+ާmt_[Ozoۻ+j V5ŒTyI>(:E(,H8EI$bN hzvs_1ۻ {RY|Ybkc QYi*W4u {rG*,8h`i#'hB tfn>o]%ˡ#{dߔڒj(jw6f|-^<29Ȧ,=[jw;*_XFPRyq9dW9~g+OJօQ<j3owЧh>I|sek#r,衆 |T*DePmvG˗%G((ZԒp4=G[/ ~~b)5X-JF}*::"~dcW);wwZu퍡cw&X%TPJi1opQT"܇x%HJ9ݹ&)-7.e Y6Uhj L2:s8E6wi:++`ji_EHd_ݻЛ66ػқEGfv3-JFB& R$!*u,M滍⽖x%JVVd<37w;4x2e`8"C>=Nyr&Sv6;w?.;o+^n WG5^o 'LSLʓ%D iu05~~.ͪٸ.-J|,OoFAvU d -jF@=(:=~=qhNd6s~][xl|;I68 boe{o kW}jj !t$AuMRi,*ڐ5``ytc:Ӵ.-+Phq_#ѧ.ssvmgmZ%Zb*E2j+Ϡ/nSz> jJf}d'/ 5eX>6ʻoHlRSfٷך}v' u:ɯU=`; ܿ$'|݁08\u>v 4W ͭWQSh7wgrdR Ƣ`qP|ܡY]-4Y#ÓTQBz;:7|€~wL.ۻ{vb,ٻ7{o,n;)+fD;b2Qu>ػ+ylqB$ 3kʤӀᨏJ-9nym^FUtV4B}ƤcdV%Y5#}4sE1rARI:h<̎=[lmk-YyȲ)#i!B( #ʾag?_g>۟#:c7(+tuΨl®SQUdV)S'Yt)ܹ_q<mod.4 iLV6MK˴,U[dž(lG}t\uoʯ);sv>'ݹ=;sm1zݽAf&;ngr_Řr4NL?]*71eT0R `25EF+Ї`^_i4gA#1u.x4x@5S2OO6oZuFPo^z.ƃ. ISU;&!AC,wQFuQ;<˻T-]P*]5 <ӿ1n]$Vh6rQ LƜEj:-0ay=n]˞~qQ읹S- Ic,C&\ ǸOm,wQKJ溅hq_#g-umnԱ "iy6w;k5;gp|qa2Lo|~;3K5C_C* DNKV*A7#-jT+*"A w#}ݥDRA 0AV θcwTٛ>COn>CÚVW㤬L@`\rp[Sx,M cЗ+kIb(¾`9aA :(gc'\?q^2>޺}{ׯV{ou}c?[mziZunOz:[z?~=coϺ:uVuoޏWao^X[SՇXW ~u=Xu~}קX'u=\|XWao=a}:zpS-{ ?.mXu׿aoOt=\qXZ~}_Q{`o_CQE=qq${Ӄ z? cXC:X[ot?.`o{zQA{GoCǕ8y}ONvzϺӫaa?_t=\p}Ϻq}ot^=aNk+\uz{}OV}o{XZotVb?{u#yOu:}uq">\u`?gXn=뭏X}9C}=nǺ>:߽uoX|O|,uxS׺:}}u׽u{{^׺u{{^׺u{{^׺u{{^׺u{{^׺u{{^׺u{{^?Gwv'J淆/ y~IS)Uv|oͽ}M}fG3ZV5U\YSwG)I1ijMSFapUD EJ?4{W,Ov%.hk?c1n;pdٍp9<~|nFRAPZZ7^UiwkoݕKl 1#^KKw /^'OJ1՛l.H=0zRXjg`xLSp^O۹,D=,R(&>Z[5-*$MH5Ş[lSTP5Lր EA)N9Wmm1u{wG\F`k2ݍ*h2;tE nGnMj4\#.V*1M$4 .6.]t7]ll~;Onl}ֲֲ H/;w0Yeh,QFi41ۣ^ŋRPž) ?zӓG4,TMIS_ kG=o[Wv+dsQU*GQ*+ :tBB$Vg2g2=~#ZOA@<''ĎSZ ,Ԟ$Мc{Ue0I ȭ|fg('}Z,isYQcgrmoUuSϻ)Qҡ}1-cMZhBjz^u7Al-_^+&\5vs٘|&a1xdIQUF@S%Pίqk8[!(I V cUkshmuիLb1 Uԙ>Ѩy=]ڑ[>3 dSpg+;YII'R9ilnRB]D` PR㑚}n߭quTX&9 1T-]IC"A#sQHՂ)GKT, jucRN hxQ,5_bct{7pnlnOᴘ-'ͿsjqjXjmrKH$nup]@R+8T]Mx9y /;3}IVקktIJ*w䨦KSQ2Kf2 ;w/ՍֵH]YzDh2 Z#JX__j>(gc/Xo~=o}cǺ:]Xp}ގ:[c>ՇX}׫??'޾έu?zX͏x~=X>iՇX[ՇXcou<:t?>:ou}:}{'ŭq }oOV?CXz s~?~ySXo{W:~=ӧˏX[>z:}?Oo.7ak}?SQWant8or}qӃ?N=zߟ>z>z;u\zuqW`o:{v[E \uq?ao7CӃ}ksXOuqz??>x~7Ƚqz?7n?u=8:{>}aot=XS-X?ӫ=ak_\ӫpzz[Ϭ-}qaﯺq0oVb?|_z<:=a?OSǫ.^8u}*u?ӁXu?ՇX뫎=׫GV:Ou}?îyunz\?޺z£ua?q|m}Y'o:M׭[׺u{{^׺u{{^׺u{{^׺u{{^׺u{{^׺u{{^׺u+etettb 2:0 ߽A z:qG#v]ӬtH;mRƑ cUl XdN)kϚ%J׻{A[M?EϝT|ORw,{>o` gрeox:c)Wesy1KYڵmC-eS==bɍ1"T =tK]Ph2ZSX U[]C^ݗ n w9˷C{M. @pbԟ2z$!׺u{{^׺u{{^׺u{{^׺u{{^׺Y(g}눣OǺ.^_ga]o׬g{zߧXxoNպOOOuìG?._OVc?_][ma=ϫ?O~cGsV:Շ_ՇX[|NaVao|}׫a?ᄋq}a[ޏV:￧}xq}^8a?zX| }W}O=zXO~~8?~tˬ'SՇX[?#C9v?Ǻ=\yu?}uaì=ӧ=:?6CՇߏu9}SXGoW;xaou=8:Om=\qo\yu~}-}?}uaì}gϧw??>׬/W)ot>}]z??u=9W`o:OWQOVaW`?~:}q?O ğ~:ߟ{ì'"`oCX[+?[nSպ?SӞaoߟ>&}}uo>Ϻu~O:O{ՇX}otì\N=q#?~?[=À[g'+7Xu?>:ߟ_N8u?"3돯t=Yx?~{c~?[Vƿ=ìMޏV.G|ȿ.w~LVE;,?E'U,j|Ѧ٧wſ< K?o{ߠ_ӟ˻_ˢӭƵ~g!7>/{Y.׿wKu>CVox}_˯vG]{?ϯ՛^~ݿ}{w^?fׇ߻o^!7>/׻Y.׿wKu>CVox}_˯vG]{?ϯ՛^~ݿ}{w^?fׇ߻o^!7>/׻Y.׿wKu>iC_Fɾ.|n@= y]vƈPNG  IHDRsRGB pHYs+@IDATx^|ՙܛ$H"ڵZd-fmXk)nOv?~bޥR_E/v֪ljVZ+ $X Hrsϙ\ȝΝtL=97}̌q730CXo _#4æꋨ@@Ĭ_FE3l!?P>Ud)fo@}O!Ko M *dM6eUntcGRU.VcHmPoQm'v<ߔ_|)puXh7R;ýomUGkPTE ƌ+ ;HK-ܣ[dh/r` A؋-ǾjSJh0-xK=GUNZ_y5LO]f|(,0(,(,1͠삀)ۣ -N{|fXɟ>1a!Xr@Ȑ2GiU q,lL#Rp$T`J0,JGk݌ſs.(@& I1u}U 1TNO@HADEZ>/)O8l܃ p2DGČ(ӧH8"N(h/eL]L)ԑnkcK֫8E+*pvG X_ً퐵)g{s?pr8o !6 ׯ(O:㸱o>~B{+tTiRmY&6A7oKe$F찝O&,@h^ӚebI̢Ɇ+_Gr<$k\^'`.ucG̞lSoOX+--'Ee)^L̃ti_ #$z4-_j38bq݁u VU~`*ǚ[4qm`'f}'z3ˏ="<Zp_/߰w6ϝ.{ } Z;aVq1݈Le ]{g3$ˢ]nQalX蒧{D[cޢO, `WȄ5O}]p|~ZTx KIpuEKMdl1Ԥߪ&1cԩEX_<ɔqhg?W~l|R3=?߶LZsc%žNISc$l= wQMO>:ikJ*^V{,s]~G 3n_˵=|sRs1 K# @`$QyaEϟ+ K+Lel?Ɋ&jLFzF  nSeMSt-!u:F,J-3sFۿ;tNo4 nIK{Ι\G>W M;7][t.}W+|g]]-|"_]r̸7oh_;:$_][3:~.|?m< ?Vc?XΣ]߽\Bߌw [1֑OA+[~uD(&x7| H^ӏt.fBp[)?C"$O*. H\St)uvgKծ)GMYRkkiBsǬ2˺|즣W޹lm.uUh*>f݆o:e_q}^R딚ŇSWQ]Lօ?N쭘ϱi2fn)Zw^z/y%Irg\$F)ZktlJ:;"wD²JZ5kIOi z)'1"VnXӲ{XKg1~>T<##lX=BU9JG n;xɩ,4lfaL4crih`w<dB A0[VT[7=J-93bfC2?{Cd#­D@<֖tLΖ%0ʲ}ȫ\#단JJ@l4c\d*(w VYls6&O1_nʿ&z> O J"U\yѹ.zWߊR*'k |`QՏt"l+ߝt[gX}AXD VCT[mn+G7zۯ/oLјO0f@"TJbD^ ss⚽ ؒmgH$ʔ[Ʉ=E;n+s"ԇu5Fߪ_ҷ_hz5{/PbX*!_u/mW~7X-V;.65?- ~0|)QF-)?8o|E/TV[N O4z}ERX}毤YxM~Z@sc3@ 3\\`Tlik].JxM%q;Q._:I"Qr+L했~%e]ro<_wcFRk[eʶ4CGu֩j\P#R(?׊^Xg??^Owz?SYt/\iom]崪ު{<#ƇV2p?FqÌWSrR;m/q#z{?u&VIqU{/w!fuQ@bcm4>E?O'r*!jI~=cDRm C}C"ێ?uGTO wU<"6 [#~7\&dH>h”Ns3hqW  pb[_b{:}ҡc:x[7jST(I 'YWQܿ $_,)#.;3?%MSb kzVԩcu^[- ZR&yr2x@خ+=Yr@84Y(A@4+{d= }vߏn7(\&#-ڒ!ځn=$z5wz{(]W߂9E) @^QyXPtOS2̒^6dZ<$v7Q]L~Δ."ӡEr t[%=` ՖOUCkƾ) S%LKHq-;IˠzȐ!0B~I==/BKrFDO薏Id]hq_nMRL IFRJ$H-)^|rO-|Ad ɓ/4uO9$ƨ'\Zҹ@˪8݆ J[Tgtg 0]6 &@w@9" W@C&4'@  l>/bm[%ZTp:.A'j.VsW;50C^^x.P@@NpMW]3Xۊkku^P~$&=`>Am< uFqttodKkwrX^9l[jQ+Pw9-;kO:~LO`}nsľX"9;Ҙ=ìKR;1Ĩ[al]JY n_8 d@#%f fi H@#@a^{+|IiĥNrK 10o/ \!` Q+ >0܅݇|Fـ. P 3 w!<'j@!@}6` 9T;]@ y~}@gPm ByNC>#jlpsvtTg@@>0܅݇|Fـ. P 3 w!<'j@!@}6` 9T;]@ *ߛ0kb-BChim=|b#C @ILZ2@ڠ:Pc*}sg5W-&C#!oqWm^f/o^|lGekl]Qmwyb  ;S/SzƄ’ O?E 7fT;/ : 4> Ga#R3!cê/` w{B.OAyA7'lR[^fOH pG^.T]X pr 5K†@n3i +!A$6#*h-KsMcxݳXtK{ѵ FbOqL@ D"f8,e6|0d=ḃV}/ ِuSY1oC]4? #bwY L= ː|ܭ=amFX\o{YԴtzMݲD$Y2ς^cDkݩ+`eȖr)r'~YT jժeKej[S;YѴY?h4m?U`^7tkgXd훫Ivl*nk-ZRo^ ^0jD,,F[sLzqʭ/?y̦>N ` ۶+VjC,ޓ T;9Cb(.rbjܯ#E@|CJ6ުVϵ3%ޱ;ٚ.VNq> +zd#49кjzbj4>r@4u⻣m w\MÔR/K"=sv$>ZնdV[؁|B@:Z0wTa".w)mG+8{FӒz;rvbIt[w\O6u>t^[UϿjguqy\04)Ezb*?էd35\Fj'SJabw?1un?K.vd:RzU0aif>SnZW#o"kޓujB$ * 'Xwa!L)Yaw} K}?_Kid6 hv[ .D}}Dpuځ pk*B^C2[BM72ϾŽ na @.#(iy\5D/61{M< g7jH >9@бp'װpoA7]pvHkv ym7rdɿ<@]}YMp:vw\@tSmF:Ki@@O@4 o u@xQPmo: gPmg( @[i33^ -T[@T/JC-Z !xK?CpFvƋ%j{˟!8#j;Ei@@O@4 o u@xQPmo: g /2t^ЧM@pږԳ@K L}.@NuGN|8SeeHxo{G)@ {EW2FU7aZVҬڦW1ӤE1@) aN.+ONBkڟj @-qv!qy@@fP̸Q 7Pmo* ڙq opU@@3F-@@N2#jgƍZ!j{ÝV!dFΌ xC;BȌ7jA wZ T;3nԂ T @ 3vfܨ@g/,IZ S%b Dh+ Tc~-| js @TO\U7#%k24] k#%+TckmTFC\SVf00;9mkqqb M;[5kM%ezD+UV}lnX 蒑va`@0+?kb-BizڦT"rVyT]XBZYuVPw/Vve+{Y{KD-]D+[̚jm:SxVmR4rKkkS @n0 Ü\3d~ڹApڢ@ҒH\ EvQKae4=^f@awQj[SI Zu!;r ɞ@ 8X;SFz֙+ @ _ O, DWpIS6&hqB%Eymnޢ#V݁m˔3vo 1)͑v4r@3LO`:&576o2fVݢ,iVW## . ,cJnuXj\ȳ>@'`=,-I4S0\( <&fct@Np! q@@v8 K/ !%jq@Pm_NCyKۡ/ ھ6򖀨6w!q@W,&՘, P?Y@ yTWÅ@sT؞w>2$#GƏ7dT`c/miy{.xi겧n:J}D߽zv̿.UA#[y6gi/]>ax}c{,[ufhO',i_쀼.37˗ߺ~YĤ,4xOfgY i康<0wSi~+® X;5a?|ed\ɩ7~_yO KnxQ"`3,"sU`iYIZ: pʼtY%ݯ&6Ka@SmT;u_ =Uw_^$eUn~Q-S]lgtL ϫ.2 ʶ++ϻn̛XX2O5aj]LP2j>/+9ex#Zs^ɝ+E8RW.kߴU+KziI=$HL>oҪaՈ7 <_ڲx-؟n2Gs:蔹N@ɵxO@?ib&j>>}A׶$?qr޹ Ə/ݴOQ+nÝkbI*+h:GPo/,v쯵 `όOiPm5W-'=NG/B^~ Ζ[n~NM~8]Sh* z!(%$7CEۮyٯq-vbĖ Jx#~I5v=k9 QVyT-\.lѿͣ+g7/_uW#-ye"YNrW.$uK$O:p.͕O(XDQ: mxU$?1"$-Drp/:L0}˖OިMrecż uIQOrlojΔ鵛[Tu<{.QšE''ϭseG<"K0?wP~z;b0̝{vDjKfyOϾa;4eBJmސO]_vkmdZ23+;j/`?t=LZ| v .l;o<&]-JKNT`UL=nV׾^'ZwoUʙDwĭ%GǛQ'b+zĺkn?nG=>dipDZp9 3G @`ȮjQ3*Ξt[)gŌ,YwwT A`n̓cb_~Lu/zU:3m[WjFC'UjV*aif oQ6F]m§y!Hjd @9 !dyls~x})+ۃ^^0; ާcgN#@^C6靄;}wleh!`Pw>g qV CzĝOOn*s䅿;y'd2u 'vr>Mm=dG<Գtd̙˓ 'sYYrf Uvj܎ߧHil^Uc'N +w޾^k=#jqT;5/KdHmv_|ʣҎf԰<GWzO/f'qFw3g.k_7rzDO/aeӺlj_kdN|k~>e]owuz:4~ ک:}JUxy_19zF<, ($]6ާ#gNpz =Đɭ2X& \&W!vvχ|97Pm@~"Tkit1@OPm?B@9 Sv~bNá:Sm}@뾏r&T{rYY$ޑWX, Au_G8+ȸQ\Y;hYJfKkRf)@aN.+ONkDlb j;ː<-ZYПEͫ ̖~X=f|Xk;7&}e[殰[KKkk.Q aN.Pޗ/P6Ǫ-JN*-I\U[Kٔ(d4Z ޼6RYuS B'CyF}p(kqv!SKb9nn*=+R(X :LMSM*Vƌfʚ+37LP i 0rO<#9NǜF ct@=O2XnNۋ#9Ut\  D@aG$L8r@a^;oq@9Bi$G @yJӁO >8܆O!@}:p )T;OnC>%jtpSv<݆|J6 P<x Ӂm@ O y:t))>(nC5u!#jݐa@Pm_CyGλ! ھ>wCN!_@}=|8T;C&jzp;v 9|M< wPr: y@ y7t5TÇ@0+?kb-BC!=k.@C_8'3ijIMڹQuGS Ν@_kT wCN!_@}=|8T;C&jzp;U_jJ$օ 7B357<یo8MY@~!^]tK{ѵ T.6krͣ;u4 #T{=+m൛GvD.Aݴ!a'-(X~O=KTN[t$ vsVY0L{ih=xg@#%Q1MK}Y֞K[*{C|vEvؾdkVhPfڢկ+5K`SF’k->Nʤ`u ;f!<$j'gHE҅[H+VmU)*:.޽UkfJcw5] [ݝ| bW\Q]:nG)C$s9ZնdؓG ]#jۇ~Yr_'kn#=Oh[;_zfms dWYbW6g4-S);0`'f\dAuǕt?nSCh߻HSng(|@Evr^;V_))f+qN#_ZyyNW_tA4#;I; wMk4cdW.^|a^uRdlgm?L:sz$Q8@ O !St@V YMcI&@C*F{#z[o C`@p_2$9<@`T~-| js @TOPm@~"ji1@OPm?B@9 W@6 ?@4Z @ 'F _!Dh+ Tc~-| js @TOPm@~"ji1@O+ '`8Ug-y8@Z}3vړ"K@9Lu_G8+GrYgv?в*f͖64 S R0 Ü\V&sڟj @-qv!qy@@fP̸Q 7Pmo* ڙq opU@@3F-@@N2#jgƍZ!j{ÝV!dFΌ xC;BȌ7jA wZ T;3nԂ T @ 3vfܨ@7i@fP̸Q 7Pmo* ڙq opU@@3F-@%nF3q ڢ  O 5Kkݵ>5̆k֚ pMTccYWőnɮ|UE>xv+6 pOJm]bXH/lGҬ\=pG"Ө[aWf+P kߌ1jñf-}mC `Wn?ZVQKkRdYRQteoFJU[HycٲS-`7#0:*W8Xw2W3*v[wmQF̸]\\Kkkݤ I0 srtsXEI%:)KĖo( SJw@'LH<ÿ@U:UYdO[[ ,ٺ=;¨ڡvۏ'uۖl|Qub7%ӲCOnn>Q-94޸( )%`#/iIO@ii[ZvD٪7Uͱ[͓9'aXxZH|.YTX;3liYk;:( d@34LӲ~q`MZí+b3e.vi`QYi]EYQX-Z'vEu`,e/OWCԘ;՘[9TtP' ѰY^[i`9fĵP7b I"f>INhv' hSr IN8}'f/dHA!0T{8, @ ڹ?Fx@ Ah 'F _!Dh+ Tc~-| js @TOPm@~"ji1@OPm?B@9 W@6 ?@4Z @ 'F _!Dh+ Tc~-| js @TOPm@~"ji1@OPm?BpMkJ$օ eL 6m dkni#\R1'{ոG}Y@ Y1oC]4? #bwY L= ː|ܭ=amFX\o{YԴtzMݲD$Yg&7GG ]r{mfTLdEӒ?gEkԯhJMy*+zTdmaϭ2pP͝A@ ;GQtʊUCZU=GJwoU瀙D؝l-9Z;@,Zu:쏤Sڏ̉&ɣㄮBB=vG:N,3lGK{>?Js@^ȮjQӯm:ފg4-S);0`'f\dAuǕt?nSǙ2AB'!"j'SJabw?1un?KV1zŽC'UjV &,T5R6QF)@`mgٹ@˪АZZ[*/ @ MijIMڹQuw/NAA0ÀGU@#`(S7jg9-A>T{ @ {P챦%@'j! d=ִ@`P3 @ǚ  =|X =vX @ GkZ 0|b@cMKO>C,@T;{i  g@#jg5-A>T{ @ {P챦%@'j! d=ִ@`P3 @ǚ dN4kڙ3& pږԳ@K L}.@NuGN|8SeeHxo{G)@ {EW2+uf-BilimSL0 @ 0ei)_vmT]3Q4۠ E 2$n5@Ȍ7jA wZ T;3nԂ T @ 3vfܨ@7ic) xJXS4@!T!0C)~ !xJ?CpHP[ƌ9VwܐzzF Y n0gUqeZާS|;K-d&aH4kٗC/Jg4Hǥ4 +D:c*-4sRGB pHYs+5IDATx^ T՝﫪7Abh8Wfdp3F'$'193Ęhx㎇Q"5ę1dEGQ{},۶UJeY^aKnT/*WEjyvVxGX| qR>BZj}I|ZBUiȣ}z-HqȵSa[گ~'cZeRLTK!i)M4x)Tv'zfXPpWw.+F#ZNdz=s8֪;[~kr]:W^)VU볁%H]N24O&&9 @pH z"*X[Z9xMmZЭoU3Jy)Ce8x|ǧ"_/h^[}[ޗXZݝ(; \:EJsU f%Z0VGjZaU4 lаWE%b9o-Qķ}eT[  9q@j*Ц"RSv"OлxDD\$tmY"K?e^HeIEEH zT1ĞrGzOcy}h1;T̃q\f Wv-|@QGc9>xףZ(Jc9R˧GLVNl/R:_5jdQʆ7 kҨ61C=$wH$Rt\-ݯC HCD\Q掚'-X1Z"'s dn{>ǫ<2?+1:y<nj(օN꘎mm큶Vc-myӪBͭ?:>x*kWz.m`7yqs9⏵6ہh֖L56}miբI AE' F[΁J\:t{:SF-ݷˉsD*r淸{= >M%GYE^=^.y+q_w~tS7;mϕǛ BiWuYZH4SO 붙F@~lyD׎b1 j7=яF:|ҳ]B=O|Kg4QT;%}IzxD,=W"KlUmsnNwg=ᜏʮ<[0xo2ڰ62CyF=ηlŘ3aQ&&nƱo;E/ǽ dлhG뽶s]4˸SUyRJ}oG;~y7MԞ߷ WOCKKwUi鿘;$% fTrDZv9GS}oݝrwO(+mVCcuQ)_}E=N;}g]RfpT?n_RKji"['l| 赛d%EYE׍۷lnZR\TE ܿ626wf1ZVۏ,ֹ۔,Sj z.ʪԓHzȒ-pnP=@U^&?y_NuFWZ5]a\s]:r'{^rԝWW^3w&8~uvue_X{( oҵ̻nīT}p3W޾}₡~)JG?wRnNJkF5"OEMK@FΦ;tnwc"2\ܒ$Ƣ'|˽:"czG&bWE" ]n,*rI~u5bdƊBui 懟*meR1D5_n\rѠْ)Nw06mn@rG6;璺חߩ/'vܥ#'WQM /ԅ/?$uXK?ZfvrMM{(wTW p3^:<ŐE P%W+u[B1&eGyŤ y 1K]]3ƷI!)S\ouEO\ON&VdĀ=6}Y7bʧ&&*C KYQ'w"TG|JGacj't-UfMo{c:ux@oslg_yve+-:z8ތD XA؞Jxl6+5CW\<\mrN{ &%q7-=(Է]*vLVyW^{fH y#tȤsIʻrbk\$#˳{lG<K7MHA(ܴj(!"-cuxe~%wR9l:(R_uY2Q`o+'/sr>N(KR{ {/,*~mEI5m'c" @XV31љ(ǢW=//]sVOd܉;)#3"֩Xt_y 6Ke;nYM-r}ou_xS_vUX|/GY;ce򺢉Onrܺ^K^ x.|<|Ͱ'c|ooo_pٷZgI4٥amS]x/P?^ ջG[=7crG@؜3VK7* oG{}߳ %zbhLwxUs7"#cY3J疚E).%J%%*8ʻLr:3npUL g"=ƷvkbB=Ó}/]ر_f7;{S$E5EmZɣ-]Vڥi oqCԐ2UU%>+n#5ܙztw$. k<`<[bdԙ.ˡ돲>{1_y]h갘Q=2zWGK7>pVEbe?o (t']ZkmJBv| oىNɟkش7cϿ=3$kt44omNB Ր\/(2#5ؤ5u_;[3*G9˚ΤqlWЃ"z5P"-^-+̦~OW5i+ViD%~ '-Lr; EΔ{mYIIA?KĐi<<@x:nʻ5jOKDve,=%QGI EO鎠.P$njmkQ}϶>ݢVw޿u"o]]/bzc&ʴuI)S-MnN:e}1 OZOonMرcZmMбpS[=EN)4B+yAr,A&ӎ7#,*0z?C@;2g~yw__hzqDEdGZsjzd~|d <ʍ\K ]\!:.]y!1/w'˞Lhr`j[&~= YgFF4D fIR_>?tNϞGw2V/N[:n r j$ω7i5\>X+rB!CN[@e !yD[Fä⒌C3Ef@"w֪Sƌ:z69^6niwxKgTUmճG7UzyEFX @;Yl߾~G"b-/6aƤ ^@`hA踸#sux坲Í߸ :S2tB.H kz S> "3tE5 >'H=}` 0 =4@Y&j8@@gEN@>C OAO9! jv >=}V 0 =4@Y&j8@@gEN@>C OAO9! jv >=}V 0 =4@Y&j8@8O%A@3B[p@ @@ q$f@@ !H̀  BA7đ@A@0n#1dž 5MԹz^C  BA7đ@A@0n#1N `Gb >@! @:}!xH! @ ˸0B/>Yt5@@ EA7˟X0Yt5@@ EA7˟X0Yt5@AwM %9#u - ЉN `Gb >@! @:}!tCt C 83 xln@oqgnt: C 83  @@ q$f@@ !H̀  BA7đ@A@0DzmCL @J . GA7ϧX(@yt|E@@ GA7ϧX(@yt|E@@ G@Ss+AHz!z!# F B$ױ0n[1 (Dz!z!# F B$ױ0n[1 (Dz!z!# F B$C E@/1B7ʭ2Qt܉1@!@ EA7ʝ2Qt܉1@!@ EA7ʝ2QD $@w݃@NW `Gb >@! @:}!tCt C 83  @@ q$f@@ !H̀  BA7đ@A@0n#1N `Gb >@! @:}!tCt C 83  @@ q$f@@ !H̀  BA7đ@A@0n#1N `Gb >@! @:}!tCt C 83  @@ q$f@@ !H̀  BA7đ@A@0n#1N `Gb >@ X& H@ NA+@0n#1N `Gb >@! @:}!tCt C 83 %`#0nC1(\z! 9s %0nC1(\z! 9s %0nC1(\Q$@{ޅ@% BA7đ@A@0n#1N `Gb >@! @:}@<'ݍ@#t: C 83  @@ q$f@@ !H̀  BA7đ@A@0n#1 e**W )+)-,.(..J|b<"[Lϟ^t6==X ^oQ ilС=<[ߵF-l˨N=#\d _P KJKJʜ򲲪JJK+JK嵹Y ̨2=#\d _h$ىdD[v{k`A/m CZcQ+|L mY:(3j#p@ 8ji$.%:"dT.2C [5W;wJ/QezF @`K\ͷm* WW7I@&p@ =O;t{O^w~-q% @~H\(ߎt: |' u=I! t@@ q$f@@ !H̀ X4Fc h\P6!&_bA.Y]x=^" zܿnu;voJed 8uh'p_Qq6?q0t/lnUUCn]8E㝈K-^۵EBm /=__NiΗ׆n5l|N%oX>so41H:@dDH%0DϿw?RFk'F' RSZǨ~'hUy1t;b@n 4zU4| X^ELRzcG%Ssju%m4JN)6Z-ydon' YC!@텋7/YkR?2ݳ^9ݼ#_ԏ;ʯ]՛j-PqS-[Qw6Ƨ-ڙ7ц;QRh:-C K?$ZrZ3T}{9½W29&nQ>(; @c???E?z}K&@|L߃@s,f";ȳpC4N֑N;@=(&^p[/p'F z{k97? 1K6NȆ@%E;{6=h7Aϝtm=A@{kOͦG׷깷eA5y.~ux=)i+s%N%x{{n$@,J_ێF#ɝ=q mx@ g6m17T@= )` DI Bۇ@Ng `Gb >@! @:}!ҏm=BpU 3  >uNUj=!OL. @=z3t, `g-=B(jk(8"㎔sQ\tS=]@@  J*9{['E.cN h,">P0wKļE%^7F)wg}jUȑX,S:YxeEȑÏW#/rQhkx:I.f3&Omy|ru=KŘyENj anNk9- >:Ѷϒlh#.O7A^e 9Q8\A] D#^XcoHǿ:b ĬaKsȧ ޓ܄xCN() x`朐C/;R>!.jexO?T|{q٘!o kW5G.|vٿ.&l ُJfǷuY6V7;5>+朐FoIKgddޗڑˊO|_gԪu3>k%G^657U%.p}U*L7E{}] vTK?J˅ݛӥkv9!ƃCН7#78mN^z5c$XUSF-ab60op _njz{ӏ՟QΞgn}X?-g^wEg`p4m8g;{Bkf3ș7Ñ-M3do/6V͘Q[=[KcOpHcUY?u-)[?rwmh^^Na=n^|śʥ`G{7>aƤn'eO5777< ˷]if1=!Sy'W?,mP?ƛG8aI\NGT j.mU_4l:ּ9أ?ڢV_2s !voU?lvͫow8̽dF^ZBo?A3B_IÍ/"&$z.& :8AMM7h7AϝtWT=aĶ;?l\{hje؛+R Aq_+.h4V=l3院p]Z?HF؛@-=(r@n XG^{;goG6gD)h(#OK{Q35ۿzvS@H :\N@v IiNb@%g'A8i,5'UF7ڡ>Һ`䤵!@ @auAorc:UT&N8 13,{5ftg8S#K'>TED;V9O#QT,iVNi4>"k)Ѩ. ̒aͮf&-UejRl#E @0)F߹IENDB`n`?DUMi$rNy$PNG  IHDRsRGB pHYs+>IDATx^} xյȓ$#! Ei U(TDi^{+VJ}W'VUkADxի `ȋG `㜜g<̙ǚoΙk߬fIH!,HpBfVA0#δWZ%D|"XQ7x$tH&O2iJ_1xfd02XH#>Z@6VFqKu_ !p-/&fȲdfj@Y$dlIUfQL$ oY yط FtϢ>o'pAz^飹 \(on9=yp\=-?v" @PE[K9NJnم뻕>O9I[.u) )(&V0NIʭb 6-IV63$?o'GC0x @=!xurMB!I0B !$X6 k%v?X; }SJUfo,DKXG:O\!+)P,6`9$'xh$PP " )")$I۳-= &`HFy5x^iiN&%D"YyrL;}8g.da+A%n#x$xNCu@~.?47׏0_9?ǿ5ikbGA!e 0;#q8/EAH W 4 "@\gs]oꘋ7t⹳!]+y=t扏qXC9Df<.7̔{1ϝ$ _AvW<3 s͔*a?p,]RhJP$*!-BD|c'V9'y<0; /$>\N\W {W~RB?"ˢ(>LQڗ]қ5;o|O޸sE;Q%%gS$TE4.%yHo*$bǜjS22`` 6Sh]>J,0? 1 eu,6{o c2ȎtB:{g^r:_u:\}y¯Nw^AH߱(3ҡW,Ʌ@}7PaD  ȖP/;K[t-$KrI}N+Ts/]PGr t+{lLjZuѥg^sʼn ;|꽭ş#ddRemvL&$beR+nBDeyM=4re Yz膱w:J9aعq`5Q2lhxMOT+'t{8sbՕ4'^7 {=?vqzgC v="y4 gdr+Ltw^PڒO掿]v^ճy~3QJ-ɐVXD` $B` EEx 'ͯд@~_͗̈́3N5O]Wkz+*S tR !|K"Ny鍼JNE?({g42\ #@!=Gr( 01%ހymr:CS?sQ`!cINAl3Y/1}Vom.'1ivjV}#O|U22mˣ+-U\g?t nSs) m50[ʏ['PɼUP':E0j󐏚wvPLE8jn19鰰ȞrQk"\wc3P*M!o۶#i%S] ☑a?,\@<9?8f:?rV~BF.JmVyWIe߻q7ɴ&y|řjeGc |Ɂ/ez&M QI EBaq?^=g CTXD LbːL𖆿!t.“OeB&%G$J2mO-5L <{ X Oq~6uҡӷ^VM]pghz9 /rͪ}ee`x圽TN|oqIBӝBwه$>iþ mk7mN+=2}<@2b3QuV%!lHV7Uǎԕq1ւB!{l 9٤qxIW 't7:ͦqIvHW)K^ LFsB2!NV6lĸ[esZ> Z, 8L9TBr1*`X"@mXuCXG[Cڅ|YtJ"0CvvztysIlp&{ ""L[ê.? . B~Ț%~ L[֋\KwNë1tr)$#si 70jڙKs3Jy7QfUm?8[ 孏]xj>pBp'ٌӣ/y|{aA֢٣3 2G#3 NBcAR r9tv_9sԩf(MPN7q{+AEh0l$?dY:͍D(2pÙHqH .Cd7! 94FݲrpS(.^a-/8K4gg6EE3XPܺXkJm9ht(`GyN?N ?i+ubcDE8|TVFvGy]uv#f\Nz/fwCM8ht(L \EI?J%4&{|w1$~GBӨB@aUq霻7i漂~P` w3iqSQxzy~?b(E]mHL Eix\)k-Z(>H_bD3a xZ¢eUyzAW+--^F.$tc=D``T8A_MvҀ oqxhr:Z1 +ױ⭥Qg"ά /mYdEYrnTtM^iTJWWD jǎ6<$?T  <FB rЀ_@.,R^/kH]D$Ӕ<HSmV*"ZWV FMrWݣ"xt )ABRI~<")ȑca (kDϸzG)g H%@XE0挕_<8<>QRc =40hkk5a´˕2U} L8#?ȑuLP=xuG.{l\| <\pWxƠGit ]}K`9!7pBf~&|+:=A{Eq ~W})oO>+)'$2צڈ%y^Z$=6?ٖFDFw^i/ >.JX;2/l(;w,8p` -e ';,Vx٬Oxs GMи/X4e,;}qEr!r n1sX8 bwl-kt-݂! }Ы+  oBOClΧ‚{E?ف4LJf*R1!@!f"@"c!@!f"@"SwlѲ!A Ej#Eko6mΦ-v '@Zɂ@ } \叜n.( ! ec\թ::AD 5@BOqE@tMR'UBBOD;`yݕn.aXǛUʌQzݵVքG`¥}sc7W86.~fne4=DHnn̼g߮yY^sڦ)2u>䜣ǒ&϶̙,U8bT,۟{ip CZ79MscM1٣bxx%Mz^4z]/XHd 9jiႣ̛{K摷?_R6C+K2WTPH"rd^uV/6O:tݥNyȶ6UCmrmGwZ!zpS鏄}&"0)03}&zy @I4,_rJC9wQbsg{ݳo?֒{8_QQT6ȶ%/6Ƴ Iޯ&Qx߇rnxpv,+'8A"k"@\%c(CTӲ;צ9}.е uD`|}{QKBc0;WB * @<1"$&H9."@]FNWTGA.u7EB7L  @ł0AJ[#[ΨW\.R2KM~=1t  ]`;DHJW,2?\EWZ l&5ԠZHAD ]Tt)9[ff.m\" 669miORB.K،Ll i@Ѧ'CY%YScjDsD@7tlZ!:qu6DtDr)*IwXNH8FBO+mFY.ʱR^8x/:u Y.t"+z5\t6CD @BOA}D  D#KB ɹ0;WB* NC 6CD @BOA}DЉN" z@Z h_W-T$#RXĠC #*>UBHͣ[m`MM  9YcX5|+K ރ3L)$xxh>ڱiS[QJwJt-(VՒn [DQM[ꉑ{"sd"PSl"g&Ru4^͂7Y,i nsC͚%LT55 :^4mQ5¢+H}yr:WC]?Hfb= >Rk#[b3NRxٲsi8>e;Vl6S}pgP_ǮQdrR`\q Q"045)"5UxD~MӖ)ݰ{^"ᢦ:UiFya*KJ[;$A!Bgf~/Bv4ܳD+ {shzރw聠>FMs#<"01 G~ѡf3C*%nC*uF2|]1Ǭ H&gX*0zOl{z~.D84d Aa=~[cbvJJM3#Q"0 l޼ZI>A\ , ]5A ,+(yFy4M/i!ǫGXM{`A*r??p4Xa ylwPlAv[&۠"oi+"Xᄼu/crt l{향SE>x'Rsm#'=}hPӊ~`#{V@D 9@BOqB-D$A! "$H1N%" ">(DX@@ =9 DD`P+  @P:V$b]LcU;jX2FI$5&Pw rȮXFG ʂ+Yj,ު~+"% Ç@H:VX4IDHmYAw-%cXVtZskh{&H9GҗPV q $5@B7D c-Ja!%L@L)X“rYNl)O%Փ7Rɉ%-pQ%DMU M$ƴ .zL=VFR{)c{ߔ WеYךE&--iEjCSM:OrH+i$d:@6O"š@#BDj"juz *h6`=t7riJwZ:vL eNUWAat$tdf%OrbIѻ}vSKi Y6򣊎z})QD@/<Ï F歐 pb“m;&%օ%'9δtFƥ߶Hq"C@H:JWbMfVmŰDd 0Kf,ռ]:Fї8δD9 6sQh_皁"Hc0}n~|M#"`%GuCD@ h5$t-`aD@$$$T@- kA " ' "hA ] JXHHcl:-3FM*$՞l-*;1@iocP'R@VP*|V *BB;"/c]#&r GՂ@wJRh"5Dp9`V8tU4/ȉ/-pD\i5 *<R,a3D UbavoiҼSq[o\Na93ռ.xZ`,WoZ56Z=tb{D`PӱRU6!͎l@9=,VMU؄,*"9mUdX倣6k-uGwZ~K +#k +" T-{h<8=aVC1UrGӖE5U1VI ;Y&ͪn(ӱ+'&IVC)=ysX0cĈd14+!"Í@d:ֺj=sY3QpаׇZli#M  I@d:VJTN[Ejύi t>Qg9hGGWZd&d^x`cΖ{n|jXH0}n[C6Ҳ8:02c!"/,ED`@BbD ;7?7tœܑ9YY#22s32lL-b/">JK QLBBO@=r9tv={̙cmN5Cioorvn3y-E$ T/9x%fsG@ʁ==СfrZ&!HI8hr!DQ+ ^S v#'%FƐГ~$рD@[fpx=( NQD =%jxwՕ"Z9/m@BOFCy?>ԣ*N] ]@BOבGME[d6?t"-`3_~@@ԞQ"@ Dc3frY_|=z:LyXjB_($t6P緜wތ`NlzG7?![%ȥJHy`~@/ DTOB˧|"cltD@HxQ  6`+ 5H]Xբt)x q@=8"#P( >q=,`222x@B"F!/oԄ Ə/WT52q#G>) 6<)${M% 54ᇯ߿^ٻ};?l=EnHPt@mݲtʠվ+Z +*#'=}hP~`#Ǚ }PX@ G)iuR8zJF!":ГzQyD@ Հ @ "f @#5$4tDH-Sk<DHcxtDH-Sk<DHcxtDH-,3ADjzZ= z**ڄ iuc;:捴DFD ڛyGI;y&@}MMbPuD@(~l7]@DHb~`E"EIl  @:#S\Wڎ )rI!ED ]@BOבGD =E 掅iP4 ~'u>|wedr;,՞iX{D{?H-Za#-{#6B?s!b\J#rsƍ+Gw("}7.bl=GGC/slaoO폿] C?8!t`7"XG.a"v>N$QQah 0NG{V_y_KOݳ3o"CG{MK:$Қ |Tbs,:Bhp]f;oߺeuI)c~|s{۬H dǗ 1<HKAB{"aw"-}T`@7D:ڛ~b aZb׈Cgy ~.n]&4HJKJRkSMw& 0ho"e|c WotwV;T|TP[$0dPBORK5w%Bc'/0gwȥ!%K1?*{.k[m;D?Ym4C3_XA?r{+[jyE~y}58S0*gꪑ^ݣSRx3ao:azqsE"@(s˞Lg{SyGLNZ+(PG#`⶧H T~: v7iWק> xvЯ J^xzSioP>*5zַ^ (j|>eչ\,=.~~㝻: nb00j<썢MtBjЖ|AɮO'27vփ1?O rƃ3Wڷ? =-]Twf͚r߭ڤ%tLpP%$03lff}rZX2'zH3_uWO6CioKo* MaGOO'-w(9q+[?HۯZ*;5/y^*թl ԴY#Fe֭WE7*0 g/AzO`rK=-:W25i4~-e? ?^-6R2qJ1:nǏuU_,l&Py+WEM?ɑttNp럖AcQjG~v)Slʴ4\QзGǾǛ̾7BTl#z#̺Yx K/Bc* 0W"a=a;[UD :S?$?48avE7܉\Bl Áy"iR7^z4\8m28V _yoziai_lwT,|1䱚rԐ ǜʐJ4$ː:8]Ǖ=[ W;FpϽvz`O#ow}>x#Xʡ|o s\`D|{#P]zCț22K/z`U4{FBօђhkWf*%|HUބJD_>8v~M{dXIi|^X*ߟ{>/hPkn'?Kfxo^]KˋG 0lgޛȉsۍOv,B{S{|ՐK ~-#;}СIhQU`|ݮc{좩QJ.IgS'ڰjql hor hB>pͤ,@yCZmoJ=E8oj9zN_hosASI) P`m}l, i9Y,֐-QZ`g 9^_JB.^( @DTBY.BD~Ї PD@LA Q" Ïj )dY99e#lِEDI}֓۝iJ(@D(rBƾ@2-Q!L3G$iᤥGGВJ(-e'Yy8 -Ѣ´C3-j%owAPM(kJ T2 hnYaK E'RJEo2l(@Gj:8bӉ;IENDB`n'6pl;P8M;PNG  IHDRaа?&sRGB pHYs+'CIDATx^} xTEY !a $3n|85*8c8"8>1(u!58* tBLIw{nw:kO}E[߭sN-WXf qkE I4 7bh1_`*E"zr_(ZD@p#8~|Vw+1eƧ[$QFFd%6M!FH$έ囕刵_ͦc{Ĉ14Z,pB LbH,ВSdeZa4E"R&+j*kݚH:GAIqI1$i"> @x\r @.= s&cSɥ z|'a]Ԛ@A9b'7dbp Hh2 &4Fh!MASM{V@IcPw&$(d`LJbR@Ľ@GH4#;uÂ) N&5ӷ{8"@{W (D ` N JC5"\TBJ4A$**\@0r7}pA7GˣK"uNb)[sa\ IL'6&hz-W9AU5*Gi?FM6f{DMA}Z|77X^R9N8GK⣴8 Gx' )x9uCCk+ T|PˊȢq#~#u1 k$%WS%TzW*2PL?SͳR *D[E=c"-!;5Ax#**!*ZPh01i9B&A4f$ `e$sgt&sHC muu6G]r9vjns8 Zی+*U7Xqnf//q0AhZΪjAF\Ԝalu*,V+5?+u5G \Yܠ&L.8AHT̿/ LF&sM/l"jvC|T~;KkZF#!;g ;VS #h[ȍfʹ?I$*5E$U!o}VWu0$Ś9˅k#v;8M}#AE$%Ib5i;dV3U7ݻ"o95և#N>FUV0O NGj۫ ܡ[8k;r3q,SJIIߪyfg\ !S% Iʆa<vhpNLxC@ ր`!xKޫq?wĜiZtwCX H60 mRlbb[GU5ɥ\ <u#\O9 DFbyY6pA"Qrx@I ]la09oDQoOϽs4mSOr؛;v׸b>unY# L)Oz湥񈃷:!o/+f D3}P-=C8+bЄ&gk'^~Nz P<,1!:L^>[6&ݛ ޚU _!GUxg)yz'R_{ Xa3_ќy*/]G|CX nb:Q<=e]Nzn.uRȧ8O-Dk^ aE(; [24wySLR/0'0 fi߲ M *$rnƗw[GrS+ڃJ+Hh0ؖTelCǰmGҖL[1j7UMRi"cst!W7'yc&\xRprCXQ++bi)+rzx::3x\.Tқ؋ {\ٿM7ٽҕy^w^4~G4OC-b?#!"@1J:%5%Bä9݃)A/zyi;у_(MplP$AeG!x6>8wm0o4? &~28#V#٧/͸˙`$TBʾ?\~ a:>jt" _im5mdh) # /~֗P뱘6L>6"_8kTyA'O)a \ۏܘ_KH$iCa ?`ƀ{:~YGJ#@TMnûK& n+Gb!,w}[9-}448BذhV;Q0hF }%޼,ᷗ/DMKU˂Ec,ɡs O$NXOzqܚϿY_c zlT1:Gk=Jҍͩ(}+w"Pƥn9NX|*ӏ:p:<tIo2BY rDEg kɸ @UkԈRL۬aj2 t.Mir)\|Q]?~ZH9`7fڔI\[Ag3x6fJgcV\F[%󾥗6V7T{DPUZدt ܘ; tS_gA9Ap]߰;&1\v$äF4=:uYr5E PmԟT8>wB~*~uȴpر(OplegnYj)ʍ=aS >y:W~ꅪ1[nǫ̒G9GY߃ _( ~]o#k:߉2yB3y6:wOpcڞm \i} mB@=M)"0_d bW0z1=!nM?7yZv ;ApqoX],;%].Qr*B6."uB þ%Cz^$@T*/o L@*pӘBW~y{xo P8+c/~5Z/;x擔ΚPSSO.i'>Ӑ~̈́'Y}aky]nh|mHvP PG\Z/\løf) IBl8v ` s 8nw;5[vvV]R$bY`}$!&, 8p0 ,F^ϗ+^YQog-#da uC =z GDHmVt^t&%48vpHn-8:rpKdR# " T:ڴ}-Q#l1dD4U;݋H!! M|^BS$sBm۲?[n{,HN8&#@hVmyJC Qm~ 898.smTDBh2<0  =J^>p<,dBF\úY GsE" u4jbTKШ↗5 Y>ѥf}Dy\]d Rs@h .& T†3LM]ϥ:K%9pVQFhsL"21y !zܱ}NonSm9A.@9"95V߻ۊ @@|~4dg?X Sa!@'D~+f9N\Q@5)wnx(0.?A+bx̯ {V h@8IcAV}9 ʞBHJzʏ9\l>Y)s=n@:1@\ܧgnQ|ťuYgk.GPbR eb\D@`0L;#2*saAΖ] ->J K Gt"#6>-#eѫbG*G_պ#ܺ9/Λ02"С G:64B>;)X,"t @gFKEA9C`B.rDyTXQDC,;v,xxV]g䈎kD<[<2G@d;!"f=@ZC9" G`@`"  Йu "D#E!񨱕@Pl#"QcC@ 6LD h No"PD5U.EKzHvyys`0DDf$eN]{[rpaرO˝FStX3z~O7D]V's/3iڕ°c9Rvd؀Gg>;,=~R_rkG}K;?w_Om'~to A<;qint7zXr'(9f4Bm; VWЫ |^I4M3:gxOu"xRc2t(ʵ#@DX|{"r.UAx,H&ɚ"A(DV =ApvNqn"I#ڳGcF-] /0O$b. p&PSdEx #Iayq$@k7ZYV =D3gn-U[RAC:#l#-^EUqjˡ,d:=2:PNY B-`.9@ [f%VLBoMC-.?۰@J_D ?$xGnΤS>t뾔 JZ=0竣σ#x7U!a<=a=, _)5m濪D` U9`g1"K}f60"Sm; b#*Yw]?@~Owyc'9֫z£iJ /n-\_z?ٙC&߰(2d_U[[U{x J9MC)ZRzdz_ƒEﳐ35 mmO0qU4Aԍ#YL) Bǭ2O͔o[,;q({TjM$V3:LO?s,gYkuhɞ;DlW^zxCxp,nwN[P0\#tiK'BtjdVPL~IQ\#'xS|ij7%nJHJ?fIQ1д D).6Ix8 yUt{0u⤹Z5#_1 OGzuw 9 52q#P|?%udy6"mS\^ؒ'{ fnCfmRtoGYl+= !!Òw-;!ᖍ@ ö́xҪ i]2:vG|"VgW>U[>mī5ԻB1].m^-tCT\RF͇Vl!j+8{RCSlǯx*&JBڠ|9߼doeM {,/.OIˠͧ\P#i :AˢwRSnUhki% /Y Q@| ƯlGU\}6Nbrpb|?'A+07f9@z*kR]9vAS)#Gdn*F@KOfdN`h˙4a2 aHh$SŻɳyՊ)-\1ILxtڦ!^֤={x&O7+R+啻g?=oJ˔@j(|7}?ʼMms]taXk :HJ2|^pN@e }C=*îaNKFd b;re.nyGhk="HF0tep/ K`⓯ 1I$Ӧ"Hجp;rnWoy2#SRM\ލVhn!e3T%Yf.#"b#/-W$"^så[^qH"8N;jlUj{]W5[-3 !sI?N^K"8};Zv'>.c[ߜ7SҮIO NvվUYu='lgvBAD E s}g_iw:i#zhtv0 51cM Xj [Uq8ju+!]}o3S删"юa(f rD⏥# " 4P9Dh'rkw..ECE["]m\cT4Oȿ8( SLA]y{lu*LEv(}xw:<:wRڻl4586ڋHk/G9"xHmBck)+r.F#t"#G4N"91 %nv;ÍsD`1mon ;x@Nx.ڋ{qԇGm I*:sֲ=E{Dcsxyqq ʳ"ʷ͚U'$zc PVm?V&xUCMۚsdl  K^]9jlYyl+˲mVybJ9 GBIިBÒ sjL%ܚo/N5E2i 6_zZa̻'^7r}jz"FR!,FbsD\fUci|С@߯)̦9*]n}CZ5v5LjZM'6ӆeHhiޔm+%BsS@]u"e!8)=4`lbDsދ 2oWW#3uSR~h2̥3חݐy={x&ϟOzPe7+h!zsUiOvv>Ѯ#e [{Rk®zx"K9Η<(KtQEZ{5 =?B5:p'%Sv{z 09mhF`stujɒ,]C3nxhc9"K4Wkt!ى]tyYrUSaP5+ى*LPg?9~(Kϡ:u/ G;x\Nڷ9So]qSù?8{JFEZ{9m]swTE6}<ۧ| V/^5:jurh8h1Q-ihhpmx2*nO1W*70#c"n- @`+p3\pb>@D9{>Wl".#…$tO2丸XصZj˥{"B/ zEEbz2>;XZTA%jVrk 淲IT-ʮܛC>4AJZy%ْ ">S?SA@vD &L "$_IENDB`n"pjDb5xSPNG  IHDRTsRGB pHYs+"yIDATx^ tB$@ GD>9( HEEBUEQ?hE*F@xyRi HHd}ޙ汛d{sg#,]xxX p@qA2&@ ER[&HDBd-7|0 uU(zTTM qI 'Hbqͤbdĥե[t?:($'ʼnq4ZtPBeq/0/$( tDy"W$YP}Bsƀh'zg8RQ~&;MՕբ8ۥtMdzUNnҽ߫/nK~wDp-N 7. $D{A5/FE8)#/ǭm':lF %J %RnDZg7VGo`jj NƼ>lE(n'|حnKmuMS}R|J$u6ē0}QӜܫ6gMjzu|ZUl*%tCI"du#@>BN W*BL 5<2}UBLI0ŒxH't 0mYhzwa3,zb6b6[~Zlvedge}x]jd$[X@ PMdEwV3˖ZMMYK SRR] Q404m`*1kSxILze.I$1}}WdALQdjH5_LtY#h#W6.2dGLY7,Iwثy˩eƯlzhZ}zʹCe]ljJ@+/H=$3cIJ7)[`y#MOo(_%/oRw|?`}fdK9O+-zSXȡ%ŧ'[oSh4nSqyo^yv t>vA?^-ֹ3%xERՍ%&6;>pHZ?(ue m+`%v.)]Abe|.4X+l ~-H*yRxY7{c+eƀ(p =f/>a|RxIчAF%Gsr{P􏠃^-|&Z@$KBjZ9|gM+1mb &=*3@#E;ԟ s}d6\]0G(Ok| "A=-v$!t!qF^ly @%gއ$tZ$ *e]x5IX3z6]t~ y.8=j7f5'/ߓ×76u 4e~W}ke%GXu%sƼtYRNl^i)x[E ? $<<*>6w4kg&]FR0 ]*4 AECW%VotVJ$ukI޲EXb2h5ԉgC~ ۡAʦԵ)rը[<6ZWɗ-Z/.J篗~(}YC-e)?7_o|5ӇqrMx _x))*>2~n}>} U^93'QԞ4__q;V潦Rd΂Ǩ/g&Q3DvwaJH B6{ a#,MšeM' -ʕ+HP1QEWtm ֕IL ȇ.e{ ~`(MMlHVtRw7'w mtt tq,7M-|ﴗ~Tgc]mrz'N>θ}{硸OX=\|ɸ|tff+L4¾3t漑i^f%uS% >nD肔aLF`g#kvt[ SK.Af~&E:!D&OJ Ux"h%%>2kǁwºl$l2ut Q7r`1AMDi.ѹ U\~.i\Sr g\JM!qOL㉣`N/;4ػhybߞoMZl[+S02e .qҮ":4 D Xѳ(? PHhqZ={?Cg(מ E < t*@n,&0Kۣ#9 hΔ]U? | bӀ 5/rem}>zÈy Og^L?\e)6Bz OڥEV.Ҵ/T s>&-de 0+?DeY0!=I^˼0'FK{>.>1/$(+v6:=D8skQI܅^bbo=3h7)6vx־kv.rNK$XLAl~yRqD Ct%]IME|+]\{ܙ Ff2moĞJ Eҏ~p"uKes`7&&ͱ'V10HX)W&:AIvfLK& g izv &Dc^htAϑǙK&CeS"mHGְlm!Öm9  iʟbΏnHI%\2t~ptDҵќvt;fTRL-D*.b}ߒKGa^12H!.z%`!eϙğǰoP.C>mim"HOq2 vR`HpaQp z)al6G>5=!xl'CH+!2 4貉¡-n5lC > SC%2Bn1? m`$|ԽP8BɁ!K>H{DOӸv6ߣk;yBO^B_ɱ/w{*uebGIfVu@h)@K mH#rUD/$ '@Hi(xu $$:@H>F7] $@^$AH  }{!$h6@H `C$@ qtսc>]z} $@ 0^yM7{Q=:D.}`H D9KdAb}Q~H 4G 2$R]  $Z!e!$T(x) $$@HYeF!il2M_rbH hͤЖsCH &1w$C 3th 9 '@_]@Tem,@M@ċ $дDc+ $PH0@k $rE@I?8'jj EHЋl!0@4@F[h!'FGH H#:[4 j#8&6X~ph: _Ma-fCr-$r7;jc]h͉5v)@]"/2@ h$GcH HAa4$Jd4:ڌ@P"ѐF nGHm)N4r@^/Ӣ4_XSa i79kNʃ Kd9iu}2otz&%#v5~Y&:$7mI{Gz D~y1]c,$7,+*ʻ<ґ%k N&t<@d$!uq>&_[V/ܵjk*%gX1=;%8X ^ jA"n ceˎUșg𾷟{f&:Y?Wj𮋌r 8{tR"a ʟU2MQtf ^G]~ {D쬾=~;}Q0>kxZ@4Θ%ou=B%TY=,/+&L^aeXS-Z΃F7w#!g.@%RdZˀ<>|d;ڲy-"0z1E_&O}{*.n sU7ӅҢ1r&v'`&d,iʯ(n cf/+΅j%[U=G u7_ C)?H/0ZV5:׸]j&O>1 vF DBֱgԦY!9d>tMSLw9*'2;95Kx.Zx3C>;X"=5~ɴ(|v#R 虗{9q.Cڍ J?%]Fu7_+YUY7\`]$tZ k*@t-)p)vUŷv޼5 cgM1FE(F+Fj]sg:MsmdNqŊΟ6Nj6{uľȰcqnmnQ1L k')?tJdߧXM[+̂ 6IHd٫_U(0H'`c'aw:|Ƨ^HC@'uKnnlKھ?B MmQّz.L\pս˦saC;2nE,eG%wQu |fQ)_|5[݆LYAƋچRs (rEe5甥BÞ>e]*xʤyul)6 Q NըʮL4KR#? KA\mEz`sU?Tj*uo)ҏsnޞ[YA֓Ps[koޭݣrϫttݴM[u9{I7/q}zMKߣ~qSa@o̦,=rbxo0tًԷ+?p-~V[Zm d=s֖*}>tޚT/1?›\_:YZvρ5?dζ5ݙnIhc/X k}m{Էn~~zGpd}kXɩɰ=vM|cY H{`x~2l=gJ";m,4vvf{AS' .ć?1uWjМ@.ۆFA:smzl4xѧϽCL|W|hى7\#l0 $ p{*SS>7|-oԻ'sr?.vCˌy-Ca𱬨pd689k>zB⤟Vx G rd|bjn)t!;oG3kkn 43/ mjꓓUM&͜a ɓ>|d)TFkܚKbhx# 7}g/)=6wC{UEDCٛ6HJz숿р`]srysmtZx:5EVq]"{G: 3.5 p:!L h$nlhwM)7'Ɋ pum1R<{o|yn\dKԽpXD³nKt;R#Vo־3Y7xۨh#8\7?,$m0Otw-We{!Bý@yvWVgo5Ekhr}N˝!ʀTU&%p%z(,Ti2I=>JGR K:I]6ɤ"fr1ͭonFx;bS375-::b,#mqբ,LZunZL:'*\mIy>I]eŻp 4G1xG7%(z"+Ui8+$^N3ԭWb˙ZLb3?-okeȗ*;3˸Է$Uڍ52\}ق@4{t銋iFI>EGQe0F;YUv|23%@)/$$NH8EtuH"~"P'y˩+VҡKCJ.% U_=Hg ?Y}J)d%5AOLǝԓb kN{ }N3hPKޜXpaWv?zo޻$Iu8djfM IL|mvHθ5T~JA_,v>Dub@*PRQ16* JB-:U,VB+@iL0&aQe tp/&0z̉zCH*&]wrքDj& .[n?`NXbŇA@@AHI%CR5UoTIxiԠ57vNۤzDNPc_}}_T'šH/ϰBL~%#V8V|AWL-/6QS\ J{1=k[)/3^th.;siR}ϚvSZ^ub{a;XV$L&E{a`cSustTکgG{ETtdsyoԎ]$1S=Yԓ풦E;ZRN'&ɆU&nVnu"גwJQ9nRQHOiDJ1='tUjYf8K@-;__dalk*mS tF34U()UCsGRLQFi<i ].4j+7jbemÍΕzs ~["$20#L:̫P' y=WD+4(?t 5x=K:w]Mc뤧x4Ӆ¨?b0KIaX=o7RNzڮH_??P7/֋:ܣ]WrkW) LGB^b'V%a\!D0";){Oֻģ^?`X㹅 Τh&Z>N4@6- 㔞kͧ[]n3iE* hU( 1O-|u&~4nWE<5 ƒ=%qRfh2JLlث|{BbJNE$zXS)\bɵV 򽍞\;VYJR-m>[f4*챂I+(MFYyk R3h_BwFvީKyƪ_x~ H·/wuNvsBhFf«wBz,F]j#@r(z~TSCIͦ/kkSJ![N*vtLW5[mF0̒캕>Iuʲ93y>'i vr>zAYBM5Ӵnx:]| 9OOk(*ٕsŒ},f/~R:z4\pǦk7jygg7jJg١`eXd)Hp+)i^{>6 @1yM@32;es'Ioʓ>I*'ЊRsJ`4uϗ* ISzʉd^rڍv#W*1+&Sn3(ϕnA9v+_<濽<럳?CysP,ڪQoW?pGO国Ҵ˵:_[f,I[yu7G4L]s]ԥ.2ζ84L_U)7oe\B@1=^*z7OoN<=<`)5MIHt"VRFFm.u^=*9u<ĚN} w6?,BbVKG4Oqu"iI( MZޤ2Oߚ惎qWjxW!jR[ꥤW_OvܓKjJRI vdO ,[Ma*;H<{]iCO"<ۦt(H%KSseE"xH!%'h"E\ۄ33(iQ;J,&2_ѥIL|=ӳFrgs'k`g:l̕RLD>;WTc!ɤpݤN:@&y=913p=i%n?Ҩ-5[VaoDXIghTh3)z3$z?ץA зR\4ׯ\.kI[M{LNLM6zLo͝Xs*mNpf{ @OWup4ϟ⫷zT<i,^?$9"YKC$d.5p{?D"% <-0RI@;89f&u.pǃkVPtnW;U{ dp0D;4.yWmAqy%IcBhSʥVҕjt?]h#- G7k*zE/RG N `.f9K z qxjԁ jϬw?&w)@ e5P{7ń BH4@Lq @$@P]Ҹl~T@  &@4.6T(yj{Jpxxu@ dPݬbSK愌L$TiN$R x)3@0f^2i{&(h@  2elGƯ(͡zҁC&,X,g dwH㌈ @ XbU%lq޶Jgݻbuk㒗 7t.w}Ji~ioe_EvirjF4CA@@PŔ {`IRLFN*5> (*LqD )cS -Ϛbbu/kN-_T'& IJxPP6|רi.ԞYp6v򧍰k?$`U5_ e`@4* 4Hk@ * @LPhH#'2ET%4(XP-ة=74fbn~ 4(  }(@ @# }Obm>@ N*"@@7?VZS>ŏAb4VZS>ŏAbS1  :({6vE]3Jx6: Nw'xm?Wj^[/  ab4z%o#eJ=mm`k-?ėP* 7 J?++seu`FK O3ӓNTCL:q4N:ƫi.*w;]32!(fLWՓb˫#p85vUq ϝ۝ũj:7a/ G*^t+ѬE*3J v nU}_2qԆِ޺`Ȅ/|GQ#6lR~t+NL*N*4a&?%d}5Ks&B|EQ ~n-IRr.Wh(?-36 9cf?YֹnEe~TǟcO?Z:*L\Nw?keR1[矒09i ?hRu[L$''3uid =Њw/=zDOczʔ5rRYkn2&1K'tձ罄#O1yf+(vgP|o`W?vPu ~㼿OW7y"0i@hŴy Zm)N=/3j`X4qxi^,E iOP6"Zí^t/;22k3V win.JL@ Oʧ>Z4OjKz Ex? ]d0BN?'5?uQN(t"*Pa)BZݭ,fB a%G6˥]jVw^6>lTyĮQY.Z˴˺)tԑ쉒FI(eJ+.'IOuQr1?i?lsM!554Oz&bJ;wɧ }]˴kcZӦ;[Uu}t(ҫ0Ή#-1XT)@L#P 3?OM35SqyrpnH }-A3I <8;lIkK*wTiL* (mD@ gCp~de"H@ @L㫽Q[,/jo@ D !dA@<_Q[&CLcQ;0 4mn_@ L al@b4)x6?L @e-z4@jˢ^ a% wO7mPÅ3aͼWE{2zU]DrÚ/D>dN"' T񇁈)q !&1 1`$  Ψ%@ @LC Ƀi|3j  biC& ~泼tf:@@ Hӂ)\!BL@@ . $%M-,M+#Cp h]GZ t4\;KF.2..震sI" <|ڢ5G,JFl_+{4W{GD@Bo-æmꦚ]t4B*]St=YLȚf"E3@HLM_Ec_.0W誴U|&Y|  -t%K3{H ̑7:L3E  UË庨4v>g_!R0pQEj _;W\ @vBIABIF./ '16F A@b@ DT7 j ymIi_F  ꡛK@@bH@ @@  A$@@bk@@bH@ (L絇54x,@q #1 K  q:@Dޫ O7Zk)BZ?|U*O-? ً0h) 4E+x)~rF0)i "  b*+=pY{ 2X VS=bwȂgyo T0iBk馽{'BP CL7/Jj\ɚYA )e9S$-ƹm ιy:lvU3fa9g7/+˿nȄ/p^h_ʭ'q|fS 2gaJ"%w력@RWw셵)pƍVy+\.pX%eCQ/Z[]tȖ{.zdK!`ʘPq.! λ-gݻAv#.nx{`9%+p \ )\\|ake-y)o͞$1ecG,>ktMxd+1F5i>mNc{U䲺 !&?:'R={.zQ_tcplPÇCL'~2̥%sH;Wј ksEٝ΄aִ sh;ILvK!Kg+uCHFK*;s11۹xQ4=]O[]e$ #t䤲 趷%퐴Vаl//!i ܁!r,duLV;cuNִsg[Ë)&2BiZzb״ eR o{\22D-?OKe6&dO1mPÅ3 IHZ '^g~CK -EFJ;EVU+0&vNb-aY ƧS!$@,1)BL=EʵrD0<yYby|P"E@ 0{@M@Qb9_@b{?pRPaBRhе1URLG  :bn~8#e8"1FUABGb:H@ (ƻJ qب"@ @LC9i42 z3F q@b*4@,'Xnd @ l aC@b4[uiP##X&1E@F%@@Wtx>4)3x;{v*%D#pPnkSĴqVLϬw6M#ww[GABAb H@ p1?8^|@W䫞3 QE%jfW5."ڹeaIH+™5)l2{oz*_i= b2ֿwdRf-\Ś]x/46.i" 47;=o:5O8>3'wL㭾b(!QĨ&@6Cyodž8jCyybvnx|6OG݊G.ROѺ ƀWUC^Nbou_y|@iއ(Y J2+i 8a969ա⭾Շ *D:F=U Ku.=+Q@L#>Db&>>N)4qxӽcU_i ܤB1y))Xɳzc޼7d^;N|u 26;M5R(,hQΨ&iʍy=\{>4y*hȫy9^>\g-Sl{2STJ+5*U_)uf` 5ןx6>9!@MGC58{/w>bZFw*uвmKr8z[ߪOa/U\n|Wn;55C7R vol|R []Oa {חSh(,nD.fCʬ1v?ŒG&uhk(ԇeVm1iL[kWwqagYWmk8X[t''r(w#@ts>b@'{Lpk?1r@e-Fu.p՛y߾U?) 6j-|]>?`nQ}dǎc2~ A 0Y'xWyq#lY ?'#.rRڻl;Wҩ|3w:xRH:;!Hny 7|!lާb !{9ֱ?*#W-}~ݡܼ7o[E.+Z 3OUe%;؆Y5+W_Nw|'X`*D 8= xJ&g\}.{;G JIXNɵn۹xJ;4רԕs٭3=v YGOݖ,t#Cs%+c׌:1VzF7Ab?oh_'fX4 5d大ImURw͚/2 A@#k!4zWEU\*㭾Vp9 G.+z=Uᰓ)ZL㭾B7?72gM}t{Ufk>wZ">*UI+>=AV_n>4d5mv9cD_ڗO?7ccXKA⭾hQ!.w'GcP߻y31,͍_y7Fb5" %Œd$z&s: hWh\R]b0kݬ"ސ@SQã #1 [  G qب*@@LC)c*eL%3BH|8ƒe4_`uU{fsLضϥZ} φ>߶},(]nkiw\|@)L'1?{Wbj;iw}#\}ŜY_L7t~y8h%kQwX| ğ㳠(4|ӟ+Ч<U_}R.}*?|Ờ~'QGL}'O9|l“bOBPÙB+pm +7A@.GR'jWsIENDB`n!i%/!ɷ!PNG  IHDR1gzsRGB pHYs+!yIDATx^] x3oF!H";!_0m"ZR+(ZD@>HC$ՊJeI1[{g-I^^^^fs~sɲL<7[3YNQb49AeI-7Hs _]&ٳp-z`w@8]Lوψ @Eiqib#d, c=6|T4&DX($P 4@]AҐF)h mDC[WKm/@RU):%CX"%v6ƹL):0E5@ TP,0)[]}+E$UFȡ%CXmb%%g*ܽ_bsᑬv7dx#LF!Ղ:Cİ Bd9HN/;f*"VœM3D%(1`"85(ׂ}7D6]qԎm8c750p,sGA\C. kUHHDd΅D n!v,9'$Ud:Rm'EK92sҕp$:^e OaӟX`I d` @e'=i^]챵 ɥE,+)9K!!3Qx]ZqndPsY8,<`D3!МvNQ1HOdGkT^*Y-.Yp1L줎XeU㥵s?{% n%. )HIۮ5aBE>25 !G<B/T\C.(UWUANC(`v@ 償de*hݯr;Kc-G!^7ӈt*hf+~|q_|f 3@uA GLLz mCu (iX*Ϭm5)hoT.uhGA /V WY!&EBZ/ɳv#nΏQ,?sibD?s\4bٳ~۝yshKmo)oHr+&r4\+ +#ËݱSGdRB)PI>:gӗYdDHѬDE@p\ >t5,8!@Jp,P3z4Gϕbks~}أXd!j;|Q`| -cy&1Cݰ-K:jIFi#+Dƶfza/p#17N-TVx0Q.k~R=3,P<:njFVpkZ3ՔF"Lf6}#c`cy̔Ek_=HfMkf(Iǡ9֩9s,t@FJt.DAZ]1 =YԮr2\Tb66qG0TzlGV\ XR[Î_$gh`&z&'$T(%鼂wfZ4;l=NcS{v ?/G%d 5Sv'?>\/RS.Q}NobOV΋ioO>j/xe/:Sk2u]~^*T $U/T+8r.%< 1=24VN~s~n?EfMo̵MSBipaD~9&o00 ai߶;KOEgM<ڸHڀ.b *5?nHoLp.[r'&n|TEᣑ_N" |,HLuLȩmҞsr'1g=4:ᧁ·iҕm lgP);;pyEpi^B0̎ )=8j+h=_R)vc/w+e;320XDg2;6<2 /qytr.\ wepVZUS"o`IObࢣa0av6.jNǬL7$hA.e5uzعPw 'J0iǴ5OXb@-#L0Afr 6D5=X ڂTy|;1M+n?yȅ*Ȭ6~ljIy3Ђz}'ҾKVVYUQYY^YYV^YV;*˫˫+*jgE*lb]p{Krxi\)7@(QS:4[k!T\fȭhz! 7Ʀ!$Ӷ_| :LFTD[&ªx<n%'8)=:??][C"CBo93-3 =3yMi7sJEmLl s5MeKŅ-s64QL x;Lo_9 e`V{* 4O}8! e0"{z90 `~)9YpF怟?VR*waw3Xor|Yzp`xޱ^k|z-=5;IzyTbX *obn/{5rjؤEGQj}ZrgV?5#[@eP|4gQ6?Q?~"Vr06̫!U$!0?YjSZG @[ 08o7/E8gXL|Ǻ>Mnx'' 胎ݞ#_j`}p56LwOAH} $Cj:vLJj^;)RxPJ7->zi0s*Yb%#oepbixȣEমaʇspIC/^wOM"2^YJM0 F /ASՀN}꺡LwDpZPa$_Zm@age p02n(ִhBK/HBG7D 䅞pcJHM> /z9Rk BOšZP`mSy4_?QzCw梤YdV6ImC OY+ 3Sp4r֫kЇ-Y:@f`zKRvd鄑6z55D U逈)!HL=U野/:7LC6:N?t,LuB7p^*(>ARɲ"ӏUT" oh ;S#cO:tfj?{)< z@AD'kgs ɲlzOɣ0ܠ Ud0Aנ~5b@Z!k|@9} 3VN5#G'f5GQV=7W#mֳspI]? Jf  鵟cR%A}HmV`Q9gsRlڈVɍT{9ϡh[ t`zR9MS1$kXY[OSUOGtaHF)jv\b];t)+%ƛ߮mA?Dw髝$ "xC.((m Ɯl*P'FO\ VP)5 xba.mF"e& -Nur,NE_wc}rl>3D>D}w݇~}9 팴-tArD^.Φ@!r*RpN"rZhjGX: "L`MMpX:0C嵲0urC8~G݀mQz1ǁZq~y)Ǜ}q(F&",u@DBN{ߩĭZv(ߌ .#Q,{sStg{UڛG яiaZy_G{OPt gcse,|tTj4=nmm0 ՟>u9}ܰ{nAf,zD7GCjhܡoMW^TM{O koZ>Xᷦ xgN. Z[{ x Dtڙw㻴6, & EZ[{dz7@aÉuQmfmclnJ4`{Ƶ(k8u檪2;O_k=/ٓ}C\n(y'}RU}/?,rd3!5z4k $j#w+=œp |={$xCn|bO->:gw>#̚p/Bgjz QQ@~ц[VVVG2BHą`wVx0lE qg+V?,l{Nc82&w}Iu7/ ^ڨ ``;3F`8nE㟨M#z{Z)ZO[-Yp"-cؙ:±&$%`Cv\2(-̸m(̚#&(QI3SYϜ?]'n_(Ɗ7쯅ۄqCbyg">0VOg'xGNI%E3UisZhmsDB-9=8YlE v%KpEnf8 ڋt".|s)gQ75Wj;)"߿SJOZ[{qxTpm ժdߑ ~z7?=~lRB[[{݀𻝢2o='n=⩿0FKUO@jNedVd4Ffܨ\ kkh UKD훅 4٬C DhE8v4tztzakF^q8"\zt@DE&G:NMocL)Up(@f)/G [H4ly֑Ouy's OEYwwzQ}u'dlP9h?D`4nH[Dj&dն8?`[1QfIENDB`nr"(6<0`:PNG  IHDR1gzsRGB pHYs+"IDATx^] |տ3oN@%vĸ`@DPZ m?J?O"ڀEMdS+*Y 1[wν3-{]¼;w=?{3,#p|qHL6pMs"^'@\" YK"G/pZ=עWL.bوiąQSi6F"qI.  {=p)I|B"fK$d$$>hmAҐEϵE, p:Fj|QXH}nF™&C 2#/ua,qH EH[KFaF$ȹ0g9A EI00D~nl$V Hu q&H69a9Xt K.@Do &$* $Y"2BJ?1t;MD TC@줼LX[?%= WKӔ &-buNd& V5~Q6qң̋~}]~&'o~X%, VSyBzb Ƞ tY8$$N3!Д>G =jvJZd]v D߅k 2lI,NnkХ oK{cE n%. nȀIaBE>@35ˊ EW<B/vzik`j NL! `Bv9l,5Li7EϟQc'|;4P)9& SmI=եuOV)&):sVet&%#UC`Ztctpкa1P4h9sg6238# M!dEo < KFSå=']I^H(}z}\ĩ8k)/ej6 3'@3_n'ANVyxb ChLR S˒K龄~FFNhSxZ^ڬD3WS}֙hƀcZ&^ǚhЈ.7&bP9MG}ۤ(j?a=d%>C* SH΢"h [6S<'Q?x:9G=>mզDT纽`/o@"BP=cLJ5xtm)@ysF ̆yP__Ŷn_ss93g_BXšKRƯLV}KMktrQʔ$dLl%.%^闢eN}d=J~R'gzvlҵϟ!ZڌLqd T5&<4f#5%' a(HpK !dxq*1Q&)So~#1A%3kS'o$Z _ҘLR %uHA kJ)ҬpcܟaQAkem<4^&bԔnɆOȀ?ܝXB]?9hiM%'h-{bk:ϰ:}yulHo|injjSVyh<uD"I F (Au@s=~;Xts` oCSx'i~ Xˎ|~AkǙK@uҎ 6>O ʎiB;:LHlh=P%.dyp 2}Amȶ ) 4 '9[48NE2W3@Rr`iJ97<367(\@L6d^Tnzp|1{>B d/McѦ(c@c4DG9p9z".Vpעf9o?g%t-6/ŊCMdEc}/a 2倘e/\w!R#/9|Wyww~y2cdD!n[X\| ޽\<"JO|%',d~hޒ]hϞq]˗5#~_-Y d ?i0 6˴Yad]u뱝 &杂 ^b9|~ڑH ꥓.||bGU/Ja_\b"$@|ao:[1Y j>i.x BG෰[*)شXx aFxݽx0IkP)\n@i/ܒ_>_ M|G^>x6d"٤[1?$lٞD7S YtixqY~ 4%"bai\ `FXsRF.'乔ODXtb3/4" \R+`$o}ӓH[:GNx%0{Hw;A,ZK , `٩` V"#z74Ԍɋ-@fѥ#SFibV(B֠-D˂MZɱ(:{ucZ}]u9 팷#taRD/LeִI%h aOF`5Lo# B#ʈ_"=^TG׬;VJ^?{d֡[!.=:&"Fʊ 'Sư7`$ صs.(wvQZdZQa-U#C҉]K)q\vS><\2H_ЫKհ[\-9#ˉGT(ch`ٹW gH~bmsC`XG^Xy#p1se]\(u/"o @[%xR "oz꼛m2*Txu6(άtAcAI|!zN%UsFDOU{.uoѓn7Z=?m |08D'Kr[!WjΔ34`V8%L UBauw!ف$kN?cUK/~Vd VKkN5e]ƃ7 w)־]Cf_M8e&<3%hƲxÓG/=c9}oek_,U_ӓowhP_sg/+bpğ1cC-#"JV=, opS`k oʀC@N(!+VsD[]h֘F#2ptڡ,&C`<[韧y^{|>S1-ƁVPw>T"n@d '7!uLz?|@]~+ow5CoE>VDņ 6T=h0Fm "!}ܣ :{s-. gcqҹdeg35񧏽Bz  sf CPSA6˯|;qJ\@2Ʋ3kڻwN8켺urvʎ^Ӷn[p!e_54%1.2,暚+,,{"IF{k("P3Sݖ.2fH f^Z'Gkl@)ΠXg!{ UA*ko BN:D(uyC[ ~ =H#}SI֐iZHPrT`BVXGkEsv=~r_iw<7hnyT_+u?F@Ss] %[y`:/JDt{-G2^'r}$`(Q-IENDB`n(.Q cyyPNG  IHDR[sRGB pHYs+(IDATx^] tU>M c,(K&ڨ:6RQD+C?v)X&"U 0VTeH 1߽{{I^^<޳ΰgˉH\7[BhOd-'}iH$-iV+"qD!B|@8[b-g V 20lbaq`SQA:YG"+nri)ݒ5Iɘ-LRdBiAAFy恞PP }xCYO4S5IK3o;p4ߎg8@׳.5͡ &$)jY8! ȉqAdJ*Do qZ/HIq/ IF?pW$QnuVQԊt8D Kd)y8slm0tUcQ1 sN%̙y.rZ;yAv-0Epz:HQ NCD1Y$c@G44[JZqc0O4VXtG'AEmg00,L 9T`۱_>twHЪ`4IԜMH,5tp(&<~#ND& `GϋzNmWW u+WԼq` n86[W|2V\o^ <-r4.d@;U=zjd 9Sh v+v[߾fu\#.o  jC ƒ/ 2AI=j N2F N8,Xwo$idDf.YhqezwR g.wivL8>vVo,v6w8P4R84ȓe=i my#He`I:njh`°309{)EK49uыϟ5\>s]׃b:.{gm'̅njS NXXb˘a'T뿤7u! ǘ.RR.5I:E`sqUb1~2VmY0V̇'|tж[=N҂r>Uۭjf\2WfxNJqW'Tކ0͓m+kpT|m#ؒ :X24 EKlJ釭?һR:P| !iJn͙ҳFXgΗ(.O$_Sy$%xf0Q68 a#ᘧ4OsSC1P{城|h&<J$IDg$ ?XQC+W8xkEpAI`nBؠ*!TŽQ9=x78d! eq}tr~Mv61)-Q ?UC%H%d݌3*tL'SP"mJ-1"($!i]Jww5s莭-,/)6iMvO&YDaQdH5  Bp'W`Vw׸/ tp46 hh9lW3i8?mnYG "Hΰ$=?j&%1ЃZ4Ix'GQ8Vߛ[=E Їtcu- jD<2 h ,!~T w,4סNSw!uM93 K#KثneT3 M(}?8n7)xآ !㟎H  2d up6I<(TՉDd/DǍ͐( 'w  V_>\^r;D͠+F!&GW>X7| 4?.^%ONn]hz?.4^3@Ne`p Q:0b>gXFz" -m܎ K\[~} K,'5 lʀ tIƭY*p" t-t_³΄5܏igm 4*#1ԧ-BNWe \j[턏rc Q:- G[K²UuyA|zK}ه}q!?1B\Dl*uv)+/"9GPUYENȹߢ;JkyֱmJ to`6 SlBXD:M?NsMCg\M`<ڸt|6~tKNPqR؊'=؃H!Z0sp%Yxyi8ඃ*)|[./O.`~*Bhk %c_Iľ|lKZ}r:cli…}XO[!;p2D9?S a Jg-:-)=4Xj{6Bh:4}ɳ-0L_&8(f7XDw 'zk3zr)*,ycX^'hV#4fm6l⏴wFiT! $@3@hu vEeA+! Ee t\"؋lCDԯ=%gW h2[CsccKcckSjk4ڛL&3d\%Xh<)k@(US:[j F.ȵ8!b芴 Ud~_ΛtAZ6#a+S@A\éX#%|l`T|K*rT5_1@$=ІokHi)N=p s5L-Fi*A;Jwn6U-p0e[a/5ȓs+ 4hlqC\aDKo`*`0R >b `iҵ  !{| zz*sԂ*v%LGS]ְ7]2fʗރ @ n`U'-caOuK)܋9c'a%?DBp~jP&[Gz-=I*Kw4K W&V́irv?i+ b* bIZaUDKM%iVa`r,5f+ĚĢT Xrﲷ* dQbePX\ʸ1SC_9\Ȝ#Kn >P3L.y{LLC\?*rܿvbOVG4DPS+*#{ЎAWǍґS] q7^)8wwHm1sKqU@GФߴNQ9")䍣R6*!uA}nZ3aǜNп Lv͈鈭A;3DI\]<~mb>𻋠˝=o _wggIl j31f遖\sJ)T9~c{՞yӊ O_h5F@>QBxHPƕ}|,Xi 'z-Mz``R qT E<}g7f%z1uH(T@! a{৙A+oڥ kYE];S )j vi|OzԊ3я}dExK{8c|a{ӥSd!h<`ֵ@jf/1^ "* `]B$ld"a97W7 NGT@qSKLqpv+tl `Epo èbR&(#8QW['W$&AxSϭXk5\E8 /o|odkyo,ټ-K˿K{2V!aBr΃&^x!.>*083<h>ܲ^.Xy吲b_/[ QV1CN݊1,|ɺŲ1{gV,9S枌!+\90X1fJ?BRыOߕO* 9HR-ӷ̉EHH,EӋ/!^[!y|_Lz :Й[19!.9zl;gu,=>?2ӱNIqH]"*?^?7:^M^,`ǐF$3AS&4 O_]{!FW(]݊>!C> ‘9![3BVOՠ#'|+h.ExE)4)ja97Bq0HaRXU[< zߎl_mfY Pl)͙42pr$v/‚n2q8wj'҂jCԲUyoC$ Z;kdKiTE }UF(8uuP>ʫဪ!DvSJ|* Dv}HRy+Iv:OCc^3TcADN|DPhR+Z7|!T"jeT@DQ۪ MdSDQ۪!V4!LDdZD0{Զ"jET@DQ۪ MdSGm* V4!LDdZD0{Զ"jET@DQ۪ MdSGmbO+-@]qRa.m A!Ɓ|WgOiɚQc+%7o%!%eKS-Z1 PO8߽5&p rg( k+]9T'T0xp@TK3olG&i+붩G?[ZypxX]+q(Ǡu)8 ƥJ(S{R^3!/}:N+<̿76c+AclV>}.V,VOxKGDŠ}U@h* T@pU*:T bsBEG4冴 >RvCIqPԇԬ82 Qe)K:o1TUmԅruYR6RLMb<  >joKgxi焅x1|3=-{ƕǃP"xy$8d3v,RKz璞Y!2`?2xK'g +Hp5 OKg Ҳl4)1u{v$kcVŤ3@BHEt}aBg.y{ @d"x $صc\+8k(Ba7].공 s3,!~@HQF\f pj|J-]" l?>G̏ 7@_.1E GUtaRy[h!-%=ީ )crjUǁ#_m)Plx}* b\&_D9:\qU`"΄BsZ.yUƔ ƛTM㼐, Kx'> }Uo^gIqJ X9ԝh\x{,o'hJiɽsɎfni_|L tA$]ϡ8oa^o@B uᄆ]DXrt#)tEN<\9}!Nޘ0ueAMgR !!4p 5~x5RmΛ] .}"X^"&OgJ>,. dNד=x9D".ם⤫s* Z̾wN<* ZwN<* ZwN<p* S~J߬ς* S~J߬ς* S~J߬ςzoJ *Cn8 *!bdk@^mWYȜgB]e$,w(VX@"M2"(mߧRgx  upجkBgHbs4h8>9IѺhUz ɤhVxǎi Yss=4\N e'Ǐ͝6d ~Ii!Nǥ{tãnvF hwih `F% f[E>"7 tDW/(à qxbYuNC$Z}+ZA"w2Ô8/P".M=aʔ πvGctn&j7MYHfwەAG(%[PIFv!E[?1S7/Tͦ?=\^[*QTj!):T.~Ϛ@m 0P֨T8Amgݣd[;lfSѨ>K:>tV/5-siii4N=m KHÞg<4QƉobg@Oے5'~\*!w;KyaK(р4y0mMŶtq--- ?=wly#?c)kڍwO?)- ncO"?mX'ϑ;ݫG=ے%Hp=!޹b"qFqtd L>ed120/o5P&4pֲnLQOةU?L:k i^O^5uc'9d0p֫0Ώށ[z\oЬ5}5]58smɺ9츇D/=[B: !ޘ˃]D݁~7& ͆pإ#v#tfBP8DYsRGB pHYs+IDATx^ `$T6mI#j姤Z5(PEm@ֿUB Lm@ %g cfgNv7n}ݙx;;^6MӘ)Ff% ّN:XrsՖ50蹸g푀=uI@3?LDߑ:`g9Sf"r2sj;lt__l]:wd؉GLRKL:†$ƂphKhB|;%T56nNkcN8RP-;zvII7m$vqS:uځ>: Br>@@@@9꒔A%U/.EWn9^Oxgƥ튏YYR蔦F{J͞l))qW[]m\ɓ }ыw"/8m'``֑K#i;95[gFRəRAjKCԸx=a4-δdvN ZKu'Cv.RXة4nK#'sH<.svG#A=i6G~f @oyEr~hAc5<; ;fSxz!%$4Hǣ#kӥ]7?* ~ $F7teFo8SKLj5p‘SȥQPrLÜL[w9֍a6(ֵ@":j9̫ ۷ƤgZ}é81$!nΆ~,'?;l; Dڑ rN8%`%WN4TZZ5o|iQ/51 n ~w[֮ ^b[B6  Sˀ-ƿLC=t2bMimc6JK e_bsRw*#3]c|oo$ƒ^ Ne̾2elPwҦ͝X lt ::?2?b[f !@LvhMXd~V.XgˏƺC#c*3M73zTֳD;ڵOmȶcҤ;^Z~:LRZ7f{hwvZȬQo]py>ݮT?J"Br>`z{hŽcۊѼ.A%\ ̯ ZDdDz7鑓K^8i;>R~).m]uRĕHu^YMgTVb43% [`Ziz3ŧ@@@ n8hyRRNKcI$h2!nξ5{Z!6Ǟtɶ8 cfk`Giv֡+R[sSHn~\hyͣnRZ>AȎ-̝ycn^Qd!6K/~v_d^'lSw<9VcK;Փ"xgg QџL [N[G {-7!7  C&ƞ|i}K=B;Szd}sӏuFnin梃RSHb ݷÔ~ւUXRAd*X?؋u#whLV7S9c/(/O:?& '!?z2"P҉d.d^0~#w^w%pM)qT{p]O{~֐f8HsƮp'|zv$7aN3ǹ]E>l @WhXGAJJqEC<4,ql}Qtio;WoI7&yX6ѝ@9wAK傎O-I _UL%xaKI:{$xz<~@}~طuS/ֻ_Bz!xVQDv!:jx_"r <2t;IlT*@Rsާ,~9ܣc5os>t8s\Ck)}ϻs?Iэ/(7cK%n?tvӋdorҩܣ8bnFȒ͹ *dePδkotɖ@8@qyP#HBn|]%JhZ_.( -DWoEjK[F.{GG6 P8< 3\n<p!FI7y3tM֐Dz8*#!{$1y2PzCJ"fFP~=NQN/#$^+_:~_ϾekgEeN1֑ۘXafoo[ʺ{L񴁥n]^q433Oo@$ฒE@h+B绉# J~[U6&)=Ľn^p)ЁD#'9lL>lBr\"٥CKXʟJ^˱,xr)٬~Zذ}=IY6%?u0QoAߒ|ma(<9#㛽K7Q6SIwYl\/#Wz9+vr>XwMԅTݢ_yR%O?觙;—l4knfc ;;V|dccgd=;n|2I7;aQz5< UӉ^6fbM$meӲܹ5t9ų g# 1ONe짿m΋[}9;wެKH<@EV?aNo4'!7e.l+$i`q^_J3d-mi2 Y7vRNCΩIU-L52dUon:U+l[kܔ(@BS@]p?wvjRl;v|kx%aӤt'hs3}0uo=|v1yNvןXͬ)R:uQk$@K\44*rU><_KyxE٪r6]Tn=ʺ^!˚A@@@ ,?`^IzI֑: ^szҍw6Oxc7|qƵv/.~WURo~7Iv;a1@7֍:Lun:Z Z֡%[]Ёvj 4ÔǺ5Qj;A@@@tkM nm-Q6Oҭ7qWc)z%wd7On}`$Hk2    ~ `im9y<:SN^EA   {ƛgB%h&fֳD2@+J]VJfD%p tt;$j`7 {*#tq-h-Jl3Ban]X  hOҸnO>ip1no rOyTqI`4jpMg>h{bJNJA+rHr& A︇շO <9lc/f)vo 9ћHfKqhvwiO.YO!BHi:)*qdHãFz03k-VH%7cRQH2Y֛IQW2BME%#΅WK/JHEAXxS"1III"ݦyMd7wMyKn񥰚͚tJ!RcBFI)b_.f1frߕF|%1:GR2EthC4u񯞷Q'e*[JTg@hJ؊d"Ou ,t7?swxo_#a)wP= x|4&bbz4\/+y/6 -?>X%>,q{477tVEtj"ogb2(Ⓖ Jy(/}x"COf)zy\UeGf#͒qeyZTӈ"3d<**sS"TۙL!6o͎O>slz퇎MUkVnعݻwuhpA '*?ܼK(~jg%/}otgs1?yf7.W|L\ FR^t0+SǢ1]Re+żGH9?Sϸ7.dtu$1t“'齢Vr豣J1c^‚,GnyS"m#?72_9g\1$W I?eQ辉&SRT>#cpЙEke'7ǯJ}xb`yO3#D)?yj30? bH\6)}[yG(-gNRCL)>t]J7/dW[:bMrx,S vNm%;1oH ԍz=<]}~?nSc׸`#bc3_\y ˥QX.{GUv*EG TC(%ʳ!,թ9(rJJ+ 8a)Zђ>E4B8[!e*Hr0,aZ"A8SgfpH79m57`UVZzޱXә;?h{~IJ'2_gd )7ХVftæECE=V8Y%;I{-Ǔ-vMs8]D2 /G^^M(3̴{ժ(Ǘh9i4v컏[K\xwynjј"*(ʋ?Ls,UBǽdPQʵR^T=m$.*1K[һasE'J##PϋjP eKXHQC[KaZZĒ׺JTI,n]1Ko_"#YcUu]UVbh~AHۄS:݋ axJ4SY\-ɮѡ&(諫ҌjjٕLg4{( BNIGϓƑ<+>swϽ^|W<:3Q.OGry~˵sQ7"Lɜ9۷3~w1yM7:|ܒ+a~k#4M{u>˩bD3pŖĺOLHQZnmPAI34MA>Ԋkk-DB_Zym~s?h/,Vt]z_Wrg?w{_X9 ?G!2c*#M,z焝^@g'\wMY4*4Yцl68bGW8<{9@  hC*IjT.٤1sU8 R(T(_Lhկ*R˲R@T.sa]G7i^ʡ7Y2o~JY쐕:"O.BCim)" u9 ){X|0'zJ4tҬA;5<|1|Zzv+9h bqw#(U|[\fr7[#>ܔsRh%\Zgs)A§)XF$STD㡤"H:\3']?f87ŝk+87.\OV}˿8޸ZQ?Rxcio>2g#m9\7!%R:jQC8|s*a^״0II޻iM]o)%=|ƭ?zb'dgH 2Mdʣv5mOOwWSorGLQ.(cR}LseM][M{w#y,|lHDb+%ݦi"-aqGAwbbl.k<wnጌt=Lk1zv|-DLZUWW" D.57*bI-i5_8FTſ+Dey%K˫G7[I(JY$ov5P֫2M1`ٗnSBX7 gq9 a=ؓO <3"o __;!q-v|)$CzGi-oGPCBK $Tz 'Uݐ,9 &(1wHɧxu|6W_]ʟ*E9Z=,0Sa:?<̖nQzʊmM:dz76f&[kZUVVRŷ৽n9IE^8sYe.E1̤L}j2I+]ɒ?Pӯb4ll~zIukh5 1.2T/6;x<%J構s8ȯAZBEyiz o-b0t۲e&M:"}W$Mw' ؙ9HGXfp&eԒ 4Fٷjt Af}SU`eN)hD*>Cc+htTwY b/ {X=HsuPOx9\un s ~//t[zu  L_Ѻn;ݰ]c•|`a].ۭzOt>husܱOn'5huoꏽiޜ"2ܒjި|A~sQ$"NO[P̱Rd<8>vzntÇ:ྯjQ/-/)=̆}SnQzݦF t68vۃzʊԳVq唯\NVOcKuoƽnBQڨUJ6ҼjIQroN&igx˸3OuPŠf)EwQ<΄L|* YQ̺v/^7R _p4Mz@@@@ b ݾ'g7{wWA>ndOpqpn=}wUm\i-K~O>LzزK*T6+&fDn4-s”,pbe&ŧuF+%0͡TйQ/:fF   m@"[meQߍ}9vx1sGv=t5b7&)A#C,JLS0*%Wq D0@@@Z鶞S8'?NҫG瓻w<{:|ucZU͊UnrB eYε ? W    кVbesmn'es5Ks;錍JslmIMP/n;"N(H=˙| gj][P:pOJG"&Zw'.vb)"vEpE6{J'[0@Mk Q&9hA߽·vX@ƪQ}3060Zڗ}Zqpw@@@Q(4&fE9 ղMtZm^Mr}ew,:՚A<֣CM_̞ )m0ۂ&˜aPeD}YQrYrDN   Њ|R&D6u|V)yΛa6zTZ,[nvyhJ8;q1KZ? I~v'e^p^?ڳCC헌 4k`٩&Oׅj-Ǎ;p.z##b{Fv.P8nd[Ѻ;LhhpB9 fϦkSuB{Q7z#_-XJ&y>Azy;> h:0Ag'9g<}y>7_n]M+J&e)pb~V^e/ͯ%swm{_9Ez9N'acXl D uŗG4~,Ϛr9)wg-gSe培\5brnU~52I9u  /2UR,Wl W y(c)ݚu#g[]:wJGjj=OmhuY_=Zҡoiߤc¯V<vWdAƶy;L 90ܳ]$!WWz<KK+~ 'iI ڨbegݹYEܱʒ_ e]rxϰAum @@ gPRiRQMi<\btkv:of][]N$qj>z:.ى=ķ;VGo~^7I+[Adq`@D_BXlM龴&ݖ4BBrRo4#nZ0ŰwuLQhXJzݎܷ]ٹzspǎ,xjW[8sbu߲:m v9T=XŹ2]9V/-G«$bG0q/~B:/^MzY+.I._\X*o#ճ*u1Gwj   M&vCԺ.8< L[q)=9۾޻z7Ro1tZro>YAv\6PN}WyڂucXM͜B{N=Y~4oAKh1h*_l9*f=7_ߜ83c[L/ /ӳ +J1p[b   b8HZ$ H4i=>AfWm!Y3ۂr9ce5Vʢ>uGsbf?[9^{j`7>ۄA o? NSGSl}#挄  I@n?'N!9Ih2\.|nIkw |zV?uK,n!tХ7I׺>_W}>pSt[|޾ @@-A]N&'-FN6c^ZD.k9R5ua9wQQZ&2DRz-p&@ZR wɝ fb)ݚu3hR/h7?< <@ ; 94 8`!`w;NO}u3ؤAr1IhbF@Ic"I@@@8--n!~i%t 򵁿78ߨ&ж|6iSl YѨV?S ~Gg(mjs&\V45h$"@}n jR[a*6Jffn4G'8ݴlOr<+ҍ+R 5zbV k“ڭzl?WKL(N}VLEf   ZZ-G^Cy?Y5P7ݖ3$c擯{7/56<7Ñ?M8?q{0+͵8HsO hD2Ғn!Q{Fn#z)?XXGe ;zQq0@@@ rК;aaZbnuC qHM_+7rSQ`Bf)YʼnY|i@N;bdUT|DEt Ji֠ٝlfl6ٙq%G-Srɠ&x?z BGc^} Y'n<]w^x~8_>aӇNvQZLCvug\⎢An>%hN"UgBBp7-f540.m84.mQ   wkq( j%nłMnilF @@@eiқJtk|m 9f'a%nr8$a݇>A͝fWőg4wBnS&{ԙ [5DhDntc7^   1'p茰yrv|@+֊`nr7Zэo_DnlZՋrHY$"6BZ[,iق4y]`.c,~ȱ~ 3\Xn\K\j`tlNs')͚rc|H N5bi+r& vc @@@Bt[F>3Ǧ~(Xf冝?ݽ{W6cKWs[<|q8p@ R5,M*s3.8/cC/μ|pC2~ՐtnjQ*6ݼKTeYT>MnW U(I @@ 4XJYl]3o)捃I{k,;ŕ@WXږ90:V-ZhM#OOfLoT5KkZcg T+Z҄bxp7f179LL}L%  /b)ݢt5:1ô4z%QGBvQYV~ 3)Xu@bL9vV8ly aQB.5فtZi~娐S.[[5Jie|^2 *qk Ab 6t2I5@@XJIi֠ٝl%4fgvƕ mؽ%Ϙ_y"S: ֬UwKrŽuQG/,lp.JsQinhHX ڹ-+nCj'L{kO-VW#tTZ'p$NZ  ОRŭ-%;}Ѭ_XdђYKSmCCu1ׄq9ag]5hjr\0\(:[nqux 3oau~Ot7 fAo5)VѷNƺiKEy[r^L(%$|C  1!K^B&H -o]TpOfmN_IJo9N16kVL6mf$Wcx7,9$M;EM  lNw>} je53nVsmv \bX=3;v9Թ|8 }ci9Ųe/'(`@l̆^Xz^X)g9>Q-kG tӠpDI@1@,;L[~ۦmkl^)OHTX A@@@QnM򺽻w/3y^?Q>u }`[ƪ;&MwlΒFdoœ -bE"K$ۇ[v>y:Z3zth>~±ucCedT|TRd4W"_6HOSxz@^7mVMD5gCWג $$ҽ:6 j_ҩKn:2Ow{:t9g=\>wyAIIR'GZ|*-b[8r"#8sSV1hzLI?>G'46F@@yb_ci YzV/ҭIHM-pE;Q9N;|pA[Yz֪=cIZ=-ՙQӛ](%Q\ўjKy40]3 D%FYDr;9ě͙7 J[(ҭF=@@8teiƺcչsΝґڵGS${գ%FV<vWdAƶCcRCe]#宔VB̭$DG zov:of][]yIfe~]5Xgv[V7{X~R~n:;TbpK4iN&6#DA ҭ^#n׾ov#9 }UV.ΜXݷtDMbx;%H96O;llVvQQ,IZZ?,a%^ˋ FhM܊BS)ŎA@@,[>(qY,^mj.ܬ)7~sg66vGATM޻Or->Z.H\eOGQŸ4Nhdm~ֆnaT@@,EX +Ԥ?72_9g\1$W I'o;O_y^j5>@J"@uRC /(v^7҈DάK9kLڰܜ?!qqآ3w2K{kO-z觚~1[R&Վ"cݤn8pCC-S^K6BFzod1[d*Nȼ=\n @@b-ݚ ~YbWt5ɮѡ&(i \Dgn4Zz1ٓJ*Hr)jf Bm vIZ9eJĠ]J"LqLuDcy蟟U+s GDQV."o m%=W< 2&ԳLKm"FXh ^,<.xǨ8}Eiu.2mDkl @@@bL -nn))f=AfW7CȲm{Fgu 578c7v`G  _߇c~)R95zF^3mqY!@Ju@@Fլg1nW(sJA _k}sΣ7E9H醶 nBn*SfݣcxjtsK6    @b)ZDZ(qZz7@@@ QR S|\JlEa;~oi0Wے_//٬K e4M@@@ REn ~Vr57Sܒ܂ 8&">0/D @@@,t[mUJ0>3Ǧ~(Xf冝?ݽ{WA܃V>??+/OF8{MxOr򑅅xZ@@@@ KfCM*s3.8/cC/μ|pC2~Րt|)=Ks[ɤv6We"?[_9:fi/ JrsK5mŤA*ieź/@;!KZ^7YnctIPvS՚MUv|qǪU+UXSr}MM=8~OW:h6:dK[qa/ȫ=K2I2qqg8gDђ<$Ƿ8:)/p˘zUɠc36*7Cȯ0t&|ƍ|k9^2Θ/Qa$ =!r-2{\oa6|Fz4x􇇮yJC~μjXF0#K|EFMiHu\wSTùx+ =O3b5t WV~Ԓ[7(;Oў)~?ksn;FE ##٥EX bw?ZbӨ>AH7%Ϗa<+l?oT@@ ڝ-[Xna!X'/:IqɩGzRk_>X%Z= !V^՞ɠ   `-|v`V#nY:zb%ݬ*qfʕ5ZMbR.4tk@ۮ;j  1!ҭ}`K$6k9ݻμLL#@ԁ   =An\vS[ys@nvc+yI9 r6   SK)Q0`{G'[FF<%-q|"bW. qIV@yRh4;otF~%!a~B3 Ao0VЖK{pO s8_nquoD@K̚rc|H (ZTVT@F}$ _+. _E,.T +S)a&3 42LZGw*BJR?/ފY%TNW3 }'K9QUR2.SJ>#AXoK6 {O cac;Ξ-$FA>3Ǧ~(Xf冝?ݽ{W7m:6f/t+nB,ߋׅ5uVX3&HOh]4)G)]c&Q# J;|TPr5:C6M K7GQAX nVxJ)pnu6Jak7UTUiwZ_r]Պ5+RLYd޸K~ImJ_;Di@|'4D6fkH$EսU!(.(e) At,da*YHyˑQ`sLh=saJ EK5k)^/WB|)t+K3^O~+fRR\e\ nV7i= <%#|tk-,W$WtKkt ݐm3Պͱ"wPf"|pU gPu v;z\O^%%rP/)H^7t$27!ьSFjYUJF sz^/ /ZM'˧vajtWck_lE2&יH Y;G6QvW/=oƻ%l!ZpۻAXϘX>%@C?s[{,J %ՙr8(yQBeW^??k.nSrM9"~Qu=7o}&X6M)"8IM풪֊aϯrFtuOEPݎcƼ0tLxŕqE~3y,|R O[35bǎ;62UEO+<|m}W&)5wkD5SSSE>ޢ 5'.i C1",[*zFn>FZ"l[$7~1=~0J9.-$ZfQԉ.Vfn:xgeǴ4l0{PFh>ٔYLhc”,OI,V-DIrr^ő#?R>#>FuZFyzOEcZKsDu:bKorD,@QJ<|_N9GW[9tAȞb YwILѮފg;`-g! W(@MT &kȼL%:)4QZaaMxªُt_ wgR1m sc-v,w%ЙGzjwUwg~7mu=(#TikbY=[Rq,&呩%ݽjYdTGwX+vZ= 5$Ү'{iUn4C/q" YG)E$~pXjڗ>{X!`2A5 x#%xjPuD*ܕ^}z>lN:Qi8C}rQcоm&ݖC cxVRz<dyR;G(?_'"#YcUu]u_ȍMӽToE MP@O 0n!^uđ>!>|p\b."!!+/۪=kɶ{k߹ddzb̻wϽQon~S|gjwەyOߛy~Ds9p }F$}27ϒKk4Y{|9RYRrd>7ӝ kgC{b4i"ϐѾbҹ ڻR5~uP )Ln|/![s.Bar9*FJd?Rdp[ BNPx?R !@IU?] &tПMJ5I?P(X#oR)[Dd2?)6440[ڦs>eړAU.BUV#;^yYAg͌Aq:=E5?W O l<[Hŭ-%;}Ѭ_XdђYK[JуuɌo.,ǚ/ؼp䲲Ut:g}ŭ}I*R:5'2v ˙sbW2s䛴9tɡr*Z0DXG)򱐦cV}A.H>^>&g\6hR~yRxmVc.((qIƞKf2$9sn篗+=./TAVGX# e+5T>F/Hy&u-R䉮LɈCM(}U:d{/R҇w5s:TU}тH"*V T ¸I?|?0{<0s:~?a'I:?7T-FP+N[ؒ=04Wγ|w{ ׊tk8k?ݞ=}2'[n ė"T%R6U/El *+wɊ? mVOa~_[xΈcOCf?KSG/U€’mN6xD,6kz\y`)GL 'B]KWO+Oe[.A)T| YzҔQ,+:zwAXCIXvʘre;3ac&h^MUC {a՛1A>Lm q+<<%opeC{@A1$7n^7}S߾ig=p.u2\_ ڡ<,{e}/5M =l;hMGgH=% N l2 潲#KA!3J+6(*l:LfBL 'R7ͣH rT%x:52X W/dta&l$U+J(%,UEȫJ )٦j&JI =㭁XWm*K<%o/!Xga9T˺IFJc jJˁaxJX~JPi^OVퟏxP, Vϡ?xq_/"7[_v?ͻsyv귞Ai wӛ ']"R.(UF* T\U]*IhJI~{JaՆz '33T9Ue.2RI t*K9ʔnu:`]{U:VDo ,[ݸAWm2QV}pooc}6w:s!;04s8##}JY^}f=Kf79֡ɴ1nDGS D-ݪD"t1gj-/Omɍ QE"!ldPb|[lD.HrU"@f^eFK*2Yr#} Dؔ]' =1xJTy;o\mG\u[U{`KѽLkp~0o??H=gn͠BdZm۶EuJanFwF/(T(zZ!hQɍnGWQHC('VIzMuwB RfU^Tա` RjޟS!d-SW:*Tja)q) ƚxJsg٘t S7_Iv֝8?jt~v'e^p^?ڳCC헌 $F{R6 WgT,4̓P^h!ͯG g׽y5?ϘBӧyd 5¾j;NVULQj#}>\-/KMj a=OԲ}&>*^W eIr[t}\,Pg$ 䋙)R嚲2 {O 6Pk%s8nts:DG".mm ԓz~Zv>ģRdH(C*'zIȼgc*7[55M\yUQIiɺ񔐷^4Dw<}~ibJ3Lϫ}nA+ S_\^[{دy/z:yWoi>jgk֎>{׿Gkڛy@>j%4-|喔8@8ߨys(( SV qpƱp "eS' B<̶\s @@bDۯ.L=v䠫A`@-))yyUƤ';ws~džkx4ٵ79;v4Z!$]FDx%WW!+X4LSV(QK&B6!o   Eʢ>uGsbf?[9^{j`7>ݯ{S}a3gy&nQ!kˣ  B -2ӃTD1nx&:fD&ƺaZd-<[,i"/vO, jaseo6EMqeA`ۣ((`]JY7?<'FȌ@*H(&̔  Ƃ@uS;̱:69VmYavwri3Z6D-+{#8 J`]mtK lPBYI?72_9g\1$W I'D5zUZI2TP?>O)(^cˋ FeSd꼥!±΅Ҋ߼긅-fnD*/U1BdX,@@@e ąt^ЯAu[jͦM;>ظcV^ &3WW]#,/X;C3+KVt +0|XsW@, ąt8֭N0wz7pӡiJykǴ40{Pȶ,urbeinLJ;*`B,o<.%5˹cեW~{3rJ=Z !,[H(   BYIi֠ٝl%4fgvƕ L(Jq]f)(*ZaMޤpmцaE4X;M$9)ݒ#W9>GIm岨BZC?O8"}oȹ߼\وcB Y4?볹靻}d98wY͌۱MxF[)ϷD=@@Z@c v_"yZJ$X/cּt<6]:~%n    fq)sʝL{,OM:?}{P@ZHq!,z@iaZ hQnλ-!n8     nn%w24GVlEa;TUZXn`*/٬K e4M4@@@&H3Lg>U&%hCtkp;܋̙LHxц b.<)T1YhQA ӶWw@@"&p茠i{q!fts>nv¢5xXE#.I/<>!tcS2\nT,A//},/+?LC[P6Q] (oִ("@@tS+h͸༌d8W ɸzhUC ,_X8dw2~-ͯE/xf*(韟-aprY.rK&e7߸9ر+QiV +@@@ MzBREIv5AALSr!0͞T Fʌt[yUPM@@@@ ąt8wj7!\:5ƜOc;V/CCi#[#q1M"g6g-E"hM7Vw#=YO2 E3BSJ<<4P W14dMSH|ظcV^P\GϚIZߟ˵lQsӭ~ii#vDI .~rE$F )pf{Z\V0 4ߐ[RVRcc-^7C KZwO_+r9=6^5f*'PgD<#AY&+&8 mUy)$[4g.;wYM4BZH E_nMZ԰ZY|) Hhꇟl(L3l-K'P$|m[VlRonFk9=y9zOD(2Sȩ(^1bH*YMJųtqMR@J[7>%լu&Cmٸӗ2lx#Fv?YYrժh*tEK'+-.:L-04SC6FqHo:."Ϙ_y"SmC |{[6Tԕ6ڕ(8wݲ}co,d&^.<4 t]db޸ng*]x~8_>aӇNvQZ|+Ff%fŋ!8=NtjP?=ZbK=<4\uAt:Lߛ7 ԩ)[8nu {ZMm߽{w TUU-Eg }AdjkIdXeu[x!/-; '%l.k"hn8)9/I8O B_VnwK.qIw\q1ڟv'''/j׮\!u܉e:_*&ӏPWVZiW[[w}.O?fCѨIOL{.ue'=mᗚrr." 05ڹgO\w֫Ә%Fd -mEyŅt8mK+5o9yߦ#QA#`MmO̻ә%V^ȧ 6%Ǝ*+lzwK;)鶩qDZR#'}Ou;~٧il:t=t8(Ikƒ={6lT.ȳyCcH7?K$ ҂4gKjllHQܺmʝL{,OMpbn2l-cYkӻ,wyoskr i#k`&s18́ Cx47ry=Qnh+6͝7ϸ2̙yO~:>}§7DEiIw6w{V?^Lylg ,Ɏ[P8v٘sۯGҍs WNu99A;Kbt#ޚЉ_^lcƚvhw]Ƥƨ(%9<,]msBeu}\h=;jcDӟxm_HO2 ι⦙B^W7-Q?/-#*.ɿw$HYg5 9ekn*)~{h].Vl"6}{}ȵ}tφJ;zaɟ?`1Tk`L0!mhô}~}|ڟ}s*F9VN2R@Knm3t_^=opjLątu?72_9g\1$W IZ90KCҴbw\eaYe)$-p"^u0=:D L1Bڰ\[h<G#Y릲ɜ0]k9V/--mL-C4HMKЯ & x=Nu1:\4GS\b0W(ۮ_a rV?ýLy\xUC^ Ls{'3Dן֧&|/S{bhVizWw|3򷃖XVuw<8o㫯ּ/tߋ/`*/ѳ7?~y]_޽?i=wuvcݺu!2WowO)x_/e dժ+*>z-[ތI_& ]zBR9x57`UVZz.3ѯ7Sg*cS*v_k$Q7aR~♟r\D -!2m&@C D">)1of$ x?ȅFrV irC Aow1MA$ߩZ$ HY r.}hmcyGd׍kF;f-=c1g7.>4'i.o gJ_ʇ`"_~Uo3__0}3U~]8/;g8S z';{f͜t2cd#oWZen1,E?|m/>ě2һ ^kZr͕<9Ao%&_9"3,#O/}⡼~'zkrd|y?7[c5qhr+#/ͫ5kFM-h `֟8[$wqHk26DIkc#,\BM-K:$n 4>8}K>i!60;)Sbz) ~4R7I>  DntP{7zieM 4@\tZV'fֻX==OLHB!WOc*aj1z/ {l+Lk="4ZLxG|Opu{ܤ՜.ML3zQ:;-D;r$5 ЪBYa*I ݩWvA/VG8/o^"xoJK/HMYUIHf:y7>6nHv7γӣ]MD@- fv_x~8_>aӇNvQZoӖuIgt]}qW㠏ʃƷu<3mq1MnVݏi2C &]qrG>` Qxa$DO~`<{T04k6Jsdҭ9*<[@JVdA 7՛9 @683Qg!J   -I ƺ~i%-91utu%),hq!,0% Sfݣcxjtsk\(@!ʼnZvl76~"u#I7Hr7Zэo_yv؄qXƺp8Lf%@ޭai g>bzW%F+$݌:y`X sKFVJ vJﳚ$d  I !E3o}^nnF 58GxwLFsbtH   ^$ Saf'96}\CG&Ǫ5+7\ݻi? $6r'' Ǒ DФQ矛qy/zq3qyݪ,o/X81?LqUU`bz9J4rQP<[=4$v!"#M0 Jk7UTUiwZ_r]Պ5+ׄ}޿S-Updb96Ѳ;*rK&eD#rsa٫4;n"8.zMktlx&:S8uy…V]"tV~r#fP@iq=Sï0x4=gHO.7ࡼ6{P6q"[ *_=HkS*2([DM1ROO#OSK㑢?W?(HQ(4sA`*C_K.r BkE |q&d%(-bzH Вڲt8O-Z;[7 J 2Vyy|y_Hų!%TkH!!@Khtn&(}tm.JݬuX@H$閒hӯ u,yh,y%IG@$tx 3oau~Ot7 fAob@H$`(Hō"$$7MϼQ5g2a>ng 5?=2bH   ȣENaɄYU4c5>}/̽i|)$Z-@@X͝NO>#~=jf~uلE3:èEJ^^-hN}Vq[@G|KPpsɑ@Z [V}}̱-,.Dx!/-;@@@@ ,H@@@[ "Bn@@@ a!B D8(nr1H! `q  A@%D&HC:ّ>бbG#o7D6bUB1iῈ89@8wkز[XDVx 6xZbxh7"Ax!uۆ5ooMx`-gGA@@80m %nm5.|G&Q?nJ,y4Su=as.Hi&YX  `HҊJ))z|x'#]v;.cX2Q3zU6ɸ R2YV _q܏_0fNӡT`BE^Rdž])k$5|`ĭ&vbE[<ϕ(:J^g"F5$h `+@[\6 jc-NZvYY.r#B]>踌)ePȋefF]LW3A}‡ e)g)y_5ua%:dof|#[ QRS?=1/ rlY- b@btKj^;q-qWWqvakoIU/+!6c<(yk%pX7sfό PҬi+,$Q#T+U_%0m<AwF~(~d$o$,h 6ڬV [#D#` % 5;Up&#ryb32w[Z3&wסIrבw(5}jg ytNg< kRڂӬH  Ќ ݚ.&r1}]K׍N_ {KyҺ\`]Ը~H AO\e\햞Y֬]^$ޞʣ4/M_`¯ :2֢>ikS̑DA+mhV+!HhX%`*noC6N'yƒf7J/Cj) nAOlCjunǫQhO]V;U:ɟ1So:4O9sYvhYpגƖ CL rǛc>U0vmoVK @fhcF`jPڈ}I;eA .Ƌ[Q<eϪ׳uAD;Lw9+A@t} <^}~05Xl\A@-tk˭@xv`ҺtG6E<-n+a͛?\,R:q1-R#NnCB&n- Ņ'< ##@=?[RǓAkقWcorlEڃ$z ])ϊ:Riꀄ6>⻢}:b\H ЊJ}f V(@@@H3L A@@@@{    M$D`   Gҭأ@#m }.9i/+ FQFrl#:d|i):jD F*zK Rp0g@:t}m"fj  V@l;-^nnˋ AZQń֣9*K Bu+&dJSQR-ΕeWj>KXD]cY<C]ǣ2V0E:%9MF͍›H%\q@ H6Ӕm"Yyy;F}HO+"QL#,L5n0&e&"6H| s ʬH(o"u"0,A G8"-]k7F0i(N)R@# EW{sdGoXS'gD5w+|by"FiN!܊ISeR*0TT{aC ˰25rB9dE   eڳ]&&IH,`ųU/di1s]}`^KW44\@!/xQAL׋AQfv6>4^(s^ZjT)5 ٤6#O/RFݥy]AO2fp@[bW;֫#]F  EcyBjڔJ}XEcXil]/焚Ƣ fֶ*g 5WeHyu*=fQ;X(GWiAOܬ@tKk-@Q}g9&GNɰ}Z3YxǺP81laIݒd武p4M\F(d,Ii~3Mz2Z-69@|t5җ.]]N|gn3:ļ |\55 uKzǷY-'ze3B#|5#i@+fg!lRUSu}8=i);DFҭh[9Jsߌ>Kj Y̜0@hϷڭ)*.fx͟WTSEֱp"ͳ6š,TMwJezJn m[[l6Q'1-`.Cm2 ^.&uH6XvX Mf"C=)N-VdQhD䯦Bp , s 3pFX`yB,KMdy\^fD|HIUF4hkl控r} Zjf~u[:lf,zFIOfSѫf Z)j6sb5odYą񼧻rJsҸuk47H8n ks}X>V^u @""`MQqH [hgZ˗R-oJEҭm'jDϪ}t J-^[v@H7    004a* `iM Ԅ /0ty%t we2g.B G @@@bL-@4HcA@@@  b ف@tk>ȹhK}gtos"tJ _M:4ϤIĈ[@"((ӌPrl#:bwϨoI.i2̦$dNX$@ p?3 fix1l+_^[2)[79s‚Hm[A=Hv 9Qbm{fϙsXyq$r2*aF(Fz3#3pa6LVo'48rH  E4ʆtk@ pQI?lLKPjœbWVgݰ-H( 'JٔFb]{3 I*7Δ + lXῈg C)-o\:A  в^̱[X\ :aQD"#[@y0Tq<%!WhG Qc E䚼i @@ q@%N[RvOҭ   804q*q,`iM /0txdc"tAм   L*aL lMH1"aڀt*A@bH9:LJA@@@@z    M%Tb   FҭУ`h*HC"k 7-Ɏ#m*\)Hf)T"71@@:Hm~Nd>njQ*6Zay՚eY%-Q)x2-4@"Z Q@@8H6mzTdVNάfO[z덼X9k^EE=iK&^8#iřLh9%HllZᴲi6EU*'\tT~FHq stZi~(rk$XUJB$ g _^uI9 1+H_u҉2šLKnq,0**n)Rn rhUgn'Z]$dӨ^YnɤFU(a*A$Psh,LhG KK+9!1@btKvQصs [4Vz.$\N.-=%X^ M3Lͭ@ ӖcJ H7FG|~ƴf    -G@ӘFa[1GI    %H("9Hc@@@@ JnQD%k 7-reڌ0r 6),Wd`{! =/s,-[( @@ N @i,IRZJA&Ê ܖ3b6jr9?S./T)}W9a*S X ٶ    ZP~h96O;ll1+([^rTf $-WQR \u|yq(C戼܊BS)B8  [lٓʪv TgE5{DkMvTPP>Pbdca{RR^$lr8!F(E\b#!/n`m)V"ZWZvWvYf%`e[ 9ҭ~V9#h(weU-/ 6_/#Hf1%e_ n1ʭ ޭDO-zȡu \IA0>D# | ںQ:@+>;޹ǐGz3s̸V&޿gta-qjZ=)_F@/cS5j6+nM    jt    qM@#u&q    `&@@@CN4i7 LS{0@@ '5t}zVҭ?뺽vU;@@@7ay$'ؕ    4*@@@@@ a`7i*    f|M8&@jM(60Fi    K w$ 8HS9Ⱦ0CA@@Dq;((WC}OQfnnKJ1ɞd.՗OSjֳnw!gh!uǏy*Z}-Tj3yTNrJsin|!{hYkb&:6JǞ?󺵷ƺw @ ]]zOSiggzuC\-9)%Y{Aҭy@@@Zi5'=Ө^k Z@@@yp~M@p754Qd̡ҭy7@@Z@Ur\!J @[QEJxv,=z#ptw9@\Eb\|]jj7yY\Ëعd̘%;eYk}z]BPa:^#n(ixuHZZAeZ~,]Yc j_~bX싖9daT\V*uT[6œ),Q^t @@@ 1xU7b_u:{[^`FJ%cu[޾Ub"i}w~76/K f.]zcb!n3=ȭ(ºUq☓6r4H@@@䂡5U_\96۬j޽wmanַUi.=z ?yΤqޭ3<̫5?S9zpe>g2c߹"$Bda4H@@@$}}o QtV d뺝ԍe[x|a"y#3c6g2ɏ]j[Ml ;A@@ (=Aee{h[A:6֙?!U3P-b+EԼzݏG8R 5XM+IQ;B! 6BC;UeM֨+J_nɮXHHY}EӤXiibqgٻT'##뛖=VZPwlhDދna!$ H;K˖ۭg83nۻ?qq*ľÔWjUNͮv?,ysFx˔pƭ?hG }Ǹ?RѢl07CN=roNڣtF%|X]N߯^:.'ַ>vsg77*  i6WΤ&&UhCY_YkH7|A@@p9U7.+qFCC=T!܂KV_yô|]Q LD {xzW!C!qoskv#괷;L!Ɔ   ם{_0D'kjW]bۀ&89KIݠgn^_<=qe[}!ݚ~C    cX$@ ,2Q Ā[ " hn-@ uRwM>3mc5t&$GZ顝8_I޶n) kT{ֺ)QNsE>*&x[Tw]c{YSuQ.w[z.]`,x5`i4!£MQmGmߩm(KX1VcdVtς( S6DKߪG#,=>gYOh>[k?0EGG,.ut)u_(ˏ0>J<گL]JbS,KbhŻ6%M@@@@9 & IENDB`nFS?ƊGPNG  IHDRPDsRGB pHYs+IDATx^] x3 !gxCZH !(i Z~h1TS $Vy$lsd6lv7\.;qι9'2ng'@JdSnZ:A &EAeI _"XKA=po@8B; ӈvD3K&+rk ܮ}"ITHQZ` iȈ|*GAtcwhMqZjxJTi#Vy ȟd8@Ô6ĔDD5P Z, Rf궫 ;wI#PTߡZK*.GPU uJ$Y}xM3&, 2/ua,qHÍ&$`pШz%ՉKEe\1`"835%S Dj3`v^GnVg;IIT  <`MYrsn=-\MHHDd΍D b"v,MMD7 \CIY4q"\ILQ 'bSX(xzF $Lޞ t(9c,کg&F;Wk}<( -rB^{B.'XF4éB(͉}T SlU^*oH6a.Dߍc *lI,N^Wӡkgܑڶ "Zⶑ5t3z/hRT0, TV%*M7F7gJ_/p'n=9$TՕANC(`v d{V]YUuٜrѿv |j<鈲.g0dP I͕HC\ 2ڵu[5 BP4h֛%X`ʙ̜ Ƅf2E/>idrgW V#IOS^P@o1L' c,j# غ]B . ZJjxeGl:VH0`k \[@mpAnj)U\Ѐ@$l[ 걣9 雈0aHBOA=!٬ufm6V#ڬR)U}kG/ps<z+U@iT&3Q6w7wxAl}:rft괩^?$]kmʕ|⩞'.3@'`"BA$(%;YW#;w?m\Np~:1&t/%@˚@t|8,J{eNšnH,;# )狋<^Y1*Cu#i54t8W9]%Vb I+Bߒʅ4a]8w9+0+.M=m0%q>`w]ž[u=?}ѦD0h:+zc o!"d0!LJ5p>һIJ"-$&y4)Ў+:t^yZBe~_~jq.y:.ջYUK 2r9{BѶYվ3 ˲Re ZVV7 AjϬKH5HcO_]sj MkT^lo`%|1% CM0 ^ B9^9sf?.s#NX5x VHۣmOP#<0k}eMQ_W s KXp-®ceE[?*?61}eĵ_!XJG3CO2^e~=CI'wqO~nFX!ۥc+Yl@y 2L椴;+A`u>(s=a:z:`hhPC̔*l$fqN%3U `Q@z}yƭ/2xԃW@0J&~R{ld}A\%߽tYP hz`zC|*`lZI*{}ߞb") y.[6[ ARygXVwvr|Z:])j댰|^հ0Z]&<>sK)QkN#v)H`d V{$peXvѽ8/ nvLA'ڕsNo(Oy r2o.vF` y+ <1 E\ʬsj4+*JZ!8?Y0pL 7 ,">~T#%hλa{e̫h-sNz>naA5:00W-=^x2<ܼ:n짤=l7 1@=P?v hzCA  CBvOJc1n jA@sWU@V!ݼi9\*XKe\"Ԛr4gG >(^LOY.S؜@J<&,Ej?jReˠ. UU1FbXR4؎kw0޿/,ql)_wYb^ dA C zP;TPSh?T.nMg\}/$oOW:axd9Lx춓>>8=ƹJȁ;jdYW.aݾtƗ0q`{Q>PeOWǻ=ei@fy9ݱ|n@ 7 5 2o3o@B+`$5^#C 2n_|Հgf`鴃=0w]٢#-=t ?|a%`ԙ%;H@`tJc˞bw<9+MK_ړ'(|;@.x¤'HZ3{۪ ep"0;EpM6OnGY*j>3ғ3Ǚ 1_uR¾#5FVX"f`s=:]߲UHMڵ'teytrn\ 8nZ+J+rS8Auf@=ȸ0z?0RAp2Ѭ8u J.5 59sla@@Ta!!~о߮=yb&(T8qLr:t _C\nR"/H< XW+$z y6G@xE4vwl4DţBNsZP/+:ݴVWYVJ*`VYj6WUN=RnFB qMҡ:C!UY*arۼ 4Dˋ{"_aCSEܻV;w|ጪ/Y̠ak "$?7]W NEz߱w.+ڇ|("tLFB/qn$:Ty`S ;EZp)r-*fB@F W[b qE&ʌ [j/~ %X lqB\zBYouE|O/S0ԹAx8+fPwp@|MaA`>D= vGBA\%3]Eۡ EAtN+48 pl(Wcؒ%k7LTW1@ mCdl0S)&5M~}4.z v GtD0rhO#nb .3#Xݼ`85i} tʓ'h} ZS ZSQض($)?КmÍ)0p! @hZ\;mmJF~3ke!ף3ސ#+7 Çs4d?0-B 0a\4F3!]v NJe9oH3ޫ dk1@AY8hq'zTlӥ` VPs^>SE%>BK!j$ \~6]%*Vܲڐ`\>Y~,9K s5$$@@2z`5h}^>ӳse?[@eP^BskYQrH̘M@f#DCmZ ÇXP0 dĊi,ߞSCXʒ<~b YV_v- j'eDE+tB9D.%ה(Ic!`Wc-#8v|ؖI\sg"ߛu* B,:}H@dk '0a! Z/n> 'OQi}"FeG8O͜ܢ' cavhZ0!`Itx i][R݄qJC#aW$.¦$x!=S7<,Ci9'ϕ_"qBh[rrNK-<9`/ @jq.;*u8ly V B!^Q紲W'Jϝs#!)b3fn jToNP'&>ttQ7XC,`R `One)"87gx^![>QHtA.h/2ɭ58`!@oa?ulu/7åT$127sr(] h+cy0@4Û'L F/7N[-%Ѩ=$F[!2lU5~:Q 6 " dVǶ-alU`"hc(u*wn fmuae~:# ÇR RoEVr[j"p@DJ'I?Ӗ5hZh[?)phx˟X n$7 |CV)x#uP@hlZ&B;-՟ʢZ-MQߑ2On-k4m*Zvʦ{}Z9/]AHȽs؉^?hⳠP }WΉsSYb-2d\J;O ߹}r{=k 5*Htɭ,\)zOn+9x?,GSYxﱬF^[g|'?yCld tn|P0Bxr,]'Olx3\Dqr+i&;BުQaԘ[mL!,VcaKtȮQ"035|zr+:zl;ǚr$`-_6{}G _̖2]c [̖2]0ct-~b(x0Q?&3FzW05ebB]0|ϖ3[-Ȩ!*$`h!DETP(.Cp%b$.̉ /[7~L7Rx!쵵<ϯw={*|ɚ%;Q٢o: {Ν\0uȌEIUؙ>(=a 4D"tcyOyQ3oFn;t$5S$ }co˔e1(w|^7~  kDC ;v]6Ư~&ĀVX@dnh2U yfM4ҤҠus" dX^2l|ɛY[]̷QJ>"1R[@USݍg;n!u(+= [ >Nі6O9Uώ uj}> r^򇇂awg?0@Sؓ#,CgK֒ 6ٙW5[_/odCޣrϳx]V0599Ʃ!pi *O+yh ўь-Shɲ&f;}gKю.Z8{D`mYю}Cy8c{71`aly?b)yOoS&ɻiJ͕~z߲"rw, lxآЌ{og ?ULY?׋T<NM07o楤@:R?( /j % 9$j ~=_Bp߾3eBS~.ElpF};@j\Ʉ}p' a`>l93#c𵼨pGʕvHme]JFvwON)9c.yU! {StFazzL;5Ia$1<>l1cs١d{K/DGdԩoU(H؆7g;U<Ŝf\նpe.z]vp[ /рtZr)lOj `qdE 5xv)..wۍP84xEɨܹh;OO^% a[0F B\m[K[s꟎wg6,u=ggtZ3: }x쩼/ D-vciOO Z5^7٪" 9Lɂ454HSa<ǺNf0 B%Fm& 2 PJ.Hc.1ЉdD(uqlj~Megƕá> |IvTcΑnI"[߫V&r DW!_TγHA hS5$!/'}bf'\Sg2V v;' ,6bnk mK}냟cGrQco=­_Hq@)r$CܜS X9܎^^hu\[3۬f:vp;l֦ ]ƀh MQaD4y[9[bԾ{iJhLHoSW5/*,q@XZhchpV@ $rؖFƐ etzFkB=h^i`}i $Fox_/FV$8ZMWmmFsn5[LƖ/Mm&b6 I']?m % h51 ADXB v7V3[ͼ[̼8jhibaۚ!oj@mrAK}4Z)|Gݻ2$~qACx|oa4 &m}>J `aDB@|4n.Jqzm;n9./vom|kH8Y<@-sQd26@9t;ۍӐ.%$=eBv{kZڕ ٭x`e[^wuƷٹ Н!s' :a 2AJ=d_6:@0bYT)}x͟J<܎խ̉f<=\܃wgB;3or0?J{}а' C '0eʒ=i On)-H]6V$Ku8YMi=z _}@t jl#r-wd4< >yp&GtݰSoBz&{y2 0GMNYk8Xu=ARLZ;'#hy/o+x}Um Aލ v4?h~:{E;V{>R]S Q=8nW|Q>[HG7E%=PW$ϾL[{Wp׮xB)Rnϟ̙ܳ&OI/?UpTNQ{`F> .*[!$FLhf+،1/ǝ#e0x i( ĉLAGlL黻j83=D3Jqm^|jÖD&Mj6㵖arzP`6RѬXcv5C@!wC-5k+{uo[3ajdbhC'V"t_Wy)tylUbTZm` aw#Dt! [<ԁ#ɣ{\cbICk%30dUQ '#z|Xu ӈCTO'=dIա]B?^bl9&Ł. EtK\>tccjP0)Eo5*r !UmG)ۖOɣ5\봃.`穆w{k+B9zL̺ w2V s<'}W_u[/@dp46 hH9lWSi qs`!G B#Pΐ(5j&%0Z4p-MۭPvۋ}4Cp{^ Ej~wThFC|+' \M o⑱ pt%#$KثؾnT_=^y3jra`P,lQ!?y#q0nN8 TXG<3 ~zu<"ڏ*'-( O: @K" Ăp_>կ] "͠+?ՠ􉅰;>%%.1'}EeuZx5t ;$slΛc+_4LCΛc6cʯ_+UJu"7¤Q b >Xys=ÿMXww 3y}mJ .tl ؄6${~5 : GPWS][X:xz5+rߣGJ&:T樂ǃ~ʤft-g&Ww:tXR+2v!]*K>p 3ߖ?2f.FƿC^OE|$E:jܱ:-_{R/l47644646CKSSkSS[sfk2ڛMf3lL߁.TI`(U>S'tH3nqQCP=Pr0C-fuO9a329,HylM޽Ͼq%>}{T8Ԅ^ʧ b(:G @L˪ EЏ8=vb/]`eP[6R 1fIEřZH̞u.; Ϸ .E"@@ |s#dLH@_RnaLy!/ˎP ^3=Q%%Nʴ6E EDz|+1J #]83Ғг"uKHM2p+qc{E/  i1 rv%^sgSk7x5dD/% D+ _a$V;2(oDC!n@={R .] X=w?dgX;nGQz2 k;Z'|֕cx^;pK*-F2ѭG nbޟօyyÂdta |" U%r q<|Ǜar>aZ-į^7>?l}ؽفVrv_;q+F*ɰV枩􃖵غΝ\dx:K"Jdn _v؜ SnX) `9EGG(]n15JN?Ӷ[݇mRuɿ;H,[2o?iM ؈_ 萡fV ds?TgeLLv"ci _UbQ%H!kJt,iDL08_vؒHۍHW#PdG[* MbAWDmի6^o7Ku:tt |]WNǼmg]}N.)D@hR+X`oDCsQxSGl* "V4!,aު!b] B!b= XPȰXϮ"%* 2,ֳu +  * b] B!b= XPȰXϮ"%* 2,ֳu +  ~b:O+?]f85seI5_qsB[Үg pa YڡlZS:|^PAo]\bKFnűMwW3Ιp|~˪;r ~o[к aXCۭ.)Km!;{BAks }fmgq?H))rhM^(osΆO~kw|b Y%N`ˉ0b:$y?W^1'1`V4ꕇ:#bX`MK4ZߴJnJJ⣟ZVp=0 +Gg<|*lU {VLU<߿a :92N /=TNz;׬P3bPq* T@~U*:T bcBEG41}P!ݵxW¥jaR5g rMC}YvV@X4JKU*,,@ $Н#]Rw>c-"F|F%S/VQg'φ;z$#As+Ȩ! B6n%U(TZ1R,((CJg `R5T_y:@,?˫-@}S仝 }9]s恊ܒLic/ˑ_SCD>*@P&V`9"T@Dg&txũ"zE ; JJ;/RǧН1Ӑ֗~MK*p+#7l\,bOr~ y1ͥesPޤ 8S靑OX+ߢβ%Xj.C+=t;Q9Rl1%n׊˝y-s?%b &j(S7/p=x[z"6 "9H۞ j2Mђծ;9T{ؑ?=ɘG5ާ9?-e#`:"26T@ٰ ȔKبR6Gf* "S.aJDXa I>Par 79* -k_D $䨀"}&p"5nJ *C8 *nstv2;a@pL^{Ĺ58gJ6 J.@B]{}ɽ?j_J ڗ}oAkW=-B* M#Uk>tT3<8q/UsXuƩ))i$N=nMIH¾<$ȋH2z乻`P )X 4rj$.7  z[SmA@=cKkk#>y')OBRZݺ﹑E~(62ÿ9"˛O#w֌xwg'Ma1i׾S3=Q8|ѤW@8x9UlGb =SVC\3o5aBgPtv+@JtդKݷe bh) 杢kwJxD|MZy q);~+.&j4:k5zwkj/51(WVPX.tS@YҩQ0d]RqබlqqB~[{F)'H2g|x쾲Qii8&eZv͓ CG`f#C& J̧|ȉ6)=4dxu)+{=MY՛׷!߀lx1t1Up(εo6xa{R ڛ']x݁y7& P6p؅#z#tfB8xD"q^|ll|n}q<lB @#,ԅ?szVKx4dx ż]nA7"{$Oy<}Պw _ŀ`YV{uq"l*{S[/oU 0N".Q3`C50pMԀe fS$3QǾo; MUFtuRi\DD,Kׇi\A}$INM' ~RZo cj߀+Gp)2DI:|TQiDO\ɪv3?$S':#;6:2dS~RC;+?t- x{rr(zIENDB`n+ˆGDƣ#PNG  IHDR[sRGB pHYs++CIDATx^} xU^&Bd7|* a@gGȨ"0|.N`P;2`TpDt!"Yb^U?ުN]KݯշzsR8Q*pӾ8#Dd6räO6x"/@ b DKqls\]&QA=~AE(3d8IYivhbqXq@~/L?`(-ŐR)$ I! FZ` (Ȉ |3Z }6.d򷣞ڜO(,w>BZoƱDϔ2攌nI&d*)j ?Y8!ˑ)ތ0 J"DJ*glm`%}(*܏&}]Dy&`"d6QD`2p!bX$ApBApxtՐAb2BSN$FbAEpf%n2Dv0Q=bH\)"H2pw  GptDŹTp XV!PM 9T`۱74]*X20 [NgsIk$)Rd3PL`~ 鑁>b5ND& . ͜I+~qEGl ypM>s,@94T.=ILN6Ps#E^Dkp0R@1"8:8G t%qNYq}C,o+:n؆kY?3WBќy}.# !fzf#]Jf#` @G !{ȭs5h ``'j30 X2wT -5{$fx!{M[SF:/WF8o] AA /V ʐYq2J]R +i-XztuybqϾ$[V wL'X>{32J X~MŔqJbxܑSL<cyvJ8b5i;-aG}24nQ"Ĕ$]'0hΣuS+m BptE1 !}(B`58! a,i0\S.!U]Gknb``"$JoAzO~鑬ʡ dMXKưPc_dJf٤̞ݾܶoNzkQ%rNꆹOpWoe=' ], dD+.A\(vwvg׸. T_EB~EdZsՌ[`EІP|4":C2A W ݨJ^L/5 xrJcYJƈqx8>_}~V4S UÀ.@OepAumJ jLjƾIN;j3̗0GtbIV-^Z}~$xxaލԴ};j7<,QOG$$ 12x upa'L%T5D>RŭZ Rf<nrFV\9\^ R'D,ɴPM&̀鏩~siaB*t3\3P=Ys/ܲn)Zb2Tx b$D>յ)aT$|Tݣr2kb{q_aHmɨD@%~k^|?6XRkXd8i`h`*F&'2H .=\#xߧpi ^O 5A32[3oP3!WX}ox rJ6DUeڗzr?4@KYQgbWHxl Z N{6c7[)6ȏT*3q^dZlGev6'|(A?`VTT'ZMGhu5F !!pldBBfyNۡXܲޮx`ej\'Ƅ=;/jxF3b=ߟM2THXxuKvoyylpK zhCȧ:57qEC{Hg1ǒvǣdW&kԄf " Gb\rO *;,o_5 zdۘ$+RԙK^(N5*" w@s0qiЦTr@>&M:7a+0 x܃ 8 \0]QR "$ DY.UNGSY. zaɘE+a S&AP OJ% ~i n|zC9:6ڶޓ4,23onTf]'"^MȞe ZX!ץKK܊@[u@DȱTXVڪ"D*"z%HXm7.!~{/+rcgJiE=JG9*"{T#1 Ͻ-Pr$xoF@$o@ N02tBǂt@H@Tm2ec@(Bl<y>#~Dڭw߳6l Oٓr;!< sGX%(Pd UU<㦔 KCn,c%SRH˔M8wzy /ZogIb J+nnhxN)\+wEU|\>C EEEq炑픷T8T 9n!u>e~q~9(ϧq'E$!KȜ˹xmR~|9wݬՊJ4T\< F.`١$\!C@a 5!: 2gn"`|aѭ0'PAaPsl$j 2g{J]v@DɊO懄©  ` !bxpJRqUXx-S x`*Znw:4rNTBlXtiQIVz\Ǘ311 fvr._^5; @S G=\^[uO'u+*XsZ.W4FQla^0Vndəh1Tbv hSEŏ.o.gAt5@Dt[FeS-5*1G,#mξDN'Kʈ~fE: "JLD(-QrGe: Gm! m[w u U-Ҷd*9{:%="\ /@taKmyT"c ; aml";Ic@b'zC=7]BFƸ˭"XZb|)ayȄvlug]N ?i0 _Z 0j&)zCļ3ۏߵ(__pB ""|Ǜ2]|zj>m_^O/;ȋݾ]&{nŨg1("oa{R9eBR48 Q ~ϢࠞͅHF~V=, N1,׭[ 1PL t"%S(+>XЭ:bYUH=wv3*/?枌! *^>?Au0:t8\PFC^5^ƿ?$i)Զ܊A ^mYx<d,Fӱ %Cr}cw~YjJʙ.jW"Ku*Tb@D=.츔?r\:DWj-  *c> A ξpNU}nIp?;w!!23vi'ڏN4مe)>:##d;Ur5!-1a> X@q)*7ܣ_sxCyYv@IJQ}bT2 TB ѩpIe'2{;8ieGmq [:(blsK7?q B3}Qx1{Bg+=pgFˀ #&[?EDT/Ο=!CYHrC$@%Ӏ`@P`R~8 IH3]Nun^t{ӿPyWMXDqW/";!u@h:bM'M_39BM7 ր[!S6殡^ɤS 6uc/k&{> Ĺ$iP,$@MJ q?C]Ӷ*>kG=C);zNJ-AzH|UQX] WpM}8^q{pZq/=e;½)~S!Z" 22Gb0CzY8jS{Ee%zJO3lZ uAȄ=wX3߶̡umMQBm.ѯ1>Jy;!  OU% ֲYB !wJ.@ Y>`ۗ3htR*aCT79UW? 9BnX}. fkvh}ʃ̔&!d/r7, .g}nc0nVjÇN1J܊qv]~&礷ZO6ѫ[7'S7F zӒG|pl{PK֘Tc']PƫJ[p@p'ScŠ;,t޷kW֏13MjR;#} h$ ^fDIH mc}I@=Ӳ=B%7x7(bƯIXGW~ \Z %o:H)UD덦0?*6g[6`Cjwړ)Vz|c{n&BM݌<, p5׍CK)k|eZ(M智siREǶF-$蘖؇9'm3}֝*ǐ3^XI2@~H4d6$ue'Y6p`ꊢ Q"! )<PeY{aY7eUP~$^Q'} QEpЌ;PAK'zxx{-

    pU<@ ԀG1 qW^HFMBFgH8g;}(|r,*dy<w'Y4|M}a~qb!0 a.p Y(]XeԾK*#QM{YcpK\|2ʈJ;HZT _4V>RTN.-BPC+UGC8)*O\^qC8"OPəLGCԬ\c10Sɑw dw:D&Q"Q9Au@pg: t@]Bt3!c@0BqguߘNLWR- ) L@wGWܧ cPU& o0o|СнZߡXI&PS "/D) QNH%7 =B dcvq3P: cr&RBm|LݰRxS!-Gf0hI޲bh:c-;Q\u||Mђ&h*DWƠ *Qp|= ?wSq>|^j-i$vt T<>gC*NYm-1@ .Sd|KyjI; yIgI^bI".ם⤣s: ́wND4: ⚽wND4: ⚽wND4: ⓽AJDФό: ⓯AJDФό: ⓯AJDФόznJ:v( :wS^bݼ$k@;8Ω&a)}I}h}HE Nuon%8뾮2tPxP׻Rox\"3h*LIɬppg4$ Fu_-V@حV9qlcq+%6sBJM$uՉ߀a4&8Y8Ru6oY^"v\ʱh 9kֈ0jpg{wqܖ_"h5*)n6W8xqx藡 x 0 C4AvάK/ h ,CH lŻ\>e7D d/ UMT߀A_5gekjߨauy z!TJ-]O XSCh u!5%3GnтW=?}s ]UU1Wk|zbk 9 RTZ%uq K&%/BAQ!yekϑbo~Zj7fz摿Α@ƒU\6,i<+==^6Cw3$H'44Ҩ($SY`ݜէ<@!w)qew~3рm0muݣqΜx}4z{ b}ެk69M4ׯyO<|;cBH@Fh7"DKunrQq/~~rC&u Vm:s\3xnS`oްx^kOY)]FgLnC &KE5 'ñ")hJƒkx Kx|k1`9ڬuKFfQe_.JS,#:6 hED (;3Gʟ{^'|5}68 @8̮@ XIuzu@PÒLIf9S5 ?NhQ5@$ymJ^}2V B@PʎV FZ؎KO6b{p( !ٚYϷ]'jjk ,iw::czXȭmyX5ϟ}^%ξú 3"Ϙ[1$"N~wv9 IlB&.>1{i'7vX"2S SG "II_I0erhgSol>NbV™89|6͡qӼw-_Z3_3 ϻ J>|R+.Ƨ;u$3+_wOU4G8yߙH0_"]Soɳ&<8MW&,exytŝ\","e69M)ݟ8ރsWL1)+{^+<+~{iIm]_uhi,ݬƖraC ,Nx]v{mNt0/;6@@L&a፱o[;N~Xo+Jq,RJT`ՓęCHw_5wܺXzLw=6N?nJ,^Uf&f^Sښ]{qܲJc݃3_1ZPAȭ?ޛ97k-S7I}6yt1T=,n{,aW1&M)h XuW#s-ݹ#^/ڐaRPKAb:&U2#Dtt`n܁-|2~[d28F6;j$!_u['OuQ#o^9? J"7Cmg .oٓ)sTEɆ#m\;k{a۷6ŘhCmon)t HxPe v}eV/>t ot p8Ӯ$<0FbWvrZ6|: =_'3xfnLR+< #Y#Կ%%NW%-XiĂT^ eԁb*T#TkM: g0Mc qoОːo?n @k&zFE{$lMZZIqL҃7R%iV@»7A@|@¹ ykyv yZ(!cc38*,1kJ8uϮ̿{ 3HkBUTM'z!Һނ ] ;Me<%oe05{zgK\jI#ןVU%Ct=wYR6^-lQ$,o\xTqɘT-c -k̫ GVS' $ao0 gr%wIqYlw DwNvm~1a:cG_5s\: QȈ~_"Iѓxl >BN7sI`0(AlW؟oٗPVk  `1Ж\Pl=PJRiJO\{4.yTTtAV8Y?k}u ざNQ-|awx3/Y8 I?ۏƃ^|Y*Ht~c1a?M !Pg =0l 9sͨ,W 3YZM&lnh47`ǻd-f{a:M-&tQ\#t'Qυ<:y{nY>ԑosQP$x7v #E @?~¹8#h0ծ^4%*=F]-$`z&쇌gCЇ*}/S8{HJe\ԙvQn%3li >6 QD;7GNzq˟ a}xP^g>lNySvRH33p%]G`}T B·l<ճ- @~t pWAw_;gd~o2>s?Bu "dddTWR xef"ۂG,;k?|Lë&GE.0n¬ 4<@Mi<שKO jC}hX1'MNb8B74]A7"@!v?H̵]oaD Hx4B CEE 1>* @b"*39UT8!qʉz Dcs:c^ jE{OD#. }xxB "%:ćvLwPDbL:>r[kXX DDdލIEœ+=` ~ 9`R[2ޒ $OOABd ) !@' 'n+Zʕ"3k4Dro#ғ1h[m[4ޥp?oѢi߭_@ێ3XEefd:;K$B@ O d־S6x-Uԥx߹- ?T\X(-j.X{t峿Zo'upuO']|9xnLJ20D L[ZJ͔߷1jmAuyń?@KoJ|~_9=7xd3\? JAZk u¿5h р@ O+RF6k{nޭN09J6s ?eyyʽ.mxq8H]EeRMi3.LZ`xݣ>LGD>pD ^[a7>,[.WvQʃ\Stπ#j)!@&k .'҉F&#̓^^N9-ɡT0r^.Ɓ&}ڭA}x#hDkkݙXϺ'X[v%շM{#(]̺^7աOӍzRZ{E[6{5+&ʈB}֋ɓEz2w|e^i&VBX*z-7ڨ.~ =RS- QKX<,#"]|!|G#=""k { zL=6hz0ED 숫 1@X!@dG>3$|pB VՁAG Q\\G ՌC"/!v[^㞟?T{ҽ^)>&tbHo g땮 ?`P֎𽘓B^Ae!|E"Ix8 "D;0D#0," (}4"$@7D @GC-b@zC<#jˀpad}Xx>PjTO0'@ ũU{œ m vܾVV_}wˎo[f7OjxaG"j~IBFWv@1-5Ɓ>)qjL06gH$ `N!,:EEis-P0:D PVQpəF[%~K*aT .STFYì BI@o1.K2WʌDob;7m8 }fCeG$a[J `l:s"<6_}f<pU w)(͂Wcr0b ㌒(uV˫b"Pʌeǀ}.5۹_}SsHEQejPP)G~zZ4)_{豥YT^+&sאSV_rRu2LA.Qԓ 95˂ s"Dy2%gfC gJESs| !!GDYS˴,,yriQy>S5sQ@x =H霧˒2ۋTS*hG@a@MvSTw,N)0+_aƞ]G0S{a&Ǟ8.:HxpƯ!A+"c'0'xyA "@@Ȕ9P?SL٬}zg3즺uKD&cʰ1Hx ǜ­&Akp`N/ } }!!Ga 0v˰zj@ϗ2ܝ|>Ճ "E,qdGhլlWh7vzǤ돧4|:azx[c@'W§@ Cig`gE_"(A/$"cA"$|DTf H @$Wb&^A?TǤAH0h|"@T>R@|"N˅[`~P v@@$|TfHPi a`6D X-thD f@LUcA 1@$-8z 0v腻ϴRX [ NHxÇZ0D <G3(PGTuaf!~(TmQ?1`DWxMg{CxC%x{ u9>&!%8ln }JY\nܕvn Zza)VW}YRē1r{wSe "[D A0r$eGGN,_/߉)+Tf^pIjg+e+ko@fac8!o|pYuZ3͙幩=1bC;w@󕆳M:RrWgў۷J嚐Xgxv &d+d|@ZsH:UJ*v+ϝ+} b "tMu _O/ϝla[ s[`a'%LoYtt:՘V"@ׄ/-A{]B׺U{Ve^7܃'W0hD t=hBŎ7V\M,"ŃH|ѣPC ܓLa@wϴbxp!eWV0GN >H"F@;IJk%d ٻ=we_zWv]Yc /POr{\ .x{T{+ab*@S"SA"tCcE @>6Qب+eG#.&?g-?90"vt=JvY ! E!>+ ?y(gk.x-bP ݄gG|pU7Nʕ5pfmRNrn@^C@Tv)Y}m9sꪎe΀ j0z

    0& pd-{OS9U wblJv5nzE@d}Փ?c,,߶xgfĭ~hptArr5)2?uq#"<;~Zr=3+ AgT ^;h %|LT3 K@b$| U6'O:|:98J_H"нQz$Uk} }_k á0@3<>@c@! F ZM UiLO~# >wm~7UeI']LJK !G\վܬPoOD9zڒZ{ft"Z^6BhCjeMEwzdEu=ڻŏAo@Jx咉 |.%_U&=+ j=33r_B)LY^5.mxqߪo.4ޏ&tq/~|9E"Zo30$5I3I1@˺qU^;HAyn=rvs/.m3@ O;lMa/nC hD/nq^NM;nťw8 r?5##c}NJnn^Cϻ匤z7E ޘ"H.$"cA"$|DTf 8h Ln^COg7=0hU`:@!UޫLuA‡rL u^D t C5:H^":SBz$|Wf:vDvha,"T@b$| U6&;uJÖ{v""}[O3EX"D\|,u g9DJ z/\>|T/  D @PecQ$<~@ !G\] շ谵. N{kR1]:AoL)ZyA"IRj+1IN<8[S^ժ}r]'O3ܠIgxz`sO%"i9j@Zrr Pmm]tbZZITy?;u}o.JVd71$|Zۍ>>JZyA?2o-?>:գ>2oNƎUktGlתJ䭇S&ˏ2d F+Ctk>n>6&J`p a, QG6GځkE‡MVHu V6bѦ^K> ^yU <- 4gΦ*S]^WCc((X[0 x[y^RR s7W$jޚ;֮g]OW-K'U݃DY̮Ƴڃ\Ѱ МQ'~굷so؟|'؃?<_yބ򿷘,~2칛kV[sO)~& J#eJLe]~>KbÃL;HY5!e!KZ9m`**à.kt돐kѵ䣯9ʿyG?\d]u[ޚ>"j*7?G7ߝݛP6?]RAaf3~܃h@tڇ!CL NUsz|~(u*2\nd %GԤUbo¥fBV>2+?ǝwrw) &ۻ~|Ή%MM ~:q)yw>~6jf]sOKSʛw?3<ȑD<^{0U.L&ͅ%eښaއ?z۬),z?wd2Ky{{%8IZwO{q$N(K=5fT ׼=/ ;^!GOrof̢#Յ>J^Ν'Bg ߃~g}xSZ^=SGz&Z, _ T=owU֛ʗ \rd˕_+_cާ:Xt7 v%{O]G I|jğL%?dneQGy{ WȣOFy%2//r$[NUN _yYabg{b:L? ^kKw*9-#˛//ys(ݸjzV ]B(vCa tZqvm7xdV2o5.xi?uq$uPw JG>Q:WYVOWIyUq >4;u@*䓺PF,tc }`ڎEJO ʑ9J!?o'-|ę4y~/á_o5?*zX Q2Nrܩ5\;T f1:kdb{l䂿? 803 l p:`em}o4n{kl5kg\X`]Z8젇&<ӢH2g7x^^k Bއ{C1t8#B1VPFwτv[;n!/2I'ONy[,?~b_3zHI!wEȤ|>y_Ԗ[mH7ʫ_T7EixULֲ?pO逷,bkJj[, Zy!<;g#v+Nh:g{uKewZy)[Q`g ±0C(qWWosd`"UcD/@oփJ o cC8JC>xbl@X#!E X<16D Ѓ C2$4v[נ}]a]"tDr4PkȠH_DϹ .o`v?h{Q:Q*M#^xϮAnNY`%L''v;_dW׻Qu ݮtN`.ҔVLI{~j!AmL\x%TIENDB`n@|&y#zsPNG  IHDRsRGB pHYs+IDATx^ |Tye~lLt󂄅:y#*R!5fKT[dWYJnl0P).q㗊 3DPEijiR#A4NR7plsιs}3sgw?3\s眹l||rjjJ * RL* I𰇥30qa iQx³-vj#i8:d^Q%Fw C9   y$G   G % <(:zPXGK,z+^ mGxpXG5+#=Lz{Y;NgТߙas'O=:m:c`['== B`ڴ2=F|0Gst|?M-0t፮ɣ _YF&g]X%1BòϣȣSNLxco=֯h|"rR1-vA;G'hJm2ڠF9SY,$'P:#(.Qy-٫ ǞGO $<ѽ3<{ǟ,ܮ~|۲ RIV=?&o£4Q#7&^Mٻ`#bS_5}gpwqY?} R{ǔ̡w!Zq5wyP?ORe=WibBO"g_o*ȣ5U4I}Eϝq0ddQ'@|Υes.Ns=O̽+.Af_2Q7elL!]@CBUN5$dpIW1NqAkABf+4'(Ss~llbjl|jb@!ei2䊀 ,GY(jlL{tOwq׿zw;gVXHn[ۗx_2M ?RD8yqaԻco.9IuEpN 穧&}~p=L>} ɭїnԭ޺.3g^y'N3gy?~,Ld)Nc$E<9kժ]'͍| Snu*J9iit+%y"@+C񳛮M{M򦏾ԡ~ڛU{7&/W^~_?~ѿկ~}[:|0MiY+o؇t5馛.]Y GǴqc7?v1B]S,+3Ṣ̣0n85h7YfjF ׄz7r?zK_4\;u {i+cc/<~څg/3/ ʬY,~%+*=9gق6G߷[droֺu,~0?ʪ5}WrQ|MI KO|.m0W<(\O)V,gn/gʺKs[,v{]PF+IN`=4=o+cA0}'?=XMwy=T9|xeD8؝O;1AǓPAme)!_~]1g2C_ڹ_}9uMգٶ?~G?/7__޿L̚3fҏG\w>}:m>N lްS¥|Y z~y}1 -m\ۼi],f;8{{o~{}gGOzV|.yz߾S|O+_^NKFS~tW~'ߘi~iy~W).5ũ'|>y}{$?MϺ/+W>Vn~ۗiЫÏ=ԁɗ*<][kc7?mv?ޡ\6"^]ǃ)ŚE%6uMF'?6}EXm;s;,;|` ٺ۩jp:Dì9\>h?>̊]_pr|+r3|5_a14ɦ|)dI#tWqM//QO{>)W:1>W7-x^zQ㹗yTA `#cɥ.&Eʊ^ eΌYkxWniy]O/ɣmq}};t&24#_Wyovm{챿si(Uml}d;RY$j@%.Okc꼅SK)Y9R57"ƦtM/ݐV,`YkdyxSLankۑ7|..bźذ mZ$I|l.ԉVnX̦"B%mEnd?ݰq>eCsimwgTȟ&1 gӔvMOyK'hbZz.ө],aݐEa3z3YulNJ>܇yv9{cMf> ΧV_ WξR7{̼]xt e>q}]D`)JbgGblĦJ9ySekנ-oyjs_'7'l[$;dU0rRX)ŚEKن;-:7OAlS=|>}Z}h;|r.l*u7kM θʧ>o:a zzo kH'~Y5hJ]_1Ƿ 5Uy]SS˿7_#%Ҵ5HL]'oOO}#yfOn^˿+Ͷ;-i[E"xbM̙?d;Gh<:W{ѣDoc_ڊȈZ|@B(}.亙ޝ..Z_8q~'fZVXOqRKNuʏϾO\*>vSӧg/%`0l7 h4Dhg\A>vt`g\kgjh/(ѳ >j)Er-4MtRaTkQVkFe2JGFDí  !   Mn{p&* qE>9Z KEoNKAe2"$=x4msE&@0ylӠC*,`rY I(CNva]xSgTxBT* j~vм#؅35ɜ?ڛ* m87±aC*)c.°3.N@@@ _ErA@@ LaDY   /"   &q'a DY   mӦw_ @wlЋTy1 d:l4 Y!{PQ ]|hX2sԮ޺Kܳ /|m#+,cOު|yVS|r %7/?H0s-NG?q-JIS"Op 3b6jr5'u$W)'wyo'uU( bm?:1(dTQ^9g1 >W@)'U*GZ:a]o{Pzw_)h1 rI QȤCdgrnjUp{菿 /|U̼'_CYu/җ`lx֏]\5oYȬ$w]L|l˶E輅6'FA@I%r 蓓.O>-'~;m&U$ư, mPjyi`zq fjR Ϫp4i׿j/ͷ PRy9/acQW~>k@0]+J g=:/W@@HDsd]5($, nt(^@P*}tE"]_u´ Y &ǞϯyW UUf|%m 5eo/[p2D(|x4]`6dH@XG*Gz E@>ɛr99/m׿T|¤GȜCiogT МIagDgg@]9O[=3鋶=GI^`;s7?o^uR/zvY~3.mW%`7!<(ɛ@$یyOny."0DoU\So0QfطvES>Ms3OztB$` n\|zS&>ʵ2;W&|PDMWMߵ,7sQjCWca[=6."!Q( .] M7K[mjXP2Ž"YFՖ~E9d[]h ^j ѱmYD|t|/{/Tf\#qMbƲQl a-U#frHN"ʋ5ԇZlU-sF6}W&T2F*xSCV$p-&m S3 )'l-@A\T!_j5É^b1&:O[<?Aco??eљU|k_ҿ߮Uo9/ O,빲f*P 蜮"bLKn) *=Pj}1🫢Q{ ]D=uhlxkX<]9=6j$.dk]Psu2RYL,,ygL}J Ug UZ jf{n{ThiS\WYn=څJc4$9\l<^[A韯~{,GWR/`6ЬtV5 4k^?mRgYR4͊4=5t鷞o3Wbm54vJ1}7}ZwU^|LjIS f~ߦX}-\zX`E[\m䷇5]þNUL]GR(Y|&N%%?x?fh\KT/6W?-xxL Qa\%o ]U8Bh3ckqc `Ny@u|!W\[]ԑkUu'љRl^W岩1Yz>vi]\9GǼ9Oے?y_*a.MVu[}\,ɥ7 25+MR+l) =o?Ja/{$[c ֬%l`lOX "$.ǁ/ٞ3#j2<=vDUg+җ_Sbd0 )(7Cd#NI7ɾ_xـ飯К];y6t=7oRI؃:=niRWJV9]4ս Ah W0|>-.oYS2߳Os#zkU0ՒyW_ Ll*P`Rғk.ܾs f?Ⰹ5-nl&CIŴFJ=˷ rU݇V?^lQ\u`nYmkn<a6ʹRι3۠\SRdۮ@!f&^*r]9_I[kf*l\9ٚoǒպ`yswEڐ͞Zx^1_^A.__[2T6:u&Uɕ{%)+ M\K5Mg^EV:aJ3WmY+mK|v!/= /)<Οm{tFAנǷT,uo}9lO8ۈ=Ll=b-ѥ73(dErw|j )9{=JK6=}LLn 17歯4/ msUGMFMG^Ee^zC77 l>aXݶKyKs֛RBNdI{|iZF|" o&uUmC ;;SJk޺I"ڥT~24}ƌp^cqyyrOJ_鵮t`'3W Q%HU8'kCViEݍtKbŸ(jimYh=^дuq“ #' jXx34dm1.8;P 0&9D(` Y!@=D( @@O=j_uNj2æOz@ 6@sKڐ"}џ=! FQ   X*ۣ?t8\(/~#\qs {O/nȥЊrx3g!(\tG^pmBi   PKa:@G/ʄ)  %L+xt2GwZ5GV>6 , e])u *ۣ{WFP:lsI@# +ʉs?EF<2)1 0@@ 9@{U;o7+ZKG`|`.Jް cVH)MP@\қnue j>hпY>EktwB+yzXZ-6]m*}[pɚ(njzW9PD:.9ib!MHvnGNi dA,1!%z\Oc0SZ}ta̹g_\],:{fK~n}n@ f Z[,KTH(e nmY| WPfʆEɖGR;UʎCa&5 ^AsPhؽ~ޠt&E1Y4M> 2*$MŹZ{bJg]uUJKW*d6ug8UG`ixM `IyZ6dqg{ Ӈ)ħGZl"vEKwBʊZ^ cVt5-c_oh=ӧz%)W,Iq/.MueEg:qlMaҔykW$zY۫Q&[OQJu=TWUy68wUu@֗KbMG[`#JK1 * ) [Lꎟu]޾y_FlϹEڨ"_kVgtwqp2w)Eݮu5=Uj[ HoKRۍ@jӷ 7kbGۣGz*=+RI=gZT|uUXx1:2_>u<[(f]ks֗b2n)Uav2%3޶饩|jt=z,qTķGz3f*@Ҧ7pNaM\_84G.KS]0/]4ȑOҦ"{b> ʗqNj:i%eݓZvBC]^|V@ȣޕ> xlQWehZEN5%E ?IofRWRX$stnqs}B>';:-eOcݑ2ME\]Eج!#jb(^AB.6dݕNvBsWvno^shU]@fs'oi6=z9m:\%}>=Z/Z=v]'S(N:MDBuكl-lJ|jEsq͐+94#LȜkҦUm+Zc[yVq}m);٪E{.9VfFe=08fE]>/oq_XV=3!ngg`2E:>qŖ R>[ GR G74ltZQi4{ [Ee= 4!f)hCjQ8M5T( @ _WcQwc?pf'$[ ZlAJِnzzZJh'OÕ@@@^Bo{\g/,Cр|aݷN%0xiA=5V ( 3g!(\{Ԕ'\( @@@ uφA(@@JOb ?[d`4eo!E=ۅaPiY vN*g㢡E~䣟+VrlW)t#rR@@@ G#  Y%U(@@rD=G!@@J]ëҖz\[Vgrת2~sHjwJj,QdtpzPJ44}S ZtJL+{xl23-}t?G|6E[! fd^RYI9J{}iTmRzXF]n3zf[ݣ=K#'NuGy$46~S_Ok͝wڵͼժ '3t\J\ᛯ5nzЭyMlBnG.ɃRdJ;m6H2٘syugywx q|@a7D}r-!neʄu=H0e 2f"-&Ё-)j-#Y|*H bw"@ovEe}JrrIm#Go$߷6l%L k+KTH(e nD܃):~(R;UʎCaVTC]xBg.˜KL00:I)5I٦ɏ>ߜ[zU|1h–C'v3͋&T6{8euNd2wgi!vK;W AWyYdM,eMc!l)_ 8qK1bBuyF{oxڽWR/`6ЬtV5 4k߱ZnNmE>l&bkjk3uO+j籛 c{v>(_`O=:]Ҕ+U gd_^qLW2n)Sֻc(,\=!*蛎 ŜFuE,yJ&i5{%i :vFjIƳ; @mbc FG:cpІ#[nԈ^ZOз\DK:rsm 5 =ԓp)6/|,MUvtHg"\MˣS 5|]1%\\)BĻ[ל [C|V:aJ۲C=˄ټg_hHKϫbYqlNߣ*5~]ҵ!}mqR@CZ+Ko1LQ.E|tf6Z5Tv#2{Ff+!:EkmSŠEsG؟YئNQ}U~3I5^q,%cܩh6c_;Tu} &3́%J%{dENS%_ ,q|O@|zX 4U~T=N?,rk_ui޲w(r{RL.Aicpjմ$BƞJE&?N߶iXKM/˚B({Gq[վV9}YJ@ahzZ4m]j?Op2VwU /׫pF)1q};_#Y|t/DN{GQ}?-OÆkEьaD0/2#gLXuK-dVw#;F݋$|Q]T?=O!@@@߽= hulsXGp)tq:zt@D  y'*   x ;xt C{RO{@ X3R,NI)hy& \r*'Q!eClN>vv{+}9=zHx]q>0 SN"gGI}:1%`Wv?q^3N[[b>;rIPVVH`~]}Ξ៟u]So0QGl!&t,qRiDPiQj@]MK5&ϲs~UB'c2޿ofvcGMߵ,7sQj,+@4BqIoqu5evn5B K2vnSʴ 5y(_`O=:]Ҕ+U gd_^qLW2n)--^~ZIa-^RDs.o Mu U2R +t'iтSs!M~cMF2s9/ƳW"(kVVl ȠRg pdEe5*갍5b']ԑk7(iН衞Kg_ dlӘ|Yzv9.-l}Jrȫ"_k6f+gMvU RCj\zӵuY(SCjZYm̧۫,22T `s~h扯=,^4ݶ&랼J쑁Bs螝.+ Âhն=ȧ'c L\g. \C:P-td(kp{^^FC~e:>~9sN!mͭhFgVQ=S|jR{TJ M }Koz.^_gt {T8|$=W:Ǽv]f$WUn򛹂sz8ʩƲk omq46I׸]guP3r7g[(䮨^VlT,6Uu l]_zOnu`nYm@]f'k-_þ}`t;=U-SY~J1n3A~,Ti:N o6D;qj6(T%G= $hd5Ͱ &7!Fm[+ {]u eٲ~zocM['O.8hU#Kwj 'd!(HUm˨e34l_VYE0^ [_p}Pݧƅ̈ԷdC 9C  "ר{8@(lQ6ڃ@QXybʤ뵢qhu4F,XG=q)tqWaگ( M@@@`m@@\O;;=ؕh=$yKjv8)@]g%֔wWÎ<6O?yw#K/R=4Rdbi^L0oyɾsgϺNǩAطGC@`tdP;s=@&=̶GkHG=sȦ*K!94&A3P>j_J?1x<˧Gh':8kO}7>je$p4?R7lng͌]I KzӍ5a@,s!JfVؽjSd' ܑ^;y∲t|.`ʑ'SQu+D\t;Vu3~v'=]Z޵ݻ6LPu9IiSX܌ԑh S+l@gǺVs=TUm.*^(vy)nfWt*\SZ}NaؿXvu(͖DVfwzy}h R&;6J!q[` _r3(`Xly)}3_xH}1vwgrK>PR]yN:jT#"mj:~soWqOmNymFJh05GaPn?E3ܻs۳{U+-C/ Cl ,LWi 5$,kv\x'n)6[Ln2hO9]*_Uڪy=;VMѩɧGf6[S\TU]yf˜b<&oh=GN4E<C#١>%s0["zY3B^ʸQ4`o3k s傼O2a'cxeb}1\ûN|wY_Z\@[ pN) (pہkDۉsQjg$&oleR3f'Ux[C.\~IZԑkM4NdPOR U@˦G :|]s" iyt*!_>kN%.n]7ݮ׮RCj\z38 SCjZYmzZax2PқkrJH0:W/umKi~Glb;q/*/8m6!R}T 2}ƗA2R啋ŭ8JF!wE e^|ۀ/Wսjl]_zTXԁM̲.V<XOS,;8<}`t;h*ӋCFۚ϶h[CGsCNje.jZ>rO)S|!9\-Ko1LQ.E撒>z9c5|gD%sVCtpnا-C^leDcB=:EUe+ut&uݓjؽVRYJ%SYQ5ɤ]}e=[īKUUwF1VbTw/0%hJ+*1T=H}G>S.cTu} &3́%J%{dEK{6NSuJ491?r8 ~^?e8委ݘ?*M" xG|*   -CJkg~?KzB+F!0s%upy43>5>@@@ igOA@@t`{5,(f=z66( " /%#^POoE -hSɐX~5#IGԞJk=43KP!wP yzFA l(   EF GYOI7lno;n-qp]E%\q:"rJ̎iO*t4/<:Yd^[bfΝ>`vTzuP8qF^-NJج7Q[cxnI[[}VnBgB"hiLG8({w>vIK0k@|?= hf(;2=ͫ]"}Jli22[[c1Sti%+E~SoZcNlSVR>+,0uDldzjWN9+L JZH6Fgcx­e=e44) Wt5[}GWW+U9hV,??ƒ]PT~C>E4:2ԉstWƕuZj(Tc.MP}'[egtfZD׮ev5Q+^*9v\rvjkc35`ScNI[VdgRmk?z'ČUVlH4gm mB6ec?n1y`F.g\rEE@ 4BEԄn&KuIp-dlQ(vpXe oRɮ*5=Uj XJ]9nV>їWV%9MFux rji^%42+(VdL#>D.JCx"(`?JcV*_df7bQEVEjN φX⨈Z:cJMB~cDnLprj#չ m+@m^8s}xjZQᆾt?h]Yk8Hەkc Ov.J ܊Q8{F2)&y+iBNcM HS ZA 9:+D9ؼ/~p ԫ=j1 )[._Bm4Wx=lfZ36G]] _8X-;qirml>k0 jE7wzImw) _c"J6 KVytz4(ml5紿FZVlFKE1Al=%۪3XM칼EUھqU Y;:VUUP]p@H|pѾ.cqA+bTiJL)*mW-<.)VG+[7DKm}>B($i-߶ '(6ݪѣnfWG UM8vsrjr<6h A_V^*niR&>7iav!ą4o0'mcF@@c{r2nbQtGV8Qh<(T(  K E3lVG}Zը8p"fB,*PvuOÈ̒xinYE([s&Z`(D 6 /w_88   2eo=z-V@W ( 3g!(\XG'J<:z+A@@ `D!   gܣOM.%` nROvY ]z P2Ck4 /fDT 9@  B <^ CM(Jw̹-zX@%٩9oQɖGh0H)*-R4Azgx~{Q\BբΜLĔgh,7!5K"SQLVl94 Y&jM4})^W[!S8!m؁Z{q= F!}S{(C]| 0O f#&o @}EW?BXg++U%hV,??Ӓ]&|փ=}-FGjQʸ24Ƕ\B:k8Ug{ <*pj-㜹څUY'kG޲g:q{];Nk%$ݲFJ*d* 1ٔLO3}Ȏtr{^n:#I0`j#u dخ=l?̣OM}(@O$+%H&@j <:xbE  h   P ыaa pJ᪔+$4B+H 8.b߇b^QJ պPxBB.4L!GH)$uD1a403ZgA@@ ѣQ@@2#?hGF=@ Ȍθ<:yM93g]u- @@@ 3=@@@  ʐ  &m(@@rA=!@@M=ۄQ>3Ȓz}?ѓTbk3sA5l9=j5%u1.CB&,<IA@dO ( fʄIhpR cyzO_Vx]%YwpD)Elyf\q)9tDMz%ÎjyZ7~2޼S]OUXVqu2vm<}?L8fvC#U]wS3kRh GXt: %U RęOTX 2+zU^f3~r=LHʒ̡AJt,?&$(Bzt]1%]ݡ&6sbRޗB4~ye^lF `'%|#ͳwuKJ#;eQwiW&|(X{t돺%+/o\kw+)o#,#S'P6>>oπ   $=FA%(9%W0@@( e(#^rUA@`Z,+=5\|b7wv QHrL*H  0=D,8zptJae9G`}N|IƪSh]:4姆oh[<8:u\Y[Cj~hkDOVO ᒌVfKw}v@@ Jd|O$N-Jhwꥑy 1?qRy%WߠNęnmY|9nXΞ1R1IoZȥx/hЪvnsuV,RzȋQVTs8vVU2ۿy'n64@C9bhH EB yXI+rO(5"]2 tzȠҹJLinJ><=;=ԣh ҭcTFR4+kv<˦v(rpFYr)ã S\)_G-u>Tɼv 46'7S,[^ @^ dͣg*e"Φ4m]pk^js=:^Uka w^aP>[3  $lL5`"ݔA,i>-_YWlhDiP \ }$,$%[WwYYFKnZ7•W*=":4p=PnTlosgt4 *$G_യk|+hy8/V'7pegqCt0{7‘lROF%EG5|YZyvғPPILT咗u| {LX.dw~3jD6XDM^:f!JB 셎   Pr5G/Nz @@BsLgByA@@ *ѣR@@2! =GJM@Ȅr2>*al(Ȑ@xt-u~s7_77SOOw_R&~: 2/y~~&WRqqܵv@Mn5 Ao5H'WuŴk~ZZs~gnt|bJtHqMY7RaU4J%:n @xt75^vbrhQےoSė)t^c[K'I[nTF!;ut: !tpMO.25H. IԸ:iu_SkÁ%Г\\ioNM1k\FI˱;Sou~rj'RŊ=yƱlʌnSE$c ?@xt{xM w*GGsW,_uS2!{;u|Uȝ}Bc&# ;+Qɳ|Џf!K P Vحuj. XZ;vvMwFZS3:Zl1ήLye,TG2J( YV?л}n4$,JOɘFZDe"S\@J!Pgm}@&rGsOӐ 7hv QJNPZ-^ $rLʞ5=r3z@ 3WGQz OɘZr Ruf+&yB{ڔ5#&l;t`'`n@{-ސ+J,gcѣӧ\7E:wUZka^\)$k鸞O]=ڢ]>PRNO" t:ٞCku";)o)i,O*"`J4E}[/.L6`57O7&^Mٻ(#SSLMM^0959>9᫦/x yy)l4e5-XG/*|5ɝϹle3w]te3C/uKf8}ͅC"ԟP J(QeF;М|blbL񲱉1S61O0.d3x\EK_"Q^ld~<_v{mP8x(Cv;Q6N^51۱2KN}   @>]NׁgSJvÍ+Z(emq$j@J@ghbPmoaXWw&>kժ{Y%Bq|g½T.(w)e:T }  :=zZx긲Frʂnm# M/X{LѷI?d||;u"  @rqr{;*ٻ$u vʂ?;k!ߙG'xRխ[1B_Bl(iv+/]w!I@1  Pt4鑣-u]C>eAvđk9\JcT>ɮmN]wSeS5\ P,oڱc__tYʲ֡C=KV2萀%ܸq3C?ḍe_mܡ{oƟQ!@;&ϴ ʾqm^4]OGCH ׳JϋF2<2Udã+uUuT}{9󿈳t`24_^5[N~K֤VkUtKsjmk{7nָB2T=wasռ+@dţc{ KKcxb$D:-sHd_A]^{߬+?ہ)wu! r/1IW ?٦{L}2wr=]JvʈMvudPZ^+WU,lr~b{;NqlmicQmTJ_^-Pdǣ}CEu~=#jQw̩4}e5^yX@- @@Su5i2:>oIlS.x~vV0!Z^Gͼ%REڴPIU5 $ dɣg/Ӈb&cяiJp~tQ5=qIMbmB]3kَQ⺦X i.2:t9֓Z:K#@=P*w:\87BSX@q>9xh)V4ޚb2|jŠ򚆣:v̛ G)JH4e읩'?SgL-.HDSY0߯Ҳ65ًѫz{ELH7 'љ?b 9C}겕}gMT|VbOOCc 0ѕ|mϬƲlmiOݵJRi/ªTGVK7Nɻ\"Z+ EϘ|7%>m[![49 gk^Oh:A\מ!DZyUJ/SqtXaǹęUmW"1VM ;I'($p&K4{B#ƕqeh-nJ 'qN/0'qldPag}M]"Rq_` Es|ܒ&@x睷Ο{bb|rrrj>Są''/YxW>5Gw@T+ƃ@. `Kڐg̸.K^rK.E/yŗҿgϾAy(%؁;xtmvNqvr6_gs1S׻G/_G7 %H+&G)NUN==)ZVVF@T GfW 2cb}T %&dD=#| v`'wN8q7ljg,9x"`e<|9օ;׉#Sw(&eC@GC'ǎ W_}22pq% f#R@-& ,0;uΜ{H"^T c 9rRBSW]5п%lWXpaXDUVT Px,F L@… ֗Յ;c}zTr!#ϵ3љo~ʖVs#%sZT?C/'sAJGB iA lּe\j&l';Fꯠ4Rk H=C=R zDW5`^+ x {Sumg{L^/:sL(ݣ~ߘџaww;M7lOo֦<&"/?~A=ESDAκ&),}" Vn)X˱QXB&Cq&!AF9vtbs 0Uz?(ҋqt-X0Dx7\G%m 9sG/gl<}?O;{zye̘qQ釧Ѡ;Liy_a]N:19vv.Xsv̹-O:l5$zygw7VPC\([One65vYdcB$.^)pNkZa97PSȤ4`h3mtwk%O1=1W =!gE-tTAgeVG)8I14ݲ:~,^B6 6{\:Q_}*t>5]BsMu/iWODgv:r *|;eM9Wg_T3^ǧ\5SOHL zɼêκf;S ;kb\˔S)+s6ڏ2J=ԣ*HZm.Lߪ%sj!fҹ?=˚%^%݋!ps?w``Og>s+;°yEƩ\9YWo@:'[=YSǓeJvY^eRFᪿS ITGYr("l&G&] .&'}kҪOǏ,Gcs__O'OuK/2:3d^9@Zڌo1V2S2Eu_^;HuejM,l_&+X&ɽ}~BC n rg]ŰQ 5 נFա.[C:ӣ/z0Vky$`^Ggh-{Y|WA|TV=G/G)¡Ë8ciPhu%Q- ~jJwY/CZҦW„.K[p INZzL+TZ:e%EqU{?t)zfR[9Bju%<6M;o[zC~dC*X q.b .ı Rv!ݠ|߮-=w-ח4ͳ/<*iWvA|s뢨Ƃ#0G/ZM P؞6 ~6 ΍߶9ΰpN:b5 `!@d@@@cٛq*H  E@ L`/S-|*CL%6qxUaXӣW/C'*j$G @d kn/zSP`*[VoJ,5azNYlR-b4xhZ$sYj!guu'P$#DW+B1)0=:-]W@g$+kk%9 ޿EZs0 rJOLsZNw(tv @fXu5~I^NXocĝ˥^Pzsw?.fkJQ:?巳mXwm 5,c7jorGokH@V? ūC9R>['fBiK,%M#cWRbO:ѳC֟hI6a'aV5跲OrcTd1oqﶜK/bJ@sCA$ MXrS|5Xkӛy;a=Y;Y:; nK [ִcvM2f?{ځ %YbAڦNXeY]kgo@:laYEiY.@Vg+ R! lk|tϝb;Hu&jw˚`;v[W2)R= LE*x(Dv9z6`A&5v +F?xhc~ZDg^LT F%-YR= Bl&9*3.*5=J@쌋ZH1PZ@|=5  =qm!!xҨgX=̼le9Kgι/x֌fΘqiM6>\0lLNEπ|hT>'.䘏*(Gr@b&pyx㗿k/_/~1;Qx3  x0( HoӤ.K^rK.E/yŗҿg2N)ӒL %D*Fv) }WWwѣV{"G=rUJޓ[u_2wlKA#7pwnS|jT)!C EM漏qw81ywvAJ=! x_~)Nw;b փxGf %b5\+WߑW++{$łx"PSH]]cvk^H5 ]A /BA@`RtN߯jlGSK/) n[zm…$bJ5{6DCǎAX k® m,X`vU3a 7x|0& 3zo{:9Nq xt49sG/gl<}?O;{zye1㢼i B@* jӧ͞=_}*t>5]Bߋ @;or,n/lHJg^> MUq .Kg-\{o}=ǞK G'G@%@.ѭh`R@)(!^ A@JvƕLUP&^ @@J3+!rG R.ۢ<^@0)Ԇ V-m5"-bVA-"(RTGLlR*`!7@69gfvv7&c=sw|&46|Cݓ"{ X⎥?%1q>B`(B<2E""~ .֝\16|iFHbԏxfԳ)XY`+q+l_r8G4+~qbl # HgeԀ3 |̀ @&G%43:HCol?R \nMH$[\R~X&Ww;|C?@I厳858"k@-V÷hT`^d`!2[z8,NՋLL  _l:%xZòr ;!;%G\_+7\w-ar&h8K?i[)ʞXuuyNN*1;* ܔF W2C#C7!̀@j?߬t4Z,ؘYpUms5*khkETee6Q F (c-[\?e@cՈWǦ\ J!jxtW8_m_"ժL>Oxߢ\%gMxQ8Xbs$U lIһL%QvQrqsX-tH`PYQ׽śfa[Eq+x\.A_Nd1DSo|R&J,0trN-bN{X!e4 4:}cS8t rqsj &?WA/ǾI{"fKEKcA/̝˟yfs^BBao`%A~2 D%Wqfr9g/RGuaD8 " )㛖8 0F9Ī^^f zH'ҏ*2螇37͌I#e&?i/3#3}˨(}IXJ`Х/v'o_-ѱ"O=<Uh|03 LtWc,)f5WݘnjX%ä g<:7s3qkJRD~ 4'bꞣ][҂ig* SNI/\ 3zR([Q??V(̍} 7v 3C|ȀTY7A`'79ܴf R Lz`,0i0fD dgdexd!`} D1%e|9w5{& q+;tA&eܙp!no]急L>;C4ʀLWT~˧ŜԂqvo,xd G]Sp BvYD˘ۚ'z +d~ [csrK!l=k`S,(`r ЙB iq7IGyGe!>F`$>qOI N`!.įBw?`]$B؍.` < c)l:D|4+/Y^l_G$b#`^#ڜÌ x.],m$ T[C?qBҎ;žߒ6O^@~R8{$y|aa+ "L`4$0,zٔ %ˆqKu.y*}\uЇt塍AS Btt-(v"]M'䎺N@˅^a'X+~^t,)S<|wӻWk@.K}`mZ NucA/@jIWmTfpߡdX;fn$]{Um֬@܊H?ya]:sQZ]p;a ڇlLmii:|=bؒa _tcPAے&B^2́>4q58+t$! @>2B/2%ت%K9(TD) 1kXa@c=n +C89.L,)5;/Q?G ~- Lm_n&у-94x4BN9|N8ŔcG`|tdÿXBD@c[@"$p<,:"`l =82` ! wBW!n7 quoWb _́}Zqt! {n1v }`3k!}{pfB6%pؼ,"y c a@W \S⚰Fl ~2, ŋ'a{'>)\ (ʖe;,V*6zв@&?=W~AOK{㵼^)\2bOy}TYi=ޒ=D_Qx<.uf^֡L&C@엸=mJ}al5q kr*O|'8JjkB^Dn[r%Ix"-tMb}&֧bDR{[͂Jv+ҹ"+,*Og=ތ" s/P NŮzwՅ ˅N$AMϡ9/>r'j厏C nkS_۵,tAYRTTI{ 7{lTw/A6\/BFr`@!{s`=^l+@"j$8·ܧT+JN!  #W4"2BФ" =b-L@ocҷՎRzOhyXD#08F /lI:D,Tޢ׆E|@c@"Q`9!@D!Ɓ~DTQcQ#/lza 㱜U&Y9]¡ *Oڴ8|LNô`_*9 r5\4?MK3?mC*.B:@lTrjsWufM\띚neΟ|j9\OYiNb" DA/: LfbJTߟk2t eIQQ 'e/ \³ PɯgܽF.B 2TH~3,'  bg1W` MQ'V#=*Qa1yD'c١fH@Dz $p/ =lD "@  pF /&0mE8D 'ItKc¿XBDZ!n@"y6.  3Q JHX~S#6G:^+4"]/ɒ" q\.֘X8W,Xh1s}/0&);DQ\v?]Hȝ6fWCV[1   ,lr4<_rYnn<;d~6[}uc8Xڢ} wy| <{ÎǬ[ V&/ "q$$phY.UAsTE$qà^bV_!År-{ A lY?eo8tܚ-_4(zHHOXTiSbuMFL@E%p8*7ϙQ 1"'_[r5wK͢zܹ[y^G>_.mߍLY!/wH!cWJzsՖo|sIRͦ:uM}<6abP,RVTywSҍ!Żl0Nx U7o?@6<ǎ Vxͣٹ0{U#HWGk}d |e'ݎ_Y@x֕ \rnҀ$~9I5_Lg},"yYCeҒ{U?z;%u]v'uWuȬ+4떌5QV=ڢoײ Sr]]Xi~%|%>k~ hDO~+jL_NM)L)?9[kB"mkeJ/9/ =V ~VwUaZG7%#GYZ7a[^_FF֩#:p¤,|мɃC޾QZԨoU:4 d`BBJ;p3x au _0ʒF&u{09jMTlg%Ut=Mg]B$a7K'Ӫ k՚.N;z|DpZ0N`՗B{ #Grhv4^[?2g~"C\{vNLJgj'7jHn7LټjO}i׌eDAWQ<۰ؘEf7;.㛭^M>ڟ *Jb>;M{bvӸ"Z __ \RaFضjXcFA]J= hb.4jȒKa/M%ɌZ&/8m SCz0Rf!@`Ip)Tpb:@ 1KD T C$Ӹ ia0Iq%Z|>(#f FIm5.d$Oj+;TԝL. NN´fEh`ʅΝ\q:NBH:;[tn25g@@N'D /en 7IENDB`nW5ViӤkO}PNG  IHDR+sRGB pHYs+4IDATx^ gbS](}IGPdWЬ 1 My1Uw @@D| l匑 *.@`fW1==;ǯlٞ:UJe4_dKE}ٙC.{d2jH1>h4iq{_m_ThsYP7s|<9dvd?RU.J:t,DHQX1,bD"7pKH%a@ZBU qs}_qkq~$n8?5}1OaI^n}KN +"[(lYn2^@*@2]+A5Owv(PlnD"FMKx ̶g +]uY蔬i-?O1)?/?M27\l2/frR9CIͽ  `ϙ| y$u\dw1ʓI\v!/ܧ\4|y6&Vա'\-)SY|&@MHmIg\mHrK xſW2YrsAz'?I7@߈M=J4_bgZYC$ w?&5Nj|F,O/tV&>RR!$W&W:~SM< vJr" ( ኤIKnǑj' KK@.,V'BUE^I&(N(V@ғH[ηy2u{vK \uE:t1ZNo6{56 GRU: R3-JQpQ#_e_LiQQ˔`]u ;{O6;V]N>}MTY@eEZgL=hN//'=l'ןK[oOVRb+vmX|+Q] ٭JguUq0,2Hu niPfxjg_ɞ=9}Jv1SC.z#v>x8ӷa?J6ƚxmbZwV iU^i"(JDDŠRL y B9EP?U^Eŭ%OIP*(ɱR^G3UId7:%h83G!nSWo⃠<yϫU; D֦8Z6~-z mw)+&0OG=#;N{4y>ZVEnigDA)OtTihwZwW^" W *7T ]i,z@TT÷di+DȀuծȁ@Vq'sVLW~g/ #V'ȯa7kh[y7% ^BT2줺};v q"z$",r=>y"0*eױ ~ۯaW}Z\~ '@+djOp./=MA>V}FU'|.^~Vh$x[$1[IٖARнrms/'I&T@uڬͩV HCY?Ӓ+l YR.8Ny7\_D!@0QOY~ǝtȆ%?]<EmLVyk6,sJnMa9a%P"TV(rW%;yy.m&1V^=Y!9ms4KU&VQFiQEt9L*c;M|buJ0 "Jğ@{pD T& XYzd>ϬPup?ν'1ZuIWa}af %-VU٧Ԗ?+fg:vNjH}dޣ²(XS'~QtT%.|ɭ%'ֽhO}xi g+اEc'j}{^9 㻧e|@ /m`#Ip}C{ػt:t`i-n.$-n#_̐Tt9=N7J?8\54/ؗg+ ++u H]i,5UPS^ZÖX @ҙp huU{ζ [t*O̲$Ba'v[ΰ[R mt1>%wݴUR Yd+}}}IGD>yB٢Yd\,Y^ivyѳ^`Ժ6|觰||%3ۭd.|UD} R@$|pS=>t�CtOS^m9EYFXO"ucy׾Μځ/Tnk Ò`7מg-$CY%hxe)@ <),Ee>'rDOr4c\m;|\+h.K^͆ kk ԧXp@SeY5Em7˟"@TN>lˡ]5S̮?iimyEH4CǢ 9'@h\#c4(M?Oa㮌5'Oy\] s#L]]s-nWZ̶Q@h3[bPhš^>CaxfOA$t Ͷ@jK(=)'Kh>'obA8 `*z-3ӦM.&V/Jk7v}>*WMN~W<@@@'>Qv#t0ș N  &šـ@(,XE kY  PX*@  `PY"_$@O|[h5@g:+t=!E8Q @(t;lt9ͬx(܉7@ >X,(lv= `9(Q@l3V֛ dVu3j̺x0VݺA̤bʴB[o_~0907͢/Eve̊ rUylݜVȱʯ$EJ5s3NxrmӢY edq(,ѝcǽ.Wk-3}2NPE ZaS$ M<@X@ F =e(Tu@ Yz\7HLu (5T2kRHI`FGy&1Z*"dx6q~FgΩ^n}vY_'d?EwƏl|cf& |v%¡7""3"UP3yHQ"dL~!1hP1)oxL-ft_׫ժTsq)_BrK'_PYɯZ Q 6*NC7AP}*cL=_K~˫h FG .>H(PM ZΪy}C\obݙK[;gtfFJɝWN|Y Ty}F01? QD,+W UtS[OU^*"o$_* d -gZJL*.X:(*=ׁk0|>dBPdZ}r:qk_,+:P^bs&\g X0dx6K|Vr2l& J?BPL,b\Q9<Aã*GۂC- b馪YGkT"$GxVF*+RFab9xQ,mA `Z!5T WhbE禘dz[IJvbẼ,Uf"'fX3sk";6xhQk[|FzQ SB3 `eEP^LDt\ AbE 0>8*b?9 _5dn8d ȓ|}evi;=Qcq&=G3|1?6L%fn\7<:ʒ= w>@hq+tLy9Hb*\5ϢIf,)"th7UoṢ3_w.{6;mUO'dOU_Ы8A*^пJ%/26 q|+ɕ D<ѽ}둍oEP> Q@kkaL[1w-#qiA ֕_63X[+FM%T (lB?  ݎ4B}ۿrq)^\#ZL/Kojq @aF @@ (,XE kY  r =  fiܙK`V Q ;! *.D @ad  6*"D @ad  6*"D @ad  @aj7D."2~ʉwk)5Oz@d 8[+lQcXe ͠g ar@„Pcl|,eh;H۰#4D @aF  #(l @&@1B46ndH 1 +@@b! KL`B q@@ PD! B  %D@a4   (l,@!MҀ@,PBHWX1m @@,2a|@@ c@"="|A#Cxn>C? W S2 \ݧ~3k>n g8N6M}ŊU70aQSjpgypίFP3X5/udq)`J&M@őV9Cd 5%ȍH߷o)eh+9{On<|oL$/f&H j{U}o݌5v€ CBv#@6,{U[xbBv3'9_`xNb] :yٲϽj{￿zU֭[G`v @Gkuԋ[N;PЏg(u?/8r)\v7g:l?(@} ^.S'{W㱗j>kTdž:[˯?¶CR6lv5JH-\EaIDYO&{^_m|nf[F\þ<^ZR\Aa.Wh:v磙  `%U]|9+Ʒsþx74rH2yߑgydy֮=q}+aޣcX8kDPع!&D'`TX% 1@@ 1Pĸ!D'1@@ 1qaj7$VR@+%I rHN=/JAP9_P6ی@:KA0*eQd   `(Ud/ıc/eS&ߣWĝca.AQK2~1~J.wL=;gG+ePs PVaw~RX_^'R؄'V(RehFc&)2-  `6ذfE~  b,UrOaҊ{Vg^*UV|S9>dusL=O}PއO8Aws=p7]cjS [Xv~a+~`_#_>\*-Ƴ7,f^KҴьMޱZGM5+}rK 2(yZB&"6~ŖLc#Š+nq9â@:3*l,I?2c^;S+f)競C.T]ge,ʡذ @nWUVofKRў 9f 8( ll&1\EpkQ//_4_Ypet1WDW/ߢ ͅTjݳu)D'Xqդ輡l5엢M_x0Æʚ 1ԣ_k|Ω7fIg::e3kf\/-}3f sځC_r+*R~D %exN X/ NwP1cYUb̼ݺ*\6s{z[mr%2z+,] v#5jhny_ƣyj>8zGTR1*'LgxvHgY9: 8d>~fK2u yc\آPZ=B>gkڶWT,n 4 -EƏ^Wp(..91ve4ӶC%]U8ڷ5CLWc@;EcQ֍CfC-Y{|!Kw}v_1T/rcc=:-2]5Qpx+ ~ػXFXдk$o ;e~}tOFnʩ) ȟuj7ڵ_N 6ve6zŬ_˴$(v= H$Y1fҴcl6 q jwiӴZI(,xq}Ҿy5i~k+L9w6E8>=yaXM2ISfJ]Z1aknKWT!/9|!dŘI.5c7o7 6ʐ쎹W[lvӴiZ8Va^~ еKǍ_T'݊vaBs@6c[hdl &@I DRC6,pS @aMʼn@@@  VZE2[Qw~ L ֽfZq(,͇M ˮv[ɑ01ف}k>RN<9wš> 3U5/Wam.C L ͽ/(lG L ͽ/QYPd(lfS;2NZ#W>4n~2jSVUFnu5SZb`R¸X tz}4ITh 9Y ,ss2~CtPkXu9Lgn ӿ&Wc6~#1n\jRbr6_P;f BMfVTϨ$~B^_L~ORi7J0DJ)/Q2AUT25J#t^zP/N[uO4 Z$K!. [9n8A5+57^c+~U[phlɅZp_h~]w1R9ɱap9,=NZ-5H,`զ$-҃yEa|1C<@,Xp5;凋B(;K4}ZW\CvOb!ɲDzk†Wz b 'Rx3;GBT(On#՚5↳`$h M$[E Mvgf7ql G7_̳b@ET%XqnǓFE1]|x;++F,5@>c<Ĉ H^ qĠ;0gU $VP[^ +Ɋ  [{d :t|pQ⧣b2~H+X0dZSs ~3&CEvKkknVˍ ~OրH֦ }f  8(l@"bUVE  \@ t;J &\8{fܘ˚ZGV֑ە q3mӇby3 H9(lʑ@!͙FCARNV݂$U}Jzh܏&h/U 9`0ңSWX>JXx<c M3{_x $)WXھy˃Ddf8Te*"6ʐjVarʮH`٠:a H}ͭ5 BqՑo.eV52UX_k)-X@_&x-_/0v; W߮ᄇ#L 7TWlrl&r2@jHB#k[DXmn3`PF[I3qL=x};g:"?ŠUk$e$i[VafثBj钨 m_h;+Fz}Ui?MlP=P:i nr{Yf |1mzEK@b2|;*l[MU^M9{m{ }eFL&Gl UQmh) lYgN\6;fm :P[Fުaqq㾹q$CTH@EC~vJHۉ4iF YaMe}!3@+0`rY>u2uba8  @+,wQ0LWL] |M҃UVE K@[\5-vFߧq5lacq(l2{͢w@@ kĮdM! iG v] d (lt% vi%@ +B0lX"; @a1@@*PX"_b UVE  @a"|A@ 1 ݼPX"CP @a1@@*PX"_b UVE  @a"|A@ 1 yCa- ,A@@b UVE  @a"|A@ 1 VZEc@& a)@a&@@@#X@a"|A@ 1 VZEc@",(,NV BaM' A@@%P@a"|A@ 1 VZEc@",b[o|XذEN  O@b}>*]ypc'|>  ~W|Da!  @",(,XE kY  \ae@@,4VfD>    `(Ud/g {w2'=-kA@@ a"XzSؖe A@@Xź  $.f$L$aB,KÚ?x 0"@@*X*x=6-FnguE*K"${~Qf7ktmfPXrZm6vqϞCفg1W|Ps ʵBacHJq5{q7ae$]ޅ [`Z{ ?l~KQ&@c\(Ȏz;f𲍻]k 1Y+| I%*e,8<_؋e R\kpK 6Ӿo;B\^ xmNgr+rCa3d&,[& vG\k N: hk6x[djHTM'HQ'좉^(lGт&@a?,S\I9nZ8䦛P0FK(TT[=~$nJUU o(j̟Dаx̠?w4:{nZ|@< jXrS;,U\jF5ۘ`NT,IuAa"BO'g_={c=VoPao3泷'hV/Hm+ k-)"ԺKZ9Ӆt6Z#Pc'q6:#+$aߧ#F_͟.R^ذ vڋ*w}O9g~j|`ͣ俊3Oλfӯ/ %ɧWO u` SltԚ $=$kgzcu>F(? Wt։?>5♟;\?}ƞ׮`0a kj7]$g6Q|J3sp_J}p΄g~(ggTG8[ - [K֚:wq/iJ3hmwK3?>\k/ֲz!N@%ْIfq&MʌC!RR^eK `K rgAp:蠆} ^eK`wy@$-3}/K=$)x= "(W çOmkm2?ڋ=2hYH7ק8twvői78NkU~3k>]>Q $g]ٴOm.|| v[nɵBa3K*gڒ6+Yú.i/9jx=N&AK [?)|9^(c Qa1#@",(,XE kY  :t/x >d-1v?=%{|na'?RE"RN e7DP4W//(e#&"- Dtމ)+(#<YWqTb=W"7jjNJȦz*'u L@A Dm(M٠Ig ';8ni$-C3[%%&nʠ`^ 5;KIENDB`n3er.`JPNG  IHDR&?w|sRGB pHYs+3XIDATx^} |TŽ(o$h%V IA*XkB`*b/޿U j %U0Y+W^@!!Ǿ73={v>~!=;}g͜YDnu'A0ўALza&3q8`6N8\d"&B\|H@I ؒS/8wz(? AL$Bi"!B"i;7NjQB[BAiȤѲD$d,V3<\ ~Z݁Nm_5b\mt>?D?۟GHd8Ҝi;SFr^e0tP"oU~ݘM9DjLMNdf|39u=%R<+$VjD+6Yi4K(Ej,&3:?IcS :C%8(t-"4b9P6kzxb"4J# [[+ˬj($7YLj`~r!szzZKW#b^KtQptG x/7t(G6;ij%uya@CQJFohzF-+^ KrTN]FV(ĠbV 2Hmڭ0g.K#ooO`%䤋އTD7X.Ax91 q2,ri&OGf<㮽ieNZJ.7 5SRќl$%^29+L*%0~$jjR9a8^ta2וKf*:Fl8#l֖N=rƎ>R]i%K/]һ;1eZ\KEIEDQ̌uRL:)A,NTԥ23|,6+>Td{+.xљ 5V| _Ή]+˱06{A(uEX>.:-]\[\}ER$TY%:$`h!%(yQ+REhi23(B;<0z"]D*Ͱs/) iVԯz#B~uW|$5 7f6"4TGv:R9}OEsgHVBlh_kX`f:C;%|MQXkM#{f~@mxmtR{zʈyt:,p4:|K7ӝ6Ͱ &d'G.500Df9t6 KQ_}aBGV..XacuM!,]uoJYT4}}stУœWrgV^+. sa鳯-}2KUoQdZ%dفQd߳z.]jnLo>̺ӼH1*7F*;/x5,;x ggg$bq3`t8x`ql-f`EEZ'O>,epa6yL?^yd%:OkpqWOXμDHQ4>1M׳8%GMU5^%2-a{G~l=֧Cf߹#-w-##uCW"@2  ^캜px0˫ޣ;ӽW.: _vV҉m 6q@̦T64IaYy{:CZ;ZHoot' SΑD]* <%b% ;MU_q-2K^#)Ib'}af]R#&$= )`.^ba[PX0=HQ_р~R%8 $$ w^9zz8%.7h돀W%^ 1j(Bv KvJ-cƄ Ϩ7>J 3Ёǭ fH ±2ElnJCB?\کO s9yƯ~;KZEB.\x%"EXAMۆfY*{7fM'鼹x[ςg_:t))>GL0h10Q򷮎%#`]3r㢭ֻdLۯJgmܼ7--a6lNzJ8"-kgLۗIJ}|t\H^:oaKj 61Zui=dA{8_֚s)sr mOu~S.Ŷam}OK?.G#2ݩ૴uӶ@Ȯ̖ގf-ߢb "AJ?>9< UaB&8ߣ;Ax6<|U%a4/{OITBf)n2[P-T@F w/ҩT'l=;D 8?5wd@ת"th`EgMtxHE]Dfohqn"})RRA^5؉ r_ޤoWtFT0{Y ̫8DӨ"aa%NjWg~'{pxщux[ J%WN-aw6Nh)ăTtHS ,[JTԢ˸=B_Km o.NΤ{Lp0e8X` UX g;?5IՅtΡc*XA3=䥑 p+)x:RBD|J$ySބ*&fhJ;`!fvIm+c*pdKHej YΊ-0[1z8Pݠ!/"{uK*pR͒Z[iӃe]`$# ^zKD !%+1TugB/Y3,#W[N@)20h ͔xm*IRA&WMKG:珕/Mx.XO(al pfk2nJnA|&aIVUoOS(\krA,g_O3޵K}aONlQ+D`>ԋ\=O~JO !)*6Hh#S$UU9*mTFHrT8 0O9(X)Wp4?ӂ(3+*tLK6קz=#p@WTX/ݤbjԻ#F. V~T8J݆G $UE*u5H(+ucT-hX4uTwSXaiSd(Oa Nd>k( d1DJ*}=tu_/SJ .,l!հc‰UցbI5)lj ?mi!xPBl&z℘X⅟(]sț;t/43#7n_zPiPTQQA*>'EU H#-i%RA^x̙#wJ_rn7gVW9! }fRnOĉF T^{3oϙ_]=[=ܷ鯗Zo? =Cj?GN)gμ-0^YHOX*tgBsSP&pR yQ^*edۑoUޝC-"ׁc( BRUV[ʋ]FUTL\Z`7y8pPYqc_Ze+JJVEI {2QB~'Pm_O&P@/C!U9yba13e>pێ%Www#=qꫯhy,gƭ> a3ИhdZrݱ%+cm\ ,.#iCxF7I4 [&4YUm)]2^HF)$}ֽDnR`yzGM*YE@SƯ4[2__H@vۡ _?iY`gRO=oPHy!rm` ˆ$P/_*gJI⒑bBF+ 菎ġdC:k=|'^֡,o`u$Rq鲷tP 6 ٫o]靀B-,p"LO0H 祓~^QU'7q %4,^z[J'oScs ƟzXnTusi-=94[*oIh!bKhft8="*HTA_B CPvs'F?F)[`c7G`vUUHrAT Xf*`pI#~p`=;,5Otdn)&XbɶQ!,O!}sr[VD gޭ#Qj8ќ=Hq^9lCAVd7j.%ϩ E\5,N(ը7.]P3nAɆ}Ј+//{@_/Uwxr.P?$DN%_w W"oYixJ=Zh$^/5!r>Ԉ3+wh;Eu(Jˎj?[vEDREB~7^4>Q j EHqN~T? *LiT)]|4Hh`2S ]DEJ*m_p]LS%}FIk1GIU $U$UW1*kBR߽҅_:i<,u1Q,mT^C յoEËT Tr#GQAQC"IMjD%:Q!k\@"!D)p/j ˚ *j ˚ *j ˚ *j ˚}Kb!8G^Rwh⼢x* K ⼂x* K ⼂x* K ⼂x@O9,qr!\_>Q?!B| =yҬNIKմRW̙36lN'WUQ/_@ȅ!R4/)n M@#/!BTWJj>!F/F ?w|^u<Gegt"M/8_}o]w FSP>-.!Bd_ܔ n.Mw@ooPB4Hչ3IK̝RoHi2X`(\R.Hwݦ]%CΝm{?U< 蔟NotOzRrg2VW} eHNpO&={vnC4?Z/Z\_kc&jبpKi &`}]||*str8F/nX}C2U5sz/``Ӟ-_u]|f mo\] aI1vs2h0/h0-ѱcj==ɟEA2j7u~:":GWb9ʈFB@qO{;Ff^\cZ{e@s设5KûmgAc*7,KUK|A@=I-Oů%|>E<}?uu-/&@>EU7n.^pFE"C`[`:o[kE>'r֗;wt>عO󹹥>lv~n)%*aᳳGߐTØ&T0("QE&0M|MZD$WvWӁ@QH?w T_uXC@ǯvI~`Έ@#T8Q﵅K8T WeXxGI5K8T WeXxG ٿgqkCi?z {J]a)5ُ7PAbVh ^7.j{*&B|%RqRk-‡J*SE`09" "`0H*Eq "`0H*݀ȭŏl8N@,DC#~Vqk匡5T(ϼɋ81-^pT1Rؽ`c;HU چ _׶w-wgTaqdǞ8c:bPBcoR]:1p7Jg:#﷋ɂw >ӵge 2ݱO뾏W#ec\PCKbyU򁖱ڷ\rm˟W#gT*{qs;$gYw#{c>B}2Ĝ-nYEdC`j`2{f!'֠,|k$S ϻs[0 +JLHYŴS?V {̐UWi J> ?$Y"_YU~d窟% w|?hNUp2yFISuݚ! ԎUK~85$>ԱOXxD bМՒ?F]%M6zST 97Y9W3v)Qz>𾪏hp>3G7jzd`՚o 5 jB?p@ $U7##xbyn^}/ػxJ?x%9g]~"AF$&^;.|2(YJ,fG")C%&pFfUl]0MCϐa7VƐ1 C/I&x_~d YB?yQw Kw9Wv,f>7}hտ$ko>92,hF~}nT茜JU"ԪxT;A M ޥ SlIi) ,&FVH 🖾5zL^`6H毡Q2eY~ڦ/@'0_X՗W<^Oz Üo)  K&EO[!V߯8٭r1wtؽU!!kKj"N*DC/B 19"*DL%ti2K0T@R (CB8M 撚=MCB UJgǺ;FP#BZ JG&wpb#npL0$8DIm0$ 0N[W9?T=K\$T[w Xr O'D P%jĤҳTN)rI=SRt?\ci ˢrrcH<4ڼŋA3I6߲{^T!.#3i\-%`P(!?SY67`{*-.4,B褂 Ey4I閄k/.K9ʍW[&$1'y6ƵMn\Kb]qt}!YiN:זڂriiz,R؊IwKJ %|S&(*exSUXq3<MaIV\4UPψr;u?{L;Z9THNqAYUDyO cº15Vܗ*%b$6tRIrK)- +Nl8[%w%fTUj˜_@('V Q'tRD+IEZ0VLGWLJI%U3VQWP~m rFTGpcUjYUyiQU/ɢO*J8yT1<%l a$|޳pTv(; f*jIU`җXv`ZIJSϵ|yB:=w\}?cuj?i)NQ{i yCݿZ&*KU LG ¬ լ[i#–`@Q"~4[˧ Rſ/D G?%RiJoO }WBkiJo-#"*ik/T<更 jQBI^ȧdiҶ=b!P!Rq|RlTLIՐk(^I.Tz,s*}{=b3&86vr>n)qX9 yA&`lH*=`.^.U#$wLX.xQvdݨ>R^V_-lȊNJ"PO=IH|:hRt#7ꘪ{%=a_HUSpF`Ϋ$">bIS+U-%G yUSPO=IX *,㖒S&p t5NqW U>}TpuRpAQ=n)xq1~ -*-,+.8 "jjG,㖂'1c3tN"A4`8[yd#(-k>3~ IalWQ c rN*~t ڸ~vuNN8vNo)=nLaYc*X? ux1NRoE S,ywO*g§U~Q-PWHZc[3 Tki7 x%{'pdBy;ˢTw)/e~Uzd ) T QkR"_RRS**ƖPI.Oh Yu.y:QMߘOQrT'> Jؽ=* Yq"dlB'UT[J8F:-T! }rT҇wSͬ1V<:چƔ,,)*l>I'Bqɜ6|DU4 OƩ@4|8 ?G쿅q6M A1-!O*C{!+,k(Ʉq*JsRR0qTef #ǚWaf &3MD#n߽:0gd3_Icxd{LD}wZHb@-U")Hc`D VH2T*4OTPKRF~a :Lh#@g2zfx8iJ؈RPgB UJC`2=P"@0QT# mI7$GI 7$GI 7$GI 7$GI 70O5 ( H>R%_F+O>TWQ;#j $U)j ڹ0CI|ut,-*M^T[Y;!j'1EIuHvM^T[Y;!j'1EIuME7T!b < QX#5_#J*Fc*ֈc~I*5HX#%=HbT0PR1 @/)F ! @4@"`0H*Eq "`0ifNC5^|z`(H >ԋ4_$'v_TQe @Rl*5? "!JM-v`@Q"6`@Q"6]Q3+whwXQ @Ӆ1z˔ZOPEDxdR&֋x|Qb "@QKEIclrt@RE_ RQ".(=U崷9]వE/ Kz.`ifYc髞@REyHnmn6LK7׾3 15t=>mǒ-EREio^]s&1rP4<jʺ@2LY멝ƋAt=*2~گ~Մ}amkG YubfyկhxnYs 3SP*TIJ5T$d,H bTA!22GPIw-Oß; Q='Uq7?{S -(>{ C.29z_jbNN.& AqW=ş)faSR⨓(}M@oGbn&xQR!FU/e9Wp˶uN7=~˦GQA,UWovKOG7eի[[dㇿǟ߽Y/\5~Ww=u_$m|"0/෡p%T1r޼azSYE:uTo[oNإ#\_Bz~g#X0$'!<Ƿm;Ro A *J˶RBCo?(|:(@ /[_@N?K~-Cd@|z|Ȉ UpIcRA{ |['%my?yiqV2:_ILg`’{g(|E\i جSF>Q 4g#DՈSduWh-G[O-,["iX SkCaiI՟|`͘k֖~j~jJPXUkH7Bt =qx Τ @"wz1"IENDB`n7虲NÛ,]b"YPNG  IHDROsRGB pHYs+IDATx^ `TLV a ?"$6M0m*V@*-b"TA, JbQ,UX7vlY% $$̼mf̼Y3y>̝}ss#Du$O1 @0(al<}EH@-h$FB,R.$&@(> 4®3uZQMX؆bb".^ӆ~G鳃RaO^xJPsjLx2U?jƷ)Yk]{ַλ|?2L:9|Eri)Ըsg=] qKiB~2S3zҋ1RprUϛ=q_&63._4@K|mQ?ɯ"#7XڈEö fb+rJuC:4~Z:{ׇvn]ٖ|_r$ >pv&`Y drǔH`1jN>9;<ǀ*1z$b~h}̃Ř-,SPJL/N__E5ҁכOmbBAp^j!:-A뻳8+i̩/ԕVCn,ʄyi W|Acۧkk"B߶'vawɑkN}lgCj7HzH:Q TwOg1ݛ&U2M s"!j V# zk>;؟.fc|5C["ERr;L0ǞD(ُ?:> > 72$*җ]!GH& .Kkd'nsuwnb&bݤQd`s{4PNqۘ#ttQ:x RFB~I5AJ:Z Iv":7 tORRHe8qJZ_gLc1ig!}I 6CFb2IW`;vm0"f-w>S~;CӦb:ocsXS#)mU ٘*VGIDH h'*'D(VpoCn]O{ 6x` aȥd:XKZL@"`Af8pТږ5Vx; KCMgE7I|f:W΃pX~j#^_9"b6dW|FWIrvttnEZ5#-H\4\5-9(c=x>OE0W?cR7P>!7=pm 9z9 7g%^/|# C-g7&9@jՐ՟3%E HYQ]:‘3i<_w4\"OC"t'QusPP6@K@k,{rPۨֆf͍q$-V-]=}PCBi>zY0 1JN-c?S 7$ W;C|$< K;iM#Y^r CVt7[/[GH w Xb'uoRTfŁ -sllhl0U]BQj^4&.U/:1nb UypD{Ziw',$, [/{P"&!u4FX?)-5tzrUuueuKU˕WkV]i1U6V]5W mA ᭥ʷ9p`?}QƤhֱQtDԃI@H@?mhbڃU?UH#l3D< fQb6bM|(:tv[=(v0oGG2[VtFa ]TtT `/]+-pxb;` ݇E٧Jf zZ 3v|]c<ҷysφ`$Ju>͕l//V6e38D&޵npz9 څ}eԣ#+ A3ٲ\P ZM]P# p9vq+B/#gna萦X@?pe=;ټ A@=1! ZW5`CXB(2a h{Ԯ6G2_K/±9&ooc0#ƭxr4?vuF]k2 7q)I}_:L @H '<#|TsGH $A $@@.[@ ڹf1@H څz b@H5T;׌0@H :TPoA?@H &j@ PTMPHج,Ti @H bH $<P킧-&H $/At6J 'F+G/;d5xEI`Yutd_җCWp'5 L\j7o=?G!O`-v!ߊ pv'O|ᦱ٣.!#73Y9|L_iR{ĢM:k<8R;:^# 0?]2_jц=ꊾs7u]?_m5'~j@fH}7~zq<6u}L|x] Uo^ϓſH _ՆV V-c༦=$$'5e\떱pBxRbOڹm|1 l珗xLh.J^p¿_{닧Rsz D:ogOX E'1 P}Fx^<(g$K\r[RRR0FYXpl NQ_?H*tz8ŗ9Et\M|Rz`|Gv͚DV,yt<3fpM7tQќ?1oTv nMv?.m|vcٷ%REbmjC²gh&v~!Gu 5y?~Y2ѿpL~[<nO=c#nIw-bЗ~sw|LFJpUp JMmnKtvPw=}8Y|/^]Q]ߺf+xϵdMI 9#N|#7dዳ>8 )e1-Z^ݔ7Ϗ~ỉ_,}]6'G;<_{z57(PtώDGY2jGgN}h(><ydy!z4v;x[{Ų=`P.tl5=xvߤs% aSXwlC`f*RHڷO.uA}{.]LJno=;sR,ҕyB1U%D?965SqQ #_.H#|u_A.s]8+!ځОyD8{Aɬ#?O=2׽me@c|)_c0hʀ 2xvd=fc%]trNƁAdC0GR`o:隷 99HU[֮捷|GƩ"žڷ3âm`JN:I4xzvM#?ˣe&8^ިcu+ϟ/0_ET$TldsǻR/?q<=5oxZ=5c_pS;։شy6}>Hzw\?17dSYl΁?lnZ Zon{GkyXar@g{_R@LXe;K=//t:ºl&XM^o əoGO1 8n^ T>}u(ET ۀ:;к(s;ܻ /7׸I*|w׭Kuؗm&MNHw{yyOa}~!ڂ^OQ'e/t7.%< BJj'J`ЪǐkRhmM mѴ U ->2WvЏL:MY) hXu,v~U;l&j״w T4mcbMD*pK#ڗ&؄fs~w@SRH Ⱦ]Κ 1(cjƄ9~z5Z`Uh6 }]Ӕ]pJ ̦#w xzC,V6_gT]ҳKtP%Q횶}$i`H]v}Zӧ|w$V::w3H @pd$ ?'Ft$:jh>L#>B!GŐk2=fCqp$l1g$FK%:!$pL${ѠV§Y}ӫtRg H $C~R\h@HwYiޤcwE82gاhwRgF?^ kGut<%[im2pj;Ȃo z`N.(w/YZϷwdë?Id3vѤ&=@ּ#MW@恫'e͓_Æn[^mT|vޛTg깸hb4E3sBH O_*]/Nܻ07σ3r"V,ُqcǾy~^BJ۞KIHTSs7U˧)O2q͓岶ޛZ2t^o Ocb`ufcu'1Ou^SLU]B.*vOz׷ :ypBe-RE ԯݪ/[c;΢~gT4U*vYhEv'Ekܢ@E#cS ((F5M3%N05H\No*;밻'`*(=~:$} Eݾw)jsZ|y6҂C:СNy8%,=6ox?eo4Kw7&)_c8C,諦#0@H k& t l4t ]9 g .`=ozx ϛ@; g+ >Dݪ,9+@M{ ylcE=mv"o@!M@X9$]j+NϏ@ $@DpC"@L.[@vE`VH%ftXrVR1<)pu(M[- 1KBH #]k2g@݅a9ś'E EՁ< }o4L.zO\i,jY#V/5y1¡ViǴ*CsM3.zEGGI+rc(E%h!F#NF7HB "xa^f+$0% $u FMH#=MfI gvQUƍ6tG|@W04%N LΪכCfR-Ȟ9,&cpvkM/k ͚i@I:yR;V(Z'wYNVЩ/ݗ[I#kR;* ?PnF_r"1U8zNO$IHT ҏ4cLM 11%P.(86y@ (F:^xU3Tjf5R!vf2cj6CS)ĘafLh!dih#Im8͆7뮒KR;^%=L;W̝;jnqMŎ(2KE + &jVMK޶?NK"I(qa2h2p|hhT~fQ{߷@pJ`!-|??C n07ҙ< ԛȕ:RZrxKr`N=H$@H!GcÉ%N-GSU[gTj͇]0f7Sef $RķL4O>aX |O~.,, k:qsY2XT.@78¹ 6RXSɉw4,tqvSt\ gB&h[>aqT/㘾/mf+*yu ԶL5u*t!FCYg5HC={рO H 5Щ98MzuȀe.Ǿ9yjOSGðC*r`8O=<ng-z'_,U/!WՒ+=_ l\'2+Zv3=F¢YYgfkMJHRJCș{fb9|fAR1_&/M&]֬ddPm15 Vpe#F(Pk0n Tz1<}HL&:`i}K KcZRn=13b{Ħ-&N1c:vDݦ}t6/?wR'3K?㴊 sǷ\Tw !wn: ttˮ*X,e3ɜ-~00/W5P8" "YT*Һ\ULʞ /?mS+LeQfk~$\l> m9l{0- 4=@E e.1W_2bcaG!: +c``v (bqQ4_sLEԮ.܋,V(Svor{.!yS;HI(B'b:bX<ssl,&h&u\?{kDLd~XwCoSy9`H؞YT[jzA0I?^lM憆c~﫟 }3+ Nslޮ)Mj=Ƿ@0 % ,e f$G -<^<#6YdPOxr1˕J@5喚Rs\Sm\d9%N FSKɱ>ÝLOt!@ؚưh*xЧkbvmٙ Njx^׬p}'f.sNus# <"A:g/'WW Xlo=awCқq~Fy;OT;OɅ_:@RuiI$maAw?m WQ%T ~tegRgjyDt;8}GWfC0 8S.g-V|LbgP>:B tOawu o+#KL!{{nu Aa9u3 朹q@𜲘S_{bֹ͝v- 3.zۭP_g99ʙ)JQ=<# `A.=OlSq>\B=؆=ࠛy4ϮNNn6'~-?Tv;wuc3L9lEs4BV4p+*9Nhɝ>QVtTwȚfe3EH 7n8q 얃tMY;Qӏ}Tc3``#Bn$oWoa$ftk,5Gq0 WR̐ЊlNAb颍~LQ(Kb0q 4 =vtwd$ cy9 ,~ftEb mI.X^Myү\2Ĝ|{ܱg~O’QUDK~Xr:ZE?lɟ͎W.d~e_\viA*ahLX;MNX)L7Xin!ލ6{WL腚{5 8Fw3H. ȆYKy9ͫ ' Ui!WaPyj˥n O6|{|u4gf[=i5kHkee̝e0IŸz~Dw˂Ťh<ߞЮgvgIS~ycGf&nseK:'j+hkb.^4#o]h! 's>&:m:G646?4rJHttpɤ]2'EBRq&(TF6}=`309a]xI,]r'KݙGkK7`&aØ FXGC NiIS*:Zû}_ma+&O< -a~ ~iH[0Mj7JgO0gݢ{BaM@TA30q~mI,N <O'iPFXYO ,K>Có׆? tAd0;=jmz0!N=b:p`+ݶF]R]m(l(%m&>2쏦'?w;r $kg.nս\YEIv"Xjm?_L>ʵ&4|J9+6CFb2IW~/N< ^gFuK:y*1v t\|c@'m8m\,임1F ZĒx''hG0 ̄ 1LfHoyELhaaճMx"󮫫!w׮n: 7ՂvH+ 'i]gSf̐o/ [RϊQLriOq^lCkb̲ $O [2̄ V9~P^P{t(,HW;Ys}}Ocjg`}s15tzL*%E)rK߮q1j[= 59bnˆV?oDǎ%#ױϿOݮVTu.YIg$d+%:*U*i?έHf$ᏱEyTЂ=Fr _|i)|WHﱺk`}TqWo1?,8;Xnz6_ yڹ5o8lͧuݙ'u;HekFέp)>YM;mh fRy;UxY X0暅zf.2_dul66Q}TwR0x֔ Oʇϖk:U\T}Re˕UՕ++TUVU_PYcmj@ Ճ},_cYN;\^#Ϊ*+(>'ڊG_UlSbj U@_l/Om|()^͚k/H!};CM0'=`N8};p-O 6m*?B5=Ck~XwRCq5]wԮgLraZb+'L~6=LDh8Yӭ:Oַ*[Ҳ{Iqi% Qjˆ>_D3n䮿sM>Ӗizf\ITBD H8Bݲ_PWonh4$fI?}}aҨoG~W3֏:8eqG'lV;Q;()drܚΝgB`#뭵ǻi.N"tZġդ.4ݸ % St)wU@#7mH*T7Y.z`XM UQ|\`g* u櫰TGO|uqNlOh %] >! r׌/!px ToC#k WA9ue00/sladOG^gveo/l^d|Y;T@Kdřl\([tYU*`']Cu5#[(W>[z,.iFք[sDu;W"MA\%N /lec5f<&P/{&"@;("MX}D~d ݐo !n&"@49R) 7B[|{ϝTX|꒴I_0 ̭w >4B_!4.Ǔ$Wtj.a$oHupiz۹7olsلٺ#ގ}FD17:2kUMgS[- 4_ mOW)sh0EJ|B v~2)rV7v~1[$J _-̑@H {#~f:h& $l1%$bB$nL7}p7 $|A1$n|MfB~m:rk$H= HΉOs{]nR#>΅ڵiw_mڳc}<*Wn=09@HK^dz#$@@ F*"$tH <#$+BJx9j~jѤ~T:OFcLɞ4H ~:wl6u!xTzV9Y4= aJ_jaVn#Ll5?I}LEMYTjZH1q*Μ1P<ćɐp@vQQHW\w֬y-A(읛ֶYJtif!+A>e6i@ ƣH\͓D(f,UrE ™̜-KhTNy &GH $б]̛vTr/6< ՚̜4W@Hun^o>MT^~^{²ک6;z؞E>MHqj6{j~0u2Yy6Oر =0pD$`G;&%I.ёL?%mq;asͿ>ܞs͇/OsוtC[ƢTR&bR)rX.nbz?b5p@?߂.'_t67+}$2iPyg3E{) G^Ἕ1Dp-pq3u Fc-:r &.ĩp!dO΀ܟqV~yySOB67#%yv: ED/8據 xj^T;36oXWT鹉ۂVgqE2d3mVpOm5&ߺ HH  xQyvD) %Ei'üL}W!1h~uQ;y 5z-X}"em>y>P;R?X;}sǐg\>{~׭pNLGy\:"լc{ UwNY9>qtLsvo񹉓ft`?Qz%'Ջ%OG:E%P0ayCxn""'j7eZ+-MyT^r\%yMGĞ1]wo ÙY[?P6?q, '?Yߪd.z+o^1PwurwbiqM~jzjwAJ\^hn=qI#5YybLB4 .rz_/`V.$, j$qҨi%#1=vmw~TK;-umym+6LM7<+ [''R*BG%n+4 #@H $ vSֶ%y* 啢K~BNl;X)B=*6}Pd|7sd"yal*d@:Y>0+ aRGWQjVʼf>BH 2zWZkW}ҩev~' pWycVD[RIn.m'Aϔx.U0PV'5QŷM q6wykg'S,@5/%Չwnyێ%my +p ;'Fk[$ӻB"5'<jLY6Fp5`|_lN9",uv +L$S2Raɀ,4SYiL`hfr-KtWh0@H@'juKM>9yBGxfOHZ>b{&H66FI,ϋk&*}_Ώx;V\V^)U̵R1vLӟ2©Y|@!N@B?>j-|⏏@b6H  Am[kKm]bH $O>g<[ܵ?mb $@ߵzh(S0xm3(lJ $S&GH $"j@H ?]k2.ID17bwGRH dcM.kzBڳc}<*Wn=~:~:ӹc/ar$~g~J&~Ѥdo H֯>{.kuFH  t;дBbhS:tX̧^hX@Isv|n=dz4F29 $DAH %`01LˑLT;jwe?g x $|MKss$h(/;*6d0ì"_|U+|hUSIJNZǯ0s$@jgPAߚu XGat+m+Z[%G f2(yR/CNEs \Gr'|kĊ)@H D&m3Ɂ{W4+@vv}8;C89{jV0Q8@ff\Pqm;i?XY"$B;jd9C$GfBR"_k A<cf G )@5Smuӑ1[N@q`aH $1#n AUsf>:כ*ç`L_,IdV#Dqq8{o"ybRp)s , , $W;:bf%k7Xh}AB*8/J\Tw d2;O,.rܶu\?GH DՎs&Vn2--:7 7]Lo12JeTc+'M*ᒥ\ Yfn3ꧦl@CS-',f nBu8Qlqq(erUM%!cXYeuabϧCV $6wԎn璴L\H\zOgsQQmuMHh?qR (z@w LXlA9T6IYsI[:)sB= !$J|f͕ޗ4nfk#=;$eg-feWJN*~o$T4+?кJ:~z15sH<ŽTa~$V{?9=S[܍B@YSFcL~0[$' mq?wΝLl$PqRF#@.9**js!WSvfڃ@خ?j Dw 0V &nj.@!I?4H $•_IGG]1٭w LQFH 4/dR;y !WauùO=f#P;-\`~y>fPѭ>#):K @'ݰ{opY8zx:ٱJyݜ԰7 DH tڅN[aM?3?V鲾SźYs >w*`.E祹Rpފu W;h=2inG#\AiYNjEy dUBH#n@6Փ}@E fe z{S4o(c.JIWY`%SBUtg&Jyhтߜ=o*~r\؟q`+f./旗?d,ƞ'TfO#C>j8^AV|N/_DH ċ{]vEs{C}\nlg'gOOB^Qbx}ґ4M-HU}Yi6<(= hKLf>л [3Q^v}; eew0[S1_CȀ* ۇ,}PiD.; rXbH ;jGU'oj&pmQ$6xd^srչ@3srҟ)x?PI~Yi6}sۇ~1>L}HI>8A} T.Qz b9 $CLjJOc Qڐm۞wCgN|!R&Z|Nt*='>HpJV-HjeݦMw^}[^xƁgV8'o$mjֱbYAm‰ J7V4yVh` wNe뎹~DqB6ٴ0C~تWփrĎF&ϡcy 6Sϟl3٦?.stDr$A3+%1;0WߏӬSȌ{^RsfL2Cc N6ƥv!Oay8jmxڇӉ:蹠jgܨy;?_A=p+ڋ\zr/lg0^u6(wƝjd?.,ӺBEClEqx[WbeY%:pK3+)psJhRqռ9@ML@^*9Ѥ>\M.;Y Ose=,y٬Ѽ9g-(;P 7]\֘Zzΐ G݁ʰyN3+fmC@4m̪v $:ɔ e_u] J3 7Oʜ4z!x7HO-ߴEnVT!2 @QU^όRC"3x7YX*-0?r_&/(_*톀bCg;m bKF $P;څePZ 6igoF86]K<j2C0 ;Na?PMIw* 2>kB7E 3@H 3OVܞ{NUXdXR/Zfi_|/x b 6@H #Hfن/v @E^..!Os4H  t"Fw|EMa)R:tX̧^0\$O. l\ ҈Z a`4H DX.tivrN )Ej/xv/]jww<0xNd7  P!KWW6_m!WaPyT;^@ @ծtзy@MLծGH @ d, $]7@ mȵ{m4v,:,)⹱smTZρ-ն՝<@uAJ*V,y1V%:fϷYݔiD渒KZmxdJ=6em#inxk[Q)L3n™;y$~U\jHrRbܵ$oWИE혾4BH .]v [ڰmND*Ղ~@Mh%PSܫS֦M.rTX`OBڒ%#1Qa4)@0[̦z@1aѱ&!*&>. WrALïp=׿n|4)w[}4YK)fC%?&cwΗ68r/ k^$JrZJtW>uۑmΛ-IaRbT\7jWW[k4m:+@DI۵N3n:Ej,, ״4biN T']NIE_4c7kS'f0%MN$/Nߩ63V5 ͧ@j1+4p34L;`| 2;Trv-=:MۑokOヂ^\5 ɾ6.=-؞d|?9 |9̭yY5S*]y`jj8iw~@E\B>}]G^8!mwleF,ce7>m?Ъа8: [CjFLO׭,'orMK8zvm;eL;EG@3\Xp: t0c1 sQ>7R0%n.A8̑G/EOTSkw4Y#gF^GqNc3roWs/B] fk+ש7%dƩF°4\~[V̇Dz~2~ Ls]zZ( 1%H3Z&x"BWuR_-[O.u {܋:з#d2O>j#cIqY>'yk>psAu0.:j! X/uSJ{k6N6/.]eE1l;ݝZ V4,Gl1O]+-ǭ.LN*FscRac#Cܢa6 p0.%.Z\b5~s`ź#ގ?|1+_iizfɳ6IE\Yor5nU'~_ h?ЪbOWSM7|AoJᛜ]uz .荾sdj|/zt }pMUxuP7>*]:i7|8}Md܊{F>TU@G,vK\^rq36?fl{_!n8ZFQoUwsy5cI)>I֛ XavVnx麏n=,kG^[y.)1W~׎fo{>Fȵxה7vSw bi,8[(PH6&ӷ,`UL)!ץkrG|tg. eUAskTc)lk[lb=ldz8av`]Bj"r_/BNռUsN֍oiGyKĒU uiB.j%,Gt?iA\J)ރS;_tХ/_0(zje;K$ye(39᧦ϖLgGyp١au[Ӧ^ǵ]*}4L0Eo!1^$?[Qw˗ +hstdzfpVY`Xm/{JVG8|6\NZةK1J/AT%Ēwv(-;yHq00K8kjѺ7LۺX)Вe~⦏}y N=\r[Q7<Lh<(]a45jv٣ދ|4UtM-b 5M5vydFJoY~؄~Dö֨6lJޞC M{9}YұPBz~Luu]2WbZ2s>JM+tkBJM}clԇ=+[ML؍d `RvL/.hqMCm-CLL01p~uiKĕQ%vi~Th77mKuGzy'ExS=!Wa/9˒uVãJ*)jaSIyƭhu*9ԇg[^ROɦ[\ #LX"E=_vM}k\W%[mS3G\9xJ0E8%"޲fuV}{'O*FNtOwd6({:rZf%f 7\:ѥd?+Wז>ګ{Mߚ4.0x_kgz\Ci_ο."uӖm dȴC4!ԥ^ۻMaȫ>L_4l ՟ӡI}i# y9':B g*{'8tIBH ؠ& Ag'uoI[1E-'o/Qi.uvTUD%͓bI" 4mc)Wu{Sxш^ERmQdiws4#*n7X~m_:ٽQ]ƿW?hե?h?pM3ݢOE0.g$S 0";鿿QJΉ&?키Clf8_Bo ps o S7f"2pCҪ+]`Կ$'P'´E76k64@TqPsȹ m&k|Z$dz3bG2\m=7{{B6iZ3tOW'=V_WY030{4ݣm@5+#22tSJV K-`՚Y(,d4uT]"*U͓qN]m2d-V]EVj>JT'4:cI}]]w]ۺ?i@3 g.|[5&ZsQ03\[ܤcoEvѿܷwŐ;S#2|4)Cj=|v[jGcE$y&dyS3 Eh= {*3]OV 9녫J|Qa?6OYyY ^g'\)OA<1-ϵ}@H v %~ 5EyXt r3?U L;N=hK? mEa-UtRZ<0( #$ xέr7 [Λg |@;& OĽxf +d9o&9@H %j BH LO`HӷқGwhtZ8R_ 5L@yRYFOOG2QCl@H @b c7 W}hGa*+6o` pX3u>,5;Pxpgm$^3 Nɩ"a$ѭ0GP;̿]Cʹ>^ @?WT,  3V渺4usAY%t*K_SYeEYVdQ,TeKLL(j2f5C ެHa%a/k4x%I0a>"ç[հ% @|r|*0qe*N࣪\:L[Q_2Tz!ÕJH K_^㪱^">>>jW[[+C5;hY%`uUF>Sa:Ja>+lͨkO؏ժ(*Z bT)hc|T\@5{$l(~S?vW\QKw=WKDX7_Y0Ho&ZQ\4.BJ;ݰ#~?*㤊$:elT3&1V3ҏ*[;fKekF~4O/KRHqS|@y᎘L\qc}٩RQTRJ5a-FU#-00.$,^eu5vN/Ѣy@]Uu_g߾6UNW/w/i7&M);a|*d@y>~>&>~Ʉ8[&vD LAHEwT%Q\B; oLe:K$N@Zci+1ewhN!'#<7 UAG5& F,G/He$wΓ;Y,li)ݾAXby_2d1kirRflK[RˤkJh5c c l)Y3ݲ%01kJS@ f4d6g,s7D.%kǎ]˳4g'ᚱգY ܵC]`TeDtc8ʜVf9]kƌY[J"*qkK*jAٚc@v@UsZj) bl@0* ZU)F0Nx ǮYKGh&Cy'wcɄi  Gh1d-򁗼8#E,-FCfh: 164X5t% F+rkӲV} 娬53 #aP%ݯ+ ;ti?LI1y" R*0IQDdJDXe˩(ϒq$Zf.1[ ۉ}Y=zc,5,+2?d`L~pNKɃKG_Et?e'!DhyyvsT2Z!׾@ g|ig:aX:wς#}yY,ԋCQ:ι@;X? M9ӾsJʀ^ dEҕ['.>_YEU$IZ4N9=B%!"VܰF^ZiT9#Jj4J-(œ0+o $l~ъQ.WWب1 ., KW&^%vhz2 7{)2&d3~ugϜ={f+lɞE,џӽ\?C^C/sJV8VCצ uhCAx~F Jv}~E8eOɱCQ&4ct ՈZDe+-eզufӗ وiun ))֣'k`)zɾR.8(5b˔]>#&)9ZwoY9b`)Ȓ} K5tǜy,E )q!4[*p- ޺ϣC"xaxi|Ch=h쵘h\vPBI6+*K\0rY.IlIB.I(^װ\x j- oN{EYbu=e 5p-A25 [w Nה;=Er:GcggxNUiܔ賎e k: l_y{_MaTH>#P7=I;ZhS-,XWΪׯT}_}~7P\Le>G?gy ҫ E'ro*2Pu)@2^=to32=ޅ_R%t4\dZF%ӲgT2+QYixǭ(!I%[r-Uy$门y?DJ$\qkXX9!n|H%"TV E(ݳ@}JK'!aؑp_xAOSZZ&/e绲^]+ DWm.~b<-{K{%.%7 H%J%/~'OR0;4JhBU:Π¤;^~*ӏFp݇#SdR"~o~#QbbeHت9{V/efW V^n0K aO]ށp"@Κ z&z$l})މ2b/ OfY;vIxHXr|BNxv2$MNX\D=,t^;5Mkqy&}Dcr~Ҳ/*\toX[*N.ݤe5w!]2C8 f"af8Hx /+K{kvǎP#IYͻy5x:77PTBIix|Y-w,5G_;{DUof./˜羹c8K(h"&l?w\d.k19pĦ-2AQb EH /N0q!^!{lcVe4<+{p*!ݲX^v},Ԝ=]T 3yy ܡعd@Fj򨂼]HsmAsnY =0y ā4H3CJhe95@VցXH $|vQBΆ;p | Mf]zS bg9=[1) &z禳Gzkv07$'[h@ $tgԜ=':éog,ɺw/KRa3_r"E6CZrEs逃Q@H LD3`[$%><)q@.n+@HG|>za6H $|Gw,1'$Vv2X/$\;1'$OV]'t$*S;LH $j#{H "jA"$"]6= vh*@H b *6FlkH H&Yk2AM'mGH D Of6:@y;xal$)B$v!jXY$G0@H pP;xFaتʝYE!XY$#]ź3/o lyѧP`Gqށ\@H $+b#8ܕ0x !3I&/9Rл{/@H JjW==woU!"/Ͽ}F2?ϧHCZ n2YyA0e~:\ $"ծbdwO, &d>׆#Arr!gܖ_PNgs+q.mf4M@H߾JH̩l0Pւ*BVs O9{:>tyV^^GJ@H P`p-T/'l0d=pߎ iJV͒V}8;fl1@H :ݎwO僖pn`'KҫpN^k:ѧu'|h`"v3zVX$n#Q Z:g3n-i߿u+nSu\2IF- $?{_J<>n1g ##$ƒ@@JvԷ gƍ!ݔVBH `G259 )ah@H @],b@H HPZ@H '@WwHhٯmKbX"$ <1[y&OHj/]7-S12@H D,Hf^h8@H EPcH %jM#$"]56XJdFŊ#$"kM&'3/w4 $}2ɌFH DTjo $ H $<@]-Z@|v?_8E 1lU 'DUU xrYE<qg3@Jo/=K{<(If <)g',Y59?.;]ڤb~6dgUò/d1M#k0 $Ԏd-,{42:^aeddYj♳'#3g2,8jbT;_75NM̩'Eo^7Els8H(M%vf,6(DҘzBx>ҧ笐CY\75Zy8'gpqYg2Î@$q"́@⣔yˋӖ8'+!=w4):v>13$_ xvsna~-gnN*4kAnVnY:(;"!Gs9GLylC4G=@ymm p Š, UP9vi: Y#AH #v\<>|͒VIn^9eD>v`IK.":v:!$wj҆. ]S25LGOϼ]Z>kۻpQ&$yR8@j3v,fuvq\rH $Фv aO8J:mj+W*!(9hƕUvq p$pI>'C~_:2AGg 4y`@'3jΓ=漿}@H @k $ŸU;@H $@]x-Z@jtJ|چ*lB$@O:R\=sT@K@v8 vh*@H b ElӣH "jA"$"]6= vh*@H b ElӣH "jA"$"]6= T)t8$ pLзVG[@J.R[FH DTHjm $P"n$@$@F[@J>.cBR_4umTh7@H x0Ԟ'{௕]]-lEc@H 8TSLMGH "@].v8oVM $&T;0@H څH $j@H ?5J%-DN -fS](BP568FCTL|\1J]"?sz U -vv7 ._X,l '&oVhoqԾuFq٦#<~l3~ ctЭPXu%l2j}՜'IT;jGiAuΞ!99}u h"}kk.3 S|sl=Trv-=:MƷC{#U '"qq`إIEӤY8o_ꦌ`fx=2ǢbWnqT;:06^,r40i,/t|0y 308f=ڃ]F;C{û}3Tu|$L`5T 7*uap.n@{C/]0.X$|lms f7t i__OF}eC=ft%yu0ImjLö%GX]0r9.9ծ͒mς#a)lYfLBB ~ZSJ([;j2^!/WWދBY>ƫEhZثlZӪ>1% ]:vAyU~y߃9pӥ|ai{ezkd|8pMU =705el;+t샯za9:=+J&QUg;=.ŋa}ulޛ㒄%&~'@NÖݿ")IƬ"9|arUm8(W|Z}﫪w??R T!}ޘB/}8U?CKɇKڕ1>/}7W79tdu#CZC_J@_αkBn5$p^ٱ91 8$G]{ثAG[4͡<7چ{v=@sͨcT}_ݙ9@Y}[O]H(!}!#.+KJj)RKv #{آW SvImjÁJBZfqS`|}R)=ypLzz\x|J C{K 7lnЩ/_D.\[%yb*;B |i]:Orpi9(Y֫y5m+#s HW~0/KDM!馞XspJUSHT^2rǼ:uw(C{ඐ/x xUUաn IܵmSU{hplݿ9#[ɽz>>-q*'s{8I6ڮ.{kL-kIj%*ڡt~#"з'~œ0mc{+6t?_%: Յqt4H >*O1!~贱 gJEEu*TIͦRNi~TzQzkO#3?pG7<n:FAKsGX]}3C/a8kΙCʚ#ā9pKaޫ.{ T;\ ii劲I߳:Th|:RG^w#_*- as-S:WU+ܙ6=&UDuCgs࣯ǧ Yλ!&oԇ_y1k}]bS^fTﹰW{眹tQj-+ܸU68Œ;B頮ͻVv[M΁9;?_*QU;V@u?y~)}DqAgltE/7U}AC>;O; DN,Ef݄b3)c*'y8{6?zT izNRǿލ$s4t%7<Í7<~ɫGpmk_(oƾ].FrQEoC6LaӇOϰ;H墯g 3?q7 vžRho5l_x&@U},t{Xv ?Mp㰺Mb)7/+>d7/c^H < YO&ʍb`E>.KGQ£wB+/ 4^4C3Ci&{>ѧ A#{Ȁg yG#ho4qԾWk6ѽCs`ynE6E2 bvt^qbcuVn_H&g^ g[ڷOi,o}|w_JƓ뛴nN7 g.|[5&ZsQ 6`J}Q|\.\'z;z4[5|ۏO[@ޮu¸=^|(ctsb@5qҾv~Y#&`00\DTo4F&#\,)qQͫaB E sFH `!;%H $!0$hJvMIFH @ g, $.!ytf/ wT;@H 󤲌DigjPNuX9Fsn܋ S 螁ڵ_p[uП-V~\VƣL#S -n5[-_[(܃,+Ԝ5vx~+0ƝܽR}_+ on|ApVnX?w t'[U[?VC&f?Ew*٭mf E˺*IGGH $ ?WUaG#4IENDB`nFRŷ\XH({;c_PNG  IHDRBdCsRGB pHYs+QIDATx^ `f7p-!$@ "`j &hBjErW_쫆+xTE4 p" rH @ !fwϙlvgtgyϙo9#40[A0єÄ$:⌬L FhY ZK*D6ہiuX$!Ǐ&$' 1N?V%2f{^eBjr$Cb/LR$,`j#jiza.Cw :eW~07qQmK&Oj$;W.Ԙwa5oȅ# gƥ6śZJ(+Y@RnVHDJɛۦz$G041UYP)'^Ré:VqD2.c_X/=j qLkEC8HsA?M<Ɉ @ v XF+bD {nH2L[NdL`41bI6LFo$/ iq!FH4ng`bWLc%vkL7X 8e @Fb&&Z&MfρpGuEZD &l ʹ7PB:.Hh\&j4zV^&V욍, γɉd|I̔ G&$m"'6p$|D VP)*bC%VYS(7G$m &dLѦgtI [rRKt@6ZVWl  1K@@H֭&1`Oܡ Sfy5gL$f&&X}keg/ 첥Mf2GјC$]tĪ^e"aW3@pf;Z13 kYK^;c&hj3x%JRb4/]lӱ3l=Y%?N2vΰ{uNg8K52 b%(ɩTQ_tTTF/,Xvq30#%[g^jL,,ϛ/Xc(Ǘ4H6 >j/;D|_ Fј2fRh1H3SuU׋I:մ\Ӧ&v:'oB˲,,嬓;Hx 1NK$1I89暳BB@7 qBOj"x$@\J_!.;J3-meR4مiR-AN'mդ Q4quHd^-7^W7feM,Tl? 1N4615OMmB߫I,k,5bCHјvè i=k6QXSWwOp.ε]$:N~Dƾ-jdNH$IKQ0fXgLu"k2+G-ɉw"-RmG'];ScF )YS3t5ЄKóg\45>֣dzՖ%),º8qYg%s-T,Z~Nƺ:llekNx'H.Y>Ws](KX25- Sp0WH K*'nƎhNIaLY-*.,4ֱ11K;$|y"'J!R&(xV_[&H I:M6ftqr`lO ?pE~!8`,b"\b3kӰQ}g ѭffiDQf[*5[+kf4蜼M]}tݐ0<0_Ph t}e7B'˨@ 2 *4kc<{4՞ԟ|nTEi%b46wLJedM[4i$Q,,d&M ðKg#:Sƒ#cSx|瑐RX=تLk#}.5pi6?&ݮ9drՆd%T"Ƴ|ט݊0IVf!8uTh x  !;KC4󐤅\I|BM\'%Mw/ه. E!b@Kjim`4']Z(cqOq4V<rti` :JG<ˌ;l [erV;=}u4&g i)nܳ sQ^5[5/<]|ۆgNJj92bU?z&޸t"akve-ِͯyUƿ%9sēvTݑwU)=b=ZQc+4.q}wٳiC06V񖽓mwX@ F P$9)|:"-Y'"6I S' 4K4 X њo'2Z#}R~Ńm =Œo$H̪kۼ[SY_~_{K)cCo1Ru//g$Ȇivzmn"S1۴K7=oucVO%l34tBmqo6N[Wׇ,#ЫoHtbj˪6]Lk-Xfy͵^;."N.y(Bߦ5f;C$} 'wuIW&=otwgxx~0 ϗJ2qҹ+MrY`MJHXI=6p!mؙ  EudСrE)h&s_??8-`~b<"8 /Mo %R6lRkw-k`h6SL cAEsz(QC#I+P.`4۾bMp {i]`i4fVxg1un< fF5rXXv]2kmdq$c$ҭ`Fe¡#J+snD4kψ\n,E{4 (  YL|˸ƶVm ɭ:= ayxf&%d'M/Y_dw$r켉5e;NG@KFω4ҰffFQ^6DG:;%e zʣLo 9-?|ٚ kjԜ=Ws<=Qܹ ]<_SwjM_2ILizmBk1XxqHvXԌ"fјPLK@iAD  "^6hxv/|xJBƇkr2jl14hi77Euj4V]]*%REhLPn#:"\ LF h-t$ŨVf-W"0s琱?~lf35Æ)o_~}ȅI%:e I]9F T5Pnz7оagoj?q:?O>V{1Fm0]G)"u>49dlkhc aN@URvmiY;wh6gm۷Im:9PdL/UL4ƻK?%k S.VHZ§ؘT$ :A1 XOq li0 ̡B&b·[MER1$]F]$ɱcF7s/<~xZ;a 3g>|e:wΦbGZg{MoapKO%Z ]E@4n;@ WiB{Yh2&iYl0:@;7@0v[8~YO?jʃ.XԩS[omܸ$[ii^w!R{'ͽnZ;kP'_yRSSwljGoя?ķ|s.B=Ș^Nt[!](Pvm؜7,;\weք%Q)(Vʘ%򹤋Lvy{wzEҶ:oK_bC, w45\39>s+&7+;BU9.ڃ@9m4ffs)z^0_7pbs7Xd읧UӾ#uχfИNP-L=K{D7ސ shlu>y#m5\~ QvO wƒYj4ѣ``'NtQUW]S #V=d]{m_i~Wv6+=~UƆd6d09 KwoFw^G\]mQc#JJ~;NڕuxZԖNL{o/\5O닾w1/*]ruew}u'3N^QA_G\a9d=Gc}7R6UGF?>Z~cN{`O*5QWH9>reiP^̐R1ktd Vh 8SeWtm+'}*VK}olԐ.n["כ@2ȱwŠ%./Wr3>N?0U?;m[Svzm6<=?6q; MhݺocVf[y}׿Z}'/$`_0\>m:ne/*IXQZ}g⌖K/;B'߁Fcl`JGMQXqt[4J:sHo{e}؞{mtߞga8R4Ʒk4]6<}ͯ];`#ópBdH kN H@)_҄IO*%+mHT 7땞;~ ȳf4g5LBWD#5eʼn TaJV<ܵȼZG1oIEk)V#{wza)Myg__]Z>Gν+T6/_7YqKć7%)qui;^v.?,c>mrU\ >]Vl|onYF-Zq߭I_׿/9IG9\SvD`:HM0gM7)4-mkH>{e?c]hDur956keƽ謺]"~]ʁtP \FcfL_rJwX)xvѧ61ڛhv-j"7BLxuX1c)xY5GOiዹq sWXuZ_|ߝ9v X̧];dǏGstbr͛7-w1!eI6񓦔k~8oo<6`Kw޵{iwjYMB~1[`"o 'fuc%<_fy{{򌁎Տ{F:u-u|ZGM<{g7%]n? DW5()Mv4>,Nb8vH:GJ˰48eFd]}מ&MjK{iM*0M?kSZnb yEONV:8P[_/Ӌ5}!s>pguNk׺ipWkJ\dސj}ݻanZkH\Ʌ4>2Ξ5kgSTC{eְ_DgŨ3HȋQ2?~`U1+'K}R qFzFqXSfD!ܫ 9_| G4ezN2QG*hvɻn&Kf˷Ec@dDcA? L@Vlmo0O$6I_WR& U̦Q[ci`~"lDc^h,dkq"O~h4Evw3Eb&Q3!1yzwMB(dk쑝Tm!!pYm~տm[;qCx(# h@XGc )l uFdRkoq"DEA'6wu"5:  "`}Xq  !% )~4-&[ѧLn LA  LdN yGt.6/ ɘFm   f* N @@ F@b&D'.cKGx  uHD._|}RFrO?Dhsq6@ )өNxU#[Y]72 ON /+&g? ^  8}Jۚ/?&ܺu225= B^x4 c  !$ !|4   +Șq>@ CNUy ~!q7^5Al[rn=M@ C$\ݲwꃅc}\C^~Z{<5M\D?~왇 PUQޡe ^U5˝mW=G3@@ T~Ʋ|*sgYsn/HAj)qk-SAWiՆ?HM9zՄ7nmh\LB9]Qbu}t&}/C%GJ0l?Y]:K zcgl5=D띍5C@#uyB  (c]?zUd9K/q 7vd f*|M΁oT@κq9KwwIIk@3㖙=~cxJJۉؽCmLW(G-@#  U( bH왥+ls" Mߦ->m-㙝Vy܄ 3xZO+ ̲\fρ_Vf%?ԃe@@ Wq)DWV ajz5ј;L'S9'I;O0&)n6dSX&|i{sm>Nuq`km<( uiRmHy@@ /c͜)Heqe8ۖ@s/'-Η7>>/l}quJSO:'kɆZ#BOc7PӓsOHVBhEU  @ycm=yl&=o,IqOQIΊ?ۍ]9o&ZxPKA-լ| @4|߶LIE ĩ  A dش =+@@| eLZ#O f @Db DcQup@@@%   cy0@@OOHnɄg9œ~<hN Ul Fz H9hӫEokdߍ B~Y4ՏA@ @Μc]33ՏȤ;d&B #BzzՂxGhV(*I_Β5le\`D45sABH HX=D   %MRA@@ L2:׶u(|pV|նv7nSBz\S#C%}w5@dhvZ2$*6+{B_Y o#0OdmHxY;q:84u۩7*stlv-!P;tF%GE JGz]{njhg 2s?uomd WP@vm|6}}mynv(+5HéjOifCԖGOZqh¹*{/' QB2cePh WS,05aثJOwOEkVћqZi&0j[:GԹ*-׭^P@"eL[ Ndtx<5iV;Q7ra9%"J&ɓvVAh=7l_$ Qt\\jAyͱ*~I1x?(0pe, !.F*6 xr\0A -evIåNbZ17G/:ښnaNwuIGUy(!࣌^ ؊/:Ul0+gD9)Ggrl1"/Aub*tV笆9ʩ_\szG0D_ekcIQP 5U&͊6oI Gf4\#j˚:0`V%A9ԷYl~Q>Ll6&s8 @"217*FX:Zx>~C$ |LQ,jIQ#+z8NBjX}HljƜVe=/Ɗ c)@Aq.˘cUlNs}J~N;-2mGj~iu:466UBylqJi}Ж7uh9J"@@ jAXv_R6N(T75G;._'G:MjTYXzySX!c"¼B~.eIJzuW8LvK!}|`iN/k$P{?˹_4R+,v=@b1Q|gȘ||I#WFf?4^dMMZ'>d{/Fo풍9If.؝'cTL>4L*.|0%8~ID_ 3}N'MH~8ANDSX .8mx8@9)Q v.Y> %g󘵨iz웪ڥZ6%6ys&X0S* 6:T=6פ6zkfS𠖠tA-޳Ù !%ZXVV[u4RG8H c@@"d,;@p D0XwLf⾽\40@s^TFB>}  1$JS  + c@d,*N@JOOJV8 B2+= ?A@ * @Ƣ[ X4$nS  + c@d,*N@JOOJV8 B2+= ?A@ * x󠖨@@–ʮyid*VV! ^a   @3B % ۮa   @3B %Ғa Pwb72!eA@™@d,6# ק@@\  L2A@ c@@"d,;@p D0XwL`<  5   cy0@@2k@@ @"`:d @yc=Vy111tA쒑M??6ӡU`)M5(  fSL]J?y`0BpExD 3=hL }EֹSFt$)_ew b2Lb!7eGp:xO2=;   :!> K@@<&N X  1Șp@ZnK8,'`ݝܽbuŊ[QOZ+Qڎ,؝'@ɂM@ CX~g, v9=SR1VP nWt,vBRl  aL (2z>{ZK :Bp$R49BKr X9=MU¸!d:(#c dTV+9py;o &xҏg\wx\rGɾ%$H+o}-wckw/fsAŒa[1˒uScыޣx^,Ʋz/xut[4JƮ^Rr(*X~jTXF'4NJyȂ| vSeǘ(OX3u|EʾXQԓ=`VGn  (cDArm.V8խ_l򈚋2>GFr(&dH٨lW1aA1Z9*wRɃ]Fcg혽g<OֹTQ(Q O>UmF:1pƝ@@ *d*VV9  aC26]C@@<'   9>#1#퀎5Ug55욙B)WU@@_iU:EġX4Cm2/@@ |1$Mh@@ P c"zA@@2h@@ P c"zA@@ ڦx`b.4 LEHfMoi<xh@@ 2 `l,2 VH c@@"D2=A@l8+,Xɥ=.H'r;>i;ҵD# v?lyʑw2 A@N 2V*6v+w-*ʕq+MN.>xd,Ul.f#[-6WXqw>i.Gi#KdG]1k>hWv|F߸eЭcKV))Mr_};f)GOH}JIk!)XC-Q㳠AFC  "RY ̝P"+ɛ]8P-aFuKJ Zp?keQ@tC!o1(LKԘyL+V.i+oN9A-lQvV/d c@5P[&CWJyE5,SeuW2}6{:Ʋ{B%H?G̷ﱢ@b"`g=:k ?k='}[$giS,Z@ChϒNQ3ַ/G¬A|gcc0@@C/JsjYk4[5>8@l;4c@@@C d    @BI# ̝)mt:{lI5XeeL,xuMB=  "N)"dլ|3m2/@@ |1$Mh@@ P c"zA@@2h@@ P c"zA@H@ʣmf*A  -Tbe[p"̱yENƐTF 5?ȘF 5? XŊ[QZtn/,bz4t|6me oV>S&x%;Y!:DOe9bU9ꁲ}9J%9eqS&4ޣU17{)+٨a~qGoX)7P=@ʘ %Jo7RBۤiT6)bKZ,SDNJ򳬯]<)I9)b+2:BgcFL?x mq~+/5k;7InGGrtDrX:NJ׭yJח22A@|%H+~i]:{YdjY=xPutH֐Jy@ּ [4Jخ^"GuYO-רg%[<3iy"995@O= 1ulL07MɓK+ͧ?r0=etb  RܵͲ+@$\\pҔէgl,nydGaN43C{$(,..!$\F3䦧[(  ?k=W[W?Q2P{ 4gJoYzx78 @@-(|P Vw( $T6 &F   cQةp @bd,v@Ea%|GRFJkkR=?VY53-)WU@@_iU:EġXLF8e I tXȢ^ 2XȢ^ ]S˰5W# N:8@@ T>#?ٗW뒑aO   k*OW͍$">:}cr*O2&h NFKE6*c"ωm{G  !#d*Vj Yaul/Fd;n8 @@ ,@¢`w cqY  aA2#@@#   2:W@@@ VfĚ@@@+E[ưo8tl'va>|M-ڍz4  Nhe 3qD0XwL`vccKZt p׽ 8qiY A@"@2vl)=  1@3c">Cꉱ@)^=} O@@  @b2D>>13kbGt,<&`M fK(E556DrZ^6`OJ4c_>ţJBwf3 s[Cf_No2zPY_C'h5!c~P@Ƨlp砡;bݙŚZX}`?P7c۽ã,t ߜP:@ŒoE(Zlw'Lb_ / A՘i5, 6scC}cc=%{b_Ș?%@ DAdnjrA=QhL|԰a @aѐQ侈[ j٢#yu/)^H*F\GyCG9rFǟ]tI+ӛM6n6Ș;B@ jb}>|-\pG[k{?3 /#wRYbM^xć^}1P@ R tŇQgn~a6?d) ~f+ 岌=G|0_w{tÇlޞoLMm+w.&ȯQo/~Z>ҖMiRF:Be=d=#Px>ժTV~ت͏+a,W)E./Y_f5fŅ gI ol-l#Gftك7?)?ZRmn=Ș[D( |:z}yea!2/n{sS3uִU0薗2D2e{x{)W>~rr|vVvPMnrGYѕ% >SSsfK$ZK9r-/>2>cwgD4 >W#{T"eo6U?vE Zw)c+3Uo>OU]GKVW҃;^F4+w8q&kp=-"RM&Z<&_Jb_}m+jE5 ;A<& MY^xu:w"2r65e45d/NM&eMɍIĜX+g7T#ڋ>Ghe-|Ux uQ)3J@4_c/vHk{@ AOşN^x=~V4KgĚ^h @ &1~xo~zd DSهn3j/6kBqN0" zDr\\1.^$I`LOEj LJ0^;cnRc_X7   pZLŀF  # [   p#F  # [   p}c)=Ze$T!Qazs|`lXKNjD9^A.Xn+/J6y@iQ`VSjO ,C$/jB=ϣV= Y~=VGxK\O.@] 'yPV)BnC OÒѳ^f^32Z%?/7xF<;RVq:rIENDB`n-xu\~*yE!:lPNG  IHDRDB,KsRGB pHYs+-IDATx^] |E֯#'!H8"\*'aA]E]Oœ[\EÍ +܄rstә$3I+BOuuիWU+a֬Y'9BH\єlQ1tmd&#s{W0%"31[?"*Ps"("Fe=%hs_ 89,JcE()7,1f3Kb-j̙:Jy)m: b 1X7šbXFˀ߲:2XV5B`y߲ds)gahV7<f ^6@6{1 hx%4bd5'P{9޹M_ޮG&"E,HD`%!0t KlɌvq`6 &!IFı :Z DBMjJ$^EH0D G~:`.I0#Ledfj,¨9Z&LjWe&YXnsL=s%6.  6 eIt n)9Ҡz(kϢ$2IpӸ#zDDHxM 9]pEl8\βN̚Osrkkن ,&Am I2[ ӑ,EsMK-o>*HCqY)Oq}REjd0#$ QRt]Ymx1e#]Z >!9Ny"#-rDEr%Kr9&[$_S(Cwv[Qf.*0V涰" 1k&f2x%0R⧏iЂU:aМd4zxU0{03k/yeykh=yNIwH¤ ۉ[1'\mnggvm[k1"Ysv2լA{[l Z'̋m$WJ17eI(/()i1>woE FA HV QBTY0aI ,h`02LFWc0Ξn, q !zЊfg@Z/\YlEVfZ ,f)YhzU[OM,lJYqFFTԊBЅ7m'KB@BJdKvdJ"KyK|䑬Sl=X+R;XUְbQ1PHKСҿ]R%1obeq)J#P])Ъd8Ey2eo3vԕl;Sw1St9%\W?Qae;sb܇.vR軇Y>GW5V(HI4lrBأÍأ=؁RGNMðSHּg4'Z;$_ZB57/?=֠"4ID'I2 gc ӗHr8d|cN@H= HFziͭx-Fн XaF3lV>QSDD3 Fϲ%Ʒcs:?م16mؼ d~Bzq4H,96H. T-~s-NlҋW E }U<B<C9PrG ]'0Lrv 3h]"fMϣM?uEd^~g!;^?a>4Êo(K+R(ŧ,ހ`ɟ Ñ j4ScX3*b!2۲7xAJ5ƾ7_Q[E4RNyx`>ޠijJPy 2Oo̟ɜ'9[u|\lie:6yd[*-ot &2$Үi.EؽF4Ȉ%bQ.V("g/EGW-"[^@uJbQ/u}5 E_8w~l|"vm['L(Kn xeI$gY<<,\9}X9ɒE>UUiMF kj9%l b6֛=bsTx?iɲ}FAgfQǦ^ JPY !er :{k4GfMG6o}lQfxK%߱w.f#cSXWn_4e^=2[ee:ۥ 슶$keRPgzk&˷b, #9hfRI]LEl!i>LX֩oGOd_ ™2"E钹p EX*pFk#}I5+Y30}AG^ }x2E%sj_e5BY3ޢx1 (CH,˒v8sxFZ|'}Wl 0&DyS.ZƱF jtt.n8lp^x7K"6NhC7|}qzQ'ݿ-Mk\E~yYR;F2SFn7߼kyM)Wv٥>ߚ덯3EysJ}|8~<+cXԞ;꥙tthۏ`5_@/w2"nmԟʘ@I喝Py%5Nm(na ^䶻 =B:8 s?I'D_]o,k,$m|NpK2}a%0>rth(4{B| 4e -ce DdcX R O̷;o$[m/49?t&,y3$o~LcT6г)qʠ({Z^2aҬ߆SwT"53&& N;zE˖H]r:gsPyP$0K.V4Gة]:5s:4`b1l4'#VE(ף|V%CEH +>AeVVWKc]r൉ x-HiU(,g0,Tqʷ#0,ŜXt0kn( t:}GwlXYLB",Y| ;W,iZ[kj^$Zηr r rrA^^a^^Q~5_̳|'.BvK:pKHf$l™` ጕ xUlղ *C(9ή"u ܬU=RB2zY8?Gw*93l^5s`^ wlyYj*Z&U}<IS̪pÈ 2xܾ}8kxIL$>g7Ҽ`F\(Pv,tD\7w4դG$4Ï'7\hTG)yd6_0OfΒ`ttJl~EI^V*&ܱ'B73֭tP5M .Zpv6$]4m`gvlSu2}lN΁jr@s5?s R8iF _t:B:jhJK)d_u:HG9`P|9s@@-.kyr@yaf5μKrȡ΁Itʬuf X{b5YoԠzߔ欭̢9|/.YWr\oY΃m{@~۟-w"~[s.\`SskkH7o r@}.F ͈Qlhn۸atR(D'6N:N0swcKvU)j6ēA-dUx~48YÆ}{֪KIa r`v!k4>^4dY ⊦ pc<eu\1>+/]pL#tL|. %'~A(*C#G{ﱍ_CcW_UZ8}:{pA˭dey\bcJM|MN?|Nx}]Sfiy^! "<8S.ںM6m"BvĠPjn~Mz[=vTCr2)𰜽2{Y`c /,*Yvz-s銁dxJ>g0ty$% h3cbH0ȫٳI팴HKܧO٢E,5aK'S?7mJ.]l3G=(窛jH"*\  S9J ]ܚc'[pDU9<*u{iWԵMC&IMɶl7LPUJf:(ǀqӤoOzjq/Ecܠ 4ϑ m]L?IH;tVV\ZjHx_),FhAGbW_]άdNʫ2/[yʘɏ,-2U @x>y*miU$ss%{ٿfi,0 ޫ~W+p,RA6&4Ef_ 4BkЮ,+߮='~a[pXN3(jhG)[n!˹ v%>a|5WxLdz8yLdkeqnS+ڒ|δg.2 d8G @^K69aZZC8wedn0D̨Cxrda99 ُ?Nsnfl6~cDF.֒%h W7@T>|8fv$s3U)ɏ̈xtaXC"M ϡ y2@pe'ÞO?;Pf 'O4l;6F  ?r)fQu3_799;aŦw:SwٵVkv:ѱmȏN,aiGX%Kjϗfϖ0T(g*`H2egK HO>IKK]DZ~8 ͚%9ʭ;^( hZRz 嗥l(_6T)ҪU5iDwNP7rv_5bHe|'䗭K[ɱjWxkf.K)jv[PQtLNknsx81O_ @rE B>_ŻQ*P{LVV=-memnv,Mvt0CNE`ְGt5[CfU' D5esov/.^΁r@sX٫s@;$A@s`R6NE8͚KfYWX)xTzfukoTW{%5[Kn׿hWy\$BK|yoYsÁH16#':/4<By;O\kABlfH1+lf|B1<{?tCnj`o6>ڼ̰p☴{vC qm }FϏsLZ괅c4 C'L _}G:nfฯA}Y?7W| e7Ưٕܕ_Ǟ~2Y 6Vg]Ʈtm<6:u (1nݛ{FHأu٨/iNjEMF+|b7D+li)m@R.pHf8+?Hu!:Qg@/҈ֶ=3׮wcdyakޯ ֻou'LjIL ^)tPsE6g4OXjQj96+}͔ :ݩ:OC2 M!3w Nm[Zu)wZu?)4 әql nvJ՞=wIPE?v 7\-ATQ`P[*3gܹwoڵp$00ە_GH*w\m}:}xdgϟPddSL9w̤A+z*;>a(2?w]̆/GtnqWӮ8#*̆z},$/ TξU~d!ixo8Rz#1 Vn.61jť7,Rx^Ler1 ^/C3KmLbZ,.-xPFBt½{`63wY7QߴvDeX9o\]3g"XL*]0V W4~7rOem)2X%U :&Ji G85`n<:O ~<`6T:J% F9 Xq #4a(@(B yؗ'a=yoY-\H^gE%?UVF5\*P5g( mnݣЦXFr mP颬SɾԄ6:We7ijҘB׿t..ԓ0+G(88Lq0Yc&o$-VFN@9#`ŋ @;ð f?0jaVF74.-V`U dhא{@uZJJ`m'C(m]$Ӧ;Ȧ&JPz1IJ-DCy0sڰ kXq-0'&+lB8\]Uo 5T̜Hi,U=D>> 9kr{@ ϙ~9PQH)89PK8tTm%Ss t0kfXA& m%!'z˛7\sYsf8Pidmj:ʁxHxe66%D<؏[-O)"z*t0kٳ`>Хez]MǘtɬYGsfXWs ^ұiD5[Y#F7t0z:4@ 9D|V*ӗF-HsYKF]2%BntuIyk\U[q nZkV ,sX))_'\ŒSR"e$F)X5^nyh)Jusg+`Ze_YǺ+٢[us:kD9^uKLJ$J"VY[" " &Sɼy=kDni!I qŤ57eN .8NUud@[sB)-9A%)$jM*IƉ4+nϬ]~%)T=?B&Mmlr/iiL.РQ i܉bbʀITݹF'3KLgO<LH "N*nz2W-n" w[ۨy8N;)MZGdHb S XeL $?o,.LH H@rvrxbI$~Q05 aNF1,hK&>z jtI3 ?õL! J%i@{j*5eְ$`- i{@H & x3^"D_39t. yܷ\ N\NmU 'ЉZ>5(Uy,>&}LM V %&-hҽ+nik~} v.']ܿ?w65׏ Mvbi 7_?KJ4cAS+ (jpc5{~D b'0h͈B0[o>Z&H& NV1ugG\Bv;#n;|,qp u e!\-$ [O>;c[SF6h?e賃G6mL$>4Y> IHZc+0Տ=Df&@Eά쮜($% II(ZHH`r"l&` R,%!$SG,Qc_1}~3ݜ(XdEu_b+d ԵW\+Me5v_if7$@oSWsOiIw?; tYjmZdC ΐ<~.pNgK{7”/iI+hrQgc%kԅ" AvkZIq(нk,@ZO̻- :;pvʥO'@Qvbݺ\ Zrs[H LT0G+z rxyj 4 K:N_aQef1/ 2Û>>V';86fp:]` o:FA$w$i ,yH Zn`k JYXe硫\*N35mj7l;Thyu VcVpdC4N>v78zFCWgeL]-IȂlםoAPvJoBpNgck X #_ڤy>VIl,;y1ksco4=dhj*L$pqN9I$"oX俎`O;uiꃯlj]p+$ߗ׋߸yɤNaW$ RB$m}[,. 8 (^2(EX9jdžv:\@Ǒ3-GQ2'4|yrK4JVy4 $Vj:XG3JUyw .?M; _wOۊC Ai @}D$'% @17=r4ܹs:p=Fؠ[vfd7 |pL.`gaQqrd(OmiXaETǂa˜Cym9]psOl ~ 9*0=_xdM_8t*ZZ}-֭~1{Be&{N8xm$g'7N;up^\Y/)9l{~yV׾(zs&?΋EN',_>䯧O+ VϴV*UQT)Yx{@ X[T[_.k\K8}t=cH SAj@x:HFc{}`PO Y` %2#$L_rNI_Hbb&R{EJ#Iӷʑ7-9v+9w*Rq)giu}i$gZI; ö8Ilϙ\sE)V2csς ^ fY*i[PE֡Μ7?Kud5yMkwVqm-kL{O(-1pkܶ 9}Cnt{ l$+ZU9hTsY>M+ovp vW) j5z  $!I9 g  C_q?-dxF%j$@CP#%H0N96N.C&!ID͒\qd Y`͑=4~dP9$fB'Ӏ/5JVoUI&*,Ki4`Z 1Mb$R;+I8W\EV>ʟ`ljj'c.5W+s}@ dr3ݳo nX\k3kk\ Am%{ =o-9^~_?*lL pso+?x2S t381d 9ZOKIH#~DS&nfp o T $K2] % w_E O|5X`, EwKU`08ڞH AtTՋ>@=+hڮg-P^󉓆(Zx\^+ ܲ$'?)~ o~RZb+Nl֣ۚΕhp0a02;|ofuYV5D9~%ܗղ 5m 0 i*hl![NvKCQ9' PyM8@9x%0?zԂ@meV8{Rǯ{{vv,^SH L{,$?̂_Ǩ"r|t!]K'}~r[a]I &d.}Vb3#r_rUiCzqL/UL֮}21Qf9DT f4?%l]G|CD}-<?'Lf4XHoCL;\nҧQn:|4(v@,qYk?\vAJ-$7TG0Eu N^]"7Ǽ!ƘKJjQT?oP13@Hװe?Y_1%?i@?pي:XFDh& t;]Vr2hH.wtx@gx=X(HtɞiHV"$" q# u+rLfP~1[2akPUo@'`׮'Hmnnhl9 [6567t4:\. X9mV;44 NhF4f0aa c.7YF|ZH@y%sMq%n2F,'%;ձd|hR" `bQUS h?4RJNpJw\Py߳=t݇\sQ4xLmM&2:IR0͒15SʊkIE&(v P>7, +sc ⢿i48: Nr0ۙ.EtIyS<`co"A=y(БgT^P"$ Dܝ+Z5.5쓸+9E4M4ȢQ_҂LMl0=ُK^LġaH F8P=t-#M8!iχ J[] g3-Upp/2@6xPL $vl/PpWtb,Df&D@3#Ư}09& Ga@F}<ߠˇ7Y)>hL!$H`ݣٓاp8@H @$N(9ok$ tL2 $FH @$N(9ok$ uf@aB'Pid%sCT`BQE@Z@?uQ5Fl<xjͻ(85f|P%G@t~}\W6?z;bЂ6`t[=${%Ň~m`E߬3 Fl7o Xnb;th`g_-hm9dUyafOse7x8 PM8'<3X}6d% jڍ-`JBNij[ ʐO[vV.$. oo[;H]+&7Ou3Ley92H ^] O>}Ƕ*w۶8w113vubR3cնzNQEExArSj+ٽ(֚1޹M+xMt>R%%@ZC٠Vq8vw]-2閺qh}]IbfeMΈrJ:/eOse3#%2[qu/ҚB`u\m $P6jZP*`ޚh,vQs g:+is*gX)݉K6%OoW*p@wY|on 3F3-hdvl _Cdb!$P@{mr =kSuN)[WZSo%9 o~^x$255MˊwYm%`qXh?[Y ߆+@Z>o]wJ=+g/$K pe5xΏ6ȹHAQHv@NiiN, ӂ}'@wPeّ1岌?Ș>%cƔ?Han$yWR  _ ,ݳx/\5ykׇܼ鄤yAC{ɚY܂lAee!8^ɲBfe(uH|M&G ztrl tb"yhϽʏgcݳݺ_`MCZ80ux6@z7W`u:_V1cn/şr N S{ת/Meb[]Blp͂`W$f 􊧋^ZtK 7S'XZ|/J8${ *ȸ ZB9p+i|#suIy)lU!xG[&zd}qH.տ?j3!S|[ЎIqȁ /_⽳ĬC,?[&޿>wYb ؉pFSF : Wƃ6W,Z 4;~ח.lGFFdl"dfg ~-?dX $#tq{CR841jRH #[ްW?|>Z! ,Ϧ=rl/rZBZSS@FE2*QU#BH $z8}ڝdOeYMG7ow?]cܾMB=ooR]T >{ST^6k)נ J~"s b*$AZAlyUMc{) Lhmn\^~ @W.\.ܲtе%}vѽWpf^VZwv+4X|dpɦiJFl< 1MAj*biVmZQk:iNϪ*)!.^!`6ȠX#l.`#I~HK?aN_)֦uH Dn pI pmĀ%؈L#[ǦMH ҫ8BCP~q7PMUDr!7PC(eB~c-0j>ٳ͗'ݷ='m/bH >#`bq|/›gаa$@$)-h\0 $@0R IPk$60!rAI|Q`u`B৉ׂ:nT(MCH 6ͅQYȺ}sRU*%ml0SϩëNX=;=cF tPIcd({]e?ou7>klYRL^U.t+YAsE#YJ1 3n{ȩ޲L]2 5#:(CI/&Z"qQ )YPYI}M>R mu'oz]YH/W"z oܬYVD+˘-qz"TTeHW_84o sIHւ\ i^Y;Ug'OAmiʺT?55O>:M_gz0ڂ0hOAw^~k9]QRkAF}\a"`@kAA iz];SPw=aOpL)=cb傲].=4m%b_SZ]#7*uU8F}{$f3#:X I,̿*Ӷ߉:#8>VPd[0E3yPzE/;[Y7TrϦ̝sj7f`6{y+m8!psVyso:mԳwZ}HZn Nl0KdN)S^ q)p%{i^O[Ho zQ3Iϯm4aSIpaΗF@e*4tԁX5tr6mc`Qc8b-!DzR>i7R3N1tzM tc"@3V0y,0߿3 b i S0:d!{s}RX" {V^bI,G.%$%wO(A6K_ ].tҟ'X' thoi`QJ`ݣ IhC+\ οgdmp@71Jg@0Rr%kF d%N}Z蘿qH J )0C3A=~X>jƖ~K+Yx%,̸(Jg@mڑ49p-:N Q/:ޙDw1vM^rHN0v #2#H:(Oj{Ǣp-|jK97S"N8DnkX۵/=]R!^t̞ToK>-=L.9uEK34vSc3]~{*h'eHwl]S{nYyGM;h012<Ȑ_^N8uf#qÑ!pgČِha,ȋw>+ P]dC/X Wz.ΠT1!$ $`@vv*9 !C:&n`]tz&=c4B@?۩]fn>!wXjF t/ 8lVq8 ~EwBqhm͡/zu3#۩p;Xy/>#?]UCAH x)Ц;eKDw,Iz  8 HWn^fzϓaoݬw[]AkS{?#:덭9ُP=73ƙU%<5Wwn9c:B'`@g;v4ztvdL,c2Oɘ1%cnprM:i,٧n[q'ŕ&Vl˩MDy_[V 2-lx6m2TUzS+m`W,("`@ւNL$-\Wr{ts[ Q4gPxv~̊EgsJ=l*`?APj []*9M5=i[tY|}Ն˄nC8d{E/-E)p|W-^/j>WU7޶H1tn 2O_㋕R_w7e|L{ 6cApxW+OU+H\瘢I8iΤ~P ӳ'b͝^ѓ?O O,erpmmmn>|hS}9W?}*?^KܷޯoCϜ|uzyӁܳ/S^'vՋ;j>8PVO;Adc.{ )&?5wnE݃+՞'yd$}-,.,h x pq..%b wtgҧ5}قJnwfP;_\Ϧxl}˝ZAYa,F i}AX<>h!'O=R~tzJmzq.QWjsdzWHۺiMU>FI2c6#ڴ>h-xAPfjS3&V.(c;** <3z|Qv%9k`15sJk ֳS[H=*>:*RVFuU}_K峺Quy`gKF t_.υ1a*KiT_0@0Rʂw<蜹XmWV ٍګ;@D iko<"M:½``_h= %ؖSKqO?fbS Ϭ"s@CfsS~֙M & wb@∼Zl,?I{@Ҍu:!$5#:>h3ulH .|b_EjT~l[B*X0⨩:.иa)Sк8hS ;Ppcoq烎)!$brggw^ӆcDH )}Ar5Z 9< `@G~ KKsrh$վ/gF1IHʂްW?|?Z! ,Ϧ=rl/rZBZ%/y 4T|gOfo BZ#&/@&@Z6mCwg$-'j:GvqB@qC!lQl=)E$M*|t (g4tOݒZ]֮EΕk_~#%7y?'5!g[\@i)Ue^ 2eyO@gf#V|gshfI V)`i*0!$B!`@eACg].'K~6OErS$8Qο#S-Dl"R }tYNW)QQ]Dyйh_=3p},EaO{ǎnnDV* eV @H)Am0SS#-s9v>+!%tN2 D<3 R#!Zmyk2)#::mcr"E@tG3-[ܑjA"`@kAtcN>lv9@CVptco8:4AmXr fr^Vf5P n)+ej @NHւ>SGkOFn&JNHg;I;f' 9tov"HŬ.h`9*tl+/RX'rWأygŵӹr_~ <*mQ;\QwČYӳQ'f="i^K,ƓGkokqϫ YcK另HvYnbgrg$,XiRCQ(?\xUj:쭢$&P`= .`}=3|~,y巿5Imɨ!`"H D* ]:~Iysh}RH pP-,h"9X7@H;|GԁS7Y wuOs}\fE\pNt;.A%LJL[apYp,6:DH .8BhBd..Qvid; E#$2#Z=rw} lo}:TޮZ_ %MN=փ薀=٢á6xO>r{gI Ot^v#$0Ruڳz3eS{l?rmˮсC0Z`)۳HJʓJh#lH #Z=k Z|ɸK'ri32S]?-㺩`J_FS۱V++it%cS+_iYq~o>T[~Ƅמ ] s5vT>ޮ[vlQSv:*AucB.#A|L6UCKfFH ?({a@C|"ád7 . ǂJA=;*[B-}.& CK2X o|Xcv+ Xhʐ9*6vwѽCR_Ea  iDg{:f@H + 鳳U7qV?!;#It>?,KKje,xseMi pZ~c$⛀mZ SdH  ~$PpxBRU{NʆL=wEZ#$$#ڴtX.Ύ)eLA)3dLA Nn #$B!g;Y2`e *RU17Z O% `>ME@,H6X thܰc4;SMv $@\XZϥyЂI!ģGH *`<註FH n iA9:&(y6ϋpH @>MŅ݋xK`tK<bxpU3!$<hZаR<~+VeppBB\k ڶzNQ{F|fI 5> GvXNc`BMḦj1x,*j>ͽFoHh8]Q/,\UǢٕR (re2"7ֆ@0Rʂ>*4YAhϹPv>&B .̬$wሐMHʞ2F&4D{#t% mx$Wtx>꠽!;m $F t_Y=?x"$7/ۥ0nc0@H+F i-A,S[N3(;."$mZ A0#@&"@%L8uxK}][vS\cuFgt#,/E,9dζĄ ^Olz.i@Gn@)iHi}88f$@\X:/T]#@ Z&dnhAch0NP S A _gЂ[9b0tPcC%QXxaU"$KD8>Y.Pk˕1!$◀K Ar5Z 9N  $ )!{a{`IznX&F(, `H  V7~w8}g?Z! ,Ϧ=rl/rZBZǬǞ8r@![`gI&d[ OttᏞ.vㄴ@byjtf0<'! å#izA VRQtKrkQt;Z½EΕk_~#%7y?'5!gX5REILRY1KќgiJzp~W3@gf{F,xy!$ '`@\Nhkm>S喧׻M%Hpڹ9#G$ZDFlA{#cU\Y,w/ >Cy$E U4C<.̐Ce<04R@ @H  hQN74?]Çm.'fIt}2!v*b#8I: b'1ig؂E}ft3=SJMM_UXf+EH@!`@4֟>qءu|[nO%Dljԙ dm;gAx=4ݳJ LC9Sȭ涵&RG-Ss=V{hc6hxۍ@Cď^J3i }3M'j>fW'ʾ'>#_f [z{䔲͹"㜠OēG]-ɜ@$\o 6wgwՑͥgOzEҼX'[o9Zr;ɏ)Xc /Ã_ bֈ .%?$ D?6ffW~q7PMUDrz6A 4|[ ('ݧN?>?{_|Қd:YX9 dTgs9|С;| 9N`)$ @k2 bא@M[W6C! ?t8]/XѸO}뷠wÂóX268/l˩A#$L!4<'ɲKnXPM,5:SKh4g3#xgYvcKkO`;..@0@봠%ć.h?ȭI$%H/}O0 $!jt\O$L!:-h"So'>cQlm[vm~Q0D4$֞0К\1s?pHO BuZK]:iܔK3]yq?:iMSZֳ ~D8VԙP҄VcdžӹƑ3hg`^ zmkv|o-;k6)Vygڠ&c.!Р,$H 0@?+ CuY`=4 ۟ j AC9Dx>} džn,O-(ԕ!IsgUlFv**LcC+E hU#A9@&` 郶U7qV?!!Ew,4xse+4^FӐjh56tycH L!:-h.N-"$kL,bנh+KKhfMl;m`"@]0@봠;eّ1岌?Ș>%cƔ?HmB.@f&``I`A(X7LYвUx̸oa%Š+ Q%2V txL7|ࠐ@ šsn0NPq -踝Xx[:.D @Uڑ`̎f҄Q .$0@봠yCbosk!tEѾ|p_ޙX4YJS!]LBH ):"i\O(;Z E!tifVpN0V@L0@G&4D{# :'@t0@A6O:!;O @HOBuCm!y.pp>l 8'` iAgl.sHht<0Z).  D)Z#rI`#H )%a*40rjC'#Ȓ+=*˪s={X]TT@H/Bu$3'dWX}1&@H3SN;).?sufi-ΰ&/ @}Gӂ684_f]lG?ޭd{LH !` iAc<边FU$ <2ma-GVvcuOs}z6j`Iq5ݱ:Xx}.б a(ͅ@G  $`NpqDulԂ}J! q؂x{aϑ@4ЎͰxH L6@ :@#` ֹ)A]3X 0@봠{Z O F/]m%8Fπ.fyh4A{'C"hEѾ|Ʈx8`%'k}vJEb"$9ă4ln˖?m[ /1s`C":,H L!G׽yY:h:__cEזQDŽ'SNtoASUQ|Nut\q5sl輂kwY$Su#$oL!:}СǃM[}KjV԰sewak2MB[|H]?8^$Hzf7?Qxn2q-jƔ.YVmn{{4 ɂxк3!$؆dA?DQ]eo/%Y OB#thܰ(tM!Ц^(;ws@šsN!@H D ahA @[l 0C:h-kkFH$L!:}|OAă ,&$@T0@봠Ca$H L!:-hAau֞&7 q @_0@봠9ƃq8qYXg9EH`O,% 4\ $`(hPAV* 9(qaȑS6D}4z4&$@&렝%[D"HDpX" -h!`@*g;PSYy_ZZNyN9<[h}0_$@qD AхCEH$uNƃ *naX8 ^Ol;1t,M+@?4{eX :Z&Gԁ#$zC7,@H PFH DƢ@D !$O:$@HPC†@'~@H $(!aBH @?cl $萰a!$@ @1 tHذ@H P[@H D 4}@H ~-hL $Љ 4^H @6`@(x $LJڤBH @50)hN v $@HPM:1-$hÄ@&! iI&D]x] $LJڤBH @50)hN v $@LdQ" 7Y8]LH @Ga} nvn(v(>N::Z;mv{[ h' Q&L4uxK}Yv]'nks'aw"m}Kci8$ hoƚyoH !46'@3WS$ tM8 !=s=EH @لp(3WS$]22ma_ng"$LD+la$m; t癨$@Rz;ሑ@}C@}ZlX|o&[EH "to0)hN v $@HXQi&Ig 4DIt;{?5+XĔdŪm1Koד' t= bx{86"^i觓$m6^_} 0@{ٗ8{ȳ {m'Z#5ǂj}}o&$)`;?0ʫ/^H7S_M5џbC:R_)l D<W_)Kb (N`'6^emЗA"7Ӡ)?1DtB6^H}$61`5{@ӈnV`FbNخS %v Qϱ5Kݏ428ˍ68vvZ~ܼ6qh7;k8ƷEq]Eh tOs$`R/ߨ[;;nkn{ 浇0u7}n|ӼnI( *-T(j7o&b ^㕅#"̀HEwYҕW=%@׭]dž5뭨;Ρ{ +*گ vܺFnA'K;~ag V zP{f9 . ]Y9ZM?qk7<4q-_:`A8޺ʷLK%g6㝉s_AvCog{N>wؙEC98zzP{D9 cBnY㵛AÁ\{8zuq9[CuΊ;Zy'Qз5{y=e7u@@9\+$;3NϜ埕'{`^^9w-5aLsh {uyKuCKKCK >n!r,?I hIػF%`qսɃ! :D-Y6]@no_.㸆 ܰEqV1<|9yܮ6J=l.tq3 sO6O[6d2ۧqXuػ{sɭ0|!⠃ZMʠDӇ7&^mwůn(Tzn |Ƭ:Ed$fd 13Be< |L#V0dln"ug!c2ߊ ".& =p/`]Ħ %D,:C2Kf:OpR}ufp0HIY吝ё˷C, ! Fhyƺ)=;K:I X&D?*D ptTv2.Fid:Q c,3wEMBll%oA Ech%^c%YK;i &D@D@ŁKLK`,!z)aYܷ=JiXHtFHLI]$|@zq[!sLo rd{&ܼb"crG*#zAe+)aoُVJgjfh1h5_Y3D_KNБIT>[ LYZ!>PÖ$[eii276={vSϛI8[$*@yFpQVb1R'കؿS_}ѦWCJM$j(] " @(0qIf[U=V z|-VF#l L/킽0Px۾]Qc#[|-.E[b9f?Lי%KcPԋz[| "" @!::ඐ}|u¯|򱻈 2#pW _oK5Mj޺EHHB0B殩X 5}y\rG9IO ξ)kv)1G =T"&}7LV=q*(h]1">Ɏ'YĽ"m։"x\A(b:oUΟO *V|'-se4_qۥ`j0JjO_?)ϝY쒒瑰`-` k5ESmoB}*W/Aoo:uNwM.,k=RmD;ҥۿ4zzT|Kcx'EDD@ oZ, &[bq+? +d%[3!GG!d#KP7ٱ;fO Ub6Q_=A&:}2wjj}#:Dđ oK_8zGj]KI#nkqDT qO%T4yLJrMt5_{ߓVu T2άϢ_mLT>̶v),他Ĵ:xRYO _6V0-d:+ " @'!'@>z!bIP0!b-7֫pVl:j0C }B7 N3sa%9KOB L3TZ޾e7JXZ` AZ dEi! at2qWduA2! 0l&Cq7)W201߇Ǎ-YkxC>lpZ>ꓛ39jx|ћv-X@v7[p:7?[L=˃А٢#U|%Թh;Ƈ ?#GCzrTc$?Я`B+V\VNZD@n@lQl V3!4<@dW':%0R$=|h1F!! ˇO_Z[KFEBR(ZPNM)8rN&ƌH?uG25]5wL|eT7t@ezmxcabekSKz3I]Bzȳ~@XKLLmb i潞̫`P7T+]2~[z4=QTd=c,/튝7s~ھ w  "0*6(O- wfA ߔ?_hzJ$-XWlI3E:6cf_v, %[*持Ӆ Vч7PJ2_,D$آ*t 97}o0*T{x\;:urf Ǫj^Q1iԽSp4&qqT/d^]<M2.YOˊPfq4!ud;EkWsPpcbHK9 (.@_;(bċ=-EcCn z>kNA)1ҙ zn -<ঐavS-X¸{]?䵩"%>ON8JbOkP6UUNy"ZۢneVEtG/MTuBX}L'TimW (2$Aw$/m9LUӢzx9~D@|}0} &yhR}ߥ<0d'B-p%=cDrKf^!fJ4 嵾dWN~ly[wzyu!!j4E8v`iÏl6dsc渷jȵ5+Ԓ_Ô&/T}`<֠lՊ#;uP6wǶQYTy>Qn%f2lↃ /ڜY|Ň혥W.fo\m+Rǻ83/W-=d^F z7H@EIG@Dc)EJتfb3XFgU3xU}ׅ߼]S0p Bp(ґ?I#M:/O97b1"D :- |P }9HmxA ̈́Mj}BZ2nd4R6@H &ubLX:u ~KiC9|^ssT+I1\$w7Toy~ u,l89sJo^.:'[>FIA9f¨CJ2vş"t (L@ (ly/~A"a:pʤa/5Y/ޠ3"pt)kO4w^`AA!lB\`2."/h,,6 oL" ܒMqtզE5%DlJA 7p N ܇3A†DS_]w 9} cED@D7"'xX!" " 򭀺8XD@Dou$" "[kg߹3p꼚;X'" " pɷ HnwvיƑ)\JgpU:ЄTQ@D@+mٷR*b>0-ԙ{cumOR;fTWmu, " @[| ;XLEqhHl6Ѷ.$d+tpK'_Uږg) uY@RUbn:;>Y.EƳNNՎJ""  btGcDĚd;ѥgu)ܝ4>kHېVOIJ0տnΊY0 UsF}KмŇoKxۣ Gg]?U @D@ ŷ5!gr6v)l$u0;IyZJWFaC%N<Ƭ},^Ŏ,/%޾`9D@D4[pƺHjۦRɍ汞_B[ɛ)Ŵ3y @D@-oRw%OؑGIٌO,Nk)B2c;K]j,&;1_U͢<J3#Y`҈" "toػt,ٻK O8ڏ_nԂqGRoяWq!" @LRXt h, " WP " ݌@GZLjZB9D@Dou nX)" " Z@)CD@D}P}a)D@D@\!S#DS_]_" " @XOW/Ic :CcaD@D@!-KD@D@Ds@չb" " B@D@Ds@չb݅@XАAn#ތ-o:7D@DoU1 " 7#|˛ @D@Mb$Xq @MU2Dpfb15%Z8,  j<-ł"ތNDe-}CرQD?{toI$WKG'PJrfڰळZ홺V%!q_k#g!:eN 1rz%r*daw*3i ߨ ]osA6o-Y'EB1d܋ Uk"Z*yaZJ/#'2>/jL=g3kh TLq(ܟEAVU3bn('CUikUyj X~(P"LΪȷP;?PQy|⻆]]PmaQ}bcc##Vc=t$M6D :giƲwfR=8dޫf_ΚaeÐP {c'TYq@ئ[T}E#b##42;Qj #ă*sdd$*Ӈ@|Z69o5(Hk|;ekR+.,fI* x:EB"EEGg ѽrf dE5s6 zTV(4IT6<R4x{58O*IYk%6TrZi at2!-¬Q+kwn]y8KW K1s? k22TvfDLؓAy?nOBx3 0(Q 35lT_/rC*|c3TUB+黌q zTJx>!s0 \ 1f&-hzG8)4$yO+B].3=0FSU)opS$Q-벪x*U F ,J'ɬχ֨ f_u[t:˯Lg¶֪)s;,1U!<铰pxJy7³g֟=r̷̓OB.< OB#̮kרk Z8v0IȨ}k^[.?޾uO {=YzvY?9P[fs;Xqȟv䗽󇇷|PR. Vk;b|1a8bيzƤMdc bNi'tDy8cpj[T#V)bbl-P?^)ÊfI^?kÌrSPB6^nBdȦ2±(0&QG b8-K@x@MZ9Awnu1TŔb0A*{NR*jM󇇾^? d.{R} $wMk L TxCԲ-D|~#)1hi* 9z8:9#%V^wʘQxKnY7!K}=$xP2SyʎOZmY-LN/T<g(S?L)TO<?TyTU~ TE?}`wJUxebûȡi(g+ߟhH6vOZ'!k]̷p53g̾,] ?p PI4dd =0K#SU ,'8u>Q\Qٻi}/jd34JGEp?xKĬ 7aL4?Bzi2BAR% c4NaB@mQBd%+ Y%nJ̒7b0yQ vyS`*TN@UXW"Q3 ԠolNlԉU2YJo2sFid(A 80qÍ0w裫m'%<^"4C2yF U3>oa|D!$ҿJ}j! ~o ")/ϡZ LZi]+OWWF'3R?-KDyB'!xҎOgI;$w)I]qo ~Q z{[uI3B Bvz'p,"U})-Jg@*VDgߓo_ʴMƍYIsQ4l;qqئXDUB'h2'S3>_ɭCr/?fGSrd'J{'N?e;@^T\&F)F*|t+sB&ޤ kxηl0Kvl[? D>H_}d 9vP?'_Nd% jSv*ϐtyȯ?r }b<+S<0ar~$& ;1%EMUJ=z0`U'SzVzžB'3@`D EE#t/K|&5@|K̐WAEH+__eON8ڬKbXJ'|bOZp'rໆ鮼>d˃o5@77b2KW=;{`~M@6ĞA.<6˥Ic0tǃCME VB~}b%!>/NBU7( Rc 2*%_p q^pP1u(*5)/d ]j ÖȌ)R:Z՛2yݢ y lV[T\KT띕Mv'5-{N?@b8LQW9(68R`>i}ZV1iP B-nA2 -Men c韈@9MܧW=%??W<tG%Sܛ:SwcAOAj,oqx>1wxҊ,LR߸HbCeqAkcEQ j/ʲcꕄ}lmXj2XU9eHUǭ+!:b krDg;ÇcP Rݮݩ<>=cUv> ާ,~8> -˖wos|N7l0h[%%% Y0{;NNƙVs_;ꬢ,+V=8>fPY t*18CxuÎ|~,CSͶW" ͐Tm)kKڬT(k[S@Z]~ݩ m@yosZ·aK(6 2L9 nli~KpC3xAnb4+,'m]G^I,5J?܄$w۔D%$"QJ{}jb0ڬVαxlL wJ,̀lBP,꣪#Gj[l}\P^|\pN:#G&xҾu! 7`0 !T1K?LYAŕY9$>*$J&52 )BF?❵:Ag<ʬQPF>x1P$g8rz'rƺ씠27n7£.o2 -FeO ݅.DfTpIa _<ǾUTTr P<[6fs̻ۗ8Q(1^;[9sR<8-!{N)%c@ds/iSzXjVőɈ4{Sjɞc2C£ "LgNLLDe7*3}7k^,Uo#Q~K`0M^NQΓ:Z\6x),ɒ*ۘ~VRvWF20ך _<#뫕֨ CGZvlhuHUW\MR=x"lEϘ1c4-Tfgwf@+Ѩ<> ;WFxq˦|۽}:ʷ f ^Fuy<^p "1-_Efb15ImPy܆ 3[Z"" " CVpR" "hEVP@D@!@ kBiYjR@W"ÕZsAe+CmiQJW=,j"~`yD"Vٗ|Vi];lhϮެ | ]s!&$WtJ "f?P@5S}b;}B/tl c "5]ͷ6N7b~ ; :ovԼqަo?ݍuH*:{CP'Q; <)3ۤ5U,0h9ȀlTIjD D`Zo^/#;j{7շE/%e^@D[L`̰"I:D$dtf37L}GhrOlN'4*z*OW:hmu>)]-dfN[˽2=$(g$}YwJ$ rҟ`DZ >]x@z׻Nmlqh޹|bL҇iW<-1sR-lܪH0-XZd&D"8%Y 7\u]oNԸg*)]uǹȂ"@t!VON0!!Pyl6?\$OoQ[9nZ|Ebb\me31f6㝂?P;עSuRI VYg2[fZ0qIQGDW7׳prڅ7o2-1'K ڙUnzÅ%N ѹqU8~s4֦B~jE'OAd˾'Q y/&GZGv[Bst\ή$ .?&((q7o.-ա↻ɷWW"_&|>ɎEaOTwiJ}pPU]C%}Ig2lH؞YN!68_]{ͷ޽DD>{9/XE":kXa%Y C:tQ #FGDD e*UZt+N1P,`6RNLQ(ŧp4+jsD/oG-ce bGaߒV2AoQ\^Z?A^D e4XjpLHH.ȑtOqqt?lkH{]l]5{SgICfլbVAZcʥi2laBW}tL~裭Z4tEDI Z0qt2E7.& ?N n[V%]&|">NJ @|-0nEDDEDDݫw?neСbIǰo}|?Y=۷ƁQ);"E9M&&'J,ł5k59PnX1EJ&e=~Iġ$i G+_ Xb`O=t&cc%i렁$yzFЧ#ӓ^/YP(@H) y NRsKR1'FCh9*6HWrԃwҴla" E|l69SS[[YS}TUEEAFEt4z?#X|:Ç'E.66v`قС[/QOl˛W sѾKhCɛ)ɳ >s `ӈ D"U\\-B8g{|cllT||4(61W~/AEo洷HJEYa).l(LIH \yq吀lůM,vMaV D@:ꚓ'ʫ+࠮E r@1싊o} `߂ƽN`m_rjH*Xغ|\#MeW͢EJHarp'2A1 DNG ,u+,\Qgԟ?l4'|==4A`bl`D$bchЛfd=,Ԟ2lm۰Ȣֲ7֣䡞Ɏ""Oz]?W꯹xy"y4GSBZZAF@pڜAopt" Ӡ֫njwAwBy@%}OmnhOC"$XAt؝ O{oNxȷ<$փƷ:ͻK-&`nhĶڬ[5W_-Fᘴ@ϽD?xN'[aWq/#;jFҁ6 Ο*̧C`xむ@N&rI|A'O8pWFVcccM-AG>v}kǷֳsU"މAI'7]^M5.:ByͰWn!dOwϐ޽Bo=agXG*J%ZܪYvt7ߎRg G߲FKm(^-$ܕX-!"'B'qF+j wR~]`aD|prvYl BG>etZ[|lI'd-TvުRY`IdC}U\V؏4ben:o<^AzE][}XLL߿L}"pG} ZoeCx<j]brա]XqpKM]qeKS]%DHsI8Et.8&NeVy7 VtJiv5k59PNTJXTԔܾ5nq>(M*n& @O-L3''9A X|!C}Ku ܺ}˯vpK}+gNTw>xu3^68 dk\̽v4~,[t{|t; $jI@KAX„vB&rݡQ[Iji%;ʄ+% }(l쟞}о }>yqFZE@fU˷9&O6&q.g*";xo$ഉCX67ދcϼci F%[ORe'dK1mFcF|&/W v뤦Q(vfpR(8?L.Y!@eO~ Xp>'VMZp#mn s;yhW~+Ê׿7z'ו/_y?۝I@DW|SK$ЩS / zC@W Т7m&ΟpJ?p?vYG=}Ϙٿ 6 "By۷x`Sm1^-V#ž21medq$9PC&GHY]<<*tz6#"O--&=j o BxQEH%}-:)AlBn|XT-<уPXbV% E*Ȅx,.&D@D@G sBcoO˞3G[+NHH`S! =!M ^.A?~t|-BL&F25CFv"ԝ>y4Xaa`p (,Aitɳ7?|psaQC%s6lĈ'*3l47XJ3o-_`?CsAs+vidԀoAwR4Dvu1,QyUV4ˁ=bB@rQwͧA}Hc5) {O-0 hqS r&c!?KN#*ml#l7%8-0:,Kٖ6{%dCzNW#O߂+'[zs%n$X0tղSiШEf1Yʜc%$.U)U٭zW O!I)8,ȋ-¤!ً|PKK髼‹p^v6Y㸩i9k!)]xVpT R蠥G<{$iHګ+K%9oуBe_$O#F45[]:=/F.u!/8S۱!"xoi1 8yp wԎ̾-_.'v }w1cAIno;?T@kg9*Ό\裏=b0y7J\uꡬiND@𤿼w ^Y&1s<[0;y|jL&RyoY{'L+/i/|(CE Z:Y!rø>M"ڂx(ӈܸI6;~Q)BPzSNtL˃#&md֕Doza_|>l9 |keVA0D8Y}'TwEo[ZTOoo/|)''O >^˹}D!|wt݅ǖ =%D{Q5cA-ǰcriE6y<`o:pC قa+##,[l]@PD@![yI]S0SOD;0j+I-Md'l]-Npo_N V-[Oϻͻo]Wϼ>Oe<ɣӈ`töiVQ@DO-?p<,oOg|ql|"*"7'j1 8ODB\#nD`D@G<95Ԃw}.mn UPs">I|`NgY`mb$-:9vkg߹ ="gxo$ЩS / zC@. g԰a:.KCӢZ]p!| ɷ.d:k[sUoˡ<; |3 [k@w`.ClK_sf I:\f|uϔ|wRNL"X oX=9tWͪmFEL*.%|dϓBaHٷv/A~ka[RҦ.N²҄/~"iS1n_zⱂZYS]5UEt[7j=tsx d[}vs /;rbyCN$ehȓsBm5gfIg? $ľ52جӁd6$Ilpl6\5гz9NǮ WiЭ8Řj?6`>RH͜լ@9L)K'ǎ쪢|3WQJՒiW}8`DO-./v肂 NeG/^r !"tf0M!v)_Œ$֩΃%iFXٺ 3h)I[;-.H 8o,-R 7=]]骏gMבW{glesXr @D$hmrD b ր ^ϟ6++>w_)++sf߲Njl1M1;{ƹqjf ?S>ի `o'>+'lye6V}+eY'JbDU<ɷپZu3ng fav5o%~kE`$ *:b/CDy<ɷ:L>X^w& d澃?!J&\̬L[b>1q]g=BaT2? pbցmROqQooo՛yGXPlMjȥJ>*?LGǤQyx3*+&;8T[V6/]bj{߭% +D'GN-yw67֫#@M,¤!*]S- %} 8-ʌ:mW  @g#I[yqM! Xcu24Lj O--;E >ŶX ڕuPcqD@|O--; 11pLY0ScB+ՇnQ[9nڰگ\2k,WY@@Aމxn8!!=>h[B!;DcTLݲj 3dwr摕Uƞ}':|GDpׂ4KD yڸn>rۧ_;޷'^d漘ؓ ~!nC֓WuE!N#zz"DԂݦq`9E)A=Xomn4]:- oK*%O"VC)q5N"*cTL)7[ t` >VWtv`}bf| 3h_ԧ$mUp"0q%i*j]%?I,^^4], p6nT^X43>=)U0vUe;]"`4Xj+]WFڻajd}.쉡|j^>,[Me.v=wX@D@#[8$ޢ=P+&,ڱK{ډ/NsԿNk%L.Z H!a X(JK @35?Yn6/KMMKP`WVUE.G@LQѰEFF`a8'Ld"$վc˝iWdfsv d:QQB&[{,s.ݵHd|l ""`xou]igVGB.0^ (99ӃkӸPIn]pf355'NUUT4 | 0MX40S Nh,7ң%.1ǂ "I]"7Rf)1.ndR!thn-^[2.,7e3'=6 kNVT(,ʆ:='dfr9 lZN%dCjzq}֮1acWNPz D@Ooy5|v}=}3UZ_`aoz0mwR LCVwԯO(!"qs%𤷷o9= QUqJԏ>l/"zVޢMt}"DL%Lt!zG|Z'JN/9u(tͷX: -[1n}?}ǃxb(Ě^86zT? \Lgcw#w6)vbDp'Dxl`N3+OԠ:AHgkra^=fI}b;tc'],> O֍||wouم 7S*sl"hCO8iǂ[?aK(8AH}pܜ-^u5.iDjMUQޥ))KAUɖ$y dX^)zm51 ue~L7 smᅣCD|wo%}wMaO _?!~P¨$4ᒝԲus] )mp]suMh+gb(u@w]$1f:ǰ@g[pZeb8Ġq!MÃ-"'Fk#dZp!i r'=Y.`v3V8qw;p)jƠq1[0"Lwڷ<5:Tڼ}5SG@dE~z&o[1b>ʽe[(b#@B|STN]߁{ru؎&h3BN]6P\jXtXۇ/ _!uQx☣$49AӀ+cD%ꍆQ@xozkwj߼&C@-w*s7^.m:vؘŹtr99Նص`1&>aZc܈4t1.Mb!~'?НTbqL,'Wh {s Ajjli@z.`ي!DFF`_o84&طJ21,HXHNuBx@D@ |ȞW{贌M"oI?]KnKmmxZO0EazIydr%槭M/׮^IYeL@u[*UF%H\0nIѰ01X&ɪ^)]$Iiľ}hijlnP˰/K{6{ɯZ}ע =}Ӄ?n⦦06m%a땈$ Vqs%96*\ uSUB5 %-™C=c3_?sWBf=m>yרg :X[^*uĩ%lguelsW`Tn*͕y [ >6֩3"ZoTsPWTI["}ke$xi[Jg&+.Qyo=Dy #;-4c͜Ī71eK+}W*оe9Q| \Iy\^bg UNo9%[T &"Ogl^w& 7%JRKg1XXmcD@Ieng7" @{|b{23Kps>A DF2߾ՍWF#ž6<DթΫkF@T-'֘<ɷw}@LQ *.3Ci'j|OD8jf< vR֛W1Ȏgげ^@`']Ѝ|"ʓtaOL"3Ίwکs&SFGpa;XgL. /I;2"xDL7oĸÝ֜w4/~z`7D 6-4BlW{XEnUȗI%$,yr+W,5#p٪Rvtղ8DGK{ "IVfI'8%{\C /)R//Ƒ'qzYIii,7})찿W%jS࿷A7L FԻVKWKcSݸn _FiD@|O--X2 ,@&7=]]τGx{XWlԸ;**ȤTvMg99E/1v߫P:.*)vRQ),2755b87.{bCN"Ph+7WcCD{$r׾GDDEDD<ѽz39T*s֡CŰ^} %OzӓIBB)և!YKq8jG=S XHi F&QmflDoJ(ݚ.tz8D@O-w[f̙Zj:UUQQzd4{ a`X"X.KJo*K+(wAM w?Kn0|$gSEA*wFl 8nD<ɷܵoUל>Q#ʆ:4ȁŰ/**[`܋{q`CT_y0\[%wP2wً|ZUtXWInjӲn!5R4 دggI1*jb" ʷ,nģwSK^65vv,؃M 8XM C;W991;y"D% ;&ԉRw۾|wAHBڴC \a] 75-55m*PDQ@@F@p,_>lPdW{?-{vδ6 ["9 pa3''9w_f5_ (?YNۼR^.8N.%l}\E<=O;+KKr*Wrwɒܬ{,B&fgXW"*ǺmY҂T"7S=1Dﴆ퀽'Vb | lLbf4"}C W~tuof@6LWO5ߖo߲[gA߲&]GH=ɠ c[,|K&acm|{x;n |-L-a!ۼyIMضX@7"|5.$toC=$lW/j֚7_d5+,!۷NE-Im򤿼[JmnH4 "xxU;~^HFT^PM.EMbe64: @|AcBi[~%Eo.9:x@M SP}yh4iotݘd),(lۖR25B" 13[8$'vgpdjm ;s";iӭDLlma=fWQw4>eT^-ApgX<,^]j' _ǡ嵸a9Z Wq/#;j_[Po/>f+{u[ L)„ [|˓ZܰBj\l;yٱED$wz8FȄ@WH#>>!vc͝ V @"Il`f35Ԧuh*~»X|  ,[]<LP$E.GQr6m:rk77c[A:'`26վ}D `$r׾U_W]s u5 u.7ȁŰ/**ط`-``j5'zA^EKFsW9U X̯a0!l4B[0D-[$G%oq=HR5w*LX9" ݊''N(9ud[?+k Xp,fq0-|wN'W?/vmkS?.jKO:ݼ$:7ߺi+Kw:hD -ݟ^MDru@q;}( i-dFv,5do)ݲo?B,xZC`"h2n(;wƇ=BD[O'j#qyI]S0SOD;0j+I-Md'l]-1,yD]>U+__-~i/|dâb$`wGyD@4!@Fп޲?-{v4M"ap>b!҈"6XMʾ$k|ƾ@mFD@om]jm&-f=(Ѝ}3gpvDwF_[4>prӷ{Vh/v!,&@0Ǭ[7 1D ',X"нh׷7!tȇSz [gh'={LM"/w,~к$Ytbӟ>P69{H7@Y?c AsSwMd:lh-D-I9lq8&,F[Z<"-iCMGȶ28w!OuDQ{r(s״9 ϕ*8˺k.vN?cpsub;VtQc ϭ&N86*"|×Ν7{g`&ƺt^w=r)Ileɷ>s 5p/#$&,ɭ\ԌeTjYa˯?4Zf(# @M<Αl0&2b5Yf-1eblź?uL ޥ))KQ<>kC$wzfIz86M 5A{X9|OLFJ+ғX+Ԏ%) 6lXVLf ,hm|5Uv3Vyyyt ۮ~?Zvr DOشFSl ߢa2Ёn^vZH賣~+9t5RL |[jؽO8KyhQlD#c3VfN80kaNzHAc,[FnxA/pX1dYǺ6:`WL2 CUe%g|W8<ɷ!@&7=]݆sCB #G}|<=ڰa]$XrORl Q!'cKVAf|"fR^aJ%\G7|Hh{n/4c0m[j_?%cIBV pNO --_Jjo J!\-lwҋAese|z 5kjO>bٓ a26zr =YYG ^>4s b9OLJ..Y7gŬ,^~Zw288AiD5<ı\ZӐ&72NLwdA&se>Tƪ~曚^{oi;[QlEDDEDDCfddt ΫRyAn:tJckoA@~࡯U4.J'H!X(Pj8x$* I PQ?SrCq#h%gű1!NpYs8v9%vNr*ݲW_45ŶVrDw >[M;y䩪u^O_Mt?L4UOY>NNMke rMIV)ʁ(({/ZM%gˎl{؀9)o(κ8D&&'J.( /UlkL Y RŇyoi;[ l>sڴNUUT4 \WOx1|x,KWQa]E"y>amG8jnje_.w~Vڴ3̱}Tv>cns(08,9oHȖ{@=[m*6[Zd~7W^_%ܾ:4uAB}CWog-}c^Cw/fj˾R HOwpk>穘d `LcWf I}dE Jᠮŵ0ȁŰ/**ط`-``j5i{$0%O`mcsn% r `2~̱UtXLdnuHl=HHl9&.f⦦4ߩbkkħ 5Зz{[8g윞=l+^GqS]' ;K1sQŎd-SYj7w|)wL"5a V(.u{*]0@Һ9*Q)mcFQTI}tS'JN/ٖ2o; YܦL!_ȅ$Nt%da&{Qldd<^Qa.dP덫I8Vn_Y,B@Ms[ȷ_&9Ul{lY/|o(_|`24"'[k6} g[2?~ɺ9l]pz֒bPã-FYPOL)O-G0gW|1l"szo4^* ORqEϱ*3 4 JQܠ\Ɠ}K;8[NC?\=fIj/ ^@FO%x0L+tc0~"xBퟞ%L)z^.nS!qcBS{eOh@B,lgW(XBCA|g d뚋ԱSV0'] _#ӈ)LR~hM:?iGmUk'[0^ UNo9%[`T`{XCbdۮz#1(6a@|1>ɡJ{l, [^Y G 8Zl| ,HJ-\oiGgX>~4*?C۲!璘DdӉO@M/#>9g;V}`Vܵԯ&&ni/s ,=Ƞ}a"{]ݯg^9<2Wn!<97n+߹ i+9N :t&}Ņl>3u\0՛y2ȁX-Rd}yP}XpkhT^XeAZ Hտ :kɑȠ/ +[' ]w!*$/}uMz%[gMpCYT`p𮟏I] Ƒ8GbZ s r}ItԾ]Oϻͻo]Wϼ>Oe<ɣ2""0؛;pFuepV=8εbܙF]΋(>s0P)_6|:aGj>J[ZO?y{x҅9r[_yrB˖v8y#e˨3+>i27AN \2 3I6α*ZORh7CŦ}V+@M<βV~%Mej05Vk  t&[Z[9@D`ӷ4W ׶||~>9 g 7O 2J wg|(1-0 H3w1;ʚ%sNr|Mc$^\YH鬬\;hԱqiDlq0ۂ=+-FEC%+" / ^3,ʷtϸpjo=tШp[/1t+l8w" >ރU.i5Oolln1措@FWlܨÔo|!wtAUQ l{SgŇo'!wg/MIY9#X%%|Uђ$\~\nԂ{|@u+.lّ-L `NӺc>^>4s Ċ'omr搔]2+^w '}Za嘹{Z[2)eqx_z?/uXpkGo '`ǰ*D@q wdnvSUg< dbiRI0FI@+}gsά@y`]I~8Z- wi 4 xNNMP'6+iĎ%?¼ jA|K}kw9K>brU%G~Ivٰps#G~o#ߵʈ5" /%M]PUۙ}K j}jI"#пwċL8[;~,O#rfӷpdl SM&4~ޥgNHh J[ p(ˢD|VEڂ‡ã|ݾ%,('Zl9^x]8j}#iO/_e_ 0]BO-#NFп}N4}y[%ntoVXXo۲#GzR_͛wP S,D<lCrr PKqo8z4M%ˬ&{Yv~9w귗[x˟+!4~|`1@xًIV0'] _#ӈ&PpڧNINzQTg4Xj+] 3::ٳ 0|H^ȟ9aiii wRHajTJDעfR&V\>.HRE=$m-^)$_ȲM&L2XdgXDpS|3.\'GV0Vor6- Q{ߒ$SDDTDD4lѽz3=3ppܹ㥥%o⋢!@2O@]VJ%U gUֵv;( ̅͘)_~;~1rg;h"e6Ϝ>qh#_,`=/3ꖛ<Ա{EEoW$+f_~VS]mfb%SN&ƹƖٮE񵫧o쌲.L" "xꚓ'ʫ+࠮5558Ш3fDyo߽/Sc=pݟ~PC|>](N\a:`U^d$h+ ,`6ItU&OFQ; " @"uZ'JN/9u(t"##Gy1/ʱ#5--_^o]G/|t^X" uy<~hcJ.-::l)9Ԋ7wQsq !jV2zL" "x~q~z̰\tPW^zo_0:]kr5n>c*eLXK 'v ؈~"@ Х'r5قAWzr%>(Cd NR&Qy)"Х`@.C%z`>QK3Ne.ekcgm{WǶ w{WPX " QUӽDaԀ2" "t1G}2Zq^=C{?a߱Xv1**R+v"Ņ @D@@-7=)[9n =9_OՑsgșd C wʵj'a%" "t} : 4zHp0?K7sMhk5D byD@Dp@-l$llnjh*AL]'?s@:d44ʌ^aHUV,6ȂB2OKWKc+$R" "[Fo8{NږGʒ+rA?R]CJʪc#_|o}C te|W^aJT \sikZ1<$"[f̙ʚ**J>Ţz_Uuz-~-}ZI*v%᣿K/u[kȀ~gýCB.lWrf< " m#dkWd{A5tӏy_rEx}/Ƕ]7J o#Н|dE28lk Ψ3t5y MD i8wG˖pجčLK_Q@3V1]MsT¢QZ$ G!>yA:$_DNݽ (" <ӑ,Yeʰ :_,+F)0()WjsԒK]H>?EĂU%eu_AJ.f kh;^yHуW fvt)a" 66Uo/xc}VKc4)/?09Tfd}5<؄~zaBnEҞ|otaBDKłAЍ4BP Wҩr< !28'RaBAf@_EG?ڴUnYy\wOHc^; n8 " "|oioe_w5W~scZnHҥo_{/s gkC|[Z[K{Qذ]KxE}/J<"iiW{ B.\3Pb-"  |K}|5d3MGnHocN;x9S­%a " !|K}+fw-DN/k |e " ~o--~R]9˷u=udib47 DΟ"[BN\afY,G(-Jhx8'7 @Dhͷطl*.2`' Lݩ> ~vo49P2KEj6^nAžENyB"X̠9-Ȗ.ab&s߯qvDH0yU|zȷd'3Fep2Ӭ?4<·Z{e"BsD| ^^`0AU[^#Y!C{_'ِaDn-ߞO}"߼J_^F̦, Wwu>ՑlAl.1;{#7R F -~Mr^1gH' "F U}ڊ#tۭnF@8@^:d]c$– j;6^T[[%BuW[WK9twƐ#Eϛ'CU/ߪ+poݻWgt?L TNDp}M^C\t7@Ne!xd:ޢ%Z }qQ v|K}y-7ѽ6waΌH7Vۮx1Uu``n4"~K9Zwd/šwk. C(OFǺqWfQ[[N,ׯᄍ)$$D3J(DF7*OdT^ѡaQa!:]0ߺs":܄c3;̡"$k[`0|` 8Xqm+%CpdOgw.\xl[T(WI,ěuEQE65]u6r˾IbЩ:ovԼqަo?ݍuH%],#Ƅxɖ#ߚ {.c}70׺9+^_.{L6&h9yc$Gd05.EȂLghI&j.M@GP[Q+9+7\ *C1\f;0G-X@vH)ZQY).7 _4e>ˇ/5\͠":z*KzΉCZYQA pݻq|K}l6hӮWmeqYEC 3a}zBfG*(:|K(5l9oMbo 9YAL9SSZࡒ R~xԴ9#ʌf3_:E9DՐ@@1"-^Y;J"q$[Jw2k59PNY#2# CoRPRWɋ¹Uֺ_z"+]?[6uwmž~/icN^}/Ij=r#Ie>?tMZ-&"%Yg&l1,f"]8?0!^:z-G!:=qߚO4Brb#Ny`g17HMq 4NT,[0|gL3'vF|б'on־/2%pQy@[[sKX.o--w]=(|ZlU.饋J"ITؾ%i4qtujJ5nA` FŬ32fK4u-@$Jihr[8J-dˑmVfvNCIga1dՊ3&Ē!Sf͟ [EUbRl6Emu*q ~vdYb\8"c3ҊYsQ`][^IDɋЭv[+]qkṗ?Pokx;((yB36bj>e 9LFDkxX>>}‚r/iRa`%0eH؀`czM-v-քpW XvfW8t%&;k7gѩQ}c|2s2u#+ೇ7E>IG8*=~ 7{m9[V /r0a.tX[|w[pqTT)wLpݻ4[B4 !R3R+*JOmZ\btÅ&!<{ٿ,[h QL xcC=NmWD7L"*tU_5*|~~-[.vBxwwӈ 3o0Wc6Ҳ~℥۾62 Ѝ"c N3[Z -[*P$C(E#teG)WI-7znB~<ȷt¿V! ߈ddoih7u a681| [lm;{m*TyW۷ҋD!UE,Jb9ֵ$U} Yg +%q<$5O|Kˍ2.BD pojm?D5`h0)Aes6DKL'F,E l9` Zf ݞȠl߲ܲ xC%D _vuKc%/Ce]ը<;EBFua|#lS~qjʛu4٦{n! -~>ӽ AIlS$gzfA89˛X7: ;k8n6~t C7cY ogf$f$3?=l- -姹lnA3d9X27LgMpl6 TI us|sdkDpDUf2M ~FLFHBYYVdl*6|@B&Q 9&@jTК.c U/2 ; bttcF`Z@Y,J)́h[ _"&;۲Gꀮ~rK+IQ=g#`)D etR*R)au[ϼF~($'ˡeفN@ zYvD}9Flv7aADh5VCc-RĬwCѕ=VЬl[`FWNB}Kk .)eXe_^"tz>:`"*ȷP9#.~ׇ coSI|+ǜՒ+%DMzFND8~^Hy|/6e9,'GA OA:XG#0LPP(< ׄc"o3d lR1Gɶ*9U |Q@: A粭VOTiƊ!PSUԅ5*rk>&[D~(3<9Lё 6oX9ԃ<,!"^?cp_DxO -Zˡ"xD[rH,:pzRU}qxt$2)v svNNɹ6r J!vq юMaBX"LS!ӑlyU^!po aQ8#"pRP2v^pΕQQMSJŭJnڄn0brvZ<2mVkD#rr!˪cU%*$A3vZ <Ђ!Š1]ӌ0∀#0)M?GD |KM;2ա܇.ٞ'!.s"*k|?A]8GS!'1ڀrYhx)CWJSybX Su:N"-܊qb|P]R Bodr6uchD@ |3A8Ϲ=\!$xcaՁ5JX)IX" !|Ya"mZf:=LapR1!* Vp߂αVD29'jfyM XO"BJ9%D;_ݗ8Eb+ӭcuouDB4-+,G:G%G TE8a &1"Ȝ8͸N^jD)I2nL9Fd~EGG"_Юr"IZ bk(e%N0F(8Nh\V2",T{݅]BD@8{ %$ եtLTfÌQJ%udfc\W)˜鏠h~0sMؑ{^LVݾ̩F-{nF゜Meȶ+QYA] ye0]0҈" "&b& J <STdہPG%ƽb@BFrĤAJ{UVlڬ8c*~6#2C(X 5j]ڤ > @0n}8+s< j9u>D__Î!" ZTw~}jX@مb|EؗHNV$6 KrbR,:ljb x 1*Nt!Ilac-_7K&=1*S,C:WҊ=a1*=*$",#73" "h[+ rTuLd<"MÊQ}}R@"e>1om> ڳOuu5{I[tXs{y񣻇 -_s :)/Qܪ~VU?{ -sk%.i-Be^ ̧X_8o)Ψ6TPv4fUpiQ̧hTs޲IZz8CVei^q$&ZZ4)#GO<ȷ`[Ι#{>{>(Ԑe4b޻=ydɽ/'ٿ o/Ĭ/*XR^[U[u$E_㔘c >1&{i;4%'1&٤!uc3^̫7e;@>)Dko+&&iAX" ׇŶcD@|Ka<_$[xww,YC^$r`z|b%KϜbk_&?JF? \E*\@I5L )AK,Bf/wBu tdZg.V/všn'ÇG< lWoLb xUթݔyz;ʷJJJdĹ9Vm_jwowΞTGY}#EqVU~,*W{Md(j/N`r*&"@W |]֞`Y_$g9[ba!c6Οje\{,O0+Pn 8敽w+Q qƷ 2u)9`F%; 󔿼VueW=`XH|00fF&[a$jc6nobg,'$2/g&j˥L-!6%Q0|(wDX(*,^+]h3n=2>ld?dq"nG| M>OK7Pd" }dw)zz> v:P  +s*/I}VU0?? )*]uc|"o`\[nbj wAH%xC| pWT89'_F+NxbC2pۋ9&NNM9P^k 7Dpڱw]Ym 6dLOyqSR o,վUTTvR y7U ^X]N3SwRrW)f\+F*fR,K*+޲~:m/0јMm:nDE{A!Nxu#N"LѾ_蓧[Yڷ^X/}D--]'VI^rOLyyw Q`R[ >N=eʷo3\5r` gOp&"fԛLZwjU LEd~dKԣ`7Z%RAgTކᵦ3fZ&cƌA՝$_nOfe; `÷G.l րDT(D@;{y}CGO<z)Ciwg>މ h;Hkyydƹq't}˓Ǻ.A $b̖kXJϏ!lĊyhuю[#kW|tس)_^ DP]˳ qG:Σ_]w " ]wɬĘcZDލe,8ϯN]dk. e1/; 1k|<2b?) s:AR}K')ž.>=)+Ax 9'zA,OZ9w#^`+%UE0] :j?q #@OwBb>Vllkɿ!ȷھ( x:"O@G<|pe!1yn|24o{q<` @@!]/`DeFi*+q7me{:g=?l'Ȟ[Ogրԝ>!IF8vaDD@amu >݇ Etsn3uSgl0H&A1HtIñI\2rPhiEy\0o6tɨ4*ڷ\_ ?*yp۾%xGQ;js XMJw՞[`ߺ߷{X9k׆D\5|V킜oPjo1Vѵ2%/κfol6>% o_?F,Aˈ"`[|˓&[_۞ӇXn" qho\U-Vn +ñRo_]݂Q-d` t-D{59# @DB`]:ګ; |3 ]7t}py-'=ZyͼL\a: F&uriۗwn0I`j1L@,b$F] P.PK4]j " "q:ʷv/Aa\~.SC[ $hP 0`gl(pj-avn%<_ՀAd" gK-9˅ mjξX?" @(߂f˒ ñllnjpMFO]UKXdShd}Hdmpb"z"'ҙV[ḤSff'*,`ơUrEMY+#^ z df};z?EHmPuwljuaS#6-H: /XE":kX'FD@:h`: gOWWpHƨNRK,54&lKS-g-7)$8qGJ̓ET~RQiE`CNdh6/h:YX}NAf|"TIyJ)-k286N4bNj W 弟> NF*7a\I.INvXC'-0he˔r9/&D6%D>=yoe+""*""Zջ{#r)cyb%jb%FNUY N[tHd`ZbWFnĈ0Vr6]}I1(l(SzrJJXOϣaP sEI'qhڱiS&Uء>8֣4awa忦]ܔDE?-{xq~uJsڴNUUT4 yŢz?KMQIei8ݚa_)Ξ>[V"×r%fRJK-h&5DĪq*IvR,jKK urn5#^.E>X4"tE0@MC@BϺ7d@I2<&DP*O՘xhGoݘ+ &㿾7~l\su۷NQ\C=-Ɔ3斦]Sכ~5;̲I>4{({pM DRUcJDZV^m%}Z= OzӠ*XTZ n)x!$%wl6{&ds;>.3<~}>%!Lz:Xj Y_S!~$m17VRY@J0nKZ޻TiF;we-JK+Kfkg֟>蝞`O;w!*$xo?A7'gpNJk]17 H :܌N[wi}7&:>339b=79zL_u/6=-qVL~֞YQXJ/CHEUQ R Rp.顳Wֲ)7et;ז&)I* BzKxwz&X-`5Ifzk Wf-#z"z9xڇAQm/V1So-M+|k&ffjؼ3LA{kOw)l/AmnT9UZE7v. 2Yzmɉ,fxI0"XE5c5Q^z["b$[CJV,8n-".›ϾwOy۞ܳ?eY}@U(қ fʃoyH|S/Ap%$$GW.jR%OqCH $0 UoCԳm-v5^zϤ}} 4>˥犌4/ ~E!~}i[֠_3 -22ۏMBH $0}b-ܐ@$w>|UVb)߸)VfavgB%gW?r 6 $PO#ܿEsA"~b+â0/o&9Yf3_l=d_a uNyFcʏZt<#Ѿ&~ǜʆ86B@<˫) ѾloiÿJY=nH DLx@!蝡2P \2x'hLf[-icCr#zU>gx/+BW_ GV$yJAzwp͘mC+:fp%\ MFhPґ7Q!&AcP~c:җ+ltzK7>zV`QzApigqrң@xUdQ2RFLN2y)ӓ'OKJ:iRJB䄄IVk՚lLVFcH@g̷ȧn)!{H5*6ݟJR!HS*?']%xwŭS1K#ONv%*J5D< nnU#C͖ "T)̅t,|!B`@iip;+S*GO[|+n!i$?@X/4 ` yrhs;uv;7-Ҧ \QvmQf32tN[&JZ7Gz>YdeP*_ -(YA$MwHh#c*~fq)SN(eTss (.Vaݒ~n./(vE3JIJIvn(lᇤ:WgdU.:!1C`06~(˰y}1ĿEMGߩcIgdMOL+uz&)} VOz> tъ}yyp`AX'6 ˉSCCKUUzBUsՕpp[aiTҲPKKij>6qP8<9>ȲJ;bkd7ܿ|PE2Gqų/f@ bun/ED5IU Mi% kKn"T釣H*EPа1lW"`FB!30=$H\lh  0]mZ&G@r°t\}$>͘("509;`jx3Aї:E>yYqu[7[ U1;1,bgƢ? B/ ~l [I&/ͯ<`[aKUS`ߺkG hюySZXQ`FLNV_P`aͧ-l-l@] G 5XE:¨ls%(nMGi%L(M)Wͩg`k JaB &0/YeE!jzq_w*[VWGKeGKyhuFg(*< XfXzhطo U0fM5E$Q` 0R5Tyt@)).=*&Ѫ\bT~r^W v<8Xpr*FP u B$sASW j *-(4%+C=,L'xd>l%èf5JaJGQk@V;/776>@"!V W.;]ޱ+UI|rÚp,)8@'/QU$b}dmE}P*0U.ejlF2(5=PJ :RcpCH $$0&sЅP@x? $!0&@[՟z?vřjAH $1tڷWVo<_Yb̞Mo=[?<0?P/QU{Qtq*g 0xF/o GH 4ˁ:zІ%SmIӻ'_Vf`:|w7m Feg`Rk‡ض &tUV(',hn4@"oYgO|"[w !4< !7fE$kߧ֨ǟ}vEw V|ˋf\kI^WVww[j~MjLu/wҕNOvUQo 0[X)l[mE5`zKGYaA Ԁh'QAHfK6\NGr{Mɩ>rז`,XK"H!;({ K5VXJWa씅Yx /K,|=A[`LH0c7$@ch-G7/ =gLbfIS-IYYKl ) 5z\NYtyI;bt JQSZXƔ( 1dzI:ҨP,gH e5nBiJ٬}2M4 |M:|Z:x$@$@h49))Brri>zXTE$#=}yvjXbL Ħ+y `Gl g+>4]X5Do?GٗrmqJc`ї@&tZyaY?%[A [=I`@HFӾ \imks4679 ?zXT~ }>AQ,[ +PS7) Yo !FH %0zB- $0dX|$ $~e zS n%z Lbٌ̺y-Qu2ˇo<}yt/r[MWC۳QX.ׂw[uo XE*lf#+#a#p7P:GUɚ֓OUZrcH Q[+{Ou7kԣ&CfV>RhwA#g6XbbʌW.ż%IAF&=oY D]΍^GvM{N>Y`u3a'#ѹJ[GnqU[=>VbСlQ`Zۚ W3h\,; g`a#ÔM0焿y@ Uo1An㒀 z >0}u($jdR&u+c@r[,q|tzU+u[&zRΖb2$%`lt{#d:}EbJO9U0n6 R ;Ve ss!,ִNl =-22c뭁ڷ??ngs_7^{3o*ncL$(`EEBP@ca'Skɚ=dւw=~HAѹn%o$̒[<{P.lu.ٵ j1xu*y:1#ykgᝍ^ nOOHNYV3$sBt]`Mi_8a9mőPc:Ϙ󗓽S]DXZ ? G^VA%Ë&ѻ/w7\mR$zYTf qXg_t\nwӮaE mֻa Cevթs#,مuq/k3q&\/7HbL>_#/oHVZ?sffs V4O~w͵׬n {zZ]f|DlBG_ PoZPRPO_Yd V<[Lo}qk ®_ k[d%{[DK4ynot?H 'B:ZZ/;Zyhu:b BaW6fbb T=]޾@}lkAl*+)"HG෯*+1Քoܔ%!oNcU[RAlK+TL@H` yKX|D[%'`\1$-xDoؒ@lM[u[+>!͗[w.sY*y[$DžX_{pO___WUڗf."$O?[V.֜h'?xYӦ^o ,vw_xee$Kg~:w=s(0h&AD]"ث zu x"qt<,c#H BߧOlܭƳo)߸kO=U?E=K,[)UO<^bk|7H $F[jL=s\hC /IsoodBM[|?Q?+L9n Dn27;&z &zK?p SC^?Ѱq9H $8&`[1q|!kH $%`lDS>V6n{8S !&FH $JzK}({uoUI]߼+Ii9ms|=gWZ7gNV?vN^G7EL蓀JB]:toDdGywZ" aajO+5D͖l \%S}\S-&Y,iyV;87|t9$ivV;yzvפּ=CH 6B frD]] #gkE"U$C-G7/\qu8c^uҤ뤬%^ׅΎf=.,0D DӓmZA(-Hw%ʾ?]iZKk l F ۗ:'ܱ*[6e9^*LC ٷK#gR,B8: A|TV4#ò,=(VzH#Mxa@ Roe+)irRR HNN:-02E(ZK$˳W`XMb"2 >rk(+SEˇtujkMPyb4d`D|e'CHK-mvGK4մ6'u.lߩhĭ'!{޲rvDW]:#,?qf*$2!b#Ro+WZڈMh } <{cQE+^F@H @l$}k %U_X<@꼇]R}W@' m[v?veM~.k̾5 Rds\"'ʕxr; g`a#eyUpMB"Ґh*?rs"^73C꭮ˎFV#_P\'$-Uɥa߲- aLpvx], qHJ?rM^HW 7!DE῾̾EJ ¼Gǫ^]ǹen.*v!:v [-͍͗Oy[~\X )~\nbKi+.}5|BcNDR8Ε7-y^t:23M 7:烺;> Ҿ) ߆Oٷ]տ~bJjۿ7g}gGY?}̦Ce/ $'`lsD5ȞgOdՖG˃CH $c-qp H $A [o ¾eFH $K5 Uڏ~7[*O6/yo^tReq/xj9ΗOW}/̗ןS"AȀ鼐FGG/e^'$, GU]]]Kk9C.0moTglX VXx4[xW $0[OՌeWŔښeejkOed\eJL XBXD6\zcH $Mxz냪 ҬIi~#W۞˫W3'hoxBwKF/X=h $@![vNCyW =XWU6[)ru: lJNsO=d0'[NpӴVI{\-ΘIZRz]:;j.%de5B9_g_^9UdrI*Qfv>9I5lӐ\fd+'" Y1?^ra;aAI ,4LH mƛ/3u?L&[p?5^d^W0iu {~1zK- EO SzĮˎF2Av:ZX|>At1L ZLl+rJaCYI鎍yznEktUO5 z4H $ `[-͍͗Oy[~\| )~\nbK5me!*(3y[קܢSȈ4$c`JΗl@H =7 li~Xl[s|򃷜=ey?mz-bw\.ў+M/]y0Q»a x~8&x"*M[b .+/.tS3ZZD$"\t[^ $@#`DN)koކߜSyng 3{,11Qg!zٷCo zaa:$0 8JġfMHOȥ3ľ{$0Lݔրf@?YiiKKGH`"{ҴP(<&&+@Q۷#$N7մP|W $`[o`;0<:wh7z~tRlS `eiMb[x%'tqCSv bIӠ}7 v`L0}S>V6n{8S 猩KAHʟ^Odʟ8Z_K?iy}$Tr}sۙd1O,#yjQe!e ljtFX8Oj4ҨH` 0ir6:^[U;u7|e3{RZ7@_nc|gc2aӐ0xd8qSi˳rP}h5t15T6qJxn3So`͂xQJq…3K?ꍏHki4 XFf CpaCxz o>5sh-_ͨ\xEPL9ZVvTFu_!{^fڬ4{aK0&[7n>ePln g^8/{w疭I;@5&el5٩-M'q9˿zպ9szˍ/tw`>ca06ٚcash`y\W!'/`~_Uw!CX9[?XM87~,0 H,{p$ZmdSt$'.ٔ-皝z|m 6bf;r>} 9v IANZg,&$F@=KsyvkYG\)n(uӞOU}Kv0Afݴ|HbByUjL(6:"l2j.6nǂ胀ɣ]Rncgꗗ_ducߺgp]"`aEjuk`EzjA.I^)HFVRd эlw?=`~d~Vr|z>Ր-l%%M299etî+{cQ) Ibzբ(X,V)`J⇘#*le>?-/:Zr 9+er֥5 F^ $@C&`H \imksBڀ@/]/'h55 GIDATBb+ v"l@H 0hihi$MNGJ &`O` Fɥa+PXf_SUccJY~$@H  [/՟(}< $~e zS n%z ĖVFo=P\aJ|޾z~HfmEBgɐ"Θ}!$02͗?[V.֜kq򃷜=ey?mz-W.^rYBPD{sc8ڎ4Bgp'ƔH`|5;r죏i[xOxNrf}MY@H tƑ=~wɪ-HKKO S"$@H`p۷Gs!$@H`a:2, $@rd@c뭁ڷ??ngs_7^{3)n㎀Ġ(7vht h/:mڑy< tڷWVo<_Yb̞Mo=[?<0?Y_.M"V_v !OƦ +AH`p61ÕzK}C,ojF+bJmѲC22f 2f%\'2LD&07#&76Ndw$0 8[ax0oռhnƵuouw&Iפ=^b{g{/]_Qر mG `(e*ZWZJ[GH@M`@XƋ˰ +bDH`Bed}y:eXVz [)Vt7H^I$, ,\-\6$XioQz ԂR 谚$Rbder&Өtd"0$12xbb\EuҤ뤬%^ׅΎf=.,T 6]^k|NP"̧| bjvdUZK"-q|waא@H H[lVߟh, $/^-b5,"#婢!F( Xݫvƾ Dl}@H L$[̾g .aĠO  &BN!`̐ٷȶO-b6 5|Dr"XW$@H@!`lD_P\'['ɥa߲/)-)HL ٱ(0sm!w@H $0 [o陿%DxlA/|$MɃ%`ⴕQ?o_yNx s}ŕ|@dbJYӂz0xٷr嘔^.#$طV]g{TֶQ,.lW=]xu{I-`M`H 0!5H=Aіpw>FG=Z8G1 SQ= Xp4P}4u(Xan!lṼc-=-<%5u𛳾q3#Սv>rf}%&&EQ[zw|$({,/ZZLkN΋?E*IŇg6 ?1gh8a!mNM3v8gط"Z<]}'w)AEFXbnL^CzT[e`"2xT ^-o15.ExK(YZR @]@_^[, 2)|h1{ocÆ`pA"$0lyy!U ff=pNpbf'V>`k2o GVҳ35ʎlX $!Фһ0&=ȅB&4#GK#JF^ul> gMs;ewpSGpi"и-kn;' 6PmlVT~緿܊mogSܐ@\|l̙+uh0b-"\yȅdZ K4sr zoSA1 m },Y(IM{,:q: l,4؆j`: dJXi3=$#z+N)W:o9e*ɝߺy|=)-z/~x`>4 f~-:&>5a)H mYz+ȹV+Sb]B.;,Oj- 4Y!Kv q:6@TU6rnf)r{xɤ# jSG'k˯_)"=&q7y?T p kt6 [fdG]ݣfٷ]G;6X>ՌeWŔښeejkOed\eJL.j N@S2)"wP{ޢs(p !؅0evPlvS/- ".-E:8ȶ٦ MCOGdX|~) )_A8'PnK2uz7K`@cd)һZ:.Ư٭x#=lnL͸֒8$)=ԙ^r_ll+;7CM8-:fȕpcw/6!mSTb0et6>~ӡTO>)>X$3/6ߧAw2wP6|Ͼ$;<8,o~0bD͖l \%S}\S-&Y,1FUA*qGt jJvf#G#J{BH`7=_̔qKU"L̃X<*0}t2qu~l4i:)+k-!uAi1K&ZV jv{ Jk6ҘE{ILhEX$e]ZT,\d*mT9VQǡԇ[F6:N^/%!Ckz !X&'%@gSNKrk8[)ũaʣ K )*4k4ڂ|$*mW #$@F$`H \imk#6&6S@/]/'h5Bb+ vg+SKZ ZPRsF4#Af$@H?[]--q t:]]z J>+|@aj~"4[E9|/54<ٱ1E, @H 0joilnoT-p؇-_0OQr-[ &N[uWE)«WQ[w.jr}W(4>`$@H@?[TPrټͷDwo9?{>~[{\䲘$tO箿G.L2HP:TFGHƹ?U+:h-7ڦ￸uGOhmkEVpW2m=@H $0v rJEva&K$% f-1} R#r  k2Dc-o\zV/EI zk,} D<ԉ=b 'H $<,'%K֨?q)b ??ngs_7^{3)nH $8'`<֎ Z[/8 \\_oB¤$ +B4iIMc#Ͼ ^;6X>ՌeWŔښeejkOed\eJLybQH $$ 23V+,1$^io% FPg44kmm_~|Ǖo,U.7^ V@H V = ,aP4~w(9AҌg߂FS($؇o^W,(hْMӑ^dSrjO6kv~%$jCt1ACi;g|Oo\6肋?%ސza S aųoV?2A{x}ʗ}[)Ƕ>Y:e?oKrl>~6 P $@ ȿŋRs1㉜[s4oAq6}w=z~Fk[(z`XzS.x:w=qX@H $0f (*4C[5Fs<`<_?1%5u𛳾q3#Սv>rf}%&&{@H đk;w-:Uq=Ok9ȶ[o\?Q&{>qU[=>Hl $0 ifxI x@L4R ^O2zOs~o l2@H $OGMj9s\$>Cʺzʔ3gN~6}+($@H@O]SG{?Q'sSl"c뭁ڷ??ngs_7^{3@76 $D! sZz9W]nWn\|m7߼ªUV̿HJJ6sNQުy+W,ٓrr۟?(w $YƳoyg_C,ojF+bJmѲC22f 2f%<>ަ5>c)H $Oo}PuAkO&٦%ǧ|\Foo{/^nΜ^r ݝ.!'gjH $00M`G6[)ru: lJNsO=d*YQbZS$Tm•XŚM/ $XlOOWWkG嶶֋--/Cp:]Tru@c^wdH_\imk#6&61(Eg `xu H-QWՕb,>Pq%X@n+b(D J XY̚PS}TNjE!$`|^0h%&&'&&䤤T@4iIM#C꭮ˎF2Av:Z.Fs%xA>Ib^5 ~?\-Qҡ~6ڂʣ;VdT+0gtj(-0A魮y$@H ~bK!cHzR߷8azKX`u$/|$ B@l4y+<]U$jW'p v[jr@d>œR[K@H [ZY ,j: >a]UxtR5LPz~s!^ +}:l^w Y%z+_jdrc[j_rf}%&&B06?3DH $N$rU[[]`&'`^zKgϳ'jˣBH $8fꥬ_||;W.\fĽ[[q"$@#djĖ⩺a2ܝ FH $G8`) ,R,ÈJjߪԫoǟ}x/T0~@H č"@uAXx1hBFxbm-gսU%[w}:WX<'p[=?χ}1Uvͱ $@H` (IE-/4.&.ٷ]G׼d`|W3*-^Sjk=1soȞ6+1Z+vdk cs]7$@H`l`~M'NPxz냪 ^J66/Y?>}7z{syusdm/^t A-1K֐fHqCH $ O`ڴY8`? ЩG)SfB?.xz P"$h+bhْMӑ^dSrjO6kv~%$ESV\+}n;xx(`JeVdLmdV@jy8'im;wESe $Cs\H }u)igΜ86ݐz (FloqƄbN4bĖQ崘%Ǽlh D)-Ab%`0:ƾ{(iBѢtʥh ;ր.!WBH $g|G{OjT8}Iܹ[ސz ,[IIR)SIGE(ZK}KO_[,V)ش?G `5 1wuF&HK/X 68%T*H $@L1'%eҥ7púnm;nQaUX_a`H_i6۔1u3+f~yX?/:Yg@H E}KL"Կ [v^{s'N0>iꭿ=%+|>w z fƑ-[mw>c&XbZbt edQ~ DS[,s&KoRZ$F![/՟(}<:b1&I |oxS`s@lGV\d^W%EO71`' Qo_[ "-2l܈*Aw0!@H $0 Oo\6肋?%ސza S V + -(MW;.s,86$@H@7ͷh߂x^߸kO=U?E=0,[)UO<^Py1q1@H $`X[jOLIM]{6kbɚpyCH LH~@tEᨪkiu-^8C2'tHY4dzq[q{ijjW]H ![GޮB4NCvOOͶ%Mnw46l8 9!+( ez8.slp65_g-Y$f#=*QȱК=9` 1Fxzz=ci$Jz ,[IIRzrri>zdx`HwCKlHzHRV#V}AsO W͂O$}+oҒ2 ʾ(1tFn*!%7H_N\HNHs!"Y`$"iJZ8La!DN,e4xk rz#ir|^,M{ LLH?»ddbˡ XdDP$0h~Xg?F()B)Ap12A>*c1 C(D)S g0yaf*[&O[ͣ@$<=K4}%UYZNXUSiI4WAn+RWoyP4Jr66@~0 +P.SWN(bS6ɭhDz)w$7t2TW:v  IU"ZSW], P& ]"C'iP/uk =7Y]|B xĦ+f3LD>HL0mU!J;Y*r~m|VQ\\Y",D^mo߱C\ώ0J=q?a_e){_58Eˠe+Qރ}"ERD!=4ԗ 8%<5|lYwd8K,6T7]9C"uLN_~zNNIN #aO)W6[z[[Ρ Kffےw;O6432҂hos\ݹ/7ՃzkP([oޠDC$[ʜgdgEhG*jJ-h^oHy$[xJC1I5FҢxJN<,dE{̺fRUGhnu@j mBXРG=${bz k_Zվݖ$_:sx_i)DV&[B z 4 O1J&K$% `TZq̤rNquM<x,E]VsR(EV|L)U\f/hRD%{rdZ׭̈́O5IV]j=/8S]X/{8⛢zڄ;0sj[|?, F<[w."p: K6%fDks˹f_[ 6mt9z|ΝrIA2lJ̠D !Z߻yN|yS-=}~N";{Ȓ C1+8AC|,貶i$+Ot,GKP+/KYXUټ^~.k#+z)/_|e1&ZvAKj?=sH ݬxG>%g! C-G7/t>{\-Θ6['MjNZbKHu]iv~rZ̡?=փع0>,Y5x_Ou-Tš4|Gxz`˂S>>yRE$/Az0U6MKUzޡ) RoUW%Z*`Q(%})Wl:̄OS/&PBV1 kw\]"8"PbdKlݢ':WR\쩡ڨRrc6UΔ>TK\SP4XJSU.kQXBn*#kCS6CZ&=iHX!fUS"\RsmHEWVsPW3~W0  Z ~1;>(ZPRPnI >   n K$tW{X~j"ڣ|ㅆpʀχHq'$T!)2|fF5 O34ZyV"\L?Ytx n&SJ xyإOƤ, ${L 2Df @"az_=O x3SBIbHzx:;NWWG < $/2VX$ ~?\-QUMf5D*EKdŤ0H+flEU|&݈ ?YoVvu]GaelpⲉF˓B'\,M&a\d #R%6R0nbl`"2ogjآ: ՄdacG+cv.n*( EqyHNBx >%ۨzXO =[-͍͗Oy[~\X )~݂KKi(dy`b:}Aao%VbMkc[:/&"@6M=f/H/S~!"Aks={`Y^sT'@[oꙆs|¿L2_Jtlj:,? x"{L )\lhM6gb 1qe\TlQn|d%6+%.ISi\-#'ԲEu~\LR%FC7fa&0zs_jАGP`|Ry:wNMǍA= #~ oF2 kO>u['?xYӦ^o}%,/uhZt{BVtt#%{C:o1*Ԅew1 ,3u7N?W~gC~mmm&{@qGJ'{&ApX/#lɆb^,.Kۗ4płW=udr.H\ew!Y&%E4Bv%h`Py8 )7RhRÜf( caKU$5q*9H #aVaFqrDxQ }T($9 DS)nWy`'z411Q3, ~Q[6wTS  $Wѯ>?ѳƔꭖ*=[)<ǖ ,Hr?#;HL68[y,.f,)?*u?PU8USj숋}p4;s[xg'd?$]T4 mK×ڃ)ܥ8,;aQ\FVg/P?Vx2$e!UD) Sb֬t=zːo{QϖmxY8F{r9b-(6ow@[Aj%y.+LlQ (: d(6l˫~z`>$ dMGAщP^2dA=!=#fO^njL|l&2fqBry04I 䣉,y::&qԴ<Z8纳A;^dcyr١1y% `|0D84bxQMN7VjLN=Î| (_#a䋈0>%*Ox(HDHa|JG)q0?W:d[#"x9\-II\?]6f&1@q ҐIҙl*öo9<#%?hUi؟.34Qoā6땷S&'Z[Ư3hJ|Ce0ݱ:?kϗ?%i<ϟwwط4Xڏ~7[*O6/yo^tR2e1C?pNN0[+ZA4aJe7N|zo]ʽF}gfy8P0 5TQ *'\Aϰ?Ϲ݇egFK3,-$PIx^^+o")BXW-[!C#rh, w势H)9!m80߰2m0n=OjUSoy*Y>b[l[s9all[vGcɆW!3s~FFZ0q\mK;Ey!z4(HN?,Г6]#>ӯ@j+c:TiP@ Lh0~$9OWTXy%)\)RE)\ \V0>>}W>B:1$dhwD&wNR*3)^'# #E2`\F"a g:^mV&07/ܰç~/Yx|ǟ}?K+Es3$N}-5IJO&5u\;~JΧ"p=ZJObL@oI)eۛ;=.7,臵}`8mg$9fVMM/2&Lq!ˠ.$rS!OR"9XTzHi>fyc9ʌ3f)0=bEŧrƕ0w:_?çDW/]:>j$I@}*V\ܩQy%=Fs)ʸ`S$1У2HUX(ERT? a!CLDEHXer(x}9!@4.E=d] SB4!^lY<#OWiH_i6۔K~GH $` q[36 09IHB $HHN"Pw:԰.c|b A @H $zk4cGxzz=W @H@[z(ahommohopb$_Po뉽 ̟@H`t ]X;@H $0 {@H .[kGH $?yzRFrkFRWKKtAŌH $W֕ $[޲2YRmϣ,0.!/ :y_f_!T Yl"`U=ZXheA{(Ūx i(P* -C|Cz~ž P%`Ѝ*&V)4Y#L(ڐԗy(c(C_bs6fOʗv(ϩ!>"ty{ؐ.Ј )PCxhI!Cj B}Ĺ* fQOIH{V\@H $ Q͋6IENDB`F3~I)hn?JFIF``C     C   " P$ p  <7<V<^@ na71GYpy:Lm ax 67g_&AbA+9|y`Mjvx|ej6(41R:}K<͌]1R:y] f9CQ#ef_y1ՏolzOr}/p溚㕙/c*n\wLs^} lv.Ce-n+Jȳb˞Op6mZXޏ^L[z}'qȊ ( (Ip:nK+@Gd* )zo?3nYiXZkX7ySrۜ;tP:.W-]MzǗv v<<*흽E]f>7<]ܽG7OPׄ~Gz{FyJIԴv23ך|o;0=?[}G3^>oo9lz#{ko+neEmjN\{!;U ]}D:kѸ#9a$rGe 8D"YӃ^G*!ܥv^+dfB#lgAYBHeِ m tG,VBP@^ خcn?Ϧ>LJalH'ͼS&<H*(R =r 2wpΧ84 P4+H+#F5AP@HnBzBS|}!]mZv+SuX]!fee=G3Ԟy'侥G#H1Bj1SrrUqmdZi1" 8PDPSYBXDF״c^ѨA@. 繓fkyh*eTZi m3g(5z[={cH3X{>=Ь*OK/-zc;gC^v[GWCZoѳd;W.myGIJEZKsFBc#d#k5A*"^W9pb+Jִu1N:Ze)m;9mu\zcgyC$r#$ FHdJ, E"gDƲK DJ(G eF^nWpUP@A@SX"(刎9#ǰk\Ѩ4TӼNXRWdN˟ SoYJ2UettS}f> GٝG ~&SF|~k$iyIBvO|^]l; n'κ]0=B[W,X2M" R$_wa+Ny9PMyԓ/S[`l( ( *M E,DqIrF1`ֹQZ}W}Zoѐ$\u|A˗%!rzZinvk\vs?J\t7{N7; !?/ϱ3q.׃w1EgdxڸSߥbw</s'OQ{=/;E<Ǣn=.?;I\Ύ0t8obdecֱv|W]aeomQP6q|NĠh@QPPQN)"#HE$dl|c k"+D.zOC~/Ѽtиt|E)r=F6--g[p#?ynѾov|wduQ>yj؝t#z x18=Wcjfپ_ca|h9'}Pѓ(m-o Ifg:U/SJ֯C>Vj`qӦ ҈%d44<{`;EPAEG 8EU6"871`9Z5ECA=} Nc ^69"{tgץ^Y׳ NZzsy+ |WȞ{ov<^t,97O: c-_|Լ./aKm鵣&CklzPkf}gCMjݘQu<)j)RiZP (  ((AUAUMhe9"#H14k\PDT;?HQ0Y&33e&IaXd'ۥu/`n*qݗpzF]}ӝ, y"bIM :^]ffiq( = e5--x輮>*LʋϽ15[5YCŠ ((p88цhb2(䌍`ƹZ戊"螱B;CHN#Yc֬gT_ķ񽷟ͮ^/opz[64rgν4--yG!,M3IBJ( (<'7>yŹ99* itXmthzG#lBWxH"8a^sDEAˇ{g{+|[#7:9MţaVg)yCM8ygx~sT>/t=O3Zy'=w<(D-+:fnMk|4xz`iy=cթl'<0TD@QD@QUEd*G}[YٌKȸl((( 888+Z H""I^эsF&O@uG<_}4[ZavIYG׃S^a`Cb+/y O{t:..j#r"fӇ9jEr_$QKhaz>>O#/$;(%QUTT ;-TpGxxyvJ+CbF1hֹw\/ü1^7n2Eos;:6(mشP3y7|C0J"f?~>.t;Ŝ\z.vHh "(z/!0 ǽ˧~ii/,빿Wǹ{qnB <}L_v)ϴf=ȈWOMLyO==~O㥫o>{.x*!H7wFṞ41%Tr?`D$IBB{5ʵ+AfSE}3=6Ghh&Vݑ R-63 +f/Swok*Ǒ717Nsp+S(v$ֹPAPD}mggscږ95|9DzH5F\4@Y\;^/ܤT=:؞OOU.<ռtѷCfR %@ (<`z?IDx$Ȓ !!-{uJ+Abf9c#c1iw8^ǟ8&'GX]z|95k\D8{fO?Kwk3WV,G{K.^Yɨ }yX)Ͼ<'jsgM6xl fikI d:rhuʸӜycr8X^=OL,BM %\Fj $ y|?˽mEz˔t=_5ϑdzY2ztɹtZy%z7hkrF GEޗ}}9fL:fN:vN`Ԛy$DprF5%Ha?zא[fέ-ޣkᠢ%Ӽ9|^Ks  k^[UKTc :  F *(/ J>96[lMsUg&1Ίz-lDM)2snxsy5jcgװ;jDu5c$rEf^XP}䴷C`~Hs#^>FJ:VJ>VL>h$O+!"tcXAwpzavfWG ,6;XCrt3͛a]^ξnѳD{0hٮnt zҖviAbHQ%ȀTT$A J^߄41:WO_u.3G[SιL9)!{AWkuE 9k=#$,r9I&bI✖x%ɣc ѲF+DbFDEhzD;Ñ}2]:,'u6lҹB`j 5z3R*_8m}?VX(ejd{ rF (( *UrUr&/|+w?z=hTUDp<{2BIch%)griMf 0`4`4AAP@BuR|3Ba IyZWjztʒT!9~"ȽC J0QU*z8Wz؞\ teLuc}zv1T@B-GYqZYlddQÜttttt0$L$0 !q:%ZVe¬v+Gf2V+Ef"Vb+2W"l uվ*XثQ)zuY[͵a܊5;=df&9 VUD",V =''I|^6XvՃ$Ë|ܩ_Gl\"f6W Ѳ_J>[I,}qO`Fb$$Td@n֖"Y-bҶ6[)j6ZVe=>ZChx6R_& N{%?kOay-_u^c(2 ؊VJQי&K^ieLg޹iub>L7'0JB܈e2O%[zI1"%/.CFvVֵ^g*>+953{ zߤds함QεL/FT8O]r6CۖogD" Ѝ4+DAP#DT8oR#M ӈ*:&&&h*N&hЭ8DAPqCDgƨ??ý}R9GF̖ +eJpJ&) o8]mZley:QF);J:يϵV,oHޫu?e|qLQE'܄)awk!w-H,::h+eSks\:cw;WOS-[+U`իSU͑~j=[g+*WY֣3 A R *wZDUZb:l,KGF&B&TK&rcݱWRE1C~[r71LF"_~((p J _$ewgV3VĽ:Ig%߬v5dLk譋1.[oނC&[5[_5lwDmӵuh;wi:%z(L^Lz%}~t2zXHrr h̅#ylllVIhW P+n;$aOX{!1iQG!Bc!k¥ϺۿѣF4hѣF5*ݛzJ\b~ŠIǡBۨ4 &g5(n8 rɮ^_Vyczt-w"1]YŠM !ˮdP"rZ}y|ԍ$zꩪ(\C]S/1yH:Y9 o7F18QK/8xTxNUSߎܮyOxR0L x/Nmw6c|;=+Y*cE5z61]ޖ:exwc XQLlGzI*W,,diBZڧwO=~H2MlKZuA;:l-j^JM0$.jԻl3ѣ]j152u`8G~uNɺ"o'bωшgHM_\%F]w>_;UsШmgAl8ٮ#bMLyfXن.R m,mbA{*eO~VkW6!qf2ueZJ&Ō}dǘ,V3.kF?hqW2-M?FGȎeC4=լIWj&-4bBT$'PQw>[?8N'Gq4q8N'[q4q8N'_Ƒ23)R͙-Ϗ%ixK'oe4hыOJHq=pJ"/_BR##Yy_(nfF"cX1.ܵ;XeNyFJT%lDB£?Q~j= -SFt7 ҾDNEUr!f~N^SU%BT%'_?T%-ܒ}ڔdTf*xc ɺ;%i&:BQE_w(SW&BNʞ3_Z9[׭']diIIIQyRH.+])V5.đB$$>5HY¤𺼿TQE_ 4_Hfρʮ\un9bgf͛FQDb8J^U<_7 Ndi?zA9i Uв Lf9.X8IG"J(^' 'iAfJ{< ("TV9;Z7H.Rr<$ Am~S軵tf&:AB#ѭD'm/rH[3?y*bJ$GnhCo_UNr 㴄g͊{>AhNнoq8H׆C z?EQEtی3;ڏ:prf=)KM.c}QEQ~]?UfUj@I%lL؆rƻD1?xp/ꀭX}|_;i5%78mҫbH捻#Fey<̢(Z$寖:ЍE!*YK}yQ_?v⨢pM³!7 |'z;oW(/',y:}7kҞM&wA8pEC䂏3_oЕ|gVJ٨^Mdt}S k]D8NfNri~~/R1)ƏCY' yHRo#aܬ !M?/J3BU7ّu:W{tTOz:%8Km% sJMaүh3\w^RN4zΫJ Yrubdmr?JdyQJuZ2bjf|XX{yĎ"#NH/vGCTy_yZ'*kz S))Psm=`__ieVMG1Y^G1EQElNOyfptnãJ7!^}'cn`}lCmF I:2?8yYdf(p8MTeAl8k"yܹǹs;aF~[é oAa{F\F..n&ړѪᔚck?!EQET#; F WczqWLuqHzf>VxJtG\wf0P(~d*SRދs(Xt{.;kd|X thѯw4 k6Vyl5ќS Mл,%B"Zsj((G*~"롋wsVDA (yQUq [#$fW{}+B-e(k{% q;8? #F3H1v3$])K= HGkPvf,$1'U>oO'yK x극xYp3Iu;;BjUltbqczV/QEQE8VF0tNy5Ej/zwwR!ަc6 I_[!|}|GZU~!5~ds/JwvUdlkRR*Ѳr}:lm%ɋt*A5jw(cawg,K9բ6 ƈÉ~r,v/r9g3QEQEyA|fd;v*㣜oF9]?# (I i"?'A:}w4E(42ި'kt1/c`سu*Ǫ MPZ!PW9uU*;)~[`ޞt{,v̓ʺ;qk/:v~Fvj1:;%q5O^7.}sf̫{J?UÅQESo]gfdXs+ݑ7$qv#[`=:ԁ\#zu):z"c^(fbrxRUs0!PBդ7 +,h>$%[^+rVw5JA(WQ4?U"hC23tl6@]媊[߁%+چ=ޙ=^Z_8QEQEcxE6꜏㰼eF[}5FHh# =ƶApWJZ_gBA% _QwLP54DS޾*͜Cđ vU6U.cTQD7<{+ybN. -PZqyF(CVB)Ϋ ^^ ((m#3/-'eΟEF- =/He"ý._Jt?!LP.ȋ?ĖNvjh3Usx;'qQ49_`O>:te =GddpOp9M6r6HAAA( y O1X3BxESաȳ- HHeWXBa /Nn,ɒz:(*6r9b}{zHWO3 rofD۔ӱi!(N bO}AAQE~ټS|hmRgqK"bw,sy!!_Eb#%'._ZQiMuy;'*prJQ)U5h:m&f+V$Q'Y}lM'9f   *(/n304$w|^4All|;!QHdCXR /H>9R2çKfMy |G3v(qaeAAAUEQTQM`|U<2pN'[˲AAk6-{qTz)"WBDV"pCR2x-OX(cBzcuc',1uw;fІՅAAAATUE_z:Ҋ/E/ >{_ (D   }X[U0r;n'1u!]Qډ]##G(l؊T_Դ-<(>wհH,8LT7񸆍ѵbhlٳf͛6lت9G)i| Wn_ѣF4hѣF r-HZ1)f{.3q̌lG;HwhF#:AAAXXevj–-ew6lٳf͜G!Tr E[hѣF4hѣF4hѣF4hѯNAAA>Mzlٳf͛6lٳf͛9EQKyBF4oE=4hѣF4hѣF4hѣF4h H  'Ճᬪlٳf͛6lٳ}[6lٳf͊)88HYQF4hѣF4hѣF4hѣF4kAAA_KvlV E' uQF4hѣF4hѣF4hѣF4h   'ԉ9J6lٳfE& /(F4hѣF4hѣF4hѣF5կAAATMz7տ|! UE uhD4hѣF4hѣF4hѣF4hѣ_  'Ԭi/0Kw;NӶi! L7ǻ^?E CF4hѣF4hѣF4hѣ_O[UY(}$AAO5N`vOCRD;ÐJ%-lGy;hrC~ FGx (YAD4"!ѣF4hѣF4hѣFCF}t6}!-fΐ*蠂  ~'Si4wu;D6;e;u;vnvlvī<#p|Q~  " hѣFM4hѣF4hШ* Cz^ {%&y~7UZ/eQ[aLN=.BkѬ%h׎vuz?&GR`tgnl~oذ;v/8= js͒`%MUAADADCF4hѣF4hѣF *u/C^V:e -̔EܳVz/y:Qe#Na CYAF\u\MY1١.N)0uC_3TOtvM;Nj"'#Q:AADADAѣF4hѣF4hШ* (3A 6_ zռ5ΒKnYFfUlV$Y3H;ﳫW=NG5Ȋs7^)  " " hѣFM4hѣF (/K%5`_tg  'H   " "4hѣF4hѡPTAPTQEPAAAAADAD4hѣF4hѣF * ((/  71!@A 2Q"03Pa#`q4BR$Cp?V~&JSOVloVUl318ecao6bV6Djjiq2%%e7SHS71l#]='Ua6W.Rl;ǪQ.uW59 I9q_1Q)rZOX=MZ-1P(Z *v#aWrle#q/iX EJ9sD*^Օi"Ne-B![9I0;9[, JBII?1Tq/hS06{8-J"0XJJLU<%:g13ӸPh"/(߻3eVT?{CP8hW@UFݕ ( *ѹg^^DZkQXa=%*Ѐu@@ߥlG8*2ⷦZ?ByX,]⹅܂bf@c~Bn廔 XKuݢtE ] &w.見5̩)u@Aqx>_1;cyL,ݦW[SZu(RM E[twiM6T2C>q :yI lKl>aۖ⡹3Kqx:mC-~8iSC0L]|M#CLF= jnðهRwƈ\L.iD-O*e|a!-x)&Fu[xQL^laQoLj|XN0q)VZY;B)6שэ]~ʨض11\k f3+&Az˟?7T]"33t}2L<=,;Uh>(uet6 ̄[~XFɼ@Ku 9UL 18m#Qqסv3!@TkU_y %1)ImwWRfٶJ;7( anQxWS.Z?b&;cV!`!7}mWDpbhӧ@n^gS Ɣty>A#H*zt'vc6p%8aF()`#s2m*MPW9n:LSNrݳ鱛DЁaOCQv/gP97hčKK mԼ/@Bnwa T-ܙ%JըB#Y }%LMu//_/84ߍ|oY>B&.%Z1"`!!}gF204!q'g240hJBFڌb?qA?6ѪEןibxi]Lx~qyyy~A:|?ߢ EoÎ!F&?8υa*mS;Oo4i./Akû/N3 |/iA3BְČH,_8|;t+y"`Z=f# v# .8BH})䋐:W|+e S(lO 4 e t/bJce%lҖS@YʆbeR"ikyyyR׻%SKd _/nʚ: (!RFBS6P,69UQhgt)*hbGUplVs4={gavT %+ *puF'X*K\ VR:vk_U2;Zc(V|S )#J@_*44w=ӌTTKt68>< >ukd! JgZ)€$8(6ṄC\È[(4Xu2V#{r%OBOfKFd0N͋FGI=BSbԔPA(8si3\ɫ,SGZPhNrR`U " I=RG>zH =dFmMt]r&P!O&HmU_$›MzNVO,IHC1ikNSk 1xCwoM):rL{pTB#(՛)L39̋. sOT/dġֳeCj͊I>0XM c T0L H $m0kahmدtۀNsQGVV:ǡq7;či#IJgtT ʳX"<]6ڜ͙&ŸǶUάj5 ;Nϊ *ZC-U!6q1zQ⭗E>WŇb(,9Igi )BǂǴՕʼ9Ֆ25WzkRW:Qc;aNBwXMS'p)IjHl2U`ȏL\9+lNkZJ׋-'z, ~ *x=6NaUWU=!ޫ|XU%Lz{d.񤂖ݽQ?sgB29Du XLtCD|># s(^ZTt3Ŀꎈdr soqܚրU 4df.TkS6lM&B PF~(ۆNj귂*3zEXpuUHM{HɢjZ,13.hRt&LL&0]*tU :1hφ؂wPFfEZ-b6'6g,+5M*3g!l7ZH|yv eeU CKDS0|4Q4n7!]ͭV8 dcQ\d-,&vn;à9B ^XqU8Q "ydívFM/h3F@X~>yVLԧbc%m$\H2ySZD*S2RSJv+Lg\R]Ta#T7I;f*]vb99Mn$>9 xz؞@2?>}auC}Ȕ \un`0^EњZH%Z i$IhK9F7(sX3'4:  Τp 1{r,ƘNUQֲEv^mLm=*ث,e" 1>B9ϊ1ǂdVG=ҨMM^eRF)<\.Q`Ɖ̗8p"V-6z48Ed &#TǾ R!D!׸HkɊe1%c- F > mr6=n*8 X&=|3`=UT<!q2kp?L&l ։6{Nn2Mn;3X~70ⶫL|9:)ũs 8L6as; uZ^Ok`")Mڏ926>-i]ڍU!P˷ou( '$4mw;*<ȵVFg8n`NV s>"JDH|Tp֓/Jv"9:pyv sQEZ?(HlB׆0%JF4&V϶[{+V kT?n%Bk4Z PX~ɽ: h.͜H%Jtj΃ޢR(MNsTX(.PCa`|ӚvќʂgL*{'FAt73s* ے?"X26 ۔Z+hiXRLvMEpv(t/ֹ@G5'> %X2$>w30ߑ5.rW<ȇ@21xL{>͚eZTa%8&BQˤT*A(p`BCf ֖+UMflӄ:])+J5C4B2Thq"ߤ5JIA26kMVR5h1M^C 72j7 ҍ .ܩxQ( smCp2BvSMSim(!Q*^$^JB{KY %C[5auXWo9ygT랊=Pv5gj0t QT)>wh?@2}Z;)Mn%>,SYVnUhW7`2kVxd-x(B%Aeڬoa Ė&ߕb:&J1cnl8PޛOS6m UZk}{7s[ Jei1? 6Mݛq? u.iOveUצ)`mE·❝$ҰMؕR[҆ћx-l2xZw\nhp]}%g{wS g;(QQ3CX;(Y!%nZLZ3a܋x$c&d߫z$j8fn9FB{w$[5)V @r:+%:,C79s=*g'q9nC< d11ܮɁ\kٿEZ"72l@ l ,̊Z2hVu<hMZ PX+, RlUv)MfFNcB9D/d#̫b;\ 2kѝ [T$w%TTg75w(QZ-Y?N9Wg'RIoE [EQ&b,]cngpxM¥6G6bB`1AJtl\{$\TC  )r 'pB%uخZJB̑xfV"N+[R%+SdG0Ч@9TŇHt;^y6'5>zE %5g%FD-^y>7Aah RwŽi5"XÙR oyF h#>Fk!iQ;iQeV#U-Z4~6-prgZ<27-VEBъB歩-:+' O7ЫVBq}{.\: J7Tg8dh^2@wD2:7!P@v䳨G\; n3RoH;q MQ݃J'6;S'~g`ω6V'i^ȶ񩰍*$xqM\z+\;ZO'԰ۃBOR"v!LI)YNe^-r 7V%̸NBjmZO':bJ,YqZF|%ԍh\Qvef>`t1!o8q6iv_U̇ãVxX{Z&dV8)|ˈŶl_%c*֕-ҢAcJl23 xMBRjfhO2QG ŭcq*#OщTqTHhU̇0=5kk욛kAv0bKP1fU+T*Ly:&O 0dhdž;U #}Z5lhG5E{e+V|zrBge(,Fc$_h7rU +7pPRh.;*hfX!M! vϱ۴]%|kEo&CU\ E~Dڨa0r.+{Id51G6M(G]%_"Ѝ l^ƿtY7QG3 \Yv#e6NYeb SrKk55\0FT0&FDm_>+JJZZxثnR4ta@R-vNbH86"c.U]P4~W8GK>PyR-çA^5=(Qe},*;,/2iMHjg`7(樏i#I J*P`C>V^/vD:u\bT$p>(_,#a"0Gr87=@ ^QgO(PfAMB.)-)?&KU]bZ]& a; |͑* Y8*s(q~'rR"W%o)ׯRڇY9n)[Н?ņn]J[#|^wybZSF+̂e!bJ5iWf9Im;y*#WCn.Tdg(VEM.jȇ^¶ i!v39lSD!8$%=S )yBeL<pZnKF+kBeZPC?)T}7)zTG% UQlRЪܟO<9oЩB8t!U_7 rtZڥNf ZU?ˊtEnXTv^)60, C(B njB`~'o@Q|aaR_)ODqV.:i1N;Ԗգ#xLd1?i{?B` m(9pq=j,QnX"ⴴ l6}q׭0eZ$."k)tF5UPz^U~nd jh vt7J]'wS*b+Z/ʼ̌a~X_/sD;~;pQݿ#x5nZϰ #b gmrvG9DmF~J4k2 -X%Sx VRSuB RCo TM 8/SI4W*|(z Yjm%DVÈm-*Z֞ 1%R݄'}S!#NVA[kslT?$Cs=Ff%>f>^xF `q pw\0}6Bp^h;Zw+s/3" Lc:vނ'djK됏GhMYsW~ Uv(U )#}ɹ9+ǞK%ܵUG,1⭎R;1{(b,~_?ׁweߢ2a׈DY殉?%|\2aaV;&,!k| lF?ԅ"!sKݴt{7O$&2xQ#Dهw74 9PlhΐR1sS[H v`}ePr\r!6~xM+ IZ/j<[K7ȯjy>[R#ӿ}txxNo+NdDNJu218994sZ[`uj!?|*C6/(FDG|w/_YcG@DjZ6TklR(Tg:Dv }Jn!rdh|4\い4nR='=.Ǻ@Y5ix%ϪtËE)fӣRa„F|Ơ@Ҝ ڶ.VM1>kȵVZL|HlQZMʋLEm6K.Țhmu$~CFsEvǢW{U% XC&ΕU5Ԩ0H*=8 uay*DIAJC\$8u& E6ޝܣG}"Z3]ry*!Ƕ?JPТ>I7-*M?h-cR\h1&NuÈ6D59)N^"JSڋgcWoH z&lrFV24:Q x+!1AQa q0@P`?!gПC/UH"גW8Tw\fLZYd%gĒq%DΟk1671鬚GBVD#FSm~cB!?V. AOY. mz|Z ԹъRw +bƛIXŢ, PzOG L%;cc$ei"TݎcRT/ndaʆ{F4ZI4Vb'09,pm?CJ8$/BAY{r`1~>dMy"b!1CEyeDÚQ2!(d la̡# (6]A"{i.%"cb8_>)"&,a)ԆfsQX"MI *zRJQo"|g]ܺ+>ټԵib`'2 .sn)k\ȥmSA ռ!O6/DKqSc59^S*̂-ՆfPYd;3ocWQȢ“؋ 24oS&N J%*ۊ̮JW:n=\ΆFO$NO6?k _{|>b.KXr%IQ1BKEdȑ#d-^QTG:N$]^luŸ;*ƢC&E)Whm2JQ;IZ;Eʲ܅mCpGj!ol)h&&lA< ?~b5\B$pb DӑGvM\HؓXGVjToD%'H8B5veRR5٥Bc5;vEe _d57D?JWɂRBWh!lRvÉ؞uQt0еgk5{#xdLy|`A$^U~PS,O pEDB܊(F%ZGwK_ TY7&Ovb8S=0,AdR&ߍ'8ER%jVx*($LQJQT8}QDm"niK:@Z 1l$e4P̮FbBU4>k)OQ_(UYA.IdM- tX0J6~3:6r2YRJ"t!:K;QP$JE@#"5]$ϰvG]tT2m9͌U&>we@NLP}BCNJ6SFlNJlr^mBHDm (АH|d]$*%#J<#R:EV&Qhrb3cJ2( z VV7LkpBRH&U1AaAAaApAAAAAFAcFGR%$%ya6Q FMqJ*LMlBцAIvw)l{iX\S-ѷ@ذ 6*U 'QAQd7QJE5pT<ֲ҅䴛|ēؼ,eVEp܎Fq̕.\`֑5A;>Gv*D^ZtsgN4q‰)HnC9 ՊUd?S3IYT9r67ҳVٱ:#TꖫT!ulȍ*S~GNߑ|[L?{@fJ:ImȍlإdZ3Qk42$cKͽ.Sz䙠%J@Qu,6/nsBIFS7$ӠbtM|;!H+. ·>nq“<_&ytdoGI;!i&hzUvc=H"jRDyQ GI?b!6r/oHeBYެx2eŐkԽaiHôvi_J{.&5Yڿ\ʡ{XΨvZ,|w *!$ 8؟Q_mv F,b2K/!lߡ6jZakO?z-D"< ff*ٙk'J+"ɡeW _J Uo0T:5z0lVYEd_/q+\§fK 1rRtE3dI*<(3m "^JcLikXrf#dyfIzQDDN<j7"oݔY!G* J"sZϝ Q:Gjj6&\TW} ̝̚ŅdL _:ՓEi2E2Ђ@B엕݊ nYs!H @=']4_A3-T1c+$d.~dfrVU}T]F^0׏Q׫~WEcF*:٫GH;i ֖JG&răzVY,dmVfIEBSD:m"q$*I-[I@Tq<$K&ɿl2ldn9!=aqR+ CI+ZMtT$P!: -B"S 0 ` R?#@h#Zl!40:mscѫqHL9K%BZTn?*F֣‚\;Fȃ2 CgHS-$]!su, ;|J[yhPh%KZw\BDՑWn%Z.]eCeD`ק>J"0)CRv|6M 0RgJoLd%n))zYBZC}Z?'@=Fde(RG.۠Bnka\MIg(w S6PJF)7hUo/)fO -$5< D4444@|jT$W)Q>)%e6,59z2KdJSyEtIڅZ% Kw='[\J AU09~cCCCCCA!)--\afOVߝ"N3O)&BP hRrwe%5cQ#9H,f$dr;92IKvHWy SR~AXf8# X1G x> ,%55#dhd[b-GRT1RgHT y Y6R׵5\SL(u>&J{NLNL3^pW*_ЃCcҪwͲd[UcB:y2TT"DMs_N)RqLx ~.p l(: 9)ǗMhhhhcxs%K-I(Jajxe2ꎭsbwͷ&E?In0( eHDU_i˔$gH0r DX#lωCe%Kw|g0A1| x)=.͉ SDo|ܴ͢,P#8(vTJ,)bhC_l. DX:J^_J(CԸ]Z[WQ-ݧG.xO#A cNj KV̗Q`'US#DHrD6m8T]Q1An-ƞ~Priإ. sbI* "aSJdr&<]Cb#ԏB8#1W_!PF4٩mvīWeSHΤzA$&\i-it[F4| `Ky$5TUl-a !š|,kWpG z>1c /eڌV UŮQέh>WB[J)$3'+!1b$nsCU (5g~IJ}%B8kV%%: ,/T>i}ɞ1OUBM95KSd gEFIo#6U _Gq:2}5%~M"Hiz=5o`<0]zUBY.ű QNF>41!6XZgb\Jur:HNȖ=݆`+9jц;E~J$]n ʂ,7jl(1lGF՛Dcb\2pDa& Xk9v'_VEwKU_Rgb GlViSЛQglqt.\9\*t w=cc:?pS}nXDCuÀ #J[b,dXKN=(2+G ʉٛ㍀.{3eW{(\ V2!]ǩ4k ҜM-@&!삈2:.b~O!>r+?"۷XN2G;jp(Ud=/LJ7wC# 011Ŀ(3CM2Mg"a4&#n)9n[ jl6ׄ #71NpP=_[`(.CDfr4a^mSiV(|{ٯn '['r1\1G1c's |`ݘ"F a)aȨJw 1|oj # B/f1UQЂ Zm-ˇKEC=ʯV/j{~ D pcN}SY|h!aihV#j &+ #=>_ˁ$ I|v] F&c\<4ꈿ{ dWWW (!P eIhF Ӂ!!,G1c0J|p?X+4"=9<$n @BQ҂j,a.ԊQ Ap hPlcSrKP%.f<Б@RDh $#M0oe2$A$zh#1|zh/aKɞ -f`VBh$im;bu-auLݮcP A`'H$yT'Lt-Z~V[6n]C*g$S=˅FHvJ:>?02BHjռ BX$$$$%1x1͐ V?l[{vFeM6N_QqA ˪r![BR7 %xY*uUeU!-X{v3-B8b6endNj!Z$1 wH!Z/Rw s& A"ft,7++ރ lOa(!6 H1c;2,?`v_EQ|%*-z& KH>X-'BnLZYf$(XZ݁ذ)x&CAPҷ%BY\Yl<xF,d R \{1g!ScjMgY4TTCQ;Mv6!!!!!!0'c1u!Wee2ʃx)}@lAu`K]I::B.H}2i!L0xl@R_q^/%Q!"1h3݇MBIHvgv8a>%Hjt/l@9wMjI B, 01c,L (.v͚5Bw>QR)%Y,!]ii,=r3aK V/Cx,OI$B h"dgB"Tu.bk|}4g)/̬Ćt듺12P|7uCEd}*1d_'s|aς-Af烒7yFwL4_0uֽ|B_= *ws8)}dm4w1)pK|!u AʝJOq%4yH~:Y2cLiC&<5 -gwT%<Ɉ7ӧyIboW>s֡Y|d}k/| j{E($\Fpu.hQ#%ݖWOnw/y, BQhGn*ׁFRɈBBBBBBBBBBB (&#fy{RNnHky9fk&q9I㾟Q=}(57i`}!4[h;pO PYꞨP݅vtSPID" 2Rblt Z܈=眃s*)zيQ QM,'Qɞ@,H8s tVq7qV$/up Y{I$I'ٲ! bQpcdž+nngKbH^:wpl5fXB$Z#q]vlkg9c8\^q0$?\l˪Ttm W BƃL&[䨯f1c3#}0OC2Iن :iQuЛa.M&dWT6"rAWS'}j ܯ`2xjfBBBBBBBBB`'^cdžŋ |wKE{O(O|ίW5$O |=#!ȹ#O8\Q`<>8> ^ ㍂uV 60B'G a k!2 ~(gZsD4yWlPIJ]$!!!!!!!!_C2 ZS_S0ވJƓo^UATB oؓ'IHwP811،= l1W0Z,EiK~ pO =cvR [WCy,0@r_Ktϡd"TEP:RSSdVBkiit%S@s˫U9`Mn1c|1w*EBzЍ2WRhQ= !!!!!!!!)n<&ZZa^W"zDK܀ԟ94"H6k35JGDSBeQj~eՁtc%pv|Xzyx+ET1˄j@V٪>r'O6+.L7 T2'ɉ [S0 n&,G:E|1–炠_ #6L%BqIKgM  71Tm9_ҩIV": _FD{ȳ"Q2Ob?8HME7,)!aFF72X2X'n"<C1&uLbh?Ц[b>ȳ<|^2$vsͶ2+\>A;H蹶K&gDw=y:AڃyP$I&"BBBB &!q1>QCz\skE"pd>-m#iNEB-ם t2LGj!n*0ש `"<,7j x4\8W7BUFo<-3;*PCTI$]1!!!AN17lcdhቐn%U^wnJM6MɁQFҧ$jryapID+6!(2a3XxEX.T6666Ik3#1 cMdͶJ_B@5um؈S\[vj>G-g4Eb(T  G$$$$  `'<$8#7.YZ\%wsA~YCج)T%*!p^3Ͱd-CfV\^=z,jOc=S}6\-Ηw> EYR׸kbrCE; Q?B   y1#_zI'0"J ygZ~VFU"4d k$+W)09Lµ Fp. Wq,΄ʤJȝyűO:&;-[\w%zQKĉD:6G9.먔Мf15,vȆ'jydl|+ N ]HHAAqۋx44A߀@7dFlW If}WZ9/@ !`qߖahSX'MX‚VE I X&f%Ԏ(e̾=]da7MPPJ Y>N/>05A4 OsVLu19rrg$#ۢ41S!AAa>3 f{lQ< _5rFKBB!u 9`&Aba؄2 "d ;oAIRPB{8LRbYʢnԊeV5!~JyPvjnf>^P%CI ܠy-x6(MF}IKz! BAAD\J3`TI.w/r%z d ՈB RƖa I]`_n$3% -܌\! 747 r3 fɫĿ)>MېF0ZiΛ̊s6׈U]Iy4ZI$I$! A0  .46ȏ"EQA̍"F*D)MI"uca4ysI2b+ I'8`< pDEF@`_[TAaĩq,HH%/_v&:4Wm}MyyBH]X'B!HX  / S 0)HQG34-+SZo-,#y鏄?t\$S 3>(XG 5@&8x1JYCX$V145ihA,LRm9,j!աB#p!HB&QE^ ac$O`ʿIM/oIYZtN|$lx AȡP> P1aa$ bb MSݺkmXX%A-)TܺjdR@8B! &AEQxto~ :Mɤ2V$ )[b88`ע(򆒖2o~F+  &׆$2! cɬ#@|*z`F(&6Ԓz15!\QEQpla6<N&BJ?,ҙF1AA@ԇ7):%s*Nn+nJ M"màc`!X"p(<&V"E$鷁$8 l'$oea^0K Qb#  !ԛu֬IJ-7 bYPZ+!BPQFH7Ԉ9/&hPJ`8xU߉OȴXY=#Ђ#  gFa111d^[pAĥ\ 8!BQE>Wȗ@0ßݛ#;Gb~ƨd/EY4+0   HH|zĶz 4!Bpccccx>?jF5gݟW1K; \ pV>4VݷnRZ-,z9W Ђ    bh$\$Kw X!B`8>O#z{HҹH'a>%~/"Q.E£Ќ   K\`A 2 <ʜ8EApB`_666HÒ5]!+*_}?Hb!B&%ߨj>u,*\"ZZ1qAD * ;_q\2&܃ZF7!܂ B! FO$9п,/K{6! fclK@(0,.Ejpz $ @$7/}1B]~Ga|+AA!`^_H 셶,H I8I$d/FA8x8@  sj78yKєa$iKĐ /^a  B,E;} ~>q냔az Y4[   XLjahh #,/H I$I$I$I$8xKÌfGrAAPXkaa #B>* I$I$I$I$8x9p5GpK$$$$$$$$  "?<a =!B?8$I$I$I$H2jb)>B "(&Xaa1P!\ ؎8㏃\ Rc, `&dx$I$I$F9 P¤)1|BXAE~ 0 4444444F)%Baqc˫a䠚#9ΦihE xxpccc qB*> $$$$$$ ¢(~eXaa <-:ؒҙ*]DBLiXщx%a !XqrLyNxɧ3?! pysz"5φ>2B!!!A=QEQ\7xl,0 JWVe3:!nHzmBO '25Vm<75 qYfU%r_;Y4d-ǭ J6R-FI]fI3Hgu9Zqx7<!HBBB "+ pYeahhhhhg "f6ٷVe cjst)UA-%E"3mZStE-CczEfs QsgGF]-}z ;8 HÒd +X]pL_  TW5]G NҡjTVX/๑1Tq*p*sh1o~dCeDJ^ڔj Ίo)Z$v]%`R4CPd)RFL"6gm9o41xUa-c pI>,{ j*=tR3I2[+2< 5 CS6>l8.x*⬂ 4R&d3/%v*2b(効٧@, $# 6 unVh`䶹 +) H(c F6 纺&h٦MdjdkШf $ ,BKhIF0fJm[;{8 CJ$c`0dc #2zKi`[&nd,##訒'{Bbihi!(KP$8e(j'ӓv? a:mx"(K82L<ь<"Go+i:He8)"ۡ8p4>%{/-檺)#ڠ0N,EqB rǎke*I9:$nhgI{:a0 (qB駦kj(膚邫+jJ/*pE!,˭#m`#*Za" 厉J&'jeK+ルk%+zm m*fl.-!1AQa0@q P`p?6n's.F&s\A.@@t t(:.&cR_C}j(<([8\3^Q-:4~ @# ؚyY' >kZ9&tg#1`yTUhK6ƐKt"Jۅ`=/@Rc7 l @8#l؎cDR H%ˌ1(1a2s&B[/uXk)sWKP7-rܡ: Is]N}j`f@no޾1[ 03Qk)%-(RCNc_e|ݐ̮1T:y]]erq]`6Yy v4s\V6i KC+_YՠՕED 5/e(";`}`ʳ Ą_%phZ巤i6>2fOYUأC1U9 L˚2hcqӆ⸎2@,|xqSYGNttSIS$d9( |*RRo-Y#RWЁ7W ₾#\how_SJ3._ U N 0{xd"UW XFi.KS@xTRŏи@ :qdU!s >&][0&(G U5fzFe{~ͧ7Y_΄. :Á ]¥%~#YVH 5ϡ{g.\(zJ.8.fఝ|-+T`ܢ?nr&XM7w0tW$_$Dy"Lh ;.!&90M*DڎUR [wCco 6KKKKa|j 75]x.\| QZ@DymTOZ|* ++"j:%fQ:Y#Q:g ۧ R;0b_ɧ}%ʸw9 0& [rX1H-,CADV^VrMP+$=N_7)D(E2,#I{FG^5P J&5( 8tP,!1AQaq @0P`p? Xg1MUM^ Zo<{߁Z:B۞!n h7 w1h4ξ 0tM^ЅVXb/^4X|SNPzcA@kV6 tઽc{BgѸ ZeTU踌*%rҖwb˛49[ al{ʫ8͹Ig!}aaD*B0UyƑ' _1b:wI#/V*{uKǶѻZTN1򠼽b%@CJ}J̙[\UTs@M Le:kF>5+F*Si}/[h  B䈭,hڀ ^p9#usMlvGULU-ցTPUqy4:ZrƐYt=Dί;W;\[Q`MJG{ϖFU?>]e81tHԃV 6e6I'h.5/ ]yQ o{?LwcJ{o7Ua;hR=>> / 8X4 Jƒ9-+f 5Mr mp(P\sY(2 ZP-@j0(^i 4pj{`%k6:Xn`'^e:>Y֞Y?l ˜FA3ONP&7E.h FZ$LVUa.{:!UX}e]e6TiZ"*)937z-S F:MO:d@'5*dUuYh@k.;BmYX:CkWҪ ~/E.TcH ]+ R jK6L*ot(年n[cQAC * @,H:``>,jo آn} ;wSAGDU EZКM'Ip cb&IQKiE#RJMCe'>%Ccܲ:PM bZpy7ƲB 譠[5i_ɻ t7^8 I[4&"L{^2[RG,tY`Ǧ> kE.5Rܹr@Ny XK/ɾ,b> B061bQpKCgP?/~IVd?i{*N{Jk@⡎ =>+ɾGa/6hx4jꊉYϹ?QFhC"\-)ίc%hv$ rk,k4?]sDy5TBc֦3)|6.L`*BrCTM:$zg [#4vd;tup .֎ܠzQ[R˖xoc5˄,MXv|č%L]"Xi"߬qG2W SycSAEm-Н쟸yx7K]9z !|xjV(~ےܸ3 jK }}'+kB gAJ:+k. =J}SrMOx܍8mXvE?ܠ*r157ݜ!l}WTÒߐ .R! eXeJ.+Ϲ~a dc2%sRSЈΡܖfRN<\%HC} p|*a atb:1F Er~@:K-K`=_&2mZ-?_b:KLŨ{LȝZ5c)_]oࢽߧ.vJvvm7칝 {'.]Dj" cB_m'RJ5N?p :D*&#auTRJOJ]"6-7xԂ-Կ t{`hQ.52 R;Db},˾DgQīs`c˿ԭ6-KˇB+(-+k ۞֊j_N!^@!WJ*W}bZ ∋VhXPa[|P]^ޱvvK:L5lEXVub_,!1AQaq 0@P`p?`Y,}?}C < zCX%+Rdh}8K>݀T -]8zEr26@TCHX,$s7<V,I+Peh:Nyq*a:h͠wa̪kF[ c\GsCᛴU$":c(aAJ6A8+rc)tkߴ=?Gye,\/@AUP]apl箥pi܂aqibijN0 *>d_} UzHeo Xl7 l_}+G1fd`)x!,JsJ-$Y|ʼn!?q-XSѽ0u(ݿt8'2:M{bai> YYdr˴}WAO?Z&nxgqJI)ʍ:^{c/ c%kag"^ IclNHZ`aE@ Ce":E@k; =H$i6Q}jYEVٝR,"4Ơ Zbhz &A\0C+= .9:{7lx g-X(Ȅ;r~%>ϱ*&nHLSp[ξqL!f%5rW^T/ɧYC$%ͥeVdt<+IJR=q%wa`QUYn& enya 6Pub`"!N2>nIr~{&5\GR2 \?a Û/BZU1YbAMȻ YY]Γz/#v^<@Is gH/is^A@_Vi5f)Z*YjbKfܣg Rw$A+򇼫A;Sp?lu㴩+AxhZ}Uئu_y*Cٖ]U֟EկQ{к gQVϾVs(4i}S]pаW,٫vnU.R] o~!'a}ɉßOOLg"v+ rEGEg?-p%0uXp"huUx9t.|W:: \sY􇀕F1c8-YϮfhHU.KCs* 򒐘&KLډn[0C8lk-ľD™Zjz{khV;{)drFBc}3 N|w+RS֫2Et %/=}riG:J5_y]Nx9z JA#*XFmT]U]bÓCg+s rjrYp`y".d\IXק11Ȍ_L lf/܎W%@M7BWQo]$ YO=ab*DFs!~Qxp+A(* k/{PR@ 2!Ha'ZߓO+N(xR-H97L.(߀OZ݃g5N6ReGQEt0]2ZCuo?i:s,_1V]_Ae08 '|kPԑ[L]0Q;Qκec[ JP^E9+Rt҅ gFf2*ٮck sdmOe^:>]>6hjDX"_(=_oX`ʘmLHjQm_ (d||B>hG59_?3Snowp 'fG\`=/9EC9^bu{SY/;Q0;pJ4Luצ8AJVC]YIb7*# l\A)C7#0RKrG]cD+%p9RS&FE-9 Et214E"eH/iCGDPD*~Q/*Q+IN Mxie` DG @JL%i)%J8GG.88QA2OG|:cszx&=L<.].9K@B@sruĪ㟂fFQ{̀/!ٴ%RHŹtiǓM9Q7uf!}Q@MYxwx/0T->N['/2[ݔ4V|D`Z_l,7םYn>C/^ba+ Ů Sjl>*<Š|ךQT;3_}D>]z?ĵy폫g6}j=@UavUk, W$sAXj\1oU۫*aMG0 3KE94fQO:GҥlEEa5ϱ\_"Cec#Z,Uiig>(Tn>=DU7f|V?Ԅ*8W}\\Qmw&\͏IJoRJq w~L:14Bѳ6i9(СXpYS`ٕU9b=/Ԭeq< LPoËb> ^V_aKE$Gcş[FuuAn3In,UYvEWI^%{{}B7 (Cguj4}HVݶe@hx'@-"G fC(_敋n BU?#8ge9y*6їm1J9@JtMJryJh)5c'r5@2o4n]ҡe|QG^H.N/DӖ{,%S_{jcC rQWry=2OLJDJwBHtM@ &*Nwwܜ]idNj7fU8\rz%>s@%M(I R\m <H2~J]ුRah{̽s*s('*{o6}cdY,|Ϣd=c%Ĩ%&Ohܜx^_(안GTvܴFT>޽R/w`@}T y'y/w /6{SO毬1ae)PCz̏s.IRͰZ#hi!J݁[Ы|Z߬f C\s4Wu5 e\XS^#I,VWX\iWj."F2_q@zBe ]>şbϱgGaxH_PUU7dl5D\tN~(͗,>2M hL&󯘽&Rdjq }0) KXuu)봁ktrK1t{EjKcQ~::*vC_l?HDW`6/!eG/5{Y*mAݎe^ ( PtLm:q*XVWDl- mfd7Eb$nyhud]Xn5Vi9ݹ. t[Q5T<h,PT ң{c9㔦زō=*Xw^GwuAN6ۻ]+_`eIAT)NkbQ`~eC~ʛ3RcM2)vZ *letGV_Co@/|L9Fcv% >tT0&鴨;5 VeTtcBE`;L\5U W9;%HӔ( uQ"Gq#N5teltb51:H;r<כ *7 D`]G㋍3z%E"Kv.ʩl^+!cҟZІi̾tYe6M\}؈EO>+C0<H"Y!ۏr{xx.p CAp4pmUI:g*ZF rb(l#r(:h r&R뚬_mtwaPCHayx wPY1K~%zÌ.E݄Q wftӉO)̹t+r(LNMci[+BSDY~CWt.^l%CsmC|4 @p8iӁYw?hі_Yr02a U:u^jrX2}3)}:GVc{hǻPPkjYTr,J:"|;l+V !l|ONG:Icw`Y|=RWAcϵBmz'Xz~øk&M,݅OF:JoM A_vY]P!EJm]q v'h*% @p?[Ŵ|~ fr~'%K7M)=45h FmBxRX?Xk8zŜjLg_:jD˱ 㺟QxXXSoۧWoQjH3ӂJ0K.'ɳ4lFxir/@P.@p8S.7m4m}Zhщs$ d>p}p{DV-(d=JR\̿S@0"AV,sUa7rOYeղfy*0yp!p1XǕFbD"N+qrT"%~㶇^~Ib0d e7Md,~ULŞeiuyV1 SvbD łnzJY0y_7!Һ ˻e(Ce!(`y"!~.'S )b3]7qtB{K[Bgkc]My 0P= QS|Ӓ$M>{bm q@ P.(\.86@^D8c4bֿ1jh>%ywH6}w ߩ7e*&8FWbc.h̰eO_t."9@F&\KV'>`;JCZ=itT}IG g]ాox UJT0^ HPdU\b4(!^ 9-W|a: >ω(%-K|Q T. !TQEqcnM 1.õUQ-^SKM`?RBt2zU} isGuɣ<@ F/@uaE RhLx{q~kjJ *58(s.-t? * +炊LÀ#oF:xYI u؟ 9er]+n4]Um &EL "2;bTyf1A>/qSr\"s70]u =beXјXY rz>+"z'W\(0Xs0m @0?4OFW.p \!C(8u.Y/I1*7g AF=&T'U\VSSzŗ@Wx;;323J5 B1E;ߪQʕ\:f 9c\-˥#M:i"66%XU]_&U_peLj*t*<ؼ._X_g@jT@\>"8:ŎBNV^ uYA/5G\@嶤;ߘԊ<ާ+"՝V]>`E`w+3Q&IV[d7"ȑV:HNow\M8qa0 ѯqAJλn`   6zQohȷnyc3XZiU5e}>xUh#ϒ,U㤵) ]/̡kp'DKf HFi)Ha2 !262ԳPsJU,(E/jἦ $sy`^p8E9wO74p+@ owm4p:c?K>JeQ|?^m,Ⱦj.B wJ-+UM]K ;X 8eoBm@`ӎ 6NtV Dm5|0ٹUcaEg9$-(nc9`5 IbBԈK%I{[2%FKcE<."!PzJ@6 þ #JC% ·bçX6UI#iol*8:m BwE\L*7 {G!&gD5P~5,jfX+_ \8ZG0\`Eh"JaPT.$GHHO$rr  ^b-9'_2—X7ug K!+Rg<}A U:qh-yJWeR3߁@TFA 0A 3^6N\?Qj*(X%*h댫ĥW?f:7** P! fh 4u< Ȋ&ovsT?Ew{@~DNID?JK%LJuv FL , 1"hcw1[W'@Q46@P A>a~;KXR"c)+Hgg?X#nL_x7|NǘP!B\8x ԠWGJE?,RW_[#n(L 5(#0f4[/ .0 gAU (f8qޕH`մGT?1k([l>LzaW{tw< wx\"BlDQ(J> +H%G-ܖ# QkoҦi6@p T P xϢP7!eg8Sm'*&B*#6YO;|+&Mpco)hA*YF/&,EV,^h/.eB:sAc9GY^\nDSPǧ 8s\%YD /OŽpc v6J#:Jl\6{7YInbQfGhF*OQXqh)lb,,8[ {ݲ&ni 9\GV@6y&2ETLngH:"6y 10:/8o  {JM| D#hP. p bw0; A\*kQ]z&?|C"g?q֡緃S#}\WFhWٿ_.apY_bTݧ؇1D4Gy%{nB:bT Ha W0sl?@ +餣9 )4Pe%bއhY*R:(3rׄ>z#-yf=1DXh}};kGy$Xn ݇/W^E>L%Ќ ;)qP{B*XXz Ies0޻/=0!%gCB 16k/ 3@ 'Eˣ^h]|1#A c4M8NY~| =w7( q#)&g0EcN?+J  {U/ EbP `nE(nY{x D!-xg![)EOهH,k0Ta:xj Ą 6>bW¸ęq>EP"Wޡ!zP۶.KWOtnx%. ު #SsIbPjye_ (fa鯠-i7lߎSF;mtd{|h5eՐ3$.| Gױ_ (P0L9Jj:B_U` +wn!h] y  npcGS,S|s x9Kh0,$jmIX }Z0q Y [~3Q L.V*^VD2Խt.~&tEDw~%Ϻ3(^"$~Rdu6p0V L?X( t޹ҠV9cD\/P(g= + O0Yߔ`nW)J"L+W!Z_LV'L3qܢ0C3z w%|rrp lo2kF2t+|O*B3КvQ4 ׸QPEt?6N=rT'T6K;RN)2q-WٶoGpA 0}& Nr,V8286iG$eݷĴ#!U-=4>~a`MY>'7>BoʊHx9b•h&< ];P&Zi}XK ĩS(I!ȈJ*hlYÔZv&\&#nzKGϨa pG}T}r뛸C|# `*N/?%Vܺn Рo*#+s~m ŻgeګZwk(#!$-GO>7iQ4Gw&['$ZE.Zhea3uNn+Os~v-rmF) @7Gvit Se2( &=6 yDr,+{nUrˀXfPp8 Q,ݔOԧ\8Gcِ{D:E\(F4{K1sxLeBL8&%rZU?TEa5E%&_3l <5te, *±jenՔ1˕栠c^o%|Blow"mX KcƷJ`%S.Y:ǹT)Z 1^8l }fl&ʬ/LFP2008< ePo/s57ߖ!SYs?$}mLV鈟_eFe -U/J˹^#b,ݳ.)Z-H2WW%TBG_ .=c0ӔiF~!;kGtԚF-ZzNZ#S"<cjW;J\nl8ˉ6ă4~8gs 1ml6>@!=?2o OkP`G?K9nŵr0LL*{*&7Jí+t#8g+kֿRTt!0jߖ`A6gEG=Q_xB#J4ۉB7Up=.DD01?Jp~Xh81}yG/}J%fx0s9Bf£,az \J-5T-TGW^Zna!:ljʞn57W; ,]Ie(PHj|Q% DU_nnY(ji;/.¯"gãv;TETm]U$(!4[9|{Qm9`+}YUEh>s+aM\fb,ZnRCO$ (OK9 Խ_}6|q!ؼ`< V]z>RyG;SD l 9*>vlKkhBrreoA#+1o93`˼/>bO$;@-&[U[+=[,\:'* T#gsJ瓕u1AH\ЕrW=3^ʘt) 2,xAJ'W]e Z)oޘ_@Ni+)l8ngx25#'VcEuBi L/yXa12zȝ̼syeטGH<M<_Qŵ^/*mD eâWs(_O,7/\qr6 90ܐ8;F[mJNvSJFMvL⣁QL_zB_z8@]mNM+zH(Ne]LdA@"T0Q/sOXy;]z̢3n pG88B[Lua~WMUSduqWtBE9:?s B%0YOvچesd|ų hu=D'4Q(m`x1NT*g<6}>@XiA|+pEX y|9ͰgR²;aUtT<hfӫ꾸UyOAF`yא,jƗStWꆄ]9].cQ0LZEРوV#|y fBr>Is~&1ZqǖYw[9?q_LОKaP0~dv+pUoahs," 1Bs_ɔm p>-z%,SKKCrh"> lP>!"cf]`ܜA Ty'=_Cpdz՟6v🹵-/W;G@yQfloM s2KmT'\ZA%;QIw*^-̙X4J`c^p<ɍ(ڽ<M?~Ҩ`y ys C`q=#=H-_(AGnax`\3O w呒gYaIhh1]o- _:3\'uԁ>`) (=`7D7F|7MO;wu58W_c Q}zh%3-Rm{JٶV8 Dr?ALn^wbՑe?QPC*éVNЬ+9dme ^A* Wa p^NoInH !xF>vMmŠb8jXS݃iK ^5w">geD˹tU<7SBE ~e98ٲV)֙g͂0 U } _2j'ZHf21hVV߹viHb8iRP9pJ,YY-샥4CϽaYd F 7!el28P8\ ƞ4~4qmsne:⩄Rz-Iw?g:Bl͇%RKt~!/lMe_Lvp{OwkO? ۊ~ئqOGN2eX.c=j>&_, X TsQPLS(MtkRw-A A8hx%/g'Zx݅9@?7aKiV.vn4^Yjf_S~wc013dR F J׷OysMU?Kh.@z9 0}c}3br3W]uшZ7X#z oT&s~: \&A0 C.h4pM| ze-p:4EhX ɰ@Oz3䋯9ycL^uD32qv9ne9'70jp8'*0%2+:A5#,y3d-w״dZYAQ(@ a !> STF<Qsn~^R2|t!~PNOĭzs.ma7\љkEdwI|7Ќ{6XόK+qu s+V~h \ BAA|4)Ur&GS @vWp b<w(;j'゙L.  a `q}ˀQX6g8 JDPt.%r{]q/ImMD6L$3Rc{&U<ʎߙ CDU ?s;O/TB !A߄BL nzeNlTXu9JWq p ACa j&Dp(-E1(v \fto~dSE\ndٝԲ{e,w ¥kN6ʻ0b0s`\ӎ*\ p BAA$p~6 yQјhv+Ւ7Ÿ)uR:*  0A C CaP0},QbWCIMG2%+5*Qh() 1i^dĊT72ՙ}_e*..GTrFU@@  I$I*°B֙{/U78;*3">3+G @p. 0xD8Pu0E &7]/gfrI!eєp\]CX]̏< 6M_S~" exT   $Np YJq||]Vbe%# SʥOT@CaHg0M9_B"8lCՄR;8-rMn"  ˏ`1IR@)TdC&<-25yW™DP.0A$AN|'<8wk!_G XbpaugGhގfܡ{2X/sh !m8Ro9fVan,ukh_` J\">WJ,Q4ѯMWsMv%DK黅3>ac 1g--^7xw(* }BP-$(I$qį&S{Ò@C8 =)olϘVz5i~fuFYaꞮC <Xf:Mg/,<Ÿkj[)u,'+(b m:q]c Q8z)NwzfWߙo/xKP9?,;<;`W@}vöIC,~QaYa @ >98uM.0qb %&4T;Dc 0,/tߎ/v. ,~fw7shHxUvxA;FYe팲 Ye=S)P.@&wܚ\.8ŋ\tNK-̎0 0 0c[w-LN@p k@NH# ^QYg+ 0 \Qj((b %H]VA\*fc ,=YeY.%;(XqG. ~p} ³Va@pۃ((qZ8ZůoИa,,xa8[GܕTLO]GAP. _vC0bʑNϔ~fS)yA˴YN: ǜ?6(u BU(Wy{@k ~_~V'bv8$vx1a}"=THjTѥ̴p8.%d>4}A4Q̅''^"sOY NfoDM""Yf^%~DQYΪaҩ邌k9Ǎ)١bA=:&{& {-0CfvFd ƅݲRBN1u #-P(.xҶPWNDF>6 ,]UP1ʄ SbS ޘiˋԭ;- U -/\tzp]>r Wj<[a3@sCVybKpAhxp{;\`8olbjv@*T=7UfVrVqxsuU1Aܧ+^/q:.GL <`WsSᮍսE출2AdU; .͡<`P K,$8CR)UTVRk(@p( T"4T[/WJE>E0T[%וC5R2ڀy n= 3BpPҦё[$`0 #0 ,Q꯯61 !a_jx濨 gs T<kwt.`C'6Vc s#Wk5QVX  snAC @(   060@0 E0 I0J0L0R0S0T0V0PW0X0[0]0a0g0i0k0m0q04t06|07`/ 0DArialUnicode MShh 0DArial Unicode MSh 0"@ .  @n?" dd@  @@`` x|    !"#$%&'()*+,-./0@12345678:<=>?@ABDEFGHIJK  OPQRS   XYZ\^_`bdefikmoqrstvwx yz{  ~eFxR$؛ Rz 8ݎb$= y]vƈ@b$\:1r5Fb$DUMi$rNy$h? b$6pl;P8M;';Lb$pjDb5xS"sb$fY1hVC.ߖb$i%/!ɷ!!b$(6<0`:z"b$.Q cyy( b$}Eu@+ M3b$FS?ƊGTb$heia*mb$ˆGDƣ#+Eb$hȕ=֔e**0qb$|&y#zsHb$̕T;c&á!4b$ViӤkO}_5Vb$er.`J3b$虲NÛ,]b"Y?b$ŷ\XH({;c_NRab$xu\~*yE!:l.>b$:6$9J.Z@b$m+eu-,m%H3Gn<$R$~I)hn?36 0e0e    ̙ A@ A5% 8c8c     ?1 d0u0@Ty2 NP'p<'pA)BCD|E||s " 0e@        @ABC DEEFGHIJK5%LMNOPQRSTUWYZ[ \]^_ `abN E5%  N E5%  N F   5%    !"?N@ABC DEFFGHIJK5%LMNOPQRSTUWYZ[ \]^_ `abp̙@8ʚ;ʚ;g4\d\d' 04ppp@ <4dddd4w 0h <4KdKd4x 0h(0___PPT10 f___PPT9H@?(?  %O  =0ik4What We re Going To Cover& `The MVC programming model Creating Traits UI Views Creating Traits UI Handlers Defining Editors 4GUI Design 101: The Model, View, Controller ApproachThe Traits UI supports, and is based on, the MVC design pattern. The MVC pattern defines a UI in terms of three components: Models Views Controllers&||GUI Design 101: ModelsA model is the data and behavior that represents (i.e. models) some aspect of a particular problem domain. For example: A well log contains measurements that model a particular physical property of a lithological column. Models do not have an inherent visual representation.$v ?GUI Design 101: ViewsA view is the visual representation of some aspects of a model. For example: A line plot can be used as a view of a well log model. It s possible, and common, for a model to have more than one view (e.g. a plot and spreadsheet view of a well log model). GUI Design 101: ControllersA controller defines behavior that mediates and controls the flow of information between the user, the view and the model. Having a controller helps prevent the model from becoming cluttered with view specific details and code.  Defining a ModeljDefining a model using Traits is easy& Any object with traits is a model that can be used with the Traits UI. The object s traits define the data and events that the model provides. Defining a ViewA Traits UI defines an MVC view using View objects. A View object defines the general characteristics of a particular user interface view. A View also contains content which describes the individual items displayed in the view. A View does not reference a model directly. In general, a View is a reusable object that can be used to create multiple, simultaneous views for different model object instances.Z The Kinds of ViewstThere are seven basic types of views supported by the Traits UI: Modal Live Livemodal Nonmodal Wizard Panel Subpanel*AZ4ZA4>L  Modal Views&  Suspend the application until the user dismisses them. Are displayed in a separate dialog window. Always make a copy of the model, interact with the copy, then update the original model from the copy when the user clicks OK. If the user cancels, no changes are made to the original model. Live Views& Allow the user to continue working with other parts of the application (they are not modal). Are always displayed in a separate window. Interact directly with the specified model. Changes made to the model in a view are immediately seen by other parts of the application.Z  Livemodal Views&  QAre a cross between live and modal views. Suspend the application until the user dismisses them (i.e. they are modal). Always appear in a separate window. Do not make copies of the model. Changes made to the model by the view are immediately seen by other parts of the application (even though the user cannot interact with those parts).RZR Nonmodal Views& Are the inverse of livemodal views& Allow the user to continue working with other parts of the application (they are not modal). Are always displayed in a separate window. Always make a copy of the model, interact with the copy, then update the original model from the copy when the user clicks OK. If the user cancels, no changes are made to the original model.kk O Wizard Views& Organize their contents into a series of  wizard pages which the user must process sequentially. Suspend the application until the user dismisses them (i.e. they are modal). Are displayed in a separate window. Operate directly on the model.Panel Views& Are displayed as part of a containing window or panel. Allow a non-Traits UI window to intermix Traits UI and non-Traits UI elements together (e.g. within a wx.Frame window). Can contain the normal buttons that the other View kinds allow. Operate directly on the model.68-iSubpanel Views& Are nearly identical to panel views. The only difference is that a subpanel view never displays any of the standard Traits UI buttons, even if the View object specifies them.6Q=CdjVET: View Editing Tool VET is a tool for building Traits user interfaces. We ll be using it today to interactively illustrate many of the Traits UI features&  View ButtonsdViews (except for subpanel) support a standard set of optional buttons: OK: Allows the user to close a view after having successfully edited the model. Cancel: Allows the user to close a view, discarding any changes made to the model. Undo/Redo: Allows the user to undo or redo any or all changes made to the model. Revert: Allows a user to undo and discard all changes made to the model without closing the View window. Apply: Allows the user to apply all changes made to a copy of the model to the original model without closing the View window. Help: Allows the user to display View specific help information.HZZ.NM HV m KLControlling a View Window s AppearanceA View defines several traits that can be used to control the appearance of a window: width, height: Window size. It can be: an absolute pixel value (e.g. width = 400 means width is 400 pixels) a fraction of the screen size (e.g. width = 0.5 means 0.5 * screen width) x, y: Window position. It can be: an absolute pixel value (e.g. x = 50 means left edge is 50 pixels from the left edge of the display, and x= -50 means the right edge is 50 pixels from the right edge of the display) a fraction of the screen size (e.g. x = 0.1 means the left edge is at 0.1 * screen width, and x = -0.1 means the right edge is 0.1 * screen width from the right edge of the display).VP'PP"PmPP m  u\Controlling a View Window s Appearance (cont.)Additional View traits that affect the window s appearance are: title: Specifies the contents of the window s title bar. resizable: If False (the default), the window has a fixed size. If True, the window will have a sizing border. @ 14 0(8Specifying a View s ContentsA View has three primary types of content: The actual editors (i.e. widgets) that appear in the view, specified using Item, Group and Include objects. A menu bar, specified using Menu, Action and Separator objects. A tool bar, specified using Action and Separator objects. It can also have various optional buttons (e.g. Undo, OK, Cancel) specified using the buttons trait on the View itself.4+ZZxZ%K& &  V The Item ObjectAn Item defines a single editor or control that appears in a view. In some cases, an Item corresponds to a single object trait to be edited. In other cases, an Item represents a non-editable, visual element in the view, such as a separator line, or extra space between fields. The type of Item is determined by the value of its traits, which can be divided up into several categories& nZNG}\8Specifying an Item s Content<name: A string that specifies the name of the trait the Item will edit. If empty, the Item defines a label only (using the label trait). If =   (a blank), the Item simply inserts extra space into the view. If =  _ , the Item inserts a separator line into the view. If =  nn , the Item inserts nn pixels of space into the view. object: A string that specifies the name of the view context object the name trait belongs to. Unless you are editing multiple objects in a single View, this is typically left to its default value of  object .6IZZZ4 !":9  / G;,\Controlling an Item s Presentation and Editingeditor: The editor (factory) used to edit the contents of the trait. If not specified, the editor will be obtained from the trait itself. format_str: A string containing standard Python formatting sequences (e.g.  %5d ) that can be used in conjunction with most text-based trait editors to format the trait value for editing. format_func: An alternative to format_str that specifies a callable that can be used with most text-based trait editors to format the trait value for editing.Z Ek  \>   v@Controlling an Item s Appearancelabel: A string specifying the label to display next to the editor. The default is to use the trait name to automatically define the label (e.g. a trait name of employee_name becomes a label of Employee name). style: A string specifying the style of editor to use. The possible values are: simple: A property sheet style editor, fits on a single line custom: A custom editor that usually presents a more elaborate UI that may require a larger amount of screen real estate. text: A single line text editor (i.e. the user must always type in a value) readonly: A non-editable (i.e. display only) editor. The default is to use the style specified by the containing Group or View.&#Z8ZKZ  L%tHi, xxvPControlling an Item s Appearance (cont.)Cwidth/height: An integer value (default: -1) specifying the desired width/height of an item. If the value is positive, max( value, minimum_needed ) is used. If the value is < -1, abs( value ) is used, even if it is less than what the item says it needs. A value of -1 means use the size requested by the item itself. resizable: A boolean (default False) specifying whether the item benefits from extra space (e.g. lists, tables, trees, multi-line text editors). padding: An integer value (default 0) specifying the amount of extra padding that should be added around an item. ^ZZZ R!  nl,VControlling an Item s Visibility and Statusdefined_when: A Python expression evaluated when the UI for a View is created. If the value of the expression is true, the item is included in the UI; otherwise the item is omitted. visible_when: A Python expression evaluated when the UI for a View is created, and also whenever any trait belonging to any object in the UI s context is changed. If the value of the expression is true, the editor for the item is visible; otherwise the editor is hidden. enabled_when: A Python expression evaluated when the UI for a View is created, and also whenever any trait belonging to any object in the UI s context is changed. If the value of the expression is true, the editor for the item is enabled; otherwise the editor is disabled. Note: These expressions are useful for relatively simple cases. For more complex cases, a Handler subclass should be used.P{P 2t 2My 2M{V6   %Providing User Assistance for an Itemtooltip: A string specifying information that should be displayed as a tooltip whenever the mouse pointer is positioned over the item s editor. The default is that no tooltip is displayed. help: A string specifying a complete help description of the item. This description is used when the Help button is clicked to automatically synthesize a complete help page for the UI.bv@oaP6@YThe Group Object{The Group object is used to organize Item or other Group objects visually and logically. Groups can be nested to any depth.H| D 8Specifying a Group s ContentfThe contents of a group are specified as any number of positional arguments to the Group constructor, or by assigning a list of values to the group s content trait. Each item added to a group can be an: Item Group Include The contents of the group are laid out in the same order they are added to the group.pZZVZS>.VNOrganizing a View s Layout using Groupsorientation: A string specifying the layout orientation used by the group. The possible values are: vertical: The contents of the group are laid out in a single column. This is the default. horizontal: The contents of the group are laid out in a single row. layout: A string specifying the layout method used by the group. The possible values are: normal: The contents of the group are laid out normally, with no special handling. This is the default. split: Similar to normal, but splitter bars are inserted between group items to allow the user to adjust the space devoted to each item. Note: the position of the splitter bars can be made a user preference item by assigning a value to the group s id trait. tabbed: Each item contained in the group is displayed on its own separate notebook page. More complex layouts are accomplished by appropriate nesting of groups.4dPPZPPHP YR :Tb qkSH!@Controlling a Group s Appearance$show_labels: A boolean (default: True) specifying whether or not labels should be displayed next to each group item. show_left: A boolean (default: True) specifying whether labels, if shown, should be displayed to the left (True) or right (False) of the group s items. show_borders: A boolean (default: False) specifying whether or not a border should be drawn around the contents of the group. label: A string specifying a label used to describe the entire group. If the value is the empty string (the default), no label is displayed. Otherwise. if show_borders is True, the label is displayed as part of the border drawn around the contents of the group. If show_borders is False, the label is displayed as a fancy text label if style is  custom , or a simple text label if it is not.P P u W Z j _   b swPControlling a Group s Appearance (cont.)style: A string specifying the default style to use for each item in the group. As with an Item, the possible values are: simple, custom, text, readonly. The group s style value is only used for items contained in the group that do not explicitly specify their own style value. padding: An integer value (default: 0) in the range from 0 to 15, specifying the amount of extra padding to insert between each group item as well as around the outside of the group. selected: A boolean (default: False) specifying whether the group should be the selected notebook page when the containing View is displayed. Obviously, this only applies when the group represents a page within a notebook, and only one group at most within the notebook should have a True value. PV~X ,A"VControlling a Group s Visibility and Statusdefined_when: A Python expression evaluated when the UI for a View is created. If the value of the expression is True, the group and its contents are included in the UI. Otherwise the group and its contents are omitted. visible_when: A Python expression evaluated when the UI for a View is created, and also whenever any trait belonging to any object in the UI s context is changed. If the value of the expression is True, the group and its contents are visible. Otherwise they are hidden.PZZZkZ 2"g 2M"E$  xfControlling a Group s Visibility and Status (cont.)enabled_when: A Python expression evaluated when the UI for a View is created, and also whenever any trait belonging to any object in the UI s context is changed. If the value of the expression is True, the group and all editors for items contained in the group are enabled Otherwise the group and all contained editors are disabled. Note that this can be used to control a user s progress through a wizard View, by enabling and disabling the groups defining each wizard page. These expressions are useful for relatively simple cases. For more complex cases, a Handler subclass should be used. Z;ZvZZ 2M"BU K#%Providing User Assistance for a Grouphelp: A string (default   ) specifying a user-oriented description of the group s contents. This information is used to automatically create a help page for the containing View when the Help button is clicked. The help information for the group will be combined with the information provided by the help traits of the group s content items.n^ m&zSpecial Group SubtypesHGroup: A group of horizontally laid out items VGroup: A group of vertically laid out items HSplit: A group of horizontally split items VSplit: A group of vertically split items Tabbed: A group of tabbed notebook itemsd)'&$#H)'&M$The Include ObjectInclude objects are references to other view elements, namely Items and Groups. Include objects allow for factoring views into smaller pieces that are dynamically included when a user interface is constructed from a View. This allows for several interesting capabilities, including: Parameterized views  Visual inheritanceZ)Z7L-?)% How Include Objects are Handled When a user interface is being constructed from a View, any imbedded Include objects are logically replaced by the Item or Group object they refer to. For example: The process is recursive, and will continue until no Include objects remain in the resulting View. PPeP2'^!&!!Creating Implicit Include ObjectsA class View containing Group or Item objects with non-default id traits is automatically refactored into an equivalent View using Include objects. For example:P 7Z ='"!Using Include Objects EffectivelyThere are several possible uses for Include objects. One example is creating  parameterized views. For example, the traits TableFilter class contains the following view: This allows TableFilter subclasses to define a new version of the filter_view Group containing their custom content without having to redefine the main TableFilter view. ZZ$R 3 + E P} 3 + K (#*View Objects as Part of a Class DefinitionJView elements, like View, Group and Item objects, can be created and used in any context, such as module level variables or dynamically created within methods or functions. However, there are some additional semantics that apply when they are created statically as part of a class definition& J&Z)$/Defining Views, Groups and Items within a ClassViews elements created within a class definition have semantics similar to methods. In particular: They are inherited by subclasses They can be overridden by subclasses This leads to a feature referred to as  visual inheritance & 6cF=cF=*%( Visual InheritanceJust like methods can be overridden in subclasses to customize behavior without rewriting an entire class, so to can view elements be overridden. For example:P.)( Visual Inheritance +&The trait_view Method$ The trait_view method allows you to get or set view element related information on an object. For example: obj.trait_view() returns the default View associated with obj. obj.trait_view(  my_view ) returns the view element named my_view (or none if my_view is not defined). obj.trait_view(  my_group , Group(  a ,  b ) ) defines a Group with the name my_group. This group can either be retrieved using trait_view or as the view element referred to by an Include object imbedded within a View.XlZZ ^  / + * ^,# 5+ Q,'The trait_views Method$  #The trait_views method can be used to return the list of names of some or all view elements associated with a class. For example: obj.trait_views() returns the names of all View objects associated with obj. obj_trait_views( Group ) returns the names of all Group objects associated with obj. sb s9AlEventsnIn the context of the Traits UI and user interface creation, an event is something that happens while a user is interacting with a user interface. More specifically, events include: Button clicks. Menu selections. Window/Dialogs being opened or closed. Changes made to a field or value by the user. Changes made to a field or value by some other part of the program.@@qmEvent HandlersOAn event handler is a method or function that is called whenever a specific event occurs (e.g. the OK button being pressed). Proper event handling is the key to writing flexible and powerful user interfaces. In a traits UI, event handlers can either be written: As methods on the model. As methods on a special object called a Handler.XJ AnThe Handler ClassnIn the MVC programming model, a Handler class instance is a controller. If you are using the VET tool, it automatically builds a Handler for you. The main purpose of a Handler subclass is: To handle events common to every traits UI view. To handle events specific to a particular model and view. To help keep model code separate from user interface specific details.  ; o The Handler Class: Common EventsJThe events (and methods) common to every Handler subclass are: init ( info ): Handle view initialization close (info, is_ok ): Handle a user request to close a dialog or window. closed ( info, is_ok ): Handles a dialog or window being closed. setattr ( info, object, name, value ): Handles a request to change a model trait value.~? ) 5+%3>vF-Qp'The Handler Class: View Specific EventsEvent handlers specific to a particular View fall into the following categories: Trait change handlers. Menu and toolbar action handlers.8Q9(%9q(The Handler Class: Trait Change HandlersA trait change handler is called for each model trait when a view is initialized or when the trait changes value. A trait change handler is optional. If one is not defined, its corresponding event is ignored. A handler always has the following method signature: object_trait_changed ( info ) where: object: The name of the context object containing the trait. trait: The name of the trait. info: A UIInfo object containing information about the current state of the user interface.ZZZZ7N,tNr/The Handler Class: Menu/Toolbar Action HandlerskMenu and toolbar action handlers are not optional. They are explicitly referenced by Action objects specified in a View. The signature for an action handler is always: method_name( info ) where: method_name: The method name specified in the corresponding Action object. info: A UIInfo object containing information about the current state of the user interface.ZZZZ%.1   1 N>  HNsThe UIInfo ObjecthA UIInfo object is automatically created whenever a View is displayed. The UIInfo object is passed on every call to a Handler method (referred to as the info argument). The UIInfo object contains all the View specific information defined by the View. It is basically a namespace containing: Each context object, referenced using its context name. Each trait editor, referenced using its Item name or id. The traits UI created UI object, referenced using the name  ui . It is your responsibility to ensure that context objects and editors don t use duplicate names.v#ZZ`Z,%%  `$`PC\ct The UI ObjectZA UI object is also created automatically each time a traits UI View is displayed. The UI object contains all the traits UI information common to every View. The UI object is returned as the result of a call to the edit_traits, configure_traits or ui method (on a View object). Although the UI object contains lots of information, the parts of most interest to a traits UI developer are: result: A boolean value indicating the result of a modal dialog (i.e. True = user clicked OK; False = user clicked Cancel). dispose(): A method that can be used to close the dialog or window under program control.^ZZ<@3 _@ QP -(The Traits UI Object Model 2-What Editors DoEditors are the heart of the traits UI. Editors create a UI toolkit specific user interface for displaying and entering a specific type of data (e.g. floats, colors, fonts, file names, & ) Every trait has a default editor associated with it. However, the default editor can be overridden either in the trait definition or in a view Item definition.*\K 3.What Editors DoOEditors and traits are loosely coupled: the editor only ensures that the type of data it understands is entered, and relies on the associated trait to actually validate the data entered. In some cases, the data allowed by the editor is a subset of the data allowed by the trait, so no user errors can occur. In other cases, the editor allows a superset of the data allowed by the trait, and catches exceptions thrown by the trait when invalid values are entered. The editor then provides feedback to the user to indicate that the entered value is not valid (e.g. the entry field turns red). PZP4/Editors and Editor FactoriesWhat we call an editor is in fact an editor factory (but editor is a less intimidating term). An editor factory can be thought of as a template for creating the real editors when needed (i.e. when a View is displayed). Because they are templates, the same editor factory object can often be re-used in multiple views, or even multiple times within the same view. When a View is displayed, the editor factory for each Item in the View is called to create an editor for that Item.BP50The Basic Editor StylesEditor factories can create any one of four different editor styles, based on the value of the corresponding Item s  style trait. The four editor styles are: simple: Fits on a single line. Can be used to create Visual Basic style property sheets. custom: Provides the richest user experience, and can use as much screen real estate as necessary. text: Fits on a single line, and is always a text entry field. readonly: Displays the current value of a trait, but does not allow the user to edit it.PTPm,S];QQ71The Basic Editor Styles82The Standard Trait EditorsThe Traits UI package comes with a large set of predefined editor factories, and an open architecture that allows for creating new ones as needed. The current set of predefined editor factories are: 93The Standard Trait EditorsIn this presentation we ll focus on six of the predefined editor factories& Three that are simple, yet very useful: ButtonEditor CustomEditor EnumEditor And three that are extremely useful, but require more setup to use properly: InstanceEditor TableEditor TreeEditor^tP%PMP&Pt%M&tt   N  :4The ButtonEditor Editor Factory The  buttons trait of a View object allows you to define standard and custom buttons along the bottom edge of a window or dialog. However, there may be other cases where it would be useful to define buttons at other points in a view. This is where the ButtonEditor comes in handy. Traits defines a Button trait, which is an Event trait combined with a ButtonEditor. It is a parameterized type whose argument is the label you want to appear on the button in a view. Z " f, X f?9The ButtonEditor Editor Factory  For example:  A:The ButtonEditor Editor Factory There are several ways to handle the button being clicked. Here s one that treats  spell check as a model function:uZuB;The ButtonEditor Editor Factory xHere s another that treats it as a view/controller function:=Z=;5The CustomEditor Editor Factory :While the Traits UI can handle most user interface requirements, occasionally there are cases where it is useful to imbed a non-traits widget in the middle of a traits View. One solution is to write a new traits EditorFactory subclass that creates the needed widget. However, in many  one of cases it may be faster and easier to simply use a CustomEditor to imbed the  foreign control into a particular View. Z, x 9C<The CustomEditor Editor Factory Using a CustomEditor requires specifying a callable function plus any additional arguments the function may require. The signature for the function must be:,P  D=The CustomEditor Editor Factory EThe following is an example of using a CustomEditor to create a view:,FP' ' F>The CustomEditor Editor Factory Which results in the following display& Note that in practice, more code is required than this, since among other things, event handlers to handle input from the control and set the appropriate trait value also need to be set up.sYThe EnumEditor Editor Factory Let s start with a trivial example of using an EnumEditor: Fruit = Enum(  apple ,  pear ,  peach ) Unsurprisingly, the EnumEditor is the default editor for the Enum trait, and will automatically yield the following results when used in a traits UI:r;Z)ZZ/ ) UP/  1 UuZThe EnumEditor Editor Factory 4Now, let s make the example a little more  real world & We re doing an interactive menu, and fruit is a trait in an Order object that represents the diner s choice from among the fruit currently on hand. Current stock is maintained in a separate Stock class instance that has a fruits trait that lists the fruit currently available. The diner should only be able to choose a fruit that is currently available. nP]}v[The EnumEditor Editor Factory We can still use the EnumEditor to create the UI, but we ll have to provide more information to help it tie things together. In this case, we ll focus on three of the EnumEditor traits that are of interest for this example: values: The values to enumerate (can be a list, tuple, dict, or a CTrait or TraitHandler than is mapped). name: Extended trait name of the trait containing the enumeration data. The values and name traits provide complementary means of accomplishing the same task: providing the set of enumeration values independently of the trait being edited. In this case, we ll use the name trait because we have access to a Stock object whose fruits trait contains the enumeration of available fruit. dPP8PP  /<  D#4t  _ w\The EnumEditor Editor Factory 8The resulting Order view would then look something like:J9Z &<6!The InstanceEditor Editor Factory;The following type of trait declaration occurs frequently: manager = Instance( Employee ) Amazingly enough, the InstanceEditor is designed to edit these types of traits. There are multiple usage scenarios for editing this type of trait though: The instance is fixed, but the user needs to edit the contents of the instance. The user needs to select from a fixed or varying set of instances, but does not need to modify the contents of the instance once selected. The user needs to select or create an instance, and then be able to edit the contents of the selected instance. The InstanceEditor is designed to handle all of these scenarios, along with several variations on how the editing should be performed. However, in order to do this, the InstanceEditor requires a more complex definition than do most other trait editors.;PPPKPP;vKF>pFG?!The InstanceEditor Editor FactoryLet s start with the easiest case: The user only needs to edit the contents of the current instance object. In this case, there are two choices: Allow the user to edit the object contents in a separate pop-up dialog. Edit the contents of the instance object  in-line , as if it were part of the main object being edited.R$I% $I%H@!The InstanceEditor Editor FactorynFor case 1, use the  simple editor style and specify some or all of the following traits: label: The label on the button that displays the pop-up editor dialog. It defaults to the trait name. view: The View object or name to display in the pop-up editor dialog. It defaults to the default View for the instance object. kind: How the pop-up editor should be displayed (e.g.  modal ). It defaults to the value specified on the view itself. [][aS3<KA!The InstanceEditor Editor Factory For example:  =7!The InstanceEditor Editor FactoryFor case 2, use the  custom editor style and ignore the label trait:.FZ9NC!The InstanceEditor Editor Factory6Now let s move on to the next case: The user needs to select from a fixed or varying set of instances, but does not need to modify the contents of the instance once selected. Within this case there are several important sub-cases: The user must select from a known set of existing instances. The set may change over time. The user must select from several different types (i.e. classes) of objects, which are only created once the user selects them. The user must select from an unknown set of existing instances (e.g. by drag and drop). t$PP8P4PP$84OD!The InstanceEditor Editor FactoryThe InstanceEditor supports all of these sub-cases, and in fact allows any combination of them to be used together in the same editor. It does this by allowing you to specify one or more InstanceChoiceItem subclasses as part of the InstanceEditor definition. There are three predefined InstanceChoiceItem subclasses, each handling one of the three previously described sub-cases: InstanceChoice: Describes a single instance object the user can select. Note: If an instance has a suitable name trait, the instance can be used instead of an InstanceChoice object. InstanceFactoryChoice: Describes a  factory (e.g. a class) which can create instance objects the user can select. InstanceDropChoice: Describes a class of object the user can drag and drop on the editor to select it. |PP(L^/ HU(L ^UPE!The InstanceEditor Editor FactorybThe list of InstanceChoiceItems can either be specified as part of the InstanceEditor itself, using the values trait, or as an external model, specified using the name trait. Or they can be used together to create a composite set. In any case, changes made to the set of InstanceChoiceItems are immediately reflected in the InstanceEditor user interface.c (5h"P ("QF!The InstanceEditor Editor Factory For example: P MB!The InstanceEditor Editor FactoryHAnd a slightly more complex example& %P%ZH!The InstanceEditor Editor FactorylAnd here s an example showing  drag and drop support:7P7\I!The InstanceEditor Editor FactoryWhich looks like:P^J!The InstanceEditor Editor FactoryFinally, it is possible to combine instance selection and editing in a single editor by simply combining the editing and selection traits:PYGThe TableEditor Editor Factory TAnother common type of trait declaration is: department = List( Employee ) In the case of a list of objects with traits, the TableEditor can be used to display the list as a table, with one object per row, and one object trait per column. In fact, the TableEditor is the default editor for a List trait whose values are objects with traits.r-ZZ Z-2 t -,} t N_KThe TableEditor Editor Factory Some of the features of the TableEditor are: Supports  editable and  read-only modes. Allows object editing either in-place within the table, or separately, in an external  inspector view. In-place editing supports many of the common trait editors, including drag and drop. Supports ascending/descending sorting on any column. Sorting can either affect the underlying model or just the view. Allows the user re-order/include/exclude any of the object columns, and persist the resulting set across application sessions. Allows searching the table contents in a wide variety of ways. Allows filtering the table contents using a wide variety of user customizable and persistable filters. Table/column/cell level context menus All changes made to the table are fully undoable/redoable. Colors, fonts and grid lines are fully customizable. R-PPP > t a9bMThe TableEditor Editor Factory An example of a TableEditor:&  `LThe TableEditor Editor Factory 0All of these features and flexibility come at a price& in this case, the amount of work needed to correctly define a TableEditor. Out of the box, a TableEditor will display many lists of objects without any extra work& but the results will often be non-optimal. The traits that can be defined for a TableEditor include: Table Attributes: Colors, fonts, sorting rules, & Table Columns: What object traits can be displayed as columns and how. Table Filters: What standard and custom filters can be applied to the table. Table Search: What filter can be used to search the table. Table Factory: An optional callable that can be used to add new object rows to the table.>P[Pa      " : @ / M>t   ecN0The TableEditor Editor Factory: Table Attributes " >8-The TableEditor Editor Factory: Table Columns fYou define the content, appearance and behavior of a table by providing ordered sets of TableColumn objects. Each TableColumn object describes a single column/object trait. You can provide two sets of columns: columns: The columns you see initially other_columns: The remaining columns These are the default columns, the user s most recent preference setting overrides them. There are two basic types of TableColumn: ObjectColumn: Used for objects with traits ListColumn: For lists and tuples You can also define subclasses to get state dependent column behavior PLPPLPGPX  U  v   GX  |    HdO-The TableEditor Editor Factory: Table Columns ObjectColumn traits: name: Name of the object trait to display/edit label: Column label to use for the column type: The type of data contained by the column text_color: Text color for this column text_font: Text font for this column cell_color: Cell background color for this column read_only_cell_color: Cell background color for non-editable columns horizontal_alignment: Horizontal alignment of text in the column vertical_alignment: Vertical alignment of text in the column visible: Is the table column visible (i.e. viewable)? editable: Is this column editable? droppable: Can external objects be dropped on the column? editor: Editor factory to use when editing the column  in-place menu: Context menu to display when this column is right-clickeddPP  +%,   (1-+/ 4;<~    (1-BeP-The TableEditor Editor Factory: Table Columns For almost every ObjectColumn trait there is a corresponding  get or  is method. For example,  editor and  get_editor() ,  editable and  is_editable() . Defining a method overrides the corresponding trait. This allows subclasses to define values that are dependent upon the state of each table object or other values.RTJ 7J> R  fQ-The TableEditor Editor Factory: Table Filters When applied to a table, a filter reduces the set of visible rows to only those objects which match the filter s criteria. A TableEditor defines two filter related traits: filter: The filter initially in effect (defaults to None =  No filter ) filters: A list of TableFilter objects that the user can choose from using the  View drop down list. ~} $.  I,}  Iy-The TableEditor Editor Factory: Table Filters There are two basic types of filters: Normal filter: An actual filter that can be applied and modified. Template filter: A filter which cannot be applied or modified, but which is used to create new normal filter objects of the same type as the template and with the same initial filter values. A template filter is simply a normal filter with its template trait set to True. User created filters are automatically persisted across application sessions as part of the TableEditor s user preference handling. You can create your own TableFilter subclasses, or use any of the standard subclasses: EvalTableFilter RuleTableFilter MenuTableFilter &PP,P0PP& 55^ 5 40b 3 4hR-The TableEditor Editor Factory: Table Filters (The EvalTableFilter: Allows a user to enter a Python expression whose value determines whether or not an object meets the filter criteria. Its use is obviously best suited to users already familiar with Python. Trait references on the object being tested do not need to be explicitly qualified. BjS-The TableEditor Editor Factory: Table Filters The RuleTableFilter: Allows users to define  rules using drop down value entry for trait names and operations. Rules can be  and ed or  or ed together. Rules can be added, deleted and modified. Introspection based& requires no set-up by the developer.:ZZ>knlT-The TableEditor Editor Factory: Table Filters The MenuTableFilter: Is similar to the RuleTableFilter The differences are: A rule is automatically created for each object trait. Rules cannot be added or deleted. Rules are implicitly  and ed together. Rules can be turned on and off.hZ7ZZ>+nU,The TableEditor Editor Factory: Table Search VMaking a table searchable allows the user to search for (and optionally select) object rows which match a specified search criteria. A table is made searchable by setting the TableEditor s search trait to a TableFilter object. Doing so adds a  search icon to the table s toolbar, which displays a pop-up search dialog. The search dialog allows the user to search for the next or previous match, or to select all matching rows.BP  ,  oV-The TableEditor Editor Factory: Table Factory If users can add new rows to a table, then a factory must be provided to create the new table objects. The TableEditor traits that specify the object factory are: row_factory: A callable that creates and returns a new object instance when the user adds a new row to the table. row_factory_args: An optional tuple that contains any positional arguments that need to be passed to the factory. row_factory_kw: An optional dictionary that contains any keyword arguments that need to be passed to the factory. PWP-7 - gbebk - gOepW*The TableEditor Editor Factory: An Example  rX*The TableEditor Editor Factory: An Example The resulting view looks like:x]The TreeEditor Editor Factory RObjects often are connected together in such a way that a hierarchical or tree view of them is a useful user interface feature. Common examples: File system explorer: files are contained in directories, which are contained in other directories& Organizational chart: Employees belong to departments, which in turn belong to the company itself. A TreeEditor allows interconnected objects with traits to be displayed as a tree.XZZRZ FZ Fy^The TreeEditor Editor Factory DUsing a TreeEditor, each connected object in the graph of objects to be displayed becomes a separate tree item. Some of the features supported by the TreeEditor include: Full drag and drop support: Objects can be dragged into the tree. Objects can be dragged out of the tree. Objects can be dragged within the tree. Structural relationships between objects are enforced. Objects can be: Added Deleted Renamed The contents of objects can be edited in a separate  inspector view if desired. Each object can have a standard or custom context menu. PPPPPPP     ,  {_The TreeEditor Editor Factory AHere are some examples of TreeEditors being used in the VET tool:.BZ  }`The TreeEditor Editor Factory hLike the TableEditor, in order to provide such a rich set of user interactions, the TreeEditor needs additional information to perform its task. The extra information is divided into two parts: General information about the editor s behavior defined by traits on the TreeEditor itself. Specific information about each of the object types that can appear in the tree, provided by a set of TreeNode objects associated with the TreeEditor.  @ dI o b @  o ~aThe TreeEditor Editor Factory "The general TreeEditor traits are:.#P     cThe TreeEditor Editor Factory \A TreeNode provides information about one or more types (i.e. classes) of object that can appear in the tree: The object classes the TreeNode applies to. Which object trait (if any) contains the children of the object. Can the object s children be renamed, copied deleted or inserted? What object class instances can be added to, copied to or moved to the children of the object. The icons used to represent the object in the tree. The context menu to display when the object is right-clicked on. The view to display when the object is selected for editing. tnZZd6d,{bThe TreeEditor Editor Factory The traits for a TreeNode are:.PdThe TreeEditor Editor Factory In addition, there are several different types of and ways to use TreeNodes: If all the information about an object type is static, simply use a TreeNode and initialize its traits. If some of the information is not known until run-time, create a subclass of TreeNode and override the necessary methods. Each trait has a corresponding method that can be used to override the trait value (e.g.  get_children() overrides  children and  can_delete() overrides  delete ). Sometimes you have data that is not explicitly or conveniently hierarchical, so you need to build a model that exposes the hierarchy. In this case, you can create your model classes as subclasses of TreeNodeObject and create corresponding ObjectTreeNodes for use with the model s TreeEditor. An ObjectTreeNode simply delegates all its methods to the object it describes.4MPPB Di   >B Fi   >eThe TreeEditor Editor Factory An example (Part 1):PfThe TreeEditor Editor Factory An example (Part 2): "PgThe TreeEditor Editor Factory An example (Part 3):hThe TreeEditor Editor Factory The resulting view looks like:P/*)Saving and Restoring User GUI PreferenceseThe Traits UI allows some user preference settings to be saved without writing any code. The preferences that can be saved automatically are defined by a View and the individual editors contained in a View. Some of the user preference settings that can be saved currently are: Window/Dialog size and position Splitter bar position User defined table filtersZZQZ+HQ0+)Saving and Restoring User GUI PreferencesUser preferences are only saved when you request them to be saved. You request a preference to be saved simply by assigning a non-empty  id trait to the corresponding View, Group or Item object. In order for any preferences to be saved, a View must have a non-empty  id . The user preference values for a View are saved in a global  database under the view s  id , so the View  id should also be unique across applications and views. For example: View( & , id =  enthought.graph.vpl.graph_canvas , & )Z> ~ 1,)Saving and Restoring User GUI PreferencesNGroup and Item  id values only need to be unique within the containing View. The currently supported preference items are: View (Window/Dialog size and position) Group (Splitter bar position when layout =  split ) Item (Splitter bar position when editor = TreeEditor) Item (User defined filters when editor = TableEditor) New editors can save their preferences by implementing the  save_prefs and  restore_prefs methods.|ZZeZH0#/& % eP + >     0` 33` Sf3f` 33g` f` www3PP` ZXdbmo` \ғ3y`Ӣ` 3f3ff` 3f3FKf` hk]wwwfܹ` ff>>\`Y{ff` R>&- {p_/̴>?" dd@|?" dd@   " @ ` n?" dd@   @@``PR    @ ` ` p>> f(  t  C >A&blue_blue_gradient"  6t 0@  T Click to edit Master title style! !  0w  `  RClick to edit Master text styles Second level Third level Fourth level Fifth level!     S  0|~     B*H  0޽h ? 3380___PPT10.'{O Default Design 0 zr@ (    0 P    P*    0ܡ     R*  d  c $ ?    0  0  RClick to edit Master text styles Second level Third level Fourth level Fifth level!     S  6 _P   P*    68 _   R*  H  0޽h ? 3380___PPT10. u 0 f^4(  4  4 NN̙ ?"6@ NNN?N4  MTraits User Interface Class<E 4 NX̙ ?"6@ NNN?N@   7David C. Morrill Enthought, Inc. dmorrill@enthought.com88!` 4 C 8A traits_ui_logo2H 4 0޽h ? 3380___PPT10.S0a$  0 <$(  <r < S 0,V0@  V r < S -V ` V H < 0޽h ? 3380___PPT10.˸>$  0 ` $(   r  S T V@  V r  S ,V ` V H  0޽h ? 3380___PPT10. $  0 p$$(  $r $ S V0@  V r $ S t V ` V H $ 0޽h ? 3380___PPT10.&Qb$  0 ($(  (r ( S JV0@  V r ( S `KV ` V H ( 0޽h ? 3380___PPT10.( @$  0 ,$(  ,r , S  MV0@  V r , S |V ` V H , 0޽h ? 3380___PPT10.),  0   0 (  0 0 S |ZV@`  V 7#GUI Design 101: The MVC Big Picture 0 0\V̙"`  P  5Model 0 0("`p @ : Controller   0 0_V"`p@ @ 4ViewR 0 s *0 0L 0@ c $  0 # lGmH9Im?"0@NNN?N@h  0  fGHI?"0@NNN?N@  0  fGHI?"0@NNN?N  0 NdV̙ ?"6@ NNN?N` X  AData and Events  0 N,jV̙ ?"6@ NNN?N` H  AData and Events  0 NmV̙ ?"6@ NNN?Np X   9Control  0 NoV̙ ?"6@ NNN?N 8 ] AData and Events H 0 0޽h ?_0 000000000000 3380___PPT10.*`uf$  0 8$(  8r 8 S dvV0@  V r 8 S tfV ` V H 8 0޽h ? 3380___PPT10.-슎$  0 <$(  <r < S V0@  V r < S V ` V H < 0޽h ? 3380___PPT10..@}}$  0 @$(  @r @ S V0@  V r @ S 0V ` V H @ 0޽h ? 3380___PPT10./@:`$  0 D$(  Dr D S 0V0@  V r D S V ` V H D 0޽h ? 3380___PPT10.0P"$  0 H$(  Hr H S PV0@  V r H S (V ` V H H 0޽h ? 3380___PPT10.0hW$  0  L$(  Lr L S ؽV0@  V r L S V ` V H L 0޽h ? 3380___PPT10.1 }$  0 0P$(  Pr P S V0@  V r P S V ` V H P 0޽h ? 3380___PPT10.1$  0 @T$(  Tr T S V0@  V r T S V ` V H T 0޽h ? 3380___PPT10.2  $  0 PX$(  Xr X S X0@   r X S  `  H X 0޽h ? 3380___PPT10.2 $  0 `\$(  \r \ S 0@   r \ S  `  H \ 0޽h ? 3380___PPT10.3@ه$  0 8$(  8r 8 S x0@   r 8 S P `  H 8 0޽h ? 3380___PPT10.[@H$  0 p`$(  `r ` S (0@   r ` S ) `  H ` 0޽h ? 3380___PPT10.4p\n_$  0 d$(  dr d S 0@   r d S Ȧ `  H d 0޽h ? 3380___PPT10.5@e$  0 p d$(  dr d S ,L0@   r d S M `  H d 0޽h ? 3380___PPT10.ڸ}$O$  0 h$(  hr h S 8R0@   r h S R `  H h 0޽h ? 3380___PPT10.$  0 l$(  lr l S c0@   r l S d `  H l 0޽h ? 3380___PPT10.lH$  0 p$(  pr p S tq0@   r p S Lr `V  H p 0޽h ? 3380___PPT10.`{ $  0 t$(  tr t S P0@   r t S ( `  H t 0޽h ? 3380___PPT10. $  0 x$(  xr x S 00@   r x S  `  H x 0޽h ? 3380___PPT10.z`G$  0  h$(  hr h S 0@   r h S  `  H h 0޽h ? 3380___PPT10.ڸ`$  0 |$(  |r | S {0@   r | S D| `c  H | 0޽h ? 3380___PPT10.}^"&$  0 $(  r  S l0@   r  S D `  H  0޽h ? 3380___PPT10.}Ul$  0 $(  r  S L0@   r  S $ `  H  0޽h ? 3380___PPT10.~iH$  0  $(  r  S  0@   r  S  `  H  0޽h ? 3380___PPT10.ip$  0 $(  r  S 0@   r  S  `  H  0޽h ? 3380___PPT10.$  0 0$(  r  S  0@   r  S  `  H  0޽h ? 3380___PPT10.Z$  0  l$(  lr l S 4 0@   r l S   `  H l 0޽h ? 3380___PPT10.ڸ($  0 @$(  r  S 0@   r  S o[/  H  0޽h ? 3380___PPT10.0>$  0  p$(  pr p S )0@   r p S * `  H p 0޽h ? 3380___PPT10.۸fX$  0 P$(  r  S :0@   r  S t; `  H  0޽h ? 3380___PPT10.0/$  0  x$(  xr x S ,Ej0@  j r x S lj ` j H x 0޽h ? 3380___PPT10.ԨI$  0 `$(  r  S I0@   r  S \J `  H  0޽h ? 3380___PPT10.pl  0 pl(  r  S W   r  S hX0@     B[̙ ?"6@ NNN?N 0g  (`  ND?̙ ?"6@ NNN?NR  $my_view = View(  a , Include(  my_group ),  e ) my_group = Group(  b ,  c ,  d ) is equivalent to: my_view = View(  a ,  b ,  c ,  d ,  e )H -#H  0޽h ? 3380___PPT10.JE3  0 XP(  r  S Xp0@   r  S q `    Bhr̙ ?"6@ NNN?N` Dmy_view = View( Group(  a ,  b , id =  my_group ), Group(  x , Item(  y , id =  my_item ) ) ) is equivalent to: my_view = View( Include(  my_group ), Group(  x , Include(  my_item ) ) ) my_group = Group(  a ,  b ) my_item = Item(  y )## ?.H  0޽h ? 3380___PPT10./;D  0 D(  r  S 0@   r  S ܈ `0    B0̙ ?"6@ NNN?N0{  htraits_view = View( 'name{Filter name}', '_', Include( 'filter_view' ), title = 'Edit filter', & ) filter_view = Group() $MH  7 \ $H  0޽h ? 3380___PPT10.VB$  0 $(  r  S h0@   r  S @ `  H  0޽h ? 3380___PPT10.9$  0 $(  r  S 0@   r  S  `  H  0޽h ? 3380___PPT10.п2|  0 |(  r  S $0@   r  S 谍0 `  P  N8̙ ?"6@ NNN?Nx@ class Camera ( HasTraits ): manufacturer = Str price = Float traits_view = View(  manufacturer,  price , Include(  other ) ) other = Group() class FilmCamera ( Camera ): film_type = Enum(  35mm ,  16mm ,  8mm ,  Polaroid ) other = Group(  film_type ) class DigitalCamera ( Camera ): storage_type = Enum(  CompactFlash ,  SD ,  xD ) other = Group(  storage_type )  $ b  @     $ H  0޽h ? 3380___PPT10.PpU#  0 D#<#4?@!(  r  S ˍ0@   `   + # #"    C ̍"`  [ b ViewElements  B    C Ӎ"` [ + (`   +  # #"      C  ֍ "`  [ c ViewIElements  B     C ۍ "` [ + (`   +  # #" t   C Xލ "`  [ H Camera class  B   C "` [ + (`   + # #"     C D"`  [ fFilmCamera class B    C "` [ + (`   + # #" |u  C $"`  [ @View B   C d"` [ + Omanufacturer, price B `   + # #" 0|p  C "`  [ AGroup B   C "` [ + (`   + # #"  wu   C \"`  [ AGroup B   C |"` [ + ` film_type   B    <?"0@NNN?Nftf  <?"0@NNN?N^ b   <?"0@NNN?Nf |v ! <?"0@NNN?NO | # <?"0@NNN?Nu w~  $ 6P $"`k  @parentB  % 6%"`A M a traits_view B   & 6&"``  Bother  B  ( 6d("` q*  Aother B  * 6*"` v_ a view_elements B   , 6|","` w} a view_elements B  " .  fGH;I`C?"0@NNN?Nh " /@  fG`H>I]j?"0@NNN?N h  " 0@  fGH?I?"0@NNN?NOu `   + 1# #"   e 2 C (2"`  [ c ViewIElements  B   3 C ,/3"` [ + (`   + 4# #"  b 5 C 15"`  [ iDigitalCamera class B   6 C  86"` [ + (`   + 7# #"  o 8 C :8"`  [ AGroup B  9 C T?9"` [ + c storage_type   B   : 6?"0@NNN?N   ; 6?"0@NNN?N    < 0D<"`,   Aother B  = 0I="`    a view_elements B  " >@  fG`HWI@i?"0@NNN?N {  " ?  fGHqIS?"0@NNN?NO H  0޽h ?   ! # .  / 063:28;82>2? 3380___PPT10.C $  0 $(  r  S $Q0@   r  S Q `  H  0޽h ? 3380___PPT10.g$  0 $(  r  S |h0@   r  S Ti `  H  0޽h ? 3380___PPT10.К$  0 @$(  @r @ S n0@   r @ S ^ `  H @ 0޽h ? 3380___PPT10.͸ VZ$  0 D$(  Dr D S 0z0@   r D S } `  H D 0޽h ? 3380___PPT10.θڙf$  0  H$(  Hr H S 0@   r H S x `  H H 0޽h ? 3380___PPT10.ϸ0$  0  L$(  Lr L S P0@   r L S ( `  H L 0޽h ? 3380___PPT10.Ѹ,$  0  P$(  Pr P S \0@   r P S   `  H P 0޽h ? 3380___PPT10.Ӹ5$  0 0 T$(  Tr T S 0@   r T S ̧ `  H T 0޽h ? 3380___PPT10.Ը`d7$  0 @ X$(  Xr X S (0@   r X S  `  H X 0޽h ? 3380___PPT10.ո$  0 P \$(  \r \ S $Ǝ0@   r \ S Ǝ `  H \ 0޽h ? 3380___PPT10.ָH<$  0 ` `$(  `r ` S ю0@   r ` S Վ `  H ` 0޽h ? 3380___PPT10.׸%8;  0 ~:v:V7(  r  S 0@   L  c $X99?6L d.# # (    3 l"`d.# b fooB (  Rl(   R l!   3 $"`R! =barB    3 "`!# ewxPython controlsB  ~L   +  #    3 "`  [ Bobject B   3 x"` [ + (`   + # #" d   3 ̙"`  [ ATrait B   3 0̙"` [ + (L   + # <   3 "`  [ c EditorFactory  B    3 "` [ + (~L   + # py  3 "`  [ BEditor B   3 "` [ + (:  3 ``:  3 `d < `F  S 8 4`   + # #" @    3 "`  [ @View B   3 "` [ + (`   + # #" @    3 !"`  [ AGroup B    3 &"` [ + (`   + !# #" @ p  " 3 ("`  [ @Item B  # 3 x-"` [ + tname =  foo  B `   + $# #" (  % 3 x2"`  [ >UI B  & 3 `8"` [ + (: ' 3  : ( 3   : ) 3 l  @ `   + *# #" @   + 3 p;"`  [ CHandler B  , 3 A"` [ + (: - 3  @ F . S   : / 3 l  l : 0@ 3 y4,p 1 C D"`H @   B.control B  2 C I"` 0 B.control B  3 0,N"` p h  ?.viewB  4 0hR"`   B.handler B  5 0V"`p  b.foo B L n * 6# PO 7 3 ["`n * \UIInfo B  8 3 b"`n  ( 9 <?"0@NNN?NO( : 0d"` ?.infoB  ; 0 <?"0@NNN?N P " ?@ NG@WHvI?"0@NNN?NP @ 0|s"` `   a.ui()B  A 0x"`T  l.simple_editor()B  `   + B# #" (  C 3 d~"`  [ b ViewElements  B   D 3 "` [ + ( E <?"0@NNN?N  F 0`"`  j.view_elementsB  " G@ TGH(4Ip?"0@NNN?N (TkL -$2 H#  p I 0?"6@ NNN?N-$2J J # "`t.@/J K # "`C0@0 L s *"`t.$/ penthought.traits.ui subclassB   M s *ܒ"`C0$w1 menthought.traits.ui classB `   + O# #"  @ p P 3 d"`  [ CToolkit B  Q 3 "` [ + ( R B?"0@NNN?N  S B?"0@NNN?Np l  T B?"0@NNN?N l   s *"`h L.trait(& ) B   s *t"`g 9  i .get_editor() B     |0e0e    BPCDE(F̙ A@  A5% 8c8c     ?1 d0u0@Ty2 NP'p<'pA)BCD|E|| p@Php\P @   s " 0e@        @ABC DEEFGHIJK5%LMNOPQRSTUWYZ[ \]^_ `abN E5%  N E5%  N F   5%    !"?N@ABC DEFFGHIJK5%LMNOPQRSTUWYZ[ \]^_ `ab`@ B @  `D?"0@NNN?N    s *"` Kp  B.handler B H  0޽h ??` '"(&)&+-&. &/ 0 %89 7< 7>8%?CEDGPRQS&QT 3380___PPT10.$  0 @$(  r  S 0@   r  S \ `  H  0޽h ? 3380___PPT10.ۯp!<$  0 P$(  r  S @0@   r  S  `  H  0޽h ? 3380___PPT10.ݯ@$  0 `$(  r  S Ю0@   r  S @/K `  H  0޽h ? 3380___PPT10.ޯ$  0 p$(  r  S ݏ0@   r  S `ޏ `  H  0޽h ? 3380___PPT10.߯е   0    (  r  S \0@   | 0 S 0e0eA     ?̙ A@  A5% 8c8c     ?1 d0u0@Ty2 NP'p<'pA)BCD|E||s " 0e@        @ABC DEEFGHIJK5%LMNOPQRSTUWYZ[ \]^_ `abN E5%  N E5%  N F   5%    !"?N@ABC DEFFGHIJK5%LMNOPQRSTUWYZ[ \]^_ `abk4  c  N`̙ ?"6@ NNN?NQx8 CFor example, these are the four styles supported by the EnumEditor:&D8 8 H  0޽h ? 3380___PPT10.ȣH>>  0 ==A>=(  r  S 0@   x  c $    < gr^  #":. rn    HJ?"6@`NNN?N  MFlash @`   H~L?"6@`NNN?N  MValue @`   H|V?"6@`NNN?N  MShell @`   H`?"6@`NNN?Ng  LPlot @`*  H  ?"6@`NNN?N   l KeyBinding    @`   H ?"6@`NNN?N   LDrop @`   H ?"6@`NNN?N  KDND @`   H4?"6@`NNN?Ng   MImage @`+  H@v?"6@`NNN?N(   m AnimatedGIF    @`   Hj?"6@`NNN?N (   KLED @`   H(%??"6@`NNN?N(  NIEHTML @`   H`??"6@`NNN?Ng(   LHTML @` [ H4??"6@`NNN?N^ @ @`% Y H ?"6@`NNN?N ^ gTuple @`  W HXX?"6@`NNN?N ^ LTree @`  U Hl|J?"6@`NNN?Ng^ LText @`   H(?"6@ NNN?N_ (  OTabular @`   H01?"6@ NNN?N _ (  MTable @`   H:?"6@ NNN?N_ (  KSet @`)  HC?"6@ NNN?Ng_ (  k RGBAColor    @`(  H4?"6@ NNN?N _  jRGBColor   @`   HU?"6@ NNN?N _  MRange @`   H^?"6@ NNN?N _  LPlot @`   Hh?"6@ NNN?Ng _  LList @`  HLY?"6@ NNN?N   PInstance   @`)  HTz?"6@ NNN?N   k ImageEnum    @`(  HЃ?"6@ NNN?N  jKivaFont   @`   H?"6@ NNN?Ng   LFont @`   H?"6@ NNN?N   LFile @`$  H4?"6@ NNN?N    fEnum @`*  Hh?"6@ NNN?N  l EnableRGBA    @`    Hx?"6@ NNN?Ng   LDrop @`   H?"6@ NNN?N;  Q Directory   @`    HÒ?"6@ NNN?N ;  NCustom @`   H ͒?"6@ NNN?N;   PCompound   @`    H?"6@ NNN?Ng;  MColor @`   Hޒ?"6@ NNN?Nr; LCode @`)  H0?"6@ NNN?N r; k CheckList    @`   H@?"6@ NNN?Nr ; NButton @`   H?"6@ NNN?Ngr; OBoolean @`B ! No ?"0@NNN?NgrrB " H1 ?"0@NNN?Ng;;B # H1 ?"0@NNN?Ng  B $ H1 ?"0@NNN?Ng  B % H1 ?"0@NNN?Ng  B & H1 ?"0@NNN?Ng_ _ B ' N1 ?"0@NNN?Ng( ( B ( No ?"0@NNN?Ng^^B * H1 ?"0@NNN?Nr( B + H1 ?"0@NNN?N r ( B , H1 ?"0@NNN?Nr( B l No ?"0@NNN?Ng( g^B ) No ?"0@NNN?Ngrg( B n N1 ?"0@NNN?N( ^B r N1 ?"0@NNN?N ( ^B v N1 ?"0@NNN?N( ^B x No ?"0@NNN?N( ^B - No ?"0@NNN?Nr( B  H1 ?"0@NNN?Ng  B  H1 ?"0@NNN?Ng  B  H1 ?"0@NNN?NgH  0޽h ? 3380___PPT10.$  0 $(  r  S \0@   r  S Ա `  H  0޽h ? 3380___PPT10.Жo$  0 $(  r  S '0@   r  S |( `  H  0޽h ? 3380___PPT10.)h  0 f^  (   r   S 90@   F   B,;̙ ?"6@ NNN?N  class EMail ( HasTraits ): msg = Str spell_check = Button( 'Spell Check' ) view = View(Group( Group ( Item('msg', style='custom', resizable=True), Item('spell_check'), show_labels=False)), height = .3 ) results in this display&   ' H  D|  0 S 0e0eA     ?̙ A@  A5% 8c8c     ?1 d0u0@Ty2 NP'p<'pA)BCD|E||s " 0e@        @ABC DEEFGHIJK5%LMNOPQRSTUWYZ[ \]^_ `abN E5%  N E5%  N F   5%    !"?N@ABC DEFFGHIJK5%LMNOPQRSTUWYZ[ \]^_ `ab    r   S I~   H   0޽h ? 3380___PPT10.k  0 @(  r  S O0@   r  S O `  g  N ) def _spell_check_fired ( self ): # Perform spell checking...,uM  +#/H  0޽h ? 3380___PPT10.K  0 rjP(  r  S g0@   r  S h=    Bi̙ ?"6@ NNN?N|~ &.class EMailHandler ( Handler ): spell_check = Button( 'Spell Check' ) def handler_spell_check_changed ( self, info ): if info.initialized: # Perform spell checking... view = View( ) class EMail ( HasTraits ): msg = Str */6  (B' H  0޽h ? 3380___PPT10.P ($  0 $(  r  S 0@   r  S Ԁ `  H  0޽h ? 3380___PPT10.o  0 vn`(  r  S 0@   r  S | `    B|̙ ?"6@ NNN?NK * function( parent, editor, args, & ) where: parent: The parent window for the custom widget editor: The CustomEditor instance being used to create the custom widget args: Any additional arguments needed by the function to create the custom widget The function must return the custom widget it creates, created as a child of the parent window.6 m  F z - + A>$L AH  0޽h ? 3380___PPT10.@Q  0 OGp (   r   S 0@   r   S /$     B̙ ?"6@ NNN?NZ`h def make_calendar ( parent, editor ): import wx import wx.calendar return wx.calendar.CalendarCtrl( parent, -1, wx.DateTime_Now() ) class Appointment ( HasTraits ): description = Str date = Str view = View(Group (Group (Group (Item( name='description', style='custom', resizable=True), show_labels=False), Group (Item( name='date', editor = CustomEditor( make_calendar )), show_labels=False), orientation='horizontal')), height = 0.18) % "       C H   0޽h ? 3380___PPT10.0s   0   $ (  $r $ S ߓ0@   x $ c $ܳ    | $0 S 0e0eA     ?̙ A@  A5% 8c8c     ?1 d0u0@Ty2 NP'p<'pA)BCD|E||s " 0e@        @ABC DEEFGHIJK5%LMNOPQRSTUWYZ[ \]^_ `abN E5%  N E5%  N F   5%    !"?N@ABC DEFFGHIJK5%LMNOPQRSTUWYZ[ \]^_ `ab/   H $ 0޽h ? 3380___PPT10.0lf   0   @ (  r  S 0@   x  c $ %z   | 0 S 0e0eA 4    ?̙ A@  A5% 8c8c     ?1 d0u0@Ty2 NP'p<'pA)BCD|E||s " 0e@        @ABC DEEFGHIJK5%LMNOPQRSTUWYZ[ \]^_ `abN E5%  N E5%  N F   5%    !"?N@ABC DEFFGHIJK5%LMNOPQRSTUWYZ[ \]^_ `ab~ %x 4 H  0޽h ? 3380___PPT10. !Q  0 LD`(  r  S 0@   r  S  `     Nc̙ ?"6@ NNN?N _ Zclass Order ( HasTraits ): fruit = Str # A simple Enum won t work anymore & class Stock ( HasTraits ): fruits = List( Str ) # List of fruits on hand & b ) %H  0޽h ? 3380___PPT10.p#b}$  0 p$(  r  S  0@   r  S  [  H  0޽h ? 3380___PPT10.֨   0    (  r  S @0@   r  S F J    N4"̙ ?"6@ NNN?N>3  class Order ( HasTraits ): fruit = Str & view = View( & , Item(  fruit , editor = EnumEditor(name =  stock.fruits ) ), & )b O   N(̙ ?"6@ NNN?N S &order.edit_traits( context = {  object : order,  stock : current_stock, view =  view )$( .   0d.  M  VThis view relies on two objects: the Order being edited, and a Stock object containing the available fruits, referred to as  stock in the above view. This requires providing a context containing both objects when the Order view is displayed:V%H  0޽h ? 3380___PPT10..Y$  0 $(  r  S 0@   r  S ? ` L H  0޽h ? 3380___PPT10. DTv:  0 ,:(  ,r , S N0@    , S DU `  "p`PpH , 0޽h ? 3380___PPT10.0b$  0 0$(  0r 0 S @`0@   r 0 S a `  H 0 0޽h ? 3380___PPT10. #  0 `X 4(  4r 4 S \k0@   x 4 c $4l> a   | 40 S 0e0eA     ?̙ A@  A5% 8c8c     ?1 d0u0@Ty2 NP'p<'pA)BCD|E||s " 0e@        @ABC DEEFGHIJK5%LMNOPQRSTUWYZ[ \]^_ `abN E5%  N E5%  N F   5%    !"?N@ABC DEFFGHIJK5%LMNOPQRSTUWYZ[ \]^_ `ab ` r    4 Bq̙ ?"6@ NNN?N x class Address ( HasTraits ): street = Str city = Str state = Str zip = Str view = View( [ [ 'street' ], [ 'city', 'state', 'zip', orientation='horizontal'] ], buttons = [ 'OK', 'Cancel' ] ) class Person ( HasTraits ): name = Str age = Int sex = Enum( 'male', 'female' ) address = Instance( Address, () ) view = View( [ [ 'name' ], [ [ 'age', 'sex', orientation='horizontal' ], [ 'address', show_labels=False], orientation='horizontal' ] ], buttons = [ 'OK', 'Cancel' ] )&)  L | 40 S 0e0eA     ?̙ A@  A5% 8c8c     ?1 d0u0@Ty2 NP'p<'pA)BCD|E||s " 0e@        @ABC DEEFGHIJK5%LMNOPQRSTUWYZ[ \]^_ `abN E5%  N E5%  N F   5%    !"?N@ABC DEFFGHIJK5%LMNOPQRSTUWYZ[ \]^_ `abP @%  "  4  `GHI?"0@NNN?N@ H 4 0޽h ? 44 4 3380___PPT10.0;[  0 h` (  r  S 0@   x  c $Ȑ s  | 0 S 0e0eA     ?̙ A@  A5% 8c8c     ?1 d0u0@Ty2 NP'p<'pA)BCD|E||s " 0e@        @ABC DEEFGHIJK5%LMNOPQRSTUWYZ[ \]^_ `abN E5%  N E5%  N F   5%    !"?N@ABC DEFFGHIJK5%LMNOPQRSTUWYZ[ \]^_ `abV F   B  B̙ ?"6@ NNN?N' N class Address ( HasTraits ): street = Str city = Str state = Str zip = Str view = View( [ [ 'street' ], [ '13', 'city', 'state', 'zip', '-' ] ], buttons = [ 'OK', 'Cancel' ] ) class Person ( HasTraits ): name = Str age = Int sex = Enum( 'male', 'female' ) address = Instance( Address, () ) view = View( [ [ '9', 'name', '-' ], [ '17', 'age', 'sex', '-' ], [ Item( 'address', style = 'custom' ), '-<>' ] ], buttons = [ 'OK', 'Cancel' ] )  MH  0޽h ? 3380___PPT10.E}$  0 H$(  Hr H S 0@   r H S 0 `  H H 0޽h ? 3380___PPT10.Fц$  0 L$(  Lr L S 0@   r L S P  H L 0޽h ? 3380___PPT10.$  0  P$(  Pr P S 4̘0@   r P S  ͘ `  H P 0޽h ? 3380___PPT10.! q`8  0 0T8(  Tr T S ۘ0@   x T c $ۘ<   | T0 S 0e0eA     ?̙ A@  A5% 8c8c     ?1 d0u0@Ty2 NP'p<'pA)BCD|E||s " 0e@        @ABC DEEFGHIJK5%LMNOPQRSTUWYZ[ \]^_ `abN E5%  N E5%  N F   5%    !"?N@ABC DEFFGHIJK5%LMNOPQRSTUWYZ[ \]^_ `abXxG   T Nݘ̙ ?"6@ NNN?N l BPclass Person ( HasTraits ): name = Str age = Int phone = Regex( value = '000-0000', regex = '\d\d\d[-]\d\d\d\d' ) view = View( 'name', 'age', 'phone' ) class Team ( HasTraits ): name = Str captain = Instance( Person ) roster = List( Person ) view = View( 'name', '_', Item( 'captain', editor = InstanceEditor( name = 'roster', editable = False ) ), buttons = [  OK ,  Cancel ] )$)@9 6G b| T0 S 0e0eA     ?̙ A@  A5% 8c8c     ?1 d0u0@Ty2 NP'p<'pA)BCD|E||s " 0e@        @ABC DEEFGHIJK5%LMNOPQRSTUWYZ[ \]^_ `abN E5%  N E5%  N F   5%    !"?N@ABC DEFFGHIJK5%LMNOPQRSTUWYZ[ \]^_ `ab Xx   H T 0޽h ? 3380___PPT10." Ytr  0  D (  Dr D S 0@   x D c $] 7  | D0 S 0e0eA     ?̙ A@  A5% 8c8c     ?1 d0u0@Ty2 NP'p<'pA)BCD|E||s " 0e@        @ABC DEEFGHIJK5%LMNOPQRSTUWYZ[ \]^_ `abN E5%  N E5%  N F   5%    !"?N@ABC DEFFGHIJK5%LMNOPQRSTUWYZ[ \]^_ `ab' m   D B̙ ?"6@ NNN?Nw Bclass Person ( HasStrictTraits ): name = Str age = Int phone = Regex( value = '000-0000', regex = '\d\d\d[-]\d\d\d\d' ) traits_view = View( ['name', 'age', 'phone', style='readonly'] ) edit_view = View( ['name', 'age', 'phone'], buttons = [ 'OK', 'Cancel' ] ) class Team ( HasStrictTraits ): name = Str captain = Instance( Person ) roster = List( Person ) traits_view = View( [ [ Item('name'), Item('_'), Item( 'captain', editor = InstanceEditor( name = 'roster', editable = False, values = [ InstanceFactoryChoice( klass = Person, name = 'Non player', view = 'edit_view' ) ] ) ), Item( '_') ], [ Item('captain', style='custom') ], buttons = [ 'OK', 'Cancel' ] )*C   L ) yO ' A S N  D ZA ?̙ ?"6@ NNN?N     D ZA P?̙ ?"6@ NNN?N#  8i P"  D  `GHIS?"0@NNN?N   D@ N?"0@NNN?N r   D N?"0@NNN?N   D ZA ?̙ ?"6@ NNN?N&rl H D 0޽h ??`DD DD D D D D D 3380___PPT10.}  0 2*l(  lr l S 0@   r l S l;E   l N-̙ ?"6@ NNN?N)v ` view = View( Group( Group( Item( 'company', editor = tree_editor, resizable = True ) show_labels=False), Group( Group( Item( label ='Employee of the Month', style = 'custom'), Item( 'eom', editor = InstanceEditor( values = [ InstanceDropChoice( klass = Employee, selectable = True ) ] ), style='custom', resizable = True ) show_labels = False), Group( Item( label = 'Department of the Month}', style='custom'), Item( 'dom', editor = InstanceEditor( values = [ InstanceDropChoice( klass = Department ) ] ), style='custom', resizable = True )), show_labels = False, layout = 'split' ), orientation = 'horizontal', show_labels = False, layout = 'split' ), title = 'Company Structure', handler = TreeHandler(), buttons = [  OK ,  Cancel ], resizable = True, width = .5, height = .5 )@ 7 5   y a 8f 6d N V uH l 0޽h ? 3380___PPT10.-^   0   p (  pr p S P0@   x p c $XVe I  | p0 S 0e0eA     ?̙ A@  A5% 8c8c     ?1 d0u0@Ty2 NP'p<'pA)BCD|E||s " 0e@        @ABC DEEFGHIJK5%LMNOPQRSTUWYZ[ \]^_ `abN E5%  N E5%  N F   5%    !"?N@ABC DEFFGHIJK5%LMNOPQRSTUWYZ[ \]^_ `ab8  VH p 0޽h ? 3380___PPT10..-e  0 xe(  xr x S Z0@   x x c $Z 5i   x N\̙ ?"6@ NNN?Np class Person ( HasTraits ): name = Str age = Int phone = Regex( value = '000-0000', regex = '\d\d\d[-]\d\d\d\d' ) traits_view = View( 'name', 'age', 'phone' ) class Team ( HasTraits ): name = Str captain = Instance( Person ) roster = List( Person ) traits_view = View( Item('name'), Item('_'), Item( 'captain', editor = InstanceEditor( name = 'roster' ), style='custom'), buttons = [ 'Undo', 'OK', 'Cancel' ] )** 7 1 P  | x0 S 0e0eA     ?̙ A@  A5% 8c8c     ?1 d0u0@Ty2 NP'p<'pA)BCD|E||s " 0e@        @ABC DEEFGHIJK5%LMNOPQRSTUWYZ[ \]^_ `abN E5%  N E5%  N F   5%    !"?N@ABC DEFFGHIJK5%LMNOPQRSTUWYZ[ \]^_ `ab)    H x 0޽h ? 3380___PPT10.3Rt_$  0 h$(  hr h S x{0@   r h S P| `  H h 0޽h ? 3380___PPT10.,p$  0 $(  r  S 0@   r  S le%e  H  0޽h ? 3380___PPT10.ͳ9t   0    (  r  S 0@   x  c $/g   | 0 S 0e0eA     ?̙ A@  A5% 8c8c     ?1 d0u0@Ty2 NP'p<'pA)BCD|E||s " 0e@        @ABC DEEFGHIJK5%LMNOPQRSTUWYZ[ \]^_ `abN E5%  N E5%  N F   5%    !"?N@ABC DEFFGHIJK5%LMNOPQRSTUWYZ[ \]^_ `ab_ 5?  H  0޽h ? 3380___PPT10.Գ@$  0 $(  r  S 0@   r  S ܢ `  H  0޽h ? 3380___PPT10.ϳV1B  0 AA @1A(  r  S ,0@   @ Z  #">2 Z *  HP̙ ?"6@ NNN?N1  l row_height    @`,  H?"6@ NNN?N 1 n columns_name    @`   H?"6@ NNN?NF  Lrows @`+  H̙?"6@ NNN?NZ F m reorderable    @` , Hܻ̙ ?"6@ NNN?N1 @ @`  + Hģ?"6@ NNN?N 1 Nsearch @`* * H,Σ?"6@ NNN?NF  l line_color    @`) ) Hգ?"6@ NNN?NZF k edit_view    @` ( Hޣ̙ ?"6@ NNN?N1   Pselected   @`/ ' H̙ ?"6@ NNN?N 1  qrow_label_width @`* & H,?"6@ NNN?NF  l label_font    @`) % Hh̙?"6@ NNN?NZ F  k deletable    @`  $ H̙?"6@ NNN?N1   Oreverse @`. # H ?"6@ NNN?N 1  prow_factory_kw @`+ " H?"6@ NNN?NF  m label_color    @` ! H!̙?"6@ NNN?NZ F  T configurable   @`(   H*̙?"6@ NNN?N1   jauto_add   @`0  H3?"6@ NNN?N 1  rrow_factory_args @`.  H\=?"6@ NNN?NF  plabel_bg_color @`   HdF?"6@ NNN?NZ F  Ocolumns @`(  HO̙?"6@ NNN?N1   jsortable   @`+  HY?"6@ NNN?N 1  m row_factory    @`   H$b?"6@ NNN?NF  Ofilters @`3  H`k̙ ?"6@ NNN?NZ F  ucolumn_label_height @`*  HHu̙?"6@ NNN?N1  l sort_model    @`-  H~?"6@ NNN?N 1  o other_columns  @`   H?"6@ NNN?NF  Nfilter @`7  HȐ?"6@ NNN?NZF  ycell_read_only_bg_color @`*  H̙?"6@ NNN?N1 l show_lines    @`  H̙ ?"6@ NNN?N 1 S orientation   @`  H̙?"6@ NNN?NF  Peditable   @`)  Hȶ?"6@ NNN?NZF k cell_font    @`2  H̙?"6@ NNN?N1 tshow_column_labels @`)  HHɬ?"6@ NNN?N 1 k on_select    @`/  HӬ?"6@ NNN?NF  qedit_view_width @`*   Hͬ?"6@ NNN?NZF l cell_color    @`/   Hx?"6@ NNN?N1 qselection_color @`)   H?"6@ NNN?N 1 k on_dclick    @`0   H?"6@ NNN?NF  redit_view_height @`-   H?"6@ NNN?NZF o cell_bg_color  @`2  H<?"6@ NNN?N1 tselection_bg_color @`   H?"6@ NNN?N 1 Mmenu  @`1  H$?"6@ NNN?NF  sedit_view_handler @`*  Hd(̙?"6@ NNN?NZF l auto_size     @`B - No ?"0@NNN?NZB . H1 ?"0@NNN?NZB / H1 ?"0@NNN?NZB 0 H1 ?"0@NNN?NZB 1 H1 ?"0@NNN?NZB 2 H1 ?"0@NNN?NZ  B 3 H1 ?"0@NNN?NZ  B 4 H1 ?"0@NNN?NZ  B 5 H1 ?"0@NNN?NZ  B 6 H1 ?"0@NNN?NZ  B 7 No ?"0@NNN?NZB 8 No ?"0@NNN?NZZB 9 H1 ?"0@NNN?NFFB : H1 ?"0@NNN?N  B ; H1 ?"0@NNN?N11B < No ?"0@NNN?NB  H1 ?"0@NNN?NZH  0޽h ? 3380___PPT10.Գ$  0 $(  r  S 0@   r  S  `  H  0޽h ? 3380___PPT10..$  0 @$(  r  S G0@   r  S `Hu[p  H  0޽h ? 3380___PPT10.߳`$  0 P$(  r  S ^0@   r  S lP `  H  0޽h ? 3380___PPT10.{$  0 `$(  r  S xj0@   r  S 2 CIC +  Hd,?"6@ NNN?NpZC m%Layout orientation of tree and editor&& @`  H0?"6@ NNN?NIZpC S orientation   @`*  H<:̙ ?"6@ NNN?Npr Z l$Called when a node is double clicked%% @`)  HJ̙ ?"6@ NNN?NIr pZ k on_dclick    @`$  HS̙ ?"6@ NNN?Np r  fCalled when a node is selected @`)  H]̙ ?"6@ NNN?NI pr  k on_select    @`!  HN?"6@ NNN?Np   cSize of the tree node icons @`)  Ho?"6@ NNN?NI p  k icon_size    @`*  H$r̙?"6@ NNN?Np   l$Should the tree root node be hidden?%% @`)  HH̙?"6@ NNN?NI p  k hide_root    @`#  Hp̙?"6@ NNN?Np   eShould tree nodes have icons? @`*  Hx̙?"6@ NNN?NI p  l show_icons    @`#  Hd̙ ?"6@ NNN?Np  eRef to a shared object editor @`    H4̙ ?"6@ NNN?NIp  Neditor @`(   H̙?"6@ NNN?Np j"Is the editor shared across trees?## @`-   H ̙?"6@ NNN?NIp o shared_editor  @`0   H(̙?"6@ NNN?Np r"Are the individual nodes editable?#" @`   H$β̙?"6@ NNN?NIp Peditable   @`]  Hϲ̙ ?"6@ NNN?Np, #(TreeNode,...) -> MultiTreeNode map$$,  @`+  H̙ ?"6@ NNN?NI,p m multi_nodes    @`B  H?"6@ NNN?NpC, Supported TreeNode objects   @`   Hd?"6@ NNN?NICp, O nodes  @`B  No ?"0@NNN?NICCB  H1 ?"0@NNN?NI,,B  H1 ?"0@NNN?NIB  H1 ?"0@NNN?NIB  H1 ?"0@NNN?NIB   H1 ?"0@NNN?NI  B ! H1 ?"0@NNN?NI  B " H1 ?"0@NNN?NI  B # H1 ?"0@NNN?NI  B $ H1 ?"0@NNN?NIr r B % H1 ?"0@NNN?NIZZB & No ?"0@NNN?NICCB ' No ?"0@NNN?NICICB ( H1 ?"0@NNN?NpCpCB ) No ?"0@NNN?NCCH  0޽h ? 3380___PPT10.0L$  0 0$(  r  S 0@   r  S ^ `  H  0޽h ? 3380___PPT10.`X /#  0 ""%E/"(  r  S 0@   x  c $< H    k[  E #"."&$&&&$&k[     H̙ ?"6@ NNN?N [  Lview @`   H)̙ ?"6@ NNN?N  Lmenu @`  H9?"6@ NNN?Nk  Q formatter   @`   H(2̙?"6@ NNN?N [  Nrename @`   HDK?"6@ NNN?N  Mlabel @`   HLT̙?"6@ NNN?Nk  Ndelete @`)  H]?"6@ NNN?N [  k on_select    @`   Hf̙?"6@ NNN?N  Ninsert @`   Hlo̙?"6@ NNN?Nk  Lcopy @`)  HTy?"6@ NNN?N c [  k on_dclick    @`)  H?"6@ NNN?N c  k icon_path    @`  H?"6@ NNN?Nkc  Pchildren   @`(   H?"6@ NNN?N =[c  jnode_for   @`)   H?"6@ NNN?N = c  k icon_open    @`)   H̙?"6@ NNN?Nk= c  k auto_open    @`    Hذ?"6@ NNN?N [= Lname @`*   H?"6@ NNN?N  = l icon_group    @`*  Hö̙?"6@ NNN?Nk = l auto_close    @`  H̶?"6@ NNN?N [ Tmove @`)  H$ֶ?"6@ NNN?N   k icon_item    @`   H߶?"6@ NNN?Nk  Kadd @`B  No ?"0@NNN?Nk[B  H1 ?"0@NNN?Nk[B  H1 ?"0@NNN?Nk=[=B  H1 ?"0@NNN?Nkc [c B  H1 ?"0@NNN?Nk [ B  H1 ?"0@NNN?Nk [ B   H1 ?"0@NNN?Nk [ B ! No ?"0@NNN?Nk [ B " No ?"0@NNN?Nkk B # H1 ?"0@NNN?N  B $ H1 ?"0@NNN?N  B % No ?"0@NNN?N[[ H  0޽h ? 3380___PPT10.@:p$  0 @$(  r  S 0@   r  S 0Ͷ `  H  0޽h ? 3380___PPT10.7  0 P (   r   S 0@   r   S  `  r   N, ̙ ?"6@ NNN?Nb  zclass Employee ( HasTraits ): name = Str( '<unknown>' ) title = Str phone = Regex( regex = r'\d\d\d-\d\d\d\d' ) def default_title ( self ): self.title = 'Senior Engineer class Department ( HasTraits ): name = Str( '<unknown>' ) employees = List( Employee ) class Company ( HasTraits ): name = Str( '<unknown>' ) departments = List( Department ) employees = List( Employee ) class Partner ( HasTraits ): name = Str( '<unknown>' ) company = Instance( Company )>> !  ) C s 3H   0޽h ? 3380___PPT10.@K   0 J B `$ (  $r $ S $0@   r $ S  `   $ NH̙ ?"6@ NNN?N R  (*no_view = View() tree_editor = TreeEditor( nodes = [ TreeNode( node_for = [ Company ], auto_open = True, children = '', label = 'name', view = View( [ 'name' ] ), TreeNode( node_for = [ Company ], auto_open = True, children = 'departments', label = '=Departments', view = no_view, add = [ Department ] ), ++  # |# ? $ N0̙ ?"6@ NNN?Nu    TreeNode( node_for = [ Company ], auto_open = True, children = 'employees', label = '=Employees', view = no_view, add = [ Employee ] ), TreeNode( node_for = [ Department ], auto_open = True, children = 'employees', label = 'name', view = View( [ 'name' ] ), add = [ Employee ] ), TreeNode( node_for = [ Employee ], auto_open = True, label = 'name', view = View( [ 'name', 'title', 'phone' ] ) ) ] ) # <& J$ W(H $ 0޽h ? 3380___PPT10.  0 `Xp((  (r ( S L0@   r ( S 8M `   ( NO̙ ?"6@ NNN?N>/:  view = View( Group( Item( name = 'company', editor = tree_editor, resizable = True ), show_labels = False), title = 'Company Structure', buttons = [  OK ,  Cancel ], resizable = True, width = .3, height = .3 ) >b d H ( 0޽h ? 3380___PPT10.O    0   , (  ,r , S a0@   x , c $b &  | ,0 S 0e0eA 7    ?̙ A@  A5% 8c8c     ?1 d0u0@Ty2 NP'p<'pA)BCD|E||s " 0e@        @ABC DEEFGHIJK5%LMNOPQRSTUWYZ[ \]^_ `abN E5%  N E5%  N F   5%    !"?N@ABC DEFFGHIJK5%LMNOPQRSTUWYZ[ \]^_ `ab'  7 H , 0޽h ? 3380___PPT10.0]f$  0 $(  r  S m0@   r  S n `  H  0޽h ? 3380___PPT10.ׯ$  0  $(  r  S y0@   r  S y `  H  0޽h ? 3380___PPT10.د z@$  0 0$(  r  S 0@   r  S x `  H  0޽h ? 3380___PPT10.گPFO"p>x @e [SS24MN 52Pn:^]Jijky eBw*,ð,* |>wcqcOXn.ρFǼc^ pcr1gba$a,t8GQx4b#<=jvPZmK4N3ZP!(m;gs{+m{zU|U3|n2ܸ,粋λuŤa-}ZqOw8Ҩf&=׭aƭ n}oA"M;P˼ߎۋ?ԼݝzK3; oa1w>±؛F<O“<O <̓l<|/ċb/EN ^Wx^x6f06°(0Cø[c4ބ1x3x x+v2N`Wݱގ=ށ~wb4āx 88p(ϜD|/4#(>$>Op# y|_ėeLWU| _7MoG|q,C??OS '$~_ Noq*Np:1g,??p?|\ qbLt_0p fRe;9f{U1 `㟸_7F܄q nſqnX;qzzXpl 1ă0CP< #H< cXlx'x xgxx^x ^aclx%^W5x-^M6([& [[v۰v.aw쁷cOw`o}@ n`}8D|/|GQ| 'I| 8G>/˘o8 ·/1.X~ 9NI8 ¯Ttc Y898< "LŘ阁`&fƥ+ws0 \0Wc>`!",?q-ÿp=n 7܊6܎;w.v8zXpl 1ă0CP< #H< cXlx'x xgxx^x ^aclx%^W5x-^M6([& [[v۰v.aw쁷cOw`o}@ n`}8D|/|GQ| 'I| 8G>/˘o8 ·/1.X~ 9NI8 ¯Ttc Y898< "LŘ阁`&fƥ+ws0 \0Wc>`!",?q-ÿp=n 7܊6܎;w.0 #@<#`<p<ģh<Fx'xxgxxx^x6&x9^WUx5^uas[`Kވ1o`[a{;`G ;ag]vx;^x>a,މqĻ0xLAx!x/އC~Lp>‡a|q|ħi#p$>/I -|h{>q~ ~xD1/+)-NiN=LsGßg p!.T\i fb.l\o ?0sq9 p5c,b: ܈p3n7nKp'BaX !FxFxxGx 8<O$<OS4<3,<s<</ "/K2lMrīj)6x=7`+[c4ބ1x3x x+vxvb7=v쉽}/Xwaލ񘀃C^0|>>O8GH7>g9|_%||_ |G[68}??8?lj8 'c2~_W5~S[;3{L8 g8??8B\03,\ٸp`r\+qj,5X'u 7f܂[o܆qN܅4Oԅa=`86A[cp;O“<O <³</‹//+J kZb3lc s l7bkƛ0o6ovooN`Wݱގ=ށ~wb.û1pރqދP!|G1|'}W}/sekh.M%BXbgƷCt+E(](](](](](]8?Br[!܅r[!܅r[!܅r[!܅r[!>ŋSBf椧G!Μ 5ȅ:gVh !D17#cfp_BC%{wBCBr[!EO+"(](](](](](](](](](](](](](](](](](](](](](]SBzBۆef1bR҈Tؚnr@l|sF,H 8' 2%}܎5ߚiϱkمD3#*{ARS&ިB#!m(do,!Yܙ~=4'%fkִkEk2=mpp7[Q ]]Wܚ;oH 9`WM7[Q 5u5 gZ;GQ#ބߜ?BZ@ ߶cTN*jIJ@3ð&Xr\aj`89rb $~q79BԴd%9xěeX`Ƥ#*Z2ݵޚu>i];W$K:'~7pB;5'3sB2N{OPB`n](](](](](](](](](](](](](](](](](](](](](](](](](](](](](](](3G&gOKn!D(gم!!vV\YcSh8?82ZSncSkHM{1k:+=՞ܓ$nڷ: DGQݹ=36{dla2(d{foZscmmبJbeB`R҂Yls;-mvzz|g+' ˧oG\fϹ4Vs% 539]9o<$krXpvط<+=E}aN !j>s22?i/-Iq`,7Nn;E/i8Rm߷ܶio<;8IJB`ip=*퀔;3kwmvg,Sܥ3+'d/)L͌K^WBAs{϶POd0+ !D-:r\ڑSƯ}C6Qu*B!*C-B-B-B-B-B-B-B-B-B-B-B-B-B-B-B-B-B-B-B-B-B-bs{)B!3HOnsssC#ngNF{3f+13^v/ B!!ђB!!\Un !D A-B-B-B-B-B-B-B-B-B-B-B-B-B-B-B-B-B-B-B-B-B-B-B]ULBT!555*jm嶚r[殦Vn)jjjj53 #(m cĤQQم\5bA5Tk(XjͷpNeKn+>ٹk5Ӟco!0 -;fFT$嶚Z\uf=M!i I ٚ5mmZx-̸xcsHL^#cFx<9hPSSsCsonW6/zfmz"G,Ha綝1sFsr(Յv3u՝(՚1F'g4s '(v&5k"vRVϙ5 SckDřΑ oTx'۟ IgÈջ8'GZm#>737D%I FTGm幽5$}~ӺvQI~g*tOv!rn)Dcs6W~&=ldtᑊsvjfn0`'v?vUVjs;[QnUsSn+Քm555w5r[MM]MVSSsWSn+Ք' Qn !D@-B-B-B-B-B-B-B-B-B-B-B-B-B-B-b/r{im!vs}BB.B.B.B.B.B.B.B.B.B.B.B.BYYqQمh% ̫gEمLgLf%cգӓB^F!oTwn :ȥ)m̬%ٗWn ! )ii,6;=k.[fZջB5fd̬vT~m$"YcSBs22?i/-Iq`'1C|k$'K*'qvˏ!!5A\OJr2*;/ivӓo ̸$CBOnW*BQܶs0k)"-Pn !pvvBpB.B.B.B.B.B.B.B.B.B.B.B.B.B.B.B.B.B.B.B.B.1g.^"b04-mr{i{ܜ!ۙA{3f+13^v/ B!!ђB!!\Un !D A-B-B-B].͚2ݷt34sBW\)UȒ5OT'<8s͛6dϟ>{| >TsQQnW!اz.͛lmw(ٹs˵΅ #X.LlΈ(椆y3j ɚ;@GREG]Usx g}n7Vuݎr ՞qFQgk>I L4c›]()ٸuxg{yg #$s{L'NpV|(EG]UX+(,FOg{y v Q(}Wj.DBwBwvof#&%UZScYۀ{[\񻃬{ =Uc"$Mn7;w_1'^G+q*ۚMg+ϒW܂Nm!D$^ܮl_nWЊ Q !Dͤs%0]8gBv&5bWRjYkqF5L򊟉 $B;8hDQim#>)GFK|iƤ#*ݣ^ښu>i];W$3K:'Ke\5SԜTЇٽϓg4mK[Ռ~_R5ԐqͥOP69h=+}7֗?o۾}GIJ~ﻠ_ ~EA0GCǚ_k6r~yb߶!G#]˰./oܸxvFwE%)'&]A<ߗΠOA8Q53/{=E]ULm}o-x7[n\kEo:;~pQ߯UߗsuJI~q)Ǻ~u<\pڸ3Q޲zK~41?)^ӚK= !/naOkMiPӢkU]Yz6)ZpÕS9}gy5`eFo/f˱~a_Q{myF+Q53{FU}Ư'?Zi.-~hu'翵xi_?.lw<ޯ;Uߗ S :/YM59iUmק!vU1e<3py^G ^^[`~w=ǚ٥gĜZs:A*A]|?}GUT=C|ͫ%jU98w9wsd}Iռܮr y]@5!m!pm!pm!pm!pm!pm!p{m!{'+"(](](](](](](]6ƦfFKF9Y#⢖r1XߏfL.fgFG GEV.L <qB확owq$lwymd[KD09Dzkab_- vZWeԎYi++T2cyJ1s;ۥ(+&jܶIu˕BʜOKng{GKRX3!Z6r22^E-APM-Pn !Pn !Pn !Pn !Pn !Pn !( BQ1BB B B B B B B B B B B B B B B}홋! MK?IOKB9$j unLKB*bnFz{A4! GK: ! sU-5B suؓBzB.B.B.B.B.B.B.B.B.B.B.B.B.B.B.B.B.B.B.B.B.U1O!DRP0=jjjjUԪ Vne{oo74Ն}iiL0wK=m嶚ZۦY͟c틦Hc7;~'o&r[MM-m,Vk#cFIƔiߘޓnzr[MM-m:cI_&i_O4 OMmÈ2J[31)iDTTva*WXPlM7{9Z 6>9#ZGDۊOvnǚoʹ[>&[v͌Im#1źĘ11ON9:xBjGn;sՙ4KH&3w_n+$'%fkִkEk2⍽9;cF猎1FQnյfbg͵ٙ~ms7ܮl_n+nۀEX0+Ù1F'g4m9m5ֶ$sBbG E_okOmKa8SZ9RaX7g+ );Șe/mzjjn;a5R;oNWK+L5g;GN,X~\Qpo߶[b'%55ж$?=% X8uӂiF|foJ`$kIgs{kIFu ^5T=:rQ_~$!04vv@̸3ҧ%X.%XZs%BT_n{uy2aW;r%[26jOsX-:r{Qn !npWJ ! 65>=]-A-B-B-B-™۩'M5R[Y-Kzro'[(}E'L:e Ojo8g1, `lgcyi BT- 5V |ղVƲ^ҋ-}Xs:k4'KD#IQw~@}Mqel0?v03kOp !?TDP>X{o76v7t3v7abScSҿD)u4bxɺ kz[E%|H_]`jeg7zOp3s{#7 # ۽MJd]5 c+21QDQtnՅ G \>l‘#/&j'rYA|qW .H^rع_gM<ل_֬m, m+_bJ@%=w-9EB~ Q+;;G]~c7sSYѩg޴SZ:2juh:̰Ҍ fl<mMhH}V8C;}a];|ߜ]׵_o,6yNYݹ0O?n+awDLlŞRvN;M6ڞ+^ռϪW];yMX}QڹG) zg3ur;Ld~[=$Vn. :GwNϥ7$i\a ))h[5|mc|0V2 ^{ݭ(rGp'e,OUEoovȒG".g#gp?S`Fndx}s+=ݜ*ޔEu"ɿ{'!Fk%_k 9y7[pA³zaoSx`˼;wQfnϻ\nEܶX:1fuGw{dݚa\5=~Ø=# 7=ioM2%O2b;p(]D6z`]_^X2w{ۺF7uS/2X=w, bIAζ1;ۦcEkK S읱n7o4BXzv[wι BgQw7elŽrrȦK\E^Ѵ8 Nx=VtvH;Ǒofn]|/'wkf;86[':-ʖ΋v֒du1[0f7k Ksoo.sHg~۴cؖfۂ[-Ëkýaݩ n!wA6y1w=툾!.ـb 9w(W{M,$ov|s7I sʼn1m =cEE؉WGb[x oiH~e|@5~;ءvǵYu Kk}y4sc$=Q$FzEl}'އEsۣ:Hn{3F6u۬a!6hQz1r76}FM0Kn"^2 ojMn%_m/s{mMBa5u,0iyEۻ:ߛ..a%OjvOeIYG^noWc>@gn)ro}N^F.sz a^^bonY9 Ѱ|3ga#{-F ljZ1E 0,ٵqrF0EK3+[ݚo-aV̊Loܱ_rϛ _lv3O7Fmv-Zq vfn_ce{ -qAo]cu;SNiwjۯ8c4Ob}Q r{ FU`~3IAt"e;miF}CV۳g z$҉];,~ϚG l^ֵr.fTnVj9iO;YlKb-bث؋ !#m75s+{$` z 4duжڹWynYyFgߺ6_m6MJ;\n/k㽹.ft۹Gˈ;ECKw 7QZkdgm򺄳ZoK?jYSn::˱%܎G 3|;Ҟ>݊3ڮ8݊;/'^;mB{ifbnl.rEEZ]xG=7;)YѰ .1/`yv>+U@iOw:r;k<^w귪^_qAu̽]I~6қyrف_m?2=[=C_!xq7tn"`n_={+"\mwSU-+_|ۊKn˹]r{Efnt/g~v2"t6sQdM0 +Bۤ [X_#\!BB?"cmCz׊{hs|;(ot_u]V\)mYM_Yrk'#vc9\>ܶ&Hloo_m嶢E-\5zvnwO,xvǮ{??V_=<$=嵉˻U- nI? ;m+Gn;Xߢankf6 DA[s,kFxpԧ3a=+C/g䚸 <^haG xGbmCVUWX~^c.7fnO=, 2䳝NVt[hąBמ 7q8o|s.Sq1a:f/Yq)2W*՘XkO\/\p#n͸߸ , =h400 #@<#`<p<ģh<Fx'xxgxxx^x6&x9^WUx5^uas[`Kވ1o`[a{;`G ;ag]vx;^x>a,މqĻ0xLAx!x/އC~Lp>‡a|q|ħi#p$>/I -|h{>q~ ~xD1/+)-NiN=LsGßg p!.T\i fb.l\o ?0sq9 p5c,b: ܈p3n7nKp'B yc=`86A!x(x$G1x,6x<Oēd<Ot<ijl<|/ċb/˰16 «æ [bހFlx-;m ;cݰ;۱';7cNp ޅqx7c{p0{>c">A>#(>$>Op# y|_ėeLWU| _7MoG|q,C??OS '$~_ Noq*Np:1g,??p?|\ qbLt_0p fRe;9qU1 `㟸_7F܄q nſqnX;qzЈ>ba$xGx<O“<O <³</‹//+J kZb3lc lQx5FM7cl`;lb숷a']+voǞ b?;?x 88p(ޏ_>>O>q3||_—1 _W5|7qo;_c]|DZ8?#?O3sp2&%~_78ũ8 8p6sp.yq.E1 1L%KW `..Wa|,B\EXZ\z܀qn-mw` ]A@X !FxFxxGx 8<O$<OS4<3,<s<</ "/K2lMrīj)6x=7`+[c4ބ1x3x x+vxvb7=v쉽}/Xwaލ񘀃C^0|>>O8GH7>g9|_%||_ |G[68}??8?lj8 'c2~_W5~S[;3{L8 g8??8B\03,\ٸp`r\+qj,5X'u 7f܂[o܆qN܅4>ba$xGx<O“ҘsMj6"Gi>va=3Կ|]œO6k7nۜ_]v_GǭMz:2z[1z ͉^t/cW*{65='6bn~`/ekL;0lkWX2Bkz[b+7)3m֛YٷcQ_U9_ ~;+Z*U36Wy5(({rkn=b}6ߵ[gQ=bnJIxL.ۦ7/k}h3ӆ>|CnZ"Maxd<̙cD#V<3_|u|kȇ ?[m6N/c9b"]潭XV=C[xbF6kS<(hBĀ1SvF},ۦn=,2;xh͈XͭYD{*&~~admӘ%x.X>|h /m 7w>iSUn4_fֿKil0;{c~mo-|Nxxxd<b[]zܗ =9߇`8yF\"_ZKz{S9(]G.n]ۧ 7sS9~4wlSi?p 17.o/Uo_axu"[P|g tŘiV.WJ rejued̔]dck k\ۯ07]xxڕi1{Koo -߲0~w0ѱ*9ӮLA]v4T>h7TYŘﹽL 6l ɓ%7{o.pu۪m~-ƈrl8dgĐfSw"]hݗIvn]-1uGr̲*KyFoG:-/ZG8*լ!X ;V__lQUeTxizVT\VjSLK%{\O߶nM/Ids+7!|R$L[]kwI-t>JPs6\8ƚ<[4ȷonZaO8JIX)W筻=?ϩtGq9Ĭy4=)@rp{^ii3gbcx_^$OeWEUL o'//΍+RUIqea2^3J6QU3^Ψ5_ҰqYTտ.u_˨]FW2_QUeTտ.u_˨]FWk OP+g4_A?ܯK7KZ+`DTԈTD]U*{ {>$L*_"ke-0#)tIkSlkɅ٩/7+DK-{"晫2猎)9焾jkI9o}'e%XE+d֜|rǿo-7,?A׿$)0'TtW8+7Qv~\-8oo AU;w^qN-flaoL3Yq[_*f&tkE`Qa_+Ω N pM߫d0җѲWҙݛ{3ޗ4V+ yk2T_Wk}#TJPCeTտ.u_˨]FW2_QUeTտ.uJa+S?Ww/EEu?݋BZ Oh1Jx.LPR!U: }%]BFBuB!BJB8^x]Bnbw!aE(K:KP.DSU"v+E qq0ZX!B! 8!?B!B!B!B!B!B! O*BGMMMMMMMMMMMMMMMMMMMMMMMMMMMZOqB!B!B!B!B!BG8!?BZУbZ˅I"^W%WPy 贼ڪ5W'?б JQw ~}c"&BHX`-ԋU1쬭B:K@pX][sk-Rk>aX!B! 8!?B!B!B!B!B!B! O*BGMMMMMMMMMMMMMMMMMMMMMMMMMMMZOqB!B!B!B!B!BG8!?BGA{/-̿my .z]iD2a¼oΏ^cH|k6bT9s+VF9sQ{ *nF]5Eӧ?[f:/t9(E!;h;5qN&kހm6>fvߞw^_tY{^3:4Ze͗Q4Sؤ_QX5(|j.y>E&r!2 d7-1a]=8nͽSGy>k׾yvm!MrHMdPip־?%uhxf1V?js~wOr|iyg Z͙fixֆFڠ/0*{ i_+ 4;H=;'}@ӬI%`Ac/kp&|o㾶skjCdf}T<4J:GECϛwwPKw܅{sUw^\xW izeZfJxJ뚞tߝ8k>vG0M{% z_Po *%ۍ&5 ϝ2 uM}&5}w,L3>5e]4qP5?oǜU}ꔴ!ժ)i3=|]`yw'kuoՌg|&4~@Ýjn9gPnޣ]SI3!IFNvN}llwn|bBX-M3FgƝpGڛ* %nJ~,sxЂ. ;_ې}o~snoz kZ]vo;憿+iJژۈ ~in#Oʎx=^! vxJelƭ3a-S/G*h%_ _[I〒6E qEڌ4W';kBLnxxtôӮNM\rq mJ5w P}UKzҤ4ygs?0KzޮqC?YWM.kx{&_Q5ᾋ&\U~[l˿i;4}{ƛmZ2_ɍ6K︤tEew )3U[u97Xu˯qt`v:lmC|eW~y𾯎8+.?3W\qƟ/S4촢󥽖_-uAJg-LɧYH'' ok/I0l'vȡg~1Ɣ#u*dB2~&2w@֟zW0Lic2OݺS70@#坦0x)[4кm 4Ba6UOlߒ4iHCcAzH iHC17YR\^Qt*+7tHx\^^D"J-iHC҂ DҐ44!ͱ iHs,HC Ґ4ǂ4!ͱ iHs,HC z@/vۦo MqGy]_km4m7Vi*ownk/z|rHK"D \ۆ .@GRz])^{Xѻ4#n8j﷫7JVrfbloVC{?]n[}&PҾJcbl#'TPۧ9wjp6 iHCAҐX49! iiHCcAҐX49!YF" i]f M77_jZ iHs,ӇמO.7ɝnw3ǿ? f7w1ǿ? 3jmLT-!âino aM~^<5|οH@dޚD\zb|LϘ M/f4`^u% GIԿ,RQ)r~4+4,VXD>*!>@U&_2Ol矼}\Tc2O,ne]^[Osg5:\Hu?~w3ǿ? w-ǿ? z{o__5_}-2=@KSo4ot{v/{/A|y݊H27~Դ7)}pby݇$< y6"hg~BW?*%}swR[Ç=zdVkR&dOvgT~ Oj%5_gU{;7}폗T?v5UK" Gy+9G3"ATd#vM#W_3`%"?v)cYD_rav$3s?~nhGv/^+Ѿv}qYef2;\v.E/?O-*K˖[APSF;z]ڒaSŹ::c=5-Qd/|jFʟ`I?E$}rjO0PSF;JΗmZƀY/Zq7ڲeCjZp j(c9ISMB͈|lu&7(%#k0ʫ%e"'>McN؄uئW9\ *wq*x,=K6zL?kp@9}D? odL&F33??t0>?ڰc玥kђԾ,Drֳ/-YzڼböoC\GE3RMeaZ?q$Kډ*Y Y?&GZ=zd*m#)G]hG:XC-'m@{'_ ƒ~|?PO2Ԍ%#%?<dU~ߐy7'8_hwMe2ڑWr~k'? f*8oé_ f7nw3ǿ? f7{I&*oz듐_n$e[]ߍva|{M~dзw?cEaE{u_p8!'*v  ?UykIIk"|Q^ÿK? f7/˱d0K)zo9ohiYA=?ڰc玥k#Y۷0YϾd?z}>okk W ھ$hmk;rS< 53r۾ iyI;Qwq:N v (@-zʹ<)NM?A:kZ.\ͦ' @w` ]/t9W0lLc{0USߗ2-sl*7T|ۃ&{XaK׼%2G˳oaf#}iuu|=0@} 3I2R#8v`2y֚4-Ș^$4k2)c/< DQyo#]X=zK,"~T~|g~8"%=T;FhG:XCuճ.h] #u[F;䷷_6׶fkDAr 5cxjQ5ݮ#GɠdUڒa@+F @/GF2Ηf H5jH|+CMH+9_{>x. i@+Fh~bHC Ґ4ǂ4!ͱ iHs, iHs HC Ґ4bJoXlkׂnw3ǿ? f_^3?=ǿ?O?KzX|g~umS)"L36Pfw9Nklh^a"}Z{%֦\[vQsmQ XV“l-92 lHCҐ iHs,HC Ґ4ǂ4!ͱ iHs,HC Ґ4ǂ49SZ'kW W?3{4k/)/N )o^nkPE JM)҅B'7;yj(cT#^"gݭƜHHsBYK )X)8X,8!k0+oR&r{%Dm1'l"Xu8,X ZOas2k3+m }vc{orl݄7`w9Qn*%{n8ߓ.y@Ɲ*Pi49! iiHCcAҐX49ҿ vHC Ґ4ǂ4!ͱ iHs,HC Ґ4ǂ4!ͱ 'Nr%Feb%blS3~sB[)aRӒiHCcAҐX_AҐ@49S KoRxGۦEZ 6֓VNM?ߞ+M=Qۨ 45Ζǵg'˵zPzE_1 ۲AdpS*E2~[̷~W>0{ߙqŷyF=;2,ë)|/9Ao<>h.G]ڈL՞x`mgK_ ? ؟?wZʏ,9p ΒIeT.=m}9v9}; {5<2}ib__CweGsCW|FQT7ͽ_6iG\7}}{KG}{iChG֥onz_\^sgiG9%1Isx^GCGIsԺUG%rUt^;sFU29,wek~E{P:|cZՋ>kYG/r^ڿsX &_/f͹Zz3BQ'k<_,<_6Q+T`mUܿ`Փb8*yNKՉgMO?&|?Δrj8Q?2Bs꥖w;_[_UVQ1ڱZ~ }/-P^kezuz oDyA:o<#x&0jp[vI_ix @TJѪffK*.+b⾠ v]3K+bi%+M DPDuaD{μaЀÙa<А[b#Nr"Ym:YZ_#IN8YdXM.$W(_1t;l`; l*a7H)-JABicTJ3<*)~UJ?~uS?~u>d_|Oͥ߷SWg$}m ]t7_j"9;ɟ=U]K=ܬ'yWux5gNN^_N~N^727›$! lj[m{Ix' -ὰ l =|vÎ(ϩ3Ga7=cZaozH$cp|\Ap0a>p|B?I~I(81p, p"')p* t$GL8 ΆO9p.  B(?s%p)\y\ E|Wku|߆p \ p|nM} Gc~?/+5p n=># x7;G<O` 'a<a< HxFs0` w;aG;.+|va>{ް}a?COAp0a>p|B?8?qp<'Ip28 NO0΄lyp> i.b.3p9\Wgsy|_+U|߄o;p5\up=߅{p|~7-CnOgs % ~ ov-w.~?_~x0 GQ'</CIOpO30Qca 0&dh)0t0flsaa eXk8p F1t;o7- l of9low;]nxl }5m|a>;ΰ `w^7=`_zp páhp'p 0Np 3,8>p`0|. ".K2 \WY|_/*/ 7|_o7[m\ p\7wF߇p ~?['S/~ _m~ w; =p/?!+< ?x  PxS0F ga<yxXaL:hI0` Li0f@3̄Y0\/|X/BxA hr^]a#ku^o7›6`3;.x7V>>Ç`0`gvn;{`//q @8CP8 zO@_GQp4cX8D8 NSTI8™p s\8·A0> p\ p|.+J,|>_/—p| _7-6|kZp#|nf~?í) ~~_6 ;NwOg < qxC`(< )#ixF³0 <c`,0&BC4$ M043f, s`.̃a>,`! /;Agl :7Mfx l[ml[Nx[{a+x55*?;ΰ `w^7=`_O~ COAp0a>p|B?8?qp<'Ip28 NO0΄lyp> i.bD~m .p\ k?{9ySgW̳ Gk yVkַ.Nn[/"޹[Ϯ7ky|*_|}UyՍ5"|_oVͦ 0LfzDuekRK}Ī ak^UU>x/W5UUULu ak^ˏvm z;>]:M7Iڜfݒh1ah闇C>9\4ydR 0 SJg3 h sam}0 a9Ӑn0ge^ȫ\\7]6^]c?qT396MϭfqIyާgoKNzzt䅤m$>5 +)Tސ8ǯ\`zphį!{\47昲sޫs ڟX{Bvc p?,:ęs?қޫs՟5JMz]zSd&E_%b]ifA}^L*sSFFVbLK3&Oĺ2^Ωw<=3Nz?߹^ͤ>O1<5dӹx[Aq禌S^QrnݱvVFl9cN;r>;!(ܹf4Op 'k;ܺCY> FBwEQLD<1օ+:ew7L1NYByVS>єz3HJ)h8;7[QY/N&Favd^}NY*bݵp!xoݏ'ek>6*a<ӱe-DW.UgCcfK[oר;/>ʿ+iH\AᩝjO-u1[ny֞v'kWZ{Ė^RXV^lQF!G jmDøz˝}unWVD[KR%ֵ>۵iJi`Eu/z* eGv!QC5 Y_-ٝ)gޠlY{:[~>-nj ƴX}R%vu=Eym({=7dI{J bc`1I V`Q>][6S9[rp/Po 6_ŗMzbWqջ^|ʹ>1Y&9ӻsrfz; qn>n~PZIJ׋EIXlPe,umΊn?Y?\/+Gʡx-Xm*Xk>E2;^F*_̱e}dz/r=󇧫)a3*^q?-UmK][o%O׫ϟ;׫Tٹ֧QS9~kyuUysI%}wbVf'Vo\oM+m}^L*󂂂|bAA&֛QIq7}^L](|YNa%Mҥ\+߯z5J{}cZc"X6m6/JNzz߯ކ^<߿ذχhcLV冕$A*gF 0aFQ<3?0L=|+a}0 \iG%ƤY9y3$aEm&r1ߜwBjzy^ G_mzNKayf9/9;;-7z<$N3-{faj(C#~ سw!1ǔS}~B>Y1b#.0 SAEǜ8sxcq7L&kR 0L gLO ?O3D22Ωw<=3N~KvbLK3&ODƥeDZaj3MT% S;IJOOH6סxckS33 SC 1%unuY<Ŗbލ7VoSW8`]T iVQN;\uAbmRP:Oէh;DSJtEyZf&j63LE3S9t9[i;R{IRy8;/h>Re-9E"vEݡ!躔󉆚lD:tɒ$iW&"kWZV6Y/I^ʫ(8_PD/~8K{m׃=-kcaBPԴ :cyFv605)hhrqvm:+=b=Nߓ53_ J#޻kmD6=+B|\кrN$m-²"$bQ&iHM>7 ;;{Wٲt|//+X?g A'ހr>٧Oeg{zm{_)o͐$G\!X\ SsAcI5癹JgO)|]qzwppU.L:/ug9猲3AE~gXw}xWt'A'gkϳrsLᬏU^׫Y>2o3s#COKbO}rWt'A'g㌦lk3L]}gF힕/0$99 s|Myŋ 0L ''//͜ON>/((gaj8 2s2ϫ<-|YNa%a&SpRfv.7>W&EBjEEڏJN>bÊO?rB!1Drܰ2T}WLbhο0 S yާhD ѱ Srt4ԋ(Z (j?>gaf 0aFa3 h#sam}0 asa+}[^`aTIڿ̀B"sBkRҲ9{o>]J}N!u*|"*}N!u9ަffz'lG"Lf3<faBHC9㓓LĔ'lMJ1/ hxT:q Bqק*ѕnuTD칏}N!uᆴ4%Q: !"t翄XR}NJ̴sS"J:u>'sCZ9''#;wPsУ9!=DP(QJXR}FoMh(~s-gQ$ !>/(,rRˠSY)촬윌ܬy/]ʷKEE"xJ}~7Bj9!h9!h9!h9!h9!h9!h9!h9!h9!h9!hhi+ !"sBsBsBsBsBsBsBsB5nsB           j>'ڃ}N!ڀ}N!ڀ}N!ڀ}N!ڀ}N!ڀ}N!ڀ}N!ڀ}N!ڀ}N!ڀ}N!ڠ~K_BU                                                  j#R+ !D !D !D !D !D !D !D !D !D !DoҘ}N!@M1Bj9!h9!h9!h9!h9!h9!h9!h9!h9!hsB         !DPgBH>'m>'m>'m>'m>'m>'m>'m>'m>'mP}B!B9! sBsBsBsBsBsBsBsB5sBjfBH>'m>'m>'m>'m>'m>'m>'m>'m>'mP}ޘ}N!@ yi=sB=         j[!6WE!BBBBBBBBA9?!5-R !D !D !D !D !D !D !D !D !D !D !D !D !D !D !D !D !D !D !Dt>'Zn9!sBsB紬#gxGw۷;dû~}h9'GbaBH}~&>;bBHD~yNE@LH0`X}N!u8DONN0SR@%6))h`0Q-sB{>8j\DWQg>9!I>GҔ8PWұ}N!uz=:<)=ѣx+*}2Q>'z(sQ麔e *`0$gd JSSU> !v}c4Mf3ԸR"| B*q-(]|KT}Ӊ9!=>O1SJWJDbaBHC9;-++53SsDN !!ܐfF+GI]Ϣ !!JGJW<,&>'' ߗ>'̕y*90 J*җa906>gF 0aFa3 h#sam}0 a906yRlokOP/k:W/>Y쯜`]wʼno;tR}~C~v0I$I$WJ}mUrZe;WW[o؂g{ɉi}M>hrQ||n>NFKqrL㤔IRZ9U2d.I2!Nr$oQ1\Y&YIR)k;V''lDq2[ެr;l2'ޚ\iΟHp-溔Mg@=SO t?O;5ݙWt St33p|P>'x~3R`mrrRB[l^9Ӷ&˗M:}ٴ˶]:wnn~KN޲϶dp>G>mM^L*KŦ uW #?l_Nյziw=+CzphA'vfG4Fk;}MN'Z88G_R,(r2[^,`Ë3 TWʖ9NG] ? ]c?؎cǷ7ẦCt7xMt+=rRjCM kizT)i~!g52=H?U7)9򉙮e(yNr_X:%H)ESL۷8=|;J=mK]!+Uy1w)Wr;׉fZmV~ aj/3Woj[_?G'|Σ1%cjb)M2YaXcI&2,SoqY;}~P3-5ֻ뽚}JZsrhCʽNV0cm.{ \oj>\TWT"ZX;?߂hBG^l {n`l?)ؾ$]_;D}~CIM~rTr^Xn\L/p,p9KWelB'7_ֽQl7|wi{Ϛ|ЂqBcL~qSS\Da⬸ͅ/{ B+7a 4L*Iۏ,HD30xY:ce=m)vw}C;qՍ>]z <GϜ2wX n)Ga},>5 5aXbSG3Abv&G(xzLI2K7bw^gO7*[nMj6tp"{XsQm}!_~b=,J>]pV V2[DI&:W8 6^^[,n4wi;,srY$Rש5yHw%dq L?܉>7Nu/#K1]S\ròq6[BʹۇK!ka>Fi ?9芇z2q\`è`_PI{ sNQ;}-| -%6{M/Ƚ?U8>GnY#[^ꛏpm4+Z>챶>06NR\ngc(6{G91B*1$z=}G̪n*.v>{@ nq6,w-{ vV__4f몬;05GP >r70vq"Az9vwk;Jw]K]񭆞nM\B;/{sg69WR樅f4ҕۖnsq8_l^ es|H^9qYq)7JS: X,nsr~n 0wV+񋍓'/5_h1%~" $y"Ͱ{}2Gq|܍%}=L@ WIˇV(P8Y#N\ldPjפ>yǷ7w>]9ȽC'yكsee[OsN&*ↈC9wZ,7_nLE?D ͪ6aGKc>RҜUɁ'MYV,=&'vWRo/ϩ[}!t:0AяIbօ>ǑnfsqӮrmf4?^>z&TR>ug$kb£c82ϩ{H?_[P4w+b}蘿Ά:/J=,&Ȑ=gO ٝU[=Qq0 SWQ|dagץ,xtxȯ7H`o?Ө#\v2љR(\\)4H:9_ [rr )d$ t̑r?82Ξ%%M*L= fQg-6%/lH]`=J\ҔgFgy=/ $Ƕ֢6\Xs\mV\n疻E!=$juQϸE.5\$VJ3KܾU3zA1W3֚h'tr IxbM7L_IRR&M[4yqb`9:'{k7,춮W9ޢ wB#1DT)iSCiNISUۉҨNE|[)?M_y+-y*SgIJn.k/ 0JTa踂EoV&/0{|Zד}=):&4*Olˁя Lyu?M>=L:-2/ϕ|$ݞ LbY(/bؿ!wפu3/8%'@QϱsQe.*=ڣW?GdjG-[V4r^k%,*ζoda)bֶനY%hVT+^ndAë1t.`+e.>֛^K9USRɽͨa4eAQۊu,iFaZk'%Qʋ3"֟?(~^q)%/_+(4qx9>}o3x[+͑Y] E;T>#Z!EWc)$Y Q*/ 7|_o7[m\ p\7wF߇p ~?['S/~ _m~ w; =p/?!+< ?x  PxS0F ga<yxXaL:hI0` Li0f@3̄Y0\/|X/BxA \% :Cx t`cAwx=o7[` o`sw» [`kx?l`; lð#|va>  `ozǡ' 8C0 C8>} Gc88N$8NSa' gYp6| ΁s<8`4\Ep1\e+9<|_/U_o*| ߀o·k:n= ?!~ O3_9?~7p;;wp {^?/p?<CWx#(  $ `8 (xF0uP `24 `:̀f `6́0^^2,(5:Cx t`cAwx=o7[` o`sw» [`kx?l`; lð#|va>  `ozǡ' 8C0 C8>} Gc88N$8NSa' gYp6| ΁s<8`4\Ep1\e+9<|_/U_o*| ߀o·k:n= ?!~ O3_9?~7p;;wp {^?/p?<CWx#(  $ `8 (xF0uP `24 `:̀f `6́0^^2,(5:Cx t`cAwx=o7[` o`sw» [`kx?l`; lð#|va>  `ozǡ' 8C0 C8>} Gc88N$8NSa' gYp6| ΁s<8`4\Ep1\e+9<|_/U_o*| ߀o·k:n= ?!~ O3_9?~7p;;wp {^?/p?<CWx#(  $ `8 (xF0uP `24 `:̀f `6́0^^2,( F1t;o7- l of9low;]nxl }5m|a>;ΰ `w^7=`_zp pvkeO7G4/y=`< -*h拸d^cEO;xJ^l:luۋ73H-V?hͳm5G= -$/n|2̽7KŗG>4<3,XiT4!Qbo%KS iR|bRqƙG{Yٍ^nrrZby-K7-[zANFT1''a̍a]59Z]"9|QQN&D*%(%B "N㭲Q-(GT%{&Ĕ%?=ID]@3SH]Jâ.P[,qVO@-R+ S>`Jf.o@Hpg?/o_ #RC똪=;3}s.? +o|6s[L0Ѳ(iQ ͌,y?G'0W_m?4*?0uNoCoIsB>ɽ<#JN9?W0οKD ;TԾ( ;'$rɳC" KV'掟ǤaQШ8WcU_?w+=Y1_͈ٕd\ڼL>v]$1aRhyşk $Wx-~u-V)quӶnp.rvi]ź Coa+]s;29? `+lwyEYGVkIۏkmXնI976W}!/()P*s6j^$dm6Rw/.vx s.)i..<9ˊKc/]i]g >mR[3R?Ϳ\A˨P1]~6/:Vܤ%m:QŒHۜ _5ɸ߆5q|Zu*[ W5G;rÞ1;~wk.>_ܺ]⬼kʹaoXxm.Ƴ;X%h^o u=`Fr/ֽF ٶݛ:6mB2[۳e HR|n/ٯFX/rp81d9{*yϱ+Cyu(}ƖfB\ٰd|]{YNٴ <+dw;"N9҄iem}lXJle\2\PRYrdUyK1:?͵w܋/9bqDoXm^R5 ٞi;kⰖr?b)\|q ҋ{DD,_-:n6ls /^ .^:Φf-Lif4-GWNK*Cdy{8J0)68k/=QsXNHO[,LPm<&rm_{ .p .p[糜p .p .pKΑp .p .p .p .p .q .p .p _P_]8uՅW_]8uՅW_]8u? .p .p . .p .pK.p .8SHZ鷱ՌqYȨ@3bI)u I 7ש NT7ǿgaI2f٤Ԝ7LdTS@&43*if1LKE>ev/7ܨ}fMaÍObBU`S=3 0 0 06–}QB!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!&ҸzFmFK}ՌqYX/4Hq 43*FĨqzF͌N>Iο>FUWu sJT$èdiQ24 9 gTӌz)PqST#^FT$0 6`CW'> 2b5ϐ'$%)aaaa4cTB!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!5DƙVIo : nN% A>(Y0O %6 ZSmEcZ" Kyweny-[ϊ 8RFw\*Hˈ4.Km{:+jqY(TZt'VӜ|.fij4vb&ڶ2)E~-ɶdtyJ#'0Ǥ_˟&%5`\uLuIKW|뱮OHav9(綏m<xx\c(6(iea~ni'>8/07?Qb}El:Ȱ<{yGrÉO^Ÿda}nĀ~]BIo}?n|NZ/dRg _n4o{?mk)kVXfHVN3.l\<^ĸx& /^_jNs3L1,o率=Ln~ zŎy4lxm[DybS\a$)!xdʚ)0,o?J7'qƠĩ&J۳MQNMyvyN$KL^Lӷu0+.pR~?h\6E]4 㺗 sc:icw`hSTKYjF|6 A-Ks}>mb} -<PGA<}[$t2S/)\ҥc;FާG2~DC[}ۭ<^gmQ<>a}1a\O]tOymX0?>8<> m #[Gܥ+a]ݵkycy>-?>yf5L5Lxǖ#0IC(}_=ĉqt+Zywo?Nihy_jh)=U$ֻMݬ!y#}K9qC`I7K4;N;d "L?Lb?{u*h50qz)t-zvP#ym#r:†1RQ\"ί#0Ҝ 0Li 4}=ICnLw8.ϳg|Ty^npQl*frϭxǍ0)S=MyL7~rƱmFēN{߶uQ̳󜢆rD]燷0c;sܘGIm~nHS }t>UkI,yU.lz!^B 6d[*l=xM#- 09scV} :u&_vNMuiM;߼ unn;7eNi2sa귙 Y꓾ܙfZ]Jf'2KSS#Z;^9],'w98'l[qB o{hH( "P+,>F)[,g3#ǜfRR,Y*޼Fjfzo/^lh)?3s=>Y,S|O] >u.S|O] >u.S|>ۋO| >u.S|O]K`7;gS|O] >u.S u.S|O] >u.ԅO] >u.S|O]|,|_g"k_ۂ[o mYs -ۂ[o m-ۂ[o m-ۂ[-ۂ[o m-X[o m-ۂ[o m-ۂ[o m-ۂ[o m-ۂ[o m-ۂ[o m-ۂ[o m-ۂ[o m-ۂ[o m-ۂ[Vۂ[o m-ۂ[o m-(=p_<6#щdTʕə+kצ_"V?|myu.S||x >u.S|O] >u.S|O] >u.A|X|'>S|O] >u.S|O] >u.S|O] >u.S|O] >u.S|O] >u.S|O] >u.S|O] >u.S|O]K`>;ugS|O] >u.S|>w|*O] >u.S|O] >u.S|O] >u gb.%hg"k_ۂ[o m-ۂ[o m-ۂ[o m-'A@$㿒WrS<|LO 'b /{ξs#+/迷CK5vZdEQYt\U /{~X]*I2CT_O\!Yɩ eH$'ZYVcD+?6>.,˿wD"J,MQeen26&ZY1D+o[wb#D+$&ZYMbdxO2⦟_:uc/7|pBWSOW!ŧ@}=Ojx{D+$/~?-x}4 L''F&SD!'㿒Wr_&7)}|_Pf _M俭OY1i>wv5Z{eF2/_ք$ߋHe8b+ȼ Wc؁2,؛ʾ/8^֓kwY s)I2Ԍ1eԾGo>[?s#-Eo(S/9挹lzs/R{3}V'b'lh>>۹Ub3q~;0_@%B!,:NEc+<ֿt)WLO_! ՆBmi!=S"ENЮBo2UGgߞ3># R٩՚>[քB5iF{*ǩ>xds>D,t8eG(isz|8ݘjP{۱s{x|,Lè>jiUgS}P{69Կ+nzbLRh moN!'^y=# $oV;n mkky̖d>MYM3O,Fi[UUUkES]7VY=iyDźuiw,4nv5߯e6Snar%'&KkUU·I׿~bbØZᙚdzzbјB!B)A~71+ B!4<|LOJ{e2_sŝC%K$rR$/]*听}%5|QҾ矯7pCrQnd=]Z&42Oe\&86OL$V{Swz@o*eq?6dEރε(hsOqޢIw[G)w>rԍP߭N ^Ƴ=s:s-=xso?ZyDhz͝т>őꓽd'ꝵ {# fv?oBzNiod "t3af;.E:46<Iw-1DmnOjdQ3"si8z0?_gW<"s۬;s &~M-{^rmY}G| kY_?)ThtLSGKGB!T` 1b{ݬ۳tKgYwr? U)dTp wJ c]7_ B!]b?^GsLzufz 11DՆBmv&XAI/3)8! IkM(TZ`gRp|8HD-;BGΖdKnsټsG(Eo@M~?Tl%ͮڐS]lFTҼ=,N&x$%acB42ִ-䝎d$/]S}p,).mUUۚb.9[Sђvjft#%%v=~Tפ%TUMc=OM4:LL3D 32O B r=b@+<0_!B!BJ_M cJ|$B*0:N yKg.4gMSZ]:|?;{?3x$˿96c/h|}]|k]˵3j__tX0k׮&->331٨GYe/2ry^cȐxF.,\_䊭"<&ǹ7a]Xtxx7dim(.8mB#BXthx'Ⱥe7rjVv81YYW i"`_wZW=or%+SUkY7Q͂֬a7jb_ɾVv, u?z#]dRd ͯ^z^7rvmh__Xq,6ԙǿ7/qa UՋYf[TH|ջˇ *ёdoluufrFKzkY5Ѧ 뒍~@*K;|3'%ԥ_VUjfP-(nڶbJGG/]8,yUQϢp}7Ű7eW~&w=.YVnl^ [Ld>n=]X]დ67ު3->g77=},}=Ojmyh15|?O$&'"ٟ7K,N''F&S&ڌ2565 we㱁ro!Q!Bn 1bu{zJy;^%iⱑ$ONG''WWLL\\6}?+Kt'>S|O]իӅ) )Xl8]z%]K>#ҳY|)2e3y_Jt~tr%*=37O1}6-3eғ>N37Ok7Fҹ$<Y|);me+o9gV&H&w񲃽xm)d=d:yiLyu*=ϝ,>s#>e,s{B_W>{kdC>q?9Ξm3 )=I!)gnN+ggY,3 i&M!Ӽ@Ϲ|Z3.k9O99Ȝ˧9y˧~8i[\>[@>bŹ|w$s л+# g@G]@>9: HġnWF7O֮?on]1@lux7Z>Z\>ZY|@<is4? jyfiM9'cs H7i 9l磼H'>9 'ߧg,&Efvvi{@ۻHVFzug kZ(Ia)Tg%ۃ/G%ϧ?bӲc8rْnwO7mN_p)ʑ2}6PGRl}oiav6?ujOH>7P9*Qj>oE9xc>Yˑ2|nk <鏤4m7NbVGj/G}VUmk <)ۈDiۼq.+nAu;ߧ~yNgj匿&3וPyIIsΗ)g 9HJ^:Yr$g0ϧS2ƾ(r$g@9NI{'&o^Rh93򜾞Ƨ_s5oUg^LTr$g*Ϲ$/~s}~kπsCxl`b<<91:5͸~ˑ枏淓 >Y'>9s6H_<)ͺ{[>jq[E|y|yPru{U߾ҹ+7>i2 XEJ|-&{/=| ;_ϻ>wo>N7S?~O.:_^}ʹOM^>{ۓ ٶA06swn{产牌۸n_޺}[ڒ?v͸[Kwou2x7;27_^|uͼo'|%< ݷ`-\|ιڑroTRsoSuՅ#[2Ǔ~Ay>q'WO2w*?wewn?ȺSOi[]^8^:ao,wǼFogIǮɟ}m/#scrA0*[w֭ޡ8_ŽΠ'}۽ΙR[._"K?9 o#M~\?-GV%X^ŻÇom>G&yWj*'ۥVF~o`pQ(&M<~m%c9mIs x] `U[**xA(V.Cxwpoy9vL6 ~#t!t'AԦOmMnzB7B7Alg O}}P8zz  o#||zL` D@CaP$4QГP44Z`ǐ B18(M&Bh*4 == ̀؞8 ́BCh!Z #'Z U5u Л_BAނz.>Oh9 Z 7} } /-;{h-#Zm6B?Ah+ ~vBB @A{}~w(J@CP2tJR#P2LPt:i\uAE2@P dJ2: *J jZ: C P#5C- ( B]0|+GB"O%ХP72r ;   nnnnzCw@wBwAwC@B}}P8zz BGУc( "'h$Sh,bih"4 MBӠ3г h&4 ́BCh!Z AKWЫkП7@-6]=}rh!1 )9o(Z Z } }_[5wZGhm~6A-Vh~vA( ~BP"Bd0BG4(: e@1( :eC'$t BP@**J #T Ai TUBUP5TBuYj&j{C!P t]u]] ]]]uz@WBWAWC@BAC7@=^-ЭmPoN.n^z~ЃCPh04=Уc( "'h$Sh,bih"4 MBӠ3г h&4 ́BCh!Z AKWЫkП7@-6]=}rh!1 )9o(Z2lq\y~B?Bh# mB۠h'+ @ nh~$t:%C(:AQ(ʄAYq(:@'Sʅ|*tP P1TR *NCg BP5AwH؛&7۱H`ٟSmx5M+!Aah_Wi6݇SS&VճB{H4V0e݈D" W_gW;Aoޣ^V{*g6͵tbLX>弹^a:$bHyʕ>a.-{ɘ';'+""H$v vź|s9OR"H$v:")ωD"HyN$As"HRD.)ӖWTԞ;S{L];(J%x쩲ހj{y{FΤ;~OMˠ`{X+a?+:jo@u9YKJkehQ[ZLذڀ 0ʧ7fy6K7Rٝ"ߔM>d+O567 7jAYѮl)ﶗe ?G?>*ߏt8ZbȐҊXQ! R>!_h,)(roW٨ XT 6/֭8zTn^L_V("(hURj<ʅϣ}}-y<_y 򱟲kmT0t|lTͷ͓&mӵ6[|. gyN V㽼,"{8*;!<P!Ļ/ 93ton'Qy 68Hn2O3^ۛZq-ۼ/ݐʳE:T{hF^s>\! }s>mX)Y^Y/+:7ڋFǚWmik[6->!*CVzQedXkethݦ |Ap`J49l?6^mv:F y6y&TgK55 ?Cg2?K>ʬfxf&đCP~:At4*Si-~ l] r \Lufj-S~j9CB};T;y|kk6tun㽲R:!8Ir ӛkjfG?X("g}QZŧKEh6tm'mhum'񰽖<r[ |^7|U`YaOj?DlEy8KT͓+무zڞ4vu|v(:j#j̜*hhm<'D 9H$)ωD"?HyN$A9ϝH$' _WkD"snH$nH$D"T9]H$sD"؉IyN$As"HRDH$D"$49H$vbRDн<߾M":sĩ}>hc"HP򼡩Ʌ~@yN ҊҊ5gjjןmls@1( sǠ<'( C<Bm<56!2^+(ԊH^g`Ui]OF_y)@ĉ'HrhVQnCdKg8T$UWĚh_TqhAyN ꎷhDH@Adrɩi)3z+!i1^Y:]ڢ]3<'| 5"n(܍޵&BݨĪJjWÚ֧>nP ''+@1TsKdpMc7QN+јǫ]?풃]b?_+J)@p 9Gy5hDۨЍM=r{]t^cͽ ^j{@-?B6/q9@yN v + /N<'| u\y\Qc"՛(/5qWӶv ݫnnc{]걾 JP>sq\E=uv(ۺϭ_]Nc) Lкx\t5B5mwדz^حٽYNcBG@se -W|[DeZzr_Yj\&Bݻm=%^UskͤtsPy?iQd*‰Z73]н~YC[&dkN tt<*Xܟ֓g\ksV-q<'::-y.օVK8-dRӓj9@yN v9cP AyNh'P>9@yN v9ct˵[ F}emqᄇ~EԪ䞬Vʕ җLTOP>sۯeQǽЅј)O8ҥ+4ܪӶDyN ~碻7!GKm\ؽ*ifs<ݍtFUr=Z Ѵss%v<'| ? ]G}eDk OGK?'sW6s 9cwk=Pj\U=<=s7nok[Q>\:voQm3{uPݻ qH7oѸr,l[) CFgQ܍ֽ֣q뾲@4x`}\j}K14|v@1Ts@1( sǠ<'( N<'| sB;@1( sǠ<',sty`>v~HyN sAy)ݽ[ k,I<.η5-P>yg'_ygyG/]ܧt( +yn鞬s@s>RP\o\+ݵ i7@yYg5FUﴲ -[( @nT-Xsh^Ay '<'As^@97@yNh'P><<'| sB;@1( sǠ<'~\R\0GrG?D—LXG~Z֙Uy.o{H} ?M} ܽgwqU'!vup K%L.*UynW"Wd[m2%JrSy̷k vVd9F.uyZiZA%j||9cP AyNh'P><'|sb St9zE}k0g?'<9QuR^9QuR^9QuR^9QuR^A9f @eKrcs1zE}w>_ÍϫwPwp[wrb1yKC 7 %n'"9C Ϗ;9srvv9Xpؘ ֘B,Ǻu<>k1y{Sƴ# l"}[ӣln<JڷQ<'|>~RVYyB)*2RNwjJ!n4ǗW[ӟ omJ ̸D"=Vuq"ҕ Ӭk޺D(^rW\k0 ̍rs"Ew\c;bȗ1KG"A`ST硶AiN,,U^.+?x 9ϕaxx\y.ksyD{ƞm ~ByN kDnyM,8pS 7Ewrѝb ׏-2]#  e=ayy3><7g/:ag O[F|%bz_B4Rg2ݗf*]en0Y8ū`_w 6"*+ƟunKS5XYA$,yt""qzPm|urSl15D.Gˆuc#",-Bc5#*cbO^L˒vzg9{ VxV' kFB^UkNccf-k˂VuOy@W>'j.Emѫj3k9ϭoR)CCyt#X9ϱj kph͵jNH `E^#rN)5(z~ݏ?uLHd3' 5K-]bx ʥ BZ/rx(~ )l\%=1>]=qK8"U혲jClزtSXW|a|τ|bA; u\xj^皥54n{b ̾/_Z}16TnK%;Hlo*cDD7`|}bÄ O-([b,X-ct7F뮋]9*WlFR?8OLcws?G~B‘‘B#B,R q[98ePus4yP)&P?z~ш9st>2$ts{Lg?y:4y9o^~?Xza9]E'޸4{ٯe-_MȚe se~O*s[FgĜ d7W~w拵HW?,]q[%sZ<φg^/Z  O-,9W7~nv}<~)jnQ,c #-4qqcoʽ~hcRӲNkߞ|ko6~l:g.Y_ :'rߝ<8@v"#Q9Ǹ2yW Y9!2+fD"M>h7&/؎W[ Cg gWH"#GjPgx_isO _Hl~I7L sg>pʹʩ솺xh5gL+>ӝk>"=g+RGsD.pIJj7ƿ]NOnWa'}LTD"іONӷn2Z-C/Bq%eh)*:gM/_AˠCoA zzzz'Z}}} }} }}}VAVC_C@?h =Z6@Mfgh mv@@;_]JvC{ߠ>h?;%A!(: @( JBP&t ʂC (: P.CP! =dBeP9t:U@PT @PtF jZ {P0 @AaPWЅEПKKnePwt%tt5t t-tt=t  @}p = A@GǠ!P4=EBO@áH( = EC1X(B Dh4MAӡggLh4=ͅABhz@/A/C@KWנס7?CoB Zz m;л{? C#cS3h%/s P< %/зAkЏ:h=m6C?C[6h; 4PAߡD( :Aa(J@iP:tʀ2cPtʆN@9Ir<(* !T!T @F*ʡj꠳P=5BMP3 B?@P<( : t1t t) nzB7B7A7C[[۠ۡН]=нP/tt?C h0(4BB} ݑ#-9&i $ \O'қ4р>FcL溃Mu3`/>sУ|g|:[W#OmH Ø>oٓ.*!H cebt0&OxPAK)zaR1dx#ysۻ̮>oqsWūx[}|"qৌl|\ UMFl;VX,$A91A30"dAMl"ȧ( ^eQ >',<!L1 l[A9ά :InaxLM oP.]VnR X eZyJ0.C"[{_\a^[9#:?0 '_z"l _˹K8qWY]^^^ ,;r[@tfٻ-?",H YG)qa:anJ׵sSaLQ}\} 79ӎoOޞgUzńiu6U l߃VͱpS0(L5o;@h /L w8dN64^Xdnknf#kx|Thq 涬 &Mf ,(/:"%ݗ >m*CSʖa9]/>*-+ff/ahWUY)Yz9;56*V {гs+xw.޽a%߱e^ < oѶHm l(HhU,-?u?!3T%̂E^gXfTwykm-%=a_gkζ5(V6}9'_ad炩A'LӇTڈ˜޾·Emt:>t<<c-9gҊr  iU)#89 !7vYp.L*erGi ,t.Q6oG/Z7NCyMqq7ȃ_T|z뭰tDev{pqjVĉt%;D'i?J81'Lcgdˇ+4>f9q$m\)ؐࡌŇAQ9*Dl|Y9WKT?$/my+%Q_ ywsbNg}Od W'?t7Wm'za}(O@ q=W俺8O"uA _]W俺 .-W俺#U.uA _]W俺 .uAOuA _]W俺 }N]W俺 .K _]W俺  _]W俺 ..uA _]WT俺 .uA M]W俺 .uA _]W俺 .uA _]ˁ󑕁;VdbYAT¤"ѩϱD:T}_uD"KjRg&c͉D4Fu O0Ud B$,CSuː9 ST&)pU@TAStg"H$D"H$Db囫U߿'@ @ @ @ @ @ @ @ @ @ @ @ @ @ @ >坒 GV?ޱ&CwJk̬ HaR1QM/"ɱc;IwF#]hNjH \>X)9Ot QMN'Ƞ´&`ܖHafPu'DbR tpwèNa~!)A,"H$D"H$DUb_=P;ZkYP^kjjp~RH7m>U7NzpݎCn=Pyuur=5 JJ -ןN*ǜ: 1y%-E-G؆SO6>|xsbVsa~]]UMML,(h)i_FӾ&a_]}lyn~YYVVH#v| s&f4Jmps;=9)ٮ3A9m|ddK2s=v]{qL. Rg6ʄ;?FcON)&&.N?z;5-ɍsՁ|syNC;D71rDH~1Eܭh{8olVe̯6τa>FpFL{xk#??B*cEf-`*k{~6yu=I˞z VOa]psS'Hf_r@waKnJ.t0Մ|pZ7Yr˶z fs`8|\ 퍶wGkYW_A_ӽxWs>?Oh|.G=O57Tmo^k9k7˴ D;6M6<"dq(,<.+#(s[%yb>ojssj&`̰ocw5nPAAj[xC oIy Gs&؝r"a#NΜh?z~ƝdnOiܒUU;`IJڲoƽ{pRS555[WlKܞT%O7%ր(kO֜?[]__xlnnhiiy87fILdvp׮ܿ_pS .:krQ;5fcE!-=.5퐱!Ͱ|I W.1Wb9W9gUmմZyKYL^-7 "ҦTl2Ĵ%5%YS+GJg߼o d]P:r rduv;[LnkA/Ƃ*ydyhuqQTo3X3y/o[bӪVN_W俺 EcW俺 .uA _]W俺 .uA _]W8A5̹zK5Y:0?.cjR(G:8݇CEoy(CVl 6vn,Yh;oܿ2h~|ci[CKPd3DܱT?>zǶ K ]O{ D۾u]}\^d~~>5 5 OszZcm2@ufɈ9{pZ'ߓ+{Z/fřo?)-KJ^P9o*~ҩS K&+ZDGrciˈ{u%&?o.p7gdov2_CY3߭XEwJ?|Ⓑu3aik /O6M(ZngM6ﻜ&314yd0ӫ?(M㻋J-~c)'-?Z7gnp)?wi#{ DxM~2&(͌*`4_TKK?~fJOU8cXG &/ ʫ~1?v̽4O[9/`ϔOߒN 3/?uӣl m?u3!-a癆NR8y`pn͏;?>tS--3[ixugssW0?7?' c*,T}uύп0VhEY[O,uMQ5#v͆.e~e~ޔOgU=yEϏc8s h0iZգpxc]S?1?1Ka~^ 4vU0envdѼQE-fyL?a3{qW>-둋7uQ0 $eWcAxz]g~BhPG3yd]K_Rq]7х9diݒMAnlS+{'S{䍾]#XZgf!E3-1P̃i}'.p3붭.H7cn f~>~` ϙGdUxrpS0) ,2@7!ݔ~E'iU|*f)wۨ'n;`CNxoIvs73muCx2^ȽA{z ?<|Sisʪԟ#n {}.e{ھ~m]7{Ow_l-dAn>|}єYWX~Nkg}V)]iӬOVZzOg%~9wbkqrg:NM|B&#OGW|܀ J*!9`mz:L^G[2mgА~73Ia@;$?;2O#^I~vLgG&N~j-]n X[W9] ?XIC)TӁiu ;8T`5ωuk6ѱO\Ӵ5UՋ'+i.j9tD pN͉}M{a*E?M;7O0Xs6r'e4Mgܝ֤9wZE?33w2?~Գt >5);y͜/~"33q\0sF ~ڹٗ| J6m)im[~bޞܨܽͻ[ȷ-i;D9S1!f/u똟X|d);J۷2XyF5ѣlGmKD/6~bSÏ~.}?ۉK~*=qx.:m< bbV;'/4`]J~n䆟՟S#/9T>yO)6MWws*'K^m O^趟[ӛ6iZܸ`On ~Ӟ'Og6Yy;IMʞ̒fK۰R ?ޮ&fi,Htƍy!':uE gb|WL ӻP5k[5Ű^a{iQ} yd+Vȓ'[!Obpş/_kQ|rY;ke.&X<g*R6q^ܔi8낧/E7}{,88(x. cS1^j >5Dqq>,qUx?G?ĉ3aϤ>:vخkP]^gM_eJɱ!R; Zݤb}˱}[ UUmJѮ}돿@Ian_JDkuVŲI+]H#*S3tJp7=ڒ<'NwrhcY_Uv}Mq5>s5W &^G?׳?g#]Oң񻑊ShfSt]6nw[ٙ+aѼP( BP( BP( BP( BP( BP( BP( BP( BP( BZ_9h4Fh4F9k{Dh4^/gBW'.v}/б4@.&m}G׸JZ)H%m=puttZxaպz'7OS %MϜ8q&?pׁ.G̳- 35_09#`ԭ.jږ/ƨ94r6cg!^9!9q\.%Ws 0|t?-5݁GN1B6!x8<#˞6 ӚTSWwY9)]XB<:U0z_0m^P-L&.g(hFT!ny<71,R%55Pu|[Kax)חuy ~tϿtUm ԢOSɨOSsk\xlʂ?;=~y.\Z ^*_N=Fc/w} 9IB\Z,do\ZY). 5/{fgx<~K{%kXȟ?_!B|bi1t(!Ze5`Бfi+A.U9NęfB[*6bC9YxA@LY )6o_J-`$V閝ڶT8cE +pFޏceP$ABGNUV·nMGPE6e7uߩG|r,D8f^_zCpo'Q ZҶ~YB| +Wȟx/] Eybd-A5nxJw;p|[47;e,G󢳻9*G0Q _MiF$'*{3 0M+c2o`Wo?r?7v ;\ ӱTjQt4$ q9CkEN?lR/c9"g'"Y30A>;[DDÀs)vŔ_"g'(ų:L_q:PSJ_?;Q6{!PÇBϿȟ(`Kv vHI? יCة&5D]- 8p+] Nh.5P bfz?556T@p.(.#1e k[D<2rي  ,/?}|ⷿ?˥d*1Ul?fRuفFh4Fh4^>~0yջw@4Fgge/c? ]]„H ~k_ZkϽs:xf`(ԾCzZx,2b遫k-Q+5|6 : ]^Y+.d< ce"ԸP׆rT<_(,q|6Ԁst*,)q!4)IhjN@4KhxҒ}b%?l=jC4 M՞]! l 43rl2XOXO%WO*-{AZsvU) *us&H sV#Пj2F?1%%:QMh͙9H$~"%#n_Zxl><)AAzw -ل^9p$((ԠVԴ?It߱Cпt, ώ[O$(9tTg˺ ZsvnP 75@ǣBK7=JNc3c#5(4=(}lwLF3sӣcU]Rz Ɩ6k5 *=d5Db!a_9R~r6 u)΂֯!dbl)WN'&ǃ~ ^1tO6?Z&5?=~.-w???M:Fug'}-^w^6Y }[\l)(䓅*zy9_=vYHRظ,22yk3*h4FZhNyzkWocNv6yJ%DNxdti)Vȓ'[1i|v"ϩ6Sdla&ϯ*O.OK19|֮O`wU<^lm"ϒur/˸)=H5੫kUI{G+(LuJy= Ocvmӭ\tvhZLy Ϫ”^pF<ϽC4׆BGZUϦ]ꗪ9DXi.qwdu>^Bl< 12<-< yd+f<ٜ\[ͳ96^n+ǙϏ,&=DrێT<]NԷVmO!V{*UKJ\ܻ[bͳ\i1(Wa܊kLڹ-1婅u*UqcK8:s6mg[_#1WKlڔϮZȓ'[!OBl< ycNi-Vȓ'[!OBl6YAȓ'[!OBlO7;3as:IU)^& wC?7/8^=7,Ux~I7"#F:|l"Ory֊k,iXHW> 5z ,^cx7ݺ˦PyDUx yw^ZeRXd΂g|:^gSSp>w\.JF) |Vxu8L;KhxҒ'5@YxʥrΝ&%OFEzSZ1%`6uL&x*5z* }HA9޹K~aI{g08w:ó! JM:pEaW)o#(6 _S }lZ5 >HYd%?K_.rIk?zs)_>uY2qO!e1Zp̉nAyO!u$$t=}ʶcySP_l.hi$w#C/=!M_ ܯE=g`u:iw6Ѷ=@OxL>CO鈗O2 _l? }w M{y;~/!M[=TvQd#Hmd ɓᇴ#m9,~ ml'/ ˿S>p0 7pܰ7tws얯m.rv7g\Hx`e$vQP'X@ H z H3@"`lgx?"Q,GOIHݒlڦwwvI6&d/'̼;3;33ϔݝ)RU̼XZAQ y{ndc 43xadҪKTꙠ2-ƿpv{lu,]+g8vgoi;w{S37}wcأ=[1Α.kӃyؓyB{Su#6M{?A_7>}L~x|R#^󁮕X'O Пt bj-|ycV6vN.nt/tW^PoA!aQ1/zzz z@A/6=:=zȘ z h$^ьoCAxh4M@Sitḧ́fA/A!&΅A󡗡PhZAW%Ro9rUhķUju M-m@kwwރ}}}} Bh# } m>mA>C_A7зwh' EB?B{('h/~ CGЯocqt:@P tAqP< E]@P Ӡt(RCH L( CP A* "*JPTU@PT IA!O :@!Z:'zЍMPg t3t unnnz@BA=^PoA!aQ1/P/$8ƨʵAC}=C*yh(4 ^C#P 4 z FCc8(M&Bh*4 ̀B,%h64 ̓C/Ch! C@K2(Z ^VBЛ["mLV# {b_^5/G:$»ϫ{!|ŬȧZ:kK]z!WVe.Auvve_.SG8;DVlzi P{5elysմ :}2->4i1}%uv)#oet z9'Ŏa`AI?="Hl b2 z A؂2 [2pY99yZCaNqI^Ii~fߥ\-3^ %Nmyc̈́^XhaVQQnIW};RFM :U{s/c:w_dDXCEnޤ;]D9u%{=w%%J*:.ؤ/< ݼU>[Kdk2hB.bZF+- El3.7/Uu!-Jw*2YMBl3K_JS])d> El39@l9No}|0D" q4 ^7fNJGګlTd%zuqqrCTGXgQ^Q6[Pr 'm k!.16GsdٗmfDۋF-dhT]%Z$7YlF7Dr<Ź Ŵwu ~ 65q*~T8 q ;ekUg\ʈ&vDJTZRYdDbfI48kUxfOaB6~?S-ܰa)_u"l49W:Ѵn\/n̉v(kg-3aKSvȈ/)#GL~Q+,t שTyr 2b [PyvJpMMk/s^-I7LԬSx jd+tQrV{)#Gی-)D u E|cHHW6]n^&FF4(ڳ1A3WEK⍢2ba0PB9J~QKA8f JJ Ɖo:^ߐf3#Z[{EF$:mՒBqiiPc##ܧʌDSLyyyY{jyyAQqVnp"#DS8q9VeU]P^QQPTv%׺/2pWLtPu;*U՗B.fy{)#G6U}^.9-vxm2p31l6_m^ly{[~EA4y,xRJ p4!U{n A؂2 [\ ~(#EOjPҊeZ PwF؆ eA {QFa eA t_l lAA-(#eA ~  2 bVNrNPS\WR_r5))/o>w)W WzF8-Nh8ݟ8 w[hgG rKJ1!ipEϷҒ64͟T]dxί~Ihkt}a? SFQq~Ǿ Ao=c?Y ƦsJ:'{(!9-JJTt]0Iɧ_:xp/ڥ1RFQq(:}}gE"@'#ˎPzl'bT5l^/iˠ ̋iqSЮ\Ckɤ2ȶFnQOET;Xyn_ٽh$.&e>??Ke]H@Rk="e!3kWP_1# r݃(? 'F2"`Hv@f^^zRJqIF痘-gfb OE ㇌~jj1? :_ll/BX=qVoόfXR{ .ƭFcr~HL<<<5[ /m `ir{[2#r H3DgDo²/2/XD)GtDHCca7z?&sDe賓5.bUkXDRU!\#-yȈ+I(6~@lόT.e=QsޒFtH]*wg"#Y-(9=`_&zFȉ[quֈ1Hɂ'#EF01}?Q땽b:g_vaF $)ODR s*ZyayĮI^X3gس xTd=,2;`蓇Kɱߕ r*<ԛ@M؃߆8VAWRUѭ$ٱڽC uv 5NN7 Fn252 A^#6 "`{(Sh(xF>3Btxľc&R@pN-u7륜 qHME.GH+WDmg18א_}xGxEUU'2&*O8}϶i0#@^a"T7,[N&7 GjOaUͺxԨD _W *_]WEa{U{I6'7م(펰/z)B)jW(#)69d9. ɈHOC>[dG QH{P^Ya(.")c¹*xu E#(f]B-޸}M(ݪCLrR4G䷠\bc/Cn/8k_c9Xy4'8<2~OQPRR ń?_RozP|FMOf2#Jn)F5K6SuD1n韑(,)!6mP!_^ڐXAR׮i>YTޕ\O;KkUG25*8TbB/gDQIINA$BaID|CZ*KkΈ(-WKKs +e'5+tsO+ "/_,///k/\-//(*ͧpn#T?#"_8Ѣo7f 섖9ALY~y:2 \S'G]]>ЗN5I  2 [PFa KF5K jCA-(#eAkA2eA6 ABt͒ Z$QFaV.AfD+~āy#*TP+#B] e*Tl*T*ʈV*T4}-+#;kWx/ 9-{TvbaOAJ f̈`;>VXev [)#Pqaiƌqq o" eй*4*#fY6oua## 432 Yf_6s ͳH2 :kWP&>w,u ;p1 R.(5PqUi+Aq! D"~[0tGPҨz6A~ W쿾X(#PiҨkؕ8 8阂2 6^- e*](#PbPFPBVBBA [2 *J#2gIIqgk \eA 䵔AXAA-(#2#Z#p9o{PFQA؂2 [X2"62PbxĨ$i ,;{0[Mԇa?!1v/[wK͌8_!q8$zRXz K 2:-8)CGR?K"k`s/ $>;՟gćq~l1D~"˞rCX f%(ozXQ506M\S%XdV8L`9( fEE,o$+^-fM;L15x8( \hbWmC=AH&SpH)]inc-^6D-xћКȈDCO P#g%dKLID~tqњ32 7ύI|so;?m;>;<~y4C^#=b=w=hGt\a\qXQwF DF}U:Tעp&x)}B'l|" 3*G]}^âsh}bz ={?.Cnkx,{݃E!"8w(_3BAQ@8јiN/KG^˘+O'?9!OpJ)O}(8،2n_qpg2{\c˜⡙ ҽɜ!k^4s:h^ޠ !Ba|`u2bZngQ.tzc]cxM5Պ\W^L3c}lXOČCgy J !v^4_v~˽F̔t32NxftuiGh{M=.̛֠gvR}3;@9ϵP[~4qM>q+2ʫXSӟӑb{hjfIYr Q{?}"#t~<#=Ć~4m4V h]i,GoSFdDy7O5xYw߶Lmc;iyځO=:=Kǡ*M;<^2s dD-w"S `cEP2KPnޞӇ˟b+&*GJm+1gCxiʽ9挈}SԂK)#^4g߿-(NVӑgĜgvVS#~:;hX5dF &DwzzSb UEeO<#6m+cnL1e^0[^cr~)#vCq۫}J2B7S-:2};YMSLG"];DoX0%@~JO؉9X~1URƨ1ckGYbwVt{>m㊱gm>dctr$מ!Ϙt:l<A4&#̿A'릭M{U7i)N74XBFgb=# Lbj@& #p HIdD9|9#22$KF$ Q:k4n ڦɇYq`,ҧoܖ'WJ ܪp_F+~/;4Eʈs!t^pݸ0 =+0GNa8:#Ǚ2A\S%}%y=3y"5VyquW@kTX͘r]>o[c>zEFkf蚬9ofN_afﴌ'ZbQi=2:sw<jAD8ُ:O8"Yd&yJ({zV;8vM[6h^̈PM{W>#&1pz!32LIr|\蘴$?x8ψ{J}wgdRXR .;pQ=w}Q}DSfpEn:n$gĘڕk__SW%iCggI1?߅}(0G #w:?]N.`g0vv]uv0񿋙zPV]XhzOeSkdńഎi X`/gpr?2כֿ9KodNM7ivbEU/e<1`*+R'"#bDFĿv][\ZK+'iIpW^w5ĥ˘v5Kk eF|".)5o"|[lIjm͚9of9suI15#C/e{x-O}q^sF?d ?3' lY,~>kGt>882EȎ)2Տ#u.X"k[+uW&-׍_ Ӽ0_C) y|޲,52`Έz2"gByÄ́ʽ ڹI>ԇ ^ U?73ggpNMOOMrIOMK~oȈ}_"eDpvzQgFȹP aDdy3ϭȶT%價xuo6""!SPUxUylVA/q34ݥڧVgs%$)qqΟy>2(8:Pث:c2&I'D0UJw fD6(6@AyGO.(<֢Fy/l%8E &|3OѼlU]8s0F2`㻑z =:Wyxp,hbHL8&'[@KAE=WYc =†'q^4XR0rr^&$l̇+]dK;Ȗ%Z67Ѳ+yyʄ=g[Xd βX%>E󿋣lVhƞ#s\j =+Nr$'p_^ {]lzX JG$ˏƦJXw׬S4L{bW}&~5hMj n]^RӪ |/1>5g,2uM! [wٶ {}дz?D/t+ؔRec;u M_}֩wr*/傶hf_0|mc+7~3Jnӻ:S.9ºG΁362RCjMebkX>ϼZͩݱ7,o3ٖl:YKD[vxyQ1%s+Dta^Vs5;S, 3 ͧ[Mgׇ^Pw T}D.oi!L>kY 恕o=5Q)X=,OAဲ!nG.!y/ZPaqyU>U9ʈzGialc ˱Go?JYZUۋYx`Aɛ :Y/->󝿆H[tW`Go?E_gU҆PdZ SmŶC؆ͩ2!(}`Eϯ'=z7旋c%inTE?Px>ϑ|$Bb J316-DPD WH4t|b\ٙDC Z%7w->}.BVбOb+-k B-vǘ'M{'wFY E"\|]Xvџ 2ߜHZ4/u0a^d!M5n3#C%?kbȖWCn_fEZe/ForbUo5|{O#ߴ#s|"Zs`/g0 QL;49=l7:rmsK7S!][ߵ>u-w-BL++M?\6 `klj,?К?;v -B*NWb7_a+0#.Ҽ/h(>JgyY6 G>wY~sj{F ?E<p}~F_Dݐ#:\C;/k jwZO3?ΐ;ӑ'wgߝwp7-?$\>' O3?ΐ;C Z_ߝ!]K+|[A K$]f#Qߍ-+ZJԁkp!lj&J&jW2.-[:!`5A:F.o)A,p_\~͙R@br1JB1R2xVasZ łѷ5Z40)-4Q_&   rs\}W*TPB *TPB *TPB *T@     h+.PL8MϛZ-ʳ)U'~;Wz(*8xSEO;Q ~:VŻ~/JҔgU}QS\yRՉ U+ GUrB#5'WRۅ+'H; vĚR%l>S%9րfiKzǏ'ۇ,~B #L)I[́ml20_j7&a(.ɤ]9 ~8^\n#c?t&-d~*T*".ёRvqjի%m%%Em!%qJŒjSLܠRmHC+F.a TϛzKzDt9r_(yp u*պիr_0iskT cz!yU&* rkh:RzlhWfgc߮Y|7.nj>|d}r2vXX X?d0Q؄W5@t\\B6д:y,nx-5/[V-S5k#(`S k 515EMpi E 1$X^{ d-M킱ĆD6<I{XĦfaTgW4Rf?}pndr[D^GP֊kIe. քd|.%Uƌ }@6 S{'=w'c羗<ݿ[$ַ6H`?~5靖7~Yeu~[s3_[9Crn$ 2BM 3#Wg,5 7xIe5'͆n{?{*K]73u+'iQx?uR὿{-zX{d6f'œ/?%YkB3ߘ{mv$qEG^ȳS2vC;-B{,́e?Ec׮^:)=u,]4V=?0cЌ~SO'yF=À;EOǚ,B"qdަԴ/L+XϰCO'1&y\`-ZTE9C<|zsntٍ|'>mgiA~\C?۹EO%ִ(m#K8U7}6;,zuRHxz(Ōdx_{M}W^jHH84jkW*nѤ~EuK' [2A6@fɥ ޯWx:Q=^멮ܢL+֢.JWZ, 4Wi׽faZ _;f;Gޑ0Ot Lݗ"nQa{bT>@3S_|<`vXMh3s|EGuA7.vm`?> I.=-ePi2Wt_QEA?3H7˲G U7kư.u"?f(2UlTp@3+mS{AvC3yP=~i}SM9nleXtN OxdV$X[yVi4 Lw%;.}e_HdJp}R?P^AwGު>cpǔg¾qFr`: OrjѪzE?zhϘa#⟿= Cސ̵O]sqmmQ]@gch܄T~};凾]v1|캨G}"+=َAwgE]o1R2wSE]97b%E.w;O(G^ūXgjclД ?O%$6D!fD O4qtm?šq"\V xusj!"%,/","ȢVlyjo.[+e]dYdE B5XȢ Y`!, [4_EdB5XȢ.,UȢ Y`E B5X[,jn.EdYDk,"ZdYDܢ.hNڕhZgEĵ:_mFk.#8nQx|UO֡E7~딵}<}Q5{윷lZtLۅr"a LXX^yE4Yj[mo l}&=ɹoeik4YԌ Eβ([ i ܟ)֑EWNu |O%'\K ?9){D -RF}K3*!N|oѿF"ej-ߵk ?WQQViId]I.4hKsJK_-mFK]-Zj˹bE/=!"'Xk+,"ȢVYDE-be#f Q([B]},"ۢ}{-+~"n_-:wwJ?v-wwJl_qwwwJl_qwK+Qүޯ;~3o1Of?^"KX~sŖnp'+_+MJĹ*ӷ/j&?/RQ-+MJ.ba{ 7 vJn=_7RnlQ>]Cs}/w6G4+>}.BLǾ?ミ;C_ߝ!w'ߵ[:QŬ`tɿ&M&ф;_6qzzy "\M8k=d{7lXȬ_uamSԼ[X6:#of[Yn>`?Ûkr1w~%,]Bi]6:[mt~:mxɴ&>nrLOml2&.  4SE>l}ic&OW6Sppnq:>ԫmT꛲Ï4"j#~Hn7Ҟgxzx#?5>Ye [xnQO22u|(R]7R}|k־53)Rο'ƿOz>->D͇4WWx3l ޽q>SM?@I]sω6y]x Qj#c[3iu4{cw@wBwAݠ{{P>'W^ЃPo!0(z u BIh(4 0oSLӌC( C@h" 3 1MB)Th4̈́fA9\( CB!&.BHh1Z EAˠ o9h-ķh#"2 &h3*?kПס77-Пhh+ vB;]=h/ >!+A@BAC//!(t:::NB~~  /3 t : Bq( J$, C"t J eBYʆC9ʅ|*b*ʠr2t U@נJ:TIF!wA!/F&t3t {V6u@];;n=нPwt==@C@B}Ǡ~Ph84 @OBCap=? _iFA1P4&@D( &A!d(MAӡLh4ͅ yP8,4@ EsP$Z-enb_ʆk/@h#"2qx0m0 pT[#K:fCsv| w-|Jw2Ǟ[ `O3·úp»0Et| u:pꦮL%8܄V7fx-at4]#c`{i&uȏ|1+S 0)>쟏#aAMGA؁< >A!${# ќK+䕕_.,Rx )h:w)W]r)KIg{^4תmR`翾$4WړGDCM|ڂZ;?%+6ј言oFJ\)## s<)5#RZt ^|J;vex{# 1~lC`n͠ :ihWF~< Cx0/HB ]Jk6pg8G@_PcAx봗kc+.90&:(/(1EEEns6 )ٵx#U6*&-:,.C6ŀ(iSGc/.yEEN^?(!lqg`dm)-,r/ꑍn/Q%QM'ƈ%y\Sy21s۫t> 3JJػr 1žhy ;w&7b6lT{k6J˗Vm9F)?9YG~HIGxFAi)&l/I|UREmM<ر՟)_u"K]-ٸl<^. ښ(O&fQI\%$QXZ JJŶĒm%xR ,HķShm^~jٸQKy{,=RU&Y`(1f?/Id;b9PGc(*+9R?0iA $y$A}# =EK [AT} P>A $y$A}7 CIaH GA؇߇~; 6Ai=zB=Z^h ^Vjq{&򨧜MjuD2\>gG l AzAZ<%DkH|p1oh7Ztp }!ᑾ5{ ˜@|QO9[,o;(Sr`d>C=A#cD?m+j~Il|/Z|V}C.(^/mVD<22F"G}$}Q/8*z$6. X\ͮizF}yN7=EGyM}U߂jz~[B#B=I䍵+7: GrS\6._RQO9<=ydӅz<#"Uc#8:jrP̪:N[H#IE`x c+Ű}?Z1Ul56qEUyWRC=`Թu*7 dLsĆC`k6ʋ4?E}Ш壳:zI>g8Fj~&Z\yZQyGJ[Zb}vB=5|ܹA!o-A<<ydKH -!#]/#[KGhG&G l-A<<ydKD=j-P/jdH Gyf@{H %)>H ېGAGt>  H GA؇< >t͆ ># BIaGH ꤴT hTJm0#)(((GC<%?<)^y7y$kFHjA)((T4#HͤYNEFF`Lxf_d}T.cb; k=/^|eGRPP4u}P/+[pD 1>̏-N>2c1b!b^1LIAAќpxT+9AMAywH U $y$A}y# GA؇< ><~? Bc $ y$A}# ;Y0,O/1.i0^ }D3FDS)0/~#O'a8&H t@d"=2,R&`<o?2R#M1x<70&ѱ=}QwJtbǑI$d a}N;sPb'ce#H_cFc*Lq;NDKA-]=c.=Դi'4WHֽAYwLt i?"}H{ GJ׵Ukenٳݵag9r3<.{fa8|`'čc>&aN"w\/F>uÑ`iLy2=魬PUʉE>u."=m,/AYeC3|$|{O?9ə>s|f 95UwdAu>Y'=p#_xdRO~F;_ˆGna <z ^|o3M]4h7WeqOо^FsF]\y/_>0]V2/z8>~{c oyb;Oܳ.ޕlΎw'2On{e*̹*7yo5n2V%/h~a5i*_2_u䥾Cge>e3?{Hv@,s0o.qsY rG7 #-f4^ +"]ב.\5vڴ#+Mqvj<:`)Ɠ橼_eXn>euH/ VLvS.W0Y>Rx;Ayda|VtqeajH;gߡ^h]JK|bzhˌL,O9󬹄kPMS8TO-M \ 5Xf 73jM_\;)*;02{#S5h 7PcbFIbݝ,~ ڲGgF$ђ&C:?KَPnrvGOc+ ]|Q9oj׵a9k:/;m󊹔>^V/Zs򀜘*sN*KPT &BlB;|ĆΜJ7{n8Ξb" LV fz&׸;sA؏|Go%rSNK9k-We4bNHpZߺ=ƈ`?{dz_|'#~dGH &qzɺjWV-GbUvFr R5L4- ϿaMvH3f~ȹYCgf>1%C褌R &/,xygrior9/覯N^ ^=~f"YOe ʴ>:yq\]c?Rxd(wxd 7]Xwu^)nNx˅;_wWk%d ,y :K͉nᲤUŘ4@7\ݟ{N~k7 K_Y}KyꦯMZ=!Rlwzo=#eq u˂ap! 1>g~CG-5Q*\2fKnG;>.|[ Q sA]N7}n mPTEJM9:#Ws4HX#GȼLFKaAMA6ݺ %b囆 ez\ΰY)_HF{G<\Gt2τAH`aL;P=A8Or4^gHS>>bQW^y;HaJX]PaD g9yv6vͳVgHS)ҏdo~&?GŊeU__o2as b0~1݄h^D TD]p{jeansX<{]V_ 5@Da)>G"&p-]3㝜{p :}} A1ߠ( -ww1q=t#o?O_SiW(@oP%Agdt@KP*CP&ilH =\(ʇ B*JR *.CWPt CUC 'jyA7B7A[CBAP'3  uC=P/A7zzz =Cǡ' h04z C>)h$ = A(h44 B(=A$( BS4h:4 ͂fCsP4 Ch!zCKP zZVBh-Zm6B/B/A/C@m6CB zzzz zB۠h' ޅރB@C@BB }  )9t:bA_A#Q[?c? {$/GB?C@ЯP,ߠ(JB9[/)j Hpen Qe*#luF (bdU|ĔD}0i6#L>\/ }aR ED=agRxCΛPt &+ \ǧ 71+h?ةUL v.vlg٬\sEٵZC5D}9)Q7߭p ^.ܼ!喍)^pӺ^/g+ۭ=V& Z ٲ+əD0ןma_ [|Eűg4&)_zsFų)ʹSQ5:Rd&ը^D[έh8@."1eί\f_.x T۫?z>ɛ%8^G.`#1sήP<\-rYEsUսɂY̵h<-{`ovU{7}2%V=KPfӺ)S2x,[6S>;#Lo0sj=YS>fu(#p[Rl^]m^ ||9x.zHC>)u" 9hz>5cjc:2%/OwVԻ16o(ft$i,[ˬ(Ŧ_;oH^bͷZ.>h:2MPsLߵ߰߃^[LeCg{*RSQSvjًQKSG[LR+ >cVU|@T#Ktj-i-m\W?uKKwTW.KweBOϏ5^7mPՍ~OoҠTueK(έÙ/b"5@έoorʮV4oR:geT[5~yRTzڇ5I/mȟPu~R[T_ho۴yTWOweҞOwaTWFO͏\ODhQ=ׄOweTWOweTWOwe#ߕKKAWOweBTW.tUua #h(>;Dy8PD*f:=`ejq3 A?Aњ!ĉUoKkDbBEʜA4'= \(P-P<U8 qh0ܛ$L`&"nq΂ E/AAAD38j@]տ@AAAA4)mTgϣ^g5_ݠTunPՍj/3Lc|72}-RmE-_̆GG7EZjcEZ6ط7M^Ul;NcDJk~-kpfu0.mw:R7_ݠT]UuFAAAÁO*'W7@        Uæ;thmtDy8Pⶤh4EYzBMej29QB҉C2uL8GjC GY(J3PwVI4*a׉Fx 8C\VL`N#_   P\OAAAAAAAAAAAAAAAAAAAAAAAAAAvB ._     6vIi E3MZoQ AW7_ݠ?ڗ)bx;cNzY+Ogԣc?{JZcb4 Fi :|>f*\jl,<@QXً̛OcW//տ [^孃QK+w?WߨhשP A AW*=+AAAA|P?zAQĬ_/f %d! q k6H&;=nOzMĆjt|i>eŃioɄDEw:hOkM^G5)Um*ڕsy \^ v*Km:# W WJ sЮir+m˗Jrk`K%JeeE.;_,ع$+L#YLM?ߴ0ǠKWJ-F:\3w"gw1gMO/v1oYg@Ml/oea/g7vN?/g4y*-=;^).4ԿEh()w[&V/9(%UQ\\jF;X9s|w$($DG'⼄,*Ʃh(7/WͺXKb,(*s" }_t)2g~L#Ic07נάn/o[rpԒRiq.R:,4_L :%ֹoZXh5degZƷW(ls89X96@AސɸPK^/-$m]zV9^6]HWj` 4JcgY Z>~t{*h] ڣ=eMW9-D 66P>o0j`r_vi%lnK #[d{UEEnaeln1!=,SӘ|r#ƄV3o'b9N\<ۼzfhٴlNZ Jn)lHfcr0º~ ,}AVHxN\]ɽF Hy S\ eq2P[)7W7ifGjd{:Y#@a c㜐~iSFDArRej;kRz2~m˚ ?ޑ-s^" uѭ]1%;*HhlVSiF<2&)enyѡZa-C^Owx7[\R^@ tkgVM^6I9!YNgONД46wѭ >ܒes6E_{avtK5kT次iSM={3&H<áLeD ;)5" bH7..m`׬q]>o3Γ3촍9N<.i{e, ^21{i%EOk#-۲>݄Yc:&?uḳ7 }ulX%ðd9RwIuOƴAYOk^4JW;.|`ΜG-I-ücn3ıH "R+le˥]ӃXd_"#|hÇgə~?_C}-7yAw0ӖĻ\~;::}r̙Ofɚ5gfΓ9g|D7~bPLGV9<.Ƀoޗ&=x'BM>oFh^YAj&ܡ 9kD!gcd< %$=ycs;ه}>^ ;.]@>گÁo}o:WLv1=?{sbl/Ik` sf]&rAt3ŖPdۈMۯؐ9ŀ \}BbhVNk$|7&K~)lMOGcOa0$q<3Ŝ_laMԓFkjذǧ[gBt2 )zfbo&g}$gE22 )Cʰ3?2l\dP/ [9}rO?k^ ] ÖRPNz>vs0gr(Cʐ2 )Cʐ2 )Cʐ2l [qEqլT2t }L[P!e U{¯zΐ3T7rkȰǦ a-(gb/gr?y9Cϼa|[k$3lw3l3t^ʦ;ұRXy eYћ7v݃ F+Sw2ӨeG*;ZϒוMf=E}%ݸE1wo#ߜVyZ7uh ޚ)q2[6&ߣUtw.=FiukP1NaV{{3ouX&i^rSiZgɰ01I)ũw”3lTdB(mE:G{^l|PW?uS]?ߕ #]aFBgrjΤT C|X[z.=\ AAAA4Ꟁrq9QvuuurGjrqm\s r[/h]|;?ҩ΁/Ii/%bSS/=}1Ⱥ2MF_-ї~l$G#_VjMF_|k&u ͹Ѷ䭿\.o6y9 RKey ^܄TT77*z墂KFן  F (9_{k5[8L#}i俐mg [99{VAK{ ~UdlF:-Cwl)C_uH#>p{ԇ2ܙ͸zdأl dOb$ݐ]ݠJm8ܢL`_7B+]FZ$+WWX9<{iNLY]}4g |z~=7:`~.J\w;K M=qyQsq{b[4h2 პi?ou  [+ k{>CmP4ޗ?eޘMr-/ub|9ev˷},yrF^Nl7mݸqWk|FXO=1y|K?H ?/jԻ~i|$iR؁w_ٸ2w >i~}oۤnƸ}bYѣ[7&B|=2y]ԙ֯1v󙇿AQ{oooD[:oaJMm;<5TKUX x`e'(b,$&B %L (Ex*oA1 ʩH(@ ?[Mٰe6 <ΖلKRW^XE/O9[{Fvkq LM~y?$k <h$4 exL@ga1Tsǔh?Kom6#vY欝}|gGK~|gx.i/.QN]7}f_jZ0<6NVn+_${4xiqe:\<#K/5kz0T~2Ggt>F,dM:_?@BAT!nF(JSN)b}/MQ.nn  $ =uz@=@EmEAУ%z B~Ph4  !8e4 FBh,$4M&BS$HS4i(C3hh& ,4 Gh.4zGE%eU_"5ߠ77wCh .Z-ރއ>V@BVBBAC5'п>9% Z}BB wFh=7#  ]n(J@{}~(:Ca A)P*$P eAP APTI*J2NA3P%TUC5YH5  AAP-]]]ju..:BЕU5еu ЍMPfV6vN.nO=PWt/tt?zzz^#Po(z BǠ>P_(@A`q(  Aáph4@c'qxh4&A)Th4 EAӡP44zz͆@sy|h"2 *g/BhWuoЛ[;ߡP zZ -CAC@+@+U?_AB Z }} bo;h#    m  vBq.h7%@{>h?BI!0tJBǠ(Jҡ (ʂ(ʃ:A'P1TBeP9TNCgJ j:X/CmPt>tt!tGb= u:CCW@WBWAWC@BAC7@7B7A][[۠ۡ;;?A@]nн}PwA!a zA1(= C!cP/ C h08 3gmzzz zzzW̯ۈ]BUdl@ sb`$(W,N]DL`Mw9xyT=ĚNUH+=O/BXqvB!uGk1M8p]0xcKꫛ ;+_gc=SpN{ؒOԉ\)4rP׀}/L~~$g&It h`8e3~3]%uQMVqv=;GqQJ&tW~S8YCEv<x[*B?wSKK&BiRBqz)!8B^J!C/%=?rDNqNV>yʅTTVQJ3ŧN;~g+^Z8˜JAS^KZӲ>~}go_t~IHo|M1ѡkފ,(+|饄c)&KDۿ79e#[lD^ť^/X:N T [2H!-7pz+$f/? G{e䦙 e CS"'8=_k/gq [J**JT{q"\: p,=X4$|Ϲ*2Ԝ|^z"hI}/oF^v,Nϗ/%=Ho)%&$UA)Z%U, D@{8Aa/'r lx#͖3SY_K/%=Ho)(Sgt^iJ/-HrzRB܃SOWO**.?nK'_z)!AzKeeʒ6{vM1X]S#jTVUU zי|4+?EK8m.ɻmEQs[^RB\ ּ~4)/$yyRB̄lK+6uof.|饄d`/;Hvڣ ?WqK/%ǡBXy)?E!h/c^ۄB^J!Ә1.hIјl-!{᥆ !QK(Bi6KuBHBK !q襄8RBqz)!8I/=K !K !q襄8RBqz)!8BG|&*^J!A/%i^y+NS-'ڶ/5 ZFeO91\F2^h,-%襞RFcA/m)A/8w/(Q"cEqpVK[J4. Jʋg/81ZF4p߰3-%襞-K{.W̱&R qb,܂9bbܰo(A1;z*Va/""2~O_%H SRc'E,׺Iy<^R "2V@U}cMV}Cin)]V/Ű [^ZK> F RzuWOU۬Y 7 { x,SC~OaYTK4t! RIvK^ȚAQyac߰z!e({){{h(K#pȘޖ{h^Z'M}ac ׈ KÞO 9BK !q襄8RBqz)!83QrK !qZ-'ԇnZZ-#Fz.A/%N^ۼtƑ6}5p|zIoK-CR$襭>K~MU7WQ*~sծU;cթ_NkI(%2VQ#BQbz 3Jm޷jFA<uZz)qң҈uk"f-!G7z)s6A?N{K+w~-8 y颠x9& U̱(^(-AV-= lB9r=?lx#Kj&/^AOJRC=2vehhmmJ eV9sBѲ2%EAQB]+CsRbnRLRJyJGMRahZpJŨ6bMdV'%jM^K5<ϔ-[ʜڭsobU&M/gf*wƖo_o>؏ۼxE[IGUy,/_m9Sx4Eq _>^6R:7T'F*ڷE{#]YB}q|_kIyvRڑ;Cu8N9$[M4*D*|}^Z\^Rl7gHMv`ҝ!`.L8W_#QvFcbt$_i<ԼtnKQhiG(A[pv_}r3XzT=P߉O;N&iЍHiMU6`>؏g\R o0p|I :6BsN ;_ZGsBElKJLh[*WR voZK'(gPT9a+ֈ#+-Gv ~yHAi܏CKk79{3lxɗC^z:akфkpz`;T|pQ9A/%Nĝ^\(;Ó?q_פCSIUN5*wH/%Nx)vx"xmVgv>\TslO݂͑C;&ZO /{Oĉjzi oRH ;UGbz4A:jUv oϗA/mm^Z] 'S`Zߕo}ٯd'K6/mnKSh^J= !륄9RcM!-ymK !ܡBK !q襄8RBqz^DBHꥬK !襄8RBqz)!8NR/т0i- oz)!noK[[K qVRB7@/mA/% 8K%u֟P 9\-=RB܀tM\1 ~9'{.׷Xy. 饎7:/8JB53٘]Iv7"/3w^&^PpK z)!nE^DT]Jވuk"7{0 x,#pfyHK q҈u0$|_5s="(H B˖uQJP &ˤVΙKz)!n^ u? Π6r 9M/L^zӼmK z)!n^^J襭>襄ziz)!nKw>7Т؉EHz)!xRBqz)!8B{"^zRB9w襄8RBqz)!8B^PK|BTmVLVLLPT1| j_UNVʟPJG)#S( Wʆ 3'('ҁ}E1@Dml#n*ʯ>g#}AtZb4xj#H*7W.*^:q}xiK'BL9kf2q6LD\~,P' !-ZbJ[CO/gm_^|ޡݾ{ܵ~ܲAk{ߟ;#Uq0Rr`cgԣ;u2o֦~Qjav,b1|%ģҚY+{]ߥo}oc߄汽W>\xCλ?=ۮ%MzHq^9Sx4q#unj_WjkS`6tpqBH+>IWu? =vox}#Svzӻ ˼6,YB. j+TO{˧K߽L{ 9rMj?Mӂm)<m' ;_!MPd;;;>t?Y7Lr e:=N_4m%g:a n}o  /.RO{hLF=e>4.nOf9&k]wIv!yRߺ /=px)gx^z͇ۋ=-Qw;J3g ҿT[}ʙI>xvEت'jsWsg&bzu7̤PG&T/S ouvUpkh\͹❭+:~!gkkrۭ SaB\ Kϩ]ة_Np*UARm~Z|OGh،G#2-&gN~0"ۄ4:?uQh8{77snpׂ!Kkk?g+ߨ2<9̪pz+'\<)#,^Z9ӿn 4YU,v{wo3uGgFQNcU5x4,ŽsSL_\D0ySns *a1V ʟ0},,\5<0*FojlMcRw140$]m^ ۄ^;LJfb.>S*Z5R`f'{;YjX'F0{Z&!,맛OזNqk.Vn,3{U_+TVrD)ףmXK GZ>P!zࢠ8\'2Bf 6#[G@Dւs;ͼ ^ F|v& / 7}DV6i'gw .*7̋&JP2FT\'XV e*Ƙ?7/- ֩,en䜵spv~JT1yiv .^}H3tkgNmpӼ͵ A,uy&dL>U#xE񴐧2Ge3lfΐhNΑg{eXnA9gv|~+4;K_' %tK1.&=̷xsF)yN?I7GWo0yi`~鹲&/K̃Uih<#|3RT>ʮJ7e@s4y .^M3=fβu6xm33 E~o!h qmx?3ٜgrᨃg֍sse_50>+^/MWztx^V:^MDŅ>2V,[>>f(Ev̅Yf_s9Ke-ڮ@*N͟k~V\0kUj_XV}32`asYoRv e,X{䍟sov ۏm._ UBQ#ʼn=Y#\ z",TziCf䌜;zvgsFg{ΑY7˺!,AYʺ~pt_y۔7 / ea-^ڤ\kHlq~8@HQSm O^Z'ΑOc>GoZ꼏Ο2]rY~Xꮢ)שMhVk9 A6!X` ОOf'= ك,F*!7>yetyMKfb CQKEfG^̛SͲM;)̇d7"]kkhCSꇅH;~z/;/@}D\Y\y͇m>,|77)~=7db;ݭB izK̙ťϾY-Zī3r`}&evh1:ߥ]rTK{(eO왡Y=J Ed;>RXrwd n|^J`QsrXU,?::R޸sG>3gȌӲO y* 05{;?1/u}Ô'/H~# pdG$pay~> irx^J J%ol/zxZ$ƒ6( N~%O~`Iᬷ f?y_΍x1o yO='w쁑YOevvk+xu_zicRiqJD߂>Ӗq#{;0!6B$u.U~;1X>,NA0)H7?wr¢Mд[x}2? F3zxL^AJ@+/gj.GK;͑<3}p'!{ѣuӜ%S*gU0ᅼgsEfNztb&{~kKoXޤ)O楘)x񩊆櫦1 !X6CPiU i\9.yn]^eb:k"A/i VZ4A%A!DE;q֟AK_CVyo$I}E%pT9f1(J t龒h_|~B<§9V/XVgNl^/FV'?+gӶf"IN˟|rԨ2GbLC Ev4+TH3}د|U'1(pӵvhRD@eS?a :rRse]M !ӀwI7_iQSڠo4_j^*NR3RXoa@4>"}H2ocC6{o?I#{]i TYROkȵ:I} /.C ꠰>R̗3-P/'/~Ktufq C) ~ FOi8 ]gF E'r]aĮ0 Zz&6wh1-ޅBˠ{ CJhO#ch5Ч>>B_A_CoX[h=m6C?@~@B?A?C۠B@BACC;P C h/%BPt: 1(JҠt(ʄl(ʅ|*CE $T @PTU@,T+z<@tBP;ЅEKPR2# ]]] ]] ]] ]]unnnnnuABAC@ݡPO(=G`(z B~Ph4  !Ph4 F@#Qh h 4z&@)h4MAOCP4EC3ggYlh4z=͇@/@/B/A/C@B-AA^  Z @Kw2h9>ZZ >)Ϡϡ//W:(Zm6Bп-O6/Яoh'vCP C CGd(t JR4(ʀ2,(ʁr<(* Pt: C%P)TC)4t 1lBr΃6P[(:j#t1t ] ]u:Aˡ++kk.-ЭmН]П{P7^>~; 0 BGP( @A}P( BP4 Ch$4  =BOBh"=M&CS4i(C3hh& ,4 ́BAE%eU_"5ߠ77wCh .Z-ރއ>V@BVBBAC5'п>9y|&AA$aʰ4Le\31_<c͌* U^Բj:,/_ڮov]VAr#7؉V|'P|mE. z sv(aʰ> 2}]7=̮U 4jrB6y,9'SRHȓ}^hq2 (~V';lBZ/xĂ8gB h4R  BrMOfj+{:"ͩS,b/}̺²Z/L̷4wx>?<4sٖZ O?,8r#\BE/&<XfaevB2%1|jY]Sk:EDT( ʌ=~?=ʌxwf|55wN:s;%yC޼+8$ZpsIތWog;{(tXZb~w&t+IRy唄]ކ 06;Cto@ߐ]~Oga[ռmesr lyZt5׿߶\y\Bj*@a%<_G-X89}zս,;]Ţ2Mʥt3o}uJY_:˭6L67}.lpv$O_nWAU?PK'[r*HIs;Qamm!0he@\,:RR2):jyyt'dP<5V<`sHhiE -ܺuٰ\ Iѥc?ʀ\wB߄rAIymy@05قYS%~gk}%SC2Oh5gs8\_NFVT_=eeh>LHhEGL1|g-u= K-B ERΖ?7mOڷ|5 j6 ˁ/Zꏑo{Y}SܾzƟ\yƞh_Yq%~776XcտѲ` a {3?p{3?Ͱ7Ӳ>`YogV3wfololƆC}n:G~|ʾeY 7YD)pV7#17 S}nYJ|c&;,u0m;v"e?Ŭ sJ9oUK7AԱ;so6S)"b"Z'o~թmS9Ֆ?e& \^"y41HfC\L D[2Hӌ&F*gpo'BH#q"0 ogL 1 .By3@|&#?MtH8ZH͎C5ۓqwKkYg|?)zϱ]V_'@ ꏃwvv䞿H5.ٞX{wc)+CCW85ߖT/_n0@U 6朗?k_oh{M|M(>вIm]&J;4o-U3'TeVܐ{Zw;3QI5(}U`*EvϿjƖ=GPE񦲣qKL_qE+Z%eV]M#<&EzAt/ oޫO(#@-9ΰ4ח]+ XN-{7ANv\[Nw 1ס˾-LnWwFicpu ui[էu a$W㿉ˆ~_m|Fk nՆu{D#s"H ^\ i1%.dT)i'ʉL8C g Q&TFub֟Bl`z;}~7~ p!}RIǻ_B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!xL `0 `0 `0 2:*QTf2D%Q' T[d(}27sD{|命.u_<57&谠jtM厘ZWs;&7&L_FШy_,$mgeTA>ϺY̮jDVN>7orτre|wL nZ&[(%<ߴs P)1  |&>'rQق,Hɰ4AX*IJ߃L ;_"el;Z!7ަּrI` $geR ?4uʒªzS{YtMdq&"Y3a$R\&<9(kJhG3  dG+vO[6a}'"M*,.n"]Qp;ϝ$ˊ>|GyZ+S^ȝ?>wܹsfȎ˚/s#ܸq=Y~s[s1mTe%'?xE(\Yܗ'#&{2wC"f>6~qeYrvS.o<!Psɝ3 YXlȼ&>?.g蜙óg ɚ6 kZMx$cé#;tlJMoH$}}Z{лR޽oPCsk$}pt]ɹi918QX|KzC7J`cZFD,. 9crkÒȠܐ>KjmCWr?n.nB Ռ?6%'qd51s-ߘ1A7=%u/"Onͥ.Y"sf ɝ52sj_˱3άa7gݐ5蚬Aצ@Q<|5o'G'؂9 g<;"pZAV~s_=誌Ws7HWz\n< ERV,=~;#òXR('Ζ>7ʜA3w>hoQNܝ,r]qNta{Rt554{aD=3gn.1oG.S.2C( ۓ5Ȍx4kj?m=35(gR܈eI%!.6"'YLGKIUImilϱޝӆ E:?'=9({R`Nă卽=Mh?Ћzon5˃Lq+Zc`{Yc9>0s\qed1w7Z`ܐv]we`k ̗y=ۓ#Wx뱡J 7u}]SGtMqwzúd]5Yfj?)_ܨr us[.:^߭qvue|}9M }o]ؕv<лCbE:?v˞׊\ɥ[$2eEyсBn sɗ]i[uű\l+ 6m뛔=?eiq\~Xܩ)63 %u{ZyS/΢_}O`&Y]SKͷyf y.g^`YgolZb[?Ͱ7 {3?Ͱ7LKykB_ nF_ߋƠx1^oxvMbYg֟f8d֟fXߛn?殺7 ǟ7 {3?Ͱ7ތY >o%8=| :/(6JMz5EѦpʖ4(756O8pƣ\0Ny&+o0 0& c !B!Bq)%_^kWjx]olololتܢ AZۘƽCaQJϘ89&*r Y:-[b,CCvS{uǎE֡)l?6Rm"cFֿhzA_ݳURj}XG%wf}Q yꔽnG%'{E)Q=;ꅍXLR7gXӳ##m؝Xz/c76Xc76X&!B!B!B+X b FaH!B!B!B!B!B!(Hz%md'463p zXp ^4MͰ{#_;@`0 /~H!B!B\/@(Sѯ7:?O` ` ذQ E=c- Z;5E(ͬ ~ iOGk;-Sr ?77_7{g[Ul}y?`?GYMM5PPUUYQVNOq0_7؞JgZ JN#E>}SEm|S2V@yyIɂt?_w_>\sIz/Dd{EcҲeIrN~i6oo?6[l9^憙j\"1wЙ()+;yD^~Nۛ{m@еx7$_{GiEyٶoN#Q9NheF/KtcեxNnV۟9%!!K47_6]i/:uf=׶yF+AZDŽK_D:M/ɓ6oo孀' 3ҏبc0ML??|S‚삂,P(n ï͹O6^\v\tke}캹Ի zhSï~.o~cQs¯Ihʥ5\< s1_NS.x}-/eϕx ʝ?*|"r$v^ʞk^Envb,* ƥler ݯ<}>x ^LwixyoV3d3 F+BXIM|hS~fݑsO|Sg{rx}.SO-\Ty Wsy\ty1uҭ; .So?&%9:u+b^|܇-{ye^Acd&}y]e%/@b&SsC퉀7Sn6h;Wܾnd'Q -yJn˔\1TKQ۫,Z(<vF}EPHbEq_^7' n7!%enj(kW.'=ct{1c oRGTW?붎6.Bp<"Ί0%ZoҖ/Pn^_yG/)!r[*l#]<1ڇ"W݇z ;Og 5n_O=%/wܷmXxonPWnP2nRv7|޲"K?o(5M>yd{%~S r:?H[C}#=n"7mZb"$#seަKyG1k`9?Aߠo {C"OϘ8ul֐"rq=jx} @T)WKefV&h*& *"b*j.z̞Tde`}usϝ;wapa7{9~~;r_́2YJHN!*s#d@jzZD:[ )9-'K/|1ȿ B! A6C@B>',׊xok7]o!_n|GO}!! @C@BA~r$4OH / g! ! !K8H<2$I$CR W 4U5H::$"ЅwDɆ( J h ZɅA!BHR)A!7!JHRC} va7g+\h$p/u7{-\Sjԏz}cs=L4y)iGE=ߛ-&u[nބ[˷\S>=HҼ=Et}"- ݇,1!U# A bH|  bP><>\f sK nFUV>W)nV߼X>$)5xa'BZ/Җ䕕п-|T|q楣6&>L|qɇ Q7}(}kv>\!DJFV YLdZ.BzFkǏƸm|ÅY8J]B㐣l2՚YIeWV^ao+(ж3ڴl+3֓>9 ϗkuײ>6'`V#|7' DItRa_Na gߋDٺt neA"H?.:)I0^dط)ć~Dľm򊊀B;vHs r xmF}/ `Up*<^ jE Cfe%lIlH֊YC8&TFv|1-BItRQ?nQ$\v2*k|hx˗B1Z, C%/M_R0+7' %Mɇ-[6Y;&RPi;(}; ڔ,TcM%f MbϷ>ަ񡠤/.>K.?8Y(P"?y/'&Un^U>4mbh^| $q?J>'  +La,9\oPXZ 0+JJȒBg)%t))#`"<ɔ+|hx1(uۚt,q, ,aaf%BIAI+Qɢ"DvW6W?TZ k$}Jf"J^ܾe^TZ+|hx1HAeK [`}S\V $Ȁ-e[IE>(&{7{QH[PYí/o,6}4>p&j+(̲ʇ6^ƇTWćFo,k-(-/+,Vjs|0ʊւE%ڼ+|h;m,_D?55UUUE%eWF 6!>3҉Vjjk]:q]6m]q'qENdkU6>Y6RFv|&je' j|`"̪e*U~ikm,$n\ Đ A ĸcTۿ#m"$8GI| A $>Hߧк!A$!ACsHE2>8mݖ$>tS~#E;&g2E1iz=‘K*/.*XU؟ *T~v/(ʇ\Ow1=_TMG?:SxeJ<^xa8b((8x"Y_A  0qLJ! ?UAPB"9`{G.Us5dՎ[X$0 !7b>гvp;X9: vrADݶ;BS2]<'{~|سsyv<9 'n?%. ؑ!(+;y!eBJ?7LaVX[;b1?=Ģ-BzpGFF+ PqBbG8aְct/z;>a0uP_}{N3D Q @.cTai<O d)"pcz;zBP. } fFzC 4 \N(ls5Qd8%Du쑪c%ka9B__⻐DZfXO֭/-25_sx)^IC >@qPb>YOZ fs}B-,/Kz>ǥ@@ P|w7͞7oMMI'\Tv޸H9 JbXƾ|h&ޏ2Kg3jdY`J>[(!Bl4TzVyZZsr͵Ki)+cNד"C6R4P`9*,@ߴL(>HE&>H} A 0'mX%n4sк!ASZ?HhIѤդ[7=k&؅1K|  b3D$>H0EKӿL ZǤZjH|h[I⃔6>$>b‡>Zb"z>|L[al16"iɑ|إgdۘv9d!ءm|HDl`*-I5rPDR?&n@yor+uFx\*5TahۖpKIEv3ヘ9/rvQa7txSpJ.tz=C}H;~c o|m\ 7wNK3 ey12u Iz>a|a/fV/`8Ш'd!w.w:)^b~VsűMZml1a0"XT,lm d>lF`|G](^-ܾ]>`x 2!kB ?5d:,! wרXlD:ÁvNa|xV}aAzt ſ[RkC# $>4[HOb AlC~-ڲ-wK|h;A-5A/)A bH| - WK|C1$>HCCCD6ak2/d3p%:=+0!u4aHtay 9gٶc |*ݏP0i(Rr/ ^}@ʇ A7DBBrf0R41,0p1CM8BFj]w9N#SHdR>Rlxe IXi|țBan@l ՋxT-tWű8l{Vu0z^k #L%Ci hP 0&5KQ,QC~HvęYd}gOO4oVHmSzrӻ﷧};ӡOyァ߁!>zpyybo!{⒴pa ̘PUPž]},gjPh:`F Ƈ\EQ^|xq=GĈ#㻏'wz5ð{}|9]]&}Nw#u>2 W.|`N`ߢC[OZן&˼Cdޓd}'e 9![Pc) v(#]&=~0/YD3E9U~B3C;Crb Ԁ\( y}0GE٫vOyC>uM 왧CB}g^b <4Wf4'E3k}A(3Ӎ#=ӛv;VՆ|=ȼ'f<]3+{phˡ3LW6Q`/O).~˛!]Og aӰN nu[H[=_ԾG/^T|.O z<iz++溗bn2ץzIz+vyS䗼ʺ/Ps''굞BZ{{ZSX+s{8֎3})>c |0RuZן۶$n->fT U4 UVHχIF>T#?ktEɃ>+ѕz>"'D5e{ULUnZNl/͋)=*a:",*5cLjz =Tş13->o &{dOpǣ3^2|4|wj\Ջsφjr{^x<}p=9B/lD82XݲA|7&~0w-`֧pkC‹^4R{( ,^1fOԩ\7aTGԖߋﳿ9*s4-áי]ά!9_ƨ/|+C+֕&!qA_]ziZ(i^IYFH>:e蹰 %PKcy20GWkNš_(, dJ}~JAg% ={{+Tw` \2_P9~jUp>*),>QpKN^(ƿ_p1%C0χ } W^j۶^yyed:X 1p(+Y8*HE͒<yZl^T%,gXd"茁_. ƭ~n6[@\Ĭ1o(&T\ԛ^!U])*wy~R_ϛb>hC/3kk?TЭq6,zIHf? Զu^|8 <u.x{́RSOtvr.@W?s}JE( 12gZ,4.Y5hI.>LxKX"xKW`~2DbZ]wX^eʃ&/SO]T5>R)ZK<3Yk ţn㲺J"}`d?E|!#+~r!AV\l$|W7AQPJs|_(X-sp4Ӯڡz֖Wn;a٩u2b(6$b E0Çkcˆ̔OR\R=u 帅F2dpvײ+^32 ]D=ȇAXX<~kNXZgsC?s"ĽG3Ř3X1ÈpEujNROT 4C"01Y= =562> & npJjm]]53]b` 1p!EHW8f0SL6C C^wM*ӄl/M1RqY} LO(aȦ H`ʇ ,>d};|PM# kWg$ q2g(v?AcFe+{/ q)-cWk4 55p#q ?$}2|pZ--|H(VoY9g釺QBWUĈپ%NQߊCMxf9X|Nߌ2\@.,$Hry1IXvyH%DmX\)7Vb>8 R{hn^Sw.ܷ5oqC5q*xr ׳_ ƊI>cDGNf!~+]tkV3}=vg2ruGrZKR 惃]8;2.i;m|X)gGXQL%c] cɆCf. \;EJQ8{h |`a'9o]˼"7 yh@YաSW'-SE*G-P Do |> _!w> p!^(?jӼjB xuv6f(ϐOM(6< 1>C_wyi#xJ}ćz@I Ň̕t`ߗ.Bߐ4M@ 7@v9oDՇb+Up0s;ƈ>}f`,ZӡzG+ XH0b {eϡޜƭvBjG^1G6`-`[})|Jp_i!=5~Id0#U)ƮԪ !QwgPQJNH.7wr'.n={!#3 AC!]!@t<y'IHoS>!@<<"Ų7;BŐ~SC>%΢b ,Py~{IJ1؜|oWoK^M먮:=UU|3Wу#Dĺ+SyȄL2-ثl- Րi*2CC33 sj{A<3vj:?OO~XE?hVW V5:S.*lU .tpPB@cyE&\u d~.|Y궤!kܖǫP2ci-nVeQL,a @}Q%FJ)2:U"^FB\Z',njg@R隧 09-AbK ,)J ݖmEfWq+RCDCGjéz+;wvkݳ6u߾v꫞o]%R=V]%SZj$6SpO@$2,,})8$n6UVdBxt{E^I`2ҏ%Kz,*}H%8LH;zrڡ>I"AyZ: @'UЄLi̦)653L81\*j*E([1uV{) K~^bwexj6_՗wuY/sPJ 9%mel\gײEu%V[pf68tnK 3>Ag{?( 1`W9MHy;TⰔ3YfBJMcb;UH-[ʷeDOlTC'J1;:^o,s} 1m)}-g+t+{*P3c_ggv&2o/g֤_O<\b%2l9Wh{?Egow :VEŃ F ҫUsǩB/ KNauLi79;TF_nX1*>p[Io>0K?3 p +%JJ~C3yfS'׬g.1Ge0{.4dXWaҧP۪G avbgva'1W̨Kz?TUL.&S?X_µi_[κNs `.Ȼ# d?-CrsR5 0OMz1y?P- * CWyx?`ZY$m{ 36t o/ԏ)$XGf|GS}Җo;:ۂJp[AW"aT!VpoTi^0.Oؐû>Y݊=,bI*7a:hL/TiHgrz)cf K`ݬN6AAa!b|`:=5p{N8ZI2W~Ҥ4XP?I d.&(荙M z)X P'۽:o\[_sqkO{>mOUm9:" FQh jZ?Yd "יJ0jcc <|J Ғ5nz}GYg<;ܒ#t<"M\KY(y>>wΉd>SgX -ܨ0}3z9PY kM`bk$s:W2hgjVP`RqT-XRnIRSEVnT.̏nٟAǢj؟bYM][wRQ9Vc%.02אiM6-6we+{n uۜ!?dEGO3z7T5_3X_qX]cӜٟ@F?swN!|%0~ n 1!Әgٶ2Al8j,U:Ls~hNy!s42$;wJ߆!߹N/XC˲-?IwnjxHbQ.C%l{c#} 8x=_n6ϼmsPV&-eW͈| ~uv'dV坋\j!ta5n 3k%|=i0цD}̮_ύ siaZ YdC"&{޺5"8{?s`R ZsB2Y@oufqUmZ .gbCC2+7o' ew]`[&^hbOVߘd~XA὞:;bg b Kd;>`bv6X?ׄL#hYm7$rYchy`q67{ QN|쉘cu#U#h4O;[SW-&MZ_-%e/_ۅd[%;x6i&V4h'>6Mn_s!Ϳ2$;Rߖ!_pg%mw%;Iwn$߹IsM[1$S#[`M;D?ۈ:Lbj܏L9Tnȶ?`Go.hѾov#T-!neM/:±Q,o$iAZY/`5_ h)0s8'5l1_QZ(&f V _L`-)rUjE7qPCRbyFi|8nH)٤_t0EPTT+nXkaPr<)( "$QIRt{-MK"< srsVP(c_ܳ@aaٗE'Мm/L 7q%v(PPS(AE "20dc_p)Wk*0]RnMxPQrȰ \$ ktZ";`6h[Ԫ38g:kd&Lщgz# :Fe\\9M =TZJ>[m+LAL^豁ǢjDy:&< !@&@0[R)d0o(EmI W.n J ]Jɬ"2{^C$,nj-h/CDC1[2[:Si*2EI 1LȤ.d4JF^@ @} ]}|f"P>,\?Ek~D BxAYMtG5kTKCyٳ_~?{P@>c05y dӇwB~j¼ 3դkՇYv甁1ҜOV]{KޗtncyusԫgWWLU- R,,Sh+x}?FM}Pg4}4|pϗy;?ٴF2Ś,P=*iV\2^16u̩F0癳ևA C>QSI7n}GRsoOU-\X0F`4`+d^HgK]> y,p/XX@9AՒ9Vl^e/%˦OP,̞;"{| ԁ(/ =ӠG>/%?Їv41m}&,#>2#g 2Y^ʳ?1ob`}kQ[^*\:{_Lʌ{2+,>?ܕx}lGʨ>5ajMx(( |"kY=eAO%}jS}^L5GVmЊ^t>&Z9Q.X8,e?Z䁏g3yt}a&Bj>:SPsQ}P-V/ t2rkTiJ{ :VߡOm`U7&R>o 6T*= U&O룚3P_3v(/?&^Ű;IW|@-'Bׇ<eNO9;'{Ov`EˊʙUӟQO ΂q^>)ϻAL?sB7?l o$I}BdOM1AE@=C^~ЃP}SFcQ%EnMv88q#{ƍ7=.0')eXd{S^3;.s9݇8>>j2}m+KſӿO;@{~{סg=8Г.{ {t&\hOcrӹO/yIS=/"#>> 72KW963}FlOa| z?f++f+``*?m!Wg>.~q6>0WIIO7#_K8}}M4v$sK<*##6O:yTGҧ4Fԧmf>-I>-J?!A}m鞒>xևNפ\c} o>ʼ˲gO_*b eGΗ-?}1% e2WIT1 zk5jΦV1)էTy\eL49Z\HehnbV 5&WA.>_t}> 0%u0)Uï*q>`f} PrS p>\TbpRPTr }TP̑ !(.:[FC2'[C!! 3:פPyl2\>l9Hqrت}*>4C}ҩ@hrrgAUTɑ .Ucqe-S}o",'e`(&/kH>fS_̞$}$}Z>NpJv_V=eWӧY(c>d?anJi& 82XoVvwjG*Li&U 4nC~A=_|Ә=u?CA'lh}.7YrDOTBw32.6w'#vH%l'|5x8 g7Adbf6YM'.a&[meVb=fY}M\ѶZv/K|3J? RCa &d}(AfHfFSt Br!hqʮ'}5^/q;"y# 蟑pJ1Bkq6~=zoJiH?#"#>UZsxmOLheˢ[R-/sxm?PV\*+-/˿YVp6AtE7 so8k?\Hߖ!_[y0.h?^5P(ᶣ^7L}Fzf~_ i^I/(> ,.\i`-yMwezu$mZԻ:ro}ksmOE:/@Q^_yGvMۏpn d",Ꭳ}i=6Ahaay[2a5Ӑp]z|~ԣ?tamHqڣ#yv|%Y焋,=yM>ȸUCzKL.0qՖ<hꍿz>qsYОxƗ] hc;BG \ǯ:Bۺq\|eh?rdӺd.><;.hg?a=IqCRܨ7lMyc kkKF{.'?j =jx} @TxJs4M$Pq@E\MP1u\Z3/MYjfee A6AQؔUY;ܹsgAƙ=s}sWT|b&È'BEePl2h^7,oK"Zj#$qL KON$/#SAҙxSog={w;2/8ﵿ%Fs睄t!?$}}|Y N.o>=gпq2Yߨ܊FPx>ߟgt{A߰=&x}~{n zv 7 3b@HG~: {j!wC z?hwhОPJ^Ї}@B>}:$)gBC@B a >O( CGC@BAC/@CaCLNC'C#SSӠӡ33Y(lйPyЗ1ЅEXХePzȡ++@Ѽ*57&t@߂ }.=~1r|WFYt;?ϡ_@~/B?7o;AwA   g!/_ACC@BA = ? I)ih,4=M@Sityh4zz ͆*t UB*j:h>Z-C@BK2h9Z VCk:((d7iF1~`FL_n.Kpė[Nw5ޝ\hK=vT!I2.GUlQ?t&_O-|)jنj_lc%&&ll5{\dg}n}+nR>$M2|T:tB{%DB[@?qz cBPB(!A"I)t%1>>\/.))+Ryjn"*-O^ lvJ*V@Q0׫J]\t|HUs۫#zɇ,?u>DQ/G^(D,p__^^\y&9/ޚy?|u<)%0@\0(_P"'B$b7x/œ-pΧzYv9N|: q'" qQ]RL>.(Tp(T;V`0r p`%[\>\:XbxRsJ8_Ɠ%%X⃋B|hCQi)X@lN(٘Z10${KJM(!'K6U,ځ|(.-JJac )*I-2>is  P%Ett=Y^j~M?,K8{KKHlQdcjƠ-*M ؒ7K)y[||R^0V86 %MiIТ-[6aćf xYu W> R&IGCP(q%mHC3v(J** `yy Yr5cr0t JK%>4S@Z04,,(@~r~sv+*YIP P'%tiBp2ޖ ||4i*+RG# |d@ɖ-AtIҟ_n<Sҽ-9򡼲K`vBCp\|:yڲ kׯWf|@%r3>w to Pa>TWWWb>Dnm3RGtoK$p`~UԥS[WGQ[[ ([zv3K%W"G@~X+l|D| QvhI#χo>^KG\;zÐ r`b\e; ~40~]|8%I"yZ|@ "Nf)cYO6S@yı [$IaH||OWXk 0E)DʅzLo`u*uzVAw&h?~|qqa寑B1]rܥ-$H|v I3Ŗo/5C_L2>܄mH$>nXғ?<$'1쯽T*3Z&&PM{N~A>d sx3od%>8S*6ڤk◡&pms(ݠ^Ckp Eէw;C?LNXvsΉ ت(>^ f_0DJ UoAzR|Kg2O'@ z*0M8nz\K6'NqI&-WG*TB2򹺋g)2OQVMa8nO$> 2C}~%hpcEM ^L9FI]pH|0b58,cKgH88uv⧊+?J Ç Dk⃄Đ A xҾ*u]B b4~>Xj|p,wZ: mxR$>HA1D3D$>H0EK&ih-!yI#|OC>>) ᒿć"N5mVƾa9!nę|A7Af_30,d16hM #| }Y..;[@\6Wjsl{B԰4OÅ,ژij! _~}YN:zkߝC8:NK Fqژ%jfl8#s1c_X@oIsx}iA' 8_Ui|hT$X6IIbZQh{wK|h; hM|p SH| A$!C mxH|  b|H(pKdIAFxD_Y}a%0X4\;~V?rt ӥ2"(CHn(i,m Pt:X닲Ip<. 3J9"c4/?=C~($BQ0 Qt)d(_`D5#3C]4+"NFsmw>TL#eSHe6<& Fli8+aɇ)t:[ TC_PGBOKxՌ-`ӽJ?{[J_di‡\*dAU-iOGRoe{e']%ta ;̘P/ J8z>Wm-ߢFc3J0>N8̽J5*ćF&cG$;"iv˳N?G{`wH %z  >&wme|$>0[>.=1OE(~& 51мǩ:n T1HA8Gz4/nL=C}B7;wKy^((|g7cPgD–_"PcB#@DVWf4& +b6E{!(CEP2%N7}j7cQk? w koRγӕyD=0StMtw zy<. ㇿ({f,=6 ΐ.~^xWtcMVr.ʏWm۫qz]VVsy\Yz6o\#{Ӟ*|J>GjX][*cMasͻ*jlWS\~}}xFSk:Ĺ// ?{LUɉ6є61p>vOj*v}샭\G}^+գ2VfWE3SmU,J 5I1fhoSNvFp?") *!F#k<=GxdPq[wyGbGN9#C@['NL;p64jh(K > ^i.(z`Gzf |H~ڗ֜FӼ:kkv.#rme>gPzA6͙VtWcקR`-֔&%N/ZR>|#?I`W6m[<އ/J^y>: ڼ<0|<vOp9wfyχk|^gem71$6cG,[Z+߆xNs @.[0)wKI/k&/Մ+B;\cxuuqn#;|Ṡ&! &z4j Vǁ`Ŝ^+@$ROBz%7]Vj(뻌A~5s”c+AeK_"YRIS޳t#t;+g3`EpyfbR0\˲}ډ|7]{B !8UXL;u6bfB:$F=J4x,B{P!BT=iz~nd$=Bv| ّJ\|hbŒd@ο0GmW8>|Q_q\$A/^NX 寊=[%5˶lS 'șr-T@xhn!:S9UJ;sv2t d`pX^S>BN1iw|@ۍ|C7P˹2@.g jC`FGP^qv8Zg^_#9si9WUub!f1]cg6XzSad*jv:-~˵b5 p*L?^!*z?0> &29%u6${G0L043qCnȽ_wY bTWf03Ll 3C C>?5]*D|w]s/P}63ЅCoʡL 7#@湃;S?x*<^aV;՛ m.\n3PBG5ȹy%MY_sO~l9X~OFE.L$s IR,9$/<\,I\L:<1TSل#1\E taW|yrYbWpDX=vjUЋyEaDdEq]Y~`|H>rk.շ\\퓵u_|Åշ^x'c;c9Ѭ%dɡq @aCV 1~]۫*+l*\^ QDAnZN=p2az\*żSsI$|8 H8dO#yq$͡(ϖ"i5 Wm ">pWW|}ug+>(} dȟdZZ;uv2MhzRĀpEϑIW L[!71(, @E1%4eT/ j.|cygw6b %\>!Fd=51)9J[/:iY$% V p%#̸QE g6! 1d/y5Ż{a!K ʑF1Jy0TzɖpP"ElOg AY' 9(p`"%R#IS`^Y/o_4{d {]+<6μP#``Et!_h4+nE~8{eW7>V>@Xk:RwzP|~Fl!.p3g7i&(ϲP `]ӑ=/]6^4M@ @oL?lUp2f%,¯- |f`,Zӱvws􂤭1B |y(7qHGUL8;!ܲqvܭO(- Ki,]4>$ 2XGXoXYV%2B<9Ʀ{]AEI\ 2J' m 6пC  z7nCv> } 0h_ǠCAA@IF.P5ȏԑt^k^,ҎDB ^Co )zdsy=ǂqZqUM먭V*W,yl~1+26A6. S sȴ<a$RGi aWةu3{/o=Az~[7QLV`p=f|E%P n4`JМp撉d_ĀXxVx.FD:ezMW(d{i=VCQ?6L6I~Q`tA%pɢx(BxTjP8։*1еT@):f>%Bl0E-^<5Ys5܈Tcp1IT`((t[_Iz\eEo[u^uW.eޫ.d0^y'% g[ d9D\D$qVI QYdb|tEKf'g%gJp(Ȥ%M;[2L*[ы18%;Џ*Ӆ;m̵)䴕G-CT/>{VTf>]VGŻ||9&V~!'M {"%.`j{ϱ7^Mx[G ϨL/X}M]|WvQ?pq빠!dsZs4$ʀ \|&WW7M˸"7mU VtY3iBM.̶r]`}e aT 47Ld39+-$Ո1s50nVi: yw; yP11]_Sbf}iqA@O.žU*x{T5O=_b dig`r2O.&Z).,<^\Gd%-}9>צﳙF'69{m3`stBLJ=OZ'Q?)8M =x/n{)WsʍwbLdqtNYT{/)Y8 ^Y"]@C1UQXx vlNB?egU܁PGaQ-%Xq2ɱ !ܝ6JE{dz ٘G Əq48p<g9?  L%y>d1Ч\s X0R0Hυ@U B}ъi9ퟺ9'R.C!_[0$>~%ۃ׷j}hY7 Ek_Jv\pp\Mtä%_[>!gϼfѧv0gaЍgStBd6͗?%r:qr,dzBC]VϢ=f9$f5| -G݇7k%iۙmƁ ]/jd)_/7, |2p? R 掅d^eywGޘ%&_BrBqo3A!=K<IG1r )3^n(WI3ӻ:L'ow8 H&lgAV;vI1ؐ1ipz7uN>i o%ϿZHw4LZZiYZZ[%$Koh?@ E J4]ls pn&T4%7OZH %eHo'\~y]("ߵ"ߵ"ߵ"ߵb{+aك£7Ō7}]妿r3Qs/sL[*t7}u冠Qo 4~oޭ߳# leNpB"cx_95-?ײްU n7q#7}F\`\OYx3}k49v;I@} %o8Gt%xΪmwoZQ&ˍ agII}or1(*4 \.zҋĞǟVް37g Vd-sK1_7oE"ߵ"]pHwHw,> (-~1[$KooR)?z9딑~.-:-_+ Donegcj P )C/4owVi\yi(PRB:898u[ K2B*M4jx c7lD Dy2G- %v;Ӥe͹tJu J?f8_ozE'C宁=w';{4#.;S=Cgzǟͪw/~T?2?2@]]-I!?lD- fܺc`?_MU?=ٜmyvI+]S4E݃=pn&vd,Po?^[r^,M?sEcI@РORiaƖ-ϱ(Rs bC8J܎"iK+*'$hy~v.7BLOӥMvο?y.s[ML~m}s@q{Q-L"~ <.<,ktwLB=4,ah SQEsIad1U =KdQdV)N/,_UcF!EIx>h$0gsxbvlvL@i5E601"TB :O 34 mkrfҼw^j̀Q{*\MꜬFicBTf?bPެar w+Bdx iSW2罕 2SK$wQ{'8{Z+_m6|",н>Onv,H튩eձSH*ޞ&Khs d=ף~dg)ܴe@Z4Lҭ]5S+^2A3.d'J؉& @ZXr>~ݥctվY>UdzqjXՂ14Q)g VL~*y\]t< XhȷbcB{4K&nZ]yv-Y6UxjAHܑy+ByٯeO2Gy&=,f=p[ g̨YaL䧄 gObaOScUHhHayf+:t=M%tFgC3Ͳ_^y/T y0^iczx7osRP)0ug@0- 4+eRcUBW<;GLɻ7%F4@X˓4K'k_^bMxö́!ݔ㺥Ԟ! jOtjOYa*6s^xEp<,, 3&+JzhCՄt;gx5_GtxTZ6c{)#UՋƫY&zv҅ޭ9oTnjԞ-k'.A*@#ϝLޜUG V/;D;Ǎ&St!{`L;쩵̰Z^'Sûá *DTGk F ?!_K]ᷜkԞQW=iV=uM`$gڼ4.,gryy""UET6n Jcwoo\{бv#~X mf b`K\Dv~~9͛+/ոUU kq '=_#ʨIWj.at3:vobPQGNPҨFHqڈ{Ӟ>cޒtse>`nOn='Go:|;ow5:w뽷e?{3?t 30&CY{r`̨=V{<DӜ` $L0gڃ'pFl` 3D%M5"N9SBONɇ{W$,4_RkLtN?*jqyX` žsjv3 4'36=ߟ3򃧫=#0pڞNJ~>Ŷ <c2sɽq3o!Ҩ=eC|i5غJ?slU؎sdO8qwC{r/>ӤY7\p? m^2&cߺ^(ӯžR2T~(MZO;Qp{neO6צ=N'ǭ(چ=.{l7[٣ɻV >Yne*[ٓٓce]i *Bd?m07W^>9NW\N͹ 'úfponNrŘazq wo΋ԩ}uN7/'&H?qRVV(Vyzn"+-a??1~[Zw/]+-OZ[%$DB 7F(4L—"بsmتf엶W67kZTCjzѧ$kW\J߱/5Ox8|ske%j7'8vc{;* ĞEss{\?@`42F:(jpWtD;z]ЗrQ2bg dF" )oHy2o+yi cN)1Gx}`TE$rzH"6"1B &@% @! x?#HQ9pG idS6!uylɲ6)3|{g>of^Ie], "mHfҖ iX4V ӋC-N0,¿HB"Ҽ):/ܞ}Lgopάb'ӷ" y!Kg{lqB,ptz9RG'hcoK`m=e2Y74܂!!)?A{K]p|/ة8V~~`ް2ح`$QtԉPMкr'.ݰ{`{ ;a#Ga`={{ Ys~a/:!E4 {P  F^ =  MMMMMMMM̀ff^͆Q%΅͇̓ "``Ö [[[{FUհ7`oނ 3/ށ wػ`>}#Xl=cF&'ͰOa[` s/aa_ ?} -;N``?¢a?v~ ?Cð#a`avvv vv; a DX,vK]]T0:΀e²`jX6LtX.,OXYgvΖ" V]]Uª`հX-&57i@ c8}q"mm SH/[jMKХ?xv݁cR=ќh~Q.F%~GBA>( (O^_r7ٽd_\&O5A}NzQofEv&͹P|eJZTw >1 :_y": zBSB.OzP CAЃGp1'?-PS\_^QXqZFDEUUA9J p+!1[ofBe`ANYYAV^^Clus7,燯%7wK=6i計喕:3W2S;xEt% ?>5-\ť 48ٹy\ y(BVw1#+ ((nmrrZ ڜ Y(WzAQ+/oW\ ]aafN 5ʥGF=igKJƒ!Qy'1QQ~~yʼnX..&$"vs rR3mF*:JDa~(]TbRFX`o1*(?JCAI _\D7481Pq?C8@^QFmEj[]'*q+RdV,78bl(J2s@=q KJ|QčJLTbI??$wc"N.'QNu^,-=4b%,ҬVcHej0=%`}Rz$%fwi>i_qzou"@Qv~ֆV^B)Pk}Rǘ'{j7^ކ顨 (,-u>N*߿4IH0i& ͦV^j!'I#Q`0>NJ2YmƒS̏,*Jޤ|\F ~! ~p CʛBotoS2;1@VVRh,ڷf1HÚ~daz(((r">9̟rH'=(e]aJcC ./ APl5b~eFҊ $E%!lL菅,Go͹E9E\z2Y5yZ[ކݏ*((j[TaSML)N/opڵW Ksmazl)VUURVSPdCtSS[KQSS"PU]]RVrY/ۀä́gյP[Ww!bN~+/{7޴j!c;0oEڴ$M4V^^KGp6-+.= ,-(P^d8xP@,ϗV^G в! CApy~SK=XwK=lz@U=p#xpzכTM=z! CAЃABB&z! CAЃw|}St3e j"$Em; ; )gK+3 (V UڙQ]Yav>0kWR鳐FmOU]X==iZUcP5!| 'ƪv,MSړF`7LJ>Jb-kF+Y][ݞ4- Ni6$j5hcL3 i$ #T|cy'M SQ'aQVvx4#ic{Ҵ0\'ؙCpXnwiy=(d X{bO| WF2JYŞ4- 73쎵zhz! AA@AB<x^$,GW.+o{q;<UV_ ʃrߏwW.CF)/>)OA |?Xo\t8!DC " Ex!(7/ VߡFQoY`O`XQ)U=k7U\ƾ@n ;^9Zm4k|Bk RFDacumwb5>J<8[ B<x=z:H$#x=z! )z:P"ߓ sb6`(%%ٱH;ؖGq;M{`2[U=8~ٱ#N;<+,!b<Z@PQX ڮfW`Y|lCe.{/P^mx=-DV5JU7l(z0;V j%Sz+U¶2-}b /*N|H揲:ߗcFmH}38KP}Tu΂Sz1sq?JЃB<x=z! CAЃB<x=z! CAS=z MǓf-_t , .O5Å>g z0@AЃB<x(zhv'l{[hzpfZvyJN/=y@VOf(;SKXyA/Cّ*O>{o cT쭎 :]y'~$n҃k&pz@_jS kUK U$JZzzR$qH8z: =UneGeyV˩j홈R4=!,COI!5 @Oc# Y54(8]YaonzseNHo\Ju HP$5Mk,GFFbO솊tR_4UBqI=О"07_rJj˿^_zz^Yݚv@j+Hh l*NI:b`(^*ғ?AV꡾!T:zTE_ )DbA|ڭ1#%W|BFE $$1P-=}@zSUֿXb%20K܎zN i )i[QqzMڔS2%Q:@ٙ__FY֎,xU'-+ mU IhCh3 x1Pݫ4P]oZ'NVzPz`ͥSNRy1g~;ti4fכ7I&BµC5j/p8H>IT1@ lD G!\M8VX:ǚ5q$TM5>T˾XR{\NQU$I`8 m@v'u@PN%!pBREu ޴L8Jۇvϻu)ȆJ\\=svUS =в T=4x= ϳ ==@Azp㽔ICәss5G8]-iAq?JB<x=̮7bI3u`|wazB"AA>=q=0XgʜVgcs}X.9Ѓ]zخC͆d6ۘC9$Mb>=\?UVů٦L]f=6 kk4@:>%/!q}YkPlcfzh݇.l?Ϊ2OɴJGm*ul yJWc^Bޭ6f%ĕCE;.$dSj-z0[Lc=`_YŇA1 CAЃNInB2x=z! CC]D_H/ag 1K,]wO}C>d3VE )#Cj*d c r$#oJ#% qqau  /P~ Ezx>䎔J:dOȝH򦒂PR2iP6߈* sHlPbԸ/`TSɵI|2)H*&)PBʂ l`R8( RI8 P1Sʰ 5 dTD]'PZ@jJQ1ū䓹>SG-yًa28=z@s<~vKd-n@LNıdMܡnٽI\7d_G42`& |u1{Йn=><,݇uz!wIRRɷ L䛞M}:zPk UX[CbpboB[,Norߥ.?5!iLP|ƫ+$جOnPg?yp}?_ )<5mPf͙6նsfJAz C㼦- XE\3K޹|{üfX¶{T+TWI|{1%n̟ rüϵsP)M[31ǦKz1F5{TK~JYgdtu>Ӳ1A{8ݝA?еz |:]W?ׇ!;OjFk a #޵}H"3Ʈ/ޡ󃖪iȜJd^%h[dov.+g{]rV_9;@NQ$ϫz~Ҿz6C?'=^I-Bլ;$돮z^SuMAyz8QPHn0Ճ*GzKώƜS;6hf2 0ިP/,O}xDeɅ䟍yV!+\ie{=RsUxom˝=cmZehsϓ8V5PwHqƌTMvɮ(Q I{nb0|Z!}i<;K%MĭoX4VQb{D`HsbqT r0T.s7[i,I#> VaNQ 婬l{r2܂O,׬nHSZ޽y`-O{9>,Xȶgv8؎|J}+bY9PP"Bg1pzf<A ifsJ Σeb-OP=01>I:AB ,\WHs1X(lƥ+CUYdT{3nWЌ<<Wӆ}7LTVszӞFeyIwYZvןXf!qAJ,ű)DYJq>K`}Vw΅` 뱗_=ff"MpQhBqy]qڻ]5]Gdu~}~B|`~5ÉMK`YvCܖ'<ٝ+Օ&]#R} ^5{d=med=c`d@JoêUee=Č&5?I',RMpuk~nlyeM+@Q*`2^fŹ(2й+zk5!izp= ̾gT=C;ruà}MHsڧ}t(7՘r)wɶb /ӷ8{Mrtߕ=ksq|soc,UQygSy!\2`oډ|]FxUg* UwRfB̈́%IKkDdeOP?qC6v7T;2OeAqL| _R%ix6Vـi"Vh-NZ.#{tQ ,<Fe>#K1@ڇ7o3!u=CD 1 \|ED;ɨ1 AzmQ4͚vq #!^zH 1s?Ǭ#COsԡwJv\F.B3#0 գ~3=d`8(16B9=u<RӶLpe9rF p`ﳋ6CE&g[+W~3NznQxW`=8z8ݗzHK3_͂4 ҕ&3nwY<[Xt{;$spRA.U=`.r! azaJ33'd<9&{_pqQ=|וި "ig/p"M8pM{ܾ=&~TZ,w+罱!o{G\E E,@CggJ7Z{lHfaXr: 'qB͹^"HBBabaXK0r.Is%Аf8y=LԾ/ ޔXj%f:.74if1_J֋YQ<3^kT|E`>0= ^%NVʛ/N]}k]zŕ\|;e mRYMRU^E@霷u)lg1Tj}ޒsb[df ce SwRz񝞦7;@_/=iIx:͢ȝa3C7&pw8ayQ_nQeFr澭fN_L;~&("{|5>!Cz[ EXCZ=zȟG(` ;E$4禦W-}?7t.dcˀffw4L@=UI}fl\|JaںLz2,bF&zP4Pf(% yS]+1x/݆y7T͍셡Yz`2P gl3TyTd`yG @h7k]!H"u  ymT~UGEn7G=fF|@3k2&WYU*`#{_t"!i֛@$AOpE XHUM)6X/XÆ?PFİP~ LE:,h"!Y & saPIW:כtGyTJ8;xPnya7nv+6a v'.ݰ{`{ ;!XðG`=az={$)Xoi3ga{6 {  FFFaa/Â`1qxD$dT4t X(l&ll\<|ث0X8ll!,l1l l), :l%ll5 ؛`o 쯰5w`ka.=`[ l3Sg¶¶KvW`_þ} =.؏hOݰ=a{a`a`~;0(w1qabaqsxX,K...RaaW`i0,~.Qc{PBCWuKvyڟe I7҆0KglD͐7 pKkAEm׿A}'?vǎRn'}@FxȰ#Rdt29Lװ_9dL֐:0Հ@K1I,(3+k*]Vȇz>/%p1lp t sAq:TAw)P E`#"AdE9πhQfQj,mDe*lE]\ӇlXP,!ӌ@#?K@ЭˌS,`'H#dD*E\( #y4Mhy蘧qNc !`bxh5pѮFCԮƸLCqgwgoS [3 >[A:+M9!'-qA'ڄdJ ʫ|aw ȯ>k[ {j$cST/( b iLޣ](etLȹl[r{݇[M</eG7*M4\e)#ci`V[gh<Ь$}^vZ>qߗȫʀ d$sІH{5ҍ'OK.kORRJmN(@Nw_DbzmM*cW9cF50Gjhg7:Ꮎ{NI <ªN]i%2;Id,HFd6MV\'zb$cOst>Ҿ'ۺwϣ4qLyWh ٜgc3ǀ&c8IAJO)=žQ1E9-M+Zl]y9m_vX1kD(AfbrJzͥa]>\GKD2vc,Fӧ^Ky!1 =X[QyJ|SK %-S)F<&'bѐM6mU[TvT=sTO"A(zt. (.Kj-]΂{z֐g#Y#.`gA۰JZX"|6._~Ut"ɵ%Xq16e(JO(~B(푍7W1&l6·z[}Dq)5Ҳ=.$e9g\̄ۈ 7;Yvrc%bUJz_2o7¿­Eqĥ4^!}aG=f X]_` a~밓pK5*wlrk5&Hw`'V__ؙ$ Iz;@⒎H47`'<򅝵xY2ֿȻ3 MHm35 wo KOƀ_ߚ!fvw;wb1Bma [3_?k_ߚ!fk_ߚ!f/4cd.LP`KwFZPK XbFfYX?4o>M&@ M~辖*}5pW(ȇ2U-n]5׏xRRV̩uW;Pe|2<5}`֌Y?7{Z͔ǵzݏ9ZY(?X;}Vg?zw|ӻ>ۣ{ӓy;Wm r F7nD#i Oe鏣ㄆG#i'?ŸO?ה/?R#4?/?v>\>kJ#q?M=1 ǚ'未TJ屄^LN?廎fW8Sw1.1:]9{ԅ)䚣 5:p^n̚sWjdJ p?qPMK5pDR5|lW`.KIV~T 0rge fp^ÁS)84Ñj`j%d/;3e8J)P*;mg}ݐ?{NTЦ ??Tlo{xCD-a'ъ%jѧv(w?^M2K}fWLU?p~::t$ <{@eOVFwGϿ * vWRn2 ; zŁ˳ ?r#cqA7 z׼cϨ㟶=+tWG |˓%c/[<+W/~wG9Mc:P^ХsʳW6kMk_ߚ!o7/nG7"_= {bc)qoޚ7p'Y @=={4Yy% ,2#w>'M1{kL_Yqm῭^}Eړƍ[4l} ϿQ<6bw{Ҹ3dSֈ'MSjSbOm;=i4Kl}c 4XyNm_/ o1 [3 5C/o [3 5C5F .7"A?Mq'|RV _IJ;~* |ɸrBU^;3q6}c2 es[6+pWy:3!on?rҴ(MXes CgSZT=tʕs]嵓8Yjkk)TTWWU\i*?U4T\+/+)*A T*C_vڵWK sm:EE\m ]P^g{8)+/H#S6oLa G N>hyyqqQN6&7P^m9s}qM=w"G7W,ygPQ俁奅" ۔$qky(--,,m&lv&? mNC!$rwYI;)JJ 5Zu X^8"Hw!-%#_8 I'D{7. [lMT^+2IR2WQ_ee(Ǔݱt@]p{(7R?G#ܐ?N#4?.?O2)ɟ?OC?ŸFr ?ŸFI }d@+4`&[_n4Q|?.>!\ؘ/Q3 (C&2O_N̷]4YΟǑ9i:c3A)X9{~*z}*i<¢}ʄN\ |ʣFI'ba:QF"(SY9)$SMχC4OC'=OYnL14,Ias^"(ЉG|nZ'c: QUe Ob{ş&241|?Mdş&2O8iߛSGsC<ٟ|chB?η{_хcdI;`dyÿgXy]=CW"Ђhh!(> [ʒW_|2lr'_Yngӽ'˯{t:ȢxNOզ.J|{Ow'7vR'ҧ7h[:IO>DRs:H@DiA~>>k;OЛ۱m"l)^}SQe'iڮ#F  ]! > 쮔Q ǥ뷷5B:~!yrڶD8Qr{Ob;CꧪJ*#_ yzвl1vya.E(|̨OB=7[p̉Eْo>j[A6P4 oH?M',11 o!L톼hiNK'|-+/-q/i: nY q;Vėkl1+퇱~ Tko|҉iݱ+h8xv_oό[X4{ G=4c$}^+@\a?A=ݷZrmdy# Fud_i*Y_ޡĠa7,חCh7D! v@OWΜ7galck(7x} @TUB+KGf/!SKQ T1!bfص~=6Llz_`  ey2 0ϹΝ;0039=ܹgd@LW>yZBL&+Mܺۼ722 )ۮ V׺3Ǵ9Ys|~w? o~ &wޝ_~ i8;=o~ߏItq$ g]3_//?̧cN?!Vql} B ~5o0 hf!:@&h~f -[CCXFGo ~n={ᔧx(>Xq ''20 "TD§g3,x,|6ax|!||1| |)< (|*q%||51x*| < _N Mռ4| |+G+W_ < &|;||'-w߃ ? ?&lhs??+K!W#_o ?';?OOgYB9yx\ x)\W5p-\x9^ WÍx-^o7/x+.t~7i@s1"hsz ׎GsDv.hI>ٹy3粎>m<ӝOi"Uv*Gzy}]0P/:HgAu^V_WѳQ8Yza988fosû蝰wY^\{ѳ!\rr=pp=pXqtЃ/$"'=иɍ끛ܸɍ끛ӿ'Bxx>˲{Ғ"mc:{!I$d&׃g3zأ"dM5HF֞T8Kە/f?BwuevS^8%OYNrA qt,Qe'SֹM-zhwNR9Ұ2HFsl tu<yTW7q=p7q=p[GWzC98q=x}S}h\ zyQ$KgzB3/^I* Y:m eaYDf߼TEi`a;\n~VX- 0ds-졫H_a[Ň1g &pWh?{gq=\yLkmN]a7]4t|ҍ1=~+3.77=z}Mn\|{>͍|Wz08ϣ8b _Cػ ϼ̿Fs꿒^==ۙ{Ip=tq=p79\Oz777uk=pt.cA=px\rp=p!Lyqv"t(98uQNC"#Lbdf  tUuoCɛ7Xd}WRBA4a(cPƑXR'8̀w{=RcR8U@ͻSEy`z?+Zlë#t.̧0'He2YJ) u-hRlКBVQ$ų| ]D., !4ΧF]"7 3I,ޭEQ5P9,_@2R0&=RfR U"5otVKژ@*Vz(>2hJRlѶӆ"*⻥dk0] O`Woίw~|ݘB>>~c>׆zC}F uCdfd݅]=@ L좳QB[F;t x%dR|f%r~[+`zhPy7O>942kČ3O6#{:-iy?5+ȿ<1!O 9: z`Āykt;zX v+bzLEh]yx@{1hk uԬs9_b$eH2dyţKF$ǩn:Bw23="glXJ%A[^%P 2-W;R1EޠұR3t 8@R!@[̈me"0CjݘԾ"mm"Z΍Q-~`aiUR%/Q.ܵP;"I7x8uH]`|q4WC0:{ t-/,Jk% :t\J2ދ L]gL[\آ358Vl%vh2mࠋ+0;)o\e仯{78iÛVJLO:1JaLy`+L:vKh(Dk\շHHфhVh&,׆&F/_P6^r:pi0=©~ˢ(O$.^_;\" ˢ6b(ݸ ɁtbdtSr-bکfr]iUSb$˩iHdmXd8\ͷ&dƁ-䨋*"vY--b1)v^/fZ 얓QMoV4n~ 0sZdքN\%eR:#5AO viT1gL=L j6ro&$6%(wQD=ŵL5ڏOdTfJ*f=rB(-nw(ެ/~>\|h+  b5=BFD?؀]sۡ%ߙX~4jGJŊ:(a[RJ=;MV#5CR fP];઻ !GFz( zP'CAEn<n>^:-nUTXŒ}B"i؂œ]dQYA,)+=k E==Ó6w\dփT&+G nσ7@vQ$%TS ;.|T5{f:m:m|Ea)Zao:Wwsn`v`jCWO?fzaВ!dsV)_dLQzx;nw- pYƺ$Q_L UBSb'*̻WTVMZD ,r%zhLS=ٖY7*0cbn9dtMB tQ9I뉁6<$% й%1>^MXk42Q;tvl͠X͠Y z\e.y|za:UtnP/L t0/ n? ިz8ڷG#O!Ao)+966 Md$ָ%+ׁ"LYwmg慄lƁ/zQZe$b1o l_/P$:|Ow!&+z4~6in9T \$mq[cՃcCg ?~zddAz&3>(O.t ~i0ʩAJ0>J%!S{wLo̦]exˎg.{mު}҆%f-2HHټumZ äť 2t7o-؀!C3{E nW {X5dVm#.wx?=PŪsf5ukYʳ%,wTaU[$qq%w+G2/:~Or&R='Sk m**z@}z㎠d$ex7W`<:s`W-dx$!5YjKU.,;DJ{N=sE=pé>H?̙B׽.sP-c`J  rEuGpCԶ-׿b8q"j!+T$&,[X2&ٶz`m(]uz9@!*@_$-I@ƞ:RfW wWas;+ ĊgK~ uGUSUQ=+g฽8M=zKrՐ`Mx^zR@a49۟VUlVU0HyL ȜuYGUι>|D=z1d]R,IBQ7A Dg~T߫|uCKCҧtK-xR7o6.]3s]Dhr11 Pԁ*V 6,G](?v'^5$?O\2je*RL\T 7P2*V9xZ؄윂V2=M@qgq*01Ճ,Fe2a(_{NXSm o_yK򕕧b)s b`J03ԧt+_}--: 7}kNr __"$0d:gem}uql-clCLVYPy줌p6Cs*]Ʈd?|aBq5,]7{PŞU $QfYV4f5mO(8MwZ#D</mL(eM)lt|hI%1s&4)vQ=C xPG^;LEəB?h40(;_vjtbLGzIxx)naSY[̏ £O(M% @Kߊ$!6;|qksށ[2?Bm5 A"7ϛ<~<_  j5¯_~|<>~#&A[Ço>~'.{Cc'?OOOG#3Qhx |<>0<O}P7Z(~K ]AXGBy7|CeeS3 }U#+3TٶjM<#d}; 97+ 3NSDSb,ѲWeed<%tc @G@.H3뻡fMM!BvFbe0pc ?7 KM0h;3-@s09:HJHY@VU0-5ELƫ6Xbm|X 6wA?yt- -:}wu)F'AB%х"dM j,WU<ɕ"{ H</s l"Ħ@5bW_\FVjOT{sK= L݅SNO2xZaY%Q.x=LWӻ- D1Mt8vϋ?_dJx¥[ZYB~M_?9t@ RWdh%kutqӔ,diHNgB ;<}rB?ď߻jV Aῶjw[8]Ϩ˙&ih5Qަ>xƿbGˁG+ī`qy t5(*Z%G8zN`"f?v Jy]:yDnX`إyH,Toݧ]ܵe̜eG2{Vo}Yiz2Ę/dtXBsȄ2]%~e!պcUuEMrAUKٵI`TB#}o` IW(jI6m ZF΢RÅ#s34gEi˼7*{UbyDڴ;I88@O$5+bQ]h_ ޫH1FiyTPꎱj}gO52ϳ]Z)f |{ev%}*zq[i$]Eg<Ͷց%+OMjxr6 >pb sHX!?@5*Oi X')pO+AJq9DmG1Q.P jG"Mw "WY,0* Tg'0-ȦLbXϟ̲[BVԾqWeSn/MӁ|9&FMjLm*8zB3Hгz ĻvE!VZhHs`#3{+8*?)P,/_F |Z;(j[}ZZNƨ2F}ma}6ڂ|@-aL'*Vϓr W^ż&-.eCշ 3=%rѶLd1 SBEY0M?FK;e pD_]תX>n S,mۥguJQ|YgkS5bBWgAwwUu~V[ŧ왣ny*nWL7y†☛)6qG+&֑,w{Ft HNV ^\i_ ?{ H6R NlN d׎Ð,Q7Kk,+V7&$N}3i(S,liCBW""ز̀w{+9Y V B/;`{ ~C߱gV*ϕ 8<?7]\bp93>sKzqk>o(0Rqk7__52OriB$u7&Ehfk)nLSL1os__p0U೽YHbWڠ9!]ٳl66~QʿyCbzDp+3V;/"xI^bߓܑTߓ=o7οw]{|v%M{8 |{ק%(ޞ{ri߻c3c^M K}ܷ"K3ދvEp+s/ t4@Ԯ"oeޘ7Sy7 KG;_t>n&'YwhVǂ[_WlY.]36j[\矗._W\2! Pz/5οwg%M{8 ?77οG]{85_jΤȭs%x%[xߝI[JsK/͞{vO9"7gZ;*LvfF:s9ߎ9"7gZ;y-$QjYm"@atTOj:?goɈdRFgWNxN g9w yE:靟׿K| ֻϠ1ZaC-l`WH)%UؠZ>Lɱ'l6׎-HOZ B"!3).+?Zݛ`Dw]+@ג"Q~im `Ih^%64 H1%ד5ua vŔJ+RĆF 0DB $-Xf[VXGd"Q-'bH(S~$( 3NS` yYt6N8tvWSO4o)n@%!T^w6msq'O'k'jRc+TR=Z:(]4(wEeiV|b>BF0&Ŀ GM_XQf;*{e/~-uO-=Dy8Ꙫ)njQ572cr3.DB[I$ģ.ӷ{߫m n({quϦ $t2hQR=Q`|N 1ȡ$1(S1T}z /ۚfwKO,ЮY^^M[0yJw@ăx #] VLl)99۞*߾4=1}ht}|9\F{~,)W-> _TS5jU_'_uqʗ>^}./JL}uO}[u&ϼY/i[uUEFmC}EcCՅ ]憮D5Mj.4*{NEs{38 rc5kI QF.ǀ}t"Ň捎 乥ctͪ;~K~V0Hy;*Qg]JKzKi=?H3z<)Y&z'qYkqe|[/Irm匴yxvB;"-/KD`}Oa$gG$S "vgO| Bj2.pA=\{ O?$=wjen~>.y16䏒e#N=uXGJ#x|ﳿ/H,y]v%G7{DX,}X}in~x#!ete⦱p6ol?VYՏLaۖn`݉M@zEWmO˸L~;wӎJK;PU2F 'µs}^G"緬C,85%_=UgeGlے:2Y˴LQ_;GZ;_cLpl7D[lU湍){]97b=NoHx}`TE$1?uNGeV Ӊ"dKJS)X Rϴ~O1ґ<6?QrN;G]jױihK<=<񿗬7[)J=ȋd^p 4ҹ!)?A{Jlq|oXV~zrezηnHH؟`nHMA5i'=uO}#N]aaAKWX7ðG`=`={$ i3ga}`}aڶ}1O`ka`66>m} OVؿ`_¾} o[wa?v~  v+7ﰃa`aG`Ž 8$4 ,, ;`$yX2",vv Kсw,Sajray|&l)` U5X% V a;=iy[i{JCOؾ8C[A[/!=<|֤ ]oǞmi ?/1.͉]r=6- jQ;?u _KB]Cu$$:6yrrԉ:s;7[(1u-^[-7?z\Ӷ [+( ?zyn HBA@ЃB<ŜBuqi~yEaբkEWUU(%׮yyCBo~}̈́Hтn^^{CLw_Ay'1k'`9~=((K̶/TTV#P JbĨW~ǖ7e}Tj JJb ]Ad,b#Ʊ0*lU./ P{ꂈ=k)EfbXycP=l/Ɔ$c8Ci) ENؠĬM(YO/IݐˁGTSF9KmM*yX|?4)%RYOeec!XX߿4Q]ZDKGlذΟ[P_pE׼PJ<{;Z$c1&&hs[?=§&*1B)I&npx#PZCʋB-{JpoT݅{#X̧$4o`1 ,Fԧ&t9Cqy9XrEEIOh/$ou @.ҲVPƣPۘt ,"-bY Ұ&ߥ,孟J**J$ί籼l$||zDb(>cjY[Xαz (~h(8=s(q4?KVzI1+6c!%_qAAQNQq&׊R^6 B3Oc[QeeE-8s3ꡩ顴"孟^VRP~jAqivN=Oy맇ʖkUU%e9EV>W'/#Z꒲ +}!w+o=ڇ5ނBF\sݼ{zU^sȮKrФ&Dm|-QuB_ۣY٠O{4^=@Qꉔ4'HRJA9 v2?_ܭAeCAЃE{= zpű zE=p!xpzכTM#=z! CAЃABBFz! CAЃ믇}}1ӆe j"$Ye= ۾ ɧך6=]1<|Cdv);͓)&lyhT=lӂpF&FᳪKl,lFc%Yİm7Yzz1| *QVerMR)`AGVnxh{WbQu{Ȯ&f6a5N.Ȉ_4R# +Ɔ5g:q5 ?J7N+);IZ~ fq3Pu*ďWIJΙI(:֫!|W6Xt9XZo*̯5;_5*vBh%D8_mH=cT@D BvCUvK_o1h>U2(?(KkS,ԲNy:W.1a*ۏ|w:vCpN=@ | 5:T|:zVs1TTk+~~!^g]*Sf&0>mm>/*!阂?ޙ=ОB"0l=MWɽ`K~;fR<bޱE (jvI.|4˃ }8&{31`ꚪڢJ_o=(Sz;  بO@>Wt\FaE $$1P[z@c聵un5ŘY ʨdiJabb1&BlRlXG& <]Aʁ}8&DmIk9́ӿ6JaOXAZQ<>N'[ڤ}6Ur[OⷳLzՙ_xzh"p!hMzBm9D0T'$ߟl^s=4FД'g_LrY '*36Ǔ-@.hSIHm(0TQ|7-MZnw5Z t[@S)c;+St6w -[@KՃ@c@AЃN{7Ws вAz! CAЃNz/4S@'\| DЃ|pϴ9=,0_獱q>l6-j6d L !lC!Z$4lIe TtD"*֕aK`K!q$[E:VC@p5ۘx_DFӯzuѐHjFm*ulF'ŢYn, }VV .уy[m\J2+o-I\ԝRl׃ɂy-$~^턺k=`f|ưb# _HkhH3W: R?nJ`z3M"ԩ g)&Lm":N@z`}D47Q*K$lUa2*zo8?ZhY=IB]zlǐiH(M']Is =>V8M^]f=L(zP8ЃA7 BJzB"AA>خO,Cփ@=R z! CAf@cb]B<x=z!._H/ag 1K,zϝWOyM>moVE #CJCZ $A26I2GZޔGRK( `,Q$~ EzP>䎔J:dOȝH򦒂PR2b(o@a9rE(q2j rtܩ$R>N$ՉX( !e_GQ0)EQLad[T̔2C^ T18;TS)*x|6'5ysK^bj NAO)_5R[%#BSqtY'wKvoR(Msc rsG^?=k{G?[{F>i#ʫ{{C?7`GLBN遝t&Ji:$H|ӥ}eZ6VE]voz0Cm3{.1]tИvz!qwIBbI LIy8r߀PXk UX[Cbp`X-LbPIwOTHz!]fu(Uխ~փ<'ϑ7/D;#{:Kvvyr_n=[5Uta7P =0,7k:ݢUą0/:>'r7K֣1|**Yk3d\cew>1y±0osoT yfLA).'kQD~:c#zKnJYgdtUӲ:As8Aڿжx|*ɣN쫌ۋ!A:Tú~m?7ë}"3Ʈ/mE[T\ndF!%PjNNS啳K5]:eGFI/\o8uTqD%uk[t'XΎkB6r8ꖣ|.C[qx`^$q +O¬_#oN쁳R}hR̓UϞ|xӃƗc=@!Tt'6-=;^sJOp2(cjڠECeX/@xB|,]wۈPo ɷ yV!+͖\ie}{ 2mǗi}pkH5wfi;hR+C|l1|Uo!h*xכ쒓]Q: aF2=|BhB uQ 䩬Lr2' m-]9YY Y|5yIỶm<{a|c]G eLr`;~>;(oҰʁ8:3F/ZT 樟Qbeti7,oyꁉ! Tefᆒ@ӟ->,Ոv \2ToyYuEB)hkYqhO% T6SY$!wRzS%>Sd=hz0]w|Iĩz=(y|ʦgY 9jSմL+-hR+L:al |,^ky!y6rxQ|FAsrp{!PdsW#1eոWC^@ uơ[uٝ;<4St1$?C 7y=ءǎʛH 4hf? uo;.WS=k:@SJ́rc/rl-Y:} >kӦT YsC+,잳x+g/Ž^}qOjs ow5h'rv V^,3VYI h&-LX=K<6Am սU]Fgtz}d>Bvtz.,}ؑD\(z*o ;ltL~>|) B$ %=NH~k/8e+dbKoFѱ}^|MVv7T;d|3 zPO& kJW5ݩ&VlaV`9$nKk$yA \5.&!%u:MQ UZ܅`D1 23_ ce SgRzqퟢ7;@_/=Ix:΢ȝa3C;&puz`P㼵n^>΍xbșbR3}f2%ꠈUh"z=84Ƚ9*Pes=֡yD & CQPIBnnJzsC_ӆ,Vcl0W,fP <5>96{`}CR\\}=tWgF&zP4P&(% yS]@i֘jcv<P7 i=3C !X= d)x'CyW]VklCK3h<2M:8eȑ"}F+ ~TI2@hy2t LUG]J{ql2"ق)YgxB~̉`e3<*xs<ԣd} _j4n ENx $2 XټVUx >j^2HgWyPY5)@$ʪRUҥ I z+b`Hu(6|` +4BêC >0jWUd]c $0΅!MeסWޜ,v=ʣjgQ呍`RXSsG3P?Rz:dJtH$`"/b9=M"0\w/B xj"P?B"8?xP`7a`a>avϰakvnXg={au={+aXw#Ga={ i3ga}`}a ^aC`Ca`Q@h؋ X0l l,l,6666 6666 66 { 666662, [[-[ [[ -- [ [{ : ؛` 6aރ!#ǰ>} [ 9l3 ?a[`[a} 5lؿaoal'l, 3l7l^>~د`; ;/SӰ3X,,K%’`aɰ KeX*, p]?JG訣=(Nɡvefn"#l=a3Fڐb ͡AG#]/jK:]]jjk.?ǑLŨ,>zJ;2ǨX ;O1"YFPN&gj13Lגj2UK11(p*&iyF{yeԶ[o{c]t6Mu%p2p Ӵ sAq_.S]kh{e=d^>+kQj,Z2~H"[-ӇlXpQyLN5,- :@/3`N )epUԂK P"42 c:W`9A6bC ϲȿ9GZQ92 N?PBoiեW^YyRn[KE$Պdi2yMM&kT?ڂOs$",<${1ϑ1wQM*㿭6 g,9sEԔq_<wR\1X7&y߹ {bq :ISMlf7,M5[kr:]t9 TD{ "6 NcGۜmW͆i_~qK5 |%_5h(V ;nfR^˄s>;<*=7u.y^7.6,1؈V.czP=7./;k)^?ȫʀ dI i^{Sɪ oa [(4r$] w]s[4,;C+Tl+$Y)o9` %3uGCyn:9*T1ѓ&zT".,"o_'Pi۰<.:\?QI)-;|Ƴ\9mfZ}~i ChjWhxo;9%3:VWR*Z/3EKOʂ4*oDAfd`uʅ˺g.zM4$`yTdNy#GY |Wj|MgH/ |&cntDɟ)9yN-6Lbh QGCf6«[-jb"{樀E;@(ztN (Z-Kj^:֐g%Y#.`gAJZX"ux66pw~Ut"ɵ9Xq2n6e(JO(~B( 7W1&V S7Ixٲ)|"#-,R3pph+s ERxT]yp2>_8:N%L=V9&:RiM 4?R* 4p2+Jk|¿L8K~1Fsd!HB Ӵ3PLMZ_7jtቖw FG/9(V0FGx h¿y#>;#SDޠoZX#¿(\!gLr,xݯWZumO,k7oL kߵ9ߒoa ;CZ4g vK+&I#F]xt2|vTHM;n3uo_km|+_ ;;"}j)]0~/WtÂ<{G=^:̈́m I*#DV1۵ʾHÁ:_e|a'7V.%Ӯv%Sމ92QCTĒL*7ݤ?}?5]<9mH]_Ql En%I5V狯5Ԅ^UIO"d mt!e|agחEL~syޜBk ;ӖĴ!2Jo{L[gMID31Pee87m/ )ʒ__՗^FmNKΌvA/?/ۨzo<~k/c&ݷ#|w=o)hq?+-#]#- ?;C/wg F x]0 OB/wg ߝ!3w_ ˿3216sI01Cj. fPߑjiA%-`UIfQ`i|7ڊ71օ^w(ڭҵZPH*<&< ~e `F@$zߌOZ/R_&cu[ൄ$Kk_aUs*y)-'3JdwJ VS/bBrF 0.œ)/Q9M j21"D%bNx@2v p)Cjmg3ת}hiރϐ3SJ. /xP;w*=%åBՋCUsf֌A@@(L 2رO&}7bG0&m¿ GI>^^ڢ x'e9Fu͹ճ4+ghVN,^2>;"H5xVi!c>wdOUNCm '9Em*E֭pI; yc\Ҿ6Sb&rR1QY/O7O?id$DN'_ Ӿ>[tIETAk0̩?7ǎg;Q sq\3zԋ[~5zY2Ipj~`Y3|3˜+ulяϳq?am` +`F9}$ge g2?gy5-葄Qԟgɱ K^уNӇD˧A9ًƪG~Yg`3Hk∮;)(Bd?ƠtQ&Q/ , xBv*f҄TcPޫ /ct]v>ӁLi4 !p^ъ[H+ԋk^ 閽 ЄuH7.٣;gv9ih]OOP5/ {Ag濠3@3p~iԌ5]G=~ WCg S9(^3uȽ͚jpgz&gF%D=ڊQ+s60jܛtL< NmT~9ٳgՄN3!_66?aOϰBO*Ÿ9vCqR y^p,}\)eM5c`ϩɞ[=1̈́q@ƿM@蠟]|yT+cL y*mi!JDFH]SSNwk֠i}==IPFRT_vc%U~Omwz1]bu-fC<7Kܐ{ܝBޖ͉tǑ;g҂_o=C㻞mwncv?=??q˞}za";ܗ3w}?RI=bg%:;*fbK a~3b' \X\&h3G_l0BL34&Ю<&2X@=]Wb ,w[X/g?~164X!_.!t?-4V|%iv8mbwP ?Mڟ{?GtiB{ _ ?ŸFger/U.sgqyM ??t'&4XS jΥ՞L<5:p_O?Ud9WXYJv]5?IϹ~1zB:u \sR ǓcI5Gk~?[uDHgR\R3p&9ܕZ<9\s,X<O,5?;#2jsiVgyiH~Eeſ6&q1IluJK 4lk׷-O>H[2ſÐH}H4A~ Ce|FjzΣV]H"vKɈC:uF^ă V$ ?[bzB~n].!IX6'l%Q=<:[^Br)\,P\@$%~kUxN'#cI1=(PTδK EѯXP"};XH!b){JqArr3S_)=_D-ܜLU%+;v/2g&=+kyٹ* "ER/]N:/pUym?oDdFjRڕ+Xh)qΟ{ UZȿ*=f_X$kH=GԾk>V^ȿ6;o=]ѿrV6s0?X]QZEѵwk%׮_qUy_ TA1*V _Q@~"~%kB]Ciu%37m:䳭u\WVֈ?I\7r38(A#?OLp1wG#? OGH+zG#4?xU#4?Mj&:NQ'=4r'Mԟ&wDTqAuF|?=_qs@z7r?Md5rc?yO JSS}IS&tb NS%5|M?{3y69FڈGH'db~>i;|,rc4ɤ1&1#EoNGGN<­s8xOO­*S$k~(4@̏DPi"'(4OޜԮO?Ÿ 8dnŏ }C/ߎ'LEq!a&݈DaywLփnr" B u\Mlp3lKK^~%򉰥JsxiRI O,/^{~WjVri%ؒnmwu;b)Һf$%-'-Tvu_yrMZ}[۱m"lIK웥=:MCusa4πRom-ge;[q ʋC 堉5ړ2w@OtݽR=a4O -cy\vyP:͋3Qsk2n?bȗi=[ǘ4h ^Uc~<:>j[A6 esjߏYA}ױ?M,{/+>[S>/)@4{'̗ŬEXyi}q)Z}!+t΢o}Ƭb8V_L[YA3YB|Hcrq+6ꁆO$M}I׊틷VυQ0Z>Ұ=5o#n'X޷oDՑ~ZyvE z~߱CnXh7 [=E^^8sޜ!N*=G)oJx} `Ev% xAb"D p 罂V@A E\LNrB9 L;tWWWUW%)쯶ߒCL@҆4&mqW+R`M:F\t"x|h5 FK$ϒ0m lN$3wq'nB:Q{pG_q!8:=,wߏIﱷ9~ҲyћAog.8~H+~?zN6Fv@R i Zui@PͨB:ePMкr3ح`  v7;^}a=a={(1'`}aOž :!i4 {P {66{66 6{I9DhX$l,lll,ll19e尗al^ {*57`o;l-۰{}a l-ll=Sga6%+fװl } =l+؏m`a?vv~ ;l߰x~AC` ;; ;;;K%N’a)TX,v ; ;; ˄eat ˃Tia"X1䜭D|Va F?Lj(pbD~mm ڄRHO/PjMKХwՁc9R=ќh~I.F%@BM6H>( N)]^6밒+l26#XsY՜fEv&͹ݛQ|eZZT7ӽU:Fw:X|;0" "~-Y@BBJ=(! z8SXUR.,)x҅-?Jp%e>^^GZw_[5{x=~8梏vk IHL۵{꘰DUUxyñyPבƧg妞N; u}v\>^^Gp|.bUQ !G=z.--6OE.z 4e9ӹ(WN/oP\^%/,l ʶ)A}ٯ DMߋw fujꐐT,o//'$&qms 3 lJʫ,*-΋ AV2bĨP!][́Qz(J˝>imI Y(ps6P Ώٹ:u'+/2\ʛT–a{16*OvqnyJ5N`9uYZ:4rZR.'ȁSFU\r._mK*yXI(4)W-8CYUXr ֤U -L1;*+瓘5iBHuքV'Wily5/go"\kx22$=-s6OJgᓴOBC*x |=Beix8%KJlyE],U#|"UwٮI(IL#X È$-̈́.gyz(KNΪBzIz-xz:$[PK =4)(TzsC֧3Dle%% ׳ 2B[,iXBU<=TTH9!Z Oaygu5H4t굡$Q~SղBІ]^*P{j=|Jiqzy zpCeM Pl~{I12jօb!1{{^rAAQM =\Iy,qJZ[ݏ(/PkSV^ʚ yzxRʂz6OuuuނKuuUՅlwA>  ^+}!_+o3ڇUgotDi퉱+'gO/CcT[ǶHAcdOEoH Y'J`7O<ޭstڱ[{ΛԵIepJ! VFyH#i@98o:\{b,8oV28Eu}N9fb$ ׏1v'\Pߟ\3#=us1;'J}kɖ/`=1y+x({xD&GD=xzPB1ЃЃzPz }t%2q>ۊ7G3 D~rwo# -ğnA|?Do\it8!+DC EDhVp.~.[|yG!q'{_o;v=Mwu˯{_{_aCGe r@y<ś^gz$^=dH] \c*_\4=~I`RtbVV&3م4O@O$FnIxg8t;SRv=h;"k{:UJ9%$lN@doSOl끯29:U,2m@oy@yi9Xkʡ|ɤ}[IU"A1oۣo y ˤFe47'C͎r0i؂l=(;   4 Q CA> ªubLlXG& \\W"?»kxTVM|Gicլ8e5+m7 ą!2˝Ub= &B,x&Clu˂t ˊELjb=Ъxk xeGߏ4qa@~Y XBEcDp] ЃCG5CPv"\ ޫ]V{/͛?ʶ,$|pflKdS:/Ӳg/Ɛ`¾m o͘?ʶP6Ɠ|5zPҳ\Vnbugy M ĵ=A܏ADPAAezA^旙ve+>lKm%Xg[B7CX\||eZ=1&z@`E|#zeŒXh[2_t-zT<-ol|\\!6^f-_C,_PჍ zΆrDma`;$D0 B"(Ѓ`2| xJz2ݘbi|ٖ/Wwq2ל\-҃Np3 Kh*uqa^:]n,=z?0 b!U7}ٖnA=p c}ٖכ}Zϲ_`}rIƛ!lz"_zXѡ zPl׃AAen=Ќ`ZɽhXn}[_zp)lvWϽX>ϕ^SMiЃ]bo݉9Ug?xGHB&=ABT= 6f-$?د/W׎4I5+lI} ѫ^6'_+R\J1Pc7ǓxJV[S =Jn=4ުЃBJ(ʾh ==AA@ G{)IՃ'.OZ5B4ߠ%ЃBJ=(! Bz/R\F'[|AAezADPA6al1v"]zLg2wlc&z ylC!xN>Y..+O&˒bљ;˒"l_VS@y56ۘqx]M6,;Yqt8 nуy[m\<ĕCXl|3d7H>T8n hg}1m l(6@1柌:,8ss`2\.+aƶD6S63gcMKadfO<o6%Gi_N= xrrXs=I5=`@,`Ӑ8;ǟqZc)zٽ`pI}(&t )OQ4O&Qb)Նju@ gڙ~Qhp-\_q'KHDR9Ԍ#S`*T=.$e/Z9l)P:TLˣ5媯n0_F6hZ\ H`5*>bUP1 6CBQ~A,)߸bMr#‰84X48-{,ʣr\zm|ۡ׷l{xmz}/|=$1^$q U uȆ@}h$gjz؊!}CI|DӔIΰbzb@Dfd= DW{Xk^γO{9ˇ:}(Z$gR:{((6G5b~AĂ^U~bǠ4\qHOg\CdwOYATHefᲒ@c.:>Rv@\2oyZE6B!bS@ {VjZ/PQ|h>YJ]b,Z U $A׃KMZ<$UM"g 9$n_@Igw{W 6tn_:"P:; h)|:/'C#[Ȯ!SXt"e8YZܽW=kg#3 bC?@X?8W.;gLUc^R]|IU/{#F(^ux^!q:lpCazH缤m~maE+ RJP})D@.rMej}UHsw"9Pm1݁eSMR _ni\ઈe)Hؖ=k z mb,QwRKܫ3JR+\<<a`o8ډ$|C #* UwBq5h-R).K<8Nuh]Uw\z{t?z/﷐'ÎᲪ8}|A ʕ2g*KQI%}+;F[J#c[ˏ[!qr_}\e{)nɦYyx,02bʒdNj 0õaLɛ\3efbt#}#~>ywu>дR=@  zJ QgC g5A;h&/TӭؤNӬz$` HY>T`m_ףTRcg9Ru}-a,UJ͌W52E(_'x*Π(g_8O;&pw z`*_rK~TP8 -0mf ̈́1U}^vzȲ9k ¬pgPS䃢W[V=3=i'z}dzNJ(zg6tMtWLJ&zxmFnaekL16@vv} ˆgYz`2P)|?CuLkVlC Ey d_tp|ȑ)}F+ )!TI2@hy2t Lu 4Ŷk\B{ql2"ق)YgxK!B2 u~9XW1>/{5(n Eax $dxjN[hժ[ّP׍t0`]oHgP} ,j@rU%ߕҕ I zs1c}(U~(6l,ll1l , 2ll%ث`ހ U`o]{a>}cj'uOa`6> ؗ`a_--;`?¶~m   +7}aa`a`v'(8,; KRait)X4 ,,vvˆr:lh*Sr`9]BS3 W֑P{S^t#mHtf3yyӠ_@~B5R_\TzrCɝ@!(fȈ&s|2U~Z2QM&kd=.gSnqcǕ[Nqۺ>P{Oo ŰOKshAvMWA(P 6hh|DgfdN MLuub#l K,;N&fF~jօЭs U.`'Hdh&* x&Tbw@Z#tci a8/lE9 ҈Tm|R`(,ttY5/F_Vzg}%W3;vg_>Cg]~, w1nA O yGɼ$&,8fqr",$ AT$:1tsMcX%[hs\/֮!O'Y{OM97 8Mc|zWbw&f|Tq-Ok-GId: #]͌sG2nFvtqrq$u;@y8 d2(?}ewo6g\5fHg5]VqK7tS,uʥs7 W89P?Rߡ6#R ^7X xdU{Zx䘛0n9gg6D ;࿘ބ4D*PV6Ke">]z-_5zhm.2sZޗHFvZʳ1Igk9(ˢOfw&Of-,Ty3$ƿ\+[!\\ڝ6GC(ҸK1lX::߬8#eR#9vcch~Qdb5yN.Ft+WȰ_eGV};jQ">,el"-2P75i/=3 4,9zib~)=[3Ɂ03f}4@:N/$#4}Pg{%{:T7jГ&Qz"N/"o [ǀ(i07y]v^8LO)-[Dz\9Y}ne6}Ch¸o j  9|(Co*P>GuRHhKIUMO)hnh;SvNafjbVi̢61:=AEi;h|ie|ΟNCdQX U"Mz!C#e_Ky괿>2zѶv7:e<w_ <1Fsd!XB q&&J˵?:;ˍL ւ3ڜysV{;_?c ( h¿yFYq=bژu6v;Qh?~VS( e\tyN퉞aK-] ߉A wo7F /_ }Ս? vnP<ē s z(woh߿xG7 O[=z3oJ ~ҒN _lW*WH:]_lm|_鿭#VeNHӂҟ5?gY&_ɍEdn4oa w-ɰ`/k_Ql& .X]oaz밓yjEw/vͿgߨ]`'__ؙ$ Qz;?]TDTEllk<߸ǟ3PVZ"J7θ&XlJ(7gkL wo ȿ7񧇀xWhpf7Z+^Kתot>=,:~ߍ:zk׿ڟK%և_V?1ihhh]to O߉A{ Fh1bPCG>|>s W1jIBk =8mϪVeƼ_{nWiS݃sSοv1{G2g1SOz&n0&_r䣤-+z-J?裥mWghVLӬY:AxlALjsQOgGI|aT?w%l!׽'sr1.||祛+^Ņo/mYpIt)  G3o=z y>(MQ OMxxRG%(zQhk35/OU/^8`HaCi F=7_ǒD<,@{t&OvϦܝNT: Wh[ 捀'ꅣ bFܘ5ci"nUw3~WϠÙԟ>Ga=ojjNX " }F=kfFܬ)Hnj5ԟ` k'.LV{Y ϬgoDgTs.zNzf͌ >dpi מ~m?2hsȽ .9#Ʃ BR0+`fzƓ>= '܃{҆]t'~ϟ3d!mϬFB܈*;oSSOjj߂)}ԓԌs* hB?^?Pбt{ِ8?ꑬG=;{lF=?UaU!lGww+52r.J?ؓ 51[CM|OҐOiޚLAӂKǯ:`O;LA3{Ceiz/=_U9ZH5;[-*¦3M91 \ÉGN7Ψ4~ns`d5<7'f4$5$P'Q@ A51fm o'9\S^~==^\&0 6 1%hġNz>L\p̕Q*;jg}]?;ЦF̾})?xh9Hkjωۏ8\~MA>fٝBdöێ|i|?hҁd&3@6dz~v\|jڽ'iDzB,ʁ3?SB 9wiYV^T3j\$ U_ǝ tLG#?G#q?W4RyVV[z%}d+[՞pemެ?[\RxT]mjyA_eʔr~Q 3|O;5tk3GK|U |<خ5>wϿa[Ğ;2\92{FIIzh^c8Iэ針j#ߑ9{)X#p4ضxIVvr=Ö[*+q_SAkbU{Ck~5㝼G]rax/}i׿2f}; -¿)HO}Aシ׵SשvIoJx/56h 'B3)0<28K\kK~ W\?.lW p_ }V hҕ7;M {Ҹ:M.1|cVឞ=i<V^ fviyV#x/,$-4W`izeh[=1oiO7o~ƳJ|8vj dڔFIFM|k6͔y;`O5n-kOObc=QV֟(z d'6gOrk=Ͽͺo1P>F ϩK^^ߗ!2__ }/}e{&_f"4?C{h;C/Y[ZuZ~։sG3t2QPGQ.]嵓8Sr7ǭ"b_d]嵓Dy\)eiPWk'Od6yUh?q*)6Eb|zʙGdpL[ζ3K EbW,4,!YI$4tky,++h lv ms!f#,EC@4%j*,/Gl$ص쐒/*+K5kҌ"]ӭNCEEIiIFu&*B ܚ4)Csϝ1$x]"&uuhT[Q.lwelG*8g'"um0 Y*s65|Z/VfSwۇrUvIΗCǝI iIؾ{WX6WWv-Hk?ߪ‹5*)+ɪ,WT\)TsD]]MAqK.wWy_ }DTAQ*V / a-N?ۿ~RJ۬Z2lIWF2[밖lx7 -r)43?}8Z_KC8u~?-O?A! vc; ?? OGB.ZzG#?i;|?f1d'oc͑"7B'{Q(VK8y?`A_=tj*%Y؞!Cx|?! p]Ly1KE/׻1& \*1841iަaP uNCQ˩44a,1_>kS>/~ ]DB/]Yy/e54T}ՑtowE`Z&oߏ$8]޴Oobjwr!={"Q/]ERbgr1וꁆg)G=&޺~K:POWg;p- X=~t&ɟX޽{ͫi0#^i{d}F~~߱}_nX>vüݰ7SZ2!E!ok'x `Sǟc:o[9M:xN7׼BN8<QQ+Ck9n[)4EU I3I4Mӥ%$f@/a8gnı\<7 .3M3C!GOH< G[Wcx't63qƯp~sqbR\T"zJO-m)kw^_ъ[O4G?x=(ş۬?]oro=ST< O3L ~Y³x^.?%x)^x^Fx6qa6ƿCh:xވ7x oیߐa o;x'ޅw=x/|f0>qlO` l5$>Oc/Nj>El#e|_o`|·|b7{!b?a?OpA)p8~#p$~ /1Gp[aklmI| gЋ> "_N2bg| _7 o]v}={ao}?~'8 pa8?8?Qfq q8'`NI843p&q."\\p9~+0W* `\ \;\܈07܊XXŸ K܉p7,ǽ+)TCGc xģgx4qx<'Is<_<O <ųl<|/ċw{^KR /? *bc6x-C^7xތ?V 7މwxދ~|ć>> l-6ħi|,sG1~p Oq0P3#s_`~8XqfDq q*N8g,_< p!. . 㷸q%j =n p3nXEX۰Kq; wcr܋4!#1p<GQ3<c8<O$9_x xYx6yx>^E;=/?%x)^x^Fx^_1 `SסF o[x+ކxĻn}x?>C GQ|  [clOS4>^|9|_=vv—|;k:]M| w]ݰ;c {cЇ#?88Cp(9/0Lcp,8p"N88t3qƯp~sqbR\[\ٸWasp5bZ\z7F܄yV,",mXwN܅ `9}hbh <'x(S<Ÿx x'dST< O3L ³</x1/K2+J 6«jow=x=ހ7Mx3ނ[6' o;x'ޅw=x/|f0>c86'V`[|§_lvė_ o[6bW=|{`O?^`_> 8??Cqpq~%fhcq qN)ǩ8 p6~skp>.72\ ƕ 1:7&͸b>`!a1n,w.܍eq/CCƒax8?#(x'xO_/Wk<Ot<o,<s<</ "/OgĿ`# kM)^Ѓ x#ބ7-ovw]x7ރ>!l#(>cs|[`Kl ')|A/>v>/;`G| ; 5|.&o;.vnC셽~~S!8p GHG_b&18xY8'd~p:8 gW8ƹ8\p1~Kp).-l\09s1kp-p=~p#n<܌[p+cb6,R܎;p'X{>41t <'x(S<Ÿx x'dST< O3L ³</x1/K2+J 6«jow=x=ހ7Mx3ނ[6' o;x'ޅw=x/|f0>c86'V`[|§_lvė_ o[6bW=|{`O?^`_> 8??Cqpq~%fhcq qN)ǩ8 p6~skp>.72\ ƕ 1:7&͸b>`!a1n,w.܍eq/CC'!#1p<GQ3<c8<O$9_x xYx6yx>^E;=/?%x)^x^Fx^_1 `SסF o[x+ކxĻn}x?>C GQ|  [clOS4>^|9|_=vv—|;k:]M| w]ݰ;c {cЇ#?88Cp(9/0Lcp,8p"N88t3qƯp~sqbR\[\ٸWasp5bZ\z7F܄yV,",mXwN܅ `9}hb伃`<?ƟxGOH< G1x, x"?ǓKOS4<37[< s\< B?G/r+/«+6ƿal:xވ7x o/xލA|>[aklmI| gЋ> "_N2bg| _7 o]v}={ao}?~'8 pa8?8?Qfq q8'`NI843p&q."\\p9~+0W* `\ \;\܈07܊XXŸ K܉p7,ǽM {<)GΝ£<<OŸ % xg-g9x.x!^b#^ex9 WlWxM4l&ſuo›ފ?_x;ށ;.{?xޏ6Ç|DZ9>-%>Ϡb;|El#e|_o`|·|b7{!b?a?OpA)p8~#p$~ /1GppҞR#R'SSj墳jإO?w|äAsAn~aUG5aFMȹ*FFvk:u[SMz{0Ώ{ZzOƤ C ݾq-NZ}aqhM(B!"Pߦ\:nVq*B!\U݄B!u7!BAM!"}PwB!H݄BŪ%wYcWZdGYYa6صxnB!Dbن<;62o>wZm.--رﯺBQXOxS7|Ьߦ;3/hqxbKnB!Db~wo>iWoY _\"0yXkCqI_u7!~1/wal`t]X)oպVa7UwB!VRTp+׭g5UwB!VZ\ 4z@^aw}~5UwB!V߶ FDXfmX}Yq&M/rFqaYԣIro:~֜6n5_Ne @hۖmwݗ-_.Fp~۽$&B/.Sm':$xYG˰p1K`Iro}e-eZhVmFw_6#{"f-wq2v6tk+ްW6I !]٨m[cɲ˶uo4k6߶qem$ޢ^1VwKh#wگFSka>F]b/$Y&B/.SXZ VI -/yiӂ̒VFmb4mճ{M OufC~Abt7r:~|sI[-k۩nB!D2EPXR,_^~Ӧ%%tFnMR L9)ضmc~A0fwKl٩֌dle+[xmc-ydѴľ+/nB!D2۷h&Fۢo?mi|il*(\!/FwKlSM?vfК?hh\[1ֱޜ~UwB!VٶcǶ:D|GSZ .޾æ=VlѴ) @}+^arojdyEk6n_vNmv}&wKWM!_X]dؖl~mEqcI=r!m`O&F3nda[󤑔1;BQLum-Ebv7Jvd'}݄Bevڵ}΂ ctnB!D2eee ʶn\P5UwB!sW9|{e{l+~  !y^󬜅}({ ,_67UwB!t0M5K6G>xqƼnB!Dbap3yQ?xIty|MWM!_,ߘGz9k;rױ_P m݄B!u7!BAM!"}n!"Pwuˢ@n#Bnn lܴbæ7.nc#BnnYeK+6m^as#Bn$Dpߚ̅-K7 !&P)nB!DQwSj9nB!Dn$1nB!D uONUwSj)nB!DQwSj9nB!DQwSj9nB!DQwSj9nB!DQwSj9nB!DQwSj9nB!DQwSj9nB!DQwSj9nB!DQwSj9nB!DQwSj9nB!DQwSj9B\gBwT߅(J)(3n(($e_|Ĩ)(3Inݔڊ(8;In7[%e.Fc~Fv^FuHƆ8{~w+(u7ݩ60oaՒ%-ˆni~/K~8kI4 ϨX " /ר4QoźΠS'(RQwSܝn/r.if$#:`‡VE^y O.U4~1\6~dD!Tt2뤜F[}m(JEMqw"t&Z_ش-zip +:VzBVV({w6uV ~6OL#i:}y\̲-\95Sf$WV4(Ro;RČ֜ѢU ӈ%}-?pn+NFVs`-Xԛ(JFwfЪ{Zb8[ԘG?qRgb3q+zSEQwSܝ8-?RQcBy^y+hMQDMqwjy뾻G3q.xJ[<^##$Vo+!EQ%M;\EQEqFMqwEQu7QwSEQgwGMQEQIvw[yanJ((Լu7RB\gBw7!U)u7!")u7!")u7!")u7!")tn)-BLHPQEWQwSu7EQEqFMqwEQu7QwSEQgwG-v ߨ0L_(!nSݭ7[vhw%0[scZh={ ~5EQFMqw~C7Zje0Gq,/Z20oa2l*Yucf >4Okxg,Xװ}&T6{,B֌\JQEqmw yE:`UŠR֤K~31nik_$EQ&Pu7%qawFv^F<͎ߤy}AOw YUwS}(k6Tu760oQ3ל?|7kTYnu˝G`G.QwSEqw'mr"qSlUhdV_|/'5s Knh&t@8u5YqL>Ĺ(ڨ)N[¨?.JVdx}Genlb9EZٚ֋%"T9$EQ QwSܝ:neKJw_Tn-}(D[ljݜdA|U(J%~EQFMqwEQu7QwSEQgwGMQEQQwSu7EQEqFMqwAwB!LH&B u7&B$u7&B$u7&B$['nJmEM!H"nJ-GM!H"nJ-GM!H"nJ-GM!H"nJ-GM!H"nJ-GM!H"nJ-GM!H"nJ-GM!H"nJ-GM!H"nJ-'!+(Jj t>CG! SMqEtT!H"nJ-GM!H"nJ-GM!H"nJ-GM!H"tW)u7!")G(u7QwSEQ8 n(gDMQEQ⌺₨)(JQwS\u7EQE3n ((qFMqAEQ%Ψ).(u7QwSEQ8 n(gDMQEQ⌺₨)(JQwS\u7EQE3n 6/(\`|u7EQEqu7@w6u%(qEMqA߼[Z|i{(J:FMqA\|i{(J:FMqA\|i{(J:FMqA\x 4uyi)Ҡ nJve>uvi)Ҡ n 4̽V% nJZ>4̽V% nJZ>4̽V% nJ ox}B͓( *n 檤j^iW6/UGILQu7QwsU0{7쵢(J([ȅU>nK$)ͪV;ñUžita/9FBt4 M~P+aV6xk}KPfOpKqXBdE ϩg.>ex4̽V% nJZ>4̽V% nJZ>4̽V% nJZ>4̽V% nJZ>4̽V% t7iH.;PE+qvWNQwSj)r-xQiJZ&{EQ amkqE+Ť| )T(u7I ?k`i]z5(T#n *R8xsF;, =xSۯ(Rͨ).HK;eW7uש PEFDUawԛ(u7I okW8z*^OZp5\~LJT8޶&XW?eo:F]s͡f~GUс㘡;\XXg5 {t]sn\qX8p5Tm\XeTwS%] 5n-zvYqX*.gnwsvoـ+Tj y뷺aإ}(3yD-b\(BC YYPMQzu7IVwj]."|2/ѮGvJIUvZ%˹:֓:AVYwj9q-#u(&n eg$m7Q@>BgMI9]CքZS[rΫݢvJf-V" E7QwS\$v3Vv\.%M9O $涜sl䍳n4UYΪ@M$ͣvnD}.rnK6[عoTw|d3kn7sRO1KY}\ cִD<G/ Q[:]̈?zV~(>gOQtvMV9`u7EQ%n RKLΨ QO(Eyt7n:>[:s֌bhoZXCK7\75TDlڧU:M> (u7Ik-ct`U/E+/*ĸQd9jZݿG[MPwS%] }*_GYXsv7dԕ`{DH6 k4=Bި 7xc.*n(u7M09H`}py޼1hX'rֱ|ee7sHOyUwV (8[Tu7ri=uEQu7Qwsi=uEQu7Qwsi=uEQu7I6i]z5T(u7I ?=Iփ^GQ%).H푶O \KV!QwS\Ď'&{1!D<).HdS>!u7Qwsi=u  n"_N"DUk)Bă₨|M:cBxPwS\u7Wփ^| Bn N{e&i,dqeg":5ⲿ31.F#kC6jEed|d9'F.M#^Tu7!D ݬ'uƃSewF3YEy2jq^Qa}Iʘ|bTݸp~Fދ&H۩YdA@ݍ佒4)?n΅_1'0Ĉݒ um߄B$u7n6\n4㇉}X+;[1'0ĨnwKl/"]PwS\nh7?lg6y+Ѻ#wQSTEҊ}H7H*w|b$!=ײҼl*+XmCY݅kvHdS>!zԖgLᨻ !҅z݆__M̞9)0m\e`UYCs&Sϋw]/˗}^b}6V{ u&3% !D<ٛ/6#TfM L::0yT kXΤa{o Vy 5,b9AS;-Qw2 t7!BT[#JFu #gԜYɡ淺n[cԮ]!Q1SvX݄B$gFu}VWg]{v.)YT#_V16]^iY,)]ZbxUwu7!"3kcurΝ=5wfVѹSFN|G}ݍFfMٯ^X,k Z*qJum f !}-dtb-4VtwiAѦ5WpgEM!H"nqλmWa4 ϞVx*@wKEQ%5Igb_yW^fuVft±)$B!DCL'y7gws蘩.L0Dr@p_w3t7UkѥTvTIU݄p!H#6wS%nB=K6ݔzu7!\%zXnqλk߲>Њз}>/6 Jŵݍ @ד|Dk}-k_JSdcٶ ewٕzJ(\R}(J]Dݭ᠇u1)L58'kPY7q*qFMQ]g'9s,m;[i5$6̠M̘>> LjUL\V -"r~!Xs^wwnSMpu%)y,Pp͞E,gP6/c#wf5$6ws| |5=0{Z`qI#We ͙8dO>/F9٪i}μ`>YĂfO/u^OeYQwSfnj Oٳd_-,\tܞoƆXDE}+a5$6go`یP}5%0}B`Qa9پo**{Cݫlu7+n܄}m,ˢ&V6oT/Mu+54YSsf3'nWOm:ӰV7Mnj-Fjmmfr[;7n غ;76XjXm~wVk6jڤZ79Mnkִ_`: QVe]nmQ#o'm`"hi};[ckMf-s /hڪ[M*t<{uZgnH : 9IB䮥n u83['s'۷w={[|wI΢ڷ*Ƴuz&;f +DDVjݬ:~ԨqKf5kU+y+y;UYwܤnvgW,gt>'Lw׌ T؟SLjܝnՌ?ą5#GaX}0OܖaֵEnY;-gz]ݸqYZ5n2Fu/>Smڴiٲek{$[o9-25' GŸ&~u :u2oނ .s&uuw'r$~Gcr%wyaO#2lv!rzsF/fg Bق1%_1nv4:ϰW ĺɯ6F{9N=]Eg^m噛67O$ĸ ۝<7Ww+((.Du8ѽۊٷ[ݥE|):?3eJ:`:9/:f׻}#tX*܌7?ǚVݫ79jywZG]ӻRl;nϷst]׷bؒyjvS+jN؟ gqʺ[mȹ5Vew1y vߌz4[[쇵y饯?kgg~yiߛPr`r٧m:nݺu3׭#s͚Pk{0^3  k.ƕxMڵfNNe$uyo?5ú=uW[nz@w='m" {FUq԰5E Y]mvutTˁ?q35*[&|Ժ8oսR7Xi Fw\q5jEv`:bg g6f̘)cWs___z暍W~bfkZe-Zm}mj$Ԙ"O3 wwwOɻ:bu72>ޝ瞝˕Wnnu$>kĝ}ճGNO nqf|.e뜷}eVw۾neF׎)L ]l)lGO!mK?T]<_"^w=w:)Z|BQW>(Jbw7gq p-8ܲG6geM[D}:7U_=fy7{a/Fƍơ%LZ:6WSH _pC`=dsw-oqOgfO'w-cӜ7wt7k=ܫCvma3πi:=fmPߌWCpF*۹gw3g/0dHGſ;#Y>w~0{i?!{}-y7gwsԴ8]nݼuK샛aGQc(I΅ Bdi_‰5̟d&ݜ3e%ƉfqnNjԸo?LU IUw;wINvo.,Mxd:\k+Wj*k뮻~i.n[b.`37oSsF8DQ-4fi𣜜Wxߝ˿dwοo}DI޴afaaΒ%f~_E-Ypu8Cw=^/v]ACJ Sfk-Y;ax>mokϦE3ջTiٵ_z '};zY6B>/̞bKv7H󄻨:ufERn=IUw0`q[߷ fIu#̏?6@<ꫯ߿Ybn/ضtlk=|hi[/y1[dN66Л5ǒ#8p[ݙ4~ɥL9,({w*u%7۲y[|wS%y~>kRM\:|k,,Sx ͦl[RHniu"U7m vN4|eyӗ _S֭ 2'uX {1vk+((nz.7޼t҅3?k-4C[/MśFaaۼ#qΤ/Coh^N#b$:*;}MgMQRw7*R#tHm:c[F7gN_\M61+>|޿}>3{gtԓm5PYJƍd&4;}-y~[gZoQ?WA)Jr;{ ʿޛ+YK-n VA5)n#G1"j敯d^)D4O> .;7p_d 333{*kO=E{FBi2$sO?$p_w3=ײҼl*+XmCY݅kv$7{|oP}} M^l5=s a Kݾ&UkϞ=OʝO q{9p`}2*3m[Ŭ'֭;Ջk~Igaa褳͛{2wR[>r3rd *+^:N%Ψ)JrS^;{Υ3w-΅Kg7~qsΘ_e{C֐Hؿ#GgyTnIszYj[HB,"S쑸J*}- ؄YQ&_5~p΄e%"r>(oq:=)JjpHڿof0WR$֘s%۰7k3' L*kh!{J7y1nqv7?ĕ&D!_ݭ^֣GݻWQHs%g>{3f۬)SG& d ˙4l}V{Fv7uk,݄hȨK6ݬ__ɨa̚399Vwz"mkי:?\vw3 E'2J nB=K6̨ndo޽ݳkoَ%;wk*r+cƽ6f'9& ѓ|Dk}-ݬ\8?`^ٹʝ6:w)(oU<>Hy#wMÝ'=m Qw…I^g2wr~[1z+]hӚ;E`O5j\ !I}-yqp~{ʓ6[YhKЊз}>/6 JGM!H"nq_ZVMe%˶m(+^pͮU{3f۬)SG& d ˙4l}VY]÷>4r͹[&B$u7k-W2kh9fNu7݆HwuaGHMY c6:X݄B$gFud$}Wg]{v.)YT#_V[Y=o6JgXUK )*KXw+*ٖWTh%-տB!DnVw .\0/wSsgfN;edwڷj^fVZ>fMy fIF_+ЮxJd݄B$g2wr~[1z+]hӚ;E`int7>-8 'jB!I۸^o8=vyIȭ#~a/ Mq3옩J\ww3B!Uw3{w/[+/u+3vLg݄BV-y7gws檨 !%[];. R%QwB!KZu8ݔtBQ]Ҫ),nB!DuI[-wlX}۷b󨟫}݌ufo h#/z65mdnM ߲Irk}f@]4`_9YXB3ͺ[1dSYƲmʊ.\+UOPmYz66g*kF3gr5g/\E^BVдwR[>r3rd *+^:N%$|Կ̷l]9\b_! Tݮ2>qQw3*>6!0cb`FV`?30yԪW3apYɆ>/g_Y˭?yeXwYoc8KnУ#xnB!ҝnqλ {9=-0sR`ڸLtST?~s{^ɧɆjVq6Xq\1SۜԈ阩BwV-| fշYS'L3i؞F{B'ZYA?Z5<^}VMRf2?hVk4X]h~A YrlW'%Uwb%3kj,`Pw[mtQ_gV4j<ꥶU|쩇KAs },Uv7ݭjTŽZKW/R!AZu83['s'۷w={[|wI΢ڷܪf`W6ⒽĹકv7v{6 ]c1/6^b̿5\c^Ƽdy3VX̀bK>hvY.A{M+X?r~|4ȫϨ[7WP>OZNBDV](;_-Y1o_2q΢W&ݾxy;wh޸޼vyPfQ"g*kZsjsYGYy@YZ4uD.a/mOzh(Vm֢l.o-QTW(JK__m~تm}4Q# *E| " B%(᳢<$D@H@Hx$M6$f ?3wݙ%@M9u!wfvl9MY(,J n!^xFx%)%36h:^v3"C7h4EQt!v[6$D")tvs77,TזjvUyc AX!^k6 /xi l[vFDݤݠUm͕YfPFF@tݩݚ[[M-- `niA-I3 ‡ څmZ[G-'ĽMUEƂ$ Zt͔`L%Ŧm4*8BkaqFW}FT+Y~|u4=hkpg4# :v&HlYA`BaO0fX>r0foCfwt>5vp軫?\B?Ta.kbksRtM)?vXKUPaC cT WW[Ih^g? GoSTy+n{[}5s w [-y+kk\W[\[;LKMtFD;EjOTOI#I-A AIվ (  Q$.O͒PK8!VY:'>0ԬNi&c)#'i֨LQkyyp.ʼt^nɵvh|B4@c@JNwSO|!i'7d*SR# vU -3?1i X8zDf[w 8 4ԍԏn{s&;~ڭae~p_9͇!D28[M.Uxh2m VҾ$~%4*EWh7W<7oh, 0B<<ˮz6Nzv Z߰k4#rl+a>tŬ4ȃC;aFZ2^LKC> 8^LNm;N*NEPw BiPOpݔb# VK7*I ,,VLD"ՠTn_UgdJ *1iΧ$ 3 c/>тp4J3J5c24O*NRgyFr8}/ۀf/2b׼|H/"yT2zEkl\uҽ:&$>HHLb M͗:&}UۭH rFZ QWGʵrCL'1nni ]n.$:&fu4][ ud9C/64Kϰ|J3w V2:ôNy 766\(ϛaoh0f3 G L_4e0g:b2~nYqMM/ߍWUFA_4Xb, 1`aIՌKDi& t5vWS #J%4yR'ywbbNs~;J#z1 MҘ8ӴÇMC7Z)9wN{@y &:nj)r$e(9 SމV#VhұZY8-xh3ы0?T +]tq(=n VfxZЄͰy1a0f3]e8ݶ9W!Qe5e2O2n6ZYzC [ Es fGz Ъݬ8ԘѬיּe@[t}ݘ3]fb!4,.'mi)w3*t6lf cCauu}1833̙Fv3R$ [ML5(bH/T _qVvGlm)=Ph :OyVݬ>6h\:hGlm[gUy:8_k+az_v0)f3 3d8ݒEG9_{GokmԾ?oW2z`\3 Bߥk&O]Q.olzIl!}*g^/;ZzeTڪ\$5PziO~lZҊ6yDZNaQHBiSyc[5M#f3 c^yAX%h鑬ҬévVR8/^낮cO&qo<Ў. [{]>).c\_7i{pu>mQ]onu>۩j~Iҭ3͆ cCf G[&nښښeHjk7~ q^n|\c:>cj|TFJ$>yTO&uthf_pzNl( >_"e(#1a1Øa0ff8R,Oֶ鵙^MjrwZS:`@:4n#|fCB;; cS2.fs3La/8%,gڭZ|~c@ k7PuBk7@ EwZܱe%/".![CC o = ʐH$Dp>6lHwC0Bh`?cH$D"[޺'2 Nyqk_ j7$D"ݦz-ޟ,MNSmmegky.1+niGrH$D=zۆWfnhL6o7m6n4~u[ߛ3#,ny2 xjBjڔ#ml{X[,]DYѢcZ3倞`3jyLnUǺCT]$K,9KD*RK5]ǣ:G$9/ڀ-u׵JٿU1T|D}/ͥ-A(f-e4p͋[U@\4P![Z?Y8z2=Xu\cW@@ Dgnk62%w@c9}9m9u9#ޜid@ A[GkLLRM;MӶh޻\;7Nߋ<}WӳW^S P![}͋ GӗMYL}V9mS{cKwߊ`F ;c-LA`07]>n P $ ~h"Й `Q[ITa nmv\AnDn:ה˴?ɔټos{k]g4oK,eo8 !6(c *,n Z4<&GB:vI6z, c5$5S};:Qex@\Zj7Nkf7X[Z$XJo֛Q/6bRbd*S#q+8- IuF{x!fnFn5=BkP@D&­z[ʚ66QKs RA;.vl1*z3c腇fbzC D[Ş2K[(y0/q)#,ڍ=W`QMON%T1g7@[}7^­;pڍ]ͤ(C,E')>,׊!"Bnk kB/- xq* \v!z :Jn#˵A @  ڭgvH\j7@ :pk邏.jG^E^Ffm wP-P![]о[몥{;V 7 Z_O!?nEnH$DaniƜY9i')[R?X!o,;ˆr2eŸ~\7 ?Hߎ1թ'Z,{w_`e6Nd~>ݐH$nъ _~&錜!۠~N^ݵUP#<Fم;e~W I`5TuI Ҽ2%~j7$D"f߭trNi785%PXf7 3B\tKq=S\Kfl1QbDQy~[~j7$D"fLoPF_[oU퍽'ayWxE$5mYHh (8ƿ@/P]j7{]>?H$D9}V`eKKu"ؾۀeGnPgK̂!vauVa~Q fM5!tTf{*ϫ𚩀 D".ewk;:d|^L L8Շ.!Kmkrn_p6+>+LʯV l+C^n,V>@1$,cYzۭ<2E\_naD"Ȱj B0Y VF dorzrꗖIY'  jhQ@Kl4\\o¥ ϰhp @ pk~7Yudܑ; Z[*}> T B xv^ X}mP_*5*@@ Dgn +_z'|)Bj7@ :pk~%,Kmph7 .^ ndeyc/YB^Mfx@FAp_9wD"Bk7ßvv ׾;ۢEQQ2K⢨iQ[ITaΊ0O(A ! 1@9g8!S j:Wixfͣ/W1QMFH ߓyn" ڭ?o/n^ڗ&C[4v r6!ּV#jA(Xؗ굛]F+]@ n~&L6Vzk<¨SB*!h7c7f0Zp>]<vn^ǒf1ٜݜٜќm}on .y)25{n4Z@]!ޮ@ ~pk^̄PPqiRqږ={7z=k{ l.oZ^Ch$jŦ{VVB^no!OsM[J'GR.oE DA[wۼX|ϖ˃LRLM>+NԜɱuRژQwR$d?ؚ7޲Wn`?oyV!XKsސnk͹-3tp)ci)uyf׺@*[-6H7kv$]݈?].n[F D ڭhG@Wh0gMHۖ4n53e56A4"|s翛[7* @ x[Z$%,9s_k}4}/#z8ƎtK]w_^@ :pkpTYXr*(=Zz`Wig[}-uM4ҍb0&&#IsR@ A[E&nښښeHj -VzUQ'׀ @  ڭY̟*ڭmk3>Jq-ЛvC 3vVyvk%/c[vC 3vnv"4vC"H$2HvywZcmKf^*E\:D"Hdn#Yk^avC"H$ m @D"Hd2ޟ7uDƗ \7/nKWq@D"Hd2ޟ,MNSmmegky.1+vC"H$ yn^ǒf1ٜݜٜќm}o ݐH$BF޾GkLLRM;MӶh޻\;7N(Kw\їҋ7WX]wEvC"H$ yn/3H1?78Ssڦ&ڗBf0 u T|&Zǒh$})6h ڍD"w\AnDn:ה˴?ɔټos{k]gKbٻM3JsJ+5xPvC"H$"BI_93Üi4/j7#n[J@Մ~Δ.vIK5SzyTWg`-a"ѻڭ")a ܹs_k}޴`%QhT F鯙֣zqD"H$Rv[[wr,ǒ{HViVFJ?+MkK;n{zy. 1$6TE)D"]wK^?vĭ7_[S[S]^lQ;ݐH$BF޾[ʚ661`G DvqAF P!H$م}=-aez9jfkQa_D"]wn(ܺݐH$BFpת_n[2R)I͐H$D2vnNkpG@ DoA8Aqaڭ D"LƯx;)?d%7fh{}7_d7 D"M MMi?{lc_~Mn =˾邏.jG^E^Ffm wP-P!H$2)gT_^z{8RBqPpI ~:[f&d& 7_xnͭ&ʖwqyE}ҏ4{ CS6?-ftİIӑCMʒPͱAz^lg tO#V]vc3(4* ?46~f ZzWnH$ %+q;7_nJ{ަTo^wt>5vp軫?\B?T@/$D"Hdx(i7^<[]i*P2!}kՓSLʲgyvC"HdDSB[s>[aG1EGDw*W@yth> ݐH$єQv✫\oWG\{a6P|BAq}[wC6vC"HdDSZ[ւgcrR[i>lΦ̤Jgw#%Lf~|pa @ Dowt2E(Ыȋ,w^=z:ݐH$D2vij eµVGqSmO/E5v 9D"H$3R2͘===79$%cKGƞ[>"|[*(n.\&*1%| 2CM/kI~:+v O Fcռd6g/hf :N"'.NivC"H$ ڭ~UAgNO>B zm];Z 5CN*a]*Q&H5xdD : dClP0 8D1TRd23r D".dWkƚUTf9m O@aQJO$t q5c.Q=`?Lq-AQs1w4O|Z,P{lz}/(JQ@D"Hd#dڍ>ytVt{OMZ0Ix#LkFe6j&+#2W)%hWn}wKj\W6M 4vʐLuS,(0Q*@k9оĐ,&'X_ hB;4@ S](٭[ܯeR剃A9گbo]"kEIBhnC"H$y[ ۘ;rG\k^}KJA(KҴYca]n]ڍ Mu Ȋ,Wd:#[Z ;Џ33:ZΨ;㫝yF[|;=Q7W 3iGb G }b1c1|pOi>S=Ȳ\F֗Z(*!˯F<b+- J G۽_TT*4T#(Z787_HEg" 3 cf>^FAV7E)d9SDBhV\\l2`')Rr\P]ăImBhw6~1Gd4) Ll+C'036-[Zc()[Z3S0f3 }7DAhVb.1JD>EV-Q*%CN%fep,*[Wn7t@e R,$.lf}PhV3c1Øa0fge LvIBk7RT*rRF)-9lYrêȤMMXMf\MR&RTӰT?\k x[Q_R5چ̰ڧs`1Øa0fX wv{_N'pݼ/MK5jAhf-ZU:ά\vQY噒תoQe ΞҲ-˖ ngCl6(ؠ,;LݶmRKZ%[6tDv;4fݗ {E@s̀;JZ F[[fHv%~[Yv;BW) PEjVLKC _'xAQQ;kV< o%ONd(iu'`%̶y#@Cl߹A*XMh 46Y|4DBS_RWB^rʝ+Iw\I#’M rNI +V׬^ɀҮAuS:KHPjymP/{䨔 +cXX/ԠSi'WfU>Y2D exnzuyڵc1Øapf[88kh?my| MtFDOzf0_ x4uU&B鮿fڍ!vs7hNW4675ԦwU꓍xRmlN[ݨHc#6҂<ܶVi,0tsϥh<VEG7OH!CV ==:&:fzU111qY0jin*q.1n>702EdV2͆ a5{jJ3Fs+b1Øapg;X%h鑬ҬévVR8/^%ľPfC&gZS%@#4 GXh VGf;ZP4phf~n4Gna˻11--jm17p1uGc*0[l[4ˑ4]_Y|HB2;3-OfMm0tmz0k A kZO:k/BY̟*ڭmk35]L[}-Gno"AIng?Z%gk+mNAҥU7@n0֑~öfqG+j A;bk?lU[QZ[y(ֳҵ`H1ØaHa̰_%zV| Kn{[s>/n֢]JsqnAhze&BTμ^v*,UϹ&|IjҞش@m4 /77/ CizR7Ҧ6Q+6> jc&F0f3 wv-p}ڭ.dDOSگZ4Ws=|J _jZO6ר.7Hj5~$֎f c1! {GMwZܱe%K aPz[=CG)WXOqعA-}~)fOS}4vt^f!'?nH>mLa0f3vB3)P Pmv7wAzbQr3Jm!L!p`1 3ܹM0^-! n@ ΋KnH䥐n@ ߤݐ=K^tD"D +y`H$sDYvC"H$R D"H$D"Hd!j7$D"ȾCnH$D"}hy11rhL۳T!HddqN3_OHV@rn?@+I(cP%1:Vf}jjH$,`"QuT!J$DF&i0B,_M{ճLuNvrڭylmj1rd{2Fa eRU<Yx;\ z.np׆D"H$ɨrk;]wMoamw+nvō}woݐ?]]<E~n;~ݘvF>Шsȷ.P^ KXwwl͗09H$ #5ڭ⚻s[Ώoe:ۻ㿾+w?q}۩rj 'u~VA:3DFZ\uD,.qM*-6ʡ瞸ԡH$D"R׷o*I%?aˆ=Pv׍^}c?]qElŷ+.>oe }oTIϙf(j&8H3zȊ."R,<;kb_wW}2ʡՒbJ0DIR?- B?YG^;:9R'V,&@±@w,-iK#N6#?"`_4Q{"̰jD<&jZraQm=ae*nPqx?|}Nmn~aTB(Rå|o|StECWiiXv] {x}=C#̹-+^YUk̀%6Zx͋ۯ}fPH=Fz]hlq@8סù=0m SC@`yXrNJؠWB3SWQ= cYxX?Z3?2D? vă0H$K]0>RϜoF^?lxʾ@3x'UFMʿ=UuDu9 iv#*}%j? \8Q B{' :5/{` }A;M۩72HGb[ѷ-AYCΥĞ[`Oۈ!v}3tj_bֻE87prPfCKxBX{Z610u3m8rfp#&#i/@SIc06n|ZЌ/?  eBD"H$ٻG i/JaCVnw?Wt+噪<Hb*r'S" )h75^N7|p MhlyreDQQ`up㊻^? TnIuLo[x+yi |T`׊h7Q(7X\Oڭ!#X?5AڭyR[@|0͚[CqnQfr oTJh02}(AYˣJHhb56]G+a!ld8hf 4oND"{+$IU[]x$D"a'lq2AxuԔgV=|Ppc~yoOɘʟxUzqi_ě@Ik7yv9c<.1oX7_!һ(EeaYx吚F33\|Kٻ [iIip_qgc.6:KyϋnEm}:uWS?qAF;:l(#oo S%-r$b6Ǚ~ǃCCKݱݒ^}-x_0u+R1a([\( 73ȑD"I&(+$vöqUr<^9&.޷?XyE||bӼkɶRI_ןZNJo2u>qqOoz?{&65Sڍ7;ȱ Sz UU-@"HdX_HdxY&I86VnzfG?ix}]݋oUO_ +NnOqLxEN{rǗμF}uUp&jb >A"yhh4xR?oUS$Rf"bs"oUo?k.zKW}Oc[x}kƔ/,~L,A[s8ynBH$D"@?1Nj k_{^p˹e0wrx@y$Ep ݘ^kR&ʚǐȈD"IvIuݒWQcI( 9KdA-mf@lp=m eAM1'!H$DF4kY 4⎓rP)mUlSu㦹0JwƠs;^ ¬A@Z#s҂|YӖH$(6XTQ]D6i j8 cNx GƋo+: @oc 7ZTM]z $0 7) +~r0D"H$22 J?}2L#8S*{EnjˁW`Qͦh7*߀ č3&ld7|6k%mDF,C@"7 /"pb`!U|sM,lvEcX)ˤ+~ yC"#ȥ8ұjS(Qb6iNŪQuĂF2=Q8w"2z}?`62}`C -o}`=+] v؏`?)~ _ ``Z~?]nv#؟ 7 v ح`6lX?b.XF 6v/}`{A{QD'{I`ς=6l XTi``f{l `6`s悽6e`-{5`^{l17-[`V ljw[Zu`%? !> &O6m 3`I`;> l7`)`{f;vaL,/;v,,q<```'N;VVV f3Y`60;XX%X ks5yZZ΂y|``02 5`]6` Mo};`]}J~`? O~s_W` `5`ׂ]{?zv0g&f[n v`# v؝`wł 6lh1`l,8{l<`=0#`=8'&= `πM{9`SM6l&`^3/`6%y`/{lث`-[:`jXE}>""" ,QVDs/D զIWҕڦ;iҼgsMBs~MBy ^7-0 &w.xL`>O39| _o - l0߃y`>`X ~?_` X ~/ V5`-Xփ `#6-`+Hi ҁ AY \A(>E`A (PAUԀ ZP?%hZ6``p8CapG1Xp8N')Tp8g9\p78 Ebp \.WJ\:Π t5;ׁAOpzAAp AVp nCP0 w`F{(0c8p<ƃxLGcqx<πgsyx^Wkux&d}LGc |>_/Wk 3w` 怹{0?,Og ,Ko`OVjzlfl 8A:p7 d^ @>(lA1( @9 T!P @8?@kR`?p(8 ځ#(p48 ǃ$p28 N3,p68 p!\ . \ :@'tWtׂ:p= n@op#@?p3n@0 n` ;.0w{Hp/F1`,x (x <O,x<^/*x o$6 S=0>i`:| > % | 3`&0|#XE`1 ~%`) ,?r V*7X ր`X6` 'H. dl< rAm`;(%@(TjP jAD@ǁh ڀ}@ ڂ@Op8A;p8ǀcqxp8NSitp8sy|\.%Rp\:+AGp:.jt׀Z\=  n}@_pn-`[m`0 C0pnp h0}~Ca0<,x<^/*x o$6 S=0>i`:| > % | 3`&0|#XE`1 ~%`) ,?r V*7X ր`X6` 'H. dl< rAm`;(%@(TjP jAD@?q-A+/ /p08 vp$8  ǂp"8 N p&8 =\.K2p9tW* t]ՠ+\z@/& [0 `p;ap' #0 F`  Ɓ0<&`"x< ')4x< σ?%2x ^7-0 &w.xL`>O39| _o - l0߃y`>`X ~?_` X ~/ V5`-Xփ `#6-`+Hi ҁ AY \A(>E`A (PAUԀ ZPq%hZ6``p8CapG1Xp8N')Tp8g9\p78 Ebp \.WJ\:Π t5;ׁAOpzAAp AVp nCP0 w`F{(0c8p<ƃxLGcqx<πgsyx^Wkux&d}LGc |>_/Wk 3w` 怹{0?,Og ,Ko`OVjzlfl 8A:p7 d^ @>(lA1( @9 T!P @8?h Z֠ }~-!Pp8G#Qhp 8'Idp 8Ng3Ylp8 Bp\.tWN3]A7p =uzzF7~f`0 w;]`8^0 cX0A0<#Qx<O3Yx^/+UxoIm0w{`*x|>t?|>K|foLf9`.G,b~KRX~`Uok:l&l[A*HN\ 2@& x< @!?(vP J@)2P*@%ՠAԂ0:q-A+/ /p08 vp$8  ǂp"8 N p&8 =\.K2p9tW* t]ՠ+\z@/& [0 `p;ap' #0 F`  Ɓ0<&`"x< ')4x< σ?%2x ^7-0 &w.xL`>O39| _o - l0߃y`>`X ~?_` X ~/ V5`-Xփ `#6-`+Hi ҁ AY \A(>E`A (PAUԀ ZPq%hZ6``p8CapG1Xp8N')Tp8g9\p78 Ebp \.WJ\:Π t5;ׁAOpzAAp AVp nCP0 w`F{(0c8p<ƃxLGcqx<πgsyxd:j0D.PfG8跎&܂r0O .~M^MI?%_w&oOIOy kl"d|-l"9 %9ziqzqm*iL}r:m;!>ǭ>5HeosO>#<=9_FSWM]NS:(S-o-PWp+=In*SiDsr8Q9NۢոVp8huPOտǭY mGdtdzqoݤҙ@ Ll'>ÊoS PU5O jNz@_ї@x9 WN2Vsc_\*{PyJߒw\sIiŭp_/x٫'mi-=1׾Ru'?GLLwOxҜ׷u kт JhrIGs껦ȳ0 /@ޖ=rVm)rEN YHxEʩ ZNijzUG!WB^PAo#廉Wsi{Y+}|TS;m^ ̚Wc^hhV;m"F.'sgdMxZ.u@Ĝu,!4'V~2HisOX,=WT^Snig5/nF`zS1(9Ǭ9+pm6zuFs\kڙ=#u#\!;yR2}<ٺ|wcvR9˯k{hPdG&Y` jpG*mۻIp8ePډL^Jo-NѥcEsc_ogeameD{kiLM]C!.2*)ciy۞sk^SS3҉8Vg$m>3Z$ڀbVvK&GxlƐzld€b$*E9mZ5{k 'DDn)qaݤj;nǟ;Z tܪZVeF4anGC YyOQuyZIZkYp}y Ve"8dav߶͐S_"&,5M;Wa]72rǏw'!C˴P)+[޼"° {VPǂ:J3[K>mzv: ;ZvkJ}Ox#_ggGk,97YX"Wl!g-x KT=yΧj5ոh1unًtnv"t:)c('>ihjw*J9΂8F9P;Ex!2{D2`S۞Gkcpi.̈́W+;tN8LZ ]>E<>ե ~q'krh;%RUʵm&2՟#CmƄQk ֯FcKf?^l$wSrJI轠ͨVA >-O*ح@5&LG'h@zQ>cc_ȗ_a22D_Ȣ/~P֟SȮZt["TL}s/τ+ÙSoUoEB`mwz*cK^;dPqM_@>MPaQWK ըO\OuۋY}wL2\5`"]Xz3Ů}p7JߚYZ*sd-_35*wCtr%.{7I.w/YעeK\qX槌m?굖HTR]`>1{v Q^t-rKIAvI? {K`_g׋ӿ]XQ;vqc=(WV믿!''>5z3lݵwey/ȝ_X4VeF-Q{Ͽ[GnW )t)M[ehr]_,v F>~7pC>Pnƨӫ"DUX*(嗅r`{jc_$oOL3?^ތ}jا1=9m̓ zÑOR1 f{l֔b[=ٱ?/8~xx[\UmO7bWK'*zTY96=V?q9{<_U>ΏF(h',ϚCˑ{L˲)޹Gw_7Pꕁo7?#\ˆF f>ۢn=pm$„v8*Wj2xW+XS:9RqDbF4>cbˇB;&&0M6mc;a grUhc07j+5U@UUҢxUxeT~ NVz1ZT߷GGL M9sAӽ>RkA1W]G n o,FZH?oB9WD/z.,^n~KS:)cUD=Ƹƹ!=ImGf^/|9O:%G=g}3':~`~jMeY]_=6~g[|1I?x' ̖_0^p}OkYpЄ/b;e}p~;1}Ϸ7-do=hft8}7c~`XwxdN1n&7wj6^v|p?UvVpޱ_nb 4S(SL͇;=#~p`u56].O>8سA&6KTQY2\𧊚̒z&dwOqn5ڻpŸ*2Xc~j ."Y?#t ђ?Ud0;~96 m/o 7ߌv7uKT_zzwo ]SEn@gEq?5~͢߅?Ud .&dwO5%ߜ-lp7S뿙Po(7Лp)]Kg9v]ĝdw,l|z4?%H o9O,i`׾YR\[7M[em/^F]~soi@,]|?1]K快O{_O%>+ZY,]|~ן͟N˒o3{ߜ-l{=/j{s?j.h~ڃotO4M@~SBg=m{1|$x7/ :;Ӭ_o֯X_T Q_#;wp ,RQߍ*٨')٩VlՈr ަd+٩yJvfPRRRRRRRjj{̫FkM;k*hM `12KS>(5L^JomӇ,N0>ϷgY91]*1?jJ Rmvs0T1)2h5Ř0VUC?4a3iTibXeuZO%Fm;BU}9FrK-ǰ 3l n =hR?l#--qi/K9cYk>U}21I^\l9No#))j5.dl\Fl/]J6EJ6*wΕtWJKJܶ'Ro>J_TE{ۿQ,qn-b[bZ,%ֆ"vQ,q#aa;l06f 1+,_8jv~j屰hY< /p Ƀ-Tb .A!H3:(F$'؄ w|2{%n51V69aj6WNg]FVY%ВF*G{ݺ&VZfw׿Y9lzۻ[وek=d[ޭvXydb# [:R5%,Ac;ݺM\F.@Cg])\yN YZ#'a]F/)--)46Nu&z@KJ6gخ0vhz~3_qWŽ/%V2Cs$|NވJKK(e[?e ow!q'th\yځhS1x=n;ؾ΢=.UXOњtyk-=h5E܇kgvtyk_=OFvfk1 2fK(>b0_ϷΝ\nz]\~jvQ.oΦous̾qY:觫4?!}vQ,qe[FocjȞ3u=8p;Me-v3tnR5%z|ᑡk3^鵕d 8цs r3ӼXuYvQ,qsr<9~6}W:uze6F]C7w;#NR5%nLʤ-UWGa1}F8F|%˘25%;vgd 5d@sbZ͎n! 2.UX\.W:۳v&1F{^}J6b\b]ew?--͙bw;18-NS9.UXo5YKG`VsqOsGտi_ߡU?M\yPoӋ\qm=W=7\Aaok{J!FPR=hTIJvx%[5"d5|J|%;?uWIiϐ+))))))@~%;~R{"u[j1UTQ-+٩ꔔVE\R=jdQeUXUQ-op9SlR&e?ULvs(`:EL.bOXX9)|}o<̾#F+Ga ha6*YKwոkEłbѹ!pͱ&؍[h[т1b-.֣mZ]'mYo7Yi>1FLc=P K_֣g4[V.okh;FahI__\EZ=gSY٣b [O|.gZzJrH[LlE_s[,;P:Jr }|7;wysu#maZ'qӦjɟvGwv;P3پ!D @f"n>'>-3m I,/?y^!| $|/k[+Gǰ:$Uot[Xer(kAf ex=CVzs7wkϋ⣍:86X@:率:n5DdE:| H,CpsNUHIƮp:E7. [˥\̅i-mMsh'|X424KOBN|M*%ÂE8t%'*MS$r]z`0MO5$}߼PF/@*)))))))5HF7-LIIIIIIIIIIIIIIIIIIIIIIII!^$_PRRRRRJB@\vMv$$ݿ+SL2eIlYzHcZNU?d zLdT%&8E;m.87>Mځ'kaJ{- ׂ{4=Ъ)KD+?'_ݲp;|xsgx=1 &IG0W3Ke;ËÆXDdžfK s‚8e 7\amrh֣hD"/,ԎwBG7p-O'*\$|RƱ ¡`X0DKۄ/Ƞ棻W1 !Cd!|5iՐ aDJԌftM/tJ" qlO-ǩpf5ՐhӝݺU 1h! VO#&HuyioÔO\-Γ$/#el#UZk,NMMtjRjh WVI?`R&Z82rJYY0ʘJ%tܘF_R t]yE9o#a80vhrap|[.z^__* Jh&fr=8CcЊftT&s ٖW8PV  *r|vaZItf@>|r@r x)@? ~X@䩧[f#2zx08/ a6%%Pi֭JEkRm@4C"fzie 'Hi5D%b٭Mϵ02&\ZS,̕g2&D+}qrt4.V5.YYV8 Pݐ9?{TMW_Ta04QƩ~K{E4ϕiI!E~I>6j37`ҁ_PdH\/Ǘ݌|>ROn54>wKZ WBHbg-h OvDxHg3-$ RxD>ԓ㾬+Fs3S|eZnn7l^fCC;s \h"^eR$L+y{y<\qb/-VYp\*%-* &pfmz﵆Gh?|{OY1IÖ1-Cg1Z<1<āp$E ̂im2Y3f0f&< dZ<3M,!Y[[Ȍ{5Lis1V8C7wέvdCcȠn7Y4XgȎ-ܬY,YIúY8Q7[OwKME$RHRq*rtAKkȀtil%l,]rv -ESI r2,8_DJ穋XҘ~d9EixJ"GߥvɓS7[4gfl&;ӌN'9(-E5ϴFr:iNSGTbw;19]S93UC:}S]᭰ԭRM{N{RM.i0:DuTx9v41j Į){?Ov HI.y_TRRRRRRRjb0%~ZԐRfh/()))))%l I._&~TRRRRRRRRRRRJD߿)SL2eʔ)SL2eʔ)SL2eʔ)S&l@15P45ڰNIJvV1uJW%SSV2RKINW N鶲rЭJvjXPFr +_-G5#"JW_III0ۓꞭ꺮aBigdX4I.kCfVvzl1'ŎdDb&:o$~lm(nIf&$pXX3,q{yXt3 Gz}=6wzxsؐ7Hpl)=9ayNXeSfava1I;.G%}a)vZ_~D4br4ZKdj!Sv|r@r x_Zr?#"O=2 VƃyaتV촤BtbVVZJ7D%R^ڤHsfZCDP%"&[*e2,X/%2.btRKJ4&\ZS,̕gKLZ6aKVl:cp]ibs8,[KbS'X٘3K}%_. +Vz@.ij{.CwԇJ3r\I#8s~@sTODbJ~Se~yy|iȂ2W)iQɕWʢjīp5Ah?|{'ǣD`!-/cZTk=|yc@=s:xb=yH>oIz'™YY&tG3Z2-Lw`1[Ȍ{5Lis1Ik&;JTaw;#M:顛hM7Y4&z8Cvm1f}nL2Nh:ĉb|[jg,'BJEWtW˕Nl].@kl,]rv NKw)\\W v8._t]D eW2Qw]d~M\"E_agfNuV s:uG}S?0ϴFr:iNSGTbw;19]S93UC:}S2'SS᭰TlJRK.i:'蚺.jO#ǎ&SSq80cuN _@Jr elR)ed2%%%%%%%%%%%%%%%%%%%%%%%%2{@\@IIIII) e?Ir7;R \vL2eʔ)SL2eʔ)SL2eʔ)SLl4e\AJ6Q%u$%;bLj5"d}Jvj`ꗣd))%**/%%;WRJ^ ,VUJvJ%;5,dJvjDDFUdlLv,=[FNFl%%%%%%%&MPf}'-"m.º}.4[\"`7?6@d˺qY*pXl?,:z# tTǗ&pX"ay%4+,qɰ<',)M^ ƬpškVm8,Ma1Q spX 'rװ YY%R8VvR a&g* a!m,"-%{d# =ސҍvD=EP/DbM Cr~(&RXdnlB/ҩٞZ(/,f4T%dL#(KPͪM5s#2Ώ=ޤ}25ҞϋaqXLŦC1(6fc/USlh V-+\"z'[?wZïi| ~[?BgEc y5ءt#se7l^3q@_:.[bڎl螎i=L>gZ#n4'cmtgZW(O-xD>ԓ㾬+ <ϐw%p2[%Fv^li6ZFɑflP?~9t*'"y\2_iTrkz'_Z.UJZT@rէ^S&bb x<9i}QD3Iy9ŔE23Z2Lg#4,Xf4L13#1p.ƽ<)SwdGpqs3%p rf6i[gȎ-ܬH,C契faݬ/Žn)H@$[\ )ΛHb" )ӜXNOwұI׶dGwSo6 /qhPrq\ZpKx&.)Cٕ.$r]z`z#s;*;B~g:}]Tɓi'-r'%gr1xxzmzxI_N)Ţ74Hwѡ)|hP[y]N/X{3gv$QNJ^qّO@(x{Aݔ7ܻ tg̸0rD2?a%\ܻJxN%2/09|w.z|O/|dH 9l߼=F^OaȾ墍75#Hl$:o iۏn{ ]z?`lrw{A"҈383<L];cЎC: 7my":mGmaK$aB?: 2GpDpXkazH>&GDfa3̻a=|D 2 [a?jüWt/a,kXeMsd ! 9%HCԇyj=)(C= !PZ d^SS]ɖX5~q51G;\Y@b -OW*Zg̫*Dh̫ROZƁ6F5؏yrۖrԭ4inƆ襯[oe`N1 S50!?b/VKJKKa[ZKO6JJi_)@,#5R%%|+/<:o'Vb2*6bD-65‡$_/*Ê`~MV q?׻#)/;0Eb:eq}>_!lH Ŏ؈ď}>$3-Zd߼-A湹^o.5m#/j-g.ԏhN^eR$כkX&W˕ H%A999G3ӚhH >CBc1< 2ςefff=ʔ2-Lw`1[@G3!pV37t8v!;p>[DD -Ģل\tfR.ESqhPrq\ZpKx& Aδ4fN)z4u:ӜkNyI 2jT)1bFד#(kLyK/s wxv%Q?RUYrwC񼞙7_ސYcS5/kXwՂU+Y9{y;?ج2G99ᵮ*gdeZ-|kx٦kC*oVH;ސA4˷o"Bڿ'hV"^ #a[jOk#Bͫ{nK ;C !kТ5Q̏kHjӂEZs!,В[xC"HMЏkkEM&ܔhgx׶H j*dg239N{tI'ΙiY5d&ح$8H&EIN*#ǶL,/)-uIiX6MQ(QRQDqb*U" UBs޽o 𾯾zN\XeNUW:43Oџ34okE*>9?_Kp8Po\OS\~r{ůXr7>Z.?$Bq+vwkKO PGWT>?19yab|.;2rv yߏgx/OmMKs7 ח7'>UE9Fw]v laẵ]Rg9FNC?L3?}兟'>4/_я3P>?g5ˎ8Z6et =vJ \_ b<}G#Ox{sW~|w˙ߙ:[=;̢5l=c4@pbtZ?yOfOnL̯O? ?}iQP돾X'gģO_3O]?uo>Ư㍷[c+QVO[v3LnS׌V]jX,]ZZ^adiA׾Ye`yE;"TB-ۭ._\^[*M-_ym!en=tdï:3/=ЏCSK[-2u}dTb\,?_~ʥ咳/WfJ5_/ݼW2> Kgˡ 7T+^N>b璏>|dzb~y_sR#_wג/|GGϦrt }Ƹ*7"Gؖ«g_N>B䏞M>CScs˛ǒQ;`=ǼD]u|m|{g|:u~!'<'@,C y>b3/SO=LϤ2g8qzo u8$}Au>a1!`XMF/7>[)l/s֮>ƿ)#+9L>f]&}g_"W6dW`q[_*ծJ5$6cWc _:ckFjeq~|h;6 WVfphD+_3ZA WV w׌VпJ׌Vп>ƿfU?ƿf1>5|h;c|jF+g1O?US_3ZA*_3ZA WV w׌VпJ׌Vп>ƿfU?ƿfuxsHsmunr3?kqE+_%rS]X7 W:_lZb7a捿D+_%_moA+OyxtGI3Au}ϿVbm;?ergS?\*ѩC'|o5bj` \?ssD~jtz"ƨH-UCaxcПП;?-?-?-?-?-]][BP]%{PZ ׀5ݵ=7ɇ{~H[TWurkI#9.l5y:k#B=Xc*?]yր?c?2菿O O O O Ov?PZ"J@OdUY3Ya 7)N-UCv,c^plu{^H9c۴؅Ԙۤ!1η|q%K~x.+FwYIv\5\V~Ʊ<+WF WJ%lKUerw+cHkU6= hv3.: 7]8*7[1ztݚM.ZҲߕ䯻(Z7A.]/N%3N_$Ǫl%#$WH[.:@IMJ%( UT,SV^,E;+ lSKEѭd. /Q6r[b-.EK2ĒƓ)Ad8Q&Uc4=5Ʌ+1%{‹ ,~ k]etYCaD ́i1z.duffTJ j=O9Gs nX[0/^x܈\t,ͻn5|}|{Æo_t,[Iɸ=}A>f!"v:4fs.fg,qxzdž w>n6a)pn/s< ;N윪C:(^N8*;YB$Df=#Ϛ3z&S3=g f+ƙ)0팳-dű:M'wlXnվ|O޵!q$|X wdeֺ)d|F6 ^ua ׯ5顒 .teIɹ}fi+Ѵjjj3*)BwOs;"M[vL4G2oy©?EEy}lFf>ǭy3-lX< NQλ]:0B^H40U^yqQN@myC6yWɩcۻcD49xӤoK\WԤ a~qJFvvr(9>q wRk&W&<VSz:");I= n}̊1e >"6>kۑ\n|<7~XDžqaiɉVqu1N6|蛯Ȩ,WlFXvգ¤J:1jzAD_:`2W٭*ˁ1W OTGU'8"HwuĈ8b9㥌"mtCldd9QV6kH$x='Ѝ!]r&㙇d9omvXar]dM9k vPdW3yy C2Gu־4Bgtbөt:r lW ,&z}DA#Ҡ*BqR2E7cz"n:5vZr3KtJ՘T2uz 2$&) 6Ltn#:mFS=Dk4|eHBj$#A8Ae5:)ݓtܞt'~0 ̀>pn40p;Q=<ۀJٜ7]O 60GpO_o([]LFc8kAC~K 6 7 Ŝtw^oAAl2?()  `ӸI@A0n/  Ls 1$?Ĝw?AAAAAItI}JZW.vwSg!JL>wκ  FC[kT3c SH0Y#> ^< >``eG<@EůA85U&ROo ?#].@/ $$(~s@hI~  `L8r8ΙsL34:4ϥs0KnIqe43h:heYdAQ r@Ѭ04˫b?g|3~SOt,Xm˧/?8H*kemLsVbr|`<f9X)$ABMIOܑHA}KV.bh\1v]V~"W>j_q,JxQ.+ʊk-6P۪%m0{AѬ:PrXQK%7ιdV^%]-r,}J7DeRI]ߕtZZ/cU+PR-TZVㄌbxY pq`ii$mEUq$Dt(r[b-.EK2ĒƓ)Ad8Q&Uc4=5Ʌ+1% F9uY4j7B` n7펻пÓJiAr2121Xp,..S3,X _41Vv>5nCSù}D#v:4Hw)<7;7ņffUY4X6^euVSYm{w|ZD&^Y gh 6+;}lgub ff # ªl&jpgչ`8fT7Cȳ0c/Vyxt/9^ڌ &W( j^=BKӼ:/yaiG5L@"M:(Y $ atr ztˮ\V_Wr>Rk骲R ONM:0I6R61uS{`jƛ'vqkAr91r CT5'}57 9"rLϙNDdwf味cȪQəƭ\rf|=>fwE-gSزYe6y 7&CHzgD =JI:\'1z2'(t'Qe<䔯,]DY=u6##NsWGxcMRXN(c{V>ƈ4HrDUHۈ4"aF Q{DRS՝lZPHZ+`F)|#7gQ;y!7GmUdTѝt`ad2rx#ϠDQ (g2yJƪNRxxhondqH֡Ȫf0dH+2}}i m񧣇 fG*vJR)Kq CZExGҢ!VYZYE(= iQuCJR)Y&/]H4MIhJ9ci(p29 $/%cuٞ4*l4!f.-" LFWq*uhs'jt6S4')=Nޅ*|f ܝ1g+,Q=<ۀJylӹz`y0?2`x+|Bg/]LFc8kAC~K Sw߀sy8 AA$       Xs 1$?Ĝw?AAAAӈ-?3Fҋ __*uvO,[d|3yOݯ̥/?4xs΁`<.IXy2+K/(={e_o?3Rzkh.LSWN8d?џ=(п`'KLvO-3Yd?{`O ?O5.1>}xY#^/?g32S0BFd<¤|2O,k8xmC@@ƁZ|Iv z/:/ j; O`k_X\͗N9'gN?U ƓoŜ/+s   UqSLVKeL Mi =甇co9ɓ&athGO9QV>8yUGE>ʛ.֝9eȷ8yySZ8-3>h1eд#)oqhV=M-$ABMIOܑHA}K+Vk5&jW#*cyx}WcPIB33]"^qPNKM9ƶ*{Inc4jD7;wTR s.Yy+#WI׭iˡ!Kf-iYܔK/ Fd,{JLXd )TtE(Uz8!%sF~VkTd[^[f^^&Ɲ(^bX{IlhƨE7LA"ɎKKE3DѭԖN.d%={.Ţ;XNkRhSxqqaQb-JÂj-5]ws`Z z\a3';҂YpFQ9{(_DYZIq-9/_P_W wo 1s|Fr[/thLב>$fvVZͲ*Ƚk:4fg.hVn 6+;}lgubpz|g8 b[(8%XDA5 *z̊_P caHuS9 jhԏn+/6b>( qgCѤruL9 02C 36be$R3RUy ]UVOԡl#40e,c_6M2ktuJl}Qbdt!uJ|(,b'|}1*zHթqv"T;被#aF Q{DR~FlZPHZ+`]etF2/QV9qeosN:0J&xE=`F otcHWC&x+f;iwЌ=wF~=֡Ȫf0dH+2`C}i m񧣇 fޛ\!YR8N)˪.VYZYE(aR)7MGU )iKtd fv"Ѵ6% z)92uzrp#VJD Sxp09L:pɤQ{nmFS=D@m0iђL* Lqܮ Is褞*ݓtܞt'jM:zRF Wx ǀR\GӀ[GnCH*MMSf x,zQØ;ȀM|L!D 4=-v(i2}m m7#Gfvf?DOJ}~? )椻p@~$ `ID1gLAAAAAAA.$4$AcH9#        ֮G1[]~h(sYc6d7NlCk.߽qn}VK[.s,m_ksV3!f;|f9fvViΩr"ګnF;$Ә̺nY=Tb0feNnVtO؛E333)xW P33 "zsp,Xn3UVyxt/9^ڌ &/$| !Du3@jvQ /:'kwzZ6e4/qgW 0cf(/ad=jzZ=meW52tU)*żv*+?zfF&ybF(<05n%BʆiMYӔצSb:)=O@'7?=,UOlo Rnx0ԯ*J攋h[T/ٞSr٭!s9Iq*]UFWjx2#>w\&sN 0˪9}t"s+c ~3hzƐ~L@9CW2V͜w{"2ιIe>œŰCCU_mȰ <`zurG)|~h_!{~3rp4ofgOgR@?~5lp|3 q H1'ٍ  M&Ŝ3eAAAAAA ИA!$   VOw4y%q&۷vm7/|LrW__?ߵko3por&|<9YMȄJҟ+_sWe֘IL߱cceO9뮻.:'?I̶mD ;O}LffW̤g\ٔý{~{o}Ɲ;>ۻkqV޻ww]Ve'ۿe/!C{|p~LQHgdMcRl##gt9zֿ裏?^&{ww7gY+h'fKN[OFˇ{3~?v(sO}6Z ;S="w,|"kJR?ÿ?п*ޟ?mOߴn}޽ÞOn޼h;^]PyCԖsյ7Dwdy9N;b'm_M$!1I?>ޟ/_ʻ֕p|LiӦc~3]{ؕʄve/2S{e|&>[Lt CBG7w_yg{|!S}s΄Ϟ٩=Mk?:˞b"wŗo?__+qga!og|RaOQ1qCzCo,d2Ik/wx#+}}̋]6;_}uKǺ0ѣGŲOlI'￿*ՑBe;y2ȼifNf335`\/ 7y̛d| 棆\ߡe#%2Ge;~fǜ~ӴJQ&Gd F!>y'sgƥ{1,k gU8% M2?|pԜ9T_i9C9wWFjKVV.];g/fJp5?ND,ߕ GKl'-mӎ%c4΢ {K,)s_VT̗KmDIp.mVboa|kɌeKn̋%l`iiITJ%^>ғ[Ӓn8qih(Uc4ji)"ŅEl,8d?*K W "3 7Fd~¼Cm1/nny8o7w9r;ngEۜګnFvǘQq|fP`3 )TAVx Fñ`]? *R~GX)"|~󼜗?ӼIyan dAV[s )q~#2t05c2ktuJl}Q?ڈ|ttddTYmı7v"T;Hۈ4"ZÌ~1Sܹs!whϭd2Oưߌ#ㄒux123 ׆ ېsc!8(  Sa:r)e8DMGU )iKtd^с)9"2dH&%3&%iђL* Lqܮ IsʄFd>pc-jmc0m>"^EId9ߧ˜`tj!O892GLdy]n~YW4{a2˗4U2׈4H2p4[<"2_WWTdl|1s'YV(Taox߬cv'gߝ>ԔqCw. L7o1cǎ||h1 L(HOߟ}__5;^Orߏ W_;9|Hzg˳mW{/أ{jx>ظlmW~܆}law=-|yH|(փ>|gȂ\y問vEZ?Pw@Z@Z@Z@Z@Z@Z@Z@Z@Z@Z@Z@Z@Z@Z@Z@Z@Z@Z@Z@Z@Z@Z@Z@Z@Z@Z@Z@Z@Z@Z@Z@Z@Z@Z@Z@Z@Z@Z@Z@Z@Z@Z@Z@Z@Z@Z@Z@Z@Z@Z@Z@Z@Z@Z@Z@Z0u[0)L/LO^`Aܵ9q(յ($q[w۽QaJzLiiii{H-UCвI-UCiiiQ=| jjП:?_O==945O.8o)RK@ZԠ4 jjП:Bmk>j?-?-?-?-uĺ[-WAh Sp[>8o+mOn3mYl>j?PmV9ߺ~G-Y񤖪!hC@+?HUy+/qZ2{x, wtE-UCXdb-b.okWVJMTq_ O=nsqeO O O O O O C{+ZԬ?{4:5 ППv7AUcm;P.5 W?Λ z<\ r~r`_JX~.O΁`&.{f#ރUCSUtu 9 ?֫#n˞Iͫ W,iI݃rKoGa^xO/nx\kTbrgws;8dFjvk M?£t>7=5:5Af_> jiiiiiQý?rYzU^폵Qﳅ"X=ws1kԿr4G>SUq66ǿ;a׽a5:kv:.pZge4ЩXU':^9t(֮:R~!пbmAJ?wQӬZ=`}zݧW}` qПППППXX7U>?@jQ?cvmQO 8nhoTxTqA Т#Sp:͸hEo"ƉϽP_- ';A}_G9sCnhh_7侬{&ƉƋA\?-O]871>8= O O O O O O O O O O O O O"T EoeOƇqF=E^$  I+  ` > ʗI{ Z9}PLJVc['Sh;` m|L)u¾u 6rk&*̔[ J?|P.iTp_|'|t›dhu^Uo\|ARV( y+`e=m:L}q3׿:-Up:5"K{`疫jl#aҋ[I*W`_,Rg4ʼn|}Wϐ՜5Wja'%w!F2Fj;llUVy܁׼g!+gU=:OW^6k}Os {*>;9u}O~{s.ПLg!x2 >wb)ӷ?K{^-Cz/=G=R|O/?ι"ǯ/䕙27#OZoji|&2F37AF?΄Oͦd$ ƐӍc>0{ei    s>ODʝП[?x'c";пnLAOп*s?΄?΄?΄U7pjiiiiiiiq(5*YXáFf0 пVHoS2h MpPɛp W8t1 п&^6~׀ݥhWTпo1^F_?-?-?-?-?-?- O O O O O O O _ z{ݽ4L-,==/(2R;%#+)KҮ5п􉝩 ߨW> ^)>s4 #}c7BSiiiiiiiE" B"8@Z@Z@Z@Z@Z@j0= Ɩ]][mu8K8ڬk۞Si8{6 .<&3кzbJިڼ۰w [N AAA: ICAAx         Xb"{Akc߃[BgBgBgBgRjdK$${   Ɛ@AAAAAAAAH}53  LtL[ '2Gȼi3{ZUjfNwdy32?"D#sd̑92G'̑92Gy̛d|T/ d^? 7ȼ@2o>y̛d| 7ȼ@2o>by:y]̛d| 7ȼ@G,2 ȼ@2o>y̛d^db{g?4oioӂOO O O O O O O O O O O O O O O OO O O O O O O O O O O O O O \sA2@ZT? V?8BgBgBgBLSZCL Yw*?3?3?3?3?3?3?3?3?3?3?3?3?3?3+/[ puc?GZ<&~0Ƽ&ljܩ ܹ6& $-|sԅsٓORKԠ?RKԠ?uhiiiiiiiiiiiiiiiiiiBյ(o ]6Jп?PaO,?+!mRΉuR5 6'6@.~u,pi!R5-X'yc$- {c$'vE%ԝTRR0-q|6o9[%n ou:ثwx BS}p[ ZӢgv$E8p|\.ʍ'O&;aą#ĐCG ^JBL y~Md@Ap{6AAAAAAAAAAAAř.<%ژ"ڒ"t|ekZG߹&o$96׸    q#LJaS_ ?-?-?-?-*?۝𯾓ӫ:C=}==}XPI[t.oo[!ï%v3&_z,Sjml?_{<='ONTW^SO5<>KŔZK&Yr%n=ZWRQhaB/ ,98Go$|6?c?M&g_U0}CV@>?P"L9 AUA|?Ӣw m~D qz>A{ܑ& )_G4=zҼ]j]&se}KU(; w bE YSGRRr(y?{=>{*2J/*^g35 'x_g[wmw{dJ57Jh ㅠR{8ߘ0_L.ןQ"L9 AAAAAAAAH1'GqAAAAAAAAAAAAř.<`,G`:qeٹ&qt':qHH'`cN;     $И+7@cN@A0$sRs@hI~       I1'GqAAAAAAAAAAAAř.`)R&9{vX+2-<}w} $$PI1'@AAAA`hI}1'  C9_9 4$AAAAAAAAp$            Lt@J^L)RL=P+w5[LHH'`cN;     $И+7@cN@A0$sRs@hI~       I1'GqA52{sgSo4/6z6Лl^lvuo}gɜ?ww?+X|tխg|?[',߸4+ї~Ŀ>ߺVBߑԥJߑ$dnLS++%Fr p[):X.rl^uo#_\\X\\X-LMG迖6B8g;3X&B̷13ooH(lIxfdž ;}2<|ύԿ6Fy wC0adosD|~:p6R&u#|ݧ ;7lzZ驱(k74Dyxe{mbOz&S䅱#q,Ė?8;;U7cÆ;NYF5?ev&kֺqffrjC4_>F3\qҰܾab;3yrGW=uQPxh:BZL,c0YG+~IYe'uprziR ϏOIE_|μ8=ύIF迖6B9d/>scF3\suo#??>6K6#IgOe?ߺ?2t|Qq|@M@,TC?Z j & 6~4&ə`T򁋥A 16}h()h7k)`g_dمۙٝ. _1<: ].AnEeֶ7o?}@ߗz>=k;6ֶxȭȟӷ3ӓ3wL/sٙDxZqV!C)/2&ZWn겙2fNYm]o2mhR:۔%[y;9 ʉp5Hn/E|OeȉȉȉȗB^(9y:ж[ }#ʂȉȉȉȉȉ<}MnRȉSՉȉ!{4̓]r|w[h^Q8号{K`bX˓g*ylXV S qݳ>LXfe]$/Ʋ.\ze\昇.+C+'Y c=d'ˍۋXHnsselŧcuA-lv,Sw6Xnyl\o:B;[A e5ϣmDVniesP'%dDCF\>} y_ϔޮ%sv+2*R XILؒP랗_`C)G½R2ğ<p)}+Jvқv?X\#m0+mmn'dWsv2Uw[aazwÄ99G }';@QWWs!|v?hHn4nиAqcH^m2$;/Sdx}`Mb   (EbD J( ="|~"HQ1@B@zvS!Haߝ;;&,ݝs}w9s{c[2ug~jDfn`,ɇ1 ^|c2< dngEwL5Ȧ/z(YWuZ+]>gd>gwy]yy[s[=t772֚=r2җ>>> >> >8u;OOOO3`+؍ c1Q/=^`o0|`E0 !Pp8| G1`8 yIN'p*8 3 WN48_烯oooo.??? ~ .Kerp \~W?׀k/\5 -ncMfGp nwqN7p; A&DxLc` x<SSi0 Ly;0ԁz0 B,πg, </U`5XւAw&`S0l^ ^^ lo[`fV6vN.n-x/l>>>> >> >>v;OOOO3`+ > 0| `/7P?"`8o[0p88 Gcp,8|N'+`$8 N p&8 gsk

    :&6wp!'./=}C#bp \.W+OUgp5_s p u__߀߂ F0n[mv'gp+w?=^p<`xLG}D4ߟOi`:fY`6:P`X,Xe`9XV kZY鼝'y9OhC#ߡ'WCOOtLZO]X9vTcK:k:OɗFR;&[u@ožcƉb4dm\]SEQ5uU=WՐ3 [.bJO+$^ HN hwA; @H%NѕUT#w'.g_NHt{|ҖB~YP^q﨟:^h].E8a_qiGO'v;ܒ20^\лh?anAaNA\O\>z*~eh_{_k_pl@_|&C<3~eո˿忰 ygd́_UN]5Yܸ&\7bB|+cOTZ8((cKJ_ڍBYurU:ūYN w}ńx+rvqS.Zzش12&^.񿮱{|^Ά[*E忬 K[Z"}th`ңˏqte),:K )W V>MY)hz[%N wr@%XT$8,INTV6E-IĂ._$)u! ܢ9:0makIRM,LϤ*",v|QE^ެLd``)vmߣE4Dʐʜ\wy45OKA[/_!#Fzrno|4:y'kL#7Hw2N#ɴcw;v~EN _G N hһ0CEZL+3M1ih"JjzfΥњ;zJ _Fs:C\r i&OGGGgj 24mVv{ƗQ #v\je#8C\L4FCϐܴw[n{q ̊$6Rqŋ'ScЛl\b .:ڱaxH~ջ|dCˉeNj\bZӻr+}[[8 Y-HFVgsMn% vsTl=F Qգ!hLB080*mNcđd%/ ӻ(YLS9'ssܿ&eom>@Ѳ|ɠ˳+/)d+qh5 %82svԜ<ȑK;e/!p@;PvW]yNb'J;Ws*ԡ'{ yt1=qj2ƳsG8Z~X4I{j~eT)Nz'#s_#}.컘{&+ړ8O]sD`;uNFƆb~S#x׉w227\h<:sv)ܿbVܞ+T{ÅT靌LK=q8Ωnlw0 N hwA; @zaH8Ϟy]W}^Z{B x\w @zqC:; @0%[#}a``J^ ,2V:.2(`,Pd"+ L9NFV/kL̓r@xeעf`SR6LXsz?١а{w"!ʟG deee;#G+ÑEH9<@ֻ ,!+p<wY,Kz'skLsi.0NAʎ SXbLv' r8w#qnNx>j/V}'s{kC](֝?/d\\C1H y"mÑuJ̾Ny 'XNzG@c[hz.qmYqdmƄwYs;tIdbēW{ޯHdnnw;v[NFFfkw22@ o;@hz]DݤwA @Hv@z';zwЗ $K<,[K01 TGoҖz?Ѝ++=H HcY,3̈Ai|K \˲*5͈M=)<,GJg)>O;3t aXHVJǰ ~ͨ"}P;]J,q;2*^beXPvn&Yӧ0vߖ8z/JG?K"ʱFi_l"P3Ո)~ TڀjBᒰ GrT/db};SSSxb\,{Ed~a/ uNo1ӈ303"z1lA뀂^,N 4ʿlĄ?^к_߼÷7u_踾Enu^ln`_X7{%&ޮ!vw*Y)b Hޕx`HzA"x l^[-:wN;z&3I-z$_-gz*]wv }z;Ďytw!j;zw,vox il\@hL؊fr[N=6iOMaHC2 l7(ΰɹ.(ilì7Vz_ڒ%ީϏay#|rG"/so?I+Mr'Lj BBS}8Lm!v<". (@0<1S.*+/+s,+ف=-^˯CKzzKm>> ^tӑ'_\ťhP-xH)'1Vz)9n0|z΅k|;\pa|:xJ8]#5]wu޿Q]aQ z7K`v|`ػr֡!5=➬M45j!Ft=fcއ^]oq':*}L4zFm>L3}`+-lufv/wf@f,XO"vUNcbVs;vP'U(iFK[rvGϴeB/Lʑ5srn$]/.8YcM, /l;o$ ٳ|V`t T#@\c:Xї' Ҥh+#x YOE,`NyȤħXs!B1ķy&[-^ R٭?? =yZ=w~zOX5mOx2Gs,.lݬ1o0;ԿfQ%zFҮc2u`MFj6K .r5l|6syOuرLg8b  |q/]!A9k(Qg}%g!3tЩK/\w@mso ͽ_m?!i;A CI0ȯp 8g bPK)4F%. Y\;_z Y-hywhV = TX!栺(DV,37nL[:@t "]d>> Nv)w(fM\1v ՛CCy'sjݧ`?@` KB)s 4>_naO3/c1ŎR]6(((璗팤w>0y '7zj|nf3 |x@Gn{;]XbA|9/ R߸O̻k)3ىMRf1,eYJ^H|G\Phvy+>~™L[3 oǽ7Qsã z9}[>; d |w % fy8 &XAͳqPwһ+ 'S9.ug}V4ޏ{I0|A)+A꿭v7ȿ X"dyl97tXE`1x< `XV9!ocp1\ .+O*3?` /9\ ~ :oowp_/^mrxhmDIg8ĘW+F/c;E>_ysk\77tls֦xg,yk}\b!L7c\>s ̴Aq=/`qI1", `/e:36c/<>1҄a;|&ʛάm1p?8.2Ç .趂㹕!2#\yQyԒCP&^L6τM*^#f4brMf\|DN30ƿARc o'1$XXTA'P(Jr"'؈9}as #$@r um7raI=REsmv'5%|;'w2`^OofމN)M`R:61\emǟmOϦfQ la30}M{ak+k#,tNzasCIǷ9=8S0x|K|7ǧ|飣,l<[~a.4(C:st`yeik6Ym.dSP3OGDh#P~YTuNt4gl)vM2M/^/ҕ^Wr]9_˓{oAQo[Q1؂fܗ81swv[]k`gLe1﹏9!g @qNY-Yb|շK,[aY_0.+Rd[Y_m*  gGkPbV7:^(ULOߠ!t:ftuإxuB_%P$8cwkE #TafG˂ELz:ɜ9_G^ŕ/Ƽ+.1}]rou/@|B2Y_Ryr"t~/&LoJ&r1) =9}BSS8LaF 0O [BP{(ǽ0fee_+b.rݻd, _c(ډ)]h2x|Bd1mL:nf)g|YH$.bLt/0EU?㮦H0)v,6N214 qh߁<iqx|8 SH)_9'xεSظX˙Åm* 2"*X-6a0ŽoE MfnHcqG*feI*pM7pߴC /9S>j"ϵoc+xms )Φȸݢ1u<2,!#"Ɔ@Seh+"H?בǺ r/90=*5U=p7&(no2FW(gAJP)Z_]P)U&n'W'WΨQkAk\ xPmoHW(_]kuhH Ql#:N^yjCbiO>ŬB|dŴ9r7_M "J1]Z!ntxC,)՜S1nRGʱ"PDGGLƚv3HB-:HQGߋ⯮Q5FW<S ?_[+U"AJP)ZşexP+Ak_S=!߅>2 }Wc>|.7Ŀsϼ#< }%o\3c_yQo>ǿ1Zyd ]pG݃φknV4n+-& Zԋn %Fİ\E p zenCA |APR1TV.x&".Tx-EԸ |#/jlB@PB PPyEJwk>n*&@ `8\J222222222222222222222222222͚7@5 @ h ZEPjiT @ ASPơ7@5 AP8_$@ @ddddddddddddddddddddddd76\V@l ?꤉iRJ,+|V{͞2n+ϭR]b>FE-LˬßYCdrV6߳TZ +c]gEwL'lV-&)'w~J|"*ؘR9Ɣb#FO`p :º_Mr! 1 K|BSY >Ɓ 8i?k-Wnyg >nT'=1qycFg3;&j9޳ݻ]4y)=+KV% >hZ'ߚ^;J=*r`5x "mnpL3/e|6}ȓ\t/ cWKJgC8L,\2pp܁]GBfgC{Ofh{gn>boA>81y͝/猂rsʙߪw=f++%iF!FvXnNi3Q!5:&6ky9 '';3ߝj}@:ts^{unᨮsZ_]}w-7~%m͹]ҹm^/>5wJx>(7j ŊaBdisV?tGwTo˥`59or&N;-uǙ缰Zen \ Գ.f#.F* {^`zޑ9qLzAv-wrOݤ@.qO~rs;֥YCM.qk,~ ;}>ݣMƀG~Nʝ;n\g}DǼ維 >Ǟx/o|뀿jr$ϧބ"1CֈgGw-{t;n߉Œn? !8qh7Nן T>D';139!-r47m5:֯ 򬺏t9 tnqwn^&n{݉=Hyk{Hv}3W'=u}v=Ⱦz->PAΚ89n7~ۡo;c;u?w3he%VV;eآQ9RC=-fKɮ uhVrO1!}~gGSP+Jw S,_q "-SfL+>Anv R v׬Ň K/i9 f]K[~;K4U{]i/>_M{Fj+%Uxׯ$e2 ׻h 뭐U}׎sIب݌L1S]wbb\)i1!!1iT$P.aAxLV>GDy"Eq(2om^(xN/+D:qA-FA֗m-څuuSf.]WmKCZL)V-A}GXU)V5 gzߋoYM8j%/A9- ={&/_^gK׬Y{Pg kD)'%xЊdEJշCYٙ3ܺ55KN(˿9E`_! Io,\#ʳVWQ}O_ׅjdP_/{_:e52>/oAҵp .0c?5ns?E' }\e2mJ=pv1V}w|]<ύZn1-$aō _L{iɂ ľV"=D5Ź0u7@^LT:C+ ‡F,gvꍺ 7ް75.W4aoSWkf.rx]`%E^by^(1b]&*x);p RvH۷}fw7o ՇFx]p٭sOCE -pxlԽ{[n >|&x\ReW_| Zu,CGn𞢍`(,/m7=M7<]{b^7~ڡ[Zp>:W4 ^ty6~.=s~s=Hsݫ][nҤo{1POn>|rg>|\y/_%K/_|jp{5kֻ | P>FMA\ |;wP#Ap'C?~IpgS.π-O,9p7>`?8~ l՜AWu7o~}?s/!Wap [wA? ?0pGGq8xIx x*xx:xx&xp <<<\^NJbR2r J*p lx_^ ^^oooooWMp xguzN.p 5hx_~n >|PaG>| Xq'O>| Tp[ig>|\y/_| ReW_| Zu7o "-`/Vmw|^};w?~QcOw?~ +Ysn/}~p'8~ 7Uko ~]{? Sg__7o߁ 3# < <<< ''')iY%Rl\pxx>x8 ^./W+KK++)j5u*F&fp5x+׀kip\7M=^Z/O[ /x?V >|(0#G >|,8'O >|*-43g >|.< /_ |)2v+W_ n|-:7o{fp|+6;w |/>;? ~(1'O;w? ~,9p7>`?8~   ~*57o ~.=? )3/_C_߃? a_  q  T4t L,?p)x6xx. <</+%eUx5_Z:zpxx#xx3x lk48׃ww&nplux_~n >|PaG>| Xq'O>| Tp[ig>|\y/_| ReW_| Zu7o "-`/Vmw|^};w?~QcOw?~ +Ysn/}~p'8~ 7Uko ~]{? Sg__7o߁ 3# < <<< ''')iY%Rl\pxx>x8 ^./W+KK++)j5u*F&fp5x+׀kip\7M=,xo>}>|5`!C>|h1cǁ>|d)6SmO>|l9s_|b%K/_|jp{5kׁ|67[o|n={?~ a#G?~ i3gρ_K^ e+W_~/m;wc'O?G߀\  #xx('_ ;x$(hXp'/p O  NgggKssey$x!\/////׀ׁׂ׃[`\opx n7w`kn pK^ |@A>|HQǀ>|DIۀO>|LY>|BE/_|JU׀;_|FM` .o|N]#Ap'C?~IpgS.π?~;~/{{{{_~M[>~~CG??Kpkpxx {p1xx0!?< <<+7?ccQ1p<<<7x"xx28  \.r"p   ^^ ^^oooooWMp xguzN.p ΂I--{xp+[>|p#G>|x 'On>|t3g>|| /_|rp;+W_|z 7=`|3| |v;w|~?~q'O? ~y `E _ ~u7o~}?s/!Wap [wA? ?0pGGq8xIx x*xx:xx&xp <<<\^NJbR2r J*p /x x-xx=  \ 65mZvpׁw; Fpx7x8 ln 7x[>|0 > |4ǃO> |2p > |6/_x?-߀x E[^ |7w?~0? ~ 4pWσ_w/%ppOp/p2po ~ o]\ZaMŨz0dQϟo{mq^u-5=fnlruY֙ssp9·{6m" Pޜ! ҠAD=@M' zNA5  j:AAt AQP AD=@M' zNA5  j:AAt Jӏ?h_: GAݦ Ut <30ٜ^r'{ҥǜyQga3=묉 .XnWR PH_|51/^vet "AMg`($@[o믿b*+.[֮ŕ^x,5 / _sruW8pnƫn*YǕ'{,5 / CWz ~uADPHК8mRa׽u\3KM'" WZi]kfA54UlAW#t bPHP~ 7|mU*e~圦sA30Ox!\:tND P J eZ: Ћ唦l,5 / ׮]='Jӥ+(M?gAxAMg`($(M|7ovW ZO[|NtB͵63Y׽u C\2KM' j:C!Ai:D|zzȺqx%Z/54}fz(8oMxһ],5 / hMڸ{j:C!z-\3uwL_?s]΍Æx= _OMǛcxhPќ^YUQP dJǩaPHAD_ *g\09Iu1dwhJX؊3z ['gz##:EX؜7N*Ǚn1:*BN#ׅ7k!AMg`($TEm3NA.G[ɀo> 3xpF@sʿ(^.vX5$2ׅiaPH5ntC*Dѱ>(+^*zF ЊqNgf $30z?V%f?­\sja7̧5PEMqiPMUmS{u8cr۩+-3PqÌwyNNk:*~IVs^XyY3|W/P8GtDѕ]Wuň^彲e{4]%ս>Df4Pp^X#gi֛ lz IS$r% އQ%U-WgTlJݜm3'TN N)kL7]^yϚ*vk5<*ntZ d/lVNjs]}0M P鹡(ԓ,?ATYh3!KkQ캬jPA<ߚ9jFŲF.zj:Cy5 ԑIkHܨ t RGe3T3'P He 5 5 QM tj:APH: jz2[qU^G}CT]\J?zPH'r"w *8j j:C!.?҈( rN%;Ʊg& [&&gz#c:EmO[:ŴYwj~iן*IW k՗䏌"|ӹ%1L8T8a'C_0<2PH(ۍr> :޴=tDA%@Uu*-}"mv*lެb/_UN2Y񶵓í*a/]aȤ>w^!L8t_#cGz!c3D82PHF7\| ;t1gyg83F:k ֮[wճEkzsNaߝpjg&g$ @ Bv_mc3Nu]e5}c*=e?ld[ioL'~^p\&fsT`dItB4ky.+\*قϦٰPAjiSD)j/4ݰv0ƞmXFw8&I9ET \dDy@Mg`($[o믿b*+.[֮ŕ^xlqr&trz\NP ͠ރiڏɹgS5fݟRSuج4^B_c8W:KX<'~cdػ]tv J,xL!beܴ^hd9$j:C!Ai:|rnnCq ;[h?])Zo58 -Pih~&5crt n`m h)f\gKZ,-DX*ڲ UFt1:iuAXÍdC°߰'Cvi%QBP JV*;dÕkR~z!)/gңbPG<30Coh65Oa׽u\3[e f?sO@M/ #$q*Oj:C!AiVAoZ津K_%t:(Y5Ehz(se(<j:C!AiV)AWP~ dK4 AMg`($(Mn6@[oj9MA54}!跬] h)ׂݟtBttÕuLNWi˖R PHКz eߪk(M?gAxAMg`($(M|7ovӖ/54fsu@uo5kx[uFn%R PHP^zr^ɺ f5/ATtB7kS7ި>~uAg87KM' j:C!aӦ6}B{zorm/j3u9޼USo[.=1a_6IpaaڻWy/6D30T(Jcn,Rn qʍ=`ؽZrcSg_W;٦} RD4 FLMq:7!^*6-FFT28h3>$&W5FՖFr-I݃t RDnkSvr]Cѿom6{t #39wm?]iSEw;P:.S {WQ֮vr265#26/N j:A`(Zпys}8g Mpon(^_\?96:zw|j6|BQ*/T/3ɢ(1]j:Aزi ̬ 5 dпeY{tDӶAM'*j:AD2DAD=@M' zNA5  j:AAt AQP eP, *Jk,C_Ӌ .EfVoD;8qnVQ epHZU7AM}(yk}qK?-UYrY~w\{i|ˉC ާU֜w[zs^(GWj-{eß}jչMtr*rw| ӉbAM}(Ϝx;Qco\6qٱskr7:Ӻag8( Aom޴]wiKPkʣw~e'l*l\~Ҧ'Z8~dUGbwjxg\O8oSl.DAM}(CǗ N09 MpQ3Ko9Quh/zkԈcRq=}Rg=j}l`1 Uɍ2і 640N䉌4}tlNemik} 5=f%O:5 GTˠT9ϧߢf'aCcD|9ᆑ$^02T iZj6fW´Ekl sWA_YV C2MG-EN-?Ǵ|w8z'45> nh|ˉj{4>#] "d=Cc#zfXpbfIP+J{OM  5T'*Hӗsӊ3% yce^UOбF'(V3d̩׷h  u#:8ۻҔX*ʣ߸rf,?U fYMs||ˉ.G>IZ&JM'EW[/s4['ıp ~9mdݗ R7-߸sOo9A u`,8 ַvŽ!P `qoKAo1}Nhzc\qA*S1'n5}zj-S]"睴e-\%ߗ,mIyZЁsOo9AkT5^#'54eMW^p}X_ͲꀚN|s&]M~&Ä{WcY+WBy4}36T]u+ldȺƆNA$#W@M'Pkʣw]r=.t%w_5yp-e_q Aߡ\#Gڇh:zߖ[+/Pʾ%@rOkӗuFm, )~ݎ_, Pkʣ雚譯_~ƝL; ];9n%Zљ3k-'*iz2HTڇhz^9/Q xvc_4>i,lbwf Kc̅q/#-Յ#Fv'>=vBX3D1Q,͈hX*4NmJr/69L-OI}lmk('Aհ0{5.gZí8LBH^?TW (z# O'o4gx@]/q{%rݴLp d:r9Ͱ6Ңt Rg۲fnkw@5CՍz! M726 +݆Ht RĖgA`9-8/K}T Zok(yMRި4/5. gZԜgd?(dg!"j:AU薄|c@7nԒdc#:@Fc? y>ݾFkF zvF?]ii{ w"-P H5r1԰1*-TGg5t B،5~˦)FEM9q5 PQ,?XDAQP AD=@M' zNA5  j:AA$6%TLJ#߱bb %ja&'54t`H+ӪQ-t-;20N.ܮ³CTJ¹e(]M?/W^M]6FN LO Ve;ߑޤ$uXVVIޅJE;m,vZRj: wGU jpYA*]YCWcPg w>6j4O,tmKG5]WscCHMGbtl@P'Ѱ=ds/j:3rix[FC1];{]ˀ[*i$b/3 nZJ]MŰ騿ntjP6Gt$pVv5=cu SӓPis|MWTs<M7 Y݃?{p܁z9gdO@t[EjT \C=a[x"j\(F%޵m2x5 1rTvht] F)x j4Fk@꯬ԓԍUe:t%QgDUrx(sIu8AkHtaU +O:%l٨w9צw`؆N{V mnqMݬw[{ "׶u'[{ַDȺIݣ鞱w?–j:Q-_-F kunszŝ&wrw@yu5r2_ʤPM|+1Wtmʂ4@SI;;ƍ95rDATiz1oUSDϺ njg*ֽ HWe6Y NMĝ zIMKۈqurJAt5(ԙNQP AD=@M' zNA5  j:AAt AQP AD=@M' zNA5  j:AAt AQP AD=@M' zNA5  T4}#Bsl@߷\9rqŠ}?u:،S*q}@sxx0n\1q@f)ط3NIsm0r!R=I42ȈQƝEGAk9T:bo* "H%Zmd.gZӝO) mi:/cӗTIk/At :tZt}t -8I_I+m))Foc@c#y,5 ѹ;TȹnKػ1:-㔢٩-E*OjE*]u=T\85]KNѵ\ӥHtORIgΎWyt졫c~Kkz~=BAi-r٩z~3'u.ڝO7 vJ'c1 K w9 "#2VqCNČ{OPpJ \(ȱtmgغw-0[.AGmaB]9ٱ͐u7W,' j,ֽ;"r 0Ŭ39NAi}sF=7A+xyoi+r^_@M'*;rAQP AD=@M' zNA5  j:AAt AQP AD=@M' zNA5  j:AAt AQP  JLNADGYfAC.陲!łJժxj9SܭIe,9y TzK KSөI*]Sө):yB^PөV< I!wt/tjzRJWtjzN⤐;jtj:5=U+pj:5=E'BqR5݋8^|@Mg`````G300000#P tz{xU сP@Mg`````G300000#P tzj:C=BM?3 (-kzx ېCgo@ԾkA&AD^tCBMϡ 6`a5 C nCm$* Co #VA#Xz`hl/h4ۍ-]?m!U\9d_ܩR*V< ЯRt y8e0$8Ul jxe%)ᴢb%ʳ0;,*Eq@M/t5=L|:<%AMWgxD7W4>vl.lo &W2,1r&* #OVxo)H39.ttO677Z_94 tqK"a%Bn躃Cy.l%S[rmO?>9ܰKըa~lq>2*Hp $z&^{<qn_1"nP.gyHSM݀&5=+QfPgس֏bZB8Em *E2E=9~[a:|푕,n'EET4]z ;M;Ŝ1*z k˙hdijZMlYnRC4]qz8('DD1fXq:*NƏѶ0 `aa3ng8/pV^_eq<,*rHn;O:]i[X?ݎN?osY&KÎ~Mx֠ی]$7fFtۯ=d!|cX63yorP|N(|fyvsç4#,r@=(GeOw6 bcToMy<0*2Cn|GV^ۯ]LQ9t]G5Ac2b\d+kȄt Yʹ@MhݻtvR[eZ]MoF{\,GKU†z0 *rg: $,n7s{|zmPl '\9HMϺ 6EM'x ېCKuB3$HPˀnh@9p6,LЀPs(m XDEAM/P…P0^tCBMϡ 6`aE[ (-O/GtAӳ/m XDEAM/P…P0^tCBMϡ 6`aE4]n SI5*~CI7 $VߋdD%>E(sޞQTD7r-YũbFMR•TT6=+sgQ*șGjz%\iKiEUK ˽;#vtjrz&hMhtnf*ʡ~k ^ NnFe/uRμUɠ|raU]Sݘ4s#1衛.o'qn_1bF %-r&j45-ek̲3Gμ;j-öt(%O*^={}”WIyd=շm_Jvy2YTDLӵ΋ݷ4p6FeQoa l컜F푦:sϲrrӪPNS끳:)̰.aXșk+AQ9${CaR&64gxir]4ۉtrh˨ˉB,7]o)μ/<:nKأEm9}2:)Lʱ0/pSĩ͐ôn'EET)kIk9m.%m45土FsmbE;,^'Q?]+i~zNƏϧ+O0f8Cw9JI+Pt8.IG)Χ‰YyZi4]~;NYqb:vI:)Lc>ݳB.,smFZ˓ɢ"*׽QF#h UV;ۣy~LHWu J?Mmj׽{ `O+吲ݔ^1׽:)[,\v<׽Χf>DYM+j fI5 ֘C nCm@M'*,4Ss(m DE׹QuE7Jpj&QQB6Gjz%\  (e@74 Jpj&QQPˀnh@9p6,LЀPs(m XDEіb  J ])8Kpj&QQPˀnh@9p6,LЀPs(m XDEQM[$TF| P<eEj[=:/ν%W ]"ʀ2hzU*6tjz*%r:!SMϢ3=^Pө驔p֠f.ϧ( !~wkcKedӵĵ%?LQ45ӝkZv7p4R]rX]6MIǁKuE:d5Q ݷS,ÍfHskyy^cXVsCIM|ME39؆JHqkľ˙a2,vgR1T[U-ol~"hqP*Mw* (j”[ qQV^Ӻ]LQ9$t6zSYаUj}3;TguNI+Pt8.9Pʏ:]HYb2FKJcVlRizt(NE:7^ΫtYJ̧ Y.O&;V7Ac2b\J{ B }sQu{Ԝ׽7TݒqW&:t_8N +٪mYur 6jCd- gqqkkb3ŕ{ "/Y)T5 SS)voX)5H,4tjz*%-Ήre̅bQӉHDOpr#5=.܆ڀIT2jz%\  (>}[[WtCBMϡ 6`aajNM)nCyg]…P0^tCBMϡ 6`aE[ (-O/GtA@{ZQpEyk6DPˀnh@ :t鮅@ @M/)\R 7 ^nA$@!nH wTHa濋An H*\R N( ىJ3EY+Ke2 N{1Lol %ί4H^ Nj)]<SV.7q^Y=-Pjz>틧eSumgad K$陠1"vn.7#w6sWk !G)q{%``p=auNq#t7?ثĠqŽ+ ڋTN7uph 8spVIʜFr.k[Ds#rdL QYyFYhieM1o*11ƒEҞ6鈳q=Qۜ8-FWdl6W8f38LZɪz_OxJ[ɧ)@ GTFJӒqe/; 8'^Hƒ$20hF{kwJoatNZ v 'fęk+ mY(YvuhO9-/\ԁEž.7T E)IYs.VN#Н}gao$[Gid(tK_bzcd4m5RCʳNZjkz0ǝ~tH G9tΓD#5=2vkgG|dQyA[zp"9M7+%],iIdTGu**#sͅVg#)U=o<\QYyǁsvFFmMYrnIO> ǠdaQv.Ixde'a?=O5]hOkn*R;`л;upMW|3J)@/kMhtg_^Jip&9?%aU2;t9!j_qR=0d91y s줍 )f { <鹕|ꈯdZӒ~[[WtCRn@֡(Ow-HC\AQZ@#SpD :t鮅@ @M/)\R 7 ^nA$5 pI-܀C7xQZ h{e>h;GUO9wq{S,OwHZ~tcI5K&l3e3QyW_J!5}o=\a$ CZ9w-5=f'ù.bq&ڜntcZ'>`dA]?]% ^I6żIp,AlMG=sqb[i:wxױp^)o@cj3i9h'E[̯."xgXJg夦ZR]ۛlj7.{!Se]ȽnWuo綥"(Z[2ZpHZյr.̶J%t _,DX[YhB@ۓ84<0h(5tsdQtRII-m[.k=GC4^r<[]iaN[/q00r\^ܹCq6On+~綥^bT5Jۺ07qq=[gW)~w| #Fn_00B@t4m5RCʳNZjkz0ǝ~tH G9tΓD#5=2t;rHؓ5czE9=%ّ%yBŒ^|綥^b;QZߖld0MKqǷPtFtqGКF;A.cV[҆[Noqǣ:듑E ۹p&-㑕џtd?]WpFSe]GjGO1\g59.M LkMX#mg?=sj~Xhz_#~ߙ޽;IoOI@Xգ-5]Ndaj_lZ|̑f6&鿰ϯ."bX̧Gj:`ZqRR~Mϴք5Z9}99E?_ab- H_Ж87>dN5IR\4i`*R3} )2G{o4ƀ_ABM޵LZ i?{q/qډʻJ=RӧpR3OQ׽kS.*a^R k#w\ġA0FuAظQaQي]i D;aad/0RxEXuͰF5yo{̂PKniDZ" fc2'ubm**gZ͒mn{. f/Жx {RˍctwX_xq܎mjvBn>5+щ6^v dµݼ`5! ctKىLk(sb81e;g͹F'dL QYyFY 5qthS̛JLd bk:As[nȚ]fy3wBΤ5]+2hzZ54~]kOWg;e];tj~?wq4=Y#_#+xcX 8rߤ7$ Qt.'2z/~KPzR,٦yN}Li&;icBʞ { vʒqN%*|zAokŽAZ+tuȱT4}*|z5!#5_*i9Vv # 0qpe;#N'h$۲YCGLo ֙Aiomҵk53Mhi.zUg.Se:ZhY2׽k3|~( k@u}Qiɻʹ=RȠ\rIyZ!w"a{_i_i'I~~iK=xjJ4oOg\›*tկ:OŒ_nvӡy^ Gzآ) \Ӄ3^|p(M4pXFGb.(|zit^VKXgvOUWǗ,Lc>= }e䜲^uv<, uq΄fg7]MWIcV6$eUa>Xh{ ҷXͰ:t;Qr>H;׽8ϊ"wOU5*ol哸0Ց-^J8wxOX OJ<)N ֽggg7~z-ȕTXM+L*9sWLV^9޵uT呚u ')f2LMO 4p.P)$j+K>Y9q) i%5<&5=.܆ڀIT2jz%\  (e@74 Jpj&QQPˀnh@9p6,LЀPs(m XDEіb  J>ːMSvz<n3X!0]M/%0 (酃%0 ( 5;n=3{Զ{3W#Ho>WDv<8G@MJA& 3]̞jfZnPV]cd^J_Y]PӻĀfWn"fZچC߾2 ͧOkm^8]b@ 3 +],wa t~d蚃[2pDEяy6zp+'n05w'=e92օpW>ihSņu=6x~xWaͭ\b+Ξu^m<[dW¶'LWU~gJR18妣cuR?A܀;蔨DɘƉy҆wS/;]F;N.&c,Y$t]uK|MGFMtdMl8q7 ~SӝIA3Y|bをh:STHȜ5\chj~gPm0rS[?}ݮFjFJ$k$۲PƯ/8u8CO饑GfPGojҕ KS'Y'-]78=GC4^r<[]iaN5=, O[.yC_~1y8# Eif[dL2*a'D딫~թ|f&.NgJ }gao$[dNSU3\B@t4~2v[MTa򬓖ۡ. q'5ݾ2Ҁ:rIOttஃ]QNQNU/ʩa=' 2Ȫ:EMwf&.d0MO0J$k$[֛KԆw8Gog0limI7bV[)ӥ1({<~>i@XT`k g2Y=IKM_3?Ԍ}C:2f3α|_ӍSJ~̭L2*a'ĩ)NMM$+8M\̺V?]>AϜGӓ55"GgJ^D`d٤ժ!(sKCMmvP~ \|. 3@uxN}Lι'MlCjƬ:bv6#r>xN^c.IV&ٕ YRϔ04q1s:R1DG4mY(3g΢ߐtgzvt-ZL֮ۖkuc䙙Cj[v{G)~͆,Ԕw]aˤ堐CitU~WZcl? c-/Pg5堧Ҳ3Euj8U>SŒ,7YnaQي]iddL GeȂhVq"2ާmYD6 搴t]u}~z-sg;}-?Ǘ?Z#癎܀™<>S,ܜq3 ɬ;)V^4}*lSsoaﺩPKPӳ.bˍ^ dgT S8['ȓAQK!lHR5MMOrEfֵϢ$.mمSY'`h6x%3<n3X!0^8]b3X!0^8]b3X!0^8]b3X!0]Mg"tCRx 7XDEіf`  J /bDS} `+&QQPˀnh@ c0 (e@74 p IT2X`$**jq0q![ShlX7 8yS5+,Q+|Ul5 w`l߉0KL[!W+ oܰ8y/5 `(WeaLy[PtP+i|8n嗢&ňvÑ7߸~;;`; FڹO%l3S̯asEi$["msډ"C5?EwYšdtxoj';iTI㹇i\QxpY=߲a%둵Q_NŠa gj2#ضsN}ShWP%CeY>QX#v 68s؀v{G'jUAf^),O?a*v~Uแ|:BF*ʴY( t՘˗+7뾒n{D68ht3ShWbEH;xؐmn:tmIYxQ,(܎|kjL 3swu\QGqΦᚘ9A8Owkz>EH0sؐ:m'1FSL᯿o R*gaYBF*ܝie,Wj-9P~8bMT! <;"~znEl$a߀vtJꛧNF|CV Ⱥ2RQt>~zrEQZM׳'zzN>ߘט=z6_E^ Ի>VW9]ShM]4="r6JcviGӓ.|Ru5]wC2bS'oTL+f(N"GT \~8 jEu+$LFn ,t]&Gkwqڜ9Io˥qr%>&0sQu~oCujPRoT?=ϴY(JF$Y;wF%G;Oϳ<Bh+/ HBZe^SSiֻsGM1E a{_B8AAq4.ؔ ϋCVmHoP؅A >V"gPө2/l>V"gPө2-J@bDMS6+ڗ %Xꟁ@MܴN30dg߾k떌*,9vcn?;tMc- W5=930tfN]o< 睨7.۸X`9ܵKYwXKUPpGnZv¦ Nx'mZ>qrGA[wXKUPtZX!p &G9jfɱnk{&U8mMf{M?qƽ=7m<^E6mA( . p-eW:q:nY,R%/];;;98'A/ ωN^#}Ġq\/2ȴU2ibج98+PːpQNxEyԕ{1x:`[wȸ%bT??-l /VV#krr9!koHSm ֚"ɂ[sV<[:}8PVc=w}v-kiD~/ˮdv:#lK6hܵ=uTKvѢ=Uu+s؎O,PNN@)Dtb0PWZg z8.rF+ID{qfjJT|BPpi35^q?Nhzk}9fݒ9M[HMGSybki:u^ O{lmmk˶ZjKڪGծ _[PT<_\(.xS]]TI&x7;yW ;{&,: V Fe6ma=$]($fܭP5j-+|dE>D{/O~h_/\{AO[Ov~j6OkՇq\G}|yz9̻={!{{Z 1 6=f .ڰqKlw4,,&mz‹.ٹv)$\!6;O޻=RslRL:Ӻ obލ{1=6福16KNM0oΛ3;TuN) 09Yɠ.u:m:֙տ[\t5}#Κԩhq_W1ǎ~wvo['s;<_xEg(jto?~ooF/8f{Xq`t.as@g=s}6p{Fm0o|^#kcrmS$"6]`(vmzӧw}~0޺ⓟߊGJzj6o<+ǿzmgLgZvME}6!{ _Nhw,ܜ(Mo*i;eGxJRsrm:}Wq~k[-|Uk?-E;&M޴Inݩ5Љ'8O:>zٲ{WldI'])yy][gԞ}n֋)<ݫצWO Y/^TxWꦆ/u[;VѦ'Tomz2u7%)vmzK.DԵuk~֬Ch?UA^OTåzKKKHs^{7p;;;'L0z#<8OJвrRӈm:CB2X 6mzJM_ٳ]?S~stծ2?mZ~ʔ6]j_7⧟}رxOJЬj= OIm:CKw=pᱵ;ׯ ]-[ M݉mzc4Ul٦n]mz{{}13_YCi3'ˌﭭWhO;'b/_p]vb>Ҕ=T9}\hJ lpkzn\-ŐrwKJ~>]MS6m!p.AbaFŝڑ #z54bK:~ Z7ߦS+MO~gP0$q\zB/Kٶzimu'ilӳڦ QR D'$;\Am^jhIK;jf앴eȓ*)  (bm+TRy d.skئ׮MK."'TlsA^^JMm:tP]r%X!$$p"JQ諝+[l٦Mg6m:tlӕr驭UئMgc6mz kg6mz>l٦6H!tpK0_I5(ellئMߙtP1Ha$ծM/`IcmBI+%jľg^.`6mz\0I[p U8emz} bK)LmzC*lK٦MOT bK)LmzC*lK٦MOT bK)LmzC*)k_Cs?1]A#2_. eA]ks?υ|w|? E$b///'Ý)Tx||%< <__ _φooooomp ρx>_^ /ww%Rx|/^W>x ~~~^ FQx8$4 ,<o_ƒ ~~ ~kpn\_ ~5no ~3 ~7?0xOß?  4_ 2x߀G߄]x$  >!#O‡‡?GG? ''¿OOO7ç-p+||:<?w?ggg> Woy//'Kv  wSitxo*j?Lx| |-|< ÷·]p7< σo{^x||'^ /Kerx||?^5Z!ax7GMcS3fs~~ C6E%ex; Nx_~j5k~f-[o~n={?a#G{?i3ge+>W_  6H < ><>>>>|CG?> >>  OO   nO[V4tx<<n >-τςφρ 3|. __O/'×p|)||9<O++ U,Z:x6|=||#||3| oonx<½Nx! ^ /{ >~x% ^ ?kC#:^o7›''ggx <mKvx< .Xs~%*k»_~#;&[o~'.{ !xG=?$7)gŸ?"%W}_ m;wp=x}x4|| ||0?    ||4| <>> >> ixx |[  g\/_χ_O//R2rx2 OWW3Y5ulzF&f8 wx.<{Bx||7|^/}Jx~kGup?o7OOOOyx+<x.;]ހ~JUw_~FxwMo~N]AC{?IxoS?EK_<&?-w#&{(h@ `!?>> <>>>9 (hx,|,<>>>>%||2| +>n[ 5 >> >>#'_σ'||!<_ _ed< O3WWg³kk M-p 9\x|;χ ;;".nx1^ /+*x5Ï~x=? o7- Vx///0\w»` ν? Wï_ ~=7o ~;w ~?=?  8 Oß? </_  :/ xMx[߅GMQAC? )|(|3x |8||$sQ1XXx||<||"K$dWp3|*§ pk7o|&||6|GOs>OBx"|< n;Kp'/x <__ Ogτg[<|+|wsOBx"|< n;Kp'/x <__ Ogτg[<|+|ws18ΆV!>b f_nT0(%̹q@Fj01w^U3-+Z=,N=iuk o$ZHծ;t1Xڊo]짯2>Ժh YTmڕT_Y#ϴ"1C|{G:-VYA^$EY ;GXu_r髮ǫ?Q~[5]WFbs1gܛX<jd+PO+}Gȟɟ"28埡aтkvsCXJVOYea[6jCg_TMDuIi٩6x  QxMC 6a]KE1~0 ;g<.,g 9g 7K\O6oRLgzӖdi/A[Ҙi J%sd+۴Q>ig(ǧm2""NqP(Q%ř *Lr\H7A;XٷN+(%#ErcB<ܒ+2h+, ,ѕ^<1?YMm. O@P/ݴвTupOo"럊Py",1B{ 5_Ru ;9#͹]ki(KbfSsUp'`٢tf’Mu5;OԶ% x`wXַ)&H34il;mb%(|K3-A R>bue6g1mLo#յ؊;mga"( ߒD&9_.vEeMW[ Hlߒ "x}i5( @DJ/y,&6ȿ_' (UenZhYPZRNz7OE?bv O@<U\^h_`7@jhsԺOK!RF#[,)/٨"ϲȟS$ubo{2?9^~1D(??x6튪ewhCE_Ÿ6]89zbX'4I=Y +1!ai/OOOOOgOK)균1000000000000000000000000004NPq P2( %C;v&T-??ٳ&S +a8 ;R)Q(x?^ȟɟS)R}Uݠ< xpUo~@?M洍ȍ^F3f[eֹrc'z>V23?'TkNnq\nԪ0I1瞳sԞdo2_VŶⴞA#si<`-x>d ȾNyHtyNL@iH4Ocw4r_ًU$ ̾'iH6eBi4=u:*Tzn48z]1b}\iw9'qN8zBHq#Ŋ4Çeߨ:ܫDA P}u\Osȿ@?9d\﹡h5O@Ƴ<Ͼd<4m٨ g?>m95 =zRiAԞ/.g,Դ 'ycaaI~8׏g QcvЋ2b+;r/`fZy ?3Lp1 ;khfLcBm1q-/`ݫ>s&wJ8eFXOFQ?cigm٨ w =}gHqূN}dNAL07k_gi*^+O3",>:֙TۧjB1_55-FnR,*GmU!׎5 ڋVP),/H IxyurIB\ lHg_y2} x' Wg?2XR2VM0YO!2ԔkVK߀ŧ'GvtWv4Z͟Z_״֗ Ib]-̦IkFvs[x {'xj}ǚvwJKޜńH:SRa%]^S YU{?fX?n1}/ Z!>5BpQA^*? '-B=3oX&EgVO)Jyvה(GD ,'#"Ko2ڼ;+ea0zʆ7TLCʿޞgȿbki! .Vs)dX u(}s)i,8~73궶`wX:s{0ѣEzLLo[yHJ/I%ھGo[ $T/Ě9{J<P?F[uv/d cۤ DJOqnrM*}O =q ;S~c}%t+ˀX?AJSMBSǿX ]/v] igb*$x)UmCOOO=k2NȟɟɿfϞ:"[UOw.]}zyYbmm_߳ƻ=/r+@BEJ)*nxT\k\]YcAO?J>}rN`7/Y[pj]u8(?qv̒Z7 淯i {e$Q:Նd|O+~_3l -/@se t5y];u'`g{1rCgHLqo>CmpSsGYʦ-ƘZ v1# 7k{*:A(&11SMFa6xuibowj ]_rd'V{:9?֙g:H*:SS1E("O#B Ե u-',?˒Ԕk.; Wϒ/BZcLWhSKFv9x;Gs[eLVl僭4MZcuhE-fX Әik-oת꼮Ay6rMFaW/^-`wVmA#񷋺Y,_6JyoO:c6;}?ɿ¬sˍZĐy+%Ps01YL 3'ӧq s&͙hǝ6b&jf\.TOߑ^\ Z?gYO2P!=ZiȲȟ,',Rm٨ lgZUO7N!hsr=ms2&Y ΊJ׬u7ز 0NF=eЂMɈп=7dߕtHyf=Nv 잙"IM[ٵōPܧ8ʿE5( R\i۴ǧm22%qCI.`3KY*:Nyx'w/-Ğ?tOVeƩc95 (_R7*g3(_i4ި6?^3:N#׏WR*(WTT''dFc]I<Dž? usZ zjNn 2#87C g\O2PCQғ5?gYOYV&5嚻U^F<}3o@lQ9sO\uhY}dG@W9}R8?ԾOKrnVvr53Y{*-n.oaG9s9iI\#NdܮIfY]rhWxŬx3)a]oeG^]&z~cPͿZߎz|B<ʙ{#:W̅yz+q W/`]rݕBk׃]v3wuBpz'gY|@ƥdJOY+Rո-`zt ߝBnn%E>ǺMͭM])kW2?IzGږ3@" ;bq7/" 5V ]ր tzĵ%)p8NF:=Sy *^,8n 94I> L"{ƥ)/Y%#74/X|kOR`$LSU9Y/GTMR;1:͜6E1UL###"7?^FPgn-w|]-VP$!FFB64",Ud;EyU1gCp~CRͿePYJL;Ӧ DOOWJd?WϬMȟɟɿf/K.䢋. ?ٳ&vW>RM[>>u(_h 1#ڦMچ iVWT*ՍGGA^=B?zh/ bmg]y[[[;hĉP]ve㎋g?*{.7/ξ+O2%UVa:_~MƄ _8lԠ[YڜCi=mܨkwkn:ujgg'=#^(Y&тg_ [h7|*m韴[n:߬|֭N=pz'x" #7=?裏w2b >Bb?}uϿ.Xqr0?U,_qEzG  9]3ok`ֵzҙ=iߟI2ҥw~]ک<7ﳏvm5#Q> CqE ז?7i|!p޿L_M^=Df}ڙc^[`Y#?=uj3?y2~8qǎm{{[כs;Sϼ-gk ۻ[?;t7-]?v)?4֖3;=tLU fϾ{kv^?|o;IڔO>$e(o^&} EQEQ*uؖM°څZ9McX(27EwAC߼@1 ї7=MC :<2³;?Ntd0!o!Z}G>tgX4?m2N$ZBI,iҢELŌ6*~/!o]IsmDP^<;"j[ /dӖv}V)ǙY<:ɎZ˹b8c ջQk7YA,󡛎u F ƕrEO'D(-R(j\sw٫Mn("'+fѧY>Ҟ6mj_ /*]]欦I|{ucCߜJ*-3o纘\%oNH2%珨r]%qJ[RA:S"iҚ̕M@sE5kf^D >~bgV )V/wU3 ]O;N*3Œ+Qz>v㗗*3Œԫδ^zE#T4FG^Og%;rEmU\.exFDqO+\ƐQ(Jȸ(M` WnPg_|4WxwD[rBPz_`gwz݃"ǿZ%`-oƐ>~(nTXIrO(ibvF͠+b]Bu1%ʳ#6;s_Ⱦ6~Pmٝnג*/_Cj0BF  YpGt O.pY{*rӾh#˿ptGy1G` Px A+~鐘_%N"e#jӁ0h|5~fH 8$/oVdI +8 >bӾ#g@V P T?Lqa犩=untᛎu F ƕ{!'$-O-',N 5+@03kMM'Fc.IP \rE]tgMO>?8=sڌ'6m6lN;4D8{v~VmI ! ugϜUWO_ye~ڴ)"҆ؐgՓS"|1mF駵636gv?}YArjzW\G:7oח+GHLbx;AWZܷy}#4hҥ}ӎ"vv{s`ǒ"w {i1fvs=>75/)SdsI{С?}b_v'Ǐ[O'Mx``MW{^/2zuizꄫNO1c\s5'tR˭M T@ua%m+QPq*;.80#LR旼*-v ~b2BF!#Ly laV:P}dǗr2Aqe9zU" @rXa#L63¤!#d2#d+RUGr>PKaOx#,'1?,% JEX(ko U+SEX(zC).Oٔm!J+rҨYQEe?HgÿFyɿ~^d?5WO wW?I^ /AX@uo96w z$~CJ6_ F ߒW?I{uL%T ٫cߧ·d૟d *Lr\D` K/ H$W,%#`8Eo.bl:~7Jÿȿb]_ꫜ/}Q٩5@_$&F,uIw ū*:zS5r䉰^RW' A)zs䡪q hsԺODu_ <*',?ER7!'Sϟ8(??x6튪wdCe_Ÿ6]x_P5e;Is←-lT9mEjHy:U_O# MG)1 `RŨ c-O=?R° !k4Bb50ydlCҎHZi5;z}  [q% P_c]˿lԄ2o^ٰE~(&ᯋJݑm۶n+',a@HuD%z?& UT^4d??Eu% G4XS^QEe9Ej+>f>#Ҝ".RX"q'S|5_Ursҵꈿ>sDk/>mlb"Nvϩ֪6ɴ8-}xER} 5 p6V`ρʿkVǫG/} Htóy<{Ă.\~%v1m^ ^zqu:_Pn7R? (GD W> R^D3^#"׿yA?5kԕſpCI֛R~7jJ^'i?}FNIB-+9(/o^+Lmi_h`&.7*^F7/gN*pm-җ+$ /[_uW=uxyt/}uyԻ]11X{ )$ܵdW |σ=4_u"jG Q,y#-J/4|RjUPdw}$@n?O߸JL?2B)//-'?"W3pkZТII_sqJh7IaDEIɀ'WgI@׼df"^A[gD*,/ h?e?Q>&ks?WO!x )R^D3^!fUivMMLo>u_R)rߺС)epyԡ.s9P`QDD?rCiJ)]!#:,)#j_l: :IW=?+@d )gPJ 꼃C5J _yPƥVyWͯ@j 64 SFz?GO٠imic:=߳wTFoᷱqZE Ņ!-">F]W̠{,Z}%3x3]O)wၻ, зX>Q+3K~,&-=v з]xaO~tV_'C_bc!x/^\kZ_-VnHֹw ;::ę)ga!'qMZ%KyUYWiEU꒿pv_!Hz_|^#?)* 6;2.Ұ˿j~W_m WCĨWi{Np8 󅮂[ܝW焾OĞGȝЁ^Lc? (ACB_.A J (8ދR{}]#:H_,A?jBKzM{[S@Q*:G-{f#Y9G^׭//`G CZv"C|urX/JL4z|8!?[uBN_ =(nhְ7WbBNDd/M:Ґ+IRZCwT4А(@jCW> R^*/?Ӑe?gYOYV9n.FUϒ3)EYβk`3!vINp| Yu^2;ҤEG|Ҟ߀C< Li: or-!L8[);ꋿkGHoă?q D'w&iR'߅QOjJ5xgB'3[9L!ҲNE/C*M7:OTȸzEe?gY?dCq4ina+_ ۵p;S>7bPeq/׭;z\p5zWJ2tg>G_eIr;Œ{.!ȧ9h/g`]o8z\pʿ,wT>W?֝'|c=Z܉5bx*=Im9xY6q{UO“aByL`OFߪoVW~)_j%UO*T=᫼у ?"})R^2^S%',?˪v?J_˫?e}33aW?j'!?xSR_FJwŽVT\ es"ժARͿ^ހ9A5ɿYJyxOȟ,',R޴Qz DOBv$%'~׵{Fyš>/.*"TiQtR~@j8SUdw3_6A>*Ezo3eQIo(Ze]j.>b$+L `gT_ 7$8:B ge+ސ;*KOC+ȸT?"ϲ*_W͐cL??JsR??(sj~qC]9B72_KyxOȟ,',R% ɿ ?!߿B`#H/5ѐ,l(u͟ S 2.AEeV;2.(!-NH=iSu"廠*53އAg[rfb=J?c/.$D"{D7ʟ hs/yT@؝SXLHG/fT _eM,Q#S(şb^B4/JJEFԦaѢIL=Qb߱ ­hgCȇХS%9 biߑ>0w< x9%J?GK8qZ7WF\OEcȵdq}+'"SZNX_%?EQT|@ƥPEQuqC}\Ii)(n8A8A8*TJKy(((fʜ2jX}T˿Z?eNJvDg9V6¤"!v5`ڔ {P_DoZ_YF{ʳ,6(˕eFzx)t%&<*:⺒_"ljw4UVvXV&Fe+´6RGV[=F(#d2DV2BFav"d!#d0;&1000& nIZ(60'''fϚLUO^rT5xOUUOYEe?gYOYEe?gY[ y!FroyioK䟵2WsMTuo+uMW*)/|c_/Bh٤)]?\ǑO5_w8][tԓbw)Be)M_!ZcݜsqJQ:;tL!#V&X\BFNgJ޷RͬI4`HٯI'XFb(] N@ml! 0HW:ZSP)D/3*S q4bbRsY{-Y ܻEL$a+EAkK#nfHW֜_"ϲȟ,',?"ϲȟ,',?"ϲȟ,oIkl9ǽ@k^&㧜tQ 5i>n}xuNwAߜ6t&4ⴅah|fXXú(@Ζ{oqڨ[B',-.Qʿ@S\%ߘjo)?Kk~Pcs?K8ςijMIP/?ȟ(((((((Ry_L[JhyG'u>Xϧ` P-bʿXY՚e 6d|E5{+Fߧ` >>!+~@P.\o Y׊;'1@~~HY5Y{>+c ]<_oź_O}CU#y?J+n:.R%(("Swɟ]R̟Sjs3,vN))I)((((SV[)IFR^*R'(ܸsV{/ͻwhβnn[Rm75SN1?yOzQ}G@VZhp ߨ y k6Z֋?̉R|ؿœnc@ύ-gOp KjSwV s](mD4vC ˮE6P-c<[ _=<umw*q\[ h[u⧼1W!\Ei\Wʯ,ܸ|e;nY ǵvk^SAk0_3_W!\wPsW w,ͽO?j~aK7ݵ5~?fx䱗~aΒs ŸhmMA{1~ɿJ6SҋZݤRNQEQEQEՉmH44Ҙ;''o gMJu/Gփ_((((((((((((((((((((((((ʀ&55MZ<QWs-y {|Ճִ嚻?[*xTg.4=R,ȭ]Vmnm;kS)fJAM9_Ag/"y_^+?x,I_ՈS["Te&)l ӆo4U* oK%]p,'{{ZY+ȟ((4W5VRfTJ׌*O)Ovè^3*)eQɨ^3*kF% J5S,)((((((((("mIHLJY''ogMJcP=(((((((((((((((((((((((((J&55MZ闺4|OT6UkdGSYv4w[(:Z?Z̘f$Vlql=s|Y>ҕz_3:31難%o2 u53ʼn'/1zwX)+_!KH*NnvJJ+꒼pM¡TE׽`{3dMh?F5q,`p?xŭݑo"2o @i_hUZ]%JLq~[W 8KHdNiJdKQEQEQU^MUd?0ɘ[<ȿZynx1]'n&KKM9cb3"A z ڄ0ߛxW.˸:$Etw;}-㎢pc^#(y[0a]oAc$^ֳN--[ߎ9p-e#$FQt%w~1vTuMmhl%"QSc}@Izx 4ՠCPk{Ŭ&k 1ɮ)ތi]FC&  H]t.w붴3g9sfݻsܹmwvܙ3ߜ9sw~{δR2M#mdKgfOD*ZKIԿ- ƽt^/dB*m)Akd:hH//c[66666666666666666666666ƏL¨M6 <<9ҙU oL[5"h玿ɑ`V99JM9bTÿg߬{#Sm#9oq>5 $@\qWRTԧ^̸(13խ^ȿ?19SoԌ9 O<{Q^tJn=1S19m?8D4ǢiJEw/+R[ґkf/\OgϟV^v?j#h n¿yyiqqn~=wn'~_yk-7p__^n9($œ>s 'N?.QK/ܿ0,/--JSF=T&ֿhvš/+|!EC$|啃/ܿh6GpA=B7t ]\ /G]HBW(?_5jhC Nu/ N$<9=> JjE+g~~]{ dU`u#"/.Ǐ'bK_GMUnK.)S ϫM3B&[jb~|?YXp. oǹ/~hCތ|/>%_~7?q>-asqtDv 4Hd}PEp_a?[Uie H8Ky!ZhXj>#O?}v꥗ub bO. ZgNr٥sϟ?{9tyj\WOڟّWп?ϟa' b#)H۴z#݂??oɫRockߒ*w|92ߚ*okrW/O*5?-K$iIԭ*:?^?T(.?*cAS itu"is*>:|<? Я8qűZkI:z ]3#=jֽ)}gjKn4a(%[hG?,V%'etF&^htdI=YMTN%SPW ۍjaݜ6y8 ֑.Q fuN/o Cy\GF5q!Dbx+e~Ӭ' &,tc(;ĵ^]}LE+\\"Xղ9lB+Zgm:ck͵9CjsJ֚m&JQ%D(\,ՠOK%Oަ߬ge]n+^CO["o$cv-ә(–RZmkE2`葔j|[0"}KVҥ©]a+ԉ93(-;FWGcV95t2'bƮt}[Ia/eWÚ`V4'V= J] B͔VU G[ڟlmNBٱs5^6z"|}4SZϧlWt}m</:J-i]S5_j-Lj61ykpHWRm?TW_NZ_WxsfEG$Zi֝W#qf Z`nn˙.Uߕ_tNa2nhXW{*-_m`'35W:ɛ\a`52;?85GtX.6 F`/rړt'O[F67~r8z ͑C9CuaĪ7-ͩƘ] G*bIFȏkc0vq\*?3gxs GtsjE=bƪM⿓M$+b8z[QJ7 S|~qqH>$;.4]n1Ժ,fėGԩ mx{}1WUHF~iSdO;m>2:8@0I<Jx} `I@4*EEEAbDB0En `A1BEgn6J[/(F)VU0Im7gsB3gvvv,ɲdw7ϳ9s{;9ΞcW~]!,/c=Tie*~fL^j,ɺZ;A`nbY2aOW/Ԯ_gizwmϟyjnQ[߷Šp7A|^ߏIl{̏=̒ptgr|D>.u;q|ަZ߲d=,L#c++_0 pqS9O}/so7A^XCa]/Apxx/8ǀcq`(}` 2^'`$84a0θgq,p68 > "0\ .$.+Dp%+'| \ r57ςρσ/n7/| *`*n7[4p';.O| ~ ~?_}~7?/? %xo+?_GL=x O?yi ]:Pe`9XVU`5XZF,x<6flA0{Ae`0s J?ц 77A`px+x8  #{{}(p48 C p0N'C0 Fp:8gq,p68 > "0\ . 2p9| LWI*0\ >>S:Wzp,<-m~L[z QُF[[4^$oOE_t,m§~^ῸIRkRDS^_:ҡٿp$Nu {7@H|җs#ǻ7]»˻~w++;Ի7Ct.MVeKuz$Mzkɐ ^U}O=ܳ]"@ e74/+[ZA <0@P~ @\~ _ũjlM㹚nDcS8wtܙj/׻URzv!)w~5ת=IއB;/-Ĕc9Wfɑ^I,o!!z+)Fee)>S ?ďCJLu>_үw3E耕W%Tx2Bc"]䫨%z7Dy.) e'tWaUת_dPe?鑯; Q+kkQ]Y{eJ 0ea:db,9skk"++.qKɯ:Sɼ*`{p.5+Kɣ_L_EPϦ3&/׻!sUm-Pi2((JTm ǐ)=(PQSt~$LI{S)Yb)"LYld2&6qm~B.uuh\0ѬmJJjVmjD/Ya۲x $+*O T~fJ+ED<$**ﻤ_((.UuJuuI,yspm^BHTV׹͔.~- |mVVSgx!,Kn\S_Tչ [DDTe+)*pjclw_Lu:ZaվϓIEʖv.El)J(~DdgۅU%z7Dy654T{ꚜJʖz^Psr'$^BH JJ׹LElovђ3"lvXLuDޤ>UK<66JEڅ(64fDֈ$b6xõ,)09ЯE2u@&>L_ < QZW#\*Eʶmt˼wAQjLr qaqm~I Q"@Z^c*r_ߺ\W_B|ܹFoAٳUJB_үB禦ނsMM eU5;%z79]|+tBm}#(|-_ڣ}EwשJ/׻;yôvs3g#Gl&P@B|;VX"eo/׻EW+ p8'}P젝EڶG_/8J%<_sA%<_sq:Z?"##묵[&W.@$TA%< _sA%<uK x.HK x.NB>V n:y +r>:=XZ1.,uӞTYl+4.zD~mJdZdٜ*5RZ$HuFLaKisq\Bj'OB̵?CР t-V,Zs%,L;gDdZ>aΏd)٢,@Rh;d;;]w2POܪ_ׯnWbVͅdjd+J)iheXS:jUZ"hWg24w\o,zAREYĪEWtEʒD\ئvV)z?jUp-~wVw$Z}$҆e2vMh.Ak$, 1Sz8JZͷR!gWاrŁN޿w2vSc2vzG ]tV)):jpWHdNعzo_gWdWh3Y #sHZ22 #sHZ22 #s:\/e /  4~a e~ @\(_yj 湞k߷hY]"../@nW4@X~ υUtK xHǂK x.|A|M \"._/~72O/Y5үgy~/̣́~O*e)ꗌLCs~TVYb:p0e^a|m[P{&wPw;01"OY'/f@>bPo1v[:ЯZ^ϴ[= J=Dnﶶqdd[gݿ #s: 闌{@h~ oS^F%< _sA%<*z_[qj,`@H~ @\(ͼ2EH!"uX_ H` ? ;PTQn&'PÊYQX|Q8Ey,oP[f')UgUv3%ԯ HCK;ԏ#8}_{̗0ausXlvv&Xt˧f:?ҺՓVUsX<4.z>@s +ڢ5hm*#kkXw+erNoJ ~@(jmYݓc'z(0 ,C@ +ˎT wwݽB>f{#>*+_~gAw1,=`hm{- ;3̴uh/++;嶦A&h^rmRޣ\?Ƞ!3oyˤ _?1ٽ'\}٘=vxฏ;֯h|!^Vs ߎūy+Gqӿq枸{g*{V᰸!1k{G]~=~aϋ~_+( lOl^T6_pkb .(8&PY5q~}UPtt_!NJc17pKk$(,r-f[~ߊ4/HQbS| *4u@c6[{!YlexMrKYAn6柏;!~Gk@hV4<"?BO'7}h^i '( [n0reJ:+"JǞqG:f!U}f "gBVN n6{vݒ0:!>PS հѢ_к?lSݓ/,{|d! a d@D<.,\TD+  -1*)Bj=Yw+ěw7?Lo^(ׯ>N$z!"Hy$q{}Vd)k15̙\[E5YVv{&v ~3G J,N59Px0Qݴ%3VbW'[%`A3GO}/B>CzB_Ŀ;hv b  v 6%ae}1dJ9L w)b5,2fne潑R{flPl\?1HLiAGi""[ 3|0 CF8:Į50ha%~ 䗽+m[fiϾ`Ղ`S>QWqJ sN(~k \}`oC˥oc z m4ɢٿbB44m^i/2IBp秳6Ƭ;@$ lH8w >iaFrItbdյo4hC9}Gngo:v>eg\V:5n߂ U*0=% >fs4RQ:/֞pَ 1{q)EWCC:먝U Gbe2 Q;~-(b\θ`qtKWmu4!}(_~>{UyScq +=3!@pbA@>\o z%-22EK>"Hle{*Nn<OtkYQIK69/83٠-E7G .=Y׏DE~gBl~ -]\FNcL}hJl޶ 6yJl/4`gڼV_&n,Kxt:źX%|۴a1EwN/À1#E4j~;[mxD֯5:P īt`I̔fs씋6ԯKN1R%IރGnKV$B?yn"U5wFp~[~?Ͽu>=&/å 9 bBc!_ + /9Bwv~BՆ7WlZAgW 𤥺0I£Vԅ Ϳs~E ~9ƉT,G[ (.HX$,jЗwi4~_Yڻo|Bj>[WƹOf6L_UB?e>Q:\;`ۍm[k~M~:]"'Eb{/y!hP`SKw|PnT~sŚWWX+K6.~t:%SF-:x{G-CY/>oر~vN!҅ ԋ?Uo{ŷz<%l 6}8k!&z4# O:66l^W+ϔ=aN,\p8/U -]0pb޽ǎV翴Lw0~N>7bBWlcVi* T'H7 Sr- ,ݺB6.j[vtB .uq ,]跪/+dP)_!9#`)${P8m7.@K$?P@S"}5^ڝ']YeqEBeEP4 u/x6sVl N$Ћz20-dY!=B{ߓÿpW0 T8֫T7ϱ=cBp_h!#@nv_߻9 佐rէp!,t 'Y⹒B՗,횿˧8aWKur$%%_Y(EjmZش:Xv^Wcexϵ(d?7Fp"?Ko߁ -Vp|  Lw {_'S0  <_5_[x߃,0OI#π`XŠԃ%4`XV`X ր&< σMl[A.||? =`xx9 xx5 ^uFp x8Omx?8-k9^{&x'z%/Ȝ^~HgЃ}WbR7ؼZu=K/=}x**z4:7&+#GtlA|-.c K#6O̷`N9ͩ$X^sMK~1EmC^5l3߿#t;;:X >vP¢O2LiES0e;RP'¾a1E,_B9W \$7naO1bJ=h|Ě@QH䗔s EKW[`"tlėlJ)SZ$.Wɘ_AF. y: :B 0Y^{׵%檰&qz_΢Gf:f{Q4 4h<VŦ <8KղP0'X#|^o X 9DhfЬ.݁yR7 }$-#ۭl mbk|9#2YxRWǟ[6?oޡ?[SVk`leR-Ѧ'6P~m4H/cEJ#+/HjLjjݓk#0%,$0^;H2[XHKf}y0h76Gm/X eōlS͘8_\Z&;hQkZBjy?R|yTyЯE{`o[ךᠡXlq"%=2/*U}Tyϔ֠~o-11'0za{䥷[(Z-f+,v`OI#svv]QEy eSEQH>JKBLy/N&5#kZ)q8t (NFVDG׼Dy(&.z,$5 vj CC?P-=X.Z֫ᰢ/؛ܦkީ͝??)Xx_1ʗ"i,%t gK@9 Bڐ "TĪ/-.S}mrOw(Zϐ\F(ښ?),lSg0o,%CZtlŒty%D.ir0D<2%%R?ŎG1u^?Ϭlr)T'i:Q`C^F?%ʂߦL#|<7.fjͥDn+1 +ȇkBNL_qdڏ0*ldbSۣX\VZYYG:J%"%q5U&mەvuŴz6al$:^CGF:G.흩*hjs?jFn-],6v2Wƿ5/uv.5k}9ge6hۆH.lkߒG'!:W(Tؔ\m/3O/MS׶Xni\D\v\I|WU5ƿE_şQkkËS}=GP) G[\x]0OmA/ߗATS}B)5F('0u&o=ecWE.n|Sou2;߃_/|~9&^{{sw̶mNڼuy`jE%9/*''mgmvgG~/ͻ{(VXA܄"TPت^m8A׉{ly~:8 .7~*E hؿR8QiU~Ω*^U)͉, u:Gwꖿ ;ߌ1Q5{(__[S}/Co&k}!FO747A2(_şˠS}/?R}mx(__[PKeP`ssv=wD/_R Dl~)AK)!h^B汢{4j'\cE h2wzǼZ/Ĝ:# -!z!X|?Z<NJ@ ,4}Zw;EtbR <9 @ '08o222222222222222222222222222184@ B?_7H @)hǡ@}@ @ d@߾n @ PjWx9>(GAv||!wʯgJ ٸ@8)('p&]2Okvegж;_%!㻼V~D*N*n?jB…Z䫺2Uu_?M{ 5T!_5Uʯ[sg6ԙ*J߉5 rc;_7_ֵ}#Y,,-"b[X@2?64L5eBr~=w!N"gNAÉ",<@}}MMuia__u@÷RrDDlQ>햺uuUGw"nːmy2[#۲]w,[VUU|w"n5PWW2do؜m'C:-/ujk+*K35Cz2 ۖJJHYXrK(dt߉5,`2UY[Xxj<·3eˉǶK).=(PSSQQ/)v'%ǵb{D9bqR2]{Кr]Iqw9nX_;__:Dw2cBW5sU-g3 M J)Ql|'?E}]f76@m>;Bȫ"GI fwdgl$g @-S%kdO` H07Es^-f*+'R䂇`f1 '£/(#6#N/ r~yl &-gKgY˶+.tٲN8BpG|9.Iu`ce Myl?u7_)A)( kkZ޶_,~e^d|z񩹆5Jc+?P7ؤ7v{g9/Eϋ KO`iU;_ؼ5e/* s B3S斬O{4pS~xOlk/FƔ9*?_'6&>'V,Y5]"Jb /QϿ`ֽǧ}_? ?kES gTl~b8afaU~En$ݢy|\;iw~| vs[8"T/u&Y*E3ʟqmww;_0 *ϹۊbLa 1k俒 6Q3e!y0J[n.Rs{?s=denrЁ;`^ɬ]_:Vr]k~So*!Su8h-eX5ݸfKgVldfߩUs>F}irgd_'S~Eꜻq&+TccQge|<QIR] qf *6$z@9|zO/aE  u{iܰ:$yP1`2d_7~ B}a P5xa~ydIҴU1VW[[݇vsrG]cS{ f[R0+pEqtCt1SGև_߳`dfu< _*?n,+[>4G_1(sɷfNOt|~o΄١W挹,{Ͼwxh@{{9oَcW~G!>q{? *}}޻҇H6Ona ,\dORm*yި5wRMZ˿},QynDÁ TO1mj)""vPTfq ?%N@`t>Z t R{ ~UsOOwܜy7~J##^#5_[fǣwyٍ{ּk~~'4O_rTE׺]K%=!=W(N^X׺rd俶Fkk俶Fkk俶Fkk俶Fkk^iͯphn??kѹoS׼$8[~<~Ew=t3`΍uEQ\gWů:97ޓփ"99^ 97ސ{ݑqx=w$-cH_]3lzXwo% r`u +b!3~ΰ)9-CZ*}z2 *bIAIت}ط%.ƒYR8&ǷEXRL3܉k+Q>:kO M՚\Tpx.R~3T,>lU97(?LVlLx.~Ow UguS^zg[ޜc_y:;;_E&&~ddٷ>2{߃r^s}}*WM,;U]_g246Tm>Xs>T{\3ʯO ^>UT]F7 ]֖si~kȹEZq~7%_|C=W~kvp_%ƞd]l4F1 c>߯}L7ȼبu*OT.|tcܝ4tRݏ>$_:ڮ[?"OʯߨK Ƴ^Fܼr/#[X^Y/i*{UFc{h65ץ_޿fEl8cv`ޢu?yC9N-ȾJOSS~x|Wnny k>xgأAkXrTY>*1KiUGy*uCpktmM ]K̑|}"6o~҇\,1kpǘ;~:u}cGsu|6R [33q|+\-ʶboe_x{/}XDeI䷦/^ב! }eqE|M |:~>K_=wTټ>lu|R/Yk#K~~yޙUYJh¬ARt |"0?6,7!}*ǭӯXwoagӟgL߿yC6ˑZ_Oe L4J7ToPq^  __«ow^ O8 o7[6x;΄w»,8V:޹pp\ p\+=^~|>Gcq( 71Ip)3Yp39|6||.||>||!||1 n /[Wmkkp;z= w;]p7;|+{½p|  x0< x8< Gcp"<N$n8 O)4x:< {GGYI?S3s\x<^/WWS%Rx_-m]x9>^  __«ow^ O8 o7[6x;΄w»,8΁s<8. ".K2 W{p%\ 7|> 㰵\ ?hOOOO7τςss %epK|9||% n_ __ _7MpGw)}P;|+{½p|vx < 08G£x,;.x"< N'Sp < πg&^>~A!aQ1qxox6<~4 ,<<~χ/ Eb%eU5Tx :&^~ ~~~^????WŸig*+kx5-lk¿p:΀7›x+ 3.8 Άs\8· B.KR .wx/\ Wjx?|>^C^hRvJ$(dNiVR>^XUhc:2ߜ?Zecjd$eyWQz Qk)ۣPl׫gB{+f ^h֞ۼ ڶ@]o+ t=Jܩ¡_gsĶ1{5m?jή"21]k.Po- dj_>vW\o'N`+ l6(VQVϨHn3>Ƶ2NB fZf ح%BNXBf,!3BK!%Xڽr8{ȁÇC]KPweb, K>:;秔VWW8eb,9o>u}>?ma雾re%U2c 1u Yweoڕ۟~ˎo*eXB],,+/(+R^ʠ 9%s7Fr+*#XBEfN޽BqŞmhWNo/3cS^Y qFY#K}8Reb,2s*:Y]q=N7,!3#7`LS+{"Ш))ldrlʍhR\Cu29KݶK2g> {wuʪ17V͏U'6vw6XkTA׮|"_P{%]ʑ.m8F>8ˌ%Xd쩮2y-طIo`=DE ubpwŮbX{[ ] 6mR'hǸL/(x\Cƶǩ%ސٷxqӾccۤAN)/"ccc{6-i*]3ck/5[ oLb_Zx{Hc^6ErZ Y7mr)W]Wخʏ0c 19F:v?h^!ƂYeL_c gmB[𦢒o/3c#+CN!b'~ͧ'fo/3cAΜ_3끵3C] *ޯ2c !D?XBږ%AfRf,!耏5gEQ!/Bw&PK!%`B~g,B dήM~,!3BK!%`B~F0EQTPKQj~ ͢( (WX(Č(OX(Č(OX(Č(OX(Tcfvo2c)3(J?1c)3(J?1c)3(J?1c)3(J?՘;0c)3(J?1c)3(J?1c)3(J?1c)3(J?0c)3(J?1c)3(J?1c)3(J?1c)3(J?՘]R EQT@bRE'f,EQ~bRE'f,EQ~bRE'f,EQ~}0c)3(J?1c)3(J?1c)Nޚ !$5cO$mSQhbR. G+0Xz36eVPa,q&!,vp[RuچUxSx EfJťfMBU#£T(3}WSTxSx DƪaeTy!oNðlFaXق` >  5PN.&z {bɲM.L}QHE&*y4RSMqq( l3 N ^p)26jZZ9x)KJCl26 S^6ɬ=ӃVaD,k|ČjXc70nm͐& tn mvW %KՃj7] GYӁ^ǹ_y&i"1cl ~Vf]֗oS_èPShezhBdH%e_TvanO,qpyf5`/`Gᗱf{*% Ʀ:U{#8RSipu?Sɬ=||Pe]6Cۖ`0We} ijYZSY޻>ČVHd>MᗱYq%[ET*K՛£ * Č\eVPa f,({x 1c)GãTK(<Ւ 1S$2Ql2Vzgl{aѡ 9GٙTK(<ΌDXEQvf,$bR. 3c 3rQxK£[-: JgDz֣qXXr\;3fYm)+bH~03UJ-YsB&m:AkTsIbf,B7cSa ZvV.#}T;@-Y&l:Qf%@eǥ{z3sg}vGXѣv˂J*~uuYS?j-c;f`R*T26~@y;%=,5|򖱩YfyVnI?ɌRdl8 sdܶ:sX0SSMVJ?DR*BG'VJ6XJ71c)qƢ[rloߝ3rQ]LRA"f,(;3 1c)GٙTK(<ΌDXEQvf,$bR. ScR?m!D2-@~\~`NdveV0K² 0cIX=))Dg,UsO9Mܷ 3sHϛ%I} :(Y$6XyhHlꈶ9.mqo릎tc<OdX!v x6zXy]+c*2VNrV`H\*CƌEڣָ.;KԼD!cUs<6P[ ou1Ì ;v!adzsjO˶iⲅ8Lm,xlaڮ`P^vy&.Y;EpUu<@RYflҙV-uB5!w<N=gv_32eۂ4q9%vvg?V^i^(iXVVj\R7s  ԕ+'hQ飼2z;}dz"c7"ckmArgzBھUuh;0>kcg=QSW]i0cA{O\mAH B䅉ek0>K² 0cIX=۠n:[k9ufM=n7L[e#Ӹeu5ٽ0Fxz,lPt".UۗS!!3X2V I 1^='SRzݪUsLIx DKk=0&+dqK<jN}{\m\2/r343ǢjXܷNvh̦86n5BEUrՏku+W&{ƺ26G5%1˒zL$e~ޏu F:wk\۽*x2~fEX5(;_Zy@8 36dH?ےt}<\y`S:R%ötP1 t,rrJUl~,N+yb=CyMN|S{S.F RA \#71 yxc<> U.k/}gEvX9:wŏ~nPs*UorkN5j ya4q 5Fd#Cf)=/z4oXq5jm˹D&f!￸}ڭr8Fڂ`|\ٟ"k.SKSojqdnIԜ ~D#=^2c .*dd>ͦsRSFIH0GkfN{:eKƅ~7*Hgy؃ESF)~I;k%T+Nn]ˌ(J?1cE'f,3(ČeR3B f,!3BSƆ BHIׂK!u3BK!%`B~0c !D?{dH!kgڦrzu~oDAo^Ff^#o'^<5cuQ&PZkn#$$E^;FǰB^WTfy.6w?ņuTzEޤds~lY_k[ eE(Q(Ɗ %ՓQ. RH'GOB}捲.v{7\H+`:^T+$S맰+3bi2!80INqdrCw'7eR1^vxr]UG˼-嚱ݟh=48lMW$if8>efC)6dBH_k+b??wNnm?8{g\^v+rʓ4^Q&.+7"feֽf,Vfq^bz𳕄(.{lwozӺk޲wzJgy=75uS[6eI7oGMQ)ZŵiJ BH7@Nz{츢k>>놄me_3,u|n! ?_AӸSz4lq1z{Vf\AE{cU3͛>d~M[fn|ybʦW|hlC|+mcTfNV4,LJĸ/A}n26a+?M#d KqOaOq_B+1ék9-OlD|B;|!_nB[v]3(㨼[?~?P}Ba7%K,n=tSJN ÖCݕS"x)kʢQs.>8)J!K_!c_5-'FcV}%F|d²ч{$QYe Gg)[Cb0|8PbWoM !$`oU+{aϬsM,P;='vXi|Qǔѕ=%I-Z)3YtmXBlrcː<4$#d+fiU}4 Og\.K8e\2{]Mdfo ! +W@eL^ĂS N-TIE'uH*Vǜ3Y⦽O䫕EʀͼF|ڑ]-y"ZflQlK~hM^&5<ʖEJZ~#m ]>U#F -t OU㡧\Y\>hjgvM\VAqAH=*/ӵnω3ӊL+.@-7EgovTfl-U3R2vӟwe~͛T9lI[7oRsK%c_z-co~_u-!WS]1\haq.%,ڏ):vgU Y?@T p]!xCVP6rܾw ^4lFQb$m).ݼVE-?;3WZ`k{,2V~vK=41ꉍLT*^o}ʭgH%OI+Rr rv>Mʬ+t/&>K1rLEZVΉyzeER#l} ¤x̢! Lv/=~yky{N>ιQX0HTd,ꔱ}WbV=Pزq Ah_ \F+w#͵|ٲOA|4g1ob.۰%BH0H^}/BNPPq#wxhJG×ϽonW̾nHX/+XWves+ѽ dtkƾ4pIy'rRި/+XK[ܼ :r.m^!(7(3v%ɳK' 6[?+;."9CaG綋Uٲ#X8 cZ[w$-KÌ"KX窰^3W хAF  S*lbVM zϿel~Ǒy7 U_=8YmuAG%c6Jƾs-c?8[nwq+_=Ԗ%cJEIdb@ XeQųKR#<ӊ\Pϖ]E+;1k|~wk4B~; j3CƪXf,!$M,twϰf?Z2 V4hjaI}&ޙ-1(tk[8 };Wfۇ;D>29`e[1+31K }⃜!#Y=L/yD>|seS,DɄNJ)pGO+\G\'F֫z6@jcV$ۿe-fHOX`DqNf[AN7|,zgU;,)l] `>X<Y40IʶϾ׆~qs[%cđ@]l咱R5]2f$c@  r7R&-2h]9y,E *u\~]*t {Ezf0dC=YO݅HTm{fy[n !$Q[hr?:Que?ek~g26XWȀjV%o+|!M !D^˼Uo&Q }d-ꡐM*AʾX$0:XڀZ9dJCoY/?N!!A OOtwaMblߩN2z.7d{.9,K_r!0ǧD-r2d|I'+?%;/z#m?)a5xwɫ*0b6#c 讞٨" Wr_jʘY#\8]QaqyEN@H}$a@=3#MCd2?Fݔ~잱R Id3]OώQN_%J"ÿJ:q=_'[fC}_Wmg} >>n 7g-KKp+r J5|n op'3 wo÷=X' }p??<Cp<< N#;(x4< '$Nx<< Ox*O3= ~~~~~~~ ~? ρ?? ?? ??υ_Bx~ ~~~~ ?8^ /߂߆߁߅+Op9% ^  5ZG'g/o:x=o3&x3 o;Lx' ΂8΃..p WU>Ç(| >[`p!8n7c&?SSgM3fpsl\<|B"b| |)|n___ Wmv p{|#||3w]n-pwV {86A`x< px|<cDx 'wIp2<OSitx<6Ϗó'ó9gg ~^///ï¯K772[;r=}x!1 N??W__o`3^   ^ po[mvx wYp6yp>\Ep1\ep9{J WAo|>a ,q ?hOOOO7τςss %epK|9||% n_ __ _7MpGw[p,>p_!P8'Hx<qp|'<O'wdx <NL ???? ?? ??ς φO/Ex!^    /_߀߄ooG'JS8 ^ {x-#3?W7wxN7_Fxop&gp p\p \ n+*x_t`ӛ8jIYMg yw\$6yU l 7S?%9bsͭVۗͲg+?тGm-a8pޯ<4aW+w}T @ޢgbH#_)F+cJEbT202? ֲ7Xx汦Y¥OZ.ki"K ]+t{+r'j.ALll:3 Ww!ю$dN\1d{Ԅ2 &`_4Lpaqϱ3-@_u#8GNIVYI%r_`/H}I&T?}2nC Խ`*ȉi+l*'S<^Rse &!Bw /$r;j5e/H*_@wYנk :{a\cCH-_cJd% يn#pcbmcT_K7U\NߥJej6[ck(K-zB2DLJYآ7J>Sf7p܁Uǭ%f{%?yVVEΘ\NK,]҅ZB ԿSJsgK2F}5Pщ[?u"񀗹qkvꉵϱտ}FZBqfkKŠ2[1ZlY^e 'i4(r̚+øe_y^ɏJ֢{?>눽k]0K S)p[K(!߷H k, &~WQO;3`hGRKd%?ƾ{Ij( ?d5=eYZ%W>6 LcMQΫ-+)щ?4i|+kl:3sKf|Ō;.\Ꮳ7H>.|l{5%;ͅ~Kp=.pkVN+mQ޷PQNp.B]" ˃}S{y9@ܶJ;qk([V"o:cy մ޶*XT Z~_u9/6 f?([Y9m{[TMWcD 벇'*XS:qgc5 u*9-m^Uc=xxz\Tk>((e B!Ŋ1/e H!B!B!B!B!B!B!#`cE ? !dƴ@t\ؒa庮VG+Bwãъh-AGjdHX;!^@qtyڑKHG=eI>;G9#=G; ^A b W'Fp&1 @Ĩb$ ͋X#0gjBRB"߀tFVE?v~b$DDǟ k|<?B|`H0Ft ?/"mlXH 0ÿB!B_1Y(JK2z+B^(v|S&G&9X/$߹JUM&)a2 e [g2U\jП]pLMEN=S l]}?|(R屼J-}?ȫj8F_oBݴNcwHwizro?o?EQEQT7@#?@!B"_%b_B!B!B!B!B!B!BG t~B!Ȍi㉁tm0B!Ԓs: 8eD#m2u KpaIp$!xcܪa:6xyr;KN_#\Ye|BZ7ͩLc_C5>UQ玦wf,d2^'C&¯_WV~! ڪ 0'0R;IO;}Sl`ÙrnUJ>`¡NJ/V;Cۘ ʕ1&vlc 'z%d~uXNpg¸q FqG^_`[.00D}V{кgQ%wAmM7DE8ֵ^7m%}5sgߵHmߎ멬N֦YiZ/EJFTBd=wOÉE3 ?;cvy:a+ .=/0=[3Fu|%.{G T\{r}Q>=E=Y6ҧRJT]%$?81(90e`eǷ/^WPzضb|mҒ-* ,y|^\ZŒgnLi`Lqm'b6} ^=t`or mWpwߢCf +P-eŷ*ܢpw^rJ;NJ/ݬA4pd_ K?܋̹*NMR2Ѯ ]C[<}rkOKi`bqKk2re8fb#ӥ`b\S:Z7֢ )Z2~{7Wlk ~TxFiP_yaILMmKᤞETԾtՎ690-{;3ҋQHb1Q[=6rg3q m71]vө`cn,ݮhd№%C/W$K|*Ji`}{3[o6z2j{C@]5kz *E{o DDHhK Eo`m/LAl b @60x!b Wh\w uPG@ÿ;FJT | 각ND4Nq!각ND} K .?ܹs}Y7OL&ӌ3^~ T|aھ}{-w[JK-˖Xrs-YYӧx߶:YN \hm+VUUYmӦM{!:5ja||yڻ尿v[o-]iBk (cPrEEl[^%;۲sB6nݿ8_ eV\lqUxXRb:uiXHaO5,D.X'l6 [( @cLc!˖-+_[nE2V‚7ҰvQBk %) MاbXppxFL ٰa Y}ڡeU6qهamfLꌠ|82ωxBG ˣvv;2/9/ZdIkj+M<8o<H/9#OsSvy+6o8-,[FMZ8pda!4[8==]81U a^u _ywfϯ/u m8'MT &X74P@ԃo{150R>eư'@ѸF}@)A9l Ԣ]3d f6 @qNzLۭ͒+bw yչ/*:o"p)]~,2Ï߫u)NUgͳlwX oA G27’Z5a,XƊ7VboXcUJTcU\jm$TSb\fX4%XJe-CASj݆ -Q{ѬGuZ{U]tKS~o ~?fê{^gTC_SC5 Ŀw3NmlwJP  F7VboXcQCR7VboXcUݶn-z(fk#ODmd>?.o-A >NyVG/#̃k;FX]G"0Vbot?JD|X*t[[U"t\mEnVۊr[C-x^UWw/ٜОPmsR'o4~;V;ǿrnH]h}5!/twa#|Ej}}"r>\u _Mȑ4#kCX.pRWg9X?-*t[[U"t\mEnVۊrtm.p۳EQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQT0d+!B!X*1D ? !B!B!%[29EE<% B$,/MBʼem$!(Ϯ^8FZ's&5TVONtc+ O~S~O\JHzׯ_#JzTt=W8>MOEB/ +ٹd8W@gf$N#76\B1=-=MGS\I[; >t8ƆtWlrbgneZ:ݍ9meNMBqA]>{<|ea8[x!!0hr dW]LV`Ɵh M@!@49 M@-(PS2M@!HB`EOU w6n &Bhr@ hZ@ &GBW@yhr@ _ 'Bhr$ /Bhr@%_U~,%f;!0 N@ ፁ@(|>tL0a.*1x ssrX@ᳩcV@_YCgSOin:V 71S-2VY#1פV99q&(&=ToLzTs9;͎-1:kj́'bY_+?(?2?2?2?2?2?2?2e+GSo40eJE wnڞdϧ^rM,Kla> $LmyHǺk/ƒ( AAAAAA g$?6 A&h(Ra]`q =- V]WW?c>5``l|4 7t ?r>{dd[yowD屑890 j|2~j#?:6ү8:&&]pԻi-szDb>?at7ezI;zhr =(1^?^+=XS&e*r؃B ߥы+hs{qdq _j77H7ޱK$+ ߨ7B^qn>7$b'K?w{(cHd8UB:uu+<9֣/6={U+Q7ܾ/=C |u73U{> _fBsi+3@O"sᠿ?qBAAS37 džR>di[2̼ =L fGa]v5!}*1hVF$hr*]io ]W >ڳ-:ڷ0;R1lWsxlC͉#0&P` aq5?br:<XO51˕R;ˡh0thfNbr Ik&eUmfm>}59j̑|mfV14P`ґSl6u$y [5,AU@tD- 3<,(Xp͏_[ebYop}R=%v^oT_JTf_󁻵4_SP=RXDe<7:/ϊ j%mJɚ\ -'iOEK?M31߬-)aec%oێ1b ypx7NIG}Okal({c}vWޣ]61գ=chͺZmRYzckjc_nN:hoFy/RzQL]Eۼ8xev1w}װvps]^Yc.yz2?]Ϥ}ۢwm uyuҹ R-ip}+ZV$eV=v5Ujn =ïI}bgQ?BA?KgiQ2Z?F˲AJWY{DX^͓mpyhH=j_}=F%}(ru#>pu׍_7ٴg=S2v2?a%^x]`U~ D9=O =A ҤE)gV zA;uCS1lz!۝-d?/̾}o7춌?gZwASDO֘vF0V|Oj zf-x< :( zWBf]z4?Uy(-vyys:|F]uyZ>lZ3FX+v^+^mVdݕNt}gFq|LcO?5v.DZ4lq&FFB~S'O)3mdG,lO9`5w}{^7//ۀmKve ,U5`GZ:/ שo xx#  v=0v}x$7 &wp 8  aC )p$8  ǂ{T>p0 m`6'޹``h,  X G18x< Ox L<l[`"-x1x  lv;ׁׂ;.M-`Woc L`/  .p08 )p$8  ǂ{T>pN'S4p:Lp8|sGOOOOsgρσ//// Ebp \.W[*p56O]=p >!\~7__&;p3p =_GO/VW0nwNpY~h0,`1Xe`9XVUqb.}}')蜬5'sca&Q^68.y̹ _cmG_s$C=/'Fu]#b ^nN 4-[m9Vu-UjC WWR9Uy BDje! Qv7T=v.BٞN {e~Ξ;ߓ"Gx詂=Tzꫫn(zrj yCa$O =bJ) %w5Eu2#[}{-I]\F a+S Oڂ~~9O8y  =˶u׾o*>uy-y*z@.&eNhBxonhWnEu4\mKS:YҪ܀2Z{ޭy{Szyѣ 9RQ}tVAUHxJ r(Ko 3L݊=Փ FaV tT#0u+SZAAf) 3L Van`jnyJTCrPSF)bJW`ST,\Ce5"T[ adOY9NY<"IJ'Yֺ?)jHa5"`),>5Ocgy7}D#~ZSIs(_S& b7aU{&]cu,Is`%OESءEvZetb5DO[3<QHJGN )bA[@eٸJq( PzxT),/Lo8%&6𝖄222OnHu Zy0 ЊPF(D+ ju{ } w ?xN(ɏo݂Sr#s?qʷu[rA 4'T)#iz!;Nqgh!V#/~^uκ>f_\Q_IU멶"(3DT?|ZT=A,„Ό#(|T(4䃑@%;~(TyvjH5Q,]C)_,BUUX O@)|#0[Z'Ow 0L\Ncjr+S*i JF,=ݱa;JJؓ,i69~}߾} p-nѮ˖V-:neܱgW_uD!>`F" .t  Gv6ws=3,9qVBڼ'9d%;rrYY_{ͱ{y *(X[({4o78R8Y?nֿc^a)Sԩn)hѢWj`el^b,seo{W8-[{֯\ [a$Tu[%ŋ1X;"Gu5GU[G+[YYY ٶm[˶oxߺ5Ww:Ĩ`siӦթn.O]D" SOn.QX[L XH lիW7;7noltTToIc>!d I{QVUu5&/O~Qleן}09P͆*p! ÖKIPS~n"a"[,0ߟB|A">Xv0 Ĺ⭷ A=~! Öe˖Iw3| QW%%W柱;gn3f̈t! Ö˗{G|s+;̜93҅{ &쀳EV6҅Bܓ-y@)A[mA"y@)A[mA": !,(zUtmS OڂM S:Oa O( yB O( Tb+s1H9ғihQWO >z.%CJgS֡s~e}u~/&ϣX]$Jˢ)K M_oXV Ήl"z"-=ѥ׭ݟLUOOɭ}a܌ O7! )ϨdI}VbHOl薌%S$a1ɷSlɊ̊JlM$HK +/F^ˤe)[(y|uR.4Fʹ:l"U rI/ 1bLpՁ/ /-v0b?nV fQs+X]ڢYsVE)M*D{J []j#V::Ko A&ja -bx%Q~{JYf qo)&0>)IR!~ e$\Wc`3et~UOYM^]m Ƿ4)iA%USQ*ܛ>ҡ^:ׯJA8&G"4$U6%OQWOzkd+izUv`3F)%]C+M?G %O(MO{*raO=S)y<^~kwNu3, M^uqWl`|q;J`+1r))S08 +t?jнW„S sk[~/27>֮om{owf;2߾gܺ]2]b )1HP=%)]4|7n2 )GdwJtwuw\~XŃP<Ă=ݒ+^ O-i_,m%y?Z5L Mf'k %ުJyW%|)'ØxwU nNZ oa|K^3L`NϪGyixέczqo~q+y\A{ ;)j?~_q${|F7t>C'(Pux#[~Rvl [3MRb'Mpq8>iȱ Gp=HqlB@'YcgÞ.[Xe3|K(}42<ʄ~#f @I٧3@ַnHNױywCHIXkba /jzlJj㑳ϸ ][ԏ=JpO*;~> } $? O6x1 ;c+>tls{*5xS'Ryb-E '| X>iKM>l4CZ)0փC]צpOmid. ui=פn_;DJPuVRan\Nx 0ߓ~T>FS6x޻q$_qO:x呻]Z4pǦƋ=$O!:b-E L.s8 ^)ti&wL.>v)ӿs3垂c_^TVWb:LTQRS۔׻7ʹumԏ+x]>q|k+ՎM8;ϽO:/݅FS/5TIVz>8p>^;ILheo+szrX#cMG#6ZC;5+u^$adU۳VGjOQB8 I>Oòc 5n# ~D* JNO<*oU44p¾s# : +tpAہ,h7(ݝ;[va'.v[ہ{ Ł~Qx,8u Y綒O9~yGQ =һ[8zfq%XˣXN8S߷[¡9_hXo+qM:Dfņ(BQו?e3~#Q1+4c(W ɿK]:0]9e3&0N=ySYJᶒ{*+Y%O(_5S'e~)(P(9N$%yqkO BX"?X3 1';bV$Yu>%W3F'Z1x}p8Qzf#Zj)ɾ-: o(myfZrO.@'#-nOM~xʼO/4c+^=8֏ %7ݸrMvzIbVlS'0z<SX6zuX`N:sعVs.G>CbSs3_,?prA ߛuLލwJͽn/=C?vzr[ *=wyxG}qMX 3čW ?+<5tF_=l֋gUӋ`>{9mu(>Z}Rrf8HSb"Ob=5baełUO//&^2h /3rtpw.p+f_;a۶﮴= D)OOb$y>@8c#xDKޠC-^S0їWZ~lk& )G5zWE*(QL0- |f%%M12{"I%'xʺ})xJXIn(i5+R2(Cbb1bX`[O Oa 驇xXb|r!S @iE8>?/BRN7q=iq$uMtUVVEx LZ; |k8k 8^hDZ Ǣk{ X]]/$<\<[H~1Rɗ؊4Ķ^vgqW=8^/Dm7;t6)KKP@8'5W$O JK( d;?{'}t7EQ ܳ۩JHC !\~7__&;p3p =_GO/VW0nwNpY~h0,`1Xe`9XVU`5XkCax<O'Si2LuF`c`<l &gg?gg 6[-V`k|B xl^ ^^;WWWW׀k׃ `&fV+ {;D0 `??8C0p8 Gp,x8Lǃ$OESit0 g@ 08|+8$48|8> <"2;\.Kerp *:0\  Wo׀kOOugzp9% ok[ n7߃? n3movwp w=^p60s<0, "8ЏZyyE\Z"XL;(˩3\y9+nIkl]6Owj9?\ֺbUM?ZsЋbsKn7`ۅlXqqlt[ƺ1BCH ٛLm:TgW,ptzib9n{/Dm$S Mnj9 ]ky! O$'68 7`/j<8{T@,㯔(DbYa \FjÿHk9_/ H[26пYbVh|OI$ -Z #J溔Tli5/!ɕZ^*Ap.?I|'ٍǟQE7V N]a}Ezy߭vJ sIC &zȕ{Cf%?[v>_?߆s~TM&O:p2ayH&~D&d+!^do%%/e Zk}L+aħ Vn(nٽ5lqYW?{;%כ !:SϐZg?ďu+5E~*!yr+Kv4kWlP5~_Dog=)nשּK= BRQǛL9į>Żck|䍧paƏ9R÷cj7,R6W Uw|b&@nNK\ oBSCbWG%(6>[Ma&e;RY˫/O;rfb]r\w糕t_†Vg 6zɴ@SΣGK2_Bw+q|sJ2o|n7+Kk~2{­&ي _>Nq`i4X~sH3N%( _ODzPx7ބV`-8Mz˱;%WKݛ;+ǭMqޛ0ǟ\Sc ||vҦ w%қz=lZiUX$~FSNa޽jH, S)WψnȻ|s]!/u~r}+ =_ey{Lbp1N; 9f?.hI.^r?uQSlIqj57B4BG'_w0SZBXa+L&JsO"ZBwA621#.i[şq4Ms]Y:Ab߹ C/[aChzޖS*R&D? IXO2H?'c+2,bDKXAA^g議ZX^)g$t3~P^;B;m.*{5n_~JlW@e+͓Σ;! .^En{Z _\9Mq{ug"ČݙrS{ MMRAK.K1eʸSB#duUt?J:Ky$Kg)=⥴sl"D]XF8:MJҦ+tPS\W{zm"OEO7ʱ>ߓU^DWTy\xhGm ]RW?h>ڹ>#udŵ/=W9\RV^L_H:pk Rl"4HT/i W1{11AA1a ? IXO2BLԘbD5㏾ u?'c? IXO2e,'c? uןIO2H?AП?@ H?A ҟe_:&_[$B_;fG_,'c?k/gjQ ( I]ۭ16DOhXkSF"? ,z?b. Q55rwR'VZ-Keu)La KeF¹w<]-_BYnIaWsa{?v_˰"s;ECUl\w{{`0F2FP!n{aXk+ x7th {Wjn-&DYH繐R!4_p6\(ϑ汣Ks9]"B0Ky*v`|Կa7%BrA"g׻>!^;BJB15ȃi/>D^H=F q,d'廣єx*V)kkP+k6{'ڈNF,Twn?T$&fq M|9@ @  O#E t[8b;XY,FkhH+ jaoy^:Z-~4 TYK`Oj`l㥍AïdoW {Q~s9m<GE!t4ơ@ 5t@ @ @ @ +V,]tѢE ,xUϵX,f e"@:{ۏ>Z𠣪Q^()q^3g e",_<|(aٲetk׮qTV?cƌ ѽQ&tk֬wYjZק^2 9z>G/+㝿َA{L%K@y|!ƑǎD,.vL>=p!P}2/^;BB?[b0L6-@!W..ؾ\|v<Glqm۶m[:/}vSe" vDg/Gy=ؓ,p9wmok*e",\Lv0! {<'٣A?[867D `69ƞ{:o;&ut1e"jb'ٜIl6wǾ}wbKvAtoc t7P{'~rޙΝ?E ] GM}!$!1k<; aVn Qy¼5o0oF+[ ּa~HHd;*JߕԅU*T5cW^~= ֵkT>-Mj}ybaF}V#T ej-zwsG4i !pX+3|E6_٣S)n^ٿUy f冼b3j.9=EQkH}_rd\UuiU|o*5WS^bW*1ڽ!2k/ UUUj݅,$C8Hzr<wKidOo_uVR'(F0D_\(p.3ρ)am [=;e%.{c&NXW? +8ڽ$_^7/43NlAm?,Co4Xɵ'VǥbְοcjҕZ2))%.ѐ cY/-j|YJ+itO1F). @, gçVVYŽ{] @;-687~h N3ws^_kedPER ,x 9;Oʿ'7{ îxeV rv e^]v([,[Hۋ]V .y.O?Px_uQE.L\0۲S:os/(9g=ee?=W׾^ŏ4L?5?~}=v˨C ȿ?1g] -./@(/n*W-(_t鋳JM)~rqE*1p &jޖ7k7npڛZ2o!ʄ2f -[ײO#l)GMV0y`>5o-H?rS:ن]u(@/ϥ85vގsQJzmuT+S}oSsIߗO7y tE૳t}چyn^UDRkS6\*y xynKs|ՎW}|녮2*s`_ح|>r,1` ErE] $o9w)n22G\4}};viCyϟף툎: ``ۂjvtiźqUlQ;<ؿhYwgNgQJ )&o@}|zùn[6/󲝼*u ^A(hư´!& Uۇh`~wNesxe2[,a)H>o\IɅN)pE{{Zms]{LYt[ﶹ#oG; '.ԫhB7.%υIgl5Mx}+$Ǻ%lQuΐmP>NL,ثpBS;dH?qUqOh*@2C9;卹5\{{V0ւq7ӹhLG/FQI{R;ohI8^怃LPfI}mпRnqcvJ;}]n` 4F}ps)E' Om_niK 3emѷCF˷>n;ZyfwzFf?v}ه/e*})ع+߽欵N?wmunqs]gםڄuW7Ywe W}|9[,V)sqGXqQ((;S9LjT|X0 Ż HeHb;Í{]/owC/ !l.!Tfb6twO`լ^}e*4b0EoEL~Q^fG6*ʤ2LٖԾLǣ7.3 }T2͢eq~IeY[*Բ=7p{2]sHY.~ \fLL]C%Eó<0ppc-Woɟtc߾  RX;^VCIJV[.=w \׿W_]?u震M(p^ݎ]SLw}-'75ff:|zb-7CC4YaoZ=bN5^_JSTãswv.ͦjȎ?9s 2 jaHj_vP jNX _5CloPT9tc _ _ _ -^Yݟ{>(;'leYi^%᭿Z"T}r[TUŭ2ZjH8X՗.ӭj_W ("jHvQDpU. UooR a$H}7H}7=/ UoUCGc>W5HVw\ )'=A-_aRŷb7Ou9G[TPKaQT/P~LP-?>#%/?E_hOG5B8^ޠzt!U#CuYe(''H}7'a@uTAAAAAAAAAAAAAAAAAAA}7H}7H}#t O2_I?6A ҟeQ]ہYfLp8]-]JK]^e w/wk!*Kky2cuEɗ"8ʺwjN*'EC/Z\׮1oڻL_^B>w!y\|Y߽Bm2jUmkwۻϵWP皫5_w_A?ռ¼5o0oF+[su#G1 ¼5y[aޚü0oaV0o+[syxۣ~ `?U&ZSss?H'b No]?TX,L?c-B WG9o~8[!diMNjM{Ӆկu`6Qs\-M.Z:5<歹< \myk.¼5y[aޚȭcANtADü0oaV0o+[sy歹< \V>}3Wh%\`hͅѿg$o#ryevᛆ=I2'Kܮ/;+0Kg=_UqlfGK\Go2z_%}#&HE ]1_=(^h|f# :ᷡo0oaV0o+[sy歹< \Fn8th'FPjn@+oyt$hH]:oF*+6SsF 4_ }ߣyW|{H\ j*3Wz{=Bŕ֨x[jsO,s/穒GN*qb/^v?˓1@>"wsO̻Wm!0~WkC߹}*zzE`GK zV!_>CH?A ?:(.̷AG0gc״1ux@G~)Oޔ_1oڙ_N{ȏͩS'g 19Hh董GAGSÇk8(B t9t@GSymm:B+%տ8xBM55Hh_S+%՟2 4WUJ S9Hh_VZXZZqtw\;rdb {߬oa%Aq}_]P^v#+:z? IXܡb4~B{n;=f:_ =cB/Vmu ٢WqUJ !~Q}u*S}CTPAT$y:A(D> !O+d9H[Sme*~C&==;߰"fOyr74Z6w=d-̈'+m oh4(n3(וUdvѠD8roh43*-~C{YRҲ^5c$9I+vD׆6!}F 6$ǡCU%Fg]1sm-݊0kE"KZr .8kiHK\+ܭ(; (r rXd"<@;e);$%-)Uҿm}yJw.Otgf|) "K (W݊ J\Ʀ!"{-l#*&tޙ}N:l_N "WEX\8+!iLf)(/~6ʝgʳeŊۨ"%* NߋW?Xڋ%.zĥ۪w,Jt%n[L {C TU~:4M~!mOMA*!{Y^-_Ņᬭ+68]Nd[JJ ꯼߰~=!ML߻XK!{܋g.TV XbR`Yb?GU7P܅g:sfyE .tgZ&^wCwoQڣ]ˏƵpoof?'ϧfo ֹ'Zai-rUr3%ynD[dcݙuw}sHp89]3>6YD)LNMl/~gs\VĹ)OVMZ*XҒ׿Mi3;gmAV%44oyK_k,]/cKW]\q]Z0F=}Ol6F>Gޫ܆2`1|/[:>wj>:k^G4yƊҺ75wWwY~4Ͽ>⛗oi0ռp?_`$0)(x 9q ~ 'OSis K+0 L~~ ~~ς fY`6 E'g^/W<0  ^ &(oBo,o K #1| >/|bj`(le@ûTJPA u4m4fZAht` .v=(0`{` Pup8||| oC0p8|G1Xp8N')T]p8g9\}p8\.%`8.~2@&\Y*p5:0׃` np' >p?x< `"dG1# O'IS3x L߀߂߁߃g90䁙` 怹y#x 䃗/0_`x [,`1x,R>pe!| > %(_+*%`-XJzlfx@ AUԀZPAAh-v:Av=`7?^`o50C~`0|!P0 G#Qhp 8'Idp 8|Ng3Ylp8| Ebp FKe`$p .W+A \ Fkhp-cp#n7[6p; w{`<`$0)(x `*1 x <?tk7w,xy`&f9`.x^%2 x_??5:Xo( "6X K=`|>O39| W`9(+J k@ X ցRl&lPrP*A5ԁzFA hmtN`'@ /'M^`o50C~`0|!P0 G#Qhp 8'Idp 8|Ng3Ylp8| Ebp FKe`$p .W+A \ Fkhp-cp#n7[6p; w{`<`$0)(x `*1 x <?tk7w,xy`&f9`.x^%2 x_??5:Xo( "6X K=`|>O39| W`9(+J k@ X ցRl&lPrP*A5ԁzFA hmtN`'@ //{` Pup8||| oC0p8|G1Xp8N')T]p8g9\}p8\.%`8.~2@&\Y*p5:0׃` np' >p?x< `"dG1# O'IS3x L߀߂߁߃g90䁙` 怹y#x 䃗/0_`x [,`1x,R>pe!| > %(_+*%`-XJzlfx@ AUԀZPAAh-v:Av=`7@Q7_>`_0C@ M-p8 app8ǀcqxp8NSwi{tp8sy|p\.`\F!rpd`ׂ0\n7q&p3 n;.p7 ƃ x< & LA@.x<'OSis K+0 L~~ ~~ς fY`6 E'g^/W<0  ^ &(oBo,o K #1| >/|bj`(le`+(TjPjA `hMVA]`;vn `& | } _77A`p68 wHp~ǁ Dp8NLp8?Bp\}==qƂ F0n[mvp{xp<0dI`2S#Q'Tc$)x < ~~{,O 8dYxul/r㧾ރO{ 5vїqg2Bϑc+ تwu j#|tfp0EQE dw^9Z׼} 1Ҽ(V8*fY5o+O>(F//Qo덁<$M5b+Ÿ]slύ\9K54" tiyD:}j`r!vIWQ4I0pŮb uS&yܐu,(S/g~'̮nEQE ˣQEQ[;(*D(A;(*D(*67zkZ;thCuuwN(mЎwlilf{ӪXӒ>O?0xU ۛ]S]ԴO ==пaR}GGSv7K0t|tK]}}GPycZjHd{S]a[<8ѫOVMu}dfbCG'ۛVE]kHhۛn-RԽSfKYɖXH]6yZqhWuk;ۛVE]7>|Ba׍Q{R;\nV`h[u* MZ[r2M0~R?PTԼ~}y%ڵMT{gnQ߲:E]i5Fan[k+QqacyUbg{ w -^]pYMyo͚7ZּZ,{-,bpCS["EX8{mH_m4v>*_R(2{K6ǔgYMZ4 شWmm(57-AJޚּmk\ڪi9ų3{P 6UT^Q9h9jvNZfll/4-Cv$[{U/TdsS&fխp~wQWTb X<wlȃ)-J?фsd[ݒruFy w 1Y^16AQ|C_cF!FH$@JlDжmÎQꤜEyk&^h]6.{ސ2%kbkCL/.|G;L0߶jM`JI:l# W6ü_*/|2kN&J mKA1$ַnm3"~DJnIgj9l/.JSng{Ja0ihi-;'[{Aw9l/.J0 ww(ܾ1·~uxE;ަ0?ۛ ^]ԕV!;`d/6oŸѳ{]Bݻvut]_@aKˁeBh[{Oڴܽg={6.w]޴j/.t@~5[{1:O\;PS6%5uloZ~ a_~ǫJ4lrO劭rpm`чۇVUBloZݗ~GQ1$~ynQk.lnwjQEQY;(*D(A;(*D(Aa.`0 #j^|uEQE =)(*ZQEQ EQQEQ EQQEQ QEQN;(*D(A;(*D(A;(*D(A;(*D(A;(*~~NQE w +='~`0i<`0R'w HHMwJEMs@50|t&RkҢ<`0Iw5- )gϟϣի{%\u |x^-+ߓ%亽bwh`0XGb5e0yHHZ8H]%M˵nZ<6`0b;EN#/{:;7++_LМ򳲌-Z:}S`0Eπi¹ĕت^ϴ<ǩw߿sí0SS,L fi$< ;1wsj2i'jq`0$0/-[ݴy>*pI暓5VyNXf1 F"INz'.2~ 3 %k _k^Mw)`-wnט<3-p`0qzyN^$g}?';Yyg3`0Iwo`0i;`CD}@ H6_r`0);`CQEQit;(r"EQQEQ EQQEQT? DQE%Diw~`0;`C ~M,T%~`0^#J~_mD~`0^#b >Xu ײK`swӋ2 \Lc0 7Nsdl~]p= tCD6Q=,דJ'sqB Hr>XSb p=1~7Xp~%O>R^o+ 1"N8L^̔)ɋNb wjx 1P"n)lΕ'ىiO^3#8NgjY1?K`0V$ߕ4pi SNOowYO>uA;ჹf`0H$ƻ\]ٶ3^o.w<(~xJS>]rs➝V 1`".>7XrŕRw`0`0;`C Ac0 F:Dd~R`0R'(4TEQE9(J(t(J(tP](4D `0`0!w H1 #~קr  Z$~7*+ aUX(j|D2L/*٦m.hKd"e*-ʋ7G0 #.m+_ʆgIwCj҂/,2KӼ|1})YYY)< Adf0 F Enm3lNP~:Isf|kXnq?@J8}G #7T' wî>۸!P@wa0{خw7mބ،7~}!26dCT_`wy޾ ClI&"/GPћ}gIMmmR&_ݸ_&0{=f[|m1lQ-J%Ff{Bʷ(/[,7V@V_R gZ-(<\ݶ5hT(k[_{P]v֭ըVZ1p`=,e+a2SwJ˞L>#3U>sel|Vi\UT~=X}ː)&?{2s t؊*2+>ñ8ic zJqJʢ֯Cz^{zHCͷ<=^g>L$V|MF+ڊ }ъʋKȨ>1J^:7*Ґ<׍W"{CQ _~_ _T̷\[F-3΍uQi6u#T!w'^bџٟTQQXeWWUi} WVJMmo.j*.kt[EPapUg'.&,xώY]EiLZnžώľ_ q;_X3cȑP^mkՂ pX&,b/g;iu,kʛų&P8n1]f0Ds4B#`Za-zN_S\#kk\Q팑#F@YFj늞+Sj|ՊQ/(=78 oL1rbQ]ф3&Lkk]𦑡V0͂ڂq ZMb뇞rQP0Q3VQI~thO|Ԇ i.ms3|$zoz|=6NʠcM\F^acG߱dΫ3E.Δ %%[m/w@uuJ~BJnYS>Pfe)P[o̮6_kX-KMUfÉ] _5`ohXcrrX^i.y/[ A&p!eE݄8z,!f,_n*] Ȟ ՠ23*&/ܨsVnFVd+#.vurcdcwԷ^6UPҲg0##(]**S2˞ 7$CVP?WzzZqa=oRFPf(].%:Ai38\p2-eNsiAk|i󙙝]w;/tZcBdE:͹+Ųe-ö[æ֕4l?c}غX?RvKarG햣rەv;Kܾݼ=Rm߮*}tQjMaKInva_Fn_l +Y%#?oS-[, ;}%Zt+Ejr\Y ݾrvb?}5UiRR@@ZT![-TG0{=fpn׮]|?|ѣǟH ==B7+{sekO9MhXOбѓz|́uahal>{8L%ҩw?NnS#sg'}?{V&3k+Uݡ_NOrݻMG0UvU^e'SCeQ=u I=:a0{8rq=mdd6]ݦdeenf_V7¶۶ѻýlf(=HHKy H\ Hl((*%D(A;(*D(A;(*D(A;(*D(A;(*%c Q_]@QEQIEQEwEQT:~RmQ;(j ~GQEwEQT:(K ` `0!w H1 #~`0t`0!w H1 #~`0t`0!w-HJk`0anӥ)7\հu.RjVCW5hBĚV*~%Ң' ^UQ|w?JŸ;RjV^ ?H7(6?C~H a[y~'EQI.X.rV~GQT*~H A(*OrZpxݹ=\-u)x=ID|fV~qDym0 D\al^mC!TQ;ݹ(Jf3m9m|5Za'L'Zgu/ nV^CPilimojҵe{ ;ɣA;vn߱9INH`$Q<|]QLl{rÏwh 牶[s\/̛DwkvIõG?LhڞV go$yeC>rd؅jjTy qR 1 _THVg?z7ɊW|T\¼LlLG7~= FhQZ $Q.FrϕrV?ϫm__|^)_dV,PU<_۸tb=Mި|wOi2p~ƓDz^%#p2GUXݗ[sk ۪ mNe [k6W\v7Iã @oO)Wa5w --EM[kחW][Zҧ7^h5m*x&^x~H  ~acyU@7ި.x^h5\oN2pxƓDDUX]c[$~m5/#c[SJޚ52]^*P2lyj ]2.H)l]i9<,²HySO1jU6lTQd6<-,2Dce~SkDs53 gP* ʘ-BU̶6Sj8OwhNd/$ 0G u ѴebXx(>~HyL5ml]cJ#H)~]ZNٍ_Zָ(G=Nr [ɢ$ . SIm--U 6; zKCdQȤ3CȜ*FQ⠨؊zHɘ-Yo.SpN=R6zfC0Ҕ2]4eWNd',[(TWٶ/4O<Sa!Ҽ¦!Wִef61.O"RW]s1W56W UPŸU7W8, zK)mY&g)l{;/3l1IC۶r!{~U \Q'tWdB뗼D2k{|X_H3(N<_^$.Q:HjfI̦S̒*E%W՜iBFȜ19*JnlRUd6F'3CY,/2?'1`j8E22_,/9S^MOlY^,vS4.-vW=tɜ̶{yD!kϴ^3Wukm(TֵI#ko{7Z?}#cg9F-.ertɗۏh:1:IM0G*>Mh.GXEb2,{^&kbR 2l<Ա^o2t#øEb=v.j 3~aGWW4%Pohi-w6G ii{B(vg/zs 2HTc#8I^9z#L؇05u GU@0D/2}m'eW/Eݪʭ5gn]E۷7W7:R_-j_v/כn/gO0veN"%ۇC]%zVh9 OވVzy줡zR0W>rO劭_õGoZUY UlÆ'\q~t{ctZB tό #"m[juN-yOENœEQT}s7m>هr*wS%Go^!lNI(j`(!~SH \MQr7}zMQr%2^T~GQTʉ~(HxSyEQ)'],#uOwEw!$EQ)'b£#p2GUXnيnXJgV%U\Ո:xժuGQԀǫWcDKH\-[CEQ'W/)Wa5w!֢(ljnGH̑rV~EQTd;)Wa5wE%wZ[ ;ei^O~F`eyn,纽@n]hZ'?K^,B|8\.hԧOj8+ԉ`SV~mQJJ`1&X5?H>lNq|׃,-WP]z ÔeQ,TDx?BBMB]8+Nu˸1Av6'[ݲ}w;yҝ9KNEQ$;o­;L%0sCrn~Zayi01Td^/gJ(*ɕ<~Uny$"h;VD K s=SWZV:!'Bx=f>ߍ/&ҥ2}zѶ.Wi]1غt E",kZȆdQTpztSgZ!l. ⩤;1àQQ>3]-p^%¨fCt^rO!+F~7(0ZX.U 5, Dlk =RST(RMG~  U+!g L>Elpx}Xܿϗ%\`|xE!Tyme{"܎;?F31cTG8xL գEY@amٺ(~O%"51*03N˸~'28OުS/Zr۝PoXV^F?.zQT0N'C u YZ(*qJyC;7G1&û|~z=3Z'r)lTʕZ?EQiOV^FKH W+5-|Lˣ(*xPPwA8X~`ZQ~`rB >qGg('֏6y[+/gtX(qnfS#JV۱O!F`wW 7nܑ~^B5hk8O4+ζjb ϝoEQTbblL&"ٖ{?/RVZy'-(o{sW~GQT+~U32di-/d;y7ۖk4|wE%w\ڱ^w;ku $D&e 3JyRr\j(Jr>:I1S߹{~lQ xyij|OSs Sy5wE%5ϓx r=X3Ï1 Yas 8OT魷>WNʫA(*ɕwbUf^bwIjCj8O!>-ʶ2wE%w(UD _WLi31D5Їb^&m\.eLL 9;*/~GQT+:twHUݿ8Za*D5QUuxb1ţQEN^?K*TeqPcV)QaR[x0R/:OTeh鴊 XFʸO(*%~1r~n9;pQٚ/k.GZ+T@yZm,tQ~G(`>ljz-~GQT(~W1ֻ:P]F̧:2ey:*ڍ*ENUd2HGIdLUyES80]xiEsWL%bw"*XS$;y\i/dS[w`01T;E6p,y=Sw\˪3JT4LbWYkg:/~Gc0w\:{ϫ 1A^/UWz\ijN'-ztyU4)˧F َr0O\Ro_ ʿhz0oēFX:O %ڤ.';/bm]Һ{HT|>*#K㩪'JPOswZ[3"W;z FFjWջKcƘl;Xu)jy%c.#*lͫ%6~9sS01u:*JeiY=ҹDJ#=1L9)&#ԬMj.\ 0`D%2^Tw]ZZW8ܕ'gʋ=q5R\4yv)'"i5LYjzZnd^/6bj$JײU7¬k1#w)g)WaSm^-ў߉阜n0ri.3'$k<0X5ILrfg ʔGpx:mu>ͼ:Fۣw }@pۼZbs=3ͰG Sḍ;wx=GI $ټZ竘ޏ`;nhӿ3\|.ϫ0pUy]{vNH #KQ%#0nR>Rjt]GUX HTR>Rj4~s*jbJ{ #*~`$*w)g)Wa5w;#QB~'ߣd}r,㕟 א#pBt]Qr,(-wga'^rk}_XHg(*J S?"N[*P8&&jejp^ ~GQT+~2a~~*>^~桵($ʏOZnz<^nOI=+ʧE%)w֡^;s e@{>UX wN^4}FoX'r(Jr%Y?=;ߩ92 UaYC/v/emM,(Jrz~X1W~^nN9l91#aP{O6-<EQI}kNrv{l#3ϱO 6/牡Bvo<ƧYsw&|ՠQ"<6J ή~7^+/~GQT(~^-4]clė S׃28O{%Z\(+WZ~HZ6`?EQɬb*>DWTyEQ)'],#uOwEwס?Qr߅nk!GWTyEQ)(]UTy;;) hGKe$V rAermI29)^Vwİ@Lζ2m3?0D]"z=`xHGP(0ߩ"a9$ƅyM2E-AuY{mD yu/kZG+(8ChЊLCR<,**DE[:Y6}p6;hw֑v&.!{Ԙ^Ɍ+bE৽]%jMhABecZXVJ~]AEUQ6ehFӆK#})b]?S=g$gdn;iNwOObTte<1T'{zGW?L](cQEW-ڶ;{x8nh v?{4*ꡢӁ;^Cqy~u]Xu ~՚jҬ8@ɝ~ a,OVX̤Aϓ8Oړ!j"߫5ݢzP׽>k9qI>۪=Ro;tu>#OV՚`uu"~g{HUz3`Fݰ`sqkO׍2q^~(+WA ힲ|>R)'zElWm:5osuȹtks9opvg{9pS H;c/(^S:A1݋9"~p֫<3'I[k uSaNVnOE!,>QCYnwcթW}uEG\Zt\fIW|eWpXy嫎~siCQIwj\&CeZNU(3-OG 6l4l+}wۖ3xw}_1dڰ! W Y?Q"QD"zw1Mhp'7hh 6PTx%VV Co>iT97nwU*/ꂻνkNSfQ:@wN.J *o}=A^fL;Lzk@xS_6t}CAު|7_v]l>?*GDΆ%i1e۳$}qK;o"+s{o{bkgݨخlGQaD*;n՟a#9vwTp|t"%j#p|͹ԞvG݉#}ޒf7j:-w.>;~gk>R¦ʇ;0V;ayzjg;_zysP={ʐaϻY<S/w7.ޝrm|H:{W=Ѝx_ʐw8opEUwy- s-0#WמqWLꡗ{X+$뙛GhߥQMZV]Z8=wh`߬ͫ"%B;^Ht 1h0U _|rYMCD~w";恄-D=uX< o<@mEtP8ҨwUd=Tu1Çk?PsZrC=o^V tiXFПfq˫po׼O~HxSycnlǪ>wv|pM@QT%mj~Ͻ|ԄњqFO6 D'9#>rtTyڃ߿ٕ=A~H w]u&>ƅ.6l'i]msoh.J0.ڻ;zk'XZw 5'9k%ɚ` W2C ntN릦8jO5=JwwVܚ[{;T_0;n8Q854L6X^1dʗPҿ4+?sko̩wTG?Skqћ^`vwQxG~WqmuOy ְl]갫"̮νT U9WߺaCQT? %ei<]wӘV[wʵ姍)?}liWyH;]B"*F(m%mAܦ_x*pppL~B}5*5}5箅vyUSM b"[=:7 'GW]yȻ+/73/?zWu>;]Hw\հ?ge_zU !tX]zEQDcmYSj~2sfo{ mG~or ,+]]x>{+?ךGUXP~7RpM;3Diyhy7PєjvBY-{y?1˻~\{jnR3vr~*LNfa/_^]VG,jgr> \r& %K}=r|Ƒ&T"Wr\ֿXiYK.!a"K*jZ24 Vfj.ᾄ 웨̙3g<~qΜYyf}Ar v/ʔa6&4-؄PO FQnSZ\|w32屼t)4׿7eQ%'[)ɹ/E"s|yٺ+zO•A0R}JWEwG<=TyMtJx8tWw ͳíTE2/AaiӲL71g\fMyۉf ߡS#ό2MTʖb=߉RBFe{\ghTxTSjǬA12rQBKAۧ8L]|wq #>M{A8 #.N,Cm6HפKH=͞}WݿAXQdc3;z2)Ww,&{z|g{z7X;~i0ܟUۧ8o';NqoP徂?39Qo(5e`^eI;2r~Y~OCj6*]N(Eߙ^EYae?I}م&>+KTitxS&;鮌qs-"s% &8CRetY[{ʴ|dz܉RRUU!5VmJf; N``E !m1>~cyO1H 90oFd0R752ިb|Cp?2mnwMqQ !4,TA `SZ\QS@c49c )-hXԞEmfhOo=L#̛ %мYrz1 :_!7S- oi[?IQ tm*Q&)>.|9aV*oX@x <79)OEZs6+aTyGaA^y-tf Lr;_e~ [N9`k~pAؑw;r=>;'7NzLYgc:{ [Nx-ݭɕ$#YVނS;; W.J0qFQNjJ=;ϔSiݍoAvV򁑲U5Ķt:Op;tHl1~fݕ&ώ*!ZnNpN9? SXcrMl0,NLeI={|2uSk+Z;֨yi&V|-70c|1Ȱ#aWe{r&JD12#O|"UhrX+ZsDcw2T|(] λMH: Ԟ0q|@b)*Tj~]•/M"WOv`6a-zjބV +Σ0cN ?ld&L7n\HHѣk͛;w樓& ;0VHJJo,s?ng̙=s[rǺu9500} ,=&7o 2XwMhJ/A:Vn1?p&UBTaԖ+ZeXwY4o9QSJ8e{ǷҘ-WoYSfewI O"ȉm'PyYd|wNٳ^dUnT}C+RϺ|״ٯN}ZhKw__Z*F.Y">-:%ǿ۵]/RLNDΉ99bv+*LPgZYH'#UkċD7lMZ.G}:ų:1|ofلƯ/ߚ`Pi| MHHK|3Md;(wGuHG>9R1[Q|zLm(n+s6)Ӂ2SXwg, \!]+%}7AfgLg}?<ܨgGMȮ;JtsMwaaabj*'oŔ>9y=a;zV2kVrs}tW bRc&/. Ԛoq пZ1|!ؖI=yL;~'sCJvc߉sWFU| 1)2Hy$=N +En2l+QtlϔN7ǯ0w#f׮fOL&WR9;e4v.;Uճb%mٲ巭jHEcbŽVm#-Ϝ9"88Ս X4c_߾ժ$2Ky_޵o˛#XKX۾9fkc]#>roM|{$!_`N+5rЖw+uIʹz_?]XȜU|}kU|b*ӷ_J21oxS՛!]GӺ|}%mI,V.D-NySuTݴ]wVX٭_5 #W]*,#W`jOtܸq,uk֬Y~=w˖-4izQ00+ KpУ_W%")vjo5iM˗Mԝ(1RBw7ȇEn$UMjeB ߝi+X]w Ta$/_=|KYM˵]IԤ,DIwP%ݪ-B - Vm|GujoIhj ;PxKBU[(A; |GA8 GAP# wA#@ pA!ۙtDWgtMAWq"cj`CjKz^R^u;֞M()%x N!B0AX!A(ҼYH$^Ǚt1؆OX\ѿžBfF^!P pF(N**N d B$Q,k~  E?˽K=OKo1 t?kt$|S*A)φBAVP_ ^fdury~ƕGs_n7uKU' gP*e:d uTNh #Nt1NzPaVcnnNA*x4gVuwX{O˟~F-5j]-۟ |ӓ'?n]쟹Hygڿw\*CM\WQLY<%7AuUѼ{L׎ñǺ6}#Ot}u;p׸w=uunk?ZrzGڭEGϖ|;wHve!.YILAjA &r֝&S1%+Oϗ]7y!ٕTLT,/l%(# *M\ᥦ,\S}/?3^h9$AYMOοrܻdOw~/fxlw=Wfd|}s=g;X~?r6K.ʧۓKF8K't(O)z[A1[,{sS,e^Y'o^6U6O.OD\:/jicΎ5W7w3LeJGx*lYRYguvE/u JM&;6^L߲c:ezeZ`Vˀ<1 Wl~gm2\_9Fe3=X;QC0*Q,L(^*CN=]Ggzj;"U`sxttg6||~&]::}|W.%;`C 'u6̅a>7uropL,f#y[qM֡:VH+{y.SxQZ)|,'Xʘ}S^{^/rW-/G5}CTK3.䚿 [}8" *AW|h?zOkLL9>:]P+|r:&wt<%=/l{ڐڳ|oԭ2acL3mlr[ S-s|dDvXXxʵA|[3A@Lc^]mΧcCS {]wC.be+G}X3K`3KAm!ia tWK&7/t+$Y}ggHy탲f'Y{k+w:~=~ w Ć݉c8_ pqA4C7v/}|s-߬wWzݚStnwS/LExgcR}IuHSºf|A!6S}o`wUf1 ^K.gڸ)<|'cFW7TQ9oT3FsnFLj|^>rQVfW}UlLy@jv -'# [G m7$m@hCdg|fC鏿h&=Ӛyש^ehd7;fi]$;֝v4w9YcNߙeHQ\уM3U |ۆⶦ^HPU1^:UO zLof4)[PFsPhvqYrk38M~gjԦ=SIyǟn^wHve;|֗;ΝjxC_oHyOJ}')O/<Ν*}Uk݅>l x.C oD&=njRaPcn3QMf0w=3}<<9ƈNC J{_33%LLm0IzOH) 6wSpE2e]Ї9ld '~7vNr>|?g{9Cf~k|f_pWܽ/pU~&w':#񔗎/s r67$;el".yK5R4w"|7e^/-\uyβ}d;$Ӳeobϸ3kx/zo-@;؂M wG<=TyMtJx8&aXNyJMÃ#.N,Cm6HפKH$;wh-介9zfmA !1Kyo"yX[W޿Cv4{~ݽ|%x <ٙwr+|ۀ*aDAe;[<ÙӉ%p~ߙב= >w 9!4Lv<])b |[4EJAAԄጼa.g`Lu{ܐHHykgNw{1Iq3СIAɅ8q9H#ykz 췇4';RɎ )-hXԞE!ch!Haz\#r %7* #{eEq|ljxrRI=p7 `8|69| > "l[—˰| _aavuv]a7ݡ=`O }`oB_ppCap8p ߆0c88'p 8 =>?™#1g9) ΅/<% .7p1\e0For\WG Wßp7_a$ pwp ` wxFÃxGQx 0'aS/x&DxI<a SaL0flsȧa/+^E^߰2X+`%:C  ox'?x6{}1>#QlO|6O3Y|_-`K|Wk5lv=;N;.+߀;ް {7 ߂}`_ A? 8p( ` G 801p,w8Np2w48߇3C8~?l8~?sap>\_Åp.KR -\+J=Wj3\un&+n{>hx< x$Ox O0 a2L`*L0f, s`.̃",ex«k:RXa"n t `CoCwOx {>Ç(| 6'`S > ç,|>_/%| [W5ma;va'_avo@OzAoo>/p @8a0#` Gm C8;p'P8NSp߃! ?Yp6 ?\9΃_08._B~%p)\a.p%Op5k: \7pFp p wp Fp`4<ax` <c xx'<aLgY0 0t3ă90|xK2 UX5x K`),VB !tۡ;' =^xl C?a|>)lSi |>/a _+Ul l;ΰ 7'7{^M{÷`~@8C? 00h6 !p  8N`( ]8 N ΄,8΁Ogp.~/ap!\.0~ ~?U' pF #fnvn^0C0<c0< `<0&3,Ly S `:̀0 f `> %x^*,%rX+w@W6n6x;tw;ݰ  7|> `8|69| > "l[—˰| _aavuv]a7ݡ=`O }`oB_ppCap8p ߆0c88'p 8 =>?™#1g9) ΅/<% .7p1\e0For\WG Wßp7_a$ pwp ` wxFÃxGQx 0'aS/x&DxI<a SaL0flsȧa/+^E^߰2X+`%O@W6n6x;tw;ݰ  7|> `8|69| > "l[—˰| _aavuv]a7ݡ=`O }`oB_ppCap8p ߆0c88'p 8 =>?™3@G.4q9Olbf"F6"yHQ q]X*l &BC&a+O2tc{tEey V_sWuYy0,0Ry+iD5ǟ{?LM;6%~3>ۻg &%e #̴9Q ĿAc=rT9);!=21uO ccg]V^~7RcON#̎3'#Ǹĭ N&nMX?^(7ăW mUh #*1Tu&/KQ3L§Lr-T T;%NG U1ALv}DvOYgɸ$>zmIx+߮BOk>)lPĿGg]zǸ gY}U1|t}a;ۖ(,15ګ6l0S1}Tvǿ)`Dpi}T3o,et)ߡoc;ظMCſ"Sϥ8jٍJ,0bUŤۢ ^KK.¨uBwoӔ[>MǫturSy-|߭WAx?WehlG 7ɥemڔ}J¢wm3?S&o<֘E٥|blΚYf8i%xn]'k9¦Y}?>η=cp9pET_ LXƋd7^W๹\:i`[0(ofݘ-JFLq[/i}ukK.Ŧ}6NUL^3Y%|YφɽX]ۚlGI,I1g?î9 %/ml cc,>sK.۲u5f6UZc.V-clç tۡfg|HTd2u ukv uJ(sXğRvJGFz*/qw-t>ua#?teNtO\[ukQO˔*fPnYc#hј.u )k0tOu3_>(s+r~ɳ6]?rğ14wq+Խ{͟QC)ς%0QwFX ~$]g?E?.ESAd _ ?w1Ŀ8Lu C /K(0]ʯyGӭ\n*x.HaO!;:u9ܕ!3`7s0DPƿs, `>C^1#a; _>s_p9ջMOwd(GOwd(GOwd(·;:w\nF׮_ԆY⏊v`qThG m1zetOrmmcvnx|v7VCi+VWI-^[~by:#c1E^}_l#b1-ذva{@6  ~Qnҝ/߉ I|:6EuU9ql2n8SVś.NaWb`SA]cBٔc#/,s╍9 2nX:]gy `FT ?/M,ț7[24ʖD?FT `7yigmg.28ԣw)h&Ht46:fxElX Z!CⶥPjo)>fZ{eݹq.O]%ȿ/%߭Eb9ZԵkOkwB׶ԩ; u' şP~׭^J_k[P-߰EşP~'AĿP^}+lcOE|sjuu?şO*ڼ|pYT6ȎgS5$sGJT_b/]UmC(}rhIY>{1TlLlY5(g}W6Dc|Umv. mY۽_O}?P[(E^SUnr:%w4IUP^E0şOk*֣͹m)eenEo}ols|/_ YUTK m_YKYO]?#[my@#j+Waտ窧y?(_$t\\wyf>%. M Bh08ȩ$7֧oAG̓CAH.%ߜ}4k ¡THhS@-0PBhBK+ aq'hd2Gh}vS(L#4ĩˉ[sl;AAAAĭ; 1B>A/jC?"8     Z.tZBEˢPG MXz)!Z_9z$ d7qQ_qQOwd(GdşP5 (3[f8%ſal?\ ͡S?ߑS*比\(mWQ=([)oDŏ}}`5ҨLqqofx%I\ YӰ\?G% aϫ(_PQHi8mao0ĊF?f<>G|r BFE-'?**Z GSV+y̕ |b!lQ}J/x|R>g0"e\RaRDaG>J>|:P>&@# oGR1sbU)hۧ*B}W3:@ :.4 uMPcp~Ljx}J[.S 6GǿVoqPؐ]]sZG«Gq*V7EɆJ&KȻASP唞eR5§ފ%N⏃u-ς9ڽ,^j\.j)6pdaO~?*ȭǭ/+W{rtexcnw/`HvdL6,.v;b}RYCkWxu||=E?cK5WҀ|]Z,Zc~uUh|Ck Btef= (|\?k7U:Or5J/R.p{i)0^yl~N7S|fHbc 7SGt哱 ˲ ٍoOfG6?yAƳ>/ͮhh|: C ]E˓Gyl]4נklmt ^X^~W"{n ^1_4] -~2ilFh2iKi Ccy׺ꆓ4ȳ'Vg u(mS(qb.uS˼֗Ie퀌y`=COR+Uv%sdt|UǰFynnHE׹Aذb B]Iz邏MQ]9%LX-fѾ=n]?Lɩ;+.uX-r Q\a6eT~~2}1{~.>]qft04J3Y]ԭ{gGS TOx o۶p&52\))n}N%0"m~yng&ܮy.zR%GQbnͬJA~G^J22w) y,+I5;7528q bV ڠ2NtnJnpe-ONĀ{DJ>R?eO!P0_y:?PPsŗ1| **2DɑN7BXI1*l,e?0%)̭o]9] cG!xGPl d1'rkVm]6_v9:ߨ!Rlpb=Yli,R`-'HȆ]^i-M5v@=o)Rj ׇԦb9fJt裢-i.?NXC_cd6-voWPe/A P) O͚W&M?E  qQAAAAAAAAAD k*TPB ]y[Bŷ 'e")AW rnAgZQoFvFͣJAhC.%UcU+C30V$8W" &4DJ_ppT /`88@ ?%MhAh/`hs_qp~)'$_!aѼttsl9=AAAXz(WaS7|_ϟ?޼ysŔN0A󭨿Xw@~%R*>D?X;pȌ 15ULNyͷb%-2y1jF߷o_ͷ`% .n]$.W[_E@@QXXօlX5ak##gM`ŊヒqYYV%K#RƤI_:tyr^AazuGK))snz͚5ׯXl9p@^vf^:VW_!(.6~|,C腃7>IIرcbȑX VPP;zOlI@hCQbŊm{wz߼#W##ă|y$ųg1cT38wnbqk|g˖|ӮSg.ű<u-G}'?|SVg>\3Z~bM{Řl12UѣG/~wE0|ٹV?lG`a|&6Xi͞gth9񙈈e".E> q],YvL |>lGjmFٱwĆ@AЍ$M)X7P-[[Tđu(fx$L\N 挘 >-۾f,u6"; +CZG?6G=+i*zt#cb+8yO~QU׽`%6k*N|ϗ=1Wcq_n.J\xlvHi$]:m|M'm7\=/ GJ#tP) şP) şWf_:"n"M' AA8          TPB *7 d@q!9:'e8tp\" !&%}ӴGOwd|3ǥã. -%EĠB-XLhI@ !N#D(wp+5Dpp? y ? 9w Giw-1+tSGvfOKBs Ɣc{<ʏÏ}fF*?>LN]=b͟G ¤ ৌSpsP"+NBv5]nL@'ưtc &t'ac5hnayNra`!|G'8L@i_ 5ۢoɶ7GQ2|ijc:-1N']VYe}q'ɮmlqmۄfjI|Xb?bKK/$}>)罀 =3Fz{=}x@ K>/l}^e[7U3@QT~bϛrW-,X6'oO~4:Ñ9fO=?{򀬉2d1ŷno~1#Tg= `dU.|Ts.OAENOpkZA:!WҠ^^90+`C;4UhD(4;CgN*tTjHRIB=PTWu^{ΩS'U.dE}%4^}v4n]:wˡ_ZwKR;}~],I̿d]LW PdC&i?tضoz[_9/Ywx5 |>sLJ^OA ,y͖}G]>_GJ2M{j#k?7x?ji }uA'w.(dGL d#k?U!r U/{3W9Z?yFvsɞ=.=2! MW󡛮$'5+l{ȕo?rYC~Њ7 \O?&р>[[鞥U :( 3"C?M?>ߞsH${~^ZdxfHK_G+G_ _W>2}#_|w誳x39n ս6x[η/^;$K9uu);k};z.W]yn{W3y7 .?ezHKe9wplOcMug5]|voo\֗?q7|e\-yo& #S=ALY?i=D;]'x?ܓ9uy<~I ;w==75 8%ڱĜ"Z)_I^_b>KML_OD})$)\9AFK3oU/RTkܚje H\!$<+ar/? J&`r}_FذǬvXg=br`,&blTTywߖ-[6nx=?N_oN|,v%|8<}@`wnJg8Im|ؓ%Jv֭j;v.C[.q>Y2!4ͮ_կvzh׃w=4nɂɒ0I2GG)G̓!滻4NB9'yf ?&D:<Iak)GiJϞqڏû~ί}w#/4)٭ '? }`a],y': G/ߡ]=۬y;o7{ɇ>_dOwdիxpF -oϭr↻<z?o9'arpI)٧$5/Vǭo-lϞ37?Ed1|[Uxp/WX3~y{#dk!9X%Z߶aY7K;O_>{gSCͽ%g2r"! θ/k :IKN]A393Дon鮳a:DN/i:xwN}g N3%NɦM# uyA'njj;Zk4nxםҋ/>wmxNa oLɯ~뮻xGI$} LRV*0J&[d+0YO3I6V0d+"30JL% L`ld+,&[d1 &V0YLbt^eP|e.fd1 L],hrKvq-gn|]b\ dK??p\q߫d[ dKAa8O/cE-\y- 0c[ d/(NndZ\,XlIV(̢`E-ɊY wy- `PzPErM+\\ V@gQsE-ɊXX dK"@0W1ْPE9&[ ("@ɖbE|LVԥ4+`QsEܒ-͊Y}C>mU+O|K/Cٟ_+N,hyDN^Ύń-yo{f2 KIeOa- yv{GC.se6C10~ Ξ $!Il,DvUR03Wf|Ѳ\sώv_e$; !3eǿL\bV~/@tcS\r=39O_JsΗ~,燽~,/z: Gv_D@2HȄ'ۅF(0`?A_A߻^Q˭{sAC؅:Mܵ5xxތY2 iwٍ1?{'ORw2%߫ׯ/D\лwo|?nN^UU!_%Ijɔ^c989PÂuyQ4m'̕L4ǿZ_3`ȰsĴʜwJƿՑہUAgo4EǿUo\칾)`xq#ƩF۹ tQv$se:.^?ƯwN2Wz)T+ȿ+ȿRb}I vy*XW5_i%?lυ)9˟/hUDп,osn2k?g{E [?E̗Lg?$CΉgyiΣ0p݃6N&ȑ8~f?`'?Gux/ăo`RpR98/ D?ӗKF O Q8W4揝9"qY&0E~,<ϺewxA v{SRdppeRovL5wYQ&‚?TEߡ?T*G3R*?ȿ@L"& 0`?A_A/V1. | -tXv_% ~]b7GKjKY*dlw pEY J( Ríu?s׎Ŀ+ծ@Pg|D ,td_?  f5|/ˇvwE.0o-Шp]6b;h0,Y;z.2!}^ dl_i7(}y^/j8 6Ku_ +l-^۹$Ex\|CVy,ܕ"7p,aZ"_?WWWJ̿7 T02M'?A)E6>Ǚ~t}VmӒ|a%+8n, ~] WC=?k(l n@AEsGv2"̕>БRai78խ8 ݑ/ |P&#?_ZढAT*/xqi"q^'4܊eެ}ͅj}Gh0 t^XR1:&o<0iI!rY%φ L:8|04Xp7ٮ.<_L}~מB6镚jƩseg@3VB)"oBI?oGLUw Q97N*J_I6 qL"hP bb]:ۈhVz&+x=]M/Q,dgmnO : N igOCPZ`Y%TT,ֿ2'J:ɿszW=탹C1 uuf M7eM>\41 1OnӾFӡ ^ 1w? ӆk L/οm{9t]?2*7׿pSO>|_s?V9d/iBq9Z` \n(*΋ɦ|po@]BG ge[%8?/7{s{EK&?_>X ~(7{g rGG 9YUB RsegʿǪ zaӑ_`VmI斅HKw }]+Z-U-vFa'Ͼk IsU Ǯq9S&eHȿ4u;,F7ʻ;uAӲ#W*ED=&e'g~򚝛K09ؑo>%< Tf*yJ7'/3?= 8f.m1ߖNnٗ?55(0ozn"=W@!=A4n#{Af֬\ycq$ymK]ݖ4uue3c?|b"uu[mxU{R6 ŻV ~V9|@ @ QT>B@ @ @ ,Κ;|j#BҍlBֵ jGP; +Prm/6BmG4mXnZQ[u-@ @ 7@V9|?@ D=/{i2#MĀX@ Wb6O[q ?|0>җ$BFۖ-mP l`uO!+6uuV}mDb|<hDF溺ͭN {޶N랔M0 '$Ok4:nkU7[ KQym.10dƯ4g&L=##LFTAjl6?kw4[~7ٽdڒU4:Ֆ;vi8%zGA_Yx]/JdƃO$L@$ {2 hl@vѯ xnܽқ쮯1ttWr@Vl<8tbѱ#LFTAj4o@fA'^֭y"Pϛ\<駁'LY=@F#'A,6ѼvcSLRL5[j,ˉRnjq5[CK:D}[kc`KZBVllm mE)H`$<:Hph+3A!wh^W7F//'N?Эf-EL%6ljyM[X#@g&# +YqΡ{wH(وGd]ظ A#ja]7פ|LcŀD_G,zx]b!@ @ *O@Q@ @ ~~O7R9Uo@ @ @OP@ @ *@ @ @ @ @ Eor~ @ o AB`#-^ђ&b@,3 ; qk2i+n 'Ͼ./H㟼@W޶ng/y@pG]TRGyp?MXO RL;OΔ<$7KsY*\LOO''2z:{Tm=Ơp7oτ Pd!,&u5OM!9Xz`r2G3p{-%7= 9'>9ٸn]#+hRWͩG2N$xcWkY{duϺ=ysQU6:ԸqɚZV.;îzkn^ng>qpkU{\vTzx䡧zz4.$Z=Hek-%''gM{ ݓtMrrYh6xS%a?d8AcGUM45pVl5O<o<Ѽnc3Jp>&Kb dp&sɷfkt&Kʄ݅R~bkk76LKR밴4ƠMvFG52ៜryKn_H7Yliv؋%G QJ-=XHo}QȦ#2_d*KeN9$r;9 ey# %O?YJ1`]7qM`plOcMug5]|voo\֗?q7|e\-yoy~~޺4a]wgUy&J=ztvj"~ 2ANONģpy&LN&b &'H(PbHFLJG!Db|<dHdHhwq$GXdl(4S bcGՐl4:::284P%ɎOLJGΪH6 kі LzC綟*r^NυyCV 0Uf#"zJbVf̆ô]*HGę۞,f̂]+9rUNnJ'0`앹araYʕ0XrLu2Gig,Q S+2hT /3W?K~*OGl{q{]ze.hwC׷hU.x\fV@͆dz/z: {ON2^q7̕ !\gӾ*?WWWJ?Q$_͂+ȿ+ȿ+ȿ+ȿ+(俚#ՌL"?< f 5G?_ȖP74%~^Qhm s)o45gd1;5섊m];-9m΁9!}L:JJD{JTd+ +ŜJl2CjLm7s|VcIS)I*aYN- )}hv1baR֔fbSrD06 :%R:oΰ? R%VU &fdmF#-Ì!ƚ-څꁁl˜2?=hfV|hJ#ȸ=0֒LNHr:LiI&E$JjJEՈiѪFK#hbF'ǵ)RԱ0,Qa29d(IIӵOӖ8rc ;6&#.IG* 6'$3^/h=Z#49;JZmku`*=*A&8 =3a%GHn*2U>N$$+zDBo$ȁX<#G uUHDo'xLbCHaKx<&u+dg3ƕ1i+0MZmL%ґ.ǛU"ND ^`EW9ĴaG1~psm惬e t۵TkXFy5 %yD[*kx!asP JF2 aj5]ȷx΢m\KjdU[<LVDD8֮m΁1(6ZkD%ž"DYc)mk.^|ycMjiPYDsѠtxh/Gxt(j)Kjj6U%%$ZR6>XXB%V]Fvr`iU!sꝤ&ϫwa#;Wm s2"-k屍,#5:G"m(8I;"2"۝+e 0ӄmXVte6 \"*fK-Ij#5~Ń6Q֑s12#;_EG Wܹja{kV> BvqkXP"*k9F;Y R*AN\=po4vρ!S^ge!;VATyA)`3 [i_^%nw(n/ܳr2u%JZJ25[^u Š@p ]sEw03L(k7,Wݞ Gn<;'ēŠw R z 4nx4k]t A2uhvx@+`&uʊo R0  f:R;́m1TEe (袋~_lsWK}Rϲ)`[2øtÛ=p-@*}u p? # nVf.&Ҁ60#dnكs%{(vt.HRdDXNE StL"nEե"H6;tvvv( +E'j՝)}RU6TvjǝPvr3w(KQcvp: ,؊jn?*v.* N,migf8  S9hiK P{O'[ʼ~8[m6mZJjofll{n&C0uph 6a!Q )Y(,俚#Y?}%a1_A_d2n,bzW 5.Duߝ55w6p9-%_kW,ݸ^pa,;v^koڰTv ՓDw>Q.B#\|Wι.N E)ܾںvʀ!|7p>m ?r 6{q/:aVtە埝sVoPiB\Y o UA (=G;#H?"288V-_ZG)4NĹgյ*VRF>B_KyTe(ȿ+ȿ$,:+ ?_@jƢ+*.^#_A_׎o =K)?)>Vݑe~ȿez}%E_ven[d7Rl6,T*Qo۳Mna}?9[»M'?q]~7:xHqw]P [pɆ !ߴH{XHpcvziw8/oؒyzo+?/lPyx#u_oG (ȿ$,:+ ?_@jƢ+*.^#gq*3<=/ZK׏ R -^=N[q_4_S3n(uNtkVwm&w^F7]?u_@8OAj&ϷWANDwN3 cؑɱTt:YDN|'z#%ucʃ+Gާ7{@=oa@\޾mٝJi"\z7N gϿDs2nv3.tS_/ ^834*i[g<,䌇"ϧN A(=JQPPPPPPPPPPPPPP|?E  >j_@ Q%P|v{&sմvF̹T7k32hӝvBŶܵHRsTR|#HoeJSy;pS@ zTCɁC`z URp0R`RO֖HJJNR)N›LRkFS0IBJ`m7`!K+hMOlR*->6 :%R:ozgggfgff-ΰ? %VYћiEtCs2^PBfǧv6aSEF4ff4%d܊|z1 bԔ%i+C;HR.I :NH%"jOhU%4)XuI(Ѐ0uȮ SSSSB&UI݊f]&]=IpӴ%\ƘtzN \uv-L:RqKpbH=+I&Lzr%ajMr`BGP%AOgw"!30QJ#z|##f#wVDyb1UmPin,AטުbeWȤFIVYՑK[Hf!0cȑBU{C eVV͵7%յƣ:iJFVL÷r#*”xTy9:QC4QA=IF=FʊEI:Ӂp'ETG>Ҏ0waɁ5_>@]Kj5uRhXC"eS-m܂4l<@ 17H#E0w\}[#"N]ģ ;ŽȨ|j 55^  dDɪ#i,chG#)UP C#.?43{ &"F*060R@i 6Ph8O6,wph }.[Fi{ DSC5,D:.۰/9 Q/>$ۆbtVڄ5q/ҙ,O8{^Yа[w!!͆U $rW=˖,EݱNK(+B.R_ﺈ<Go-`p :72kёh+O A=5PύA'MX40-zrWF9=z΋lec UPz# d`@,FԞT趟K@?JIYOݧe.r N`g4xF":m1TEUij e#zzDETҒjϲ)`[2øtÛ㚡45;lVDRMpwwAB-Jбh.n" eV2}̻C>] TI e#IAsa:a7Ou1UH2bKP\t+"An wvttpNQjj xG5L;kS>ZAS;MCy^:US lil@I݌'Ja!?ɺa[{;,2ݦQvhx; AlJ Dm {:=RpSڼ4m Um,e/n}dư!1L2daGV|@ xRqUo@  @_߿r~@ U/~_T fkX$6y R UH^YǂyuO=uڊ!zϿmii! I*+68y O^^q]''FFo~-x_*-ˊsV|mZ|@^GdWTkf|}KlL't|:q,1>֛KNOOEYR`vv# D @ ~)NO3J|ѽpC鐾(d)w#|߿T,@ Uߏ~_UD @ *w+̽EnyeΥ^YӜAǸְ*u@ܫnv)2tj.6"g!=%*͕RubN %}R!Tu5қ9mj>1BkmT꩔$$025G(蟮K-+.u(RF pӕg'7S#a) x]sw YYbY]`XiFfD 0":̈b" ]8f` `:#PȈffu׌2Aƭk"͏drEL ̌mIх7&m~{nao5br'iђrqmcu:$Kh@o &o))FI›&=,&ͮNמ>M[eI7d[[g¤#g`8}a,'$CbBIB=oL$yB. SlPr`B=q++zQ'\P $ P13Lję1mP۸0}kLoNb,{Ƹ2&mFIVYՑK[(Ӄz': UQU1`n2t GǩD؆)(DyFCZ>Ԍ i*atwTzaQa@{F/ ^2•DQJ;!uicSŠ0 Sg"b; _.Qޝ@N^aGdT^ G(U#%[2V`F@]xݕhfa!Q͈t [YFUsd=y3<&aO4|t\O7 eR^؊v=G#HgFDs#`-$\JO0 t )z*D EOP|@ˀ|*`Fa5jP؉0m@2 1(}hӠt@wރ-Q$GuAd)ZE" ѹI BHYY 7u\ʁ&`8LAjǰ9]0 8 x*Mcԕޞ^OëlVcWm{F=rxs\ܣ4ҴG91`?xS7o㍭r9r.{ݿ'"^5=`:]S(>3Nz w&bG&ǒSS J"@ QXc?qCgb ܍~R@T!v<|?G}U @ @ .H{e!s)h`ζu5qNj ;b[N doν}TjNKͥDE֙QJY^)U.x0H`p2Sr)z*r z2DR*TpJptzDJ L7-GF>lThv1)J+hMOn&>%GcàS)PBJz7L1<;33;3KVfXefFfD 0":̈܎XWBx` `:í(2Y5:Gq`ڟaF#3ݴY]bx:LNO3$rZeImey(/Yy5,iѪFK#hb iOɤ}dDa$Y@ze0ys[\frIu'HLLGns 6 RϼP$LWWOgw"!30QJ#z|##f#w#W@ O1Gob18/Aa1:ًbʘV&Ye6U(Ӄz': UQU1`n2S)zex<'VJeʄ{`|E0e:;*=IF=FʊEI:Ӂp'ETG>Ҏ0wH&Jt m$ꑈ"b; _.Qޝ@N^aGdT[i~xd$!#YKXSXjVA% =+-\fC8,Q*060R@i 6'm0< oD)$K0,!6 ezX4,GΆ F4L ғ0 C]CCH  I8 R6 3< ?M&(K (Da<(LǠMbUxʮHTǁWhGP "xՁgA&څ@?!?A`@*laȆc`s Ad`p pxUڅ#(#C1<6h?&F dKOO/h+4nm]{sHQsk5MTn)=¥G*{UԎ w"]LXy-v uwz+uK(v]K1W 5pԩxI$ԭB踺T$]VU":X[#`K'+uMlTtEtvH}v ; e'7SN=p)t>:5f ؀ܡ;hAPA0EdQQ$}y1|XJ;x>.*,r;liԦ](g;j7LFͧ)`{:=RvP5]ne&.m XEa6Á[~>m2cXwƐnm xp9p݋av@@ru J_V9|@ *UQPPPPPPPPPPPPPPPPPPPPPPPPPP*G| @T!|r}R2:cGa]euD<,Y^^];^S==&'w/߸#@$9Yr}_NLS)jt2>3Nz|O?&y~'(d1YL“=d:YZ^, 鼒%W_6]:<п7ܓ]-YgE*LE*,&[b l5'eS1d+lB|T,O<}d1YLd+ 0Yk1K}+P\JnWIksunm1;%9$;bss)DsL%Z4H`p2So*%}.@Py!٣G QLEREZl=A5e>z?dggfgfff0ȢfTܐڱ]1s;0f ȈH)dTĆmeYijdT-2Ij$0&ʟ$}씋L?*̒Ö8tO:M^#@SS9$;0$05 w} L d;p{%@tFzSCq-1\ǫ@0 6η<(LYY>){]m]I P.@  {πiḾ"d{]G6:=zLszi9ܹC]]j]- 7.W 5puwu9pؕCD:tpOTtEtvH={A1?r)L90ҩpԘj`Nn?ť~ݦQvgviFg;j7L٦Ӕv0Dɶri6^Q%T[Y%Xv6~>m2cXwC%4Вm/j`MzE>F{?D-gd1YLLd?;d1YLLvd/~d1Y?S*gP2l.:` LRQgc9L5^d1YLLd?;d1YLܓ;0ٲ<3|UMcLRQgc9LuMzCq>mU+}?dV֚ȿRHYSsgS߻^wj>l .a5:kߖ|&Ⱦ#ʱsn/![鲓Wl"?(Fvw;'g) W*X55mt> ?8^PE pN/M_Q_t Y}݆xˈ4LxjٛTt@qF8_ťHS^iknc}r<},1F6/tD`xqIWWtqA0\Н r ǭ3dxHAqb8h9OWBRx}*7 @ @ (8|_o.@ J,+ Z>&[+7؎yZ/yYr̨MŜl㠥R>E˘<;I(U冲ʀ#UX 9Ȑí/x}z[߿B|&G@  `U9^*eZ`L+R \xelnͬvCYXKer?‹ `U9|@ @ @ DXUv@ 1|%ػ6ڂ|{g_ut}ym۟i. ʗO^)C[7/9\ѢwY*3%trzz@]`H?52RPi YRk&_a|r V@Wkmiul4klF9mJdߊouDp nkuuK5OU6YNQ"1^0P[ zkmd-M;EppqD"R0lb( kJt_cRA^K?(+ꭚ@ӼncHEhO7i XlhRcnlU;-J5l|E|h,6ZX] jy# ht@]gKߕёAQASmmy;г}@ok_OsO )**Yok|y/ol=bax=N@ D͊`١h|/;sKU4qݮ!\>%XUo@ @ @ QpVߪ]|"Qj}xP\wy1ػH-Un`3V~ )z[oir[zCW޶;q%^o4yYW${|,ጧmJ2ͷ[q=:;5'/^[<%^o-*Wӓh$LƍFǃU-,qLN&bPxJV[%&H(&)ozU{-˺iOŢPxXo-' 6NmRU{_[!WR8ELJGƓz[tQ-~YyAR|[*Tٶ:UHGBCuC\nV+_mP{R-4{绛KUKRLؑ`oxr\ooݝ`Z~3wK ]:Kq u,U|wlo]cW*Ph'm<9岨u7@3^uT\|*T5V}CAWS v'jug'st(͉MS|ckŢT A4:::284.b+:i5:#|i] T!-EW ⋂5vѫhtdt$84Е6ۂj04 յfʤU֥``3M<%[o-Wb,YpAKѥjc߃d1YL6d+~Q*ԏ&\_xI5aN:Y8&4Ş+&E&Ş&[3 9堙lA/&[Ic<'Ӄ;Qm9Uu>]'vŝ&t- ,6),pXΏ{~ƹMg&p lSƄx砃;ߚf^zԊ0L6ldbиqbY6&='Lb)&3wA$SdK:ա v-3N0%-U0ي! &[1d}sd}PAyM4Y}Ti?-=0J&[0z| ilYL ,MXp iGgD>rבJ:=GH\ֶ5n!s5r[=4pDG+nlx'TRc'^~ iHX[6~E[ϽRW/[=icmK^/1ZrےϓJ89^Ot}c瞽:I\e~-OַY?p-[#z?Py6F8G>&R9=٪#/{^O՟IǛbO)u\%;WIt?;_On_ʧXgoI?Y#)Fz7= 2ɪjx&8430gO)=%jq-Vz2T2^^8|ᦨ/xy`˜gIl5/Kq;q-c&UsMv{-[GY+ٝdwO?7y JV7i#9rr; ?'DCqQy&wap+ŷeOZcfY'Ix2ix:gYx6x.x!^%x)^qWx^x^7`-u,w˺2ٲUo[6۱6FYϐe5V{^la[h?;C.aw=셽ɉqO§|/"/+*| oo8 w=p|?qďc?sįkQq G xp_8'Nq Ni8g`"Y8\q&B\1RLTNq%f`&fa6\kp-7&܌[p+nw.܍{] +cV@XC0<#(<c8<O$<OS4<3,<x.x!^%x)^cMījzkamuF cfoX`Cl ށMމͱĻƻ}vvNvƇ vn{`O|{ao|1|$ŧi|y|?/ 888[6w]|8C##?O3/+oq~{cq3o8NI7_SqN3q98|\IbL%S0a.+131 qj\kq 7f܂[qnwn܃_2V`U<jx<<£<<O“<O <³yx>^Ex1^ex9+J kZ:Xoě0ozXo[6۱6F[`K [ak{ އm~l`{|;`Gag|`WݱGG1DZ>Ob_| gY| p0Cp(o0|}?8?? ¯Gw818'?/+wL D?8)8t8glsq q.d\K1Sqr\300Wa5z܀qn-v܁;q=C{T +cV@XC0<#(<c8<O$<OS4<3,<x.x!^%x)^cMījzkamuF cfoX`Cl ށMމͱĻƻ}vvNvƇ vn{`O|{ao|1|$ŧi|y|?/ 888[6w]|8C##?O3/+oq~{cq3o8NI7_SqN3q98|\IbL%S0a.+131 qj\kq 7f܂[qnwn܃>/WXU<Ax0VCP< #H< cX<D< OST< O3L< sx^x ^X+*k:oZX`]Ѿ1x3x ފa,ގ !6#b3c lwa+lw=x/-ޏlb숝!cݰ;(c8'IO >a|_—|_qq7qŷmq88GG1~g9~_W5~(G8#gp< 8?/smzO~sp.p!.ŘKp)`*.4\+0Wbfbf*ո:\p#n͸6܎;p'>'ksyv~y=kqhdV G*}S=G_1PfWz{[3k0o^=zh!“qևٌ8-FI0(fZ26툕bZJaS4%kEZrr (9JXZa?ɦO֬CSZb>KZa\ P7\)%Eۆw(ԮЮ᱇M(?>Q߀"#Vr?>3~n3WyԚ.-iY-g}%B2Tט\=fJ,)+G!(*S !&1!^F=&˨DŽBx({u7޼e햭l[ٲcGel۾~붥VU !;sWfwתF\m|l>~[MklBwR!I3{38ǟF;.6mBw-]oSI6wi꼥˸]),?Gk|cBԥiqrW˰ 2$J[zLQ1Ue2׬]0mlͺ =vzLQ^1끳{9 k٫_X_c7lsv/vbucL4wzr-Gzŵ:N+V$/_gݻ3]VӮ=_uSnXV= ?TrߥxW=&( `g}ӫ]WY33gw&dJ!L)5[*gIxٝn3|ڭAsǚ}qv֔bt=̽qp>uf3w)U !;R6pž;ԙ2hAgWo3t.C])CKVd걈7xwXĀw34)ŵ1!Dyǜn^ qpk6sٸcY :j}5K3CXd;t˪o.Gsf3wqO 0_c6m7/ofk7scݭocSek -ٰ!c=;?4C7a鯩?0Sެo8Ϫ?LqY9DoΛpkcB9߼ٽiS=o9Sޜ>mϟo>Re 3׬M]"9N7[ȪeUVϷu?4²z:S wYi~zLQ1 [lڋzo/RX7s.v! 6_ܠ/xȩ=kԌ=;=,vgGq9 ž1!Dyǜ7nJo'9QbLeh}n$qǸ-L0Zue cfM~x*mv_c뛶l?hZ>-d5=qKRop]qzLQ0۶m)/l޺u+W豊vյFBWy}ǎ vذisu!zzLQޙd]wڵ\c _)AzLQa|үU {QvٳxF({u_y| ̔yo=5/#zLQޙb沕m>U>xqPW=&(<)KZ 9?p0b'mcB!zL!Q !2=BB!L=,(({BLPQ!(B/BecB!LQzl̙ B!D4;h֬11qB 9s(؄YT !6̙,ǒjO! ؈Bz,x'BDJR !1!^F=&˨DŽBxB/BecB!zL!)}_"NX!DؒևW%턅B| JvB!"# D{LO !0.d?*B!kcB!1!^F=&˨DŽBxX?N!DY{,*=SEQRJy_*(RRQEQzLQQEQXĽN;Ź ˊa3[VR1RۊI6H=(RS~{ldVeuMՕ)&bǽB{,>gY| qaN Hl|fpJRcÜGQ"Xi)-1O88=s*Ί? cJK|)܎eQEl_ ǜ81') b'.֊ 3yEQ*leŹK)) ̣(Ra3#>cDz+)ɷR<(6%cuK{eXtepQElP=\3,$)yc":w_ gEQ c(JYzL=(c(^zL=(c(^z=&BQQ !2/UBBecB!z,&G!D0걢X(Oc1EQ/G=SEr*XY]FzLQ`=~T.zLQ)=#rj[] OcJLV 3EQ~JNzY1FuM}Q#M-7.:XLɛ-豸Ӈ8h~CEQ*Xoh2~`i33H3=Z{V({{,>ŗύ/֊3IgƫEQJ-LSQEEgǜ#C1̄W(R)=6d30 ͹\Wlw9-WcԗeŚ>,WEQO$c.7H ~URXSgXE;WEQM1}LL혮R;31LxLQtR{lUk;h.Xyb|EQJ'NJ|>`v1EQҌz,SEQHc1EQ/G=SErJǒX#1EQzLQQDŽB5cB!#1!^F=&˨DŽBxB/ "BLXc(>QEQzLQQEQzLQ~IJ4ۏSO.kn%OK4|òbLm$MSE))G=62FJ9Szl$guMuckRs'%qq>_ S|q<|7>2((]YїSD 1wㅃcNM%؝K)NGq;..3G;(R>S^z,W C3}ҲF|!Ƨ2͜Ncf X+u(JetҨNM9_m?*gd˹Tȧ)+ mdVb{.wz]]G ,*a _,R3ؗ-˹ި(DrcG%RV.=0ti-b2eV nŜ3)1:eo9Ssjl(R,~QPc "Ƨ_~GBИEQ%D-k8z1v%tCQ%Z~&'*1EQ(F=Q)(e$1(S)x91(S)x9"BzL!DyD=&DTu߃B!@MP !Yš${l֬?gώB9&͙3!=f木*BQ&ΙSqZ=俺 BJDŽBC=&˨DŽBxB/BecB!zL!Q !21!^F=&˨DŽBxB/BecB!zL!))(J zϿB!R1!4Q !21!^F=&˨DŽBx^QEBc(SEQ(EQ/G=(x91EQQ)(^zLQErc(SEQ(EQ/G=(x91EQ)bDQƉcQQ%TX)=]xEQ*rcZE(JEzS(xzEQyj%Oo(91OW"G=U\%x3YkOt'bs$c-,BxEQ{,gĜ\ =s921Nsޟ8ig->)?>6c"8X_Mw!l(i%W{bB陳o0c?yڬVI'O`af_BCo(JO);]Y)yiJ@B efVQ\WDJ |OdJCo(JO{XJ~'y}.>UKXw]62{Ā-;8RS.1y|EQ~c =?~Ӧܯ,a C%߿/^ ;c҄}it ;[QEJc^~=O;/7OIcQ'o;-t;bEQ2cp/G"CwŸXcw!E(J1SR-i/c +`'&q 늦bïݯm(RzG-\ ;)ܜ{ClA(DJ?z_+R*J.xEQ*r"W+U\Ŧ?ѽ~\LEQr(XspA=+!q !2edpŸX{ޯW4$S)D("\壵bdOi\: 'iNeǜk 3E엻ScD8ݚvX\>9rpK¾ay?+/ÜX ~uOEhc)w q]N¾6g|CKV> bp4{9e1^(JyM4z,>%yAC8ЯWtяaKNq"~YtI N+!o܇1̍WEVcS^>m'0~1ab~y;O7 WEhJbI1s]ಟ_&+K\O'c3?щ0{_>k za~+x(c  y:L]ڽ gDwWi2Jq;?(MX Oo(9_1*(xzE ǖǢ7^QVQr+R*J.xEQ*rJ8M2N("R:D)UG[(Ic1(^NXTfB7>zBEQȣ+*zGQ(ycZQ= EQ"zX u?G(J䉸"zŵ-iTS`Σ+(쑩6_>M>n1ܷ=cdC3q]KlkӘp?{,U?1v|/O}(ŸS=(J9H=\<=bMG(qN}wpNjE1؞i,{i=HWB{;]?zLQr9nYY?Yu-,j'U ?zLQrhfhcf⬆;Sҟp.-9dJǜ+aac6g S=(J9H{<-PG39scTx,;.u?;Y-{Owk?zLQr2cϕ1~X}O,Y0]Ai-g^O٭^E1ZßS=(J9H{}}ِa{fQSBXݽ֚ ?1gl>_u5psE)7v?O^o_h^cϖ=эn+‚?zLQr!>{rZԽ%Γrc,9x!O [6)kN?zLQRﱂtXl[;h: b0ϩS$KJBK(R)Z ]Q= EQ"zX u?G(J)bc1EQeJR:)NɨEQJ?cze su(ކRXEn|ԷA!"@=UG}"cZEn|ԷA!"@=UG}""wYFX#Jbe £cKX#N =j~[񮢬BxXačj3$S !D .=f 5vCܶ%`GJf k'03YT9BjV1P !P !FzLQcg0VdT2fqFڋJ8R^B ~ΔyU/UzL!'Kk( v?LJ%+Uu1!(X/t㣾 B1OD7> *Jt㣾 B1OD7> *Jt㣾 Bcэ)Bx1NBD@iBQcE{_*!4ɧXFB!DiBecB!LqX_V(ϔVB!DR=&(iBn*<kTx}[#F=& \okĨDŽ:Kmoß \okxJ*FzLQJ1ۖ/^Oߟt1M ۲|O=Vз5bc|Ƌf9wQx}cl$z}E1SGo<&=?wY)7~?O g#MB h 󥀻ny2cݿ&ۮY{oHt 0W_ ~D1zIJ[~Ի埃O:K~8_^zѣk׮;w.Җ|';vFNVZ}⦄{v;\$Q]SeYc5:w$-38H7ˮc9/N9tQ͍]Iy(3F!gqݝ4\#?UkO;i/s]y9ǍJqFk7B{=.w|0 zcY;k1:0:$zvl3=ZhQ푞nᇾ}kZ[߳q@nYD-=⬄K:N}Ma!v-]:|*ҥKQ䣻k|v.۷~;]+֬qKOyq0rq y~,gdnY^arN*k8OD<0K|! ez]b63~t?nIgÒmb9w Sϯ+B 93F+~IQ]͜dv4uvPߴh<ÞާC8hР1#G2bdY-z͢ʬ~z;/n|wʍK.xwY'&V>*cT6wv P̛2ed>ޖqNa{鱼Z H1XN-YhI=p1bB w-n]YIڵ6W ~#≩0NԎy zMF4>Xwz5ߊ 龴15a 37N q c>ؗ[f:k=*^Rrrr̜9ﵗg]}u]xqoFT¥K}ɾ uV- FeɳgVNoإʼ Еd9CQt812'p!$g|]1=QpǶLصd%3v/2GO37"1y$gcj |{k$3ӷr%b )+hȑ|ӟlof_u'Yc_!sѷxq=ddYꕳzW Еd~9#k;ODɱxl]Ned&;UcoXP!SNoYfy nwmNWW̙}_;q_^6k׎3_NNn_f?ŧYYݻ*C{!6k'B3],'%!Y wKdjC..S cF_lhjɒ=zdfa:_Q}IS\kyv/apf&cN"x4! 4૳g1cyzڵkWnpO{Wc1|2[|ZB+,č{,cG^fWTf̖4m3i2o1O{Y=تG_ kekU6֚vֆ6uR)*ٴDgD(O#nk=֖V6,ǺfFGՍlH~=܏U-V.vueg*Rv~BkX[Vv'_+'}Nj:-րKsv+Z{h;=pK\L~W[uk"K!'v98Oj~u_uG_W|sDo|~_풱\8+7:k87*3#c1+u@ * W|}s{"Wm#(#>릝\wڙkܖxmܚxm3ϼui>y<VgA*]>˺k,31Jlg*;z=f*+9KϗBi<&kta&Ro\v+[\*fԚw^zײ [h+Nl~tlueeW}v崫ѮJ6Y~Uۤ?tʇmVƿRa6iF5jN93bz̹܅LWw$g/>UJ3ݬ= L X{Vu^0C[>;9KvljA;SUB4QZF)c^Rajˮwy+ܿV׷Mmތ2OoubӬ#dUYΊWίt(cc4=6lu (|q:?+Abb)5 *S̋3c|U焺YGv|{t]͋Br2n;^{rVgUyveloyUvtwŽ1yW樘LZU ^] H>۰^잟rkYŤwqo@!7w93 ް?Hl6WWw3=#l֞^!Df/} [RiFP)t^PFWϼm3!١WVF豴Z h-3fzlB<>آ+7US̸ Qo8 c^Lg aw}uuzy' 6ܮ{;ksý݆eyuakjYpFz rߪܷ;sS՜aS?N5w1ް{wEs% ַWajcJome+ʺsF״t4FG߶1̔XO:=\Ze3^ӛU6 f6vӹen Z;s3ٹW9U~631'{NÚ,xEuf-6poUn{7%mH{kYpFzd ~nu_bX3 ߆{c}q)74f^OM+p%9o a0 o9;]tDzhu=3hcoan߼'740IUsF5:FêbfQN]>۰Z^,dX꜅y}׬nnO&>6kMXo[ b!B4N}fpEZÇ[;طbY"!(f?~|NvLo7ә|7.E nhܹ}N㴳hvA =o&aǂzly {ǂcΔDRFvҬi0`8upR VLbض!ǖc;޾cǢx B=&y#}O^u~!KfՉXQ״[e5IiR/l<1=fckٵ[^/'x,Xؘ?aKYcR,S>\צ-fBcҪG^]E9ƝoWpJs_rgڥw\xΩ7=Fc_`iVrʌ^4E㜟lS>&%pOIcrkz,?=B!+˦zu՟Ӷ2~{Uvk1*n{TͯkLyM鱼XpMjjan]eVzX-cO+}M]-#_e7NC=&"{u\+ V:<̻h3Y ;7^7[qCg6}5_c\aMc-ZJǚ!$=jewese.\qo0+z\XuEc3oL*[A5>㪎{'[b"D$*ZcM{|@Πt݋yl/Qbٝˢ2퓙w?yW=V9!-SϾu ܑN]rzcSb*}43TY]].Q?0U1SSY-{e42Sj+ .ov~oIIkS^D1> zlCic\cBQ1q~qnkWٝU9PbjU51j >WJYDtq>\TXNg?G'];1rÞG^?ʾ~G@mYV+4ir{_R/ UU*YVh?RUXU<Ax0VCP< #H< cX<D< OST< O3L< sx^x ^X+*k:oZX`]oa}o0oaclw`Slwbsl-.ln6x>A>a]v^8| >>})|g9|_b?/K2k:7p &|a<GH?OS ?/K o[hXğp1''qdTq&Lqy8`.Eq .LeqJLl\9Z\qnMV܆q]XU<Ax0VCP< #H< cX<D< OST< O3L< sx^x ^X+*k:oZX`]oa}o0oaclw`Slwbsl-.ln6x>A>a]v^8| >>})|g9|_b?/K2k:7p &|a<GH?OS ?/K o[hXğp1''qdTq&Lqy8`.Eq .LeqJLl\9Z\qnMV܆q]XU<Ax0VCP< #H< cX<D< OST< O3L< sx^x ^X+*k:oZX`]oa}o0oaclw`Slwbsl-.ln6x>A>a]v^8| >>})|g9|_b?/K2k:7p &|a<GH?OS ?/K o[hXğp1''qdTq&Lqy8`.Eq .LeqJLl\9Z\qnMV܆q]jcex <!x(x$G1x, x"')x*x&g9X|/ċb/rWx^x^7`-u.ވ7a ތ>ނmcl16;)6;9xx7ރb v 0v c쉏`/썏b>c|ľ>>/%|_W5|8`|P| a0~#p$~')~%~_7-p4~cp,?O8_W ‰8 p2Sp*N8q&8< 0 "\ɸb 2Lq%f`&fa6\kp-7&܌[p+nw.܍{ЇAcex <!x(x$G1x, x"')x*x&g9X|/ċb/rWx^x^7`-u.ވ7a ތ>ނmcl16;)6;9xx7ރb v 0v c쉏`/썏b>c|ľ>>/%|_W5|8 ];.yxGx y<6+[@ X5ws;_||{/6O6ow뎲NX3u ~U߿ã>p}O8ʻ;V6 rij5_fݳja>Vz jeݓaɲoɣ*>Α76{}}'} |Ws6u޲qXD8}Y| |ņE·(">LYU V^T鱹J|GGwWsDsL<~|̜"r=){$>|/.E_i/׋0jlrq} B.o؋3<&G?L);s{ 3_̯@:Wp9|s]=i>Geio <6.ZrǑ)M̅F|BM'2%w(Gp;{n}2+,uw|;C?;JU˱[| 注Q}m.?ij.?ٹa2}i;lsV;f>k60}۾[.)ڻ\6{ f1ٺ.ɺuwE#ggͫt͹ǿ}> |E;Ѭ|GVqUsaŋCA 炀Qи>. r]*\$!!3_}M\{jfSS]U_UWtwzhb6`qP6d/P򑌧Cm@EL'6S|m؅4^Ӏ^id|Pkܘ1Qα ReteEFZ];'ĥ:FfI7#A:Gþ,man?HvTasXzq1S31C5f ZYS,П{] ,//I6<+UWZh jK$/Wnf/B+yvXB"(gg.فOF%yUog%(jZ]fuy'X>Fef[DV*:.nC2mS +ȅyu)_%e$)\W^@+K:#nt?txxWD0M1FXIcVS1ϵco6S\,Om[|>s♕l,mJ-yXJ*"̢щ.o/pՆD]Jj:["2Ϊeհg| u/׼K^{kz磒s%Y15kWuk̍[i3l?pf;9JUPkVۧ|m󡭺e6LQ}J .Z"^,6rq)5}Ǝ5%.Pw>?./v .20ɽ/OnUX V+῕{?}l{ܛfm6+῕oeBW26YPD?_˞?{H%q%+_#!m_j3+/oN흾n&Šܕ6[ŗr2vdVW Nqya;7T5nCck|U6BM-Kb j/ЂjtIY".ݍ2擂fq}NNc[}uL% kh!+YNyd)I,'{ēgZ̙mq_݀w1iug5R`@j3jGY,Lzrw6S;86oij uN6Ic^uV/;CC}vϦ Aʹ6;NwiI4 wrŹ=G)aᅨ^y[le/v/D}t8o G?Rz3l e@o?2?_>d(a Vs?3OFlBdNW!?7޳3) =n bz){Q ڊ9^!)1AHL `S7+six/kD/?-Ij6\mwlBP$ K{3,kʔb2qg+Gbu~>T{vjZ_UR_ Sj%J̬_m/K~9rc~-(:9Eqۿ*_?(^gN;*P4gon-{Fο*aΜ4C)X{ÜzvrrݚՑ,d{_;9ݚ w:š1 01Lu (i4۾Yafpe i{o0'jf pw=#P??M3IוxK ڷmy^M%UHW&?8}{#iy<RG)jʏ mj':òer!"[l5AAu{K9c>PdBOt5fZc\b_$o]+@c&}iƆ.sdeZȃ,4i7js 'rC _1VjX;3ʡp\/vicTYZ0NOt(ԩFJP.Wm:QٲJ{Y[XA_3uGGʴ W6R%*P]O5~Uu$lv oϼ %->b 9_ lrm9GPm=tYna 5%x9E;-Y܌ ~W\q1`" JSXg-"oixpK:IǧW&66F(ujQU ڇ@nKSfik"_}` cA;Y'J9KŔQd]O+gyNRݤf=J4 y@^脦6[cư\{*2sďmIe_,"=)ۣDYƶvf;1tN˄j! _G#p^kO=˔byYí. svB[ "<"fRiS B1u\T0->UH<5 2'&FX֠N9w<; &oT|vު)ʿnqk(DE'|^N7bF)=:nӵ),=2):"Ƃ30\x4ƒ!#Q"!.7,pw9Ip@x%c_@k1pzힼ`pʯCOqjq k3>o9}`1 V+῕?\Q\ؘ[H; L9KS EV+>˶g$ȉiIDo$ma_93+$e]U=w_/o8=[8Xm܁oe?2'pG.pmyߌۊ"R?_|uTyeto~귨f|f;(IO1'ˏ |\vxК3iMNG9?r@yw5{ Rg]:'KK]OwvƳvG>Udz9_cF;9_jw0^?_|%n0fgWܒxi` QS{Clj̀#£u'OFVSQTpNxRu~m0Wsaї\o DŞOsϪd8(f=:1),s_g*;?4뒦 7%)uI:*~ZhPݶv@PTK?i$hd:E_7?jӌoI㟾[Q|ϾJдO,AYY??veg6OEh'?'[,8?l M;4'?wYW~-~7dHf0f#:!Q7@ᅥ       0pb-'QSt 8"\nٸ7òoeddɒ ϟ?޼yT33gnE>[=j;|V_o[&V^n+)͘1ȑ#}-oə4FҥK-ZOx_>W_6v O݊Pi~~>-ggg/[D3PכiaO~{Wf^jUog\݊P{>㌌+V< ϙ˧hgIӜC'i76_Sc#+*lbwɤI([oTY^A'L7;vЧ *=ؚ8qi] rFd~mfZJuuiӦIcc$t'hd{ -ĭX/ >jVkyV_`4HWW3@ 2uT1GFJOrnY]`[-I'%hΧi'h҄ 裢") *6msιwml1iʺ[%71 Ϥ}׺b-m!)ǟ'Sfy^yqMM;Ұ^{D#)2LBssYw.IPt9gVJ)/-92ؕ2%hv44mײ= <>%%eIv}zJI%Pvh^Wٰ'YAd)CJPvhz%aVq?ivAC4ιShgNAx[R;햛+ {a@;߷~[ʟyPP+῕oe+ܛ!4 !O/q /?       2F˜ZݠIA.Q^Ո USU+ְ}QYl.-A).н\* Q B(nI7ϗѾCAڷtP%.~Fzɝ5yt|/hFnqCհ몆trukv]Y}{A}uˋj†UO 1fõ3FєU=uC_Ԍk^Y=ʊW'Eѫ\Uʢ~/ ű a(ϷоzPj)퍟GZ8v䵵~[3ʁWv?,Z2X샃&jInT=_=uha&fb$U77/|.ɺ"9 zD"Dՠ28䁮LSՓWO~ؚ jz֏w^nE[ Smf"Ӧ jijɡ(T)|BW=1zBk{=zsQUu/;QlGQL<$m5 +S9O>zUj]Տ{kmBWpשO?vXwCb1;( :9j~u07zKF4QK^G\9[尫*sUŕ}/,>^((:|7ξwmr!~wkZo t;{~YozaYÏzCD޼[.^{}{/]ۣG=:/-& nQNX=6;G->qE;5i1jlܢ.(̞i͙%yM8)>ŠQǙҫ20{>U7(ȠR\⊄E(Y~{BtDAt$ Ҿ["*.T@9CFoz,\ A"D@"/g9VK_lp= 1wo (qQ8 *D()|bɒ% .LOO?y\Thjj̙3=TjH!FclG֬ɮJJl3fEŋo!ŢE]b;rEORC $ Aavff{e_ロzuUorevJJJ )$(hd N662kjlԀ [inR[k[bIQ[T5ӯhX]m#PTd6mJ*zA A[][rrJhHІ@HtϫN]k&|鱍c@QtuOs|׭[muk.SRVBamԉd7:aYg[H Qdӷm= b{mwImmBAA; x7i*4#-XVSSE}94-$(@ܺ__?ml?]=~=m'OPA'I/P! B"4AE(BD:x.зK? Ȳ#{( W(?D: Q"t\}Q D(@AB{(xw!D:B#+FAW'y7مEQEQ|Q*D:# 78pyrS̨bhmۖ/TPSۀQ*$^UHyU! I_2^ IUYAҙ#7C{b#}V=?>Y*)LYkf 5mZ?)MOtPysW]`˩gs1i{V.ۀ_ׯ};4jp^`8UzXf=k>:gi9LFx(^/evr>m }sʞj6SZ0ϸ/8aY9Wᮙ'jxU! BU$ϫ IW?*$`BU$ϫ IΫ IW ˻Jg4LW?8 o]QJ#7+sWwۥN0 ;ȥN5wbTq?j; ꚣ[; louZ#wy50GWKϡ}sއaS'p叟;^ckQGj6*OAa7-Ưʯ?cSS?`OH@秈;9T! BU$ϫ IW=D8H+W?_| +W?_|@?2[?_? _?2[ V+῕^ p)g~6)L ݻoE4p! ῁)|ombğ4RۆLVUtBvu%Oò?_| &wk]&C4e? qI.N3ޞ7o㚿 ElTǻ38s>H78SewZ|'p!ﯜ}ɮw .bOY ]K{x3eiinp fϋo +W?_"O?_|}zRif9O,/^O }3.xŻ^/>e.dJ.%_ s_Y`7NWD8py1"y߆2;$|׹QxSvS•o?_Iw+W?_|     S O\8HrG J&@AAAAAA9~p ,_                           'q8\8HrG J&@AAAAAA9~p ,_                           'q8\8HrG J&@AAAAAA9~p ,_    rwiB|ЧXJ&!Б(B QEQEQEQEQ,E_b50O"t(BG"t(BG"t(BG"t(BG"t(BG"t(BG"t(BG"td(ܭ QXǖݻs Ql߽{KGAx9B< /(X|yQ,@Q D-> (@Q C}"@"_ DaPUA}f@QX6 7v?pyrS̨KܴނFi% ss ;Ҳu((Z2ue*+'|Xa:)BY&5j673RB|W[q:¾I)&;PG$u- YXO}u}?0 V&~ +WLd^DaV+^=[r"| )g~6)/fޒW9~῕oe?2lj N'x߈kA9`6[:\ZQBg ߢoe?2[ ?+῕oe?2[ V+῕UI]pGo"?_2~{ҧ?}O/֪?Y,j_0 WCmr  Y' >       (r~ hpT8.\8S̀'}P}ܶ-0Lj]5]WB,*ojx_o1o?  '?gq,        8}S~p6s9O. @#7)jbsh^0 }Ƨ6\Hߊ5 [#  Y' >       (r~ hpT8.\8S),UVciAhVPkp(:C5jLuY 3%zFyɿ' ca՝8|̣klrٷ$Npvt N[OJ!n>#k<~#u5-^W[n4dx⽚o^S#8vȑںjO-7Bs.1;E9K#]#--MMkjJ߼:hUYV ߛ=)VmֆSғ__s__W^_ZA]ƻǏן:ɿxpGk9yGN`lh]Wj1qjoow<փAp&NU02Q Qo7 @(ED D(@«(ٹPD(@Q[a[~f((v( ޽%((/xGlr,x_E! D("# }8F|ШD6G .Qp= 1%"t(\?r"hg W(A8 Qhv4QۑߤFa1'젍/ (x2~V6 \/?'k@tU.kg<]`yCBpu!evtr@g7({,!{)}#~*>|t9i ϧ )b6!s%=<Q?%E}<.!n̓Q{\nlҲÊL}ԶikpijԮSDBR z*6 ݣ땷S?Yy&Ey0{szšlg},dQzeKgBldZ>6YVpX'$k0a3Sy{Zg[9  Oɱ(?gjͺb"|˓?!E蠛i~>yP}-Gas|yOm+O+?Y<}.XO =,\s } ԯS3t6np-u?6["VK3 `a5Nl /3O<Ԍ?$?#I}~?<3ӻfx#.p1{&\&!d5L#?k:RZLq.K|T)Ż)n:i u>IKofGgy?` 8KϽѪ_G{|[5]r}ySq 8_"e+q(Ai0^d1 ^F2!%+EY J|#(z<;ڙKѲ:?a."xFcLߑQ뜟u+Jꏶ>#guaϙڨ.~hk#{-wtX{ZG]H^Fӿ/gc'_bߓm6Az&)47xg`!rξ%1GMߔ;<R|J-=B/.L[n:KDe_nHx:A?tna#g-jӍMG?|8&^8>OTG]1IOY_CLl);<4k_1Es)-+kFwiyÆ/nƑҿ1vʼB_+Q2>hûyó0o`x:ddSl[Tx]`$OA "(4 `(4GA@*CQHBBHB^77{{{˥]K2?߱;;;;|Sv6av~"3wk y $MS/̜J f|[µs´+h8w#W{WYm֬2?c?ˣל{9iܠW?hK a^: fk̹1=-1Cnۅ{WXw gu*o-q}wxz"->>>>>> >>>> |:ęYw9y@|B"8QR2r{pU5?7? 9 &W-; {n W~oCGG''??<7%eU5x:<߄G#Q[hx <~ dO'p\ Wp < O3p-< ρ8:3u K1pI@sg+~? !z>n.ޤkV&1J!j-tWzW qx mQ2ځfO~Zذƨ Sa1,5SF\S1,n6j/[Ϟ=r)L4ǮM$M*OI!UQ"$Աwd(WbiΠLM]*=#AILF3yyiΠT@ Pgd$iRn'Q.Y9Z?eZEwqz,p+ssԣ yyj#}X^ֽ(wrJj\‚kR^in .6{uJݻ}IeZ9\?o]gߝfjDm]Ta9VhuW<+_PXXXTT\RRZZsޏqTY*=ܜ8Γi> - 'J*;Éaͥ+*..Dބ2  DB:ot}9GLe,U1=| ڵ+GO&y?t{!<+߾__?v_&W eBQ{7)ΪUFoaw6) d۱ZƄHG6)39*4rAiѢ8,As戧W2o!~j1ɛIFWcr9zjjfӼ]l*33~ܙoܘlOƗv !-Z/ȷl@226m;v;t :U8ӓoԈ_Hɺh8Ņ_F6|8?r!': 2w?}njFڤ"]7}ܶӲө1iD1>'_zJە8R ppݽgSSiJ$oNқ$7;h<S>>D5&!{wB7l(hBI64qvMЌl-[nN霍Ml'&رf?6mܡ՞gɶK茵3┽:)Ҭ8qݿO*WʓLqL MeͦZbiUr3 3A>7l?AA-c)B瓤BCnVdN ۋN/-6%yZ4TI-{LxF69Tֻ[lf#,.ߵ :yG kˆ94_ [#('ܜ,iL樖/ |*?ghYɯ+bɓ09"kV$a:J-$H5& ieXl,O F W<;Ջ>l8\&R-W|$roDzLi|Z:ֈ>>ӓC46!&7sz#\b '~Xd"/23H&T)e/'NchT$y/Uӂ+bz beA nr?6Z7ܯJ zܱf%1{l'd͊ZG;ǎ۶#"*M#6l k!~]g7Lh>{7fy$*s}lpxry'k2΢8Ƕ1-3>YEYY~{F]/ZCnWjh; ivfZИlk}U{]6^fef1eہiٞƴ`;0-zvôlGcZfi]00-g_Z}ffA5_/k7Z1v53ƴ nk>udS"{EΥ߳>|(/FZ*~Q"9jDZO~M"¯WO_?UtS!NjC~.[3o-KULaO-'%7;v?CU"%o"?} ?F3'ȉԨ s>- ) ҥGd}&%j)8I$Xa8[6$tIIQ|l7B!g|Zl-b;kyv\ww^"?ROx[\ee _|Aw*l#ZN$͗_&/hNqNN&3qs#Vm׎8kJhӲ!ߛ "%8 98-]=6o%-b{jU"a PKZiiSN^>n />4۵#ݲ1r}{2oHނ;cAM?䑓{CTH Ӧ86F?S -;qd-a`ݰLU'jX 1Eu4jz-KLpRI-}[/;MIB.ƑQjnՊx7 nu_H7+ӲDtP1g.OLkF يmD5<G~؝G}/ձ.~.cd$tKl;*LJɛ ?%o7ٵ{X>},{*袘wSC@3Aʻ 3(% 3݁Rhw ~B"b¿iTqspG"xz#ip42Oi"c)f, e@Zq*hv'&gjr)ɯOw":YqX3ՏnrJK8W\/݅iNՆR%Mm?go6{9Qh>D8ċNNwIb`U4W͗NwV_8}p?|K:4y~]7*CS̎hިdqE1=jfӔfh i˦P]\_|CLJ(o^b|׫`<"wӈޜ|B.Bf(iYI|#Ӳj]he1?/o[|}F; ~Mb2[;N <Ơ"wl' )w7z֐fRL/![#ILdcs&Heኖ>ÒZj Y/C8eL>&ղƯ ZϜQnk>"؉!].1k] DtseJ̺CjF)aE: Svw| >|q隅r$rJ~|^7$iҧr#inb7Qsr)֍%|:9|HK>>iBr7 CIK=9iTa> djϏHnVraI$}+ǻ-ChW253-D- !T\k lQIjAQ==6 \_g;&v!s!L4hjw.r(ye:.'0 ZVN(0?^a4ě4h#|NBȪyr0=S'%vqٸl7/t%o5]ݘÅr7qn,tȅ|N_hǻS@)vCFqX)Krɏ0sQ㖨6J|w[FkSS_=_9bblrwO 1[be;r{}o Uŭ~Ʊ+}#zAbNiT5uO2|\6f|޶/Z(zEHUV^\5~j|Ű;';)LS2-@5|œ -ds F24\z F3mɵL!-QeeoGߤ/<%pBB5ejR՘EJ@r ]dmuzP#Ѯi r,Nk*j2m'H=*g妙q E?M_[ P )d̨N%5) xeL"yTb{3^a&d=] BrTf,7]LtQNr.,BSI'WDc՛g~9B}23;VmX2ӑi>vZ 2TTڹnGo_PAnd5|d .to`57m9 Zƴ #Dh۽% Ks]@t& !P6!Ë@xs#D[wɚJ~OvG"/ko9aZsf5~!I~e?A5A/i CBQئ/]BDf }c8$녭 @d`L+3l7$ڇ`FpXȪCpB,Zgy@0V6) $u9 [klX8SQ $}VE?gRtP?Wt8& 0!ʔ=O St?KE‡~T5ZHCC dvf^/̍3o=.P={2f5s5m-^yM̅4>;!~?x#(1 1xSx3xsx 'O[Ÿ?  , 9x[x;/;_w  *5xwx^>77^pox@ `P0|8-/||$||4>>>>>>> >>>> |:||&||6]x||.|<> ||%||5| }?O?%< |3| |+k6v7wwG{? 0wQ1q I)igg// ~ ~~~~ o7Hx<߆2x<O x <Og3Zx<R|AsIHp2"*2K'G q%3 6$OUG HfcEzV^\N[1 H:ɬ2, "-WƍJ&$sSm 7EMPrdcR ΅*)D) f4\Td%Ŀ |?93&>[ V.h灅:&ws bHZx1T4 }72#fQ.sFťnxnS 4{`.sI!?4o:ijZ Rc -C%u # e8+O0)ɠvj ÿI"Љa:S0T5YE%+u3R~r'$RkE6 _qcV=:x/[`y,8[fF;Nr'-49W970n E }J>&Bt.7*L_7"z|ܩS7cOǒO'n %<yj8u 'Q {,z'д]G";fM)D[p+y1k\8hJ_mƞxUnwD yK*h?HS)2v~.Xbr4Dkt\> }=%̄ ҳ$m|ѡ#5ɘ<,h#L"? ޡ>?n(Bx ߱4KKےhw< =LZ}q!IF]etշBRSN{=w])rČ?Gsz圾P.`0"7ZC"+O<%rQ^3Qgs)]kZᬟݫ?oG%7-M"X/-l@7>6}]!^P眣l{qu/u\8$\exȠCH9g:spc4Mkȳ eRapy^eSτs}~"> S` Yr쒐n<@"y47ǀ# oLxuȍ:(}/B7k:C|]^qG&B.=FW4u+'b 16OE7NEns/j8B"}(ZՆH G;nO?O,Zpi;ّ(HPDy[?:EWne"tBjbn z R8L[gȿ%O'#aG  %NQIS0P0]|(ȷQMF%)p2Ȍ +̂F KNt*B9\f~wR _q?_w,<0]9vv,el/c;֜[{(׿O'\$%85$uGsXxTĿk+LC6mJh"X.:btZLAzx~yz]O_~Rw65X-)/D?6:ʣߴ!drsiF9TFO*3FG=W !cv*b͵k0σC#&p=HO$'KkghVZ楨^/XQ藽9ReZU6BPdڛJ;gu*MHZWu)fCL_]hVȿMai_Wof__wz~Iw5ʞ:Cg`;20gg0 33Q1:1v͎6cOl#!oց3 ?z:8>!' Gȼbb={*o(;S.OP*4Ej:##ȟ>܃V`bo7{n'&ޕU*9J*ߖіʒfdgkh*J|V+$o參Ŀ-%?Quw_^sCV qʐ9mg.m~_7%N|p,Ӗ"Gx ?v_XXprHO 7PB#4"~+ZՇ/|dԥHi$3Jį!ה}3 A7zzwv_EEٹy#9.)IGC !Typ#ɕMLP吱=5AlNvO&_=6_RR|jQ7 Mȸ:XN5Vi/..:s5ڨ jMr9i.K}SMZ:v헷gJX~%[y2 ຘjG~iiI,7T!Dlڱ9eі\1c`#L.HZ BP+7L5͒_6oe*$kh1,KG{MeԷm C y>eɯ(oz/Gdiwp5&`EiWviV[[ۗ1> y?Eؒ_ogg0}IGZC8}_X w.qQm*(Nݫ(JWay?2H[cq 4y߫Á XN5{·֝Hd %Zu__-{uJ]rib+,cGO :+(..B2+,?SD 7'+3]re*,3_P_PW7MO(,/-3🗗Cv@nn63%Ehiy,"kog_G-[i_pj35)#$Vĭ8ͷݣuU{k7 -k^"''33CQ,o(c9@(IHW_E&G쌌tZaiy1VC$9(H 78 ["w-cde)Ux KgVC&oMFq-s9ӣVCVVZzB%kCy/h~Ĝ[9- oœ =ُ99 ?MUeeEEyBYYڌ}ܱ`33 PTArZ H 2pOn6OY&|4F& .dg x#1.Etn|U6 B&q Av$|{#|9VmF@HMFghA|%A RlhJw:.8\8gt.Jvj)@,Ũ_ _Zs5j1I37U-OdB>]/4Yń(-Yڰ}ўgnR%ĥ|X_O#9(7NH H 2q jFt | Np;5T؟Gg~,㇠̽[ҿ( g?^=MbjEcكߐu rMrq4'wD3GܷM]S7Ltygޟ*Pz[|jxax%>/4%/1+"? dPMdyR>YY^3]FgBz$7&y5|X B[]rvIC|$=#S7Hݼ_HBW%<}@TϤ=@x~K?)kw8(cM|1:&OǼ*'|)~d>egr/I )bJ'ikbH};Ft[jAw,b-7__>LT^Ny.q賉>me/D {Oru%kQCb- oI{czC&9&W%3$#O~MҰVI>&6rHۃ)TX0+?D[y#Tƪ0Y8V8f 3/Q>Yrg`f\=c 7aVdI- o\0ZpjC>&WSlR2iX˨-Ҙ&?Y`LyKxEsyQb3Ѝ{Z*'>-f߻_Nq_9$0M6mҔ>Ys+yV~S9ja}\5Y&oS$O%|JwʉUc۪G?_({+۹Hwۙw p-÷Zȗ^}UE6K˜N~GM}F>q&r6p2KròIN~yU O+ Z&>:t`ЁB<6͍?7Ekvߍ.;p~7߉gޔ|=3eGnŇw~.u}l_6k]9;=p%nvyq{?Y[[w|os&Ef~USKfi2NοtŜN>aC LDGtCh2'=N 2GL2+C= !C#Z |bx:@?8?@zrqM>s4)ba0?맾g33S`3-N^Uӌ"䆬Jt_7υg矾sjމ+KG= l˿F/]ʯ_oq׷tO1K ?As/Vq/"Tv+1਒7K\/:u9Fb7.T%|Trn)h_. , I#C-2OQt;."gYe~/m<twt~Tp~:8RL7*oy3e-3_?viy#A>hrBW@M i<_.nǯ! *NÿT,}wIw杌i K󥴋 @t Z-:|g_^#S} D,>{SXHѱ_ewZp?yXaa+H(=XEG.VĿo>JoaQνi?qȍ!~ V:oo+S uB8YRo|o ]oW 6|BΉ; z %u@\?Lkoekf;N*8SE`-|U5>qkdtkwcy9`3-)1S`oߦp0d hX7)r^E)o/Ubo72Gń~{5;ڌ7>^-0kN\߳7EqW wJAo;'NL+'TTB&~GLC%h֌ܙlZ.d˒f+ G''ڴ?yt;f~q UF+/G? Pf}~y睺8fWC>EF~ٛ3ElΗYY;"f;p# +'qOH&Z0۰=AKح*+_ 'aas|hՠk ж1)3WڿdP"IHw+UvYZzoTehULzua0ͳL7E_{Ol-8K\b\vjׂIk;,%KD9Z?<+$RNQ6pu\;סpu߇Ku(_wxQS q:"/l;E]ī$ܹxjru2 ڎf>{Ur8P[g}߫ VzcץibM쿿+))(..BG_/**(L׀tY㿠 V#??7[QR-[unnn63%EXi,//j8hcU>,snnVIQ%R^xR{raF{j(eҸkkixV/tGj|!h >@8eNBN HW";n!y\]4.>-#b:HYHTk-E&oMfɉ//J ++-=M߭#@sKq`DmUNhIHզʕw_ڴ#b'QG#"ģ^m1D+lmb*DDffjj\T{OtU9z2/ncC<.grJH#Mp4̘1cƌ3f̘1cƌ3f̘1cƌ3f̘1cƌ3fbc*G,68wspU˖W_[ZHﭖ:zoӆ;y s\/'Oeٴ,(p`glr9IZ|<Ⴐ=nN[/^_zҹ3./Z]]>}xB8ѐ#H}~rLY&Fye),;v6Ǐ!01Q/k)e c|N|֭ƍ3f2ܴ)-e({!|4g&א dI A=1_\l,&\IvI Pk>C~ZC&M{kZѐ1<`BZٲTB8:6wj(' e{/E_y5?i11fŲ4n\nYv4#!$% صFe=41Ԏ!׺|eA QR ge.Р fdԨ W j!-E A+ ![7wo!#ɟգNL mNXBRIbĸIzeA4y4-Kr2߶-=FT\QQMC޼|z.vyn+b0aر((…iihH!,L' \qRA=+fV}Yq:ʌ3f̘9a(YaX;w|XS^%մ=%s7#Sݻ'FE5uk}Lz܄ZJ YLaAQmke *14 Çm %KM3j?@yew3&.'yV؄ 5dd֠dQ3s&C+VT,cǒe3!`,6a${Ӧ ](PΜΝ XK5VR߾ܹ(??FçxǣM^Y5ܮA?hGò24&Ҭp l(,*w۶*孷HW, i[7~*Oz VRԉ-ˠAF၁݇6zP\IZfٛj͔eT,yO <nj1SVz >t,#GjUOCssLҲ`loDG瓤BCM˂ŋ N_9Kڊ}Ktn| PwcÆBY =IYP(:7 x횰ֈį\IY23Iސ'7&ě;v-HӲ%whY0AЎɽn(͏'i #Y4D(Ț5B4zN;8D۠Ii2JrL͂f};l!>uGvtDH'2 ֩S];̌3f̘1cƌ3f̘1cƌ3f̘1cƌ3f̘1cƌ3f̘[G8scw|%u^V189lp1~}&9$̘1_]&/Y%v;;=Lzxra=e[1{ŝl#(R})Քp؊~n]4uZ5Z8"~9߿tmѐ3#86>h\dNFtVIʆpK+cSe? e_dxGFR T rkZKYtCg8}>g%e 4]HYv?lS&]]6kt |otrC|"ꓸK[?:$>M_iKo\NHOHGfr^ƻ}p"F1'4Sh^Zjiy3=rGW~_dЧT3yprnָ='h-xvꯛҶI:3p2p宿:ztC=kSNmkk9oOfnktYZJ[!| ۟\~҅[n6}?bĉM#WߖrpEtF{(띃sq.D\:R\NWJ\хooV@O܆qD/܅胻[܃Հ N[=ax 细\)8a]v&.jOu?+k?0]uTfKOQƛw~۴A]7yhgT^מ>:l4=A6gp `P A !b$>)F3q_b<50S=~TLtLl`~\|,B,b,R,rbVbVc ?!!؀Pl&l![mD1Ev؅x$ IHnA RtEa?2a(;9EQB%8RQJT0QZQCрC?FY8+sqp.%Kq:r\+q5z܀_3nM肛tE7t-=v܁; w7nw^܇~ x1~x`!a1`)a9+ AXuXl@(6b6c 6D" ؎";`NB<$$c7 HC:"0"(@!PD)p@%pըA-(hqXI OpF{ \q.D\q :R\NWJ\q u7qf] q nEmwN]>=c@` x1c('$)h|1c1_K70a&c iYyXEX%XeX@JjA'c#؄؂0c+" B4#$" ؍=HA*Ґ>G&8,d#C> P"Q2B9*P*F5jP#8cC=p(Ɉv8 g=~sp..Ÿq).C'\+p%ո:\ktƍ ]p3~z6܎;p'z.F܍}{܋1c Axb0a|/% _| 0 1L4L ,FG<,",,2,G V`%Va5 k FlflA±؆HD!XaL؉]Gd iH^d`#f@\!(DQ(ED51ԡ 8 FY8+sqp.%Kq:r\+q5z܀_3nM肛tE7t-=v܁; w7nw^܇~ x1~x`!a1`)a9+ AXuXl@(6b6c 6D" ؎";`NB<$$c7 HC:"0"(@!PD)p@%pըA-(hqXQgD;98|\ b\.Wj\kq 5:F܄.AWtCw܂[=qnpzoq~=}1 <1C0x ? 'O1a >X?| Lwɘbcfbf#s#bcbc b#+k Xb#6a3 ؊lC$A,&.#HB2vcR4c/2 3 A.(A P T QPʛFY8+sqp.%Kq:r\+q5z܀_3nM肛tE7t-=v܁; w7nw^܇~ x1~x`!a1`)a9+ AXuXl@(6b6c 6D" ؎";`NB<$$c7 HC:"0"(@!PD)p@%pըA-(hqX|@Ljv8 g=~sp..Ÿq).C'\+p%ո:\ktƍ ]p3~z6܎;p'z.F܍}{܋1c Axb0a|/% _| 0 1L4L ,FG<,",,2,G V`%Va5 k FlflA±؆HD!XaL؉]Gd iH^d`#f@\!(DQ(ED51ԡ 8 glǯpy8BtE#.eqUZ\q~θ7 noV@O܆qD/܅胻[܃ߡ/~{q?~ 0A !H| FSg1x_k|o1&a2{阁XXXX *a-~B0a=B؈M،-C8"(Dc;b8 ; H@"؃" ؋ ~dŒB6r<(B1Jp(!aT88:ԣa\#,8\tĥ p9 W\p=n7&t ܊p;腻}p7~{;q/C?܏<?<c#P OI Sxx/W /Ex+W^xom ]>>1>(| c9b+|o-&`"$L|0030010 K˰XXX O:G6  aVD`"hlG b0a'v! HD{T!{}؏LqYFr|E(F e8rTU8jԠGpPz48,P>oD;98|\ b\.Wj\kq 5:F܄.AWtCw܂[=qnpzoq~=}1 <1C0x ? 'O1a >X?| Lwɘbcfbf#s#bcbc b#+k Xb#6a3 ؊lC$A,&.#HB2vcR4c/2 3 A.(A P T QP/glǯpy8BtE#.eqUZ\q~ p?{s/C?3?`!CxQSjL_t=u^az9H`.yk޽ʃu CQ'*LͶͰ ؗk[Eyp,k߫[DZߌ 2jۏ ۞Qh[V;LS=ǀ )/Y^8Na9r nyMy 3;ޙ䘫>dʺ \֒uJOGr2*WƆTnP꘵ᘠ5krY2%|9yko{ J]>1ֶOY8VWoܱ]쳍O(Ǻ%6v|kӯB}!6O[ζO>3,gXΰa[ӗ6kSXu68gmhތǏIԼUu6۵>kc]vݱhژQE-[f{e~dZ33u3e!mIe:33&ȴ/rpL337y'u[7\9+gXΰ9g%&K[;C%mOB%oO^_.>δU%Xβ f[GcَIYIʶ/y,&3f;mXD d7a鸲u[vY3۱G?ݲazru[ΰng'VsoI歚m+[ Dc_|~w(>qmݮUVVŴ$oPgLNaW/0rcbW11+˘2uWF jc1**|ڷU,,21[S!Iki/,dJ!SoM&)*2Sᄁ)s}k*RG Vf)***M(t +,g[HVcEEV**-m_]m8Fh]۝m O+xc5x>'b{J.w9dq"gXΰa9ÿ3yUYYvӠg._~t?0ȑ#eS7,B~WT[qRbhGJvڱ˗(U\\^[U̬U&*!KbބůNع>D;a.딝lEYF ڗnuc_źG@c ٧;scbx}m%ƜI؀vL}iv>vgXTtӊKv_3t{VAÔ3,g n3y33o56o1|FPٻX맂+\$]OG8j 'ީ%pe$o]R[JZ瘲sR2k0YGז oy>1tA ea&'epYY7Xt [Pʊʶxp2׹eO&[*s~L]mqev7=fzt$-9 gXw8r ? ;n<x p[D0S-KB"l_yZm5T )ceVe;$5d8fqtpq%fi*ffDt ~a&i]3vݩ|L>-nصEa2mbC*_{Л7I֩}g-}i<ĔpSƘV#kR1(^s;l.8q0%xY=׼f-Y{Z-}~bK#L}i– w5ǩo,D$-"vyk%䭊JPTVTQS ~J*UZ'h+T:&UFֵ+۫TmRѶl:>jkp: m3ut:=1#p,;jk[ӎtGj; Mkۗ3,gXΰqr*6~ծ[sn| T;ZaSc@]v3ؔa9.>6 EᶜkkõʵYy]ߺuqexn} /GIZjm&Hׯ-a]"`S~^ŋ)0sjԉK[YJRrOPSW]Z\X[ثV{pZZunuT}PʾJj6mmӴh?juj=ZZ':k[UW[@k蟺^ Tΰa->p[[uu 4OcU5孮 SQ\ǚfzfkeo,fvScvlWG+x?K8?m굥'kkҾHm[կPoN}n .{p9؆cu:8խDڦ7R_t"tOþzc3,gXΰ6}ljAejS{~dy>w_69nS_[-[a$=.iɺuNclm(u{.>x 'y9rfF+gi{jlgix}"o5;wm1#pДV:#]wSA_a.Aςn`TcO+<)uƸ^死9*̔rK![E×nko5t[-BJXe/B7m0YgobuYow?^3s,U;.Ws5VYѮ3ȴ h3_R[FOiQfDu4imֵD~~kֆgz-CiFvښv[ s?cVzVn|ھ4KtbJ)icL|cy[@ӑnZzopa9͝~L6~Q0%xY=׼fO)d5J6@r 3fi7I٩vkm #rEЩ yI{;/څJ/Bk[]Q[icԺbaQFڅM)+YEn>P.8O̷Ģ| m ڱnMSiީ8;ڞ&GStm-Pe Q ړ_͵.u=62꘸. wl{r ahyy<2\@'3E{Gqm֩$@:p҉FK}\NQ1`&ws{O]bjkKh<'*Y.2b䁒)%l}cX?`#kU֏j.xbbDmљVJ]]gG)<8wKm[F h2qm46Ȕk.vnD2ĽfbDOnoˬun@'\+Mi?өnp:E8enBVSЄ.,]nݧFu;d@Ҏ%ꒉvo 軠Yv//}zbHKKK7U][K%))؛[0YGh7{y{Foۏtj0KmD5KO$ZKejOPOo[?ci?ii3UW__˭ m,ou]ld:43[+n.{xc5vk8e>Zᾮ(˛K]<@fq|^o4uMo,J-s>s[͚~*_aN"Ο{u/sS@r [Gz[M>%jqIޒ>U|*PLi?ii3zhE÷{32tKu ҌcC<;Opa5ygtw'~)ժyŤݻ)xgvjAejS{~dy>w_69nSS[-[IJ򖴴tkt[ͽ)o}vtiF64eo[ZV\-[ a>_D[Rm$oIy72)_giiՑ;𳋵[RgDIޒr5+o'&FHؒJN\m'oIɒ%j^jwNj-sV9?b⒒8*,[R^f学 --  Xi3L/J?oa_t/K򖔗K򖴴t ~;2{+{TuŁdti`G-X!ג+[R^.[-6Va[D,J/Z,t[c/󴨡joR'(ʬ.w H4~j8nq:~{{Q7v a}~,q?Y=| Sΐ%咼%--݂ny+=+7>m_lr$:1%ܔ1&- GT -7N?&TuƬk^SV>jOQ7YbSjoM-)/-iit[e JK[9ErL)s^kQ!%w2)Ʃqbz[z|e?,,ߟPe sIoMf;ZIޒruJ$--}VsϬUQ]mp X?޶p[碎VBt s/qޫsoo}͝Mت8Cޒ:-%yK%yKJJV߿mBo?fedž?_yvBžk:/0=ONMOIIޒrIޒ򅒼ꕝMy 3;ޙ䘫>dʺ NaJJ򖔗K򖔔/䭖Wsswnچ3 MٛqV:<-)/-))_([RRN%yK%yKJPi]l욨U.:2rmt4+O'[RgDIޒrIޒ"5; ܻWT/[R^.[RRPEieeF^޾|hi99%,5)"yK\2gex˨#&+.)i:ɒ%咼%% Xfgg甔ZoM̀-o'X2dHjnUVV4hrp7d0/ :r>:)%咼%% XdKKsN]D\y߿y3u^?t]q%f2~d,8:Q n:AIޒrIޒLkFsRTryhfJZ%zIH␭B57Of~!%}zlǀD6Ǭ37n?{i~ն;LS|'S\|}dC"bcyT#y5rE*=+7>m_lr&:1%ܔ1&- GT -7N?&TuƬkk RZ-wk7cf LbJuNC[.[R^.[RRP|5lF)D.[J*+rR2[&0,CJ^ce֕SS?'EK]Vt3{r2ETK򖔗K򖔔/bYPP\^Nk+TVUo[yyrUEu2jRcQxo[:C6]sUU&\)`ˎo&o2IBIޒrIޒR_,̅ ZYZRLfEԔWk֭SF,ãv 4wH\=|=JR#h7G~D䯦%-K ,.j0'Y\|b5l\R?2>ykΝ[uu64>V__USX0a5uigV \ƒkn758mqv%o}}],Yj2Rxi1Kzɗ-)/-))_(-o8ť[|#]wSA_a.Aςn`p/? -)/-iit0S-KB"l_yZm5T )ceVe;$5d8fqtpquWݩ6nhc>wit-)/-iit[Yib.щ)ᦤ1nqOG>bPhVt4w1]rGůÔ5f]\VŢyZ-7jw֦0UZ%yK%yKZZVYeRVNQqSJFcy܄ET?|`Hkr{̺r~qjX(5rˊ1o,YO )%tL:qIޒrIޒnAUQ]mp X?޶p[碎dʺ Nɾ$o|Iޒr5+o7'o郑~SdtD#x獤كTj5NV<%Xho,o9xHn^{X~]崩F 2+1 ŴdpE% u {ƮI }]90]k,o<ͦ,֤TԜ咢e*t8]"v}Ûp-6Y\[i7"=E.C{WK}c+%iM FF'vVsoI%yKܼf}W1LD}t>]"J`irqe#ڵDM[=ja>v}@TjHr|zL}[mEm|6lӯ韻[>=] s [sWZޒj%yKՂOyy[e=#lvScC݈ A*5i"Gn-F?15uua[>ծm\9؉WIyXOg׷RQkoi'幻<jhO>%ݴn;y׷Yc2jKJZ㨤N$oIyZ5o.ӴZUuaʳ"jXLl%njqJh.^)״H\)WܷsD*[R^.[ҭm'o5nZxX 7̉`R3{w6w Hn7a~HyKƮK([R^/ϧ)׷m@hL Ry݂4;N\sM^ǝ_JIޒrUKKKHXy_«_1Y<MC [͒%jV OL%-sWNjwNy;`O0bF){3?J'ղ%j^jwNޒjuJVqܲCfWVSD Nj-3$oIy$oIKK7T,[R^.[-h[-e3'QGLW\RG%u%yK%yKZZCyUYYvӠg._~t?0ȑ#褜J򖔗K򖴴t wo{\~gu뎕ٽ~*K2:e4YqtЍ,g!u%咼\IKZh"ZVn^8d+h}mLӢn_HI+,1 Eq& 1댣[쾮><0mԟQK˗oM{NUIޒrm՜u璒jtJʍOFNL 7%mo,ou {:-BK .:{ӏ";*~1ڿ皷ozif +u%yK%yKTcy`yyAi)y+8@)%}nKq "\>05=Vf]9?85CqRڿeEWQ~kzھT\$oII5V>* ΕaV~+\ԑ'"oM7)M䚸M_\vܼ3J򖔗K-)ʧVum-}dsboV^c׷n]2d}[C@bKQ&G o?%Mx?Na?sKudIޒrIޒ%%Xtު嶡&mw.>-EO[PJK=mi ЂKK I rĉ#'>[C|+C%YmwV#F,J~_gǧvMxê?*gDZv*)=;&7\y9qէyV^&q*$@"+PEQn{Eť&I$j]S 0# c$(&NrI(KKѩd^\2vvpV$0J+lXiQ\Z8?@ ѼSO6N}k 3WeyFs w]($@C͢x%wPLr SC뒨p^^]$1lJaźc2-+ryo=zҋS0OtHMoʢ2oSsOz߯d7lvWWo@"KL8FH`(&B12^0⮲K$DTy>o $"Pgw8pEl"zwӋX(-Y֬y$o(5-oEP}S q*-VXՠzmo{Jؙlwʾ9֙ 5䟀@"+0K~@&-ЂHd}.LJ@%-V~oi::7Kź*Y!|)Mza kGKAJƚ&&&[_~,x<NsK[[+NiO;:{˩?>z{itz}ڍ` (-)ry \Qw:XVѬ#'rܽ_%O/ݟ5pOZm;n:q[{k-9 lzc>Gy]烸Hh\2%EQar] R\[ SDVs^V;_t[?[t<✡:uk'쎊Yژq;|x_;H*G1ѸYLɤTf*-)ykXBեoܥP[DWL?Uh|1B$xg󉈜\(}G`d--P0 }-bFi[(5_By+Kru2#cP7dHdo`?AU ޾v;ji R@ x $w?)o-HdyA+_Ƿ@B[ h x $@ wbs㾔_;,!` Q[ o {^X=[=] zU7֤l=611ցY[[ ;E.o܋Kwݴӝwkj4zC:>ug| ,qx+Y;MŪ2S I$Z%E#r%82N"%82NBJKdPNejsf%A[䊺yDz f ?S7,yh|I{Һo;qӉK[Koɡ^7g3o>*m$MIQ;LG«J"-XlƳ$ q 8E3]h$؊1,{ yKX;&[?qEn+W?^|E2;ފMO ݗwי[S4?)fwTP΂ƌyjtwޚ@"KZ#CD#QNqXl4J-,],56!V-IN!.7ihGG{oui/w)Tx5Od m;wPE^AVN'`zq^F(* qHd[x21W+C0&DZ$\̩ 2t(#> yW.x r-H9Uy bH7={aoV|HWw)U)XpD[rHdyI/p[+NiO;:{˩?>z{itz}ڍ.@Y[`0/pBh%Wԝ+=Ux4Hf"wygɓEKtg ܓ}鎛N\ZzK9ޘAOQa^r|U*`ȥܩf'n9uG [-tふ6]:äQ[`0/|jT׫ǔ֪u˔x{+.6=[3t_z]gnM\QC9 ^3~[~3ՌR6g0[9x aox~O-oFYu[7ddclϯR;xk5B"v'x[ä( x+ ¼5^eQO+REdKM*\=VR֥c@s'`e4L&D9cnw[>HQ^(F.xY0lUvXbV![T% YXg hx ~@-bFFSa䎷n9sqvkݓÏarU;+YZ}JWn&r<%M3V§Q(:Hf "-5[ӪR֍EBֵ9 sU2q[:]hHp0D[[ ;E4oӣM+5d¯}̕%Ew\v>]E7 @" x x r-_߿sK/NR~-7~g{>Ȏ<%@" x x r'-VXՠzmo{Jؙlwʾ9֙ 5䟀@"+EQ|sE 82NሏPjI$\ Nħ81w+:uFHET,%XwU/$\7-X[ @%eXuj,pJ6o%k\$Q-Wb" \$͍aS yKX;&X:`/$Ky[\.,956!V-IN!fy8$-Ђ|>$[h Ac)Jy[\O#S+cPɲ}X,<@H-MG})XwpYBr@ @"+#b,`X-AxJ*yȩ@PEx&:'XaOHE(og/,ޞ݊i.*kRNm(^m:GoDVP%x=^ 2q֩AV +F"x$+4[ ּ-__{rTqsc~QMFoHקݸ}B݊y,|"Hd0;kx1}2 AHӼ-VrEcYG d)rwx @}XKs6@ ѴxRV^XTZWԝ/S㭟xDnq}}w5Eszbvc,xm̸>oɉMH!JJ\xUhxNsq +& ns-2#ȝxO> BSeZ x  o CC=]] U;޺@MtSsG%Y)rTџWПӉ%^qQ^xw 0n .^.I>[[ /B-H9Uy XQ8OP[[ ;-2[,i-)0nr[VT8;SL0靕,o>% q`衤Ix !a$r;y,w7#)\X/A5_By+Kru2Po`? P%V\ ^ċA\-HI]f\rbi9I`d<+O0M-:ev/-f0pzyjk \C%)d9r]i0 rUܨf&@aqr49\0tI}}ȼeX6[,Sa䎷/探ŗrZ7u$N$M1;&lA.^!N[/qGr+#,,4%.e.dkţPf$Ղ^.(KGl^ES31bh\lT3ZͺQTC9[m hIbW%yÓ}u&KHZ񄤅Oo<Y`)QaL擽 =5(azՕ#?op7*jy˿r Wq[S>g@s.\$޸)Y)P%+-9f_Ϧq^v51DGrrq:a_yp)>4ޙ@V@D%R\Y+ nWp*o\o aȘq8n0&&Mf33[9*`%X/جl䇃Q& - DEm}-BHdU |q>?He>>@<#a7E%*T\&!zDPx $[st0p t| Bd uhP}Ɔk߷U6\:äQ[`0x!9LǷ@ ,O E&.> `p0[+io>H槉V]ioVf2Uig&ONK׎ԧo C-꽳!ם̭g[k5}icyW-Rpz&*~>)KMRlϵgk34"!+`p0HBh%Wԝ+=Ux4Hf"6|Zfqstwv2qٜ)ܯ!qc}E'}SS侁W-3ŠQwqUs3rD;^{hp{iN\QQQ^ umS,Exjsc!!:uⴡ x o`8qEn+Wi*_QwL鉷6f2ܛc_h%MkF,~|۹&\f!$6`FL,JݽS[NE ]f&Z{;Dv&{bᝒ\onluQnM*oБݬQWCYbqRf,;o欹#ǡqհ^?8:;4xK|Kj[.2UFfyCĎ0/ 13weqV";n@K@]*[DGj-u& fU2XG:\af`#M pQ[ꊙKzvbR_¾'&o˜]Ro -0C[>(-oh|@ I,(ץL]fuE";U~>RA[`0{[m~6[XTڜMeRҩ7OEl5܃kR]a+6䛪sU+tųc%=m(eGA!R!a՘VHX5&Pw~ݵ?d5- O-w΅Uc[][ P[[pG\HX5xtPx ?[ a`-0xx 佀x^A=x B[{ Zϋ?[`'u#F[(JRT,EQ<|ߒWC֋╔]8p?rvvܦ![ (ޢbd91dz{5b8DUpt @єdrPmÓE@A-{X8!7<>a~HS h!@˟\IZ@F ˉ-gyKSw_ܵ&Gb dLΗ]5hpq+QP 5KnMgPX:W= [d"sUqh1r[[_$@-KRx[d8Ԭr`:nlV;)Df#U3/dfOw@N4(9fs1(90oo@ 4611xJ\(ᘲm贋^NQ(#E=c=H*sJ w9¶+WvS+NI{)Յ\]\.Ǚ-^=]Ya_T~3y_S jZ7{x ䷀x^AzG/)R>H!]==8GQ(⣗zQG(!qREԓWG=dty!-J2HɣKY\3o?x^AZmr_ڧ}zO1{ecxcC+QK(*^C)P}}brNKZNJTO C iO[Z+a{vťbpz\&m+ݷR8%g'm 9&zX%5~͵wֹ @~KrUcSCCyYl`?_5/ T'vWȪ~nx :PMQRX|j%kRRqkz~h ﷷC6k`)5i?qoe攔p^'~*mvn;~]IqN PpW 󽇁@~+$UyYl"P32xhH֘a62A"ᒽ,L%Ԣe 5(N2l? ӗWQ H$ 5(Nie@aasaW% \V9,(ajXXSs\W-!Jr@ta-Cb7/AёѺŋGQԨ5F/[X\uzq$@#QkZkz~9}=[(]|j6b;GGx%5}ĖV4>Gsڿ.+7b.xL{܈Lqdp=l/L uJgE"S-ȩso A-c׳_*yNӿ ?Xk`SZ#\# Kב!YsNmtuDZ=hĥsN+FsGImuܐq\^6W@r=L͕/@~ x ֘Uh{-c[wiا?:FڜO4m.x~z6@k-ܪq\jՔK]`S'Xj;^| x [xD3 a ^`.icGhi[ @eq؇}Vk[ma.[=-ܣmk1-=xlυ.Bq7F^g7np@]=_z0{iOMZm +5Fix [>ƷoX/^y ^/|o  [ a-7{o`gB7C ^^`OBE[ a-PX{hؠ?,ۡ>~ [[o52 cmtfI?3[9!`08l|#jױҗ\8ޑ~$ijj*HQzM\|x 4G[r7Uq^(y1_ܲ%љCÏd{Fc DW4S_e(FacY䱲4CL_|fTf@sO_[[GKgZ8Vα]/|w=ݾ'sHy>R7 yːb2\=U}yYm0WMV,?e*;/9R֧g@so?j`p$xYŅ[^*c/}8w\|.XJ_x=-{>w{{y4 EEQV%EQ AQI#F%EEHJ*G1(2eR(rqXh)./ 0Rc3XjLsmYfV"2Pʺ} h򉷼DY,f|$Lӛ&zp3=z>ĕ$::~^F=ɥR(Fc0\ bR2/4ME3TR:f~ x)["Kc!ciAe=kN8m(@[`0GoyOWG?WH@EOYC-}yy@ 0o./c4v:&RluQ`ˢ*FnY=U)3e@s [x|1FcӦOhvp /eg`)gŽ+<ؼV9Kc 6[6@J73[9 x -ozs{x[RuZCW->xEQQx&i4^|"JH .(.l*2ͪ2df8!WU29 x -ooo除k?c<2y~E׻tӛtuK"]O:% /#;.]EQha4TT^.G e[/OIm 81VeE]N GLUXe[G89{`»9xn9t#s&/L¸-_^K_^E-oc]7c/#IWr|+(w1¼eεVYZer󈱐1l!PC h ot:ah7&&'f W~PCoԔق:`!»e1[ތou=&Lr|;S` Vӟuu5m^FPӚ<˥Kxb!*[ 1[9(582Y{PQ<ȽԊ8`D*%.%AiQ ]J.bQzNE(K]BV c$Jq!q %CHBwg;MFyX/G,׸O<q1XTf3z}#3sߝIO$7~~ڳ~(಍o5[:/ײ\-sc&!T[[!@~+=c$9(^CK$=y^. zT !Ӑrt=JezxF!dQx1͍g#sQJIYx8߲ޡp4%cw??\+R 5Tq⌼w?;x˛-=>6˞d"mBw_c~Śʋ"40oM+fzZ,]VvK} c vVKe.[}}U&bг$Sx^?QgFjlzKSeg+PL/ՈQz\`ZTn/[ML\xR .}#W}!;U~>RAn**}p9oet"rG=QQ=#S|qg@sPye*ރ{ *&\>n;/\TzhaQ!XBPEܩUn.Fh,8=+oe^z~<-nômEKf=*/^m}[RczywK.feGo:Ur!_zG 4G!~ucÆ1q`2ML&''Igf@sTy&c\&s)d @wgHp)-gмߚ\z˔^o=[>IvL/zst#ٞ} O,z #%|HdTG\qtděB|-oQWoDV`y"oƄ@x+lo{mլ+ԁ1=] x~tz g {Z}r t 3RR9JW%i4xإGe;0Hd<<9 -Ï f|$Lӛ&zp3=zvj<ĕ$::~^FUs{N{4{ ^Q^;:12ڌBr»wGs7JtezzZ!=O}t tX:4?ҧ>{RG0{,%GFIIb(\.J)"[oDVx G#cڇ/l>M^_"WR:,u{MX>9][U[ p7ۉDzj=9f"ŻLŻ_^/_Mn{n_Iwv~|1Q¼ܶR  5f,Y«d ޢQX.Y^ίƑnyk{˳<'6\8: 2u $K0llo5w\QJkUeJw-]kX— _t?iVZOwG/rt!椨$ v{%)a%g4\b, 3 @"ribE z7%8-/Vpvְ^?8:;430إoܥP2M f?;}v*sf&ngߔ=Jxf^F0*!A//|_SK"phUת_E[S3Mwk7SQ]oNeё]ǫW#XX (@}OÅ&?YNS\:o7w|pӺ; 6qwu Kq3 B*g7DȭĆGX^Ja|pY[`08)I}]=ѕo#_8@[Hdo`8,xoazӃ-a]iOK=ev LSVY[`08z_IUWmNO/e>;:ad2yYS x $`p0v?kL˙͌53ٯ} j#A x $`p0vf='WތN]mi x $`p0v(Ys G',_yKfj0!s8ϮYs7`08<_@B+t^鱬£YG2OyxMc?޺7nxwc+__q%--uM[s_ޑ2oTLimmo^%cp T^EM{` x $`;(muM|E2;2__w_~f`(..^u|b,{-η{{]uq{K3yCtNoGyKhP"R* oI4Q( LGN[ ؼ5 vi/w)TXI/F1^Mٽkox)?}?(G UI [$<.Y<ȭ5(,-0-H9Uy X<";{uQ@'}O_#?V7Ȕ?/HJrѕ4I#[!e,$i@" x ayh2!aZ:=:tPBBd)-O'%1iii[n[Q^܄#7`~1F#OVzaVCR+Yyf5lx&tDag<5t&8l-6o-3za[,Sa䁷vܹo>hkO?<^q^Eur(D\RwK"q,ۺx9B$n<>ZoDVy?Ox*UUyθ\y!Q GWCG:3%Ʋl^ '[>#ٳi( _C|Y}}}jaao q~hAHd$_ZW1n2MLNNNMNOO7+.KbZ>S R/NB)qZS12t):FX|K >?f$u Qhe\<&YX[x΂øs% >YT[3gYe;/U"sߙk~֭x}%X [\@YEgu$rW¢S1*셞8M>i4% $KO&^tkȸ3 Ft:ԟBu0KAC'| g$ ZTUo_]yW__˱/^3/mf4,i x $[gBᾝ7v; R?y!,ť&$Ǡ'41^8"!P$p)uԑ˴?LaG )N!S.b,;]2C['=Yi^ 'O۹7-釮?=Nz;O^:׿y+- oZLYi{/oH7yN<\y\,=,nz|ثg-ӑȨJsNvC.sg[jU}`_[HKSSS; 7Y\^o`uy'- omx&M!cַA[ '3akyRV^FRZWԝ/S㭶[qժUW^n7l`26oތk۶m(o2ܯQoJTxjF.rP1c=AFn @"+#bְ^?8:;4xK|Kju[?@a*RhD599iXp를B9]aȅYG|3 @" x ayKg4RΪk`ŸX$it+^ť7niӦ-[8-9VX x $`&ɄHkl||`Tn|K1:._|ʕ.U\SSS3339[y뷒(9 |"*˅QX*u@" x ayloza OY,q;I@Ƿ~{Ŋr {ڰab۶$q4âU.S$|.sa0Hdo`?,6oKD˗/wuuuwwjirzn0~5/KrwsbP x $`o w[oo;x BZfڵk'''Ket>Ĺ!/-~8/?Y:/_Σ.[V#BHdo`?'Vds}VzS#hA x $`-񓥟 wrw}qK'o>19`+Y[`04@" x p<+ 56Ju. 3ȳ`wKoUd3Y&soC);?[[UNl훧V~Wr[CƐ x Vpn-)amǢ"VimmfZ._DI]|Rr Rw TOZ555eXfffxiZ//~KxnpR62ݵ O߄(bBl3qZ yXϏ-__VyåZ r^Кz\>­Yxk m`v0w]/%ฮ= o5]8O%*7+/~:Ajȟ\gĿXmӫLv&؝B+t^޷Qy 5wݕJ{k=5!5 BHy;@]HB !B y $1N <;Xr"mdc[~YF,ɒv;;3;;xߖ{ׇ*+{{ɺZR"_WCrԀ+*zDװ$'gNA깱!Eg?,g;f%dmPo'>\I~@o ucþ}@+&W658 O<Aa[tmyvӑC_J1s||UK(K3aW] zX*nضzCHɢ4E!+q6 :>GƜ>*SХ fLgըIfh~ZLJ UnYDv&-U5 喺J T}a9hܟ2dVT֣G+?ߚgɱfgC߫yGz3‡kr^e]oE@v}Pŵ?+Bl``~.T;^ [d0oɆY  a*1pv Z5^r\ ko@{&BjLi[R@! zVfi}}Pj-)lR ղ4CldEJ5XOo Lm4Z55fA1&j!VOWoo[w7&{GM}l,qlGF@u_WIJr:e-&n647zxЫƔ͏2HMH!'ٹ&GV$!/ꭰa+8. c^룊?32WZo n+X[>{5w> ZkOƷo=C%W?CwƷai @JcV zk&HS%T֗uTEe2B+Wo_[^ELc`^ U58o9'LV[4+~MCMl&^...iE{=OjbEWX9Ae A-(|ZEPTZfQ[vJv׊-..oc'[jV-!n(I[Nڪcz__[/}M)⸛*4͉UiK{/`LJ]p 2-z5Wd3-0ppx':{jZVp\dW}=-l }?u ?CZ:y6=dC!`S+?>#!A".[a}:i]\ýQ\gΜ]"ЦODwPV{7xuX[/=[ZyW9>cq1qMk%36?^Ѣ!RbXP1JںƖ_*1W&`.-璁:a}b`+[1cz!`K[yGnj Vs|nu:-7,2,@Vr:wWCbƎ(SuEz'AY!6hd3+ih &d4/O(x:Po@tVF$sh:N5^2Hڲ)J[ 8^ W K0~kk'_uh>Ʒhf.UfST񣼅t¶.eLѪcc1*ѩF_*(LִukbuEk#A_僭LJ U Sܰe  Pr{.Q_? [aG߁ h7^"c@uI=|bc׿w>X}oCK HS czߑ ?JڧT~XL8[PQtޗJ؞Ql 2v>U,֫(r9a[Q [+ļ".$..=8dȀz+X wO}[ FFF _{> |τOXءOU3٫x g[^+ӰgWp˞īJ1;(rhz26[Aqs$?^EoE(ꭰcpO$WQc mU9ޚ {ܬ\voyvW"h̠zjZ%zۂV&-Ԅ+脒ޚq WS[5a*T!z}`+:5WC _`Gun}yV&VF-:qz++:Rqpc߁ ݹZvڰV-+Vla[8K]YƩ 0@<®Hd"0>[- ꭠ}WML^KKyQ#;'?G[rYB"!0N[wZZC[WNXUeݵK4q|Vq"ssOmوQyE +Po!ު+1-)yBY\l"Po>Po@;ӻ/~D5-Vz D@H譠`4~Vx?Nk׻AϦDƧsM [aljW[C]>yAu'y^SsO[H$2FMo;bMCs syQ?e2ɬ 3'֣G99??.ͧRLJĠ ܬB'e ńz+(r}Pŵ?+B *#u[TQo!bzz{ۺAo5;jꛌf?eccd=2JJV2)knXUGo)$xh0MfP/e>0D@v4|$& TG!ou4_zKJ3-$[VádQEtj55VxF2Q\~(Yz7X$#$z+(oe׫$jw)V>0Uo)Rs%*@D"C`ax u6oY+- c::(z[Ko z H-X7n֟BXB]h$$CPoOYvq Gq9s&w@'qR>T z D"cƿ5tA9>z G΁az *+ пa!#C ' ^Pht !(#ꭠ}םN~4̡Xt:~V(QOCgIi*8zk7J|Zl&l7HVHdR.k-uŕxVϋ4&̊ 2sh=zT<1/Ϛr1ON|79AB-v5.)ⷮ lm.]J+| q [H$2U Emd﨩o2ZE#hr_}k+r57WXô/-+UP  Z; @l=WAo%ٻWD}r0Jo]O [H$2Ԅ+/; ڊtot]BA\\8zk5Po@D"#AM4!͝px}@- BH4.@z DoPji& HLwA1|Hd0|bϿ3&So "pWD"W-D 1QDPz+ -$2y2"/WL[("8H$28abb &搘z+9BJo҉D"> ̿9|E^:D>Sd0OUU]Dq#ˆ[!|ƌz D-mHU\fa!wARclJ Gop3-$ѿ h^鱖=@55d ueE6 ˦`}b6)UɈ[ F?}r|HLAm:DacHdD"kX.~:k76MOpLGg%EI@? udnpEջV?W᪓miU\n+)0M[&h=*Ogɱfg{[ljf&Po!-OlN[16J̾bffTh2ћB5!@?y+Ŵmd﨩o2"1&:uʚ+VU[LfyHdk-"$.MoYbֿE_34N~~ B[h T2HzKɨ.^H^!/}0.Q{0gCM5abNl>Cjz˫1[%0VTWqFqJ{OlhW*'p-BjW^[,Att/Df&nas]CļY  @L4Oԃ,ZbJ;-ccBCc@\g龱Ҝ\iz1< Kmh ՘'[Ѧ-h&$dX,dLYل.'(xdP\z6^mcEט-xy6Z2AP 뭢lg1si>BX ǿN --m9$ h5?0SuК 3__eiyF_"EQ"AĐBc"4X!` 4f)B,ћ}]Ti D$ޚGR.2FZ[h{R!**g\-*l(U EMv^of"-Rҩf®h[Zo3Т(6'OCg  ʺkh46܉^ofRM/n7IVx?BexyzKg"qEMiۊ,,$_.(~ۚM7[A>Mފ4-żyi-n<, |^!AAzKL%kIN ģPSC?? D=;Mi:"65Gi8 j[3qsz hy?_ܺ/[!&XyB?(6.d b6FޚzQDIhQoiֿuSo]o[u dM'ecpMi^]ߦ㳿; c)a?qVMCsrtœo4}^TOoL@i˳X-OMv+@?1UCґ#[И5"|-PoūB F9 _o? ]ͿC!4T"Ɩz}Zq״*wzz{ۺ[;5MF՟޲Xı1):ez ȩ[A^?.VRU3>C[Z[o_3d~~ @Mvzs5gzpFWJj|:K*Id Ѐz @F!m+- f::ȈzhD٫ޢê@i]境ySBJSC?kF,-0prhşKF-6k;E'ė4Vז {i/hQoivBĎK=N zSNeqGmܨ|_Yj)zkt:CV-(ʭT9){[]~DVgPo! -6#dJA~s'UIxrP[!44[H$2\:yí4w\IV{1BV6{a[@o!)`Fqs$?^]o![A#$BEz D@o)"mRgBi9D9B"![c1W5B"![F#r6z+r@D"C [XD魨υD5ĘC-4A[z+ڕ@ 4IF--demmV|('G%8^^;9Z- Z[oU\ jlź_"כ7vՁ_fgɅf-gu5z @FzLnm(һgƭ!X!TTEct::"'3OU#AH+Ѵ@~u_C~>?A5'\aTnAd#Po!_ТҮdv䕋w_T=t}wZvѣ֜kvWK%~BK ܄^3Ah͟RW\i9j:QXa7>/*&ܤvbm tz}M"@@ |AzKeccݝ? p//_ؽ{zbUU z+YHM=-2q@DB] jg*!d﨩o2o% OOڢ \M7 1yKo,ry( )Ya4E!+,4k :zhN3ŘomBKТފ ~G7p-tׄzKW@{! 1!z`o9'LV[^M+dALtJo`zKa?pd|=3 H%"̴rfqg T6q \TwF)Po!F!-Wg788X_Z*׃@E{aT[! -0prh%-jB!H[?Qa?,$$ 6Ѣ‹-& ›[~mBKТ- ]]]_=g]eZZKGXoAGɂ2AqYX["1sl|\9t GbU$`C=^^a?ԁ I$dk!~'[,!j,[CzKp身CKbG"WH#zD+'Uao@ofD[%Po!_Т(6'lC7 =ʺkh4FhP3M+{jF$:3g(XFɥ}B EM[n_fgɅzK3Ђ ĴLH~zġ.qlYW^y} @L5Po!_Т(@odc-)ۉxjjd>ϻBW||>Y11-@pÕG[5$ˣI'!VvZ*hOxHT|ΉN$AuĥB"v[&TwּȜY zf-gv@(_z 8_5Po!-OHz ~AofsUf'BiD}:X?D]\p/?j̙3'C!B-W`hWlT2Y>5Po!p"2xsGZI彏zi/hAobZݹnx&I4Q\o믿q^{q:#VUYwFЂޚ |^ڜtϾDI"z &x!K(TD\@z+(@5!GIb7 AxY @r[BЂ ٿuѵWwͫѳf|9e2DKoY|zEjz H+Ѵ@~u_C=譲2{z%%b{;u54/WM —2bf:~:/9|tuYVް~֜LՋx]i_˯*⨿&Ekd(}rjtŌRdtR|h{!zqifזx-BŞeQ?hQo]U\n+5(0M[&̊ P]Vz(r[99l_z6BtB6[0mAYEO%}MWΟ'wZ-@rWV,::K Zo [޾o^Kvq%?tٿ*[lR/${OgL7p0٨2l6[ɒ3О"z DU3it.X8z 2hQoU=]mݭ]M&OoY,Y":uʚ+VUGoȀz",rFˠ%zgzGo &to] [M-UhwR^m~>0Qr|NVZoGV>Cjzfů2jjf3rd,uqI-ԙhsma1 ˠ]Ӄ8# T!Z[j T7-ooͿoq͍|q̙`d~lrzPi- Z[Atcx8H94WϿe¯XD AofW'^*27 mvH1$ӞDĆp=bhAowny7z+ {ʟΥ$*$6,:T:\B,i2B#Eu|5hQos:e:NаBrߢ(Rk {ylDr~]C z+c۟^s~o5vuv?]v;#EBjijg@FޫMhr?eJ1^^=,^1vVz /]v& dU>B^Q49G +sGK ߺ_<~?uw7,kb @k-"Eo; t|cW!!-#<߃pd|eU/y[E)ˇ -qs$?^Eo!bTo9mt4iu6TW'[k-b譺BQ'R||a37w~ge_K"[":"[:1Yo6>OX(8Tqt̑:&Fÿ ${C 15@@ |Az+Fǃ/jɿ9ĥB"Vd[见m5Y-U=kzw6O~q+z m2ĐB"S8oENdtk)nPDb@D"C[u FNdhzT?}d+_8EE 6T1;I-6yΤǓNJz0IܷHdԶks\VK>t|:` vFb$k$1a[FJ化})|wJ+HV[XW_NO߽fͱ>8]XGo@o%g՟B&d خrXȉU "R-rh*y}xn1)_A"Z(Kqe n'_d ٿUuP5^_A\kyH.Ѳ/ު>H 1^{>_\㥗>]kھݜk޻WxydW!#(уzK#NoUT4>zkJ%zK:j6eI,*1*D#S4Z͝pxRo"԰Rb˱3&q+rU}Yy|'/dVmI89 &[GPz"u"&IW[,텤 'B"5Õ 匾ޚ!5 VK g:^ǩ9DrV1y?rqz+xD[W*9*%02ϕ_}`d=W~5ǼjH$2*$D-1!d krltxxpYc8<9Z1mb$kxBz+MTQU/Ke!ǁLiyy5D QZ*8"!O<2'vY>* չc {za!&i=~Řxxe5lv_*0- AE/AGȺm֨s5rS&Xܱj%.s\i9x9= Qa Oo2}U*Řxx1VB¬jkkV6tp|@pū,Z.VA+՝=;**ɝ=re.U| oW3k0o2ј5ƃƃ55r Yo;Y8 '啼)Fp%`PזNN7Oֹ2w @OQ'Sǂpcxd}R.+;U ^+Wyu-rXʮkx5xUƬa44ak8z)甴}xa[,+omJ0).yNRN) w9x:;{/Tʣ^ŕg&YêySTU=ms>OGd/m5{NqoOpVcEcc(H{9ȍFWdG_ꪆ# Y働C|Ǚi]YH< ШHNy&w)Q1\5%BbpM&V;(G\^+,+4=t Yo5a!ܼyUW57`苑kѦ5EMdYހp>Ahn&T$ܷM*2"md У\Rњ"ѕR /E\1XBW:CTMtĥhrɑ\Ms5y5kT|9b] {uuE㣹W5xY);\2:q,jZgܹu7ڍ)̝'`Uǟ1f<Й%*ziiEe\+(+mP6`Ap>CR7>ivNtI.ǣ|d%eIbCVtMLi-إg,I&=l3_~Ғ*JIhOfo4So/M0m +7)Y H.M*gT&i7CtS^!w0K/2Wu>iO>Y \\rpvg4G<%r<*M,45H4i W }74ӧϘC3`<`Q;ϸӧwΖw`2)3Di<(ɼw7%E%Y3*,<0{xҞx“x}d~7V6gvU?ٓ]hKp XշCH5zʕϝc\~dX}lϣ}},>wW<s**lj5̲d OaK5xWuU_ߜ96.s)bU'1f<;gc9HQU4{57k8zkdttT^$HQ+m){oI\qGiJ>oAK9RrY˥s;/++ŨvԵK$iiem]Ԯ_ vմ#2b ]Á7Xh̊FAìƜccJ r.w#{TdyNtpzI.ө<}aNs/I|d]Q'w'pN)]X$WIr|+;s|\qEa=Q8zD !,+g&NGEpɩ,p5xUƬ8'0p5fuƅq/[^[qE03>Ng?a<;u{qqL/9aU'1_J4XNm#&3%Ehh<"dYo!GM&a[HdWDNH4$B?DL4$B?DL4$B?DL4$B"H$,Qo!H$DFH$D"#K[H$D"Oo"@ пD"H$YWPo!H$Dj/>9D"Hddz D"H$2L,صRˣ^$D"C[H$D"ezKThJc9 ]A V@4 0S,]twi )Y f-3.@{Isg䯅ᯝnoCΩ4˃D"H$21E4SI1"%m[*B490ŽF`9Z^ d%EP-(U^qtH$D"^{<ʆO6uiqX8T4f)J8OD"H$f39[*i2zPi!H$ A-*'#pasN"f :Z٥?ICh/$un.MBՅD"H$?^{^%b=?4.=~t. ϳwRl0~/ H$LXBiG=!D"H$r ObzK=h x(H$Di4 $D"<D"HD"Hddz D"H$2DD"H$Yz/8_(Mt|ī9rL $2YfgB%†[+aҜH2O[DYPhZ 4.p{"H$2>X?w{?T7[% Wvgɤ!ki})ـ5_:Kw׍B-] =K;{Φ\(0%I{Hd|ӉD&~!풩.|y0p0t0| !l .tEkɚzno}K%Ks(e晔T!qMD"A wLt$]-}iT{u[r,I5rYQIi+<9#g|P&U]ķN[RfF˃D"H$2T/{{~4g?h.].K{Ɏx?]՟s.:GYI?;3Wu/-[ToѿJ^E+x:3֚f\6#BFQ: H$2T]Szq9+s+}?oϝe+7siuE…3S -5zCDoQQEo[QpQ|ZEg"H$'_Y5kO~/^\M?ō߼+u?AZ6-~;\Dru-9m"}N}tOr9{QkygWfZl3EXX7-s: .*n)8o:դxӜ}gtLchZvN-2h7C|iirȓ(u \5D"Z`G SI"f`~76润+hh-?䶶 nn΍o.<9Ӯh>dRo^,-xD+˾F3|Yſ\-d'^Q+o[G3Fw b{F(; +6Mp 1 tuw'CcwOEgb/tIg'ΧҎ C9FNm8  C \4w6Yox5OC|dbC |b{tz2 W.HD"!m~!UZ}ҷL7Su?vCmtK+i}3 ۿ]To5̒VȭFo:g-9ޙ=?IҶ[;|}7fy;%r7˲ctcdt8X@3wOuqތˁoݛAB*;x`Dٿ$&ީm k4#oy݅Wrq "H$22\$ZKnlˍnnյ`y,n;o]ߺ[s_9(͔hEުT³(vHJo;c[|~ b{~f#>5Ng}9@R3T,Z/!(!7O@zP'3 GOf.x DX׻n·AQfy N3ء>k:o)ǨC[ytZ>ʽ`-'-$F( dҹ.k:-2&7 D"LfQHzW6-zGo}ۮuAJ︱\?7|wa˷|gA5뗒뮐'R/Ec.[3ј68 Gv'C l()iڷޭl:_Y42Ӓy{}XgEzj _x"f lMWsr*RE"H$9elY$m'=Ouٯ>ڿL<">qQir`CoB&/$2peBYoOz]|`UDz<׾]ODj6eukÏ mɯ`-e"[ǯ> \ :J/ם$ax[lz+$DQo )`$nB_ە[{WWy;;l[k鑶^%͗,;bbr[zN8~mBobJ' ثL,F!D"Qo)#y*[0%O?ܶ 5ޖ=uC_Bq=d/\@\A=٫ZWqOHd|-D ~!S@S־HAҞ:׹Ud{ϼ1{-^^4N\җ p;Ψ' o [8|/"lma'M湄M.94_KF͟}yזt?,:u8*"$gE 3EXVUjX˰X^e-,`I%l5X6:,`y7a [/, ; {X>CXVX>cX>e,²%`ɂe,aɆe,{a~Xr`ɅsX%`ɇ0,G`)(,Er XRK9,aK%,UTb,Xj` K,'a9 zX`i fXZ`i vXt K,ݰr^X`eAX 2 ˗2 ,NXa9g<>>A]*Wt *t+bG:." E@$I)@ B„|nyM #=޻s=ech {A3h{C @8Cp(p8p?1g88N/p t8΄l8΅|.bZCh B;h7Wp\ w p#7-nv w] {_p/tC ݠ; x7<c8<OS4<s<^>"/+*C_xAa !00ނxFHxއC>O)|`|Kc+_x0&dSaL̄Y0~9 ̅y a,ER` ,e ia!2! V*Ȇȅ"/+*C_xAa !00ނxFHxއC>O)|`|Kc+_x0&dSaL̄Y0~9 ̅y a,ER` ,e ia!2! V*Ȇȅs/`4| c`,|k&$ S;~a*L0~0 f0~0a>,C , R! C: d@&dJXِa |(B(uJ ʡ6@%l*P! phMa/haoh7ppàG;=?'8 qp<'Ip2NSpgp pp\ @khmRh2 nf:­pw?N :p o]tz}p?<C0GQx 'Ix gYx ^ex^u o@?` 00x[6 | >s/`4| c`,|k&$ S;~a*L0~0 f0~0a>,C , R! C: d@&dJXِa |(B(uJ ʡ6@%l*P! p hMa/haoh7ppàG;=?'8 qp<'Ip2NSpgp pp\ @khmRh2 nf:­pw?N :p o]tz}p?<C0GQx 'Ix gYx ^ex^u o@?` 00x[6 | >s/`4| c`,|k&$ S;~a*L0~0 f0~0a>,C , R! C: d@&dJXِa |(B(uJ ʡ6@%l*P! phMa/haoh7ppàG;=?'8 qp<'Ip2NSpgp pp\ @khmRh2 nf:­pw?N :p o]tz}p?<C0GQx 'Ix gYx ^ex^u o@?` 00x[6 | >s/`4| c`,|k&$ S;~a*L0~0 f0~0a>,C , R! C: d@&dJXِa |(B(uJ ʡ6@%l*P! p0hMa/haoh7ppàG;=?'8 qp<'Ip2NSpgp pp\ @khmRh2 nf:­pw?N :p o]tz}p?<C0GQx 'Ix gYx ^ex^u o@?` 00x[6 | >s/`4| c`,|k&$ S;~a*L0~0 f0~0a>,C , R! C: d@&dJXِa |(B(uJ ʡ6@%l*P! phMa/haoh7ppàG;=?'8 qp<'Ip2NSpgp pp\ @khmRh2 nf:­pw?N :p o]tz}p?<C0GQx 'Ix gYx ^ex^u o@?` 00x[6 | >s/`4| c`,|k&$ S;~a*L0~0 f0~0a>,C , R! C: d@&dJXِa |(B(uJ ʡ6@%l*P! [^ }%o8- A+8#w{?Op x8Nd _48΀3,8΁s<8. ".¥ep9 :p%\W5p-p7p t[6pt B7='~xa <O< </ ހ~@a a& mxޅ0ރ>/| ?_hX 0&7-LI0w=?Ta3ăasa$|X a,XKaB,tXAȀLȂ !r!VX PP`=C BClJU 6Cja AP^ }%o8- A+8#w{?Op x8Nd _48΀3,8΁s<8. ".¥ep9 :p%\W5p-p7p t[6pt B7='~xa <O< </ ހ~@a a& mxޅ0ރ#>§|Q/a `| a|Da |#Li0fO0fl/0A2̇bH%A*rH Ȅ,X r `5PEC1@)A9TPj`3@M)͠9 -`Z~? 8¡p8~?QG8X8D8 N)p*N 8΂8΃.h m-\ =\x.VGZyv88DZnn" !܎*_5?pJO)XĐueVq*pNoX!e<QVI7vYt*cկGm:yb_+,N ⅐x !qU5:F׺H2jxuN/NnrYx Z-.H놵Z$Y딍n%nѪOV_M#R+[xc~PM!xbqGZiީ6+Km*s-\^d.kz]2E&T)͞7n)+쟋_'iZ00)<7-n_qa0#Kz o NpŨGuWYuo֘]K.+NewFfSzڢj'Q)[W-.xNcbh柁w?^9M;n/wԱU-h_q+pn>B7?lvT oy]fn|:RίU9{SEX4-[ 'ɲwZ#˛Kj풗uY@kޣbc1-.-F(,r{(Rg8f?fr?woLT͕@lpoAfQnGօ?cao=}캕c\~IY#WoqIf79}DcojemvF~ [rab\YXd^V>ʵqգ4:pBrW&:ɍ ɿP`F7H[<;iqDWW n娻ZʭL{^F?_#&ט4^T)H[-nm֩ZeiZ3dw _]&Sxe++EVfwWW~)1pLE$__XDe tFI:"r=nqhQ?8]χV:D{*CdغGF@58Y,lIbeÝ'kń\FvބG7&wE6_ֹUcwXA_/2Z9&StߒԽ:"@^줨 3^cTҟ;oԌʍQCĨ{u?O2ߴj)r*Ƀ[9aSasmǷ{UF|{\TEit?9#ntZ״ڦG-hޥy"G]V7QF}[Ws 0>-./@'rl3QaNnPCnN]3[ݾDfsg63X=]GL\.bdM\;whTs9+!zcȅ:$87]kmTW|%ڽ2Ts.i%JJvIl/']k GF`Q%gQzK. 0^8駀x4^(<$ŽȻ5ůU:?9O Q)PQ*HW{-n\Wj'1m۾pGgEUU_Ɇ￙gd)^ƿ6‹q\ɞHxg==Tc2"w'b/Scv7GO{~ݥ),#7]( 23$wHSQbUj㺫sG>:ۖ%ewSr)6x6< w\4Hu|o7'F/P_ŝČ\'! #w;07Ol64=߿*{ʼ)n2r~z+{v6ڲo"v׹F(,,Ƭx,'{ř-Ն#:vFXmNJvƚ LycS>?䞣řZis'Ώl6UC VN&2d i"u]yyZs5l#!P$2mBƙfICf/ !f 𐓅)8_ooEԏH!ozMMDZrWusKժV-{ 'j9BU-zdAK&6WW{ɌeTj,jE56X^J"fP17ū3<Ū6U*T6SU"dIFE5G6ǩ٪ '%lԼas-zkQnh1F[Wu3UnV+&*cxTnL{}71c}+"ҽ22zÆ ZpCܣ^kƍ[i6kTG+*X**dgܿMNsAAr,:m Hnnm?W53,f)eJHa[teo! P2n/4!tmܸhurG|O Tl/P_y#Q eeY؝ye0Eo?=T/&'oaMX_A96WFvAk_>[~5Hkr=?αOiiݼy5k~)U7eJpzJ4ϋXc$$棐eg,_|/i)?.}ghO2syΕNTg_DhFyH>p' CȿxSv\w%8mw탃]u[9C+T%|p܅2/,Q@So'u_{r((K>86H<<ֺ'/ wDb,]œ.^?N@ة*:׶o}r;8w ۍw w/&[' y7zVgm`׷߿o?7k;va:_k.AcעA1\Ʃ|nDcŻuO^]m}4{#Ixgts; 9'Ʊ?enUw'6Qomd<]r (O@"auV Y%TˬkV[%R7Z%R խ-4NϞu%~1P6Uf7vnĿ *78j+eggOk֩2jٹ*3uX\bi$&ݷf>-n"VVDM﫳J~4r5Zc@%Xc5V TREV T9ڦ` jOREņu%K2F(Ƭ8pOÆ${şsj63jt=Vl61WfD3j&uPϿ+T[rUԨǐ IGuTޢ?$+n4kI7eʐ9&Quc!؜\kFBz롐/?f7+ Nr|.!]6sL7^(.,d`NzCC홻 mEԏH!oFqNVJnƫ:rZ"39ZsEVSMlU˨6\YԊkmD̠֭ToVɰ*_M *ו枪*jw Eg׫g2L=S)[FQ]*+72Zn+<[FK(sTFF3[YuذQ [/"q7H*#hDgEpC*-Oͪ&6C裚J0FV]eztEځoƪTy߰!2XѨOe\k,wg5ZE%瓃2X|NKU^\@jweR2冫 ]ϿJR,pR6,z@c":n;u>C.UeY.[*g0"biEuл )1"]z+/?fׯ__98}(6^b# -7G*l+Q6G3T>\+,t Iw; e3Gp_̪Q)4#uZ  ]X(,.ҧ"oBk͘Y7Ɵ3dA|Yrn8m] 3׃TDW` W.f*P3`('7֖/CHz@TchN[?fXk ?樼 ]^=3jڢT}7rҼV~3jϕcgmXSʜ:3]eŚ-KCW. -M|Ќ%Lυiu6 g"\$ynhqV/Cf-qk$oy+lDg.W`̣96 q\f,o8q&瞓sR7k~O<3G$wKlD˕?Iɟ>HIKlDyC.E:B.yolDyo;Bӗl6oy s5 s6jG'e5߸?&LyK}'Ւ)KC_/tՌSYg&OyC'hš 6[3nDGϨ&?.!n>$ś&oqlDGM8zο|zBw75䏙Sլ:M?iIOB27| fW|5<=w#c7y5򹾬bԴ_lyO?䞣Σ^W XA(MdEՅУBnS )C昐F-Rw\G=lZ6[|AB!c9&͕fa!GΏWWn!$۴f" ;!' 3SCgCjFcns!*"Rۀ^oԽGYB WTW#%\U&RU39Z~n*U4US \]6'3QmV YԊkmԺuz{ԓMQj\UU唝j&yr7vU窰8QDQazƪMq|z*t ۃ:5o؜rZeT5Zbg?]ܨW0Uã?22t̘~_E22zÆ ZJ"E7m~Xjņ :D琺S+cXE0Q 0xlTCQ#3rztEځoƪTymƷf}{5)+[0tiy?QVOy<9*%}mj[(S2X|NKU^[M%*PY7^A7sOBWtx2ñ7PfKpIIɒ N))-))Ⱥ!S%%N{ £ׄJKS(̉붋ri>䶕(YS*{JJ|]ʷT`ER%%2(BJHrK]yJͽ{z;4_x\@qym.R5 lg,ض'&F8;b"J9 nőŚXőm1faƞb-X*"jqX(*,**#clm9-ж}oT8]c{Xz-tENfըȒv,FanL5 YTh5f̬ } uzb {<'s#rxA~A~~mߦX26NU~r6ƌ'1cwp䒯50|e9_yy.nX'T/CHz@Tch﹉@z&jcExrݍKh<_6ME~ڸnsֶr\%uTS˝ӷusho?*JQrsEѩmYIWX;\ P=i1lf7{#T\\d,[79I.Λ&s,[ZNvNse KɺttȉR|Ȃ9~7oA9b^`;'G1cDLMWo (z&+WfettK֖`ey^3,5^[kEFֳh[ kȒ!}qTc gd:&ȱL :22F=SpE2a3qPs 2|!LX"=ęM0ӜO$7+C$ˁ i:PTc+Q +'Z֠l3]{< 1()MBG(A=[3'/';#D@Ky+{"RŨ$r/ff_zGxxSbL7}j~_gX̰-SЕ𑑑¶,Fc)ͩXZj>4薖jq $z"Ǥ%?"oh\Ӷ!ڔOEUBV TΚ]&ϚvV[Wg};y+Kڎ0yTTU_9C+1k#{wy>}Q/嫯~կߤѣSLI(m٭LIKڶp>osG5dȴٳׯZU ֥F{ޞŪ sI~߾c zo?l/OfĈ_V\oMt|ɭ{OC{Oo1|Do$ƛN3Ѧvq_ӯ߸!C&0e޼IǍHN_,n͚ےgZ$#D 8MFE4/ScwI#\y 8zɓsǎԙ3$% JJ3^-[6Ϗk^uumEۘ-S%ۯw/4֌vΜӳ箤ג뮙}ة2܊|뛔S4/̡ʯԻG2dJ~r/q'MWq{R$%=}T"+Gn&Pg V~POԻCv}NEo9cϕO_^wWYot2#ժbQ?;?~ltΙ}\!%'{ޟ]!|pp'JHge#b{oG,{ >C)v ׵:{H߽ O)֧ꃝh xv|&].uwo{}sr>Ft n L٤IygTNns,ȏ# w{/l7{UUUǎ_eL9'9Nt~1?%A'B4@̆p==6ta{Nן>vWLawsO]cJ%eF5?۾S?wOk?䴶Nk7k^Kv?Zkf|1CgTl-xկ~uT˟Ѳ'{?k--ИkY>;ȸ9Nʨ缺kRV,.~_v:Wsslu)N u6JgߪǸsLz&'줧Vq[Ç6lСC6סߪ.GϕOu "|BГ} +#?p<1v `W;*-[<ʔ|?js3/v^jrLz"+LGe$=eРA0`@~>b_.y[_:[%PY%P'J>dPu.JwX%RX%R7ZYYY%F7*jJ][%RUZ%R{&ݷXYYYY5B:Z%RBV5VTV TpVVVVVVVVVVVVVVVVVVVVVVVVVVV{O~XYYYYYiUTl(+[WZ(#ioj<2OƆDOZ 'z l[\ݲ5RFWm js bwŜ zrjC!=BϤr!!,sw?zhH G5H]sr޼a! +B(.GH4)P?$|%TwxSS]ȵjǭTWZ1<ܡ]KS^5f/gUCz^uWؚS1&s*U6(NUUnY>&GGUUhYU)f>o zw!l C퇥BM[3ZYyY9&NeeE4;65-jdh,SYJ'r!R~RC,&)oХbkh[SUWU8'zZѨ]]\G*mKUo/fCK b,cvo4Pp t/qzgsK;7eOE/b,񂏗f(w,Էʊ}03ZzqqG=5vh`.m shS jCEXݚXMx-VU2 PEbNj-jv(¢¢"qP4]?!,tc ܚ1WKHMMMSniF/' =7.GåEt&|ki>.cq3sw욲oj<n]D;#)m.~So ?'vP~|ԁ)2=-"> ͱ;HtmC-hT\SR/iw8Ij?k^xZJʌwm?!ZvKUk;WUy kfǔ0M<_jw-m_weUߣĕ)щfXO'lk65Ěb?fX秮N=2N0Ěb? nJ ۅb۶/n[pE?h`==-">n]o?@̔:Q=+]ہ[u81$vm|l**6a-ܑOhmȿy')X>tG^{*?fXOߍ?xi6F@|ìvNhH5}h+Jjǩi2*pGU"qU"ucVtCͿcMbz?VVTUV?ڡ|+Xau[U"թ*Jw+ t Y85龥~:Z8m5VVVV;H 6vmwt_ɺ&w+|T+jG*meeeeeeeeeeeeeeeeeeeeeeeeee(_|-NSZB-kű-Iì6O6稃GuGF뷕y|᾵c]1SseKļ_5yŐѦBoU!`[ V3zAԴ@ yCu+caƨ-2d iB:-ޒCa^12 m,;CIDis(dWV2B,!\tPhZ^dDs1œ^U8MW1"jqX1CD P_M|;XSx}mXPTTmw[4, +,t[SnЫO=J^W3BN:L.bQn!FЌP[B3-C;ܬ-$o۶rA~A68_L ԣQsJJ7TV dVPK:^T M-Xz uzS.f*P3`(=_0&m5NMkFw Ѽր7seڥusrmo?N˨.F-7yQ{vڜ\E딗+r)7777MgxR,^ZϮW󤫱hʪQ2ڲ"<dH_(eEz"2̷fZĎL'3S(-fV!jD; # 3MGU-#CGTezb^ ٠&uoxQ4w7)'7B2TW8tt=o3W'TF@ot0h !hN1Vh)Kzn{ 4 7} zу2Ԩ^ܱt' ŬG֘c)ћ>冕,==E²al?ݛ$ _xz3Z(QewMJhSFNޟfFM3bd lIQEIg4";C}kq[jiѢV T/+++++++++++++++++++FNJ֬Yf͚5k֬Yf͚5k֬Yf͚5k֬Y,6r%[%PJNhH%b@5\f@(J:JnȶJnZmexuUB **jXlPUX%RUZ%RU"չ*j}UB`cW_5n]*Jj*J`[YYYYYYYYYYYYYYYYYYYYYYYYYY1J5vK`eeeeeeeeeePKmQVkũV6-Q,j;¥6z^5A̕. #áZı6jsD"|n5XAԴ@ ykuڐ9YmjQ!]7Cʩ cBNPW5Jfm!W 92]䏫!=Pw+%mR׾ޯM8\ (W8^9SscH $ 'J‘89ùO'+)_-ےeY-g;䕴~ywvoշ0ST+(g u*`( #%b12REcFP(Cx) I FhCx!m᜴RZ^S#:R0bR6kir fU&@H6=$ CCab[!*Cf)V"(<Aʖ.oE1L 9Z.gܤlW ZĚaZG/ jTvW߳ 2 $, )!$W@)uA5)uR1 Dm8V]x? .~?@7q.e1dMJkSD]-i[YʿRWUJۢifO eTӆ}QVDU'҇RG4pO \J1}liZT/md4Oi>N2ESv_MRKwҼJ˩"yzŅ[迯$zHr2Kk/R.$yI}R!룦l^RTEr1 4땛HVUr>}rIZ5Z%^vKĤ")܃à7B퍘%jf/^j9hMhۧH^]h wwwu9Ѝ7ĥ)iK R$K.ХD+[L]]l&ZM7-ҤѥA6.nUٶtԥeͺ j].ESr]""k&8˥u5l"RP.D-Tq!n!r4kl;yIXڭXrܪY2ԞAW6Oaq GJqRV*nI $ 'oHp(ڦ2kcr͔F\)i+m$+hUV(]VUZU%-"4,q(tҐ9|ʆӉ8xƨyuJ$> tjn8U[~kwZ1TGΖQN bpƨF&QQHT#G 뚡0l9ԌA1վlF%k#UmB(PzAPI"eŐ،r&ԓzZA+-kP"qCjIll+`@@wg-[eXH$iLd;q!RRA lq&/Iq8dXBi5[`Zf&HqzhxR-,?FWqZePj8µe:vNJctu̦:B_A:6U\MUzݯS1 c XpMJm>Vemqc5fhzM :66\S&ӗ2k\^@ IcYY1#w @ @ #xw3#M2of9?[\"ofث]..RmJ1WƷDqSeբUo-󢿩b~?_s^"O&|0h;gg? w4_@&.§"d7k>s;DNl'D`^"fDU?!.o۾ɽvզ!YW\\-%U%ֹ;E~`N8?w];=ñ^rΗ O4SRqq}AZ>~G'vL mwq!b#:o'^~zӷ;}yep]Cd~v@k`lB4qw|+Ӻ>5A ]Az+kre_/i7}!"s?@ჅF U"%s%5{Σ]/Nq?w;:}${E_V\h"9tL|:=o>lKv1퓯j{|9}-E-iʫ56@Jl1GuyxrKyUF4bF0~xtD z;Kn_+4_ʫ~w~[+qVLjTsMӅ!ZhH):y;:ǫbWjs] >3dTqQxܫ5k3Z~U糪tVo;>ZsjnM׺Ʌ7uEQmGn|F#QGx*uNAuw\}jU^yjSל5N͢3ŽTzQ?}:rF:k+\'> NUjӷj͵h.eEz7/ Hg~A}:k[y8$go]~JE_-#>RswDʐ84]=ͿnHk|Y}9wG+5]'^`  K{ To\s{.' :'u[u˙(>i{¿_Ś_n?C!"s?=#ZETaב-7ߵ~A~~k7n:}iHIkKvL좹+d9tjƂL9I7pEoeso<Ɵ;'ڮ9oO8]ͿL/la#EdN53r?*)%~K/??3ǟ^q.NY})U{X)?۾'OƝ&v@emI].[鏖++Oڊs쯭T(BW.7Daڕ!TvmcfǺu#%QG.D!bʲ RQ+.{{Ç.wE(.v 69 QFx*dFv9//"}H;H[H|׮5@stȆoo'ݻbw84_R'$Ꟛ#HQ׆?ő?ő?{ƟHE%Ucv 8?]ۘɃL{J-F7OM ak},<'N߅oV] lwHx<ͿdK]A߅;vofg| R!/oD@{~OOϸ~~'?#Gy䐃8)?y/#/N?ƭ?Ëf_3?1V{ {y=P=\NfezwzΙ]_r_=~oݷl_v+w+!?wtj wݺ{e74D\|B1_6 w,N4dCOhjtv&?2-##?S V AYm#0W=/XgȆH t?L0p[̳[H&5er31 R3'O>_wze/> xN͆of9YGqKz@?:~_4?Qhս}ȿ[̧F  7JTdCSI=OjgvO}g|?ǂVxIA&/1x98XR\v Oop_l&hOͯVx ƟhȆoߌ_]Ns]?@eCYlS[HHHn.?qW_w _4Hț;x$uL1ӘL2>nHpxxdD$ۈƗ4ߜD?:G3ϸ g۶08vvppno9>6_GF׹R_X ೩GI(/wMU;g?& {^OOO'.wkk3$Je($N)̎ѥm&vzw\X[K]}mimAuDOトz>yP߿thG9g^7 \>xջ6-߹#.8q(y9LPB4c^?VcZ0oi33anߎ?YRݖ^*q\GzDS/h]C{m<f8 id0FLcO91?Eό?$SZ>裏?NSO=ӦM{VB(Wt$hAK,O>Q瞛>}:%ܚs8?V7>Ѳv{<@ pĉgҖ.jpu~y? fΜݮ-qYSyhPc6^N+qcr㣫Dc>ƌ}}}~v'O>rf<@q=4<#qcrKMSO1磑_5kٳ_~eYFucF<sbz\r$Ln|thAÎ=? .yp>Ֆ1xɍzĝAKPDc>X$ O]-ixy^}c+T#3߯@K:ΏW^y|$yPo!gސt|tɳgg[e1ő|笅f}{ycsM}i3?>gVa_?o/ts;ot^M RI#ﶓ}{y?_'oٯ\|M~O7:7V\u 7R Yz r͝Ol)O?_ߞ5xٙ$Lti4Dz3n/‚ zѷ4ug̘Ƽ5?'O6,nH~QKPXs.\:6/ o/\.s׮'}̰ q8wyQHS7o<TS|ny{)}}(#T\$?#Gqxea;n[ŋȹ{ڵA1cPS2ga]Q7v0ONXS["W|r· ,E_a]Fg=MuVT# XT4~SR|>S΄C#jU Y9XTx$p?_|?_`7 U#*ÈE G7|?Fށc }m AAXB/|?_|dT?r-'kLXgk||A3d r П/@ П/?_|z۽ L ?A8wظϷ`~J@>=u(=⠿U@}}j HaA݋V>g߯sV-\y7N B!đ'1Ak?_|?_|?_|?_|?_|?_|?_|?_|?_|?_$S,^uT|Lf'D-4xgeRn]o࠿xNRƃL6ꯛiq9 Y7}27o,Χl'f|FϬB > a8b#E¨Dx4`#m Wc}vw4&fWSG=*P}6XIS(fםeiWp8<Gڕ!(KGQ8~?ȾN e| H`0 qqppno9>_pz"&=// 9hrT`L˝Ymj!"rs@E{^OOO\.-Ō쥗PEZkKk+T.WQEH>bŕo&Г8e#_QRo]⎍v}--BqN@OyOIAseVz`O;?xAosG~׮5$ $,,w脌F4#KJ-)9v_> v~q 2$AgƁf3_Q@/HHS 1Ycg7Fϣf4qP^()1Mo{p7^}!:"8 0y7P߿tH74s= el1J(ߍ ƟSc k'AF5@k*Vf'Cb̬dS?';n$=mwS%)Z5TH1E0GAݥ%5Yby v{ǁYs; B;3FS2fgNVa̵ "h7Z+`/ ֶY /-j-ۘ0\#= `z>Znv4 ۄBupG|y_0 M`Z#aduDl D?hD_^8|D=-v{]8̡gVK[Y tEKR A?SK獓9.O!Qrf QV%^SvR!IAM M.dKJdG1bPGl< "mцa ڤ>oz\+wjQC9R<-30㒡0 SmΔIƽ- D@&?_|'o @ П/2O?oQc?_|EfNgUm [yx {ƶ'ۨ^ľ*U<<1fa_PHMtOĤ|Hɗc}Mf'U_>F77_i?A=1&cKcF7П/@"#OiI П/@"Fwp-//QQtYG**pW/ktҝ'C o @ П/@*s&Wô1?_Km$VWUE8&G"m<IX,YRcjj: 䈈 o_U6$v{ gKkFIRISvTJʓcXuE)-*Hn4CÖb"A(Fdys6ہT\u_͖U7X|x?_$Sr|R2E~_CD9goA$޼<\Or@ П/@\lEg8-QpW;Yu7phoSKA(D+6noolx}}OwR$m͗aAs>_۰Q;sQ~Ť?ѰwW/OTCPEI9귣&WMQ^Rpј3> GFF_W3K@o68Lu;*DW;,֒:zO[njQ޲=|nXGGRS/iUё;-ۦ?OX?.5:1 ;7PLO П/@ П/@ П/@|14~-WJ<9]0X{7qIXOke>o>#e[qςI$yɵ7&X<Sp'G~<ɵG<"ڗҲ5g>& dԼ]~DR(by zY9Gp?x O72P(#% eǟ؟-<ߘ!sSοL~t b} _/ f3M8F=YN@ ~} . kȦE02/O `N aR _Dz9haރg[ /b Tof9?@ Ʋb Gr@ @ &ܿg9w @ @ @ L&J[qLDaýUΚ[~ w䣍;hWR0D877;v<*,3N ^>׷~OJU DH^yy"OD<v }CV<4m\~ /͝46jS-# n>P0O*Ԫ=)WHh'A'HB'(_8ER|q8?l QFW;zQPɻT @vtI# \AxХ[K4?9>4_su>ԋez>aau2/DW;脋|4 ËE@ @ '<Ƌ_Z[r)b}-g*JL?ߒl[o+w[SlX̃pcrE gJc}QKDXKUe1Z3*ы<DFҘ>fY)>UF)L?_O >fg2Mgo4$I"ʳsL?ZLQpcrٹBԜ3eR&OϿ-"@ rrt@ ,'@ Brr7@@ IcYY1#w @ @_;@ @ @ lEg8-QpoU6f/-h?)7e(ţT-l"'Nr{<;%(5 Kf*k K4Ƞ~SRفȦFμm"D<v }CV<1BtEc~$P('I9Qcp%H1s-t$~(t D_a$۔DRР\ۤBUm(Ɩ[L$j'C[F~˦I cT]R3##GDMMMti(֒:Oц#&mQf/Nru>ԋeY8E{L"_s5<#郭,I#.5:1 ;7@ @ @ @ @ _:mvaKlds7&s߹ K3s4ؾs֛Kҵl.zN3sYb.=3Y;ts`́902\Um Wb`Ֆ'\e1WY̛Ys rqǨ2űs`̥.5Yw ֈidٱ3wz̜is`́ ~s`́90s`e #sm/S)_)w7GzlwzNOwGQ] y)+ss[oʿ|EwWhqm&&i4ƖWl H$RW_[Z[ybuD?.⎿M7={<2)O{O_VTWJJbˑbJmD<AC$EIwmZsG;6.]q$v,_33_\[y`/4u<~vòZ2lAa}*k=~yj_I'nwddsO3<̧YW*eTeg}}*w&cp{x1?W@*r1? w=~un^ܕ'RAja~C[[ϞqnaSgL9FHhE ^o_uKWA$ (K}qic>Wq/- GPSWoL;zUKec8<<2BPpqPٔ{s?^I9޷/|kpp01@syygMLU.S\ɉ`@r<7o9g?S7n_~ό_i;gޱ-Qs̝HWTgSNϑ#]F@gSQWDܹǧvDuXÊa?#'/l58y֒Aej:„Qn|'} #]+~O˄3 7mީzN ϼad| !p??xpV 8]-߻j}n}/[.~}?291 ߽c߹dF/_q=F__kn4%C\/x~zus߱_~n=T~c/X؋$82xz^_K?5n:'n;#ldd$y2H#1q})(ܴ3E߹[6,>?n&k +6흶xӕ6V_9l>ηmi?⟾%~p ' B9MqK+.KBHkിuXw@xtpy+o| /8~77_|nfnw1~;NkZ-;7Gᩝ;_?Ӛ4/=}MOPp Œ]¬]#E#vfTn#UG|G,w 7N~iɳ)\FxjKLc5+D}%{ܿT_q40ũWG!G?7Z+޲㵿o7x?z}P7ٯWiK}c˟S]m8 'ի/_ڒ=FD?&{g쳺FsVW?ѩ/HﻋXIݭ>`􋡴߼gϢ1hsWU`e_T-ދ4qO! /?u\|xu0PE2KHkcnغ/[?444F i Zc>'giM!Kk ~??U? 0с2ǁʙ$N7Tק"rE&f(3Yz>&O)mR>QXp% * 8EkS!nJ#b2:JWِ,ㅈ>Rn>Y8Eۋ7m\T`GN)VIeKUgRe($ҢE1z sș x oIk_N21R^j8G>juuz#poofԿK}d^NμxyETz^䊡)4-e2I<@Lbݴ-dKiWALBAWrh˶LT SichqiRۅ.~%md9oq1IrPZ}tVKy]YRnZ'gt15"WFbr*{rwMs*hZ\,I9MCrwq8+͕߿E]!y__<~")Z#aRֶֶVhp`Eʰ1cq>L.oC#lhT(ul18,nV&NJZ&JRI\CWJ)ڨ.8Io6ViH+k5KEwJ (蔷٢$\x38 N'5TUcK:ٔ9q#duMw2y:SN8^JFg5Fz)'3%4lZKm1cPL6,il骋6M 7h5&ooohPQ/A &HH RS=V3&6/zUĒ 8 A 7q =ZHi١$-JiK j،ʤq&!iC4 HJa%iA:laʡ>NkwZjA PBlq&/I ME4mmm]!ش:Qˡ סuEX+R:&W*l_S tC[R_fvYg{5̪Fbk4ɩ2+k"ӗ2k\BI#;5P ?p<Õ?S_?`B3`6#ulgϴ}X<:\`Nvαz1꟔-0ϜFЛ#KpVK^'G*htmT2s_56\ ?Smʹ/n KjV.J',?y?H':D?HO4&S) e 3/hߑ |t{inӾ{t:3o8^Ն=ӷͱzvw9>] -N,쬸'4 OWQ)uL JGok BC=ËB!08[Lo:| _Wʠf)sAhЫLa`\>zJA8]R<{?_KAwS#BWV}ɶ{ߝ C[fg~v8AH넶+eBesJEl+oKK_>R[hXhKy^o4~K_^P#VpZrӜo|l߭?6ʩ&7}Bepf ]Y/{+}l'xy7N B!đ'1Q~T R^S?_|?_|?_|?_|?_|?_|?_|?_|%}}=ͽݵL П/Hޘ uzv˚‡jC{*;ʂێ l:G9Kk:`QG׶Gk{{+E񷗅**@9>.X#0R@0}LpW[ 9ØH#G|1XtE=UwGD18p8K|ѥIrqa#~ ׿/xML<氇с؀>yXuūTvE ?__!?& z` LtI_[:Yxx!Q'"vMtvW >E+ß >4Tx _=+w&:;+U#7dxPXw7m>:,~::hhsx]{dxBb_XG뉍|o>Da4#=0PϿfO] ʹzG[c&3gk?Ѳj_M+Qs 4x* >&*ơ??B.w],q@aD_k4`jqlU%TD$@TB tT1$Q.;ʕ(wwa(sKᮎR)PE¥(n;lRc*D)p -;۔tWvxte8>2+$arq*Zrlem8P&XJ<*SyDզ萈6;ZK:[ԿPSD`7Ѳ4˛dJ}R 8c$Fm${l{5l)st!&?rh p,_Al&!V!>O9ˆfK0La D#-%D2el݈ɱ|$JZ`<>##aD6 -8-L2K97N y)Os@ L2a/7n3ؖ2tHz8OaaJ?U0 I[FR%darcakB]-J wݛ0-lmtX)Lv >CА I2ŸCʦH !RvHo3) B%7h:Аb8R* Ƙf)V` ItvSyFjLA1UMZHO\1Gҕ}~ ,g5K{\҇#}r^9dIJ*-{SBLTdUn}O> ^+$^)/Id 9-+*fk2J%uRWi&ykzi.${ /ӆ1͕k"{UZ====J^MP1HTl+k]lj$mj/FF1."4Dҥ.bҤ:9]^F[C7nK[?M]TեN"nL.mcFu*Ƨ% .HHZp<.­uBrr).3yIXڭXrܪY=Rt䶹HrMm*SE6ӿ`ӿMDk[kk+YlmJh6JVRdkdmejlPJQR{\TZl_)ѦXPt!IƧ SwJtdlQ ggpNjƩ1Ɩt) rF*N')dd:uqt7*FŧSSSc#obH"Mcr2SB5*63djʲYɖm4p3JTP/HqS 5H8 lP"T(ɶR 60 xA ԫ$dȍYHjA-l`BJ5&mQE"Uħq8l8%ҿ-)V)d8MQ28]R-L"8]9iRQCm:ausSWHV;u菀0cF.*ib誎m IaH%\چױMKE-*Lڒ]Ojj-jU6F1j&:6\ G֨LYuLQeI}> lB6xz{k@՟of3q Ufg?^X33/:؉2w8kNX3|1lNm$wLIAm 8WF ϸo(?HLeVqC3+mi!;DDٜx55E2h]k6?|x]-"TۘC0fFX%@MX̒_;(~uxw8 qѱa< (?>*;NW:&4 Y5_2<` k5?š"7%9J9t7M7wT<cKM~οLxޣgSW[K}ǔKٛ?!v]׀!8tؔAo:d*f㮥{^,++G)y O?_Ĩe;**vq.kHCPEe!؁Bq-<?_|?_Xj*̜%5sVTL.e+bF9\eTsWWDɇ"H<4fl@>8Յrl)eѱ-FB?<<ѿKz[Q^0&4W)7Nʹ5;e4b%Dva%!XtGF_$5> ЁÚI)S&\AD-/<G&.>?yj?bRn8zIF!$՟tO&]|A>#S''VrPOkUu?F_ϗ5g'_|?_XsڑV=v-'S$_L/3ݗX'M~n+ @П#_fA=Q)] y O`SteӿBX2U 4s!o _ʤ ^|=G-9vNE◕o=렿%9"u哧o8[xyĜlw~p?/xY*?_p .;?_|?_|?_Ĩ?h1b?1F{`CKA|?_|Bp-oۙ 9 M-]]]欘%$րQ*1Sy `߰%/9$6Y$/ۖǷc B'4ɵ/᥹SqrUeL'aJYj?rcpKiMKQKBƨ+T c#Vi$.-?G Ors{*p$d){*[}H9n[kS d Lh^,H6sL*0 dŪ")__8e..[˨`RO%2? >(6d)"(*bp)WqO GyaMڦ-9yd+_K#O$gSE Ih{:QHd+O`L^w["19ȣxrsƉKb$4 'q~ibPf~r2NUu?3:eܨӖS%+fT?Aל}F>MBKqfU3H%Nيʌ-ݔd/tOGKEբ\tX"xU0bmIq1p6άGYyvY# OD'jN1̢6zP4jDžɣ'XƃCd"I]V j}䱈zŒ7('3tJϳqJ7Ɵ*哘+v@ @@ @ Lrr@ @ @ U<͐{;3Hƺft=w_(KfJ׎+)d[}1o>]aHy^q%f"~ 0Uk#(LgR0b)K\QIi]`pPr|JjjPz$As[~ΤB u~5'DNCӌS0B}ffSI*X:o-(ZNJdGX$ථLIlm7iI[od6vI&qwLv3Yı4h7M[Vd;uȖdG$?$˒,ɔ(_HI=ߗFhznƻodu'z#Sxjhns=}_cLکY⯬8]L#,*2V7eOj}yʻ~߈W] W{0[?nmc{n!G!8cO⯮zWİ4B : DN?x;jf)էX&zHX]~w!oLmX#[/dq8fڭHmu?   n1ZT   MۜwAAAAAAi}IDX÷Nۙп#ʅٹBqx?SFR%P|~#C;~'3݇@qRDYE?֥>7=0' X_zwӕj_|'9߂/ܶ/TI _B5ڽ|3?ke3]?`[ ?   ,Z~   MۜwAA#5Tݘ]+vRΟo^iNco?G֔!!\Db8DoRf16ur !!!!!\c7pV"tcmsC8kpٺx;C8C8C8kpM>ynY [2\2v µ\ڦ扎:[4׬I=wpp=}CsgnƷ.\?0FB#_=ǿhC+7+og{/Пx7wzهヽ/_nPz E+_#R/ş8wS_\>߆~?8ë^C_z3GS{>ػY ry|%^DRI&C-5LX̏粣⽝a]X9}ћ/m}3}N+t_v׃z鑁DWϩzϟ8[ |pikD}>8׼oOaOp@i?U;Ѻ Ȩ9Y8/8E3.AO@A79lDF~p;"8XzZS->8C~pi70jDZ-H`0_G`.0o-=gqSLlADQ֝mE1_v(;c?#`6HHI7"8hs-w!ۙ?x~>av og?.A3(]GQB)Sr8oo6U;EQ;CCIsA}#sJ:6c$"?M"c?]Uo]4+K߯ 6(riE[no\3Boq?Ap#`6HHan(ֿ>?B-~88~@V6.WoE`s?loO sAoi6S54$ׂ\dSOk"-pylSڇk4 ׂMo'ZH_Z6H`[c-տF4#9[_7Z~ /DQf]8_O~"(߼H|A ۹S+]w(߬h~M Qa?GNwZ5@ c?h;Q|}oѿ~G`BDQVy-4fZO@oӿ~G`Zjw֛C~s'{O/W/qw;ܙ僯ɒN[*MǦ -37.]0~rE} z:./:G)8v9,?oLϟwNCЉ+ҳLxCϮBzãaпw]7?ӿCs8`  ̿ o o8 o.pc.r3]M҅Tu9C ]xDʒdٓsN'[G5?y4͞/_/=J{ |o<|ᎇ{残w=~޽ə /,a07;=?WTʋ E•˗} o.]@v o.]@v o.]z>vA?a_ʾ_w~6n_vпatnjqXҿ݋oAK]@v o.]@v o.]@v_(LL' o.]@v o.]@v o.]@v o.]@v o.]@v+~Ri8=6=5TWF?]M҅T7.]@v o.]-1{߷;۶}kf稣b U}t{c;;kVר/{meze>tww~ ĆZ|x{5Wp s-rk[xFL`KPzA}֔n6nm9ӺtF/wmkdmG'岪[s4xEO{ϳ#*X˭m9<\hǍ%N1߳}Ƕm'WAnm јO z\[^_m{MgGC^2[s_v o.]@v o.]@h-IWK;}lw5PU7@{ {nq BJ*8e6C|:osKG' b*P~43Qok YjGUE#1y]G'֤+*wšq[M "M"v$q[ W)WXd8om9]@v o.]x/L&G& п v o.l꿱}6y}yG˝L]mj6[]?i슸u\6]jڭ}8} o _m}Wu'`' =u[ 6.]@v o.]S#ukgϟ?}Ժtm$=uw^O#Y:m ϱm  o.w[{7r3Íocw|Ӷ6L5Z}]v o@wޝ]l XITVOŮZbzׅD* x4D퇸m96?7tOT.nI$6+'h'iX ~}Pm}M+^{4٘Q*qx/Վ?vPҞ;~D=G>#kwO4q?#Ǿ j&#?.]@vۅGdn8M@&^?Ѣm9꨿D.]@v o.]@vѶ AAV}ּu o.]@Q XIM?k пǟ XOv֋A9V}ּu o.]@ov׿مWs\OSv o7? ͧsčoy o.]@v׿ пǟ׿? o.6?X/n@AA#si}AA"vkojiA}wgw [/=tsc6mpWﮋ1ұM>)h=aw*Igvzq:ŜtorVCy5KQ˧,wݕ[cף>q ;@tz7 p%mye__.q3Uӻ/LpP ןw#e3wHՔeݝ2?p_n=baI.=cgžS>?d,8kKR{]o5<ς簰IZ1GKG$,mg}+ӎ?굿{s~ǿe%h'k_=噗?{w:=x]f}0NS|s}~ছ??tefGw<n&>y3.w. 8CF=A<ɩ 2\vt@3l?+o;zs?-wF7p?wwˎzw@љoς+v!E鑁DW)2';t|!Lz.{S>G^O?==ܷ;߁ǟq֟>;vg;t1qWQԿ>z৯x'?~߿??p3&;Dw]9睂Vn&*j^zG?SOϯԯz_̟wgz?5yc(̼CcOog~W VR⯈(5@ o_#65/NI!r7w_Ll}9:oL/[Wf96@gѷk"}AA;v8̿ss3EQ&f??t៚&14B*eq5s{7}(} ֈos/_?( i5@ o_#\_gotޢ )ϼ`/ } 7OvURRԴOoEwg&*st  *7i?cIM#WWҺhe tdo |r۵ {fBy"cݿAE[1]"֘[kGWa?5LFNW>71]S9{> [7`W?-1Z{.WSDQ7gFkkomPDQkƷ|_A*mx97f6H`68_̆ytDNC 4ojlkl?ogB΄п [Aw~n=o܆6^Ro~q[1)ސ$ooH~صP[ܵ K]ǮEX49KX.N+beyyayypeeeFbOzAu0QNf/L^.]@vE3.8Wħ-aM6u;NnQ4}dz27&z׺ o.]@h1szhg0㏸S&f0oa8 o.]@vFX[ CcmE@vGOmQwJcmE]K(ϛt+mQwϜ~!ڊ]@v{~9X[ZŠ" @=_]2]5lsvN/9Wyl3s^g_s|61H,S#ii<Ԗӟ?<8~ˆsʹs6rz`d^h٥IJϝF]塶$d':V=/s^4XjKO31G d<ϑ^|Υgy- Oλg;Rv%gχwG A6<Ԗk<3JZfҳ'VׯCmI0zum՞Up|ygO/;tt4M<Ԗ9rIW_>ԳL\~d$5B\GϿx٥.rs9E"'+{ϬwXaP[L'JNyI2opD7}Gbwk!(e9+ N 2,oʲǜw3IrNfk3p$Dkƌ8a{c)룿2čSzԛ2-*Jx|pMf/ 4D9*M֡ acS w)IM%aKU+M r6r IOo@  =̰rكr0VJr;7mZ Y↺Nk jRֽB.|^ fr{ns{gٶힳy%wm qZ-'󴋻ipܶ'/D,Qʩ֞͜0 sΉ֐#`r%̮9nEU9#QU&Sy#}ݓ2»g3 ]d9mO=;YC~";'&n'dݞ3}3vY4Lhi*+}iʞdxlVQOp“Sպf2cd$5WVbH"# _=@ʟ㻿:RJn)Ťq;&ң'X>%OjF'Vd۵MaO:\FԋQV~l.Mn9*ёL#"P+Jc%Qc' \pFR)wʬ4mn7HSg8т=SJ(ŒTTwM驧4۔/T*+\2Fkx)<<<<44.0-K,1ۆCk ;$›qrH!s'*Y iS1]~De‰ LHkH$U%OԜU-)XVLj lyG} NfN*'>LIJLJ6 EY6(w1AJq۔8M݅.[A|PǕޙWƙ\UA=νetJQՃ-oaITd8Rp a@o> <@,<5r5@.zs@0LE|3-{ mhfEH*^^[}4K'osyJY$C.sA4rI݆lRrRgNc> ̹K*7yAҳXm 󦟼[).s&e%7n}ned`e03`?,>s2Wj%̌L7ݚեRX:[3hҴ&ϸy$cSr3ش +7ӔuGZ Z߷;lO<;.S\^ +%뾢5XM|qC݉ZÎWu`5Fo%x]XgE++VI׎Yfq.w授sc=fȅYtt TD=3W QxyYv+p<1L4 U9z=J3Zsdb;"[O>O7p)8»a"+bbkhF "hveCX--^ԃii,jJ"2Z\R|-.K"EMSXT,nŒڬpeaBlU2%Fza͕[XV* ^?UFhFZS*oLN˾QjE(QH-b-| uܔ=ROӖ8 16zͅGz:|C'S||D gg˳QvtYVGlh6eJ.Y 2+MWWY9p}e1#I-+-k1vc^-)\)UrtZ:ыJYb%a+-7bMbJے 7#M<yFgF DQ ^f@ȡ=KF_4}+(-tkZ5tWQpSZ}\E՛mGжbQZ"Xg*i-]቏2VRؕCWKj-A O Zmm ^a/* !eݻ6 "+ׂI?<[*Xmʉ}oj9C^޴ҿnfs9!Y*Rպ渭YVDVlNU{G<)#} {u3 E6#JQͺVb,ψ]<4[^V*RVDβFDثxܹ0+e]0wRxzZ׌Qf٬,)1f!f$B@ VNGGك-U*-JLF\K47<eAGUW=-z2iO]zU>JʓOZ*56Qyɩ0i.Y+1S7 l7)jUɍ<5ܐbTwMh4۔/T*+\2FkxA Qx`hhXǐBoRVz1s:CF-< nV*hHC!#*e־ '/%RNRjNL-%E/&NpOI ax_n-dन&xʉ()S2@jI9LGVbri)l⢝N(7:G*^ⲏJGƌf:qc`(A͖$xZ2B)Hp?ŀ|[o3倧FEB{hVo4z|Bi oO'ϐiV\pooo`0z>,j+Z3 'R094B1L^Ea۟;_  ` 0aҾqCfW0}dS s[ ͍zn"m}9AAܹàEZ@A\v 2tѱ_Qgbo,ث8!Xi2*/dsbbrbLzB_q)Zi9}4[3ic= Rvkz'V4BY|rTke燓 &(+k$=nrꛞuQ3_- h}WJ@In{DmZ &_PwְUtةJp6:fU5\VYqHYkwI[mn& Gpgap#|ΑpZRZ+Tg <>QPd(?\G-v9xQ5Qp>ZvaxzjV\֓vϓ {@ nHvu\,RĢHkGKn=+vah8fvzQƪfȬ("E\2|-.K"EMdwˮjS&S3peZeME*ݬͲRY2ZE3MҟTxcuZ- FVd(QH-*9Nƣ üsy%s~os\xݭs`a7Z8}g/WMt.Гjfu˳ec[.d}XzrY-]ѮbŹӟ:.!Y-tke1vc,פʑԯ G15c@@3y*KV [nĚ&m%o Gl!x3* wQjrCc(i#&r(iyjJ`FKEB+69\ZiDiaO[&a[mXV8i'2+_]!{wvAdZ!9P |]UX/IÜi"e r4[^+eBL,ǝg3=+e]0wRxzZ׌Qf٬,)1f!f$B@3bb4%&%V䕣(_minJLu]*&kU Zy<IKV%#*O<9&٥5QHE3DT@9JrJo)oϔi&MJқS)55"|Ru,_Q\2FkxA4>iu%Voc>Ls; )!s?(*Y iS1]~DߗP( e2I ` ZJ ^Lj l]2C ޗp 8)ꒉ+$UdR+!3I$x-UOʁde:"vR:R%Ue2!Wc Sx0N0g eLz0q~*M%νetJUfzz\tГ#Y)?D—֦uU_bW3 hFf5`ՀDZǧ-{T<!WcWj"KY*Zl%~>}"#lC#dPôo A!}X}0   6JMޘ! &1i_M{hڂW Z/~+ ScdJr|~v:mX||vh;r/xffv:m.*& mvNdܩSGmv`OL  mv`'i;7L8m>iN? \REZ  MsA  6YHy{|=Z!ݔzGƻ]JxҬ? f'\ sYw魔RNO>&GϹ\2X bV3&Ux@NNFC׆&YQ9- m)qVs\t9^ 5z*=f9.yx-@1r5L^z(GrـUSxDf)ۤ(JYb$lkUږ/e+h̨3܁JG%}Cc(i#&r(iyf4%'J2OH V0]&iVv |A[YžVGjf[mXV8i'2+˻ $Zv\NȇQA]rfCG+`vq[=7-2ٜ랔>='iRWtǨp6QdՃԹ,kÌFꀮҗfIGfeMd w)<Y=UkFx(3HFYqe!f$2"'>L8`Q JSGYmѴnJJ7lRDGSEiZV#,y}*MOn7H!m} |wRC|.ȿ:TuJVf됷F94#*e־4BGZxN$ Ew!ؚ5-f.iM`5.HBREN O&2Dr[2T#b'#YRU&25Vm`\!{A "kZBǵ733sys/qه12pH'na\%afE=A9RK_ZYOQ7S``¿/bWw2qib@ !7=<fOEH*Ut= 2+ZA |i&}}!}kx m={}} 0   6JMސ\7oLl7a}ټfƹлZW Zd0%RLfxbbh|<9:ڿyhަ8d͛8ѣ\= ʤ($$1WwOLN1'%~J 7?WTʋ E•˗ٽVGQeOO!%( G!A=r)sBaHPhxD,ŚO1 H $POcQ$YԩX%pgμXpDFBmp{SXD=yi]Ȯ(X"\;XpzDE4"y1=DI}ñ$X5L枉=3G{&$n?oQSdϬjY j s# s6q_#k؈np̍wQʮҙLNn"tTr\mL)#' 3F嫔xESr°pMqZۜ] n,zMrn.TczKpuLu 5 QczpAwk g|z_ٮ1\vkجQ}Pcz}pe¿Ƿo^_1\;Vcz}UpUF}}_5Lg"!|7DC/p$kݹ_]ᗓ="_q󯤺zE1]bP#wb%i[G݅--w{?F+io;Z[k};~㶯э7׾~sw{Q`_p]ͷ?&{1N:o;~v5W$r6M3;>Bl>L?y)ͬo/v_fM߿#Oϛh71}ƹQ7W}&8܆̛8K{ j7zELgwQͯ<WH  }Wt_n{3.W: [߻7| _x/y>n'X?'ϹO]m7GOOd dm1 _.µ^L?{:<%6xʹ߿=cd>Cgng7::H|7^~g5۷|C7.]S=}{ ypyit`t?Gυ;c"۟џ7|_­ysq~{3 W|Hy'gx~_ǟ y7߿.鈉}m/'/?p8n?n ߿?&m2tr#<x`e_B7*'n-Ke Ȳl"A@Q2,do-{BYm'=7IZB6˛i ]0f+ng5emwemE M5nn& T;Ar:OÞb- ge!e"U}{˙oϿo?qMǢ.{9_>'UG734~}gի&zZj6 [/y0iav+kqߓ7NÌ]c~PtL25!}cax<O'Si x/yx^Wku? >O -6x @C4MطaC_GL|2ZVc Z6-hڃtb{.+'>ޠ >? +q  ` F` 9 <L$0LS`~3L0 ~+y70, ";X `xOjւu`=6M`3m`;v?p#(8 N)pg9΃ \ep\ Ѓk @(; D( b@, $$ R@*H d, ru @>(] T5m ;j;]npuA ꁇ#Qx<O3Y/Ke x^oMx!h&)4G-A+1AA :Π  zOg7A?@0 CW p0h0| 4- x0Ld0L߃i`:3,# ?_` |,`1Xe ? X ց`6` `  {^ApZ 8N?/8 ip?8.p\p  t@`  @4 ă@2H @6A."`쬡$v6jZ.p7 : x(x <O,xσ%2x ^7@&x 4A|#xx֠ h ځt@gt@w3}_` !+a`8FQ`4 ߁q`<&I`240f`f/W0o`>XEw,K2`X V` zlflvv=`/ 8-GQp 'S48΂s@. A:@0PAQ ĀXAHI TAY  |P Ajۀ jwp;wp/!PG18x< Og9P<^/*x  -6x @C4M@S!h@ ZVc Z6-hڃtAtA@o~ | `  ` F` h7[0|Ɓ`&` tfYG ~_0`Xb,,`%XV5/ll >` `8 G1p~_pN 8 p\".+*A \!BA DhbA $dRAH dl\A(E@Da8n/v b4ƇwZxvAqluf5GkN}-yiH5#I\T/j1cN-vnfXK`!d! ѫ0Mūl6lmr*dnl˪t\ҷw[i/i'xTgYP2t7o5e7I}me[l*|-N?Z_ږ§*cο*QOzZ7OjiL&ɕaDďR>pK$d#L&fg2LV)d2Y|&deLv^'Rb2SsRT*Vi9CR\x)d8nZ:izL~KL&;âV`tmkN>~1˻7s^ Y.~dLHz_^1_  5?@o8{UwnWtZ/3> Q F'F;qaqᑗ“\x)d5ϫx869%,6>(<KL&;y^%H-8.%%">A -%]xM3|yԲ/$Oyhv ;ῠ}7ȊLHGD9^A!F=G7%d*w`E{dUrz:fG#ǐ^0wwɇt#RِJE{dڔW0U;ӛ#.-Z]J!fGR /i-JF2*1ZdLY\<(~\͙o4;F-RtjLv^J̄yjſ]#9OF06˻[0a ш䐨2|kyPR;c|l|y:xZKL&;y^ff)=-+W2 r"ш2|{r[,{-ãHE1uH06Rj< -_$5ݏLv^JN7F kWp{4ʊpFhO;ʾEdŧ&9^\F1iR>kWٙR1NHM /3v<3~f-g2Y*l8+''9-#:>|v)[83|y_K̊ON-#]x)dkax_.(W3q\뉸R>k'?-R t8$?^g2yڰhIݮ jQ2h c\x)d9ChDʮǷ/3F uZ*+ʳ8./3L&+Ӕd2LS>d2ML&ʴy>S 2LVKSI$UZ>/ݰL&Ɏr=L&)d2Y|&deL&ig2LV)d2Y|&de,d2Y|޷| >pdrU`g*ELV7(IN!g |֔ݔ$3Y|&RdJQ>U`g*ELV)I3Y|&RdJQ>U`g*ELV)I3Yg}7I |&U`g*ELV)I3Y|&RdJQ>U`g*ELV)I3Y|&RdJQ>U`g*ELVw7e+)I3Y|&RdICO-y< ^K' Hx22|v|;yq&geGE榄^O ^U`'SF]ަ?Q~%8zI3Yv|^3unuݫ;=keD/eV0< ha-3J >d 8؊.fH&)MdIypAݑ=:ߺ[oXtBY-y Rzzx[#<=oF,|&Nϛ/|LuG|}Ѕߨ?1?+~YhE{2OޑF,Oc 4k|V(*y-7wCWB;~Hg-M >CViBo |v.Q>U`峍>w*n efd'1qX[FF+ 0gF6 9o$3YDA,gù3CNr'֐Û '/cq),}c( xyxP(g |Q;M5.tAnfnfrjl &T/g |g,|]?md yzK /I|&Jgwɏ4.*sdI |&Jg| LL(ɪF!as&[~ 6Ar5Q>U`峍glQ>U`3dQ>U`峍gL*[dXil3lQ>U`峍gL*[dؔϏ+%mT?˫!I?dUuGVRKuX8"Ud+-m?eQ$&T,l yzK6)mgNJ+-mT?%T,Znf)&g |fɩsXti> 7PY|&JgU϶Yg TZQ|&JJ|KfEז-f.DLV6|ͮW%xI3YVZ>Wjg1r-g |QTT(*?"g ,^ [w(L*[dXiLIv^))'9F&Jg.|&JgL(*FQgR٢|&JgL(*Fe#ƟrY$*D|&J*,o1gbJ[\ >FZQ>U`峍`g&e>{zk&PP'3YVZ>WMg~u*~y>3AZ*f:vS[䳻S+RY+t˻Zcuh)/\|&J*-/.zgV:.b$3YVZ>ۨr =5ҥŴRQdXi\\bꁖ$9(*FUFg?IdXiLIv3YVZ>(L*[dXiLIv3YVZ>Hv3YVZ>Sg]DLV6?3Yݔz\)LIv3YVZ>(*&u4Vg|ΚV(*فy;H(ZYɢ|&JgeϞ[\ò,_)DLṼW~)+gg3YVZ>(Ff<[Y(K*PdXiX2whGk3Zm!tB+-mTe.C4t|&Jg=)EdXil*3IM|&JgLBMLKJI IJ7\7:_ L*[dXiLIv3YVZ>Hv3YVZ>Sg]DLV6?3YVZ>Sg]DLV6"HwܼݬT׸\ n(_dXi\ee?KWfA*_dXil*XY~ײ3fJk~팢|&J窩j]*Srv 0cҸI|&JgUϖ, 23NI-9/|&J*lyU7Z>EbELV6gȯBR(* ?K*3YVZ>(L*[dXiLIv3YVZ>(L*[dXiLIv3YVZ>Hv3YVB>+بfˊ;<__t.(55alac6k[?g$g3Yc>_#v:DLVψb홀m6=~xMEgmM{E*\h&4  ^;kwF2[g&zYM'ɋgɋ I?߶d֢ HI 3Yo>EoU W|8_F>טowV>gG7Q'tF -Åbbʆ4[ʲWl|}cse`&^I(j>[`~xpc.g l|NNOOLMIJNH 8sEWZ>Y[;} ?-f Dw#',_>kL{vy5$j Ri[11訢DLViYYF <-86f}|m 0P)O- ؄GǬ5P%島Tв3:f-Y|&笜8;;;!5-8,sKCx@/\aF09KspAӮ7[,9 F3kgliRj>c)_{kbDLV&dd甖wǯه%eψHz;OXx])37U`-Kb ɜ1i_a󣺋GǙ"W0(*؂S ֞mS !ZB!Nn6u8A" 5%=-&%54)%vG[2yO"|&,C~>gk>\DdXig.6 MƘA"TdXg|&?H3Yv.<[$xeN G#|&c eƙz=)xGDLV]zH^; ڝ6_dG#|&^3Uݱ{utw~ڼ,"Fse%3TՓfQ>U`W?o%\Q#{tڿuߠ?>?3v+W&EJťak RY]DLV]Ĉ>qXwt]hY~,9-zz-BlQ>U`-K[BLVwZϛ@>'Z=,9ңT-`q,J3YvSq[UXXT_0/ 7 ' ;鏉,=|46ߐWq-_.|&*?T< Ξ9r|[Co.N8r[@rK R"/h|&9o-v r3s3ScCWO0<=^bELV]g,|]?md˥*NS8+_d{(M1袂˅|pV(* as&[~ [ 9(*I."g jgsTEDLV+)+"|&."g jr,s[ʨgjU,g jֲ3׈jU,g j 29-H\|&*?Wvg>X/by*ggZ"A*l9lYYTjDLVU*R[P_y{kxVYØA2OoosZ5gO4, 4(ᅚvL.]\Z Z. fU&g l^Z?Sg3Y [i|Vl?(*p%bE]Dd$3Yv3EDLV]L]DdTEDLV]L]Dd,AfşGƝTpDLV]\Z2K şiF+uH3 I6oTeg̀9Е ZoO ہ|&jj]*Srv 0cҸRELV]\ϖ, 23NI-9/]|&*?TlyU7Z>EReZgȯBR(*+RYFpV(*Z?(* +Z"|&&(*S`WQUH%g |^_t.(55alac69zHJ3Y|ţ;v-h/czI;o / N{-l۷NR(*ψb홀m6=~xMEgmM{E*\h&4  ^;kwF2[ ۬ 55wf i^Y*,=$CjJQ>U`%Aa.p3N5۝Ͼّ#Mz cBpe+͖BZj54" RrF-o,ln){y lrRU+q抮|fXw&~[ F ^)—Bx1G Nb#WQ~AkY LdX!紬,vtϖeo}p>> U`rrLԴన~>|*+/  Bf s_r,iqpx|ĘjG-7̊6Ė ;R7gpBHLKJN7\+_P_XhrAA^AAFvNi\}{|}XRx]׽ׅ12scOZ/XRag3MAU#~ZKnSPW(*BϷ..'s7Ǥ})z迆~Ϗ. B goV Lk5 L롘]@Q>U`|vD =fgϧdG B Br of>bl9-ELV7U`W?*^[w~ѽ:?Cۃl ޻Vom^F^KYTI5wGU^dg{TwDwo7ό]:4ZLi)MOq2Yr߽Ĉ>qXwt]hY~|xn%hO IA|~\g2і9Sp???$泖kωVߐ"'E,Ԡ$YhT|&)Ygjܩ-*,,*/̿^]QZaKBZt˻ZYSK;J$%57gù3CNr'֐Û '\ǬC/W6h^|tTQ>,?[ySwh ]xag䳧F䷘֮MR(IJjY~(ɖKtU,w=Y|{N ?4o_rLI4 ƚPv=Cs[gQ>ji>]T9+z, YVE]h%|քzs9?S>;lgՑYf]\ICMO/{|PZ rl>+VL쌲e9]x-eRiD-"u.?[U9?S>;l}J4OHK'ݕXSYkq~V7DrlU Y,8?x1""."ȫ/?"faqq 3p)+IAEg*Ggy&F J}/o?pw.h:,wb >CEELRlU,PHl!wa˻0G̚!OD!¬%sdr>1(f2?[3U|(,)"gEg*Gg,aΉgyͰ@>HB\|&)YQj>#u7R ^ יi jʓLϖ*Gt~2OaL&'~MP>+MdXєruҴ+#)+[dYr|vFQ>U`?jggg3Yvs9DRZ>;l|&}gG?%$ɱ\5`6|%WER(IJϯ/:oms皀o1S$d9Q|giT{٤R7B…O'Wی=l6z['R$%ˉψb홀m6=~xMEgmM{E*\h&4  ^;kwF2[ 3y%snF y eKԖ~8Rif/lU|v@ pPX\pLesivg}v}qBm2\h*&lJWh1ҵؐ¼]r!o|i,v'߽HUEY~E4)^˫tnRY?4gzJRWtm [ԘĈЈ3Wt3ź0al0*MJLzNjY=h@plAy>kh6h-;B3X泼E.iAeKS{>c?z/-_<嗝koLK>ًV4_|l)MϷNb7*@glY7R )~e 33aB"61k6$/6p;R6Z̏n %rT,ի~Ko4ezl/ch=iMZ[>i/TOϕ-?g iaQ|TV^^W? *򅅹/9Œlar8"lAy?"7FX^f\P~RU, nH%%i->Y-l7x*I/(/,4 #;|>ؽ M>~>t,)k.~FDyMۙ?#@-_ꘖ147oA-=zAU#ݵ !U\ϫ<8̇#^VeK>ö|?-ϕ-?R$o >?(1xtV@RχYyy6fM-3?>9i,o'wTq9Q|?Clk϶qq)i-!G'n7n+bIe|lM-q0/0|4+ϥ}+>r]G[2yX;;yZB!lχLӔ.}JYѵKr.=jg.6 MƘArI%?H\$%5$3Ir3q&]$%YϕWƙ"qeQ>gq|ve)0d8RY~)՞K=[#3Gyi/ˉҊ}ET 53C稔T}L.U_Y[\}K:TYj=[f5K+lQ>U`s՗q]KkݖiYlUuo˥W}gP5+åDjŻ̖ VF)-k,]Z/bb5x1N'T?[4#*pnGqcZ{G9\ώ~JH ҭvg%qQϪ3Iɪ|3.|&)Yg*Lr(IJ3|&)YN&*([Y~q.)gKt}X/kI..?H-jCR\yY>FۇްB[m"Ս1+  H߆PU.<[$xeN*H.[g1e)`y$/Fa[o f࿨|V4m^fy1yyi)ד\/SIŶY`sHlQ2YM a,0nlW˪_JY'PO-|^=e$naNmK _2nHU5tk6e{>3ۇZ^^Lar{u6gF\GEwْԗvP>;!9ZU爍ZCb>ky>ovhg4|v))44ʪ|;E\E r3 rR 8r)g銁eR6{Fc Tl.,c7,7HbDφsg gO 9?O!7d'{OnTij)Fl9{+r-bӬlАBDLR2wΛ*kE]箟6JRY,>ELR2.QcEy<ƒvΙl#$ELRr!$-IL&] ,3 XTڧ|&a|.I$#OS>T%g \|&”dg3L&Ɣd2LS>d2ML&43L&+Ӕd2LS>d2ML&  mdt[߂y[V/y03_ +hb]xW_iy|>D|T| C8 ַڑEgMR2͍Gdrڊlem?o͌nbrpL- j(:};nsq9ѕEГ%fX,m` !%΃p PMt?FV ʶ_!䛸ԛ]Ų>e=YvS4&2Nŷ|ةXJ;?Nt,3mZp(]05,]UMV/)L1re>Vz$F-V7s,UC,i\81&)YS&󔶗ΦuOh^v}wL&-:kW_u+{c 6?X-uzomwvͧ;k >mO6){fg)39hb>ϥ]|`Y\çeuk}^UdrE8ػ |v9n{6ڠ{^^K#nU3QFV?{vy><)\~cՈRt܀k&|%:jpDܸx &ZZMw - (o·ѯ~SG]w_nM"uy>K~ |`L[ Ͽ5{hvg7_m\,{uz/'B))[<AXkyn%Lj\viL½O}14]0]~mjl[;o3jH~̬qDnmC^lOqW̘xtә"A6r4pA?Sr?KJ,\Ƅ/kiuO-"=oطbm7?Y}v $Ϳ,}3^;5Q]Gb`tMD?~/{Wc L&;ҽKnbp=npsQRZ|pL1}oԎ.]ѵ]Nψbsx s$zsy>ùlc Z={XV[wm#7]-gs+fř| XITp49ݘhknK9WNmEޞݴп| |)m܄OjfY\~f_%8%[-dk5;y>GBGOLpZ7xoڤoâ;!1Ƽ?VjycCb|yw&3ueS>뛈miDc3>۩:OfKM,k h-ڳ?3S|kD&{LwfJr'9kV+ w\no̅C;( ׭]>ts]&=Ýpq+B?sz6YgrجE\Nӿ8i|&-kvy3"V<)̓9C_$C;22ӈn20Oe=5юkGWi7fw|>B1;s gOv֯Q%d>IўiLF=L3]33.6| hJrƉM+Br>4xn|X[r\Oޞ3MZJ8GHMny'OG AH;D=X &⺵Xf6-?@n 2;[]ٲCV vXy,wL&WX:]~uTL1F"+h<9я~]c8n,8| oB۞ϗzc>FL-0;zD_x6LШ5o {#7% \g|Ksz7ߤ'խ--|+(VވU7M^g;3Yb }kwsKؒ$kyK`L xJfQ|mYYqG{Vޞ!4OYrǂ07/%m ]jV=D86,܊ٓ2Gu29ޤD^cb0$^'Ox}DO극{b>Hd3ϓ.g}㶺qeؗsb8N(x;v=8Lgwuc{'w3? {귉VM$^k#Ɋp'YbQ;$IQ4]GHܰw4\gچoRЗ]zmR>#O>%9Qϖgx..dX=T.|&d5|<2fo$pLZQ1V"#^b/z끌bx|g|>=]Ώ`15.v0gH^W!L&K瞚؟'*eʢ^Iq}uLב1GfHǗlЗ{wKi|MVW?f]Sv;SԍS7v8wM?x߹ack~b&A, Lƿd%; Y6-SoL]% )=1?' ?cObB\4GFje{^l[bf<Ϸ|⿨t0ƾ%,gdg|Sx P8oMu3%kˆYASΟ;.7tzHov }7΋B85y3|-Tsv]5|FK,7o)r8?!h#:AN7!^ў>"Ȧn;~{8Fۇ>Rv)Y0YgL[泡|N$KYmK2A&/#Ftg7Dt~Rn*Fn@8AqZ߆mx8[g)ӿ*YV'c܉JwLAj޵#Da܁2_X>Ҽی|Ft|,giêsgQhd 9K _T-{ƯJ\ט@\#73C>g 休54 f:|_7 SJ%%$ɷnJ5ǝU-w`uGWM"է|f6C EK|7S27))yk":="E0/l`L&]CnϚi(ˣU2u Gւ3z?6FDÈh,3/(EMD/TJv>WxnK3<)V3C+ҾF<ۖCCgJΎ6Y'-6xubree[9FVMor ңRx n5#m+܇#wŇڊWD8r f]wh]CPkމބ;oc.p7 : x(x <O,xσ%2x ^7@&x 4A|#xx֠ h ځt@gt@w3}_` !+a`8FQ`4 ߁q`<&I`2[=݈*Uc҄l0 L?`&~?g s<`,KR r'XVU`5Xk:l&l[6;v` 8@ |p '8 ΀.K2@5 p"A1 āxAH) t2A9:y B1c@uP@MP 6{}~P<x<Ix<ρyx^Wku?ho;]x|F1hA3|ZOmA;t z|z>/_K0 `0|P0 #H0 c@߂;0D0 LST= 0?l3 怹`  B%`)X`9+*`X6` `von~pa8 ?p gYp%p\WA @ @8 D @@sh<@K | (T5m ;j;]npuA ꁇ#Qx<O3Y/Ke x^oMx!h&)4G-A+1AA :Π  zOg7A?@0 CW p0h0| 4- x0Ld0L߃i`:3,# ?_` |,`1Xe ? X ց`6` `  {^ApZ 8N?/8 ip?8.p\p  t@`  @4 ă@2H @6A." ?jۀ jؐ+hT]HݒpcR= BVC0 j1s n3?D?0D||u|%MnZ/v{SgMLDq_tGNJw/+k{Z06&w e]اo E<>aĉ@3ϟ3k)3^xdgg/*4EhPh&DT6ZzÓ_}XZ-MOOWxe^7a1sbYj`68#j_}]XciJ|7]lM۳SCnoQGdx`F76zlO L*R$q~&N$߂O}RfϹ؂xGw{Fms=V9=1i |(Y|anoL(=> Si|RZ['Q,Qx4b\J** K@ U0%$kggg'~?oͷ3Lf85xR5b볪Zn BZgIcI[%˘yUTѾ~}^;@Pʷ 6HiTœ+sN:؄a(7Pert'Z'p{ed0&2![ߪ%wKߕb{u^R7b<嬅v^W̔ƙG)}tX97FXݷY)lf:1[ƔsK;| x c#_d2^}td<$YpdzqT‚WF0a6%$&EZ, Bʦ$U?-|/e|3ZHooY*-cFi@K|:0Lb@{v&= ,TO#}'t$$ȥ_!+ql\ oXa .!ℊ<=/!|Ce=^Nyߋ)%=X,cZGi|”Y I?s]rRݶN)Lƍz>xGU0~.ٌII-=&rKgus bT4 h n_WZݦvk4U@Oj9lo BBmuKE7G—)c!nO-ڬ] 3oD0! T7G"~WxV'~ꥵ 2MlG(~wI| äXa|b &Zc5x "MBN?&rgP=ACV||"S%dS*Pւ.bՀA(xYM e p涪fwI͌oLϤO;ԃǺב׽߻m=[ؑ5rۦI7DBIzj^JZZ5S=NSɌ̱5%Xt6Z*ۦi6?B4d4^Sb t#O%XfCx7cY_~ؼӑXcǪ9 ׫ TT[;3x݈e[C^w}cnvhtKB_~X;8ʱi,_8Vn9VgI}'-n=ض]o3HȃmBAuMg폥 3G^rlLl=A6;ZRg93#Ӏ#C׿o }hfU+,t>"w;sls`-|oˤǗuq^Ռ:?8^K[DJZO׸}9} k.׭[(KJmmYS|Iד]{&:vyk} ^?.ѿxD~ʜkxj|K|s|S|lfJ?uk[5z\ca<6Rkkjj**+=ec#{->98o3T~ntlGxwqk?Kʌ.M}yǜGϏ>Þ{g` 3|8 7A57=ؿe Ĩ{DלkvNEEfڳ퓝+wd~Dؾ][V|&y[}?޷m>?ヨw_j9Oߙf|ZmKQ*=נ"mt&({Nwo?żr>9mz:i,nHe}k9|˿6ʼn`/bt3p"@3m ª[0NDWD]ɀO­ѿm뿅[E­ѿm뿅[~?,ޗ =e2nUgiQAA4mp"]6ʟ7~~+WSz_<k:V ϩ>H:%" k'}bnUW O*[MєٿmcVE:Ri/S^ voVE~MJ ݼ%fˇIdmP-ݪHUyeo'-8XQ%[UQ85}iXUQ4mphKosӶ*oGIWC]J[*P]&ݖkxxTmJ wU%kWjKKEWy`nbF軚\kO`*P jTYJ;?36ď* ٙGk4TQPxv~TA<;'꿝??./zt6tכ W0߃Lޕ$xtwY!o?NtG t`bT߮.<a~woѵ7U@|a)b3S&zC+XP $.bl❲Zy5}V7Kۭ5{}^)7VEK@[AWF0?_bܖ1׸""ߵ~O {>F>#̗>Jh/MVOXL] ۗ|Tb侹؟M]?QE6)'e0S} O„Meɷ7K|UY>O- d&ɷs) ܴKjH-E,!BެTPwr8fnYOG;/ڙ$or;cPc m? 1<̗Z"7H9jC C<܀l s@1fBފrfN1gggggggggg3o_Rzg9bM{?1R߅ǬңR?1t엯22]_Qd.ntъu ~ _/*HF*)ρ٦ HcFN}?<1d}p%yLoq*ʘuR-XrL_}7lBs+66666666666666666666sc%q KyxKz̴# S:!ݥ%cKFؑ߆01FňjXR@TKsW[/X20$;MhS;l7=N`0;0B -#€l7F] ַd{mmmm"ߕ"<})%pisaZJ0/|_X{%.C\xt& lR`UxpODjJ6mIjV {au(}FL[ǥSJ*OّvQz]2Z)=ZͩZP<ʶT6'S^ _]we/n:TUJO[z۟[=U?;۟4ն+:}}_8_ztCIϨnG#O7ggggp  0??1+g~1 EIQ,dGKUt(4i--{ :">^J:D߿-]FHe]MAY2ll4.SU+Y O ^ҍߔ:ݼ.uwrpAW` -^w7ĿBN-XIDeKQoy-Xoc0-+y\n]X],K|i>Wwst_4Nok߲GXђ.OvpWUc 666@a~tallll)3%,4jxCv3=RIrH2e| o3HkvΒ^Re8S iw;Ј5ZԢt7Z֝ +?/߽$.L]d!M Ŀ> 7@'LŮ^[%^xEJӹFЧvZN?Sm-v7eo-TȪn>u:߀t26D%hȲx@wloQL NE_X܅"[rWD9-jOD?6666 (JƖ[΍)=BGA % AAzh2~G+7,\k0傧)g=E\=?,J~/ [q1 Č :TtD#=f9i'U!!%qbsI=&z&4pk;׻j[dB.0p$~xb+('[xO)Í-. .r[e0J8#t9W}ǝ.VkU,l@ !6dosoK&+_Uv<惋\I}3}P3l~UG/ex>-DNKGJyxtqТS k _O(/!?Qn="MەGET7~|. fo=Ǫ[\ᒪ7.ףe=9ts+y||J3iË&R8+`ˋZoޏDLΟXjwU+xev 3˞Z䤒y'-uOIG)@W:ns7e]aOIXbU>}<>xꈢI&Jqn(ZWrڿEEC?3}d+W6iڻ̼3IC T8Ƃ-jR^}\]~p?}?oE4C勦??e$ȹp|"'+F^vph~S4qЯռ%)* PxOᗸGK>lX)=k]i!dk3?B k~%Q?eQR{DKT=בI=vOh0`킂! M.<|ϏX!b)*%3ǔcGcm+ ur?Asr?_^"uE{f<|{ɌdwV_ύ%\<2'sy$QY"|a~EOVQC ~.GGvw3YƜ~AbyuM~[4qWs?tԯɗT %S%P?C*D򧿚Z"x;3ܑ'+ݣowe/).ˢE~Vx_nGTD/ZEEnhս^g|aͿ7_àt~ugp'|'켄[sݯ~~z|C&G#?xч{MV]U_>ng^vJƥ.O~]v(׼~o=BԒ5=FU9I~-߅ 2gI;1& ]Ok*v(TWSeA3 v > XB^ɯ-Y*gO€(  a¥xǹWq_d>36~~~ =m 4lx6%Eu8c1~?Ɓq8c1~?Ɓq8c ~7۷ܷq~~V0ߡ|$q8c1~?Ɓq8c1 ~wr?~ca?~o/;>$h8c1~?Ɓq8]񃿿m8c1~?ƁXüAخ/???~LɁ'_qԪ9'?{ʹ]|S?}r ʨԯ_<Rs m~Ƨ|#|͔ N2+N#-wr-y,uoJwwRڪJOEyqYiA^`,pl'Ƿmg|u*_q?_v7_:;=E¼cΣG_aρ۳?z6lxs3/ĻodDJ7CR&ү_+ygaXڳ퓝+wd~Dؾ][V|&y[}?޷m>?ヨw_j9OߙX[Y,-]Vգy3fEm:y Yr|b}9qwx6=va-Mê[˶/x6tlȁ~dr vVrgDv%rRc/!v!O;koJ2%ngn_96tdd9}XűjkNEC.l7]G<ϭ!vڱm;YF3X4FTSlwZՉ SǢU߱WM;f9>±r:K['?7?UfUMZf,xnزˑzlv׵lhhSf/A7P~3G!6窭8Lz|Y4h?8^K[DJZi ߭%Ӂ[ly֜ "S C p+7{J]^Ͽubw_sy){?S5+ =Ч}y|yrosB߶Ήo6ot۲?oc}|?6G?HZ9_?wտ&MN- CJQ- C}۷xG?,Pr6)(اo{ki7̃=sB) \tӿx|>ߚoacbɮ=;c&c-c6>c;GT)ht?[oo9v;7;r?no9vo2v rd|˱GEG1;GT)ht?"6";7?MWΊudBy(rNfM:TͿ:vo2o_݁lW};oI$@M&Oykձ8ogdb8og?cqܿj?sqܿ7Xs_MΏe{cD u5555yՕǂg5g9lllllllllllllllllllllllllllllC{kk=Օhllllllllllllllc o RqŊ???mmmmmmmmmmoEMuQuenU?????????????????????????????5eUU9e?????????????????????????????PU?Krrsz|fl1 ???v~~~ 'ӹ뉟w~~V={???~~~ '??cO~~Ǟ={???~~';~~~O???cO~~Ǟ=~rs/ae}Z<:'XWlÙ+wJ2uungn_96tdd9}XűjkNEC9+=%%{ƥe n;HחMfsk뮯;vl-N"pQo 1cƶ*_~l/+58X;8ʱi,_8Vn9VgI}'gT~REUG'˱=uc.GNgg2Y\ג:;N̪aɊ+A;7Y~3G!6窭8Lz|YX3ypݽַԵxZk[ -'z|)yrd'7S筘:/]bn餐:IO,?w?c&LMee澴0ٵO}z֬M"_҆Qb&NoU`&o*hm+Te'RXc&`_ hmonovXV5vo/x>מosUnƎ܏ٖ[6uNU>|%fG0=M]Ɉ;w^VV6W`lYiR3^DXJ݈x赪 2v5^kZtOnd,+Q5Z Dt5[nW5ۋoG6hg#! lllllllll#?V1 3A0?Vg+x<۟Sx?g?3 <?3 <6H\'S/|QSn9vҍG;>s9#&cP͒iH3v'ܹo #wwLi"B5-S/s_`1+6WWy**ʏW2e=1gggggggggggggggggggggg⿲U^zs#ggggggggggggggggggggggggggggg⿼g"|=;aIhcj?Gͷg} 5xR3.ڏ ֖ZR+]cyfWG~Pښ2M;pE[UYVl'6ofy~ħ74zkK ycǭ k_SޚՏe"˫]*Iy>\;KO-7^^)۵UU"?LjF6N}&M(X2P.Q5~b~HWUozM Fx,/ڏʊ"Oaxm%Jf$/ڏ隚#?-,ϑp 6L[» ڇ,ڏ=vʋח55ViY^ՃzkOԞhVWjY^G~~&~LhȹtGq}k$cnZWw{ԏ~5&dO$oS3WBcN8./ȿspӹK!]}?z>+AxYOb2p ]&:A7D:?!g]&)/GWkt;uC}Ν.\8|}woyC~I姑ӹ+N~eΙ) WJt&w^iYU\Mj묓afu%J>KR C5=_SNqL0KXN>7ɪe۩Ԟ?t·􃒞r2U币q~k.3_st.ǩt2!$<C8I;VL⍢:eiU%<9dw\?jz~i ӡ8~fqkӿRnjEmd|l,:=GJ?k<.Z/h?r꣗ӹ⼜t~o8Sȸ'g]I}`+Q+t,MH SzƓէq3~ʜ7N_EGH^J\_}GO1ao5kIo=\DQ%:̽@7~!q:4G?IYʼ{IM^sxX,}9K_8'_5W ` _O6FF?̟+r` #Ϣ'S_;gCoKw'S[SCGIsK+ak%>jAmŢ DcN)0x. SJFHVJtrtw4y6sV@m/w[- n 3z+-/i *4( > Ii LT_.aҁ q(2`3D8p:j|~{€gxyevѨϬMOQ#TOV{XZ\^/1N 1(   060@0 E0 I0J0Oh+'0T `hx  Slide 1Jon-Eric SteinbomerDavid C. Morrill226Microsoft PowerPoint@Ø- @0Xs@rߨ GSg  )'    """)))UUUMMMBBB999|PP3f333f3333f3ffffff3f̙3ff333f333333333f33333333f33f3ff3f3f3f3333f33̙33333f333333f3333f3ffffff3f33ff3f3f3f3fff3ffffffffff3ffff̙fff3fffff3fff333f3f3ff3ff33f̙̙3̙ff̙̙̙3f̙3f333f3333f3ffffff3f̙3f3f3f333f3333f3ffffff3f̙3f3ffffffffff!___www4'A x(xKʦ """)))UUUMMMBBB999|PP3f3333f333ff3fffff3f3f̙f3333f3333333333f3333333f3f33ff3f3f3f3333f3333333f3̙33333f333ff3ffffff3f33f3ff3f3f3ffff3fffffffff3fffffff3f̙ffff3ff333f3ff33fff33f3ff̙3f3f3333f333ff3fffff̙̙3̙f̙̙̙3f̙3f3f3333f333ff3fffff3f3f̙3ffffffffff!___wwwCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCmCmCmCmmmmmmmmmmmmmmmmmmmmmmmmmmmmmCmCmCmCmCCCmCCCCCCCmCCCmCmCmCmCmCmmmC?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~      !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~      !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~      !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~      !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~      !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~      !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~      !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~      !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~                           ! " # $ % & ' ( ) * + , - . / 0 1 2 3 4 5 6 7 8 9 : ; < = > ? @ A B C D E F G H I J K L M N O P Q R S T U V W X Y Z [ \ ] ^ _ ` a b c d e f g h i j k l m n o p q r s t u v w x y z { | } ~                            ! " # $ % & ' ( ) * + , - . / 0 1 2 3 4 5 6 7 8 9 : ; < = > ? @ A B C D E F G H I J K L M N O P Q R S T U V W X Y Z [ \ ] ^ _ ` a b c d e f g h i j k l m n o p q r s t u v w x y z { | } ~                            ! " # $ % & ' ( ) * + , - . / 0 1 2 3 4 5 6 7 8 9 : ; < = > ? @ A B C D E F G H I J K L M N O P Q R S T U V W X Y Z [ \ ] ^ _ ` a b c d e f g h i j k l m n o p q r s t u v w x y z { | } ~                            ! " # $ % & ' ( ) * + , - . / 0 1 2 3 4 5 6 7 8 9 : ; < = > ? @ A B C D E F G H I J K L M N O P Q R S T U V W X Y Z [ \ ] ^ _ ` a b c d e f g h i j k l m n o p q r s t u v w x y z { | } ~                            ! " # $ % & ' ( ) * + , - . / 0 1 2 3 4 5 6 7 8 9 : ; < = > ? @ A B C D E F G H I J K L M N O P Q R S T U V W X Y Z [ \ ] ^ _ ` a b d e f g h i j k l m n o p q r s t u v w x y z { | } ~  Root EntrydO)ߨ Picturesȷ Current UserPSummaryInformation(c TPowerPoint Document(DocumentSummaryInformation8 L0R0S0T0V0PW0X0[0]0a0g0i0k0m0q04t06|07`/ 0DArialUnicode MShh 0DArial Unicode MSh 0"@ .  @n?" dd@  @@`` x|    !"#$%&'()*+,-./0@12345678:<=>?@ABDEFGHIJK  OPQRS   XYZ\^_`bdefikmoqrstvwx yz{  ~eFxR$؛ Rz 8ݎb$= y]vƈ@b$\:1r5Fb$DUMi$rNy$h? b$6pl;P8M;';Lb$pjDb5xS"sb$fY1hVC.ߖb$i%/!ɷ!!b$(6<0`:z"b$.Q cyy( b$}Eu@+ M3b$FS?ƊGTb$heia*mb$ˆGDƣ#+Eb$hȕ=֔e**0qb$|&y#zsHb$̕T;c&á!4b$ViӤkO}_5Vb$er.`J3b$虲NÛ,]b"Y?b$ŷ\XH({;c_NRab$xu\~*yE!:l.>b$:6$9J.Z@b$m+eu-,m%H3Gn<$R$~I)hn?36 0e0e    ̙ A@ A5% 8c8c     ?1 d0u0@Ty2 NP'p<'pA)BCD|E||s " 0e@        @ABC DEEFGHIJK5%LMNOPQRSTUWYZ[ \]^_ `abN E5%  N E5%  N F   5%    !"?N@ABC DEFFGHIJK5%LMNOPQRSTUWYZ[ \]^_ `abp̙@8ʚ;ʚ;g4\d\d' 04ppp@ <4dddd4w 0h <4KdKd4x 0h(0___PPT10 f___PPT9H@?(?  %O  =0ik4What We re Going To Cover& `The MVC programming model Creating Traits UI Views Creating Traits UI Handlers Defining Editors 4GUI Design 101: The Model, View, Controller ApproachThe Traits UI supports, and is based on, the MVC design pattern. The MVC pattern defines a UI in terms of three components: Models Views Controllers&||GUI Design 101: ModelsA model is the data and behavior that represents (i.e. models) some aspect of a particular problem domain. For example: A well log contains measurements that model a particular physical property of a lithological column. Models do not have an inherent visual representation.$v ?GUI Design 101: ViewsA view is the visual representation of some aspects of a model. For example: A line plot can be used as a view of a well log model. It s possible, and common, for a model to have more than one view (e.g. a plot and spreadsheet view of a well log model). GUI Design 101: ControllersA controller defines behavior that mediates and controls the flow of information between the user, the view and the model. Having a controller helps prevent the model from becoming cluttered with view specific details and code.  Defining a ModeljDefining a model using Traits is easy& Any object with traits is a model that can be used with the Traits UI. The object s traits define the data and events that the model provides. Defining a ViewA Traits UI defines an MVC view using View objects. A View object defines the general characteristics of a particular user interface view. A View also contains content which describes the individual items displayed in the view. A View does not reference a model directly. In general, a View is a reusable object that can be used to create multiple, simultaneous views for different model object instances.Z The Kinds of ViewstThere are seven basic types of views supported by the Traits UI: Modal Live Livemodal Nonmodal Wizard Panel Subpanel*AZ4ZA4>L  Modal Views&  Suspend the application until the user dismisses them. Are displayed in a separate dialog window. Always make a copy of the model, interact with the copy, then update the original model from the copy when the user clicks OK. If the user cancels, no changes are made to the original model. Live Views& Allow the user to continue working with other parts of the application (they are not modal). Are always displayed in a separate window. Interact directly with the specified model. Changes made to the model in a view are immediately seen by other parts of the application.Z  Livemodal Views&  QAre a cross between live and modal views. Suspend the application until the user dismisses them (i.e. they are modal). Always appear in a separate window. Do not make copies of the model. Changes made to the model by the view are immediately seen by other parts of the application (even though the user cannot interact with those parts).RZR Nonmodal Views& Are the inverse of livemodal views& Allow the user to continue working with other parts of the application (they are not modal). Are always displayed in a separate window. Always make a copy of the model, interact with the copy, then update the original model from the copy when the user clicks OK. If the user cancels, no changes are made to the original model.kk O Wizard Views& Organize their contents into a series of  wizard pages which the user must process sequentially. Suspend the application until the user dismisses them (i.e. they are modal). Are displayed in a separate window. Operate directly on the model.Panel Views& Are displayed as part of a containing window or panel. Allow a non-Traits UI window to intermix Traits UI and non-Traits UI elements together (e.g. within a wx.Frame window). Can contain the normal buttons that the other View kinds allow. Operate directly on the model.68-iSubpanel Views& Are nearly identical to panel views. The only difference is that a subpanel view never displays any of the standard Traits UI buttons, even if the View object specifies them.6Q=CdjVET: View Editing Tool VET is a tool for building Traits user interfaces. We ll be using it today to interactively illustrate many of the Traits UI features&  View ButtonsdViews (except for subpanel) support a standard set of optional buttons: OK: Allows the user to close a view after having successfully edited the model. Cancel: Allows the user to close a view, discarding any changes made to the model. Undo/Redo: Allows the user to undo or redo any or all changes made to the model. Revert: Allows a user to undo and discard all changes made to the model without closing the View window. Apply: Allows the user to apply all changes made to a copy of the model to the original model without closing the View window. Help: Allows the user to display View specific help information.HZZ.NM HV m KLControlling a View Window s AppearanceA View defines several traits that can be used to control the appearance of a window: width, height: Window size. It can be: an absolute pixel value (e.g. width = 400 means width is 400 pixels) a fraction of the screen size (e.g. width = 0.5 means 0.5 * screen width) x, y: Window position. It can be: an absolute pixel value (e.g. x = 50 means left edge is 50 pixels from the left edge of the display, and x= -50 means the right edge is 50 pixels from the right edge of the display) a fraction of the screen size (e.g. x = 0.1 means the left edge is at 0.1 * screen width, and x = -0.1 means the right edge is 0.1 * screen width from the right edge of the display).VP'PP"PmPP m  u\Controlling a View Window s Appearance (cont.)Additional View traits that affect the window s appearance are: title: Specifies the contents of the window s title bar. resizable: If False (the default), the window has a fixed size. If True, the window will have a sizing border. @ 14 0(8Specifying a View s ContentsA View has three primary types of content: The actual editors (i.e. widgets) that appear in the view, specified using Item, Group and Include objects. A menu bar, specified using Menu, Action and Separator objects. A tool bar, specified using Action and Separator objects. It can also have various optional buttons (e.g. Undo, OK, Cancel) specified using the buttons trait on the View itself.4+ZZxZ%K& &  V The Item ObjectAn Item defines a single editor or control that appears in a view. In some cases, an Item corresponds to a single object trait to be edited. In other cases, an Item represents a non-editable, visual element in the view, such as a separator line, or extra space between fields. The type of Item is determined by the value of its traits, which can be divided up into several categories& nZNG}\8Specifying an Item s Content<name: A string that specifies the name of the trait the Item will edit. If empty, the Item defines a label only (using the label trait). If =   (a blank), the Item simply inserts extra space into the view. If =  _ , the Item inserts a separator line into the view. If =  nn , the Item inserts nn pixels of space into the view. object: A string that specifies the name of the view context object the name trait belongs to. Unless you are editing multiple objects in a single View, this is typically left to its default value of  object .6IZZZ4 !":9  / G;,\Controlling an Item s Presentation and Editingeditor: The editor (factory) used to edit the contents of the trait. If not specified, the editor will be obtained from the trait itself. format_str: A string containing standard Python formatting sequences (e.g.  %5d ) that can be used in conjunction with most text-based trait editors to format the trait value for editing. format_func: An alternative to format_str that specifies a callable that can be used with most text-based trait editors to format the trait value for editing.Z Ek  \>   v@Controlling an Item s Appearancelabel: A string specifying the label to display next to the editor. The default is to use the trait name to automatically define the label (e.g. a trait name of employee_name becomes a label of Employee name). style: A string specifying the style of editor to use. The possible values are: simple: A property sheet style editor, fits on a single line custom: A custom editor that usually presents a more elaborate UI that may require a larger amount of screen real estate. text: A single line text editor (i.e. the user must always type in a value) readonly: A non-editable (i.e. display only) editor. The default is to use the style specified by the containing Group or View.&#Z8ZKZ  L%tHi, xxvPControlling an Item s Appearance (cont.)Cwidth/height: An integer value (default: -1) specifying the desired width/height of an item. If the value is positive, max( value, minimum_needed ) is used. If the value is < -1, abs( value ) is used, even if it is less than what the item says it needs. A value of -1 means use the size requested by the item itself. resizable: A boolean (default False) specifying whether the item benefits from extra space (e.g. lists, tables, trees, multi-line text editors). padding: An integer value (default 0) specifying the amount of extra padding that should be added around an item. ^ZZZ R!  nl,VControlling an Item s Visibility and Statusdefined_when: A Python expression evaluated when the UI for a View is created. If the value of the expression is true, the item is included in the UI; otherwise the item is omitted. visible_when: A Python expression evaluated when the UI for a View is created, and also whenever any trait belonging to any object in the UI s context is changed. If the value of the expression is true, the editor for the item is visible; otherwise the editor is hidden. enabled_when: A Python expression evaluated when the UI for a View is created, and also whenever any trait belonging to any object in the UI s context is changed. If the value of the expression is true, the editor for the item is enabled; otherwise the editor is disabled. Note: These expressions are useful for relatively simple cases. For more complex cases, a Handler subclass should be used.P{P 2t 2My 2M{V6   %Providing User Assistance for an Itemtooltip: A string specifying information that should be displayed as a tooltip whenever the mouse pointer is positioned over the item s editor. The default is that no tooltip is displayed. help: A string specifying a complete help description of the item. This description is used when the Help button is clicked to automatically synthesize a complete help page for the UI.bv@oaP6@YThe Group Object{The Group object is used to organize Item or other Group objects visually and logically. Groups can be nested to any depth.H| D 8Specifying a Group s ContentfThe contents of a group are specified as any number of positional arguments to the Group constructor, or by assigning a list of values to the group s content trait. Each item added to a group can be an: Item Group Include The contents of the group are laid out in the same order they are added to the group.pZZVZS>.VNOrganizing a View s Layout using Groupsorientation: A string specifying the layout orientation used by the group. The possible values are: vertical: The contents of the group are laid out in a single column. This is the default. horizontal: The contents of the group are laid out in a single row. layout: A string specifying the layout method used by the group. The possible values are: normal: The contents of the group are laid out normally, with no special handling. This is the default. split: Similar to normal, but splitter bars are inserted between group items to allow the user to adjust the space devoted to each item. Note: the position of the splitter bars can be made a user preference item by assigning a value to the group s id trait. tabbed: Each item contained in the group is displayed on its own separate notebook page. More complex layouts are accomplished by appropriate nesting of groups.4dPPZPPHP YR :Tb qkSH!@Controlling a Group s Appearance$show_labels: A boolean (default: True) specifying whether or not labels should be displayed next to each group item. show_left: A boolean (default: True) specifying whether labels, if shown, should be displayed to the left (True) or right (False) of the group s items. show_borders: A boolean (default: False) specifying whether or not a border should be drawn ar      !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz|}~ound the contents of the group. label: A string specifying a label used to describe the entire group. If the value is the empty string (the default), no label is displayed. Otherwise. if show_borders is True, the label is displayed as part of the border drawn around the contents of the group. If show_borders is False, the label is displayed as a fancy text label if style is  custom , or a simple text label if it is not.P P u W Z j _   b swPControlling a Group s Appearance (cont.)style: A string specifying the default style to use for each item in the group. As with an Item, the possible values are: simple, custom, text, readonly. The group s style value is only used for items contained in the group that do not explicitly specify their own style value. padding: An integer value (default: 0) in the range from 0 to 15, specifying the amount of extra padding to insert between each group item as well as around the outside of the group. selected: A boolean (default: False) specifying whether the group should be the selected notebook page when the containing View is displayed. Obviously, this only applies when the group represents a page within a notebook, and only one group at most within the notebook should have a True value. PV~X ,A"VControlling a Group s Visibility and Statusdefined_when: A Python expression evaluated when the UI for a View is created. If the value of the expression is True, the group and its contents are included in the UI. Otherwise the group and its contents are omitted. visible_when: A Python expression evaluated when the UI for a View is created, and also whenever any trait belonging to any object in the UI s context is changed. If the value of the expression is True, the group and its contents are visible. Otherwise they are hidden.PZZZkZ 2"g 2M"E$  xfControlling a Group s Visibility and Status (cont.)enabled_when: A Python expression evaluated when the UI for a View is created, and also whenever any trait belonging to any object in the UI s context is changed. If the value of the expression is True, the group and all editors for items contained in the group are enabled Otherwise the group and all contained editors are disabled. Note that this can be used to control a user s progress through a wizard View, by enabling and disabling the groups defining each wizard page. These expressions are useful for relatively simple cases. For more complex cases, a Handler subclass should be used. Z;ZvZZ 2M"BU K#%Providing User Assistance for a Grouphelp: A string (default   ) specifying a user-oriented description of the group s contents. This information is used to automatically create a help page for the containing View when the Help button is clicked. The help information for the group will be combined with the information provided by the help traits of the group s content items.n^ m&zSpecial Group SubtypesHGroup: A group of horizontally laid out items VGroup: A group of vertically laid out items HSplit: A group of horizontally split items VSplit: A group of vertically split items Tabbed: A group of tabbed notebook itemsd)'&$#H)'&M$The Include ObjectInclude objects are references to other view elements, namely Items and Groups. Include objects allow for factoring views into smaller pieces that are dynamically included when a user interface is constructed from a View. This allows for several interesting capabilities, including: Parameterized views  Visual inheritanceZ)Z7L-?)% How Include Objects are Handled When a user interface is being constructed from a View, any imbedded Include objects are logically replaced by the Item or Group object they refer to. For example: The process is recursive, and will continue until no Include objects remain in the resulting View. PPeP2'^!&!!Creating Implicit Include ObjectsA class View containing Group or Item objects with non-default id traits is automatically refactored into an equivalent View using Include objects. For example:P 7Z ='"!Using Include Objects EffectivelyThere are several possible uses for Include objects. One example is creating  parameterized views. For example, the traits TableFilter class contains the following view: This allows TableFilter subclasses to define a new version of the filter_view Group containing their custom content without having to redefine the main TableFilter view. ZZ$R 3 + E P} 3 + K (#*View Objects as Part of a Class DefinitionJView elements, like View, Group and Item objects, can be created and used in any context, such as module level variables or dynamically created within methods or functions. However, there are some additional semantics that apply when they are created statically as part of a class definition& J&Z)$/Defining Views, Groups and Items within a ClassViews elements created within a class definition have semantics similar to methods. In particular: They are inherited by subclasses They can be overridden by subclasses This leads to a feature referred to as  visual inheritance & 6cF=cF=*%( Visual InheritanceJust like methods can be overridden in subclasses to customize behavior without rewriting an entire class, so to can view elements be overridden. For example:P.)( Visual Inheritance +&The trait_view Method$ The trait_view method allows you to get or set view element related information on an object. For example: obj.trait_view() returns the default View associated with obj. obj.trait_view(  my_view ) returns the view element named my_view (or none if my_view is not defined). obj.trait_view(  my_group , Group(  a ,  b ) ) defines a Group with the name my_group. This group can either be retrieved using trait_view or as the view element referred to by an Include object imbedded within a View.XlZZ ^  / + * ^,# 5+ Q,'The trait_views Method$  #The trait_views method can be used to return the list of names of some or all view elements associated with a class. For example: obj.trait_views() returns the names of all View objects associated with obj. obj_trait_views( Group ) returns the names of all Group objects associated with obj. sb s9AlEventsnIn the context of the Traits UI and user interface creation, an event is something that happens while a user is interacting with a user interface. More specifically, events include: Button clicks. Menu selections. Window/Dialogs being opened or closed. Changes made to a field or value by the user. Changes made to a field or value by some other part of the program.@@qmEvent HandlersOAn event handler is a method or function that is called whenever a specific event occurs (e.g. the OK button being pressed). Proper event handling is the key to writing flexible and powerful user interfaces. In a traits UI, event handlers can either be written: As methods on the model. As methods on a special object called a Handler.XJ AnThe Handler ClassnIn the MVC programming model, a Handler class instance is a controller. If you are using the VET tool, it automatically builds a Handler for you. The main purpose of a Handler subclass is: To handle events common to every traits UI view. To handle events specific to a particular model and view. To help keep model code separate from user interface specific details.  ; o The Handler Class: Common EventsJThe events (and methods) common to every Handler subclass are: init ( info ): Handle view initialization close (info, is_ok ): Handle a user request to close a dialog or window. closed ( info, is_ok ): Handles a dialog or window being closed. setattr ( info, object, name, value ): Handles a request to change a model trait value.~? ) 5+%3>vF-Qp'The Handler Class: View Specific EventsEvent handlers specific to a particular View fall into the following categories: Trait change handlers. Menu and toolbar action handlers.8Q9(%9q(The Handler Class: Trait Change HandlersA trait change handler is called for each model trait when a view is initialized or when the trait changes value. A trait change handler is optional. If one is not defined, its corresponding event is ignored. A handler always has the following method signature: object_trait_changed ( info ) where: object: The name of the context object containing the trait. trait: The name of the trait. info: A UIInfo object containing information about the current state of the user interface.ZZZZ7N,tNr/The Handler Class: Menu/Toolbar Action HandlerskMenu and toolbar action handlers are not optional. They are explicitly referenced by Action objects specified in a View. The signature for an action handler is always: method_name( info ) where: method_name: The method name specified in the corresponding Action object. info: A UIInfo object containing information about the current state of the user interface.ZZZZ%.1   1 N>  HNsThe UIInfo ObjecthA UIInfo object is automatically created whenever a View is displayed. The UIInfo object is passed on every call to a Handler method (referred to as the info argument). The UIInfo object contains all the View specific information defined by the View. It is basically a namespace containing: Each context object, referenced using its context name. Each trait editor, referenced using its Item name or id. The traits UI created UI object, referenced using the name  ui . It is your responsibility to ensure that context objects and editors don t use duplicate names.v#ZZ`Z,%%  `$`PC\ct The UI ObjectZA UI object is also created automatically each time a traits UI View is displayed. The UI object contains all the traits UI information common to every View. The UI object is returned as the result of a call to the edit_traits, configure_traits or ui method (on a View object). Although the UI object contains lots of information, the parts of most interest to a traits UI developer are: result: A boolean value indicating the result of a modal dialog (i.e. True = user clicked OK; False = user clicked Cancel). dispose(): A method that can be used to close the dialog or window under program control.^ZZ<@3 _@ QP -(The Traits UI Object Model 2-What Editors DoEditors are the heart of the traits UI. Editors create a UI toolkit specific user interface for displaying and entering a specific type of data (e.g. floats, colors, fonts, file names, & ) Every trait has a default editor associated with it. However, the default editor can be overridden either in the trait definition or in a view Item definition.*\K 3.What Editors DoOEditors and traits are loosely coupled: the editor only ensures that the type of data it understands is entered, and relies on the associated trait to actually validate the data entered. In some cases, the data allowed by the editor is a subset of the data allowed by the trait, so no user errors can occur. In other cases, the editor allows a superset of the data allowed by the trait, and catches exceptions thrown by the trait when invalid values are entered. The editor then provides feedback to the user to indicate that the entered value is not valid (e.g. the entry field turns red). PZP4/Editors and Editor FactoriesWhat we call an editor is in fact an editor factory (but editor is a less intimidating term). An editor factory can be thought of as a template for creating the real editors when needed (i.e. when a View is displayed). Because they are templates, the same editor factory object can often be re-used in multiple views, or even multiple times within the same view. When a View is displayed, the editor factory for each Item in the View is called to create an editor for that Item.BP50The Basic Editor StylesEditor factories can create any one of four different editor styles, based on the value of the corresponding Item s  style trait. The four editor styles are: simple: Fits on a single line. Can be used to create Visual Basic style property sheets. custom: Provides the richest user experience, and can use as much screen real estate as necessary. text: Fits on a single line, and is always a text entry field. readonly: Displays the current value of a trait, but does not allow the user to edit it.PTPm,S];QQ71The Basic Editor Styles82The Standard Trait EditorsThe Traits UI package comes with a large set of predefined editor factories, and an open architecture that allows for creating new ones as needed. The current set of predefined editor factories are: 93The Standard Trait EditorsIn this presentation we ll focus on six of the predefined editor factories& Three that are simple, yet very useful: ButtonEditor CustomEditor EnumEditor And three that are extremely useful, but require more setup to use properly: InstanceEditor TableEditor TreeEditor^tP%PMP&Pt%M&tt   N  :4The ButtonEditor Editor Factory The  buttons trait of a View object allows you to define standard and custom buttons along the bottom edge of a window or dialog. However, there may be other cases where it would be useful to define buttons at other points in a view. This is where the ButtonEditor comes in handy. Traits defines a Button trait, which is an Event trait combined with a ButtonEditor. It is a parameterized type whose argument is the label you want to appear on the button in a view. Z " f, X f?9The ButtonEditor Editor Factory  For example:  A:The ButtonEditor Editor Factory There are several ways to handle the button being clicked. Here s one that treats  spell check as a model function:uZuB;The ButtonEditor Editor Factory xHere s another that treats it as a view/controller function:=Z=;5The CustomEditor Editor Factory :While the Traits UI can handle most user interface requirements, occasionally there are cases where it is useful to imbed a non-traits widget in the middle of a traits View. One solution is to write a new traits EditorFactory subclass that creates the needed widget. However, in many  one of cases it may be faster and easier to simply use a CustomEditor to imbed the  foreign control into a particular View. Z, x 9C<The CustomEditor Editor Factory Using a CustomEditor requires specifying a callable function plus any additional arguments the function may require. The signature for the function must be:,P  D=The CustomEditor Editor Factory EThe following is an example of using a CustomEditor to create a view:,FP' ' F>The CustomEditor Editor Factory Which results in the following display& Note that in practice, more code is required than this, since among other things, event handlers to handle input from the control and set the appropriate trait value also need to be set up.sYThe EnumEditor Editor Factory Let s start with a trivial example of using an EnumEditor: Fruit = Enum(  apple ,  pear ,  peach ) Unsurprisingly, the EnumEditor is the default editor for the Enum trait, and will automatically yield the following results when used in a traits UI:r;Z)ZZ/ ) UP/  1 UuZThe EnumEditor Editor Factory 4Now, let s make the example a little more  real world & We re doing an interactive menu, and fruit is a trait in an Order object that represents the diner s choice from among the fruit currently on hand. Current stock is maintained in a separate Stock class instance that has a fruits trait that lists the fruit currently available. The diner should only be able to choose a fruit that is currently available. nP]}v[The EnumEditor Editor Factory We can still use the EnumEditor to create the UI, but we ll have to provide more information to help it tie things together. In this case, we ll focus on three of the EnumEditor traits that are of interest for this example: values: The values to enumerate (can be a list, tuple, dict, or a CTrait or TraitHandler than is mapped). name: Extended trait name of the trait containing the enumeration data. The values and name traits provide complementary means of accomplishing the same task: providing the set of enumeration values independently of the trait being edited. In this case, we ll use the name trait because we have access to a Stock object whose fruits trait contains the enumeration of available fruit. dPP8PP  /<  D#4t  _ w\The EnumEditor Editor Factory 8The resulting Order view would then look something like:J9Z &<6!The InstanceEditor Editor Factory;The following type of trait declaration occurs frequently: manager = Instance( Employee ) Amazingly enough, the InstanceEditor is designed to edit these types of traits. There are multiple usage scenarios for editing this type of trait though: The instance is fixed, but the user needs to edit the contents of the instance. The user needs to select from a fixed or varying set of instances, but does not need to modify the contents of the instance once selected. The user needs to select or create an instance, and then be able to edit the contents of the selected instance. The InstanceEditor is designed to handle all of these scenarios, along with several variations on how the editing should be performed. However, in order to do this, the InstanceEditor requires a more complex definition than do most other trait editors.;PPPKPP;vKF>pFG?!The InstanceEditor Editor FactoryLet s start with the easiest case: The user only needs to edit the contents of the current instance object. In this case, there are two choices: Allow the user to edit the object contents in a separate pop-up dialog. Edit the contents of the instance object  in-line , as if it were part of the main object being edited.R$I% $I%H@!The InstanceEditor Editor FactorynFor case 1, use the  simple editor style and specify some or all of the following traits: label: The label on the button that displays the pop-up editor dialog. It defaults to the trait name. view: The View object or name to display in the pop-up editor dialog. It defaults to the default View for the instance object. kind: How the pop-up editor should be displayed (e.g.  modal ). It defaults to the value specified on the view itself. [][aS3<KA!The InstanceEditor Editor Factory For example:  =7!The InstanceEditor Editor FactoryFor case 2, use the  custom editor style and ignore the label trait:.FZ9NC!The InstanceEditor Editor Factory6Now let s move on to the next case: The user needs to select from a fixed or varying set of instances, but does not need to modify the contents of the instance once selected. Within this case there are several important sub-cases: The user must select from a known set of existing instances. The set may change over time. The user must select from several different types (i.e. classes) of objects, which are only created once the user selects them. The user must select from an unknown set of existing instances (e.g. by drag and drop). t$PP8P4PP$84OD!The InstanceEditor Editor FactoryThe InstanceEditor supports all of these sub-cases, and in fact allows any combination of them to be used together in the same editor. It does this by allowing you to specify one or more InstanceChoiceItem subclasses as part of the InstanceEditor definition. There are three predefined InstanceChoiceItem subclasses, each handling one of the three previously described sub-cases: InstanceChoice: Describes a single instance object the user can select. Note: If an instance has a suitable name trait, the instance can be used instead of an InstanceChoice object. InstanceFactoryChoice: Describes a  factory (e.g. a class) which can create instance objects the user can select. InstanceDropChoice: Describes a class of object the user can drag and drop on the editor to select it. |PP(L^/ HU(L ^UPE!The InstanceEditor Editor FactorybThe list of InstanceChoiceItems can either be specified as part of the InstanceEditor itself, using the values trait, or as an external model, specified using the name trait. Or they can be used together to create a composite set. In any case, changes made to the set of InstanceChoiceItems are immediately reflected in the InstanceEditor user interface.c (5h"P ("QF!The InstanceEditor Editor Factory For example: P MB!The InstanceEditor Editor FactoryHAnd a slightly more complex example& %P%ZH!The InstanceEditor Editor FactorylAnd here s an example showing  drag and drop support:7P7\I!The InstanceEditor Editor FactoryWhich looks like:P^J!The InstanceEditor Editor FactoryFinally, it is possible to combine instance selection and editing in a single editor by simply combining the editing and selection traits:PYGThe TableEditor Editor Factory TAnother common type of trait declaration is: department = List( Employee ) In the case of a list of objects with traits, the TableEditor can be used to display the list as a table, with one object per row, and one object trait per column. In fact, the TableEditor is the default editor for a List trait whose values are objects with traits.r-ZZ Z-2 t -,} t N_KThe TableEditor Editor Factory Some of the features of the TableEditor are: Supports  editable and  read-only modes. Allows object editing either in-place within the table, or separately, in an external  inspector view. In-place editing supports many of the common trait editors, including drag and drop. Supports ascending/descending sorting on any column. Sorting can either affect the underlying model or just the view. Allows the user re-order/include/exclude any of the object columns, and persist the resulting set across application sessions. Allows searching the table contents in a wide variety of ways. Allows filtering the table contents using a wide variety of user customizable and persistable filters. Table/column/cell level context menus All changes made to the table are fully undoable/redoable. Colors, fonts and grid lines are fully customizable. R-PPP > t a9bMThe TableEditor Editor Factory An example of a TableEditor:&  `LThe TableEditor Editor Factory 0All of these features and flexibility come at a price& in this case, the amount of work needed to correctly define a TableEditor. Out of the box, a TableEditor will display many lists of objects without any extra work& but the results will often be non-optimal. The traits that can be defined for a TableEditor include: Table Attributes: Colors, fonts, sorting rules, & Table Columns: What object traits can be displayed as columns and how. Table Filters: What standard and custom filters can be applied to the table. Table Search: What filter can be used to search the table. Table Factory: An optional callable that can be used to add new object rows to the table.>P[Pa      " : @ / M>t   ecN0The TableEditor Editor Factory: Table Attributes " >8-The TableEditor Editor Factory: Table Columns fYou define the content, appearance and behavior of a table by providing ordered sets of TableColumn objects. Each TableColumn object describes a single column/object trait. You can provide two sets of columns: columns: The columns you see initially other_columns: The remaining columns These are the default columns, the user s most recent preference setting overrides them. There are two basic types of TableColumn: ObjectColumn: Used for objects with traits ListColumn: For lists and tuples You can also define subclasses to get state dependent column behavior PLPPLPGPX  U  v   GX  |    HdO-The TableEditor Editor Factory: Table Columns ObjectColumn traits: name: Name of the object trait to display/edit label: Column label to use for the column type: The type of data contained by the column text_color: Text color for this column text_font: Text font for this column cell_color: Cell background color for this column read_only_cell_color: Cell background color for non-editable columns horizontal_alignment: Horizontal alignment of text in the column vertical_alignment: Vertical alignment of text in the column visible: Is the table column visible (i.e. viewable)? editable: Is this column editable? droppable: Can external objects be dropped on the column? editor: Editor factory to use when editing the column  in-place menu: Context menu to display when this column is right-clickeddPP  +%,   (1-+/ 4;<~    (1-BeP-The TableEditor Editor Factory: Table Columns For almost every ObjectColumn trait there is a corresponding  get or  is method. For example,  editor and  get_editor() ,  editable and  is_editable() . Defining a method overrides the corresponding trait. This allows subclasses to define values that are dependent upon the state of each table object or other values.RTJ 7J> R  fQ-The TableEditor Editor Factory: Table Filters When applied to a table, a filter reduces the set of visible rows to only those objects which match the filter s criteria. A TableEditor defines two filter related traits: filter: The filter initially in effect (defaults to None =  No filter ) filters: A list of TableFilter objects that the user can choose from using the  View drop down list. ~} $.  I,}  Iy-The TableEditor Editor Factory: Table Filters There are two basic types of filters: Normal filter: An actual filter that can be applied and modified. Template filter: A filter which cannot be applied or modified, but which is used to create new normal filter objects of the same type as the template and with the same initial filter values. A template filter is simply a normal filter with its template trait set to True. User created filters are automatically persisted across application sessions as part of the TableEditor s user preference handling. You can create your own TableFilter subclasses, or use any of the standard subclasses: EvalTableFilter RuleTableFilter MenuTableFilter &PP,P0PP& 55^ 5 40b 3 4hR-The TableEditor Editor Factory: Table Filters (The EvalTableFilter: Allows a user to enter a Python expression whose value determines whether or not an object meets the filter criteria. Its use is obviously best suited to users already familiar with Python. Trait references on the object being tested do not need to be explicitly qualified. BjS-The TableEditor Editor Factory: Table Filters The RuleTableFilter: Allows users to define  rules using drop down value entry for trait names and operations. Rules can be  and ed or  or ed together. Rules can be added, deleted and modified. Introspection based& requires no set-up by the developer.:ZZ>knlT-The TableEditor Editor Factory: Table Filters The MenuTableFilter: Is similar to the RuleTableFilter The differences are: A rule is automatically created for each object trait. Rules cannot be added or deleted. Rules are implicitly  and ed together. Rules can be turned on and off.hZ7ZZ>+nU,The TableEditor Editor Factory: Table Search VMaking a table searchable allows the user to search for (and optionally select) object rows which match a specified search criteria. A table is made searchable by setting the TableEditor s search trait to a TableFilter object. Doing so adds a  search icon to the table s toolbar, which displays a pop-up search dialog. The search dialog allows the user to search for the next or previous match, or to select all matching rows.BP  ,  oV-The TableEditor Editor Factory: Table Factory If users can add new rows to a table, then a factory must be provided to create the new table objects. The TableEditor traits that specify the object factory are: row_factory: A callable that creates and returns a new object instance when the user adds a new row to the table. row_factory_args: An optional tuple that contains any positional arguments that need to be passed to the factory. row_factory_kw: An optional dictionary that contains any keyword arguments that need to be passed to the factory. PWP-7 - gbebk - gOepW*The TableEditor Editor Factory: An Example  rX*The TableEditor Editor Factory: An Example The resulting view looks like:x]The TreeEditor Editor Factory RObjects often are connected together in such a way that a hierarchical or tree view of them is a useful user interface feature. Common examples: File system explorer: files are contained in directories, which are contained in other directories& Organizational chart: Employees belong to departments, which in turn belong to the company itself. A TreeEditor allows interconnected objects with traits to be displayed as a tree.XZZRZ FZ Fy^The TreeEditor Editor Factory DUsing a TreeEditor, each connected object in the graph of objects to be displayed becomes a separate tree item. Some of the features supported by the TreeEditor include: Full drag and drop support: Objects can be dragged into the tree. Objects can be dragged out of the tree. Objects can be dragged within the tree. Structural relationships between objects are enforced. Objects can be: Added Deleted Renamed The contents of objects can be edited in a separate  inspector view if desired. Each object can have a standard or custom context menu. PPPPPPP     ,  {_The TreeEditor Editor Factory AHere are some examples of TreeEditors being used in the VET tool:.BZ  }`The TreeEditor Editor Factory hLike the TableEditor, in order to provide such a rich set of user interactions, the TreeEditor needs additional information to perform its task. The extra information is divided into two parts: General information about the editor s behavior defined by traits on the TreeEditor itself. Specific information about each of the object types that can appear in the tree, provided by a set of TreeNode objects associated with the TreeEditor.  @ dI o b @  o ~aThe TreeEditor Editor Factory "The general TreeEditor traits are:.#P     cThe TreeEditor Editor Factory \A TreeNode provides information about one or more types (i.e. classes) of object that can appear in the tree: The object classes the TreeNode applies to. Which object trait (if any) contains the children of the object. Can the object s children be renamed, copied deleted or inserted? What object class instances can be added to, copied to or moved to the children of the object. The icons used to represent the object in the tree. The context menu to display when the object is right-clicked on. The view to display when the object is selected for editing. tnZZd6d,{bThe TreeEditor Editor Factory The traits for a TreeNode are:.PdThe TreeEditor Editor Factory In addition, there are several different types of and ways to use TreeNodes: If all the information about an object type is static, simply use a TreeNode and initialize its traits. If some of the information is not known until run-time, create a subclass of TreeNode and override the necessary methods. Each trait has a corresponding method that can be used to override the trait value (e.g.  get_children() overrides  children and  can_delete() overrides  delete ). Sometimes you have data that is not explicitly or conveniently hierarchical, so you need to build a model that exposes the hierarchy. In this case, you can create your model classes as subclasses of TreeNodeObject and create corresponding ObjectTreeNodes for use with the model s TreeEditor. An ObjectTreeNode simply delegates all its methods to the object it describes.4MPPB Di   >B Fi   >eThe TreeEditor Editor Factory An example (Part 1):PfThe TreeEditor Editor Factory An example (Part 2): "PgThe TreeEditor Editor Factory An example (Part 3):hThe TreeEditor Editor Factory The resulting view looks like:P/*)Saving and Restoring User GUI PreferenceseThe Traits UI allows some user preference settings to be saved without writing any code. The preferences that can be saved automatically are defined by a View and the individual editors contained in a View. Some of the user preference settings that can be saved currently are: Window/Dialog size and position Splitter bar position User defined table filtersZZQZ+HQ0+)Saving and Restoring User GUI PreferencesUser preferences are only saved when you request them to be saved. You request a preference to be saved simply by assigning a non-empty  id trait to the corresponding View, Group or Item object. In order for any preferences to be saved, a View must have a non-empty  id . The user preference values for a View are saved in a global  database under the view s  id , so the View  id should also be unique across applications and views. For example: View( & , id =  enthought.graph.vpl.graph_canvas , & )Z> ~ 1,)Saving and Restoring User GUI PreferencesNGroup and Item  id values only need to be unique within the containing View. The currently supported preference items are: View (Window/Dialog size and position) Group (Splitter bar position when layout =  split ) Item (Splitter bar position when editor = TreeEditor) Item (User defined filters when editor = TableEditor) New editors can save their preferences by implementing the  save_prefs and  restore_prefs methods.|ZZeZH0#/& % eP + >   r ,s 1././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/docs/traits_ui_slides.pdf0000644000175100001730000242260200000000000021166 0ustar00runnerdocker00000000000000%PDF-1.4 %äüöß 2 0 obj <> stream xuRMk0 W\h*3h[ة[7F^'cI% Q߀F+]V:_$Yܟ,t_mTaI l-CGKDetS"L O5j4h5'p yψu2Wbհ3Cdds.2jǗ xa|uULTz1"t d瓯ɒk5,̀A23i'?1pHJ$YcNaLp@x @M%g"24!83l෿rި endstream endobj 3 0 obj 337 endobj 5 0 obj <> stream JFIFC     C   " P$ p  <7<V<^@ na71GYpy:Lm ax 67g_&Ab]Mk><0d&a;<^2yB]Z)h^>%mwv\lf.z~ezYOz|ϳKҲ} ^3w/`nvrw=O'>Ùs]MMylߙ^ƷK_v\ ܦ9jx/I;Ά;E!̲ΎS`Yvy^M^'8SWF6-,oGs[s&O[l[z}'qȊ ( (Ip:nK+@Gd* )wStݻgSfbMkcOYnsAC_4-\t#ws7lw]0;K8lsvSSڙvSmX޷ywrY=C;^=5Qos{mb-8 -nOuLJalH'ͼS&<H*(R =r 2wpΧ.rh~3?O?%eαb9VEqA@p푣0r G EU.eV"o FʘR';Ҟ_&?Oxͽk^LD-lŪ`keJZ'=d 9E4hA(  j#f~lf[[xʻ+M}ТQ-Lz?yq. n+`GL5(ekL7{7u-zK[+ (@l"l#l״j9EA #iy MvٷiڌYLWatF~qhRz?砜OK$@"#,WHǡ ɫhMʳ˷IVŵj+1HDjB0J "(@@MfO eadi^э{Fj9AThw+/ÞN}\R:j5 <͜'[$nn=#B9b;5 Bܪ=,鎒[/VìZym^Y j'F'u\u8pw2JV=2/.nRr]P7# 2$i^ѭr G4AP@͞`[^vzVrpiK)Kop9̝Lk:?<ͽNb#ׄD F2@#$Pd)f+>R&5"Y"P^P@9 xc2&H6EAPP74??qއ ԕaU5OIh^!K"ا!~r=/5yzsœ>sAP}|d\>?46 @PEDPPPQQSh#"匉00c\Z戊02B-R̕`q]AryQgvgk>yߡyɔ6Gџ[U4s[MGueD8ھ'bP_ ((  ((C$C"26>1s='!?:|\X:>_ RԔfJZFXRVvpI7h7>;:\<Ԃ5l[C>Bj'^ NC$'UlڣYog`{_@{ T4d+a[Rr@YΕK>թVaչٟL3t{ZQ (( ((8PQ*$dq1sA"*uz >_O"shOZaۣ<ƽ*Xev:Ի/1XS:D-o[\{\_stS츓7O7fx ecVZLm/^i2͏-K37kLˉ@P 轟 _])hkEqR̨߹Y;bfPf𢂢 (*(8G4a"9##c1hֹ"z'yо!/G0X56y#rt->6Oyy!D *^Eml]idKyXsxw׻'ֶ=^&sƽ|>TpGxxyvJ+CbF1hֹw\/ü1^7n2Eos;:6(mشP3y7|CJ"f?~>.t;Ŝ\z.vHh "(z/!0 ǽ˧~ii,빿Vǹ{q~B <}L_v)ϴf=̈WOMLyO==~O㥫o>- =myJ#2ƐppĖO{/{Q]zqLN{<,/AsKUbj6>q^~SfFX ',41wk`\<ɓQPswgV&h43 ڜYSe6t:67<:C/;ZvwRBܚbr:tnØ܎ oW/kqryuG/'qȔ/пQȴf(J" 瞇\EWő$Dt(PIi X"8=Zyz=[}O: : |Տ;=OAU d|żLV[5{΍}]åpyzyҍ@Q]N5j0M_Yzf:skʱ[_ М7XoLjfPhɪipdZD+]v&2A=.%V+mLaW5})szkK".d93s{ xJn8:+\/q/ryqrh[f[nhF[Udh6=c QDK'Ṝznq侑oB )&9cyܝ fWFl(^86kBo^tqPX(c1A@Tp*r 9 PAD4 NWw^g Lkw3bz->ۗO9va"(W"r8W{d#$#d+%4sMG9,yr t#cXbF jwawG{=8?yRshd9iN^N;D=[6XS˽ph ˕-B9#@QG****(4p* Kw=[IrE`R=+zeoGv*n(G99xdQG)$IFHI,rKE1,NKb!xUhVh4@A|EVizU.^5.ѼJDG] B=hҖNI^6z.{6'b>9ֳ^PG,b (**QQWEZXK:Bjj6$Ot.a+q{4r8QTQG{=x!$I4Sij94&Ę00 !zz)>RنWXeTUW#r< <GA{=#t"YBN\vBXi]WmavNL6шW5Ǐ7 A)<,OZr-X9 jlɊʇVd:$ÐL YJHFivWL kd^!H(Q½+, $!(##$"|!t YԮXBYiYXVmafY;3+w5C:u?**D*(UUG¹95ddd2a2qӶa$̓(׽+e 'Bi]XUecV;1V+0 ^9!hY+##"(9RGI"y4HO5yKV=Y8!12 "#03@$4P`AB%56CD&VSvKz$+ʾ8JVyŅd>2W"l uվ*XثQ)zuY[͵a܊5;=df&9 VUD",V =''I|^6XvՃ$Ë|ܩ_Gl\"f6W Ѳ_J>[I,}qO`Fb$$Td@n֖"Y-bҶ6[)j6ZVe=>ZChx6R_& N{%?kOay-_u^c(2 ؊VJQי&K^ieLg޹iub>L7'0JB܈e2O%[zI1"%/.CFvVֵ^g*>+953{ zߤds함QεL/FT8O]r6CۖogD" Ѝ4+DAP#DT8oR#M ӈ*:&&&h*N&hЭ8DAPqCDgƨ??ý}R9GF̖ +eJpJ&) o8]mZley:QF);J:يϵV,oHޫu?e|qLQE'܄)awk!w-H,::h+eSks\:cw;WOS-[+U`իSU͑~j=[g+*WY֣3 A R *wZDUZb:l,KGF&B&TK&rcݱWRE1C~[r71LF"_~((p J _$ewgV3VĽ:Ig%߬v5dLk譋1.[oނC&[5[_5lwDmӵuh;wi:%z(L^Lz%}~t2zXHrr h̅#ylllVIhW P+n;$aOX{!1iQG!Bc!k¥ϺۿѣF4hѣF5*ݛzJ\b~ŠIǡBۨ4 &g5(n8 rɮ^_Vyczt-w"1]YŠM !ˮdP"rZ}y|ԍ$zꩪ(\C]S/1yH:Y9 o7F18QK/8xTxNUSߎܮyOxR0L x/Nmw6c|;=+Y*cE5z61]ޖ:exwc XQLlGzI*W,,diBZڧwO=~H2MlKZuA;:l-j^JM0$.jԻl3ѣ]j152u`8G~uNɺ"o'bωшgHM_\%F]w>_;UsШmgAl8ٮ#bMLyfXن.R m,mbA{*eO~VkW6!qf2ueZJ&Ō}dǘ,V3.kF?hqW2-M?FGȎeC4=լIWj&-4bBT$'PQw>[?8N'Gq4q8N'[q4q8N'_Ƒ23)R͙-Ϗ%ixK'oe4hыOJHq=pJ"/_BR##Yy_(nfF"cX1.ܵ;XeNyFJT%lDB£?Q~j= -SFt7 ҾDNEUr!f~N^SU%BT%'_?T%-ܒ}ڔdTf*xc ɺ;%i&:BQE_w(SW&BNʞ3_Z9[׭']diIIIQyRH.+])V5.đB$$>5HY¤𺼿TQE_ 4_Hfρʮ\un9bgf͛FQDb8J^U<_7 Ndi?zA9i Uв Lf9.XIG"J(^' 'iAfJ{< ("TV9;Z7H.Rr<$ Am~S軵tf&:AB#ѭD'l}J#*ۚ">cGgl<[Wy,48!?hcYsbϐZt/iYp8N"5~ޏ?QEQ~6 /gѭEcEFJtlMDSlq!6!oiNnh$dy_ER<_3G8& 2j6x5<\ӹYF-Ԑu7=Zv#F7}mIpopMA(*Mm+}R;YʩƤ#f+IԂ+&pFV:gETNl G!Uv&NsEc&/xvL܏}<,^4<-EQEX;*ߩ2)_qυMV ]$k[kn[tN8:fTYol߿ZFE>GcUd%H-Sʑ1?5EQEXVLl!/qp{ݑwGXS+)LunEH'S8=RkO gERNk%ON#.;\wiVe(?RJRE﹔m,w:=ۗzpӵ]>j,:4h׻њM+[db=܈wwBVrGvFףIc{/t^YN2|n#5(SvB 3)w#xy;]@UU<[Kݡ̚[)7CX^+UTQEQ~Ѷs>{l1Zı^Ԃb+wč5oydDV_'__U_eꤍ_KҝY![7ĔTlD#{ rbE `lDJ]K{RN`iCuhk10Ѣ~r,v/r9g3QEQEyA|fd;v*㣜oF9]?# (I i"?'A:}w4E(42ި'kt1/c`سх*Ǫ MPZ!PW9uU*;)~[`ޝ{,v̈́ʺ;qk/:v~Fvj1:;%q5O^7.}sf̫{J?UÅQESo]gfdXs+ݑ7$qv#[`=:ԁ\#zu):z"c^(fbrxRUs0!PBդ7 +,h>$%[^+rVw5JA(WQ4?U"hC23tl6@]媊[߁%+چ=ޙ=^Z_8QEQEcxE6꜏㰼eF[}5FHh# =ƶApWJZ_gBA% _QwLP54DS޾*͜Cđ vU6U.cTQD7<{+ybN. -PZqy&F(CVB)Ϋ ^^ ((m#3/-'eΟEF- =/He"ý._Jt?!LP.ȋ?ĖNvjh3Usx;'qQ49_`O>:te =GddpOp9M6r6HAAA( y O1X3BxESաȳ- HHeWXBa /Nn,ɒz:(*6r9b}{zHWO3 rofD۔ӱi!(N bO}AAQE~ټS|hmRgqK"bw,sy!!_Eb#%'._ZQiMuy;'*prJQ)U5h:m&f+V$Q'Y}lM'9f   *(/n304$w|^4All|;!QHdCXR /H>9R2çKfMy |G3v(qaeAAAUEQTQM`|U<2pN'[˲AAk6-{qTz)"WBDV"pCR2x/OX(cBzcuc&:;x;fІՅAAAATUE_z:Ҋ/E/ >{_ (D   }X[U0r;n'1u!]Qډ]##G(l؊T_Դ-<(hѣFS#l;ÿy,n%ys9w_   ߫O0AdDb *lXvwf1 kPh<(4hѣFhMr0P\k=iw    W thj$p&*uxCFahڱ4DD6lٳf͛6lU ꄄ` /ѣF4hѣFb9j-tgc=fF{6#$;h#H   }\Rjr25aK岻6lٳfG#9ERʉꄄh"‹z4hѣF4hѣF4hѣF4h'Ru   Vq{6lٳf͛6lٳf͜  'XwC;'P"wؖĶwljh49!qIBT#_ "AЈhѣF4hѣF4hѣBFGsO^ (UAADAD4hѣF&4hѣF4hTAP_>; rK?C`tyUh)GoE2T'f:)Kc118,lQ% F ^:LE6IԘ)1[f[v,;NwwCnCdFX;} cc#h/ADDADAѣF4hѣF4hѣB* K:WC s#rz7,՞l fg+NYHӣXlx~+7q汘faQ6WWG+ZLwhc˒LlPWL]"NwC;NADADAD4hѣF4hѣF4* * (PCw ^os s-㨒ۖw2đٕ[!VL59;d`mkdr"\#t " " "4hѣGF4hѣBB* (RMA>s6    " "!F4hѣF4hTAPTQETAA?AAAADAD 4hѣF4hѣB* * (  7!1@A 2Q"0Pa#3q4`$BCRp?zO?3N%Uj6X]A*S \yҲ]AjbV6Djjx{G}LSŲwOU#>mʯ߷)6JdQ[ǪQ.w+v+*%:[+Cʕ;o*alB5{RXݠRb;Uw&Q_Co(@Tk2mE4`"SF/hT@i"Ne][@)Bk &4eD9RP ;JhK*7{GVWM~ҥ2H.SN313SPh^Q[kkfIf VT/=@U}b*W ( !j7Ay{j= CXg* Q,6Tv ~Qg%}wi++]&+6a7@-4头+ ٻE骋Iw8лK+2hI߮'SqnFM(zZ+K< Veif2ۘ%׼K7epzm_xZ/rôJmQQBߨu?mtx=C$¶%C OQc0"İ3Kx ü Y6|%~8iSc0L]lM#cIO0{b#UcUf9al=1KJ!_*e&aƂZS}%*)GIZY;B)6ߩэ]O ض11\k 3+&Q{˟5?WT]"33t2L=]=: P_DՖ؃2 ny`N+-/05Z[M]Q A~#hA(ʏ^j;;!|]Y>d~Pm/̀w˔ۤ%<)3q5ҏ a[am#`ϺSv1aPBo:$ ǖGBDѧN=_h4;oŇa ASߐۡ#q+U^wo>ҏʿ|s-șy~Gnunxʖ#+0?\J ,b (Pep6@:`gĞ$eiaqo'; Q,26ݦO{5H8 <5CLs漼Oe|?o_&"LjF/tɍ5%N jgq)&./@-qVL64IVz Yib,a# ]`N(]e0-[ :N~>eC}f2#]\U3,8/E83x55)Oi?$ r?yiKo/6!}^((G%&Q5!12@AQ "0P3a#Bq4R`p?~vO.^n,Һ s-.T9#}*m_C[^g#Ի۵hxXԛqĥSh[D= S^fpI*"A}JfWqiY<Ǡ 7YQ55u!L}5)NmMC)ѧ|QjRx@OPܦKPz @j14TA?+U3K%cL^qq*Ӑ\VU'j4pu5o~V 35u ksH-cې` @!j/Gh9ܭ@Py],cM>$nrf3IENcOKo=mۘD'=5U2[)E޳1 a"'Y\@Py.S rN&(O/x\ '~f =eLیZbGp V8 6KZ{5h[W`o6~us"OIuTM `'GIe{ Oʎgb<VٓF9 6E`a  izvJ.Bd;>t2y~Sʹf#1~eQ+wXIq}iapw\'#0#!`a_#v])Zm ^|kvWC[A.ѺM(𱚍s?*2|?m=]J|S<j|kvWsWOͿbbbbc.tdD?,&,+9Tc2@O=7b]wJm!siM W4;6M6V?= ۪myy'9cVf6Ͱ>ffAoSN8@jX"t0O)/#Om6X(l;)_]~ؽL]s6/w+;@51)Hm Ʈb6}"q6M6ud0N͋FGI=BSbԔPA(8si3\ɫ,SGZPhNrR`U " I=RG>zH =dFmMt]r&P!O&HmU_$›MzNVO,IHC1ikNSk 1xCwoM):rL{pTB#(՛)L39̋. sOT/dġֳeCj͊I>0XM c T0L H $m0kahmدtۀNsQGVV:ǡq7;či#IJgtT ʳX"<]6ڜ͙&ŸǶUάj5 ;Nϊ *ZC-U!6q1zQ⭗E>WŇb(,9Igi )BǂǴՕʼ9Ֆ25WzkRW:Qc;aNBwXMS'p)IjHl2U`ȏL\9+lNkZJ׋-'z, ~ *x=6NaUWU=!ޫ|XU%Lz{d.񤂖ݽQ?sgB29Du XLtCD|># s(^ZTt3Ŀꎈdr soqܚրU 4df.TkS6lM&B PF~(ۆNj귂*3zEXpuUHM{HɢjZ,13.hRt&LL&0]*tU :1hφ؂wPFfEZ-b6'6g,+5M*3g!l7ZH|yv eeU CKDS0|4Q4n7!]ͭV8 dcQ\d-,&vn;à9B ^XqU8Q "ydívFM/h3F@X~>yVLԧbc%m$\H2ySZD*S2RSJv+Lg\R]Ta#T7I;f*]vb99Mn$>9 xz؞@2?>}auC}Ȕ \un`0^EњZH%Z i$IhK9F7(sX3'4:  Τp 1{r,ƘNUQֲEv^mLm=*ث,e" 1>B9ϊ1ǂdVG=ҨMM^eRF)<\.Q`Ɖ̗8p"V-6z48Ed &#TǾ R!D!׸HkɊe1%c- F > mr6=n*8 X&=|3`=UT<!q2kp?L&l ։6{Nn2Mn;3X~70ⶫL|9:)ũs 8L6as; uZ^Ok`")Mڏ926>-i]ڍU!P˷ou( '$4mw;*<ȵVFg8n`NV s>"JDH|Tp֓/Jv"9:pyv sQEZ?(HlB׆0%JF4&V϶[{+V kT?n%Bk4Z PX~ɽ: h.͜H%Jtj΃ޢR(MNsTX(.PCa`|ӚvќʂgL*{'FAt73s* ے?"X26 ۔Z+hiXRLvMEpv(t/ֹ@G5'> %X2$>w30ߑ5.rW<ȇ@21xL{>͚eZTa%8&BQˤT*A(p`BCf ֖+UMflӄ:])+J5C4B2Thq"ߤ5JIA26kMVR5h1M^C 72j7 ҍ .ܩxQ( smCp2BvSMSim(!Q*^$^JB{KY %C[5auXWo9ygT랊=Pv5gj0t QT)>wh?@2}Z;)Mn%>,SYVnUhW7`2kVxd-x(B%Aeڬoa Ė&ߕb:&J1cnl8PޛOS6m UZk}{7s[ Jei1? 6Mݛq? u.iOveUצ)`mE·❝$ҰMؕR[҆ћx-l2xZw\nhp]}%g{wS g;(QQ3CX;(Y!%nZLZ3a܋x$c&d߫z$j8fn9FB{w$[5)V @r:+%:,C79s=*g'q9nC< d11ܮɁ\kٿEZ"72l@ l ,̊Z2hVu<hMZ PX+, RlUv)MfFNcB9D/d#̫b;\ 2kѝ [T$w%TTg75w(QZ-Y?N9Wg'RIoE [EQ&b,]cngpxM¥6G6bB`1AJtl\{$\TC  )r 'pB%uخZJB̑xfV"N+[R%+SdG0Ч@9TŇHt;^y6'5>zE %5g%FD-^y>7Aah RwŽi5"XÙR oyF h#>Fk!iQ;iQeV#U-Z4~6-prgZ<27-VEBъB歩-:+' O7ЫVBq}{.\: J7Tg8dh^2@wD2:7!P@v䳨G\; n3RoH;q MQ݃J'6;S'~g`ω6V'i^ȶ񩰍*$xqM\z+\;ZO'԰ۃBOR"v!LI)YNe^-r 7V%̸NBjmZO':bJ,YqZF|%ԍh\Qvef>`t1!o8q6iv_U̇ãVxX{Z&dV8)|ˈŶl_%c*֕-ҢAcJl23 xMBRjfhO2QG ŭcq*#OщTqTHhU̇0=5kk욛kAv0bKP1fU+T*Ly:&O 0dhdž;U #}Z5lhG5E{e+V|zrBge(,Fc$_h7rU +7pPRh.;*hfX!M! vϱ۴]%|kEo&CU\ E~Dڨa0r.+{Id51G6M(G]%_"Ѝ l^ƿtY7QG3 \Yv#e6NYeb SrKk55\0FT0&FDm_>+JJZZxثnR4ta@R-vNbH86"c.U]P4~W8GK>PyR-çA^5=(Qe},*;,/2iMHjg`7(樏i#I J*P`C>V^/vD:u\bT$p>(_,#a"0Gr87=@ ^QgO(PfAMB.)-)?&KU]bZ]& a; |͑* Y8*s(q~'rR"W%o)ׯRڇY9n)[Н?ņn]J[#|^wybZSF+̂e!bJ5iWf9Im;y*#WCn.Tdg(VEM.jȇ^¶ i!v39lSD!8$%=S )yBeL<pZnKF+kBeZPC?)T}7)zTG% UQlRЪܟO<9oЩB8t!U_7 rtZڥNf ZU?ˊtEnXTv^)60, C(B njB`~'o@Q|aaR_)ODqV.:i1N;Ԗգ#xLd1?i{?B` m(9pq=j,QnX"ⴴ l6}q׭0eZ$."k)tF5UPz^U~nd jh vt7J]'wS*b+Z/ʼ̌a~X_/sD;~;pQݿ#x5nZϰ #b gmrvG9DmF~J4k2 -X%Sx VRSuB RCo TM 8/SI4W*|(z Yjm%DVÈm-*Z֞ 1%R݄'}S!#NVA[kslT?$Cs=Ff%>f>^xF `q pw\0}6Bp^h;Zw+s/3" Lc:vނ'djK됏GhMYsW~ Uv(U )#}ɹ9+ǞK%ܵUG,1⭎R;1{(b,~_?ׁweߢ2a׈DY殉?%|\2aaV;&,!k| lF?ԅ"!sKݴt{7O$&2xQ#Dهw74 9PlhΐR1s<10oO;D(Ck27{X]?hsP84hz)6,!B۪m%9;ID"nPgm6#P/AHޙ \3[:q!WL@ߍߗ|clj7qGf_t%~k{y+0kE;Rj*9{2^ϢVTG~^)ә:lQ*<z(RlO r,O1 ;DqN):{!;h9Xd9ګ1HO N$GU m: y8םq3ڋ0)h 69:H5aͲ ]~MU`F2B"sx'fg^V-Uj|\VX9^|C[ΏMOq:t-j cG<'ҋ o3@d6ns0GVwV3p' O2ЯدSnk翇Q]. q.K=CZm|GwqiD~VQ /{4(|ɶQ!k~a^wA0&T H4fMʳW7iOx0q+G%4$VH,A|荶utgp r@o'y &p=;}&%^f(5+\ю;5mG83$&֩PMC@1n7)# =q jr(pοeȓzi*io\òWXUgyt1H25O䪛\ٸg ܤ.RV N>UII?}\Qcda =؏icmr=ë0 #;ǞqHT<INq qC+JxqHgL=] H\%C97{zM=Z7Rw}sm[7G zR.oVj+:V+qJzZ[>BO|%d7쏊G[c{Զڵ~wrr[|֯j`ɯGһ߼?C/?%Xs1~oX^UZ.̽_HED3c{-kmF+\}g<At|O [M~+^6,pc̶J#d?@b[DhXmtqWr*4~t +4NCkLFˊe"FsG`GhԦq&FȜJ8CF#B}3{l\G]L8PRFm:5!(Ol' j %QhBhᆼ[j娏ĆuEܨVi4ꌉƌFQbF8g4Wlz) wPbQh ʼn2k[S]J CF($B/:!i9͕韱ѨuvQrڵ.?肘k,$64HdtG¥UB %Ҟcyq Q;Hj<(qйV.Qy2N}t?EʰbCRa`Xmܝ4w(0uw(L{jh *e #Ԑ+sz"By.#v5 I64xBqt(uRcREW>P dNr )5Kl>J;; +/өWdex¨=syCyUrhTg.F3i++!1AQa q0@P`?!gПC/UH"גW8Tw\fLZYd%gĒq%DΟk1671鬚GBVD#FSm~cB!?V. AOY. mz|Z ԹъRw +bƛIXŢ, PzOG L%;cc$ei"TݎcRT/ndaʆ{F4ZI4Vb'09,pm?CJ8$/BAY{r`1~>dMy"b!1CEyeDÚQ2!(d la̡# (6]A"{i.%"cb8_>)"&,a)ԆfsQX"MI *zRJQo"|g]ܺ+>ټԵib`'2 .sn)k\ȥmSA ռ!O6/DKqSc59^S*̂-ՆfPYd;3ocWQȢ“؋ 24oS&N J%*ۊ̮JW:n=\ΆFO$NO6?k _{|>b.KXr%IQ1BKEdȑ#d-^QTG:N$]^luŸ;*ƢC&E)Whm2JQ;IZ;Eʲ܅mCpGj!ol)h&&lA< ?~b5\B$pb DӑGvM\HؓXGVjToD%'H8B5veRR5٥Bc5;vEe _d57D?JWɂRBWh!lRvÉ؞uQt0еgk5{#xdLy|`A$^U~PS,O pEDB܊(F%ZGwK_ TY7&Ovb8S=0,AdR&ߍ'8ER%jVx*($LQJQT8}QDm"niK:@Z 1l$e4P̮FbBU4>k)OQ_(UYA.IdM- tX0J6~3:6r2YRJ"t!:K;QP$JE@#"5]$ϰvG]tT2m9͌U&>we@NLP}BCNJ6SFlNJlr^mBHDm (АH|d]$*%#J<#R:EV&Qhrb3cJ2( z VV7LkpBRH&U1AaAAaApAAAAAFAcFGR%$%ya6Q FMqJ*LMlBцAIvw)l{iX\S-ѷ@ذ 6*U 'QAQd7QJE5pT<ֲ҅䴛|ēؼ,eVEp܎Fq̕.\`֑5A;>Gv*D^ZtsgN4q‰)HnC9 ՊUd?S3IYT9r67ҳVٱ:#TꖫT!ulȍ*S~GNߑ|[L?{@fJ:ImȍlإdZ3Qk42$cKͽ.Sz䙠%J@Qu,6/nsBIFS7$ӠbtM|;!H+. ·>nq“<_&ytdoGI;!i&hzUvc=H"jRDyQ GI?b!6r/oHeBYެx2eŐkԽaiHôvi_J{.&5Yڿ\ʡ{XΨvZ,|w *!$ 8؟Q_mv F,b2K/!lߡ6jZakO?z-D"< ff*ٙk'J+"ɡeW _J Uo0T:5z0lVYEd_/q+\§fK 1rRtE3dI*<(3m "^JcLikXrf#dyfIzQDDN<j7"oݔY!G* J"sZϝ Q:Gjj6&\TW} ̝̚ŅdL _:ՓEi2E2Ђ@B엕݊ nYs!H @=']4_A3-T1c+$d.~dfrVU}T]F^0׏Q׫~WEcF*:٫GH;i ֖JG&răzVY,dmVfIEBSD:m"q$*I-[I@Tq<$K&ɿl2ldn9!=aqR+ CI+ZMtT$P!: -B"S 0 ` R?#@h#Zl!40:mscѫqHL9K%BZTn?*F֣‚\;Fȃ2 CgHS-$]!su, ;|J[yhPh%KZw\BDՑWn%Z.]eCeD`ק>J"0)CRv|6M 0RgJoLd%n))zYBZC}Z?'@=Fde(RG.۠Bnka\MIg(w S6PJF)7hUo/)fO -$5< D4444@|jT$W)Q>)%e6,59z2KdJSyEtIڅZ% Kw='[\J AU09~cCCCCCA!)--\afOVߝ"N3O)&BP hRrwe%5cQ#9H,f$dr;92IKvHWy SR~AXf8# X1G x> ,%55#dhd[b-GRT1RgHT y Y6R׵5\SL(u>&J{NLNL3^pW*_ЃCcҪwͲd[UcB:y2TT"DMs_N)RqLx ~.p l(: 9)ǗMhhhhcxs%K-I(Jajxe2ꎭsbwͷ&E?In0( eHDU_i˔$gH0r DX#lωCe%Kw|g0A1| x)=.͉ SDo|ܴ͢,P#8(vTJ,)bhC_l. DX:J^_J(CԸ]Z[WQ-ݧG.xO#A cNj KV̗Q`'US#DHrD6m8T]Q1An-ƞ~Priإ. sbI* "aSJdr&<]Cb#ԏB8#1W_!PF4٩mvīWeSHΤzA$&\i-it[F4| `Ky$5TUl-a !U|,kWpG z>1c /eڌV UŮQέh>WB[J)$3'+!1b$nsCU (5g~IJ}%B8kV%%: ,-T>i}ɞ1OUBM95KSd gEFIo#6U _Gq:2}5%~M"Hiz=5o`<0]zUBY.ű QNF>41!6XZgb\Jur:HNȖ=݆`+9jц;E~J$]n ʂ,7jl+,if/ؗ *GQ>XIV"Z]y տffmW$co؃+5Q<#Uew&u|:$+?U, W ?@.8B8c<Ώ94[@?!Җ򰘡K=DF F8J £r|Ɔ6f&x`'*C|?CwAM2+G,΢yy.1\DCq:D])uc99g}fmFE݇֎z."j#I5: X9w ӵEVCī3qqx0"?ccKs4>oM> * ޫOvr&cBn6b届PƦj }8xIA)2 <3qc=![ ܑ%51tFg#I~e6h߁gu|/g,#pAp1{g0 ق*yanI,pcOVi2 ?kqm0M!LBcI0nU]= VEKr2p3rՋ߂B\cǃd;ST_8/Z*.XZjZڪBaJHso .-BRecCQfiX*9 4A/DCY2#AeUBF T=Be:2YCx9HHK1cnj=wO df3)ۃf?'Дt~ˀ5"BPe='#r.ܴ KC񙶆 $P&2H-cwƧvI!Lm̉E`I#y^ !!!!,G1cީuZ/R3rgY;Մx1Z;[p؝KEfp J ?A# :eE &NjٵdOstQS9&r/\*4E;UHQ.ᐂCVW!!!!. 1clStIb۲3*m᷂rf]Sƶy Ii+_S..ڬ}uQfj[mjQ+s"tcQ !KF zKlۙ1Z 6[eA ]^\1> eZ PCm@ qce_G5oՔLDɛli>k^px\PD$$$$$$$$$(&1cwEQn.6n*89Dv ?z^DBĕyf<ia1ᛙP7c< q"Z3Φ˚ʦ j3+mfLrFgJ"^;wz/$՝DwM}z7^8Ԥ"xaǩbyi!LȖD74 黝k~=fgq'cNj+ls&hߢo_qH@scruS,,Ee%_e$SY FM/2 Ah@g|hυR_Y K'u2c[.e)o .TbȿO3[ y܂$"o8 X9HHh`땭{I.z5CtAlUqRGkۺh9V>.bMTI C 뛻 O;a㉺ JyڕI/ $$eבI|_Uyۏ.膰BQEL c Ï$y'DϯVO(Qr*G!pd)RE*pADaxkyzS™-:}hOٚ uLe ҇tMy|kZﱐ+K\yoN|ޮ}ǭCw,*_tuQ#bH\ТFK1-4/5ST^XeZۊBp#)Bd!!!!!!!!!!!QEc3GJ(&Vs!y8Dt >p咇!Mℨ = ):-Qhqpm=lZ/ᑽKF\Nq~$F5Sp;u9r44!fIwl1҇a%wRp,n6[,a/jNZh̐AU/ cUsѼ'mlzrP]nۇG-~%̫wbRł0gb$N iAE1cǕGޥd?3<)'b7$5{ %tJgWS$d[+RJYؚRE'df\d.Ejpc pcj/:+qbOBZSЃ|0sw5 IDd|b?bQ9Ԉ"pku+(bYS.Koi /cTC-))iD%OIE/wvĪ|!I A$;GC^nVlF6d-"4%f?Mׅ 8'Rrʻ)Uû <{S 9/ʥgвUWg*"xN)S)2+j54ڈ9q*&QDξQVis!= yhFrj+)4(݇Nk}b7 c- -}/K+=O%@jOȃxF|5gӚ#"l! A( B5?2j18S;P>,vORERd*n n HMfl]H9{ty@n]lnqYv-s7@KCm\@"KsPT+ &!l8I$%3Ac}e6 v/TS~bSca/բ=Y(v'1B$&֢#X\ʛj, xhv!IFBBgwE:1t4Sv-dYq>/{;9 AR $t\d3u"{Owu mA}H_nWۀ>8 BKw#"W&5HkCMͫBgO͓W3M=FR}0? `Y)s`ӝ6+m4221%.5h$I$IB`AAE\il 1'#DD)UR5 QCST0rXhJ_㝉N lOSc+^^i>V P!B" (T 0NJR19|"d`GVEs F 6lc"xO)c 9%0e 89zj9E96^ ZfCt%A1.| #K"[NK%uhbP\B QEa2GK?dVVǝ.D8aPr(c -2yz0F1AA 0Ä؂TnZq F;on*JU-~D.8 !B QE^x߂N;ri #$BVأ&NX&u8|C߁=  !! :HHk!1&_ 7)gQJ>$ c`!BAQE\[c?SO>F*,{&7z>Pr |QcAA$$ 8;DLz_Y{'77%Vrr6\1İB, 8EEQE ȫA"It 67W G% J1^AA@jMVbY VƖ, -Qb!^$ccjD\WUD_]c4N%0|MU= [Ixk QG!^1 kF#ϫZA+xBj )z-aAAAAAaHQGsI$'p %!  dY1+$+2p [%Z"XKp|*= " %@n3 ̩#^A ! ccd<9.Qہ5I$6"!kB]Rpir-EA$A z   HA-Ak}AAp!B>z֞{Đ;B'/ ؇sɍι, "1e# HAzHn^6 c6~?Зw_#6k_;GAAAb!X7<;{!me+ *i%NI$==eY ѐj$y^"%$$$$$$ ,@AG!`X'#TGI^ Q#OO X.$AA~ 1AG!b.1HT 0\ jɠޠHHHHHHHHAAEznQUɏ)o4fg"no9rxoDFy\B$$ '*(+ Ye| iU̦~[-M_1!WCYJͻ\N+;BʶĢ5WRFqK&UiZF](Ҝ9k=&i +U W>AOr,a7!jY ng4 (P6E ԩነ" dN37B&UJ(d3H;Cg6fL ӿQ&!P H1&$ElJԜa!BBAODQQXk,ЃCCCCC>XW4WQͺ(nS%HZ mD5*)WjҘ[-61hE2Hްբ/36@;Z?tͷFs 寯A2[bwg!rPQWco^"ƪ搪*f \ҝegkrDnL]KL~ ۔nɮLy &>o I B  z@*(5YeYaA*?9Dv Wdo${1̅p$4^0TCV0{D扎{$6j*%RԤPOtT;~ߍH*$.p*gЩpHr!Mw2ao;nIy)FJ5k Inx<|SIB, '+, CC  CǃBaq {{{{{6|Co vmXsofN*@6B1ҁ }F`NHPcZL} ["ok掺8m2`j`([瀞o04[$<[ۆ1[Į/!a {{{ノ#V /ﮒ"I4{e:i{J 0ˬ/黎b½h@e}~\[+sB{{{Lzݛɬチ AA[)uM۬h (f[Q3zE$#J<6Z{, |90H,N B ӯ )miJ qK{ᄏ9ネ${i$#Xf mcJ E[Mm/˥ h,G>c>9 yh8*,e3 *{j-ntNL0Zmk&)"OX07 .)eTb"$c 럭.`vh[䦻᮹E,A =,G ᄎ-TN% "(xL kI@:Z8w@ 1o:ezl{h# b3 `OɧE/7; i`;P]8K.%ۖS(h( e `N9J Jɥʂ/\e  쮨"Khh 4 'A R *"t(LnQlh%Ȧ LȢ C8;kOtYR:^UzF-t*%ɠr ! (/9ol6(5AsbJdH*RJ4߰&{ j*=tR3I2[+>~ w€KpS6?fHJm[;{8 CJ$c`0dc #:"{Ki`[&nd,##訒'{Bbihi!(KP$8e(j'דv? a:mx"(K82L<ь<"Go+i:He8)"ۡ8p4>%{/-檺)#ڠ0N,EqB rǎke*I9:$nhgI{:a0 (qB駦kj(膚邫7 jJ/*pE!,˭#m`rr~^q Za" 厉J&'jeK6+ルk%+zm m*fl.-!1A@Qaq 0P`p?`7O\t [oph׹ ۗo A[J]CFqWQroURv- .(2#&Vcx֧oo ]X$Z "-aG8#@jP :\n>IPI35YXƦ* UOWw"T3<4m^C0>RXBB=w ,5 OHt2^Rʅ7~T{C,B1CX7in@)[p.ï􇺥hJUf-a=HqDt¿Q,v@ߘD2q %,1X= 4.dT ejV f`F\Hb01} ϱqm[, (-CX0~?fyTZrKn TS2u̮1T:y]etq]6Yy v4u\V6 KC+_YA+U]*X6dCC,tS{F :{J,HLD:xklJ6˅jߌo@nS uz^jZ;/3fn6V xEVnd7DgAמLeSyK6U9Rq?! Rp?T%2!nGZD\5w6:uk[t;k#׆Ō[]jhq%S/o՘C5YuX'Y]fKkы^MhZ10Ƈ.bx0Zi1&^hHEJYNefe9u*I? gF|A<Zch:b`! aMr~d'!߼m@h3;ҢfWCwNhVվǫIKtiR7KL,Ge:bAro\Y+xj %Jv#ʺpZb`W%J++'2.$jS_Y/˗BhS_=r`X˻j]KT-tSUf,|l}e՝LC1U:h*˗4d1"NPqDeЁن_xqSZGNvSIc$$d:H |*RRo-Y#ȥ'8!;o1.1jX_CujWIrjnpo._ȡWn<Uy|M1E@tthO}@;th Zx!5G_ZY+nuz>&][bK?U5gzDEOYni޲՝uY %Q$#= Qa.\P4}ߥAoRLp`r3G |-+T`ꆿQBE9Fi/f߫;}>_ƫ/"=HZ^\4r;.AM&cD`U'D ێUR {cCv?1]5Dx.\| QZCBQ k HƱZ;DټcEdCyf t(sPAܣ?eBWOhW>`!Hݖ[ӧwpĩxv9 0&/ KrX1H/.cUNV^ZtMP+D<=NW   Խ(3(9ư J&5( 8vP,!1AQaq @0P`p?WEZǼlWUSzW-[:b6VHVs1MNVj 5/>u@Lto4t*Ÿ+azEŽ{ȄVrOj^x.Ӄwщ FjR+ ˭ U踌*%rЖwb˛49fê6p͹Uo>Z]r] sH/0`:{ xU/^QKӪ^=Zҧg͞py)Q(WԭoЭ\H ^pA!UM;ݎ@M Le:5kF<5+F*Qi}/[j  B䈭(h ^p0#usMlvEULU-ցPPUqyvZrƐCYt=h#^v~B[z5ܔm{"[aوиϗFYE6 ?1zi,PjQSm HѼp:z}Zr /'KKmWkT@u}eڶ:ϼ Ze;U_=7ݛNj=!TwϿ /s 0X4 @I֕eC9dN6vP\Δ(s.)J VE P:5L> B;" :^X-~1Mchk'IՙjμyOuF(ĸKB"r(LӔ-3z_Y(ƍPnծrDnXaͷSFUXQIw&P2ݛ)H( QO]GP71Hwݤ_{!j 0"jy"=h!S"^k6D \w՗sjzQZP c|,tme ,t@`jX_XSP4,ʣ3(@vѼT УѸVS}mE] V>(hoxa"遃Ϧ44| ^w 7b y&0-O :#]`U xM66!dtYyJeq랇+6[i8usxC4%}hX Jޫ3Z_KDeJkyTiJ>5Q;z@GA`m^Z k}mS;/iDrίApE Y=1Z)tʕ˗/0-9QL/1.[)|Y |>2`mcС/~V?{`u*U w:.ud|W|4th/c-6hx4jꊍZ0e";-EĹs)[c_^*SKѸH@Xif~H Dy5TCc֦3)׏|Yr4_N'/>M|ڨL-Ǫ&b3GfA#JWW =hC-xȵ.\g;3PoBՁ:IhɼHt%Ɵ%w|Ds+p798\)9&+a=};?ط+ T P5D4P R>}g}FjEAGۙ85S3ԦQ{ĦUvu1CTS}g(]!&__C7+4b޻C1 j:K6X#3¥N/ଠ˴˵.>~a ds3%sRSЈΡK3S) RrAQ .|݂&j!b=R:~ cojPW01 xVy?!]%0HC{XtD-柄L}]/%g=ʌPV?'DAhXG#* D(7@ fx [gBZqF- 4 yc%6u*T^ 1cZJ #irrbZEJ*T$GUޣ`s{yHH/2JqCUԣ>] *|4jߨ% w$]'c:Ⱦ% 0AD ߰Ԥm4$Z߱ 2r/H+xԩR}OR#"#a J".7fo & TKY`gZ,rr,!1AQaq 0@P`p?`Y,}?}C < zCX%+Rdh}8K>݀T -]8zEr26@TCHX,$s7<V,I+Peh:Nyq*a:h͠wa̪kF[ c\GsCᛴU$":c(aAJ6A8+rc)tkߴ=?Gye,\/@AUP]apl箥pi܂aqibijN0 *>d_} UzHeo Xl7 l_}+G1fd`)x!,JsJ-$Y|ʼn!?q-XSѽ0u(ݿt8'2:M{bai> YYdr˴}WAO?Z&nxgqJI)ʍ:^{c/ c%kag"^ IclNHZ`aE@ Ce":E@k; =H$i6Q}jYEVٝR,"4Ơ Zbhz &A\0C+= .9:{7lx g-X(Ȅ;r~%>ϱ*&nHLSp[ξqL!f%5rW^T/ɧYC$%ͥeVdt<+IJR=q%wa`QUYn& enya 6Pub`"!N2>nIr~{&5\GR2 \?a Û/BZU1YbAMȻ YY]Γz/#v^<@Is gH/is^A@_Vi5f)Z*YjbKfܣg Rw$A+򇼫A;Sp?lu㴩+AxhZ}Uئu_y*Cٖ]U֟EկQ{к gQVϾVs(4i}S]pаW,٫vnU.R] o~!'a}ɉßOOLg"v+ rEGEg?-p%0uXp"huUx9t.|W:: \sY􇀕F1c8-YϮfhHU.KCs* 򒐘&KLډn[0C8lk-ľD™Zjz{khV;{)drFBc}3 N|w+RS֫2Et %/=}riG:J5_y]Nx9z JA#*XFmT]U]bÓCg+s rjrYp`y".d\IXק11Ȍ_L lf/܎W%@M7BWQo]$ YO=ab*DFs!~Qxp+A(* k/{PR@ 2!Ha'ZߓO+N(xR-H97L.(߀OZ݃g5N6ReGQEt0]2ZCuo?i:s,_1V]_Ae08 '|kPԑ[L]0Q;Qκec[ JP^E9+Rt҅ gFf2*ٮck sdmOe^:>]>6hjDX"_(=_oX`ʘmLHjQm_ (d||B>hG59_?3Snowp 'fG\`=/9EC9^bu{SY/;Q0;pJ4Luצ8AJVC]YIb7*# l\A)C7#0RKrG]cD+%p9RS&FE-9 Et214E"eH/iCGDPD*~Q/*Q+IN Mxie` DG @JL%i)%J8GG.88QA2OG|:cszx&=L<.].9K@B@sruĪ㟂fFQ{̀/!ٴ%RHŹtiǓM9Q7uf!}Q@MYxwx/0T->N['/2[ݔ4V|D`Z_l,7םYn>C/^ba+ Ů Sjl>*<Š|ךQT;3_}D>]z?ĵy폫g6}j=@UavUk, W$sAXj\1oU۫*aMG0 3KE94fQO:GҥlEEa5ϱ\_"Cec#Z,Uiig>(Tn>=DU7f|V?Ԅ*8W}\\Qmw&\͏IJoRJq w~L:14Bѳ6i9(СXpYS`ٕU9b=/Ԭeq< LPoËb> ^V_aKE$Gcş[FuuAn3In,UYvEWI^%{{}B7 (Cguj4}HVݶe@hx'@-"G fC(_敋n BU?#8ge9y*6їm1J9@JtMJryJh)5c'r5@2o4n]ҡe|QG^H.N/DӖ{,%S_{jcC rQWry=2OLJDJwBHtM@ &*Nwwܜ]idNj7fU8\rz%>s@%M(I R\m <H2~J]ුRah{̽s*s('*{o6}cdY,|Ϣd=c%Ĩ%&Ohܜx^_(안GTvܴFT>޽R/w`@}T y'y/w /6{SO毬1ae)PCz̏s.IRͰZ#hi!J݁[Ы|Z߬f C\s4Wu5 e\XS^#I,VWX\iWj."F2_q@zBe ]>şbϱgGaxH_PUU7dl5D\tN~(͗,>2M hL&󯘽&Rdjq }0) KXuu)봁ktrK1t{EjKcQ~::*vC_l?HDW`6/!eG/5{Y*mAݎe^ ( PtLm:q*XVWDl- mfd7Eb$nyhud]Xn5Vi9ݹ. t[Q5T<h,PT ң{c9㔦زō=*Xw^GwuAN6ۻ]+_`eIAT)NkbQ`~eC~ʛ3RcM2)vZ *letGV_Co@/|L9Fcv% >tT0&鴨;5 VeTtcBE`;L\5U W9;%HӔ( uQ"Gq#N5teltb51:H;r<כ *7 D`]G㋍3z%E"Kv.ʩl^+!cҟZІi̾tYe6M\}؈EO>+C0<H"Y!ۏr{xx.p CAp4pmUI:g*ZF rb(l#r(:h r&R뚬_mtwaPCHayx wPY1K~%zÌ.E݄Q wftӉO)̹t+r(LNMci[+BSDY~CWt.^l%AcmC|4 @p8iӁYw?hі_Yr02a U:u^jrX2}3)}:GVc{hǻPPkjYTr,J:"|;l+V !l|ONG:Icw`Y|=RWAcϵBmz'X~ øk&M,݅OF:JoM A_vY]P!EJm]q v'h*% @p?[Ŵ|~ fr~'%K7M)=45h FmBxRX?Xk8zŜjLg_:jD˱ 㺟QxXXSoۧWoQjH3ӂJ0K.'ɳ4lFxir/@P.@p8S.7m4m}Zhщs$ d>p}p{DV-(d=JR\̿S@0"AV,sUa7rOYeղfy*0yp!p1XǕFbD"N+qrT"%~㶇^~Ib+)2ۛȲxtſW*D&ITb 24ܺ %s/_Y\ c1Y_54$kwp>Dyo2 x#+jV11]P4_XMX2'E:LYt@Q@r%+ N哟o0u!-z* eJ@3X_VXu *%*/$(\2 Je*[e`C@m D{ݯHA0fgk\ jK|Q T. !TQEqcnM 1.õUQ-^SKM`?RBt2zU} isGuɣ<@ F/@uaE RhLx{q~kjJ *58(s.-t? * +炊LÀ#oF:xYI u؟ 9er]+n4]Um &EL "2;bTyf1A>/qSr\"s70]u =beXјXY rz>+"z'W\(0Xs0m @0?4OFW.p \!C(8u.Y/I1*7g AF=&T'U\VSSzŗ@Wx;;323J5 B1E;ߪQʕ\:f 9c\-˥#M:i"66%XU]_&U_peLj*t*<ؼ._X_g@jT@\>"8:ŎBNV^ uYA/5G\@嶤;ߘԊ<ާ+"՝V]>`E`w+3Q&IV[d7"ȑV:HNow\M8qa0 ѯqAJλn`   6zQohȷnyc3XZiU5e}>xUh#ϒ,U㤵) ]/̡kp'DKf HFi)Ha2 !262ԳPsJU,(E/jἦ $sy`^p8E9wO74p+@ owm4p:c?K>JeQ|?^m,Ⱦj.B wJ-+UM]K QbXa+zp#hxpQt |2 #i)vGEz .k9!!iCs!͠mmg  bLBY.ZK:ٖ*$^)uA.qW,h*Y ڗ9V ĠG*8r2Orp @P*#  IjuIf4EPUvSߣ2N`?iKǝtsT/hv{'B+8 MUw 2#=njpK{@2z%B: {DzSbG!x}:g5H~b{i#화nzRQyzuF-:+кݢ# 1E#dhhkg$(:$z% wC!̱E~6E ^hpȂ|3%.̮RwbR|3  @p}`T34A p:SdE V2Mw c]$v\ӯ9 ?]MZSh'Eq98#^ ̹[/4yԭ\kn?TS(Q X88(n#`c*,h|)osOM|BlK;!,}gg>WzwH9mi=JֿD^"'$Sg %늦w:;CO@#&{oz1Ԋ;`(@CP \j   d h0?)~a[1Ŕ3 ,ose\k&][/ Sk'cPMfjP+" Dv)Z۫/ei7OVCf&[O3nKY-QtYYv8qJNI0[tdjDK\#̪mT5g_io a&bgcB̰+rFj=;X;.u}!TCw6pQ(J%b|Y|h@#M{KtLyA(S4J @* @}{!NPRbexGLsv+}4k߯o08@[ѯrE*nՈlC#p_J1*RV$AQ0a9C~ PQ^Ӕ(MBnT|D2O1^Cl,u)r~Rakx=B݀<,U4X>lfCz,Gr6XhFa"͟Zr⒀hF{8H=`!,s={Ӥ s0޻/=0)Ë%gCB 16k/ 3@ 'Eˣ^h]|1#A c4M8NY~| =w7( q#)&g0EcN?+J  {U/ EbP `nE(nY{x D!-xg![)EOهH,k0Ta:xj Ą 6>bW¸ęq>EP"Wޡ!zP۶.KWOtnx%. ު #SsIbPjye_ (fa鯠-i7lߎSF;mtD{|h5eՐ3$.|Gױ_ (P0LyF !/հCۈsrc^BLj o<"s(>0R/DeqL>k)7p:lGjRV6E|֌(B(|柌FvC9*5̵/E-}KQ-߉s J)߯Y,M, r" %7GXQ)뺆=ോ%YH uLw.ki,=R>>%q|v*3tzVEn>eW8;0 >&2 \w())ަ5_7\/i+[̚= #Slcfʀн &M.!s+g~]  l0 U T1aynLdn|~>mC IӜ 啿(drVgIzU=;0`@q%PC->`EGc Q_Qh_fWmk#Qh(uX=5.NbbeLߒ+_CR siH߷F 9(ұY[uk(BtsP(yhƘN|ǷdlQ/nwm9$-5nǽk OM_X-VOϐ_IJaR?jX$0v=Ɇn)>"rN>I}AZ_V0m};?pR±*T%n*Hhr">RʹZ6Y07Ɉۃ^r}~l*oܺ<#n (O mt'.[(߮zBnlj6@: d-?QϹkAwuMzOGH鐂 -/ *y@xi+ &;GeTpBҔKl w NQA+f>O[ܶr1͆:V+<=S(1N[\?ߵVOѲhŲ[1?̵3_07sQeбֈ#me~HסE+_8J`"p)oiSvGߴJliԩGœWs!'nsfA+blP>XmM'o\L Kwj*8C# فkG=+y5f@ĈXyBѭ.^% &ਓ-#IQ_b d+02VO`}z%~@Y̧> P+V\jAAy?Cco@/XkS[߸n u~⑅=JBǦP ݇]2,Tṵe7|bs:ɏMQ2- ۏ!};Ƒ^W|Ym{`}]1To:.xTMyY_@ҥU6$OTr҆|MR,'#`v;NsNRnTW-MMe ;?rɉ-DJuϣv8M=״K9+ϙ#UcJ\`$*A:3<'Q 0^( ÂbYW*l5\{JyQ@TRYbe8Vϩn p`P OGO]}໲|+6f\f]YCK._j 0[W-ɱۮ#Sp_au5=GiTk&[@RE^J|O" K̻6-21JIuJ){},ur^OIɛ,4$z(kS1 9HfDk.BwOIb嫢;52#x qa??vUyc1̸K=slH3@W}=ЪvkXCjtb&90$oy[6 4{3=nY[W#4ast:2@01Srk*N_pwKXfh~dT{~?t"9Y>4I]}$#|NQCOK\u+-U|b>%+K9LjZqj:;LL703+:8,Źdb"(Rѵ~ٸI -%1NvT+C}tf&T ~)aI^UgThgjy1@ KD^j6JzgȆ#QTT9~ 1w3Ϸ(OPc?$Xg #Ps XTu{ŃC 4/Ai]Gu9qbys̡Rcz]YLViW (qQG,ټZ;͓ e/|4pA=:9zmSXPx}Zj[v$ܼ z:C1t<.9B?<'okdn-ʮwd!PX Q<唢ȓQ۵{x" ޚ~QH7U h yL >*zFz[[sP0Tf+7#%K,* “  yvb([@ucfN냩|SPz0n9n,n'v jpۧ%?P/!/Jf*Z\m#p~ *̽Tū" ~9KjXq:B\.%-y,$09^%{;Ke&= @ a ª/w&a6{TqL21LfG c)L_v .[D-{Ȋ6.]T=O u0MZ%fXZg6V#c65|bT ak"]!Y[~5٤ "}PE!@qy(eI E>idA`1Bb.^fW@L߄tmph@ p.g/xB8(ŵ͹[{ K%,I9B*(`Ws!g6K1/s)4q}2}>&Yݬf0f?&n)b= ;ebrk Lzښ? jvtX#SU~4{D2` QQFA1O435]7YH $ ׈_* C : 1\-VĞQj1vuc /]X)+~gxj4]f{\}LSxJ=ݎӀ͒KG* (E*^>)6}T-WJ`W8SF$/ z,pinbgN"I]tzLQF"31j3VcTH<.mSțufr8 \p@A 0A 5./"X/>`/&_xE= ϒ.0滹2)z%yf_Sj幗s,缟d,,<0a(Vs}p#ǀN s!d&Wgtj2$GӒY .fZ hȴ( U");JQ@A0A4C 0} ܍9JxɎ(K:eT!B5:d2[\oX3L0\L.o7fm̱w˟ 8Vh&*W ʬ+@*2  h!5SP`Mcs6"j}#yL Q Sϕ9cO2O\HA0A 05Jp#n(jmp@k%\.]|K?S>+2Rw^ښ#.lfHg)|6My3 '\ wƟ _@B y؄`狏.'$sQb~/(@ 0 0A 0CuMSW QEZnbQǼ"Ȧ +JY;;3e˪YSG q)&zoK^mwq`a7M 2T@ I$>1l-~9ef dEŨn ]T;Np* & b!Pa dy}pa Es|7pxT  !qA(IǴW@Q^5qL. b}x(l[/fZe.mA0͸pjpfVan,ukh_` J\">WJ,Q4ѯMWsMv%DK黅3>ac 1g--^7x|p@ >T(W$I8]WC)aWsc!8 =)olϘVz5i~fuFYaꞮC <Xf:Mg/,<Ÿkj[)u,'+(b m:q]c Q8z)NwzfWߙo/xKP9?,;<;`W@}vöIC,~QaYa @ >98&Br\ŸkoSH1aYeo~_Hxy?3;yY \*;aYeY+ 0 Cp8mqEX8-M-QbзLLr0eYeXeM-nJR&|.# A;!xag6ExVa.S˜iqQSGVd[oRaSta^dž,#xY8X8B{w9 @8UNpNI{c={<HV84EqE|'-&eHg?r)̔WCHA}^x Xc`f:!*+K<=͠T5APAdG?g;;<f0v>^}Da*$p]+KfZyqi Mq2AM>ڠId?GB9X',tK?37Dm"y &,B ?"(FgU0~_1edEJUv{0vgb1s(00 &fCfx `@FYp]osYd9"Fs!(/ICSi\..騢ki0e~&8i'';xF:ʂaORw (`u /7n)5A 0UaPN}*3ߥ@ ^i8]a0KJwY8;OS,Z 0ckҵ>3dJ[V!t.)b-Ua,4Dlo!rX/m3zt[xr&oaa4N4  SM5'c\/qUtJGQJo-ȁW#:US7@ $MR~[Yr-F̵jlFb9Ry0h8iH'rntׄMwͦiCϤ\ L `L%SFcVc:+/pA(_M/cvc{'gOӇQ(Ob 2TJXUdRS-Pb5jKWwXN)`u#"pia!̼JlG>ҩ邌k9Ǎ)١bA=:&{& `{-0CfvGd ƅݲRBNƩ #-P(.xҶPWND|6 ,]US sʄ SbS ޘiˋԭ;- #U -/\tzp]>r Wj<[a3@sCVybKpAhxp{;\`8olbjv@*T=7UfVrVqxsuU1Aܧ+^/qj.GL <``UsSᮍսE출2AdU; .͡<`P K,$8CR)UTVRk(@p( T"4T[/WJE>E0T[%וC5R2ڀy n= 3BpPҦё[$`0 #0 ,Q꯯61 !a_jx濨 gs T<kwt. C'66c s#Wk5QVX  snAC @ endstream endobj 4 0 obj <> stream JFIFC     C   Sj" UTYll,llʖellʜ[-e-e[-eelryYl6[-el[*[-ʋ-U-[-)( a-R/x::GxΎSxޓ:*/z'w:/|Ά/}΄۠ߎ/:K"yz yK9+I/gWdyKxvhyGs/s^qKלG^qK!ǤoyzAǤoyzAǤoyzAǤo v':4ͭgO6I<=$hͣO6I<=$hl::,$TRȒBY,,jJ2lY,N\edY,^3,Y,q%,K%9qK%ɮ2dgRY,Y,K%Œd$Y,TY,K,̀d_ߔ'Riޘ1qa]?|ߵ? qy_76_c~W:˱|.{tJ*YXK%%%d~? 23_0>f& &grDD !,@//K!,K,a,,ԄHMqH5KDԂK!RKB H*_>k)`23@5Xҹ+ҹ+ҹ+ҹ+ҹ+ҹ+ҹ+ҹ+ҹ+ҹ+ҹ+ҹ+ҹ+ҹ+ҹ+ҹ+ҹ+ҹ+ҹ+ҹ+ҹ+ҹ+ҹ+ҹ+ҹ+ҹ+ҹ+ҹ+ڹͫڹͫڱͫڱͫڱ+++++ΪΪΪΪ***ϪϪϪS4 fL)3@hT S4*fLЩ%#DhR4JFHѩ5#FhԍQ4j&DѨEHiM"Q4&DҨUJiT *P4@ҨUJiT?8888Ì8Ì88999#9C9C9c9999Ü99::#:C:c::::ì:;;;#;C;C;c;;;;ü;ü;;<<<#<#<#<#<#<#<#<#<#<#<#<#<#<#<#<#WjRH0 x`<0 x`<0 yrͬnn1)}+bC(J_#QXW8|1nag@QfH/l;eCe{' mwI(]7q8xITo/1_[rW8 3/s v|hJa[nB]h? Gg< ˃sВhk$o4ˈqJrڌhE]`vVgAkѴ{ a,ßի.!@nY F왩[ )#`~ |(%QJN*3T#0ͤC21C3O;-;-MqzOr[%q,n/Pի.!@v)d+8{oQ#sPnHԑ0Ga.'DQ#I|?~1b|Zqi4(Eă46}D۶Q F7f% \BփLvBSv1KXiAljӀqPQ&P#/?2 tVȾ+H4{B\lM]'y۬M3ڶZka3QQ$a/vg(7i+kFJkIy2o}C/a`-5=Lt !>~=`~(9YЉTFAmCUuTBLcI?>W0hB˙(B7b/ޣF4ŊN\rOU $e)* &&j3RͺV/dCd[B {+,?QD2RHi Dcfߐ8=D5[eaCGPPE׸]P]$ڷs?`mB4llmy?J | MT(J" %6LHׂC\BD vز}xaJEa읜!r!r!r!r!r,!1AQa 0@qP`ѱ?!qю`! C!psGpAXbŋ,Xbŋ,Xbŋ,Xbŋ,Xbŋ,Xbŋ,X0 …RJ.L"ŪTB5˗ 5fϛ~ըPBzɕEt\4aXTVEQ_WEgQXWEqQ\TVElQ[VEtQ]WeEbQXW%EzQ^WEjQ_VEfQYl 129wFDeuWTdFuWuUwwwwwwwwwwwwwtdX1#۶(&b +^{^{^{^{^{D=AOYn7!j?p)]NG<x (Пc4 !$#|&0B#K,6#qױhK2CJʊ@qؔ O{|>zSiAa 4 !WAL'pb * H_Ad+H#tUS ˕^݌Cc$ L D232E] ,AH>L>ܯ\'I1" 2hme;qɾv#ǎ <&gf~UGec E#78ܰ;"D"\#4CE\}yǡ<\`~QβT#"&uT{G9BHy'*FB0CňI`X*V U`X*V U`X+e uHC W{ UkQ~$A @0%O[F<::y!m c |a0|:0c힨<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<"dNKnxoCd}G@5"0 0 0 2 0 0 *JOw: >CZr%4$җ o#SWOCҖz; opC a8@8A7 /ރ'|+a !1Q@Aq0P?V:uut}]]]]XV1Ռuc뾂 / / / / / / / ^Ay AqPC.ce2e2s ?fs ?v ?~ _/C.Nk?~ x8 p6 k~`hXL R԰`lKhKnK5ZX6jX4ZX7zX7JX4JX6jX6jX6jX6jX6jX6jX6jX6jX6qeip?,,,, *UBBB"ߎWq"r˷n>뺳Bm\ypv\zEiv6T <~~QJIzTTBP..T*!QQQ|2/ |揖a#1?'3ɜMҨ*t!U UTTTBNG)aQ !1@Aq0?J*PBWB]*+A PQP] *EDQEDQEDQEDQEDQEDQE5kW %+)..%K).R\\ \W(Q] r5иBE=иE=и7E Ct. Ct. Ct. Ct. Ct. KtQE=Bmy?o#{7#C6<#S~F?qM MGɵy6Ky5{yoo#M-MMMMmMmMmMmMmMmMmY5M$976w_ɩɩɩɩ>8888?YR;9|P.s.KF{}Sc6Y}/yF5'矩v۹%#o5*Kï,|888&g~SbWj8sLHH$^ $u8882dɓ&L1(!1AQa q@P?G82D/LX0;2D`F#w`;0c僯c ;5X#D`> """"pzC?L;w#DD{`DD`hD`#"0#A;8Ќ0x&"#hc$FDGx"""#"0""8"!`wX<|Ou""0uDFh#`1uFQDF F @ @ @ @ @ @ |B >|^0pܻ1&"Xe:"EX b| @00H+p;C|B °RXJQBa|[ FXroEȹ̧_4&,-%Hh뀎 @HuL.t3:_ﻬg q.:WX l H:ѹD\g-G\X:;1nuꫯ+=Es4t߲Z׃OS) ~?OS) ~?OL9p,ˇpϾz3=L3?-LL3S3<=ϙ]&fa~8p _<-%=+AW.8<6rEE'Ck oG t5ˇÇÖg8;?+чm^swt='C QxB=BSG: o{U%-v-@<wz0h_->cXO}4.^9`!^c֒uBtv)^6:MA$CGߦ]G@hnb4: }W)wv~5΍:rO3*cx9@{[NHv4^2L&I'?/ƭpSg߬^y}NwƟ9ЦFڡA8[}R{@![^O(. oH"ʈO; y4ŷ:C MO"k\{loP/q9{+]t;ZڬjmqRG ][Q$- dy9YD[4ܰgNjwvwFNB65Ðu#qzGƎ2QODrɧ|/RgiBkiͦг[( j3 &o|>o&<3833333> stream xSKk0W3ɲ.C! ɥ3#{wٶ {Ѽ}3WQAO{z]ow q{I> hqOt5 ]>ՋߊP~\cZ=i7ѭ'5!_I݀+Lɮ4ED p]ff-:1%!s1~/؁7fR_: N-pXJ3ٰ f+l̇  0Iq&dؼτ.s.U b]ƤyR3DkۤwPZOD6<69FE8@^i_NhN!UOw:#?(S endstream endobj 8 0 obj 396 endobj 10 0 obj <> stream xTn0 +t.W$% -uElX/d^ݠY#$̟귱fgyPFd|n>ߘyTX]*0~'> stream xUK0W輐4,!7ڦlZ˖lDVFyvꏱfcqh}f>=_3~|mQ\?T7;zދQеE/99~alNuz#'ͬ;=wCguvol:~N5!*(Cn?OB> stream xTM0Wf$Y =,=MKٴt/ɲclcY'jiGgy|>=_(~^Wdwx|K.݋Q(tNF|9WV#.ᇩ6jG[5: qY([vG;#v>:Nmw4wdicQ9zӼ] rlPCQ}<}V5Ti\Aov.sS4O U6tbqjC%Y-r@rhI4Ua`ȸ MækmKTT%ŭvi{().c\ ݖe$鑔T;wɌ(-f@o~.ĬH䰖Kݤ- o}Rй Big% }'#Xyq|]CtkeGQ,o|S-e:[!,7H$M;zfp˜,Kf!M.A@^)j endstream endobj 17 0 obj 612 endobj 19 0 obj <> stream xTK0Wf$Cl =ڦ˲i^;IZLd1|ꗱmy}f>=~޾Wwx}ћ,vּW\5/śOsTp<ht- K4UGOImr>|0`ozj|oPSj J6VVw}Caq+P;^!wshk4#Cf7Y9Lrcdo >'c`D (#"Nө[ A/ pZ N܄z 6JM\*#w("f 3+Kq Iš"lG -9,t(Y؄{*" Q]o vvځd/ZkD!hRpwCSL4@JPMDnUl=> stream xVKk1 ϯ9K- vS۴lKs߯$[Iz2%}z{p3(m$iOgWN]Hc5gѻw?3.EIݑowev wn'ᮻ:t7|'uJL~`?&fޡp ~ɏcYI0fm N,0 +919)/e.L'TtlK<'+r0'ENzA*RSQJT!sO pt_#GNQ@"& $8a%ݺ=v!EH *RxQsMk2@xȫO2q=J `AO *RxQ ^f[?Wj#%R}@]HFP5Kk6:-E9/*y1RSYK<ГkOTKzMj{* D 9$K ygUW.-5-.ZJ;/(U/8aݑa"EQZKq@EXpQ,(ejᯈ޸rYMt$ӦZ'JJ&5hx**5xUTA/-Qͭ`/= ej)WÕ%NTԂt%7E8hSTr'othKX{_X(O Q=migfF ߂7,t*bla| B!RrzII^ A h:e} M0):hJUn^<c\ 4/?/ĭuCGC߸C endstream endobj 23 0 obj 852 endobj 25 0 obj <> stream xTMk@s|,-U JOiҿ,Eehvvy#CO!n@̊t-0#$t}.Wfӝvx)ţ_w},Rb؟ }8P}TUE=ƓX/ BlC M@HIB2گH,@¦\O!+`E)y0+0&/<k{έTv 58[RbЕ`:s/E^ W(?1+ {;َ*aCidV[ǭ9{'[\K;562r|h.; x'_tM]U9) unI*hAi435o^oHBv\.X*ӿHS#JybPhi#y_ B8* endstream endobj 26 0 obj 477 endobj 28 0 obj <> stream xVK0WF#ج7JOmR6-K~%dG684/}͌y _qvnijo٧?o?*p'w}u*dQ.4n|V Oce[d7xrkK:@'+c6 |[tϟA\[. $T^ aA# e"P҅ [C!b3dtmEb,:2W9&{PI| ˰"Hi{̷D/6ŤFib>C@ѺVe2u~O)'s|8g~vm-u/V&&i<-0]udQڜ0hFEiI!7r(E9jBe2Ȳ%2`NARާ{LB2ڱ n -j}n>nєhA "LHEƚB&[+xKCb$:Gqç)?m" VdGnb m$m8jD?Kuj07v wQ Zl_^BGrBMSb6_sJMI!FD%2PbāK'v/Ȭ^{X+irjAN9X|'#Z$~(^ YkXt򢸚rH5r* jtH3h6 2t[ӽ*^_Vn4+;;$r e/+ q,?3 endstream endobj 29 0 obj 771 endobj 31 0 obj <> stream xTMo0 W\H0M tvv Ea, E=>R6-7[~Qy+ryl$]5Hǽ~[|<T`XM_aÓԶg !<} f :`G6cg\n3bVՃX7c\6[_Oѱ|(ćP*S6 T!kvq@ɪՍ72LWVL6eX7ydă$i,Poڱ3lE _ "(pc^M3•ё#xIsLք|%rV\% @4MJ'C&M)m5N: 6B$4;,h 4^֔[)ITm BQB} 6\R\(njX8\#uf!(æHCHS~6$DNx#-c%f|KIB(Fqbi& ~.b endstream endobj 32 0 obj 512 endobj 34 0 obj <> stream xTM0W4#ú =,=MKٴt/VqB1Q4y[1,m5x^_OW +[;WЏ\݋Q(_ㇳ7HsaJF3}5#Nuz?UGbn=9߻΅~G+j#St=t,w>^Kaks6FXDlڅ8: Tmps ?wv#&…a4mW>r(u88):P밂 11dU)%dZz3\uJd(j +*dڅ@FrN7zvO߮oV cB8Z@q>BQ+0y(o9h̓ąvrjsiIdf-LHʨfX\ SeًT4^#RTV:j6#rIl79F/$t#;E|̓yZnd:%F8_|N.j@df2X-\ΒW ѰT*"SeCjԨP?B endstream endobj 35 0 obj 629 endobj 37 0 obj <> stream xTn0+x`$-Dq7zzjAܢҢNaKpcX6c<0_n̯ ^Vnk)TB_J_RW^Su5Ivn?6Z>ߏUCekgv7㱵ЍX(t4.#͎ Z:homͱEP4:~~wTT!waYـ4[v nysEC?MK1qO31erTE%V+А'e>e({?%JFt$T{ ZO5'agӉ\Z 5V= JhtMi\*Y*wVdQ-hH8F$B*u@ő~E+ âcZbY),s`*އXsS<f390=3 zppd@}~[\{/$sg/Mp"Q*js*E>@ٸtې t2@we&1oΥmh֊/zuQ^-eZ[`TՕ endstream endobj 38 0 obj 610 endobj 40 0 obj <> stream xTM0Wf$C@{ zXzji^;-uvGF=--kv5;x~|3̯ ?/ WY~?mFqGquZr-?\96'ZEMiKg_ޜ/x<'lxuAkC-@<}[vZ/an8[iާ;t;7ApȐq H|S: ccDytDiXD@=m-"i?e^KM 4L+KrF*dw&T jQ @I` uF.M52:'΍~0}#2V +e|У8~.)LJD&5@AjOۊAnXJ\1S'9ALl}S )'m')PkwQZ p%HX"zG? 26 ~bC-BʰlJRYr0auU曡K'+l򚷣ebC %=bp3DA9\7JˆV]&MP5hhS3` )"TLk:d=Aߦ@lBަS;%dyU[ڬ{  endstream endobj 41 0 obj 686 endobj 43 0 obj <> stream xVn0 +t.W% u-eCtX/Ql1CE(,[R$NkFkC|oJݩ+~ _{e2 SqdD[p{r Ea.A F V*?O67u]6ʛVYNK 8a&{~[nž6aRp(YVK/.)4UUW)/v ܎ҕ tCA$FmnbpcTY/(7W(RMy"w*t:dqf?RgF2GtdnL{.e#18c XaXŚҢr\d+rޱc*!@ )ad%E}`Sr2uUmW]T#WuJPGcY8ޱ06SWMy`D.i 1sJ~ L"ȲԸӢˬ,'m"'..WS=)% Kvln43V/,v)..be,+;kϨ\v7kםYرT:-.ܘ_+M"|[xxDzp7fBjNÒS"y;N)l2$i%|MJY1ǟi;Bou n1EzIxlc endstream endobj 44 0 obj 722 endobj 46 0 obj <> stream xTM0 W0YI'1@3i zXzj;-e{߯dى%ğ{l0;aCz'7{`ܹqG{4?e\8;36G^sհ[ښx|g~|+5Mf<6uv`y)QGSǰ 2AyxɣҶZt6(xD[ LY qc#KF#^Sً0F5VWc~R_U^' =vHGPACHt/${= YpTm\ڨ1MrtH&k}E7kd~#J($'ZBiGlUFd:Y::#_K5Fs|U+y&r}˙T\d!OJ6j*f5OJe!Q&r Yk4VRWr i&bC\\Rm=L(c.5\Cg/\A7j.)NMjiULfkiT|SVoJ 0ڎF( endstream endobj 47 0 obj 585 endobj 49 0 obj <> stream xUQ0 ~ϯAsd'1@sm`{+pne$N[ҧO[S6,Ok;߾OgFoU] _MW!Cl-QC׹εh珷91Mx"͜O4xn1XRX[@%4H#BLG~H]rS~|>Wq EӮ%$\SPu*NbDA SMSsBG0_PJ/ T 1.QTx%HfFm|?qR)q:YJ#5J1Qv8$Fy\f*&#VQ;&*j*n N@.]F{=Vm{í'v=xn{@+]ccS.nI_B۵BH"涇FZBqogk3y+.j]n0E;xCS^}@ީwjшΌI *->FrKQL*^c endstream endobj 50 0 obj 671 endobj 52 0 obj <> stream xTˊ@+ m?F %ǐ 8!~{FXWO-)~;p%4k|>޹:}܀] 4fc{tߋ˝@픤kq;?LE"U/$sKO?wSq6>If۞:l;:Xd-S _RG$wKg{AKx LI .aC* EHL|/Tj9@A3Gpӂ8! 9 HE@~]'sR*P $cHd: y&IjMϘK"Ep ׻A}vs[ ~5"WuRʋRTHR:=9 nP)lO7;23Rb~܈vX-׶x^fEv([~ԵJZ8~l3f[J 4犸 \Q{ܧha xumarS,$zڄѴ392fVG{v=s endstream endobj 53 0 obj 529 endobj 55 0 obj <> stream xRN0 +rFj8IJUu[%MqB]}l']@%{υOA@i@q<>+l;({T2p{vW:'?>8~[>hQ/zʝv-8͠B B(ZĈ(Fb . |,l M,L.|'4#*):>6R @qx:KDm #M, /bm'uE1`(sV}>\ [ ҿdDM}zN&Wt욣;1u6*C6S`0۔͙<4qZ%Uf8c_ƖY'6fcC1 I>7d2 [@j,vU]Eszze ]^/duiߝLy3QaVpLNGBXNMVYKFԹx1¤q*$V endstream endobj 56 0 obj 451 endobj 58 0 obj <> stream xXM9W9`Gn0 vo==%].K~T%uugD#˥ӫW%)R<߾tAGo\4h+C3ST;tlDOO=_?WepY]>wh`2P>C_-?фfp0=HB#Snky b]0Mģ뇃]L]Eh`bn?hNwcAv"!Ǯáh @`sW朿ĕi9v1^)g =7Dtƫ7GF Cr,=/bI sq' f <[Mb <a?FL$7Q ‘ 45Py΅P#i7PRL(3mό ,LHR$<@Q'!(3B5y: ~dD^ZTf3d'P(iDqҌa 5 ˠE9 S7"wA[ 30a`VB} ->hVBsk u9TvO˯֝0@G!2 DZZJYV'y!VTeY f8bE(2/;ejѢ M V(l:")` 7IJܒ^!CXˍ)>CZÄIs*B󰶦Gkռg<TԿ(hrxj髵rSVRü[:9CRg0U|o (%y ,3NؚUJ{iUO~}bTSSQ5EQPb+1zR:u5DTZer3?asj\?V z܈uXftN-+s8x%.4,MZ6%+ۭ;͔UJCr,Y|5EBRm%]^&\=U^;/K5˦ \!?xT9 _JU)ɽ*s `q`}R=ðb .C|A!xzJ ~E#?җ[&z9Wdeoݭt> stream xWM6WgH`Xk -`ۢȶh. )d5@-i~Ǜ7Zsߍiӻkӻ4m {mMGQ}_wȴL˦{5ZO] ~i_ipc|۽p3nw!ihw?ќ=M;aoy&qLOm {1b<oAGd . >og^ j'ChybN}q|:/\R7v5]_a!Qt nSSO T@-XpƱ‡ g7CðOh<},Ŕ]m6[PXE d^&>qI]c?r`C$iF=z*rܘ/ s)-ǔ-z{O Duk:"(etN ÔT5Zi)Nt )v $)-%s6էTödő{(GqX`Vu,+ 79Kb͆ p*jRץ&DCu^1cg͘ - U^PO1@1k5z&TzB+ G}H#"?HcבBN+^ecP|gpd/d/Ri@6C!VKS/:2,8v!Xf\N#SsuW؝+PWX"$6dL\fh.Bdܨqu]RȌyɨBC:jo8DWBWNWnU豪ףƂ*%O*YAݥ کټᜅ@{TT>(xwjiRѦWŠxfږufۄQ[ڏzo/Kk)sRBsk a+RSXۻ{LݏFN ܔcaZaR]3D_%oW-B_*V}'vj+)rN6􇾮Fx 0w|Fec3ZTťZf̴w jzs}10 w%DC!K,IC7K Q endstream endobj 62 0 obj 1168 endobj 64 0 obj <> stream xVn0 +t.W$ qۭ@enX/d+nbTQEz㣢[P_JGp~41&'#7-Ōߓ\(t\Nܿ{jȼ΍c[ΟV/9ʟzivyqq=;t7͸3L 'HSǃA;;".hh9b-NpOB#VctK{"<}N7c FVD^4b5̾ʃlx$t_#sFOr>Bh9"ߏv%XρA^Y`qz #2L.K#IAq ҡbLh6geC GhPY1MzNz\4B EkV) ߠu$$X{IԣEճ)B-$S* RgI 41v|Cis|+$!_rp-m`ik H$|skp\*ݿ񍶵T"sB<g,7_J`P LtgJ P-5B<0.WaD R%80saUƷ#9~dUL/kϻ~ aI*65\|e|B#"H1ЅBc<l}ܐ3[85OoR>^ ,Q?ş*Xp$ endstream endobj 65 0 obj 749 endobj 67 0 obj <> stream xWK8W*l0vwln {Xd.%J%w[KF󫯾*T{NQ#psZNѺGe==_wttS'Ӟmiz=~4u{Ծi/9_{?yz_fPLjI' -Z` PBaf6e&r"f_>Y`4M&d8앋Fr}J gS=^/#WLgl:Bdy=G;3jqo}aɋ_"jV7ѢJ\*.zHbI9'.Jib݆ ^ Q .DkJ et4Ҭm8D G(}c Jh؄Fud\0.m+І[©_C6g(dĀ(yP~ Z9oQ*V"cb@^Vjqz] *Xp5.D %JYRKT_լB-^SQjtJ5)pH<Hտ)эuS*)%.P<`@Cߧh .`g/z:#*޻D:CNBwUAZ>R~_JkNvP>.NMPlM_ Hfٯ/ m ]Lʨ(ьh--RR<*S0ccx[bJ0|QݔT$A溌0\*NfX9.sYWTe]SkPAѯ)C͑<-=8\ysGPx0xZ:0].Ί` `wzK|Mƫ\b 4^ou&4Eꃍl?H7 !L]?W.t- endstream endobj 68 0 obj 1054 endobj 70 0 obj <> stream xVMk0WXGI ư' =nKIZK~5&eYJJ?**zOg[U2~^VaKpD_\(.74-Ӧyz)n?r]>'s[ )_ۇnS?,#?ij.;S->uNY4:8UcOa5)^f 68W'uÝda:bL\ b 4)͞r֎ZNcFH-`''.ze%W^h&X.Y7ѥ8W9.xLzrB#!"Z@5n-hl2 mվa`q_ L!SV\s}dRJWj fX܆}mYVJ- !;S$ex>u9c=A3$2鋖|&F3]!dRaS)E]!8{/QlS q1rL?)7$.$L=Tzr  CF?Z5Čj')pV~]aHօŋ82:RFț63B1rQ β$Qj4>)$v5ݪ|VQ iM `P]L405y‰fQ r xabeȒ4Gݯ4vf)0ݨ~??M ޅVI2mH(U&fWټaVk|=]8yNB %ewd>0M1`gi$i# {U>SndF2^ K#oť$ TrX endstream endobj 71 0 obj 857 endobj 73 0 obj <> stream xXM6 W@")0$1àmbgIɲ%EǖE||4mm\u|a?l~|@??Px&^/wW}k޼}b:/9Ӽxt,}s~_7͋?tu>G{{㈴\oitO-Ӏ'px4J dg0³Hw&h^~ܮ.Xu9Fa**:i/ښ@Jt`/[Þ,' )61,|)L0Ḡhc9cR,ϑU#ڥ\+j ZJ<1 :7jaJ 9j hX8%XTtܨ;D{)fKZ Ɏ&$Q ט,1)ojqp0IiЂ MEu1A/ً*z&UAf<)Z hgAɅIySB"ojʢbAHh`(>jſ@iSGٿ`'NPԵ0asd9 8'#}ށc?;7yqwwz),ݢ a Y@ =g( +Pgu$[{({ќb4ÒFQyv2uyFT;314w-='_P/nEŰ(-]yJT|O).HqpxXڶ0y-9iE0K@YV?JH\B3AO6 $ endstream endobj 74 0 obj 1246 endobj 76 0 obj <> stream xVj0+\GO?]Bi)IKw4l K!43gΈ5[Y}`0]~d3-df;GtGegd_^/>s8~U墑k}oO#ܪ 꺦̞Gv1j&s#k4˰psaf|oZ|"+G[ђO_I<90Zr[>i=qۛ^)oR}EFQėtn5ңHXl;K"煄/niB^os8ُ'TnG8mC|+Փ\یm臦}OPy)j! z``"t BսR')j?"*.葨d4oqS蕖u!IEKeH:B1l%9q#:HE4d\qJk/r5E Cwʲr( $n } ezJr ~B4)nM(ч>#Դ8[[674OOmZ˦mM]I9'dR Al7{†P?[r[|GyN.ATEU6$⧤x? 3:KS@q}%@^T]aC .e o"WpY"ǽDu+z5: g;>iŕ ?M&O2_1 hgM[?pnƓ-UNtЧցUH*9 Q于ZH 8-,z@!? endstream endobj 77 0 obj 975 endobj 79 0 obj <> stream xXK6 W@$K6hoaӶӢiѽQ8A0FH~oo6{ }~E }T-"Do?fyEt$4 no3Csy۷0^ڜ.v>UG ‰U%rZr(Y{sNJѻz^pct~hwDhU4(ivN686;LCOep!5@59Y"Ф 2uE؏Gy3Y5Y$ؕײwaoQTLLa죊%ji˒Y\uzf62HKvJBIPkOX[dQ^ ]1S pEt u֡1Yrj'rhY.p::}6:Ԗ~pjρDQ{ RIJŒ;sWS|s #7ؓnz)]\ZyPKQ+5Z5LMDW_=vn+7Z95waus~.h"Y]O(WKa:[׏˘-Muz}r dUm _* endstream endobj 80 0 obj 1436 endobj 82 0 obj <> stream xWK6 W@"%[6zzvZ;-ޙCbV(Gr;hݸfhm_>5nom|Hm߯藶'R*_6x 慖麉t,7_w0^؜1xqgymcC;n۽-]7qe&. *c~Ec,:b8$ rD,Qpme(X^E5ǟy2xJE)"쪱^C|y^c;^q@\[E+VMaT:U5DNN| H!O[ϩuBih}vܘpb61Ag ϼ+ɘ*l+ieYdzQ5'{0\V)U󂢺IA*r?ѯV4_I2&TJJ>RdjAZkЃ)Nhлy3%J8S:&F_' "#Rk})~(`wKWkVn=Ji\3'~j'{/aӂӥf塿Z.[kȆv$ܦI.x L}yZ\Ɛ7TpOP| GyXWP̦w=]kBCem] z vw7 A0xuEe}޶bOߩ B+~MszYIM8.v oJbjK>3*œnbߔ*c_Jg[r!4 _%" TahC wwÌ$[I5Qfୖ̢ɦK/M endstream endobj 83 0 obj 1218 endobj 85 0 obj <> stream xXM6 W@"%dm=,zj;-K~)R-{n`-#"SCTGC}8~]*=|h}2c--e|xݳV:-:2 Xs~O'~xdG'w]Wq"43_(DZhb5bН @o6h( g+~*;cr \ka$h%|N5G(_"JXٙJ^3T$F?Feb|%H".?lHb/?A2vB+lں[ƂgT{TeSRr3mfx!ѷ0IrrqX€&XH]VkHb,I> stream xVMo0 W\  qۭ@enX/HY?02EQOR* ^о|/?ݕ (0B"OC3)Y\5Qx>myS>~]o˵S_~KH'[fػ~uL:‹a`:5>4vly88o i%[rNJ:|m|9#GjyrgkpӦ1U;tohBBUO(|o:P@rD[Qs^6p!& F@lҨDF2B^gh!@_=M(aE!gcd9i\(=SC9NNTD&|ԋ-!Mpo M,"S1Una;_oo"L:HV0+YHd sMZ-6yZ~.؜b]FK+Y 6W3RP>"A|B;x\urf;arRhƤFebDTX[]QajTU j 'k% f9&0Ou7[? 9#pf8Y.k]12Fu׵ǑFTZa^iœVaJaꥻF8+z꺄m$Z - AۛXf S/a 0D{wi7TL0\.(XYY[BK`eU"NN' 7> stream xTMo0 W\.?$ qS-flX/#);NS"ǧ@ e"cz^;@NNgr9=x#9;prNKr-R~|-`7t;PRY$SW` Q rՌċ ąvj`sPuk  aiR˩^-\ f6pMs4U]ZVgLpgoA& ,,@{KU){vpbQ u],H'Nz+Z8jnu~*#"FD-Ղ!95n>j_ݼMh7%\i`khxt3ec?uL X# ^nّ?|LIsej`iT$? J΢%ږvӨ>oҤ{YQbȂ rȝ>/+ endstream endobj 92 0 obj 486 endobj 94 0 obj <> stream xVM0Wf$Y1:مmZʦ{hF䯐J(x)izVgE}S?*P2&KWz}ґ)|ݫVȵlQx\w?3SXڨguDVΝz p .R@!&oxi n+qq BZV̀=>&#KdF8{%6'"|A ٱvBܦ89`9ר:+nD Z(31Aic'RJeͱq6b;SD[ Lj= yQNx!i)Sf`ǢSLVZ/96KǦnR|h [aD+Ḁ/t-$n !tgz"M7n`sg1L-t4_ 9s&QŠK|Ml)/ɷsHԩbJ)S6,v,"?5A٭\?ߤo^Ry endstream endobj 95 0 obj 777 endobj 97 0 obj <> stream xXKFW0ޮ~I##Hn 9,9%^%{ F2G=әmo<]}~pѠ?.}8}BXᄏ}УYG׽}^wo'럇v>EqH޾dOMO':5LprVLO6I󽵤Xz.*Xʅbo0YJ= KqRa,~~q 7] /xV|=;M,{.A6Ʉ&?P2!g!#8˹!bVf3r6Bjt Þ`.64zr6Xn0&~:*,撂4Z1U"…y+{̅]1ÈRȞKUw?ׇFW`K5iFN25Jn^$ծ!mtƯ̤g!"[cX#rMƝ +4lB(¥-z]#+#^57ks0uÂwΎs6GK2)>a(G!Z ~ap4¸MxI'?]]Yܸv{wXdP2up}y՞̰U; .Qpop8+;SXfjN8S, DK>0ק*UnSƫbsnj&[5FjWo4,C2Xt'N3eI~LX婜UУLֹ "]4;lUC,nmzF9?`{xEC? endstream endobj 98 0 obj 1562 endobj 100 0 obj <> stream xXKFW`oW$1$CKNI&!$d/W?gXYVW}UW2hӘfot ~k~>~߹hpێ_7.[}mؽ~G˰L/o?w?ǟn{p}s{=no6>eoC[{r4σ?BhN&hܰw<W+478ۗ{É:ZVwj4zޞغ %#L<;KhU4 /( ,B{8-̿x# ۊYxH؎Im1M<;V8r& iQhidz0A^=#EaCZ늩#tQ7`_-`iD-XIAv?d:PHD*.mABԕI,4]q N@/d,W+aTtL/Gك]vr?ȅ61AucXe phg54p|ָ֣AK.'֐E`qѵ5Lq͔pSZ0=d Ś,S[if+V[(J uU&g2xQ!S *C΂'I eAmh! a0onK*~M M3kB[d$ R)H9DQ""i[lo&yGl=BG@HVa+ۍ$B!c(4laqYJ*QWCZs5"RE3ˮI.W(BE^%)DNXucf8rȼ`lIeU-K9?Nm?35KV@zf[{|tߧ5ty0w('07 -xaw:7!$`84M,uVoToT!+uoTϋTμ*d^^ ^Jku CzW%NCjU'o^~;)Hʣ(}t 4D{ endstream endobj 101 0 obj 1351 endobj 103 0 obj <> stream xXMo6 ϯy)gmZK~)$2E>Oq';[x uK㏃o{;wGe2-eQv[]u6}Zo'pSm{zחp gwpv̀g燣)ӕnC2 GZ]{x'+zVbO; :)֢w7gđ-yt5 jOH> &r?F>@%J!6]vJv. Gj L9-D8$iwC(c=ڲb1bcv$_P7g[2هPNbrk,5wVo~Ǡ Oa%!"לkj_mc;]sBe ypmi }&\ԐѢώE`jz.E3f@^k. K.[b}7orshwĕ똛U~v_"~0j8F"?B`|Vx!زSюJ$uiwI rz9vZدB @ hۥ +> stream xWKk@ W3YҪՏZՍm~Ưwg j\iH-}ARݜp]uԻ=귇#=M}(_CuŎO7]IiwsQـQ۱r#nt{Zikb036e{>$CWԼdU]EXe76&lqb]va Z|b_^"^1(Mgkx/@; 7!艣ؙ}>f 202dI8Is?Y{tV{lwl"!9pFJ.'%Q0c;hep'gHqla{.SE]0V8O5E2)Z ,(֬vlARI1݊Ѯ6.lk\L:Ex!ل993qeڰ$ئ\ 'QNЭ`4 jRRa*0*(h`z!30!~1h ~h#@)<X$f)K cp#[Z\`+x™wG͒_3Geu&%[: 7 [7W:8Q И4&wԻ@Z 5}MQ|bk㚹0Wt(4omI:t6;۱^Rtrc:] 4Л2$F?P tIf6_ Vct#,tb> stream xXK6WfC^-n"ۢw^|H`.fdtn4!;;{{-͌R*?_^wz}..Y뮿u/4tk|}GCG|>ϓ?B}ػid.<Q'i_pGYT%EdK2M{S<yE`V?[s#y=#bQpM9=0P7n ыzKYqpo4˶ rZos8Bz`38#MBBLj~]$Lp{T*A|Qaؾ6*8T)|$b3"ZMETV_*~[ZǙ\- 68e@k@Vd6ƙt %9 q&c.:9OŋKJ(m&+Ԃ++$-: Zm*H0eC]"6PW>ĜBLl( )Sv7_q TZ9hH/np9<-w9F?ZT$o9g`0@E `ISPe3* 1GI &*=bשLֳT:p08>jslcId&ނAahѨ~C߰́ 0/llO )&FsrV9q G^T'=!|T:ݻ #42ӝ$u;Iv  k(sV@R bd3Xfvò`_)}\ \E*C7|6ʥ8mqKN%i jI[旳mT䛜uB8}do#p2JcWP@{ -a\*i (gv0Zj{>h˭j\@-A@껭TNSM˾6ax_Yۤk@oԢkIx0k{P]e=BR=| {N!G~K ίD!?j'[;7N.Cُ0BCy6%td$}I\;̭> stream xVM0W4,ao=ڦt/(<ͼydUOTAP,tIoڽ4{q{@a9kՖï򂏓xʔ/綼:Z\8Pds}Ռ@2Z9Aw8=Ni[^v29Ϡ%ᙏye[v%hb9Dnh_K4j'9w TA£ %M*yP 44SQ8n'N;%83czeRyBeir \F>,0xmS2 V%+@ W]`0u3ѧt;Zsl31Qta x8&g'+^x&>MRtQ c;*̑YM?Ee1rS.㟠bHI5[s!'0 kܣ;'2{"ߣVQ !l,_*Jml9$S)*ƣ,.Q[Cd-]앪b{(T dS "ϔc:"9 `ڬcDmEIYR(c!skF:Lh{P^5=Os~hnSY飉axMd[eيIo܎bh v6s4E*-S\\(n" Bj/" (JQ\cvFPE^vy9,EnRí endstream endobj 113 0 obj 814 endobj 115 0 obj <> stream xMk0:֙ч%15CR-ͥ3#ql1+x4zͣ<>}Qoԏ _O_+#il{Tߪ NIù}jSd+OnQF@sS ؍߫X%P.r}^tߚVn-Dຝi!Rw^|'ˍ,34~x)Fk(Hh4WSbƇ@ -4-'BxNtXs!vn 19qzS#IF$9s0?Yo1Px^!#v+G1ot̍0H#qw 3#z?Wz)s^3J,䧓Z:D>>Alpd-1M@g68Ne'ծDrِP }"뛋+HHb-w}m7X.y?/KJBZQOBq)CZӟ2?? endstream endobj 116 0 obj 594 endobj 118 0 obj <> stream xVMk0WXGcYhoBOmRҿ_iFGeZ^Y3z3ɢwIG+h_nzZVu/-swgž=ߪ皦i>T^t}Y?tdu}}z@Zkk.qrTq$q3qaO\/Ww䝟Pj<, "%D4b Lc@AF|'f|nݴJ(%-"ŕb Ýٌfarg9#0 8ܤ]]!Ucy$G0-L&+eMˁK:ʐۥFxxx&F"$T#!f CQ1;PS{@ojndr!rz&c75DLNUt'k6M42CKdOK\JpdҸ'll 8K;B$VB7рv$Mr*CrC n[Ǣ~ƒ 3nR&;&ec]$[q3D&@)S2ĈT2g3[ḳ9d™'ݱp"q'L˷b+˃7Ze eU ;ndg6VӔd&Voap}Yڰ]\Њ.hJgk/I9 UdM2ehY~[DjIc~dCԽ} 0*{NbM ˠNiC6f :M> endstream endobj 119 0 obj 828 endobj 121 0 obj <> stream xVKk@WهV#- [CmZJ\;}yL6jfo^ۭivSZC۷C~ow(wjh}?={m4/hZE~_N㇓i~wᱠֺ=~m|6etzk+XA+1Aʹj teeZ y]bS1 A2^َqr/Y\#Û'^2}L!JGCƃ*2Dp9TXcÐ-XvR(@uXW'Rcyl}BlX7{̡jhti̪0"{VSb\GX5x`xgWSlHc/Z y80<=E ܄r>&I>@yE2hCrلt8RD)ۥ4]݀Tje}leF'krNg'fiǒl8)݉cBULXXF sb-% nnC!pM ;,ܸZq >Z$p:amZW;"XHnf %^R{Q l^tфF6]Ĉ#1kMɡLFw(2>Kr`B7H9>wg̺筕\ҳxWjg"zS>HXx/smדMՓi>'W?^˼K?}f endstream endobj 122 0 obj 921 endobj 124 0 obj <> stream xWKk0We-CmZJ\;ږwhYUoyE-?RS/_wByVFs'3-^<ށkf#|~.=|p.+UcS{!Gq<?qn^E+ CWpez>kJ񉶓W& E= h;8IpBk?c(0u'"^ei鶛h\NF2djF#І`3.V+"iid3sECLcm ^ ^+m{Jɱn:[ u 6h b޲ɷf<@J9&2N^erR|2}3rIɩץ&q}6ٔ QXh>YՏښ8ր~3MMbQs+k;9Ied@dOӸ*ϐ}HŐDS%>[,sV~3kVqQB)j)hȎ*5q [bݸ(\-h2޼&@Q&3*gĘPIJ)U,IRZ<,&< )|$vE`qw$RFe f9 umI\p|<0X_Xqf -c"G-{rdNڦR6 *]2ye-r]頶OY[ai5uioͨ |}(Y:]#fR)k1(e98ėE>mx?(:2(Ag,bh&p(2Z~Qbn*l?F˘r_Oțk3Y +W)p W }XI @%] .c枀v^c͹&Nݠ> stream xWM6Wg%^m==EEs|"-Y(䐚ypl~mkM{04O? gc!׆蛶T#V_:ZՈ?6?oLӭZmo4ve00nnͳ8ʟbb> stream xUMk0WXG,aC{ ,PzJ4·$k+K4[M3 otWl [Cv_\KzкFCƞ)ۂ]|IBiA05{81#4 UfK E[tPQLY< ix4B!#YZj GfTtPŅ KhA/{v"[(6Q yr4W(*p=bEO4Aa!]%e/+ ˛- \p`_2UƃX_T/lnR\ga(Hv7R[]]PN<_}˘7 endstream endobj 131 0 obj 754 endobj 133 0 obj <> stream xTM0W`>" `@{ zXz6-eҽwf$Y-&b=ͼyPi~+P-kt~6y֘J?ћ,fNW9Q9y97V#-ǩugEhnty­]_f!Bl|@Fhو 1NbE*+S]@ Q||faca( s%:r'^;`eKrYCzZF| qȆg m`#({ å]#EంenX Aa]GӺ1oq>%ɕS1&taS {"Jӟ083H,"qQׁc8ݰ_՞殢])ŬxmWѥ %9.=04͒LC_%Ɗ\.$;ޘnS.yΝxW6л3{˳f|l͒7"[tv-OnSSSnQr9i+66҂лOBo7MwLs?rY,mǧO&<4s0?ҵ5ܾ.V#qsmf'#V#;藌Jg?%#5TG& endstream endobj 134 0 obj 668 endobj 136 0 obj <> stream xWK6WgA0CS۴(-K~AJ\I^;Yi|P6{UզF;QV6{xBoR)fr_꿪ȴ.?=W?9A@' mD!X*|܉/@C1%3tKL DhcށcɌP.QacC}f`_-6NJͺ%pՒđY&jը}k΁X[PIUN* pT>KS-8)= ]g6a!Aٔ׬%-+3G*2rSg嵮ShQ}qV{tذصcJ蠗x(܈x񖕄89ċ=4oX&Q'e,s#.`'ha(Ώ9BC̈́ONU1U |Rr7`FVGn9InI~bn~ ޠd\pO6ŧE > stream xYKoEϯ37Z)cD$!8 N@@D.ԳkfIz^KS:ԓ3:KYSI!7,bڻۥ.+_\+W}w4+DtzmХnc#@T[؀ )@ ;R/']CAe[3RN"4\"D.- `YUa,#6HA{lu4@4kJБEI8ΜwM=7#HPZ$7ޚ۳un{7_F"~-TzO¹mt*@I8'&jsQb#G gۿn])ݍe")-қnОp(bܳKsoP/]T]BJr6vӥoTX2Mrbbc m{zƩ$t-нЩsۡ tZJM|,]9~HgЊ1uPX]﹋Ј=Ɂ9lO5 Eg \P4:(fO%F;IDiS.;/5ğ?y|ֹiɫ4%C7Y$873օ $[&HEUj2mYi-P&IڎR#fJpVut\uȀn;w2\ LFUD…P6'\n߂i}jIάQ'/"?G@PhO/KnD6"_8Wt T@N7/H&يKmtWE!u53|(rGB8Q͹hqp[XYhc`-SuAZux^xu2X/:u{<ݧ~aüΖ Jm gT"Nī !lt  T::kO a8I,Zmw<ԡdb aO87:&iò禗A3Ve(DI5qFC5Ht[ӹ)98mudW9 d+_ϫ! I"Zt)XP(-4h7_ιX:5p,%\!c:;"G9MU~Qކ zf/*d8敏 欄$ PZĔΡ5o/;  _(èP- t5a.*|;c'H3PPE]eIu =2xd ˶- {&-Ft| 1O<5Bו&?:1e#T.-!$ w.(AW(koZ FK@:5T *@ٜښ_Zm$iO BըueY k{| =!aegb+:jK ٵ՜Ztf%H.B^Qc YeӔL_6Ek6+վfEYmO?' endstream endobj 140 0 obj 2063 endobj 142 0 obj <> stream xXKFW`mW$ؖ6CKNI&!$d/fXB0H_}UuɪF5GY }q0^_7.)[}i<|@r[&Oiq2:tko'eT0:\ogJJOfn:¨a:QyGz;tȉyQm7~ҴM4Ckc\vJPǣY 0} cxk70VsEDRs4rtp{.\durOQ3ݸĆiFZ-r?:cTL/'V\v)rKG#`kd#v^CXi`)a;\OiKX.Bq9 zqCcJs`l&‡%|[bY_']$ IL<1Z kfcp83ʊ:m&pKI!J4#Slju)W"ꈼNi .9” :pXluIb~ .1F {j3fAh:&*щu+fPVHqYXBuL2s6ф^Is2!Y=&TOAlJxd oB^T_TJR|*񴁬!<=d9㰆X5"hY)2BS4QjAc &rNGy/;A*~ҍcǖnms- 䢅`U:}}@Q؟F=C']i _RtlB_2$f8k~/JF҈<Ð{% ?'"՗@2v*}[O!T=ӻ ԐLEKݯ+kʈrM?,DrjuntY-9#F)ᔕdݫE| V?i961߅5zWV| F:B8)y>J2ajuL GD}IEvB3U_GBSls/e5E|qը+v@kOv. x)QTWUJp B PӛNo^{! jIfqɵ-E;l3} Ue;TwY0 endstream endobj 143 0 obj 1161 endobj 145 0 obj <> stream xUMo0 W\ H} uۭ@enX/#)R AS#[PJtݕހxM~O %7+G9^Q`N\yzKt5I]٪q05A'ۮkt|?jݴQrRoNc`3vKiц3[&u\˄ nNN\ui)*eYc Q 6 h'lxb g"OXWˋEzBV4"ctfI> stream xVn0 +t.Wd uۭ@enX/Q,n.i0(M>>>5/Nқxo\|Qnԏ T^V\~ U fvקݳSO9x<S[unvx5 z * N=tv3sXuXfnM)LhM%+oW 76%`62ag{e*=9u 0t 1G'A\̢jfqJ8bvIlW72i[Ѥ6E1V1 W)7 6$$ɀr_N@e -9Wi$`*3<^BФ̋Q`/\3BZ\/) J8ESc/ؚCՋ=}fRf~22a2Өqm*ur4͸ h[elX?6CGMq,2 x6t鮂hKNIkg{:0F 2xn, (pThÊw:e~"mFPR H_?IAƓ/޹%[w̖Y> stream xVn0 +|.`Wd q]ۭ@unX/eIDQd|||Z]~֪nTz8|+]KevOo"fMt~>>Wom=t| 5S}Ĺ[u(~=AU=F76ly .=fl̠R`kbdӛ6l"TC2DNGN6mގi{;įfC\~=ZX#mLq6QQ ɰk$㎖\nP3WS 3Ql3lSyl4gX{ EHVȖ܃Eӻ~f \pzn&hNۘG#I@w%*Yh ]Ck츶A9欪ɒ U01B:isF0صVZxM+mw+`f覬Yߩnx(y{>5fNTbBg,@ֳ͈)-N)ɺ,p),OI>SmO|@MS_(J=@,5j?JNUJŞKmUpZj1COttN {G(iKQM_זvߑ&~u@5@/ 0TM+ݺC{N]rJ%u*ipGiwH/o2 N&Vr3[,땙'ćj ךdf(y:%misWLEC;.> stream xVMk0WXG_#ٰ[`S۴w>$kֻN6bV͛7#ζ߭i7퇫gc[z5>{jh|<\kY#z>?5B߳$<ֺη/C{x;~4极SCJ]_>n֦qcfюu04Du<g;?n|6chA$OP|֭B7AZSm"؀1DVbӘÄLJ('+&vNJ+)c k^5d\KK{ӛNK+6Śͬ2[g,vPE SpyGgƼ~$Q^Qq<]7YMRuPgڋ6NjhWY*p!?2ἄx,v0dCY}^>ADJys)tݨ9;vqz,ӲRu.fΠv0ըC Rtj s>\مJ03ƼYڏjyޡB3a( D@Vp%ׯ*uxԥVbEdvN Wx1{|չ,|ljerrC9SN(@9I+ա'Zbq,tSAO+\E\FjrR~& á_kj*P|Ga`b1W'3TjlOHA} iTBOTsY‚%r塒)̼OH-@\"Ԭ+a8^\MFT{hvU{( rhC:sسgvMjv:_E endstream endobj 155 0 obj 885 endobj 157 0 obj <> stream xVKo0 W\ eNV Nݺah7![')6UTȏ)ԟjq U}Q?+P}lQP QѯȽ e鵺K=l~WS[nq?ݾz`G. Ξ{m! G:^h8ٸIZ=ZѤMY<4|Aqtl4S9VOYCPU((c6;V`,УGK`*GK9B;^p'hD Co;́@@qqk >!-Zny;9y2SQ2-رfÙ?)V5k&4?dE/V|=`\شEGPh]zfXheMOe?<Ybň(tC̖SۭbcD8xIJV(?),?l zxڤ\llHKͬ:9w'#(%'{cEڰAFXj˕z!/SNMeڜ7]!vI):'SJkX6v?J9vdL!e)pJʉ%=g_8}H]T%EOqA*yb1pMcaaf`WΒ>R8N%M-K| A ,Wu,]$绖_#R6KtlYەƶ3){agȫ?hg_e)(*[mq6|I/Ӳ:[*2HtqJjnyA[]dwIs(> endstream endobj 158 0 obj 854 endobj 160 0 obj <> stream xTK0Wf$YAo =,=mi^;mPLɼ3r uHLg ||Ʒg)d#[7sԪV'~5=yI?L&QY鳽?tyYgu)5$]{zrc!1bPSY|9J`+4&9JzĬ)\wdBib$GvGTJJ|><;7t.]U}V~%smj'S]/eQ<`N݉\|%́m#Y"!dS\U0áVE7$̭У` sبwC؅zRT7TA$%ʟs'LX C`NJQgPWK4q{3vW+3i-殉.@oA݈w* ʢeEjtl*&.z/\Bѝ׭Ü8cLc*^<:Fa+QVWbjѭG6 jz3o endstream endobj 161 0 obj 535 endobj 163 0 obj <> stream xWMo69*E `==u n\3,kR,,k{oHnBcG|W4]Gq~? 4rx@g<~LׯA[!{z;3~:t4'`+L9#~QJ;A^kGi᪈{zS tv'O-:,e>?ˌChbh”iZLFuw\L*BF^]>EaL%}ʫ#Dլ,J ;Uj3P_?k RÑRͮ@ga~2E'L QBvE=XrS) i݋[܋{E ffsY WRε.n|=M/#{Tɸ,5r3+A%#~KdLVzYWGBX\t.v)H*E&V,-T[@j6Ľ6%3RoAȰ DWFs}~P-~ r#梽ݩh nahlV/~5qcU붕I}gR^ʂI2>UC ?{s endstream endobj 164 0 obj 1085 endobj 166 0 obj <> stream xVIk0WGzl C<6-%ii.}6XNPG}oQn5[.}~i7ˏ =7}x bESyAѲ-l VCgk۞G<0t>\=~LfôӣWO;M1WOSg d6͠P1_VKttµ_VwꀅUWɉ.ӗ5ȬǍ2ƌU?pp2nY)B&6D 5osdD HmȐDB .#?*x9 ]_ot{#uR3upv +`bϴ ,$wwA^6,XLۼ!H8;%^QEL,.QR@!.fn$y5l0hR -IxKy$QLIS8".Cpj4U-cH)-:t4է̲n'$L""_O4^#t=-q2毋jܢjc\/%nojǦI:`'iO`auX܊a973 H;r^%m ziM$z\0SpQ?q"jo. +\+j F܍^]#Cݻ@ז $HF G1ޯ[ĻPQ/!x8B|}gb S D"erHAa4. RdIϳhOſ$+VcHe r,93NI: zEhfm Zh-Xߪ婵ĮWt0Tau-T8W&B?MlQ:j2VHӜl"ء):Mtݯ]hBcIط-ysެ0f!qf~Nd8Wcˉ4%JRP@bamK> cV}!n,jR0 ? 5 endstream endobj 167 0 obj 1004 endobj 169 0 obj <> stream xXKo6W輀K -ڦbEҿ _E;n # oy߻: =5Oݷt;ɽ+)(OZ^P}wv/9.;OtwLc]ގ ˗e†"n?IizT8#qG5=U?CyN`>xSx(JQʍ?_~ܲh2E)l:6M~8bh,40#ሴ Os܄ P4ԁSޖie%kFW` ++1$ ==0kyC⌥F UP@f1*cZ Ma-KM|ebr Ӊ}Y9Ehk 0ν#hǨ l(g1зy NM =BTqMA|kK0ޤ Ah Ylz /Z4밦@QykW|]!жP{ Z Tﭡәa JHVv"wf>WW`I9&nʩ&C'uTed}HS>S>83o"zH ׫p V*@48ױ9@7lhEivkJvde%)l\|TW> N%2YWՑ˶zYm0Զ+d_*IA?WȽ}f}8 IN5#mANfG~ܘ\2MO8JZ[hck(%@}` O ABWGIqaMÐc-EFJn ȍ> OqDOOB,Shj~z7a6;Qbr#te -*iWӏyAŎ;p/L_eBG>a􈸌a^ž>3e^4f-ȳU f!0@9x=T&>T5äeDfd^AJvwsRBiSs 7PSU%U:ad AXy 7A,w}sM7J))[i2 ^mV[KK̰J]) tv~;erZJ4sCO\J)2wᢓݳ-PН (Cχ#t]P]AHj-sehb3a5(v: j<'ꖰTl3-oo4IYjo**M+]FX2{#_ Sr/˛T{t~ endstream endobj 170 0 obj 1324 endobj 172 0 obj <> stream xXKo6W輀$ˊ@AOmb٢{)5 :S3_(XeIa\ ,.ỏQ]>S*AI;29i0N}Q)j 2/*XRKZ(3l_B-Fg3V>7N endstream endobj 173 0 obj 1321 endobj 175 0 obj <> stream x\ˎ%9nW($Pex/. +mcP׆g3o񐢨[ـ1YdxĐ(E7w >~ϯwv_ouͿF߿ݏ?=>Xrԡևx?~t}_|yaP^__\OC__Ǐ2?U5z$~?#eO?)v>>~ɳ'6޽zΤ4m7jqPAe OGz1_?K)>?Q?&Da;`DOG@=?he2O)| 9 #(J-bH~= ֻB^h-ʼnHA{e1I/,2&N>q̜!W_1 fqa~mOE1T^ZEy<~ޏr_oR'-w[n(OMG| #)'%.6$7NZ#F@(5_Ld.vIcw轳z'/etR6Mr礓6/1{8B9b ia-cnƟ[݇\n"nb*TpKn/>N3nP%ٷg Id<*J~07[Ư[y&j,*X/E颞y˿I͹kbG{ϯfͻtT. PcBiO&?7"! z`Єp$JlP. IH9Rġ%/dD@hF@2X:nY099[Mu#tbV<G}45 saA3q 7'5p%e:uZ'th"#Pͫ@5pH( Qv~pE* $*% kű;+එ1*9<4*mԢY'nTa?Zoژr Ǩ˃t3zoce +jǍE$ڙPxVh${5MA64+zEzP17鹥bQϬ'ެ^uwϙ5)FHӖh˦(СLu58ڀh$B&"=4[goVu|q38d6Sa-bXDE]}G\p8m!$C5QO"&SbT ?>쯩QnP![oOv>TOJ->֫?|eÏJ]4ϲcEfД=x"(ṥbQOmߪRiq=!z!Ol~upMAG*Wj3זl !zq30'Z;W4ӢC\bxվMA]&wqD.<_뮍eˠy߷UlzTcr_eQ=FF X:eQm9 bSd@b<4Z,6UiS׀ELZٹ_vq.%ܙhcΪQǷpF=LmkAHB!d=Y`ZI9k{i2Oc9N3 q4Wjϖ;TڼҼE()TUgbq ]-)5G֌2ЀFD - 46Tjm82),O13|Gq\C,qͷNZZTDCh5꺲n5ES Q`x3LrzIJ"itQϬޤAw`hG;R|3QևqbXZ%黩,IYϘ`v,IVVXrbt䊺,@wN6*# )CahD[-< t?|hr 9Ϣ[9sRz,Fc<+DE0T ) :pUJ)V)c:W.~vq}r: Uƾ'FtKem9<֘47=ߏʩwA ^vXu/Vc;])W5aE) gx8TW3d2ΰ!>WfDۇӺB !+Y|xKlԛ^Ѫ@upJ'[D9x J.Wk*Ͱاc )l9ePn{,E,UѓAz D|D9VBdӁ^5=Iю7F8Tb |[*mt{A?BXQS̥YʘfR19mK֜LL$M :%YsšqӲ'颢]t078Aa ͕6^\߾/ -e&$:cCJ{XuZ5SU&.QwLvݘh ?wJ?Xs0 "KT4CK7xv#?o6֧}奱Sӱ^M%vX+#2%.W9 k$YTV6}J3iUu?QK_nm4X37>ZMDE_Bk Bi hT~bQW.궐$.zl\6nᒁym˰&>D:bMgDIZ|HJ^U]+;aޣL఑ 0="6dKo0T8PG|KH,#55nLX'FØ-#5.&5v)ߨ7&ψBS%S Ô*8`pL8)#W1q-u6*6uTɷJ)i|^l,Fed]B|I'@1RӞeqlR"Y+t[zVJTc"ƹY]- CHiyA+I 521u׌!lC @R k-f+Ml|Z x74\̬Zp ݑc#ME9 \:ܬRӞ5s'hCLzNq#󢄑R c<8$^L҆ƷrJ_&*)5aZ4r U퐄6&Kb8z(&gwrm47K0/D'E"=niBRY. fx$/9@y_ɻIKJs3jIsH'asjx&s)œ+ϮDc7䕗;8/?<7L8J0#aռY<=<@ihwg9̹.^+KN3#ǐI3 pjf9i~pʢS clg\R SL$ ;"4$Kذeiv@;4<8NL}E$'Ix9-_\FhIDۙ.^ϢZ u,..)=V9tx;YrђrxD$}nb#ҫz8ѓ AV\}Q dSx UegheBNn|L)UKÐzdMRr4af/ 92NR7PU)q\Y,)2>˵L(rjBZB;[9q%y[} M(׉~ƹ 9[z`̪Jiwޥ+&TN򢑷pSNoSace~㟳E_^JKN0HT>+4˩ +A@ #6d[2SMju.ێ2ä@Sw| "Iaa?8 ~!UCO3TIeޛY|k$'D=^I8d4ݛe>^2 sB XyL\kzd5 jYuWq?wbqeJ Pb,Si 1/u|dy"{go@oJExYE%>iSט(Ϯ*䥨`gWs3T&B|RL"݈EJ$z`=cU 0,h?0PJ2(oxȽ?n#xM ]Ջ98WϏ<>| \+[ɡ/,mjx0ҟ &oLs9ƚ|&-i6zI,|ƪ̱ ccB.XoCPq>Ve1 Tkp^~7*X/ɱ(6kB%Y5ˋ uȀ,KzB8kI:cXW{sڳl@1kIYdɛ%6clWkfM4#ar^ É`f߬ƚ` WkMQ8\0X42zmJͦ(4!hdYkRIJ8/Do0&7kt~*,ɱ`ި nvO#^65._RCr6Q0±. w~YcdncѺ12 m?W0Xc=Kru>5„OQ{IXz%nK_, ݦe`o09=б2ax3_n7 endstream endobj 176 0 obj 6199 endobj 178 0 obj <> stream xV˪0+_ F U۴MW4I"F3gFclqjrk%~ʍ/C7-d V]:{>]Wņ7X l̞O8Wlz3VgO=]S_AϕGh,›MkAڝ칉qn)[ؽX\"{7Nu~aGH K;#9fX [*9{"Q>$|C! "gx.뉸IdSOD@a8 ˊAHX `n݊(I때 C@sV̵ vA]/ JuKe@--,݀-ՄQ)6Eû࢑yd"ȅ&b4T-<8Ap1l^r!u 5SzGGu׺#rhŚ5QtO'Q-ډKAB:YO&VQ?DXH/ ;JD׼d<~ULp[t7Ӄ~ǫwM om R3rXJX%@`+yH4XnKjm\-zU\_ endstream endobj 179 0 obj 691 endobj 181 0 obj <> stream xWM0Wl q@{[Pzj-e{Hvv)!#7oȢO-ꃀ[ܵ5xWd>/+ { `shT`iF#\ݿ6~t,l+U>?w;>X^㧣(vAE*.cA aFzMM_\%\jK0; \&?N oE?&C~)sZ8(?dJG oP$*o[xI恉<̒$nװa]!`d-0|&Q!Cpǖ>,|F!!Us]IS|WmĕҪ3a06 sLg -g,ϸA9l3 cCZx;<'Ʉ[:y/EUbuC¼L+Lo%eAO)̒J24z2" ׊Ţb MMV;hNX?8GиAZKjul!&CCYT,4LBQ[Nv )e,7Bv3nZr(Ò{:Шq-5/bL.nv~,s9`4ΡQsvv{ % l:' r;!Yt^E,nSbwGgژpYmM5Ȣgw՜8I>ѯNXtpa aˍfv,VhTPW(}#bb1_Rzwʒ|C9 l^(ik);nkʘ̬Y,g^.=PUpZ*^S+9JK2n2 9=s/x7P ~~wٷAҤ_JT endstream endobj 182 0 obj 935 endobj 184 0 obj <> stream xWK0Wf$Y1m!CmZnKҿH/!OmEtiЭZ{@>o? {mB_r_z]l:|}m?}S1ZNo㙮U{˯ti}ܕ1vG~Aoqh7 7H;a((ل[ޞGp!F?ڍ/.]'FSjLBΉH-;=SRwa9iX|i֕X#E@<$R@%AE,ڄ>.lK`^Ώp}Xun6{*sorGy;8tû$28$8 j/)fH9bӨB4̘ 6N-S".|=wX&Sn%Q@J,f2/ރD sfX173"cDHbRq&ZFPtr&9#f.x J!G1219^xs̚f,P8YW$Sbgي7:Ѭ` {8%M1<2}B肋s`k1MJuĉS=mU"a:MY(UƣPVV犛5&A\LҭK:f:>-C|}}KUIr(8Ff9Aq˗4w,u W0,Tfw3B7IDBM3sZq*9ǵEy~uKkN?A endstream endobj 185 0 obj 899 endobj 187 0 obj <> stream xVKk@ W\؍5O! =ڦ$-ͥi;$PN>IiWa&Oڏ_ ѮC&/^_/=?wY(<6WMn8ύG=mNmtܞ;r4'y_NMz?`Za:Qä~.| VEt6tIM?x;Rs!?}>xWY=tGW9;Bt?bC7_ewC+G<#e! +s K9){ZK}vQW8 !΍bZ^kO] eia+gDM\ɖc]`j[J/RWQ!Hx#cZY{ҶJ%gG0WAB]C%g̔۩B۬/yTz؅:OYTw3` kQԷռ4`o@jo| YE\Aljsw>HKT TR`#Td3 KlV*㕥E"H\u%Մ|[xUױ#E#5Ġ~ Rч) SM_̪B985zM?n@<Q%gȘq 86 f ƞ;ˡwAj\0gv;J8 _&LK(U[~LBfΑDD41"flm DUOMiH cNR1Į%U!TU-/Sx%TI%MOZXRcLJpÛ( o)9wb>ep$ĦǏ=(]o8_hx @+o}n`y²BZj~%%s8 l:Ҫb.S.=]#zdӘvoYo˵afBjgӊ?[GQ<4ꕸi3oGyi" f޼kxXC endstream endobj 188 0 obj 1020 endobj 190 0 obj <> stream x}RKk0 W\Hkɯ]ncmF{ߟ$'n(:jCjDޱr?\h/~{v3J]7'sMTsgHwz%yxWA(qNH!6A $ d!c2N2d|&uSDP/͍ҴُFk$Mkki%2vzo(' S-]3c!*RYmn"h&'p!aLj5ZV:a%vD*(1I7ĵ-&x tRctVB;}0KTWc|T_ם\b .h‰&[ΠڎgaV"_E|li?wgoQѿ endstream endobj 191 0 obj 393 endobj 192 0 obj <> stream x tTա~ܲگUT|U[@Q@%yD4A6OkZsYx{o}|hޥ 1\0%d=cg~yg-LC7νN=B~4ҪtfɔwW>􅫍sWhzh׆jGgHdbK&>x?vd4=G6>܆4<0{œow}մ%c`uMhF7N^!Uh,88'ǁo7b"BtyZێv {V땧x37-Y !kfeLs̎Wǝ~"+}Ѧhn4{NJr=$H'R[(ŽEj*c$ېue~}MOd43{3.}|]8`2VM=•W,}fmG[dzk+%GGEʇAδ"uN|{[5QEgzRZ{ڹY̼ZloUO}ʥu2WhRFkeGsH7 qӢ fՇTZ[N;Xr0kN"x &C+*ׁѮHc]5}sѢ/Wv.D[ʹEŖ{lPѮ; @W!ʤ?'Oj\x냃_OQ/cݛAw̌y}Fnȿ,hϵ1+Şx^ &_IkN}AILێdeh'F=?<]5߾963/$OMo$ؕ/ou㸡,aX/}&bnxx^#im,vkf_/v9ϪNZ5/&\Se IrQZ{v큣\,mph?E]>5sR[Ĵ%pɥuoՇȜni9 w̖;e"a*c|{N,#fʧ`jɸ473ݯ;K-wl7\Q jXrypw3CK8NO[i/W7>UWa9xǾXjpGHyU|zey[Zj{& $'233 oF}ٺM9nYugԥY=qH-Cr8IFڭd[̴=mUyG9 9p ɳ'-9,J^_/2yGIߓfWm6iF~:foԻh{D*ߖ2q1qKm_sE~K؝o >f;6c}S w;Lw>iDȧAs[z_z EA3ms xZ}i5N+{ۗHF{Dr-}MF{/s}k}'fJZ~DB~bM5bQ3/&O?hEW&:`܏f?c ;Xi>eŻm{6yA4~_Wsȵ)'[`peO! N\;NzgCvm2?wmvUl6ā75b;-9x7%OEh;kcAWmU5~k 3eQsEP귬h5w~".W:xnjy컹#+&~QTO@OLl5sFݝRhb=N`dC#NipR̓*aߓv5#Snms),lG;2[O\Ԗ]Jx7e/ڳ*iyYqX^C־O$ l9_piƢ9IGܘc}BEn|}{/*qtt[,p#-L}Oe A/vt~=U:f5sFU;gDݜk\Up﬋f mzfS4Ɓ/sn?+b-=Vlm$ra1>>EKv"- n؝]MoU?ji_O8~}eU3.6j/va픡άpJ]ʠ1߯n@KalCF{>mm<mmEm5%eCqC%]-={/oO vݷGr~Ճ;!7-|v!(ԲN*=lʯ+W6\hyfp=㌶1k=ٶwbw ]*w®,|a߮o<b؏7/>hհ^w.{'^xb'w^U+GE? *; HrʽJM9!5ju? xPz82iցQܶ0{Oҭ{kAN5ܺZ_[O?/#þ?ڎ(KQ}f=cD=o0m1yV]M1a|>*jj$(#WXUd)wcI$~* 5b οI &VAEMQ>au_ZQ[kJWWZrD\oEF{Lm;a${ύ/kG\I}X HLƈ/!oh@# ! ҇\ #QF^0G%nYwEHhG|@T#zO} 2`yDncN%4ڪbgy*m93̴ bu#??q ;Aݶ/z'zʹC]O}S༭#DmښYna0XYl]l˖5uZz;>==݌쳯Ⲹ%_2 #MSbX~koC.YtmFģ9c9#Gfyđe֡[%`09rOr:Imr7~wSK{bF;)9a"i?h=\1! s\};~ +6׭,- o}|kRjni[Ζ͇֜ٔk} Oc0 h6`hm]d[*rAiNq[$A״`0:+_К[/)Oyǔ(M޸hmUw?{|&}wSΦb-@;& Ch'nfh@|mmmmmmmmmmmt66Oq8h]ZZvo}dzQt5=fs&v3y<ѯ9w9"!@7D0hzmmmmmmmmЈd 85 Z  ! !v^1̲"]>be幾ws/wh@blwFg}ͶSqnEv53w-Xu3U22=3|Grswu'%%%%656djm3j7k*ߧ"d%2UsHtPPZ vnf-\E'CC<’hݵh~(3t"!G6dzQt5=m10D:CGeY%p#͚6"EFd=Z\=h^+wIGDm}[+$=m@gmmmmt>[6K'}8# ! !ڱUշWKgOx+.-qW}s~\!ڱX!S__Wmܾc67]|4]wy?:sއX_3Y暏vx5MM͍w7qǗ=N{ϯꆆ:qYe8>hDŽP4Bg}|->la䥇sCN%ayNaqsniׁ]鿌n!1۫>WUozjܯ6oiwo}U˕]#q>Oa!Q_J9/~n!4ynzn:9}юiEF5ŞV6y_s_.zQС>-8"׳UlUE J9cH}ͦ29QAUb~;2;GOOl[_by8{خVEzC 9gb>|h_*묆CWHF~xH}9w\P;Tgj.Pю 1X&e vY6DZn0)r=|_7X✯$R=Z[\g&:hīo3o/]/xΟyvVȑ#~>'d}?Eb9OH8r-g0ysn=p02Qw?.6/c5^[5Q՗# 2$s-az=Hs:ϳz0CxEh?]͵Vh?-%=5F/ ;#`|/, G9oG=jX1?-γo[Mo}Z[OXVTw͕syߖ(6+8.[OlV;bϗIh[ySiG0ޟƨd}sq%!GT?7m&g8/e+~ڵJ펏T: @O mߢHu>$h4ڏew<zG[g=`xzhhDg_;$B/nOMkoa=~#??ޟpLD;&:z_,* ^y0̴ ]H$ޟQkD/@CD;&:y>shDg_;/`M;"Fs Y.vtkھOaw nю^;s[ 9У6hhF6hhF6h]Ovii۽!# FhZ͙]tFlG;yXel.tCA #horhF6hhF6hhF6hhF6hhF6hhF6hhF6hhF6hhF6hhF6hhFhh@#G[ZVx Hp=2B6D7-޵&n"Ӟ鎙1'Mw]RRPRo( "A,6VJH}KAii~rm݅?\`*e+,8^y<%%vk.--p7D!(akGl]fϮE[;!xy,3/hf"G(i8DHhwxDr#rm:w D8~EB#tmmmmmmt6*6KѾND{" ! ! ! ! ! ! !1+k vOG`0b46f0 M Fhm D`h46f0F{kL2cdvQfIb`0f$bcD;Fho_Nt{-  fE3evU]o2]ju.ߞjFNHOm*aO ̴󳌬cla0p#\QAۘfm j YJhNb3m8,'DmښYna0G"F;L;d^aK$7UZp :Bk)vS.V؁[`0}?͞GsnFQhCE;hK\ VCh=Ev,nD<8v<z(G| !YY*ס[ 㑈z_NH ZFvH8xaGƎB #mA6hm`04Dh3 & CA{!CD ! ! ! ! ! ! ! ! ! ! ! !vA?AKJK{0ڥn7x<EWٳkn6gnwa?SНSsl.tCA #ݏ6mmmmmmmmmmm$b޼"h"v-3,.$\{[ZV/}l-AB+RD@_׃іuE[<`9&}ʌLUͷKdѮN7u^ř?$D$ߦ6gO4-PgA?4#o]EݖuLDžh;9OAii~/D[ܖ]=l96ֹ!jgvU[oK)7$($nw-ڥn\s;Zmk dzQt5=m1QW߃2>@dV3y㕬iHlQ.t5$6ڏ,@t-q,Dmqp>/whÑJm-G;QO !Hz9Σ z! ! ! ! ! ! ! ! ! ! ! ! ! ! ! W]s.OIbAܢTjzJhΰfޙ9]U۴La]˷9i--Szb2SnkJc/!GT6Y[B(v/}g;?c7qk[Oi(an-U 5WGt?IMV?;D:󶂎ZJ؛Gq4GRo"@%t}rɢh;7"yF_<z#7FbM[0'L$ #F;d-"pr{> stream xZM6 ϯ98dK600 Cz(zjn撿_iɡXd#DҞ 龞<`4/t?>.|׽sQE\߸M4qQ}O5wx=vO.Y>Nh)x`Ww1a=˰Y^.n_ӧpwzv|ަ%j 0>% DGX}P`t~.ZM#07`[;ksxb݊7# fsv DɤW!d8q'xl1I ӝ`bl|&:)Q+{hl_Gύ@JaCPgOr7oJ9%fny.\Rss?n%29Rb|\'rI}(% O٘( FO9u*hubƵ0o9;7JLUqS Ž|"Bsq#5cn\b#C5_b7'Ca1 ҮZ՟Odd\f:j#5=ytS$?rC(}1P} 9f֌y“Ze@&v 6=vSl#B%/!ӦpMhNPBz6]|8Lx2&l2HAeBy/nX8r:ٮP-@w-0f1D _ /Dκ- H--2Vn5lY8GjLlV=Dw` LSDJp=b(d3TfSgb" kwJ$[Dr-X- H%&T!j(BWꖋ R+g*De5Zr l(вNKRuJ}F%>L[ ND8 ,zy(bQi(p[ X@NJUBTb41mD)u*^R5aF3RGY$ە"@ȥǕ2$ZEm0.GC\|]9ƀ5YA.5QIj`ĸ\m`{ K-ǔ!RL 2^ImQdɤ-xN$cX^0S'QJ5aӸGYE#t *y%4D8-Ậߢ_G= ^ Pb/  MbP;eЗpQpfLI¥1+%k +|!i/CE?~޺)t_!܅{c@r~Q_#2L ]8vY᝺tI靺ԼH}<.UwSiĝz5M꾖 J rۆM*5eN endstream endobj 196 0 obj 2000 endobj 198 0 obj <> stream xVj0+tIڦ$-ͥьlD+Fћ7iZc 7N*@{a*M~E_藶d+~/G|G,NZx[=,VϭnY< _ru]6?jL uwV{;`k[]ls`& oXb.) {@Ea~.>e` `.E -nn4~3Nij"$%ɢ$K4͌qj1o=d1cMvĕPP3^>a1Ho i\F4W3)4>-IկH:]r3mɠ/.`JZ*Ym )AnRh)QyȌ(\5+M x"}7X TpP=nF@ٰ#69KUw]Nd@32l8:EЮY؋ޏ ָJ_8^ Z%qK?꺴ݪsg_nCֹLk(\.'5`c)ᎳxB~66T \bqRͪS5݃ >`twK,ct|XgFH/ثmf/PK/΁IXjy drރI bG endstream endobj 199 0 obj 725 endobj 201 0 obj <> stream xVKk@ WXg5,$ -C)mZJ\?-K3eӧOl4[:Mkt/W3;^ c2Ûi8?]K{{{^^Båy@/}WptjƝ<rܩ:=_eM.M]mSx`z_kZ/r3{j /mC(LlƄCN>| cSI0Kj㺞R#b\tb|,,Wcqgr)3fx@$_Or§HPBeO=ե8Uc:g.cwq2,ӅΉp ,1P-X)۩ Ŏ} 6p*qTTqjS3Sҝ:N,)ԫ]?t h weGwIbZ:1!?g'/##^@Džǎ9Vz|yFe>jĚc&};;sź.utTvPOT5 DWTS{ -4.;j*#b,(BG^a4a:3JcZx^8|p Vh`g^fjl7En2M&e LfWHj+z${1lB3 t$BFԹvVڭE\h9тja4m֚YÖ8+pʟ> stream xWKo0WgرE4J+q@P j/}WI{@zx<kvy,<W[ϙ 2oGlO[f3W[%yEL}.^y~7״LKBעerG f Ld枵r[)#`9L!>@z@ñiuō b̋6uI M΋ZA B҂w nOQ{CCfaah OEF{DK?K$teB_#.%ʄ Np iD˫+FAU 6 ̘Z#> stream x tT塨7pi=G[c- OD@(| Zy!jT) hSѮ1Đy{2L$Ξf&3o}k{?g2`,5Xh\[:"c^qXVnKnZrmE~4a.`&sNy9m^qWMsz2T\7re>㒒p?]:CIOUۏ'&и%nFjRW&Y. !"bAz yE.|gDjZᨼ*cF|NzSO^1ʈ[U+r/0\E4ӝ~g6c5YS );eEƅ_}6re5-2rAemoDDZG9ux{c*#{[ukUst ?v4qk' P:욼3.=xޏ+J?TmLP7_=֟깣Ϟ[ emSr6qZ>3IGj|~}ӟ6+wr_z=K[7n\_[^P~ǰ?Gb&n3j\LUc{Z(-܊O1[#kzݕ߯]yzEŔk78`pkʡpisբe{=x: ?fσٔ=Kq])Ku<}w^m}}13eyRfy`%V78Rܞvݜ3k~oWޙHϿSs|n|3(zw15Yt{#=k]{ͬ/.HNammCČ1=ܻ׸^=1VoMYyoȞSw\ўwu㪧wz ?R{ >i|fwT;ja(fٻbt?}RH-Z6'^7g٘yŪ瑤cg0P% ˌ?4 ٳOˡ?g-t#~0x9N]n5+V[nzˇǫ{ /nNW3w{XĒ +0@q}A,%6;q$8<\=_|{\m =/vߺЕ[1u.s9t;nW9ɞ_'r.Ϗ}$GhҐиFqO=RD;2ߏ?ŬsyF1u愿yb9y"n[Ě[Nι0}/~E3_άٱǵZPGwΖ5pqd:?ԣuWMrlܷ.2߽J꼸sW}+Y'_v#{>Fry|.z~J1J|n'v[8v|ی"6RxS} ޹w8Cmk#<,kA/Ĭ?IEj;dH)+nGz(\Uu: ;\CW߀Ƃ(`S& Oi٢\s{2lr-TϜoNp8aG|jLYU_}\\ܔ=6~n̅o諊=IzfO˞ˉ3o6{O{T_Zz w F]f sJԜ#fs+^6g^g)sjingw`ߩ>5$n8]123'/\1ݵzk͜{E{^5Ϭ[q?meϧyu==湝=tq'w^阕{n:`;[0jLbxϝhϟ^m+YK2e,huEw.{jce֜湔{WYޅtkS06wmP ZV EE/=nѹ\qUXK+oߒ$}?r{+{.ҭ>ݞtŮo>ncL1>|䇏y?}ͮѫ Gpul쒝fϯQ_/0$;(wn\$szfߚoO?[>{/^ğns5ط]wőebB4ČR`Lwd^O=vY^(2r\˦n|Ik-כӿfmhe*o.H~ŨksYSpMޱ+|cZ9g. )zhL~X>CݸcNAd_WOﳯ_2)^|;)rCQ\ l_y-s|: g5@fVͪ&+53'^5 K8X2ٹ\Mr_= ޫN88wL*WO8D=׋Z6v=vm׸9sjzg`ȟh/(hry_ޛsYgXG5',X଺g:Mp99k:sx\?itsu%*+Ʈ>>oTƼWNzz8ND!q;+y;/FQEO(˾U|i]vjɥ']W>ز)_. eޙKc0?TW{1O#`Q.<ɖ`yg_z~i1/L gc2W&ֳg}[1ʷ/:WZ|CX=W꿄HՕ*[3?8yn4h5˟pP8._IsX ҞmRi+68;f?3GH҃״+.l7uHŕȥb9ɘ(/WѸ: 3˘)[S)#p<5az~ˎP Ez.>cOKKPt sC\ļ-$LMMFy$LsQl֦h#%W1\V+*v9@z˒+͌;:b] sC竳cʸyQaz~z\)2׫K¿9@zT[1W+Uw|+Ls#@eO8Wχiϳ')@\k<E5yUX\ўWל{5 Q=w:.qz͹:"bT sQopKVǞ0=HWTvn&rz 0χ+qrUEg]Ӆ"桐_YRn|3WVs zܽ;ڱcl}W{_+.yUlkmlokh9z@GwwgwC]w+zns=HvM) =?z@9s=z@=z@9s=z@=z@9$ %$W'm)=z@9s=z@ =RCq2=H@9s=z@=z@s޿ 5$UIsA=z@9s=z@ =CR@{~2=H@9s=z@=zI $9@*z@=z@9s=zgO) ER=z@9s=z@ @9s=z@=z@9-z_,z z@9s=z@=z9 9s=z@=z@9s=z@=z@9s=z@=z@9s=z@=z@9s=z@=z@9s=z@=z@9s=z@ i=gDR=z@9s=z@=z޿ s=z@=z@9s=Hvs@=z@9s=z@Az8Yz29s=z@=z@9s=}>xk74 'sԡ;:ZC!0}:Zv\=--A1ӹ7Ls󎎖vss j075fb0=H?TEE[[1\<pY=yC3zd.K43p9wz/Ls~E2nz^Tv;vs1E\煅 sC V.h5Νo9@z.ZI\C9@zz.Z^"2n< /=H7= ݽ.2zkWsTzt \"Ӡ*OPzX=VO*?[=w;{ ]Q=--^h/zk_}}ExχsԢz.WU}u1](b Ŝ%%9@!z.9see`/y-ݻ?ڹ;}[>mm!z9""9""Q9"3NL3]zQ9"sDD=<Ҟ{eOk}\S1*1#~hEo"+؞uj ݁IVG0=&D;E #{ߦ~|.z.̷ZuR *}C M1yvww66AvI "blkmn Dx5=Ҟwvvtv#"bRhk >WzjڂvVlX.3V>cc[[s%꾚!./oXz{ f-j=칸yGɴ`Zj եm'r #7k|)655z}ܺ#d*z ss׬}#:12!gSruٳMW-y6EW ɲ1s6u$Ȕ2ۜemk//_}~iJ.-f-69l̍ckCL`qV'– b*ܶ:Rrs+Lٸ1mj9-q7{OsmEaLpd\ܺ#T5Xz=Sm|>TS7ݜ4vy^*v7N3#P!t;%Oa(@Ls\ϑjJjC^6 bӴt4q\]Y>Oz7ئD)l(utYM8G~q &"(66Ȝ @fh<ޘI5dǺ)Eds6 Co;\UAϭ;BL~]þ;&Smý1yju{̞& ~6H]pEc25J剑`]b{^qylu5Kk+pE`J.\hCθuIƶW=\[?=JO""@qd7lmu󀯲1Pr:;ۛ:;3ƮvČs|#؟؞#"bzJ#"A ""fϏ1=爈zH#"!=GDCz{BD2y2η "q9"bk\\e#"fQ9"szeOk}\S#V<{éQZ+Ɏ[XV>;Mp{`ſ?qz.v}m>= ĈĸH8;{H(ݷoWz.tٕbDb\wWmUOq|wDw= ŸHotk`wwW{kWcwƫ}WKr@;Z>1` QWyggGggvtk1uZ= ~g˖w<6V lkk5vؘBprCx?qL2ob7?9s#C|ŀ=W;Ksbx"׈Doy3w 5v57;-'fKwob8G9^zľ{[+Swcš:.v$OvGn5޶{C(9ON=*[[>ẁ}x堌77\s4lg}9W·#x+2D"=SWTh=cfx9ƘGg=O'A|pKٲ*U$NM)>^n]}5q46=^@}xwmc7Dͯcܴ62ib"EnNfÑk=]ϭWn5k&w[cxv>[ W2}mwY}toqp36:#'{`>&vz~֩2ou!;1Jt<+N֑Ob5! *EӦ&᪯oC}PO 1 Ɯmb(l7El1mg_}qsAJ4Uh`cwo7mNowv|j=76=>On2ao)Ij=o~A\rgW*KuwDwh=}LbDf~k/W2WU%{wnŸZB.;;d7`ڢ=; ϿRP^A[V~Yl}cyxGxWU*.E>_roh25ޡQ@uKholoH]]vG:;;BwD#"!=GDCzQ9"Z=7V$~̕NUq?}""";^o"(7I38as"W5-&<!q&A8)qK%$'!?T+O<%/1(gwP endstream endobj 207 0 obj 9666 endobj 209 0 obj <> stream xVߋ0 ~_,Nj/{{֍qؽߟ,ى&mo#qeI>ɟkPJiqt6_Շoi4靪8ЏM&,EE}ZĢϧɪ/L˃M_-`mTE=hnU4Gg4>mzfǏ`:h :y@,h@Fp,, ]ݫ{XwgJdftԿ͚NpX3h%2)@MDmy aj]=s==4'8$v5&¡\y9?.`&Xt|Jt('{ab׊]rk ˿$qҸ9'D2OqdA[#(v2>uX Cd.r鮵L(S?l2}SVcK\͂ B ys.\ͼH)Pq`'n'|Zߙ_@ߦfιLZ"3ijxq+Ò=ҷ ~X_`g endstream endobj 210 0 obj 760 endobj 212 0 obj <> stream xVKk0W/0 Bi)IKs<$Y7NSȊ4f曇d+0ƚҴEkW,[KrGb#ٌ*`7*ĿϏGov̑Trh~0~SqC7>cGw Ў% #B :B-+aZv"nPkp/ ^݅svt0xgi DFS+<;p7`= 䯓yU 6dŰI^\z(.STE4^檎0ʊ iN܌JuҹdV i ej&@얢ѴUCye( ~)=8.jH'ڬ4oJ YٽdTB\@z_ -:M)0xs3x̦d)HbT*%NwNa\,]`> DρhŭYUb#5*ijcԚS@&|-wRB/@^HbEG\l%urd΅^{]VB ' K^y^2ZCpJͨ61_+I5o5Đ*-B }Dn-]~so.$K.APrg(Vv,[R' 1JReRiˇcDH&]V5k7Ҫ>^ ^s?Z\~uZ`PR /JG endstream endobj 213 0 obj 794 endobj 215 0 obj <> stream xVKo@ +8GZ2OfҲliڦUj.3 в13_VAe0pOwJplKj?~"VOz.nMbRKuӟ1UjkW_zrPX݅Ͻ2uWp0 GՂH{x#hlϬ{eIvg@ g.@< q?/׏r6:f@z |H)=yD ,6FF9R:Ž'\`ȈR]H_w*Q`Jp9Ę3jtt9pdõp@Bf|cRL:qA@PFD qM{,s($vIhMC{F n|r0\MCKt'"$-PN Nngn_o&1h ݭQw4{FĜ )t6qѓ,Ap?WhĒc-wycB.f9MVeа X)1Ė\ga%G9}E[Sf{8.j;2,R|#noWjUPX䛖}EUSvh nlsi )->){]껩ui.Fx=T endstream endobj 216 0 obj 869 endobj 218 0 obj <> stream xWKo6W輀!)J c0CӶilѽw|IX$r8o2 ;?֐ێoGb-?U[iߗ_\sy~V q(,uM$3ϓ%D5SI 鎐Jxd}~PrUVQ)LD=ɺ27luz^x4ozR3i ] }8K 3h3NxD\_2UpiKmQRRqf)pzD }cMX6>ydj_BKQXhL<.O,HT(1Y]ILR 8ȴy+\Ѯ?8 ڃmxErGu*;IEU7~GGpHڀa:IS" b憧fwk#ȹ3< 66W'`Zbzm +30 x_-i|urN(#U/&>6U,`{?m?6L[PŅ:c}]nMKU^o:W܋ +>}1 ܵh 5cRcK:]bRŽc+1+g⹒gUoN95ߕӄעre税WKUUcUa_5 !Z^XK %{*Jhy?)f%g]XYfU;+5#W@FYɴ9v7KJc쭋Ue$Fk [h 99>z]1لcpouvUo7I슡""8nqܢaplV׳Dmz!5?74k endstream endobj 219 0 obj 1173 endobj 221 0 obj <> stream xYK6 ϯ9LDRd0Pf[ͥ|Hgvf7`.ZE~|?֎_#ʳMa>~%/o_{_{ìmڈOnyg="Puz{w_ZᴺU}T~mvBкFЭuZH:])e[YQWku+L4СɄAILﻵoSznS)8g q6% ^u QƼ^'L-h*:ހGgRG[,t&JQ88 sEw0o씱jÌ#Dž TPV9!5 Màdkج{})2K]_fpِ(l%)t |,@ \B[.ĮyC3D{(k-[!pJdɘPIzX-<`K~Yd8Pgx6~Kl!J1@oLȎ[#IY@:QX9(9Tg pʾ\ ny擛հDe954#.N&@D$ӌXiL\PC/"Iǐ̦h TD.KSX:z}lMBEaN5A<9$ugGvǧ4֢g.(`IX_ Q̪rRJRÐIP^jW !VHXlc_( jѲ>>x ^pmun4qhͫ¥3Zi@)|ʚҝHPۡD-ž_j\X JnPf0}Dr͢!5ܴ<Ͼ4.?*Yg&|U,|)n2^O"HbDnxcd[$ 7M>\P9j. FWF8 .s.P_Cghy_DAM + endstream endobj 222 0 obj 1229 endobj 224 0 obj <> stream xVM0We[CmZʦ%{Ȓ,'٤HhFzFoۜgC'5۷ۏFY~Wa^KGstyxl0BMƚMGE~5۩P|v풿 6Z6m^tdpܢI|Nf  imD74ܒF6/7_O Sk]N7` spbeoA&N0C^́"1ǁi(1Z'Ge\kV J u >o '4N +.)u+L)W P_bgRtqB#á YgUׇ=d Hvm :rjD.ShW ;,xYE$yu ƌuRFG9ʥ xjDz|b` 6W3elIA+\wQ(İm%1X L:a(+ s=,RqtXRE:Z)R\VO^g !ٗEl^iWdrP➝Vm9U#~-ܣH6}㥴+gp1ʔ endstream endobj 225 0 obj 643 endobj 226 0 obj <> stream x |xiyyUzyZZ/b5 B +"Aۏom}z}iǏUTV%@A$r%6Wȅ}g;3;{>f왳?3gx~ ZB>"g~"ЈǼdLJ"_#W= p+KZYIN270[,;cmMPW6CiuG UZR(2OUg~aJK[@/9kܿUrsٍo#@nC^Ss%3^YZag nxxިs*;N?+र'p8>rYw OŎ_io/n~.7K]ˇ]Ow=5ϵzَ4.nָ:z{YH_ N<2N/O~ᩓO35_e:ŧ3Q-&(f+'ng;:z+,c{kqN'8*K WNLLv!gϯj^W~1\+fؗN/z`n{?*z0xxNzfGiՎ앹ҪŕێUm)|v Dž0|ϰ<,ܹޖjy#{Ycs+fڗ6>rW;lb{wYckg^_6{rRWo`<'J(}o gZ+<{-QނohuS34'?r廟9x0q$y) ~rxsJ$i&qVt!eZV- T(y^?53oP9bFшZȬx9ьm'ٖ`Ԩ\ ᤲSxzzׂ)=xj͆1uӿs|w>7Tj|z0!_eNxn>ݑs:鯭>—|Z[>uO1}yvY̋xwgtyU,Nj9Կb}u*!g 8f](υn:+fs=>׹*ϑ/h Ly߳ϼ>W4L+?"[&:/#7JO?0b,<&SsůĿyflG<һ 96|Byy̜pco͜&g-ӊr6RoN_rIf.^w1Ϳby:gyk}4V A0YEuE}?}U]?\ycyn@9jW:]Ƚ6Ҋ. '1_U+J5׎ Q4ba/vL%“`~z OxHne<+a]ʊܿwٜX`,邓>w`~Ɵf;F$wȟ|\uߕp^8?裱|R=Cu5_#9ec㷇3x~S3Hoi`OÞ|1쁦9F,p<򑦑KG̝1r}K%gp؅2/ȼҗUG_-!J~gOaykQ}sp93íb}[XuVJPmu 0lECpZHub[R7eL}cm8ob&lwqtMӿFsWƉ?$Sx>3+<g^G#ʡg)g`l d~^xuaIubr_>u驩ל~XYcf}]ìkgi~}9ڳxۨU.t9o9G:F̃xf9y/}Oi`[MW=VW;//?b҅\qo_ǮZ[W\Bʛ輕>r@?&C- `n`\akv,~k6\/0]t>[ڑ>+/~٣:%$6Ի%s/@yyN"ǿG~F.~M=0"[I<(BjrDRxh,9=xҾ%̟=Hý?>з@]5vh,S[}p?--vo:un᫬ ӣ}59\cCdJ9Z3tje\xnJ鵯q9 n%;ǩƪ6i >| KN =)r,k_)s:8ac$'s_>yO`Ahd8 }}]==vgg"$rN9kkKsJCqNre]%_)]%gdj.'C;d8ω9[[ ++zӉx˼ a@1G9R ^1`νE;d8[Zr˜[ĸ]vyr&8k䬫58/H(, LaRT뙧P cπ,_nDYHWw/Ć/y6 C)<'V7)(ڕ5^svʱNRu J!JJb rxy+(yOLTD-LoϷSNp_'J'ь?!Ȱd99s8ySS9<с?Ts"\YF%m>47-s>gCϯxλ? -? 󼳳GS2 rwZ{B?- o=6@xx>Kyp\!"S_bƫDo:O0@49M'ݥsHrl`H:!RϻN8p΃߿i߾6xҍ'mg}~s.vtS˜`^epNv^;Mtv0SxAMjn:{پ3gCC pyJyxʦ}'x CŚihy,nCo.[CAŔ0s2E9wds8!p C)[9+BŔyNg{#xJ< 5W|NO{9Y( xJKds(F!b;u׽cnѓ62C 8G-4v!a>.2x͓׮uK-Xe!x]u923 t?4%e4sXhRyә첀09Uqߢj`G+n_J y~艁u? ܫߑZ:qjZy|PՊ < ey\=$O5<<׾σWL?;5a"F CɄ.[+_UW'ZCn~"(F痃dT9#ds(F!bPCŲsXyh7q9x&Jl!ȸ4xA)/.X9#ds(F!bPCϡ < X!(7SI}Q_cU{G=쿉sXqy)H&F(]NtwT 9/bp\: w d9Yσ w6qD{A_N)~V5L5%3xA<7OܶӖsHrMw^,WJ\Ԇ!ϝ$clB`[ (<;Om&t= M*'ph5ν9Y乑y I TW7s#k>xz';⳥BCŊJQds(F!bPCϡ < C1xA C5ߌhqRc shNT?>Pwʉ!fa SN-67(롡6Zo࠵tvJ,xA ߢ̿wbynT3^Qsސ3FSy57zg Y!(9!bPCϡ).!Ȩs(F!bPCϡ < C1xA+<טP59!蚳TNmǃ湍fDYj 7WNt!bœF}[W{>X= ;m7R/h,^T<dEEUȼ9/m@9~ Ufh]sX )\4|ϨHIXSFXyճ?/D$,V\sʾ-Gc[=jynu6!B5T14RxAx</7`\/9;s輊痃dT2kڜH's75ՁPT!bq'[[tePs C < /r;9sX 'Do>FQMr>qfz(sX睝Dw8jdR)L0xܔt<"xAxN3WWa\(BEjs2.<'9oڷ o%%==P9%"9pc0l#9}ځaN40 éaa85 0 Nx~( 0haS9 pjW~'?ߙ2./݇x0^:(%OΘ5­rXJFi^YqtOwsj"oK:gũsEy"zտo (L #N-T:ع)" [tS˭h͹PѓWks5MxEGw?z S\7?u~ E {;)ZěVwě2֌ot\3Mhk:oZ ?o8 Ϝ;s75ikmvM^7xܽqn;MyoopOOv5[[Jv+)p 7*v%5ߨ^[E7Qǫ#K6),lAVkYtSϷ==OGUo-SmfIx3T c0^Ɓ1RyƽMՖ@xuĨdS㕏w:LؑbO%d|Bze/gDZ^;a7rwwwt75;Mg,(!vJ߲!+%=O=CU@8n.[(<_vImj rK1V򼻸P/AFlyhA617^~+Gy&gZJcv+׀OsWW{{oz-{5[fuS1WWJI+)D¶@* oꩼ Xb ƫ74]vב*N\W -zo16WReQPBy]9RLs9;7 +,+};;Z.{m錗*O!ɰuY&oSv(}%[NQ6zCilW{ U;D8,i;t&śpB֋ǥt8 Y/x%%%䎎a 7񲠄[[oE$JeoQa[Cգ^% t@=!Qg޲\`T*> stream xVMk0W#d#wz(=ݖ$·,ɛu%2{YwFnVi{GWFlz~ nĐjFO{Pߛ .-fqݣS_ꈯ Ϊu{SM4G)*;pVxG$B 'xQ_$*^X'ؿz6`d/x0"$-[丈y[pT<]YJ%t QPi#+1J+ڪzLHq;65uT^ endstream endobj 230 0 obj 755 endobj 231 0 obj <> stream x xE;(*!"A$ `ܑ@ RQY*.P@ h\$!0} ߮#kgH}O?U=鷪ϴi31Č3ƼQlfN󎒙+Ԛceʷ.PYt/"r[s+ɅL`d`7>&y3FZ}R3 W nQiuֲO -Ym(睨7w+zd$/!W k[R 83R4dY풢!KF68DM$__7I D :3q,Kfخvx3Smr@̢<".nY5ͬ[:,@B-nĤ2(%G$?w$?"M@__ͼ2fn1Y |ȼ|^rq !|B@S3X9,,Odޑib,5M\# "UφD+Pr^)є4g*#C+H <Wʲ4 ?<%gh^k$.8⻴u)ʘUqOX=K[TQs O樺t.q! hU r~ Vp噺p:-%gʬu˽|-sHP,'PWQSzM=zk1 7?^ f,KcLCX4Ȳa%JE JDڥ&!o`僂[ -7fotaǾ5sUkVP˜f1=M$ϮY~!+c-]ۄAwshd4ok<Ú;Ky㔎<&dfEb:%0Qz Xnt u#Sû~l^Ҭ OuUVX6Y01]״Cc=Xe)M`+)/,F3Ko٭_뷘Q\5j r;P^06)w/7t=2PP7T4/P}4GrrTR/xlu{3ciLwX ,?NY̐eC^)K I MI%* C`3&\Tb𭤠#1C1׭uMՠZ {d,+Mݶ&wrEU Y1Ct#s̹.3g%;(2(vҀcßzz 2)˒E:h4.b}ziDLNq?Sò5>U|sSd<{Y,+xd7@9w>5!<}˲c=X~Y*C᲏Nª8+Ա}w@,G^{FAXsDE]FF0l@FY16?S}8G3]3g;gL]IbgTэ%,MŲ lk{?ܱ󊴛 ^3gYXMW6v@.C!񽃭Sy3)XVf* zzG /{N|)nKxՐDʲĦaK\X#6}E,{u>),s^Jgodֱɴ s(#ǒDCw?\kL4t29eRE%x Jh(@U3pSV,"_/^GyVw8oh>\aV布\p9sSvO.,9|9DD6 #+g |%F+cKWP \{\?Q[F-tPCKT+|T+g(('# g|J r'HĐX'r檾D.Q,/mn7ķ528`ur'=˰cPCE:M{d378Lr?ˤHr1a }\f3G`[`޹*׻L'MF7c<2Bb~SYS%V* х ,3uS,+MQz eU9eE=T(=fzΧjT<0Y~ۖTQj4d9 d.AA+wN/8AgjGe/}2NM{SGUõm!ϐKZZC#2wqGjCG (uz_r5G݃J4SްyM4B9E%K=:K7B|J5sNo>~G^tʲ d4-t}Vr5i7CW͂*fcM 4UVsj:LV|~,_3q䐜]6:*dx!&~>0@0?%8P{DSj݅͘( ]#ʌΥ>I ڻ~.}뙾o~xh"'9/ Yp{SSaEp|,,OP@Σut::}qX_艡Ό  1ߗi'.;_/ȸ̱ CXw"rYCܽ'Geo.{w8}:j8MiY?qn᪴WkM"Pj=7kY+g!DsQ,pVS5_^xYrL>ۯ۟|dyX61BYĸ6v:N6e;[a7Oi nAVE˱_hH=j61dy] "T?h5;`x@8CB`K~ž!0SN{_`&V&>f7lՑʼn੧;\\~LX=yR]qA4Y9n4Ӧ:f|#ss\9N9o U9@;kjwh`SVutOxD Xv,1OG ,C0vk-nr~2/ &P#[ l 0{ć$Fv;ɯȼ_My-1ucڴ2.ܽ̑Sz9ғz5qi[*a,W[RK&aCX6dK:6peY8")b3Qn='jL=o3Ŀ9aTxx8~C\:;2}&%Y&3Ls*]W>M_͸VkalmA w'Du{GWǎ?qxPǣ;=zz#A/;T_mbp{eYzXNX&KKyA \_bn=|?5Ə,_ mڅ[.)ReKy~JaT 6صXP+CV%:ͪfT|ʙ(X.eJ%YUL,% ^8bQ%4e'n6 5-v4J"t#CO jL ?'K) Y(H,'%a ,RQ%4-]egh"PB|сV%R(둍Ɍ%pqlkɝf%RQVS20ߖm(TeYJ,SOe)a2لeDY(TeYJ,SOe)1Le&Q42DY,߇PQ;&ZeK_j-X~,Q"VjĪ?b+B+AnV\(=e24GS Y'XajOEX.+cSSYdVr3򦌌9H-r}Krr2;v,kgǟ[7[F\O?,5|&.|O3sϣ+f˔eIC+| '~/DւB"kJ,La)ep>#{2ٿp<&;,7IcPS H/ݬL/_as8Zrc('\8n۽YNgq9Hp>Z ,YC.mUso..fֈ3U~h47, ;P_24~rzb`Kt%tfBpnx>OYcOc ,&WӧVJHի/!G[eY?^^nqZ~S08̙x}n u&bYoWC,kll_ A`K=u$:8 ucYv<ͲPl*v}{v XDŽxx>O};w_~>0'Y+>eRp__mۿ?f,Q, 䡸ԉ=t1JXfh|&5gI엃QHȗbk.,>"z,[F ';f kc-db:,T|_=960<_h_}!7oe{)^yK6cΚ_5oF0:, ލ/M;:BXpu-Mă+_>Wڰ~Q5Nݨ>^6Lȷ'Zt?3:, XQsiv`HX XF%MRL&c\Z~9+T@ƾ |2bsZ\`>4~E]xXԙ['"kNܨ>ވN1DKò?6k<.XPza22FX~1зn:iAA3 f-gږ6~Yj'Zo1,hECϷkW~$y,?$rŽE˓']11|xHkooАY /S%_o\_w U6|Zթ?W_hڳ$4`>oe~oX8ӄ1x.rf&ۻ7FDZqp䮮nj->ɆſKJz4FU|KS(> zaXMm>R ݵh^6?_ ;yWnƑg_(@bX`rv6/O R+*e38ȸLzNFeE\+gˈi @N{ͣ;)/w12y" Dރe~0m} _s fYZ(Re|,Ki}&EպE(TeYJ,SOe)'|0EͨQ' OiW0]oFY2ZIJ@1e!e* 28qӡ",RFY2̲A%f94'߳œذNNlddÅلi[PBR'}9ퟰuxUykd=|-/LͰ=25mYe+o[n:0YomB Z* y[|%9% y歑j R,죏$bƌi~?o,t~lT<].OS*3Yg~5Ajb#A7:z'=\R!{Dܰ4yk[a,7bΝqiTұFuznxP_Ml|VIwv:o!޲E+ Z၁ICR[6?viH<[9,dH= WWWC!8q{z!Y]w&$ dL7!AR?d,@I7/ɛ8v }5ٳqvP.Tұkg/&pTT &ܹ<,}>ӦCvt`NN,O }fc8[8дˋ/xӷ \5CmԸ6D=!OԨ#f>Xjj/H}BºmāڗI@keb!Zq7D?1e8? ZPI Ś 6e0)r +WjAO>ulVA?}dhZkϵ%yr]]N}vFxCRWW77=>DE4'K̛YY&fhi/{x 5«5566짟!Ї}uwdMG0b'N`j=5>6#"MhCׯ2={WKHm$@3#-=qj {oВiUoӉ2%iu3ofX}DKra! {)|hձ#jm^ ǟu΄dpb~0&Mѣ!YhiC48@^='}}qH9vR\#mE!Dmوga?ai(o:}S#.I3y}I21}|El1qbp+V*qlaMhfك`{&~j*e8j| NY?:v? >, /n=vZBIY9 6MHHoA? yY_5ebbwR Y&j{dy&Z21B45jFJXj2 HZFY2L2Le>Q42ČqpR%22DYʄEYګYtFB,Kiil,Kie*,Q'.,VLe>Q42DY,r2>Sv3Ԕk(%$~ Jˋ,ԩ5,2TEZڝȌX:EIj,KiePzpTPu;V^ʲfqCLߪ.(W^vr//2eY",ՂFZ]]-Vb(WA,Y/CFj-,+-.φr槶R4k`u(?/[aS^ʲf ,r*i*++.*QaʛmGʋsw82`Tb*q n0\y0;Gf6o5MŗŤ,Ki2T &]f2H 0nDYT,7X(&z2z$\S,Yv^RZa:;D! ߺ} ,uN%%,1[؝nU\w.ブ DW@3%2k`Y&SoA\ _iIxiOZXH1r+.4- $H5peŠR6CLZ^ʲf eVddga픗c%2k`٭ClUZfzWxy)R5ɑrQWP,ɩw槷R-rƽu=]z/.^<Q42DY(TL(TeYJ,SOe)y* p8YNnvX{U3 eY2c: ~ז:'2W7Zj:'2Nd\Ml7jjqP1扽p*KW6aw endstream endobj 232 0 obj 9286 endobj 234 0 obj <> stream xWKo8 W\` [P쩻EtW$$8D?~_j*JE*p-2e)gdEFcl0RhLLn,FvdI9 oRd6O22!Lnn*NPL:\% m~;6c'9)"v:G؎}M($>MI8QmL jG2XpM .(,s®Bq\,r6=C&(U7~q"Ĭ(qE`deDI2r b/Lj|0hk)/5!#CcH]![bq P1{.,2zAH#$񆱅6^&hSeD$#| 9mc( k[Ζd/+*JqY&~S /RJ7AډrEU5oC˶h3ĜHuKf) n&V;0.&Fռ 5`EIu`8G7O bU8|NoW8բ3vnW'OUd'=C7JLGfGc Ab˰%Gg@V!=w7VwXg:>-Ю8܋eD\91<_{?WJeZYp{i.ܽeor, endstream endobj 235 0 obj 1172 endobj 237 0 obj <> stream xXK6y*Il0~LCrhaiɲ$d/^~= tR_U}jwŸ^gg/?ߏ?){;wG1%/'T-E>f_3>NmA|o_G~xfC/[>k2*=tQ Q&=3݀?],MG3* W'9bǎ& V^ ]FMrkKh^׼w/09\e+>ʦX2V*wn|u0a\˧ŎhHW1];VhԎ/Ѣn xI`hld}D첫I>q AbRAt^txRlyLNdh@a:WdC3߿Q`,;i`Ie6~Q =eta,V᣼X$7Pfv,sƚ0kE=!bN$ګ+Ģr"a䊋$1r}HB[$ Uh1M6!O ̣xAMz.hQk ȴpwKķlIc2Q%փ$]Q@h57fT{C,&jG|uHWk=1u6G_Z2Hr&O/)6b"$yu|9"tAӇmO:LHZx˸ũ,& l VN {`W)I~I1>seyn*3 endstream endobj 238 0 obj 1375 endobj 240 0 obj <> stream xXK#7Wy^- LmCr00$%?Уn%L[J.ANu[C,=s}1B =?7n[>wo>j!|z|v_ .eX;]~>ql^pcsl^PTq.dy׽҃كW0lAH9 Y [W= ;6Ԡ®_?͐3 X>x @:, vgΞCˎ=p\xHõQ9 Kh2K@8@ % B4Ӽ#'K-lH뼂;zĄtM -G 19RLl hTJ9MIN.o\A֣#$ĦGpm븡"Dڊc2?04$hU2V}3 =0 f%}^Q=O3u*aW+ \:ڷj!G߳0f! af(x{R*߶*fE 9z*`@|HH'V nنQp-[B"Lzע(>|'q!V_ي޵"u˃ s$""(xYhtK"h%epahPlsBi| !>6c쇓T]_;phqDٍV=YtN>- D wB'W[ɬ3{Y``)v>?crYҢe'z*6EWfe\E8͚؆Ԍ̢kƂ:0wXg[YAS[Î@P@ OsvYRdttT4Ue! |܂%zI'!pȲ[EJI+ PrTA`\ٞٴjW1baxtO{XBY <  EQ-H0hƖ"[ WdJLWk@=]f!0;,ijUsV R s˒:c84eYT0!̥uw%>3FrxJTNCJfz~0q֬6=5L%L endstream endobj 241 0 obj 1220 endobj 243 0 obj <> stream xXM6W`T64q{6BNI&!$d/+$UI첐Ь+R}dVuw_lڸ'N+y?wviIkn׿g8-<cЫknux 9?l>Sx9/n{w1EO׼ X2,vf+! +ݎEѿ mdw@Η mJe/fr5Ʋ )d?PC..*v@3'MBoZKp ">uT"Dׯ*aXEa ( FP!0>`jB2sNHvFNtcw ¨9)/8ϓ' v*D?lF Q15G w STF@EP3rO6|xjY݈bM y\ GuඩťE$DRI L'S3]&@L&sHL!܊"2V툸u@Gw D!~,okEXXm2P?pRHu0f) &(w7JOb<|sĹdMY8y ٻq5Pvr 6N1j; JH/o%N!um6dk~*L,VBbҷЖ:r4%!lM=˲HR 3yaHXeA*IAR˅й[2»>,a9TUy%FϤ(Ȓ]X p*GX<&JL*{Uİ!a^dŀ oTipeGI 2̯ዌkK%!-P/ ob}=Jo; K؞4r:8%_d0I#N4zHJqBʨA$*Iߩnm`(/QinyF[3H˓QK=K9l-m+2;hoyƵ޳~^ha;:a]uytts+scye5$<.;&4*%3yQ7)͛ 9~&$ endstream endobj 244 0 obj 1347 endobj 246 0 obj <> stream xUK0Wf$Ɛ'@mR6-K~!Gg"='̿⯱fei[#|y2 0(\eIRF?R2vzogq~"*V%~^/7?H73o@{oNBUOQ'.Aiw~,_Ag[Y_agײnDB_}g+x|P(+Ǫ$"p"c=! qOq$j]c rGp#-zݤK%Upq{[s^چAJ9!vqnZ%Š5;q/5RM> stream xWKo0 W\ HI~IV N۲ah7Qdӯ= FRMOn ݻpe{fKCz/Y?/-QOWLX%>"_=|i t=r]UuGz<;B5 ߡakg }u)YX"򃬀E٠J9GQmY}>\KYֶh&9  Lq ҉!K&ZIoUYm`6@-+:"A7$؂dljΡ*DZH@Mqg R): #[xNglK:@X]NMtZ(+Z01@p9M/$KX+!: :a /b<OZ|n,vӉ\fK15(CǠx'{䶬6X;5 AbK9X;%Y xMii.oFڲ 4>*kI<(4?sUўӈq^#7zx5SI&K W6J {So iՠ  n⾡='j-&g]wv^q\o:T'K,N1b`*ZV64D2btix4,gQoW+u,ZCh@.^2̠CdGrwgZǩ#0:>ЧAM!FL_q0LeLvAo팪$^Es]' IݬTIQvU/q> stream xYK6W^e1lOCrh!dt%?U%enwgBvh#˥Tݼ1Nh|~5r4|@aI6¿޼|h?E_>Gs〭b_5; \?sL_"ε]0zqrTpA r|niFӌ?ŽrxPH &MfI'$Z>A5R|}MD%Zô:2"npPPy-x@;.!?7z ZcgBpPw0v d6i&!؆̤0J+8O&+'}'5{ps~_Ff^1օdqD\qaT/Z9OVWCT~ނɝmW:p޾ӭ~נa~{tpW8(]㲠w"Ԡr ta!k/;\4T-/u!U!!&w&*PBYOQIIrftSb5|F\@W"a[6R9ԂjFlhPUdC,8Y\rD(gs&Yae"dtEO~2C I5DBd)RףG't@}֜EuЉ$.S>= ME'RQ$*|iBI(zF̓;]Ŧu.lBHV:,^9+Xn]]jz D[)5oz]͉v'Hʋ#0{fS,]~X5(S1K5y#(ls :Ef_R+˟b92ƾWS4;l bfdlZjLZY洨E*T cn0"aiyq6aNa6RNGXdS +}md76Xy /[H 0-td-W`srΊ }πJuuQ2`ˮ}]v㴼>V{8~eG8\Іv{W)VZK8s74uy7nޣ5xF0u0~eVlhG[p&{l0آͯ{m> stream x XUU7pA[wFKk+1bM PQ@D$D U=׭ ؔ6__ӘLqL) ~(rk}>/9poukˊp'}g(sKb+* 5c>+\{B-5Ё"MM7n/_y,145Zh2ʗDp2@Gu|{e;ab X:l}-]O뭮u욶 Q@vZ Jz":^+D\z]f z]4lU2$ɱS'1Cɐ@Đ2ѱ~)2I0ew-\3ɅVse>i&AlAm[E`U*d0`UeS 療>i[Қ|Rڸf2* ,$(.4Zn6dI 3:.lI<ɠ}lDZ (ORV|V1%Y$hUJOL97mn=NSw!d<)TpuFe xD2_TjAS{YFI.4ꑈp@CJXX Md63o@?!j'\]QUnzsakn]6#F*J135KCՋOP>sL~ %^JxmRmӐeb)(i@"̀w!GrLzo=AN;Mײ^Zex>Uy~s~Sn"mV&}:ʄ8=Ic$k<ߙԐ>WtK5n>{/k7>Wƭ/ xPmqJ]k ]^ X-ZH ix)=Mߩ[Lo7vtiD+.oLf/dS?Q4`OK yֆ(I2'.ϭ=巾>ŭUlU <ޙq?O8v{Wkq+;(7.&nlVݮLo3n0R\nC.{vm&=Z>wZ8sE_.RJi -(=*9v䓂)or?4 =PDCn\y yђIY5K$M%-YuBpElhjȇ C?H Qg;U-ЕAoҐ[**0X+]]=K7$ot qA sҐڌkY7&GIGg=V/zP*^p_łOX!!II5x,(tvu]% :)wiDbowcwc)}fԇ<([aamv84&mtzrbdIe56q]ձ+=3Gn yw)2N# 9C_Iez5r`H +ʑf[M ;Ɔm2ZaHu]BI5Ksgu1*c.wGn|Ķ!ɹbh[H YKc6d}:wû7-=٤6;9Cgo5Qwles&>슷*6 ;l-A^q2@?!$u'MoL7Y[͎dRFw;j&wx͂!Y7r1jHʩ%)@d- F!s9OE~̐$7utyf{L-ѓvuCίGBX?O}C9w&('SeJg| v=Y,o3LmA2ebkgA[JįwuG"U&Τv)!'7$%jύ-Ix&~&vMm1{ԐJn$CxSaŦr|RpnF! ?x'h'QG}HVoNbRzh4 9F+'Y\XQ%N(slN$1.2A2e:d7\;ϗeh''f~Hm|F]v6+,ƴP6pIp>]M7yl_2Jrj@ IF!L[=lB}/IhȰ!j}Ԑ0F򏁋m\E80r:f)%w6-`s9^'&P29q)mWg:i~_~NBI(=Iq5ɡQ5kczLL6]jmY?ozG{ߵԐz.lHҙ7ww&sÞ./>튨#qe*M|hȽ:DOᇩ7|lӦQDISg\$ Za={z q21]JeZnlXBeX X*CdB+3o^:193uu+C3Kr/ם ʽ'ȐIrfCF#öy}71|-asKNf\%dtWV(5ZU5ʚoż!نL ›D5+o̺\4a[EEh;ޠQ{ڛ]>}[ e*6LKozQqGV0RwTyE!̤bz*ӈ$۾1-B]KOlX:јt'5W Qk®9wRCnjβ,Yb]K='46M>>Ґ#ޔu#YO2 㒥' J{zt[ trvbt{՞/#'|xu'OIN;麿s'N;rϡ; Uv42ddH>w&[O2\aspk6'x9FqF_㿤}nrzMU9M7CZ2 \WI4 ;k9dŻ{-wvJ5Ob; $rq+k5W.1N E|"}gR3uy|b)15VZ?W$"Y QZs@+x 248үZʅoC~#0,!=!1GߩA00` !-0` !-0` !-0` q!rR2Kh~'{^lvh ds VaeH9.5W08hLr IoB.CKv\2yXl_= v&magٲEKߢ}i))?pl۸ː6ΝލN}L;fкzsA-8l)a~ݩQ7ҭUίBvѶl)r1޳3{|b|-k7dh 9XCzB<ߐ8\r:rr %C,<{q=  !0$؃ wj`!!{`amȃŅF/7J )6ۻ!`j`H !0$`+~:cei-:""CGa߲q۸#c^\_// 3E Q UIغOm!pb қ:s!?%3yy}3lJչiZgC˗;[I*0$@T꠹4TXo$j83ŋ.^l\T_g:0W2$~>2hkknj4 q-0$}C4;ܚ]KHNLBZ(,RW.^i[[SSѤrhH'otfwK C 1ȁF5rfw0z$dۙ pϸrQWА}/8Uy)J\C[1cu%F*!2ҫŻ^o:2d_{zW.!)w:I;DVB,P_3aHz 3t"z) D;\.%u:!X_ &8:*P3R,v|G]/ 3h Ի푑ۋD krǝk8w]Fss]}V9А}Jۋ䓱a𻥯;q}aHC:\GnPVRS^ wsY.]@SSm]F>}UXBemH (qQ}%CN }\Hp."P%n"v7D;"X[khkGd9bXeYX_Z٬#rސ5gqi}yC^6}D4q-Od  6U]ic֤֔;4d+#"2#̆T֗ wlť!pf X44LMYJ60cǚqhP7ե1gÐ fZdj) ꢬ\!+W{y}aH|ȘOߏŕ  gN=~ԫI!pMjk]CG9)Gsx@[+wjm!y}a'Cw[/ ^^_e u-M /^G::ي/]lx{`jH<lC=`HdHfaE5P y޲dw8N*K~#w j:ut7?pOtt?QCްtޑ endstream endobj 256 0 obj 6655 endobj 254 0 obj <> stream x xSUڀoۿPtE\₲(Rl)ee- 㸍Ji_DPMF6&mӝ߹&Mo4%}{rss7$7o3aZGLW.,W̱H$^P`" eELa;Xa.@N ! ryHXfZN@ *}J\%!.c!G~0 s7gAq%Kx/\e"tAz s+t*cK{7c{s g SNR Џ6 Aރo1U*XU\YfxE楚h(岧JK Us"=X=lmqW|G΄F0*k/Qĝ_Sҭ"x-̠a?8Ɂo [h3~^TyjO6|ANuoԽ\qvbus1SK;M"n?Lpk O"oҫ6m,fsٵ ~%a0\¼Xl0+: ^" # Uv9T +jIJSgn2N_Wk__m^}yv|u,U 峑MCúD2LM3Ly&9Ys>d]^Z>ϬOTLT֖2q9n:~"{q}}N B*Hsy axo~K累,ּ8_2[&Vzj4Xb؊9"Ɯ!I"@OѯP+>_r8<I gIgJS9] *'?D+vEhRfn4 ~h8{YZ|nrIbXǠ=?oBhEG"qysJv\ڗ^묈rRK|LρEl~ÕxW$P98+Bmx' ~咉9Uz ?"h+%Fg+5X\_V쑃"gYETӫo6Pҳ0-lM◆}JSOcߏ hDZ1Yդ ˋy(E*MP{ᇙwWŌ/?zԷ)\VX]^/ފXҐ؜EFH"6qpт"`6/[t3%zbڈ>zn~~?6qVt'Eh7-վ<_I;ApwVM*zDE=3MDWtghvאp"a"䫛Es:0՟lpN7wy6ΝЧ,$ysuZ~"Sfw۬w90 A-b_ Az-1]Ëx`xciWĩg;2nw(fY}{#dFs#ڐMx&(BBr"8Klc΍WҬ0;nE4$UFs5?di28~n\t5:,ΰ?-`Iw {iΖӅm*& [fyV8fw[ʄ6Rgs}ւucp_ZH iTEY]7W|ڹY T{*.U[UѷU^8uGE-Њ"cf+a om 3P!Q%X5{fkѾINviEhy:fŌ 'C"UEQHZ eD"Zر.v`h,c3|||QM[ha6Nut&tĻ~ WMǬKRz)͗ ca=Dv ']lJMǼAv Ҩ}V Rs"!\O߫|VN rФĩcfX1N\hgܤA9 O >"&VKPt C[b:qqq /]JK@PB3uIf?;xFn̿'!מ#¡Pa-*t~TX~4lGV.|ia#ߦӁ9s-VS5\ Tfi+NƘ#Wl*cP, ?vaEzE i-'ICuQת&^u&RM|hT;[#7 Z ^Y yV xN7`~ЕTWV`m Inmp̒_Æ9&HyB|аOÖ@!z={ϯ.w:cyv//,uN{dm1[aEnhC V_}+OP.\}ལ|;zȾ?dt8~< 'sm̼L1u#afTG,hEcW3,) O"]F!V8b7ߥ"L@YF~ LD 7;-DZńNJf<*"RJZ[~yHStF/`h |dh9,QW Baƕ0ÿb|@#AD@E "Pt|q.T nAE "bT ." }Yo|vACE /ꃊX100)VTF*C$P" TH\ n[oIQWl^#ƧEE``\a*-L"g`A$d>qTnn\}qykE}8EaSW%ip'K"eյYk e@2$gʈ dKd \ yvMV˳\IV,Yj5MJ7EoF6*HtxWv4E)ҦCϓ Vmq9{)>< \kADZ@Ӟx5A]cMFFy_H)t|_cOp+,CYzH D] >_":QV vt\ R,gN6|w3E jA +biPREcoCO쁓_@P{{~gF~UŠrL\J/d9QwGU)]ήik>ߗY{JGLKFvVTrj{ C+Sk;gL?uu(AI_RmZ4zSVЋ47zMKܓZMLrOaMFFyQDַChB:/ ]6RO4)Ub1&2b"r-\anZ$[ӝ55FAV)J1ILGUK*o "\ŧEE =Ğ2oaLԷ;1 JLTWX)hAISH" S!e\Vi}QH)_ljAPU("p+bhT 4e؋M?6kJeU12^_r[v")c4 `$\ :/% Hk\EE =DQ~vhG?]%/(/,ĕ ,BN֑#PZ:닊@zR]֦}/0ĕ,W쬨Ԅ"p_u?c7^_D!&}II^kM F[McC/l=;65Z,զ/*AP@AP@D^̍RY**O; S}dܒ>;^lZN權"//Al-/}~x^\zeLOvGAw?Q endstream endobj 257 0 obj 7754 endobj 259 0 obj <> stream xWKo8W\.9$E0ر ۴(."%Q 058*O$H:Z}މҵDNJdd;~o;ǁ~<q[p>`/{#.;tQ%rܶ􀻇; )nNz^Uzjv'n>F)X>|C:ň6ŧ;^Ws,-O?K"M2J{%'0 .G7|H>Xڷj (kֺ @=g}૊_D@Rܐ8}Rc@N=#ptFѭ~sBT!sjD[[Qd"4 ^'Z8`Ǡ]3'9W[IK ׻4sU[ ܆́*emep}nM6CyȽQrKPIz}e"IJJPSS}|  g2+)]ocG/XC J6z"[0;کRI$:1CȒm,V~Z'ppY''hPMsu*~OvEW}LgZx$^/;s޷QÊQf]Ag$=׶&:M̂]wJ$`'Y;:˜M( 㢁@OkVnZ o4tmfj vA֋i*E Iͱ Kiln#E *1$Ñ^ַE( TAÞZx#KRfRC羄"$.RtsU+o)I|_ʈ~2.YV=WZ7̕EY_S_OZ'k\uLrf$a_3Mj/Q8Yh Qա5&z$QǸd5:7hhdzsj䳩$dSIfC JͰ,8f `~5]o<ykȽ 0 endstream endobj 260 0 obj 1132 endobj 261 0 obj <> stream x tU<}32 .cpA m"%aQ e1 *.O8hdq#BCHBe%;d!޺U՝N'UNUuv}mnڑsqܬ2J.,-#&nN,W |:e曁QI<@ ?A%*I2Ah*He yК!w)׿}ӛ=-Jx'm*)!̮ࢎsVN;=Rb%%YWQԫ=@_%$|k(,"q3HI0I2fQJ bt9taψKĥN t*UF+RZ:\'(D 6T)Rt'1'\.2[(p„v߃K%%dvA?QuW6t[&CMGwՒqE#~q;u ;riCJ)%eO^Do}ҥԖ̙QWY m;rnCr~@z5o>SQُ7׽J^MrK-/&/4O4oZ3ǘgX>r s&]kڝv_zA{ҳH/~.S?%/_be/xNrk >{{{--t0z 0nuY?rӨK-//9ɻ ߫{~cM++-XnS >f^;߸j!yzQeNӻ.K ĥ9hu)?7@a.í<&7l5>ƭUd}tzkW)aKFޥE?|vկڐly sV67,fX>=P`l霻s~ԥRKoХGK=~roIH)%in.}cE;Vy3q..=Դjf[k~- jZ3ߴrayl**[1>R"Jfy|mإT{H8_jwʕ.}Wj1;:;=K! ;_YTRcrlկ?EzxN\8|RyKfq<֑KL栗Mo;ϣT}̴$$$;&ҿw$?`Kޏ>ya1GcCamzgLa9nDI*wU3n*UQ5u}ԜK%ӷ]Izv*4HcmLK#w)y ->0uns p KY]j~~$QSlI ZE{CcGݒ7/Jp=]WN;͢KuMmHvBti U%jnI>?ulZūGl}AǶgK rGZeR|qJv9ZgbN) 8Hi&fT*ƛΧy̴fq,CʌnTTS{]#sեk:j7u)*)TwJ\꜎eZu^.m/Ҵn$O{Mww qimBdϰdف<#[`_tא#J&|mh s|S0.r6̯rΈ\ ˳n5$d)R5/}?նx15sO7kLI_6YRamY73^c|ƈ)#2J]Y̍-_: _~KWn ;{~ mrq .P{}6.fg'/%/5%^"F~KB]qΰ3E)ˌ#1_579Wx5|ŧ8!:fn\6[ßγ^tUa:%Giц'mZ=TiLXEԔpi㌫WWL`_uurn\)u_J\zm.,qiܥ^\2_J P&o`1_w .oYأn.LZXiz6Z=3DiBVDNNam^w}b3Qoa5YzX?|kƔĢUV.dZ̳G2^Q9*Kufnbԥ<.!ĥ j߷ Xn.Әx%|N63EEe}&.(,nNZ{95׼G±W[.>~ [SX'ga"6oX]0l_IVTc2sȊHòƕqV.дd9zwyC E .%ީRA.',l]wP{J2hȒOPw2 tPu En .z鋰E4Uayl^7lCz{Ҝ}{./v&z%o3FT:&f!C1], dHFMh&nCהϻ2Aò)׌2-әgN*vN}ɉq3oH]:jt~K!Bߺl뗍}>!c |a@r;[mPb+ܒ=ԱOT|oS̻<k^$RzB3]vC:ݼa0]D޽֙R-ͷͭm 3_7x6fko| vi: xuIFϑ+X0eTKKt'"MI,U5j=Zb.5LĽa_"@\z_c__֛W\&t)q3a˪>YMYfM**[c0aojZ\$/<:2Ɩ!zˏ]b{u~~/ g,gۼ#=߰YZl+ ڻ>$"_Y02q!~CcMf;2Z2'RsȰB2n@]:IFD]kR"ŭot \>hU¯8x`_IR!sF%>,s驨OOߖ$U:҄9es(OU9kdeܵWb.0c>\tJ\.e:.]>pE'=*R;:שN<Rz):5vrs}C'\4ړHݲQs;*;[[ r[/~/?ѿ˶!_D"K❷gysΛ|q}Uyo7Rɥ1tQvP2Vt2BS:axu(z),m;M}4F .|=>A[Uٵ{9d˗S/dr(d"0#?MUKNK/DD  ]Q%ӧKsBxr\<%#+M()2RRKaf2F~)ӬGTspIlNiYT]R<\*Ƈb9yD:.u1p)].)$CڞAuRϯ¥..ϥ5tn e'N_D8q*1xwT: JO $gR \6p)@sz x4Ҁ\ K>p). xKʥEX<ԥKwK@=p).K({NsRP\ K@=p)GR@nn@- 6~Roē.v]h\ 6:v*<}9YG۾nsuv%@s|7\ 4. KH˪.4^+R%Z#Dz&[P8}.$*t*M¥@s.4m.Mz)wSizM20w1ucFa+'Tmbo~BZ=)aBwjΑ*T c4Cn=90TK]2kO.5W=Ҟ?/[ \*W%ңFyJ*CiK>51E槧O %,KG'iB'aoX/2Z| jxix+Χo:wHdnquΗPmx 6~y{v٫p) xӥuݦ,DQ?O,\ ;EpӲtzJr:\ ƫ.Mʴ)J$"ƴd2cUȖ%]\$jANj.Lz%ʔ *YWĶ_ "xSFը=QL x3wٓKtMAŋ.I{}'VT"[ւ1> ]f[NkON|P/;jS"-kOx<1p) \ >p)  "OAP|zRP\ K@=p)win8\ .zݥ^?-/|[H?'e|kq0vKKR_\_BR"=..hR \ f$qRk7t\g q {^lݦ^-8i4'>7ZKs\R43:#ԭqH!Ge6NO!RƥtHh.MI̒6 fG:;-φqR_4-?bG([O+ZJ]\8e9vcR VPwNy:?$Mೡ.N>F#o t툖R^b^6b;@[.~R#R %|6\L v~/;)ZJJTKKG~qPB_.u=\{-n7gq!pLˉSqRKݖ47]{9lm/EI-φN۷8ml)r RlpR׊VPwSGD'OKͅ*|6ŵ;m{Xj l*NNrRTUB3*vY .ug^}I[ gZܐ@riWSR \ $6pC KKR_H]z \K%p/$8] \-/|[w)p). p).zRPHRP\ K@=p)GpR7J$nVݓK \3p).(\:(.FIR \ g+K>TZ(.}HBS~0].3X60e2K ?CĒR;Lj–OiV_@Bzh_t;zԫdWR(;} : |:FRڒ R_ {!4r Ӝε47":[WK -ε;45V4x [Z[yƆr'.n\¥x-lԖw91BA {RIS{4~ӖU2.UQ_~<6--.3n!$=*5;Q\';W:m9v4+$\;'|GFsٳ*SSR\UiUHE*\:h4\ [`nJ R +)st GūT__g=%fl|Vv"n/&R stTjF>CMTn.ZED9_d68qi?K6T Vr;ٸ]b iWwݥM}R? [C.30HGvdrydox\㹶3A^_R?p).cfN=\,^'w%ObvSKWn!z#toh%KI.U.ڴ~ o8WEEwQ~%}p}]u/荦 |Zph.V֥-Qdž;4 %jӅjRO ~.ZIkӸ|<>WW[#?/ endstream endobj 262 0 obj 8510 endobj 264 0 obj <> stream xVKo0Wq,EM+J*N@Agd[z@hę73߬lM{e6Xz_w ސco%1%U[yxEXyѵˏ^ lc]{| Lǯͱ@![{p0]h\a,'֍''zʑEʁʁX\vmo("كxm>CŒcdLYL( +p?bu7p|rSZI VK&KM}}4ފPz0@Ȯp9G?Txe> z*> " Pj)c;nsIOf0  bکA^yZɯ*RN=F %S)DT(sQ[= endstream endobj 265 0 obj 978 endobj 267 0 obj <> stream xXKFW`oW?eِ 9,9%%=$a4RuWWU_Q^[,^c |ugZvx~Zw6^?YJwol^x`4poQp}w^QCu{rTz0G+ {}T'$ OQ$$a+4ܰ7\hGi} jQÏB1*}p)FDav hDc+6:Ra [R­ GFLAV N\.: ;k/ɣ5+^:Yb+础ć2vX.9;d-W{'#|l,T+d1[⿾T݌ mB0RTEn)+||:|憈 A6S^as%jx~[px yR<5?׾Fu쳀3\ q"{`Q)'n9SG/ose ,5G`?Pl3f2SQr3\Q)*=}!ŵX XcBp D.Recf:t>Өk(YvIsEFG6ڽ;*M"N7*SiJ[ Xfs2U8%wCm C6QrWZYC t9 ~+xj\2yƱ)nG^Q:>Ŷ]8S5*AgVwk.x:wp E4mXEź"t7e0xXXxzeF|,P0ؔ!PT=Y|cx,@!^+^pe,vyș6*Ȋ(V@|s2odpf6o>;I֠2@]+,xBҒGNN1u3l cOCp>^9X(SI5c S]TR˭U.,_gFF5](NwY*~(ѵ^6ӕ¹/P-C_ZMl%Y,(B8"VjXhzja"[Q/ KY"4@K(uw(mUS[nF[|vh}im:$׳Sp) gYFpdrƑXYUOxUc M FKPPH|SN ^lO8Ȫ8~j˟=o/cOA]>r /'VjgfhouP5cTx.r0g֘K"Y-o3<>t*um4M endstream endobj 268 0 obj 1418 endobj 270 0 obj <> stream xVK@ WH3BSm)-K~5x,BxYFO4cxzϷ击ge}+lmH﵈ }+)~Kx#"Zܿ{ue|KygW[Gx.8]~Gzzl Ak>ak|YB,q,y/o@a "lT%geۜ[)GU۲t~۪fM x(.R=@$ATNd>")mZqbLF Dmo.4_34"o&pj | l֌a&1Ť_khTN^mNT^y,}iV^ay譥1v@µv* endstream endobj 271 0 obj 834 endobj 273 0 obj <> stream xɎ0y-H-M%T 1g&@ۖ`wƚeB{|)C%ۂ#4dų}6+"`Emŭ7暶c-`yv7ǛB{TtZoD)UyXl]@ck`c2BQ ^ -6 1*}<aYcG1딖%=xHet#vL d.(mV1"bT#ksg| `!߆PUKxF>'dB+A4W2Ξ?b0&> ֓G#{~ȈBLS՗DT Q gDZ-ډ^;X1*w4^F%R 0o5HUTEH>@ꩁ Y1ʐ1t(T8ei5i6&\Nj mfW `m75R-ļP$. cF5[:9)KeWUGaH9!&-ISA4K?VQ=E ȴjd]xJyt(qp0o\綧~Sk_$ #a`1@r/[Ow #vŖrOk''BJ 9[h{y}FVD'vZړs1uGXQno=;kjVi]rt1_Rd;հ;:\.Sr/ݨT_QJvPKts^x_>oj@Zi2BQ+=g\u\sF?k`ظTxe)# V DފފEt-۴ʉ%#7O{^s9'pr]/4 w.Ӻ?CG3cG` endstream endobj 274 0 obj 968 endobj 276 0 obj <> stream x tTEoס5O (0 Fp AbB0!$A0C3 w<,FDcHB;=+w}u.u{KBwI?ͽuVWWWw)L&1aY^3A̴KH3 9ERfOLYg"^*ܡjj2ҟEazvCn.fj`Qr hsFwM}hi6eS8e>7B ӖPI ^]H0Y0xrr H  xd(L=Ac[,&AH4`M'W :D1Q^ fE賦 a^YgmN]{ Zu_6WRw|ObO|W\#Y~.].%ӢJ )@)9j&8 ,yb(EÄ*6BR^DZZv鐢 A /afPq登A0=QŜ$*8T'JH\QAFtخU'l7j+mE,}lr` @w~mCs <2c@<ш%)rtFU6}[iI7Fµ k^ ,R/ })j*|TɔaQO$lQB0}xXA`@@2 a `aZk_݊חoI.s 6ׯK&N==acG$&h70{ |7^ee˷m*o^uCbf]k+&=P(qb${Bxʄ`x0]?m+>XWj $^[lH[3[4Sbpq93F{N0VgV^d}Ef}QLޥe!{:]&*z)}jH$*_[9rWѭ&ig$9 O<<#mѫCvׅ@TKkbmT0xkBkk=#0롲ѪGsGx^%A!RC?/Df,(x`ƀ-`Wf?Զ騮3Fsu;w!+S@ `Ye#T| .Jt<m\DI*ObtETTuFj>ع>oͪZt E=ȝ"2p2*:\g[%D ^"L/P:}D>Iī!rЭҮe&! gQ~ #g84n"FC< ѝPrj_|lS7Wcpկ2 U~ ޯd1Y1c^˖)AbqZŲ}E(Ո~Zȩq f!lh&o.t.OT`e9t#~<' 2W bO9--B>،rpG嫬>j)efI+#֙!%{Pe?E׊"+3VR9s /GtC*?GgͺW~zSɁa9L` !Z࡝aLo/6}̦6'rv'7nƉwr=K@8=GnHy&VI*%cY>mJ5/k?;]Z&[9#ّh \`OTNfLNƖb5<BNv,/:oiQ\~dF_aJi^1Y)!YS8E*@P 唡e,{^bC)AeO>&"1.=xH<C< m`{'?0Ig;ƽ0;:+ ļs])FCd$aѶAh.,aGAd}L !_ou:zQ)PCfWXG*{h}tm99 F-]Vs|'J-_Ȧ,[oԯ\6J;`(^&6P$P}V;Nx~ᣆ9`` >4ҋ\ ! `jYmNߵSp9A8]|,Y٘JP̕Od q_QWg?~R*_5׿teѣ5Q#4ô3ֆߦ4@5̾z ) Cb%H8KMx ^i'[?*ڪά2ʜt"dbŴfO{9SO'^-O1^G3Lf7 CE`/jN:c؂-A7ܡ~S Q_>r1czc;yL67mϑ>8Q!ɌaL&X=IxY+ =ʰ|nrЭL\nGyM*v')9i|!w6z* @U HB~D erzbSŃW*TVL@13kf_)T*(TnnA8´$-FEb6Pν+eiFo\A7Ŧ0ȋG=&0q\oJ7t^j0"9/Q#gХB 'rOr<|GY'fI~mkۇ/9 b;#RBFl}k1,hL@qrLOꂇ}*` U9>_Pz圪 TRpR yGMs&(DWFlf&G޸qAMA+!ͽ'nR_ ި@]O7&7h6M3_(>y ߷Ao(6 0eǪb D;;;@^|vS Ϡ$0fwP kiC.<(J w{Q/;hЀתˡUCKKsKK${[S|W$/< ~U>|^-2A|Q? Ҡ+ e6y󀻦ι*"C;BBv ?$C?ԙ xzk@CSykl:Qv3!/qGְH4^a y b@A/;hz}to}{kqRQHG'q^tР:|IdfQ 0Q4h@VTj@NSmDaQHvbyRWW<%3A;򈔼!![g]Uގ &9 <8hЀz<(kkALlXy0؛58zc}A-XG 3ܢ@}5Cg֤66T⡛Qow=?ݲ:^P`ŠWʛ_TT)kMUM5-MZ{QmmMy&/O҈R4h֖ښp3^rИ.> stream x xE;ɛ0ot=XT"x(1!B8C@bD0C>V]waU}|F¥"+LE95Oz23̐I뮩㗪Ꞧ1E I0"q3%ş LeY:p+26b 0(3l=|PAp@^jE&9%`e>{U{T|*ÆDß[[@:ZjvLMb:~*Q=2Rmh|x82a@<' |7 {͵;V_v»+;W-`VySǬ2qyAnKL@)oBV}ݴ/?.۴J?>%kޙKINz=d)>w/Z0OkZZ" ,2S03I1~IPoKxvts0>p3!S],0rg[?4~°.AbݻڷfkfhCS rgNxxJ& yxfb`73"TELϲ )j J6iz٦e.A1+gkGCU'(U5?/-5cB> C2UFD lEߔz}X]S$^[+,[v!I}˛3pe7]6A! # T ƃ 3K?]4v>|5 %{DBL|.ep/o|ln;* *6A{dn#Ԓ8A;V h VNY/Zdxi؈BGA·'2DC5 @a 烕|]ٳ|PVDeM2(_fW!DnM&3y6q ,2>VHi'>q/fYއf[CW O莨Zr2l۪Kx1tws5+:X|vLJ*:Ta+:.Ⱥ$D%B珃t|X9h"KAGS }䵡9S}?"FE>c/pHcZ3+")l;QjV Eǔr|O؉cﷲ(),I2h%S>eIa|Ր`|>߸.>|GR2v܇;2#E08/D>D >[b%O/w9uf_. v֬Ñ8k^6kaʄ15)#ކ z::g("<Yʼ겭|3WU~JU*kO^,&h"4 b:n]TS}0)L! ``1=2om99C~le?>W[';(2|fNn}&Oy\߽m NDd`&=~4v6:RgT C&J,ƇSlYOX\qH th`Ͳia_Ǝ-\_M4 ߥ<}'!@Z|M ͖B[{μ~<71Z21M<-IHއͼd16r ˟)ٹ`|&>K#tBkк HNk Ji]0lBƪ)<ZJQ4Y/^2Y!Mb?ݢ O[{M]W|RF>@c%^|8~S'Ǒ|l=pHhn):֌D f-IڼWpOgp)D dcvCWN>væЧyoL(#}g|4r*b*lzT/+yYf aj R|!{Y+~JH3}&9HG|-}Kc\zEŤI˘蕉C2'<3ၜWwKw_WFz|?npIC%Xɍd9o+lNqk*r3 ]F{ߍ绑ԀKy׏ܙl TxexN@X# UZhk3٠27 \1961&6zߦ `Mp8; WЃ5~ LG^/Q,_3;f_2E B"p>C_S(DAO}F>8Ն2Ph CG||MHs>JI-|x@}>lP*h8D_+ltb+% N!a~۽Q@G‽> !`Ҁq/9Շ?+P7.Cdm.F)Ye6ɂ/H.-'`tnHp).bv>(> m҃'6rBdJ.߶h`ʇ.[eN۝f;"ӰoYhõ])IKI`Uv rRfQ^Y:ჳqmWJ3>t34t-tćn'(tC_ ~GQ#e>Pz@!=@D>j߷;\lMJ6o.iOI&tJ_^oRH($|>PHz. f=!}^{ASzB^٬ ?]=v:C;V[A-#PbY]2^v!\^ z&ט`dWn؄yi9iY  fꃻ&Ȑ~4i~&>r\z :;tMSvI>>qe$Z0\g -O!ˠ,onII[\!xgW p=@>l 6o ƽ7a_Hh+=^ Qzb6}xے̢W.AcNgG.1X ד_d09>GqB_|m)3}AJ aĐc7!v/%jr'a=:a@j SE拥Z1̦KE}Zt: dH=Wn52j9p8tl'1z䷞8DmR#99EHOִ\ AO٭?*ZR.6:WgzX rכ6f58'ma;>XN'6G BsJ ]k ȱ>Bz09d@}pu^|)Bq:ݓL8r|)7zk^l:~ݑ9@?d8pᇴj35eLW?_#z4#ȐDuuԇJ7q'(=B,@!|G}P(b.Ufɇ&}pT\!BE BYU ;3=pN鍋P/]/\'|L4%k~;4j3.=|Se}Ɖ|)ܩ)AnK_o| oUh;yԫP닞0 hiizUv{|Qhrj* P>^߮X44՘* > >ty9>,5n@]]hԕ:ᶫoޖ-y=P_N|̫%o[OeHlc1OZWg2U>tlaiM6-YBn d6_vSX%n‘n*1UUzء]/x=dBH]_~{=n!g1 GitTVVG>tY[l;wk ?^߲uܚE%A-`}Hnԗx>>9KF`(P"Hw.1Zȁ]/ L!6xA퐹%l(%M}C%PSS!96"xa2[ɨ.(7ХڨoY'm!m(q\߮ ,)Ll"&{cPlY]%IL&SYyZַ]pHqmVXHT,>0&Sdo`7fsВL S@.2cs #I2ZtC*,KwSZjW**Z-7HZ_i¶SH8D!IeFFYЇח q_&. /lMA2VHZ߮ TVN};as6peЕJ)EweFѨ TAz6*Jx};ƥυҢ%7r`/*̼v5ȉP׷>Kqb7Rs3z;9gsN9z$8qc§j}\O5n<Ჿ_m׷冂in9U75V76*ov{C} :Ž>PH~([5l @|x-, {"!K&vw[H2[^V F|oQoD>ܳ}R(,L endstream endobj 278 0 obj 6633 endobj 280 0 obj <> stream xZKo697)`[==M"w|iEiN0%pg|ǦZCOu?ڨ>ѮG󆮏dC cd-ޟ7o8wgilukm=|jaBn߆D󮇽)Q~S1|Ger{)[voX)h ]Q" y^ קPW )5Ćܶ'HYm"Q]&SP!L&'12ܧ+>'*mQ] 2RFِb*mMK[$51U e]Eay IџM+#vvI % p.ȱpH>Yx:gh}rc_pgH)S >*gu=')f__[i2:}cyjy+P@*}ǫ`EpkW%h1QL ׶2UV3_FV,>Aőt3UirΈBG|2VT=v\pBځHЬ(x9g.wCj_%m" wk A8Jaɘ c%%9ojlLl2h7\K9U:\r|h!>T% xy  O Rb0O1KFHfrWg!J8JYѼ~c%Yck"-k]y |-G;E(eA/,:sPď90$:`J%J@Wx kc6Uf쾻}^o]ʓ<g}*Qb y#Bε9N2#(\Gs{ns=® *عzM6(Uz)([i6pOpb1@)?po> stream x |E; D킂"G0!@82 T@݇~Xu]aYܸ>~ZAPVuL/0/0 f\D** "'(,W+j!JA~At9M8Q0Q*&4h Ĭ6B[Bt! !, [!k{1XAXqicv/kY!hQ ykC.dQ-fj,5D3ڎCYe7JjEP5Z4kgOnU?1L7L=}XѤa_?40p{íyLd!Iࡓ?C{=l Jn^q` |~:+; .z]xhn Px2[ 6/_"7T,MF`3?k+/ hMnB=.zz<K$$D E+- Y 7nLSCÄJexCC5|aud,"b0A?"N2(p:=|a[3 l.jPBria Wwac^a[@3<F.x5I;țɡPJ<YHd[y򷓥. w`U||:KǣRn4m:%R"Vjnuʧ cC4?]x20DQ ضiaEQسv(d<ı3=i-&an8A)Hs}r\G}\x $؁gB3;uhς7A&6ghUPLnD$ʰМ88+z565B27 }Ez; vl cg ؁-ԾMU ą~r)/båCRkÒ.T33/*3J*)))(TCds*,))RoԜ!,FyFid噱^,f1%@Եybwqg^z]}:Wݔ|CԒ[2Yx l6dfɌ,Ҝa`D'xx]o Ax$ؠ}Ƕr;3dC$0"/~k}W:M./ n(8<ÃF6"]= <ߐ72z"|;ڕ vᓓzyfF}VK '=o$8$$߻}ȃ+@ H]?Zx_QFyFij6TnI*oy#偊偊偊偊ߍ7!&"s ]l|{+!I#=5.= _CAqTg 1`Mi' xH:X1&{qHuk~H;xj $p,X>÷9:>5l'&C&.pD ru1s O"Q.D w]ߨ:?qd <%Gnic01 ןXoxo'%V'&nSʾw돹%$R(p>ҁs]r:kO~rl^ ?P|ew9 ~l'z# (a=᫓m_g7Z(nԗpƣ6 C=bA3`(QZ(Ԥn9kbj"2= a~Mtb|$_k [h{҄5*Xv'7 l"͗<+lGx)8^/G~T7,|F+ԲEyp vrE;#!ȍdePS < ^V-cM/гןTzFiٝc#Rm`r^D`FuJ_W78 K_x䋎Q$ܰ>8I=+(ջKp")$Ԉ*U?|j\E^QćPz//'J ~#AW|בg=TY*R*R*R*R*RLE)TX+P@.ɾY(~3!! %/PBYoUAbәod ~gGyў7'8'?oo(uGm﷝ qY3qK[za5U1C<2?ʖ߶"qN ,YY̓?7kLU ,y8χ!z3q&1nLyݟljQDdȉ#04$+ARx~]IzuOySQXnmEiuuj(d-P֝u䫦Teyp4=p@u:;;Z!_u5 ϯ<@(X_[ Px~˗[Cmm-M 5:<('̪)Po2[#&fG.8FϪa P q6‘toP_m2T:ec < ,Jك:^45=-&UؾO/\ZCMui]MySrk6/{"n~r[C]%@^偊偊偊偊s 4St &JaWhcOfMGO$݇$/{)KQAq,d׳C^>F;7pnb0 endstream endobj 286 0 obj 8018 endobj 284 0 obj <> stream x xE;K]QD $&@8 !! ^U]yAָ\aB.1wfr_b޿9$ 5GUbH3-+`z6LZW$3Okd= X\$|#Z3 \gVx}Nӳ ZvV٘ijf hU'U2OXAȊ>:mwi=K_,-c""v i88O1:O{ @-%߬e\`WI )%+"z`HǐZoWD*p}t̜fP0 &8` `VTa5m`|g: S 82dUh^p^L^8.mL@J[֠A51 뙧y,oFXu4R3QLDô,R$,1b7 r[8%eu9h4C$j910`W[L-AcBa2:aq-PA(!Xl !%T go3jFwʂ'd1g pBitMa( d✱~z[m%w7nZ_K.,~RLfx@=,P1aDˆa%e'-46} 4гWz B{5 2@t{/|izo}[iZi|m%  ֥iWN,z,qdNl|2W@g'")J{6\ pM%7\o(}YWb1|!I,/4b&b|b+x*`|mY;~XMﮫ|sO+ .%^\`H[3S2]LTQ9S}h>4! zIfVZ3UsjN-ܙ 2 Y67>p1P9`B'Ux)W^J??Wfnuv4ʩYzُL[0xxR y"߄^;Cٿ>c |(nGt\E/ ,>m[s8AlP72ËBkGiD<&0㞊efKx-C+y1(>+h@a#{>jCPVC9u!o5qb9|HI|0l\d LO%Qػ+Y;,޼~mȇG mC&BB ftSޭ'>X/_I}U'&i} TV-ފH|l%_&ى>g{{ 'S@`\>A&A:" {4OݥGuO ɟ:dߨ[9p1]Q+ Ӟlwo`'"jX?uSee!tFnh{?NS4ag'>>N`lMaZHY/3t>嗿rU&|-m,B\yC9{(wwζ~%ly>0w~ua].(&fR+& M_0Tw6m젊 hØ"߻昜aAfzlh! ?lNt.|Tsm:([o<}OFбjZ2toȊQf26tͧL0 pWjA9LD !Q?\Ç bq3=lj |9g Wef}'C|#__k}†[%WM\ݍT*T/@Uj3BןUls^³c+5&=JVъZSi֥kti"զ:ìhr߅aB9RC>*|vktLP㇭ȓ9 ͻ l/j`~Cns>*6D{̧[y\qj)_7Ba#yrPba?}m3^p= wXe (R+Ё Q'GhWVЭeX~8waz~O.fl4oZXoCP <8\JXl+K,jiGEj)ZF> M"vp"XܐurxC'> )پ`ol|+PYaM7_ـǏBbtCGp>'F3{8{n'NFV]`>`Eb#gQ:9i<]1E!ʉc KL~@Tbu7\q_+o>xu}Ǿw;}Ǿw2nR@@JqT*M6R/hև4lᾄ]`Ljв~L`$;ޮB7ۜL|uNާĄsiүC ~(}m%)!cf:n).-A#EEF藌6,i?r(?1ܨp݅ǂ'00ݕ vLl !z/, fbq+AoYJ>/ ;ANp\s缬= K lد\2e$a:4&2g=I,$&?M[0R?~CAY@`D<oh4Coȇ?#aU[Pj{Ț>k:CrowpYY]DP앗7nشpԍ^>̇J.Kz,1lvx+khiߪ駍A3O1_ 7 %_H}J V["|pRcqd~M%UYSTуU&UM;{]' ΙxG_0_~č_pb8ոArzJʭXǵdP>  GW}y# ]ƈ{f tofTZΖeebF ̪ &smQHƖܨ2x^ Cܱ|K]SAC,ZIx7[||ze \a"cxWG^d~+7k | X F/2k'Z Ȑ0}&, ^@l ʰ~o|ۋTmУL;Ǎ*A#c P6}R!X%+l Ć_^_yJuh%0c_0B}) BB}>\G}%S(@!>b\}K>Qz}>@ d>P;&fwk;/D^!a"?ʭ}#2 }^uSm"Fr֫>|,-!&=fe1ɧ"zdpއ>Jj&eq\I2*(AnxȔ\ l!]4BNbocW=08`,Ha׺}U J'Ya-Uv2!X:Èڔ)dZݸVa1Ne|)x+|G/ԲtDWcb=m;Z w'+̨x vn !TaD{ύ:Ŀp~`;doӇ>AW,q>z|e @} @} л}>4(_ >PH@!|qכOD\bp Iƺu3AY(@!>PHYEB/Pzw]uӅ˾=gn?ٲ?M2KtCO܎1 էP^yXw5d_8wtQv DnOڎjpև;zPD)WZPqr'hM8Uؑߑ~F>@'>uM 2d] 0'u ?CxK6}]6!ۇnׅxj gp0[3MKGUٍ} @%vP.j  J%#tҝjLcgodW2!v}@bJgS>l1nn0Vzn^-)VN=fKn*a+6|g\TYQWNl(l[#܁z}Jta'T=xn;p\pzZZf{AhpSr>5ok>ÁV g}(|1t0cw6}p~ &!/w%V ډD0F8~N.qm&O`M3)Hyl M= ǟ\3 *v#ŪObF7!"á\ik%gyxjy֠l8]>x`I}F>A>PA$10lo>՚zn?mͯ7:z\;#sD.(cɖo3WVwTS  G t2 Ejtc@z7p7)@!>PH@!qCssDQήs&=:(wC}P($ ޤP(@!>PH@!>PH@!>PH@!QćHv0nÓ1 H*X:GO+S' YIe K%x \rqػ-JBSjΤ>:plQlMa-Z 6Jaz=r AmORmg1*05OQ̅=alI@G?>P@!>PH@>PH~8W>RJq`~p2pEğtOz,mK"nϻ]x?A.Y$'IxƕHS3"Ih5t &׆Ɠν|'BC>Ax4xJw@!>PH@!a".1L}`NēFH1dȘ=HvB KIʑ佬^G| _WʓJwft | 1F|BI%͑佬ވ2>6&nrfr'2 iw`|\:ap{Y>n8|&g&wr}(7-!Hg5wty h]{1AQz؜hNi㑺e]A3qn]n:(&ɝܚ?+#_¯kNnߙmL2DWo>PH@!>PH@ߗP($ BB}P($t>RoccwQb탅?P($} b@F}\e%@I !Sq ru؉<{χ$mfƥǾ49U+(W=w*K M;[~]~)Ε\Uعb?/ `C{/USUu~Ph-\ij|Vy~ʕ+W{--M uU:>O~󡹹T_Wk2*[=:z{xRA0By(8p?wX65V }B~s+sd(_>#]v >b]Vah-G%il1Vf~Q5rE=V| @Ԏ"o+>e*GCCMMs䃻m vD-k5!bP 7)@~Aسb K"1(a{rǒR__]]7hKf~! \ͤg1iT9ۣ(ȂQ\]jj5_ ă W1oJg^v[)>^'m'eنdzbq@==+GF=%Wmd2="G]ʬk.9Ze Kv)ۉ,F9@]]mdqPLԶ캼-Q\d(6FуfIS;L1Vsg2v@* bDgDžl$qKR&IS8&bQiQY*#D >tAs/hTk+.:(vϕ4k*)٤3@p.vgǾz?+J ._· iI Y|5<-W-k}̞́N~g:%=M&?ϯ>uyʮ/{-~_W},*m77U4\iAښ=q+WZj.y~Qx@!>PH[UDߙZlE*:_2<3CoЅ'~Is}%_"I=ەr#2YȮgy-M'R endstream endobj 287 0 obj 7796 endobj 283 0 obj <> stream x |E;oWPQDHTHLB@H !6vяkKx)rp,Ǫϧ`bHB@&g2NS=adߧ?NIoȘ!K@̜H*9%ə)%ېz&CBaHĂjA-$laL0 1@LA*("'(,W+j!JA~Ay fqii($r9$bas[u!o} x  Mq 1xm,sHf y.iC.$im!$Uu4ZX^`,<ğg +u  @,dIPzڈ9hAh,Xc? ! U̼*4&!"ΠCeN~h!!t/n+U/YTzNehdV.Dg!ms;1?9Y1B hdyƔRyP/C/$_]YSex jrʧgjFEJ(O_5kTIc_$`qfAýAi ;ՈH+I(.7~hn|ks 4{&k?սZJs˵eh7k6.R&UfΔFs,~JpW_^F4˚T*V-F̖_H@'A! KY 1g-76_]ZYn ˴ʧck'飷<8dpcq X:}`h8Həx$Smb [u^2A-[*dh[^ZZ; 3kN/ztF `<{ @R!$8+\-O0 ̃ hnE0CCysk۵/a<Zgeary4XXP/$B9BveF5u_ED>S_b|.S0.TJ_6hyy)ubS&ߧ|rL&SycO7#)M(j3䖔 _1Y,Yвi<>g/>GcI4|;o8ńhy'\K5m>uIeCvdsҶ Ĭ>t8K: >-Dr EeW\2ȅBg zÎiރ>ăA,\ 'k'k7iaB!M=Z3GBUڃq{T wF)GW}ai&ڇox0d:GYj'@"۾YfnXl?5>k;DkkiJ#C{JKAwI41eG'jޟ LJs_>nYiJK3 yKؔ2/qJ5[DPT,P>^B͆EgRaIj44 FGFY1{GoAF-fBs!;tQn@,i}?%K[6Ψ(1Jfd8l.ڽ|ֆJX3gP,;eʗMrOǩ% un(b.MS0B9o'=5xCă v,-@j$e<i U0UhtKqL'Z ]{ϮB5</%谕ᠬ^ Bwq%G N? Q52uNz}OhkWZ۵) ݈x2 P ؾL;(gkCr$9ݨH ez8}󰤓$G<6R/wap^æۤ<BLu:(]`Mhs$DFvP"׬ӴgvvR(Fd߷oZJH"Yw+OR|BfP굳4kb4"+g?w)C͸8ntsϾdΘg.uw;˟V=<抩7?o?O 9;l(k`Ia&BfZSk1v~v4*J7o$O&r0bCěEINrtB>J_VzriFB E3GVN&ym#ѤCu^MeZ]` ndC$&[W2h^HBZ0PkYan|-ЫLB#AQf\yN4{m;ڷCl1EB®p$!>qs3C^?f]m^1*VLef'7)TFX*R7P> @E@EJA~˃/hQ<0 2 $ ᕔ _nCpi!ab>{%&bÀ2r]sj+HߞpƢtnGɅyUf׶}U :fQ[\m7p i<<}ĺZ(x sÌ*`D5 'I̓t偊偊T?_OKc(TUOCh!X)W(fjQ(SܚS~,ʀ/<x0Յ2Jux>ñzx d*ʀ0POiggYx44_=΂Hw,ʃGjYR}kkC}/7O_ Qהlشmа}pIEy<ںKK9SUO@Y~_>.k@׮u67_uA/=}~/N'@Q{KscI~5A{<Z̦: {<6j 4  ʝqq;+Ch_>8FϪrww29F8 ϜM[ZA[㔇6g|lQ.ɢ=câ<;p 0/zX :F7EN1'yP,==' vWRN\;T_oxp dbW,^gYwqAyDž!BP X_RAHc*GeCrKFNUU;M!3_јc|*ҝq|PdA).|ԃLS@(c0azLu:Vu)nk)>2N^NFVn8. 9ebv!eqqexXW3=;r2uFFy kKh?DJ*C9 ΃*qPLsYA,X# MA FAuƃ""(72-AA֙K<꯻'U5ū_{ކ*(v=~L[-͆ N]~` s[sCr}]uYRZEuvzOč;uW_Wy lQH!\YEآTA!r< ;HG:@AQ4Q"/? L:%嬠܈/;p^b endstream endobj 288 0 obj 5884 endobj 282 0 obj <> stream x xE;K]EEAb#B@ r@ڇ|,.*E9TDX"&" s'"Gu0ɿ7_wMQUTV pc ĕp 3:.H?sf#7LVfL :iFXe)z9ȯCk:M# ]=euP B6ZgKco@LxrChN+!BCt)>%PrvP Vf!pqm!D `Že@  Ifi1I@ IR/kiMy+ WJ  ^Zn:|Jt, ,Г M Hi !h~mFnV 96U3,B˅BZ.*(}=S,fXA!n8| $19Zs*#u;@rS D4~tJbqEO(uA!'$2 >B %$+V8]V}킍fQѲ\I"h\yS8AB F#fj)E_:ÇV>dckiQŻ/6.K4<{6Z7k1 (0!ᡒ؁EnLx:?k[Й$ ݵp{> 9h`@ Ӑso ]/TKbkvlp]{+J+" ̯5eZlZ9͸|!-Nhn%3G߱oC}Z q2psWjUgYSt+E_={З !/r/sk܂|E%fk%&ֿL4L |vUmٰ떘|P¼f饙 K'S&融*6$k/t@P9ROsiܒA˳in|.ՌܢR4ۛ:RM.!NCOXJ(Sӊ`B'Un~ekS̯3l|aqdÒxEH 2I팡%Sɚ0hc7>d}y`VƒYP@B`3`&邽/Cw}KC+dDK'Y6l|dh>y,f\_7Z7+R;}v^We eO﫡|!ò]FB)luO>/V`Um/iEg;Lw Їx+f2anHl#Rx٤'=kߓN{B:֋ibJyJ7gCGqC'м-xߎ`ƺ.n0^s^SGCo47,nNnfvc .T_v젯YLE }h_a?gʹ>|P9 ~pm۪Z | \x]q=>V6|nL, Y;^;$ |MćGTC*BAʶ3S<fd>Xy_X6VK6U-)Fa_+*Se +z͎EEZbQp@n"^ =}Dr߭{?jco/}z@(@A M(%9m JȤ>Jb3m* y͢H1_]B'Vm#I$ϰZ>~B~Sj^<˗*Фo^[ŻCѳXPdݗ 4rυc>l|Q^{ه % >Lb\oZdeII1Ra'ܥ]{>h_ϻ VÒqǂft[f~X-PB"8*fo;_?W'pf!bxaf "t"ˑoL̈QZ3I6tڍ +!>ܑG,.(.òNB{1 ўiִ ?T0Jz- C!A/7 ~OҎWZǔ7+1WhHAEM·Lvw_y̪^ȡU&_SUX-1IUב>}hCD04/k0?\l5Mo{n gmF%(">\7lʡw @ N֐H-}JO:1xiU`8^Ei9≆%q 2n+_n4M'jCV&ofY奕(-/l-pʩE;0[."FZ}:U:h#1.+ Y-+ۂSY.@aͩz/?r"ScKe09Bfs1 f0HV$6pţn-nSc2,e\i74e%}͙aa !>@S%MN%>{}wzgy[uC^rHi7/+)>JdayI (sï&trvݬ]0}!3% 0O  La༇w U+hj Jm YkiG2a#'AQdi2lf^&RR:y+ V@l& 8W[c qXh=J+;2ADHJڊBHy\݆cޟz}{PA{Nns4 6D~ҧL|E XAYNzʹndF_(B2xzӂC}ʇX~0B-eGP/j,pB+wX>NdFЁ{ z7XᓮHp'jZ!P0lqDg"H[D!crb q;X?ftIgC^e:8oTy D(6GZ@x<_xm+ oׇ( WB.4A+4>/z=R7*4ŜXa\Ϲ>A (AaAɇ|(n}@(‚> ,ׇ?:x]\\!4}'p׵%ق>>jdgfi[}+Dr\͓oFF}!rnj';Շ?hH1m-l8;K:(d~C|of\.e;ᜰpІl:^Xbc !@kvٺlkt>8(8V~wXNP|+&+í^;/_"1lJ+$Tnnnq/= Q"NaJ LEϱƥ 7۴cV~7C}pH}_*ee‰7_ =SF_=pG,wN(- R+kڑpБpPLO72e.A$ f}^NmE\g٦eqÇ}@v7_(n}@(‚> ,‚> ,~\9V7|.j^!Wҭ ]9Wҭ7‚> ,"}@t_|`I7ŏ}G;oA{1T.8Kv Sߟl8piFc{Ӹ+@a;>taz2_,4\)%bO8^q, Hk?փ[O>tj!_~|h@M-====2I|JÊ>2ζcm:$ObE\@dzf}(L#pOg/Ё$s|!M>8FW|?Hem !11J iv|G3j\.ۯd15H7z?(X $8b{V'3Q|o@7IYrXBtu=D50H2n;-]Fw>r;I'3k~uáo 9RcŊ.t4==O߹>[M{5Z}E{7foI g)͠A QMVWn)<$ʀ {Nhuc>3W}|Cg- ő,L0a6 Zv(|/4?rTy"-rPCn(偞o胿/gZ{෺K&)L@2h|.> o"~   |nm6}Q]lʹnxĿAaADD       xŇH~0aǓ13";>ىSJUK>3eLjKΚiv6͠B6'9ܞ*{: ;%IIɌ )٥i'ge%]b, ^A=YIU+K:;Y\fV p,=o۩K`Rk[i/lE& }{꤈SUOqZBvQw ]j/m%o|>۟l냽މr/GPFV*߀> ,‚> ,‚> ,8݂> ‚> ,+r\|0 {CPv_Ky OtL>{4>E$[S䞞=(spȧKA<§>،}W;O^z||[!?]'"r Ы|:b }@d}@X}@X81P>\귊3>JxJ(ە8~e }Wy>l@x\BM~GJ;>}X/>JCqe||f5/<(&qYl5>Jn ع>.@Wć[/`;'X:;/G"q:O/.U^y샇?7zەGem]_8C9:ĕ>8:+q `?8 㣘6Q!J؁8O ?   !"‚> ,‚> ,""ab϶"=j`{C|a@QfcT%EuEyG}tHN.H]O>A 76 i-7|7ؤzxzI|uyR[l不!rTEZ:: ,/}p4=Ptt65A+{xz~/pV j!]5U%=Hg>HG$=iܒ}$< dؔs)4N|0p'-Ѹ}bɇMblF.S*!:zC"2a>6-zʪJIީNp!FU;g< {N"70!9룣ϺʜM\nQRiuizmE%*h?D TDz9@mmE6!'9룄̍Q|WO X,z֙$[Wܛx"1L^شqHn0.^M>y>%m/($Fh{Қ KΠ-reUo@Rd6+MNW+~@u\/;ć^ϞGG.T++󧴇3, =`!/3ɜ’s9*u}c_;ey%sJJ(왌}Ak => stream xZIoFW@ g dI@{ `iQ$-K~6 !)v8`oߛiwoo6ogOo7ϧ?6k ~?0x_s Lͯ~~yGۜiAsx;ּm[59oF[w!}W~ت}ӷzHoێXzOmf[m@]h{( j?5Em3[Tʁ+&Oۜ_L^bNb"PzߋwD]6s5N&?g^W_3Fɗ!Yh)ĵ%֣\BӲ !9;QJǬ(4-T~EaW*{s&!ſƏ2ߵg  j}~eZW;|u(LދB"Q>8#It!Sq6vƾTMJ=J҇I'a 5*% ީ*LO+{w^~dmȰޔGE&f-5s</@ˠF7RCY"VEXH ϝWaꟵ;屎}IqkoYF5ohDl88[\R~ԓzThaj@U JDN4h+0kɡ> +HտC?0O(xQa=/Ks((-0>ES?+p1s$Tm8R-aw2vשNpN+,vF,"f}~BcB il8o3 P|m6lf+NX?h g[thͶ+,ܨ5䵋CC&O_[P[Vp9O%8ȩ<zҬX:I YuF)V!*&8hU^1S6DbZyQ@|a2c ܒwH+볙Թvd*\g]Cb00ОiʬDdnLNc_Z7WY_n 3A Wum}RP4j\ńD0!YVW`BIe]?3 endstream endobj 292 0 obj 1772 endobj 294 0 obj <> stream xmRj0 +t.$d;nVv v;h6~$ Eyz{mkos 4%^_qA͸2o20߼&w݌n]B2K`""l{d10^Z>z7)Y.a&j\Qwp'()xWv`)!)r@P)=P;7KӁNUl~#OeF3b(C.;UE: bJ|rGqLfV#=X+<8u\ cūTv3~Oڦq?q܁ endstream endobj 295 0 obj 308 endobj 296 0 obj <> stream x `U՝/2(S;3Sӱv Z\nR ЦEMQA@8V$@ l/'-={/s}W qIs 9ʥ*_WIkv z׵#tmZQ v1!0iQMw*;9T"wTJVQ=[ b]W<<_^|?ސQZOLyM@Xb :D_/'mjF ~>bޤ1Vv٣Ķ]6>RS3ozB!F!b!3`l* `Np,?AzS^_|&GLi$HSō=rx9wOZ] 1`ֈ"ƈ8eS#u>n7Gopstc6bW3(4s05!M93? #hgb$ר{˟Smu7-ҲQ+mL.jO:ιU:7ͭ VZgAlUG0%MSl|w/<^5qwU?=턪n"T>r=):o~ZX@m̹>~tVzxcef̡i=8ٸ0hb Ћ.elA7кaUۇ[ֽlxqZ?Y7~ps̜T=o.ړ~;~TknuuUŶ}3y=w/8~ [aI6˪z'ZuZJU>QWVV[dմ_H).Uż!Ee;jɕW{ɹIۦNxWfEW}8|;&i"srkE҅SLLZpM~=uoWsy!i;iXsӜ=thչd|m9Fޓ?QGy{%NW-}Y\Ύϣt>XyڄsG_<~D\9K% *sx'673YGm]\w]S>{W޹MrT[v3xNunMZĹe.kAӭ]XE{γ`ٞ{ھ"o]2ۉ;?o$rm:r~Ċ~?A *4a7ޕ2sm&Bs:tެmD[y7j.az}YUO[6oWʻ9s5;ϺscnO΍X8JYp7[p93rRqnyDLH?)1ڏ]Zr{MݭA"?b-P޺]J>GтQzP=9HJܴSG=(qsc98<5qyLۯ\9I;'LN?LR#wo}w_F߭Lkvj+2Zە_s w@Yj{=\53寧;m~Nw}untJel=DIse|] }P$bB֌j(./be^f̮f(S |x$-[hJUMN2J` 7l$.J8N]"0=¨9強ք[$Rz|Uʩݝ?n52&po5$=!Mfj{yf<":y#];jgf_;ޚwjNߪ5w}ί~EܮwIחSt͍8oP;Gs*ss{/yݓ?wKVxM=)$ppX7f7U ءǻ nݎ]/s'H{A*rnT(ߴʫaz{OW$NRйdhݶtP5Q_m%ԹzVJ[[ߟ*۶z;[pRVlw:"~P^.gĹ!ͪ4Y|%Mw*My5oKbV 1b;|cԉ53۶iS;冺'{K'sݝ_ o|;7.ݹ}ܔ+u΍ɣ,wk>MqFm#Dq#A]DjjDl^AJŹ o~پW=gi0;DdJQ붫XDěQ݊k=S5qsI 4¦NU087:ž2m*]x-_w=Ӫ'zmvKqO]_7Lnnǚ w+wԹ4fޮ=;;j:F<˿xrA5_=TpY\߈8A'6q[`z}xF-WjJ5RSVw\yՎQOyծR:*ݪ:չZʄS2}OwF=}&wKQAk2^#~)ClNz2S 4.;)@mF֗Bh J0 3rVث+a#}p/mSLWԍOk~NNzߞ]qwn~@?_-uIw4w)ǛF>^;u˟|j4RV_L ejk#g)G Ncԭ{WX# JԂդߎU/qLu5J_k=[y'R\Ej4X&4!xqW~(c l&zo+ED{޷Xr×︦Wc*7V=vc'zj7U5F&\Yu㨳?lwFPvg'_v#mL.unrBsnĶ|)<#{/bgW<"OG>'sf?Ks[:p>8ŸcWKn?ǔ?0~}TMF\=K+~٩'mĝ3oĹFun<Fyoun[X\fWB;䉷򺢰Ob2o۾/'&|/9OۧnV-_?s˿37_S?}i ߠ.ss~ӡ-f_ d|K]eӧn9{r~Gu7_?k/?wwdҶىj5ƶ#oŷ.3|ӽV؜z1K_ RV7)_:~Glۨ0~UI]J+=NY9C]O*")7*D&"E9q#Aꥐ۲{ڤ;[閕jol3arnԼK7IOp[(테w9Ҥrj&8U b[]#Ni*7ñ*,BQ~^Wʞ8O|s:7żiV˰G #m{O\ĵDvb[/ s* `Zd!LK &s,_R870έ ؔmѻAAPDi %wnAEĹˉM’!έOj)-o[) *qvF xHymt3m?Da*02 Ei¹-[BXVa]9˶f);(8Qp膿-az̪k5V>XχR6ouڱ6^KU[m Уi:'( Fad fW8+vEA ;7WQ}ʗW,i;>ۘkZb*B >OAؕP3n>uDalq>KғO?m #摐aQ pdx[^?qnEn[v1giee}KO-wn[R!n{C"V#7/* ܼJV zgnq0anz~(4n=%ykO^OFadtMxH̛ϝ wPXχKun;ק [ܹ{F{Ya/ݧKo/oŠkG ^nڈJzJb9S5OQ>^DZp):ܹ0uh6 fn^/&P,T6vOc]Qzlv}&$ĵnӰ̬[{4n}ͻ-uʰX*1ބv֭N6BadX2<qn7:ʝdwX~YCIKnM/mO45BJӼNze&gH[,t$ɉJz']+-L@=IS#;k~s6\$s4Æ^:0B ìC˰q¬B˰d s6\adF-n8~gxk5fdLj)TY$lJ_bi=";ࣄgnN_pf;7";Eg쐿vCQ՗V+m6]k촫y1묽]VVJI|OgjGuSQz3'JɄ Yԩs M;) k);aT̰:2fOP_!02 j¹-ZA[f3#yc_#)ñcKcsZISwz ffbf5W#r+cs !Q[k[[k+A.xs7Oom=M"\rvXM$mZxO+?OjjMJ:, "4J뷵;$=ڢ>[=7T+bSf#(C7$HH0Iz"Cm,2a Ɋf[ӦWkjim,M[۸XJEeVmb- 3ix2Zl^H0]'~m[;jFadx3<mk[6##=GqnԹm&m:u&ܴU58H(skurD[uԟї3$m\ͬhn=ʁٟƓVauDfh>ZHѴ[piѾ[ glEJI ߦۖSZ2NY4bڵؔ:hZWK4@jTb{6)д[Y!b02,g [%ܞ_lm;ȑ|qc~GśҜu]/mj?=lvWK?iQZ FOނ87RCopn[ccc~57nqvt\(;j59]r)4l`T^v*}M plBd_QEiC:C^ڠ[ohMy5dy9f@hr"&ѐamڞz JKؚw/p d45ﺡXgݨ4S6܄PaT<#02 Y3<m dHvd9KiS7stzpnXχ6 Yaj>0asy}mlA8UiZ}'N FadxP2<m֢d7D;ù#ݹ9͹1չ!jL$ XEavQ [&wn51vGm#g$=Bg|Hj *kĦH:OX,ʬlצBADܞCo@0:=OU]|~5&i^9 NN FQ8 *s:͆7o~HU-";1ԜtUb'ӶiZJ~,6WUH2+efyseVUwVs4i7k=h5N(T?A lVS3dXo^'lzzUک*+ WzZ)eXZԛԊԆхUz^xs>}=E Fadx2<mw0 qƂD$_7텷-I.E; WRv]z7 *絛o~-G)cUOK˥ +-]j\lZK,vxS8uPeeP ]X&%L 1 Rtaa02, #fx E( mpܹ*ߥlj1ѡe7ݴPH/!;HS6 4S7oKZY 87mXZjh,-5ӟ -B̰yj02 #p2<8mG[TpV,q:%%](NEjZTA%^PɏNaD(&6|K>*uҬ \ƴi9*ѦESSρڭDk02 #pWޛҾW䗥/IaDhܹ9b% Z(Q^nZ^;vyi/}bwšѧ6NaxZB]^&*LFM2 #02}ȹe,{J-<\nnpk||ۛ'Q>`k,oԹZrL j;aNʅ6!ǹ?10U5qnA sV"t5VWrn鯏teěDٽZܽأ|jS…@ gRBnZ3@e6Tw(j1PQw sn7]-s0Ɩ*^%_!{^{^ɝfT~z,ohT[f|_*ӫJ|W;۝X[)/ZsHx[GGK[[SssmScMC}eeyl^5ԥұrI˒˔8r#rC}U4?~VhWKo-: aζU(-(6M=FFܪ*' OzSy'CaK#ݟ$(ɽĭɵUwȮ[[d׭m'nk|p]\%7!/ZV5{ tEҲS a+87 ׹eJ`&Bf٩[8WGwNj$=䊱rurOd׏[Nd7?y ^ /k*U2[ {lX^*p ޅؼA 'atnyn)˯޵@[W?]k9!QH={i\%87 ѹ]ޢ; 6|>ő~/m ׃Jܢ@!:7R ܶ繥>xV`6r5 (dBAPh1˹L5-SC{3N'8ޛוr'h7a=wt9kֿѶPt @,Dl[EC_qMgN;GG Ӿ=>knyoJ^_$m KXhضJߩR?1l̳}Z99M!-$uny]ֿx-uw/s2LNն|6Ƒ3^Ҏ5s۟+kn˥U_"pn 87 9+}ѷcNݕW#Lܹs~ aThKE9'(? #/2Nol+Qv֫AZj/vBun_s=d!2 ެ{vkg/^;ő,Wmnf .~ qw؂]O܈m#@"Ɋy#Dʹk͍84sȾ ܲW^ %X.u7g78VrMٓ(_][0ݵE 2袛W洇[)[AV|P&`)E]sۿZ[(gH0kGj?Ն/'F-)XjG Ĥ1>9"̼zBz{==]mŁ[#叞W/j(%SCP\sۛ P|:g'U{F6 w"LM'.έknQeXWvipn}ovJ==/]m3*?Sl~*O3g[\EcTapn[dVMR͛1SgLGMm|cC}n-mmM͵M5 EyRJ>/%/K/S#ϖ[ U=\YA[4 7wqn@ 2;.ŹT6ܨI6Lopn#=4vi{I^s(r(<鹂gOe8}/v$&&+W!no]z'U" R;#ҜYBǰ;7vS^byruR ]-s0>ܓuڗsV' l^<{t[zF[A^Fnl"d&%sKyut{H#>M+_'DvJvW?s3g01!AZ.EUcoֆUGx_ 7WKpvw(gJ!-4e)|c ]繥,zIn]%Hv} >WFm#},>7ZtknЇ) (?>O-`^<4 a"-5>`}ni/l>>%mݹC}nJ̆a^eJ>wKoc<Ķ8һ+sxACBpnb&GO;}کAf||kwn'mE=ֿf-, UAA2-[BXVa]9˶f)S ݹ@knLĶƉs[b ѢМB!ܴ>_s+ruü]pn@5kCb޼~44U?x7?WsH-^eXUctf/~/X~ #xN;^v,,hifniҤK'k:7W󳱔l) PL #18>E~Gz8ñ}Miκ..t ֋Z \6{Mvf;R{إҴ)9k:=ql[F~8!a~2pQښ۬EvoVswsGssscsC6wMFbm9.ST{[]9+a$ܦ'~ 9 g,H$ABu^xBFbm da$ܠAFbmm’ V"YA% #&6zf'mݶEХh872O nrJ_{?-j%:7Ҿ>VHK_-:-ҩ bZunQtMxH̛ϝM3SIjSs ,87  EXsUU5{]n_KmlQoNg"WpnAf+wmceGK3sKS&,ݞ^5??Ѧ2T %$ meB{XtpnAf+}^]p;ݾ^_so'roF-[Fųi[~=Ufhs PknDVP疙q8ؗHbOulN+i?CA5`Ԗ=68}Mx|*-q^>qMd?Мz[z $jk[6##=GqnԹm&m:u6ʹše-C[reAawnQ>M3lt땑Ə  bm-D=t3v0ױ# R79뺼_j"BYsfasnMLvwCi",Ksߜ5;bA AaVo ۴lGj#%˱]*M+ޘRq7❛~Vj?j*ݰ*OfuO(9$R9oݼ`--5MoD.8% h knh=*QppHwnNsnLunHu= Ě˹YwOms!ƥCzq?P6nzm$w hX knzzèvzj:!>3s^yY/Zx`ݜZ7eO|~umwY.2ֹYD'3] j$$heS疁cACȹ%έ_"[X]Qȹ>}TӖoeo^-QyE^XvQY>)jePYw7d0$5vɨ2Gsn+q48rW7;%WȞ䞗yr)[6T@O HOL wa%J::Zښkk++ˋg.}8^K_d_5Gn-Mz[jTP :ƀcDUU9 O\᳧r 2OI r¾Fz?IzQ{wM[k]Ȯ[Nxs#VTʀWK?)"-oZ@pn@5Y)LDL0;uKx$G|\1V.N.q'~57#! 1;,OEWw(?pn knyn)˯޵@[W?]k9!QH='DsD@ (?>O-`^<4h8H iEֹ]GC_> s P4ܠ!!87  EPAAlm a[I+v,FҦHOmx BQS_ܢv͍68qn W 08Ǎ{97].j,g-g$e ګ}XA͊sZέ~܊\0oaE8q}yrwd6e$b+[?$nÄcfb0! fkCb޼~4lD.ƹi>mُsŋմ?Y6hm_1lxaU9^.k=fg(KrnKOV۪8nTHв. 򴈺̹!ޝ Mo; /AP knZr'/;_{434Pi[K[z|m>ް[Fb8+okncpɾb!97MV"54e_! .7XlŜ[" n=k͘M3s{+ څ5+e1KzfU8or:w.6?0)P/p~,AŬbs-h 2s9I[RJܬO-yU.6"wiWK+funywݏ%?{ntdLC2z fym%ÎÎŹeR継84ZJΪQ#|:qnWK1tu֗O(Ѱk:0b (knK7-ms9:>pl(xS;~W6 M4q rKL/ sC!pk?AA'j dHvd9KiS7stzpnOW%^x`֬WTcIxA"j֢d7D;ù#ݹ9͹1չ!jLvg[s P57[==ހPauzj;=5vOyԹk"=a%87  EXs/|7~a"gA$Hn o[ ]  (a AAP(XV4ܠArn@dsz[j’ V"Y`y蒵h87`H0@-j܈mpn"R\sK<>O[-sH-],saӌ}3xCX]9 ,87 ښUU5{]n_KmlQ%<&N ɉŪaX[s[r'/;_{434Pi[K[z|m),Cua RB"?mOLLL`+tz!}z$L0sknsSy}ttz}=˗Ψe`45ݒ~ ,ad-ZA[f3#yc_#=t>ձ9k ՀGS%Z2&Ggunbmmk[6##=GqnԹm&m:u6 >MY37avA=t3v0ױ# R79뺼_,N8s39=87͹5V"ZE!QIцFֿPPXAj'F5"6_h&F M 5,"bkBH+-4a)jZHtEB 4AsdE  b7^P 4R".D#R:Z8`΅:RCVLH&Q}^(B(ݯ))5kNQXiZ^IB5Qo? O "UܢVo ۴lGj#%˱]*M+ޘRq7?M~jA*ؔƼsD-%PeCCkZ'lK4ju,n7ך+iՔ漂PѻeވmPʾyFU E,uXsjۢPAj ,xVFjN5(gPԜujiZN16`)bAa R*FiBB-9"ӄZC7JQmU)P[N)( <`+UL&* ')ou0bJFFR!\ 6<%^#j*\:<^qkn'۽Y% i΍ NWgܵ[.bX$C(*):H5}ti_s+ruü][gAAѮqnQtMxH̛ϝM3 )s7Z l -iǁV83e܀ݩQC=R^m]madض 2 #}Q_s[ʰ֧8^.k=fg ![>mO/Ujh|ڱ؅l'i$cmL׫ |^EE)_^̧xLlcMjm5`>qqcWZ;CϸbI/KO>ڞ;2 GBF #ђX_sS_y}ttz}=k~(1c Ms_ -9o҇ldX)sxʗ w=eߣR2eϭyPIo-XnVwr.GX;s p UGQg!v)[Kxz| n=?02 #^s %Z:gF!Ǿ,G{|csZIS ; -. n&VZ\Dp^Woyet/ +{k5zQ[k#7k(F*UL N4&?E}4z"BkɊ'o s;oFC#_sV>:HQ[&unslmM\- aAmY-Ppnܹ0uh6 f5M]Xtd3F7a%e6S,m 2a_Ω36;ݖSNBXghg8ĵnMmw•; 6TD+4lYNcVS!SShwwù;16{Mvf;R{إҴ)9k:=q87IiOLH&%N]!Xaέ|/[|֡~F]̽J_hP}Qz_Y6jce; C+"2tTۮEDJi;ЧѮVρ|L6{D&L?~rk3ϣCIx2h2%SOfx2&R3uoر+CrDǑC"O X5vTiʝ+JhzV2/oX%@a=NZ[1N7KgJi?+Zz4ROHWvѕ.dDuhbظ >KP&6vڄU##Eڄ!XD L=wφ׭a^F k#ի#&V2gp?⛺z}lK '.'v͚'G8z$adFÑa; s57hܹՈ:رo۞g;vH!"dQJZܼt8=4z9>'Mx'l[ A  sD){UJ~Y9iFH[eeeUUeU%UՑo֯ZPE$IPm*ij7:*{JT*T*k;Q*ު@2J-,(Ө0M-kVF§ǤtN47Lr< s)?J*6qej*yuU^ūV =V}T bVMyUjSLG[T#Tiͅ = '[3w0 2 #px2<$_<=lB8!έU|EdץB/y_ǼBҴriøq0XMMRX,vxS8uPeecNS2)p }"3Dad'2l>ðg8rWIKE܆gE;2]핲CV->즛bJH/vmKR-V-G4A ;RS@M3,@B ^ "ÆnsnN?v[R1w_z竑vY&RY9 +IzL;*s+q8Β .QJ"-P*/Q[(Yzs["h`>%I:iV.Qc KF hSR˩ĩ@Y"VTdFadxHgx1ik|xoڭ5XvxzYGcL,_v*s+&r8)J\ 82Qzc*h͵CסwU^ZދCkOGӡ l"`jg)MOUdFadxfx@9dtrn+/^C ,Z+_9Iϯ-"[h$!AYKKrѢj\J+*>`4#uħ"*έV"YjH"SD{)RMQEB"CUu|CO)C-=N 2 #02l8un ]@-ܞx3,/{ejr+r²cM Zʞɜ},#eAܹOvcY"H,i ΚtNx9 ΚƶZ9qWx1 bmΩS0 k߿CÐv@vCZF Ȱ̑adX ZԹuwKF9 scwUJ)ٿB&$w͓;'ͨNY޲ knJc Kbtbg  j[GGK[[SssmScMC}eeyl^5ԥұrI˒˔8r#rC}U4?~֠97ԅ PA1c:pέYx =[yHZ;4IҋܻKnLܚ\{\uE.EvvƇɹiO]-˫0HYb8FA[AACC 2rwd|t0e!3-A[ʫs߻EirX:'-'Pg ^͸fCAZ0`bss+amz9dRw 繥,zIn]%Hv} >WFm#}%oWԛJp #B}[tg'8r-a|Pi?p(aܮh~xgfPpn@ ca$ڜ6sH9lm a[I+v,FҦHO 2(DF@c8܈mp$- ٹ@B-Dέ~܊\0o#87 !5!1o^?wn6͌e9xaU9^.k=fǕ¼ 2[knW$ۼerf斦*M:X=tkziK/~~M3Rvy:xUum/^ٓ{#e9Ŝ[" n=k͘M3#,LPzjQg76e_fR:!qZeUƓė?g?2 ~`un9ΌC}Y -i%Mrn'μ*KunY'gf\o՞Q[~=B-D{m+fvdv(-:Ĺw6PΦRԨ^{ƍ.ZwnVgꘟ\-,V[[C{yZ^ -{yv73AiC‰3[s{~f%`c!GAoJsuy`׏e$3!h2q ZgK_mLT|-C7x,9^modHp9 !f/@iَlGJcTV1x>gM'n^禯= fT0k+P;}Ŀx >MxX†Mv} !۬EvoVswsGssscsC6w QpoȹY`s=pH==ހPauzj;=5vOyԹk"=G:Mj*g .j)!MĹ%-5߉_nD΂ I7ݴ޶t׹iHMµQ;{ l9w(2sxg9ܠ"87 !M!,y+`.œe"'87 !mqm[EDpn@B-D΍>s{ĹaޢGpn@B-5kCb޼~4lO>^~o(=g2[ZʰƜykr׵^M3껄$É!91U CŹxN;^v,,hifniҤK'4S X2D~ MY37Mpn@6ܞ_lm;ȑ|qc~GśҜu]/cuY'Mչ[hsH95{Mvf;R{إҴ)9k:=q맦{ĚM)[P܈c*m[hkn'۽Y% i΍ NWgܵ:O(D"=M .܊Ěs ]F]SjO&sp#B\s>/L,H  qM{m{KAF͹ALpn@64Thsn!j’ V"Y`y(rsH9܈mpn"("sH9%:7Ҿ>VHOfa$ڜ[knK$ļyܹi4c'$|ɺpn 87 Ds QWV՘s;w}-]ً߳iF{ v8OCPa$ڜ[knW$ۼerf斦*M:X=tkziK/~~M3śe-#AJHVʄzF< @:`km-D1֧[{|3l?{3j2,MJ5CC߂s a ~1B97|K 2s9Ii%M^g(/Omٳ oܘA! (%BԼ׶maGaGz2sL~gunliKgše-C[r:[8@?Xchsn!ɖضg8w|Z)Y]?V3fZi+sXhDEs# FO$R?ֿ/o֪Ux˗iXIHHxgL@Ds Qo ۴lGj#%˱]*M+ޘRq7❛ݢ ^0EiQ;HCpnC}919Q99﩮].L={ ϤSnk9DO'6۬EvoVswsGssscsC6w Qpn^մN pT硘w$M'3mNN;&N8mR[ (v޶NIr_Ǐ v['[;7@l?\60-/IϞݳyٳg} :ڳ?{U~5;w\^w4sikiʭTu ϧVnfK'^?JKI^΅rj/@8?7poŁ\;u_zAȶ7qถp½Gv\w ?iкKSnդu֬Y7`K_k+eҤIޗsyg_z%qw_\q%DB܌Z\s}w̛0!F'>ז°CD>1"f{փC[kl۰W+5;Ӕ[5n9so4 EZSS4… ,h?iĉrw۷Onjj={G/~!o0W_mZiW^i?\Q+70(7pٲڶeHE}PۺU ˭/-x1-%|wz_W|/t'ǎM{W׿?oyΝ+G?#NLz-#I?-s9?aiqUr۾}??=nKOEzKG͟$:'Jr?oo<ՑnjOޝy lO{^+Z fWI:!J<.d޽tMNח>q":#Yٞ})scq{ny`hDEmOj'Gye]3'-f|J6?,wx._PǨA4,Rm"t?(\{آ[W(H(7˯[3T,_oo>Z* 8ڝb8Ծ]=ށqJO_|3MO|~-;SF4 5͝4gNM>/mOܝ~r߄/=8QOhQ…R[7g~x*,%KVl?Ho?o{,q7=U۷?U/֎Z\s{U"ga7G: :#1C{GFg |D|ۍ ׯ.\6{exw>zfi9Q_:1/# h ܟe09vdIq{֎z2}^Y;] @p߹in>6]s`hAM^Jr4._7?xw.r["Zne/uG0Mvw_sssFN mۨ>L,N7}/oe˒%O8#>}Ϟ=R[bB"J͟?B֨eȆSw֗_/v:,^'ą3fҥn96nM%oiYekZ=zҠg(/7-f%j3[9+?Er [5BPolaA r[_OCv .1R^/%ߎ#Bqc-8N?4.n5{9s}u J㌕rrc~MMlؚ)Z-.թ\݊9ZEh,tcLž/"Znk[Ĩ_mookYe-˓-okyu͑%kS;]XEl9dQy@(5O&}{ f[A=xZ6>nVn>yxߵ[lniҤ*]{dqūS=Rn5?ICV  g\k海WzV9W^LmܪI*1j^p@oL'/ vꃿzIArP V=rVQ+7knOn”y4d0~r(܇Rv\nU矇JZAdT !"nVn> .PnJZ§fN~Gy]A 1_rVQ+7kn"r\@ b=ÕJZD.7Koqm(/o!@?rM 凞ܞ~ir3t5YT@ q,7LnZUkC׎u><^1GKC4jsmj2m;vm޺q[-+n}cCkZ/Mxk\Ii-^e>4cC C4jnd~vyzߵ+C]{}-YuɺǓ_g 9r-.9}~DDrܼz96nM%oiYekZ=z`PpL$[K|' 冈Q+7<ĿiNˆrۨRQnRZ_.-reʰ\Qn%C!""hLO/l{{[˺--nYlyc]˫k,Y}rN)Pg*9[1(G&1\K""F .,]eM-MҵG7Y:uo`c喬3!u]]Mh+Ph/rArS= P=z1.VpMzrMG377M-^zyMCzܔxǏaTK1 "G 87p z^R=FР"e_ʬя\nOn”y4d0~r(܇S൥0\Pn2փ/eǀ .7 [K1 "rPn2փ/eǀG ~5׍H$FL}ռe*"~2C59aۦsϝrCĸHrTn ?fغ%QndtrCDtr唧Lz[~->vh_mӚ)7 A Sr[ip*yc7+JO6v^f+wZVa[n^#1ܫ~dL94Tk=b1.Rn[&Ҥ6˔V Ң\dJ)Z?piT ;ZkZƟhq"dt.75(V ")cinG-yʴ-l)9+l#1MmAd,܂΢X)7D(9}o7csN`&rsYG_2a9YlERn)7PNym_rὉS<0.Z½T2[c2`_s+"E Sr3I\)Qt~g[N;[rs}[c2`zoY+冈qr唽ȽhiYn--qALr5NHy1>Rn>*RzZHr(H2kc@D#ʡ"e_ʬя(r|)V>DD?Rn-RzZHr(H2kc@D#ʡ"e_ʬя('@YpUя('@}6գvEmƥߘ2k?8@9FXO_ʬUr.7v:~b=Rf(7P)c=Rf(7P)c=Rf(7PNr~1Em8aGd{ϝ)Mo~r~{vE']EGo%m_i \CYĭaw{!EH )΍S~G$%xaM ݜ, V9핹|JW0]xLኮP ..(7( [&Y3&EfpEmf-%W4xې1 Pn0M N#^G TO˒G\%k1_g+9wN鴞Ӵ1w+_{HwYd޿Q;>fe5?iy4w[ԓq!n8ރL@\@9[.%#-q\ԋ%BX fY:}}J(hLWܯF(\nm _H[2nyv֡l8߃N r@rSnهrm)SXkǒ51 [iYn}`y\=ז.^V) Y rv70qrhi'cz`ᗛzm{JMuT\UrNtH@@rNvJbޗ}鈧t1}BvF>/WZ 3ݛô&fG:-ӴZ^W굞0x߆c-j|)G r@rJ-79Tfp:)ceΐ TXj0[kOYQܯG F _s ܌xRSf^n;EZXh Sr-.,ѩ7cwy^x5=<(t4i>m>{he ffXh B->ElKNVk+#pÑb1No999[V1  FeZئ5 f+M}\O-NYr>'c}_OQlE )r9J1g i(7PN(fznX~"äB]s/q+r3w~ܬ c:hZڴ]`q~m.>o_ *! BO|`.H(r/p]Uʡ"yp CEX>09k#(r|`sG(7P)b=T Pn&gU/(7Pkn"փLuʡ"E5 CEX>09k#(r|`sG(7P)b=T Pn>qi `&.u"XePUPnM~X>=ix?ҙXgZjnZ2rL'b^&ͧgݏ7|DJǍ + ]nknA'b;v,gq;\P * Ο?9,s<[VfznYk{Qd閿br3Uqi#);̼P^ϝs"`أiM~hӌ;zޒ^[갋)7(7P~)"1a/H(rtz؈]Uʡ"yp CEX>09k#(r|`sG(7P)b=T Pn&gU/(7PN孹 c-3珟?wLv3er ʡ|Br(7Pn '(r ʡ|Br(7Pn '(r ʡ|Br(7Pn '(r  Pn&erCDD Q 冈ZݞܠLPn!JAYCr^u""bR   RDDܠPn!JAYCrB!""(erCDD Q 冈 1D)7(+""bRnPV(7DDܠPn!JAYC4SnG7/ܠPn!JAYCrrGDD W "˓/77!TD @r(CPne(7  CAP6r=˭&̥I PnefF4#J+|z%ꒅgNDa]ssl͜k0kqr&/͉{8gr-[  "QFYeO>i.m3]~urEeɫ,GK3vIkxڥL.7s-ۄ-r('&6Sܲ'P56uI-<Dȕ[~M'WqEgB)칂qin[ z[n TXn <y*12]S+|""b%y™˗\z2:0p|JWtՋW\jRnqrCDDD"""b\"冈goo]2""""ȶ/m>>åhIKGKSK|7]^MW|ԻWU9xﱾ&.qZ} 2.̝PT:v%qjԛnaI*)Sj%@J1괚ܩ}Y;oh""""b08 endstream endobj 297 0 obj 35279 endobj 299 0 obj <> stream xYK6W`>EkHn aSmdSl.$k07b'ۿVwNh _o~~id_ht'`sgR^yz̋gj~njoM{~ϟy E//7^qjԃtQwj4ߚAtD1DawnTTDƥ@DD :Im^E#hr,J %/-_L4lmd:(Đ5'fZgI,N;IA@LTFfhC|d2o#{zvGTjL:rX?ՁV5'?O9i֕B\3I W}Sjp$DJ uCCbldiv\b7k\hRP†И%9`CBi5XIC%dGjMߡj} >?eHPv(5RJ:kfnGKsߩyVlk%48c(6 7R|k_`q=ň0tV(r>+XtZTߊ1~WZ M~)!0e?6񔉤]cEI7zu}XQSee{Ve. ~5)hLvRA]DTZ)!LۙX5$2 >6#e~!"W-+A&KZX AtQB]I^ 0)~}*mOj^oYVߤ&JoTgYBCYCVY(Z)V2Z0IJ-" HUi b[+~0r羧H2Wr%#v&uWQW!iv1[}Z^(Cx{VIo+xoiY,53Eme?s.XvZO$~O-ߦյvku*όSQTˣ + ]EBcNc4C҅ko+dc̵+FQ_L|ghczsZ?.oEjaFοR*ZK[Ls+-BtJ%q* ᗺF541hbi7=t|,:%pUz|3ӽU>h}ba8bV]H{(>?D}F}igGڢ,^ ӥߛ^JB-֛ۑ=zh endstream endobj 300 0 obj 1300 endobj 301 0 obj <> stream x tU+d`q{¨#Ab"F vdSI \O@|BYTDQ!!! e%;֭7:^=ӧ꺝U]r8XApq3SEb2a?F/؅ǭl;-3P t̴[~1qVq,NSd\0A쩕)tQ'}7]1y/W7W070|#2&!MySiK}Пy^OI^Kk^׿ Vh ]hrmy5TBS軼f #4}!DWq)7LGOӣR'xahxkpM^ [z/i轰6b^0Nx\|UGOy+#L-vR*f ҅ Uh'}Os`*),\OV2WD/7Ͱ0%HdbFbP -`ܔCp$}G+?g;^%'KhWSR~g\ Qi͕mNfEӅcLvҜ͇DvLރ NѬ3Sk;nN,Z􎵵g/̳jIJ0//PٓqesGG%bL(ᖂ˘|wMn :|n᦭?v{[5[_:ӕpw|>48gk>Z{Z֪RcEVϵc[5Ӻr%%ѼѲyM1]4H~/3hTϞF0?g_>/\bAN_:?> /XEA7VA.M/Qvw }^?x+\Y2,e!)oa'l3-'Ɨ=[<̱|vo;~!Wٮ=uGBJD+hFgLq[:t7cwAH 6.W!9H gm3oUmxk/=e33˧XM4/g^2=X:{xѴ3zwy';}#+Y9ӺlyIBScƔ^:~*C 'ߙ5/_|7S}ǾFd{>XQIzW;+~GѶ96NURQwP\`d"7]%)ⵔ7]KIiw+%n/tGV'ߩdݔRq'UZ|g? *ˮ\5Ybucho EeODθW}%C&ޖ0|5C׿+O4˾S/_ߧ{OLD%%ߝעFqM?N~dY1yn >NyY֔)ഷʞpcɸߗ$ .J[|*=}Vw]M4WT}*՛a~:Ƨϧ:^F> jҼ|.My]RjwltZ1si ڶgUgR?1Җ: s,lNЧleP:J~_챃[w|'eIG2\wڒ+ j\mVgN ԯ̆cQpTQ7hANG#<{@XIݤE&jgcz],=*~2fW줺F/-L45MlE7$Lú{&VN=3Ӻb%e;f#l鷙'dN9Zsu%ym dϓx}F/ۘ+Py0/no{$T}9:~gؿKC V:5,ZĞ~5Av]&o%7|՜.쏺2#'WlbC\-_5|*i{*2q֧XWL==պ|eiSnzu W[.uΘ՟>~C2T6q"xX/o"<(*5X 6~x~ /C}b瑳$n>Rbn a&^3D{THOWξ9lQNWL]U&K0Y'Zf֟#ʞ$O ӭ'[R&/kmS%^eMl앧|]̔1)hf^SLD eGcp{ߣ5mqwFk!RQ]L*{2yMqɴ@Kkv?yXy~;-SЯ0O]g휖fwCQ?Gysؗ!>mHΉ6/,`Yʾ!‘y{om=2 I2{MQN=]w[P*v{{M;{ \"aM,Lic8V|9OfZ(eY*V Z5Fv3[)շFMrd7Kq2{ [{e:fŷs68}x` me_[2޲?hYuqϹ]U~Z.?5_0Imܺ??#įsYY.IʅBBV=Tb5Pn^$qbI[cnޛV{Ro[ ٵ/lzǨ[~tL݊u ڵF}l|mWe?40g+rU=ĝ?>y/ǻxIjq~{8Br}c/z:'[>[χtد?a6ˡv_qۣ҆IΛ#Q:Ej^Y4bG YYΡѾ+|h .! tv̧+|Nw=Atي ڶ3W]ʏk'~7_$Ș"y^1nz0AS #PxN_ԛ 'Gӈ>&Ě"2z=Pn&PaH(U+cp>lD̨|h^BJ=n}51E+0 nwLzҞ K  {;`;`;AWps͇x!| q8 W߇&yA_Aw8yak91Rw˦Mwߑ O#1o{%&S:Ig/={̂䘑ksm*4R&Riz(ͪt[,zU^(ՕL]qFyzWLI[)H X9}-&mMD;i#%_rrг:zC^J^b ܴ$_( Seܒ,,H.Iݷ5wwm[8ʗ|G>=dY9NNmd/6ܦ|wZߑ /1|ۊ=z/wf_{vei4{n'sud屽̃|G<Z^ޜ{]EڑWi<[i\~5K,䥎󐞕]y$ӳ+{lw^'-ֿ;{Oߑ OޓwpAw1N;'Ap5;`;A=  k6w|8O6$k@HL|B&t;~ w |8w|8wC( \e\ A<ǫ;c,':ռ?~m(PH8ffy0U޿9A%E y7T>V.A.{6Gr;Hm΁6b6+]q٥@}At^WߝWI|CݯTGG<4*Z%hHxy\V)Z}:ByԮA}_V۾,[|0p:*?+#H'헦3 9M!hC=,4},Lf^Dkw#HP6}Ncdﻳ;NoKovC?¾#@ M:;C@&;Cdm /|w A4>ڲx;cVfJI/5o?X9]z~ϵ{=\@Owj0zv=H8<^r|8w|8wC(O8BM 000| 00 j߻@/ w'w$T;bw'|-|IO||#|Y3rP;Ѵ\&؟ʧJ5WLI[)'~rZ`ϓ[+M_ښR<. Ҕ- #nMKuW0=U&X-‚49˾k:xVw`KdY9NNu{x,}7MԒǠc{^|;ߩ iNꧦJ68|Gދ,Ҏ/yuH7]r 1^9۝I vo#Hh$ t찶˗/x;;bwluZEb 1nvf"""绺/VMm9w$wc*tB /᳸ߕߒ6 {:%Yۉ{T!Rd -n}T?| Du^Nh;̝n}<!J~m~ssBy?#|b;v}Vw81N;|Gū%u5T#Jy !="Wu7痘rm GSu7ssuJͅ8__jqygk}ҕy^}ZuW/l1ߑ |t7.28ѽ#=/g_e 7J;[A=6FsNweّ |;ntq$}Be|gH{AEw$$߁Hw81N{@Zwqq] GbpwAt1N;|G}?#\!|HHw81%t +_J\?U{k}4]s]YӾ"^R^ӗ:c(Cw֗R1j ;gZrUM%\t2X]Η֩З:Q ߯ u;GH WJX-2>(;nuݗ:Qc]/"^Lai ˸KW܉KĨ1N{]KQ)75Z=ݚƗ:Q]=-yb/VTAwIyKȁ=qٹ ;l1N|}T|Hw81N{@Z;Auׯ w|8w|8w{| a׫=0od# p8(<] s;`;-vkvY3G 9?vʻPsJvQ ޮ困!;'ROO_#L &eVI゚_GnΑrƆ𠺪ZDB{_K[~0ϙم6j >u^ΝOK=]8mj z .U:?M*7x{;v.vh@Ni Z]5Uo/|v.ol;W]N:W]dwۿ˷ϟo k-^|7N{Cw۷`hSSH}XW{Vko^zwٗm;}[ҭA;xKqwOBi+IMkkϕW؊~ W[_g_'uf++Ojia[9ntrQSx!M;rΝ[F ){{ӗW{F>Z5A{ij[N?ۿ.7|z})_sWv;핷odN6򥻨d_Tp~oUUWYl^}R{4J.WSƧfg?~ {wupԧ{X绲!fIMɵqqkOǺc4DmmeUZvƋ]mKxB}ݗ(N q*S{;֮x^V3zwX^Ә#'kOJO!v]FɵRaƺXįoJ;WYYazeLYSmdM^ؔrاyZUůU|nL9`u"Cxh.Ch{_)hba K7=wRZW(PNؔXw}{iBQh NtM7t|W0^Q^j.9w{^[NyxPSm+ ߽oFk/++,fa ] v9kWW|s/"/g L_=Y|6&BZ‚ӧwG]=;󧰼ދK<bӢ>bG_8dgmg])kPavJ#]h[NK? -4477UwTTZj7;܍6u7k9_w 000oM|Ifn}"B%~~gl':ft~Qt.n,^}{:zYٵkǟ?z]|&/7GKT endstream endobj 302 0 obj 8504 endobj 304 0 obj <> stream xUMk0W\XG3,az=MKIZK~Ceoi1h>{36 ߵyחw j~^U5d\D?2R_;rjO{zO*PX/L{WGuw%Q~sB%{`pe|{+e?P ?`ˁq6;.;JCf˚k59KE.EYd*F. 兇{32)0JrÉsśiu+HFh *mB4YrI+H,̬%LDv E ]åK 1tT&)-:AHVr$*搎U vκPXP#ʋ5+SJH_HO:¶b#lCotdT2N%jRFlKŬLJ,[q&VhRKb#u=֟;]@V\܄N$;pEb됌`,_X 2%IҔ}і, Y( ecx0K鐕Tȡԇ9bIe}[!i?W~8Z3ҳ(#c16 I*3aU%뢰݁.$~?p?ˆB۝ry{"կty9ҧmzlLI ycfPUGBP@{H1mյ]G_R]eԖ]48u@gSB*%/ a;8r^d*7j=t_` endstream endobj 305 0 obj 825 endobj 307 0 obj <> stream xXM6 W@$K6` âmEӢ{߯(R%;q`æ(CVݩn­x-^s}mg r;~ A,%I[SPMI>n_kxϷۂ>Kmw{o^nh(qaH:ܽ}> q{UD$fRA *}K{`nO2.?~\ɚ,Ø (\C_-2!գspRG=QBxH|`܀ ~=ӡ΢䀶9^(ԩp#S4`F X"X/=SpMZT|'>t VRI#s%"Mlf}d[kOQ,8DiYH«Y-f yI.(ЫTjH\C`cAdCJi` X-̜`b P%w2U ᕕGOpJ?,êO9aX$bTY/'%)}l%#z%A`;ie,\ڪv ._X-O+Y4V d9WoQw9,OF[Du.Ɖ)B0)FL[*D)uĢquZݧU`M 鳱:gLD\O?xzU.]TUV,B7"r;cA=Ti"'ꔸ6e񆢦@T(&{4O-=C,+ءͺE[T/\>!F$C/elg>S[#_F]*3lT,劵k/EY endstream endobj 308 0 obj 1440 endobj 310 0 obj <> stream xuRj0 +t.$di0v֍l$;Mh1$=’Ϝ@Q+Vנ&vIN ?} endstream endobj 311 0 obj 338 endobj 312 0 obj <> stream x}Ǚ7@ Kp{'$t\ow+k3+ l/.{w^Yk(!{׶,5#ɔ7$%J"iREZJ^I(r_{z]-or)}?x G{_67籽R|.y䞈7/]4aC.A!17=;E)>A0 t:}܂nA!}_W/_җ=ٶmu]׵.7KA6P;}vء ySxr']wq +?9c/2AW[P.ڑGԮ=/|{ zxߞ=G߸㎕Sf'ONa.z>{ٙ3_=t-15739=wyM#[ԥ S fg@?-EM=ٮ_=xMA?^WO 0XpccsG_:e\}xk?~&t6lA!V/E5ܾ}{x㳝Wetii.^{-o/Rԧ>.'3{-BuOXLkcb:sx\}C!-i+_Wp4ze_VWCo_7VnmeǎEcc7׿u뮻n`Bя~EO;\ַV' vbW8 X6A?~UWrAy\jL AA=Ag 6"f:5V~K/^|qv.Їz-07~qfV,S-e 4:uA9PfL@Gߜ{~iA1>vl}.ǟge={/ϟ'xC/?k?{صo7g9WsOϸ'쟯6=ё'"~,o枤# ۶BAm>u?9q;j:,v6ө^6y6l[[/jr!tz ڃ;5O]?^_uf+lP 矟-,,TΙe#[Ϟz[;tgo{uKo3gfo}kPSf6N^O?БKq޺+qو>!eAqb׶mβ{j˚ɮSJS۱M>A9B-&+Y?<:ĻL̴MS  lk6jf UΎ F,fAן$cù2M] p>9so̗ A; }s:~p١}n[ajΆ=$__+4]p5믿!m;/%AS-D+Z_ZԴRN`dGfD~r)FSBM/>3e Yknc7ܰ(P]\btAf/5tO\ٙ3_SW<T&. tYfǮ;ɭr7|+g]O\<9/#ħ>?_|;V8ߞoX[mǖv%Yg" %Z'߬%m5v_sʺg'ba#U=\ҝ A;_7uP?447@oփ%)}3MXB^)Aߙ->A׊&slS23OX?uߙ}oo8#o|~6;zt쳳??ptƵ^Z?~%oԩHV~?5];?/~zk_Zg*\ AfN>*O'!ݞL{0 ApϻW 5惇2Aw{ʲ|%8'Ae]q`s+[[ ]fML7 \亢A5_fo}_vllR0A?yrƹ3o{ֿ׮ģd,<-z}^믿|6Z2.O;MW 8gN'+O& c#v)yp2Oyz'.9?k3.kk|eK<}5:mmeBJ%ǍS׋]N)Ssu.fՁ9?.lc^%̮Wٿ<糛׿С>NqvX9~yy>pcw)~~3/{|[?>?̓k;&~X:t0hp[bY7t6!0X7ɯ?|0u8΃6d&x;t%\A~xc퉋?kO'IKLͿv'?PA\!0 [{}{߹zʞ{l9S^zwA\!0( >p[#!{|J<D?zQ *i-8=U0|}QwU.]CvzQ *4zC Π%zA7 zQ zA  n+JAq `t\Ė/|9)P' j :?w; zp{/cpP`Aze 8 L0P B@ (*oc@ .&:m\fo 8S;hNLֵnۮ=DZ'Fۯ%zO`NJ׷ -O:q4UtSJ\}K=pB wOӲ"eUEH!^r tS:(cr:`! z_V69ȃ']B~s)=Iw{k?X1䏆@kRoB聠`nOI.ɓrL|ٙ!O_+O61>%SO ?)dt7qCXsϽ! v~瀇JpQD4ulGJH7~SBKLw-`Z$o]k٤V鵊b_|ECvU}X]R#>CDҙB F,ȁT:y!SwXJ 61w );w-X=StK/E\/IVJfe-ot2Z z_!2™ `džԤBVyNq)u?D,];Bi}'[(N+ *|FS~)DӤ-OH;A)DJUw(jQTߚ TQ7v=zNANw2Ѣ8y#O1y6;h d[P_}{ )zg dm7h=AoV@˯DhtQv1BE:Ȉkk\Sw{^sneJV_86]K#A'(eH# L>aySTI;Pğ1|j Z)&ϛ/y߆wG=خRP1e*gMu(4}bٔ2m˫{l=dZK*|J:MWM1Ui4Gԍ>wG;تRQ[(5P`@3~SB! Kb᣻)86篟;R{P@ЃA(`=uft }=^('c G2w&(L^)p@ `@Ѓ3#x; wAgg Z}{;&x; Vuzpo}"D@  =,8 zX q&(_0Aշ^mK:t^cB.˷Ojn'E-oz:]0m" ͣ]tn=zY'׫~ JM$=64gF7AD̰q)rWr{ݮ ǭlfnUKQx˝90N 3l Jӫ3}~d;?(|&OM=<|Vw2G1}-Rkѥ2ߢ8,#z8'(jڽOxэs^[:!,A04H E+Z֙XYCp!)1w1M}nIzhLߚTe.ӤUWP8HvڽJw!yYUh.tIT/̅t|3E3: |И.bTi,pXe&г ^K+ҷEzrœ]AsjR+:E08Ƕ4aznDM6GW硘4X3wƙ|CEU\tuEQΪnsA@1ӝ'm~}%iѣn[/&2^dL.ORƤ5+:C0ݺ8F|Jδ;u#t;I'韰ō6)⦼ZZ>Yv_+$W lyU Jе3+BܠfȭhiAJ51֐=k~ )K@qU:#tw@Иta&T6U$>͂YQX^-&AI:{<]ʞLjKZiWkvZ%eoNݠʧ#tlu+.8+IC߱A0 ;(N^tMt"5zt[ c'YonMZ*,dРӮեsQY UkTV2|Nk=&Nf-P嗫o攥Չ1U~^j|mMl=t`,Pk+:E00Qo".5[":z鋣~\+8)͝[:z]z(N貄g.n=AϿ8homjX"݁nVJ$XwNYi.BSʽ 6 'D#ºm&MSޜ_Q}XL,tV&U /zH-,e#jIz>}@W ݈*{]ݥ+̩&G:Ț~)νg@2ONÁ 8_0;bxZXZA }"X aոjkQK s_V2gG q LӶcA.mvc B`s@' 8 L0P aOp@ `tX6#x; wAgg Z}c,v C}gyD0)"܇р`Azzh@0@g a4 `@Ѓ3C0L0P *o[X?t z8 =D8޲ u+u7?'@mA}H{/`{8f=[A!UEd5`6aa?DA{Rz7ewa8c0Ű05}eiەˇG_ɏ.="|&O>Lvp~a)ɵX/s'pc~L5Յ"a]<Ņ⯭Jo5E+04*.MUm :>̓Y͹S^.1`n$m R_;wp7?'@JY E27 ɁCAc%RYa[rPwZlsY܄@-)i,&Aa"P*J/'DSz آփ1T:r\O ?!-/@$-h%RkX $!dfF=)>/zɺ|o~N.[KPZZ)ڙKd 0宖!k| yR?}ީ~If1 ~ZЋRT>4&-̪; 2 ztXs?3dpw7?':usrnuW͂]AGBXOW u]w7?'@#A4K$-ߡQɬ9M]]=i(c'AIf#,P*ƖB/j}iL9nb R/a{,uv'wD.SD-ifu[.V݁nVJ$'M+ױ^J84k܄:KT,Soy9AeVO-1L$w@ ;sRָ CF鍨}4)4ڗ`z[ˤ}|@Ѓ3zـWM}f˸ / <,v~D!ڌM6KS/!l@{p ͂:n`nMQ/+NA-Cew> Q/wBpWzzh@0@g a4 ` :;тzg>PYP @6Zgu  g Gp= @6Zgu  g Gp= @6Zgu  g Gp= @6Zgu  g Gp= @6Zgu  {v{ -Hzzb"ũ;ww >m61BL,`,;sAammEӋ9.k;L][G_z/`{:;%iP6ɬ-OЙsn "8uXe$m]ͳ)GZ4JXAzg=LJIEhUö^W!qLLA4$̛Kz.D3RB^Cd^F"eNW;:u*ЭStE'.05)caQ`|vZ;SVaqrʽ֢8c"-GZSR"[an `Ǥzuc'YonMVjEZqD6ku\uBBArjeh]uj=&Nf-P嗻8]ݡ%e+{XzģSХtMy.}WZНfKz-*)c*gZ2~&i~> m17ƘXKgx":zub\[nF~4/=ҸFS*ou8AgV{ZWAϿ8homjab;*wC_߾BYUrlM+]yWŹʽҹ9S]o_G]L]|ԠV=FpД*.[=)BJm0j4]*w{V1,8ٴt容Jeq|*w{]ݥ+̩&;: zk\jr搩W:Pv)= umD?7{E'@ЃC|qAp tپޮN[G`AWۯ&p1&ta.B+WL]DžA ZW>NA77N[G~KX=h =+stmSE}V>zp{tmSE}V>zp{tmSE}V>I/@8 zg wkB-곺@Ѓ3#wkB-곺@Ѓ3#wkB-곺@Ѓ3#wkB-곺@Ѓ3#wkB-곺@U=Y:=(Xv$nk~{[mulMn.O.wTIL γN-裯Wpj^gh?~.蹼1zA[][yқGд;)Wpzfo)⩸tz1ta0&ZwkwMrW UГ]ڥն=Bmhj?7}p@q9ߢ8L[y #iꙮǵ1z|wɉm_X\eN'}%*jVcB*}($U]\!#Kmqci?t)M7}nt]=4_TyxoV:~N[_.:$6km4BH-R3K)?}yJͷ<3=Ac%RY3-9;l=2J`"@f*^5 AO^ 75Ux,/{u1m-ƺ-KG%L:,j\]C'תF"I^T_Y@U5ҋGE{Zizjŧ;IOqVc1/qS3ŃA V 2kΪOEAWRy2Se~炫.[M94hvL.> R:,tWv;yhN-ndI7URuX.=?Y… D b&eUY4A͐s$$KᐵeJtӵL#y{np@@N3azQ׈nKU{Fa zvPirG߰<ڂ>z{zםpcFӊSg8:JADLڪ 0;"4*|"{]Mq+S{>cQs6J5詸G#Bл&*gTV%,Wpz ^=٥]Q}}\m#̭C_XߺЗMgj[ --s$v4%)TPR]|?m+)܋3w쐟C2i E]P2)n_viA/TA=7b2'IUVʼLmw&ʒ[p$j[Ro\C{m$R HToooYS79AJA^:֢8"Iz>39zz└z4 B wuL> zp{@{ppF JmAݞO7=dz}жX/C$h t/}UZ4Zk0= A^(:Kk_AL4tbw *;R[TfD&UypfSsl{:|s>D]3WvhHM[w_;˛k, m!=DK!kHndKl}yGk&F#Ar͟&R 冀+ev $3J>j[)%Lx*2SgLun1SGّ._ɣOq<(T`sdK-kQE%_K!MM*)Uj)\*2SpUa @4G Ɋ'EZJctk"Ъ,/D8#+IiAj yGɯ'Vls-/iF΂8]i4]LJzzWHE.*-va}!· }^5ёucGEF莗:'FwƷlUh}&$_Tղ;tJ .Wu]((X=qC7E5]jdtL)H}u|E%(Ͽs*w;h[X5߮fdx[W&ʝL*j0Lū=->]1LhB+1 zUQf^Էl ;rN!k˽^.vG.&Yeg[y Ct S&XI zp t/?ӑ}5-k^t% kD٤*}<@{p9fvAkֵ%]I:-/()}@{2"TAzzh@0@g a4 ` :;тzg>PFлppF  =8S= =D8)"܇р`Azzh@0@}z =u0L0PA7]R+ICѭBA!} mڇ!PS!=D8 =xC{ppF fN=4¿rqf'a[^ʌȤC̒v9\qo]Q"CpVX]ǀ;G'cյn8h3=ֺ*RYjtI ;\=^3)04 zޡtܧo6}bFBmĒ?˞W>KMWgs *L.8UbtZ NsEAVʃrc xj5D@Cv]nv"C|ZIăRݦx9RX}K:^2̈́!"35%ewlokֲǹjZoKYp,SW 2-*RY5DTک=-/7tN vГ֪ YE*e$/iWңrW)8ox \\"W5IЭ |#ԨE*~2hս6L$t'PTQ"͒CYBR92m8Bӯtiɧ Mw!Ʈϡ9 hEc{HrAh]}3.RghUj?zޡk*_羪٠] .8ηZ75Kb{C4I*V4].()04ڭrsqRF5))UdRTa*^u5UtpyMg5$y^K- Uti4"*dY MR~;!k˽^.vGKmAf_Oo,c,^0^Q@`xpˤā9 o,#VЅ7N/()m(ZDd!]GxGE#t&鴼@ 3zp{@{ppF  =8S= =D8]ٙЃ?S2bu0L0P BA!} &(L> zp{@{ppF  =8S= =D8WJwMG#N=H}uUppFC`jPQ76޶/em Z0A9x =D8BEL`)54PtW<8C{pʙ Rj:hJ3A'v[_XO߰t'a[^ʌȤj%%sz3CgCrqc! Y"ыzqqnM"[cQ+LF1Wb+3-:d0 5C*쾗k{q0r>a&Yؼ@(\ E Jz)tU~:`CZq.gs4#VU {-Y%ڄō\ylY#R+Y^BHs-#Pxz"C|1RT&Mn)%fBWJַ`cFe1m#H=^kg19|kuoDbEtA;ނIn2> K׫..7tN vГFUĞS6RJ̴1*L - \\au^ `j'=^kg}Aoֈ֥Qgn6h0MƇ]ߡ}*I7bBЛ:!<;t0]6FCkQ:,Hx]UB~LvnzrFInpR22,C*r_N&L5&$UW;[5ϡH+W uxZDz6jn܈po%gƓޏ$VCu{5+K*R[{Ťb =1"Ao@g-1>-@#ta9ڵpb6Fk0[ƧeX-UÁ ] &V!a^~bW*zlcP .HlձAV `P =8S= =D8)"܇р`.LD`0 Eot F`t F`t F`t F`t F`t F`t F`e!0  `0 `0 `0 `0 `0Rq 5X:lcͫrFY:(+_<.K@!p:gz :ހA_<AACc!t1' V tzb 83~J7 r?2| `V):0 #:0 #:0 #:0 #:02V{O6ssۗ7̱|Nǥ҃=i-Ǘ87}ݓKWAoc/]8STC<^ <w'c˫?'H#}x>B,3MZ(Ҕ]Q>ra:JƒPsw%$8 KXO~@MS";72/糁pŸ5ₖK*z֕ѷbv|mqamu!+Tt8\.RBddJJ uX]Z;R~e竅YB#zUʓOyFHJwq(׺MГy5dIĥ’PYZRDHw#tI%]fsj A_e/^Mz?57~C= 1 k4': HfGv%+t"5\#tgti\ƥ\t-/׌ ;bI0՜vgC`Yڔ{?b)w!0徑F/gMU|]&bXRKA'_[ t\YbSŕ WKJ̏Eq[R ]˗W hR䵸BC`2ElA9))L+0 #:0 #Dos7_!0^̉ ;I4@(NLŀX9tss= I6ALE[>: & n:Lm([̕;.,Kfms|#i.n&lEjڜܐizusW:gy^ib. \pHGowb$666rˤ7z-t挚3eq ")ՅlRd0PEɴ܏5Ϻ\UMܝQ0^Xm-(Zlj,>KiQ5g2葩Ij!uV00m+:` Ѝ8(I#n}7RmߍZ% [@LTŇv}V&輣.VAA|,3ViwqbUPK6A/J YQ0]AϏ$wu#}?MS, qNs컊y<:AŢ8S 8952N<;Pn˝ÊڢߴB/g&*c+]ɲX7ܷu:a5].=-Ң|B^ڈ jz/V;Ey|D'קA\D^6@#z>[(:0 #:0 :?~ LWJ {5Pe4m&О-]I.YXvd눁j@An,՝[c-L%GmvK+RXZ==QIot"qA#nx;ta31|OnĻk3sa'ڽqEw8?!A#LvMgo=}]st#v>u1h#/sSz 0q x͙}i s){tk݇Dٰs-A>6qΞÛL&;j@}L k[6 m!#_]DתA`"xtA߹hK!@Lq5%'jgK^O_,`"-7:4/WnUKfN != mWY4n>u?9q;j: .G_}wru d ]NOq½%?}O|L9d>YᚎEq󝒲?~ucgA/ ZhZ!芦C1,|`y^Pg'0̍y7ʹ@|P&t/~XfJM6G-@ @:Zp6M"l`ښz.kj?yC[@G*'S~A#j3qלNߓrυ~s0tK-&cU>*O'WqW>nGП齵ݷ9Ϲg35w=CGzN."\ kjaiqAٵſBAP xA'¼6C.%U2* qڔRW:^`J_#t?tp@:0 #:0 @߂ïÁ@ abG3 W5 ,ЄEp΂AChb&.+G]K,'CՅb_b9jNZ9T`؜Eυ3)M7gB6)A4A,r Ta3ru;E1`=Oi}~x!>5 G :?9}1 }e'*JЕPM`J@M u8ݢt(b\xD Pȷ/#7=QI ߪ[&YQy:پM i/!`LJg0ؐm^Ng ZK3!ܞSˮ5*0.irgZ?}aiq!A/g'>6AgNmm]r?7O"kkkJ3rH-+r_C̲k-Ccr$:W G  0X…s8{o믟|W^yq_~n},;ԩg h!`t.?;9w͔7~z-3O88@a :lojI9W3gN~ـ: ֿqASu OxAX` oja:/rA߿ A7.课z8B9L9\П|`o\ЙX+c3gNΆ|QP ߸3~ Ͽv8 Y"I텠,@a-cLs5=g:^u__8!nÿ5t~gΜ:=L_}]޶[:@)rg\V2JA$ ~ة .`[!L߀-W^xfAݥgJ;Hc~Z~om<A7.'O>˄Cű99^:: (Nْ̲aNI9N+=˖[Onh^-VKwR+d]XU(iHP(cR !nkf\āChe Ռ ( 7Щ~4?B %X,o\y >},xg05?w vNK^].\< ^=,ljѓ˸)^"8ntυ.-wkM$@)K.JDԽ͎-W9#%(> QCJ'PC| ]ʱ,boLlۧ]:J{Z.QP/Pf:lD)m7etqw.OGRqSb;#:5mn%eʚ#·%WA7&g}vϓO>ww?]?xguH]Eќ5d")v>RAtN>tqFEe_5vP*5ȥ,o{s~[…m\xas͋˗/&Oчtu kYSaby=]#ɨYЩ|/N*%)w2ZY|șK Eű*-Kٓ >0,vd1Րʚ#FlE@am.MJz!M>ה8m \]yO*n笌Oe t)_\Hꂮ\%,V:cNP^7s(l~;%y?RQs/?N6"b 0XG>ܥZλ|yj@ v: ֿAЛý$SqoǸ`: ֿA /}M@a.bW:,Q]X^ti:lzAxǣ/_D_LҩMS:,QН~P%b~mG !=}7*~PX9;tX<AW -gj0|r7 x/^ BaP}QW^dۖkbEqL~O>57bⱡzxu%oʔ61 ]m͸;@>Ula`=|jZM OЉE8ZޜEYԚm[,p%Z]|/v\XPViӪVR 2}Actu( U|ڂblܤ\7+u\ AA :9ch+GSm`327jN񳘍Gt%t[~"X2Ta8)赤Ru?tg`mj[G_z eu?RtWM8Hȟm` nzwYJCwk@am(:L} A7z3ptd 0Xzko ATA7>paܹ7Ξ}o'_}W^`>}嗟[_?s̎3;;gt: ֿ1AgsǓ>P ߘ}=O>޽wcwiWyf J 0P 0)A`oGh#:0 #:0 #:0 #:0`k endstream endobj 313 0 obj 24591 endobj 315 0 obj <> stream xXMoFϯ] B;؂'ߋ`lO>lS 7#~^g̍R֩ˌ(J %>i}Ld$7ؗD(S}l +yפSq&_@$NT gz RƖLX|H)Fz,{L% E_x*$7!P z\=q(/뀲ND z_K.H&ZzV>:q ;(4(`P ~W 6Pu{ι֛.~j\IJ)]OdwGųmrUH[-ayZ< endstream endobj 316 0 obj 1400 endobj 318 0 obj <> stream xM7 s>g4a f!==M`Es/ER"ekAM{řW#J5G;};;C~?~60__? wpQV~q/_o5 ?~OzaZw'd ?2<,1Ͽ;]dV|vȯO2gw4"^g(ʽoO1iK(׺ vm:Bc_rc/pţ *3]83Ѭt=%q^;",~K6 4 `G0br`zl<+0;%GATCoW MyGD&[U_4PTYdW5eTduKcv+I!rJ1Di2_wJE_ֻ﫱0Jl*(M%LPݺ)!W1 ^i~Kd(e Qnf*:hFI QA5JU,S!1b).5*de;=m+dXJ]W?rP x ])E|MFq!R/K8"6-;Zip: }BOzժ'r6)~Б+ˬ\TΦDuqvɔiJR2Ss"T@|GЖ:A=O\lb"k0F irwiOE[UT$u2!ojOp.p9$ޭƼʫʼs2mfxu @bxK,ӔMV=շXY-a)h^&īywnw }v;;n7.$NAы&&xMC6]"PYK(w.7TD'kh#Xh`oV $UvtWtwsO.0@0| /QN䨸)A#tg2dw[ EU/^Blz4Aƶ"k[7&Sj򛤍j -g3|wI*ٌ _?rt^LXx2zz;31cfx =pX =צTfl9OhmqLa5v(YhtߠM:$> stream xXKFW`oWWK-ث1$CKNI&!$d/W?$Khڥz|UU4i\w=moOv;ɽxF?$Fܷ'R*ğ_wxwJ_g.Yl6/xsrݽ)!C==}=:?⸇dqy$ϗ1@7#D;d Q2۷E./jnb.tc ־!i/ϴβǟ?pmIp:yZ l+cr,-_}ǸT%yO"mـS%]mQCV6C֩jNG_W,}B'l(vC M}kp: xJz+@ _9^%*xNRׂܬ'8t%2"!7SNTsQ ׉/,%]X*"qH&OD`[(zj| d֦<+V)"C ى2$pi 1oIEQ}@\kn,zC ݌rAm~x6|)ɽO|p **DKkTŌ&ulod$]6B|mx}J*fJr|!/ PkЅVN#Y R;Fu122I ֨2"nd!wa)X? KXt2!xquDއkGS 0y<SX2vrVyt`nƏe> Qݙf>IAyb3rvӊP:翌0chQ\}>f#J=#0V+$C8SYRz#=W2K6#"ތs~Jjy/`ݒ?>a+q -N2<"C@12qnu_!r@K4"4 b35 6Fd+_]D1%4i&JkTh˔5L5 {f!]28Snօ ~1R_.ҥ7պuY&lIɝ.Ͻ?ELhӚ6ji-FvJ*㖘8$w߉lc(TSwX]]<oʅ{yhFQn%*N+l. endstream endobj 322 0 obj 1244 endobj 324 0 obj <> stream xXM6 WdEJl 00I@{ @EONK~ER_-;8X&)JͿ<)wk?_>5Ϗ?Snݟ+}˸'gZ~A~ӻi7q4s xMs{;)o/+O>i؇~=) '5'~FvRQqsz.!kpgJSgDWe]Nc0颻0MniG#wxRrJA9*к$,r!P' SnxN2О8RB0J\3K(^&z(CZ%Β:φ] > stream xVMk0WXGI!^' =ڦ$-ͥ![co&,h͛3?ƚez@ϗoӕU9Co,=W|//Q+'zBײ-F\]xfm~8W uP{sjp?n=O1)dz|,s?6=wd:'?F dit"*vv$/oeH0Ej}.9L&rE0Flyn ;M4+:" 8BHȴEtW-NDsB[Dҫ$<,$ |XLLZP"kh\BF 1a}hʆŽ 6O/)dڗJ3aE<VGۅ^Æfuhi]`2n\,z* ~ ƀD4nݔ(g9gy,D4yq.gCa+K -T4C܋F,B+`Gr$AU0׊vq5+ p 6M BS{PUe`- D9.÷VׂssK!ғnɦ(7ۃTzx5N5ϑupnGJKG@һvFu!3%<%7AvI&ạ F2 QR'kR>SYVtC":յ|s/GCdWZ jGߟ9TJ鴞^4=/i+$:]*_~Kuܛh endstream endobj 328 0 obj 815 endobj 330 0 obj <> stream xVMo0 W\ DI[;;m놡ݰ^GRGR;]QTe{iUS8@4|k?ݴt T3vO]hD_϶:Nǰ=}mo8(=~6wbnp0(͠Ӄ }\dxM^o=0虩Nj0`hmQNxˎw8|tm^8  x1NyC ҽ#a "*w)!_u%VͤLƌhݜ‘Jt\ǃ0&- Qqʭ.Pc=\R !LF/:F endstream endobj 331 0 obj 910 endobj 333 0 obj <> stream xXKo6W輀>D q zX6],6-Γdˉ D83|`7t:͞)/6Ϗ?7w(wE14wWw_7pkY!m؝^uxdT ~3::.6ϗ J?U<ۛǻ/S!O[W><A$Ҵ x6U9RFw"^=d]]cg>[0ŀ8!'\*?~ ,g:\}";N 'F%*O*򤟜ګP@W }EH'°0qÁhryt baB( Ew0QcNY Vg7BDJq ڛ^95X,_aZLHE***F6t C{>`G'f{4wQlQwX{Sܫۑ/U4ΥF]En,>WA%^ 5 [>do{QW-5$EkeV`)[I Eˎ$S)CWư3hAY %e$uYѯy %Jߏ샘BҵH.ΧM h΅)j#Q`MI)jM[JҒ0V,ҼXzM>U`AQܼK%͎(~ g L~Zʺ 7yE~Ӗ6y7[tXku2557P]dpjb-c G֞l+C_U2m{Mu{$SրLb L,O*HL;.Pve{nMPk1pj/ Deʆ| |ၙek{ oC}6!<6z H0vR`dV эVt#&X3Bu*;͙훒J> %rwД_ʚwhn6W7v7SaVև+j+`1qb+n-Xk#If.> stream xVKo0 W< exi Nۺah7!Y42EɏLt oiCw|Wީ (hlQ視OR)zgMX:l~:7݂i:SwG;u~5ܜ(} QhGqx3$yYhF wv,,,`#K= /zٗGʏ@:K[V!e`vV;<&ttqR<پ+y5.Pbb MXNosf4u1aI,1L%> FerH~~Kա$2-XE_C+䐢L'>l:Rur/ tjkYfutFJW%ӆuɕ9Ec)Vq:Xv1㭼0]s|>` V|yf8I?ǂ9؆M2!˦󸮂`q65Ic0&eݕ5M{\ZIa,T ]cގ}%G}[O)T*mE[-bDR9z4J`!a֍bm{\!&!{RDk;WUTLDXz mSg#zܠءuCaKQT񙿐8T4%b:C |0O]x;ztuEyAqW7\^2 &I c!` hWQRIW]S:ʝg17bFkzԳraY!d{83IȋOIYJ endstream endobj 337 0 obj 811 endobj 338 0 obj <> stream x xUpU>cABmH)R*PJ tmŻy} ʂݫ`kmKB6iwKLf4MBj2t+gr~=df̍,9/my*s!dA̻H_b*L62))@&V,3!2sPvl xS45FNh eSbjLYIkdf.JwV vƁ;o1uNwa$D!#t |~sB^{&40V@~9]7`V@g8A BR iHD,UO e ̌BA^&{&Inu5pVSv0ynNëH'hM]P}5%$+ȡ' BdKo뿾%$YU<ž.Y.%̱BJkxC (sy tr eB\ޒ@A OWVBV eZBĤ$TT@Hl3A CR ayÿ!K߄"9~[oEH\WC@V`UUj.zMb&<8MD~OƜ{WY+Ё{U$hjRq 8ڝ2[OmJfj*իP?^8PbPQBDU9 --\z9vIf:W_Y:%\=_pB V^h CMZr>m-׳7l_kx!MUߥ$6/fi?NJ /;DDso}Zd6|9Cūi#?^+ys+,k؞^>ˑߨ޹fk ׿ _n>'YX@tTE?yؖ%]cqbRhv fmf} B},o*&sީٳ& Ëk{6YD1^>F~>iQΪZ1]LF/Ygr?/J0+:#g_ @F"olWzw?GR6'6&hǪUZ>*y*7ga ^gG'rBkbB2 7 ұ/)3U*O\~y"oN>ҒԻ/ir.DsI7!yW#^!JfĚ^2 ԼbԫfK+?Tc/ )ezoC`P;kߏv|iIZbeO&ka=[[Q+?"Ql" I ]l~)]\zuzeDղVyW7Aϱ>6I+L }~xLjBם0PI6b+ܪCwFs)G`+Amkr]AXZ卽2رtIV:t`A^eЋ-t[NN'uə}lkYjk*ʿ03BActDҕ39dyJj4vKCzјbcMVNA䩥J%RXܢYy'fhK#E,5q(A|ԖojdQV_!E]]%g߸)_&}5@%MRHuLMߜ6Y)Ic1aN]A~{ǻ_%9y4G%R'+{Sx*51=l%كdJ! T;0(( A(RD1cR"4̅ڍqVy?KO?/7t֯gfTZ˶YyIUetp}@f"u^ڪ4¬kG4H=.ouw^ =oN.%%:o1cw=+D'6Pr猬\6Yfݓst"ukf&S*<[./Jo4Yynf^_P}M.֜$CWu0P);jkkBD Y#[}sdt̟L<.t<;8Dz`R;*rDVf)fjtP/QH]T}jacrAyfM'+>w^-wk?Wc-3SiWACМKACu3a" k&4j`NpPvt?qD@:vhBW, Z>M^NII][>ia(h4_~KƬ2;v:W8yA{55~ߝ=:y[0oOsGY<'\qkɴ'OCM`>9oe U𵑭ś'y}~Opؐ^\Re_v<#? |4Wyo7(4o€r^QƢ,U 9)h]C3@2ōb)7[fh;Z_c+b@O|!kJnP{&aI6r=a}xHFiy\_?{PzQ4 pסW 4k $z{%q}=F+OsOEܚl-ݜM$ILvô ttMCp)t/'-zSh !S{?dAoχ7 މ16 %UI&Țvp_Vl(kTdӰ)T*_ Ù1#䯳#GnC~V0IzvҚulg 7ᚽ+@;_qHjtu_.x[q pҪփи]O!agZY&q`Ii;~e ?s;0 ߫Hok(cWfpo<~8+2#,E/ڋᧁ<\+b:\@} ``xx 5"H"|ADX GDf"K_//*Gɂnsq}__ō;p7/ ~ E"|/r@*/ȗ`_# ₿;~!@} ``x误? 'y00<ןqKA 7[Ja7Pa_ kzæ__ѣ}OZKk}"03s wA#:4<ؗ}\M-zd~pbp@DFN$̢[MS{? >x,ptn$XciD7V=N1;+Ao>%/,%G!%nVl(kTdӰ)T*_ Oϴƴu;Z#)jTL ͬdץvG7aHu5f+nFI=벿]b+rINo+_a}/mD[F miξ T|?f;c_B5)KjN+{Ooh~fG"E]Qخ~sNǯ8!Gؾ> gXPy+ /9p4:;;6ׯ77CjkʃY,^Nl(^{Z<=!6< %{e1D)EѹEhw$#rX,5=ͯ]hDi6;IR~ -,/R+-f3Q ,EQ\b(6ūꪫMm3=/y=yܚ#>ˢ>$YDž!7j~Yq9`3ܻl"WP T|TmRkʜy~ř&PeW_ ,cW6< %!5U+N  GZْ` jzB]y-A_r _7 ڤ54@5A+_ R/z\Ekľt,/Q]+V yYᕋN> jhi==n뻋 Vד?G^!Y7w/ endstream endobj 339 0 obj 6178 endobj 341 0 obj <> stream xVMk194ZchoCi)IKr|HvhG3o޼ӽ(6hoӍE ;Z4;'{Ar,F~xvj[|"5[unw8j3v@S]PwɎ&N3ī6@ǴwS11aш6v|;Y2jWb=BP~;YT5. `phe O@<#FspŅIiK~4p DGAa1|jf9g{{_O[]8"BӊH~:qjoZ`1gq-w)*O%W;S%}vI|eC *h$dyMsanLÐ5"JNku] r+^ZB`ߥN3aR '$pA ݿ!*,WE#{>2 u fP^K!29J֥8S > stream x |h RA`QAE@0NP$@ZVK9<E El(k[#Lȹ}ro^ݙ~f|1< 7v_q C1EKR .0Ktr3Li5z`gBQNX0&Cdl97B&C'-U _[ je^<˲ ݞߔ^oX=?`?x %n=e'^NH`pΐ7_#O6;/:O Z#X֎Uv\%"SvlQfo^,K@P>ddJ=R|(ÄIyMk˂F|%q)G;̭!pD2Nt3@t"E]䲘%83!Z|MV*l`e*>TUQºda/?W]ϖּ˪ҜK'W'9%V̾ᄊY3ʦNI$‘Op>q,+쵟RiøGjKD*kvv0U XO@26 @+oƷտBǪ_̨~nAճ~3Y_\62#ٱ%uα9ϊ=,LPv_SNl t"ޙ퐈,ʁ`v-K[w_mFծye/,zn뙹ıUOv=+kzeDGKߕ?n~`@K;<_,c`ѷt{'*&S6l^YٚVHzׯR̜TxcXǂ1t mx%SoWPޙOaG̜X׵kCzjZlsqcAR#*f(1|ݤMPr0`iO 34ӭJv3ʎMye?E2˩,?ޡ;#k8),UKkҜ~LV`+ӓj^̨yi :bֽĮeS&R<ˆ`IɽG& Z} o\r #yacd #x;fM#Uz̗tm=?W^Jl? 1wTs 3s]O._P\;vCYr O=WS},XG [~ k|akcgNlj ; }5vbY*fq1SUK貖?cElX a)`BRc݂u=o g$٨3t}k˒$t`Ms(A\FQYU%]]ʑ*X#* Lv*D+!ULY&IY'zN qf7r?viZ?\623űp-X։ ,1-)U!t_+HY~Ľ /v3`1ZڃMi.g/߉00[->6}*vdb)YӮ!AZױ=Vt}yd E/U`E&ԨqPN˹xkT/;N̘Hn0lCkIIה}u+;e`vlb kWcgƼpX7x/6) V'IT^VIcj8W}>5`)w'Jߘ"vq=G# cbG7brɱG:Lr.Z2ٙ9rQ9w+.wQWteTCʸ!%T=X"ku`'KE+sL N vN?Crv \udy'w]Z씓*ߏqRccfПR'F,Z:īE<P1%Xeݚmy抇*'jufTfLp{unx5+*gc8t_wo)u쭅TBKRV3R>r{<lב.`uSΐOe*ƤE J˳N,ZC'3 *;(V@{h',>qW.Nvu}ιC]sܟ"J5{nx-%~,Xҕ#]+tTM:/:OYtAIdș Jf{D!X)x{FH{gA&'ʷO L{˿R΍;8眻]if~=)?ksUН1Q,juqqZ=pDŽ]{_u1v١XEyoξ-LoI1"M|XL=UK Rt~H67-{~#h^?ٺ3I i>ϸ"5bV\EG=Ի*5Oդk$WWB׊wַK;k*؟ɂL3ϒ.k❶}dX9n"~:O<5K$yh܍'x%S(]#bc^?Qp_n|K\{e[v``xm~|%;nޯ;Çs[^S} o&ȂevJkЮl-&[F8j<Ь6Iptok1ҵ D]rKa}yM?-~FkIn) J_6!3@eʃ4-7됰H|wZvZzaWj=@/@0$Dݍ5Xm|!+Bİ{%X w݇ş `?Bt+<*Xfua3P fة  |MlpUY)+q{z}ڪ 9~{B &I@`, &IhCBl\!s ٍ@`5 AtBEĤ@, &`JqvbbvydFh0]Hp$x}~Ĉ"P ˅f5(s"+w5XBAd'91v6G";SfvVYQlZ 6l6UTAu] WuQb`ui)s;[|*^ڞ= 0=pMZ>9ýUvuU/mżd.MSWڙ3w ST܌ʫ@P|Z. y#QHlY(TGRr9ޤ$}ۙ Mwr?l%vݸ17r^=r.\оAKj*Bi}C'v%}WbMy4WGr!>Ͻ[Z~EVF]7`i`xp+VW܋rOrcě`XunC}?/w(.-#ZP? 4yFvvNQ̀%hincǼ$熾A=uuutr]J(.!X0y {4=j f <5=7=jl  orep{4=jl ,_ۣ@ٕ5.G]2usw}Nh[8Nhs\*M4 AB (>ܽ|gFCתQtD'zAT>z~ w'־^キӶv`ÂNukʡio[bod󻤒S 47"Py3S s$dVFN݊ivt]⒯G{&A+7*F5򏮓'Z$hpJo<.s+TI]#DYsik$A:]s=-^{!_U]qːKmn9haEMqcs @È`7IKU:(Rcrwtb=TFFiI pñHf׽dTW! huy\>cE'O8T=FQ|>G E׈"5GF/oNHu n@~-X jgQyw۲8\n(,ۥA"Q"QQ"QQ"QQ&|^e嚳n@2!kv33:C٥,0;5,'W2 a, XRfK.Y]"9IYbZ69*k H\5؀[QuS†!XĬXDuGSv9Iy휊i) 9 *4lH,H 6XiX\ E,j畴c)_qE쪶զn{r)yNŃa(X^aX~0V:r׬RA}f7_(ݦZc "ƪU]"t{e"` 2:jvؼV2W15VfTOx Vz `+ud!XXU7sVՙaA.V~28Rf| {"fE{4w?C`e,%`e,%`e,%`e,%`e,%1Uj @H=XD{4{o$ ;'!XKۣ ",ۥQ `eb@2hv{A,!Xg\+|`$ X0 L`$420 ! 6PM.B &I v;/"X>"`$4 TH7 @`,YPQҢoJ  1y+:eǿ%"k` 䪬,wL] ͲݓM|B;/ @!=bOo">>/(v=]]{/ @!!}^U.W]v^_` `Eߐט.'9gΝ;{ԫW=BfCztX_MX_ ApN>uI{pԉJ/m?` L8'O ؀'kj\e^u=.!aa A$ᰫ9ސIIp&fؕAʗ FOhjjqzl+춘% n+%` puJ/rhCu Um+nir> stream xVMo0 W\ DI[; ;m놡ݐ^ɖ&i1U"Y֨IiѸVh}>ݨߍQyNSC#~jGb~67Zň>_ON>܏ǦǰZNmWsl >6{AC :jV>%Xΐv%# #/!0̮qQ . ^?Fȓ 1.}@=a 5[t . Fcz5 9c3r!z5c@*_ׇ$I gEօ֯bBʜTE]J'o]Šv#W.yL"xE!g۲n-fmCXs-\lr8&\Ϥbi;4 [@T& n'ScsR3R ZTʠɅ}8-XoeD`ID.()Pi[RAvX4I;R09L#n8r I a/DxWYLԸ.szrU~be)KyMr`b7fL5Mi7OMq1wrfa%+Y+Yt'v5[-v,*}kZUũ4hnfm`A 3ceCܬ(sת\gM40<ׁzSs_ֆR/? endstream endobj 347 0 obj 747 endobj 348 0 obj <> stream x |սT>RWQAE⃗bD   ШVZ ǍVh""QJm`$<6n$c̙g?g̜sddQ~ۍ'q@pF7{v3kA7lL=.g/򲎡ב>~o[>ed%76[IVS4oyMs[;-9O/]Ϛ@Uăi8Inw붍&- 4R:.8Xq}zc?Tj ;^VK dݴ2h@ZF;Іo)M]2$R$SFr΃ǸEI-|eTp]6P_w9q??aILUJ4 ќh׹݂ ql mR:~Z;nn787{oՓWHP+ !"I݌FϬk7i`|`r5`z\9qF]z 7o@'iOSD>zn$L]SULIPO} ~3.-]3Wǽ`3*WΔԽfOJIy_éU+ WLxܭe7 zϸۛV(54AU3EHdͬ &f*;S:ǼGdhW!TAGz/+?VBvj~=We)%3qOل{]u3B?Z(QIeP}r6 on8X@"iIy'$)M2^Ȯ{qPR^5^bRz؛=co.]): ^9x1?aT^e;9a鍌u~w{;iO|YKmcx>'!FGfc "S§jfJC*'j.w_A!=~Jbj jT=$esJIn]Ui[L<J4BC1U>mYr3~/SfG@mY S'g5Ne S*F^Sޣ,ƽng F0!#P!!anߔm8<q!DhjEyh?w yC0?:T՘rO+ӯ)Gq)U0pA*81i=qnkzչ+Z?Kќya7/&tlqWo=ѤfGH#72%8я(yA 6;itЇv'4-bEuz[4޷x_~7olWT_Q9}îKS*ek~ LMNYTrü3N6'ē:0Dڶ _#?(Ե}:gDe]s>9"z=yټJǦ$͓‡9NoE #2_i_5;zGfѷfZ)AmkҾ{HS``Z_aWAVHG|M9U)Bɲ^Oy[P|#loJʺ>7n;i䤻eٲ{Gy8#}'wl<>:bs'%Ц.JTN*sPAUw{3~ojo5㯆tRU~SvQDŽ a'SR'Es:U&izp5c6h#k.)W؟y&V6 }&)xKń^UzTe\coZoU;۟q] LQ):S2E"L;3&& JÔ9Vq옷 M6} k兩 (޳pu{_EC*zy}]ۥx~vx5ajL./ə ЮVq%BeTk 6v8%ox?|%ѻ^ov6zǷ\ͽ:nÖ}xqյ{ n6`LbvLL:VA:FZ2>.ڎ+c̓s )o9*|JT:KO/Tn66L,v̟_EXziPrJx>7$1lLh%=}F5W4RY>]CLdu2.ߢGGĘRBF$z!i ?H|k~;@.S))+,] R~َmZKqv(n}N՘<\Z37])4ھ)4)4)4)4)4)4UL}B05-d fDco|F42b*oPr!S)d V2b+d*䥥yd@YP2!C+55g [LG(0eRKX++Xy0+yB+\NY)MSSpU"B W܍6 ruliqTS`ZcQbJW1s% ijțSRoy^M[li4Y !h v)BMq,=zeq)9UZcAmĔl؍ YHx7p.!,wS'D W ;Κ?fNAsO!!)!lBe-|Gma#OO٩i-?2]/Ճ hQJ)Rtzcj+$d o|)Ă))q,N,dmbo Ss Ln]YS4d5mDʃX) BN^&g4:N6M:{;q 렡YcATm[>剺\Tq kc46݅;X$T0zg5a#p2 0(cԅԃJ =S9C2%h-Ȕ|% 7t|ʓ'?ʃڀ \6 7jJщ&O)lK'@=sF #cA1cJ/``YgJL!+3`542+a(x"~PH&*."eP-zM)e=PM,2h8ӯ%y9ٕ4cQafrgΔxS֬֘2zLN?FYQLus aG@-d*:U eI2b+d B2L 2F42F)4)4)4)4)4)4#cj;@%G;FńX); (*Lh##Sh4[JFh{&L .\LLLlLlLlLlLlLlb F47R*d fcd fkd fkd fkd fkd fkd fkd F[n+W\|/pDnnŋ}UL(Ю0 ǎ 44jk6{XhQ"Shxڵq`r)f MM:8D Z#Sh7Aг?97O_>|3?//?;;;Bр й пד @ii $S52v_~e\( ~l~H55 hA?;:8ڍ^jL|aF n5ǭ,X 6lَ{mۛԑ#ÇCDkd FS5L`4VpNfr࣒ٳgU] 7/}|G› z `&@A'u}"Sh{ʕ6S C>=q/qˏ=-g9i8!=\yk.`)k?ʦYB+V5}g9=*Oő/B0ud 0_A:t(p@`@VVVXeCn40UQn@=O'{EfL^]s{r2U\xG*2v{<꼼Rz{4[}]&ɿg`Iキ__ni뎆2! LFz-,,+))y7M)~tK替|ӧ__ΆԼy*2F52F52F$Eh[S1ta*S_([ h)4)4)4)4#cjq#htzQ8,,72TP.CaS"J"`<L@֤uH19 daM:!^#@֤uH19 daM:!^#@֤:m9nw)ַq1ʼnꟻ=b/0z7c*} 6Úe_X1ŗdKq(ޘCWTӍ[hٙ6`jsK0EncȔ ʢRT d~M24i/%hiB˶eǤܶ)59I O)'/KcK>TŻ~Ka z}e% dʦ3\SեWڡff@x VR4*yK;biT~mT Jx 2=8o^&QX0*bg^Hټ% dʦC6fг"i(BSzX/a?Ş)P:5z6tѺlŷt|QO5 NIeSg!H)٭U,O6܏Mp#}G_Cy dhS2J!l[q1\"WGLRf { [$WGLCCaSb%4æ\2Ji)M:2Gc*vh3n)M: d R ru+986()VrH3pH1lQ Sfbؔ@X!!Ű`_++91;6dJQp$ b%4fær/BXI k:jSR4l[v0~),~#o$3 bӬAް^z)RM4?C5ZLIg5%X[X)`Je_z)R6xQ>~h]„){`MjQSuu{Ix ۲!}6'=~hmܔ)/l-"Sc . : ] zvKSi"lp)<#ukѣM52S"eon n۲T-EbŔtu JYOԗIOرFMSTb["[|ksO P/ ¿SabH­$=ZS\J466^ S L2l]eٸb 5bTҩ.6(bTرFf.^X iVH,ԟ2sKKCcC{Ĕ JLW˳mՎD|DZ!"SbJ:9f,eԕ{kK5CL(*5a*x5AѸDe)R F"Sb ޻zrZ#zOޕ)B)| ӓVW1q$(n[!RxI[ڐmh4^d*vL w<[-|xh Z!ۓ=ZW_WU]YbTʃRSR 򪐽T0SՉ75Vz+08"Sb:F <^d*VLUy5PCءҽ?jHxw]]ӟ)}eG) )):>5W'xT[^8煷F,^wx¿mdkZʬ6oKxkӯg3,ݟZ[kOhLxwCϱf߉ 'O4:q(̙ѳD-O:p$EhFhFhFhFh2uX/ŪZh4͟˙x\tr@knp v> stream xWn0+t.`EA@'@{ `iQ$-Kp%KNaD!Hۼ)zMOg[]>7Sh bD<íeZyŶ?nAM{ھDZmO;7 >z?D{A  ';M47< t@,ܸ3 'xeK!ru"^FCL݆ }ƝH??.d 2d.3gꋌ*^Y.xv0=GБD,qSLbm¾ČB޻}_XB}8V;h!ي^^E\எ)qsי՘\UL#nM 8B|Fz &Ra O) w!% G#Bg)BU\pr129n@qWk墠1wc> stream x `xiymŶUw[KRP#WA@D#aA0Hhѷ1xUn &MB }g9vg|~\fyn@-0|ga$F c U˴jaGJ d@K* m1QA_W YS w(dV; k<̖Ϝ'\>Y2p%W,aPqN$5W MZguAN>!Nuvc$wB,*|"\*,6b@US ̼=!6 !]]&=ez AyA[fkGt%e$}Y20d9HgP`sf%.]umzc”&>&*LTK_Ijiƕ CŪ )W2JH0#ǠgJBRÃi5]Uw}C[I_!A\1et)baX1}Dy8 Kgt+1$8S,O&%qqwX4xe:L3k#V( !AxGoJ )f38B22's13 |!貤Kg,= P3T_>_Qwzp:jv?ٹLN18#TilNi0p`::ctk?V\£M72}dŌT״_2P>?)c+ye@!;t.vQ;cX޴j5qvii0x b :KY&.1#1Z_+^f?eVaߧ~|{ĪTfukmqUTK#k6O㇒{HC£U 3L]:>+)ZAL!O0 oҸ]D 󫟛,SӉy~7ͽwʹwWp=8ě ]?"R#תPr)f|+d2 9/LGw sTCמ8K*[L`1YNΝu/5|yՋ=O>G'VW9gLŬщͩOWz #^wn*0|*)GE>*%c+u{.47|Jz 铜dD9B|޴naS\jӫ嬝$ˉ,[;1fb1l35ʬy2SJfbv{/RHE[=zef$f"UM cv<6oͮb?̢OMw/kIuQWt̵E#~ rZUbvce /#Yo6fgO 3Ngp<ّդ_-I.FmXM1f"&1;$ WTkUw+Ou?/~iޥ3T22:4ͮlp!}~jw{B1C;TVUG`[>ۺQVU ٦FjC;[}^\W|T[gVf.0qXb* *fyΚH|쁪+玭5 o.+MzgcM;\Ⰾ~&gv9vwrzrN`tVsjZbvɣV |[IcۆyzV=#LejDO5 ?6zQ$g_%bGn}zw1eA/MS lY ȸmX#{9srN㌩B'\5g{M̼~|杊TLb/yQȋF]Ko٥Ǩݏ^fGVUө)tGW|u\>1;g%g\~r&fGMgE{58%Mm=ekLndL7bɵGZV :c]3/y1]60a|8] tǤy|N'DC|Y?.iL=$uGMl%iS\|;)D=՘>>ݑʗYؘ_OGMR9=UiLziy'{*-4f0%ͮnS=&t}fuk(3+?\IY4-lL/uuzaʗɽ43{w|z^'O!5W$liU_'oj~̞*g kwx)fR|EW1(8>wdK>6O&tMܿbo|o׺'3dX50i]/BnD4#f+jv?W̎8]i'No6[s{=}YBgzda{%2U֐݊fwiG ;<##?x߾^s~u|nErs..pq-̮T1;mj#Z$c`t8mF(8#hVϨ$M>9fo N&=;ٍDXqra*}dF j| L#RIs{k?)Ggv߁AP]zf/N"lu-=i{+ïO5{[KqefיqZW\\BDMnٍ7. ug1S5…oƦZO5_w%qcgUsHǮg<ӄm:>/tWG4:>N+Khݺ\qO|<ΝSN%%Kȱ/^+|AA"ijכXHb7;ғI@,CR]Itw>zxotl۱B?pfשqZ/_N/ҫnṋ5kr|37rWQa! rnnJV͛裏4h uV#Bxb̎we}Ytv zibp:@u.|]s@/Wn8ڻa7w$˽#G;[owY~ի_uqw;.G<Ρ jk`k?v{IMm]#lmkSy|!H`]ݎgSRoi-3AqZ_z%ҥ.='x{̙3f G!չfMo%[w86u,%}1 I1!-+sD|JJXE"9g޷6?.-KS,~{A[IԱrۼ''9 \D~dWpޤ;"ޤwZ;;<0Aр;})C7 (;ZI1 5ԣ1>n͚55|>״Qޯl]){wodsaٳeРAdd~[]wcQSC^PKweˎ2\_[ a ,HH֮][YNOs [VTxW*VUלҤm(fG ]nՑ=%ClPO!6G #,ad/T\\죐:Dw^5ۯi$o!biJ_6?_2 eF7G&C{A\)&jFvġzҎw.w|Buԑ]_YPaW)7L.H^tSN:bGj]~du.~BR7ina<6 '~di{r8$vi3uo72?8esvfn`vlǿͭV[<ҟI|7O?Us3>{>?ZU7A{?p$SVeqj'Ax OAxė\HSȽ1:{i:H.Y){'ytν&!>I],[v..Dgf;K#> n'a2;@# _PnK-E}?TnY}iw~c ]Y4=fg|Wx<f彽sÏ^I?+߼}W_ePT {KJUrO<ݢ=),<2T/H>죐+.U[n"Vu~uL#Y>hݡ<ҞI= t:חv<"_۹g'*P1%[Tw@#c>/̏t|Z!ܯ[CƔz{ۍ4iUNd4|caz,ejO\.*ݳ4&s&U07;cËjHJOk/o333322f`l@'x !?yf~k>o֭ :HN>,? RȄ ~mmwI,줎em*f'3z} ~18 |W.P9A>.fVgqݘ э>IB)-Em}Aibv}Ӗ.s,1nv=^jU1w?kmGN/(>~Hη}mM0NNĸAP|pM_5noj׭n!c:tOxav VgAupZ%fNG:Kl N5+&$8 @: X=x :Kfg4 Df! Ⱦev@̮G0; dv$,0; Of7oC _ C.O7҇.`^0;@Fh;oX1@!`x F`v4xc0C; p㳛C;6g ϼM& "ZɵpF0;@a{~B %!ievqlv?}F_֧fH0"kvGmav}Fup(fpI DEӭosTJS&9`,*Y!NM5jT2 DhתqSj~Q7e'eR.E-`b0"MZƜUF S2 D

    -*=-9LWD,GvaW&,i9ឝfY¯i> ۂ~_ z9Y޳ F\_F"cv3yӞw>סC,5gls/o2p.iw0۷.`]a5|www<#hgwww<#Oc&#x F`v4xc0C;6  4dfGGH8~e@ev 4;21|fD r޸R+ 2*\f9c7C^D0+ 2 f!]:x `vR+ 2*f%gK]U"dU!ͦf%++/,-fiYiFl\a?UA0K>PKuvdfv|j1%_h7+ a?ut wvQ@Ov@bt0pK\^L&{I0In@Dj/1 :J\Ȫ2\E! tQ-V렌=X$1I*v{m˟iuȋRF죶W\El1̹OxGeyXWo~(WRn{ sx[uʫ{B酺 ~'Sy6;LuU4@MfyEʋ r<'jsx}8js ^t]:GYr)~w_(YB':'`̳̬Q *jXC1 AY_UD鷹X0̆)Ii:.$:gԐ zT=Kf0~9S)ۅeC]evNOxL^[&J"[`%C &e ]ۻNo6.S[)M}MdUJZ'1'I!+ =[%c+u{.M]fM]'ҿg2_hefdKmhv lj7\6e/-ٻtU{F\֟0;Gwcv˨O"%tIdczt NzBydmg*1yg#u*QUk*MyV9UDDb~8@tQwë#&G.{jʹc+2F:"ט+\|G^z7}M5;=DȟL?AuM[KαDg$=Noɖ-e~|Hj jWO8y^ԳO1M7YzI,#]Գ뫏ɻkd6jddGvLM{-ZË +wbaL:Уf79c'X5oBe3Tz Vb#/yq٨KuIAipGFv9;ҒBH ޏN'L_Ӛ4mJÍrloT,5xxx#~d|ܻ+guDU/QT|@;.GGv01]\( ,f7^6;M;6o.(2L6K?!.qE1/O>vPQe15_8C(U3fP[mos'=w^aO^FuXWVWtWiB<'ȦVBjn:+![$!1enc%WQGT޻|GU:ݬ_WMNS{y;=ps~]Ae0;GB .'8ΜswjqZXKrv6j~%yZG_ΖzΒ ]W=eK,IDL9d[%'d#{Qd[DVw#?Ep8@ġX{orb]JS9)UwX= ʡ3"]󻾟bv~낷4UtS(U]0gz>x&Z%qyͶ*Z0yb7%DPVy5W.eK,}m^ݲٱr^Q"[ ,>aj+:47pI<:cc~Y>>N7}HʇV3?''7~bwߔ]iug=5[G6b77;eRk]RS-w/VaS%ܦVQc.h"iK{%-߇i2ax ,4{9ʞ^ONGnޤ䫤w|܋B[vM?5J-iS{[Დrf+c73 R((Zd a)V䁮)]WLb͕Zޞq{F C{&k]$#;bv}-BEi*ʂW8ZKʻ4;R/Mlf Ɩ-v>5 MT]oIbvlX̮[s{=}Ys:mW"S }L-t2n!qѻU2 7SzOclծ]/s+ԣ%s.aD3;bvLcav=v!PmΙh^>moQ}v1;3Fw 8މ`U?+wrA?zI<y;.)t7ܽW Xov pg(ta={ٙ|4mG@gdF'mI:o,Յ:6p2cmz˛/L3g\áWs]?}u?Xs;_~w/sse WtYsa݀Xm;ۅDrܳә۫ԙ=5QYv*5X^S*To?рIKI?$tsl^̮T1;mj#Z$c`t8m]lTˆwaa1OxVEmnjIiRE;htУ)G,Lq^{Vh٩ cҵ\*ߏٍR VTMoƧ%]G@xqYsXLjHxJ½')da|0Eze cm%86ݑ2J1'KY/Fv!Y¶ٙH SN>`ԎgSR͏!;؝F(oȉw@3q;ҒBH xJX'(fgrƟ5F,oQ[mpPŇB3bAGGKu/ AHY.QeXZ<ȖFgKZZM30_2@J ׬*#;u/L(JfM6ܗdC5ѪLI/ xfW <7^*a0;;WmЖ)FI)ɏ9n%V;MIg(RY e3;83){[Gf%`v[@b xDh񝚇)MQ#ԭ*)A1#]*?{ɾohvv&iv4&9n.x|CgGGpܘ,,/fߩLGkGkA޳$[PX]Tٍ/,TS\oIG,] ;%*h"S-7ƛ)cyNmvlfRZazS4%`.&BӘF1 "5c2*FS iL|Jtŗ-]e]+lq/MlfrQcުxMdksHf8 g^:n03F&QӮ|@%%:0)̎e7K#^oO@nUiDmGOƅf `uŏّe!نÌ욝:畆0dkYV:*F%/oˏqƐ'(# bzЀM!znh|M (#0; ~!LRo*ovFYODb5TexDR d:|TMWb|]ARߠުޛӌ3R(fggSsirPmhzώs:ԥjav0;rJqav 0ӛJ2;͸O1; UeRFL-vpyDle%f=u0;9x0;:D$N?rcv&;e7;Cc:]Mb:[!@[8͎+\_kK?q<^>D T1:/bP$Cx@jt!|>Rn*R7j/ǘAuhR-f "dv ?^"~]+"tfq`vvf"+0;0!oT:Әf#]avXf#]avX)##H$`vX)3^v i>PTmfvmESq0@n;~: EEHcLE_q0@ͮVR(, )* .yavd& l59 YjvyYU%ڃJPZ jh9Jq]+8;V˂b׵ʽ(W5f$vH͍OnXmjmvwM<;˂b N+wvΑ!N]ס`vxZV^1%.`DvcZ6מbh2+2;nJN]}Sv4ڮDNbdV5l憳3xU0;@4&;Vl˗ٙm4txkQ=9;pRN:eN(qp8{N#dv>v(hAQG}Œ wpm{vDQ3NP#ut6Gv(\ff6&is шcƧ-0ޟf|ݳsT0mZ51$6 fq9>C~t# _b?Q+ͼ$M}fgv0@ׅ:Nݷo]0|4 *av!('~$? Aq0@? 2V]ynvS L]wb~.0]`v)##H$`vX#Q fq`v1 WšݎgSR͏ň.`^a5tU_?͜0;f#y2)0df#yEf rU&Jþ%[h&"͓!&edK9u/ }IfRxcG] :8ov~8_P_(B?٪J\ `?=_ymZ̎ytiL Y"_eAJrk TnV =!]Pj3LmQʌaavG cv_tc@ZKM}Z ٯ)szL2=ܢW}/+#;nhvpI4l7z[@|dOijҚ]*~bBV+i~No->BOcs'K#vǟY@>mfC8=u`FUgvy6KlNœj`NDsq#3{'¶q7,FvaqӃ<Žg3lhzѧ[hH7ƴE bdk/z{x64?٭{M_HsO4HjK{'Bzl>jާ1@bt'~f>)ΐ@>]ⴌTJzFv8 Ә{s;Eag' S.\3l-|~/dI^4f˽4 .>b{3xNr@ f'vg|.ϫe4SZE4 :j'¶1fg2v)rꞝv2w(Ϫ߉v55Ә^yu>P1yt1Χϣs6y'8Obg0G MNsɧi:%򲹗1-j" vp|3Boߊy1.Fwb](mut*vń.`^[ ?wtnǘ. <"\&.yjvBaᗎ OQїVN`vW+'3df̎t Y`v\ZQHCtuq5`v c%x 9J0;@s/!avf_8C(qf7۲PY}E>`Ce\.6YeMWGےjJ*]Tʱ $mE~Aʖ|A W N4>~ fGjm~JVwStyvTd^FsȺ5`vx$fG:Ҳv g!p0;; %v0;1D[RrrLc0;bN#~.lR/ MKj֧= yGT+xb[',d%|jt h TfchHhv-ܳqcGv8Q)n2*շhwTzUD7o_P̎H\587V/ۄ#UM$H:e` #e՜a8ٙLΘxGlnS eIRhz'`_q7Uߴy&:LuP'Ƨ{N$2c/H ?hivԥ4D6. zrda0cG Μ :>~ԫxIxqQE'5>.鄗3#DEY鲐dl(##KM~9ovDkd?lv/[hjKN45Uz5ԕv Z6nq:U|2uc(Ncu=<>nHk &Mo?Q46jOjGijv~~SN$'O67%RCo ;?x]:2ycjvQsq D/asFG6#h1`O44,?3*jL:t;qE9X1ta/'M 552;\*DM96"O[TVgGST/*0HzGx4;Nis^.l/>XЗo (kZ[krj 6T}}?Wk,WO*! 6gu#tc}.rGpyз%| vsyyeP﩮**ʫ 0Pk=ֆ|f !]EmIuA^!yu!W!g웝6dKɽ9^1=sgۜ<|!L1J٨ $:d^ij;^(˙T<5%{W tUYjVxf.8R:-GMڊl̔ 5O-o [M -iyɉV'2XkSe)0T jΗZAX1;w]C6zBss%{ l1OJ1$sQL{ݖXtx⨽^PpTqU6nUqhu3[49hn&nƪOWZ㷅@*,XM3n!A*UyiVgPw,P*n0;GB >DS&Y:t=>*Ŭ=+R2W|8[4,hl=^[Q:b3xkjb^Cgؔ6Be99 0xus=~[$mVfT5:A^7e W5 `v{DfDu-ٳtȐ{)|LД=S~/M!]/dn4:Bښr[ݻI_SL ̜=dyRNzYvi,X>DnZc\BHxV*Fnit Vnp  . 7XA2B3#D%Wo,+e|@(Z"s )|!CݞKWVi&Kez13#fT~cYq0Ny$vzDxbv+9\'Y].g٠ԢN.\ڠ.SINJU$e㬊PJM,͎+[si\xˆhSN\*maeNNݸ`Z~k>PV1b/ݸW' 9,7+-F w Gjjj)KRRbvĝ$eed|"ݽZ٥F?=Fοˤt:;=#l%{׌Nb쌌tS5˰K-VbQj!ţĶբZC@\WڄdgjgjTf#VsGTewr[k4G<>KWjn]*])Ϧ[*Ztd5Tnj)/wd%Z(kjmdF֖f+"^-WS[I0Z-d G]j R`Vh8ۣڍ6a-Lc-&D>[i l|m,W"nbȅ-fǓ csKE%);xP]fx%>$5@|ha-j7^'{u$iJ oer(/e޾R&RBA-Jp4m0wd~Uehdl5c!Rt̮*wS~6u6n;t[lf^,ym,1'ɚ>x8z(V,;\N6|l%Evuޙ)(G)UWc;.riYWmL&*5v}t[Xi$\K9zylgAJN @jRԑ/Z/rWܜRt-,NIZF9j..ʓvg[eaHytDrr(.NDfS5A+-F }p fwDرce(X&K/eJ.jPf\VLSw0~2m652Z0Lڵ[f8TIW3o+Emm+ږI-LI)Xha0ZX[f|pt̮DQQtj_~Q5aLv=zTI YƦ t{2To_yР?iKSS*Zr_mTfUB tGTha tGTha[;R| tD\)%gLR~DCuubnA H()I4"P|DNS"<OII%$95b%b beeZ-F Z8:fwÇ%qELW(Xf/5a帇Ղ;KY6)J9죆r`m85p{Zɇ*8ha0Z-6k݁0yA~ACA.Ev(\5Yk6U;Xl M@ ha !cvt_^6ߧOl.rb^~Cּ|CCGui<0ZXs0ZP!rppt U `vf k{zG:!AP'Sfw   Jx,U*5uU { "kv1w3AP(.ERlVw"/[q,*1]脺]lyF bZbv9yGv"͹6CHhG6NHRr<|-MfpGg71 A0iS̋:h^jdiLtDŽ J$Ų/Tx+Y%{FvA X6)Gɔx~˦iLU< JPŲil׳|@OmvAP+ *tK}fA1ovم  fA%`vAP«NH2;0; @$R\al5T7bXAEŴٙNcZw`4~*2zSDC (Šm3@:7#=; (C* 0;N(AAA ?AŸB0;Afҩ@$<0; @$<0; @£3}av jv $*0; @k[eυ Fڛ9( Ce R](([v^LѲؕ5C'ݺ.I,mMӤ'I9ٓs|'ys'{>ON4*?]ŋt6lذamL̃<(;̃<(;̃e{+}[[vH/PW/b_G_9ggn1Cp|K#[vڑ繣gs_Krc_GBp94^-RlĈxi2I/Ύh}7?|L|Qvt!YJB bD4_5a?羇O Q(;:_V渣g? nٳJ@|EQa_fK^P- Lx2ku/ʎ. C2IUj vw|O%LeG íw C2IU*%"˅|.NM¿|ef|;eG:aKfO&?ziTfl~%}6厗´$)eS60(ǠÔ|r>K1Wδk&l4t:EeשP $|W}pl㖩*aH2D,]vq%w@Ґ[$g8%(Q*r֚i+Hq)P|_2F@C_],,;t~Z>:TЋn]Sg ׷IǚTv3#?O#?Pzvs4#Knz)=l6NFuq)GzOEǑyh঺ aW <ՋuuSn:Xbm٩jGF|e5C#ݷYTxZ( F}㳅jP#(]f;5)H W-B&o%7&ߦ/[*a%j/5$'8 ~NSj |O$7WMÿ-:R~_ jw뮨_eiwA,} vuujv&>)>_]Pvjlp[6(';}&F(Ja=&<\p@3ti q (;[+&߮AJRk돨э􄯁"tj-(;Phߧ15 mFMk.мiiX6('1]-oRvIL>S$0ǹEukh!tɃ C.qrfb,~r)ʚWxVCskKzt/6lW.(;:P?ЗH6@Q;xvmޱd/>CPQ_3EyxA9ӽ\.HGLÿ}_98%#^\%gKc5Yn_G;ȝvEQt _f0);5F N7OL£0/3#)6fdl#X |W.0/3N:Nov(l I^[ G48|勲 ? 12 ڝF?盙GCkj/dⓥ;wW1|Qvt .c򛬉|/|3t7 . 9gʥ4|勲rpmR%?|٠v"@N^ȉ3l@^kڭd(_.|*VVJk!{{QiPgU:eyPveyPveyegR߽iyztMko|}ǯ-7wlȝ8ZვVnڭL%y,v-=1l,jmiDpYnVB{{Pv _l'0Vf-úinrk-`Su :MegU? e`~; endstream endobj 354 0 obj 27855 endobj 356 0 obj <> stream xWM0 W4%qҤJ-+m8 N g?fg$4L7u=#:m~= ^ϗg#[QFsfW~ߚW蚷>=7>v>~>7Щ}׺=?BN͛sHO6vCz0 (tp~_k]?LD FtR6mNzSYCGXMK (Z,bOpcNzL>!>⤜La`srla!%?O)O`pM39do ncƖ%ɷݔLw\0R l.RG.e >>c u3 iQ$$%;ׅ*P0]BIS2b6m6.qqJ_D[;dEȕR,;D&]+QN :AT$BU[y`QAH|:8 ~e)ƺji'7ߵkwzQT:*GZ6+o+/qu~wo%ؗs0m:Hx.7h;J9U)Av+JV.- $e8S]_LAA1Y&*I> stream xnF9}Kɤ@EOmݢ[4~籏!dq0Bmy쪃]j` >kOV? >>??_"Fpw5f qGW UsByXV_`~ſ ?ǣ2=jQuk;|/}Mo!~o^ B23\\ˌ@.UՀ0S\VHbps1&U^$$=ts>ϰ<X3ßi} 7!gF ׷$@Vs4w4ϰ|}(Ul׸Pд I5fN5ͦpG)83kDM E%>i` 3}&%#DSՉglӮ,<_H{G#t< )#$*X3g B5ٺD[i֣0z4>vONj2vF(ܺ%M<̯-afXhen)նݡ~R8ؗKثH=p(bb Vtdnf7 ] J0d@Y"5$"÷ma0"Ǭ}aQ#B Se DL T k f9LdC!AJ21|7! ٰZ:˹}4F:'ЩQdcBՂ2l暃zM}5@25}M KEXHņڦӬ>P(RvK?`lX Y +kgdV%-CXX=y6IU!TPmf0ea0фw LSBBc+WԜd vː C@hԦMfSַ䵾z^ƠuP Fh3'W]NZ%DfrĚ ˶k\e^J8pHT"(tD~\UVY1tPb(79氊9i&ݸhja׆,oRS7TX'yV"AWX<_Q!_$a3zBlLp&Ƙ],.E?FTs;$[c2LF$(}ЦWh d[9@K66ЋPrt-[yd\b8Uq' CX2wE`R.ŕt"Q5Sf1B֋*sjEsyuM6X+]gi⿛&b'd#)6)3X!Kjw`f^}FC曐X攪YCe`^>=pa2V'1eqƓ =-,wKRw'ŖujMƠ%rԐ#.3dL[W#]ZJ-p8Iܞ`@&䬶Ȱ}=ANnlK+W +t))J^P,Aw\RkirSb"f9yu8mbsV q!)hr_16{-M\ި6K ?h-vB&|'("EH.'wSsſώq| E7 <ɶ[ntaaZ8_b HQ!C*z?5T^ c#ؘW`:mQܴHP  endstream endobj 360 0 obj 1902 endobj 362 0 obj <> stream xRMk0 W\H*#NVv v;mh7$9mR $= cMcNP! /x~|9wdc9eɿa.ty9z &r[{= cBç Wrfn/zf(v++^i 5&*+PSBHdUu^vUHm-UKEqMNM~Hoĵ 7&R衖˄jiK^BYi0tԍK-> stream x xExYo_]XuZ4 b8!D@뵺~ %x"A.r" sr'3 [TLz&ө=T;3KUwfDQaNò)CF ʄIc)¤* ab]2a|=k&TpǵCwdqhSdh0B Z>Zx GY^]6/^o/s{bK="/ W,T:zM]g =pWX':ht캿S~HM-d5dا, R>![my0I Q%Ýj´:b.j&^=eHɣV(/ȅ]g_ C9yM@i/Y#d:7w;e.q bN ǤS$^ab-}$%cHRBS* 8-6tXIBll42j4П&i5:L4UzgrXѓb"BzvQؽ+? ) 4)CmlJp~_@b{ :t°DZ(F :nʉ/fso$Vuz_V9ɕ/N|nBŜGG:.yhlA7|9Gu2mܣ^Ȼ/#?yHG.~.#x8œ^"> {Q`/vs CŪw⣥ kׯ{킗jޚS̪N|5)?Q4|#aeO(poΐ?2z/X;#^{ GK-$d Y.sr"flaf&}R90ߑwc.+: /HLOu|ysvW|aRų*f)91}c:@Eޕ35\I-vOE؍k -`sΡ͹7C<9efC2?)w~aNX +-V1/.3Qؙ=hɜ 3G;/{jP{N7}i؞qۧ6/>9F2ni6nŞo,dn))LL h䵩 D/bkHy2 ӺRIaM~(ѻGn/|wÍ,&(["IhK+9/JqiGb>\xX/rn;hswYNIkaym9; ccc-Tt"ո'i*2~q*lM%#o-u[[?*jnҍ4Ukc+tҿ"S-U&Q]1kΒEXE{ٹbkiy:&`1XK1IW$ѴU:SaS2=F޲-kb-6&Tk6O=Z~KӏzT}Q0M6?e4Sc8ȵv{OVHڂ ZRjUDki{ꫫwjbӥt[z=iv~gsü{Zls (h#HTDʤ F?=Q1gBю>en* ~]:?cݽ5ñ[7RAe@~ܢ6%ђ?y]F!sEڑ.ӸzPW!Rֿ~zѕs _1GZF쾊ٟF3gE_A>A⽃Zj#,kW)չaWkUvskqƩ zi15{]1N:`&|?k<7Cף㫈q~1;=tt½3GUW섊l+K#GjPSbVݯXJW7bI&bfgeyZ [i'v~֒{e"zݿ>7-7|o[X'HT2όh3c+f=R>cXb\W>W>j>%B"jбص9Tm{0kaKt˔9ELז6ynOԭm)˯VȥN+wLEV`7'{S덌fڰ˨ Fq괝~<וol{2kmE?ޘD7,(um6̭=k7> d|N.;A?Ɂ #*f.OP N5Tr*_Y6ʃZ,RxwbZ<MZ]Lb1ךiQOzN }oR=| KYZA'ӯڼyoǾ,w{_<>sN!0B!㹌kɈ r@5jԗE޼-bn+(yݯXTsCæ&Ƥ\yFYtsU VqNކtޤJ1iODQ[G]Q2 z~ Ō˨0 6\R6}tBiaV&=xeߕNMDv/E?կ"ʘ^UojMx'ΊbU}VfK5Jd1]b*#3&]5y*xҼ~?=pO;:mUbN,v[ń<"esљ'vt }jqSxxIK>dMK?uڞ?[ş Glk^[?s+,&?viAIe23Ze$#Yǁ0yx@G@w#bҙKDm% cņKЪ|e3g!%AMY/4X= b+s ݗf?0 ,=IGU򣼠108}]sbTdEB=€N:?Dߛ)ix M";[ܿ~@2;)C=Џ'SXy\B{ܗ||iYI-&Ls! m}fC[:`kL*_dvcV!AsX#b/~ ,_`1X bb?lYYl0Xw[!'gn'2r)4c99lo?0'7wQ[bbdg{Z: Y`*>.Jp,_`1be( XkX @w7|.$o`1]H_xXdCbY0.$׺dɒ ̟?7$չ4{l.v©D bC,>4IC֩Sח帧OĞݻO}\Lt/'۞8A@k,"UU̙3ߐ9IyHPL`AA;1l%mYg1u%%oʺc*ү3ÔO tedŽJhs}yoŢ-}SҌzț] =Z,-Akl|Zj68߿?}?QJ21^yO+ř/e Չ2￟hHX( a@Jf̘e'iiil{9λo^_/:NPǎGO-wBFd5|(bNSgŨR_αXݍU;$+ś3YG}} +ab[!0y 6|X(ȋ@J6J"#c1Bz_*6ᆱu -Ύ°a!G2qo^"6l{w\q.1ɩ^% y=* / ‚3 G^vřg[یnmjIre:Q:Զ[wUdhS@Z%>^Pm(2MncQ 5e'+LNNYù{ '<9aJJJZZ̪UJy&%ozL+WƛW8U[_In?LbE˖vzBBO-ٵ+&i9 1rw&gM:3bb򒦯11%/͢Qm^䢪OwL"8 XlJ[G!Ž4L[&: [OYv{1ô=ZKioLJk7ҏ F"0չ+_1cdCyzQQrq ^hb6}qĩ|ծ^ ;'Ńx_[c»׎[3c~ݟش6Ē%KLŮ Ŕ3R^ilӗ"=u,W;72ob΁:d#hXlSڵ0V^lݎbE|A/If#n1D 3XC`vvHD!C'jQxC^C00%AxNfsRqm=/_X\L'(LVX^OԒ:19y-ӯqg'+-&i$ϯg: ['$9̍q];\v6t|uIMYL7sB lEeƥ^.ϵ.b? EG}^?:P#Gnܶ˶}8 qeﻇ8!exaK,_^4) ͝Xj1iȯD\%g^Br-396!>yiu1ygFQssMv,?8<:3[4nNy]3J7v#5vSXLPfQtLbf,FbbV`<}nff<f +W<4`/| ?Y~~obbbBBwoGɸq>#;abWS.U씾Ŕ#Vl.bx}otPm̥g۲nw;sw<,Ҍþݜ7*XQ } H$8 ,_`1t` ,ֿ@z=)9}'HzºbN =LY1Waimo,Q66bUaimo,Q66bUaimo=۔WZ_2neM1+ 6YY K\#-&ݵm[=3NI 7z'}cy ?,zzkw;cvYL7bsc , VqLS#Te+S'HUpmZRC0M{;:9=౑b$Y0:vҟvŊ&ey"Npƃb[)SʉTWm(vrNBs魑bpL;Ba bCA]+FMWUnG;9Ή X )v[LZ&p)qvn3\f'!]$C;yHYL?Ux/OG&΃'`1o1i.5vܡ]1~ŸD9i,f Oj1c1σR?x\q^jb ݲbW`L*Fv]\b|<8CXLvLPK6{fqM1zxmܣh l1(z<8??p b0kd83D$>Ut(`1g1|-UXx~ ߁lX*,mm?8XtcK 8BѾڷoŌȆT7.35vbC *h;J*LMN&, 0s5.]DE贛-v Q^QZ={{TB붉5a1AcW;UT[;(3< u1`-I ;Wtl7|Uv˽ fA1+b׷ V\ S~C͎䪝qETzSlU/lM:cMX AtL \rWW7]Lٱ{֦+_φU/&N/kb,֭_D@fm{ "#X(,]˨=% Ҏb2jOm{ "#XE`x^ H0-fc`1X A ,bXtc+AğX,.\7Ap,Qօ#΁8ºpxA9GUX H8?Ėł_us|XA{-V-$e蚐 Vo0:0U8g|z\~֮sg`1A8M-'KYi1jetQڝRO:|)anq1쮼7AbIlgJT7gYg1*Mz]u5.zs] a1A8M-QjLTO$2ZgtO|Kk>?%VQaRmn?`1A8 c;ayf̋i4V0cKTAyYeҒNwgа>l<.er`1A8sjLb.243^ZKtvT`/VKw]!xAbT #tm1i./9'M hcWe iS]bYL:<:.X AMSUL[|s>-(B6p#F!i@Ŏ4^-[{ێKJG*<5|!<./3 Qj1O H8 u bUa]n< X* ׍G$qTus`1.\7ApNp,F:I O0 u bUa]n< X* ׍G$Ey׍G$lºpxA9X̷ @ts> U|\nmrVf5)ǰ &,~ۗH]~ ,f ]+2Cz Ob迤>X,p_is/U!i췘{۾\bKX{n< X* ׍G$qTus`1.\7Ap,Qօ#΁8ºpxA9$AW" ?  A~! CA , X A7 o`1Ab A~! CA , X A7 oc1 b/~K`-f2 EDAB3L`1X b/~ ,_`1X b/~bYY[9fgd -FnpDnvD> ŲllD"fִbdThy)&n'K;,vT:-t4`1AX C7, o`1X Ab XLR?A-=ebѩRaj4D b_XTlBTߔ=6YLD&=&G{-#I^vA3[l^X!>Xfr_U(h9}nK]S)QXL3[>A=b TKVrɂDj tʹ!v}*/y[- ƔSg5\RC HBbt&RRN+SWFAX-ԏ d^,&m\ 03}&mnbd> #!d1jL&zU= A»ł , b A ,!X C7V[[hY @Gzb/~ ,p3o11AA ,!X C7Xp; bu`1,}CtRDy !!m-)Q5/3HR]UZH~ P؞yQQ Q}Sbi}ɏl 2F%oY/G״@y,QeGKŧL  ]mIGabIŴAHXUlyQ%; Af%*JuѨF=!1j侬褢sPX̗{?4&r 1A%T-NVi꠬QdV挢0A`&T-d":xw[b bgBbm@tV HGU#Zl}9`1A.Vk' HG ,!X C7Oр~ ,_`1l;y ., o`1X Ab `1A~"3OV[,#ѩ&Kܓ05xm3 i8XS]` ݉4Sb$FIdC: ^-9k]@9 fA po2, I?"n5M٣,2g-.r)4 HYe&UbRB׉O5^G\3'ibZg}XI+o%jr.ьźo0Xsa1X b/~ ,_`11:.b1Ohj1oX`7lb_vb-t4`1X xؚ(,&" Hf ,_\,֭,Wp,_`1@hSUWV订@m֡Bnr\xa1@hC⬗1n7)4|NeY!̏6oxҥ}%mkf'l^a%9>6 ɨ$c_}GqUM]Bߤ[ RZB'q+ ޯ\pNbFϞ=s̙SM u^,>b1| Đ{ӧOJ4N56,bw\osګ]b/(d?-T:miMMԩ5^-֎/lxa1@h3a9d0pAYN\8P.$ €%Yɓ'N(j1?I}is]h'$ BW@C5`姁ʪroxs4hԱSRfmNSBu/,m^]Lt/iEyxcc}}]EЋ<^lx/+|9SHLVTKY4P-N{ NT`6j~T`  OWH:_rE3ןƺJ1x G-w^[Ǯ&/lȮO#OܫkaY(D-ϔO80eXQ>64:*ʎzv8i3. ]vRܫ76.._% Hٯ<%,j؟2@)Z8@* \'N8KY㥇#ؠ>ݐ8cXԃeJ#A=Ӻv/,m^] _IPde'jjkK Ze $I )Y`OTp$ BWRG/ ܫKauhpԫbFkkkjZJ9C]ᢣ阫$̏Wհ 4)=fgWIၢc~!/,9rp[jj(8s ݵdy߯etbJ[iNքbЦ|?y|e=&dT2zvzCc{|zZwhǗͽYm׼K-vZUkp5XΘEVU/Θ5Xu,"t72,y[w¯Z#! ' endstream endobj 365 0 obj 14180 endobj 367 0 obj <> stream xVn0 +|.W$+ qSۭ@unX/Q"eN,02)O$MOmꝡ׀q.O7 ~lkH3HIE{TO7t4oP|T^\}]?T2 էpYݟh| gy`{zӥny;tR_9O.ǕY3|>}|+g) PBW> stream xWK6W O0X50CSmd[4̃/)r曏j^;à,=>_Wh GfDK{}K˴緷Oo?տ|n z0cc۟_ ܽp6aGJOfa^E~´4Mc3i~䝛vfhE9P(ReҺ\2 b4kI.)9&@a9k7a!}Y&+h_fa&0\?<(5KH4uBжdSRzI% ̀! g%!i2_s!kԲC,ɶ'UgAb*Cvc [d%ᇰ֣$Dr`7O5b>!!G+0Jpy]Q?0>H"BkjiN2Ь.#Pu\:Yyx(5aG=WΈq $J'~-3,UЌGl##3KNU<)gXv\y_YÒgUK_IRH'J@yQfc+`O[`̹2ȓ R:XYQZ_$S Hi9P7x'SIOlJ !km@UDCAm8;,'%J&vL=ܜZX,oX*}\:v+켱d̚T'Ǽ9][y|ػ,r47b\Z_Q @R&qMd$Lt6&nl9 cїw5;\V`&S̩-kڔФT.}lŵz JgX@ߥ-5,[O↼-lbja c"},"7N^TPW\ akߦ3SMj RMfτ>lKm^oW endstream endobj 371 0 obj 1184 endobj 373 0 obj <> stream xSMk0 W DQBeٍ$6xgw b\c7bw0߼̓wl8u3InVs,R`xن}ö7Lʚ>z~{Rr-Takj1U$.X-6Ẕ*F&n/ x$lznc8Vǡ\N>rC\''AmP6ұDmTjN`KMaJd%{KɯQEms-hZZ%I} (,TvBzq( zo:eC=7ʃoGu5 X7jES"ŐoJ_D endstream endobj 374 0 obj 395 endobj 375 0 obj <> stream x `ս7GiU[[>,Z& (CR|\|EHH"@ $dcn&&r[#[|&;iu;-m?a_J7KֱW׼k o qX K=c:Q2gqY.)^*oU$Ys!:kf9i#(H\翁%#"`0 coMBM$D(H4J7 )&|SȖ{{,W<#p%rݿž⺩Gi f-[?Š/Y޼zJ@EJGڤ!85(`08 n2˲+?Ig:{KWi[6LoP[ͳ9g5uw]z v|mnmG9 733z_|%'̫Ηs|.b{ ˻|O:=--|MK>x{ףSsnm=(hD="2 I}Fd؂U:p/`0 6ʷˍ5Y>{Osl.ܯ8%~wtj pP'[Qs7YjG(xp/U`08,A樂_לz+h}DY]EVF]w:?[~ww\1n%Z&cM ƭ_q mbj7*}ϭ;r֝et[6qt≗p}Z lg-ZY6 YIVA HFeʲ`0 #(uW/u}?9QD׷<蕭/k{qo}ĕl[>Ɋץn܂$>ԍl5߹rwXhwֽRFR- #J+D4 {bNp#^<0AȆJ|0>˟DٲڜAd*?NxSp)9n8ȆŲOQ$lw'"v"H h>08M' NM^mi| D\]Gj~y×]S9'~?~q mei1zc-ފ|h;v'g?ٌsf&AFE{O]ܴOe'wd Ϻ7!y;"wdNCQ-Nd"Ɂ,):Ԭ{_O"eÍ=R|;i~lOuR±/}jz `G4(㸓_?UM]|_3ם?rMknbU~{VIn\.)Ƹe"n!HY 18.uyjgFyv$}ܳ,ْ~0SN1Y$E`0 cjB\S{o]]}Iݿi뢦;~4G[ro5]}Ngeq!"7E ֌gna?;-7yZx1pDQw/`0 6 rvϵ^eם_矕²~zpV^ns+ת?~S~qҁ,_Lƭzeu ,w.:;e@ ].=֊z7HCh 8zUw]M|Kgn܌KuvٿuY󤌟ZNQeե[u"n帇q.>qCjs&108g `0le?6H9;|[6Zl&bZϏ{7F/у׈-N08i8_408~`wu׈[ ۏVq*$+oXn9d Ugj,g]mN42SٴwK`0 uPǹܬկL|૫ɂI Z^OHFp[זaK.M b~ &_08'%"_&~U7=ᮓ.; `0xL\Ym9s˹on`0 n-0 p `08+nY$'/) ǹf0 :PBc5=XHYt|/]*[=I:y>!`0 Gu"c-?e{<f-)T?`08' krߗ@J2+XTL9eLʦХ `6f"L\u<33G0,s:MS!sL'V9+f `0XD0Tׯ9EܒaKq9(JQH`06 B>w3k`0< zv cP<&Ϻ˞ lHn 5T}?`08!G~z\E*uY)?*MBzL'_Gn&w>`0 ӉVyx p `08N,`0 `0 j-0 Z[`0 6G bA @FH΁`0 a-0 p `08`0 qޮi 8  b-pFLpȆ 18 #&8dCp `C,nY@XiFh#ȤB7@3Ny@ P2F$ +UܩҰƚW7kXgFgQW:!UO,4',7 -L >b򁠩G9;>:3t<ha[ü|>AK?>' ads|6]0(C2>?$mR.a)|)OR'~tOVG!#g-r >bBSCgV0txZx- M Yq$( Jk`@jR}) H-j >bBSCgV0tQ^C)˫wbu׭әU)EPgkNj5Wt?x^y)uMUպuT >b*MXI ::e-8nM1*nu7N\*RRO_./v i@ q)G^)C*uϫ*#|z7} )bzEjD[XQnGLYkT}? :$4ut;WA\ Mv}jO8:x#B:[zB S q Wвu^h<|b7.m z֑zR o]B.Wm5uuّu!'%BE§NK!UMe?!V%2vu$S0uL-,-)o0DMh9΃{t\o&ǠF:$hf[ip!:|=iid?n鱭ڵ6h+ fqIvV}Oҹ]=\=OW<e[g%A<]8?_!ms I%zI!'UG&:-G<=F |ZYR*T-뢳v骸+xzU# V=_Ohae ӓӏs3-3>~bjK`|Ru9nד$tPȝGX5k]^_̤kPV}IkW\F\YY~nWZh4[MMMX_#+fwM1^&,OlilP'"B"R(f綸sυğ)zܩ7 RGklR%#4'YH>aHcp4ZI$%INIb!yphtI벓[-Ԩ;BRت. KDlcJ Š6NhA n1dm85׼qH_ AV u0dª ґ3sf.|C+tzB GŭZI))_ RׯԾSR.1èuJ8"d=oӪ_57G:HSVZXw >b*냦*P9$.㯮y~zʪ*T ['kNԜ<;ɭ6j8 .ȥJpI_S -VOR; 5*# Nds>)gx>XH:'OI8j3@$QXw@ 13+Z:thah[XܪFb_6t1,#)\TXʊbqqH3ta>!b _ #NG<&#X~:Ptr5CN?UdƁ13+Z:thah[Xܲ>pr(C"'/B+r9dYv*T|iʒl;MyXZ8S&E~ vtf~W >b:T;P[:[҃r8ɎB든xRuxIn`*ʲGVh> 6 cuIrf4\ ֲ7O *UU`ܷ`wna'M Sl%hK*XB[:][R R7 PWai ኊǏN\٥6J̏,(մ:\ғO^/[c 1c_1Ƈ@ H/KVցJvlyK'mZdkm{+Wlj˫ڽJdm< F Ƭpf;y˒EY'd)]`y]Qq] 1Y,a Y˖2mmCL]w/SU3ѨEpk3IbG";=6>kvlxe˾x+V䓯[ow>Org,T%[cPPuV[&P<R?>H!0 6Gh,n|CüѺ7D!ܲ[%% ­VN(nM>5)JNOR3Xj,Jdkh8zίd +[J1c|h 1b=q!4Rǂ[V\zڵ7o>VRoްn/nQ|Wmwn[M-4U|CDә,4ni+˗$n^`ۂ+~3yDVx$ŕ ΣO4{zwRTQĵ.Ecí+w]gҌ}32Vkˑ# V'(鍤zvӖǨ88U*FeIHXP84V!8¸T僧+JNW۬eG[S[V+s0ڷe|v c~Kn,)EPJTғNl"u@P&.,FoG&.::5vVʕ۶?JZII&%}sQ= iS({׎n UyT @[`08Glr>+ٌ\gSQ?KǵZ[S[l~>p99eVܒ2dS5DV#k {U e"oq ZȅCTv2K2:bLtkժo}`ŊO?=~ZkBZǝ oꆤ:n MqUfbT4X+eRs##-0#[!w]\Z b-Vv6c~t+4祦j;$nm=;_gZh_CW1XkwЎLжC֚5t^{ٲ[KG%%-A-ZI/&%=nb^[`08G_9ð-ZVѓnŔnm)st tδtҁ2xcCۏ 8ҿP7kģ9Qo$nld].mme^NwP{v !8ZE~{_0E5hTb))-!F;xw*~؇rfTa, j%NBSTDZ`p6st+4EItt[)np3gt< @ -02 (.𩳫gKA,?x飯_ж dNӪuOvtO&`p]HP, `PYnƠ-`Q6n5P](zޫA$OnZWJ^yŲΉ"o"^_Y'nG[ HO&ĭy?/bΠikïce4o&DL$T³iT>e>L Y[;tߎ=3u si|dK&~߽,!y9Egn_[ HO&ĭ[|ah[nlxŒ~ERz͛Ұ#Ndl;.N9n9Wb}e>b*F..B;s̗{l(h/֤?᷋+c!N_t{6w<ŹW|>],)I6Iz >,A*A*);O/yM;@D q+(!rE;bI|SI¤Pp+n~dC+*I7ٴ:/Rg]&Dk 2n2Fy@ dB 6vYelHUn9rj (%p!Zd"c7R%@pKIS-GvٳF]>'VԾ(4!E7QI@ ɄVuw=Ζ6Wcc( k1{(܌'^G2jw~h} Oѭ閔4AaÙ7ZgB)[ PeB {JJ8UX "Omeg3;vG@#3Vo?|__ 5M\?g_&nI+F)~,:7AHɄV!B|tˢ"kDZ ⧶kY0O< 5-Xi{uo]koF[txJqKMh[ PdB 뜉0Eu 59A1h▰:Tl7-Z7A˄"3(Qܢo Y)Ĕ|t( G-(2!n5elAe܂P2kN 2dL[܅9/Z_45fn@ =)25Z!Kz+ؽ{*mA`J Bˤ'XWay睷~ǺuX(Kb@7L[cV\ 6f-FN 3bo?^/-Y|m8b IgOa:TîWYulpWI4[yhX7leEz뭷HŠʒ\: _jzRn[oBQ&j}PCv-0Ct|{!-j/41zv:Cl;Fp+BˤN1kyq8Kyi2 )tXCpE/CnI*uqʘ &Hf@֛;y&?4$ܚ1دY&==}+WD/٣]dh)fE?)HQ)ʸ߂T![GАvt)6/O8DN\2Hf@<-Ih ֐NZVBoʹWw*K#M( ^ J[`08'Zt˿Gl 2ݺr>&7|vK~kFT7;6|Y:?V`2nҵ}^ Ĥ9zٞYL*f[}L}S[T![G2^ѣLk+Mb}=8lNS^ ÖQy*<;n+W#U V !9=6w^tkɧ'XIٓ)+W\bI~,'& *n+p+f-FJJ bByj+;ٱC;e` ^f@֢q nz7t";OlV)QN~YTߵXa\b-5ln/3V Z~wi{Q9x`QGI[ HO&-sF`ꘓp+>d]%/=}-w΢]-zmO|s֊ n@ =@ Cd%@ L[nbNf-Z %-'97cN2g"tp˺$9yIYn@& q˜-4޽kWo ۣ-w-Z %pKH!d2)2&&-m5~ѭ7Q ѭ0v-Pdd n-aJJJ[Q[T{g(]3A]ȇ~MVOB;)ʧIBf]4nI銲ӳ\:6:ieN) Yu90ֆUWTUnHъn! *8K|!іaeKʜnIH,lLK-a.""i[ \{Úvp:;JJ w[d)d*x)R&1 I1gFt+9ev,=ĥύH-("2!n-AnmdO}%[ovśenɟnVD'k,hԸuʇ];9|ѭzG^Qrvf-(;e8E-ҭCj7Mw?. n@FɄeNxV_Et'J))s8؜\'#HLKStn8S&AXe8Yaf=)ji`!0Dhza>0MQ1''-0!E [ZGQy)q+|}p+|![ЖݟnG1[(> >n -Zh"Q!ꝓa|$ Uí^BY//.-0!ELQVng^6&:v^JWxs:-b^;u!I\\TB\ ][Q%O6 &{t6AC[I :b,)&4!Imآ̳a0?*`&Q*MEkĐ}Y^@q۴LNG]p c)K"u83FDcƭ}d,nScn $J䎟Z-"#( Z+[RzeZ[^ν}}]vg}-͗nDDMuphl;x*@Z2!nE+)Ml'!('%|}@Ys˦Vԟ(Ez,r26[}2NeHxVɁJ°-ٴe[[ H q˜2hb;.5nS.1&(^p+pttvt7LOtng{Sl^sj| I[ HO&->nn!oأ_H nQ)<[) Jo7b52{E bȗ~pfj}\&;[-(.7v껓[[ H q˜BcVi)=ʴ⧶|`srrgԷn,_hZ2K%=*_&bnU6k~9lF.س(o OB@Qy*cE( .d (PL*)aD$2h\#Odg3;vh21%$[1[!w+>i{4܊8n)K^/E?z'9,49k 'dиf:VsB듀[fr;=WE#[%Kʌ,u?OSQ>UJ݂)cNq[0Ed SL"ܒOR[cUwP˄e{Tk7SV/Sg֛4*ӨV %9EF+di3^/~w[~Q]w/SU9Bя)A=R2#:k>ά5hn @z2!n6PB#ƠʻnKuZ0q.ˠx H%L4?n|CبCuo?nԙ^NQՄғ q˜K@&zj?a;#_a~rEotKwEW ғ q+Z-K0­vyˁ&g1[nMTYDvfhH{(܌'^5˧0$~aD{ռOtL/iKp4b~CH)pK [ umD,+}Ȋĸn6np$-"W-c# ^;hLa5V07QV4Pڷ}vU-?/YɊG ֶ`p8lVwlA6Wcc(CCx90l=47Nbq~pK_Ry"-[Kޟ9Yz_ Mjw%h*/Y ʎ|gAE/->QodgnR#],x5aF*pK[GnA߂4"`$~en%Ϩ* EV`p8s.Ntf/ޟq œ{6m3 Bˣۍomϫ׳ RзKtbYb׍*p+B>%{$MQ`1ӛH[`08pn0_ e*;-%n[@~2 n}\esU HQGH%`pnUhA#/(0ln.~^2V+L_J VPBLD}ʫ,=0f(E4[ў Np5O[yG[zoAdJܒ"W~jeYK9Rn:)c,Jn"pf;yAO=D/qC8bt 8 J­u/J6BVOaiL$$F`y<-XnmV8HukwuzssyF[Nɱdc劵_F qK+:/S'gQ/Qy{ MQ'bt ԻQ|6xT>|fVSZNX2 rhVXEp+ii?7 }v";2\2-c7-lU;[ɞt//WN.+_8FI0n!JW5Rc $n/Z"ֲX4чp륷⯓ OD[`08^-fy6~/vnk ce*OoNѽn@[k.<\8Ua[{1b C_\}_-~:bJ` 1BX c[, -Ql6qz6hOy7LT(/.=-=D1kz__z*e/fPc -0c%܋}}]vg}ux[dݴ|qi1EG9[O"+)O᫚:?-=&nU]hXǵ<,OUTG_GQJ]sCpD.\^-S^XFj(q~͛2HRDxE:(dn%E|snNٍ!j>4ge_Q8Қ>Z!8V[n]|\u~[v;34x<ѣLs39)6/K#fPpkL2\3ђzdi=Ne/:R1Xn5;je&U6kAّ/B,rԖ>̇crsl-U u/ޥk]+?u3τS1%z" M[A)ݲkв# m4U'-Rꚣ&-0c%e8Yaf=-X^8ڊڪgA7NzRD2j a!0V2\K>= ~jK?HUvrG-Rꚣ$-0c"{JJ8UX "Omeg3;vhE@&nч6-?-sn,nMoG|tˢ"kDZ ⧶kY0O<ppy=ipp󍌌(<̼J|bp pp 뜉0Eup+({gkK=ªy=Znڢ]Ͼ f?gn- (!8.[VPCp\Ffbp+tk}޵:Wvù<-'_Xr/}QU(`{^z%ZG)QbИИu\FJkWoGA[A)gVoeλMV ĭ{竏/䋼CknEsnc}gHLK[A)ݺ^D9[bG6­*"-*Gwf==]^MŒz_XFHc箃?pŖZU&b~1#F{nqUcv;7̷7qձ{2UUzo^Z7t17 ]hypPރ'OD3_2O<88Tx\;A]`?ؾejh=yͦ7}dN߆&OJa 5ԣ}]٫ҍjZҵ15emM}8Dt a74<|}=qK- ϜXROH-7OpOm ,=}`-IǺx#3pmT:—PM|Bm+W0qޥu%(bsU|+k"ߝ iTVwZ<(֤ZQuK~wĵkUy۵E ߳Xk`L^I#Mt a:·,!==} l$8 5=zx3H%ݘDSmŷ,ŷn(,lc (u!&6NE;b'܋ZOtfa=+b%dHMdg%T+viϧ䧩O.eVv%bnk̜8222wY(W ODbTBy#HQ䉜­ ^T[_K!NͿOE\{ k xP1?βR(pmL 7<n+1[jj=<@3+ŸVMxXMЛ6f;мU,0{GJtUn%r8Y1htV7I[ky. LK|*|.4M4n%E|snNٍ!j>4gsOh} ne?Bl\;GF =PF\ =KB b,Bzx044]qs#՘Jcu0͎JRd_sˎVwlA7EYDvfh/XxأGf<Sl^9nISsiK{dѬ6ܢ^ze5>w"BZQ$3 HWv*F-uV`_Cg\ gtO)y=;1L[pSzR1Qc[l5Ԍ76Oј S;|W\D52{E ≪bȗ~p !qy9~jjeC[1Lvv W] Nr ۋtinhԟDZy Z{d-~1CN|'ԷyIyh-EepC3BP2Qfb E[|DHc(<IFT媾x+~lcJͨZWw0uGS[7*o8FqXn٬3|V`Ϧ-~p !ǃn7k⧶|`srniÌ;&jܒTĥJDqcpp-+-Q\#Lt͎Qy!ECA"#p{n}ȦfnRHFDSXZʏz3r ![nƼՏw<ucK7'X. #ۘ.eI]:^h7ѭ޻URˆ..qByj+;ٱC+_Z?I.e עm,[~B[H3gOE}te n-۷5,5ꭹG*{{m̸N4&X1 V_9ð-ZVkГ ׻'J}>Ȉ"T$*e>2af:bcnؘ`Dt+XՉ#p蝭//Y -kiv=r/l3gX2`p"8X:R[(!8Z(qdn鍘[ `p6ytk_/XwvWz^ݮ7w8gԿkK6V_Ŝ@ fn(lyǢj7W" p anl|g{9Ν;o޼ 8 ,\_\{8(!8%a^Z//p巊%JkX(!8xвt\zϜ9388`eIgf7P p ana⦨X,híN3gk ,@+iѢE>%5 J{b)U%M!ߔgB[ p Vs/b.OZVvܹsEZpӧ{zzȼeyʒYnuo&JyvzRj~_vEX[`08G.\"|s͛Gz_yŋ/Ydҥ˖-C_zpK2`p0nuw=.m:SeՋn-ϨGˎvwuuV#^ݩ,"2@ ƭzG^Qf-(;n--Ihk…a-Zڲex+V ,P$> x|ҺHX ֛H[ p ͚_?@g9 l*ۢ[K>====^̙32<5seIAHKE=z Iy3 p a[Uw/{Zu!F 6ޭER|jkѢEK,~WXXk(ì*EXV[ˉϿǗ}EޡڌG\t7D YlG_x/g lK.vp|###<3l0ꈠ(2ܳ`ENg}KKc[[sG鎝Eep`],CRFu mڕ_W44r:Z[=wՏ#ݘl9^䥵^\U8Y빅YϼzZ .-PenU9οU5kuu}Ȇ7¨qy_zthwq(pL塟4H{:x\1ci?ec-hTn",p{_ aSlGZ^oOm(_B0S,}cMB^n̠qk,s6yt /-Penһ_"q-ZwUvm5 8WRVOJ^gNW`|#1nNOAӳpߗ2]7 IZTctG yOk=RI76nIPޞ|UI2GOmքK쑌ntwVLϜkŔn9n",p낻ג{SoS5^v_Z=>G*9ݒdq7ĻQ]T}i/mRs,kX gwnL  Y" b{*بE<:Lm>6nmo>?yg'}ot+V|붣ONys|Ū@ѭG|d~?>w96b-ohQ QH+pK=5hfsb={>JteܫM)'m­@T1~Sgzy8s/YD9,zvskƛ@n=b3aVw{w~D5<ٸ|Ŋ~w7]N[T3] ո:枀[7g1l0̲ۯEstcCMp8Y EDIbz l?ًYZꋩťZ#*vu)N~rawm$n=^/ҺMK"Y,Wɻ}=+ȭ/^?^n= n-YdEZEzUdV#SS%)Hgts \];1\ZTy9;5̐EX`vs3L ڲeΝ;s#SSNXR+B"'qYG$=^=0!p^xX#- 勷ʿy4:l=S\Ob"t,^1 Flgkq1cqn1cdttdp܊d1~솿:}+7>!!aڵٴfIPI .],Vr$=\2X-`aĹl\Lx`ܲ_m~WQw+ޥCCC|ɯ/_RĬFMNpKIKXY,[:n0 6z:z{ڻZZ.[z[eWaǫ>tYLXXm/|wy oc=ݪNqKb]p 2X-=Twƒoȳ3 Nf흧kGK;qy/q<*JwƩx8 'Db0y˟nA ð6,cY3PHڟrܭX[:{גfkw'oaG[f^ GMPȒİ%3뷇"d[0 {-ASSS!믧|O7.o?qkwO#4܂ p a<*/UmyW'bK.^R@@R-`[~0<\Q1?pґ?w/?;>*nA-ܪȰX~aؿ̥܂ [r z%w{W5^A$p 2\V/3HlaJg887quMNNq_vw7N><c4knA܂as \2W$Z>|m+l>62L-``nRFl"6M||y2C1vRӹ-d[0 *ܪ3<{aXA%#>*c_|(ލ5quX-``nv)ԝ#^Kux(~rЍgK- , þ-crj3v8&Nֶ-*_a,b"x{ݷBӈ}V&.#,`dQTh+d[0 svʣ#=mMgpב8.xį3.YCܺ'?Iӭ+j{/s˾UVqjn=jZbP\՚_šZK ܂a#e j~WQϾGn.!W}h+]w%[go5IE'uLЛ+܂ qlմuݔkRk4pyP@o__GoO{wWK˅zYw? cgS@~|}_O^|:b!ģ?[]ܚ[>p 2X-Dv sB\,jm;W[V[YXcɷVYepwZ_,&kw"~rxsUVDđBq0Q>t9HyU-`y Y#-mR+,:TDHn1k<72BKuT9[sz^ڱ䃙(g$N9[?xxxߟ&x49Ċ[6ȟޘ*/JSՓ9̒ά.df갪,(RR &]a"SE԰йXlcvGp%~_/3޻a/?zXH>ks-ﲉ?*ĽJ5(?  ]qBki/Q[F%Mb"G2"`=]z{VE|m>Ϣ[rY_}Z?55~謚[nA-ܪȰX4oN[B .++s4XMNN ̐?lv=߆Ap 2XnVy V{RȠDnYaȥraqHq^nAMrvy%΁JGJ ^$9B|b'6N0bݩ9ĒL4/֨@\lJwah] ܂an7>M VvO<_JfߤSEO2|vTSsł{łhm ܂an2w+.%C%^|`k"zb_Eu(ISWyIq<ô-w܂ p;|;S3\Q'Tu{ng+|z 3/O\hT[{:#=H zEJ3F,|My B^ĭsjX?_wB}sζޮ{/s:--mmM-Sunfփ)_S!U򸮭Y 1/T葀[:n=f~W?)dv C:G z/.G';Oo(M´-܂ wqCfQ>?>50pK<}-v?pk2=a{"ͮ>Ƴ.T's5k޻>%OQY ,/+>g=[zA:1^֜>e)4x6WAF'JOYȋ!埃B2lG:` 1")Ntk a*wYb i(2uF]:A{Lʳpre3uk<kK 孏UU5O°X~*gYF~p[мOFGQxŞQnO-.%& m#ǩM$p [RT<: e(È"t=Ӎ [&}Ѕp:f!Ҋ$G0E,3`;yXP1iP0Uw+:S|e3\:e(k[]Uј$\-T'VP53{좃\ᨩo8צ|wyɿfuNVZuR,nQ=}}QqaQZ?UT7UP;kudU:ΔN*R4-h"n9HMM93J\@gT K:2cRTN쀚 𘚚2 (:C4].@k.t43>ƚWՕϕN9n{ꢃ\Z~ӎ[vo5/n&64/W_,514rE,nՎSU8?łVa#ڑ[IVZLDyJZL33p X^-it1/ړe;7PYcW@o@COCtfs5[sDd6}OrEi1|%qC䕻Hbamy%ʜxiVN*wPEzӥSϔN1ӽ[Y$p [RT]TtDI\WN)[,T6ERd(J5( GǢA ^uPȯ"o7bg})OKE)3>ƺu?Z]Iv[n}㣶g2ȍ4tII!q'{zx="n)^#BXJ+D$kRJJ&-e"[,Er"UϩHqC>K뒱!sxHFR.L!Bgs=}xwx~9w^3\陊*ۗk|l?N>?#X[NJ,dt n"*&;J$"\5mě%ZMb*s$RK%!ݢX-T `q+:nAqCT=9w-WzkUyG>G~zxx^HE B)(s-PY0Sqq(YF]y/G)ĢȊuf$哥B ULڕ܂f%-S3\#NƈMKȿ%eeğ|%?v+srRKYڽ[r"SQ! Yx(l0]'K&Oӽ[-hn0Tb{oQ";qdžN?ϡת~&x˫+8B#q˥ -X:Q0J@Y"kN&L$2/ؙh![,܂an7>M VvO<_JfߤSEO2|vT Х[Y\+g&YO/nAp a<ܭ\zVDX{]ğD~B,N$~@<P2$#HIXk0Q[1yK[GGWleWNeU;>*GRx#tXD|z /ðrQD$RAI1ħ h_UѕZJT{Mq Cv4K[̐"ua8(10"p T n0 hp 羥Z\0 ewq 睁[n0 7p r]-}anA ð/ ܂-u`]­==lJ=={:ja+(X2Qllnmy~˸vn` t<n|٥4?ZO:˄>D1ywNy|W-c*V5VzjTZf*r[@\+p{v+0(b,˘@Y7.]-u,֐210 g[] ;Yw41]nǹs].t<Io;p}'w w1x.m,m>]n&['qmҀ7khv0j&\-S厶*Ivaf[VJSNjl0o cirma-|LKm[@9c! uc!Ky+vKxvUn6vn{qSp r]­E& Ůօ //gv˵97F.4re auL$QRUd .@m͌@c6A]X8q|$pNNF'(.3n~#o&G<|>O_lvC]&p r]^-R [0vJN5p r]ǭ0lU0(:IWd9lL-uy[E|HhRPQ`4a+s}eBc } ; .n,Qs%rJJpGKT3p r]nVEEŒӘ%C%nI`̥߮7p r]nVy w~xnn_s3)[&~h[rtoj纇Db!a0Saq<fp X-Mw3`8 ܚ5:yP)W6shJݖI-㢘f8s|Sڻ\a n }e.Fa57jKE; B !Cؗ1UW!:T)1|+Eqp q8$OBQ2\a0p X`Sn9&q?oRz!\425JvX#$WCZ yQB>Q|Dk-m$xB{5WxWy, 511Ǜ !& 9ȧiD!~9*v"4YhX$xŽPpCl+,L+s[n0}[.R/Eˆ`avj P?>/z:T)Й$ 2++: l`Fc%D_lL6 ^\¶M>\+5 Wm@hހ _PEi.x<|$M.d\a䱀[0 {`_VT3~*2$$\?/t>q__s}l"f. .$*կ>/WFW넥~fRگS4_>Yk%Wy, ԟ޼^}[===ݴzJև/ewґ%T$uTE' CbnX,TpUl{pb2FrBgJ&Y5>*4[5_ZHz"\n4')¥up@-c`/7dqV'Ng id.Bi6PW'ç<]ǘ]n,%CLjM±.NIOV]?G[ _P$^+S?.NH$!P&KɺI9Jp|+IO? WM_r%+ ܂ l0n55571#*s&񰐊&U'h77 4 T&,O&6mXps3YfIfUUql{P5Jv\(*T6nA ~![γ|PDHST^^=/Is{tۦh殓ViyE1e ur[n Cuh֎[axyll6!du=o\tZ25;w6 50; L".os0sܾ$ 9 4pċo4K!T -l'dĝP$-b -9'^s ҄|X8+<+ ܂+Ia5U {z[nq33n ]yd'[m1Fo2jkkj5]'=VWԩRuT:EOEZ NN_vPQ$UH!(sKR2_'ٕG  }h▿/%3 ܒ)q˪PG '9XcUF k$)AE%%S V|U*%* Ѿ-c k1U4kR3nA]A-cvN!s1VȜjΈ!A܊9g ڦ&JOY3˅̩[ SkLIMd@OFGQxŞQnO-.%& m#ǩM$p [8\Ȝjqk^Xw+Ch_ ^EVS&GŅGiTQTAdՑU8S:qpJYV@Ys`w+/ҔΔyp'.nnUY;\s捻^[Ekն8*OUXȭvVҬV8S2bR%[,܂a[ [qYoITw~+h*w{eOKV^,w8:;sbvwˏk+Ug픾fB,XʩZ% (TORut҉3fw+>nAp al$nQdV<MS@:fMr'GN$'u׽A:Ǽ`bpV0n uAQVJT$U$p [0 {`qᘜm21av[ku5uT $y$I2>BNFw׼DNl;&n g^QIBK. lQ"a@*ĥ ۩,ȋuN A [B^,nK%g-XSeT䱀[0 {`qn1#=mMg֙7O@U>I$Ir7JNl ?j)}[w \[*$Uz8Y 1#pH&X\p\*g1y ͣI8vJ۠2K[&qJW [ţeNQXRR[[Ь&ne&\pEe j~WQ򾩽Sk('6SkH rsLcE3\$_ڒG'q1b-A#ۄJ4'Sv9m猣Y[,sf5ًŭ"0:rRJ\'K&Oӽ[-hr **2,,a++s4ŭ޾ZZ.[Nz#L-yO%6SHۛk+0OdgO[=/[RarTu1pA 0]-v<)KREp։jq gU;R%ĕX2ȼbgnA[ˌ X,9~0/WV2TwkEŜQVp n\e'Vȑ#~Kv>5\h޶&9qӮL$~C|4PG2'.a=:]5(*IK5)C5mxbsiV`[Y\+M\6z­*GBM\6xxnA )R7[3#,cY3PHڟr n%g:z} KA;K'eo&Tc0N.ט*O&pa6[$$'tY KG$0nz R-X:=VNb+-ʙՓn%U:W8bK'Mw <-nה1m '[sғpgqלKp69oX܊ɲЯ3ʹLU8(VNRub2l2yA-}a/gX蛦ɲ.旓#J jU{3Y={/ ,oVnn|vj4=L]߻Ūhdq?m.Y5zh'AIr8m7{ fM-;g/}& (r+= ܂f!oVR"7/v"#ΥEjػU tsTC}q&bw kd &lX"^yX)SlQw*V) viZ٘֘ߓ$i1o;8xxܚ%owڶ;E9{F9RܑsTʎB_ć Y΢IF98J2P!i0ѕ$i & D5#F,|!!iEt33}z](^m/Rgcrj3v8&zM֐r5ʁ\nw$N'9}y;9I3cIhD$4AD/Q BB%zD\_b[8Jn߈bqDhn&.dAYrJx#d"yAnVy VkFdr8I{/I1TyEisuis%$eҫHQ8XwNL`x{DVkSݹڲK"b(+8d0i;ybǑxl}l y'd󢁲w`"cmGu_k|NR]##ʪUtg^LG1P\3nAFˋ "`[sz^ڱ䃙(g$N9kλV\M?d?j䚯~IkdN=U |omp}0Τ(H9.&te.#w*_r^B-/zQ>U?GsnA 4p ]{}3y1p,_KL_ P,_e0U2X-=>&{㖽_= 10" aUK:!Zh%nA ðEi׋WLڈR-`` HO-`)></6u`-\ , xIלB ðwn$6r_̶)w֟NOOynA ðwۚ~P}>[Cɂg#9bW_['7=rzc >[n0n®콏v?,q(mqmw#Dk:l5E[]%r h)pdZa]\7u`8qu-R/pKrYA{[nAZՖVXysFYi=*ў`})v&w{c]W|BkurqUDZκ3ei &JguqilquP݂ p+6pu?yuH2o%nA,cY3PHڟrXr\?x0mQOm w6uQQuf-qj Z+kȬ5Q8ćũʣ1}zZť/|32X^ĭ'tOS"nAQ{S-)q][;N~ *|A»D{DJ"zŵyH27@x)d[Pㄸ裡cp |-4<4txT.yc$bZ¾cccms 5+aj`à#GڒJä\&S0+b).52$4ޕbu-AM1;W]=K{zțnAWEc?VEҀ+,6A)u.DҨl=aB4{\,V# p D9nA,d|[.)Z qK: S,:e4/c-+lgW,"WrA& rՎ,f"]v0Qa\-E.\Dk2=Z0b,5b|$nA7%I]-\2),䞐аeʯ=[L ^--kUIly;n9徘mKS?kQ τo&B7/z̸UWȲPU7c)X3 ܂ n3Tϖi~~GHX`?M'D( ʻnA-w_ؕѮ'/-Nr(qMgm7h낣˿4C-܂ p Az Z{^|́ –\Ϋ6\ZWZL]ԝtOcmQWd$uq-?r,p ̸pLNM6LTᘰu gF̎$[̧Si՞onx 8&n. Nv~_\Bb,@3 ,p ̸eP3::28tV4/[scEԂĻcVﹼ3¼vFb4zI3J֣-ܣXZ5䂼D'0CsD-)qf4Xoaqt[GdKc|jٺ,kQЕI#-NVgo&nA np9hqkh`b uV^VSE'RIiqӱ{Qf DGDġ-(!5kU27\8tpX0Kx->o8.geJy2nA RJki2#^G.QnAܒ`Ojd=Z p 撂ɩ)& ;a=ܲhّ֓y 1t?͗o!7G-an%y$+0(( OᏘ,㪀[2h2@-K fܲmG(ik:[~H-i"jAg]1 \^aJ;o#[1[%^(LEPǤiUdsA-`[e YJcCBT<lB acH $jӑ!L|2.OX,ؤ,\+ A`-m$Wc-mվe/f$9foEAWv$8 KTKhK@N܂ sbCMPe dQuhUV!*}زPqEE5@4-̸54?0A1NwWK˅zYw|©P[֓$Mֽ(LV"#"}%nZ-`2UI%*byLE8Zc6넭(Bi`S0VkSݹڲBfaȳ3 NVQԍKy3cV"_e0- mw[nA˘o&DPlxY׊ezX XNX-R)qbNK;|03匤)p+wً$ۖpgӱ_WuEYgЬ8^̒Bv|/Lbѻ- eЋ bCe c'tI(az0Q줢ˮ6UT7=l}hزP٤ZB4q +hqn&l[Rcv^UwDk3N2, 'nA˨n'_ZLyatOTypSQJRp u-4<4txT.yc$bZ¾cccn$p 2Xs5s M-5)575\wD.EY ,p ܂U-`['Q4܂ p AznA*d[-ptwZUrގ[Nm/fҔ;Z}<3 r `8 rQmgb?>r-!dܑ`+DƯN^fB[r܂aX-w߻Ѯ'/-Nr(qMgm7h낣˿dx xW*p 2X-=p`k|o(v^ҲϿw`{n"s%눣on o ðxr8&hL&*pLGp˺{3YOfG-`{NjϷ 7_RDd|L}[Uz5}i qx>f0DF"0܂ p aeP1::28tV4/[scEԂĻcVﹼ3¼vFb4zKT3ru#|]9Q!.f^p6MX[[-R~x;{z{vUypa8He j~WQ#쥱ߌd>w5lM(ʎ$RPlOZP22DZiԭ D r~ ŭ޾ JZZ.[z[NڲH}'95MnFfr7)^(p 2Dp)nA*q\mYme![֊<9nEhyj0;n1fa+r>X:8G38PW.!eQ(d[-S0Ŝv,+`fISVG-I-Φc75"2ά%NY+q%q`ͽ%|:SupK:&,JW]ȧ6I]\-`[-w߻UmI뚏q Go?xU %#V+8uțM > ,p 噆Jyeu4or̛D;_Kطvll˕f'd[-ptDJM1;W]=K{z}*hnA˟e^y+f A-nw.p H - H3p 潀[G[p 2X^-As[-(P܂ pe\oWX,YY&4,DÔCeb$K%4L jX4ڽs M--kUIly;n9徘mKS?kQ ܂ O)aPƨe{`CN'' JS%- e nXF pu5}HϞ?rGrĂNnz8!z qnA˧[ePr$1q]RedxuɌtIRUdq{@ Zr[{J" YP$GIt&sk.8K'd-i[o_+bc>.S@ Z{{^|́ –\Ϋ6\ZWZL]ԝtOcmQWd$uq-">܂ ȇKJ2tș; 'd}*s44lYlXֈ`" fr8&hL&*pLGp˺{3YOfG-`{NjϷ 7_RDd|L}[s-=\+.㣳  AjL ȉ[n[0lNLCCgG%n|мW~9M"杯%[;66A2 `v[H1f窽+~'riOO/ZR-``[P@ , 8xp˃-h , 8xp H ܂a[ wwrwZUrގ[Nm/fҔ;Z}<3yts QnVo0 WT n3Tϖi~~GHX`?M'DtVK2y,p"b}a28O0JVGR_B86;Dj|5w\䚢 .R[ ,pAXr~a_qe^A+9_+yuֆK>Jki2#^G"nA]e- A \ݲhّ֓y 1t?͗o!7G-X:8`|P:,JO,m?$[3 , þp+rhE=%t즆UD]FQ֙ĩ54k%$W#3WpIZs81_ʻ"d[0 [n&l[Rcv^UwDk3N2$s!= e(nY!Kek"V"j|_; XM7'r]CCgG%n|мW~9M"杯%[;66%[܂ pKz81f0hHݮNe^c-+H솩/N5d &#MTF'ϒ(⚏[H1f窽+~'riOO/ZR-`%r3YjŒnÖ$K1[P@ ,▅~ EF٭ i!4D^!*R ]1Rltp NiME&q1Uq5MǭxF#jYvTB1 _`_ ;w[p 2X_ć s1OgW\V)g+т.ixu(CFazP,vT PWǢ[4qթ SRZ ջŝuc#/w-[1Jf#} P£/kFܒts)J(,JQJM;=QΓ]=(rڻUNbI-R"*yx`[-kUIly;n9徘mKS?kQ ܂ [UQadi*g@xpK ܒIJ`@K㖋}P3Ns)GJ޷y\n<u▋jok>Al' ={_! 6~=opBJT nA-*/׽jdpJ/}k~MWbYHja/[ڸ%$ "G qZIδswqI#%KLB9aEm1w f="[W횏UDx{J" YP$GIt&sk.8KipknAMҽ4U^2\%DS1Rl(IfO/A01NTAx0Et;S%ŒST1C:W]ҿ…8IuA7a3'kܢʭJ-ﹲWgm1;Ƹ*\I:[E|<pknA[ {v)C\w195Ed2Qac>:[}7sz2;0o!3O]sU{]-D&"cZ%Neo,,# DQ]#d<{r|Xu[av \FGGzښցyKcnZЙxWc=~FHVFd^$N˒RnJL ܂ax;qf4Xoaqt[GdKc|jٺ,kQЕI#-lo4LJsK3bO ܂a[Cɂg#9bW_['7=r^p 2X-  g6H1P`/㞁 kJh!}VGR_B86;Dj|5w\䚢 .l[[nAP0}XP@EZh.Vmm!U_s{C鿰=W }ָ;Su'w[e/I]G}KnyI-` (>,z{~-X3ƻ<2=n{ty[mD nY`yf4H¼|?uqVvKokᖸR PmW@\GrK-` (>, XVYYܸeP@3::28tV4/[scEԂĻcVﹼ3¼vFb4z^:XxZ5D5䦀[nAP0H[=RbMQVOO eq ܸe j~WQ#쥱ߌd>oc㨲~]I|,^ <`@IX%)$DA0D"F3 7,BH `0d!$qҶ;݋n^wn-cs{禎ӭrݭuUBUpDnVxJE[ (baŠZHhabV-!s@f5wi5`[MgWLվ),xݞɔ>y&_iUq* x VXr f|@R*`]0f3GAn]=5Hni+[ōyVm]o8r7[2_2֩VYZ3ֵ=T'7jI*{7 a-@  $nܰ{C?oaV[OV04RIv"bqŲ+Vű'G˶ س'콲9~ RY}M%[/91xE'xT~>r fNn }Z][Heae^[uU'v4ZnH~~ד^zdl7TTnNjs.,-@r PŢ_p·ֲXt} [ð۝ydĒߥR}uoccc -@r PxX3 g"ASޓsv 0G %_, $;M4;T3@nJXݎ~ñ/#;"Z.yAn1-@r P`蘡swwEr 3@nJ}qVskjUTHZY~=-niS?<\α ;s3Nk57o\-@r PL5C )?_)~կ~k&|LUꗇݚ~~w3@nJȭ{^no]#{都S,kVi.=t@3@nJȭOTx4۔UŖ.%(8@%}! %VH@盜>߄wt8\x[e(1U򗡂m?CͯwR?QI_-a;fŊY-ءG-bQ*s[Ү1d/!H:Rr f˭Xݵa8tiw[wWЩndөVߔ8#Rɬn1 bkpInf[071s3Tny<1t,*Ƕ ?H[Cy[ )4 aDW!u3"WjV-T'؍ 5n*;@n [/^egXt}&Sǯlv9%6xKQ[j:b=OvgM+5 JkTяTOVܢKHjK܋]"!"8&XE (bx<.n5tnܰ22̄DZʖ[&}WOgcgk 2uqcu^0UmW͖̗ugLkaumDbV\ G"s %` [0THk89 -YYWjQU&f_*ND,κX{%ܪ8h6W{䕽W7/A*+?Z}e:O{Pp x( u`yT^˃ %_,u-Zha2*i`y,B}V]UA }m8 -:a> g~[f o bb˭B0vgg٬>w>T_ݛ~aXDB"@l0/r+3 3 %[vĚ,hܪb꞉NuzO®gi"*r f|XtHb!ep-Rs [0RxQT\ֲ Qy[Tܚ-@n [/n_k6pȎeG--%~e-orjfLL *|`rKs)o_GK(W_ v5Qw:JD%})g"oqMEar+Dxy,[(qltL (ba0tй~pP(z i2::rAV;tc-t7>N-TVDH&ߎ 6լ6ɍT % utTչ55**S$L@? JB:m [ocۅgUm䭡uUBUptrK!5"% ؏&&A3@n`Gu.2v9%6xKQ[j:b=OvgM+5 JkTяTOC[OQb##%,Y- a<- }E*L$4͕-`r6ioۮ7-/SWT+Z,-^Z*-E=Ǝp%- a ,T^n* K/d'"g],˽LnU{rlp={}Z}+˛ GkT2~b1DzDK'ʍL @,r+nUpj!W_OzJ{цPR黩/ιqoR-@r ΂-fa;?#>f%]PK9  -@r ΂ʑ[`T$zܡvm! a- 8 *Gn aFr8@fȭYnn!ɭ▖҈ekkmpk[-@d G=[[g\AȭPW4m)Uz.Չݚ7o.Px Un 2/[ HO{1 a7~uUw1U}_vtk<+lr fBn@\r+n%f8Qp[GRYQMY2^]V{<6;gP0YrkXhJ9<@  `'Eoz*mFJ2T͞scTQ'* {&ݬT3x7Zn[ e@k1w Z .^.vz 2::rAV;tc-t7>N-T2[ĶR`6VěR3Ҋ;cÎAr Yu}j]Ou".<W:m [ocۅgUm䭡uUBUpDnwq9-nP e}mψ/rAcihޥըn5]1Utu{&Sڋ䕚kVaG*{%Tzr™ɮe7Ji4 204 4 eh片<"IgΑ]Dk y;]d"r6#Y>$Ua6} M&^Ur/IAFq+_e <FEDZlghi7άۺiZŔ5ʳ#Y h N Q 9MNs:vH_58Dv1CDm4-|7Ej+Qe C*-4y#[lf(6YYDMI·c>yZŔmFPm$iiC&ewt6v ilQ7V[O{vl|ɘZZzfiZPL PGAn&JJDLXH-WJKhz ]|zhZ";C@ gއHWh7Y%-|3ÀA4& oy=-kɐ/̜ e ~F g0k-4B;*3Ubn[iҭ49O%HUܥ Q҄,4VljUŭ́V T-j?MVi4_l! -b{063l02d D+4d}$$&1M4ۥilS6֭b Mfh5F3G 6In* K/d'"g],˽LnU{rlp={}Z}+˛ GkT2'߃=*/xĽB{pBOqq .6IJNh h퓡#[Չ&Ysԉy]^Q$aC. rzh 3Z<{Ջbcħ6ʐk7}Z 4iT+Ru un3!_6nXhj5v Bn$B}V]UA }m8 -: O.Gq@ \8ʭ=1Yrkvv33lVXuTSMll,X/bq V@ 0 5r+= z]Ϝ;n-DT9Q#Q [@ Fܚ- FFn *bWD uuKVQrmPؙqZۭyZnE=Pܚ!گnW?YN5{c>_׎nM?{'W -n 46J0%SfM42E2o8ȏ519s~SU0dX2 ^V{^no]#{都S,kVi.=+ȭYa >Hf_ W!ة}|7$_)U|` 5D||A|>"$ Ða0dX VOTx4۔UŖ.%(8@%}!g5+ THPo}$ >F F^fƒG@N!'jr.b }1NW!Ð rȹ@_XnsI_: p0Qc/Cyn;_;FU[Ħ<1ܦ>zJ42n#9 `ΕP1q6ζNlvqv`dD+w~,\Dic )ͧN!a0d25bբ.,L.;TXu,J0UqѲm-k+{,o_TV~*g2wSt. zPh<rkVr[wۋWBB Yp˭;@f.n87n 272>w\q2tl9w Putexwv:t_z \blN')|;/u<{ v?>M%7?97Ϛ) ' ~*SGҕ 2  C#h[w ĎS zSK6MJMxqif Xne׍"fI$ⵆIBqn Mn9'p !@CRӡ͛59vdBeq8y agv\7 R I[@d!m(H]ڍ?aqQIF (J=/7CM`|"wȰ2  ;; Gܚ#'t]|@}.{/ ֋Xܚ[@nʪHw]̩WB[vFӆ?0l)+ppӦ ا&dg-4p+sx<,]?R\7W U2-; 6p 6֗7Dҭ|6l/aщmiÒxprJT BhDg c B3שNI̹C;Q"*գ!2U&3'2Al"4,V dKI7Jߓ_y堚q-KX鞨-̐r`xtBLON0M+cnf0JJtp#X7<?5>`r Yz+5?d2  ^n= *VZhiBÒZ鍓iurLWʯTkV=&ې.|~ 3Q%ȭYnn`T6ZDbuWʬqZ};ډ٬nFh20{#oW &ƝoD^NDdHf:zfZ;9.qDŽ?HWw2U7 #p&~vb|I΍$093F.L.|'2  C#a%-@T-U'~iIdauikVz2f~2nijoj:}w.5= d[%F7x^[8>&.hlVo)dƁ   ^Vܚ!گnW?YN5{c>_׎nM?;Lq8Uʮq crF1z$r8eE(r&eJL@70Nr=wl`GaBK4;AhHv&\؅'l|||'!&zXzHzp\o C!Ðad8ZV*Kx͚qjwd沈N-dO[xlw! U60C~,f.Zm7ɖ c Z|0VJ| kάQ0q7?Eъ>x@,Is7˶ P`@!ÐaȰ2r+;p|ci:ء 4H|L"?rG~>\iǗ (Rnk4#M)2,2 /2|gx~xsr y|- pnl=6K`@ @F=-Zc:`lx0@ T AnF]ZSݧx0@ T Any<# H[@  J[@  J[@  J[@  Jܺ~|6k@ hg InZ9klQTsczna%cXÎd[y?sHe[a/Dq깅bV<@˭gBxv\؏F {NRg#92vv"ĹTe2r l0b?ɊOt]'i8}@ . ?G endstream endobj 376 0 obj 58624 endobj 378 0 obj <> stream xWMo0 W\ DI[; ;m놡ݰ^}:"#EH)jV;Ct/O7FyјNsC'bDbloеL}>7m{>~4հ7[{{Ʊm/Wsi8Pdsσ ڏ;=?4?u7 wfPYky[',?O3#\fq)k0!ӛJhU/( ԃ$- qiz% zIb=M.zwǛMH ĭmfZE;ei lP(d'& Z~^!E1d old#~D@ʣ]RG|{"JSlN$q Ş]VP' "R'#]8R,cj6)*k<7>gLEJw_2+srWȉΩ+lAQDeՊh% ݙkr2RBdUfAVXV1JLf ȥdk g.T5J }nK̔<@,6'L+hk] Pc<l!G, +FE%ES=Q%B%Prwi4غ⎈9!#Zߏ ѵڏU!x3Bd)z $c$ikqC-0\ q{ ;P5l >\DK7]ז{Pd+V ܥ ! ~,rVxc:΋AY)@LrB>FQψkֶBiEIkBR5QV2B~\E5B604%PFi]q6o[`ۺ f:Kخ,8MU(f1]:U}{+9fy# on7d3noL$̶mIl Q@ endstream endobj 379 0 obj 997 endobj 381 0 obj <> stream xXKo6W@|"`=5âipZt/;/Zv/f373&=~` ^ǟ? z?;+{zoPFr,_߆߿y6ϿaNt^Qq´$y;evAT?cϷi~vf־Hrn8VBf^ݴ/M_ PSOFAVn_4&IA NY"F<ʪ˞ ԴUy`\Β\ma.J57 AzpBңr!ABmY AF/T$LnF𺼻Y\o!Ưzx@X՞v,Ngn8Xgf0C8xs\ma+g˕j V2t\et9zgu'A$VpRI9 - sBX^*߭ܚbWM68'biD`S^P59ۙ6Rz @aHos݁85 t=Ob)2E**1L#o,l:h;iOP8?~vd7/cY80hpo?So) L-*Sgew*{e#i{0tr:c{\ܫ(D޽*Nպ4[~ ILL_祯n~qՏ_7׬+/b1} endstream endobj 382 0 obj 1385 endobj 384 0 obj <> stream xWɊIW 9#Z@tiAC3'{zqی/K.QԺ SYDe6ck ՟??+5ǽU?8E=ѷQ^?-ǛOg|Uۂݻxg___|9Pm]o`;8[v [h{y;FC(y{3wxnOA8͙맵|Ä\oMNOv2`{DبVϳy$193b}Z㐼"4#  %6i#W3GFe<\{ )E&@"$՛v72Wr}K yG~`r @=4ݞ&i'q!4N8Ȓ8t%8KgX)ʦ[&GMM=b( 0\ @b< ^ u2P/3e.BF85Il3 U`Ѧ7/2X~;3Tz]}[M$x (an%ULTMfc̘^O%cGd ֈQ˱b٩{Rc&8)G[Yȗ0ǛUp}1vk$r_YTxOCiyb,I kC pf,Ti1rR1zK|[A-\n^ y,f-Ǖm@IĪ.%F! PdCcp3}8KȰ OFiqlK%fϚ@2!\Ѿ[UcY2^U"YZmOӐ,@[Z`/3UR,Ya$P̩z迋wzの};ek}[wzu*&y\8Wr endstream endobj 385 0 obj 1126 endobj 387 0 obj <> stream xM6 :XGL6h"m\CMje;XG|i֣Q߆/J Mci0*};aː;4%(}㼻ox~4NJ^?xu6LpYcGnpr4 O4}~So;iy0g=,6NnYOŞ w16ag>j,sz`Cn0 iPڍOx|q|),!Mwi.\ݔ,.0{b:q[@Ϯq7%e&QNms]G1(W *o  ;K9g}Yٰ!rzڐv:0vA‰zw@vTIN)Rtͬǘz8DfZ"+FP㮂u-)4IƊ! PoX*,Ң@n[8 +!"H Iʷ !;D &{ܒ$ؑKrʗ_UgAY:Kf /=Of5hՋzZ5gl܉/)HحPT^Z{A[\4-$͈LjVabKOՀ8ƻ+Eq&).-)tnvs%Ί}_[+{݂ u +JB*'zUY)$ EʣO[j8x$"k2HPF*U/ cWMuV8 x!側^ʊotɀ7CA AA /h!{xjY707YIь<E3^^̍kmX@U+\V^z귨j@ryORq ܂؞ӿ?`zW + m o"enF8h"I) ˂D!-FX'fcԸ@Y2tv6h6ܼÎօI}#R#<_N2 ߲y֡-MbDZK=XsҾ..όb˱+.T39akyƖZ^^J9s': endstream endobj 388 0 obj 1120 endobj 390 0 obj <> stream xXMo6 ϯẙ,ۀa 3[zz6--JDk&]qvv{09}z~Cu]ǽK8E_{!.MiP˛Ntϱ?\vCu?u6\>.g#|'ζ{<]#Zl@SiV6aeр>7˲m_ gళ@)G8z*&CY73iꡳ<-;/~r6X PTCZ,H.~fK{+kڲCr15Cz.G=6vly̔X4Иqc;NtA HĿK&IĿKRt 3bCQϛl vj@ F!؆my qiYxei( Lq!0mc^6f?nڙ| ]t|K]JGvSP5d+ʰΌԦt7H5 quL8 'zʍr"Pr nshWyK2mC꼾^m[֙ʪ.|n`ts'a\/RalVU+_E$Vgauh=~R~P>[5]ka/ :;4XVlZ[}Ӥt}gjV L ꄶ b֗Ao[ߐBԹTкYY]LL_["XmT9q* ~6ƕTYmهXǫ-8G{tۑQ $͏K K?+Jq_G='ius? endstream endobj 391 0 obj 1568 endobj 393 0 obj <> stream xWKk0W\et_ =ڦ$-ͥdIGbhYh^YO&uCV1XkM5|U#VY1^Iv߫7V;vBPݼ{W}kf)ky}R\^׻PsQ-: (.j @UѸR08=713l;i>x};481KnZ5D0 A4l0p6@h_bgxQCǹ@4j`s~$gik5<`Cˆ̓§ks.nD8`.D#eӘЮKF% ^)aja"ADAh eiJP72]RiwqLl:8k^(rcVWhLf㡲U{Sh!s$#ڽ~Q2_eE{9?ҷC 0bo*Y$W]IqcU8wۏ'8't19U Q!n*urgR,u@X l0 8&Y'# obDC Vyiɲ<30N̰ L$n>}bS009C Ynkʽ*穦営q|~źG-m5 u1g$dG!*r'Uc%#4Tthrs*4sM tډ9Edi/j/`mR55\ҵg}lH-բKVnX9+rx=/$-|>i%]/gp/Um-{g2hYC6$K mfFh%U,_KNҖ]HIW=B3%Zl`n~܉C endstream endobj 394 0 obj 940 endobj 396 0 obj <> stream xZMo8W\DI CiQ4]l.~I4l+jaD&$||3y8C3?]M59]#|^$ȰKb7̷=<ám|>$z#>?)}ĶHOU-y8%4݅2gz~2yRʔ1s۴o+Uzǫ0!ozcN'|gzq%E.)3APCTpZGK9,x<̾(C,pžtwb.(Me' ҄].aّEQ1KKQ d(7݂ -7`F`0'eV&Zfukɮch+@ᠧ9-Yd}ܬKC{` 9.1T.mlj&;yH){d/9H 6 Ȣ/7y(CzbO/8ӽMrs:A&! }k^}.7cvdGce|:N(u[>,Ѥ)gM<46S4 5C0'n\pt\FC*6ݒư!}SHяXߵňrPjbF,,uwCJa p$UHe܎$ v'I:|}M> stream xWK@ Wy!ޑc6@S۴MKҿ_IWɃҥǒFrt WSi4 ȣ<|V_/_ 5 GftW3'g?$NF|}<oV?ԖԓfWZ4jI=4j4ԻoE+܆PG{}6z2BBԓXF5Vt^J{7[a'+c{dD]ؽڐz& adM2p5l*z>ʠhIeOm  q9])0hbтhkDٿLw>%2D`M#F2o&UCB#AcD2bBA94X@#>0r^ Yh-.jpܲڔ&&%D>9D)9F 2ET1EA`a{pI" *GKѺ'\迶8cz: )nЦK yi(jT{[~*uXܒTӢKLz4*-fV)ۡ> ]œ.|` H&%dufY5E긱4nٲE|b:Y!  HuW\ pJxzEvrXOTG)2Ywn%m ʧƖM Vb(J>A2P+п VFp: endstream endobj 400 0 obj 720 endobj 402 0 obj <> stream xRj0+]/: JOmRdǥPVhf5%ӗRa6ZGzЇau}5]36h8!ygz3 Fg8t=_iIowi,Kh|mx nQ]+7M.|Awz V ĂRXڻxS:J|W[+!>/dƧQ-Ov1D¾bqN\[ַO4)*^ld?@ߵ endstream endobj 403 0 obj 309 endobj 404 0 obj <> stream x |h4-~Uk_VD F9 *( 7"7bES@D4JH rg$Mޯ}>sɾev;<n>#R9'1g 7&>y_=\A'y Z`j*Fnr)mD0A #Ëa%2AJ,KD-_dӅs:.s߾U~k[^ zrZ33.'Mi7gb37TX+p5}$#c+#dp17$7c?]q*DWKPW{d;DUSheG*(cK%xE+. io>]HXZ .'k&gA"x>5IJX].m޾-O,Ϗ(y@5o~Gz~.dm7lnfVy`N.`N kt֪WX06{lY'bJ{cCK&F(~x )]_0*띿:~Mu粈E?f5qnO{K?Z^{eFȵ(&D9H%\6hM^}rgT,yby/!ۜf.rw p|o~}iCA{ϖX?yy,p',ߛL7ssq lti8]r߄\>7pé+L߲־}]{/W>9/LxvJD -HyCe3-Qؐ‡nκ窭7~HL ~.?cߞ%]$H~7ᦞf禟7'+j6>MϤ33X[f{yl"1nrYL\ 1sWk\Rbv؊E?3CMS:ex{?|K7ef_.~KE|랬='sdGJ6!OV?+~C7-)g7XGDEMxKErF83W="ԯ?(iΈrwX@C{c0j?w|cܿl3W\-.9]`էF\~!F%~V@Tco;Σߙ}ɲ{ޯ_) 8X0j[t{- ޥOf{oΨ`ӣq =cἊ4F6.)j\M1z/ܷvip-MXJVrIvg<*V(a7lw_)iy[V~6-/6 0zmǔWCfKRy.EΥTzKGt#U43w_S?rdK%4/zB%<}Gom=XT6z[qaz{`7ISVQ% M#@zЧ[ʅyT7zkZqɸjo߮\B|h;G?T .n\Ĩڔ}phEGs;qAd/k,;JzX Y Zt numf[6m~+Fl/G̰_'Ͽ=Cg_eQi?U]8܅gucm})N1 2s8"YhB E4f Y(?]LWiEbm21_tH@cRWVEf(ؼ%ۢ{cR8DRĨOfv[91l]d5Fr_kSv+}`#\䱡eSG3fW6cK˓( KK/lswRE_g~)s%=G1AXAr&8Zu3E'I5fߩHNۣTB\ͮYeӻK88/9Kޢ8.ÂzԐMH[x6Vy]jřMڭȯ2 ӀzҴ~eAOƔMU6}KSn|k$7*F|%w]t{ VL ig\ʄbB!-OF8'f7.d/S۸N{ۉPOJ֯暞t 竞dg2;t,n#d7}8=#NM7 kFM(A^9舸񻇫_A6o˪"n}t:U(Dެw^^_J&ɻWM`'r.E?eCpv`]:bQ7wyܘ@_|?'yYE7?}bnrX:f{J=.zTv2;'bOv2SCgP髖Өw,(G :3=}Uz_o2V:|Jd7Z1bV.-v?XNjĪăR۲z[qn ң=7 ̍(ji[Ar e=wJomr>70bEӂW?_+?d Kte`ן]Yq*FC :scCpn&7?_ϜpY b۴#UlNp%YD4מ9Ы>_?d ?rOp'7\]kw]s%ϧSO{sgD;ME9EMmDgf&` GQ[xfJDQhp;H mP*f\7VpP _>}&'߄76$|gCrݸV D+aQPSύO4~.wgwRw ]@5tϋ 0SCl9?+m7G$s[q<ޒM9XC2E+E̯Z>/x}g^xSűcs+!\]3 ~s?9`N% ,hll۫J몬!?$h݀ۉeV$}i;asD~L@<]A!~^rW/D'fs?wvvvtv:mE͆):?>&&>@,9z@v~t#yL>`I:`IL|.Hٔi;ߋ5߯K3ٛ|S\>s}>u+^~,t?P???cAn%uqq|} Y9y3s/0RȄn n g.@HJ~bݒlݔdݘd-;{;/^ķQ!&~8+Vg,[Q.yL97L&pER s/('bl'ҏ*"[QbwԵԜrڠgK~p~X9qn]ݭin- ?3`yw/~oxEœABuS t9)u?#9_}㈧&TTkA_g1[jz½ZOD𳰈t"Z$*?.qoBW`!yz"HM?+ghF$_ |}"ys la,&>A»~NysԈӻH?#fN+U?UFg$ψ|?L?_xB}ܱMNزNgy<|㎅~v > ug 5$Ub֯mejj~dK'bmg/hψ/p'~v:;:$ȴhmi=ǹ[5̯+~|#?Ɩ5i+lϼXrk%2[@te'iYU_ݤĄ~@M7|1~nmmh"4J s9>_?++,y& 3;[Tv[=][0~.)̶ך="kinQ 79/ bt܇p7bXg Az#3bQlȂ9Y?q4"~F̜Ȭ8Ӌ~om dտϼḹi3G#gM.Wz q)(iYLϦnwGxG [Exi)2eS]̩V5.!=N!k ,M[% ֭{qX V|]60לoj;n?{F-Iϡi0}ugKC9a*7i<~޿[eOK.o#nolJ«~^hB˯cu]ųx}q{TY.ͣW~5@CMDS4 sZyĨl蠨麥Jо -IxϡhQx-u_ϟ:DS$H?E7uǂ]pO*/ҙ㼜v|.jɈN«~Ah#t0ֱ9:cAt~asŹ]T`znj!09:Ds(Gk?{>ulF⾠)DvtϷY |G#H3^ ^lg 981sGgِ̉̀C?2cB?g^z;luaʝ/m'znEpYwy]?13`ΎN9YWs8~ iO;$?+hQxEyG\#ɜ?/<~ݘ!0!i#SHGGg-6g}9mỚE7T' =I2}H04e^׃,1C &Jh#ɒiZ[R-;Y&Y$V7OϮX,JvӸnˁ3n濼<ʒ%4-{4*W:.[s?#fNd][# ރ-Z>k8)gs}z,*P F%g_ ;?1U^g$$AYK7tɍM9Xu-S*_?+>Y]:!|~ZgAK6/sRX$[7%Y7&Y),_z;s7$c?kD>1;ioǦFnBnK?hr7:lGQPזSsyk5~Vw[}?`Y{V:7qu$[n} }#! gL]M_/(~ڢx2H]nʂw&g;&qݿ~vE2=T+\s٨L=iURI-ݒuWW}~VƔbE,jS}j»IUL{%g3$0?3ngEbEM-yb7T#Bu{48^ Ӯ+ tg/[AltVP޿; 3"U5#r6x /EЫڱ:xDVP<$i?#oWswZ8ʧ8NVOE}*|>豕.ֆ7u@ABYyV \_Оm9r"߯܊0SnLF}?u3?>}թnF"7u@gj8i3nL#8U0=U?%ψY7 ,|afS#9Y?FguT734 ?#fgF)"٣~NUg&99;Lg?@gOzp}6HDŽ~@M7|~6mL㟟yB)ҡ%?~3DZ@g$?9H8~ i3!DZDHgG~9n.Ի,3`W8GK8k10S?gVT F>%~ޙ~򙷿^jئ'wStlYKqO+fouJg9 ~Ϊٺֲ{$_ĸ_Ԇº쐿 55ꪲʊl˙ܑ|9ޙ7X1aΒjr07T7CQƼy~πLR?f[e;٬Μ:u<ׯ˄g9u'_=8˗ 䋆gt?a*tOAogu~ Yg&f|̈́Hω/Fg?%oؘ////~_?ó~ˬ|PQiםuEC0ҝ8n_p=_tu˲>>+#=3`3ρ%>ϵfgmۊ0t܇eD0߿π3`H<6ȱyiyI}-%9}abmҔG+i #ɒiZ[R-;Y&Y$V7O햟=-Dq:DZ"~ iԅN_-'؅,xGH~ i3!DZDHgV?s~ i3?GHgX?+pg2=^&5;P?F>BoA4/B! l'Seu[@ljնhwO̮wC~Q uuڪп{Yh4bgQU47?%π{ɭy;Vǩ  Vzyk ?#3`.cDgՈg hO?k|?%3`Hgs3~π"~F  gπ"~F ό@"$3`^ig$B?#$3`H9B?YFg?G9 π"~AguP?'5O=/o\Z/D\ܣJT3wV$yij_4:B0cso>Fqo1ɸπ"~ig_4l\g~ƈ|t ̄Ngi~qDZxgs\}W{gղDԾE~ i3?GHgV?s~ i3?GHgV?#0@H~ `ܞs_ך="kin 7g3  ~&`:lY賁0 ~ `6?q4bHgfyD² E+w澴0͡5~DκϋW2t l~3j?wvvvtv:mEP4?0ݭ~J!YAg1 [U:?0UnMmڦYKYLvf63`yk;o8t0/a')yRj[,\E~*犹Up%w~ `6?ϝB:::۝xnqt49[5-i YL{9^{A# l~lH}+YSYI$ZvlMlIέnn-?{ArZ[auX~ `6?/o#eI9$9y ^k%:to⚁L 5m0 l~3˷G"-[>K|qRdkySzѶw#X&FL0gf-HXX-'7s6%lc-ktgNs"FiL%w_M0g>NUE~fv& l~3=,4Ui c*꟒QZ/3`98ņHn4폼i:N}23`қ[ h9?0Y5 l~3g h9t~ ?23`C?0 ?#?01$3`AA|~ `6?3bHgfAgwgf3?#~ `6?^o230z%!a0gz3Pgfsh?kg*wx~ `6??k3 b'hH~ sh?sj's6)0g,?*3jp l~egUuT(43`Y:BMz?01$3`Ϡ3;3`C?0ĐπgF l~3s*Thψ!~W?# ~ 99P#?0L?=C~ `6?sFH|NSުJ\πg?ӳJT3KkY+6bhEP~ `6?~1['y.b0gzY4?S9ob?.3`9ΪSwud~~C5EP~ `6??szͽueZZ"H?01$3`Ϡ3;3`ϗ0 ~ `6?3bHgfAgwgf3?#~ `6?F|~ `6?ng:\Btgf \zgfsV"YY=[h>a0gij~vqV8Km6O9L?0{GlžGr]^h#1,3`۬ }R8K?- ?mga?+OMqs 9?S?YնY&>W?83`03?#?S?3;3`03?#?S?#_Ԛs!3dg3dg3dgsKh ?#L~ gπgπgπgπgπgπgπgπgπ4~p9a 3`AVU a!3`a23`a23`a23`a23`a2ZY~,+o4~F E-YJNڊ  &C~Q2[γ  '+@t]d? @p`Ng0'3̉7?oyo~ s7` w[`*g0'3v$oLzlNQr\"xg.+<>͏5 .ɗ>(/"D/"hCZ&;,/'G-d/ @B|E*oL#[ 9ʜ?^00rG -M j#xgRO775V㪯-ㅟ_D_?z| 67kk*|9rWg }$ "~6[3tsp@"J|7˹rruJ"fHAg%fЧ{pnK1~Y =^Eyw}k7tYiLAgW7䭳FnQV/=(nF4qkG(/ ?RA<33o QShl-(@Cuv̛CzZpoΔfV^}ㅟ_D_)t8Msm/"5ۖ; 5|~xΓC1×ӹ q3G됵f<J2xgWuZ8Сv$R%'r^30jjkK|99(yui͂i%/wq ^sJ5rjСN 5 f2tv{uMu$ׇ=^Kt?FeP͈A ?"G&5rj`nȪSSb6nSHWF (ؗ>^zPܴiŒi #Y5"/= z諡zZcW/b/JXI J"giF뫪Jʊ>$_2cݏL_ SN ?R PWWQUY\Zd9^ENd’lW~/?D_UWUU)e 9 *SN-ㅟ_^{fi™> stream xVQk0 ~ϯs!%q!pV8CS?[c%$K>V5?*+hM_o_{^Fy"Oϋ_"fƛe sqٔOֺ<}-o~nӹW0~N=9,n$~v~u;`PAP^.^:u+tCMXK+zÚ6hNiYbGSZ&)&9 oq gC!qQ `s<9L#t R+|1`;_j?û/O[tP).KޞVaZ+?lVo)^ nX0 xoqY{Z<4vxEH蕖AK-#b: a|8UfXF#Fv}cF\.D#bc~`=>q-dh==VE@8$1א*1nHJ{ 6qbsz `ƫ%O^)SŻN6i 58 Z }롸&!p &vǀS4n!:J(oV#|㧑rhқx+'=eϥ_g)Cp2'D=ooK_%TD3HpaMƈȞb/{z&HLF)X]С*r.95~r3|4y"3 1CϚUŔ1KJzׄuKR}8 y¸fb.'4IYN^"9Mj; MmCcL8j$uђZ 7zZ f{ y̚S1}^-i'-1V 2$r)\c{=72XO1FfpX0.5-e6qB\'2EpK9$tc%W7!]-k+#"a8)itaM3>Up,x J8Yv<):мCQ)1lz"8M:%f-qv=j-ULfatYM;e.$`pIc΀ -+b5륾#t-2QݗiwlsoyX|$}i^}04dۜ -eJe]0F.fAr=ȄN] R xJT|:}Kc%-ǡD4YEvepErܩFņq,>7 endstream endobj 411 0 obj 1106 endobj 413 0 obj <> stream xWK#7{V74 mÒ&vC.ѣTg3訅@_>v:?\Qp~գoY{eX[П_p?^Z7K__cڽCÔ;~Zń`k #7Sy>:f q=`a *! p\M䲰f%,^hzf*&e7u}=fmr.S>=o) aϤ ]XӊK1m02̬on=fǍA qa{+o%'?p}ldܻfjvoM; nqX<rG'FN&$ ?O6YM٧9HyⷚwS3D灀uZc|| ڈ-Fg'!)+$) * XԩJxIg&`Ttxvc81 >VG1͹j[K7 'UWJʛ19գ~ (T.mo(c<ɖq1RgaS/RW]8A%E=o7"nZ6-z(q W/i@FfD.b 4bs yI GawDzw;[Ƽw;GxW6ʦ ?B̄i=\h4Kl^2`Ү'.$˓!bV4e WZ!P%֭t-v4讳E'eiCw8R^&o#JK/̐K? endstream endobj 414 0 obj 1025 endobj 416 0 obj <> stream x}{`T׾f% ټH.@+%ِ<7 !j ŴW+"VYŀhK-U|~EZ*m[wfM}_>;gf̜99gn;BqHmh vu^ !t!lXۭ!IJNA?{P2Kd>%#{w$ Kۆ[bY)ZFb?v8z gkы%D9IG ܨmG/` աhZc}t 砥-A?A p|!&c0TKX+Cȍ|d#E/kO^A%НM ui/VDѯUrP)v?P"0JcuGv(9hZZCQ$!(I(~;7W 0x4 VU ݊Dςyq K(<]#L :t3Vvx|&^h-t=-8nISb(4BQ ~t@O7Es\/??~9t DϣW@nNB2_8w#w?wV_`@eY 9oD[#hA@owR_gGƓD\:;縗y?sœRp?ĖǾAدc/ ^wº'C :^Goлu[p2.bނo?wqxT&rʸK!>/+5|7P­6apZ\t p\W bH@@zAxrЎ_}h?X??uoBowϱ '`7|q|^ R߃ǻ~?k8ogW|pC$JL&r;M^=9LWIr\>W tn{;|5p!~+$B.U->[Mtbxx8$RT,]',#LeH3l5<4~9 ፸[f\\$~wy6Mb90ab'0F4ԋ~wv堻0,p): pk2gy {I:_))/1L&{NhaG^yt)U!gGCbos /"Ot X4-"{?`O0}/FOpm4WO9 =Vx7N+cv5M#Aʫȋcc{q2=`&x,wA[gt`A<6اw'-t]MQx/P~)6ݨ| ҉Y%fHq4&WΨU˧]:)e%E'N, dOgf)q)IwbB+6l2$Q982mVͬisrZ*c* Tͺ'34BL0j(&hjnRD_ -{3)g0 V(|Ct7T^Zۼ~&lHrsN@3@Ĵ8qfI &ͬzfR\Fe1Zhyd.7'+VEQڌ=PP&*VD%6A(;smuHFƴˣ\3Wp/ΊǶ&s+a7oިD-Z>Guu0%7ςo\t!|mQJZS5Hk|U=$is7$U*kiu);]hsMߠGU<7w:`Bm be'5* P< RJRCh#!5Voz?*di=_. 5b1 UQ8D^H Hq+"K:d2`^}>*[T keJ@j~.Ji˾%eHh4PY|5d儸)Q5!jqZբ˗+uV^PKGtk (WsrZBƬpa@c4b9L4$sl(P+FG:!2or+rYgϐdi(vbnS/|ug7o6]6 lͳҔY7bV)r VlPl-Y"lfLÛTŗ/ G4eS6+gLTVKFkiI%TaW kJޭ"򬂕0bu:V':EPMx/S'IQ"n$Or$Ob1“g@>3u`sxL,9| x`ģn窀>C "?ŗ#^>n7DI2yAT ䷤d$:i̽ H8~Slk}274ۺđD(jn٬X.nA.-$Dydse(OM]oYbX[ݒo)peXHjq,`ϖ$K'q9V:38ʔkʧQ |[޿1Aβ7dl+8 `72NekX N Ü:L}RqBpߞ6^D(@x'4\u}>SQ5|ܼkK= I%W-w2a(~eA~1Ǥ>-G׌{Fr P'Qiq:HQ%uƫ-Z6$ ƢYXg ,b\Oe{^cq8NMsmp ǹ 2J87Eu.☈E?< IX9de$Cd{\D'^:$!@ƫr~%Oy 3Od^?9&En&U3's'๢3 30hGd*SBNEs+: ˪Bu QAc) $V20Qс$8wlMLzJWݲC3_\K7U/y]K`ù=tcIݬpƭ#FM;G Apnax? siBX, ;;ԀCF.J>;Bp8Ds$!"``>L-\E9 &z&sC\jK2!ICb LKjyQ[ W/8wB;|䳁Α-E^F{I:W=}Oož?{ګjϮQ㐠Vp rRTխK+eapQ$3~*=YD1X9 a-=)`GXƽ/N ?4s77n.)w{;Yz8rH@$(H,'8N$'"~_~O:ͼ;!p H2f$ KL6 97YI\/ga{3;E=ZChOLQP(Hj4CAI!(ś 8 '(IPrb:uL|j 2w3'PtOh @xȝ*^>𳑚j/4v98A8$b+aRa$,\wwέix9쒸r-ssF 2\eH{'ďnwMD ~M&f/Ib-Ml|A6&Ckv;<(?]UO SU蕪BTRQdG`bDbg&N꾑?LeF:̠edJFdc2~tǮ ̴G|ƈ8WW= y~l+Lkt=dGGM7-5n%ђ2k̼.uAtz~i=ҏm bXjllvdC10 E.-,B,nM"ziD-;a^aޓ ؾESt|┼ &0k!!#!2Н\",Bҽ"[w6͟3ﮥٳh@: Sko|`Sv#T3J-䀚+S0+kv.pn3w#3Yb:gY ,z^҅2Q\TZ܈pzStxNsa>o:h>bMF8Bw<~uk$LhLԭB(lXDd"T^sy12f̙)㪭+g%5^#8zsX/vH`iƇӁvO4b [P|~ESiO8mS}4IH,..>T ذ8nGH WԽ6p`\&A$C<1Nddw2eKptUJ]\/~$ +Dw"H>ďG" Gȿc;)>`#)R1 1Iҏ ;ERQ 6;6hL '!' <0.fjJ,3iPdGf|>BIqO~z썱y x.V3ϕ4=5b_n$Ů=f媖OT'MP2QeZCxwOhg}B=A0sޑ``2qAGfJG[m 'W:Nc=}gf`D<'}a&)y͆/W`׀_r2ڥ&'lK֐55j#uS02yM Ja9`9ǫf7{x/XPZ.wJ;b8 m>>dblI KtZwSF\x?E}^$z+"p0`AYPL#il.scxX9cLusϿrQ>N{[Pn6:݉dӢTӱv]/}:>RAb^~БX N5L#pCR<=(sނiq WCәq1v-)y3R/lZ^37;%g_}g$/*, ]R֥H$/aCh{>:iɖmʊuv9em$3Had+6Mq~lBF5x(&Nr'kAi8''S2CةÙq4y~Ș*5!$|i?|ڽ+|+^]Ӆt.ظEyWoIgg#ּ2|z"Ze$*5 lK4edQ4؉yӧȈ!Pe<{7I  4 )gN$@DB]Noj^qo=ܔ;h/x!|+^96v;OP-vtZ*r"od]` (\_Mk 7 ~Ϳ̻I!NQtzWO9 q%>B.,jvb?9\ËƏjn%)DA'6:«T ݈:K,}۲r~!O$MvΣSRui<\7)"ӊ΢o'KzM9Ы d_AL^p w™ x3ȒQ3Q؈B`(‹Tل皶)vcY]n=rv퉩2-Io,p,J%$ &<џ2ga 8n/̅DR,i3 2(3$fLULcGey~%}r {3#9#5sBfIbgIrJll㖚ٗ%o$7oV6>Y?|8/;r0Zww%=.}`>7⏦;}/~ʇi~>Ks1&dgc]:.)'p 6rAMrJ:Ε:gg+ pfL&SZ6;aRQ@USM# ,1DQ)5Tr8>EAps)Cup}/ H`K#151.^͉s'XDS@ I2 k^(Mad̥a7c'2ˬZ&C\1w~\~lKcs}M7y^ )0*ْDOC߽qyu˦zW9:~xݗ]\j~a7hux f~[}<0pX.5;+?1wwsoţ^%G~KOǹyv'&aK܍ۉzD3T[*d\RCq p3E/7n9r<6vxb^ˇJ/.IfPA Qmrv$U vѴXu 7HX! i r5G5i! \k8:悃R\}P]{_Q<9Q G΍o'ИwNj= C#:0[@h̳H2=~G!A.geWǞdkjqz-7'+]5 u,Zqߊx&Oa  |g(A] W[b1|RZ5~A6U0M¼ǞɧۖWfu0nc0%ovqsr +4Sr&l`Yܖ\71'%'+f *lI ;^ෙd B4.-\`g/}vsQ,'=EC@K$OG|sAzqTO%\іt㴃w[dN4n-Т9=ń~ 7W~.E#illD2rBBgJ(9Q(q6-1*|dB1{n:,lʝV}C%5U߫zwҜ|_z4׾ǧp֎ӟʮ݃qݷ>ҪW_LuzdAI1u\ϑ[$ݔ@ G8lvLe9'ce,tbr ]w6C@VhՆzϊTAkb #`d,'{CKp32zL^i魷 Eࠜeݎ;^|k8~f "k63[L{~޼=y"$H7>xyf9j}S~|BlfjUl888@L\ W$YI,[kmX}d|[-f۷m4-ɶD^%0Msd=39/glfסN D 'ao[p[wHn3X 9wSo:w;[ZΨ/Ogj뺬K1esI9+if\Oz3MLsOz;ؤ\`HLq9f5ɤ7+W='0˶܃'pl#_,5) EM連!gܸ\r:$WY>2#EZ >j|4~pbNiwu=M8&!@ }v(삝I (lJ3򢕉 QMgws@e+OuYPGO_F:1USU#F-C ]@D__?RWLLuB).,/wLvX|/NjysaxmFKw%l}g/xʴYUnsW=5=oZӒT~iaeyCgQs3Qy'8ŕ8ݹۆNbe"۸ DblQdH)ac|# ! 5oIӆѯZIZJ6Rj U|7߅zT4. m!FC}\LR\B ZOPwHYLy'x7r 9ʝ XZ!pq>5{LXg؊m䉎E߱"S@6m)ڕAO0gP?>;Ky<${0)QQV!Sz0p38@:o_a)c: t>ep*ՁEAr4Bp9:̣S:su%u:̏Qc0[Wrk)l0L.'u֕GP-C(P5@!բfχh RUnG3a@M h&_?JCK 邺kMDe)@:TjCkj EZx6Vڃ9Ff5aTOC>SgdE"J`D.HMo¿1ү|ў_;1&Fc:J @_733n}% EUo|nb >QzYO:ZEhҴ4unWbVR^Fyú2hGtf P3J{~/gZ FI7"@XA]5= 372 XAFFt7lD :kХPty~}{C`驶 u{l a,U#^6vml,mz:GtM^7b\ Cݠ0NS: VQJ٪_WT:y.4&M=װv}%aJŴQwq]kmlcPKF6JEߥ#*6BtBP?Y +lb0Xx{ԞhRk[nҵZ[Dj_Zk6{#V%}Lds=qxdCnЖq < /Zf2V_ 5Iu|%|Zd3ML.khO4vGcf9Qmu٥lШMN-]5ꍴ93#w:~nbمc1_QTW1˨QJL>$gSEV|yFXEAEvnCjn%Ps}I!=si;(` ο#sEŶ1md^jIcHdL3}sRѫ e_ä961WP{ͣݭE]?[ uVt^?沵Yr#9!=BV{ Lm_A >?rV{a-zx= 'Eq_ƭkxDb/\  ) Ql7d:NYOF%p2SI\8kMTTcd"L4 U,@{Ƒ/pou !a9ook*= v۔]|O[H w7367`jH]wo;z"}y t;H2 [vhu袓Q"0}k;LnkfKk U.:K(nc50lo聉46i{o3\itGڕ!vDPiv(!][Ѓ!`c[C&RB`1>j)N 5ۭ.}*tJ1n:{(= JS;,FEuwS=GB nP S'[W`PwC4h )hPoWGHF ;Ev6Z^swwǔ޼V]a[[[[?ڵ2HG+ . Ν5bz܅ VT.X\L]SY9rAd56[GFYLe G/b"5S{hmg4`: ՑPjbRݚ6CSAMtQ1&~ U=04;ĵ]#D"b36em' 4,2wl*`M*]0/\.1m}afshu/n $ =KSR jϪpW3 :Myu]8ܦ֫'Ŧ+t3pW3W_Z>I47qkam>/cNuŇe$v>B,N trdbinqITL8y2KkQ xbpg^xD {%7Ҙ_ͻoލ|nw#߼7Fy7ͻoލ|nw#߼7F{72zF#8ek0ΌcGwg3ˣGΰ;WcB@i : }[jpUX##vh;Bj쥺}?9ǎqq9u-'ë֣vլ>gǎՈvuMkJ&|1_ʫ|_> jT~:o⎣jY/^EDjcpWT57 m_1Fv`U8)҇=G $?:.VO*IX9W+VZ6&'AKBxn>Z#H*BHAA]o'ex UO𹹃rs}ށıF"-ϟ~π-^i=zwu7Oqis?M'2Xep 0XqDZ"5c1D{%C!$P\(hWp3A3A3`3(ggv@)IDD^HBHA^H{ H&EP*bcN08M0k-q3AJS "R\>4 ޶&MeIEkZqz=ɓX6I lhYek-2%jYkK̩eV-hf:1~F_#Ư׈k5b1~F_#Ư׈k5b1~F_#ƯsGsBzwdy-;!oMT/BZ RlH~H>ÕlٴA%ͻrCZ6HE_ȑq0YaqcŃx%ˣ(Gg@| >>~Yh +Ɓ,ȂY)?N7eo.Enȗ d ͵Zx 26¸۽8#,lȀSGvy?^s~}%k`cދޗ|/zu0Uw_Eﳀ3 pw6p.~?! ]f?퀡`v/n ỡ7]oʻ+pƀˠt &O<;; WP:ӳ؈*d量KEٗx'eWzLJa'KFRI?YϗRԟ!S%i 6`2 \CcjK(LgL_l hrF*RxF4P5$j%[wbF5UJ!lZtyTH*TU;%0]ZN;ܘLzn^U,6::](5"z8vzlCiѝjԺh!buUћ+W,M$rnHA⮬̺*1D@]Ԕ6#jN_Sq 0u B%*p=<Ms&ydQ%=x$C]c+. wex RwW7骜 ݨ**ZJI3.oXN̺.'`:QAAxA`AHA0AVuUp*xM,|IXBB7W!LP;_U0Ttac~PVL!Ӛzkz[# endstream endobj 417 0 obj 15823 endobj 418 0 obj <> endobj 419 0 obj <> stream x]n0 նW? L0wA\H*a:=-+zqQj/8A_v(~ {g: Q ꣼`'DE! S㞛bZ&lwӼ% h-YEFhA'I!*"ߞsɵ՟2&Iz.+y|@2;lr|q\!wȜ!?o}6'Ρgӷ*b,쯰dr?!/S-,?w?H,=ލ }>.'YcnpXE7 endstream endobj 420 0 obj <> endobj 421 0 obj <> stream xy|Uչ0><$!;9O&B Zi p$s=^9XCȇ }0F؉2-Cs0eF|g|}Ct _oBϡ3P~Pm$Ȅ&؋k p ?~zuJ4M??Ѝ t3zx+veZSG( Eס8=mg(ͤ.<=AQ;ڀ/Nƿ?DB9S+y^ ]x(1O8V; ǰO %$K,4(2YAOOf4-S⯑\ɽ aumڃ#GI͟1v$<7O4;CܟxLQ/= "b_;.|7>Fs^⇅رWϐхrhStB^E؎'<ᏉBIv{/|?_K" 5H?(/X,T  ^zZFڟWK ߊ_(dANW[yMN>.uq?A%}gx~gJY>AX)6ҵ W Zb 2H@{@~O:\4 xWj\%8[-x7߃ #1 pd i Qr-J~@yF C qan<7[]̵z+k7sp/rp'!C…B| / g"bX$^&'M BzO?Nsm q7!HH<%@U\ bx)ŏ2Y$Xb:B&Wq=vw$ =hy !W@kѝ iVaeGw"#ܓh:Nӌ@+W}~t$؉KЯ`*(s5 4Cq[D NZGqW>؅x995AB8(:wi8 ՃZQ 'VIc␾f$t,l0a PYl1 BԥzՔ *'OX^)-_\TXiRqj1TEDF3C@V=C 0&~@ hv~IJ6(vUk3CfAr 0#T 1xw0pZTf[fh^9Pe~:=4= @@P~웂@|3''HRЌ 9i`3g$鍡(4mfEt̀8}@bht4m7)pɊC3|B+Mj4}Vmcs軶ڀ$~{5t}#fjW K*1hh&MLPBB-/ś1m_"6Pmߍ/t0ks  l DGĊSf(e1(4b@k!DND'B1b5iPoOii?C Oi0RLgTNFE Gpx /4x 8NadBӮAC ii7 h5DH5:ҋµ,9#9C ɇB3 gٽ-_dG5KB5Vfn7h[X"h $P^2ZFVL'2n@(Y֪Z5-1'i-f`90)|~|y3o_>,]}z^^5۫CZ !~#wάa` I7 Z$Vmxے+amjg@ފ,ҘFc 2J:#ry⍃4y$ AH4B/f} M;DpLIB*1(B 8~OWWη7\4G#^w.+RI>DJd[:fg !SW!fݢb7ϊTޜWp+c6P-(9jy<*(h%̖(F~Q٨n0oE6e&zUxUyS}?.W>R@_3i @+I"|[,Ҙy$јh Lp>I~L_␌|I1>cQUapVT$z `|AQU P/YZț9A5I,ʒ$kx6Woۮ)3 9oy[:_NZY ~1G,GٸIjK$ϕvΈg$%ϒ?IWKZoS7+[U/Y񒤨^^RJP'յ>14H0ڃNq1cvD0ZFx4 0u7#eM"&}҄) ۔7h(eE1!aKt]`c9f,M&&KfOM5Y%򡄮MgɹJ6ռLR8nfꍘ "8Ra1Ƹ47m ||>(mnP:*BD8P@[  gN(ׁU;F0$Df4epmYA@pe Φz=KPzVYd„ <Ģ(&h<2c7gu?{r…5K>(_1™v[^Xl.L.AMrh%K3@+ Ґ'F:Z%#z."G 2S2k\Q>?wO}͏r_=kEE !wr\=x?4^ x]TUKv#;)99+[Hٳ}eYw8cT`RVJ2u$d i21(ؔ*l(OH>HQdslekl>;n덉Pah3nPuVKÔ8oa /ԑY#h{.''%v"%{؉qUNY7߿tٲ.{qavse~5  #}n$ے=:HG:"6S=t\ >t\hĐh )KI",LMDOfnޛZt&?$4\I[``ѸU:Ǐɺ)f 1f!et_qWq6x9S W%LKX1g؍;LQ~5-Кbs;yyF?*:ĐsBݻoS$w8>^tkYfZn3rK?lYkқ:_gMs- liҥrsYZYzY,CU-77ym\w綼CB~|Fy 䋾4xHR9maM$SD2t}@߅8qivl/iȰ,"t(Xot< fguOh l%dB/1gOHN[k}MwG`Z:qY$'|uia]RpSוt0'܏Dq4~`jF"Z ^rkJq<RyWP )0S"\uİC %1!OCRthԧG|陹vmTeA==#b x. 詰1R[GaK|꘹̈+&g-^@a9iv+(HG# HX֢:W94e01fЁCJ!a=n*Ag{c[y3'W[ʉ}XXKO׮8ɛø(xYNrq;j,}?ai8Orܚ-qEK` Ew龋k;Nb%tԐ "|M^znU],QWյ^)@~?'_5e)2^&jL3oΌ3qUB.9Ye`l@An'[>p!.ݝ{do,:yAq|س0[P2΀gYIĹ\\..&.eddWYLR㚙ttAA e2ِK8{.l"f܉@Jԑp]QM}*pt\7}njfo5YK.N=.`>7nŁ< xTw͵k[#'o};{ڻH޽ :T^,rY(cf6bcB8A Ц$tlKmme,YeoT68 --׻y͒SvKA3=AU`Q~:]TN$7ZqRҙҟ7ERN{6tE\ CWXǶ Q:c"tYQJv;Gvz/\Ů=;w~cOvwO7/bx왯(o@sY"v:UUUQYYQAD$%EEUU8NT9MPz I@+t/_L7dvV[N}{;r8 KF Z6K:MU`e3U/*^y0y-aB(B(B؏#{Tb^oU`/ ÿ9{,68})Htߢ\޻Wd*9K;')[YrI>%y\  ߘ*fvz$ n ^ #gZʆذM#0}b]>A`1={%lRb']dTǬBStx]be2gxN-,_Sq/mrJb޻O~\:P}G/c}2Y_E+lZo¿)2?g5=gDDD\\oj&9˽~nUb>psQ/;K9OXZرŸ mN PPX$LS.PXV'Ii@pEbʢTD  S2'!%bA }PN锢c6.hbڅÇc3K,'ﺸW`X a gc6.Gm'JHvUfɳ"e}}cN>c=ElƈH.l,/0z¤$3?hII{&IhH 5}ݧ~ &w;c٭z^\7} )_+)‘?9qeۚgc3omsٕdvZv E?L"IH`.7<3#pan5IVXWkd1fC&RVTrtJ*FgWqc=S Gݍ1jž=sjv͸a۵kuq-2~ c?] tݥ?w}' /w-wnyyPyYT}{B|x9ؾGI%wOL"I4t&\Ġ{VZ0V?a2tR//+#+sx]#>%vlo$F,n=~}mw޻ O`594QOA!.Z࿓;a g44X¥k_>M̊H39LPY,*k&vᶺmn,[uմQXMؒ"*^bYXZo*X5 .׻ۍ4zbZ=H =Y0 )){8 _yqkgk ycgg ʭ:+;}S&tnQz}4BS(c(tgⲃֽ?zYNG{_OpՃiwq3VL{Y~Jv{`QD/dlF Nc3ɲj6y8TQy涓7EH0QL4ɴ)Ƒ-f֖IEx,Jqv;MHMSa jurNdI7H/hWE,M<0#_ְ8wV@ƙ,4gR>\Gw%AOu}>[;'Zi1-( fn?`xqH͵Mεڤ9.799ER%n]ؐooCp1&!<@I)R*«'z9y{zw{%Iv.v0m;_3Pf`|G'VΣw!@L@mݏǁR|茀=Ģ'S#TVZ 5e'gWA>Xl䤷J~+?]Aop<z-mzI7td^[ED^{C_ yEy|'r|DfE^~折 @nxIITFeeULQS"j&WPk(1 X )K >$CWR\+D9B2hMB4fw0zIֈ2-wj%{eg CJnmphyH<:_'O-؆ㄧb~tS92W9F%[/mw!g]tooEdK/ i)4bv\""@ n,&;$+Jfcgp(vm^h~`ZK`ӳp R,OLT(j&|(!.ʼn$&$ &(a)YZ-\Bp(uQ+ҍ.tŏHGYČ0 8'0O_V7Øn*0&/:m8WRRj9 I d!`L\nf{),UG"dRdm(8`m w3T&;fydɑ\9сd] Rb'ET`~om7 L[Y6-àa}^LW= ^8=I O4j7Β208>eFv!bȑ[>wx7[Zm5"e0G2XG*xɛT9%gy{yr׮XSҔo}ԯᶖ5=ҼC#7M8zeh%O~|޻Xquy/hK²J>gTRn2+H+n%;KOcf^-"=Nz SR]QYE0uH^*)jf,PϤ'K6Mtfh m>|I\;e6hC2h `OΧplc]~ (rr(MO3?;!KUG LyJ\IøY6j0XJ-*-EI(K`iBi'pue7%/tq||,Ns`3/[)|P"X YbxjA=pҫ<,IYIwycC7 &qUʺ؞Gޏ=?#v[?O=>$}>VyPIJ"z]!\n )(GʗwC٣>N_STOTez FQA(_J43""e*"kL#d*q̋zH)qڜ fI*nv eKD_lV%"숉 .Naă?-jz1pѐOC4!i0>Y/^4^VwsO`-C᰽}k2跃 ]4cw-%ӫu'q]tqZbq3~XOĆboލ8~/> Ssf ;Sy.\y7Zzs42תˬkC-f皴֜5RL3;C@?5BC=F/ =tJ ]~U a5ϒDB5*8CYMӯlOr_KQ.jMCǾ~=E:ÿOGH%/`pRCIx/G׃vyt/⫑{vb8UE%+#~ ی:x"lrIz$L D=?^8ऱ5Pci0CBU53IdwLj9ա6lHl/9jlZp֛nߺ+jNQDKz#d-J#ߏ)0?q؝"6&λ'Do ynv? |d8P/axw.w&.f; |zEz]s~AAN!Յf«Lq1QKÖnڡ]zc]tUJ^ƏsEU[7+Wޣ^a|ąpWbwl8 ugiRQb+R&%gwU޷'k ؉[]bQg=ܡZxp+/P,GE:^[裔faEƉrɼ| 䈞%5#y`=IvKЁS6]xj]yj]EM7M\DHfG%伵:f-\κ0ĘW6Qb>ݚf YKGGS Ŷf|R~Sˆ^J8㫹^|^ߒ9+#d[=^ay{J6%uD/e[^tKQIh% eFRx4K&'O'dR*'t Y.)n$#3;ߓ71eiQ&=DDuZ!42Z:tFV`W0p|Y=qv"cGպ5ǶoH#$NG h!_ +A޵XuCç`uƑe2kBy)!%ѧo۸ڣׯ,[t!x˱glLLtݟMZ"}VI9'p:$CLq^6b34||d\S8` Wuzf.1)vVPlс_yظ3/;c7vs-o~n&0}v"R 2L8azIJe*򸽒 YptE,_:lZ %8{G9 d4^cA׷N2Z/^3Eg"J6iB$pdZn,mtMĢ8W9B, ,xY8zRy8B3gk*JZ^h(.lɤqŰ(!$~|/;G=^7~*K=#U`Z=r܃zc"`dVx :" >E#,==`([ X*Q^eNr;&ЎPJ I(mKsu/GB7[m  I[nvTC3f [.? kii$gONV9e\PBh*' 5/(ow s~ULyD8L>rƚTeVf x@LՊ(ېxSS;B68'/42cɟ0oԄBr6gl]|wͿ셧yxKgx~x`3:Lpԅ-h43ż(DvII*SXD`땟5#@倊TAJ2YO=~+d=P~<) nj8I*F5|ްo6Wh.q=!Lrr6!xz ?Vne ̂>Whn%KIy+' n1(>&JrP35r-bײַkkkl?<x.$5iם7TKQʟ}ܑqR/Iݑ+SP).3uZ8i~9m1"X=(Le 0 Gnb,gC,,i Nv?ilB53 "0`+v騌m1`{ۧ0 G݀do4`k XB; XF.Uk&4_7,uŀ-J̀ `Re< e)EH}0,Q>7`/`~OeB/7`ò@  1`o7`o]07&]d_-`_{ jRZec4A3'`<f%g9l5`%1ڹـi;?eiaTllGqf9LRaHB&,Mg2*ЭpѠT! jeobFe5F[ 7ZHnX k _DźGcަQip0hä1154XF8OiCX-c`m%IOPנH!=( mFUZ!Lh1JS:'LsEױS6HzVc }-B3%Rѵՠn1VVqI:ݷĈm kckڋ  -Y{ KMԧj]!#ޠvs־*!a1VƵuL'ӽ4QɢLÆzCvg(m,vNz{6#|m|FY }MfuAel7ɶd`#mQ{:=fHhS!eg"q]'6s轉QiɦQl&o`6:b/?HLژ}7 "h~ YY6(ti@m3EYb2xhw{$&Q6:`¼_ t< /T:TB>R, fwK],R&M="khzBO4h~NFYgF,WMPoQۖsXkhB[ cJt&Rm5zڽްGgD2#sèunf?z }n65b1od̀TW3˘zvoP6JXoJ7{mԊ50_z]gPǰ!oJern/1JXQ'l;< YlcFQu,=W-=FnR6\ucxdMx}$+mEM?U|п9M΍^tΉZb4 qk<ϵü>'7ڀzW{mƚۼo1Ask#kz<±ѺsTfc5^{FZ듩(>a{< `}a,; %!wmA[RM,muoz@f)Ӻ=_KJ5Bf[GwTkkkhƖFDZ{` mowўklZڣچF&Z€v/ DImmBF7j hCod }cܷn W辭:imoeCݴ.:TZ{h/vVc-47AG 66ZZKt]'PC[Ӻ> 0o9(Ю7tvFQ$AVJ,-]I쬣mc5jj}= RѮ>l_#CaPTN`Q{/Hm k.om A֞u hv{tCOgC'E0-Z+lTTaÆ6C` ;ڊZz gUx!M7+l(29sO]:g|mAv3/Mxy3/ui ujĔ'(Q[T 2Mڦ>ZJЙQB,A8AڡxÚhJbV Z@ :VS5!Cs(0.J)m>7EYر&ʊ0ր녦Ш1A yi5 k [P[dv(`Lnz:`t9r Τmhjj2Rͬr>Mfe5ֵA'܆= !e;6A[m% :7i 5(t~Gs Zh^cA7+ѷ th}ktC\}cp2 霉# kc9Ӂ5X7{  A? heK$31RXP).Ve5X<~|$r|BYEYE'Z/Ɗ r5j8Ir~N/p ><̹ft6fTcs4n$,?/c ;6c ;6c ;6c ;6A+g; !LHkK;7sg1s^j!עPCH;:#UǷx.w9ƖIflso[u0[RҰO~"5|ߚ[wtΥVc<ؼs5okI7.R2&4ߕ6v{J(^GZ݁lvw>~*ѧڹBx档݌6Cx́%)pPء H7fqZ./m6őp/Y8mDr(R+!a#)RP<6{I?Wū8Tn*E%(;`Mw 'dM=JdN:P=QTLm잒'8 T?ڞTT҃vmwG#;#;qITŊW hRʪVcHM˔t"T@`q+V¬f²֏-;DW(]Ƙ;ӋRcgod{}wySR&q_|BCP}O"}.^_Uޗn? IJwOnt(ySYĈ/hŊh^ u9SY@nӴY, ĎrR-!oC,`MfהF¹(S=sY;V8nW◇t5(хݚ bn+{}r${JzN(-kad+]E[:kTE?m{_d 9V:.5UE/ؗF .}zWYA?u+0pO;{k)ً& <z_7lMi k&gGtT!+]iK51N#˻*wQsx>iO8ȆP~fED#EkM5VdE=zP_VBP0h-EepE ,{) ÊeƿqLGZ,ҜG_\]&06fasyEbCzm;$+fpȁȁȁI""""'^ " " " "+,,,$D HHHHI""""%$t:. BKB%DDDDH!!!Ih 4M BBI ) '''$r~ 0A4A4A4A4%єDDD͔D@@@iii=/a`0%[˫ a0%a0A LI L$J J J J((((I$n&o(԰4ŕӃR}dC;,m,-rI:CRў<jH1S8;% L6٘>bU%uEn[Q*뵏ۗ+;m+i QZ{2CҨE=~#zW?wtOS=GmLǍ i{o"^Io-ﳼJZrP@ʰe%Xae~vk0ld`9˕Ϻ^jB|bq$>g]=TԒ_Z|rG ZC3 nt t8n-^A.6'y<=HӦ"-~2bcA|b⩝e*tAmTοR w=Uׂ?-SPn)_\-`Ձ◴*<{|?U- _$fo6c~L[Kڍp+_**jQ=[QCNGqt9:as0q;;b#b&NFsISFE>쎗zb2]^קap}6DOuQ-;!KXQcjCTGE,C2DouJ$&vZa;Ù؀3wR,a-@rn ACTs&zPۮrE(s}G9N⛑iTLs6-^EoyiyғLoo* ")p4IYË"T6A١e"ftE8 P/މ.%[|ePm\`c3֨_f3WFZS֞;"Tz_,FJR-ږQȗť]VH>0p gHk@;$D&Nti|ᄴʧI˹U(< mDV$"yTR endstream endobj 422 0 obj 21108 endobj 423 0 obj <> endobj 424 0 obj <> stream x]ˎ@E|E/'tu3cɋ<O>CAa߇[HY:4Uէʮ>C=Ŝmm0pĊvO^)I㶄a8M~_ݖax ܅.G}\iaXLTy󹙾4אj[_yM}Ti.ܦ s3\Bɲl* C߻"f^CeeY,SwW8g ‚KBʹny ~ccy>6#`= L2seGCQ/ыK9Xc6GB_nqߣk<=/ѯп:qQ?f+]ed;l]?/twi =:zWsX2}e֝6C8!K?qdl endstream endobj 425 0 obj <> endobj 426 0 obj <> stream x|y|[ŵ+]dK]YX;v VcgvIJ,Jl˖$BPR $la-$؁BB.kK-um X̽Bh}?C̙33g6gڎEȀv!}nqBupJ`[LzwT]}Ǟ@H(ݽ]:kFMꞠ kQ(~7 N(r;>wJʡ,<)(p4v'v%ڜI"j(W#pcPM˄UjA FYXmiv3=#3kKr?ɮC(CރM%SI)yD(:$buat|_@G{у6|EN0o ,ɎwQ)BO+Q zFmSb+#'E4νqGe:~yrʡN!ҢbۂRQ%ZeӣK8_U;6r#Z#[CV$CaՆ{`IP UmG<>:l/pğzf>e l^Dߠ xznWSiSG,]&Ab/b`|J du[x^C \C&T\>|+c/?+jTTTԎS'~ 2u|t1yaV5:u/|C#8Cz"|]Oc&!i#~ 9yM0<|a%$%FoLL_ZݣC= k.*jZi< =~G‡8y&;s7TFRM⸉q6=Ϣ8ODρ'#[ތ/'q!ϑ3#GmѶAxԍĆ^#wj-zh1^IuC>@wXp ~2Rx'[b@$y.{ {*܁&ѻA.ـu![cKͪ?Ŀí予<{N :'FsO/ZI>w( ;_x r+x=ZrG1'؋;p?}~ }>?͍*̩'?ߚ:A*mHt{y'W{# !=z  x k*6-xyD1ׁ*p>j&h+Y&Hjz-[﫹V-\\^haiIqQ`AO[rHw:i6kE4N*"Fv)<+VѲ,D{\T\Ȥ> OMSbQFERGHcxӚoJ 71c#nh!;z8n zַA#z]6+*D#:=zv_@KGx>Q\n3zMK}]ZTǵOGy^Fj0qum\`H!:t4Rxj1u{ Ne-qJǰxaܺ}L:Om=6[Iwn)~pMZ7BЖ6mQ):J>zK{׷x O.sĆu$ܚ"KsdVq6c#Piqbʑg%A\ HI&ނK O+VNPC(m+.x>=ޏ3Q! R60Oq7^P@BEmc3 J-Ьui Zi̇:ߵE.K#x[㤝֜J֤n55=`GYJM4k}8NAqqͦ~o"sJr:[k[ @$c`MB!Yr瘠Sd,5UvFcSV1La3;|ޜ {9`#7ݫSqg԰}ljWG={#䑽IM)#ް&у|ă=6Yr`R۾u$Z% K$j`GUejy`F I0 '2|Mկ/Nhgq1d!@c95j'õKwxŏ'ųMը`S-,u[ܖ\aģO%ԧ>I)KV|sMil$Ȱf Iͳ5ZL\jyO7 h(Z]j 7؍FH41CƈE*fc%WB>N#h)wIpKRRUg;s䄜P=}w:(-HG\ P HSWu|FzeK.`w15_My$.#h0m䁘ŏۼM 3ff"Z$H (RUz0" /yOv^Y,nu-lQv+V|{K`tmG9d=޳5vRC[?/<כ׺wܷ4 Xۃ`mðӡ|)9РVnhwt`ZҥuKJՖiT6F"* )hQNۂ4hp*ZK!UVL;uGm`#Jj0 F1 j 6fG5{92ka58pr 7LFp -wvmZ^mL3Ҽ#\AVF,̄=ZBڜth3H6ED*DD!} } 6Aq5Q;?Ar6 Ԩ]ƦN&{ XiΞGxZ@sw)o|M0bN|ZM`҅3pEk[[ꢒ%_mqb"F sYP·]WvRqRNT8E< b~M vG2<rɔY %[>R:׶^Ouֿab|;|_&c[s۝,iMĴG5c(\d2o36mQmiQ>^rlxeK7" oo=M]+fjR(+}7XpO+?PdW=7E9t<~Smjn8Vz;rrί{+?G]Cͻ򭊍w]s ުooh_VRPpsAxUG2l\(/bSKztnAn_vk#ySVjibW=d^JڬǦ>AgR:vaǍ.73,+pPkL˲E2)KP]&%R:JߜNLjۗCsqf&EjkQv>MQ5z$VC"ۻOILt=!RU2+wL +jab vIG]mo<1bqF=uM{DHơ3y[xjݷt5ݾuRZg֩1Mkh2 "+pfQ,L6A0M:Aɤi'YфǤkk(♒ ؠB9{AATM>Èj& K%p%;b Bdqevaґ#oO^qw&t_/|OŗA,};KpKO *G^jV5AptCizX=F6?RZ;P{;{1XQ~fo&4MBN>öjg "V{'Q|ϤqQ *z(Ea2\g{>}[tl Mģ%&ȡiy0s_< .#\Jܠۭy"t ;5;a7Y&bL;q8GOrVKFX `CQ{+)Q[b1^m|ȕtDGKH3Yٲ3ZٝfKO՞hNJNɒh Ѭ}ܤngO_կI۝H`3lbOwl)ɛurʗwXrVb >ӷLDQ]o'gϲscb)O Tъ/k7~'ۢ5o3~=?24q/è`uD ߐSޡ?.׽h_5CC$Z3_W&l Cu!.,+-|a{Ҝ4Xu5,iNgƦ)"Riƙf;:Uz~S-ihqbѹʹvNhEC3 R zG+QըiWU%/(A=O B$t ħ{k~qɒU]ڷK[===Ҫ1Έ0OcwUj!GkE(ipOx\i d)nb@5Qʹ&Úkˇ[G*%ӮRsB>sZMd/ YwG_?q'V>T ?ydkF.8ʂLwfS,'24-kItImnuTK"]uj#^(U0abPK+M:uSuѼ641s-J<)o"'XF~V^S*arjmt =af) '~^=wMhpEWte[C+9JZ0d_f|Z3X3},[lx]q㶄cǤ>XI`+t7!vL4< `CF޶AF94:T\|)t '_hȢ3&\>O,I%<. ɉG>+wRÊ[`H!|5S7`^]uĠ<Ƣ||R3U4jc6,:ިcTG_b,Caէ7R>`ǞgzpLHŇj&(Ob+hv'%ǕUJ]Z(s YYŎS w-VLWk@^I\B<,ʥӾ o%z`˜3DP[TzjA!N ҃M PV3Wmu%*ۨY!IuVѹIz3v6#db1} UegN Qr29`1lQCGGIQti}.iB{,MO ڡk+ $ RR|l.<}ZO#ڂHm:GL.?O^I/ eƋSYv(sݡh_44j|U;r2(gFUUb?J`f9*YK~lHt%gŶ4t2If"Yt4udάZR>Үsisv3a3!&q|df[wQe^5LkCjF3\%s/ MV?g=se4#T֡m,7ڎfgWE/>/;Wf2\?NjYw#]rT=dw×J؇Ip_"cUԨPBB)WJTmU^^Z?_u%z~x ǥ=A)J G,zR?/JhgҺpDnaUUiKz{XTZ#ۂ"!ʘ7Hဢ{ʥ(fQqU DpWlL,ͦa"> #[p׿ v`$)nX'Ǥ0TT50,ƫHh@L+f&GP0ʆFDq'< >-.sO@A3!nz ، ]-cyJG0?lX % ʅKJKK ,]eReUF?錴T6Cq1݀>v[vsl\*OhfṃܳS(7΍̦ė |+/_A| W_ė |+/_A| W§OLCDa~f)[b:J94I\s5_TdfyTr ǯϙt >~_4j̭inM3E>ŗ=fͭepGwzB: p |Б[|ckϋov,-tG- rNaa`z7ýM͒imC'C@{O)ԫJ>6v7{ Y=Oav/av_avgx=/bb!]W}.k`7_]Ϻ>r]zI8Ps5`ygvZ ̝qp%V놇O @M\-Yb\nꡲ` .FWYk6=Z芸]lBy2oq5fԻ6Fm?&?$JؿLBؿX_*/{,IшƠi4 _uz_"}yz,z'!"rqr?@Ru1[),FԸ~#8&LWzK[F0qg -cIQg<Zu^s}N]sk+JVIRP9v>asM`qr˨:_:(Ȋٸ%DVk|Z㷯.kO裵e+OׯxT쟦tOi~j(X0:0;.MBFIߢttw> endobj 429 0 obj <> stream x]Mn0Fta !$Qi@`H YpzfV9fxUQa _ְ~0y .ccX~J |{V9uusf`!,ScBVuKiX:mӂkTLe`{Q%l*2>g59"NbFs&GrLs|d.Og|@9S Tg_KΠ{?w)O$k\5e_Byx^1dϢ?I 2;kfLu6q9? 4|4x*zC endstream endobj 430 0 obj <> endobj 431 0 obj <> stream xԽy|U?|ﭽz߲tӝ,`$"5Ⱦ DPTPeW qaGaF\g< =Vw}S{|Ϲ+]=ih-9s>-Bv\"r_/8 qs]]jBW]zΕ_:c-BK͞1k:Bv=΃ҿ`GX\=-Z2sFͮu)o_xAdEivn8]nCH(VO'+z]PٻOUuMmߺ~/lh@y% 2tKG\6r1cǍ0q)MS?(KHx /6=?%=Nk8= =g|ډ6'GЯ+h*܊O_` UAFtp ½ W݂]Ơ%6|Yj4oBu2tZfdnܕy B6B3w(A毨\q7w){ OY g-Cp)gf~7kx4$ wwy4Ӛ9g@p-J!gޏv}kG/XNf˜DATC}[KwK7RFP=Y 1;DЄ*ͼ<o\9W!Kr'6J<O"ed y[dxbB?Ih0(4FO8EA;lFr  $W߹_Of@/Gmi=v~x,5xߏ#Kr1@oy\  fg2Xuwfa!c۰~u߆; rgH$J#5W09?W%Zk[mo/7>3*aCxZxE8)j/d$yѮO(1}Ozw-76  Q -$n'zk.e+܂W'o߲w\z l'y/ 2~٤l%w68s^ʥ n5wʽ};͝_WBOI~(50]xCLTz]o4@#R>h/zgǹu`n/TAy 4I@R\HJ\G|x*FN x>DlߣNE[pUo ߊڍg'7QG<)x LAQ9_[@G@/LU.82 ݄P'^ɠBmҎ  Z!8458?3] }!}@;-RT=|0dzMC2s~8s3@xmGQcfK]Rа+ѥx0UG]!R146Dh^f^D4CJ'NlpQÅTW]yAdyYiI"^+F ¡`z.;MUdIx`T1869Қhnaz (ѣ5EC?5N gٙu}&6" WEdp,zhP,ҎmbMNFdVFہFAV:dMvԁ^hjT?t w$ZCA[A ZZnj2xP8mUъΌ]يbIv *lc"imȮM[ tesR5cVnF}3 D.5pʆGܦiӆHSzuS%!͛G-MSZ- UٱyAU]iA34MhS+:;2gȦ SbpiƠ]i=A3y[rxu|؈SDojvĄeV)\d)Q'ӝ)Z+"Y RJpdHK.S'n˺K3_yRTGz])Cez{_o]=U>&|{Kl6qb m >U/Bҿx?.pVkAqs" dEyM=__1V1InJΑ'Wj״?&u55'pEْ2RWht8moCh|XR.;4UHpqvԾݞ<5@~pZf3Ȥ.-n݌vm)8Ÿ_Tv2t(a$"މ d E=>Fe Vӻl~n;ぢ")E":hME#jeZ(:huE>\ӯ[R-::O夫+gDHUeءR毧2Fq lPK\cEښ}د ]R]z|X%5ڿ`C]xt.ܺ1/z1%ӫϟH"CeԺQ=TWuQSKesƥ:y斋KґÚй@ Q}uȼ\K93Z٪lWZrRR,U*۲EǕ*oD8EnHD^mvb'G`sL?mIҧ%gM?J: ^ӥOoZ֒tV{9gsc[[ÇxCřHp?#5ӱ'Q@] KC>CI9׍"T3*b.krQ7ٻם,-xx1`#d,-J߶#+BG/]zs~:S_TT=ZXRGʊ9CV#b98Gg>1]S咸H.n3T +;, .  ;$7/1 lbb# l`A~x|58 /'y^sM]`+ PqtjTpkME&+zU髠?*sT\ͫ?RSTmA}{T4V"UƸg"׈e2(aKDuJgGi:LA3kC\s}jQ?ZFfj',lʪd%9ʅ# mla}6ͪ܁MWALT$pXdR* V؍:baTkrƥ%*&0*4$V>_e4/[lbxu.-Lnn`qu|Z\}SM~Ӈ3_E=CSMS)k2ms{kj !JqK]qDœ Zӆ;Z6$h#bp7G}K}k}ϞXwWj:=nz K}`{&~*x*5彰A7[;GT kL1]R!|Ԃ>gzo~eV;oZgop쮏gۿF+_Fj+]Ex/Dy^2K )em6PX̶^Q90Q*?K\ ?yN>>yF.7õ84#o{ȇu?GvS%tNm!囝@_mc{x, q(BQ-qRvM)m )9`A3f 9(%< ]`NN4vBK5t403bFܲ ##EY hA*-]gTw2sKQ2V75OIGp!氆Kӟ4";w8qA1^P2h]Tö5ֶWok[Zfmcqk_`m!5FMD*8..h;jE|%2t D+[NgΉ_<8m.">"SM$>pp!P 0~כndn|}>ɝv< 6{jg d|D8<>WϮ8`7 my(Q@lcVΠF桲?Rncn-c6*@[8 KH\mN'cO9. n{v|: rXTK(p^0~eTTYTN4NƺT|:jAؙGΘ \qc |O{w^:dU/ͮ.vY+nx+fpiA'r^jo v _i&_sb/Iz<.6d&#wa3ӯ1%WXЁiL0&2,blȊwF,џ1g\4|@Qt(LMitv(n7vW81"2xZ #1|:lIeԝ^Hwoݴ~U>=ea͐ɮߨCrvt N!ڡw^f8Kas¼fwA 1㱐&ۃ2[ \-?U{5ͧ>% }~սi j8h {w4n};7Nc7/03px߿+``{g[RTe Zݜ= <־k~Wwsb3;)8OXk~y;cd]P!'K뙉UZoawƅ xH藢D*'"2Y@̜nQf~2 餸t@j0vqWWYnsQ~m\g܉W\qկO/|'Ϸg~gxo~#o|!&AnUu+ΌW]?ˈZ}me̸_ 6I+ kU g蝘B܆p(p?--"gUH.䂍Y/^OZ: tQxS3!$L 7B

    >䬮qRj' 9U/۝ [}83>0h *J;| Ԡt * ժPuD)<̗+5Z^\nzViI OS}=v?Gѻ?ѧtJSKQBSG#SUY:R@'e.CJ h68 PPU66::U帢zEL_y@ TE *B"4MeBvkBæ!&.}*j`W+ tHYD  pAn@ggcP)>:w5ϥ׉xa DsLXI6AC^_hJx{^cdj=yug|\2K4u#83Ϸ??^)&C Ј­~? C<&s퟉_~§r0A'Ry-PQܩ N\ vE;ĩjd UHNUHN i_vҾ| sV_KǤ缢^QIe/6ZFvvC- Fptq3GNK e)87^yꕿqݪ?̣0iz߽z̓*tB P W%Fv ע{СCuho~$gPv؝wL7f)4=, Ld@BT< D1W@VPɩڳ[0頑d$ gi<(9Z`kv$Xsq,NJXIaC$̒LR kUAtؐ,cҊg/Id{Pv)w[i]NUdPLʪ:lވ.{HXWXR ׹2>npL+첍rc4 LyzGG=!.;"d em>!L3Bos8 Nͮ.:@v ;[XfEڍJ: vl3,F;|Dh-/ٱI#4ATT! t{D ?`K8_iAj? KH˜|{k#Z58V=.JKy;*v Uu J{[,oZF] &0A ǰ>\kXx!=igzpww w!gjg"eKӰ ^Eujߖh+;Ԡ6T&O|Y1jter|,ckoVUD> LPiluxəgyL"R~S{Yw=sI.#AD+txMo #ɐ" 0-NF@;z+P{@lp i XW0ᖅ'DGvFK2j'04tBФ2^\)\\:P "w]9[]ҝwoX{-v7.}eo] \oGݦW dYS*O W44FD yʡœ9]8hS'pDAQ#7]>%Ͼ,x6L,FhrGƀ|'}do՗>?AςA b {>G 97D`S;R!jallgJ`Kb= q4vCF]pm J=+]/P/ 0qGG!F"FQ$ݸXv4{:j_ouZGfZ?_hL`Z 1j ,pe‚-~z_ʤc㨨e~L|ި9!6PWOvx^=X79=`|u4v>AN-iA?ugYB 4$0ڒTsvڪ~cW~t?ⵏh}}ׯ쵫vL W͚Z׺7||[[{vW=͕7|* бFU9$=-1fs evO?Yy;vC̀n`: 5R}3/wPi g1G TNpE(= ~h?P,"+)pDXA a1pJ=[$NĆLG0CI!60?Yi'Kql3\8ejVvۅ;LYJӖ 3|T4I(, ^ ƊH2,zGF)|+td`qS8XFTר}޲ 5hК4G3g4;ќa{s ; =U/Xyo ?ԞK6eeG]q;u,ݏuKvZ5;>X]]4MFߛEZ{ccx`PtHC'Mp\[pB)OՆO 4ON*4ik9%stC"087`zIoI04 / ϩp8\$x$*!/VKXj'71f*vm׋{K bO0y_y1yT"68ek|cP!|'L(`}cD+g@ç,n߸u}]58V8*_)YlAM ExkHI6w&?T_gkAY~e<~}XWN[C$}kf]e3{ ִ 0lY`>:bڼmÍۧ욱E\4x=QzN?d ޸쳻_XǑ7^{'{$/71g=o} BJ@v(C g+G}hskh` 0I96p(5 a6 6;K-VhyuK=[dxiD)6b6 8GSL"ttkNJ D(W@o\x]@"ӥhr+NCC^I+V`\.!n  P x xt# swGr&gR_adO AX5Q=W:R,5EEtD%:df &FpY*ˊp4<~(}_Fz}U/(kbP=ijKd3, oL-}*]%)?,/d2Q |lD`~PFO.LN٬fcP`itDA]&y$]arZEZϿ]ز+S _B/mž4e7vq(8*`Rf pa_IEHpfcSѶlz)3?֖|`baFt+јnnyC90ByQQ8HW$ģ8bVJ49X1g3rڸ#_P"Llrrwl m6J^o-\;o894>/[_{xy_^v؛Gov0u v[8.7@nL(̫ /yNUv-5y S|Eٞft,,.ŸuvG< xF~CߤHO }zkeP-y{[@SRT腋FF/*{[dŕ.G\\zkڭVC;"&g lHfJ2UEȂ#8B!*"Y-Df !cdV*>ƦW|F%Έ0F fa!lckaZND'P3@#fLM[CV\ndcT/i}&Sֈ ?_AaQ/ծWg_t*yېGɺPҼ]3z# 1!2-_ʿ$idkDiIMO/j۞.89Qpv;.p>rOX+=_xiر[2=>]g|7^|\EH4,i,Kp=B vn7nԏ}:g5GP,d%;U|4Sb#$2\3pʕpxDO6OV{Ѿ,{Ot=x:79g5{Mh"e=:̐[s]= "a3 #TG7)"S" _؈foRkNl 7+&pB<_62}OR-=",~l60 SdV`8 K ֏B~z.Q88k$13BRN3™vgBFr_S)~]*Fυ³;(ƑI+1PMii׭|#)!h;- +΋ /Rv1'@ZLoO+)|nWP<014G P/x vTlll2"= zϻe:}cM[Zsdmb$hv`q]ɋFF5H{ip`x{wzp\x6ӽȻ0^MWڮ7Il]4n(DZ\rAtւ?nF:Yj "֗1n@|.A@Imv$N\W\Y}'׬y\"oc_{ҙtP|ڴ5x+t-M#9-7m)U, T%KK_aC+O?ctnEk=<~} ֝7QO&)o<>(\6X@y>D۲]Wl1߽BR3Επx}y{šX, Rq.I6v\ms;[m:mgpqf Sg]AU6҃YS$z ȩ/[]rQ ~zhי#Myta:rgZ))56h4\݆q;0dg%[Ekv#iJ:Wi=1䤳%4 ](u/Ny8={q=Loߥex@Qmfe!D$K!$d_dda)H P/Sei7?}|35fq zZlm"Ovt6W&.E9&X?UG!tVQmrk2$5٦8ٞ'ڟl;4r*d|r.MC.C)bpPJ#hFUZ9I`Qq _bq:UUB8R<) 82qMTE8"4vap ZL71Zf&w) \RSόS2Oe}zD__7wְEܐm@~ۖ_9XhpE3>YOj? {􂼊wC{_R}&u/mJ^X>+#Gl\^Pw6m"YY V&C s{uEۻ=Dܶ0o9=\u zKMuӏ,DzgʼH^;iܫ/LO;`]\zE\}:.B*=ĸeFhe4=:4#k]uQBDB8u"Hqzg(`t2 D9)x@Y|Q2_'ɾzT{"o}VzRT|%|&  zŠa~gBq0i[zŽLI}(-/Ү^f%YxO;/iOtrh̯bT{(&Ė+7+Re&&XS@Iy/_Qܮ2de8HU"U3娉,$2OdRdfZOhy MilB=K UHJ|B(sn~r3: h? ];H GTD tzU9socmI_ߥ#Gx剁SO{KY@ٗ?^]ڹp㜾9#/);]-2nu ŎhY1۹CF#<ӌiަИ?`Y5WBf|lmvEEB?Y,/[R7͝:DNgFF.kJ]k/2Ըл`^ zA˂ <7v~M߳RS?inB[mfXi%C3PicbY2Mf؂nϳ?hHQ[@#;-f LY ٚs =!j+J ]u SҴ):F-=oa]߶?xwl?8rEkq4fzCVb_u:[ϽI+U0 [MSײ (h +0 {z-rvKC{ٶcG?1/ 3d%H?UUdUx>"A)">H <&MF|;o]h:/\*2[o Ggk875[m\zrMGa,; V , Ts;r5}p,rg>d0sr:i7#da(R Q7.N\R|@, gsEzS6ƸeYQ $=u?Eu> NHy빳ϿN 5Zlj\d*K_k1JW qnKGz_#~ K}\_e #9 4 E$H& O"<E!7[O ̇fM?HEl8bR` E}>3F zlV,~Ϣ92ǟ#BLD ŗ9߱oHlVYdU2C'F Ye4HXt>7?82l4V#GlZ8pfOCgRMS Ӵk2q*j@wk+^1/*F=W!d<\mLy2Xg:BNٌooU~ħH8('pTc o pՃ:znjV%Ȫă14DfI@ ;SM1C%XoeI_o!~~ٷ;0rfZIUL9V w"<-A:,̍"J ڧi@HFIg >dP2-){pBdK{yEJ+ 6)/+/|RgIx` 9յXOaou[J[>%=%]t]~ m/ߛx͒L~9%\}@ntYACJsMɥ1>~2"9"#JrDu.dD(C(UjOb$W _d[`_Xm~~quznV&6wdݍ^ф+) :Be!_'fZ5LqWAI@%{R |'_b.^b&DeIm"(pI:}Kaz<7x+q;n5ݽ#/U iWBUP'pQHrj^uM`1>ل mtاxFMlv9S-)+.Iϡ9d0ʼnۯK?fEcϽ_=z쓭NY{3c_n|A3u^Y\|i^wՇJFH?PCfaњ'N*npZ䔜q*l`J`PLQK;eT%5t}3ER\)&I"5)Wſ'įeK&MX)na%Oq ? 4˄TT<"E#lE m#xkM4Zs]Lz|1:';LvdER@h{:@^cl7Ppr3GTK5U"Y~~?) zgwmvE1:C1QfwOa{f,r}c"{< lWsW>+Mַ[Fk^mlO}^_$/ʮY4|!|,(^7DŽt1y/ƿy%ۿKo'<ߧރmL<Lc3o ootMKee7)tm67CMnnp3Y$/TdC}<|蘷{By6!zxrՈΈױzV 6tAz M}/-C;"pb6 gz a fwѯP< `g2}Λ_Q~ė.y?<}fw|"_8 _ܳGj/dY .%K92$R6bRm ?E6F>,DK s8)b7aB )8(Oj4İZ6 2(~<'pzp}^|f";N}]SyړkpwWPY30{3B!)m^ڛS̚effYȷ[e!1p;qjZ<10m&~6i6h#6~V۫V%dO0Bc1K0TG[tTRv߅-졯t k=lutܯqѥxMut  iځ6! $~g^I62gޗݤ h邶DYǭ[03Zt)3&s))d6ǽIX3*RWYW[BMePqM'Jž{%ڞӉ6i%ǷGwÓ|C0 _ypP)u(-M#-.+R6?)|e; ޸L{~xZu=ÍSٟmXg~۳WAn^z])tZ~JSrBmFnA BCu*LODjO5JG qªb(mnfݓ?MU|;*<ޢ${䎭E6^ Ѳ[tο|=MXx.Y^u[3١aF ?QlK4jP954!č 0!->P ,&MUCbp@DGF' | *"L8A>:Թ>iO[?ܼ;+k_$LjnWڮt֩Ie{fGZÑH1ڞe{4f8y"w8O|!Rg$*|Ng3$E]{e<[-MH/M={aJm|s}icw16U&R78 #3]V@I&giYXSܤ$,5i,&jw&nU gh:5hBN$yhܖs_;C6F?z}Ht=DTFy- 8MOE&ZD='Y+4SNwŖ)L_)f*^y ;A%*AÔ' ؊aU`H'ߣ,]}j5@Ҋ#]V:P+|>&0v>g$v*\0 g"[m뿭T] -tGtҩ:wȩkT*I]F7 R(tjeb:YcA̶\pR/7DhmuCůNJ{⇋+=@@bDU;kUj{KK0eᎢWJ  R]nO&_.g63LR;<Xdwd3X[k+FޓF%^;N$Jb^ՔgLE-ڙ\mSWjASf ^Bk )&&ft+@p ,^vYAHm%,nּ#x3wC·c߼sPb췂X*Twﰪa=s^ej=:ƣBW%|ߥy~D$oҢ5I$!ҼCʣv4V sllyN\ ]0iuڂEE3&B \4@8{No>R 4"ŐD~j`Z51 `ګS\ƕA~,pEA _+&LR&ݻmz]lGFo_ܑ|8oh2-YP2Oogo &}?8Ϊ'[FO\MI)aDap񋅒ؒ"[K,pٌ.!~7(4K]1im4;Q!%$DBM4Rx)\$R\|h U>K.D%C]9;Ř)|k:^~T:)u~|Gߕ(%fr3Iլׇ }ƍvCqVƻ?dEN1Q,FgAtM (#Et#'T;:ˆ+<)1[&#]~{ݱx#ٽzg{w5ݺM2&}s#oH1Bs;͝[n[߶؂]$_yx?ɦYQ wۭW=n-GÜ1Jq6:[z _e[93"fم_dzp.{gyؤ_v7[uw77?6l1 ՜lf֖Su*& JVjOOsّ_pN/l6i\ӃU] ?ͷ3Ubno`WtC}l|_!}?v]U'm&iB{L<,4^sRmU7CrzӪiU60&ʒ &ef={=)G(bM-h\eOxW,n,}t} vDy/nQW_U tRk4&mLfegqN:{UDTYV^mѬM6nJTb71dBr.SHv]*VY3t yvSnJayɂ@,-)kzV4 L!\Is2IY6 -^v2:s@Sc?^vKhuC4nn7=kB;{wl)}2ͦXs^5P.z:~04&GRJjV+Z* \. u9M W Lgn1э#k )o/ܜBuRVs0C&0^aҕ~0$]r$rá xf33U''U'ԧ5?rZ ͦu6FƤ;m~O '$ ӒiqYT6)QO4j>tJuƙtket`tYPmR(uN3e]*0!l&`Y.4ʵV\I݂I>riJ|niKkz+*=)+={>n~K^ౕjkr7˟|;| )włO]-TYB 0ST((QIUZ_@4C○'YRrRwT_7 =s+pӒE`TJ%eB~^~$<}`߼x,ht(C=;ܭ?ޞwPzn^:z.K{v˹":S . 3ZeP\FA@T2' Q ~΋0|gܗzq^_ȪxbFR9_I].tvwlLٵ#.מϦ>#31ԇS ՓT,s4s \ո,髋$e0}V=%]Spp'քFeZuW鉳5TN\++- w竤7 Z>Q7n^48}b_V,dGv ~,_#5yX¶6J~LùM CN<1h c0v76ǘ}Bi-/:~ͼi֢97P.<úo)>}~ױ]-"dz vSM5=|&e6Tgj[8)O{sj:wMvX"7zt7Zna]WߔXa |_g57P*U.ؓJ&ƕfKJ-=QxdX^%+n[>#B g>Zܻ[y,5]QxnuX|{毚 cΟL%zVl!J_Qz'VE\KO;/WAli(5IpnaxN!IMKc60s IۦqnqTUj {t!=~wLt+"c硌7|` }`Cp4x^ 5׀[xƲ/24y|$&O^,7FVHbHYIO  L>''P<_x9ЦTQWgc}:!}aq;X5٘bLzjF#i539[3=2B=@fo̹:GKzxRdc ?OM=^tK+Rbinҗ敝ߩH󪢪5:3i555R?ԍ_&RL}:ܱja$q[ҦOe 4UDG2$z >VSqa-aCU HFÚdDV&& S0#j H*VN +^U Ie kHt%eX|Y2'I{e@%TŠdAMȾ JBHu.+#9尚pX8E_ag2 O땿zK6pG9lⰔIOs8o84[@98R2a7 9+8<- a / V#lqץ4л4E?"trx 0QK=D  T})'J&&"} WʯT_ *Ceܷbtt@^G)@AY{e8m"\9syK<yڝqC<-)︿DL,KfS%_FoaN.Kcs\rM)S.wA!&AǟƹRKwyD]07 dyo'ʭ4N6wc @/6wPĹpo(C=boHE} R1&&6'"xUU%S*+khD\mᠯkq'[=Eٷzi   p(jNXǂfþ@/N R@G0 mbG&q/*zbc{{i]@'%R:¾΁^.t` v8 I/ ;C]L=}b d,l1 b[ "Jt/ݡp@@E/C5!@=|=" `@^`DBV;ŠV'  ݡPF_? @_xHm}h8vCF#r/EhA&HW5 LJjs =#Z>Hb`[NĎG"bwh7d A$*n.L;QD1Zb_DS3 G!2FQN= lɸxc÷1I) z[0,0vO?녢J1`ˆuxjEhouqqQ,EPwqg;wGVE/FtoGYDnT(?w@Wqzo^ {@GAX.7Gtڠ m#p2=@x#w8b>O9AnrB/ZKja(,$VVN-)/)j4gIiiy9*ĊӫWu_icħbxT5'yma_ kjNQik ' O]YҿO,_YҿeIʒ%+KW,_YҿeIʒKaǿ\_hM |I]\'<+\REb*./Ke!o3HCQvq.'|ߡfsm`'#7`^0*b}B`69 S_.*>q*8Er\/u%j \.#S$זW6X\mJ~*^);E̔7yDH!C`}m$S T`ǞZVccb#n'vV agGLֲײ_#_O(=^0''`T}{e3{ f5`Nہl0 z l3{7ٛPW+ʎqXy2`ϐ[j(ܣ#b ׈f5`zz ]`3Fq^8C3`~ uR f 0d3~2Me&v +1{/#p]a/ ܤVX[ JH=Vke'W`<ٍwN V, Y,˛iqZ -;좍1 xX'G^>$dFaC^_]$JH䦆ׯX~H5X긟^_?:Y) DA?VF}/%d'FEaV" t++W,?`H< ^lIh tʮ DHJ , endstream endobj 432 0 obj 29527 endobj 433 0 obj <> endobj 434 0 obj <> stream x]͎@~ 7{?!? { 9quu)PM͸gvߵpim ~{ uOvy֣=}=l=Wc|3iW0u+:'GG647hW7 ^_{?CG~иwٛ7ӓZpz œ_4&֚l֓kO{0{_y_!G= / ᡘ8#v&ىk 5B_aB~q t  :߃!{b{'׸`9+TcG%`'?h/#E?bhX+_pvآ,DG'p 31 A YzT7> endstream endobj 435 0 obj <> endobj 436 0 obj <> endobj 437 0 obj <> /ProcSet[/PDF/Text/ImageC/ImageI/ImageB] >> endobj 1 0 obj <>/Contents 2 0 R>> endobj 6 0 obj <>/Contents 7 0 R>> endobj 9 0 obj <>/Contents 10 0 R>> endobj 12 0 obj <>/Contents 13 0 R>> endobj 15 0 obj <>/Contents 16 0 R>> endobj 18 0 obj <>/Contents 19 0 R>> endobj 21 0 obj <>/Contents 22 0 R>> endobj 24 0 obj <>/Contents 25 0 R>> endobj 27 0 obj <>/Contents 28 0 R>> endobj 30 0 obj <>/Contents 31 0 R>> endobj 33 0 obj <>/Contents 34 0 R>> endobj 36 0 obj <>/Contents 37 0 R>> endobj 39 0 obj <>/Contents 40 0 R>> endobj 42 0 obj <>/Contents 43 0 R>> endobj 45 0 obj <>/Contents 46 0 R>> endobj 48 0 obj <>/Contents 49 0 R>> endobj 51 0 obj <>/Contents 52 0 R>> endobj 54 0 obj <>/Contents 55 0 R>> endobj 57 0 obj <>/Contents 58 0 R>> endobj 60 0 obj <>/Contents 61 0 R>> endobj 63 0 obj <>/Contents 64 0 R>> endobj 66 0 obj <>/Contents 67 0 R>> endobj 69 0 obj <>/Contents 70 0 R>> endobj 72 0 obj <>/Contents 73 0 R>> endobj 75 0 obj <>/Contents 76 0 R>> endobj 78 0 obj <>/Contents 79 0 R>> endobj 81 0 obj <>/Contents 82 0 R>> endobj 84 0 obj <>/Contents 85 0 R>> endobj 87 0 obj <>/Contents 88 0 R>> endobj 90 0 obj <>/Contents 91 0 R>> endobj 93 0 obj <>/Contents 94 0 R>> endobj 96 0 obj <>/Contents 97 0 R>> endobj 99 0 obj <>/Contents 100 0 R>> endobj 102 0 obj <>/Contents 103 0 R>> endobj 105 0 obj <>/Contents 106 0 R>> endobj 108 0 obj <>/Contents 109 0 R>> endobj 111 0 obj <>/Contents 112 0 R>> endobj 114 0 obj <>/Contents 115 0 R>> endobj 117 0 obj <>/Contents 118 0 R>> endobj 120 0 obj <>/Contents 121 0 R>> endobj 123 0 obj <>/Contents 124 0 R>> endobj 126 0 obj <>/Contents 127 0 R>> endobj 129 0 obj <>/Contents 130 0 R>> endobj 132 0 obj <>/Contents 133 0 R>> endobj 135 0 obj <>/Contents 136 0 R>> endobj 138 0 obj <>/Contents 139 0 R>> endobj 141 0 obj <>/Contents 142 0 R>> endobj 144 0 obj <>/Contents 145 0 R>> endobj 147 0 obj <>/Contents 148 0 R>> endobj 150 0 obj <>/Contents 151 0 R>> endobj 153 0 obj <>/Contents 154 0 R>> endobj 156 0 obj <>/Contents 157 0 R>> endobj 159 0 obj <>/Contents 160 0 R>> endobj 162 0 obj <>/Contents 163 0 R>> endobj 165 0 obj <>/Contents 166 0 R>> endobj 168 0 obj <>/Contents 169 0 R>> endobj 171 0 obj <>/Contents 172 0 R>> endobj 174 0 obj <>/Contents 175 0 R>> endobj 177 0 obj <>/Contents 178 0 R>> endobj 180 0 obj <>/Contents 181 0 R>> endobj 183 0 obj <>/Contents 184 0 R>> endobj 186 0 obj <>/Contents 187 0 R>> endobj 189 0 obj <>/Contents 190 0 R>> endobj 194 0 obj <>/Contents 195 0 R>> endobj 197 0 obj <>/Contents 198 0 R>> endobj 200 0 obj <>/Contents 201 0 R>> endobj 203 0 obj <>/Contents 204 0 R>> endobj 208 0 obj <>/Contents 209 0 R>> endobj 211 0 obj <>/Contents 212 0 R>> endobj 214 0 obj <>/Contents 215 0 R>> endobj 217 0 obj <>/Contents 218 0 R>> endobj 220 0 obj <>/Contents 221 0 R>> endobj 223 0 obj <>/Contents 224 0 R>> endobj 228 0 obj <>/Contents 229 0 R>> endobj 233 0 obj <>/Contents 234 0 R>> endobj 236 0 obj <>/Contents 237 0 R>> endobj 239 0 obj <>/Contents 240 0 R>> endobj 242 0 obj <>/Contents 243 0 R>> endobj 245 0 obj <>/Contents 246 0 R>> endobj 248 0 obj <>/Contents 249 0 R>> endobj 251 0 obj <>/Contents 252 0 R>> endobj 258 0 obj <>/Contents 259 0 R>> endobj 263 0 obj <>/Contents 264 0 R>> endobj 266 0 obj <>/Contents 267 0 R>> endobj 269 0 obj <>/Contents 270 0 R>> endobj 272 0 obj <>/Contents 273 0 R>> endobj 279 0 obj <>/Contents 280 0 R>> endobj 290 0 obj <>/Contents 291 0 R>> endobj 293 0 obj <>/Contents 294 0 R>> endobj 298 0 obj <>/Contents 299 0 R>> endobj 303 0 obj <>/Contents 304 0 R>> endobj 306 0 obj <>/Contents 307 0 R>> endobj 309 0 obj <>/Contents 310 0 R>> endobj 314 0 obj <>/Contents 315 0 R>> endobj 317 0 obj <>/Contents 318 0 R>> endobj 320 0 obj <>/Contents 321 0 R>> endobj 323 0 obj <>/Contents 324 0 R>> endobj 326 0 obj <>/Contents 327 0 R>> endobj 329 0 obj <>/Contents 330 0 R>> endobj 332 0 obj <>/Contents 333 0 R>> endobj 335 0 obj <>/Contents 336 0 R>> endobj 340 0 obj <>/Contents 341 0 R>> endobj 345 0 obj <>/Contents 346 0 R>> endobj 350 0 obj <>/Contents 351 0 R>> endobj 355 0 obj <>/Contents 356 0 R>> endobj 358 0 obj <>/Contents 359 0 R>> endobj 361 0 obj <>/Contents 362 0 R>> endobj 366 0 obj <>/Contents 367 0 R>> endobj 369 0 obj <>/Contents 370 0 R>> endobj 372 0 obj <>/Contents 373 0 R>> endobj 377 0 obj <>/Contents 378 0 R>> endobj 380 0 obj <>/Contents 381 0 R>> endobj 383 0 obj <>/Contents 384 0 R>> endobj 386 0 obj <>/Contents 387 0 R>> endobj 389 0 obj <>/Contents 390 0 R>> endobj 392 0 obj <>/Contents 393 0 R>> endobj 395 0 obj <>/Contents 396 0 R>> endobj 398 0 obj <>/Contents 399 0 R>> endobj 401 0 obj <>/Contents 402 0 R>> endobj 406 0 obj <>/Contents 407 0 R>> endobj 409 0 obj <>/Contents 410 0 R>> endobj 412 0 obj <>/Contents 413 0 R>> endobj 415 0 obj <> endobj 438 0 obj <> >> endobj 439 0 obj < /Author /Creator /Producer /CreationDate(D:20080229120943-06'00')>> endobj xref 0 440 0000000000 65535 f 0000636210 00000 n 0000000019 00000 n 0000000427 00000 n 0000079276 00000 n 0000000447 00000 n 0000636356 00000 n 0000095090 00000 n 0000095557 00000 n 0000636502 00000 n 0000095577 00000 n 0000096221 00000 n 0000636649 00000 n 0000096242 00000 n 0000097028 00000 n 0000636797 00000 n 0000097049 00000 n 0000097734 00000 n 0000636945 00000 n 0000097755 00000 n 0000098395 00000 n 0000637093 00000 n 0000098416 00000 n 0000099341 00000 n 0000637241 00000 n 0000099362 00000 n 0000099912 00000 n 0000637389 00000 n 0000099933 00000 n 0000100777 00000 n 0000637537 00000 n 0000100798 00000 n 0000101383 00000 n 0000637685 00000 n 0000101404 00000 n 0000102106 00000 n 0000637833 00000 n 0000102127 00000 n 0000102810 00000 n 0000637981 00000 n 0000102831 00000 n 0000103590 00000 n 0000638129 00000 n 0000103611 00000 n 0000104406 00000 n 0000638277 00000 n 0000104427 00000 n 0000105085 00000 n 0000638425 00000 n 0000105106 00000 n 0000105850 00000 n 0000638573 00000 n 0000105871 00000 n 0000106473 00000 n 0000638721 00000 n 0000106494 00000 n 0000107018 00000 n 0000638869 00000 n 0000107039 00000 n 0000108281 00000 n 0000639017 00000 n 0000108303 00000 n 0000109544 00000 n 0000639165 00000 n 0000109566 00000 n 0000110388 00000 n 0000639313 00000 n 0000110409 00000 n 0000111536 00000 n 0000639461 00000 n 0000111558 00000 n 0000112488 00000 n 0000639609 00000 n 0000112509 00000 n 0000113828 00000 n 0000639757 00000 n 0000113850 00000 n 0000114898 00000 n 0000639905 00000 n 0000114919 00000 n 0000116428 00000 n 0000640053 00000 n 0000116450 00000 n 0000117741 00000 n 0000640201 00000 n 0000117763 00000 n 0000119097 00000 n 0000640349 00000 n 0000119119 00000 n 0000120042 00000 n 0000640497 00000 n 0000120063 00000 n 0000120622 00000 n 0000640645 00000 n 0000120643 00000 n 0000121493 00000 n 0000640793 00000 n 0000121514 00000 n 0000123149 00000 n 0000640941 00000 n 0000123171 00000 n 0000124597 00000 n 0000641090 00000 n 0000124620 00000 n 0000126075 00000 n 0000641240 00000 n 0000126098 00000 n 0000127161 00000 n 0000641390 00000 n 0000127183 00000 n 0000128547 00000 n 0000641540 00000 n 0000128570 00000 n 0000129459 00000 n 0000641690 00000 n 0000129481 00000 n 0000130150 00000 n 0000641840 00000 n 0000130172 00000 n 0000131075 00000 n 0000641990 00000 n 0000131097 00000 n 0000132093 00000 n 0000642140 00000 n 0000132115 00000 n 0000133168 00000 n 0000642290 00000 n 0000133190 00000 n 0000134407 00000 n 0000642440 00000 n 0000134430 00000 n 0000135259 00000 n 0000642590 00000 n 0000135281 00000 n 0000136024 00000 n 0000642740 00000 n 0000136046 00000 n 0000137151 00000 n 0000642890 00000 n 0000137174 00000 n 0000139312 00000 n 0000643040 00000 n 0000139335 00000 n 0000140571 00000 n 0000643190 00000 n 0000140594 00000 n 0000141449 00000 n 0000643340 00000 n 0000141471 00000 n 0000142313 00000 n 0000643490 00000 n 0000142335 00000 n 0000143214 00000 n 0000643640 00000 n 0000143236 00000 n 0000144196 00000 n 0000643790 00000 n 0000144218 00000 n 0000145147 00000 n 0000643940 00000 n 0000145169 00000 n 0000145779 00000 n 0000644090 00000 n 0000145801 00000 n 0000146961 00000 n 0000644240 00000 n 0000146984 00000 n 0000148063 00000 n 0000644390 00000 n 0000148086 00000 n 0000149485 00000 n 0000644540 00000 n 0000149508 00000 n 0000150904 00000 n 0000644690 00000 n 0000150927 00000 n 0000157201 00000 n 0000644840 00000 n 0000157224 00000 n 0000157990 00000 n 0000644990 00000 n 0000158012 00000 n 0000159022 00000 n 0000645140 00000 n 0000159044 00000 n 0000160018 00000 n 0000645290 00000 n 0000160040 00000 n 0000161135 00000 n 0000645440 00000 n 0000161158 00000 n 0000161626 00000 n 0000161648 00000 n 0000172042 00000 n 0000645590 00000 n 0000172066 00000 n 0000174141 00000 n 0000645740 00000 n 0000174164 00000 n 0000174964 00000 n 0000645890 00000 n 0000174986 00000 n 0000176047 00000 n 0000646040 00000 n 0000176069 00000 n 0000176958 00000 n 0000176980 00000 n 0000186814 00000 n 0000646190 00000 n 0000186837 00000 n 0000187672 00000 n 0000646340 00000 n 0000187694 00000 n 0000188563 00000 n 0000646490 00000 n 0000188585 00000 n 0000189529 00000 n 0000646640 00000 n 0000189551 00000 n 0000190799 00000 n 0000646790 00000 n 0000190822 00000 n 0000192126 00000 n 0000646940 00000 n 0000192149 00000 n 0000192867 00000 n 0000192889 00000 n 0000204601 00000 n 0000647090 00000 n 0000204625 00000 n 0000205455 00000 n 0000205477 00000 n 0000214931 00000 n 0000647240 00000 n 0000214954 00000 n 0000216201 00000 n 0000647390 00000 n 0000216224 00000 n 0000217674 00000 n 0000647540 00000 n 0000217697 00000 n 0000218992 00000 n 0000647690 00000 n 0000219015 00000 n 0000220437 00000 n 0000647840 00000 n 0000220460 00000 n 0000221270 00000 n 0000647990 00000 n 0000221292 00000 n 0000222324 00000 n 0000648140 00000 n 0000222346 00000 n 0000223719 00000 n 0000230588 00000 n 0000223742 00000 n 0000230565 00000 n 0000238510 00000 n 0000648290 00000 n 0000238533 00000 n 0000239740 00000 n 0000239763 00000 n 0000248441 00000 n 0000648440 00000 n 0000248464 00000 n 0000249517 00000 n 0000648590 00000 n 0000249539 00000 n 0000251032 00000 n 0000648740 00000 n 0000251055 00000 n 0000251964 00000 n 0000648890 00000 n 0000251986 00000 n 0000253029 00000 n 0000259869 00000 n 0000253051 00000 n 0000259846 00000 n 0000266670 00000 n 0000649040 00000 n 0000266693 00000 n 0000268693 00000 n 0000290987 00000 n 0000284912 00000 n 0000276925 00000 n 0000268716 00000 n 0000276902 00000 n 0000284889 00000 n 0000290964 00000 n 0000298772 00000 n 0000649190 00000 n 0000298795 00000 n 0000300642 00000 n 0000649340 00000 n 0000300665 00000 n 0000301048 00000 n 0000301070 00000 n 0000336517 00000 n 0000649490 00000 n 0000336541 00000 n 0000337916 00000 n 0000337939 00000 n 0000346611 00000 n 0000649640 00000 n 0000346634 00000 n 0000347534 00000 n 0000649790 00000 n 0000347556 00000 n 0000349071 00000 n 0000649940 00000 n 0000349094 00000 n 0000349507 00000 n 0000349529 00000 n 0000374288 00000 n 0000650090 00000 n 0000374312 00000 n 0000375787 00000 n 0000650240 00000 n 0000375810 00000 n 0000377716 00000 n 0000650390 00000 n 0000377739 00000 n 0000379058 00000 n 0000650540 00000 n 0000379081 00000 n 0000380629 00000 n 0000650690 00000 n 0000380652 00000 n 0000381542 00000 n 0000650840 00000 n 0000381564 00000 n 0000382549 00000 n 0000650990 00000 n 0000382571 00000 n 0000384007 00000 n 0000651140 00000 n 0000384030 00000 n 0000384916 00000 n 0000384938 00000 n 0000391284 00000 n 0000651290 00000 n 0000391307 00000 n 0000392156 00000 n 0000392178 00000 n 0000402098 00000 n 0000651440 00000 n 0000402121 00000 n 0000402943 00000 n 0000402965 00000 n 0000412832 00000 n 0000651590 00000 n 0000412855 00000 n 0000413919 00000 n 0000413941 00000 n 0000441964 00000 n 0000651740 00000 n 0000441988 00000 n 0000443096 00000 n 0000651890 00000 n 0000443119 00000 n 0000445096 00000 n 0000652040 00000 n 0000445119 00000 n 0000445529 00000 n 0000445551 00000 n 0000459899 00000 n 0000652190 00000 n 0000459923 00000 n 0000460935 00000 n 0000652340 00000 n 0000460957 00000 n 0000462216 00000 n 0000652490 00000 n 0000462239 00000 n 0000462709 00000 n 0000462731 00000 n 0000521523 00000 n 0000652640 00000 n 0000521547 00000 n 0000522619 00000 n 0000652790 00000 n 0000522641 00000 n 0000524101 00000 n 0000652940 00000 n 0000524124 00000 n 0000525325 00000 n 0000653090 00000 n 0000525348 00000 n 0000526543 00000 n 0000653240 00000 n 0000526566 00000 n 0000528209 00000 n 0000653390 00000 n 0000528232 00000 n 0000529247 00000 n 0000653540 00000 n 0000529269 00000 n 0000530570 00000 n 0000653690 00000 n 0000530593 00000 n 0000531388 00000 n 0000653840 00000 n 0000531410 00000 n 0000531794 00000 n 0000531816 00000 n 0000547783 00000 n 0000653990 00000 n 0000547807 00000 n 0000548712 00000 n 0000654140 00000 n 0000548734 00000 n 0000549915 00000 n 0000654290 00000 n 0000549938 00000 n 0000551038 00000 n 0000654440 00000 n 0000551061 00000 n 0000566973 00000 n 0000566997 00000 n 0000567202 00000 n 0000567621 00000 n 0000567902 00000 n 0000589099 00000 n 0000589123 00000 n 0000589319 00000 n 0000589881 00000 n 0000590288 00000 n 0000603713 00000 n 0000603737 00000 n 0000603936 00000 n 0000604378 00000 n 0000604675 00000 n 0000634291 00000 n 0000634315 00000 n 0000634506 00000 n 0000635186 00000 n 0000635706 00000 n 0000635774 00000 n 0000655478 00000 n 0000655630 00000 n trailer < ] /DocChecksum /1693DFC9B750B3F896CF468A1AF48A0B >> startxref 655953 %%EOF ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/docs/traitsuidocreadme.txt0000644000175100001730000000024000000000000021362 0ustar00runnerdocker00000000000000Traits documentation files in this directory: traits_ui.ppt Slides from Dave Morrill's class on Traits UI traits_ui_slides.pdf PDF of Traits UI slides ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/edm.yaml0000644000175100001730000000052400000000000015617 0ustar00runnerdocker00000000000000# This edm.yaml configuration file provides a standard EDM setup to be used by # the etstool.py script, particularly for CI store_url: "https://packages.enthought.com" # Packages' repositories repositories: - enthought/free - enthought/lgpl - enthought/gpl # Directory to use to cache downloaded files. files_cache: "~/.cache/edm" ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1684768028.0318072 traitsui-8.0.0/examples/0000755000175100001730000000000000000000000016003 5ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/examples/README.rst0000644000175100001730000000047100000000000017474 0ustar00runnerdocker00000000000000This folder contains examples for demonstration and documentation purposes. To aid discovery, symbolic links are created for files hosted in the package in order to be included as package data. If you have ``etsdemo`` package installed, you can browse the demo files with the following command:: $ etsdemo ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1684768028.0318072 traitsui-8.0.0/examples/demo/0000755000175100001730000000000000000000000016727 5ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1684768028.0398073 traitsui-8.0.0/examples/demo/Advanced/0000755000175100001730000000000000000000000020434 5ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/examples/demo/Advanced/Adapted_tree_editor_demo.py0000644000175100001730000001030600000000000025741 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Demonstrates an alternative method of defining a **TreeEditor** by creating **ITreeNodeAdapter** subclasses. To run this demonstration successfully, you must have **AppTools** (``apptools``) installed. Using **ITreeNodeAdapters** can be useful in cases where the kind of content of the tree is not always known ahead of time. For example, you might be creating a reusable tool or component which can display its data in a tree view, but you do not know what kind of data it will be asked to display when you write the code. Therefore, it may be impossible for you to specify a **TreeEditor** with a correct set of **TreeNode** objects that will work in all possible future cases. Using **ITreeNodeAdapter** subclasses, you can allow the clients of your code to solve this problem by providing one of more **ITreeNodeAdapters** that can be used to provide the correct tree node information for each type of data that will appear in the **TreeEditor** view. In this demo, we define an **ITreeNodeAdapter** subclass that adapts the *apptools.io.file.File* class to be displayed in a file explorer style tree view. """ # -- Imports -------------------------------------------------------------- from os import getcwd from traits.api import ( HasTraits, Property, Directory, register_factory, cached_property ) from traitsui.api import ( View, VGroup, Item, TreeEditor, ITreeNode, ITreeNodeAdapter, ) from apptools.io.api import File # -- FileAdapter Class ---------------------------------------------------- class FileAdapter(ITreeNodeAdapter): # -- ITreeNodeAdapter Method Overrides ------------------------------------ def allows_children(self): """Returns whether this object can have children.""" return self.adaptee.is_folder def has_children(self): """Returns whether the object has children.""" children = self.adaptee.children return (children is not None) and (len(children) > 0) def get_children(self): """Gets the object's children.""" return self.adaptee.children def get_label(self): """Gets the label to display for a specified object.""" return self.adaptee.name + self.adaptee.ext def get_tooltip(self): """Gets the tooltip to display for a specified object.""" return self.adaptee.absolute_path def get_icon(self, is_expanded): """Returns the icon for a specified object.""" if self.adaptee.is_file: return '' if is_expanded: return '' return '' def can_auto_close(self): """Returns whether the object's children should be automatically closed. """ return True # -- FileTreeDemo Class --------------------------------------------------- class FileTreeDemo(HasTraits): # The path to the file tree root: root_path = Directory(entries=10) # The root of the file tree: root = Property(observe='root_path') # The traits view to display: view = View( VGroup( Item('root_path'), Item('root', editor=TreeEditor(editable=False, auto_open=1)), show_labels=False, ), width=0.33, height=0.50, resizable=True, ) # -- Traits Default Value Methods ----------------------------------------- def _root_path_default(self): return getcwd() # -- Property Implementations --------------------------------------------- @cached_property def _get_root(self): return File(path=self.root_path) # -- Create and run the demo ---------------------------------------------- register_factory(FileAdapter, File, ITreeNode) demo = FileTreeDemo() # Run the demo (if invoked form the command line): if __name__ == '__main__': demo.configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/examples/demo/Advanced/Apply_Revert_handler_demo.py0000644000175100001730000000513400000000000026126 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Apply / Revert Provides support in a dialog box for an "Apply" button which modifies the object being viewed, and a "Revert" button, which returns the object to its starting state (before any "Apply"). Note that this does not automatically provide a full (multi-step incremental) Undo capability. """ from traits.api import HasTraits, Str, List from traitsui.api import Item, View, Handler, HGroup, VGroup, TextEditor class ApplyRevert_Handler(Handler): def apply(self, info): print('apply called...') object = info.object object.stack.insert(0, object.input) object.queue.append(object.input) def revert(self, info): # Do something exciting here... print('revert called...') class ApplyRevertDemo(HasTraits): # Trait definitions: input = Str() stack = List() queue = List() # Traits view definitions: traits_view = View( VGroup( VGroup( Item( 'input', show_label=False, editor=TextEditor(auto_set=True) ), label='Input', show_border=True, ), HGroup( VGroup( Item( 'stack', show_label=False, height=50, width=100, style='readonly', ), label='Stack', show_border=True, ), VGroup( Item( 'queue', show_label=False, height=50, width=100, style='readonly', ), label='Queue', show_border=True, ), ), ), resizable=True, height=300, title='Apply/Revert example', buttons=['Apply', 'Revert'], handler=ApplyRevert_Handler, ) # Create the demo: modal_popup = ApplyRevertDemo() # Run the demo (if invoked from the command line): if __name__ == '__main__': modal_popup.configure_traits(kind='modal') ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/examples/demo/Advanced/Auto_editable_readonly_table_cells.py0000644000175100001730000001151200000000000027775 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ This example shows how to define a read-only, auto-edit table column using a custom pop-up view. The example displays a list of integer values from 1 to n, where 'n' can be set using the slider at the top of the view. Each entry in the list shows the value of the integer and the number of unique factors it has. Mousing over the number of factors for a particular integer displays a pop-up list containing the unique factors for the integer. Mousing out of the cell causes the pop-up list to be removed (and perhaps causes a new pop-up list to be displayed, depending upon whether the mouse entered a new cell or not). Creating the auto pop-up effect is achieved by setting the 'auto_editable' trait of the associated ObjectColumn to True and also specifying a view to display on mouse over as the value of the ObjectColumn's 'view' trait. Note that this style of auto pop-up view can only be used with non-editable table editor fields. If the field is editable, then setting 'auto_editable' to True will cause the editor associated with the ObjectColumn to be automatically activated on mouse over, rather than the pop-up view specified by the 'view' trait. """ # -- Imports -------------------------------------------------------------- from operator import attrgetter from traits.api import ( HasTraits, Int, List, Range, Property, cached_property, ) from traitsui.api import View, VGroup, Item, TableEditor from traitsui.table_column import ObjectColumn # -- Integer Class -------------------------------------------------------- class Integer(HasTraits): # The value: n = Int() # -- Factor Class --------------------------------------------------------- class Factor(HasTraits): # The number being factored: n = Int() # The list of factors of 'n': factors = Property(List, observe='n') @cached_property def _get_factors(self): n = self.n i = 1 result = [] while (i * i) <= n: j = n // i if (i * j) == n: result.append(Integer(n=i)) if i != j: result.append(Integer(n=j)) i += 1 result.sort(key=attrgetter('n')) return result # -- The table editor used for the pop-up view ---------------------------- factor_table_editor = TableEditor( columns=[ ObjectColumn( name='n', width=1.0, editable=False, horizontal_alignment='center' ) ], sortable=False, auto_size=False, show_toolbar=False, show_column_labels=False, ) # -- The table editor used for the main view ------------------------------ factors_view = View( Item( 'factors', id='factors', show_label=False, editor=factor_table_editor, ), id='traits.examples.demo.Advanced.factors_view', kind='info', height=0.30, ) factors_table_editor = TableEditor( columns=[ ObjectColumn( name='n', width=0.5, editable=False, horizontal_alignment='center' ), ObjectColumn( name='factors', width=0.5, editable=False, horizontal_alignment='center', auto_editable=True, format_func=lambda f: '%s factors' % len(f), view=factors_view, ), ], sortable=False, auto_size=False, show_toolbar=False, ) # -- Factors Class -------------------------------------------------------- class Factors(HasTraits): # The maximum number to include in the table: max_n = Range(1, 1000, 20, mode='slider') # The list of Factor objects: factors = Property(List, observe='max_n') # The view of the list of Factor objects: view = View( VGroup( VGroup( Item('max_n'), show_labels=False, show_border=True, label='Maximum Number', ), VGroup( Item('factors', show_label=False, editor=factors_table_editor), ), ), title='List of numbers and their factors', width=0.2, height=0.4, resizable=True, ) @cached_property def _get_factors(self): return [Factor(n=i + 1) for i in range(self.max_n)] # -- Create and run the demo ---------------------------------------------- # Create the demo: demo = Factors() # Run the demo (if invoked from the command line): if __name__ == '__main__': demo.configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/examples/demo/Advanced/Auto_update_TabularEditor_demo.py0000644000175100001730000000670200000000000027112 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ **WARNING** This demo might not work as expected and some documented features might be missing. ------------------------------------------------------------------------------- Auto-update from a list to a TabularEditor Demonstrates using a TabularEditor with the 'auto_update' feature enabled, which allows the tabular editor to automatically update itself when the content of any object in the list associated with the editor is modified. To interact with the demo: - Select an employee from the list. - Adjust their salary increase. - Click the **Give raise** button. - Observe that the table automatically updates to reflect the employees new salary. In order for auto-update to work correctly, the editor trait should be a list of objects derived from HasTraits. Also, performance can be affected when very long lists are used, since enabling this feature adds and removed Traits listeners to each item in the list. """ # Issues related to the demo warning: # enthought/traitsui#960 from traits.api import HasTraits, Str, Float, List, Instance, Button from traitsui.api import ( View, HGroup, Item, TabularAdapter, TabularEditor, spring, ) # -- EmployeeAdapter Class ------------------------------------------------ class EmployeeAdapter(TabularAdapter): columns = [('Name', 'name'), ('Salary', 'salary')] def get_default_value(self, object, trait): return Employee(name="John", salary=30000) # -- Employee Class ------------------------------------------------------- class Employee(HasTraits): name = Str() salary = Float() # -- Company Class -------------------------------------------------------- class Company(HasTraits): employees = List(Employee) employee = Instance(Employee) increase = Float() give_raise = Button('Give raise') traits_view = View( Item( 'employees', show_label=False, editor=TabularEditor( adapter=EmployeeAdapter(), selected='employee', auto_resize=True, auto_update=True, ), ), HGroup( spring, Item('increase'), Item( 'give_raise', show_label=False, enabled_when='employee is not None', ), ), title='Auto Update Tabular Editor demo', height=0.25, width=0.30, resizable=True, ) def _give_raise_changed(self): self.employee.salary += self.increase self.employee = None # -- Set up the demo ------------------------------------------------------ demo = Company( increase=1000, employees=[ Employee(name='Fred', salary=45000), Employee(name='Sally', salary=52000), Employee(name='Jim', salary=39000), Employee(name='Helen', salary=41000), Employee(name='George', salary=49000), Employee(name='Betty', salary=46000), ], ) # Run the demo (if invoked from the command line): if __name__ == '__main__': demo.configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/examples/demo/Advanced/Date_editor_demo.py0000644000175100001730000000573200000000000024244 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ **WARNING** This demo might not work as expected and some documented features might be missing. ------------------------------------------------------------------------------- Implementation of a DateEditor demo plugin for Traits UI demo program. This demo shows a few different styles of the DateEditor and how it can be customized. """ # Issue related to the demo warning: enthought/traitsui#962 from traits.api import HasTraits, Date, List, Str from traitsui.api import View, Item, DateEditor, Group class DateEditorDemo(HasTraits): """Demo class to show Date editors.""" single_date = Date() multi_date = List(Date) info_string = Str( 'The editors for Traits Date objects. Showing both ' 'the defaults, and one with alternate options.' ) multi_select_editor = DateEditor( allow_future=False, multi_select=True, shift_to_select=False, on_mixed_select='max_change', # Qt ignores these setting and always shows only 1 month: months=2, padding=30, ) traits_view = View( Item('info_string', show_label=False, style='readonly'), Group( Item('single_date', label='Simple date editor'), Item('single_date', style='custom', label='Default custom editor'), Item( 'single_date', style='readonly', editor=DateEditor( strftime='You picked %B %d %Y', message='Click a date above.', ), label='ReadOnly editor', ), label='Default settings for editors', ), Group( Item( 'multi_date', editor=multi_select_editor, style='custom', label='Multi-select custom editor', ), label='More customized editor: multi-select; disallow ' 'future; selection style; etc.', ), resizable=True, ) def _multi_date_changed(self): """Print each time the date value is changed in the editor.""" print(self.multi_date) def _simple_date_changed(self): """Print each time the date value is changed in the editor.""" print(self.simple_date, self.single_date) def _single_date_changed(self): """Print each time the date value is changed in the editor.""" print(self.single_date) # -- Set Up The Demo ------------------------------------------------------ demo = DateEditorDemo() if __name__ == "__main__": demo.configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/examples/demo/Advanced/Date_range_editor_demo.py0000644000175100001730000000303300000000000025410 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Implementation of a DateRangeEditor demo plugin for Traits UI demo program. This demo shows a custom style DateRangeEditor. Note that this demo only works with qt backend. """ # Issue related to the demo warning: enthought/traitsui#962 from traits.api import HasTraits, Date, Tuple from traits.etsconfig.api import ETSConfig from traitsui.api import View, Item, DateRangeEditor, Group class DateRangeEditorDemo(HasTraits): """Demo class to show DateRangeEditor.""" date_range = Tuple(Date, Date) traits_view = View( Group( Item( 'date_range', editor=DateRangeEditor(), style='custom', label='Date range', ), label='Date range', ), resizable=True, ) def _date_range_changed(self): print(self.date_range) # -- Set Up The Demo ------------------------------------------------------ if __name__ == "__main__": if ETSConfig.toolkit in {"qt", "qt4"}: # DateRangeEditor is currently only available for qt backend. demo = DateRangeEditorDemo() demo.configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/examples/demo/Advanced/Dynamic_EnumEditor_demo.py0000644000175100001730000001151300000000000025532 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Dynamic changing an Enum selection list, depending on checked items Another way to dynamically change the values shown by an EnumEditor. The scenario is a restaurant that at the beginning of the day has a menu list of entrees based upon a fully stocked kitchen. However, as the day progresses, the kitchen's larder gets depleted, and the kitchen may no longer be able to prepare certain entrees, which must then be deleted from the menu. Similarly, deliveries may allow certain entrees to be added back onto the menu. The demo is divided into two tabs: Order and Kitchen. The Order tab represents a customer's order and consists of a single Entree field, which represents the customer's selection from the drop-down list of entrees that the kitchen can currently prepare. The Kitchen tab represents the current set of entrees that the kitchen can prepare, based upon the current contents of its larder. As entrees are checked on or off from the Kitchen tab, the customer's Entree drop-down is dynamically updated with the current list of available entrees. Notes: - The key point of the demo is the use of the 'name' trait in the EnumEditor definition, which links the list of available entrees from the KitchenCapabilities object to the OrderMenu object's entree EnumEditor. - The user can freely type any value they want, but only items in the capabilities will be accepted, due to the use of the 'values' argument to the Enum trait. This will be updated as the capabilities change. - With the Qt backend, the user can type text and it will be auto-completed to valid values as a the user types. The Wx backend doesn't support this capability in the underlying toolkit. - The design will work with any number of active OrderMenu objects, since they all share a common KitchenCapabilities object. As the KitchenCapabilities object is updated, all OrderMenu UI's will automatically update their associated Entree's drop-down list. - A careful reader will also observe that this example contains only declarative code. No imperative code is required to handle the automatic updating of the Entree list. """ from traits.api import Enum, HasPrivateTraits, Instance, List from traitsui.api import ( View, Item, VGroup, HSplit, EnumEditor, CheckListEditor, ) # -- The list of possible entrees ----------------------------------------- possible_entrees = [ 'Chicken Fried Steak', 'Chicken Fingers', 'Chicken Enchiladas', 'Cheeseburger', 'Pepper Steak', 'Beef Tacos', 'Club Sandwich', 'Ceasar Salad', 'Cobb Salad', ] # -- The KitchenCapabilities class ---------------------------------------- class KitchenCapabilities(HasPrivateTraits): # The current set of entrees the kitchen can make (based on its larder): available = List(possible_entrees) # The KitchenCapabilities are shared by all waitstaff taking orders: kitchen_capabilities = KitchenCapabilities() # -- The OrderMenu class -------------------------------------------------- class OrderMenu(HasPrivateTraits): # The person's entree order: entree = Enum(values='capabilities.available') # Reference to the restaurant's current entree capabilities: capabilities = Instance(KitchenCapabilities) def _capabilities_default(self): return kitchen_capabilities # The user interface view: view = View( HSplit( VGroup( Item( 'entree', editor=EnumEditor( name='object.capabilities.available', evaluate=str, completion_mode='popup', ), ), label='Order', show_border=True, dock='tab', ), VGroup( Item( 'object.capabilities.available', show_label=False, style='custom', editor=CheckListEditor(values=possible_entrees), ), label='Kitchen', show_border=True, dock='tab', ), ), title='Dynamic EnumEditor Demo', resizable=True, ) # ------------------------------------------------------------------------- # Create the demo: demo = OrderMenu() # Run the demo (if invoked from the command line): if __name__ == '__main__': demo.configure_traits() print(demo.entree) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/examples/demo/Advanced/Dynamic_range_trait_and_editor.py0000644000175100001730000001576300000000000027155 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ This program demonstrates defining and visualizing dynamic ranges. A dynamic range is a range whose low or high limit can be modified dynamically at run time. You can define a dynamic range using the standard Range trait and specifying the name of other traits as either the low or high limit value (or both). In fact, it is even possible to specify the default value of the range trait as another trait if desired. The traits used as low, high or default values need not be defined on the same object, but can be defined on any object reachable from the object (i.e. it allows use of extended trait names). In this completely artificial example, we present an example of the first hotel at the North Pole. The hotel guarantees that each room will be heated to a certain minimum temperature. However, that minimum value is determined both by the time of year and the current cost of heating fuel, so it can vary; but each room is guaranteed the same minimum temperature. Each guest of the hotel can choose among several different plans that allow them to control the maximum room temperature. Higher maximum room temperatures correspond to higher plan costs. Thus each guest must decide which plan (and highest maximum room temperature) to pay for. And finally, each guest is free to set the current room temperature anywhere between the hotel minimum value and the guest's maximum plan value. The demo is organized as a series of tabs corresponding to each guest of the hotel, with access to the plan they have chosen and the current room temperature setting. In addition, there is a master set of hotel information displayed at the top of the view which allows you to change the season of the year and the current fuel cost. There is also a button to allow more guests to be added to the hotel. Notes: - The dynamic range trait is the 'temperature' trait in the Guest class. It depends upon traits defined both in the Guest instance as well as in the containing Hotel object. - As with most traits code and examples, observe how much of the code is 'declarative' versus 'imperative'. Note also the use of properties and 'observe' metadata, as well as 'cached_property' and 'on_trait_change' method decorators. - Try dragging the guest tabs around so that you can see multiple guests simultaneously, and then watch the behavior of the guest's 'temperature' slider as you adjust the hotel 'season', 'fuel cost' and each guest's 'plan'. """ # -- Imports -------------------------------------------------------------- import logging import sys from random import choice from traits.api import ( HasPrivateTraits, Str, Enum, Range, List, Button, Instance, Property, cached_property, observe, ) from traitsui.api import View, VGroup, HGroup, Item, ListEditor, spring logging.basicConfig(stream=sys.stderr) # -- The Hotel class ------------------------------------------------------ class Hotel(HasPrivateTraits): # The season of the year: season = Enum('Winter', 'Spring', 'Summer', 'Fall') # The current cost of heating fuel (in dollars/gallon): fuel_cost = Range(2.00, 10.00, 4.00) # The current minimum temparature allowed by the hotel: min_temperature = Property(observe='season, fuel_cost') # The guests currently staying at the hotel: guests = List # ( Instance( 'Guest' ) ) # Add a new guest to the hotel: add_guest = Button('Add Guest') # The view of the hotel: view = View( VGroup( HGroup( Item('season'), '20', Item('fuel_cost', width=300), spring, Item('add_guest', show_label=False), show_border=True, label='Hotel Information', ), VGroup( Item( 'guests', style='custom', editor=ListEditor( use_notebook=True, deletable=True, dock_style='tab', page_name='.name', ), ), show_labels=False, show_border=True, label='Guests', ), ), title='The Belmont Hotel Dashboard', width=0.6, height=0.2, resizable=True, ) # Property implementations: @cached_property def _get_min_temperature(self): return {'Winter': 32, 'Spring': 40, 'Summer': 45, 'Fall': 40}[ self.season ] + min(int(60.00 / self.fuel_cost), 15) # Event handlers: @observe('guests.items') def _guests_modified(self, event): # The entire guests list changed if isinstance(event.object, Hotel): for guest in event.new: guest.hotel = self # the contents of the guests list changed else: for guest in event.added: guest.hotel = self def _add_guest_changed(self): self.guests.append(Guest(hotel=self)) # -- The Guest class ------------------------------------------------------ class Guest(HasPrivateTraits): # The name of the guest: name = Str() # The hotel the guest is staying at: hotel = Instance(Hotel) # The room plan the guest has chosen: plan = Enum('Flop house', 'Cheap', 'Cozy', 'Deluxe') # The maximum temperature allowed by the guest's plan: max_temperature = Property(observe='plan') # The current room temperature as set by the guest: temperature = Range('hotel.min_temperature', 'max_temperature') # The view of the guest: view = View(Item('plan'), Item('temperature')) # Property implementations: @cached_property def _get_max_temperature(self): return {'Flop house': 62, 'Cheap': 66, 'Cozy': 75, 'Deluxe': 85}[ self.plan ] # Default values: def _name_default(self): return choice( [ 'Leah', 'Vibha', 'Janet', 'Jody', 'Dave', 'Evan', 'Ilan', 'Gael', 'Peter', 'Robert', 'Judah', 'Eric', 'Travis', 'Mike', 'Bryce', 'Chris', ] ) # -- Create the demo ------------------------------------------------------ # Create the demo object: demo = Hotel(guests=[Guest() for i in range(5)]) # Run the demo (if invoked from the command line): if __name__ == '__main__': logging.info('Start!') demo.configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/examples/demo/Advanced/Dynamic_views_demo.py0000644000175100001730000001133000000000000024611 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Demonstrates how to use the Dynamic Views facility. """ from traits.api import Bool, HasTraits, Str, Instance, Button from traitsui.api import View, HGroup, Group, Item, Handler, Label, spring from traitsui.extras.has_dynamic_views import DynamicView, HasDynamicViews class HasFooView(HasDynamicViews): """A base class declaring the existence of the 'foo' dynamic view.""" def __init__(self, *args, **traits): """Constructor. Extended to declare our dynamic foo view. """ super().__init__(*args, **traits) # Declare and add our dynamic view: declaration = DynamicView( name='foo', id='traitsui.demos.dynamic_views', keywords={ 'buttons': ['OK'], 'dock': 'tab', 'height': 0.4, 'width': 0.4, 'resizable': True, 'scrollable': True, }, use_as_default=True, ) self.declare_dynamic_view(declaration) class MyInfoHandler(Handler): def object_first_changed(self, info): info.object.derived = info.object.first class BaseFoo(HasFooView): """A base class that puts some content in the 'foo' dynamic view.""" first = Str('My first name') last = Str('My last name') # A derived trait set by the handler associated with out dynamic view # contribution: derived = Str() ui_person = Group( Item( label='On this tab, notice how the sub-handler keeps\n' 'the derived value equal to the first name.\n\n' 'On the next tab, change the selection in order to\n' 'control which tabs are visible when the ui is \n' 'displayed for the 2nd time.' ), spring, 'first', 'last', spring, 'derived', label='My Info', _foo_order=5, _foo_priority=1, _foo_handler=MyInfoHandler(), ) class FatherInfoHandler(Handler): def object_father_first_name_changed(self, info): info.object.father_derived = info.object.father_first_name class DerivedFoo(BaseFoo): """A derived class that puts additional content in the 'foo' dynamic view. Note that the additional content could also have been added via a traits category contribution, or even dynamic manipulation of metadata on a UI subelement. The key is what the metadata represents when the view is *created* """ knows_mother = Bool(False) mother_first_name = Str("My mother's first name") mother_last_name = Str("My mother's last name") knows_father = Bool(True) father_first_name = Str("My father's first name") father_last_name = Str("My father's last name") father_derived = Str() ui_parents = Group( 'knows_mother', 'knows_father', label='Parents?', _foo_order=7, _foo_priority=1, ) ui_mother = Group( 'mother_first_name', 'mother_last_name', label="Mother's Info", _foo_priority=1, ) ui_father = Group( 'father_first_name', 'father_last_name', spring, 'father_derived', label="Father's Info", _foo_order=15, _foo_priority=1, _foo_handler=FatherInfoHandler(), ) def _knows_mother_changed(self, old, new): ui_mother = self.trait_view('ui_mother') if new: ui_mother._foo_order = 10 elif hasattr(ui_mother, '_foo_order'): del ui_mother._foo_order def _knows_father_changed(self, old, new): ui_father = self.trait_view('ui_father') if new: ui_father._foo_order = 15 elif hasattr(ui_father, '_foo_order'): del ui_father._foo_order class FooDemo(HasTraits): """Defines a class to run the demo.""" foo = Instance(DerivedFoo, ()) configure = Button('Configure') view = View( Label( "Try configuring several times, each time changing the items " "on the 'Parents?' tab." ), '_', HGroup(spring, Item('configure', show_label=False)), ) def _configure_changed(self): self.foo.configure_traits() # Create the demo: popup = FooDemo() # Run the demo (if invoked from the command line): if __name__ == '__main__': popup.configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/examples/demo/Advanced/Dynamically_changing_buttons_demo.py0000644000175100001730000000463000000000000027677 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ This demo shows how to dynamically change the label or image of a button. Note in this demo clicking the button itself does nothing. Please refer to the `ButtonEditor API docs`_ for further information. .. _ButtonEditor API docs: https://docs.enthought.com/traitsui/api/traitsui.editors.button_editor.html#traitsui.editors.button_editor.ButtonEditor """ from pyface.api import Image, ImageResource from traits.api import Button, Enum, HasTraits, Instance, List, Str from traitsui.api import ( ButtonEditor, Group, ImageEditor, InstanceChoice, InstanceEditor, Item, UItem, View, ) import traitsui.extras class ImageChoice(InstanceChoice): def get_view(self): return View(UItem('name', editor=ImageEditor(image=self.object))) class ButtonEditorDemo(HasTraits): my_button = Button() my_button_label = Str("Initial Label") my_button_image = Image() my_button_image_options = List( Image(), value=[ ImageResource("run", [traitsui.extras]), ImageResource("previous", [traitsui.extras]), ImageResource("next", [traitsui.extras]), ImageResource("parent", [traitsui.extras]), ImageResource("reload", [traitsui.extras]), ], ) def _my_button_image_default(self): return self.my_button_image_options[0] traits_view = View( Item( "my_button", style="custom", editor=ButtonEditor( label_value="my_button_label", image_value="my_button_image", orientation="horizontal", ), ), Item("my_button_label"), Item( "my_button_image", editor=InstanceEditor( name="my_button_image_options", adapter=ImageChoice ), style="custom", ), ) # Create the demo: demo = ButtonEditorDemo() # Run the demo (if invoked from the command line): if __name__ == '__main__': demo.configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/examples/demo/Advanced/HDF5_tree_demo.py0000644000175100001730000001702100000000000023520 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """This demo shows how to use Traits TreeEditors with PyTables to walk the hierarchy of an HDF5 file. This only picks out tables, arrays and groups, but could easily be extended to other structures. In the demo, the path to the selected item is printed whenever the selection changes. An example HDF5 file is provided here, but you could easily change the path given at the bottom of this file to a path to your own HDF5 file. To run this demonstration successfully, you must have the following packages installed: - **PyTables** (``tables``) - **HDF5 for Python** (``h5py``) Note that PyTables can't read HDF5 files created with h5py, but h5py can read HDF5 files created with PyTables. See HDF5_tree_demo2 for an example using h5py. """ import os import tables as tb from traits.api import HasTraits, Str, List, Instance, Any from traitsui.api import TreeEditor, TreeNode, View, Item, Group ROOT = os.path.dirname(__file__) # View for objects that aren't edited no_view = View() # HDF5 Nodes in the tree class Hdf5ArrayNode(HasTraits): name = Str('') path = Str('') parent_path = Str('') class Hdf5GroupNode(HasTraits): name = Str('') path = Str('') parent_path = Str('') # Can't have recursive traits? Really? # groups = List(Hdf5GroupNode) groups = List() arrays = List(Hdf5ArrayNode) groups_and_arrays = List() class Hdf5FileNode(HasTraits): name = Str('') path = Str('/') groups = List(Hdf5GroupNode) arrays = List(Hdf5ArrayNode) groups_and_arrays = List() # Recursively build tree, there is probably a better way of doing this. def _get_sub_arrays(group, h5file): """Return a list of all arrays immediately below a group in an HDF5 file.""" return [ Hdf5ArrayNode( name=array._v_name, path=array._v_pathname, parent_path=array._v_parent._v_pathname, ) for array in group if isinstance(array, (tb.Array, tb.Table)) ] # More pythonic # for array in h5file.iter_nodes(group, classname='Array')] # Old call def _get_sub_groups(group, h5file): """Return a list of all groups and arrays immediately below a group in an HDF5 file.""" subgroups = [] for subgroup in h5file.iter_nodes(group, classname='Group'): subsubgroups = _get_sub_groups(subgroup, h5file) subsubarrays = _get_sub_arrays(subgroup, h5file) subgroups.append( Hdf5GroupNode( name=subgroup._v_name, path=subgroup._v_pathname, parent_path=subgroup._v_parent._v_pathname, groups=subsubgroups, arrays=subsubarrays, groups_and_arrays=subsubgroups + subsubarrays, ) ) return subgroups def _hdf5_tree(filename): """Return a list of all groups and arrays below the root group of an HDF5 file.""" with tb.open_file(filename, 'r') as h5file: subgroups = _get_sub_groups(h5file.root, h5file) subarrays = _get_sub_arrays(h5file.root, h5file) h5_tree = Hdf5FileNode( name=filename, groups=subgroups, arrays=subarrays, groups_and_arrays=subgroups + subarrays, ) return h5_tree # Get a tree editor def _hdf5_tree_editor(selected=''): """Return a TreeEditor specifically for HDF5 file trees.""" return TreeEditor( nodes=[ TreeNode( node_for=[Hdf5FileNode], auto_open=True, children='groups_and_arrays', label='name', view=no_view, ), TreeNode( node_for=[Hdf5GroupNode], auto_open=False, children='groups_and_arrays', label='name', view=no_view, ), TreeNode( node_for=[Hdf5ArrayNode], auto_open=False, children='', label='name', view=no_view, ), ], editable=False, selected=selected, ) class ATree(HasTraits): h5_tree = Instance(Hdf5FileNode) node = Any() traits_view = View( Group( Item( 'h5_tree', editor=_hdf5_tree_editor(selected='node'), resizable=True, show_label=False, ), orientation='vertical', ), title='HDF5 Tree Example', buttons=['Undo', 'OK', 'Cancel'], resizable=True, width=0.3, height=0.3, ) def _node_changed(self): print(self.node.path) def make_test_datasets(): """Makes the test datasets and store it in the current folder""" import h5py import numpy as np import pandas as pd # pandas uses pytables to store datasets in hdf5 format. from random import randrange n = 100 df = pd.DataFrame( dict( [ ("int{0}".format(i), np.random.randint(0, 10, size=n)) for i in range(5) ] ) ) df['float'] = np.random.randn(n) for i in range(10): df["object_1_{0}".format(i)] = [ '%08x' % randrange(16 ** 8) for _ in range(n) ] for i in range(7): df["object_2_{0}".format(i)] = [ '%15x' % randrange(16 ** 15) for _ in range(n) ] df.info() df.to_hdf('test_fixed.h5', 'data', format='fixed') df.to_hdf('test_table_no_dc.h5', 'data', format='table') df.to_hdf('test_table_dc.h5', 'data', format='table', data_columns=True) df.to_hdf( 'test_fixed_compressed.h5', 'data', format='fixed', complib='blosc', complevel=9, ) # h5py dataset time = np.arange(n) x = np.linspace(-7, 7, n) axes_latlon = [ ('time', time), ('coordinate', np.array(['lat', 'lon'], dtype='S3')), ] axes_mag = [ ('time', time), ('direction', np.array(['x', 'y', 'z'], dtype='S1')), ] latlon = np.vstack( (np.linspace(-0.0001, 0.00001, n) + 23.8, np.zeros(n) - 82.3) ).T mag_data = np.vstack( ( -(1 - np.tanh(x) ** 2) * np.sin(2 * x), -(1 - np.tanh(x) ** 2) * np.sin(2 * x), -(1 - np.tanh(x) ** 2), ) ).T datasets = ( axes_mag + axes_latlon + [('magnetic_3_axial', mag_data), ('latlon', latlon)] ) with h5py.File(os.path.join(ROOT, 'test_h5pydata.h5'), "a") as h5file: h5group = h5file.require_group("run1_test1") for data_name, data in datasets: h5group.require_dataset( name=data_name, dtype=data.dtype, shape=data.shape, data=data, # **options ) def main(): import sys filename = os.path.join(ROOT, 'test_fixed.h5') filename = os.path.join(ROOT, 'test_table_no_dc.h5') if len(sys.argv) > 1: filename = sys.argv[1] a_tree = ATree(h5_tree=_hdf5_tree(filename)) return a_tree demo = main() if __name__ == '__main__': # make_test_datasets() demo.configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/examples/demo/Advanced/HDF5_tree_demo2.py0000644000175100001730000002312200000000000023601 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """This demo shows how to use Traits TreeEditors with h5py to walk the hierarchy of several HDF5 files in a folder. All Datasets and Groups are shown for each file. In the demo, the path to the selected item is printed whenever the selection changes. An example HDF5 file is provided here, but you could easily change the path given at the bottom of this file to a path to your own HDF5 file. To run this demonstration successfully, you must have the following packages installed: - **PyTables** (``tables``) - **HDF5 for Python** (``h5py``) Note that PyTables can't read HDF5 files created with h5py, but h5py can read HDF5 files created with PyTables. See HDF5_tree_demo for an example using PyTables. """ import os import warnings import h5py from traits import api import traitsui.api as ui ROOT = os.path.dirname(__file__) # View for objects that aren't edited no_view = ui.View() # HDF5 Nodes in the tree class Hdf5ArrayNode(api.HasTraits): name = api.Str('') path = api.Str('') parent_path = api.Str('') class Hdf5GroupNode(api.HasTraits): name = api.Str('') path = api.Str('') parent_path = api.Str('') # Can't have recursive traits? Really? # groups = api.List( Hdf5GroupNode ) groups = api.List() arrays = api.List(Hdf5ArrayNode) groups_and_arrays = api.List() class Hdf5FileNode(api.HasTraits): name = api.Str('') path = api.Str('/') groups = api.List(Hdf5GroupNode) arrays = api.List(Hdf5ArrayNode) groups_and_arrays = api.List() class Hdf5FilesNode(api.HasTraits): name = api.Str('') path = api.Str('/') files = api.List(Hdf5FileNode) groups_and_arrays = api.List() # Recursively build tree, there is probably a better way of doing this. def _get_sub_arrays(group, parent_path): """Return a list of all arrays immediately below a group in an HDF5 file.""" return [ Hdf5ArrayNode( name=name, path=parent_path + name, parent_path=parent_path ) for name, array in group.items() if isinstance(array, h5py.Dataset) ] def _get_sub_groups(group, parent_path): """Return a list of all subgroups and arrays immediately below a group in an HDF5 file.""" subgroups = [] for name, subgroup in group.items(): if isinstance(subgroup, h5py.Group): path = parent_path + name + '/' subsubarrays = _get_sub_arrays(subgroup, path) subsubgroups = _get_sub_groups(subgroup, path) subgroups.append( Hdf5GroupNode( name=name, path=path, parent_path=parent_path, arrays=subsubarrays, subgroups=subsubgroups, groups_and_arrays=subsubgroups + subsubarrays, ) ) return subgroups def _hdf5_tree(filename): with h5py.File(filename, 'r') as h5file: path = ( filename + '#' ) # separate dataset name from name of hdf5-filename subgroups = _get_sub_groups(h5file, path) subarrays = _get_sub_arrays(h5file, path) file_tree = Hdf5FileNode( name=os.path.basename(filename), path=filename, groups=subgroups, arrays=subarrays, groups_and_arrays=subgroups + subarrays, ) return file_tree def _hdf5_trees(filenames): """Return a list of all groups and arrays below the root group of an HDF5 file.""" if isinstance(filenames, str): filenames = [filenames] files_tree = Hdf5FilesNode() files_tree.name = root = os.path.dirname(filenames[0]) files_tree.path = root for filename in filenames: folder = os.path.dirname(filename) if folder != root: warnings.warn( "Expected same folder for all files, but got {} != {}".format( root, folder ) ) file_tree = _hdf5_tree(filename) files_tree.files.append(file_tree) files_tree.groups_and_arrays.extend(file_tree.groups_and_arrays) return files_tree # Get a tree editor def _hdf5_tree_editor(selected=''): """Return a ui.TreeEditor specifically for HDF5 file trees.""" return ui.TreeEditor( nodes=[ ui.TreeNode( node_for=[Hdf5FilesNode], auto_open=True, children='files', label='name', view=no_view, ), ui.TreeNode( node_for=[Hdf5FileNode], auto_open=True, children='groups_and_arrays', label='name', view=no_view, ), ui.TreeNode( node_for=[Hdf5GroupNode], auto_open=False, children='groups_and_arrays', label='name', view=no_view, ), ui.TreeNode( node_for=[Hdf5ArrayNode], auto_open=False, children='', label='name', view=no_view, ), ], editable=False, selected=selected, ) class _H5Tree(api.HasTraits): h5_tree = api.Instance(Hdf5FileNode) node = api.Any() path = api.Str() traits_view = ui.View( ui.Group( ui.Item( 'h5_tree', editor=_hdf5_tree_editor(selected='node'), resizable=True, ), ui.Item('path', label='Selected node'), orientation='vertical', ), title='HDF5 Tree Example', buttons=['OK', 'Cancel'], resizable=True, width=0.3, height=0.3, ) def _node_changed(self): self.path = self.node.path print(self.node.path) class _H5Trees(api.HasTraits): h5_trees = api.Instance(Hdf5FilesNode) node = api.Any() path = api.Str() traits_view = ui.View( ui.Group( ui.Item( 'h5_trees', editor=_hdf5_tree_editor(selected='node'), resizable=True, ), ui.Item('path', label='Selected node'), orientation='vertical', ), title='Multiple HDF5 file Tree Example', buttons=['OK', 'Cancel'], resizable=True, width=0.3, height=0.3, ) def _node_changed(self): self.path = self.node.path print(self.node.path) def hdf5_tree(filename): # if isinstance(filename, str): # return _H5Tree(h5_tree=_hdf5_tree(filename)) return _H5Trees(h5_trees=_hdf5_trees(filename)) def make_test_datasets(): """Makes the test datasets and store it in the current folder""" import numpy as np import pandas as pd # pandas uses pytables to store datasets in hdf5 format. from random import randrange n = 100 df = pd.DataFrame( dict( [ ("int{0}".format(i), np.random.randint(0, 10, size=n)) for i in range(5) ] ) ) df['float'] = np.random.randn(n) for i in range(10): df["object_1_{0}".format(i)] = [ '%08x' % randrange(16 ** 8) for _ in range(n) ] for i in range(7): df["object_2_{0}".format(i)] = [ '%15x' % randrange(16 ** 15) for _ in range(n) ] df.info() df.to_hdf('test_fixed.h5', 'data', format='fixed') df.to_hdf('test_table_no_dc.h5', 'data', format='table') df.to_hdf('test_table_dc.h5', 'data', format='table', data_columns=True) df.to_hdf( 'test_fixed_compressed.h5', 'data', format='fixed', complib='blosc', complevel=9, ) # h5py dataset time = np.arange(n) x = np.linspace(-7, 7, n) axes_latlon = [ ('time', time), ('coordinate', np.array(['lat', 'lon'], dtype='S3')), ] axes_mag = [ ('time', time), ('direction', np.array(['x', 'y', 'z'], dtype='S1')), ] latlon = np.vstack( (np.linspace(-0.0001, 0.00001, n) + 23.8, np.zeros(n) - 82.3) ).T mag_data = np.vstack( ( -(1 - np.tanh(x) ** 2) * np.sin(2 * x), -(1 - np.tanh(x) ** 2) * np.sin(2 * x), -(1 - np.tanh(x) ** 2), ) ).T datasets = ( axes_mag + axes_latlon + [('magnetic_3_axial', mag_data), ('latlon', latlon)] ) with h5py.File(os.path.join(ROOT, 'test_h5pydata.h5'), "a") as h5file: h5group = h5file.require_group("run1_test1") for data_name, data in datasets: h5group.require_dataset( name=data_name, dtype=data.dtype, shape=data.shape, data=data, # **options ) def main(): filenames = [ 'test_fixed.h5', 'test_table_no_dc.h5', 'test_table_dc.h5', 'test_fixed_compressed.h5', 'test_h5pydata.h5', ] fullfiles = [os.path.join(ROOT, fname) for fname in filenames] h5_trees = hdf5_tree(fullfiles) return h5_trees demo = main() if __name__ == '__main__': # make_test_datasets() ok_status = demo.configure_traits() print('The End', ok_status) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/examples/demo/Advanced/History_demo.py0000644000175100001730000000463700000000000023465 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ This program demonstrates the use of editors that support *history*. A history is a persistent record of the last 'n' values the user has entered or selected for a particular trait. In order for the history to be recorded correctly, you must specify an *id* for both the Item containing the history editor and the View containing the Item. The maximum number of history entries recorded is specified by the value of the history editor's *entries* trait. If *entries* is less than or equal to 0, then a normal, non-history, version of the editor will be used. A history editor also attempts to restore the last value set as the current value for the trait if the current value of the trait is the empty string. You can see this for yourself by setting some values in the fields, selecting a different demo, then reselecting this demo. Each field should have the same value it had when the demo was run the previous time. """ # -- Imports -------------------------------------------------------------- from traits.api import HasTraits, Str, File, Directory from traitsui.api import View, Item, FileEditor, DirectoryEditor, HistoryEditor # -- HistoryDemo Class ---------------------------------------------------- class HistoryDemo(HasTraits): name = Str() file = File() directory = Directory() view = View( Item('name', id='name', editor=HistoryEditor(entries=5)), Item('file', id='file1', editor=FileEditor(entries=10)), Item( 'file', id='file2', editor=FileEditor( entries=10, filter=['All files (*.*)|*.*', 'Python files (*.py)|*.py'], ), ), Item('directory', id='directory', editor=DirectoryEditor(entries=10)), title='History Editor Demo', id='enthought.test.history_demo.HistoryDemo', width=0.33, resizable=True, ) # Create the demo: demo = HistoryDemo() # Run the demo (if invoked from the command line): if __name__ == '__main__': demo.configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/examples/demo/Advanced/Invalid_state_handling.py0000644000175100001730000001241500000000000025443 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Sometimes the inputs to a model are not correlated. That is, any valid model input produces a corresponding valid model state. However, in other cases, some or all of the model inputs are correlated. That is, there may exist one or more combinations of individually valid model inputs which produce invalid model states. In cases where this can happen, it is very often desirable to warn the user if a particular combination of input values will not produce a usable result. This problem cannot be solved solely though the use of carefully chosen trait types and editors, because each individual input may be valid, but it is the combination of inputs which is invalid. Solving this problem therefore typically requires providing a way of determining whether or not the model is in a valid state, and then communicating that information to the user via the user interface. This demonstration provides an example of doing this using the TraitEditor's 'invalid' trait. Each trait editor has an 'invalid' trait which can be set equal to the name of a trait in the user interface context which contains a boolean value reflecting whether or not the user interface (and underlying model) are in a invalid state or not. A True value for the trait indicates that the editor's current value produces an invalid model state. By associating the same 'invalid' trait with one or more editors in the user interface, the resulting user interface can indicate to the user which combination of input values is producing the invalid state. In this example, we have a very simple model which allows the user to control the mass and velocity of a system. The model also defines the kinetic energy of the resulting system. For safety reasons, the kinetic energy of the system should not exceed a certain threshold. If it does, the user should be warned so that they can reduce either or both of the system mass and velocity back down to a safe level. In the model, an 'error' property is defined which is True whenever the kinetic energy level of the system exceeds the safety threshold. This trait is then synchronized with the user interface's 'mass', velocity' and 'status' editors, turning them red whenever the model enters an invalid state. The 'status' trait is another property, based on the 'error' trait, which provides a human readable description of the current system state. Note that in this example, we synchronize the 'error' trait with the user interface using 'sync_to_view' metadata, whose value is a list of user interface editor traits the trait should be synchronized 'to' (i.e. changes to the 'error' trait will be copied to the corresponding trait in the editor, but not vice versa). We could also have explicitly set the 'invalid' trait of each corresponding editor in the view definition to 'error' as well. To use the demo, simply use the 'mass' and 'velocity' sliders and observe the changes to the 'kinetic_energy' of the system. When the kinetic energy exceeds 50,000, notice how the 'mass', 'velocity' and 'status' fields turn red, and that when the kinetic energy drops below 50,000, the fields return to their normal color. """ # -- Imports -------------------------------------------------------------- from traits.api import ( HasTraits, Range, Float, Bool, Str, Property, ) from traitsui.api import View, VGroup, Item # -- System Class --------------------------------------------------------- class System(HasTraits): # The mass of the system: mass = Range(0.0, 100.0) # The velocity of the system: velocity = Range(0.0, 100.0) # The kinetic energy of the system: kinetic_energy = Property(Float, observe='mass, velocity') # The current error status of the system: error = Property( Bool, sync_to_view='mass.invalid, velocity.invalid, status.invalid', observe='kinetic_energy', ) # The current status of the system: status = Property(Str, observe='error') view = View( VGroup( VGroup( Item('mass'), Item('velocity'), Item('kinetic_energy', style='readonly', format_str='%.0f'), label='System', show_border=True, ), VGroup( Item('status', style='readonly', show_label=False), label='Status', show_border=True, ), ) ) def _get_kinetic_energy(self): return (self.mass * self.velocity * self.velocity) / 2.0 def _get_error(self): return self.kinetic_energy > 50000.0 def _get_status(self): if self.error: return 'The kinetic energy of the system is too high.' return '' # -- Create and run the demo ---------------------------------------------- # Create the demo: demo = System() # Run the demo (if invoked from the command line): if __name__ == '__main__': demo.configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/examples/demo/Advanced/ListStrAdapter_demo.py0000644000175100001730000000563600000000000024731 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Display an editable List of Strings. The ListStrEditor uses a custom ListStrAdapter to define colors and tooltips. """ from traitsui.list_str_adapter import ListStrAdapter from traits.api import HasTraits, List, Str from traitsui.api import View, Item, ListStrEditor # -- The adapter ---------------------------------------------------------- class HeadlinesListAdapter(ListStrAdapter): """ Custom adapter for string lists being edited in the ListStrEditor. - Style ALL CAPS titles in red. - Describe the capitalization in the tooltip. """ def get_text_color(self, object, trait, row): try: row_data = getattr(object, trait)[row] except IndexError: return "" if row_data.isupper(): return "red" return super().get_text_color(object, trait, row) def get_tooltip(self, object, trait, row): try: row_data = getattr(object, trait)[row] except IndexError: # if the ListStr is editable, IndexError indicates the empty next slots return "" if row_data.isupper(): return "An uppercase headline" elif row_data.istitle(): return "A headline in title case" elif row_data.islower(): return "A fully lowercase headline" else: return self.tooltip # -- The class providing the view ------------------------------------- class HeadlinesListDemo(HasTraits): headlines = List(Str) view = View( Item( "headlines", show_label=False, editor=ListStrEditor( title="List of headlines (hover for description)", adapter=HeadlinesListAdapter( # the default tooltip for generic rows tooltip="A headline" ), auto_add=True ), # (QT only) the tooltip shown in the area outside of rows # or when the row's tooltip is empty tooltip="List of headlines" ), title='List of headlines', width=320, height=370, resizable=True, ) # -- Set up the Demo ------------------------------------------------------ demo = HeadlinesListDemo( headlines=[ "Shark bites man", "MAN BITES SHARK", "The London Stock Exchange Collapses", "she sells sea shells on the sea shore" ] ) # Run the demo (in invoked from the command line): if __name__ == "__main__": demo.configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/examples/demo/Advanced/ListStrEditor_demo.py0000644000175100001730000000335000000000000024566 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Edit a List of Strings Simple demonstration of the ListStrEditor, which can be used for editing and displaying lists of strings, or other data that can be logically mapped to a list of strings. """ from traits.api import HasTraits, List, Str from traitsui.api import View, Item, ListStrEditor # -- ShoppingListDemo Class ----------------------------------------------- class ShoppingListDemo(HasTraits): # The list of things to buy at the store: shopping_list = List(Str) # -- Traits View Definitions ---------------------------------------------- view = View( Item( 'shopping_list', show_label=False, editor=ListStrEditor(title='Shopping List', auto_add=True), ), title='Shopping List', width=0.2, height=0.5, resizable=True, ) # -- Set up the Demo ------------------------------------------------------ demo = ShoppingListDemo( shopping_list=[ 'Carrots', 'Potatoes (5 lb. bag)', 'Cocoa Puffs', 'Ice Cream (French Vanilla)', 'Peanut Butter', 'Whole wheat bread', 'Ground beef (2 lbs.)', 'Paper towels', 'Soup (3 cans)', 'Laundry detergent', ] ) # Run the demo (in invoked from the command line): if __name__ == '__main__': demo.configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/examples/demo/Advanced/List_editor_notebook_selection_demo.py0000644000175100001730000000767300000000000030255 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Displaying a list of objects in notebook tabs A list of objects can be displayed in a tabbed notebook, one object per tab. This example also shows how the currently active notebook tab of a ListEditor can be controlled using the ListEditor's 'selected' trait. Note the interaction between the spinner control (for the 'index' trait) and the currently selected notebook tab. Try changing the spinner value, then try clicking on various notebook tabs. Finally, note that the ListEditor will automatically scroll the tabs to make the selected tab completely visible. """ # The following text was removed from the module docstring (only works in wx): # Also note that rearranging the notebook tabs (using drag and drop) does not # affect the correspondence between the index value and its associated notebook # tab. The correspondence is determined by the contents of the 'people' trait, # and not by the physical layout of the notebook tabs. from traits.api import HasStrictTraits, Str, Int, Regex, List, Instance, Range from traitsui.api import View, VGroup, Item, ListEditor # -- Person Class --------------------------------------------------------- class Person(HasStrictTraits): # Trait definitions: name = Str() age = Int() phone = Regex(value='000-0000', regex=r'\d\d\d[-]\d\d\d\d') # Traits view definition: traits_view = View( 'name', 'age', 'phone', width=0.18, buttons=['OK', 'Cancel'] ) # -- Sample Data ---------------------------------------------------------- people = [ Person(name='Dave Chomsky', age=39, phone='555-1212'), Person(name='Mike Wakowski', age=28, phone='555-3526'), Person(name='Joe Higginbotham', age=34, phone='555-6943'), Person(name='Tom Derringer', age=22, phone='555-7586'), Person(name='Dick Van Der Hooten', age=63, phone='555-3895'), Person(name='Harry McCallum', age=46, phone='555-3285'), Person(name='Sally Johnson', age=43, phone='555-8797'), Person(name='Fields Timberlawn', age=31, phone='555-3547'), ] # -- ListEditorNotebookSelectionDemo Class -------------------------------- class ListEditorNotebookSelectionDemo(HasStrictTraits): # -- Trait Definitions ---------------------------------------------------- # List of people: people = List(Person) # The currently selected person: selected = Instance(Person) # The index of the currently selected person: index = Range(0, 7, mode='spinner') # -- Traits View Definitions ---------------------------------------------- traits_view = View( Item('index'), '_', VGroup( Item( 'people', id='notebook', show_label=False, style='custom', editor=ListEditor( use_notebook=True, deletable=False, selected='selected', export='DockWindowShell', page_name='.name', ), ) ), id='traitsui.demo.Traits UI Demo.Advanced.' 'List_editor_notebook_selection_demo', dock='horizontal', ) # -- Trait Event Handlers ------------------------------------------------- def _selected_changed(self, selected): self.index = self.people.index(selected) def _index_changed(self, index): self.selected = self.people[index] # -- Set Up The Demo ------------------------------------------------------ demo = ListEditorNotebookSelectionDemo(people=people) if __name__ == "__main__": demo.configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/examples/demo/Advanced/List_editors_demo.py0000644000175100001730000000717300000000000024466 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ **WARNING** This demo might not work as expected and some documented features might be missing. ------------------------------------------------------------------------------- This shows the three different types of editor that can be applied to a list of objects: - Table - List - Dockable notebook (a list variant) Each editor style is editing the exact same list of objects. Note that any changes made in one editor are automatically reflected in the others. """ # Issue related to the demo warning: enthought/traitsui#948 from traits.api import HasStrictTraits, Str, Int, Regex, List, Instance from traitsui.api import ( Item, ListEditor, ObjectColumn, RuleTableFilter, Tabbed, TableEditor, View, ) from traitsui.table_filter import ( RuleFilterTemplate, MenuFilterTemplate, EvalFilterTemplate, ) # 'Person' class: class Person(HasStrictTraits): # Trait definitions: name = Str() age = Int() phone = Regex(value='000-0000', regex=r'\d\d\d[-]\d\d\d\d') # Traits view definition: traits_view = View( 'name', 'age', 'phone', width=0.18, buttons=['OK', 'Cancel'] ) # Sample data: people = [ Person(name='Dave', age=39, phone='555-1212'), Person(name='Mike', age=28, phone='555-3526'), Person(name='Joe', age=34, phone='555-6943'), Person(name='Tom', age=22, phone='555-7586'), Person(name='Dick', age=63, phone='555-3895'), Person(name='Harry', age=46, phone='555-3285'), Person(name='Sally', age=43, phone='555-8797'), Person(name='Fields', age=31, phone='555-3547'), ] # Table editor definition: filters = [EvalFilterTemplate, MenuFilterTemplate, RuleFilterTemplate] table_editor = TableEditor( columns=[ ObjectColumn(name='name', width=0.4), ObjectColumn(name='age', width=0.2), ObjectColumn(name='phone', width=0.4), ], editable=True, deletable=True, sortable=True, sort_model=True, auto_size=False, filters=filters, search=RuleTableFilter(), row_factory=Person, show_toolbar=True, ) # 'ListTraitTest' class: class ListTraitTest(HasStrictTraits): # Trait definitions: people = List(Instance(Person, ())) # Traits view definitions: traits_view = View( Tabbed( Item('people', label='Table', id='table', editor=table_editor), Item( 'people', label='List', id='list', style='custom', editor=ListEditor(style='custom', rows=5), ), Item( 'people', label='Notebook', id='notebook', style='custom', editor=ListEditor( use_notebook=True, deletable=True, export='DockShellWindow', page_name='.name', ), ), id='splitter', show_labels=False, ), id='traitsui.demo.Traits UI Demo.Advanced.List_editors_demo', dock='horizontal', width=600, ) # Create the demo: demo = ListTraitTest(people=people) # Run the demo (if invoked from the command line): if __name__ == '__main__': demo.configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/examples/demo/Advanced/MVC_demo.py0000644000175100001730000000640200000000000022441 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Supporting Model/View/Controller (MVC) pattern Demonstrates one approach to writing Model/View/Controller (MVC)-based applications using Traits UI. This example contains a trivial model containing only one data object, the string 'myname'. In this example, the Controller contains the View. A more rigorous example would separate these. A few key points: - the Controller itself accesses the model as self.model - the Controller's View can access model traits directly ('myname') """ from traits.api import HasTraits, Str, Bool, TraitError from traitsui.api import View, VGroup, HGroup, Item, Controller class MyModel(HasTraits): """Define a simple model containing a single string, 'myname'""" # Simple model data: myname = Str() class MyViewController(Controller): """Define a combined controller/view class that validates that MyModel.myname is consistent with the 'allow_empty_string' flag. """ # When False, the model.myname trait is not allowed to be empty: allow_empty_string = Bool() # Last attempted value of model.myname to be set by user: last_name = Str() # Define the view associated with this controller: view = View( VGroup( HGroup( Item('myname', springy=True), '10', Item('controller.allow_empty_string', label='Allow Empty'), ), # Add an empty vertical group so the above items don't end up # centered vertically: VGroup(), ), resizable=True, ) # -- Handler Interface ---------------------------------------------------- def myname_setattr(self, info, object, traitname, value): """Validate the request to change the named trait on object to the specified value. Validation errors raise TraitError, which by default causes the editor's entry field to be shown in red. (This is a specially named method _setattr, which is available inside a Controller.) """ self.last_name = value if (not self.allow_empty_string) and (value.strip() == ''): raise TraitError('Empty string not allowed.') return super().setattr(info, object, traitname, value) # -- Event handlers ------------------------------------------------------- def controller_allow_empty_string_changed(self, info): """'allow_empty_string' has changed, check the myname trait to ensure that it is consistent with the current setting. """ if (not self.allow_empty_string) and (self.model.myname == ''): self.model.myname = '?' else: self.model.myname = self.last_name # Create the model and (demo) view/controller: demo = MyViewController(MyModel()) # Run the demo (if invoked from the command line): if __name__ == '__main__': demo.configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/examples/demo/Advanced/Multi_select_string_list.py0000644000175100001730000000636600000000000026073 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ **WARNING** This demo might not work as expected and some documented features might be missing. ------------------------------------------------------------------------------- Creating a multi-select list box How to use a TabularEditor to create a multi-select list box. This demo uses two TabularEditors, side-by-side. Selections from the left table are shown in the right table. Each table has only one column. """ # Issues related to the demo warning: enthought/traitsui#14, # enthought/traitsui#15, enthought/traitsui#960 from traits.api import HasPrivateTraits, List, Str, Property from traitsui.api import View, HGroup, UItem, TabularAdapter, TabularEditor class MultiSelectAdapter(TabularAdapter): """This adapter is used by both the left and right tables""" # Titles and column names for each column of a table. # In this example, each table has only one column. columns = [('', 'myvalue')] # Magically named trait which gives the display text of the column named # 'myvalue'. This is done using a Traits Property and its getter: myvalue_text = Property() # The getter for Property 'myvalue_text' simply takes the value of the # corresponding item in the list being displayed in this table. # A more complicated example could format the item before displaying it. def _get_myvalue_text(self): return self.item class MultiSelect(HasPrivateTraits): """This is the class used to view two tables""" # FIXME (TraitsUI defect #14): When multi-select is done by keyboard # (shift+arrow), the 'selected' trait list does not update. # FIXME (TraitsUI defect #15): In Windows wx, when show_titles is False, # left table does not draw until selection passes through all rows. # (Workaround here: set show_titles True and make column titles empty.) choices = List(Str) selected = List(Str) traits_view = View( HGroup( UItem( 'choices', editor=TabularEditor( show_titles=True, selected='selected', editable=False, multi_select=True, adapter=MultiSelectAdapter(), ), ), UItem( 'selected', editor=TabularEditor( show_titles=True, editable=False, adapter=MultiSelectAdapter(), ), ), ), resizable=True, width=200, height=300, ) # Create the demo: demo = MultiSelect( choices=[ 'one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine', 'ten', ] ) # Run the demo (if invoked from the command line): if __name__ == '__main__': demo.configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/examples/demo/Advanced/Multi_thread_demo.py0000644000175100001730000000435100000000000024436 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Monitoring threads in the user interface Shows a simple user interface being updated by multiple threads. When the *Start Threads* button is pressed, the program starts three independent threads running. Each thread counts from 0 to 199, updating its own thread-specific trait, and performs a sleep of a thread-specific duration between each update. The *Start Threads* button is disabled while the threads are running, and becomes active again once all threads have finished running. """ from threading import Thread from time import sleep from traits.api import HasTraits, Int, Button from traitsui.api import View, Item, VGroup class ThreadDemo(HasTraits): # The thread specific counters: thread_0 = Int() thread_1 = Int() thread_2 = Int() # The button used to start the threads running: start = Button('Start Threads') # The count of how many threads ae currently running: running = Int() view = View( VGroup( Item('thread_0', style='readonly'), Item('thread_1', style='readonly'), Item('thread_2', style='readonly'), ), '_', Item('start', show_label=False, enabled_when='running == 0'), resizable=True, width=250, title='Monitoring threads', ) def _start_changed(self): for i in range(3): Thread( target=self.counter, args=('thread_%d' % i, (i * 10 + 10) / 1000.0), ).start() def counter(self, name, interval): self.running += 1 count = 0 for i in range(200): setattr(self, name, count) count += 1 sleep(interval) self.running -= 1 # Create the demo: demo = ThreadDemo() # Run the demo (if invoked from the command line): if __name__ == '__main__': demo.configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/examples/demo/Advanced/Multi_thread_demo_2.py0000644000175100001730000000547700000000000024671 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Monitoring a dynamic number of threads Shows a simple user interface being updated by a dynamic number of threads. When the *Create Threads* button is pressed, the *count* method is dispatched on a new thread. It then creates a new *Counter* object and adds it to the *counters* list (which causes the *Counter* to appear in the user interface. It then counts by incrementing the *Counter* object's *count* trait (which again causes a user interface update each time the counter is incremented). After it reaches its maximum count, it removes the *Counter* from the *counter* list (causing the counter to be removed from the user interface) and exits (terminating the thread). Note that repeated clicking of the *Create Thread* button will create additional threads. """ from time import sleep from traits.api import HasTraits, Int, Button, List from traitsui.api import View, Item, ListEditor # -- The Counter objects used to keep track of the current count ---------- class Counter(HasTraits): # The current count: count = Int() view = View(Item('count', style='readonly')) # -- The main 'ThreadDemo' class ------------------------------------------ class ThreadDemo(HasTraits): # The button used to start a new thread running: create = Button('Create Thread') # The set of counter objects currently running: counters = List(Counter) view = View( Item('create', show_label=False), '_', Item( 'counters', style='custom', show_label=False, editor=ListEditor(use_notebook=True, dock_style='tab'), ), resizable=True, width=300, height=150, title='Dynamic threads', ) def __init__(self, **traits): super().__init__(**traits) # Set up the notification handler for the 'Create Thread' button so # that it is run on a new thread: self.on_trait_change(self.count, 'create', dispatch='new') def count(self): """This method is dispatched on a new thread each time the 'Create Thread' button is pressed. """ counter = Counter() self.counters.append(counter) for i in range(1000): counter.count += 1 sleep(0.030) self.counters.remove(counter) # Create the demo: demo = ThreadDemo() # Run the demo (if invoked from the command line): if __name__ == '__main__': demo.configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/examples/demo/Advanced/NumPy_array_tabular_editor_demo.py0000644000175100001730000000440500000000000027343 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Displaying large NumPy arrays with TabularEditor A demonstration of how the TabularEditor can be used to display (large) NumPy arrays, in this case 100,000 random 3D points from a unit cube. In addition to showing the coordinates of each point, it also displays the index of each point in the array, as well as a red flag if the point lies within 0.25 of the center of the cube. """ from numpy import sqrt from numpy.random import random from traits.api import HasTraits, Property, Array from traitsui.api import View, Item, TabularAdapter, TabularEditor # -- Tabular Adapter Definition ------------------------------------------- class ArrayAdapter(TabularAdapter): columns = [('i', 'index'), ('x', 0), ('y', 1), ('z', 2)] font = 'Courier 10' alignment = 'right' format = '%.4f' index_text = Property() index_image = Property() def _get_index_text(self): return str(self.row) def _get_index_image(self): x, y, z = self.item if sqrt((x - 0.5) ** 2 + (y - 0.5) ** 2 + (z - 0.5) ** 2) <= 0.25: return '@icons:red_ball' return None # -- ShowArray Class Definition ------------------------------------------- class ShowArray(HasTraits): data = Array traits_view = View( Item( 'data', show_label=False, editor=TabularEditor( adapter=ArrayAdapter(), auto_resize=True, # Do not allow any kind of editing of the array: editable=False, operations=[], drag_move=False, ), ), title='Array Viewer', width=0.3, height=0.8, resizable=True, ) # Create the demo: demo = ShowArray(data=random((100000, 3))) # Run the demo (if invoked from the command line): if __name__ == '__main__': demo.configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/examples/demo/Advanced/NumPy_array_view_editor_demo.py0000644000175100001730000000547600000000000026674 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Using ArrayViewEditor to display large NumPy arrays in a table. This example displays 100,000 random 3D points from a unit cube. Note that the ArrayViewEditor has the following traits:: # Should an index column be displayed: show_index = Bool(True) # List of (optional) column titles: titles = List(Str) # Should the array be logically transposed: transpose = Bool(False) # The format used to display each array element: format = Str('%s') By default, the array row index will be shown in column one. If 'show_index' is False, then the row index column is omitted. If the list of 'titles' is empty, no column headers will be displayed. If the number of column headers is less than the number of array columns, then there are two cases: - If (number of array_columns) % (number of titles) == 0, then the titles are used to construct a series of repeating column headers with increasing subscripts (e.g. an (n x 6) array with titles of ['x','y','z'] would result in column headers of: 'x0', 'y0', 'z0', 'x1', 'y1', 'z1'). - In all other cases the titles are used as the column headers for the first set of columns, and the remaining column headers are set to the empty string (e.g. an (n x 5) array with titles of ['x','y','z'] would result in column headers of: 'x', 'y', 'z', '', ''). Setting 'transpose' to True will logically transpose the input array (e.g. an (3 x n) array will be displayed as an (n x 3) array). """ from numpy.random import random from traits.api import HasTraits, Array from traitsui.api import View, Item from traitsui.ui_editors.array_view_editor import ArrayViewEditor # -- ShowArray demo class ------------------------------------------------- class ShowArray(HasTraits): data = Array view = View( Item( 'data', show_label=False, editor=ArrayViewEditor( titles=['x', 'y', 'z'], format='%.4f', # Font fails with wx in OSX; # see traitsui issue #13: # font = 'Arial 8' ), ), title='Array Viewer', width=0.3, height=0.8, resizable=True, ) # -- Run the demo --------------------------------------------------------- # Create the demo: demo = ShowArray(data=random((100000, 3))) # Run the demo (if invoked from the command line): if __name__ == '__main__': demo.configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/examples/demo/Advanced/Popup_Dialog_demo.py0000644000175100001730000000640100000000000024375 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Demonstrates using a popup view within another view. Try changing the gender of the person from 'Male' to 'Female' using the drop-down list. When the person's gender is changed, a pop-up dialog is displayed immediately below the gender field providing you with the opportunity to cancel the gender change. If you click the Cancel button, the person's gender will return to its previous value. If you click anywhere else outside of the pop-up dialog, the pop-up dialog will simply disappear, leaving the person's new gender value as is. The main items of interest in this demo are: - The: kind = 'popup' trait set in the PersonHandler View which marks the view as being a popup view. - The parent = info.gender.control value passed to the edit_traits method when the popup dialog is created in the object_gender_changed method. This value specifies the control that the popup dialog should be positioned near. Notes: - This demo only works on the wx backend. - Traits UI will automatically position the popup dialog near the specified control in such a way that the pop-up dialog will not overlay the control and will be entirely on the screen (as long as these two conditions do not conflict). """ # -- Imports -------------------------------------------------------------- from traits.api import HasPrivateTraits, Str, Int, Enum, Instance, Button from traitsui.api import View, HGroup, Item, Handler, UIInfo, spring # -- The PersonHandler class ---------------------------------------------- class PersonHandler(Handler): # The UIInfo object associated with the view: info = Instance(UIInfo) # The cancel button used to revert an unintentional gender change: cancel = Button('Cancel') # The pop-up customization view: view = View( HGroup( spring, Item('cancel', show_label=False), ), kind='popup', ) # Event handlers: def object_gender_changed(self, info): if info.initialized: self.info = info self._ui = self.edit_traits(parent=info.gender.control) def _cancel_changed(self): object = self.info.object object.gender = ['Male', 'Female'][object.gender == 'Male'] self._ui.dispose() # -- The Person class ----------------------------------------------------- class Person(HasPrivateTraits): # The person's name, age and gender: name = Str() age = Int() gender = Enum('Male', 'Female') # The traits UI view: traits_view = View( Item('name'), Item('age'), Item('gender'), title='Button Popup Demo', handler=PersonHandler, ) # -- Create and run the demo ---------------------------------------------- # Create the demo: demo = Person(name='Mike Thomas', age=32) # Run the demo (if invoked from the command line): if __name__ == '__main__': demo.configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/examples/demo/Advanced/Property_List_demo.py0000644000175100001730000001120700000000000024632 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ This demo shows the proper way to create a **Property** whose value is a list, especially when the value of the **Property** will be used in a user interface, such as with the **TableEditor**. Most of the demo is just the machinery to set up the example. The key thing to note is the declaration of the *people* trait: people = Property( List, observe = 'ticker' ) In this case, by defining the **Property** as having a value of type **List**, you are ensuring that the computed value of the property will be validated using the **List** type, which in addition to verifying that the value is indeed a list, also guarantees that it will be converted to a **TraitListObject**, which is necessary for correct interaction with various Traits UI editors in a user interface. Note also the use of the *observe* metadata to trigger a trait property change whenever the *ticker* trait changes (in this case, it is changed every three seconds by a background thread). Finally, the use of the *@cached_property* decorator simplifies the implementation of the property by allowing the **_get_people** *getter* method to perform the expensive generation of a new list of people only when the *ticker* event fires, not every time it is accessed. """ from random import randint, choice from threading import Thread from time import sleep from traits.api import ( HasStrictTraits, HasPrivateTraits, Str, Int, Enum, List, Event, Property, cached_property, ) from traitsui.api import ObjectColumn, Item, TableEditor, View # -- Person Class --------------------------------------------------------- class Person(HasStrictTraits): """Defines some sample data to display in the TableEditor.""" name = Str() age = Int() gender = Enum('Male', 'Female') # -- PropertyListDemo Class ----------------------------------------------- class PropertyListDemo(HasPrivateTraits): """Displays a random list of Person objects in a TableEditor that is refreshed every 3 seconds by a background thread. """ # An event used to trigger a Property value update: ticker = Event() # The property being display in the TableEditor: people = Property(List, observe='ticker') # Tiny hack to allow starting the background thread easily: begin = Int() # -- Traits View Definitions ---------------------------------------------- traits_view = View( Item( 'people', show_label=False, editor=TableEditor( columns=[ ObjectColumn(name='name', editable=False, width=0.50), ObjectColumn(name='age', editable=False, width=0.15), ObjectColumn(name='gender', editable=False, width=0.35), ], auto_size=False, show_toolbar=False, sortable=False, ), ), title='Property List Demo', width=0.25, height=0.33, resizable=True, ) # -- Property Implementations --------------------------------------------- @cached_property def _get_people(self): """Returns the value for the 'people' property.""" return [ Person( name='%s %s' % ( choice(['Tom', 'Dick', 'Harry', 'Alice', 'Lia', 'Vibha']), choice(['Thomas', 'Jones', 'Smith', 'Adams', 'Johnson']), ), age=randint(21, 75), gender=choice(['Male', 'Female']), ) for i in range(randint(10, 20)) ] # -- Default Value Implementations ---------------------------------------- def _begin_default(self): """Starts the background thread running.""" thread = Thread(target=self._timer) thread.daemon = True thread.start() return 0 # -- Private Methods ------------------------------------------------------ def _timer(self): """Triggers a property update every 3 seconds for 30 seconds.""" for i in range(10): sleep(3) self.ticker = True # Create the demo: demo = PropertyListDemo() demo.begin # Run the demo (if invoked from the command line): if __name__ == '__main__': demo.configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/examples/demo/Advanced/Scrubber_editor_demo.py0000644000175100001730000001601100000000000025126 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ This example demonstrates several different variations on using the ScrubberEditor. A 'scrubber' is a type of widget often seen in certain types of applications, such as video editing, image editing, 3D graphics and animation. These types of programs often have many parameters defined over various ranges that the user can tweak to get varying effects or results. Because the number of parameters is typically fairly large, and the amount of screen real estate is fairly limited, these program often use 'scrubbers' to allow the user to adjust the parameter values. A scrubber often looks like a simple text field. The user can type in new values, if they need a precise setting, or simply drag the mouse over the value to set a new value, much like dragging a slider control. The visual feedback often comes in the form of seeing both the text value of the parameter change and the effect that the new parameter value has on the underlying model. For example, in a 3D graphics program, there might be a scrubber for controlling the rotation of the currently selected object around the Y-axis. As the user scrubs the rotation parameter, they also see the model spin on the screen as well. This visual feedback is what makes a scrubber more useful than a simple text entry field. And the fact that the scrubber takes up no more screen real estate that a text entry field is what makes it more useful than a full-fledged slider in space limited applications. The Traits UI ScrubberEditor works as follows: - When the mouse pointer moves over the scrubber, the cursor pointer changes shape to indicate that the field has some additional control behavior. - The control may optionally change color as well, to visually indicate that the control is 'live'. - If you simply click on the scrubber, an active text entry field is displayed, where you can type a new value for the trait, then press the Enter key. - If you click and drag while over the scrubber, the value of the trait is modified based on the direction you move the mouse. Right and/or up increases the value, left and/or down decreases the value. Holding the Shift key down while scrubbing causes the value to change by 10 times its normal amount. Holding the Control key down while scrubbing changes the value by 0.1 times its normal amount. - Scrubbing is not limited to the area of the scrubber control. You can drag as far as you want in any direction, subject to the maximum limits imposed by the trait or ScrubberEditor definition. The ScrubberEditor also supports several different style and functional variations: - The visual default is to display only the special scrubber pointer to indicate to the user that 'scrubber' functionality is available. - By specifying a 'hover_color' value, you can also have the editor change color when the mouse pointer is over it. - By specifying an 'active_color' value, you can have the editor change color while the user is scrubbing. - By specifying a 'border_color' value, you can display a solid border around the editor to mark it as something other than an ordinary text field. - By specifying an 'increment' value, you can tell the editor what the normal increment value for the scrubber should be. Otherwise, the editor will calculate the increment value itself. Explicitly specifying an increment can be very useful in cases where the underlying trait has an unbounded value, which makes it difficult for the editor to determine what a reasonable increment value might be. - The editor will also correctly handle traits with dynamic ranges (i.e. ranges whose high and low limits are defined by other traits). Besides correctly handling the range limits, the editor will also adjust the default tooltip to display the current range of the scrubber. In this example, several of the variations described above are shown: - A simple integer range with default visual cues. - A float range with both 'hover_color' and 'active_color' values specified. - An unbounded range with a 'border_color' value specified. - A dynamic range. This consists of three scrubbers: one to control the low end of the range, one to control the high end, and one that uses the high and low values to determine its range. For comparison purposes, the example also shows the same traits displayed using their default editors. Notes: - This demo only works on the wx backend. """ # -- Imports -------------------------------------------------------------- from traits.api import HasTraits, Range, Float from traitsui.api import View, VGroup, HGroup, Item, ScrubberEditor, spring # -- Shared Item Definition ---------------------------------------- class TItem(Item): editor = ScrubberEditor() # -- ScrubberDemo Class --------------------------------------------------- class ScrubberDemo(HasTraits): # Define some sample ranges and values: simple_integer = Range(0, 100) rollover_float = Range(-10.0, 10.0) bordered_unbounded = Float() dynamic_low = Range(high=-0.01, value=-10.0) dynamic_high = Range(low=0.01, value=10.0) dynamic_value = Range('dynamic_low', 'dynamic_high', 0.0) # Define the demo view: view = View( HGroup( VGroup( Item('simple_integer', editor=ScrubberEditor()), Item( 'rollover_float', editor=ScrubberEditor( hover_color=0xFFFFFF, active_color=0xA0CD9E ), ), Item( 'bordered_unbounded', editor=ScrubberEditor( hover_color=0xFFFFFF, active_color=0xA0CD9E, border_color=0x808080, ), ), Item('dynamic_low', editor=ScrubberEditor()), Item('dynamic_high', editor=ScrubberEditor()), Item('dynamic_value', editor=ScrubberEditor()), show_border=True, label='Scrubber Editors', ), VGroup( Item('simple_integer'), Item('rollover_float'), Item('bordered_unbounded'), Item('dynamic_low'), Item('dynamic_high'), Item('dynamic_value'), show_border=True, label='Default Editors', ), spring, ), title='Scrubber Editor Demo', ) # -- Create and run the demo ---------------------------------------------- # Create the demo: demo = ScrubberDemo() # Run the demo (if invoked from the command line): if __name__ == '__main__': demo.configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/examples/demo/Advanced/Settable_cached_property.py0000644000175100001730000001115700000000000026011 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Creating settable cached Property in Traits How to create a Traits Property which is cached but is not read-only. The example demonstrates how to create a 'settable cached' property. The example itself is nonsensical, and is provided mainly to show how to set up such a property, and how it differs from a standard 'cached' property. A normal 'cached' property does not have a 'setter' method, meaning that the property is read-only. It usually represents a value which depends upon the state of other traits for its value. The cached property provides both a mechanism for remembering (i.e. caching) the current value of the property as well as a means of automatically detecting when the value of the property changes (which causes the cache to be flushed), and notifying any associated trait listeners that the value of the property has changed. Normally there is no 'setter' for such a property because the value is derived from the value of other traits. However, it is possible to define a 'settable cached' property which in addition to the capabilities of a normal 'cached' property, also allows the property's value to be explicitly set. To accomplish this, simply add a '_set_*' method as usual for a settable property (see the '_set_c' method in the example code) which takes the value provided and changes the values the property depends on appropriately based on the new value. This allows code to set the value of the property directly if desired, subject to any constraints specified in the property declaration. For example, in the example, the 'c' trait is a 'settable cached' property whose value must be an integer. Attempting to set a non-integer value of the property will raise an exception, just like any other trait would. If any of the traits which the property depends upon change value, the current value of the property will be flushed from the cache and a change notification for the property will be generated. Any code that then attempts to read the value of the property will result in the cache being reloaded with the new value returned by the property's 'getter' method. In the example, trait 'c' is a 'settable cached' property which returns the product of 'a' times 'b', and trait 'd' is a normal 'cached' property that returns double the value of 'c'. To see the effect of these traits in action, try moving the 'a' and 'b' sliders and watching the 'c' and 'd' traits update. This demonstrates how 'c' and 'd' are properties that depend upon the values of other traits. Now try changing the value of the 'c' trait by moving the slider or typing a new value into the text entry field. You will see that the 'd' trait updates as well, illustrating that the 'c' trait can be set directly, as well as indirectly by changes to 'a' and 'b'. Also, try typing non-numeric values into the 'c' field and you will see that any values set are being type checked as well (i.e. they must be integer values). Now try typing a value into the 'd' trait and you will see that an error results (indicated by the text entry field turning red), because this is a normal 'cached' trait that has no 'setter' method defined. """ from math import sqrt from traits.api import HasTraits, Int, Range, Property, cached_property from traitsui.api import View, Item, RangeEditor # -- Demo Class ----------------------------------------------------------- class SettableCachedProperty(HasTraits): a = Range(1, 10) b = Range(1, 10) c = Property(Int, observe=['a', 'b']) d = Property(observe='c') view = View( Item('a'), Item('b'), '_', Item('c', editor=RangeEditor(low=1, high=100, mode='slider')), Item('c'), '_', Item('d', editor=RangeEditor(low=1, high=400, mode='slider')), Item('d'), width=0.3, ) @cached_property def _get_c(self): return self.a * self.b def _set_c(self, value): self.a = int(sqrt(value)) self.b = int(sqrt(value)) @cached_property def _get_d(self): return self.c + self.c # -- Run the demo --------------------------------------------------------- # Create the demo: demo = SettableCachedProperty() # Run the demo (if invoked from the command line): if __name__ == '__main__': demo.configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/examples/demo/Advanced/Statusbar_demo.py0000644000175100001730000000531200000000000023763 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Displaying a statusbar in a Traits UI A statusbar may contain one or more fields, of fixed or variable size. Fixed width fields are specified in pixels, while variable width fields are specified as fractional values relative to other variable width fields. The content of a statusbar field is specified via the extended trait name of the object attribute that will contain the statusbar information. In this example, there are two statusbar fields: - The current length of the text input data (variable width) - The current time (fixed width, updated once per second). Note the use of a timer thread to update the status bar once per second. """ from time import sleep, strftime from threading import Thread from traits.api import HasPrivateTraits, Str, Property from traitsui.api import View, Item, StatusItem, Label # -- The demo class ------------------------------------------------------- class TextEditor(HasPrivateTraits): # The text being edited: text = Str() # The current length of the text being edited: length = Property(observe='text') # The current time: time = Str() # The view definition: view = View( Label('Type into the text editor box:'), Item('text', style='custom', show_label=False), title='Text Editor', id='traitsui.demo.advanced.statusbar_demo', width=0.4, height=0.4, resizable=True, statusbar=[ StatusItem(name='length', width=0.5), StatusItem(name='time', width=85), ], ) # -- Property Implementations --------------------------------------------- def _get_length(self): return 'Length: %d characters' % len(self.text) # -- Default Trait Values ------------------------------------------------- def _time_default(self): thread = Thread(target=self._clock) thread.daemon = True thread.start() return '' # -- Private Methods ------------------------------------------------------ def _clock(self): """Update the statusbar time once every second.""" while True: self.time = strftime('%I:%M:%S %p') sleep(1.0) # Create the demo object: popup = TextEditor() # Run the demo (if invoked from the command line): if __name__ == '__main__': popup.configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/examples/demo/Advanced/String_list_ui_editor.py0000644000175100001730000001072500000000000025357 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ **WARNING** This demo might not work as expected and some documented features might be missing. ------------------------------------------------------------------------------- Another demo showing how to use a TabularEditor to create a multi-select list box. This demo creates a reusable StringListEditor class and uses that instead of defining the editor as part of the demo class. This approach greatly simplifies the actual demo class and shows how to construct a reusable Traits UI-based editor that can be used in other applications. """ # Issue related to the demo warning: enthought/traitsui#960 from traits.api import HasPrivateTraits, List, Str, Property, observe from traits.etsconfig.api import ETSConfig from traitsui.api import ( BasicEditorFactory, HGroup, Item, TabularAdapter, TabularEditor, View, ) if ETSConfig.toolkit == 'wx': from traitsui.wx.ui_editor import UIEditor else: from traitsui.qt.ui_editor import UIEditor # -- Define the reusable StringListEditor class and its helper classes -------- # Define the tabular adapter used by the Traits UI string list editor: class MultiSelectAdapter(TabularAdapter): # The columns in the table (just the string value): columns = [('Value', 'value')] # The text property used for the 'value' column: value_text = Property() def _get_value_text(self): return self.item # Define the actual Traits UI string list editor: class _StringListEditor(UIEditor): # Indicate that the editor is scrollable/resizable: scrollable = True # The list of available editor choices: choices = List(Str) # The list of currently selected items: selected = List(Str) # The traits UI view used by the editor: traits_view = View( Item( 'choices', show_label=False, editor=TabularEditor( show_titles=False, selected='selected', editable=False, multi_select=True, adapter=MultiSelectAdapter(), ), ), id='string_list_editor', resizable=True, ) def init_ui(self, parent): self.sync_value(self.factory.choices, 'choices', 'from', is_list=True) self.selected = self.value return self.edit_traits(parent=parent, kind='subpanel') @observe('selected') def _selected_modified(self, event): self.value = self.selected # Define the StringListEditor class used by client code: class StringListEditor(BasicEditorFactory): # The editor implementation class: klass = _StringListEditor # The extended trait name containing the editor's set of choices: choices = Str() # -- Define the demo class ---------------------------------------------------- class MultiSelect(HasPrivateTraits): """This class demonstrates using the StringListEditor to select a set of string values from a set of choices. """ # The list of choices to select from: choices = List(Str) # The currently selected list of choices: selected = List(Str) # A dummy result so that we can display the selection using the same # StringListEditor: result = List(Str) # A traits view showing the list of choices on the left-hand side, and # the currently selected choices on the right-hand side: traits_view = View( HGroup( Item( 'selected', show_label=False, editor=StringListEditor(choices='choices'), ), Item( 'result', show_label=False, editor=StringListEditor(choices='selected'), ), ), width=0.20, height=0.25, ) # Create the demo: demo = MultiSelect( choices=[ 'one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine', 'ten', ], selected=['two', 'five', 'nine'], ) # Run the demo (if invoked from the command line): if __name__ == '__main__': demo.configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/examples/demo/Advanced/Table_editor_with_checkbox_column.py0000644000175100001730000000743300000000000027670 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ This shows a table editor which has a checkbox column in addition to normal data columns. """ from random import randint from traits.api import HasStrictTraits, Str, Int, Float, List, Bool, Property from traitsui.api import Item, ObjectColumn, TableEditor, View from traitsui.extras.checkbox_column import CheckboxColumn # Create a specialized column to set the text color differently based upon # whether or not the player is in the lineup: class PlayerColumn(ObjectColumn): # Override some default settings for the column: width = 0.08 horizontal_alignment = 'center' def get_text_color(self, object): if object.in_lineup: return 'black' return 'light grey' # The 'players' trait table editor: player_editor = TableEditor( sortable=False, configurable=False, auto_size=False, selected_indices='selected_player_indices', columns=[ CheckboxColumn(name='in_lineup', label='In Lineup', width=0.12), PlayerColumn( name='name', editable=False, width=0.24, horizontal_alignment='left', ), PlayerColumn(name='at_bats', label='AB'), PlayerColumn(name='strike_outs', label='SO'), PlayerColumn(name='singles', label='S'), PlayerColumn(name='doubles', label='D'), PlayerColumn(name='triples', label='T'), PlayerColumn(name='home_runs', label='HR'), PlayerColumn(name='walks', label='W'), PlayerColumn( name='average', label='Ave', editable=False, format='%0.3f' ), ], ) class Player(HasStrictTraits): # Trait definitions: in_lineup = Bool(True) name = Str() at_bats = Int() strike_outs = Int() singles = Int() doubles = Int() triples = Int() home_runs = Int() walks = Int() average = Property(Float) def _get_average(self): """Computes the player's batting average from the current statistics.""" if self.at_bats == 0: return 0.0 return ( float(self.singles + self.doubles + self.triples + self.home_runs) / self.at_bats ) class Team(HasStrictTraits): # Trait definitions: players = List(Player) selected_player_indices = List() # Trait view definitions: traits_view = View( Item('players', show_label=False, editor=player_editor), title='Baseball Team Roster Demo', width=0.5, height=0.5, resizable=True, ) def random_player(name): """Generates and returns a random player.""" p = Player( name=name, strike_outs=randint(0, 50), singles=randint(0, 50), doubles=randint(0, 20), triples=randint(0, 5), home_runs=randint(0, 30), walks=randint(0, 50), ) return p.trait_set( at_bats=( p.strike_outs + p.singles + p.doubles + p.triples + p.home_runs + randint(100, 200) ) ) # Create the demo: demo = Team( players=[ random_player(name) for name in [ 'Dave', 'Mike', 'Joe', 'Tom', 'Dick', 'Harry', 'Dirk', 'Fields', 'Stretch', ] ] ) # Run the demo (if invoked from the command line): if __name__ == '__main__': demo.configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/examples/demo/Advanced/Table_editor_with_context_menu_demo.py0000644000175100001730000001206500000000000030236 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Defining column-specific context menu in a Table Shows a TableEditor which has column-specific context menus. The demo is a simple baseball scoring system, which lists each player and their current batting statistics. After a given player has an at bat, you right-click on the table cell corresponding to the player and the result of the at-bat (e.g. 'S' = single) and select the 'Add' menu option to register that the player hit a single and update the player's overall statistics. This demo also illustrates the use of Property traits, and how using 'event' meta-data can simplify event handling by collapsing an event that can occur on a number of traits into a category of event, which can be handled by a single event handler defined for the category (in this case, the category is 'affects_average'). """ from random import randint from traits.api import HasStrictTraits, Str, Int, Float, List, Property from traitsui.api import Action, Item, Menu, ObjectColumn, TableEditor, View # Define a custom table column for handling items which affect the player's # batting average: class AffectsAverageColumn(ObjectColumn): # Define the context menu for the column: menu = Menu( Action(name='Add', action='column.add(object)'), Action(name='Sub', action='column.sub(object)'), ) # Right-align numeric values (override): horizontal_alignment = 'center' # Column width (override): width = 0.09 # Don't allow the data to be edited directly: editable = False # Action methods for the context menu items: def add(self, object): """Increment the affected player statistic.""" setattr(object, self.name, getattr(object, self.name) + 1) def sub(self, object): """Decrement the affected player statistic.""" setattr(object, self.name, getattr(object, self.name) - 1) # The 'players' trait table editor: player_editor = TableEditor( editable=True, sortable=False, auto_size=False, columns=[ ObjectColumn(name='name', editable=False, width=0.28), AffectsAverageColumn(name='at_bats', label='AB'), AffectsAverageColumn(name='strike_outs', label='SO'), AffectsAverageColumn(name='singles', label='S'), AffectsAverageColumn(name='doubles', label='D'), AffectsAverageColumn(name='triples', label='T'), AffectsAverageColumn(name='home_runs', label='HR'), AffectsAverageColumn(name='walks', label='W'), ObjectColumn( name='average', label='Ave', editable=False, width=0.09, horizontal_alignment='center', format='%0.3f', ), ], ) # 'Player' class: class Player(HasStrictTraits): # Trait definitions: name = Str() at_bats = Int() strike_outs = Int(event='affects_average') singles = Int(event='affects_average') doubles = Int(event='affects_average') triples = Int(event='affects_average') home_runs = Int(event='affects_average') walks = Int() average = Property(Float) def _get_average(self): """Computes the player's batting average from the current statistics.""" if self.at_bats == 0: return 0.0 return ( float(self.singles + self.doubles + self.triples + self.home_runs) / self.at_bats ) def _affects_average_changed(self): """Handles an event that affects the player's batting average.""" self.at_bats += 1 class Team(HasStrictTraits): # Trait definitions: players = List(Player) # Trait view definitions: traits_view = View( Item('players', show_label=False, editor=player_editor), title='Baseball Scoring Demo', width=0.5, height=0.5, resizable=True, ) def random_player(name): """Generates and returns a random player.""" p = Player( name=name, strike_outs=randint(0, 50), singles=randint(0, 50), doubles=randint(0, 20), triples=randint(0, 5), home_runs=randint(0, 30), walks=randint(0, 50), ) return p.trait_set( at_bats=( p.strike_outs + p.singles + p.doubles + p.triples + p.home_runs + randint(100, 200) ) ) # Create the demo: demo = Team( players=[ random_player(name) for name in [ 'Dave', 'Mike', 'Joe', 'Tom', 'Dick', 'Harry', 'Dirk', 'Fields', 'Stretch', ] ] ) # Run the demo (if invoked from the command line): if __name__ == '__main__': demo.configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/examples/demo/Advanced/Table_editor_with_live_search_and_cell_editor.py0000644000175100001730000003512200000000000032174 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ This example demonstrates how to implement 'live search' using a TableEditor, as well as how to embed more sophisticated editors, such as a CodeEditor, within a table cell. This example also makes extensive use of cached properties. The example is a fairly simple source code file search utility. You determine which files to search and what to search for using the various controls spread across the top line of the view. You specify the root directory to search for files in using the 'Path' field. You can either: - Type in a directory name. - Click the '...' button and select a directory from the drop-down tree view that is displayed. - Click on the directory name drop-down to display a history list of the 10 most recently visited directories, and select a directory from the list. You can specify whether sub-directories should be included or not by toggling the 'Recursive' checkbox on or off. You can specify the types of files to be searched by clicking on the 'Type' drop-down and selecting a file type such as Python or C from the list. You can specify what string to search for by typing into the 'Search' field. The set of source files containing the search string is automatically updated as you type (this is the 'live search' feature). The search field also maintains a history of previous searches, so you can click on the drop-down arrow to display and select a previous search. Note that entries are only added to the search history when the enter key is pressed. This prevents each partial substring typed from being added to the history as a separate entry. You can specify whether the search is case sensitive or not by toggling the 'Case sensitive' checkbox on or off. The results of the search are displayed in a table below the input fields. The table contains four columns: - #: The number of lines matching the search string in the file. - Matches: A list of all lines containing search string matches in the file. Normally, only the first match is displayed, but you can click on this field to display the entire list of matches (the table row will expand and a CodeEditor will be displayed showing the complete list of matching source code file lines). You can click on or cursor to lines in the code editor to display the corresponding source code line in context in the code editor that appears at the bottom of the view. - Name: Displays the base name of the source file with no path information. - Path: Displays the portion of the source file path not included in the the root directory being used for the search. Selecting a line in the table editor will display the contents of the corresponding source file in the Code Editor displayed at the bottom of the view. After clicking on a 'Matches' column entry you can use the cursor up and down keys to select the various matching source code lines displayed in the table cell editor. You can move to the next or previous 'Matches' entry by pressing the Ctrl-Up and Ctrl-Down cursor keys. You can also use the Ctrl-Left and Ctrl-Right cursor keys to move to the previous or next column on the same line. You can also exit the 'Matches' code editor by pressing the Escape key. Finally: - You can click and drag the little circle to the right of the currently selected file to drag and drop the file. This can be useful, for example, to drag and drop the file into your favorite text editor. - Similarly, you can also drag the contents of the 'Name' column into your favorite text editor to edit the file corresponding to that line in the table. """ # -- Imports -------------------------------------------------------------- from os import walk, listdir from os.path import basename, dirname, splitext, join from traits.api import ( Any, Bool, Directory, Enum, File, HasTraits, Instance, Int, List, Property, Str, cached_property, observe, ) from traitsui.api import ( CodeEditor, DNDEditor, HGroup, HistoryEditor, Item, TableEditor, TableFilter, TitleEditor, VGroup, View, VSplit, ) from traitsui.table_column import ObjectColumn from io import open # -- Constants ------------------------------------------------------------ FileTypes = { 'Python': ['.py'], 'C': ['.c', '.h'], 'C++': ['.cpp', '.h'], 'Java': ['.java'], 'Ruby': ['.rb'], } DEFAULT_ROOT = dirname(__file__) # -- The Live Search table editor definition ------------------------------ class MatchesColumn1(ObjectColumn): def get_value(self, object): n = len(self.get_raw_value(object)) if n == 0: return '' return str(n) class MatchesColumn2(ObjectColumn): def is_editable(self, object): return len(object.matches) > 0 class FileColumn(ObjectColumn): def get_drag_value(self, object): return object.full_name table_editor = TableEditor( columns=[ MatchesColumn1( name='matches', label='#', editable=False, width=0.05, horizontal_alignment='center', ), MatchesColumn2( name='matches', width=0.35, format_func=lambda x: (x + [''])[0].strip(), editor=CodeEditor( line='object.live_search.selected_match', selected_line='object.live_search.selected_match', ), style='readonly', edit_width=0.95, edit_height=0.33, ), FileColumn(name='base_name', label='Name', width=0.30, editable=False), ObjectColumn( name='ext_path', label='Path', width=0.30, editable=False ), ], filter_name='filter', auto_size=False, show_toolbar=False, selected='selected', selection_color=0x000000, selection_bg_color=0xFBD391, ) # -- LiveSearch class ----------------------------------------------------- class LiveSearch(HasTraits): # The currenty root directory being searched: root = Directory(DEFAULT_ROOT, entries=10) # Should sub directories be included in the search: recursive = Bool(True) # The file types to include in the search: file_type = Enum('Python', 'C', 'C++', 'Java', 'Ruby') # The current search string: search = Str() # Is the search case sensitive? case_sensitive = Bool(False) # The live search table filter: filter = Property(Instance(TableFilter), observe='search, case_sensitive') # The current list of source files being searched: source_files = Property( List(Instance("SourceFile")), observe='root, recursive, file_type', ) # The currently selected source file: selected = Instance("SourceFile") # The contents of the currently selected source file: selected_contents = Property(List(Str), observe='selected') # The currently selected match: selected_match = Int() # The text line corresponding to the selected match: selected_line = Property(Int, observe='selected, selected_match') # The full name of the currently selected source file: selected_full_name = Property(File, observe='selected.full_name') # The list of marked lines for the currently selected file: mark_lines = Property(List(Int), observe='selected') # Summary of current number of files and matches: summary = Property(Str, observe='source_files, search, case_sensitive') # -- Traits UI Views ------------------------------------------------------ view = View( VGroup( HGroup( Item('root', id='root', label='Path', width=0.5), Item('recursive'), Item('file_type', label='Type'), Item( 'search', id='search', width=0.5, editor=HistoryEditor(auto_set=True), ), Item('case_sensitive'), ), VSplit( VGroup( Item('summary', editor=TitleEditor()), Item( 'source_files', id='source_files', editor=table_editor ), dock='horizontal', show_labels=False, ), VGroup( HGroup( Item( 'selected_full_name', editor=TitleEditor(), springy=True, ), Item( 'selected_full_name', editor=DNDEditor(), tooltip='Drag this file', ), show_labels=False, ), Item( 'selected_contents', style='readonly', editor=CodeEditor( mark_lines='mark_lines', line='selected_line', selected_line='selected_line', ), ), dock='horizontal', show_labels=False, ), id='splitter', ), ), title='Live File Search', id='enthought.examples.demo.Advanced.' 'Table_editor_with_live_search_and_cell_editor.LiveSearch', width=0.75, height=0.67, resizable=True, ) # -- Property Implementations --------------------------------------------- @cached_property def _get_filter(self): if len(self.search) == 0: return lambda x: True return lambda x: len(x.matches) > 0 @cached_property def _get_source_files(self): root = self.root if root == '': root = DEFAULT_ROOT file_types = FileTypes[self.file_type] if self.recursive: result = [] for dir_path, dir_names, file_names in walk(root): for file_name in file_names: if splitext(file_name)[1] in file_types: result.append( SourceFile( live_search=self, full_name=join(dir_path, file_name), ) ) return result return [ SourceFile(live_search=self, full_name=join(root, file_name)) for file_name in listdir(root) if splitext(file_name)[1] in file_types ] def _get_selected_contents(self): if self.selected is None: return '' return ''.join(self.selected.contents) @cached_property def _get_mark_lines(self): if self.selected is None: return [] return [int(match.split(':', 1)[0]) for match in self.selected.matches] @cached_property def _get_selected_line(self): selected = self.selected if (selected is None) or (len(selected.matches) == 0): return 1 return int(selected.matches[self.selected_match - 1].split(':', 1)[0]) @cached_property def _get_selected_full_name(self): if self.selected is None: return '' return self.selected.full_name @cached_property def _get_summary(self): source_files = self.source_files search = self.search if search == '': return 'A total of %d files.' % len(source_files) files = 0 matches = 0 for source_file in source_files: n = len(source_file.matches) if n > 0: files += 1 matches += n return 'A total of %d files with %d files containing %d matches.' % ( len(source_files), files, matches, ) # -- Traits Event Handlers ------------------------------------------------ @observe('selected') def _update_selected_match(self, event): self.selected_match = 1 @observe('source_files') def _update_selected_file(self, event): if len(self.source_files) > 0: self.selected = self.source_files[0] else: self.selected = None # -- SourceFile class ----------------------------------------------------- class SourceFile(HasTraits): # The search object this source file is associated with: live_search = Instance(LiveSearch) # The full path and file name of the source file: full_name = File() # The base file name of the source file: base_name = Property(Str, observe='full_name') # The portion of the file path beyond the root search path: ext_path = Property(Str, observe='full_name') # The contents of the source file: contents = Property(List(Str), observe='full_name') # The list of matches for the current search criteria: matches = Property(List(Str), observe='full_name, live_search.[search, case_sensitive]') # -- Property Implementations --------------------------------------------- def _get_base_name(self): return basename(self.full_name) def _get_ext_path(self): return dirname(self.full_name)[len(self.live_search.root) :] @cached_property def _get_contents(self): try: with open(self.full_name, 'rU', encoding='utf8') as fh: contents = fh.readlines() return contents except Exception: return '' @cached_property def _get_matches(self): search = self.live_search.search if search == '': return [] case_sensitive = self.live_search.case_sensitive if case_sensitive: return [ '%5d: %s' % ((i + 1), line.strip()) for i, line in enumerate(self.contents) if line.find(search) >= 0 ] search = search.lower() return [ '%5d: %s' % ((i + 1), line.strip()) for i, line in enumerate(self.contents) if line.lower().find(search) >= 0 ] # -- Set up and run the demo ---------------------------------------------- # Create the demo object: demo = LiveSearch() # Run the demo (if invoked from the command line): if __name__ == '__main__': demo.configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/examples/demo/Advanced/Table_editor_with_progress_column.py0000644000175100001730000000360400000000000027742 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ A demo to demonstrate the progress column in a TableEditor. The demo only works with qt backend. """ import random from pyface.api import GUI from traits.api import Button, HasTraits, Instance, Int, List, observe, Str from traitsui.api import ObjectColumn, TableEditor, UItem, View from traitsui.extras.progress_column import ProgressColumn class Job(HasTraits): name = Str() percent_complete = Int() class JobManager(HasTraits): jobs = List(Instance(Job)) start = Button() def populate(self): self.jobs = [ Job(name='job %02d' % i, percent_complete=0) for i in range(1, 25) ] def process(self): for job in self.jobs: job.percent_complete = min( job.percent_complete + random.randint(0, 3), 100 ) if any(job.percent_complete < 100 for job in self.jobs): GUI.invoke_after(100, self.process) @observe('start') def _populate_and_process(self, event): self.populate() GUI.invoke_after(1000, self.process) traits_view = View( UItem( 'jobs', editor=TableEditor( columns=[ ObjectColumn(name='name'), ProgressColumn(name='percent_complete'), ] ), ), UItem('start'), resizable=True, ) demo = JobManager() # Run the demo (if invoked from the command line): if __name__ == '__main__': demo.configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/examples/demo/Advanced/Tabular_editor_demo.py0000644000175100001730000003010700000000000024753 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ **WARNING** This demo might not work as expected and some documented features might be missing. ------------------------------------------------------------------------------- Tabular editor The TabularEditor can be used for many of the same purposes as the TableEditor, that is, for displaying a table of attributes of lists or arrays of objects. While similar in function, the tabular editor has advantages and disadvantages relative to the table editor. See the Traits UI User Manual for details. This example defines three classes: - *Person*: A single person. - *MarriedPerson*: A married person (subclass of Person). - *Report*: Defines a report based on a list of single and married people. It creates a tabular display of 10,000 single and married people showing the following information: - Name of the person. - Age of the person. - The person's address. - The name of the person's spouse (if any). In addition: - It uses a Courier 10 point font for each line in the table. - It displays age column right, instead of left, justified. - If the person is a minor (age < 18) and married, it displays a red flag image in the age column. - If the person is married, it makes the background color for that row a light blue. - If this demo is running under QT, it displays each person's surname in a row label. This example demonstrates: - How to set up a *TabularEditor*. - The display speed of the *TabularEditor*. - How to create a *TabularAdapter* that meets each of the specified display requirements. Additional notes: - You can change the current selection using the up and down arrow keys. - If the demo is running under WX, you can move a selected row up and down in the table using the left and right arrow keys. - If the demo is running under QT, you can move rows by clicking and dragging. Hopefully, this simple example conveys some of the power and flexibility that the `TabularAdapter` class provides you. But, just in case it doesn't, let's go over some of the more interesting details: - Note the values in the `~TabularAdapter.columns` trait. The first three values define *column ids* which map directly to traits defined on our data objects, while the last one defines an arbitrary string which we define so that we can reference it in the `MarriedPerson_spouse_text` and `Person_spouse_text` trait definitions. - Since the font we want to use applies to all table rows, we just specify a new default value for the existing `TabularAdapter.font` trait. - Since we only want to override the default left alignment for the age column, we simply define an `age_alignment` trait as a constant ``'right'`` value. We could have also used ``age_alignment = Str('right')``, but `Constant` never requires storage to be used in an object. - We define the `MarriedPerson_age_image` property to handle putting the ``'red flag'`` image in the age column. By including the class name of the items it applies to, we only need to check the `age` value in determining what value to return. - Similary, we use the `MarriedPerson_bg_color` trait to ensure that each `MarriedPerson` object has the correct background color in the table. - Finally, we use the `MarriedPerson_spouse_text` and `Person_spouse_text` traits, one a property and the other a simple constant value, to determine what text to display in the *Spouse* column for the different object types. Note that even though a `MarriedPerson` is both a `Person` and a `MarriedPerson`, it will correctly use the `MarriedPerson_spouse_text` trait since the search for a matching trait is always made in *mro* order. Although this is completely subjective, some of the things that the author feels stand out about this approach are: - The class definition is short and sweet. Less code is good. - The bulk of the code is declarative. Less room for logic errors. - There is only one bit of logic in the class (the ``if`` statement in the `MarriedPerson_age_image` property implementation). Again, less logic usually translates into more reliable code). - The defined traits and even the property implementation method names read very descriptively. `_get_MarriedPerson_age_image` pretty much says what you would write in a comment or doc string. The implementation almost is the documentation. """ # Issue related to the demo warning: enthought/traitsui#960 from functools import partial from random import randint, choice, shuffle from traits.api import HasTraits, Str, Int, List, Instance, Property, Constant from traits.etsconfig.api import ETSConfig from traitsui.api import ( View, Group, Item, TabularAdapter, TabularEditor, Color, ) # -- Person Class Definition ---------------------------------------------- class Person(HasTraits): name = Str() address = Str() age = Int() # surname is displayed in qt-only row label: surname = Property(fget=lambda self: self.name.split()[-1], observe='name') # -- MarriedPerson Class Definition --------------------------------------- class MarriedPerson(Person): partner = Instance(Person) # -- Tabular Adapter Definition ------------------------------------------- class ReportAdapter(TabularAdapter): """The tabular adapter interfaces between the tabular editor and the data being displayed. For more details, please refer to the traitsUI user guide. """ # List of (Column labels, Column ID). columns = [ ('Name', 'name'), ('Age', 'age'), ('Address', 'address'), ('Spouse', 'spouse'), ] row_label_name = 'surname' # Interfacing between the model and the view: make some of the cell # attributes a property whose behavior is then controlled by the get # (and optionally set methods). The cell is identified by its column # ID (age, spouse). # Overwrite default value font = 'Courier 10' age_alignment = Constant('right') MarriedPerson_age_image = Property() MarriedPerson_bg_color = Color(0xE0E0FF) MarriedPerson_spouse_text = Property() Person_spouse_text = Constant('') def _get_MarriedPerson_age_image(self): if self.item.age < 18: return '@icons:red_ball' return None def _get_MarriedPerson_spouse_text(self): return self.item.partner.name # -- Tabular Editor Definition -------------------------------------------- # The tabular editor works in conjunction with an adapter class, derived from # TabularAdapter. tabular_editor = TabularEditor( adapter=ReportAdapter(), operations=['move', 'edit'], # Row titles are not supported in WX: show_row_titles=(ETSConfig.toolkit in {"qt", "qt4"}), ) # -- Report Class Definition ---------------------------------------------- class Report(HasTraits): people = List(Person) traits_view = View( Group( Item('people', id='table', editor=tabular_editor), show_labels=False, ), title='Tabular Editor Demo', id='traitsui.demo.Applications.tabular_editor_demo', width=0.60, height=0.75, resizable=True, ) # -- Generate 10,000 random single and married people --------------------- male_names = [ 'Michael', 'Edward', 'Timothy', 'James', 'George', 'Ralph', 'David', 'Martin', 'Bryce', 'Richard', 'Eric', 'Travis', 'Robert', 'Bryan', 'Alan', 'Harold', 'John', 'Stephen', 'Gael', 'Frederic', 'Eli', 'Scott', 'Samuel', 'Alexander', 'Tobias', 'Sven', 'Peter', 'Albert', 'Thomas', 'Horatio', 'Julius', 'Henry', 'Walter', 'Woodrow', 'Dylan', 'Elmer', ] female_names = [ 'Leah', 'Jaya', 'Katrina', 'Vibha', 'Diane', 'Lisa', 'Jean', 'Alice', 'Rebecca', 'Delia', 'Christine', 'Marie', 'Dorothy', 'Ellen', 'Victoria', 'Elizabeth', 'Margaret', 'Joyce', 'Sally', 'Ethel', 'Esther', 'Suzanne', 'Monica', 'Hortense', 'Samantha', 'Tabitha', 'Judith', 'Ariel', 'Helen', 'Mary', 'Jane', 'Janet', 'Jennifer', 'Rita', 'Rena', 'Rianna', ] all_names = male_names + female_names male_name = partial(choice, male_names) female_name = partial(choice, female_names) any_name = partial(choice, all_names) age = partial(randint, 15, 72) def family_name(): return choice( [ 'Jones', 'Smith', 'Thompson', 'Hayes', 'Thomas', 'Boyle', "O'Reilly", 'Lebowski', 'Lennon', 'Starr', 'McCartney', 'Harrison', 'Harrelson', 'Steinbeck', 'Rand', 'Hemingway', 'Zhivago', 'Clemens', 'Heinlien', 'Farmer', 'Niven', 'Van Vogt', 'Sturbridge', 'Washington', 'Adams', 'Bush', 'Kennedy', 'Ford', 'Lincoln', 'Jackson', 'Johnson', 'Eisenhower', 'Truman', 'Roosevelt', 'Wilson', 'Coolidge', 'Mack', 'Moon', 'Monroe', 'Springsteen', 'Rigby', "O'Neil", 'Philips', 'Clinton', 'Clapton', 'Santana', 'Midler', 'Flack', 'Conner', 'Bond', 'Seinfeld', 'Costanza', 'Kramer', 'Falk', 'Moore', 'Cramdon', 'Baird', 'Baer', 'Spears', 'Simmons', 'Roberts', 'Michaels', 'Stuart', 'Montague', 'Miller', ] ) def address(): number = randint(11, 999) text_1 = choice( [ 'Spring', 'Summer', 'Moonlight', 'Winding', 'Windy', 'Whispering', 'Falling', 'Roaring', 'Hummingbird', 'Mockingbird', 'Bluebird', 'Robin', 'Babbling', 'Cedar', 'Pine', 'Ash', 'Maple', 'Oak', 'Birch', 'Cherry', 'Blossom', 'Rosewood', 'Apple', 'Peach', 'Blackberry', 'Strawberry', 'Starlight', 'Wilderness', 'Dappled', 'Beaver', 'Acorn', 'Pecan', 'Pheasant', 'Owl', ] ) text_2 = choice( [ 'Way', 'Lane', 'Boulevard', 'Street', 'Drive', 'Circle', 'Avenue', 'Trail', ] ) return '%d %s %s' % (number, text_1, text_2) people = [ Person( name='%s %s' % (any_name(), family_name()), age=age(), address=address(), ) for i in range(5000) ] marrieds = [ ( MarriedPerson( name='%s %s' % (female_name(), last_name), age=age(), address=address, ), MarriedPerson( name='%s %s' % (male_name(), last_name), age=age(), address=address ), ) for last_name, address in [(family_name(), address()) for i in range(2500)] ] for female, male in marrieds: female.partner = male male.partner = female people.extend([female, male]) shuffle(people) # Create the demo: demo = Report(people=people) # Run the demo (if invoked from the command line): if __name__ == '__main__': demo.configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/examples/demo/Advanced/Tabular_editor_with_context_menu_demo.py0000644000175100001730000001275500000000000030607 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ **WARNING** This demo might not work as expected and some documented features might be missing. ------------------------------------------------------------------------------- Defining column-specific context menu in a Tabular Editor. Shows how the example for the Table Editor (`Table_Editor_with_context_menu_demo.py`) can be adapted to work with a Tabular Editor. The demo is a simple baseball scoring system, which lists each player and their current batting statistics. After a given player has an at bat, you right-click on the table cell corresponding to the player and the result of the at-bat (e.g. 'S' = single) and select the 'Add' menu option to register that the player hit a single and update the player's overall statistics. This demo also illustrates the use of Property traits, and how using 'event' meta-data can simplify event handling by collapsing an event that can occur on a number of traits into a category of event, which can be handled by a single event handler defined for the category (in this case, the category is 'affects_average'). """ # Issue related to the demo warning: enthought/traitsui#960 from random import randint from traits.api import HasStrictTraits, Str, Int, Float, List, Property from traitsui.api import ( Action, Item, Menu, TabularAdapter, TabularEditor, View, ) # Define a custom tabular adapter for handling items which affect the player's # batting average: class PlayerAdapter(TabularAdapter): # Overwrite default values alignment = 'center' width = 0.09 def get_menu(self, object, trait, row, column): column_name = self.column_map[column] if column_name not in ['name', 'average']: menu = Menu( Action(name='Add', action='editor.adapter.add(item, column)'), Action(name='Sub', action='editor.adapter.sub(item, column)'), ) return menu else: return super().get_menu(object, trait, row, column) def get_format(self, object, trait, row, column): column_name = self.column_map[column] if column_name == 'average': return '%0.3f' else: return super().get_format(object, trait, row, column) def add(self, object, column): """Increment the affected player statistic.""" column_name = self.column_map[column] setattr(object, column_name, getattr(object, column_name) + 1) def sub(self, object, column): """Decrement the affected player statistic.""" column_name = self.column_map[column] setattr(object, column_name, getattr(object, column_name) - 1) # The 'players' trait table editor: columns = [ ('Player Name', 'name'), ('AB', 'at_bats'), ('SO', 'strike_outs'), ('S', 'singles'), ('D', 'doubles'), ('T', 'triples'), ('HR', 'home_runs'), ('W', 'walks'), ('Ave', 'average'), ] player_editor = TabularEditor( editable=True, auto_resize=True, auto_resize_rows=True, stretch_last_section=False, auto_update=True, adapter=PlayerAdapter(columns=columns), ) # 'Player' class: class Player(HasStrictTraits): # Trait definitions: name = Str() at_bats = Int() strike_outs = Int(event='affects_average') singles = Int(event='affects_average') doubles = Int(event='affects_average') triples = Int(event='affects_average') home_runs = Int(event='affects_average') walks = Int() average = Property(Float) def _get_average(self): """Computes the player's batting average from the current statistics.""" if self.at_bats == 0: return 0.0 return ( float(self.singles + self.doubles + self.triples + self.home_runs) / self.at_bats ) def _affects_average_changed(self): """Handles an event that affects the player's batting average.""" self.at_bats += 1 class Team(HasStrictTraits): # Trait definitions: players = List(Player) # Trait view definitions: traits_view = View( Item('players', show_label=False, editor=player_editor), title='Baseball Scoring Demo', width=0.5, height=0.5, resizable=True, ) def random_player(name): """Generates and returns a random player.""" p = Player( name=name, strike_outs=randint(0, 50), singles=randint(0, 50), doubles=randint(0, 20), triples=randint(0, 5), home_runs=randint(0, 30), walks=randint(0, 50), ) return p.trait_set( at_bats=( p.strike_outs + p.singles + p.doubles + p.triples + p.home_runs + randint(100, 200) ) ) # Create the demo: demo = Team( players=[ random_player(name) for name in [ 'Dave', 'Mike', 'Joe', 'Tom', 'Dick', 'Harry', 'Dirk', 'Fields', 'Stretch', ] ] ) # Run the demo (if invoked from the command line): if __name__ == '__main__': demo.configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/examples/demo/Advanced/Time_editor_demo.py0000644000175100001730000000255000000000000024260 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ A timer editor Display or edit a time. You can edit the time directly, or by using only the arrow keys (left & right to navigate, up & down to change). """ import datetime from traits.api import HasTraits, Time from traitsui.api import View, Item, TimeEditor class TimeEditorDemo(HasTraits): """Demo class.""" time = Time(datetime.time(12, 0, 0)) traits_view = View( Item('time', label='Simple Editor'), Item( 'time', label='Readonly Editor', style='readonly', # Show 24-hour mode instead of default 12 hour. editor=TimeEditor(strftime='%H:%M:%S'), ), resizable=True, ) def _time_changed(self): """Print each time the time value is changed in the editor.""" print(self.time) # -- Set Up The Demo ------------------------------------------------------ demo = TimeEditorDemo() if __name__ == "__main__": demo.configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/examples/demo/Advanced/Tree_editor_required_traits_demo.py0000644000175100001730000001114100000000000027543 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ This is a modified version of the Standard_Editors/TreeEditor_demo.py in which the `Employee` class is now a subclass of HasRequiredTraits. Thus, to create a new `TreeNode` for `Employee`, to create the associated `Employee` object we can not simply do `Employee()`. Luckily the `TreeNode` API provides a means for specifying a callable that returns a created instance when we want to add a new node. To do so we can simply pass a tuple of the for (klass, prompt, factorry) as an item the `add` list trait for a TreeNode. klass is the class of object we want to be addable, prompt is a boolean indicating whether or not we want to prompt the user to specify traits after we instantiate the object before it is added to the tree, and factory is the previously described callable that must return an instance of klass. """ from traits.api import HasRequiredTraits, HasTraits, Str, Regex, List, Instance from traitsui.api import Item, View, TreeEditor, TreeNode class Employee(HasRequiredTraits): """Defines a company employee.""" name = Str('') title = Str() phone = Regex(regex=r'\d\d\d-\d\d\d\d') def default_title(self): self.title = 'Senior Engineer' def create_default_employee(): return Employee(name="Dilbert", title="Engineer", phone="999-8212") class Department(HasTraits): """Defines a department with employees.""" name = Str('') employees = List(Employee) class Company(HasTraits): """Defines a company with departments and employees.""" name = Str('') departments = List(Department) employees = List(Employee) # Create an empty view for objects that have no data to display: no_view = View() # Define the TreeEditor used to display the hierarchy: tree_editor = TreeEditor( nodes=[ # The first node specified is the top level one TreeNode( node_for=[Company], auto_open=True, # child nodes are children='', label='name', # label with Company name view=View(['name']), ), TreeNode( node_for=[Company], auto_open=True, children='departments', label='=Departments', # constant label view=no_view, add=[Department], ), TreeNode( node_for=[Company], auto_open=True, children='employees', label='=Employees', # constant label view=no_view, add=[(Employee, True, create_default_employee)], ), TreeNode( node_for=[Department], auto_open=True, children='employees', label='name', # label with Department name view=View(['name']), add=[(Employee, True, create_default_employee)], ), TreeNode( node_for=[Employee], auto_open=True, label='name', # label with Employee name view=View(['name', 'title', 'phone']), ), ] ) class Partner(HasTraits): """Defines a business partner.""" name = Str('') company = Instance(Company) traits_view = View( Item(name='company', editor=tree_editor, show_label=False), title='Company Structure', buttons=['OK'], resizable=True, style='custom', width=0.3, height=500, ) # Create an example data structure: jason = Employee(name='Jason', title='Senior Engineer', phone='536-1057') mike = Employee(name='Mike', title='Senior Engineer', phone='536-1057') dave = Employee(name='Dave', title='Senior Developer', phone='536-1057') martin = Employee(name='Martin', title='Senior Engineer', phone='536-1057') duncan = Employee(name='Duncan', title='Consultant', phone='526-1057') # Create the demo: demo = Partner( name='Enthought, Inc.', company=Company( name='Enthought', employees=[dave, martin, duncan, jason, mike], departments=[ Department(name='Business', employees=[jason, mike]), Department(name='Scientific', employees=[dave, martin, duncan]), ], ), ) # Run the demo (if invoked from the command line): if __name__ == '__main__': demo.configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/examples/demo/Advanced/index.rst0000644000175100001730000000007200000000000022274 0ustar00runnerdocker00000000000000These examples demonstrate advanced features of TraitsUI. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/examples/demo/Advanced/test_fixed.h50000644000175100001730000411322000000000000023033 0ustar00runnerdocker00000000000000HDF  ` TREEHEAPXdataH TITLE (CLASSGROUP (VERSION1.0 8PYTABLES_FORMAT_VERSION2.1TREE XHEAPxh( TITLESNOD(H8P TITLE (CLASSGROUP (VERSION1.0 0 pandas_typeframe 0pandas_version0.15.2 0 encodingUTF-8 (errorsstrict 0 ndim  0axis0_varietyregularH H ^ (CLASSARRAY (VERSION2.4 TITLESNOD (H%8hHXX'int0int1int2int3int4floatobject_1_0object_1_1object_1_2object_1_3object_1_4object_1_5object_1_6object_1_7object_1_8object_1_9object_2_0object_2_1object_2_2object_2_3object_2_4object_2_5object_2_6  !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abc;M`5~"@?ΐk ?bJ⿮N,?PpXE?12`ʿ3OiC?Nܜ?[-;ο wԿgoÿ¨bV3?bLOUm?㫢Y5޿#%?i["k@ѿ8Iiׇ?0?E"?rS8p?˰+?1Á<¹?}Uݿ n`?)lQ ?+-)ȿ\vUaf5ۿ?J!6l?S( AۿmsSiϿp(]zά?9-ل?͑n?ՙTd?.\B?EuZvZ +#~3?{3Naӿ _Sȝ?-܈˳?b2 Tݿ5{?H|?poN:ҿJ?>??-sʡAu쿶p O3xaݿ'uӜ?fL?/}RGmͿGe([@ؿ쌗7T?S ?YT>ǿu?$ O?`J:}Qh濭AaYؿE[ݧg?s݈ ?M\? ʢ?/uIEM mٿ Dh=!u?k \ @?mB^?ݟl?bg0|?$T4/floatint0int1int2int3int4object_1_0object_1_1object_1_2object_1_3object_1_4object_1_5object_1_6object_1_7object_1_8object_1_9object_2_0object_2_1object_2_2object_2_3object_2_4object_2_5object_2_6 (FLAVORnumpy 8 transposed (kindstring (nameN.hP 0axis1_varietyregular dd@. ^ (CLASSARRAY (VERSION2.4 TITLE (FLAVORnumpy 8 transposed (kindinteger (nameN.PX 0 nblocks  (dd ?@4 4N ^ (CLASSARRAY (VERSION2.4 TITLE (FLAVORnumpy 8 transposed(%X 8block0_items_varietyregular n^ (CLASSARRAY (VERSION2.4 TITLE (FLAVORnumpy 8 transposed (kindstring (nameN. (dd ^ (CLASSARRAY (VERSION2.4$                                                     TITLE (FLAVORnumpy 8 transposedX 8block1_items_varietyregular s^ (CLASSARRAY (VERSION2.4 TITLE (FLAVORnumpy 8 transposed (kindstring (nameN.)X`p)^ (CLASSVLARRAY (VERSION1.4h(axis0axis1block0_valuesblock0_itemsblock1_valuesblock1_itemsblock2_valuesblock2_items8 0 PSEUDOATOMobjectTREEGCOL\\\numpy.core.multiarray _reconstructnumpyndarrayKCbR(KKdKhdtypeO8KKR(K|NNNJJK?tb](a1ed8f2d8b8a9850eef27646045f0dda921ef8cf2ae9ef79342b7834a8bd96c6fb17fb6d36cccce779936df6966dd268ef1d8a7e09c3ec21f0e40756e85ccd7cf14b16965a9984dc0b3e918aee13330ad3e01bf7c559a69d133413482de66deda9ce5673d9d30ca39d9f89773306b28929c80c283011680a41491e6294fcd75a32ae9c22319adb3d66c301fd20831a4006d86c9eb3205c71d43064215683c2c6a4f39b342c27562d953f9f8e14ea66ce52b329a0bf86f9f1b492d7c410e45bd1a398898d2df24bc1e6a0e78f76defbbd80d9688e04706b2800b6c13279efbad3ae 6fb804a87e444751c817e7d97c4ea6070f4f69ab1591e3c3724912d5981d5b0110173f85fe94277d9c5cdd3e25ca21a91cdcc31c6eb28adb960e7c7a98706d8f1508353d1efedd251fe12061f818e3c3140eecae11d480cfb4c4624984bb9bda88866cc27f0a96b9d9846a1cac7214a36566992482c9c1acb23f482fc50c8ed86c8c95791661a03a5dfaaa35fef621d7c3ddeeb10ff72442c69147c66eabcd7e52dff5f8b64eea29c536e0063d2d4f30d3abcde9f6ac034067179fa3389c7cfa440e2d0c764b0701c59788968fe835a8448b85ffd23efceed29d005ffc86a3b419098c788286a3d5a98d96ee0a6483544fe963039f9b3b579e12af26bb7e0522a78c4a2a7555ad88b7b416c89b4b8fc8a6c940ec0b 66266cac0c476ffb22e7295dd0eddc9f1ccf7fad2d7063d4718a15215a7feec5a632dacf0283647596e187261e0a5cc72246b2da886d01826edeb4a76bdd81d7d7739faf54310096a2e25becf8757997517e19db6d706a2d330257842994c1fb23c15a1aeb8c8689285ba4c920c9082dcc26ccced3971540bbe582f780f24bd49c41086d09dec6e6b1f07685910d653ce65b7c139667a6ecc0dd05c318db61e85a95d1d7563c856fabc851e24009b7b93c9a4862634431e8d6941887a47e51781ffad972c09cda284ff41168b22f215d8bfaec8d175ffda20043f45d670ddf099bf8cd63d247fcc9d845df0932eb441e3d2a9ecd0cacc6e2dbee6b69e053a1fdf6e2641fa794b8056f559733d23ff05a8e20d215872d93436279b0e2e296ada23a03c2894f4f95002b4dafe7213d2c410239e1b644f59352f78ede6df8bdbaa16c6f11ba397dbc6bb7c888fc7eb52dcffa97b8cbf026d734405ee51647377e11a27c2e6f402e49b999c8cb743e7ef2c79f1d8d986a8b73f758d38e986edb5fa3bf6ba7216cf445e82010d72bff6c94562ef328f6ddf49087d451ba1db5c2dd427ab42343563bb1e841df91642aa44ca49e71b6a3b91d94b8c23bff59cc9695874ad35da6912a9b78d29315a1251cc55a9237a0eaaee5fe701a3cfa0a7c5d884cb0be161835e02402c6124d40 6cb2a70628d877463fe196e350f22 57e002d6ab3810798afa2dd5902635e774ac0bab797d21417df68a69f9d3ab175a5ba15876523a0ed6e1d6254567844c34dbee8e89dc6e0df53bce8a76b41f93303f8844346984c9eeb0f8fb7073a649c2ebe0139 cf736e246d43d2ae833f18a16776c2d345ebb73c4bd3c6b4b4eb65e5b02a1ccbe28c360ee9d114c946f94e278316e995132b6074d7761ba9a613a5005464a6c83b5cee855acd45205f44c2ecd896d8c458273f102407a636176a9a3e6bf038ea85bf0b58c802a9ce2385ad52eae87048f48ebee4ef229e924bc0cd175249128ef21ee097db61b050ee9ca0bda04fa5fb13586fec2144149efd3054336a3376b08b9ba98016e580370ff2a863583d58f9ac127542d6deb0cc1d27d6d8c0bf1091cdeef37a0e7fb78498e5715bbd9bf1612c698449a0bacbdc676f195f864fe2293e10521f435dc94bff11fea4f8136f010aca573c66e203c3f658186c4e03f898594156be60a66dbf79423909632847828baae5c126859be0bdae54589c97fe7e4c2fc3ced6f9d52c846790af6f354f8b9b459abf5fdb217da79c2bfb2fe0ede1e9d442cfd12fb3bc2c43ae94b7e010b75586152c14f1878e0f9569700c3cf2903fd6b1e795c2559f19d944ffea7461c1441b190e973dcd8b7566b77c1a868ec1b87aad7c1bf836ce08cbe9cc735547d4af3879635c41db080c9538c55213645ff00ad777df58e620aa542c3414b83f7699c684b40990ac299a4eb969e9866e3d3521957db9f8d5bb22fe79b02215823fe865d41a9f1eede8cdbab6f14b0a705b2f0d4e2e72ced21199c1c2e7dc363f9f1c7ee1dd3436414725c843d9023fc18e54258d99e32e662ab8fbd7439b32cb1cad1dd2bcd86b568750dae024cc4ee55f1f80869a60dc256dedfa1d1d80899b39da5baa2c4cbc4746a7d63268bc39027ca8c566f5065209702f0730de3c51c73e77c29bda177840c798dc936abfdff2144bfc51174519886a85a4ea0153690c73452b4e1b245c98d6fcac6b0e558fcfe9352a4858c9b397add9b44ecd1ee4a52ba3bdb7f9beb37fdc371872afca731ca4638c27f80a70bdeb873ba909d91c5f9cd04c14438f43094e2bbc9630bb53ac7f5bd652bc110111958f6a01fc9043f8dc38dc2c9b1cb98d9c2baf227bf69b5c7e30c1dc6a4baf239b43df0187ea6d3be3d3cbf7efed34857acbdda54f8ca1106b1be8d2bed416cb5e79bad3b081d66ca71d5ad5c8ecea8be175de3a6ed1dc99bbf1736a7e34a40a3926dc382fc6b5ddb8dd5b58c1ad9ca2e1d9bfcf9e8e631ad58131bc20fd677137322512512341283904b4a1d9a17f2251460f7b375cb9157deb6be5140722a7117ad5de948f81a1d0f4281c8fc909ab7e8ded37c16a520f11df99c117b8f7e309aed70800f2c7421efe2d5fecccad059c1ae090942a9a652cc419424fd94e88e40d76d00671904b364144c150f02646873a00e94427ce9aa2f0fa22522f12b628ccd94d172970840d3eed2d2208a330864b7f30702804595ee4f3c9c2d70278abd7a73cb409bf2904e4c6537919ba1aa10d56a8e4652497aa982beb8741531e0d5b5a438b18eb06e9b24175345dcaf3765208d103aeb44e92729c0b5c78974fc96ea3e6a248fa443dddb06f725f5f989c099ad4dbb279d0f4693ee3df1a10a74a45818a52893757072720c9869cfc3cdb2a75209b622a1d397daa57c7e3d0a67b8c893cd97a9990d92d4a5077917122e66091b7042996bb8e031ca98b792d93b6b39f1e9ccd39134e5c15cfc0319d242d23f93b1 1da3c3f1f84bfb6e8fb50cbd715cbe8cca19a9c207d979d52176ca3b917cc0e9f84ab3870142d78bca095dcaaa7450b86bc1732b3867d7182f7c031a9dee276a95c3e392ea4d61749d8087e5ecd6fa11d9254654543b0a1a124075ac4dd2498e300a6d91f400bdaa1328302f2871dc46691e854d445d498d6c27 4f86a3c760ace08b7516ace67eedc6e48c7d01daea9f5407713d52a846c56f3feeec60399699b9c4792aeb49d8559026dd14b0c892f46e5daed2874c22a0effbb69ffdcc0111a55afd90a9fcf27a0fa1631f5bba5c0730d1674d819d167881d87045c40ded69b63153f3e6f304c2ffddc0050a62188728d52a61141f823b7ada4061911f08c87f9157271f025aaddf60b9ef5314a43a45e8ac403af932015299a0157ac07b15fd51370d929aeeebf23147408d60554943d1fca873b5b0fbd9e676ca9c2d940eb6f9797074bd726145c4e0969fc4fa4f12fd696524c1b93558cddd9ed6464a9e239f8a70993d33b86ef59e48f5b4e289e1ffb03d54d479d383579fd61f479eb93a00a3cb552dd4bfd0860951026b2d639a41002a88e61461085d893a11c39051001087ce446ec094f948e8821b7c77e9a3ff0512b20d81c335bd9e1fb9e2ae3c1cff498d00e9782fb7019ea239bbd23411220dbdbede6f6366dd383adb764b01d844b85613f6c390baea93fa9562871236e575dbb918e58c6d095387268444487f24fb143c8a41fe53fcd82028621562d23ce1dc901f74657ca6bf12dba3135b221ffd59fa00e83a61172dec9125314d44f1fe33357ff4c34e5c4f3a2d7e7c5022907ba95ae7e646ef4385d4d64b2942202238974818e84dfafffeb8afb1d0c565f64bf00f82d0509fef4d4c4c853c864a17d4e441225548c308b74f90bca05c9f09304eaff4bb0b9205e2cfa23c394c5f008bbd0822d4a058e2ed02a6a6052409f79af02b7ae045f968ae9022efc8c618adacb7d4a8035fff951ab0eed60ee8e44fee2d6044864bb59a73ccd7ee6d91d290ae6c d54865080c122b3755ff68777c4ead0930dd684e1c8765e3cf60215eae0b5bed21687404b4d88c240b2f5e1ee13fc0965e4f53af5f5c1431d36e2967b1f88698780376c16115a64a159e584975d661ba0f904cf564a8547d1ce3dac195cc9bb6d84a9f53b8a21c0974c56f 94a3eedd211d0687baa6883323fd537ab0db15767b3ee06a7076a7f6759f47887abcea580a24466edb5f794d925ebc5e242826815a52ebf856d7425d91e0db82ca0635e06e20e44bdf1c05100635e8453f0c377b4e90f4ddc56dea3ed55d4bce9caccdfa4683430477dfc436be0486d02367ca4573fd5013f23f5c3ac6474bcac23ffc476f3f871a5610b57312d0ec7f63695650461566f41f21ca550788c62c3e74faf62e3911bb68b3771ea29f3ed0fea816bc67fff4772e8ac9420fee4142c200204ad32cfc8fd5539de9c52465586b3b681175489f095ad42560a7aea16bbc8245c056bfb65db101b0c480c4abedbab11a7f3b7e27200642fdc2cf84d5644fcbe8b65fe2d0595071f8b3f8b7f909a8083e8a01ffd9474c6d26efe5b955f097363b8fbb626e5116db4445cd55ceb206a7d2d3d68104f85bf4e0c68dddace5c7255b45c5a2ad2ac91b82440ebbbf8c38c545e0432f3fdf346c9e731f300a624bbf98daedb05e4828ddf8f3649f0c64eb25d9ea7500a581f14fe47fcbd8531a7 1230d8580aedce8073359e88f449a25a37071edef0b812d6634350aa1c764c53dfb7f79675ffa18e078378040c694308a2329ea3c5f8379c22cc366c6370368459543ce6bffc0cb25ab5553d475762da0c5fa0e32f2e3f1022796212ecf54fb051a1cf1d729e50bc009bf8ad83b45bbf0900539390e60bd7a4b7d455abef23e8d4cddb77a2ed815814ec9306f09502b6f7738461b23f40f5b492721db68bd70a5456491119a7dd6a992 69fbdd759568a689e8998c0aed3ccdc03174e0d399cbb8cbcae6818d511b445d90e5115876a076b39f73c34fd865568589bd6078334bc582ec22487ccb00f2e71fc5833c5ed9e9ff6ed695aad271f3729ff79b158a28c7dbe2a476ef 4e387062b4493132ff7f4042a25582577c8e2f9d70a6d6cbb85172b7a4fc360268c2f2a0452ab1d67c33f492b90eb392b3788cad6b01048c5344e39d8ff9b921de1f2c9755480521190665eeee d7170c15dc3fa923d84085f9ea52e8064d33f9a73aa93ed7c031fcbebc35f5f21d8c47ea86acf49363cbaa3c1477a3f859d2191767ac715cc8818b89454867eb01020ab92b1b180134d4476508bb2d8915eb5069688976cbd9cbe3553a71189f3c0694e fb8a85024271ddd91327795e54efcbe876f95de3bb801715b466940dbae4bd898c15930caedf4bb772c252e928d1ea62448209c362dac69a79fb139c0d32107b8e38a5ff28cc0332dcaafc38ad4adf9505475399ec48f781425954b994460ec04c477c0199c4834d5bc20d2ba4fa3fdd560f61d7bbda76ebd716769ab175cf864383d84fa67eca411d4833ff579c771ff13788a1b6df196a16ec1f0d343232191c2ceee50d99458fafd9084ef9012a0f6bfe849d66e14e54dfe9843980894dedf87a36f4f4ddf043871598af3284c2 d8b17d40f7b8b2416be25d268d8e6be12db13302bfdcecf5251cefa9aceab5418b74f5276d27442472eb7862305fed82c8d057b16cd83c4c1f60958bb7d9d623d01c72ee10543f6ef4796cdd24b3ceb0e5a1230f96b999434c218019897aa215512cda7f2b977fd82309226b58679c78d92f77160d0d0645d8bbbae28a6d7ba85b0909432cde4a446c6d180c5a3b4c7778fc2c4a105a42ce7e8675bf9755b0eb99d192d1f78767eeb84a7dd159d118b86a632bcecbf2ebe21c29f71a8be2eb883df12614eb5b22dfbe2062b68aca028f355f6652ffaa9a1d8ac38ddf31ca671071fe6cd824d2925d2cacd5ed10f776baff97f81fcc6af728e3230db56976593504ef350bcf1f7711d29f4ae074f2e0750628c983f2776ba85d7fccd0590a7b75d9e56a14185ee7e86e101803edb09d12d1806a835d2940a223fbec7905bdf914cda92ed419210f7ab288ab464b8b5b00e37d69931542948e92d820f7154c12a7e50deea456577139f6f249fa5f28d90fe9f8e98475ce22e94527f6acd4d8237026b88d204abc448c6601652dcf0daa48ee87b7bf963a8ac513497d71a30f697450b0fd32a2e6998d3ea79dbef36cacfe 42f0fdfb014850bce9b453d27bad2 6381407628d75cd7b0bd5bd2578a938e3444ce469752a8b0fca4faaa0b6af0d5a6c3dcfafc0946ded5192e7ae4adefd78a52efca580273d6b5c65ae1a0cceb7d3d0cd60459a0122719bf609663c5534a352bbbc691843799bcdd9d769a6dbe4ffed03f191a19af0e06db264a39a628a02df4cd91470ca146e3d489b1872f2f4a6994ee442cd545f8eb24fe88966ca677a4f4771eddbfaeba32e67e756033c129b43cd6af1e29e831a094d68bdd57ee000c802ce2a84a49df69d6b590976aa2767ffc8a67d01e573dfc2345e94d0818e75568a25b39b1fa6b84ba282e9905bde5af41a98c5f13d1e5b3730ad0c038e4fbc3a0c4bad5b30bf76383c83c6683379fdb9d7c1ffdda8d04edd04295899a42656333ae82e8c7307cf28fa0e682e8765446665b155d007b629cb084249f1fb3bfefdae240c8df2930b3560a4b5248ab67e87d6120383e1197af6d59a08d3e9c22f0b0b600722923ae467f20bab3215cf6c79e98f64d1753758e37fa2d12bb7a8f38cbd7d7147ebce5c12875e0d1cb9db2c14b57a82f6017366ee3acb9703d3eb58ff7c767764eea82d28cf0eefea60fb02a6bebdeef3a9b1156e2ae10db1e4bdde66b7644df7fd7eb4c8856fc8076d4651da726b3f59bcd2d1d19e0dd5a73bb146621f3b0faed37b6dcfc7d891c72c918568d9274face3c25eeb2f69ea5e348d7986f3a91e8ddf07b52898379dfdeba664565c937d9d9e4c4d736374c0c20b5c89ec2400e9e508a07fd6ffe9f9c938d12c907f5dce392ce611a7dec9a0c4cf9ab41171bbe67ac041c35b8b7943af6ac523849af42bf7c85b75d8f76eb3edec03e43f5b5348d2be37b19d6cc1780e385fdc7aa1033d32d09cb88fbcbd65471f0938e26f3bd4bf4ade5e64e784484e04c810b5d72be3005e4d13e81abf82f5e953a2fe28e1082924ce5ebd88c9a43e2b6d499c476cf42df109efed4bd9982eadd4aca404664e6cca53007bb2b1f58a3449d8b40b9b94a7b9d66e5725b3347952af8d5fbeff3de123092ff904682 2a7b983b22599085ee7bdbcf1ba882b46a5b11120c306b616e2ceab9ab5f80d98de070726d56395ea73db4915df1531b9b0846832613f34e683a76b99dd681f172689d8eebc799455cf0bffa72832deee3e7ced72834c91a3c6902327afbd63446aaed4c5b8c586f3e56bdafb3630d55972b6e(6baa3090d3fe60b79a67b53851730cd8fc345ff9b570da9deda2270c1e93cf30d7045ada9bdca427e6d8df28d7607b3ed655f872425256cd8deee210ea98b752631c8a3b94068c777c38f34dd078fff0cfa02ce01e9a4dfa0b8f008597ba305ad665e425b281d0c5a55c3377f17b12eca69c59ee3032b49b0d626089f3515f6acf39acc88d48ca51b9acd649eac9fcbd48d3069243a78d5ca596d0b6f38e2e8bf4f6fdb91729e5f4e0185ae0be2815ca49efd25fe1691dec446a34e726d5a6c9cde7b7bffa157aa2 601b0c6e55b65ec62327b321c5d0376543ecf394338ec37df945f2dc842947aed16659fd9177df4bd83d5f64b485c1b532159f5fbc582baea9e5f0c29a6d200026f725f101efce5ef79ca8ac5b10157fdff8ea6ab3e6abfba502b708 a9b6ef6465873556708507b32b9e729b818a264d1472f81fba98e9de32820b9ab4ad87440f4dd85c56964183be684288b86a3b408fc26a92dae9fe8df3015a15dfe4cfab9921ffc1af9d96ab96dba727301810b1f7ec530799857816263e0936c171b94f73be8b52a4e375645860576c27755301faf7e9a9e671ecae8055c8f352ddc0c3bec368b4164d55a38c6505f503931c4744daaec7f511685af75d15d408fded36d03b4a9cd12fc701da9b855c24 922db02ac2c24e6ade73fc1f383d64a5504330424cbe1a6dddba938e24ce954177ef173c78e67e9abde250bf8c00c0eb6b63a2cad400538fdf14b75c105297fa83c51dc304a9ba83a0be6d510389498ba95b2ae3387a820b030ee7a93790a997283e46f3e8e7c24e6dfd27377c1487920e2dadc8af9641c5ebe25eb24d2fd42fe51e2b7057b136e1a438382508d73efbf5cbab68be1fc407eed78725dd8cca1593e8c29f05597c59b67d3e04238054070bc491d9a083c535f909f00253a47fe0fbce0d1e721a9a4a475061abe6414799a09f5f21463943cd02459be501c6d4262f1d7cb1a520920d3747e6ffac0143246281a650fef1acab737d8402aa2bd32f9863817ab98140561d50e92f51bbcbdd38649d4fa24846eca5ffd9961f1eb3a8263b832f4477b05465e5b5d001541a2a9c5a723f93f692ab65ef10e37367b8a1f96810d98b90f960e3485b3f3ed3a77a2ee4917a54486ee4f5419211b22ff5a8aeaa2b30469d0cb88a9b2d9b909533d855b661a83752db44afb706414d7e337a178fa8a4227440f0775e769a25a0aebd67701c4319de2e9d4c7e1f28d3f232795fc911ef900cc51381a27e551e6621687b14316c3ff715ac9bd9f170ef7b952ac2397920ab85994d84691ccdc3af cbd8b850ff1d53daf346abcee48ed3e1a12c6c53f3b6d46abc4cbe6cbea9ba3e2945fe16f1d24cc390a91aed7ffdda54a2efc58d6a5673651b70b806c6977314bd689c3415 5c9005cc15538a909cce14470c4b35864355584f5cdcc638a43a8fc4d35d0add6cbe1895d6201991cad9870eab0fc041a29119b6b7cf0971e728518c809c667f3bb0038a9d756feebc5c0e7130da74537aa55de809ec067d20c8cc059b6b6cdaea87916d0a7091ba1f190de4e9b3d0cb24de4b182e77429ee12f8b9cb067f572eb11bcb2b08479b9d8b915950925db89aca66a7ffa0b7eb61b8c890b995e126622d7f768d0e92ed2f607d1dfba9390fd222b9ab664ab6bf84b295aec70257b96 e07af91ff296de4bf36f0bb276829e558e2ea72e33691ac967beaeba41ae2d5b5e3fe8c111ee87b51a953adc74269d5720161d68b49caf650224771db3273bc412328908586a808c621f3a96c5c0cf5f0300b53f719b19e79f93aff5edbfff8f85b6301 5e8863f3540b96a060721bc6eb0ecad3ce116bcc7f862eabc8ecf2ac819ccc7d7bf0efc0d805ec3c7c6fd4a4dbb7d592e11bf302aa395b84359c0c1374a3f831fbbcb824d4d63c3d246e657fa28dcd1e02d8c5b2267a96c08586e347bf9d12c6925c90e803241953ae386e7a675f4e22f7aa5ae037760552848d31353ac3523446c24f468ed3265cf010211908628ce036c044eca823a5b903986809ea01a3e3c077614fa39a928a8d8bdad37cf02d43ce76bd74eb547e46362af55c970275b643e9ab24fe39cc7 69a090cf8bd3702f0a7583ab909d4c5c809459a43ca2992fc5fe3660d25f8e9275ea2619ebf66a8ad874b755826bc1b6434e34538de739f4f785f3183fe740cdd2646b4f75a3c265ea7f59a60c6aba4139a1b36a4 d840d34c4276916067e5151c49a4d43b29aceae0e52fcddfe418a0cdde112e7c4776c5f2f3b7c1d74f811e018fc59cdf96a73013d46f65ef3f72991991d94d66e4c99a0ab2467d27a9108d9564283efb2e37746515da52afc377fe66590d82b8a89d02f9e367684cceebd60f53de14fc05524f6aac61a4b006245c31d5da9c0cb73d6629c211139dfe5451b59f9fcf862d6d3ee3d420fbc9362f06f418ff547a07d51a986090d7db11ab217a315782069d999ea6f1bae60cc37948307c3111625ce9c7dd15b1c9d092142eb4f44250eca9c4c007ba8845f811fa1c2d1c6b42d306c170a44a07ddc9e0e9f3f784a47e5 4a138ec7d6ddfdc22d07bdc40717698347edacccbf7d12b67b0aecf1ea556ae3b5b11abaa73ae4e7766e325cfcfec41d6db69cffa8e338b4b0b87529994d82c5ed8a42a70562a942e0f4c901d43cf026b54f29052cac5c36635c90abf6c373727f29b7a33a834b67807eb83a599bcf8af7ab0b4da447465e3cecffeb35d90871a2d29de36ee445a916b6c96200f76134bfc550af9ee621a893e6a485ffdce04ddb5e993f0053d1c17559dd2620d5ae5118535859c2aa768277b88b9b5d03e8407e8058def1f0d7532ab196b127e6eb559dac252a22202cd52eda570eff06375f0865beb95c5 cc21d8854c090ef6ec75fee0b14c3d633d35b1d3c3a71db3fd9df436515bfef5db7d35822f03b12590d17251d8a479500d1bb098b5576cbbd2203d1386a299f415521ce8c321ba905d19ec94c3ae49678ddd51da693502cc90242cc27ed4a33ce276f921adf91ecc19476c22631a0b23dcbffe5d0bf006c653248c1081c79d949aad51e0df38d11b44982867213196c02279691f5d021bba1a18ce3269562d1fe1aab6b2568fc80e5a35c4256e1b90e45c928883350853ae0892236a2d083d68cdc4dc5c043395d551d4ad49fb9a804f56d44e3a621f3dcad1ed73be1855f52239314f1af987e135151fc5d13fa258e772052592e25af7c5515f9c55667f1a2087019abcf445dc1a55753132b98908c744d74b4081 3bcdbced9a109b103b32995377f790d8178f1400bf6535ee6cc4908170c1347b5de086df154e8e05ed8448b9de6c2080c0d187e5d1db634508c778e613917ccf4ef9c7aa73dca77cfbb3c95a27caa350b25d7c9cf30096c1f2057fba5a39ed1c1ce8b6a463f2f8336d38e9dd297d87e3499b0003c8c6039ec638027219862e49e7cf73ed536aacb472ee8e4b3effaa5d4c2aa5f4f4481e031c036aab95a0786874b8566b77331010bddc57eebef603bce2664718836012788a788902910a013b3e54629fbdd9586959ec95e6fe33a2700fea56a36c57f618a990d213fd242cce90c1520f3bdc2f5a9748d05df631059f28b7821c8c386b38235cc92e1af3c9e17b682fa44459 84e43bb2cd279464e3702d3350589fb91be1e29b04e668d00334e2803ab63964afd1f750d82cade861e09275fc1ec0278357aa482a2730cfd923ba5066f7646df091591c04319eb978e2cbe813ff42228f645b3d44a2b08e95b0669b41e49fc80927651b82467b3015623c25aaf4ed93805561cd5efb42294307e9e05ac996a3899ac4f3d10405aec4d60b9f5384ab589951f954c5494c4d6aa7bb4d41a6d208532d14707a98df31a9f59880c0df14d6352d583ffce3cfc4d634d2a7e13e62222aa0d3744c187b34908aa37eb0d9ec4c92160657d7cb7f1172a598d3fe096998c9e7fcbb65db69179923fe5177f50d7e098de00ad88332 f16ec32a9d2751eff69ea90859dc4a8efdbc25c7ce8bd8e1b9b71d3eacc4964d689e0f78c8b176c3ce70c1886ceaa3122e101a248cbc780bb361c0f5b9e403d5c0ff5985c6d359e85bf8d12b29600bfe99a1ced37d2a65df540b8f54 46325b2cefdbfb b5677b7883d487988d730de322833ef4e003e5f70f2a df3a92793773a197e4f76e6cdf0f8e982fdef9d35b51fcd9c627123684e74cc014bd7053dfc61f6d245b41f140ea5c21b9bc47e1bb83244a938df770095ce5ee155943a53378e6bc00974e75034df42cdfd71a9958b40e55c2757d9aac67889706886432d39200e50c4edd b4c2063459962645dc278cd5a503328cb022af9e7f319c9e522bd53e4e34359c31f9b8c51e2f35b6dffcb4bd01c786d1d8f407ba2e737eceff6dfcb9d5a7276c9bcbed19d7d2a32bbb64ce16894861afcb9fed531a5b14d7a3c60354f3df7079f2ebf5778c135b4ce2a2706cb6ba32a941f1e03a0aa2e09b45340f5c0518236c408550f3c43ab5e6aca33ad1495eeccfcd624eb51b1d8c16b6a9ecd14f5411a7a25ed1a53b5f2e31a7a83b9b483b774d4a79cd627548da05b8c6e8b33f3dbef112dc84e0267406908dd8fa24ca16cf21af0ccf6de8452868d24a011b167382f6bc673b200e1d440c96a4bc87f1dec9eea3b83da5eb66ca7d191af8ea25c3bb7e60df5b8241b91625ff117862aeec7a891b8a566878b67f8ff348b40075667f64f10d144721188cefd5a439d18ba1c9194a8c52762c1a3856888c068bc49e2fa70dcbd008dc8cb6736b5c3921716e58383424de843caea6b9ff67956500e264ffaa87bdbe2c8c451810b5407f89c3eedf8069fab0c7262bf79c149eed7a13ced9031fbd7a9047966028de0673e3a0bafcff75ec37bb28bd97a4e914d8a1af4500f2833630ef9404dbe5aaec1d282384a36ed6fef0dd1e4edeeb7ff0e383cbfb3437ace0b662dad72d5dd960d9a2d72f56b104c0accfb65db964d5fe58ccffff28844524902281d29a971a43493d5bbbe4acf9583f1b05d9d502289514b4666f8b997071ca2aa24eacb851b5788855b5224c678f35254a38f6bf561592929ebe0c0f911cca2467ffbbba83ad6e1fdf24d231ffac1917c5d9688d246fdf2b31fb0d45e315e5d9f7f385071803435439f15e619b684c25ce3b2e6acd95f461d8f965c7a2786a6656d0d81ee00623d52854d9c3f5bef5e994c19c77c05bb5db8247021cd26fcc9edb24 5184be02e37a2aad383931106c37f617cb55af97fb3619e2155a4dfa89d0a7980ad14a5705fb9b128e890c75a0ea70c12506113d9c1e881ccc38455a21e99e0b8c2e84bc65ad87f3456633bc551117db926bddf40e4ff034c368dc3dd4f08db89986c49etb. 8 transposed(H 8block2_items_varietyregular  ^ (CLASSARRAY (VERSION2.4 TITLE (FLAVORnumpy 8 transposed (kindstring (nameN.\1././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/examples/demo/Advanced/test_fixed_compressed.h50000644000175100001730000414767100000000000025300 0ustar00runnerdocker00000000000000HDF  ` TREEHEAPXdataH TITLE (CLASSGROUP (VERSION1.0 8PYTABLES_FORMAT_VERSION2.1TREE XHEAPx`N TITLESNOD(HP TITLE (CLASSGROUP (VERSION1.0 0 pandas_typeframe 0pandas_version0.15.2 0 encodingUTF-8 (errorsstrict 0 ndim  0axis0_varietyregular  0}blosc   ^ (CLASSCARRAYH XSNOD (,` H C86h@XPM (VERSION1.1 TITLETREE p  8 transposed (kindstring (nameN. P 0axis1_varietyregular 8d X 0}blosc ( ^ (CLASSCARRAY (VERSION1.1 TITLETREEz @ 8 transposed (kindinteger (nameN.X,X 0 nblocks  P(d +` 0}blosc ! ^ (CLASSCARRAY (VERSION1.1 TITLETREEc  ?@4 4 8 transposedBX 8block0_items_varietyregular0 6 0}blosc -33^ (CLASSCARRAY (VERSION1.1 TITLETREEY3333 8 transposed (kindstring (nameN. H(d pBX 0}blosc 88 ^ (CLASSCARRAY (VERSION1.1 TITLETREEAF  8 transposedX 8block1_items_varietyregular0L 0}blosc `D@^ (CLASSCARRAY (VERSION1.1 TITLETREE\@@ 8 transposed (kindstring (nameN.OX`hO^ (CLASSVLARRAY (VERSION1.4h(axis0axis1block0_valuesblock0_itemsblock1_valuesblock1_itemsblock2_valuesblock2_items8 0 PSEUDOATOMobjectTREEGCOL\\\numpy.core.multiarray _reconstructnumpyndarrayKCbR(KKdKhdtypeO8KKR(K|NNNJJK?tb](a1ed8f2d8b8a9850eef27646045f0dda921ef8cf2ae9ef79342b7834a8bd96c6fb17fb6d36cccce779936df6966dd268ef1d8a7e09c3ec21f0e40756e85ccd7cf14b16965a9984dc0b3e918aee13330ad3e01bf7c559a69d133413482de66deda9ce5673d9d30ca39d9f89773306b28929c80c283011680a41491e6294fcd75a32ae9c22319adb3d66c301fd20831a4006d86c9eb3205c71d43064215683c2c6a4f39b342c27562d953f9f8e14ea66ce52b329a0bf86f9f1b492d7c410e45bd1a398898d2df24bc1e6a0e78f76defbbd80d9688e04706b2800b6c13279efbad3ae 6fb804a87e444751c817e7d97c4ea6070f4f69ab1591e3c3724912d5981d5b0110173f85fe94277d9c5cdd3e25ca21a91cdcc31c6eb28adb960e7c7a98706d8f1508353d1efedd251fe12061f818e3c3140eecae11d480cfb4c4624984bb9bda88866cc27f0a96b9d9846a1cac7214a36566992482c9c1acb23f482fc50c8ed86c8c95791661a03a5dfaaa35fef621d7c3ddeeb10ff72442c69147c66eabcd7e52dff5f8b64eea29c536e0063d2d4f30d3abcde9f6ac034067179fa3389c7cfa440e2d0c764b0701c59788968fe835a8448b85ffd23efceed29d005ffc86a3b419098c788286a3d5a98d96ee0a6483544fe963039f9b3b579e12af26bb7e0522a78c4a2a7555ad88b7b416c89b4b8fc8a6c940ec0b 66266cac0c476ffb22e7295dd0eddc9f1ccf7fad2d7063d4718a15215a7feec5a632dacf0283647596e187261e0a5cc72246b2da886d01826edeb4a76bdd81d7d7739faf54310096a2e25becf8757997517e19db6d706a2d330257842994c1fb23c15a1aeb8c8689285ba4c920c9082dcc26ccced3971540bbe582f780f24bd49c41086d09dec6e6b1f07685910d653ce65b7c139667a6ecc0dd05c318db61e85a95d1d7563c856fabc851e24009b7b93c9a4862634431e8d6941887a47e51781ffad972c09cda284ff41168b22f215d8bfaec8d175ffda20043f45d670ddf099bf8cd63d247fcc9d845df0932eb441e3d2a9ecd0cacc6e2dbee6b69e053a1fdf6e2641fa794b8056f559733d23ff05a8e20d215872d93436279b0e2e296ada23a03c2894f4f95002b4dafe7213d2c410239e1b644f59352f78ede6df8bdbaa16c6f11ba397dbc6bb7c888fc7eb52dcffa97b8cbf026d734405ee51647377e11a27c2e6f402e49b999c8cb743e7ef2c79f1d8d986a8b73f758d38e986edb5fa3bf6ba7216cf445e82010d72bff6c94562ef328f6ddf49087d451ba1db5c2dd427ab42343563bb1e841df91642aa44ca49e71b6a3b91d94b8c23bff59cc9695874ad35da6912a9b78d29315a1251cc55a9237a0eaaee5fe701a3cfa0a7c5d884cb0be161835e02402c6124d40 6cb2a70628d877463fe196e350f22 57e002d6ab3810798afa2dd5902635e774ac0bab797d21417df68a69f9d3ab175a5ba15876523a0ed6e1d6254567844c34dbee8e89dc6e0df53bce8a76b41f93303f8844346984c9eeb0f8fb7073a649c2ebe0139 cf736e246d43d2ae833f18a16776c2d345ebb73c4bd3c6b4b4eb65e5b02a1ccbe28c360ee9d114c946f94e278316e995132b6074d7761ba9a613a5005464a6c83b5cee855acd45205f44c2ecd896d8c458273f102407a636176a9a3e6bf038ea85bf0b58c802a9ce2385ad52eae87048f48ebee4ef229e924bc0cd175249128ef21ee097db61b050ee9ca0bda04fa5fb13586fec2144149efd3054336a3376b08b9ba98016e580370ff2a863583d58f9ac127542d6deb0cc1d27d6d8c0bf1091cdeef37a0e7fb78498e5715bbd9bf1612c698449a0bacbdc676f195f864fe2293e10521f435dc94bff11fea4f8136f010aca573c66e203c3f658186c4e03f898594156be60a66dbf79423909632847828baae5c126859be0bdae54589c97fe7e4c2fc3ced6f9d52c846790af6f354f8b9b459abf5fdb217da79c2bfb2fe0ede1e9d442cfd12fb3bc2c43ae94b7e010b75586152c14f1878e0f9569700c3cf2903fd6b1e795c2559f19d944ffea7461c1441b190e973dcd8b7566b77c1a868ec1b87aad7c1bf836ce08cbe9cc735547d4af3879635c41db080c9538c55213645ff00ad777df58e620aa542c3414b83f7699c684b40990ac299a4eb969e9866e3d3521957db9f8d5bb22fe79b02215823fe865d41a9f1eede8cdbab6f14b0a705b2f0d4e2e72ced21199c1c2e7dc363f9f1c7ee1dd3436414725c843d9023fc18e54258d99e32e662ab8fbd7439b32cb1cad1dd2bcd86b568750dae024cc4ee55f1f80869a60dc256dedfa1d1d80899b39da5baa2c4cbc4746a7d63268bc39027ca8c566f5065209702f0730de3c51c73e77c29bda177840c798dc936abfdff2144bfc51174519886a85a4ea0153690c73452b4e1b245c98d6fcac6b0e558fcfe9352a4858c9b397add9b44ecd1ee4a52ba3bdb7f9beb37fdc371872afca731ca4638c27f80a70bdeb873ba909d91c5f9cd04c14438f43094e2bbc9630bb53ac7f5bd652bc110111958f6a01fc9043f8dc38dc2c9b1cb98d9c2baf227bf69b5c7e30c1dc6a4baf239b43df0187ea6d3be3d3cbf7efed34857acbdda54f8ca1106b1be8d2bed416cb5e79bad3b081d66ca71d5ad5c8ecea8be175de3a6ed1dc99bbf1736a7e34a40a3926dc382fc6b5ddb8dd5b58c1ad9ca2e1d9bfcf9e8e631ad58131bc20fd677137322512512341283904b4a1d9a17f2251460f7b375cb9157deb6be5140722a7117ad5de948f81a1d0f4281c8fc909ab7e8ded37c16a520f11df99c117b8f7e309aed70800f2c7421efe2d5fecccad059c1ae090942a9a652cc419424fd94e88e40d76d00671904b364144c150f02646873a00e94427ce9aa2f0fa22522f12b628ccd94d172970840d3eed2d2208a330864b7f30702804595ee4f3c9c2d70278abd7a73cb409bf2904e4c6537919ba1aa10d56a8e4652497aa982beb8741531e0d5b5a438b18eb06e9b24175345dcaf3765208d103aeb44e92729c0b5c78974fc96ea3e6a248fa443dddb06f725f5f989c099ad4dbb279d0f4693ee3df1a10a74a45818a52893757072720c9869cfc3cdb2a75209b622a1d397daa57c7e3d0a67b8c893cd97a9990d92d4a5077917122e66091b7042996bb8e031ca98b792d93b6b39f1e9ccd39134e5c15cfc0319d242d23f93b1 1da3c3f1f84bfb6e8fb50cbd715cbe8cca19a9c207d979d52176ca3b917cc0e9f84ab3870142d78bca095dcaaa7450b86bc1732b3867d7182f7c031a9dee276a95c3e392ea4d61749d8087e5ecd6fa11d9254654543b0a1a124075ac4dd2498e300a6d91f400bdaa1328302f2871dc46691e854d445d498d6c27 4f86a3c760ace08b7516ace67eedc6e48c7d01daea9f5407713d52a846c56f3feeec60399699b9c4792aeb49d8559026dd14b0c892f46e5daed2874c22a0effbb69ffdcc0111a55afd90a9fcf27a0fa1631f5bba5c0730d1674d819d167881d87045c40ded69b63153f3e6f304c2ffddc0050a62188728d52a61141f823b7ada4061911f08c87f9157271f025aaddf60b9ef5314a43a45e8ac403af932015299a0157ac07b15fd51370d929aeeebf23147408d60554943d1fca873b5b0fbd9e676ca9c2d940eb6f9797074bd726145c4e0969fc4fa4f12fd696524c1b93558cddd9ed6464a9e239f8a70993d33b86ef59e48f5b4e289e1ffb03d54d479d383579fd61f479eb93a00a3cb552dd4bfd0860951026b2d639a41002a88e61461085d893a11c39051001087ce446ec094f948e8821b7c77e9a3ff0512b20d81c335bd9e1fb9e2ae3c1cff498d00e9782fb7019ea239bbd23411220dbdbede6f6366dd383adb764b01d844b85613f6c390baea93fa9562871236e575dbb918e58c6d095387268444487f24fb143c8a41fe53fcd82028621562d23ce1dc901f74657ca6bf12dba3135b221ffd59fa00e83a61172dec9125314d44f1fe33357ff4c34e5c4f3a2d7e7c5022907ba95ae7e646ef4385d4d64b2942202238974818e84dfafffeb8afb1d0c565f64bf00f82d0509fef4d4c4c853c864a17d4e441225548c308b74f90bca05c9f09304eaff4bb0b9205e2cfa23c394c5f008bbd0822d4a058e2ed02a6a6052409f79af02b7ae045f968ae9022efc8c618adacb7d4a8035fff951ab0eed60ee8e44fee2d6044864bb59a73ccd7ee6d91d290ae6c d54865080c122b3755ff68777c4ead0930dd684e1c8765e3cf60215eae0b5bed21687404b4d88c240b2f5e1ee13fc0965e4f53af5f5c1431d36e2967b1f88698780376c16115a64a159e584975d661ba0f904cf564a8547d1ce3dac195cc9bb6d84a9f53b8a21c0974c56f 94a3eedd211d0687baa6883323fd537ab0db15767b3ee06a7076a7f6759f47887abcea580a24466edb5f794d925ebc5e242826815a52ebf856d7425d91e0db82ca0635e06e20e44bdf1c05100635e8453f0c377b4e90f4ddc56dea3ed55d4bce9caccdfa4683430477dfc436be0486d02367ca4573fd5013f23f5c3ac6474bcac23ffc476f3f871a5610b57312d0ec7f63695650461566f41f21ca550788c62c3e74faf62e3911bb68b3771ea29f3ed0fea816bc67fff4772e8ac9420fee4142c200204ad32cfc8fd5539de9c52465586b3b681175489f095ad42560a7aea16bbc8245c056bfb65db101b0c480c4abedbab11a7f3b7e27200642fdc2cf84d5644fcbe8b65fe2d0595071f8b3f8b7f909a8083e8a01ffd9474c6d26efe5b955f097363b8fbb626e5116db4445cd55ceb206a7d2d3d68104f85bf4e0c68dddace5c7255b45c5a2ad2ac91b82440ebbbf8c38c545e0432f3fdf346c9e731f300a624bbf98daedb05e4828ddf8f3649f0c64eb25d9ea7500a581f14fe47fcbd8531a7 1230d8580aedce8073359e88f449a25a37071edef0b812d6634350aa1c764c53dfb7f79675ffa18e078378040c694308a2329ea3c5f8379c22cc366c6370368459543ce6bffc0cb25ab5553d475762da0c5fa0e32f2e3f1022796212ecf54fb051a1cf1d729e50bc009bf8ad83b45bbf0900539390e60bd7a4b7d455abef23e8d4cddb77a2ed815814ec9306f09502b6f7738461b23f40f5b492721db68bd70a5456491119a7dd6a992 69fbdd759568a689e8998c0aed3ccdc03174e0d399cbb8cbcae6818d511b445d90e5115876a076b39f73c34fd865568589bd6078334bc582ec22487ccb00f2e71fc5833c5ed9e9ff6ed695aad271f3729ff79b158a28c7dbe2a476ef 4e387062b4493132ff7f4042a25582577c8e2f9d70a6d6cbb85172b7a4fc360268c2f2a0452ab1d67c33f492b90eb392b3788cad6b01048c5344e39d8ff9b921de1f2c9755480521190665eeee d7170c15dc3fa923d84085f9ea52e8064d33f9a73aa93ed7c031fcbebc35f5f21d8c47ea86acf49363cbaa3c1477a3f859d2191767ac715cc8818b89454867eb01020ab92b1b180134d4476508bb2d8915eb5069688976cbd9cbe3553a71189f3c0694e fb8a85024271ddd91327795e54efcbe876f95de3bb801715b466940dbae4bd898c15930caedf4bb772c252e928d1ea62448209c362dac69a79fb139c0d32107b8e38a5ff28cc0332dcaafc38ad4adf9505475399ec48f781425954b994460ec04c477c0199c4834d5bc20d2ba4fa3fdd560f61d7bbda76ebd716769ab175cf864383d84fa67eca411d4833ff579c771ff13788a1b6df196a16ec1f0d343232191c2ceee50d99458fafd9084ef9012a0f6bfe849d66e14e54dfe9843980894dedf87a36f4f4ddf043871598af3284c2 d8b17d40f7b8b2416be25d268d8e6be12db13302bfdcecf5251cefa9aceab5418b74f5276d27442472eb7862305fed82c8d057b16cd83c4c1f60958bb7d9d623d01c72ee10543f6ef4796cdd24b3ceb0e5a1230f96b999434c218019897aa215512cda7f2b977fd82309226b58679c78d92f77160d0d0645d8bbbae28a6d7ba85b0909432cde4a446c6d180c5a3b4c7778fc2c4a105a42ce7e8675bf9755b0eb99d192d1f78767eeb84a7dd159d118b86a632bcecbf2ebe21c29f71a8be2eb883df12614eb5b22dfbe2062b68aca028f355f6652ffaa9a1d8ac38ddf31ca671071fe6cd824d2925d2cacd5ed10f776baff97f81fcc6af728e3230db56976593504ef350bcf1f7711d29f4ae074f2e0750628c983f2776ba85d7fccd0590a7b75d9e56a14185ee7e86e101803edb09d12d1806a835d2940a223fbec7905bdf914cda92ed419210f7ab288ab464b8b5b00e37d69931542948e92d820f7154c12a7e50deea456577139f6f249fa5f28d90fe9f8e98475ce22e94527f6acd4d8237026b88d204abc448c6601652dcf0daa48ee87b7bf963a8ac513497d71a30f697450b0fd32a2e6998d3ea79dbef36cacfe 42f0fdfb014850bce9b453d27bad2 6381407628d75cd7b0bd5bd2578a938e3444ce469752a8b0fca4faaa0b6af0d5a6c3dcfafc0946ded5192e7ae4adefd78a52efca580273d6b5c65ae1a0cceb7d3d0cd60459a0122719bf609663c5534a352bbbc691843799bcdd9d769a6dbe4ffed03f191a19af0e06db264a39a628a02df4cd91470ca146e3d489b1872f2f4a6994ee442cd545f8eb24fe88966ca677a4f4771eddbfaeba32e67e756033c129b43cd6af1e29e831a094d68bdd57ee000c802ce2a84a49df69d6b590976aa2767ffc8a67d01e573dfc2345e94d0818e75568a25b39b1fa6b84ba282e9905bde5af41a98c5f13d1e5b3730ad0c038e4fbc3a0c4bad5b30bf76383c83c6683379fdb9d7c1ffdda8d04edd04295899a42656333ae82e8c7307cf28fa0e682e8765446665b155d007b629cb084249f1fb3bfefdae240c8df2930b3560a4b5248ab67e87d6120383e1197af6d59a08d3e9c22f0b0b600722923ae467f20bab3215cf6c79e98f64d1753758e37fa2d12bb7a8f38cbd7d7147ebce5c12875e0d1cb9db2c14b57a82f6017366ee3acb9703d3eb58ff7c767764eea82d28cf0eefea60fb02a6bebdeef3a9b1156e2ae10db1e4bdde66b7644df7fd7eb4c8856fc8076d4651da726b3f59bcd2d1d19e0dd5a73bb146621f3b0faed37b6dcfc7d891c72c918568d9274face3c25eeb2f69ea5e348d7986f3a91e8ddf07b52898379dfdeba664565c937d9d9e4c4d736374c0c20b5c89ec2400e9e508a07fd6ffe9f9c938d12c907f5dce392ce611a7dec9a0c4cf9ab41171bbe67ac041c35b8b7943af6ac523849af42bf7c85b75d8f76eb3edec03e43f5b5348d2be37b19d6cc1780e385fdc7aa1033d32d09cb88fbcbd65471f0938e26f3bd4bf4ade5e64e784484e04c810b5d72be3005e4d13e81abf82f5e953a2fe28e1082924ce5ebd88c9a43e2b6d499c476cf42df109efed4bd9982eadd4aca404664e6cca53007bb2b1f58a3449d8b40b9b94a7b9d66e5725b3347952af8d5fbeff3de123092ff904682 2a7b983b22599085ee7bdbcf1ba882b46a5b11120c306b616e2ceab9ab5f80d98de070726d56395ea73db4915df1531b9b0846832613f34e683a76b99dd681f172689d8eebc799455cf0bffa72832deee3e7ced72834c91a3c6902327afbd63446aaed4c5b8c586f3e56bdafb3630d55972b6e(6baa3090d3fe60b79a67b53851730cd8fc345ff9b570da9deda2270c1e93cf30d7045ada9bdca427e6d8df28d7607b3ed655f872425256cd8deee210ea98b752631c8a3b94068c777c38f34dd078fff0cfa02ce01e9a4dfa0b8f008597ba305ad665e425b281d0c5a55c3377f17b12eca69c59ee3032b49b0d626089f3515f6acf39acc88d48ca51b9acd649eac9fcbd48d3069243a78d5ca596d0b6f38e2e8bf4f6fdb91729e5f4e0185ae0be2815ca49efd25fe1691dec446a34e726d5a6c9cde7b7bffa157aa2 601b0c6e55b65ec62327b321c5d0376543ecf394338ec37df945f2dc842947aed16659fd9177df4bd83d5f64b485c1b532159f5fbc582baea9e5f0c29a6d200026f725f101efce5ef79ca8ac5b10157fdff8ea6ab3e6abfba502b708 a9b6ef6465873556708507b32b9e729b818a264d1472f81fba98e9de32820b9ab4ad87440f4dd85c56964183be684288b86a3b408fc26a92dae9fe8df3015a15dfe4cfab9921ffc1af9d96ab96dba727301810b1f7ec530799857816263e0936c171b94f73be8b52a4e375645860576c27755301faf7e9a9e671ecae8055c8f352ddc0c3bec368b4164d55a38c6505f503931c4744daaec7f511685af75d15d408fded36d03b4a9cd12fc701da9b855c24 922db02ac2c24e6ade73fc1f383d64a5504330424cbe1a6dddba938e24ce954177ef173c78e67e9abde250bf8c00c0eb6b63a2cad400538fdf14b75c105297fa83c51dc304a9ba83a0be6d510389498ba95b2ae3387a820b030ee7a93790a997283e46f3e8e7c24e6dfd27377c1487920e2dadc8af9641c5ebe25eb24d2fd42fe51e2b7057b136e1a438382508d73efbf5cbab68be1fc407eed78725dd8cca1593e8c29f05597c59b67d3e04238054070bc491d9a083c535f909f00253a47fe0fbce0d1e721a9a4a475061abe6414799a09f5f21463943cd02459be501c6d4262f1d7cb1a520920d3747e6ffac0143246281a650fef1acab737d8402aa2bd32f9863817ab98140561d50e92f51bbcbdd38649d4fa24846eca5ffd9961f1eb3a8263b832f4477b05465e5b5d001541a2a9c5a723f93f692ab65ef10e37367b8a1f96810d98b90f960e3485b3f3ed3a77a2ee4917a54486ee4f5419211b22ff5a8aeaa2b30469d0cb88a9b2d9b909533d855b661a83752db44afb706414d7e337a178fa8a4227440f0775e769a25a0aebd67701c4319de2e9d4c7e1f28d3f232795fc911ef900cc51381a27e551e6621687b14316c3ff715ac9bd9f170ef7b952ac2397920ab85994d84691ccdc3af cbd8b850ff1d53daf346abcee48ed3e1a12c6c53f3b6d46abc4cbe6cbea9ba3e2945fe16f1d24cc390a91aed7ffdda54a2efc58d6a5673651b70b806c6977314bd689c3415 5c9005cc15538a909cce14470c4b35864355584f5cdcc638a43a8fc4d35d0add6cbe1895d6201991cad9870eab0fc041a29119b6b7cf0971e728518c809c667f3bb0038a9d756feebc5c0e7130da74537aa55de809ec067d20c8cc059b6b6cdaea87916d0a7091ba1f190de4e9b3d0cb24de4b182e77429ee12f8b9cb067f572eb11bcb2b08479b9d8b915950925db89aca66a7ffa0b7eb61b8c890b995e126622d7f768d0e92ed2f607d1dfba9390fd222b9ab664ab6bf84b295aec70257b96 e07af91ff296de4bf36f0bb276829e558e2ea72e33691ac967beaeba41ae2d5b5e3fe8c111ee87b51a953adc74269d5720161d68b49caf650224771db3273bc412328908586a808c621f3a96c5c0cf5f0300b53f719b19e79f93aff5edbfff8f85b6301 5e8863f3540b96a060721bc6eb0ecad3ce116bcc7f862eabc8ecf2ac819ccc7d7bf0efc0d805ec3c7c6fd4a4dbb7d592e11bf302aa395b84359c0c1374a3f831fbbcb824d4d63c3d246e657fa28dcd1e02d8c5b2267a96c08586e347bf9d12c6925c90e803241953ae386e7a675f4e22f7aa5ae037760552848d31353ac3523446c24f468ed3265cf010211908628ce036c044eca823a5b903986809ea01a3e3c077614fa39a928a8d8bdad37cf02d43ce76bd74eb547e46362af55c970275b643e9ab24fe39cc7 69a090cf8bd3702f0a7583ab909d4c5c809459a43ca2992fc5fe3660d25f8e9275ea2619ebf66a8ad874b755826bc1b6434e34538de739f4f785f3183fe740cdd2646b4f75a3c265ea7f59a60c6aba4139a1b36a4 d840d34c4276916067e5151c49a4d43b29aceae0e52fcddfe418a0cdde112e7c4776c5f2f3b7c1d74f811e018fc59cdf96a73013d46f65ef3f72991991d94d66e4c99a0ab2467d27a9108d9564283efb2e37746515da52afc377fe66590d82b8a89d02f9e367684cceebd60f53de14fc05524f6aac61a4b006245c31d5da9c0cb73d6629c211139dfe5451b59f9fcf862d6d3ee3d420fbc9362f06f418ff547a07d51a986090d7db11ab217a315782069d999ea6f1bae60cc37948307c3111625ce9c7dd15b1c9d092142eb4f44250eca9c4c007ba8845f811fa1c2d1c6b42d306c170a44a07ddc9e0e9f3f784a47e5 4a138ec7d6ddfdc22d07bdc40717698347edacccbf7d12b67b0aecf1ea556ae3b5b11abaa73ae4e7766e325cfcfec41d6db69cffa8e338b4b0b87529994d82c5ed8a42a70562a942e0f4c901d43cf026b54f29052cac5c36635c90abf6c373727f29b7a33a834b67807eb83a599bcf8af7ab0b4da447465e3cecffeb35d90871a2d29de36ee445a916b6c96200f76134bfc550af9ee621a893e6a485ffdce04ddb5e993f0053d1c17559dd2620d5ae5118535859c2aa768277b88b9b5d03e8407e8058def1f0d7532ab196b127e6eb559dac252a22202cd52eda570eff06375f0865beb95c5 cc21d8854c090ef6ec75fee0b14c3d633d35b1d3c3a71db3fd9df436515bfef5db7d35822f03b12590d17251d8a479500d1bb098b5576cbbd2203d1386a299f415521ce8c321ba905d19ec94c3ae49678ddd51da693502cc90242cc27ed4a33ce276f921adf91ecc19476c22631a0b23dcbffe5d0bf006c653248c1081c79d949aad51e0df38d11b44982867213196c02279691f5d021bba1a18ce3269562d1fe1aab6b2568fc80e5a35c4256e1b90e45c928883350853ae0892236a2d083d68cdc4dc5c043395d551d4ad49fb9a804f56d44e3a621f3dcad1ed73be1855f52239314f1af987e135151fc5d13fa258e772052592e25af7c5515f9c55667f1a2087019abcf445dc1a55753132b98908c744d74b4081 3bcdbced9a109b103b32995377f790d8178f1400bf6535ee6cc4908170c1347b5de086df154e8e05ed8448b9de6c2080c0d187e5d1db634508c778e613917ccf4ef9c7aa73dca77cfbb3c95a27caa350b25d7c9cf30096c1f2057fba5a39ed1c1ce8b6a463f2f8336d38e9dd297d87e3499b0003c8c6039ec638027219862e49e7cf73ed536aacb472ee8e4b3effaa5d4c2aa5f4f4481e031c036aab95a0786874b8566b77331010bddc57eebef603bce2664718836012788a788902910a013b3e54629fbdd9586959ec95e6fe33a2700fea56a36c57f618a990d213fd242cce90c1520f3bdc2f5a9748d05df631059f28b7821c8c386b38235cc92e1af3c9e17b682fa44459 84e43bb2cd279464e3702d3350589fb91be1e29b04e668d00334e2803ab63964afd1f750d82cade861e09275fc1ec0278357aa482a2730cfd923ba5066f7646df091591c04319eb978e2cbe813ff42228f645b3d44a2b08e95b0669b41e49fc80927651b82467b3015623c25aaf4ed93805561cd5efb42294307e9e05ac996a3899ac4f3d10405aec4d60b9f5384ab589951f954c5494c4d6aa7bb4d41a6d208532d14707a98df31a9f59880c0df14d6352d583ffce3cfc4d634d2a7e13e62222aa0d3744c187b34908aa37eb0d9ec4c92160657d7cb7f1172a598d3fe096998c9e7fcbb65db69179923fe5177f50d7e098de00ad88332 f16ec32a9d2751eff69ea90859dc4a8efdbc25c7ce8bd8e1b9b71d3eacc4964d689e0f78c8b176c3ce70c1886ceaa3122e101a248cbc780bb361c0f5b9e403d5c0ff5985c6d359e85bf8d12b29600bfe99a1ced37d2a65df540b8f54 46325b2cefdbfb b5677b7883d487988d730de322833ef4e003e5f70f2a df3a92793773a197e4f76e6cdf0f8e982fdef9d35b51fcd9c627123684e74cc014bd7053dfc61f6d245b41f140ea5c21b9bc47e1bb83244a938df770095ce5ee155943a53378e6bc00974e75034df42cdfd71a9958b40e55c2757d9aac67889706886432d39200e50c4edd b4c2063459962645dc278cd5a503328cb022af9e7f319c9e522bd53e4e34359c31f9b8c51e2f35b6dffcb4bd01c786d1d8f407ba2e737eceff6dfcb9d5a7276c9bcbed19d7d2a32bbb64ce16894861afcb9fed531a5b14d7a3c60354f3df7079f2ebf5778c135b4ce2a2706cb6ba32a941f1e03a0aa2e09b45340f5c0518236c408550f3c43ab5e6aca33ad1495eeccfcd624eb51b1d8c16b6a9ecd14f5411a7a25ed1a53b5f2e31a7a83b9b483b774d4a79cd627548da05b8c6e8b33f3dbef112dc84e0267406908dd8fa24ca16cf21af0ccf6de8452868d24a011b167382f6bc673b200e1d440c96a4bc87f1dec9eea3b83da5eb66ca7d191af8ea25c3bb7e60df5b8241b91625ff117862aeec7a891b8a566878b67f8ff348b40075667f64f10d144721188cefd5a439d18ba1c9194a8c52762c1a3856888c068bc49e2fa70dcbd008dc8cb6736b5c3921716e58383424de843caea6b9ff67956500e264ffaa87bdbe2c8c451810b5407f89c3eedf8069fab0c7262bf79c149eed7a13ced9031fbd7a9047966028de0673e3a0bafcff75ec37bb28bd97a4e914d8a1af4500f2833630ef9404dbe5aaec1d282384a36ed6fef0dd1e4edeeb7ff0e383cbfb3437ace0b662dad72d5dd960d9a2d72f56b104c0accfb65db964d5fe58ccffff28844524902281d29a971a43493d5bbbe4acf9583f1b05d9d502289514b4666f8b997071ca2aa24eacb851b5788855b5224c678f35254a38f6bf561592929ebe0c0f911cca2467ffbbba83ad6e1fdf24d231ffac1917c5d9688d246fdf2b31fb0d45e315e5d9f7f385071803435439f15e619b684c25ce3b2e6acd95f461d8f965c7a2786a6656d0d81ee00623d52854d9c3f5bef5e994c19c77c05bb5db8247021cd26fcc9edb24 5184be02e37a2aad383931106c37f617cb55af97fb3619e2155a4dfa89d0a7980ad14a5705fb9b128e890c75a0ea70c12506113d9c1e881ccc38455a21e99e0b8c2e84bc65ad87f3456633bc551117db926bddf40e4ff034c368dc3dd4f08db89986c49etb. 8 transposed(H 8block2_items_varietyregular0  0}blosc  ^ (CLASSCARRAY (VERSION1.1 TITLETREE  8 transposed (kindstring (nameN. -!ii fo-!nn lb-!tt oj-&01234ae-! tc`,!@t,!@_0!@12,!@_9!@01234567890123456?  !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abc*!*!*!*!*!*!*!?bP̆bbi81˝\ǂSp].Ev#{5fm(Yջs /  b?;~13N[ 뉽[IEr8}+fJ(iz؅33 -bpʾ3/[T$`Aumg$?kNpO-LҚiSˣ áGuJ}aݓMIٖ0T?M" ,X;կOm#ޮnl U6m-T NSNҋ-Apx} EŢED=B|?`2igVǗY%""`ls(٣Ba {|uOa'Re@7SOQY[g IJMmh!\4?5@JE`Cwo3U5k0p+)a5ASndZ+ÇT:suLGT >:h֞\ uk ^l/?㵶ؾm"?? ?@@????? @  @ `? @? ?@? Y=!fX=!lX=!oX=!aX=!tXA?"      :             @         g             u X         A{!  !  !  !09J!.J!.J!.\N!ii .N!nn .N!tt .N%01234.\W (!oo(!bb(!jj(!ee(!cc(!tt(!__+!112(!__/)0123456789 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/examples/demo/Advanced/test_h5pydata.h50000644000175100001730000002063000000000000023451 0ustar00runnerdocker00000000000000HDF  !`TREEHEAPXrun1_test1@HhTREE8HEAPPSNOD Hhdd 8^  !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcxyzlatlon?7@33333TAQ7@33333T2q\d7@33333TȠw7@33333T^Ф7@33333TH7@33333T/7@33333T!_7@33333T57@33333TM7@33333T}7@33333Ty" 7@33333TM7@33333T|j17@33333T<D7@33333T۲V7@33333Th Wi7@33333T:{7@33333Tj7@33333T*C7@33333T7@33333TW7@33333T(07@33333TX7@33333Tx7@33333T7@33333TE#7@33333Te67@33333TqF I7@33333Tv[7@33333TQn7@33333T47@33333T7@33333T`4>7@33333Tc7@33333T7@33333T"*7@33333T7@33333TO"s7@33333TQ7@33333T{(7@33333T_;7@33333TN7@33333T=`7@33333T?Ls7@33333Tio7@33333T7@33333T87@33333T,ܽ7@33333T-7@33333TX]%7@33333T7@33333Tm7@33333T7@33333T-7@33333TGKZ@7@33333TzR7@33333Tse7@33333T Fx7@33333T 7@33333T597@33333Th37@33333Tb7@33333T{7@33333T7@33333T$'7@33333TVh 7@33333TP 7@33333T浰27@33333T|TE7@33333TW7@33333TDj7@33333T?tA}7@33333Tգ7@33333TkӉ7@33333T.7@33333T27@33333T-bv7@33333Tđ7@33333TZ7@33333Tb7@33333T %7@33333TP77@33333TOJ7@33333TH\7@33333Tޗo7@33333Tu<7@33333T >7@33333Tm7@33333T7(7@33333T7@33333Tcp7@33333T+7@33333T[7@33333T%]7@33333T*7@33333TR<7@33333TJO7@33333T~Ia7@33333Tyt7@33333TSNOD H0( ^ ^(dd ?@4 48` ^h(dd ?@4 4 @^htimedirectioncoordinatemagnetic_3_axiallatlon`z >z >0˾J'm>J'm>ȂҾǯ>ǯ>Ǐؾ@G>@G>:tKZk>Zk>n!],X>],X>m쾬GtӾGtӾY<Y<EGA~~@ vLVV@DW;7,w 7,w o ̩u̩uPP.\3:!޾Sf޾Sf&2L2L‘:T.V}𾾖V}h4eWL?eWL?C:aw1?aw1?xϵAkg @?kg @?GGRnK?RnK?䁚,Oցu6=T?ցu6=T?EѭTuFjZ[?uFjZ[?mn[Qa?Qa?7m1bǻTc?ǻTc?z\>"h\Ec?\Ec?Gp)^?)^?V18uOUyB?OUyB?Q"|u߻]u߻]@e w wP1]l]lu#[jȒjȒࡌ3*қ*қayᢿayᢿ@梿g'g'a~ުު`tg8]n8]n8xpʉ~͟~͟b.1HHA1HHATZ¿YĉC?YĉC?\,ǿeWZغ?eWZغ?Huοߍ*?ߍ*?ktӿWv?Wv?N]xؿnw;?nw;?XoMO޿<k?<k?v$kxܪ?xܪ?GI?"?"?]ӗH""4d?""4d?cOZ1?Z1?+ZU-XU j?XU j?(.U jU j(.@1ٿ@1ٿ.ZU-""4d""4dcO""]ӗHxܪxܪ俢GI?<k<kv$k}w;ݿ}w;ݿdoMO޿WvԿWvԿN]xؿߍ*ʿߍ*ʿktӿfWZغfWZغTuοĉCĉCh,ǿ1HHA?1HHA?TZ¿~͟?~͟?b.8]n?8]n?Hxpʉު?ު?`tgg'?g'?a~ay?ay?@梿*қ?*қ?jȒ?jȒ?ࡌ3]l?]l?u#[ w? w?P1߻]?߻]?@eUyBUyBQ"|)^)^V18u\Ec\EcGpǻTcǻTcz\>"hQaQa7m1buFjZ[uFjZ[mn[؁u6=T؁u6=TEѭTRnKRnK䁚,Okg @kg @GG8aw18aw1xϵA"fWL"fWLC:V}>V}>h42L?2L?‘:T.޾Sf?޾Sf?&??\3:!P?P?.̩u?̩u?7,w ?7,w ?o V?V?@DW;~>~>@ vLY<>Y<>EGAGt>Gt>*X㑾*X㑾mZkɾZkɾn!&GҾ&GҾ:tKǯҾǯҾǏؾE'mоE'mоȂҾz ˾z ˾0˾././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/examples/demo/Advanced/test_table_dc.h50000644000175100001730000127055000000000000023500 0ustar00runnerdocker00000000000000HDF  hq` TREEHEAPXdataH TITLE (CLASSGROUP (VERSION1.0 8PYTABLES_FORMAT_VERSION2.1TREEHEAPX htable_i_table8SNOD(H(H TITLE (CLASSGROUP (VERSION1.0 8 pandas_type frame_table 0pandas_version0.15.2 8 table_typeappendable_frame H index_cols(lp0 (I0 Vindex p1 tp2 a.  values_colsa(lp0 Vint0 p1 aVint1 p2 aVint2 p3 aVint3 p4 aVint4 p5 aVfloat p6 aVobject_1_0 p7 aVobject_1_1 p8 aVobject_1_2 p9 aVobject_1_3 p10 aVobject_1_4 p11 aVobject_1_5 p12 aVobject_1_6 p13 aVobject_1_7 p14 aVobject_1_8 p15 aVobject_1_9 p16 aVobject_2_0 p17 aVobject_2_1 p18 aVobject_2_2 p19 aVobject_2_3 p20 aVobject_2_4 p21 aVobject_2_5 p22 aVobject_2_6 p23 a. non_index_axesq(lp0 (I1 (lp1 Vint0 p2 aVint1 p3 aVint2 p4 aVint3 p5 aVint4 p6 aVfloat p7 aVobject_1_0 p8 aVobject_1_1 p9 aVobject_1_2 p10 aVobject_1_3 p11 aVobject_1_4 p12 aVobject_1_5 p13 aVobject_1_6 p14 aVobject_1_7 p15 aVobject_1_8 p16 aVobject_1_9 p17 aVobject_2_0 p18 aVobject_2_1 p19 aVobject_2_2 p20 aVobject_2_3 p21 aVobject_2_4 p22 aVobject_2_5 p23 aVobject_2_6 p24 atp25 a.  data_columnsa(lp0 Vint0 p1 aVint1 p2 aVint2 p3 aVint3 p4 aVint4 p5 aVfloat p6 aVobject_1_0 p7 aVobject_1_1 p8 aVobject_1_2 p9 aVobject_1_3 p10 aVobject_1_4 p11 aVobject_1_5 p12 aVobject_1_6 p13 aVobject_1_7 p14 aVobject_1_8 p15 aVobject_1_9 p16 aVobject_2_0 p17 aVobject_2_1 p18 aVobject_2_2 p19 aVobject_2_3 p20 aVobject_2_4 p21 aVobject_2_5 p22 aVobject_2_6 p23 a. (nan_repnan 0 encodingUTF-8 (errorsstrict 0 levels  0 metadata(lp0 . hinfoE(dp0 I1 (dp1 Vnames p2 (lp3 NasVtype p4 VIndex p5 ssVindex p6 (dp7 s.H8index@int0 int1  int2 int3 int4 float ?@4 4object_1_0$object_1_1,object_1_24object_1_3<object_1_4Dobject_1_5Lobject_1_6Tobject_1_7\object_1_8dobject_1_9lobject_2_0tobject_2_1object_2_2object_2_3object_2_4object_2_5object_2_6@4(^SNOD; <@>d (CLASSTABLE (VERSION2.7 TITLE 0 FIELD_0_NAMEindex 0 FIELD_1_NAMEint0 0 FIELD_2_NAMEint1 0 FIELD_3_NAMEint2 0 FIELD_4_NAMEint3 0 FIELD_5_NAMEint4 0 FIELD_6_NAMEfloat 8 FIELD_7_NAME object_1_0 8 FIELD_8_NAME object_1_1 8 FIELD_9_NAME object_1_2 8FIELD_10_NAME object_1_3 8FIELD_11_NAME object_1_4 8FIELD_12_NAME object_1_5 8FIELD_13_NAME object_1_6 8FIELD_14_NAME object_1_7 8FIELD_15_NAME object_1_8 8FIELD_16_NAME object_1_9 8FIELD_17_NAME object_2_0 8FIELD_18_NAME object_2_1 8FIELD_19_NAME object_2_2 8FIELD_20_NAME object_2_3 8FIELD_21_NAME object_2_4 8FIELD_22_NAME object_2_5 8FIELD_23_NAME object_2_6 8 FIELD_0_FILL@ 8 FIELD_1_FILL  8 FIELD_2_FILL  8 FIELD_3_FILL  8 FIELD_4_FILL  8 FIELD_5_FILL  @ FIELD_6_FILL ?@4 4 0 FIELD_7_FILL 0 FIELD_8_FILL 0 FIELD_9_FILL 0FIELD_10_FILL 0FIELD_11_FILL 0FIELD_12_FILL 0FIELD_13_FILL 0FIELD_14_FILL 0FIELD_15_FILL 0FIELD_16_FILL 0FIELD_17_FILL 0FIELD_18_FILL 0FIELD_19_FILL 0FIELD_20_FILL 0FIELD_21_FILL 0FIELD_22_FILL 0FIELD_23_FILL 0 index_kindinteger 8 int0_kind(lp0 Vint0 p1 a. 0 int0_metaN. 0 int0_dtypeint32 8 int1_kind(lp0 Vint1 p1 a. 0 int1_metaN. 0 int1_dtypeint32 8 int2_kind(lp0 Vint2 p1 a. 0 int2_metaN. 0 int2_dtypeint32 8 int3_kind(lp0 Vint3 p1 a. 0 int3_metaN. 0 int3_dtypeint32 8 int4_kind(lp0 Vint4 p1 a. 0 int4_metaN. 0 int4_dtypeint32 @ float_kind(lp0 Vfloat p1 a. 0 float_metaN. 0 float_dtypefloat64 @object_1_0_kind(lp0 Vobject_1_0 p1 a. 0object_1_0_metaN. 8object_1_0_dtypebytes64 @object_1_1_kind(lp0 Vobject_1_1 p1 a. 0object_1_1_metaN. 8object_1_1_dtypebytes64 @object_1_2_kind(lp0 Vobject_1_2 p1 a. 0object_1_2_metaN. 8object_1_2_dtypebytes64 @object_1_3_kind(lp0 Vobject_1_3 p1 a. 0object_1_3_metaN. 8object_1_3_dtypebytes64 @object_1_4_kind(lp0 Vobject_1_4 p1 a. 0object_1_4_metaN. 8object_1_4_dtypebytes64 @object_1_5_kind(lp0 Vobject_1_5 p1 a. 0object_1_5_metaN. 8object_1_5_dtypebytes64 @object_1_6_kind(lp0 Vobject_1_6 p1 a. 0object_1_6_metaN. 8object_1_6_dtypebytes64 @object_1_7_kind(lp0 Vobject_1_7 p1 a. 0object_1_7_metaN. 8object_1_7_dtypebytes64 @object_1_8_kind(lp0 Vobject_1_8 p1 a. 0object_1_8_metaN. 8object_1_8_dtypebytes64 @object_1_9_kind(lp0 Vobject_1_9 p1 a. 0object_1_9_metaN. 8object_1_9_dtypebytes64 @object_2_0_kind(lp0 Vobject_2_0 p1 a. 0object_2_0_metaN. 8object_2_0_dtypebytes120 @object_2_1_kind(lp0 Vobject_2_1 p1 a. 0object_2_1_metaN. 8object_2_1_dtypebytes120 @object_2_2_kind(lp0 Vobject_2_2 p1 a. 0object_2_2_metaN. 8object_2_2_dtypebytes120 @object_2_3_kind(lp0 Vobject_2_3 p1 a. 0object_2_3_metaN. 8object_2_3_dtypebytes120 @object_2_4_kind(lp0 Vobject_2_4 p1 a. 0object_2_4_metaN. 8object_2_4_dtypebytes120 @object_2_5_kind(lp0 Vobject_2_5 p1 a. 0object_2_5_metaN. 8object_2_5_dtypebytes120 @object_2_6_kind(lp0 Vobject_2_6 p1 a. 0object_2_6_metaN. 8object_2_6_dtypebytes120 0 NROWS@dTREEp<(  ;M`5a1ed8f2d8b8a9850eef27646045f0dda921ef8cf2ae9ef79342b7834a8bd96c6fb17fb6d36cccce779936df6966dd268ef1d8a7e09c3ec21f0e40756e85ccd7cf14b16965a9984dc0b3e918aee13330ad3e01bf7c559a69d133413482 ~"@?de66deda9ce5673d9d30ca39d9f89773306b28929c80c283011680a41491e6294fcd75a32ae9c22319adb3d66c301fd20831a4006d86c9eb3205c71d43064215683c2c6a4f39b342c27562d953f9f8e14ea66ce52b329a0bf86f9f1b4ΐk ?92d7c410e45bd1a398898d2df24bc1e6a0e78f76defbbd80d9688e04706b2800b6c13279efbad3ae 6fb804a87e444751c817e7d97c4ea6070f4f69ab1591e3c3724912d5981d5b0110173f85fe94277d9c5cdd3e25ca21a91cdcc31cbJ6eb28adb960e7c7a98706d8f1508353d1efedd251fe12061f818e3c3140eecae11d480cfb4c4624984bb9bda88866cc27f0a96b9d9846a1cac7214a36566992482c9c1acb23f482fc50c8ed86c8c95791661a03a5dfaaa35fef621d7cN,?3ddeeb10ff72442c69147c66eabcd7e52dff5f8b64eea29c536e0063d2d4f30d3abcde9f6ac034067179fa3389c7cfa440e2d0c764b0701c59788968fe835a8448b85ffd23efceed29d005ffc86a3b419098c788286a3d5a98d96ee0aPpXE?6483544fe963039f9b3b579e12af26bb7e0522a78c4a2a7555ad88b7b416c89b4b8fc8a6c940ec0b 66266cac0c476ffb22e7295dd0eddc9f1ccf7fad2d7063d4718a15215a7feec5a632dacf0283647596e187261e0a5cc72246b2da12`ʿ886d01826edeb4a76bdd81d7d7739faf54310096a2e25becf8757997517e19db6d706a2d330257842994c1fb23c15a1aeb8c8689285ba4c920c9082dcc26ccced3971540bbe582f780f24bd49c41086d09dec6e6b1f07685910d653ce 3OiC?65b7c139667a6ecc0dd05c318db61e85a95d1d7563c856fabc851e24009b7b93c9a4862634431e8d6941887a47e51781ffad972c09cda284ff41168b22f215d8bfaec8d175ffda20043f45d670ddf099bf8cd63d247fcc9d845df0932Nܜ?eb441e3d2a9ecd0cacc6e2dbee6b69e053a1fdf6e2641fa794b8056f559733d23ff05a8e20d215872d93436279b0e2e296ada23a03c2894f4f95002b4dafe7213d2c410239e1b644f59352f78ede6df8bdbaa16c6f11ba397dbc6bb7c [-;ο888fc7eb52dcffa97b8cbf026d734405ee51647377e11a27c2e6f402e49b999c8cb743e7ef2c79f1d8d986a8b73f758d38e986edb5fa3bf6ba7216cf445e82010d72bff6c94562ef328f6ddf49087d451ba1db5c2dd427ab42343563b  wԿb1e841df91642aa44ca49e71b6a3b91d94b8c23bff59cc9695874ad35da6912a9b78d29315a1251cc55a9237a0eaaee5fe701a3cfa0a7c5d884cb0be161835e02402c6124d40 6cb2a70628d877463fe196e350f22 57e002d6ab3810 goÿ798afa2dd5902635e774ac0bab797d21417df68a69f9d3ab175a5ba15876523a0ed6e1d6254567844c34dbee8e89dc6e0df53bce8a76b41f93303f8844346984c9eeb0f8fb7073a649c2ebe0139 cf736e246d43d2ae833f18a16776c ¨2d345ebb73c4bd3c6b4b4eb65e5b02a1ccbe28c360ee9d114c946f94e278316e995132b6074d7761ba9a613a5005464a6c83b5cee855acd45205f44c2ecd896d8c458273f102407a636176a9a3e6bf038ea85bf0b58c802a9ce2385ad  bV3?52eae87048f48ebee4ef229e924bc0cd175249128ef21ee097db61b050ee9ca0bda04fa5fb13586fec2144149efd3054336a3376b08b9ba98016e580370ff2a863583d58f9ac127542d6deb0cc1d27d6d8c0bf1091cdeef37a0e7fb78bLOU498e5715bbd9bf1612c698449a0bacbdc676f195f864fe2293e10521f435dc94bff11fea4f8136f010aca573c66e203c3f658186c4e03f898594156be60a66dbf79423909632847828baae5c126859be0bdae54589c97fe7e4c2fc3ce m?d6f9d52c846790af6f354f8b9b459abf5fdb217da79c2bfb2fe0ede1e9d442cfd12fb3bc2c43ae94b7e010b75586152c14f1878e0f9569700c3cf2903fd6b1e795c2559f19d944ffea7461c1441b190e973dcd8b7566b77c1a868ec1b 㫢Y5޿87aad7c1bf836ce08cbe9cc735547d4af3879635c41db080c9538c55213645ff00ad777df58e620aa542c3414b83f7699c684b40990ac299a4eb969e9866e3d3521957db9f8d5bb22fe79b02215823fe865d41a9f1eede8cdbab6f14b  #%?0a705b2f0d4e2e72ced21199c1c2e7dc363f9f1c7ee1dd3436414725c843d9023fc18e54258d99e32e662ab8fbd7439b32cb1cad1dd2bcd86b568750dae024cc4ee55f1f80869a60dc256dedfa1d1d80899b39da5baa2c4cbc4746a7di["k@63268bc39027ca8c566f5065209702f0730de3c51c73e77c29bda177840c798dc936abfdff2144bfc51174519886a85a4ea0153690c73452b4e1b245c98d6fcac6b0e558fcfe9352a4858c9b397add9b44ecd1ee4a52ba3bdb7f9beb3ѿ7fdc371872afca731ca4638c27f80a70bdeb873ba909d91c5f9cd04c14438f43094e2bbc9630bb53ac7f5bd652bc110111958f6a01fc9043f8dc38dc2c9b1cb98d9c2baf227bf69b5c7e30c1dc6a4baf239b43df0187ea6d3be3d3cbf 8Iiׇ?7efed34857acbdda54f8ca1106b1be8d2bed416cb5e79bad3b081d66ca71d5ad5c8ecea8be175de3a6ed1dc99bbf1736a7e34a40a3926dc382fc6b5ddb8dd5b58c1ad9ca2e1d9bfcf9e8e631ad58131bc20fd677137322512512341280?3904b4a1d9a17f2251460f7b375cb9157deb6be5140722a7117ad5de948f81a1d0f4281c8fc909ab7e8ded37c16a520f11df99c117b8f7e309aed70800f2c7421efe2d5fecccad059c1ae090942a9a652cc419424fd94e88e40d76d00 E"?671904b364144c150f02646873a00e94427ce9aa2f0fa22522f12b628ccd94d172970840d3eed2d2208a330864b7f30702804595ee4f3c9c2d70278abd7a73cb409bf2904e4c6537919ba1aa10d56a8e4652497aa982beb8741531e0d rS5b5a438b18eb06e9b24175345dcaf3765208d103aeb44e92729c0b5c78974fc96ea3e6a248fa443dddb06f725f5f989c099ad4dbb279d0f4693ee3df1a10a74a45818a52893757072720c9869cfc3cdb2a75209b622a1d397daa57c7e 8p?3d0a67b8c893cd97a9990d92d4a5077917122e66091b7042996bb8e031ca98b792d93b6b39f1e9ccd39134e5c15cfc0319d242d23f93b1 1da3c3f1f84bfb6e8fb50cbd715cbe8cca19a9c207d979d52176ca3b917cc0e9f84ab38701˰+?42d78bca095dcaaa7450b86bc1732b3867d7182f7c031a9dee276a95c3e392ea4d61749d8087e5ecd6fa11d9254654543b0a1a124075ac4dd2498e300a6d91f400bdaa1328302f2871dc46691e854d445d498d6c27 4f86a3c760ace018b7516ace67eedc6e48c7d01daea9f5407713d52a846c56f3feeec60399699b9c4792aeb49d8559026dd14b0c892f46e5daed2874c22a0effbb69ffdcc0111a55afd90a9fcf27a0fa1631f5bba5c0730d1674d819d167881d87045c40 Á<¹?ded69b63153f3e6f304c2ffddc0050a62188728d52a61141f823b7ada4061911f08c87f9157271f025aaddf60b9ef5314a43a45e8ac403af932015299a0157ac07b15fd51370d929aeeebf23147408d60554943d1fca873b5b0fbd9e6}Uݿ76ca9c2d940eb6f9797074bd726145c4e0969fc4fa4f12fd696524c1b93558cddd9ed6464a9e239f8a70993d33b86ef59e48f5b4e289e1ffb03d54d479d383579fd61f479eb93a00a3cb552dd4bfd0860951026b2d639a41002a88e61 n`?461085d893a11c39051001087ce446ec094f948e8821b7c77e9a3ff0512b20d81c335bd9e1fb9e2ae3c1cff498d00e9782fb7019ea239bbd23411220dbdbede6f6366dd383adb764b01d844b85613f6c390baea93fa9562871236e575)ldbb918e58c6d095387268444487f24fb143c8a41fe53fcd82028621562d23ce1dc901f74657ca6bf12dba3135b221ffd59fa00e83a61172dec9125314d44f1fe33357ff4c34e5c4f3a2d7e7c5022907ba95ae7e646ef4385d4d64b294Q ?2202238974818e84dfafffeb8afb1d0c565f64bf00f82d0509fef4d4c4c853c864a17d4e441225548c308b74f90bca05c9f09304eaff4bb0b9205e2cfa23c394c5f008bbd0822d4a058e2ed02a6a6052409f79af02b7ae045f968ae90  +-)ȿ22efc8c618adacb7d4a8035fff951ab0eed60ee8e44fee2d6044864bb59a73ccd7ee6d91d290ae6c d54865080c122b3755ff68777c4ead0930dd684e1c8765e3cf60215eae0b5bed21687404b4d88c240b2f5e1ee13fc0965e4f53af! \vUa5f5c1431d36e2967b1f88698780376c16115a64a159e584975d661ba0f904cf564a8547d1ce3dac195cc9bb6d84a9f53b8a21c0974c56f 94a3eedd211d0687baa6883323fd537ab0db15767b3ee06a7076a7f6759f47887abcea580a" f5ۿ24466edb5f794d925ebc5e242826815a52ebf856d7425d91e0db82ca0635e06e20e44bdf1c05100635e8453f0c377b4e90f4ddc56dea3ed55d4bce9caccdfa4683430477dfc436be0486d02367ca4573fd5013f23f5c3ac6474bcac23# ?ffc476f3f871a5610b57312d0ec7f63695650461566f41f21ca550788c62c3e74faf62e3911bb68b3771ea29f3ed0fea816bc67fff4772e8ac9420fee4142c200204ad32cfc8fd5539de9c52465586b3b681175489f095ad42560a7ae$J!6l?a16bbc8245c056bfb65db101b0c480c4abedbab11a7f3b7e27200642fdc2cf84d5644fcbe8b65fe2d0595071f8b3f8b7f909a8083e8a01ffd9474c6d26efe5b955f097363b8fbb626e5116db4445cd55ceb206a7d2d3d68104f85bf4e% S( Aۿ0c68dddace5c7255b45c5a2ad2ac91b82440ebbbf8c38c545e0432f3fdf346c9e731f300a624bbf98daedb05e4828ddf8f3649f0c64eb25d9ea7500a581f14fe47fcbd8531a7 1230d8580aedce8073359e88f449a25a37071edef0b8& msS12d6634350aa1c764c53dfb7f79675ffa18e078378040c694308a2329ea3c5f8379c22cc366c6370368459543ce6bffc0cb25ab5553d475762da0c5fa0e32f2e3f1022796212ecf54fb051a1cf1d729e50bc009bf8ad83b45bbf09005'iϿ39390e60bd7a4b7d455abef23e8d4cddb77a2ed815814ec9306f09502b6f7738461b23f40f5b492721db68bd70a5456491119a7dd6a992 69fbdd759568a689e8998c0aed3ccdc03174e0d399cbb8cbcae6818d511b445d90e5115876( p(a076b39f73c34fd865568589bd6078334bc582ec22487ccb00f2e71fc5833c5ed9e9ff6ed695aad271f3729ff79b158a28c7dbe2a476ef 4e387062b4493132ff7f4042a25582577c8e2f9d70a6d6cbb85172b7a4fc360268c2f2a045)]zά?2ab1d67c33f492b90eb392b3788cad6b01048c5344e39d8ff9b921de1f2c9755480521190665eeee d7170c15dc3fa923d84085f9ea52e8064d33f9a73aa93ed7c031fcbebc35f5f21d8c47ea86acf49363cbaa3c1477a3f859d21917*9-ل?67ac715cc8818b89454867eb01020ab92b1b180134d4476508bb2d8915eb5069688976cbd9cbe3553a71189f3c0694e fb8a85024271ddd91327795e54efcbe876f95de3bb801715b466940dbae4bd898c15930caedf4bb772c252e92+͑n?8d1ea62448209c362dac69a79fb139c0d32107b8e38a5ff28cc0332dcaafc38ad4adf9505475399ec48f781425954b994460ec04c477c0199c4834d5bc20d2ba4fa3fdd560f61d7bbda76ebd716769ab175cf864383d84fa67eca411d, ՙTd?4833ff579c771ff13788a1b6df196a16ec1f0d343232191c2ceee50d99458fafd9084ef9012a0f6bfe849d66e14e54dfe9843980894dedf87a36f4f4ddf043871598af3284c2 d8b17d40f7b8b2416be25d268d8e6be12db13302bfdc- .\B?ecf5251cefa9aceab5418b74f5276d27442472eb7862305fed82c8d057b16cd83c4c1f60958bb7d9d623d01c72ee10543f6ef4796cdd24b3ceb0e5a1230f96b999434c218019897aa215512cda7f2b977fd82309226b58679c78d92f7.EuZ7160d0d0645d8bbbae28a6d7ba85b0909432cde4a446c6d180c5a3b4c7778fc2c4a105a42ce7e8675bf9755b0eb99d192d1f78767eeb84a7dd159d118b86a632bcecbf2ebe21c29f71a8be2eb883df12614eb5b22dfbe2062b68aca02/  vZ +8f355f6652ffaa9a1d8ac38ddf31ca671071fe6cd824d2925d2cacd5ed10f776baff97f81fcc6af728e3230db56976593504ef350bcf1f7711d29f4ae074f2e0750628c983f2776ba85d7fccd0590a7b75d9e56a14185ee7e86e101800#~3edb09d12d1806a835d2940a223fbec7905bdf914cda92ed419210f7ab288ab464b8b5b00e37d69931542948e92d820f7154c12a7e50deea456577139f6f249fa5f28d90fe9f8e98475ce22e94527f6acd4d8237026b88d204abc448c13?6601652dcf0daa48ee87b7bf963a8ac513497d71a30f697450b0fd32a2e6998d3ea79dbef36cacfe 42f0fdfb014850bce9b453d27bad2 6381407628d75cd7b0bd5bd2578a938e3444ce469752a8b0fca4faaa0b6af0d5a6c3dcfafc2{3Naӿ0946ded5192e7ae4adefd78a52efca580273d6b5c65ae1a0cceb7d3d0cd60459a0122719bf609663c5534a352bbbc691843799bcdd9d769a6dbe4ffed03f191a19af0e06db264a39a628a02df4cd91470ca146e3d489b1872f2f4a6993 _S4ee442cd545f8eb24fe88966ca677a4f4771eddbfaeba32e67e756033c129b43cd6af1e29e831a094d68bdd57ee000c802ce2a84a49df69d6b590976aa2767ffc8a67d01e573dfc2345e94d0818e75568a25b39b1fa6b84ba282e99054 ȝ?bde5af41a98c5f13d1e5b3730ad0c038e4fbc3a0c4bad5b30bf76383c83c6683379fdb9d7c1ffdda8d04edd04295899a42656333ae82e8c7307cf28fa0e682e8765446665b155d007b629cb084249f1fb3bfefdae240c8df2930b35605-܈˳?a4b5248ab67e87d6120383e1197af6d59a08d3e9c22f0b0b600722923ae467f20bab3215cf6c79e98f64d1753758e37fa2d12bb7a8f38cbd7d7147ebce5c12875e0d1cb9db2c14b57a82f6017366ee3acb9703d3eb58ff7c767764eea6b2 Tݿ82d28cf0eefea60fb02a6bebdeef3a9b1156e2ae10db1e4bdde66b7644df7fd7eb4c8856fc8076d4651da726b3f59bcd2d1d19e0dd5a73bb146621f3b0faed37b6dcfc7d891c72c918568d9274face3c25eeb2f69ea5e348d7986f3a975{?1e8ddf07b52898379dfdeba664565c937d9d9e4c4d736374c0c20b5c89ec2400e9e508a07fd6ffe9f9c938d12c907f5dce392ce611a7dec9a0c4cf9ab41171bbe67ac041c35b8b7943af6ac523849af42bf7c85b75d8f76eb3edec03e8 H|?43f5b5348d2be37b19d6cc1780e385fdc7aa1033d32d09cb88fbcbd65471f0938e26f3bd4bf4ade5e64e784484e04c810b5d72be3005e4d13e81abf82f5e953a2fe28e1082924ce5ebd88c9a43e2b6d499c476cf42df109efed4bd9989poN:ҿ2eadd4aca404664e6cca53007bb2b1f58a3449d8b40b9b94a7b9d66e5725b3347952af8d5fbeff3de123092ff904682 2a7b983b22599085ee7bdbcf1ba882b46a5b11120c306b616e2ceab9ab5f80d98de070726d56395ea73db4915: J?df1531b9b0846832613f34e683a76b99dd681f172689d8eebc799455cf0bffa72832deee3e7ced72834c91a3c6902327afbd63446aaed4c5b8c586f3e56bdafb3630d55972b66baa3090d3fe60b79a67b53851730cd8fc345ff9b570d;>?a9deda2270c1e93cf30d7045ada9bdca427e6d8df28d7607b3ed655f872425256cd8deee210ea98b752631c8a3b94068c777c38f34dd078fff0cfa02ce01e9a4dfa0b8f008597ba305ad665e425b281d0c5a55c3377f17b12eca69c59<?ee3032b49b0d626089f3515f6acf39acc88d48ca51b9acd649eac9fcbd48d3069243a78d5ca596d0b6f38e2e8bf4f6fdb91729e5f4e0185ae0be2815ca49efd25fe1691dec446a34e726d5a6c9cde7b7bffa157aa2 601b0c6e55b65e= -sc62327b321c5d0376543ecf394338ec37df945f2dc842947aed16659fd9177df4bd83d5f64b485c1b532159f5fbc582baea9e5f0c29a6d200026f725f101efce5ef79ca8ac5b10157fdff8ea6ab3e6abfba502b708 a9b6ef64658735>ʡAu56708507b32b9e729b818a264d1472f81fba98e9de32820b9ab4ad87440f4dd85c56964183be684288b86a3b408fc26a92dae9fe8df3015a15dfe4cfab9921ffc1af9d96ab96dba727301810b1f7ec530799857816263e0936c171b94? pf73be8b52a4e375645860576c27755301faf7e9a9e671ecae8055c8f352ddc0c3bec368b4164d55a38c6505f503931c4744daaec7f511685af75d15d408fded36d03b4a9cd12fc701da9b855c24 922db02ac2c24e6ade73fc1f383d6@ 4a5504330424cbe1a6dddba938e24ce954177ef173c78e67e9abde250bf8c00c0eb6b63a2cad400538fdf14b75c105297fa83c51dc304a9ba83a0be6d510389498ba95b2ae3387a820b030ee7a93790a997283e46f3e8e7c24e6dfd27A  O377c1487920e2dadc8af9641c5ebe25eb24d2fd42fe51e2b7057b136e1a438382508d73efbf5cbab68be1fc407eed78725dd8cca1593e8c29f05597c59b67d3e04238054070bc491d9a083c535f909f00253a47fe0fbce0d1e721a9a4B3xaݿa475061abe6414799a09f5f21463943cd02459be501c6d4262f1d7cb1a520920d3747e6ffac0143246281a650fef1acab737d8402aa2bd32f9863817ab98140561d50e92f51bbcbdd38649d4fa24846eca5ffd9961f1eb3a8263b832fC'u4477b05465e5b5d001541a2a9c5a723f93f692ab65ef10e37367b8a1f96810d98b90f960e3485b3f3ed3a77a2ee4917a54486ee4f5419211b22ff5a8aeaa2b30469d0cb88a9b2d9b909533d855b661a83752db44afb706414d7e337a1DӜ?78fa8a4227440f0775e769a25a0aebd67701c4319de2e9d4c7e1f28d3f232795fc911ef900cc51381a27e551e6621687b14316c3ff715ac9bd9f170ef7b952ac2397920ab85994d84691ccdc3af cbd8b850ff1d53daf346abcee48eE fL?d3e1a12c6c53f3b6d46abc4cbe6cbea9ba3e2945fe16f1d24cc390a91aed7ffdda54a2efc58d6a5673651b70b806c6977314bd689c3415 5c9005cc15538a909cce14470c4b35864355584f5cdcc638a43a8fc4d35d0add6cbe1895d6F/}RG201991cad9870eab0fc041a29119b6b7cf0971e728518c809c667f3bb0038a9d756feebc5c0e7130da74537aa55de809ec067d20c8cc059b6b6cdaea87916d0a7091ba1f190de4e9b3d0cb24de4b182e77429ee12f8b9cb067f572eb1GmͿ1bcb2b08479b9d8b915950925db89aca66a7ffa0b7eb61b8c890b995e126622d7f768d0e92ed2f607d1dfba9390fd222b9ab664ab6bf84b295aec70257b96 e07af91ff296de4bf36f0bb276829e558e2ea72e33691ac967beaeba41aHGee2d5b5e3fe8c111ee87b51a953adc74269d5720161d68b49caf650224771db3273bc412328908586a808c621f3a96c5c0cf5f0300b53f719b19e79f93aff5edbfff8f85b6301 5e8863f3540b96a060721bc6eb0ecad3ce116bcc7f86I ([@ؿ2eabc8ecf2ac819ccc7d7bf0efc0d805ec3c7c6fd4a4dbb7d592e11bf302aa395b84359c0c1374a3f831fbbcb824d4d63c3d246e657fa28dcd1e02d8c5b2267a96c08586e347bf9d12c6925c90e803241953ae386e7a675f4e22f7aa5J 쌗7T?ae037760552848d31353ac3523446c24f468ed3265cf010211908628ce036c044eca823a5b903986809ea01a3e3c077614fa39a928a8d8bdad37cf02d43ce76bd74eb547e46362af55c970275b643e9ab24fe39cc7 69a090cf8bd370KS ?2f0a7583ab909d4c5c809459a43ca2992fc5fe3660d25f8e9275ea2619ebf66a8ad874b755826bc1b6434e34538de739f4f785f3183fe740cdd2646b4f75a3c265ea7f59a60c6aba4139a1b36a4 d840d34c4276916067e5151c49a4dL YT>ǿ43b29aceae0e52fcddfe418a0cdde112e7c4776c5f2f3b7c1d74f811e018fc59cdf96a73013d46f65ef3f72991991d94d66e4c99a0ab2467d27a9108d9564283efb2e37746515da52afc377fe66590d82b8a89d02f9e367684cceebd6Mu?0f53de14fc05524f6aac61a4b006245c31d5da9c0cb73d6629c211139dfe5451b59f9fcf862d6d3ee3d420fbc9362f06f418ff547a07d51a986090d7db11ab217a315782069d999ea6f1bae60cc37948307c3111625ce9c7dd15b1c9dN$ O?092142eb4f44250eca9c4c007ba8845f811fa1c2d1c6b42d306c170a44a07ddc9e0e9f3f784a47e5 4a138ec7d6ddfdc22d07bdc40717698347edacccbf7d12b67b0aecf1ea556ae3b5b11abaa73ae4e7766e325cfcfec41d6db69cffO `J:a8e338b4b0b87529994d82c5ed8a42a70562a942e0f4c901d43cf026b54f29052cac5c36635c90abf6c373727f29b7a33a834b67807eb83a599bcf8af7ab0b4da447465e3cecffeb35d90871a2d29de36ee445a916b6c96200f76134bP}Qhfc550af9ee621a893e6a485ffdce04ddb5e993f0053d1c17559dd2620d5ae5118535859c2aa768277b88b9b5d03e8407e8058def1f0d7532ab196b127e6eb559dac252a22202cd52eda570eff06375f0865beb95c5 cc21d8854c090eQ AaYؿf6ec75fee0b14c3d633d35b1d3c3a71db3fd9df436515bfef5db7d35822f03b12590d17251d8a479500d1bb098b5576cbbd2203d1386a299f415521ce8c321ba905d19ec94c3ae49678ddd51da693502cc90242cc27ed4a33ce276f92R E[1adf91ecc19476c22631a0b23dcbffe5d0bf006c653248c1081c79d949aad51e0df38d11b44982867213196c02279691f5d021bba1a18ce3269562d1fe1aab6b2568fc80e5a35c4256e1b90e45c928883350853ae0892236a2d083d68Sݧg?cdc4dc5c043395d551d4ad49fb9a804f56d44e3a621f3dcad1ed73be1855f52239314f1af987e135151fc5d13fa258e772052592e25af7c5515f9c55667f1a2087019abcf445dc1a55753132b98908c744d74b4081 3bcdbced9a109bT  s103b32995377f790d8178f1400bf6535ee6cc4908170c1347b5de086df154e8e05ed8448b9de6c2080c0d187e5d1db634508c778e613917ccf4ef9c7aa73dca77cfbb3c95a27caa350b25d7c9cf30096c1f2057fba5a39ed1c1ce8b6aU ݈ ?463f2f8336d38e9dd297d87e3499b0003c8c6039ec638027219862e49e7cf73ed536aacb472ee8e4b3effaa5d4c2aa5f4f4481e031c036aab95a0786874b8566b77331010bddc57eebef603bce2664718836012788a788902910a013bVM\?3e54629fbdd9586959ec95e6fe33a2700fea56a36c57f618a990d213fd242cce90c1520f3bdc2f5a9748d05df631059f28b7821c8c386b38235cc92e1af3c9e17b682fa44459 84e43bb2cd279464e3702d3350589fb91be1e29b04e6W  ʢ?68d00334e2803ab63964afd1f750d82cade861e09275fc1ec0278357aa482a2730cfd923ba5066f7646df091591c04319eb978e2cbe813ff42228f645b3d44a2b08e95b0669b41e49fc80927651b82467b3015623c25aaf4ed9380556X /uIEM 1cd5efb42294307e9e05ac996a3899ac4f3d10405aec4d60b9f5384ab589951f954c5494c4d6aa7bb4d41a6d208532d14707a98df31a9f59880c0df14d6352d583ffce3cfc4d634d2a7e13e62222aa0d3744c187b34908aa37eb0d9ecYmٿ4c92160657d7cb7f1172a598d3fe096998c9e7fcbb65db69179923fe5177f50d7e098de00ad88332 f16ec32a9d2751eff69ea90859dc4a8efdbc25c7ce8bd8e1b9b71d3eacc4964d689e0f78c8b176c3ce70c1886ceaa3122e101a24Z  D8cbc780bb361c0f5b9e403d5c0ff5985c6d359e85bf8d12b29600bfe99a1ced37d2a65df540b8f54 46325b2cefdbfb b5677b7883d487988d730de322833ef4e003e5f70f2a df3a92793773a197e4f76e6cdf0f8e982fdef9d35b51[ hfcd9c627123684e74cc014bd7053dfc61f6d245b41f140ea5c21b9bc47e1bb83244a938df770095ce5ee155943a53378e6bc00974e75034df42cdfd71a9958b40e55c2757d9aac67889706886432d39200e50c4edd b4c20634599626\=!u?45dc278cd5a503328cb022af9e7f319c9e522bd53e4e34359c31f9b8c51e2f35b6dffcb4bd01c786d1d8f407ba2e737eceff6dfcb9d5a7276c9bcbed19d7d2a32bbb64ce16894861afcb9fed531a5b14d7a3c60354f3df7079f2ebf57]k78c135b4ce2a2706cb6ba32a941f1e03a0aa2e09b45340f5c0518236c408550f3c43ab5e6aca33ad1495eeccfcd624eb51b1d8c16b6a9ecd14f5411a7a25ed1a53b5f2e31a7a83b9b483b774d4a79cd627548da05b8c6e8b33f3dbef1^ \ @12dc84e0267406908dd8fa24ca16cf21af0ccf6de8452868d24a011b167382f6bc673b200e1d440c96a4bc87f1dec9eea3b83da5eb66ca7d191af8ea25c3bb7e60df5b8241b91625ff117862aeec7a891b8a566878b67f8ff348b4007_ ?5667f64f10d144721188cefd5a439d18ba1c9194a8c52762c1a3856888c068bc49e2fa70dcbd008dc8cb6736b5c3921716e58383424de843caea6b9ff67956500e264ffaa87bdbe2c8c451810b5407f89c3eedf8069fab0c7262bf79c`mB^?149eed7a13ced9031fbd7a9047966028de0673e3a0bafcff75ec37bb28bd97a4e914d8a1af4500f2833630ef9404dbe5aaec1d282384a36ed6fef0dd1e4edeeb7ff0e383cbfb3437ace0b662dad72d5dd960d9a2d72f56b104c0accfba ݟl?65db964d5fe58ccffff28844524902281d29a971a43493d5bbbe4acf9583f1b05d9d502289514b4666f8b997071ca2aa24eacb851b5788855b5224c678f35254a38f6bf561592929ebe0c0f911cca2467ffbbba83ad6e1fdf24d231ffb bg0|?ac1917c5d9688d246fdf2b31fb0d45e315e5d9f7f385071803435439f15e619b684c25ce3b2e6acd95f461d8f965c7a2786a6656d0d81ee00623d52854d9c3f5bef5e994c19c77c05bb5db8247021cd26fcc9edb24 5184be02e37a2ac $T4/ad383931106c37f617cb55af97fb3619e2155a4dfa89d0a7980ad14a5705fb9b128e890c75a0ea70c12506113d9c1e881ccc38455a21e99e0b8c2e84bc65ad87f3456633bc551117db926bddf40e4ff034c368dc3dd4f08db89986c49>TREE@B@MF@HEAP`Pw (VERSION1.1 <@> HTITLE'Indexes container for table /data/table (CLASSTINDEX (VERSION1.0CpTREEG@U8HEAPhxX(YP (VERSION1.1SNOD8(EPEpG??Ajk m0jP 8TITLEIndex for index column (CLASSINDEX (VERSION2.1 0 FILTERS@ 8 superblocksize@ 8 blocksize@ 8 slicesize  8 chunksize  8 optlevel  8 reduction  (PH 8shuffledeflate^ 0CLASS INDEXARRAYSNOD0O(NIXhW@R@ (VERSION1.1 0TITLE Sorted Values 0 EXTDIM  (  8shuffledeflate^ 0CLASS INDEXARRAY (VERSION1.1 8TITLENumber of chunk in table 0 EXTDIM  (@ 8shuffledeflate^ 0CLASS CACHEARRAY (VERSION1.1 0TITLE Range Values 0 EXTDIM  @ 8shuffledeflate ^ (CLASSEARRAY (VERSION1.1 0TITLE Median ranges 0 EXTDIM  (@ 8shuffledeflate^ 0CLASS CACHEARRAY (VERSION1.1 0TITLEBoundary Values 0 EXTDIM  @ 8shuffledeflate ^ (CLASSEARRAY (VERSION1.1 0TITLE Start bounds 0 EXTDIM  @ 8shuffledeflate ^ (CLASSEARRAY (VERSION1.1 0TITLE End bounds 0 EXTDIM  @ 8shuffledeflate ^ (CLASSEARRAY (VERSION1.1 0TITLE Median bounds 0 EXTDIM  V 8shuffledeflate(Z^ 0CLASS LASTROWARRAYSNOD pLJEHT8PQxYX (VERSION1.1 @TITLELast Row sorted values + boundsAH 8shuffledeflateXb^ 0CLASS LASTROWARRAYsortedindicesrangesmrangesboundsaboundszboundsmboundssortedLRindicesLRHYX 0TITLELast Row indices@ 8 nelements d 8 nelements dTREE%TREE)$?A 0 DIRTY mpTREEq@8HEAPh8P (VERSION1.1P 8TITLEIndex for int0 column (CLASSINDEX (VERSION2.1 0 FILTERS@ 8 superblocksize@ 8 blocksize@ 8 slicesize  8 chunksize  8 optlevel  8 reduction  (`r 8shuffledeflate^ 0CLASS INDEXARRAYSNOD0y(xsXx@| (VERSION1.1 0TITLE Sorted Values 0 EXTDIM  (  8shuffledeflate^ 0CLASS INDEXARRAY (VERSION1.1 8TITLENumber of chunk in table 0 EXTDIM  (  8shuffledeflate ^ 0CLASS CACHEARRAY (VERSION1.1 0TITLE Range Values 0 EXTDIM    8shuffledeflate@^ (CLASSEARRAY (VERSION1.1 0TITLE Median ranges 0 EXTDIM  (  8shuffledeflate^ 0CLASS CACHEARRAY (VERSION1.1 0TITLEBoundary Values 0 EXTDIM    8shuffledeflate@^ (CLASSEARRAY (VERSION1.1 0TITLE Start bounds 0 EXTDIM    8shuffledeflate@^ (CLASSEARRAY (VERSION1.1 0TITLE End bounds 0 EXTDIM    8shuffledeflate@^ (CLASSEARRAY (VERSION1.1 0TITLE Median bounds 0 EXTDIM   8shuffledeflate8^ 0CLASS LASTROWARRAYSNOD vtpH~8`{X (VERSION1.1 @TITLELast Row sorted values + bounds@mH 8shuffledeflateh^ 0CLASS LASTROWARRAYsortedindicesrangesmrangesboundsaboundszboundsmboundssortedLRindicesLRHX 0TITLELast Row indices 8 nelements d 8 nelements dTREED$TREE)z$k m 0 DIRTY pTREE(@8HEAPhHP (VERSION1.1P 8TITLEIndex for int1 column (CLASSINDEX (VERSION2.1 0 FILTERS@ 8 superblocksize@ 8 blocksize@ 8 slicesize  8 chunksize  8 optlevel  8 reduction  (p 8shuffledeflate^ 0CLASS INDEXARRAYSNOD0أ(((X@ (VERSION1.1 0TITLE Sorted Values 0 EXTDIM  (  8shuffledeflate^ 0CLASS INDEXARRAY (VERSION1.1 8TITLENumber of chunk in table 0 EXTDIM  (  8shuffledeflate ^ 0CLASS CACHEARRAY (VERSION1.1 0TITLE Range Values 0 EXTDIM    8shuffledeflate@^ (CLASSEARRAY (VERSION1.1 0TITLE Median ranges 0 EXTDIM  (  8shuffledeflate^ 0CLASS CACHEARRAY (VERSION1.1 0TITLEBoundary Values 0 EXTDIM    8shuffledeflate@^ (CLASSEARRAY (VERSION1.1 0TITLE Start bounds 0 EXTDIM    8shuffledeflate@^ (CLASSEARRAY (VERSION1.1 0TITLE End bounds 0 EXTDIM    8shuffledeflate@^ (CLASSEARRAY (VERSION1.1 0TITLE Median bounds 0 EXTDIM   8shuffledeflateH^ 0CLASS LASTROWARRAYSNOD H8pX (VERSION1.1 @TITLELast Row sorted values + boundsPH 8shuffledeflatex^ 0CLASS LASTROWARRAYsortedindicesrangesmrangesboundsaboundszboundsmboundssortedLRindicesLRHX 0TITLELast Row indices 8 nelements d 8 nelements dTREED6$TREE) $0 0 DIRTY pTREE8@8HEAPhXP (VERSION1.1P 8TITLEIndex for int2 column (CLASSINDEX (VERSION2.1 0 FILTERS@ 8 superblocksize@ 8 blocksize@ 8 slicesize  8 chunksize  8 optlevel  8 reduction  ( 8shuffledeflate^ 0CLASS INDEXARRAYSNOD0(88X@ (VERSION1.1 0TITLE Sorted Values 0 EXTDIM  (  8shuffledeflate^ 0CLASS INDEXARRAY (VERSION1.1 8TITLENumber of chunk in table 0 EXTDIM  (  8shuffledeflate ^ 0CLASS CACHEARRAY (VERSION1.1 0TITLE Range Values 0 EXTDIM    8shuffledeflate@^ (CLASSEARRAY (VERSION1.1 0TITLE Median ranges 0 EXTDIM  (  8shuffledeflate^ 0CLASS CACHEARRAY (VERSION1.1 0TITLEBoundary Values 0 EXTDIM    8shuffledeflate@^ (CLASSEARRAY (VERSION1.1 0TITLE Start bounds 0 EXTDIM    8shuffledeflate@^ (CLASSEARRAY (VERSION1.1 0TITLE End bounds 0 EXTDIM    8shuffledeflate@^ (CLASSEARRAY (VERSION1.1 0TITLE Median bounds 0 EXTDIM   8shuffledeflateX^ 0CLASS LASTROWARRAYSNOD (H8X (VERSION1.1 @TITLELast Row sorted values + bounds`H 8shuffledeflate^ 0CLASS LASTROWARRAYsortedindicesrangesmrangesboundsaboundszboundsmboundssortedLRindicesLRHX 0TITLELast Row indices 8 nelements d 8 nelements dTREED#TREE)# @ 0 DIRTY pTREEH@8HEAPhhP (VERSION1.1P 8TITLEIndex for int3 column (CLASSINDEX (VERSION2.1 0 FILTERS@ 8 superblocksize@ 8 blocksize@ 8 slicesize  8 chunksize  8 optlevel  8 reduction  ( 8shuffledeflate^ 0CLASS INDEXARRAYSNOD0(HHX@( (VERSION1.1 0TITLE Sorted Values 0 EXTDIM  (  8shuffledeflate^ 0CLASS INDEXARRAY (VERSION1.1 8TITLENumber of chunk in table 0 EXTDIM  (  8shuffledeflate ^ 0CLASS CACHEARRAY (VERSION1.1 0TITLE Range Values 0 EXTDIM    8shuffledeflate@^ (CLASSEARRAY (VERSION1.1 0TITLE Median ranges 0 EXTDIM  (  8shuffledeflate^ 0CLASS CACHEARRAY (VERSION1.1 0TITLEBoundary Values 0 EXTDIM    8shuffledeflate@^ (CLASSEARRAY (VERSION1.1 0TITLE Start bounds 0 EXTDIM    8shuffledeflate@^ (CLASSEARRAY (VERSION1.1 0TITLE End bounds 0 EXTDIM    8shuffledeflate@^ (CLASSEARRAY (VERSION1.1 0TITLE Median bounds 0 EXTDIM   8shuffledeflateh^ 0CLASS LASTROWARRAYSNOD 8H8X (VERSION1.1 @TITLELast Row sorted values + boundspH 8shuffledeflate ^ 0CLASS LASTROWARRAYsortedindicesrangesmrangesboundsaboundszboundsmboundssortedLRindicesLRHX 0TITLELast Row indices 8 nelements d 8 nelements dTREED+TREE)*0P 0 DIRTY pTREEX@/8HEAPh2x3P (VERSION1.1DP 8TITLEIndex for int4 column (CLASSINDEX (VERSION2.1 0 FILTERS@ 8 superblocksize@ 8 blocksize@ 8 slicesize  8 chunksize  8 optlevel  8 reduction  ( 8shuffledeflate^ 0CLASS INDEXARRAYSNOD0"(X XX1@8- (VERSION1.1 0TITLE Sorted Values 0 EXTDIM  (  8shuffledeflate^ 0CLASS INDEXARRAY (VERSION1.1 8TITLENumber of chunk in table 0 EXTDIM  (  8shuffledeflate ^ 0CLASS CACHEARRAY (VERSION1.1 0TITLE Range Values 0 EXTDIM    8shuffledeflate@^ (CLASSEARRAY (VERSION1.1 0TITLE Median ranges 0 EXTDIM  (  8shuffledeflate^ 0CLASS CACHEARRAY (VERSION1.1 0TITLEBoundary Values 0 EXTDIM    8shuffledeflate@^ (CLASSEARRAY (VERSION1.1 0TITLE Start bounds 0 EXTDIM x  Om( 0` 0`xл '/5.-=3NYX=5oc7Cd@@@~,x  Om( 0` 0`xл '/La*wYDTW#vo郉   x  Om( 0` 0`xл {^ +,XL3SI]Q6N @@@ Lx  Om( 0` 0`xUB@%EBI%T;[bNߛ# Qgl׷`(y~d*b\?ohвɿ-=??|KIQ3Vtlۺ1ڥfd Ĥ-0;(Wc[E:B~o$t+TS{R1ֶT|wP&^(ת9βpUt-8EXk'2~㝿==@zNKzi|$x6ym<.}**QY"۷D-Q 3V`EXɏ#|#>R&KOǒCgGi*V鋶1iekFo|oOOО*R qp^okf |0#c ܄tVN0e҉P.^o#֎O7v12.}jMY[(oʜ7+$z[zPx  Om( 0` 0`x٭8 DoHI#R! - QlC:vq3K#+hYտtF#?O#uHvuqs$f\s,AXGd !"5[Chh^uGM{uu IZz#s:ceUlicݎkaFrvQ=ҏ# x<^t %{=I`\S82{W[*+Xp-3UQ%(߸GOgZLdXtP Z6AI 2'l=q"(G ɞ>_^8vBTdyZ\u1i$MyCvxP$ یw<q$TLϳc\;u~WFN[?Y|ϼRHV d0% OLK$z,>ǎpqfRckAߝZ#gȟbpBsWMurNʭE={'5x  Om( 0` 0`x  Om( 0` 0`xл '/ B2n-=SrTn=)f',   Xx  Om( 0` 0`xл '/La@aƥgʷVCP3!}    cx  Om( 0` 0`   8shuffledeflate@^ (CLASSEARRAY (VERSION1.1 0TITLE End bounds 0 EXTDIM    8shuffledeflate@^ (CLASSEARRAY (VERSION1.1 0TITLE Median bounds 0 EXTDIM  (1 8shuffledeflatex4^ 0CLASS LASTROWARRAYSNOD HH.8+3X (VERSION1.1 @TITLELast Row sorted values + boundsH 8shuffledeflate<^ 0CLASS LASTROWARRAYsortedindicesrangesmrangesboundsaboundszboundsmboundssortedLRindicesLRH 4X 0TITLELast Row indices 8 nelements d 8 nelements dTREED*TREE)x*@` 0 DIRTY GpTREEhK@@Z8HEAPh0]]P (VERSION1.1HoP 8TITLEIndex for float column (CLASSINDEX (VERSION2.1 0 FILTERS@ 8 superblocksize@ 8 blocksize@ 8 slicesize  8 chunksize  8 optlevel  8 reduction  ( ?@4 4 8shuffledeflate^LSNOD0PT(RMX \@W 0CLASS INDEXARRAY (VERSION1.1 0TITLE Sorted Values 0 EXTDIM  (  8shuffledeflate^ 0CLASS INDEXARRAY (VERSION1.1 8TITLENumber of chunk in table 0 EXTDIM  ( ?@4 4 8shuffledeflate^ 0CLASS CACHEARRAY (VERSION1.1 0TITLE Range Values 0 EXTDIM   ?@4 4 8shuffledeflate ^ (CLASSEARRAY (VERSION1.1 0TITLE Median ranges 0 EXTDIM  ( ?@4 4 8shuffledeflate^ 0CLASS CACHEARRAY (VERSION1.1 0TITLEBoundary Values 0 EXTDIM   ?@4 4 8shuffledeflate ^ (CLASSEARRAY (VERSION1.1 0TITLE Start bounds 0 EXTDIM   ?@4 4 8shuffledeflate ^ (CLASSEARRAY (VERSION1.1 0TITLE End bounds 0 EXTDIM   ?@4 4 8shuffledeflate ^ (CLASSEARRAY (VERSION1.1 0TITLE Median bounds 0 EXTDIM  [0^` 8shuffledeflate^^ 0CLASS LASTROWARRAYSNOD P@OXJH0Y8U (VERSION1.1 @TITLELast Row sorted values + boundsGH 8shuffledeflateg^ 0CLASS LASTROWARRAYsortedindicesrangesmrangesboundsaboundszboundsmboundssortedLRindicesLRH^X 0TITLELast Row indices ?@4 4 8 nelements d 8 nelements dTREEXPTREE)O*PEpG 0 DIRTY sxTREEv@8HEAPh@P (VERSION1.1 @TITLEIndex for object_1_7 column (CLASSINDEX (VERSION2.1P @TITLEIndex for object_1_0 column (CLASSINDEX (VERSION2.1 0 FILTERS@ 8 superblocksize@ 8 blocksize@ 8 slicesize  8 chunksize  8 optlevel  8 reduction ( 8shuffledeflate`>P^ 0CLASS INDEXARRAYSNOD0(X}hxX@  0TITLE Sorted Values 0 EXTDIM  (  8shuffledeflate^ 0CLASS INDEXARRAY (VERSION1.1 8TITLENumber of chunk in table 0 EXTDIM  ( 8shuffledeflate^ 0CLASS CACHEARRAY (VERSION1.1 0TITLE Range Values 0 EXTDIM   8shuffledeflate ^ (CLASSEARRAY (VERSION1.1 0TITLE Median ranges 0 EXTDIM  ( 8shuffledeflate^ 0CLASS CACHEARRAY (VERSION1.1 0TITLEBoundary Values 0 EXTDIM   8shuffledeflate ^ (CLASSEARRAY (VERSION1.1 0TITLE Start bounds 0 EXTDIM   8shuffledeflate ^ (CLASSEARRAY (VERSION1.1 0TITLE End bounds 0 EXTDIM   8shuffledeflate ^ (CLASSEARRAY (VERSION1.1 0TITLE Median bounds 0 EXTDIM  ` 8shuffledeflateH^ 0CLASS LASTROWARRAYxSNOD { zuH8 (VERSION1.1 @TITLELast Row sorted values + boundsrH 8shuffledeflatex^ 0CLASS LASTROWARRAYsortedindicesrangesmrangesboundsaboundszboundsmboundssortedLRindicesLRHX 0TITLELast Row indices 8 nelements d 8 nelements dTREE1(TREE)(oq 0 DIRTY xTREE@8HEAPhhP (VERSION1.1SNOD  @(0P0@`@ooqP @TITLEIndex for object_1_1 column (CLASSINDEX (VERSION2.1 0 FILTERS@ 8 superblocksize@ 8 blocksize@ 8 slicesize  8 chunksize  8 optlevel  8 reduction  ( 8shuffledeflateТ^ 0CLASS INDEXARRAYSNOD0((X@H (VERSION1.1 0TITLE Sorted Values 0 EXTDIM  (  8shuffledeflate^ 0CLASS INDEXARRAY (VERSION1.1 8TITLENumber of chunk in table 0 EXTDIM  ( 8shuffledeflate^ 0CLASS CACHEARRAY (VERSION1.1 0TITLE Range Values 0 EXTDIM   8shuffledeflate ^ (CLASSEARRAY (VERSION1.1 0TITLE Median ranges 0 EXTDIM  ( 8shuffledeflate^ 0CLASS CACHEARRAY (VERSION1.1 0TITLEBoundary Values 0 EXTDIM   8shuffledeflate ^ (CLASSEARRAY (VERSION1.1 0TITLE Start bounds 0 EXTDIM   8shuffledeflate ^ (CLASSEARRAY (VERSION1.1 0TITLE End bounds 0 EXTDIM   8shuffledeflate ^ (CLASSEARRAY (VERSION1.1 0TITLE Median bounds 0 EXTDIM  ` 8shuffledeflatep^ 0CLASS LASTROWARRAY0xSNOD HxHخ8 (VERSION1.1 @TITLELast Row sorted values + bounds`H 8shuffledeflate^ 0CLASS LASTROWARRAYsortedindicesrangesmrangesboundsaboundszboundsmboundssortedLRindicesLRHX 0TITLELast Row indices 8 nelements d 8 nelements dTREE#%TREE)% @ 0 DIRTY xTREEh@8HEAPhHP (VERSION1.1P @TITLEIndex for object_1_2 column (CLASSINDEX (VERSION2.1 0 FILTERS@ 8 superblocksize@ 8 blocksize@ 8 slicesize  8 chunksize  8 optlevel  8 reduction  ( 8shuffledeflate^ 0CLASS INDEXARRAYSNOD0(`pX@( (VERSION1.1 0TITLE Sorted Values 0 EXTDIM  (  8shuffledeflate^ 0CLASS INDEXARRAY (VERSION1.1 8TITLENumber of chunk in table 0 EXTDIM  ( 8shuffledeflate^ 0CLASS CACHEARRAY (VERSION1.1 0TITLE Range Values 0 EXTDIM   8shuffledeflate ^ (CLASSEARRAY (VERSION1.1 0TITLE Median ranges 0 EXTDIM  ( 8shuffledeflate^ 0CLASS CACHEARRAY (VERSION1.1 0TITLEBoundary Values 0 EXTDIM   8shuffledeflate ^ (CLASSEARRAY (VERSION1.1 0TITLE Start bounds 0 EXTDIM   8shuffledeflate ^ (CLASSEARRAY (VERSION1.1 0TITLE End bounds 0 EXTDIM   8shuffledeflate ^ (CLASSEARRAY (VERSION1.1 0TITLE Median bounds 0 EXTDIM  ` 8shuffledeflateP^ 0CLASS LASTROWARRAYxSNOD (XH8 (VERSION1.1 @TITLELast Row sorted values + boundsH 8shuffledeflate^ 0CLASS LASTROWARRAYsortedindicesrangesmrangesboundsaboundszboundsmboundssortedLRindicesLRHX 0TITLELast Row indices 8 nelements d 8 nelements dTREE$ϴTREE)Hh 0 DIRTY xTREEH@ 8HEAPhx(P (VERSION1.1 P @TITLEIndex for object_1_3 column (CLASSINDEX (VERSION2.1 0 FILTERS@ 8 superblocksize@ 8 blocksize@ 8 slicesize  8 chunksize  8 optlevel  8 reduction  ( 8shuffledeflate^ 0CLASS INDEXARRAYSNOD0(@PXh @  (VERSION1.1 0TITLE Sorted Values 0 EXTDIM x3q-WݲZ"#iL\H.YŮc.)jL_.k Sk&ߺ |iil4k[$Sןo UP *׮OcL@ILxJ;^d`4aToZ{?2WL ͅ?Y-c|i03A ` gĕs1SZNφ"T> TuT;m*~DM憖 YTk4ϡ#ri, Q͚f<@8^4R) ~Ce2QkVXx)OB*%M?z$cg}t]olP|G#g/#Nv  Tgw.,m Sqf - I@h"5j$?(+[S뼺Q174.0{H )ɪ e/w\=PmV K+7c-^uuLzr3gAwMǒ *ø8Ƿ {BW%3Pۊ':GC5\cвOyj;y'#lENW$죫X<΃`g lQ #uu'y% ơ.BBBJrw"^p܉pֹ33lP=kնÈwQjb7kmcm9h@:l4N SSz(hF*jp^sOwG\ZSZYQV"JeO eA0ఫwe-6n`&Vwx>+%$X>~#O:?__GOG -dYfYݍrNΌ^$;M4@Q->W7|x9op̿N`Aƒ-=05)[…Zq>2F^3>ba k*.C.B`Y @?7&wEeo묾yKp4l9cVbXRJi[e͌}(﬐FW]G&qxG<̓d'YX R+6Phu3*pEPB![SXkc.xHѢQ1r:]%TRSY1ÉƧzk=+R7oO#0.}xMZhM>ȾVw6w@qwsb`9_I& `@'x jx  Om( 0` 0`x0E'$@ $D!,MUA n# D4Gl#Sg_X1_+ ek'K͝@<"RG :azTمA]iE#AӣTzG?.!x  Om( 0` 0`x  Om( 0` 0`xۭ$1DoH`6?ew7p}u[>Va_W+|tF#*FQ}<_HX[#7{!DNm\,%Mk|C/a[8UɮL}i-9 %@vM[,}<H`@eN-yjiQҧbH`ә;d% GvY^ ,xS7ƣ{z~Ox  Om( 0` 0`x  Om( 0` 0`x  Om( 0` 0`x  Om( 0` 0`x  Om( 0` 0`x  Om( 0` 0`x  Om( 0` 0`  8shuffledeflate ^ (CLASSEARRAY (VERSION1.1 0TITLE Median bounds 0 EXTDIM  ` 8shuffledeflate^ 0CLASS LASTROWARRAYpxSNOD 0H8 (VERSION1.1 @TITLELast Row sorted values + bounds0H 8shuffledeflate^ 0CLASS LASTROWARRAYsortedindicesrangesmrangesboundsaboundszboundsmboundssortedLRindicesLRHXX 0TITLELast Row indices 8 nelements d 8 nelements dTREE!qTREE) 0 DIRTY  xTREE@8HEAPhP (VERSION1.1P @TITLEIndex for object_1_8 column (CLASSINDEX (VERSION2.1 0 FILTERS@ 8 superblocksize@ 8 blocksize@ 8 slicesize  8 chunksize  8 optlevel  8 reduction  ( 8shuffledeflate^ 0CLASS INDEXARRAYSNOD0H(X@h (VERSION1.1 0TITLE Sorted Values 0 EXTDIM  (  8shuffledeflate^ 0CLASS INDEXARRAY (VERSION1.1 8TITLENumber of chunk in table 0 EXTDIM  ( 8shuffledeflate^ 0CLASS CACHEARRAY (VERSION1.1 0TITLE Range Values 0 EXTDIM   8shuffledeflate ^ (CLASSEARRAY (VERSION1.1 0TITLE Median ranges 0 EXTDIM  ( 8shuffledeflate^ 0CLASS CACHEARRAY (VERSION1.1 0TITLEBoundary Values 0 EXTDIM   8shuffledeflate ^ (CLASSEARRAY (VERSION1.1 0TITLE Start bounds 0 EXTDIM   8shuffledeflate ^ (CLASSEARRAY (VERSION1.1 0TITLE End bounds 0 EXTDIM   8shuffledeflate ^ (CLASSEARRAY (VERSION1.1 0TITLE Median bounds 0 EXTDIM  ` 8shuffledeflate^ 0CLASS LASTROWARRAYPxSNOD hH8 (VERSION1.1 @TITLELast Row sorted values + boundsH 8shuffledeflate^ 0CLASS LASTROWARRAYsortedindicesrangesmrangesboundsaboundszboundsmboundssortedLRindicesLRH8X 0TITLELast Row indices 8 nelements d 8 nelements dTREE"OTREE)_ 0 DIRTY HxTREE@08HEAPhP (VERSION1.1SNODJJMuv0xȟ`'P @TITLEIndex for object_1_9 column (CLASSINDEX (VERSION2.1 0 FILTERS@ 8 superblocksize@ 8 blocksize@ 8 slicesize  8 chunksize  8 optlevel  8 reduction  ( 8shuffledeflate^ 0CLASS INDEXARRAYSNOD0p ( X@ (VERSION1.1 0TITLE Sorted Values 0 EXTDIM  (  8shuffledeflate^ 0CLASS INDEXARRAY (VERSION1.1 8TITLENumber of chunk in table 0 EXTDIM  ( 8shuffledeflate^ 0CLASS CACHEARRAY (VERSION1.1 0TITLE Range Values 0 EXTDIM   8shuffledeflate ^ (CLASSEARRAY (VERSION1.1 0TITLE Median ranges 0 EXTDIM  ( 8shuffledeflate^ 0CLASS CACHEARRAY (VERSION1.1 0TITLEBoundary Values 0 EXTDIM   8shuffledeflate ^ (CLASSEARRAY (VERSION1.1 0TITLE Start bounds 0 EXTDIM   8shuffledeflate ^ (CLASSEARRAY (VERSION1.1 0TITLE End bounds 0 EXTDIM   8shuffledeflate ^ (CLASSEARRAY (VERSION1.1 0TITLE Median bounds 0 EXTDIM  ` 8shuffledeflate^ 0CLASS LASTROWARRAYxxSNOD 8 H 8 (VERSION1.1 @TITLELast Row sorted values + boundsH 8shuffledeflate^ 0CLASS LASTROWARRAYsortedindicesrangesmrangesboundsaboundszboundsmboundssortedLRindicesLRH`X 0TITLELast Row indices 8 nelements d 8 nelements dTREEETREE)h 0 DIRTY (*xTREE-@<8HEAPh>?P (VERSION1.1PP @TITLEIndex for object_2_0 column (CLASSINDEX (VERSION2.1 0 FILTERS@ 8 superblocksize@ 8 blocksize@ 8 slicesize  8 chunksize  8 optlevel  8 reduction  ( 8shuffledeflate.^ 0CLASS INDEXARRAYSNOD0P6(4/X=@p9 (VERSION1.1 0TITLE Sorted Values 0 EXTDIM  (  8shuffledeflate^ 0CLASS INDEXARRAY (VERSION1.1 8TITLENumber of chunk in table 0 EXTDIM  ( 8shuffledeflate^ 0CLASS CACHEARRAY (VERSION1.1 0TITLE Range Values 0 EXTDIM   8shuffledeflate^ (CLASSEARRAY (VERSION1.1 0TITLE Median ranges 0 EXTDIM  ( 8shuffledeflate^ 0CLASS CACHEARRAY (VERSION1.1 0TITLEBoundary Values 0 EXTDIM   8shuffledeflate^ (CLASSEARRAY (VERSION1.1 0TITLE Start bounds 0 EXTDIM   8shuffledeflate^ (CLASSEARRAY (VERSION1.1 0TITLE End bounds 0 EXTDIM   8shuffledeflate^ (CLASSEARRAY (VERSION1.1 0TITLE Median bounds 0 EXTDIM  ?` 8shuffledeflate@^ 0CLASS LASTROWARRAYX=xSNOD 3p1,H;87 (VERSION1.1 @TITLELast Row sorted values + bounds)H 8shuffledeflateH^ 0CLASS LASTROWARRAYsortedindicesrangesmrangesboundsaboundszboundsmboundssortedLRindicesLRH@@X 0TITLELast Row indices 8 nelements d 8 nelements dTREE0TREE)') 0 DIRTY TxTREEW@e8HEAPhhpiP (VERSION1.1zP @TITLEIndex for object_2_1 column (CLASSINDEX (VERSION2.1 0 FILTERS@ 8 superblocksize@ 8 blocksize@ 8 slicesize  8 chunksize  8 optlevel  8 reduction  ( 8shuffledeflateX^ 0CLASS INDEXARRAYSNOD00`(^YXg@Pc (VERSION1.1 0TITLE Sorted Values 0 EXTDIM  (  8shuffledeflate^ 0CLASS INDEXARRAY (VERSION1.1 8TITLENumber of chunk in table 0 EXTDIM  ( 8shuffledeflate^ 0CLASS CACHEARRAY (VERSION1.1 0TITLE Range Values 0 EXTDIM   8shuffledeflate^ (CLASSEARRAY (VERSION1.1 0TITLE Median ranges 0 EXTDIM  ( 8shuffledeflate^ 0CLASS CACHEARRAY (VERSION1.1 0TITLEBoundary Values 0 EXTDIM   8shuffledeflate^ (CLASSEARRAY (VERSION1.1 0TITLE Start bounds 0 EXTDIM   8shuffledeflate^ (CLASSEARRAY (VERSION1.1 0TITLE End bounds 0 EXTDIM   8shuffledeflate^ (CLASSEARRAY (VERSION1.1 0TITLE Median bounds 0 EXTDIM  i` 8shuffledeflatexj^ 0CLASS LASTROWARRAY8gxSNOD \P[VHd8a (VERSION1.1 @TITLELast Row sorted values + boundsSH 8shuffledeflater^ 0CLASS LASTROWARRAYsortedindicesrangesmrangesboundsaboundszboundsmboundssortedLRindicesLRH jX 0TITLELast Row indices 8 nelements d 8 nelements dTREE!iNTREE)UpQS 0 DIRTY }xTREEp@N8HEAPhΝP (VERSION1.16P @TITLEIndex for object_2_2 column (CLASSINDEX (VERSION2.1 0 FILTERS@ 8 superblocksize@ 8 blocksize@ 8 slicesize  8 chunksize  8 optlevel  8 reduction  ( 8shuffledeflate^ 0CLASS INDEXARRAYSNOD0(xX@ (VERSION1.1 0TITLE Sorted Values 0 EXTDIM  (  8shuffledeflate^ 0CLASS INDEXARRAY (VERSION1.1 8TITLENumber of chunk in table 0 EXTDIM x[<HSH ;9{^ V#qylRϟc jf_c"b7ohğyx|!p6yκu/uD4P?"=W?/𷤇CgUZut8u;h1s"姗ꥸ%wdc5GWq8rr!FZxle?ېk/&5*/v.Y 89QQ٭CPYF5^Wx̸kݳ сf"^Aw_T0jg&;E(CKq 2i>oD-&İ|_- j[P5@L6o>${-SRšlTY]xoM@{ԎC3ڽ}0?'d:$C$bWQ˧-̆AZ۔q?oDYѿvr=2[gƶ0Ct@ gg9LЅ= {['N-4׃їC6Ծl/9rGKaBa5030qblhN~=QNWi2ٿ=sv!HKr}rg'͙P @1石w"U,4LQ$ Xp8{VN }!aaAzC G ֿU=~rW ̭ݨIs!&ܼWbFP5j!lnCul8-tX 04~<W2j(|ڮy$V^\`4,xI`@h"8n~wJpH / QgkzЮݽI#ۼ{^JHS84/lboL!IBY'!qx홉0 E$^:Xţb F|C>E!"}$5Z֏#GUUP_W%B0D&R;۹ H}n= DtB(@j_;K9Uy!{l5;c9r  Pj۬>΄=lT H ")bȫ}ξΠh23wˋLذ&:ª' S)n@ ߸;KXx{H'1B ur^{XЊ%mvJщ) tVL)è ]$PL.yaҩ"vHmߙ~ QfZ/*ulُpj YT@qr+~'u zDmC9;74ɄΎp1zOiEZ3٨c.Wm׎~N~`$EaVsy2#sRo'v/85K& LrwR' b qUP1 (OiƳ|a =za{!RG3*Pޙ;w;-]{3 Pne!Nf]- U&}%/K6tQHvo5n:aXwBXghfͅIz~dW@h=yK Zfun/vwlzx'ShɵW 0` XC=#;c.Gd(;ӂb,7IBp.Xԑ;5<\Cxj`w7q:?wSܜx[#;Eϐx 1cȇwYQmW++HH?9QS?yz/$c@; :/ҸȞ ,35]4X/'s{')%^`^-PKz g_nUZ>#Cú: 4Xcl~cIn۽ص QMH#|#R},3(jhMvCWa ܚu$@vXX2H$xX^ƟiMW˄N8؍0vo<~,Y rI] ޘmmjRS&ljzG lX^e*y0iHm] s2OҭsT0ws9V~TPhEPmx;nƝ5=Njh29bh&F]ezv揂m(rʇ "@" q~o$0v_܆3&So.<̛r91N`/Ám1{ Ez3"\oߚG?˸3 ( 8shuffledeflate^ 0CLASS CACHEARRAY (VERSION1.1 0TITLE Range Values 0 EXTDIM   8shuffledeflate^ (CLASSEARRAY (VERSION1.1 0TITLE Median ranges 0 EXTDIM  ( 8shuffledeflate^ 0CLASS CACHEARRAY (VERSION1.1 0TITLEBoundary Values 0 EXTDIM   8shuffledeflate^ (CLASSEARRAY (VERSION1.1 0TITLE Start bounds 0 EXTDIM   8shuffledeflate^ (CLASSEARRAY (VERSION1.1 0TITLE End bounds 0 EXTDIM   8shuffledeflate^ (CLASSEARRAY (VERSION1.1 0TITLE Median bounds 0 EXTDIM  ` 8shuffledeflate֞^ 0CLASS LASTROWARRAYxSNOD V`H>8 (VERSION1.1 @TITLELast Row sorted values + bounds}H 8shuffledeflate^ 0CLASS LASTROWARRAYsortedindicesrangesmrangesboundsaboundszboundsmboundssortedLRindicesLRH~X 0TITLELast Row indices 8 nelements d 8 nelements dTREE$EJTREE),P{p} 0 DIRTY xTREE@v8HEAPhFP (VERSION1.1SNOD@hh'')HQpQS({P{p}α 0@n..0^P @TITLEIndex for object_2_3 column (CLASSINDEX (VERSION2.1 0 FILTERS@ 8 superblocksize@ 8 blocksize@ 8 slicesize  8 chunksize  8 optlevel  8 reduction  ( 8shuffledeflate^^ 0CLASS INDEXARRAYSNOD0(X6@ (VERSION1.1 0TITLE Sorted Values 0 EXTDIM  (  8shuffledeflate^ 0CLASS INDEXARRAY (VERSION1.1 8TITLENumber of chunk in table 0 EXTDIM  ( 8shuffledeflate^ 0CLASS CACHEARRAY (VERSION1.1 0TITLE Range Values 0 EXTDIM   8shuffledeflate^ (CLASSEARRAY (VERSION1.1 0TITLE Median ranges 0 EXTDIM  ( 8shuffledeflate^ 0CLASS CACHEARRAY (VERSION1.1 0TITLEBoundary Values 0 EXTDIM   8shuffledeflate^ (CLASSEARRAY (VERSION1.1 0TITLE Start bounds 0 EXTDIM   8shuffledeflate^ (CLASSEARRAY (VERSION1.1 0TITLE End bounds 0 EXTDIM   8shuffledeflate^ (CLASSEARRAY (VERSION1.1 0TITLE Median bounds 0 EXTDIM  F` 8shuffledeflate^ 0CLASS LASTROWARRAYxSNOD ~ֺHf8F (VERSION1.1 @TITLELast Row sorted values + boundsH 8shuffledeflate.^ 0CLASS LASTROWARRAYsortedindicesrangesmrangesboundsaboundszboundsmboundssortedLRindicesLRHX 0TITLELast Row indices 8 nelements d 8 nelements dTREE"#FTREE)α 0 DIRTY nxTREE@V8HEAPh&P (VERSION1.1>P @TITLEIndex for object_2_4 column (CLASSINDEX (VERSION2.1 0 FILTERS@ 8 superblocksize@ 8 blocksize@ 8 slicesize  8 chunksize  8 optlevel  8 reduction  ( 8shuffledeflate>^ 0CLASS INDEXARRAYSNOD0(X@ (VERSION1.1 0TITLE Sorted Values 0 EXTDIM  (  8shuffledeflate^ 0CLASS INDEXARRAY (VERSION1.1 8TITLENumber of chunk in table 0 EXTDIM  ( 8shuffledeflate^ 0CLASS CACHEARRAY (VERSION1.1 0TITLE Range Values 0 EXTDIM   8shuffledeflate^ (CLASSEARRAY (VERSION1.1 0TITLE Median ranges 0 EXTDIM  ( 8shuffledeflate^ 0CLASS CACHEARRAY (VERSION1.1 0TITLEBoundary Values 0 EXTDIM   8shuffledeflate^ (CLASSEARRAY (VERSION1.1 0TITLE Start bounds 0 EXTDIM   8shuffledeflate^ (CLASSEARRAY (VERSION1.1 0TITLE End bounds 0 EXTDIM   8shuffledeflate^ (CLASSEARRAY (VERSION1.1 0TITLE Median bounds 0 EXTDIM  &` 8shuffledeflate^ 0CLASS LASTROWARRAYxSNOD ^HF8& (VERSION1.1 @TITLELast Row sorted values + boundsH 8shuffledeflate^ 0CLASS LASTROWARRAYsortedindicesrangesmrangesboundsaboundszboundsmboundssortedLRindicesLRHX 0TITLELast Row indices 8 nelements d 8 nelements dTREEBTREE)ڹ 0 DIRTY NxTREE @68HEAPhP (VERSION1.1.P @TITLEIndex for object_2_5 column (CLASSINDEX (VERSION2.1 0 FILTERS@ 8 superblocksize@ 8 blocksize@ 8 slicesize  8 chunksize  8 optlevel  8 reduction  ( 8shuffledeflate ^ 0CLASS INDEXARRAYSNOD0v( X@ (VERSION1.1 0TITLE Sorted Values 0 EXTDIM  (  8shuffledeflate^ 0CLASS INDEXARRAY (VERSION1.1 8TITLENumber of chunk in table 0 EXTDIM  ( 8shuffledeflate^ 0CLASS CACHEARRAY (VERSION1.1 0TITLE Range Values 0 EXTDIM   8shuffledeflate^ (CLASSEARRAY (VERSION1.1 0TITLE Median ranges 0 EXTDIM  ( 8shuffledeflate^ 0CLASS CACHEARRAY (VERSION1.1 0TITLEBoundary Values 0 EXTDIM   8shuffledeflate^ (CLASSEARRAY (VERSION1.1 0TITLE Start bounds 0 EXTDIM   8shuffledeflate^ (CLASSEARRAY (VERSION1.1 0TITLE End bounds 0 EXTDIM   8shuffledeflate^ (CLASSEARRAY (VERSION1.1 0TITLE Median bounds 0 EXTDIM  ` 8shuffledeflate^ 0CLASS LASTROWARRAY~xSNOD > H&8 (VERSION1.1 @TITLELast Row sorted values + boundsH 8shuffledeflate%^ 0CLASS LASTROWARRAYsortedindicesrangesmrangesboundsaboundszboundsmboundssortedLRindicesLRHfX 0TITLELast Row indices 8 nelements d 8 nelements dTREEhTREE)l 0 DIRTY .1xTREE4@S8HEAPhjVWP (VERSION1.1hP @TITLEIndex for object_2_6 column (CLASSINDEX (VERSION2.1 0 FILTERS@ 8 superblocksize@ 8 blocksize@ 8 slicesize  8 chunksize  8 optlevel  8 reduction  ( 8shuffledeflate5^ 0CLASS INDEXARRAYSNOD0V=(;6XZU@v@ (VERSION1.1 0TITLE Sorted Values 0 EXTDIM  (  8shuffledeflate^ 0CLASS INDEXARRAY (VERSION1.1 8TITLENumber of chunk in table 0 EXTDIM  ( 8shuffledeflate^ 0CLASS CACHEARRAY (VERSION1.1 0TITLE Range Values 0 EXTDIM   8shuffledeflate^ (CLASSEARRAY (VERSION1.1 0TITLE Median ranges 0 EXTDIM  ( 8shuffledeflate^ 0CLASS CACHEARRAY (VERSION1.1 0TITLEBoundary Values 0 EXTDIM   8shuffledeflate^ (CLASSEARRAY (VERSION1.1 0TITLE Start bounds 0 EXTDIM   8shuffledeflate^ (CLASSEARRAY (VERSION1.1 0TITLE End bounds 0 EXTDIM   8shuffledeflate^ (CLASSEARRAY (VERSION1.1 0TITLE Median bounds 0 EXTDIM xZە,9PnᘇaUȡ?GtT[=,/ '_s_x<~˫h,+U!DMd2 ~yUv.,zυ{wo-!_)~ǰaƲyiּ}h=-s:yVҹau̶^wHZ.`G;#;6~H?jCv{:r=M/)h|q ӶMiE,aMN Ҍ󷦇Ce+ ބN$gSg!'4 5 #o7CCA$DIx" >WCAۥѻj3ϰc(A OP>jAGq_[C!ӢvXw@3Jg*n Ruqm졞tB%)m *C!cSk%]^m'cmY87DԁG5r / twϭwx\L%?1׋V廡d>t.oCK=c5u-W^0wnhx",`k./}2ޖhB53+nփJ'1:\L*yQ jH r_[C!pˬ+hQ-d3̆q1t X?E1p2d=6| 'k!O;p~<~FT:8lCއk@]p "D c5w3%y:aIL&ٰ&30 A;kz< Mz E@Gc/K0z5f]:9,P 3' Z x"a?Gr6}ѽJ doڿ`$d88!4U` RphP ₈󷦇C #hnxyv6><7hj2$1BPR\qF?J_kjl O"7NIx[ۍ%+ P6?0plp}H7̨uA1.Wv~GWL@X7΍B/"p{SJgj6SZQ=Y"VcΣ ]foKJ[ӡs05Fm>|l¯9fye;ͣ֘zKg1Nqćp=</"0I8&]zc վG&;d#uRnVFX<ڨ:ZH4DU\k] œ==1Z'UQh Ψ˪0~JІ.Jzf]ES$ec8 E:T橳Waܤfvbwj7t"[ Xqߦ<N{#:IO-E˚6+8 om@yC϶Ҳ7ն>7p dfǏ}IOPۓwBhh `Rro vYM50?DV/ D^WDn7d`f) fA1\E3L濄HzC@{`NV =Ggtu!`eO1]!QqHu nO>'C!En_, J~o+D; >ubr3L({fAS?da`8/~oO>5gN#v:|%Ola_ }R _7ĂX5x@?_D`G-nJ0Y\s9πGȳ`[['9ÏmpA,h_~50g0+nΥN -!(p~<x[퍣=Rrl>/ܽ"Z$`71O07_䟨wzx|!.ɬ׊ c /1hG3o XLF[?ww"?: 3wG 6(Ņbn_߆rPgzt.\>EQDoM*ae 0Zɹc)A- _wE?$x[ە:P&C@8HBpgCt{\6???_o'o'&D?b/~"P=)gd;p GƷ͟}UmFMddd^[C!lm7NܙYf! :XH>1{j04,݉]AC8|@?߈ϰ$uJbR 4ׯ>4R}A޳4ث2׽g[wyo򷦇C`ys3ُ=.W5#G/7uIJs"U(k*A'Zu9Z>oD sU:tYS3{*!W#xQ,»>s̵8 f9GfgFo򷦇C MV~R@]m L9ksa4{1DJ 8A{|%@^LfKL=A쑠%sn HB/ZrA_oǮ{4 yW_7C_rrbϣ;@GPt 4 J!eB fR!'0Zp~<??d;l4Pg9ĀΞ/̐ OP@+Fo ooM? Y}l2/M|z'9KU`o6U*D,mS ccYi`I~{_;l{΃9Tӄ j`bV3s9A;{\` 5-!:l~$iRO ^ ;_ Щ3'y |A5jډ jiC8 >x4 ~ݥkF!1n|Z9R.@F>ֿ5=+S6'@ wd!k පlt7@py@`Ta0yG+a{DyT|{_! jW` 8shuffledeflate"X^ 0CLASS LASTROWARRAYTxSNOD :v83HR8> (VERSION1.1 @TITLELast Row sorted values + bounds0H 8shuffledeflateR`^ 0CLASS LASTROWARRAYsortedindicesrangesmrangesboundsaboundszboundsmboundssortedLRindicesLRHWX 0TITLELast Row indices 8 nelements d 8 nelements dTREE%mTREE)?q.0 0 DIRTY x[3;RRlC9KfsZ&RJ=x!?9s~n\./DY$m|&j2=ϩc恜`铈Xt|$ޜln."pI>f'Bґ;:vӳWV^:o#(>7Ι:L6i^=.8kMN8!8Ԣ°FҎFS7\4̦nO _NG{.*J^H]h&bp~=7@f|zz5\}УIQ].#^8N-Q'~p:z5piSZM+HPS5]~FY"D[v %}F%V9gnK5( E"FYR~]KG?ѴoG>oVWxQ lel`7"p Y g>;nMJtS{ B0(ݙkn O0%suhhEP/76ِrri_ԩRҶK׆ٗ( =_[{ t: ϴw~tc]H$)GG qk%)]rja xvDZD_/7"pR^%{O1;h=Nu Ckj9 x'ܨ qƟ"yށ>ߵu~ <+sCS!7ܓZ)PGm[ԣh32i EŸ&m_MO 1ÿ/'ccL~x;~-OŴD0 ^0 0޸󻦋Eon0ׇ`&>~ %H^ҚW#%f81?_Y|x  Om( 0` 0`x[ٱ#9 P&px)ޙ\v7( `'''6Ο!a#4_"sf!X=b;,2swY7@`Uw܄_Ǝ/C!O)Ì2t󘺦7X.+UI,:k]d Z$%nCH՞XN./j%zك̍p{+(XY#'gxӈk͂>R;s<,xk  "[ԱO).P`Mqq JoQ\nx8?߈@8j=l xp{=: ŀ w Ω14'] kR<iKVS>䠠ƙ1=Qˊ%J].sӮE{l_M|I3yAgjU9(nTTx߉@(NׇdGBx:r`i(֕dLޣ.> BfZ^Mߨ~<%щd0v[˖=)~kM:l(Mt6J p\pᛰ ^w"ޏ]{<*n \)v5ul2?5U=c* eE*@.5ιQ=~:ıd* &`vf]ݥoY)Xr{qx31*Q~|'eGF~{?ZmjD†>ֿމ@?׊5D~ߨ?܎ ?otBc!EolB\4 `H~ph>o7/ETGI'6r_(c/Ȉ'$~]n7|2O-t7 AJ\gHOG >=N?a9deda2270c1e93cf30d7045ada9bdca427e6d8df28d7607b3ed655f872425256cd8deee210ea98b752631c8a3b94068c777c38f34dd078fff0cfa02ce01e9a4dfa0b8f008597ba305ad665e425b281d0c5a55c3377f17b12eca69c59<?ee3032b49b0d626089f3515f6acf39acc88d48ca51b9acd649eac9fcbd48d3069243a78d5ca596d0b6f38e2e8bf4f6fdb91729e5f4e0185ae0be2815ca49efd25fe1691dec446a34e726d5a6c9cde7b7bffa157aa2 601b0c6e55b65e=-s c62327b321c5d0376543ecf394338ec37df945f2dc842947aed16659fd9177df4bd83d5f64b485c1b532159f5fbc582baea9e5f0c29a6d200026f725f101efce5ef79ca8ac5b10157fdff8ea6ab3e6abfba502b708 a9b6ef64658735>ʡAu56708507b32b9e729b818a264d1472f81fba98e9de32820b9ab4ad87440f4dd85c56964183be684288b86a3b408fc26a92dae9fe8df3015a15dfe4cfab9921ffc1af9d96ab96dba727301810b1f7ec530799857816263e0936c171b94?p f73be8b52a4e375645860576c27755301faf7e9a9e671ecae8055c8f352ddc0c3bec368b4164d55a38c6505f503931c4744daaec7f511685af75d15d408fded36d03b4a9cd12fc701da9b855c24 922db02ac2c24e6ade73fc1f383d6@ 4a5504330424cbe1a6dddba938e24ce954177ef173c78e67e9abde250bf8c00c0eb6b63a2cad400538fdf14b75c105297fa83c51dc304a9ba83a0be6d510389498ba95b2ae3387a820b030ee7a93790a997283e46f3e8e7c24e6dfd27A O 377c1487920e2dadc8af9641c5ebe25eb24d2fd42fe51e2b7057b136e1a438382508d73efbf5cbab68be1fc407eed78725dd8cca1593e8c29f05597c59b67d3e04238054070bc491d9a083c535f909f00253a47fe0fbce0d1e721a9a4B3xaݿa475061abe6414799a09f5f21463943cd02459be501c6d4262f1d7cb1a520920d3747e6ffac0143246281a650fef1acab737d8402aa2bd32f9863817ab98140561d50e92f51bbcbdd38649d4fa24846eca5ffd9961f1eb3a8263b832fC'u4477b05465e5b5d001541a2a9c5a723f93f692ab65ef10e37367b8a1f96810d98b90f960e3485b3f3ed3a77a2ee4917a54486ee4f5419211b22ff5a8aeaa2b30469d0cb88a9b2d9b909533d855b661a83752db44afb706414d7e337a1DӜ?78fa8a4227440f0775e769a25a0aebd67701c4319de2e9d4c7e1f28d3f232795fc911ef900cc51381a27e551e6621687b14316c3ff715ac9bd9f170ef7b952ac2397920ab85994d84691ccdc3af cbd8b850ff1d53daf346abcee48eEfL? d3e1a12c6c53f3b6d46abc4cbe6cbea9ba3e2945fe16f1d24cc390a91aed7ffdda54a2efc58d6a5673651b70b806c6977314bd689c3415 5c9005cc15538a909cce14470c4b35864355584f5cdcc638a43a8fc4d35d0add6cbe1895d6F/}RG201991cad9870eab0fc041a29119b6b7cf0971e728518c809c667f3bb0038a9d756feebc5c0e7130da74537aa55de809ec067d20c8cc059b6b6cdaea87916d0a7091ba1f190de4e9b3d0cb24de4b182e77429ee12f8b9cb067f572eb1GmͿ1bcb2b08479b9d8b915950925db89aca66a7ffa0b7eb61b8c890b995e126622d7f768d0e92ed2f607d1dfba9390fd222b9ab664ab6bf84b295aec70257b96 e07af91ff296de4bf36f0bb276829e558e2ea72e33691ac967beaeba41aHGee2d5b5e3fe8c111ee87b51a953adc74269d5720161d68b49caf650224771db3273bc412328908586a808c621f3a96c5c0cf5f0300b53f719b19e79f93aff5edbfff8f85b6301 5e8863f3540b96a060721bc6eb0ecad3ce116bcc7f86I([@ؿ 2eabc8ecf2ac819ccc7d7bf0efc0d805ec3c7c6fd4a4dbb7d592e11bf302aa395b84359c0c1374a3f831fbbcb824d4d63c3d246e657fa28dcd1e02d8c5b2267a96c08586e347bf9d12c6925c90e803241953ae386e7a675f4e22f7aa5J쌗7T? ae037760552848d31353ac3523446c24f468ed3265cf010211908628ce036c044eca823a5b903986809ea01a3e3c077614fa39a928a8d8bdad37cf02d43ce76bd74eb547e46362af55c970275b643e9ab24fe39cc7 69a090cf8bd370KS ?2f0a7583ab909d4c5c809459a43ca2992fc5fe3660d25f8e9275ea2619ebf66a8ad874b755826bc1b6434e34538de739f4f785f3183fe740cdd2646b4f75a3c265ea7f59a60c6aba4139a1b36a4 d840d34c4276916067e5151c49a4dLYT>ǿ 43b29aceae0e52fcddfe418a0cdde112e7c4776c5f2f3b7c1d74f811e018fc59cdf96a73013d46f65ef3f72991991d94d66e4c99a0ab2467d27a9108d9564283efb2e37746515da52afc377fe66590d82b8a89d02f9e367684cceebd6Mu?0f53de14fc05524f6aac61a4b006245c31d5da9c0cb73d6629c211139dfe5451b59f9fcf862d6d3ee3d420fbc9362f06f418ff547a07d51a986090d7db11ab217a315782069d999ea6f1bae60cc37948307c3111625ce9c7dd15b1c9dN$ O?092142eb4f44250eca9c4c007ba8845f811fa1c2d1c6b42d306c170a44a07ddc9e0e9f3f784a47e5 4a138ec7d6ddfdc22d07bdc40717698347edacccbf7d12b67b0aecf1ea556ae3b5b11abaa73ae4e7766e325cfcfec41d6db69cffO`J: a8e338b4b0b87529994d82c5ed8a42a70562a942e0f4c901d43cf026b54f29052cac5c36635c90abf6c373727f29b7a33a834b67807eb83a599bcf8af7ab0b4da447465e3cecffeb35d90871a2d29de36ee445a916b6c96200f76134bP}Qhfc550af9ee621a893e6a485ffdce04ddb5e993f0053d1c17559dd2620d5ae5118535859c2aa768277b88b9b5d03e8407e8058def1f0d7532ab196b127e6eb559dac252a22202cd52eda570eff06375f0865beb95c5 cc21d8854c090eQAaYؿ f6ec75fee0b14c3d633d35b1d3c3a71db3fd9df436515bfef5db7d35822f03b12590d17251d8a479500d1bb098b5576cbbd2203d1386a299f415521ce8c321ba905d19ec94c3ae49678ddd51da693502cc90242cc27ed4a33ce276f92RE[ 1adf91ecc19476c22631a0b23dcbffe5d0bf006c653248c1081c79d949aad51e0df38d11b44982867213196c02279691f5d021bba1a18ce3269562d1fe1aab6b2568fc80e5a35c4256e1b90e45c928883350853ae0892236a2d083d68Sݧg?cdc4dc5c043395d551d4ad49fb9a804f56d44e3a621f3dcad1ed73be1855f52239314f1af987e135151fc5d13fa258e772052592e25af7c5515f9c55667f1a2087019abcf445dc1a55753132b98908c744d74b4081 3bcdbced9a109bTs  103b32995377f790d8178f1400bf6535ee6cc4908170c1347b5de086df154e8e05ed8448b9de6c2080c0d187e5d1db634508c778e613917ccf4ef9c7aa73dca77cfbb3c95a27caa350b25d7c9cf30096c1f2057fba5a39ed1c1ce8b6aU݈ ? 463f2f8336d38e9dd297d87e3499b0003c8c6039ec638027219862e49e7cf73ed536aacb472ee8e4b3effaa5d4c2aa5f4f4481e031c036aab95a0786874b8566b77331010bddc57eebef603bce2664718836012788a788902910a013bVM\?3e54629fbdd9586959ec95e6fe33a2700fea56a36c57f618a990d213fd242cce90c1520f3bdc2f5a9748d05df631059f28b7821c8c386b38235cc92e1af3c9e17b682fa44459 84e43bb2cd279464e3702d3350589fb91be1e29b04e6W ʢ? 68d00334e2803ab63964afd1f750d82cade861e09275fc1ec0278357aa482a2730cfd923ba5066f7646df091591c04319eb978e2cbe813ff42228f645b3d44a2b08e95b0669b41e49fc80927651b82467b3015623c25aaf4ed9380556X/uIEM  1cd5efb42294307e9e05ac996a3899ac4f3d10405aec4d60b9f5384ab589951f954c5494c4d6aa7bb4d41a6d208532d14707a98df31a9f59880c0df14d6352d583ffce3cfc4d634d2a7e13e62222aa0d3744c187b34908aa37eb0d9ecYmٿ4c92160657d7cb7f1172a598d3fe096998c9e7fcbb65db69179923fe5177f50d7e098de00ad88332 f16ec32a9d2751eff69ea90859dc4a8efdbc25c7ce8bd8e1b9b71d3eacc4964d689e0f78c8b176c3ce70c1886ceaa3122e101a24Z D 8cbc780bb361c0f5b9e403d5c0ff5985c6d359e85bf8d12b29600bfe99a1ced37d2a65df540b8f54 46325b2cefdbfb b5677b7883d487988d730de322833ef4e003e5f70f2a df3a92793773a197e4f76e6cdf0f8e982fdef9d35b51[h fcd9c627123684e74cc014bd7053dfc61f6d245b41f140ea5c21b9bc47e1bb83244a938df770095ce5ee155943a53378e6bc00974e75034df42cdfd71a9958b40e55c2757d9aac67889706886432d39200e50c4edd b4c20634599626\=!u?45dc278cd5a503328cb022af9e7f319c9e522bd53e4e34359c31f9b8c51e2f35b6dffcb4bd01c786d1d8f407ba2e737eceff6dfcb9d5a7276c9bcbed19d7d2a32bbb64ce16894861afcb9fed531a5b14d7a3c60354f3df7079f2ebf57]k78c135b4ce2a2706cb6ba32a941f1e03a0aa2e09b45340f5c0518236c408550f3c43ab5e6aca33ad1495eeccfcd624eb51b1d8c16b6a9ecd14f5411a7a25ed1a53b5f2e31a7a83b9b483b774d4a79cd627548da05b8c6e8b33f3dbef1^ \ @12dc84e0267406908dd8fa24ca16cf21af0ccf6de8452868d24a011b167382f6bc673b200e1d440c96a4bc87f1dec9eea3b83da5eb66ca7d191af8ea25c3bb7e60df5b8241b91625ff117862aeec7a891b8a566878b67f8ff348b4007_? 5667f64f10d144721188cefd5a439d18ba1c9194a8c52762c1a3856888c068bc49e2fa70dcbd008dc8cb6736b5c3921716e58383424de843caea6b9ff67956500e264ffaa87bdbe2c8c451810b5407f89c3eedf8069fab0c7262bf79c`mB^?149eed7a13ced9031fbd7a9047966028de0673e3a0bafcff75ec37bb28bd97a4e914d8a1af4500f2833630ef9404dbe5aaec1d282384a36ed6fef0dd1e4edeeb7ff0e383cbfb3437ace0b662dad72d5dd960d9a2d72f56b104c0accfbaݟl? 65db964d5fe58ccffff28844524902281d29a971a43493d5bbbe4acf9583f1b05d9d502289514b4666f8b997071ca2aa24eacb851b5788855b5224c678f35254a38f6bf561592929ebe0c0f911cca2467ffbbba83ad6e1fdf24d231ffbbg0|? ac1917c5d9688d246fdf2b31fb0d45e315e5d9f7f385071803435439f15e619b684c25ce3b2e6acd95f461d8f965c7a2786a6656d0d81ee00623d52854d9c3f5bef5e994c19c77c05bb5db8247021cd26fcc9edb24 5184be02e37a2ac$T4/ ad383931106c37f617cb55af97fb3619e2155a4dfa89d0a7980ad14a5705fb9b128e890c75a0ea70c12506113d9c1e881ccc38455a21e99e0b8c2e84bc65ad87f3456633bc551117db926bddf40e4ff034c368dc3dd4f08db89986c49[!TREE$HEAPX!indexH  HTITLE'Indexes container for table /data/table (CLASSTINDEX (VERSION1.0+&pTREE)@388HEAPh;;P (VERSION1.1SNOD#"K"k$+MP 8TITLEIndex for index column (CLASSINDEX (VERSION2.1 0 FILTERS@ 8 superblocksize@ 8 blocksize@ 8 slicesize  8 chunksize  8 optlevel  8 reduction  (* 8shuffledeflate^ 0CLASS INDEXARRAYSNOD0[2(0+X :@5@ (VERSION1.1 0TITLE Sorted Values 0 EXTDIM  (  8shuffledeflate^ 0CLASS INDEXARRAY (VERSION1.1 8TITLENumber of chunk in table 0 EXTDIM  (@ 8shuffledeflate^ 0CLASS CACHEARRAY (VERSION1.1 0TITLE Range Values 0 EXTDIM  @ 8shuffledeflate ^ (CLASSEARRAY (VERSION1.1 0TITLE Median ranges 0 EXTDIM  (@ 8shuffledeflate^ 0CLASS CACHEARRAY (VERSION1.1 0TITLEBoundary Values 0 EXTDIM  @ 8shuffledeflate ^ (CLASSEARRAY (VERSION1.1 0TITLE Start bounds 0 EXTDIM  @ 8shuffledeflate ^ (CLASSEARRAY (VERSION1.1 0TITLE End bounds 0 EXTDIM  @ 8shuffledeflate ^ (CLASSEARRAY (VERSION1.1 0TITLE Median bounds 0 EXTDIM  {9 8shuffledeflate<^ 0CLASS LASTROWARRAYSNOD /c-(H#783<X (VERSION1.1 @TITLELast Row sorted values + bounds$H 8shuffledeflateD^ 0CLASS LASTROWARRAYsortedindicesrangesmrangesboundsaboundszboundsmboundssortedLRindicesLRHs<X 0TITLELast Row indices@ 8 nelements d 8 nelements dTREE{MTREE)'NK"k$ 0 DIRTY xUB@%EBI%T;[bNߛ# Qgl׷`(y~d*b\?oh 64KB. - Blue ball if the file size > 16KB. """ from time import localtime, strftime from os import listdir from os.path import ( getsize, getmtime, isfile, join, splitext, basename, dirname, ) from traits.api import ( HasPrivateTraits, Str, Float, List, Directory, File, Code, Instance, Property, cached_property, ) import traitsui.api from traitsui.api import View, Item, HSplit, VSplit, TabularEditor, Font from traitsui.tabular_adapter import TabularAdapter from pyface.image_resource import ImageResource from io import open # -- Constants ------------------------------------------------------------ # The images folder is in the same folder as this file: search_path = [dirname(__file__)] # -- FileInfo Class Definition -------------------------------------------- class FileInfo(HasPrivateTraits): file_name = File() name = Property() size = Property() time = Property() date = Property() @cached_property def _get_name(self): return basename(self.file_name) @cached_property def _get_size(self): return getsize(self.file_name) @cached_property def _get_time(self): return strftime('%I:%M:%S %p', localtime(getmtime(self.file_name))) @cached_property def _get_date(self): return strftime('%m/%d/%Y', localtime(getmtime(self.file_name))) # -- Tabular Adapter Definition ------------------------------------------- class FileInfoAdapter(TabularAdapter): columns = [ ('File Name', 'name'), ('Size', 'size'), ('', 'big'), ('Time', 'time'), ('Date', 'date'), ] even_bg_color = (201, 223, 241) # FIXME: Font fails with wx in OSX; see traitsui issue #13: font = Font('Courier 10') size_alignment = Str('right') time_alignment = Str('right') date_alignment = Str('right') big_text = Str() big_width = Float(18) big_image = Property() def _get_big_image(self): size = self.item.size if size > 65536: return ImageResource('red_ball') return (None, ImageResource('blue_ball'))[size > 16384] # -- Tabular Editor Definition -------------------------------------------- tabular_editor = TabularEditor( editable=False, selected='file_info', adapter=FileInfoAdapter(), operations=[], images=[ ImageResource('blue_ball', search_path=search_path), ImageResource('red_ball', search_path=search_path), ], ) # -- PythonBrowser Class Definition --------------------------------------- class PythonBrowser(HasPrivateTraits): # -- Trait Definitions ---------------------------------------------------- dir = Directory() files = List(FileInfo) file_info = Instance(FileInfo) code = Code() # -- Traits View Definitions ---------------------------------------------- view = View( HSplit( Item('dir', style='custom'), VSplit( Item('files', editor=tabular_editor), Item('code', style='readonly'), show_labels=False, ), show_labels=False, ), resizable=True, width=0.75, height=0.75, ) # -- Event Handlers ------------------------------------------------------- def _dir_changed(self, dir): if dir != '': self.files = [ FileInfo(file_name=join(dir, name)) for name in listdir(dir) if ((splitext(name)[1] == '.py') and isfile(join(dir, name))) ] else: self.files = [] def _file_info_changed(self, file_info): fh = None try: fh = open(file_info.file_name, 'rU', encoding='utf8') self.code = fh.read() except: pass if fh is not None: fh.close() # Create the demo: demo = PythonBrowser(dir=dirname(dirname(__file__))) # Run the demo (if invoked from the command line): if __name__ == '__main__': demo.configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/examples/demo/Applications/converter.py0000644000175100001730000000636000000000000023743 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Tiny application: convert length measurements from one unit system to another Select the input and output units using the drop down combo-boxes in the *Input:* and *Output:* sections respectively. Type the input quantity to convert into the left most text box. The output value corresponding to the current input value will automatically be updated in the *Output:* section. Use the *Undo* and *ReDo* buttons to undo and redo changes you have made to any of the input fields. Note that other than the 'output_amount' property implementation, the rest of the code is simply declarative. """ # FIXME: Help is broken in QT from traits.api import HasStrictTraits, Trait, CFloat, Property from traitsui.api import View, VGroup, HGroup, Item # Help text: ViewHelp = """ This program converts length measurements from one unit system to another. Select the input and output units using the drop down combo-boxes in the *Input:* and *Output:* sections respectively. Type the input quantity to convert into the left most text box. The output value corresponding to the current input value will automatically be updated in the *Output:* section. Use the *Undo* and *ReDo* buttons to undo and redo changes you have made to any of the input fields. """ # Units trait maps all units to centimeters: Units = Trait( 'inches', { 'inches': 2.54, 'feet': (12 * 2.54), 'yards': (36 * 2.54), 'miles': (5280 * 12 * 2.54), 'millimeters': 0.1, 'centimeters': 1.0, 'meters': 100.0, 'kilometers': 100000.0, }, ) # Converter Class: class Converter(HasStrictTraits): # Trait definitions: input_amount = CFloat(12.0, desc="the input quantity") input_units = Units('inches', desc="the input quantity's units") output_amount = Property( observe=['input_amount', 'input_units', 'output_units'], desc="the output quantity", ) output_units = Units('feet', desc="the output quantity's units") # User interface views: traits_view = View( VGroup( HGroup( Item('input_amount', springy=True), Item('input_units', show_label=False), label='Input', show_border=True, ), HGroup( Item('output_amount', style='readonly', springy=True), Item('output_units', show_label=False), label='Output', show_border=True, ), help=ViewHelp, ), title='Units Converter', buttons=['Undo', 'OK', 'Help'], ) # Property implementations def _get_output_amount(self): return (self.input_amount * self.input_units_) / self.output_units_ # Create the demo: popup = Converter() # Run the demo (if invoked from the command line): if __name__ == '__main__': popup.configure_traits() ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1684768028.0398073 traitsui-8.0.0/examples/demo/Applications/images/0000755000175100001730000000000000000000000022622 5ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/examples/demo/Applications/images/GG5.png0000644000175100001730000000240500000000000023713 0ustar00runnerdocker00000000000000PNG  IHDRfg7 pHYs  ~IDATxK"m.:ZB:1ATL%ftl*%T_@CJ((: :#˾ϳyv|O?\̑_|&"i~bDtBDDz&M6 M zݮ4D^=:MCD nooqzzJ#UU!SX,86~ dZ-ejl²,uijLӄaPUIƞnFvvV˲PVa&J4dbBa #N#qLړh4tnJ0pxx]ױ' G.C.HRəzNDhZIyxx@T?*"ɤ c! "`RJ_. cuu6θfpvv68+LJH$l愈PVa#_gHQA0'N'"1&*GϐiBp6̣D=X4gfØutQUz]. affffbF`$0M`&0L i4a0M`&0L i4a0M`&0L i4a0M`&0L i4a0M`&0L i4a0M`&0L i4a0M`&0L i4a0M`&0L i4a0M`&0L i4a0]"?PE`FrޝG"B:FVCX, m'' AuB/3*|>m'`& Àar#_ghgg^d҆tŒN4 @Ӹa&0}pppt: ]EQx<b1n{8aH$HRPULfi(^/~nkLlnnnH$xx^\.looFIvx"BӁ(|XZZfff!"VVVN3x|=)?LQq"wa@D }b@k=0Ɔn lIENDB`././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/examples/demo/Applications/images/TFB.png0000644000175100001730000000305500000000000023746 0ustar00runnerdocker00000000000000PNG  IHDRfg7 pHYs  ~IDATxOwq?g 7Yr]2R!eFh`8pHmSUl'-Pj.gQly?^cwwsΕ㬭UGbDXZzN]_@NG4WqҊ8[ >oձ|ɱ&Y{ GY)?Ps-͕S \:̀p@@l : sh; H:%7H_Vt_'& _ m32' ;(+\ 842mzC_jEljEn rsȐ?paߔJ|g0NzFJ a#Js'ia4ԟQVJ# )ݵF (ldGId]L X0y;2яȧGߥW5P=&_6ҷ 2.?ِ>:AJ ´Ab6Nu0>'w\ҹs )]/w{Td3~fM;̫0 Na0BS)Fh #4Ma0BS)Fh #4Ma0BS)Fh #4Ma0BS)Fh #4Ma0BS)Fh #4Ma0BS)Fh #4Ma0BS)Fh #4Ma0BS)Fh #4Ma0BS)Fh #4Ma0B{} 0 SB?/\9ìlCF\Id]|{ 3 F-e;LqVV` xfLI1?SLcćz9{"f&?7:r;Io]ƻO{'sٳEN3>o=f} _HbsёK~l#I)|uw\y8;L 0>IdP+#pf6Rv$ )/ߤ6Q<l; eX[[وt2D[ d;_:۩Y&x7ӹ& 67ot7/^ n##2Ӌ2=Ý$X =t]߉]׿: h 6b{A{1j9w7_~/ C8H?@1Agf5;HI\[@~?_TPd f`ar7@A?CnX|b`ec` ,@+ h{@q77v? gк`+i@ wV MΉ1@T7 'Ö@@Ԁ>mF gd&o@`ydDy /+< P@0ـbOف+@`Xajbvـg9L@ @Y8()<@ iQ^ @ d?` 33$ʀbsgo Adobe Fireworks CS3 2007-10-06T22:15:44Z 2007-10-06T22:30:22Z image/png mprVWxX]hWٽww'; aUA߬D(UĢ>tBJyԧ%/jEhXK_%%jsfvl|np~33ιߜ3?{"QV]T9syÅ._eXr,XYU &Cu|_u|_ %"1 ħ|wrN<8`fwqll&# {0>?%K&+6ōυl9>((&l85C]Fp|Iw x+-5QF㷷JAuJrpy>֨+3fo8\FLpC\OIvs3)IXBB3ؒ6/lF] cA8$)B_iؕ:k Z>%o'*g;#sYoJ)>Gi}nsh$Qc2"WuJAr 䞟qZ\"ꀈ[ss߲XR] qb.UQ|S&P<__ DLIq*k¬u5y7ճB ?- ʄWz\4!߭ցsZBߦAu|_#Fqs/^3&_pȰ :}g3ٙGo9tHaðwal^Hf@""3"2232+AvNM32#}"2r/Ze}Ekgti5ҵ۵9> [K۩79nmʸ~4BW Co>\m?k+{ƒ5zi cmdvwЕ1bxJ[.P^A-vnjx ބ6\kF~Pc~5Zp%'GI%;9lVV貆6&ЁC&Z dg2v]3kB̬iavb8A0kg `3Tb ؘ1jNx:FxjvKTKwnc!BR׌H#h!K`"€š@S84%I䫝a\%WCtqd(S@!ŒB^sDUbi |eNZ6h?qFW)@ l`~ 'gO9n4,ȺF~[̤:@6D (ơd/rG%Jk] iQqs=%N:h:NICFѥF=`&{džʹ/,li UɱbäNZevIHll% Y7>۠hnW j$ &bx@K_cK#J ptHITJ+8q,nVDVy::K))3tJ'hYmZhp+n:ppЏ$@;Y<iI+=fTz-7E@Î4Btλ*gpʶ֣EGqj-hEJ;.17`4([ Q<3NS |eԆr鬟w ɳ/Pki>zOL(Gٸa#O`uƝl6jam:ӄCj[mvr&EV!HbV>,+}ynI>6xb?L\aEİŠRM$}eBqMVv;sGT=`oXuěɥ6/%>KafQHaBɤ/M`Jh#´WS:{ib^!;8mg[ 8˻IhXDJeNQHD RI)|o>୫YW2HA@7INx"zhG-\j'řJGeY\*OF'{:G;,. {]éM(Q fx: HeΎ̗ 9#hO! TNc~ybR-Ml.ү~JM,:#u2st73vM/$ASn8!}qxĔ_o=)J6=Ƿ*d~H V, J5/gOZG~EI]nfx,݇~tKf,R NHa#J]~ΟIz,Ed/H㛜ԗɠ/qv5q~cj6]~:٠a%v#:XHTI'0|e6_tM'84 (%g;<*OB=אU+A(5V- >nrn#v8y +ԫ*V8 I{E2Dž01nl_6'p6$F#JC.lpyyAiQljlh:iȵMrLJqő~MjQlMIߢQ{z}:9S8UG2_!eU4|]#ķMKlbc.W|͜kt]r/2Z(W!|s {o-LMJ{瑪#28!Mћ&:0zkbeK8ˢg^;axI.HDղ ai[u`w`G糱# neV,GqJU-[ @q>j[[i.}.7>8yGUQbl^ ^ UL=XSa'gG+OK ~GWyp%ggYcX9yٸ5 2n*~ӏkbeܐpn(4/Fk+eb#_p=[#-uCn +T"7`*& (iZ*ٵ5TN h3\6 9m"RiU2/x(m4)jMݩ#{kUNV '^fOYW[rcQ(/b lw䋕qʘZ@6RXs`q>oSQ>eUgCTdd͎W\bt(82-6 Zu 9h$]E:։]/;cw (Jg`9Ud3M&["WT +10|k5VKn^;W^+9W՚zխX8t'93jg#aY9&̋Pvl:eXGnQLڦ ([zF 5p!ΠgZQB&l-хn-ѵ@;<ÇPQ$:BTcn#7!79d$Iq8!W.:SiUGw~~5dg=Nټc7 snCxh"׍J׾@ޢx{Kw622=a\UZ1,1 XF{ Mt5tdk,Ժ3.pDkJJ>GaΌg7prb{'ݳa3lʢ#G]xSx }pm؋Yr >ftsa8}!c巐+H=_B9t;(󌾎c+˒"O2z o Trz߃g_,2ւ~%t}c(a :nCEP[ O;Km )AK{X" ˯'-ZӐEXA"%C0$-y)K^% \@Nr!.stlP]"gqK:` :y+sa a|:WXBOBW ]XBmD>^XBOB˶Ѕ.,B;liyXh6Ӷva ]l }/ADu8]XBOB;.,ta} 7rUXʻ:_wHgXyWXEʻ:Y:y+ XyWXBoʻB/.V:XyWŰʻf6[f+ 6XyWF|9+ XyWXBzX`'BZouE>WZK,i~S;i5_lXW |($l#7cTd%uEԫqVPZjYш}=*mߑw.~#ILt,*n$*cޑh*FE$moYh ,t 0DcBE _ n`O~~_b Erȍn O/[Q<mt}\TYk}crS>qٓe <قp|,>C#QƟ&KQר#N_]Lpn;.]CQ L;߂B gz:}+oɯކ_FGoR0' sl ,,A٭NWٺL/tgaj%6}a8d}vGk̯A i! _#*&4Gnc頙 Q;c_{OsW2|_cAw">x{., 8F&H,dA"`Ɛ̺fFl]t}(By s倪by Zr'?AHmyFuV WVi.W^%tcp-"#q}tMQE=O_jrFT= I5DB[z䌎A%ncB>UU_{J,k) !a-uL[[h˷Y>V҅.YMx=4ǹ`p=x p_3(3&Zs#L?v8~^N%^pT dq݉2>BH\߉1/ ,㉒FK\;VA)tq`=X{W\h%:aZK‡`\KƢ._7zE$|`<@w.WϽ: g{/T*KŷQE*sK~/KŚ~^`}[xA ,)$oE<5l]}g8%N-qٯbfm-B>P$.{7$ѣ:ʚ?|Dejx_"z&z>x{|7痍=ܾmhtsz\wɹ!ܣxMm镕56"3@% Ϛ?]z&?Cq:n{gUv =hosGNmZ{߶Ɲ=h_ml# +8’~k9t'>쯯>"`wwKgtYaH9<o d7vqoek>qCŝ]t:6[O:} Be0  Ѱ :$d  Stj6Ἵrģp|ct{ö iC5 mc$>7q][_Bq>w#i u.+A D76yؕ3BgYm5h{]Thvxr`^k`>5肍;nn_Wj_*bV j{To6e^Vmtr*MH먀YfݩWDW \]d2gUʦ[s[7ѡ^V!T{k1^n:f-m[gyN@ kesՇ@!VTz٭T;DĶ]bS"rZA%SE*nRe-=Zd"HVkW*NT*eqܪK8g =J 5FiNr "HvݬP"VIWqTy*x:%bxx"F9DlGuHj!"W:D:<ԃj6e:ut*VVZj4'Ab=ڡJRuQ TٸzF1i@VfY:TE*lX.) M*2N٬Yש9"sQK0"dsRqD22˩A@ڵ2QJ>AO>!{eS*6!')'EX!5!e5~6au5>{`K ^JV/^{KgU#() ed>/tc hifu@ c|=h۾*S>ob-M `k&a ֞h9A4>Cxy sK<(КpN͑F_ڰ7)^ kGӉD@6}N&dftJ܃+H)XCm>ZwP+AߧꙏPУ3Dl$<}a«9ʍ]̓ENІSVK1c]m.Z@EtXי5yHm=FF~P#yQ> i!~t; 8ЀPl?8޾4B Sk)e>*٣ޝskOkK߇sZ{ %;Zq/M~ ϡm=Ft(Mϙ_ bG։v ӕA{ȎQE?խ!fEHkkO8ge_ѳ"ϔ|z|Y=VňϨ+/dVTO)kczDs_:.}GH\_Bn#x#DfgO^o!'[tL:$Cc#h?~"xg=*P)Ex!k~kB-#Ve5#Tac3U۵Stx׉xw MX&)H)Gebݡ- =i)@a~091LæVlCf#FsGQoiY3U_(kFb '&pm*Ϣ3@Rp? ?Fyxy6¨bM9 Klr%,x[.J\<.3HO7uM J6=: ea%̓,IgLՀ Ѓw}XًRcȁT.,]lv~t%"B#+t"Aʄ?RQ-dDɎE.QlzLL)G@IqN L(9ɝJ(Owa7@xTw>7%Gj>98<69 .ȯ(d'٭WT>pFgkz{|0f;iW@LntM(@z>\Gpu'#32#kVt&Y tڨLIFwնjOD;dUApV 3Z>Q$л$.ğAM~+JzO=cmrmA.rI$Y,zq%N]I3>ĵkқZٹ|A*g\7mdNMŏ̧/xQ>aB1J^]7yb mEl>Ki֣4;+_|8%ΫuE<|ZEΒq1>-Ld6#'Y,PDЫŘM/,O("t|0>a6yEPW',j֦'3Յy Y":41U:?[F(SL2 s)^&Tv55ٹ99zNS^5 t=c`}hwg lV"q_ꈶmCe'U[iXmBgA;Uk:/'4iI͔5|ąF-)WhR2IWU>ڑ5,]YcXSc5FՈFYm  էn;}//TSJGEÊa)hXJ)4,a4,䒊#pbh'B4?BLl܃Z>5ȾGQ44ݏ R62Oۄ]H|;{-Y[yvs!9:s"1< #guLꠖK='VmшGSIO2jegH;ȮdG.aVⲂr v\z=#9gF'\:sޝAnG̰"1V#ݒq׷fbD䜏NnQܪr{˪3i2^I\9WG{G78Km>z#z%DGǢD"cؓDq7|YCY(1q,w}:kX}ͯYbtk4fQ*~n:oO2I5,RYxOFC&:i[$'[ Uy:9<*0YCO,1)p#KG6.(xQFDcy5sӒ(?|dyH՞R|G󸊑rl]eތx5Oi_,/t53W?!ϓ-~Vȗ\̻SSy"l%v| yK"%i20z4%I.%ټ;1/iz5%W?RUٴwHVx^:=y"_RDGL;S3+9_,s53%ETW'QUg9Ga)9y?H9,rNx] ׮vU|Cqw/I.g{NDx߇3!O0͛z{rGkw[6 2<ɒe&B[M=I:Wfc)xqy]p`rϡd+m&|{uJLS'ʷښ5~}4-PI ӏD9dU[Wp,SW'LY3{uywl:h`:6vz+_t>-}̚Qь;2қiu2ɪf7v=,^Lr,:Oק; E'Ol&zzuz2«܍&y7Mw4}Hu-G([imii4!.Dๆ>%FdrЗI",'g #2-wN5 $t [  Na}|tvJG+bA,}fHjzRP-ĭQ++WWx\^7r-kBx|$ywۤUol$5$`֖?G;3vc=zzkGL^'Wί^ѣ|6:el?c%θza/qvUtc P*_2gttA~갤:)aKu8S`tzJ]#w ${^x*lK@D\g%FIjx'!E(>rG<@7.AcS|.NI4LGsw3>#LJݥ|*$K_H#vѻ>2OCWfO5Chv2j^FvYS21T:&EMI!YTZf-S2+%IJY!~Ogb-SiYʢee+tɨKMjðn <`:e(ݴ瀧LgMQƗ$ӥ(tb=3VYh3teߨә՜=c=]d! QX+t> ~bN>jՙއoJ‘h^7.Z28Sх$G 6Ex΃Qg4J@ɽ%n67/j? uK16{p W#܃ \9Ρ`{P}:_ RQ"j"fQVQ;==r_ytN`M xS(t3Vz=}aM¯A~DʝAvIk±zFu#&Yrl<؝d~qJ@ЕT(Ep+uGuu6kIB-Ķm9Ui'/x\(bA;T@Q62e! *!:/俦Ge0*ZadU˜՞I%٥ziuiaՐ\qD}#+hoG4dgH4y$\Afg<6b[ xO|HGkxoqXQX[x4F,x%[jIuLGQ#8)*Bŋר1fVud{_GrKP$˨8Y8<VKu8ͅÓ?ZQ鏓M$mciKgQ1 ,eϜŏ\Řivӻ7:'Thos5.>?fbC?="[14=^KI{MθgH~[Ҽ[(-, $T=fdB}ws996dd쯁e0,̯ҳ}DY?UPٻж9'5\\c^cps ULG;yrPBQR1d6@]2}vE.K~* ]~Y ,e_{) 3if~l936ixsF7[k+2;M,zkArΧ9&iUt^;{;9ʦ麦71᥉6,,\ĩEr.vձZj*gkZsr{ MzYƯxo*FH>2L\̓."VKjeڶU{$d!K~$Hf FH2*/a,s,a]㴺:e'4P[0E62U[ԃ僇X<XƤ/!GЯK w kømGZ.]۩]>Λ:}ska`[e2nap3jY[C)z Fr]9FWnMOlue(;xu} ru5}tgހB\koauN~]J`[{P6Zp%'GI%;9lVV貆6&ЁC&Z ǐsYa9,1&̚f(f llظA 6^OƌW#8vjt,!t<5OS_Zvj^_‡/ɃfGMIД$vƆ!l7Wɕ69C!]}DN> $Z0&kKk櫷N/vmF)F[ {JzƜ6P~rGvH2K[igκL: dC bJZ +w>PY+;N)q)=Fsq<7msJz|KE=G`:&up{zqe]|g3 cCѳ@chLI1hnShV`T*G@ Qޡy4RK`lAԟ02 B6A%! 0[ WЉ4q$ksQ`QqrG>Q;alt0 YԶE:L:NY{%OהqbCN0Zpã$| YRckR<&62o'NBbI "4]$sp\ewhCBn=4†GGW(Oh)V\$}|7Qi}ؤ&WE^$4 -bP`OW-;+dy@\l< eja Ղ"gրpkPp+`L1M?:=sC¹а۾ n-m_lgkE3nq6m#r_{xY&=_%/ NPTMkk 44 ޜw^ ]+,ta JaoqN,tluٷOѫbX4)ܝΚ/K6i +lUczB1y* Vغ"U8YHsZ-hľ6;Tp^kؑ$&:W71HXe|#QK"Ϸ{,4jB˱fa ]X"W~7F?F/p"g9m- 6>.*Ϭ@1}¸ɲB lA8>PIK{!@(OHk'{/.i&E87ehdXxH&o_G}]Eu 3=Auw¾}~MoCK@m#7PE)zkfFWX RrS9vmSC  I'HHlX&OS30܈x>0^BJ2DP;SqOWf{ p邴_WGre rlu`1tL(A]ŝOs/'Q׹+GkJVů1 }WɃE PyZ=#?$i t_BcHfN3k#.>I j!<r@_1<-9jOn<:+MR+A4kz+_18Befّw>"/?5{9QwW"S-p=rFGH VN1hx}*Ј=%5:B_Bfۭ-[hw,tT +]XJVz&Kt<gGpu/b Թ;FwBY?/'ɉze~#5@ |isNT |'`At};^ïQ> qpȷ d;֡.就t0 ++n9%@愱\2&`Ɩ鑧;VKۈ>~|qrJe,!I1'0xD39`.ԄQ|G9B@s)} /4z9߃!xs%vu,8݁5.ȼ ̻07bxZ"/̉! ߛ =/8߆vٸD!$zDStDId%CmU 錔 8I+.q4XP0-I%Cw0%A^cQE"I>0^hqaܫ^ɽ*ytۨGe΢?x%%buZk/Uw64NSG9hY>ÆlV؀>~ 6[ `o0a cYPRwZN]{X;5\&1^iFF|乄:W: }S~[<;ͳ`=2dMyl}߼8>j1<BUYAI}-XO`#zdqsYܳKRgx xRp% @rLXۅ<[dۣQ.Il/x,<;P wwGygeA/Q Ou,fUb{4ΪskJ3&|{:}r =3HO.~_5v]zs <Jr1ub:p9.d4!MnCQ3O/!c0le~|rVZ ^0 tgp2;:?&pX|[fM%&}vc?Q)q[qlZ,G)qyS&p5*\|%=*{ ; #!+'dxM>V3nj+vxD6(ߡc8na [|ldz'ArH7ȯq[w $뷹3BgZ渱:‡;K]:ZP"6a{痬ɑqn6T| ㆢ;膝um| IWBF{k lnm.t2U!r{.22aAt:l36La*v}{ip5Z:F. zzCx|Xƚɭ}/ol -8JAW[M=p =^.ב=<_k ̧f]v kj_ k\E jXmo&̖E]Z^ŶI{CI{0^լ;ꗈꗠ+_"AsJtkW#t+&:u*jt-F+MǬ%Pm\, ȶtlzg(a˵Na hH@e:U(uS6JCQ uut8Jq({<鞈 @ŭ* kXLb zli7[mԃX˖p#ĪXހU/c'ӒضVlJZW+d*HŭW%]L*5aMVŲ҉T\J8[u)\L'RԨ#ͩXA=ۮJ*#*N2*OV^D̲SwOO^6"(؞hp.U-BV*VUH'zP&cP\Lџ@*W*bbUkU" VG;T)W#*} 1WϨ8&? =TrݪLQ1UWP"[ K%%IE)5 :U5G$b"jFd\ 2u^*n#2HRFc94TD*2:3ޚZTE25TOLM&SG1әD.#}rMV!:VIg8H*[cTVd*t)N_cQkU/Ct2tz%Et2uF DZpf8pU}8lU;b#G|H=7N%~$wȀ՜ZNd#Y Iqu'QLHjSTdbuh4gp/J%ٿ8( Q*lt)NN~d&$ /ZWL~ Id&$xeFF=Y$;!NٞF{ѩ6&{+Ǔ“Ј6چOx@ed8RWh4VdORsO4R-^C[#BZ\?ALB=Ѳshh|䷇x76YRxg wI5dU#%aP$sO'Q v9 nJdI)s#` j1aB ~g>BAHi:8 k^yWFPn8bd/Rw6B'P@_:$uY nu.ZD"w>xtάC}l31|4^Ӆ ,nH h< ~KJ3 2)؞Y=l=boH N K );`D}2E$D)_Q4bWz[`Y>ب#;TdQ l}(9g{/a ZD7Ҥ8xcDRu{ ({ ihls|{ME?-խ!fEHkkO8g5_ѳ"#ϔ|z|Y=VňϨ+/dVTO)kczDs_:.}GH\_Bɦ#x9D0)Ξ.B" =N訙uJIM-%(RG&~6XE%'zUt&RCתZF$j\G \;0mÖ gj!kn=7ayG»i’7OAOAF/]=%*I>gua%ֿW63ޟuT̿m>1Kdon`i0Y!=tdk{?oM e&g)6C?+L_~?]o? ԓ_bKo?=BG _K QӏpƱbd-޲D$pakIعxD1=%!Wr}D 1*&^(/Q4 ^+#By]ٰ pSa":b\ۢ%Oi6FUh }WI-.u'e}:ku69l3 fߕ=":mѻ#m;|ZٸuV68w~~[&p_ހg ߦ#yj/`Mw.a4D z~ zcY&VڂcHBtnS4>1lZ+n6d1k;wu5^M!{=nf,*11 /Y g_k$i[Ix~߈ꁗg';**֔Zɪ&W‚Q=ĥMk;sAqQzhӣCp_QI<ɒt\ p =8G({!;H/f@]r\-"dM!K@J@1B'RyL(Y1(#%{ (JvJX"˦G$Or@ɄI) (tyJu s#^R* !~#DxȱJ gae'mcQIj~&qDERxus&=/,Gx:ZUt}^gdQXIkc?ɕ5UT;SWsSV' I7)unUs:]ݬ(ɺSI3k+]}q;(r\+X1^ы;6}RbP6?u䣨諘csG0~ z{]ym~E㐎ht(ќiη cC}} $FgєAfx'SuW(}22{)S;&/jJgiIGd-?mACVau;#ke Z¢"Bg* /kw^M Xazu|¢fmq>S]8K<ᛐ)iKs\ZceS'ADρI<˃qS OF=Ӿe+ʖe[IɁ|󤫣5iҹt)gS9d.N??wEjbOeYQ{M3zg>UyO3 > Fm/_ +qz k-eίhk_v;:T\~}:\eՖk|,)x4SKrBIkLY#G\hhђr}&%ә}^僪YcX5z5u.:5V#kTn@fO>P}NӧH*I-_>tT4H"[AOiJ.h8 '~"(D#=S{E@{/UOmc.󄿯Mۅ~=|ך܁Rga7k;Q3(r࿋>bqVj/snx=$QFqJx$ h(.+(.hNɥ?s:yftu:v .ΡPxi5]?-w}k&A4Jḓ*Ga{:F*e+Duxsud91yt{эӽ棗:'QZnYOtt,JK+b?=IIw~1ȧ5D"WNɲW٧;OeyYꟕ/L&@hV] a$D[-$i4DMlmocyٟ%Oi{PGyXsɳy uiZO&LBk k_"qlD!})t rr8+rTú`)ANApo0xz __MGgml}4G(27чhƏ]x{ ܒ_@zkr{{++Ԋp{oJwMJYƶy_]ORJfmO|ө׊מjvdTu"}[=JgSS-Z9&3V+aOM_og[K:[U>k+8}FGD~aMKÞBT3:VKGܥ:rKNJ%hgȶ$=^ OuV?awRԌsK }{chI9=5DtMP:w03¤]ɧ}Ai+MX4bg+t9tevxZT:-`'#U8o$j%?>/CchQԴLZ8eBk=-RiYkgt&2;%Y,ZfxڱZfKBk}]@nj$T6 립к/]\M{xz9{tv-t|<e|p;N2]H'3:)=jEKX־ =oy\<uFT[f| pOAs/`~P'kC[K_p?R=q}N[ |Ε %:ߧs% %BIɯ&bEaX:Qc!Gm$ NJP:L7aYݴ/:+}AĬow}/6,gdY7Bh!SH7AK ]Me] )h[Pw`^Pg!Ɵ$hNliܖCXvzRX ̅L ),dHee)cq ]@*{Ͱ-Jkz,PVQ 㫢FVE+ix^)T]*Z^Y cF+̝!s|kěN9Bm4#Aw0n"+CK7rk~..qD#JvDGB<8d l'qSh#՟''tJ޺^ek>wNcĢP柄Pg:>Yt5>y-{.QcoU{_Gu$.ď~Ar1PӑlqXW9|q\8B+lWh:!-\GijeOXf 9]WA\\Lf[xilϣTs1H QlHاEt(SAZ_Y X mkBTWx흍8 FSHI!)$FRHnw HYx3ꇤsaaaaxIǏ'U{o_ھgW9 o'GW {>~Jlo߾)*/N\ϱov[iZ_ձaJΝ/:6O- 92b?Tlk%?_21B sY5>:>c=1Ow y^- ڶ,XzusM#גU]>H_yYv!ۉ_mi Rus]Xm_g)YY)m]y,m z1aaaxEߓGקo/Y\k6xjgH|yu.\aæM&wk#ϐ$?]Mo\Ⱦ,/ڥQ@~6s?)}, l gX #vQg Bٙ^uのuhm?}{].~}v_J;xogJY]޳@.)oqC?}>@Xߘ'-(W? źvƔOʙRv[K?[A}?-wmՑ}g\=c}M ggg DŽ-B^k_g?F? v0||؎=ǧHPgs/hؑI t~{n^}ZyD5XWvO)"c0vY Z|~_%/,p\ɹyΰZ/;/xs_9?Pܯ5ݻ\[y|č8gʱL{? 0 0 _k3>z_\S |<)b|7aaaxn.ta?l^Cvkؽ#~e)3<3^kdlc&jK+o"e<.ʞ`^(3zu l+6v<ï k7]/lc[`On}򚄫 G뎱zt^v2)?;Wmr5ocIz?Ozx{&!ez."ѯ 1Gg{+ҏlw<=}GݽFƨ^)zIpG K֜{{e G12ۭqiumf>.}~a? 0 0 [u+7Svq֭y΅ ?ނ}XwŶv?ߩDZۓ-q/?߳=<~#>Fk"qzrQo 9r,nY[;o:)@-`ק-7({߯S@µK9֠ɸ>:n3 _[_*mtcmC>qSL=<6;ǫsaaa{xˌ\ފpx?0׋#5zяc]x^l򼠕(f:~٣^lin59W~\;?vn6erUbS~v^U O7O(|;+SG4|?f*?rW~2oNٟS9~daևmH6mX[J~s.ym4ٶO|Bd/b5ɿyU? 0 0 0 0 0 0 0.P~*1@G\⟿KrKXs2(ߥ纎J8'>X@▼QQbqwx b)_K|v 1M6kee-2Ǜ59?K^E~9ϱQﱮYF8N?~;:=J<-tĒyNAgC \NXKs)'^Kg\~2}6}Գ)n]Or^j~"{p29w6/.z-v:+M{WJYZ굢`% Ҥl9ힶկ#OUz+U?;sd~vND7*.Y+v:ye;8}~|+ÑޅN9}{Bƞ#txխsXɿkSV/uJ=o G<ջL'L:D]6jfgLz/+ؽ[{rCMYq~[{yy czA;w9zszWHVax3 %mkBTx흍) q ĉ8D^>׻gI@XjjgiЃ`0 `0 ?ϟ|:seQ3|ӧO|:2|.};7eGFO6_Qv]T]^ˮg{>pjzkuo{yye?{-x/ D:3D&򈼹e^Hyi#/OGzϪ߯_~ :sMe#M3Y#=2 QЙ[\s=E8}E>GȩT ڲTg-}VfoSVwzV}./>~!?U1<#}=F[ ~QڋBN..+푹^edLo+[\-k dW(}6q$#?z6Bөi?L7!3O_Q}Пuo[=tkȋM!'}/Ƈdr2_Cﲨ: `0 :8o=+8-4}۞cĥXdq{bUq©ήm!ƶg*ΪU\z[GA=^+ru{LV U?)V>ғ)x|Yҁgi\yi^cUo*= !TY?rfgWsʽVn*VX#=Fϫ+[F~yH\L~[O҇h5ݵTow|Sfӟ+);F;:x )/OS yUo2e)Ve3'wgGg=J^`0  ľu kU,Ksؑ5nY,bXw{ w&3QהNQev ]ƷgcH˞i{A3I8hwduwUIWq8I>+@pQşGcZ\ƪUߝ]/:3d;ɫ:gB9R|GW~w2;fzt|+i5nΟgZY|<1NyŬ|E7k?z/k><=Α}N΅>uWydʬdz `0 *\?W8GY:Dgcg< 2+'W6qn؟{ru"wU쏘~c#T?+y{Q,,^qF/Xv8.֩g3}ȸOP ~n%hUG4(_sn|W}Tg&x^c,Fѭ+ <#+}/Uw8BRh_|33!mr\7U9m({ѝpvew[xG]߱?g;,nҽow8]וb?OV=Z_#ve?vN_WrYLo;1g9pV^G~>[_vNOS3 `0Q[ veO\k^8֔v<Zbz\Opbn$~}oz3ј mK vU]^iNWA#x딫jt q :E= z%օq)CcYEqyRG-+u (K\hP'*^ء^q=m=y|Kvūe\rȊ4={W1;=ݷxp;o@>ȘT\Ԏ+C=*ɫ|GJOCW]x1.ﵠ9_Eб Vq)v(ʑ}[GwǺ{-oSdו_˞׃2;iT&w*w:g׭SOsj%Z[~_˯d֮+w]7 `0]kIu+eL]ւoA^;=GR?v쯱;<y o$N1紈=:ߥPVu< <&3KyC/4r)i=*/|Ύ^]QNН1qGw>ù{ ?Kv:A}E:_n+{u=rq͓̳]>>d}+|L01`0 leg:׺񶊝`W,3O?]\9P~[kOWiGc~)-<w.3q}'vuw$Vnv(r52S;Wk_Kϔ8B/hEՠ'9w?K;x:x<|@cϽVyc@ۖSw8Bq]=2lBe6V}eR( VeZT4ade2ޒ+nYBTqSߔ<[&=f[|szP)G}{Zׅ3n7jpWwftEw[ǽ;`l? `0 `0 `{~i`oLy>uoi\qK|}7Svu9G쯿c¾#>,jow{ՆݲL=mW2u_8دjo?kD߱mw>#}E:OۡO;y`$jwymkBT6x횉m0]HI!)$FR?6c>>~sm+vuՑνYu8uN?WP>1JsWiV_uKEϸ/rˆ_gKW]ױEYcl,[TYHT}xL#}A GV7^}>iҞ-i;}LJX&TP3T#ߨgJl e'=?͘ona|7>?ǐU%;/mN/IfQփz{G}?v✽3X~j{zTAO^ʰ>?sy|G)PU{ ..T}6ڳ-F`p]k߅~b  О$wݓٱ|sCoA+q3lOx@(0a+? T,_7s\Ϙ^Bl1)C+k(FyN"8dPC_9>O0&l4Im+nwGrŰ)/tihf ѸX>E)<,6s45zb?J\<OM%O#(76:= ӋYAƒH Ls6MXBcX&ǘJte. 3.je(??Lj=%wZizFTx$kP8Em jAOހ>~؆B9 ֤8UKCvjbL Cy ;mj P. DkwUE€3ܨ8xUJs\ɟ+;}sFQ(KIXݛƨ 1 +KdX];Jģcx$D׷X`i @l̏rnm$^9΄zBGϞQ=nfkDe; <a>,⢞jk0B[p($Ǡp4 nq`XƓ vϵ.xHnorJ5Hu뇗 f a[Z:>36[g RL؍?( &w.7C#~B{] UW 71jk~ecGrD.=K@WDZM0倐0\xvqNZ ># BE )&yA}t?B Ym(WIpɱ |2+\2 )l8tl@Z.Be񅋍RSƃm>dIl'N adĢG3%#)?$s _5=YBR#-k"qGP-e"f%֩-ϓ378M9ϊ,_*n;HEBƱcl~ ˝[/sagIE2,z1t:kLș壋G){7ond{@rP>kwk׽ #kXfyEAB9uM4P=_lgW؇N#_nGpp ,ZUu6ȓVӰ0EK7*|]{75F\ԶzQz! uH>upT٣o3P)[^6` -d&*=%fY<^ط`_6|h3ء>2 Pq7ώ ,NsjF=B` 큳CiU)R鐏@LҮǧmb<2FHRqùFXi䎲OmGA}:*u f:@ʫRH.66jcGOpO- 6HKJU:Jǃv,3DZEƮqq7p?ȌK%ȧ$;?Qr6pP7`a^=R_)m>D3#£ _' Iɭu͋C-Rne㯄ssL<ȭ/R)|Lt_1Lk=rr 4/gEr~PnB[\g[{gYvRW' {Fem1{ wL;7&$xc0 n&u@5sCCձm8Heft x{q(aтa?Q%l4ςxmWI׆GC1kQ3iJh,KRO`ʲ4)%b6B8\pe;u)ko)#WSncRx{[sXv195_0Kՙ7>Tp5ٴl3S"؝LX睫[5m Q="u}pϘ*xbՉ#iM+@Z! Ϯ~jYݬ$?5mtu] %@݅:4h8ۃtu3; ΑO1A/r R*5i&j#Y2:$Z(ad@>'z L뇶6Z8|`6"X1_z' F-я?X^ A:?1;h/KVB' vOnFS ƤQ{=kh7MwXQp\v͓O/. N3HKRlK"q^Wh1wt h@3e6N|I;y?8t[[! $,ήLe"z%IކAkRl!3u8ځy?_W)AbCO!rza5Sn֗#<43y6"R߃CQ&>[# BHǽ{vekOTlq(UH͵h ݔ8,@tՂL{p/*L"d_y k,4 G̖bD>,.ok"D;|7[.DCA#ilϟI֬Dq]+eE _-- ڰc^Lq1~CCC9gNH8BkhJ#Z-`VoMa 9r$պZ-hkh ?C$ ^tď9d(8P݅]ڶw[wl;dn׆oKd Hބ(DInI M_(5)6H/Y1 QRk,nXHʉ?>df&6^EJmt{CCc`0ʅv5x<\9Yc}106"״!֏9dl:' 1H"z'7QqɌ#KR./CVgQȬ\ `?d1yuM6Ƶ8ZX]8^pwQE &1frRKi$GݜЕh3'{;;~FK37ku<pdʎ+C RMzƏ7)nҀ lEGyl:̑IoBS%|ЕsTulebA}Aʹ10A{KʘӺtjdLI=r PRg_LbR Şl?␔)![Fo wi&k^CV(t@pW2{hxHGRn͉eCbxԉ6GQd27\ثdS=\Ff*0ۣOP5(rZߙxQZ>~GAeN-jY7Ҿn;n?ӹ"Px}/NW:݊&׾:x" ꭥу;R펔 c䛅љElmG§a= h¨BG_uYnZ쫭FYs U"zM&:Gnu.DX5Xn;}ԫ%XO?~2&Frjj8 yA*W I9/ub)Zl: s 85J>~iI3Yԕ;:#hELם[ROd^GA˩f~Y!En0~/A Km>^WYq"<цF*c:xw|͞w%ehRgd9̕v3v Dgh>>?3hYDkgC(ʹƒԕSԜ| 2Q94(?OGQ34 fccPopTYaW(>@tX4`LGٞpɄaŰl\[9c26U M6f,'C4i?W~psϠ?kAKrŵk@I|>^xs?\`,D̒5W^w DMXf_8<%|8_왉pP1Wlm߃f?4:́_Ԕv M;k:p_sj؎qw]$F}y ,b'N=o0, ~M YR46+!}@~ujctCP.Y(x׎z?70WXFܣo3z0c8RGg0 TU򄽻w"/4֏CQ`[{Ocn]+{{ N!33+5]qpj' r9FDȬ)~: 9Gmx2-?sraG"yvUpa;Ră A\& ?#n 0eed~oq嶭!!DzP^H)>oȑ.ļԶ=Hy7S-M ?8ycߧq|#5"2Б lm#UeΤVbM͘jAc7Z ]> 4gb s 2WRsKg6 's8qzTT[R[w)I95xWj #!nN+zPڔ KgTE,?{^RDݥ=Ru^zîc&D'i74SJߔ&HUG[crͦ<׿~4}څh;lpAZ%XZ;tQ?yk1+Ƴu6[ Dc4Ɯ*dB#!}e>samhG3c^8u9󼵕⸈߂UyB;f "Yi=D =4&|C3g]~WgjhSIXU"1A5Fr4{AljwTt6</N \Rta| i>T.Wo>>xϯY{緷m,J{gg}v~)]s!?wXGFl!7U|Cnfﳅ:.@mq%臔Ru?.:aBֺE#Gg'yXDuSWNJD)21ѵVagWPqȒ s?¶@g")s\T{f3go^w:^"{d#!φt},nyWFKv„X4|VB~,˘_&fjp/WԍwaO H 3I`u1ͤ+_W|+_W|+_W|+_W|+_W|+_W|+_W|+_W|+_W|+_W|+_W|+_W|+_W|+_W|+_W|+_Wݚw)2iTXtXML:com.adobe.xmp Adobe Fireworks CS3 2007-10-06T22:15:44Z 2007-10-07T00:39:07Z image/png 9-)IDATx_\=suQ%M_bBA>_$}KAӇBA lZ(>t-Iy1Eۂ"L yPN i05ӇΚh>/33w_=1 l,! KD%d-;`ͥ;:03GaIJ8VONN(Wrf//'Տ2?:-[9Nc m !8g`2Kdi}7T^_{ݶuSi(Q].r9`1ˢc` $Aŵ#N>zg+M nŇs* 8 DQQG>W!xƥ21JPJhXs羪֮41=-gKqb0!B><υR0f0@kD)$BBK慝jWX5~Z=`4cӦĈ!!m fY6BKZcz1HImbq><}|hPR2[sEy ff"ąaއ:Bs~y-F88ߕBz9,vzů[VYŁ! q; |\ m?Bx+˜!8r> q?맟c[mn:aG>džme13!l[qlcqeREu.nH)aYZBn,Y(|1[m\x%x== !d}Yb-89|ɪ lhuRR@pF3-!S1 eAp)mۼ7b|ݫ2]+)_̪ nKRJRBʑk0!2\Y9BpeaL._,{gm} 3p!k !ӳl̚8[zwXV(j Yd3hK(ff13f2C΄1 IT%^Z-T#U J)6 )L gZjϸ~ |07Bj||쉬3[9tިtI@)40Zidh$QtVػ>?BAEUH˄W-.ܷyhU[6ڝDA]rs 2J+e~\3g?o4[h6h;vP5z B C RoͿ-_MG֡˗VmN)hh;{:ژ YxjuTQoUКKzfHknWU;oҸp׏+ťjZ:J-ߥ}^B&[{Wtw_IWW wQGg%dFڨ7O|d,7ơ\2b.% v| Xӹv\ bκ}Ԝ+/BYƋ`{/7e +َǿlG~~a~#Ғ|V'Uc:-RiL0ԢB'TS5R45~ d<# ٜƖsk:Pި B'rYAVb)Ӣ_isP $ia5-]%{mA++zHL?XDYz1r~>#i}VЧ6P}1m"'~ l6S\@ln(ɳ-}v}/HmkBF)3v/mkTSx}Kl$Iz^jw׮:xa֒3=g=f%HV3|V. %Pe|ڀlÆo>d6 { 6to_,@#"3"2232IvNM32#vqE5hq]?]SuVuhg}xev*5Z7[#UϮ35c#7Eo'j􍂱qsc5TggP6Qǵ]T۝q Zu܆Zk ~vsQulCz ~՛p#7[ 76nBv =r^õõ2ƾl;p6XC4g to Y!=-8{ ѱf?:QZ0F)FkSD }zJzb`7O%D|*)"B e&! q%V }uэ}ҌVknaq+o/$>sliU)v^@ g]= :b#O\ԸpLy?3łiQmoZlL$䰣-vEKb,x. tAn0%Œ3vFeYV~tS J5^bH-PNXbfT|A*M zfȥ*MרΆޤ=D\NYb1pV{5[)=19nb)^S"&)r/=K%Yb]mۙ](Q!@bS(sQ*$2ˉ M }Fd8bZʍ% F%K35o4HN;w@=\-M& fш,V 2ǞM#ag UxPnŇ[KYf c:lWM[VU"h(t(cJ[1JTU"h(EbJю3iJU$e弒&ÁtLFX*kqfKflS``@Mۓ̞ OA*+K2Q@eɞNm$#4<89SiHc՞qFMI1 4&$Имd!:}D]o%Ny! M`z0z]G`LSQu<܄65 f:EWإƩ+ud9@ B2n LOi)h+;5B7M%㦐#FE݈xgC$rLJ([O%8j1z`HT!T-?Dd >XG7)XxʀBtc ےN x4# -PڀCKK#E+d#5Jd+1Gbۈ2)dL:TڳZƈNa"%V(lBQ`{y4A# 05tLlg+Su ~яws A&(*#Foׅ9Tb3MjVgICԬX߶`\4%7 8kH@;YS|GC1o92S47Hk(nWPJ28e=qGr*. Q&դ@[CJf#} e#y\8O97KZ8#+*d*u<2L.f3Lt ["]NB Shr_ض`?i\?ើj}R!@vN]O~( $4jbLɍ.DӒJ$9` bKs3&&c7idWT|J@SD9lX=tTsF Q4!8h3ܜ4fQ[  M^!ezόcI_btb ,lUZK0,vR@XRҩ!SYƅ´)Ձ+_}ÑIኣZiں50V]7yHX^C25`Яt$ zaFh׌Ҍc5`ZQLzRѵ*]Q-H Ky-9s): b0 e<{zY}[UXh|dO|N}Jej˄C[dhNx"Zhui|Iʙ]d4VZD q&eE1G9@P~8sߠ :Fme}4:CGtqD(Bza3o[Nhܜ) #s=*eh۾I ^uҟ*N)@&vзG#4zprփQ'ԉn$i6ɳ-ry/:?#>T%AlUI_Q}z|Dr97s]M_ 6{Q~oF4,urĊ G{D_3sr:VF/=#֤YoNoi=oMLMHG4ihh'4=z˗D<}~\3 سgz%==EIsdQ@#Dղ.MӥoՁ3F! uG{+Ko5qH it?ʤyoe: `?**yowiom3e~㝓wiR$֗Uҗռ/[/k0zON}f^OiiaY!yDOYY?,V!Zp ~何e|be!鹡P> ]|;cAlh_7,-r_𫍩 Mߧlڄ%`eN[sUaCHQ gwۂ'/]HKTYZkWSN'>,YQP1'Yׯbw|6xNH$VP1?-@TKH11g"EAbjd3Xun͡OjvjD=UL@gQ 1w* J =:u+3`gs'ɲ/+(o+pV, %9hbP.*猅y- (I`ad6M"CV)Gyb9K168Ւk@,u=O3ɏ'\UmedIWzNOK={`  ^?ܕ¢m&χ{G'ɨm:3խn[wzL^ za–\#-]/)nQNeGzm$K>rHa_{_4#4l#F>#ЙFwqqY56,jj^2l^lFjc}Fʛ6mP6"zDX c.;B]8flؠy~ݸ2dXc Y/t ek,h20aoҹ].5[tJ􊟧lxRMD ʃ 3uzNy1 wp34J/ԸAo 77d\~ T# >reGS\lx:|-o sr߃/iKA?7c %,`Oa}d #`Q՟-lv>B&}$9D"4; i9O 8<3>dSiHs9eSB+T:]7QGwgLj'ğ{f;@PZ;-p:Tt܄5\Orkkg/ιvεv^ (\s =k ]5ts BCmz@s =s -\C:* aA^\gZg۹uv5t-IOs=k::ѹNtts =s :й5_˕wV@;+r|ӵsίʻ\;/vW9:[ykeʻ\C:~}Vz4t.йʻ\g/W:;::u\yeʻ\G::MYykeʻ\C:(?߉~, V5\#zhx.y#o Yivq%rLK:Axs=OW[WDkk%iuzVIk1G;767EvƯ3v$EDv,W_GOԒd;D@" ͡=nFвk\C:LWCSz"/9m]xTAb]FgA pT@a܇udOy % vHf'ā\5'{/.iE86yhx;N]A1QLj;_ZCe 2=Feu¶%}~Io@ @ #7PE.zkF@Oh \rK9lSA  I+IٺL-t aj96ҥ}a8d}v{k_xA Bl]%~GTLi:Vޒ(A3"u9g{Ok2q 5d!ШB-X8{J}~G+9gS0xLucэq1S7boz!:,].~)+9[DZo}tIQ/En=O_lbFˢsW"S;r䨂A^٦cPC1~*Ј/=%%˰B;@[khi6Z:ҹεi-x=4p9x0QVeK7iاO!θKmN N~v8~lHu,q#u@8n,+rN1AѾz_{H.y.23Ӥy;SӒS(~6Ņh_K \+tAOIknq!B݃u:T6Ua=-ʁoD+bo>}s¸/}_%&`V *v/@h(g^c59 uX]<0Ks zXfYr!^~gcS!r}'2Oеc V8O<9T^ ߕ ҇YtaMJStu^Bh\s( );m̙?0|/`d>yi>mqngor:2|0|ᯩ6jQ~ ͏Er&qX*'#<l4ރVzStB#Yi^? bhkrdVI#PdNz\{\Cߢ+NaMvM'VVI{]Hje;`i<3< mG_e=I޿PP;.½QRf[3]%4vų~ pL¸>ų YEǥV@^*FMX0b+?ZFŒ oNCqiGgnD|o9uIlj9h9e ~0'3ÑX!X85:E왑6*zgE.HydkV;am.7WՐ7_N׫Zz! 88#! Isc{e_6pi펮l=Be67 IIm#|.Ho '9_\=l4sE֗lV7FWaB>BGno}*؁4=rI47B5vȥ6Bd(`ѕ-s\86|i:I~1Z5v=h{;| zkg\ojb"[m6@qa| WXp7~T*>څ@ėGk@0{3*2$\L邮u7 R{^ہp\QpweۀۻWc{\_v,4Z +2UCu` NEa!"w}8hBPywAZ;H1l;;Ѐ]2MLrs|s5I43iyo2-,eŗo.Z78y8ubenWtK%DZvݒM8jKbTjjb=Zd"HR,ge#<8[V4KV2O$RAuPT*Œ84J< vդ;EĚ^I&R扔U+UBTti@Rቔ&2%lG"J‰ @^*Df:2wy"b-WJ%F*Wʞx"Y,쪿AX**VbKceWCX*eTrա2)>;R [2,)VѬXS QsD26^vRȸ"+6"d"IzLI$U2ͭ)LY$SBi*r)H"2LUƴRD*)#rMJU!2L薱I6HZ,Dzi48ғJ%qSDFP8V쥳l@㙎]-P*N:߳D!Ŏ:t8ҖNOXL[Wxښ58B֣'ıRD%!ڵ-I >mkG}`xBBXށTMem'PZ_\\}j]21ݻHlp u`K#8`A|6Udc`󤟠 `E (L- |0wK{맪Utݧ:}Q`]2{p|隃Tg&5m@& a}=U >d}Fy'=σ7< DFOr)Muat5rEXJ+H6IhX@g2Z !}y}7m%o9<{)[K ; Va<  gQצ2{76I? QO%JO[{#蜸;(ixdXڟcx2t#9$H pB:lmWޗ1y,+j< E ?w๨ {75AGN3mOm{:&uDh7$HB;dm  Xxl. veOZwI$F B\yJFQOnxD8X G9KʾgwL I,}TϞHVs*.)?t }5 jKg릟@i=At MϹ_ܻb&;uj^@k9D!'q4 W׆^!?ΟQpݥ˥w>;*e:OSryrB4~Am1d 'KecD~$pg/왉5>}ĶB'tLrz|6NY-#N}_y`zUt$ÇM/U_ 8ZIոvRaڂ3O2BWPn=O?@ M33KWxrdzaDJ֥zPgu^߁W63ޟ_[+|l>tKC/'w^uJ3Õ_d6=kq%qm\BO#b'o9h$aVy,6وDy><^N&q"=߀O`~m<U;¯vk# _OZn &0:՝鴂/YiT1D?WE/GzR̳qil8]1,= vᎿ<}CGf/,`Mlw. a4DN湃Z~ ZcQ&VZkH@ tn3;i|bشT\ ciQ}kn [՗0 tXo.BWb;bHq_6%>HNVג}#jV͟B0XRB>J%[\ )z\+W%wI˿PƸn=n[rաWWȗ}AdR3O$fDM:c`ܿ^FҝGtas6%"B "-t*Aʄ?RQg-dDɎE.Q1mzL)G@I!qN L(9ɝJOwTE1G(-$o7Tg{Ex<蝬% ZiiOJݚ)u{ԝSrS4 7)unmu:]c(ɺ3I2K]}q;H⍲_+X1^ы;:mRbP6?u죨V1&02a!6$b=lQ\1sX}BwlGM)lhQ&7:t@={ }i'#32ckZd&y tZ(MIFOjKx;dUApV ;RVCga|GhGZ׻|?.VH&>P48͚ja~u,ןdҕ;C\ɽ]k;itntuttmt4Zz6ӕI쁵`X^^09bV&,UG f1Nria{Z8z槅u<σw\gӪ]_?nx$_xK'I.,j9fSg&:SZ|QiE^f!W7&,k&';Ӟ'|6w>knkVhl\#epxyy&y6ތO# =n߀5ʖ=ʶ3ʎ6ΓۦIҍݧ|dLO"%],Ɏ(ɞzI3eT{6M$~M93 Y?; F/#w G˕xzET99gB2NGOhm=2Pipy?e2xM; -PhS4ȩZӁ5!IS%n,#.5JhJľBLƾBYbX%z%e.uJ,Gہ^!v>P}zNӧH*IUR:*V$ KARra ۧa4le/h8 'ޑ|!eqJ Ex,`t?6?j6=e+znv)]<f*O`wm]G8sojcjɅtr%\)c'wG=A)W='ZmSG\Tte<^Rk#2ǫQR.,':2%%ÙċGa"B+GGd޿lӝާ^[ülʟ/L&@HV]/a}Ӷ$x[ّ-$i$DmMlmkyޟ&OY[i9*Zg2<P&y <&ydiuf+;JHM9gcV\eo/O3Tts1詮f&UWegG,cyE{xM2:c̑,|N'^bgIZd$M,B/^&/ICQ/^<^ͺscz3^r=U5͊'T=ae+C߂SSed֑yrٟ)ɽZ[)[y:DԼc\ڒM <1g1I_틙,1)E}1yc6]g9-^3q׈,'TK3Tts1.z52y?3qc?9?HM/z_2ܳ9:a$_i#ZUq7!;|'^b.ggND|͟߇#!;z{rGqj;w[5tأdnjX٭s*g[F1}<ո}CV,Z LzyuJ̒'m5mkjEYI[8I;d'go|2zgihlOF2'-}zgu:։б[wYcֈ,d}M(~M̢=#y{.zn:{vo7Ccy4Dz ӵNB|Ie' mLՊdF]&ںN^q,c@[iuii4!.x`_CFdrЖjI<'#"MwA% 8 d 1QW `9} ļwvFG+|A}dP:F%=.(jDĭQ+W.\^rMjD'b$ywۤUol[IJVq5Pv8"5=~#ةlʵ"˵gZY3rr_Zxz%\)ۖpGeKqΥ { {~+SD{9V}x'@wʨ> "O?˰fP%aϠ [*ÙAW+ǥcRk%')~si(e0-HzS$_bNCQP}n6G,@7cf=H` jQf|DK1TďS_J#v֣w}|>] =VF^ץXq]`UڙoIy +`i.440_N{(=T %wtC)aH6rCȷLKU2@YE)VY0{d\INk^x]3Xg52Zp `^DhcTGzc$Mde}?DDy#+ioԣdw7y,'!YXMdzԓ[lZm1ud_7Tv4|5w1IWSPלJlPTs  Cq wf, wU}c3RNs"w/k e/<[],mUQ*+S_G3b rBHMl#iK[ͶF]m4Nc4e{GxbLǤ3:֏h*Ɖ`}ŭzL}ǡϧ+FIJ}饔s#m)k3l(-/ $Tt{Z3~2L>;=b>a Eue)oqeu(s]*se)mS^cK ;VS|f,bmQ?ӓrglQq 7B]Za*D:$|cG8M>xj4WV4]ƻ52ņe8%A3=[E.;~Dc!ntxh]\7Wvh׫O{>!z+ kn7з_Wyrmt/[]R*+жŪgyNR8G]=A2c0B9;oqtj M*z%àx(@Jb~qzܸz3C/ĞZ亾3Kʱ&`s1>~m]\_V Z×qccl/#Tz]?]SuVu 7I 677[ëjil`qcg@3#{mt;UqscPAe\FvQnwƵ&$jqj-U!ᲾG lw iOބjqM vK9طpu6mLfc߁Kݴ&_ؼ¦ 49)?)BGqO c'1 d\KrǭƗW[-E7I3Z}w(mC riU)v^@ g]= :x"GຟBqI]/yf<ӢRW]oZlL$䰣-vEKb,x. tAn0%Œ3vFeYV~l>r=ՕxkH!2 F8Al[968S0oU@i$͐K=U>Q I1zJtING_ ǧըߪ;>^OOMt1PGS,DLR^zrKڶ3PmCĦ&= PxQ*$2ˉ M }Fd8bZʍ% F%K35o4HN;w@=\-M& fш,V 2ǞM#ag UxPnŇZ4L!cCG R`Pc*JePelc[)p|V)1 AJPCќEHXL !818z& TiJWWr$w80ә)H!FR{V%36ݩ?00 &oiIf 槠pD[Q%WYU(}Di{]Zd 6L>89Si 0}>p܌4jLI1 4&Ԅ=% ٘#zs/q#y!o"׋0P<G`:U0xy5 f:]m`bi\L6A~jS;ԝ*'s сx#!lG@ЛȒqSȑU #nD3Sbí_b49SI$ZG`!43> =U$U)|KpO;9%9ƑM >2tJwɺ8f`r% P0th`ii3](Xv}`6`R|\|pb"r>-FvWo#:835Pm jϖk}#";aXZV GU:d_'/,h1iBpN,H7RG?v5"VR%# W@D8I_[>SQt6[X:'i Qb}ۂrt2ଡ#dMu .ӿWLް#p]A*GqȡDVfm *p`W.Aߢ3G~)|Q+Yad~E F?wDO$J.f3/|Jp-.f'}`!ob9/l[牟URf -VI'UdY 9W K|H!S҄^FZ珫8x1%7>LMK*̃6U/}lĚHLޤ]Q) Ma6gy_O`QdG~: ʇ{㰣psEmdTΏ6y$ӗMa?3%}Ia&2* -%D ;) ,)PԐ,^ BQaڔ Ho>h$IpQ LRm]L}m), qPɞ~ޯ>`X 2HA0EZ3Pi$.Q2 Lʊ0b r:Cp斿AAt :ht4"VQ"fvE "6: N89/SG 2+zJc TК+d})="/a+esgSJ(9I-:o]$>mH;͠!`=97Qzӌt"%V2hdA؁}t^IO!~V°l4Tɝ2ky3JJGɄՊ85TZ V*P\ԋ؄]Q1˥t?r~CpYb̚ނ8;]Qnqq; L&!+ RC &3౲ DX|¦_ԙM$w)&wi/|3^=XzFe#INV*]Vj%o Bma]uhȡut]Bo 9B0z*3ew6.ICF"!tiCGD>Q'ԉn&Ro6ɳ-ry/:?#>T%AlUI_-6ѧxMG|qR_!=f/@>NX11b+!~fNNH=WCb~ߚ0+OC-MGM|{}Js&[vHvMӣ|Isؗ5Cn=;y\snsYdQ|KEu4iWʻ4MU 4D]./,0ȻמJ?ʤyoe: `?**yowiomyKɻT)Sd*jޗi-ڗ5L=XwSS/4 U!S`LPx[pKiIUS5Rk {#>u}0^Z{ȎT(AWE;><`|q+b *lw䇥qȘʘ F[E1Sx2,:7'^]5;^5q垪c&(;KijE:γI_܃8:IFm酼dɭnluC׋`Rg3(&iRWQLq2w*;z8 n$64^(dCg,$G ڣ%xa14:79aQ3V5NabN8oWMno?Hy޴KB%H=wc [42ywᘱmcu(Ӓai*Cwlx RAI߆S_ =B+Aq-3- SHyK&Oio RKDyʆ'DT<~ԧd?Ni6O &ޢv6xFQ7MX!p&U˯!ޑZjs R¢sz`|*˒rOOOse rX{% c)&q) LpD,s-}h*'Ws Mr/%H; sImsD x%ꉮ;Di7Pmv5spy$g|vҔ+\sr- WuNio2ώO>?Exvw8[t.yj/ADe<5tg\C:VXCvs=s::ѹNhN܃ε쵳k\;9EPa5tgK5t54n!/Vuu\g:;W\CzZֵ5묡Zr9_yk目D;~+r :_yk\Chq]AG+r41c]A;+rk41g]AC+r kh ur|]sƫ~}Vz4t.й4JOws 櫭;HU }^e*:eH-!w:k>-9X1.Q=Lt|X_SԒYBVWSDqlН& (-.^Q/`{ eqQ\6^)` {*n{ Ԍ"80FdN<E8:yh;Nu>J?ImkP[k!ԀhǨSz؛z~2_+P}H| v617x %'J(p^zk |aavPHE?}{չETE r# cjOn7.yJ~17Q]>,].~)+9[DZo}tIQ/En=O_lbEQ9U{OBR+)p9QH RN1OUwhėn HeXwQ~ =l킿G*v94ݝ Q::D]'עxEh#> VF:V汎(q$~z#2£wx9Dg0]r69 }ڏ0g+];X^WOG |-,PW݀=3F g F:~E!{瑻8DGIv`yJfǦJuQCc rpp .kp{v{]">}lYQ#Hf%>Qǿ}o62mUb\̢%-]WyfI#.2joȣyT"J?*dxq6XQ5D9g*:/:{,=[2-h -'kfDRȄC M <6_Tx˟y$mgCC?cF\+7J uKhڻ?0zf' ~ͦ㋛/,]/$/S3} ~]fy_+mZT])-߃~c{C=VK]B  B|؂U.ZWCϦ[>^/6 q-='||7B"/|'2Oеc s"z6n_xGJa Pc6a k uA aNɆ,ѭ=|Ґʜ=]'u kOkGx-,[} Eyv_nVzc]mW^v>nD^;N*d~5 xX^}0v}_3^1Zt@E%X_c-1ΑDpAcZQg[$=7=r;W?GNǨ7ܒr$ ǣt%;tl;:oEUbvj`,a92ar-kᛧ+Zx#ZxXk_\wFWC߸N~u;]jk _O@1Hox~= 8҈%fVZ@u4J&g'467W渶7Kkt *vwQkMHԪPu[[ppYߣ6نD4jq]mB"z䲽E9pƾ5l;p[u\|L߆H`;E AŧNX'܀$&yE.e~Tb='W(Mok%g/ۨUywm?OxQ⪾[߁J7v%5#HM>XkK 7zmm~m P YG+_[>4pm|tulcrjNÕ{vZA.;-.ζ;l_;-Fjb,/ ǏPGPvȡhXp0{3*2$\L邮u7 2d{^ j7BḢʰp`at( C 6w'5z $,AءS. 0!쀧tq]6L6Wa{Jv;C>9h㸃]d:p6MLnsxso}xj\K=C&BpˆŘorSz-|bٚto %߄4jV$/8.i0 "Y닋ZW=c~>F{ cAwi;զ lgLxt]Z!SsAnɑTT]:!,=xON|S!]sV8 ľW evdd!н`/ϠoƽM]һ6GP:H#p~iq{x*lgA8A3`9'{E93^uM)ѼzQҦ.H^M4↤~t= 8Ѐ퓀Ph?8ܾ4ASe6:ٮiSNC[1\k0O9)W'K>gVP ڴݬy?يqN3!=zㅾڞAmɡ'}zS_Q=DZOHegxs1Ul=:5RQ5p^ǺkCF/LՊpϨ 8 ҵ_ѻbO2De'pl)ՊG|J9<9m!zge|Ϡ_cC21"h?C3Pgl5>ltĶB'tLrz|6NY-#N}_y`zUt$ÇM/U_ 8ZIոvRaڂeZzxow o͟<]r#.# sx'RbU=.~wj>߭C-X2߇Gͷ̟J^c[ i>iP2͟ "&2DnmDs}k-Hޟ7ug'zVd- ?_#h)zBd;G؜ڼʞ/~{2l0 3t6?O1_dj⏾??xï?C=${B([O{GD5yoBcZ8~"!F=#qY,(;Fh|v/cL>y <F# CcFm$yT,WgP^q4 x3LdVGk4ƨ~[TOj}rd0ѩ,O|ɺNSp`5&GDW.z9փ0͗JeK_ve؏a w x :2{axylbsIe!r{_"V2^4Fb0wCh@A1LæZ@d#N{玢s_7tfتQ3xK&piJϼ#@Rߵp?(1?DrȫQX=ldjQŒr!W*Y5 RXHNR\(?.;H\-2EvP=զW^]!_I7%GߤԹ!tң$$ɔ v,uя 57~cnjzF/jHķI嗋iNCiԱ@[\?[xCkȓqKGt8@uDqa==NBuE7=zG,!P:%rP):mJVԎ}5̑RL4:hP?K1Ԗ8vȪ .pwdi㥬>ϐ8к8.w\7L|*ɡ>ip5պN2 'ReY.?'_7ɤ+Iwz-{ɻTywrكڃڪ؃i5ӵl+Sk 6a=s4a>r".MX.f!W7*,c&!9Rkak)pϓ竅(O x7/ﺹ&ΤU~|]52GIr&N2͋,]=Yr̦N.,M=t|46a6ӎ6!+BnMX֨M>NwK=O:Dmr|iz"BIF=*Ll"G=AvF{ݾek -%e{ m%eg m'].YMOO\$7}?wEJbYQ==g؉mVIvA.=rf@~LwZ_>Gz+r r+.e^zzeD镡~IpevZ[.Ц`/hSsjBJKJY"G\jhєr}$%ә}\MU;İqK"K \r?X,Q5"#BZ|^ہs>=mOO-]ő!T~->tT4H"[AOi^Rp$N #8DC$$CA=X~lKծmܧ#zy>8kTl)ʻppȓ (ѝKSN`7X{1R+{Nõڦx= dT%! >O SBWWSPNљJǡHL3.S׹U/g#\CBmLctF`X(9[UEnU1♇iqYu6 WV6bxs}x9>yt}эNG.eDW$]nYNtd,JJ+?3Iw~>ȧ%D"WLɼW٦;OyyU?/>_.3lMb&+VE_mI&&#[* oIHښDG?kMĿdsBU&aey'$+桊'LP&yLJ1<ͺ3 :V8ws^ϘǬ2$^Sg䩪&cS]MlNۗ ]Ydyeju#YPO/ߓxIX^$MF/^^$y$uw0t#% Tg{j>Nz o]VW%:*Ȭ#5?3R{Eӵ*$Suye%6y8cb3YcRzblrZ{ 3 9gY/Of䩪&c7S]jeTeggmfesfsOo'摒s_egsBu}I0vFo&׫nBv2NN߽R']!ߝR`u=8 ?GBw0-,9$vj2G'ɒe&B[M=:Ufc.xq ;?f-.Xꮕ%O*jkծpB!o'-vNdMn]LVٞH3edNZifx(tcgy糒ǬY^9QE{Fz3 \&Yptn iekӝʦO6v3|{=:; «܍&y7Mu4}Xu)ǀ(.> QiQViwC7U]p>%!:-Ԓx4O΁GyEJX4pn c5\_?s~y֍W&t*OJz\P5Ո[oͣW.\^r{嚠~jOjII)ضHÓykSpDjz!FSٔkEkϴ\;\g:-J̧3(S-R8&3KaW(&/ѷsd©ۭO*WՕQ=}NGD~a͠ K*ÞAT32 VKG)ܥ2rKNR4P`d[rBI9K߿4 R Ĝ,5z mXto \-)0}K{9~;%"mԢ0Iub/Hmq˧FG}z2?}?KF1&Lqx%Hy${ZOW-tBd# q%]R}t"<3$o=ό2N|)F׆bN}駾R{>P!+1 tO P2AA5J~֓_!jAM,B X:Qc!G m$u t~V$Z'J%\S(RV~8JC6\lW;YykAdcg<3<ǁQ:Vi6Wz:bt-w)3՛s9y*>RemIU6U1vwVr\q=dl@mu$vkr^VK8Q$'p[Ʀ7Md\axUɱ[:3֨[J]hH=tt|>yHS?fVGo|E߯!tpQS wia!w ރ;Լ3Ƴ^}&OܼV@sw=UIO'-j.jF  @3]x6lq5]u *u>XxD!aXiZTg4=nCQ^u+3`zNC , CA!\7E6 t-vE/ܶi^(~:G%0N321@6h}:KA{ i B9Gt&ٿW3mkBSx]N0Æ WM- 2*5X|DZ=fLc(F? hҶ BLrGpodgd/e>f 9]WA\\Lf[xilϣTs1H QlHاEt(SAZ_Y X mkBTWx흍8 FSHI!)$FRHnw HYx3ꇤsaaaaxIǏ'U{o_ھgW9 o'GW {>~Jlo߾)*/N\ϱov[iZ_ձaJΝ/:6O- 92b?Tlk%?_21B sY5>:>c=1Ow y^- ڶ,XzusM#גU]>H_yYv!ۉ_mi Rus]Xm_g)YY)m]y,m z1aaaxEߓGקo/Y\k6xjgH|yu.\aæM&wk#ϐ$?]Mo\Ⱦ,/ڥQ@~6s?)}, l gX #vQg Bٙ^uのuhm?}{].~}v_J;xogJY]޳@.)oqC?}>@Xߘ'-(W? źvƔOʙRv[K?[A}?-wmՑ}g\=c}M ggg DŽ-B^k_g?F? v0||؎=ǧHPgs/hؑI t~{n^}ZyD5XWvO)"c0vY Z|~_%/,p\ɹyΰZ/;/xs_9?Pܯ5ݻ\[y|č8gʱL{? 0 0 _k3>z_\S |<)b|7aaaxn.ta?l^Cvkؽ#~e)3<3^kdlc&jK+o"e<.ʞ`^(3zu l+6v<ï k7]/lc[`On}򚄫 G뎱zt^v2)?;Wmr5ocIz?Ozx{&!ez."ѯ 1Gg{+ҏlw<=}GݽFƨ^)zIpG K֜{{e G12ۭqiumf>.}~a? 0 0 [u+7Svq֭y΅ ?ނ}XwŶv?ߩDZۓ-q/?߳=<~#>Fk"qzrQo 9r,nY[;o:)@-`ק-7({߯S@µK9֠ɸ>:n3 _[_*mtcmC>qSL=<6;ǫsaaa{xˌ\ފpx?0׋#5zяc]x^l򼠕(f:~٣^lin59W~\;?vn6erUbS~v^U O7O(|;+SG4|?f*?rW~2oNٟS9~daևmH6mX[J~s.ym4ٶO|Bd/b5ɿyU? 0 0 0 0 0 0 0.P~*1@G\⟿KrKXs2(ߥ纎J8'>X@▼QQbqwx b)_K|v 1M6kee-2Ǜ59?K^E~9ϱQﱮYF8N?~;:=J<-tĒyNAgC \NXKs)'^Kg\~2}6}Գ)n]Or^j~"{p29w6/.z-v:+M{WJYZ굢`% Ҥl9ힶկ#OUz+U?;sd~vND7*.Y+v:ye;8}~|+ÑޅN9}{Bƞ#txխsXɿkSV/uJ=o G<ջL'L:D]6jfgLz/+ؽ[{rCMYq~[{yy czA;w9zszWHVax3 %mkBTx흍) q ĉ8D^>׻gI@XjjgiЃ`0 `0 ?ϟ|:seQ3|ӧO|:2|.};7eGFO6_Qv]T]^ˮg{>pjzkuo{yye?{-x/ D:3D&򈼹e^Hyi#/OGzϪ߯_~ :sMe#M3Y#=2 QЙ[\s=E8}E>GȩT ڲTg-}VfoSVwzV}./>~!?U1<#}=F[ ~QڋBN..+푹^edLo+[\-k dW(}6q$#?z6Bөi?L7!3O_Q}Пuo[=tkȋM!'}/Ƈdr2_Cﲨ: `0 :8o=+8-4}۞cĥXdq{bUq©ήm!ƶg*ΪU\z[GA=^+ru{LV U?)V>ғ)x|Yҁgi\yi^cUo*= !TY?rfgWsʽVn*VX#=Fϫ+[F~yH\L~[O҇h5ݵTow|Sfӟ+);F;:x )/OS yUo2e)Ve3'wgGg=J^`0  ľu kU,Ksؑ5nY,bXw{ w&3QהNQev ]ƷgcH˞i{A3I8hwduwUIWq8I>+@pQşGcZ\ƪUߝ]/:3d;ɫ:gB9R|GW~w2;fzt|+i5nΟgZY|<1NyŬ|E7k?z/k><=Α}N΅>uWydʬdz `0 *\?W8GY:Dgcg< 2+'W6qn؟{ru"wU쏘~c#T?+y{Q,,^qF/Xv8.֩g3}ȸOP ~n%hUG4(_sn|W}Tg&x^c,Fѭ+ <#+}/Uw8BRh_|33!mr\7U9m({ѝpvew[xG]߱?g;,nҽow8]וb?OV=Z_#ve?vN_WrYLo;1g9pV^G~>[_vNOS3 `0Q[ veO\k^8֔v<Zbz\Opbn$~}oz3ј mK vU]^iNWA#x딫jt q :E= z%օq)CcYEqyRG-+u (K\hP'*^ء^q=m=y|Kvūe\rȊ4={W1;=ݷxp;o@>ȘT\Ԏ+C=*ɫ|GJOCW]x1.ﵠ9_Eб Vq)v(ʑ}[GwǺ{-oSdו_˞׃2;iT&w*w:g׭SOsj%Z[~_˯d֮+w]7 `0]kIu+eL]ւoA^;=GR?v쯱;<y o$N1紈=:ߥPVu< <&3KyC/4r)i=*/|Ύ^]QNН1qGw>ù{ ?Kv:A}E:_n+{u=rq͓̳]>>d}+|L01`0 leg:׺񶊝`W,3O?]\9P~[kOWiGc~)-<w.3q}'vuw$Vnv(r52S;Wk_Kϔ8B/hEՠ'9w?K;x:x<|@cϽVyc@ۖSw8Bq]=2lBe6V}eR( VeZT4ade2ޒ+nYBTqSߔ<[&=f[|szP)G}{Zׅ3n7jpWwftEw[ǽ;`l? `0 `0 `{~i`oLy>uoi\qK|}7Svu9G쯿c¾#>,jow{ՆݲL=mW2u_8دjo?kD߱mw>#}E:OۡO;y`$jwymkBT6x횉m0]HI!)$FR?6c>>~sm+vuՑνYu8uN?WP>1JsWiV_uKEϸ/rˆ_gKW]ױEYcl,[TYHT}xL#}A GV7^}>iҞ-i;}LJX&TP3T#ߨgJl e'=?͘ona|7>?ǐU%;/mN/IfQփz{G}?v✽3X~j{zTAO^ʰ>?sy|G)PU{ ..T}6ڳ-F`p]k߅~b  О$wݓٱ|sCoA+q3lOx@(0a+? T,_7s\Ϙ^Bl1)C+k(FyN"8dPC_9>O0&l4Im+nwGrŰ)/tihf ѸX>E)<,6s45zb?J\<OM%O#(76:= ӋYAƒH Ls6MXBcX&ǘJte. 3.je(??Lj=%wZizFTx$kP8Em jAOހ>~؆B9 ֤8UKCvjbL Cy ;mj P. DkwUE€3ܨ8xUJs\ɟ+;}sFQ(KIXݛƨ 1 +KdX];Jģcx$D׷X`i @l̏rnm$^9΄zBGϞQ=nfkDe; <a>,⢞jk0B[p($Ǡp4 nq`XƓ vϵ.xHnorJ5Hu뇗 f a[Z:>36[g RL؍?( &w.7C#~B{] UW 71jk~ecGrD.=K@WDZM0倐0\xvqNZ ># BE )&yA}t?B Ym(WIpɱ |2+\2 )l8tl@Z.Be񅋍RSƃm>dIl'N adĢG3%#)?$s _5=YBR#-k"qGP-e"f%֩-ϓ378M9ϊ,_*n;HEBƱcl~ ˝[/sagIE2,z1t:kLș壋G){7ond{@rP>kwk׽ #kXfyEAB9uM4P=_lgW؇N#_nGpp ,ZUu6ȓVӰ0EK7*|]{75F\ԶzQz! uH>upT٣o3P)[^6` -d&*=%fY<^ط`_6|h3ء>2 Pq7ώ ,NsjF=B` 큳CiU)R鐏@LҮǧmb<2FHRqùFXi䎲OmGA}:*u f:@ʫRH.66jcGOpO- 6HKJU:Jǃv,3DZEƮqq7p?ȌK%ȧ$;?Qr6pP7`a^=R_)m>D3#£ _' Iɭu͋C-Rne㯄ssL<ȭ/R)|Lt_1Lk=rr 4/gEr~PnB[\g[{gYvRW' {Fem1{ wL;7&$xc0 n&u@5sCCձm8Heft x{q(aтa?Q%l4ςxmWI׆GC1kQ3iJh,KRO`ʲ4)%b6B8\pe;u)ko)#WSncRx{[sXv195_0Kՙ7>Tp5ٴl3S"؝LX睫[5m Q="u}pϘ*xbՉ#iM+@Z! Ϯ~jYݬ$?5mtu] %@݅:4h8ۃtu3; ΑO1A/r R*5i&j#Y2:$Z(ad@>'z L뇶6Z8|`6"X1_z' F-я?X^ A:?1;h/KVB' vOnFS ƤQ{=kh7MwXQp\v͓O/. N3HKRlK"q^Wh1wt h@3e6N|I;y?8t[[! $,ήLe"z%IކAkRl!3u8ځy?_W)AbCO!rza5Sn֗#<43y6"R߃CQ&>[# BHǽ{vekOTlq(UH͵h ݔ8,@tՂL{p/*L"d_y k,4 G̖bD>,.ok"D;|7[.DCA#ilϟI֬Dq]+eE _-- ڰc^Lq1~CCC9gNH8BkhJ#Z-`VoMa 9r$պZ-hkh ?C$ ^tď9d(8P݅]ڶw[wl;dn׆oKd Hބ(DInI M_(5)6H/Y1 QRk,nXHʉ?>df&6^EJmt{CCc`0ʅv5x<\9Yc}106"״!֏9dl:' 1H"z'7QqɌ#KR./CVgQȬ\ `?d1yuM6Ƶ8ZX]8^pwQE &1frRKi$GݜЕh3'{;;~FK37ku<pdʎ+C RMzƏ7)nҀ lEGyl:̑IoBS%|ЕsTulebA}Aʹ10A{KʘӺtjdLI=r PRg_LbR Şl?␔)![Fo wi&k^CV(t@pW2{hxHGRn͉eCbxԉ6GQd27\ثdS=\Ff*0ۣOP5(rZߙxQZ>~GAeN-jY7Ҿn;n?ӹ"Px}/NW:݊&׾:x" ꭥу;R펔 c䛅љElmG§a= h¨BG_uYnZ쫭FYs U"zM&:Gnu.DX5Xn;}ԫ%XO?~2&Frjj8 yA*W I9/ub)Zl: s 85J>~iI3Yԕ;:#hELם[ROd^GA˩f~Y!En0~/A Km>^WYq"<цF*c:xw|͞w%ehRgd9̕v3v Dgh>>?3hYDkgC(ʹƒԕSԜ| 2Q94(?OGQ34 fccPopTYaW(>@tX4`LGٞpɄaŰl\[9c26U M6f,'C4i?W~psϠ?kAKrŵk@I|>^xs?\`,D̒5W^w DMXf_8<%|8_왉pP1Wlm߃f?4:́_Ԕv M;k:p_sj؎qw]$F}y ,b'N=o0, ~M YR46+!}@~ujctCP.Y(x׎z?70WXFܣo3z0c8RGg0 TU򄽻w"/4֏CQ`[{Ocn]+{{ N!33+5]qpj' r9FDȬ)~: 9Gmx2-?sraG"yvUpa;Ră A\& ?#n 0eed~oq嶭!!DzP^H)>oȑ.ļԶ=Hy7S-M ?8ycߧq|#5"2Б lm#UeΤVbM͘jAc7Z ]> 4gb s 2WRsKg6 's8qzTT[R[w)I95xWj #!nN+zPڔ KgTE,?{^RDݥ=Ru^zîc&D'i74SJߔ&HUG[crͦ<׿~4}څh;lpAZ%XZ;tQ?yk1+Ƴu6[ Dc4Ɯ*dB#!}e>samhG3c^8u9󼵕⸈߂UyB;f "Yi=D =4&|C3g]~WgjhSIXU"1A5Fr4{AljwTt6</N \Rta| i>T.Wo>>xϯY{緷m,J{gg}v~)]s!?wXGFl!7U|Cnfﳅ:.@mq%臔Ru?.:aBֺE#Gg'yXDuSWNJD)21ѵVagWPqȒ s?¶@g")s\T{f3go^w:^"{d#!φt},nyWFKv„X4|VB~,˘_&fjp/WԍwaO H 3I`u1ͤ+_W|+_W|+_W|+_W|+_W|+_W|+_W|+_W|+_W|+_W|+_W|+_W|+_W|+_W|+_W|+_Wݚw)2iTXtXML:com.adobe.xmp Adobe Fireworks CS3 2007-10-06T22:15:44Z 2007-10-06T22:29:43Z image/png IDATxOl&v! vtibۢJՖV*J 7=T=QjU@J(ZuѪ]BB 'NDZ=37c;dm1-}ӌ'{Ͽ20WR VJb01Ub \ne$9z߬gVF~W] y3^{>l*`GLY$x+ JuݧdÇq1+q\qR_kkх~:`[xAmW$ ۩oL&Ϸe +G[UL&$Rk1F &{BACcuֵo=zoܸ1dwψDڟi`q|L_ϟ0Ưea^Mﰢcvuu}1 h*$3/T,>`qD" |(͛w;vl'x ;0C\ZhȌ?԰ 0?}d'Fp'rС@ H̐wb;Z/;=myjkk[uwfl]-A SI cF1Bktww8娫R$n*ZhQ>q c|| @GG;v.ب[D]7O:u~+RWW߿Kخr+nPh gN%M=ʈ['Zj[KA0bQxV2EoۿٹԩS߾}0kE^fA>'s3444q̙ON Adobe Fireworks CS3 2007-10-06T21:59:47Z 2007-10-06T22:14:15Z image/png n?prVWxZnI8Ѭ)1@#. f&# шK)&B= ȯ K< ++S^}q<V]\sٟv OOOi9'dOʶ/)dz ?AxKoc[C3S? hå 96)v[hg)v9F'2oe[snR 6kr}}E-W7$2P_ASmW` WaWgkkG֮,s1EV(Waku{ 'Llv]i= z=_5JAk8R"/а+[$˟oIÇ5]9Ub{NBbepp_i!܈K=';9g]*4um}|ҏ5\@+p/D$2#<-m0?!ܛd:ew^G_HEG2"~ApV^.Ͽl癣/m~+L:vF=h9>ا3u?0(.B?pMq:bPy?Y#m+q!j7S+&=([?S川Y"ty}|gMЏ 7gmau=??_[?6|V3v胏kKG6Eh<f#XfwbvVJ.p\@g.j+y3%,N*:/vF#H&|?mzP(QY#P̤B- ,3P{t(dphGHs+8 8mWƏ-`` &㙵+P&/V&>.b ̉f) Ǥ~5} +.wEJAmZ-"ycԀq4+ 3`aW݌F# _@9;/'Ln-j T!J;b+@?YM XkkG֣t(cb6) .I>a9CW>E쿙dV U;,iG!y/Q R%cz5dӒ< A!T9pFy[D:sȨ,l ;HF߂ڿz]%Lc[q3+acktA䊲E R-Baf}pcQYׅDBgy 7JR4>ū<7XY`|6wZ+1 &ooELp6­aA>JYj֣`zQqwc0 i_|f3JFɟ~: D4#O(@i @w>)AV?BTnKݡJ4 oPU"c,2]@;B?b5|5 (LDJ p%%[+txu bg QD.+yɊܙL󒜍l/sDj6V0'&zL 8d  .ܓ,.$*p,._jF0@GGH/7kDhõK'0B !#1\5[~Ȝ$2TW쟳^.L`a֡Y/wW7(B so,YG%-3~EZTq ڠ6AF3HNYgdd iGK7uQk>g 2o9[HQeWh7pgۭ%s~jF:#6*\g$.in:W_.Aԕߩ.XA >RfX?ˈ-k s.˹,CU ^cKb=TNDuub5R/>jzȥ7U&\rDE+<) {:z\CPdv툏}lA`ƛ{Sb?)r:CGhH_=9?"zΘu Rژ ӦT8w R^-_/C`;u> ~.;V[&vo,}/.޶W]jسҹgC9 X1/'V^)}9cxX T}J(ٱGk8 7UR.6d|2Jw(4A1K1IcRilNغT[4]*{4쫊!g{uAל|:%Fa8/YGwO"E n9;aIoߙ]]W7g)~]g)mrRk>Z."M8t#w&6K66a ek@zjT}lL1E}l`dž9)>}lSOy! HmkBF)3 mkTSx}KIz^jwg8|KkH/Jr|c~;fmUsȞ@ԓ,MM{Ze|ڀlÆo>d6 { 6to_,@㏈̈Ȭj2Y3UD|Gl5{gzudld0{>9g{rkcrw79ig޽)$X$&o~yց?7wOZ5·?ڹO.ıVz)gݍBR 5.p?[mb^nemo}F`]ЛΔjm>ܥw{;l$!gwI..!qKnpq#2CZn# ڷ41sYffYTl|8(ac`3lRl<77:OSx&Iiz/c]@- 3`g#0R#nfMT8_t"^g>ѫ; 5ǩ5d ^Qjt&~r m?&atat mӧv 6by?͜S<{$0XJ@t#o$ Z6BcShl M)4M`k̢. | POvD9=#"R|O透t4# ȃ!7{&{A~Hje0HŠtig\`-30c`cQn8V0zabxκc6)ѳ,nKў3ʍ]@7F"7.ɭ 3(@?9T@~@r oM1 *alB(TgFj@y+ԢڅЧlr᳷Fii(,xiiU=s|0AԵҿ:C~6ܴvt?_qZg)J 3oB#G({6HArX ğ02m HIǑQn-_/_! 6#s<1LHf0FK 1L5qJgJeEF2'HptSp X*"i1$$0Ù9DyJf]Z_b#4lL te- =5d"]IW;:0a<;$$wJ)5Ivӱnc?F@;ߎvӖĝZkG&hMbgo%3:JjVn %d 3QE6gx^r }v9ri2m]͚4%>!EE[_Bx5WM 7Y䡫x#Ro=Æ&IFC఩Zd˿t/܅tv wu]qI)&yyBfqBv!ꖘ'nFb&z5YDL_ :L /ÿS`aod@&tG3ҧHzC$^="܊F0)ۥheJ%NI16 65+ѷ=\ n.Ι$f#m6ie&BodhWfR@y_'NUl4`pޥLhg2Yxx*"u _u30ENg P mhΆqh<ҽ+0MVVcl6tJD;=; {\n܉f1:au"I$`ᑞE]gpβGٟgO ! t:O1s@X򡭃\(w+:_s.XtcVjXmx -x# _aC<ӭlxSvgjK۞egq+?@ p'>Uԇw6V%' 1;:_n:'e>E_(>~s;zO",T2] yقߔ(0g0Yi!dShhMM6ut) X_#fI9dNj-3Y'T!q 2cTErE8xgzg6܎e0tq)_Ȳ~Bc~EBBhp3 >Gi*wZ`ep`/R0le[q@ߔ$CsxMk-jؕUC 9Fhfb؍+4P?[ Cplgy/iv]LSnqRg!+`(7V-G?;Q8 00B64pM_a=i"}蘵7^ {^'m Hb2T{Akw70IKݦ2k8Z!ᗌؔ*O%>)V%V .}P`H8O0!$vh} lˏhYķ9MXLxGl<$mq9„˚bl_.C/Jb~~o~2Ch--V?/~ hwUo:Ca/ { VV"FDF!W&f[yz][l_sos.]{)ee<}yNSpWA(ub44)O?] wDV;ݱS6d< xWMΪ;>`ݱq. G1Y20棄L Z g؟A$1yHFg|/92"B bjG $cc$-8}%Y{oBlr.=IYF]r iܢ )#xy5d&g N8ҷf˓M3nZܴM[){C[,V0̈́w -kujӺǶ\kFMtM׀'6~Lـ:^x_HyeqLu%m#t>X ֯9rXH("䷅gM,oN0>*HaEtr+9{mj`^\Gj{#z_AO ߡR%~aL#a9CC?{fw>DVeA:Ut5\*\ikΕvvjXJCWz^iJCW:m۽Е^Vum+ ]ihŸfqE_>[Jg/Zgήtv t%@xSJG/ZGttsD>]iJC/\C{4tC 7rSi%ʻJ;92ˠwvv~VUz4tЕΣߜw^ ]4tM4tˡwήt~WU:zttѕxsVUz4tЕ4"/gx9љTZkӹX2$Zg-4FWt6)-HN8?H"fU8^+sZ37o=6;H\kS8&UUq֞hpϴt}C=dz!~d MɬȥUT޼`֏#ir>֧^/?{9I9u{OBVk0)F5Gd`w kPΧ߻4K{OEvI$$2Η ̬zkn _: JKWZҕ^>-u_D{F$v5)t#\}ӛIicW > r Qn6mۼ6]{eBۉ?9:/Ol(,Kb4ɬQ4DzT,Yq<1!F֡İ8++NJl8%_yysDzC{_pQ&nh;o}|,rEF9\bCOɈfsT\g;ѷ4$.,wM3 E2x35D֟F|+}."<_c}n՗i,0~WyZϭsD )]OxǹmANZy S= /b=y|\wϿɖۉ ׾/Yoҟ|t1?'P&:Ԯvhf| r= xMLяy ℴ/>u"'k,Z ļk/8z)/C/W;zK[/HI-_;K\ [=xYsKlwu= ܯX_X䤩'%HYa#ZF+\Do%Zc \4On~ijq/kiQׇ(IdGVݗ5TԶQ"ߝK9t{R؈Y W)Ղ e}ۨz#PIWd=cKXeMwڠ7vŏ/C=xy>4.F=NE WyrZHʜ>(smc {-8גz?3gב~~9c.U'ݍ{]f/;~NOYI]6< 'xShg/Ow MӞf@φ/[ЊݭIc2'}.lAI.6V-Dw.%e gvM/=7 Rn t~8= ȵGޥE.?4}nҟzkv!&{e^{;$z4fgG~m;]6Pq kU|pEr\lD@_I} 8b+Iz``P;{sIEpggKmA:[Bn9N%Udӛ>U ]EHC%)6*`tڦ7@>Enbo3Y(}s66NYG2[6MBr3rs5}&b٬ Q9P#eӲnu;[]dIٽO+}5L[{;YvPi"H4c{dф/{:mOyдk^~Fͯ;?l*7mq[~UAoP7N R%Npfk~I0 |` J/#nf-קtoCn;V4JwhZӆJY=ֲ=Q(S=b"# :q !A*9l|?gz Z(w!tPM"'u4X'Jt cVRhX!M5lC]1$#AmyWk4<;gSSɸ o~qkA f6W!x69{NTS4 )-uvlӪM69P4[Swf R2%2ZipѨyzx:LC$Xk6kכl2M 밎rZj)d o-?$c-τL_"む7|?MPd @ZlDƯ5Z nde#JPkm?(ʶbIf,rAB2 [fHdb+=?ZX :fԁf+_'i|(_R \}d F[8e4-7#+ 0/|JUw-7:]jrg/|T@GVM˧ہPթ7Y # ?4V=2c Ya3Q: 'O0t[݆E:\ьįhsīPZRqL)zBE&zBf-7A}iתv)&d1Zi nI%P/Ȕ/]KxnyDei-6/y(R7iub0tiVf?i4Gl{Po(zB1@ǯݮ '2i> ij %6 @Ff!\ ١4nU ٽ59PUkvyh 7f ub,/Axh"P<`4V^,᫱. d] U{ edojh@yHaSi1SjC A+dpr4*/]THTk6ؽdyMKgmZ"뽐E+JSf J,n( Ъt bG/x))/YEŬ!H_q| U '7SHķ&:\WIE"oqN+ 耝u ata:0-mdkxD]#1zÓWucƒ~>ڽ FbYGezlEXW2Iw-~'Y BRꇜDNU|Ƹ\7U:uYŤfGƙuE8;֛0eV. JEML}ݘ{[X CM} N3js垱+\ ˽{-7Q'Ismraۼ8u`=dh?&IG&R;Tm  ""xDl. v.tof_'?A@*GU?>A~Xr"Qڏs"}"o*O'C-uwA=R]tFLнO/$X[z̈]74x*֧@7329 ˾i*Ҁk:9>6R?ÓEhm:kOgrآ/]>~s~Y~1|}"3ΡOC[ȵޡ!b_!kĚ`~3L{u֏4 %ՙ<ŝWɵ54$z{YdiCVOoB[oMepu2vpO`ڳ!z,}+KR)SVe=S \gzk or#OE$\Q!r!e*G"r9*8%VJw?P<l2bwg ۓu/?};z+߶ݲbWfnl/ra)GX:kZk/fXyp??1Chx]kE+?_hi}o?'2?Q( ڼk/~k>l 3 t46?O 1N_dg⏾?Z+O_G?C3$;X+o?=CKjɅ&iQ3DZg-ފx4pKHItD/Z3v%黂b[4ݰx:oDzF"'b1/z< eD!o`G8DgudRh.UWOS#D'b-7QjjLZ!l2&u1ˑtdGh~ZٴeGZ61qQ^<'څ2S6@ƸG69f2T]rZ~ZQ&^UVkHa tn(!U˶>\V*6F wj> 5 4jFW8 H:C.AĻ)́+SP wQćWyZ>a%c ;X+*R5m!C!ʼn fp,9iPo}mGK.zK0ȜLk(NygLC( /a( itO#J8˹.ͥ2Br$ :PқBIAI)( @ɕPrs䦢t dTN)?y @ʓrH%J^!~/g5\-HJCw/M8rv qtCg{$ZyG̲ocIӺq%?VzzFRdusm{,_>i {F: AjUa_gɣ)Z{TB N=\e,PM]X,ѧ>߼ԅU1lu֣T v]-lF~ ijW珛ϫ&^(I;INgdI.T"j9fS˳ j*ܩ/MXMq7&TcEf5jSȝi ߄M/X֪w_,"k #<Ϟ&ϼ;ʹ=< 7(;%Pv(Z^ = 8ϻ::^i723w)ٓHo2iËweYP[zIW2z=vr*I.j1G ֏΂WGdOH*gˢl"\H啑+Ck5"Z}M^(nMՌ6Kx;g4qlPR%4Mg64rUnbq)K$~4)XnDFO#$ozZ|Bi;#vΈCZ &򋧔C&[!W}h84 GE4ܐp)4dGAiu,S{DD2,]ۤOyڿعM۱Gv=~|t֚=uw=4)+'Q %d7G=Q)'{N#b|=lt%>OStbW5WsPtKY?c:eFte:7¶b;r]sAy.XBm&A%JD떥u[]6=|ab\VÕU#ղx.^ή^.O\btd/ej˭ʉ%I:wEux&)⮳oPTb1)it5i8/jZ^e擭y,;2$證klML4YhKV6Su11pCTO'^I:xIXY$O,^Y$UuOa'KS=lQ;H+MeX\_T*#Ԝ/9+*ת%џUy5"g{yz9詞jeT?3qc?9?Hs?}s/Lh|K՚_ʻ <;=|2Kgw9?ˇ~J%],h> tֽnf)W5&sUkv=:Nⶨ6ZjyVжv=kۺ;J'vA;-OY/y~)*siy=}_oz"c=tV~hDiz`yFg&=#\Ypt^eAx 'kӽ˦7zfuq2_W+ҹwnd:{iFK\M,zgn|} }T<.8@j+ =ZDz\^r{ެuQ"^g%楬{cY_dJ>юGʳ 7X/\'\w庉z -K(P WlQe>^@m/X6ٞGB ҽp8M÷#V#|N'HUwʤ>b *O, G)]@R2< ^F7\c2r͒KNSG,Xhd= sBl(5sRҌso|{cEѲb [ja)ht*b&Fik O|QjGHMX12b=z-Wؓ)W14T\ ;i}_Ft9*1t2eI*![tR, 2'eqJ9?9=<%eY!nz[ѽ#K$//K6u .l`l&ɦ{x%[|zB<= {LrYJH/2z9-nEK\)X|>zggd˘wF71ԎQ2s8y(V}:5^Y8z^G !^%U }DD*+;F |'#߭`}\<8uNc+ͻ"b ֏Q~{q~Rb]刧>S ?ul>mx +'1V`OP(QVOq }(n5$ kPS$}ϏVCԷC\0+JṡYK[tu[_A =~%av'Yk[Fu#&]zİ;B΢+ }JWSMt >Z'zZ~g i=a%C'R"ѳ4ڍ~##՟''tJQ箞Qe691bї$Z(US;!N3M6]&ľVIIC]ci/_Ƙ[WW}q".#BWOW#xjpzؔ{Xg?}J\^z Nh~~ILVinDͻ2niL]k%:sK7Gsq޴>UGu~%o\K:Ry,G9^U^.zG))p5?)%8GN8v$u8k]%9K1pɷ~r߰w Do>Iu4N^dF Wª]1$K/v3mmo+yüפ8Zc}PFmxH%EnОךeQ\ Q Ih -z*rj+{iG@Yְ?´CcwMKPXrJpc%\H<~SܓRkRq>sjӮ57$Zmy5L\Hf˓˓ t-ן푟7[ݛk;'/Xlw~GVS/->rm [-6,Nݨg75FSS` YNX|eK\Tz0 dfSj6Q9语Juvo<^܄Bַ'o֦FQk0un]rz^5ڟmAp! w竈˔f0וXYw.͝)^zoܜmlM kw@&&Y6^ֶgxYa6kś;f3=igkkxwRBPzu@Cje6:ӓ&\\Bf㮇6tҋG.k/O(roYp 1 cnn d×-ݭIc2'6r$R.iK.}o2} /6i}tz&6uuu6Q:A~|a|g6\/w7xyB|M[rzg+Z|ԾlrSTӻ;(xNR2!lc&u.?턢|}}`$5j.{_@{ =q܋ai۴UmR9~a;]n>M/|a? b:c1ӈ阆cI4E%Aeѵku|~SOk6S:Zm=7C8?qg:Dk ǵ Fnp6tz>l<؁+WԊX;&ct&k {|&hZh-1F@Dc8xA`i9X/=?^]ai9o@4$j/[a9ٞ:|eh* ]*LszSjЧN˦TxENͷkeSe*[pӱ $x^A4k_Zl"u_0ךdu4d"Щ˚ly&D"]j q|iD9Fhzl"}He؍jBgDZ5;}!aPHñkA'{n˄GԹV9M X`6# lM"Hh2 Q&JϏVKNwZd C^3@FV~WwoZ~рF]\x4d2S[x4e2Įʥ LK&&cL_%B;GELubk 34j[e:H%Z͆ϊJܖϦJYw|7u[%"^O#t1t̅ @YX_}> {i|9WLlF߶Y;ڴ6X]I77ɟp,"[Cs\<dlcsH- ^@au~7g֪Uun:}\5za/4L).,Yd+:<X_etoեܗl4 v 7i}){R yK~'HiMrYO/60Zq ٞwBR8NMKtG*gSQjB:S&BwJ6WH 9 t2ƤK{OKszjAJ'w7w;)lچZ]\I_@6d~ ;놪5bosUG3ɾl]y1ljE߷=MWfevyÑUe 2ћV¢';'>)NuoQ;+jH`lrt t?|gg筏Zоg=U]'ڻp> :PQz1GmAOzsV,&Js!k`I1i~FHEyq7`poL9ŏI!zFp(c-~IUbE?4DS[=V fv.GlgT4ϓ߫LU?tȫף[T0<뻨 zN)LнHzXC-=oĮajk|_֧@7gle=-[^o x-BRځzQP='֧u!53~Bm5amkOgrJl yjLY?L+r1$:kCC2C׈5=c8B<i ZKܽKu|ݕ7.5\<.dҟW ZgC*XMEC痪R,z\\0.gzkG3o[佤}oo"Ns|"~x͑^ܱv.LÆ <@G?oTϰD %JA/;?AK~~?4CBן[+ӣ?ο1t&\hl2kݟJx5sp_h!O4> hc^>}w?o}N<){#y*"=BqBLiIRW_w޴SaD&rkEP%rP*]5-'s CTαBۀ2Ev!鰫ˮNNaН(N軲iT-y=NH{WŹ_gwQL_-#,!R߬}<1VNoS%'%q .%R %7k rH[RT(yP | %?%9+]bF4UBYDZm>d_+ YɸfL/q/㣏rBc[}ݼ/Q7nZR; .W:p=@S?7uaD|RV?Ǩ%]wYxxZKQjoTZ9&g"(^ؑIl/Q秞(UKOp߂s5|۝yd=uAB(u5#7=@zG,R+RW`o"S|ӑJړkzkRt2L(N>Z=vBl: C"߂J&J|9Ժ8.wɺ4Z(F ΋q~M*矓ϛҕ;CZΒ{çɻN;otnA:\{P*הk N׳)WF /{ݥ#GE,sc*TEf1NSriawZOOW Q:=-ln~i_iBU󪉗9JRN4qY~q Z²ڄC'wKc/jSv\M XarلeTrty7!jSy6De.ȣ7ϳ3γ2z3m?Ey${M}#N -eʮWeOC1󮎎lڦy`|Jd"䛹L%]e&^'界nܦJt3c`9Qk8Y+ٲ94H>aq:2uye$ipu?e2DMx_jS4ʩ[A5#R%Y"G谔j#$eә#\Xb\%%e.MJl$ہ^;oP#vΈ3b箉)P*VU2::N" GCr 74\m/hx /ّ$F}euKԢ%x(t??n6);e+vnv]<fkO`vw]]O:sojjJɅtzgp<rS kO:6ruTʉSH#$G'5]*gh=d{S@U )zzNx΍f9؎azkc+KG1+ֵPePRc>&Ѻe_]VM+y'G`pe:oH,K9燗뻗'׷4jm9r,#f$%rrb"cIR]Q]1IJA>-,!,rLdJk6]}5˫ڶWirٟdk 4dU^*7]%ۚώo4|%#!zk[M.5y.ڒ甪:j]L)OH>.0_CO/1/x̣HG1.(XzT^G|<ŕIvu=UwAnv*q=Bkږjܾ\V|F>B9X]WwUV9^)Ӊ%{2^'a/1h/ju/)fSc 5Tg{z>[ oS+V׫%:Ȣ#5egʵ*goIlU1y+SsWbіl>TO'yxgwپbR4S<c}eYF5$xy>cj}Anv*:zgy<2|yϢl"c*R:z_2܋9:a$_i#o׫nB~2NO!߃R]߽R`uu< Z9B"XL)QHkHrB~Ops1W!_-4>&|d|_9O %=G.(JBQ+WWz\^7r]jקzjIy)v״)YB,C`!֋)I,]hnbB*+[xPj[ Mg<ő±t5)N @xU< R*_2oe8 (QpP- O(Q 1ט\Ӕ|K82ٮ(zO$[+J <4{E(x}CvsvJ*56ڦ%ݻaS)_RS?VL}y^ Ut5t GU ;WNEZW,]t| EY҆mȖ2jḔ9K-eB%eY.eN?ONOon)ssIY*"en*e+ĭEtɩ˒MjølK-)X|e*!IA0^+G6OO_z \$K^N˨[>o~ 1e&(2Q:p%| uԯ 3>g41UgfgWQBȺuIGlB! Q9Iw+rxe39NXA*JXë¼c_s_ԩ/XWb9⩏Bꏴ2r[Ak b "BԮ硿ayGabc>?Guٺ rEX%JN}oGI#qRRPXפ1fUb}_%rKPmUUH8Ezx1b6>zOxz:}/qZN7Gp*d0Ga܀{7-p_77+?u$9W1rGRT*_aUşޑjJe5\ h'Oyʨ%X %2ߩ)ǚjӤ9syX.G[G(Ur=)qX+.Gm~O֜cQb90cgh AΖ=BV-)&z qi+NS"Т1C=yj\dzӔ>xyrǶ''NmĆǦkI5ȿYg{ZHC!\cԃSks\'O 9ѱ"u7A7޸9["\UngkMLkmm͵lwwv/;f3%Zۥw)-G/[7I-wH]gz҄Kl҆.\\zef/A*S6?:M̜91se-o<B8b3Nb3J) ͍Sx>IiRx^ (e0صsP p}1C:}Y1&F4VNFoiih*/W:Bбq{5G"0HP1@gp$DlRǼLtbLr~^M 6Om^91l䟒-#t%&aGТ6 s[:Niqb)?Fg8)`.yk^Z;/P Sԧ0=MSPs+p^w1ĉwtNc > Z6BcShl M)4M` Y5E#]*S)@<FszF.Eȥ!"ViGC>n޵9tmyZLi1(AY20n F:7+R&g&g;_n" 9I=˒6l9إ tcDk)r ;ͭp *;3ȊCSP(ţ6h&-y SdPP hP@*..f'Zt )^Ghdqau-/x=Ʊ;aSMl/ʓ ?_qZg)J 3oB#G({6HArX ğ02m HIǑQn-_/_! 6#s<1LHf0FK 1L5qJgJeNp"u ⫦_},U<aCv^פ R!CpT-2_fBvt|Ӌ;亮$t՘5Q˳"7c=⶛ QER$V4I.E;/p=|>%NI16 65+ѷ=\ n.Ι$f#m6ie&BodhWfR@y_'NUl4`pޥLhg2srhxx*"u _u30ENg P mhΆqh<ҽ+0MVVcl6tJD;=; {\n܉f1:au"I$`ᑞE]gpβGٟgӭܖydMu:O1s@X򡭃\(w+:_s.XtcVjXmx -x# _in{| V"VkMIy04NjB{<1Z }]Y1ԫ@nf6[)(B0 ǖ =Ny_tS<$g/>*B=WU[Qn qZ !~vlq$xa`0*4lhVp'ü{D:'81kooM/eGqBl/)UK|RJ.\n5P`H8O0!#Uo}S[VҦGo&'e/( YF]r iܢ )#xy5d&g N8ҷf䍒g8ķx(+nP9g3!e)"/Vr7ɏB6 K@(sYmsDy+艁;TY޷=i5#rQ"g|7CTZM=@K{a1w}]z0g(pOl ݇J,hA碊.Q+pTڹΕv6C#2WiJC/ZC+ ]iJC=vW Ъ4t+ п,=.Cg+]Elٕήtξt({_@@z*]Ehѕttȇ+ ]ikhЕ4t5Fs*ڹZyWi];GYv~3VUysΕv6oʻJC/VUy4ˠw4VU:{9tvٕ6oʻJG/VU:y"oʻJC/VUБހT 8::stu[3zRu: Gd밤+D-72c # otZ\x?;]i@Q~!T\;x}u?=2u>ԗIŋ)Hodt lϊ _Bam]W7$dhr>D!B$W2-1- #at:^mF𿍜t tcH K#>< u@yRr㯇L?ߏAy3s}TJ##Òқr _cqbt$Q.'R֋G/gҘ8 5~iP*}-&]ЈFt ,.a}Tt{WFzii.钄Z2<QoͭPCK\iJKWZ˧?nhH_#1X9#Q8unk~Ocz1)8M`0 Ǵa\N<ٍrmrtܦk]h;g [VgɃu=zE wɗY&5*HJ2גR=+݁O9=B1B=u:6qeXu /qNHqh/k. DӍ-bǺ-O^n#yY:(gkRl ;Lp* qClG9Ţ.6 y=b(9}YpqQo8ch1=osߴOEgpϭ2V2OKt(85> 87 Y =/D~x*|A}!PE'/7!r};1ڷ%+ 㙜@.D=VM،8ANp\{AV\o)1aQs‡ѸNuEKჿ7yE'|`݂\/p8od M}=鱱P>xo}wUs^QwULqg]==]S.XA2 ;V,X؀@,@aY2[6vH,EȌ̬̌)MgUf9'N_OvϽ~ՙ:{z|0}ϱߨEe΢?KVOs]{5>0NS'9hYaVg+l>^v6[ vatgǞ~t !zݟSok0Q۠IxgYU/UNB\Yn&g|.dܛ:ha5dt/'>| Oȓ?Kq\+( w}PRۋ?/!e<8*I?[${v W0_W -o 9&=<[dۦQ.$6cx,<jC뭟W|AIEz l@˳+P;DP䕆T#i!l WgaM]AFĪ f|'Yk_s[?wl^[yVAvY>4,Ff8sw 1ίC &[`Q82#+mj ~" m.^cV¦3XnWVz.7 Nm"D{~zk6 OJ;a]>=4K&g;4 N.9Fmhe{.hy.+[ps\Vw( ׂB+7wf7WF+"[Md##TcSevް.6eݴ&_FO? ݡ 4nVm`l ڱEȳ䲏/!3·LݥVD_6խ͵ް]Ȃ74Zo7[- -i,G4Z.4qW\1*+5z$?wɥė!- 0vin{[F.[M6č9h_[MutEL}/ F@6p&쯁o#hg=?ae.XbJtx# 7eРV DŽ;`5@0aWBnhsX"\kT2d*c vZ]l߶ӷwGOS[ + ^oAKImmB?naM\V[ o ST:s%W(^4^A6VPo6p4Fc[ oUAk G]Vs X.mpθoIʉx VKeHY q3p'Wo.gݐ@CW2h.{ 򒁷{F7x:m~` 1XǴ4eyʞPW &bip8>D`k',;zO>M6;.hx0\7Mon/!˄:tsÏUX$Py Q`"H_0ꕀJkOT O:`ѥRiYihL0E̬JK]Zj0̗J^2W6w`e܏O`  M#[td k ~g@u !2=ecY&oR!i@ }`MæX1[X}[ۆn {՗0 '6C{ncPy莘 %nK~|FwYC+0FUJ緍Xm6P5+5 ˕`\͕HoT.i\qEvզW^]oDI&y%1#./p0p+;(;=<ʅ_η@H-99d!K Jud%+KHA]lKvJ.ٱ\ZD:*M){,yr #pIqN .9cqK \ri&y5y܈?r9D! І yLdsęcz};,;E% X=9b3>߻z]O xPp~;Bf5ʈ̻K)ut47qcnjz/jHhۤ22ԉEuށ98<69 .ȯ ȓl K*OVU;з8 !y0f{ҴҵD,@:Yq i'#32((T: ,N:XMT$`M-U ;Z>P$yAhu-}_ &҉w$=ԇ1ڬ\i;EEuɲ/ɳf;v%tGeJovxk*Ys|}U> ϸ&_o0&_)K̋^{ja|P^]50yÛ.+lO cIJdYa哐]wnDzyQ7gOgOsd;I&3zy i ߩLOMy' EU ӚYYyt^4ǵ1Y:PR28Duu<ˣ7Sȫ'~O7lRBsl+!;9@vj90f6e}חn ˇa+{IÑ`810ƻHI-A=P0?j6eݷ H|;{-][yvI "w.$8;@#R:&Hue}fsM{*I A3+#^x0D+qIq)D;wN.#9gF'S׹Uf3Z̡Pxi5\?ƃ[2}k& %|tuҿ̭*G_W0M]>/xO"e\sfG=HWɣn%6=YҒte=ѱ(-][~$%Q- ![(1qxttJEeRGzm 򒲭។Oɦ[Y![#+pcVsyw񍂎NeA4*Ɯ(J*U쑪]P\cHuil⢲eGOgQ*d{UdhFFsb.;2{0%ˬYŽK/p NB~Iӳ o-NCAx"yw~Wiksi:Ɖ j,'Ql^.AkkAiaaD`l|R5|k_b؈CRB-9H`$L˝S ;K$[[u8&棳>͏gbA,}f+m!LAE@|kksϕ+z+V{허0=7/J۲BVvW`VI-v8#gvV$^Pv$^PNޗQ8S-aO8&3V+as ~!#vUtcuqx3: UKaÖp8p8 KG,RY4sII^x*lK@x*`"Y (I5Y)ꍂk A=ҝx bCvs|:%2mE[aRj^PJ8/=FG}}z29~ZcS͍7ddN*7Jh1y(jZ[ Z-2k.DˬTZ$)Zf3drt2kJ˒,8Zfc̖bTb j_nfuӞjt'ɗn.Mٵ|t(K$ӥ(tb=3fϙjL3t%Qs  C} I)@13t>!g@Ŝ|Ԭ3Oe+[!x :k83GP%{ :B4+Ao}`@WRf=x }+~@'ksKu܂wЧJK]9Ρ`{P;~ 0NTsoP:9!|#\ya*Ns sz@Cyxݴ/:+}A[3^ inX8A2o$3B(@H qR>+T -OGm({D&@4ih#0. @&:VklomZ--TG}OXo4F=Xgwٝ vgYA44V*tus Fcs~|}\\zV>@V>X[||SY\tat1j{Ml_ԗ-i (Yk4Jb@F2u}?U6j3*ӨM#?bܹCAh cc,yv\elLrB6ec!S ٘lc,x#At6y&+V}BgRS-WjűLE{^ZYRp]0d\ׇZ8֑"iNU5 תAj]S/;K,u٩9uצhpGC|pv 9O1HӘk-M*@`:2^Ut^ 9T]wT+éU,N̊)㨧.@p1t(MYX<&¡ 0E vrٲ * "*SJ\cZqZ%QmdP٫#Cgl4Dfq,OTT۳5ϩM38Z 10YIlB܀M~xcUxsxŎ'M~ G[p3QG_fu@Ba}@':ԧ=5rt<r7-&XF QbjXkةkQ5FM[{0؀dk>ÿi74#)8yTOT[HZ}伵i23 /]lfljǜ Nz+AvWw?OA;(``\ sC\c 2@< :~~$^7}z゚f${1w)Ow'@#PA`ʘ3X[6 ;A%dC`1i6I'R VO=IJ8qPO`J]j#Ȋ633رxsF9Tb; dS(wz+:,pE?h<-"awS>^ufMv)eG#l_# cYܐޏ3Ѐ k]Ov{ La9VfƍOz32**c2%sg(1lx$<{wzlΩ=U}eɖ߆8K$ j@-فC~iR{@) 7yά'K9c;5~ ls<=@ CDѓjN9>5xz w~H}i|Vg2'K=+zZE#>C1?#j_K31}}E½o'zEuxY=:j&N4 LV`h ?Va@ )6DG"}C&תW+ =_S uȯڮ}J&$~h}At縈G^KXUJtC?Eiz=g}ȼ5Y ^fOͻW2|h>lKQ㭽_?W>q*BD8=t`g;^˞_;?{}􍏿}=%yuw~?O|W?3-z"L絯;o׻?f6`Xf"(t_1xsq!>h;(}ҷQKw8?%gw>?OMD~:7֩7O~5Hs]Xh)bv/yqKꮠ 1zyT.}P^q68;}xJO'˫/Tg.Dx~W=O0BJ|:1: a;ʙ5ZiC^t$^,lIѸq^OiM}^a"lb3:T9Wb:8 J*n_@O.p6Ŋ؂0*36tfl]hy!b=q7oXTb;bw[6_1s-*IG-/ޟlUXS!>ఒYr\ yط0(AdmSy`w3ZOw1_m7Ыj7 =ɒ}י2KTvQv W˅_η@Lb\-r*C!ǡcX^KX\bďD.p6䳁pɎ"QlzNcɓSC>XW\r3<p&b9ѼRPqmnKJEq9bm@N3 \q㘞gQ+n{O|.wJꓺC[nA68B7z4WVB;C#ss$ ..Fdޕg<^`Աe?g,(ǵbNO3eP#o*.˄ϨaN|.U9LpG~SptoqBtaik_C2Y4t&8:_{EᓑJډkkU*u'&!6VvV)䎬m30I^Zw]Kz>A>ka ϸ&_o0&_)D /x8Au QBzuռ4i#o=VؚJ+<')͓z&gu"OBvݹ%E<=iUKa6yE^0+WW'Lkf>NwSg~O:dmyƼfX2BIz{9Pɺ,ތ[O# =jdK Q@~笳Øu۔]_TN%$nS~LlUvLv{NS۔&zM3 > Fm/#W G땸{EMqE pD[pDp,=+WEXmMZРjN&-)c#G 40ZФd8YQU>\#1,ыֹ~XĨ@j`{a_mCwvp }xyq$<i_ GÊa)`XJ)0,a0,% G+OB0"]&;( C{[ǗU۸O{tyҿt&m ŧ_<f*w`wm]Gͳ'5,ܹBv༇ Pw{ >"8cTXk[{N8ڤ= d0d=C >w CBR@sQ?s:yft8y~[j1îE1Vc<%cѷf"QrG'[7-ܪru= l$Rj>gvd9ރTp<^Rkhѓ(-IW[7`ORݢp ""GGd_Ti!uְ,/)ۚItleѬyT4^E_$Yl~$[$Mt,9/E[$-ړ-g̻z<_r2N-l9>ɗcKs.3_&/I˗/Ij<_2ww'0t#%Tg3_2ZΊ쑪'սp{pzydfTe):S3)9.3yTW|Oek{Zt]֋=YrNN&ጝp.!u޺l9sxwye^G o+sfc\,gT+Ej59^liT?{(Sd=Qx8ϔ\e˜8rNx= ׮Vv*&d;dwKĚeu9ˇ|w"Ʒ]`/h~> {]{)) ]ؾK podbfY2/?V⅊M%rǏ[c)kr/@vϡl3Y$_\"e?[|[g^o-OO!?Y|%IfWp,SW3Yq3YnN6(Ҟ6QX'7EE"@WEFfd?'##7 \z5]a"01}P|>!_$T>=ۛ4$[g+w7we9&ڻf=crb4m&Nφ'YG9`%!<-%ԒCh@ʴ9հ#O@k5xj]^ 9=_lb>:Sh~(2ЇhƻҮx<8)\Dͷ6?Wn~\r׹r{`~3?}r$9-+dՉmiu{8 fLO}n3Ry`>kE kGu D}[%q8el?c%θ:װ 8og[Kw9[7*Wgʨ> O_qX$v8l StzJ"ՑEC`>ǧS ,6ZԽ&)O-4ciztܧK'5&?ܸqC-N洺y#Q,y*G%ߐea-ZBJeIeV?O&O'٩,BeO;Vl)V{M .ɠ%jaX7Mw0|(ݴ/^0]G72t8;N2]H'3:)=jFKX־gWz-}nM¯:A~D;憅c!FL2# $pK/2Me"x!xHa D| OK31-O`VZ@*3P $łwt쁢l e,.aHeBUBu^T/U2 R*XaΪ`y~wL*.x. K{TGF0+;] 7VhBo¸, 9W\]TK7rk~0넶|..~8%C'ϏY 2D'q#%n7 < [~aiGabc~زUeĚEXԘx%[j~%:`˨8)*3Fb5joU^D{ɗA$_Gr.!zoq8p1b֕p=Rz8{%<DzNKq>=Z<KcXKgXE5Cn'.JәϗY8*zu #:2B5_sR12J֩<7T<^B3?oe`9D@Z:q%ܑ%[=ˢ K/|=y[팍4h>6*۵Ѷiu&pLrYTyfgKi^^GUm pZqTFh<6ZUt2:W{:!H+;35j/x#Yoj!0o6:vTp,.yACW^c jg4Y5U5JA+cY"!%Sf~e`ɿ77·ʽ;-|skgAf<@[0\C0&^µխ-$=|}k|7v>\SAr]Eh C5֐pYdwGH.Bqg}hh.1;imkBSx]N0Æ WM- 2*5X|DZ=fLc(F? hҶ BLrGpodgd/e>f 9]WA\\Lf[xilϣTs1H QlHاEt(SAZ_Y X mkBTWx흍8 FSHI!)$FRHnw HYx3ꇤsaaaaxIǏ'U{o_ھgW9 o'GW {>~Jlo߾)*/N\ϱov[iZ_ձaJΝ/:6O- 92b?Tlk%?_21B sY5>:>c=1Ow y^- ڶ,XzusM#גU]>H_yYv!ۉ_mi Rus]Xm_g)YY)m]y,m z1aaaxEߓGקo/Y\k6xjgH|yu.\aæM&wk#ϐ$?]Mo\Ⱦ,/ڥQ@~6s?)}, l gX #vQg Bٙ^uのuhm?}{].~}v_J;xogJY]޳@.)oqC?}>@Xߘ'-(W? źvƔOʙRv[K?[A}?-wmՑ}g\=c}M ggg DŽ-B^k_g?F? v0||؎=ǧHPgs/hؑI t~{n^}ZyD5XWvO)"c0vY Z|~_%/,p\ɹyΰZ/;/xs_9?Pܯ5ݻ\[y|č8gʱL{? 0 0 _k3>z_\S |<)b|7aaaxn.ta?l^Cvkؽ#~e)3<3^kdlc&jK+o"e<.ʞ`^(3zu l+6v<ï k7]/lc[`On}򚄫 G뎱zt^v2)?;Wmr5ocIz?Ozx{&!ez."ѯ 1Gg{+ҏlw<=}GݽFƨ^)zIpG K֜{{e G12ۭqiumf>.}~a? 0 0 [u+7Svq֭y΅ ?ނ}XwŶv?ߩDZۓ-q/?߳=<~#>Fk"qzrQo 9r,nY[;o:)@-`ק-7({߯S@µK9֠ɸ>:n3 _[_*mtcmC>qSL=<6;ǫsaaa{xˌ\ފpx?0׋#5zяc]x^l򼠕(f:~٣^lin59W~\;?vn6erUbS~v^U O7O(|;+SG4|?f*?rW~2oNٟS9~daևmH6mX[J~s.ym4ٶO|Bd/b5ɿyU? 0 0 0 0 0 0 0.P~*1@G\⟿KrKXs2(ߥ纎J8'>X@▼QQbqwx b)_K|v 1M6kee-2Ǜ59?K^E~9ϱQﱮYF8N?~;:=J<-tĒyNAgC \NXKs)'^Kg\~2}6}Գ)n]Or^j~"{p29w6/.z-v:+M{WJYZ굢`% Ҥl9ힶկ#OUz+U?;sd~vND7*.Y+v:ye;8}~|+ÑޅN9}{Bƞ#txխsXɿkSV/uJ=o G<ջL'L:D]6jfgLz/+ؽ[{rCMYq~[{yy czA;w9zszWHVax3 %mkBTx흍) q ĉ8D^>׻gI@XjjgiЃ`0 `0 ?ϟ|:seQ3|ӧO|:2|.};7eGFO6_Qv]T]^ˮg{>pjzkuo{yye?{-x/ D:3D&򈼹e^Hyi#/OGzϪ߯_~ :sMe#M3Y#=2 QЙ[\s=E8}E>GȩT ڲTg-}VfoSVwzV}./>~!?U1<#}=F[ ~QڋBN..+푹^edLo+[\-k dW(}6q$#?z6Bөi?L7!3O_Q}Пuo[=tkȋM!'}/Ƈdr2_Cﲨ: `0 :8o=+8-4}۞cĥXdq{bUq©ήm!ƶg*ΪU\z[GA=^+ru{LV U?)V>ғ)x|Yҁgi\yi^cUo*= !TY?rfgWsʽVn*VX#=Fϫ+[F~yH\L~[O҇h5ݵTow|Sfӟ+);F;:x )/OS yUo2e)Ve3'wgGg=J^`0  ľu kU,Ksؑ5nY,bXw{ w&3QהNQev ]ƷgcH˞i{A3I8hwduwUIWq8I>+@pQşGcZ\ƪUߝ]/:3d;ɫ:gB9R|GW~w2;fzt|+i5nΟgZY|<1NyŬ|E7k?z/k><=Α}N΅>uWydʬdz `0 *\?W8GY:Dgcg< 2+'W6qn؟{ru"wU쏘~c#T?+y{Q,,^qF/Xv8.֩g3}ȸOP ~n%hUG4(_sn|W}Tg&x^c,Fѭ+ <#+}/Uw8BRh_|33!mr\7U9m({ѝpvew[xG]߱?g;,nҽow8]וb?OV=Z_#ve?vN_WrYLo;1g9pV^G~>[_vNOS3 `0Q[ veO\k^8֔v<Zbz\Opbn$~}oz3ј mK vU]^iNWA#x딫jt q :E= z%օq)CcYEqyRG-+u (K\hP'*^ء^q=m=y|Kvūe\rȊ4={W1;=ݷxp;o@>ȘT\Ԏ+C=*ɫ|GJOCW]x1.ﵠ9_Eб Vq)v(ʑ}[GwǺ{-oSdו_˞׃2;iT&w*w:g׭SOsj%Z[~_˯d֮+w]7 `0]kIu+eL]ւoA^;=GR?v쯱;<y o$N1紈=:ߥPVu< <&3KyC/4r)i=*/|Ύ^]QNН1qGw>ù{ ?Kv:A}E:_n+{u=rq͓̳]>>d}+|L01`0 leg:׺񶊝`W,3O?]\9P~[kOWiGc~)-<w.3q}'vuw$Vnv(r52S;Wk_Kϔ8B/hEՠ'9w?K;x:x<|@cϽVyc@ۖSw8Bq]=2lBe6V}eR( VeZT4ade2ޒ+nYBTqSߔ<[&=f[|szP)G}{Zׅ3n7jpWwftEw[ǽ;`l? `0 `0 `{~i`oLy>uoi\qK|}7Svu9G쯿c¾#>,jow{ՆݲL=mW2u_8دjo?kD߱mw>#}E:OۡO;y`$jwymkBT6x횉m0]HI!)$FR?6c>>~sm+vuՑνYu8uN?WP>1JsWiV_uKEϸ/rˆ_gKW]ױEYcl,[TYHT}xL#}A GV7^}>iҞ-i;}LJX&TP3T#ߨgJl e'=?͘ona|7>?ǐU%;/mN/IfQփz{G}?v✽3X~j{zTAO^ʰ>?sy|G)PU{ ..T}6ڳ-F`p]k߅~b  О$wݓٱ|sCoA+q3lOx@(0a+? T,_7s\Ϙ^Bl1)C+k(FyN"8dPC_9>O0&l4Im+nwGrŰ)/tihf ѸX>E)<,6s45zb?J\<OM%O#(76:= ӋYAƒH Ls6MXBcX&ǘJte. 3.je(??Lj=%wZizFTx$kP8Em jAOހ>~؆B9 ֤8UKCvjbL Cy ;mj P. DkwUE€3ܨ8xUJs\ɟ+;}sFQ(KIXݛƨ 1 +KdX];Jģcx$D׷X`i @l̏rnm$^9΄zBGϞQ=nfkDe; <a>,⢞jk0B[p($Ǡp4 nq`XƓ vϵ.xHnorJ5Hu뇗 f a[Z:>36[g RL؍?( &w.7C#~B{] UW 71jk~ecGrD.=K@WDZM0倐0\xvqNZ ># BE )&yA}t?B Ym(WIpɱ |2+\2 )l8tl@Z.Be񅋍RSƃm>dIl'N adĢG3%#)?$s _5=YBR#-k"qGP-e"f%֩-ϓ378M9ϊ,_*n;HEBƱcl~ ˝[/sagIE2,z1t:kLș壋G){7ond{@rP>kwk׽ #kXfyEAB9uM4P=_lgW؇N#_nGpp ,ZUu6ȓVӰ0EK7*|]{75F\ԶzQz! uH>upT٣o3P)[^6` -d&*=%fY<^ط`_6|h3ء>2 Pq7ώ ,NsjF=B` 큳CiU)R鐏@LҮǧmb<2FHRqùFXi䎲OmGA}:*u f:@ʫRH.66jcGOpO- 6HKJU:Jǃv,3DZEƮqq7p?ȌK%ȧ$;?Qr6pP7`a^=R_)m>D3#£ _' Iɭu͋C-Rne㯄ssL<ȭ/R)|Lt_1Lk=rr 4/gEr~PnB[\g[{gYvRW' {Fem1{ wL;7&$xc0 n&u@5sCCձm8Heft x{q(aтa?Q%l4ςxmWI׆GC1kQ3iJh,KRO`ʲ4)%b6B8\pe;u)ko)#WSncRx{[sXv195_0Kՙ7>Tp5ٴl3S"؝LX睫[5m Q="u}pϘ*xbՉ#iM+@Z! Ϯ~jYݬ$?5mtu] %@݅:4h8ۃtu3; ΑO1A/r R*5i&j#Y2:$Z(ad@>'z L뇶6Z8|`6"X1_z' F-я?X^ A:?1;h/KVB' vOnFS ƤQ{=kh7MwXQp\v͓O/. N3HKRlK"q^Wh1wt h@3e6N|I;y?8t[[! $,ήLe"z%IކAkRl!3u8ځy?_W)AbCO!rza5Sn֗#<43y6"R߃CQ&>[# BHǽ{vekOTlq(UH͵h ݔ8,@tՂL{p/*L"d_y k,4 G̖bD>,.ok"D;|7[.DCA#ilϟI֬Dq]+eE _-- ڰc^Lq1~CCC9gNH8BkhJ#Z-`VoMa 9r$պZ-hkh ?C$ ^tď9d(8P݅]ڶw[wl;dn׆oKd Hބ(DInI M_(5)6H/Y1 QRk,nXHʉ?>df&6^EJmt{CCc`0ʅv5x<\9Yc}106"״!֏9dl:' 1H"z'7QqɌ#KR./CVgQȬ\ `?d1yuM6Ƶ8ZX]8^pwQE &1frRKi$GݜЕh3'{;;~FK37ku<pdʎ+C RMzƏ7)nҀ lEGyl:̑IoBS%|ЕsTulebA}Aʹ10A{KʘӺtjdLI=r PRg_LbR Şl?␔)![Fo wi&k^CV(t@pW2{hxHGRn͉eCbxԉ6GQd27\ثdS=\Ff*0ۣOP5(rZߙxQZ>~GAeN-jY7Ҿn;n?ӹ"Px}/NW:݊&׾:x" ꭥу;R펔 c䛅љElmG§a= h¨BG_uYnZ쫭FYs U"zM&:Gnu.DX5Xn;}ԫ%XO?~2&Frjj8 yA*W I9/ub)Zl: s 85J>~iI3Yԕ;:#hELם[ROd^GA˩f~Y!En0~/A Km>^WYq"<цF*c:xw|͞w%ehRgd9̕v3v Dgh>>?3hYDkgC(ʹƒԕSԜ| 2Q94(?OGQ34 fccPopTYaW(>@tX4`LGٞpɄaŰl\[9c26U M6f,'C4i?W~psϠ?kAKrŵk@I|>^xs?\`,D̒5W^w DMXf_8<%|8_왉pP1Wlm߃f?4:́_Ԕv M;k:p_sj؎qw]$F}y ,b'N=o0, ~M YR46+!}@~ujctCP.Y(x׎z?70WXFܣo3z0c8RGg0 TU򄽻w"/4֏CQ`[{Ocn]+{{ N!33+5]qpj' r9FDȬ)~: 9Gmx2-?sraG"yvUpa;Ră A\& ?#n 0eed~oq嶭!!DzP^H)>oȑ.ļԶ=Hy7S-M ?8ycߧq|#5"2Б lm#UeΤVbM͘jAc7Z ]> 4gb s 2WRsKg6 's8qzTT[R[w)I95xWj #!nN+zPڔ KgTE,?{^RDݥ=Ru^zîc&D'i74SJߔ&HUG[crͦ<׿~4}څh;lpAZ%XZ;tQ?yk1+Ƴu6[ Dc4Ɯ*dB#!}e>samhG3c^8u9󼵕⸈߂UyB;f "Yi=D =4&|C3g]~WgjhSIXU"1A5Fr4{AljwTt6</N \Rta| i>T.Wo>>xϯY{緷m,J{gg}v~)]s!?wXGFl!7U|Cnfﳅ:.@mq%臔Ru?.:aBֺE#Gg'yXDuSWNJD)21ѵVagWPqȒ s?¶@g")s\T{f3go^w:^"{d#!φt},nyWFKv„X4|VB~,˘_&fjp/WԍwaO H 3I`u1ͤ+_W|+_W|+_W|+_W|+_W|+_W|+_W|+_W|+_W|+_W|+_W|+_W|+_W|+_W|+_W|+_Wݚw)2iTXtXML:com.adobe.xmp Adobe Fireworks CS3 2007-10-06T21:59:47Z 2007-10-07T00:32:43Z image/png XIDATxo}<%RR,˲:"rT#@ZISN!0z@ ZI*=4&;N8-aQݙyz%M_H[0 w3Zgfn9g>ى/ ;%b$w .Ƙy~??׿|%[`d[V* |<ϳ,....?~_}SROR*)S?bsi>B A((Iyi^:=s~wAҺ6cˑƆX5bĘŨ%$,,˺W^}'|W_]ԖԑU9X8\esITeYTMZ1@̲,}WԩS%iEҪ`bvzOzLPi-jo^F(,몵 괖 Br?IԐTsv\U b괗kR}|FxGvJ*iYҚc/I\k cݺ4]/cǎ=ZA"TTDIQ]1j.D!o|㋒fTbBRME,*|*hf޺||ks=㏪8f1"um:vHrFU-t:)$ tw >{FӡS;nkiڔ$=ztNE$+*b1Y$.\8b!IꮯMH;^/^jwg$(Tv>֜B][miƊ& MLLhvnNz jl6'nܸPq̢q,i6ki/|銲}zv~~PU1JsW~(ΟO?YwըszXS!o>ڵ ysyVG}D9(φxιO W[_y=j5=vz`b$v[޹-ZSfI>|" c1;lՅw.PT3N{I(ꥶ_qdolT$U(Vg/^SQөGkn{vzM+T%!qVUT$P2v;^KBбOh `,/]Vk)$)<,*Jrt1UHnȔއ_,ht'&nk]w$BJ-WyT*0ȓ}7rmmM!:<7^k.m)`4,$)0:,;oH{ ] mNzo6C>Ƹ`$F).vƸ/V}|_䣱W~{noJ֛U5F,}ݺ!Igi޶(7鮫RI4=ݔsLNaGohb$Acc<vWl6UU7`~_P aWfSNnٯrpgMN+\i|2;;y$Ewo8S㪌U$5Ə11 o }+΀tkuwOzڕr>-\"9ij~hJ{/lR6tⓧthD1)ԻCv)ԜZZ&ɩVj^VkuI.vZm9yHSs:|rngJ{; ic!I㍪BUՒ|<|rreL,s{ g(ϕTySohnU+UQtS yy%[sK)\1%lzg8%IKeiq`TNur*$Nqo1U!6p)oI=~z`B, & `lHTup{^eij4]vاfN_nbX0!LbX0!LbX0!LO V4!w&Lbӊ s: `B, & `B,lm~X0!LbX0!LbX0!LbX0!LbX0!LbX0!LbX0!LbX0!LbX0!LbX0!LbX0!LbX0!LbK%a`cdX0^# & `B, & `B, & `B, & `B, & `B, & `B, & `B, & `B, & `B, & `B, & `B, & `B, & `B, & `B, & `B, & `B, & `B, & `B, & `B, & `B, & `B, & `B, & `B, & `B, & `B, & `B, & `B, & `B, & `B,Hbˇ ^F<ˢT"jE7wݛBN'K%k 2oZqdy/#d3PlOpEwa,\H䣏}yk|,x% #!_YYe9p.CESQ1xܹk"Uq/z INIRۋ .T~Pv~{{KEz"y~A*IM̞@TϽK~ӟ^WR G?O?2MbRs5f4>1'soQ~@娢=+*.-<9E9s?/\YsNƴxsNIRc:8'tЧTMʹ"n7˳g^0-Ik*l\H?uԑ澘$IEt+\QQq&$_|?~|k_;,iI/WϭIJc ꒚_җN<z49H'# ࣧ,IVwsg}Ch↤k.K iUR'ƘIR btѐ4%io~_>$ Z91Ƙyxٳg3ϼvڵ]UI*pYňb\[R7kSI $KtHҬC'O<O0|ްh+=bb lll>h //0 !@13m)eh ̊ ׀tw~?yP XX ؁)P#/P\ L:& 8#@_vvNϟ1rq10h$334$ӗ/ l@?_@,~8WT&# JkA}(ÇׯYG LFnn_@3`6H $4/AW1 Xǵ{ @li uNl +P13PXmk +d?/~>0= ^;.|Eh; !3>۷X7@ Q#K\PЈ$ׯ>|@FVF@CANRIENDB`././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/examples/demo/Applications/index.rst0000644000175100001730000000024700000000000023221 0ustar00runnerdocker00000000000000These examples demonstrate two simple applications built using TraitsUI - a Python source code browser, - a length measurement converter between various unit systems.././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1684768028.0398073 traitsui-8.0.0/examples/demo/Applications/tests/0000755000175100001730000000000000000000000022517 5ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/examples/demo/Applications/tests/test_converter.py0000644000175100001730000000344300000000000026143 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ This example demonstrates how to test a simple application using UI Tester. The GUI being tested is written in the demo under the same name (minus the preceding 'test') in the outer directory. """ import os import runpy import unittest from traitsui.testing.api import ( DisplayedText, KeyClick, KeySequence, MouseClick, UITester, ) #: Filename of the demo script FILENAME = "converter.py" #: Path of the demo script DEMO_PATH = os.path.join(os.path.dirname(__file__), "..", FILENAME) class TestConverter(unittest.TestCase): def test_converter(self): demo = runpy.run_path(DEMO_PATH)["popup"] tester = UITester() with tester.create_ui(demo) as ui: input_amount = tester.find_by_name(ui, "input_amount") output_amount = tester.find_by_name(ui, "output_amount") for _ in range(4): input_amount.perform(KeyClick("Backspace")) input_amount.perform(KeySequence("14.0")) self.assertEqual( output_amount.inspect(DisplayedText())[:4], "1.16", ) tester.find_by_id(ui, "Undo").perform(MouseClick()) self.assertEqual( output_amount.inspect(DisplayedText()), "1.0", ) # Run the test(s) unittest.TextTestRunner().run( unittest.TestLoader().loadTestsFromTestCase(TestConverter) ) ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1684768028.0398073 traitsui-8.0.0/examples/demo/Dynamic_Forms/0000755000175100001730000000000000000000000021461 5ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/examples/demo/Dynamic_Forms/dynamic_form_using_instances.py0000644000175100001730000001103400000000000027755 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Dynamic restructuring a user interface using Instance editor and Handler How to dynamically change the *structure* of a user interface (not merely which components are visible), depending on the value of another trait. This code sample shows a simple implementation of the dynamic restructuring of a View on the basis of some trait attribute's assigned value. The demo class "Person" has attributes that apply to all instances ('first_name', 'last_name', 'age') and a single attribute 'misc' referring to another object whose traits are specific to age group (AdultSpec for adults 18 and over, ChildSpec for children under 18). The 'misc' attribute is re-assigned to a new instance of the appropriate type when a change to 'age' crosses the range boundary. The multi-attribute instance assigned to 'misc' is edited by means of a single InstanceEditor, which is displayed in the 'custom' style so that the dynamic portion of the interface is displayed in a panel rather than a separate window. It is necessary to use a Handler because otherwise when the instance is updated dynamically, Traits UI will not detect the change and the UI will not be reconfigured. This is due to architectural limitations of the current version of Traits UI. Compare this to the simpler, but less powerful demo of *enabled_when*. """ from traits.api import HasTraits, Str, Range, Enum, Bool, Instance from traitsui.api import Item, Group, View, Handler, Label class Spec(HasTraits): """An empty class from which all age-specific trait list classes are derived. """ pass class ChildSpec(Spec): """Trait list for children (assigned to 'misc' for a Person when age < 18).""" legal_guardian = Str() school = Str() grade = Range(1, 12) traits_view = View('legal_guardian', 'school', 'grade') class AdultSpec(Spec): """Trait list for adults (assigned to 'misc' for a Person when age >= 18).""" marital_status = Enum('single', 'married', 'divorced', 'widowed') registered_voter = Bool() military_service = Bool() traits_view = View( 'marital_status', 'registered_voter', 'military_service' ) class PersonHandler(Handler): """Handler class to perform restructuring action when conditions are met. The restructuring consists of replacing a ChildSpec instance by an AdultSpec instance, or vice-versa. We would not need a handler to listen for a change in age, but we do need a Handler so that Traits UI will respond immediately to changes in the viewed Person, which require immediate restructuring of the UI. """ def object_age_changed(self, info): if (info.object.age >= 18) and ( not isinstance(info.object.misc, AdultSpec) ): info.object.misc = AdultSpec() elif (info.object.age < 18) and ( not isinstance(info.object.misc, ChildSpec) ): info.object.misc = ChildSpec() class Person(HasTraits): """Demo class for demonstrating dynamic interface restructuring.""" first_name = Str() last_name = Str() age = Range(0, 120) misc = Instance(Spec) # Interface for attributes that are always visible in interface: gen_group = Group( Item(name='first_name'), Item(name='last_name'), Item(name='age'), label='General Info', show_border=True, ) # Interface for attributes that depend on the value of 'age': spec_group = Group( Group(Item(name='misc', style='custom'), show_labels=False), label='Additional Info', show_border=True, ) # A simple View is enough as long as the right handler is specified: view = View( Group( gen_group, '10', Label("Using Instances and a Handler:"), '10', spec_group, ), title='Personal Information', buttons=['OK'], resizable=True, width=300, handler=PersonHandler(), ) # Create the demo: demo = Person( first_name="Samuel", last_name="Johnson", age=18, misc=AdultSpec() ) # Run the demo (if invoked from the command line): if __name__ == '__main__': demo.configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/examples/demo/Dynamic_Forms/dynamic_range_editor.py0000644000175100001730000001065500000000000026210 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Dynamic Range editor Demonstrates how to dynamically modify the low and high limits of a Range Trait. In the simple *Range Editor* example, we saw how to define a Range Trait, whose values were restricted to a fixed range. Here, we show how the limits of the range can be changed dynamically using the editor's *low_name* and *high_name* attributes. In this example, these range limits are set with sliders. In practice, the limits would often be calculated from other user input or model data. The demo is divided into three tabs: * A dynamic range using a simple slider. * A dynamic range using a large-range slider. * A dynamic range using a spinner. In each section of the demo, the top-most 'value' trait can have its range end points changed dynamically by modifying the 'low' and 'high' sliders below it. The large-range slider includes low-end and high-end arrows, which are used to step the visible range through the full range, when the latter is too large to be entirely visible. This demo also illustrates how the value, label formatting and label widths can also be specified if desired. """ # Imports: from traits.api import HasPrivateTraits, Float, Range, Int from traitsui.api import View, Group, Item, Label, RangeEditor class DynamicRangeEditor(HasPrivateTraits): """Defines an editor for dynamic ranges (i.e. ranges whose bounds can be changed at run time). """ # The value with the dynamic range: value = Float() # This determines the low end of the range: low = Range(0.0, 10.0, 0.0) # This determines the high end of the range: high = Range(20.0, 100.0, 20.0) # An integer value: int_value = Int() # This determines the low end of the integer range: int_low = Range(0, 10, 0) # This determines the high end of the range: int_high = Range(20, 100, 20) # Traits view definitions: traits_view = View( # Dynamic simple slider demo: Group( Item( 'value', editor=RangeEditor( low_name='low', high_name='high', format_str='%.1f', label_width=28, mode='auto', ), ), '_', Item('low'), Item('high'), '_', Label( 'Move the Low and High sliders to change the range of ' 'Value.' ), label='Simple Slider', ), # Dynamic large range slider demo: Group( Item( 'value', editor=RangeEditor( low_name='low', high_name='high', format_str='%.1f', label_width=28, mode='xslider', ), ), '_', Item('low'), Item('high'), '_', Label( 'Move the Low and High sliders to change the range of ' 'Value.' ), label='Large Range Slider', ), # Dynamic spinner demo: Group( Item( 'int_value', editor=RangeEditor( low=0, high=20, low_name='int_low', high_name='int_high', format_str='%d', is_float=False, label_width=28, mode='spinner', ), ), '_', Item('int_low'), Item('int_high'), '_', Label( 'Move the Low and High sliders to change the range of ' 'Value.' ), label='Spinner', ), title='Dynamic Range Editor Demonstration', buttons=['OK'], resizable=True, ) # Create the demo: demo = DynamicRangeEditor() # Run the demo (if invoked from the command line): if __name__ == '__main__': demo.configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/examples/demo/Dynamic_Forms/dynamic_selector.py0000644000175100001730000000673100000000000025366 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Dynamic changing a selection list, using Handler One way to dynamically change the list of values shown by an EnumEditor. This example demonstrates several useful Traits UI concepts. It dynamically changes the values which an EnumEditor presents to the user for selection. It does this with a custom *Handler* which is assigned to the view, listens for changes in a viewed trait, and changes the selection list accordingly. Various implementations of dynamic data retrieval are possible. This example shows how a Handler can interact with the traits in a view, separating model logic from the view implementation. Demo class *Address* has a simple set of attributes: *street_address*, *state* and *city*. The values of *state* and *city* are to be chosen from enumerated lists; however, the user does not want to see every city in the USA, but only those for the chosen state. Note that *city* is simply defined as a trait of type Str. By default, a Str would be displayed using a simple TextEditor, but in this view we explicitly specify that *city* should be displayed with an EnumEditor. The values that appear in the GUI's enumerated list are determined by the *cities* attribute of the view's handler, as specified in the EnumEditor's *name* parameter. """ from traits.api import HasTraits, Str, Enum, List from traitsui.api import View, Item, Handler, EnumEditor # Dictionary of defined states and cities. cities = { 'GA': ['Athens', 'Atlanta', 'Macon', 'Marietta', 'Savannah'], 'TX': ['Austin', 'Amarillo', 'Dallas', 'Houston', 'San Antonio', 'Waco'], 'OR': ['Albany', 'Eugene', 'Portland'], } class AddressHandler(Handler): """ Handler class to redefine the possible values of 'city' based on 'state'. This handler will be assigned to a view of an Address, and can listen and respond to changes in the viewed Address. """ # Current list of available cities: cities = List(Str) def object_state_changed(self, info): """ This method listens for a change in the *state* attribute of the object (Address) being viewed. When this listener method is called, *info.object* is a reference to the viewed object (Address). """ # Change the list of available cities self.cities = cities[info.object.state] # As default value, use the first city in the list: info.object.city = self.cities[0] class Address(HasTraits): """Demo class for demonstrating dynamic redefinition of valid trait values.""" street_address = Str() state = Enum(list(cities.keys())[0], list(cities.keys())) city = Str() view = View( Item(name='street_address'), Item(name='state'), Item( name='city', editor=EnumEditor(name='handler.cities'), ), title='Address Information', buttons=['OK'], resizable=True, handler=AddressHandler, ) # Create the demo: demo = Address(street_address="4743 Dudley Lane") # Run the demo (if invoked from the command line): if __name__ == '__main__': demo.configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/examples/demo/Dynamic_Forms/enabled_when.py0000644000175100001730000000636500000000000024460 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Dynamic enabling parts of a user interface using 'enabled_when' How to dynamically enable or disable components of a Traits UI view, depending on the value of another trait. The demo class "Person" has a set of attributes that apply to all instances ('first_name', 'last_name', 'age'), a set of attributes that apply only to children (Persons whose age is under 18), and a set of attributes that apply only to adults. As a Person's age changes, only the age-appropriate attributes will be enabled (available for editing). **Detail:** The optional *enabled_when* attribute of an Item or Group is a string containing a boolean expression (logical condition) indicating when this Item or Group will be enabled. The boolean expression is evaluated for the object being viewed, so that in this example, 'age' refers to the 'age' attribute of the Person being viewed. Compare this to very similar demo of *visible_when*. """ from traits.api import HasTraits, Str, Range, Bool, Enum from traitsui.api import Item, Group, View, Label class Person(HasTraits): """Example of enabling/disabling components of a user interface.""" # General traits: first_name = Str() last_name = Str() age = Range(0, 120) # Traits for children only: legal_guardian = Str() school = Str() grade = Range(1, 12) # Traits for adults only: marital_status = Enum('single', 'married', 'divorced', 'widowed') registered_voter = Bool(False) military_service = Bool(False) # Interface for attributes that are always visible in interface: gen_group = Group( Item(name='first_name'), Item(name='last_name'), Item(name='age'), label='General Info', show_border=True, ) # Interface for attributes of Persons under 18: child_group = Group( Item(name='legal_guardian'), Item(name='school'), Item(name='grade'), label='Additional Info for minors', show_border=True, enabled_when='age < 18', ) # Interface for attributes of Persons 18 and over: adult_group = Group( Item(name='marital_status'), Item(name='registered_voter'), Item(name='military_service'), '10', label='Additional Info for adults', show_border=True, enabled_when='age >= 18', ) # A simple View is sufficient, since the Group definitions do all the work: view = View( Group( gen_group, '10', Label("Using 'enabled_when':"), '10', child_group, adult_group, ), title='Personal Information', resizable=True, buttons=['OK'], ) # Create the demo: demo = Person(first_name="Samuel", last_name="Johnson", age=16) # Run the demo (if invoked from the command line): if __name__ == '__main__': demo.configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/examples/demo/Dynamic_Forms/index.rst0000644000175100001730000000007000000000000023317 0ustar00runnerdocker00000000000000Implementations of dynamic form behavior using TraitsUI ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1684768028.0398073 traitsui-8.0.0/examples/demo/Dynamic_Forms/tests/0000755000175100001730000000000000000000000022623 5ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/examples/demo/Dynamic_Forms/tests/test_visible_when.py0000644000175100001730000000400400000000000026710 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ This example demonstrates how to test the use of visible_when to make UI components visible or not The GUI being tested is written in the demo under the same name (minus the preceding 'test') in the outer directory. """ import os import runpy import unittest from traitsui.testing.api import IsVisible, UITester #: Filename of the demo script FILENAME = "visible_when.py" #: Path of the demo script DEMO_PATH = os.path.join(os.path.dirname(__file__), "..", FILENAME) class TestVisibleWhenDemo(unittest.TestCase): def test_visible_when_demo(self): demo = runpy.run_path(DEMO_PATH)["demo"] tester = UITester() with tester.create_ui(demo) as ui: # person should be 16 by default self.assertLess(demo.age, 18) marital_status_field = tester.find_by_name(ui, 'marital_status') legal_guardian_field = tester.find_by_name(ui, 'legal_guardian') self.assertFalse(marital_status_field.inspect(IsVisible())) self.assertTrue(legal_guardian_field.inspect(IsVisible())) # UITester is yet to support SimpleSpinEditor so we change the # value of the trait directly demo.age = 20 registered_voter_field = tester.find_by_name( ui, 'registered_voter' ) school_field = tester.find_by_name(ui, 'school') self.assertTrue(registered_voter_field.inspect(IsVisible())) self.assertFalse(school_field.inspect(IsVisible())) # Run the test(s) unittest.TextTestRunner().run( unittest.TestLoader().loadTestsFromTestCase(TestVisibleWhenDemo) ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/examples/demo/Dynamic_Forms/visible_when.py0000644000175100001730000000655700000000000024526 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Dynamic restructuring a user interface using 'visible_when' How to dynamically change which components of a Traits UI view are visible, depending on the value of another trait. The demo class "Person" has a set of attributes that apply to all instances ('first_name', 'last_name', 'age'), a set of attributes that apply only to children (Persons whose age is under 18), and a set of attributes that apply only to adults. As a Person's age changes, only the age-appropriate attributes will be visible. **Detail:** The optional *visible_when* attribute of an Item or Group is a string containing a boolean expression (logical condition) indicating when this Item or Group will be visible. The boolean expression is evaluated for the object being viewed, so that in this example, 'age' refers to the 'age' attribute of the Person being viewed. Compare this to very similar demo of *enabled_when*, and the visually similar, but structurally very different demo of *dynamic restructuring of a user interface using an Instance editor and a Handler*. """ from traits.api import HasTraits, Str, Range, Bool, Enum from traitsui.api import Item, Group, View, Label class Person(HasTraits): """Example of restructuring a user interface by controlling visibility.""" # General traits: first_name = Str() last_name = Str() age = Range(0, 120) # Traits for children only: legal_guardian = Str() school = Str() grade = Range(1, 12) # Traits for adults only: marital_status = Enum('single', 'married', 'divorced', 'widowed') registered_voter = Bool(False) military_service = Bool(False) # Interface for attributes that are always visible in interface: gen_group = Group( Item(name='first_name'), Item(name='last_name'), Item(name='age'), label='General Info', show_border=True, ) # Interface for attributes of Persons under 18: child_group = Group( Item(name='legal_guardian'), Item(name='school'), Item(name='grade'), label='Additional Info for minors', show_border=True, visible_when='age < 18', ) # Interface for attributes of Persons 18 and over: adult_group = Group( Item(name='marital_status'), Item(name='registered_voter'), Item(name='military_service'), label='Additional Info for adults', show_border=True, visible_when='age >= 18', ) # A simple View is sufficient, since the Group definitions do all the work: view = View( Group( gen_group, '10', Label("Using 'visible_when':"), '10', child_group, adult_group, ), title='Personal Information', resizable=True, buttons=['OK'], ) # Create the demo: demo = Person(first_name="Samuel", last_name="Johnson", age=16) # Run the demo (if invoked from the command line): if __name__ == '__main__': demo.configure_traits() ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1684768028.0438073 traitsui-8.0.0/examples/demo/Extras/0000755000175100001730000000000000000000000020175 5ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/examples/demo/Extras/Image_editor_demo.py0000644000175100001730000000536500000000000024154 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ A simple demonstration of how to use the ImageEditor to add a graphic element to a Traits UI View. This example needs NumPy to show an example of a dynamically created image. """ # Imports: from os.path import join, dirname import numpy as np from pyface.api import ArrayImage, Image, ImageResource from traits.api import HasTraits, Str from traitsui.api import View, VGroup, Item, ImageEditor # Constants: # The images folder is in the same folder as this file: search_path = [dirname(__file__)] # Define the demo class: class Employee(HasTraits): # Define the traits: name = Str() dept = Str() email = Str() picture = Image() gradient = Image() # Define the view: view = View( VGroup( VGroup( Item( 'name', show_label=False, editor=ImageEditor( image=ImageResource('info', search_path=search_path) ), ) ), VGroup( Item('name'), Item('dept'), Item('email'), Item( 'picture', editor=ImageEditor( scale=True, preserve_aspect_ratio=True, allow_upscaling=True, ), springy=True, ), Item( 'gradient', editor=ImageEditor( scale=True, preserve_aspect_ratio=True, allow_upscaling=True, ), springy=True, ), ), ), resizable=True, ) # generate a 2D NumPy array of RGB values gradient = np.empty(shape=(256, 256, 3), dtype='uint8') gradient[:, :, 0] = np.arange(256).reshape(256, 1) gradient[:, :, 1] = np.arange(256).reshape(1, 256) gradient[:, :, 2] = np.arange(255, -1, -1).reshape(1, 256) # Create the demo: popup = Employee( name='William Murchison', dept='Receiving', email='wmurchison@acme.com', picture=ImageResource('python-logo', search_path=search_path), gradient=ArrayImage(data=gradient), ) # Run the demo (if invoked form the command line): if __name__ == '__main__': popup.configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/examples/demo/Extras/LED_display.py0000644000175100001730000000670300000000000022706 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ This demo illustrates use of the LEDEditor for displaying numeric values using a simulated LED display control. """ from threading import Thread from time import sleep from traits.api import HasTraits, Instance, Int, Bool, Float from traitsui.api import View, Item, HGroup, Handler, UIInfo, spring from traits.etsconfig.api import ETSConfig if ETSConfig.toolkit == 'wx': from traitsui.wx.extra.led_editor import LEDEditor else: from traitsui.qt.extra.led_editor import LEDEditor # Handler class for the LEDDemo class view: class LEDDemoHandler(Handler): # The UIInfo object associated with the UI: info = Instance(UIInfo) # Is the demo currently running: running = Bool(True) # Is the thread still alive? alive = Bool(True) def init(self, info): self.info = info Thread(target=self._update_counter).start() return True def closed(self, info, is_ok): self.running = False while self.alive: sleep(0.05) def _update_counter(self): while self.running: self.info.object.counter1 += 1 self.info.object.counter2 += 0.001 sleep(0.01) self.alive = False # The main demo class: class LEDDemo(HasTraits): # A counter to display: counter1 = Int() # A floating point value to display: counter2 = Float() # The traits view: view = View( Item( 'counter1', label='Left aligned', editor=LEDEditor(alignment='left'), ), Item( 'counter1', label='Center aligned', editor=LEDEditor(alignment='center'), ), Item( 'counter1', label='Right aligned', editor=LEDEditor(), # default = 'right' aligned ), Item( 'counter2', label='Float value', editor=LEDEditor(format_str='%.3f'), ), '_', HGroup( Item( 'counter1', label='Left', height=-40, width=120, editor=LEDEditor(alignment='left'), ), spring, Item( 'counter1', label='Center', height=-40, width=120, editor=LEDEditor(alignment='center'), ), spring, Item( 'counter1', label='Right', height=-40, width=120, editor=LEDEditor(), # default = 'right' aligned ), spring, Item( 'counter2', label='Float', height=-40, width=120, editor=LEDEditor(format_str='%.3f'), ), ), title='LED Editor Demo', buttons=['OK'], handler=LEDDemoHandler, ) # Create the demo: demo = LEDDemo() # Run the demo (if invoked from the command line): if __name__ == '__main__': demo.configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/examples/demo/Extras/Tree_editor_with_TreeNodeRenderer.py0000644000175100001730000001246200000000000027330 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ This demo illustrates use of TreeNodeRenderers for displaying more complex contents inside the cells of a TreeEditor. To run this demonstration successfully, you must have **NumPy** (``numpy``) installed. """ from random import choice, uniform import colorsys import numpy as np from pyface.qt import QtCore, QtGui, qt_api from traits.api import Array, Float, HasTraits, Instance, Int, List, Str from traitsui.api import TreeEditor, TreeNode, UItem, View, RGBColor from traitsui.tree_node_renderer import AbstractTreeNodeRenderer from traitsui.qt.tree_editor import WordWrapRenderer class MyDataElement(HasTraits): """A node in a tree of data.""" #: Some text to display. text = Str() #: A random walk to plot. data = Array #: A color to show as an icon. color = RGBColor def _data_default(self): return np.random.standard_normal((1000,)).cumsum() def _color_default(self): return colorsys.hsv_to_rgb(uniform(0.0, 1.0), 1.0, 1.0) class MyData(HasTraits): """The root node for a tree of data.""" #: The name of the root node. name = Str('Rooty McRootface') #: The elements contained in the root node. elements = List(Instance(MyDataElement)) def _elements_default(self): DATA_ELEMENTS = ( 'I live on\nmultiple\nlines!', 'Foo\nBar', 'Baz', 'Qux', 'z ' * 20, __doc__, ) return [MyDataElement(text=choice(DATA_ELEMENTS)) for _ in range(5)] class SparklineRenderer(AbstractTreeNodeRenderer): """Renderer that draws sparklines into a cell.""" #: This renderer handles all rendering. handles_all = True #: This renderer handles text rendering (there is none). handles_text = True #: The scale for y-values. y_scale = Float(1.0) #: The extra border applied by Qt internally # XXX get this dynamically from Qt? How? extra_space = Int(8) def paint(self, editor, node, column, object, paint_context): painter, option, index = paint_context data = self.get_data(object) xs = ( np.linspace(0, option.rect.width(), len(data)) + option.rect.left() ) ys = (data.max() - data) / self.y_scale + option.rect.top() height = option.rect.height() plot_height = ys.ptp() extra = height - plot_height if bool(option.displayAlignment & QtCore.Qt.AlignmentFlag.AlignVCenter): ys += extra / 2 elif bool(option.displayAlignment & QtCore.Qt.Bottom): ys += extra if bool(option.state & QtGui.QStyle.StateFlag.State_Selected): painter.fillRect(option.rect, option.palette.highlight()) points = [QtCore.QPointF(x, y) for x, y in zip(xs, ys)] old_pen = painter.pen() if bool(option.state & QtGui.QStyle.StateFlag.State_Selected): painter.setPen(QtGui.QPen(option.palette.highlightedText(), 0)) try: if qt_api.startswith('pyside'): painter.drawPolyline(points) else: painter.drawPolyline(*points) finally: painter.setPen(old_pen) return None def get_data(self, object): return object.data def size(self, editor, node, column, object, size_context): data = self.get_data(object) return (100, data.ptp() / self.y_scale + self.extra_space) class SparklineTreeNode(TreeNode): """A TreeNode that renders sparklines in column index 1""" #: static instance of SparklineRenderer #: (it has no state, so this is fine) sparkline_renderer = SparklineRenderer() #: static instance of WordWrapRenderer #: (it has no state, so this is fine) word_wrap_renderer = WordWrapRenderer() def get_renderer(self, object, column=0): if column == 1: return self.sparkline_renderer else: return self.word_wrap_renderer def get_icon(self, object, is_expanded): return object.color class SparklineTreeView(HasTraits): """Class that views the data with sparklines.""" #: The root of the tree. root = Instance(MyData, args=()) traits_view = View( UItem( 'root', editor=TreeEditor( nodes=[ TreeNode( node_for=[MyData], children='elements', label='name', ), SparklineTreeNode( node_for=[MyDataElement], auto_open=True, label='text', ), ], column_headers=["The Tree View", "The Sparklines"], hide_root=False, editable=False, ), ), resizable=True, width=400, height=300, ) if __name__ == '__main__': SparklineTreeView().configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/examples/demo/Extras/animated_GIF.py0000644000175100001730000000344500000000000023024 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ This demo shows you how to use animated GIF files in a traits user interface. Note that this demo only works with wx backend. """ from os.path import join, dirname, abspath from traits.api import HasTraits, File, Bool from traitsui.api import View, VGroup, HGroup, Item, EnumEditor from traitsui.wx.animated_gif_editor import AnimatedGIFEditor # Some sample animated GIF files: base_path = join(dirname(__file__), 'images') files = [ abspath(join(base_path, 'logo_64x64.gif')), abspath(join(base_path, 'logo_48x48.gif')), abspath(join(base_path, 'logo_32x32.gif')), ] class AnimatedGIFDemo(HasTraits): # The animated GIF file to display: gif_file = File(files[0]) # Is the animation playing or not? playing = Bool(True) # The traits view: view = View( VGroup( HGroup( Item( 'gif_file', editor=AnimatedGIFEditor(playing='playing'), show_label=False, ), Item('playing'), ), '_', Item( 'gif_file', label='GIF File', editor=EnumEditor(values=files) ), ), title='Animated GIF Demo', buttons=['OK'], ) # Create the demo: demo = AnimatedGIFDemo() # Run the demo (if invoked from the command line): if __name__ == '__main__': demo.configure_traits() ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1684768028.0438073 traitsui-8.0.0/examples/demo/Extras/images/0000755000175100001730000000000000000000000021442 5ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/examples/demo/Extras/images/info.png0000644000175100001730000002104700000000000023107 0ustar00runnerdocker00000000000000PNG  IHDRuuxF2zTXtDescriptionx /MHMQHTKWp/J,L.VIM k IDATxyTՙ?B44l FF 3L4&&1l31ƘQ!qhb&FTDQhLA6WYꪺۙ? *b穧o{sG|26PGZ&mOQ1E_JM] Д)ImU'S} ~k9:B Wu(IYuH/=^non|/םM*sؾ/H`k/pϭ[dmXM3NwX ]9V^Q5(p}]M *AUUp=?(cgx>7MS!N<^&-}R>bF `'sS?LW/M#Z#ϻ5/ĸY8+%.F&m=m8iZٰM93ntaQ;חbGq,(}34U}N{3lȸIY.ez 2&=G.:$θ{f@aRJM,xaU?j-. yOp r:SNKiuKPy>k7QX4m" |۳-BU\קh7w9!h{VA;9lTLm)%m=ZLUQ,Pt3_5S!py )TB|s#eihn;4Ҩ읦$c1Z:e0dp9&;s(u/I`+HZZȄ#5nt^y ,!eD̯&48dJLNfaԄ;)џ\ilWhLf̘_ amXe9^~5@'_r/T9\eh|AZĶB1oP-G}]-TTd+9).ʌsQc/d2iWrՏCļw B 3oտ [+_|-;z㧑4Δs YUtbSٱ'{muVvnDޕwElz\ܮm;/bm:ҙ8=}| :}#@S'㘁eGd44Y~/Nꨪ|eWM3%iR+䜋d~Ipg'͞}zSc@J3&v5a[NQ6ؽ7ejzض/%/i45dR`5i$4,SOEtRR,z@I0)/³K]59I2 _Ġїy0tUr}ޮΒ/z8K.BH 4|JF3׮`ͪ%\q,_uOΪ$2L\tYKמUmJs _ 5{^ڼ49ιjy/}qr^J\S =/u[m(Z#a#L'sni6+Oc;^wp /랦zv^{^*oPغy'qEof@ 22qQU4MzJ&tqm]שŧ~57=݆úg푒z/n{.ǍߞBK/CWcI*el֕!rL%t*PUB)RI(})lsϓ(t] xE@`{ -̟u=R1I'XU$|1tcϾe5(OЯW MShɻxl5g&,ϗ #]STIG@Lmۣ`*Z(\sa{D6a?κ|_"{QU,C=/~%"NHP@q||?XL93' GT4(]w;8<$XͿ+AJ߸xyUAQE@6?Ӎn COUUB=H@6,эw4MSHA:e`]SdŚ׍)$L(Ry;E b92OVۗH)Cٮ. )F&mУ̢$cbG7IN|zg56H&t4557b%p*LĞ܏^[@  |)C®m+ykٝ1ޢ6e'"W,1 -@m Mx-AF2cBH0mE&HǝEI naIyw=سv6Bm a(55O >*_W4U!{,=L`ߦR></b؞#&PU+x^0Z }dMS?GŠ1g4!:Kh-ڵf,TE'PZu&7Dggše/i c KUDOHWK'u% hTEAS4-hw/%LS?{+RJt]c϶ G.eob oTJ\'Ndδkiq\5BA/aδkIfzWA6rgoCJɰO|9ӮR{գ - ys]A6 9>Uy49nV\L6gcdmZrLl,~|v<"ohV*aiN +A[q\|xTN9kȅ =n<<ry 6h2-.[#@ӂ:َ ]+:|y뗣**t2W*Iuf/^/_˹݁*󝟿=6,y8G﹆Wz o@yIrw غyC`ơ2z8NI&{JXAI|' uɤtL@Q Qlw]/PΗXr1BvoW?@&cYftXy,OrQ`@U *3Z%X޺vih{heJȷC 8 6/̼N$-Y%6Vi {'۶._%f" M\קԢoєkc;>ċugK ؎+$,-Ӳ |45|x45]U@QgA("4># )8Q@WiHcHjVq\_yl.O )Ҵe$7ENj'oIIyiϨ(YĤ$eP^fA2tui},C%:gF ÞEGtRǗ{J8z@ Uċdܞp* .!TlApC<E@ٵF.@%} CÏbV@JEza\L`qkjjE4Zv/kHb S R)#R۩D%6f_=KXZh.X!ڳul)x w& 6/YwE|,ļD].!DZRt>)l*R& D$2BĦj~TVG,Fg,wUC]Q4&;8QCyG?5D2_%pR`GS¸_>V"nt)'A7X |TU=ntF-򜎢B5 #4쎄1 &&G"qKG/D@k[~y/uCHtn£8 N-`AxT I  Cs[Z7H)zW)76p?|c(4-_!iֺq?Nm,;x , \J蒽}_I y|K)%vlXܘ@L;Zc]Qt5)?.2UleH ͤ  +%%>xKyұmǩc;nBU1Z4JxmŮ  ̑Q_v6>?Uļ78bVdf*'?Fa%KPX̳dT3O>Ɯ { .v^x;_C( dbHASFgMm/Wq :3jSdׄc*;X9FQtD=%Gv׫NUHȳ~Q/Kska\v%,JplA{4]UÃ<$XV,b͛s)oӴkov׫( wPsNJKػ{HwAgn@hOxmP~Udʫ{0^־"*ذq CE}8cGQw>׀CF`؍"= 9QU-"($:oο8xU'^?9).o38zXV̟1<]$wH{q@#JST~twX͛0+=/a)xp;}s?S0j̩Z93nB^ڶCS&/ރ@ܣ7X(ǫcRfuZjA;ͽtJD SQAnh9hH)_ްa< ؎ۡu{oHj<Ƕ-ى(lE쩦Ύm54P bѡXz[î+|fsj^Sy3?A C[ulfe|))\.G>_vV6Gߑp0>$X4{q[kV>u{;uɵ8qA+<)Tv/ի}ٲ;n:uyr<uuٽ!1&;OhST@I8 aOҜmBo|@m}%jk] kqwlYvd~oi)}T{g8Z|-v *PT2L]m?n§'VM d;7-zó(j j,T눞EѢSG*ul=*0`P,xk;,~‡8k—٫_lݼ^}ӫw_Ȃ?,*xWqO3椳sY<m.b*ޘθ/%JSZ֓{9(Xh:2~io|#D5ijkv" 6dM0pIJ񬷯 fev}ɟћehmDz` t4}}cYv9r0pX)İѾ|KLGQGZh:gM={w:^Qq_d*Dԉ5Xt~=Vc*o[zhkjf<f G ZK'28FSc]86hm@hi[dwS ,C -)6n254Se14YesQJEC3d˦uhF )=,||Q"ن]xRcǦ6a*+;Tz I֠TU }qݕc ,#^1)-1)ɘSɄߴ(tV[j+X){UQ[ V`DҏOG2BçoeАѬ_))e .ڜH>*jxm2)R' )dRFI m{Q ]W%.v<7 ضGvq?VkbB% q_);M'IN0Ab)*z-M62$dk(0b[[VpQ"JlyE%WkHzH9MC /V]Rv._J|O25Z@ikZ"p/@F^i]S,-IDATv+ m,J\70C.`Ra1`ZpPNU%a F S(i=UzW[ _)9p6Q=2ا*"m}w/ʲx{/~u0 pgu(V[ҽttx(BhEJ ] Nv(kXAq~ ( HB5CWH&t|-p)ڦ0^p[߃fjrV߇wKC;p87Tpݲ> ]1@viG+>)_*!#r::T)AW$"0uhsr :7|rItFcF{(w**y<84Gw%{@wnt>%xIENDB`././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/examples/demo/Extras/images/logo_32x32.gif0000644000175100001730000005603300000000000023741 0ustar00runnerdocker00000000000000GIF89a Sm%dkq[ m/[ϏW;yxu@\G瑦;?_r&}XسtyF-]ʑͧQaQBFͬ;3l3#,{4 #W5<؂Pj3,+R7@v@ ~&8t,<ʺ$H`~w̄>$@E6<=R@pLIuyφR6 ɌX8kх)&#Z6)"#,#= 1R F)&16I8 >IiY 2bā2Elh B YrҰP7Ҥ=H3-~e h9}5C-k96M4ډ%kQ,8BmVj*ATE(>PMRVP8Ύ5iM a̹ ˊZũm֩ǡUAP͹톊94"ʶy1"2Sۏ(1:-;龜7&?=*)`@wƘ B՗=k\c# I `p  lCѧBk0 l $H` 1hL'  d .#> F#K-u% QAw-d %CqP@! , HPb\"8財!CEHQʙ jլ{0A1"mfjC|lL @SdFQi$ZTdͷ)ek!~ȺtAkFЪRE. JKA3%-r21^ف8ʌ&m Bm"uʇ eq'6UTcqWҜ]‡'hw{LDY94<@O"0jJ*E#u+gb_zb#@f]h#ĶAF9t OElHQx3 @F W^R̎ʴnVb 2:ĚͮT.Am^zr 99 &8|;>cM.܇V[FB_c]>pA.4[)Á* 8 H#>D!a$XBP6s*J#\X`&".eQ x@M2ݳBaC])XcY֥sAd}(5Y_@! , Hp"E,xPb0{ Cml\l=YA(I = Mܻ'mKҐLԤ)ͦ ?}q)SԢmHqQ@T#4\Ȉ@d]2vt;jv|xˇ(LpFe)18X\vӦgsҧkʺ"ԁ`8ͮ "fܘ6+g{(ACwZ=΁kdt,x~!ayp1zckq$ `!<~ H.a")LzHq̓>cL e1W% QxPv$pe! , `AF ;# ^#cT@体M͚ZH$)RH)3&)I!Т$eHfS#A؈6mB&M;F<]9,# 'ce*R֥ba4(t|R 5,@L. 샳=N>Pum`o;mUA(_V6Sed`\ }N] #ճ3ZU0MYeԨ͓;oޓ]><1IäX\CqSl( Z53caQȊ>hlAZSGUZjY]k-;g#*4߿1*C g~z@9?uCױcvf cyG#Ϲvk : ! , )b#m*TCmk6ɛ IA2 RޛJE%fM'S6i2 b(֢?,d3C8.j3i)}4ï~ $^Cc~mHY.Ȳ`Uj+ك(y#)Ʉ@eg1ؼY_bMi˖1Ԗ&wmK6+SADcC].l[|"KfWt3UZĉWaN 3F l*"A\ȃsĽdHpO10tO*dC*\#O7 =96Se! , Ttm()R$ÆQ fBʾ6A\HI=kticGEq,&r2R ? [J6"M.:jc̵8!`TR!MFh42dҐ" uM_4 CxXaKsMݓ.h8e2 .?^S5.pَ\atu!T<4tY WVE "J֍aڇN}4*81&{bBjcGY P!5S @>IF\̽1=Ia0R~HONP I&I#0G+p8T([EP@! , 2RlNF&.E R9" ( {zL iE[ ɱD4'g5א(AhP,ȗrἷsV?A5i N-W(Nx%( #ءUc C&M@ f^aRNHCEpƘ x_nZ.hÁ50n߇. ">ZT B^iTU@23c8:.FAXP\Ao,rF$8_>UK4 `{/姇Iec?a}DEЀ6^xԦi1\ň ЯBH'/FQ܃A X|аMJCe9U.-K k24HK)EEb+CBQQ)A! , (PQF*\HP w{HȰC0)R`bE d÷s:~#5bh5" irA$$143͆Guj"Z$ݛ%(EEȝQȇT EŻYLɇy;Qi0sGOۿ(p>W>Ԧ@4`R C k.t;P5.Ьgd !PQh9R;LC.*$V#t, ]hr2ƙj&bM#FH)䠗3b8iK@8_xcZ{J G NcL?` M[1i4`2Y | 3!&bAŵ7 x"X+ I|2~! , HPQ*\P w\$dHQk`R0,Ċ &M#>hLJbXJp @V.&{PZBdLL fG4XygZd:kV?# j3|ij"FRlb6AB8j8 5Wt'.TI0ax FӦ`2R$*!`g2$B,Y> )Cm.]PUNVa[,6TiՈ9Xdl!x1ůT4[6!`D9h' eqO h~rҐsUPeӀl*3-ipx"T1~pp=Fx#sHA/F^! , H🢃*̾6p-)ܝ8$a?чD$VB@LDh0Բl5#GFITBMq,Hdk BfGFD͌aLf1iP]Μ>)Q.Y20Hħ>CvDEZ aT<UX e66Wcy5s sNzd;ti tԁApҡȻ "ۣ1MFsYV Bz 󅊮",Q^NP{H(S.@:v.`<BDM 4H=k` M9YRܓ? 3`A;gH564G?< u! , Hp"E*ȆfJ NL-i)0 n@bV$Gm S0Pۙ/El TJfBkՒ`+S}"T>F0CVC@ ,&ƁSj25Afz#vpf*9  wn]EQuYU#Ft#-&2!ڧtVn2i ᘣM&ğΒّKd6cCMPN"wZ 6MȚtlCgƉ X@R9h&ȨDT&`P%q4)pz޷F ) `IȳJPz`:nt_`!Cي͡K06#56W`@! , H"F)*Ȱ(F!qfaÆe"(Z 5)0c1Ji#VeTt<2t+ۃZEB gXhedB<8l5Eڬ@{iSgl5sUSfIZ$дj}R6 Lc$65Ȓ֍tl&BO\W2GŒ )C4Ckdڠ6#Vali$9CWGht di( =TԾ2,R )d@X E -D C`LݴDM}5!AaTRXA⊊#aH! , HP"Ep\PQ ܔQȰ"}٤h7GQŃP`A# ‰G 45^m0Î\!81Bx/q-A&c{0( d䏩32.E1cif4Kև+ )҃*NE1d6Κ5C+L6^(XL)0q-"Fi>݁/ .4Q?E\B] T B2j3uD+MowQ7[ݍ:cm\Ѣ:ہ6)PDS7f HHAx+ YaKޓiӐLqo,\1B!NE~_X(A0(Fgi[Ȩ4$)$ i4e[hD9o@m>Shg'7R{{ @-N,.HS @@EVOZq+Z)5Ї(9v2-rR47kCĂ' ~!ÐdS`< *2ڳD881ъ hrP:*&gN4$t22R`hDHq!#N;ߛƉ#vӯN83j a5ФQ O4MEތ5oD=b $`u(GȴD:Y60 1C BH#daIgaA"! , (PA"TTJ(J'\ giPۘ}з#Am>8ٙL(ʐ^bD tF&L YQFՆ~4^6i)RCSRm!?ȹ&C~XS|$4`EfiAC+ 2d i`ڌD7" EŞ2߻׏M Ҥ-dMfF:O[bC@ɂ k8sˁ]fJdlzpԦAL2+OZ* T&$PIgzZ)aX>FH޽!Ϛ&'W 2d.WhҔ[6WP̌5t%!#jcisfkh"q| RA iQL6BBB! , HPE*$ -(PQ1 !E8T?fHgdH姗dgBEp|:pCrhj&D]J"I~GR٤cQљp$K˥͘  I[E XlsVkxsF$ضk*8;>|eڃR̸r=7hOѵrt 7`RX'.8wAہUk ׵>VA`4kYbX{*MGh"ZSt q{,=4FWpLjUĵ1Qp81MWg4G({ m&-#K !^ $eS-ag]wQ!g%mM=Mi0 MIA{R\9q&}) H==CitQFbl^&tXI'Z48|7%UNVYo%NN" klvNDkc,2MN\MH|C^P0 ٭H<{ēI$P602N†;P@! , HP"F *\hPu@0Oۙj8""AEŬxq32qyR$ɒd`r("t85fہaԠQ:4;3 3MjC%@,l#, 9m,J𴘐l`(΄ DXbIYy:"RiêbȤ d a01MAF;E&}J* 2櫄M'oÕ|hb[ZON9Z|,sΌ\X$Ί P4Qy2Sc7 a_Et3Ap.QV, *EJEsr(4("C ,! , HP*TH],[JT%mQ$q"mgxqtd'&ey(5yr5u 8&.&?-6 A,ƒ`R,q@-Ac(Lɒ[cxmթq#N$Ҥ)Q^i],N!EF)x,)QGU62f"[ДƠ' DQaMV% 3`0lq(=ٳfIaGNfEٓ5ahK# VDos6S*#ExD 8Bj-wQDؑ6UhD hdžH@! , Hp&<% u.T԰m(a>pvه82ʥ\5J*TmLxIʛgd*uc@:M*RCl7;jsĵkYD'ܴti+fd.Wd6x!)H!mr wQ2 dH, {*b!ƠG4NN@kV ߾ݓ-2`$P:0!z X)T I:n*_gymAH"\SF)xw/7,Xl%=ǪedK%%|F3r FK*ݘ !7Xac-U2IJ{ |E-вIḦ́oZ}c w\;xȌT02U8aQVkW%v,#'.w&';!G?V [Ͼm_P1O@! , HPj;cØÇQ .?8 A҆6H:@=AQ[ )ZQ$Y!a% gWhlQdJHr%QX16lXƒ%,hCgnhCKDKpV[Fj0ɛ#GʾQd-m8& o"xh?z1Dvݴ3ؙ۸s3ر | aO⊢<.| bnp8wq! ,SH*\H*Jœ9c18p`PsvZ #F(V%on YR QX#^ bU',>iD>{7Dj k~0>6tJ$3·XPRV' C6v(] ByF _E{0*BiC.F +mfATMDF8 K~&LkGwX <(p&+3 JxЅӧ+0Qv,&CЄ>vo*_ $P@!, H*!W &D(DfPu"" o`XH(b(͔)Wˈle•hcieOHK10 ib2JxAڠ*T+G.!m#MBN] $^(6m!ŃQ`ۜk*22kVY= Ӥ[ઠ<] Zͺ7v2mgc2rFo1aY. b3s(B AӪzſP ]~ufBM_W&g[.x fMHPn! , M&GpS2za3 * d%g>J\. HL t.dp^%"PTCeigQ){A'I! YQ #[Qd`hkB UddV&Ҥr +8D2Z`tEd2X1<&!YTaP SA+, H :%.BUtLϺ_B pBFpaD8"EN$ƘHk(@p&'h37lB ܄i Va(Pa\8D.q@cߊ*! , Q<8\p6wȐ ѓف53JBLtcf؂* S@rtqB pcL.tAe[Wn,a n J4dShō^2b"d&d^.դ4),.LE):M"M L*%#֎AKŒ5^%@KΦKZZܴZëQ )UbKd Âݷ&kЛX7tuc@H`\z ?gHi K`Vx #E~IItB! , 8P#E*\HP[VbBȰ?ET sl 3-dN8rM[BڤB%BEs0e8klcғ{451ɱ'*"4AUT\CX dH4QT4I! *҆\S{YL,{nZZhAR( 5Is$#5מBEޛ-!WmR%Ű屁:hcMΦ(0ŀNpȨ  +2*Y'1VĈ& j@s@014" D(f& !(3H.h#8:0v>rd#hX$>rXKr1.eAOP@! , (P6E@("Ȱ!A_LoËIř)yX8PQ ]>|x`JF#/*A@*gDQL@&)5 i"Mo~Y}V0JYRXQoIunB ]M!C^TA2bBXQ)B̀^xaOQM˜E8JpY[5& Ou?dDMӦHѤĩT({ԩ`h6YݻmA7mz[ʑSK80FRQ.wږ4$Ncnj\0N5#0b C 6 X"\&`1p#'N&*fpLb@ bC;2c! ҈*5`! hޗiA! , H"u\`rS$8"  ܜᄆa& nb %w!4rB##R@ 6&Hdfx̐K*h!N(e>@y%nd! , HP6u\p3_xaCEŰpqbEˇjx( 3ZȐuف+*bdDJ idNSV ΁^T(C ,6~,o֬&Rȥ-$8d8j 9gװ\@  7s*Xicf(3PQ5ǽ7vf㿽}1mEVм bN&#/ <뼆#<9!آ38" h.L3jstOk=^Q[1iLyJdf3HKp7gQ2.ɂ' ^c\xB 02A xL!6 ڜP&ҌfPJB2Ԟ12&y`! , H"mgr)*Ȱ"FLfaCp~< . |pF,F /4d\ym17hɇ5>EěYEFK*mdyZŰ KUmqt@Ed4hYEHܛAZqiA̸VZ ڇo%0+ CAYiZ#@L`>z0K Ő" 81w1'>aFlQpH# b!f0b&4r XE!q!>'4rH &76*cC#=2p̓Q6!cB"&A! , HP"mŊ)*Ȑb?L Wa?Z42d]>|pvSva"4i(dy FjXyQ5rqlhSI@UTa@Ae F1 P1zYiƛA5 b+*MHH 16&o1)MnfS3 Ȧo)lEf[2BYbbv)q6aЄuحLf4VD{hZ];LiC 2ѪxE 0"zgP"$01pCw!p$!`2~_d 7' C rL '1ߋx&8GPx9'Pf1G! , HP"m@(*Р""ÎQCi! jY8PC\pdCˇ8]zaH!e l C#]fSN IqĬ?Esd(ŠB81Oa2Dg6TEz!m%+kAF ARtS8Akw_Q`x;S)}"r4sFaATR`: j+  QF0cL0] *Jf}C[ w*3F9^,#@as w10Ј%b r~31l p#F^a#f@lr.aRC W!s 3 xQC&yQ@! , Hp"E,xbĊňAQl\SЂ,d9ۆj` 3Dd]>̌8=DAhr* ªT$$qoMׅCx'حrrM#|1_|8p`]G),)dZ B@Iz04IAB6\2Sc뿨E0zo$?^>Y #EfnM+  WuspHi~=P#=S238 i֫6#(`Uf|ti^EG1%m 8j^kmXm]qmyrm;ty1:v,Ã9gľщ(ZuMܟ$ Nl^{g`# `̂ 6hB )! , H*&oN36`H[5G H(%1a 3&c= )ICˇXQ彘3kv@4ѻ䞴?HjB·5 Tel.*ҖjOarxBʰzxO ctP[5$ +aJkR,X;1Pl4MsCbGeE?`Mcb'h\HZ -XR`U>V8"8O"4BTr2!{}o U;C'nL}0 $1pD1 n&P8HSJ rnH#' 7b &"Nv6D$ -1ߐ'P@! , TH*\\CɋA œ1ɽH,*"N %TT= )ICˇd&LP6afV| 0) A 9dhV9HV]jX驨M*F E6-hk4+0׷1^ 4S4!F QiXi]-A4 9!,h\QLAMNFYU[ }lG͌(O48=B H*f|,]|?t:\{~BE n{ȣ#wŐ !##p@){#nrt [N"4rŒ3rsy+] #"p#vA"a_F+ u! , ڜ(\p h3 *0k= YբW<5 HiҐɜiתɽhB3gYrE ҠEXha<=?P1|Fxzt/ 94"4:*sF0s)b/4{r_`Q48Fk|XQ}8!W<3h+m [ @*fiڥZxr(/׷950sLƲ5B_U#|cqa1 B',A} q3zx!hrQJɃ+%1h&D1?p{@آ>+L.&4r?6 {)2 # ܈!)DV"Q$1>bR Ef! , O"E*$yQ.Yā p̚CZHY&&M2d4І,uYc7c!0g*`%H 9]:;͖NJzLJB"mQ}Z#8LȖ?8X>8)nbFPjX!,І),)| p*F MIv׫fp,lݚfFKY1};7ckEV0^* 6h3욙U4v<$R@@} H|cu*daxgA{IꉧM)0&4@ K!Ckfテ!#Eq] l <(AQรHrOiHsXؤ~O.X֕IP@! , (DE*LL?#.YL rpbBE=.6!E 0s"zTdC=)DA $&d^ HѢY.dpOV$%H 9]>huv \*)ᐕ1tS2E4L!@+ aRIq*+61Ebg8sVS.ړŰl,?F4@*\krTӪfLn6!\1~AEQz=b ~6sH]fTCzE^6HqFSwAdJzp1Ǹw |6AiÃ`  )\.lb&p%6ؤ YE-6&"#q 54r“TBpL#bt3\9vr2G J2.!, 8PE*\HP[~C dHCMfb f5!Ep:VƬU{%DFKYJ,]2d]E1fE.]>| oLѣ̾%Y:;WBRO"AEg}m]֨bJ$?Fh LbX>\x #MhQ`X"dU>|Dy Y09AXLQZ>̬, ʷ2Z5̱AyV9\(aܡW >qo1~ZQ(c\gw"@ m'Dg`Fl!.H` z lȉT NH" '4 ۀ1 7I|b}A#bp"Dzgd#dR;././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/examples/demo/Extras/images/logo_48x48.gif0000644000175100001730000011516600000000000023762 0ustar00runnerdocker00000000000000GIF89a00Qi*\lnqd!l/[֏];yxx@\ѐ@E?&}W~ző;bvtl-PysuwNG^|s/r[<x'[Rj2A燜\燧sΑo6s,c}5%yMiTƅbZsEJy0@pi[>Q†ljS2cyG;ߞnϗGݜd%u8ؐ{,YpLizjwn{~ۢJOo>hYv"g4}/yzm燮+lqv?},iZz|S賵.ϜSІZxxz|֫ʐN*&r]Dy÷dH`#`|D˘kLi5`uog;z~Uo0zBcvoRwMv/vXz(cȿ탪3g3̇1d4ϝk}Lt1Z"r! NETSCAPE2.0! ,00 H*\ȰÇ#6ċxbW; Y+O9n,G ybFʆ)9ECOJ𨼉Jy$$J>NaZ)(J帬[zU[( |.ָ[ɴ [ G#v$B=uKSeMLHM?)iP5Vl”fSzk7Wk9n;sYkenm*)=H>زJN\k ͯϿ)݂$'ЦMi9g.w䨁^ۛ8,ȒCq_F]v;@s'"t)_9ІmdW 7b~_Ȣ"-ԎI4iwх"^H8$nR76JT\j) F)p4U@ٍxUX)'IT'$9V Z/nt͊ jbK v'h6s&-C '32S-NAI~b!q\m3O >D<03o1 ,3CpH+!N;{d ;訸,pԁwt;aD; ( ֓|I@};@c~(tFP;x%<QX~P6BĀI))*ȠCc6<"}IXH#$|7EY[LHye,|iwx2f6eOHYlSz6g#q"UP;PZg6*5S3 'Cz_ +3BHΣ6'<9+h:YZ`0(5ŒcOBASL$>q4Nf ɢĬ#4B. H<qBK p QHl2:q\Կ4"? \5̨:!AcA! ,#* H ? *\_X},kHQk|+gaCŏ|X+IJC<~\ EyB+%#̘4L G-㍣DgР5j@Li] t` @Gk|$-5$ڌ+nY:\ږo9q[% mo _F#!49A|~a:uˬɄk5gph@]Pp8ήM2[/ҺRĆ$.<{H<&ȼT6rOG BR_da|oOȨnj6mQlǟz&G;3]z 4q8wwb_;xa!0)5A֬"2BY'rb;aD} ͆a#<=<!&x"pd[$4ftHaF6H6YGE&xY "PN!g(m՛ { ~tHgpQlPƙ3v R; 0SSHBkr3D%@ZSP`!pjQz }(5Lc.c^=F%"!EL4h[^ %|A0ӊ 4rĜЯP]s gKH*Tɨ*R 5"P))RP*! >wL43@l(HS,]H! ,"* H`?*\x2HOX|8ba"?(+;<*tE1dI?aNѰTC.P! >si t_VPg M>Ev 4;CʂگPPn][n!v੠_^ۆMv7PI7AQed'両*Plj[bdž7U4)Zc&C)A~"h2a-~$!ȼ1`x@08O P<UpHj)'W@24Xy3!̳%ڊ!ฺ-S3qG,mL+pn>%,cM\3 *4ryr/TRr`C+Nr5 |'u_P!ct<R;p?>4*A4`R)A -RA=P@! ,"( H`?ܴ3ȰAi@Ë!Jl.E:0:xGRyP@9$2&MI,bDDL)ijh*FѧD֘5,VbEGݼpmʖ*b֨jߺVOg[vމXCBǷ@!ߟ8r"^= 2c 5A3'&nk)3 WngnȓP bmAܔ+I{""k?nۢT-yw ǀb{8( A^y~`G#- (y&$`~tt@D#,G,!) #pLe$ V<Í b6:= ~" T]AJf@\d#&Zf@ģ "xEנGvBo dC.=t@FB̐/UA6dsJYSr/p!DҀE"TI#03ɠ8mSt4dsH=MS5 ~a!(|J:XESw< 3*Le) PP*Q" JxwY+*B!鮻Kb *F>!Q-yGpfC6̈́p y(1 ]"|$L?! ,  ( HA*\8'HxbHϟ v9Vc,-bܕC<(cbŋsrʔ()}@PSK,Zii%CS$lY%uP+IZ;M5Q20pG}KlP5 %1 @ FHF0%XRI)R_UY/aX'T`tGSO/&L/@fx$ 'sb@(gE^8nzG@! , % C*H۷oTf'O j\A4'iƌqdo$G`_sj9g 1 _,-Ӻb/9x-䄱\gϟwW^%95o4ij-?xy*϶ݻo u rQaNEۭmB7<*5KbTy5$鱅L,඘7Lן`X [0P& h 6҃6fÅ&!xGH'AG- 4n!l(E,ӎ' ab/V(61 6*T:@<mC\ )_id]B}mANN 眔MAxC>ZY]@qBm8EĠTN9:]x:@}5EHx~D$oOO%լ} 2É$ ԳAMU`ij˰氊 }i۔:O@! , & džv\Ȱ!CvU.7qBja;hQvkjA)8IF 1n3k9Eg \H1s5]O+RKRcD"͔蜑.BRd5k9%2|1!Ipk0z yt:ŕ\>1*$EkNN▶ [R C[QO YK$`bIЃ  !;d͆h{&( Xc:<-@"v&8e|G3/(c|Q_R< 7@X5p@mxđ ͍1,X`P!,|>@(ZW8U2Nt 4t)FAx=`c-B@) aaEE쐎o", ;]K 3 (' SR;AAvXE AvVlQ[J&Yi|Ʌ]I J5%|+n|'Z݄kOx ;VƲJ:|s`9MS[M9*|9.pc@7)H9Y@S$ rδ0(]3Fpz>S] 0{eu5ICE1‰4 T*j߬b뭮Fp*'Z5źD߰Q5 ,C3`k h\֭l0uZ z{nXk ,„yP@! , #) Hp\Ȱ!?:UСņԨ7O /]..Ȇn89X|iP6La %4JZ4x`·ufҼ9%J߆D@ƔVj}g 6q&)Yk+_(m@ IъQJ40mjR"%a48B +p2PmEWNq:klR|3N'A'$ ~S;5pNY0C}̬b067G! , #* H?<\ȰA<и㰢?\-Kfh4Dɏ- eLS4с3@7&FS SW(  uJV6GR'b:R GfYWY*['ySc&d!*8DP8aSpڅsd0A >pJQd mb 1OSOOQ+Wǟ?ȭTmxg}Z]eSG8ө2f (ӮM摱 GБh] )_|K%|XSA8]u)ps-"Z5_C"݅*v<(L|05V@zjfB" PʼnMnY7-":`#^ŽNxa) W VLz͔qUdy5K;zdPCL d@69!C dAQ\0|qͣ=PN 0QY@P蜅yJhPC\J@AI5@Ac%A9 ' pi^ Q7FS46ue;P;$H,:"P.AqK/ኻA ﯞ {,ӰYGP@! , #* H? *\OX}taHC]jTX?ֆ̫QCqx&znۼ.bqBOCWI*m9N% J1PR ),1U'ւ C'VBL Sص5=L;/P'YcJ)ɗr(o`hӖ 3t=5j5p#Ѧ׀J?:np9pPT+A5!B0N Hs(@(2E ciE5@a lê B1y9NZ맲E{쬬5;b &sm%^cAZnP2úN@cͿ9i@! , ") H?cC< 3.5B۠?[Rv&3uF@ t54hcLRx>r$Y݄ ڬĩaɴ ӃO',Ptʔ_> zz$)Sj0Ķq" XrPB SOmC9L,̢FFf! , ") H? *\O;k$XcHQCh.Sŏ]]k^':Kn! ,  ' H`?m0Ȱ@֨p3w0 5.ɘϬ>`b?kg:)i0Z̈́I#Vl\ˇ40$H`ʔOm.54LSbR$ ;ҷ6T@;*VD)v!B JʝuAGcNR)yj?%Opݶ]AT*OLyIS}M)YрDxv:)|Rp\}zśjUܶ:i4Bn@4eB&fSS&},(Q_UVI8a9p0B (@㙠 VEU<wu([BQ/C@ds0 4vOmM,W$C4B%ȡC$ pEp'y %PQhȠ6-3w685dWZЦ5fjMh`68'f@p&ևD?H"z *4! , & H?DӦTg];0a—/w6>ք#Ryi/i0^&&hT"4i3 r :5W~y25@v ȡh?s5UL2H!^EUc,:lG>\d!=IDP5N"W:A;brDP;hGqtQg8U(G|Bۢz J(yb-R7~j*jyJС*ACTp# B Z'jX身@ָ+Ay! , % H\h5$ݕ+=lhډ.j $0aE˹:M`Ԗ8nO=!j?7  "**ԄxiiAAGKՄ}STALn2f)3nU79YCJxyCpUjO-K<xL_NVL08pNh3f_yXe(Dir& _Rq(eYނ[O9@LE)"m9x8A&b]x8sG|@~R*ܓDm Cp \S:;ahCy Vh9=)< r,ٝ|jDfB  qs pB. >.! , #Hp࿃,(O#r .'! C`?G%H`ʔm-0.SlȒ&ؤ.5dbsI*hRøތ$i΄=]L""U#:0-ѫ6uuMS$&et ЊM)~%Yr,O4ڟM8P.c`v-ASL 0֘1TէvvDKJZ/O=(iD?EGHdnY7KQ~ҲypY!:*?|>%ܨ8TpwAИSC5hKޑ}S.hOVfϹpfGwk9 ?C6/t<2PmO̺v͍u'"M?ko4àkJ8SW)5Ф -S  XJir%YzyPpJw˅PWh 6 ,n!/$F]N`|A5x  ;|C79# 3P*K<Ă@ gQ fiX| spֹ yĥg~ cӟeF4&~&bY&' ˠfjC! , # ݟ*\σ^Pac-A5jz,aFO:&0e* 1 ! Q2ur/n0q~[j ]~D,MjWg?tvtdFv^"˭7rW".V5s0aN3؎4hX'_2Y`j5LeIq[;Sv1*i;P+bOMQ;wB () c~+C! ,  " o!ȰÁ 1* jdخ׎ ԘPiƓ~hiDv&*T+M%P&CkVLB,& Сrk2@Sr@φ͓Z nQN5MEl;2 uflgoK%}: Ԗ!A <2kwL|r#a  ҄f]1S TSՙzsL2V_x m^)SC! B X ^5%\'u]iS!"+b=ɀE'ЅY1'ܣNP- aDP K)8 xH[xyLA -_)?qx(Y*_B<NՎߐ]G#.yQ 7E3L@M@ĐNЕX>>q@n& vFXǼGj78qMc$Fjz.&(k'L8kC I&+:k! ,!" 8?*\Ȑ^@ذb?v$ -g톶 LQ Ȇhh;/+)-3ѥC;|8㣡D+O1hIk&Qbt`!rAW%4PvPĊ[璞4eb N~\0wׄ9]!X 3,|4pҽ?5eh90b3M*5hRY_fL)ХM9RINYnF8"_ԃr M5EXӇ Y`BC/x 8 w&|؂yT D'An|F&- iM!Lڇk 8Cb ~P"_d؝ky2'HaSrv"A|%(;BG? )hv7@n.! OO_P&)(lPP/ Gbe35l0 )@b|.XiL*cAd9)sXe#8Q Nă*-A$BjQ;whͭ/D6 lJ{Z ! ,"  H>)ȰÁ*B‡;'A{glboiK`JML$K6JLSfAk v4eG>;'c$r tC%!ƍoTv]*К5qɽ ׁPK gEWn:Tbu"L*D)=uOR7p CdxG&uR5h7d418iǺqɈ|aM] ט|Yet~IAL$SBn|3ΐO ?2')G$kDI$xRA0.,\ rL$_R(n` /8qT8Zt@ p3 kR&GS${WR&A\EmFQ"nkR|3Ĵ:D"6! ,# HPk|)ȰÁ$ELË #jӖ*bi- LJ@ȇH@S,\dخ؛5M£s'vGlLi m'ʦO l-^;[.Nv CD*OcUnJ҆tw# \~)Wy$D` Wiy()Rh!OP؋谦~#PG(L@ b4摇"hGD4,:/L2 q}A%E"oKbK i0F3ŞȂ <>li!$^NW')Biǰ ^K3< Vl@0MH"ǣJ펼!Sf?<6o:sd$%tz׎0쑩қo<}ik)-^%Έ֗RecVa;9 :.lWl'Zhm].P(EݮjYG-tufbHig.Hb@Wtԁ갉<{ ,O -ct ~U\;ƽ2Mhjq}!eӂyOko7٦ך4E bzJ|&^`;*᜗-hr ? PvSL(`n8"d sdF>pA~(G<ad@v18\" +b;SBP)!% <8w}AyAIL7,?b@ 0EC:!I(P_ d$`0K`NϤ°~IJDf;'$/8RA?y3^\nşit /C[!B}P;hJ,DG uKPc:æyz"OB vtP~ {;<|1 B"3gZt5M^A! ,# H? *\P`F64)z 񑐢LJiKʃ+^|dS&zPA;F43.5-Ja$tMysGΝ&L֮&A,4'v`zZ$%2۱ g-$2l˖tcf tKQvPiqUc(L5>Z'桫Av>UgL4n/:@.$Q5J/5fs /  "ܚkؔ17 S"MT!}sW573p,t^A]pX Hz^H9xBLÁzQ)1_p!'p(!_ KY~Z<I𣁔 ,WBʋ($7˜/ȀG,LᏔ_,-5 ]"$u_o-!B0 QJ'$`(Ptb* (l8tl!B''Zuu6Ư &-`)5׶J5ECFW TgÔoH-]|#ak_J$4ݺ~Ͷ b(cÖk\@"z3E$|姁< kĽ0E =TXQ,pax8ŌSȰ(2AC|H4NO3*‡,>! @! xH\30ԁ_\$ ;"CdIt9̒," (Dr\PCobL\॑^zEDE0ś0Ġ,sP'_I V x"х,@]*(j 3WmlJf^*T1 Ie|"YK~dގk5KQ'vxjO[2jaD?eĈ˳p$ܴztwk65Z4,- d o5FB@g1J!Vİ<àf% qȆvAGC`AS[rRE#J '^]43 nȄ<ȉHbtG]qkWڙAqtS]5?Xp\j:`Hه߁S /ܐSmP<!SĈ B C0χ!?S 982%MWE$/dKI&a(|'0>#\ d$/iÇx,_|1,D¦'UpޜS%GFzz  Tz2SD(FLSCj]_y4%P;بkC* n9UࣙFi4_f, Ր'&j„>,zM],&I<|ƻ&yüfI;! ,"! H`?61ȰAxT`͡Ń"VУ-A3.:(͎MeP@:UxǔL QtDQ& Ւ!Bk~[&=o5&I<55#'3ꪩ{GUxϬvv,U ý\+֥@OVPB=5kuvV +TInE`9~4L]:5jt]M6l1@_~凟5hP/!M:V\!e O"\b} ]@}MUrax %|r]\E|3FNqA> ϖ!y] D|L i pBɑ.c %P)נ Rr` Ȁ43|"GS",ds״IÇ%l:WC;Mh#_y?C|!K6+Aįʎå(̒jy" )^-lk#B ޒ).! , " H? *\HGhaHQ BAF[#*ȹ4ovh[ k"sL-FAkeS@<85OVZFO7 jDv=pɊKEGjL$RbA=Yk Bn`Г$Jd]ƒo.QIo{Iȴ!\n $-V,%Wxr# bE*_=J`;vKjUâOD*,̂:pY8v!Z#Qb܌cJ7ovJTF

    b-(Q_5XbCm ~7p!F&a[lP`⧁U7"3NN:d1O ]Ԡ;xT!p}D"L$8hSS%7|A,`߉rQRBɕVX l9fs?Ta%%a)nAJ> ?CP],<18rQDYVƐ',hU(ki** U _@n64K(l* B<묵[-MŶ*,< .閻5;! ," H ?6XÂyZXp ?3(-Z@&A " y-5>╥ -2nhгԂzL1#A7LsiDjbpok$2y%+ JH72 Sbۂx:6iS;!˲0aCo!KڑGwىě#1F٠鱋KGq܂3dƞ(;cu y4/3n+ٰE#:5 ^AD 0;`_ X1OdA LBCDN֠hcMN8\0?09œs9hp?>OHIɓ`N011_\] yPBI(rG쒘'yB l 7T!T\#B2Mm:GiB&Dcv iy«8ȂB>t*jkA bר C<{- ! ,# H`?4O;O6ʠb4n]EO)BL76kĕ&\bgŲ F{dž'ѩ{,F=2>[Ϟɳd) {je5O%yZq0YAe_,,p3Zv 1bXSN}X:.t ؊^OZyƓɚ!1{K8/bG{uֻ1co zaMG<*<EWR n8Xs+֜Qt(tZdRb`^VXa@B|0yA=fTB0@@:l<NhNgDYqB8ml<]`#:H'B PRϐCHbhPLp#`$CJ2U E(3 " D,UvY,_rMks e&BXmz矌F)_@jhL .t Z5*(h)J]¥C˞aI8o; Yg8JcƜ8!C" T"QL"eԓ6a_D  4WaCD0MASD"%P_ctQÌvq/O|l(n%iSX!F[9HPw7AC1="x"H,NmdApKQKcF'QH2/bH A$ # b;䑇?.Sta?`C86Sp.$3h`83?xݡ D'tJPFs )qyB,,(C`J\;_4↧@s&KP;(*B:kB@*vz+H\+:l;< :>$! ,& *ǏbT‹=FN.QafEg7-,-t] >VŃ2Ӄ(K\xcbś9Yp{Hc@..-3iYT P/!)a 2bϩM栵z`x19R>R%[Z-Ybgv|//PlɇxQ)73fPSJ`cJbiJ;" Q/V(2 <Xx#'dhP rH㶋]Iu̘262C8|C5 yӏ(,  "}pć?Na]KDAdK3+U7.,AS(ePQDaB#6]C>"q?a3 PO=5#0C.r3Ï]td864r^TiB9TQcs2yNyb7DLp!pvA"($ȰCу><-|Q`, Ѧ Z\٠OG2&7-&km|󆎘eA.XnkܰtZjf!CQitBO2@&5Ldl4fǭB pKSIIR`RB$I*rEɥZ+$a :zX BA(Pv|]p:^iG^d 1xL\P]#?Gdl p 9D~ BdAA?ErN?Th'2P?B8ؐqL%8 "\p9$xߠ!$$]aC=Y  |!Zkn|3/ 9E c V`6k=@̐b xF$}hǡsq7pActa h*l^t)C1A;P,A5v )D| *d]y2,&lLÌ'r,(ʰVK p0sHZC|q!P6mȒ 3LmdC B68LKqC1s]3r! ,!( 8\Ȱ! 60РPË -6b(xC܃GlƔ͓)w@k) Deaτ Gq6je dJS%#FKmjF9nij"ɯj2CG8F,I@L:lŕ;W'7mIr/Q.85DCw|88t1*1?}3@՘?(:S- eBI5V 01ΎDd7f7tU(/` ZLdvQM(@P@29&1"KD|A>R@,G;)5R >_Gă,q@6*Lix4n6B ̴ v +K#(14+, 44p0 ;,)Ғ,̴nKD#_! ٜnf 30c G60sVo#BK3 fl ]s<5ƾ (r! ,#) Hp>\Ȱ!?P2AË bJTT*b"GS hӦ'$j g”͔+wXq 3fIk'L5UJm:yhK&5Lc^lT[šFʪhdZZ^=ĖFBk!ډWMcAh{€qGX['tfݛz%)iA]ZvׄzFib; L]C@n.5jLa8g8<5Li.а=pm%_^/*pVC99f3}>|B7:E  ;E`v [0-AaՀ ,rXIsA)G5"$x#&bLI$S\#`2 YȂB# (tC J !Ebs+)(:Kon2M+n¬‘ \k+~!!˃,D4 <4̵ѡf-30bédEs p4ԛƗJ6hT!"vJ@ÃZ\ȑ ِReLt@7q"@v9g]z \ēC |xlj͈biN(APQaJ((@ң 60,C]3,:4aH< u6iAHJ*'dӊ [5Ӻj?\!B[ G#\bH+r+˻!l  xM6_42pL]ll oOc3p o a{ͳD 2f"7ehCK0@Bd#GP;?0ɐWY;@}DtpPMqtY@5tiL v} `i9ui'(pL(פĩD4҈ıA2̴R@5(0Bj(\E K,yª)2 Ҫ@-E~>L3 *4r Yl#42*p ٫l6d %e ̮|՚ k3lLTArp RBZqD{2]2{7B6D 2K77 ! ,") H`? *\O;3Ga/xNj?ё5 9HP;`,$15E98CkL$UTp-uǗ  L D4A3EQ|oVG{SpA~7'| ɀ j)c*C*h6؎<&?. kLӊ DZ^H#>KD6H^.(4!2-).M̼o+pLZ{ro + Bɢ /nL\ZN-+ g5TP˖z2/,_P@! , ' H`?|lcC=W/O3F\Sh+AO:8O  N#xu"@[/_?7-DF t|RhHm;0$O\yL4TUaF(WvjϩNEȬ[zb!7,٨Plk~p_)si'?<[b&Mk}Ǒw(ȕfdXs ^Lov!Z7l.TjXIDk{$j#Z~Y8Dat'ֿf&c;|xP2QkZY.i;hpX Ptא' '99dN /cCPS5ta ~ =tbUpP#@PE}(JN98|AIC_beB ?x9,&55ag#K9$,aMBIi{Nd;n҈>nوz)(4r b,nqd2sH+`l+uf# pȴӴƪpd{H^klllJ;.׶rHD@<4 n24di4 3ِrM&AC#M#C1W6ÞƂA&! ,& H\h0Aa?鈷=qBqCc xL0e*Am<C.n9QnŔY<;sCuF!UL;?yX-U&bҼIrXЪz)'lSU:zb֟cFO<?>ۼV ;Jj2PXSj.X=~g'lWc;q+ ӸX>+ =+L}onzm x:-{sDyU\%V :`\Y,-}|rB EcTq |RpEE0 5C @Pi1gP;<" Rb(6l6'qE%i]dHP;~Ix.gt)K6P;@#@F &%@ 03KR͝j* v2s *Pj竲LZ+3LcfH0#!J+$_j6BK4" S+.X{!l0뮭0 IB=pR56(YrOLP@! ,% HyamßQPx,v& _z#CɮoD5ʘ1b2a”Pm_<@LP[H5Ѭk{3[ b!<0SkD@ \)(|\#d#By! , $ Hߟ"AÇJ(XEbQH؆J9~]С'MjLLp2EC*Ʉ hDΞ@ hyKSQI29ROS5UzVvz1D['7x@̳v <&ڠ!I-Oz 714vzyN8vejfJ<k9r,1_9.ڄ٘uHv%*POIN|t0|LxiWɽ5tkk{МϨ ?U?:Sŀ\nq3@ 17Ap HP;px':D8)_TGy$@ĉYnB/჌hcD4Ȏ<N"dCd3AK6dGJ (4G6NB)PSpb6B^#%jAd(dr|~nMf̢4Њ,R$LcH*B(Hdfhj+:*!,'f^yi6p8 xsFP@! , $c͍ ڵð#F'NAoĨ(ѡ „ !h/O)S4ԨPnKe˕+YjuGv@,\pA$Xn8:8-@8ĉQBIqzG/N@,xT@/ƈL됐%)$8z۠#Dp?od#I8_ ~C IDڠ)餪agKH ~#&z6i`ybfL,rA' yL4l6"'DDM2sȷLcH+*&`4z-n!ކʼP,![o{z5Cf4kope  ςc@! ,& gC?*\poK4\2…_"QSF< IR %‰lcrar4̩SM&Lw# IR)ӝNoA5aTzdN5j)@&c&ƏB3F*K %lEO) P ڐ'#kVێsk=Nݶ =6ڠd쁱M.k=Rjnqa,k.bpN~ Eh~^{G6}cCGJvmgjXs%4[d ؆7]tT)768x2)u D:4G)Cݱ.( pBX@-NQ6ZCaJ,l C2p\QFqcP_09D ?NUH6xi6`ǔD1hh 9dF:]`e#i7s[ͫ¶訯qfՎ,M\H1E̐!+* WSw#!Ez1 j@S@53!rS̹-4 +bsNj2A=Sz|J9U ر&LM`1s7Ćp ޽|Qm@cWt3Ǎb%ԘwZdFG‹7[5yM%ж\;4Lm6dd?ۂߑB%X7]-{CMg:5Y{l@iwwC`SPlG+,GBdqH~iS[/r:ȏj8{`x% =! Y:q_ 3fp.G;<}fGmcD4҈:]hCij22-dMHB禜:Нa'Zf+E>#٠0PSJ2ЧQ?dJ0pÍeQ$w!2 3>S l<amN!{f<44d 045Pm7o+P@h1Ũ 5(! ,") Hߟ*\XP]0Ȱ]\r򁐢v(oʌ":zTGIy4LѠb%G)yLY(.cDQq83@'[cQ1$T EVplYx[aJCf-nidHCX\'tA \|0_~1.TtPb<AȦR'HD+vfdJ2j KGsj -ljm; ÎQ6,F6G Yg[:ng;\n0g^,1QzG]#]5%A?Q4vKr t6Є5" \Bў;@CM(ˉp!x0s5&@pX~?+|#>@_iCG1a 7\cHfY=XҎ_H#cܣSȢgh Ý" G2QpO֜P,Ƒ{fM;yF PDn NZ %}w fl1G)Cn$i6DD=(Pn3`KJN щ0 pn l :J&e &< 3pLn;rD;EC#2!hqS@Bs1ǭв 0*e2arHpAn0A6@` +ƶ !,$* H  *\Pa; EyJذbÇ1p呱 ŏ/)wpH+P tA7t) 2!H,)[,E ({έ> T;././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/examples/demo/Extras/images/logo_64x64.gif0000644000175100001730000016002700000000000023752 0ustar00runnerdocker00000000000000GIF89a@@Qj%[eil_"q0\X|ӏ;yvx@񶺾쇾IҐ@[&}>cwXt?Ͱxy-ˑvmH^|QtuwiMz0u=\52'[ɆtiiK*c~m?Wu$yp:n4͢LzUوa0R-XnU[kM{=tY~ɵ݇+ämÇ5=y<$u]Gͤckv}MpHj~Qnz|Yv!h/j1HTk}mor?}0yWr_;izx/Tޠjm2ֆ\z䕖N«ʑ1Oh{*%rDw·쑬dϘH˗jۆ3d|L2^u϶MvfTTG]By#UmjlnZ޸zIؘ_{z.vnG#`};3(cȩjo`xCp! NETSCAPE2.0! ,@@ H*\ȰÇ#JHŋ 5qa#2tرFA`.IQ:/PhIߕx qϞV )v))ӠPna ?IjYrm.h%+e_Xv궞_gKПhQ߿v7&HPM{egW=Mz[S ʀb-F۸kA]\#j# vpZҟ[;wCZ&$n6ǑD(o=;TDe }$'&8ӟ TʅDcDx#$%Z ry9s $@ ~3W)(@wyQ$p P#`v86dYd:c۵`} #KJb:`12.(-4葘tD $iye(i :]BGzbd ~3 B)e:jL5J("~1?pK+ 'pZBN'*򙇤%Ưt$Gښ&ɞ,P^<#TQu&-~8[֎zhG(2Xz+N ڑr[ suPſ{ښ0!̮$QjA`P#Bd*~QKFzrAA4ҁ,WL)FIo-؁BcJGH5˖B[Tt]QW'.;tE&OipZ+EV7w%mjk8ѓ~T%%jMKL-i?9`LY*}$d7`vFN|Y74w/T3Y8Hld-zӂ/gUI0OD2;†g-=xu[Nqw#l`Pn bR]=Џ. 3A / 'v %qЄȂQppC0=0DhN`g o}6|& 9 kbY:GL1$ iqd7T;I<16q $Zdbc>AL"y! , /8 HA5:ȰÆ Yŋy`1<Q/xaKgC3%(4B|XfM(@7sA: eC 3JҞPL>ݰcÆ.,D&S^X[om bE׵x.^a֦yw]חm?<,5(^٫ǗώA `H'9E̵L7Z1ۡ^fW [S "7/p&i$So?yP=|G]\C^<&Qb#c^vBq*5 gd_wQD!*$&4҈zfڇp y>Xbw?N.at. OAC:bbX0܊K"ӆX< ~cϒ5d;й[DKIf:&=z#V#DBEÓ9gV2IzCb8^&#-Ye&!D~oꇠ0$z*@氵QAlzR*@*L2K7."Yr?U`c¹VIgRK4[ NYd@+˹(2?X`o*/o)ɊTAO7ԃ1o5.k3=@-Ht8Օ|Sɵ(H(3;Dw(7tsx@"E8@-;7H]7~=3YB%Z9 AYdz߇M"SxTyBl| lvRm9#l"|iϻm'#BihR#I*RKFe' ϱ~R#@L2*HrixEhaJoͫ "`B[q-UbP#. \c"$ qzCNGD-3e,zFϵL3B^;lNPt(BBLfuI]1k ]KŰk#DH{{C:FYK၎x޵=7>SxԠ˿8AeU(,23ގ<l<8qW酃c?3nb tC0kKHÇkiH7_ YL HN=UbP&;̓kR o`_?.uA 6MdB x4# !6dy! asMb Ģ2ⅱ! , -6 HA wÇyAF3jl$Ȓ8QIlq /cx;yR"E.sB M-S'(L؉ѪVn`S0@YU+h7#c75bݺmhю1ԹZnȻc׮z#!C ΫY+N Ãc0a}qCDfb˖WĘ>]z]C pfp@A%̹EEb^r_ߓ \P(n{'1kE}84+[73vO]q}&$;M d^(. ӀwpСT?KGaBN*Ό&PsItc,6 &8#;r "uI2IN2Θ$$ F.(K tp$;&$e $PG+39B~QBBr&Yg: "ٹ< 'ȁZZDžuŽ$!)B 栨py( S $*(r*TCbZ:2 Э~(򌤍Xy*|ҨC&H rIw^F@G-Y0H xRL5rE%I{?tC:(0H-p{?> KqFc~u28l+b.GD .'Q#i.sC@I/E4ÔsC=oRku3aU#HBOlA#w]VGP(lA 2|}W}'I<"9.h|/Utr-H( #n<3W`7?~#/<FwhX ?3zKbi!EU pD}-$.',r.` ~pI (1 x&63l.WCw! |t|T%. `0A `@A:IxA%@?[ˆA* ~R! , +6 H\Ȱ!FUСņYBWC>h ƌ_ƟHKƒ %x@٫ o2ĉs(:O=ЛP> jϋ:3+קŠ"V޴NݰV; „ʃ-ܷs׫KJo;Kwcz& AA9ʲuWf̒w!qYPP3KO>=@-Ou7v?fP݁Xy 7—v8wE.Q/<_f.~dӂI &TwB.xSH޴ `$RqEE.%h;3z@h @`CΑ>I$Y@)$3ʕWH#KG-Ո InjU% 1,~.ZK! I 4?ȥ7B M|x3ò o/kP A Vf! , *4 HkC:x !J fʘQoWT%/-<(D1+ E'@П gS?a*0bLj \B0ԠoݔNW(u 7. T z6fҤrwa0`Â.<HFVeTxDhM Me&S啨ʑ m͙Da6qk'* $F H0d"ԩbxΞ}&':Q8 E u@7(CXr8%Ġ:B٧ zw"Ԕ?IL(T-BjzbL4hlNw`BQP#~@\ Bn+CyyíwTc8AX+#9P# 8XP#i낃t`upcYl}@rI.قmW?X[Yd |cGތF7Ai-[n?\qs Z=aVWxݒ xea7\]%~llu\/U @lswuRTUoL&OX{S0L. FM72,z2'{Ŏ܈-B5G@! ,%1 HA*<ȰF#1N4B9K3M.5y vhqcbdd ېna\yUYC`Z]_}'`.XJ1/3_G8TX`a(8|Fb y54Cꕰz/ܹ!o  _/Co! , "/ HF*\(S xHHQ3j%Ol"oP&On K4hٝRP C+P_}bϳv‚vQ]P=nޯ}mt@<_p7߶q\]޼O+D87M<0mD$؍,Fh AtHttH!&H!"Ra^΅M+#&Hc)816 *B22Id6 T()eC&`PWVI&aVʓ>Qpysyǝxƙ@ r "M4Ɯ`L8020]$DiuZZC'@:j4b*{*u k"k݊kZ'? kOy>ݴI77AI@G -҂pb-1px`UmE|Od`BTE5PB5ܰx'B,]h2VgB8S,7V75G83:,? odPPh2! , %0 APO5ZȰ#Bl1ЉAmXWx, b† %klbB2i&58ŨƇt>!Cļ|憙Pe*_X̘?auiճTNE0@ 1wXPΜZ*`^mK0avʌC{ZD _|ӌXزfZV([@7JYȑsYrvAoHP\SA8TBuPI]te^ G!C=.ںN|t1DM~3O|6@DD"(P#mҠ%RRBq3 /]06-pvtF4WD@ 9)/U&4OAMxKP6 3^y;`JdR$;Yf G}o*c‰Jr)fOi;T+t&@sJhp)&@!h:Pj)^:rw bJy( qꫮN*(tbBD" 0` P D*CDODS\@xZ;RDBׁKirE4A$ܞe73p1[>-+B& RgfZ 0̵C^d$aMߵPYyY0@@<8lQj?N%hL*pV38[ g\A2UX3PM)>U%sSWbq)"{g]@! ,'2 F *\ȰFU  9_6L$ nU,)#G@93(@d`+[&(X=Hmބӝ`%FHjEZ3dS(x o(0Gƭ ױd -.7vrK0u/aMrZDkַ]ՋFB W.pѠ-5aŒ\/n@6[Duy R e6l#op}\gw`~ޏy05}c<^=tyD.4gF  w- 29m\KV)&^d&#&4ф" Hʈ>D9RA6@( /Ief*:NYp}Rf gH%NG5 袨䙧}N)a>)Es:tꩢ2B,|`駝';=Ȑ'!2 (,֊hs!A@_A8P ֡+m<[h.+a|ҁCLK(z\t @» i[B 'H.1Q¹7t0sD(~2G߸k'{4p=tpP!CtaDv2s7+ /]T[39Bls3IHtVa;QC8R \u5^0Ni7Wjwd/ȵeKZM(jH'?GQ4ēԙE=;vWU;Z- x F> vCNfϊ\[[n!;L5+D8-ڬpS C&|6  G:B ݵؼv$nc؁r(A$芬}{w4JPU& '?m%H1: Fp&H?@-N ŸDA WO};ЁC4M s.t;F޼`#k-?IN0?v6{zb#B(|H\T^v#"ؐ? PQGQIy*Xh!t`1 v( b (dā t/c =Z5:x 1_ArL $H* &Cx!\RD$d6Fj2U'\A<|Bfȃm Z)E7=iMvjơ^"Td3Ԉ=(kn!2Ԉ iQ J)± 3Ij7. .RZj<*ȸю;'搈A=Kĸ2AϚ2hXښA%+... ъ9pHǾ vR]#T,1ipZЀ BIHL3C\sF6c'mmH00sv߰Z4wUrC6$FAT=70X}f( *mG{hmK7̡MGvG0 w` 'qbBtqs'H]K9y垍 xNҳTۀky䨫vMǍ,Fwݶ_ϢQ~+h ǣ5v 7__#[bTb,r~CI0! , .7 H`F:42ȰÇ 3&B3 ey]H`#oH:ذC#Kfl$̎zwtٰa҇E1e>īQ9yBQEЇxkM7@ӆ eћY.$װfmV(xİdUs]x@_D&DP/È#pG\j F@$"Mi9gʼpjֱ7/Mȹh@Q.\|[VJ5oFkP^L&s?ϡOh5 CcN :xzIzuFi/4҈?ݸ+u zE A@Tvq'|ԑ@Ĉ QN~A0cNJm:A,"Ë2؟ @`#ċ/7R#Dc/I-roZMbg!dd2I#MK(c/pYO+Px6X(NC 0ahGճAĞd.#&`=vh$7Ł5|JG &+t'f%JX!+r0P(itcC>-rbK.iKI˜K$CD(l(+#r6.R\H*6! W Pw`ʟ݄?PƎpMd(r"u7g`8țWwNe1@;\?hԳ(KiXXK6<@R=-6PB`=zAܔa7CwsS x|v W V.]S=m9e@6A/ppwM3v#ĠJ;;yѺ;@ K^rH"g :I^w(=ATQC`O- ozoȀ>% ! , .7 HA\ȰFT簢Ł] 3KCxM;6L(FQ+AE5mnؐNd4P,66[4>eA*C.Ŋr(`tjjn|MyZ,((`|P]lk2loc֚^˰7"L?AIpIh><ȉc؉-7EInPgicd&EOpzb{w/ǙK@:*tz?cPc.F (X^.PK9Ew "~*\QT݀*6iDA!ᄹ?bU ly D(!A6E#}x @0 !Ā;'T kd TfIei/Î)#9Ui;@]WxA%ɟn&PBU@pY_BC{v;ghTxqK(PCM:OJ췟+RfvRjCF7I^tj] 7DKXk 7A(PD<;ܳz&*(V bأ-pM*r?뎴oja FG UR#äǏKVS;T2jeK6.gMJ;3%ͭi@Nl͌B}P,Fu̿@q IP#8P;P5r mPQY0XM.c6( wsu/7Bp˽/8HWT7MK85O"1 kU7M)Hx|1X}S=)-z|Ե<{5CQC>"ÓX; #! , -6 H"MPrځٜ3Z Q$ Tw.:tԶm;(; Y]M4ϓ[13*M.N.g-wbIUdkyE%FGO=^u_bIr̙McBu.܈p 3#b.ܴQhA# .U60 7w"="0,ex=f \D0/|px 3DE.=yǣp'^W&((X@( }\s>&0T .iY_b&yHdui (q+5Kj%(3G BBk*g\;y qo~XRij  a #.d92 ݇ d`'::3K( gd\- 3qL-i|AC@-4`?!$;D0Ł!D_ch75}CnJԈ%xlPf'0],R]-UpCjkP`5/ݤnw#427uCԆ?D5л8[ԽC aO.dFCl}"~Huo0H y^_P{AT (+9 7! , )4 H5BȰ!B СE53Fގ1.^1]۱cä+G:l,®+Yn@R\ʲ${168JT1膫0zbd:N&&"Rj ;T2iA Zplk°7s Ԃ4$X\׮ l0aЕ]6v `#"iB* z mvxѺ7W$ZTI8F䂍>}nO/\{ ݍ1$G ' O Q1:0Nʫx0}Wc~ZB}F"M0uIKK!u9o~8HQI6XKy"B qKPlp3Q4 T H_EKHULg"Y#x=f . ÌbgUڝ9Cg%m 8fD0.XU"P&=rצ*h= /xI_^)Uxzpk X 7,(.. ΁&n$ĀD0At@@"DI 9+AzrmA@N wٹ.b0Cgok5n~L0N b6LqQ|?xB10s ,,'1@QOi$!B6D;@O !-S#m> .R=R#Z#D%;ܐuqM",ͧ <͐-Iqz#4%59u^tF܋G^g9r]u ^":I?@ty@! , (3 H!\Ȱ`2[Ë1L̘3]&uɠIOv[cÎ1<]|ب=!Qy6)8dxЁ*7TsR0-F+vLi+ܰÂ$H UX .,N bГs _i_n2^:+K'hP"IAUlNޜI܊'+1l?)xJg0gFEWl{5o*UOu $lxŶh^zje2|&<' rz0]s?J U }Q`ZVDJ(.$!PL^ Ѝwfb%@F~ _"X.@BN -K%o'\\ e-`C</[Z{u㓘,WfXa)6v?AH.Pp曣 KLI40fb%(5(J%BdVr;L2 0d"S7g#YpA柩 .]!,M}6YI5 .W0CQ.L@-`EG $EP8 5bOvI 5#1ƶL2$.1;2ƈ }3.Ќq;@1 D3C '4@F2EXg>H==xp5 WrSIS]g3kOm3pǽW:u; H.-! , $1 H5BȐa#x[ʏ`*T1t2iGE6|XW;rnm 0ꈠBDu^ V'Lm!FDt%4H`5V^656 ګر%F<ؼ:N") Pn%lڗkM0 g(ps4MQK ,[i3YTI&.0#c&`A IfP$CIRgia@Hi! (4@*9$ L`-y`n.EAt&AG0/JvP In5E$`9t'57xX0&)~%ܐԈwU#ߜgF^! , -+ HF *\Ȱ@OI谢ENqeDClNT\areHTӑ$;v&xCAjDӄ wA|ZLJPF-T?dwժ˪%QpsaAl@zaW=AgW= )d1kͭFS<0؁$-Jچڀ-z`Tν|֨ C*W+TU4.;6I2?SF3_koX5j-jI`:(w"&W,ݬbI$ z A oMА.._B12qAArP` gp,GBAU#xJTFhecc "eŘPO.r\F# /@d IKzL @ɣHl3z2 ~QhiB@tFjh)xew5D*z11f%)2q;+z(HF"jtAq2Z+(B|ʼBU#Hx,H#d q[/1 9AT xk#<*[/ . 3 R+L E: 7/xP(-󢜆wl䍏+I5Ag@B *xj& :jr\5 ڭ'مNAh {c lInMkMpwahQ:6ȔOK[DEKUjQl0c7nt]7bF0͢O,F8WS rkzÇ0CS2u7MD< o.TDbEeZt#VD8cDiI)D4AT&!<cK-2tMrc03.A;TpD6L3-ލP@%D1AuC c2e /XS +c 1DL0AO /騣{p.b&<3g8 Eg1\ ~!"4] (R NA$}GIxJS0'| {)[1 $ BTHR-$e;,PC.F(D$-0zix po%UKKLB / /D#C-I¿$lDB¢@"L, 15Ú?F D0 ]|9¤XhtM5bW .|0  O,}zI P 5 ro׍dp"B\Lq35C{7EF7ohN'Dd͌;b%PXoJݔe}79U/Wbsf==B8I! , .) H`2(Ԉo/+o7! , -+ Hy:ȰC<Ë5L"5H`E|H*WFq\,Ydg(̙ 7FpGʖ)eg TyԥU&!gF(͹sV=l,)tYG[KTnfL$D;")tKӓ +`#g]55k ͚N iF%6\u?c&v:LTPmr<{E֜TԨZu^03LSӜY׆D-|lĂ*t<1t3t PDWpC=AՈ🀇ٳя&Dx"HR "$l K iD7Q< ۨ";4E75C+E1<9G ,lH!σ-6f`P BUk졎*`(D!- 0$P8 )lK=X@G"kp 0@tHOn;`y.qb7|N=̣;<;rC=`eƩAsA7ܰC]peFZO":5 99BLEA/W)n$6"? Tp=(xq=Dp&@-n dP0ۀf$cl+˭&K :K Qf#)S&LQYMudcJГi)1ԜT͙A \YԶAqװ0oj4xNy lD$n#s֑ 7gd^Ee'ʗ h7^0{ P"0S$ZO4E('L`#JFP@xc? UT0Xq`4-"Wt/A0äB 1D>N> !O;tQ-ܰ.Fy"$>E8#gCMY/cϊm@9hP2=C`i050` s%H'hp!7`*xR#RP %lstv!S#h({r0_Pl`/PKt1F0 12 L"xo;\<1dKB|k//cȃ`0 P (mA. i҂<  !t 0 rgt72bd5-t%dP27~  e'dbtyDlop\|.:lkӌKyw Ihwυ.5ONAk3y.InDk'1櫷7뵫88 .n'i/uZiW͜8&=+oQqʼn-. ٲ傂"y4SDO5\pvO`Wr&=>^C{Y>:eߵoHJ>ݞK9#uI:{$xϞȨ3l}dvO,T2e 1\!ƚs T@ 6xA72/u3ɥ=p %(J%z܂ 6,75ұ$P\ EPrͯYF]j5 *z霽"Aۜ÷] O,xKz)ۅ<]`j)VWWlp H /̃ %`{@<}C%Wq,` !*-'157sw>6:`ТNݹ+*=f t6(p*h-"5P#UD!Eh1?kPЗ.DpK;k܅Æ6='Lbxq S0ߍ y2$7E!G!5l-]#99B" d@7?\=iO Ple \2Ђv2>;b@| ']tq=S ec:k(C?QPx x ȣ=$г?S!h77晻ޜ'A0>9.azwBB_>@@>'λ'7H! , '2 S*\Ȱ?[6.j\;uK8 xƆzNwdx%cM" ޾ZdKP.7z;F *`U)X[ E+1*lF+W"nKO#XأAO- T`#r4]+B qȌPjAv[zH:h-XDŽqԊjȑ["<:̉\SlN)XF3F |ajՊ͇JӦe>ULٳ5? c|txt) EZ>Z`H"9," !4b p﮻ T 9P4"x +˩1Pr!0>H|3hM a`0ɦn$g-^s =̑d4S1m OgMzpQ /lWA lb(BC&PK-ʹ "ma"x&i_  ,ޢ n#hF>#{zB(OD(ҹ9G(Yd" u".D^;. #CPLϺѻ@p=.0.@x@A~O'ILpA7O|續($ ! , *4 HF*\Ȱ!Fm`„3jP#7l u>&.2Juҡ?.쨸rcwz4{:Cؓ=*ءB5WN(j e}3eFL54P*Y%OY{zVT ECDc*P k^bEf B [V`apSXs"DaΜ>3ElWjTB΋0RnV,.FhF?|3:pģ {Ebr؜!npO<7'`[,L`W"0dOGGB@\ц,(!}H0M=4v؄!xF KTP 4t3 -(1wAͻq o<NdHc=ڻtE6_<3bkt /t ~&,xU C?iù.( ljM(IlgdI(D~o@p<8# " l prG-Dj}~(K/$A~R#CH"6~E-?!HC-=!P`  @<ȐMp !@HBR†t p` ?o a^bD’! ,,5 Hp`*\Ȱ`#d$AذŁD$H`!".,60gJ"ő]b:ta2$̅3(JW*XS˟ɢcjQ\sݻQJET]hw(%.E쁼UҤi•a 2}V֧FmnDg:TMB0(FfĜ=u (ҁ3zHvUG&}2+g͔ `뺳yjHDR9K{\.Pϗμ}EXdwc9"1df1 n?hSXFw@p9=Ȅ>Wh(P#?LBrX_̘[hsE| u;PX zbC]1,rYPl`HK2t Y-5lP \TxYetQ n% P'UBg=Ll)hCĀPЉ;XwCt$+)XڥC`Pj;DFDp6 /5꫍t 5i"pk=X 6x3$AN]~:(*zq 1^r)t1ɝېץ+Onz!h 1]6TKm }]p6zD'EDƭ" j+ĹFk^(D(|AC2'dn6]APG< Xc#HCg\go1 Y@3tL}w$zI|1# CэC: BO0.~0.'A!29"72#ׂqJލ|/< 'iJAD-;zsyl\ܮu`1# p@wo1@ EAH,L  D@AfR ! ,-6 HF *\ȰF A谢Ň %@AC*T a0ȋdQaw:zra#hD@V,)4hQL@j[ 8RJҪSI„ lبPLiݻT)`}Kݝ;F36F?v,2l茕몎F2Sn\KxMn X0Od~vJl#CE Sʬ[,zM l`P5Lx7ΓAHv/W "AtWPAo 66iz3Z! a+[OAMհ7,5"$("CM mL^1@'/4 ^  9$Z  :xŹD9Ԉ?<؁ ApO 0b ~]`6擁Q !MebA$RL"yL^Y$'<+3UYQ#Q/qy"#Bl蠙&tءtI`=cAC (蔷c|7@f$~;-ǃaI~$43#kU`%il38?4V 2I@  YS3Bk ȈCD-/PTtv([K-egt I(F ᄓK.~('墈#o utxGDl. t浭H뛼ۍޚw`#'{"D(L?;"읅#K<M6<폸7G-E뻳](z_۟U(?PH`W"s[ JPh|X_7{d.# \B+d @! ,.8 H`F *\ȰFi䰢EJ`DC*()?YTرCE3A@XtGL4ONAZ1aBӓ:t1%Hс(]ʴL tNng)צ@Jm5j#O veۯru,Rz]Lg^WAQFA ᅍYZ8|}51;5'y*c6pζaew*X_I;v̮WE, @wP:Ti lH>۳/^ k.`R$PӎЍX BN^%PSXw`7cK xqKF=tq0K}Ŏ+u?4b %K7`M PDWuPQ@K/8x;lCYq/ԘA^na DVuӖ;CkERЁjg:S\$(E 6B:4X=׵<6 r;ŢD*aƒ9Г|Ev7ފFB@~7dcKrH7U0 Ky=3 H73\qtf25b :E퐍}52N oI]FЁyPW6Whcv_qw+" #Kd +icgF8&0܇&PE.ftE *h)$ВA VW>\"PXatP.]D r}WB0qt uzGM c ^Oy9*z "*$+,A1â>K +@ S#`k T!IҊXuLJw3 Db캀ػ$b&e%OA'ma+y0&,%Db+MblQ9j0 Saeo!g$8C1 y-"F B3Ձ.D5pbI Bi"4Ј8y@D( G{yaѻ,@0,, V {8! b󥯅 t5vx$B!L7dHDJ b+ [ d'! ,.7 Hy:Ȱà E!ŋ-#"A(14(PyLЪC#;T C?Z1AQ x铤*z|J( t$]ڔ`+|J&N:t(eӟ-1Ď:*Εjy]Lc{7j\1D2iÎ b@Ք9 WSPJP?+@:q7(V͉!>J}R\άWOWm{cȥ@X_p9-Qo)c5f6ZT Aqf_.^v+}x yOZ0 "1؇hA O@PYcφW7P;B]af1.s`?%:6u9RT7_ 2_'8C? Li\1"I\~գ$Sc"x&QP0n)?h8 +Xq,zdFy[< Q" "ײyPHCP#xI|JHo#@}N"~3=6~/p./n] >./Ȼ@!P<pY0#?WpxW[<6d! <&? {`FZH xeυr ϗ6"f\|#bČ (ƀ! ,+6 H\Ȱ!F bE|' Fᅊ Ta0CZ1&ʔ`:\ 8@Mdh|ɘ]%ɛ: j $1ԋlP8VґNj1iǎzwN:t j+J@ٰ!X_@!Q*-Ҍ_lXlif!ϋ"_Q-Shty%6z5ѭIO&AV ;vh Ǩѣ5oÀ'o3H0[#*l] Comλ+ŀLuA1 ҥ7f[t׼Wtgk:P%h(:Eo{z4ExE$ltMugHn[ q;ͯ:|QIwU(VAikC@pX*ye]K zLVij…y UC?p"`;b>R9 .RXZqYAxtb GUu Rb#eW1CD$\$ɜqIݗyظCGdD&,hhd=3L> O#DI R SM<*P#X]=- ;3oq"t4L'UXb ..$~TR<+ G>1iD~PRŶcJ(0k#nA0KxN ;qG=僩 JNJ2A5o֫2x@$P5n#;\s-C7Ƹ\ 5D3/lu]rOb"8 iS@.Y,w=y#{I8P~Ԓ#+7jwP^yYd 3 ŷ3o{7<#H 39z l0vLy##/޻#C㉤,1\o߾9p~z8+:A^ߎ;H,~=!D׵@@%"P@Oį0@(raZTdI@! , (3 H5BȰaAwB5)qx!U 2>Ǖ)Z̛G;v&P%HPF]1L̘]|)N&q"A~dSiҤ ;]7IMP@O*.;SfѪ &eAOԵرL~E (lٳgZV;<_7xc LϢ-+^5L`6pD0Iz"6xC(t- ZP#xҁ3+dg(V+@<۠ $J,1MlDB~JL @ibmIk#LJ`*# +/.bf0]P^+*cff@("CK.Pk1i\m-x<$]yq*0;@*/Q(\3K8sA @ dK.15ʊXNv-Y<& d@=@=8#l &7<\K+v-8"9ۄ`AߗcNxy?.z铻:y{ 훨#DhƟN:/lD|C~2=<BڋbOs"M6_dyS7\ f,U! , %1 H&C  eGh Fhq+? K=tb+T #$؏ >@I]%/8& OU OWLh'-a¡q;CcoeƊv5MMm ^7A_#mVJgs=rBP&.mXPMAUz I%TWfDbTnU@x{AP'X7XO#hU^-xP#XwDClf}p`CBh7T t?pC"]tqc'7xYP %G$ǴhPV XDLJ$!"ZK? #bx b.DB .| ~(n(pcJ&o_ Qbn0b,f+md1kj9q Oۯ$:t n,ݹh'.H-8haHI( \K9#C%(@-#E7F5W3MEj/b@.ݪ#l< rK'. /lb&K_3g#G8_I88L+:K:X;v|l/ ^y깇úI ;+&s9k.t{8HKk1'Iԭc3K(D D吩}_#5%i ! , "/5H6:詡Ç h@'c1ëE /(Q2&Btb8嫧/8pLeGU' M9u3PPi=Pncj=?u_ք2ԪjMXźNӵ{'мlݚ)ͺewA<\cקq2nܢEAR^dE⣝n?ߟ=3[n}͚@GFj>ޠ7vE짃۟Ͱ{Slɗwsq.r7]Tof?`'_#՜08 ]dŋ'gas6}h`#vՖ}Eh8V:b#*@`sHNztGj ~Q#9VH@ŗ`v5Lz(E UX"gGAd^@tzbJi o,2JIdꇣ))vZfPi2H"׈=`2 DSk"nڈ.h-{@H-H{l"z+䂨nVn-њ.˭6Djm {Ch\ `@R(w<:#@ϠY820 /<'3. #l&Go9[rYH/<ћd \<#l85MsY?3d}k'5-`/'tC[}HƮ@xL?nc_.Ho! , %0 l`*\ȰCBԠ-C*Wx"HO&tГ_(rҎIg`M;rv^ =5Z+YnذcG|I Ta@g(MBꫪLC*AD PYVRO Ts ߾P6@Jළ%kte2mrʝ  Kjٲ!y M@)"i=ryDТqTi2nȋ&򆍀`3L@ȃbΒq$ں;x)d|J=uaMcVOH 6 KK 6B\Z^hV=?֠EȘK95oU!A ! zȅ{$5%a=mHP V4bPL⥓?WבA?05Z7RYtsPG5" -zCOI>gk1A]tQyG# 0CaIȺe AKI.ӈ=?TL=W3@ʺP$AMNJZs&oH&66.$ o0ƧFŠ(B~\mɑ:;T#Zr0G3K;Ԣ# y N~D7D%)킿!עQy"Q̴A8xF \}aj -; 4o*ӵ$Q1.N@D.`KG|wVuuZMP(4@EqMfb D% PXG3^I g?db5Kܒ0zYd3Y8H+cIg YDo³ڳ!3$KfH_z;#?" /Rݐm=ͯ~_8D7@rNx4 wMc{ >NYMoBH (  „x# ça ! , *4 HF*\pa#K0`Ëv5>`ƓK<@OQ2(x@@tdž]ΌIq%Hɜ:`RhfF bZ=oC ;M6 eСXۺׄaL8%Z ejVp 0bvP%2.4._ k61;ТP%(Հwȏa"9ТuD3=:;FҠ mR0 uXYsب rCqzJ4U7_Bpa^n BApeEay |e|ƁcbuxGi uPdX=-w7Ml~y\X Xfɲ~:0p`u|Fxatꐊ2pՍ4@y:́1%VE n_X=]y#gt1_o`|X 8 ATaB2s{` y#ADLJO:Pi#ۋ5" z I$"TM,榃vN`t.&k+.0+n IGtpA A *BE#q4AEd j({pT7mLߴm>[gH@|⪱͡䌥$PG-rdmԳ2ͬ@-U@5׵ ٳ\C+uI9=Q.PpwvEݲ,=kˡ.DEk(JDݎLגD(|ԯudxg;K(d $d#;"}-~.A Ν:{;; D d D$Q[> }r[ b"<sDHApTSh bpr |ta 1pl ?\A  '6a.^:A|80EHObXOb! , +5 HP`*\Ȱ7? lH 3~qqŏ-r5dCOjJ2Myh\ara3deÆG`HeOALҴYzXږbӊޘZhY}UF٪AY X_8伅VocpLHfGXY :&Ԣww._8J̮5ul((̡hsPЕҨk f Kզz)lt&rt`KrWRîu9x:w?LRUhXE4"C18`==td:wHvbLȐ'\x}H V,BQ#tXyWa5UO*.$-.rH u"Z-,ؐ?>L&$V^/5M ۜu@Bᥓ1*Xg\E&X(7 ~3 dށyp2A(~)}c3?b~ kv `driʐ?~d49Fߠ,PD@Ǐ Y7-Bж l#@B%p8 1p &"?\"U'biBR+7E qTH2 u"$r\Bv\K;W$PFr>A#ȼ.|tSdOp m\u+Qx<7\¶B @$Hj,f{60\1t@-0yG\Zƞ@TLYԂ_q65]P/lyg38s$o˜-.~Y| E|AD|'A"M2tQ#?gyo7&>|AY8ЁpLf7Mx4[# a/ñ<`o^a:PHdCf @%]4Yx<4mH҄[_xtdž.FZC I0ݰk]"?3v[_v";fP:vmV/J٪3+n 7p`8ᘠÅABȫqURllf&Q^]>m E!83l)G%" "Q- 6u"k١B dٜn*&<ŏ/">Fmc Q@0r@́U[u Fxx%Ņe0Ʉn`FBGrws8auȒ|СrџA^1C"y G\XMFhMɖY2^֣952A``_ݨaQ.DP#`:8)BuxL:uJ.JP懶).$%Kx-z"z"k#*j_>ڪB**>Hq$5 9뷊ࢭ-ܧ_y:`AfC 0a8;," wظd5t&.g$DZKŪ}zsG4v$4.\"yplP('2M$H 5drIVAA3+bIVt愂 HL'x Ѥ&p̄vH" 9I@&1L$ ?($<(~eP*#t  tB &EH@;././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/examples/demo/Extras/images/python-logo.png0000644000175100001730000002331200000000000024430 0ustar00runnerdocker00000000000000PNG  IHDR !esBIT|dtEXtSoftwarewww.inkscape.org< IDATxy`Tw}M&3",67**lZZVk]ZZ_E@"@ld_&3}aI2IfL2?ɽs=wCq"I,D! C``D_t ttt0Icc#EQ_k.& O:uJb(! CW`V+k\5^D:uJ/}M3F0[nlnnp\SgrUI AT! B0Q1e^Ӊ8qRFj2. IOOw 5yj5Ft:|>RMM ]\\lѣGgXlg(~:BYYYN GzBniiauvv2{zzX&̴8 d29yEJk4FnySSS] ,0٣9p&-vơC$Νz< ZͬTVVE>m۶)jjjR"j8Nlw}b8xloW\O06 SS\.׈r7v͞=:5 >h-333 !\6Vuvttp^x? )^a‚ٲeK/`oYCц_Dݻ_}A[o?pDg}ॗ^ a7oqgZ)K̴-[LS\\lD^ۍ9rDg8Nػwoܹs"h;d-Dׯ_߽b }hD~r{zz. ݺukXx14]KK _|;?//1<_Rz뭷rT*r;w޾HA(^oW [N}jBRIhrq3ϴ,^FVZ[~}w F?3sfM6 =miii'!JB|3Xr'?I' \!n5k֨Cx=lkXtq4UUUz@k(J4}"nne޼y1?mBR'vJKKENO!PYYizIcU#%rC*:n[ENjd" iRT #SI\f}!i3AKr#ʇJ\%X,`83yU0ýἒHUfSQ,q/D(&HXֈ%H&5qLi ާ$\dbaRSS*Z=6J̙L&K_WW/GQW`N'l_tm,ZommpϟeWcES]]-zVXc;rcc#nkkL&7p*\WWdzZcvzbJsiٳgG5 ={dLoʕ#Bl۶m\ZMݾ}bCIOOƻ 'f˗|>oY,-&RI۱cGy-ZH;LIĝJbrܾh"\.wd2R;.d2۸qcg8h^^cɒ%GM0K.ՔRt"Fn]](ҦyEEy߾}r׋x<t֭w}rDт(cf4iJի6&7LFΝ;+XYY[~}<1D0RԡJsNhtl-))5LpÆ JVK Įx<iL d< E;{vؑpYٳgEĵvZ… c>VI.~W_}5~wH$rfy#s͚5}v@+D0_K;p8f{mz*++kPHE͛7?sFR%H8͛g*-->偘]&9jp ={ HZ  FX,/vPT*8f".{ꩧZݛc\.4 HȰ/X0V'%B w{ÏNz뭽k׮ vzZL&JVVc2AÁI$b@ "#<2r~醬q1 ^O5 dXMdAT 5;;1ZBc"h^NG$G"$, 3~\BBX,ִBg:3^0AoD"qM$ rf`Ο?'K05h1>B &Όɓ?03ϝ;L1ZhWWQ[[+ iӦ'`诿zp/(YFl2p Al`V^:u(49rf# z*R#==ݑD(L!7772+< EX2i\]+AT! B0Q1ձ"Z8)zlbFlsL*e|<g||'}6'0".GZ HjֱF%US-tC8!BCLO XZ\05ǍÙ$m+i]o_m|t{C8.h3rAHRFgqL6czS ps ~ !I!d幰l>ݸL)./)&LVR8>>v |~~ $<&nSs >ї6cIj|R \&,1|yZER$X s\5=W=W`& !HJoe^O3V/u%#I!柗/z18lTIǴd#01@53mZVͧ`0 =':D-*^FW4\׊Qhb(=ZQN`9@Pei1m8;-MLi-~b̷@f% #7B0j:,̫Rݛ l({PS"78V$YmqxG&EonpƖ VۭbI⏟0v$T0O8g$f=(gMX0VӮЎ6gpk*[>#4&%L0o$* QY'D0.RG'EwS MBZ ggz6oZ4EL88<@?!V&!9֬>a3`W;5C/- i,$D0jjFX[9i+:^4bm wlnERr'\35EWG>zs0Kr4wpFy&x5XFv>2i fZhxrn.G3䶆Lfʊ\]p;RpuM v\`^oN ߰|VOο\52@+2ʓ£ɓ"#/&`|6Vud NU3ld %2Z0t RPl=/Q{9L?O&gH)ٹRE4B+5/-$2mqܴXnQ9Y-vz_V[qL+Kg$^DwZZ  )xu_xdIH{â&{f*W}^.3Ě8Y徜6p V Ըfw,T;wXe 푀@ x5tlhd: ̏koáSbRxٽAx(z5 ܤv,JaPj{qd3G.`SnLܸ W!bM\&{ .A쳠ڒ 3@%`|"sz\SK~KN4¸'`3S& [,y`;9>c 2nH`S8n&<%߅i&$+k=@w-޹]0$ S9Sbe!ݕ4mG#dncJHfI`JTs0N@}-WʄR0NyD`eupߩ/ Z3 V9 t8aze~nC#A֯z~ +Ѡ\PfSZF$\0| ;snC'Mb)(j,,"7M}F`aã.owwrf~%dsԐŒHMOq"txm/bXkGF/3ʴ(fӪHV4tQ,[ph{`TJ ` ATu@@:{| < `& o @SQ-5 lKŐ$)| +b @(6r'C04E|YX%QUfìȞ}A%9Cym 3ryHN =7~^TG6w<C1 f7@]= @#}HkHVlu cB0QA *3`{Nة fd(lUE4Э#u-Ǝ~VZG}aymVb$ntdzSҗ!bs{} ֽr!麪i4 szX\(S8((p!k JfKٯe(d'7dMr{IgXg=jf)Js,(JEqϷ1mַ+UxSXh+iR}I-*;NoXLDa8 IkKsG]{J^uH\ pܨBAgG\Ut]&ܹ=w֯m^P6:>ipޯ^C?m,+p?ż~?hv0Op}]ʮVkY+.3Xl~7WɘC,X:H?:xZ?k%cCK.DX|o■osƄ-W/~.UH;UheΒ,ASm*p/?~OuGĉYߜnW9k-=̯j (`ɴ>gkc(`b!cnYN0:~Y,_\LvE 'EϾ,߶Dj9L:]] ٷfWk[Pst&C/_^RXXkZ4\tLbu&+ܖߥ3sb,GVi/m/;z.-=r{_T}&R;Y0ၓg/M:y‚Crۋ)R;fƿ(O{xBLJ~ŞYMi>;z?QZUYd屙6+9or8Y`iXei\[ך(2 >\ 5GZ /rX:;ا5X/m2ܴPUAWv-j`8yڲ!9,[2`eRj Wvb^{rc[fڕ"z|ۢJ߼U0Ep']QPlX+ɱ)̴i5؁|>?i| ۷Qf?!,AW\e߱s#:~ P+6jmuN5v((-8%1B}وNy%9v>xΏE I'Ձ:(XWU %&}nfohjJY*PrS&V J:X+T#W3nY4KZ?-f\n/7ZPj!0 4wqWyt*ҥ3m :-X C*rrٌe92@SzR{}r0 זi ~xNXNoY<+YM̄H3WH>w?O2feIvau( :6(rb;֛)ą6%duRV+_[cF=ro4s#K VdwO"p $04?CB󎖶>mpqn 懐1,ؘP@~-5 Y k+b 䱂I:5Fu$p ` }jÐV+6z}.|W7(ų9OMљNVeIx`U5$X0=j=u,^ھ?s+TG(~|$,Lde4a F#7UW sq7,(3Ϗ6 9%HxUeƅOM,*K~ s-=_9-3,Ai +x[K/^B͈\0M?s c]YtJF@ E&4uls:41oȋqY3]J;V-PIDATd%SdS.L94&಼Frl 4uNy!K W, oM5<3߂tIp,X/)bnZP$_lQ_ZQ0KQpDhRXl fJTr9r"8@Cg?Zq_\zW][|aKXsS'.^ߨ7s">]bވerD<t+^N6>ɇkAyMpwrtwY##/q`;VϏ+}eפFs,/Shԓ V'-ec->zlK7@"xZ^9eo~˃*BbᆅP_I(UsLLVe^qeGAX<{X2ZI raNOlIɻ\|kaJg܏vR ejX;&-'Y!75}fc+d"~bwR,v'壯OIfIblߟgBF?\]6g6_Q(֞ŋҜtO5~ڮ\C~y-J29=I!{oYb_T'~pcDѬg3XTbw(Pb>GFLS'k52/[dTw<-6T&Ea;FCb#wPqYʛpo-op^!m0åS)^ױdԖ?wJ}ktH$*p ,4g!uy_h ̛ !T=c,kLV'Ec{/u7wΧGjhE{,}p]:UxExwrKQkKxy1ЭQn!dЩ~J CjKõWiw\NO:huPJe'7[Q95'.\ڧ13RP#gE=j]cR"gEQE(ε0k.q.v*Y=zz*U-s+ɶ-,ϋh6N /v.0$gpWv|-uy|m1BV:[k  \ib׺p7wү3x,/Su<;>%:yқi.nEhVh~Y[g9()΂L4G&/}L`&{!3"D *D!fZ-vJ' C )IV 7}][ Y # C` AT! B0QA *DlG @IENDB`././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/examples/demo/Extras/index.rst0000644000175100001730000000011100000000000022027 0ustar00runnerdocker00000000000000These demos showcase some extra behaviors that are possible with TraitsUI././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1684768028.0438073 traitsui-8.0.0/examples/demo/Extras/tests/0000755000175100001730000000000000000000000021337 5ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/examples/demo/Extras/tests/__init__.py0000644000175100001730000000000000000000000023436 0ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/examples/demo/Extras/tests/test_Image_editor_demo.py0000644000175100001730000000201600000000000026343 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ This file provides tests for the demo of the same name located in the directory one level up. """ import os import runpy import unittest #: Filename of the demo script FILENAME = "Image_editor_demo.py" #: Path of the demo script DEMO_PATH = os.path.join(os.path.dirname(__file__), "..", FILENAME) class TestImageEditorDemo(unittest.TestCase): def test_image_path_exists(self): (search_path,) = runpy.run_path(DEMO_PATH)["search_path"] self.assertTrue(os.path.exists(search_path)) # Run the test(s) unittest.TextTestRunner().run( unittest.TestLoader().loadTestsFromTestCase(TestImageEditorDemo) ) ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1684768028.0438073 traitsui-8.0.0/examples/demo/Extras/windows/0000755000175100001730000000000000000000000021667 5ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/examples/demo/Extras/windows/flash.py0000644000175100001730000000321000000000000023332 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Demo showing how to use the Windows specific Flash editor. """ # Imports: from traitsui.wx.extra.windows.flash_editor import FlashEditor from traits.api import Enum, HasTraits from traitsui.api import View, HGroup, Item # The demo class: class FlashDemo(HasTraits): # The Flash file to display: flash = Enum( 'http://www.ianag.com/arcade/swf/sudoku.swf', 'http://www.ianag.com/arcade/swf/f-336.swf', 'http://www.ianag.com/arcade/swf/f-3D-Reversi-1612.swf', 'http://www.ianag.com/arcade/swf/game_234.swf', 'http://www.ianag.com/arcade/swf/flashmanwm.swf', 'http://www.ianag.com/arcade/swf/2379_gyroball.swf', 'http://www.ianag.com/arcade/swf/f-1416.swf', 'http://www.ianag.com/arcade/swf/mah_jongg.swf', 'http://www.ianag.com/arcade/swf/game_e4fe4e55fedc2f502be627ee6df716c5.swf', 'http://www.ianag.com/arcade/swf/rhumb.swf', ) # The view to display: view = View( HGroup(Item('flash', label='Pick a game to play')), '_', Item('flash', show_label=False, editor=FlashEditor()), ) # Create the demo: demo = FlashDemo() # Run the demo (if invoked from the command line): if __name__ == '__main__': demo.configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/examples/demo/Extras/windows/internet_explorer.py0000644000175100001730000000555200000000000026020 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Demo showing how to use the Windows specific Internet Explorer editor. """ # Imports: from traitsui.wx.extra.windows.ie_html_editor import IEHTMLEditor from traits.api import Str, List, Button, HasTraits from traitsui.api import View, VGroup, HGroup, Item, TextEditor, ListEditor # The web page class: class WebPage(HasTraits): # The URL to display: url = Str('http://code.enthought.com') # The page title: title = Str() # The page status: status = Str() # The browser navigation buttons: back = Button('<--') forward = Button('-->') home = Button('Home') stop = Button('Stop') refresh = Button('Refresh') search = Button('Search') # The view to display: view = View( HGroup( 'back', 'forward', 'home', 'stop', 'refresh', 'search', '_', Item('status', style='readonly'), show_labels=False, ), Item( 'url', show_label=False, editor=IEHTMLEditor( home='home', back='back', forward='forward', stop='stop', refresh='refresh', search='search', title='title', status='status', ), ), ) # The demo class: class InternetExplorerDemo(HasTraits): # A URL to display: url = Str('http://') # The list of web pages being browsed: pages = List(WebPage) # The view to display: view = View( VGroup( Item( 'url', label='Location', editor=TextEditor(auto_set=False, enter_set=True), ) ), Item( 'pages', show_label=False, style='custom', editor=ListEditor( use_notebook=True, deletable=True, dock_style='tab', export='DockWindowShell', page_name='.title', ), ), ) # Event handlers: def _url_changed(self, url): self.pages.append(WebPage(url=url.strip())) # Create the demo: demo = InternetExplorerDemo( pages=[ WebPage(url='http://code.enthought.com/projects/traits/'), WebPage(url='http://dmorrill.com'), ] ) # Run the demo (if invoked from the command line): if __name__ == '__main__': demo.configure_traits() ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1684768028.0438073 traitsui-8.0.0/examples/demo/Misc/0000755000175100001730000000000000000000000017622 5ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/examples/demo/Misc/demo_group_size.py0000644000175100001730000000574000000000000023374 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Control the height and width of a Group TraitsUI does not permit explicit specification of the height or width of a Group (or its descendants). The workaround is to put each Group whose size you wish to control into its own View class, which can be an Item (hence can be size-controlled) in the larger View. Sometimes it is necessary to repeat such surgery at several levels to get the desired layout. We separate the left and right groups by a splitter (HSplit), and also make the window itself resizable. """ from numpy import linspace, pi, sin from traits.api import Code, HasTraits, Instance, Str, Int # UItem is Unlabeled Item from traitsui.api import ( View, Item, UItem, HSplit, InstanceEditor, VGroup, HGroup, ) class InstanceUItem(UItem): """Convenience class for including an Instance in a View""" style = Str('custom') editor = Instance(InstanceEditor, ()) class CodeView(HasTraits): """Defines a sub-view whose size we wish to explicitly control.""" n = Int(123) code = Code() view = View( # This HGroup keeps 'n' from over-widening, by implicitly placing # a spring to the right of the item. HGroup(Item('n')), UItem('code'), resizable=True, ) class VerticalBar(HasTraits): """Defines a sub-view whose size we wish to explicitly control.""" a = Str('abcdefg') b = Int(123) view = View( VGroup( Item('a'), Item('b'), show_border=True, ), ) class BigView(HasTraits): """Defines the top-level view. It contains two side-by-side panels (a vertical bar and a code editor under an integer) whose relative sizes we wish to control explicitly. If these were simply defined as Groups, we could not control their sizes. But by extracting them as separate classes with their own views, the resizing becomes possible, because they are loaded as Items now. """ bar = Instance(VerticalBar, ()) code = Instance(CodeView) view = View( HSplit( # Specify pixel width of each sub-view. (Or, to specify # proportionate width, use a width < 1.0) # We specify the height here for sake of demonstration; # it could also be specified for the top-level view. InstanceUItem('bar', width=150), InstanceUItem('code', width=500, height=500), show_border=True, ), resizable=True, ) x = linspace(-2 * pi, 2 * pi, 100) pv = CodeView(code=open(__file__).read()) bv = BigView(code=pv) bv.configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/examples/demo/Misc/index.rst0000644000175100001730000000030400000000000021460 0ustar00runnerdocker00000000000000These examples demonstrate two useful features of TraitsUI - Controlling the height and width of Groups in your TraitsUI application, - Spacing widgets in your TraitsUI application using springs.././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/examples/demo/Misc/using_springs.py0000644000175100001730000000561000000000000023070 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Spacing widgets using springs *Springs* are a simple technique for adding space before, after, or between Traits UI editors (a.k.a. widgets). By default, Traits UI arranges widgets immediately adjacent to each other -- from left to right in a horizontal group, or from top to bottom in a vertical group. Sometimes this works well, but sometimes it results in widgets that are placed confusingly, or have been unattractively stretched to fill available space. When you place a *spring* in a horizontal group, Traits UI will not try to fill that space with an adjacent widget, instead allowing empty space which varies depending on the overall size of the containing group. """ from traits.api import HasTraits, Button from traitsui.api import View, VGroup, HGroup, Item, spring, Label # dummy button which will be used repeatedly to demonstrate widget spacing: button = Item('ignore', show_label=False) class SpringDemo(HasTraits): ignore = Button('Ignore') view = View( VGroup( '10', Label(label='Spring in a horizontal group moves widget right:'), '10', HGroup( button, button, show_border=True, label='Left justified (no springs)', ), HGroup( spring, button, button, show_border=True, label='Right justified with a spring ' 'before any buttons', ), HGroup( button, spring, button, show_border=True, label='Left and right justified with a ' 'spring between buttons', ), HGroup( button, button, spring, button, button, spring, button, button, show_border=True, label='Left, center and right justified ' 'with springs after the 2nd and 4th ' 'buttons', ), spring, Label( 'Spring in vertical group moves widget down ' '(does not work on Wx backend).' ), button, ), width=600, height=600, resizable=True, title='Spring Demo', buttons=['OK'], ) demo = SpringDemo() if __name__ == '__main__': demo.configure_traits() ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1684768028.0478072 traitsui-8.0.0/examples/demo/Standard_Editors/0000755000175100001730000000000000000000000022160 5ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/examples/demo/Standard_Editors/ArrayEditor_demo.py0000644000175100001730000000224200000000000025763 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """Demo of the ArrayEditor""" import numpy as np from traits.api import Array, HasPrivateTraits from traitsui.api import ArrayEditor, Item, View from traitsui.menu import NoButtons class ArrayEditorTest(HasPrivateTraits): three = Array(np.int64, (3, 3)) four = Array( np.float64, (4, 4), editor=ArrayEditor(width=-50), ) view = View( Item('three', label='3x3 Integer'), '_', Item('three', label='Integer Read-only', style='readonly'), '_', Item('four', label='4x4 Float'), '_', Item('four', label='Float Read-only', style='readonly'), buttons=NoButtons, resizable=True, ) demo = ArrayEditorTest() if __name__ == '__main__': demo.configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/examples/demo/Standard_Editors/BooleanEditor_demo.py0000644000175100001730000000372600000000000026274 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Implementation of a BooleanEditor demo plugin for Traits UI demo program. This demo shows each of the four styles of the BooleanEditor. Please refer to the `BooleanEditor API docs`_ for further information. .. _BooleanEditor API docs: https://docs.enthought.com/traitsui/api/traitsui.editors.boolean_editor.html#traitsui.editors.boolean_editor.BooleanEditor """ from traits.api import HasTraits, Bool from traitsui.api import Item, Group, View # ------------------------------------------------------------------------- # Demo Class # ------------------------------------------------------------------------- class BooleanEditorDemo(HasTraits): """This class specifies the details of the BooleanEditor demo.""" # To demonstrate any given Trait editor, an appropriate Trait is required. boolean_trait = Bool() # Items are used to define the demo display - one Item per editor style bool_group = Group( Item('boolean_trait', style='simple', label='Simple', id='simple'), Item('_'), Item('boolean_trait', style='custom', label='Custom', id='custom'), Item('_'), Item('boolean_trait', style='text', label='Text', id='text'), Item('_'), Item( 'boolean_trait', style='readonly', label='ReadOnly', id='readonly' ), ) # Demo view traits_view = View( bool_group, title='BooleanEditor', buttons=['OK'], width=300 ) # Hook for 'demo.py' demo = BooleanEditorDemo() # Run the demo (if invoked from the command line): if __name__ == '__main__': demo.configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/examples/demo/Standard_Editors/BooleanEditor_simple_demo.py0000644000175100001730000000511700000000000027641 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Boolean editor (checkbox or text) A Boolean (True/False) trait is displayed and edited as a checkbox, by default. It can also be displayed as text 'True'/'False', either editable or read-only. This example also shows how to listen for a change in a trait, and take action when its value changes. It also demonstrates how to add vertical space and a Label (plain text which is not editable.) Please refer to the `BooleanEditor API docs`_ for further information. .. _BooleanEditor API docs: https://docs.enthought.com/traitsui/api/traitsui.editors.boolean_editor.html#traitsui.editors.boolean_editor.BooleanEditor """ from traits.api import HasTraits, Bool, Int from traitsui.api import Item, Label, Group, View class BooleanEditorDemo(HasTraits): """Defines the main BooleanEditor demo class.""" # a boolean trait to view: my_boolean_trait = Bool() count_changes = Int(0) # When the trait's value changes, do something. # The listener method is named '_TraitName_changed', where # 'TraitName' is the name of the trait being monitored. def _my_boolean_trait_changed(self): self.count_changes += 1 # Demo view traits_view = View( '10', # vertical space Item('my_boolean_trait', style='simple', id='simple'), '10', # vertical space # We put this label in its own group so that it will be left justified. # Otherwise it will line up with other edit fields (indented): Group( Label( 'The same Boolean trait can also be displayed and edited as ' 'text (True/False):' ) ), '10', # vertical space Item( 'my_boolean_trait', style='readonly', label='Read-only style', id='readonly', ), Item('my_boolean_trait', style='text', label='Text style', id='text'), '10', 'count_changes', title='Boolean trait', buttons=['OK'], resizable=True, ) # Create the demo view (but do not yet display it): demo = BooleanEditorDemo() # Display and edit the demo (if invoked from the command line): if __name__ == '__main__': demo.configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/examples/demo/Standard_Editors/ButtonEditor_demo.py0000644000175100001730000000407500000000000026166 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Implementation of a ButtonEditor demo plugin for Traits UI demo program. This demo shows each of the two styles of the ButtonEditor. (As of this writing, they are identical.) Please refer to the `ButtonEditor API docs`_ for further information. .. _ButtonEditor API docs: https://docs.enthought.com/traitsui/api/traitsui.editors.button_editor.html#traitsui.editors.button_editor.ButtonEditor """ from traits.api import HasTraits, Button, observe from traitsui.api import Item, View, Group, message # ------------------------------------------------------------------------- # Demo Class # ------------------------------------------------------------------------- class ButtonEditorDemo(HasTraits): """This class specifies the details of the ButtonEditor demo.""" # To demonstrate any given Trait editor, an appropriate Trait is required. fire_event = Button('Click Me') @observe('fire_event') def _button_clicked_message(self, event): message("Button clicked!") # ButtonEditor display # (Note that Text and ReadOnly versions are not applicable) event_group = Group( Item('fire_event', style='simple', label='Simple', id="simple"), Item('_'), Item('fire_event', style='custom', label='Custom', id="custom"), Item('_'), Item(label='[text style unavailable]'), Item('_'), Item(label='[readonly style unavailable]'), ) # Demo view traits_view = View( event_group, title='ButtonEditor', buttons=['OK'], width=250 ) # Create the demo: demo = ButtonEditorDemo() # Run the demo (if invoked from the command line): if __name__ == '__main__': demo.configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/examples/demo/Standard_Editors/ButtonEditor_simple_demo.py0000644000175100001730000000317000000000000027532 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Button editor A Button trait is displayed as a button in a Traits UI view. When the button is clicked, Traits UI will execute a method of your choice (a 'listener'). In this example, the listener just increments a click counter. Please refer to the `ButtonEditor API docs`_ for further information. .. _ButtonEditor API docs: https://docs.enthought.com/traitsui/api/traitsui.editors.button_editor.html#traitsui.editors.button_editor.ButtonEditor """ from traits.api import HasTraits, Button, Int, observe from traitsui.api import Item, View class ButtonEditorDemo(HasTraits): """Defines the main ButtonEditor demo class.""" # Define a Button trait: my_button_trait = Button('Click Me') click_counter = Int(0) # When the button is clicked, do something. @observe("my_button_trait") def _increment_counter(self, event): self.click_counter += 1 # Demo view: traits_view = View( 'my_button_trait', Item('click_counter', style='readonly'), title='ButtonEditor', buttons=['OK'], resizable=True, ) # Create the demo: demo = ButtonEditorDemo() # Run the demo (if invoked from the command line): if __name__ == '__main__': demo.configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/examples/demo/Standard_Editors/CSVListEditor_demo.py0000644000175100001730000001520300000000000026175 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Demonstrate the CSVListEditor class. This editor allows the user to enter a *single* line of input text, containing comma-separated values (or another separator may be specified). Your program specifies an element Trait type of Int, Float, Str, Enum, or Range. Please refer to the `CSVListEditor API docs`_ for further information. .. _CSVListEditor API docs: https://docs.enthought.com/traitsui/api/traitsui.editors.csv_list_editor.html#traitsui.editors.csv_list_editor.CSVListEditor """ from traits.api import ( HasTraits, List, Int, Float, Enum, Range, Str, Button, Property, observe, ) from traitsui.api import ( View, Item, Label, Heading, VGroup, HGroup, UItem, spring, TextEditor, CSVListEditor, ) class CSVListEditorDemo(HasTraits): list1 = List(Int) list2 = List(Float) list3 = List(Str, maxlen=3) list4 = List(Enum('red', 'green', 'blue', 2, 3)) list5 = List(Range(low=0.0, high=10.0)) # 'low' and 'high' are used to demonstrate lists containing dynamic ranges. low = Float(0.0) high = Float(1.0) list6 = List(Range(low=-1.0, high='high')) list7 = List(Range(low='low', high='high')) pop1 = Button("Pop from first list") sort1 = Button("Sort first list") # This will be str(self.list1). list1str = Property(Str, observe='list1') traits_view = View( HGroup( # This VGroup forms the column of CSVListEditor examples. VGroup( Item( 'list1', label="List(Int)", editor=CSVListEditor(ignore_trailing_sep=False), tooltip='options: ignore_trailing_sep=False', ), Item( 'list1', label="List(Int)", style='readonly', editor=CSVListEditor(), ), Item( 'list2', label="List(Float)", editor=CSVListEditor(enter_set=True, auto_set=False), tooltip='options: enter_set=True, auto_set=False', ), Item( 'list3', label="List(Str, maxlen=3)", editor=CSVListEditor(), ), Item( 'list4', label="List(Enum('red', 'green', 'blue', 2, 3))", editor=CSVListEditor(sep=None), tooltip='options: sep=None', ), Item( 'list5', label="List(Range(low=0.0, high=10.0))", editor=CSVListEditor(), ), Item( 'list6', label="List(Range(low=-1.0, high='high'))", editor=CSVListEditor(), ), Item( 'list7', label="List(Range(low='low', high='high'))", editor=CSVListEditor(), ), springy=True, ), # This VGroup forms the right column; it will display the # Python str representation of the lists. VGroup( UItem( 'list1str', editor=TextEditor(), enabled_when='False', width=240, ), UItem( 'list1str', editor=TextEditor(), enabled_when='False', width=240, ), UItem( 'list2', editor=TextEditor(), enabled_when='False', width=240, ), UItem( 'list3', editor=TextEditor(), enabled_when='False', width=240, ), UItem( 'list4', editor=TextEditor(), enabled_when='False', width=240, ), UItem( 'list5', editor=TextEditor(), enabled_when='False', width=240, ), UItem( 'list6', editor=TextEditor(), enabled_when='False', width=240, ), UItem( 'list7', editor=TextEditor(), enabled_when='False', width=240, ), ), ), '_', HGroup('low', 'high', spring, UItem('pop1'), UItem('sort1')), Heading("Notes"), Label( "Hover over a list to see which editor options are set, " "if any." ), Label( "The editor of the first list, List(Int), uses " "ignore_trailing_sep=False, so a trailing comma is " "an error." ), Label("The second list is a read-only view of the first list."), Label( "The editor of the List(Float) example has enter_set=True " "and auto_set=False; press Enter to validate." ), Label("The List(Str) example will accept at most 3 elements."), Label( "The editor of the List(Enum(...)) example uses sep=None, " "i.e. whitespace acts as a separator." ), Label( "The last three List(Range(...)) examples take neither, one or " "both of their limits from the Low and High fields below." ), width=720, title="CSVListEditor Demonstration", ) def _list1_default(self): return [1, 4, 0, 10] def _get_list1str(self): return str(self.list1) @observe("pop1") def _pop_from_list1(self, event): if len(self.list1) > 0: x = self.list1.pop() print(x) @observe('sort1') def _sort_list1(self, event): self.list1.sort() if __name__ == "__main__": demo = CSVListEditorDemo() demo.configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/examples/demo/Standard_Editors/CheckListEditor_demo.py0000644000175100001730000000612500000000000026562 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Implementation of a CheckListEditor demo plugin for the Traits UI demo program. For each of three CheckListEditor column formations, this demo shows each of the four styles of the CheckListEditor. Please refer to the `CheckListEditor API docs`_ for further information. .. _CheckListEditor API docs: https://docs.enthought.com/traitsui/api/traitsui.editors.check_list_editor.html#traitsui.editors.check_list_editor.CheckListEditor """ from traits.api import HasTraits, List from traitsui.api import Item, Group, View, CheckListEditor # Define the demo class: class CheckListEditorDemo(HasTraits): """Define the main CheckListEditor demo class.""" # Define a trait for each of three formations: checklist_4col = List( editor=CheckListEditor(values=['one', 'two', 'three', 'four'], cols=4) ) checklist_2col = List( editor=CheckListEditor(values=['one', 'two', 'three', 'four'], cols=2) ) checklist_1col = List( editor=CheckListEditor(values=['one', 'two', 'three', 'four'], cols=1) ) # CheckListEditor display with four columns: cl_4_group = Group( Item('checklist_4col', style='simple', label='Simple'), Item('_'), Item('checklist_4col', style='custom', label='Custom'), Item('_'), Item('checklist_4col', style='text', label='Text'), Item('_'), Item('checklist_4col', style='readonly', label='ReadOnly'), label='4-column', ) # CheckListEditor display with two columns: cl_2_group = Group( Item('checklist_2col', style='simple', label='Simple'), Item('_'), Item('checklist_2col', style='custom', label='Custom'), Item('_'), Item('checklist_2col', style='text', label='Text'), Item('_'), Item('checklist_2col', style='readonly', label='ReadOnly'), label='2-column', ) # CheckListEditor display with one column: cl_1_group = Group( Item('checklist_1col', style='simple', label='Simple'), Item('_'), Item('checklist_1col', style='custom', label='Custom'), Item('_'), Item('checklist_1col', style='text', label='Text'), Item('_'), Item('checklist_1col', style='readonly', label='ReadOnly'), label='1-column', ) # The view includes one group per column formation. These will be # displayed on separate tabbed panels. traits_view = View( cl_4_group, cl_2_group, cl_1_group, title='CheckListEditor', buttons=['OK'], resizable=True, ) # Create the demo: demo = CheckListEditorDemo() # Run the demo (if invoked from the command line): if __name__ == '__main__': demo.configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/examples/demo/Standard_Editors/CheckListEditor_simple_demo.py0000644000175100001730000000516700000000000030140 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Checklist editor for a List of strings The checklist editor provides a simple way for the user to select multiple items from a list of known strings. This example demonstrates the checklist editor's two most useful styles: * 'custom' displays all the strings in columns next to checkboxes. * 'readonly' displays only the selected strings, as a Python list of strings. We do *not* demonstrate two styles which are not as useful for this editor: * 'text' is like 'readonly' except editable. It will accept a list of strings or numbers or even expressions. This is useful for quick, non-production data entry, but it ignores the editor's list of valid 'values'. * 'simple' (the default) only lets you select one item at a time, from a drop-down widget. Please refer to the `CheckListEditor API docs`_ for further information. .. _CheckListEditor API docs: https://docs.enthought.com/traitsui/api/traitsui.editors.check_list_editor.html#traitsui.editors.check_list_editor.CheckListEditor """ from traits.api import HasTraits, List from traitsui.api import UItem, Group, View, CheckListEditor, Label class CheckListEditorDemo(HasTraits): """Define the main CheckListEditor simple demo class.""" # Specify the strings to be displayed in the checklist: checklist = List( editor=CheckListEditor( values=['one', 'two', 'three', 'four', 'five', 'six'], cols=2 ) ) # CheckListEditor display with two columns: checklist_group = Group( '10', # insert vertical space (10 empty pixels) Label('The custom style lets you select items from a checklist:'), UItem('checklist', style='custom', id="custom"), '10', '_', '10', # horizontal line with vertical space above and below Label( 'The readonly style shows you which items are selected, ' 'as a Python list:' ), UItem('checklist', style='readonly', id="readonly"), ) traits_view = View( checklist_group, title='CheckListEditor', buttons=['OK'], resizable=True, ) # Create the demo: demo = CheckListEditorDemo() # Run the demo (if invoked from the command line): if __name__ == '__main__': demo.configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/examples/demo/Standard_Editors/CodeEditor_demo.py0000644000175100001730000000321700000000000025562 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Implementation of a CodeEditor demo plugin for Traits UI demo program. This demo shows each of the four styles of the CodeEditor. Please refer to the `CodeEditor API docs`_ for further information. .. _CodeEditor API docs: https://docs.enthought.com/traitsui/api/traitsui.editors.code_editor.html#traitsui.editors.code_editor.CodeEditor """ from traits.api import HasTraits, Code from traitsui.api import Item, Group, View # The main demo class: class CodeEditorDemo(HasTraits): """Defines the CodeEditor demo class.""" # Define a trait to view: code_sample = Code('import sys\n\nsys.print("hello world!")') # Display specification: code_group = Group( Item('code_sample', style='simple', label='Simple'), Item('_'), Item('code_sample', style='custom', label='Custom'), Item('_'), Item('code_sample', style='text', label='Text'), Item('_'), Item('code_sample', style='readonly', label='ReadOnly'), ) # Demo view: traits_view = View( code_group, title='CodeEditor', width=600, height=600, buttons=['OK'] ) # Create the demo: demo = CodeEditorDemo() # Run the demo (if invoked from the command line): if __name__ == "__main__": demo.configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/examples/demo/Standard_Editors/ColorEditor_demo.py0000644000175100001730000000363200000000000025767 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ **WARNING** This demo might not work as expected and some documented features might be missing. ------------------------------------------------------------------------------- Implementation of a ColorEditor demo plugin for Traits UI demo program. This demo shows each of the four styles of the ColorEditor. Please refer to the `ColorEditor API docs`_ for further information. .. _ColorEditor API docs: https://docs.enthought.com/traitsui/api/traitsui.editors.color_editor.html#traitsui.editors.color_editor.ColorEditor """ # Issues related to the demo warning: # enthought/traitsui#946 from traits.api import HasTraits from traitsui.api import Item, Group, View, Color # Demo class definition: class ColorEditorDemo(HasTraits): """Defines the main ColorEditor demo.""" # Define a Color trait to view: color_trait = Color() # Items are used to define the demo display, one item per editor style: color_group = Group( Item('color_trait', style='simple', label='Simple'), Item('_'), Item('color_trait', style='custom', label='Custom'), Item('_'), Item('color_trait', style='text', label='Text'), Item('_'), Item('color_trait', style='readonly', label='ReadOnly'), ) # Demo view traits_view = View( color_group, title='ColorEditor', buttons=['OK'], resizable=True ) # Create the demo: demo = ColorEditorDemo() # Run the demo (if invoked from the command line): if __name__ == '__main__': demo.configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/examples/demo/Standard_Editors/CompoundEditor_demo.py0000644000175100001730000000402700000000000026474 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ **WARNING** This demo might not work as expected and some documented features might be missing. ------------------------------------------------------------------------------- Implementation of a CompoundEditor demo plugin for Traits UI demo program. This demo shows each of the four styles of the CompoundEditor. Please refer to the `CompoundEditor API docs`_ for further information. .. _CompoundEditor API docs: https://docs.enthought.com/traitsui/api/traitsui.editors.compound_editor.html#traitsui.editors.compound_editor.CompoundEditor """ # Issue related to the demo warning: enthought/traitsui#945 from traits.api import Enum, HasTraits, Range, Union from traitsui.api import Item, Group, View # Define the demo class: class CompoundEditorDemo(HasTraits): """Defines the main CompoundEditor demo class.""" # Define a compound trait to view: compound_trait = Union( Range(1, 6), Enum('a', 'b', 'c', 'd', 'e', 'f'), default_value=1 ) # Display specification (one Item per editor style): comp_group = Group( Item('compound_trait', style='simple', label='Simple'), Item('_'), Item('compound_trait', style='custom', label='Custom'), Item('_'), Item('compound_trait', style='text', label='Text'), Item('_'), Item('compound_trait', style='readonly', label='ReadOnly'), ) # Demo view: traits_view = View( comp_group, title='CompoundEditor', buttons=['OK'], resizable=True ) # Create the demo: demo = CompoundEditorDemo() # Run the demo (if invoked from the command line): if __name__ == '__main__': demo.configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/examples/demo/Standard_Editors/DataFrameEditor_demo.py0000644000175100001730000000600300000000000026530 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # DataFrameEditor_demo.py -- Example of using dataframe editors # Dataset from https://www.kaggle.com/mokosan/lord-of-the-rings-character-data """ **WARNING** This demo might not work as expected and some documented features might be missing. ------------------------------------------------------------------------------- To run this demonstration successfully, you must have **pandas** installed. Please refer to the `DataFrameEditor API docs`_ for further information. .. _DataFrameEditor API docs: https://docs.enthought.com/traitsui/api/traitsui.ui_editors.data_frame_editor.html#traitsui.ui_editors.data_frame_editor.DataFrameEditor """ # Issue related to the demo warning: enthought/traitsui#944 import numpy as np from pandas import DataFrame from traits.api import HasTraits, Instance from traitsui.api import View, Item from traitsui.ui_editors.data_frame_editor import DataFrameEditor class DataFrameEditorDemo(HasTraits): df = Instance(DataFrame) traits_view = View( Item( 'df', show_label=False, editor=DataFrameEditor( formats={ 'RuntimeInMinutes': '%.4d', 'BudgetInMillions': '%d', 'BoxOfficeRevenueInMillions': '%d', 'AcademyAwardNominations': '%d', 'AcademyAwardWins': '%d', 'RottenTomatoesScore': '%.2f', } ), ), title="DataFrameEditor", resizable=True, id='traitsui.demo.Applications.data_frame_editor_demo', ) # Sample Data lotrMovieData = np.array( [ [558, 281, 2917.0, 30, 17, 94.0], [178, 93, 871.5, 13, 4, 91.0], [179, 94, 926.0, 6, 2, 96.0], [201, 94, 1120, 11, 11, 95.0], [462.0, 675, 2932.0, 7, 1, 66.33333333], [169, 200, 1021.0, 3, 1, 64.0], [161, 217, 958.4, 3, 0, 75.0], [144, 250, 956.0, 1, 0, 60.0], ] ) col_names = [ 'RuntimeInMinutes', 'BudgetInMillions', 'BoxOfficeRevenueInMillions', 'AcademyAwardNominations', 'AcademyAwardWins', 'RottenTomatoesScore', ] index_names = [ 'The Lord of the Rings Series', 'The Fellowship of the Ring', 'The Two Towers ', 'The Return of the King', 'The Hobbit Series', 'The Unexpected Journey', 'The Desolation of Smaug', 'The Battle of the Five Armies', ] # Create & run the demo df = DataFrame(data=lotrMovieData, columns=col_names, index=index_names) demo = DataFrameEditorDemo(df=df) # Run the demo (if invoked from the command line): if __name__ == '__main__': demo.configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/examples/demo/Standard_Editors/DatetimeEditor_demo.py0000644000175100001730000000377500000000000026455 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ **WARNING** This demo might not work as expected and some documented features might be missing. ------------------------------------------------------------------------------- A Traits UI editor that edits a datetime panel. Please refer to the `DatetimeEditor API docs`_ for further information. .. _DatetimeEditor API docs: https://docs.enthought.com/traitsui/api/traitsui.editors.datetime_editor.html#traitsui.editors.datetime_editor.DatetimeEditor """ # Issue related to the demo warning: enthought/traitsui#943 import datetime from traits.api import HasTraits, Datetime, Str from traitsui.api import View, Item, Group class DateEditorDemo(HasTraits): """Demo class to show Datetime editors.""" datetime = Datetime(allow_none=True) info_string = Str('The editors for Traits Datetime objects.') traits_view = View( Item( 'info_string', show_label=False, style='readonly', ), Group( Item( 'datetime', label='Simple date editor', ), Item( 'datetime', style='readonly', label='ReadOnly editor', ), label='Default settings for editors', ), resizable=True, ) def _datetime_changed(self): """Print each time the date value is changed in the editor.""" print(self.datetime) # -- Set Up The Demo ------------------------------------------------------ demo = DateEditorDemo(datetime=datetime.datetime.now()) if __name__ == "__main__": demo.configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/examples/demo/Standard_Editors/DirectoryEditor_demo.py0000644000175100001730000000375700000000000026665 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ **WARNING** This demo might not work as expected and some documented features might be missing. ------------------------------------------------------------------------------- Implementation of a DirectoryEditor demo plugin for Traits UI demo program. This demo shows each of the four styles of the DirectoryEditor. Please refer to the `DirectoryEditor API docs`_ for further information. .. _DirectoryEditor API docs: https://docs.enthought.com/traitsui/api/traitsui.editors.directory_editor.html#traitsui.editors.directory_editor.DirectoryEditor """ # Issue related to the demo warning: enthought/traitsui#889 from traits.api import HasTraits, Directory from traitsui.api import Item, Group, View # Define the demo class: class DirectoryEditorDemo(HasTraits): """Define the main DirectoryEditor demo class.""" # Define a Directory trait to view: dir_name = Directory() # Display specification (one Item per editor style): dir_group = Group( Item('dir_name', style='simple', label='Simple'), Item('_'), Item('dir_name', style='custom', label='Custom'), Item('_'), Item('dir_name', style='text', label='Text'), Item('_'), Item('dir_name', style='readonly', label='ReadOnly'), ) # Demo view: traits_view = View( dir_group, title='DirectoryEditor', width=400, height=600, buttons=['OK'], resizable=True, ) # Create the demo: demo = DirectoryEditorDemo() # Run the demo (if invoked from the command line): if __name__ == '__main__': demo.configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/examples/demo/Standard_Editors/EnumEditor_demo.py0000644000175100001730000000627300000000000025621 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Enum editor The Enum editor provides a simple way for the user to choose one item from a list of known values (normally strings or numbers). An Enum trait can take any value from a specified list of values. These values are typically strings, integers, or floats, but can also be None or hashable tuples. An Enum can be displayed / edited in one of five styles: * 'simple' displays a drop-down list of allowed values * 'custom' by default, displays one or more columns of radio buttons (only one of which is selected at a time). * 'custom' in 'list' mode (see source code below), displays a list of all the allowed values at once. * 'readonly' displays the current value as non-editable text. * 'text' displays the current value as text. You can also edit this text, but your text must be in the list of allowed values. Please refer to the `EnumEditor API docs`_ for further information. .. _EnumEditor API docs: https://docs.enthought.com/traitsui/api/traitsui.editors.enum_editor.html#traitsui.editors.enum_editor.EnumEditor """ from traits.api import HasTraits, Enum from traitsui.api import Item, Group, View, EnumEditor class EnumEditorDemo(HasTraits): """Defines the main EnumEditor demo class.""" # Define an Enum trait to view. name_list = Enum( 'A-495', 'A-498', 'R-1226', 'TS-17', 'TS-18', 'Foo', 12345, (11, 7), None, ) # Items are used to define the display, one Item per editor style: enum_group = Group( Item('name_list', style='simple', label='Simple', id="simple"), Item('_'), # The simple style also supports text entry: Item( 'name_list', style='simple', label='Simple (text entry)', editor=EnumEditor( values=name_list, completion_mode='popup', evaluate=True ), id="simple_text", ), Item('_'), # The custom style defaults to radio button mode: Item('name_list', style='custom', label='Custom radio', id="radio"), Item('_'), # The custom style can also display in list mode, with extra work: Item( 'name_list', style='custom', label='Custom list', editor=EnumEditor(values=name_list, mode='list'), id="list", ), Item('_'), Item('name_list', style='text', label='Text', id="text"), Item('_'), Item('name_list', style='readonly', label='ReadOnly', id="readonly"), ) # Demo view: traits_view = View( enum_group, title='EnumEditor', buttons=['OK'], resizable=True ) # Create the demo: demo = EnumEditorDemo() # Run the demo (if invoked from the command line): if __name__ == '__main__': demo.configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/examples/demo/Standard_Editors/FileEditor_demo.py0000644000175100001730000000370300000000000025567 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ **WARNING** This demo might not work as expected and some documented features might be missing. ------------------------------------------------------------------------------- Implementation of a FileEditor demo plugin for Traits UI demo program. This demo shows each of the four styles of the FileEditor. Please refer to the `FileEditor API docs`_ for further information. .. _FileEditor API docs: https://docs.enthought.com/traitsui/api/traitsui.editors.file_editor.html#traitsui.editors.file_editor.FileEditor """ # Issue related to the demo warning: enthought/traitsui#889 from traits.api import HasTraits, File from traitsui.api import Item, Group, View # Define the demo class: class FileEditorDemo(HasTraits): """Defines the main FileEditor demo class.""" # Define a File trait to view: file_name = File() # Display specification (one Item per editor style): file_group = Group( Item('file_name', style='simple', label='Simple', id='simple_file'), Item('_'), Item('file_name', style='custom', label='Custom'), Item('_'), Item('file_name', style='text', label='Text'), Item('_'), Item('file_name', style='readonly', label='ReadOnly'), ) # Demo view: traits_view = View( file_group, title='FileEditor', width=400, height=600, buttons=['OK'], resizable=True, ) # Create the demo: demo = FileEditorDemo() # Run the demo (if invoked from the command line): if __name__ == '__main__': demo.configure_traits() ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1684768028.0478072 traitsui-8.0.0/examples/demo/Standard_Editors/File_Dialog/0000755000175100001730000000000000000000000024316 5ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/examples/demo/Standard_Editors/File_Dialog/File_Open.py0000644000175100001730000001126400000000000026534 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ **WARNING** This demo might not work as expected and some documented features might be missing. ------------------------------------------------------------------------------- This demonstrates one of the simplest cases of using the TraitsUI file dialog to select a file for opening (i.e. reading or editing). The first question of course is why use the TraitsUI file dialog at all, when the standard OS file dialog is also available? And the answer is that you can use either, but the advantages of using the TraitsUI file dialog are: - It supports history. That is, each time the user selects a file for opening, the file is added to a persistent history list, similar to many applications *Open recent...* function, but built directly into the file dialog. The amount of history remembered can be specified by the developer, with the default being the last 10 files opened. - It is resizable. Some standard OS file dialogs are not resizable, which can be very annoying to the user trying to select a file through a tiny peephole view of the file system. In addition, if the user resizes the dialog, the new size and position will be persisted, so that the file dialog will appear in the same location the next time the user wants to open a file. - There is a very nice synergy between the file system view and the history list. Quite often users shuttle between several *favorite* locations in the file system when opening files. The TraitsUI file dialog automatically discovers these favorite locations just by the user opening files. When a user opens the file dialog, they can select a previously opened file from the history list, which then automatically causes the file system view to expand the selected file's containing folder, thus allowing them to select a different file in the same location. Since the history list is updated each time a user selects a file, It tends to automatically discover a *working set* of favorite directories just through simple use, without the user having to explicitly designate them as such. - It's customizable. The TraitsUI file dialog accepts extension objects which can be used to display additional file information or even modify the selection behavior of the dialog. Several extensions are provided with TraitsUI (and are demonstrated in some of the other examples), and you are free to write your own by implementing a very simple interface. - The history and user settings are customizable per application. Just by setting a unique id in the file dialog request, you can specify that the history and window size and position information are specific to your application. If you have file dialog extensions added, the user can reorder, resize and reconfigure the overall file dialog layout, including your extensions, and have their custom settings restored each time they use the file dialog. If you do not specify a unique id, then the history and user settings default to the system-wide settings for the file dialog. It's your choice. - It's easy to use. That's what this particular example is all about. So take a look at the source code for this example to see how easy it is... """ # Issue related to the demo warning: enthought/traitsui#953 from traits.api import HasTraits, File, Button from traitsui.api import View, HGroup, Item from traitsui.file_dialog import open_file # -- FileDialogDemo Class ------------------------------------------------- class FileDialogDemo(HasTraits): # The name of the selected file: file_name = File() # The button used to display the file dialog: open = Button('Open...') # -- Traits View Definitions ---------------------------------------------- traits_view = View( HGroup( Item('open', show_label=False), '_', Item('file_name', style='readonly', springy=True), ), width=0.5, ) # -- Traits Event Handlers ------------------------------------------------ def _open_changed(self): """Handles the user clicking the 'Open...' button.""" file_name = open_file() if file_name != '': self.file_name = file_name # Create the demo: demo = FileDialogDemo() # Run the demo (if invoked from the command line): if __name__ == '__main__': demo.configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/examples/demo/Standard_Editors/File_Dialog/File_Open_with_Custom_Extension.py0000644000175100001730000000664600000000000033165 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ **WARNING** This demo might not work as expected and some documented features might be missing. ------------------------------------------------------------------------------- This demonstrates using the TraitsUI file dialog with a custom written file dialog extension, in this case an extension called **LineCountInfo**, which displays the number of text lines in the currently selected file. For more information about why you would want to use the TraitsUI file dialog over the standard OS file dialog, select the **File Open** demo. """ # Issue related to the demo warning: enthought/traitsui#953 from os.path import getsize from traits.api import HasTraits, File, Button, Property, cached_property from traitsui.api import View, VGroup, HGroup, Item from traitsui.file_dialog import open_file, MFileDialogModel # -- LineCountInfo Class -------------------------------------------------- class LineCountInfo(MFileDialogModel): """Defines a file dialog extension that displays the number of text lines in the currently selected file. """ # The number of text lines in the currently selected file: lines = Property(observe='file_name') # -- Traits View Definitions ---------------------------------------------- traits_view = View( VGroup( Item('lines', style='readonly'), label='Line Count Info', show_border=True, ) ) # -- Property Implementations --------------------------------------------- @cached_property def _get_lines(self): try: if getsize(self.file_name) > 10000000: return 'File too big...' with open(self.file_name, 'r', encoding='utf8') as fh: data = fh.read() except OSError: return '' if (data.find('\x00') >= 0) or (data.find('\xFF') >= 0): return 'File contains binary data...' return '{:n} lines'.format(len(data.splitlines())) # -- FileDialogDemo Class ------------------------------------------------- # Demo specific file dialig id: demo_id = 'traitsui.demo.standard_editors.file_dialog.line_count_info' class FileDialogDemo(HasTraits): # The name of the selected file: file_name = File() # The button used to display the file dialog: open = Button('Open...') # -- Traits View Definitions ---------------------------------------------- traits_view = View( HGroup( Item('open', show_label=False), '_', Item('file_name', style='readonly', springy=True), ), width=0.5, ) # -- Traits Event Handlers ------------------------------------------------ def _open_changed(self): """Handles the user clicking the 'Open...' button.""" file_name = open_file(extensions=LineCountInfo(), id=demo_id) if file_name != '': self.file_name = file_name # Create the demo: demo = FileDialogDemo() # Run the demo (if invoked from the command line): if __name__ == '__main__': demo.configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/examples/demo/Standard_Editors/File_Dialog/File_Open_with_FileInfo_Extension.py0000644000175100001730000000462700000000000033403 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ **WARNING** This demo might not work as expected and some documented features might be missing. ------------------------------------------------------------------------------- This demonstrates using the TraitsUI file dialog with a file dialog extension, in this case, the **FileInfo** extension, which displays information about the currently selected file, such as: - File size. - Date and time last accessed. - Date and time last modified. - Date and time last created. For more information about why you would want to use the TraitsUI file dialog over the standard OS file dialog, select the **File Open** demo. For a demonstration of writing a custom file dialog extension, select the **File Open with Custom Extension** demo. """ # Issue related to the demo warning: enthought/traitsui#953 from traits.api import HasTraits, File, Button from traitsui.api import View, HGroup, Item from traitsui.file_dialog import open_file, FileInfo # -- FileDialogDemo Class ------------------------------------------------- # Demo specific file dialig id: demo_id = 'traitsui.demo.standard_editors.file_dialog.file_info' class FileDialogDemo(HasTraits): # The name of the selected file: file_name = File() # The button used to display the file dialog: open = Button('Open...') # -- Traits View Definitions ---------------------------------------------- traits_view = View( HGroup( Item('open', show_label=False), '_', Item('file_name', style='readonly', springy=True), ), width=0.5, ) # -- Traits Event Handlers ------------------------------------------------ def _open_changed(self): """Handles the user clicking the 'Open...' button.""" file_name = open_file(extensions=FileInfo(), id=demo_id) if file_name != '': self.file_name = file_name # Create the demo: demo = FileDialogDemo() # Run the demo (if invoked from the command line): if __name__ == '__main__': demo.configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/examples/demo/Standard_Editors/File_Dialog/File_Open_with_ImageInfo_Extension.py0000644000175100001730000000550200000000000033537 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ **WARNING** This demo might not work as expected and some documented features might be missing. ------------------------------------------------------------------------------- This demonstrates using the TraitsUI file dialog with a file dialog extension, in this case, the **ImageInfo** extension, which displays (if possible) the contents, width and height information for the currently selected image file so that the user can quickly verify they are opening the correct file before leaving the file dialog. For more information about why you would want to use the TraitsUI file dialog over the standard OS file dialog, select the **File Open** demo. For a demonstration of writing a custom file dialog extension, select the **File Open with Custom Extension** demo. This example also shows setting a file name filter which only allows common image file formats (i.e. ``*.png``, ``*.gif``, ``*.jpg``, ``*.jpeg``) files to be viewed and selected. """ # Issue related to the demo warning: enthought/traitsui#953 from traits.api import HasTraits, File, Button from traitsui.api import View, HGroup, Item from traitsui.file_dialog import open_file, ImageInfo # -- FileDialogDemo Class ------------------------------------------------- # Demo specific file dialig id: demo_id = 'traitsui.demo.standard_editors.file_dialog.image_info' # The image filters description: filters = [ 'PNG file (*.png)|*.png', 'GIF file (*.gif)|*.gif', 'JPG file (*.jpg)|*.jpg', 'JPEG file (*.jpeg)|*.jpeg', ] class FileDialogDemo(HasTraits): # The name of the selected file: file_name = File() # The button used to display the file dialog: open = Button('Open...') # -- Traits View Definitions ---------------------------------------------- traits_view = View( HGroup( Item('open', show_label=False), '_', Item('file_name', style='readonly', springy=True), ), width=0.5, ) # -- Traits Event Handlers ------------------------------------------------ def _open_changed(self): """Handles the user clicking the 'Open...' button.""" file_name = open_file( extensions=ImageInfo(), filter=filters, id=demo_id ) if file_name != '': self.file_name = file_name # Create the demo: demo = FileDialogDemo() # Run the demo (if invoked from the command line): if __name__ == '__main__': demo.configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/examples/demo/Standard_Editors/File_Dialog/File_Open_with_Multiple_Extensions.py0000644000175100001730000000551700000000000033665 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ **WARNING** This demo might not work as expected and some documented features might be missing. ------------------------------------------------------------------------------- This demonstrates using the TraitsUI file dialog with multiple file dialog extensions, in this case, the **FileInfo**, **TextInfo** and **ImageInfo** extensions. For more information about why you would want to use the TraitsUI file dialog over the standard OS file dialog, select the **File Open** demo. For a demonstration of writing a custom file dialog extension, select the **File Open with Custom Extension** demo. Suggestion: Try resizing the dialog and dragging the various file dialog extensions around to create a better arrangement than the rather cramped default vertical arrangement. Close the dialog, then re-open it to see that your new arrangement has been correctly restored. Try a different file dialog demo to verify that the customizations are not affected by any of the other demos because this demo specifies a custom id when invoking the file dialog. """ # Issue related to the demo warning: enthought/traitsui#953 from traits.api import HasTraits, File, Button from traitsui.api import View, HGroup, Item from traitsui.file_dialog import open_file, FileInfo, TextInfo, ImageInfo # -- FileDialogDemo Class ------------------------------------------------- # Demo specific file dialig id: demo_id = 'traitsui.demo.standard_editors.file_dialog.multiple_info' # The list of file dialog extensions to use: extensions = [FileInfo(), TextInfo(), ImageInfo()] class FileDialogDemo(HasTraits): # The name of the selected file: file_name = File() # The button used to display the file dialog: open = Button('Open...') # -- Traits View Definitions ---------------------------------------------- traits_view = View( HGroup( Item('open', show_label=False), '_', Item('file_name', style='readonly', springy=True), ), width=0.5, ) # -- Traits Event Handlers ------------------------------------------------ def _open_changed(self): """Handles the user clicking the 'Open...' button.""" file_name = open_file(extensions=extensions, id=demo_id) if file_name != '': self.file_name = file_name # Create the demo: demo = FileDialogDemo() # Run the demo (if invoked from the command line): if __name__ == '__main__': demo.configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/examples/demo/Standard_Editors/File_Dialog/File_Open_with_TextInfo_Extension.py0000644000175100001730000000515000000000000033440 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ **WARNING** This demo might not work as expected and some documented features might be missing. ------------------------------------------------------------------------------- This demonstrates using the TraitsUI file dialog with a file dialog extension, in this case, the **TextInfo** extension, which displays (if possible) the contents of the currently selected file in a read-only text editor so the user can quickly verify they are opening the correct file before leaving the file dialog. For more information about why you would want to use the TraitsUI file dialog over the standard OS file dialog, select the **File Open** demo. For a demonstration of writing a custom file dialog extension, select the **File Open with Custom Extension** demo. This example also shows setting a file name filter which only allows Python source (i.e. ``*.py``) files to be viewed and selected. """ # Issue related to the demo warning: enthought/traitsui#953 from traits.api import HasTraits, File, Button from traitsui.api import View, HGroup, Item from traitsui.file_dialog import open_file, TextInfo # -- FileDialogDemo Class ------------------------------------------------- # Demo specific file dialog id: demo_id = 'traitsui.demo.standard_editors.file_dialog.text_info' class FileDialogDemo(HasTraits): # The name of the selected file: file_name = File() # The button used to display the file dialog: open = Button('Open...') # -- Traits View Definitions ---------------------------------------------- traits_view = View( HGroup( Item('open', show_label=False), '_', Item('file_name', style='readonly', springy=True), ), width=0.5, ) # -- Traits Event Handlers ------------------------------------------------ def _open_changed(self): """Handles the user clicking the 'Open...' button.""" file_name = open_file( extensions=TextInfo(), filter='Python file (*.py)|*.py', id=demo_id ) if file_name != '': self.file_name = file_name # Create the demo: demo = FileDialogDemo() # Run the demo (if invoked from the command line): if __name__ == '__main__': demo.configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/examples/demo/Standard_Editors/FontEditor_demo.py0000644000175100001730000000407300000000000025617 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Font editor A Font editor in a Traits UI allows the user to select a font from the operating system. Typically, you then pass the Font trait to another UI editor, which uses it to display text. You can also read the Font trait as a string, or access its individual attributes (note that these attributes are specific to the UI toolkit -- QT or WX.) The default 'simple' Font editor style is usually the most useful and powerful style - it pops up a font selection dialog which is specific to the OS and toolkit. This example also displays some other less common style choices. Please refer to the `FontEditor API docs`_ for further information. .. _FontEditor API docs: https://docs.enthought.com/traitsui/api/traitsui.editors.font_editor.html#traitsui.editors.font_editor.FontEditor """ from traits.api import HasTraits from traitsui.api import Item, Group, View, Font class FontEditorDemo(HasTraits): """Defines the main FontEditor demo class.""" # Define a Font trait to view: my_font_trait = Font() # Display specification (one Item per editor style): font_group = Group( Item('my_font_trait', style='simple', label='Simple'), Item('_'), Item('my_font_trait', style='custom', label='Custom'), Item('_'), Item('my_font_trait', style='text', label='Text'), Item('_'), Item('my_font_trait', style='readonly', label='ReadOnly'), ) # Demo view: traits_view = View( font_group, title='FontEditor', buttons=['OK'], resizable=True ) # Create the demo: demo = FontEditorDemo() # Run the demo (if invoked from the command line): if __name__ == '__main__': demo.configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/examples/demo/Standard_Editors/HTMLEditor_demo.py0000644000175100001730000000462500000000000025460 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ HTML editor The HTML editor displays simple formatted HTML text in a Traits UI view. If the text is held in an HTML trait, then the HTMLEditor is the default. If the text is held in a Str trait, then you may specify the HTMLEditor explicitly if you wish to display it as HTML. The supported subset of HTML tags and features depends on the UI toolkit (WX or QT). This editor does not support style sheets. It does not support WYSIWYG editing of the text, though the unformatted text can be edited in a plain text editor. The HTML editor can optionally be configured to do simple formatting of lists and paragraphs without HTML tags, by setting the editor's 'format_text' parameter True. Please refer to the `HTMLEditor API docs`_ for further information. .. _HTMLEditor API docs: https://docs.enthought.com/traitsui/api/traitsui.editors.html_editor.html#traitsui.editors.html_editor.HTMLEditor """ from traits.api import HasTraits, HTML from traitsui.api import UItem, View, HTMLEditor # Sample text to display as HTML: header, plus module docstring, plus # some lists. The docstring and lists will be auto-formatted # (format_text=True). sample_text = ( """

    HTMLEditor example

    """ + __doc__ + """ Here are some lists formatted in this way: Numbered list: * first * second * third Bulleted list: - eat - drink - be merry """ ) class HTMLEditorDemo(HasTraits): """Defines the main HTMLEditor demo class.""" # Define a HTML trait to view my_html_trait = HTML(sample_text) # Demo view traits_view = View( UItem( 'my_html_trait', # we specify the editor explicitly in order to set format_text: editor=HTMLEditor(format_text=True), ), title='HTMLEditor', buttons=['OK'], width=800, height=600, resizable=True, ) # Create the demo: demo = HTMLEditorDemo() # Run the demo (if invoked from the command line): if __name__ == '__main__': demo.configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/examples/demo/Standard_Editors/ImageEnumEditor_demo.py0000644000175100001730000000475300000000000026565 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ **WARNING** This demo might not work as expected and some documented features might be missing. ------------------------------------------------------------------------------- Implementation of an ImageEnumEditor demo plugin for the Traits UI demo program. This demo shows each of the four styles of the ImageEnumEditor. Please refer to the `ImageEnumEditor API docs`_ for further information. .. _ImageEnumEditor API docs: https://docs.enthought.com/traitsui/api/traitsui.editors.image_enum_editor.html#traitsui.editors.image_enum_editor.ImageEnumEditor """ # Issues related to the demo warning: # enthought/traitsui#947 from traits.api import Enum, HasTraits, Str from traitsui.api import Item, Group, View, ImageEnumEditor # This list of image names (with the standard suffix "_origin") is used to # construct an image enumeration trait to demonstrate the ImageEnumEditor: image_list = ['top left', 'top right', 'bottom left', 'bottom right'] class Dummy(HasTraits): """Dummy class for ImageEnumEditor""" x = Str() traits_view = View() class ImageEnumEditorDemo(HasTraits): """Defines the ImageEnumEditor demo class.""" # Define a trait to view: image_from_list = Enum( *image_list, editor=ImageEnumEditor( values=image_list, prefix='@icons:', suffix='_origin', cols=4, klass=Dummy, ), ) # Items are used to define the demo display, one Item per editor style: img_group = Group( Item('image_from_list', style='simple', label='Simple'), Item('_'), Item('image_from_list', style='text', label='Text'), Item('_'), Item('image_from_list', style='readonly', label='ReadOnly'), Item('_'), Item('image_from_list', style='custom', label='Custom'), ) # Demo view: traits_view = View( img_group, title='ImageEnumEditor', buttons=['OK'], resizable=True ) # Create the demo: demo = ImageEnumEditorDemo() # Run the demo (if invoked from the command line): if __name__ == '__main__': demo.configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/examples/demo/Standard_Editors/InstanceEditor_demo.py0000644000175100001730000000552200000000000026455 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ **WARNING** This demo might not work as expected and some documented features might be missing. ------------------------------------------------------------------------------- Implementation of an InstanceEditor demo plugin for the Traits UI demo program. This demo shows each of the four styles of the InstanceEditor Fixme: This version of the demo only shows the old-style InstanceEditor capabilities. Please refer to the `InstanceEditor API docs`_ for further information. .. _InstanceEditor API docs: https://docs.enthought.com/traitsui/api/traitsui.editors.instance_editor.html#traitsui.editors.instance_editor.InstanceEditor """ # Issue related to the demo warning: enthought/traitsui#939 from traits.api import HasTraits, Str, Range, Bool, Instance from traitsui.api import Item, Group, View # ------------------------------------------------------------------------- # Classes: # ------------------------------------------------------------------------- class SampleClass(HasTraits): """This Sample class is used to demonstrate the InstanceEditor demo.""" # The actual attributes don't matter here; we just need an assortment # to demonstrate the InstanceEditor's capabilities.: name = Str() occupation = Str() age = Range(21, 65) registered_voter = Bool() # The InstanceEditor uses whatever view is defined for the class. The # default view lists the fields alphabetically, so it's best to define one # explicitly: traits_view = View('name', 'occupation', 'age', 'registered_voter') class InstanceEditorDemo(HasTraits): """This class specifies the details of the InstanceEditor demo.""" # Create an Instance trait to view: sample_instance = Instance(SampleClass, ()) # Items are used to define the demo display, one item per editor style: inst_group = Group( Item('sample_instance', style='simple', label='Simple', id="simple"), Item('_'), Item('sample_instance', style='custom', label='Custom', id="custom"), Item('_'), Item('sample_instance', style='text', label='Text'), Item('_'), Item('sample_instance', style='readonly', label='ReadOnly'), ) # Demo View: traits_view = View( inst_group, title='InstanceEditor', buttons=['OK'], resizable=True ) # Create the demo: demo = InstanceEditorDemo() # Run the demo (if invoked from the command line): if __name__ == '__main__': demo.configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/examples/demo/Standard_Editors/ListEditor_demo.py0000644000175100001730000000351400000000000025623 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Implemention of a ListEditor demo plugin for Traits UI demo program This demo shows each of the four styles of ListEditor. Please refer to the `ListEditor API docs`_ for further information. .. _ListEditor API docs: https://docs.enthought.com/traitsui/api/traitsui.editors.list_editor.html#traitsui.editors.list_editor.ListEditor """ from traits.api import HasTraits, List, Str from traitsui.api import Item, Group, View # Define the demo class: class ListEditorDemo(HasTraits): """Defines the main ListEditor demo class.""" # Define a List trait to display: play_list = List(Str, ["The Merchant of Venice", "Hamlet", "MacBeth"]) # Items are used to define display, one per editor style: list_group = Group( Item( 'play_list', style='simple', label='Simple', height=75, id='simple' ), Item('_'), Item('play_list', style='custom', label='Custom', id='custom'), Item('_'), Item('play_list', style='text', label='Text', id='text'), Item('_'), Item('play_list', style='readonly', label='ReadOnly', id='readonly'), ) # Demo view: traits_view = View( list_group, title='ListEditor', buttons=['OK'], height=600, width=400, resizable=True, ) # Create the demo: demo = ListEditorDemo() # Run the demo (if invoked from the command line): if __name__ == '__main__': demo.configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/examples/demo/Standard_Editors/RGBColorEditor_demo.py0000644000175100001730000000367500000000000026331 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ **WARNING** This demo might not work as expected and some documented features might be missing. ------------------------------------------------------------------------------- Implementation of a RGBColorEditor demo plugin for Traits UI demo program. This demo shows each of the four styles of the ColorEditor. Please refer to the `RGBColorEditor API docs`_ for further information. .. _RGBColorEditor API docs: https://docs.enthought.com/traitsui/api/traitsui.editors.rgb_color_editor.html#traitsui.editors.rgb_color_editor.RGBColorEditor """ # Issue related to the demo warning: enthought/traitsui#939 from traits.api import HasTraits from traitsui.api import Item, Group, View, RGBColor # Demo class definition: class RGBColorEditorDemo(HasTraits): """Defines the main RGBColorEditor demo.""" # Define a Color trait to view: color_trait = RGBColor() # Items are used to define the demo display, one item per editor style: color_group = Group( Item('color_trait', style='simple', label='Simple'), Item('_'), Item('color_trait', style='custom', label='Custom'), Item('_'), Item('color_trait', style='text', label='Text'), Item('_'), Item('color_trait', style='readonly', label='ReadOnly'), ) # Demo view traits_view = View( color_group, title='RGBColorEditor', buttons=['OK'], resizable=True ) # Create the demo: demo = RGBColorEditorDemo() # Run the demo (if invoked from the command line): if __name__ == '__main__': demo.configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/examples/demo/Standard_Editors/RangeEditor_demo.py0000644000175100001730000001103400000000000025740 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Range editor A Range Trait holds a numeric value which is restricted to a specified range. This example shows how the RangeEditor's simple and custom styles vary depending on the type (integer or float) and size (small, medium, or large integer) of the specified range. The example also shows how multiple Groups at the top level of a View are automatically placed into separate tabs. For an example of how to dynamically vary the bounds of a Range trait, see the *Dynamic Range Editor* example. Please refer to the `RangeEditor API docs`_ for further information. .. _RangeEditor API docs: https://docs.enthought.com/traitsui/api/traitsui.editors.range_editor.html#traitsui.editors.range_editor.RangeEditor """ from traits.api import HasTraits, Range from traitsui.api import Item, Group, View class RangeEditorDemo(HasTraits): """Defines the RangeEditor demo class.""" # Define a trait for each of four range variants: small_int_range = Range(1, 16) medium_int_range = Range(1, 25) large_int_range = Range(1, 150) float_range = Range(0.0, 150.0) # RangeEditor display for narrow integer Range traits (< 17 wide): int_range_group1 = Group( Item( 'small_int_range', style='simple', label='Simple', id='simple_small', ), Item('_'), Item( 'small_int_range', style='custom', label='Custom', id='custom_small', ), Item('_'), Item('small_int_range', style='text', label='Text', id='text_small'), Item('_'), Item( 'small_int_range', style='readonly', label='ReadOnly', id='readonly_small', ), label='Small Int', ) # RangeEditor display for medium-width integer Range traits (17 to 100): int_range_group2 = Group( Item( 'medium_int_range', style='simple', label='Simple', id='simple_medium', ), Item('_'), Item( 'medium_int_range', style='custom', label='Custom', id='custom_medium', ), Item('_'), Item('medium_int_range', style='text', label='Text', id='text_medium'), Item('_'), Item( 'medium_int_range', style='readonly', label='ReadOnly', id='readonly_medium', ), label='Medium Int', ) # RangeEditor display for wide integer Range traits (> 100): int_range_group3 = Group( Item( 'large_int_range', style='simple', label='Simple', id='simple_large', ), Item('_'), Item( 'large_int_range', style='custom', label='Custom', id='custom_large', ), Item('_'), Item('large_int_range', style='text', label='Text', id='text_large'), Item('_'), Item( 'large_int_range', style='readonly', label='ReadOnly', id='readonly_large', ), label='Large Int', ) # RangeEditor display for float Range traits: float_range_group = Group( Item('float_range', style='simple', label='Simple', id='simple_float'), Item('_'), Item('float_range', style='custom', label='Custom', id='custom_float'), Item('_'), Item('float_range', style='text', label='Text', id='text_float'), Item('_'), Item( 'float_range', style='readonly', label='ReadOnly', id='readonly_float', ), label='Float', ) # The view includes one group per data type. These will be displayed # on separate tabbed panels: traits_view = View( int_range_group1, int_range_group2, int_range_group3, float_range_group, title='RangeEditor', buttons=['OK'], resizable=True, ) # Create the demo: demo = RangeEditorDemo() # Run the demo (if invoked from the comand line): if __name__ == '__main__': demo.configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/examples/demo/Standard_Editors/SetEditor_demo.py0000644000175100001730000000656400000000000025453 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Implementation of a SetEditor demo plugin for the Traits UI demo program. The four tabs of this demo show variations on the interface as follows: - Unord I: Creates an alphabetized subset, has no "move all" options - Unord II: Creates an alphabetized subset, has "move all" options - Ord I: Creates a set whose order is specified by the user, no "move all" - Ord II: Creates a set whose order is specifed by the user, has "move all" Please refer to the `SetEditor API docs`_ for further information. .. _SetEditor API docs: https://docs.enthought.com/traitsui/api/traitsui.editors.set_editor.html#traitsui.editors.set_editor.SetEditor """ from traits.api import HasTraits, List from traitsui.api import Item, Group, View, SetEditor # Define the main demo class: class SetEditorDemo(HasTraits): """Defines the SetEditor demo class.""" # Define a trait each for four SetEditor variants: unord_nma_set = List( editor=SetEditor( values=['kumquats', 'pomegranates', 'kiwi'], left_column_title='Available Fruit', right_column_title='Exotic Fruit Bowl', can_move_all=False, ) ) unord_ma_set = List( editor=SetEditor( values=['kumquats', 'pomegranates', 'kiwi'], left_column_title='Available Fruit', right_column_title='Exotic Fruit Bowl', ) ) ord_nma_set = List( editor=SetEditor( values=['apples', 'berries', 'cantaloupe'], left_column_title='Available Fruit', right_column_title='Fruit Bowl', ordered=True, can_move_all=False, ) ) ord_ma_set = List( editor=SetEditor( values=['apples', 'berries', 'cantaloupe'], left_column_title='Available Fruit', right_column_title='Fruit Bowl', ordered=True, ) ) # SetEditor display, unordered, no move-all buttons: no_nma_group = Group( Item('unord_nma_set', style='simple'), label='Unord I', show_labels=False, ) # SetEditor display, unordered, move-all buttons: no_ma_group = Group( Item('unord_ma_set', style='simple'), label='Unord II', show_labels=False, ) # SetEditor display, ordered, no move-all buttons: o_nma_group = Group( Item('ord_nma_set', style='simple'), label='Ord I', show_labels=False ) # SetEditor display, ordered, move-all buttons: o_ma_group = Group( Item('ord_ma_set', style='simple'), label='Ord II', show_labels=False ) # The view includes one group per data type. These will be displayed # on separate tabbed panels: traits_view = View( no_nma_group, no_ma_group, o_nma_group, o_ma_group, title='SetEditor', buttons=['OK'], ) # Create the demo: demo = SetEditorDemo() # Run the demo (if invoked from the command line): if __name__ == '__main__': demo.configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/examples/demo/Standard_Editors/TableEditor_demo.py0000644000175100001730000000726400000000000025745 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ **WARNING** This demo might not work as expected and some documented features might be missing. ------------------------------------------------------------------------------- Implementation of a TableEditor demo plugin for Traits UI demo program This demo shows the full behavior of a straightforward TableEditor. Only one style of TableEditor is implemented, so that is the one shown. Please refer to the `TableEditor API docs`_ for further information. .. _TableEditor API docs: https://docs.enthought.com/traitsui/api/traitsui.editors.table_editor.html#traitsui.editors.table_editor.TableEditor """ # Issue related to the demo warning: enthought/traitsui#948 from traits.api import HasTraits, HasStrictTraits, Str, Int, Regex, List from traitsui.api import ( View, Group, Item, TableEditor, ObjectColumn, ExpressionColumn, EvalTableFilter, ) from traitsui.table_filter import ( EvalFilterTemplate, MenuFilterTemplate, RuleFilterTemplate, ) # A helper class for the 'Department' class below: class Employee(HasTraits): first_name = Str() last_name = Str() age = Int() phone = Regex(value='000-0000', regex=r'\d\d\d[-]\d\d\d\d') traits_view = View( 'first_name', 'last_name', 'age', 'phone', title='Create new employee', width=0.18, buttons=['OK', 'Cancel'], ) # For readability, the TableEditor of the demo is defined here, rather than in # the View: table_editor = TableEditor( columns=[ ObjectColumn(name='first_name', width=0.20), ObjectColumn(name='last_name', width=0.20), ExpressionColumn( label='Full Name', width=0.30, expression="'%s %s' % (object.first_name, " "object.last_name )", ), ObjectColumn(name='age', width=0.10, horizontal_alignment='center'), ObjectColumn(name='phone', width=0.20), ], deletable=True, sort_model=True, auto_size=False, orientation='vertical', edit_view=View( Group('first_name', 'last_name', 'age', 'phone', show_border=True), resizable=True, ), filters=[EvalFilterTemplate, MenuFilterTemplate, RuleFilterTemplate], search=EvalTableFilter(), show_toolbar=True, row_factory=Employee, ) # The class to be edited with the TableEditor: class Department(HasStrictTraits): employees = List(Employee) traits_view = View( Group( Item('employees', show_label=False, editor=table_editor), show_border=True, ), title='Department Personnel', width=0.4, height=0.4, resizable=True, buttons=['OK'], kind='live', ) # Create some employees: employees = [ Employee(first_name='Jason', last_name='Smith', age=32, phone='555-1111'), Employee(first_name='Mike', last_name='Tollan', age=34, phone='555-2222'), Employee( first_name='Dave', last_name='Richards', age=42, phone='555-3333' ), Employee(first_name='Lyn', last_name='Spitz', age=40, phone='555-4444'), Employee(first_name='Greg', last_name='Andrews', age=45, phone='555-5555'), ] # Create the demo: demo = Department(employees=employees) # Run the demo (if invoked from the command line): if __name__ == '__main__': demo.configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/examples/demo/Standard_Editors/TextEditor_demo.py0000644000175100001730000000632700000000000025641 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Edit a string, password, or integer The TextEditor displays a Str, Password, or Int trait for the user to edit. The demo shows all styles of the editor for each of the traits, however certain styles are more useful than others: - When editing a Str, consider styles 'simple' (one-line), 'custom' (multi-line), or 'readonly' (multi-line). - When editing a Password, style 'simple' is recommended (shows asterisks). - When editing an Int, consider styles 'simple' and 'readonly'. Please refer to the `TextEditor API docs`_ for further information. .. _TextEditor API docs: https://docs.enthought.com/traitsui/api/traitsui.editors.text_editor.html#traitsui.editors.text_editor.TextEditor """ from traits.api import HasTraits, Str, Int, Password from traitsui.api import Item, Group, View # The main demo class: class TextEditorDemo(HasTraits): """Defines the TextEditor demo class.""" # Define a trait for each of three TextEditor variants: string_trait = Str("sample string") int_trait = Int(1) password = Password() # TextEditor display with multi-line capability (for a string): text_str_group = Group( Item('string_trait', style='simple', label='Simple'), Item('_'), Item('string_trait', style='custom', label='Custom'), Item('_'), Item('string_trait', style='text', label='Text'), Item('_'), Item('string_trait', style='readonly', label='ReadOnly'), label='String', ) # TextEditor display without multi-line capability (for an integer): text_int_group = Group( Item('int_trait', style='simple', label='Simple', id="simple_int"), Item('_'), Item('int_trait', style='custom', label='Custom', id="custom_int"), Item('_'), Item('int_trait', style='text', label='Text', id="text_int"), Item('_'), Item( 'int_trait', style='readonly', label='ReadOnly', id="readonly_int", ), label='Integer', ) # TextEditor display with secret typing capability (for Password traits): text_pass_group = Group( Item('password', style='simple', label='Simple'), Item('_'), Item('password', style='custom', label='Custom'), Item('_'), Item('password', style='text', label='Text'), Item('_'), Item('password', style='readonly', label='ReadOnly'), label='Password', ) # The view includes one group per data type. These will be displayed # on separate tabbed panels: traits_view = View( text_str_group, text_pass_group, text_int_group, title='TextEditor', buttons=['OK'], ) # Create the demo: demo = TextEditorDemo() # Run the demo (if invoked from the command line): if __name__ == "__main__": demo.configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/examples/demo/Standard_Editors/TitleEditor_demo.py0000644000175100001730000000754300000000000025777 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Demonstrates the use of the TitleEditor. A TitleEditor can be used to dynamically label sections of a user interface. The text displayed by the TitleEditor is specified by a trait associated with the view. This demonstration shows three variations of using a TitleEditor: * In the first example, the TitleEditor values are supplied by an Enum trait. Simply select a new value for the title from the drop-down list to cause the title to change. * In the second example, the TitleEditor values are supplied by a Str trait. Simply type a new value into the title field to cause the title to change. * In the third example, the TitleEditor values are supplied by a Property whose value is derived from a calculation on a Float trait. Type a number into the value field to cause the title to changed. Please refer to the `TitleEditor API docs`_ for further information. .. _TitleEditor API docs: https://docs.enthought.com/traitsui/api/traitsui.editors.title_editor.html#traitsui.editors.title_editor.TitleEditor """ from traits.api import HasTraits, Enum, Str, Float, Property, cached_property from traitsui.api import View, VGroup, HGroup, Item, TitleEditor class TitleEditorDemo(HasTraits): # Define the selection of titles that can be displayed: title = Enum( 'Select a new title from the drop down list below', 'This is the TitleEditor demonstration', 'Acme Widgets Sales for Each Quarter', 'This is Not Intended to be a Real Application', ) # A user settable version of the title: title_2 = Str('Type into the text field below to change this title') # A title driven by the result of a calculation: title_3 = Property(observe='value') # The number used to drive the calculation: value = Float() # Define the test view: traits_view = View( VGroup( VGroup( HGroup( Item( 'title', show_label=False, springy=True, editor=TitleEditor(), ) ), Item('title'), show_border=True, ), VGroup( HGroup( Item( 'title_2', show_label=False, springy=True, editor=TitleEditor(), ) ), Item('title_2', label='Title'), show_border=True, ), VGroup( HGroup( Item( 'title_3', show_label=False, springy=True, editor=TitleEditor(), ) ), Item('value'), show_border=True, ), ), width=0.4, ) # -- Property Implementations --------------------------------------------- @cached_property def _get_title_3(self): if self.value >= 0: return 'The square root of {} is {}'.format( self.value, self.value ** 0.5 ) else: return 'The square root of {} is {}i'.format( self.value, (-self.value) ** 0.5 ) # Create the demo: demo = TitleEditorDemo() # Run the demo (if invoked from the command line): if __name__ == '__main__': demo.configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/examples/demo/Standard_Editors/TreeEditor_demo.py0000644000175100001730000001206000000000000025603 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Tree editor for hierarchal data Demonstrates using the TreeEditor to display a hierarchically organized data structure. In this case, the tree has the following hierarchy: - Partner - Company - Department - Employee The TreeEditor generates a hierarchical tree control, consisting of nodes. It is useful for cases where objects contain lists of other objects. The tree control is displayed in one pane of the editor, and a user interface for the selected object is displayed in the other pane. The layout orientation of the tree and the object editor is determined by the *orientation* parameter of TreeEditor(), which can be 'horizontal' or 'vertical'. You must specify the types of nodes that can appear in the tree using the *nodes* parameter, which must be a list of instances of TreeNode (or of subclasses of TreeNode). You must specify the classes whose instances the node type applies to. Use the **node_for** attribute of TreeNode to specify a list of classes; often, this list contains only one class. You can have more than one node type that applies to a particular class; in this case, each object of that class is represented by multiple nodes, one for each applicable node type. See the Traits User Manual for more details. Please refer to the `TreeEditor API docs`_ for further information. .. _TreeEditor API docs: https://docs.enthought.com/traitsui/api/traitsui.editors.tree_editor.html#traitsui.editors.tree_editor.TreeEditor """ from traits.api import HasTraits, Str, Regex, List, Instance from traitsui.api import Item, View, TreeEditor, TreeNode class Employee(HasTraits): """Defines a company employee.""" name = Str('') title = Str() phone = Regex(regex=r'\d\d\d-\d\d\d\d') def default_title(self): self.title = 'Senior Engineer' class Department(HasTraits): """Defines a department with employees.""" name = Str('') employees = List(Employee) class Company(HasTraits): """Defines a company with departments and employees.""" name = Str('') departments = List(Department) employees = List(Employee) # Create an empty view for objects that have no data to display: no_view = View() # Define the TreeEditor used to display the hierarchy: tree_editor = TreeEditor( nodes=[ # The first node specified is the top level one TreeNode( node_for=[Company], auto_open=True, # child nodes are children='', label='name', # label with Company name view=View(['name']), ), TreeNode( node_for=[Company], auto_open=True, children='departments', label='=Departments', # constant label view=no_view, add=[Department], ), TreeNode( node_for=[Company], auto_open=True, children='employees', label='=Employees', # constant label view=no_view, add=[Employee], ), TreeNode( node_for=[Department], auto_open=True, children='employees', label='name', # label with Department name view=View(['name']), add=[Employee], ), TreeNode( node_for=[Employee], auto_open=True, label='name', # label with Employee name view=View(['name', 'title', 'phone']), ), ] ) class Partner(HasTraits): """Defines a business partner.""" name = Str('') company = Instance(Company) traits_view = View( Item(name='company', editor=tree_editor, show_label=False), title='Company Structure', buttons=['OK'], resizable=True, style='custom', width=0.3, height=500, ) # Create an example data structure: jason = Employee(name='Jason', title='Senior Engineer', phone='536-1057') mike = Employee(name='Mike', title='Senior Engineer', phone='536-1057') dave = Employee(name='Dave', title='Senior Developer', phone='536-1057') martin = Employee(name='Martin', title='Senior Engineer', phone='536-1057') duncan = Employee(name='Duncan', title='Consultant', phone='526-1057') # Create the demo: demo = Partner( name='Enthought, Inc.', company=Company( name='Enthought', employees=[dave, martin, duncan, jason, mike], departments=[ Department(name='Business', employees=[jason, mike]), Department(name='Scientific', employees=[dave, martin, duncan]), ], ), ) # Run the demo (if invoked from the command line): if __name__ == '__main__': demo.configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/examples/demo/Standard_Editors/TupleEditor_demo.py0000644000175100001730000000361100000000000025777 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ **WARNING** This demo might not work as expected and some documented features might be missing. ------------------------------------------------------------------------------- Implementation of a TupleEditor demo plugin for Traits UI demo program. This demo shows each of the four styles of the TupleEditor. Please refer to the `TupleEditor API docs`_ for further information. .. _TupleEditor API docs: https://docs.enthought.com/traitsui/api/traitsui.editors.tuple_editor.html#traitsui.editors.tuple_editor.TupleEditor """ # Issue related to the demo warning: enthought/traitsui#939 from traits.api import HasTraits, Tuple, Range, Str from traitsui.api import Item, Group, View, Color # The main demo class: class TupleEditorDemo(HasTraits): """Defines the TupleEditor demo class.""" # Define a trait to view: tuple = Tuple(Color, Range(1, 4), Str) # Display specification (one Item per editor style): tuple_group = Group( Item('tuple', style='simple', label='Simple'), Item('_'), Item('tuple', style='custom', label='Custom'), Item('_'), Item('tuple', style='text', label='Text'), Item('_'), Item('tuple', style='readonly', label='ReadOnly'), ) # Demo view traits_view = View( tuple_group, title='TupleEditor', buttons=['OK'], resizable=True ) # Create the demo: demo = TupleEditorDemo() # Run the demo (if invoked from the command line): if __name__ == '__main__': demo.configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/examples/demo/Standard_Editors/VideoEditor_demo.py0000644000175100001730000001246200000000000025760 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Demonstrates a 'display only' video editor for Qt5+. Please refer to the `VideoEditor API docs`_ for further information. .. _VideoEditor API docs: https://docs.enthought.com/traitsui/api/traitsui.editors.video_editor.html#traitsui.editors.video_editor.VideoEditor """ import numpy as np from PIL import Image from pyface.qt import is_qt5 from pyface.qt.QtGui import QImage from traits.api import ( Bool, Button, Callable, Float, HasTraits, Range, Str, observe, ) from traitsui.api import ButtonEditor, ContextValue, HGroup, Item, UItem, View from traitsui.editors.video_editor import MediaStatus, PlayerState, VideoEditor def QImage_from_np(image): assert np.max(image) <= 255 image8 = image.astype(np.uint8, order='C', casting='unsafe') height, width, colors = image8.shape bytesPerLine = 4 * width image = QImage( image8.data, width, height, bytesPerLine, QImage.Format.Format_RGB32 ) return image def np_from_QImage(qimage): # Creates a numpy array from a pyqt(5) QImage object width, height = qimage.width(), qimage.height() channels = qimage.pixelFormat().channelCount() return ( np.array(qimage.bits().asarray(width * height * channels)) .reshape(height, width, channels) .astype('u1') ) def qimage_function(antialiasing=True): def antialias_func(image_func): """ Turns an image function into a QImage function, bound within the viewing frames box. """ def qimage_conv_func(image, box_dims): _np_image = image_func(np_from_QImage(image)) pil_image = Image.fromarray(_np_image) if antialiasing: pil_image.thumbnail(box_dims, Image.ANTIALIAS) else: pil_image.thumbnail(box_dims) _np_image = np.array(pil_image) image = QImage_from_np(_np_image) return image, _np_image return qimage_conv_func return antialias_func @qimage_function(antialiasing=False) def test_image_func(image): return image.transpose(1, 0, 2) class VideoEditorDemo(HasTraits): """Defines the main VideoEditor demo class.""" #: The URL that holds the video data. video_url = Str() #: A button that plays/pauses play_pause_button = Button() button_label = Str('Play') state = PlayerState duration = Float position = Range(low=0.0, high='duration') error = Str status = MediaStatus buffer = Range(0, 100) muted = Bool(True) volume = Range(0.0, 100.0, value=100.0) playback_rate = Float(1.0) image_func = Callable() @observe('state') def _state_update(self, event): if event.new == 'stopped' or event.new == 'paused': self.button_label = 'Play' elif event.new == 'playing': self.button_label = 'Pause' @observe('play_pause_button') def _play_pause_button_click(self, event): if self.state == 'stopped' or self.state == 'paused': self.state = 'playing' else: self.state = 'paused' # Demo view traits_view = View( UItem('video_url'), UItem( 'video_url', editor=VideoEditor( state=ContextValue('state'), position=ContextValue('position'), duration=ContextValue('duration'), video_error=ContextValue('error'), media_status=ContextValue('status'), buffer=ContextValue('buffer'), muted=ContextValue('muted'), volume=ContextValue('volume'), playback_rate=ContextValue('playback_rate'), image_func=ContextValue('image_func'), notify_interval=0.5, ), ), HGroup( UItem( 'play_pause_button', editor=ButtonEditor(label_value='button_label'), enabled_when='not bool(error)', width=100, ), UItem('position'), ), HGroup( Item('playback_rate'), Item('muted', label='Mute'), Item('volume'), ), HGroup( Item('state', style='readonly'), Item('status', style='readonly'), Item('buffer', style='readonly'), Item('error', visible_when="bool(error)", style='readonly'), ), title='VideoEditor', buttons=['OK'], width=800, height=600, resizable=True, ) url = "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerEscapes.mp4" # noqa: E501 # Create the demo: if is_qt5: demo = VideoEditorDemo(image_func=test_image_func, video_url=url) else: # Qt 6 can't do on-the-fly image manipulation demo = VideoEditorDemo(video_url=url) # Run the demo (if invoked from the command line): if __name__ == '__main__': demo.configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/examples/demo/Standard_Editors/index.rst0000644000175100001730000000012600000000000024020 0ustar00runnerdocker00000000000000These examples demonstrate how the Standard Editors available in TraitsUI can be used.././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1684768028.0478072 traitsui-8.0.0/examples/demo/Standard_Editors/tests/0000755000175100001730000000000000000000000023322 5ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/examples/demo/Standard_Editors/tests/test_BooleanEditor_demo.py0000644000175100001730000000401200000000000030462 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ This example demonstrates how to test interacting with a boolean using BooleanEditor. The GUI being tested is written in the demo under the same name (minus the preceding 'test') in the outer directory. """ import os import runpy import unittest from traitsui.testing.api import ( DisplayedText, KeyClick, KeySequence, MouseClick, UITester, ) #: Filename of the demo script FILENAME = "BooleanEditor_demo.py" #: Path of the demo script DEMO_PATH = os.path.join(os.path.dirname(__file__), "..", FILENAME) class TestBooleanEditorDemo(unittest.TestCase): def test_boolean_editor_demo(self): demo = runpy.run_path(DEMO_PATH)["demo"] tester = UITester() with tester.create_ui(demo) as ui: simple = tester.find_by_id(ui, 'simple') custom = tester.find_by_id(ui, 'custom') text = tester.find_by_id(ui, 'text') readonly = tester.find_by_id(ui, 'readonly') simple.perform(MouseClick()) self.assertEqual(demo.boolean_trait, True) custom.perform(MouseClick()) self.assertEqual(demo.boolean_trait, False) for _ in range(5): text.perform(KeyClick("Backspace")) text.perform(KeySequence("True")) text.perform(KeyClick("Enter")) self.assertEqual(demo.boolean_trait, True) demo.boolean_trait = False displayed = readonly.inspect(DisplayedText()) self.assertEqual(displayed, "False") # Run the test(s) unittest.TextTestRunner().run( unittest.TestLoader().loadTestsFromTestCase(TestBooleanEditorDemo) ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/examples/demo/Standard_Editors/tests/test_BooleanEditor_simple_demo.py0000644000175100001730000000446600000000000032050 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ This example demonstrates how to test interacting with a boolean using BooleanEditor. The GUI being tested is written in the demo under the same name (minus the preceding 'test') in the outer directory. """ import os import runpy import unittest from traitsui.testing.api import ( DisplayedText, IsChecked, KeyClick, KeySequence, MouseClick, UITester, ) #: Filename of the demo script FILENAME = "BooleanEditor_simple_demo.py" #: Path of the demo script DEMO_PATH = os.path.join(os.path.dirname(__file__), "..", FILENAME) class TestBooleanEditorSimpleDemo(unittest.TestCase): def test_boolean_editor_simple_demo(self): demo = runpy.run_path(DEMO_PATH)["demo"] tester = UITester() with tester.create_ui(demo) as ui: simple = tester.find_by_id(ui, 'simple') readonly = tester.find_by_id(ui, 'readonly') text = tester.find_by_id(ui, 'text') count_changes = tester.find_by_name(ui, "count_changes") simple.perform(MouseClick()) self.assertEqual(demo.my_boolean_trait, True) for _ in range(4): text.perform(KeyClick("Backspace")) text.perform(KeySequence("False")) text.perform(KeyClick("Enter")) self.assertEqual(demo.my_boolean_trait, False) displayed_count_changes = count_changes.inspect(DisplayedText()) self.assertEqual(displayed_count_changes, '2') self.assertEqual(displayed_count_changes, str(demo.count_changes)) demo.my_boolean_trait = True displayed = readonly.inspect(DisplayedText()) self.assertEqual(displayed, "True") simple_is_checked = simple.inspect(IsChecked()) self.assertEqual(simple_is_checked, True) # Run the test(s) unittest.TextTestRunner().run( unittest.TestLoader().loadTestsFromTestCase(TestBooleanEditorSimpleDemo) ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/examples/demo/Standard_Editors/tests/test_ButtonEditor_demo.py0000644000175100001730000000544100000000000030365 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ This example demonstrates how to test interacting with a button created using ButtonEditor. It also demonstarates the use of UI Tester, together with pyface's ModalDialogTester. The GUI being tested is written in the demo under the same name (minus the preceding 'test') in the outer directory. """ import os import runpy import unittest from pyface.toolkit import toolkit_object from pyface.constant import OK from traitsui.testing.api import MouseClick, UITester ModalDialogTester = toolkit_object( "util.modal_dialog_tester:ModalDialogTester" ) no_modal_dialog_tester = ModalDialogTester.__name__ == "Unimplemented" #: Filename of the demo script FILENAME = "ButtonEditor_demo.py" #: Path of the demo script DEMO_PATH = os.path.join(os.path.dirname(__file__), "..", FILENAME) @unittest.skipIf(no_modal_dialog_tester, "ModalDialogTester unavailable") class TestButtonEditorDemo(unittest.TestCase): def test_button_editor_demo_simple(self): demo = runpy.run_path(DEMO_PATH)["demo"] tester = UITester() with tester.create_ui(demo) as ui: simple_button = tester.find_by_id(ui, "simple") # funcion object for instantiating ModalDialogTester should be a # function that opens the dialog # we want clicking the buttons to do that def click_simple_button(): simple_button.perform(MouseClick()) mdtester_simple = ModalDialogTester(click_simple_button) mdtester_simple.open_and_run(lambda x: x.click_button(OK)) self.assertTrue(mdtester_simple.dialog_was_opened) def test_button_editor_demo_custom(self): demo = runpy.run_path(DEMO_PATH)["demo"] tester = UITester() with tester.create_ui(demo) as ui: custom_button = tester.find_by_id(ui, "custom") # funcion object for instantiating ModalDialogTester should be a # function that opens the dialog # we want clicking the buttons to do that def click_custom_button(): custom_button.perform(MouseClick()) mdtester_custom = ModalDialogTester(click_custom_button) mdtester_custom.open_and_run(lambda x: x.click_button(OK)) self.assertTrue(mdtester_custom.dialog_was_opened) # Run the test(s) unittest.TextTestRunner().run( unittest.TestLoader().loadTestsFromTestCase(TestButtonEditorDemo) ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/examples/demo/Standard_Editors/tests/test_ButtonEditor_simple_demo.py0000644000175100001730000000342400000000000031735 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ This example demonstrates how to test interacting with a button created using ButtonEditor. The GUI being tested is written in the demo under the same name (minus the preceding 'test') in the outer directory. """ import os import runpy import unittest from traitsui.testing.api import DisplayedText, MouseClick, UITester #: Filename of the demo script FILENAME = "ButtonEditor_simple_demo.py" #: Path of the demo script DEMO_PATH = os.path.join(os.path.dirname(__file__), "..", FILENAME) class TestButtonEditorSimpleDemo(unittest.TestCase): def test_button_editor_simple_demo(self): demo = runpy.run_path(DEMO_PATH)["demo"] tester = UITester() with tester.create_ui(demo) as ui: button = tester.find_by_name(ui, "my_button_trait") for index in range(5): button.perform(MouseClick()) self.assertEqual(demo.click_counter, index + 1) click_counter = tester.find_by_name(ui, "click_counter") displayed_count = click_counter.inspect(DisplayedText()) self.assertEqual(displayed_count, '5') demo.click_counter = 10 displayed_count = click_counter.inspect(DisplayedText()) self.assertEqual(displayed_count, '10') # Run the test(s) unittest.TextTestRunner().run( unittest.TestLoader().loadTestsFromTestCase(TestButtonEditorSimpleDemo) ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/examples/demo/Standard_Editors/tests/test_CheckListEditor_simple_demo.py0000644000175100001730000000302200000000000032325 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ This example demonstrates how to test interacting with a checklist created using CheckListEditor. The GUI being tested is written in the demo under the same name (minus the preceding 'test') in the outer directory. """ import os import runpy import unittest from traitsui.testing.api import Index, MouseClick, UITester #: Filename of the demo script FILENAME = "CheckListEditor_simple_demo.py" #: Path of the demo script DEMO_PATH = os.path.join(os.path.dirname(__file__), "..", FILENAME) class TestCheckListEditorSimpleDemo(unittest.TestCase): def test_checklist_editor_simple_demo(self): demo = runpy.run_path(DEMO_PATH)["demo"] tester = UITester() with tester.create_ui(demo) as ui: checklist = tester.find_by_id(ui, "custom") item3 = checklist.locate(Index(2)) item3.perform(MouseClick()) self.assertEqual(demo.checklist, ["three"]) item3.perform(MouseClick()) self.assertEqual(demo.checklist, []) # Run the test(s) unittest.TextTestRunner().run( unittest.TestLoader().loadTestsFromTestCase(TestCheckListEditorSimpleDemo) ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/examples/demo/Standard_Editors/tests/test_EnumEditor_demo.py0000644000175100001730000000635500000000000030023 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ This example demonstrates how to test interacting with the various modes of the EnumEditor. The GUI being tested is written in the demo under the same name (minus the preceding 'test') in the outer directory. """ import os import runpy import unittest from traitsui.testing.api import ( DisplayedText, Index, KeyClick, KeySequence, MouseClick, SelectedText, UITester, ) #: Filename of the demo script FILENAME = "EnumEditor_demo.py" #: Path of the demo script DEMO_PATH = os.path.join(os.path.dirname(__file__), "..", FILENAME) class TestEnumEditorDemo(unittest.TestCase): def test_enum_editor_demo(self): demo = runpy.run_path(DEMO_PATH)["demo"] tester = UITester() with tester.create_ui(demo) as ui: simple_enum = tester.find_by_id(ui, "simple") simple_text_enum = tester.find_by_id(ui, "simple_text") radio_enum = tester.find_by_id(ui, "radio") list_enum = tester.find_by_id(ui, "list") text = tester.find_by_id(ui, "text") readonly = tester.find_by_id(ui, "readonly") self.assertEqual(demo.name_list, 'A-495') simple_enum.locate(Index(1)).perform(MouseClick()) self.assertEqual(demo.name_list, 'A-498') for _ in range(5): simple_text_enum.perform(KeyClick("Backspace")) simple_text_enum.perform(KeySequence("R-1226")) simple_text_enum.perform(KeyClick("Enter")) self.assertEqual(demo.name_list, 'R-1226') radio_enum.locate(Index(5)).perform(MouseClick()) self.assertEqual(demo.name_list, 'Foo') list_enum.locate(Index(3)).perform(MouseClick()) self.assertEqual(demo.name_list, 'TS-17') for _ in range(5): text.perform(KeyClick("Backspace")) text.perform(KeySequence("A-498")) text.perform(KeyClick("Enter")) self.assertEqual(demo.name_list, 'A-498') demo.name_list = 'Foo' displayed_simple = simple_enum.inspect(DisplayedText()) disp_simple_text = simple_text_enum.inspect(DisplayedText()) selected_radio = radio_enum.inspect(SelectedText()) selected_list = list_enum.inspect(SelectedText()) displayed_text = text.inspect(DisplayedText()) displayed_readonly = readonly.inspect(DisplayedText()) displayed_selected = [ displayed_simple, disp_simple_text, selected_radio, selected_list, displayed_text, displayed_readonly, ] for text in displayed_selected: self.assertEqual(text, 'Foo') # Run the test(s) unittest.TextTestRunner().run( unittest.TestLoader().loadTestsFromTestCase(TestEnumEditorDemo) ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/examples/demo/Standard_Editors/tests/test_FileEditor_demo.py0000644000175100001730000000401200000000000027762 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ This example demonstrates how to test interacting with a textbox created using FileEditor. The GUI being tested is written in the demo under the same name (minus the preceding 'test') in the outer directory. """ import os import runpy import unittest from traitsui.testing.api import DisplayedText, KeyClick, KeySequence, UITester #: Filename of the demo script FILENAME = "FileEditor_demo.py" #: Path of the demo script DEMO_PATH = os.path.join(os.path.dirname(__file__), "..", FILENAME) class TestFileEditorDemo(unittest.TestCase): def test_run_demo(self): demo = runpy.run_path(DEMO_PATH)["demo"] tester = UITester() with tester.create_ui(demo) as ui: # simple FileEditor simple_file_field = tester.find_by_id(ui, "simple_file") # Modify the value on the GUI # The path does not exist, which is allowed by the trait. # The custom FileEditor will ignore nonexisting file path. simple_file_field.perform(KeySequence("some_path")) simple_file_field.perform(KeyClick("Enter")) self.assertEqual(demo.file_name, "some_path") # Modify the value on the object demo.file_name = FILENAME self.assertEqual( simple_file_field.inspect(DisplayedText()), FILENAME ) # On wx, an assertion error occurs upon disposing the UI. # That error is linked to the custom FileEditor. # See enthought/traitsui#752 # Run the test(s) unittest.TextTestRunner().run( unittest.TestLoader().loadTestsFromTestCase(TestFileEditorDemo) ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/examples/demo/Standard_Editors/tests/test_InstanceEditor_demo.py0000644000175100001730000000422600000000000030656 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ This example demonstrates how to test interacting with a UI created using InstanceEditor. The GUI being tested is written in the demo under the same name (minus the preceding 'test') in the outer directory. """ import os import runpy import unittest from traitsui.testing.api import ( DisplayedText, KeyClick, KeySequence, MouseClick, UITester, ) #: Filename of the demo script FILENAME = "InstanceEditor_demo.py" #: Path of the demo script DEMO_PATH = os.path.join(os.path.dirname(__file__), "..", FILENAME) class TestInstanceEditorDemo(unittest.TestCase): def test_instance_editor_demo(self): demo = runpy.run_path(DEMO_PATH)["demo"] tester = UITester() with tester.create_ui(demo) as ui: simple = tester.find_by_id(ui, "simple") custom = tester.find_by_id(ui, "custom") occupation = custom.find_by_name("occupation") occupation.perform(KeySequence("Job")) occupation.perform(KeyClick("Enter")) self.assertEqual(demo.sample_instance.occupation, "Job") simple.perform(MouseClick()) name = simple.find_by_name("name") name.perform(KeySequence("ABC")) name.perform(KeyClick("Enter")) self.assertEqual(demo.sample_instance.name, "ABC") demo.sample_instance.name = "XYZ" simple_displayed = name.inspect(DisplayedText()) custom_name = custom.find_by_name("name") custom_displayed = custom_name.inspect(DisplayedText()) self.assertEqual(simple_displayed, "XYZ") self.assertEqual(custom_displayed, "XYZ") # Run the test(s) unittest.TextTestRunner().run( unittest.TestLoader().loadTestsFromTestCase(TestInstanceEditorDemo) ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/examples/demo/Standard_Editors/tests/test_ListEditor_demo.py0000644000175100001730000000313600000000000030024 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ This example demonstrates how to test interacting with a ListEditor. The GUI being tested is written in the demo under the same name (minus the preceding 'test') in the outer directory. """ import os import runpy import unittest from traitsui.testing.api import Index, KeyClick, KeySequence, UITester #: Filename of the demo script FILENAME = "ListEditor_demo.py" #: Path of the demo script DEMO_PATH = os.path.join(os.path.dirname(__file__), "..", FILENAME) class TestListEditorDemo(unittest.TestCase): def test_list_editor_demo(self): demo = runpy.run_path(DEMO_PATH)["demo"] tester = UITester() with tester.create_ui(demo) as ui: custom_list = tester.find_by_id(ui, "custom") item1 = custom_list.locate(Index(1)) for _ in range(6): item1.perform(KeyClick("Backspace")) item1.perform(KeySequence("Othello")) item1.perform(KeyClick("Enter")) self.assertEqual( demo.play_list, ["The Merchant of Venice", "Othello", "MacBeth"], ) # Run the test(s) unittest.TextTestRunner().run( unittest.TestLoader().loadTestsFromTestCase(TestListEditorDemo) ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/examples/demo/Standard_Editors/tests/test_RangeEditor_demo.py0000644000175100001730000001543100000000000030146 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ This example demonstrates how to test interacting with the various styles of RangeEditor. The GUI being tested is written in the demo under the same name (minus the preceding 'test') in the outer directory. """ import os import runpy import unittest from traitsui.testing.api import ( DisplayedText, Index, KeyClick, KeySequence, MouseClick, Slider, Textbox, UITester, ) #: Filename of the demo script FILENAME = "RangeEditor_demo.py" #: Path of the demo script DEMO_PATH = os.path.join(os.path.dirname(__file__), "..", FILENAME) class TestRangeEditorDemo(unittest.TestCase): def test_run_demo(self): demo = runpy.run_path(DEMO_PATH)["demo"] tester = UITester() with tester.create_ui(demo) as ui: simple_small = tester.find_by_id(ui, 'simple_small') custom_small = tester.find_by_id(ui, 'custom_small') text_small = tester.find_by_id(ui, 'text_small') readonly_small = tester.find_by_id(ui, 'readonly_small') simple_medium = tester.find_by_id(ui, 'simple_medium') custom_medium = tester.find_by_id(ui, 'custom_medium') text_medium = tester.find_by_id(ui, 'text_medium') readonly_medium = tester.find_by_id(ui, 'readonly_medium') # Testing for SimpleSpinEditor is not supported yet so the simple # and custom styles for the large_range_int are not included here text_large = tester.find_by_id(ui, 'text_large') readonly_large = tester.find_by_id(ui, 'readonly_large') simple_float = tester.find_by_id(ui, 'simple_float') custom_float = tester.find_by_id(ui, 'custom_float') text_float = tester.find_by_id(ui, 'text_float') readonly_float = tester.find_by_id(ui, 'readonly_float') # Tests for the small_int_range ################################## simple_small_slider = simple_small.locate(Slider()) simple_small_slider.perform(KeyClick("Page Up")) self.assertEqual(demo.small_int_range, 2) simple_small_text = simple_small.locate(Textbox()) simple_small_text.perform(KeyClick("Backspace")) simple_small_text.perform(KeyClick("3")) simple_small_text.perform(KeyClick("Enter")) self.assertEqual(demo.small_int_range, 3) custom_small.locate(Index(0)).perform(MouseClick()) self.assertEqual(demo.small_int_range, 1) text_small.perform(KeyClick("0")) text_small.perform(KeyClick("Enter")) self.assertEqual(demo.small_int_range, 10) demo.small_int_range = 7 displayed_small = readonly_small.inspect(DisplayedText()) self.assertEqual(displayed_small, '7') # Tests for the medium_int_range ################################# simple_medium_slider = simple_medium.locate(Slider()) # on this range, page up/down corresponds to a change of 2. simple_medium_slider.perform(KeyClick("Page Up")) self.assertEqual(demo.medium_int_range, 3) simple_medium_text = simple_medium.locate(Textbox()) simple_medium_text.perform(KeyClick("Backspace")) simple_medium_text.perform(KeyClick("4")) simple_medium_text.perform(KeyClick("Enter")) self.assertEqual(demo.medium_int_range, 4) custom_medium_slider = custom_medium.locate(Slider()) custom_medium_slider.perform(KeyClick("Page Down")) self.assertEqual(demo.medium_int_range, 2) custom_medium_text = custom_medium.locate(Textbox()) custom_medium_text.perform(KeyClick("Backspace")) custom_medium_text.perform(KeyClick("1")) custom_medium_text.perform(KeyClick("Enter")) self.assertEqual(demo.medium_int_range, 1) text_medium.perform(KeyClick("0")) text_medium.perform(KeyClick("Enter")) self.assertEqual(demo.medium_int_range, 10) demo.medium_int_range = 7 displayed_medium = readonly_medium.inspect(DisplayedText()) self.assertEqual(displayed_medium, '7') # Tests for the large_int_range ################################## # Testing for SimpleSpinEditor is not supported yet text_large.perform(KeySequence("00")) text_large.perform(KeyClick("Enter")) self.assertEqual(demo.large_int_range, 100) demo.large_int_range = 77 displayed_large = readonly_large.inspect(DisplayedText()) self.assertEqual(displayed_large, '77') # Tests for the float_range ###################################### simple_float_slider = simple_float.locate(Slider()) # on this range, page up/down corresponds to a change of 1.000. simple_float_slider.perform(KeyClick("Page Up")) self.assertEqual(demo.float_range, 1.000) simple_float_text = simple_float.locate(Textbox()) for _ in range(3): simple_float_text.perform(KeyClick("Backspace")) simple_float_text.perform(KeyClick("5")) simple_float_text.perform(KeyClick("Enter")) self.assertEqual(demo.float_range, 1.5) custom_float_slider = custom_float.locate(Slider()) # after the trait is set to 1.5 above, the active range shown by # the LargeRangeSliderEditor for the custom style is [0,11.500] # so a page down is now a decrement of 1.15 custom_float_slider.perform(KeyClick("Page Down")) self.assertEqual(round(demo.float_range, 2), 0.35) custom_float_text = custom_float.locate(Textbox()) for _ in range(5): custom_float_text.perform(KeyClick("Backspace")) custom_float_text.perform(KeySequence("50.0")) custom_float_text.perform(KeyClick("Enter")) self.assertEqual(demo.float_range, 50.0) text_float.perform(KeyClick("5")) text_float.perform(KeyClick("Enter")) self.assertEqual(demo.float_range, 50.05) demo.float_range = 72.0 displayed_float = readonly_float.inspect(DisplayedText()) self.assertEqual(displayed_float, '72.0') # Run the test(s) unittest.TextTestRunner().run( unittest.TestLoader().loadTestsFromTestCase(TestRangeEditorDemo) ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/examples/demo/Standard_Editors/tests/test_TableEditor_demo.py0000644000175100001730000000271500000000000030142 0ustar00runnerdocker00000000000000""" This example demonstrates how to test interacting with a TableEditor. The GUI being tested is written in the demo under the same name (minus the preceding 'test') in the outer directory. """ import os import runpy import unittest from traitsui.testing.api import ( Cell, KeyClick, KeySequence, MouseClick, UITester ) from traitsui.tests._tools import requires_toolkit, ToolkitName #: Filename of the demo script FILENAME = "TableEditor_demo.py" #: Path of the demo script DEMO_PATH = os.path.join(os.path.dirname(__file__), "..", FILENAME) class TestTableEditorDemo(unittest.TestCase): @requires_toolkit([ToolkitName.qt]) def test_list_editor_demo(self): demo = runpy.run_path(DEMO_PATH)["demo"] tester = UITester() with tester.create_ui(demo) as ui: employees_table = tester.find_by_name(ui, "employees") # clicking a cell enters edit mode and selects full text cell_21 = employees_table.locate(Cell(2, 1)) cell_21.perform(MouseClick()) cell_21.perform(KeySequence("Jones")) cell_21.perform(KeyClick("Enter")) self.assertEqual(demo.employees[0].last_name, 'Jones') # third column corresponds to Full Name property cell_32 = employees_table.locate(Cell(3, 2)) cell_32.perform(MouseClick()) # Run the test(s) unittest.TextTestRunner().run( unittest.TestLoader().loadTestsFromTestCase(TestTableEditorDemo) ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/examples/demo/Standard_Editors/tests/test_TextEditor_demo.py0000644000175100001730000000465500000000000030044 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ This example demonstrates how to test interacting with a textbox created using TextEditor. The GUI being tested is written in the demo under the same name (minus the preceding 'test') in the outer directory. """ import os import runpy import unittest from traitsui.testing.api import DisplayedText, KeyClick, KeySequence, UITester #: Filename of the demo script FILENAME = "TextEditor_demo.py" #: Path of the demo script DEMO_PATH = os.path.join(os.path.dirname(__file__), "..", FILENAME) class TestTextEditorDemo(unittest.TestCase): def test_run_demo(self): demo = runpy.run_path(DEMO_PATH)["demo"] tester = UITester() with tester.create_ui(demo) as ui: simple_int_field = tester.find_by_id(ui, "simple_int") custom_int_field = tester.find_by_id(ui, "custom_int") text_int_field = tester.find_by_id(ui, "text_int") readonly_int_field = tester.find_by_id(ui, "readonly_int") # Modify the value to something invalid simple_int_field.perform(KeyClick("Backspace")) simple_int_field.perform(KeySequence("a")) # not a number! # Check the value has not changed self.assertEqual(demo.int_trait, 1) self.assertEqual(custom_int_field.inspect(DisplayedText()), "1") self.assertEqual(text_int_field.inspect(DisplayedText()), "1") self.assertEqual(readonly_int_field.inspect(DisplayedText()), "1") # Modify the value on the GUI to a good value simple_int_field.perform(KeyClick("Backspace")) simple_int_field.perform(KeySequence("2")) # Check self.assertEqual(demo.int_trait, 2) self.assertEqual(custom_int_field.inspect(DisplayedText()), "2") self.assertEqual(text_int_field.inspect(DisplayedText()), "2") self.assertEqual(readonly_int_field.inspect(DisplayedText()), "2") # Run the test(s) unittest.TextTestRunner().run( unittest.TestLoader().loadTestsFromTestCase(TestTextEditorDemo) ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/examples/demo/examples.cfg0000644000175100001730000000027700000000000021234 0ustar00runnerdocker00000000000000[global] destdir = TraitsUI [Traits UI Uber-demo] files = demo.py, traits_ui_demo.jpg, Advanced/*, Applications/*, Dynamic Forms/*, Extras/*, Standard Editors/*, Tools/*, Misc/*, images/* ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/examples/demo/index.rst0000644000175100001730000000003600000000000020567 0ustar00runnerdocker00000000000000.. image:: traits_ui_demo.jpg ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/examples/demo/traits_ui_demo.jpg0000644000175100001730000012430300000000000022443 0ustar00runnerdocker00000000000000JFIFC     C   " P(@rAT@QDPQ :e Aqt#R *( z/,>Ksi:G8n> dTPE@pJOؚpt)i;_ޣJ .Ik_|vO8r.zg;r5[JrxfWw>o-UQ [ؐqt=ԷO yaR /xуx]5M/~O3\*Ǔ}T߹|V~gzaox]R;5m+'y$_qy9#7P RFj9gRp J}=} /Q>&l_&_ϱ<O6O0&C|͖ۨW4aX|>o>;q˄wj"'0c^Ѩ:;YQz'O^/^LMz>y o*ns>y_9jIξOfa]"M~Ƕg;{&ȸMzƃW!(ahHTߤPQPAADQ@U4+AfsDD1A5s:9+0{D} %Wp0[Nr^w>g~_F}wnj[UAk|on3=vxvk߼|<8%DIr#tWrŘ (((x')b#c5[Ӽ\y\Uc<&n_+> -fa>1F *4\TZw5?Vǩ|nA߰@QAE#W!XH!QZj>֭Б隸eg.|oRZŷV+n9./G<<^?c9*#?=Weݟ.폗k]<=+iPt˰)qMqiz}'+Osb:ܝFS<7P8;C E$ckn>X~e$gNVJ̮ZXVVqyk}Ͻoz_~ I~f~:K+2KQ<P 5zM9'c)}-Vr7oNWO?G#GyЯjZ $dl{ESd?BXssib'ik@ pVoU +!jz3Lֵ'z|1@Q@ĿH*^C*K?/g./P;|wv( $BB[ u+<$l{ګ=[8yÕr}:9I^˹.Xᤍ0D1o= QoP ONpye{aq_929b8Z&ןcy/"_'?+_kr>;y HHt+]U,} ޅSjvKqfEV׌l{qM)9]XFm((kzlw*YA^X!:f\>qZeÙnO[y_S6}GT4z~o;9F84cJJzOƀ.ɞׯ:L'?Gz6-gqUF +u>wq"f(2%IbzgE? f\NiiMy!lj+ῧ*';osTYɩёYQ4aEא *+մ41jR..c?D7"}:^1S+VDS%E.`D="b ~$~82uDMwQ _L^1χ?A d[pu O|=7.QWgYjd-M%/\jʾ=s d  !y' lXN]Y5% N9^HxS+.OWZjf)2EuE䫕k U#ڹTHWÝ=sg-YHrm~5fW 2fĉ7m"ڹ _PNեT_HWz=u]6o^nv+5LKֳ$Y;TȞMvoú2ڝ]mqFe .>b%^ FXhfidM ^E8}Xr~ؙɠᡶ#T݊Zmt\s>i $H!'Tj;}F>9 ֘0`PUs%$/b dRNݥУ&hH!#U KCM-i vS)̚$iu  0`Eb#ֳ)j7QwFpAE_C_Kr`]e+qPa]Hy5 w?'a@ J#0`jK6KK-N圪z"2߈V$d^''bvY]l'erW95J 9?ezP ehsPn $-IL0`i 3*}CIG:KdG% *#墼WD߶rXryz *'kM44Q}94,7 F4<%c|P8 #1: ԅSpM}c.L0`heH.64\܍2IA a?C@?նJq'U!b bDԵ< PxjɍÈ CrLm#yC~&tm"ɵ5߂^ 0`i ilst43(4I)@ldF70G=Yg?rn>ԜRoT+Թ %`ұxy R]!P)$!p8>G֥G11~wK~0T[aT#X<$9n23Hᑸ@dJVd[~+_rBbͲkx}Nz$g qk!a$lѨc ^q)~"  >bs7H'3U4k.pBn-#kqR jCEC^^QdN\c0ˆ70C?rq|5`ĭ%`}L u!3'Ӗ3Do~b{A#  DAD 0}4̭[GoJeІGhl ܉g5!C~׽"_01r?Q.#Fȅu> XfW~,;4*Lu)㓲e>@>DhBӱ b 8 sP(E;p@ D(JN8J7&dH,dNB"B?//D{]_ji0KXn:xI(lFZ[aUrH99(4M?_Ht'Ҫa7@8BE.3a1jԠmw3dbFEx:  DAA@OSv{&X{`z46HLj5D_H^;fR$[h{unbk< kvJ[cY+F"ٿ뛆E\Z!RfjW[:+J%>L@"@ ERq <:T>MH8 fkCjXm@b.(^R$_~if =6&0P1<9W|$A8 m!j O:IC~'tt D" pB7# qJQ6IVChYs?h|OHH:7<0a$vN1HP̥ɬ& BT |-a5 `z@"@  L{<33RS+p5 (Ѵm^ Ye=+yXB,U8D14}S K1xiJȫ_ɳ:w="  1ܺ֍\]OѴmFсs10*0lP?*8G>ޓԲ<CkEo2H.siNI)(! -H@$H,,((0}mbd፪Lp8 @Lt"IIYN3zBMrDo^Y^m QCX\`I6#Scikc%K!3$qi2 @A$AFaF >89@2:7Y#222222&/219^ >oXmP?0d"UH´dfcl)qJ@?b< @BBH$H)AF`ϤFfBWjon> x }Ⱦ}_i@D[E'ډILd)[Rٚ#FRȒ1n!n1)ER܍՟GP?3Mq#{cM> gAn\21wnFn,09ilIim)A ݍկGEA^f:4ۤ8ؓ5Cx7 y$ @BBBBz tᚈl*!^xo,Ϙnm0BbiE~z h#\VtU܀̥?( I&@ʖ01 @ +K2f(0HJz22227 p "GP?#h6h6h\$$$$$#Ȫ/Ϸ#########p2 AF%"z8}>@ h6h6h6 y A!!!!!>EX3aBP#bϽ$ D4&eLfF3ZMŢ1.ʅ7 Pn7MO&NN^Q* TuҔ&rGBiܵNQbWoibz_d&ڝJ&,[(fzy2lmjTHHHHHG_FFFFFFFFFFz2 „FI,_>BڙNDOQP)mD|ʑzzleZ|S~z0v,K\fXK; %xzrۺ[v+sPsڜةsS/zVȐ 9[FܙHHHHOje}1bS`0H2222 "WϬ@"@ IIf(n6o*Ę/D= ܛ)r]qm~A$HIp8 Xä7CYlL0dRwKRQ(ֽjA׹FnĤq޲g2l6= @I23. D 6h6H4H2AD 2@ Fa&H @III$H$h6  @ `0 16!01A2@ "3PQaBpq$R?lߓ!'(M4=1s4rߩ&&s"aW=- ?uτh|' l> S_Uo#fV&p:ϼ,4)<^|I*C?Bݺ2;sƭ naS \F튮p4?{-eqyf8ò}s.S O/[Tnu1ݸN!eemtܰݵIrz7_V7kOji)@`" 7ibdk BrWUOiqf&Ld͗u*.Y󍠄g}>?@ 51 9'OJov0p)vA("`\.-[QO;R$QRlv:v$}SQ ݃nC~S ,R?ij$o<6@ -c]]-&BZ_%7 Q݂l9Ag5C"sXԪ*sK If.^JYz㯰ut9oFF~sL!b_vXKw ?t*ˆP=,CVc,ǞxP/tâ7`? :X8=]f6Y7ff b24YxiXk,֬֡#I bڃ6B6-Ph, >UZ llbmM1{Y'ebwB#t" t7+(u~Qe~SXZK !1AQq "2@a#03BRbr$P`sC%t4Sc5p?+ƪ!ii ($c p { '3>$xQ&gPڣXe&_;Ģb*J!*^;qZ8:8FJ1a~-pMk$ b6z|qn_٢l"=[5s({VbGcP20 lp1`86({W|׎IڤHE3surwHAekwYwuF!4Kk7P\ SbSZbc웬lE-$fI?* mCiCgyqOW^ٝ?~y7d݊ROIpcܥ-+;[wxR>,A_")-x՘BM JZ]MS`T.3}q*WsV _j=O g I(n2Gp[mFZwV5ͨHMh)xVXJej) 5QKgx\`ɮ uЃFCEPbJ='@R Z]vA"7/KWSbn(ֿ1ax_M6bP%{3B#loN 6s7(fuŦf/R+FbZkGcc|OGM7븊^աJ[=׮%„7ruGus+,ӱ/"#Uqfҏk;}VlzGWJإkspDNAZsl*]pTD5&]T1A+[ ƃlSe$o(x৔J9Һ2LNUB[$J1qT 9语"N$C@'@VZ"WݟQ7>.U:EJc%}WjكG?hw44j#8zn`̠Ƌ@ BWQtܭ$S0INt g4{ydX8|Yyvc\_pxel 6F6HjہP63]*>iB0B8]zޑ̧lhv7]+_ ֞l ) \>hxNE o(XM Qvy1D#cI!YL,)$M'3>qӑ<cFvUcbK;J'1d5sgD}[m缫Y٢m4ڻӂkȢ$6 ;ޢXҊ[Y.$V8(c+]f5*Y]Fm.k6;lrIbV=E);b m_Gf(ƍ jVGb3`Naʺ]ho @x&\~ӒgZr&g9U@kbVv~hLyK1o`;/FO V,pD>Pj#C]68d|ʻj-o̮g8%]n.U骣:#7&;di Mci+T&bȪ:,Lυs5p'+j,l!ʕV"k ?se;,J 4~@ RW+8wr v,fN@T5E9@B?Eu>A]nwh!wU;՛Y#tڎ+=-_U.a .Xk[c!A-^r%{kޱ?6Wʠh7)V~ڟ[*(N+/q{̓uI(rNA`Y"tؕ%bWIt8 ;i!cu7Od`fScnc}Kz ϨOnz W4~#)Ggb6N5q\Wwr 䎯޾($8R _RANKT@O s;?tBxSkyцW9Xի XcABwWωޥ!`A]l1 #8#`s8,ǬD=촕w9Q>Kr*t SnWq4L|åsBkCNEQ\lN!s]*hs-W!s>U Kw$+綞}l:4\z}߹;{IRQ5EvX/kFDiT^CUlDj !>i2<ԉ.ss)P.\v87*gpN$Ӱio q-j °̴GsqشkV"ek_[X8,]x{خ| 8+M5O\= ~Mj wkϑ^BCəwhGf GwV(lߕɗ[dWyRڟT4c74̽[`}Viy \nR@5ADvO#s {XUlakEͰM`~Or =Ǩ4{!= uZzʧy1 '@'8%;~65,#`>k/ߚƽXNpsn3HO衳ڝlopUuīL|AC&Ql& zіՏ˗:\$yN^5:`eO-?}>Eۥi)9. ʋ m9o|>.A yirw8|h4kA-vU9o$ΡrcNEӠYr>~>}F],XaK'v6֓`ĕYP軔xU^pv ȡqgE' ^N"wH#wnۋrhSl~gllދc}MtՑYe^N梳.ah4̦Tԡ%4 =wWIfŒ.[4Q]wj.C*>cބu0GY ]mbCm뒻]h MOVH;tr + &5ϪSދoJ(3;"-2\Uޙjqiλx6*[C BP ,^eh@7\j6L| 3V;,Ki"^+%eײ^pHK\i[2guڢ(]KԝǪ7T]s] aEe ڟVfD^ˆ7Ҙ$J~9y7SF#j7je42FSbt,1Vsty/aLT"l7pQ6kQBB#7x%"Q5вWCv'DzMiwj39@k5, b7􉪞Ӫk"Cw(L Ǧ,< 77zYl`n0H>bsVF맆4SG}TY&ſ/O-ص2cޛeȢ-{K\6#r9sM,+%2d~pʊ3⥜ 3ª~%X-(#G5-,jUceE5"/k5eq"S{0ˠ!l@nR^migy&m/kM IaܭRj#]c/wjXIs mG5r}b4v3dkMmSf\Xܼ+ "k\#tN*L݆\ddgUhH2 %]%d*Cر{s49 ]qN 69sF\M{P+G?418d^7^oo\Zwzob]Ī %9ANvx恾({\s^Mghq?+!1AQaq 0@P`?!_ZX]PFt\r-,ݼ%`*f$4f[xG Vl14|w#35„53-e@BF6%|g|Q1&Cpfϳ;lr6Nl10h/^u%vtDin1gBR ]!)|k(>GWiB+Uh{ Ff]j#}=^3͓oDSU GzwVŬlS]X?G[Iڼݦqp]1~kMZnާf8t¦\` W"-ҧܢm o7 "ѱkC\pʥ!11ݪә,%yCh4?ݣ o0Ǝ+N.sw ;L$5͌xg0vy79 .PNsYcNc{NaGPl L$a,CQSEDeD鬹~W~_ÑЌ6JY8(]C;|ׁƸZJ!+o'u_kmt2H`6r b$X[0 TիLnZ3PU@sT \OG1z k]RҲ/G>O6Um~׏Tlf6Et"]aekU   sܥ zCϾQ?+T\GH ց?ى>\nvTۇ\˿{`71'X_)VS8_JI]F&`*ig#uOcң,#lF"t09%ZNrZ i&8^[{ ȵ0 2=aHPT7Ku"ݒU5VK=Hј@] PŤ\pNRun !MJm65G0`F{&?BŠ"4@ r\%._w ҪN.8![Z`XCWnVq ֲ0IvW`u:)6< \on9'_rhnTVadv+2Yذ-e{R+Gyث?=9hSI"ԊȨ"F}~}fu-PەO4mbWɼ*9IAڍeAXkWUr @#\KJk=k~ s>Ăө_F]?~ -CeBΥd -r?%JYB" 2m'EϬ~Q)r>|%pAiM(tztcgR$HeV^J],0[sDӬ$tvscĴ|_[tBV7s"+42<7s2{  <*dOD/,6 rl7>j[8jגhW]a؇7 ~ܾ\#*Q݈{M5>d%hYJBl]`f,Yx4cˇQ ׿X ~Q"6lzx=.}▪ݒwESOld9u3^N7e((2j2x:"҃4ūTkh0 ezR(@ W,ueJ-]+v' q0LBa{YvcEA1CȺ@XN#cZůE|LVw֮?\puLiQ`'=^lEym1)NА4kmҪfLsv7&m0gkH3t2Z};?k/0̄H:@ԏčhy@QkW'tWR<3M{?L$c΍E1ʳ|L c\+yccsk ny?+^IEg`Hq/2ĥ[o)S̓{n>s5%AsDy) 0qUZ>c>D,! ~5h wy*m|MxASGjbzBT2q_<7u/j rr|8ǎ|BPNHR .ksdr!Y~zc߆MpCHy`i %kCX(M"6fL~~xc}cE.tJ?.hs5k;2D>:p/W=@ hf}u2mr 憭{4WcʓcrCե(F/?Tp9vXYZ\K55¥xcכ~>z2[xWme Z-~9ARy3K湱j 10)wmE]qbS1q#>y|.Ȏ.CxIbJ&'?H5dR 6wOY7YuI?S=6̓Sٖ1I;kO}ȤMz#C)44"kqv?Jpx/;JzbߜYXM2gFyg%b :3L$.V=5Z0,A% faņ/Znδ/.% 1W~P5߁7 |,@ Dgɫ>RJμ弡qhJ)]br~:ܡyATyJzir%rf?<<ÆI~[уQG&ӝ@gd "&oTOH5mtϴ}J @#DžkgNF7h:uD+L "i-trS |;, }UF-N,% yLsX uEyż,qs+cǃVRLC `,*aA͖sv K!ΒguD~fe I_̋=x;!j)w Jj!4,vјJ0A*!p<:[̝Q֨QMY`Q E5F]XRq!> A7^8\ɯo uM#+S350K`}\^x)O f_ @ xQc)UL'd_ }*wW MJl0LgYNՆ3=(x%LVt3eù~Ox#= :.}j?,5+1G۴p[`p+G)>pG!xLJS5hiVEu;@.uH0@9oⲂA [5fcG%q'NFߪȔm߹H5rb<~R@a<:?\}>Qa@{c1K!YU5e|#]CGhzYWt8 J JYnzNħ>S)d4s;(h~;h hu~d <:/ s _vU|9ߋ39W)CuBeuʠ@<@#1:>՚IfZs lE5j%!(sv/&E=ZF<4 K5#\.PP2)}ȃsH6䛿Zz}yxN?О 1rPHUޮf.!K+[˙2ue=n}kXhʈz<*tj YWh:n fT] H8EM5#xH2&yBELr^z1@r::zYj/bo3 \jlrJku`@NCH9YL'5.\qL&)hq:NШA8PfL䕸s15G9~G)>}MU?Ϗ  #yy¸G"84գЉ4JQqud)T>%M%P>՛5y{П}}&v}QD0 qE3?k)KlOcQk\L{Rŋ.\w5M <ϳ&iS-x_ܝ=b|mOsfr2h~*TIL7;yAOvP&2%q0 H/h^\k;J\d!BRu-Y/>x<蚱d/"8UL ėS)4;%ԁ'} yw/! a>^  !{~ XS3^Yx#|l9%볩RJ*,ޒ+4K; 6F2W{wCnRyCJ@AaaVŏzٔtw7=.eK*TR,B# y:5&SVayChU:*TBa!,XX: S"0b/65c;4c_f38S5 J*T^8ǖ9OHי5!by*T\!0 c,b6m6+Kc{y,*XL|/w4RJE^:KYqRJ`,XxuuHV[){*eq"KB_3Yn4_fbZ! = TAǑP򊧺WU83M))+="TR@A0bǍoNE4Hm{43| ,O{&;hypHqH&G $OavT{SyP YDI{gJCuMIt.-S}o{7E3,QT!A$IeJ*egFY|5pv B<H 0,*T>q;2 ?Dqf,O/8 lj`iZ4?+eW,*R2 e4: /\&-HHX,^X=ǻ_(ۊURVc+,<ݣ {NpaBU ht-}b;U}br*"aa6F[=#WXwX ]%ȜcFÜ{ܹhROhr}AZ9<&8~9~!G,a[ AB.d9Io_y40mȀr8QϪaX+-$,A,Uˡ+%uXckK")G`!c+o(r Xk6g8T& <0Vͽصh4 ٨"7*-MCq:~DJ*EHΆ-t09g*+(?V](jM E%]0,y_G7!>_’AI XmN;"NQ&xua#aLLjn&f:HB6MbD8/Up39sBy7 êUa Syc @<5nn]z7KUnufXS45yzB_QY +߼eYD\h-`mM p+X-/lD"Ռ[{#.)tEg-w77buJSJyGT*}Ew6'A L9{6βЃhٙǧ^̩@*6rV_ (]S;ah'Jo|E!xjZ*2~zGiPen-αLolR" E).F긯\yꯒ* Zkc=m\JeC[Ѣ Ct;hsZPmOsUUdE2Hp(pAԡ^~j#!8a`*q?LBb?  {{k$ᄁ< oϼ3Oo{Lyooq> z,2믾(>Yon8IZ8҂WPᆱ->8ӏz曆ﺺ!  boqH>G]~,n8/;᠞Jk850 M>џ@hbl?>~8n{HY(xeSlGZ8 x//+ȭ_A kŔgT8g싿yF3(i_YBݵoﶺ<~x&* AkyBG#`*,nm3c4p6Nƥ46+[!\oWN]9ݫe")(P6%rjywnj (tD"סZ"Hb>~i†c8!. @痱亗/S?&xq8*{x M5-4%+螣3vH 6b۫AXH;)k* 4TP #{gaYnXj.l)Ӊ " h{0G*9/U[<_rz- KAO*k5oI-/{b{k' 1EȆ)㢢[gՂ[[>9c*b.(/*!10AQa @qPp?1# f~ ט9} -o_̝>7[m+Pr~u|IGZeV8hލY=b hk+31;쩳aG״2h^ f8뉘><)N>'Rkf ;y%Vˊ :Sxm16V//ٔn_oK7⭄vb5qDT9NھDeDl_G[^UQ0|_*߅c0&[,WKGQ6 :Ap |ܨ*?,Mz#qxO8q3Wn g 7%0\-zǦ`(Rj[Ct~x.9#ݕ c(c\DZTFa)a SK7x?,`BT]G{ond A cT5@*DUsӓDj*_(7qFep[P'W)-Q(j܅fO Fʉ\V\åhNK^C&JMƠ-I[OMK}r˗.\rr*T11+Z%J*TIRJ*T_*!1A0Qa q@P?ᅉJش\ ʈK u 'Dʵv~&KHWł2M9p @ 1G j"YIL°~{v`:~ Ak_Ή Se:3drƨ0J%HTh9~?sNbQVe0tX!B-e :62`}ai* 67[X-9Sw) {#^f_GL%/ eX`/i]!bUNg Iv\Z@6Zblo2L@D Z7t୘bHwADXV@*b6e0@ xĮfϷ7hGpiՌk'k-ՈH WId,[jr|b[7~ aq/{*'4X1`."m4;ndDjD\Ϝ(5 ˗._p=tRkE`e~_ НbރGBxKbbh (n 1tl,~HifuSFWzژie˹=*eVadԆ6F]!":5^Cꔴ@؃`xӲol;NfM2Lr%'Q:i"\RH2 rXF[j?FKq%Wr_ R _;һlxTSFQzkӉ3QJri+1jp*:1A<ਣEǸV'/qq_Ȝ` x\uU+j Y[ 9~:ifKm )+apnm]?=7p̻mMȵ\)XPbS[x|88Ғ1()bDL Q8'f}j̏#uNL Ym6XwExwO^Q%aڐ@\` F;SAjz69p҉vDU~4. U#( 0x[ouaQA|..@<"%NyI30U78/(%+ZY۶' Ba_.B2#V˱ %rxFfP9^N` \Ro _p.$/P)o=D F0YPZH"L5߬OTEE{sɵ%]rV",(٧6ٓHrin)mB[ 'Q 7W?.z\K7V"WUno_HEJzWNq-h+RrPO,GMX= .UkV?o PͮS+ WK:#)Dcˁ]`HL3Cx; crZޓfT$ ha߈OMwbBC\yC!bZ[2B0gg8ѝ0YA'd?:w ;eѵ༼=LTMKA*&Tl|ywvW~=(]).Tw̗OPR{UýVHr+Fu6(u-Ӄkm:W0Уye49xm*1{s7CvZ[@yYH29X+XDڻ]F?%q H³WqDk')~PĪmz/xCNrE {g9Զ峺r< U+IURby Y7A0T.AcP^X&BV.&Ә ŧ,B.Q'rl9MWs\+s靜jJ Wfn-N1@dy+Tz80yB˪dID ]dE.% ߬JTsIX J -[$e'x$bx/Mö&Jٲ]#$RUB뤤[Ӄ ^kMit҄L!,Mw&JicX @ڈhGRaT; k ##"%@trr\6&z7ei >bb/Ơ~UPR"$ޗԴlmlIj*@*ijIOS Slڜ<0#b]AĆLˌ" !13-&/b@(R"2dȬ/PV4bV˴i&R`r`dM Ov]W3%P#92k34ac{&AyU- 3>aYMAΠR;N/n°4ǜDZK$Q-x4CK]&A1lhڐ(U]ePCe3XI QWPSaz8PȀ% G {XJmUB|rû[_C͞[NN~ҌPm" |r t;lT¤wTi}2/?CN1e[mH ޻;1WG 4,?DvU7{wUKEy^F/e L' "eUXop .M)QtKbQ%=]wpU*w(Xeux؁1x0bFX"E=m È~blҒN̩rz%F[h4֎XWQ6vӛ6B|,:œPN"D^`ø~\( @_y Dh<ΰ3J9Jը| p*kt .)ySN B6.qWlBޝqv>pPJ5QX L0G@]CLG00)d+S/f.G Պz&9B݊PŭTo&m(_x3cFSnب`+ۦ>Ѻ~z1OYA]TC@0 J1LvoDxmp .Aq-Ÿ|hgfV \7Esv]ݹUվTM:#z+r65zwׯ{WB8ap,fXN8C'_w7XK2!gqfloԨ^%-MWb=e_F C@<3o }1M [ĢyYe/ Pj. .;{w_ua}3~lZОry"\yc:<Aq(c:G"œBt^F/fDۗTP)()`Migr/Bؕ.Ƭgm3ATܼx|}"r\㵽ÉKڡhZ!Uyr+,uXRis`QY]bꤹXĭ58F -y}7.N Rֹe]Ee ᆼ % \ A^X6G׈ukܿӝ`)E+OcKqAx DהRQp](K_3۪%dZ^![n>Å96}0"?@.ýA(P ǤC#Q:?!|zFmk!qrI};LtyK(5l+uQT׼Y5+:-:a߈) h=;BP.KGP 3>RAUeZ|+ZAlJ@ :#<@ lv,~+sNyiJ-* '1NiVe _:ԣeغ6=]2;scqeе{@L50=w,E1Zf"ƋJ_h4Hkx@n+g3GA਩mn5#,PQ3pp蜒(1DQѓ{T(< HdÿC+98Ɔ(sS 2 x'q>7!Gy>#-aɋ!O)μq =,(?W t}ԨߣF9;CoA؛VV- @7@E|  v*oG1M%*P_)53k@UB;^w &X Dӟy+߻MQV^hh fhQ+)CoO6;r +#m꤇*Bi-kv*9Mq8"ދ|A6[})oflm;IUV`S7*1zkڍCTbrʨIAk9Juޓ|\RsJ_ B*%=?3Ho&ٿ &f?r &} /f"3t,װI 7R)!pEDdT*4oy#WվxI*%~ px&^-_2>LF)V4"{ETC˦R=FSX?\ȕ ?HP*[՝uh ol.0 :z41v-˻'5Y&npq /%Y^j3fc_\k[Dea{\^wg*TA^),vJ ^$ E+AGuIҭ׬|x.l'GUz|?6yҬ.+'+`dwNC_0Rx-.%y5-fY}f(0UK~%)rࢇ[.-Pw@(X1j7|u,7Gc6/ᄥJ꒯DK^FoUWx !?\u310E͎t`'rI~}F߸>\{̊|2D٬kS ߕKW2#kAy ~S HӒUKߒĺ`fʘ^洽kSUpA.6Gv_U/ kϕX&7,\ TrNh?Sˉm{h *|'D.c>C6C WOwcHepz:2CgAVN亼r=swH FX3 w|P>ٸ;s|8Id^!Rf > a.@~JfбF v ƹ$x_ݟk|2Dz-|ÐҏaZ &ZLj.^l׉Yx(pf({q7G>jO zt O1w'{QB}C\Y8@raP``Nr@_ -iĦŗ{|'wEbM+JݟSOEhmS EwS6[lk4 ̫G펆^)mݧ焣x#C8AYʓ얻4o#+إDǤ5S +@>l`N a@ga(lSsygU燚zKW5H'1EbqMkB\R(OqKs~8zf}MW tEVT=(HVu*'̬&F%N3ݎP F2Mi+Ἰ<ӼF W ઄7 GFjm(wVv ;*(u6O5D g>mXLBV }[rnjt)Kjhr9/V{hUF%w5~t[r bP wd\̢4oo^ cŢZ$7F[ǜ.d|Ɲ TTL1!(J;dc-W)&^LHe Q-ZҖQ OZ@N\/!P˃(F%[yPyt[W|)\3 A{}"}Pf pq}s,5pwfm/ Ew2bO=)+"j<\2T:,=c K/ .=10G^ rgk*E~7ec!r\;%@Ԍ)Nw6DpZkfZ:W"k6Ev*L|R8_ҁqGlvg-K[WApYS\5Mm[r|S KiROa !qp5CXr*6:@ǺyǾ6'WKVbU`|p K2/͖nppp Ρ*u>iQ[ 1 Hv=Y݊sYLJn3C<# :38WEL}}-k/B\؜ΟN|/KJ'^ulĉߔu; _߷]913neٙ cYGl @.4N̹C-M5P sݿ]zr[l  Rj#@a39',0뇓Qc ۅ~J nA&nTDVg>L_9D:ʜNYrL+2Д>X2B~A_۝؇d=pddav/F (a 䜼nYan-\qwgrwc{u9m/cce"bx&?0;3ĭ;l{x=ٝڕr,rEZ&0n,Gs⬲Fpfٝ=:1).!vKwhyc u#2Cͫ VE+l:2DD Ǥ Uc8l^@CzDnt/`EsԳ}~![3@4=t9`=۳%TȭsU(ťJcCExp%P8‰&Kj/3ʒ_~<23us4Jjifr-Av(`' &#=gGCHCܜ}`g.Lq SʌxV~]Kk:щg)ڎcNz|6^ϫ% dEە`pDA-nvjn N\ c TkZDӼs>"J>%MMs\ŭ~(v ?D# ?2 T?l4ɇ03K+|<DBZՙ h8m)d:ǾMBMZElHDd)B])2sНՁ UL_{,/ Q@ΘȾ Ff=ӭԛ)s%@['9Yr$]0N-X-jQt+Aj-:̠TjpJa4Z$s*yA'RwcD@M` )h}2J5ǻUX C!3 JE=t DZyb%s_T4qs;ЛJY~sJq=7*RHܱC\4NTLMDFgeGRR @%u4'ùTPT/.,`XF* tzbF?oHpt{BlylOv%[lݕl_7sm/3u-ۢ).[:\@(sG?Vv&ΑaźvaZ5Dՙ;ᨿ+rJ 0AW7!N8=ڔ,= $"g<Ş5wg?. !0A|++sG;2ŮjvNw~'&4`M\s././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1684768028.0478072 traitsui-8.0.0/examples/tutorials/0000755000175100001730000000000000000000000020031 5ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/examples/tutorials/default.css0000644000175100001730000000011100000000000022160 0ustar00runnerdocker00000000000000pre { border: 1px solid black; background-color: #E8E8E8; padding: 4px } ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1684768028.0518072 traitsui-8.0.0/examples/tutorials/doc_examples/0000755000175100001730000000000000000000000022474 5ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/examples/tutorials/doc_examples/default.css0000644000175100001730000000150000000000000024626 0ustar00runnerdocker00000000000000body { background-color: #FFFFFF; } h1 { font-family: Arial; font-size: 14pt; color: #303030; background-color: #FCD062; padding-top: 3px; padding-left:8px; padding-bottom: 3px; padding-right: 8px; } h2 { font-family: Arial; font-size: 12pt; color: #303030; background-color: #FCD062; padding-top: 3px; padding-left:8px; padding-bottom: 3px; padding-right: 8px; } pre { border: 1px solid #A0A0A0; background-color: #FDF7E7; padding: 4px; } dl { border: 1px solid #A0A0A0; background-color: #FDF7E7; padding-top: 4px; padding-bottom: 6px; padding-left: 8px; padding-right: 8px; } dl dl { border: 0px solid #A0A0A0; } dt { font-family: Arial; font-weight: bold; dd { padding-bottom: 10px; } ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/examples/tutorials/doc_examples/doc_examples.rst0000644000175100001730000000415600000000000025677 0ustar00runnerdocker00000000000000Documentation Examples ====================== This tutorial is simply a collection of the various examples taken from the Traits documentation. They are presented as a tutorial so that interested users can see the results of actually executing the example code, as well as to promote further study and exploration of various Traits topics in an interactive environment that allows easy modification and testing of changes to the code. So please feel free to explore the examples as your time, fancy and interest dictate. Have fun! A brief description of each of the examples presented in this tutorial follows: Add Class Traits Defining mutually-referring classes using the *add_class_trait* method. All Traits Features Shows the five primary features of the Traits package. Bad Self Ref Shows the incorrect way of defining a self-referencing class. Cast Simple Shows use of some of the simple casting types. Circular Definition Shows the incorrect way of defining mutually-referring classes. Compound Shows the definition of a compound trait. Custom TraitHandler Example of a custom TraitHandler Use Custom TraitHandler Example of using a custom TraitHandler Delegate Example of trait delegation. Delegate Prefix Examples of the Delegate() 'prefix' parameter. Delegation Notification Example of notification with delegation. Event Shows the definition of a trait event List Notifiers Example of zero-parameter handlers for an object containing a list Metadata Example of accessing trait metadata attributes Minimal Minimal example of using Traits. Override Default Example of overriding a default value for a trait attribute in a subclass Static Notification Example of static attribute notification This Example of This predefined trait Trait Reuse Example of reusing trait definitions Temp Wildcard Example of using a wildcard with a trait attribute name. All Wildcard Shows the trait attribute wildcard rules. Wildcard rules Example of trait attribute wildcard rules ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1684768028.0518072 traitsui-8.0.0/examples/tutorials/doc_examples/examples/0000755000175100001730000000000000000000000024312 5ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/examples/tutorials/doc_examples/examples/include_extra.py0000644000175100001730000000113000000000000027505 0ustar00runnerdocker00000000000000# (C) Copyright 2007-2023 Enthought, Inc., Austin, TX # License: BSD Style. # include_extra.py --- Example of Include object # provided for subclasses from traits.api import HasTraits, Int, Str from traitsui.api import Group, Include, View class Person(HasTraits): name = Str() age = Int() person_view = View('name', Include('extra'), 'age') class LocatedPerson(Person): street = Str() city = Str() state = Str() zip = Str() extra = Group('street', 'city', 'state', 'zip') Person().configure_traits() LocatedPerson().configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/examples/tutorials/doc_examples/examples/lesson.desc0000644000175100001730000000153200000000000026456 0ustar00runnerdocker00000000000000Examples from the Traits User Guide all_traits_features all_wildcard bad_self_ref circular_definition add_class_trait cast_simple compound custom_traithandler default_trait_ui delegate delegate_prefix disallow dynamic_notification enum_simple event explicit false graphic_example imageenumeditor include_extra instanceeditor instance_example keywords mapped map_simple minimal multiple_criteria object_trait_attrs override_default override_editor person_bmi range reuse_editor static_notification temp_wildcard this traitcasttype traitdict traitenum traitinstance traitlist traitmap traitprefixlist traitprefixmap traitrange traitstring traittuple traitype trait_reuse type_checked_methods type_simple user_custom_th use_custom_th validator view_attributes view_multi_object view_standalone widget wildcard wildcard_all wildcard_name wildcard_rules wizard ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/examples/tutorials/doc_examples/examples/override_editor.py0000644000175100001730000000056400000000000030056 0ustar00runnerdocker00000000000000# (C) Copyright 2007-2023 Enthought, Inc., Austin, TX # License: BSD Style. # override_editor.py --- Example of overriding a trait # editor from traits.api import HasTraits, Trait from traitsui.api import Color, ColorEditor class Polygon(HasTraits): line_color = Trait(Color((0, 0, 0)), editor=ColorEditor()) Polygon().configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/examples/tutorials/doc_examples/examples/view_attributes.py0000644000175100001730000000072300000000000030106 0ustar00runnerdocker00000000000000# (C) Copyright 2007-2023 Enthought, Inc., Austin, TX # License: BSD Style. # view_attributes.py --- Example of a view as an # attribute of a class from traits.api import HasTraits, Int, Str, Trait from traitsui.api import View class Person(HasTraits): first_name = Str() last_name = Str() age = Int() gender = Trait(None, 'M', 'F') name_view = View('first_name', 'last_name') bill = Person() bill.configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/examples/tutorials/doc_examples/examples/view_multi_object.py0000644000175100001730000000215500000000000030401 0ustar00runnerdocker00000000000000# (C) Copyright 2007-2023 Enthought, Inc., Austin, TX # License: BSD Style. # view_multi_object.py --- Example of a view for # editing multiple objects import wx from traits.api import HasTraits, Str from traitsui.api import View class Person(HasTraits): first_name = Str() last_name = Str() class Company(HasTraits): company_name = Str() # Standalone View object referencing objects in the UI context employee_view = View('e.first_name', 'e.last_name', 'c.company_name') bill = Person(first_name='Bill') acme = Company(company_name='Acme Products') class TraitApp(wx.App): def __init__(self, obj1, obj2, view): self.obj1 = obj1 self.obj2 = obj2 self.view = view wx.InitAllImageHandlers() wx.App.__init__(self, 1, 'debug.log') self.MainLoop() def OnInit(self): # This is the call to the ui() method, which includes a # context dictionary ui = self.view.ui({'e': self.obj1, 'c': self.obj2}) self.SetTopWindow(ui.control) return True # Main program: TraitApp(bill, acme, employee_view) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/examples/tutorials/doc_examples/examples/view_standalone.py0000644000175100001730000000174500000000000030055 0ustar00runnerdocker00000000000000# (C) Copyright 2007-2023 Enthought, Inc., Austin, TX # License: BSD Style. # view_standalone.py --- Example of a view as a # standalone object import wx from traits.api import HasTraits, Int, Str, Trait from traitsui.api import View class Person(HasTraits): first_name = Str() last_name = Str() age = Int() gender = Trait(None, 'M', 'F') name_view = View('first_name', 'last_name') # Note that person_view is a standalone object. person_view = View('first_name', 'last_name', 'age', 'gender') bill = Person() class TraitApp(wx.App): def __init__(self, object, view): self.object = object self.view = view wx.InitAllImageHandlers() wx.App.__init__(self, 1, 'debug.log') self.MainLoop() def OnInit(self): # This is the call to the ui() method. ui = self.view.ui(self.object) self.SetTopWindow(ui.control) return True # Main program: TraitApp(bill, person_view) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/examples/tutorials/doc_examples/lesson.desc0000644000175100001730000000006500000000000024640 0ustar00runnerdocker00000000000000The Traits Documentation Examples Tutorial examples ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1684768028.0518072 traitsui-8.0.0/examples/tutorials/traitsui_4.0/0000755000175100001730000000000000000000000022256 5ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/examples/tutorials/traitsui_4.0/buttons.py0000644000175100001730000000474100000000000024334 0ustar00runnerdocker00000000000000# (C) Copyright 2007-2023 Enthought, Inc., Austin, TX # License: BSD Style. # --(View Default Button Changes)------------------------------------------ """ View Default Button Changes =========================== For the last year or so, use of the following **View** traits for managing the default buttons displayed at the bottom of a view has been deprecated: - apply - revert - undo - ok - cancel Use of these traits has been supplanted by use of the *buttons* trait instead. As part of the ongoing phasing out of these traits, the following changes have been implemented in Traits 3.0: - All use of the *apply*, *revert*, *undo*, *ok* and *cancel* traits have been removed from views contained within the traits package itself, and have been replaced with the *buttons* trait. - The default value for each of the deprecated traits has been changed from **True** to **False**. While use of the deprecated **View** traits is still allowed at the moment, the affect of these changes could cause changes in behavior within existing code that has not yet removed references to the deprecated traits. In particular, the most likely side effect is for some or all of the default **View** buttons to disappear from views which are implicitly relying on the default values for each of the deprecated traits. Views which explicitly set the deprecated **View** traits or use the newer *buttons* trait should not be affected. The correct fix for any **View** which has buttons disappear after installing Traits 3.0 is to add a *buttons* trait with the correct value set to the **View**. Note that in a future release, the deprecated view traits will actually be removed from the **View** class. """ # ---------------------------------------------------------------- from traits.api import Float, HasTraits, Property from traitsui.api import Item, View # --[Adder Class]---------------------------------------------------------- # Click the run button to view the pop-up dialog... class Adder(HasTraits): value_1 = Float() value_2 = Float() sum = Property(observe=['value_1', 'value_2']) view = View( Item('value_1'), Item('value_2'), '_', Item('sum', style='readonly'), title='Adding Machine', buttons=['OK'], ) def _get_sum(self): return self.value_1 + self.value_2 # ---------------------------------------------------------------- popup = Adder() if __name__ == '__main__': popup.configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/examples/tutorials/traitsui_4.0/default.css0000644000175100001730000000150000000000000024410 0ustar00runnerdocker00000000000000body { background-color: #FFFFFF; } h1 { font-family: Arial; font-size: 14pt; color: #303030; background-color: #FCD062; padding-top: 3px; padding-left:8px; padding-bottom: 3px; padding-right: 8px; } h2 { font-family: Arial; font-size: 12pt; color: #303030; background-color: #FCD062; padding-top: 3px; padding-left:8px; padding-bottom: 3px; padding-right: 8px; } pre { border: 1px solid #A0A0A0; background-color: #FDF7E7; padding: 4px; } dl { border: 1px solid #A0A0A0; background-color: #FDF7E7; padding-top: 4px; padding-bottom: 6px; padding-left: 8px; padding-right: 8px; } dl dl { border: 0px solid #A0A0A0; } dt { font-family: Arial; font-weight: bold; dd { padding-bottom: 10px; } ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/examples/tutorials/traitsui_4.0/deferred.py0000644000175100001730000000516600000000000024420 0ustar00runnerdocker00000000000000# (C) Copyright 2007-2023 Enthought, Inc., Austin, TX # License: BSD Style. # --(Deferred UI Notifications)-------------------------------------------- """ Deferred UI Notifications ========================= In Traits 3.0, a change has been made to the way that events are handled in the Traits UI that should improve the performance and responsive of Traits-based user interfaces in certain important situations. In particular, changes made to the underlying model being displayed by a Traits UI are no longer reflected immediately in the user interface, but are instead queued up to be processed by the UI thread at its first available opportunity. More precisely, the first time that a user interface related model trait is modified, an event requesting a user interface update is generated containing the old and new values of the trait. Subsequent changes to the same model trait result in no new events being generated, but instead simply update the original event information with the latest value of the modified trait. Eventually the UI thread will process the original event, at which point it will update the user interface using the original old value of the trait along with the latest new value. Although this may sound like it should slow down user interface updates, in many cases where a model is being rapidly updated by calculations running either on a background or UI thread, it should actually appear to make the system more responsive, and should in fact, help prevent or reduce situations where the user interface would previously have appeared to be unresponsive due to an excessive number of screen updates. """ # ---------------------------------------------------------------- from traits.api import Button, HasTraits, Int from traitsui.api import Item, View # --[Count Class]---------------------------------------------------------- class Count(HasTraits): count = Int() go = Button('Count') view = View(Item('count', style='readonly'), Item('go', show_label=False)) def _go_changed(self): # Even though the 'count' trait (which is visible in the UI) is being # rapidly updated here, the UI should show only a single update each # time the 'Count' button is clicked. In previous Traits versions, the # user would actually see the counter update sequentially through all # 10,000 values, during which time the user interface would be # unresponsive: for i in range(10000): self.count += 1 # --------------------------------------------------------------- demo = Count() if __name__ == '__main__': demo.configure_traits() ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1684768028.0518072 traitsui-8.0.0/examples/tutorials/traitsui_4.0/editors/0000755000175100001730000000000000000000000023727 5ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/examples/tutorials/traitsui_4.0/editors/animated_gif.py0000644000175100001730000000460500000000000026715 0ustar00runnerdocker00000000000000# --(Animated GIF Editor)-------------------------------------------------- """ Animated GIF Editor =================== In Traits 3.0, a new **AnimatedGIFEditor** has been added to the Traits UI package. The purpose of the editor is to allow inclusion of simple animated graphics into a traits UI via the use of animated GIF files. The traits supported by the **AnimatedGIFEditor** editor are as follows: playing A string that specifies the extended name of a trait that specifies whether the animated GIF file is playing or not. If not specified, the default is to play the animated GIF file endlessly. The value associated with **AnimatedGIFEditor** should be the name of the animated GIF image file to be displayed. No user editing of the value is provided by this editor, it is display only. Notes: - This demo only works on the wx backend. """ # --[Imports]-------------------------------------------------------------- from os.path import join, dirname from traits.api import HasTraits, File, Bool, Int from traitsui.api import View, VGroup, HGroup, Item, EnumEditor from traitsui.wx.animated_gif_editor import AnimatedGIFEditor # --[Setup]---------------------------------------------------------------- # Some sample animated GIF files: import traitsui as ui base_path = join(dirname(ui.__file__), 'demo', 'Extras', 'images') # Get the names of the animated GIF files that can be displayed: files = [ join(base_path, 'logo_64x64.gif'), join(base_path, 'logo_48x48.gif'), join(base_path, 'logo_32x32.gif'), ] # --[AnimatedGIFDemo Class]------------------------------------------------ class AnimatedGIFDemo(HasTraits): # The animated GIF file to display: gif_file = File(files[0]) # Is the animation playing or not? playing = Bool(True) # The traits view: view = View( VGroup( HGroup( Item( 'gif_file', editor=AnimatedGIFEditor(playing='playing'), show_label=False, ), Item('playing'), ), '_', Item( 'gif_file', label='GIF File', editor=EnumEditor(values=files) ), ), title='Animated GIF Demo', resizable=True, buttons=['OK'], ) # --------------------------------------------------------------- demo = AnimatedGIFDemo() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/examples/tutorials/traitsui_4.0/editors/flash.py0000644000175100001730000000437100000000000025403 0ustar00runnerdocker00000000000000# --(Flash Editor (Windows Only))------------------------------------------ """ Flash Editor (Windows Only) =========================== In Traits 3.0, a new **FlashEditor** has been added to the Traits UI package. The editor allows displaying and interacting with Adobe Flash compatible files. This editor is currently only available for the Windows platform and is located in the wxPython version of the Traits UI in the *traitsui.wx.extras.windows* package. The purpose of the *extras.windows* package is to provide a location for editors which may be toolkit and Windows platform specific, and not necessarily available in all Traits UI toolkit packages or platforms. The **FlashEditor** has no developer settable traits. The value edited by a **FlashEditor** should be a string containing either the URL or file name of the Flash file to display. This is a *read only* value that is not modified by the editor. Changing the value causes the editor to display the Flash file defined by the new value of the trait. """ # --[Imports]-------------------------------------------------------------- from traitsui.wx.extra.windows.flash_editor import FlashEditor from traits.api import HasTraits, Enum from traitsui.api import View, HGroup, Item # --[FlashDemo Class]------------------------------------------------------ class FlashDemo(HasTraits): # The Flash file to display: flash = Enum( 'http://www.ianag.com/arcade/swf/sudoku.swf', 'http://www.ianag.com/arcade/swf/f-336.swf', 'http://www.ianag.com/arcade/swf/f-3D-Reversi-1612.swf', 'http://www.ianag.com/arcade/swf/game_234.swf', 'http://www.ianag.com/arcade/swf/flashmanwm.swf', 'http://www.ianag.com/arcade/swf/2379_gyroball.swf', 'http://www.ianag.com/arcade/swf/f-1416.swf', 'http://www.ianag.com/arcade/swf/mah_jongg.swf', 'http://www.ianag.com/arcade/swf/game_e4fe4e55fedc2f502be627ee6df716c5.swf', 'http://www.ianag.com/arcade/swf/rhumb.swf', ) # The view to display: view = View( HGroup(Item('flash', label='Pick a game to play')), '_', Item('flash', show_label=False, editor=FlashEditor()), ) # --------------------------------------------------------------- demo = FlashDemo() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/examples/tutorials/traitsui_4.0/editors/ie_html.py0000644000175100001730000001443700000000000025733 0ustar00runnerdocker00000000000000# --(Internet Explorer HTML Editor (Windows Only))------------------------- """ Internet Explorer HTML Editor (Windows Only) ============================================ In Traits 3.0, a new **IEHTMLEditor** has been added to the Traits UI package. The editor allows displaying (but not editing) HTML pages using the Microsoft Internet Explorer browser. This editor is currently only available for the Windows platform and is located in the wxPython version of the Traits UI in the *traitsui.wx.extras.windows* package. The purpose of the *extras.windows* package is to provide a location for editors which may be toolkit and Windows platform specific, and not necessarily available in all Traits UI toolkit packages or platforms. The traits supported by the **IEHTMLEditor** editor are as follows: home A string specifying the optional extended name of a trait event used to tell Internet Explorer to display the user's home page. Set the specified trait to **True** to cause the browser to display the user's home page. back A string specifying the optional extended name of a trait event used to tell Internet Explorer to display the previous browser page. Set the specified trait to **True** to cause the browser to display the previous browser page. forward A string specifying the optional extended name of a trait event used to tell Internet Explorer to display the next (i.e. forward) browser page. Set the specified trait to **True** to cause the browser to display the next browser page (if available). stop A string specifying the optional extended name of a trait event used to tell Internet Explorer to stop loading the current page. Set the specified trait to **True** to cause the browser to stop loading the current page. refresh A string specifying the optional extended name of a trait event used to tell Internet Explorer to refresh the current page. Set the specified trait to **True** to cause the browser to refresh the current page. search A string specifying the optional extended name of a trait event used to tell Internet Explorer to initiate a search of the current page. Set the specified trait to **True** to cause the browser to start a search of the current page. status A string specifying the optional extended name of a trait used contain the current Internet Explorer status. This trait is automatically updated by the browser as its internal status changes. title A string specifying the optional extended name of a trait used contain the current Internet Explorer page title. This trait is automatically updated by the browser as the current page title is changed. page_loaded A string specifying the optional extended name of a trait used contain the URL of the current Internet Explorer page. This trait is automatically updated by the browser as a page is loaded. html A string specifying the optional extended name of a trait used to get or set the HTML page content of the current Internet Explorer page. This trait is automatically updated by the browser as a page is loaded, and can also be set by an application to cause the browser to display the content provided. The value edited by an **IEHTMLEditor** should be a string containing either the URL or file name of the file that Internet Explorer should display. This is a *read only* value that is not modified by the editor. Changing the value causes the browser to display the page defined by the new value of the trait. """ # --[Imports]-------------------------------------------------------------- from traitsui.wx.extra.windows.ie_html_editor import IEHTMLEditor from traits.api import HasTraits, Str, List, Button from traitsui.api import ( View, VGroup, HGroup, Item, TextEditor, ListEditor, spring, ) # --[WebPage Class]-------------------------------------------------------- class WebPage(HasTraits): # The URL to display: url = Str('http://code.enthought.com') # The page title: title = Str() # The page status: status = Str() # The browser navigation buttons: back = Button('<--') forward = Button('-->') home = Button('Home') stop = Button('Stop') refresh = Button('Refresh') search = Button('Search') # The view to display: view = View( HGroup( 'back', 'forward', 'home', 'stop', 'refresh', 'search', '_', Item('status', style='readonly'), show_labels=False, ), Item( 'url', show_label=False, editor=IEHTMLEditor( home='home', back='back', forward='forward', stop='stop', refresh='refresh', search='search', title='title', status='status', ), ), ) # --[InternetExplorerDemo Class]------------------------------------------- class InternetExplorerDemo(HasTraits): # A URL to display: url = Str('http://') # The list of web pages being browsed: pages = List(WebPage) # The view to display: view = View( VGroup( Item( 'url', label='Location', editor=TextEditor(auto_set=False, enter_set=True), ) ), Item( 'pages', show_label=False, style='custom', editor=ListEditor( use_notebook=True, deletable=True, dock_style='tab', export='DockWindowShell', page_name='.title', ), ), ) # Event handlers: def _url_changed(self, url): self.pages.append(WebPage(url=url.strip())) # --(Demo Notes)----------------------------------------------------------- """ Demo Notes ========== - Try dragging one of the browser tabs completely out of the tutorial window and see what happens... - Then, just for fun, try dragging it back... """ # --------------------------------------------------------------- demo = InternetExplorerDemo( pages=[ WebPage(url='http://code.enthought.com/traits/'), WebPage(url='http://dmorrill.com'), ] ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/examples/tutorials/traitsui_4.0/editors/led.py0000644000175100001730000000762700000000000025061 0ustar00runnerdocker00000000000000# --(LED Editor)----------------------------------------------------------- """ LED Editor ========== In Traits 3.0, a new **LEDEditor** has been added to the Traits UI package. The editor allows displaying (but not editing) numeric values using a set of simulated LEDs. This editor is currently only available in the wxPython version of the Traits UI in the *traitsui.wx.extras* package. The purpose of the *extras* package is to provide a location for editors which may be toolkit specific, and not necessarily available in all Traits UI toolkit packages. The traits supported by the **LEDEditor** editor are as follows: alignment Specifies the alignment of the numeric text within the control. The possible values are: *right* (the default), *left* and *center*. The value edited by an **LEDEditor** should be an integer or float value, or a string value containing only characters that would be found in an interger or float value. """ # --[Imports]-------------------------------------------------------------- from threading import Thread from time import sleep from traits.api import HasTraits, Instance, Int, Float, Bool from traitsui.api import View, Item, HGroup, Handler, UIInfo, spring from traitsui.wx.extra.led_editor import LEDEditor # --[LEDDemoHandler Class]------------------------------------------------- # Handler class for the LEDDemo class view: class LEDDemoHandler(Handler): # The UIInfo object associated with the UI: info = Instance(UIInfo) # Is the demo currently running: running = Bool(True) # Is the thread still alive? alive = Bool(True) def init(self, info): self.info = info Thread(target=self._update_counter).start() return True def closed(self, info, is_ok): self.running = False while self.alive: sleep(0.05) def _update_counter(self): while self.running: self.info.object.counter1 += 1 self.info.object.counter2 += 0.001 sleep(0.01) self.alive = False # --[LEDDemo Class]-------------------------------------------------------- # The main demo class: class LEDDemo(HasTraits): # A counter to display: counter1 = Int() # A floating point value to display: counter2 = Float() # The traits view: view = View( Item( 'counter1', label='Left aligned', editor=LEDEditor(alignment='left'), ), Item( 'counter1', label='Center aligned', editor=LEDEditor(alignment='center'), ), Item( 'counter1', label='Right aligned', editor=LEDEditor(), # default = 'right' aligned ), Item( 'counter2', label='Float value', editor=LEDEditor(format_str='%.3f'), ), '_', HGroup( Item( 'counter1', label='Left', height=-40, width=120, editor=LEDEditor(alignment='left'), ), spring, Item( 'counter1', label='Center', height=-40, width=120, editor=LEDEditor(alignment='center'), ), spring, Item( 'counter1', label='Right', height=-40, width=120, editor=LEDEditor(), # default = 'right' aligned ), spring, Item( 'counter2', label='Float', height=-40, width=120, editor=LEDEditor(format_str='%.3f'), ), ), title='LED Editor Demo', buttons=['OK'], handler=LEDDemoHandler, ) # --------------------------------------------------------------- demo = LEDDemo() ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1684768028.0518072 traitsui-8.0.0/examples/tutorials/traitsui_4.0/editors/tabular_editor/0000755000175100001730000000000000000000000026727 5ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/examples/tutorials/traitsui_4.0/editors/tabular_editor/numpy_array.py0000644000175100001730000000543200000000000031653 0ustar00runnerdocker00000000000000# --(NumPy Array Example)-------------------------------------------------- """ This lesson demonstrates how the **TabularEditor** can be used to display (large) NumPy arrays. In this example, the array consists of 100,000 random 3D points from a unit cube. In addition to showing the coordinates of each point, the example code also displays the index of each point in the array, as well as a red flag if the point lies within 0.25 of the center of the cube. As with the other tabular editor tutorials, this example shows how to set up a **TabularEditor** and create an appropriate **TabularAdapter** subclass. In this case, it also shows: - An example of using array indices as *column_id* values. - Using the *format* trait to format the numeric values for display. - Creating a *synthetic* index column for displaying the point's array index (the *index_text* property), as well as a flag image for points close to the cube's center (the *index_image* property). """ # ---------------------------------------------------------------- from os.path import join, dirname from numpy import sqrt from numpy.random import random from traits.api import HasTraits, Property, Array from traitsui.api import View, Item, TabularEditor from traitsui.tabular_adapter import TabularAdapter from traitsui.menu import NoButtons from pyface.image_resource import ImageResource # -------------------------------------------------------------- # Necessary because of the dynamic way in which the demos are loaded: import traitsui.api search_path = [join(dirname(traitsui.api.__file__), 'demo', 'Advanced')] # --[Tabular Adapter Definition]------------------------------------------- class ArrayAdapter(TabularAdapter): columns = [('i', 'index'), ('x', 0), ('y', 1), ('z', 2)] font = 'Courier 10' alignment = 'right' format = '%.4f' index_text = Property() index_image = Property() def _get_index_text(self): return str(self.row) def _get_index_image(self): x, y, z = self.item if sqrt((x - 0.5) ** 2 + (y - 0.5) ** 2 + (z - 0.5) ** 2) <= 0.25: return 'red_flag' return None # --[Tabular Editor Definition]-------------------------------------------- tabular_editor = TabularEditor( adapter=ArrayAdapter(), images=[ImageResource('red_flag', search_path=search_path)], ) # --[ShowArray Class]------------------------------------------------------ class ShowArray(HasTraits): data = Array view = View( Item('data', editor=tabular_editor, show_label=False), title='Array Viewer', width=0.3, height=0.8, resizable=True, buttons=NoButtons, ) # --[Example Code*]-------------------------------------------------------- demo = ShowArray(data=random((100000, 3))) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/examples/tutorials/traitsui_4.0/editors/tabular_editor/python_source_browser.py0000644000175100001730000001247600000000000033757 0ustar00runnerdocker00000000000000# --(Python Source Browser Example)---------------------------------------- """ This lesson shows a combination of the **DirectoryEditor**, the **TabularEditor** and the **CodeEditor** used together to create a very simple Python source browser. In the **Demo** tab you can: - Use the **DirectoryEditor** on the left to navigate to and select directories containing Python source files. - Use the **TabularEditor** on the top-right to view information about and to select Python source files in the currently selected directory. - View the currently selected Python source file's contents in the **CodeEditor** in the bottom-right. As an extra *feature*, the **TabularEditor** also displays a: - Red ball if the file size > 64KB. - Blue ball if the file size > 16KB. As with the *Single and Married Person Example* tutorial, this example shows you how to: - Set up a **TabularEditor**. - Define a **TabularAdapter** subclass that meets the display requirements of the application. In this example, please note the use of the *even_bg_color* trait in the **FileInfoAdapter** adapter class to set up alternating line colors in the table for improved readability. Also note that the *name*, *size*, *time* and *date* columns define *column_id* values which correspond directly with traits defined in the **FileInfo** class, but the *big* column id is an artifical column defined to display the file size related *blue ball* and *red ball* images when the file size exceeds various thresholds. The column id is used simply to provide a name reference for the related trait and property definitions in the adapter class itself. """ # ---------------------------------------------------------------- import traits import traitsui import wx from time import localtime, strftime from os import listdir from os.path import ( getsize, getmtime, isfile, join, splitext, basename, dirname, ) from traits.api import ( HasPrivateTraits, Str, Float, List, Directory, File, Code, Instance, Property, cached_property, ) from traitsui.api import View, Item, HSplit, VSplit, TabularEditor from traitsui.tabular_adapter import TabularAdapter from pyface.image_resource import ImageResource # -------------------------------------------------------------- # Necessary because of the dynamic way in which the demos are loaded: search_path = [join(dirname(traitsui.api.__file__), 'demo', 'Applications')] # --[FileInfo Class]------------------------------------------------------- class FileInfo(HasPrivateTraits): file_name = File() name = Property() size = Property() time = Property() date = Property() @cached_property def _get_name(self): return basename(self.file_name) @cached_property def _get_size(self): return getsize(self.file_name) @cached_property def _get_time(self): return strftime('%I:%M:%S %p', localtime(getmtime(self.file_name))) @cached_property def _get_date(self): return strftime('%m/%d/%Y', localtime(getmtime(self.file_name))) # --[FileInfoAdapter Class]------------------------------------------------ class FileInfoAdapter(TabularAdapter): columns = [ ('File Name', 'name'), ('Size', 'size'), ('', 'big'), ('Time', 'time'), ('Date', 'date'), ] even_bg_color = wx.Colour(201, 223, 241) font = 'Courier 10' size_alignment = Str('right') time_alignment = Str('right') date_alignment = Str('right') big_text = Str() big_width = Float(18) big_image = Property() def _get_big_image(self): size = self.item.size if size > 65536: return 'red_ball' return (None, 'blue_ball')[size > 16384] # --[Tabular Editor Definition]-------------------------------------------- tabular_editor = TabularEditor( editable=False, selected='file_info', adapter=FileInfoAdapter(), operations=[], images=[ ImageResource('blue_ball', search_path=search_path), ImageResource('red_ball', search_path=search_path), ], ) # --[PythonBrowser Class]-------------------------------------------------- class PythonBrowser(HasPrivateTraits): dir = Directory() files = List(FileInfo) file_info = Instance(FileInfo) code = Code() view = View( HSplit( Item('dir', style='custom'), VSplit( Item('files', editor=tabular_editor), Item('code', style='readonly'), show_labels=False, ), show_labels=False, ), resizable=True, width=0.75, height=0.75, ) # -- Event Handlers ------------------------------------------------------- def _dir_changed(self, dir): self.files = [ FileInfo(file_name=join(dir, name)) for name in listdir(dir) if ((splitext(name)[1] == '.py') and isfile(join(dir, name))) ] def _file_info_changed(self, file_info): fh = None try: fh = open(file_info.file_name, 'rb') self.code = fh.read() except: pass if fh is not None: fh.close() # --[Example*]------------------------------------------------------------- demo = PythonBrowser(dir=dirname(traits.api.__file__)) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/examples/tutorials/traitsui_4.0/editors/tabular_editor/sm_person_example.py0000644000175100001730000001775200000000000033035 0ustar00runnerdocker00000000000000# --(Single/Married Person Example)---------------------------------------- """ This lesson contains a tabular editor example based upon the **Person** and **MarriedPerson** example presented in the *Tabular Editor Introduction* tutorial. This example defines three main classes: - **Person**: A single person. - **MarriedPerson**: A married person (a subclass of **Person**). - **Report**: A report based on a list of single and married people. The example code creates a tabular display of 10,000 single and married people showing the following information: - Name of the person. - Age of the person. - The person's address. - The name of the person's spouse (if any). In addition: - It uses a Courier 10 point font for each line in the table. - It displays the age column right, instead of left, justified. - If a person is a minor (age < 18) and married, it displays a red flag image in the age column. - If the person is married, it makes the background color for that row a light blue. This example demonstrates: - How to set up a **TabularEditor**. - The display speed of the **TabularEditor**. - How to create a **TabularAdapter** that meets each of the specified display requirements. Additional notes: - You can change the current selection using the up and down arrow keys. - You can move a selected row up and down in the table using the left and right arrow keys. """ # ---------------------------------------------------------------- from os.path import join, dirname from random import randint, choice, shuffle from traits.api import HasTraits, Str, Int, List, Instance, Property, Constant from traitsui.api import View, Group, Item, Margin, TabularEditor, Color from traitsui.tabular_adapter import TabularAdapter from traitsui.menu import NoButtons from pyface.image_resource import ImageResource # -------------------------------------------------------------- # Necessary because of the dynamic way in which the demos are loaded: import traitsui.api search_path = [join(dirname(traitsui.api.__file__), 'demo', 'Advanced')] # --[Person Class]--------------------------------------------------------- class Person(HasTraits): name = Str() address = Str() age = Int() # --[MarriedPerson Class]-------------------------------------------------- class MarriedPerson(Person): partner = Instance(Person) # --[Tabular Adapter Definition]------------------------------------------- class ReportAdapter(TabularAdapter): columns = [ ('Name', 'name'), ('Age', 'age'), ('Address', 'address'), ('Spouse', 'spouse'), ] font = 'Courier 10' age_alignment = Constant('right') MarriedPerson_age_image = Property() MarriedPerson_bg_color = Color(0xE0E0FF) MarriedPerson_spouse_text = Property() Person_spouse_text = Constant('') def _get_MarriedPerson_age_image(self): if self.item.age < 18: return 'red_flag' return None def _get_MarriedPerson_spouse_text(self): return self.item.partner.name # --[Tabular Editor Definition]-------------------------------------------- tabular_editor = TabularEditor( adapter=ReportAdapter(), operations=['move'], images=[ImageResource('red_flag', search_path=search_path)], ) # --[Report Class]--------------------------------------------------------- class Report(HasTraits): people = List(Person) view = View( Group( Item('people', id='table', editor=tabular_editor), show_labels=False, ), title='Tabular Editor Demo', id='traitsui.demo.Applications.tabular_editor_demo', width=0.60, height=0.75, resizable=True, buttons=NoButtons, ) # -------------------------------------------------------- male_names = [ 'Michael', 'Edward', 'Timothy', 'James', 'George', 'Ralph', 'David', 'Martin', 'Bryce', 'Richard', 'Eric', 'Travis', 'Robert', 'Bryan', 'Alan', 'Harold', 'John', 'Stephen', 'Gael', 'Frederic', 'Eli', 'Scott', 'Samuel', 'Alexander', 'Tobias', 'Sven', 'Peter', 'Albert', 'Thomas', 'Horatio', 'Julius', 'Henry', 'Walter', 'Woodrow', 'Dylan', 'Elmer', ] female_names = [ 'Leah', 'Jaya', 'Katrina', 'Vibha', 'Diane', 'Lisa', 'Jean', 'Alice', 'Rebecca', 'Delia', 'Christine', 'Marie', 'Dorothy', 'Ellen', 'Victoria', 'Elizabeth', 'Margaret', 'Joyce', 'Sally', 'Ethel', 'Esther', 'Suzanne', 'Monica', 'Hortense', 'Samantha', 'Tabitha', 'Judith', 'Ariel', 'Helen', 'Mary', 'Jane', 'Janet', 'Jennifer', 'Rita', 'Rena', 'Rianna', ] all_names = male_names + female_names male_name = lambda: choice(male_names) female_name = lambda: choice(female_names) any_name = lambda: choice(all_names) age = lambda: randint(15, 72) family_name = lambda: choice( [ 'Jones', 'Smith', 'Thompson', 'Hayes', 'Thomas', 'Boyle', "O'Reilly", 'Lebowski', 'Lennon', 'Starr', 'McCartney', 'Harrison', 'Harrelson', 'Steinbeck', 'Rand', 'Hemingway', 'Zhivago', 'Clemens', 'Heinlien', 'Farmer', 'Niven', 'Van Vogt', 'Sturbridge', 'Washington', 'Adams', 'Bush', 'Kennedy', 'Ford', 'Lincoln', 'Jackson', 'Johnson', 'Eisenhower', 'Truman', 'Roosevelt', 'Wilson', 'Coolidge', 'Mack', 'Moon', 'Monroe', 'Springsteen', 'Rigby', "O'Neil", 'Philips', 'Clinton', 'Clapton', 'Santana', 'Midler', 'Flack', 'Conner', 'Bond', 'Seinfeld', 'Costanza', 'Kramer', 'Falk', 'Moore', 'Cramdon', 'Baird', 'Baer', 'Spears', 'Simmons', 'Roberts', 'Michaels', 'Stuart', 'Montague', 'Miller', ] ) address = lambda: '%d %s %s' % ( randint(11, 999), choice( [ 'Spring', 'Summer', 'Moonlight', 'Winding', 'Windy', 'Whispering', 'Falling', 'Roaring', 'Hummingbird', 'Mockingbird', 'Bluebird', 'Robin', 'Babbling', 'Cedar', 'Pine', 'Ash', 'Maple', 'Oak', 'Birch', 'Cherry', 'Blossom', 'Rosewood', 'Apple', 'Peach', 'Blackberry', 'Strawberry', 'Starlight', 'Wilderness', 'Dappled', 'Beaver', 'Acorn', 'Pecan', 'Pheasant', 'Owl', ] ), choice( [ 'Way', 'Lane', 'Boulevard', 'Street', 'Drive', 'Circle', 'Avenue', 'Trail', ] ), ) people = [ Person( name='%s %s' % (any_name(), family_name()), age=age(), address=address(), ) for i in range(5000) ] marrieds = [ ( MarriedPerson( name='%s %s' % (female_name(), last_name), age=age(), address=address, ), MarriedPerson( name='%s %s' % (male_name(), last_name), age=age(), address=address ), ) for last_name, address in [(family_name(), address()) for i in range(2500)] ] for female, male in marrieds: female.partner = male male.partner = female people.extend([female, male]) shuffle(people) # --[Example Code*]-------------------------------------------------------- demo = Report(people=people) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/examples/tutorials/traitsui_4.0/editors/tabular_editor/tabular_editor.htm0000644000175100001730000014255700000000000032457 0ustar00runnerdocker00000000000000 The TabularEditor

    The TabularEditor

    Traits 3.0 introduces a new editor, called the TabularEditor, located in the traitsui.wx.extras.tabular_editor module that can be used for many of the same purposes as the existing TableEditor. However, while similar in function, each editor has its own particular strengths and weaknesses.

    Some of the strengths of the new TabularEditor are:

    • Very fast. The editor uses a virtual model which only accesses data from the underlying data model as needed. For example, if you have a million element array, but can only view 50 rows at a time, the editor will only request 50 rows at a time.
    • Very flexible data model. The editor uses a new adapter model for interfacing with your underlying data that allows it to easily deal with many types of data representation: from lists of objects, to arrays of numbers, to tuples of tuples, and many other formats as well.
    • Supports a useful set of data operations. The editor includes built-in support for a number of useful data operations, including:
      • Moving the selection up and down using the keyboard arrow keys.
      • Moving rows up and down using the keyboard arrow keys.
      • Inserting and deleting rows using the keyboard.
      • Begin editing table rows using the keyboard.
      • Drag and drop of table items to and from the editor, including support for both copy and move operations for single or multiple table items.
    • Visually appealing. The editor, in general, uses the underlying OS's native table or grid control, and as a result often looks better than the control used by the TableEditor.
    • Supports displaying text and images in any cell. Note however that the images displayed must all be the same size for optimal results.

    Some of the weaknesses of the TabularEditor compared to the TableEditor are:

    • Not as full-featured. The TableEditor includes support for arbitrary data filters and searches and different types of data sorting. The differences here may narrow over time as new features get added to the TabularEditor.
    • Limited data editing capabilities: The TabularEditor only supports editing textual values, unlike the TableEditor, which supports a wide variety of column editors and can be extended with more as needed. This is due to limitations of the underlying native OS control used by the editor.

    Setting up a TabularEditor for use in a Traits UI is divided into two main parts:

    • Configuring the TabularEditor object.
    • Creating a suitable adapter (or adapters) for use with the editor.

    We'll start off first with a description of the TabularEditor class, and describe the adapter interface in a later section.

    The TabularEditor Class

    The TabularEditor class defines a large number of traits which are used to configure the editor. We'll divide the set of traits into several categories, and describe each in turn.

    Visual Traits

    show_titles
    A boolean value which specifies whether or not column headers should be displayed at the top of the table. It defaults to True.
    horizontal_lines
    A boolean value which specifies whether or not horizontal lines should be drawn between rows in the table. It defaults to True.
    vertical_lines
    A boolean value which specifies whether or not vertical lines should be drawn between columns in the table. It defaults to True.

    Control Traits

    editable
    A boolean value that specifies whether or not the user is allowed to edit data in the table. It defaults to True.
    multi_select
    A boolean value that specifies whether or not the user is allowed to select multiple rows in the table at once. It defaults to False.
    operations

    A list of strings that specify what operations the user is allowed to perform on items in the table. The possible values are:

    • delete: The user can delete table rows.
    • insert: The user can insert new table rows at any position in the table.
    • append: The user can append new table rows to the end of the table.
    • edit: The user can edit the contents of table rows.
    • move: The user can move table rows within the table.

    You should include in the list all operations that you want to allow the user to perform (e.g. ['delete','insert','append']).

    drag_move
    A boolean value that specifies whether drag move operations are allowed (True), or should all drag operations be treated as drag copy operations (False). The default is False.
    adapter
    An instance of TabularAdapter that the editor uses to interface to the underlying Item data supplied to the editor. This is normally an instance of a subclass of TabularAdapter specially written for the type of data being edited. This will be described more fully in the TabularAdapter section.
    adapter_name
    An optional string that specifies the extended trait name of a trait that contains the TabularAdapter the editor should used. If the value of the trait changes while the editor is active, the editor will automatically start using the new adapter. Normally you will use either the adapter trait or the adapter_name trait, but not both. However, it is possible to use both together if needed.
    images
    An optional list of ImageResource objects describing a set of images that can be displayed in the table's cells. Specifying a set here allows you to refer to the images by name within the adapter. However, it is also possible for the adapter to supply the ImageResource object directly for any image it wants to display.

    Event Handling Traits

    selected
    An optional string that specifies the extended trait name of a trait that contains the currently selected table items. For a single-select mode editor, this should be a scalar trait, and for a multi-select mode editor, it should be a list trait. The type of the trait should be the same as the type of data being displayed in the table. This trait can be used to both set and get the current table selection.
    selected_row
    An optional string that specifies the extended trait name of a trait that contains the currently selected table item indices. For a single-select mode editor this should be an Int value, and for a multi-select mode editor it should be a List(Int) value. This trait can be used to both set and get the current table selection.
    activated
    An optional string that specifies the extended trait name of a trait that contains the currently activated table item. It should be an instance of the table item data type. The trait can only be used to get the value of the most recently activated table item. An item is activated either by the user double-clicking on it or by pressing the Enter key when the item is selected.
    activated_row
    An optional string that specifies the extended trait name of a trait that contains the currently activated table item index. It should be an Int value. The trait can only be used to get the index of the most recently activated table item. An item is activated either by the user double-clicking on it or by pressing the Enter key when the item is selected.
    clicked
    An optional string that specifies the extended trait name of a trait that contains a TabularEditorEvent object containing the information associated with the most recent left mouse button click within the editor. The trait can only be used to get the TabularEditorEvent object. The TabularEditorEvent object is described in the next section.
    dclicked
    An optional string that specifies the extended trait name of a trait that contains a TabularEditorEvent object containing the information associated with the most recent left mouse button double-click within the editor. The trait can only be used to get the TabularEditorEvent object. The TabularEditorEvent object is described in the next section.
    right_clicked
    An optional string that specifies the extended trait name of a trait that contains a TabularEditorEvent object containing the information associated with the most recent right mouse button click within the editor. The trait can only be used to get the TabularEditorEvent object. The TabularEditorEvent object is described in the next section.
    right_dclicked
    An optional string that specifies the extended trait name of a trait that contains a TabularEditorEvent object containing the information associated with the most recent right mouse button double-click within the editor. The trait can only be used to get the TabularEditorEvent object. The TabularEditorEvent object is described in the next section.

    The TabularEditorEvent Class

    Objects of the TabularEditorEvent class contain information related to a mouse button click that occurs within the editor. The class has no methods, but does define the following traits:

    row
    An integer specifying the index of the table item that was clicked on.
    column
    A value specifying the column id of the table cell that was clicked on. This value will correspond to the second element of the tuple used to define the column in the TabularEditor adapter supplied to the editor. This will be described in a later section.
    item
    The data item corresponding to the table row that was clicked on. The type of this data will depend upon the type of data contained in the underlying data model.

    The TabularEditor User Interface

    Depending upon how you have configured the TabularEditor and its associated adapter, the following user interface features may be available:

    • Up arrow: Move the selection up one line.
    • Down arrow: Move the selection down one line.
    • Page down: Append a new item to the end of the list ('append').
    • Left arrow: Move the current selection up one line ('move').
    • Right arrow: Move the current selection down one line ('move').
    • Backspace, Delete: Delete all items in the current selection from the list ('delete').
    • Enter, Escape: Edit the current selection ('edit').
    • Insert: Insert a new item before the current selection ('insert').

    In the preceding list, the values in parentheses refer to the operation that must be included in the TabularEditor operations trait in order for the specified key to have any effect.

    The append, move, edit and insert operations are only available if a single item is selected. The delete operation works when the selection has one or more items.

    Depending upon how the TabularEditor and adapter are specified, drag and drop operations may also be available. If multiple items are selected and the user drags one of the selected items, all selected items will be included in the drag operation. If the user drags a non-selected item, only that item will be dragged.

    The editor also supports both drag-move and drag-copy semantics. A drag-move operation means that the dragged items will be sent to the target and removed from the list data. A drag-copy operation means that the dragged items will be sent to the target, but will not be deleted from the Item data. Note that in a drag-copy operation, if you do not want the target to receive the same data contained in the list, then you must return a copy or clone of the data when the editor requests the drag data from the adapter.

    You can prevent drag-move operations by making sure that the TabularEditor drag_move trait is set to False (the default).

    Note that the default operation when a user begins a drag operation is drag_move. A drag-copy operation occurs when the user also holds the Ctrl key down during the drag operation (the mouse pointer changes to indicate the change in drag semantics). If drag_move operations are disabled by setting the TabularEditor drag_move trait to False, any drag-move operation is automatically treated as a drag_copy.

    The tabular editor only allows the user to edit the first column of data in the table (a restriction imposed by the underlying OS widget). If the 'edit' operation is enabled, the user can begin editing the first column either by clicking on the row twice, or by selecting the row and pressing the Enter or Escape key.

    Finally, the user can resize columns in the table by dragging the column title dividers left or right with the mouse. Once resized in this manner, the column remains that size until the user resizes the column again. This is true even if you assigned a dynamic width to the column (see the TabularAdapter section for more information about what this means). If the user wants to allow a previously user-sized column to be restored to its original developer specified size again, they must right-click on the column title to release its user specified size and restore its original size.

    If you enable persistence for the editor by specifying a non-empty id trait for the editor's Item and View objects, any user specified column widths will be saved across application sessions.

    The TabularAdapter Class

    The power and flexibility of the tabular editor is mostly a result of the TabularAdapter class, which is the base class from which all tabular editor adapters must be derived.

    The TabularEditor object interfaces between the underlying toolkit widget and your program, while the TabularAdapter object associated with the editor interfaces between the editor and your data.

    The design of the TabularAdapter base class is such that it tries to make simple cases simple and complex cases possible. How it accomplishes this is what we'll be discussing in the following sections.

    The TabularAdapter columns Trait

    First up is the TabularAdapter columns trait, which is a list of values which define, in presentation order, the set of columns to be displayed by the associated TabularEditor.

    Each entry in the columns list can have one of two forms:

    • string
    • ( string, any )

    where string is the user interface name of the column (which will appear in the table column header) and any is any value that you want to use to identify that column to your adapter. Normally this value is either a trait name or an integer index value, but it can be any value you want. If only string is specified, then any is the index of the string within columns.

    For example, say you want to display a table containing a list of tuples, each of which has three values: a name, an age, and a weight. You could then use the following value for the columns trait:

    columns = [ 'Name', 'Age', 'Weight' ]
    

    By default, the any values (also referred to in later sections as the column ids) for the columns will be the corresponding tuple index values.

    Say instead that you have a list of Person objects, with name, age and weight traits that you want to display in the table. Then you could use the following columns value instead:

    columns = [ ( 'Name',   'name' ),
                ( 'Age',    'age' ),
                ( 'Weight', 'weight' ) ]
    

    In this case, the column ids are the names of the traits you want to display in each column.

    Note that it is possible to dynamically modify the contents of the columns trait while the TabularEditor is active. The TabularEditor will automatically modify the table to show the new set of defined columns.

    The Core TabularAdapter Interface

    In this section, we'll describe the core interface to the TabularAdapter class. This is the actual interface used by the TabularEditor to access your data and display attributes. In the most complex data representation cases, these are the methods that you must override in order to have the greatest control over what the editor sees and does.

    However, the base TabularAdapter class provides default implementations for all of these methods. In subsequent sections, we'll look at how these default implementations provide simple means of customizing the adapter to your needs. But for now, let's start by covering the details of the core interface itself.

    To reduce the amount of repetition, we'll use the following definitions in all of the method argument lists that follow in this section:

    object
    The object whose trait is being edited by the TabularEditor.
    trait
    The name of the trait the TabularEditor is editing.
    row
    The row index (starting with 0) of a table item.
    column
    The column index (starting with 0) of a table column.

    The adapter interface consists of a number of methods which can be divided into two main categories: those which are sensitive to the type of a particular table item, and those which are not. We'll begin with the methods that are sensitive to an item's type:

    get_alignment ( object, trait, column )

    Returns the alignment style to use for a specified column.

    The possible values that can be returned are: 'left', 'center' or 'right'. All table items share the same alignment for a specified column.

    get_width ( object, trait, column )

    Returns the width to use for a specified column. The result can either be a float or integer value.

    If the value is <= 0, the column will have a default width, which is the same as specifying a width of 0.1.

    If the value is > 1.0, it is converted to an integer and the result is the width of the column in pixels. This is referred to as a fixed width column.

    If the value is a float such that 0.0 < value <= 1.0, it is treated as the unnormalized fraction of the available space that is to be assigned to the column. What this means requires a little explanation.

    To arrive at the size in pixels of the column at any given time, the editor adds together all of the unnormalized fraction values returned for all columns in the table to arrive at a total value. Each unnormalized fraction is then divided by the total to create a normalized fraction. Each column is then assigned an amount of space in pixels equal to the maximum of 30 or its normalized fraction multiplied by the available space. The available space is defined as the actual width of the table minus the width of all fixed width columns. Note that this calculation is performed each time the table is resized in the user interface, thus allowing columns of this type to increase or decrease their width dynamically, while leaving fixed width columns unchanged.

    get_can_edit ( object, trait, row )

    Returns a boolean value indicating whether the user can edit a specified object.trait[row] item.

    A True result indicates that the value can be edited, while a False result indicates that it cannot.

    get_drag ( object, trait, row )

    Returns the value to be dragged for a specified object.trait[row] item. A result of None means that the item cannot be dragged. Note that the value returned does not have to be the actual row item. It can be any value that you want to drag in its place. In particular, if you want the drag target to receive a copy of the row item, you should return a copy or clone of the item in its place.

    Also note that if multiple items are being dragged, and this method returns None for any item in the set, no drag operation is performed.

    get_can_drop ( object, trait, row, value )

    Returns whether the specified value can be dropped on the specified object.trait[row] item. A value of True means the value can be dropped; and a value of False indicates that it cannot be dropped.

    The result is used to provide the user positive or negative drag feedback while dragging items over the table. Value will always be a single value, even if multiple items are being dragged. The editor handles multiple drag items by making a separate call to get_can_drop for each item being dragged.

    get_dropped ( object, trait, row, value )

    Returns how to handle a specified value being dropped on a specified object.trait[row] item. The possible return values are:

    • 'before': Insert the specified value before the dropped on item.
    • 'after': Insert the specified value after the dropped on item.

    Note there is no result indicating do not drop since you will have already indicated that the object can be dropped by the result returned from a previous call to get_can_drop.

    get_font ( object, trait, row )

    Returns the font to use for displaying a specified object.trait[row] item.

    A result of None means use the default font; otherwise a wx.Font object should be returned. Note that all columns for the specified table row will use the font value returned.

    get_text_color ( object, trait, row )

    Returns the text color to use for a specified object.trait[row] item.

    A result of None means use the default text color; otherwise a wx.Colour object should be returned. Note that all columns for the specified table row will use the text color value returned.

    get_bg_color ( object, trait, row )

    Returns the background color to use for a specified object.trait[row] item.

    A result of None means use the default background color; otherwise a wx.Colour object should be returned. Note that all columns for the specified table row will use the background color value returned.

    get_image ( object, trait, row, column )

    Returns the image to display for a specified object.trait[row].column item.

    A result of None means no image will be displayed in the specified table cell. Otherwise the result should either be the name of the image, or an ImageResource object specifying the image to display.

    A name is allowed in the case where the image is specified in the TabularEditor images trait. In that case, the name should be the same as the string specified in the ImageResource constructor.

    get_format ( object, trait, row, column )

    Returns the Python formatting string to apply to the specified object.trait[row].column item in order to display it in the table.

    The result can be any Python string containing exactly one Python formatting sequence, such as '%.4f' or '(%5.2f)'.

    get_text ( object, trait, row, column )

    Returns a string containing the text to display for a specified object.trait[row].column item.

    If the underlying data representation for a specified item is not a string, then it is your responsibility to convert it to one before returning it as the result.

    set_text ( object, trait, row, text ):

    Sets the value for the specified object.trait[row].column item to the string specified by text.

    If the underlying data does not store the value as text, it is your responsibility to convert text to the correct representation used. This method is called when the user completes an editing operation on a table cell.

    get_tooltip ( object, trait, row, column )

    Returns a string containing the tooltip to display for a specified object.trait[row].column item.

    You should return the empty string if you do not wish to display a tooltip.

    The following are the remaining adapter methods, which are not sensitive to the type of item or column data:

    get_item ( object, trait, row )

    Returns the specified object.trait[row] item.

    The value returned should be the value that exists (or logically exists) at the specified row in your data. If your data is not really a list or array, then you can just use row as an integer key or token that can be used to retrieve a corresponding item. The value of row will always be in the range: 0 <= row < len( object, trait ) (i.e. the result returned by the adapter len method).

    len ( object, trait )

    Returns the number of row items in the specified object.trait list.

    The result should be an integer greater than or equal to 0.

    delete ( object, trait, row )

    Deletes the specified object.trait[row] item.

    This method is only called if the delete operation is specified in the TabularEditor operation trait, and the user requests that the item be deleted from the table. The adapter can still choose not to delete the specified item if desired, although that may prove confusing to the user.

    insert ( object, trait, row, value )

    Inserts value at the specified object.trait[row] index. The specified value can be:

    • An item being moved from one location in the data to another.
    • A new item created by a previous call to get_default_value.
    • An item the adapter previously approved via a call to get_can_drop.

    The adapter can still choose not to insert the item into the data, although that may prove confusing to the user.

    get_default_value ( object, trait )

    Returns a new default value for the specified object.trait list.

    This method is called when insert or append operations are allowed and the user requests that a new item be added to the table. The result should be a new instance of whatever underlying representation is being used for table items.

    Creating a Custom TabularAdapter

    Having just taken a look at the core TabularAdapter interface, you might now be thinking that there are an awful lot of methods that need to be specified to get an adapter up and running. But as we mentioned earlier, TabularAdapter is not an abstract base class. It is a concrete base class with implementations for each of the methods in its interface. And the implementations are written in such a way that you will hopefully hardly ever need to override them.

    In this section, we'll explain the general implementation style used by these methods, and how you can take advantage of them in creating your own adapters.

    One of the things you probably noticed as you read through the core adapter interface section is that most of the methods have names of the form: get_xxx or set_xxx, which is similar to the familiar getter/setter pattern used when defining trait properties. The adapter interface is purposely defined this way so that it can expose and leverage a simple set of design rules.

    The design rules are followed consistently in the implementations of all of the adapter methods described in the first section of the core adapter interface, so that once you understand how they work, you can easily apply the design pattern to all items in that section. Then, only in the case where the design rules will not work for your application will you ever have to override any of those TabularAdapter base class method implementations.

    So the first thing to understand is that if an adapter method name has the form: get_xxx or set_xxx it really is dealing with some kind of trait called xxx, or which contains xxx in its name. For example, the get_alignment method retrieves the value of some alignment trait defined on the adapter. In the following discussion we'll simply refer to an attribute name generically as attribute, but you will need to replace it by an actual attribute name (e.g. alignment) in your adapter.

    The next thing to keep in mind is that the adapter interface is designed to easily deal with items that are not all of the same type. As we just said, the design rules apply to all adapter methods in the first group, which were defined as methods which are sensitive to an item's type. Item type sensitivity plays an important part in the design rules, as we will see shortly.

    With this in mind, we now describe the simple design rules used by the first group of methods in the TabularAdapter class:

    • When getting or setting an adapter attribute, the method first retrieves the underlying item for the specified data row. The item, and type (i.e. class) of the item, are then used in the next rule.

    • The method gets or sets the first trait it finds on the adapter that matches one of the following names:

      • classname_columnid_attribute
      • classsname_attribute
      • columnid_attribute
      • attribute

      where:

      • classname is the name of the class of the item found in the first step, or one of its base class names, searched in the order defined by the mro (method resolution order) for the item's class.
      • columnid is the column id specified by the developer in the adapter's column trait for the specified table column.
      • attribute is the attribute name as described previously (e.g. alignment).

    Note that this last rule always finds a matching trait, since the TabularAdapter base class provides traits that match the simple attribute form for all attributes these rules apply to. Some of these are simple traits, while others are properties. We'll describe the behavior of all these default traits shortly.

    The basic idea is that rather than override the first group of core adapter methods, you simply define one or more simple traits or trait properties on your TabularAdapter subclass that provide or accept the specified information.

    All of the adapter methods in the first group provide a number of arguments, such as object, trait, row and column. In order to define a trait property, which cannot be passed this information directly, the adapter always stores the arguments and values it computes in the following adapter traits, where they can be easily accessed by a trait getter or setter method:

    • row: The table row being accessed.
    • column: The column id of the table column being accessed (not its index).
    • item: The data item for the specified table row (i.e. the item determined in the first step described above).
    • value: In the case of a set_xxx method, the value to be set; otherwise it is None.

    As mentioned previously, the TabularAdapter class provides trait definitions for all of the attributes these rules apply to. You can either use the default values as they are, override the default, set a new value, or completely replace the trait definition in a subclass. A description of the default trait implementation for each attribute is as follows:

    default_value = Any( '' )

    The default value for a new row.

    The default value is the empty string, but you will normally need to assign a different (default) value.

    format = Str( '%s' )

    The default Python formatting string for a column item.

    The default value is '%s' which will simply convert the column item to a displayable string value.

    text = Property

    The text to display for the column item.

    The implementation of the property checks the type of the column's column id:

    • If it is an integer, it returns format%item[column_id].
    • Otherwise, it returns format%item.column_id.

    Note that format refers to the value returned by a call to get_format for the current column item.

    text_color = Property

    The text color for a row item.

    The property implementation checks to see if the current table row is even or odd, and based on the result returns the value of the even_text_color or odd_text_color trait if the value is not None, and the value of the default_text_color trait if it is. The definition of these additional traits are as follows:

    • odd_text_color = Color( None )
    • even_text_color = Color( None )
    • default_text_color = Color( None )

    Remember that a None value means use the default text color.

    bg_color = Property

    The background color for a row item.

    The property implementation checks to see if the current table row is even or odd, and based on the result returns the value of the even_bg_color or odd_bg_color trait if the value is not None, and the value of the default_bg_color trait if it is. The definition of these additional traits are as follows:

    • odd_bg_color = Color( None )
    • even_bg_color = Color( None )
    • default_bg_color = Color( None )

    Remember that a None value means use the default background color.

    alignment = Enum( 'left', 'center', 'right' )

    The alignment to use for a specified column.

    The default value is 'left'.

    width = Float( -1 )

    The width of a specified column.

    The default value is -1, which means a dynamically sized column with an unnormalized fractional value of 0.1.

    can_edit = Bool( True )

    Specifies whether the text value of the current item can be edited.

    The default value is True, which means that the user can edit the value.

    drag = Property

    A property which returns the value to be dragged for a specified row item.

    The property implementation simply returns the current row item.

    can_drop = Bool( False )

    Specifies whether the specified value be dropped on the current item.

    The default value is False, meaning that the value cannot be dropped.

    dropped = Enum( 'after', 'before' )

    Specifies where a dropped item should be placed in the table relative to the item it is dropped on.

    The default value is 'after'.

    font = Font

    The font to use for the current item.

    The default value is the standard default Traits font value.

    image = Str( None )

    The name of the default image to use for a column.

    The default value is None, which means that no image will be displayed for the column.

    # The text of a row/column item: text = Property

    tooltip = Str

    The tooltip information for a column item.

    The default value is the empty string, which means no tooltip information will be displayed for the column.

    The preceding discussion applies to all of the methods defined in the first group of TabularAdapter interface methods. However, the design rules do not apply to the remaining five adapter methods, although they all provide a useful default implementation:

    get_item ( object, trait, row )

    Returns the value of the object.trait[row] item.

    The default implementation assumes the trait defined by object.trait is a sequence and attempts to return the value at index row. If an error occurs, it returns None instead. This definition should work correctly for lists, tuples and arrays, or any other object that is indexable, but will have to be overridden for all other cases.

    Note that this method is the one called in the first design rule described previously to retrieve the item at the current table row.

    len ( object, trait )

    Returns the number of items in the specified object.trait list.

    Again, the default implementation assumes the trait defined by object.trait is a sequence and attempts to return the result of calling len( object.trait ). It will need to be overridden for any type of data which for which len will not work.

    delete ( object, trait, row )

    Deletes the specified object.trait[row] item.

    The default implementation assumes the trait defined by object.trait is a mutable sequence and attempts to perform a del object.trait[row] operation.

    insert ( object, trait, row, value )

    Inserts a new value at the specified object.trait[row] index.

    The default implementation assumes the trait defined by object.trait is a mutable sequence and attempts to perform an object.trait[row:row]=[value] operation.

    get_default_value ( object, trait )

    Returns a new default value for the specified object.trait list.

    The default implementation simply returns the value of the adapter's default_value trait.

    A TabularAdapter Example

    Having now learned about the core adapter interface as well as the design rules supported by the default method implementations, you're probably wondering how you can use a TabularAdapter for creating a real user interface.

    So in this section we'll cover a simple example of creating a TabularAdapter subclass to try and show how all of the pieces fit together.

    In subsequent tutorials, we'll provide complete examples of creating user interfaces using both the TabularEditor and TabularAdapter in combination.

    For this example, let's assume we have the following two classes:

    class Person( HasTraits ):
    
        name = Str()
        age = Int()
        address = Str()
    
    class MarriedPerson( Person ):
    
        partner = Instance( Person )
    

    where Person represents a single person, and MarriedPerson represents a married person and is derived from Person but adds the partner trait to reference the person they are married to.

    Now, assume we also have the following additional class:

    class Report( HasTraits ):
    
        people = List( Person )
    

    which has a people trait which contains a list of both Person and MarriedPerson objects, and we want to create a tabular display showing the following information:

    • Name of the person
    • Age of the person
    • The person's address
    • The name of the person's spouse (if any)

    In addition:

    • We want to use a Courier 10 point font for each line in the table.
    • We want the age column to be right, instead of left, justified
    • If the person is a minor (age < 18) and married, we want to show a red flag image in the age column.
    • If the person is married, we want to make the background color for that row a light blue.

    Given this set of requirements, we can now define the following TabularAdapter subclass:

    class ReportAdapter( TabularAdapter ):
    
        columns = [ ( 'Name',    'name' ),
                    ( 'Age',     'age' ),
                    ( 'Address', 'address' )
                    ( 'Spouse',  'spouse' ) ]
    
        font                      = 'Courier 10'
        age_alignment             = Constant( 'right' )
        MarriedPerson_age_image = Property()
        MarriedPerson_bg_color    = Color( 0xE0E0FF )
        MarriedPerson_spouse_text = Property()
        Person_spouse_text        = Constant( '' )
    
        def _get_MarriedPerson_age_image ( self ):
            if self.item.age < 18:
                return 'red_flag'
            return None
    
        def _get_MarriedPerson_spouse_text ( self ):
            return self.item.partner.name
    

    Hopefully, this simple example conveys some of the power and flexibility that the TabularAdapter class provides you. But, just in case it doesn't, let's go over some of the more interesting details:

    • Note the values in the columns trait. The first three values define column ids which map directly to traits defined on our data objects, while the last one defines an arbitrary string which we define so that we can reference it in the MarriedPerson_spouse_text and Person_spouse_text trait definitions.
    • Since the font we want to use applies to all table rows, we just specify a new default value for the existing TabularAdapter font trait.
    • Since we only want to override the default left alignment for the age column, we simply define an age_alignment trait as a constant 'right' value. We could have also used age_alignment = Str('right'), but Constant never requires storage to be used in an object.
    • We define the MarriedPerson_age_image property to handle putting the red flag image in the age column. By including the class name of the items it applies to, we only need to check the age value in determining what value to return.
    • Similary, we use the MarriedPerson_bg_color trait to ensure that each MarriedPerson object has the correct background color in the table.
    • Finally, we use the MarriedPerson_spouse_text and Person_spouse_text traits, one a property and the other a simple constant value, to determine what text to display in the Spouse column for the different object types. Note that even though a MarriedPerson is both a Person and a MarriedPerson, it will correctly use the MarriedPerson_spouse_text trait since the search for a matching trait is always made in mro order.

    Although this is completely subjective, some of the things that the author feels stand out about this approach are:

    • The class definition is short and sweet. Less code is good.
    • The bulk of the code is declarative. Less room for logic errors.
    • There is only one bit of logic in the class (the if statement in the MarriedPerson_age_image property implementation). Again, less logic usually translates into more reliable code).
    • The defined traits and even the property implementation method names read very descriptively. _get_MarriedPerson_age_image pretty much says what you would write in a comment or doc string. The implementation almost is the documentation.

    Look for a complete traits UI example based on this sample problem definition in the Single and Married Person Example tutorial in this section.

    Now, as the complexity of a tabular view increases, the definition of the TabularAdapter class could possibly start to get large and unwieldy. At this point we could begin refactoring our design to use the ITabularAdapter interface and AnITabularAdapter implementation class to create sub-adapters that can be added to our TabularAdapter subclass to extend its functionality even further. Creating sub-adapters and adding them via the TabularAdapter adapters trait is a topic covered in a follow-on tutorial.

    ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/examples/tutorials/traitsui_4.0/editors/tabular_editor/tabular_editor.rst0000644000175100001730000012222300000000000032463 0ustar00runnerdocker00000000000000The TabularEditor ================= Traits 3.0 introduces a new editor, called the **TabularEditor**, located in the *traitsui.wx.extras.tabular_editor* module that can be used for many of the same purposes as the existing **TableEditor**. However, while similar in function, each editor has its own particular strengths and weaknesses. Some of the strengths of the new **TabularEditor** are: - **Very fast**. The editor uses a *virtual* model which only accesses data from the underlying data model as needed. For example, if you have a million element array, but can only view 50 rows at a time, the editor will only request 50 rows at a time. - **Very flexible data model**. The editor uses a new adapter model for interfacing with your underlying data that allows it to easily deal with many types of data representation: from lists of objects, to arrays of numbers, to tuples of tuples, and many other formats as well. - **Supports a useful set of data operations**. The editor includes built-in support for a number of useful data operations, including: - Moving the selection up and down using the keyboard arrow keys. - Moving rows up and down using the keyboard arrow keys. - Inserting and deleting rows using the keyboard. - Begin editing table rows using the keyboard. - Drag and drop of table items to and from the editor, including support for both *copy* and *move* operations for single or multiple table items. - **Visually appealing**. The editor, in general, uses the underlying OS's native table or grid control, and as a result often looks better than the control used by the **TableEditor**. - **Supports displaying text and images in any cell**. Note however that the images displayed must all be the same size for optimal results. Some of the weaknesses of the **TabularEditor** compared to the **TableEditor** are: - **Not as full-featured**. The **TableEditor** includes support for arbitrary data filters and searches and different types of data sorting. The differences here may narrow over time as new features get added to the **TabularEditor**. - **Limited data editing capabilities**: The **TabularEditor** only supports editing textual values, unlike the **TableEditor**, which supports a wide variety of column editors and can be extended with more as needed. This is due to limitations of the underlying native OS control used by the editor. Setting up a **TabularEditor** for use in a Traits UI is divided into two main parts: - Configuring the **TabularEditor** object. - Creating a suitable adapter (or adapters) for use with the editor. We'll start off first with a description of the **TabularEditor** class, and describe the adapter interface in a later section. The TabularEditor Class ----------------------- The **TabularEditor** class defines a large number of traits which are used to configure the editor. We'll divide the set of traits into several categories, and describe each in turn. Visual Traits ------------- show_titles A boolean value which specifies whether or not column headers should be displayed at the top of the table. It defaults to **True**. horizontal_lines A boolean value which specifies whether or not horizontal lines should be drawn between rows in the table. It defaults to **True**. vertical_lines A boolean value which specifies whether or not vertical lines should be drawn between columns in the table. It defaults to **True**. Control Traits -------------- editable A boolean value that specifies whether or not the user is allowed to edit data in the table. It defaults to **True**. multi_select A boolean value that specifies whether or not the user is allowed to select multiple rows in the table at once. It defaults to **False**. operations A list of strings that specify what operations the user is allowed to perform on items in the table. The possible values are: - **delete**: The user can delete table rows. - **insert**: The user can insert new table rows at any position in the table. - **append**: The user can append new table rows to the end of the table. - **edit**: The user can edit the contents of table rows. - **move**: The user can move table rows within the table. You should include in the list all operations that you want to allow the user to perform (e.g. *['delete','insert','append']*). drag_move A boolean value that specifies whether *drag move* operations are allowed (**True**), or should all drag operations be treated as *drag copy* operations (**False**). The default is **False**. adapter An instance of **TabularAdapter** that the editor uses to interface to the underlying **Item** data supplied to the editor. This is normally an instance of a subclass of **TabularAdapter** specially written for the type of data being edited. This will be described more fully in the **TabularAdapter** section. adapter_name An optional string that specifies the extended trait name of a trait that contains the **TabularAdapter** the editor should used. If the value of the trait changes while the editor is active, the editor will automatically start using the new adapter. Normally you will use either the *adapter* trait *or* the *adapter_name* trait, but not both. However, it is possible to use both together if needed. images An optional list of **ImageResource** objects describing a set of images that can be displayed in the table's cells. Specifying a set here allows you to refer to the images by name within the adapter. However, it is also possible for the adapter to supply the **ImageResource** object directly for any image it wants to display. Event Handling Traits --------------------- selected An optional string that specifies the extended trait name of a trait that contains the currently selected table items. For a single-select mode editor, this should be a scalar trait, and for a multi-select mode editor, it should be a list trait. The type of the trait should be the same as the type of data being displayed in the table. This trait can be used to both set and get the current table selection. selected_row An optional string that specifies the extended trait name of a trait that contains the currently selected table item indices. For a single-select mode editor this should be an **Int** value, and for a multi-select mode editor it should be a **List(Int)** value. This trait can be used to both set and get the current table selection. activated An optional string that specifies the extended trait name of a trait that contains the currently activated table item. It should be an instance of the table item data type. The trait can only be used to get the value of the most recently activated table item. An item is activated either by the user double-clicking on it or by pressing the **Enter** key when the item is selected. activated_row An optional string that specifies the extended trait name of a trait that contains the currently activated table item index. It should be an **Int** value. The trait can only be used to get the index of the most recently activated table item. An item is activated either by the user double-clicking on it or by pressing the **Enter** key when the item is selected. clicked An optional string that specifies the extended trait name of a trait that contains a **TabularEditorEvent** object containing the information associated with the most recent left mouse button click within the editor. The trait can only be used to get the **TabularEditorEvent** object. The **TabularEditorEvent** object is described in the next section. dclicked An optional string that specifies the extended trait name of a trait that contains a **TabularEditorEvent** object containing the information associated with the most recent left mouse button double-click within the editor. The trait can only be used to get the **TabularEditorEvent** object. The **TabularEditorEvent** object is described in the next section. right_clicked An optional string that specifies the extended trait name of a trait that contains a **TabularEditorEvent** object containing the information associated with the most recent right mouse button click within the editor. The trait can only be used to get the **TabularEditorEvent** object. The **TabularEditorEvent** object is described in the next section. right_dclicked An optional string that specifies the extended trait name of a trait that contains a **TabularEditorEvent** object containing the information associated with the most recent right mouse button double-click within the editor. The trait can only be used to get the **TabularEditorEvent** object. The **TabularEditorEvent** object is described in the next section. The TabularEditorEvent Class ---------------------------- Objects of the **TabularEditorEvent** class contain information related to a mouse button click that occurs within the editor. The class has no methods, but does define the following traits: row An integer specifying the index of the table item that was clicked on. column A value specifying the column id of the table cell that was clicked on. This value will correspond to the second element of the tuple used to define the column in the **TabularEditor** adapter supplied to the editor. This will be described in a later section. item The data item corresponding to the table row that was clicked on. The type of this data will depend upon the type of data contained in the underlying data model. The TabularEditor User Interface -------------------------------- Depending upon how you have configured the **TabularEditor** and its associated adapter, the following user interface features may be available: - **Up arrow**: Move the selection up one line. - **Down arrow**: Move the selection down one line. - **Page down**: Append a new item to the end of the list (*'append'*). - **Left arrow**: Move the current selection up one line (*'move'*). - **Right arrow**: Move the current selection down one line (*'move'*). - **Backspace, Delete**: Delete all items in the current selection from the list (*'delete'*). - **Enter, Escape**: Edit the current selection (*'edit'*). - **Insert**: Insert a new item before the current selection (*'insert'*). In the preceding list, the values in parentheses refer to the operation that must be included in the **TabularEditor** *operations* trait in order for the specified key to have any effect. The *append*, *move*, *edit* and *insert* operations are only available if a single item is selected. The *delete* operation works when the selection has one or more items. Depending upon how the **TabularEditor** and adapter are specified, drag and drop operations may also be available. If multiple items are selected and the user drags one of the selected items, all selected items will be included in the drag operation. If the user drags a non-selected item, only that item will be dragged. The editor also supports both *drag-move* and *drag-copy* semantics. A *drag-move* operation means that the dragged items will be sent to the target and removed from the list data. A *drag-copy* operation means that the dragged items will be sent to the target, but will *not* be deleted from the **Item** data. Note that in a *drag-copy* operation, if you do not want the target to receive the same data contained in the list, then you must return a copy or clone of the data when the editor requests the drag data from the adapter. You can prevent *drag-move* operations by making sure that the **TabularEditor** *drag_move* trait is set to **False** (the default). Note that the default operation when a user begins a drag operation is *drag_move*. A *drag-copy* operation occurs when the user also holds the *Ctrl* key down during the drag operation (the mouse pointer changes to indicate the change in drag semantics). If *drag_move* operations are disabled by setting the **TabularEditor** *drag_move* trait to **False**, any *drag-move* operation is automatically treated as a *drag_copy*. The tabular editor only allows the user to edit the first column of data in the table (a restriction imposed by the underlying OS widget). If the *'edit'* operation is enabled, the user can begin editing the first column either by clicking on the row twice, or by selecting the row and pressing the **Enter** or **Escape** key. Finally, the user can resize columns in the table by dragging the column title dividers left or right with the mouse. Once resized in this manner, the column remains that size until the user resizes the column again. This is true even if you assigned a dynamic width to the column (see the **TabularAdapter** section for more information about what this means). If the user wants to allow a previously user-sized column to be restored to its original developer specified size again, they must right-click on the column title to *release* its user specified size and restore its original size. If you enable *persistence* for the editor by specifying a non-empty *id* trait for the editor's **Item** and **View** objects, any user specified column widths will be saved across application sessions. The TabularAdapter Class ------------------------ The power and flexibility of the tabular editor is mostly a result of the **TabularAdapter** class, which is the base class from which all tabular editor adapters must be derived. The **TabularEditor** object interfaces between the underlying toolkit widget and your program, while the **TabularAdapter** object associated with the editor interfaces between the editor and your data. The design of the **TabularAdapter** base class is such that it tries to make simple cases simple and complex cases possible. How it accomplishes this is what we'll be discussing in the following sections. The TabularAdapter *columns* Trait ---------------------------------- First up is the **TabularAdapter** *columns* trait, which is a list of values which define, in presentation order, the set of columns to be displayed by the associated **TabularEditor**. Each entry in the *columns* list can have one of two forms: - string - ( string, any ) where *string* is the user interface name of the column (which will appear in the table column header) and *any* is any value that you want to use to identify that column to your adapter. Normally this value is either a trait name or an integer index value, but it can be any value you want. If only *string* is specified, then *any* is the index of the *string* within *columns*. For example, say you want to display a table containing a list of tuples, each of which has three values: a name, an age, and a weight. You could then use the following value for the *columns* trait:: columns = [ 'Name', 'Age', 'Weight' ] By default, the *any* values (also referred to in later sections as the *column ids*) for the columns will be the corresponding tuple index values. Say instead that you have a list of **Person** objects, with *name*, *age* and *weight* traits that you want to display in the table. Then you could use the following *columns* value instead:: columns = [ ( 'Name', 'name' ), ( 'Age', 'age' ), ( 'Weight', 'weight' ) ] In this case, the *column ids* are the names of the traits you want to display in each column. Note that it is possible to dynamically modify the contents of the *columns* trait while the **TabularEditor** is active. The **TabularEditor** will automatically modify the table to show the new set of defined columns. The Core TabularAdapter Interface --------------------------------- In this section, we'll describe the core interface to the **TabularAdapter** class. This is the actual interface used by the **TabularEditor** to access your data and display attributes. In the most complex data representation cases, these are the methods that you must override in order to have the greatest control over what the editor sees and does. However, the base **TabularAdapter** class provides default implementations for all of these methods. In subsequent sections, we'll look at how these default implementations provide simple means of customizing the adapter to your needs. But for now, let's start by covering the details of the core interface itself. To reduce the amount of repetition, we'll use the following definitions in all of the method argument lists that follow in this section: object The object whose trait is being edited by the **TabularEditor**. trait The name of the trait the **TabularEditor** is editing. row The row index (starting with 0) of a table item. column The column index (starting with 0) of a table column. The adapter interface consists of a number of methods which can be divided into two main categories: those which are sensitive to the type of a particular table item, and those which are not. We'll begin with the methods that are sensitive to an item's type: get_alignment ( object, trait, column ) Returns the alignment style to use for a specified column. The possible values that can be returned are: *'left'*, *'center'* or *'right'*. All table items share the same alignment for a specified column. get_width ( object, trait, column ) Returns the width to use for a specified column. The result can either be a float or integer value. If the value is <= 0, the column will have a *default* width, which is the same as specifying a width of *0.1*. If the value is > 1.0, it is converted to an integer and the result is the width of the column in pixels. This is referred to as a *fixed width* column. If the value is a float such that 0.0 < value <= 1.0, it is treated as the *unnormalized fraction of the available space* that is to be assigned to the column. What this means requires a little explanation. To arrive at the size in pixels of the column at any given time, the editor adds together all of the *unnormalized fraction* values returned for all columns in the table to arrive at a total value. Each *unnormalized fraction* is then divided by the total to create a *normalized fraction*. Each column is then assigned an amount of space in pixels equal to the maximum of 30 or its *normalized fraction* multiplied by the *available space*. The *available space* is defined as the actual width of the table minus the width of all *fixed width* columns. Note that this calculation is performed each time the table is resized in the user interface, thus allowing columns of this type to increase or decrease their width dynamically, while leaving *fixed width* columns unchanged. get_can_edit ( object, trait, row ) Returns a boolean value indicating whether the user can edit a specified *object.trait[row]* item. A **True** result indicates that the value can be edited, while a **False** result indicates that it cannot. get_drag ( object, trait, row ) Returns the value to be *dragged* for a specified *object.trait[row]* item. A result of **None** means that the item cannot be dragged. Note that the value returned does not have to be the actual row item. It can be any value that you want to drag in its place. In particular, if you want the drag target to receive a copy of the row item, you should return a copy or clone of the item in its place. Also note that if multiple items are being dragged, and this method returns **None** for any item in the set, no drag operation is performed. get_can_drop ( object, trait, row, value ) Returns whether the specified *value* can be dropped on the specified *object.trait[row]* item. A value of **True** means the *value* can be dropped; and a value of **False** indicates that it cannot be dropped. The result is used to provide the user positive or negative drag feedback while dragging items over the table. *Value* will always be a single value, even if multiple items are being dragged. The editor handles multiple drag items by making a separate call to *get_can_drop* for each item being dragged. get_dropped ( object, trait, row, value ) Returns how to handle a specified *value* being dropped on a specified *object.trait[row]* item. The possible return values are: - **'before'**: Insert the specified *value* before the dropped on item. - **'after'**: Insert the specified *value* after the dropped on item. Note there is no result indicating *do not drop* since you will have already indicated that the *object* can be dropped by the result returned from a previous call to *get_can_drop*. get_font ( object, trait, row ) Returns the font to use for displaying a specified *object.trait[row]* item. A result of **None** means use the default font; otherwise a **wx.Font** object should be returned. Note that all columns for the specified table row will use the font value returned. get_text_color ( object, trait, row ) Returns the text color to use for a specified *object.trait[row]* item. A result of **None** means use the default text color; otherwise a **wx.Colour** object should be returned. Note that all columns for the specified table row will use the text color value returned. get_bg_color ( object, trait, row ) Returns the background color to use for a specified *object.trait[row]* item. A result of **None** means use the default background color; otherwise a **wx.Colour** object should be returned. Note that all columns for the specified table row will use the background color value returned. get_image ( object, trait, row, column ) Returns the image to display for a specified *object.trait[row].column* item. A result of **None** means no image will be displayed in the specified table cell. Otherwise the result should either be the name of the image, or an **ImageResource** object specifying the image to display. A name is allowed in the case where the image is specified in the **TabularEditor** *images* trait. In that case, the name should be the same as the string specified in the **ImageResource** constructor. get_format ( object, trait, row, column ) Returns the Python formatting string to apply to the specified *object.trait[row].column* item in order to display it in the table. The result can be any Python string containing exactly one Python formatting sequence, such as *'%.4f'* or *'(%5.2f)'*. get_text ( object, trait, row, column ) Returns a string containing the text to display for a specified *object.trait[row].column* item. If the underlying data representation for a specified item is not a string, then it is your responsibility to convert it to one before returning it as the result. set_text ( object, trait, row, text ): Sets the value for the specified *object.trait[row].column* item to the string specified by *text*. If the underlying data does not store the value as text, it is your responsibility to convert *text* to the correct representation used. This method is called when the user completes an editing operation on a table cell. get_tooltip ( object, trait, row, column ) Returns a string containing the tooltip to display for a specified *object.trait[row].column* item. You should return the empty string if you do not wish to display a tooltip. The following are the remaining adapter methods, which are not sensitive to the type of item or column data: get_item ( object, trait, row ) Returns the specified *object.trait[row]* item. The value returned should be the value that exists (or *logically* exists) at the specified *row* in your data. If your data is not really a list or array, then you can just use *row* as an integer *key* or *token* that can be used to retrieve a corresponding item. The value of *row* will always be in the range: 0 <= row < *len( object, trait )* (i.e. the result returned by the adapter *len* method). len ( object, trait ) Returns the number of row items in the specified *object.trait* list. The result should be an integer greater than or equal to 0. delete ( object, trait, row ) Deletes the specified *object.trait[row]* item. This method is only called if the *delete* operation is specified in the **TabularEditor** *operation* trait, and the user requests that the item be deleted from the table. The adapter can still choose not to delete the specified item if desired, although that may prove confusing to the user. insert ( object, trait, row, value ) Inserts *value* at the specified *object.trait[row]* index. The specified *value* can be: - An item being moved from one location in the data to another. - A new item created by a previous call to *get_default_value*. - An item the adapter previously approved via a call to *get_can_drop*. The adapter can still choose not to insert the item into the data, although that may prove confusing to the user. get_default_value ( object, trait ) Returns a new default value for the specified *object.trait* list. This method is called when *insert* or *append* operations are allowed and the user requests that a new item be added to the table. The result should be a new instance of whatever underlying representation is being used for table items. Creating a Custom TabularAdapter -------------------------------- Having just taken a look at the core **TabularAdapter** interface, you might now be thinking that there are an awful lot of methods that need to be specified to get an adapter up and running. But as we mentioned earlier, **TabularAdapter** is not an abstract base class. It is a concrete base class with implementations for each of the methods in its interface. And the implementations are written in such a way that you will hopefully hardly ever need to override them. In this section, we'll explain the general implementation style used by these methods, and how you can take advantage of them in creating your own adapters. One of the things you probably noticed as you read through the core adapter interface section is that most of the methods have names of the form: *get_xxx* or *set_xxx*, which is similar to the familiar *getter/setter* pattern used when defining trait properties. The adapter interface is purposely defined this way so that it can expose and leverage a simple set of design rules. The design rules are followed consistently in the implementations of all of the adapter methods described in the first section of the core adapter interface, so that once you understand how they work, you can easily apply the design pattern to all items in that section. Then, only in the case where the design rules will not work for your application will you ever have to override any of those **TabularAdapter** base class method implementations. So the first thing to understand is that if an adapter method name has the form: *get_xxx* or *set_xxx* it really is dealing with some kind of trait called *xxx*, or which contains *xxx* in its name. For example, the *get_alignment* method retrieves the value of some *alignment* trait defined on the adapter. In the following discussion we'll simply refer to an attribute name generically as *attribute*, but you will need to replace it by an actual attribute name (e.g. *alignment*) in your adapter. The next thing to keep in mind is that the adapter interface is designed to easily deal with items that are not all of the same type. As we just said, the design rules apply to all adapter methods in the first group, which were defined as methods which are sensitive to an item's type. Item type sensitivity plays an important part in the design rules, as we will see shortly. With this in mind, we now describe the simple design rules used by the first group of methods in the **TabularAdapter** class: - When getting or setting an adapter attribute, the method first retrieves the underlying item for the specified data row. The item, and type (i.e. class) of the item, are then used in the next rule. - The method gets or sets the first trait it finds on the adapter that matches one of the following names: - *classname_columnid_attribute* - *classsname_attribute* - *columnid_attribute* - *attribute* where: - *classname* is the name of the class of the item found in the first step, or one of its base class names, searched in the order defined by the *mro* (**method resolution order**) for the item's class. - *columnid* is the column id specified by the developer in the adapter's *column* trait for the specified table column. - *attribute* is the attribute name as described previously (e.g. *alignment*). Note that this last rule always finds a matching trait, since the **TabularAdapter** base class provides traits that match the simple *attribute* form for all attributes these rules apply to. Some of these are simple traits, while others are properties. We'll describe the behavior of all these *default* traits shortly. The basic idea is that rather than override the first group of core adapter methods, you simply define one or more simple traits or trait properties on your **TabularAdapter** subclass that provide or accept the specified information. All of the adapter methods in the first group provide a number of arguments, such as *object*, *trait*, *row* and *column*. In order to define a trait property, which cannot be passed this information directly, the adapter always stores the arguments and values it computes in the following adapter traits, where they can be easily accessed by a trait getter or setter method: - *row*: The table row being accessed. - *column*: The column id of the table column being accessed (not its index). - *item*: The data item for the specified table row (i.e. the item determined in the first step described above). - *value*: In the case of a *set_xxx* method, the value to be set; otherwise it is **None**. As mentioned previously, the **TabularAdapter** class provides trait definitions for all of the attributes these rules apply to. You can either use the default values as they are, override the default, set a new value, or completely replace the trait definition in a subclass. A description of the default trait implementation for each attribute is as follows: default_value = Any( '' ) The default value for a new row. The default value is the empty string, but you will normally need to assign a different (default) value. format = Str( '%s' ) The default Python formatting string for a column item. The default value is *'%s'* which will simply convert the column item to a displayable string value. text = Property() The text to display for the column item. The implementation of the property checks the type of the column's *column id*: - If it is an integer, it returns *format%item[column_id]*. - Otherwise, it returns *format%item.column_id*. Note that *format* refers to the value returned by a call to *get_format* for the current column item. text_color = Property() The text color for a row item. The property implementation checks to see if the current table row is even or odd, and based on the result returns the value of the *even_text_color* or *odd_text_color* trait if the value is not **None**, and the value of the *default_text_color* trait if it is. The definition of these additional traits are as follows: - odd_text_color = Color( None ) - even_text_color = Color( None ) - default_text_color = Color( None ) Remember that a **None** value means use the default text color. bg_color = Property() The background color for a row item. The property implementation checks to see if the current table row is even or odd, and based on the result returns the value of the *even_bg_color* or *odd_bg_color* trait if the value is not **None**, and the value of the *default_bg_color* trait if it is. The definition of these additional traits are as follows: - odd_bg_color = Color( None ) - even_bg_color = Color( None ) - default_bg_color = Color( None ) Remember that a **None** value means use the default background color. alignment = Enum( 'left', 'center', 'right' ) The alignment to use for a specified column. The default value is *'left'*. width = Float( -1 ) The width of a specified column. The default value is *-1*, which means a dynamically sized column with an *unnormalized fractional* value of *0.1*. can_edit = Bool( True ) Specifies whether the text value of the current item can be edited. The default value is **True**, which means that the user can edit the value. drag = Property() A property which returns the value to be dragged for a specified row item. The property implementation simply returns the current row item. can_drop = Bool( False ) Specifies whether the specified value be dropped on the current item. The default value is **False**, meaning that the value cannot be dropped. dropped = Enum( 'after', 'before' ) Specifies where a dropped item should be placed in the table relative to the item it is dropped on. The default value is *'after'*. font = Font The font to use for the current item. The default value is the standard default Traits font value. image = Str( None ) The name of the default image to use for a column. The default value is **None**, which means that no image will be displayed for the column. # The text of a row/column item: text = Property() tooltip = Str() The tooltip information for a column item. The default value is the empty string, which means no tooltip information will be displayed for the column. The preceding discussion applies to all of the methods defined in the first group of **TabularAdapter** interface methods. However, the design rules do not apply to the remaining five adapter methods, although they all provide a useful default implementation: get_item ( object, trait, row ) Returns the value of the *object.trait[row]* item. The default implementation assumes the trait defined by *object.trait* is a *sequence* and attempts to return the value at index *row*. If an error occurs, it returns **None** instead. This definition should work correctly for lists, tuples and arrays, or any other object that is indexable, but will have to be overridden for all other cases. Note that this method is the one called in the first design rule described previously to retrieve the item at the current table row. len ( object, trait ) Returns the number of items in the specified *object.trait* list. Again, the default implementation assumes the trait defined by *object.trait* is a *sequence* and attempts to return the result of calling *len( object.trait )*. It will need to be overridden for any type of data which for which *len* will not work. delete ( object, trait, row ) Deletes the specified *object.trait[row]* item. The default implementation assumes the trait defined by *object.trait* is a mutable sequence and attempts to perform a *del object.trait[row]* operation. insert ( object, trait, row, value ) Inserts a new value at the specified *object.trait[row]* index. The default implementation assumes the trait defined by *object.trait* is a mutable sequence and attempts to perform an *object.trait[row:row]=[value]* operation. get_default_value ( object, trait ) Returns a new default value for the specified *object.trait* list. The default implementation simply returns the value of the adapter's *default_value* trait. A TabularAdapter Example ------------------------ Having now learned about the core adapter interface as well as the design rules supported by the default method implementations, you're probably wondering how you can use a **TabularAdapter** for creating a real user interface. So in this section we'll cover a simple example of creating a **TabularAdapter** subclass to try and show how all of the pieces fit together. In subsequent tutorials, we'll provide complete examples of creating user interfaces using both the **TabularEditor** and **TabularAdapter** in combination. For this example, let's assume we have the following two classes:: class Person( HasTraits ): name = Str() age = Int() address = Str() class MarriedPerson( Person ): partner = Instance( Person ) where **Person** represents a single person, and **MarriedPerson** represents a married person and is derived from **Person** but adds the *partner* trait to reference the person they are married to. Now, assume we also have the following additional class:: class Report( HasTraits ): people = List( Person ) which has a *people* trait which contains a list of both **Person** and **MarriedPerson** objects, and we want to create a tabular display showing the following information: - Name of the person - Age of the person - The person's address - The name of the person's spouse (if any) In addition: - We want to use a Courier 10 point font for each line in the table. - We want the age column to be right, instead of left, justified - If the person is a minor (age < 18) and married, we want to show a red flag image in the age column. - If the person is married, we want to make the background color for that row a light blue. Given this set of requirements, we can now define the following **TabularAdapter** subclass:: class ReportAdapter( TabularAdapter ): columns = [ ( 'Name', 'name' ), ( 'Age', 'age' ), ( 'Address', 'address' ) ( 'Spouse', 'spouse' ) ] font = 'Courier 10' age_alignment = Constant( 'right' ) MarriedPerson_age_image = Property() MarriedPerson_bg_color = Color( 0xE0E0FF ) MarriedPerson_spouse_text = Property() Person_spouse_text = Constant( '' ) def _get_MarriedPerson_age_image ( self ): if self.item.age < 18: return 'red_flag' return None def _get_MarriedPerson_spouse_text ( self ): return self.item.partner.name Hopefully, this simple example conveys some of the power and flexibility that the **TabularAdapter** class provides you. But, just in case it doesn't, let's go over some of the more interesting details: - Note the values in the *columns* trait. The first three values define *column ids* which map directly to traits defined on our data objects, while the last one defines an arbitrary string which we define so that we can reference it in the *MarriedPerson_spouse_text* and *Person_spouse_text* trait definitions. - Since the font we want to use applies to all table rows, we just specify a new default value for the existing **TabularAdapter** *font* trait. - Since we only want to override the default left alignment for the age column, we simply define an *age_alignment* trait as a constant *'right'* value. We could have also used *age_alignment = Str('right')*, but *Constant* never requires storage to be used in an object. - We define the *MarriedPerson_age_image* property to handle putting the *red flag* image in the age column. By including the class name of the items it applies to, we only need to check the *age* value in determining what value to return. - Similary, we use the *MarriedPerson_bg_color* trait to ensure that each **MarriedPerson** object has the correct background color in the table. - Finally, we use the *MarriedPerson_spouse_text* and *Person_spouse_text* traits, one a property and the other a simple constant value, to determine what text to display in the *Spouse* column for the different object types. Note that even though a **MarriedPerson** is both a **Person** and a **MarriedPerson**, it will correctly use the *MarriedPerson_spouse_text* trait since the search for a matching trait is always made in *mro* order. Although this is completely subjective, some of the things that the author feels stand out about this approach are: - The class definition is short and sweet. Less code is good. - The bulk of the code is declarative. Less room for logic errors. - There is only one bit of logic in the class (the *if* statement in the *MarriedPerson_age_image* property implementation). Again, less logic usually translates into more reliable code). - The defined traits and even the property implementation method names read very descriptively. *_get_MarriedPerson_age_image* pretty much says what you would write in a comment or doc string. The implementation almost is the documentation. Look for a complete traits UI example based on this sample problem definition in the *Single and Married Person Example* tutorial in this section. Now, as the complexity of a tabular view increases, the definition of the **TabularAdapter** class could possibly start to get large and unwieldy. At this point we could begin refactoring our design to use the **ITabularAdapter** interface and **AnITabularAdapter** implementation class to create *sub-adapters* that can be added to our **TabularAdapter** subclass to extend its functionality even further. Creating sub-adapters and adding them via the **TabularAdapter** *adapters* trait is a topic covered in a follow-on tutorial. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/examples/tutorials/traitsui_4.0/editors/tabular_editor/tutorial.desc0000644000175100001730000000033400000000000031432 0ustar00runnerdocker00000000000000Tabular Editor tabular_editor: Tabular Editor Introduction sm_person_example: Single and Married Person Example python_source_browser: Python Source Browser Example numpy_array: NumPy Array Example ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/examples/tutorials/traitsui_4.0/editors/tutorial.desc0000644000175100001730000000033600000000000026434 0ustar00runnerdocker00000000000000New Traits UI Editors animated_gif: Animated GIF Editor led: LED Editor ie_html: Internet Explorer HTML Editor (Windows Only) flash: Flash Editor (Windows Only) tabular_editor: Tabular Editor ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/examples/tutorials/traitsui_4.0/items.py0000644000175100001730000002127400000000000023757 0ustar00runnerdocker00000000000000# (C) Copyright 2007-2023 Enthought, Inc., Austin, TX # License: BSD Style. # --(Extended Traits UI Item and Editor References)------------------------ """ Extended Traits UI Item and Editor References ============================================= In Traits 3.0, the Traits UI **Item** class and various *editor* classes have been extended to use the new extended trait name support provided by the *on_trait_change* method. In previous Traits versions, for example, **Item** objects could only refer to traits defined directly on a UI context object. For example:: view = View( Item( 'name' ), Item( 'object.age' ), Item( 'handler.status' ) ) In Traits 3.0 this restriction has been lifted, and now **Items** can reference (i.e. edit) any trait reachable from a UI context object:: view = View( Item( 'object.mother.name' ), Item( 'object.axel.chassis.serial_number', style = 'readonly' ) ) Similarly, any Traits UI *editor* classes that previously accepted a trait name now accept an extended trait name:: view = View( Item( 'address' ), Item( 'state', editor = EnumEditor( name = 'handler.country.states' ) ) Because **Items** and *editors* only refer to a single trait, you should not use extended trait references that refer to multiple traits, such as *'foo.[bar,baz]'* or *'foo.+editable'*, since the behavior of such references is not defined. Look at the code tabs for this lesson for a complete example of a Traits UI using extended **Item** and editor references. In particular, the **LeagueModelView Class** tab contains a **View** definition containing extended references. Code Incompatibilities ---------------------- Note that the editor enhancement may cause some incompatibities with editors that previously supported both an *object* and *name* trait, with the *object* trait containing the context object name, and the *name* trait containing the name of the trait on the specified context object. Using the new extended trait references, these have been combined into a single *name* trait. If you encounter such an occurrence in existing code, simply combine the context object name and trait name into a single extended name of the form:: context_object_name.trait_name Warning ------- Avoid extended **Item** references that contain intermediate links that could be *None*. For example, in the following code:: view = View( ... Item( 'object.team.players', ... ) ... ) an exception will be raised if *object.team* is *None*, or is set to *None*, while the view is active, since there is no obvious way to obtain a valid value for *object.team.players* for the associated **Item** *editor* to display. Note that the above example is borrowed from this lesson's demo code, which has additional code written to ensure that *object.team* is not *None*. See the *_model_changed* method in the **LeagueModelView Class** tab, which makes sure that the *team* trait is intialized to a valid value when a new **League** model is set up. """ # ---------------------------------------------------------------- from traits.api import ( Button, HasTraits, Instance, Int, List, Property, Str, cached_property, ) from traitsui.api import ( HGroup, Item, ModelView, ObjectColumn, TableEditor, View, VGroup, ) # --[Player Class]--------------------------------------------------------- # Define a baseball player: class Player(HasTraits): # The name of the player: name = Str('') # The number of hits the player made this season: hits = Int() # --[Team Class]----------------------------------------------------------- # Define a baseball team: class Team(HasTraits): # The name of the team: name = Str('') # The players on the team: players = List(Instance(Player)) # The number of players on the team: num_players = Property(observe='players') def _get_num_players(self): """Implementation of the 'num_players' property.""" return len(self.players) # --[League Class]--------------------------------------------------------- # Define a baseball league model: class League(HasTraits): # The name of the league: name = Str('') # The teams in the league: teams = List(Instance(Team)) # --[LeagueModelView Class]----------------------------------------------------- # Define a ModelView for a League model: class LeagueModelView(ModelView): # The currently selected team: team = Instance(Team) # The currently selected player: player = Instance(Player) # Button to add a hit to the current player: got_hit = Button('Got a Hit') # The total number of hits total_hits = Property(depends_on='model.teams.players.hits') @cached_property def _get_total_hits(self): """Returns the total number of hits across all teams and players.""" return sum(sum(p.hits for p in t.players) for t in self.model.teams) view = View( VGroup( HGroup( Item('total_hits', style='readonly'), label='League Statistics', show_border=True, ), VGroup( Item( 'model.teams', show_label=False, editor=TableEditor( columns=[ ObjectColumn(name='name', width=0.70), ObjectColumn( name='num_players', label='# Players', editable=False, width=0.29, ), ], selected='object.team', auto_add=True, row_factory=Team, configurable=False, sortable=False, ), ), label='League Teams', show_border=True, ), VGroup( Item( 'object.team.players', # <-- Extended Item name show_label=False, editor=TableEditor( columns=[ ObjectColumn(name='name', width=0.70), ObjectColumn( name='hits', editable=False, width=0.29 ), ], selected='object.player', auto_add=True, row_factory=Player, configurable=False, sortable=False, ), ), '_', HGroup( Item( 'got_hit', show_label=False, enabled_when='player is not None', ) ), label='Team Players', show_labels=False, show_border=True, ), ), resizable=True, ) def _model_changed(self, model): """Handles the 'league' model being initialized.""" if len(model.teams) > 0: self.team = model.teams[0] def _got_hit_changed(self): """Handles the currently selected player making a hit.""" self.player.hits += 1 def _team_changed(self, team): """Handles a new team being selected.""" if len(team.players) > 0: self.player = team.players[0] else: self.player = None # Function to add two numbers (used with 'reduce'): add = lambda a, b: a + b # --[Example*]------------------------------------------------------------- # Define some sample teams and players: blue_birds = Team( name='Blue Birds', players=[ Player(name='Mike Scott', hits=25), Player(name='Willy Shofield', hits=37), Player(name='Tony Barucci', hits=19), ], ) chicken_hawks = Team( name='Chicken Hawks', players=[ Player(name='Jimmy Domore', hits=34), Player(name='Bill Janks', hits=16), Player(name='Tim Saunders', hits=27), ], ) eagles = Team( name='Eagles', players=[ Player(name='Joe Peppers', hits=33), Player(name='Sam Alone', hits=12), Player(name='Roger Clemson', hits=23), ], ) # Create a league and its corresponding model view: demo = LeagueModelView( League( name='National Baseball Conference', teams=[blue_birds, chicken_hawks, eagles], ) ) if __name__ == '__main__': demo.configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/examples/tutorials/traitsui_4.0/model_view.py0000644000175100001730000001275000000000000024767 0ustar00runnerdocker00000000000000# (C) Copyright 2007-2023 Enthought, Inc., Austin, TX # License: BSD Style. # --(ModelView and Controller Classes)------------------------------------- """ ModelView and Controller Classes ================================ Traits 3.0 introduces two new **Handler** subclasses, **ModelView** and **Controller**, both of which are intended to help simplify the process of creating MVC (i.e. Model/View/Controller) based applications. Both **Controller** and **ModelView** classes add the following new traits to the **Handler** subclass:: # The model this handler defines a view and controller for: model = Instance( HasTraits ) # The UIInfo object associated with the controller: info = Instance( UIInfo ) The *model* trait provides simple access to the model object associated with either the **Controller** or **ModelView** class. Normally, the *model* trait is set in the constructor when a **Controller** or **ModelView** subclass is created. See the **Example** tab for an example of this. The *info* trait provides access to the **UIInfo** object associated with the active user interface view for the handler object. The *info* trait is set automatically when the handler's object view is created. So far, the **Controller** and **ModelView** classes are identical. The only difference between them is in the *context* dictionary each class defines when it creates it associated user interface. For a **Controller** subclass, the *context* dictionary contains: object The **Controller**'s *model* object. controller The **Controller** object itself. For a **ModelView** subclass, the *context* dictionary contains: object The **ModelView** object itself. model The **ModelView**'s *model* object. The **Controller** class is normally used when implementing a standard MVC-based design. In this case, the *model* object contains most, if not all, of the data being viewed, and can be easily referenced in the controller's **View** definition using unqualified trait names (e.g. *Item( 'name' )*). The **ModelView** class is useful when creating a variant of the standard MVC-based design the author likes to refer to as a *model-view*. In this pattern, the **ModelView** subclass typically reformulates a number of the traits on its associated *model* object as properties on the **ModelView** class, usually for the purpose of converting the model's data into a more user interface friendly format. So in this design, the **ModelView** class not only supplies the view and controller, but also, in effect, the model (as a set of properties wrapped around the original model). Because of this, the **ModelView** context dictionary specifies itself as the special *object* value, and assigns the original model as the *model* value. Again, the main purpose of this is to allow easy reference to the **ModelView**'s property traits within its **View** using unqualified trait names. Other than these somewhat subtle, although useful, distinctions, the **Controller** and **ModelView** classes are identical, and which class to use is really a personal preference as much as it is a design decision. Calling the Constructor ----------------------- Both the **ModelView** and **Controller** classes have the same constructor signature:: ModelView( model = None, **metadata ) Controller( model = None, **metadata ) So both of the following are valid examples of creating a controller for a model:: mv = MyModelView( my_model ) c = MyController( model = my_model ) An Example ---------- The code portion of this lesson provides a complete example of using a **ModelView** design. Refer to the lesson on *Delegation Fixes and Improvements* for an example of a related **Controller** based design. """ # ---------------------------------------------------------------- from traits.api import ( HasTraits, Instance, List, Property, PrototypedFrom, Str, ) from traitsui.api import Item, ModelView, ObjectColumn, TableEditor, View # --[Parent Class]--------------------------------------------------------- class Parent(HasTraits): first_name = Str() last_name = Str() # --[Child Class]---------------------------------------------------------- class Child(HasTraits): mother = Instance(Parent) father = Instance(Parent) first_name = Str() last_name = PrototypedFrom('father') # --[ChildModelView Class]------------------------------------------------- class ChildModelView(ModelView): # Define the 'family' ModelView property that maps the child and its # parents into a list of objects that can be viewed as a table: family = Property(List) # Define a view showing the family as a table: view = View( Item( 'family', show_label=False, editor=TableEditor( columns=[ ObjectColumn(name='first_name'), ObjectColumn(name='last_name'), ] ), ), resizable=True, ) # Implementation of the 'family' property: def _get_family(self): return [self.model.father, self.model.mother, self.model] # --[Example*]------------------------------------------------------------- # Create a sample family: mom = Parent(first_name='Julia', last_name='Wilson') dad = Parent(first_name='William', last_name='Chase') son = Child(mother=mom, father=dad, first_name='John') # Create the controller for the model: demo = ChildModelView(model=son) if __name__ == '__main__': demo.configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/examples/tutorials/traitsui_4.0/tutorial.desc0000644000175100001730000000034000000000000024756 0ustar00runnerdocker00000000000000Traits UI Changes and Enhancements deferred: Deferred UI Notifications buttons: View Default Button Changes model_view: ModelView and Controller Classes items: Extended Traits UI Item and Editor References editors ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/examples/tutorials/tutor.py0000644000175100001730000000250400000000000021561 0ustar00runnerdocker00000000000000# ------------------------------------------------------------------------- # # (C) Copyright 2007-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # # ------------------------------------------------------------------------- """ Script to run the tutorial. """ import os import sys from traitsui.extras.demo import demo # Correct program usage information: usage = """ Correct usage is: tutor.py [root_dir] where: root_dir = Path to root of the tutorial tree If omitted, 'root_dir' defaults to the current directory.""" def main(root_dir): # Create a tutor and display the tutorial: path, name = os.path.splitext(root_dir) demo(dir_name=root_dir) if __name__ == '__main__': # Validate the command line arguments: if len(sys.argv) > 2: print(usage) sys.exit(1) # Determine the root path to use for the tutorial files: if len(sys.argv) == 2: root_dir = sys.argv[1] else: root_dir = os.getcwd() main(root_dir) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/image_LICENSE.txt0000644000175100001730000000415200000000000017154 0ustar00runnerdocker00000000000000The icons are mostly derived work from other icons. As such they are licensed accordingly to the original license: Project License File ---------------------------------------------------------------------------- Eclipse Eclipse Public License image_LICENSE_Eclipse.txt Enthought BSD 3-Clause LICENSE.txt GV (Gael Varoquaux) Public Domain N/A Qt GNU LGPL 2.1 Nuvola LGPL image_LICENSE_Nuvola.txt OOo LGPL image_LICENSE_OOo.txt Python PSF Trademark https://www.python.org/community/logos/ Unless stated in this file, icons are the work of Enthought, and are released under a 3 clause BSD license. Files and original authors: ---------------------------------------------------------------------------- traitsui/examples/demo/Applications/images: blue_ball.png | Nuvola traitsui/examples/demo/Extras/images: python-logo.png | Python traitsui/extras/images: next.png | Nuvola parent.png | Nuvola previous.png | Nuvola reload.png | Nuvola run.png | Nuvola traitsui/qt/images: closetab.png | Qt next.png | Qt previous.png | Qt traitsui/wx/images: group.png | Nuvola item.png | Eclipse open.png | Nuvola table_add.png | OOo table_colors.png | GV table_delete.png | OOo table_delete_synthetic.png | OOo table_display.png | OOo table_move_down.png | GV table_move_up.png | GV table_no_sort.png | OOo table_prefs.png | OOo table_search.png | OOo table_synthetic.png | OOo table_undelete.png | OOo ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/image_LICENSE_Eclipse.txt0000644000175100001730000002604300000000000020623 0ustar00runnerdocker00000000000000Eclipse Public License - v 1.0 THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE PUBLIC LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION OF THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. 1. DEFINITIONS "Contribution" means: a) in the case of the initial Contributor, the initial code and documentation distributed under this Agreement, and b) in the case of each subsequent Contributor: i)changes to the Program, and ii)additions to the Program; where such changes and/or additions to the Program originate from and are distributed by that particular Contributor. A Contribution 'originates' from a Contributor if it was added to the Program by such Contributor itself or anyone acting on such Contributor's behalf. Contributions do not include additions to the Program which: (i) are separate modules of software distributed in conjunction with the Program under their own license agreement, and (ii) are not derivative works of the Program. "Contributor" means any person or entity that distributes the Program. "Licensed Patents " mean patent claims licensable by a Contributor which are necessarily infringed by the use or sale of its Contribution alone or when combined with the Program. "Program" means the Contributions distributed in accordance with this Agreement. "Recipient" means anyone who receives the Program under this Agreement, including all Contributors. 2. GRANT OF RIGHTS a) Subject to the terms of this Agreement, each Contributor hereby grants Recipient a non-exclusive, worldwide, royalty-free copyright license to reproduce, prepare derivative works of, publicly display, publicly perform, distribute and sublicense the Contribution of such Contributor, if any, and such derivative works, in source code and object code form. b) Subject to the terms of this Agreement, each Contributor hereby grants Recipient a non-exclusive, worldwide, royalty-free patent license under Licensed Patents to make, use, sell, offer to sell, import and otherwise transfer the Contribution of such Contributor, if any, in source code and object code form. This patent license shall apply to the combination of the Contribution and the Program if, at the time the Contribution is added by the Contributor, such addition of the Contribution causes such combination to be covered by the Licensed Patents. The patent license shall not apply to any other combinations which include the Contribution. No hardware per se is licensed hereunder. c) Recipient understands that although each Contributor grants the licenses to its Contributions set forth herein, no assurances are provided by any Contributor that the Program does not infringe the patent or other intellectual property rights of any other entity. Each Contributor disclaims any liability to Recipient for claims brought by any other entity based on infringement of intellectual property rights or otherwise. As a condition to exercising the rights and licenses granted hereunder, each Recipient hereby assumes sole responsibility to secure any other intellectual property rights needed, if any. For example, if a third party patent license is required to allow Recipient to distribute the Program, it is Recipient's responsibility to acquire that license before distributing the Program. d) Each Contributor represents that to its knowledge it has sufficient copyright rights in its Contribution, if any, to grant the copyright license set forth in this Agreement. 3. REQUIREMENTS A Contributor may choose to distribute the Program in object code form under its own license agreement, provided that: a) it complies with the terms and conditions of this Agreement; and b) its license agreement: i) effectively disclaims on behalf of all Contributors all warranties and conditions, express and implied, including warranties or conditions of title and non-infringement, and implied warranties or conditions of merchantability and fitness for a particular purpose; ii) effectively excludes on behalf of all Contributors all liability for damages, including direct, indirect, special, incidental and consequential damages, such as lost profits; iii) states that any provisions which differ from this Agreement are offered by that Contributor alone and not by any other party; and iv) states that source code for the Program is available from such Contributor, and informs licensees how to obtain it in a reasonable manner on or through a medium customarily used for software exchange. When the Program is made available in source code form: a) it must be made available under this Agreement; and b) a copy of this Agreement must be included with each copy of the Program. Contributors may not remove or alter any copyright notices contained within the Program. Each Contributor must identify itself as the originator of its Contribution, if any, in a manner that reasonably allows subsequent Recipients to identify the originator of the Contribution. 4. COMMERCIAL DISTRIBUTION Commercial distributors of software may accept certain responsibilities with respect to end users, business partners and the like. While this license is intended to facilitate the commercial use of the Program, the Contributor who includes the Program in a commercial product offering should do so in a manner which does not create potential liability for other Contributors. Therefore, if a Contributor includes the Program in a commercial product offering, such Contributor ("Commercial Contributor") hereby agrees to defend and indemnify every other Contributor ("Indemnified Contributor") against any losses, damages and costs (collectively "Losses") arising from claims, lawsuits and other legal actions brought by a third party against the Indemnified Contributor to the extent caused by the acts or omissions of such Commercial Contributor in connection with its distribution of the Program in a commercial product offering. The obligations in this section do not apply to any claims or Losses relating to any actual or alleged intellectual property infringement. In order to qualify, an Indemnified Contributor must: a) promptly notify the Commercial Contributor in writing of such claim, and b) allow the Commercial Contributor to control, and cooperate with the Commercial Contributor in, the defense and any related settlement negotiations. The Indemnified Contributor may participate in any such claim at its own expense. For example, a Contributor might include the Program in a commercial product offering, Product X. That Contributor is then a Commercial Contributor. If that Commercial Contributor then makes performance claims, or offers warranties related to Product X, those performance claims and warranties are such Commercial Contributor's responsibility alone. Under this section, the Commercial Contributor would have to defend claims against the other Contributors related to those performance claims and warranties, and if a court requires any other Contributor to pay any damages as a result, the Commercial Contributor must pay those damages. 5. NO WARRANTY EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Each Recipient is solely responsible for determining the appropriateness of using and distributing the Program and assumes all risks associated with its exercise of rights under this Agreement , including but not limited to the risks and costs of program errors, compliance with applicable laws, damage to or loss of data, programs or equipment, and unavailability or interruption of operations. 6. DISCLAIMER OF LIABILITY EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT NOR ANY CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION LOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 7. GENERAL If any provision of this Agreement is invalid or unenforceable under applicable law, it shall not affect the validity or enforceability of the remainder of the terms of this Agreement, and without further action by the parties hereto, such provision shall be reformed to the minimum extent necessary to make such provision valid and enforceable. If Recipient institutes patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Program itself (excluding combinations of the Program with other software or hardware) infringes such Recipient's patent(s), then such Recipient's rights granted under Section 2(b) shall terminate as of the date such litigation is filed. All Recipient's rights under this Agreement shall terminate if it fails to comply with any of the material terms or conditions of this Agreement and does not cure such failure in a reasonable period of time after becoming aware of such noncompliance. If all Recipient's rights under this Agreement terminate, Recipient agrees to cease use and distribution of the Program as soon as reasonably practicable. However, Recipient's obligations under this Agreement and any licenses granted by Recipient relating to the Program shall continue and survive. Everyone is permitted to copy and distribute copies of this Agreement, but in order to avoid inconsistency the Agreement is copyrighted and may only be modified in the following manner. The Agreement Steward reserves the right to publish new versions (including revisions) of this Agreement from time to time. No one other than the Agreement Steward has the right to modify this Agreement. The Eclipse Foundation is the initial Agreement Steward. The Eclipse Foundation may assign the responsibility to serve as the Agreement Steward to a suitable separate entity. Each new version of the Agreement will be given a distinguishing version number. The Program (including Contributions) may always be distributed subject to the version of the Agreement under which it was received. In addition, after a new version of the Agreement is published, Contributor may elect to distribute the Program (including its Contributions) under the new version. Except as expressly stated in Sections 2(a) and 2(b) above, Recipient receives no rights or licenses to the intellectual property of any Contributor under this Agreement, whether expressly, by implication, estoppel or otherwise. All rights in the Program not expressly granted under this Agreement are reserved. This Agreement is governed by the laws of the State of New York and the intellectual property laws of the United States of America. No party to this Agreement will bring a legal action under this Agreement more than one year after the cause of action arose. Each party waives its rights to a jury trial in any resulting litigation. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/image_LICENSE_Nuvola.txt0000644000175100001730000005664500000000000020516 0ustar00runnerdocker00000000000000GNU LESSER GENERAL PUBLIC LICENSE Version 2.1, February 1999 Copyright (C) 1991, 1999 Free Software Foundation, Inc. 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. [This is the first released version of the Lesser GPL. It also counts as the successor of the GNU Library Public License, version 2, hence the version number 2.1.] Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public Licenses are intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This license, the Lesser General Public License, applies to some specially designated software packages--typically libraries--of the Free Software Foundation and other authors who decide to use it. You can use it too, but we suggest you first think carefully about whether this license or the ordinary General Public License is the better strategy to use in any particular case, based on the explanations below. When we speak of free software, we are referring to freedom of use, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish); that you receive source code or can get it if you want it; that you can change the software and use pieces of it in new free programs; and that you are informed that you can do these things. To protect your rights, we need to make restrictions that forbid distributors to deny you these rights or to ask you to surrender these rights. These restrictions translate to certain responsibilities for you if you distribute copies of the library or if you modify it. For example, if you distribute copies of the library, whether gratis or for a fee, you must give the recipients all the rights that we gave you. You must make sure that they, too, receive or can get the source code. If you link other code with the library, you must provide complete object files to the recipients, so that they can relink them with the library after making changes to the library and recompiling it. And you must show them these terms so they know their rights. We protect your rights with a two-step method: (1) we copyright the library, and (2) we offer you this license, which gives you legal permission to copy, distribute and/or modify the library. To protect each distributor, we want to make it very clear that there is no warranty for the free library. Also, if the library is modified by someone else and passed on, the recipients should know that what they have is not the original version, so that the original author's reputation will not be affected by problems that might be introduced by others. Finally, software patents pose a constant threat to the existence of any free program. We wish to make sure that a company cannot effectively restrict the users of a free program by obtaining a restrictive license from a patent holder. Therefore, we insist that any patent license obtained for a version of the library must be consistent with the full freedom of use specified in this license. Most GNU software, including some libraries, is covered by the ordinary GNU General Public License. This license, the GNU Lesser General Public License, applies to certain designated libraries, and is quite different from the ordinary General Public License. We use this license for certain libraries in order to permit linking those libraries into non-free programs. When a program is linked with a library, whether statically or using a shared library, the combination of the two is legally speaking a combined work, a derivative of the original library. The ordinary General Public License therefore permits such linking only if the entire combination fits its criteria of freedom. The Lesser General Public License permits more lax criteria for linking other code with the library. We call this license the "Lesser" General Public License because it does Less to protect the user's freedom than the ordinary General Public License. It also provides other free software developers Less of an advantage over competing non-free programs. These disadvantages are the reason we use the ordinary General Public License for many libraries. However, the Lesser license provides advantages in certain special circumstances. For example, on rare occasions, there may be a special need to encourage the widest possible use of a certain library, so that it becomes a de-facto standard. To achieve this, non-free programs must be allowed to use the library. A more frequent case is that a free library does the same job as widely used non-free libraries. In this case, there is little to gain by limiting the free library to free software only, so we use the Lesser General Public License. In other cases, permission to use a particular library in non-free programs enables a greater number of people to use a large body of free software. For example, permission to use the GNU C Library in non-free programs enables many more people to use the whole GNU operating system, as well as its variant, the GNU/Linux operating system. Although the Lesser General Public License is Less protective of the users' freedom, it does ensure that the user of a program that is linked with the Library has the freedom and the wherewithal to run that program using a modified version of the Library. The precise terms and conditions for copying, distribution and modification follow. Pay close attention to the difference between a "work based on the library" and a "work that uses the library". The former contains code derived from the library, whereas the latter must be combined with the library in order to run. TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License Agreement applies to any software library or other program which contains a notice placed by the copyright holder or other authorized party saying it may be distributed under the terms of this Lesser General Public License (also called "this License"). Each licensee is addressed as "you". A "library" means a collection of software functions and/or data prepared so as to be conveniently linked with application programs (which use some of those functions and data) to form executables. The "Library", below, refers to any such software library or work which has been distributed under these terms. A "work based on the Library" means either the Library or any derivative work under copyright law: that is to say, a work containing the Library or a portion of it, either verbatim or with modifications and/or translated straightforwardly into another language. (Hereinafter, translation is included without limitation in the term "modification".) "Source code" for a work means the preferred form of the work for making modifications to it. For a library, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the library. Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running a program using the Library is not restricted, and output from such a program is covered only if its contents constitute a work based on the Library (independent of the use of the Library in a tool for writing it). Whether that is true depends on what the Library does and what the program that uses the Library does. 1. You may copy and distribute verbatim copies of the Library's complete source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and distribute a copy of this License along with the Library. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Library or any portion of it, thus forming a work based on the Library, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: * a) The modified work must itself be a software library. * b) You must cause the files modified to carry prominent notices stating that you changed the files and the date of any change. * c) You must cause the whole of the work to be licensed at no charge to all third parties under the terms of this License. * d) If a facility in the modified Library refers to a function or a table of data to be supplied by an application program that uses the facility, other than as an argument passed when the facility is invoked, then you must make a good faith effort to ensure that, in the event an application does not supply such function or table, the facility still operates, and performs whatever part of its purpose remains meaningful. (For example, a function in a library to compute square roots has a purpose that is entirely well-defined independent of the application. Therefore, Subsection 2d requires that any application-supplied function or table used by this function must be optional: if the application does not supply it, the square root function must still compute square roots.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Library, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Library, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Library. In addition, mere aggregation of another work not based on the Library with the Library (or with a work based on the Library) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may opt to apply the terms of the ordinary GNU General Public License instead of this License to a given copy of the Library. To do this, you must alter all the notices that refer to this License, so that they refer to the ordinary GNU General Public License, version 2, instead of to this License. (If a newer version than version 2 of the ordinary GNU General Public License has appeared, then you can specify that version instead if you wish.) Do not make any other change in these notices. Once this change is made in a given copy, it is irreversible for that copy, so the ordinary GNU General Public License applies to all subsequent copies and derivative works made from that copy. This option is useful when you wish to copy part of the code of the Library into a program that is not a library. 4. You may copy and distribute the Library (or a portion or derivative of it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange. If distribution of object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place satisfies the requirement to distribute the source code, even though third parties are not compelled to copy the source along with the object code. 5. A program that contains no derivative of any portion of the Library, but is designed to work with the Library by being compiled or linked with it, is called a "work that uses the Library". Such a work, in isolation, is not a derivative work of the Library, and therefore falls outside the scope of this License. However, linking a "work that uses the Library" with the Library creates an executable that is a derivative of the Library (because it contains portions of the Library), rather than a "work that uses the library". The executable is therefore covered by this License. Section 6 states terms for distribution of such executables. When a "work that uses the Library" uses material from a header file that is part of the Library, the object code for the work may be a derivative work of the Library even though the source code is not. Whether this is true is especially significant if the work can be linked without the Library, or if the work is itself a library. The threshold for this to be true is not precisely defined by law. If such an object file uses only numerical parameters, data structure layouts and accessors, and small macros and small inline functions (ten lines or less in length), then the use of the object file is unrestricted, regardless of whether it is legally a derivative work. (Executables containing this object code plus portions of the Library will still fall under Section 6.) Otherwise, if the work is a derivative of the Library, you may distribute the object code for the work under the terms of Section 6. Any executables containing that work also fall under Section 6, whether or not they are linked directly with the Library itself. 6. As an exception to the Sections above, you may also combine or link a "work that uses the Library" with the Library to produce a work containing portions of the Library, and distribute that work under terms of your choice, provided that the terms permit modification of the work for the customer's own use and reverse engineering for debugging such modifications. You must give prominent notice with each copy of the work that the Library is used in it and that the Library and its use are covered by this License. You must supply a copy of this License. If the work during execution displays copyright notices, you must include the copyright notice for the Library among them, as well as a reference directing the user to the copy of this License. Also, you must do one of these things: * a) Accompany the work with the complete corresponding machine-readable source code for the Library including whatever changes were used in the work (which must be distributed under Sections 1 and 2 above); and, if the work is an executable linked with the Library, with the complete machine-readable "work that uses the Library", as object code and/or source code, so that the user can modify the Library and then relink to produce a modified executable containing the modified Library. (It is understood that the user who changes the contents of definitions files in the Library will not necessarily be able to recompile the application to use the modified definitions.) * b) Use a suitable shared library mechanism for linking with the Library. A suitable mechanism is one that (1) uses at run time a copy of the library already present on the user's computer system, rather than copying library functions into the executable, and (2) will operate properly with a modified version of the library, if the user installs one, as long as the modified version is interface-compatible with the version that the work was made with. * c) Accompany the work with a written offer, valid for at least three years, to give the same user the materials specified in Subsection 6a, above, for a charge no more than the cost of performing this distribution. * d) If distribution of the work is made by offering access to copy from a designated place, offer equivalent access to copy the above specified materials from the same place. * e) Verify that the user has already received a copy of these materials or that you have already sent this user a copy. For an executable, the required form of the "work that uses the Library" must include any data and utility programs needed for reproducing the executable from it. However, as a special exception, the materials to be distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. It may happen that this requirement contradicts the license restrictions of other proprietary libraries that do not normally accompany the operating system. Such a contradiction means you cannot use both them and the Library together in an executable that you distribute. 7. You may place library facilities that are a work based on the Library side-by-side in a single library together with other library facilities not covered by this License, and distribute such a combined library, provided that the separate distribution of the work based on the Library and of the other library facilities is otherwise permitted, and provided that you do these two things: * a) Accompany the combined library with a copy of the same work based on the Library, uncombined with any other library facilities. This must be distributed under the terms of the Sections above. * b) Give prominent notice with the combined library of the fact that part of it is a work based on the Library, and explaining where to find the accompanying uncombined form of the same work. 8. You may not copy, modify, sublicense, link with, or distribute the Library except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense, link with, or distribute the Library is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 9. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Library or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Library (or any work based on the Library), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Library or works based on it. 10. Each time you redistribute the Library (or any work based on the Library), the recipient automatically receives a license from the original licensor to copy, distribute, link with or modify the Library subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties with this License. 11. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Library at all. For example, if a patent license would not permit royalty-free redistribution of the Library by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Library. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply, and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 12. If the distribution and/or use of the Library is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Library under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 13. The Free Software Foundation may publish revised and/or new versions of the Lesser General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Library specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Library does not specify a license version number, you may choose any version ever published by the Free Software Foundation. 14. If you wish to incorporate parts of the Library into other free programs whose distribution conditions are incompatible with these, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/image_LICENSE_OOo.txt0000644000175100001730000001641100000000000017731 0ustar00runnerdocker00000000000000GNU LESSER GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. This version of the GNU Lesser General Public License incorporates the terms and conditions of version 3 of the GNU General Public License, supplemented by the additional permissions listed below. 0. Additional Definitions. As used herein, this License refers to version 3 of the GNU Lesser General Public License, and the GNU GPL refers to version 3 of the GNU General Public License. The Library refers to a covered work governed by this License, other than an Application or a Combined Work as defined below. An Application is any work that makes use of an interface provided by the Library, but which is not otherwise based on the Library. Defining a subclass of a class defined by the Library is deemed a mode of using an interface provided by the Library. A Combined Work is a work produced by combining or linking an Application with the Library. The particular version of the Library with which the Combined Work was made is also called the Linked Version. The Minimal Corresponding Source for a Combined Work means the Corresponding Source for the Combined Work, excluding any source code for portions of the Combined Work that, considered in isolation, are based on the Application, and not on the Linked Version. The Corresponding Application Code for a Combined Work means the object code and/or source code for the Application, including any data and utility programs needed for reproducing the Combined Work from the Application, but excluding the System Libraries of the Combined Work. 1. Exception to Section 3 of the GNU GPL. You may convey a covered work under sections 3 and 4 of this License without being bound by section 3 of the GNU GPL. 2. Conveying Modified Versions. If you modify a copy of the Library, and, in your modifications, a facility refers to a function or data to be supplied by an Application that uses the facility (other than as an argument passed when the facility is invoked), then you may convey a copy of the modified version: * a) under this License, provided that you make a good faith effort to ensure that, in the event an Application does not supply the function or data, the facility still operates, and performs whatever part of its purpose remains meaningful, or * b) under the GNU GPL, with none of the additional permissions of this License applicable to that copy. 3. Object Code Incorporating Material from Library Header Files. The object code form of an Application may incorporate material from a header file that is part of the Library. You may convey such object code under terms of your choice, provided that, if the incorporated material is not limited to numerical parameters, data structure layouts and accessors, or small macros, inline functions and templates (ten or fewer lines in length), you do both of the following: * a) Give prominent notice with each copy of the object code that the Library is used in it and that the Library and its use are covered by this License. * b) Accompany the object code with a copy of the GNU GPL and this license document. 4. Combined Works. You may convey a Combined Work under terms of your choice that, taken together, effectively do not restrict modification of the portions of the Library contained in the Combined Work and reverse engineering for debugging such modifications, if you also do each of the following: * a) Give prominent notice with each copy of the Combined Work that the Library is used in it and that the Library and its use are covered by this License. * b) Accompany the Combined Work with a copy of the GNU GPL and this license document. * c) For a Combined Work that displays copyright notices during execution, include the copyright notice for the Library among these notices, as well as a reference directing the user to the copies of the GNU GPL and this license document. * d) Do one of the following: o 0) Convey the Minimal Corresponding Source under the terms of this License, and the Corresponding Application Code in a form suitable for, and under terms that permit, the user to recombine or relink the Application with a modified version of the Linked Version to produce a modified Combined Work, in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source. o 1) Use a suitable shared library mechanism for linking with the Library. A suitable mechanism is one that (a) uses at run time a copy of the Library already present on the user's computer system, and (b) will operate properly with a modified version of the Library that is interface-compatible with the Linked Version. * e) Provide Installation Information, but only if you would otherwise be required to provide such information under section 6 of the GNU GPL, and only to the extent that such information is necessary to install and execute a modified version of the Combined Work produced by recombining or relinking the Application with a modified version of the Linked Version. (If you use option 4d0, the Installation Information must accompany the Minimal Corresponding Source and Corresponding Application Code. If you use option 4d1, you must provide the Installation Information in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source.) 5. Combined Libraries. You may place library facilities that are a work based on the Library side by side in a single library together with other library facilities that are not Applications and are not covered by this License, and convey such a combined library under terms of your choice, if you do both of the following: * a) Accompany the combined library with a copy of the same work based on the Library, uncombined with any other library facilities, conveyed under the terms of this License. * b) Give prominent notice with the combined library that part of it is a work based on the Library, and explaining where to find the accompanying uncombined form of the same work. 6. Revised Versions of the GNU Lesser General Public License. The Free Software Foundation may publish revised and/or new versions of the GNU Lesser General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Library as you received it specifies that a certain numbered version of the GNU Lesser General Public License or any later version applies to it, you have the option of following the terms and conditions either of that published version or of any later version published by the Free Software Foundation. If the Library as you received it does not specify a version number of the GNU Lesser General Public License, you may choose any version of the GNU Lesser General Public License ever published by the Free Software Foundation. If the Library as you received it specifies that a proxy can decide whether future versions of the GNU Lesser General Public License shall apply, that proxy's public statement of acceptance of any version is permanent authorization for you to choose that version for the Library. ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1684768028.0518072 traitsui-8.0.0/integrationtests/0000755000175100001730000000000000000000000017573 5ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/integrationtests/styled_date_editor_test.py0000644000175100001730000000350700000000000025060 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! from datetime import date from traits.api import Date, Dict, HasTraits, List, observe from traitsui.api import View, Item, StyledDateEditor from traitsui.editors.styled_date_editor import CellFormat class Foo(HasTraits): dates = Dict() styles = Dict() fast_dates = List() slow_dates = List() current_date = Date() traits_view = View( Item( "current_date", style="custom", show_label=False, editor=StyledDateEditor( dates_trait="dates", styles_trait="styles" ), ) ) def __init__(self, *args, **kw): HasTraits.__init__(self, *args, **kw) self.styles = { "fast": CellFormat(bold=True, fgcolor="darkGreen"), "slow": CellFormat(italics=True, fgcolor="lightGray"), } self.fast_dates = [ date(2010, 7, 4), date(2010, 7, 3), date(2010, 7, 2), ] self.slow_dates = [ date(2010, 6, 28), date(2010, 6, 27), date(2010, 6, 24), ] @observe("fast_dates,slow_dates") def _update_dates_dict(self, event): self.dates["fast"] = self.fast_dates self.dates["slow"] = self.slow_dates def _current_date_changed(self, old, new): print("Old:", old, "New:", new) def main(): foo = Foo() foo.configure_traits() if __name__ == "__main__": main() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/integrationtests/test_all_examples.py0000644000175100001730000002600000000000000023650 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Tests for demo and tutorial examples. """ import contextlib import io import os import sys import traceback import unittest from unittest import mock import pkg_resources from traits.api import HasTraits from traitsui.tests._tools import ( BaseTestMixin, is_qt, is_qt5, is_qt6, is_wx, process_cascade_events, requires_toolkit, ToolkitName, ) from traitsui.testing.api import UITester # This test file is not distributed nor is it in a package. HERE = os.path.dirname(__file__) class ExampleSearcher: """This object collects and reports example files to be tested.""" def __init__(self, source_dirs): """ Parameters ---------- source_dirs : list of str List of directory paths from which Python files will be collected. """ self.source_dirs = source_dirs self.files_may_be_skipped = {} def skip_file_if(self, filepath, condition, reason): """Mark a file to be skipped for a given condition. Parameters ---------- filepath : str Path of the file which may be skipped from tests. condition: callable() -> bool The condition for skipping a file. reason : str Reason for skipping the file. """ filepath = os.path.abspath(filepath) self.files_may_be_skipped[filepath] = (condition, reason) def is_skipped(self, filepath): """Return if the Python file should be skipped in test. Parameters ---------- path : str Path to a file. Returns ------- skipped : bool True if the file should be skipped. reason : str Reason why it should be skipped. """ path = os.path.abspath(filepath) if path not in self.files_may_be_skipped: return False, "" condition, reason = self.files_may_be_skipped[path] return condition(), reason def validate(self): """Validate configuration. Currently this checks all files that may be skipped still exist. """ for filepath in self.files_may_be_skipped: if not os.path.exists(filepath): raise RuntimeError("{} does not exist.".format(filepath)) @staticmethod def _is_python_file(path): """Return true if the given path is (public) non-test Python file.""" _, basename = os.path.split(path) _, ext = os.path.splitext(basename) return ( ext == ".py" and not basename.startswith("_") and not basename.startswith("test_") ) def get_python_files(self): """Report Python files to be tested or to be skipped. Returns ------- accepted_files : list of str Python file paths to be tested. skipped_files : (filepath: str, reason: str) Skipped files. First item is the file path, second item is the reason why it is skipped. """ accepted_files = [] skipped_files = [] for source_dir in self.source_dirs: for root, _, files in os.walk(source_dir): for filename in files: path = os.path.abspath(os.path.join(root, filename)) if not self._is_python_file(path): continue skipped, reason = self.is_skipped(path) if skipped: skipped_files.append((path, reason)) else: accepted_files.append(path) # Avoid arbitrary ordering from the OS return sorted(accepted_files), sorted(skipped_files) # ============================================================================= # Configuration # ============================================================================= # Tutorial files are not part of the package data TUTORIALS = os.path.join( HERE, "..", "examples", "tutorials", "doc_examples", "examples", ) # Demo files are part of the package data. DEMO = pkg_resources.resource_filename("traitsui", "examples/demo") #: Explicitly include folders from which example files should be found #: recursively. SOURCE_DIRS = [ DEMO, TUTORIALS, ] SEARCHER = ExampleSearcher(source_dirs=SOURCE_DIRS) SEARCHER.skip_file_if( os.path.join(DEMO, "Advanced", "HDF5_tree_demo.py"), lambda: sys.platform == "darwin", "This example depends on PyTables which may be built to require CPUs with " "a specific AVX version that is not supported on a paricular OSX host.", ) SEARCHER.skip_file_if( os.path.join(DEMO, "Advanced", "Table_editor_with_progress_column.py"), is_wx, "ProgressRenderer is not implemented in wx.", ) SEARCHER.skip_file_if( os.path.join(DEMO, "Advanced", "Scrubber_editor_demo.py"), is_qt, "ScrubberEditor is not implemented in qt.", ) SEARCHER.skip_file_if( os.path.join(DEMO, "Extras", "animated_GIF.py"), lambda: not is_wx(), "Only support wx", ) SEARCHER.skip_file_if( os.path.join(DEMO, "Extras", "Tree_editor_with_TreeNodeRenderer.py"), lambda: not is_qt(), "Only support Qt", ) SEARCHER.skip_file_if( os.path.join(DEMO, "Extras", "windows", "flash.py"), lambda: not is_wx(), "Only support wx", ) SEARCHER.skip_file_if( os.path.join(DEMO, "Extras", "windows", "internet_explorer.py"), lambda: not is_wx(), "Only support wx", ) SEARCHER.skip_file_if( os.path.join(TUTORIALS, "view_multi_object.py"), lambda: True, "Require wx and is blocking.", ) SEARCHER.skip_file_if( os.path.join(TUTORIALS, "view_standalone.py"), lambda: True, "Require wx and is blocking.", ) # Validate configuration. SEARCHER.validate() # ============================================================================= # Test run utility functions # ============================================================================= def replaced_configure_traits( instance, filename=None, view=None, kind=None, edit=True, context=None, handler=None, id="", scrollable=None, **args, ): """Mocked configure_traits to launch then close the GUI.""" ui_kwargs = dict( view=view, parent=None, kind="live", # other options may block the test context=context, handler=handler, id=id, scrollable=scrollable, **args, ) with UITester().create_ui(instance, ui_kwargs): pass @contextlib.contextmanager def replace_configure_traits(): """Context manager to temporarily replace HasTraits.configure_traits with a mocked version such that GUI launched are closed soon after they are open. """ original_func = HasTraits.configure_traits HasTraits.configure_traits = replaced_configure_traits try: yield finally: HasTraits.configure_traits = original_func def run_file(file_path): """Execute a given Python file. Parameters ---------- file_path : str File path to be tested. """ with open(file_path, "r", encoding="utf-8") as f: content = f.read() globals = { "__name__": "__main__", "__file__": file_path, } with replace_configure_traits(), mock.patch( "sys.stdout", new_callable=io.StringIO ), mock.patch("sys.argv", [file_path]): # Mock stdout: Examples typically print educational information. # They are expected but they should not pollute test output. # Mock argv: Some example reads sys.argv to allow more arguments # But all examples should support being run without additional # arguments. exec(content, globals) # ============================================================================= # load_tests protocol for unittest discover # ============================================================================= def load_tests(loader, tests, pattern): """Implement load_tests protocol so that when unittest discover is run with this test module, the tests in the demo folder (not a package) are also loaded. See unittest documentation on load_tests """ # Keep all the other loaded tests. suite = unittest.TestSuite() suite.addTests(tests) # Expand the test suite with tests from the examples, assuming # the test for ``group/script.py`` is placed in ``group/tests/`` directory. accepted_files, _ = SEARCHER.get_python_files() test_dirs = set( os.path.join(os.path.dirname(path), "tests") for path in accepted_files ) test_dirs = set(path for path in test_dirs if os.path.exists(path)) for dirpath in sorted(test_dirs): # Test files are scripts too and they demonstrate running the # tests. Mock the run side-effect when we load the test cases. with mock.patch.object(unittest.TextTestRunner, "run"): test_suite = unittest.TestLoader().discover( dirpath, pattern=pattern ) if is_qt() or is_wx(): suite.addTests(test_suite) return suite # ============================================================================= # Test cases # ============================================================================= @requires_toolkit([ToolkitName.qt, ToolkitName.wx]) class TestExample(BaseTestMixin, unittest.TestCase): def setUp(self): BaseTestMixin.setUp(self) def tearDown(self): BaseTestMixin.tearDown(self) def test_run(self): accepted_files, skipped_files = SEARCHER.get_python_files() for file_path in accepted_files: with self.subTest(file_path=file_path): try: run_file(file_path) except Exception as exc: message = "".join( traceback.format_exception(*sys.exc_info()) ) self.fail( "Executing {} failed with exception {}\n {}".format( file_path, exc, message ) ) finally: # Whatever failure, always flush the GUI event queue # before running the next one. process_cascade_events() # Report skipped files for file_path, reason in skipped_files: with self.subTest(file_path=file_path): # make up for unittest not reporting the parameter in skip # message. raise unittest.SkipTest( "{reason} (File: {file_path})".format( reason=reason, file_path=file_path ) ) ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1684768028.055807 traitsui-8.0.0/integrationtests/ui/0000755000175100001730000000000000000000000020210 5ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/integrationtests/ui/array_editor_test.py0000644000175100001730000000344400000000000024312 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # ------------------------------------------------------------------------- # Imports: # ------------------------------------------------------------------------- from traits.api import HasPrivateTraits, Array from traitsui.api import View, ArrayEditor # ------------------------------------------------------------------------- # 'Test' class: # ------------------------------------------------------------------------- class Test(HasPrivateTraits): # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- three = Array(int, (3, 3)) four = Array(float, (4, 4), editor=ArrayEditor(width=-50)) # ------------------------------------------------------------------------- # Traits view definitions: # ------------------------------------------------------------------------- view = View( 'three', '_', 'three', '_', 'three~', '_', 'four', '_', 'four', '_', 'four~', title='ArrayEditor Test Case', resizable=True, ) # ------------------------------------------------------------------------- # Run the test case: # ------------------------------------------------------------------------- if __name__ == '__main__': Test().configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/integrationtests/ui/buttons_test.py0000644000175100001730000000653500000000000023330 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! from traits.api import Int, Regex, Str from traitsui.api import ( Action, CancelButton, Group, Handler, LiveButtons, ModalButtons, OKButton, View, ) # ------------------------------------------------------------------------- # 'Person' class: # ------------------------------------------------------------------------- class Person(Handler): # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- name = Str() age = Int() phone = Regex(value='000-0000', regex=r'\d\d\d[-]\d\d\d\d') notes = Str() # ------------------------------------------------------------------------- # Handles the 'Annoy' button being clicked: # ------------------------------------------------------------------------- def _annoy_clicked(self, info): self.edit_traits( view=View(title='Annoying', kind='modal', buttons=['OK']) ) # ------------------------------------------------------------------------- # Run the tests: # ------------------------------------------------------------------------- if __name__ == '__main__': AnnoyButton = Action( name='Annoy', tooltip='Click me to be annoyed', enabled_when='age >= 40', ) person = Person(name='Bill', age=42, phone='555-1212') fields = Group('name', 'age', 'phone', 'notes~') person.notes = ( "Should have 6 standard 'live' buttons: Undo, Redo, " "Revert, OK, Cancel, Help" ) person.configure_traits( view=View(fields, kind='livemodal', buttons=LiveButtons) ) person.notes = ( "Should have 5 standard 'modal' buttons: Apply, Revert, " "OK, Cancel, Help" ) person.configure_traits(view=View(fields, buttons=ModalButtons)) person.notes = "Should have 2 standard buttons: OK, Cancel" person.configure_traits( view=View(fields, buttons=[OKButton, CancelButton]) ) person.notes = "Should have 1 standard button: OK (enabled when age >= 40)" person.configure_traits( view=View( fields, buttons=[Action(name='OK', enabled_when='age >= 40')] ) ) person.notes = "Should have 1 standard button: OK (visible when age >= 40)" person.configure_traits( view=View( fields, buttons=[Action(name='OK', visible_when='age >= 40')] ) ) person.notes = ( "Should have 2 standard buttons: OK, Help (defined when " "age >= 50)" ) person.configure_traits( view=View( fields, buttons=['OK', Action(name='Help', defined_when='age >= 50')], ) ) person.notes = ( "Should have 1 user and 5 standard buttons: Annoy (enabled " "when age >= 40), Apply, Revert, OK, Cancel, Help" ) person.configure_traits( view=View(fields, buttons=[AnnoyButton] + ModalButtons) ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/integrationtests/ui/check_list_editor_test.py0000644000175100001730000000446500000000000025310 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # ------------------------------------------------------------------------- # Imports: # ------------------------------------------------------------------------- from traits.api import Enum, List from traitsui.api import Handler, View, Item, CheckListEditor # ------------------------------------------------------------------------- # Constants: # ------------------------------------------------------------------------- colors = ['red', 'orange', 'yellow', 'green', 'blue', 'indigo', 'violet'] numbers = [ 'one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine', 'ten', ] # ------------------------------------------------------------------------- # 'CheckListTest' class: # ------------------------------------------------------------------------- class CheckListTest(Handler): # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- case = Enum('Colors', 'Numbers') value = List(editor=CheckListEditor(values=colors, cols=5)) # ------------------------------------------------------------------------- # Event handlers: # ------------------------------------------------------------------------- def object_case_changed(self, info): if self.case == 'Colors': info.value.factory.values = colors else: info.value.factory.values = numbers # ------------------------------------------------------------------------- # Run the tests: # ------------------------------------------------------------------------- if __name__ == '__main__': clt = CheckListTest() clt.configure_traits(view=View('case', '_', Item('value', id='value'))) print('value:', clt.value) clt.configure_traits(view=View('case', '_', Item('value@', id='value'))) print('value:', clt.value) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/integrationtests/ui/check_list_editor_test2.py0000644000175100001730000000464200000000000025367 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # ------------------------------------------------------------------------- # Imports: # ------------------------------------------------------------------------- from traits.api import Enum, List, Str from traitsui.api import Handler, View, Item, CheckListEditor # ------------------------------------------------------------------------- # 'CheckListTest' class: # ------------------------------------------------------------------------- class CheckListTest(Handler): # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- value = List(editor=CheckListEditor(name='values', cols=5)) values = List(Str) values_text = Str('red orange yellow green blue indigo violet') # ------------------------------------------------------------------------- # Traits view definitions: # ------------------------------------------------------------------------- simple_view = View('value', 'values_text@') custom_view = View('value@', 'values_text@') # ------------------------------------------------------------------------- # 'Initializes the object: # ------------------------------------------------------------------------- def __init__(self, **traits): super().__init__(**traits) self._values_text_changed() # ------------------------------------------------------------------------- # Event handlers: # ------------------------------------------------------------------------- def _values_text_changed(self): self.values = self.values_text.split() # ------------------------------------------------------------------------- # Run the tests: # ------------------------------------------------------------------------- if __name__ == '__main__': clt = CheckListTest() clt.configure_traits(view='simple_view') print('value:', clt.value) clt.configure_traits(view='custom_view') print('value:', clt.value) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/integrationtests/ui/code_editor_test.py0000644000175100001730000000533600000000000024110 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Test using a KeyBindings object with the traits CodeEditor.""" # ------------------------------------------------------------------------- # Imports: # ------------------------------------------------------------------------- from traits.api import HasPrivateTraits, Code, Str from traitsui.api import View, Item, Handler, CodeEditor from traitsui.key_bindings import KeyBinding, KeyBindings # ------------------------------------------------------------------------- # Define a KeyBindings object: # ------------------------------------------------------------------------- key_bindings = KeyBindings( KeyBinding( binding1='Ctrl-s', description='Save to a file', method_name='save_file', ), KeyBinding( binding1='Ctrl-r', description='Run script', method_name='run_script' ), KeyBinding( binding1='Ctrl-q', description='Edit key bindings', method_name='edit_bindings', ), ) # ------------------------------------------------------------------------- # 'CodeHandler' class: # ------------------------------------------------------------------------- class CodeHandler(Handler): def save_file(self, info): info.object.status = "save file" def run_script(self, info): info.object.status = "run script" def edit_bindings(self, info): info.object.status = "edit bindings" key_bindings.edit_traits() # ------------------------------------------------------------------------- # 'TestCode' class: # ------------------------------------------------------------------------- class TestCode(HasPrivateTraits): code = Code() status = Str() view = View( [ Item( 'code', style='custom', resizable=True, editor=CodeEditor(key_bindings=key_bindings), ), 'status~', '|<>', ], id='traitsui.tests.test_code_editor.TestCode', title='Sample Code Editor', width=0.4, height=0.4, resizable=True, handler=CodeHandler(), ) # ------------------------------------------------------------------------- # Run the test: # ------------------------------------------------------------------------- if __name__ == '__main__': TestCode().configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/integrationtests/ui/enum_dynamic_test.py0000644000175100001730000000167600000000000024303 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! from traits.api import HasTraits, List, Str from traitsui.api import EnumEditor, Item, View def evaluate_value(v): print('evaluate_value', v) return str(v) class Team(HasTraits): captain = Str('Dick') players = List(['Tom', 'Dick', 'Harry', 'Sally'], Str) captain_editor = EnumEditor(name='players', evaluate=evaluate_value) view = View( Item('captain', editor=captain_editor), '_', 'players@', height=200 ) if __name__ == '__main__': team = Team() team.configure_traits() team.print_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/integrationtests/ui/enum_editor_test.py0000644000175100001730000000345100000000000024136 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # ------------------------------------------------------------------------- # Imports: # ------------------------------------------------------------------------- from traits.api import HasTraits, Trait, Enum, Range from traitsui.api import View, Item, EnumEditor # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- values = ['one', 'two', 'three', 'four'] enum = Enum(*values) range = Range(1, 4) # ------------------------------------------------------------------------- # 'TestEnumEditor' class: # ------------------------------------------------------------------------- class TestEnumEditor(HasTraits): # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- value = Trait(1, enum, range) other_value = Range(0, 4) trait_view = View( Item('value', editor=EnumEditor(values=values, evaluate=int)), Item('other_value'), resizable=True, ) # ------------------------------------------------------------------------- # Run the test: # ------------------------------------------------------------------------- if __name__ == '__main__': test = TestEnumEditor() test.configure_traits() test.print_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/integrationtests/ui/html_editor_test.py0000644000175100001730000000444000000000000024135 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # ------------------------------------------------------------------------- # Imports: # ------------------------------------------------------------------------- from traits.api import HasPrivateTraits, Code from traitsui.api import View, Group, Item, HTMLEditor # ------------------------------------------------------------------------- # Constants: # ------------------------------------------------------------------------- sample = """This is a code block: def foo ( bar ): print 'bar:', bar This is an unordered list: - An - unordered - list This is an ordered list: * One * Two * Three Lists can be nested: * One * 1.1 * 1.2 * Two * 2.1 * 2.2 """ # ------------------------------------------------------------------------- # 'TestHTML' class: # ------------------------------------------------------------------------- class TestHTML(HasPrivateTraits): # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- # Text string to display as HTML: html = Code(sample) # ------------------------------------------------------------------------- # Traits view definitions: # ------------------------------------------------------------------------- view = View( Group( [Item('html#@', editor=HTMLEditor(format_text=True)), '|<>'], ['{Enter formatted text and/or HTML below:}@', 'html#@', '|<>'], '|<>', layout='split', ), title='HTML Editor Test', resizable=True, width=0.4, height=0.6, ) # ------------------------------------------------------------------------- # Run the test: # ------------------------------------------------------------------------- if __name__ == '__main__': TestHTML().configure_traits() ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1684768028.055807 traitsui-8.0.0/integrationtests/ui/images/0000755000175100001730000000000000000000000021455 5ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/integrationtests/ui/images/bottom_left_origin.gif0000644000175100001730000000125000000000000026027 0ustar00runnerdocker00000000000000GIF89a< ccc]]]KKKqqqVVVzyy~~~bbbƆuu>xRR((GG飣dd[["""쟟ZZZUUhUUooo׻XXaa,<      ɂʿҶ  !"#$$%&'b*Np - ,2j܈F bX$!c&8F y 8q2". 9TL=|@) ,H!=5 !-@B@3B (|G@* ;././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/integrationtests/ui/images/bottom_right_origin.gif0000644000175100001730000000125000000000000026212 0ustar00runnerdocker00000000000000GIF89a< bbb]]]KKKqqqVVVyyy\\\~~~Ɔuu>xRR((kkkLLhhh !!!wwwWW..Ӿ??||˴SSaa,<       ņ  ׻߲!"#$%L8B>B R|RA nVBA*_Pq@16*Xi &fHP@4Ԩa?m#GMtCNq GmC V!!5@H[ v'* ;././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/integrationtests/ui/images/top_left_origin.gif0000644000175100001730000000125000000000000025325 0ustar00runnerdocker00000000000000GIF89a<JJ]]dd[[ """쟟[[[tttaaa]]]UU `MMnnn¸oIIZe&&JIIyyy555~~~WWWfff===Ǔ,<   ȶ !"#$$%&Я'()*+,-,,./%01 234567H5G&bIAtqP++&X#ǓxLA*0Uf̕4_(3Γ=].VZ- %ʁ-,L2 Ä?nH0Z @48AnxÞ^ m'BN ;././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/integrationtests/ui/images/top_right_origin.gif0000644000175100001730000000123400000000000025512 0ustar00runnerdocker00000000000000GIF89a<JJkkk ddޝ!!!wwwWW&&Ӿ?? lYYkEEZc$$KKK~~~^^^555oooaaaWWW ===,<   ± "#$%&'('&)&$+,-(./.'0//023456&u@ IN5G 9^ X5 E  nH1cG;>`dH%[D SD7`ƴKY)^, %ʩ0~bCH!@׃ ?@ؕ<@ݔ#Eo;././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/integrationtests/ui/instance_drag_test.py0000644000175100001730000001611700000000000024430 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # ------------------------------------------------------------------------- # Imports: # ------------------------------------------------------------------------- from traits.api import HasTraits, Str, Regex, List, Instance from traitsui.api import ( TreeEditor, TreeNode, View, Group, Item, Handler, InstanceEditor, ) from traitsui.instance_choice import InstanceDropChoice from traitsui.menu import Menu, Action, Separator from traitsui.editors.tree_editor import ( NewAction, CopyAction, CutAction, PasteAction, DeleteAction, RenameAction, ) # ------------------------------------------------------------------------- # 'Employee' class: # ------------------------------------------------------------------------- class Employee(HasTraits): name = Str('') title = Str() phone = Regex(regex=r'\d\d\d-\d\d\d\d') view = View('title', 'phone') def default_title(self): self.title = 'Senior Engineer' # ------------------------------------------------------------------------- # 'Department' class: # ------------------------------------------------------------------------- class Department(HasTraits): name = Str('') employees = List(Employee) view = View(['employees', '|<>']) # ------------------------------------------------------------------------- # 'Company' class: # ------------------------------------------------------------------------- class Company(HasTraits): name = Str('') departments = List(Department) employees = List(Employee) # ------------------------------------------------------------------------- # 'Partner' class: # ------------------------------------------------------------------------- class Partner(HasTraits): name = Str('') company = Instance(Company) eom = Instance(Employee) dom = Instance(Department) # ------------------------------------------------------------------------- # Create a hierarchy: # ------------------------------------------------------------------------- jason = Employee(name='Jason', title='Sr. Engineer', phone='536-1057') mike = Employee(name='Mike', title='Sr. Engineer', phone='536-1057') dave = Employee(name='Dave', title='Sr. Engineer', phone='536-1057') martin = Employee(name='Martin', title='Sr. Engineer', phone='536-1057') duncan = Employee(name='Duncan', title='Sr. Engineer') partner = Partner( name='eric', company=Company( name='Enthought, Inc.', departments=[ Department(name='Business', employees=[jason, mike]), Department(name='Scientific', employees=[dave, martin, duncan]), ], employees=[dave, martin, mike, duncan, jason], ), ) # ------------------------------------------------------------------------- # Define the tree trait editor: # ------------------------------------------------------------------------- no_view = View() tree_editor = TreeEditor( editable=False, nodes=[ TreeNode( node_for=[Company], auto_open=True, children='', label='name', view=View(['name', '|<']), ), TreeNode( node_for=[Company], auto_open=True, children='departments', label='=Departments', view=no_view, add=[Department], ), TreeNode( node_for=[Company], auto_open=True, children='employees', label='=Employees', view=no_view, add=[Employee], ), TreeNode( node_for=[Department], auto_open=True, children='employees', label='name', menu=Menu( NewAction, Separator(), DeleteAction, Separator(), RenameAction, Separator(), CopyAction, CutAction, PasteAction, ), view=View(['name', '|<']), add=[Employee], ), TreeNode( node_for=[Employee], auto_open=True, label='name', menu=Menu( NewAction, Separator(), Action(name='Default title', action='object.default_title'), Action( name='Department', action='handler.employee_department(editor,object)', ), Separator(), CopyAction, CutAction, PasteAction, Separator(), DeleteAction, Separator(), RenameAction, ), view=View(['name', 'title', 'phone', '|<']), ), ], ) # ------------------------------------------------------------------------- # 'TreeHandler' class: # ------------------------------------------------------------------------- class TreeHandler(Handler): def employee_department(self, editor, object): dept = editor.get_parent(object) print('%s works in the %s department.' % (object.name, dept.name)) # ------------------------------------------------------------------------- # Define the View to use: # ------------------------------------------------------------------------- view = View( Group( [Item('company', editor=tree_editor, resizable=True), '|<>'], Group( [ '{Employee of the Month}@', Item( 'eom@', editor=InstanceEditor( values=[ InstanceDropChoice(klass=Employee, selectable=True) ] ), resizable=True, ), '|<>', ], [ '{Department of the Month}@', Item( 'dom@', editor=InstanceEditor( values=[InstanceDropChoice(klass=Department)] ), resizable=True, ), '|<>', ], show_labels=False, layout='split', ), orientation='horizontal', show_labels=False, layout='split', ), title='Company Structure', handler=TreeHandler(), buttons=['OK', 'Cancel'], resizable=True, width=0.5, height=0.5, ) # ------------------------------------------------------------------------- # Edit it: # ------------------------------------------------------------------------- if __name__ == '__main__': partner.configure_traits(view=view) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/integrationtests/ui/instance_editor_test.py0000644000175100001730000000562600000000000025004 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! from traits.api import HasStrictTraits, Instance, Int, List, Regex, Str from traitsui.api import InstanceEditor, Item, View # ------------------------------------------------------------------------- # 'Person' class: # ------------------------------------------------------------------------- class Person(HasStrictTraits): # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- name = Str() age = Int() phone = Regex(value='000-0000', regex=r'\d\d\d[-]\d\d\d\d') # ------------------------------------------------------------------------- # Traits view definition: # ------------------------------------------------------------------------- traits_view = View('name', 'age', 'phone') # ------------------------------------------------------------------------- # Sample data: # ------------------------------------------------------------------------- people = [ Person(name='Dave', age=39, phone='555-1212'), Person(name='Mike', age=28, phone='555-3526'), Person(name='Joe', age=34, phone='555-6943'), Person(name='Tom', age=22, phone='555-7586'), Person(name='Dick', age=63, phone='555-3895'), Person(name='Harry', age=46, phone='555-3285'), Person(name='Sally', age=43, phone='555-8797'), Person(name='Fields', age=31, phone='555-3547'), ] # ------------------------------------------------------------------------- # 'Team' class: # ------------------------------------------------------------------------- class Team(HasStrictTraits): # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- name = Str() captain = Instance(Person) roster = List(Instance(Person)) # ------------------------------------------------------------------------- # Traits view definitions: # ------------------------------------------------------------------------- traits_view = View( 'name', '_', Item('captain@', editor=InstanceEditor(name='roster')), buttons=['Undo', 'OK', 'Cancel'], ) # ------------------------------------------------------------------------- # Run the test: # ------------------------------------------------------------------------- if __name__ == '__main__': Team(name='Vultures', captain=people[0], roster=people).configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/integrationtests/ui/instance_editor_test2.py0000644000175100001730000000607400000000000025064 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! from traits.api import HasStrictTraits, Instance, Int, List, Regex, Str from traitsui.api import InstanceEditor, InstanceFactoryChoice, Item, View # ------------------------------------------------------------------------- # 'Person' class: # ------------------------------------------------------------------------- class Person(HasStrictTraits): # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- name = Str() age = Int() phone = Regex(value='000-0000', regex=r'\d\d\d[-]\d\d\d\d') # ------------------------------------------------------------------------- # Traits view definition: # ------------------------------------------------------------------------- traits_view = View('name', 'age', 'phone') # ------------------------------------------------------------------------- # Sample data: # ------------------------------------------------------------------------- people = [ Person(name='Dave', age=39, phone='555-1212'), Person(name='Mike', age=28, phone='555-3526'), Person(name='Joe', age=34, phone='555-6943'), Person(name='Tom', age=22, phone='555-7586'), Person(name='Dick', age=63, phone='555-3895'), Person(name='Harry', age=46, phone='555-3285'), Person(name='Sally', age=43, phone='555-8797'), Person(name='Fields', age=31, phone='555-3547'), ] # ------------------------------------------------------------------------- # 'Team' class: # ------------------------------------------------------------------------- class Team(HasStrictTraits): # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- name = Str() captain = Instance(Person) roster = List(Instance(Person)) # ------------------------------------------------------------------------- # Traits view definitions: # ------------------------------------------------------------------------- traits_view = View( 'name', '_', Item( 'captain@', editor=InstanceEditor( name='roster', values=[ InstanceFactoryChoice(klass=Person, name='Non player') ], ), ), ) # ------------------------------------------------------------------------- # Run the test: # ------------------------------------------------------------------------- if __name__ == '__main__': Team(name='Vultures', captain=people[0], roster=people).configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/integrationtests/ui/instance_editor_test3.py0000644000175100001730000000561300000000000025063 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! from traits.api import HasStrictTraits, Instance, Int, List, Regex, Str from traitsui.api import InstanceEditor, Item, View # ------------------------------------------------------------------------- # 'Person' class: # ------------------------------------------------------------------------- class Person(HasStrictTraits): # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- name = Str() age = Int() phone = Regex(value='000-0000', regex=r'\d\d\d[-]\d\d\d\d') # ------------------------------------------------------------------------- # Traits view definition: # ------------------------------------------------------------------------- traits_view = View('name', 'age', 'phone') # ------------------------------------------------------------------------- # Sample data: # ------------------------------------------------------------------------- people = [ Person(name='Dave', age=39, phone='555-1212'), Person(name='Mike', age=28, phone='555-3526'), Person(name='Joe', age=34, phone='555-6943'), Person(name='Tom', age=22, phone='555-7586'), Person(name='Dick', age=63, phone='555-3895'), Person(name='Harry', age=46, phone='555-3285'), Person(name='Sally', age=43, phone='555-8797'), Person(name='Fields', age=31, phone='555-3547'), ] # ------------------------------------------------------------------------- # 'Team' class: # ------------------------------------------------------------------------- class Team(HasStrictTraits): # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- name = Str() captain = Instance(Person) roster = List(Instance(Person)) # ------------------------------------------------------------------------- # Traits view definitions: # ------------------------------------------------------------------------- traits_view = View( 'name', '_', Item('captain@', editor=InstanceEditor(name='roster')), '_', 'roster', ) # ------------------------------------------------------------------------- # Run the test: # ------------------------------------------------------------------------- if __name__ == '__main__': Team(name='Vultures', captain=people[0], roster=people).configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/integrationtests/ui/instance_editor_test4.py0000644000175100001730000000562100000000000025063 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! from traits.api import HasStrictTraits, Instance, Int, List, Regex, Str from traitsui.api import InstanceEditor, Item, View # ------------------------------------------------------------------------- # 'Person' class: # ------------------------------------------------------------------------- class Person(HasStrictTraits): # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- name = Str() age = Int() phone = Regex(value='000-0000', regex=r'\d\d\d[-]\d\d\d\d') # ------------------------------------------------------------------------- # Traits view definition: # ------------------------------------------------------------------------- traits_view = View('name', 'age', 'phone') # ------------------------------------------------------------------------- # Sample data: # ------------------------------------------------------------------------- people = [ Person(name='Dave', age=39, phone='555-1212'), Person(name='Mike', age=28, phone='555-3526'), Person(name='Joe', age=34, phone='555-6943'), Person(name='Tom', age=22, phone='555-7586'), Person(name='Dick', age=63, phone='555-3895'), Person(name='Harry', age=46, phone='555-3285'), Person(name='Sally', age=43, phone='555-8797'), Person(name='Fields', age=31, phone='555-3547'), ] # ------------------------------------------------------------------------- # 'Team' class: # ------------------------------------------------------------------------- class Team(HasStrictTraits): # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- name = Str() captain = Instance(Person) roster = List(Person) # ------------------------------------------------------------------------- # Traits view definitions: # ------------------------------------------------------------------------- traits_view = View( 'name', '_', Item('captain@', editor=InstanceEditor(name='roster', editable=False)), '_', 'roster', ) # ------------------------------------------------------------------------- # Run the test: # ------------------------------------------------------------------------- if __name__ == '__main__': Team(name='Vultures', captain=people[0], roster=people).configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/integrationtests/ui/instance_editor_test5.py0000644000175100001730000000674400000000000025073 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! from traits.api import HasStrictTraits, Instance, Int, List, Regex, Str from traitsui.api import InstanceEditor, InstanceFactoryChoice, Item, View # ------------------------------------------------------------------------- # 'Person' class: # ------------------------------------------------------------------------- class Person(HasStrictTraits): # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- name = Str() age = Int() phone = Regex(value='000-0000', regex=r'\d\d\d[-]\d\d\d\d') # ------------------------------------------------------------------------- # Traits view definition: # ------------------------------------------------------------------------- traits_view = View('name~', 'age~', 'phone~') edit_view = View('name', 'age', 'phone') # ------------------------------------------------------------------------- # Sample data: # ------------------------------------------------------------------------- people = [ Person(name='Dave', age=39, phone='555-1212'), Person(name='Mike', age=28, phone='555-3526'), Person(name='Joe', age=34, phone='555-6943'), Person(name='Tom', age=22, phone='555-7586'), Person(name='Dick', age=63, phone='555-3895'), Person(name='Harry', age=46, phone='555-3285'), Person(name='Sally', age=43, phone='555-8797'), Person(name='Fields', age=31, phone='555-3547'), ] # ------------------------------------------------------------------------- # 'Team' class: # ------------------------------------------------------------------------- class Team(HasStrictTraits): # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- name = Str() captain = Instance(Person) roster = List(Person) # ------------------------------------------------------------------------- # Traits view definitions: # ------------------------------------------------------------------------- traits_view = View( [ [ 'name', '_', Item( 'captain@', editor=InstanceEditor( name='roster', editable=False, values=[ InstanceFactoryChoice( klass=Person, name='Non player', view='edit_view', ) ], ), ), '_', ], ['captain@', '|<>'], ] ) # ------------------------------------------------------------------------- # Run the test: # ------------------------------------------------------------------------- if __name__ == '__main__': t = Team(name='Vultures', captain=people[0], roster=people) t.configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/integrationtests/ui/instance_editor_test6.py0000644000175100001730000000643300000000000025067 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! from traits.api import HasStrictTraits, Instance, Int, List, Regex, Str from traitsui.api import InstanceEditor, InstanceFactoryChoice, Item, View # ------------------------------------------------------------------------- # 'Person' class: # ------------------------------------------------------------------------- class Person(HasStrictTraits): # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- name = Str() age = Int() phone = Regex(value='000-0000', regex=r'\d\d\d[-]\d\d\d\d') # ------------------------------------------------------------------------- # Traits view definition: # ------------------------------------------------------------------------- traits_view = View('name', 'age', 'phone', buttons=['OK', 'Cancel']) # ------------------------------------------------------------------------- # Sample data: # ------------------------------------------------------------------------- people = [ Person(name='Dave', age=39, phone='555-1212'), Person(name='Mike', age=28, phone='555-3526'), Person(name='Joe', age=34, phone='555-6943'), Person(name='Tom', age=22, phone='555-7586'), Person(name='Dick', age=63, phone='555-3895'), Person(name='Harry', age=46, phone='555-3285'), Person(name='Sally', age=43, phone='555-8797'), Person(name='Fields', age=31, phone='555-3547'), ] # ------------------------------------------------------------------------- # 'Team' class: # ------------------------------------------------------------------------- class Team(HasStrictTraits): # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- name = Str() captain = Instance(Person) roster = List(Person) # ------------------------------------------------------------------------- # Traits view definitions: # ------------------------------------------------------------------------- traits_view = View( [ 'name', '_', Item( 'captain', editor=InstanceEditor( name='roster', label='Edit...', values=[ InstanceFactoryChoice( klass=Person, name='Non player', view='edit_view' ) ], ), ), ], buttons=['OK', 'Cancel'], ) # ------------------------------------------------------------------------- # Run the test: # ------------------------------------------------------------------------- if __name__ == '__main__': Team(name='Vultures', captain=people[0], roster=people).configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/integrationtests/ui/large_range_editor.py0000644000175100001730000000305100000000000024375 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! from traits.api import HasTraits, Float, List from traitsui.api import View, Item, RangeEditor # Tests the Large Range Slider editor. It also tests the case where the # editor is embedded in a list. class TestRangeEditor(HasTraits): x = Float() low = Float(123.123) high = Float(1123.123) list = List( Float( editor=RangeEditor( low_name='low', high_name='high', # These force the large range # slider to be used. low=100.0, high=10000.123, ) ) ) view = View( Item( name='x', editor=RangeEditor( low_name='low', high_name='high', # These force the large range # slider to be used. low=100.0, high=10000.123, ), ), Item('list'), resizable=True, ) def test(): a = TestRangeEditor() a.x = 500 a.list.append(500) a.edit_traits() # Just close the resulting dialog. assert a.x == 500 assert a.list[0] == 500 test() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/integrationtests/ui/list_traits_ui_test.py0000644000175100001730000001072300000000000024662 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # ------------------------------------------------------------------------- # Imports: # ------------------------------------------------------------------------- from traits.api import HasStrictTraits, Str, Int, Regex, List, Instance from traitsui.api import View, Item, VSplit, TableEditor, ListEditor from traitsui.table_column import ObjectColumn from traitsui.table_filter import ( TableFilter, RuleTableFilter, RuleFilterTemplate, MenuFilterTemplate, EvalFilterTemplate, ) # ------------------------------------------------------------------------- # 'Person' class: # ------------------------------------------------------------------------- class Person(HasStrictTraits): # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- name = Str() age = Int() phone = Regex(value='000-0000', regex=r'\d\d\d[-]\d\d\d\d') # ------------------------------------------------------------------------- # Traits view definition: # ------------------------------------------------------------------------- traits_view = View( 'name', 'age', 'phone', width=0.18, buttons=['OK', 'Cancel'] ) # ------------------------------------------------------------------------- # Sample data: # ------------------------------------------------------------------------- people = [ Person(name='Dave', age=39, phone='555-1212'), Person(name='Mike', age=28, phone='555-3526'), Person(name='Joe', age=34, phone='555-6943'), Person(name='Tom', age=22, phone='555-7586'), Person(name='Dick', age=63, phone='555-3895'), Person(name='Harry', age=46, phone='555-3285'), Person(name='Sally', age=43, phone='555-8797'), Person(name='Fields', age=31, phone='555-3547'), ] # ------------------------------------------------------------------------- # Table editor definition: # ------------------------------------------------------------------------- filters = [EvalFilterTemplate, MenuFilterTemplate, RuleFilterTemplate] table_editor = TableEditor( columns=[ ObjectColumn(name='name'), ObjectColumn(name='age'), ObjectColumn(name='phone'), ], editable=True, deletable=True, sortable=True, sort_model=True, filters=filters, search=RuleTableFilter(), row_factory=Person, ) # ------------------------------------------------------------------------- # 'ListTraitTest' class: # ------------------------------------------------------------------------- class ListTraitTest(HasStrictTraits): # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- people = List(Person) # ------------------------------------------------------------------------- # Traits view definitions: # ------------------------------------------------------------------------- traits_view = View( VSplit( Item('people', id='table', editor=table_editor), Item( 'people@', id='list', editor=ListEditor(style='custom', rows=5) ), Item( 'people@', id='notebook', editor=ListEditor( use_notebook=True, deletable=True, export='DockShellWindow', dock_style='tab', page_name='.name', ), ), id='splitter', show_labels=False, ), title='List Trait Editor Test', id='traitsui.tests.list_traits_ui_test', dock='horizontal', width=0.4, height=0.6, resizable=True, kind='live', ) # ------------------------------------------------------------------------- # Run the tests: # ------------------------------------------------------------------------- if __name__ == '__main__': ListTraitTest(people=people).configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/integrationtests/ui/set_dynamic_test.py0000644000175100001730000000152200000000000024120 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! from traits.api import HasTraits, List, Str from traitsui.api import Item, SetEditor, View class Team(HasTraits): batting_order = List(Str) roster = List(['Tom', 'Dick', 'Harry', 'Sally'], Str) view = View( Item('batting_order', editor=SetEditor(name='roster', ordered=True)), '_', 'roster@', height=500, resizable=True, ) if __name__ == '__main__': Team().configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/integrationtests/ui/shell_editor_test.py0000644000175100001730000000330400000000000024276 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! from traits.api import Dict, Float, HasPrivateTraits, Int, Str from traitsui.api import Item, ShellEditor, View # ------------------------------------------------------------------------- # 'ShellTest' class: # ------------------------------------------------------------------------- class ShellTest(HasPrivateTraits): # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- name = Str() age = Int() weight = Float() shell_1 = Str() shell_2 = Dict() # ------------------------------------------------------------------------- # Traits view definitions: # ------------------------------------------------------------------------- view = View( 'name', 'age', 'weight', '_', Item('shell_1', editor=ShellEditor()), Item('shell_2', editor=ShellEditor()), id='traitsui.tests.shell_editor_test', resizable=True, width=0.3, height=0.3, ) # ------------------------------------------------------------------------- # Run the test: # ------------------------------------------------------------------------- if __name__ == '__main__': ShellTest().configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/integrationtests/ui/table_editor_focus_bug.py0000644000175100001730000000453400000000000025261 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! from traits.api import HasTraits, Str, List from traitsui.api import Group, Item, TableEditor, View from traitsui.table_column import ObjectColumn class Word(HasTraits): word = Str() class Foo(HasTraits): # arbitrary string containing spaces input = Str() # input split on space parsed = List() def _input_changed(self): words = self.input.split() for word in self.parsed[:]: if word.word in words: words.remove(word.word) else: self.parsed.remove(word) for word in words: self.parsed.append(Word(word=word)) return table_editor = TableEditor( columns=[ObjectColumn(name='word')], editable=True ) help = Str( """Type in the 'input' box before clicking the Parsed tab. The first non-whitespace character will cause changes to the parsed trait and therefore changes to the table rows. That is expected. BUG: the table grabs the focus from 'input' and thus subsequent typing goes into one of the table cells. If you click the 'Parsed' tab, to view the table, and then the 'Inputs' tab the focus will stay with the 'input' box. """ ) traits_view = View( Group(Item('help', style='readonly'), Item('input'), label='Input'), Group(Item('parsed', editor=table_editor), label='Parsed'), dock='tab', resizable=True, width=320, height=240, ) if __name__ == '__main__': # simple test of the model foo = Foo() foo.input = 'these words in the list' assert [word.word for word in foo.parsed] == [ 'these', 'words', 'in', 'the', 'list', ] foo.input = 'these dudes in the bar' assert [word.word for word in foo.parsed] == [ 'these', 'in', 'the', 'dudes', 'bar', ] foo.configure_traits(kind='modal') print(foo.input, [word.word for word in foo.parsed]) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/integrationtests/ui/table_editor_test.py0000644000175100001730000001724600000000000024270 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # ------------------------------------------------------------------------- # Imports: # ------------------------------------------------------------------------- from traits.api import HasStrictTraits, Str, Int, Regex, List, Instance from traitsui.api import View, Group, Item, TableEditor, EnumEditor from traitsui.table_column import ObjectColumn from traitsui.table_filter import ( TableFilter, RuleTableFilter, RuleFilterTemplate, MenuFilterTemplate, EvalFilterTemplate, ) # ------------------------------------------------------------------------- # 'Person' class: # ------------------------------------------------------------------------- class Person(HasStrictTraits): # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- name = Str() age = Int() phone = Regex(value='000-0000', regex=r'\d\d\d[-]\d\d\d\d') state = Str() # ------------------------------------------------------------------------- # Traits view definition: # ------------------------------------------------------------------------- traits_view = View( 'name', 'age', 'phone', 'state', title='Create new person', width=0.18, buttons=['OK', 'Cancel'], ) # ------------------------------------------------------------------------- # Sample data: # ------------------------------------------------------------------------- people = [ Person(name='Dave', age=39, phone='555-1212'), Person(name='Mike', age=28, phone='555-3526'), Person(name='Joe', age=34, phone='555-6943'), Person(name='Tom', age=22, phone='555-7586'), Person(name='Dick', age=63, phone='555-3895'), Person(name='Harry', age=46, phone='555-3285'), Person(name='Sally', age=43, phone='555-8797'), Person(name='Fields', age=31, phone='555-3547'), ] # ------------------------------------------------------------------------- # 'AgeFilter' class: # ------------------------------------------------------------------------- class AgeFilter(TableFilter): # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- name = "Age filter" _name = "Age filter" age = Int(0) # ------------------------------------------------------------------------- # Traits view definitions: # ------------------------------------------------------------------------- # filter_view = Group( 'age{Age >=}' ) # ------------------------------------------------------------------------- # Returns whether an object passes the filter or not: # ------------------------------------------------------------------------- def filter(self, person): """Returns whether an object passes the filter or not.""" return person.age >= self.age # ------------------------------------------------------------------------- # Returns a user readable description of what the filter does: # ------------------------------------------------------------------------- def description(self): """Returns a user readable description of what the filter does.""" return 'Age >= %d' % self.age def _age_changed(self, old, new): self.name = self.description() print('AgeFilter _age_changed', self.name) # ------------------------------------------------------------------------- # 'NameFilter' class: # ------------------------------------------------------------------------- class NameFilter(TableFilter): # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- mname = Str() # ------------------------------------------------------------------------- # Traits view definitions: # ------------------------------------------------------------------------- filter_view = Group('mname{Name contains}') # ------------------------------------------------------------------------- # Returns whether an object passes the filter or not: # ------------------------------------------------------------------------- def filter(self, person): """Returns whether an object passes the filter or not.""" return person.name.lower().find(self.mname.lower()) >= 0 # ------------------------------------------------------------------------- # Returns a user readable description of what the filter does: # ------------------------------------------------------------------------- def description(self): """Returns a user readable description of what the filter does.""" return "Name contains '%s'" % self.mname # ------------------------------------------------------------------------- # Table editor definition: # ------------------------------------------------------------------------- filters = [ AgeFilter(age=30), NameFilter(mname='d'), EvalFilterTemplate, MenuFilterTemplate, RuleFilterTemplate, ] def evaluate_value(v): print('evaluate_value', v) return str(v) # ------------------------------------------------------------------------- # 'TableTest' class: # ------------------------------------------------------------------------- class TableTest(HasStrictTraits): # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- # people = Instance( Person ) people = List(Person) # ------------------------------------------------------------------------- # Traits view definitions: # ------------------------------------------------------------------------- _valid_states = List(["AL", "AR", "AZ", "AK"]) _state_editor = EnumEditor( name="_valid_states", evaluate=evaluate_value, object='table_editor_object', ) table_editor = TableEditor( columns=[ ObjectColumn(name='name'), ObjectColumn(name='age'), ObjectColumn(name='phone'), ObjectColumn(name='state', editor=_state_editor), ], editable=True, deletable=True, sortable=True, sort_model=True, show_lines=True, orientation='vertical', show_column_labels=True, edit_view=View( ['name', 'age', 'phone', 'state', '|[]'], resizable=True ), filter=None, filters=filters, row_factory=Person, ) traits_view = View( [Item('people', id='people', editor=table_editor), '|[]<>'], title='Table Editor Test', id='traitsui.tests.table_editor_test', dock='horizontal', width=0.4, height=0.3, resizable=True, kind='live', ) # ------------------------------------------------------------------------- # Run the tests: # ------------------------------------------------------------------------- if __name__ == '__main__': tt = TableTest(people=people) tt.configure_traits() for p in tt.people: p.print_traits() print('--------------') ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/integrationtests/ui/table_editor_test2.py0000644000175100001730000000761300000000000024347 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # ------------------------------------------------------------------------- # Imports: # ------------------------------------------------------------------------- from traits.api import HasStrictTraits, Str, Int, Regex, List from traitsui.api import View # ------------------------------------------------------------------------- # 'Person' class: # ------------------------------------------------------------------------- class Person(HasStrictTraits): # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- name = Str() age = Int() phone = Regex(value='000-0000', regex=r'\d\d\d[-]\d\d\d\d') # ------------------------------------------------------------------------- # Traits view definition: # ------------------------------------------------------------------------- traits_view = View( 'name', 'age', 'phone', title='Create new person', width=0.18, buttons=['OK', 'Cancel'], ) # ------------------------------------------------------------------------- # 'WorkingPerson' class # ------------------------------------------------------------------------- class WorkingPerson(Person): # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- job = Str() # ------------------------------------------------------------------------- # Traits view definition: # ------------------------------------------------------------------------- traits_view = View( 'name', 'age', 'phone', 'job', title='Create new working person.........', width=0.18, buttons=['OK', 'Cancel'], ) # ------------------------------------------------------------------------- # Sample data: # ------------------------------------------------------------------------- people = [ Person(name='Dave', age=39, phone='555-1212'), Person(name='Mike', age=28, phone='555-3526'), WorkingPerson(name='Joe', age=34, phone='555-6943', job='Fireman'), Person(name='Tom', age=22, phone='555-7586'), Person(name='Dick', age=63, phone='555-3895'), Person(name='Harry', age=46, phone='555-3285'), WorkingPerson(name='Sally', age=43, phone='555-8797', job='Soldier'), Person(name='Fields', age=31, phone='555-3547'), ] # ------------------------------------------------------------------------- # 'TableTest' class: # ------------------------------------------------------------------------- class TableTest(HasStrictTraits): # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- people = List(Person) # ------------------------------------------------------------------------- # Traits view definitions: # ------------------------------------------------------------------------- traits_view = View(['people#', '|<>'], resizable=True) # ------------------------------------------------------------------------- # Run the tests: # ------------------------------------------------------------------------- if __name__ == '__main__': tt = TableTest(people=people) tt.configure_traits() for p in tt.people: p.print_traits() print('--------------') ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/integrationtests/ui/table_list_editor_test.py0000644000175100001730000000550300000000000025314 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ TableEditor test case for Traits UI which tests editing of lists instead of editing of objects.""" # ------------------------------------------------------------------------- # Imports: # ------------------------------------------------------------------------- from traits.api import HasStrictTraits, List from traitsui.api import View, Item, TableEditor from traitsui.table_column import ListColumn from traitsui.table_filter import TableFilter # ------------------------------------------------------------------------- # Sample data: # ------------------------------------------------------------------------- people = [ ['Dave', 39, '555-1212'], ['Mike', 28, '555-3526'], ['Joe', 34, '555-6943'], ['Tom', 22, '555-7586'], ['Dick', 63, '555-3895'], ['Harry', 46, '555-3285'], ['Sally', 43, '555-8797'], ['Fields', 31, '555-3547'], ] # ------------------------------------------------------------------------- # Table editor definition: # ------------------------------------------------------------------------- table_editor = TableEditor( columns=[ ListColumn(index=0, label='Name'), ListColumn(index=1, label='Age'), ListColumn(index=2, label='Phone'), ], editable=False, show_column_labels=True, # ) # ------------------------------------------------------------------------- # 'TableTest' class: # ------------------------------------------------------------------------- class TableTest(HasStrictTraits): # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- people = List() # ------------------------------------------------------------------------- # Traits view definitions: # ------------------------------------------------------------------------- traits_view = View( [Item('people', editor=table_editor, resizable=True), '|[]<>'], title='Table Editor Test', width=0.17, height=0.23, buttons=['OK', 'Cancel'], kind='live', ) # ------------------------------------------------------------------------- # Run the tests: # ------------------------------------------------------------------------- if __name__ == '__main__': tt = TableTest(people=people) tt.configure_traits() for p in tt.people: print(p) print('--------------') ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/integrationtests/ui/test_ui.py0000644000175100001730000002206400000000000022242 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # ------------------------------------------------------------------------- # Imports: # ------------------------------------------------------------------------- import wx from traits.api import ( Trait, HasTraits, Str, Int, Range, List, Event, File, Directory, Bool, Enum, ) from traitsui.api import ( View, Handler, Item, CheckListEditor, ButtonEditor, FileEditor, DirectoryEditor, ImageEnumEditor, Color, Font, ) # ------------------------------------------------------------------------- # Constants: # ------------------------------------------------------------------------- origin_values = ['top left', 'top right', 'bottom left', 'bottom right'] # ------------------------------------------------------------------------- # 'Instance' class: # ------------------------------------------------------------------------- class Instance(HasTraits): # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- integer_text = Int(1) enumeration = Enum('one', 'two', 'three', 'four', 'five', 'six', cols=3) float_range = Range(0.0, 10.0, 10.0) int_range = Range(1, 5) boolean = Bool(True) view = View( 'integer_text', 'enumeration', 'float_range', 'int_range', 'boolean' ) # ------------------------------------------------------------------------- # 'TraitsTest' class # ------------------------------------------------------------------------- class TraitsTest(HasTraits): # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- integer_text = Int(1) enumeration = Enum('one', 'two', 'three', 'four', 'five', 'six', cols=3) float_range = Range(0.0, 10.0, 10.0) int_range = Range(1, 6) int_range2 = Range(1, 50) compound = Trait( 1, Range(1, 6), 'one', 'two', 'three', 'four', 'five', 'six' ) boolean = Bool(True) instance = Trait(Instance()) color = Color font = Font check_list = List( editor=CheckListEditor(values=['one', 'two', 'three', 'four'], cols=4) ) list = List( Str, ['East of Eden', 'The Grapes of Wrath', 'Of Mice and Men'] ) button = Event(0, editor=ButtonEditor(label='Click')) file = File() directory = Directory() image_enum = Trait( editor=ImageEnumEditor( values=origin_values, suffix='_origin', cols=4, klass=Instance ), *origin_values, ) # ------------------------------------------------------------------------- # View definitions: # ------------------------------------------------------------------------- view = View( ( '|{Enum}', ( '|<[Enumeration]', 'enumeration[Simple]', '_', 'enumeration[Custom]@', '_', 'enumeration[Text]*', '_', 'enumeration[Readonly]~', ), ( '|<[Check List]', 'check_list[Simple]', '_', 'check_list[Custom]@', '_', 'check_list[Text]*', '_', 'check_list[Readonly]~', ), ), ( '|{Range}', ( '|<[Float Range]', 'float_range[Simple]', '_', 'float_range[Custom]@', '_', 'float_range[Text]*', '_', 'float_range[Readonly]~', ), ( '|<[Int Range]', 'int_range[Simple]', '_', 'int_range[Custom]@', '_', 'int_range[Text]*', '_', 'int_range[Readonly]~', ), ( '|<[Int Range 2]', 'int_range2[Simple]', '_', 'int_range2[Custom]@', '_', 'int_range2[Text]*', '_', 'int_range2[Readonly]~', ), ), ( '|{Misc}', ( '|<[Integer Text]', 'integer_text[Simple]', '_', 'integer_text[Custom]@', '_', 'integer_text[Text]*', '_', 'integer_text[Readonly]~', ), ( '|<[Compound]', 'compound[Simple]', '_', 'compound[Custom]@', '_', 'compound[Text]*', '_', 'compound[Readonly]~', ), ( '|<[Boolean]', 'boolean[Simple]', '_', 'boolean[Custom]@', '_', 'boolean[Text]*', '_', 'boolean[Readonly]~', ), ), ( '|{Color/Font}', ( '|<[Color]', 'color[Simple]', '_', 'color[Custom]@', '_', 'color[Text]*', '_', 'color[Readonly]~', ), ( '|<[Font]', 'font[Simple]', '_', 'font[Custom]@', '_', 'font[Text]*', '_', 'font[Readonly]~', ), ), ( '|{List}', ( '|<[List]', 'list[Simple]', '_', 'list[Custom]@', '_', 'list[Text]*', '_', 'list[Readonly]~', ), ), ( '|{Button}', ('|<[Button]', 'button[Simple]', '_', 'button[Custom]@'), # 'button[Text]*', # 'button[Readonly]~' ), ( '|<[Image Enum]', 'image_enum[Simple]', '_', 'image_enum[Custom]@', '_', 'image_enum[Text]*', '_', 'image_enum[Readonly]~', ), ( '|<[Instance]', 'instance[Simple]', '_', 'instance[Custom]@', '_', 'instance[Text]*', '_', 'instance[Readonly]~', ), ), ( '|{File}', ( '|<[File]', 'file[Simple]', '_', 'file[Custom]@', '_', 'file[Text]*', '_', 'file[Readonly]~', ), ( '|<[Directory]', 'directory[Simple]', '_', 'directory[Custom]@', '_', 'directory[Text]*', '_', 'directory[Readonly]~', ), ), buttons=['Apply', 'Revert', 'Undo', 'OK'], ) # ------------------------------------------------------------------------- # 'TraitSheetApp' class: # ------------------------------------------------------------------------- class TraitSheetApp(wx.App): # ------------------------------------------------------------------------- # Initialize the object: # ------------------------------------------------------------------------- def __init__(self, object): self.object = object wx.InitAllImageHandlers() wx.App.__init__(self, 1, 'debug.log') self.MainLoop() # ------------------------------------------------------------------------- # Handle application initialization: # ------------------------------------------------------------------------- def OnInit(self): ui = self.object.edit_traits(kind='modal') ui = self.object.edit_traits(kind='wizard') ui = self.object.edit_traits(kind='nonmodal') ui = self.object.edit_traits(kind='live') self.SetTopWindow(ui.control) return True # ------------------------------------------------------------------------- # Main program: # ------------------------------------------------------------------------- if __name__ == '__main__': TraitSheetApp(TraitsTest()) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/integrationtests/ui/test_ui3.py0000644000175100001730000000511200000000000022320 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # ------------------------------------------------------------------------- # Imports: # ------------------------------------------------------------------------- import wx from traits.api import Trait, HasTraits, Str, Int from traitsui.api import Color, View, Group # ------------------------------------------------------------------------- # Model/View classes: # ------------------------------------------------------------------------- class Employer(HasTraits): company = Str() boss = Str() view = View('company', 'boss') class Person(HasTraits): name = Str('David Morrill') age = Int(39) view = View('name', '', 'age', kind='modal') class ExtraPerson(Person): sex = Trait('Male', 'Female') eye_color = Color extra = Group('sex', 'eye_color') class LocatedPerson(Person): street = Str() city = Str() state = Str() zip = Int(78663) extra = Group('street', 'city', 'state', 'zip') class EmployedPerson(LocatedPerson): employer = Trait(Employer(company='Enthought, Inc.', boss='eric')) extra = Group('employer', '') # ------------------------------------------------------------------------- # 'TraitSheetApp' class: # ------------------------------------------------------------------------- class TraitSheetApp(wx.App): # ------------------------------------------------------------------------- # Initialize the object: # ------------------------------------------------------------------------- def __init__(self): wx.InitAllImageHandlers() wx.App.__init__(self, 1, 'debug.log') self.MainLoop() # ------------------------------------------------------------------------- # Handle application initialization: # ------------------------------------------------------------------------- def OnInit(self): Person().edit_traits() ExtraPerson().edit_traits() LocatedPerson().edit_traits() EmployedPerson().edit_traits() return True # ------------------------------------------------------------------------- # Main program: # ------------------------------------------------------------------------- TraitSheetApp() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/integrationtests/ui/test_ui4.py0000644000175100001730000000573300000000000022332 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # ------------------------------------------------------------------------- # Imports: # ------------------------------------------------------------------------- import wx from traits.api import Trait, HasTraits, Str, Int from traitsui.api import Color, View, Group # ------------------------------------------------------------------------- # Model classes: # ------------------------------------------------------------------------- class Employer(HasTraits): company = Str('Enthought, Inc.') boss = Str('eric') view = View('company', 'boss') class Person(HasTraits): name = Str('David Morrill') age = Int(39) class ExtraPerson(Person): sex = Trait('Male', 'Female') eye_color = Color class LocatedPerson(Person): street = Str() city = Str() state = Str() zip = Int(78663) class EmployedPerson(LocatedPerson): employer = Trait(Employer()) # ------------------------------------------------------------------------- # View classes: # ------------------------------------------------------------------------- class PersonView(HasTraits): view = View('name', '', 'age', kind='modal') class ExtraPersonView(PersonView): extra = Group('sex', 'eye_color') class LocatedPersonView(PersonView): extra = Group('street', 'city', 'state', 'zip') class EmployedPersonView(LocatedPersonView): extra = Group('employer', '') # ------------------------------------------------------------------------- # 'TraitSheetApp' class: # ------------------------------------------------------------------------- class TraitSheetApp(wx.App): # ------------------------------------------------------------------------- # Initialize the object: # ------------------------------------------------------------------------- def __init__(self): wx.InitAllImageHandlers() wx.App.__init__(self, 1, 'debug.log') self.MainLoop() # ------------------------------------------------------------------------- # Handle application initialization: # ------------------------------------------------------------------------- def OnInit(self): PersonView().edit_traits(context=Person()) ExtraPersonView().edit_traits(context=ExtraPerson()) LocatedPersonView().edit_traits(context=LocatedPerson()) EmployedPersonView().edit_traits(context=EmployedPerson()) return True # ------------------------------------------------------------------------- # Main program: # ------------------------------------------------------------------------- TraitSheetApp() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/integrationtests/ui/test_ui5.py0000644000175100001730000002337500000000000022335 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Traits Test case """ # ------------------------------------------------------------------------- # Imports: # ------------------------------------------------------------------------- import wx from traits.api import ( Trait, HasTraits, Str, Int, Range, List, Event, File, Directory, Bool, ) from traitsui.api import ( View, Handler, Item, CheckListEditor, ButtonEditor, FileEditor, DirectoryEditor, ImageEnumEditor, Color, Font, ) # ------------------------------------------------------------------------- # Constants: # ------------------------------------------------------------------------- origin_values = ['top left', 'top right', 'bottom left', 'bottom right'] # ------------------------------------------------------------------------- # 'Instance' class: # ------------------------------------------------------------------------- class Instance(HasTraits): # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- integer_text = Int(1) enumeration = Trait('one', 'two', 'three', 'four', 'five', 'six', cols=3) float_range = Range(0.0, 10.0, 10.0) int_range = Range(1, 5) boolean = Bool(True) view = View( 'integer_text', 'enumeration', 'float_range', 'int_range', 'boolean' ) # ------------------------------------------------------------------------- # 'TraitsTestHandler' class: # ------------------------------------------------------------------------- class TraitsTestHandler(Handler): def object_enabled_changed(self, info): enabled = info.object.enabled for i in range(1, 63): getattr(info, 'f%d' % i).enabled = enabled # ------------------------------------------------------------------------- # 'TraitsTest' class # ------------------------------------------------------------------------- class TraitsTest(HasTraits): # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- enabled = Bool(True) integer_text = Int(1) enumeration = Trait('one', 'two', 'three', 'four', 'five', 'six', cols=3) float_range = Range(0.0, 10.0, 10.0) int_range = Range(1, 6) int_range2 = Range(1, 50) compound = Trait( 1, Range(1, 6), 'one', 'two', 'three', 'four', 'five', 'six' ) boolean = Bool(True) instance = Trait(Instance()) color = Color('cyan') font = Font() check_list = List( editor=CheckListEditor(values=['one', 'two', 'three', 'four'], cols=4) ) list = List( Str, ['East of Eden', 'The Grapes of Wrath', 'Of Mice and Men'] ) button = Event(0, editor=ButtonEditor(label='Click')) file = File() directory = Directory() image_enum = Trait( editor=ImageEnumEditor( values=origin_values, suffix='_origin', cols=4, klass=Instance ), *origin_values, ) # ------------------------------------------------------------------------- # View definitions: # ------------------------------------------------------------------------- view = View( ( '|{Enum}', ('enabled',), ( '|<[Enumeration]', 'f1:enumeration[Simple]', '_', 'f2:enumeration[Custom]@', '_', 'f3:enumeration[Text]*', '_', 'f4:enumeration[Readonly]~', ), ( '|<[Check List]', 'f5:check_list[Simple]', '_', 'f6:check_list[Custom]@', '_', 'f7:check_list[Text]*', '_', 'f8:check_list[Readonly]~', ), ), ( '|{Range}', ( '|<[Float Range]', 'f9:float_range[Simple]', '_', 'f10:float_range[Custom]@', '_', 'f11:float_range[Text]*', '_', 'f12:float_range[Readonly]~', ), ( '|<[Int Range]', 'f13:int_range[Simple]', '_', 'f14:int_range[Custom]@', '_', 'f15:int_range[Text]*', '_', 'f16:int_range[Readonly]~', ), ( '|<[Int Range 2]', 'f17:int_range2[Simple]', '_', 'f18:int_range2[Custom]@', '_', 'f19:int_range2[Text]*', '_', 'f20:int_range2[Readonly]~', ), ), ( '|{Misc}', ( '|<[Integer Text]', 'f21:integer_text[Simple]', '_', 'f22:integer_text[Custom]@', '_', 'f23:integer_text[Text]*', '_', 'f24:integer_text[Readonly]~', ), ( '|<[Compound]', 'f25:compound[Simple]', '_', 'f26:compound[Custom]@', '_', 'f27:compound[Text]*', '_', 'f28:compound[Readonly]~', ), ( '|<[Boolean]', 'f29:boolean[Simple]', '_', 'f30:boolean[Custom]@', '_', 'f31:boolean[Text]*', '_', 'f32:boolean[Readonly]~', ), ), ( '|{Color/Font}', ( '|<[Color]', 'f33:color[Simple]', '_', 'f34:color[Custom]@', '_', 'f35:color[Text]*', '_', 'f36:color[Readonly]~', ), ( '|<[Font]', 'f37:font[Simple]', '_', 'f38:font[Custom]@', '_', 'f39:font[Text]*', '_', 'f40:font[Readonly]~', ), ), ( '|{List}', ( '|<[List]', 'f41:list[Simple]', '_', 'f42:list[Custom]@', '_', 'f43:list[Text]*', '_', 'f44:list[Readonly]~', ), ), ( '|{Button}', ('|<[Button]', 'f45:button[Simple]', '_', 'f46:button[Custom]@'), # 'button[Text]*', # 'button[Readonly]~' ), ( '|<[Image Enum]', 'f47:image_enum[Simple]', '_', 'f48:image_enum[Custom]@', '_', 'f49:image_enum[Text]*', '_', 'f50:image_enum[Readonly]~', ), ( '|<[Instance]', 'f51:instance[Simple]', '_', 'f52:instance[Custom]@', '_', 'f53:instance[Text]*', '_', 'f54:instance[Readonly]~', ), ), ( '|{File}', ( '|<[File]', 'f55:file[Simple]', '_', 'f56:file[Custom]@', '_', 'f57:file[Text]*', '_', 'f58:file[Readonly]~', ), ( '|<[Directory]', 'f59:directory[Simple]', '_', 'f60:directory[Custom]@', '_', 'f61:directory[Text]*', '_', 'f62:directory[Readonly]~', ), ), buttons=['Apply', 'Revert', 'Undo', 'OK'], handler=TraitsTestHandler(), ) # ------------------------------------------------------------------------- # 'TraitSheetApp' class: # ------------------------------------------------------------------------- class TraitSheetApp(wx.App): # ------------------------------------------------------------------------- # Initialize the object: # ------------------------------------------------------------------------- def __init__(self, object): self.object = object wx.InitAllImageHandlers() wx.App.__init__(self, 1, 'debug.log') self.MainLoop() # ------------------------------------------------------------------------- # Handle application initialization: # ------------------------------------------------------------------------- def OnInit(self): ui = self.object.edit_traits(kind='live') ui = self.object.edit_traits(kind='modal') ui = self.object.edit_traits(kind='nonmodal') ui = self.object.edit_traits(kind='wizard') self.SetTopWindow(ui.control) return True # ------------------------------------------------------------------------- # Main program: # ------------------------------------------------------------------------- TraitSheetApp(TraitsTest()) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/integrationtests/ui/text_editor_invalid.py0000644000175100001730000000226500000000000024627 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # Test for TextEditor 'invalid' trait. # # Look for: # # background color should be correctly to the error indicating color # whenever the name is invalid (all whitespace). In particular: # # background color should be set at initialization. from traits.api import Bool, HasTraits, Property, Str from traitsui.api import Item, View from traitsui.api import TextEditor class Person(HasTraits): name = Str() invalid = Property(Bool, observe='name') def _get_invalid(self): # Name is valid iff it doesn't consist entirely of whitespace. stripped_name = self.name.strip() return stripped_name == '' traits_view = View( Item('name', editor=TextEditor(invalid='invalid')), ) if __name__ == '__main__': Person().configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/integrationtests/ui/tree_editor_test.py0000644000175100001730000001467400000000000024142 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Test case for the traits tree editor.""" # ------------------------------------------------------------------------- # Imports: # ------------------------------------------------------------------------- from traits.api import HasTraits, Str, Regex, List, Instance from traitsui.api import ( TreeEditor, TreeNode, View, Item, VSplit, HGroup, Handler, spring, ) from traitsui.menu import Menu, Action, Separator from traitsui.editors.tree_editor import ( NewAction, CopyAction, CutAction, PasteAction, DeleteAction, RenameAction, ) # ------------------------------------------------------------------------- # 'Employee' class: # ------------------------------------------------------------------------- class Employee(HasTraits): name = Str('') title = Str() phone = Regex(regex=r'\d\d\d-\d\d\d\d') def default_title(self): self.title = 'Senior Engineer' # ------------------------------------------------------------------------- # 'Department' class: # ------------------------------------------------------------------------- class Department(HasTraits): name = Str('') employees = List(Employee) # ------------------------------------------------------------------------- # 'Company' class: # ------------------------------------------------------------------------- class Company(HasTraits): name = Str('') departments = List(Department) employees = List(Employee) # ------------------------------------------------------------------------- # 'Partner' class: # ------------------------------------------------------------------------- class Partner(HasTraits): name = Str('') company = Instance(Company) # ------------------------------------------------------------------------- # Create a hierarchy: # ------------------------------------------------------------------------- jason = Employee(name='Jason', title='Sr. Engineer', phone='536-1057') mike = Employee(name='Mike', title='Sr. Engineer', phone='536-1057') dave = Employee(name='Dave', title='Sr. Engineer', phone='536-1057') martin = Employee(name='Martin', title='Sr. Engineer', phone='536-1057') duncan = Employee(name='Duncan', title='Sr. Engineer') partner = Partner( name='eric', company=Company( name='Enthought, Inc.', departments=[ Department(name='Business', employees=[jason, mike]), Department(name='Scientific', employees=[dave, martin, duncan]), ], employees=[dave, martin, mike, duncan, jason], ), ) # ------------------------------------------------------------------------- # Define the tree trait editor: # ------------------------------------------------------------------------- no_view = View() tree_editor = TreeEditor( nodes=[ TreeNode( node_for=[Company], auto_open=True, children='', label='name', view=View(['name', '|<']), ), TreeNode( node_for=[Company], auto_open=True, children='departments', label='=Departments', view=no_view, add=[Department], ), TreeNode( node_for=[Company], auto_open=True, children='employees', label='=Employees', view=no_view, add=[Employee], ), TreeNode( node_for=[Department], auto_open=True, children='employees', label='name', menu=Menu( NewAction, Separator(), DeleteAction, Separator(), RenameAction, Separator(), CopyAction, CutAction, PasteAction, ), view=View(['name', '|<']), add=[Employee], ), TreeNode( node_for=[Employee], auto_open=True, label='name', menu=Menu( NewAction, Separator(), Action(name='Default title', action='object.default_title'), Action( name='Department', action='handler.employee_department(editor,object)', ), Separator(), CopyAction, CutAction, PasteAction, Separator(), DeleteAction, Separator(), RenameAction, ), view=View( VSplit( HGroup('3', 'name'), HGroup('9', 'title'), HGroup('phone'), spring, id='vsplit', ), id='traitsui.test.tree_editor_test.employee', dock='vertical', ), ), ] ) # ------------------------------------------------------------------------- # 'TreeHandler' class: # ------------------------------------------------------------------------- class TreeHandler(Handler): def employee_department(self, editor, object): dept = editor.get_parent(object) print('%s works in the %s department.' % (object.name, dept.name)) # ------------------------------------------------------------------------- # Define the View to use: # ------------------------------------------------------------------------- view = View( [ Item(name='company', id='company', editor=tree_editor, resizable=True), '|<>', ], title='Company Structure', id='traitsui.tests.tree_editor_test', dock='horizontal', drop_class=HasTraits, handler=TreeHandler(), buttons=['Undo', 'OK', 'Cancel'], resizable=True, width=0.3, height=0.3, ) # ------------------------------------------------------------------------- # Edit it: # ------------------------------------------------------------------------- if __name__ == '__main__': partner.configure_traits(view=view) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/pyproject.toml0000644000175100001730000000375300000000000017111 0ustar00runnerdocker00000000000000[project] name = 'traitsui' description = 'Traits-capable user interfaces' readme = 'README.rst' requires-python = '>=3.7' authors = [{name='Enthought', email='info@enthought.com'}] keywords = ['gui', 'traits', 'traitsui', 'pyqt', 'pyside', 'wxpython'] classifiers = [ 'Development Status :: 5 - Production/Stable', 'Intended Audience :: Developers', 'License :: OSI Approved :: BSD License', 'Operating System :: OS Independent', 'Programming Language :: Python :: 3', ] dependencies = [ 'traits>=6.2', 'pyface>=8.0', 'importlib-metadata>=3.6; python_version<"3.8"', ] license = {file = "LICENSE.txt"} version = "8.0.0" [project.entry-points.'traitsui.toolkits'] qt = 'traitsui.qt:toolkit' qt4 = 'traitsui.qt:toolkit' wx = 'traitsui.wx:toolkit' null = 'traitsui.null:toolkit' [project.entry-points.'etsdemo_data'] demo = 'traitsui.extras._demo_info:info' [project.optional-dependencies] docs = ['enthought-sphinx-theme', 'sphinx', 'sphinx-copybutton', 'configobj'] editors = ['numpy', 'pandas'] examples = ['apptools', 'h5py', 'numpy', 'pandas', 'pillow', 'tables'] pyqt5 = ['pyqt5', 'pygments'] pyqt6 = ['pyqt6', 'pygments'] pyside2 = ['pyside2', 'pygments'] pyside6 = ['pyside6', 'pygments'] test = ['packaging', 'numpy'] wx = ['wxPython>=4', 'numpy'] [project.urls] source = 'https://github.com/enthought/traitsui' docs = 'https://docs.enthought.com/traitsui' [build-system] requires = ['setuptools>=61', 'wheel'] build-backend = 'setuptools.build_meta' [tool.black] line-length = 79 [tool.isort] profile = 'black' sections = ['FUTURE', 'STDLIB', 'THIRDPARTY', 'ENTHOUGHT', 'FIRSTPARTY', 'LOCALFOLDER'] known_enthought = ['apptools', 'pyface', 'traits'] line_length = 79 order_by_type = false [tool.setuptools] packages = ['traitsui'] [tool.setuptools.package-data] traitsui = [ 'examples/demo/*', 'examples/demo/*/*', 'examples/demo/*/*/*', 'extras/images/*', 'image/library/*.zip', 'images/*', 'wx/images/*', 'qt/images/*', 'testing/data/*', ] ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1684768028.1278067 traitsui-8.0.0/setup.cfg0000644000175100001730000000004600000000000016006 0ustar00runnerdocker00000000000000[egg_info] tag_build = tag_date = 0 ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1684768028.063807 traitsui-8.0.0/traitsui/0000755000175100001730000000000000000000000016031 5ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/__init__.py0000644000175100001730000000503000000000000020140 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! def __getattr__(name): """Handle deprecated attributes.""" if name == "__version__": try: from importlib.metadata import version except ImportError: from importlib_metadata import version from warnings import warn warn( f"traitsui.{name} is deprecated, " f"use impportlib.metadata.version('traitsui') ", DeprecationWarning, ) return version('traitsui') raise AttributeError(f"module {__name__!r} has no attribute {name!r}") # ============================= Test Loader ================================== def load_tests(loader, standard_tests, pattern): """Custom test loading function that enables test filtering using regex exclusion pattern. Parameters ---------- loader : unittest.TestLoader The instance of test loader standard_tests : unittest.TestSuite Tests that would be loaded by default from this module (no tests) pattern : str An inclusion pattern used to match test files (test*.py default) Returns ------- filtered_package_tests : unittest.TestSuite TestSuite representing all package tests that did not match specified exclusion pattern. """ from os.path import dirname from unittest import TestSuite from traits.etsconfig.api import ETSConfig from traitsui.tests._tools import filter_tests # Make sure the right toolkit is up and running before importing tests from traitsui.toolkit import toolkit toolkit() if ETSConfig.toolkit.startswith("qt"): exclusion_pattern = "wx" elif ETSConfig.toolkit == "wx": exclusion_pattern = "qt" else: exclusion_pattern = "(wx|qt)" this_dir = dirname(__file__) package_tests = loader.discover(start_dir=this_dir, pattern=pattern) if exclusion_pattern is None: return package_tests filtered_package_tests = TestSuite() for test_suite in package_tests: filtered_test_suite = filter_tests(test_suite, exclusion_pattern) filtered_package_tests.addTest(filtered_test_suite) return filtered_package_tests ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/api.py0000644000175100001730000002042400000000000017156 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ API for the traitsui package. Editor Factories ---------------- - :class:`~.BasicEditorFactory` - :class:`~.EditorFactory` Context Values -------------- - :attr:`~.CV` - :attr:`~.CVFloat` - :attr:`~.CVInt` - :attr:`~.CVStr` - :func:`~.CVType` - :class:`~.ContextValue` Editors ------- - :class:`~.Editor` - :attr:`~.ArrayEditor` - :attr:`~.BooleanEditor` - :attr:`~.ButtonEditor` - :attr:`~.CheckListEditor` - :attr:`~.CodeEditor` - :func:`~.ColorEditor` - :attr:`~.CompoundEditor` - :class:`~.CSVListEditor` - :attr:`~.CustomEditor` - :class:`~.DateEditor` - :class:`~.DatetimeEditor` - :attr:`~.DateRangeEditor` - :class:`~.DefaultOverride` - :attr:`~.DirectoryEditor` - :attr:`~.DNDEditor` - :attr:`~.DropEditor` - :attr:`~.EnumEditor` - :attr:`~.FileEditor` - :func:`~.FontEditor` - :attr:`~.HistoryEditor` - :attr:`~.HTMLEditor` - :attr:`~.KeyBindingEditor` - :class:`~.ImageEditor` - :attr:`~.ImageEnumEditor` - :attr:`~.InstanceEditor` - :attr:`~.ListEditor` - :class:`~.ListStrEditor` - :attr:`~.NullEditor` - :class:`~.PopupEditor` - :attr:`~.ProgressEditor` - :attr:`~.RangeEditor` - :func:`~.RGBColorEditor` - :class:`~.ScrubberEditor` - :class:`~.SearchEditor` - :attr:`~.SetEditor` - :attr:`~.ShellEditor` - :attr:`~.StyledDateEditor` - :attr:`~.TableEditor` - :class:`~.TabularEditor` - :attr:`~.TextEditor` - :class:`~.TimeEditor` - :attr:`~.TitleEditor` - :attr:`~.TreeEditor` - :attr:`~.TupleEditor` - :attr:`~.ValueEditor` Layout Support -------------- - :class:`~.Group` - :class:`~.HFlow` - :class:`~.HGroup` - :class:`~.HSplit` - :class:`~.Tabbed` - :class:`~.VFlow` - :class:`~.VFold` - :class:`~.VGrid` - :class:`~.VGroup` - :class:`~.VSplit` Handlers -------- - :class:`~.Controller` - :class:`~.Handler` - :class:`~.ModelView` - :class:`~.ViewHandler` - :func:`~.default_handler` UI Items -------- - :class:`~.Custom` - :class:`~.Heading` - :class:`~.Item` - :class:`~.Label` - :class:`~.Readonly` - :class:`~.Spring` - :class:`~.UCustom` - :class:`~.UItem` - :class:`~.UReadonly` - :attr:`~.spring` Menus and Actions ----------------- - :class:`~.Action` - :attr:`~.ActionGroup` - :attr:`~.ApplyButton` - :attr:`~.CancelButton` - :attr:`~.CloseAction` - :attr:`~.HelpAction` - :attr:`~.HelpButton` - :attr:`~.LiveButtons` - :attr:`~.Menu` - :attr:`~.MenuBar` - :attr:`~.ModalButtons` - :attr:`~.NoButton` - :attr:`~.NoButtons` - :attr:`~.OKButton` - :attr:`~.OKCancelButtons` - :attr:`~.PyFaceAction` - :attr:`~.RedoAction` - :attr:`~.RevertAction` - :attr:`~.RevertButton` - :attr:`~.Separator` - :attr:`~.StandardMenuBar` - :attr:`~.ToolBar` - :attr:`~.UndoAction` - :attr:`~.UndoButton` Table and List UI ----------------- - :class:`~.ListStrAdapter` - :class:`~.TabularAdapter` - :attr:`~.TableEditor` Table column types: - :class:`~.ExpressionColumn` - :class:`~.ListColumn` - :class:`~.NumericColumn` - :class:`~.ObjectColumn` - :class:`~.TableColumn` Table filter types: - :class:`~.EvalTableFilter` - :class:`~.MenuTableFilter` - :class:`~.RuleTableFilter` - :class:`~.TableFilter` Instance UI ----------- - :class:`~.InstanceChoice` - :class:`~.InstanceChoiceItem` - :class:`~.InstanceDropChoice` - :attr:`~.InstanceEditor` - :class:`~.InstanceFactoryChoice` Toolkit Object -------------- - :func:`~.toolkit` - :func:`~.toolkit_object` Custom Traits ------------- - :attr:`~.Color` - :func:`~.ColorTrait` - :attr:`~.Font` - :func:`~.FontTrait` - :attr:`~.RGBColor` - :func:`~.RGBColorTrait` Custom UI Traits ---------------- - :attr:`~.Border` - :attr:`~.HasBorder` - :attr:`~.HasMargin` - :attr:`~.Image` - :attr:`~.Margin` - :class:`~.StatusItem` Tree UI ------- - :class:`~.ITreeNode` - :class:`~.ITreeNodeAdapter` - :class:`~.MultiTreeNode` - :class:`~.ObjectTreeNode` - :attr:`~.TreeEditor` - :class:`~.TreeNode` - :class:`~.TreeNodeObject` UI and UI Support ----------------- - :class:`~.UI` - :class:`~.UIInfo` Undo Support ------------ - :class:`~.AbstractUndoItem` - :class:`~.ListUndoItem` - :class:`~.UndoHistory` - :class:`~.UndoHistoryUndoItem` - :class:`~.UndoItem` View and View Elements ---------------------- - :class:`~.View` - :class:`~.ViewElement` - :class:`~.ViewSubElement` - :mod:`~.view_elements` Miscellaneous ------------- - :func:`~.on_help_call` - :func:`~.help_template` - :class:`~.Include` - :func:`~.auto_close_message` - :func:`~.error` - :func:`~.message` - :attr:`~._constants` - :attr:`~.WindowColor` - :func:`~.raise_to_debug` """ from .basic_editor_factory import BasicEditorFactory from .context_value import CV, CVFloat, CVInt, CVStr, CVType, ContextValue from .editor import Editor from .editor_factory import EditorFactory try: from .editors.api import ArrayEditor except ImportError: # ArrayEditor depends on numpy, so ignore if numpy is not present. pass from .editors.api import ( BooleanEditor, ButtonEditor, CheckListEditor, CodeEditor, ColorEditor, CompoundEditor, CustomEditor, CSVListEditor, DNDEditor, StyledDateEditor, DateEditor, DatetimeEditor, DateRangeEditor, DefaultOverride, DirectoryEditor, DropEditor, EnumEditor, FileEditor, FontEditor, HTMLEditor, HistoryEditor, ImageEditor, ImageEnumEditor, InstanceEditor, KeyBindingEditor, ListEditor, ListStrEditor, NullEditor, PopupEditor, ProgressEditor, RGBColorEditor, RangeEditor, ScrubberEditor, SearchEditor, SetEditor, ShellEditor, TableEditor, TabularEditor, TextEditor, TimeEditor, TitleEditor, TreeEditor, TupleEditor, ValueEditor, ) from .group import ( Group, HFlow, HGroup, HSplit, Tabbed, VFlow, VFold, VGrid, VGroup, VSplit, ) from .handler import ( Controller, Handler, ModelView, ViewHandler, default_handler, ) from .help import on_help_call from .help_template import help_template from .include import Include from .item import ( Custom, Heading, Item, Label, Readonly, Spring, UCustom, UItem, UReadonly, spring, ) from .list_str_adapter import ListStrAdapter from .menu import ( Action, ActionGroup, ApplyButton, CancelButton, CloseAction, HelpAction, HelpButton, LiveButtons, Menu, MenuBar, ModalButtons, NoButton, NoButtons, OKButton, OKCancelButtons, PyFaceAction, RedoAction, RevertAction, RevertButton, Separator, StandardMenuBar, ToolBar, UndoAction, UndoButton, ) from .message import auto_close_message, error, message from .table_column import ( ExpressionColumn, ListColumn, NumericColumn, ObjectColumn, TableColumn, ) from .table_filter import ( EvalTableFilter, MenuTableFilter, RuleTableFilter, TableFilter, ) from .tabular_adapter import TabularAdapter from .instance_choice import ( InstanceChoice, InstanceChoiceItem, InstanceDropChoice, InstanceFactoryChoice, ) from .toolkit import toolkit, toolkit_object from .toolkit_traits import ( Color, ColorTrait, Font, FontTrait, RGBColor, RGBColorTrait, ) from .tree_node import ( ITreeNode, ITreeNodeAdapter, MultiTreeNode, ObjectTreeNode, TreeNode, TreeNodeObject, ) from .ui import UI from .ui_info import UIInfo from .ui_traits import Border, HasBorder, HasMargin, Image, Margin, StatusItem from .undo import ( AbstractUndoItem, ListUndoItem, UndoHistory, UndoHistoryUndoItem, UndoItem, ) from .view import View from .view_element import ViewElement, ViewSubElement from . import view_elements _constants = toolkit().constants() WindowColor = _constants.get("WindowColor", 0xFFFFFF) def raise_to_debug(): """When we would otherwise silently swallow an exception, call this instead to allow people to set the TRAITS_DEBUG environment variable and get the exception. """ import os if os.getenv("TRAITS_DEBUG") is not None: raise ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/base_panel.py0000644000175100001730000001365700000000000020510 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! from pyface.action.api import ActionController from traits.api import Any, Instance from traitsui.menu import Action # Set of all predefined system button names: SystemButtons = {"Undo", "Redo", "Apply", "Revert", "OK", "Cancel", "Help"} class BasePanel(ActionController): """Base class for Traits UI panels and dialog boxes. Concrete subclasses of BasePanel are the Python-side owners of the top-level toolkit control for a UI. They also implement the Pyface ActionController API for menu and toolbar action handling. """ #: The top-level toolkit control of the UI. control = Any() #: The UI instance for the view. ui = Instance("traitsui.ui.UI") def default_icon(self): """Return a default icon for a TraitsUI dialog.""" from pyface.image_resource import ImageResource return ImageResource("frame.png") def check_button(self, buttons, action): """Adds *action* to the system buttons list for this dialog, if it is not already in the list. """ name = action.name for button in buttons: if self.is_button(button, name): return buttons.append(action) def is_button(self, action, name): """Returns whether a specified action button is a system button.""" if isinstance(action, str): return action == name return action.name == name def coerce_button(self, action): """Coerces a string to an Action if necessary.""" if isinstance(action, str): return Action( name=action, action="" if action in SystemButtons else "?" ) return action # Button handlers -------------------------------------------------------- def _on_undo(self, event=None): """Handles an "Undo" change request.""" self.ui.history.undo() def _on_redo(self, event=None): """Handles a "Redo" change request.""" self.ui.history.redo() def _on_revert(self, event=None): """Handles a request to revert all changes.""" ui = self.ui if ui.history is not None: ui.history.revert() ui.handler.revert(ui.info) def _on_help(self, event=None): """Handles the user clicking the Help button.""" self.ui.handler.show_help(self.ui.info) # ------------------------------------------------------------------------ # ActionController interface # ------------------------------------------------------------------------ def perform(self, action, event): """Dispatches the action to be handled by the handler. Parameters ---------- action : Action instance The action to perform. event : ActionEvent instance The event that triggered the action. Returns ------- result : any The result of the action's perform method (usually None). """ handler = self.ui.handler self.ui.do_undoable(handler.perform, self.ui.info, action, event) def add_to_menu(self, menu_item): """Adds a menu item to the menu bar being constructed. The bulk of the back-end work is done in Pyface. This code is simply responsible for hooking up radio groups, checkboxes, and enabled status. This routine is also used to add items to the toolbar, as logic and APIs are identical. Parameters ---------- menu_item : toolkit MenuItem The Pyface toolkit-level item to add to the menu. """ item = menu_item.item action = item.action if action.id != "": self.ui.info.bind(action.id, menu_item) if action.enabled_when != "": self.ui.add_enabled(action.enabled_when, menu_item) if action.visible_when != "": self.ui.add_visible(action.visible_when, menu_item) if action.checked_when != "": self.ui.add_checked(action.checked_when, menu_item) def add_to_toolbar(self, toolbar_item): """Adds a menu item to the menu bar being constructed. The bulk of the back-end work is done in Pyface. This code is simply responsible for hooking up radio groups, checkboxes, and enabled status. This simply calls the analagous menu as logic and APIs are identical. Parameters ---------- toolbar_item : toolkit Tool The Pyface toolkit-level item to add to the toolbar. """ self.add_to_menu(toolbar_item) def can_add_to_menu(self, action): """Should the toolbar action be defined in the user interface. This simply calls the analagous menu as logic and APIs are identical. Parameters ---------- action : Action The Action to add to the toolbar. Returns ------- defined : bool Whether or not the action should be added to the menu. """ if action.defined_when == "": return True return self.ui.eval_when(action.defined_when) def can_add_to_toolbar(self, action): """Should the toolbar action be defined in the user interface. This simply calls the analagous menu as logic and APIs are identical. Parameters ---------- action : Action The Action to add to the toolbar. Returns ------- defined : bool Whether or not the action should be added to the toolbar. """ return self.can_add_to_menu(action) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/basic_editor_factory.py0000644000175100001730000000417300000000000022566 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Defines the BasicEditorFactory class, which allows creating editor factories that use the same class for creating all editor styles. """ from traits.api import Any from .editor_factory import EditorFactory class BasicEditorFactory(EditorFactory): """Base class for editor factories that use the same class for creating all editor styles. """ # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- # Editor class to be instantiated klass = Any() # ------------------------------------------------------------------------- # Property getters. # ------------------------------------------------------------------------- def _get_simple_editor_class(self): """Returns the editor class to use for "simple" style views. Overridden to return the value of the 'klass' trait. """ return self.klass def _get_custom_editor_class(self): """Returns the editor class to use for "custom" style views. Overridden to return the value of the 'klass' trait. """ return self.klass def _get_text_editor_class(self): """Returns the editor class to use for "text" style views. Overridden to return the value of the 'klass' trait. """ return self.klass def _get_readonly_editor_class(self): """Returns the editor class to use for "readonly" style views. Overridden to return the value of the 'klass' trait. """ return self.klass def __call__(self, *args, **traits): new = self.clone_traits() new.trait_set(**traits) return new ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/color_column.py0000644000175100001730000000370700000000000021105 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Table column object for RGBColor traits. """ from traitsui.table_column import ObjectColumn class ColorColumn(ObjectColumn): """Table column object for RGBColor traits.""" #: For display by default. style = "readonly" # -- ObjectColumn Overrides ----------------------------------------------- def get_cell_color(self, object): """Returns the cell background color for the column for a specified object. """ color_values = getattr(object, self.name + "_", None) if color_values is None: tk_color = super().get_cell_color(object) elif isinstance(color_values, tuple): tk_color = self._as_int_rgb_tuple(color_values) else: tk_color = color_values return tk_color def get_value(self, object): """Gets the value of the column for a specified object.""" value = getattr(self.get_object(object), self.name, "") if isinstance(value, tuple): value = self._float_rgb_tuple_to_str(value) elif not isinstance(value, str): value = "" return value # -- Private Methods ------------------------------------------------------ def _as_int_rgb_tuple(self, color_values): """Returns object color as RGB integers.""" return tuple(int(255 * v + 0.5) for v in color_values) def _float_rgb_tuple_to_str(self, color_values): """Returns object color as RGB floats.""" csv = ", ".join("{:5.3f}".format(x) for x in color_values) return "({})".format(csv) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/context_value.py0000644000175100001730000000724500000000000021273 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Defines some helper classes and traits used to define 'bindable' editor values. These classes and associated traits are designed to simplify writing editors by allowing the editor factory to specify a context value instance for attributes and have the value on the editor be synchronized with the corresponsing value rom the context. The factory should look something like this:: class MyEditorFactory(EditorFactory): #: The minimum value. minimum = CVInt #: The suffix for the data. suffix = CVType(Str) The editor class needs to have traits which correspond to the context value traits, and should be able to react to changes to them:: class MyEditor(Editor): #: The minimum value. minimum = Int() #: The suffix for the data. suffix = Str() This can then be used in views, with values either as constants or as instances of :class:`ContextValue` (abbreviated as ``CV``):: class MyObject(HasTraits): #: An important value. my_value = Str() #: The minimum value. my_minimum = Int(10) traits_view = View( Item( 'my_value', editor=MyEditorFactory( minimum=CV('my_minimum'), suffix='...', ), ) ) """ from traits.api import HasStrictTraits, Instance, Str, Int, Float, Union class ContextValue(HasStrictTraits): """Defines the name of a context value that can be bound to an editor Resolution of the name follows the same rules as for context values in Item objects: if there is no dot in it then it is treated as an attribute of the 'object' context value, other wise the first part specifies the object in the context and the rest are dotted attribute look-ups. """ #: The extended trait name of the value that can be bound to the editor #: (e.g. 'selection' or 'handler.selection'): name = Str() # ------------------------------------------------------------------------ # object Interface # ------------------------------------------------------------------------ def __init__(self, name): super().__init__(name=name) #: Define a shorthand name for a ContextValue: CV = ContextValue # Trait definitions useful in defining bindable editor traits --------------- def CVType(type, **metadata): """Factory that creates a union of a trait type and a ContextValue trait. This also sets up one-way synchronization to the editor if no other synchronization is specified. Parameters ---------- type : trait type The trait type that is expected for constant values. **metadata Additional metadata for the trait. Returns ------- cv_type_trait : trait A trait which can either hold a constant of the specified type or an instance of the ContextValue class. """ metadata.setdefault("sync_value", "to") return Union(type, InstanceOfContextValue, **metadata) #: Shorthand for an Instance of ContextValue trait. InstanceOfContextValue = Instance(ContextValue, allow_none=False) #: Int or Context value trait CVInt = CVType(Int) #: Float or Context value trait CVFloat = CVType(Float) #: Str or Context value trait CVStr = CVType(Str) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/default_dock_window_theme.py0000644000175100001730000000243300000000000023602 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Defines the default DockWindow theme. """ from .dock_window_theme import DockWindowTheme from .theme import Theme # The original DockWindows UI redone as a theme: default_dock_window_theme = DockWindowTheme( use_theme_color=False, tab_active=Theme("@std:tab_active", label=(0, -3), content=(7, 6, 0, 0)), tab_inactive=Theme("@std:tab_inactive", label=(0, -1), content=(5, 0)), tab_hover=Theme("@std:tab_hover", label=(0, -2), content=(5, 0)), tab_background=Theme("@std:tab_background"), tab=Theme("@std:tab", content=0, label=(-7, 0)), vertical_splitter=Theme( "@std:vertical_splitter", content=0, label=(0, -25) ), horizontal_splitter=Theme( "@std:horizontal_splitter", content=0, label=(-24, 0) ), vertical_drag=Theme("@std:vertical_drag", content=(0, 10)), horizontal_drag=Theme("@std:horizontal_drag", content=(10, 0)), ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/delegating_handler.py0000644000175100001730000001310100000000000022177 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ A handler that delegates the handling of events to a set of sub-handlers. This is typically used as the handler for dynamic views. See the **traits.has_dynamic_view** module. """ # Enthought library imports from traits.api import HasTraits, List from .ui import Dispatcher # Local imports. from .handler import Handler # Set up a logger: import logging logger = logging.getLogger(__name__) class DelegatingHandler(Handler): """A handler that delegates the handling of events to a set of sub-handlers. """ # -- Public 'DelegatingHandler' Interface --------------------------------- #: The list of sub-handlers this object delegates to: sub_handlers = List(HasTraits) # -- Protected 'DelegatingHandler' Interface ------------------------------ #: A list of dispatchable handler methods: _dispatchers = List() # ------------------------------------------------------------------------- # 'Handler' interface: # ------------------------------------------------------------------------- # -- Public Methods ------------------------------------------------------- def closed(self, info, is_ok): """Handles the user interface being closed by the user. This method is overridden here to unregister any dispatchers that were set up in the *init()* method. """ for d in self._dispatchers: d.remove() def init(self, info): """Initializes the controls of a user interface. This method is called after all user interface elements have been created, but before the user interface is displayed. Use this method to further customize the user interface before it is displayed. This method is overridden here to delegate to sub-handlers. Parameters ---------- info : *UIInfo* object The UIInfo object associated with the view Returns ------- initialized : bool A boolean, indicating whether the user interface was successfully initialized. A True value indicates that the UI can be displayed; a False value indicates that the display operation should be cancelled. """ # Iterate through our sub-handlers, and for each method whose name is # of the form 'object_name_changed', where 'object' is the name of an # object in the UI's context, create a trait notification handler that # will call the method whenever object's 'name' trait changes. logger.debug("Initializing delegation in DelegatingHandler [%s]", self) context = info.ui.context for h in self.sub_handlers: # fixme: I don't know why this wasn't here before... I'm not # sure this is right! h.init(info) for name in self._each_trait_method(h): if name[-8:] == "_changed": prefix = name[:-8] col = prefix.find("_", 1) if col >= 0: object = context.get(prefix[:col]) if object is not None: logger.debug( "\tto method [%s] on handler[%s]", name, h ) method = getattr(h, name) trait_name = prefix[col + 1 :] self._dispatchers.append( Dispatcher(method, info, object, trait_name) ) # Also invoke the method immediately so initial # user interface state can be correctly set. if object.base_trait(trait_name).type != "event": method(info) # fixme: These are explicit workarounds for problems with:- # # 'GeometryHierarchyViewHandler' # # which is used in the :- # # 'GeometryHierarchyTreeEditor' # # which are in the 'encode.cad.ui.geometry' package. # # The tree editor has dynamic views, and hence the handler gets # wrapped by a 'DelegatingHandler'. Unfortunately the handler # has a couple of methods that aren't picked up by the usual # wrapping strategy:- # # 1) 'tree_item_selected' # # - which is obviously called when a tree item is selected. # # 2) 'inspect_object' # # - which is called directly as as action from the context menu # defined in the tree editor. # elif name in ["tree_item_selected", "inspect_object"]: self.__dict__[name] = self._create_delegate(h, name) return True def _create_delegate(self, h, name): """Quick fix for handler methods that are currently left out!""" def delegate(*args, **kw): method = getattr(h, name) return method(*args, **kw) return delegate ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/dock_window_theme.py0000644000175100001730000000611400000000000022076 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Defines the theme style information for a DockWindow and its components. """ from pyface.ui_traits import Image from traits.api import HasPrivateTraits, Bool, Property, cached_property from .ui_traits import ATheme class DockWindowTheme(HasPrivateTraits): """Defines the theme style information for a DockWindow and its components.""" # -- Public Trait Definitions --------------------------------------------- #: Use the theme background color as the DockWindow background color? use_theme_color = Bool(True) #: Draw notebook tabs at the top (True) or the bottom (False)? tabs_at_top = Bool(True) #: Active tab theme: tab_active = ATheme #: Inactive tab theme: tab_inactive = ATheme #: Optional image to use for right edge of rightmost inactive tab: tab_inactive_edge = Image #: Tab hover theme (used for inactive tabs): tab_hover = ATheme #: Optional image to use for right edge of rightmost hover tab: tab_hover_edge = Image #: Tab background theme: tab_background = ATheme #: Tab theme: tab = ATheme #: Vertical splitter bar theme: vertical_splitter = ATheme #: Horizontal splitter bar theme: horizontal_splitter = ATheme #: Vertical drag bar theme: vertical_drag = ATheme #: Horizontal drag bar theme: horizontal_drag = ATheme #: The bitmap for the 'tab_inactive_edge' image: tab_inactive_edge_bitmap = Property(observe="tab_inactive_edge") #: The bitmap for the 'tab_hover_edge' image: tab_hover_edge_bitmap = Property(observe="tab_hover_edge") # -- Property Implementations --------------------------------------------- @cached_property def _get_tab_inactive_edge_bitmap(self): image = self.tab_inactive_edge if image is None: return None return image.create_bitmap() @cached_property def _get_tab_hover_edge_bitmap(self): image = self.tab_hover_edge if image is None: return self.tab_inactive_edge_bitmap return image.create_bitmap() # ------------------------------------------------------------------------- # Default theme handling # ------------------------------------------------------------------------- #: The current default DockWindow theme _dock_window_theme = None def dock_window_theme(theme=None): """Get or set the default DockWindow theme.""" global _dock_window_theme if _dock_window_theme is None: from .default_dock_window_theme import default_dock_window_theme _dock_window_theme = default_dock_window_theme old_theme = _dock_window_theme if theme is not None: _dock_window_theme = theme return old_theme ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/dockable_view_element.py0000644000175100001730000001140200000000000022710 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Defines the DockableViewElement class, which allows Traits UIs and Traits UI elements to be docked in external Pyface DockWindow windows. """ from traits.api import HasPrivateTraits, Instance, Bool from .ui import UI from .group import Group from .view import View from .view_element import ViewSubElement from pyface.dock.idockable import IDockable # ------------------------------------------------------------------------- # 'DockableViewElement' class: # ------------------------------------------------------------------------- class DockableViewElement(HasPrivateTraits, IDockable): """Allows Traits UIs and Traits UI elements to be docked in external Pyface DockWindow windows. """ # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- #: The Traits UI that can be docked with an external DockWindow ui = Instance(UI) #: The (optional) element of the Traits UI that can be docked element = Instance(ViewSubElement) #: Should the DockControl be closed on redocking? should_close = Bool(False) # -- IDockable interface -------------------------------------------------- def dockable_should_close(self): """Should the current DockControl be closed before creating the new one? """ element = self.element if element is None: element = self.ui.view.content if not isinstance(element, Group): element = Group().trait_set(content=[element]) group = Group().trait_set(content=[element]) self._view = ( View() .trait_set(**self.ui.view.get()) .trait_set(content=group, title="") ) # FIXME: The following private traits are being set here to facilitate # rebuilding the ui (which will require the context and the handler). # When a current dock control is closed (close_dock_control method), # the contents of self.ui have been disposed of and self.ui is now # None. Now if a new UI needs to be created by calling # dockable_get_control (e.g., when doing an 'undock' action on a dock # window), we need to pass on the context and handler to the UI. # Therefore, we are setting these private traits here so # dockable_get_control can access them. In future, we need to # investigate if there is a better way to do this. self._context = self.ui.context.copy() # Make copy since context will be emptied when calling # self.ui.dispose() self._handler = self.ui.handler return self.should_close or (self.element is None) def dockable_get_control(self, parent): """Gets a control that can be docked into a DockWindow.""" # Create the new UI: ui = self._view.ui( self._context, parent=parent, kind="subpanel", handler=self._handler, ) # Discard the reference to the view created previously: self._view = None # If the old UI was closed, then switch to using the new one: if self.element is None: self.ui = ui else: self._ui = ui return ui.control def dockable_init_dockcontrol(self, dock_control): """Allows the object to override the default DockControl settings.""" dockable = self if self.element is not None: dockable = DockableViewElement( ui=self._ui, element=self.element, should_close=True ) self._ui = None dock_control.trait_set( dockable=dockable, on_close=dockable.close_dock_control ) def close_dock_control(self, dock_control, abort): """Handles the closing of a DockControl containing a Traits UI.""" ui = self.ui # Ask the traits UI handler if it is OK to close the window: if (not abort) and (not ui.handler.close(ui.info, True)): # If not, tell the DockWindow not to close it: return False # Otherwise, clean up and close the traits UI: ui.dispose(abort=abort) # Break our linkage to the UI and ViewElement object: self.ui = self.element = None # And tell the DockWindow to remove the DockControl: return True ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/editor.py0000644000175100001730000006665000000000000017706 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Defines the abstract Editor class, which represents an editing control for an object trait in a Traits-based user interface. """ from contextlib import contextmanager from functools import partial from traits.api import ( Any, Bool, Callable, HasPrivateTraits, HasTraits, Instance, List, Property, ReadOnly, Set, Str, TraitError, TraitListEvent, Tuple, Undefined, cached_property, ) from traits.trait_base import not_none, xgetattr, xsetattr from .editor_factory import EditorFactory from .context_value import ContextValue from .undo import UndoItem from .item import Item # Reference to an EditorFactory object factory_trait = Instance(EditorFactory) class Editor(HasPrivateTraits): """Represents an editing control for an object trait in a Traits-based user interface. """ #: The UI (user interface) this editor is part of: ui = Instance("traitsui.ui.UI", clean_up=True) #: Full name of the object the editor is editing (e.g. #: 'object.link1.link2'): object_name = Str("object") #: The object this editor is editing (e.g. object.link1.link2): object = Instance(HasTraits, clean_up=True) #: The name of the trait this editor is editing (e.g. 'value'): name = ReadOnly() #: The context object the editor is editing (e.g. object): context_object = Property() #: The extended name of the object trait being edited. That is, #: 'object_name.name' minus the context object name at the beginning. For #: example: 'link1.link2.value': extended_name = Property() #: Original value of object.name (e.g. object.link1.link2.value): old_value = Any(clean_up=True) #: Text description of the object trait being edited: description = ReadOnly() #: The Item object used to create this editor: item = Instance(Item, (), clean_up=True) #: The GUI widget defined by this editor: control = Any(clean_up=True) #: The GUI label (if any) defined by this editor: label_control = Any(clean_up=True) #: Is the underlying GUI widget enabled? enabled = Bool(True) #: Is the underlying GUI widget visible? visible = Bool(True) #: Is the underlying GUI widget scrollable? scrollable = Bool(False) #: The EditorFactory used to create this editor: factory = Instance(EditorFactory, clean_up=True) #: Is the editor updating the object.name value? updating = Bool(False) #: Current value for object.name: value = Property() #: Current value of object trait as a string: str_value = Property() #: The trait the editor is editing (not its value, but the trait itself): value_trait = Property() #: The current editor invalid state status: invalid = Bool(False) # -- private trait definitions ------------------------------------------ #: A set to track values being updated to prevent infinite recursion. _no_trait_update = Set(Str) #: A list of all values synchronized to. _user_to = List(Tuple(Any, Str, Callable)) #: A list of all values synchronized from. _user_from = List(Tuple(Str, Callable)) # ------------------------------------------------------------------------ # Editor interface # ------------------------------------------------------------------------ # -- Abstract methods --------------------------------------------------- def init(self, parent): """Create and initialize the underlying toolkit widget. This method must be overriden by subclasses. Implementations must ensure that the :attr:`control` trait is set to an appropriate toolkit object. Parameters ---------- parent : toolkit control The parent toolkit object of the editor's toolkit objects. """ raise NotImplementedError("This method must be overriden.") def update_editor(self): """Updates the editor when the value changes externally to the editor. This should normally be overridden in a subclass. """ pass def error(self, excp): """Handles an error that occurs while setting the object's trait value. Parameters ---------- excp : Exception The exception which occurred. """ from pyface.api import information information( parent=self.get_control_widget(), title=self.description + " value error", message=str(excp), text_format='plain', ) def set_focus(self): """Assigns focus to the editor's underlying toolkit widget. This method must be overriden by subclasses. """ raise NotImplementedError("This method must be overriden.") def set_tooltip_text(self, control, text): """Sets the tooltip for a toolkit control to the provided text. This method must be overriden by subclasses. Parameters ---------- text : str The text to use for the tooltip. control : toolkit control The toolkit control that is having the tooltip set. """ raise NotImplementedError("This method must be overriden.") def string_value(self, value, format_func=None): """Returns the text representation of a specified object trait value. This simply delegates to the factory's `string_value` method. Sub-classes may choose to override the default implementation. Parameters ---------- value : any The value being edited. format_func : callable or None A function that takes a value and returns a string. """ return self.factory.string_value(value, format_func) def restore_prefs(self, prefs): """Restores saved user preference information for the editor. Editors with state may choose to override this. It will only be used if the editor has an `id` value. Parameters ---------- prefs : dict A dictionary of preference values. """ pass def save_prefs(self): """Returns any user preference information for the editor. Editors with state may choose to override this. It will only be used if the editor has an `id` value. Returns ------- prefs : dict or None A dictionary of preference values, or None if no preferences to be saved. """ return None # -- Editor life-cycle methods ------------------------------------------ def prepare(self, parent): """Finish setting up the editor. Parameters ---------- parent : toolkit control The parent toolkit object of the editor's toolkit objects. """ name = self.extended_name if name != "None": self.context_object.on_trait_change( self._update_editor, name, dispatch="ui" ) self.init(parent) self._sync_values() self.update_editor() def dispose(self): """Disposes of the contents of an editor. This disconnects any synchronised values and resets references to other objects. Subclasses may chose to override this method to perform additional clean-up. """ if self.ui is None: return name = self.extended_name if name != "None": self.context_object.on_trait_change( self._update_editor, name, remove=True ) for name, handler in self._user_from: self.on_trait_change(handler, name, remove=True) for object, name, handler in self._user_to: object.on_trait_change(handler, name, remove=True) # Break linkages to references we no longer need: for name in self.trait_names(clean_up=True): setattr(self, name, None) # -- Undo/redo methods -------------------------------------------------- def log_change(self, undo_factory, *undo_args): """Logs a change made in the editor with undo/redo history. Parameters ---------- undo_factory : callable Callable that creates an undo item. Often self.get_undo_item. *undo_args Any arguments to pass to the undo factory. """ ui = self.ui # Create an undo history entry if we are maintaining a history: undoable = ui._undoable if undoable >= 0: history = ui.history if history is not None: item = undo_factory(*undo_args) if item is not None: if undoable == history.now: # Create a new undo transaction: history.add(item) else: # Extend the most recent undo transaction: history.extend(item) def get_undo_item(self, object, name, old_value, new_value): """Creates an undo history entry. Can be overridden in a subclass for special value types. Parameters ---------- object : HasTraits instance The object being modified. name : str The name of the trait that is to be changed. old_value : any The original value of the trait. new_value : any The new value of the trait. """ return UndoItem( object=object, name=name, old_value=old_value, new_value=new_value ) # -- Trait synchronization code ----------------------------------------- def sync_value( self, user_name, editor_name, mode="both", is_list=False, is_event=False, ): """Synchronize an editor trait and a user object trait. Also sets the initial value of the editor trait from the user object trait (for modes 'from' and 'both'), and the initial value of the user object trait from the editor trait (for mode 'to'), as long as the relevant traits are not events. Parameters ---------- user_name : str The name of the trait to be used on the user object. If empty, no synchronization will be set up. editor_name : str The name of the relevant editor trait. mode : str, optional; one of 'to', 'from' or 'both' The direction of synchronization. 'from' means that trait changes in the user object should be propagated to the editor. 'to' means that trait changes in the editor should be propagated to the user object. 'both' means changes should be propagated in both directions. The default is 'both'. is_list : bool, optional If true, synchronization for item events will be set up in addition to the synchronization for the object itself. The default is False. is_event : bool, optional If true, this method won't attempt to initialize the user object or editor trait values. The default is False. """ if user_name == "": return key = "%s:%s" % (user_name, editor_name) parts = user_name.split(".") if len(parts) == 1: user_object = self.context_object xuser_name = user_name else: user_object = self.ui.context[parts[0]] xuser_name = ".".join(parts[1:]) user_name = parts[-1] if mode in {"from", "both"}: self._bind_from(key, user_object, xuser_name, editor_name, is_list) if not is_event: # initialize editor value from user value with self.raise_to_debug(): user_value = xgetattr(user_object, xuser_name) setattr(self, editor_name, user_value) if mode in {"to", "both"}: self._bind_to(key, user_object, xuser_name, editor_name, is_list) if mode == "to" and not is_event: # initialize user value from editor value with self.raise_to_debug(): editor_value = xgetattr(self, editor_name) xsetattr(user_object, xuser_name, editor_value) # -- Utility methods ----------------------------------------------------- def parse_extended_name(self, name): """Extract the object, name and a getter from an extended name Parameters ---------- name : str The extended name to parse. Returns ------- object, name, getter : any, str, callable The object from the context, the (extended) name of the attributes holding the value, and a callable which gets the current value from the context. """ base_name, __, name = name.partition(".") if name: object = self.ui.context[base_name] else: name = base_name object = self.context_object return (object, name, partial(xgetattr, object, name)) def set_tooltip(self, control=None): """Sets the tooltip for a specified toolkit control. This uses the tooltip_text method to get the text to use. Parameters ---------- control : optional toolkit control The toolkit control that is having the tooltip set. If None then the editor's control is used. Returns ------- tooltip_set : bool Whether or not a tooltip value could be set. """ text = self.tooltip_text() if text is None: return False if control is None: control = self.control self.set_tooltip_text(control, text) return True def tooltip_text(self): """Get the text for a tooltip, checking various sources. This checks for text from, in order: - the editor's description trait - the base trait's 'tooltip' metadata - the base trait's 'desc' metadata Returns ------- text : str or None The text for the tooltip, or None if no suitable text can be found. """ if self.description: return self.description base_trait = self.object.base_trait(self.name) text = base_trait.tooltip if text is not None: return text text = base_trait.desc if text is not None: return "Specifies " + text return None def get_control_widget(self): """Get the concrete widget for the control. The default implementation returns the control, however some editors in some backends may store a layout or sizer instead of a proper widget or control, which may not be suitable for certain usages. """ return self.control # -- Utility context managers -------------------------------------------- @contextmanager def no_trait_update(self, name): """Context manager that blocks updates from the named trait.""" if name in self._no_trait_update: yield return self._no_trait_update.add(name) try: yield finally: self._no_trait_update.remove(name) @contextmanager def raise_to_debug(self): """Context manager that uses raise to debug to raise exceptions.""" try: yield except Exception: from traitsui.api import raise_to_debug raise_to_debug() @contextmanager def updating_value(self): """Context manager to handle updating value.""" if self.updating: yield return self.updating = True try: yield finally: self.updating = False # ------------------------------------------------------------------------ # object interface # ------------------------------------------------------------------------ def __init__(self, parent, **traits): """Initializes the editor object.""" super().__init__(**traits) try: self.old_value = getattr(self.object, self.name) except AttributeError: ctrait = self.object.base_trait(self.name) if ctrait.type == "event" or self.name == "spring": # Getting the attribute will fail for 'Event' traits: self.old_value = Undefined else: raise # Synchronize the application invalid state status with the editor's: self.sync_value(self.factory.invalid, "invalid", "from") # ------------------------------------------------------------------------ # private methods # ------------------------------------------------------------------------ def _update_editor(self, object, name, old_value, new_value): """Performs updates when the object trait changes. This is designed to be used as a trait listener. """ # If background threads have modified the trait the editor is bound to, # their trait notifications are queued to the UI thread. It is possible # that by the time the UI thread dispatches these events, the UI the # editor is part of has already been closed. So we need to check if we # are still bound to a live UI, and if not, exit immediately: if self.ui is None: return # If the notification is for an object different than the one actually # being edited, it is due to editing an item of the form: # object.link1.link2.name, where one of the 'link' objects may have # been modified. In this case, we need to rebind the current object # being edited: if object is not self.object: self.object = self.ui.get_extended_value(self.object_name) # If the editor has gone away for some reason, disconnect and exit: if self.control is None: self.context_object.on_trait_change( self._update_editor, self.extended_name, remove=True ) return # Log the change that was made (as long as the Item is not readonly # or it is not for an event): if ( self.item.style != "readonly" and object.base_trait(name).type != "event" ): # Indicate that the contents of the UI have been changed: self.ui.modified = True if self.updating: self.log_change( self.get_undo_item, object, name, old_value, new_value ) # If the change was not caused by the editor itself: if not self.updating: # Update the editor control to reflect the current object state: self.update_editor() def _sync_values(self): """Initialize and synchronize editor and factory traits Initializes and synchronizes (as needed) editor traits with the value of corresponding factory traits. The name of the factory trait and the editor trait must match and the factory trait needs to have ``sync_value`` metadata set. The strategy followed is: - for each factory trait with ``sync_value`` metadata: 1. if the value is a :class:`ContextValue` instance then call :meth:`sync_value` with the ``name`` from the context value. 2. if the trait has ``sync_name`` metadata, look at the referenced trait value and if it is a non-empty string then use this value as the name of the value in the context. 3. otherwise initialize the current value of the factory trait to the corresponding value of the editor. - synchronization mode in cases 1 and 2 is taken from the ``sync_value`` metadata of the editor trait first and then the ``sync_value`` metadata of the factory trait if that is empty. - if the value is a container type, then the `is_list` metadata is set to """ factory = self.factory for name, trait in factory.traits(sync_value=not_none).items(): value = getattr(factory, name) self_trait = self.trait(name) if self_trait.sync_value: mode = self_trait.sync_value else: mode = trait.sync_value if isinstance(value, ContextValue): self.sync_value( value.name, name, mode, bool(self_trait.is_list), self_trait.type == "event", ) elif ( trait.sync_name is not None and getattr(factory, trait.sync_name, "") != "" ): # Note: this is implemented as a stepping stone from things # like ``low_name`` and ``high_name`` to using context values. sync_name = getattr(factory, trait.sync_name) self.sync_value( sync_name, name, mode, bool(self_trait.is_list), self_trait.type == "event", ) elif mode != "to" and value is not Undefined: setattr(self, name, value) def _bind_from(self, key, user_object, xuser_name, editor_name, is_list): """Bind trait change handlers from a user object to the editor. Parameters ---------- key : str The key to use to guard against recursive updates. user_object : object The object in the TraitsUI context that is being bound. xuser_name: : str The extended name of the trait to be used on the user object. editor_name : str The name of the relevant editor trait. is_list : bool, optional If true, synchronization for item events will be set up in addition to the synchronization for the object itself. The default is False. """ def user_trait_modified(new): if key not in self._no_trait_update: with self.no_trait_update(key), self.raise_to_debug(): xsetattr(self, editor_name, new) user_object.on_trait_change(user_trait_modified, xuser_name) self._user_to.append((user_object, xuser_name, user_trait_modified)) if is_list: def user_list_modified(event): if ( isinstance(event, TraitListEvent) and key not in self._no_trait_update ): with self.no_trait_update(key), self.raise_to_debug(): n = event.index getattr(self, editor_name)[ n : n + len(event.removed) ] = event.added items = xuser_name + "_items" user_object.on_trait_change(user_list_modified, items) self._user_to.append((user_object, items, user_list_modified)) def _bind_to(self, key, user_object, xuser_name, editor_name, is_list): """Bind trait change handlers from a user object to the editor. Parameters ---------- key : str The key to use to guard against recursive updates. user_object : object The object in the TraitsUI context that is being bound. xuser_name: : str The extended name of the trait to be used on the user object. editor_name : str The name of the relevant editor trait. is_list : bool, optional If true, synchronization for item events will be set up in addition to the synchronization for the object itself. The default is False. """ def editor_trait_modified(new): if key not in self._no_trait_update: with self.no_trait_update(key), self.raise_to_debug(): xsetattr(user_object, xuser_name, new) self.on_trait_change(editor_trait_modified, editor_name) self._user_from.append((editor_name, editor_trait_modified)) if is_list: def editor_list_modified(event): if key not in self._no_trait_update: with self.no_trait_update(key), self.raise_to_debug(): n = event.index value = xgetattr(user_object, xuser_name) value[n : n + len(event.removed)] = event.added self.on_trait_change(editor_list_modified, editor_name + "_items") self._user_from.append( (editor_name + "_items", editor_list_modified) ) def __set_value(self, value): """Set the value of the trait the editor is editing. This calls the appropriate setattr method on the handler to perform the actual change. """ with self.updating_value(): try: handler = self.ui.handler obj_name = self.object_name name = self.name method = ( getattr(handler, "%s_%s_setattr" % (obj_name, name), None) or getattr(handler, "%s_setattr" % name, None) or getattr(handler, "setattr") ) method(self.ui.info, self.object, name, value) except TraitError as excp: self.error(excp) raise # -- Traits property getters and setters -------------------------------- @cached_property def _get_context_object(self): """Returns the context object the editor is using In some cases a proxy object is edited rather than an object directly in the context, in which case we return ``self.object``. """ object_name = self.object_name context_key = object_name.split(".", 1)[0] if (object_name != "") and (context_key in self.ui.context): return self.ui.context[context_key] # This handles the case of a 'ListItemProxy', which is not in the # ui.context, but is the editor 'object': return self.object @cached_property def _get_extended_name(self): """Returns the extended trait name being edited.""" return ("%s.%s" % (self.object_name, self.name)).split(".", 1)[1] def _get_value_trait(self): """Returns the trait the editor is editing (Property implementation).""" return self.object.trait(self.name) def _get_value(self): """Returns the value of the trait the editor is editing.""" return getattr(self.object, self.name, Undefined) def _set_value(self, value): """Set the value of the trait the editor is editing. Dispatches via the TraitsUI Undo/Redo mechanisms to make change reversible, if desired. """ if self.ui and self.name != "None": self.ui.do_undoable(self.__set_value, value) def _get_str_value(self): """Returns the text representation of the object trait.""" return self.string_value(getattr(self.object, self.name, Undefined)) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/editor_factory.py0000644000175100001730000002462700000000000021433 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Defines the abstract EditorFactory class, which represents a factory for creating the Editor objects used in a Traits-based user interface. """ import logging from traits.api import ( HasPrivateTraits, Callable, Str, Bool, Any, Property, ) from .toolkit import toolkit_object logger = logging.getLogger(__name__) # ------------------------------------------------------------------------- # 'EditorFactory' abstract base class: # ------------------------------------------------------------------------- class EditorFactory(HasPrivateTraits): """Represents a factory for creating the Editor objects in a Traits-based user interface. """ # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- #: Function to use for string formatting format_func = Callable() #: Format string to use for formatting (used if **format_func** not set). format_str = Str() #: Is the editor being used to create table grid cells? is_grid_cell = Bool(False) #: Are created editors initially enabled? enabled = Bool(True) #: The extended trait name of the trait containing editor invalid state #: status: invalid = Str() #: Text aligment to use in most readonly editors #: Possible values: left, right, top, bottom, just, vcenter, hcenter, #: center #: Example: left,vcenter text_alignment = Str() #: The editor class to use for 'simple' style views. simple_editor_class = Property() #: The editor class to use for 'custom' style views. custom_editor_class = Property() #: The editor class to use for 'text' style views. text_editor_class = Property() #: The editor class to use for 'readonly' style views. readonly_editor_class = Property() def __init__(self, *args, **traits): """Initializes the factory object.""" HasPrivateTraits.__init__(self, **traits) self.init(*args) def init(self): """Performs any initialization needed after all constructor traits have been set. """ pass def named_value(self, name, ui): """Returns the value of a specified extended name of the form: name or context_object_name.name[.name...]: """ names = name.split(".") if len(names) == 1: # fixme: This will produce incorrect values if the actual Item the # factory is being used with does not use the default object='name' # value, and the specified 'name' does not contain a '.'. The # solution will probably involve providing the Item as an argument, # but it is currently not available at the time this method needs # to be called... names.insert(0, "object") value = ui.context[names[0]] for name in names[1:]: value = getattr(value, name) return value # ------------------------------------------------------------------------- # Methods that generate backend toolkit-specific editors. # ------------------------------------------------------------------------- def simple_editor(self, ui, object, name, description, parent): """Generates an editor using the "simple" style.""" return self.simple_editor_class( parent, factory=self, ui=ui, object=object, name=name, description=description, ) def custom_editor(self, ui, object, name, description, parent): """Generates an editor using the "custom" style.""" return self.custom_editor_class( parent, factory=self, ui=ui, object=object, name=name, description=description, ) def text_editor(self, ui, object, name, description, parent): """Generates an editor using the "text" style.""" return self.text_editor_class( parent, factory=self, ui=ui, object=object, name=name, description=description, ) def readonly_editor(self, ui, object, name, description, parent): """Generates an "editor" that is read-only.""" return self.readonly_editor_class( parent, factory=self, ui=ui, object=object, name=name, description=description, ) # ------------------------------------------------------------------------- # Private methods # ------------------------------------------------------------------------- @classmethod def _get_toolkit_editor(cls, class_name): """ Returns the editor by name class_name in the backend package. """ editor_factory_modules = [ factory_class.__module__ for factory_class in cls.mro() if issubclass(factory_class, EditorFactory) ] for index, editor_module in enumerate(editor_factory_modules): try: editor_module_name = editor_module.split(".")[-1] object_ref = ":".join([editor_module_name, class_name]) return toolkit_object(object_ref, True) except RuntimeError as e: msg = "Can't import toolkit_object '{}': {}" logger.debug(msg.format(object_ref, e)) if index == len(editor_factory_modules) - 1: raise e return None def string_value(self, value, format_func=None): """Returns the text representation of a specified object trait value. If the **format_func** attribute is set on the editor factory, then this method calls that function to do the formatting. If the **format_str** attribute is set on the editor factory, then this method uses that string for formatting. If neither attribute is set, then this method just calls the appropriate text type to format. """ if self.format_func is not None: return self.format_func(value) if self.format_str != "": return self.format_str % value if format_func is not None: return format_func(value) return str(value) # ------------------------------------------------------------------------- # Property getters # ------------------------------------------------------------------------- def _get_simple_editor_class(self): """Returns the editor class to use for "simple" style views. The default implementation tries to import the SimpleEditor class in the editor file in the backend package, and if such a class is not to found it returns the SimpleEditor class defined in editor_factory module in the backend package. """ try: SimpleEditor = self._get_toolkit_editor("SimpleEditor") except Exception as e: msg = "Can't import SimpleEditor for {}: {}" logger.debug(msg.format(self.__class__, e)) SimpleEditor = toolkit_object("editor_factory:SimpleEditor") return SimpleEditor def _get_custom_editor_class(self): """Returns the editor class to use for "custom" style views. The default implementation tries to import the CustomEditor class in the editor file in the backend package, and if such a class is not to found it returns simple_editor_class. """ try: CustomEditor = self._get_toolkit_editor("CustomEditor") except Exception as e: msg = "Can't import CustomEditor for {}: {}" logger.debug(msg.format(self.__class__, e)) CustomEditor = self.simple_editor_class return CustomEditor def _get_text_editor_class(self): """Returns the editor class to use for "text" style views. The default implementation tries to import the TextEditor class in the editor file in the backend package, and if such a class is not found it returns the TextEditor class declared in the editor_factory module in the backend package. """ try: TextEditor = self._get_toolkit_editor("TextEditor") except Exception as e: msg = "Can't import TextEditor for {}: {}" logger.debug(msg.format(self.__class__, e)) TextEditor = toolkit_object("editor_factory:TextEditor") return TextEditor def _get_readonly_editor_class(self): """Returns the editor class to use for "readonly" style views. The default implementation tries to import the ReadonlyEditor class in the editor file in the backend package, and if such a class is not found it returns the ReadonlyEditor class declared in the editor_factory module in the backend package. """ try: ReadonlyEditor = self._get_toolkit_editor("ReadonlyEditor") except Exception as e: msg = "Can't import ReadonlyEditor for {}: {}" logger.debug(msg.format(self.__class__, e)) ReadonlyEditor = toolkit_object("editor_factory:ReadonlyEditor") return ReadonlyEditor # ------------------------------------------------------------------------- # 'EditorWithListFactory' abstract base class: # ------------------------------------------------------------------------- class EditorWithListFactory(EditorFactory): """Base class for factories of editors for objects that contain lists.""" # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- #: Values to enumerate (can be a list, tuple, dict, or a CTrait or #: TraitHandler that is "mapped"): values = Any() #: Extended name of the trait on **object** containing the enumeration data object = Str("object") #: Name of the trait on 'object' containing the enumeration data name = Str() ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1684768028.067807 traitsui-8.0.0/traitsui/editors/0000755000175100001730000000000000000000000017502 5ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/editors/__init__.py0000644000175100001730000000212400000000000021612 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! def __getattr__(name): # For backwards compatibility, continue to make the editors available for # import here, but warn it is deprecated. import traitsui.editors.api if name in traitsui.editors.api.__dict__: obj = getattr(traitsui.editors.api, name) import warnings warnings.warn( "Using or importing Editor factories from 'traitsui.editors' " "instead of from 'traitsui.editors.api' is deprecated and will " "stop working in TraitsUI 9.0", DeprecationWarning, stacklevel=2, ) globals()[name] = obj return obj raise AttributeError(f'module {__name__!r} has no attribute {name!r}') ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/editors/api.py0000644000175100001730000001023700000000000020630 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ API for traitsui.editors subpackage. Note that the following are also available from :mod:`traitsui.api`, which is the preferred module for imports. - :attr:`~.ArrayEditor` - :attr:`~.BooleanEditor` - :attr:`~.ButtonEditor` - :attr:`~.CheckListEditor` - :attr:`~.CodeEditor` - :func:`~.ColorEditor` - :attr:`~.CompoundEditor` - :class:`~.CSVListEditor` - :attr:`~.CustomEditor` - :class:`~.DateEditor` - :class:`~.DatetimeEditor` - :attr:`~.DateRangeEditor` - :class:`~.DefaultOverride` - :attr:`~.DirectoryEditor` - :attr:`~.DNDEditor` - :attr:`~.DropEditor` - :attr:`~.EnumEditor` - :attr:`~.FileEditor` - :func:`~.FontEditor` - :attr:`~.HistoryEditor` - :attr:`~.HTMLEditor` - :attr:`~.KeyBindingEditor` - :class:`~.ImageEditor` - :attr:`~.ImageEnumEditor` - :attr:`~.InstanceEditor` - :attr:`~.ListEditor` - :class:`~.ListStrEditor` - :attr:`~.NullEditor` - :class:`~.PopupEditor` - :attr:`~.ProgressEditor` - :attr:`~.RangeEditor` - :func:`~.RGBColorEditor` - :class:`~.ScrubberEditor` - :class:`~.SearchEditor` - :attr:`~.SetEditor` - :attr:`~.ShellEditor` - :attr:`~.StyledDateEditor` - :attr:`~.TableEditor` - :class:`~.TabularEditor` - :attr:`~.TextEditor` - :class:`~.TimeEditor` - :attr:`~.TitleEditor` - :attr:`~.TreeEditor` - :attr:`~.TupleEditor` - :attr:`~.ValueEditor` Tree Editor Actions / Traits ---------------------------- - :attr:`~.CopyAction` - :attr:`~.CutAction` - :attr:`~.DeleteAction` - :attr:`~.IconSize` - :attr:`~.NewAction` - :attr:`~.PasteAction` - :attr:`~.RenameAction` """ from ..toolkit import toolkit try: from .array_editor import ArrayEditor except ImportError: # check if failure is due to missing numpy, otherwise re-raise try: import numpy except ImportError: import warnings warnings.warn( "ArrayEditor is not available due to missing numpy", ImportWarning ) else: del numpy raise from .boolean_editor import BooleanEditor from .button_editor import ButtonEditor from .check_list_editor import CheckListEditor from .code_editor import CodeEditor from .color_editor import ColorEditor from .compound_editor import CompoundEditor from .csv_list_editor import CSVListEditor from .custom_editor import CustomEditor from .date_editor import DateEditor from .datetime_editor import DatetimeEditor from .date_range_editor import DateRangeEditor from .styled_date_editor import StyledDateEditor from .default_override import DefaultOverride from .directory_editor import DirectoryEditor from .dnd_editor import DNDEditor from .drop_editor import DropEditor from .enum_editor import EnumEditor from .file_editor import FileEditor from .font_editor import FontEditor from .key_binding_editor import KeyBindingEditor from .image_editor import ImageEditor from .image_enum_editor import ImageEnumEditor from .instance_editor import InstanceEditor from .list_editor import ListEditor from .list_str_editor import ListStrEditor from .null_editor import NullEditor from .range_editor import RangeEditor from .rgb_color_editor import RGBColorEditor from .set_editor import SetEditor from .text_editor import TextEditor from .table_editor import TableEditor from .time_editor import TimeEditor from .title_editor import TitleEditor from .tree_editor import TreeEditor from .tuple_editor import TupleEditor from .history_editor import HistoryEditor from .html_editor import HTMLEditor from .popup_editor import PopupEditor from .value_editor import ValueEditor from .shell_editor import ShellEditor from .scrubber_editor import ScrubberEditor from .tabular_editor import TabularEditor from .progress_editor import ProgressEditor from .search_editor import SearchEditor from traitsui.editors.tree_editor import ( CopyAction, CutAction, DeleteAction, IconSize, NewAction, PasteAction, RenameAction, ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/editors/array_editor.py0000644000175100001730000001656600000000000022556 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Defines the array editor factory for all traits toolkit backends. """ import numpy from traits.api import Bool, HasTraits, Int, Float, Instance, TraitError from traitsui.editor import Editor from traitsui.editor_factory import EditorFactory from traitsui.group import Group from traitsui.item import Item from traitsui.view import View class ArrayEditor(EditorFactory): """Editor factory for array editors.""" # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- #: Width of the individual fields width = Int(-80) #: Is user input set on every keystroke? auto_set = Bool(True) #: Is user input set when the Enter key is pressed? enter_set = Bool(False) class ArrayStructure(HasTraits): # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- #: Editor that this structure is linked to editor = Instance(Editor) #: The constructed View for the array view = Instance(View) def __init__(self, editor): """Initializes the object.""" super().__init__(editor=editor) # Set up the field width for each item: width = editor.factory.width # Set up the correct style for each filed: style = "simple" if editor.readonly: style = "readonly" # Get the array we are mirroring: object = editor.value # Determine the correct trait type to use for each element: trait = Float() if object.dtype.kind == "i": trait = Int() if len(object.shape) == 1: self.view = self._one_dim_view(object, style, width, trait) elif len(object.shape) == 2: self.view = self._two_dim_view(object, style, width, trait) else: raise TraitError("Only 1D or 2D arrays supported") # ------------------------------------------------------------------------- # 1D view: # ------------------------------------------------------------------------- def _one_dim_view(self, object, style, width, trait): content = [] shape = object.shape items = [] format_func = self.editor.factory.format_func format_str = self.editor.factory.format_str for i in range(shape[0]): name = "f%d" % i self.add_trait( name, trait( object[i], event="field", auto_set=self.editor.factory.auto_set, enter_set=self.editor.factory.enter_set, ), ) items.append( Item( name=name, style=style, width=width, format_func=format_func, format_str=format_str, padding=-3, ) ) group = Group(orientation="horizontal", show_labels=False, *items) content.append(group) return View(Group(show_labels=False, *content)) # ------------------------------------------------------------------------- # 2D view: # ------------------------------------------------------------------------- def _two_dim_view(self, object, style, width, trait): content = [] shape = object.shape format_func = self.editor.factory.format_func format_str = self.editor.factory.format_str for i in range(shape[0]): items = [] for j in range(shape[1]): name = "f%d_%d" % (i, j) self.add_trait( name, trait( object[i, j], event="field", auto_set=self.editor.factory.auto_set, enter_set=self.editor.factory.enter_set, ), ) items.append( Item( name=name, style=style, width=width, format_func=format_func, format_str=format_str, padding=-3, ) ) group = Group(orientation="horizontal", show_labels=False, *items) content.append(group) return View(Group(show_labels=False, *content)) def _field_changed(self): """Updates the underlying array when any field changes value.""" if not self.editor._busy: # Get the array we are mirroring: object = self.editor.value shape = object.shape value = numpy.zeros(shape, object.dtype) # 1D if len(shape) == 1: for i in range(shape[0]): value[i] = getattr(self, "f%d" % i) # 2D elif len(shape) == 2: for i in range(shape[0]): for j in range(shape[1]): value[i, j] = getattr(self, "f%d_%d" % (i, j)) self.editor.update_array(value) # ------------------------------------------------------------------------- # Toolkit-independent 'SimpleEditor' class: # ------------------------------------------------------------------------- class SimpleEditor(Editor): """Simple style of editor for arrays.""" # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- #: Is the editor read-only? readonly = Bool(False) def init(self, parent): """Finishes initializing the editor by creating the underlying toolkit widget. """ self._as = _as = ArrayStructure(self) ui = _as.view.ui(_as, parent, kind="subpanel") ui.parent = self.ui self.control = ui.control def update_editor(self): """Updates the editor when the object trait changes externally to the editor. """ if not self._busy: self._busy = True object = self.value shape = object.shape _as = self._as # 1D if len(shape) == 1: for i in range(shape[0]): setattr(_as, "f%d" % i, object[i]) # 2D elif len(shape) == 2: for i in range(shape[0]): for j in range(shape[1]): setattr(_as, "f%d_%d" % (i, j), object[i, j]) self._busy = False def update_array(self, value): """Updates the array value associated with the editor.""" self._busy = True self.value = value self._busy = False # This alias is deprecated and will be removed in TraitsUI 8. ToolkitEditorFactory = ArrayEditor ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/editors/boolean_editor.py0000644000175100001730000000447400000000000023052 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Defines the Boolean editor factory for all traits toolkit backends. """ from traits.api import Dict, Str, Any from traitsui.editors.text_editor import TextEditor from traitsui.view import View # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- # Mapping from user input text to Boolean values mapping_trait = Dict( Str, Any, { "True": True, "true": True, "t": True, "yes": True, "y": True, "False": False, "false": False, "f": False, "no": False, "n": False, }, ) class BooleanEditor(TextEditor): """Editor factory for Boolean editors.""" # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- #: Dictionary mapping user input to other values. #: These definitions override definitions in the 'text_editor' version mapping = mapping_trait # ------------------------------------------------------------------------- # Traits view definition: # ------------------------------------------------------------------------- traits_view = View() # ------------------------------------------------------------------------- # EditorFactory methods # ------------------------------------------------------------------------- def _get_custom_editor_class(self): """Returns the editor class to use for "custom" style views. Overridden to return the simple_editor_class (instead of the CustomEditor class for the text editor's factory, which this class inherits from). """ return self.simple_editor_class # This alias is deprecated and will be removed in TraitsUI 8. ToolkitEditorFactory = BooleanEditor ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/editors/button_editor.py0000644000175100001730000000566300000000000022747 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Defines the button editor factory for all traits toolkit backends. """ from pyface.ui_traits import Image from traits.api import Str, Range, Enum, Property, Union from traitsui.editor_factory import EditorFactory from traitsui.ui_traits import AView from traitsui.view import View class ButtonEditor(EditorFactory): """Editor factory for buttons.""" # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- #: Value to set when the button is clicked value = Property() #: Optional label for the button label = Str() #: The name of the external object trait that the button label is synced to label_value = Str() #: The name of the trait on the object that contains the list of possible #: values. If this is set, then the value, label, and label_value traits #: are ignored; instead, they will be set from this list. When this button #: is clicked, the value set will be the one selected from the drop-down. values_trait = Union(None, Str) #: (Optional) Image to display on the button image = Image #: The name of the external object trait that the button image is synced to image_value = Str() #: Extra padding to add to both the left and the right sides width_padding = Range(0, 31, 7) #: Extra padding to add to both the top and the bottom sides height_padding = Range(0, 31, 5) #: Presentation style style = Enum("button", "radio", "toolbar", "checkbox") #: Orientation of the text relative to the image orientation = Enum("vertical", "horizontal") #: The optional view to display when the button is clicked: view = AView # ------------------------------------------------------------------------- # Traits view definition: # ------------------------------------------------------------------------- traits_view = View(["label", "value", "|[]"]) def _get_value(self): return self._value def _set_value(self, value): self._value = value if isinstance(value, str): try: self._value = int(value) except ValueError: try: self._value = float(value) except ValueError: pass def __init__(self, **traits): self._value = 0 super().__init__(**traits) # This alias is deprecated and will be removed in TraitsUI 8. ToolkitEditorFactory = ButtonEditor ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/editors/check_list_editor.py0000644000175100001730000000321600000000000023534 0ustar00runnerdocker00000000000000# (C) Copyright 2009-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # ------------------------------------------------------------------------------ # Copyright (c) 2008, Riverbank Computing Limited # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # # Author: David C. Morrill # # ------------------------------------------------------------------------------ """ Defines the editor factory for multi-selection enumerations, for all traits toolkit backends. """ from traits.api import Range from traitsui.editor_factory import EditorWithListFactory class CheckListEditor(EditorWithListFactory): """Editor factory for checklists.""" # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- #: Number of columns to use when the editor is displayed as a grid cols = Range(1, 20) # This alias is deprecated and will be removed in TraitsUI 8. ToolkitEditorFactory = CheckListEditor ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/editors/code_editor.py0000644000175100001730000000600500000000000022335 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Defines the code editor factory for all traits toolkit backends, useful for tools such as debuggers. """ from traits.api import Instance, Str, Enum, Bool from traitsui.editor_factory import EditorFactory from traitsui.toolkit_traits import Color class CodeEditor(EditorFactory): """Editor factory for code editors.""" # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- #: Object trait containing list of line numbers to mark (optional) mark_lines = Str() #: Background color for marking lines mark_color = Color(0xECE9D8) #: Object trait containing the currently selected line (optional) selected_line = Str() #: Object trait containing the currently selected text (optional) selected_text = Str() #: Object trait containing the currently selected text start position #: (optional) selected_start_pos = Str() #: Object trait containing the currently selected text end position #: (optional) selected_end_pos = Str() #: Background color for selected lines selected_color = Color(0xA4FFFF) #: Where should the search toolbar be placed? search = Enum("top", "bottom", "none") #: Background color for lines that match the current search search_color = Color(0xFFFF94) #: Current line line = Str() #: Current column column = Str() #: Should code folding be enabled? foldable = Bool(True) #: Should line numbers be displayed in the margin? show_line_numbers = Bool(True) #: Is user input set on every change? auto_set = Bool(True) #: Should the editor auto-scroll when a new **selected_line** value is set? auto_scroll = Bool(True) #: Optional key bindings associated with the editor key_bindings = Instance("traitsui.key_bindings.KeyBindings") #: Calltip clicked event calltip_clicked = Str() #: The lexer to use. Default is 'python'; 'null' indicates no lexing. lexer = Str("python") #: Object trait containing the list of line numbers to dim (optional) dim_lines = Str() #: Object trait to dim lines to. Can be of form #rrggbb or a color spec. If #: not specified, dark grey is used. dim_color = Str() #: Object trait containing the list of line numbers to put squiggles under #: (optional) squiggle_lines = Str() #: Object trait for the color of squiggles. If not specified, red is used. squiggle_color = Str() # This alias is deprecated and will be removed in TraitsUI 8. ToolkitEditorFactory = CodeEditor ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/editors/color_editor.py0000644000175100001730000000460600000000000022546 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Defines the color editor factory for the all traits toolkit backends. """ from traits.api import Bool from traitsui.editor_factory import EditorFactory from traitsui.toolkit import toolkit_object from traitsui.view import View # ------------------------------------------------------------------------- # 'ToolkitEditorFactory' class: # ------------------------------------------------------------------------- class ToolkitEditorFactory(EditorFactory): """Editor factory for color editors.""" # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- #: Is the underlying color trait mapped? mapped = Bool(True) #: Do we use a native dialog for the popup or the toolkit's? #: At present, only affects Qt. use_native_dialog = Bool(True) # ------------------------------------------------------------------------- # Traits view definition: # ------------------------------------------------------------------------- traits_view = View(["mapped{Is value mapped?}", "|[]>"]) # Define the ColorEditor class # The function will try to return the toolkit-specific editor factory (located # in traitsui..color_editor, and if none is found, the # ToolkitEditorFactory declared here is returned. def ColorEditor(*args, **traits): r"""Returns an instance of the toolkit-specific editor factory declared in traitsui..color_editor. If such an editor factory cannot be located, an instance of the abstract ToolkitEditorFactory declared in traitsui.editors.color_editor is returned. Parameters ---------- \*args, \*\*traits arguments and keywords to be passed on to the editor factory's constructor. """ try: return toolkit_object("color_editor:ToolkitEditorFactory", True)( *args, **traits ) except: return ToolkitEditorFactory(*args, **traits) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/editors/compound_editor.py0000644000175100001730000000255300000000000023253 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Defines the compound editor factory for all traits toolkit backends. """ from traits.api import Bool, List from traitsui.editor_factory import EditorFactory # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- # List of component editor factories used to build a compound editor editors_trait = List(EditorFactory) class CompoundEditor(EditorFactory): """Editor factory for compound editors.""" # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- #: Component editor factories used to build the editor editors = editors_trait #: Is user input set on every keystroke? auto_set = Bool(True) # This alias is deprecated and will be removed in TraitsUI 8. ToolkitEditorFactory = CompoundEditor ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/editors/csv_list_editor.py0000644000175100001730000003243100000000000023253 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """This modules defines CSVListEditor. A CSVListEditor provides an editor for lists of simple data types. It allows the user to edit the list in a text field, using commas (or optionally some other character) to separate the elements. """ from traits.api import Str, Int, Float, Enum, Range, Bool, TraitError, Union from traits.trait_handlers import RangeTypes from traitsui.editors.text_editor import TextEditor from traitsui.helper import enum_values_changed def _eval_list_str(s, sep=",", item_eval=None, ignore_trailing_sep=True): """Convert a string into a list. Parameters ---------- s : str The string to be converted. sep : str or None `sep` is the text separator of list items. If `sep` is None, each contiguous stretch of whitespace is a separator. item_eval : callable or None `item_eval` is used to evaluate the list elements. If `item_eval` is None, the list will be a list substrings of `s`. ignore_trailing_sep : bool If `ignore_trailing_sep` is False, it is an error to have a separator at the end of the list (i.e. 'foo, bar,' is invalid). If `ignore_trailing_sep` is True, a separator at the end of the string `s` is ignored. Returns ------- values : list List of converted values from the string. """ if item_eval is None: item_eval = lambda x: x s = s.strip() if sep is not None and ignore_trailing_sep and s.endswith(sep): s = s[: -len(sep)] s = s.rstrip() if s == "": values = [] else: values = [item_eval(x.strip()) for x in s.split(sep)] return values def _format_list_str(values, sep=",", item_format=str): """Convert a list to a string. Each item in the list `values` is converted to a string with the function `item_format`, and these are joined with `sep` plus a space. If `sep` is None, a single space is used to join the items. Parameters ---------- values : list The list of values to be represented as a string. sep : str String used to join the items. A space is also added after `sep`. item_format : callable Converts its single argument to a string. Returns ------- s : str The result of converting the list to a string. """ if sep is None: joiner = " " else: joiner = sep + " " s = joiner.join(item_format(x) for x in values) return s def _validate_range_value(range_object, object, name, value): """Validate a Range value. This function is used by the CSVListEditor to validate a value when editing a list of ranges where the Range is dynamic (that is, one or both of the 'low' and 'high' values are strings that refer to other traits in `object`. The function implements the same validation logic as in the method traits.trait_types.BaseRange._set(), but does not call the set_value() method; instead it simply returns the valid value. If the value is not valid, range_object.error(...) is called. Parameters ---------- range_object : instance of traits.trait_types.Range object : instance of HasTraits This is the HasTraits object that holds the traits to which the one or both of range_object.low and range_object.high refer. name : str The name of the List trait in `object`. value : object (e.g. int, float, str) The value to be validated. Returns ------- value : object The validated value. It might not be the same type as the input argument (e.g. if the range type is float and the input value is an int, the return value will be a float). """ low = eval(range_object._low) high = eval(range_object._high) if low is None and high is None: if isinstance(value, RangeTypes): return value else: new_value = range_object._typed_value(value, low, high) satisfies_low = ( low is None or low < new_value or ((not range_object._exclude_low) and (low == new_value)) ) satisfies_high = ( high is None or high > new_value or ((not range_object._exclude_high) and (high == new_value)) ) if satisfies_low and satisfies_high: return value # Note: this is the only explicit use of 'object' and 'name'. range_object.error(object, name, value) def _prepare_method(cls, parent): """Unbound implementation of the prepare editor method to add a change notification hook in the items of the list before calling the parent prepare method of the parent class. """ name = cls.extended_name if name != "None": cls.context_object.on_trait_change( cls._update_editor, name + "[]", dispatch="ui" ) super(cls.__class__, cls).prepare(parent) def _dispose_method(cls): """Unbound implementation of the dispose editor method to remove the change notification hook in the items of the list before calling the parent dispose method of the parent class. """ if cls.ui is None: return name = cls.extended_name if name != "None": cls.context_object.on_trait_change( cls._update_editor, name + "[]", remove=True ) super(cls.__class__, cls).dispose() class CSVListEditor(TextEditor): """A text editor for a List. This editor provides a single line of input text of comma separated values. (Actually, the default separator is a comma, but this can changed.) The editor can only be used with List traits whose inner trait is one of Int, Float, Str, Enum, or Range. The 'simple', 'text', 'custom' and readonly styles are based on TextEditor. The 'readonly' style provides the same formatting in the text field as the other editors, but the user cannot change the value. Like other Traits editors, the background of the text field will turn red if the user enters an incorrectly formatted list or if the values do not match the type of the inner trait. This validation only occurs while editing the text field. If, for example, the inner trait is Range(low='lower', high='upper'), a change in 'upper' will not trigger the validation code of the editor. The editor removes whitespace of entered items with strip(), so for Str types, the editor should not be used if whitespace at the beginning or end of the string must be preserved. Parameters ---------- sep : str or None, optional The separator of the values in the list. If None, each contiguous sequence of whitespace is a separator. Default is ','. ignore_trailing_sep : bool, optional If this is False, a line containing a trailing separator is invalid. Default is True. auto_set : bool If True, then every keystroke sets the value of the trait. enter_set : bool If True, the user input sets the value when the Enter key is pressed. Example ------- The following will display a window containing a single input field. Entering, say, '0, .5, 1' in this field will result in the list x = [0.0, 0.5, 1.0]. """ #: The separator of the element in the list. sep = Union(None, Str, default_value=",") #: If False, it is an error to have a trailing separator. ignore_trailing_sep = Bool(True) #: Include some of the TextEditor API: #: Is user input set on every keystroke? auto_set = Bool(True) #: Is user input set when the Enter key is pressed? enter_set = Bool(False) def _funcs(self, object, name): """Create the evalution and formatting functions for the editor. Parameters ---------- object : instance of HasTraits This is the object that has the List trait for which we are creating an editor. name : str Name of the List trait on `object`. Returns ------- evaluate, fmt_func : callable, callable The functions for converting a string to a list (`evaluate`) and a list to a string (`fmt_func`). These are the functions that are ultimately given as the keyword arguments 'evaluate' and 'format_func' of the TextEditor that will be generated by the CSVListEditor editor factory functions. """ t = getattr(object, name) # Get the list of inner traits. Only a single inner trait is allowed. it_list = t.trait.inner_traits() if len(it_list) > 1: raise TraitError( "Only one inner trait may be specified when " "using a CSVListEditor." ) # `it` is the single inner trait. This will be an instance of # traits.traits.CTrait. it = it_list[0] # The following 'if' statement figures out the appropriate evaluation # function (evaluate) and formatting function (fmt_func) for the # given inner trait. if ( it.is_trait_type(Int) or it.is_trait_type(Float) or it.is_trait_type(Str) ): evaluate = lambda s: _eval_list_str( s, sep=self.sep, item_eval=it.trait_type.evaluate, ignore_trailing_sep=self.ignore_trailing_sep, ) fmt_func = lambda vals: _format_list_str(vals, sep=self.sep) elif it.is_trait_type(Enum): values, mapping, inverse_mapping = enum_values_changed(it) evaluate = lambda s: _eval_list_str( s, sep=self.sep, item_eval=mapping.__getitem__, ignore_trailing_sep=self.ignore_trailing_sep, ) fmt_func = lambda vals: _format_list_str( vals, sep=self.sep, item_format=inverse_mapping.__getitem__ ) elif it.is_trait_type(Range): # Get the type of the values from the default value. # range_object will be an instance of traits.trait_types.Range. range_object = it.handler if range_object.default_value_type == 8: # range_object.default_value is callable. defval = range_object.default_value(object) else: # range_object.default_value *is* the default value. defval = range_object.default_value typ = type(defval) if range_object.validate is None: # This will be the case for dynamic ranges. item_eval = lambda s: _validate_range_value( range_object, object, name, typ(s) ) else: # Static ranges have a validate method. item_eval = lambda s: range_object.validate( object, name, typ(s) ) evaluate = lambda s: _eval_list_str( s, sep=self.sep, item_eval=item_eval, ignore_trailing_sep=self.ignore_trailing_sep, ) fmt_func = lambda vals: _format_list_str(vals, sep=self.sep) else: raise TraitError( "To use a CSVListEditor, the inner trait of the " "List must be Int, Float, Range, Str or Enum." ) return evaluate, fmt_func def simple_editor(self, ui, object, name, description, parent): """Generates an editor using the "simple" style.""" self.evaluate, self.format_func = self._funcs(object, name) return self.simple_editor_class( parent, factory=self, ui=ui, object=object, name=name, description=description, ) def custom_editor(self, ui, object, name, description, parent): """Generates an editor using the "custom" style.""" self.evaluate, self.format_func = self._funcs(object, name) return self.custom_editor_class( parent, factory=self, ui=ui, object=object, name=name, description=description, ) def text_editor(self, ui, object, name, description, parent): """Generates an editor using the "text" style.""" self.evaluate, self.format_func = self._funcs(object, name) return self.text_editor_class( parent, factory=self, ui=ui, object=object, name=name, description=description, ) def readonly_editor(self, ui, object, name, description, parent): """Generates an "editor" that is read-only.""" self.evaluate, self.format_func = self._funcs(object, name) return self.readonly_editor_class( parent, factory=self, ui=ui, object=object, name=name, description=description, ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/editors/custom_editor.py0000644000175100001730000000303500000000000022735 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Defines the editor factory used to wrap a non-Traits based custom control. """ from traits.api import Callable, Tuple, Property from traitsui.basic_editor_factory import BasicEditorFactory from traitsui.toolkit import toolkit_object class CustomEditor(BasicEditorFactory): """Editor factory for custom editors.""" #: Editor class to be instantiated. klass = Property() #: Factory function used to create the custom control factory = Callable() #: Arguments to be passed to the user's custom editor factory args = Tuple() def __init__(self, *args, **traits): if len(args) >= 1: self.factory = args[0] self.args = args[1:] super().__init__(**traits) # ------------------------------------------------------------------------- # Property getters # ------------------------------------------------------------------------- def _get_klass(self): """Returns the editor class to be created.""" return toolkit_object("custom_editor:CustomEditor") # This alias is deprecated and will be removed in TraitsUI 8. ToolkitEditorFactory = CustomEditor ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/editors/date_editor.py0000644000175100001730000000717400000000000022350 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ A Traits UI editor that wraps a WX calendar panel. """ from traits.trait_types import Bool, Instance, Int, Enum, Str from traitsui.editor_factory import EditorFactory from traitsui.ui_traits import AView class CellFormat(object): """Styling attributes for calendar widget cells. Encapsulates some common visual attributes to set on the cells of a calendar widget. All attributes default to None, which means that they will not override the existing values of the calendar widget. The color attributes should be strings representing color names, from the list: red, green, blue, cyan, magenta, yellow, gray, white, darkRed, darkGreen, darkBlue, darkCyan, darkmagenta, darkYellow, darkGray, black, lightGray. Alternatively, they can be a tuple of (R,G,B) values from 0-255. """ #: Whether to display in an italic style. italics = None #: Whether to use a bold weight. bold = None #: Whether to underline the text. underline = None #: The background color. bgcolor = None #: The text color. fgcolor = None def __init__(self, **args): for key, val in args.items(): setattr(self, key, val) class DateEditor(EditorFactory): """Editor factory for date/time editors.""" # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- # -- ReadonlyEditor traits ------------------------------------------------ #: Message to show when Date is None. message = Str("Undefined") #: The string representation of the date to show. Uses time.strftime #: format. strftime = Str("%B %d %Y (%a)") #: An optional view to display when a read-only text editor is clicked: view = AView # -- CustomEditor traits -------------------------------------------------- #: Should users be able to pick future dates when using the CustomEditor? allow_future = Bool(True) #: How many months to show at a time. months = Int(3) #: True: Must be a List of Dates. False: Must be a Date instance. multi_select = Bool(False) #: When a user multi-selects entries and some of those entries are already #: selected and some are not, what should be the behavior for the seletion? #: #: Options: #: #: - 'toggle': Toggle each day to the opposite of the current state. #: - 'on': Always turn them on. #: - 'off': Always turn them off. #: - 'max_change': Change all to same state, with most days changing. #: For example 1 selected and 9 not, then they would all get selected. #: - 'min_change': Change all to same state, with min days changing. #: For example 1 selected and 9 not, then they would all get unselected. on_mixed_select = Enum("toggle", "on", "off", "max_change", "min_change") #: How much space to put between the individual months. padding = Int(5) #: Does the user have to hold down Shift for the left-click multiselect? shift_to_select = Bool(False) #: Style used when a date is selected. selected_style = Instance( CellFormat, kw={"bold": True, "fgcolor": (255, 255, 255), "bgcolor": (0, 128, 0)}, ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/editors/date_range_editor.py0000644000175100001730000000203600000000000023514 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! from traits.api import Bool, Constant from traitsui.editors.date_editor import DateEditor class DateRangeEditor(DateEditor): """Editor for a date range. The target value should be a tuple containing two dates (start date, end date) """ #: This must be set to true for setting a date range. multi_select = Constant(True) #: Whether it is possible to unset the date range. #: If true, then the date range will be set to (None, None) #: when all the dates are unselected. allow_no_selection = Bool(False) # This alias is deprecated and will be removed in TraitsUI 8. ToolkitEditorFactory = DateRangeEditor ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/editors/datetime_editor.py0000644000175100001730000000252300000000000023220 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ A Traits UI editor """ import datetime from traits.api import Datetime, Str from traitsui.editor_factory import EditorFactory class DatetimeEditor(EditorFactory): """Editor factory for the datetime editor.""" # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- #: The earliest datetime allowed by the editor minimum_datetime = Datetime(datetime.datetime(100, 1, 1), allow_none=True) #: The latest datetime allowed by the editor maximum_datetime = Datetime(datetime.datetime.max, allow_none=True) # -- ReadonlyEditor traits ------------------------------------------------ #: Message to show when datetime is None. message = Str("Undefined") #: The string representation of the datetime to show. Uses time.strftime #: format. strftime = Str("%Y-%m-%dT%H:%M:%S") ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/editors/default_override.py0000644000175100001730000000474600000000000023412 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Editor factory that overrides certain attributes of the default editor. For example, the default editor for Range(low=0, high=1500) has '1500' as the upper label. To change it to 'Max' instead, use my_range = Range(low=0, high=1500, editor=DefaultOverride(high_label='Max')) Alternatively, the override can also be specified in the view: View(Item('my_range', editor=DefaultOverride(high_label='Max')) """ from traits.api import Dict from traitsui.editor_factory import EditorFactory class DefaultOverride(EditorFactory): """Editor factory for selectively overriding certain parameters of the default editor. """ _overrides = Dict() def __init__(self, *args, **overrides): EditorFactory.__init__(self, *args) self._overrides = overrides def _customise_default( self, editor_kind, ui, object, name, description, parent ): """ Obtain the given trait's default editor and set the parameters specified in `overrides` above. """ trait = object.trait(name) editor_factory = trait.trait_type.create_editor() for option in self._overrides: setattr(editor_factory, option, self._overrides[option]) editor = getattr(editor_factory, editor_kind)( ui, object, name, description, parent ) return editor def simple_editor(self, ui, object, name, description, parent): return self._customise_default( "simple_editor", ui, object, name, description, parent ) def custom_editor(self, ui, object, name, description, parent): return self._customise_default( "custom_editor", ui, object, name, description, parent ) def text_editor(self, ui, object, name, description, parent): return self._customise_default( "text_editor", ui, object, name, description, parent ) def readonly_editor(self, ui, object, name, description, parent): return self._customise_default( "readonly_editor", ui, object, name, description, parent ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/editors/directory_editor.py0000644000175100001730000000134100000000000023425 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Defines the directory editor factory for all traits toolkit backends. """ from traitsui.editors.file_editor import FileEditor class DirectoryEditor(FileEditor): """Editor factory for directory editors.""" pass # This alias is deprecated and will be removed in TraitsUI 8. ToolkitEditorFactory = DirectoryEditor ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/editors/dnd_editor.py0000644000175100001730000000236600000000000022176 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Defines the editor factory for a drag-and-drop editor. A drag-and-drop editor represents its value as a simple image which, depending upon the editor style, can be a drag source only, a drop target only, or both a drag source and a drop target. """ from pyface.ui_traits import Image from traitsui.editor_factory import EditorFactory class DNDEditor(EditorFactory): """Editor factory for drag-and-drop editors.""" # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- #: The image to use for the target: image = Image #: The image to use when the target is disabled: disabled_image = Image # This alias is deprecated and will be removed in TraitsUI 8. ToolkitEditorFactory = DNDEditor ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/editors/drop_editor.py0000644000175100001730000000234400000000000022371 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Defines a drop editor factory for all traits toolkit backends. A drop target editor handles drag and drop operations as a drop target. """ from traits.api import Any, Bool from traitsui.editors.text_editor import TextEditor class DropEditor(TextEditor): """Editor factory for drop editors.""" # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- #: Allowable drop objects must be of this class (optional) klass = Any() #: Must allowable drop objects be bindings? binding = Bool(False) #: Can the user type into the editor, or is it read only? readonly = Bool(True) # This alias is deprecated and will be removed in TraitsUI 8. ToolkitEditorFactory = DropEditor ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/editors/enum_editor.py0000644000175100001730000000570600000000000022376 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Defines the editor factory for single-selection enumerations, for all traits user interface toolkits. """ import os import sys from traits.api import Any, Range, Enum, Bool, Str from traitsui.editor_factory import EditorWithListFactory from traitsui.toolkit import toolkit_object # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- #: Supported display modes for a custom style editor Mode = Enum("radio", "list") #: Supported display modes for a custom style editor CompletionMode = Enum("inline", "popup") class EnumEditor(EditorWithListFactory): """Editor factory for enumeration editors.""" # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- #: (Optional) Function used to evaluate text input: evaluate = Any() #: Is user input set on every keystroke (when text input is allowed)? auto_set = Bool(True) #: Number of columns to use when displayed as a grid: cols = Range(1, 20) #: Display modes supported for a custom style editor: mode = Mode #: Completion mode for editors with text-entry (Qt only): completion_mode = CompletionMode #: Whether values trait contains separators (Qt only) use_separator = Bool(False) #: The separator string used in values trait (Qt only) separator = Str("") # ------------------------------------------------------------------------- # 'Editor' factory methods: # ------------------------------------------------------------------------- def _get_custom_editor_class(self): """Returns the editor class to use for "custom" style views. Overridden to return the editor class for the specified mode. """ editor_file_name = os.path.basename( sys.modules[self.__class__.__module__].__file__ ) try: if self.mode == "radio": return toolkit_object( editor_file_name.split(".")[0] + ":RadioEditor", raise_exceptions=True, ) else: return toolkit_object( editor_file_name.split(".")[0] + ":ListEditor", raise_exceptions=True, ) except: return super()._get_custom_editor_class() # This alias is deprecated and will be removed in TraitsUI 8. ToolkitEditorFactory = EnumEditor ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/editors/file_editor.py0000644000175100001730000000625300000000000022347 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Defines the file editor factory for all traits toolkit backends. """ from traits.api import Bool, File, Int, List, Str from traitsui.editors.text_editor import TextEditor from traitsui.group import Group from traitsui.view import View # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- #: Wildcard filter: filter_trait = List(Str) class FileEditor(TextEditor): """Editor factory for file editors.""" # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- #: Wildcard filter to apply to the file dialog: filter = filter_trait #: Optional extended trait name of the trait containing the list of #: filters: filter_name = Str() #: Should file extension be truncated? truncate_ext = Bool(False) #: Can the user select directories as well as files? allow_dir = Bool(False) #: Is user input set on every keystroke? (Overrides the default) ('simple' #: style only): auto_set = False #: Is user input set when the Enter key is pressed? (Overrides the default) #: ('simple' style only): enter_set = True #: The number of history entries to maintain: #: FIXME: This is currently only supported on wx. Qt support needs to be #: added entries = Int(10) #: The root path of the file tree view ('custom' style only, not supported #: under wx). If not specified, the filesystem root is used. root_path = File() #: Optional extend trait name of the trait containing the root path. root_path_name = Str() #: Optional extended trait name used to notify the editor when the file #: system view should be reloaded ('custom' style only): reload_name = Str() #: Optional extended trait name used to notify when the user double-clicks #: an entry in the file tree view. The associated path is assigned it: dclick_name = Str() #: The style of file dialog to use when the 'Browse...' button is clicked #: Should be one of 'open' or 'save' dialog_style = Str("open") # ------------------------------------------------------------------------- # Traits view definition: # ------------------------------------------------------------------------- traits_view = View( [ [ "", "truncate_ext{Automatically truncate file extension?}", "|options:[Options]>", ], ["filter", "|[Wildcard filters]<>"], ] ) extras = Group() # This alias is deprecated and will be removed in TraitsUI 8. ToolkitEditorFactory = FileEditor ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/editors/font_editor.py0000644000175100001730000000332100000000000022367 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Defines the font editor factory for all traits user interface toolkits. """ from traitsui.editor_factory import EditorFactory from traitsui.toolkit import toolkit_object # ------------------------------------------------------------------------- # 'ToolkitEditorFactory' class: # ------------------------------------------------------------------------- class ToolkitEditorFactory(EditorFactory): """Editor factory for font editors.""" pass # Define the FontEditor class # The function will try to return the toolkit-specific editor factory (located # in traitsui..font_editor, and if none is found, the # ToolkitEditorFactory declared here is returned. def FontEditor(*args, **traits): r"""Returns an instance of the toolkit-specific editor factory declared in traitsui..font_editor. If such an editor factory cannot be located, an instance of the abstract ToolkitEditorFactory declared in traitsui.editors.font_editor is returned. Parameters ---------- \*args, \*\*traits arguments and keywords to be passed on to the editor factory's constructor. """ try: return toolkit_object("font_editor:ToolkitEditorFactory", True)( *args, **traits ) except Exception: return ToolkitEditorFactory(*args, **traits) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/editors/history_editor.py0000644000175100001730000000252500000000000023127 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Defines a text editor which displays a text field and maintains a history of previously entered values. """ from traits.api import Int, Bool from traitsui.basic_editor_factory import BasicEditorFactory from traitsui.toolkit import toolkit_object # Define callable which returns the 'klass' value (i.e. the editor to use in # the EditorFactory. def history_editor(*args, **traits): return toolkit_object("history_editor:_HistoryEditor")(*args, **traits) # ------------------------------------------------------------------------- # Create the editor factory object: # ------------------------------------------------------------------------- class ToolkitEditorFactory(BasicEditorFactory): #: The number of entries in the history: entries = Int(10) #: Should each keystroke update the value (or only the enter key, tab, #: etc.)? auto_set = Bool(False) HistoryEditor = ToolkitEditorFactory(klass=history_editor) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/editors/html_editor.py0000644000175100001730000001142400000000000022370 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Defines the HTML editor factory. HTML editors interpret and display HTML-formatted text, but do not modify it. """ from traits.api import Bool, Str from traitsui.basic_editor_factory import BasicEditorFactory from traitsui.toolkit import toolkit_object # Callable that returns the editor to use in the UI. def html_editor(*args, **traits): return toolkit_object("html_editor:SimpleEditor")(*args, **traits) # Template used to create code blocks embedded in the module comment block_template = """
    %s
    """ # Template used to create lists embedded in the module comment list_template = """<%s> %s """ # ------------------------------------------------------------------------------ # 'ToolkitEditorFactory' class: # ------------------------------------------------------------------------------ class ToolkitEditorFactory(BasicEditorFactory): """Editor factory for HTML editors.""" # -------------------------------------------------------------------------- # Trait definitions: # -------------------------------------------------------------------------- #: Should implicit text formatting be converted to HTML? format_text = Bool(False) #: External objects referenced in the HTML are relative to this url base_url = Str() #: The object trait containing the base URL base_url_name = Str() #: Should links be opened in an external browser? open_externally = Bool(False) def parse_text(self, text): """Parses the contents of a formatted text string into the corresponding HTML. """ text = text.replace("\r\n", "\n") lines = [("." + line).strip()[1:] for line in text.split("\n")] ind = min( *( [self.indent(line) for line in lines if line != ""] + [1000, 1000] ) ) if ind >= 1000: ind = 0 lines = [line[ind:] for line in lines] new_lines = [] i = 0 n = len(lines) while i < n: line = lines[i] m = self.indent(line) if m > 0: if line[m] in "-*": i, line = self.parse_list(lines, i) else: i, line = self.parse_block(lines, i) new_lines.append(line) else: new_lines.append(line) i += 1 text = "\n".join(new_lines) paragraphs = [p.strip() for p in text.split("\n\n")] for i, paragraph in enumerate(paragraphs): if paragraph[:3].lower() != "

    ": paragraphs[i] = "

    %s

    " % paragraph return "\n".join(paragraphs) def parse_block(self, lines, i): """Parses a code block.""" m = 1000 n = len(lines) j = i while j < n: line = lines[j] if line != "": k = self.indent(line) if k == 0: break m = min(m, k) j += 1 j -= 1 while (j > i) and (lines[j] == ""): j -= 1 j += 1 temp = [ ((" " * (self.indent(line) - m)) + line.strip()) for line in lines[i:j] ] return (j, block_template % "\n
    ".join(temp)) def parse_list(self, lines, i): """Parses a list.""" line = lines[i] m = self.indent(line) kind = line[m] result = ["
  • " + line[m + 1 :].strip()] n = len(lines) j = i + 1 while j < n: line = lines[j] k = self.indent(line) if k < m: break if k == m: if line[k] != kind: break result.append("
  • " + line[k + 1 :].strip()) j += 1 elif line[k] in "-*": j, line = self.parse_list(lines, j) result.append(line) else: result.append(line.strip()) j += 1 style = ["ul", "ol"][kind == "*"] return (j, list_template % (style, "\n".join(result), style)) def indent(self, line): """Calculates the amount of white space at the beginning of a line.""" return len(line) - len((line + ".").strip()) + 1 HTMLEditor = ToolkitEditorFactory(klass=html_editor) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/editors/image_editor.py0000644000175100001730000000327300000000000022511 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Traits UI 'display only' image editor. """ from pyface.ui_traits import Image from traits.api import Bool, Property from traitsui.basic_editor_factory import BasicEditorFactory from traitsui.toolkit import toolkit_object # ------------------------------------------------------------------------- # 'ImageEditor' editor factory class: # ------------------------------------------------------------------------- class ImageEditor(BasicEditorFactory): #: The editor class to be created: klass = Property() #: The optional image resource to be displayed by the editor (if not #: specified, the editor's object value is used as the ImageResource to #: display): image = Image #: The following traits are currently supported on Qt only #: Whether or not to scale the image to fit the available space scale = Bool() #: Whether or not to scale the image larger than the original when scaling allow_upscaling = Bool() #: Whether or not to preserve the aspect ratio when scaling preserve_aspect_ratio = Bool() #: Whether or not to allow the image to be clipped when not scaling allow_clipping = Bool() def _get_klass(self): """Returns the editor class to be instantiated.""" return toolkit_object("image_editor:_ImageEditor") ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/editors/image_enum_editor.py0000644000175100001730000000503500000000000023533 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Defines the image enumeration editor factory for all traits user interface toolkits. """ from os import getcwd from os.path import join, dirname, exists import sys from traits.api import Module, Type, Str, observe from traitsui.editors.enum_editor import EnumEditor class ImageEnumEditor(EnumEditor): """Editor factory for image enumeration editors.""" # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- #: Prefix to add to values to form image names: prefix = Str() #: Suffix to add to values to form image names: suffix = Str() #: Path to use to locate image files: path = Str() #: Class used to derive the path to the image files: klass = Type() #: Module used to derive the path to the image files: module = Module def init(self): """Performs any initialization needed after all constructor traits have been set. """ super().init() self._update_path() @observe("path, klass, module") def _update_path(self, event=None): """Handles one of the items defining the path being updated.""" if self.path != "": self._image_path = self.path elif self.klass is not None: module = self.klass.__module__ if module == "___main___": module = "__main__" try: self._image_path = join( dirname(sys.modules[module].__file__), "images" ) except: self._image_path = self.path dirs = [ join(dirname(sys.argv[0]), "images"), join(getcwd(), "images"), ] for d in dirs: if exists(d): self._image_path = d break elif self.module is not None: self._image_path = join(dirname(self.module.__file__), "images") # This alias is deprecated and will be removed in TraitsUI 8. ToolkitEditorFactory = ImageEnumEditor ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/editors/instance_editor.py0000644000175100001730000000623400000000000023233 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Defines the instance editor factory for all traits user interface toolkits. """ from traits.api import Bool, Enum, List, Str, Type from traitsui.editor_factory import EditorFactory from traitsui.instance_choice import InstanceChoice, InstanceChoiceItem from traitsui.ui_traits import AView from traitsui.view import View, AKind class InstanceEditor(EditorFactory): """Editor factory for instance editors.""" # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- #: List of items describing the types of selectable or editable instances values = List(InstanceChoiceItem) #: Extended name of the context object trait containing the list of types #: of selectable or editable instances name = Str() #: Is the current value of the object trait editable (vs. merely #: selectable)? editable = Bool(True) #: Should the object trait value be selectable from a list of objects (a #: value of True forces a selection list to be displayed, while a value of #: False displays a selection list only if at least one object in the list #: of possible object values is selectable): selectable = Bool(False) #: Should the editor support drag and drop of objects to set the trait #: value (a value of True forces the editor to allow drag and drop, while #: a value of False only supports drag and drop if at least one item in the #: list of possible objects supports drag and drop): droppable = Bool(False) #: Should factory-created objects be cached? cachable = Bool(True) #: Optional label for button label = Str() #: Optional instance view to use view = AView #: Extended name of the context object trait containing the view, or name #: of the view, to use view_name = Str() #: The ID to use with the view id = Str() #: Kind of pop-up editor (live, modal, nonmodal, wizard) kind = AKind #: The orientation of the instance editor relative to the instance selector orientation = Enum("default", "horizontal", "vertical") #: The default adapter class used to create InstanceChoice compatible #: adapters for instance objects: adapter = Type(InstanceChoice, allow_none=False) # ------------------------------------------------------------------------- # Traits view definitions: # ------------------------------------------------------------------------- traits_view = View( [ ["label{Button label}", "view{View name}", "|[]"], ["kind@", "|[Pop-up editor style]<>"], ] ) # This alias is deprecated and will be removed in TraitsUI 8. ToolkitEditorFactory = InstanceEditor ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/editors/key_binding_editor.py0000644000175100001730000000223100000000000023702 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Defines the key binding editor for use with the KeyBinding class. This is a specialized editor used to associate a particular key with a control (i.e., the key binding editor). """ from traitsui.basic_editor_factory import BasicEditorFactory from traitsui.toolkit import toolkit_object # Callable which returns the editor to use in the ui. def key_binding_editor(*args, **traits): return toolkit_object("key_binding_editor:KeyBindingEditor")( *args, **traits ) # ------------------------------------------------------------------------- # Create the editor factory object: # ------------------------------------------------------------------------- KeyBindingEditor = ToolkitEditorFactory = BasicEditorFactory( klass=key_binding_editor ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/editors/list_editor.py0000644000175100001730000001522200000000000022377 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Defines the list editor factory for the traits user interface toolkits.. """ from traits.api import ( Any, BaseTraitHandler, Bool, Callable, Dict, Enum, HasTraits, Instance, Int, Property, PrototypedFrom, Range, Str, Tuple, ) from traitsui.editor_factory import EditorFactory from traitsui.helper import DockStyle from traitsui.item import Item from traitsui.toolkit import toolkit_object from traitsui.ui_traits import style_trait, AView from traitsui.view import View # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- #: Trait whose value is a BaseTraitHandler object handler_trait = Instance(BaseTraitHandler) #: The visible number of rows displayed rows_trait = Range(1, 50, 5, desc="the number of list rows to display") #: The visible number of columns displayed columns_trait = Range(1, 10, 1, desc="the number of list columns to display") editor_trait = Instance(EditorFactory) class ListEditor(EditorFactory): """Editor factory for list editors.""" # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- #: The editor to use for each list item: editor = editor_trait #: Can the list be reorganized, or have items added and deleted. mutable = Bool(True) #: Should the scrollbars be displayed if the list is too long. scrollable = Bool(True, sync_value=True) #: The style of editor to use for each item: style = style_trait #: The trait handler for each list item: trait_handler = handler_trait #: The number of list rows to display: rows = rows_trait #: The number of list columns to create: columns = columns_trait #: Use a notebook for a custom view? use_notebook = Bool(False) #: Show a right-click context menu for the notebook tabs? (Qt only) show_notebook_menu = Bool(False) #: Factory that will be called to create and add a new element to this #: list. If None, the default value for the trait of interest is used. item_factory = Callable() #: Tuple of positional arguments to be passed to the default factory #: callable when creating new elements item_factory_args = Tuple() #: Dictionary of keyword arguments to be passed to the default factory #: callable when creating new elements item_factory_kwargs = Dict() # -- Notebook Specific Traits --------------------------------------------- #: Are notebook items deletable? deletable = Bool(False) #: The extended name of the trait on each page object which should be used #: to determine whether or not an individual page should be deletable. deletable_trait = Str() #: FIXME: Currently, this trait is used only in the wx backend. #: The DockWindow graphical theme dock_theme = Any() #: FIXME: Currently, this trait is used only in the wx backend. #: Dock page style to use for each DockControl: dock_style = DockStyle #: Export class for each item in a notebook: export = Str() #: Name of the view to use in notebook mode: view = AView #: The type of UI to construct ('panel', 'subpanel', etc) ui_kind = Enum("subpanel", "panel") #: A factory function that can be used to define that actual object to be #: edited (i.e. view_object = factory( object )): factory = Callable() #: Extended name to use for each notebook page. It can be either the actual #: name or the name of an attribute on the object in the form: #: '.name[.name...]' page_name = Str() #: Name of the [object.]trait[.trait...] to synchronize notebook page #: selection with: selected = Str() # ------------------------------------------------------------------------- # Traits view definition: # ------------------------------------------------------------------------- traits_view = View( [ ["use_notebook{Use a notebook in a custom view}", "|[Style]"], [ Item("page_name", enabled_when="object.use_notebook"), Item("view", enabled_when="object.use_notebook"), "|[Notebook options]", ], [ Item("rows", enabled_when="not object.use_notebook"), "|[Number of list rows to display]<>", ], ] ) # ------------------------------------------------------------------------- # 'Editor' factory methods: # ------------------------------------------------------------------------- def _get_custom_editor_class(self): if self.use_notebook: return toolkit_object("list_editor:NotebookEditor") return toolkit_object("list_editor:CustomEditor") # ------------------------------------------------------------------------- # 'ListItemProxy' class: # This class is used to update the list editors when the object changes # external to the editor. # ------------------------------------------------------------------------- class ListItemProxy(HasTraits): #: The list proxy: list = Property() #: The item proxies index into the original list: index = Int() #: Delegate all other traits to the original object: _ = PrototypedFrom("_zzz_object") #: Define all of the private internal use values (the funny names are an #: attempt to avoid name collisions with delegated trait names): _zzz_inited = Any() _zzz_object = Any() _zzz_name = Any() def __init__(self, object, name, index, trait, value): super().__init__() self._zzz_inited = False self._zzz_object = object self._zzz_name = name self.index = index if trait is not None: self.add_trait("value", trait) self.value = value self._zzz_inited = self.index < len(self.list) def _get_list(self): return getattr(self._zzz_object, self._zzz_name) def _value_changed(self, old_value, new_value): if self._zzz_inited: self.list[self.index] = new_value # This alias is deprecated and will be removed in TraitsUI 8. ToolkitEditorFactory = ListEditor ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/editors/list_str_editor.py0000644000175100001730000000671100000000000023272 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Traits UI editor factory for editing lists of strings. """ from pyface.image_resource import ImageResource from traits.api import Any, Str, Enum, List, Bool, Instance, Property from traitsui.basic_editor_factory import BasicEditorFactory from traitsui.toolkit import toolkit_object # ------------------------------------------------------------------------- # 'ListStrEditor' editor factory class: # ------------------------------------------------------------------------- class ListStrEditor(BasicEditorFactory): """Editor factory for list of string editors.""" # -- Trait Definitions ---------------------------------------------------- #: The editor class to be created: klass = Property() #: The optional extended name of the trait to synchronize the selection #: values with: selected = Str() #: The optional extended name of the trait to synchronize the selection #: indices with: selected_index = Str() #: The optional extended name of the trait to synchronize the activated #: value with: activated = Str() #: The optional extended name of the trait to synchronize the activated #: value's index with: activated_index = Str() #: The optional extended name of the trait to synchronize the right clicked #: value with: right_clicked = Str() #: The optional extended name of the trait to synchronize the right clicked #: value's index with: right_clicked_index = Str() #: Can the user edit the values? editable = Bool(True) #: Are multiple selected items allowed? multi_select = Bool(False) #: Should horizontal lines be drawn between items? horizontal_lines = Bool(False) #: The title for the editor: title = Str() #: The optional extended name of the trait containing the editor title: title_name = Str() #: Should a new item automatically be added to the end of the list to allow #: the user to create new entries? auto_add = Bool(False) #: The adapter from list items to editor values: adapter = Instance("traitsui.list_str_adapter.ListStrAdapter", ()) #: The optional extended name of the trait containing the adapter: adapter_name = Str() #: What type of operations are allowed on the list: operations = List( Enum("delete", "insert", "append", "edit", "move"), ["delete", "insert", "append", "edit", "move"], ) #: Are 'drag_move' operations allowed (i.e. True), or should they always be #: treated as 'drag_copy' operations (i.e. False): drag_move = Bool(False) #: The set of images that can be used: images = List(ImageResource) #: Right-click context menu (Qt4 only). The value can be one of: #: #: - Instance( Menu ): Use this menu as the context menu #: - string: Name of traits that will provide menu #: - None: Use the default context menu #: - False: Do not display a context menu menu = Any() def _get_klass(self): """Returns the editor class to be created.""" return toolkit_object("list_str_editor:_ListStrEditor") ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/editors/null_editor.py0000644000175100001730000000174000000000000022376 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Defines a completely empty editor, intended to be used as a spacer. """ from traitsui.basic_editor_factory import BasicEditorFactory from traitsui.toolkit import toolkit_object # Callable which returns the editor to use in the ui. def null_editor(*args, **traits): return toolkit_object("null_editor:NullEditor")(*args, **traits) # ------------------------------------------------------------------------- # Create the editor factory object: # ------------------------------------------------------------------------- NullEditor = BasicEditorFactory(klass=null_editor) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/editors/popup_editor.py0000644000175100001730000000577000000000000022576 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! from traits.api import Float, Enum, Any, Property from traitsui.basic_editor_factory import BasicEditorFactory from traitsui.editor_factory import EditorFactory from traitsui.editors.text_editor import TextEditor from traitsui.item import Item from traitsui.toolkit import toolkit_object from traitsui.ui_editor import UIEditor from traitsui.ui_traits import EditorStyle from traitsui.view import View # ------------------------------------------------------------------------- # '_PopupEditor' class: # ------------------------------------------------------------------------- class _PopupEditor(UIEditor): def init_ui(self, parent): """Creates the traits UI for the editor.""" return self.object.edit_traits(view=self.base_view(), parent=parent) def base_view(self): """Returns the View that allows the popup view to be displayed.""" return View( Item( self.name, show_label=False, style="readonly", editor=TextEditor(view=self.popup_view()), padding=-4, ), kind="subpanel", ) def popup_view(self): """Returns the popup View.""" factory = self.factory item = Item( self.name, show_label=False, padding=-4, style=factory.style, height=factory.height, width=factory.width, ) editor = factory.editor if editor is not None: if not isinstance(editor, EditorFactory): editor = editor() item.editor = editor return View(item, kind=factory.kind) # ------------------------------------------------------------------------- # 'PopupEditor' class: # ------------------------------------------------------------------------- class PopupEditor(BasicEditorFactory): #: The class used to construct editor objects: klass = Property() #: The kind of popup to use: kind = Enum("popover", "popup", "info") #: The editor to use for the pop-up view (can be None (use default editor), #: an EditorFactory instance, or a callable that returns an EditorFactory #: instance): editor = Any() #: The style of editor to use for the popup editor (same as Item.style): style = EditorStyle #: The height of the popup (same as Item.height): height = Float(-1.0) #: The width of the popup (same as Item.width): width = Float(-1.0) def _get_klass(self): """The class used to construct editor objects.""" return toolkit_object("popup_editor:_PopupEditor") ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/editors/progress_editor.py0000644000175100001730000000327000000000000023270 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Defines the progress editor factory for all traits toolkit backends, """ from traits.api import Int, Bool, Str from traitsui.editor_factory import EditorFactory class ProgressEditor(EditorFactory): """Editor factory for code editors.""" # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- #: The title title = Str() #: The message to be displayed along side the progress guage message = Str() #: The name of an [object.]trait that defines the message string message_name = Str() #: The starting value min = Int() #: The name of an [object.]trait that defines the starting value min_name = Str() #: The ending value max = Int() #: The name of an [object.]trait that defines the ending value max_name = Str() #: If the cancel button should be shown (not very sensible as an editor) can_cancel = Bool(False) #: If the estimated time should be shown (not very sensible as an editor) show_time = Bool(False) #: if the percent complete should be shown show_percent = Bool(False) # This alias is deprecated and will be removed in TraitsUI 8. ToolkitEditorFactory = ProgressEditor ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/editors/range_editor.py0000644000175100001730000002314500000000000022523 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Defines the range editor factory for all traits user interface toolkits. """ import warnings from types import CodeType from traits.api import ( Any, Bool, CTrait, Enum, Int, Property, Range, Str, Undefined, ) from traitsui.editor_factory import EditorFactory from traitsui.toolkit import toolkit_object from traitsui.view import View class RangeEditor(EditorFactory): """Editor factory for range editors.""" # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- #: Number of columns when displayed as an enumeration cols = Range(1, 20) #: Is user input set on every keystroke? auto_set = Bool(True) #: Is user input set when the Enter key is pressed? enter_set = Bool(False) #: Label for the low end of the range low_label = Str() #: Label for the high end of the range high_label = Str() #: FIXME: This is supported only in the wx backend so far. #: The width of the low and high labels label_width = Int() #: The name of an [object.]trait that defines the low value for the range low_name = Str() #: The name of an [object.]trait that defines the high value for the range high_name = Str() #: Formatting string used to format value and labels format_str = Str("%s") #: Is the range for floating pointer numbers (vs. integers)? is_float = Bool(Undefined) #: Function to evaluate floats/ints when they are assigned to an object #: trait evaluate = Any() #: The object trait containing the function used to evaluate floats/ints evaluate_name = Str() #: Low end of range low = Property() #: High end of range high = Property() #: Display mode to use mode = Enum( "auto", "slider", "xslider", "spinner", "enum", "text", "logslider" ) # ------------------------------------------------------------------------- # Traits view definition: # ------------------------------------------------------------------------- traits_view = View( [ ["low", "high", "|[Range]"], ["low_label{Low}", "high_label{High}", "|[Range Labels]"], [ "auto_set{Set automatically}", "enter_set{Set on enter key pressed}", "is_float{Is floating point range}", "-[Options]>", ], ["cols", "|[Number of columns for integer custom style]<>"], ] ) def init(self, handler=None): """Performs any initialization needed after all constructor traits have been set. """ if handler is not None: if isinstance(handler, CTrait): handler = handler.handler if self.low_name == "": if isinstance(handler._low, CodeType): self.low = eval(handler._low) else: self.low = handler._low if self.high_name == "": if isinstance(handler._low, CodeType): self.high = eval(handler._high) else: self.high = handler._high else: if (self.low is None) and (self.low_name == ""): self.low = 0.0 if (self.high is None) and (self.high_name == ""): self.high = 1.0 def _get_low(self): return self._low def _set_low(self, low): old_low = self._low self._low = low = self._cast(low) if self.is_float is Undefined: self.is_float = isinstance(low, float) if (self.low_label == "") or (self.low_label == str(old_low)): self.low_label = str(low) def _get_high(self): return self._high def _set_high(self, high): old_high = self._high self._high = high = self._cast(high) if self.is_float is Undefined: self.is_float = isinstance(high, float) if (self.high_label == "") or (self.high_label == str(old_high)): self.high_label = str(high) def _cast(self, value): if not isinstance(value, str): return value try: return int(value) except ValueError: return float(value) # -- Private Methods ------------------------------------------------------ def _get_low_high(self, ui): """Returns the low and high values used to determine the initial range.""" low, high = self.low, self.high if (low is None) and (self.low_name != ""): low = self.named_value(self.low_name, ui) if self.is_float is Undefined: self.is_float = isinstance(low, float) if (high is None) and (self.high_name != ""): high = self.named_value(self.high_name, ui) if self.is_float is Undefined: self.is_float = isinstance(high, float) if self.is_float is Undefined: self.is_float = True return (low, high, self.is_float) # ------------------------------------------------------------------------- # Property getters. # ------------------------------------------------------------------------- def _get_format(self): warnings.warn( "Use of format trait is deprecated. Use format_str instead.", DeprecationWarning, stacklevel=2, ) return self.format_str def _set_format(self, format_string): warnings.warn( "Use of format trait is deprecated. Use format_str instead.", DeprecationWarning, stacklevel=2, ) self.format_str = format_string def _get_simple_editor_class(self): """Returns the editor class to use for a simple style. The type of editor depends on the type and extent of the range being edited: * One end of range is unspecified: RangeTextEditor * **mode** is specified and not 'auto': editor corresponding to **mode** * Floating point range with extent > 100: LargeRangeSliderEditor * Integer range or floating point range with extent <= 100: SimpleSliderEditor * All other cases: SimpleSpinEditor """ low, high, is_float = self._low_value, self._high_value, self.is_float if (low is None) or (high is None): return toolkit_object("range_editor:RangeTextEditor") if (not is_float) and (abs(high - low) > 1000000000): return toolkit_object("range_editor:RangeTextEditor") if self.mode != "auto": return toolkit_object("range_editor:SimpleEditorMap")[self.mode] if is_float and (abs(high - low) > 100): return toolkit_object("range_editor:LargeRangeSliderEditor") if is_float or (abs(high - low) <= 100): return toolkit_object("range_editor:SimpleSliderEditor") return toolkit_object("range_editor:SimpleSpinEditor") def _get_custom_editor_class(self): """Creates a custom style of range editor The type of editor depends on the type and extent of the range being edited: * One end of range is unspecified: RangeTextEditor * **mode** is specified and not 'auto': editor corresponding to **mode** * Floating point range: Same as "simple" style * Integer range with extent > 15: Same as "simple" style * Integer range with extent <= 15: CustomEnumEditor """ low, high, is_float = self._low_value, self._high_value, self.is_float if (low is None) or (high is None): return toolkit_object("range_editor:RangeTextEditor") if self.mode != "auto": return toolkit_object("range_editor:CustomEditorMap")[self.mode] if is_float or (abs(high - low) > 15): return self.simple_editor_class return toolkit_object("range_editor:CustomEnumEditor") def _get_text_editor_class(self): """Returns the editor class to use for a text style.""" return toolkit_object("range_editor:RangeTextEditor") # ------------------------------------------------------------------------- # 'Editor' factory methods: # ------------------------------------------------------------------------- def simple_editor(self, ui, object, name, description, parent): """Generates an editor using the "simple" style. Overridden to set the values of the _low_value, _high_value and is_float traits. """ self._low_value, self._high_value, self.is_float = self._get_low_high( ui ) return super().simple_editor(ui, object, name, description, parent) def custom_editor(self, ui, object, name, description, parent): """Generates an editor using the "custom" style. Overridden to set the values of the _low_value, _high_value and is_float traits. """ self._low_value, self._high_value, self.is_float = self._get_low_high( ui ) return super().custom_editor(ui, object, name, description, parent) # This alias is deprecated and will be removed in TraitsUI 8. ToolkitEditorFactory = RangeEditor ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/editors/rgb_color_editor.py0000644000175100001730000000362600000000000023401 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Defines a subclass of the base color editor factory, for colors that are represented as tuples of the form ( *red*, *green*, *blue* ), where *red*, *green* and *blue* are floats in the range from 0.0 to 1.0. """ from traitsui.editors.color_editor import ToolkitEditorFactory as EditorFactory from traitsui.toolkit import toolkit_object # ------------------------------------------------------------------------- # 'ToolkitEditorFactory' class: # ------------------------------------------------------------------------- class ToolkitEditorFactory(EditorFactory): """Factory for editors for RGB colors.""" pass # Define the RGBColorEditor class # The function will try to return the toolkit-specific editor factory (located # in traitsui..rgb_color_editor, and if none is found, the # ToolkitEditorFactory declared here is returned. def RGBColorEditor(*args, **traits): r"""Returns an instance of the toolkit-specific editor factory declared in traitsui..rgb_color_editor. If such an editor factory cannot be located, an instance of the abstract ToolkitEditorFactory declared in traitsui.editors.rgb_color_editor is returned. Parameters ---------- \*args, \*\*traits arguments and keywords to be passed on to the editor factory's constructor. """ try: return toolkit_object("rgb_color_editor:ToolkitEditorFactory", True)( *args, **traits ) except: return ToolkitEditorFactory(*args, **traits) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/editors/scrubber_editor.py0000644000175100001730000000361700000000000023240 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Editor factory for scrubber-based integer or float value editors. """ from pyface.ui_traits import Alignment from traits.api import Float, Property from traitsui.basic_editor_factory import BasicEditorFactory from traitsui.toolkit import toolkit_object from traitsui.toolkit_traits import Color # ------------------------------------------------------------------------- # Create the editor factory object: # ------------------------------------------------------------------------- # Editor factory for scrubber-based integer or float value editors. class ScrubberEditor(BasicEditorFactory): #: The editor class to be instantiated: klass = Property() #: The low end of the scrubber range: low = Float() #: The high end of the scrubber range: high = Float() #: The normal increment (default: auto-calculate): increment = Float() #: The alignment of the text within the scrubber: alignment = Alignment("center") #: The background color for the scrubber: color = Color(None) #: The hover mode background color for the scrubber: hover_color = Color(None) #: The active mode background color for the scrubber: active_color = Color(None) #: The scrubber border color: border_color = Color(None) #: The color to use for the value text: text_color = Color("black") def _get_klass(self): """Returns the toolkit-specific editor class to be instantiated.""" return toolkit_object("scrubber_editor:_ScrubberEditor") ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/editors/search_editor.py0000644000175100001730000000306500000000000022673 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ A single line text widget that supports functionality common to native search widgets. """ from traits.api import Bool, Property, Str from traitsui.basic_editor_factory import BasicEditorFactory from traitsui.toolkit import toolkit_object class SearchEditor(BasicEditorFactory): """A single line text widget that supports functionality common to native search widgets. """ #: The editor class to be created: klass = Property() #: The descriptive text for the widget text = Str("Search") #: Is user input set on every keystroke? auto_set = Bool(True) #: Is user input set when the Enter key is pressed? enter_set = Bool(False) #: Whether to show a search button on the widget search_button = Bool(True) #: Whether to show a cancel button on the widget cancel_button = Bool(False) #: Fire this event on the object whenever a search should be triggered, #: regardless of whether the search term changed search_event_trait = Str() def _get_klass(self): """Returns the toolkit-specific editor class to be instantiated.""" return toolkit_object("search_editor:SearchEditor") ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/editors/set_editor.py0000644000175100001730000000234700000000000022223 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Defines the set editor factory for all traits user interface toolkits. """ from traits.api import Bool, Str from traitsui.editor_factory import EditorWithListFactory class SetEditor(EditorWithListFactory): """Editor factory for editors for sets.""" # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- #: Are the items in the set ordered (vs. unordered)? ordered = Bool(False) #: Can the user add and delete all items in the set at once? can_move_all = Bool(True) #: Title of left column: left_column_title = Str() #: Title of right column: right_column_title = Str() # This alias is deprecated and will be removed in TraitsUI 8. ToolkitEditorFactory = SetEditor ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/editors/shell_editor.py0000644000175100001730000001653000000000000022536 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Editor that displays an interactive Python shell. """ from traits.api import Bool, Str, Event, Property, observe from traits.observation.api import match from traitsui.basic_editor_factory import BasicEditorFactory from traitsui.editor import Editor from traitsui.toolkit import toolkit_object class _ShellEditor(Editor): """Base class for an editor that displays an interactive Python shell.""" #: An event fired to execute a command in the shell. command_to_execute = Event() #: An event fired whenver the user executes a command in the shell: command_executed = Event(Bool) #: Is the shell editor is scrollable? This value overrides the default. scrollable = True # ------------------------------------------------------------------------- # 'Editor' Interface # ------------------------------------------------------------------------- def init(self, parent): """Finishes initializing the editor by creating the underlying toolkit widget. """ # Moving the import here, since PythonShell is implemented in the # Pyface backend packages, and we want to delay loading this toolkit # specific class until this editor is actually used. from pyface.python_shell import PythonShell locals = None self._base_locals = None value = self.value if self.factory.share and isinstance(value, dict): locals = value self._shell = shell = PythonShell(parent) shell.create() self.control = shell.control if locals: for item in locals.items(): shell.bind(*item) if locals is None: object = self.object shell.bind("self", object) shell.observe( self.update_object, "command_executed", dispatch="ui" ) if not isinstance(value, dict): self._any_trait_observer = lambda name, ctrait: True object.observe( self.update_any, match(self._any_trait_observer), dispatch="ui", ) else: self._base_locals = locals = {} for name in self._shell.interpreter().locals.keys(): locals[name] = None # Synchronize any editor events: self.sync_value( self.factory.command_to_execute, "command_to_execute", "from" ) self.sync_value( self.factory.command_executed, "command_executed", "to" ) self.set_tooltip() def update_object(self, event): """Handles the user entering input data in the edit control.""" locals = self._shell.interpreter().locals base_locals = self._base_locals if base_locals is None: object = self.object for name in object.trait_names(): if name in locals: try: setattr(object, name, locals[name]) except: pass else: dic = self.value for name in locals.keys(): if name not in base_locals: try: dic[name] = locals[name] except: pass self.command_executed = True def update_editor(self): """Updates the editor when the object trait changes externally to the editor. """ if self.factory.share: value = self.value if isinstance(value, dict): self._shell.interpreter().locals = value else: locals = self._shell.interpreter().locals base_locals = self._base_locals if base_locals is None: object = self.object for name in object.trait_names(): locals[name] = getattr(object, name, None) else: dic = self.value for name, value in dic.items(): locals[name] = value def update_any(self, event): """Updates the editor when the object trait changes externally to the editor. """ name, new = event.name, event.new locals = self._shell.interpreter().locals if self._base_locals is None: locals[name] = new else: self.value[name] = new def dispose(self): """Disposes of the contents of an editor.""" if not (self.factory.share and isinstance(self.value, dict)): self._shell.observe( self.update_object, "command_executed", remove=True, dispatch="ui", ) if self._base_locals is None: self.object.observe( self.update_any, match(self._any_trait_observer), remove=True, dispatch="ui", ) super().dispose() def restore_prefs(self, prefs): """Restores any saved user preference information associated with the editor. """ shell = self._shell try: history = prefs.get("history", []) history_index = prefs.get("history_index", -1) shell.set_history(history, history_index) except: pass def save_prefs(self): """Returns any user preference information associated with the editor.""" history, history_index = self._shell.get_history() return {"history": history, "history_index": history_index} # ------------------------------------------------------------------------- # Private Interface # ------------------------------------------------------------------------- # Trait change handlers -------------------------------------------------- @observe("command_to_execute") def _execute_command(self, event): """Handles the 'command_to_execute' trait being fired.""" # Show the command. A 'hidden' command should be executed directly on # the namespace trait! command = event.new self._shell.execute_command(command, hidden=False) class ShellEditor(BasicEditorFactory): """Editor factory for shell editors.""" #: The editor class to be instantiated. klass = Property() #: Should the shell interpreter use the object value's dictionary? share = Bool(False) #: Extended trait name of the object event trait which triggers a command #: execution in the shell when fired. command_to_execute = Str() #: Extended trait name of the object event trait which is fired when a #: command is executed. command_executed = Str() def _get_klass(self): """Returns the toolkit-specific editor class to be used in the UI.""" return toolkit_object("shell_editor:_ShellEditor") # This alias is deprecated and will be removed in TraitsUI 8. ToolkitEditorFactory = ShellEditor ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/editors/styled_date_editor.py0000644000175100001730000000267500000000000023735 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! from traits.api import Bool, List, Str from traitsui.editors.date_editor import DateEditor class StyledDateEditor(DateEditor): """A DateEditor that can show sets of dates in different styles.""" #: The name of a dictionary on the object that maps names to groups #: (list/tuples) of datetime.date objects. Each of these groups can be #: styled using the **styles** dict. dates_trait = Str() #: The name of a dictionary on the object that maps names of styles to #: CellFormat objects. The names used must match the names used in the #: **dates** dict. styles_trait = Str() #: Allow selection of arbitrary dates in the past. allow_past = Bool(True) #: Allow selection of arbitrary dates in the future. allow_future = Bool(True) #: A list of strings that will be offered as an alternative to specifying #: an absolute date, and instead specify a relative date. relative_dates = List() # This alias is deprecated and will be removed in TraitsUI 8. ToolkitEditorFactory = StyledDateEditor ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/editors/table_editor.py0000644000175100001730000004557300000000000022527 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Defines the table editor factory for all traits user interface toolkits. """ from traits.api import ( Int, Float, List, Instance, Str, Any, Tuple, Dict, Enum, Bool, Callable, Range, Trait, observe, ) from traitsui.editor_factory import EditorFactory from traitsui.editors.enum_editor import EnumEditor from traitsui.handler import Handler from traitsui.helper import Orientation from traitsui.item import Item from traitsui.table_filter import TableFilter from traitsui.toolkit_traits import Color, Font from traitsui.ui_traits import AView from traitsui.view import View # The filter used to indicate that the user wants to customize the current # filter customize_filter = TableFilter(name="Customize...") # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- # A trait whose value can be True, False, or a callable function BoolOrCallable = Trait(False, Bool, Callable) class TableEditor(EditorFactory): """Editor factory for table editors.""" # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- #: List of initial table column descriptors columns = List(Instance("traitsui.table_column.TableColumn")) #: List of other table column descriptors (not initially displayed) other_columns = List(Instance("traitsui.table_column.TableColumn")) #: The object trait containing the list of column descriptors columns_name = Str() #: The desired number of visible rows in the table rows = Int() #: The optional extended name of the trait used to specify an external #: filter for the table data. The value of the trait must either be an #: instance of TableEditor, a callable that accepts one argument #: (a table row) and returns True or False to indicate whether the #: specified object passes the filter or not, or **None** to indicate that #: no filter is to be applied: filter_name = Str() #: Initial filter that should be applied to the table filter = Instance("traitsui.table_filter.TableFilter") #: List of available filters that can be applied to the table filters = List(Instance("traitsui.table_filter.TableFilter")) #: The optional extended trait name of the trait used to notify that the #: filter has changed and the displayed objects should be updated. #: It should be an Event. update_filter_name = Str() #: Filter object used to allow a user to search the table. #: NOTE: If left as None, the table will not be searchable. search = Instance("traitsui.table_filter.TableFilter") #: Default context menu to display when any cell is right-clicked menu = Instance("traitsui.menu.Menu") #: Default trait name containg menu menu_name = Str() #: Are objects deletable from the table? deletable = BoolOrCallable(False) #: Is the table editable? editable = Bool(True) #: Should the editor become active after the first click edit_on_first_click = Bool(True) #: Can the user reorder the items in the table? reorderable = Bool(False) #: Can the user configure the table columns? configurable = Bool(True) #: Should the cells of the table automatically size to the optimal size? auto_size = Bool(True) #: Mirrors the Qt QSizePolicy.Policy attribute, for horizontal and vertical #: dimensions. For these to be useful, set auto_size to False. If these #: are None, then the table size policy will not be set in that dimension #: (for backwards compatibility). h_size_policy = Enum( None, "preferred", "fixed", "minimum", "maximum", "expanding", "minimum_expanding", "ignored", ) v_size_policy = Enum( None, "preferred", "fixed", "minimum", "maximum", "expanding", "minimum_expanding", "ignored", ) #: Should a new row automatically be added to the end of the table to allow #: the user to create new entries? If True, **row_factory** must be set. auto_add = Bool(False) #: Should the table items be presented in reverse order? reverse = Bool(False) #: The DockWindow graphical theme: dock_theme = Any() #: View to use when editing table items. #: NOTE: If not specified, the table items are not editable in a separate #: pane of the editor. edit_view = AView(" ") #: The handler to apply to **edit_view** edit_view_handler = Instance(Handler) #: Width to use for the edit view edit_view_width = Float(-1.0) #: Height to use for the edit view edit_view_height = Float(-1.0) #: Layout orientation of the table and its associated editor pane. This #: attribute applies only if **edit_view** is not ' '. orientation = Orientation #: Is the table sortable by clicking on the column headers? sortable = Bool(True) #: Does sorting affect the model (vs. just the view)? sort_model = Bool(False) #: Should grid lines be shown on the table? show_lines = Bool(True) #: Should the toolbar be displayed? (Note that False will override settings #: such as 'configurable', etc., and is a quick way to prevent the toolbar #: from being displayed; but True will not cause a toolbar to appear if one #: would not otherwise have been displayed) show_toolbar = Bool(False) #: The vertical scroll increment for the table: scroll_dy = Range(1, 32) #: Grid line color. Does not work on Qt. line_color = Color(0xC4C0A9) #: Show column labels? show_column_labels = Bool(True) #: Show row labels? show_row_labels = Bool(False) #: Font to use for text in cells cell_font = Font #: Color to use for text in cells cell_color = Color(default=None, allow_none=True) #: Color to use for cell backgrounds cell_bg_color = Color(default=None, allow_none=True) #: Color to use for read-only cell backgrounds cell_read_only_bg_color = Color(default=None, allow_none=True) #: Whether to even-odd alternate the background color. Qt only. alternate_bg_color = Bool(False) #: Font to use for text in labels label_font = Font #: Color to use for text in labels label_color = Color(default=None, allow_none=True) #: Color to use for label backgrounds. Some Qt styles (eg. MacOS) ignore #: this. label_bg_color = Color(default=None, allow_none=True) #: Background color of selected item. selection_bg_color = Color(default=None, allow_none=True) #: Color of selected text. selection_color = Color(default=None, allow_none=True) #: Height (in pixels) of column labels column_label_height = Int(25) #: Width (in pixels) of row labels row_label_width = Int(82) #: The initial height of each row (<= 0 means use default value): row_height = Int(0) #: The optional extended name of the trait that the indices of the items #: currently passing the table filter are synced with: filtered_indices = Str() #: The selection mode of the table. The meaning of the various values are #: as follows: #: #: row #: Entire rows are selected. At most one row can be selected at once. #: This is the default. #: rows #: Entire rows are selected. More than one row can be selected at once. #: column #: Entire columns are selected. At most one column can be selected at #: once. #: columns #: Entire columns are selected. More than one column can be selected at #: once. #: cell #: Single cells are selected. Only one cell can be selected at once. #: cells #: Single cells are selected. More than one cell can be selected at once. selection_mode = Enum("row", "rows", "column", "columns", "cell", "cells") #: The optional extended name of the trait that the current selection is #: synced with: selected = Str() #: The optional extended trait name of the trait that the indices of the #: current selection are synced with: selected_indices = Str() #: The optional extended trait name of the trait that should be assigned #: an ( object, column ) tuple when a table cell is clicked on (Note: If #: you want to receive repeated clicks on the same cell, make sure the #: trait is defined as an Event): click = Str() #: The optional extended trait name of the trait that should be assigned #: an ( object, column ) tuple when a table cell is double-clicked on #: (Note: if you want to receive repeated double-clicks on the same cell, #: make sure the trait is defined as an Event): dclick = Str() #: Called when a table item is selected on_select = Any() #: Called when a table item is double clicked on_dclick = Any() #: A factory to generate new rows. #: NOTE: If None, then the user will not be able to add new rows to the #: table. If not None, then it must be a callable that accepts #: **row_factory_args** and **row_factory_kw** and returns a new object #: that can be added to the table. row_factory = Any() #: Arguments to pass to the **row_factory** callable when a new row is #: created row_factory_args = Tuple() #: Keyword arguments to pass to the **row_factory** callable when a new row #: is created row_factory_kw = Dict() #: Hooks for replacing parts of the implementation. table_view_factory = Callable() source_model_factory = Callable() model_factory = Callable() # ------------------------------------------------------------------------- # Traits view definitions: # ------------------------------------------------------------------------- traits_view = View( [ "{Initial columns}@", Item("columns", resizable=True), "{Other columns}@", Item("other_columns", resizable=True), "|{Columns}<>", ], [ [ "deletable{Are items deletable?}", "9", "editable{Are items editable?}", "9", "-[Item Options]>", ], [ "show_column_labels{Show column labels?}", "9", "configurable{Are columns user configurable?}", "9", "auto_size{Should columns auto size?}", "-[Column Options]>", ], [ "sortable{Are columns sortable?}", Item( "sort_model{Does sorting affect the model?}", enabled_when="sortable", ), "-[Sorting Options]>", ], [ ["show_lines{Show grid lines?}", "|>"], ["_", "line_color{Grid line color}@", "|<>"], "|[Grid Line Options]", ], "|{Options}", ], [ [ "cell_color{Text color}@", "cell_bg_color{Background color}@", "cell_read_only_bg_color{Read only color}@", "|[Cell Colors]", ], ["cell_font", "|[Cell Font]<>"], "|{Cell}", ], [ [ "label_color{Text color}@", "label_bg_color{Background color}@", "|[Label Colors]", ], ["label_font@", "|[Label Font]<>"], "|{Label}", ], [ [ "selection_color{Text color}@", "selection_bg_color{Background color}@", "|[Selection Colors]", ], "|{Selection}", ], height=0.5, ) # ------------------------------------------------------------------------- # 'Editor' factory methods: # ------------------------------------------------------------------------- def readonly_editor(self, ui, object, name, description, parent): """Generates an "editor" that is read-only. Overridden to set the value of the editable trait to False before generating the editor. """ self.editable = False return super().readonly_editor(ui, object, name, description, parent) # ------------------------------------------------------------------------- # Event handlers: # ------------------------------------------------------------------------- @observe("filters.items") def _update_filter_editor(self, event): """Handles the set of filters associated with the editor's factory being changed. """ values = {None: "000:No filter"} i = 0 for filter in self.filters: if not filter.template: i += 1 values[filter] = "%03d:%s" % (i, filter.name) values[customize_filter] = "%03d:%s" % ((i + 1), customize_filter.name) if self._filter_editor is None: self._filter_editor = EnumEditor(values=values) else: self._filter_editor.values = values # This alias is deprecated and will be removed in TraitsUI 8. ToolkitEditorFactory = TableEditor # ------------------------------------------------------------------------- # Base class for toolkit-specific editors # ------------------------------------------------------------------------- class BaseTableEditor(object): """Base class for toolkit-specific editors.""" # ------------------------------------------------------------------------- # Interface for toolkit-specific editors: # ------------------------------------------------------------------------- def set_menu_context(self, selection, object, column): """Call before creating a context menu for a cell, then set self as the controller for the menu. """ self._menu_context = { "selection": selection, "object": object, "column": column, "editor": self, "info": self.ui.info, "handler": self.ui.handler, } # ------------------------------------------------------------------------- # pyface.action 'controller' interface implementation: # ------------------------------------------------------------------------- def add_to_menu(self, menu_item): """Adds a menu item to the menu bar being constructed.""" action = menu_item.item.action self.eval_when(action.enabled_when, menu_item, "enabled") self.eval_when(action.checked_when, menu_item, "checked") def add_to_toolbar(self, toolbar_item): """Adds a toolbar item to the toolbar being constructed.""" self.add_to_menu(toolbar_item) def can_add_to_menu(self, action): """Returns whether the action should be defined in the user interface.""" if action.defined_when != "": if not eval(action.defined_when, globals(), self._menu_context): return False if action.visible_when != "": if not eval(action.visible_when, globals(), self._menu_context): return False return True def can_add_to_toolbar(self, action): """Returns whether the toolbar action should be defined in the user interface. """ return self.can_add_to_menu(action) def perform(self, action, action_event=None): """Performs the action described by a specified Action object.""" self.ui.do_undoable(self._perform, action) def _perform(self, action): method_name = action.action info = self.ui.info handler = self.ui.handler context = self._menu_context self._menu_context = None selection = context["selection"] if method_name.find(".") >= 0: if method_name.find("(") < 0: method_name += "()" try: eval(method_name, globals(), context) except: # fixme: Should the exception be logged somewhere? pass return method = getattr(handler, method_name, None) if method is not None: method(info, selection) return if action.on_perform is not None: action.on_perform(selection) return action.perform(selection) # ------------------------------------------------------------------------- # Menu support methods: # ------------------------------------------------------------------------- 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 = bool(eval(condition, globals(), self._menu_context)) setattr(object, trait, value) # ------------------------------------------------------------------------- # Helper class for toolkit-specific editors to implement 'reversed' option: # ------------------------------------------------------------------------- class ReversedList(object): """A list whose order is the reverse of its input.""" def __init__(self, list): self.list = list def insert(self, index, value): """Inserts a value at a specified index in the list.""" return self.list.insert(self._index(index - 1), value) def index(self, value): """Returns the index of the first occurrence of the specified value in the list. """ list = self.list[:] list.reverse() return list.index(value) def __len__(self): """Returns the length of the list.""" return len(self.list) def __getitem__(self, index): """Returns the value at a specified index in the list.""" return self.list[self._index(index)] def __setslice__(self, i, j, values): """Sets a slice of a list to the contents of a specified sequence.""" return self.list.__setslice__(self._index(i), self._index(j), values) def __delitem__(self, index): """Deletes the item at a specified index.""" return self.list.__delitem__(self._index(index)) def _index(self, index): """Returns the "reversed" value for a specified index.""" if index < 0: return -1 - index result = len(self.list) - index - 1 if result >= 0: return result return index ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/editors/tabular_editor.py0000644000175100001730000001374100000000000023062 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ A traits UI editor for editing tabular data (arrays, list of tuples, lists of objects, etc). """ import warnings from pyface.ui_traits import Image from traits.api import Str, Bool, Property, List, Enum, Instance from traitsui.basic_editor_factory import BasicEditorFactory from traitsui.toolkit import toolkit_object class TabularEditor(BasicEditorFactory): """Editor factory for tabular editors.""" # -- Trait Definitions ---------------------------------------------------- #: The editor class to be created: klass = Property() #: Should column headers (i.e. titles) be displayed? show_titles = Bool(True) #: Should row headers be displayed (Qt4 only)? show_row_titles = Bool(False) #: The optional extended name of the trait used to indicate that a complete #: table update is needed: update = Str() #: The optional extended name of the trait used to indicate that the table #: just needs to be repainted. refresh = Str() #: Should the table update automatically when the table item's contents #: change? Note that in order for this feature to work correctly, the #: editor trait should be a list of objects derived from HasTraits. Also, #: performance can be affected when very long lists are used, since #: enabling this feature adds and removed Traits listeners to each item in #: the list. auto_update = Bool(False) #: The optional extended name of the trait to synchronize the selection #: values with: selected = Str() #: The optional extended name of the trait to synchronize the selection #: rows with: selected_row = Str() #: Whether or not to allow selection. selectable = Bool(True) #: The optional extended name of the trait to synchronize the activated #: value with: activated = Str() #: The optional extended name of the trait to synchronize the activated #: value's row with: activated_row = Str() #: The optional extended name of the trait to synchronize left click data #: with. The data is a TabularEditorEvent: clicked = Str() #: The optional extended name of the trait to synchronize left double click #: data with. The data is a TabularEditorEvent: dclicked = Str() #: The optional extended name of the trait to synchronize right click data #: with. The data is a TabularEditorEvent: right_clicked = Str() #: The optional extended name of the trait to synchronize right double #: clicked data with. The data is a TabularEditorEvent: right_dclicked = Str() #: The optional extended name of the trait to synchronize column #: clicked data with. The data is a TabularEditorEvent: column_clicked = Str() #: The optional extended name of the trait to synchronize column #: right clicked data with. The data is a TabularEditorEvent: column_right_clicked = Str() #: The optional extended name of the Event trait that should be used to #: trigger a scroll-to command. The data is an integer giving the row. scroll_to_row = Str() #: The optional extended name of the Event trait that should be used to #: trigger a scroll-to command. The data is an integer giving the column. scroll_to_column = Str() #: Deprecated: Controls behavior of scroll to row and scroll to column scroll_to_row_hint = Property(Str, observe="scroll_to_position_hint") #: (replacement of scroll_to_row_hint, but more clearly named) #: Controls behavior of scroll to row and scroll to column scroll_to_position_hint = Enum("visible", "center", "top", "bottom") #: Can the user edit the values? editable = Bool(True) #: Can the user edit the labels (i.e. the first column) editable_labels = Bool(False) #: Are multiple selected items allowed? multi_select = Bool(False) #: Should horizontal lines be drawn between items? horizontal_lines = Bool(True) #: Should vertical lines be drawn between items? vertical_lines = Bool(True) #: Should the columns automatically resize? Don't allow this when the #: amount of data is large. auto_resize = Bool(False) #: Should the rows automatically resize (Qt4 only)? Don't allow #: this when the amount of data is large. auto_resize_rows = Bool(False) #: Whether to stretch the last column to fit the available space. stretch_last_section = Bool(True) #: The adapter from trait values to editor values: adapter = Instance("traitsui.tabular_adapter.TabularAdapter", ()) #: What type of operations are allowed on the list: operations = List( Enum("delete", "insert", "append", "edit", "move"), ["delete", "insert", "append", "edit", "move"], ) #: Are 'drag_move' operations allowed (i.e. True), or should they always be #: treated as 'drag_copy' operations (i.e. False): drag_move = Bool(True) #: The set of images that can be used: images = List(Image) def _get_klass(self): """Returns the toolkit-specific editor class to be instantiated.""" return toolkit_object("tabular_editor:TabularEditor") def _get_scroll_to_row_hint(self): warnings.warn( "Use of scroll_to_row_hint trait is deprecated. " "Use scroll_to_position_hint instead.", DeprecationWarning, ) return self.scroll_to_position_hint def _set_scroll_to_row_hint(self, hint): warnings.warn( "Use of scroll_to_row_hint trait is deprecated. " "Use scroll_to_position_hint instead.", DeprecationWarning, ) self.scroll_to_position_hint = hint ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/editors/text_editor.py0000644000175100001730000000705000000000000022410 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Defines the text editor factory for all traits toolkit backends. """ from traits.api import Dict, Str, Any, Bool from traitsui.editor_factory import EditorFactory from traitsui.group import Group from traitsui.ui_traits import AView from traitsui.view import View # ------------------------------------------------------------------------- # Define a simple identity mapping: # ------------------------------------------------------------------------- class _Identity(object): """A simple identity mapping.""" def __call__(self, value): return value # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- #: Mapping from user input text to other value mapping_trait = Dict(Str, Any) #: Function used to evaluate textual user input evaluate_trait = Any(_Identity()) class TextEditor(EditorFactory): """Editor factory for text editors.""" # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- #: Dictionary that maps user input to other values mapping = mapping_trait #: Is user input set on every keystroke? auto_set = Bool(True) #: Is user input set when the Enter key is pressed? enter_set = Bool(False) #: Is multi-line text allowed? multi_line = Bool(True) #: Is editor readonly (will use custom / default editor appearance with #: readonly flag set to true) in contrast with readonly style for item #: when completely another editor is used read_only = Bool(False) #: Is user input unreadable? (e.g., for a password) password = Bool(False) #: Function to evaluate textual user input evaluate = evaluate_trait #: The object trait containing the function used to evaluate user input evaluate_name = Str() #: The optional view to display when a read-only text editor is clicked: view = AView #: In a read-only text editor, allow selection and copying of the text. readonly_allow_selection = Bool(True) #: Grayed-out placeholder text to be displayed when the editor is empty. placeholder = Str() #: Whether or not to display a clear button for the text. This only works #: in the qt>=5.2 backend for simple/text styles of the editor. Note this #: trait is currently provisional and may be replaced in the future by a #: more general feature. cancel_button = Bool(False) # ------------------------------------------------------------------------- # Traits view definition: # ------------------------------------------------------------------------- traits_view = View( [ "auto_set{Set value when text is typed}", "enter_set{Set value when enter is pressed}", "multi_line{Allow multiple lines of text}", "", "|options:[Options]>", ] ) extras = Group("password{Is this a password field?}") # This alias is deprecated and will be removed in TraitsUI 8. ToolkitEditorFactory = TextEditor ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/editors/time_editor.py0000644000175100001730000000230100000000000022354 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ A Traits UI editor that wraps a timer control. """ from traits.api import Str from traitsui.editor_factory import EditorFactory from traitsui.ui_traits import AView class TimeEditor(EditorFactory): """Editor factory for time editors.""" # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- # -- ReadonlyEditor traits ------------------------------------------------ #: Message to show when Time is None. message = Str("Undefined") #: The string representation of the time to show. Uses time.strftime #: format. strftime = Str("%I:%M:%S %p") #: An optional view to display when a read-only text editor is clicked: view = AView ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/editors/title_editor.py0000644000175100001730000000243200000000000022544 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Defines the title editor factory for all traits toolkit backends. """ from traits.api import Bool from traitsui.editor_factory import EditorFactory from traitsui.toolkit import toolkit_object class TitleEditor(EditorFactory): """Editor factory for Title editors.""" allow_selection = Bool(False) def _get_simple_editor_class(self): """Returns the editor class to use for "simple" style views. The default implementation tries to import the SimpleEditor class in the editor file in the backend package, and if such a class is not to found it returns the SimpleEditor class defined in editor_factory module in the backend package. """ SimpleEditor = toolkit_object("title_editor:SimpleEditor") return SimpleEditor # This alias is deprecated and will be removed in TraitsUI 8. ToolkitEditorFactory = TitleEditor ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/editors/tree_editor.py0000644000175100001730000001370100000000000022363 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Defines the tree editor factory for all traits user interface toolkits. """ from traits.api import Any, Dict, Bool, Tuple, Int, List, Instance, Str, Enum from traitsui.dock_window_theme import DockWindowTheme from traitsui.editor_factory import EditorFactory from traitsui.helper import Orientation from traitsui.menu import Action from traitsui.tree_node import TreeNode # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- #: Size of each tree node icon IconSize = Tuple((16, 16), Int, Int) # ------------------------------------------------------------------------- # The core tree node menu actions: # ------------------------------------------------------------------------- NewAction = "NewAction" CopyAction = Action( name="Copy", action="editor._menu_copy_node", enabled_when="editor._is_copyable(object)", ) CutAction = Action( name="Cut", action="editor._menu_cut_node", enabled_when="editor._is_cutable(object)", ) PasteAction = Action( name="Paste", action="editor._menu_paste_node", enabled_when="editor._is_pasteable(object)", ) DeleteAction = Action( name="Delete", action="editor._menu_delete_node", enabled_when="editor._is_deletable(object)", ) RenameAction = Action( name="Rename", action="editor._menu_rename_node", enabled_when="editor._is_renameable(object)", ) class TreeEditor(EditorFactory): """Editor factory for tree editors.""" # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- #: Supported TreeNode objects nodes = List(TreeNode) #: Mapping from TreeNode tuples to MultiTreeNodes multi_nodes = Dict() #: The column header labels if any. column_headers = List(Str) #: Are the individual nodes editable? editable = Bool(True) #: Selection mode. selection_mode = Enum("single", "extended") #: Is the editor shared across trees? shared_editor = Bool(False) #: Reference to a shared object editor editor = Instance(EditorFactory) # FIXME: Implemented only in wx backend. #: The DockWindow graphical theme dock_theme = Instance(DockWindowTheme) #: Show icons for tree nodes? show_icons = Bool(True) #: Hide the tree root node? hide_root = Bool(False) #: Layout orientation of the tree and the editor orientation = Orientation #: Number of tree levels (down from the root) that should be automatically #: opened auto_open = Int() #: Size of the tree node icons icon_size = IconSize #: Called when a node is selected on_select = Any() #: Called when a node is clicked on_click = Any() #: Called when a node is double-clicked on_dclick = Any() #: Called when a node is activated on_activated = Any() #: Call when the mouse hovers over a node on_hover = Any() #: The optional extended trait name of the trait to synchronize with the #: editor's current selection: selected = Str() #: The optional extended trait name of the trait that should be assigned #: a node object when a tree node is activated, by double-clicking or #: pressing the Enter key when a node has focus (Note: if you want to #: receive repeated activated events on the same node, make sure the trait #: is defined as an Event): activated = Str() #: The optional extended trait name of the trait that should be assigned #: a node object when a tree node is clicked on (Note: If you want to #: receive repeated clicks on the same node, make sure the trait is defined #: as an Event): click = Str() #: The optional extended trait name of the trait that should be assigned #: a node object when a tree node is double-clicked on (Note: if you want #: to receive repeated double-clicks on the same node, make sure the trait #: is defined as an Event): dclick = Str() #: The optional extended trait name of the trait event that is fired #: whenever the application wishes to veto a tree action in progress (e.g. #: double-clicking a non-leaf tree node normally opens or closes the node, #: but if you are handling the double-click event in your program, you may #: wish to veto the open or close operation). Be sure to fire the veto #: event in the event handler triggered by the operation (e.g. the 'dclick' #: event handler. veto = Str() #: The optional extended trait name of the trait event that is fired when #: the application wishes the currently visible portion of the tree widget #: to repaint itself. refresh = Str() #: Mode for lines connecting tree nodes #: #: * 'appearance': Show lines only when they look good. #: * 'on': Always show lines. #: * 'off': Don't show lines. lines_mode = Enum("appearance", "on", "off") # FIXME: Document as unimplemented or wx specific. #: Whether to alternate row colors or not. alternating_row_colors = Bool(False) #: Any extra vertical padding to add. vertical_padding = Int(0) #: Whether or not to expand on a double-click. expands_on_dclick = Bool(True) #: Whether the labels should be wrapped around, if not an ellipsis is shown #: This works only in the qt backend and if there is only one column in #: tree word_wrap = Bool(False) # This alias is deprecated and will be removed in TraitsUI 8. ToolkitEditorFactory = TreeEditor ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/editors/tuple_editor.py0000644000175100001730000001337000000000000022557 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Defines the tuple editor factory for all traits user interface toolkits. """ from traits.api import ( BaseTuple, Bool, HasTraits, List, Str, Int, Any, TraitType, ) from traits.trait_base import SequenceTypes from traitsui.editor import Editor from traitsui.editor_factory import EditorFactory from traitsui.group import Group from traitsui.item import Item from traitsui.view import View class TupleEditor(EditorFactory): """Editor factory for tuple editors.""" # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- #: Trait definitions for each tuple field types = Any() #: Labels for each of the tuple fields labels = List(Str) #: Editors for each of the tuple fields: editors = List(EditorFactory) #: Number of tuple fields or rows cols = Int(1) #: Is user input set on every keystroke? This is applied to every field #: of the tuple, provided the field does not already have an 'auto_set' #: metadata or an editor defined. auto_set = Bool(True) #: Is user input set when the Enter key is pressed? This is applied to #: every field of the tuple, provided the field does not already have an #: 'enter_set' metadata or an editor defined. enter_set = Bool(False) class SimpleEditor(Editor): """Simple style of editor for tuples. The editor displays an editor for each of the fields in the tuple, based on the type of each field. """ def init(self, parent): """Finishes initializing the editor by creating the underlying toolkit widget. """ self._ts = ts = TupleStructure(self) self._ui = ui = ts.view.ui(ts, parent, kind="subpanel").trait_set( parent=self.ui ) self.control = ui.control self.set_tooltip() def update_editor(self): """Updates the editor when the object trait changes external to the editor. """ ts = self._ts for i, value in enumerate(self.value): setattr(ts, "f%d" % i, value) def get_error_control(self): """Returns the editor's control for indicating error status.""" return self._ui.get_error_controls() class TupleStructure(HasTraits): """Creates a view containing items for each field in a tuple.""" # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- #: Editor this structure is linked to editor = Any() #: The constructed View for the tuple view = Any() #: Number of tuple fields fields = Int() def __init__(self, editor): """Initializes the object.""" super().__init__(editor=editor) factory = editor.factory types = factory.types labels = factory.labels editors = factory.editors cols = factory.cols # Get the tuple we are mirroring: object = editor.value # For each tuple field, add a trait with the appropriate trait # definition and default value: content = [] self.fields = len(object) len_labels = len(labels) len_editors = len(editors) if types is None: type = editor.value_trait.handler if isinstance(type, BaseTuple): types = type.types if not isinstance(types, SequenceTypes): types = [types] len_types = len(types) if len_types == 0: types = [Any] len_types = 1 for i, value in enumerate(object): type = types[i % len_types] auto_set = enter_set = None if isinstance(type, TraitType): auto_set = type.auto_set enter_set = type.enter_set if auto_set is None: auto_set = editor.factory.auto_set if enter_set is None: enter_set = editor.factory.enter_set label = "" if i < len_labels: label = labels[i] field_editor = None if i < len_editors: field_editor = editors[i] name = "f%d" % i self.add_trait( name, type( value, event="field", auto_set=auto_set, enter_set=enter_set, ), ) item = Item(name=name, label=label, editor=field_editor) if cols <= 1: content.append(item) else: if (i % cols) == 0: group = Group(orientation="horizontal") content.append(group) group.content.append(item) self.view = View(Group(show_labels=(len_labels != 0), *content)) def _field_changed(self, name, old, new): """Updates the underlying tuple when any field changes value.""" index = int(name[1:]) value = self.editor.value if new != value[index]: self.editor.value = tuple( [getattr(self, "f%d" % i) for i in range(self.fields)] ) # This alias is deprecated and will be removed in TraitsUI 8. ToolkitEditorFactory = TupleEditor ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/editors/value_editor.py0000644000175100001730000000573500000000000022550 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Defines the tree-based Python value editor and the value editor factory. """ from traits.api import Bool, Instance, Int from traitsui.editor import Editor from traitsui.editor_factory import EditorFactory from traitsui.editors.tree_editor import TreeEditor from traitsui.item import Item from traitsui.value_tree import RootNode, value_tree_nodes from traitsui.view import View # ------------------------------------------------------------------------- # 'SimpleEditor' class: # ------------------------------------------------------------------------- class _ValueEditor(Editor): """Simple style of editor for values, which displays a tree.""" # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- #: Is the editor read only? readonly = Bool(False) #: The root node of the value tree root = Instance(RootNode) #: Is the value editor scrollable? This values overrides the default. scrollable = True def init(self, parent): """Finishes initializing the editor by creating the underlying toolkit widget. """ self.update_editor() editor = TreeEditor( auto_open=self.factory.auto_open, hide_root=True, editable=False, nodes=value_tree_nodes, ) self._ui = self.edit_traits( parent=parent, view=View( Item("root", show_label=False, editor=editor), kind="subpanel" ), ) self._ui.parent = self.ui self.control = self._ui.control def update_editor(self): """Updates the editor when the object trait changes external to the editor. """ self.root = RootNode(name="", value=self.value, readonly=self.readonly) def dispose(self): """Disposes of the contents of an editor.""" self._ui.dispose() super().dispose() def get_error_control(self): """Returns the editor's control for indicating error status.""" return self._ui.get_error_controls() class ValueEditor(EditorFactory): """Editor factory for tree-based value editors.""" # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- #: Number of tree levels to automatically open auto_open = Int(2) # This alias is deprecated and will be removed in TraitsUI 8. ToolkitEditorFactory = ValueEditor ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/editors/video_editor.py0000644000175100001730000000656600000000000022545 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """Traits UI 'display only' video editor.""" from traits.api import Bool, Callable, Enum, Float, Instance, Property, Range from traitsui.context_value import ContextValue, CVType from traitsui.basic_editor_factory import BasicEditorFactory from traitsui.toolkit import toolkit_object AspectRatio = Enum('keep', 'ignore', 'expand') PlayerState = Enum('stopped', 'playing', 'paused') MediaStatus = Enum( 'unknown', 'no_media', 'loading', 'loaded', 'stalled', 'buffering', 'buffered', 'end', 'invalid', ) class VideoEditor(BasicEditorFactory): """Traits UI 'display only' video editor. This editor only displays the video stream, and does not attempt to provide UI elements for controlling playback. It does provide a rich set of trait references that can be synchronised with the internal state of the video player. """ #: The editor class to be created: klass = Property() #: The behaviour of the video display when the widget aspect ratio #: doesn't match the aspect ratio of the video stream. aspect_ratio = AspectRatio() #: True if the audio is muted, False otherwise muted = CVType(Bool, default_value=False, sync_value='from') #: Audio volume on a logarithmic scale volume = CVType(Range(0.0, 100.0), default_value=75.0, sync_value='from') #: The playback speed of the video. Negative values are allowed but may not #: be supported by the underlying implementation. playback_rate = CVType(Float, default_value=1.0, sync_value='from') #: The state (stopped, playing, paused) of the player state = CVType(PlayerState, default_value='stopped', sync_value='both') #: The current position, in seconds, in the video. position = CVType(Float, default_value=0.0, sync_value='both') #: Duration of the loaded video in seconds duration = Instance( ContextValue, args=('',), allow_none=False, sync_value='to' ) #: The status of the loaded video (see ``MediaStatus``) media_status = Instance( ContextValue, args=('',), allow_none=False, sync_value='to' ) #: An integer percentage representing how much of the player's buffer #: is filled. buffer = Instance( ContextValue, args=('',), allow_none=False, sync_value='to' ) #: A string describing an error encountered by the player video_error = Instance( ContextValue, args=('',), allow_none=False, sync_value='to' ) #: Callable to apply to video frames. Takes ref to new frame and a size #: tuple. Must return a QImage and a numpy array. image_func = CVType(Callable, sync_value='from') #: The name of a trait to synchronise with the player's notify interval. #: The referenced trait should be a Float representing time in seconds. notify_interval = CVType(Float, default_value=1.0, sync_value='from') def _get_klass(self): """Returns the editor class to be instantiated.""" return toolkit_object('video_editor:VideoEditor') ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1684768027.9998076 traitsui-8.0.0/traitsui/examples/0000755000175100001730000000000000000000000017647 5ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1684768028.067807 traitsui-8.0.0/traitsui/examples/demo/0000755000175100001730000000000000000000000020573 5ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1684768028.075807 traitsui-8.0.0/traitsui/examples/demo/Advanced/0000755000175100001730000000000000000000000022300 5ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/examples/demo/Advanced/Adapted_tree_editor_demo.py0000644000175100001730000001030600000000000027605 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Demonstrates an alternative method of defining a **TreeEditor** by creating **ITreeNodeAdapter** subclasses. To run this demonstration successfully, you must have **AppTools** (``apptools``) installed. Using **ITreeNodeAdapters** can be useful in cases where the kind of content of the tree is not always known ahead of time. For example, you might be creating a reusable tool or component which can display its data in a tree view, but you do not know what kind of data it will be asked to display when you write the code. Therefore, it may be impossible for you to specify a **TreeEditor** with a correct set of **TreeNode** objects that will work in all possible future cases. Using **ITreeNodeAdapter** subclasses, you can allow the clients of your code to solve this problem by providing one of more **ITreeNodeAdapters** that can be used to provide the correct tree node information for each type of data that will appear in the **TreeEditor** view. In this demo, we define an **ITreeNodeAdapter** subclass that adapts the *apptools.io.file.File* class to be displayed in a file explorer style tree view. """ # -- Imports -------------------------------------------------------------- from os import getcwd from traits.api import ( HasTraits, Property, Directory, register_factory, cached_property ) from traitsui.api import ( View, VGroup, Item, TreeEditor, ITreeNode, ITreeNodeAdapter, ) from apptools.io.api import File # -- FileAdapter Class ---------------------------------------------------- class FileAdapter(ITreeNodeAdapter): # -- ITreeNodeAdapter Method Overrides ------------------------------------ def allows_children(self): """Returns whether this object can have children.""" return self.adaptee.is_folder def has_children(self): """Returns whether the object has children.""" children = self.adaptee.children return (children is not None) and (len(children) > 0) def get_children(self): """Gets the object's children.""" return self.adaptee.children def get_label(self): """Gets the label to display for a specified object.""" return self.adaptee.name + self.adaptee.ext def get_tooltip(self): """Gets the tooltip to display for a specified object.""" return self.adaptee.absolute_path def get_icon(self, is_expanded): """Returns the icon for a specified object.""" if self.adaptee.is_file: return '' if is_expanded: return '' return '' def can_auto_close(self): """Returns whether the object's children should be automatically closed. """ return True # -- FileTreeDemo Class --------------------------------------------------- class FileTreeDemo(HasTraits): # The path to the file tree root: root_path = Directory(entries=10) # The root of the file tree: root = Property(observe='root_path') # The traits view to display: view = View( VGroup( Item('root_path'), Item('root', editor=TreeEditor(editable=False, auto_open=1)), show_labels=False, ), width=0.33, height=0.50, resizable=True, ) # -- Traits Default Value Methods ----------------------------------------- def _root_path_default(self): return getcwd() # -- Property Implementations --------------------------------------------- @cached_property def _get_root(self): return File(path=self.root_path) # -- Create and run the demo ---------------------------------------------- register_factory(FileAdapter, File, ITreeNode) demo = FileTreeDemo() # Run the demo (if invoked form the command line): if __name__ == '__main__': demo.configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/examples/demo/Advanced/Apply_Revert_handler_demo.py0000644000175100001730000000513400000000000027772 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Apply / Revert Provides support in a dialog box for an "Apply" button which modifies the object being viewed, and a "Revert" button, which returns the object to its starting state (before any "Apply"). Note that this does not automatically provide a full (multi-step incremental) Undo capability. """ from traits.api import HasTraits, Str, List from traitsui.api import Item, View, Handler, HGroup, VGroup, TextEditor class ApplyRevert_Handler(Handler): def apply(self, info): print('apply called...') object = info.object object.stack.insert(0, object.input) object.queue.append(object.input) def revert(self, info): # Do something exciting here... print('revert called...') class ApplyRevertDemo(HasTraits): # Trait definitions: input = Str() stack = List() queue = List() # Traits view definitions: traits_view = View( VGroup( VGroup( Item( 'input', show_label=False, editor=TextEditor(auto_set=True) ), label='Input', show_border=True, ), HGroup( VGroup( Item( 'stack', show_label=False, height=50, width=100, style='readonly', ), label='Stack', show_border=True, ), VGroup( Item( 'queue', show_label=False, height=50, width=100, style='readonly', ), label='Queue', show_border=True, ), ), ), resizable=True, height=300, title='Apply/Revert example', buttons=['Apply', 'Revert'], handler=ApplyRevert_Handler, ) # Create the demo: modal_popup = ApplyRevertDemo() # Run the demo (if invoked from the command line): if __name__ == '__main__': modal_popup.configure_traits(kind='modal') ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/examples/demo/Advanced/Auto_editable_readonly_table_cells.py0000644000175100001730000001151200000000000031641 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ This example shows how to define a read-only, auto-edit table column using a custom pop-up view. The example displays a list of integer values from 1 to n, where 'n' can be set using the slider at the top of the view. Each entry in the list shows the value of the integer and the number of unique factors it has. Mousing over the number of factors for a particular integer displays a pop-up list containing the unique factors for the integer. Mousing out of the cell causes the pop-up list to be removed (and perhaps causes a new pop-up list to be displayed, depending upon whether the mouse entered a new cell or not). Creating the auto pop-up effect is achieved by setting the 'auto_editable' trait of the associated ObjectColumn to True and also specifying a view to display on mouse over as the value of the ObjectColumn's 'view' trait. Note that this style of auto pop-up view can only be used with non-editable table editor fields. If the field is editable, then setting 'auto_editable' to True will cause the editor associated with the ObjectColumn to be automatically activated on mouse over, rather than the pop-up view specified by the 'view' trait. """ # -- Imports -------------------------------------------------------------- from operator import attrgetter from traits.api import ( HasTraits, Int, List, Range, Property, cached_property, ) from traitsui.api import View, VGroup, Item, TableEditor from traitsui.table_column import ObjectColumn # -- Integer Class -------------------------------------------------------- class Integer(HasTraits): # The value: n = Int() # -- Factor Class --------------------------------------------------------- class Factor(HasTraits): # The number being factored: n = Int() # The list of factors of 'n': factors = Property(List, observe='n') @cached_property def _get_factors(self): n = self.n i = 1 result = [] while (i * i) <= n: j = n // i if (i * j) == n: result.append(Integer(n=i)) if i != j: result.append(Integer(n=j)) i += 1 result.sort(key=attrgetter('n')) return result # -- The table editor used for the pop-up view ---------------------------- factor_table_editor = TableEditor( columns=[ ObjectColumn( name='n', width=1.0, editable=False, horizontal_alignment='center' ) ], sortable=False, auto_size=False, show_toolbar=False, show_column_labels=False, ) # -- The table editor used for the main view ------------------------------ factors_view = View( Item( 'factors', id='factors', show_label=False, editor=factor_table_editor, ), id='traits.examples.demo.Advanced.factors_view', kind='info', height=0.30, ) factors_table_editor = TableEditor( columns=[ ObjectColumn( name='n', width=0.5, editable=False, horizontal_alignment='center' ), ObjectColumn( name='factors', width=0.5, editable=False, horizontal_alignment='center', auto_editable=True, format_func=lambda f: '%s factors' % len(f), view=factors_view, ), ], sortable=False, auto_size=False, show_toolbar=False, ) # -- Factors Class -------------------------------------------------------- class Factors(HasTraits): # The maximum number to include in the table: max_n = Range(1, 1000, 20, mode='slider') # The list of Factor objects: factors = Property(List, observe='max_n') # The view of the list of Factor objects: view = View( VGroup( VGroup( Item('max_n'), show_labels=False, show_border=True, label='Maximum Number', ), VGroup( Item('factors', show_label=False, editor=factors_table_editor), ), ), title='List of numbers and their factors', width=0.2, height=0.4, resizable=True, ) @cached_property def _get_factors(self): return [Factor(n=i + 1) for i in range(self.max_n)] # -- Create and run the demo ---------------------------------------------- # Create the demo: demo = Factors() # Run the demo (if invoked from the command line): if __name__ == '__main__': demo.configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/examples/demo/Advanced/Auto_update_TabularEditor_demo.py0000644000175100001730000000670200000000000030756 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ **WARNING** This demo might not work as expected and some documented features might be missing. ------------------------------------------------------------------------------- Auto-update from a list to a TabularEditor Demonstrates using a TabularEditor with the 'auto_update' feature enabled, which allows the tabular editor to automatically update itself when the content of any object in the list associated with the editor is modified. To interact with the demo: - Select an employee from the list. - Adjust their salary increase. - Click the **Give raise** button. - Observe that the table automatically updates to reflect the employees new salary. In order for auto-update to work correctly, the editor trait should be a list of objects derived from HasTraits. Also, performance can be affected when very long lists are used, since enabling this feature adds and removed Traits listeners to each item in the list. """ # Issues related to the demo warning: # enthought/traitsui#960 from traits.api import HasTraits, Str, Float, List, Instance, Button from traitsui.api import ( View, HGroup, Item, TabularAdapter, TabularEditor, spring, ) # -- EmployeeAdapter Class ------------------------------------------------ class EmployeeAdapter(TabularAdapter): columns = [('Name', 'name'), ('Salary', 'salary')] def get_default_value(self, object, trait): return Employee(name="John", salary=30000) # -- Employee Class ------------------------------------------------------- class Employee(HasTraits): name = Str() salary = Float() # -- Company Class -------------------------------------------------------- class Company(HasTraits): employees = List(Employee) employee = Instance(Employee) increase = Float() give_raise = Button('Give raise') traits_view = View( Item( 'employees', show_label=False, editor=TabularEditor( adapter=EmployeeAdapter(), selected='employee', auto_resize=True, auto_update=True, ), ), HGroup( spring, Item('increase'), Item( 'give_raise', show_label=False, enabled_when='employee is not None', ), ), title='Auto Update Tabular Editor demo', height=0.25, width=0.30, resizable=True, ) def _give_raise_changed(self): self.employee.salary += self.increase self.employee = None # -- Set up the demo ------------------------------------------------------ demo = Company( increase=1000, employees=[ Employee(name='Fred', salary=45000), Employee(name='Sally', salary=52000), Employee(name='Jim', salary=39000), Employee(name='Helen', salary=41000), Employee(name='George', salary=49000), Employee(name='Betty', salary=46000), ], ) # Run the demo (if invoked from the command line): if __name__ == '__main__': demo.configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/examples/demo/Advanced/Date_editor_demo.py0000644000175100001730000000573200000000000026110 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ **WARNING** This demo might not work as expected and some documented features might be missing. ------------------------------------------------------------------------------- Implementation of a DateEditor demo plugin for Traits UI demo program. This demo shows a few different styles of the DateEditor and how it can be customized. """ # Issue related to the demo warning: enthought/traitsui#962 from traits.api import HasTraits, Date, List, Str from traitsui.api import View, Item, DateEditor, Group class DateEditorDemo(HasTraits): """Demo class to show Date editors.""" single_date = Date() multi_date = List(Date) info_string = Str( 'The editors for Traits Date objects. Showing both ' 'the defaults, and one with alternate options.' ) multi_select_editor = DateEditor( allow_future=False, multi_select=True, shift_to_select=False, on_mixed_select='max_change', # Qt ignores these setting and always shows only 1 month: months=2, padding=30, ) traits_view = View( Item('info_string', show_label=False, style='readonly'), Group( Item('single_date', label='Simple date editor'), Item('single_date', style='custom', label='Default custom editor'), Item( 'single_date', style='readonly', editor=DateEditor( strftime='You picked %B %d %Y', message='Click a date above.', ), label='ReadOnly editor', ), label='Default settings for editors', ), Group( Item( 'multi_date', editor=multi_select_editor, style='custom', label='Multi-select custom editor', ), label='More customized editor: multi-select; disallow ' 'future; selection style; etc.', ), resizable=True, ) def _multi_date_changed(self): """Print each time the date value is changed in the editor.""" print(self.multi_date) def _simple_date_changed(self): """Print each time the date value is changed in the editor.""" print(self.simple_date, self.single_date) def _single_date_changed(self): """Print each time the date value is changed in the editor.""" print(self.single_date) # -- Set Up The Demo ------------------------------------------------------ demo = DateEditorDemo() if __name__ == "__main__": demo.configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/examples/demo/Advanced/Date_range_editor_demo.py0000644000175100001730000000303300000000000027254 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Implementation of a DateRangeEditor demo plugin for Traits UI demo program. This demo shows a custom style DateRangeEditor. Note that this demo only works with qt backend. """ # Issue related to the demo warning: enthought/traitsui#962 from traits.api import HasTraits, Date, Tuple from traits.etsconfig.api import ETSConfig from traitsui.api import View, Item, DateRangeEditor, Group class DateRangeEditorDemo(HasTraits): """Demo class to show DateRangeEditor.""" date_range = Tuple(Date, Date) traits_view = View( Group( Item( 'date_range', editor=DateRangeEditor(), style='custom', label='Date range', ), label='Date range', ), resizable=True, ) def _date_range_changed(self): print(self.date_range) # -- Set Up The Demo ------------------------------------------------------ if __name__ == "__main__": if ETSConfig.toolkit in {"qt", "qt4"}: # DateRangeEditor is currently only available for qt backend. demo = DateRangeEditorDemo() demo.configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/examples/demo/Advanced/Dynamic_EnumEditor_demo.py0000644000175100001730000001151300000000000027376 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Dynamic changing an Enum selection list, depending on checked items Another way to dynamically change the values shown by an EnumEditor. The scenario is a restaurant that at the beginning of the day has a menu list of entrees based upon a fully stocked kitchen. However, as the day progresses, the kitchen's larder gets depleted, and the kitchen may no longer be able to prepare certain entrees, which must then be deleted from the menu. Similarly, deliveries may allow certain entrees to be added back onto the menu. The demo is divided into two tabs: Order and Kitchen. The Order tab represents a customer's order and consists of a single Entree field, which represents the customer's selection from the drop-down list of entrees that the kitchen can currently prepare. The Kitchen tab represents the current set of entrees that the kitchen can prepare, based upon the current contents of its larder. As entrees are checked on or off from the Kitchen tab, the customer's Entree drop-down is dynamically updated with the current list of available entrees. Notes: - The key point of the demo is the use of the 'name' trait in the EnumEditor definition, which links the list of available entrees from the KitchenCapabilities object to the OrderMenu object's entree EnumEditor. - The user can freely type any value they want, but only items in the capabilities will be accepted, due to the use of the 'values' argument to the Enum trait. This will be updated as the capabilities change. - With the Qt backend, the user can type text and it will be auto-completed to valid values as a the user types. The Wx backend doesn't support this capability in the underlying toolkit. - The design will work with any number of active OrderMenu objects, since they all share a common KitchenCapabilities object. As the KitchenCapabilities object is updated, all OrderMenu UI's will automatically update their associated Entree's drop-down list. - A careful reader will also observe that this example contains only declarative code. No imperative code is required to handle the automatic updating of the Entree list. """ from traits.api import Enum, HasPrivateTraits, Instance, List from traitsui.api import ( View, Item, VGroup, HSplit, EnumEditor, CheckListEditor, ) # -- The list of possible entrees ----------------------------------------- possible_entrees = [ 'Chicken Fried Steak', 'Chicken Fingers', 'Chicken Enchiladas', 'Cheeseburger', 'Pepper Steak', 'Beef Tacos', 'Club Sandwich', 'Ceasar Salad', 'Cobb Salad', ] # -- The KitchenCapabilities class ---------------------------------------- class KitchenCapabilities(HasPrivateTraits): # The current set of entrees the kitchen can make (based on its larder): available = List(possible_entrees) # The KitchenCapabilities are shared by all waitstaff taking orders: kitchen_capabilities = KitchenCapabilities() # -- The OrderMenu class -------------------------------------------------- class OrderMenu(HasPrivateTraits): # The person's entree order: entree = Enum(values='capabilities.available') # Reference to the restaurant's current entree capabilities: capabilities = Instance(KitchenCapabilities) def _capabilities_default(self): return kitchen_capabilities # The user interface view: view = View( HSplit( VGroup( Item( 'entree', editor=EnumEditor( name='object.capabilities.available', evaluate=str, completion_mode='popup', ), ), label='Order', show_border=True, dock='tab', ), VGroup( Item( 'object.capabilities.available', show_label=False, style='custom', editor=CheckListEditor(values=possible_entrees), ), label='Kitchen', show_border=True, dock='tab', ), ), title='Dynamic EnumEditor Demo', resizable=True, ) # ------------------------------------------------------------------------- # Create the demo: demo = OrderMenu() # Run the demo (if invoked from the command line): if __name__ == '__main__': demo.configure_traits() print(demo.entree) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/examples/demo/Advanced/Dynamic_range_trait_and_editor.py0000644000175100001730000001576300000000000031021 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ This program demonstrates defining and visualizing dynamic ranges. A dynamic range is a range whose low or high limit can be modified dynamically at run time. You can define a dynamic range using the standard Range trait and specifying the name of other traits as either the low or high limit value (or both). In fact, it is even possible to specify the default value of the range trait as another trait if desired. The traits used as low, high or default values need not be defined on the same object, but can be defined on any object reachable from the object (i.e. it allows use of extended trait names). In this completely artificial example, we present an example of the first hotel at the North Pole. The hotel guarantees that each room will be heated to a certain minimum temperature. However, that minimum value is determined both by the time of year and the current cost of heating fuel, so it can vary; but each room is guaranteed the same minimum temperature. Each guest of the hotel can choose among several different plans that allow them to control the maximum room temperature. Higher maximum room temperatures correspond to higher plan costs. Thus each guest must decide which plan (and highest maximum room temperature) to pay for. And finally, each guest is free to set the current room temperature anywhere between the hotel minimum value and the guest's maximum plan value. The demo is organized as a series of tabs corresponding to each guest of the hotel, with access to the plan they have chosen and the current room temperature setting. In addition, there is a master set of hotel information displayed at the top of the view which allows you to change the season of the year and the current fuel cost. There is also a button to allow more guests to be added to the hotel. Notes: - The dynamic range trait is the 'temperature' trait in the Guest class. It depends upon traits defined both in the Guest instance as well as in the containing Hotel object. - As with most traits code and examples, observe how much of the code is 'declarative' versus 'imperative'. Note also the use of properties and 'observe' metadata, as well as 'cached_property' and 'on_trait_change' method decorators. - Try dragging the guest tabs around so that you can see multiple guests simultaneously, and then watch the behavior of the guest's 'temperature' slider as you adjust the hotel 'season', 'fuel cost' and each guest's 'plan'. """ # -- Imports -------------------------------------------------------------- import logging import sys from random import choice from traits.api import ( HasPrivateTraits, Str, Enum, Range, List, Button, Instance, Property, cached_property, observe, ) from traitsui.api import View, VGroup, HGroup, Item, ListEditor, spring logging.basicConfig(stream=sys.stderr) # -- The Hotel class ------------------------------------------------------ class Hotel(HasPrivateTraits): # The season of the year: season = Enum('Winter', 'Spring', 'Summer', 'Fall') # The current cost of heating fuel (in dollars/gallon): fuel_cost = Range(2.00, 10.00, 4.00) # The current minimum temparature allowed by the hotel: min_temperature = Property(observe='season, fuel_cost') # The guests currently staying at the hotel: guests = List # ( Instance( 'Guest' ) ) # Add a new guest to the hotel: add_guest = Button('Add Guest') # The view of the hotel: view = View( VGroup( HGroup( Item('season'), '20', Item('fuel_cost', width=300), spring, Item('add_guest', show_label=False), show_border=True, label='Hotel Information', ), VGroup( Item( 'guests', style='custom', editor=ListEditor( use_notebook=True, deletable=True, dock_style='tab', page_name='.name', ), ), show_labels=False, show_border=True, label='Guests', ), ), title='The Belmont Hotel Dashboard', width=0.6, height=0.2, resizable=True, ) # Property implementations: @cached_property def _get_min_temperature(self): return {'Winter': 32, 'Spring': 40, 'Summer': 45, 'Fall': 40}[ self.season ] + min(int(60.00 / self.fuel_cost), 15) # Event handlers: @observe('guests.items') def _guests_modified(self, event): # The entire guests list changed if isinstance(event.object, Hotel): for guest in event.new: guest.hotel = self # the contents of the guests list changed else: for guest in event.added: guest.hotel = self def _add_guest_changed(self): self.guests.append(Guest(hotel=self)) # -- The Guest class ------------------------------------------------------ class Guest(HasPrivateTraits): # The name of the guest: name = Str() # The hotel the guest is staying at: hotel = Instance(Hotel) # The room plan the guest has chosen: plan = Enum('Flop house', 'Cheap', 'Cozy', 'Deluxe') # The maximum temperature allowed by the guest's plan: max_temperature = Property(observe='plan') # The current room temperature as set by the guest: temperature = Range('hotel.min_temperature', 'max_temperature') # The view of the guest: view = View(Item('plan'), Item('temperature')) # Property implementations: @cached_property def _get_max_temperature(self): return {'Flop house': 62, 'Cheap': 66, 'Cozy': 75, 'Deluxe': 85}[ self.plan ] # Default values: def _name_default(self): return choice( [ 'Leah', 'Vibha', 'Janet', 'Jody', 'Dave', 'Evan', 'Ilan', 'Gael', 'Peter', 'Robert', 'Judah', 'Eric', 'Travis', 'Mike', 'Bryce', 'Chris', ] ) # -- Create the demo ------------------------------------------------------ # Create the demo object: demo = Hotel(guests=[Guest() for i in range(5)]) # Run the demo (if invoked from the command line): if __name__ == '__main__': logging.info('Start!') demo.configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/examples/demo/Advanced/Dynamic_views_demo.py0000644000175100001730000001133000000000000026455 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Demonstrates how to use the Dynamic Views facility. """ from traits.api import Bool, HasTraits, Str, Instance, Button from traitsui.api import View, HGroup, Group, Item, Handler, Label, spring from traitsui.extras.has_dynamic_views import DynamicView, HasDynamicViews class HasFooView(HasDynamicViews): """A base class declaring the existence of the 'foo' dynamic view.""" def __init__(self, *args, **traits): """Constructor. Extended to declare our dynamic foo view. """ super().__init__(*args, **traits) # Declare and add our dynamic view: declaration = DynamicView( name='foo', id='traitsui.demos.dynamic_views', keywords={ 'buttons': ['OK'], 'dock': 'tab', 'height': 0.4, 'width': 0.4, 'resizable': True, 'scrollable': True, }, use_as_default=True, ) self.declare_dynamic_view(declaration) class MyInfoHandler(Handler): def object_first_changed(self, info): info.object.derived = info.object.first class BaseFoo(HasFooView): """A base class that puts some content in the 'foo' dynamic view.""" first = Str('My first name') last = Str('My last name') # A derived trait set by the handler associated with out dynamic view # contribution: derived = Str() ui_person = Group( Item( label='On this tab, notice how the sub-handler keeps\n' 'the derived value equal to the first name.\n\n' 'On the next tab, change the selection in order to\n' 'control which tabs are visible when the ui is \n' 'displayed for the 2nd time.' ), spring, 'first', 'last', spring, 'derived', label='My Info', _foo_order=5, _foo_priority=1, _foo_handler=MyInfoHandler(), ) class FatherInfoHandler(Handler): def object_father_first_name_changed(self, info): info.object.father_derived = info.object.father_first_name class DerivedFoo(BaseFoo): """A derived class that puts additional content in the 'foo' dynamic view. Note that the additional content could also have been added via a traits category contribution, or even dynamic manipulation of metadata on a UI subelement. The key is what the metadata represents when the view is *created* """ knows_mother = Bool(False) mother_first_name = Str("My mother's first name") mother_last_name = Str("My mother's last name") knows_father = Bool(True) father_first_name = Str("My father's first name") father_last_name = Str("My father's last name") father_derived = Str() ui_parents = Group( 'knows_mother', 'knows_father', label='Parents?', _foo_order=7, _foo_priority=1, ) ui_mother = Group( 'mother_first_name', 'mother_last_name', label="Mother's Info", _foo_priority=1, ) ui_father = Group( 'father_first_name', 'father_last_name', spring, 'father_derived', label="Father's Info", _foo_order=15, _foo_priority=1, _foo_handler=FatherInfoHandler(), ) def _knows_mother_changed(self, old, new): ui_mother = self.trait_view('ui_mother') if new: ui_mother._foo_order = 10 elif hasattr(ui_mother, '_foo_order'): del ui_mother._foo_order def _knows_father_changed(self, old, new): ui_father = self.trait_view('ui_father') if new: ui_father._foo_order = 15 elif hasattr(ui_father, '_foo_order'): del ui_father._foo_order class FooDemo(HasTraits): """Defines a class to run the demo.""" foo = Instance(DerivedFoo, ()) configure = Button('Configure') view = View( Label( "Try configuring several times, each time changing the items " "on the 'Parents?' tab." ), '_', HGroup(spring, Item('configure', show_label=False)), ) def _configure_changed(self): self.foo.configure_traits() # Create the demo: popup = FooDemo() # Run the demo (if invoked from the command line): if __name__ == '__main__': popup.configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/examples/demo/Advanced/Dynamically_changing_buttons_demo.py0000644000175100001730000000463000000000000031543 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ This demo shows how to dynamically change the label or image of a button. Note in this demo clicking the button itself does nothing. Please refer to the `ButtonEditor API docs`_ for further information. .. _ButtonEditor API docs: https://docs.enthought.com/traitsui/api/traitsui.editors.button_editor.html#traitsui.editors.button_editor.ButtonEditor """ from pyface.api import Image, ImageResource from traits.api import Button, Enum, HasTraits, Instance, List, Str from traitsui.api import ( ButtonEditor, Group, ImageEditor, InstanceChoice, InstanceEditor, Item, UItem, View, ) import traitsui.extras class ImageChoice(InstanceChoice): def get_view(self): return View(UItem('name', editor=ImageEditor(image=self.object))) class ButtonEditorDemo(HasTraits): my_button = Button() my_button_label = Str("Initial Label") my_button_image = Image() my_button_image_options = List( Image(), value=[ ImageResource("run", [traitsui.extras]), ImageResource("previous", [traitsui.extras]), ImageResource("next", [traitsui.extras]), ImageResource("parent", [traitsui.extras]), ImageResource("reload", [traitsui.extras]), ], ) def _my_button_image_default(self): return self.my_button_image_options[0] traits_view = View( Item( "my_button", style="custom", editor=ButtonEditor( label_value="my_button_label", image_value="my_button_image", orientation="horizontal", ), ), Item("my_button_label"), Item( "my_button_image", editor=InstanceEditor( name="my_button_image_options", adapter=ImageChoice ), style="custom", ), ) # Create the demo: demo = ButtonEditorDemo() # Run the demo (if invoked from the command line): if __name__ == '__main__': demo.configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/examples/demo/Advanced/HDF5_tree_demo.py0000644000175100001730000001702100000000000025364 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """This demo shows how to use Traits TreeEditors with PyTables to walk the hierarchy of an HDF5 file. This only picks out tables, arrays and groups, but could easily be extended to other structures. In the demo, the path to the selected item is printed whenever the selection changes. An example HDF5 file is provided here, but you could easily change the path given at the bottom of this file to a path to your own HDF5 file. To run this demonstration successfully, you must have the following packages installed: - **PyTables** (``tables``) - **HDF5 for Python** (``h5py``) Note that PyTables can't read HDF5 files created with h5py, but h5py can read HDF5 files created with PyTables. See HDF5_tree_demo2 for an example using h5py. """ import os import tables as tb from traits.api import HasTraits, Str, List, Instance, Any from traitsui.api import TreeEditor, TreeNode, View, Item, Group ROOT = os.path.dirname(__file__) # View for objects that aren't edited no_view = View() # HDF5 Nodes in the tree class Hdf5ArrayNode(HasTraits): name = Str('') path = Str('') parent_path = Str('') class Hdf5GroupNode(HasTraits): name = Str('') path = Str('') parent_path = Str('') # Can't have recursive traits? Really? # groups = List(Hdf5GroupNode) groups = List() arrays = List(Hdf5ArrayNode) groups_and_arrays = List() class Hdf5FileNode(HasTraits): name = Str('') path = Str('/') groups = List(Hdf5GroupNode) arrays = List(Hdf5ArrayNode) groups_and_arrays = List() # Recursively build tree, there is probably a better way of doing this. def _get_sub_arrays(group, h5file): """Return a list of all arrays immediately below a group in an HDF5 file.""" return [ Hdf5ArrayNode( name=array._v_name, path=array._v_pathname, parent_path=array._v_parent._v_pathname, ) for array in group if isinstance(array, (tb.Array, tb.Table)) ] # More pythonic # for array in h5file.iter_nodes(group, classname='Array')] # Old call def _get_sub_groups(group, h5file): """Return a list of all groups and arrays immediately below a group in an HDF5 file.""" subgroups = [] for subgroup in h5file.iter_nodes(group, classname='Group'): subsubgroups = _get_sub_groups(subgroup, h5file) subsubarrays = _get_sub_arrays(subgroup, h5file) subgroups.append( Hdf5GroupNode( name=subgroup._v_name, path=subgroup._v_pathname, parent_path=subgroup._v_parent._v_pathname, groups=subsubgroups, arrays=subsubarrays, groups_and_arrays=subsubgroups + subsubarrays, ) ) return subgroups def _hdf5_tree(filename): """Return a list of all groups and arrays below the root group of an HDF5 file.""" with tb.open_file(filename, 'r') as h5file: subgroups = _get_sub_groups(h5file.root, h5file) subarrays = _get_sub_arrays(h5file.root, h5file) h5_tree = Hdf5FileNode( name=filename, groups=subgroups, arrays=subarrays, groups_and_arrays=subgroups + subarrays, ) return h5_tree # Get a tree editor def _hdf5_tree_editor(selected=''): """Return a TreeEditor specifically for HDF5 file trees.""" return TreeEditor( nodes=[ TreeNode( node_for=[Hdf5FileNode], auto_open=True, children='groups_and_arrays', label='name', view=no_view, ), TreeNode( node_for=[Hdf5GroupNode], auto_open=False, children='groups_and_arrays', label='name', view=no_view, ), TreeNode( node_for=[Hdf5ArrayNode], auto_open=False, children='', label='name', view=no_view, ), ], editable=False, selected=selected, ) class ATree(HasTraits): h5_tree = Instance(Hdf5FileNode) node = Any() traits_view = View( Group( Item( 'h5_tree', editor=_hdf5_tree_editor(selected='node'), resizable=True, show_label=False, ), orientation='vertical', ), title='HDF5 Tree Example', buttons=['Undo', 'OK', 'Cancel'], resizable=True, width=0.3, height=0.3, ) def _node_changed(self): print(self.node.path) def make_test_datasets(): """Makes the test datasets and store it in the current folder""" import h5py import numpy as np import pandas as pd # pandas uses pytables to store datasets in hdf5 format. from random import randrange n = 100 df = pd.DataFrame( dict( [ ("int{0}".format(i), np.random.randint(0, 10, size=n)) for i in range(5) ] ) ) df['float'] = np.random.randn(n) for i in range(10): df["object_1_{0}".format(i)] = [ '%08x' % randrange(16 ** 8) for _ in range(n) ] for i in range(7): df["object_2_{0}".format(i)] = [ '%15x' % randrange(16 ** 15) for _ in range(n) ] df.info() df.to_hdf('test_fixed.h5', 'data', format='fixed') df.to_hdf('test_table_no_dc.h5', 'data', format='table') df.to_hdf('test_table_dc.h5', 'data', format='table', data_columns=True) df.to_hdf( 'test_fixed_compressed.h5', 'data', format='fixed', complib='blosc', complevel=9, ) # h5py dataset time = np.arange(n) x = np.linspace(-7, 7, n) axes_latlon = [ ('time', time), ('coordinate', np.array(['lat', 'lon'], dtype='S3')), ] axes_mag = [ ('time', time), ('direction', np.array(['x', 'y', 'z'], dtype='S1')), ] latlon = np.vstack( (np.linspace(-0.0001, 0.00001, n) + 23.8, np.zeros(n) - 82.3) ).T mag_data = np.vstack( ( -(1 - np.tanh(x) ** 2) * np.sin(2 * x), -(1 - np.tanh(x) ** 2) * np.sin(2 * x), -(1 - np.tanh(x) ** 2), ) ).T datasets = ( axes_mag + axes_latlon + [('magnetic_3_axial', mag_data), ('latlon', latlon)] ) with h5py.File(os.path.join(ROOT, 'test_h5pydata.h5'), "a") as h5file: h5group = h5file.require_group("run1_test1") for data_name, data in datasets: h5group.require_dataset( name=data_name, dtype=data.dtype, shape=data.shape, data=data, # **options ) def main(): import sys filename = os.path.join(ROOT, 'test_fixed.h5') filename = os.path.join(ROOT, 'test_table_no_dc.h5') if len(sys.argv) > 1: filename = sys.argv[1] a_tree = ATree(h5_tree=_hdf5_tree(filename)) return a_tree demo = main() if __name__ == '__main__': # make_test_datasets() demo.configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/examples/demo/Advanced/HDF5_tree_demo2.py0000644000175100001730000002312200000000000025445 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """This demo shows how to use Traits TreeEditors with h5py to walk the hierarchy of several HDF5 files in a folder. All Datasets and Groups are shown for each file. In the demo, the path to the selected item is printed whenever the selection changes. An example HDF5 file is provided here, but you could easily change the path given at the bottom of this file to a path to your own HDF5 file. To run this demonstration successfully, you must have the following packages installed: - **PyTables** (``tables``) - **HDF5 for Python** (``h5py``) Note that PyTables can't read HDF5 files created with h5py, but h5py can read HDF5 files created with PyTables. See HDF5_tree_demo for an example using PyTables. """ import os import warnings import h5py from traits import api import traitsui.api as ui ROOT = os.path.dirname(__file__) # View for objects that aren't edited no_view = ui.View() # HDF5 Nodes in the tree class Hdf5ArrayNode(api.HasTraits): name = api.Str('') path = api.Str('') parent_path = api.Str('') class Hdf5GroupNode(api.HasTraits): name = api.Str('') path = api.Str('') parent_path = api.Str('') # Can't have recursive traits? Really? # groups = api.List( Hdf5GroupNode ) groups = api.List() arrays = api.List(Hdf5ArrayNode) groups_and_arrays = api.List() class Hdf5FileNode(api.HasTraits): name = api.Str('') path = api.Str('/') groups = api.List(Hdf5GroupNode) arrays = api.List(Hdf5ArrayNode) groups_and_arrays = api.List() class Hdf5FilesNode(api.HasTraits): name = api.Str('') path = api.Str('/') files = api.List(Hdf5FileNode) groups_and_arrays = api.List() # Recursively build tree, there is probably a better way of doing this. def _get_sub_arrays(group, parent_path): """Return a list of all arrays immediately below a group in an HDF5 file.""" return [ Hdf5ArrayNode( name=name, path=parent_path + name, parent_path=parent_path ) for name, array in group.items() if isinstance(array, h5py.Dataset) ] def _get_sub_groups(group, parent_path): """Return a list of all subgroups and arrays immediately below a group in an HDF5 file.""" subgroups = [] for name, subgroup in group.items(): if isinstance(subgroup, h5py.Group): path = parent_path + name + '/' subsubarrays = _get_sub_arrays(subgroup, path) subsubgroups = _get_sub_groups(subgroup, path) subgroups.append( Hdf5GroupNode( name=name, path=path, parent_path=parent_path, arrays=subsubarrays, subgroups=subsubgroups, groups_and_arrays=subsubgroups + subsubarrays, ) ) return subgroups def _hdf5_tree(filename): with h5py.File(filename, 'r') as h5file: path = ( filename + '#' ) # separate dataset name from name of hdf5-filename subgroups = _get_sub_groups(h5file, path) subarrays = _get_sub_arrays(h5file, path) file_tree = Hdf5FileNode( name=os.path.basename(filename), path=filename, groups=subgroups, arrays=subarrays, groups_and_arrays=subgroups + subarrays, ) return file_tree def _hdf5_trees(filenames): """Return a list of all groups and arrays below the root group of an HDF5 file.""" if isinstance(filenames, str): filenames = [filenames] files_tree = Hdf5FilesNode() files_tree.name = root = os.path.dirname(filenames[0]) files_tree.path = root for filename in filenames: folder = os.path.dirname(filename) if folder != root: warnings.warn( "Expected same folder for all files, but got {} != {}".format( root, folder ) ) file_tree = _hdf5_tree(filename) files_tree.files.append(file_tree) files_tree.groups_and_arrays.extend(file_tree.groups_and_arrays) return files_tree # Get a tree editor def _hdf5_tree_editor(selected=''): """Return a ui.TreeEditor specifically for HDF5 file trees.""" return ui.TreeEditor( nodes=[ ui.TreeNode( node_for=[Hdf5FilesNode], auto_open=True, children='files', label='name', view=no_view, ), ui.TreeNode( node_for=[Hdf5FileNode], auto_open=True, children='groups_and_arrays', label='name', view=no_view, ), ui.TreeNode( node_for=[Hdf5GroupNode], auto_open=False, children='groups_and_arrays', label='name', view=no_view, ), ui.TreeNode( node_for=[Hdf5ArrayNode], auto_open=False, children='', label='name', view=no_view, ), ], editable=False, selected=selected, ) class _H5Tree(api.HasTraits): h5_tree = api.Instance(Hdf5FileNode) node = api.Any() path = api.Str() traits_view = ui.View( ui.Group( ui.Item( 'h5_tree', editor=_hdf5_tree_editor(selected='node'), resizable=True, ), ui.Item('path', label='Selected node'), orientation='vertical', ), title='HDF5 Tree Example', buttons=['OK', 'Cancel'], resizable=True, width=0.3, height=0.3, ) def _node_changed(self): self.path = self.node.path print(self.node.path) class _H5Trees(api.HasTraits): h5_trees = api.Instance(Hdf5FilesNode) node = api.Any() path = api.Str() traits_view = ui.View( ui.Group( ui.Item( 'h5_trees', editor=_hdf5_tree_editor(selected='node'), resizable=True, ), ui.Item('path', label='Selected node'), orientation='vertical', ), title='Multiple HDF5 file Tree Example', buttons=['OK', 'Cancel'], resizable=True, width=0.3, height=0.3, ) def _node_changed(self): self.path = self.node.path print(self.node.path) def hdf5_tree(filename): # if isinstance(filename, str): # return _H5Tree(h5_tree=_hdf5_tree(filename)) return _H5Trees(h5_trees=_hdf5_trees(filename)) def make_test_datasets(): """Makes the test datasets and store it in the current folder""" import numpy as np import pandas as pd # pandas uses pytables to store datasets in hdf5 format. from random import randrange n = 100 df = pd.DataFrame( dict( [ ("int{0}".format(i), np.random.randint(0, 10, size=n)) for i in range(5) ] ) ) df['float'] = np.random.randn(n) for i in range(10): df["object_1_{0}".format(i)] = [ '%08x' % randrange(16 ** 8) for _ in range(n) ] for i in range(7): df["object_2_{0}".format(i)] = [ '%15x' % randrange(16 ** 15) for _ in range(n) ] df.info() df.to_hdf('test_fixed.h5', 'data', format='fixed') df.to_hdf('test_table_no_dc.h5', 'data', format='table') df.to_hdf('test_table_dc.h5', 'data', format='table', data_columns=True) df.to_hdf( 'test_fixed_compressed.h5', 'data', format='fixed', complib='blosc', complevel=9, ) # h5py dataset time = np.arange(n) x = np.linspace(-7, 7, n) axes_latlon = [ ('time', time), ('coordinate', np.array(['lat', 'lon'], dtype='S3')), ] axes_mag = [ ('time', time), ('direction', np.array(['x', 'y', 'z'], dtype='S1')), ] latlon = np.vstack( (np.linspace(-0.0001, 0.00001, n) + 23.8, np.zeros(n) - 82.3) ).T mag_data = np.vstack( ( -(1 - np.tanh(x) ** 2) * np.sin(2 * x), -(1 - np.tanh(x) ** 2) * np.sin(2 * x), -(1 - np.tanh(x) ** 2), ) ).T datasets = ( axes_mag + axes_latlon + [('magnetic_3_axial', mag_data), ('latlon', latlon)] ) with h5py.File(os.path.join(ROOT, 'test_h5pydata.h5'), "a") as h5file: h5group = h5file.require_group("run1_test1") for data_name, data in datasets: h5group.require_dataset( name=data_name, dtype=data.dtype, shape=data.shape, data=data, # **options ) def main(): filenames = [ 'test_fixed.h5', 'test_table_no_dc.h5', 'test_table_dc.h5', 'test_fixed_compressed.h5', 'test_h5pydata.h5', ] fullfiles = [os.path.join(ROOT, fname) for fname in filenames] h5_trees = hdf5_tree(fullfiles) return h5_trees demo = main() if __name__ == '__main__': # make_test_datasets() ok_status = demo.configure_traits() print('The End', ok_status) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/examples/demo/Advanced/History_demo.py0000644000175100001730000000463700000000000025331 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ This program demonstrates the use of editors that support *history*. A history is a persistent record of the last 'n' values the user has entered or selected for a particular trait. In order for the history to be recorded correctly, you must specify an *id* for both the Item containing the history editor and the View containing the Item. The maximum number of history entries recorded is specified by the value of the history editor's *entries* trait. If *entries* is less than or equal to 0, then a normal, non-history, version of the editor will be used. A history editor also attempts to restore the last value set as the current value for the trait if the current value of the trait is the empty string. You can see this for yourself by setting some values in the fields, selecting a different demo, then reselecting this demo. Each field should have the same value it had when the demo was run the previous time. """ # -- Imports -------------------------------------------------------------- from traits.api import HasTraits, Str, File, Directory from traitsui.api import View, Item, FileEditor, DirectoryEditor, HistoryEditor # -- HistoryDemo Class ---------------------------------------------------- class HistoryDemo(HasTraits): name = Str() file = File() directory = Directory() view = View( Item('name', id='name', editor=HistoryEditor(entries=5)), Item('file', id='file1', editor=FileEditor(entries=10)), Item( 'file', id='file2', editor=FileEditor( entries=10, filter=['All files (*.*)|*.*', 'Python files (*.py)|*.py'], ), ), Item('directory', id='directory', editor=DirectoryEditor(entries=10)), title='History Editor Demo', id='enthought.test.history_demo.HistoryDemo', width=0.33, resizable=True, ) # Create the demo: demo = HistoryDemo() # Run the demo (if invoked from the command line): if __name__ == '__main__': demo.configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/examples/demo/Advanced/Invalid_state_handling.py0000644000175100001730000001241500000000000027307 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Sometimes the inputs to a model are not correlated. That is, any valid model input produces a corresponding valid model state. However, in other cases, some or all of the model inputs are correlated. That is, there may exist one or more combinations of individually valid model inputs which produce invalid model states. In cases where this can happen, it is very often desirable to warn the user if a particular combination of input values will not produce a usable result. This problem cannot be solved solely though the use of carefully chosen trait types and editors, because each individual input may be valid, but it is the combination of inputs which is invalid. Solving this problem therefore typically requires providing a way of determining whether or not the model is in a valid state, and then communicating that information to the user via the user interface. This demonstration provides an example of doing this using the TraitEditor's 'invalid' trait. Each trait editor has an 'invalid' trait which can be set equal to the name of a trait in the user interface context which contains a boolean value reflecting whether or not the user interface (and underlying model) are in a invalid state or not. A True value for the trait indicates that the editor's current value produces an invalid model state. By associating the same 'invalid' trait with one or more editors in the user interface, the resulting user interface can indicate to the user which combination of input values is producing the invalid state. In this example, we have a very simple model which allows the user to control the mass and velocity of a system. The model also defines the kinetic energy of the resulting system. For safety reasons, the kinetic energy of the system should not exceed a certain threshold. If it does, the user should be warned so that they can reduce either or both of the system mass and velocity back down to a safe level. In the model, an 'error' property is defined which is True whenever the kinetic energy level of the system exceeds the safety threshold. This trait is then synchronized with the user interface's 'mass', velocity' and 'status' editors, turning them red whenever the model enters an invalid state. The 'status' trait is another property, based on the 'error' trait, which provides a human readable description of the current system state. Note that in this example, we synchronize the 'error' trait with the user interface using 'sync_to_view' metadata, whose value is a list of user interface editor traits the trait should be synchronized 'to' (i.e. changes to the 'error' trait will be copied to the corresponding trait in the editor, but not vice versa). We could also have explicitly set the 'invalid' trait of each corresponding editor in the view definition to 'error' as well. To use the demo, simply use the 'mass' and 'velocity' sliders and observe the changes to the 'kinetic_energy' of the system. When the kinetic energy exceeds 50,000, notice how the 'mass', 'velocity' and 'status' fields turn red, and that when the kinetic energy drops below 50,000, the fields return to their normal color. """ # -- Imports -------------------------------------------------------------- from traits.api import ( HasTraits, Range, Float, Bool, Str, Property, ) from traitsui.api import View, VGroup, Item # -- System Class --------------------------------------------------------- class System(HasTraits): # The mass of the system: mass = Range(0.0, 100.0) # The velocity of the system: velocity = Range(0.0, 100.0) # The kinetic energy of the system: kinetic_energy = Property(Float, observe='mass, velocity') # The current error status of the system: error = Property( Bool, sync_to_view='mass.invalid, velocity.invalid, status.invalid', observe='kinetic_energy', ) # The current status of the system: status = Property(Str, observe='error') view = View( VGroup( VGroup( Item('mass'), Item('velocity'), Item('kinetic_energy', style='readonly', format_str='%.0f'), label='System', show_border=True, ), VGroup( Item('status', style='readonly', show_label=False), label='Status', show_border=True, ), ) ) def _get_kinetic_energy(self): return (self.mass * self.velocity * self.velocity) / 2.0 def _get_error(self): return self.kinetic_energy > 50000.0 def _get_status(self): if self.error: return 'The kinetic energy of the system is too high.' return '' # -- Create and run the demo ---------------------------------------------- # Create the demo: demo = System() # Run the demo (if invoked from the command line): if __name__ == '__main__': demo.configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/examples/demo/Advanced/ListStrAdapter_demo.py0000644000175100001730000000563600000000000026575 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Display an editable List of Strings. The ListStrEditor uses a custom ListStrAdapter to define colors and tooltips. """ from traitsui.list_str_adapter import ListStrAdapter from traits.api import HasTraits, List, Str from traitsui.api import View, Item, ListStrEditor # -- The adapter ---------------------------------------------------------- class HeadlinesListAdapter(ListStrAdapter): """ Custom adapter for string lists being edited in the ListStrEditor. - Style ALL CAPS titles in red. - Describe the capitalization in the tooltip. """ def get_text_color(self, object, trait, row): try: row_data = getattr(object, trait)[row] except IndexError: return "" if row_data.isupper(): return "red" return super().get_text_color(object, trait, row) def get_tooltip(self, object, trait, row): try: row_data = getattr(object, trait)[row] except IndexError: # if the ListStr is editable, IndexError indicates the empty next slots return "" if row_data.isupper(): return "An uppercase headline" elif row_data.istitle(): return "A headline in title case" elif row_data.islower(): return "A fully lowercase headline" else: return self.tooltip # -- The class providing the view ------------------------------------- class HeadlinesListDemo(HasTraits): headlines = List(Str) view = View( Item( "headlines", show_label=False, editor=ListStrEditor( title="List of headlines (hover for description)", adapter=HeadlinesListAdapter( # the default tooltip for generic rows tooltip="A headline" ), auto_add=True ), # (QT only) the tooltip shown in the area outside of rows # or when the row's tooltip is empty tooltip="List of headlines" ), title='List of headlines', width=320, height=370, resizable=True, ) # -- Set up the Demo ------------------------------------------------------ demo = HeadlinesListDemo( headlines=[ "Shark bites man", "MAN BITES SHARK", "The London Stock Exchange Collapses", "she sells sea shells on the sea shore" ] ) # Run the demo (in invoked from the command line): if __name__ == "__main__": demo.configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/examples/demo/Advanced/ListStrEditor_demo.py0000644000175100001730000000335000000000000026432 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Edit a List of Strings Simple demonstration of the ListStrEditor, which can be used for editing and displaying lists of strings, or other data that can be logically mapped to a list of strings. """ from traits.api import HasTraits, List, Str from traitsui.api import View, Item, ListStrEditor # -- ShoppingListDemo Class ----------------------------------------------- class ShoppingListDemo(HasTraits): # The list of things to buy at the store: shopping_list = List(Str) # -- Traits View Definitions ---------------------------------------------- view = View( Item( 'shopping_list', show_label=False, editor=ListStrEditor(title='Shopping List', auto_add=True), ), title='Shopping List', width=0.2, height=0.5, resizable=True, ) # -- Set up the Demo ------------------------------------------------------ demo = ShoppingListDemo( shopping_list=[ 'Carrots', 'Potatoes (5 lb. bag)', 'Cocoa Puffs', 'Ice Cream (French Vanilla)', 'Peanut Butter', 'Whole wheat bread', 'Ground beef (2 lbs.)', 'Paper towels', 'Soup (3 cans)', 'Laundry detergent', ] ) # Run the demo (in invoked from the command line): if __name__ == '__main__': demo.configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/examples/demo/Advanced/List_editor_notebook_selection_demo.py0000644000175100001730000000767300000000000032121 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Displaying a list of objects in notebook tabs A list of objects can be displayed in a tabbed notebook, one object per tab. This example also shows how the currently active notebook tab of a ListEditor can be controlled using the ListEditor's 'selected' trait. Note the interaction between the spinner control (for the 'index' trait) and the currently selected notebook tab. Try changing the spinner value, then try clicking on various notebook tabs. Finally, note that the ListEditor will automatically scroll the tabs to make the selected tab completely visible. """ # The following text was removed from the module docstring (only works in wx): # Also note that rearranging the notebook tabs (using drag and drop) does not # affect the correspondence between the index value and its associated notebook # tab. The correspondence is determined by the contents of the 'people' trait, # and not by the physical layout of the notebook tabs. from traits.api import HasStrictTraits, Str, Int, Regex, List, Instance, Range from traitsui.api import View, VGroup, Item, ListEditor # -- Person Class --------------------------------------------------------- class Person(HasStrictTraits): # Trait definitions: name = Str() age = Int() phone = Regex(value='000-0000', regex=r'\d\d\d[-]\d\d\d\d') # Traits view definition: traits_view = View( 'name', 'age', 'phone', width=0.18, buttons=['OK', 'Cancel'] ) # -- Sample Data ---------------------------------------------------------- people = [ Person(name='Dave Chomsky', age=39, phone='555-1212'), Person(name='Mike Wakowski', age=28, phone='555-3526'), Person(name='Joe Higginbotham', age=34, phone='555-6943'), Person(name='Tom Derringer', age=22, phone='555-7586'), Person(name='Dick Van Der Hooten', age=63, phone='555-3895'), Person(name='Harry McCallum', age=46, phone='555-3285'), Person(name='Sally Johnson', age=43, phone='555-8797'), Person(name='Fields Timberlawn', age=31, phone='555-3547'), ] # -- ListEditorNotebookSelectionDemo Class -------------------------------- class ListEditorNotebookSelectionDemo(HasStrictTraits): # -- Trait Definitions ---------------------------------------------------- # List of people: people = List(Person) # The currently selected person: selected = Instance(Person) # The index of the currently selected person: index = Range(0, 7, mode='spinner') # -- Traits View Definitions ---------------------------------------------- traits_view = View( Item('index'), '_', VGroup( Item( 'people', id='notebook', show_label=False, style='custom', editor=ListEditor( use_notebook=True, deletable=False, selected='selected', export='DockWindowShell', page_name='.name', ), ) ), id='traitsui.demo.Traits UI Demo.Advanced.' 'List_editor_notebook_selection_demo', dock='horizontal', ) # -- Trait Event Handlers ------------------------------------------------- def _selected_changed(self, selected): self.index = self.people.index(selected) def _index_changed(self, index): self.selected = self.people[index] # -- Set Up The Demo ------------------------------------------------------ demo = ListEditorNotebookSelectionDemo(people=people) if __name__ == "__main__": demo.configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/examples/demo/Advanced/List_editors_demo.py0000644000175100001730000000717300000000000026332 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ **WARNING** This demo might not work as expected and some documented features might be missing. ------------------------------------------------------------------------------- This shows the three different types of editor that can be applied to a list of objects: - Table - List - Dockable notebook (a list variant) Each editor style is editing the exact same list of objects. Note that any changes made in one editor are automatically reflected in the others. """ # Issue related to the demo warning: enthought/traitsui#948 from traits.api import HasStrictTraits, Str, Int, Regex, List, Instance from traitsui.api import ( Item, ListEditor, ObjectColumn, RuleTableFilter, Tabbed, TableEditor, View, ) from traitsui.table_filter import ( RuleFilterTemplate, MenuFilterTemplate, EvalFilterTemplate, ) # 'Person' class: class Person(HasStrictTraits): # Trait definitions: name = Str() age = Int() phone = Regex(value='000-0000', regex=r'\d\d\d[-]\d\d\d\d') # Traits view definition: traits_view = View( 'name', 'age', 'phone', width=0.18, buttons=['OK', 'Cancel'] ) # Sample data: people = [ Person(name='Dave', age=39, phone='555-1212'), Person(name='Mike', age=28, phone='555-3526'), Person(name='Joe', age=34, phone='555-6943'), Person(name='Tom', age=22, phone='555-7586'), Person(name='Dick', age=63, phone='555-3895'), Person(name='Harry', age=46, phone='555-3285'), Person(name='Sally', age=43, phone='555-8797'), Person(name='Fields', age=31, phone='555-3547'), ] # Table editor definition: filters = [EvalFilterTemplate, MenuFilterTemplate, RuleFilterTemplate] table_editor = TableEditor( columns=[ ObjectColumn(name='name', width=0.4), ObjectColumn(name='age', width=0.2), ObjectColumn(name='phone', width=0.4), ], editable=True, deletable=True, sortable=True, sort_model=True, auto_size=False, filters=filters, search=RuleTableFilter(), row_factory=Person, show_toolbar=True, ) # 'ListTraitTest' class: class ListTraitTest(HasStrictTraits): # Trait definitions: people = List(Instance(Person, ())) # Traits view definitions: traits_view = View( Tabbed( Item('people', label='Table', id='table', editor=table_editor), Item( 'people', label='List', id='list', style='custom', editor=ListEditor(style='custom', rows=5), ), Item( 'people', label='Notebook', id='notebook', style='custom', editor=ListEditor( use_notebook=True, deletable=True, export='DockShellWindow', page_name='.name', ), ), id='splitter', show_labels=False, ), id='traitsui.demo.Traits UI Demo.Advanced.List_editors_demo', dock='horizontal', width=600, ) # Create the demo: demo = ListTraitTest(people=people) # Run the demo (if invoked from the command line): if __name__ == '__main__': demo.configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/examples/demo/Advanced/MVC_demo.py0000644000175100001730000000640200000000000024305 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Supporting Model/View/Controller (MVC) pattern Demonstrates one approach to writing Model/View/Controller (MVC)-based applications using Traits UI. This example contains a trivial model containing only one data object, the string 'myname'. In this example, the Controller contains the View. A more rigorous example would separate these. A few key points: - the Controller itself accesses the model as self.model - the Controller's View can access model traits directly ('myname') """ from traits.api import HasTraits, Str, Bool, TraitError from traitsui.api import View, VGroup, HGroup, Item, Controller class MyModel(HasTraits): """Define a simple model containing a single string, 'myname'""" # Simple model data: myname = Str() class MyViewController(Controller): """Define a combined controller/view class that validates that MyModel.myname is consistent with the 'allow_empty_string' flag. """ # When False, the model.myname trait is not allowed to be empty: allow_empty_string = Bool() # Last attempted value of model.myname to be set by user: last_name = Str() # Define the view associated with this controller: view = View( VGroup( HGroup( Item('myname', springy=True), '10', Item('controller.allow_empty_string', label='Allow Empty'), ), # Add an empty vertical group so the above items don't end up # centered vertically: VGroup(), ), resizable=True, ) # -- Handler Interface ---------------------------------------------------- def myname_setattr(self, info, object, traitname, value): """Validate the request to change the named trait on object to the specified value. Validation errors raise TraitError, which by default causes the editor's entry field to be shown in red. (This is a specially named method _setattr, which is available inside a Controller.) """ self.last_name = value if (not self.allow_empty_string) and (value.strip() == ''): raise TraitError('Empty string not allowed.') return super().setattr(info, object, traitname, value) # -- Event handlers ------------------------------------------------------- def controller_allow_empty_string_changed(self, info): """'allow_empty_string' has changed, check the myname trait to ensure that it is consistent with the current setting. """ if (not self.allow_empty_string) and (self.model.myname == ''): self.model.myname = '?' else: self.model.myname = self.last_name # Create the model and (demo) view/controller: demo = MyViewController(MyModel()) # Run the demo (if invoked from the command line): if __name__ == '__main__': demo.configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/examples/demo/Advanced/Multi_select_string_list.py0000644000175100001730000000636600000000000027737 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ **WARNING** This demo might not work as expected and some documented features might be missing. ------------------------------------------------------------------------------- Creating a multi-select list box How to use a TabularEditor to create a multi-select list box. This demo uses two TabularEditors, side-by-side. Selections from the left table are shown in the right table. Each table has only one column. """ # Issues related to the demo warning: enthought/traitsui#14, # enthought/traitsui#15, enthought/traitsui#960 from traits.api import HasPrivateTraits, List, Str, Property from traitsui.api import View, HGroup, UItem, TabularAdapter, TabularEditor class MultiSelectAdapter(TabularAdapter): """This adapter is used by both the left and right tables""" # Titles and column names for each column of a table. # In this example, each table has only one column. columns = [('', 'myvalue')] # Magically named trait which gives the display text of the column named # 'myvalue'. This is done using a Traits Property and its getter: myvalue_text = Property() # The getter for Property 'myvalue_text' simply takes the value of the # corresponding item in the list being displayed in this table. # A more complicated example could format the item before displaying it. def _get_myvalue_text(self): return self.item class MultiSelect(HasPrivateTraits): """This is the class used to view two tables""" # FIXME (TraitsUI defect #14): When multi-select is done by keyboard # (shift+arrow), the 'selected' trait list does not update. # FIXME (TraitsUI defect #15): In Windows wx, when show_titles is False, # left table does not draw until selection passes through all rows. # (Workaround here: set show_titles True and make column titles empty.) choices = List(Str) selected = List(Str) traits_view = View( HGroup( UItem( 'choices', editor=TabularEditor( show_titles=True, selected='selected', editable=False, multi_select=True, adapter=MultiSelectAdapter(), ), ), UItem( 'selected', editor=TabularEditor( show_titles=True, editable=False, adapter=MultiSelectAdapter(), ), ), ), resizable=True, width=200, height=300, ) # Create the demo: demo = MultiSelect( choices=[ 'one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine', 'ten', ] ) # Run the demo (if invoked from the command line): if __name__ == '__main__': demo.configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/examples/demo/Advanced/Multi_thread_demo.py0000644000175100001730000000435100000000000026302 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Monitoring threads in the user interface Shows a simple user interface being updated by multiple threads. When the *Start Threads* button is pressed, the program starts three independent threads running. Each thread counts from 0 to 199, updating its own thread-specific trait, and performs a sleep of a thread-specific duration between each update. The *Start Threads* button is disabled while the threads are running, and becomes active again once all threads have finished running. """ from threading import Thread from time import sleep from traits.api import HasTraits, Int, Button from traitsui.api import View, Item, VGroup class ThreadDemo(HasTraits): # The thread specific counters: thread_0 = Int() thread_1 = Int() thread_2 = Int() # The button used to start the threads running: start = Button('Start Threads') # The count of how many threads ae currently running: running = Int() view = View( VGroup( Item('thread_0', style='readonly'), Item('thread_1', style='readonly'), Item('thread_2', style='readonly'), ), '_', Item('start', show_label=False, enabled_when='running == 0'), resizable=True, width=250, title='Monitoring threads', ) def _start_changed(self): for i in range(3): Thread( target=self.counter, args=('thread_%d' % i, (i * 10 + 10) / 1000.0), ).start() def counter(self, name, interval): self.running += 1 count = 0 for i in range(200): setattr(self, name, count) count += 1 sleep(interval) self.running -= 1 # Create the demo: demo = ThreadDemo() # Run the demo (if invoked from the command line): if __name__ == '__main__': demo.configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/examples/demo/Advanced/Multi_thread_demo_2.py0000644000175100001730000000547700000000000026535 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Monitoring a dynamic number of threads Shows a simple user interface being updated by a dynamic number of threads. When the *Create Threads* button is pressed, the *count* method is dispatched on a new thread. It then creates a new *Counter* object and adds it to the *counters* list (which causes the *Counter* to appear in the user interface. It then counts by incrementing the *Counter* object's *count* trait (which again causes a user interface update each time the counter is incremented). After it reaches its maximum count, it removes the *Counter* from the *counter* list (causing the counter to be removed from the user interface) and exits (terminating the thread). Note that repeated clicking of the *Create Thread* button will create additional threads. """ from time import sleep from traits.api import HasTraits, Int, Button, List from traitsui.api import View, Item, ListEditor # -- The Counter objects used to keep track of the current count ---------- class Counter(HasTraits): # The current count: count = Int() view = View(Item('count', style='readonly')) # -- The main 'ThreadDemo' class ------------------------------------------ class ThreadDemo(HasTraits): # The button used to start a new thread running: create = Button('Create Thread') # The set of counter objects currently running: counters = List(Counter) view = View( Item('create', show_label=False), '_', Item( 'counters', style='custom', show_label=False, editor=ListEditor(use_notebook=True, dock_style='tab'), ), resizable=True, width=300, height=150, title='Dynamic threads', ) def __init__(self, **traits): super().__init__(**traits) # Set up the notification handler for the 'Create Thread' button so # that it is run on a new thread: self.on_trait_change(self.count, 'create', dispatch='new') def count(self): """This method is dispatched on a new thread each time the 'Create Thread' button is pressed. """ counter = Counter() self.counters.append(counter) for i in range(1000): counter.count += 1 sleep(0.030) self.counters.remove(counter) # Create the demo: demo = ThreadDemo() # Run the demo (if invoked from the command line): if __name__ == '__main__': demo.configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/examples/demo/Advanced/NumPy_array_tabular_editor_demo.py0000644000175100001730000000440500000000000031207 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Displaying large NumPy arrays with TabularEditor A demonstration of how the TabularEditor can be used to display (large) NumPy arrays, in this case 100,000 random 3D points from a unit cube. In addition to showing the coordinates of each point, it also displays the index of each point in the array, as well as a red flag if the point lies within 0.25 of the center of the cube. """ from numpy import sqrt from numpy.random import random from traits.api import HasTraits, Property, Array from traitsui.api import View, Item, TabularAdapter, TabularEditor # -- Tabular Adapter Definition ------------------------------------------- class ArrayAdapter(TabularAdapter): columns = [('i', 'index'), ('x', 0), ('y', 1), ('z', 2)] font = 'Courier 10' alignment = 'right' format = '%.4f' index_text = Property() index_image = Property() def _get_index_text(self): return str(self.row) def _get_index_image(self): x, y, z = self.item if sqrt((x - 0.5) ** 2 + (y - 0.5) ** 2 + (z - 0.5) ** 2) <= 0.25: return '@icons:red_ball' return None # -- ShowArray Class Definition ------------------------------------------- class ShowArray(HasTraits): data = Array traits_view = View( Item( 'data', show_label=False, editor=TabularEditor( adapter=ArrayAdapter(), auto_resize=True, # Do not allow any kind of editing of the array: editable=False, operations=[], drag_move=False, ), ), title='Array Viewer', width=0.3, height=0.8, resizable=True, ) # Create the demo: demo = ShowArray(data=random((100000, 3))) # Run the demo (if invoked from the command line): if __name__ == '__main__': demo.configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/examples/demo/Advanced/NumPy_array_view_editor_demo.py0000644000175100001730000000547600000000000030540 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Using ArrayViewEditor to display large NumPy arrays in a table. This example displays 100,000 random 3D points from a unit cube. Note that the ArrayViewEditor has the following traits:: # Should an index column be displayed: show_index = Bool(True) # List of (optional) column titles: titles = List(Str) # Should the array be logically transposed: transpose = Bool(False) # The format used to display each array element: format = Str('%s') By default, the array row index will be shown in column one. If 'show_index' is False, then the row index column is omitted. If the list of 'titles' is empty, no column headers will be displayed. If the number of column headers is less than the number of array columns, then there are two cases: - If (number of array_columns) % (number of titles) == 0, then the titles are used to construct a series of repeating column headers with increasing subscripts (e.g. an (n x 6) array with titles of ['x','y','z'] would result in column headers of: 'x0', 'y0', 'z0', 'x1', 'y1', 'z1'). - In all other cases the titles are used as the column headers for the first set of columns, and the remaining column headers are set to the empty string (e.g. an (n x 5) array with titles of ['x','y','z'] would result in column headers of: 'x', 'y', 'z', '', ''). Setting 'transpose' to True will logically transpose the input array (e.g. an (3 x n) array will be displayed as an (n x 3) array). """ from numpy.random import random from traits.api import HasTraits, Array from traitsui.api import View, Item from traitsui.ui_editors.array_view_editor import ArrayViewEditor # -- ShowArray demo class ------------------------------------------------- class ShowArray(HasTraits): data = Array view = View( Item( 'data', show_label=False, editor=ArrayViewEditor( titles=['x', 'y', 'z'], format='%.4f', # Font fails with wx in OSX; # see traitsui issue #13: # font = 'Arial 8' ), ), title='Array Viewer', width=0.3, height=0.8, resizable=True, ) # -- Run the demo --------------------------------------------------------- # Create the demo: demo = ShowArray(data=random((100000, 3))) # Run the demo (if invoked from the command line): if __name__ == '__main__': demo.configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/examples/demo/Advanced/Popup_Dialog_demo.py0000644000175100001730000000640100000000000026241 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Demonstrates using a popup view within another view. Try changing the gender of the person from 'Male' to 'Female' using the drop-down list. When the person's gender is changed, a pop-up dialog is displayed immediately below the gender field providing you with the opportunity to cancel the gender change. If you click the Cancel button, the person's gender will return to its previous value. If you click anywhere else outside of the pop-up dialog, the pop-up dialog will simply disappear, leaving the person's new gender value as is. The main items of interest in this demo are: - The: kind = 'popup' trait set in the PersonHandler View which marks the view as being a popup view. - The parent = info.gender.control value passed to the edit_traits method when the popup dialog is created in the object_gender_changed method. This value specifies the control that the popup dialog should be positioned near. Notes: - This demo only works on the wx backend. - Traits UI will automatically position the popup dialog near the specified control in such a way that the pop-up dialog will not overlay the control and will be entirely on the screen (as long as these two conditions do not conflict). """ # -- Imports -------------------------------------------------------------- from traits.api import HasPrivateTraits, Str, Int, Enum, Instance, Button from traitsui.api import View, HGroup, Item, Handler, UIInfo, spring # -- The PersonHandler class ---------------------------------------------- class PersonHandler(Handler): # The UIInfo object associated with the view: info = Instance(UIInfo) # The cancel button used to revert an unintentional gender change: cancel = Button('Cancel') # The pop-up customization view: view = View( HGroup( spring, Item('cancel', show_label=False), ), kind='popup', ) # Event handlers: def object_gender_changed(self, info): if info.initialized: self.info = info self._ui = self.edit_traits(parent=info.gender.control) def _cancel_changed(self): object = self.info.object object.gender = ['Male', 'Female'][object.gender == 'Male'] self._ui.dispose() # -- The Person class ----------------------------------------------------- class Person(HasPrivateTraits): # The person's name, age and gender: name = Str() age = Int() gender = Enum('Male', 'Female') # The traits UI view: traits_view = View( Item('name'), Item('age'), Item('gender'), title='Button Popup Demo', handler=PersonHandler, ) # -- Create and run the demo ---------------------------------------------- # Create the demo: demo = Person(name='Mike Thomas', age=32) # Run the demo (if invoked from the command line): if __name__ == '__main__': demo.configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/examples/demo/Advanced/Property_List_demo.py0000644000175100001730000001120700000000000026476 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ This demo shows the proper way to create a **Property** whose value is a list, especially when the value of the **Property** will be used in a user interface, such as with the **TableEditor**. Most of the demo is just the machinery to set up the example. The key thing to note is the declaration of the *people* trait: people = Property( List, observe = 'ticker' ) In this case, by defining the **Property** as having a value of type **List**, you are ensuring that the computed value of the property will be validated using the **List** type, which in addition to verifying that the value is indeed a list, also guarantees that it will be converted to a **TraitListObject**, which is necessary for correct interaction with various Traits UI editors in a user interface. Note also the use of the *observe* metadata to trigger a trait property change whenever the *ticker* trait changes (in this case, it is changed every three seconds by a background thread). Finally, the use of the *@cached_property* decorator simplifies the implementation of the property by allowing the **_get_people** *getter* method to perform the expensive generation of a new list of people only when the *ticker* event fires, not every time it is accessed. """ from random import randint, choice from threading import Thread from time import sleep from traits.api import ( HasStrictTraits, HasPrivateTraits, Str, Int, Enum, List, Event, Property, cached_property, ) from traitsui.api import ObjectColumn, Item, TableEditor, View # -- Person Class --------------------------------------------------------- class Person(HasStrictTraits): """Defines some sample data to display in the TableEditor.""" name = Str() age = Int() gender = Enum('Male', 'Female') # -- PropertyListDemo Class ----------------------------------------------- class PropertyListDemo(HasPrivateTraits): """Displays a random list of Person objects in a TableEditor that is refreshed every 3 seconds by a background thread. """ # An event used to trigger a Property value update: ticker = Event() # The property being display in the TableEditor: people = Property(List, observe='ticker') # Tiny hack to allow starting the background thread easily: begin = Int() # -- Traits View Definitions ---------------------------------------------- traits_view = View( Item( 'people', show_label=False, editor=TableEditor( columns=[ ObjectColumn(name='name', editable=False, width=0.50), ObjectColumn(name='age', editable=False, width=0.15), ObjectColumn(name='gender', editable=False, width=0.35), ], auto_size=False, show_toolbar=False, sortable=False, ), ), title='Property List Demo', width=0.25, height=0.33, resizable=True, ) # -- Property Implementations --------------------------------------------- @cached_property def _get_people(self): """Returns the value for the 'people' property.""" return [ Person( name='%s %s' % ( choice(['Tom', 'Dick', 'Harry', 'Alice', 'Lia', 'Vibha']), choice(['Thomas', 'Jones', 'Smith', 'Adams', 'Johnson']), ), age=randint(21, 75), gender=choice(['Male', 'Female']), ) for i in range(randint(10, 20)) ] # -- Default Value Implementations ---------------------------------------- def _begin_default(self): """Starts the background thread running.""" thread = Thread(target=self._timer) thread.daemon = True thread.start() return 0 # -- Private Methods ------------------------------------------------------ def _timer(self): """Triggers a property update every 3 seconds for 30 seconds.""" for i in range(10): sleep(3) self.ticker = True # Create the demo: demo = PropertyListDemo() demo.begin # Run the demo (if invoked from the command line): if __name__ == '__main__': demo.configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/examples/demo/Advanced/Scrubber_editor_demo.py0000644000175100001730000001601100000000000026772 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ This example demonstrates several different variations on using the ScrubberEditor. A 'scrubber' is a type of widget often seen in certain types of applications, such as video editing, image editing, 3D graphics and animation. These types of programs often have many parameters defined over various ranges that the user can tweak to get varying effects or results. Because the number of parameters is typically fairly large, and the amount of screen real estate is fairly limited, these program often use 'scrubbers' to allow the user to adjust the parameter values. A scrubber often looks like a simple text field. The user can type in new values, if they need a precise setting, or simply drag the mouse over the value to set a new value, much like dragging a slider control. The visual feedback often comes in the form of seeing both the text value of the parameter change and the effect that the new parameter value has on the underlying model. For example, in a 3D graphics program, there might be a scrubber for controlling the rotation of the currently selected object around the Y-axis. As the user scrubs the rotation parameter, they also see the model spin on the screen as well. This visual feedback is what makes a scrubber more useful than a simple text entry field. And the fact that the scrubber takes up no more screen real estate that a text entry field is what makes it more useful than a full-fledged slider in space limited applications. The Traits UI ScrubberEditor works as follows: - When the mouse pointer moves over the scrubber, the cursor pointer changes shape to indicate that the field has some additional control behavior. - The control may optionally change color as well, to visually indicate that the control is 'live'. - If you simply click on the scrubber, an active text entry field is displayed, where you can type a new value for the trait, then press the Enter key. - If you click and drag while over the scrubber, the value of the trait is modified based on the direction you move the mouse. Right and/or up increases the value, left and/or down decreases the value. Holding the Shift key down while scrubbing causes the value to change by 10 times its normal amount. Holding the Control key down while scrubbing changes the value by 0.1 times its normal amount. - Scrubbing is not limited to the area of the scrubber control. You can drag as far as you want in any direction, subject to the maximum limits imposed by the trait or ScrubberEditor definition. The ScrubberEditor also supports several different style and functional variations: - The visual default is to display only the special scrubber pointer to indicate to the user that 'scrubber' functionality is available. - By specifying a 'hover_color' value, you can also have the editor change color when the mouse pointer is over it. - By specifying an 'active_color' value, you can have the editor change color while the user is scrubbing. - By specifying a 'border_color' value, you can display a solid border around the editor to mark it as something other than an ordinary text field. - By specifying an 'increment' value, you can tell the editor what the normal increment value for the scrubber should be. Otherwise, the editor will calculate the increment value itself. Explicitly specifying an increment can be very useful in cases where the underlying trait has an unbounded value, which makes it difficult for the editor to determine what a reasonable increment value might be. - The editor will also correctly handle traits with dynamic ranges (i.e. ranges whose high and low limits are defined by other traits). Besides correctly handling the range limits, the editor will also adjust the default tooltip to display the current range of the scrubber. In this example, several of the variations described above are shown: - A simple integer range with default visual cues. - A float range with both 'hover_color' and 'active_color' values specified. - An unbounded range with a 'border_color' value specified. - A dynamic range. This consists of three scrubbers: one to control the low end of the range, one to control the high end, and one that uses the high and low values to determine its range. For comparison purposes, the example also shows the same traits displayed using their default editors. Notes: - This demo only works on the wx backend. """ # -- Imports -------------------------------------------------------------- from traits.api import HasTraits, Range, Float from traitsui.api import View, VGroup, HGroup, Item, ScrubberEditor, spring # -- Shared Item Definition ---------------------------------------- class TItem(Item): editor = ScrubberEditor() # -- ScrubberDemo Class --------------------------------------------------- class ScrubberDemo(HasTraits): # Define some sample ranges and values: simple_integer = Range(0, 100) rollover_float = Range(-10.0, 10.0) bordered_unbounded = Float() dynamic_low = Range(high=-0.01, value=-10.0) dynamic_high = Range(low=0.01, value=10.0) dynamic_value = Range('dynamic_low', 'dynamic_high', 0.0) # Define the demo view: view = View( HGroup( VGroup( Item('simple_integer', editor=ScrubberEditor()), Item( 'rollover_float', editor=ScrubberEditor( hover_color=0xFFFFFF, active_color=0xA0CD9E ), ), Item( 'bordered_unbounded', editor=ScrubberEditor( hover_color=0xFFFFFF, active_color=0xA0CD9E, border_color=0x808080, ), ), Item('dynamic_low', editor=ScrubberEditor()), Item('dynamic_high', editor=ScrubberEditor()), Item('dynamic_value', editor=ScrubberEditor()), show_border=True, label='Scrubber Editors', ), VGroup( Item('simple_integer'), Item('rollover_float'), Item('bordered_unbounded'), Item('dynamic_low'), Item('dynamic_high'), Item('dynamic_value'), show_border=True, label='Default Editors', ), spring, ), title='Scrubber Editor Demo', ) # -- Create and run the demo ---------------------------------------------- # Create the demo: demo = ScrubberDemo() # Run the demo (if invoked from the command line): if __name__ == '__main__': demo.configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/examples/demo/Advanced/Settable_cached_property.py0000644000175100001730000001115700000000000027655 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Creating settable cached Property in Traits How to create a Traits Property which is cached but is not read-only. The example demonstrates how to create a 'settable cached' property. The example itself is nonsensical, and is provided mainly to show how to set up such a property, and how it differs from a standard 'cached' property. A normal 'cached' property does not have a 'setter' method, meaning that the property is read-only. It usually represents a value which depends upon the state of other traits for its value. The cached property provides both a mechanism for remembering (i.e. caching) the current value of the property as well as a means of automatically detecting when the value of the property changes (which causes the cache to be flushed), and notifying any associated trait listeners that the value of the property has changed. Normally there is no 'setter' for such a property because the value is derived from the value of other traits. However, it is possible to define a 'settable cached' property which in addition to the capabilities of a normal 'cached' property, also allows the property's value to be explicitly set. To accomplish this, simply add a '_set_*' method as usual for a settable property (see the '_set_c' method in the example code) which takes the value provided and changes the values the property depends on appropriately based on the new value. This allows code to set the value of the property directly if desired, subject to any constraints specified in the property declaration. For example, in the example, the 'c' trait is a 'settable cached' property whose value must be an integer. Attempting to set a non-integer value of the property will raise an exception, just like any other trait would. If any of the traits which the property depends upon change value, the current value of the property will be flushed from the cache and a change notification for the property will be generated. Any code that then attempts to read the value of the property will result in the cache being reloaded with the new value returned by the property's 'getter' method. In the example, trait 'c' is a 'settable cached' property which returns the product of 'a' times 'b', and trait 'd' is a normal 'cached' property that returns double the value of 'c'. To see the effect of these traits in action, try moving the 'a' and 'b' sliders and watching the 'c' and 'd' traits update. This demonstrates how 'c' and 'd' are properties that depend upon the values of other traits. Now try changing the value of the 'c' trait by moving the slider or typing a new value into the text entry field. You will see that the 'd' trait updates as well, illustrating that the 'c' trait can be set directly, as well as indirectly by changes to 'a' and 'b'. Also, try typing non-numeric values into the 'c' field and you will see that any values set are being type checked as well (i.e. they must be integer values). Now try typing a value into the 'd' trait and you will see that an error results (indicated by the text entry field turning red), because this is a normal 'cached' trait that has no 'setter' method defined. """ from math import sqrt from traits.api import HasTraits, Int, Range, Property, cached_property from traitsui.api import View, Item, RangeEditor # -- Demo Class ----------------------------------------------------------- class SettableCachedProperty(HasTraits): a = Range(1, 10) b = Range(1, 10) c = Property(Int, observe=['a', 'b']) d = Property(observe='c') view = View( Item('a'), Item('b'), '_', Item('c', editor=RangeEditor(low=1, high=100, mode='slider')), Item('c'), '_', Item('d', editor=RangeEditor(low=1, high=400, mode='slider')), Item('d'), width=0.3, ) @cached_property def _get_c(self): return self.a * self.b def _set_c(self, value): self.a = int(sqrt(value)) self.b = int(sqrt(value)) @cached_property def _get_d(self): return self.c + self.c # -- Run the demo --------------------------------------------------------- # Create the demo: demo = SettableCachedProperty() # Run the demo (if invoked from the command line): if __name__ == '__main__': demo.configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/examples/demo/Advanced/Statusbar_demo.py0000644000175100001730000000531200000000000025627 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Displaying a statusbar in a Traits UI A statusbar may contain one or more fields, of fixed or variable size. Fixed width fields are specified in pixels, while variable width fields are specified as fractional values relative to other variable width fields. The content of a statusbar field is specified via the extended trait name of the object attribute that will contain the statusbar information. In this example, there are two statusbar fields: - The current length of the text input data (variable width) - The current time (fixed width, updated once per second). Note the use of a timer thread to update the status bar once per second. """ from time import sleep, strftime from threading import Thread from traits.api import HasPrivateTraits, Str, Property from traitsui.api import View, Item, StatusItem, Label # -- The demo class ------------------------------------------------------- class TextEditor(HasPrivateTraits): # The text being edited: text = Str() # The current length of the text being edited: length = Property(observe='text') # The current time: time = Str() # The view definition: view = View( Label('Type into the text editor box:'), Item('text', style='custom', show_label=False), title='Text Editor', id='traitsui.demo.advanced.statusbar_demo', width=0.4, height=0.4, resizable=True, statusbar=[ StatusItem(name='length', width=0.5), StatusItem(name='time', width=85), ], ) # -- Property Implementations --------------------------------------------- def _get_length(self): return 'Length: %d characters' % len(self.text) # -- Default Trait Values ------------------------------------------------- def _time_default(self): thread = Thread(target=self._clock) thread.daemon = True thread.start() return '' # -- Private Methods ------------------------------------------------------ def _clock(self): """Update the statusbar time once every second.""" while True: self.time = strftime('%I:%M:%S %p') sleep(1.0) # Create the demo object: popup = TextEditor() # Run the demo (if invoked from the command line): if __name__ == '__main__': popup.configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/examples/demo/Advanced/String_list_ui_editor.py0000644000175100001730000001072500000000000027223 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ **WARNING** This demo might not work as expected and some documented features might be missing. ------------------------------------------------------------------------------- Another demo showing how to use a TabularEditor to create a multi-select list box. This demo creates a reusable StringListEditor class and uses that instead of defining the editor as part of the demo class. This approach greatly simplifies the actual demo class and shows how to construct a reusable Traits UI-based editor that can be used in other applications. """ # Issue related to the demo warning: enthought/traitsui#960 from traits.api import HasPrivateTraits, List, Str, Property, observe from traits.etsconfig.api import ETSConfig from traitsui.api import ( BasicEditorFactory, HGroup, Item, TabularAdapter, TabularEditor, View, ) if ETSConfig.toolkit == 'wx': from traitsui.wx.ui_editor import UIEditor else: from traitsui.qt.ui_editor import UIEditor # -- Define the reusable StringListEditor class and its helper classes -------- # Define the tabular adapter used by the Traits UI string list editor: class MultiSelectAdapter(TabularAdapter): # The columns in the table (just the string value): columns = [('Value', 'value')] # The text property used for the 'value' column: value_text = Property() def _get_value_text(self): return self.item # Define the actual Traits UI string list editor: class _StringListEditor(UIEditor): # Indicate that the editor is scrollable/resizable: scrollable = True # The list of available editor choices: choices = List(Str) # The list of currently selected items: selected = List(Str) # The traits UI view used by the editor: traits_view = View( Item( 'choices', show_label=False, editor=TabularEditor( show_titles=False, selected='selected', editable=False, multi_select=True, adapter=MultiSelectAdapter(), ), ), id='string_list_editor', resizable=True, ) def init_ui(self, parent): self.sync_value(self.factory.choices, 'choices', 'from', is_list=True) self.selected = self.value return self.edit_traits(parent=parent, kind='subpanel') @observe('selected') def _selected_modified(self, event): self.value = self.selected # Define the StringListEditor class used by client code: class StringListEditor(BasicEditorFactory): # The editor implementation class: klass = _StringListEditor # The extended trait name containing the editor's set of choices: choices = Str() # -- Define the demo class ---------------------------------------------------- class MultiSelect(HasPrivateTraits): """This class demonstrates using the StringListEditor to select a set of string values from a set of choices. """ # The list of choices to select from: choices = List(Str) # The currently selected list of choices: selected = List(Str) # A dummy result so that we can display the selection using the same # StringListEditor: result = List(Str) # A traits view showing the list of choices on the left-hand side, and # the currently selected choices on the right-hand side: traits_view = View( HGroup( Item( 'selected', show_label=False, editor=StringListEditor(choices='choices'), ), Item( 'result', show_label=False, editor=StringListEditor(choices='selected'), ), ), width=0.20, height=0.25, ) # Create the demo: demo = MultiSelect( choices=[ 'one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine', 'ten', ], selected=['two', 'five', 'nine'], ) # Run the demo (if invoked from the command line): if __name__ == '__main__': demo.configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/examples/demo/Advanced/Table_editor_with_checkbox_column.py0000644000175100001730000000743300000000000031534 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ This shows a table editor which has a checkbox column in addition to normal data columns. """ from random import randint from traits.api import HasStrictTraits, Str, Int, Float, List, Bool, Property from traitsui.api import Item, ObjectColumn, TableEditor, View from traitsui.extras.checkbox_column import CheckboxColumn # Create a specialized column to set the text color differently based upon # whether or not the player is in the lineup: class PlayerColumn(ObjectColumn): # Override some default settings for the column: width = 0.08 horizontal_alignment = 'center' def get_text_color(self, object): if object.in_lineup: return 'black' return 'light grey' # The 'players' trait table editor: player_editor = TableEditor( sortable=False, configurable=False, auto_size=False, selected_indices='selected_player_indices', columns=[ CheckboxColumn(name='in_lineup', label='In Lineup', width=0.12), PlayerColumn( name='name', editable=False, width=0.24, horizontal_alignment='left', ), PlayerColumn(name='at_bats', label='AB'), PlayerColumn(name='strike_outs', label='SO'), PlayerColumn(name='singles', label='S'), PlayerColumn(name='doubles', label='D'), PlayerColumn(name='triples', label='T'), PlayerColumn(name='home_runs', label='HR'), PlayerColumn(name='walks', label='W'), PlayerColumn( name='average', label='Ave', editable=False, format='%0.3f' ), ], ) class Player(HasStrictTraits): # Trait definitions: in_lineup = Bool(True) name = Str() at_bats = Int() strike_outs = Int() singles = Int() doubles = Int() triples = Int() home_runs = Int() walks = Int() average = Property(Float) def _get_average(self): """Computes the player's batting average from the current statistics.""" if self.at_bats == 0: return 0.0 return ( float(self.singles + self.doubles + self.triples + self.home_runs) / self.at_bats ) class Team(HasStrictTraits): # Trait definitions: players = List(Player) selected_player_indices = List() # Trait view definitions: traits_view = View( Item('players', show_label=False, editor=player_editor), title='Baseball Team Roster Demo', width=0.5, height=0.5, resizable=True, ) def random_player(name): """Generates and returns a random player.""" p = Player( name=name, strike_outs=randint(0, 50), singles=randint(0, 50), doubles=randint(0, 20), triples=randint(0, 5), home_runs=randint(0, 30), walks=randint(0, 50), ) return p.trait_set( at_bats=( p.strike_outs + p.singles + p.doubles + p.triples + p.home_runs + randint(100, 200) ) ) # Create the demo: demo = Team( players=[ random_player(name) for name in [ 'Dave', 'Mike', 'Joe', 'Tom', 'Dick', 'Harry', 'Dirk', 'Fields', 'Stretch', ] ] ) # Run the demo (if invoked from the command line): if __name__ == '__main__': demo.configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/examples/demo/Advanced/Table_editor_with_context_menu_demo.py0000644000175100001730000001206500000000000032102 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Defining column-specific context menu in a Table Shows a TableEditor which has column-specific context menus. The demo is a simple baseball scoring system, which lists each player and their current batting statistics. After a given player has an at bat, you right-click on the table cell corresponding to the player and the result of the at-bat (e.g. 'S' = single) and select the 'Add' menu option to register that the player hit a single and update the player's overall statistics. This demo also illustrates the use of Property traits, and how using 'event' meta-data can simplify event handling by collapsing an event that can occur on a number of traits into a category of event, which can be handled by a single event handler defined for the category (in this case, the category is 'affects_average'). """ from random import randint from traits.api import HasStrictTraits, Str, Int, Float, List, Property from traitsui.api import Action, Item, Menu, ObjectColumn, TableEditor, View # Define a custom table column for handling items which affect the player's # batting average: class AffectsAverageColumn(ObjectColumn): # Define the context menu for the column: menu = Menu( Action(name='Add', action='column.add(object)'), Action(name='Sub', action='column.sub(object)'), ) # Right-align numeric values (override): horizontal_alignment = 'center' # Column width (override): width = 0.09 # Don't allow the data to be edited directly: editable = False # Action methods for the context menu items: def add(self, object): """Increment the affected player statistic.""" setattr(object, self.name, getattr(object, self.name) + 1) def sub(self, object): """Decrement the affected player statistic.""" setattr(object, self.name, getattr(object, self.name) - 1) # The 'players' trait table editor: player_editor = TableEditor( editable=True, sortable=False, auto_size=False, columns=[ ObjectColumn(name='name', editable=False, width=0.28), AffectsAverageColumn(name='at_bats', label='AB'), AffectsAverageColumn(name='strike_outs', label='SO'), AffectsAverageColumn(name='singles', label='S'), AffectsAverageColumn(name='doubles', label='D'), AffectsAverageColumn(name='triples', label='T'), AffectsAverageColumn(name='home_runs', label='HR'), AffectsAverageColumn(name='walks', label='W'), ObjectColumn( name='average', label='Ave', editable=False, width=0.09, horizontal_alignment='center', format='%0.3f', ), ], ) # 'Player' class: class Player(HasStrictTraits): # Trait definitions: name = Str() at_bats = Int() strike_outs = Int(event='affects_average') singles = Int(event='affects_average') doubles = Int(event='affects_average') triples = Int(event='affects_average') home_runs = Int(event='affects_average') walks = Int() average = Property(Float) def _get_average(self): """Computes the player's batting average from the current statistics.""" if self.at_bats == 0: return 0.0 return ( float(self.singles + self.doubles + self.triples + self.home_runs) / self.at_bats ) def _affects_average_changed(self): """Handles an event that affects the player's batting average.""" self.at_bats += 1 class Team(HasStrictTraits): # Trait definitions: players = List(Player) # Trait view definitions: traits_view = View( Item('players', show_label=False, editor=player_editor), title='Baseball Scoring Demo', width=0.5, height=0.5, resizable=True, ) def random_player(name): """Generates and returns a random player.""" p = Player( name=name, strike_outs=randint(0, 50), singles=randint(0, 50), doubles=randint(0, 20), triples=randint(0, 5), home_runs=randint(0, 30), walks=randint(0, 50), ) return p.trait_set( at_bats=( p.strike_outs + p.singles + p.doubles + p.triples + p.home_runs + randint(100, 200) ) ) # Create the demo: demo = Team( players=[ random_player(name) for name in [ 'Dave', 'Mike', 'Joe', 'Tom', 'Dick', 'Harry', 'Dirk', 'Fields', 'Stretch', ] ] ) # Run the demo (if invoked from the command line): if __name__ == '__main__': demo.configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/examples/demo/Advanced/Table_editor_with_live_search_and_cell_editor.py0000644000175100001730000003512200000000000034040 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ This example demonstrates how to implement 'live search' using a TableEditor, as well as how to embed more sophisticated editors, such as a CodeEditor, within a table cell. This example also makes extensive use of cached properties. The example is a fairly simple source code file search utility. You determine which files to search and what to search for using the various controls spread across the top line of the view. You specify the root directory to search for files in using the 'Path' field. You can either: - Type in a directory name. - Click the '...' button and select a directory from the drop-down tree view that is displayed. - Click on the directory name drop-down to display a history list of the 10 most recently visited directories, and select a directory from the list. You can specify whether sub-directories should be included or not by toggling the 'Recursive' checkbox on or off. You can specify the types of files to be searched by clicking on the 'Type' drop-down and selecting a file type such as Python or C from the list. You can specify what string to search for by typing into the 'Search' field. The set of source files containing the search string is automatically updated as you type (this is the 'live search' feature). The search field also maintains a history of previous searches, so you can click on the drop-down arrow to display and select a previous search. Note that entries are only added to the search history when the enter key is pressed. This prevents each partial substring typed from being added to the history as a separate entry. You can specify whether the search is case sensitive or not by toggling the 'Case sensitive' checkbox on or off. The results of the search are displayed in a table below the input fields. The table contains four columns: - #: The number of lines matching the search string in the file. - Matches: A list of all lines containing search string matches in the file. Normally, only the first match is displayed, but you can click on this field to display the entire list of matches (the table row will expand and a CodeEditor will be displayed showing the complete list of matching source code file lines). You can click on or cursor to lines in the code editor to display the corresponding source code line in context in the code editor that appears at the bottom of the view. - Name: Displays the base name of the source file with no path information. - Path: Displays the portion of the source file path not included in the the root directory being used for the search. Selecting a line in the table editor will display the contents of the corresponding source file in the Code Editor displayed at the bottom of the view. After clicking on a 'Matches' column entry you can use the cursor up and down keys to select the various matching source code lines displayed in the table cell editor. You can move to the next or previous 'Matches' entry by pressing the Ctrl-Up and Ctrl-Down cursor keys. You can also use the Ctrl-Left and Ctrl-Right cursor keys to move to the previous or next column on the same line. You can also exit the 'Matches' code editor by pressing the Escape key. Finally: - You can click and drag the little circle to the right of the currently selected file to drag and drop the file. This can be useful, for example, to drag and drop the file into your favorite text editor. - Similarly, you can also drag the contents of the 'Name' column into your favorite text editor to edit the file corresponding to that line in the table. """ # -- Imports -------------------------------------------------------------- from os import walk, listdir from os.path import basename, dirname, splitext, join from traits.api import ( Any, Bool, Directory, Enum, File, HasTraits, Instance, Int, List, Property, Str, cached_property, observe, ) from traitsui.api import ( CodeEditor, DNDEditor, HGroup, HistoryEditor, Item, TableEditor, TableFilter, TitleEditor, VGroup, View, VSplit, ) from traitsui.table_column import ObjectColumn from io import open # -- Constants ------------------------------------------------------------ FileTypes = { 'Python': ['.py'], 'C': ['.c', '.h'], 'C++': ['.cpp', '.h'], 'Java': ['.java'], 'Ruby': ['.rb'], } DEFAULT_ROOT = dirname(__file__) # -- The Live Search table editor definition ------------------------------ class MatchesColumn1(ObjectColumn): def get_value(self, object): n = len(self.get_raw_value(object)) if n == 0: return '' return str(n) class MatchesColumn2(ObjectColumn): def is_editable(self, object): return len(object.matches) > 0 class FileColumn(ObjectColumn): def get_drag_value(self, object): return object.full_name table_editor = TableEditor( columns=[ MatchesColumn1( name='matches', label='#', editable=False, width=0.05, horizontal_alignment='center', ), MatchesColumn2( name='matches', width=0.35, format_func=lambda x: (x + [''])[0].strip(), editor=CodeEditor( line='object.live_search.selected_match', selected_line='object.live_search.selected_match', ), style='readonly', edit_width=0.95, edit_height=0.33, ), FileColumn(name='base_name', label='Name', width=0.30, editable=False), ObjectColumn( name='ext_path', label='Path', width=0.30, editable=False ), ], filter_name='filter', auto_size=False, show_toolbar=False, selected='selected', selection_color=0x000000, selection_bg_color=0xFBD391, ) # -- LiveSearch class ----------------------------------------------------- class LiveSearch(HasTraits): # The currenty root directory being searched: root = Directory(DEFAULT_ROOT, entries=10) # Should sub directories be included in the search: recursive = Bool(True) # The file types to include in the search: file_type = Enum('Python', 'C', 'C++', 'Java', 'Ruby') # The current search string: search = Str() # Is the search case sensitive? case_sensitive = Bool(False) # The live search table filter: filter = Property(Instance(TableFilter), observe='search, case_sensitive') # The current list of source files being searched: source_files = Property( List(Instance("SourceFile")), observe='root, recursive, file_type', ) # The currently selected source file: selected = Instance("SourceFile") # The contents of the currently selected source file: selected_contents = Property(List(Str), observe='selected') # The currently selected match: selected_match = Int() # The text line corresponding to the selected match: selected_line = Property(Int, observe='selected, selected_match') # The full name of the currently selected source file: selected_full_name = Property(File, observe='selected.full_name') # The list of marked lines for the currently selected file: mark_lines = Property(List(Int), observe='selected') # Summary of current number of files and matches: summary = Property(Str, observe='source_files, search, case_sensitive') # -- Traits UI Views ------------------------------------------------------ view = View( VGroup( HGroup( Item('root', id='root', label='Path', width=0.5), Item('recursive'), Item('file_type', label='Type'), Item( 'search', id='search', width=0.5, editor=HistoryEditor(auto_set=True), ), Item('case_sensitive'), ), VSplit( VGroup( Item('summary', editor=TitleEditor()), Item( 'source_files', id='source_files', editor=table_editor ), dock='horizontal', show_labels=False, ), VGroup( HGroup( Item( 'selected_full_name', editor=TitleEditor(), springy=True, ), Item( 'selected_full_name', editor=DNDEditor(), tooltip='Drag this file', ), show_labels=False, ), Item( 'selected_contents', style='readonly', editor=CodeEditor( mark_lines='mark_lines', line='selected_line', selected_line='selected_line', ), ), dock='horizontal', show_labels=False, ), id='splitter', ), ), title='Live File Search', id='enthought.examples.demo.Advanced.' 'Table_editor_with_live_search_and_cell_editor.LiveSearch', width=0.75, height=0.67, resizable=True, ) # -- Property Implementations --------------------------------------------- @cached_property def _get_filter(self): if len(self.search) == 0: return lambda x: True return lambda x: len(x.matches) > 0 @cached_property def _get_source_files(self): root = self.root if root == '': root = DEFAULT_ROOT file_types = FileTypes[self.file_type] if self.recursive: result = [] for dir_path, dir_names, file_names in walk(root): for file_name in file_names: if splitext(file_name)[1] in file_types: result.append( SourceFile( live_search=self, full_name=join(dir_path, file_name), ) ) return result return [ SourceFile(live_search=self, full_name=join(root, file_name)) for file_name in listdir(root) if splitext(file_name)[1] in file_types ] def _get_selected_contents(self): if self.selected is None: return '' return ''.join(self.selected.contents) @cached_property def _get_mark_lines(self): if self.selected is None: return [] return [int(match.split(':', 1)[0]) for match in self.selected.matches] @cached_property def _get_selected_line(self): selected = self.selected if (selected is None) or (len(selected.matches) == 0): return 1 return int(selected.matches[self.selected_match - 1].split(':', 1)[0]) @cached_property def _get_selected_full_name(self): if self.selected is None: return '' return self.selected.full_name @cached_property def _get_summary(self): source_files = self.source_files search = self.search if search == '': return 'A total of %d files.' % len(source_files) files = 0 matches = 0 for source_file in source_files: n = len(source_file.matches) if n > 0: files += 1 matches += n return 'A total of %d files with %d files containing %d matches.' % ( len(source_files), files, matches, ) # -- Traits Event Handlers ------------------------------------------------ @observe('selected') def _update_selected_match(self, event): self.selected_match = 1 @observe('source_files') def _update_selected_file(self, event): if len(self.source_files) > 0: self.selected = self.source_files[0] else: self.selected = None # -- SourceFile class ----------------------------------------------------- class SourceFile(HasTraits): # The search object this source file is associated with: live_search = Instance(LiveSearch) # The full path and file name of the source file: full_name = File() # The base file name of the source file: base_name = Property(Str, observe='full_name') # The portion of the file path beyond the root search path: ext_path = Property(Str, observe='full_name') # The contents of the source file: contents = Property(List(Str), observe='full_name') # The list of matches for the current search criteria: matches = Property(List(Str), observe='full_name, live_search.[search, case_sensitive]') # -- Property Implementations --------------------------------------------- def _get_base_name(self): return basename(self.full_name) def _get_ext_path(self): return dirname(self.full_name)[len(self.live_search.root) :] @cached_property def _get_contents(self): try: with open(self.full_name, 'rU', encoding='utf8') as fh: contents = fh.readlines() return contents except Exception: return '' @cached_property def _get_matches(self): search = self.live_search.search if search == '': return [] case_sensitive = self.live_search.case_sensitive if case_sensitive: return [ '%5d: %s' % ((i + 1), line.strip()) for i, line in enumerate(self.contents) if line.find(search) >= 0 ] search = search.lower() return [ '%5d: %s' % ((i + 1), line.strip()) for i, line in enumerate(self.contents) if line.lower().find(search) >= 0 ] # -- Set up and run the demo ---------------------------------------------- # Create the demo object: demo = LiveSearch() # Run the demo (if invoked from the command line): if __name__ == '__main__': demo.configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/examples/demo/Advanced/Table_editor_with_progress_column.py0000644000175100001730000000360400000000000031606 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ A demo to demonstrate the progress column in a TableEditor. The demo only works with qt backend. """ import random from pyface.api import GUI from traits.api import Button, HasTraits, Instance, Int, List, observe, Str from traitsui.api import ObjectColumn, TableEditor, UItem, View from traitsui.extras.progress_column import ProgressColumn class Job(HasTraits): name = Str() percent_complete = Int() class JobManager(HasTraits): jobs = List(Instance(Job)) start = Button() def populate(self): self.jobs = [ Job(name='job %02d' % i, percent_complete=0) for i in range(1, 25) ] def process(self): for job in self.jobs: job.percent_complete = min( job.percent_complete + random.randint(0, 3), 100 ) if any(job.percent_complete < 100 for job in self.jobs): GUI.invoke_after(100, self.process) @observe('start') def _populate_and_process(self, event): self.populate() GUI.invoke_after(1000, self.process) traits_view = View( UItem( 'jobs', editor=TableEditor( columns=[ ObjectColumn(name='name'), ProgressColumn(name='percent_complete'), ] ), ), UItem('start'), resizable=True, ) demo = JobManager() # Run the demo (if invoked from the command line): if __name__ == '__main__': demo.configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/examples/demo/Advanced/Tabular_editor_demo.py0000644000175100001730000003010700000000000026617 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ **WARNING** This demo might not work as expected and some documented features might be missing. ------------------------------------------------------------------------------- Tabular editor The TabularEditor can be used for many of the same purposes as the TableEditor, that is, for displaying a table of attributes of lists or arrays of objects. While similar in function, the tabular editor has advantages and disadvantages relative to the table editor. See the Traits UI User Manual for details. This example defines three classes: - *Person*: A single person. - *MarriedPerson*: A married person (subclass of Person). - *Report*: Defines a report based on a list of single and married people. It creates a tabular display of 10,000 single and married people showing the following information: - Name of the person. - Age of the person. - The person's address. - The name of the person's spouse (if any). In addition: - It uses a Courier 10 point font for each line in the table. - It displays age column right, instead of left, justified. - If the person is a minor (age < 18) and married, it displays a red flag image in the age column. - If the person is married, it makes the background color for that row a light blue. - If this demo is running under QT, it displays each person's surname in a row label. This example demonstrates: - How to set up a *TabularEditor*. - The display speed of the *TabularEditor*. - How to create a *TabularAdapter* that meets each of the specified display requirements. Additional notes: - You can change the current selection using the up and down arrow keys. - If the demo is running under WX, you can move a selected row up and down in the table using the left and right arrow keys. - If the demo is running under QT, you can move rows by clicking and dragging. Hopefully, this simple example conveys some of the power and flexibility that the `TabularAdapter` class provides you. But, just in case it doesn't, let's go over some of the more interesting details: - Note the values in the `~TabularAdapter.columns` trait. The first three values define *column ids* which map directly to traits defined on our data objects, while the last one defines an arbitrary string which we define so that we can reference it in the `MarriedPerson_spouse_text` and `Person_spouse_text` trait definitions. - Since the font we want to use applies to all table rows, we just specify a new default value for the existing `TabularAdapter.font` trait. - Since we only want to override the default left alignment for the age column, we simply define an `age_alignment` trait as a constant ``'right'`` value. We could have also used ``age_alignment = Str('right')``, but `Constant` never requires storage to be used in an object. - We define the `MarriedPerson_age_image` property to handle putting the ``'red flag'`` image in the age column. By including the class name of the items it applies to, we only need to check the `age` value in determining what value to return. - Similary, we use the `MarriedPerson_bg_color` trait to ensure that each `MarriedPerson` object has the correct background color in the table. - Finally, we use the `MarriedPerson_spouse_text` and `Person_spouse_text` traits, one a property and the other a simple constant value, to determine what text to display in the *Spouse* column for the different object types. Note that even though a `MarriedPerson` is both a `Person` and a `MarriedPerson`, it will correctly use the `MarriedPerson_spouse_text` trait since the search for a matching trait is always made in *mro* order. Although this is completely subjective, some of the things that the author feels stand out about this approach are: - The class definition is short and sweet. Less code is good. - The bulk of the code is declarative. Less room for logic errors. - There is only one bit of logic in the class (the ``if`` statement in the `MarriedPerson_age_image` property implementation). Again, less logic usually translates into more reliable code). - The defined traits and even the property implementation method names read very descriptively. `_get_MarriedPerson_age_image` pretty much says what you would write in a comment or doc string. The implementation almost is the documentation. """ # Issue related to the demo warning: enthought/traitsui#960 from functools import partial from random import randint, choice, shuffle from traits.api import HasTraits, Str, Int, List, Instance, Property, Constant from traits.etsconfig.api import ETSConfig from traitsui.api import ( View, Group, Item, TabularAdapter, TabularEditor, Color, ) # -- Person Class Definition ---------------------------------------------- class Person(HasTraits): name = Str() address = Str() age = Int() # surname is displayed in qt-only row label: surname = Property(fget=lambda self: self.name.split()[-1], observe='name') # -- MarriedPerson Class Definition --------------------------------------- class MarriedPerson(Person): partner = Instance(Person) # -- Tabular Adapter Definition ------------------------------------------- class ReportAdapter(TabularAdapter): """The tabular adapter interfaces between the tabular editor and the data being displayed. For more details, please refer to the traitsUI user guide. """ # List of (Column labels, Column ID). columns = [ ('Name', 'name'), ('Age', 'age'), ('Address', 'address'), ('Spouse', 'spouse'), ] row_label_name = 'surname' # Interfacing between the model and the view: make some of the cell # attributes a property whose behavior is then controlled by the get # (and optionally set methods). The cell is identified by its column # ID (age, spouse). # Overwrite default value font = 'Courier 10' age_alignment = Constant('right') MarriedPerson_age_image = Property() MarriedPerson_bg_color = Color(0xE0E0FF) MarriedPerson_spouse_text = Property() Person_spouse_text = Constant('') def _get_MarriedPerson_age_image(self): if self.item.age < 18: return '@icons:red_ball' return None def _get_MarriedPerson_spouse_text(self): return self.item.partner.name # -- Tabular Editor Definition -------------------------------------------- # The tabular editor works in conjunction with an adapter class, derived from # TabularAdapter. tabular_editor = TabularEditor( adapter=ReportAdapter(), operations=['move', 'edit'], # Row titles are not supported in WX: show_row_titles=(ETSConfig.toolkit in {"qt", "qt4"}), ) # -- Report Class Definition ---------------------------------------------- class Report(HasTraits): people = List(Person) traits_view = View( Group( Item('people', id='table', editor=tabular_editor), show_labels=False, ), title='Tabular Editor Demo', id='traitsui.demo.Applications.tabular_editor_demo', width=0.60, height=0.75, resizable=True, ) # -- Generate 10,000 random single and married people --------------------- male_names = [ 'Michael', 'Edward', 'Timothy', 'James', 'George', 'Ralph', 'David', 'Martin', 'Bryce', 'Richard', 'Eric', 'Travis', 'Robert', 'Bryan', 'Alan', 'Harold', 'John', 'Stephen', 'Gael', 'Frederic', 'Eli', 'Scott', 'Samuel', 'Alexander', 'Tobias', 'Sven', 'Peter', 'Albert', 'Thomas', 'Horatio', 'Julius', 'Henry', 'Walter', 'Woodrow', 'Dylan', 'Elmer', ] female_names = [ 'Leah', 'Jaya', 'Katrina', 'Vibha', 'Diane', 'Lisa', 'Jean', 'Alice', 'Rebecca', 'Delia', 'Christine', 'Marie', 'Dorothy', 'Ellen', 'Victoria', 'Elizabeth', 'Margaret', 'Joyce', 'Sally', 'Ethel', 'Esther', 'Suzanne', 'Monica', 'Hortense', 'Samantha', 'Tabitha', 'Judith', 'Ariel', 'Helen', 'Mary', 'Jane', 'Janet', 'Jennifer', 'Rita', 'Rena', 'Rianna', ] all_names = male_names + female_names male_name = partial(choice, male_names) female_name = partial(choice, female_names) any_name = partial(choice, all_names) age = partial(randint, 15, 72) def family_name(): return choice( [ 'Jones', 'Smith', 'Thompson', 'Hayes', 'Thomas', 'Boyle', "O'Reilly", 'Lebowski', 'Lennon', 'Starr', 'McCartney', 'Harrison', 'Harrelson', 'Steinbeck', 'Rand', 'Hemingway', 'Zhivago', 'Clemens', 'Heinlien', 'Farmer', 'Niven', 'Van Vogt', 'Sturbridge', 'Washington', 'Adams', 'Bush', 'Kennedy', 'Ford', 'Lincoln', 'Jackson', 'Johnson', 'Eisenhower', 'Truman', 'Roosevelt', 'Wilson', 'Coolidge', 'Mack', 'Moon', 'Monroe', 'Springsteen', 'Rigby', "O'Neil", 'Philips', 'Clinton', 'Clapton', 'Santana', 'Midler', 'Flack', 'Conner', 'Bond', 'Seinfeld', 'Costanza', 'Kramer', 'Falk', 'Moore', 'Cramdon', 'Baird', 'Baer', 'Spears', 'Simmons', 'Roberts', 'Michaels', 'Stuart', 'Montague', 'Miller', ] ) def address(): number = randint(11, 999) text_1 = choice( [ 'Spring', 'Summer', 'Moonlight', 'Winding', 'Windy', 'Whispering', 'Falling', 'Roaring', 'Hummingbird', 'Mockingbird', 'Bluebird', 'Robin', 'Babbling', 'Cedar', 'Pine', 'Ash', 'Maple', 'Oak', 'Birch', 'Cherry', 'Blossom', 'Rosewood', 'Apple', 'Peach', 'Blackberry', 'Strawberry', 'Starlight', 'Wilderness', 'Dappled', 'Beaver', 'Acorn', 'Pecan', 'Pheasant', 'Owl', ] ) text_2 = choice( [ 'Way', 'Lane', 'Boulevard', 'Street', 'Drive', 'Circle', 'Avenue', 'Trail', ] ) return '%d %s %s' % (number, text_1, text_2) people = [ Person( name='%s %s' % (any_name(), family_name()), age=age(), address=address(), ) for i in range(5000) ] marrieds = [ ( MarriedPerson( name='%s %s' % (female_name(), last_name), age=age(), address=address, ), MarriedPerson( name='%s %s' % (male_name(), last_name), age=age(), address=address ), ) for last_name, address in [(family_name(), address()) for i in range(2500)] ] for female, male in marrieds: female.partner = male male.partner = female people.extend([female, male]) shuffle(people) # Create the demo: demo = Report(people=people) # Run the demo (if invoked from the command line): if __name__ == '__main__': demo.configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/examples/demo/Advanced/Tabular_editor_with_context_menu_demo.py0000644000175100001730000001275500000000000032453 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ **WARNING** This demo might not work as expected and some documented features might be missing. ------------------------------------------------------------------------------- Defining column-specific context menu in a Tabular Editor. Shows how the example for the Table Editor (`Table_Editor_with_context_menu_demo.py`) can be adapted to work with a Tabular Editor. The demo is a simple baseball scoring system, which lists each player and their current batting statistics. After a given player has an at bat, you right-click on the table cell corresponding to the player and the result of the at-bat (e.g. 'S' = single) and select the 'Add' menu option to register that the player hit a single and update the player's overall statistics. This demo also illustrates the use of Property traits, and how using 'event' meta-data can simplify event handling by collapsing an event that can occur on a number of traits into a category of event, which can be handled by a single event handler defined for the category (in this case, the category is 'affects_average'). """ # Issue related to the demo warning: enthought/traitsui#960 from random import randint from traits.api import HasStrictTraits, Str, Int, Float, List, Property from traitsui.api import ( Action, Item, Menu, TabularAdapter, TabularEditor, View, ) # Define a custom tabular adapter for handling items which affect the player's # batting average: class PlayerAdapter(TabularAdapter): # Overwrite default values alignment = 'center' width = 0.09 def get_menu(self, object, trait, row, column): column_name = self.column_map[column] if column_name not in ['name', 'average']: menu = Menu( Action(name='Add', action='editor.adapter.add(item, column)'), Action(name='Sub', action='editor.adapter.sub(item, column)'), ) return menu else: return super().get_menu(object, trait, row, column) def get_format(self, object, trait, row, column): column_name = self.column_map[column] if column_name == 'average': return '%0.3f' else: return super().get_format(object, trait, row, column) def add(self, object, column): """Increment the affected player statistic.""" column_name = self.column_map[column] setattr(object, column_name, getattr(object, column_name) + 1) def sub(self, object, column): """Decrement the affected player statistic.""" column_name = self.column_map[column] setattr(object, column_name, getattr(object, column_name) - 1) # The 'players' trait table editor: columns = [ ('Player Name', 'name'), ('AB', 'at_bats'), ('SO', 'strike_outs'), ('S', 'singles'), ('D', 'doubles'), ('T', 'triples'), ('HR', 'home_runs'), ('W', 'walks'), ('Ave', 'average'), ] player_editor = TabularEditor( editable=True, auto_resize=True, auto_resize_rows=True, stretch_last_section=False, auto_update=True, adapter=PlayerAdapter(columns=columns), ) # 'Player' class: class Player(HasStrictTraits): # Trait definitions: name = Str() at_bats = Int() strike_outs = Int(event='affects_average') singles = Int(event='affects_average') doubles = Int(event='affects_average') triples = Int(event='affects_average') home_runs = Int(event='affects_average') walks = Int() average = Property(Float) def _get_average(self): """Computes the player's batting average from the current statistics.""" if self.at_bats == 0: return 0.0 return ( float(self.singles + self.doubles + self.triples + self.home_runs) / self.at_bats ) def _affects_average_changed(self): """Handles an event that affects the player's batting average.""" self.at_bats += 1 class Team(HasStrictTraits): # Trait definitions: players = List(Player) # Trait view definitions: traits_view = View( Item('players', show_label=False, editor=player_editor), title='Baseball Scoring Demo', width=0.5, height=0.5, resizable=True, ) def random_player(name): """Generates and returns a random player.""" p = Player( name=name, strike_outs=randint(0, 50), singles=randint(0, 50), doubles=randint(0, 20), triples=randint(0, 5), home_runs=randint(0, 30), walks=randint(0, 50), ) return p.trait_set( at_bats=( p.strike_outs + p.singles + p.doubles + p.triples + p.home_runs + randint(100, 200) ) ) # Create the demo: demo = Team( players=[ random_player(name) for name in [ 'Dave', 'Mike', 'Joe', 'Tom', 'Dick', 'Harry', 'Dirk', 'Fields', 'Stretch', ] ] ) # Run the demo (if invoked from the command line): if __name__ == '__main__': demo.configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/examples/demo/Advanced/Time_editor_demo.py0000644000175100001730000000255000000000000026124 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ A timer editor Display or edit a time. You can edit the time directly, or by using only the arrow keys (left & right to navigate, up & down to change). """ import datetime from traits.api import HasTraits, Time from traitsui.api import View, Item, TimeEditor class TimeEditorDemo(HasTraits): """Demo class.""" time = Time(datetime.time(12, 0, 0)) traits_view = View( Item('time', label='Simple Editor'), Item( 'time', label='Readonly Editor', style='readonly', # Show 24-hour mode instead of default 12 hour. editor=TimeEditor(strftime='%H:%M:%S'), ), resizable=True, ) def _time_changed(self): """Print each time the time value is changed in the editor.""" print(self.time) # -- Set Up The Demo ------------------------------------------------------ demo = TimeEditorDemo() if __name__ == "__main__": demo.configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/examples/demo/Advanced/Tree_editor_required_traits_demo.py0000644000175100001730000001114100000000000031407 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ This is a modified version of the Standard_Editors/TreeEditor_demo.py in which the `Employee` class is now a subclass of HasRequiredTraits. Thus, to create a new `TreeNode` for `Employee`, to create the associated `Employee` object we can not simply do `Employee()`. Luckily the `TreeNode` API provides a means for specifying a callable that returns a created instance when we want to add a new node. To do so we can simply pass a tuple of the for (klass, prompt, factorry) as an item the `add` list trait for a TreeNode. klass is the class of object we want to be addable, prompt is a boolean indicating whether or not we want to prompt the user to specify traits after we instantiate the object before it is added to the tree, and factory is the previously described callable that must return an instance of klass. """ from traits.api import HasRequiredTraits, HasTraits, Str, Regex, List, Instance from traitsui.api import Item, View, TreeEditor, TreeNode class Employee(HasRequiredTraits): """Defines a company employee.""" name = Str('') title = Str() phone = Regex(regex=r'\d\d\d-\d\d\d\d') def default_title(self): self.title = 'Senior Engineer' def create_default_employee(): return Employee(name="Dilbert", title="Engineer", phone="999-8212") class Department(HasTraits): """Defines a department with employees.""" name = Str('') employees = List(Employee) class Company(HasTraits): """Defines a company with departments and employees.""" name = Str('') departments = List(Department) employees = List(Employee) # Create an empty view for objects that have no data to display: no_view = View() # Define the TreeEditor used to display the hierarchy: tree_editor = TreeEditor( nodes=[ # The first node specified is the top level one TreeNode( node_for=[Company], auto_open=True, # child nodes are children='', label='name', # label with Company name view=View(['name']), ), TreeNode( node_for=[Company], auto_open=True, children='departments', label='=Departments', # constant label view=no_view, add=[Department], ), TreeNode( node_for=[Company], auto_open=True, children='employees', label='=Employees', # constant label view=no_view, add=[(Employee, True, create_default_employee)], ), TreeNode( node_for=[Department], auto_open=True, children='employees', label='name', # label with Department name view=View(['name']), add=[(Employee, True, create_default_employee)], ), TreeNode( node_for=[Employee], auto_open=True, label='name', # label with Employee name view=View(['name', 'title', 'phone']), ), ] ) class Partner(HasTraits): """Defines a business partner.""" name = Str('') company = Instance(Company) traits_view = View( Item(name='company', editor=tree_editor, show_label=False), title='Company Structure', buttons=['OK'], resizable=True, style='custom', width=0.3, height=500, ) # Create an example data structure: jason = Employee(name='Jason', title='Senior Engineer', phone='536-1057') mike = Employee(name='Mike', title='Senior Engineer', phone='536-1057') dave = Employee(name='Dave', title='Senior Developer', phone='536-1057') martin = Employee(name='Martin', title='Senior Engineer', phone='536-1057') duncan = Employee(name='Duncan', title='Consultant', phone='526-1057') # Create the demo: demo = Partner( name='Enthought, Inc.', company=Company( name='Enthought', employees=[dave, martin, duncan, jason, mike], departments=[ Department(name='Business', employees=[jason, mike]), Department(name='Scientific', employees=[dave, martin, duncan]), ], ), ) # Run the demo (if invoked from the command line): if __name__ == '__main__': demo.configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/examples/demo/Advanced/index.rst0000644000175100001730000000007200000000000024140 0ustar00runnerdocker00000000000000These examples demonstrate advanced features of TraitsUI. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/examples/demo/Advanced/test_fixed.h50000644000175100001730000411322000000000000024677 0ustar00runnerdocker00000000000000HDF  ` TREEHEAPXdataH TITLE (CLASSGROUP (VERSION1.0 8PYTABLES_FORMAT_VERSION2.1TREE XHEAPxh( TITLESNOD(H8P TITLE (CLASSGROUP (VERSION1.0 0 pandas_typeframe 0pandas_version0.15.2 0 encodingUTF-8 (errorsstrict 0 ndim  0axis0_varietyregularH H ^ (CLASSARRAY (VERSION2.4 TITLESNOD (H%8hHXX'int0int1int2int3int4floatobject_1_0object_1_1object_1_2object_1_3object_1_4object_1_5object_1_6object_1_7object_1_8object_1_9object_2_0object_2_1object_2_2object_2_3object_2_4object_2_5object_2_6  !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abc;M`5~"@?ΐk ?bJ⿮N,?PpXE?12`ʿ3OiC?Nܜ?[-;ο wԿgoÿ¨bV3?bLOUm?㫢Y5޿#%?i["k@ѿ8Iiׇ?0?E"?rS8p?˰+?1Á<¹?}Uݿ n`?)lQ ?+-)ȿ\vUaf5ۿ?J!6l?S( AۿmsSiϿp(]zά?9-ل?͑n?ՙTd?.\B?EuZvZ +#~3?{3Naӿ _Sȝ?-܈˳?b2 Tݿ5{?H|?poN:ҿJ?>??-sʡAu쿶p O3xaݿ'uӜ?fL?/}RGmͿGe([@ؿ쌗7T?S ?YT>ǿu?$ O?`J:}Qh濭AaYؿE[ݧg?s݈ ?M\? ʢ?/uIEM mٿ Dh=!u?k \ @?mB^?ݟl?bg0|?$T4/floatint0int1int2int3int4object_1_0object_1_1object_1_2object_1_3object_1_4object_1_5object_1_6object_1_7object_1_8object_1_9object_2_0object_2_1object_2_2object_2_3object_2_4object_2_5object_2_6 (FLAVORnumpy 8 transposed (kindstring (nameN.hP 0axis1_varietyregular dd@. ^ (CLASSARRAY (VERSION2.4 TITLE (FLAVORnumpy 8 transposed (kindinteger (nameN.PX 0 nblocks  (dd ?@4 4N ^ (CLASSARRAY (VERSION2.4 TITLE (FLAVORnumpy 8 transposed(%X 8block0_items_varietyregular n^ (CLASSARRAY (VERSION2.4 TITLE (FLAVORnumpy 8 transposed (kindstring (nameN. (dd ^ (CLASSARRAY (VERSION2.4$                                                     TITLE (FLAVORnumpy 8 transposedX 8block1_items_varietyregular s^ (CLASSARRAY (VERSION2.4 TITLE (FLAVORnumpy 8 transposed (kindstring (nameN.)X`p)^ (CLASSVLARRAY (VERSION1.4h(axis0axis1block0_valuesblock0_itemsblock1_valuesblock1_itemsblock2_valuesblock2_items8 0 PSEUDOATOMobjectTREEGCOL\\\numpy.core.multiarray _reconstructnumpyndarrayKCbR(KKdKhdtypeO8KKR(K|NNNJJK?tb](a1ed8f2d8b8a9850eef27646045f0dda921ef8cf2ae9ef79342b7834a8bd96c6fb17fb6d36cccce779936df6966dd268ef1d8a7e09c3ec21f0e40756e85ccd7cf14b16965a9984dc0b3e918aee13330ad3e01bf7c559a69d133413482de66deda9ce5673d9d30ca39d9f89773306b28929c80c283011680a41491e6294fcd75a32ae9c22319adb3d66c301fd20831a4006d86c9eb3205c71d43064215683c2c6a4f39b342c27562d953f9f8e14ea66ce52b329a0bf86f9f1b492d7c410e45bd1a398898d2df24bc1e6a0e78f76defbbd80d9688e04706b2800b6c13279efbad3ae 6fb804a87e444751c817e7d97c4ea6070f4f69ab1591e3c3724912d5981d5b0110173f85fe94277d9c5cdd3e25ca21a91cdcc31c6eb28adb960e7c7a98706d8f1508353d1efedd251fe12061f818e3c3140eecae11d480cfb4c4624984bb9bda88866cc27f0a96b9d9846a1cac7214a36566992482c9c1acb23f482fc50c8ed86c8c95791661a03a5dfaaa35fef621d7c3ddeeb10ff72442c69147c66eabcd7e52dff5f8b64eea29c536e0063d2d4f30d3abcde9f6ac034067179fa3389c7cfa440e2d0c764b0701c59788968fe835a8448b85ffd23efceed29d005ffc86a3b419098c788286a3d5a98d96ee0a6483544fe963039f9b3b579e12af26bb7e0522a78c4a2a7555ad88b7b416c89b4b8fc8a6c940ec0b 66266cac0c476ffb22e7295dd0eddc9f1ccf7fad2d7063d4718a15215a7feec5a632dacf0283647596e187261e0a5cc72246b2da886d01826edeb4a76bdd81d7d7739faf54310096a2e25becf8757997517e19db6d706a2d330257842994c1fb23c15a1aeb8c8689285ba4c920c9082dcc26ccced3971540bbe582f780f24bd49c41086d09dec6e6b1f07685910d653ce65b7c139667a6ecc0dd05c318db61e85a95d1d7563c856fabc851e24009b7b93c9a4862634431e8d6941887a47e51781ffad972c09cda284ff41168b22f215d8bfaec8d175ffda20043f45d670ddf099bf8cd63d247fcc9d845df0932eb441e3d2a9ecd0cacc6e2dbee6b69e053a1fdf6e2641fa794b8056f559733d23ff05a8e20d215872d93436279b0e2e296ada23a03c2894f4f95002b4dafe7213d2c410239e1b644f59352f78ede6df8bdbaa16c6f11ba397dbc6bb7c888fc7eb52dcffa97b8cbf026d734405ee51647377e11a27c2e6f402e49b999c8cb743e7ef2c79f1d8d986a8b73f758d38e986edb5fa3bf6ba7216cf445e82010d72bff6c94562ef328f6ddf49087d451ba1db5c2dd427ab42343563bb1e841df91642aa44ca49e71b6a3b91d94b8c23bff59cc9695874ad35da6912a9b78d29315a1251cc55a9237a0eaaee5fe701a3cfa0a7c5d884cb0be161835e02402c6124d40 6cb2a70628d877463fe196e350f22 57e002d6ab3810798afa2dd5902635e774ac0bab797d21417df68a69f9d3ab175a5ba15876523a0ed6e1d6254567844c34dbee8e89dc6e0df53bce8a76b41f93303f8844346984c9eeb0f8fb7073a649c2ebe0139 cf736e246d43d2ae833f18a16776c2d345ebb73c4bd3c6b4b4eb65e5b02a1ccbe28c360ee9d114c946f94e278316e995132b6074d7761ba9a613a5005464a6c83b5cee855acd45205f44c2ecd896d8c458273f102407a636176a9a3e6bf038ea85bf0b58c802a9ce2385ad52eae87048f48ebee4ef229e924bc0cd175249128ef21ee097db61b050ee9ca0bda04fa5fb13586fec2144149efd3054336a3376b08b9ba98016e580370ff2a863583d58f9ac127542d6deb0cc1d27d6d8c0bf1091cdeef37a0e7fb78498e5715bbd9bf1612c698449a0bacbdc676f195f864fe2293e10521f435dc94bff11fea4f8136f010aca573c66e203c3f658186c4e03f898594156be60a66dbf79423909632847828baae5c126859be0bdae54589c97fe7e4c2fc3ced6f9d52c846790af6f354f8b9b459abf5fdb217da79c2bfb2fe0ede1e9d442cfd12fb3bc2c43ae94b7e010b75586152c14f1878e0f9569700c3cf2903fd6b1e795c2559f19d944ffea7461c1441b190e973dcd8b7566b77c1a868ec1b87aad7c1bf836ce08cbe9cc735547d4af3879635c41db080c9538c55213645ff00ad777df58e620aa542c3414b83f7699c684b40990ac299a4eb969e9866e3d3521957db9f8d5bb22fe79b02215823fe865d41a9f1eede8cdbab6f14b0a705b2f0d4e2e72ced21199c1c2e7dc363f9f1c7ee1dd3436414725c843d9023fc18e54258d99e32e662ab8fbd7439b32cb1cad1dd2bcd86b568750dae024cc4ee55f1f80869a60dc256dedfa1d1d80899b39da5baa2c4cbc4746a7d63268bc39027ca8c566f5065209702f0730de3c51c73e77c29bda177840c798dc936abfdff2144bfc51174519886a85a4ea0153690c73452b4e1b245c98d6fcac6b0e558fcfe9352a4858c9b397add9b44ecd1ee4a52ba3bdb7f9beb37fdc371872afca731ca4638c27f80a70bdeb873ba909d91c5f9cd04c14438f43094e2bbc9630bb53ac7f5bd652bc110111958f6a01fc9043f8dc38dc2c9b1cb98d9c2baf227bf69b5c7e30c1dc6a4baf239b43df0187ea6d3be3d3cbf7efed34857acbdda54f8ca1106b1be8d2bed416cb5e79bad3b081d66ca71d5ad5c8ecea8be175de3a6ed1dc99bbf1736a7e34a40a3926dc382fc6b5ddb8dd5b58c1ad9ca2e1d9bfcf9e8e631ad58131bc20fd677137322512512341283904b4a1d9a17f2251460f7b375cb9157deb6be5140722a7117ad5de948f81a1d0f4281c8fc909ab7e8ded37c16a520f11df99c117b8f7e309aed70800f2c7421efe2d5fecccad059c1ae090942a9a652cc419424fd94e88e40d76d00671904b364144c150f02646873a00e94427ce9aa2f0fa22522f12b628ccd94d172970840d3eed2d2208a330864b7f30702804595ee4f3c9c2d70278abd7a73cb409bf2904e4c6537919ba1aa10d56a8e4652497aa982beb8741531e0d5b5a438b18eb06e9b24175345dcaf3765208d103aeb44e92729c0b5c78974fc96ea3e6a248fa443dddb06f725f5f989c099ad4dbb279d0f4693ee3df1a10a74a45818a52893757072720c9869cfc3cdb2a75209b622a1d397daa57c7e3d0a67b8c893cd97a9990d92d4a5077917122e66091b7042996bb8e031ca98b792d93b6b39f1e9ccd39134e5c15cfc0319d242d23f93b1 1da3c3f1f84bfb6e8fb50cbd715cbe8cca19a9c207d979d52176ca3b917cc0e9f84ab3870142d78bca095dcaaa7450b86bc1732b3867d7182f7c031a9dee276a95c3e392ea4d61749d8087e5ecd6fa11d9254654543b0a1a124075ac4dd2498e300a6d91f400bdaa1328302f2871dc46691e854d445d498d6c27 4f86a3c760ace08b7516ace67eedc6e48c7d01daea9f5407713d52a846c56f3feeec60399699b9c4792aeb49d8559026dd14b0c892f46e5daed2874c22a0effbb69ffdcc0111a55afd90a9fcf27a0fa1631f5bba5c0730d1674d819d167881d87045c40ded69b63153f3e6f304c2ffddc0050a62188728d52a61141f823b7ada4061911f08c87f9157271f025aaddf60b9ef5314a43a45e8ac403af932015299a0157ac07b15fd51370d929aeeebf23147408d60554943d1fca873b5b0fbd9e676ca9c2d940eb6f9797074bd726145c4e0969fc4fa4f12fd696524c1b93558cddd9ed6464a9e239f8a70993d33b86ef59e48f5b4e289e1ffb03d54d479d383579fd61f479eb93a00a3cb552dd4bfd0860951026b2d639a41002a88e61461085d893a11c39051001087ce446ec094f948e8821b7c77e9a3ff0512b20d81c335bd9e1fb9e2ae3c1cff498d00e9782fb7019ea239bbd23411220dbdbede6f6366dd383adb764b01d844b85613f6c390baea93fa9562871236e575dbb918e58c6d095387268444487f24fb143c8a41fe53fcd82028621562d23ce1dc901f74657ca6bf12dba3135b221ffd59fa00e83a61172dec9125314d44f1fe33357ff4c34e5c4f3a2d7e7c5022907ba95ae7e646ef4385d4d64b2942202238974818e84dfafffeb8afb1d0c565f64bf00f82d0509fef4d4c4c853c864a17d4e441225548c308b74f90bca05c9f09304eaff4bb0b9205e2cfa23c394c5f008bbd0822d4a058e2ed02a6a6052409f79af02b7ae045f968ae9022efc8c618adacb7d4a8035fff951ab0eed60ee8e44fee2d6044864bb59a73ccd7ee6d91d290ae6c d54865080c122b3755ff68777c4ead0930dd684e1c8765e3cf60215eae0b5bed21687404b4d88c240b2f5e1ee13fc0965e4f53af5f5c1431d36e2967b1f88698780376c16115a64a159e584975d661ba0f904cf564a8547d1ce3dac195cc9bb6d84a9f53b8a21c0974c56f 94a3eedd211d0687baa6883323fd537ab0db15767b3ee06a7076a7f6759f47887abcea580a24466edb5f794d925ebc5e242826815a52ebf856d7425d91e0db82ca0635e06e20e44bdf1c05100635e8453f0c377b4e90f4ddc56dea3ed55d4bce9caccdfa4683430477dfc436be0486d02367ca4573fd5013f23f5c3ac6474bcac23ffc476f3f871a5610b57312d0ec7f63695650461566f41f21ca550788c62c3e74faf62e3911bb68b3771ea29f3ed0fea816bc67fff4772e8ac9420fee4142c200204ad32cfc8fd5539de9c52465586b3b681175489f095ad42560a7aea16bbc8245c056bfb65db101b0c480c4abedbab11a7f3b7e27200642fdc2cf84d5644fcbe8b65fe2d0595071f8b3f8b7f909a8083e8a01ffd9474c6d26efe5b955f097363b8fbb626e5116db4445cd55ceb206a7d2d3d68104f85bf4e0c68dddace5c7255b45c5a2ad2ac91b82440ebbbf8c38c545e0432f3fdf346c9e731f300a624bbf98daedb05e4828ddf8f3649f0c64eb25d9ea7500a581f14fe47fcbd8531a7 1230d8580aedce8073359e88f449a25a37071edef0b812d6634350aa1c764c53dfb7f79675ffa18e078378040c694308a2329ea3c5f8379c22cc366c6370368459543ce6bffc0cb25ab5553d475762da0c5fa0e32f2e3f1022796212ecf54fb051a1cf1d729e50bc009bf8ad83b45bbf0900539390e60bd7a4b7d455abef23e8d4cddb77a2ed815814ec9306f09502b6f7738461b23f40f5b492721db68bd70a5456491119a7dd6a992 69fbdd759568a689e8998c0aed3ccdc03174e0d399cbb8cbcae6818d511b445d90e5115876a076b39f73c34fd865568589bd6078334bc582ec22487ccb00f2e71fc5833c5ed9e9ff6ed695aad271f3729ff79b158a28c7dbe2a476ef 4e387062b4493132ff7f4042a25582577c8e2f9d70a6d6cbb85172b7a4fc360268c2f2a0452ab1d67c33f492b90eb392b3788cad6b01048c5344e39d8ff9b921de1f2c9755480521190665eeee d7170c15dc3fa923d84085f9ea52e8064d33f9a73aa93ed7c031fcbebc35f5f21d8c47ea86acf49363cbaa3c1477a3f859d2191767ac715cc8818b89454867eb01020ab92b1b180134d4476508bb2d8915eb5069688976cbd9cbe3553a71189f3c0694e fb8a85024271ddd91327795e54efcbe876f95de3bb801715b466940dbae4bd898c15930caedf4bb772c252e928d1ea62448209c362dac69a79fb139c0d32107b8e38a5ff28cc0332dcaafc38ad4adf9505475399ec48f781425954b994460ec04c477c0199c4834d5bc20d2ba4fa3fdd560f61d7bbda76ebd716769ab175cf864383d84fa67eca411d4833ff579c771ff13788a1b6df196a16ec1f0d343232191c2ceee50d99458fafd9084ef9012a0f6bfe849d66e14e54dfe9843980894dedf87a36f4f4ddf043871598af3284c2 d8b17d40f7b8b2416be25d268d8e6be12db13302bfdcecf5251cefa9aceab5418b74f5276d27442472eb7862305fed82c8d057b16cd83c4c1f60958bb7d9d623d01c72ee10543f6ef4796cdd24b3ceb0e5a1230f96b999434c218019897aa215512cda7f2b977fd82309226b58679c78d92f77160d0d0645d8bbbae28a6d7ba85b0909432cde4a446c6d180c5a3b4c7778fc2c4a105a42ce7e8675bf9755b0eb99d192d1f78767eeb84a7dd159d118b86a632bcecbf2ebe21c29f71a8be2eb883df12614eb5b22dfbe2062b68aca028f355f6652ffaa9a1d8ac38ddf31ca671071fe6cd824d2925d2cacd5ed10f776baff97f81fcc6af728e3230db56976593504ef350bcf1f7711d29f4ae074f2e0750628c983f2776ba85d7fccd0590a7b75d9e56a14185ee7e86e101803edb09d12d1806a835d2940a223fbec7905bdf914cda92ed419210f7ab288ab464b8b5b00e37d69931542948e92d820f7154c12a7e50deea456577139f6f249fa5f28d90fe9f8e98475ce22e94527f6acd4d8237026b88d204abc448c6601652dcf0daa48ee87b7bf963a8ac513497d71a30f697450b0fd32a2e6998d3ea79dbef36cacfe 42f0fdfb014850bce9b453d27bad2 6381407628d75cd7b0bd5bd2578a938e3444ce469752a8b0fca4faaa0b6af0d5a6c3dcfafc0946ded5192e7ae4adefd78a52efca580273d6b5c65ae1a0cceb7d3d0cd60459a0122719bf609663c5534a352bbbc691843799bcdd9d769a6dbe4ffed03f191a19af0e06db264a39a628a02df4cd91470ca146e3d489b1872f2f4a6994ee442cd545f8eb24fe88966ca677a4f4771eddbfaeba32e67e756033c129b43cd6af1e29e831a094d68bdd57ee000c802ce2a84a49df69d6b590976aa2767ffc8a67d01e573dfc2345e94d0818e75568a25b39b1fa6b84ba282e9905bde5af41a98c5f13d1e5b3730ad0c038e4fbc3a0c4bad5b30bf76383c83c6683379fdb9d7c1ffdda8d04edd04295899a42656333ae82e8c7307cf28fa0e682e8765446665b155d007b629cb084249f1fb3bfefdae240c8df2930b3560a4b5248ab67e87d6120383e1197af6d59a08d3e9c22f0b0b600722923ae467f20bab3215cf6c79e98f64d1753758e37fa2d12bb7a8f38cbd7d7147ebce5c12875e0d1cb9db2c14b57a82f6017366ee3acb9703d3eb58ff7c767764eea82d28cf0eefea60fb02a6bebdeef3a9b1156e2ae10db1e4bdde66b7644df7fd7eb4c8856fc8076d4651da726b3f59bcd2d1d19e0dd5a73bb146621f3b0faed37b6dcfc7d891c72c918568d9274face3c25eeb2f69ea5e348d7986f3a91e8ddf07b52898379dfdeba664565c937d9d9e4c4d736374c0c20b5c89ec2400e9e508a07fd6ffe9f9c938d12c907f5dce392ce611a7dec9a0c4cf9ab41171bbe67ac041c35b8b7943af6ac523849af42bf7c85b75d8f76eb3edec03e43f5b5348d2be37b19d6cc1780e385fdc7aa1033d32d09cb88fbcbd65471f0938e26f3bd4bf4ade5e64e784484e04c810b5d72be3005e4d13e81abf82f5e953a2fe28e1082924ce5ebd88c9a43e2b6d499c476cf42df109efed4bd9982eadd4aca404664e6cca53007bb2b1f58a3449d8b40b9b94a7b9d66e5725b3347952af8d5fbeff3de123092ff904682 2a7b983b22599085ee7bdbcf1ba882b46a5b11120c306b616e2ceab9ab5f80d98de070726d56395ea73db4915df1531b9b0846832613f34e683a76b99dd681f172689d8eebc799455cf0bffa72832deee3e7ced72834c91a3c6902327afbd63446aaed4c5b8c586f3e56bdafb3630d55972b6e(6baa3090d3fe60b79a67b53851730cd8fc345ff9b570da9deda2270c1e93cf30d7045ada9bdca427e6d8df28d7607b3ed655f872425256cd8deee210ea98b752631c8a3b94068c777c38f34dd078fff0cfa02ce01e9a4dfa0b8f008597ba305ad665e425b281d0c5a55c3377f17b12eca69c59ee3032b49b0d626089f3515f6acf39acc88d48ca51b9acd649eac9fcbd48d3069243a78d5ca596d0b6f38e2e8bf4f6fdb91729e5f4e0185ae0be2815ca49efd25fe1691dec446a34e726d5a6c9cde7b7bffa157aa2 601b0c6e55b65ec62327b321c5d0376543ecf394338ec37df945f2dc842947aed16659fd9177df4bd83d5f64b485c1b532159f5fbc582baea9e5f0c29a6d200026f725f101efce5ef79ca8ac5b10157fdff8ea6ab3e6abfba502b708 a9b6ef6465873556708507b32b9e729b818a264d1472f81fba98e9de32820b9ab4ad87440f4dd85c56964183be684288b86a3b408fc26a92dae9fe8df3015a15dfe4cfab9921ffc1af9d96ab96dba727301810b1f7ec530799857816263e0936c171b94f73be8b52a4e375645860576c27755301faf7e9a9e671ecae8055c8f352ddc0c3bec368b4164d55a38c6505f503931c4744daaec7f511685af75d15d408fded36d03b4a9cd12fc701da9b855c24 922db02ac2c24e6ade73fc1f383d64a5504330424cbe1a6dddba938e24ce954177ef173c78e67e9abde250bf8c00c0eb6b63a2cad400538fdf14b75c105297fa83c51dc304a9ba83a0be6d510389498ba95b2ae3387a820b030ee7a93790a997283e46f3e8e7c24e6dfd27377c1487920e2dadc8af9641c5ebe25eb24d2fd42fe51e2b7057b136e1a438382508d73efbf5cbab68be1fc407eed78725dd8cca1593e8c29f05597c59b67d3e04238054070bc491d9a083c535f909f00253a47fe0fbce0d1e721a9a4a475061abe6414799a09f5f21463943cd02459be501c6d4262f1d7cb1a520920d3747e6ffac0143246281a650fef1acab737d8402aa2bd32f9863817ab98140561d50e92f51bbcbdd38649d4fa24846eca5ffd9961f1eb3a8263b832f4477b05465e5b5d001541a2a9c5a723f93f692ab65ef10e37367b8a1f96810d98b90f960e3485b3f3ed3a77a2ee4917a54486ee4f5419211b22ff5a8aeaa2b30469d0cb88a9b2d9b909533d855b661a83752db44afb706414d7e337a178fa8a4227440f0775e769a25a0aebd67701c4319de2e9d4c7e1f28d3f232795fc911ef900cc51381a27e551e6621687b14316c3ff715ac9bd9f170ef7b952ac2397920ab85994d84691ccdc3af cbd8b850ff1d53daf346abcee48ed3e1a12c6c53f3b6d46abc4cbe6cbea9ba3e2945fe16f1d24cc390a91aed7ffdda54a2efc58d6a5673651b70b806c6977314bd689c3415 5c9005cc15538a909cce14470c4b35864355584f5cdcc638a43a8fc4d35d0add6cbe1895d6201991cad9870eab0fc041a29119b6b7cf0971e728518c809c667f3bb0038a9d756feebc5c0e7130da74537aa55de809ec067d20c8cc059b6b6cdaea87916d0a7091ba1f190de4e9b3d0cb24de4b182e77429ee12f8b9cb067f572eb11bcb2b08479b9d8b915950925db89aca66a7ffa0b7eb61b8c890b995e126622d7f768d0e92ed2f607d1dfba9390fd222b9ab664ab6bf84b295aec70257b96 e07af91ff296de4bf36f0bb276829e558e2ea72e33691ac967beaeba41ae2d5b5e3fe8c111ee87b51a953adc74269d5720161d68b49caf650224771db3273bc412328908586a808c621f3a96c5c0cf5f0300b53f719b19e79f93aff5edbfff8f85b6301 5e8863f3540b96a060721bc6eb0ecad3ce116bcc7f862eabc8ecf2ac819ccc7d7bf0efc0d805ec3c7c6fd4a4dbb7d592e11bf302aa395b84359c0c1374a3f831fbbcb824d4d63c3d246e657fa28dcd1e02d8c5b2267a96c08586e347bf9d12c6925c90e803241953ae386e7a675f4e22f7aa5ae037760552848d31353ac3523446c24f468ed3265cf010211908628ce036c044eca823a5b903986809ea01a3e3c077614fa39a928a8d8bdad37cf02d43ce76bd74eb547e46362af55c970275b643e9ab24fe39cc7 69a090cf8bd3702f0a7583ab909d4c5c809459a43ca2992fc5fe3660d25f8e9275ea2619ebf66a8ad874b755826bc1b6434e34538de739f4f785f3183fe740cdd2646b4f75a3c265ea7f59a60c6aba4139a1b36a4 d840d34c4276916067e5151c49a4d43b29aceae0e52fcddfe418a0cdde112e7c4776c5f2f3b7c1d74f811e018fc59cdf96a73013d46f65ef3f72991991d94d66e4c99a0ab2467d27a9108d9564283efb2e37746515da52afc377fe66590d82b8a89d02f9e367684cceebd60f53de14fc05524f6aac61a4b006245c31d5da9c0cb73d6629c211139dfe5451b59f9fcf862d6d3ee3d420fbc9362f06f418ff547a07d51a986090d7db11ab217a315782069d999ea6f1bae60cc37948307c3111625ce9c7dd15b1c9d092142eb4f44250eca9c4c007ba8845f811fa1c2d1c6b42d306c170a44a07ddc9e0e9f3f784a47e5 4a138ec7d6ddfdc22d07bdc40717698347edacccbf7d12b67b0aecf1ea556ae3b5b11abaa73ae4e7766e325cfcfec41d6db69cffa8e338b4b0b87529994d82c5ed8a42a70562a942e0f4c901d43cf026b54f29052cac5c36635c90abf6c373727f29b7a33a834b67807eb83a599bcf8af7ab0b4da447465e3cecffeb35d90871a2d29de36ee445a916b6c96200f76134bfc550af9ee621a893e6a485ffdce04ddb5e993f0053d1c17559dd2620d5ae5118535859c2aa768277b88b9b5d03e8407e8058def1f0d7532ab196b127e6eb559dac252a22202cd52eda570eff06375f0865beb95c5 cc21d8854c090ef6ec75fee0b14c3d633d35b1d3c3a71db3fd9df436515bfef5db7d35822f03b12590d17251d8a479500d1bb098b5576cbbd2203d1386a299f415521ce8c321ba905d19ec94c3ae49678ddd51da693502cc90242cc27ed4a33ce276f921adf91ecc19476c22631a0b23dcbffe5d0bf006c653248c1081c79d949aad51e0df38d11b44982867213196c02279691f5d021bba1a18ce3269562d1fe1aab6b2568fc80e5a35c4256e1b90e45c928883350853ae0892236a2d083d68cdc4dc5c043395d551d4ad49fb9a804f56d44e3a621f3dcad1ed73be1855f52239314f1af987e135151fc5d13fa258e772052592e25af7c5515f9c55667f1a2087019abcf445dc1a55753132b98908c744d74b4081 3bcdbced9a109b103b32995377f790d8178f1400bf6535ee6cc4908170c1347b5de086df154e8e05ed8448b9de6c2080c0d187e5d1db634508c778e613917ccf4ef9c7aa73dca77cfbb3c95a27caa350b25d7c9cf30096c1f2057fba5a39ed1c1ce8b6a463f2f8336d38e9dd297d87e3499b0003c8c6039ec638027219862e49e7cf73ed536aacb472ee8e4b3effaa5d4c2aa5f4f4481e031c036aab95a0786874b8566b77331010bddc57eebef603bce2664718836012788a788902910a013b3e54629fbdd9586959ec95e6fe33a2700fea56a36c57f618a990d213fd242cce90c1520f3bdc2f5a9748d05df631059f28b7821c8c386b38235cc92e1af3c9e17b682fa44459 84e43bb2cd279464e3702d3350589fb91be1e29b04e668d00334e2803ab63964afd1f750d82cade861e09275fc1ec0278357aa482a2730cfd923ba5066f7646df091591c04319eb978e2cbe813ff42228f645b3d44a2b08e95b0669b41e49fc80927651b82467b3015623c25aaf4ed93805561cd5efb42294307e9e05ac996a3899ac4f3d10405aec4d60b9f5384ab589951f954c5494c4d6aa7bb4d41a6d208532d14707a98df31a9f59880c0df14d6352d583ffce3cfc4d634d2a7e13e62222aa0d3744c187b34908aa37eb0d9ec4c92160657d7cb7f1172a598d3fe096998c9e7fcbb65db69179923fe5177f50d7e098de00ad88332 f16ec32a9d2751eff69ea90859dc4a8efdbc25c7ce8bd8e1b9b71d3eacc4964d689e0f78c8b176c3ce70c1886ceaa3122e101a248cbc780bb361c0f5b9e403d5c0ff5985c6d359e85bf8d12b29600bfe99a1ced37d2a65df540b8f54 46325b2cefdbfb b5677b7883d487988d730de322833ef4e003e5f70f2a df3a92793773a197e4f76e6cdf0f8e982fdef9d35b51fcd9c627123684e74cc014bd7053dfc61f6d245b41f140ea5c21b9bc47e1bb83244a938df770095ce5ee155943a53378e6bc00974e75034df42cdfd71a9958b40e55c2757d9aac67889706886432d39200e50c4edd b4c2063459962645dc278cd5a503328cb022af9e7f319c9e522bd53e4e34359c31f9b8c51e2f35b6dffcb4bd01c786d1d8f407ba2e737eceff6dfcb9d5a7276c9bcbed19d7d2a32bbb64ce16894861afcb9fed531a5b14d7a3c60354f3df7079f2ebf5778c135b4ce2a2706cb6ba32a941f1e03a0aa2e09b45340f5c0518236c408550f3c43ab5e6aca33ad1495eeccfcd624eb51b1d8c16b6a9ecd14f5411a7a25ed1a53b5f2e31a7a83b9b483b774d4a79cd627548da05b8c6e8b33f3dbef112dc84e0267406908dd8fa24ca16cf21af0ccf6de8452868d24a011b167382f6bc673b200e1d440c96a4bc87f1dec9eea3b83da5eb66ca7d191af8ea25c3bb7e60df5b8241b91625ff117862aeec7a891b8a566878b67f8ff348b40075667f64f10d144721188cefd5a439d18ba1c9194a8c52762c1a3856888c068bc49e2fa70dcbd008dc8cb6736b5c3921716e58383424de843caea6b9ff67956500e264ffaa87bdbe2c8c451810b5407f89c3eedf8069fab0c7262bf79c149eed7a13ced9031fbd7a9047966028de0673e3a0bafcff75ec37bb28bd97a4e914d8a1af4500f2833630ef9404dbe5aaec1d282384a36ed6fef0dd1e4edeeb7ff0e383cbfb3437ace0b662dad72d5dd960d9a2d72f56b104c0accfb65db964d5fe58ccffff28844524902281d29a971a43493d5bbbe4acf9583f1b05d9d502289514b4666f8b997071ca2aa24eacb851b5788855b5224c678f35254a38f6bf561592929ebe0c0f911cca2467ffbbba83ad6e1fdf24d231ffac1917c5d9688d246fdf2b31fb0d45e315e5d9f7f385071803435439f15e619b684c25ce3b2e6acd95f461d8f965c7a2786a6656d0d81ee00623d52854d9c3f5bef5e994c19c77c05bb5db8247021cd26fcc9edb24 5184be02e37a2aad383931106c37f617cb55af97fb3619e2155a4dfa89d0a7980ad14a5705fb9b128e890c75a0ea70c12506113d9c1e881ccc38455a21e99e0b8c2e84bc65ad87f3456633bc551117db926bddf40e4ff034c368dc3dd4f08db89986c49etb. 8 transposed(H 8block2_items_varietyregular  ^ (CLASSARRAY (VERSION2.4 TITLE (FLAVORnumpy 8 transposed (kindstring (nameN.\1././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/examples/demo/Advanced/test_fixed_compressed.h50000644000175100001730000414767100000000000027144 0ustar00runnerdocker00000000000000HDF  ` TREEHEAPXdataH TITLE (CLASSGROUP (VERSION1.0 8PYTABLES_FORMAT_VERSION2.1TREE XHEAPx`N TITLESNOD(HP TITLE (CLASSGROUP (VERSION1.0 0 pandas_typeframe 0pandas_version0.15.2 0 encodingUTF-8 (errorsstrict 0 ndim  0axis0_varietyregular  0}blosc   ^ (CLASSCARRAYH XSNOD (,` H C86h@XPM (VERSION1.1 TITLETREE p  8 transposed (kindstring (nameN. P 0axis1_varietyregular 8d X 0}blosc ( ^ (CLASSCARRAY (VERSION1.1 TITLETREEz @ 8 transposed (kindinteger (nameN.X,X 0 nblocks  P(d +` 0}blosc ! ^ (CLASSCARRAY (VERSION1.1 TITLETREEc  ?@4 4 8 transposedBX 8block0_items_varietyregular0 6 0}blosc -33^ (CLASSCARRAY (VERSION1.1 TITLETREEY3333 8 transposed (kindstring (nameN. H(d pBX 0}blosc 88 ^ (CLASSCARRAY (VERSION1.1 TITLETREEAF  8 transposedX 8block1_items_varietyregular0L 0}blosc `D@^ (CLASSCARRAY (VERSION1.1 TITLETREE\@@ 8 transposed (kindstring (nameN.OX`hO^ (CLASSVLARRAY (VERSION1.4h(axis0axis1block0_valuesblock0_itemsblock1_valuesblock1_itemsblock2_valuesblock2_items8 0 PSEUDOATOMobjectTREEGCOL\\\numpy.core.multiarray _reconstructnumpyndarrayKCbR(KKdKhdtypeO8KKR(K|NNNJJK?tb](a1ed8f2d8b8a9850eef27646045f0dda921ef8cf2ae9ef79342b7834a8bd96c6fb17fb6d36cccce779936df6966dd268ef1d8a7e09c3ec21f0e40756e85ccd7cf14b16965a9984dc0b3e918aee13330ad3e01bf7c559a69d133413482de66deda9ce5673d9d30ca39d9f89773306b28929c80c283011680a41491e6294fcd75a32ae9c22319adb3d66c301fd20831a4006d86c9eb3205c71d43064215683c2c6a4f39b342c27562d953f9f8e14ea66ce52b329a0bf86f9f1b492d7c410e45bd1a398898d2df24bc1e6a0e78f76defbbd80d9688e04706b2800b6c13279efbad3ae 6fb804a87e444751c817e7d97c4ea6070f4f69ab1591e3c3724912d5981d5b0110173f85fe94277d9c5cdd3e25ca21a91cdcc31c6eb28adb960e7c7a98706d8f1508353d1efedd251fe12061f818e3c3140eecae11d480cfb4c4624984bb9bda88866cc27f0a96b9d9846a1cac7214a36566992482c9c1acb23f482fc50c8ed86c8c95791661a03a5dfaaa35fef621d7c3ddeeb10ff72442c69147c66eabcd7e52dff5f8b64eea29c536e0063d2d4f30d3abcde9f6ac034067179fa3389c7cfa440e2d0c764b0701c59788968fe835a8448b85ffd23efceed29d005ffc86a3b419098c788286a3d5a98d96ee0a6483544fe963039f9b3b579e12af26bb7e0522a78c4a2a7555ad88b7b416c89b4b8fc8a6c940ec0b 66266cac0c476ffb22e7295dd0eddc9f1ccf7fad2d7063d4718a15215a7feec5a632dacf0283647596e187261e0a5cc72246b2da886d01826edeb4a76bdd81d7d7739faf54310096a2e25becf8757997517e19db6d706a2d330257842994c1fb23c15a1aeb8c8689285ba4c920c9082dcc26ccced3971540bbe582f780f24bd49c41086d09dec6e6b1f07685910d653ce65b7c139667a6ecc0dd05c318db61e85a95d1d7563c856fabc851e24009b7b93c9a4862634431e8d6941887a47e51781ffad972c09cda284ff41168b22f215d8bfaec8d175ffda20043f45d670ddf099bf8cd63d247fcc9d845df0932eb441e3d2a9ecd0cacc6e2dbee6b69e053a1fdf6e2641fa794b8056f559733d23ff05a8e20d215872d93436279b0e2e296ada23a03c2894f4f95002b4dafe7213d2c410239e1b644f59352f78ede6df8bdbaa16c6f11ba397dbc6bb7c888fc7eb52dcffa97b8cbf026d734405ee51647377e11a27c2e6f402e49b999c8cb743e7ef2c79f1d8d986a8b73f758d38e986edb5fa3bf6ba7216cf445e82010d72bff6c94562ef328f6ddf49087d451ba1db5c2dd427ab42343563bb1e841df91642aa44ca49e71b6a3b91d94b8c23bff59cc9695874ad35da6912a9b78d29315a1251cc55a9237a0eaaee5fe701a3cfa0a7c5d884cb0be161835e02402c6124d40 6cb2a70628d877463fe196e350f22 57e002d6ab3810798afa2dd5902635e774ac0bab797d21417df68a69f9d3ab175a5ba15876523a0ed6e1d6254567844c34dbee8e89dc6e0df53bce8a76b41f93303f8844346984c9eeb0f8fb7073a649c2ebe0139 cf736e246d43d2ae833f18a16776c2d345ebb73c4bd3c6b4b4eb65e5b02a1ccbe28c360ee9d114c946f94e278316e995132b6074d7761ba9a613a5005464a6c83b5cee855acd45205f44c2ecd896d8c458273f102407a636176a9a3e6bf038ea85bf0b58c802a9ce2385ad52eae87048f48ebee4ef229e924bc0cd175249128ef21ee097db61b050ee9ca0bda04fa5fb13586fec2144149efd3054336a3376b08b9ba98016e580370ff2a863583d58f9ac127542d6deb0cc1d27d6d8c0bf1091cdeef37a0e7fb78498e5715bbd9bf1612c698449a0bacbdc676f195f864fe2293e10521f435dc94bff11fea4f8136f010aca573c66e203c3f658186c4e03f898594156be60a66dbf79423909632847828baae5c126859be0bdae54589c97fe7e4c2fc3ced6f9d52c846790af6f354f8b9b459abf5fdb217da79c2bfb2fe0ede1e9d442cfd12fb3bc2c43ae94b7e010b75586152c14f1878e0f9569700c3cf2903fd6b1e795c2559f19d944ffea7461c1441b190e973dcd8b7566b77c1a868ec1b87aad7c1bf836ce08cbe9cc735547d4af3879635c41db080c9538c55213645ff00ad777df58e620aa542c3414b83f7699c684b40990ac299a4eb969e9866e3d3521957db9f8d5bb22fe79b02215823fe865d41a9f1eede8cdbab6f14b0a705b2f0d4e2e72ced21199c1c2e7dc363f9f1c7ee1dd3436414725c843d9023fc18e54258d99e32e662ab8fbd7439b32cb1cad1dd2bcd86b568750dae024cc4ee55f1f80869a60dc256dedfa1d1d80899b39da5baa2c4cbc4746a7d63268bc39027ca8c566f5065209702f0730de3c51c73e77c29bda177840c798dc936abfdff2144bfc51174519886a85a4ea0153690c73452b4e1b245c98d6fcac6b0e558fcfe9352a4858c9b397add9b44ecd1ee4a52ba3bdb7f9beb37fdc371872afca731ca4638c27f80a70bdeb873ba909d91c5f9cd04c14438f43094e2bbc9630bb53ac7f5bd652bc110111958f6a01fc9043f8dc38dc2c9b1cb98d9c2baf227bf69b5c7e30c1dc6a4baf239b43df0187ea6d3be3d3cbf7efed34857acbdda54f8ca1106b1be8d2bed416cb5e79bad3b081d66ca71d5ad5c8ecea8be175de3a6ed1dc99bbf1736a7e34a40a3926dc382fc6b5ddb8dd5b58c1ad9ca2e1d9bfcf9e8e631ad58131bc20fd677137322512512341283904b4a1d9a17f2251460f7b375cb9157deb6be5140722a7117ad5de948f81a1d0f4281c8fc909ab7e8ded37c16a520f11df99c117b8f7e309aed70800f2c7421efe2d5fecccad059c1ae090942a9a652cc419424fd94e88e40d76d00671904b364144c150f02646873a00e94427ce9aa2f0fa22522f12b628ccd94d172970840d3eed2d2208a330864b7f30702804595ee4f3c9c2d70278abd7a73cb409bf2904e4c6537919ba1aa10d56a8e4652497aa982beb8741531e0d5b5a438b18eb06e9b24175345dcaf3765208d103aeb44e92729c0b5c78974fc96ea3e6a248fa443dddb06f725f5f989c099ad4dbb279d0f4693ee3df1a10a74a45818a52893757072720c9869cfc3cdb2a75209b622a1d397daa57c7e3d0a67b8c893cd97a9990d92d4a5077917122e66091b7042996bb8e031ca98b792d93b6b39f1e9ccd39134e5c15cfc0319d242d23f93b1 1da3c3f1f84bfb6e8fb50cbd715cbe8cca19a9c207d979d52176ca3b917cc0e9f84ab3870142d78bca095dcaaa7450b86bc1732b3867d7182f7c031a9dee276a95c3e392ea4d61749d8087e5ecd6fa11d9254654543b0a1a124075ac4dd2498e300a6d91f400bdaa1328302f2871dc46691e854d445d498d6c27 4f86a3c760ace08b7516ace67eedc6e48c7d01daea9f5407713d52a846c56f3feeec60399699b9c4792aeb49d8559026dd14b0c892f46e5daed2874c22a0effbb69ffdcc0111a55afd90a9fcf27a0fa1631f5bba5c0730d1674d819d167881d87045c40ded69b63153f3e6f304c2ffddc0050a62188728d52a61141f823b7ada4061911f08c87f9157271f025aaddf60b9ef5314a43a45e8ac403af932015299a0157ac07b15fd51370d929aeeebf23147408d60554943d1fca873b5b0fbd9e676ca9c2d940eb6f9797074bd726145c4e0969fc4fa4f12fd696524c1b93558cddd9ed6464a9e239f8a70993d33b86ef59e48f5b4e289e1ffb03d54d479d383579fd61f479eb93a00a3cb552dd4bfd0860951026b2d639a41002a88e61461085d893a11c39051001087ce446ec094f948e8821b7c77e9a3ff0512b20d81c335bd9e1fb9e2ae3c1cff498d00e9782fb7019ea239bbd23411220dbdbede6f6366dd383adb764b01d844b85613f6c390baea93fa9562871236e575dbb918e58c6d095387268444487f24fb143c8a41fe53fcd82028621562d23ce1dc901f74657ca6bf12dba3135b221ffd59fa00e83a61172dec9125314d44f1fe33357ff4c34e5c4f3a2d7e7c5022907ba95ae7e646ef4385d4d64b2942202238974818e84dfafffeb8afb1d0c565f64bf00f82d0509fef4d4c4c853c864a17d4e441225548c308b74f90bca05c9f09304eaff4bb0b9205e2cfa23c394c5f008bbd0822d4a058e2ed02a6a6052409f79af02b7ae045f968ae9022efc8c618adacb7d4a8035fff951ab0eed60ee8e44fee2d6044864bb59a73ccd7ee6d91d290ae6c d54865080c122b3755ff68777c4ead0930dd684e1c8765e3cf60215eae0b5bed21687404b4d88c240b2f5e1ee13fc0965e4f53af5f5c1431d36e2967b1f88698780376c16115a64a159e584975d661ba0f904cf564a8547d1ce3dac195cc9bb6d84a9f53b8a21c0974c56f 94a3eedd211d0687baa6883323fd537ab0db15767b3ee06a7076a7f6759f47887abcea580a24466edb5f794d925ebc5e242826815a52ebf856d7425d91e0db82ca0635e06e20e44bdf1c05100635e8453f0c377b4e90f4ddc56dea3ed55d4bce9caccdfa4683430477dfc436be0486d02367ca4573fd5013f23f5c3ac6474bcac23ffc476f3f871a5610b57312d0ec7f63695650461566f41f21ca550788c62c3e74faf62e3911bb68b3771ea29f3ed0fea816bc67fff4772e8ac9420fee4142c200204ad32cfc8fd5539de9c52465586b3b681175489f095ad42560a7aea16bbc8245c056bfb65db101b0c480c4abedbab11a7f3b7e27200642fdc2cf84d5644fcbe8b65fe2d0595071f8b3f8b7f909a8083e8a01ffd9474c6d26efe5b955f097363b8fbb626e5116db4445cd55ceb206a7d2d3d68104f85bf4e0c68dddace5c7255b45c5a2ad2ac91b82440ebbbf8c38c545e0432f3fdf346c9e731f300a624bbf98daedb05e4828ddf8f3649f0c64eb25d9ea7500a581f14fe47fcbd8531a7 1230d8580aedce8073359e88f449a25a37071edef0b812d6634350aa1c764c53dfb7f79675ffa18e078378040c694308a2329ea3c5f8379c22cc366c6370368459543ce6bffc0cb25ab5553d475762da0c5fa0e32f2e3f1022796212ecf54fb051a1cf1d729e50bc009bf8ad83b45bbf0900539390e60bd7a4b7d455abef23e8d4cddb77a2ed815814ec9306f09502b6f7738461b23f40f5b492721db68bd70a5456491119a7dd6a992 69fbdd759568a689e8998c0aed3ccdc03174e0d399cbb8cbcae6818d511b445d90e5115876a076b39f73c34fd865568589bd6078334bc582ec22487ccb00f2e71fc5833c5ed9e9ff6ed695aad271f3729ff79b158a28c7dbe2a476ef 4e387062b4493132ff7f4042a25582577c8e2f9d70a6d6cbb85172b7a4fc360268c2f2a0452ab1d67c33f492b90eb392b3788cad6b01048c5344e39d8ff9b921de1f2c9755480521190665eeee d7170c15dc3fa923d84085f9ea52e8064d33f9a73aa93ed7c031fcbebc35f5f21d8c47ea86acf49363cbaa3c1477a3f859d2191767ac715cc8818b89454867eb01020ab92b1b180134d4476508bb2d8915eb5069688976cbd9cbe3553a71189f3c0694e fb8a85024271ddd91327795e54efcbe876f95de3bb801715b466940dbae4bd898c15930caedf4bb772c252e928d1ea62448209c362dac69a79fb139c0d32107b8e38a5ff28cc0332dcaafc38ad4adf9505475399ec48f781425954b994460ec04c477c0199c4834d5bc20d2ba4fa3fdd560f61d7bbda76ebd716769ab175cf864383d84fa67eca411d4833ff579c771ff13788a1b6df196a16ec1f0d343232191c2ceee50d99458fafd9084ef9012a0f6bfe849d66e14e54dfe9843980894dedf87a36f4f4ddf043871598af3284c2 d8b17d40f7b8b2416be25d268d8e6be12db13302bfdcecf5251cefa9aceab5418b74f5276d27442472eb7862305fed82c8d057b16cd83c4c1f60958bb7d9d623d01c72ee10543f6ef4796cdd24b3ceb0e5a1230f96b999434c218019897aa215512cda7f2b977fd82309226b58679c78d92f77160d0d0645d8bbbae28a6d7ba85b0909432cde4a446c6d180c5a3b4c7778fc2c4a105a42ce7e8675bf9755b0eb99d192d1f78767eeb84a7dd159d118b86a632bcecbf2ebe21c29f71a8be2eb883df12614eb5b22dfbe2062b68aca028f355f6652ffaa9a1d8ac38ddf31ca671071fe6cd824d2925d2cacd5ed10f776baff97f81fcc6af728e3230db56976593504ef350bcf1f7711d29f4ae074f2e0750628c983f2776ba85d7fccd0590a7b75d9e56a14185ee7e86e101803edb09d12d1806a835d2940a223fbec7905bdf914cda92ed419210f7ab288ab464b8b5b00e37d69931542948e92d820f7154c12a7e50deea456577139f6f249fa5f28d90fe9f8e98475ce22e94527f6acd4d8237026b88d204abc448c6601652dcf0daa48ee87b7bf963a8ac513497d71a30f697450b0fd32a2e6998d3ea79dbef36cacfe 42f0fdfb014850bce9b453d27bad2 6381407628d75cd7b0bd5bd2578a938e3444ce469752a8b0fca4faaa0b6af0d5a6c3dcfafc0946ded5192e7ae4adefd78a52efca580273d6b5c65ae1a0cceb7d3d0cd60459a0122719bf609663c5534a352bbbc691843799bcdd9d769a6dbe4ffed03f191a19af0e06db264a39a628a02df4cd91470ca146e3d489b1872f2f4a6994ee442cd545f8eb24fe88966ca677a4f4771eddbfaeba32e67e756033c129b43cd6af1e29e831a094d68bdd57ee000c802ce2a84a49df69d6b590976aa2767ffc8a67d01e573dfc2345e94d0818e75568a25b39b1fa6b84ba282e9905bde5af41a98c5f13d1e5b3730ad0c038e4fbc3a0c4bad5b30bf76383c83c6683379fdb9d7c1ffdda8d04edd04295899a42656333ae82e8c7307cf28fa0e682e8765446665b155d007b629cb084249f1fb3bfefdae240c8df2930b3560a4b5248ab67e87d6120383e1197af6d59a08d3e9c22f0b0b600722923ae467f20bab3215cf6c79e98f64d1753758e37fa2d12bb7a8f38cbd7d7147ebce5c12875e0d1cb9db2c14b57a82f6017366ee3acb9703d3eb58ff7c767764eea82d28cf0eefea60fb02a6bebdeef3a9b1156e2ae10db1e4bdde66b7644df7fd7eb4c8856fc8076d4651da726b3f59bcd2d1d19e0dd5a73bb146621f3b0faed37b6dcfc7d891c72c918568d9274face3c25eeb2f69ea5e348d7986f3a91e8ddf07b52898379dfdeba664565c937d9d9e4c4d736374c0c20b5c89ec2400e9e508a07fd6ffe9f9c938d12c907f5dce392ce611a7dec9a0c4cf9ab41171bbe67ac041c35b8b7943af6ac523849af42bf7c85b75d8f76eb3edec03e43f5b5348d2be37b19d6cc1780e385fdc7aa1033d32d09cb88fbcbd65471f0938e26f3bd4bf4ade5e64e784484e04c810b5d72be3005e4d13e81abf82f5e953a2fe28e1082924ce5ebd88c9a43e2b6d499c476cf42df109efed4bd9982eadd4aca404664e6cca53007bb2b1f58a3449d8b40b9b94a7b9d66e5725b3347952af8d5fbeff3de123092ff904682 2a7b983b22599085ee7bdbcf1ba882b46a5b11120c306b616e2ceab9ab5f80d98de070726d56395ea73db4915df1531b9b0846832613f34e683a76b99dd681f172689d8eebc799455cf0bffa72832deee3e7ced72834c91a3c6902327afbd63446aaed4c5b8c586f3e56bdafb3630d55972b6e(6baa3090d3fe60b79a67b53851730cd8fc345ff9b570da9deda2270c1e93cf30d7045ada9bdca427e6d8df28d7607b3ed655f872425256cd8deee210ea98b752631c8a3b94068c777c38f34dd078fff0cfa02ce01e9a4dfa0b8f008597ba305ad665e425b281d0c5a55c3377f17b12eca69c59ee3032b49b0d626089f3515f6acf39acc88d48ca51b9acd649eac9fcbd48d3069243a78d5ca596d0b6f38e2e8bf4f6fdb91729e5f4e0185ae0be2815ca49efd25fe1691dec446a34e726d5a6c9cde7b7bffa157aa2 601b0c6e55b65ec62327b321c5d0376543ecf394338ec37df945f2dc842947aed16659fd9177df4bd83d5f64b485c1b532159f5fbc582baea9e5f0c29a6d200026f725f101efce5ef79ca8ac5b10157fdff8ea6ab3e6abfba502b708 a9b6ef6465873556708507b32b9e729b818a264d1472f81fba98e9de32820b9ab4ad87440f4dd85c56964183be684288b86a3b408fc26a92dae9fe8df3015a15dfe4cfab9921ffc1af9d96ab96dba727301810b1f7ec530799857816263e0936c171b94f73be8b52a4e375645860576c27755301faf7e9a9e671ecae8055c8f352ddc0c3bec368b4164d55a38c6505f503931c4744daaec7f511685af75d15d408fded36d03b4a9cd12fc701da9b855c24 922db02ac2c24e6ade73fc1f383d64a5504330424cbe1a6dddba938e24ce954177ef173c78e67e9abde250bf8c00c0eb6b63a2cad400538fdf14b75c105297fa83c51dc304a9ba83a0be6d510389498ba95b2ae3387a820b030ee7a93790a997283e46f3e8e7c24e6dfd27377c1487920e2dadc8af9641c5ebe25eb24d2fd42fe51e2b7057b136e1a438382508d73efbf5cbab68be1fc407eed78725dd8cca1593e8c29f05597c59b67d3e04238054070bc491d9a083c535f909f00253a47fe0fbce0d1e721a9a4a475061abe6414799a09f5f21463943cd02459be501c6d4262f1d7cb1a520920d3747e6ffac0143246281a650fef1acab737d8402aa2bd32f9863817ab98140561d50e92f51bbcbdd38649d4fa24846eca5ffd9961f1eb3a8263b832f4477b05465e5b5d001541a2a9c5a723f93f692ab65ef10e37367b8a1f96810d98b90f960e3485b3f3ed3a77a2ee4917a54486ee4f5419211b22ff5a8aeaa2b30469d0cb88a9b2d9b909533d855b661a83752db44afb706414d7e337a178fa8a4227440f0775e769a25a0aebd67701c4319de2e9d4c7e1f28d3f232795fc911ef900cc51381a27e551e6621687b14316c3ff715ac9bd9f170ef7b952ac2397920ab85994d84691ccdc3af cbd8b850ff1d53daf346abcee48ed3e1a12c6c53f3b6d46abc4cbe6cbea9ba3e2945fe16f1d24cc390a91aed7ffdda54a2efc58d6a5673651b70b806c6977314bd689c3415 5c9005cc15538a909cce14470c4b35864355584f5cdcc638a43a8fc4d35d0add6cbe1895d6201991cad9870eab0fc041a29119b6b7cf0971e728518c809c667f3bb0038a9d756feebc5c0e7130da74537aa55de809ec067d20c8cc059b6b6cdaea87916d0a7091ba1f190de4e9b3d0cb24de4b182e77429ee12f8b9cb067f572eb11bcb2b08479b9d8b915950925db89aca66a7ffa0b7eb61b8c890b995e126622d7f768d0e92ed2f607d1dfba9390fd222b9ab664ab6bf84b295aec70257b96 e07af91ff296de4bf36f0bb276829e558e2ea72e33691ac967beaeba41ae2d5b5e3fe8c111ee87b51a953adc74269d5720161d68b49caf650224771db3273bc412328908586a808c621f3a96c5c0cf5f0300b53f719b19e79f93aff5edbfff8f85b6301 5e8863f3540b96a060721bc6eb0ecad3ce116bcc7f862eabc8ecf2ac819ccc7d7bf0efc0d805ec3c7c6fd4a4dbb7d592e11bf302aa395b84359c0c1374a3f831fbbcb824d4d63c3d246e657fa28dcd1e02d8c5b2267a96c08586e347bf9d12c6925c90e803241953ae386e7a675f4e22f7aa5ae037760552848d31353ac3523446c24f468ed3265cf010211908628ce036c044eca823a5b903986809ea01a3e3c077614fa39a928a8d8bdad37cf02d43ce76bd74eb547e46362af55c970275b643e9ab24fe39cc7 69a090cf8bd3702f0a7583ab909d4c5c809459a43ca2992fc5fe3660d25f8e9275ea2619ebf66a8ad874b755826bc1b6434e34538de739f4f785f3183fe740cdd2646b4f75a3c265ea7f59a60c6aba4139a1b36a4 d840d34c4276916067e5151c49a4d43b29aceae0e52fcddfe418a0cdde112e7c4776c5f2f3b7c1d74f811e018fc59cdf96a73013d46f65ef3f72991991d94d66e4c99a0ab2467d27a9108d9564283efb2e37746515da52afc377fe66590d82b8a89d02f9e367684cceebd60f53de14fc05524f6aac61a4b006245c31d5da9c0cb73d6629c211139dfe5451b59f9fcf862d6d3ee3d420fbc9362f06f418ff547a07d51a986090d7db11ab217a315782069d999ea6f1bae60cc37948307c3111625ce9c7dd15b1c9d092142eb4f44250eca9c4c007ba8845f811fa1c2d1c6b42d306c170a44a07ddc9e0e9f3f784a47e5 4a138ec7d6ddfdc22d07bdc40717698347edacccbf7d12b67b0aecf1ea556ae3b5b11abaa73ae4e7766e325cfcfec41d6db69cffa8e338b4b0b87529994d82c5ed8a42a70562a942e0f4c901d43cf026b54f29052cac5c36635c90abf6c373727f29b7a33a834b67807eb83a599bcf8af7ab0b4da447465e3cecffeb35d90871a2d29de36ee445a916b6c96200f76134bfc550af9ee621a893e6a485ffdce04ddb5e993f0053d1c17559dd2620d5ae5118535859c2aa768277b88b9b5d03e8407e8058def1f0d7532ab196b127e6eb559dac252a22202cd52eda570eff06375f0865beb95c5 cc21d8854c090ef6ec75fee0b14c3d633d35b1d3c3a71db3fd9df436515bfef5db7d35822f03b12590d17251d8a479500d1bb098b5576cbbd2203d1386a299f415521ce8c321ba905d19ec94c3ae49678ddd51da693502cc90242cc27ed4a33ce276f921adf91ecc19476c22631a0b23dcbffe5d0bf006c653248c1081c79d949aad51e0df38d11b44982867213196c02279691f5d021bba1a18ce3269562d1fe1aab6b2568fc80e5a35c4256e1b90e45c928883350853ae0892236a2d083d68cdc4dc5c043395d551d4ad49fb9a804f56d44e3a621f3dcad1ed73be1855f52239314f1af987e135151fc5d13fa258e772052592e25af7c5515f9c55667f1a2087019abcf445dc1a55753132b98908c744d74b4081 3bcdbced9a109b103b32995377f790d8178f1400bf6535ee6cc4908170c1347b5de086df154e8e05ed8448b9de6c2080c0d187e5d1db634508c778e613917ccf4ef9c7aa73dca77cfbb3c95a27caa350b25d7c9cf30096c1f2057fba5a39ed1c1ce8b6a463f2f8336d38e9dd297d87e3499b0003c8c6039ec638027219862e49e7cf73ed536aacb472ee8e4b3effaa5d4c2aa5f4f4481e031c036aab95a0786874b8566b77331010bddc57eebef603bce2664718836012788a788902910a013b3e54629fbdd9586959ec95e6fe33a2700fea56a36c57f618a990d213fd242cce90c1520f3bdc2f5a9748d05df631059f28b7821c8c386b38235cc92e1af3c9e17b682fa44459 84e43bb2cd279464e3702d3350589fb91be1e29b04e668d00334e2803ab63964afd1f750d82cade861e09275fc1ec0278357aa482a2730cfd923ba5066f7646df091591c04319eb978e2cbe813ff42228f645b3d44a2b08e95b0669b41e49fc80927651b82467b3015623c25aaf4ed93805561cd5efb42294307e9e05ac996a3899ac4f3d10405aec4d60b9f5384ab589951f954c5494c4d6aa7bb4d41a6d208532d14707a98df31a9f59880c0df14d6352d583ffce3cfc4d634d2a7e13e62222aa0d3744c187b34908aa37eb0d9ec4c92160657d7cb7f1172a598d3fe096998c9e7fcbb65db69179923fe5177f50d7e098de00ad88332 f16ec32a9d2751eff69ea90859dc4a8efdbc25c7ce8bd8e1b9b71d3eacc4964d689e0f78c8b176c3ce70c1886ceaa3122e101a248cbc780bb361c0f5b9e403d5c0ff5985c6d359e85bf8d12b29600bfe99a1ced37d2a65df540b8f54 46325b2cefdbfb b5677b7883d487988d730de322833ef4e003e5f70f2a df3a92793773a197e4f76e6cdf0f8e982fdef9d35b51fcd9c627123684e74cc014bd7053dfc61f6d245b41f140ea5c21b9bc47e1bb83244a938df770095ce5ee155943a53378e6bc00974e75034df42cdfd71a9958b40e55c2757d9aac67889706886432d39200e50c4edd b4c2063459962645dc278cd5a503328cb022af9e7f319c9e522bd53e4e34359c31f9b8c51e2f35b6dffcb4bd01c786d1d8f407ba2e737eceff6dfcb9d5a7276c9bcbed19d7d2a32bbb64ce16894861afcb9fed531a5b14d7a3c60354f3df7079f2ebf5778c135b4ce2a2706cb6ba32a941f1e03a0aa2e09b45340f5c0518236c408550f3c43ab5e6aca33ad1495eeccfcd624eb51b1d8c16b6a9ecd14f5411a7a25ed1a53b5f2e31a7a83b9b483b774d4a79cd627548da05b8c6e8b33f3dbef112dc84e0267406908dd8fa24ca16cf21af0ccf6de8452868d24a011b167382f6bc673b200e1d440c96a4bc87f1dec9eea3b83da5eb66ca7d191af8ea25c3bb7e60df5b8241b91625ff117862aeec7a891b8a566878b67f8ff348b40075667f64f10d144721188cefd5a439d18ba1c9194a8c52762c1a3856888c068bc49e2fa70dcbd008dc8cb6736b5c3921716e58383424de843caea6b9ff67956500e264ffaa87bdbe2c8c451810b5407f89c3eedf8069fab0c7262bf79c149eed7a13ced9031fbd7a9047966028de0673e3a0bafcff75ec37bb28bd97a4e914d8a1af4500f2833630ef9404dbe5aaec1d282384a36ed6fef0dd1e4edeeb7ff0e383cbfb3437ace0b662dad72d5dd960d9a2d72f56b104c0accfb65db964d5fe58ccffff28844524902281d29a971a43493d5bbbe4acf9583f1b05d9d502289514b4666f8b997071ca2aa24eacb851b5788855b5224c678f35254a38f6bf561592929ebe0c0f911cca2467ffbbba83ad6e1fdf24d231ffac1917c5d9688d246fdf2b31fb0d45e315e5d9f7f385071803435439f15e619b684c25ce3b2e6acd95f461d8f965c7a2786a6656d0d81ee00623d52854d9c3f5bef5e994c19c77c05bb5db8247021cd26fcc9edb24 5184be02e37a2aad383931106c37f617cb55af97fb3619e2155a4dfa89d0a7980ad14a5705fb9b128e890c75a0ea70c12506113d9c1e881ccc38455a21e99e0b8c2e84bc65ad87f3456633bc551117db926bddf40e4ff034c368dc3dd4f08db89986c49etb. 8 transposed(H 8block2_items_varietyregular0  0}blosc  ^ (CLASSCARRAY (VERSION1.1 TITLETREE  8 transposed (kindstring (nameN. -!ii fo-!nn lb-!tt oj-&01234ae-! tc`,!@t,!@_0!@12,!@_9!@01234567890123456?  !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abc*!*!*!*!*!*!*!?bP̆bbi81˝\ǂSp].Ev#{5fm(Yջs /  b?;~13N[ 뉽[IEr8}+fJ(iz؅33 -bpʾ3/[T$`Aumg$?kNpO-LҚiSˣ áGuJ}aݓMIٖ0T?M" ,X;կOm#ޮnl U6m-T NSNҋ-Apx} EŢED=B|?`2igVǗY%""`ls(٣Ba {|uOa'Re@7SOQY[g IJMmh!\4?5@JE`Cwo3U5k0p+)a5ASndZ+ÇT:suLGT >:h֞\ uk ^l/?㵶ؾm"?? ?@@????? @  @ `? @? ?@? Y=!fX=!lX=!oX=!aX=!tXA?"      :             @         g             u X         A{!  !  !  !09J!.J!.J!.\N!ii .N!nn .N!tt .N%01234.\W (!oo(!bb(!jj(!ee(!cc(!tt(!__+!112(!__/)0123456789 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/examples/demo/Advanced/test_h5pydata.h50000644000175100001730000002063000000000000025315 0ustar00runnerdocker00000000000000HDF  !`TREEHEAPXrun1_test1@HhTREE8HEAPPSNOD Hhdd 8^  !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcxyzlatlon?7@33333TAQ7@33333T2q\d7@33333TȠw7@33333T^Ф7@33333TH7@33333T/7@33333T!_7@33333T57@33333TM7@33333T}7@33333Ty" 7@33333TM7@33333T|j17@33333T<D7@33333T۲V7@33333Th Wi7@33333T:{7@33333Tj7@33333T*C7@33333T7@33333TW7@33333T(07@33333TX7@33333Tx7@33333T7@33333TE#7@33333Te67@33333TqF I7@33333Tv[7@33333TQn7@33333T47@33333T7@33333T`4>7@33333Tc7@33333T7@33333T"*7@33333T7@33333TO"s7@33333TQ7@33333T{(7@33333T_;7@33333TN7@33333T=`7@33333T?Ls7@33333Tio7@33333T7@33333T87@33333T,ܽ7@33333T-7@33333TX]%7@33333T7@33333Tm7@33333T7@33333T-7@33333TGKZ@7@33333TzR7@33333Tse7@33333T Fx7@33333T 7@33333T597@33333Th37@33333Tb7@33333T{7@33333T7@33333T$'7@33333TVh 7@33333TP 7@33333T浰27@33333T|TE7@33333TW7@33333TDj7@33333T?tA}7@33333Tգ7@33333TkӉ7@33333T.7@33333T27@33333T-bv7@33333Tđ7@33333TZ7@33333Tb7@33333T %7@33333TP77@33333TOJ7@33333TH\7@33333Tޗo7@33333Tu<7@33333T >7@33333Tm7@33333T7(7@33333T7@33333Tcp7@33333T+7@33333T[7@33333T%]7@33333T*7@33333TR<7@33333TJO7@33333T~Ia7@33333Tyt7@33333TSNOD H0( ^ ^(dd ?@4 48` ^h(dd ?@4 4 @^htimedirectioncoordinatemagnetic_3_axiallatlon`z >z >0˾J'm>J'm>ȂҾǯ>ǯ>Ǐؾ@G>@G>:tKZk>Zk>n!],X>],X>m쾬GtӾGtӾY<Y<EGA~~@ vLVV@DW;7,w 7,w o ̩u̩uPP.\3:!޾Sf޾Sf&2L2L‘:T.V}𾾖V}h4eWL?eWL?C:aw1?aw1?xϵAkg @?kg @?GGRnK?RnK?䁚,Oցu6=T?ցu6=T?EѭTuFjZ[?uFjZ[?mn[Qa?Qa?7m1bǻTc?ǻTc?z\>"h\Ec?\Ec?Gp)^?)^?V18uOUyB?OUyB?Q"|u߻]u߻]@e w wP1]l]lu#[jȒjȒࡌ3*қ*қayᢿayᢿ@梿g'g'a~ުު`tg8]n8]n8xpʉ~͟~͟b.1HHA1HHATZ¿YĉC?YĉC?\,ǿeWZغ?eWZغ?Huοߍ*?ߍ*?ktӿWv?Wv?N]xؿnw;?nw;?XoMO޿<k?<k?v$kxܪ?xܪ?GI?"?"?]ӗH""4d?""4d?cOZ1?Z1?+ZU-XU j?XU j?(.U jU j(.@1ٿ@1ٿ.ZU-""4d""4dcO""]ӗHxܪxܪ俢GI?<k<kv$k}w;ݿ}w;ݿdoMO޿WvԿWvԿN]xؿߍ*ʿߍ*ʿktӿfWZغfWZغTuοĉCĉCh,ǿ1HHA?1HHA?TZ¿~͟?~͟?b.8]n?8]n?Hxpʉު?ު?`tgg'?g'?a~ay?ay?@梿*қ?*қ?jȒ?jȒ?ࡌ3]l?]l?u#[ w? w?P1߻]?߻]?@eUyBUyBQ"|)^)^V18u\Ec\EcGpǻTcǻTcz\>"hQaQa7m1buFjZ[uFjZ[mn[؁u6=T؁u6=TEѭTRnKRnK䁚,Okg @kg @GG8aw18aw1xϵA"fWL"fWLC:V}>V}>h42L?2L?‘:T.޾Sf?޾Sf?&??\3:!P?P?.̩u?̩u?7,w ?7,w ?o V?V?@DW;~>~>@ vLY<>Y<>EGAGt>Gt>*X㑾*X㑾mZkɾZkɾn!&GҾ&GҾ:tKǯҾǯҾǏؾE'mоE'mоȂҾz ˾z ˾0˾././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/examples/demo/Advanced/test_table_dc.h50000644000175100001730000127055000000000000025344 0ustar00runnerdocker00000000000000HDF  hq` TREEHEAPXdataH TITLE (CLASSGROUP (VERSION1.0 8PYTABLES_FORMAT_VERSION2.1TREEHEAPX htable_i_table8SNOD(H(H TITLE (CLASSGROUP (VERSION1.0 8 pandas_type frame_table 0pandas_version0.15.2 8 table_typeappendable_frame H index_cols(lp0 (I0 Vindex p1 tp2 a.  values_colsa(lp0 Vint0 p1 aVint1 p2 aVint2 p3 aVint3 p4 aVint4 p5 aVfloat p6 aVobject_1_0 p7 aVobject_1_1 p8 aVobject_1_2 p9 aVobject_1_3 p10 aVobject_1_4 p11 aVobject_1_5 p12 aVobject_1_6 p13 aVobject_1_7 p14 aVobject_1_8 p15 aVobject_1_9 p16 aVobject_2_0 p17 aVobject_2_1 p18 aVobject_2_2 p19 aVobject_2_3 p20 aVobject_2_4 p21 aVobject_2_5 p22 aVobject_2_6 p23 a. non_index_axesq(lp0 (I1 (lp1 Vint0 p2 aVint1 p3 aVint2 p4 aVint3 p5 aVint4 p6 aVfloat p7 aVobject_1_0 p8 aVobject_1_1 p9 aVobject_1_2 p10 aVobject_1_3 p11 aVobject_1_4 p12 aVobject_1_5 p13 aVobject_1_6 p14 aVobject_1_7 p15 aVobject_1_8 p16 aVobject_1_9 p17 aVobject_2_0 p18 aVobject_2_1 p19 aVobject_2_2 p20 aVobject_2_3 p21 aVobject_2_4 p22 aVobject_2_5 p23 aVobject_2_6 p24 atp25 a.  data_columnsa(lp0 Vint0 p1 aVint1 p2 aVint2 p3 aVint3 p4 aVint4 p5 aVfloat p6 aVobject_1_0 p7 aVobject_1_1 p8 aVobject_1_2 p9 aVobject_1_3 p10 aVobject_1_4 p11 aVobject_1_5 p12 aVobject_1_6 p13 aVobject_1_7 p14 aVobject_1_8 p15 aVobject_1_9 p16 aVobject_2_0 p17 aVobject_2_1 p18 aVobject_2_2 p19 aVobject_2_3 p20 aVobject_2_4 p21 aVobject_2_5 p22 aVobject_2_6 p23 a. (nan_repnan 0 encodingUTF-8 (errorsstrict 0 levels  0 metadata(lp0 . hinfoE(dp0 I1 (dp1 Vnames p2 (lp3 NasVtype p4 VIndex p5 ssVindex p6 (dp7 s.H8index@int0 int1  int2 int3 int4 float ?@4 4object_1_0$object_1_1,object_1_24object_1_3<object_1_4Dobject_1_5Lobject_1_6Tobject_1_7\object_1_8dobject_1_9lobject_2_0tobject_2_1object_2_2object_2_3object_2_4object_2_5object_2_6@4(^SNOD; <@>d (CLASSTABLE (VERSION2.7 TITLE 0 FIELD_0_NAMEindex 0 FIELD_1_NAMEint0 0 FIELD_2_NAMEint1 0 FIELD_3_NAMEint2 0 FIELD_4_NAMEint3 0 FIELD_5_NAMEint4 0 FIELD_6_NAMEfloat 8 FIELD_7_NAME object_1_0 8 FIELD_8_NAME object_1_1 8 FIELD_9_NAME object_1_2 8FIELD_10_NAME object_1_3 8FIELD_11_NAME object_1_4 8FIELD_12_NAME object_1_5 8FIELD_13_NAME object_1_6 8FIELD_14_NAME object_1_7 8FIELD_15_NAME object_1_8 8FIELD_16_NAME object_1_9 8FIELD_17_NAME object_2_0 8FIELD_18_NAME object_2_1 8FIELD_19_NAME object_2_2 8FIELD_20_NAME object_2_3 8FIELD_21_NAME object_2_4 8FIELD_22_NAME object_2_5 8FIELD_23_NAME object_2_6 8 FIELD_0_FILL@ 8 FIELD_1_FILL  8 FIELD_2_FILL  8 FIELD_3_FILL  8 FIELD_4_FILL  8 FIELD_5_FILL  @ FIELD_6_FILL ?@4 4 0 FIELD_7_FILL 0 FIELD_8_FILL 0 FIELD_9_FILL 0FIELD_10_FILL 0FIELD_11_FILL 0FIELD_12_FILL 0FIELD_13_FILL 0FIELD_14_FILL 0FIELD_15_FILL 0FIELD_16_FILL 0FIELD_17_FILL 0FIELD_18_FILL 0FIELD_19_FILL 0FIELD_20_FILL 0FIELD_21_FILL 0FIELD_22_FILL 0FIELD_23_FILL 0 index_kindinteger 8 int0_kind(lp0 Vint0 p1 a. 0 int0_metaN. 0 int0_dtypeint32 8 int1_kind(lp0 Vint1 p1 a. 0 int1_metaN. 0 int1_dtypeint32 8 int2_kind(lp0 Vint2 p1 a. 0 int2_metaN. 0 int2_dtypeint32 8 int3_kind(lp0 Vint3 p1 a. 0 int3_metaN. 0 int3_dtypeint32 8 int4_kind(lp0 Vint4 p1 a. 0 int4_metaN. 0 int4_dtypeint32 @ float_kind(lp0 Vfloat p1 a. 0 float_metaN. 0 float_dtypefloat64 @object_1_0_kind(lp0 Vobject_1_0 p1 a. 0object_1_0_metaN. 8object_1_0_dtypebytes64 @object_1_1_kind(lp0 Vobject_1_1 p1 a. 0object_1_1_metaN. 8object_1_1_dtypebytes64 @object_1_2_kind(lp0 Vobject_1_2 p1 a. 0object_1_2_metaN. 8object_1_2_dtypebytes64 @object_1_3_kind(lp0 Vobject_1_3 p1 a. 0object_1_3_metaN. 8object_1_3_dtypebytes64 @object_1_4_kind(lp0 Vobject_1_4 p1 a. 0object_1_4_metaN. 8object_1_4_dtypebytes64 @object_1_5_kind(lp0 Vobject_1_5 p1 a. 0object_1_5_metaN. 8object_1_5_dtypebytes64 @object_1_6_kind(lp0 Vobject_1_6 p1 a. 0object_1_6_metaN. 8object_1_6_dtypebytes64 @object_1_7_kind(lp0 Vobject_1_7 p1 a. 0object_1_7_metaN. 8object_1_7_dtypebytes64 @object_1_8_kind(lp0 Vobject_1_8 p1 a. 0object_1_8_metaN. 8object_1_8_dtypebytes64 @object_1_9_kind(lp0 Vobject_1_9 p1 a. 0object_1_9_metaN. 8object_1_9_dtypebytes64 @object_2_0_kind(lp0 Vobject_2_0 p1 a. 0object_2_0_metaN. 8object_2_0_dtypebytes120 @object_2_1_kind(lp0 Vobject_2_1 p1 a. 0object_2_1_metaN. 8object_2_1_dtypebytes120 @object_2_2_kind(lp0 Vobject_2_2 p1 a. 0object_2_2_metaN. 8object_2_2_dtypebytes120 @object_2_3_kind(lp0 Vobject_2_3 p1 a. 0object_2_3_metaN. 8object_2_3_dtypebytes120 @object_2_4_kind(lp0 Vobject_2_4 p1 a. 0object_2_4_metaN. 8object_2_4_dtypebytes120 @object_2_5_kind(lp0 Vobject_2_5 p1 a. 0object_2_5_metaN. 8object_2_5_dtypebytes120 @object_2_6_kind(lp0 Vobject_2_6 p1 a. 0object_2_6_metaN. 8object_2_6_dtypebytes120 0 NROWS@dTREEp<(  ;M`5a1ed8f2d8b8a9850eef27646045f0dda921ef8cf2ae9ef79342b7834a8bd96c6fb17fb6d36cccce779936df6966dd268ef1d8a7e09c3ec21f0e40756e85ccd7cf14b16965a9984dc0b3e918aee13330ad3e01bf7c559a69d133413482 ~"@?de66deda9ce5673d9d30ca39d9f89773306b28929c80c283011680a41491e6294fcd75a32ae9c22319adb3d66c301fd20831a4006d86c9eb3205c71d43064215683c2c6a4f39b342c27562d953f9f8e14ea66ce52b329a0bf86f9f1b4ΐk ?92d7c410e45bd1a398898d2df24bc1e6a0e78f76defbbd80d9688e04706b2800b6c13279efbad3ae 6fb804a87e444751c817e7d97c4ea6070f4f69ab1591e3c3724912d5981d5b0110173f85fe94277d9c5cdd3e25ca21a91cdcc31cbJ6eb28adb960e7c7a98706d8f1508353d1efedd251fe12061f818e3c3140eecae11d480cfb4c4624984bb9bda88866cc27f0a96b9d9846a1cac7214a36566992482c9c1acb23f482fc50c8ed86c8c95791661a03a5dfaaa35fef621d7cN,?3ddeeb10ff72442c69147c66eabcd7e52dff5f8b64eea29c536e0063d2d4f30d3abcde9f6ac034067179fa3389c7cfa440e2d0c764b0701c59788968fe835a8448b85ffd23efceed29d005ffc86a3b419098c788286a3d5a98d96ee0aPpXE?6483544fe963039f9b3b579e12af26bb7e0522a78c4a2a7555ad88b7b416c89b4b8fc8a6c940ec0b 66266cac0c476ffb22e7295dd0eddc9f1ccf7fad2d7063d4718a15215a7feec5a632dacf0283647596e187261e0a5cc72246b2da12`ʿ886d01826edeb4a76bdd81d7d7739faf54310096a2e25becf8757997517e19db6d706a2d330257842994c1fb23c15a1aeb8c8689285ba4c920c9082dcc26ccced3971540bbe582f780f24bd49c41086d09dec6e6b1f07685910d653ce 3OiC?65b7c139667a6ecc0dd05c318db61e85a95d1d7563c856fabc851e24009b7b93c9a4862634431e8d6941887a47e51781ffad972c09cda284ff41168b22f215d8bfaec8d175ffda20043f45d670ddf099bf8cd63d247fcc9d845df0932Nܜ?eb441e3d2a9ecd0cacc6e2dbee6b69e053a1fdf6e2641fa794b8056f559733d23ff05a8e20d215872d93436279b0e2e296ada23a03c2894f4f95002b4dafe7213d2c410239e1b644f59352f78ede6df8bdbaa16c6f11ba397dbc6bb7c [-;ο888fc7eb52dcffa97b8cbf026d734405ee51647377e11a27c2e6f402e49b999c8cb743e7ef2c79f1d8d986a8b73f758d38e986edb5fa3bf6ba7216cf445e82010d72bff6c94562ef328f6ddf49087d451ba1db5c2dd427ab42343563b  wԿb1e841df91642aa44ca49e71b6a3b91d94b8c23bff59cc9695874ad35da6912a9b78d29315a1251cc55a9237a0eaaee5fe701a3cfa0a7c5d884cb0be161835e02402c6124d40 6cb2a70628d877463fe196e350f22 57e002d6ab3810 goÿ798afa2dd5902635e774ac0bab797d21417df68a69f9d3ab175a5ba15876523a0ed6e1d6254567844c34dbee8e89dc6e0df53bce8a76b41f93303f8844346984c9eeb0f8fb7073a649c2ebe0139 cf736e246d43d2ae833f18a16776c ¨2d345ebb73c4bd3c6b4b4eb65e5b02a1ccbe28c360ee9d114c946f94e278316e995132b6074d7761ba9a613a5005464a6c83b5cee855acd45205f44c2ecd896d8c458273f102407a636176a9a3e6bf038ea85bf0b58c802a9ce2385ad  bV3?52eae87048f48ebee4ef229e924bc0cd175249128ef21ee097db61b050ee9ca0bda04fa5fb13586fec2144149efd3054336a3376b08b9ba98016e580370ff2a863583d58f9ac127542d6deb0cc1d27d6d8c0bf1091cdeef37a0e7fb78bLOU498e5715bbd9bf1612c698449a0bacbdc676f195f864fe2293e10521f435dc94bff11fea4f8136f010aca573c66e203c3f658186c4e03f898594156be60a66dbf79423909632847828baae5c126859be0bdae54589c97fe7e4c2fc3ce m?d6f9d52c846790af6f354f8b9b459abf5fdb217da79c2bfb2fe0ede1e9d442cfd12fb3bc2c43ae94b7e010b75586152c14f1878e0f9569700c3cf2903fd6b1e795c2559f19d944ffea7461c1441b190e973dcd8b7566b77c1a868ec1b 㫢Y5޿87aad7c1bf836ce08cbe9cc735547d4af3879635c41db080c9538c55213645ff00ad777df58e620aa542c3414b83f7699c684b40990ac299a4eb969e9866e3d3521957db9f8d5bb22fe79b02215823fe865d41a9f1eede8cdbab6f14b  #%?0a705b2f0d4e2e72ced21199c1c2e7dc363f9f1c7ee1dd3436414725c843d9023fc18e54258d99e32e662ab8fbd7439b32cb1cad1dd2bcd86b568750dae024cc4ee55f1f80869a60dc256dedfa1d1d80899b39da5baa2c4cbc4746a7di["k@63268bc39027ca8c566f5065209702f0730de3c51c73e77c29bda177840c798dc936abfdff2144bfc51174519886a85a4ea0153690c73452b4e1b245c98d6fcac6b0e558fcfe9352a4858c9b397add9b44ecd1ee4a52ba3bdb7f9beb3ѿ7fdc371872afca731ca4638c27f80a70bdeb873ba909d91c5f9cd04c14438f43094e2bbc9630bb53ac7f5bd652bc110111958f6a01fc9043f8dc38dc2c9b1cb98d9c2baf227bf69b5c7e30c1dc6a4baf239b43df0187ea6d3be3d3cbf 8Iiׇ?7efed34857acbdda54f8ca1106b1be8d2bed416cb5e79bad3b081d66ca71d5ad5c8ecea8be175de3a6ed1dc99bbf1736a7e34a40a3926dc382fc6b5ddb8dd5b58c1ad9ca2e1d9bfcf9e8e631ad58131bc20fd677137322512512341280?3904b4a1d9a17f2251460f7b375cb9157deb6be5140722a7117ad5de948f81a1d0f4281c8fc909ab7e8ded37c16a520f11df99c117b8f7e309aed70800f2c7421efe2d5fecccad059c1ae090942a9a652cc419424fd94e88e40d76d00 E"?671904b364144c150f02646873a00e94427ce9aa2f0fa22522f12b628ccd94d172970840d3eed2d2208a330864b7f30702804595ee4f3c9c2d70278abd7a73cb409bf2904e4c6537919ba1aa10d56a8e4652497aa982beb8741531e0d rS5b5a438b18eb06e9b24175345dcaf3765208d103aeb44e92729c0b5c78974fc96ea3e6a248fa443dddb06f725f5f989c099ad4dbb279d0f4693ee3df1a10a74a45818a52893757072720c9869cfc3cdb2a75209b622a1d397daa57c7e 8p?3d0a67b8c893cd97a9990d92d4a5077917122e66091b7042996bb8e031ca98b792d93b6b39f1e9ccd39134e5c15cfc0319d242d23f93b1 1da3c3f1f84bfb6e8fb50cbd715cbe8cca19a9c207d979d52176ca3b917cc0e9f84ab38701˰+?42d78bca095dcaaa7450b86bc1732b3867d7182f7c031a9dee276a95c3e392ea4d61749d8087e5ecd6fa11d9254654543b0a1a124075ac4dd2498e300a6d91f400bdaa1328302f2871dc46691e854d445d498d6c27 4f86a3c760ace018b7516ace67eedc6e48c7d01daea9f5407713d52a846c56f3feeec60399699b9c4792aeb49d8559026dd14b0c892f46e5daed2874c22a0effbb69ffdcc0111a55afd90a9fcf27a0fa1631f5bba5c0730d1674d819d167881d87045c40 Á<¹?ded69b63153f3e6f304c2ffddc0050a62188728d52a61141f823b7ada4061911f08c87f9157271f025aaddf60b9ef5314a43a45e8ac403af932015299a0157ac07b15fd51370d929aeeebf23147408d60554943d1fca873b5b0fbd9e6}Uݿ76ca9c2d940eb6f9797074bd726145c4e0969fc4fa4f12fd696524c1b93558cddd9ed6464a9e239f8a70993d33b86ef59e48f5b4e289e1ffb03d54d479d383579fd61f479eb93a00a3cb552dd4bfd0860951026b2d639a41002a88e61 n`?461085d893a11c39051001087ce446ec094f948e8821b7c77e9a3ff0512b20d81c335bd9e1fb9e2ae3c1cff498d00e9782fb7019ea239bbd23411220dbdbede6f6366dd383adb764b01d844b85613f6c390baea93fa9562871236e575)ldbb918e58c6d095387268444487f24fb143c8a41fe53fcd82028621562d23ce1dc901f74657ca6bf12dba3135b221ffd59fa00e83a61172dec9125314d44f1fe33357ff4c34e5c4f3a2d7e7c5022907ba95ae7e646ef4385d4d64b294Q ?2202238974818e84dfafffeb8afb1d0c565f64bf00f82d0509fef4d4c4c853c864a17d4e441225548c308b74f90bca05c9f09304eaff4bb0b9205e2cfa23c394c5f008bbd0822d4a058e2ed02a6a6052409f79af02b7ae045f968ae90  +-)ȿ22efc8c618adacb7d4a8035fff951ab0eed60ee8e44fee2d6044864bb59a73ccd7ee6d91d290ae6c d54865080c122b3755ff68777c4ead0930dd684e1c8765e3cf60215eae0b5bed21687404b4d88c240b2f5e1ee13fc0965e4f53af! \vUa5f5c1431d36e2967b1f88698780376c16115a64a159e584975d661ba0f904cf564a8547d1ce3dac195cc9bb6d84a9f53b8a21c0974c56f 94a3eedd211d0687baa6883323fd537ab0db15767b3ee06a7076a7f6759f47887abcea580a" f5ۿ24466edb5f794d925ebc5e242826815a52ebf856d7425d91e0db82ca0635e06e20e44bdf1c05100635e8453f0c377b4e90f4ddc56dea3ed55d4bce9caccdfa4683430477dfc436be0486d02367ca4573fd5013f23f5c3ac6474bcac23# ?ffc476f3f871a5610b57312d0ec7f63695650461566f41f21ca550788c62c3e74faf62e3911bb68b3771ea29f3ed0fea816bc67fff4772e8ac9420fee4142c200204ad32cfc8fd5539de9c52465586b3b681175489f095ad42560a7ae$J!6l?a16bbc8245c056bfb65db101b0c480c4abedbab11a7f3b7e27200642fdc2cf84d5644fcbe8b65fe2d0595071f8b3f8b7f909a8083e8a01ffd9474c6d26efe5b955f097363b8fbb626e5116db4445cd55ceb206a7d2d3d68104f85bf4e% S( Aۿ0c68dddace5c7255b45c5a2ad2ac91b82440ebbbf8c38c545e0432f3fdf346c9e731f300a624bbf98daedb05e4828ddf8f3649f0c64eb25d9ea7500a581f14fe47fcbd8531a7 1230d8580aedce8073359e88f449a25a37071edef0b8& msS12d6634350aa1c764c53dfb7f79675ffa18e078378040c694308a2329ea3c5f8379c22cc366c6370368459543ce6bffc0cb25ab5553d475762da0c5fa0e32f2e3f1022796212ecf54fb051a1cf1d729e50bc009bf8ad83b45bbf09005'iϿ39390e60bd7a4b7d455abef23e8d4cddb77a2ed815814ec9306f09502b6f7738461b23f40f5b492721db68bd70a5456491119a7dd6a992 69fbdd759568a689e8998c0aed3ccdc03174e0d399cbb8cbcae6818d511b445d90e5115876( p(a076b39f73c34fd865568589bd6078334bc582ec22487ccb00f2e71fc5833c5ed9e9ff6ed695aad271f3729ff79b158a28c7dbe2a476ef 4e387062b4493132ff7f4042a25582577c8e2f9d70a6d6cbb85172b7a4fc360268c2f2a045)]zά?2ab1d67c33f492b90eb392b3788cad6b01048c5344e39d8ff9b921de1f2c9755480521190665eeee d7170c15dc3fa923d84085f9ea52e8064d33f9a73aa93ed7c031fcbebc35f5f21d8c47ea86acf49363cbaa3c1477a3f859d21917*9-ل?67ac715cc8818b89454867eb01020ab92b1b180134d4476508bb2d8915eb5069688976cbd9cbe3553a71189f3c0694e fb8a85024271ddd91327795e54efcbe876f95de3bb801715b466940dbae4bd898c15930caedf4bb772c252e92+͑n?8d1ea62448209c362dac69a79fb139c0d32107b8e38a5ff28cc0332dcaafc38ad4adf9505475399ec48f781425954b994460ec04c477c0199c4834d5bc20d2ba4fa3fdd560f61d7bbda76ebd716769ab175cf864383d84fa67eca411d, ՙTd?4833ff579c771ff13788a1b6df196a16ec1f0d343232191c2ceee50d99458fafd9084ef9012a0f6bfe849d66e14e54dfe9843980894dedf87a36f4f4ddf043871598af3284c2 d8b17d40f7b8b2416be25d268d8e6be12db13302bfdc- .\B?ecf5251cefa9aceab5418b74f5276d27442472eb7862305fed82c8d057b16cd83c4c1f60958bb7d9d623d01c72ee10543f6ef4796cdd24b3ceb0e5a1230f96b999434c218019897aa215512cda7f2b977fd82309226b58679c78d92f7.EuZ7160d0d0645d8bbbae28a6d7ba85b0909432cde4a446c6d180c5a3b4c7778fc2c4a105a42ce7e8675bf9755b0eb99d192d1f78767eeb84a7dd159d118b86a632bcecbf2ebe21c29f71a8be2eb883df12614eb5b22dfbe2062b68aca02/  vZ +8f355f6652ffaa9a1d8ac38ddf31ca671071fe6cd824d2925d2cacd5ed10f776baff97f81fcc6af728e3230db56976593504ef350bcf1f7711d29f4ae074f2e0750628c983f2776ba85d7fccd0590a7b75d9e56a14185ee7e86e101800#~3edb09d12d1806a835d2940a223fbec7905bdf914cda92ed419210f7ab288ab464b8b5b00e37d69931542948e92d820f7154c12a7e50deea456577139f6f249fa5f28d90fe9f8e98475ce22e94527f6acd4d8237026b88d204abc448c13?6601652dcf0daa48ee87b7bf963a8ac513497d71a30f697450b0fd32a2e6998d3ea79dbef36cacfe 42f0fdfb014850bce9b453d27bad2 6381407628d75cd7b0bd5bd2578a938e3444ce469752a8b0fca4faaa0b6af0d5a6c3dcfafc2{3Naӿ0946ded5192e7ae4adefd78a52efca580273d6b5c65ae1a0cceb7d3d0cd60459a0122719bf609663c5534a352bbbc691843799bcdd9d769a6dbe4ffed03f191a19af0e06db264a39a628a02df4cd91470ca146e3d489b1872f2f4a6993 _S4ee442cd545f8eb24fe88966ca677a4f4771eddbfaeba32e67e756033c129b43cd6af1e29e831a094d68bdd57ee000c802ce2a84a49df69d6b590976aa2767ffc8a67d01e573dfc2345e94d0818e75568a25b39b1fa6b84ba282e99054 ȝ?bde5af41a98c5f13d1e5b3730ad0c038e4fbc3a0c4bad5b30bf76383c83c6683379fdb9d7c1ffdda8d04edd04295899a42656333ae82e8c7307cf28fa0e682e8765446665b155d007b629cb084249f1fb3bfefdae240c8df2930b35605-܈˳?a4b5248ab67e87d6120383e1197af6d59a08d3e9c22f0b0b600722923ae467f20bab3215cf6c79e98f64d1753758e37fa2d12bb7a8f38cbd7d7147ebce5c12875e0d1cb9db2c14b57a82f6017366ee3acb9703d3eb58ff7c767764eea6b2 Tݿ82d28cf0eefea60fb02a6bebdeef3a9b1156e2ae10db1e4bdde66b7644df7fd7eb4c8856fc8076d4651da726b3f59bcd2d1d19e0dd5a73bb146621f3b0faed37b6dcfc7d891c72c918568d9274face3c25eeb2f69ea5e348d7986f3a975{?1e8ddf07b52898379dfdeba664565c937d9d9e4c4d736374c0c20b5c89ec2400e9e508a07fd6ffe9f9c938d12c907f5dce392ce611a7dec9a0c4cf9ab41171bbe67ac041c35b8b7943af6ac523849af42bf7c85b75d8f76eb3edec03e8 H|?43f5b5348d2be37b19d6cc1780e385fdc7aa1033d32d09cb88fbcbd65471f0938e26f3bd4bf4ade5e64e784484e04c810b5d72be3005e4d13e81abf82f5e953a2fe28e1082924ce5ebd88c9a43e2b6d499c476cf42df109efed4bd9989poN:ҿ2eadd4aca404664e6cca53007bb2b1f58a3449d8b40b9b94a7b9d66e5725b3347952af8d5fbeff3de123092ff904682 2a7b983b22599085ee7bdbcf1ba882b46a5b11120c306b616e2ceab9ab5f80d98de070726d56395ea73db4915: J?df1531b9b0846832613f34e683a76b99dd681f172689d8eebc799455cf0bffa72832deee3e7ced72834c91a3c6902327afbd63446aaed4c5b8c586f3e56bdafb3630d55972b66baa3090d3fe60b79a67b53851730cd8fc345ff9b570d;>?a9deda2270c1e93cf30d7045ada9bdca427e6d8df28d7607b3ed655f872425256cd8deee210ea98b752631c8a3b94068c777c38f34dd078fff0cfa02ce01e9a4dfa0b8f008597ba305ad665e425b281d0c5a55c3377f17b12eca69c59<?ee3032b49b0d626089f3515f6acf39acc88d48ca51b9acd649eac9fcbd48d3069243a78d5ca596d0b6f38e2e8bf4f6fdb91729e5f4e0185ae0be2815ca49efd25fe1691dec446a34e726d5a6c9cde7b7bffa157aa2 601b0c6e55b65e= -sc62327b321c5d0376543ecf394338ec37df945f2dc842947aed16659fd9177df4bd83d5f64b485c1b532159f5fbc582baea9e5f0c29a6d200026f725f101efce5ef79ca8ac5b10157fdff8ea6ab3e6abfba502b708 a9b6ef64658735>ʡAu56708507b32b9e729b818a264d1472f81fba98e9de32820b9ab4ad87440f4dd85c56964183be684288b86a3b408fc26a92dae9fe8df3015a15dfe4cfab9921ffc1af9d96ab96dba727301810b1f7ec530799857816263e0936c171b94? pf73be8b52a4e375645860576c27755301faf7e9a9e671ecae8055c8f352ddc0c3bec368b4164d55a38c6505f503931c4744daaec7f511685af75d15d408fded36d03b4a9cd12fc701da9b855c24 922db02ac2c24e6ade73fc1f383d6@ 4a5504330424cbe1a6dddba938e24ce954177ef173c78e67e9abde250bf8c00c0eb6b63a2cad400538fdf14b75c105297fa83c51dc304a9ba83a0be6d510389498ba95b2ae3387a820b030ee7a93790a997283e46f3e8e7c24e6dfd27A  O377c1487920e2dadc8af9641c5ebe25eb24d2fd42fe51e2b7057b136e1a438382508d73efbf5cbab68be1fc407eed78725dd8cca1593e8c29f05597c59b67d3e04238054070bc491d9a083c535f909f00253a47fe0fbce0d1e721a9a4B3xaݿa475061abe6414799a09f5f21463943cd02459be501c6d4262f1d7cb1a520920d3747e6ffac0143246281a650fef1acab737d8402aa2bd32f9863817ab98140561d50e92f51bbcbdd38649d4fa24846eca5ffd9961f1eb3a8263b832fC'u4477b05465e5b5d001541a2a9c5a723f93f692ab65ef10e37367b8a1f96810d98b90f960e3485b3f3ed3a77a2ee4917a54486ee4f5419211b22ff5a8aeaa2b30469d0cb88a9b2d9b909533d855b661a83752db44afb706414d7e337a1DӜ?78fa8a4227440f0775e769a25a0aebd67701c4319de2e9d4c7e1f28d3f232795fc911ef900cc51381a27e551e6621687b14316c3ff715ac9bd9f170ef7b952ac2397920ab85994d84691ccdc3af cbd8b850ff1d53daf346abcee48eE fL?d3e1a12c6c53f3b6d46abc4cbe6cbea9ba3e2945fe16f1d24cc390a91aed7ffdda54a2efc58d6a5673651b70b806c6977314bd689c3415 5c9005cc15538a909cce14470c4b35864355584f5cdcc638a43a8fc4d35d0add6cbe1895d6F/}RG201991cad9870eab0fc041a29119b6b7cf0971e728518c809c667f3bb0038a9d756feebc5c0e7130da74537aa55de809ec067d20c8cc059b6b6cdaea87916d0a7091ba1f190de4e9b3d0cb24de4b182e77429ee12f8b9cb067f572eb1GmͿ1bcb2b08479b9d8b915950925db89aca66a7ffa0b7eb61b8c890b995e126622d7f768d0e92ed2f607d1dfba9390fd222b9ab664ab6bf84b295aec70257b96 e07af91ff296de4bf36f0bb276829e558e2ea72e33691ac967beaeba41aHGee2d5b5e3fe8c111ee87b51a953adc74269d5720161d68b49caf650224771db3273bc412328908586a808c621f3a96c5c0cf5f0300b53f719b19e79f93aff5edbfff8f85b6301 5e8863f3540b96a060721bc6eb0ecad3ce116bcc7f86I ([@ؿ2eabc8ecf2ac819ccc7d7bf0efc0d805ec3c7c6fd4a4dbb7d592e11bf302aa395b84359c0c1374a3f831fbbcb824d4d63c3d246e657fa28dcd1e02d8c5b2267a96c08586e347bf9d12c6925c90e803241953ae386e7a675f4e22f7aa5J 쌗7T?ae037760552848d31353ac3523446c24f468ed3265cf010211908628ce036c044eca823a5b903986809ea01a3e3c077614fa39a928a8d8bdad37cf02d43ce76bd74eb547e46362af55c970275b643e9ab24fe39cc7 69a090cf8bd370KS ?2f0a7583ab909d4c5c809459a43ca2992fc5fe3660d25f8e9275ea2619ebf66a8ad874b755826bc1b6434e34538de739f4f785f3183fe740cdd2646b4f75a3c265ea7f59a60c6aba4139a1b36a4 d840d34c4276916067e5151c49a4dL YT>ǿ43b29aceae0e52fcddfe418a0cdde112e7c4776c5f2f3b7c1d74f811e018fc59cdf96a73013d46f65ef3f72991991d94d66e4c99a0ab2467d27a9108d9564283efb2e37746515da52afc377fe66590d82b8a89d02f9e367684cceebd6Mu?0f53de14fc05524f6aac61a4b006245c31d5da9c0cb73d6629c211139dfe5451b59f9fcf862d6d3ee3d420fbc9362f06f418ff547a07d51a986090d7db11ab217a315782069d999ea6f1bae60cc37948307c3111625ce9c7dd15b1c9dN$ O?092142eb4f44250eca9c4c007ba8845f811fa1c2d1c6b42d306c170a44a07ddc9e0e9f3f784a47e5 4a138ec7d6ddfdc22d07bdc40717698347edacccbf7d12b67b0aecf1ea556ae3b5b11abaa73ae4e7766e325cfcfec41d6db69cffO `J:a8e338b4b0b87529994d82c5ed8a42a70562a942e0f4c901d43cf026b54f29052cac5c36635c90abf6c373727f29b7a33a834b67807eb83a599bcf8af7ab0b4da447465e3cecffeb35d90871a2d29de36ee445a916b6c96200f76134bP}Qhfc550af9ee621a893e6a485ffdce04ddb5e993f0053d1c17559dd2620d5ae5118535859c2aa768277b88b9b5d03e8407e8058def1f0d7532ab196b127e6eb559dac252a22202cd52eda570eff06375f0865beb95c5 cc21d8854c090eQ AaYؿf6ec75fee0b14c3d633d35b1d3c3a71db3fd9df436515bfef5db7d35822f03b12590d17251d8a479500d1bb098b5576cbbd2203d1386a299f415521ce8c321ba905d19ec94c3ae49678ddd51da693502cc90242cc27ed4a33ce276f92R E[1adf91ecc19476c22631a0b23dcbffe5d0bf006c653248c1081c79d949aad51e0df38d11b44982867213196c02279691f5d021bba1a18ce3269562d1fe1aab6b2568fc80e5a35c4256e1b90e45c928883350853ae0892236a2d083d68Sݧg?cdc4dc5c043395d551d4ad49fb9a804f56d44e3a621f3dcad1ed73be1855f52239314f1af987e135151fc5d13fa258e772052592e25af7c5515f9c55667f1a2087019abcf445dc1a55753132b98908c744d74b4081 3bcdbced9a109bT  s103b32995377f790d8178f1400bf6535ee6cc4908170c1347b5de086df154e8e05ed8448b9de6c2080c0d187e5d1db634508c778e613917ccf4ef9c7aa73dca77cfbb3c95a27caa350b25d7c9cf30096c1f2057fba5a39ed1c1ce8b6aU ݈ ?463f2f8336d38e9dd297d87e3499b0003c8c6039ec638027219862e49e7cf73ed536aacb472ee8e4b3effaa5d4c2aa5f4f4481e031c036aab95a0786874b8566b77331010bddc57eebef603bce2664718836012788a788902910a013bVM\?3e54629fbdd9586959ec95e6fe33a2700fea56a36c57f618a990d213fd242cce90c1520f3bdc2f5a9748d05df631059f28b7821c8c386b38235cc92e1af3c9e17b682fa44459 84e43bb2cd279464e3702d3350589fb91be1e29b04e6W  ʢ?68d00334e2803ab63964afd1f750d82cade861e09275fc1ec0278357aa482a2730cfd923ba5066f7646df091591c04319eb978e2cbe813ff42228f645b3d44a2b08e95b0669b41e49fc80927651b82467b3015623c25aaf4ed9380556X /uIEM 1cd5efb42294307e9e05ac996a3899ac4f3d10405aec4d60b9f5384ab589951f954c5494c4d6aa7bb4d41a6d208532d14707a98df31a9f59880c0df14d6352d583ffce3cfc4d634d2a7e13e62222aa0d3744c187b34908aa37eb0d9ecYmٿ4c92160657d7cb7f1172a598d3fe096998c9e7fcbb65db69179923fe5177f50d7e098de00ad88332 f16ec32a9d2751eff69ea90859dc4a8efdbc25c7ce8bd8e1b9b71d3eacc4964d689e0f78c8b176c3ce70c1886ceaa3122e101a24Z  D8cbc780bb361c0f5b9e403d5c0ff5985c6d359e85bf8d12b29600bfe99a1ced37d2a65df540b8f54 46325b2cefdbfb b5677b7883d487988d730de322833ef4e003e5f70f2a df3a92793773a197e4f76e6cdf0f8e982fdef9d35b51[ hfcd9c627123684e74cc014bd7053dfc61f6d245b41f140ea5c21b9bc47e1bb83244a938df770095ce5ee155943a53378e6bc00974e75034df42cdfd71a9958b40e55c2757d9aac67889706886432d39200e50c4edd b4c20634599626\=!u?45dc278cd5a503328cb022af9e7f319c9e522bd53e4e34359c31f9b8c51e2f35b6dffcb4bd01c786d1d8f407ba2e737eceff6dfcb9d5a7276c9bcbed19d7d2a32bbb64ce16894861afcb9fed531a5b14d7a3c60354f3df7079f2ebf57]k78c135b4ce2a2706cb6ba32a941f1e03a0aa2e09b45340f5c0518236c408550f3c43ab5e6aca33ad1495eeccfcd624eb51b1d8c16b6a9ecd14f5411a7a25ed1a53b5f2e31a7a83b9b483b774d4a79cd627548da05b8c6e8b33f3dbef1^ \ @12dc84e0267406908dd8fa24ca16cf21af0ccf6de8452868d24a011b167382f6bc673b200e1d440c96a4bc87f1dec9eea3b83da5eb66ca7d191af8ea25c3bb7e60df5b8241b91625ff117862aeec7a891b8a566878b67f8ff348b4007_ ?5667f64f10d144721188cefd5a439d18ba1c9194a8c52762c1a3856888c068bc49e2fa70dcbd008dc8cb6736b5c3921716e58383424de843caea6b9ff67956500e264ffaa87bdbe2c8c451810b5407f89c3eedf8069fab0c7262bf79c`mB^?149eed7a13ced9031fbd7a9047966028de0673e3a0bafcff75ec37bb28bd97a4e914d8a1af4500f2833630ef9404dbe5aaec1d282384a36ed6fef0dd1e4edeeb7ff0e383cbfb3437ace0b662dad72d5dd960d9a2d72f56b104c0accfba ݟl?65db964d5fe58ccffff28844524902281d29a971a43493d5bbbe4acf9583f1b05d9d502289514b4666f8b997071ca2aa24eacb851b5788855b5224c678f35254a38f6bf561592929ebe0c0f911cca2467ffbbba83ad6e1fdf24d231ffb bg0|?ac1917c5d9688d246fdf2b31fb0d45e315e5d9f7f385071803435439f15e619b684c25ce3b2e6acd95f461d8f965c7a2786a6656d0d81ee00623d52854d9c3f5bef5e994c19c77c05bb5db8247021cd26fcc9edb24 5184be02e37a2ac $T4/ad383931106c37f617cb55af97fb3619e2155a4dfa89d0a7980ad14a5705fb9b128e890c75a0ea70c12506113d9c1e881ccc38455a21e99e0b8c2e84bc65ad87f3456633bc551117db926bddf40e4ff034c368dc3dd4f08db89986c49>TREE@B@MF@HEAP`Pw (VERSION1.1 <@> HTITLE'Indexes container for table /data/table (CLASSTINDEX (VERSION1.0CpTREEG@U8HEAPhxX(YP (VERSION1.1SNOD8(EPEpG??Ajk m0jP 8TITLEIndex for index column (CLASSINDEX (VERSION2.1 0 FILTERS@ 8 superblocksize@ 8 blocksize@ 8 slicesize  8 chunksize  8 optlevel  8 reduction  (PH 8shuffledeflate^ 0CLASS INDEXARRAYSNOD0O(NIXhW@R@ (VERSION1.1 0TITLE Sorted Values 0 EXTDIM  (  8shuffledeflate^ 0CLASS INDEXARRAY (VERSION1.1 8TITLENumber of chunk in table 0 EXTDIM  (@ 8shuffledeflate^ 0CLASS CACHEARRAY (VERSION1.1 0TITLE Range Values 0 EXTDIM  @ 8shuffledeflate ^ (CLASSEARRAY (VERSION1.1 0TITLE Median ranges 0 EXTDIM  (@ 8shuffledeflate^ 0CLASS CACHEARRAY (VERSION1.1 0TITLEBoundary Values 0 EXTDIM  @ 8shuffledeflate ^ (CLASSEARRAY (VERSION1.1 0TITLE Start bounds 0 EXTDIM  @ 8shuffledeflate ^ (CLASSEARRAY (VERSION1.1 0TITLE End bounds 0 EXTDIM  @ 8shuffledeflate ^ (CLASSEARRAY (VERSION1.1 0TITLE Median bounds 0 EXTDIM  V 8shuffledeflate(Z^ 0CLASS LASTROWARRAYSNOD pLJEHT8PQxYX (VERSION1.1 @TITLELast Row sorted values + boundsAH 8shuffledeflateXb^ 0CLASS LASTROWARRAYsortedindicesrangesmrangesboundsaboundszboundsmboundssortedLRindicesLRHYX 0TITLELast Row indices@ 8 nelements d 8 nelements dTREE%TREE)$?A 0 DIRTY mpTREEq@8HEAPh8P (VERSION1.1P 8TITLEIndex for int0 column (CLASSINDEX (VERSION2.1 0 FILTERS@ 8 superblocksize@ 8 blocksize@ 8 slicesize  8 chunksize  8 optlevel  8 reduction  (`r 8shuffledeflate^ 0CLASS INDEXARRAYSNOD0y(xsXx@| (VERSION1.1 0TITLE Sorted Values 0 EXTDIM  (  8shuffledeflate^ 0CLASS INDEXARRAY (VERSION1.1 8TITLENumber of chunk in table 0 EXTDIM  (  8shuffledeflate ^ 0CLASS CACHEARRAY (VERSION1.1 0TITLE Range Values 0 EXTDIM    8shuffledeflate@^ (CLASSEARRAY (VERSION1.1 0TITLE Median ranges 0 EXTDIM  (  8shuffledeflate^ 0CLASS CACHEARRAY (VERSION1.1 0TITLEBoundary Values 0 EXTDIM    8shuffledeflate@^ (CLASSEARRAY (VERSION1.1 0TITLE Start bounds 0 EXTDIM    8shuffledeflate@^ (CLASSEARRAY (VERSION1.1 0TITLE End bounds 0 EXTDIM    8shuffledeflate@^ (CLASSEARRAY (VERSION1.1 0TITLE Median bounds 0 EXTDIM   8shuffledeflate8^ 0CLASS LASTROWARRAYSNOD vtpH~8`{X (VERSION1.1 @TITLELast Row sorted values + bounds@mH 8shuffledeflateh^ 0CLASS LASTROWARRAYsortedindicesrangesmrangesboundsaboundszboundsmboundssortedLRindicesLRHX 0TITLELast Row indices 8 nelements d 8 nelements dTREED$TREE)z$k m 0 DIRTY pTREE(@8HEAPhHP (VERSION1.1P 8TITLEIndex for int1 column (CLASSINDEX (VERSION2.1 0 FILTERS@ 8 superblocksize@ 8 blocksize@ 8 slicesize  8 chunksize  8 optlevel  8 reduction  (p 8shuffledeflate^ 0CLASS INDEXARRAYSNOD0أ(((X@ (VERSION1.1 0TITLE Sorted Values 0 EXTDIM  (  8shuffledeflate^ 0CLASS INDEXARRAY (VERSION1.1 8TITLENumber of chunk in table 0 EXTDIM  (  8shuffledeflate ^ 0CLASS CACHEARRAY (VERSION1.1 0TITLE Range Values 0 EXTDIM    8shuffledeflate@^ (CLASSEARRAY (VERSION1.1 0TITLE Median ranges 0 EXTDIM  (  8shuffledeflate^ 0CLASS CACHEARRAY (VERSION1.1 0TITLEBoundary Values 0 EXTDIM    8shuffledeflate@^ (CLASSEARRAY (VERSION1.1 0TITLE Start bounds 0 EXTDIM    8shuffledeflate@^ (CLASSEARRAY (VERSION1.1 0TITLE End bounds 0 EXTDIM    8shuffledeflate@^ (CLASSEARRAY (VERSION1.1 0TITLE Median bounds 0 EXTDIM   8shuffledeflateH^ 0CLASS LASTROWARRAYSNOD H8pX (VERSION1.1 @TITLELast Row sorted values + boundsPH 8shuffledeflatex^ 0CLASS LASTROWARRAYsortedindicesrangesmrangesboundsaboundszboundsmboundssortedLRindicesLRHX 0TITLELast Row indices 8 nelements d 8 nelements dTREED6$TREE) $0 0 DIRTY pTREE8@8HEAPhXP (VERSION1.1P 8TITLEIndex for int2 column (CLASSINDEX (VERSION2.1 0 FILTERS@ 8 superblocksize@ 8 blocksize@ 8 slicesize  8 chunksize  8 optlevel  8 reduction  ( 8shuffledeflate^ 0CLASS INDEXARRAYSNOD0(88X@ (VERSION1.1 0TITLE Sorted Values 0 EXTDIM  (  8shuffledeflate^ 0CLASS INDEXARRAY (VERSION1.1 8TITLENumber of chunk in table 0 EXTDIM  (  8shuffledeflate ^ 0CLASS CACHEARRAY (VERSION1.1 0TITLE Range Values 0 EXTDIM    8shuffledeflate@^ (CLASSEARRAY (VERSION1.1 0TITLE Median ranges 0 EXTDIM  (  8shuffledeflate^ 0CLASS CACHEARRAY (VERSION1.1 0TITLEBoundary Values 0 EXTDIM    8shuffledeflate@^ (CLASSEARRAY (VERSION1.1 0TITLE Start bounds 0 EXTDIM    8shuffledeflate@^ (CLASSEARRAY (VERSION1.1 0TITLE End bounds 0 EXTDIM    8shuffledeflate@^ (CLASSEARRAY (VERSION1.1 0TITLE Median bounds 0 EXTDIM   8shuffledeflateX^ 0CLASS LASTROWARRAYSNOD (H8X (VERSION1.1 @TITLELast Row sorted values + bounds`H 8shuffledeflate^ 0CLASS LASTROWARRAYsortedindicesrangesmrangesboundsaboundszboundsmboundssortedLRindicesLRHX 0TITLELast Row indices 8 nelements d 8 nelements dTREED#TREE)# @ 0 DIRTY pTREEH@8HEAPhhP (VERSION1.1P 8TITLEIndex for int3 column (CLASSINDEX (VERSION2.1 0 FILTERS@ 8 superblocksize@ 8 blocksize@ 8 slicesize  8 chunksize  8 optlevel  8 reduction  ( 8shuffledeflate^ 0CLASS INDEXARRAYSNOD0(HHX@( (VERSION1.1 0TITLE Sorted Values 0 EXTDIM  (  8shuffledeflate^ 0CLASS INDEXARRAY (VERSION1.1 8TITLENumber of chunk in table 0 EXTDIM  (  8shuffledeflate ^ 0CLASS CACHEARRAY (VERSION1.1 0TITLE Range Values 0 EXTDIM    8shuffledeflate@^ (CLASSEARRAY (VERSION1.1 0TITLE Median ranges 0 EXTDIM  (  8shuffledeflate^ 0CLASS CACHEARRAY (VERSION1.1 0TITLEBoundary Values 0 EXTDIM    8shuffledeflate@^ (CLASSEARRAY (VERSION1.1 0TITLE Start bounds 0 EXTDIM    8shuffledeflate@^ (CLASSEARRAY (VERSION1.1 0TITLE End bounds 0 EXTDIM    8shuffledeflate@^ (CLASSEARRAY (VERSION1.1 0TITLE Median bounds 0 EXTDIM   8shuffledeflateh^ 0CLASS LASTROWARRAYSNOD 8H8X (VERSION1.1 @TITLELast Row sorted values + boundspH 8shuffledeflate ^ 0CLASS LASTROWARRAYsortedindicesrangesmrangesboundsaboundszboundsmboundssortedLRindicesLRHX 0TITLELast Row indices 8 nelements d 8 nelements dTREED+TREE)*0P 0 DIRTY pTREEX@/8HEAPh2x3P (VERSION1.1DP 8TITLEIndex for int4 column (CLASSINDEX (VERSION2.1 0 FILTERS@ 8 superblocksize@ 8 blocksize@ 8 slicesize  8 chunksize  8 optlevel  8 reduction  ( 8shuffledeflate^ 0CLASS INDEXARRAYSNOD0"(X XX1@8- (VERSION1.1 0TITLE Sorted Values 0 EXTDIM  (  8shuffledeflate^ 0CLASS INDEXARRAY (VERSION1.1 8TITLENumber of chunk in table 0 EXTDIM  (  8shuffledeflate ^ 0CLASS CACHEARRAY (VERSION1.1 0TITLE Range Values 0 EXTDIM    8shuffledeflate@^ (CLASSEARRAY (VERSION1.1 0TITLE Median ranges 0 EXTDIM  (  8shuffledeflate^ 0CLASS CACHEARRAY (VERSION1.1 0TITLEBoundary Values 0 EXTDIM    8shuffledeflate@^ (CLASSEARRAY (VERSION1.1 0TITLE Start bounds 0 EXTDIM x  Om( 0` 0`xл '/5.-=3NYX=5oc7Cd@@@~,x  Om( 0` 0`xл '/La*wYDTW#vo郉   x  Om( 0` 0`xл {^ +,XL3SI]Q6N @@@ Lx  Om( 0` 0`xUB@%EBI%T;[bNߛ# Qgl׷`(y~d*b\?ohвɿ-=??|KIQ3Vtlۺ1ڥfd Ĥ-0;(Wc[E:B~o$t+TS{R1ֶT|wP&^(ת9βpUt-8EXk'2~㝿==@zNKzi|$x6ym<.}**QY"۷D-Q 3V`EXɏ#|#>R&KOǒCgGi*V鋶1iekFo|oOOО*R qp^okf |0#c ܄tVN0e҉P.^o#֎O7v12.}jMY[(oʜ7+$z[zPx  Om( 0` 0`x٭8 DoHI#R! - QlC:vq3K#+hYտtF#?O#uHvuqs$f\s,AXGd !"5[Chh^uGM{uu IZz#s:ceUlicݎkaFrvQ=ҏ# x<^t %{=I`\S82{W[*+Xp-3UQ%(߸GOgZLdXtP Z6AI 2'l=q"(G ɞ>_^8vBTdyZ\u1i$MyCvxP$ یw<q$TLϳc\;u~WFN[?Y|ϼRHV d0% OLK$z,>ǎpqfRckAߝZ#gȟbpBsWMurNʭE={'5x  Om( 0` 0`x  Om( 0` 0`xл '/ B2n-=SrTn=)f',   Xx  Om( 0` 0`xл '/La@aƥgʷVCP3!}    cx  Om( 0` 0`   8shuffledeflate@^ (CLASSEARRAY (VERSION1.1 0TITLE End bounds 0 EXTDIM    8shuffledeflate@^ (CLASSEARRAY (VERSION1.1 0TITLE Median bounds 0 EXTDIM  (1 8shuffledeflatex4^ 0CLASS LASTROWARRAYSNOD HH.8+3X (VERSION1.1 @TITLELast Row sorted values + boundsH 8shuffledeflate<^ 0CLASS LASTROWARRAYsortedindicesrangesmrangesboundsaboundszboundsmboundssortedLRindicesLRH 4X 0TITLELast Row indices 8 nelements d 8 nelements dTREED*TREE)x*@` 0 DIRTY GpTREEhK@@Z8HEAPh0]]P (VERSION1.1HoP 8TITLEIndex for float column (CLASSINDEX (VERSION2.1 0 FILTERS@ 8 superblocksize@ 8 blocksize@ 8 slicesize  8 chunksize  8 optlevel  8 reduction  ( ?@4 4 8shuffledeflate^LSNOD0PT(RMX \@W 0CLASS INDEXARRAY (VERSION1.1 0TITLE Sorted Values 0 EXTDIM  (  8shuffledeflate^ 0CLASS INDEXARRAY (VERSION1.1 8TITLENumber of chunk in table 0 EXTDIM  ( ?@4 4 8shuffledeflate^ 0CLASS CACHEARRAY (VERSION1.1 0TITLE Range Values 0 EXTDIM   ?@4 4 8shuffledeflate ^ (CLASSEARRAY (VERSION1.1 0TITLE Median ranges 0 EXTDIM  ( ?@4 4 8shuffledeflate^ 0CLASS CACHEARRAY (VERSION1.1 0TITLEBoundary Values 0 EXTDIM   ?@4 4 8shuffledeflate ^ (CLASSEARRAY (VERSION1.1 0TITLE Start bounds 0 EXTDIM   ?@4 4 8shuffledeflate ^ (CLASSEARRAY (VERSION1.1 0TITLE End bounds 0 EXTDIM   ?@4 4 8shuffledeflate ^ (CLASSEARRAY (VERSION1.1 0TITLE Median bounds 0 EXTDIM  [0^` 8shuffledeflate^^ 0CLASS LASTROWARRAYSNOD P@OXJH0Y8U (VERSION1.1 @TITLELast Row sorted values + boundsGH 8shuffledeflateg^ 0CLASS LASTROWARRAYsortedindicesrangesmrangesboundsaboundszboundsmboundssortedLRindicesLRH^X 0TITLELast Row indices ?@4 4 8 nelements d 8 nelements dTREEXPTREE)O*PEpG 0 DIRTY sxTREEv@8HEAPh@P (VERSION1.1 @TITLEIndex for object_1_7 column (CLASSINDEX (VERSION2.1P @TITLEIndex for object_1_0 column (CLASSINDEX (VERSION2.1 0 FILTERS@ 8 superblocksize@ 8 blocksize@ 8 slicesize  8 chunksize  8 optlevel  8 reduction ( 8shuffledeflate`>P^ 0CLASS INDEXARRAYSNOD0(X}hxX@  0TITLE Sorted Values 0 EXTDIM  (  8shuffledeflate^ 0CLASS INDEXARRAY (VERSION1.1 8TITLENumber of chunk in table 0 EXTDIM  ( 8shuffledeflate^ 0CLASS CACHEARRAY (VERSION1.1 0TITLE Range Values 0 EXTDIM   8shuffledeflate ^ (CLASSEARRAY (VERSION1.1 0TITLE Median ranges 0 EXTDIM  ( 8shuffledeflate^ 0CLASS CACHEARRAY (VERSION1.1 0TITLEBoundary Values 0 EXTDIM   8shuffledeflate ^ (CLASSEARRAY (VERSION1.1 0TITLE Start bounds 0 EXTDIM   8shuffledeflate ^ (CLASSEARRAY (VERSION1.1 0TITLE End bounds 0 EXTDIM   8shuffledeflate ^ (CLASSEARRAY (VERSION1.1 0TITLE Median bounds 0 EXTDIM  ` 8shuffledeflateH^ 0CLASS LASTROWARRAYxSNOD { zuH8 (VERSION1.1 @TITLELast Row sorted values + boundsrH 8shuffledeflatex^ 0CLASS LASTROWARRAYsortedindicesrangesmrangesboundsaboundszboundsmboundssortedLRindicesLRHX 0TITLELast Row indices 8 nelements d 8 nelements dTREE1(TREE)(oq 0 DIRTY xTREE@8HEAPhhP (VERSION1.1SNOD  @(0P0@`@ooqP @TITLEIndex for object_1_1 column (CLASSINDEX (VERSION2.1 0 FILTERS@ 8 superblocksize@ 8 blocksize@ 8 slicesize  8 chunksize  8 optlevel  8 reduction  ( 8shuffledeflateТ^ 0CLASS INDEXARRAYSNOD0((X@H (VERSION1.1 0TITLE Sorted Values 0 EXTDIM  (  8shuffledeflate^ 0CLASS INDEXARRAY (VERSION1.1 8TITLENumber of chunk in table 0 EXTDIM  ( 8shuffledeflate^ 0CLASS CACHEARRAY (VERSION1.1 0TITLE Range Values 0 EXTDIM   8shuffledeflate ^ (CLASSEARRAY (VERSION1.1 0TITLE Median ranges 0 EXTDIM  ( 8shuffledeflate^ 0CLASS CACHEARRAY (VERSION1.1 0TITLEBoundary Values 0 EXTDIM   8shuffledeflate ^ (CLASSEARRAY (VERSION1.1 0TITLE Start bounds 0 EXTDIM   8shuffledeflate ^ (CLASSEARRAY (VERSION1.1 0TITLE End bounds 0 EXTDIM   8shuffledeflate ^ (CLASSEARRAY (VERSION1.1 0TITLE Median bounds 0 EXTDIM  ` 8shuffledeflatep^ 0CLASS LASTROWARRAY0xSNOD HxHخ8 (VERSION1.1 @TITLELast Row sorted values + bounds`H 8shuffledeflate^ 0CLASS LASTROWARRAYsortedindicesrangesmrangesboundsaboundszboundsmboundssortedLRindicesLRHX 0TITLELast Row indices 8 nelements d 8 nelements dTREE#%TREE)% @ 0 DIRTY xTREEh@8HEAPhHP (VERSION1.1P @TITLEIndex for object_1_2 column (CLASSINDEX (VERSION2.1 0 FILTERS@ 8 superblocksize@ 8 blocksize@ 8 slicesize  8 chunksize  8 optlevel  8 reduction  ( 8shuffledeflate^ 0CLASS INDEXARRAYSNOD0(`pX@( (VERSION1.1 0TITLE Sorted Values 0 EXTDIM  (  8shuffledeflate^ 0CLASS INDEXARRAY (VERSION1.1 8TITLENumber of chunk in table 0 EXTDIM  ( 8shuffledeflate^ 0CLASS CACHEARRAY (VERSION1.1 0TITLE Range Values 0 EXTDIM   8shuffledeflate ^ (CLASSEARRAY (VERSION1.1 0TITLE Median ranges 0 EXTDIM  ( 8shuffledeflate^ 0CLASS CACHEARRAY (VERSION1.1 0TITLEBoundary Values 0 EXTDIM   8shuffledeflate ^ (CLASSEARRAY (VERSION1.1 0TITLE Start bounds 0 EXTDIM   8shuffledeflate ^ (CLASSEARRAY (VERSION1.1 0TITLE End bounds 0 EXTDIM   8shuffledeflate ^ (CLASSEARRAY (VERSION1.1 0TITLE Median bounds 0 EXTDIM  ` 8shuffledeflateP^ 0CLASS LASTROWARRAYxSNOD (XH8 (VERSION1.1 @TITLELast Row sorted values + boundsH 8shuffledeflate^ 0CLASS LASTROWARRAYsortedindicesrangesmrangesboundsaboundszboundsmboundssortedLRindicesLRHX 0TITLELast Row indices 8 nelements d 8 nelements dTREE$ϴTREE)Hh 0 DIRTY xTREEH@ 8HEAPhx(P (VERSION1.1 P @TITLEIndex for object_1_3 column (CLASSINDEX (VERSION2.1 0 FILTERS@ 8 superblocksize@ 8 blocksize@ 8 slicesize  8 chunksize  8 optlevel  8 reduction  ( 8shuffledeflate^ 0CLASS INDEXARRAYSNOD0(@PXh @  (VERSION1.1 0TITLE Sorted Values 0 EXTDIM x3q-WݲZ"#iL\H.YŮc.)jL_.k Sk&ߺ |iil4k[$Sןo UP *׮OcL@ILxJ;^d`4aToZ{?2WL ͅ?Y-c|i03A ` gĕs1SZNφ"T> TuT;m*~DM憖 YTk4ϡ#ri, Q͚f<@8^4R) ~Ce2QkVXx)OB*%M?z$cg}t]olP|G#g/#Nv  Tgw.,m Sqf - I@h"5j$?(+[S뼺Q174.0{H )ɪ e/w\=PmV K+7c-^uuLzr3gAwMǒ *ø8Ƿ {BW%3Pۊ':GC5\cвOyj;y'#lENW$죫X<΃`g lQ #uu'y% ơ.BBBJrw"^p܉pֹ33lP=kնÈwQjb7kmcm9h@:l4N SSz(hF*jp^sOwG\ZSZYQV"JeO eA0ఫwe-6n`&Vwx>+%$X>~#O:?__GOG -dYfYݍrNΌ^$;M4@Q->W7|x9op̿N`Aƒ-=05)[…Zq>2F^3>ba k*.C.B`Y @?7&wEeo묾yKp4l9cVbXRJi[e͌}(﬐FW]G&qxG<̓d'YX R+6Phu3*pEPB![SXkc.xHѢQ1r:]%TRSY1ÉƧzk=+R7oO#0.}xMZhM>ȾVw6w@qwsb`9_I& `@'x jx  Om( 0` 0`x0E'$@ $D!,MUA n# D4Gl#Sg_X1_+ ek'K͝@<"RG :azTمA]iE#AӣTzG?.!x  Om( 0` 0`x  Om( 0` 0`xۭ$1DoH`6?ew7p}u[>Va_W+|tF#*FQ}<_HX[#7{!DNm\,%Mk|C/a[8UɮL}i-9 %@vM[,}<H`@eN-yjiQҧbH`ә;d% GvY^ ,xS7ƣ{z~Ox  Om( 0` 0`x  Om( 0` 0`x  Om( 0` 0`x  Om( 0` 0`x  Om( 0` 0`x  Om( 0` 0`x  Om( 0` 0`  8shuffledeflate ^ (CLASSEARRAY (VERSION1.1 0TITLE Median bounds 0 EXTDIM  ` 8shuffledeflate^ 0CLASS LASTROWARRAYpxSNOD 0H8 (VERSION1.1 @TITLELast Row sorted values + bounds0H 8shuffledeflate^ 0CLASS LASTROWARRAYsortedindicesrangesmrangesboundsaboundszboundsmboundssortedLRindicesLRHXX 0TITLELast Row indices 8 nelements d 8 nelements dTREE!qTREE) 0 DIRTY  xTREE@8HEAPhP (VERSION1.1P @TITLEIndex for object_1_8 column (CLASSINDEX (VERSION2.1 0 FILTERS@ 8 superblocksize@ 8 blocksize@ 8 slicesize  8 chunksize  8 optlevel  8 reduction  ( 8shuffledeflate^ 0CLASS INDEXARRAYSNOD0H(X@h (VERSION1.1 0TITLE Sorted Values 0 EXTDIM  (  8shuffledeflate^ 0CLASS INDEXARRAY (VERSION1.1 8TITLENumber of chunk in table 0 EXTDIM  ( 8shuffledeflate^ 0CLASS CACHEARRAY (VERSION1.1 0TITLE Range Values 0 EXTDIM   8shuffledeflate ^ (CLASSEARRAY (VERSION1.1 0TITLE Median ranges 0 EXTDIM  ( 8shuffledeflate^ 0CLASS CACHEARRAY (VERSION1.1 0TITLEBoundary Values 0 EXTDIM   8shuffledeflate ^ (CLASSEARRAY (VERSION1.1 0TITLE Start bounds 0 EXTDIM   8shuffledeflate ^ (CLASSEARRAY (VERSION1.1 0TITLE End bounds 0 EXTDIM   8shuffledeflate ^ (CLASSEARRAY (VERSION1.1 0TITLE Median bounds 0 EXTDIM  ` 8shuffledeflate^ 0CLASS LASTROWARRAYPxSNOD hH8 (VERSION1.1 @TITLELast Row sorted values + boundsH 8shuffledeflate^ 0CLASS LASTROWARRAYsortedindicesrangesmrangesboundsaboundszboundsmboundssortedLRindicesLRH8X 0TITLELast Row indices 8 nelements d 8 nelements dTREE"OTREE)_ 0 DIRTY HxTREE@08HEAPhP (VERSION1.1SNODJJMuv0xȟ`'P @TITLEIndex for object_1_9 column (CLASSINDEX (VERSION2.1 0 FILTERS@ 8 superblocksize@ 8 blocksize@ 8 slicesize  8 chunksize  8 optlevel  8 reduction  ( 8shuffledeflate^ 0CLASS INDEXARRAYSNOD0p ( X@ (VERSION1.1 0TITLE Sorted Values 0 EXTDIM  (  8shuffledeflate^ 0CLASS INDEXARRAY (VERSION1.1 8TITLENumber of chunk in table 0 EXTDIM  ( 8shuffledeflate^ 0CLASS CACHEARRAY (VERSION1.1 0TITLE Range Values 0 EXTDIM   8shuffledeflate ^ (CLASSEARRAY (VERSION1.1 0TITLE Median ranges 0 EXTDIM  ( 8shuffledeflate^ 0CLASS CACHEARRAY (VERSION1.1 0TITLEBoundary Values 0 EXTDIM   8shuffledeflate ^ (CLASSEARRAY (VERSION1.1 0TITLE Start bounds 0 EXTDIM   8shuffledeflate ^ (CLASSEARRAY (VERSION1.1 0TITLE End bounds 0 EXTDIM   8shuffledeflate ^ (CLASSEARRAY (VERSION1.1 0TITLE Median bounds 0 EXTDIM  ` 8shuffledeflate^ 0CLASS LASTROWARRAYxxSNOD 8 H 8 (VERSION1.1 @TITLELast Row sorted values + boundsH 8shuffledeflate^ 0CLASS LASTROWARRAYsortedindicesrangesmrangesboundsaboundszboundsmboundssortedLRindicesLRH`X 0TITLELast Row indices 8 nelements d 8 nelements dTREEETREE)h 0 DIRTY (*xTREE-@<8HEAPh>?P (VERSION1.1PP @TITLEIndex for object_2_0 column (CLASSINDEX (VERSION2.1 0 FILTERS@ 8 superblocksize@ 8 blocksize@ 8 slicesize  8 chunksize  8 optlevel  8 reduction  ( 8shuffledeflate.^ 0CLASS INDEXARRAYSNOD0P6(4/X=@p9 (VERSION1.1 0TITLE Sorted Values 0 EXTDIM  (  8shuffledeflate^ 0CLASS INDEXARRAY (VERSION1.1 8TITLENumber of chunk in table 0 EXTDIM  ( 8shuffledeflate^ 0CLASS CACHEARRAY (VERSION1.1 0TITLE Range Values 0 EXTDIM   8shuffledeflate^ (CLASSEARRAY (VERSION1.1 0TITLE Median ranges 0 EXTDIM  ( 8shuffledeflate^ 0CLASS CACHEARRAY (VERSION1.1 0TITLEBoundary Values 0 EXTDIM   8shuffledeflate^ (CLASSEARRAY (VERSION1.1 0TITLE Start bounds 0 EXTDIM   8shuffledeflate^ (CLASSEARRAY (VERSION1.1 0TITLE End bounds 0 EXTDIM   8shuffledeflate^ (CLASSEARRAY (VERSION1.1 0TITLE Median bounds 0 EXTDIM  ?` 8shuffledeflate@^ 0CLASS LASTROWARRAYX=xSNOD 3p1,H;87 (VERSION1.1 @TITLELast Row sorted values + bounds)H 8shuffledeflateH^ 0CLASS LASTROWARRAYsortedindicesrangesmrangesboundsaboundszboundsmboundssortedLRindicesLRH@@X 0TITLELast Row indices 8 nelements d 8 nelements dTREE0TREE)') 0 DIRTY TxTREEW@e8HEAPhhpiP (VERSION1.1zP @TITLEIndex for object_2_1 column (CLASSINDEX (VERSION2.1 0 FILTERS@ 8 superblocksize@ 8 blocksize@ 8 slicesize  8 chunksize  8 optlevel  8 reduction  ( 8shuffledeflateX^ 0CLASS INDEXARRAYSNOD00`(^YXg@Pc (VERSION1.1 0TITLE Sorted Values 0 EXTDIM  (  8shuffledeflate^ 0CLASS INDEXARRAY (VERSION1.1 8TITLENumber of chunk in table 0 EXTDIM  ( 8shuffledeflate^ 0CLASS CACHEARRAY (VERSION1.1 0TITLE Range Values 0 EXTDIM   8shuffledeflate^ (CLASSEARRAY (VERSION1.1 0TITLE Median ranges 0 EXTDIM  ( 8shuffledeflate^ 0CLASS CACHEARRAY (VERSION1.1 0TITLEBoundary Values 0 EXTDIM   8shuffledeflate^ (CLASSEARRAY (VERSION1.1 0TITLE Start bounds 0 EXTDIM   8shuffledeflate^ (CLASSEARRAY (VERSION1.1 0TITLE End bounds 0 EXTDIM   8shuffledeflate^ (CLASSEARRAY (VERSION1.1 0TITLE Median bounds 0 EXTDIM  i` 8shuffledeflatexj^ 0CLASS LASTROWARRAY8gxSNOD \P[VHd8a (VERSION1.1 @TITLELast Row sorted values + boundsSH 8shuffledeflater^ 0CLASS LASTROWARRAYsortedindicesrangesmrangesboundsaboundszboundsmboundssortedLRindicesLRH jX 0TITLELast Row indices 8 nelements d 8 nelements dTREE!iNTREE)UpQS 0 DIRTY }xTREEp@N8HEAPhΝP (VERSION1.16P @TITLEIndex for object_2_2 column (CLASSINDEX (VERSION2.1 0 FILTERS@ 8 superblocksize@ 8 blocksize@ 8 slicesize  8 chunksize  8 optlevel  8 reduction  ( 8shuffledeflate^ 0CLASS INDEXARRAYSNOD0(xX@ (VERSION1.1 0TITLE Sorted Values 0 EXTDIM  (  8shuffledeflate^ 0CLASS INDEXARRAY (VERSION1.1 8TITLENumber of chunk in table 0 EXTDIM x[<HSH ;9{^ V#qylRϟc jf_c"b7ohğyx|!p6yκu/uD4P?"=W?/𷤇CgUZut8u;h1s"姗ꥸ%wdc5GWq8rr!FZxle?ېk/&5*/v.Y 89QQ٭CPYF5^Wx̸kݳ сf"^Aw_T0jg&;E(CKq 2i>oD-&İ|_- j[P5@L6o>${-SRšlTY]xoM@{ԎC3ڽ}0?'d:$C$bWQ˧-̆AZ۔q?oDYѿvr=2[gƶ0Ct@ gg9LЅ= {['N-4׃їC6Ծl/9rGKaBa5030qblhN~=QNWi2ٿ=sv!HKr}rg'͙P @1石w"U,4LQ$ Xp8{VN }!aaAzC G ֿU=~rW ̭ݨIs!&ܼWbFP5j!lnCul8-tX 04~<W2j(|ڮy$V^\`4,xI`@h"8n~wJpH / QgkzЮݽI#ۼ{^JHS84/lboL!IBY'!qx홉0 E$^:Xţb F|C>E!"}$5Z֏#GUUP_W%B0D&R;۹ H}n= DtB(@j_;K9Uy!{l5;c9r  Pj۬>΄=lT H ")bȫ}ξΠh23wˋLذ&:ª' S)n@ ߸;KXx{H'1B ur^{XЊ%mvJщ) tVL)è ]$PL.yaҩ"vHmߙ~ QfZ/*ulُpj YT@qr+~'u zDmC9;74ɄΎp1zOiEZ3٨c.Wm׎~N~`$EaVsy2#sRo'v/85K& LrwR' b qUP1 (OiƳ|a =za{!RG3*Pޙ;w;-]{3 Pne!Nf]- U&}%/K6tQHvo5n:aXwBXghfͅIz~dW@h=yK Zfun/vwlzx'ShɵW 0` XC=#;c.Gd(;ӂb,7IBp.Xԑ;5<\Cxj`w7q:?wSܜx[#;Eϐx 1cȇwYQmW++HH?9QS?yz/$c@; :/ҸȞ ,35]4X/'s{')%^`^-PKz g_nUZ>#Cú: 4Xcl~cIn۽ص QMH#|#R},3(jhMvCWa ܚu$@vXX2H$xX^ƟiMW˄N8؍0vo<~,Y rI] ޘmmjRS&ljzG lX^e*y0iHm] s2OҭsT0ws9V~TPhEPmx;nƝ5=Njh29bh&F]ezv揂m(rʇ "@" q~o$0v_܆3&So.<̛r91N`/Ám1{ Ez3"\oߚG?˸3 ( 8shuffledeflate^ 0CLASS CACHEARRAY (VERSION1.1 0TITLE Range Values 0 EXTDIM   8shuffledeflate^ (CLASSEARRAY (VERSION1.1 0TITLE Median ranges 0 EXTDIM  ( 8shuffledeflate^ 0CLASS CACHEARRAY (VERSION1.1 0TITLEBoundary Values 0 EXTDIM   8shuffledeflate^ (CLASSEARRAY (VERSION1.1 0TITLE Start bounds 0 EXTDIM   8shuffledeflate^ (CLASSEARRAY (VERSION1.1 0TITLE End bounds 0 EXTDIM   8shuffledeflate^ (CLASSEARRAY (VERSION1.1 0TITLE Median bounds 0 EXTDIM  ` 8shuffledeflate֞^ 0CLASS LASTROWARRAYxSNOD V`H>8 (VERSION1.1 @TITLELast Row sorted values + bounds}H 8shuffledeflate^ 0CLASS LASTROWARRAYsortedindicesrangesmrangesboundsaboundszboundsmboundssortedLRindicesLRH~X 0TITLELast Row indices 8 nelements d 8 nelements dTREE$EJTREE),P{p} 0 DIRTY xTREE@v8HEAPhFP (VERSION1.1SNOD@hh'')HQpQS({P{p}α 0@n..0^P @TITLEIndex for object_2_3 column (CLASSINDEX (VERSION2.1 0 FILTERS@ 8 superblocksize@ 8 blocksize@ 8 slicesize  8 chunksize  8 optlevel  8 reduction  ( 8shuffledeflate^^ 0CLASS INDEXARRAYSNOD0(X6@ (VERSION1.1 0TITLE Sorted Values 0 EXTDIM  (  8shuffledeflate^ 0CLASS INDEXARRAY (VERSION1.1 8TITLENumber of chunk in table 0 EXTDIM  ( 8shuffledeflate^ 0CLASS CACHEARRAY (VERSION1.1 0TITLE Range Values 0 EXTDIM   8shuffledeflate^ (CLASSEARRAY (VERSION1.1 0TITLE Median ranges 0 EXTDIM  ( 8shuffledeflate^ 0CLASS CACHEARRAY (VERSION1.1 0TITLEBoundary Values 0 EXTDIM   8shuffledeflate^ (CLASSEARRAY (VERSION1.1 0TITLE Start bounds 0 EXTDIM   8shuffledeflate^ (CLASSEARRAY (VERSION1.1 0TITLE End bounds 0 EXTDIM   8shuffledeflate^ (CLASSEARRAY (VERSION1.1 0TITLE Median bounds 0 EXTDIM  F` 8shuffledeflate^ 0CLASS LASTROWARRAYxSNOD ~ֺHf8F (VERSION1.1 @TITLELast Row sorted values + boundsH 8shuffledeflate.^ 0CLASS LASTROWARRAYsortedindicesrangesmrangesboundsaboundszboundsmboundssortedLRindicesLRHX 0TITLELast Row indices 8 nelements d 8 nelements dTREE"#FTREE)α 0 DIRTY nxTREE@V8HEAPh&P (VERSION1.1>P @TITLEIndex for object_2_4 column (CLASSINDEX (VERSION2.1 0 FILTERS@ 8 superblocksize@ 8 blocksize@ 8 slicesize  8 chunksize  8 optlevel  8 reduction  ( 8shuffledeflate>^ 0CLASS INDEXARRAYSNOD0(X@ (VERSION1.1 0TITLE Sorted Values 0 EXTDIM  (  8shuffledeflate^ 0CLASS INDEXARRAY (VERSION1.1 8TITLENumber of chunk in table 0 EXTDIM  ( 8shuffledeflate^ 0CLASS CACHEARRAY (VERSION1.1 0TITLE Range Values 0 EXTDIM   8shuffledeflate^ (CLASSEARRAY (VERSION1.1 0TITLE Median ranges 0 EXTDIM  ( 8shuffledeflate^ 0CLASS CACHEARRAY (VERSION1.1 0TITLEBoundary Values 0 EXTDIM   8shuffledeflate^ (CLASSEARRAY (VERSION1.1 0TITLE Start bounds 0 EXTDIM   8shuffledeflate^ (CLASSEARRAY (VERSION1.1 0TITLE End bounds 0 EXTDIM   8shuffledeflate^ (CLASSEARRAY (VERSION1.1 0TITLE Median bounds 0 EXTDIM  &` 8shuffledeflate^ 0CLASS LASTROWARRAYxSNOD ^HF8& (VERSION1.1 @TITLELast Row sorted values + boundsH 8shuffledeflate^ 0CLASS LASTROWARRAYsortedindicesrangesmrangesboundsaboundszboundsmboundssortedLRindicesLRHX 0TITLELast Row indices 8 nelements d 8 nelements dTREEBTREE)ڹ 0 DIRTY NxTREE @68HEAPhP (VERSION1.1.P @TITLEIndex for object_2_5 column (CLASSINDEX (VERSION2.1 0 FILTERS@ 8 superblocksize@ 8 blocksize@ 8 slicesize  8 chunksize  8 optlevel  8 reduction  ( 8shuffledeflate ^ 0CLASS INDEXARRAYSNOD0v( X@ (VERSION1.1 0TITLE Sorted Values 0 EXTDIM  (  8shuffledeflate^ 0CLASS INDEXARRAY (VERSION1.1 8TITLENumber of chunk in table 0 EXTDIM  ( 8shuffledeflate^ 0CLASS CACHEARRAY (VERSION1.1 0TITLE Range Values 0 EXTDIM   8shuffledeflate^ (CLASSEARRAY (VERSION1.1 0TITLE Median ranges 0 EXTDIM  ( 8shuffledeflate^ 0CLASS CACHEARRAY (VERSION1.1 0TITLEBoundary Values 0 EXTDIM   8shuffledeflate^ (CLASSEARRAY (VERSION1.1 0TITLE Start bounds 0 EXTDIM   8shuffledeflate^ (CLASSEARRAY (VERSION1.1 0TITLE End bounds 0 EXTDIM   8shuffledeflate^ (CLASSEARRAY (VERSION1.1 0TITLE Median bounds 0 EXTDIM  ` 8shuffledeflate^ 0CLASS LASTROWARRAY~xSNOD > H&8 (VERSION1.1 @TITLELast Row sorted values + boundsH 8shuffledeflate%^ 0CLASS LASTROWARRAYsortedindicesrangesmrangesboundsaboundszboundsmboundssortedLRindicesLRHfX 0TITLELast Row indices 8 nelements d 8 nelements dTREEhTREE)l 0 DIRTY .1xTREE4@S8HEAPhjVWP (VERSION1.1hP @TITLEIndex for object_2_6 column (CLASSINDEX (VERSION2.1 0 FILTERS@ 8 superblocksize@ 8 blocksize@ 8 slicesize  8 chunksize  8 optlevel  8 reduction  ( 8shuffledeflate5^ 0CLASS INDEXARRAYSNOD0V=(;6XZU@v@ (VERSION1.1 0TITLE Sorted Values 0 EXTDIM  (  8shuffledeflate^ 0CLASS INDEXARRAY (VERSION1.1 8TITLENumber of chunk in table 0 EXTDIM  ( 8shuffledeflate^ 0CLASS CACHEARRAY (VERSION1.1 0TITLE Range Values 0 EXTDIM   8shuffledeflate^ (CLASSEARRAY (VERSION1.1 0TITLE Median ranges 0 EXTDIM  ( 8shuffledeflate^ 0CLASS CACHEARRAY (VERSION1.1 0TITLEBoundary Values 0 EXTDIM   8shuffledeflate^ (CLASSEARRAY (VERSION1.1 0TITLE Start bounds 0 EXTDIM   8shuffledeflate^ (CLASSEARRAY (VERSION1.1 0TITLE End bounds 0 EXTDIM   8shuffledeflate^ (CLASSEARRAY (VERSION1.1 0TITLE Median bounds 0 EXTDIM xZە,9PnᘇaUȡ?GtT[=,/ '_s_x<~˫h,+U!DMd2 ~yUv.,zυ{wo-!_)~ǰaƲyiּ}h=-s:yVҹau̶^wHZ.`G;#;6~H?jCv{:r=M/)h|q ӶMiE,aMN Ҍ󷦇Ce+ ބN$gSg!'4 5 #o7CCA$DIx" >WCAۥѻj3ϰc(A OP>jAGq_[C!ӢvXw@3Jg*n Ruqm졞tB%)m *C!cSk%]^m'cmY87DԁG5r / twϭwx\L%?1׋V廡d>t.oCK=c5u-W^0wnhx",`k./}2ޖhB53+nփJ'1:\L*yQ jH r_[C!pˬ+hQ-d3̆q1t X?E1p2d=6| 'k!O;p~<~FT:8lCއk@]p "D c5w3%y:aIL&ٰ&30 A;kz< Mz E@Gc/K0z5f]:9,P 3' Z x"a?Gr6}ѽJ doڿ`$d88!4U` RphP ₈󷦇C #hnxyv6><7hj2$1BPR\qF?J_kjl O"7NIx[ۍ%+ P6?0plp}H7̨uA1.Wv~GWL@X7΍B/"p{SJgj6SZQ=Y"VcΣ ]foKJ[ӡs05Fm>|l¯9fye;ͣ֘zKg1Nqćp=</"0I8&]zc վG&;d#uRnVFX<ڨ:ZH4DU\k] œ==1Z'UQh Ψ˪0~JІ.Jzf]ES$ec8 E:T橳Waܤfvbwj7t"[ Xqߦ<N{#:IO-E˚6+8 om@yC϶Ҳ7ն>7p dfǏ}IOPۓwBhh `Rro vYM50?DV/ D^WDn7d`f) fA1\E3L濄HzC@{`NV =Ggtu!`eO1]!QqHu nO>'C!En_, J~o+D; >ubr3L({fAS?da`8/~oO>5gN#v:|%Ola_ }R _7ĂX5x@?_D`G-nJ0Y\s9πGȳ`[['9ÏmpA,h_~50g0+nΥN -!(p~<x[퍣=Rrl>/ܽ"Z$`71O07_䟨wzx|!.ɬ׊ c /1hG3o XLF[?ww"?: 3wG 6(Ņbn_߆rPgzt.\>EQDoM*ae 0Zɹc)A- _wE?$x[ە:P&C@8HBpgCt{\6???_o'o'&D?b/~"P=)gd;p GƷ͟}UmFMddd^[C!lm7NܙYf! :XH>1{j04,݉]AC8|@?߈ϰ$uJbR 4ׯ>4R}A޳4ث2׽g[wyo򷦇C`ys3ُ=.W5#G/7uIJs"U(k*A'Zu9Z>oD sU:tYS3{*!W#xQ,»>s̵8 f9GfgFo򷦇C MV~R@]m L9ksa4{1DJ 8A{|%@^LfKL=A쑠%sn HB/ZrA_oǮ{4 yW_7C_rrbϣ;@GPt 4 J!eB fR!'0Zp~<??d;l4Pg9ĀΞ/̐ OP@+Fo ooM? Y}l2/M|z'9KU`o6U*D,mS ccYi`I~{_;l{΃9Tӄ j`bV3s9A;{\` 5-!:l~$iRO ^ ;_ Щ3'y |A5jډ jiC8 >x4 ~ݥkF!1n|Z9R.@F>ֿ5=+S6'@ wd!k පlt7@py@`Ta0yG+a{DyT|{_! jW` 8shuffledeflate"X^ 0CLASS LASTROWARRAYTxSNOD :v83HR8> (VERSION1.1 @TITLELast Row sorted values + bounds0H 8shuffledeflateR`^ 0CLASS LASTROWARRAYsortedindicesrangesmrangesboundsaboundszboundsmboundssortedLRindicesLRHWX 0TITLELast Row indices 8 nelements d 8 nelements dTREE%mTREE)?q.0 0 DIRTY x[3;RRlC9KfsZ&RJ=x!?9s~n\./DY$m|&j2=ϩc恜`铈Xt|$ޜln."pI>f'Bґ;:vӳWV^:o#(>7Ι:L6i^=.8kMN8!8Ԣ°FҎFS7\4̦nO _NG{.*J^H]h&bp~=7@f|zz5\}УIQ].#^8N-Q'~p:z5piSZM+HPS5]~FY"D[v %}F%V9gnK5( E"FYR~]KG?ѴoG>oVWxQ lel`7"p Y g>;nMJtS{ B0(ݙkn O0%suhhEP/76ِrri_ԩRҶK׆ٗ( =_[{ t: ϴw~tc]H$)GG qk%)]rja xvDZD_/7"pR^%{O1;h=Nu Ckj9 x'ܨ qƟ"yށ>ߵu~ <+sCS!7ܓZ)PGm[ԣh32i EŸ&m_MO 1ÿ/'ccL~x;~-OŴD0 ^0 0޸󻦋Eon0ׇ`&>~ %H^ҚW#%f81?_Y|x  Om( 0` 0`x[ٱ#9 P&px)ޙ\v7( `'''6Ο!a#4_"sf!X=b;,2swY7@`Uw܄_Ǝ/C!O)Ì2t󘺦7X.+UI,:k]d Z$%nCH՞XN./j%zك̍p{+(XY#'gxӈk͂>R;s<,xk  "[ԱO).P`Mqq JoQ\nx8?߈@8j=l xp{=: ŀ w Ω14'] kR<iKVS>䠠ƙ1=Qˊ%J].sӮE{l_M|I3yAgjU9(nTTx߉@(NׇdGBx:r`i(֕dLޣ.> BfZ^Mߨ~<%щd0v[˖=)~kM:l(Mt6J p\pᛰ ^w"ޏ]{<*n \)v5ul2?5U=c* eE*@.5ιQ=~:ıd* &`vf]ݥoY)Xr{qx31*Q~|'eGF~{?ZmjD†>ֿމ@?׊5D~ߨ?܎ ?otBc!EolB\4 `H~ph>o7/ETGI'6r_(c/Ȉ'$~]n7|2O-t7 AJ\gHOG >=N?a9deda2270c1e93cf30d7045ada9bdca427e6d8df28d7607b3ed655f872425256cd8deee210ea98b752631c8a3b94068c777c38f34dd078fff0cfa02ce01e9a4dfa0b8f008597ba305ad665e425b281d0c5a55c3377f17b12eca69c59<?ee3032b49b0d626089f3515f6acf39acc88d48ca51b9acd649eac9fcbd48d3069243a78d5ca596d0b6f38e2e8bf4f6fdb91729e5f4e0185ae0be2815ca49efd25fe1691dec446a34e726d5a6c9cde7b7bffa157aa2 601b0c6e55b65e=-s c62327b321c5d0376543ecf394338ec37df945f2dc842947aed16659fd9177df4bd83d5f64b485c1b532159f5fbc582baea9e5f0c29a6d200026f725f101efce5ef79ca8ac5b10157fdff8ea6ab3e6abfba502b708 a9b6ef64658735>ʡAu56708507b32b9e729b818a264d1472f81fba98e9de32820b9ab4ad87440f4dd85c56964183be684288b86a3b408fc26a92dae9fe8df3015a15dfe4cfab9921ffc1af9d96ab96dba727301810b1f7ec530799857816263e0936c171b94?p f73be8b52a4e375645860576c27755301faf7e9a9e671ecae8055c8f352ddc0c3bec368b4164d55a38c6505f503931c4744daaec7f511685af75d15d408fded36d03b4a9cd12fc701da9b855c24 922db02ac2c24e6ade73fc1f383d6@ 4a5504330424cbe1a6dddba938e24ce954177ef173c78e67e9abde250bf8c00c0eb6b63a2cad400538fdf14b75c105297fa83c51dc304a9ba83a0be6d510389498ba95b2ae3387a820b030ee7a93790a997283e46f3e8e7c24e6dfd27A O 377c1487920e2dadc8af9641c5ebe25eb24d2fd42fe51e2b7057b136e1a438382508d73efbf5cbab68be1fc407eed78725dd8cca1593e8c29f05597c59b67d3e04238054070bc491d9a083c535f909f00253a47fe0fbce0d1e721a9a4B3xaݿa475061abe6414799a09f5f21463943cd02459be501c6d4262f1d7cb1a520920d3747e6ffac0143246281a650fef1acab737d8402aa2bd32f9863817ab98140561d50e92f51bbcbdd38649d4fa24846eca5ffd9961f1eb3a8263b832fC'u4477b05465e5b5d001541a2a9c5a723f93f692ab65ef10e37367b8a1f96810d98b90f960e3485b3f3ed3a77a2ee4917a54486ee4f5419211b22ff5a8aeaa2b30469d0cb88a9b2d9b909533d855b661a83752db44afb706414d7e337a1DӜ?78fa8a4227440f0775e769a25a0aebd67701c4319de2e9d4c7e1f28d3f232795fc911ef900cc51381a27e551e6621687b14316c3ff715ac9bd9f170ef7b952ac2397920ab85994d84691ccdc3af cbd8b850ff1d53daf346abcee48eEfL? d3e1a12c6c53f3b6d46abc4cbe6cbea9ba3e2945fe16f1d24cc390a91aed7ffdda54a2efc58d6a5673651b70b806c6977314bd689c3415 5c9005cc15538a909cce14470c4b35864355584f5cdcc638a43a8fc4d35d0add6cbe1895d6F/}RG201991cad9870eab0fc041a29119b6b7cf0971e728518c809c667f3bb0038a9d756feebc5c0e7130da74537aa55de809ec067d20c8cc059b6b6cdaea87916d0a7091ba1f190de4e9b3d0cb24de4b182e77429ee12f8b9cb067f572eb1GmͿ1bcb2b08479b9d8b915950925db89aca66a7ffa0b7eb61b8c890b995e126622d7f768d0e92ed2f607d1dfba9390fd222b9ab664ab6bf84b295aec70257b96 e07af91ff296de4bf36f0bb276829e558e2ea72e33691ac967beaeba41aHGee2d5b5e3fe8c111ee87b51a953adc74269d5720161d68b49caf650224771db3273bc412328908586a808c621f3a96c5c0cf5f0300b53f719b19e79f93aff5edbfff8f85b6301 5e8863f3540b96a060721bc6eb0ecad3ce116bcc7f86I([@ؿ 2eabc8ecf2ac819ccc7d7bf0efc0d805ec3c7c6fd4a4dbb7d592e11bf302aa395b84359c0c1374a3f831fbbcb824d4d63c3d246e657fa28dcd1e02d8c5b2267a96c08586e347bf9d12c6925c90e803241953ae386e7a675f4e22f7aa5J쌗7T? ae037760552848d31353ac3523446c24f468ed3265cf010211908628ce036c044eca823a5b903986809ea01a3e3c077614fa39a928a8d8bdad37cf02d43ce76bd74eb547e46362af55c970275b643e9ab24fe39cc7 69a090cf8bd370KS ?2f0a7583ab909d4c5c809459a43ca2992fc5fe3660d25f8e9275ea2619ebf66a8ad874b755826bc1b6434e34538de739f4f785f3183fe740cdd2646b4f75a3c265ea7f59a60c6aba4139a1b36a4 d840d34c4276916067e5151c49a4dLYT>ǿ 43b29aceae0e52fcddfe418a0cdde112e7c4776c5f2f3b7c1d74f811e018fc59cdf96a73013d46f65ef3f72991991d94d66e4c99a0ab2467d27a9108d9564283efb2e37746515da52afc377fe66590d82b8a89d02f9e367684cceebd6Mu?0f53de14fc05524f6aac61a4b006245c31d5da9c0cb73d6629c211139dfe5451b59f9fcf862d6d3ee3d420fbc9362f06f418ff547a07d51a986090d7db11ab217a315782069d999ea6f1bae60cc37948307c3111625ce9c7dd15b1c9dN$ O?092142eb4f44250eca9c4c007ba8845f811fa1c2d1c6b42d306c170a44a07ddc9e0e9f3f784a47e5 4a138ec7d6ddfdc22d07bdc40717698347edacccbf7d12b67b0aecf1ea556ae3b5b11abaa73ae4e7766e325cfcfec41d6db69cffO`J: a8e338b4b0b87529994d82c5ed8a42a70562a942e0f4c901d43cf026b54f29052cac5c36635c90abf6c373727f29b7a33a834b67807eb83a599bcf8af7ab0b4da447465e3cecffeb35d90871a2d29de36ee445a916b6c96200f76134bP}Qhfc550af9ee621a893e6a485ffdce04ddb5e993f0053d1c17559dd2620d5ae5118535859c2aa768277b88b9b5d03e8407e8058def1f0d7532ab196b127e6eb559dac252a22202cd52eda570eff06375f0865beb95c5 cc21d8854c090eQAaYؿ f6ec75fee0b14c3d633d35b1d3c3a71db3fd9df436515bfef5db7d35822f03b12590d17251d8a479500d1bb098b5576cbbd2203d1386a299f415521ce8c321ba905d19ec94c3ae49678ddd51da693502cc90242cc27ed4a33ce276f92RE[ 1adf91ecc19476c22631a0b23dcbffe5d0bf006c653248c1081c79d949aad51e0df38d11b44982867213196c02279691f5d021bba1a18ce3269562d1fe1aab6b2568fc80e5a35c4256e1b90e45c928883350853ae0892236a2d083d68Sݧg?cdc4dc5c043395d551d4ad49fb9a804f56d44e3a621f3dcad1ed73be1855f52239314f1af987e135151fc5d13fa258e772052592e25af7c5515f9c55667f1a2087019abcf445dc1a55753132b98908c744d74b4081 3bcdbced9a109bTs  103b32995377f790d8178f1400bf6535ee6cc4908170c1347b5de086df154e8e05ed8448b9de6c2080c0d187e5d1db634508c778e613917ccf4ef9c7aa73dca77cfbb3c95a27caa350b25d7c9cf30096c1f2057fba5a39ed1c1ce8b6aU݈ ? 463f2f8336d38e9dd297d87e3499b0003c8c6039ec638027219862e49e7cf73ed536aacb472ee8e4b3effaa5d4c2aa5f4f4481e031c036aab95a0786874b8566b77331010bddc57eebef603bce2664718836012788a788902910a013bVM\?3e54629fbdd9586959ec95e6fe33a2700fea56a36c57f618a990d213fd242cce90c1520f3bdc2f5a9748d05df631059f28b7821c8c386b38235cc92e1af3c9e17b682fa44459 84e43bb2cd279464e3702d3350589fb91be1e29b04e6W ʢ? 68d00334e2803ab63964afd1f750d82cade861e09275fc1ec0278357aa482a2730cfd923ba5066f7646df091591c04319eb978e2cbe813ff42228f645b3d44a2b08e95b0669b41e49fc80927651b82467b3015623c25aaf4ed9380556X/uIEM  1cd5efb42294307e9e05ac996a3899ac4f3d10405aec4d60b9f5384ab589951f954c5494c4d6aa7bb4d41a6d208532d14707a98df31a9f59880c0df14d6352d583ffce3cfc4d634d2a7e13e62222aa0d3744c187b34908aa37eb0d9ecYmٿ4c92160657d7cb7f1172a598d3fe096998c9e7fcbb65db69179923fe5177f50d7e098de00ad88332 f16ec32a9d2751eff69ea90859dc4a8efdbc25c7ce8bd8e1b9b71d3eacc4964d689e0f78c8b176c3ce70c1886ceaa3122e101a24Z D 8cbc780bb361c0f5b9e403d5c0ff5985c6d359e85bf8d12b29600bfe99a1ced37d2a65df540b8f54 46325b2cefdbfb b5677b7883d487988d730de322833ef4e003e5f70f2a df3a92793773a197e4f76e6cdf0f8e982fdef9d35b51[h fcd9c627123684e74cc014bd7053dfc61f6d245b41f140ea5c21b9bc47e1bb83244a938df770095ce5ee155943a53378e6bc00974e75034df42cdfd71a9958b40e55c2757d9aac67889706886432d39200e50c4edd b4c20634599626\=!u?45dc278cd5a503328cb022af9e7f319c9e522bd53e4e34359c31f9b8c51e2f35b6dffcb4bd01c786d1d8f407ba2e737eceff6dfcb9d5a7276c9bcbed19d7d2a32bbb64ce16894861afcb9fed531a5b14d7a3c60354f3df7079f2ebf57]k78c135b4ce2a2706cb6ba32a941f1e03a0aa2e09b45340f5c0518236c408550f3c43ab5e6aca33ad1495eeccfcd624eb51b1d8c16b6a9ecd14f5411a7a25ed1a53b5f2e31a7a83b9b483b774d4a79cd627548da05b8c6e8b33f3dbef1^ \ @12dc84e0267406908dd8fa24ca16cf21af0ccf6de8452868d24a011b167382f6bc673b200e1d440c96a4bc87f1dec9eea3b83da5eb66ca7d191af8ea25c3bb7e60df5b8241b91625ff117862aeec7a891b8a566878b67f8ff348b4007_? 5667f64f10d144721188cefd5a439d18ba1c9194a8c52762c1a3856888c068bc49e2fa70dcbd008dc8cb6736b5c3921716e58383424de843caea6b9ff67956500e264ffaa87bdbe2c8c451810b5407f89c3eedf8069fab0c7262bf79c`mB^?149eed7a13ced9031fbd7a9047966028de0673e3a0bafcff75ec37bb28bd97a4e914d8a1af4500f2833630ef9404dbe5aaec1d282384a36ed6fef0dd1e4edeeb7ff0e383cbfb3437ace0b662dad72d5dd960d9a2d72f56b104c0accfbaݟl? 65db964d5fe58ccffff28844524902281d29a971a43493d5bbbe4acf9583f1b05d9d502289514b4666f8b997071ca2aa24eacb851b5788855b5224c678f35254a38f6bf561592929ebe0c0f911cca2467ffbbba83ad6e1fdf24d231ffbbg0|? ac1917c5d9688d246fdf2b31fb0d45e315e5d9f7f385071803435439f15e619b684c25ce3b2e6acd95f461d8f965c7a2786a6656d0d81ee00623d52854d9c3f5bef5e994c19c77c05bb5db8247021cd26fcc9edb24 5184be02e37a2ac$T4/ ad383931106c37f617cb55af97fb3619e2155a4dfa89d0a7980ad14a5705fb9b128e890c75a0ea70c12506113d9c1e881ccc38455a21e99e0b8c2e84bc65ad87f3456633bc551117db926bddf40e4ff034c368dc3dd4f08db89986c49[!TREE$HEAPX!indexH  HTITLE'Indexes container for table /data/table (CLASSTINDEX (VERSION1.0+&pTREE)@388HEAPh;;P (VERSION1.1SNOD#"K"k$+MP 8TITLEIndex for index column (CLASSINDEX (VERSION2.1 0 FILTERS@ 8 superblocksize@ 8 blocksize@ 8 slicesize  8 chunksize  8 optlevel  8 reduction  (* 8shuffledeflate^ 0CLASS INDEXARRAYSNOD0[2(0+X :@5@ (VERSION1.1 0TITLE Sorted Values 0 EXTDIM  (  8shuffledeflate^ 0CLASS INDEXARRAY (VERSION1.1 8TITLENumber of chunk in table 0 EXTDIM  (@ 8shuffledeflate^ 0CLASS CACHEARRAY (VERSION1.1 0TITLE Range Values 0 EXTDIM  @ 8shuffledeflate ^ (CLASSEARRAY (VERSION1.1 0TITLE Median ranges 0 EXTDIM  (@ 8shuffledeflate^ 0CLASS CACHEARRAY (VERSION1.1 0TITLEBoundary Values 0 EXTDIM  @ 8shuffledeflate ^ (CLASSEARRAY (VERSION1.1 0TITLE Start bounds 0 EXTDIM  @ 8shuffledeflate ^ (CLASSEARRAY (VERSION1.1 0TITLE End bounds 0 EXTDIM  @ 8shuffledeflate ^ (CLASSEARRAY (VERSION1.1 0TITLE Median bounds 0 EXTDIM  {9 8shuffledeflate<^ 0CLASS LASTROWARRAYSNOD /c-(H#783<X (VERSION1.1 @TITLELast Row sorted values + bounds$H 8shuffledeflateD^ 0CLASS LASTROWARRAYsortedindicesrangesmrangesboundsaboundszboundsmboundssortedLRindicesLRHs<X 0TITLELast Row indices@ 8 nelements d 8 nelements dTREE{MTREE)'NK"k$ 0 DIRTY xUB@%EBI%T;[bNߛ# Qgl׷`(y~d*b\?oh 64KB. - Blue ball if the file size > 16KB. """ from time import localtime, strftime from os import listdir from os.path import ( getsize, getmtime, isfile, join, splitext, basename, dirname, ) from traits.api import ( HasPrivateTraits, Str, Float, List, Directory, File, Code, Instance, Property, cached_property, ) import traitsui.api from traitsui.api import View, Item, HSplit, VSplit, TabularEditor, Font from traitsui.tabular_adapter import TabularAdapter from pyface.image_resource import ImageResource from io import open # -- Constants ------------------------------------------------------------ # The images folder is in the same folder as this file: search_path = [dirname(__file__)] # -- FileInfo Class Definition -------------------------------------------- class FileInfo(HasPrivateTraits): file_name = File() name = Property() size = Property() time = Property() date = Property() @cached_property def _get_name(self): return basename(self.file_name) @cached_property def _get_size(self): return getsize(self.file_name) @cached_property def _get_time(self): return strftime('%I:%M:%S %p', localtime(getmtime(self.file_name))) @cached_property def _get_date(self): return strftime('%m/%d/%Y', localtime(getmtime(self.file_name))) # -- Tabular Adapter Definition ------------------------------------------- class FileInfoAdapter(TabularAdapter): columns = [ ('File Name', 'name'), ('Size', 'size'), ('', 'big'), ('Time', 'time'), ('Date', 'date'), ] even_bg_color = (201, 223, 241) # FIXME: Font fails with wx in OSX; see traitsui issue #13: font = Font('Courier 10') size_alignment = Str('right') time_alignment = Str('right') date_alignment = Str('right') big_text = Str() big_width = Float(18) big_image = Property() def _get_big_image(self): size = self.item.size if size > 65536: return ImageResource('red_ball') return (None, ImageResource('blue_ball'))[size > 16384] # -- Tabular Editor Definition -------------------------------------------- tabular_editor = TabularEditor( editable=False, selected='file_info', adapter=FileInfoAdapter(), operations=[], images=[ ImageResource('blue_ball', search_path=search_path), ImageResource('red_ball', search_path=search_path), ], ) # -- PythonBrowser Class Definition --------------------------------------- class PythonBrowser(HasPrivateTraits): # -- Trait Definitions ---------------------------------------------------- dir = Directory() files = List(FileInfo) file_info = Instance(FileInfo) code = Code() # -- Traits View Definitions ---------------------------------------------- view = View( HSplit( Item('dir', style='custom'), VSplit( Item('files', editor=tabular_editor), Item('code', style='readonly'), show_labels=False, ), show_labels=False, ), resizable=True, width=0.75, height=0.75, ) # -- Event Handlers ------------------------------------------------------- def _dir_changed(self, dir): if dir != '': self.files = [ FileInfo(file_name=join(dir, name)) for name in listdir(dir) if ((splitext(name)[1] == '.py') and isfile(join(dir, name))) ] else: self.files = [] def _file_info_changed(self, file_info): fh = None try: fh = open(file_info.file_name, 'rU', encoding='utf8') self.code = fh.read() except: pass if fh is not None: fh.close() # Create the demo: demo = PythonBrowser(dir=dirname(dirname(__file__))) # Run the demo (if invoked from the command line): if __name__ == '__main__': demo.configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/examples/demo/Applications/converter.py0000644000175100001730000000636000000000000025607 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Tiny application: convert length measurements from one unit system to another Select the input and output units using the drop down combo-boxes in the *Input:* and *Output:* sections respectively. Type the input quantity to convert into the left most text box. The output value corresponding to the current input value will automatically be updated in the *Output:* section. Use the *Undo* and *ReDo* buttons to undo and redo changes you have made to any of the input fields. Note that other than the 'output_amount' property implementation, the rest of the code is simply declarative. """ # FIXME: Help is broken in QT from traits.api import HasStrictTraits, Trait, CFloat, Property from traitsui.api import View, VGroup, HGroup, Item # Help text: ViewHelp = """ This program converts length measurements from one unit system to another. Select the input and output units using the drop down combo-boxes in the *Input:* and *Output:* sections respectively. Type the input quantity to convert into the left most text box. The output value corresponding to the current input value will automatically be updated in the *Output:* section. Use the *Undo* and *ReDo* buttons to undo and redo changes you have made to any of the input fields. """ # Units trait maps all units to centimeters: Units = Trait( 'inches', { 'inches': 2.54, 'feet': (12 * 2.54), 'yards': (36 * 2.54), 'miles': (5280 * 12 * 2.54), 'millimeters': 0.1, 'centimeters': 1.0, 'meters': 100.0, 'kilometers': 100000.0, }, ) # Converter Class: class Converter(HasStrictTraits): # Trait definitions: input_amount = CFloat(12.0, desc="the input quantity") input_units = Units('inches', desc="the input quantity's units") output_amount = Property( observe=['input_amount', 'input_units', 'output_units'], desc="the output quantity", ) output_units = Units('feet', desc="the output quantity's units") # User interface views: traits_view = View( VGroup( HGroup( Item('input_amount', springy=True), Item('input_units', show_label=False), label='Input', show_border=True, ), HGroup( Item('output_amount', style='readonly', springy=True), Item('output_units', show_label=False), label='Output', show_border=True, ), help=ViewHelp, ), title='Units Converter', buttons=['Undo', 'OK', 'Help'], ) # Property implementations def _get_output_amount(self): return (self.input_amount * self.input_units_) / self.output_units_ # Create the demo: popup = Converter() # Run the demo (if invoked from the command line): if __name__ == '__main__': popup.configure_traits() ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1684768028.079807 traitsui-8.0.0/traitsui/examples/demo/Applications/images/0000755000175100001730000000000000000000000024466 5ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/examples/demo/Applications/images/GG5.png0000644000175100001730000000240500000000000025557 0ustar00runnerdocker00000000000000PNG  IHDRfg7 pHYs  ~IDATxK"m.:ZB:1ATL%ftl*%T_@CJ((: :#˾ϳyv|O?\̑_|&"i~bDtBDDz&M6 M zݮ4D^=:MCD nooqzzJ#UU!SX,86~ dZ-ejl²,uijLӄaPUIƞnFvvV˲PVa&J4dbBa #N#qLړh4tnJ0pxx]ױ' G.C.HRəzNDhZIyxx@T?*"ɤ c! "`RJ_. cuu6θfpvv68+LJH$l愈PVa#_gHQA0'N'"1&*GϐiBp6̣D=X4gfØutQUz]. affffbF`$0M`&0L i4a0M`&0L i4a0M`&0L i4a0M`&0L i4a0M`&0L i4a0M`&0L i4a0M`&0L i4a0M`&0L i4a0M`&0L i4a0]"?PE`FrޝG"B:FVCX, m'' AuB/3*|>m'`& Àar#_ghgg^d҆tŒN4 @Ӹa&0}pppt: ]EQx<b1n{8aH$HRPULfi(^/~nkLlnnnH$xx^\.looFIvx"BӁ(|XZZfff!"VVVN3x|=)?LQq"wa@D }b@k=0Ɔn lIENDB`././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/examples/demo/Applications/images/TFB.png0000644000175100001730000000305500000000000025612 0ustar00runnerdocker00000000000000PNG  IHDRfg7 pHYs  ~IDATxOwq?g 7Yr]2R!eFh`8pHmSUl'-Pj.gQly?^cwwsΕ㬭UGbDXZzN]_@NG4WqҊ8[ >oձ|ɱ&Y{ GY)?Ps-͕S \:̀p@@l : sh; H:%7H_Vt_'& _ m32' ;(+\ 842mzC_jEljEn rsȐ?paߔJ|g0NzFJ a#Js'ia4ԟQVJ# )ݵF (ldGId]L X0y;2яȧGߥW5P=&_6ҷ 2.?ِ>:AJ ´Ab6Nu0>'w\ҹs )]/w{Td3~fM;̫0 Na0BS)Fh #4Ma0BS)Fh #4Ma0BS)Fh #4Ma0BS)Fh #4Ma0BS)Fh #4Ma0BS)Fh #4Ma0BS)Fh #4Ma0BS)Fh #4Ma0BS)Fh #4Ma0B{} 0 SB?/\9ìlCF\Id]|{ 3 F-e;LqVV` xfLI1?SLcćz9{"f&?7:r;Io]ƻO{'sٳEN3>o=f} _HbsёK~l#I)|uw\y8;L 0>IdP+#pf6Rv$ )/ߤ6Q<l; eX[[وt2D[ d;_:۩Y&x7ӹ& 67ot7/^ n##2Ӌ2=Ý$X =t]߉]׿: h 6b{A{1j9w7_~/ C8H?@1Agf5;HI\[@~?_TPd f`ar7@A?CnX|b`ec` ,@+ h{@q77v? gк`+i@ wV MΉ1@T7 'Ö@@Ԁ>mF gd&o@`ydDy /+< P@0ـbOف+@`Xajbvـg9L@ @Y8()<@ iQ^ @ d?` 33$ʀbsgo Adobe Fireworks CS3 2007-10-06T22:15:44Z 2007-10-06T22:30:22Z image/png mprVWxX]hWٽww'; aUA߬D(UĢ>tBJyԧ%/jEhXK_%%jsfvl|np~33ιߜ3?{"QV]T9syÅ._eXr,XYU &Cu|_u|_ %"1 ħ|wrN<8`fwqll&# {0>?%K&+6ōυl9>((&l85C]Fp|Iw x+-5QF㷷JAuJrpy>֨+3fo8\FLpC\OIvs3)IXBB3ؒ6/lF] cA8$)B_iؕ:k Z>%o'*g;#sYoJ)>Gi}nsh$Qc2"WuJAr 䞟qZ\"ꀈ[ss߲XR] qb.UQ|S&P<__ DLIq*k¬u5y7ճB ?- ʄWz\4!߭ցsZBߦAu|_#Fqs/^3&_pȰ :}g3ٙGo9tHaðwal^Hf@""3"2232+AvNM32#}"2r/Ze}Ekgti5ҵ۵9> [K۩79nmʸ~4BW Co>\m?k+{ƒ5zi cmdvwЕ1bxJ[.P^A-vnjx ބ6\kF~Pc~5Zp%'GI%;9lVV貆6&ЁC&Z dg2v]3kB̬iavb8A0kg `3Tb ؘ1jNx:FxjvKTKwnc!BR׌H#h!K`"€š@S84%I䫝a\%WCtqd(S@!ŒB^sDUbi |eNZ6h?qFW)@ l`~ 'gO9n4,ȺF~[̤:@6D (ơd/rG%Jk] iQqs=%N:h:NICFѥF=`&{džʹ/,li UɱbäNZevIHll% Y7>۠hnW j$ &bx@K_cK#J ptHITJ+8q,nVDVy::K))3tJ'hYmZhp+n:ppЏ$@;Y<iI+=fTz-7E@Î4Btλ*gpʶ֣EGqj-hEJ;.17`4([ Q<3NS |eԆr鬟w ɳ/Pki>zOL(Gٸa#O`uƝl6jam:ӄCj[mvr&EV!HbV>,+}ynI>6xb?L\aEİŠRM$}eBqMVv;sGT=`oXuěɥ6/%>KafQHaBɤ/M`Jh#´WS:{ib^!;8mg[ 8˻IhXDJeNQHD RI)|o>୫YW2HA@7INx"zhG-\j'řJGeY\*OF'{:G;,. {]éM(Q fx: HeΎ̗ 9#hO! TNc~ybR-Ml.ү~JM,:#u2st73vM/$ASn8!}qxĔ_o=)J6=Ƿ*d~H V, J5/gOZG~EI]nfx,݇~tKf,R NHa#J]~ΟIz,Ed/H㛜ԗɠ/qv5q~cj6]~:٠a%v#:XHTI'0|e6_tM'84 (%g;<*OB=אU+A(5V- >nrn#v8y +ԫ*V8 I{E2Dž01nl_6'p6$F#JC.lpyyAiQljlh:iȵMrLJqő~MjQlMIߢQ{z}:9S8UG2_!eU4|]#ķMKlbc.W|͜kt]r/2Z(W!|s {o-LMJ{瑪#28!Mћ&:0zkbeK8ˢg^;axI.HDղ ai[u`w`G糱# neV,GqJU-[ @q>j[[i.}.7>8yGUQbl^ ^ UL=XSa'gG+OK ~GWyp%ggYcX9yٸ5 2n*~ӏkbeܐpn(4/Fk+eb#_p=[#-uCn +T"7`*& (iZ*ٵ5TN h3\6 9m"RiU2/x(m4)jMݩ#{kUNV '^fOYW[rcQ(/b lw䋕qʘZ@6RXs`q>oSQ>eUgCTdd͎W\bt(82-6 Zu 9h$]E:։]/;cw (Jg`9Ud3M&["WT +10|k5VKn^;W^+9W՚zխX8t'93jg#aY9&̋Pvl:eXGnQLڦ ([zF 5p!ΠgZQB&l-хn-ѵ@;<ÇPQ$:BTcn#7!79d$Iq8!W.:SiUGw~~5dg=Nټc7 snCxh"׍J׾@ޢx{Kw622=a\UZ1,1 XF{ Mt5tdk,Ժ3.pDkJJ>GaΌg7prb{'ݳa3lʢ#G]xSx }pm؋Yr >ftsa8}!c巐+H=_B9t;(󌾎c+˒"O2z o Trz߃g_,2ւ~%t}c(a :nCEP[ O;Km )AK{X" ˯'-ZӐEXA"%C0$-y)K^% \@Nr!.stlP]"gqK:` :y+sa a|:WXBOBW ]XBmD>^XBOB˶Ѕ.,B;liyXh6Ӷva ]l }/ADu8]XBOB;.,ta} 7rUXʻ:_wHgXyWXEʻ:Y:y+ XyWXBoʻB/.V:XyWŰʻf6[f+ 6XyWF|9+ XyWXBzX`'BZouE>WZK,i~S;i5_lXW |($l#7cTd%uEԫqVPZjYш}=*mߑw.~#ILt,*n$*cޑh*FE$moYh ,t 0DcBE _ n`O~~_b Erȍn O/[Q<mt}\TYk}crS>qٓe <قp|,>C#QƟ&KQר#N_]Lpn;.]CQ L;߂B gz:}+oɯކ_FGoR0' sl ,,A٭NWٺL/tgaj%6}a8d}vGk̯A i! _#*&4Gnc頙 Q;c_{OsW2|_cAw">x{., 8F&H,dA"`Ɛ̺fFl]t}(By s倪by Zr'?AHmyFuV WVi.W^%tcp-"#q}tMQE=O_jrFT= I5DB[z䌎A%ncB>UU_{J,k) !a-uL[[h˷Y>V҅.YMx=4ǹ`p=x p_3(3&Zs#L?v8~^N%^pT dq݉2>BH\߉1/ ,㉒FK\;VA)tq`=X{W\h%:aZK‡`\KƢ._7zE$|`<@w.WϽ: g{/T*KŷQE*sK~/KŚ~^`}[xA ,)$oE<5l]}g8%N-qٯbfm-B>P$.{7$ѣ:ʚ?|Dejx_"z&z>x{|7痍=ܾmhtsz\wɹ!ܣxMm镕56"3@% Ϛ?]z&?Cq:n{gUv =hosGNmZ{߶Ɲ=h_ml# +8’~k9t'>쯯>"`wwKgtYaH9<o d7vqoek>qCŝ]t:6[O:} Be0  Ѱ :$d  Stj6Ἵrģp|ct{ö iC5 mc$>7q][_Bq>w#i u.+A D76yؕ3BgYm5h{]Thvxr`^k`>5肍;nn_Wj_*bV j{To6e^Vmtr*MH먀YfݩWDW \]d2gUʦ[s[7ѡ^V!T{k1^n:f-m[gyN@ kesՇ@!VTz٭T;DĶ]bS"rZA%SE*nRe-=Zd"HVkW*NT*eqܪK8g =J 5FiNr "HvݬP"VIWqTy*x:%bxx"F9DlGuHj!"W:D:<ԃj6e:ut*VVZj4'Ab=ڡJRuQ TٸzF1i@VfY:TE*lX.) M*2N٬Yש9"sQK0"dsRqD22˩A@ڵ2QJ>AO>!{eS*6!')'EX!5!e5~6au5>{`K ^JV/^{KgU#() ed>/tc hifu@ c|=h۾*S>ob-M `k&a ֞h9A4>Cxy sK<(КpN͑F_ڰ7)^ kGӉD@6}N&dftJ܃+H)XCm>ZwP+AߧꙏPУ3Dl$<}a«9ʍ]̓ENІSVK1c]m.Z@EtXי5yHm=FF~P#yQ> i!~t; 8ЀPl?8޾4B Sk)e>*٣ޝskOkK߇sZ{ %;Zq/M~ ϡm=Ft(Mϙ_ bG։v ӕA{ȎQE?խ!fEHkkO8ge_ѳ"ϔ|z|Y=VňϨ+/dVTO)kczDs_:.}GH\_Bn#x#DfgO^o!'[tL:$Cc#h?~"xg=*P)Ex!k~kB-#Ve5#Tac3U۵Stx׉xw MX&)H)Gebݡ- =i)@a~091LæVlCf#FsGQoiY3U_(kFb '&pm*Ϣ3@Rp? ?Fyxy6¨bM9 Klr%,x[.J\<.3HO7uM J6=: ea%̓,IgLՀ Ѓw}XًRcȁT.,]lv~t%"B#+t"Aʄ?RQ-dDɎE.QlzLL)G@IqN L(9ɝJ(Owa7@xTw>7%Gj>98<69 .ȯ(d'٭WT>pFgkz{|0f;iW@LntM(@z>\Gpu'#32#kVt&Y tڨLIFwնjOD;dUApV 3Z>Q$л$.ğAM~+JzO=cmrmA.rI$Y,zq%N]I3>ĵkқZٹ|A*g\7mdNMŏ̧/xQ>aB1J^]7yb mEl>Ki֣4;+_|8%ΫuE<|ZEΒq1>-Ld6#'Y,PDЫŘM/,O("t|0>a6yEPW',j֦'3Յy Y":41U:?[F(SL2 s)^&Tv55ٹ99zNS^5 t=c`}hwg lV"q_ꈶmCe'U[iXmBgA;Uk:/'4iI͔5|ąF-)WhR2IWU>ڑ5,]YcXSc5FՈFYm  էn;}//TSJGEÊa)hXJ)4,a4,䒊#pbh'B4?BLl܃Z>5ȾGQ44ݏ R62Oۄ]H|;{-Y[yvs!9:s"1< #guLꠖK='VmшGSIO2jegH;ȮdG.aVⲂr v\z=#9gF'\:sޝAnG̰"1V#ݒq׷fbD䜏NnQܪr{˪3i2^I\9WG{G78Km>z#z%DGǢD"cؓDq7|YCY(1q,w}:kX}ͯYbtk4fQ*~n:oO2I5,RYxOFC&:i[$'[ Uy:9<*0YCO,1)p#KG6.(xQFDcy5sӒ(?|dyH՞R|G󸊑rl]eތx5Oi_,/t53W?!ϓ-~Vȗ\̻SSy"l%v| yK"%i20z4%I.%ټ;1/iz5%W?RUٴwHVx^:=y"_RDGL;S3+9_,s53%ETW'QUg9Ga)9y?H9,rNx] ׮vU|Cqw/I.g{NDx߇3!O0͛z{rGkw[6 2<ɒe&B[M=I:Wfc)xqy]p`rϡd+m&|{uJLS'ʷښ5~}4-PI ӏD9dU[Wp,SW'LY3{uywl:h`:6vz+_t>-}̚Qь;2қiu2ɪf7v=,^Lr,:Oק; E'Ol&zzuz2«܍&y7Mw4}Hu-G([imii4!.Dๆ>%FdrЗI",'g #2-wN5 $t [  Na}|tvJG+bA,}fHjzRP-ĭQ++WWx\^7r-kBx|$ywۤUol$5$`֖?G;3vc=zzkGL^'Wί^ѣ|6:el?c%θza/qvUtc P*_2gttA~갤:)aKu8S`tzJ]#w ${^x*lK@D\g%FIjx'!E(>rG<@7.AcS|.NI4LGsw3>#LJݥ|*$K_H#vѻ>2OCWfO5Chv2j^FvYS21T:&EMI!YTZf-S2+%IJY!~Ogb-SiYʢee+tɨKMjðn <`:e(ݴ瀧LgMQƗ$ӥ(tb=3VYh3teߨә՜=c=]d! QX+t> ~bN>jՙއoJ‘h^7.Z28Sх$G 6Ex΃Qg4J@ɽ%n67/j? uK16{p W#܃ \9Ρ`{P}:_ RQ"j"fQVQ;==r_ytN`M xS(t3Vz=}aM¯A~DʝAvIk±zFu#&Yrl<؝d~qJ@ЕT(Ep+uGuu6kIB-Ķm9Ui'/x\(bA;T@Q62e! *!:/俦Ge0*ZadU˜՞I%٥ziuiaՐ\qD}#+hoG4dgH4y$\Afg<6b[ xO|HGkxoqXQX[x4F,x%[jIuLGQ#8)*Bŋר1fVud{_GrKP$˨8Y8<VKu8ͅÓ?ZQ鏓M$mciKgQ1 ,eϜŏ\Řivӻ7:'Thos5.>?fbC?="[14=^KI{MθgH~[Ҽ[(-, $T=fdB}ws996dd쯁e0,̯ҳ}DY?UPٻж9'5\\c^cps ULG;yrPBQR1d6@]2}vE.K~* ]~Y ,e_{) 3if~l936ixsF7[k+2;M,zkArΧ9&iUt^;{;9ʦ麦71᥉6,,\ĩEr.vձZj*gkZsr{ MzYƯxo*FH>2L\̓."VKjeڶU{$d!K~$Hf FH2*/a,s,a]㴺:e'4P[0E62U[ԃ僇X<XƤ/!GЯK w kømGZ.]۩]>Λ:}ska`[e2nap3jY[C)z Fr]9FWnMOlue(;xu} ru5}tgހB\koauN~]J`[{P6Zp%'GI%;9lVV貆6&ЁC&Z ǐsYa9,1&̚f(f llظA 6^OƌW#8vjt,!t<5OS_Zvj^_‡/ɃfGMIД$vƆ!l7Wɕ69C!]}DN> $Z0&kKk櫷N/vmF)F[ {JzƜ6P~rGvH2K[igκL: dC bJZ +w>PY+;N)q)=Fsq<7msJz|KE=G`:&up{zqe]|g3 cCѳ@chLI1hnShV`T*G@ Qޡy4RK`lAԟ02 B6A%! 0[ WЉ4q$ksQ`QqrG>Q;alt0 YԶE:L:NY{%OהqbCN0Zpã$| YRckR<&62o'NBbI "4]$sp\ewhCBn=4†GGW(Oh)V\$}|7Qi}ؤ&WE^$4 -bP`OW-;+dy@\l< eja Ղ"gրpkPp+`L1M?:=sC¹а۾ n-m_lgkE3nq6m#r_{xY&=_%/ NPTMkk 44 ޜw^ ]+,ta JaoqN,tluٷOѫbX4)ܝΚ/K6i +lUczB1y* Vغ"U8YHsZ-hľ6;Tp^kؑ$&:W71HXe|#QK"Ϸ{,4jB˱fa ]X"W~7F?F/p"g9m- 6>.*Ϭ@1}¸ɲB lA8>PIK{!@(OHk'{/.i&E87ehdXxH&o_G}]Eu 3=Auw¾}~MoCK@m#7PE)zkfFWX RrS9vmSC  I'HHlX&OS30܈x>0^BJ2DP;SqOWf{ p邴_WGre rlu`1tL(A]ŝOs/'Q׹+GkJVů1 }WɃE PyZ=#?$i t_BcHfN3k#.>I j!<r@_1<-9jOn<:+MR+A4kz+_18Befّw>"/?5{9QwW"S-p=rFGH VN1hx}*Ј=%5:B_Bfۭ-[hw,tT +]XJVz&Kt<gGpu/b Թ;FwBY?/'ɉze~#5@ |isNT |'`At};^ïQ> qpȷ d;֡.就t0 ++n9%@愱\2&`Ɩ鑧;VKۈ>~|qrJe,!I1'0xD39`.ԄQ|G9B@s)} /4z9߃!xs%vu,8݁5.ȼ ̻07bxZ"/̉! ߛ =/8߆vٸD!$zDStDId%CmU 錔 8I+.q4XP0-I%Cw0%A^cQE"I>0^hqaܫ^ɽ*ytۨGe΢?x%%buZk/Uw64NSG9hY>ÆlV؀>~ 6[ `o0a cYPRwZN]{X;5\&1^iFF|乄:W: }S~[<;ͳ`=2dMyl}߼8>j1<BUYAI}-XO`#zdqsYܳKRgx xRp% @rLXۅ<[dۣQ.Il/x,<;P wwGygeA/Q Ou,fUb{4ΪskJ3&|{:}r =3HO.~_5v]zs <Jr1ub:p9.d4!MnCQ3O/!c0le~|rVZ ^0 tgp2;:?&pX|[fM%&}vc?Q)q[qlZ,G)qyS&p5*\|%=*{ ; #!+'dxM>V3nj+vxD6(ߡc8na [|ldz'ArH7ȯq[w $뷹3BgZ渱:‡;K]:ZP"6a{痬ɑqn6T| ㆢ;膝um| IWBF{k lnm.t2U!r{.22aAt:l36La*v}{ip5Z:F. zzCx|Xƚɭ}/ol -8JAW[M=p =^.ב=<_k ̧f]v kj_ k\E jXmo&̖E]Z^ŶI{CI{0^լ;ꗈꗠ+_"AsJtkW#t+&:u*jt-F+MǬ%Pm\, ȶtlzg(a˵Na hH@e:U(uS6JCQ uut8Jq({<鞈 @ŭ* kXLb zli7[mԃX˖p#ĪXހU/c'ӒضVlJZW+d*HŭW%]L*5aMVŲ҉T\J8[u)\L'RԨ#ͩXA=ۮJ*#*N2*OV^D̲SwOO^6"(؞hp.U-BV*VUH'zP&cP\Lџ@*W*bbUkU" VG;T)W#*} 1WϨ8&? =TrݪLQ1UWP"[ K%%IE)5 :U5G$b"jFd\ 2u^*n#2HRFc94TD*2:3ޚZTE25TOLM&SG1әD.#}rMV!:VIg8H*[cTVd*t)N_cQkU/Ct2tz%Et2uF DZpf8pU}8lU;b#G|H=7N%~$wȀ՜ZNd#Y Iqu'QLHjSTdbuh4gp/J%ٿ8( Q*lt)NN~d&$ /ZWL~ Id&$xeFF=Y$;!NٞF{ѩ6&{+Ǔ“Ј6چOx@ed8RWh4VdORsO4R-^C[#BZ\?ALB=Ѳshh|䷇x76YRxg wI5dU#%aP$sO'Q v9 nJdI)s#` j1aB ~g>BAHi:8 k^yWFPn8bd/Rw6B'P@_:$uY nu.ZD"w>xtάC}l31|4^Ӆ ,nH h< ~KJ3 2)؞Y=l=boH N K );`D}2E$D)_Q4bWz[`Y>ب#;TdQ l}(9g{/a ZD7Ҥ8xcDRu{ ({ ihls|{ME?-խ!fEHkkO8g5_ѳ"#ϔ|z|Y=VňϨ+/dVTO)kczDs_:.}GH\_Bɦ#x9D0)Ξ.B" =N訙uJIM-%(RG&~6XE%'zUt&RCתZF$j\G \;0mÖ gj!kn=7ayG»i’7OAOAF/]=%*I>gua%ֿW63ޟuT̿m>1Kdon`i0Y!=tdk{?oM e&g)6C?+L_~?]o? ԓ_bKo?=BG _K QӏpƱbd-޲D$pakIعxD1=%!Wr}D 1*&^(/Q4 ^+#By]ٰ pSa":b\ۢ%Oi6FUh }WI-.u'e}:ku69l3 fߕ=":mѻ#m;|ZٸuV68w~~[&p_ހg ߦ#yj/`Mw.a4D z~ zcY&VڂcHBtnS4>1lZ+n6d1k;wu5^M!{=nf,*11 /Y g_k$i[Ix~߈ꁗg';**֔Zɪ&W‚Q=ĥMk;sAqQzhӣCp_QI<ɒt\ p =8G({!;H/f@]r\-"dM!K@J@1B'RyL(Y1(#%{ (JvJX"˦G$Or@ɄI) (tyJu s#^R* !~#DxȱJ gae'mcQIj~&qDERxus&=/,Gx:ZUt}^gdQXIkc?ɕ5UT;SWsSV' I7)unUs:]ݬ(ɺSI3k+]}q;(r\+X1^ы;6}RbP6?u䣨諘csG0~ z{]ym~E㐎ht(ќiη cC}} $FgєAfx'SuW(}22{)S;&/jJgiIGd-?mACVau;#ke Z¢"Bg* /kw^M Xazu|¢fmq>S]8K<ᛐ)iKs\ZceS'ADρI<˃qS OF=Ӿe+ʖe[IɁ|󤫣5iҹt)gS9d.N??wEjbOeYQ{M3zg>UyO3 > Fm/_ +qz k-eίhk_v;:T\~}:\eՖk|,)x4SKrBIkLY#G\hhђr}&%ә}^僪YcX5z5u.:5V#kTn@fO>P}NӧH*I-_>tT4H"[AOiJ.h8 '~"(D#=S{E@{/UOmc.󄿯Mۅ~=|ך܁Rga7k;Q3(r࿋>bqVj/snx=$QFqJx$ h(.+(.hNɥ?s:yftu:v .ΡPxi5]?-w}k&A4Jḓ*Ga{:F*e+Duxsud91yt{эӽ棗:'QZnYOtt,JK+b?=IIw~1ȧ5D"WNɲW٧;OeyYꟕ/L&@hV] a$D[-$i4DMlmocyٟ%Oi{PGyXsɳy uiZO&LBk k_"qlD!})t rr8+rTú`)ANApo0xz __MGgml}4G(27чhƏ]x{ ܒ_@zkr{{++Ԋp{oJwMJYƶy_]ORJfmO|ө׊מjvdTu"}[=JgSS-Z9&3V+aOM_og[K:[U>k+8}FGD~aMKÞBT3:VKGܥ:rKNJ%hgȶ$=^ OuV?awRԌsK }{chI9=5DtMP:w03¤]ɧ}Ai+MX4bg+t9tevxZT:-`'#U8o$j%?>/CchQԴLZ8eBk=-RiYkgt&2;%Y,ZfxڱZfKBk}]@nj$T6 립к/]\M{xz9{tv-t|<e|p;N2]H'3:)=jEKX־ =oy\<uFT[f| pOAs/`~P'kC[K_p?R=q}N[ |Ε %:ߧs% %BIɯ&bEaX:Qc!Gm$ NJP:L7aYݴ/:+}AĬow}/6,gdY7Bh!SH7AK ]Me] )h[Pw`^Pg!Ɵ$hNliܖCXvzRX ̅L ),dHee)cq ]@*{Ͱ-Jkz,PVQ 㫢FVE+ix^)T]*Z^Y cF+̝!s|kěN9Bm4#Aw0n"+CK7rk~..qD#JvDGB<8d l'qSh#՟''tJ޺^ek>wNcĢP柄Pg:>Yt5>y-{.QcoU{_Gu$.ď~Ar1PӑlqXW9|q\8B+lWh:!-\GijeOXf 9]WA\\Lf[xilϣTs1H QlHاEt(SAZ_Y X mkBTWx흍8 FSHI!)$FRHnw HYx3ꇤsaaaaxIǏ'U{o_ھgW9 o'GW {>~Jlo߾)*/N\ϱov[iZ_ձaJΝ/:6O- 92b?Tlk%?_21B sY5>:>c=1Ow y^- ڶ,XzusM#גU]>H_yYv!ۉ_mi Rus]Xm_g)YY)m]y,m z1aaaxEߓGקo/Y\k6xjgH|yu.\aæM&wk#ϐ$?]Mo\Ⱦ,/ڥQ@~6s?)}, l gX #vQg Bٙ^uのuhm?}{].~}v_J;xogJY]޳@.)oqC?}>@Xߘ'-(W? źvƔOʙRv[K?[A}?-wmՑ}g\=c}M ggg DŽ-B^k_g?F? v0||؎=ǧHPgs/hؑI t~{n^}ZyD5XWvO)"c0vY Z|~_%/,p\ɹyΰZ/;/xs_9?Pܯ5ݻ\[y|č8gʱL{? 0 0 _k3>z_\S |<)b|7aaaxn.ta?l^Cvkؽ#~e)3<3^kdlc&jK+o"e<.ʞ`^(3zu l+6v<ï k7]/lc[`On}򚄫 G뎱zt^v2)?;Wmr5ocIz?Ozx{&!ez."ѯ 1Gg{+ҏlw<=}GݽFƨ^)zIpG K֜{{e G12ۭqiumf>.}~a? 0 0 [u+7Svq֭y΅ ?ނ}XwŶv?ߩDZۓ-q/?߳=<~#>Fk"qzrQo 9r,nY[;o:)@-`ק-7({߯S@µK9֠ɸ>:n3 _[_*mtcmC>qSL=<6;ǫsaaa{xˌ\ފpx?0׋#5zяc]x^l򼠕(f:~٣^lin59W~\;?vn6erUbS~v^U O7O(|;+SG4|?f*?rW~2oNٟS9~daևmH6mX[J~s.ym4ٶO|Bd/b5ɿyU? 0 0 0 0 0 0 0.P~*1@G\⟿KrKXs2(ߥ纎J8'>X@▼QQbqwx b)_K|v 1M6kee-2Ǜ59?K^E~9ϱQﱮYF8N?~;:=J<-tĒyNAgC \NXKs)'^Kg\~2}6}Գ)n]Or^j~"{p29w6/.z-v:+M{WJYZ굢`% Ҥl9ힶկ#OUz+U?;sd~vND7*.Y+v:ye;8}~|+ÑޅN9}{Bƞ#txխsXɿkSV/uJ=o G<ջL'L:D]6jfgLz/+ؽ[{rCMYq~[{yy czA;w9zszWHVax3 %mkBTx흍) q ĉ8D^>׻gI@XjjgiЃ`0 `0 ?ϟ|:seQ3|ӧO|:2|.};7eGFO6_Qv]T]^ˮg{>pjzkuo{yye?{-x/ D:3D&򈼹e^Hyi#/OGzϪ߯_~ :sMe#M3Y#=2 QЙ[\s=E8}E>GȩT ڲTg-}VfoSVwzV}./>~!?U1<#}=F[ ~QڋBN..+푹^edLo+[\-k dW(}6q$#?z6Bөi?L7!3O_Q}Пuo[=tkȋM!'}/Ƈdr2_Cﲨ: `0 :8o=+8-4}۞cĥXdq{bUq©ήm!ƶg*ΪU\z[GA=^+ru{LV U?)V>ғ)x|Yҁgi\yi^cUo*= !TY?rfgWsʽVn*VX#=Fϫ+[F~yH\L~[O҇h5ݵTow|Sfӟ+);F;:x )/OS yUo2e)Ve3'wgGg=J^`0  ľu kU,Ksؑ5nY,bXw{ w&3QהNQev ]ƷgcH˞i{A3I8hwduwUIWq8I>+@pQşGcZ\ƪUߝ]/:3d;ɫ:gB9R|GW~w2;fzt|+i5nΟgZY|<1NyŬ|E7k?z/k><=Α}N΅>uWydʬdz `0 *\?W8GY:Dgcg< 2+'W6qn؟{ru"wU쏘~c#T?+y{Q,,^qF/Xv8.֩g3}ȸOP ~n%hUG4(_sn|W}Tg&x^c,Fѭ+ <#+}/Uw8BRh_|33!mr\7U9m({ѝpvew[xG]߱?g;,nҽow8]וb?OV=Z_#ve?vN_WrYLo;1g9pV^G~>[_vNOS3 `0Q[ veO\k^8֔v<Zbz\Opbn$~}oz3ј mK vU]^iNWA#x딫jt q :E= z%օq)CcYEqyRG-+u (K\hP'*^ء^q=m=y|Kvūe\rȊ4={W1;=ݷxp;o@>ȘT\Ԏ+C=*ɫ|GJOCW]x1.ﵠ9_Eб Vq)v(ʑ}[GwǺ{-oSdו_˞׃2;iT&w*w:g׭SOsj%Z[~_˯d֮+w]7 `0]kIu+eL]ւoA^;=GR?v쯱;<y o$N1紈=:ߥPVu< <&3KyC/4r)i=*/|Ύ^]QNН1qGw>ù{ ?Kv:A}E:_n+{u=rq͓̳]>>d}+|L01`0 leg:׺񶊝`W,3O?]\9P~[kOWiGc~)-<w.3q}'vuw$Vnv(r52S;Wk_Kϔ8B/hEՠ'9w?K;x:x<|@cϽVyc@ۖSw8Bq]=2lBe6V}eR( VeZT4ade2ޒ+nYBTqSߔ<[&=f[|szP)G}{Zׅ3n7jpWwftEw[ǽ;`l? `0 `0 `{~i`oLy>uoi\qK|}7Svu9G쯿c¾#>,jow{ՆݲL=mW2u_8دjo?kD߱mw>#}E:OۡO;y`$jwymkBT6x횉m0]HI!)$FR?6c>>~sm+vuՑνYu8uN?WP>1JsWiV_uKEϸ/rˆ_gKW]ױEYcl,[TYHT}xL#}A GV7^}>iҞ-i;}LJX&TP3T#ߨgJl e'=?͘ona|7>?ǐU%;/mN/IfQփz{G}?v✽3X~j{zTAO^ʰ>?sy|G)PU{ ..T}6ڳ-F`p]k߅~b  О$wݓٱ|sCoA+q3lOx@(0a+? T,_7s\Ϙ^Bl1)C+k(FyN"8dPC_9>O0&l4Im+nwGrŰ)/tihf ѸX>E)<,6s45zb?J\<OM%O#(76:= ӋYAƒH Ls6MXBcX&ǘJte. 3.je(??Lj=%wZizFTx$kP8Em jAOހ>~؆B9 ֤8UKCvjbL Cy ;mj P. DkwUE€3ܨ8xUJs\ɟ+;}sFQ(KIXݛƨ 1 +KdX];Jģcx$D׷X`i @l̏rnm$^9΄zBGϞQ=nfkDe; <a>,⢞jk0B[p($Ǡp4 nq`XƓ vϵ.xHnorJ5Hu뇗 f a[Z:>36[g RL؍?( &w.7C#~B{] UW 71jk~ecGrD.=K@WDZM0倐0\xvqNZ ># BE )&yA}t?B Ym(WIpɱ |2+\2 )l8tl@Z.Be񅋍RSƃm>dIl'N adĢG3%#)?$s _5=YBR#-k"qGP-e"f%֩-ϓ378M9ϊ,_*n;HEBƱcl~ ˝[/sagIE2,z1t:kLș壋G){7ond{@rP>kwk׽ #kXfyEAB9uM4P=_lgW؇N#_nGpp ,ZUu6ȓVӰ0EK7*|]{75F\ԶzQz! uH>upT٣o3P)[^6` -d&*=%fY<^ط`_6|h3ء>2 Pq7ώ ,NsjF=B` 큳CiU)R鐏@LҮǧmb<2FHRqùFXi䎲OmGA}:*u f:@ʫRH.66jcGOpO- 6HKJU:Jǃv,3DZEƮqq7p?ȌK%ȧ$;?Qr6pP7`a^=R_)m>D3#£ _' Iɭu͋C-Rne㯄ssL<ȭ/R)|Lt_1Lk=rr 4/gEr~PnB[\g[{gYvRW' {Fem1{ wL;7&$xc0 n&u@5sCCձm8Heft x{q(aтa?Q%l4ςxmWI׆GC1kQ3iJh,KRO`ʲ4)%b6B8\pe;u)ko)#WSncRx{[sXv195_0Kՙ7>Tp5ٴl3S"؝LX睫[5m Q="u}pϘ*xbՉ#iM+@Z! Ϯ~jYݬ$?5mtu] %@݅:4h8ۃtu3; ΑO1A/r R*5i&j#Y2:$Z(ad@>'z L뇶6Z8|`6"X1_z' F-я?X^ A:?1;h/KVB' vOnFS ƤQ{=kh7MwXQp\v͓O/. N3HKRlK"q^Wh1wt h@3e6N|I;y?8t[[! $,ήLe"z%IކAkRl!3u8ځy?_W)AbCO!rza5Sn֗#<43y6"R߃CQ&>[# BHǽ{vekOTlq(UH͵h ݔ8,@tՂL{p/*L"d_y k,4 G̖bD>,.ok"D;|7[.DCA#ilϟI֬Dq]+eE _-- ڰc^Lq1~CCC9gNH8BkhJ#Z-`VoMa 9r$պZ-hkh ?C$ ^tď9d(8P݅]ڶw[wl;dn׆oKd Hބ(DInI M_(5)6H/Y1 QRk,nXHʉ?>df&6^EJmt{CCc`0ʅv5x<\9Yc}106"״!֏9dl:' 1H"z'7QqɌ#KR./CVgQȬ\ `?d1yuM6Ƶ8ZX]8^pwQE &1frRKi$GݜЕh3'{;;~FK37ku<pdʎ+C RMzƏ7)nҀ lEGyl:̑IoBS%|ЕsTulebA}Aʹ10A{KʘӺtjdLI=r PRg_LbR Şl?␔)![Fo wi&k^CV(t@pW2{hxHGRn͉eCbxԉ6GQd27\ثdS=\Ff*0ۣOP5(rZߙxQZ>~GAeN-jY7Ҿn;n?ӹ"Px}/NW:݊&׾:x" ꭥу;R펔 c䛅љElmG§a= h¨BG_uYnZ쫭FYs U"zM&:Gnu.DX5Xn;}ԫ%XO?~2&Frjj8 yA*W I9/ub)Zl: s 85J>~iI3Yԕ;:#hELם[ROd^GA˩f~Y!En0~/A Km>^WYq"<цF*c:xw|͞w%ehRgd9̕v3v Dgh>>?3hYDkgC(ʹƒԕSԜ| 2Q94(?OGQ34 fccPopTYaW(>@tX4`LGٞpɄaŰl\[9c26U M6f,'C4i?W~psϠ?kAKrŵk@I|>^xs?\`,D̒5W^w DMXf_8<%|8_왉pP1Wlm߃f?4:́_Ԕv M;k:p_sj؎qw]$F}y ,b'N=o0, ~M YR46+!}@~ujctCP.Y(x׎z?70WXFܣo3z0c8RGg0 TU򄽻w"/4֏CQ`[{Ocn]+{{ N!33+5]qpj' r9FDȬ)~: 9Gmx2-?sraG"yvUpa;Ră A\& ?#n 0eed~oq嶭!!DzP^H)>oȑ.ļԶ=Hy7S-M ?8ycߧq|#5"2Б lm#UeΤVbM͘jAc7Z ]> 4gb s 2WRsKg6 's8qzTT[R[w)I95xWj #!nN+zPڔ KgTE,?{^RDݥ=Ru^zîc&D'i74SJߔ&HUG[crͦ<׿~4}څh;lpAZ%XZ;tQ?yk1+Ƴu6[ Dc4Ɯ*dB#!}e>samhG3c^8u9󼵕⸈߂UyB;f "Yi=D =4&|C3g]~WgjhSIXU"1A5Fr4{AljwTt6</N \Rta| i>T.Wo>>xϯY{緷m,J{gg}v~)]s!?wXGFl!7U|Cnfﳅ:.@mq%臔Ru?.:aBֺE#Gg'yXDuSWNJD)21ѵVagWPqȒ s?¶@g")s\T{f3go^w:^"{d#!φt},nyWFKv„X4|VB~,˘_&fjp/WԍwaO H 3I`u1ͤ+_W|+_W|+_W|+_W|+_W|+_W|+_W|+_W|+_W|+_W|+_W|+_W|+_W|+_W|+_W|+_Wݚw)2iTXtXML:com.adobe.xmp Adobe Fireworks CS3 2007-10-06T22:15:44Z 2007-10-07T00:39:07Z image/png 9-)IDATx_\=suQ%M_bBA>_$}KAӇBA lZ(>t-Iy1Eۂ"L yPN i05ӇΚh>/33w_=1 l,! KD%d-;`ͥ;:03GaIJ8VONN(Wrf//'Տ2?:-[9Nc m !8g`2Kdi}7T^_{ݶuSi(Q].r9`1ˢc` $Aŵ#N>zg+M nŇs* 8 DQQG>W!xƥ21JPJhXs羪֮41=-gKqb0!B><υR0f0@kD)$BBK慝jWX5~Z=`4cӦĈ!!m fY6BKZcz1HImbq><}|hPR2[sEy ff"ąaއ:Bs~y-F88ߕBz9,vzů[VYŁ! q; |\ m?Bx+˜!8r> q?맟c[mn:aG>džme13!l[qlcqeREu.nH)aYZBn,Y(|1[m\x%x== !d}Yb-89|ɪ lhuRR@pF3-!S1 eAp)mۼ7b|ݫ2]+)_̪ nKRJRBʑk0!2\Y9BpeaL._,{gm} 3p!k !ӳl̚8[zwXV(j Yd3hK(ff13f2C΄1 IT%^Z-T#U J)6 )L gZjϸ~ |07Bj||쉬3[9tިtI@)40Zidh$QtVػ>?BAEUH˄W-.ܷyhU[6ڝDA]rs 2J+e~\3g?o4[h6h;vP5z B C RoͿ-_MG֡˗VmN)hh;{:ژ YxjuTQoUКKzfHknWU;oҸp׏+ťjZ:J-ߥ}^B&[{Wtw_IWW wQGg%dFڨ7O|d,7ơ\2b.% v| Xӹv\ bκ}Ԝ+/BYƋ`{/7e +َǿlG~~a~#Ғ|V'Uc:-RiL0ԢB'TS5R45~ d<# ٜƖsk:Pި B'rYAVb)Ӣ_isP $ia5-]%{mA++zHL?XDYz1r~>#i}VЧ6P}1m"'~ l6S\@ln(ɳ-}v}/HmkBF)3v/mkTSx}Kl$Iz^jw׮:xa֒3=g=f%HV3|V. %Pe|ڀlÆo>d6 { 6to_,@#"3"2232IvNM32#vqE5hq]?]SuVuhg}xev*5Z7[#UϮ35c#7Eo'j􍂱qsc5TggP6Qǵ]T۝q Zu܆Zk ~vsQulCz ~՛p#7[ 76nBv =r^õõ2ƾl;p6XC4g to Y!=-8{ ѱf?:QZ0F)FkSD }zJzb`7O%D|*)"B e&! q%V }uэ}ҌVknaq+o/$>sliU)v^@ g]= :b#O\ԸpLy?3łiQmoZlL$䰣-vEKb,x. tAn0%Œ3vFeYV~tS J5^bH-PNXbfT|A*M zfȥ*MרΆޤ=D\NYb1pV{5[)=19nb)^S"&)r/=K%Yb]mۙ](Q!@bS(sQ*$2ˉ M }Fd8bZʍ% F%K35o4HN;w@=\-M& fш,V 2ǞM#ag UxPnŇ[KYf c:lWM[VU"h(t(cJ[1JTU"h(EbJю3iJU$e弒&ÁtLFX*kqfKflS``@Mۓ̞ OA*+K2Q@eɞNm$#4<89SiHc՞qFMI1 4&$Имd!:}D]o%Ny! M`z0z]G`LSQu<܄65 f:EWإƩ+ud9@ B2n LOi)h+;5B7M%㦐#FE݈xgC$rLJ([O%8j1z`HT!T-?Dd >XG7)XxʀBtc ےN x4# -PڀCKK#E+d#5Jd+1Gbۈ2)dL:TڳZƈNa"%V(lBQ`{y4A# 05tLlg+Su ~яws A&(*#Foׅ9Tb3MjVgICԬX߶`\4%7 8kH@;YS|GC1o92S47Hk(nWPJ28e=qGr*. Q&դ@[CJf#} e#y\8O97KZ8#+*d*u<2L.f3Lt ["]NB Shr_ض`?i\?ើj}R!@vN]O~( $4jbLɍ.DӒJ$9` bKs3&&c7idWT|J@SD9lX=tTsF Q4!8h3ܜ4fQ[  M^!ezόcI_btb ,lUZK0,vR@XRҩ!SYƅ´)Ձ+_}ÑIኣZiں50V]7yHX^C25`Яt$ zaFh׌Ҍc5`ZQLzRѵ*]Q-H Ky-9s): b0 e<{zY}[UXh|dO|N}Jej˄C[dhNx"Zhui|Iʙ]d4VZD q&eE1G9@P~8sߠ :Fme}4:CGtqD(Bza3o[Nhܜ) #s=*eh۾I ^uҟ*N)@&vзG#4zprփQ'ԉn$i6ɳ-ry/:?#>T%AlUI_Q}z|Dr97s]M_ 6{Q~oF4,urĊ G{D_3sr:VF/=#֤YoNoi=oMLMHG4ihh'4=z˗D<}~\3 سgz%==EIsdQ@#Dղ.MӥoՁ3F! uG{+Ko5qH it?ʤyoe: `?**yowiom3e~㝓wiR$֗Uҗռ/[/k0zON}f^OiiaY!yDOYY?,V!Zp ~何e|be!鹡P> ]|;cAlh_7,-r_𫍩 Mߧlڄ%`eN[sUaCHQ gwۂ'/]HKTYZkWSN'>,YQP1'Yׯbw|6xNH$VP1?-@TKH11g"EAbjd3Xun͡OjvjD=UL@gQ 1w* J =:u+3`gs'ɲ/+(o+pV, %9hbP.*猅y- (I`ad6M"CV)Gyb9K168Ւk@,u=O3ɏ'\UmedIWzNOK={`  ^?ܕ¢m&χ{G'ɨm:3խn[wzL^ za–\#-]/)nQNeGzm$K>rHa_{_4#4l#F>#ЙFwqqY56,jj^2l^lFjc}Fʛ6mP6"zDX c.;B]8flؠy~ݸ2dXc Y/t ek,h20aoҹ].5[tJ􊟧lxRMD ʃ 3uzNy1 wp34J/ԸAo 77d\~ T# >reGS\lx:|-o sr߃/iKA?7c %,`Oa}d #`Q՟-lv>B&}$9D"4; i9O 8<3>dSiHs9eSB+T:]7QGwgLj'ğ{f;@PZ;-p:Tt܄5\Orkkg/ιvεv^ (\s =k ]5ts BCmz@s =s -\C:* aA^\gZg۹uv5t-IOs=k::ѹNtts =s :й5_˕wV@;+r|ӵsίʻ\;/vW9:[ykeʻ\C:~}Vz4t.йʻ\g/W:;::u\yeʻ\G::MYykeʻ\C:(?߉~, V5\#zhx.y#o Yivq%rLK:Axs=OW[WDkk%iuzVIk1G;767EvƯ3v$EDv,W_GOԒd;D@" ͡=nFвk\C:LWCSz"/9m]xTAb]FgA pT@a܇udOy % vHf'ā\5'{/.iE86yhx;N]A1QLj;_ZCe 2=Feu¶%}~Io@ @ #7PE.zkF@Oh \rK9lSA  I+IٺL-t aj96ҥ}a8d}v{k_xA Bl]%~GTLi:Vޒ(A3"u9g{Ok2q 5d!ШB-X8{J}~G+9gS0xLucэq1S7boz!:,].~)+9[DZo}tIQ/En=O_lbFˢsW"S;r䨂A^٦cPC1~*Ј/=%%˰B;@[khi6Z:ҹεi-x=4p9x0QVeK7iاO!θKmN N~v8~lHu,q#u@8n,+rN1AѾz_{H.y.23Ӥy;SӒS(~6Ņh_K \+tAOIknq!B݃u:T6Ua=-ʁoD+bo>}s¸/}_%&`V *v/@h(g^c59 uX]<0Ks zXfYr!^~gcS!r}'2Oеc V8O<9T^ ߕ ҇YtaMJStu^Bh\s( );m̙?0|/`d>yi>mqngor:2|0|ᯩ6jQ~ ͏Er&qX*'#<l4ރVzStB#Yi^? bhkrdVI#PdNz\{\Cߢ+NaMvM'VVI{]Hje;`i<3< mG_e=I޿PP;.½QRf[3]%4vų~ pL¸>ų YEǥV@^*FMX0b+?ZFŒ oNCqiGgnD|o9uIlj9h9e ~0'3ÑX!X85:E왑6*zgE.HydkV;am.7WՐ7_N׫Zz! 88#! Isc{e_6pi펮l=Be67 IIm#|.Ho '9_\=l4sE֗lV7FWaB>BGno}*؁4=rI47B5vȥ6Bd(`ѕ-s\86|i:I~1Z5v=h{;| zkg\ojb"[m6@qa| WXp7~T*>څ@ėGk@0{3*2$\L邮u7 R{^ہp\QpweۀۻWc{\_v,4Z +2UCu` NEa!"w}8hBPywAZ;H1l;;Ѐ]2MLrs|s5I43iyo2-,eŗo.Z78y8ubenWtK%DZvݒM8jKbTjjb=Zd"HR,ge#<8[V4KV2O$RAuPT*Œ84J< vդ;EĚ^I&R扔U+UBTti@Rቔ&2%lG"J‰ @^*Df:2wy"b-WJ%F*Wʞx"Y,쪿AX**VbKceWCX*eTrա2)>;R [2,)VѬXS QsD26^vRȸ"+6"d"IzLI$U2ͭ)LY$SBi*r)H"2LUƴRD*)#rMJU!2L薱I6HZ,Dzi48ғJ%qSDFP8V쥳l@㙎]-P*N:߳D!Ŏ:t8ҖNOXL[Wxښ58B֣'ıRD%!ڵ-I >mkG}`xBBXށTMem'PZ_\\}j]21ݻHlp u`K#8`A|6Udc`󤟠 `E (L- |0wK{맪Utݧ:}Q`]2{p|隃Tg&5m@& a}=U >d}Fy'=σ7< DFOr)Muat5rEXJ+H6IhX@g2Z !}y}7m%o9<{)[K ; Va<  gQצ2{76I? QO%JO[{#蜸;(ixdXڟcx2t#9$H pB:lmWޗ1y,+j< E ?w๨ {75AGN3mOm{:&uDh7$HB;dm  Xxl. veOZwI$F B\yJFQOnxD8X G9KʾgwL I,}TϞHVs*.)?t }5 jKg릟@i=At MϹ_ܻb&;uj^@k9D!'q4 W׆^!?ΟQpݥ˥w>;*e:OSryrB4~Am1d 'KecD~$pg/왉5>}ĶB'tLrz|6NY-#N}_y`zUt$ÇM/U_ 8ZIոvRaڂ3O2BWPn=O?@ M33KWxrdzaDJ֥zPgu^߁W63ޟ_[+|l>tKC/'w^uJ3Õ_d6=kq%qm\BO#b'o9h$aVy,6وDy><^N&q"=߀O`~m<U;¯vk# _OZn &0:՝鴂/YiT1D?WE/GzR̳qil8]1,= vᎿ<}CGf/,`Mlw. a4DN湃Z~ ZcQ&VZkH@ tn3;i|bشT\ ciQ}kn [՗0 tXo.BWb;bHq_6%>HNVג}#jV͟B0XRB>J%[\ )z\+W%wI˿PƸn=n[rաWWȗ}AdR3O$fDM:c`ܿ^FҝGtas6%"B "-t*Aʄ?RQg-dDɎE.Q1mzL)G@I!qN L(9ɝJOwTE1G(-$o7Tg{Ex<蝬% ZiiOJݚ)u{ԝSrS4 7)unmu:]c(ɺ3I2K]}q;H⍲_+X1^ы;:mRbP6?u죨V1&02a!6$b=lQ\1sX}BwlGM)lhQ&7:t@={ }i'#32ckZd&y tZ(MIFOjKx;dUApV ;RVCga|GhGZ׻|?.VH&>P48͚ja~u,ןdҕ;C\ɽ]k;itntuttmt4Zz6ӕI쁵`X^^09bV&,UG f1Nria{Z8z槅u<σw\gӪ]_?nx$_xK'I.,j9fSg&:SZ|QiE^f!W7&,k&';Ӟ'|6w>knkVhl\#epxyy&y6ތO# =n߀5ʖ=ʶ3ʎ6ΓۦIҍݧ|dLO"%],Ɏ(ɞzI3eT{6M$~M93 Y?; F/#w G˕xzET99gB2NGOhm=2Pipy?e2xM; -PhS4ȩZӁ5!IS%n,#.5JhJľBLƾBYbX%z%e.uJ,Gہ^!v>P}zNӧH*IUR:*V$ KARra ۧa4le/h8 'ޑ|!eqJ Ex,`t?6?j6=e+znv)]<f*O`wm]G8sojcjɅtr%\)c'wG=A)W='ZmSG\Tte<^Rk#2ǫQR.,':2%%ÙċGa"B+GGd޿lӝާ^[ülʟ/L&@HV]/a}Ӷ$x[ّ-$i$DmMlmkyޟ&OY[i9*Zg2<P&y <&ydiuf+;JHM9gcV\eo/O3Tts1詮f&UWegG,cyE{xM2:c̑,|N'^bgIZd$M,B/^&/ICQ/^<^ͺscz3^r=U5͊'T=ae+C߂SSed֑yrٟ)ɽZ[)[y:DԼc\ڒM <1g1I_틙,1)E}1yc6]g9-^3q׈,'TK3Tts1.z52y?3qc?9?HM/z_2ܳ9:a$_i#ZUq7!;|'^b.ggND|͟߇#!;z{rGqj;w[5tأdnjX٭s*g[F1}<ո}CV,Z LzyuJ̒'m5mkjEYI[8I;d'go|2zgihlOF2'-}zgu:։б[wYcֈ,d}M(~M̢=#y{.zn:{vo7Ccy4Dz ӵNB|Ie' mLՊdF]&ںN^q,c@[iuii4!.x`_CFdrЖjI<'#"MwA% 8 d 1QW `9} ļwvFG+|A}dP:F%=.(jDĭQ+W.\^rMjD'b$ywۤUol[IJVq5Pv8"5=~#ةlʵ"˵gZY3rr_Zxz%\)ۖpGeKqΥ { {~+SD{9V}x'@wʨ> "O?˰fP%aϠ [*ÙAW+ǥcRk%')~si(e0-HzS$_bNCQP}n6G,@7cf=H` jQf|DK1TďS_J#v֣w}|>] =VF^ץXq]`UڙoIy +`i.440_N{(=T %wtC)aH6rCȷLKU2@YE)VY0{d\INk^x]3Xg52Zp `^DhcTGzc$Mde}?DDy#+ioԣdw7y,'!YXMdzԓ[lZm1ud_7Tv4|5w1IWSPלJlPTs  Cq wf, wU}c3RNs"w/k e/<[],mUQ*+S_G3b rBHMl#iK[ͶF]m4Nc4e{GxbLǤ3:֏h*Ɖ`}ŭzL}ǡϧ+FIJ}饔s#m)k3l(-/ $Tt{Z3~2L>;=b>a Eue)oqeu(s]*se)mS^cK ;VS|f,bmQ?ӓrglQq 7B]Za*D:$|cG8M>xj4WV4]ƻ52ņe8%A3=[E.;~Dc!ntxh]\7Wvh׫O{>!z+ kn7з_Wyrmt/[]R*+жŪgyNR8G]=A2c0B9;oqtj M*z%àx(@Jb~qzܸz3C/ĞZ亾3Kʱ&`s1>~m]\_V Z×qccl/#Tz]?]SuVu 7I 677[ëjil`qcg@3#{mt;UqscPAe\FvQnwƵ&$jqj-U!ᲾG lw iOބjqM vK9طpu6mLfc߁Kݴ&_ؼ¦ 49)?)BGqO c'1 d\KrǭƗW[-E7I3Z}w(mC riU)v^@ g]= :x"GຟBqI]/yf<ӢRW]oZlL$䰣-vEKb,x. tAn0%Œ3vFeYV~l>r=ՕxkH!2 F8Al[968S0oU@i$͐K=U>Q I1zJtING_ ǧըߪ;>^OOMt1PGS,DLR^zrKڶ3PmCĦ&= PxQ*$2ˉ M }Fd8bZʍ% F%K35o4HN;w@=\-M& fш,V 2ǞM#ag UxPnŇZ4L!cCG R`Pc*JePelc[)p|V)1 AJPCќEHXL !818z& TiJWWr$w80ә)H!FR{V%36ݩ?00 &oiIf 槠pD[Q%WYU(}Di{]Zd 6L>89Si 0}>p܌4jLI1 4&Ԅ=% ٘#zs/q#y!o"׋0P<G`:U0xy5 f:]m`bi\L6A~jS;ԝ*'s сx#!lG@ЛȒqSȑU #nD3Sbí_b49SI$ZG`!43> =U$U)|KpO;9%9ƑM >2tJwɺ8f`r% P0th`ii3](Xv}`6`R|\|pb"r>-FvWo#:835Pm jϖk}#";aXZV GU:d_'/,h1iBpN,H7RG?v5"VR%# W@D8I_[>SQt6[X:'i Qb}ۂrt2ଡ#dMu .ӿWLް#p]A*GqȡDVfm *p`W.Aߢ3G~)|Q+Yad~E F?wDO$J.f3/|Jp-.f'}`!ob9/l[牟URf -VI'UdY 9W K|H!S҄^FZ珫8x1%7>LMK*̃6U/}lĚHLޤ]Q) Ma6gy_O`QdG~: ʇ{㰣psEmdTΏ6y$ӗMa?3%}Ia&2* -%D ;) ,)PԐ,^ BQaڔ Ho>h$IpQ LRm]L}m), qPɞ~ޯ>`X 2HA0EZ3Pi$.Q2 Lʊ0b r:Cp斿AAt :ht4"VQ"fvE "6: N89/SG 2+zJc TК+d})="/a+esgSJ(9I-:o]$>mH;͠!`=97Qzӌt"%V2hdA؁}t^IO!~V°l4Tɝ2ky3JJGɄՊ85TZ V*P\ԋ؄]Q1˥t?r~CpYb̚ނ8;]Qnqq; L&!+ RC &3౲ DX|¦_ԙM$w)&wi/|3^=XzFe#INV*]Vj%o Bma]uhȡut]Bo 9B0z*3ew6.ICF"!tiCGD>Q'ԉn&Ro6ɳ-ry/:?#>T%AlUI_-6ѧxMG|qR_!=f/@>NX11b+!~fNNH=WCb~ߚ0+OC-MGM|{}Js&[vHvMӣ|Isؗ5Cn=;y\snsYdQ|KEu4iWʻ4MU 4D]./,0ȻמJ?ʤyoe: `?**yowiomyKɻT)Sd*jޗi-ڗ5L=XwSS/4 U!S`LPx[pKiIUS5Rk {#>u}0^Z{ȎT(AWE;><`|q+b *lw䇥qȘʘ F[E1Sx2,:7'^]5;^5q垪c&(;KijE:γI_܃8:IFm酼dɭnluC׋`Rg3(&iRWQLq2w*;z8 n$64^(dCg,$G ڣ%xa14:79aQ3V5NabN8oWMno?Hy޴KB%H=wc [42ywᘱmcu(Ӓai*Cwlx RAI߆S_ =B+Aq-3- SHyK&Oio RKDyʆ'DT<~ԧd?Ni6O &ޢv6xFQ7MX!p&U˯!ޑZjs R¢sz`|*˒rOOOse rX{% c)&q) LpD,s-}h*'Ws Mr/%H; sImsD x%ꉮ;Di7Pmv5spy$g|vҔ+\sr- WuNio2ώO>?Exvw8[t.yj/ADe<5tg\C:VXCvs=s::ѹNhN܃ε쵳k\;9EPa5tgK5t54n!/Vuu\g:;W\CzZֵ5묡Zr9_yk目D;~+r :_yk\Chq]AG+r41c]A;+rk41g]AC+r kh ur|]sƫ~}Vz4t.й4JOws 櫭;HU }^e*:eH-!w:k>-9X1.Q=Lt|X_SԒYBVWSDqlН& (-.^Q/`{ eqQ\6^)` {*n{ Ԍ"80FdN<E8:yh;Nu>J?ImkP[k!ԀhǨSz؛z~2_+P}H| v617x %'J(p^zk |aavPHE?}{չETE r# cjOn7.yJ~17Q]>,].~)+9[DZo}tIQ/En=O_lbEQ9U{OBR+)p9QH RN1OUwhėn HeXwQ~ =l킿G*v94ݝ Q::D]'עxEh#> VF:V汎(q$~z#2£wx9Dg0]r69 }ڏ0g+];X^WOG |-,PW݀=3F g F:~E!{瑻8DGIv`yJfǦJuQCc rpp .kp{v{]">}lYQ#Hf%>Qǿ}o62mUb\̢%-]WyfI#.2joȣyT"J?*dxq6XQ5D9g*:/:{,=[2-h -'kfDRȄC M <6_Tx˟y$mgCC?cF\+7J uKhڻ?0zf' ~ͦ㋛/,]/$/S3} ~]fy_+mZT])-߃~c{C=VK]B  B|؂U.ZWCϦ[>^/6 q-='||7B"/|'2Oеc s"z6n_xGJa Pc6a k uA aNɆ,ѭ=|Ґʜ=]'u kOkGx-,[} Eyv_nVzc]mW^v>nD^;N*d~5 xX^}0v}_3^1Zt@E%X_c-1ΑDpAcZQg[$=7=r;W?GNǨ7ܒr$ ǣt%;tl;:oEUbvj`,a92ar-kᛧ+Zx#ZxXk_\wFWC߸N~u;]jk _O@1Hox~= 8҈%fVZ@u4J&g'467W渶7Kkt *vwQkMHԪPu[[ppYߣ6نD4jq]mB"z䲽E9pƾ5l;p[u\|L߆H`;E AŧNX'܀$&yE.e~Tb='W(Mok%g/ۨUywm?OxQ⪾[߁J7v%5#HM>XkK 7zmm~m P YG+_[>4pm|tulcrjNÕ{vZA.;-.ζ;l_;-Fjb,/ ǏPGPvȡhXp0{3*2$\L邮u7 2d{^ j7BḢʰp`at( C 6w'5z $,AءS. 0!쀧tq]6L6Wa{Jv;C>9h㸃]d:p6MLnsxso}xj\K=C&BpˆŘorSz-|bٚto %߄4jV$/8.i0 "Y닋ZW=c~>F{ cAwi;զ lgLxt]Z!SsAnɑTT]:!,=xON|S!]sV8 ľW evdd!н`/ϠoƽM]һ6GP:H#p~iq{x*lgA8A3`9'{E93^uM)ѼzQҦ.H^M4↤~t= 8Ѐ퓀Ph?8ܾ4ASe6:ٮiSNC[1\k0O9)W'K>gVP ڴݬy?يqN3!=zㅾڞAmɡ'}zS_Q=DZOHegxs1Ul=:5RQ5p^ǺkCF/LՊpϨ 8 ҵ_ѻbO2De'pl)ՊG|J9<9m!zge|Ϡ_cC21"h?C3Pgl5>ltĶB'tLrz|6NY-#N}_y`zUt$ÇM/U_ 8ZIոvRaڂeZzxow o͟<]r#.# sx'RbU=.~wj>߭C-X2߇Gͷ̟J^c[ i>iP2͟ "&2DnmDs}k-Hޟ7ug'zVd- ?_#h)zBd;G؜ڼʞ/~{2l0 3t6?O1_dj⏾??xï?C=${B([O{GD5yoBcZ8~"!F=#qY,(;Fh|v/cL>y <F# CcFm$yT,WgP^q4 x3LdVGk4ƨ~[TOj}rd0ѩ,O|ɺNSp`5&GDW.z9փ0͗JeK_ve؏a w x :2{axylbsIe!r{_"V2^4Fb0wCh@A1LæZ@d#N{玢s_7tfتQ3xK&piJϼ#@Rߵp?(1?DrȫQX=ldjQŒr!W*Y5 RXHNR\(?.;H\-2EvP=զW^]!_I7%GߤԹ!tң$$ɔ v,uя 57~cnjzF/jHķI嗋iNCiԱ@[\?[xCkȓqKGt8@uDqa==NBuE7=zG,!P:%rP):mJVԎ}5̑RL4:hP?K1Ԗ8vȪ .pwdi㥬>ϐ8к8.w\7L|*ɡ>ip5պN2 'ReY.?'_7ɤ+Iwz-{ɻTywrكڃڪ؃i5ӵl+Sk 6a=s4a>r".MX.f!W7*,c&!9Rkak)pϓ竅(O x7/ﺹ&ΤU~|]52GIr&N2͋,]=Yr̦N.,M=t|46a6ӎ6!+BnMX֨M>NwK=O:Dmr|iz"BIF=*Ll"G=AvF{ݾek -%e{ m%eg m'].YMOO\$7}?wEJbYQ==g؉mVIvA.=rf@~LwZ_>Gz+r r+.e^zzeD镡~IpevZ[.Ц`/hSsjBJKJY"G\jhєr}$%ә}\MU;İqK"K \r?X,Q5"#BZ|^ہs>=mOO-]ő!T~->tT4H"[AOi^Rp$N #8DC$$CA=X~lKծmܧ#zy>8kTl)ʻppȓ (ѝKSN`7X{1R+{Nõڦx= dT%! >O SBWWSPNљJǡHL3.S׹U/g#\CBmLctF`X(9[UEnU1♇iqYu6 WV6bxs}x9>yt}эNG.eDW$]nYNtd,JJ+?3Iw~>ȧ%D"WLɼW٦;OyyU?/>_.3lMb&+VE_mI&&#[* oIHښDG?kMĿdsBU&aey'$+桊'LP&yLJ1<ͺ3 :V8ws^ϘǬ2$^Sg䩪&cS]MlNۗ ]Ydyeju#YPO/ߓxIX^$MF/^^$y$uw0t#% Tg{j>Nz o]VW%:*Ȭ#5?3R{Eӵ*$Suye%6y8cb3YcRzblrZ{ 3 9gY/Of䩪&c7S]jeTeggmfesfsOo'摒s_egsBu}I0vFo&׫nBv2NN߽R']!ߝR`u=8 ?GBw0-,9$vj2G'ɒe&B[M=:Ufc.xq ;?f-.Xꮕ%O*jkծpB!o'-vNdMn]LVٞH3edNZifx(tcgy糒ǬY^9QE{Fz3 \&Yptn iekӝʦO6v3|{=:; «܍&y7Mu4}Xu)ǀ(.> QiQViwC7U]p>%!:-Ԓx4O΁GyEJX4pn c5\_?s~y֍W&t*OJz\P5Ո[oͣW.\^r{嚠~jOjII)ضHÓykSpDjz!FSٔkEkϴ\;\g:-J̧3(S-R8&3KaW(&/ѷsd©ۭO*WՕQ=}NGD~a͠ K*ÞAT32 VKG)ܥ2rKNR4P`d[rBI9K߿4 R Ĝ,5z mXto \-)0}K{9~;%"mԢ0Iub/Hmq˧FG}z2?}?KF1&Lqx%Hy${ZOW-tBd# q%]R}t"<3$o=ό2N|)F׆bN}駾R{>P!+1 tO P2AA5J~֓_!jAM,B X:Qc!G m$u t~V$Z'J%\S(RV~8JC6\lW;YykAdcg<3<ǁQ:Vi6Wz:bt-w)3՛s9y*>RemIU6U1vwVr\q=dl@mu$vkr^VK8Q$'p[Ʀ7Md\axUɱ[:3֨[J]hH=tt|>yHS?fVGo|E߯!tpQS wia!w ރ;Լ3Ƴ^}&OܼV@sw=UIO'-j.jF  @3]x6lq5]u *u>XxD!aXiZTg4=nCQ^u+3`zNC , CA!\7E6 t-vE/ܶi^(~:G%0N321@6h}:KA{ i B9Gt&ٿW3mkBSx]N0Æ WM- 2*5X|DZ=fLc(F? hҶ BLrGpodgd/e>f 9]WA\\Lf[xilϣTs1H QlHاEt(SAZ_Y X mkBTWx흍8 FSHI!)$FRHnw HYx3ꇤsaaaaxIǏ'U{o_ھgW9 o'GW {>~Jlo߾)*/N\ϱov[iZ_ձaJΝ/:6O- 92b?Tlk%?_21B sY5>:>c=1Ow y^- ڶ,XzusM#גU]>H_yYv!ۉ_mi Rus]Xm_g)YY)m]y,m z1aaaxEߓGקo/Y\k6xjgH|yu.\aæM&wk#ϐ$?]Mo\Ⱦ,/ڥQ@~6s?)}, l gX #vQg Bٙ^uのuhm?}{].~}v_J;xogJY]޳@.)oqC?}>@Xߘ'-(W? źvƔOʙRv[K?[A}?-wmՑ}g\=c}M ggg DŽ-B^k_g?F? v0||؎=ǧHPgs/hؑI t~{n^}ZyD5XWvO)"c0vY Z|~_%/,p\ɹyΰZ/;/xs_9?Pܯ5ݻ\[y|č8gʱL{? 0 0 _k3>z_\S |<)b|7aaaxn.ta?l^Cvkؽ#~e)3<3^kdlc&jK+o"e<.ʞ`^(3zu l+6v<ï k7]/lc[`On}򚄫 G뎱zt^v2)?;Wmr5ocIz?Ozx{&!ez."ѯ 1Gg{+ҏlw<=}GݽFƨ^)zIpG K֜{{e G12ۭqiumf>.}~a? 0 0 [u+7Svq֭y΅ ?ނ}XwŶv?ߩDZۓ-q/?߳=<~#>Fk"qzrQo 9r,nY[;o:)@-`ק-7({߯S@µK9֠ɸ>:n3 _[_*mtcmC>qSL=<6;ǫsaaa{xˌ\ފpx?0׋#5zяc]x^l򼠕(f:~٣^lin59W~\;?vn6erUbS~v^U O7O(|;+SG4|?f*?rW~2oNٟS9~daևmH6mX[J~s.ym4ٶO|Bd/b5ɿyU? 0 0 0 0 0 0 0.P~*1@G\⟿KrKXs2(ߥ纎J8'>X@▼QQbqwx b)_K|v 1M6kee-2Ǜ59?K^E~9ϱQﱮYF8N?~;:=J<-tĒyNAgC \NXKs)'^Kg\~2}6}Գ)n]Or^j~"{p29w6/.z-v:+M{WJYZ굢`% Ҥl9ힶկ#OUz+U?;sd~vND7*.Y+v:ye;8}~|+ÑޅN9}{Bƞ#txխsXɿkSV/uJ=o G<ջL'L:D]6jfgLz/+ؽ[{rCMYq~[{yy czA;w9zszWHVax3 %mkBTx흍) q ĉ8D^>׻gI@XjjgiЃ`0 `0 ?ϟ|:seQ3|ӧO|:2|.};7eGFO6_Qv]T]^ˮg{>pjzkuo{yye?{-x/ D:3D&򈼹e^Hyi#/OGzϪ߯_~ :sMe#M3Y#=2 QЙ[\s=E8}E>GȩT ڲTg-}VfoSVwzV}./>~!?U1<#}=F[ ~QڋBN..+푹^edLo+[\-k dW(}6q$#?z6Bөi?L7!3O_Q}Пuo[=tkȋM!'}/Ƈdr2_Cﲨ: `0 :8o=+8-4}۞cĥXdq{bUq©ήm!ƶg*ΪU\z[GA=^+ru{LV U?)V>ғ)x|Yҁgi\yi^cUo*= !TY?rfgWsʽVn*VX#=Fϫ+[F~yH\L~[O҇h5ݵTow|Sfӟ+);F;:x )/OS yUo2e)Ve3'wgGg=J^`0  ľu kU,Ksؑ5nY,bXw{ w&3QהNQev ]ƷgcH˞i{A3I8hwduwUIWq8I>+@pQşGcZ\ƪUߝ]/:3d;ɫ:gB9R|GW~w2;fzt|+i5nΟgZY|<1NyŬ|E7k?z/k><=Α}N΅>uWydʬdz `0 *\?W8GY:Dgcg< 2+'W6qn؟{ru"wU쏘~c#T?+y{Q,,^qF/Xv8.֩g3}ȸOP ~n%hUG4(_sn|W}Tg&x^c,Fѭ+ <#+}/Uw8BRh_|33!mr\7U9m({ѝpvew[xG]߱?g;,nҽow8]וb?OV=Z_#ve?vN_WrYLo;1g9pV^G~>[_vNOS3 `0Q[ veO\k^8֔v<Zbz\Opbn$~}oz3ј mK vU]^iNWA#x딫jt q :E= z%օq)CcYEqyRG-+u (K\hP'*^ء^q=m=y|Kvūe\rȊ4={W1;=ݷxp;o@>ȘT\Ԏ+C=*ɫ|GJOCW]x1.ﵠ9_Eб Vq)v(ʑ}[GwǺ{-oSdו_˞׃2;iT&w*w:g׭SOsj%Z[~_˯d֮+w]7 `0]kIu+eL]ւoA^;=GR?v쯱;<y o$N1紈=:ߥPVu< <&3KyC/4r)i=*/|Ύ^]QNН1qGw>ù{ ?Kv:A}E:_n+{u=rq͓̳]>>d}+|L01`0 leg:׺񶊝`W,3O?]\9P~[kOWiGc~)-<w.3q}'vuw$Vnv(r52S;Wk_Kϔ8B/hEՠ'9w?K;x:x<|@cϽVyc@ۖSw8Bq]=2lBe6V}eR( VeZT4ade2ޒ+nYBTqSߔ<[&=f[|szP)G}{Zׅ3n7jpWwftEw[ǽ;`l? `0 `0 `{~i`oLy>uoi\qK|}7Svu9G쯿c¾#>,jow{ՆݲL=mW2u_8دjo?kD߱mw>#}E:OۡO;y`$jwymkBT6x횉m0]HI!)$FR?6c>>~sm+vuՑνYu8uN?WP>1JsWiV_uKEϸ/rˆ_gKW]ױEYcl,[TYHT}xL#}A GV7^}>iҞ-i;}LJX&TP3T#ߨgJl e'=?͘ona|7>?ǐU%;/mN/IfQփz{G}?v✽3X~j{zTAO^ʰ>?sy|G)PU{ ..T}6ڳ-F`p]k߅~b  О$wݓٱ|sCoA+q3lOx@(0a+? T,_7s\Ϙ^Bl1)C+k(FyN"8dPC_9>O0&l4Im+nwGrŰ)/tihf ѸX>E)<,6s45zb?J\<OM%O#(76:= ӋYAƒH Ls6MXBcX&ǘJte. 3.je(??Lj=%wZizFTx$kP8Em jAOހ>~؆B9 ֤8UKCvjbL Cy ;mj P. DkwUE€3ܨ8xUJs\ɟ+;}sFQ(KIXݛƨ 1 +KdX];Jģcx$D׷X`i @l̏rnm$^9΄zBGϞQ=nfkDe; <a>,⢞jk0B[p($Ǡp4 nq`XƓ vϵ.xHnorJ5Hu뇗 f a[Z:>36[g RL؍?( &w.7C#~B{] UW 71jk~ecGrD.=K@WDZM0倐0\xvqNZ ># BE )&yA}t?B Ym(WIpɱ |2+\2 )l8tl@Z.Be񅋍RSƃm>dIl'N adĢG3%#)?$s _5=YBR#-k"qGP-e"f%֩-ϓ378M9ϊ,_*n;HEBƱcl~ ˝[/sagIE2,z1t:kLș壋G){7ond{@rP>kwk׽ #kXfyEAB9uM4P=_lgW؇N#_nGpp ,ZUu6ȓVӰ0EK7*|]{75F\ԶzQz! uH>upT٣o3P)[^6` -d&*=%fY<^ط`_6|h3ء>2 Pq7ώ ,NsjF=B` 큳CiU)R鐏@LҮǧmb<2FHRqùFXi䎲OmGA}:*u f:@ʫRH.66jcGOpO- 6HKJU:Jǃv,3DZEƮqq7p?ȌK%ȧ$;?Qr6pP7`a^=R_)m>D3#£ _' Iɭu͋C-Rne㯄ssL<ȭ/R)|Lt_1Lk=rr 4/gEr~PnB[\g[{gYvRW' {Fem1{ wL;7&$xc0 n&u@5sCCձm8Heft x{q(aтa?Q%l4ςxmWI׆GC1kQ3iJh,KRO`ʲ4)%b6B8\pe;u)ko)#WSncRx{[sXv195_0Kՙ7>Tp5ٴl3S"؝LX睫[5m Q="u}pϘ*xbՉ#iM+@Z! Ϯ~jYݬ$?5mtu] %@݅:4h8ۃtu3; ΑO1A/r R*5i&j#Y2:$Z(ad@>'z L뇶6Z8|`6"X1_z' F-я?X^ A:?1;h/KVB' vOnFS ƤQ{=kh7MwXQp\v͓O/. N3HKRlK"q^Wh1wt h@3e6N|I;y?8t[[! $,ήLe"z%IކAkRl!3u8ځy?_W)AbCO!rza5Sn֗#<43y6"R߃CQ&>[# BHǽ{vekOTlq(UH͵h ݔ8,@tՂL{p/*L"d_y k,4 G̖bD>,.ok"D;|7[.DCA#ilϟI֬Dq]+eE _-- ڰc^Lq1~CCC9gNH8BkhJ#Z-`VoMa 9r$պZ-hkh ?C$ ^tď9d(8P݅]ڶw[wl;dn׆oKd Hބ(DInI M_(5)6H/Y1 QRk,nXHʉ?>df&6^EJmt{CCc`0ʅv5x<\9Yc}106"״!֏9dl:' 1H"z'7QqɌ#KR./CVgQȬ\ `?d1yuM6Ƶ8ZX]8^pwQE &1frRKi$GݜЕh3'{;;~FK37ku<pdʎ+C RMzƏ7)nҀ lEGyl:̑IoBS%|ЕsTulebA}Aʹ10A{KʘӺtjdLI=r PRg_LbR Şl?␔)![Fo wi&k^CV(t@pW2{hxHGRn͉eCbxԉ6GQd27\ثdS=\Ff*0ۣOP5(rZߙxQZ>~GAeN-jY7Ҿn;n?ӹ"Px}/NW:݊&׾:x" ꭥу;R펔 c䛅љElmG§a= h¨BG_uYnZ쫭FYs U"zM&:Gnu.DX5Xn;}ԫ%XO?~2&Frjj8 yA*W I9/ub)Zl: s 85J>~iI3Yԕ;:#hELם[ROd^GA˩f~Y!En0~/A Km>^WYq"<цF*c:xw|͞w%ehRgd9̕v3v Dgh>>?3hYDkgC(ʹƒԕSԜ| 2Q94(?OGQ34 fccPopTYaW(>@tX4`LGٞpɄaŰl\[9c26U M6f,'C4i?W~psϠ?kAKrŵk@I|>^xs?\`,D̒5W^w DMXf_8<%|8_왉pP1Wlm߃f?4:́_Ԕv M;k:p_sj؎qw]$F}y ,b'N=o0, ~M YR46+!}@~ujctCP.Y(x׎z?70WXFܣo3z0c8RGg0 TU򄽻w"/4֏CQ`[{Ocn]+{{ N!33+5]qpj' r9FDȬ)~: 9Gmx2-?sraG"yvUpa;Ră A\& ?#n 0eed~oq嶭!!DzP^H)>oȑ.ļԶ=Hy7S-M ?8ycߧq|#5"2Б lm#UeΤVbM͘jAc7Z ]> 4gb s 2WRsKg6 's8qzTT[R[w)I95xWj #!nN+zPڔ KgTE,?{^RDݥ=Ru^zîc&D'i74SJߔ&HUG[crͦ<׿~4}څh;lpAZ%XZ;tQ?yk1+Ƴu6[ Dc4Ɯ*dB#!}e>samhG3c^8u9󼵕⸈߂UyB;f "Yi=D =4&|C3g]~WgjhSIXU"1A5Fr4{AljwTt6</N \Rta| i>T.Wo>>xϯY{緷m,J{gg}v~)]s!?wXGFl!7U|Cnfﳅ:.@mq%臔Ru?.:aBֺE#Gg'yXDuSWNJD)21ѵVagWPqȒ s?¶@g")s\T{f3go^w:^"{d#!φt},nyWFKv„X4|VB~,˘_&fjp/WԍwaO H 3I`u1ͤ+_W|+_W|+_W|+_W|+_W|+_W|+_W|+_W|+_W|+_W|+_W|+_W|+_W|+_W|+_W|+_Wݚw)2iTXtXML:com.adobe.xmp Adobe Fireworks CS3 2007-10-06T22:15:44Z 2007-10-06T22:29:43Z image/png IDATxOl&v! vtibۢJՖV*J 7=T=QjU@J(ZuѪ]BB 'NDZ=37c;dm1-}ӌ'{Ͽ20WR VJb01Ub \ne$9z߬gVF~W] y3^{>l*`GLY$x+ JuݧdÇq1+q\qR_kkх~:`[xAmW$ ۩oL&Ϸe +G[UL&$Rk1F &{BACcuֵo=zoܸ1dwψDڟi`q|L_ϟ0Ưea^Mﰢcvuu}1 h*$3/T,>`qD" |(͛w;vl'x ;0C\ZhȌ?԰ 0?}d'Fp'rС@ H̐wb;Z/;=myjkk[uwfl]-A SI cF1Bktww8娫R$n*ZhQ>q c|| @GG;v.ب[D]7O:u~+RWW߿Kخr+nPh gN%M=ʈ['Zj[KA0bQxV2EoۿٹԩS߾}0kE^fA>'s3444q̙ON Adobe Fireworks CS3 2007-10-06T21:59:47Z 2007-10-06T22:14:15Z image/png n?prVWxZnI8Ѭ)1@#. f&# шK)&B= ȯ K< ++S^}q<V]\sٟv OOOi9'dOʶ/)dz ?AxKoc[C3S? hå 96)v[hg)v9F'2oe[snR 6kr}}E-W7$2P_ASmW` WaWgkkG֮,s1EV(Waku{ 'Llv]i= z=_5JAk8R"/а+[$˟oIÇ5]9Ub{NBbepp_i!܈K=';9g]*4um}|ҏ5\@+p/D$2#<-m0?!ܛd:ew^G_HEG2"~ApV^.Ͽl癣/m~+L:vF=h9>ا3u?0(.B?pMq:bPy?Y#m+q!j7S+&=([?S川Y"ty}|gMЏ 7gmau=??_[?6|V3v胏kKG6Eh<f#XfwbvVJ.p\@g.j+y3%,N*:/vF#H&|?mzP(QY#P̤B- ,3P{t(dphGHs+8 8mWƏ-`` &㙵+P&/V&>.b ̉f) Ǥ~5} +.wEJAmZ-"ycԀq4+ 3`aW݌F# _@9;/'Ln-j T!J;b+@?YM XkkG֣t(cb6) .I>a9CW>E쿙dV U;,iG!y/Q R%cz5dӒ< A!T9pFy[D:sȨ,l ;HF߂ڿz]%Lc[q3+acktA䊲E R-Baf}pcQYׅDBgy 7JR4>ū<7XY`|6wZ+1 &ooELp6­aA>JYj֣`zQqwc0 i_|f3JFɟ~: D4#O(@i @w>)AV?BTnKݡJ4 oPU"c,2]@;B?b5|5 (LDJ p%%[+txu bg QD.+yɊܙL󒜍l/sDj6V0'&zL 8d  .ܓ,.$*p,._jF0@GGH/7kDhõK'0B !#1\5[~Ȝ$2TW쟳^.L`a֡Y/wW7(B so,YG%-3~EZTq ڠ6AF3HNYgdd iGK7uQk>g 2o9[HQeWh7pgۭ%s~jF:#6*\g$.in:W_.Aԕߩ.XA >RfX?ˈ-k s.˹,CU ^cKb=TNDuub5R/>jzȥ7U&\rDE+<) {:z\CPdv툏}lA`ƛ{Sb?)r:CGhH_=9?"zΘu Rژ ӦT8w R^-_/C`;u> ~.;V[&vo,}/.޶W]jسҹgC9 X1/'V^)}9cxX T}J(ٱGk8 7UR.6d|2Jw(4A1K1IcRilNغT[4]*{4쫊!g{uAל|:%Fa8/YGwO"E n9;aIoߙ]]W7g)~]g)mrRk>Z."M8t#w&6K66a ek@zjT}lL1E}l`dž9)>}lSOy! HmkBF)3 mkTSx}KIz^jwg8|KkH/Jr|c~;fmUsȞ@ԓ,MM{Ze|ڀlÆo>d6 { 6to_,@㏈̈Ȭj2Y3UD|Gl5{gzudld0{>9g{rkcrw79ig޽)$X$&o~yց?7wOZ5·?ڹO.ıVz)gݍBR 5.p?[mb^nemo}F`]ЛΔjm>ܥw{;l$!gwI..!qKnpq#2CZn# ڷ41sYffYTl|8(ac`3lRl<77:OSx&Iiz/c]@- 3`g#0R#nfMT8_t"^g>ѫ; 5ǩ5d ^Qjt&~r m?&atat mӧv 6by?͜S<{$0XJ@t#o$ Z6BcShl M)4M`k̢. | POvD9=#"R|O透t4# ȃ!7{&{A~Hje0HŠtig\`-30c`cQn8V0zabxκc6)ѳ,nKў3ʍ]@7F"7.ɭ 3(@?9T@~@r oM1 *alB(TgFj@y+ԢڅЧlr᳷Fii(,xiiU=s|0AԵҿ:C~6ܴvt?_qZg)J 3oB#G({6HArX ğ02m HIǑQn-_/_! 6#s<1LHf0FK 1L5qJgJeEF2'HptSp X*"i1$$0Ù9DyJf]Z_b#4lL te- =5d"]IW;:0a<;$$wJ)5Ivӱnc?F@;ߎvӖĝZkG&hMbgo%3:JjVn %d 3QE6gx^r }v9ri2m]͚4%>!EE[_Bx5WM 7Y䡫x#Ro=Æ&IFC఩Zd˿t/܅tv wu]qI)&yyBfqBv!ꖘ'nFb&z5YDL_ :L /ÿS`aod@&tG3ҧHzC$^="܊F0)ۥheJ%NI16 65+ѷ=\ n.Ι$f#m6ie&BodhWfR@y_'NUl4`pޥLhg2Yxx*"u _u30ENg P mhΆqh<ҽ+0MVVcl6tJD;=; {\n܉f1:au"I$`ᑞE]gpβGٟgO ! t:O1s@X򡭃\(w+:_s.XtcVjXmx -x# _aC<ӭlxSvgjK۞egq+?@ p'>Uԇw6V%' 1;:_n:'e>E_(>~s;zO",T2] yقߔ(0g0Yi!dShhMM6ut) X_#fI9dNj-3Y'T!q 2cTErE8xgzg6܎e0tq)_Ȳ~Bc~EBBhp3 >Gi*wZ`ep`/R0le[q@ߔ$CsxMk-jؕUC 9Fhfb؍+4P?[ Cplgy/iv]LSnqRg!+`(7V-G?;Q8 00B64pM_a=i"}蘵7^ {^'m Hb2T{Akw70IKݦ2k8Z!ᗌؔ*O%>)V%V .}P`H8O0!$vh} lˏhYķ9MXLxGl<$mq9„˚bl_.C/Jb~~o~2Ch--V?/~ hwUo:Ca/ { VV"FDF!W&f[yz][l_sos.]{)ee<}yNSpWA(ub44)O?] wDV;ݱS6d< xWMΪ;>`ݱq. G1Y20棄L Z g؟A$1yHFg|/92"B bjG $cc$-8}%Y{oBlr.=IYF]r iܢ )#xy5d&g N8ҷf˓M3nZܴM[){C[,V0̈́w -kujӺǶ\kFMtM׀'6~Lـ:^x_HyeqLu%m#t>X ֯9rXH("䷅gM,oN0>*HaEtr+9{mj`^\Gj{#z_AO ߡR%~aL#a9CC?{fw>DVeA:Ut5\*\ikΕvvjXJCWz^iJCW:m۽Е^Vum+ ]ihŸfqE_>[Jg/Zgήtv t%@xSJG/ZGttsD>]iJC/\C{4tC 7rSi%ʻJ;92ˠwvv~VUz4tЕΣߜw^ ]4tM4tˡwήt~WU:zttѕxsVUz4tЕ4"/gx9љTZkӹX2$Zg-4FWt6)-HN8?H"fU8^+sZ37o=6;H\kS8&UUq֞hpϴt}C=dz!~d MɬȥUT޼`֏#ir>֧^/?{9I9u{OBVk0)F5Gd`w kPΧ߻4K{OEvI$$2Η ̬zkn _: JKWZҕ^>-u_D{F$v5)t#\}ӛIicW > r Qn6mۼ6]{eBۉ?9:/Ol(,Kb4ɬQ4DzT,Yq<1!F֡İ8++NJl8%_yysDzC{_pQ&nh;o}|,rEF9\bCOɈfsT\g;ѷ4$.,wM3 E2x35D֟F|+}."<_c}n՗i,0~WyZϭsD )]OxǹmANZy S= /b=y|\wϿɖۉ ׾/Yoҟ|t1?'P&:Ԯvhf| r= xMLяy ℴ/>u"'k,Z ļk/8z)/C/W;zK[/HI-_;K\ [=xYsKlwu= ܯX_X䤩'%HYa#ZF+\Do%Zc \4On~ijq/kiQׇ(IdGVݗ5TԶQ"ߝK9t{R؈Y W)Ղ e}ۨz#PIWd=cKXeMwڠ7vŏ/C=xy>4.F=NE WyrZHʜ>(smc {-8גz?3gב~~9c.U'ݍ{]f/;~NOYI]6< 'xShg/Ow MӞf@φ/[ЊݭIc2'}.lAI.6V-Dw.%e gvM/=7 Rn t~8= ȵGޥE.?4}nҟzkv!&{e^{;$z4fgG~m;]6Pq kU|pEr\lD@_I} 8b+Iz``P;{sIEpggKmA:[Bn9N%Udӛ>U ]EHC%)6*`tڦ7@>Enbo3Y(}s66NYG2[6MBr3rs5}&b٬ Q9P#eӲnu;[]dIٽO+}5L[{;YvPi"H4c{dф/{:mOyдk^~Fͯ;?l*7mq[~UAoP7N R%Npfk~I0 |` J/#nf-קtoCn;V4JwhZӆJY=ֲ=Q(S=b"# :q !A*9l|?gz Z(w!tPM"'u4X'Jt cVRhX!M5lC]1$#AmyWk4<;gSSɸ o~qkA f6W!x69{NTS4 )-uvlӪM69P4[Swf R2%2ZipѨyzx:LC$Xk6kכl2M 밎rZj)d o-?$c-τL_"む7|?MPd @ZlDƯ5Z nde#JPkm?(ʶbIf,rAB2 [fHdb+=?ZX :fԁf+_'i|(_R \}d F[8e4-7#+ 0/|JUw-7:]jrg/|T@GVM˧ہPթ7Y # ?4V=2c Ya3Q: 'O0t[݆E:\ьįhsīPZRqL)zBE&zBf-7A}iתv)&d1Zi nI%P/Ȕ/]KxnyDei-6/y(R7iub0tiVf?i4Gl{Po(zB1@ǯݮ '2i> ij %6 @Ff!\ ١4nU ٽ59PUkvyh 7f ub,/Axh"P<`4V^,᫱. d] U{ edojh@yHaSi1SjC A+dpr4*/]THTk6ؽdyMKgmZ"뽐E+JSf J,n( Ъt bG/x))/YEŬ!H_q| U '7SHķ&:\WIE"oqN+ 耝u ata:0-mdkxD]#1zÓWucƒ~>ڽ FbYGezlEXW2Iw-~'Y BRꇜDNU|Ƹ\7U:uYŤfGƙuE8;֛0eV. JEML}ݘ{[X CM} N3js垱+\ ˽{-7Q'Ismraۼ8u`=dh?&IG&R;Tm  ""xDl. v.tof_'?A@*GU?>A~Xr"Qڏs"}"o*O'C-uwA=R]tFLнO/$X[z̈]74x*֧@7329 ˾i*Ҁk:9>6R?ÓEhm:kOgrآ/]>~s~Y~1|}"3ΡOC[ȵޡ!b_!kĚ`~3L{u֏4 %ՙ<ŝWɵ54$z{YdiCVOoB[oMepu2vpO`ڳ!z,}+KR)SVe=S \gzk or#OE$\Q!r!e*G"r9*8%VJw?P<l2bwg ۓu/?};z+߶ݲbWfnl/ra)GX:kZk/fXyp??1Chx]kE+?_hi}o?'2?Q( ڼk/~k>l 3 t46?O 1N_dg⏾?Z+O_G?C3$;X+o?=CKjɅ&iQ3DZg-ފx4pKHItD/Z3v%黂b[4ݰx:oDzF"'b1/z< eD!o`G8DgudRh.UWOS#D'b-7QjjLZ!l2&u1ˑtdGh~ZٴeGZ61qQ^<'څ2S6@ƸG69f2T]rZ~ZQ&^UVkHa tn(!U˶>\V*6F wj> 5 4jFW8 H:C.AĻ)́+SP wQćWyZ>a%c ;X+*R5m!C!ʼn fp,9iPo}mGK.zK0ȜLk(NygLC( /a( itO#J8˹.ͥ2Br$ :PқBIAI)( @ɕPrs䦢t dTN)?y @ʓrH%J^!~/g5\-HJCw/M8rv qtCg{$ZyG̲ocIӺq%?VzzFRdusm{,_>i {F: AjUa_gɣ)Z{TB N=\e,PM]X,ѧ>߼ԅU1lu֣T v]-lF~ ijW珛ϫ&^(I;INgdI.T"j9fS˳ j*ܩ/MXMq7&TcEf5jSȝi ߄M/X֪w_,"k #<Ϟ&ϼ;ʹ=< 7(;%Pv(Z^ = 8ϻ::^i723w)ٓHo2iËweYP[zIW2z=vr*I.j1G ֏΂WGdOH*gˢl"\H啑+Ck5"Z}M^(nMՌ6Kx;g4qlPR%4Mg64rUnbq)K$~4)XnDFO#$ozZ|Bi;#vΈCZ &򋧔C&[!W}h84 GE4ܐp)4dGAiu,S{DD2,]ۤOyڿعM۱Gv=~|t֚=uw=4)+'Q %d7G=Q)'{N#b|=lt%>OStbW5WsPtKY?c:eFte:7¶b;r]sAy.XBm&A%JD떥u[]6=|ab\VÕU#ղx.^ή^.O\btd/ej˭ʉ%I:wEux&)⮳oPTb1)it5i8/jZ^e擭y,;2$證klML4YhKV6Su11pCTO'^I:xIXY$O,^Y$UuOa'KS=lQ;H+MeX\_T*#Ԝ/9+*ת%џUy5"g{yz9詞jeT?3qc?9?Hs?}s/Lh|K՚_ʻ <;=|2Kgw9?ˇ~J%],h> tֽnf)W5&sUkv=:Nⶨ6ZjyVжv=kۺ;J'vA;-OY/y~)*siy=}_oz"c=tV~hDiz`yFg&=#\Ypt^eAx 'kӽ˦7zfuq2_W+ҹwnd:{iFK\M,zgn|} }T<.8@j+ =ZDz\^r{ެuQ"^g%楬{cY_dJ>юGʳ 7X/\'\w庉z -K(P WlQe>^@m/X6ٞGB ҽp8M÷#V#|N'HUwʤ>b *O, G)]@R2< ^F7\c2r͒KNSG,Xhd= sBl(5sRҌso|{cEѲb [ja)ht*b&Fik O|QjGHMX12b=z-Wؓ)W14T\ ;i}_Ft9*1t2eI*![tR, 2'eqJ9?9=<%eY!nz[ѽ#K$//K6u .l`l&ɦ{x%[|zB<= {LrYJH/2z9-nEK\)X|>zggd˘wF71ԎQ2s8y(V}:5^Y8z^G !^%U }DD*+;F |'#߭`}\<8uNc+ͻ"b ֏Q~{q~Rb]刧>S ?ul>mx +'1V`OP(QVOq }(n5$ kPS$}ϏVCԷC\0+JṡYK[tu[_A =~%av'Yk[Fu#&]zİ;B΢+ }JWSMt >Z'zZ~g i=a%C'R"ѳ4ڍ~##՟''tJQ箞Qe691bї$Z(US;!N3M6]&ľVIIC]ci/_Ƙ[WW}q".#BWOW#xjpzؔ{Xg?}J\^z Nh~~ILVinDͻ2niL]k%:sK7Gsq޴>UGu~%o\K:Ry,G9^U^.zG))p5?)%8GN8v$u8k]%9K1pɷ~r߰w Do>Iu4N^dF Wª]1$K/v3mmo+yüפ8Zc}PFmxH%EnОךeQ\ Q Ih -z*rj+{iG@Yְ?´CcwMKPXrJpc%\H<~SܓRkRq>sjӮ57$Zmy5L\Hf˓˓ t-ן푟7[ݛk;'/Xlw~GVS/->rm [-6,Nݨg75FSS` YNX|eK\Tz0 dfSj6Q9语Juvo<^܄Bַ'o֦FQk0un]rz^5ڟmAp! w竈˔f0וXYw.͝)^zoܜmlM kw@&&Y6^ֶgxYa6kś;f3=igkkxwRBPzu@Cje6:ӓ&\\Bf㮇6tҋG.k/O(roYp 1 cnn d×-ݭIc2'6r$R.iK.}o2} /6i}tz&6uuu6Q:A~|a|g6\/w7xyB|M[rzg+Z|ԾlrSTӻ;(xNR2!lc&u.?턢|}}`$5j.{_@{ =q܋ai۴UmR9~a;]n>M/|a? b:c1ӈ阆cI4E%Aeѵku|~SOk6S:Zm=7C8?qg:Dk ǵ Fnp6tz>l<؁+WԊX;&ct&k {|&hZh-1F@Dc8xA`i9X/=?^]ai9o@4$j/[a9ٞ:|eh* ]*LszSjЧN˦TxENͷkeSe*[pӱ $x^A4k_Zl"u_0ךdu4d"Щ˚ly&D"]j q|iD9Fhzl"}He؍jBgDZ5;}!aPHñkA'{n˄GԹV9M X`6# lM"Hh2 Q&JϏVKNwZd C^3@FV~WwoZ~рF]\x4d2S[x4e2Įʥ LK&&cL_%B;GELubk 34j[e:H%Z͆ϊJܖϦJYw|7u[%"^O#t1t̅ @YX_}> {i|9WLlF߶Y;ڴ6X]I77ɟp,"[Cs\<dlcsH- ^@au~7g֪Uun:}\5za/4L).,Yd+:<X_etoեܗl4 v 7i}){R yK~'HiMrYO/60Zq ٞwBR8NMKtG*gSQjB:S&BwJ6WH 9 t2ƤK{OKszjAJ'w7w;)lچZ]\I_@6d~ ;놪5bosUG3ɾl]y1ljE߷=MWfevyÑUe 2ћV¢';'>)NuoQ;+jH`lrt t?|gg筏Zоg=U]'ڻp> :PQz1GmAOzsV,&Js!k`I1i~FHEyq7`poL9ŏI!zFp(c-~IUbE?4DS[=V fv.GlgT4ϓ߫LU?tȫף[T0<뻨 zN)LнHzXC-=oĮajk|_֧@7gle=-[^o x-BRځzQP='֧u!53~Bm5amkOgrJl yjLY?L+r1$:kCC2C׈5=c8B<i ZKܽKu|ݕ7.5\<.dҟW ZgC*XMEC痪R,z\\0.gzkG3o[佤}oo"Ns|"~x͑^ܱv.LÆ <@G?oTϰD %JA/;?AK~~?4CBן[+ӣ?ο1t&\hl2kݟJx5sp_h!O4> hc^>}w?o}N<){#y*"=BqBLiIRW_w޴SaD&rkEP%rP*]5-'s CTαBۀ2Ev!鰫ˮNNaН(N軲iT-y=NH{WŹ_gwQL_-#,!R߬}<1VNoS%'%q .%R %7k rH[RT(yP | %?%9+]bF4UBYDZm>d_+ YɸfL/q/㣏rBc[}ݼ/Q7nZR; .W:p=@S?7uaD|RV?Ǩ%]wYxxZKQjoTZ9&g"(^ؑIl/Q秞(UKOp߂s5|۝yd=uAB(u5#7=@zG,R+RW`o"S|ӑJړkzkRt2L(N>Z=vBl: C"߂J&J|9Ժ8.wɺ4Z(F ΋q~M*矓ϛҕ;CZΒ{çɻN;otnA:\{P*הk N׳)WF /{ݥ#GE,sc*TEf1NSriawZOOW Q:=-ln~i_iBU󪉗9JRN4qY~q Z²ڄC'wKc/jSv\M XarلeTrty7!jSy6De.ȣ7ϳ3γ2z3m?Ey${M}#N -eʮWeOC1󮎎lڦy`|Jd"䛹L%]e&^'界nܦJt3c`9Qk8Y+ٲ94H>aq:2uye$ipu?e2DMx_jS4ʩ[A5#R%Y"G谔j#$eә#\Xb\%%e.MJl$ہ^;oP#vΈ3b箉)P*VU2::N" GCr 74\m/hx /ّ$F}euKԢ%x(t??n6);e+vnv]<fkO`vw]]O:sojjJɅtzgp<rS kO:6ruTʉSH#$G'5]*gh=d{S@U )zzNx΍f9؎azkc+KG1+ֵPePRc>&Ѻe_]VM+y'G`pe:oH,K9燗뻗'׷4jm9r,#f$%rrb"cIR]Q]1IJA>-,!,rLdJk6]}5˫ڶWirٟdk 4dU^*7]%ۚώo4|%#!zk[M.5y.ڒ甪:j]L)OH>.0_CO/1/x̣HG1.(XzT^G|<ŕIvu=UwAnv*q=Bkږjܾ\V|F>B9X]WwUV9^)Ӊ%{2^'a/1h/ju/)fSc 5Tg{z>[ oS+V׫%:Ȣ#5egʵ*goIlU1y+SsWbіl>TO'yxgwپbR4S<c}eYF5$xy>cj}Anv*:zgy<2|yϢl"c*R:z_2܋9:a$_i#o׫nB~2NO!߃R]߽R`uu< Z9B"XL)QHkHrB~Ops1W!_-4>&|d|_9O %=G.(JBQ+WWz\^7r]jקzjIy)v״)YB,C`!֋)I,]hnbB*+[xPj[ Mg<ő±t5)N @xU< R*_2oe8 (QpP- O(Q 1ט\Ӕ|K82ٮ(zO$[+J <4{E(x}CvsvJ*56ڦ%ݻaS)_RS?VL}y^ Ut5t GU ;WNEZW,]t| EY҆mȖ2jḔ9K-eB%eY.eN?ONOon)ssIY*"en*e+ĭEtɩ˒MjølK-)X|e*!IA0^+G6OO_z \$K^N˨[>o~ 1e&(2Q:p%| uԯ 3>g41UgfgWQBȺuIGlB! Q9Iw+rxe39NXA*JXë¼c_s_ԩ/XWb9⩏Bꏴ2r[Ak b "BԮ硿ayGabc>?Guٺ rEX%JN}oGI#qRRPXפ1fUb}_%rKPmUUH8Ezx1b6>zOxz:}/qZN7Gp*d0Ga܀{7-p_77+?u$9W1rGRT*_aUşޑjJe5\ h'Oyʨ%X %2ߩ)ǚjӤ9syX.G[G(Ur=)qX+.Gm~O֜cQb90cgh AΖ=BV-)&z qi+NS"Т1C=yj\dzӔ>xyrǶ''NmĆǦkI5ȿYg{ZHC!\cԃSks\'O 9ѱ"u7A7޸9["\UngkMLkmm͵lwwv/;f3%Zۥw)-G/[7I-wH]gz҄Kl҆.\\zef/A*S6?:M̜91se-o<B8b3Nb3J) ͍Sx>IiRx^ (e0صsP p}1C:}Y1&F4VNFoiih*/W:Bбq{5G"0HP1@gp$DlRǼLtbLr~^M 6Om^91l䟒-#t%&aGТ6 s[:Niqb)?Fg8)`.yk^Z;/P Sԧ0=MSPs+p^w1ĉwtNc > Z6BcShl M)4M` Y5E#]*S)@<FszF.Eȥ!"ViGC>n޵9tmyZLi1(AY20n F:7+R&g&g;_n" 9I=˒6l9إ tcDk)r ;ͭp *;3ȊCSP(ţ6h&-y SdPP hP@*..f'Zt )^Ghdqau-/x=Ʊ;aSMl/ʓ ?_qZg)J 3oB#G({6HArX ğ02m HIǑQn-_/_! 6#s<1LHf0FK 1L5qJgJeNp"u ⫦_},U<aCv^פ R!CpT-2_fBvt|Ӌ;亮$t՘5Q˳"7c=⶛ QER$V4I.E;/p=|>%NI16 65+ѷ=\ n.Ι$f#m6ie&BodhWfR@y_'NUl4`pޥLhg2srhxx*"u _u30ENg P mhΆqh<ҽ+0MVVcl6tJD;=; {\n܉f1:au"I$`ᑞE]gpβGٟgӭܖydMu:O1s@X򡭃\(w+:_s.XtcVjXmx -x# _in{| V"VkMIy04NjB{<1Z }]Y1ԫ@nf6[)(B0 ǖ =Ny_tS<$g/>*B=WU[Qn qZ !~vlq$xa`0*4lhVp'ü{D:'81kooM/eGqBl/)UK|RJ.\n5P`H8O0!#Uo}S[VҦGo&'e/( YF]r iܢ )#xy5d&g N8ҷf䍒g8ķx(+nP9g3!e)"/Vr7ɏB6 K@(sYmsDy+艁;TY޷=i5#rQ"g|7CTZM=@K{a1w}]z0g(pOl ݇J,hA碊.Q+pTڹΕv6C#2WiJC/ZC+ ]iJC=vW Ъ4t+ п,=.Cg+]Elٕήtξt({_@@z*]Ehѕttȇ+ ]ikhЕ4t5Fs*ڹZyWi];GYv~3VUysΕv6oʻJC/VUy4ˠw4VU:{9tvٕ6oʻJG/VU:y"oʻJC/VUБހT 8::stu[3zRu: Gd밤+D-72c # otZ\x?;]i@Q~!T\;x}u?=2u>ԗIŋ)Hodt lϊ _Bam]W7$dhr>D!B$W2-1- #at:^mF𿍜t tcH K#>< u@yRr㯇L?ߏAy3s}TJ##Òқr _cqbt$Q.'R֋G/gҘ8 5~iP*}-&]ЈFt ,.a}Tt{WFzii.钄Z2<QoͭPCK\iJKWZ˧?nhH_#1X9#Q8unk~Ocz1)8M`0 Ǵa\N<ٍrmrtܦk]h;g [VgɃu=zE wɗY&5*HJ2גR=+݁O9=B1B=u:6qeXu /qNHqh/k. DӍ-bǺ-O^n#yY:(gkRl ;Lp* qClG9Ţ.6 y=b(9}YpqQo8ch1=osߴOEgpϭ2V2OKt(85> 87 Y =/D~x*|A}!PE'/7!r};1ڷ%+ 㙜@.D=VM،8ANp\{AV\o)1aQs‡ѸNuEKჿ7yE'|`݂\/p8od M}=鱱P>xo}wUs^QwULqg]==]S.XA2 ;V,X؀@,@aY2[6vH,EȌ̬̌)MgUf9'N_OvϽ~ՙ:{z|0}ϱߨEe΢?KVOs]{5>0NS'9hYaVg+l>^v6[ vatgǞ~t !zݟSok0Q۠IxgYU/UNB\Yn&g|.dܛ:ha5dt/'>| Oȓ?Kq\+( w}PRۋ?/!e<8*I?[${v W0_W -o 9&=<[dۦQ.$6cx,<jC뭟W|AIEz l@˳+P;DP䕆T#i!l WgaM]AFĪ f|'Yk_s[?wl^[yVAvY>4,Ff8sw 1ίC &[`Q82#+mj ~" m.^cV¦3XnWVz.7 Nm"D{~zk6 OJ;a]>=4K&g;4 N.9Fmhe{.hy.+[ps\Vw( ׂB+7wf7WF+"[Md##TcSevް.6eݴ&_FO? ݡ 4nVm`l ڱEȳ䲏/!3·LݥVD_6խ͵ް]Ȃ74Zo7[- -i,G4Z.4qW\1*+5z$?wɥė!- 0vin{[F.[M6č9h_[MutEL}/ F@6p&쯁o#hg=?ae.XbJtx# 7eРV DŽ;`5@0aWBnhsX"\kT2d*c vZ]l߶ӷwGOS[ + ^oAKImmB?naM\V[ o ST:s%W(^4^A6VPo6p4Fc[ oUAk G]Vs X.mpθoIʉx VKeHY q3p'Wo.gݐ@CW2h.{ 򒁷{F7x:m~` 1XǴ4eyʞPW &bip8>D`k',;zO>M6;.hx0\7Mon/!˄:tsÏUX$Py Q`"H_0ꕀJkOT O:`ѥRiYihL0E̬JK]Zj0̗J^2W6w`e܏O`  M#[td k ~g@u !2=ecY&oR!i@ }`MæX1[X}[ۆn {՗0 '6C{ncPy莘 %nK~|FwYC+0FUJ緍Xm6P5+5 ˕`\͕HoT.i\qEvզW^]oDI&y%1#./p0p+;(;=<ʅ_η@H-99d!K Jud%+KHA]lKvJ.ٱ\ZD:*M){,yr #pIqN .9cqK \ri&y5y܈?r9D! І yLdsęcz};,;E% X=9b3>߻z]O xPp~;Bf5ʈ̻K)ut47qcnjz/jHhۤ22ԉEuށ98<69 .ȯ ȓl K*OVU;з8 !y0f{ҴҵD,@:Yq i'#32((T: ,N:XMT$`M-U ;Z>P$yAhu-}_ &҉w$=ԇ1ڬ\i;EEuɲ/ɳf;v%tGeJovxk*Ys|}U> ϸ&_o0&_)K̋^{ja|P^]50yÛ.+lO cIJdYa哐]wnDzyQ7gOgOsd;I&3zy i ߩLOMy' EU ӚYYyt^4ǵ1Y:PR28Duu<ˣ7Sȫ'~O7lRBsl+!;9@vj90f6e}חn ˇa+{IÑ`810ƻHI-A=P0?j6eݷ H|;{-][yvI "w.$8;@#R:&Hue}fsM{*I A3+#^x0D+qIq)D;wN.#9gF'S׹Uf3Z̡Pxi5\?ƃ[2}k& %|tuҿ̭*G_W0M]>/xO"e\sfG=HWɣn%6=YҒte=ѱ(-][~$%Q- ![(1qxttJEeRGzm 򒲭។Oɦ[Y![#+pcVsyw񍂎NeA4*Ɯ(J*U쑪]P\cHuil⢲eGOgQ*d{UdhFFsb.;2{0%ˬYŽK/p NB~Iӳ o-NCAx"yw~Wiksi:Ɖ j,'Ql^.AkkAiaaD`l|R5|k_b؈CRB-9H`$L˝S ;K$[[u8&棳>͏gbA,}f+m!LAE@|kksϕ+z+V{허0=7/J۲BVvW`VI-v8#gvV$^Pv$^PNޗQ8S-aO8&3V+as ~!#vUtcuqx3: UKaÖp8p8 KG,RY4sII^x*lK@x*`"Y (I5Y)ꍂk A=ҝx bCvs|:%2mE[aRj^PJ8/=FG}}z29~ZcS͍7ddN*7Jh1y(jZ[ Z-2k.DˬTZ$)Zf3drt2kJ˒,8Zfc̖bTb j_nfuӞjt'ɗn.Mٵ|t(K$ӥ(tb=3fϙjL3t%Qs  C} I)@13t>!g@Ŝ|Ԭ3Oe+[!x :k83GP%{ :B4+Ao}`@WRf=x }+~@'ksKu܂wЧJK]9Ρ`{P;~ 0NTsoP:9!|#\ya*Ns sz@Cyxݴ/:+}A[3^ inX8A2o$3B(@H qR>+T -OGm({D&@4ih#0. @&:VklomZ--TG}OXo4F=Xgwٝ vgYA44V*tus Fcs~|}\\zV>@V>X[||SY\tat1j{Ml_ԗ-i (Yk4Jb@F2u}?U6j3*ӨM#?bܹCAh cc,yv\elLrB6ec!S ٘lc,x#At6y&+V}BgRS-WjűLE{^ZYRp]0d\ׇZ8֑"iNU5 תAj]S/;K,u٩9uצhpGC|pv 9O1HӘk-M*@`:2^Ut^ 9T]wT+éU,N̊)㨧.@p1t(MYX<&¡ 0E vrٲ * "*SJ\cZqZ%QmdP٫#Cgl4Dfq,OTT۳5ϩM38Z 10YIlB܀M~xcUxsxŎ'M~ G[p3QG_fu@Ba}@':ԧ=5rt<r7-&XF QbjXkةkQ5FM[{0؀dk>ÿi74#)8yTOT[HZ}伵i23 /]lfljǜ Nz+AvWw?OA;(``\ sC\c 2@< :~~$^7}z゚f${1w)Ow'@#PA`ʘ3X[6 ;A%dC`1i6I'R VO=IJ8qPO`J]j#Ȋ633رxsF9Tb; dS(wz+:,pE?h<-"awS>^ufMv)eG#l_# cYܐޏ3Ѐ k]Ov{ La9VfƍOz32**c2%sg(1lx$<{wzlΩ=U}eɖ߆8K$ j@-فC~iR{@) 7yά'K9c;5~ ls<=@ CDѓjN9>5xz w~H}i|Vg2'K=+zZE#>C1?#j_K31}}E½o'zEuxY=:j&N4 LV`h ?Va@ )6DG"}C&תW+ =_S uȯڮ}J&$~h}At縈G^KXUJtC?Eiz=g}ȼ5Y ^fOͻW2|h>lKQ㭽_?W>q*BD8=t`g;^˞_;?{}􍏿}=%yuw~?O|W?3-z"L絯;o׻?f6`Xf"(t_1xsq!>h;(}ҷQKw8?%gw>?OMD~:7֩7O~5Hs]Xh)bv/yqKꮠ 1zyT.}P^q68;}xJO'˫/Tg.Dx~W=O0BJ|:1: a;ʙ5ZiC^t$^,lIѸq^OiM}^a"lb3:T9Wb:8 J*n_@O.p6Ŋ؂0*36tfl]hy!b=q7oXTb;bw[6_1s-*IG-/ޟlUXS!>ఒYr\ yط0(AdmSy`w3ZOw1_m7Ыj7 =ɒ}י2KTvQv W˅_η@Lb\-r*C!ǡcX^KX\bďD.p6䳁pɎ"QlzNcɓSC>XW\r3<p&b9ѼRPqmnKJEq9bm@N3 \q㘞gQ+n{O|.wJꓺC[nA68B7z4WVB;C#ss$ ..Fdޕg<^`Աe?g,(ǵbNO3eP#o*.˄ϨaN|.U9LpG~SptoqBtaik_C2Y4t&8:_{EᓑJډkkU*u'&!6VvV)䎬m30I^Zw]Kz>A>ka ϸ&_o0&_)D /x8Au QBzuռ4i#o=VؚJ+<')͓z&gu"OBvݹ%E<=iUKa6yE^0+WW'Lkf>NwSg~O:dmyƼfX2BIz{9Pɺ,ތ[O# =jdK Q@~笳Øu۔]_TN%$nS~LlUvLv{NS۔&zM3 > Fm/#W G땸{EMqE pD[pDp,=+WEXmMZРjN&-)c#G 40ZФd8YQU>\#1,ыֹ~XĨ@j`{a_mCwvp }xyq$<i_ GÊa)`XJ)0,a0,% G+OB0"]&;( C{[ǗU۸O{tyҿt&m ŧ_<f*w`wm]Gͳ'5,ܹBv༇ Pw{ >"8cTXk[{N8ڤ= d0d=C >w CBR@sQ?s:yft8y~[j1îE1Vc<%cѷf"QrG'[7-ܪru= l$Rj>gvd9ރTp<^Rkhѓ(-IW[7`ORݢp ""GGd_Ti!uְ,/)ۚItleѬyT4^E_$Yl~$[$Mt,9/E[$-ړ-g̻z<_r2N-l9>ɗcKs.3_&/I˗/Ij<_2ww'0t#%Tg3_2ZΊ쑪'սp{pzydfTe):S3)9.3yTW|Oek{Zt]֋=YrNN&ጝp.!u޺l9sxwye^G o+sfc\,gT+Ej59^liT?{(Sd=Qx8ϔ\e˜8rNx= ׮Vv*&d;dwKĚeu9ˇ|w"Ʒ]`/h~> {]{)) ]ؾK podbfY2/?V⅊M%rǏ[c)kr/@vϡl3Y$_\"e?[|[g^o-OO!?Y|%IfWp,SW3Yq3YnN6(Ҟ6QX'7EE"@WEFfd?'##7 \z5]a"01}P|>!_$T>=ۛ4$[g+w7we9&ڻf=crb4m&Nφ'YG9`%!<-%ԒCh@ʴ9հ#O@k5xj]^ 9=_lb>:Sh~(2ЇhƻҮx<8)\Dͷ6?Wn~\r׹r{`~3?}r$9-+dՉmiu{8 fLO}n3Ry`>kE kGu D}[%q8el?c%θ:װ 8og[Kw9[7*Wgʨ> O_qX$v8l StzJ"ՑEC`>ǧS ,6ZԽ&)O-4ciztܧK'5&?ܸqC-N洺y#Q,y*G%ߐea-ZBJeIeV?O&O'٩,BeO;Vl)V{M .ɠ%jaX7Mw0|(ݴ/^0]G72t8;N2]H'3:)=jFKX־gWz-}nM¯:A~D;憅c!FL2# $pK/2Me"x!xHa D| OK31-O`VZ@*3P $łwt쁢l e,.aHeBUBu^T/U2 R*XaΪ`y~wL*.x. K{TGF0+;] 7VhBo¸, 9W\]TK7rk~0넶|..~8%C'ϏY 2D'q#%n7 < [~aiGabc~زUeĚEXԘx%[j~%:`˨8)*3Fb5joU^D{ɗA$_Gr.!zoq8p1b֕p=Rz8{%<DzNKq>=Z<KcXKgXE5Cn'.JәϗY8*zu #:2B5_sR12J֩<7T<^B3?oe`9D@Z:q%ܑ%[=ˢ K/|=y[팍4h>6*۵Ѷiu&pLrYTyfgKi^^GUm pZqTFh<6ZUt2:W{:!H+;35j/x#Yoj!0o6:vTp,.yACW^c jg4Y5U5JA+cY"!%Sf~e`ɿ77·ʽ;-|skgAf<@[0\C0&^µխ-$=|}k|7v>\SAr]Eh C5֐pYdwGH.Bqg}hh.1;imkBSx]N0Æ WM- 2*5X|DZ=fLc(F? hҶ BLrGpodgd/e>f 9]WA\\Lf[xilϣTs1H QlHاEt(SAZ_Y X mkBTWx흍8 FSHI!)$FRHnw HYx3ꇤsaaaaxIǏ'U{o_ھgW9 o'GW {>~Jlo߾)*/N\ϱov[iZ_ձaJΝ/:6O- 92b?Tlk%?_21B sY5>:>c=1Ow y^- ڶ,XzusM#גU]>H_yYv!ۉ_mi Rus]Xm_g)YY)m]y,m z1aaaxEߓGקo/Y\k6xjgH|yu.\aæM&wk#ϐ$?]Mo\Ⱦ,/ڥQ@~6s?)}, l gX #vQg Bٙ^uのuhm?}{].~}v_J;xogJY]޳@.)oqC?}>@Xߘ'-(W? źvƔOʙRv[K?[A}?-wmՑ}g\=c}M ggg DŽ-B^k_g?F? v0||؎=ǧHPgs/hؑI t~{n^}ZyD5XWvO)"c0vY Z|~_%/,p\ɹyΰZ/;/xs_9?Pܯ5ݻ\[y|č8gʱL{? 0 0 _k3>z_\S |<)b|7aaaxn.ta?l^Cvkؽ#~e)3<3^kdlc&jK+o"e<.ʞ`^(3zu l+6v<ï k7]/lc[`On}򚄫 G뎱zt^v2)?;Wmr5ocIz?Ozx{&!ez."ѯ 1Gg{+ҏlw<=}GݽFƨ^)zIpG K֜{{e G12ۭqiumf>.}~a? 0 0 [u+7Svq֭y΅ ?ނ}XwŶv?ߩDZۓ-q/?߳=<~#>Fk"qzrQo 9r,nY[;o:)@-`ק-7({߯S@µK9֠ɸ>:n3 _[_*mtcmC>qSL=<6;ǫsaaa{xˌ\ފpx?0׋#5zяc]x^l򼠕(f:~٣^lin59W~\;?vn6erUbS~v^U O7O(|;+SG4|?f*?rW~2oNٟS9~daևmH6mX[J~s.ym4ٶO|Bd/b5ɿyU? 0 0 0 0 0 0 0.P~*1@G\⟿KrKXs2(ߥ纎J8'>X@▼QQbqwx b)_K|v 1M6kee-2Ǜ59?K^E~9ϱQﱮYF8N?~;:=J<-tĒyNAgC \NXKs)'^Kg\~2}6}Գ)n]Or^j~"{p29w6/.z-v:+M{WJYZ굢`% Ҥl9ힶկ#OUz+U?;sd~vND7*.Y+v:ye;8}~|+ÑޅN9}{Bƞ#txխsXɿkSV/uJ=o G<ջL'L:D]6jfgLz/+ؽ[{rCMYq~[{yy czA;w9zszWHVax3 %mkBTx흍) q ĉ8D^>׻gI@XjjgiЃ`0 `0 ?ϟ|:seQ3|ӧO|:2|.};7eGFO6_Qv]T]^ˮg{>pjzkuo{yye?{-x/ D:3D&򈼹e^Hyi#/OGzϪ߯_~ :sMe#M3Y#=2 QЙ[\s=E8}E>GȩT ڲTg-}VfoSVwzV}./>~!?U1<#}=F[ ~QڋBN..+푹^edLo+[\-k dW(}6q$#?z6Bөi?L7!3O_Q}Пuo[=tkȋM!'}/Ƈdr2_Cﲨ: `0 :8o=+8-4}۞cĥXdq{bUq©ήm!ƶg*ΪU\z[GA=^+ru{LV U?)V>ғ)x|Yҁgi\yi^cUo*= !TY?rfgWsʽVn*VX#=Fϫ+[F~yH\L~[O҇h5ݵTow|Sfӟ+);F;:x )/OS yUo2e)Ve3'wgGg=J^`0  ľu kU,Ksؑ5nY,bXw{ w&3QהNQev ]ƷgcH˞i{A3I8hwduwUIWq8I>+@pQşGcZ\ƪUߝ]/:3d;ɫ:gB9R|GW~w2;fzt|+i5nΟgZY|<1NyŬ|E7k?z/k><=Α}N΅>uWydʬdz `0 *\?W8GY:Dgcg< 2+'W6qn؟{ru"wU쏘~c#T?+y{Q,,^qF/Xv8.֩g3}ȸOP ~n%hUG4(_sn|W}Tg&x^c,Fѭ+ <#+}/Uw8BRh_|33!mr\7U9m({ѝpvew[xG]߱?g;,nҽow8]וb?OV=Z_#ve?vN_WrYLo;1g9pV^G~>[_vNOS3 `0Q[ veO\k^8֔v<Zbz\Opbn$~}oz3ј mK vU]^iNWA#x딫jt q :E= z%օq)CcYEqyRG-+u (K\hP'*^ء^q=m=y|Kvūe\rȊ4={W1;=ݷxp;o@>ȘT\Ԏ+C=*ɫ|GJOCW]x1.ﵠ9_Eб Vq)v(ʑ}[GwǺ{-oSdו_˞׃2;iT&w*w:g׭SOsj%Z[~_˯d֮+w]7 `0]kIu+eL]ւoA^;=GR?v쯱;<y o$N1紈=:ߥPVu< <&3KyC/4r)i=*/|Ύ^]QNН1qGw>ù{ ?Kv:A}E:_n+{u=rq͓̳]>>d}+|L01`0 leg:׺񶊝`W,3O?]\9P~[kOWiGc~)-<w.3q}'vuw$Vnv(r52S;Wk_Kϔ8B/hEՠ'9w?K;x:x<|@cϽVyc@ۖSw8Bq]=2lBe6V}eR( VeZT4ade2ޒ+nYBTqSߔ<[&=f[|szP)G}{Zׅ3n7jpWwftEw[ǽ;`l? `0 `0 `{~i`oLy>uoi\qK|}7Svu9G쯿c¾#>,jow{ՆݲL=mW2u_8دjo?kD߱mw>#}E:OۡO;y`$jwymkBT6x횉m0]HI!)$FR?6c>>~sm+vuՑνYu8uN?WP>1JsWiV_uKEϸ/rˆ_gKW]ױEYcl,[TYHT}xL#}A GV7^}>iҞ-i;}LJX&TP3T#ߨgJl e'=?͘ona|7>?ǐU%;/mN/IfQփz{G}?v✽3X~j{zTAO^ʰ>?sy|G)PU{ ..T}6ڳ-F`p]k߅~b  О$wݓٱ|sCoA+q3lOx@(0a+? T,_7s\Ϙ^Bl1)C+k(FyN"8dPC_9>O0&l4Im+nwGrŰ)/tihf ѸX>E)<,6s45zb?J\<OM%O#(76:= ӋYAƒH Ls6MXBcX&ǘJte. 3.je(??Lj=%wZizFTx$kP8Em jAOހ>~؆B9 ֤8UKCvjbL Cy ;mj P. DkwUE€3ܨ8xUJs\ɟ+;}sFQ(KIXݛƨ 1 +KdX];Jģcx$D׷X`i @l̏rnm$^9΄zBGϞQ=nfkDe; <a>,⢞jk0B[p($Ǡp4 nq`XƓ vϵ.xHnorJ5Hu뇗 f a[Z:>36[g RL؍?( &w.7C#~B{] UW 71jk~ecGrD.=K@WDZM0倐0\xvqNZ ># BE )&yA}t?B Ym(WIpɱ |2+\2 )l8tl@Z.Be񅋍RSƃm>dIl'N adĢG3%#)?$s _5=YBR#-k"qGP-e"f%֩-ϓ378M9ϊ,_*n;HEBƱcl~ ˝[/sagIE2,z1t:kLș壋G){7ond{@rP>kwk׽ #kXfyEAB9uM4P=_lgW؇N#_nGpp ,ZUu6ȓVӰ0EK7*|]{75F\ԶzQz! uH>upT٣o3P)[^6` -d&*=%fY<^ط`_6|h3ء>2 Pq7ώ ,NsjF=B` 큳CiU)R鐏@LҮǧmb<2FHRqùFXi䎲OmGA}:*u f:@ʫRH.66jcGOpO- 6HKJU:Jǃv,3DZEƮqq7p?ȌK%ȧ$;?Qr6pP7`a^=R_)m>D3#£ _' Iɭu͋C-Rne㯄ssL<ȭ/R)|Lt_1Lk=rr 4/gEr~PnB[\g[{gYvRW' {Fem1{ wL;7&$xc0 n&u@5sCCձm8Heft x{q(aтa?Q%l4ςxmWI׆GC1kQ3iJh,KRO`ʲ4)%b6B8\pe;u)ko)#WSncRx{[sXv195_0Kՙ7>Tp5ٴl3S"؝LX睫[5m Q="u}pϘ*xbՉ#iM+@Z! Ϯ~jYݬ$?5mtu] %@݅:4h8ۃtu3; ΑO1A/r R*5i&j#Y2:$Z(ad@>'z L뇶6Z8|`6"X1_z' F-я?X^ A:?1;h/KVB' vOnFS ƤQ{=kh7MwXQp\v͓O/. N3HKRlK"q^Wh1wt h@3e6N|I;y?8t[[! $,ήLe"z%IކAkRl!3u8ځy?_W)AbCO!rza5Sn֗#<43y6"R߃CQ&>[# BHǽ{vekOTlq(UH͵h ݔ8,@tՂL{p/*L"d_y k,4 G̖bD>,.ok"D;|7[.DCA#ilϟI֬Dq]+eE _-- ڰc^Lq1~CCC9gNH8BkhJ#Z-`VoMa 9r$պZ-hkh ?C$ ^tď9d(8P݅]ڶw[wl;dn׆oKd Hބ(DInI M_(5)6H/Y1 QRk,nXHʉ?>df&6^EJmt{CCc`0ʅv5x<\9Yc}106"״!֏9dl:' 1H"z'7QqɌ#KR./CVgQȬ\ `?d1yuM6Ƶ8ZX]8^pwQE &1frRKi$GݜЕh3'{;;~FK37ku<pdʎ+C RMzƏ7)nҀ lEGyl:̑IoBS%|ЕsTulebA}Aʹ10A{KʘӺtjdLI=r PRg_LbR Şl?␔)![Fo wi&k^CV(t@pW2{hxHGRn͉eCbxԉ6GQd27\ثdS=\Ff*0ۣOP5(rZߙxQZ>~GAeN-jY7Ҿn;n?ӹ"Px}/NW:݊&׾:x" ꭥу;R펔 c䛅љElmG§a= h¨BG_uYnZ쫭FYs U"zM&:Gnu.DX5Xn;}ԫ%XO?~2&Frjj8 yA*W I9/ub)Zl: s 85J>~iI3Yԕ;:#hELם[ROd^GA˩f~Y!En0~/A Km>^WYq"<цF*c:xw|͞w%ehRgd9̕v3v Dgh>>?3hYDkgC(ʹƒԕSԜ| 2Q94(?OGQ34 fccPopTYaW(>@tX4`LGٞpɄaŰl\[9c26U M6f,'C4i?W~psϠ?kAKrŵk@I|>^xs?\`,D̒5W^w DMXf_8<%|8_왉pP1Wlm߃f?4:́_Ԕv M;k:p_sj؎qw]$F}y ,b'N=o0, ~M YR46+!}@~ujctCP.Y(x׎z?70WXFܣo3z0c8RGg0 TU򄽻w"/4֏CQ`[{Ocn]+{{ N!33+5]qpj' r9FDȬ)~: 9Gmx2-?sraG"yvUpa;Ră A\& ?#n 0eed~oq嶭!!DzP^H)>oȑ.ļԶ=Hy7S-M ?8ycߧq|#5"2Б lm#UeΤVbM͘jAc7Z ]> 4gb s 2WRsKg6 's8qzTT[R[w)I95xWj #!nN+zPڔ KgTE,?{^RDݥ=Ru^zîc&D'i74SJߔ&HUG[crͦ<׿~4}څh;lpAZ%XZ;tQ?yk1+Ƴu6[ Dc4Ɯ*dB#!}e>samhG3c^8u9󼵕⸈߂UyB;f "Yi=D =4&|C3g]~WgjhSIXU"1A5Fr4{AljwTt6</N \Rta| i>T.Wo>>xϯY{緷m,J{gg}v~)]s!?wXGFl!7U|Cnfﳅ:.@mq%臔Ru?.:aBֺE#Gg'yXDuSWNJD)21ѵVagWPqȒ s?¶@g")s\T{f3go^w:^"{d#!φt},nyWFKv„X4|VB~,˘_&fjp/WԍwaO H 3I`u1ͤ+_W|+_W|+_W|+_W|+_W|+_W|+_W|+_W|+_W|+_W|+_W|+_W|+_W|+_W|+_W|+_Wݚw)2iTXtXML:com.adobe.xmp Adobe Fireworks CS3 2007-10-06T21:59:47Z 2007-10-07T00:32:43Z image/png XIDATxo}<%RR,˲:"rT#@ZISN!0z@ ZI*=4&;N8-aQݙyz%M_H[0 w3Zgfn9g>ى/ ;%b$w .Ƙy~??׿|%[`d[V* |<ϳ,....?~_}SROR*)S?bsi>B A((Iyi^:=s~wAҺ6cˑƆX5bĘŨ%$,,˺W^}'|W_]ԖԑU9X8\esITeYTMZ1@̲,}WԩS%iEҪ`bvzOzLPi-jo^F(,몵 괖 Br?IԐTsv\U b괗kR}|FxGvJ*iYҚc/I\k cݺ4]/cǎ=ZA"TTDIQ]1j.D!o|㋒fTbBRME,*|*hf޺||ks=㏪8f1"um:vHrFU-t:)$ tw >{FӡS;nkiڔ$=ztNE$+*b1Y$.\8b!IꮯMH;^/^jwg$(Tv>֜B][miƊ& MLLhvnNz jl6'nܸPq̢q,i6ki/|銲}zv~~PU1JsW~(ΟO?YwըszXS!o>ڵ ysyVG}D9(φxιO W[_y=j5=vz`b$v[޹-ZSfI>|" c1;lՅw.PT3N{I(ꥶ_qdolT$U(Vg/^SQөGkn{vzM+T%!qVUT$P2v;^KBбOh `,/]Vk)$)<,*Jrt1UHnȔއ_,ht'&nk]w$BJ-WyT*0ȓ}7rmmM!:<7^k.m)`4,$)0:,;oH{ ] mNzo6C>Ƹ`$F).vƸ/V}|_䣱W~{noJ֛U5F,}ݺ!Igi޶(7鮫RI4=ݔsLNaGohb$Acc<vWl6UU7`~_P aWfSNnٯrpgMN+\i|2;;y$Ewo8S㪌U$5Ə11 o }+΀tkuwOzڕr>-\"9ij~hJ{/lR6tⓧthD1)ԻCv)ԜZZ&ɩVj^VkuI.vZm9yHSs:|rngJ{; ic!I㍪BUՒ|<|rreL,s{ g(ϕTySohnU+UQtS yy%[sK)\1%lzg8%IKeiq`TNur*$Nqo1U!6p)oI=~z`B, & `lHTup{^eij4]vاfN_nbX0!LbX0!LbX0!LO V4!w&Lbӊ s: `B, & `B,lm~X0!LbX0!LbX0!LbX0!LbX0!LbX0!LbX0!LbX0!LbX0!LbX0!LbX0!LbX0!LbK%a`cdX0^# & `B, & `B, & `B, & `B, & `B, & `B, & `B, & `B, & `B, & `B, & `B, & `B, & `B, & `B, & `B, & `B, & `B, & `B, & `B, & `B, & `B, & `B, & `B, & `B, & `B, & `B, & `B, & `B, & `B,Hbˇ ^F<ˢT"jE7wݛBN'K%k 2oZqdy/#d3PlOpEwa,\H䣏}yk|,x% #!_YYe9p.CESQ1xܹk"Uq/z INIRۋ .T~Pv~{{KEz"y~A*IM̞@TϽK~ӟ^WR G?O?2MbRs5f4>1'soQ~@娢=+*.-<9E9s?/\YsNƴxsNIRc:8'tЧTMʹ"n7˳g^0-Ik*l\H?uԑ澘$IEt+\QQq&$_|?~|k_;,iI/WϭIJc ꒚_җN<z49H'# ࣧ,IVwsg}Ch↤k.K iUR'ƘIR btѐ4%io~_>$ Z91Ƙyxٳg3ϼvڵ]UI*pYňb\[R7kSI $KtHҬC'O<O0|ްh+=bb lll>h //0 !@13m)eh ̊ ׀tw~?yP XX ؁)P#/P\ L:& 8#@_vvNϟ1rq10h$334$ӗ/ l@?_@,~8WT&# JkA}(ÇׯYG LFnn_@3`6H $4/AW1 Xǵ{ @li uNl +P13PXmk +d?/~>0= ^;.|Eh; !3>۷X7@ Q#K\PЈ$ׯ>|@FVF@CANRIENDB`././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/examples/demo/Applications/index.rst0000644000175100001730000000024700000000000025065 0ustar00runnerdocker00000000000000These examples demonstrate two simple applications built using TraitsUI - a Python source code browser, - a length measurement converter between various unit systems.././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1684768028.079807 traitsui-8.0.0/traitsui/examples/demo/Applications/tests/0000755000175100001730000000000000000000000024363 5ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/examples/demo/Applications/tests/test_converter.py0000644000175100001730000000344300000000000030007 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ This example demonstrates how to test a simple application using UI Tester. The GUI being tested is written in the demo under the same name (minus the preceding 'test') in the outer directory. """ import os import runpy import unittest from traitsui.testing.api import ( DisplayedText, KeyClick, KeySequence, MouseClick, UITester, ) #: Filename of the demo script FILENAME = "converter.py" #: Path of the demo script DEMO_PATH = os.path.join(os.path.dirname(__file__), "..", FILENAME) class TestConverter(unittest.TestCase): def test_converter(self): demo = runpy.run_path(DEMO_PATH)["popup"] tester = UITester() with tester.create_ui(demo) as ui: input_amount = tester.find_by_name(ui, "input_amount") output_amount = tester.find_by_name(ui, "output_amount") for _ in range(4): input_amount.perform(KeyClick("Backspace")) input_amount.perform(KeySequence("14.0")) self.assertEqual( output_amount.inspect(DisplayedText())[:4], "1.16", ) tester.find_by_id(ui, "Undo").perform(MouseClick()) self.assertEqual( output_amount.inspect(DisplayedText()), "1.0", ) # Run the test(s) unittest.TextTestRunner().run( unittest.TestLoader().loadTestsFromTestCase(TestConverter) ) ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1684768028.079807 traitsui-8.0.0/traitsui/examples/demo/Dynamic_Forms/0000755000175100001730000000000000000000000023325 5ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/examples/demo/Dynamic_Forms/dynamic_form_using_instances.py0000644000175100001730000001103400000000000031621 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Dynamic restructuring a user interface using Instance editor and Handler How to dynamically change the *structure* of a user interface (not merely which components are visible), depending on the value of another trait. This code sample shows a simple implementation of the dynamic restructuring of a View on the basis of some trait attribute's assigned value. The demo class "Person" has attributes that apply to all instances ('first_name', 'last_name', 'age') and a single attribute 'misc' referring to another object whose traits are specific to age group (AdultSpec for adults 18 and over, ChildSpec for children under 18). The 'misc' attribute is re-assigned to a new instance of the appropriate type when a change to 'age' crosses the range boundary. The multi-attribute instance assigned to 'misc' is edited by means of a single InstanceEditor, which is displayed in the 'custom' style so that the dynamic portion of the interface is displayed in a panel rather than a separate window. It is necessary to use a Handler because otherwise when the instance is updated dynamically, Traits UI will not detect the change and the UI will not be reconfigured. This is due to architectural limitations of the current version of Traits UI. Compare this to the simpler, but less powerful demo of *enabled_when*. """ from traits.api import HasTraits, Str, Range, Enum, Bool, Instance from traitsui.api import Item, Group, View, Handler, Label class Spec(HasTraits): """An empty class from which all age-specific trait list classes are derived. """ pass class ChildSpec(Spec): """Trait list for children (assigned to 'misc' for a Person when age < 18).""" legal_guardian = Str() school = Str() grade = Range(1, 12) traits_view = View('legal_guardian', 'school', 'grade') class AdultSpec(Spec): """Trait list for adults (assigned to 'misc' for a Person when age >= 18).""" marital_status = Enum('single', 'married', 'divorced', 'widowed') registered_voter = Bool() military_service = Bool() traits_view = View( 'marital_status', 'registered_voter', 'military_service' ) class PersonHandler(Handler): """Handler class to perform restructuring action when conditions are met. The restructuring consists of replacing a ChildSpec instance by an AdultSpec instance, or vice-versa. We would not need a handler to listen for a change in age, but we do need a Handler so that Traits UI will respond immediately to changes in the viewed Person, which require immediate restructuring of the UI. """ def object_age_changed(self, info): if (info.object.age >= 18) and ( not isinstance(info.object.misc, AdultSpec) ): info.object.misc = AdultSpec() elif (info.object.age < 18) and ( not isinstance(info.object.misc, ChildSpec) ): info.object.misc = ChildSpec() class Person(HasTraits): """Demo class for demonstrating dynamic interface restructuring.""" first_name = Str() last_name = Str() age = Range(0, 120) misc = Instance(Spec) # Interface for attributes that are always visible in interface: gen_group = Group( Item(name='first_name'), Item(name='last_name'), Item(name='age'), label='General Info', show_border=True, ) # Interface for attributes that depend on the value of 'age': spec_group = Group( Group(Item(name='misc', style='custom'), show_labels=False), label='Additional Info', show_border=True, ) # A simple View is enough as long as the right handler is specified: view = View( Group( gen_group, '10', Label("Using Instances and a Handler:"), '10', spec_group, ), title='Personal Information', buttons=['OK'], resizable=True, width=300, handler=PersonHandler(), ) # Create the demo: demo = Person( first_name="Samuel", last_name="Johnson", age=18, misc=AdultSpec() ) # Run the demo (if invoked from the command line): if __name__ == '__main__': demo.configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/examples/demo/Dynamic_Forms/dynamic_range_editor.py0000644000175100001730000001065500000000000030054 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Dynamic Range editor Demonstrates how to dynamically modify the low and high limits of a Range Trait. In the simple *Range Editor* example, we saw how to define a Range Trait, whose values were restricted to a fixed range. Here, we show how the limits of the range can be changed dynamically using the editor's *low_name* and *high_name* attributes. In this example, these range limits are set with sliders. In practice, the limits would often be calculated from other user input or model data. The demo is divided into three tabs: * A dynamic range using a simple slider. * A dynamic range using a large-range slider. * A dynamic range using a spinner. In each section of the demo, the top-most 'value' trait can have its range end points changed dynamically by modifying the 'low' and 'high' sliders below it. The large-range slider includes low-end and high-end arrows, which are used to step the visible range through the full range, when the latter is too large to be entirely visible. This demo also illustrates how the value, label formatting and label widths can also be specified if desired. """ # Imports: from traits.api import HasPrivateTraits, Float, Range, Int from traitsui.api import View, Group, Item, Label, RangeEditor class DynamicRangeEditor(HasPrivateTraits): """Defines an editor for dynamic ranges (i.e. ranges whose bounds can be changed at run time). """ # The value with the dynamic range: value = Float() # This determines the low end of the range: low = Range(0.0, 10.0, 0.0) # This determines the high end of the range: high = Range(20.0, 100.0, 20.0) # An integer value: int_value = Int() # This determines the low end of the integer range: int_low = Range(0, 10, 0) # This determines the high end of the range: int_high = Range(20, 100, 20) # Traits view definitions: traits_view = View( # Dynamic simple slider demo: Group( Item( 'value', editor=RangeEditor( low_name='low', high_name='high', format_str='%.1f', label_width=28, mode='auto', ), ), '_', Item('low'), Item('high'), '_', Label( 'Move the Low and High sliders to change the range of ' 'Value.' ), label='Simple Slider', ), # Dynamic large range slider demo: Group( Item( 'value', editor=RangeEditor( low_name='low', high_name='high', format_str='%.1f', label_width=28, mode='xslider', ), ), '_', Item('low'), Item('high'), '_', Label( 'Move the Low and High sliders to change the range of ' 'Value.' ), label='Large Range Slider', ), # Dynamic spinner demo: Group( Item( 'int_value', editor=RangeEditor( low=0, high=20, low_name='int_low', high_name='int_high', format_str='%d', is_float=False, label_width=28, mode='spinner', ), ), '_', Item('int_low'), Item('int_high'), '_', Label( 'Move the Low and High sliders to change the range of ' 'Value.' ), label='Spinner', ), title='Dynamic Range Editor Demonstration', buttons=['OK'], resizable=True, ) # Create the demo: demo = DynamicRangeEditor() # Run the demo (if invoked from the command line): if __name__ == '__main__': demo.configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/examples/demo/Dynamic_Forms/dynamic_selector.py0000644000175100001730000000673100000000000027232 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Dynamic changing a selection list, using Handler One way to dynamically change the list of values shown by an EnumEditor. This example demonstrates several useful Traits UI concepts. It dynamically changes the values which an EnumEditor presents to the user for selection. It does this with a custom *Handler* which is assigned to the view, listens for changes in a viewed trait, and changes the selection list accordingly. Various implementations of dynamic data retrieval are possible. This example shows how a Handler can interact with the traits in a view, separating model logic from the view implementation. Demo class *Address* has a simple set of attributes: *street_address*, *state* and *city*. The values of *state* and *city* are to be chosen from enumerated lists; however, the user does not want to see every city in the USA, but only those for the chosen state. Note that *city* is simply defined as a trait of type Str. By default, a Str would be displayed using a simple TextEditor, but in this view we explicitly specify that *city* should be displayed with an EnumEditor. The values that appear in the GUI's enumerated list are determined by the *cities* attribute of the view's handler, as specified in the EnumEditor's *name* parameter. """ from traits.api import HasTraits, Str, Enum, List from traitsui.api import View, Item, Handler, EnumEditor # Dictionary of defined states and cities. cities = { 'GA': ['Athens', 'Atlanta', 'Macon', 'Marietta', 'Savannah'], 'TX': ['Austin', 'Amarillo', 'Dallas', 'Houston', 'San Antonio', 'Waco'], 'OR': ['Albany', 'Eugene', 'Portland'], } class AddressHandler(Handler): """ Handler class to redefine the possible values of 'city' based on 'state'. This handler will be assigned to a view of an Address, and can listen and respond to changes in the viewed Address. """ # Current list of available cities: cities = List(Str) def object_state_changed(self, info): """ This method listens for a change in the *state* attribute of the object (Address) being viewed. When this listener method is called, *info.object* is a reference to the viewed object (Address). """ # Change the list of available cities self.cities = cities[info.object.state] # As default value, use the first city in the list: info.object.city = self.cities[0] class Address(HasTraits): """Demo class for demonstrating dynamic redefinition of valid trait values.""" street_address = Str() state = Enum(list(cities.keys())[0], list(cities.keys())) city = Str() view = View( Item(name='street_address'), Item(name='state'), Item( name='city', editor=EnumEditor(name='handler.cities'), ), title='Address Information', buttons=['OK'], resizable=True, handler=AddressHandler, ) # Create the demo: demo = Address(street_address="4743 Dudley Lane") # Run the demo (if invoked from the command line): if __name__ == '__main__': demo.configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/examples/demo/Dynamic_Forms/enabled_when.py0000644000175100001730000000636500000000000026324 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Dynamic enabling parts of a user interface using 'enabled_when' How to dynamically enable or disable components of a Traits UI view, depending on the value of another trait. The demo class "Person" has a set of attributes that apply to all instances ('first_name', 'last_name', 'age'), a set of attributes that apply only to children (Persons whose age is under 18), and a set of attributes that apply only to adults. As a Person's age changes, only the age-appropriate attributes will be enabled (available for editing). **Detail:** The optional *enabled_when* attribute of an Item or Group is a string containing a boolean expression (logical condition) indicating when this Item or Group will be enabled. The boolean expression is evaluated for the object being viewed, so that in this example, 'age' refers to the 'age' attribute of the Person being viewed. Compare this to very similar demo of *visible_when*. """ from traits.api import HasTraits, Str, Range, Bool, Enum from traitsui.api import Item, Group, View, Label class Person(HasTraits): """Example of enabling/disabling components of a user interface.""" # General traits: first_name = Str() last_name = Str() age = Range(0, 120) # Traits for children only: legal_guardian = Str() school = Str() grade = Range(1, 12) # Traits for adults only: marital_status = Enum('single', 'married', 'divorced', 'widowed') registered_voter = Bool(False) military_service = Bool(False) # Interface for attributes that are always visible in interface: gen_group = Group( Item(name='first_name'), Item(name='last_name'), Item(name='age'), label='General Info', show_border=True, ) # Interface for attributes of Persons under 18: child_group = Group( Item(name='legal_guardian'), Item(name='school'), Item(name='grade'), label='Additional Info for minors', show_border=True, enabled_when='age < 18', ) # Interface for attributes of Persons 18 and over: adult_group = Group( Item(name='marital_status'), Item(name='registered_voter'), Item(name='military_service'), '10', label='Additional Info for adults', show_border=True, enabled_when='age >= 18', ) # A simple View is sufficient, since the Group definitions do all the work: view = View( Group( gen_group, '10', Label("Using 'enabled_when':"), '10', child_group, adult_group, ), title='Personal Information', resizable=True, buttons=['OK'], ) # Create the demo: demo = Person(first_name="Samuel", last_name="Johnson", age=16) # Run the demo (if invoked from the command line): if __name__ == '__main__': demo.configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/examples/demo/Dynamic_Forms/index.rst0000644000175100001730000000007000000000000025163 0ustar00runnerdocker00000000000000Implementations of dynamic form behavior using TraitsUI ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1684768028.079807 traitsui-8.0.0/traitsui/examples/demo/Dynamic_Forms/tests/0000755000175100001730000000000000000000000024467 5ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/examples/demo/Dynamic_Forms/tests/test_visible_when.py0000644000175100001730000000400400000000000030554 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ This example demonstrates how to test the use of visible_when to make UI components visible or not The GUI being tested is written in the demo under the same name (minus the preceding 'test') in the outer directory. """ import os import runpy import unittest from traitsui.testing.api import IsVisible, UITester #: Filename of the demo script FILENAME = "visible_when.py" #: Path of the demo script DEMO_PATH = os.path.join(os.path.dirname(__file__), "..", FILENAME) class TestVisibleWhenDemo(unittest.TestCase): def test_visible_when_demo(self): demo = runpy.run_path(DEMO_PATH)["demo"] tester = UITester() with tester.create_ui(demo) as ui: # person should be 16 by default self.assertLess(demo.age, 18) marital_status_field = tester.find_by_name(ui, 'marital_status') legal_guardian_field = tester.find_by_name(ui, 'legal_guardian') self.assertFalse(marital_status_field.inspect(IsVisible())) self.assertTrue(legal_guardian_field.inspect(IsVisible())) # UITester is yet to support SimpleSpinEditor so we change the # value of the trait directly demo.age = 20 registered_voter_field = tester.find_by_name( ui, 'registered_voter' ) school_field = tester.find_by_name(ui, 'school') self.assertTrue(registered_voter_field.inspect(IsVisible())) self.assertFalse(school_field.inspect(IsVisible())) # Run the test(s) unittest.TextTestRunner().run( unittest.TestLoader().loadTestsFromTestCase(TestVisibleWhenDemo) ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/examples/demo/Dynamic_Forms/visible_when.py0000644000175100001730000000655700000000000026372 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Dynamic restructuring a user interface using 'visible_when' How to dynamically change which components of a Traits UI view are visible, depending on the value of another trait. The demo class "Person" has a set of attributes that apply to all instances ('first_name', 'last_name', 'age'), a set of attributes that apply only to children (Persons whose age is under 18), and a set of attributes that apply only to adults. As a Person's age changes, only the age-appropriate attributes will be visible. **Detail:** The optional *visible_when* attribute of an Item or Group is a string containing a boolean expression (logical condition) indicating when this Item or Group will be visible. The boolean expression is evaluated for the object being viewed, so that in this example, 'age' refers to the 'age' attribute of the Person being viewed. Compare this to very similar demo of *enabled_when*, and the visually similar, but structurally very different demo of *dynamic restructuring of a user interface using an Instance editor and a Handler*. """ from traits.api import HasTraits, Str, Range, Bool, Enum from traitsui.api import Item, Group, View, Label class Person(HasTraits): """Example of restructuring a user interface by controlling visibility.""" # General traits: first_name = Str() last_name = Str() age = Range(0, 120) # Traits for children only: legal_guardian = Str() school = Str() grade = Range(1, 12) # Traits for adults only: marital_status = Enum('single', 'married', 'divorced', 'widowed') registered_voter = Bool(False) military_service = Bool(False) # Interface for attributes that are always visible in interface: gen_group = Group( Item(name='first_name'), Item(name='last_name'), Item(name='age'), label='General Info', show_border=True, ) # Interface for attributes of Persons under 18: child_group = Group( Item(name='legal_guardian'), Item(name='school'), Item(name='grade'), label='Additional Info for minors', show_border=True, visible_when='age < 18', ) # Interface for attributes of Persons 18 and over: adult_group = Group( Item(name='marital_status'), Item(name='registered_voter'), Item(name='military_service'), label='Additional Info for adults', show_border=True, visible_when='age >= 18', ) # A simple View is sufficient, since the Group definitions do all the work: view = View( Group( gen_group, '10', Label("Using 'visible_when':"), '10', child_group, adult_group, ), title='Personal Information', resizable=True, buttons=['OK'], ) # Create the demo: demo = Person(first_name="Samuel", last_name="Johnson", age=16) # Run the demo (if invoked from the command line): if __name__ == '__main__': demo.configure_traits() ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1684768028.079807 traitsui-8.0.0/traitsui/examples/demo/Extras/0000755000175100001730000000000000000000000022041 5ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/examples/demo/Extras/Image_editor_demo.py0000644000175100001730000000536500000000000026020 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ A simple demonstration of how to use the ImageEditor to add a graphic element to a Traits UI View. This example needs NumPy to show an example of a dynamically created image. """ # Imports: from os.path import join, dirname import numpy as np from pyface.api import ArrayImage, Image, ImageResource from traits.api import HasTraits, Str from traitsui.api import View, VGroup, Item, ImageEditor # Constants: # The images folder is in the same folder as this file: search_path = [dirname(__file__)] # Define the demo class: class Employee(HasTraits): # Define the traits: name = Str() dept = Str() email = Str() picture = Image() gradient = Image() # Define the view: view = View( VGroup( VGroup( Item( 'name', show_label=False, editor=ImageEditor( image=ImageResource('info', search_path=search_path) ), ) ), VGroup( Item('name'), Item('dept'), Item('email'), Item( 'picture', editor=ImageEditor( scale=True, preserve_aspect_ratio=True, allow_upscaling=True, ), springy=True, ), Item( 'gradient', editor=ImageEditor( scale=True, preserve_aspect_ratio=True, allow_upscaling=True, ), springy=True, ), ), ), resizable=True, ) # generate a 2D NumPy array of RGB values gradient = np.empty(shape=(256, 256, 3), dtype='uint8') gradient[:, :, 0] = np.arange(256).reshape(256, 1) gradient[:, :, 1] = np.arange(256).reshape(1, 256) gradient[:, :, 2] = np.arange(255, -1, -1).reshape(1, 256) # Create the demo: popup = Employee( name='William Murchison', dept='Receiving', email='wmurchison@acme.com', picture=ImageResource('python-logo', search_path=search_path), gradient=ArrayImage(data=gradient), ) # Run the demo (if invoked form the command line): if __name__ == '__main__': popup.configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/examples/demo/Extras/LED_display.py0000644000175100001730000000670300000000000024552 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ This demo illustrates use of the LEDEditor for displaying numeric values using a simulated LED display control. """ from threading import Thread from time import sleep from traits.api import HasTraits, Instance, Int, Bool, Float from traitsui.api import View, Item, HGroup, Handler, UIInfo, spring from traits.etsconfig.api import ETSConfig if ETSConfig.toolkit == 'wx': from traitsui.wx.extra.led_editor import LEDEditor else: from traitsui.qt.extra.led_editor import LEDEditor # Handler class for the LEDDemo class view: class LEDDemoHandler(Handler): # The UIInfo object associated with the UI: info = Instance(UIInfo) # Is the demo currently running: running = Bool(True) # Is the thread still alive? alive = Bool(True) def init(self, info): self.info = info Thread(target=self._update_counter).start() return True def closed(self, info, is_ok): self.running = False while self.alive: sleep(0.05) def _update_counter(self): while self.running: self.info.object.counter1 += 1 self.info.object.counter2 += 0.001 sleep(0.01) self.alive = False # The main demo class: class LEDDemo(HasTraits): # A counter to display: counter1 = Int() # A floating point value to display: counter2 = Float() # The traits view: view = View( Item( 'counter1', label='Left aligned', editor=LEDEditor(alignment='left'), ), Item( 'counter1', label='Center aligned', editor=LEDEditor(alignment='center'), ), Item( 'counter1', label='Right aligned', editor=LEDEditor(), # default = 'right' aligned ), Item( 'counter2', label='Float value', editor=LEDEditor(format_str='%.3f'), ), '_', HGroup( Item( 'counter1', label='Left', height=-40, width=120, editor=LEDEditor(alignment='left'), ), spring, Item( 'counter1', label='Center', height=-40, width=120, editor=LEDEditor(alignment='center'), ), spring, Item( 'counter1', label='Right', height=-40, width=120, editor=LEDEditor(), # default = 'right' aligned ), spring, Item( 'counter2', label='Float', height=-40, width=120, editor=LEDEditor(format_str='%.3f'), ), ), title='LED Editor Demo', buttons=['OK'], handler=LEDDemoHandler, ) # Create the demo: demo = LEDDemo() # Run the demo (if invoked from the command line): if __name__ == '__main__': demo.configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/examples/demo/Extras/Tree_editor_with_TreeNodeRenderer.py0000644000175100001730000001246200000000000031174 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ This demo illustrates use of TreeNodeRenderers for displaying more complex contents inside the cells of a TreeEditor. To run this demonstration successfully, you must have **NumPy** (``numpy``) installed. """ from random import choice, uniform import colorsys import numpy as np from pyface.qt import QtCore, QtGui, qt_api from traits.api import Array, Float, HasTraits, Instance, Int, List, Str from traitsui.api import TreeEditor, TreeNode, UItem, View, RGBColor from traitsui.tree_node_renderer import AbstractTreeNodeRenderer from traitsui.qt.tree_editor import WordWrapRenderer class MyDataElement(HasTraits): """A node in a tree of data.""" #: Some text to display. text = Str() #: A random walk to plot. data = Array #: A color to show as an icon. color = RGBColor def _data_default(self): return np.random.standard_normal((1000,)).cumsum() def _color_default(self): return colorsys.hsv_to_rgb(uniform(0.0, 1.0), 1.0, 1.0) class MyData(HasTraits): """The root node for a tree of data.""" #: The name of the root node. name = Str('Rooty McRootface') #: The elements contained in the root node. elements = List(Instance(MyDataElement)) def _elements_default(self): DATA_ELEMENTS = ( 'I live on\nmultiple\nlines!', 'Foo\nBar', 'Baz', 'Qux', 'z ' * 20, __doc__, ) return [MyDataElement(text=choice(DATA_ELEMENTS)) for _ in range(5)] class SparklineRenderer(AbstractTreeNodeRenderer): """Renderer that draws sparklines into a cell.""" #: This renderer handles all rendering. handles_all = True #: This renderer handles text rendering (there is none). handles_text = True #: The scale for y-values. y_scale = Float(1.0) #: The extra border applied by Qt internally # XXX get this dynamically from Qt? How? extra_space = Int(8) def paint(self, editor, node, column, object, paint_context): painter, option, index = paint_context data = self.get_data(object) xs = ( np.linspace(0, option.rect.width(), len(data)) + option.rect.left() ) ys = (data.max() - data) / self.y_scale + option.rect.top() height = option.rect.height() plot_height = ys.ptp() extra = height - plot_height if bool(option.displayAlignment & QtCore.Qt.AlignmentFlag.AlignVCenter): ys += extra / 2 elif bool(option.displayAlignment & QtCore.Qt.Bottom): ys += extra if bool(option.state & QtGui.QStyle.StateFlag.State_Selected): painter.fillRect(option.rect, option.palette.highlight()) points = [QtCore.QPointF(x, y) for x, y in zip(xs, ys)] old_pen = painter.pen() if bool(option.state & QtGui.QStyle.StateFlag.State_Selected): painter.setPen(QtGui.QPen(option.palette.highlightedText(), 0)) try: if qt_api.startswith('pyside'): painter.drawPolyline(points) else: painter.drawPolyline(*points) finally: painter.setPen(old_pen) return None def get_data(self, object): return object.data def size(self, editor, node, column, object, size_context): data = self.get_data(object) return (100, data.ptp() / self.y_scale + self.extra_space) class SparklineTreeNode(TreeNode): """A TreeNode that renders sparklines in column index 1""" #: static instance of SparklineRenderer #: (it has no state, so this is fine) sparkline_renderer = SparklineRenderer() #: static instance of WordWrapRenderer #: (it has no state, so this is fine) word_wrap_renderer = WordWrapRenderer() def get_renderer(self, object, column=0): if column == 1: return self.sparkline_renderer else: return self.word_wrap_renderer def get_icon(self, object, is_expanded): return object.color class SparklineTreeView(HasTraits): """Class that views the data with sparklines.""" #: The root of the tree. root = Instance(MyData, args=()) traits_view = View( UItem( 'root', editor=TreeEditor( nodes=[ TreeNode( node_for=[MyData], children='elements', label='name', ), SparklineTreeNode( node_for=[MyDataElement], auto_open=True, label='text', ), ], column_headers=["The Tree View", "The Sparklines"], hide_root=False, editable=False, ), ), resizable=True, width=400, height=300, ) if __name__ == '__main__': SparklineTreeView().configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/examples/demo/Extras/animated_GIF.py0000644000175100001730000000344500000000000024670 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ This demo shows you how to use animated GIF files in a traits user interface. Note that this demo only works with wx backend. """ from os.path import join, dirname, abspath from traits.api import HasTraits, File, Bool from traitsui.api import View, VGroup, HGroup, Item, EnumEditor from traitsui.wx.animated_gif_editor import AnimatedGIFEditor # Some sample animated GIF files: base_path = join(dirname(__file__), 'images') files = [ abspath(join(base_path, 'logo_64x64.gif')), abspath(join(base_path, 'logo_48x48.gif')), abspath(join(base_path, 'logo_32x32.gif')), ] class AnimatedGIFDemo(HasTraits): # The animated GIF file to display: gif_file = File(files[0]) # Is the animation playing or not? playing = Bool(True) # The traits view: view = View( VGroup( HGroup( Item( 'gif_file', editor=AnimatedGIFEditor(playing='playing'), show_label=False, ), Item('playing'), ), '_', Item( 'gif_file', label='GIF File', editor=EnumEditor(values=files) ), ), title='Animated GIF Demo', buttons=['OK'], ) # Create the demo: demo = AnimatedGIFDemo() # Run the demo (if invoked from the command line): if __name__ == '__main__': demo.configure_traits() ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1684768028.079807 traitsui-8.0.0/traitsui/examples/demo/Extras/images/0000755000175100001730000000000000000000000023306 5ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/examples/demo/Extras/images/info.png0000644000175100001730000002104700000000000024753 0ustar00runnerdocker00000000000000PNG  IHDRuuxF2zTXtDescriptionx /MHMQHTKWp/J,L.VIM k IDATxyTՙ?B44l FF 3L4&&1l31ƘQ!qhb&FTDQhLA6WYꪺۙ? *b穧o{sG|26PGZ&mOQ1E_JM] Д)ImU'S} ~k9:B Wu(IYuH/=^non|/םM*sؾ/H`k/pϭ[dmXM3NwX ]9V^Q5(p}]M *AUUp=?(cgx>7MS!N<^&-}R>bF `'sS?LW/M#Z#ϻ5/ĸY8+%.F&m=m8iZٰM93ntaQ;חbGq,(}34U}N{3lȸIY.ez 2&=G.:$θ{f@aRJM,xaU?j-. yOp r:SNKiuKPy>k7QX4m" |۳-BU\קh7w9!h{VA;9lTLm)%m=ZLUQ,Pt3_5S!py )TB|s#eihn;4Ҩ읦$c1Z:e0dp9&;s(u/I`+HZZȄ#5nt^y ,!eD̯&48dJLNfaԄ;)џ\ilWhLf̘_ amXe9^~5@'_r/T9\eh|AZĶB1oP-G}]-TTd+9).ʌsQc/d2iWrՏCļw B 3oտ [+_|-;z㧑4Δs YUtbSٱ'{muVvnDޕwElz\ܮm;/bm:ҙ8=}| :}#@S'㘁eGd44Y~/Nꨪ|eWM3%iR+䜋d~Ipg'͞}zSc@J3&v5a[NQ6ؽ7ejzض/%/i45dR`5i$4,SOEtRR,z@I0)/³K]59I2 _Ġїy0tUr}ޮΒ/z8K.BH 4|JF3׮`ͪ%\q,_uOΪ$2L\tYKמUmJs _ 5{^ڼ49ιjy/}qr^J\S =/u[m(Z#a#L'sni6+Oc;^wp /랦zv^{^*oPغy'qEof@ 22qQU4MzJ&tqm]שŧ~57=݆úg푒z/n{.ǍߞBK/CWcI*el֕!rL%t*PUB)RI(})lsϓ(t] xE@`{ -̟u=R1I'XU$|1tcϾe5(OЯW MShɻxl5g&,ϗ #]STIG@Lmۣ`*Z(\sa{D6a?κ|_"{QU,C=/~%"NHP@q||?XL93' GT4(]w;8<$XͿ+AJ߸xyUAQE@6?Ӎn COUUB=H@6,эw4MSHA:e`]SdŚ׍)$L(Ry;E b92OVۗH)Cٮ. )F&mУ̢$cbG7IN|zg56H&t4557b%p*LĞ܏^[@  |)C®m+ykٝ1ޢ6e'"W,1 -@m Mx-AF2cBH0mE&HǝEI naIyw=سv6Bm a(55O >*_W4U!{,=L`ߦR></b؞#&PU+x^0Z }dMS?GŠ1g4!:Kh-ڵf,TE'PZu&7Dggše/i c KUDOHWK'u% hTEAS4-hw/%LS?{+RJt]c϶ G.eob oTJ\'Ndδkiq\5BA/aδkIfzWA6rgoCJɰO|9ӮR{գ - ys]A6 9>Uy49nV\L6gcdmZrLl,~|v<"ohV*aiN +A[q\|xTN9kȅ =n<<ry 6h2-.[#@ӂ:َ ]+:|y뗣**t2W*Iuf/^/_˹݁*󝟿=6,y8G﹆Wz o@yIrw غyC`ơ2z8NI&{JXAI|' uɤtL@Q Qlw]/PΗXr1BvoW?@&cYftXy,OrQ`@U *3Z%X޺vih{heJȷC 8 6/̼N$-Y%6Vi {'۶._%f" M\קԢoєkc;>ċugK ؎+$,-Ӳ |45|x45]U@QgA("4># )8Q@WiHcHjVq\_yl.O )Ҵe$7ENj'oIIyiϨ(YĤ$eP^fA2tui},C%:gF ÞEGtRǗ{J8z@ Uċdܞp* .!TlApC<E@ٵF.@%} CÏbV@JEza\L`qkjjE4Zv/kHb S R)#R۩D%6f_=KXZh.X!ڳul)x w& 6/YwE|,ļD].!DZRt>)l*R& D$2BĦj~TVG,Fg,wUC]Q4&;8QCyG?5D2_%pR`GS¸_>V"nt)'A7X |TU=ntF-򜎢B5 #4쎄1 &&G"qKG/D@k[~y/uCHtn£8 N-`AxT I  Cs[Z7H)zW)76p?|c(4-_!iֺq?Nm,;x , \J蒽}_I y|K)%vlXܘ@L;Zc]Qt5)?.2UleH ͤ  +%%>xKyұmǩc;nBU1Z4JxmŮ  ̑Q_v6>?Uļ78bVdf*'?Fa%KPX̳dT3O>Ɯ { .v^x;_C( dbHASFgMm/Wq :3jSdׄc*;X9FQtD=%Gv׫NUHȳ~Q/Kska\v%,JplA{4]UÃ<$XV,b͛s)oӴkov׫( wPsNJKػ{HwAgn@hOxmP~Udʫ{0^־"*ذq CE}8cGQw>׀CF`؍"= 9QU-"($:oο8xU'^?9).o38zXV̟1<]$wH{q@#JST~twX͛0+=/a)xp;}s?S0j̩Z93nB^ڶCS&/ރ@ܣ7X(ǫcRfuZjA;ͽtJD SQAnh9hH)_ްa< ؎ۡu{oHj<Ƕ-ى(lE쩦Ύm54P bѡXz[î+|fsj^Sy3?A C[ulfe|))\.G>_vV6Gߑp0>$X4{q[kV>u{;uɵ8qA+<)Tv/ի}ٲ;n:uyr<uuٽ!1&;OhST@I8 aOҜmBo|@m}%jk] kqwlYvd~oi)}T{g8Z|-v *PT2L]m?n§'VM d;7-zó(j j,T눞EѢSG*ul=*0`P,xk;,~‡8k—٫_lݼ^}ӫw_Ȃ?,*xWqO3椳sY<m.b*ޘθ/%JSZ֓{9(Xh:2~io|#D5ijkv" 6dM0pIJ񬷯 fev}ɟћehmDz` t4}}cYv9r0pX)İѾ|KLGQGZh:gM={w:^Qq_d*Dԉ5Xt~=Vc*o[zhkjf<f G ZK'28FSc]86hm@hi[dwS ,C -)6n254Se14YesQJEC3d˦uhF )=,||Q"ن]xRcǦ6a*+;Tz I֠TU }qݕc ,#^1)-1)ɘSɄߴ(tV[j+X){UQ[ V`DҏOG2BçoeАѬ_))e .ڜH>*jxm2)R' )dRFI m{Q ]W%.v<7 ضGvq?VkbB% q_);M'IN0Ab)*z-M62$dk(0b[[VpQ"JlyE%WkHzH9MC /V]Rv._J|O25Z@ikZ"p/@F^i]S,-IDATv+ m,J\70C.`Ra1`ZpPNU%a F S(i=UzW[ _)9p6Q=2ا*"m}w/ʲx{/~u0 pgu(V[ҽttx(BhEJ ] Nv(kXAq~ ( HB5CWH&t|-p)ڦ0^p[߃fjrV߇wKC;p87Tpݲ> ]1@viG+>)_*!#r::T)AW$"0uhsr :7|rItFcF{(w**y<84Gw%{@wnt>%xIENDB`././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/examples/demo/Extras/images/logo_32x32.gif0000644000175100001730000005603300000000000025605 0ustar00runnerdocker00000000000000GIF89a Sm%dkq[ m/[ϏW;yxu@\G瑦;?_r&}XسtyF-]ʑͧQaQBFͬ;3l3#,{4 #W5<؂Pj3,+R7@v@ ~&8t,<ʺ$H`~w̄>$@E6<=R@pLIuyφR6 ɌX8kх)&#Z6)"#,#= 1R F)&16I8 >IiY 2bā2Elh B YrҰP7Ҥ=H3-~e h9}5C-k96M4ډ%kQ,8BmVj*ATE(>PMRVP8Ύ5iM a̹ ˊZũm֩ǡUAP͹톊94"ʶy1"2Sۏ(1:-;龜7&?=*)`@wƘ B՗=k\c# I `p  lCѧBk0 l $H` 1hL'  d .#> F#K-u% QAw-d %CqP@! , HPb\"8財!CEHQʙ jլ{0A1"mfjC|lL @SdFQi$ZTdͷ)ek!~ȺtAkFЪRE. JKA3%-r21^ف8ʌ&m Bm"uʇ eq'6UTcqWҜ]‡'hw{LDY94<@O"0jJ*E#u+gb_zb#@f]h#ĶAF9t OElHQx3 @F W^R̎ʴnVb 2:ĚͮT.Am^zr 99 &8|;>cM.܇V[FB_c]>pA.4[)Á* 8 H#>D!a$XBP6s*J#\X`&".eQ x@M2ݳBaC])XcY֥sAd}(5Y_@! , Hp"E,xPb0{ Cml\l=YA(I = Mܻ'mKҐLԤ)ͦ ?}q)SԢmHqQ@T#4\Ȉ@d]2vt;jv|xˇ(LpFe)18X\vӦgsҧkʺ"ԁ`8ͮ "fܘ6+g{(ACwZ=΁kdt,x~!ayp1zckq$ `!<~ H.a")LzHq̓>cL e1W% QxPv$pe! , `AF ;# ^#cT@体M͚ZH$)RH)3&)I!Т$eHfS#A؈6mB&M;F<]9,# 'ce*R֥ba4(t|R 5,@L. 샳=N>Pum`o;mUA(_V6Sed`\ }N] #ճ3ZU0MYeԨ͓;oޓ]><1IäX\CqSl( Z53caQȊ>hlAZSGUZjY]k-;g#*4߿1*C g~z@9?uCױcvf cyG#Ϲvk : ! , )b#m*TCmk6ɛ IA2 RޛJE%fM'S6i2 b(֢?,d3C8.j3i)}4ï~ $^Cc~mHY.Ȳ`Uj+ك(y#)Ʉ@eg1ؼY_bMi˖1Ԗ&wmK6+SADcC].l[|"KfWt3UZĉWaN 3F l*"A\ȃsĽdHpO10tO*dC*\#O7 =96Se! , Ttm()R$ÆQ fBʾ6A\HI=kticGEq,&r2R ? [J6"M.:jc̵8!`TR!MFh42dҐ" uM_4 CxXaKsMݓ.h8e2 .?^S5.pَ\atu!T<4tY WVE "J֍aڇN}4*81&{bBjcGY P!5S @>IF\̽1=Ia0R~HONP I&I#0G+p8T([EP@! , 2RlNF&.E R9" ( {zL iE[ ɱD4'g5א(AhP,ȗrἷsV?A5i N-W(Nx%( #ءUc C&M@ f^aRNHCEpƘ x_nZ.hÁ50n߇. ">ZT B^iTU@23c8:.FAXP\Ao,rF$8_>UK4 `{/姇Iec?a}DEЀ6^xԦi1\ň ЯBH'/FQ܃A X|аMJCe9U.-K k24HK)EEb+CBQQ)A! , (PQF*\HP w{HȰC0)R`bE d÷s:~#5bh5" irA$$143͆Guj"Z$ݛ%(EEȝQȇT EŻYLɇy;Qi0sGOۿ(p>W>Ԧ@4`R C k.t;P5.Ьgd !PQh9R;LC.*$V#t, ]hr2ƙj&bM#FH)䠗3b8iK@8_xcZ{J G NcL?` M[1i4`2Y | 3!&bAŵ7 x"X+ I|2~! , HPQ*\P w\$dHQk`R0,Ċ &M#>hLJbXJp @V.&{PZBdLL fG4XygZd:kV?# j3|ij"FRlb6AB8j8 5Wt'.TI0ax FӦ`2R$*!`g2$B,Y> )Cm.]PUNVa[,6TiՈ9Xdl!x1ůT4[6!`D9h' eqO h~rҐsUPeӀl*3-ipx"T1~pp=Fx#sHA/F^! , H🢃*̾6p-)ܝ8$a?чD$VB@LDh0Բl5#GFITBMq,Hdk BfGFD͌aLf1iP]Μ>)Q.Y20Hħ>CvDEZ aT<UX e66Wcy5s sNzd;ti tԁApҡȻ "ۣ1MFsYV Bz 󅊮",Q^NP{H(S.@:v.`<BDM 4H=k` M9YRܓ? 3`A;gH564G?< u! , Hp"E*ȆfJ NL-i)0 n@bV$Gm S0Pۙ/El TJfBkՒ`+S}"T>F0CVC@ ,&ƁSj25Afz#vpf*9  wn]EQuYU#Ft#-&2!ڧtVn2i ᘣM&ğΒّKd6cCMPN"wZ 6MȚtlCgƉ X@R9h&ȨDT&`P%q4)pz޷F ) `IȳJPz`:nt_`!Cي͡K06#56W`@! , H"F)*Ȱ(F!qfaÆe"(Z 5)0c1Ji#VeTt<2t+ۃZEB gXhedB<8l5Eڬ@{iSgl5sUSfIZ$дj}R6 Lc$65Ȓ֍tl&BO\W2GŒ )C4Ckdڠ6#Vali$9CWGht di( =TԾ2,R )d@X E -D C`LݴDM}5!AaTRXA⊊#aH! , HP"Ep\PQ ܔQȰ"}٤h7GQŃP`A# ‰G 45^m0Î\!81Bx/q-A&c{0( d䏩32.E1cif4Kև+ )҃*NE1d6Κ5C+L6^(XL)0q-"Fi>݁/ .4Q?E\B] T B2j3uD+MowQ7[ݍ:cm\Ѣ:ہ6)PDS7f HHAx+ YaKޓiӐLqo,\1B!NE~_X(A0(Fgi[Ȩ4$)$ i4e[hD9o@m>Shg'7R{{ @-N,.HS @@EVOZq+Z)5Ї(9v2-rR47kCĂ' ~!ÐdS`< *2ڳD881ъ hrP:*&gN4$t22R`hDHq!#N;ߛƉ#vӯN83j a5ФQ O4MEތ5oD=b $`u(GȴD:Y60 1C BH#daIgaA"! , (PA"TTJ(J'\ giPۘ}з#Am>8ٙL(ʐ^bD tF&L YQFՆ~4^6i)RCSRm!?ȹ&C~XS|$4`EfiAC+ 2d i`ڌD7" EŞ2߻׏M Ҥ-dMfF:O[bC@ɂ k8sˁ]fJdlzpԦAL2+OZ* T&$PIgzZ)aX>FH޽!Ϛ&'W 2d.WhҔ[6WP̌5t%!#jcisfkh"q| RA iQL6BBB! , HPE*$ -(PQ1 !E8T?fHgdH姗dgBEp|:pCrhj&D]J"I~GR٤cQљp$K˥͘  I[E XlsVkxsF$ضk*8;>|eڃR̸r=7hOѵrt 7`RX'.8wAہUk ׵>VA`4kYbX{*MGh"ZSt q{,=4FWpLjUĵ1Qp81MWg4G({ m&-#K !^ $eS-ag]wQ!g%mM=Mi0 MIA{R\9q&}) H==CitQFbl^&tXI'Z48|7%UNVYo%NN" klvNDkc,2MN\MH|C^P0 ٭H<{ēI$P602N†;P@! , HP"F *\hPu@0Oۙj8""AEŬxq32qyR$ɒd`r("t85fہaԠQ:4;3 3MjC%@,l#, 9m,J𴘐l`(΄ DXbIYy:"RiêbȤ d a01MAF;E&}J* 2櫄M'oÕ|hb[ZON9Z|,sΌ\X$Ί P4Qy2Sc7 a_Et3Ap.QV, *EJEsr(4("C ,! , HP*TH],[JT%mQ$q"mgxqtd'&ey(5yr5u 8&.&?-6 A,ƒ`R,q@-Ac(Lɒ[cxmթq#N$Ҥ)Q^i],N!EF)x,)QGU62f"[ДƠ' DQaMV% 3`0lq(=ٳfIaGNfEٓ5ahK# VDos6S*#ExD 8Bj-wQDؑ6UhD hdžH@! , Hp&<% u.T԰m(a>pvه82ʥ\5J*TmLxIʛgd*uc@:M*RCl7;jsĵkYD'ܴti+fd.Wd6x!)H!mr wQ2 dH, {*b!ƠG4NN@kV ߾ݓ-2`$P:0!z X)T I:n*_gymAH"\SF)xw/7,Xl%=ǪedK%%|F3r FK*ݘ !7Xac-U2IJ{ |E-вIḦ́oZ}c w\;xȌT02U8aQVkW%v,#'.w&';!G?V [Ͼm_P1O@! , HPj;cØÇQ .?8 A҆6H:@=AQ[ )ZQ$Y!a% gWhlQdJHr%QX16lXƒ%,hCgnhCKDKpV[Fj0ɛ#GʾQd-m8& o"xh?z1Dvݴ3ؙ۸s3ر | aO⊢<.| bnp8wq! ,SH*\H*Jœ9c18p`PsvZ #F(V%on YR QX#^ bU',>iD>{7Dj k~0>6tJ$3·XPRV' C6v(] ByF _E{0*BiC.F +mfATMDF8 K~&LkGwX <(p&+3 JxЅӧ+0Qv,&CЄ>vo*_ $P@!, H*!W &D(DfPu"" o`XH(b(͔)Wˈle•hcieOHK10 ib2JxAڠ*T+G.!m#MBN] $^(6m!ŃQ`ۜk*22kVY= Ӥ[ઠ<] Zͺ7v2mgc2rFo1aY. b3s(B AӪzſP ]~ufBM_W&g[.x fMHPn! , M&GpS2za3 * d%g>J\. HL t.dp^%"PTCeigQ){A'I! YQ #[Qd`hkB UddV&Ҥr +8D2Z`tEd2X1<&!YTaP SA+, H :%.BUtLϺ_B pBFpaD8"EN$ƘHk(@p&'h37lB ܄i Va(Pa\8D.q@cߊ*! , Q<8\p6wȐ ѓف53JBLtcf؂* S@rtqB pcL.tAe[Wn,a n J4dShō^2b"d&d^.դ4),.LE):M"M L*%#֎AKŒ5^%@KΦKZZܴZëQ )UbKd Âݷ&kЛX7tuc@H`\z ?gHi K`Vx #E~IItB! , 8P#E*\HP[VbBȰ?ET sl 3-dN8rM[BڤB%BEs0e8klcғ{451ɱ'*"4AUT\CX dH4QT4I! *҆\S{YL,{nZZhAR( 5Is$#5מBEޛ-!WmR%Ű屁:hcMΦ(0ŀNpȨ  +2*Y'1VĈ& j@s@014" D(f& !(3H.h#8:0v>rd#hX$>rXKr1.eAOP@! , (P6E@("Ȱ!A_LoËIř)yX8PQ ]>|x`JF#/*A@*gDQL@&)5 i"Mo~Y}V0JYRXQoIunB ]M!C^TA2bBXQ)B̀^xaOQM˜E8JpY[5& Ou?dDMӦHѤĩT({ԩ`h6YݻmA7mz[ʑSK80FRQ.wږ4$Ncnj\0N5#0b C 6 X"\&`1p#'N&*fpLb@ bC;2c! ҈*5`! hޗiA! , H"u\`rS$8"  ܜᄆa& nb %w!4rB##R@ 6&Hdfx̐K*h!N(e>@y%nd! , HP6u\p3_xaCEŰpqbEˇjx( 3ZȐuف+*bdDJ idNSV ΁^T(C ,6~,o֬&Rȥ-$8d8j 9gװ\@  7s*Xicf(3PQ5ǽ7vf㿽}1mEVм bN&#/ <뼆#<9!آ38" h.L3jstOk=^Q[1iLyJdf3HKp7gQ2.ɂ' ^c\xB 02A xL!6 ڜP&ҌfPJB2Ԟ12&y`! , H"mgr)*Ȱ"FLfaCp~< . |pF,F /4d\ym17hɇ5>EěYEFK*mdyZŰ KUmqt@Ed4hYEHܛAZqiA̸VZ ڇo%0+ CAYiZ#@L`>z0K Ő" 81w1'>aFlQpH# b!f0b&4r XE!q!>'4rH &76*cC#=2p̓Q6!cB"&A! , HP"mŊ)*Ȑb?L Wa?Z42d]>|pvSva"4i(dy FjXyQ5rqlhSI@UTa@Ae F1 P1zYiƛA5 b+*MHH 16&o1)MnfS3 Ȧo)lEf[2BYbbv)q6aЄuحLf4VD{hZ];LiC 2ѪxE 0"zgP"$01pCw!p$!`2~_d 7' C rL '1ߋx&8GPx9'Pf1G! , HP"m@(*Р""ÎQCi! jY8PC\pdCˇ8]zaH!e l C#]fSN IqĬ?Esd(ŠB81Oa2Dg6TEz!m%+kAF ARtS8Akw_Q`x;S)}"r4sFaATR`: j+  QF0cL0] *Jf}C[ w*3F9^,#@as w10Ј%b r~31l p#F^a#f@lr.aRC W!s 3 xQC&yQ@! , Hp"E,xbĊňAQl\SЂ,d9ۆj` 3Dd]>̌8=DAhr* ªT$$qoMׅCx'حrrM#|1_|8p`]G),)dZ B@Iz04IAB6\2Sc뿨E0zo$?^>Y #EfnM+  WuspHi~=P#=S238 i֫6#(`Uf|ti^EG1%m 8j^kmXm]qmyrm;ty1:v,Ã9gľщ(ZuMܟ$ Nl^{g`# `̂ 6hB )! , H*&oN36`H[5G H(%1a 3&c= )ICˇXQ彘3kv@4ѻ䞴?HjB·5 Tel.*ҖjOarxBʰzxO ctP[5$ +aJkR,X;1Pl4MsCbGeE?`Mcb'h\HZ -XR`U>V8"8O"4BTr2!{}o U;C'nL}0 $1pD1 n&P8HSJ rnH#' 7b &"Nv6D$ -1ߐ'P@! , TH*\\CɋA œ1ɽH,*"N %TT= )ICˇd&LP6afV| 0) A 9dhV9HV]jX驨M*F E6-hk4+0׷1^ 4S4!F QiXi]-A4 9!,h\QLAMNFYU[ }lG͌(O48=B H*f|,]|?t:\{~BE n{ȣ#wŐ !##p@){#nrt [N"4rŒ3rsy+] #"p#vA"a_F+ u! , ڜ(\p h3 *0k= YբW<5 HiҐɜiתɽhB3gYrE ҠEXha<=?P1|Fxzt/ 94"4:*sF0s)b/4{r_`Q48Fk|XQ}8!W<3h+m [ @*fiڥZxr(/׷950sLƲ5B_U#|cqa1 B',A} q3zx!hrQJɃ+%1h&D1?p{@آ>+L.&4r?6 {)2 # ܈!)DV"Q$1>bR Ef! , O"E*$yQ.Yā p̚CZHY&&M2d4І,uYc7c!0g*`%H 9]:;͖NJzLJB"mQ}Z#8LȖ?8X>8)nbFPjX!,І),)| p*F MIv׫fp,lݚfFKY1};7ckEV0^* 6h3욙U4v<$R@@} H|cu*daxgA{IꉧM)0&4@ K!Ckfテ!#Eq] l <(AQรHrOiHsXؤ~O.X֕IP@! , (DE*LL?#.YL rpbBE=.6!E 0s"zTdC=)DA $&d^ HѢY.dpOV$%H 9]>huv \*)ᐕ1tS2E4L!@+ aRIq*+61Ebg8sVS.ړŰl,?F4@*\krTӪfLn6!\1~AEQz=b ~6sH]fTCzE^6HqFSwAdJzp1Ǹw |6AiÃ`  )\.lb&p%6ؤ YE-6&"#q 54r“TBpL#bt3\9vr2G J2.!, 8PE*\HP[~C dHCMfb f5!Ep:VƬU{%DFKYJ,]2d]E1fE.]>| oLѣ̾%Y:;WBRO"AEg}m]֨bJ$?Fh LbX>\x #MhQ`X"dU>|Dy Y09AXLQZ>̬, ʷ2Z5̱AyV9\(aܡW >qo1~ZQ(c\gw"@ m'Dg`Fl!.H` z lȉT NH" '4 ۀ1 7I|b}A#bp"Dzgd#dR;././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/examples/demo/Extras/images/logo_48x48.gif0000644000175100001730000011516600000000000025626 0ustar00runnerdocker00000000000000GIF89a00Qi*\lnqd!l/[֏];yxx@\ѐ@E?&}W~ző;bvtl-PysuwNG^|s/r[<x'[Rj2A燜\燧sΑo6s,c}5%yMiTƅbZsEJy0@pi[>Q†ljS2cyG;ߞnϗGݜd%u8ؐ{,YpLizjwn{~ۢJOo>hYv"g4}/yzm燮+lqv?},iZz|S賵.ϜSІZxxz|֫ʐN*&r]Dy÷dH`#`|D˘kLi5`uog;z~Uo0zBcvoRwMv/vXz(cȿ탪3g3̇1d4ϝk}Lt1Z"r! NETSCAPE2.0! ,00 H*\ȰÇ#6ċxbW; Y+O9n,G ybFʆ)9ECOJ𨼉Jy$$J>NaZ)(J帬[zU[( |.ָ[ɴ [ G#v$B=uKSeMLHM?)iP5Vl”fSzk7Wk9n;sYkenm*)=H>زJN\k ͯϿ)݂$'ЦMi9g.w䨁^ۛ8,ȒCq_F]v;@s'"t)_9ІmdW 7b~_Ȣ"-ԎI4iwх"^H8$nR76JT\j) F)p4U@ٍxUX)'IT'$9V Z/nt͊ jbK v'h6s&-C '32S-NAI~b!q\m3O >D<03o1 ,3CpH+!N;{d ;訸,pԁwt;aD; ( ֓|I@};@c~(tFP;x%<QX~P6BĀI))*ȠCc6<"}IXH#$|7EY[LHye,|iwx2f6eOHYlSz6g#q"UP;PZg6*5S3 'Cz_ +3BHΣ6'<9+h:YZ`0(5ŒcOBASL$>q4Nf ɢĬ#4B. H<qBK p QHl2:q\Կ4"? \5̨:!AcA! ,#* H ? *\_X},kHQk|+gaCŏ|X+IJC<~\ EyB+%#̘4L G-㍣DgР5j@Li] t` @Gk|$-5$ڌ+nY:\ږo9q[% mo _F#!49A|~a:uˬɄk5gph@]Pp8ήM2[/ҺRĆ$.<{H<&ȼT6rOG BR_da|oOȨnj6mQlǟz&G;3]z 4q8wwb_;xa!0)5A֬"2BY'rb;aD} ͆a#<=<!&x"pd[$4ftHaF6H6YGE&xY "PN!g(m՛ { ~tHgpQlPƙ3v R; 0SSHBkr3D%@ZSP`!pjQz }(5Lc.c^=F%"!EL4h[^ %|A0ӊ 4rĜЯP]s gKH*Tɨ*R 5"P))RP*! >wL43@l(HS,]H! ,"* H`?*\x2HOX|8ba"?(+;<*tE1dI?aNѰTC.P! >si t_VPg M>Ev 4;CʂگPPn][n!v੠_^ۆMv7PI7AQed'両*Plj[bdž7U4)Zc&C)A~"h2a-~$!ȼ1`x@08O P<UpHj)'W@24Xy3!̳%ڊ!ฺ-S3qG,mL+pn>%,cM\3 *4ryr/TRr`C+Nr5 |'u_P!ct<R;p?>4*A4`R)A -RA=P@! ,"( H`?ܴ3ȰAi@Ë!Jl.E:0:xGRyP@9$2&MI,bDDL)ijh*FѧD֘5,VbEGݼpmʖ*b֨jߺVOg[vމXCBǷ@!ߟ8r"^= 2c 5A3'&nk)3 WngnȓP bmAܔ+I{""k?nۢT-yw ǀb{8( A^y~`G#- (y&$`~tt@D#,G,!) #pLe$ V<Í b6:= ~" T]AJf@\d#&Zf@ģ "xEנGvBo dC.=t@FB̐/UA6dsJYSr/p!DҀE"TI#03ɠ8mSt4dsH=MS5 ~a!(|J:XESw< 3*Le) PP*Q" JxwY+*B!鮻Kb *F>!Q-yGpfC6̈́p y(1 ]"|$L?! ,  ( HA*\8'HxbHϟ v9Vc,-bܕC<(cbŋsrʔ()}@PSK,Zii%CS$lY%uP+IZ;M5Q20pG}KlP5 %1 @ FHF0%XRI)R_UY/aX'T`tGSO/&L/@fx$ 'sb@(gE^8nzG@! , % C*H۷oTf'O j\A4'iƌqdo$G`_sj9g 1 _,-Ӻb/9x-䄱\gϟwW^%95o4ij-?xy*϶ݻo u rQaNEۭmB7<*5KbTy5$鱅L,඘7Lן`X [0P& h 6҃6fÅ&!xGH'AG- 4n!l(E,ӎ' ab/V(61 6*T:@<mC\ )_id]B}mANN 眔MAxC>ZY]@qBm8EĠTN9:]x:@}5EHx~D$oOO%լ} 2É$ ԳAMU`ij˰氊 }i۔:O@! , & džv\Ȱ!CvU.7qBja;hQvkjA)8IF 1n3k9Eg \H1s5]O+RKRcD"͔蜑.BRd5k9%2|1!Ipk0z yt:ŕ\>1*$EkNN▶ [R C[QO YK$`bIЃ  !;d͆h{&( Xc:<-@"v&8e|G3/(c|Q_R< 7@X5p@mxđ ͍1,X`P!,|>@(ZW8U2Nt 4t)FAx=`c-B@) aaEE쐎o", ;]K 3 (' SR;AAvXE AvVlQ[J&Yi|Ʌ]I J5%|+n|'Z݄kOx ;VƲJ:|s`9MS[M9*|9.pc@7)H9Y@S$ rδ0(]3Fpz>S] 0{eu5ICE1‰4 T*j߬b뭮Fp*'Z5źD߰Q5 ,C3`k h\֭l0uZ z{nXk ,„yP@! , #) Hp\Ȱ!?:UСņԨ7O /]..Ȇn89X|iP6La %4JZ4x`·ufҼ9%J߆D@ƔVj}g 6q&)Yk+_(m@ IъQJ40mjR"%a48B +p2PmEWNq:klR|3N'A'$ ~S;5pNY0C}̬b067G! , #* H?<\ȰA<и㰢?\-Kfh4Dɏ- eLS4с3@7&FS SW(  uJV6GR'b:R GfYWY*['ySc&d!*8DP8aSpڅsd0A >pJQd mb 1OSOOQ+Wǟ?ȭTmxg}Z]eSG8ө2f (ӮM摱 GБh] )_|K%|XSA8]u)ps-"Z5_C"݅*v<(L|05V@zjfB" PʼnMnY7-":`#^ŽNxa) W VLz͔qUdy5K;zdPCL d@69!C dAQ\0|qͣ=PN 0QY@P蜅yJhPC\J@AI5@Ac%A9 ' pi^ Q7FS46ue;P;$H,:"P.AqK/ኻA ﯞ {,ӰYGP@! , #* H? *\OX}taHC]jTX?ֆ̫QCqx&znۼ.bqBOCWI*m9N% J1PR ),1U'ւ C'VBL Sص5=L;/P'YcJ)ɗr(o`hӖ 3t=5j5p#Ѧ׀J?:np9pPT+A5!B0N Hs(@(2E ciE5@a lê B1y9NZ맲E{쬬5;b &sm%^cAZnP2úN@cͿ9i@! , ") H?cC< 3.5B۠?[Rv&3uF@ t54hcLRx>r$Y݄ ڬĩaɴ ӃO',Ptʔ_> zz$)Sj0Ķq" XrPB SOmC9L,̢FFf! , ") H? *\O;k$XcHQCh.Sŏ]]k^':Kn! ,  ' H`?m0Ȱ@֨p3w0 5.ɘϬ>`b?kg:)i0Z̈́I#Vl\ˇ40$H`ʔOm.54LSbR$ ;ҷ6T@;*VD)v!B JʝuAGcNR)yj?%Opݶ]AT*OLyIS}M)YрDxv:)|Rp\}zśjUܶ:i4Bn@4eB&fSS&},(Q_UVI8a9p0B (@㙠 VEU<wu([BQ/C@ds0 4vOmM,W$C4B%ȡC$ pEp'y %PQhȠ6-3w685dWZЦ5fjMh`68'f@p&ևD?H"z *4! , & H?DӦTg];0a—/w6>ք#Ryi/i0^&&hT"4i3 r :5W~y25@v ȡh?s5UL2H!^EUc,:lG>\d!=IDP5N"W:A;brDP;hGqtQg8U(G|Bۢz J(yb-R7~j*jyJС*ACTp# B Z'jX身@ָ+Ay! , % H\h5$ݕ+=lhډ.j $0aE˹:M`Ԗ8nO=!j?7  "**ԄxiiAAGKՄ}STALn2f)3nU79YCJxyCpUjO-K<xL_NVL08pNh3f_yXe(Dir& _Rq(eYނ[O9@LE)"m9x8A&b]x8sG|@~R*ܓDm Cp \S:;ahCy Vh9=)< r,ٝ|jDfB  qs pB. >.! , #Hp࿃,(O#r .'! C`?G%H`ʔm-0.SlȒ&ؤ.5dbsI*hRøތ$i΄=]L""U#:0-ѫ6uuMS$&et ЊM)~%Yr,O4ڟM8P.c`v-ASL 0֘1TէvvDKJZ/O=(iD?EGHdnY7KQ~ҲypY!:*?|>%ܨ8TpwAИSC5hKޑ}S.hOVfϹpfGwk9 ?C6/t<2PmO̺v͍u'"M?ko4àkJ8SW)5Ф -S  XJir%YzyPpJw˅PWh 6 ,n!/$F]N`|A5x  ;|C79# 3P*K<Ă@ gQ fiX| spֹ yĥg~ cӟeF4&~&bY&' ˠfjC! , # ݟ*\σ^Pac-A5jz,aFO:&0e* 1 ! Q2ur/n0q~[j ]~D,MjWg?tvtdFv^"˭7rW".V5s0aN3؎4hX'_2Y`j5LeIq[;Sv1*i;P+bOMQ;wB () c~+C! ,  " o!ȰÁ 1* jdخ׎ ԘPiƓ~hiDv&*T+M%P&CkVLB,& Сrk2@Sr@φ͓Z nQN5MEl;2 uflgoK%}: Ԗ!A <2kwL|r#a  ҄f]1S TSՙzsL2V_x m^)SC! B X ^5%\'u]iS!"+b=ɀE'ЅY1'ܣNP- aDP K)8 xH[xyLA -_)?qx(Y*_B<NՎߐ]G#.yQ 7E3L@M@ĐNЕX>>q@n& vFXǼGj78qMc$Fjz.&(k'L8kC I&+:k! ,!" 8?*\Ȑ^@ذb?v$ -g톶 LQ Ȇhh;/+)-3ѥC;|8㣡D+O1hIk&Qbt`!rAW%4PvPĊ[璞4eb N~\0wׄ9]!X 3,|4pҽ?5eh90b3M*5hRY_fL)ХM9RINYnF8"_ԃr M5EXӇ Y`BC/x 8 w&|؂yT D'An|F&- iM!Lڇk 8Cb ~P"_d؝ky2'HaSrv"A|%(;BG? )hv7@n.! OO_P&)(lPP/ Gbe35l0 )@b|.XiL*cAd9)sXe#8Q Nă*-A$BjQ;whͭ/D6 lJ{Z ! ,"  H>)ȰÁ*B‡;'A{glboiK`JML$K6JLSfAk v4eG>;'c$r tC%!ƍoTv]*К5qɽ ׁPK gEWn:Tbu"L*D)=uOR7p CdxG&uR5h7d418iǺqɈ|aM] ט|Yet~IAL$SBn|3ΐO ?2')G$kDI$xRA0.,\ rL$_R(n` /8qT8Zt@ p3 kR&GS${WR&A\EmFQ"nkR|3Ĵ:D"6! ,# HPk|)ȰÁ$ELË #jӖ*bi- LJ@ȇH@S,\dخ؛5M£s'vGlLi m'ʦO l-^;[.Nv CD*OcUnJ҆tw# \~)Wy$D` Wiy()Rh!OP؋谦~#PG(L@ b4摇"hGD4,:/L2 q}A%E"oKbK i0F3ŞȂ <>li!$^NW')Biǰ ^K3< Vl@0MH"ǣJ펼!Sf?<6o:sd$%tz׎0쑩қo<}ik)-^%Έ֗RecVa;9 :.lWl'Zhm].P(EݮjYG-tufbHig.Hb@Wtԁ갉<{ ,O -ct ~U\;ƽ2Mhjq}!eӂyOko7٦ך4E bzJ|&^`;*᜗-hr ? PvSL(`n8"d sdF>pA~(G<ad@v18\" +b;SBP)!% <8w}AyAIL7,?b@ 0EC:!I(P_ d$`0K`NϤ°~IJDf;'$/8RA?y3^\nşit /C[!B}P;hJ,DG uKPc:æyz"OB vtP~ {;<|1 B"3gZt5M^A! ,# H? *\P`F64)z 񑐢LJiKʃ+^|dS&zPA;F43.5-Ja$tMysGΝ&L֮&A,4'v`zZ$%2۱ g-$2l˖tcf tKQvPiqUc(L5>Z'桫Av>UgL4n/:@.$Q5J/5fs /  "ܚkؔ17 S"MT!}sW573p,t^A]pX Hz^H9xBLÁzQ)1_p!'p(!_ KY~Z<I𣁔 ,WBʋ($7˜/ȀG,LᏔ_,-5 ]"$u_o-!B0 QJ'$`(Ptb* (l8tl!B''Zuu6Ư &-`)5׶J5ECFW TgÔoH-]|#ak_J$4ݺ~Ͷ b(cÖk\@"z3E$|姁< kĽ0E =TXQ,pax8ŌSȰ(2AC|H4NO3*‡,>! @! xH\30ԁ_\$ ;"CdIt9̒," (Dr\PCobL\॑^zEDE0ś0Ġ,sP'_I V x"х,@]*(j 3WmlJf^*T1 Ie|"YK~dގk5KQ'vxjO[2jaD?eĈ˳p$ܴztwk65Z4,- d o5FB@g1J!Vİ<àf% qȆvAGC`AS[rRE#J '^]43 nȄ<ȉHbtG]qkWڙAqtS]5?Xp\j:`Hه߁S /ܐSmP<!SĈ B C0χ!?S 982%MWE$/dKI&a(|'0>#\ d$/iÇx,_|1,D¦'UpޜS%GFzz  Tz2SD(FLSCj]_y4%P;بkC* n9UࣙFi4_f, Ր'&j„>,zM],&I<|ƻ&yüfI;! ,"! H`?61ȰAxT`͡Ń"VУ-A3.:(͎MeP@:UxǔL QtDQ& Ւ!Bk~[&=o5&I<55#'3ꪩ{GUxϬvv,U ý\+֥@OVPB=5kuvV +TInE`9~4L]:5jt]M6l1@_~凟5hP/!M:V\!e O"\b} ]@}MUrax %|r]\E|3FNqA> ϖ!y] D|L i pBɑ.c %P)נ Rr` Ȁ43|"GS",ds״IÇ%l:WC;Mh#_y?C|!K6+Aįʎå(̒jy" )^-lk#B ޒ).! , " H? *\HGhaHQ BAF[#*ȹ4ovh[ k"sL-FAkeS@<85OVZFO7 jDv=pɊKEGjL$RbA=Yk Bn`Г$Jd]ƒo.QIo{Iȴ!\n $-V,%Wxr# bE*_=J`;vKjUâOD*,̂:pY8v!Z#Qb܌cJ7ovJTF

    HTMLEditor example

    """ + __doc__ + """ Here are some lists formatted in this way: Numbered list: * first * second * third Bulleted list: - eat - drink - be merry """ ) class HTMLEditorDemo(HasTraits): """Defines the main HTMLEditor demo class.""" # Define a HTML trait to view my_html_trait = HTML(sample_text) # Demo view traits_view = View( UItem( 'my_html_trait', # we specify the editor explicitly in order to set format_text: editor=HTMLEditor(format_text=True), ), title='HTMLEditor', buttons=['OK'], width=800, height=600, resizable=True, ) # Create the demo: demo = HTMLEditorDemo() # Run the demo (if invoked from the command line): if __name__ == '__main__': demo.configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/examples/demo/Standard_Editors/ImageEnumEditor_demo.py0000644000175100001730000000475300000000000030431 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ **WARNING** This demo might not work as expected and some documented features might be missing. ------------------------------------------------------------------------------- Implementation of an ImageEnumEditor demo plugin for the Traits UI demo program. This demo shows each of the four styles of the ImageEnumEditor. Please refer to the `ImageEnumEditor API docs`_ for further information. .. _ImageEnumEditor API docs: https://docs.enthought.com/traitsui/api/traitsui.editors.image_enum_editor.html#traitsui.editors.image_enum_editor.ImageEnumEditor """ # Issues related to the demo warning: # enthought/traitsui#947 from traits.api import Enum, HasTraits, Str from traitsui.api import Item, Group, View, ImageEnumEditor # This list of image names (with the standard suffix "_origin") is used to # construct an image enumeration trait to demonstrate the ImageEnumEditor: image_list = ['top left', 'top right', 'bottom left', 'bottom right'] class Dummy(HasTraits): """Dummy class for ImageEnumEditor""" x = Str() traits_view = View() class ImageEnumEditorDemo(HasTraits): """Defines the ImageEnumEditor demo class.""" # Define a trait to view: image_from_list = Enum( *image_list, editor=ImageEnumEditor( values=image_list, prefix='@icons:', suffix='_origin', cols=4, klass=Dummy, ), ) # Items are used to define the demo display, one Item per editor style: img_group = Group( Item('image_from_list', style='simple', label='Simple'), Item('_'), Item('image_from_list', style='text', label='Text'), Item('_'), Item('image_from_list', style='readonly', label='ReadOnly'), Item('_'), Item('image_from_list', style='custom', label='Custom'), ) # Demo view: traits_view = View( img_group, title='ImageEnumEditor', buttons=['OK'], resizable=True ) # Create the demo: demo = ImageEnumEditorDemo() # Run the demo (if invoked from the command line): if __name__ == '__main__': demo.configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/examples/demo/Standard_Editors/InstanceEditor_demo.py0000644000175100001730000000552200000000000030321 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ **WARNING** This demo might not work as expected and some documented features might be missing. ------------------------------------------------------------------------------- Implementation of an InstanceEditor demo plugin for the Traits UI demo program. This demo shows each of the four styles of the InstanceEditor Fixme: This version of the demo only shows the old-style InstanceEditor capabilities. Please refer to the `InstanceEditor API docs`_ for further information. .. _InstanceEditor API docs: https://docs.enthought.com/traitsui/api/traitsui.editors.instance_editor.html#traitsui.editors.instance_editor.InstanceEditor """ # Issue related to the demo warning: enthought/traitsui#939 from traits.api import HasTraits, Str, Range, Bool, Instance from traitsui.api import Item, Group, View # ------------------------------------------------------------------------- # Classes: # ------------------------------------------------------------------------- class SampleClass(HasTraits): """This Sample class is used to demonstrate the InstanceEditor demo.""" # The actual attributes don't matter here; we just need an assortment # to demonstrate the InstanceEditor's capabilities.: name = Str() occupation = Str() age = Range(21, 65) registered_voter = Bool() # The InstanceEditor uses whatever view is defined for the class. The # default view lists the fields alphabetically, so it's best to define one # explicitly: traits_view = View('name', 'occupation', 'age', 'registered_voter') class InstanceEditorDemo(HasTraits): """This class specifies the details of the InstanceEditor demo.""" # Create an Instance trait to view: sample_instance = Instance(SampleClass, ()) # Items are used to define the demo display, one item per editor style: inst_group = Group( Item('sample_instance', style='simple', label='Simple', id="simple"), Item('_'), Item('sample_instance', style='custom', label='Custom', id="custom"), Item('_'), Item('sample_instance', style='text', label='Text'), Item('_'), Item('sample_instance', style='readonly', label='ReadOnly'), ) # Demo View: traits_view = View( inst_group, title='InstanceEditor', buttons=['OK'], resizable=True ) # Create the demo: demo = InstanceEditorDemo() # Run the demo (if invoked from the command line): if __name__ == '__main__': demo.configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/examples/demo/Standard_Editors/ListEditor_demo.py0000644000175100001730000000351400000000000027467 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Implemention of a ListEditor demo plugin for Traits UI demo program This demo shows each of the four styles of ListEditor. Please refer to the `ListEditor API docs`_ for further information. .. _ListEditor API docs: https://docs.enthought.com/traitsui/api/traitsui.editors.list_editor.html#traitsui.editors.list_editor.ListEditor """ from traits.api import HasTraits, List, Str from traitsui.api import Item, Group, View # Define the demo class: class ListEditorDemo(HasTraits): """Defines the main ListEditor demo class.""" # Define a List trait to display: play_list = List(Str, ["The Merchant of Venice", "Hamlet", "MacBeth"]) # Items are used to define display, one per editor style: list_group = Group( Item( 'play_list', style='simple', label='Simple', height=75, id='simple' ), Item('_'), Item('play_list', style='custom', label='Custom', id='custom'), Item('_'), Item('play_list', style='text', label='Text', id='text'), Item('_'), Item('play_list', style='readonly', label='ReadOnly', id='readonly'), ) # Demo view: traits_view = View( list_group, title='ListEditor', buttons=['OK'], height=600, width=400, resizable=True, ) # Create the demo: demo = ListEditorDemo() # Run the demo (if invoked from the command line): if __name__ == '__main__': demo.configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/examples/demo/Standard_Editors/RGBColorEditor_demo.py0000644000175100001730000000367500000000000030175 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ **WARNING** This demo might not work as expected and some documented features might be missing. ------------------------------------------------------------------------------- Implementation of a RGBColorEditor demo plugin for Traits UI demo program. This demo shows each of the four styles of the ColorEditor. Please refer to the `RGBColorEditor API docs`_ for further information. .. _RGBColorEditor API docs: https://docs.enthought.com/traitsui/api/traitsui.editors.rgb_color_editor.html#traitsui.editors.rgb_color_editor.RGBColorEditor """ # Issue related to the demo warning: enthought/traitsui#939 from traits.api import HasTraits from traitsui.api import Item, Group, View, RGBColor # Demo class definition: class RGBColorEditorDemo(HasTraits): """Defines the main RGBColorEditor demo.""" # Define a Color trait to view: color_trait = RGBColor() # Items are used to define the demo display, one item per editor style: color_group = Group( Item('color_trait', style='simple', label='Simple'), Item('_'), Item('color_trait', style='custom', label='Custom'), Item('_'), Item('color_trait', style='text', label='Text'), Item('_'), Item('color_trait', style='readonly', label='ReadOnly'), ) # Demo view traits_view = View( color_group, title='RGBColorEditor', buttons=['OK'], resizable=True ) # Create the demo: demo = RGBColorEditorDemo() # Run the demo (if invoked from the command line): if __name__ == '__main__': demo.configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/examples/demo/Standard_Editors/RangeEditor_demo.py0000644000175100001730000001103400000000000027604 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Range editor A Range Trait holds a numeric value which is restricted to a specified range. This example shows how the RangeEditor's simple and custom styles vary depending on the type (integer or float) and size (small, medium, or large integer) of the specified range. The example also shows how multiple Groups at the top level of a View are automatically placed into separate tabs. For an example of how to dynamically vary the bounds of a Range trait, see the *Dynamic Range Editor* example. Please refer to the `RangeEditor API docs`_ for further information. .. _RangeEditor API docs: https://docs.enthought.com/traitsui/api/traitsui.editors.range_editor.html#traitsui.editors.range_editor.RangeEditor """ from traits.api import HasTraits, Range from traitsui.api import Item, Group, View class RangeEditorDemo(HasTraits): """Defines the RangeEditor demo class.""" # Define a trait for each of four range variants: small_int_range = Range(1, 16) medium_int_range = Range(1, 25) large_int_range = Range(1, 150) float_range = Range(0.0, 150.0) # RangeEditor display for narrow integer Range traits (< 17 wide): int_range_group1 = Group( Item( 'small_int_range', style='simple', label='Simple', id='simple_small', ), Item('_'), Item( 'small_int_range', style='custom', label='Custom', id='custom_small', ), Item('_'), Item('small_int_range', style='text', label='Text', id='text_small'), Item('_'), Item( 'small_int_range', style='readonly', label='ReadOnly', id='readonly_small', ), label='Small Int', ) # RangeEditor display for medium-width integer Range traits (17 to 100): int_range_group2 = Group( Item( 'medium_int_range', style='simple', label='Simple', id='simple_medium', ), Item('_'), Item( 'medium_int_range', style='custom', label='Custom', id='custom_medium', ), Item('_'), Item('medium_int_range', style='text', label='Text', id='text_medium'), Item('_'), Item( 'medium_int_range', style='readonly', label='ReadOnly', id='readonly_medium', ), label='Medium Int', ) # RangeEditor display for wide integer Range traits (> 100): int_range_group3 = Group( Item( 'large_int_range', style='simple', label='Simple', id='simple_large', ), Item('_'), Item( 'large_int_range', style='custom', label='Custom', id='custom_large', ), Item('_'), Item('large_int_range', style='text', label='Text', id='text_large'), Item('_'), Item( 'large_int_range', style='readonly', label='ReadOnly', id='readonly_large', ), label='Large Int', ) # RangeEditor display for float Range traits: float_range_group = Group( Item('float_range', style='simple', label='Simple', id='simple_float'), Item('_'), Item('float_range', style='custom', label='Custom', id='custom_float'), Item('_'), Item('float_range', style='text', label='Text', id='text_float'), Item('_'), Item( 'float_range', style='readonly', label='ReadOnly', id='readonly_float', ), label='Float', ) # The view includes one group per data type. These will be displayed # on separate tabbed panels: traits_view = View( int_range_group1, int_range_group2, int_range_group3, float_range_group, title='RangeEditor', buttons=['OK'], resizable=True, ) # Create the demo: demo = RangeEditorDemo() # Run the demo (if invoked from the comand line): if __name__ == '__main__': demo.configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/examples/demo/Standard_Editors/SetEditor_demo.py0000644000175100001730000000656400000000000027317 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Implementation of a SetEditor demo plugin for the Traits UI demo program. The four tabs of this demo show variations on the interface as follows: - Unord I: Creates an alphabetized subset, has no "move all" options - Unord II: Creates an alphabetized subset, has "move all" options - Ord I: Creates a set whose order is specified by the user, no "move all" - Ord II: Creates a set whose order is specifed by the user, has "move all" Please refer to the `SetEditor API docs`_ for further information. .. _SetEditor API docs: https://docs.enthought.com/traitsui/api/traitsui.editors.set_editor.html#traitsui.editors.set_editor.SetEditor """ from traits.api import HasTraits, List from traitsui.api import Item, Group, View, SetEditor # Define the main demo class: class SetEditorDemo(HasTraits): """Defines the SetEditor demo class.""" # Define a trait each for four SetEditor variants: unord_nma_set = List( editor=SetEditor( values=['kumquats', 'pomegranates', 'kiwi'], left_column_title='Available Fruit', right_column_title='Exotic Fruit Bowl', can_move_all=False, ) ) unord_ma_set = List( editor=SetEditor( values=['kumquats', 'pomegranates', 'kiwi'], left_column_title='Available Fruit', right_column_title='Exotic Fruit Bowl', ) ) ord_nma_set = List( editor=SetEditor( values=['apples', 'berries', 'cantaloupe'], left_column_title='Available Fruit', right_column_title='Fruit Bowl', ordered=True, can_move_all=False, ) ) ord_ma_set = List( editor=SetEditor( values=['apples', 'berries', 'cantaloupe'], left_column_title='Available Fruit', right_column_title='Fruit Bowl', ordered=True, ) ) # SetEditor display, unordered, no move-all buttons: no_nma_group = Group( Item('unord_nma_set', style='simple'), label='Unord I', show_labels=False, ) # SetEditor display, unordered, move-all buttons: no_ma_group = Group( Item('unord_ma_set', style='simple'), label='Unord II', show_labels=False, ) # SetEditor display, ordered, no move-all buttons: o_nma_group = Group( Item('ord_nma_set', style='simple'), label='Ord I', show_labels=False ) # SetEditor display, ordered, move-all buttons: o_ma_group = Group( Item('ord_ma_set', style='simple'), label='Ord II', show_labels=False ) # The view includes one group per data type. These will be displayed # on separate tabbed panels: traits_view = View( no_nma_group, no_ma_group, o_nma_group, o_ma_group, title='SetEditor', buttons=['OK'], ) # Create the demo: demo = SetEditorDemo() # Run the demo (if invoked from the command line): if __name__ == '__main__': demo.configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/examples/demo/Standard_Editors/TableEditor_demo.py0000644000175100001730000000726400000000000027611 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ **WARNING** This demo might not work as expected and some documented features might be missing. ------------------------------------------------------------------------------- Implementation of a TableEditor demo plugin for Traits UI demo program This demo shows the full behavior of a straightforward TableEditor. Only one style of TableEditor is implemented, so that is the one shown. Please refer to the `TableEditor API docs`_ for further information. .. _TableEditor API docs: https://docs.enthought.com/traitsui/api/traitsui.editors.table_editor.html#traitsui.editors.table_editor.TableEditor """ # Issue related to the demo warning: enthought/traitsui#948 from traits.api import HasTraits, HasStrictTraits, Str, Int, Regex, List from traitsui.api import ( View, Group, Item, TableEditor, ObjectColumn, ExpressionColumn, EvalTableFilter, ) from traitsui.table_filter import ( EvalFilterTemplate, MenuFilterTemplate, RuleFilterTemplate, ) # A helper class for the 'Department' class below: class Employee(HasTraits): first_name = Str() last_name = Str() age = Int() phone = Regex(value='000-0000', regex=r'\d\d\d[-]\d\d\d\d') traits_view = View( 'first_name', 'last_name', 'age', 'phone', title='Create new employee', width=0.18, buttons=['OK', 'Cancel'], ) # For readability, the TableEditor of the demo is defined here, rather than in # the View: table_editor = TableEditor( columns=[ ObjectColumn(name='first_name', width=0.20), ObjectColumn(name='last_name', width=0.20), ExpressionColumn( label='Full Name', width=0.30, expression="'%s %s' % (object.first_name, " "object.last_name )", ), ObjectColumn(name='age', width=0.10, horizontal_alignment='center'), ObjectColumn(name='phone', width=0.20), ], deletable=True, sort_model=True, auto_size=False, orientation='vertical', edit_view=View( Group('first_name', 'last_name', 'age', 'phone', show_border=True), resizable=True, ), filters=[EvalFilterTemplate, MenuFilterTemplate, RuleFilterTemplate], search=EvalTableFilter(), show_toolbar=True, row_factory=Employee, ) # The class to be edited with the TableEditor: class Department(HasStrictTraits): employees = List(Employee) traits_view = View( Group( Item('employees', show_label=False, editor=table_editor), show_border=True, ), title='Department Personnel', width=0.4, height=0.4, resizable=True, buttons=['OK'], kind='live', ) # Create some employees: employees = [ Employee(first_name='Jason', last_name='Smith', age=32, phone='555-1111'), Employee(first_name='Mike', last_name='Tollan', age=34, phone='555-2222'), Employee( first_name='Dave', last_name='Richards', age=42, phone='555-3333' ), Employee(first_name='Lyn', last_name='Spitz', age=40, phone='555-4444'), Employee(first_name='Greg', last_name='Andrews', age=45, phone='555-5555'), ] # Create the demo: demo = Department(employees=employees) # Run the demo (if invoked from the command line): if __name__ == '__main__': demo.configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/examples/demo/Standard_Editors/TextEditor_demo.py0000644000175100001730000000632700000000000027505 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Edit a string, password, or integer The TextEditor displays a Str, Password, or Int trait for the user to edit. The demo shows all styles of the editor for each of the traits, however certain styles are more useful than others: - When editing a Str, consider styles 'simple' (one-line), 'custom' (multi-line), or 'readonly' (multi-line). - When editing a Password, style 'simple' is recommended (shows asterisks). - When editing an Int, consider styles 'simple' and 'readonly'. Please refer to the `TextEditor API docs`_ for further information. .. _TextEditor API docs: https://docs.enthought.com/traitsui/api/traitsui.editors.text_editor.html#traitsui.editors.text_editor.TextEditor """ from traits.api import HasTraits, Str, Int, Password from traitsui.api import Item, Group, View # The main demo class: class TextEditorDemo(HasTraits): """Defines the TextEditor demo class.""" # Define a trait for each of three TextEditor variants: string_trait = Str("sample string") int_trait = Int(1) password = Password() # TextEditor display with multi-line capability (for a string): text_str_group = Group( Item('string_trait', style='simple', label='Simple'), Item('_'), Item('string_trait', style='custom', label='Custom'), Item('_'), Item('string_trait', style='text', label='Text'), Item('_'), Item('string_trait', style='readonly', label='ReadOnly'), label='String', ) # TextEditor display without multi-line capability (for an integer): text_int_group = Group( Item('int_trait', style='simple', label='Simple', id="simple_int"), Item('_'), Item('int_trait', style='custom', label='Custom', id="custom_int"), Item('_'), Item('int_trait', style='text', label='Text', id="text_int"), Item('_'), Item( 'int_trait', style='readonly', label='ReadOnly', id="readonly_int", ), label='Integer', ) # TextEditor display with secret typing capability (for Password traits): text_pass_group = Group( Item('password', style='simple', label='Simple'), Item('_'), Item('password', style='custom', label='Custom'), Item('_'), Item('password', style='text', label='Text'), Item('_'), Item('password', style='readonly', label='ReadOnly'), label='Password', ) # The view includes one group per data type. These will be displayed # on separate tabbed panels: traits_view = View( text_str_group, text_pass_group, text_int_group, title='TextEditor', buttons=['OK'], ) # Create the demo: demo = TextEditorDemo() # Run the demo (if invoked from the command line): if __name__ == "__main__": demo.configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/examples/demo/Standard_Editors/TitleEditor_demo.py0000644000175100001730000000754300000000000027643 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Demonstrates the use of the TitleEditor. A TitleEditor can be used to dynamically label sections of a user interface. The text displayed by the TitleEditor is specified by a trait associated with the view. This demonstration shows three variations of using a TitleEditor: * In the first example, the TitleEditor values are supplied by an Enum trait. Simply select a new value for the title from the drop-down list to cause the title to change. * In the second example, the TitleEditor values are supplied by a Str trait. Simply type a new value into the title field to cause the title to change. * In the third example, the TitleEditor values are supplied by a Property whose value is derived from a calculation on a Float trait. Type a number into the value field to cause the title to changed. Please refer to the `TitleEditor API docs`_ for further information. .. _TitleEditor API docs: https://docs.enthought.com/traitsui/api/traitsui.editors.title_editor.html#traitsui.editors.title_editor.TitleEditor """ from traits.api import HasTraits, Enum, Str, Float, Property, cached_property from traitsui.api import View, VGroup, HGroup, Item, TitleEditor class TitleEditorDemo(HasTraits): # Define the selection of titles that can be displayed: title = Enum( 'Select a new title from the drop down list below', 'This is the TitleEditor demonstration', 'Acme Widgets Sales for Each Quarter', 'This is Not Intended to be a Real Application', ) # A user settable version of the title: title_2 = Str('Type into the text field below to change this title') # A title driven by the result of a calculation: title_3 = Property(observe='value') # The number used to drive the calculation: value = Float() # Define the test view: traits_view = View( VGroup( VGroup( HGroup( Item( 'title', show_label=False, springy=True, editor=TitleEditor(), ) ), Item('title'), show_border=True, ), VGroup( HGroup( Item( 'title_2', show_label=False, springy=True, editor=TitleEditor(), ) ), Item('title_2', label='Title'), show_border=True, ), VGroup( HGroup( Item( 'title_3', show_label=False, springy=True, editor=TitleEditor(), ) ), Item('value'), show_border=True, ), ), width=0.4, ) # -- Property Implementations --------------------------------------------- @cached_property def _get_title_3(self): if self.value >= 0: return 'The square root of {} is {}'.format( self.value, self.value ** 0.5 ) else: return 'The square root of {} is {}i'.format( self.value, (-self.value) ** 0.5 ) # Create the demo: demo = TitleEditorDemo() # Run the demo (if invoked from the command line): if __name__ == '__main__': demo.configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/examples/demo/Standard_Editors/TreeEditor_demo.py0000644000175100001730000001206000000000000027447 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Tree editor for hierarchal data Demonstrates using the TreeEditor to display a hierarchically organized data structure. In this case, the tree has the following hierarchy: - Partner - Company - Department - Employee The TreeEditor generates a hierarchical tree control, consisting of nodes. It is useful for cases where objects contain lists of other objects. The tree control is displayed in one pane of the editor, and a user interface for the selected object is displayed in the other pane. The layout orientation of the tree and the object editor is determined by the *orientation* parameter of TreeEditor(), which can be 'horizontal' or 'vertical'. You must specify the types of nodes that can appear in the tree using the *nodes* parameter, which must be a list of instances of TreeNode (or of subclasses of TreeNode). You must specify the classes whose instances the node type applies to. Use the **node_for** attribute of TreeNode to specify a list of classes; often, this list contains only one class. You can have more than one node type that applies to a particular class; in this case, each object of that class is represented by multiple nodes, one for each applicable node type. See the Traits User Manual for more details. Please refer to the `TreeEditor API docs`_ for further information. .. _TreeEditor API docs: https://docs.enthought.com/traitsui/api/traitsui.editors.tree_editor.html#traitsui.editors.tree_editor.TreeEditor """ from traits.api import HasTraits, Str, Regex, List, Instance from traitsui.api import Item, View, TreeEditor, TreeNode class Employee(HasTraits): """Defines a company employee.""" name = Str('') title = Str() phone = Regex(regex=r'\d\d\d-\d\d\d\d') def default_title(self): self.title = 'Senior Engineer' class Department(HasTraits): """Defines a department with employees.""" name = Str('') employees = List(Employee) class Company(HasTraits): """Defines a company with departments and employees.""" name = Str('') departments = List(Department) employees = List(Employee) # Create an empty view for objects that have no data to display: no_view = View() # Define the TreeEditor used to display the hierarchy: tree_editor = TreeEditor( nodes=[ # The first node specified is the top level one TreeNode( node_for=[Company], auto_open=True, # child nodes are children='', label='name', # label with Company name view=View(['name']), ), TreeNode( node_for=[Company], auto_open=True, children='departments', label='=Departments', # constant label view=no_view, add=[Department], ), TreeNode( node_for=[Company], auto_open=True, children='employees', label='=Employees', # constant label view=no_view, add=[Employee], ), TreeNode( node_for=[Department], auto_open=True, children='employees', label='name', # label with Department name view=View(['name']), add=[Employee], ), TreeNode( node_for=[Employee], auto_open=True, label='name', # label with Employee name view=View(['name', 'title', 'phone']), ), ] ) class Partner(HasTraits): """Defines a business partner.""" name = Str('') company = Instance(Company) traits_view = View( Item(name='company', editor=tree_editor, show_label=False), title='Company Structure', buttons=['OK'], resizable=True, style='custom', width=0.3, height=500, ) # Create an example data structure: jason = Employee(name='Jason', title='Senior Engineer', phone='536-1057') mike = Employee(name='Mike', title='Senior Engineer', phone='536-1057') dave = Employee(name='Dave', title='Senior Developer', phone='536-1057') martin = Employee(name='Martin', title='Senior Engineer', phone='536-1057') duncan = Employee(name='Duncan', title='Consultant', phone='526-1057') # Create the demo: demo = Partner( name='Enthought, Inc.', company=Company( name='Enthought', employees=[dave, martin, duncan, jason, mike], departments=[ Department(name='Business', employees=[jason, mike]), Department(name='Scientific', employees=[dave, martin, duncan]), ], ), ) # Run the demo (if invoked from the command line): if __name__ == '__main__': demo.configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/examples/demo/Standard_Editors/TupleEditor_demo.py0000644000175100001730000000361100000000000027643 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ **WARNING** This demo might not work as expected and some documented features might be missing. ------------------------------------------------------------------------------- Implementation of a TupleEditor demo plugin for Traits UI demo program. This demo shows each of the four styles of the TupleEditor. Please refer to the `TupleEditor API docs`_ for further information. .. _TupleEditor API docs: https://docs.enthought.com/traitsui/api/traitsui.editors.tuple_editor.html#traitsui.editors.tuple_editor.TupleEditor """ # Issue related to the demo warning: enthought/traitsui#939 from traits.api import HasTraits, Tuple, Range, Str from traitsui.api import Item, Group, View, Color # The main demo class: class TupleEditorDemo(HasTraits): """Defines the TupleEditor demo class.""" # Define a trait to view: tuple = Tuple(Color, Range(1, 4), Str) # Display specification (one Item per editor style): tuple_group = Group( Item('tuple', style='simple', label='Simple'), Item('_'), Item('tuple', style='custom', label='Custom'), Item('_'), Item('tuple', style='text', label='Text'), Item('_'), Item('tuple', style='readonly', label='ReadOnly'), ) # Demo view traits_view = View( tuple_group, title='TupleEditor', buttons=['OK'], resizable=True ) # Create the demo: demo = TupleEditorDemo() # Run the demo (if invoked from the command line): if __name__ == '__main__': demo.configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/examples/demo/Standard_Editors/VideoEditor_demo.py0000644000175100001730000001246200000000000027624 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Demonstrates a 'display only' video editor for Qt5+. Please refer to the `VideoEditor API docs`_ for further information. .. _VideoEditor API docs: https://docs.enthought.com/traitsui/api/traitsui.editors.video_editor.html#traitsui.editors.video_editor.VideoEditor """ import numpy as np from PIL import Image from pyface.qt import is_qt5 from pyface.qt.QtGui import QImage from traits.api import ( Bool, Button, Callable, Float, HasTraits, Range, Str, observe, ) from traitsui.api import ButtonEditor, ContextValue, HGroup, Item, UItem, View from traitsui.editors.video_editor import MediaStatus, PlayerState, VideoEditor def QImage_from_np(image): assert np.max(image) <= 255 image8 = image.astype(np.uint8, order='C', casting='unsafe') height, width, colors = image8.shape bytesPerLine = 4 * width image = QImage( image8.data, width, height, bytesPerLine, QImage.Format.Format_RGB32 ) return image def np_from_QImage(qimage): # Creates a numpy array from a pyqt(5) QImage object width, height = qimage.width(), qimage.height() channels = qimage.pixelFormat().channelCount() return ( np.array(qimage.bits().asarray(width * height * channels)) .reshape(height, width, channels) .astype('u1') ) def qimage_function(antialiasing=True): def antialias_func(image_func): """ Turns an image function into a QImage function, bound within the viewing frames box. """ def qimage_conv_func(image, box_dims): _np_image = image_func(np_from_QImage(image)) pil_image = Image.fromarray(_np_image) if antialiasing: pil_image.thumbnail(box_dims, Image.ANTIALIAS) else: pil_image.thumbnail(box_dims) _np_image = np.array(pil_image) image = QImage_from_np(_np_image) return image, _np_image return qimage_conv_func return antialias_func @qimage_function(antialiasing=False) def test_image_func(image): return image.transpose(1, 0, 2) class VideoEditorDemo(HasTraits): """Defines the main VideoEditor demo class.""" #: The URL that holds the video data. video_url = Str() #: A button that plays/pauses play_pause_button = Button() button_label = Str('Play') state = PlayerState duration = Float position = Range(low=0.0, high='duration') error = Str status = MediaStatus buffer = Range(0, 100) muted = Bool(True) volume = Range(0.0, 100.0, value=100.0) playback_rate = Float(1.0) image_func = Callable() @observe('state') def _state_update(self, event): if event.new == 'stopped' or event.new == 'paused': self.button_label = 'Play' elif event.new == 'playing': self.button_label = 'Pause' @observe('play_pause_button') def _play_pause_button_click(self, event): if self.state == 'stopped' or self.state == 'paused': self.state = 'playing' else: self.state = 'paused' # Demo view traits_view = View( UItem('video_url'), UItem( 'video_url', editor=VideoEditor( state=ContextValue('state'), position=ContextValue('position'), duration=ContextValue('duration'), video_error=ContextValue('error'), media_status=ContextValue('status'), buffer=ContextValue('buffer'), muted=ContextValue('muted'), volume=ContextValue('volume'), playback_rate=ContextValue('playback_rate'), image_func=ContextValue('image_func'), notify_interval=0.5, ), ), HGroup( UItem( 'play_pause_button', editor=ButtonEditor(label_value='button_label'), enabled_when='not bool(error)', width=100, ), UItem('position'), ), HGroup( Item('playback_rate'), Item('muted', label='Mute'), Item('volume'), ), HGroup( Item('state', style='readonly'), Item('status', style='readonly'), Item('buffer', style='readonly'), Item('error', visible_when="bool(error)", style='readonly'), ), title='VideoEditor', buttons=['OK'], width=800, height=600, resizable=True, ) url = "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerEscapes.mp4" # noqa: E501 # Create the demo: if is_qt5: demo = VideoEditorDemo(image_func=test_image_func, video_url=url) else: # Qt 6 can't do on-the-fly image manipulation demo = VideoEditorDemo(video_url=url) # Run the demo (if invoked from the command line): if __name__ == '__main__': demo.configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/examples/demo/Standard_Editors/index.rst0000644000175100001730000000012600000000000025664 0ustar00runnerdocker00000000000000These examples demonstrate how the Standard Editors available in TraitsUI can be used.././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1684768028.087807 traitsui-8.0.0/traitsui/examples/demo/Standard_Editors/tests/0000755000175100001730000000000000000000000025166 5ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/examples/demo/Standard_Editors/tests/test_BooleanEditor_demo.py0000644000175100001730000000401200000000000032326 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ This example demonstrates how to test interacting with a boolean using BooleanEditor. The GUI being tested is written in the demo under the same name (minus the preceding 'test') in the outer directory. """ import os import runpy import unittest from traitsui.testing.api import ( DisplayedText, KeyClick, KeySequence, MouseClick, UITester, ) #: Filename of the demo script FILENAME = "BooleanEditor_demo.py" #: Path of the demo script DEMO_PATH = os.path.join(os.path.dirname(__file__), "..", FILENAME) class TestBooleanEditorDemo(unittest.TestCase): def test_boolean_editor_demo(self): demo = runpy.run_path(DEMO_PATH)["demo"] tester = UITester() with tester.create_ui(demo) as ui: simple = tester.find_by_id(ui, 'simple') custom = tester.find_by_id(ui, 'custom') text = tester.find_by_id(ui, 'text') readonly = tester.find_by_id(ui, 'readonly') simple.perform(MouseClick()) self.assertEqual(demo.boolean_trait, True) custom.perform(MouseClick()) self.assertEqual(demo.boolean_trait, False) for _ in range(5): text.perform(KeyClick("Backspace")) text.perform(KeySequence("True")) text.perform(KeyClick("Enter")) self.assertEqual(demo.boolean_trait, True) demo.boolean_trait = False displayed = readonly.inspect(DisplayedText()) self.assertEqual(displayed, "False") # Run the test(s) unittest.TextTestRunner().run( unittest.TestLoader().loadTestsFromTestCase(TestBooleanEditorDemo) ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/examples/demo/Standard_Editors/tests/test_BooleanEditor_simple_demo.py0000644000175100001730000000446600000000000033714 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ This example demonstrates how to test interacting with a boolean using BooleanEditor. The GUI being tested is written in the demo under the same name (minus the preceding 'test') in the outer directory. """ import os import runpy import unittest from traitsui.testing.api import ( DisplayedText, IsChecked, KeyClick, KeySequence, MouseClick, UITester, ) #: Filename of the demo script FILENAME = "BooleanEditor_simple_demo.py" #: Path of the demo script DEMO_PATH = os.path.join(os.path.dirname(__file__), "..", FILENAME) class TestBooleanEditorSimpleDemo(unittest.TestCase): def test_boolean_editor_simple_demo(self): demo = runpy.run_path(DEMO_PATH)["demo"] tester = UITester() with tester.create_ui(demo) as ui: simple = tester.find_by_id(ui, 'simple') readonly = tester.find_by_id(ui, 'readonly') text = tester.find_by_id(ui, 'text') count_changes = tester.find_by_name(ui, "count_changes") simple.perform(MouseClick()) self.assertEqual(demo.my_boolean_trait, True) for _ in range(4): text.perform(KeyClick("Backspace")) text.perform(KeySequence("False")) text.perform(KeyClick("Enter")) self.assertEqual(demo.my_boolean_trait, False) displayed_count_changes = count_changes.inspect(DisplayedText()) self.assertEqual(displayed_count_changes, '2') self.assertEqual(displayed_count_changes, str(demo.count_changes)) demo.my_boolean_trait = True displayed = readonly.inspect(DisplayedText()) self.assertEqual(displayed, "True") simple_is_checked = simple.inspect(IsChecked()) self.assertEqual(simple_is_checked, True) # Run the test(s) unittest.TextTestRunner().run( unittest.TestLoader().loadTestsFromTestCase(TestBooleanEditorSimpleDemo) ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/examples/demo/Standard_Editors/tests/test_ButtonEditor_demo.py0000644000175100001730000000544100000000000032231 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ This example demonstrates how to test interacting with a button created using ButtonEditor. It also demonstarates the use of UI Tester, together with pyface's ModalDialogTester. The GUI being tested is written in the demo under the same name (minus the preceding 'test') in the outer directory. """ import os import runpy import unittest from pyface.toolkit import toolkit_object from pyface.constant import OK from traitsui.testing.api import MouseClick, UITester ModalDialogTester = toolkit_object( "util.modal_dialog_tester:ModalDialogTester" ) no_modal_dialog_tester = ModalDialogTester.__name__ == "Unimplemented" #: Filename of the demo script FILENAME = "ButtonEditor_demo.py" #: Path of the demo script DEMO_PATH = os.path.join(os.path.dirname(__file__), "..", FILENAME) @unittest.skipIf(no_modal_dialog_tester, "ModalDialogTester unavailable") class TestButtonEditorDemo(unittest.TestCase): def test_button_editor_demo_simple(self): demo = runpy.run_path(DEMO_PATH)["demo"] tester = UITester() with tester.create_ui(demo) as ui: simple_button = tester.find_by_id(ui, "simple") # funcion object for instantiating ModalDialogTester should be a # function that opens the dialog # we want clicking the buttons to do that def click_simple_button(): simple_button.perform(MouseClick()) mdtester_simple = ModalDialogTester(click_simple_button) mdtester_simple.open_and_run(lambda x: x.click_button(OK)) self.assertTrue(mdtester_simple.dialog_was_opened) def test_button_editor_demo_custom(self): demo = runpy.run_path(DEMO_PATH)["demo"] tester = UITester() with tester.create_ui(demo) as ui: custom_button = tester.find_by_id(ui, "custom") # funcion object for instantiating ModalDialogTester should be a # function that opens the dialog # we want clicking the buttons to do that def click_custom_button(): custom_button.perform(MouseClick()) mdtester_custom = ModalDialogTester(click_custom_button) mdtester_custom.open_and_run(lambda x: x.click_button(OK)) self.assertTrue(mdtester_custom.dialog_was_opened) # Run the test(s) unittest.TextTestRunner().run( unittest.TestLoader().loadTestsFromTestCase(TestButtonEditorDemo) ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/examples/demo/Standard_Editors/tests/test_ButtonEditor_simple_demo.py0000644000175100001730000000342400000000000033601 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ This example demonstrates how to test interacting with a button created using ButtonEditor. The GUI being tested is written in the demo under the same name (minus the preceding 'test') in the outer directory. """ import os import runpy import unittest from traitsui.testing.api import DisplayedText, MouseClick, UITester #: Filename of the demo script FILENAME = "ButtonEditor_simple_demo.py" #: Path of the demo script DEMO_PATH = os.path.join(os.path.dirname(__file__), "..", FILENAME) class TestButtonEditorSimpleDemo(unittest.TestCase): def test_button_editor_simple_demo(self): demo = runpy.run_path(DEMO_PATH)["demo"] tester = UITester() with tester.create_ui(demo) as ui: button = tester.find_by_name(ui, "my_button_trait") for index in range(5): button.perform(MouseClick()) self.assertEqual(demo.click_counter, index + 1) click_counter = tester.find_by_name(ui, "click_counter") displayed_count = click_counter.inspect(DisplayedText()) self.assertEqual(displayed_count, '5') demo.click_counter = 10 displayed_count = click_counter.inspect(DisplayedText()) self.assertEqual(displayed_count, '10') # Run the test(s) unittest.TextTestRunner().run( unittest.TestLoader().loadTestsFromTestCase(TestButtonEditorSimpleDemo) ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/examples/demo/Standard_Editors/tests/test_CheckListEditor_simple_demo.py0000644000175100001730000000302200000000000034171 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ This example demonstrates how to test interacting with a checklist created using CheckListEditor. The GUI being tested is written in the demo under the same name (minus the preceding 'test') in the outer directory. """ import os import runpy import unittest from traitsui.testing.api import Index, MouseClick, UITester #: Filename of the demo script FILENAME = "CheckListEditor_simple_demo.py" #: Path of the demo script DEMO_PATH = os.path.join(os.path.dirname(__file__), "..", FILENAME) class TestCheckListEditorSimpleDemo(unittest.TestCase): def test_checklist_editor_simple_demo(self): demo = runpy.run_path(DEMO_PATH)["demo"] tester = UITester() with tester.create_ui(demo) as ui: checklist = tester.find_by_id(ui, "custom") item3 = checklist.locate(Index(2)) item3.perform(MouseClick()) self.assertEqual(demo.checklist, ["three"]) item3.perform(MouseClick()) self.assertEqual(demo.checklist, []) # Run the test(s) unittest.TextTestRunner().run( unittest.TestLoader().loadTestsFromTestCase(TestCheckListEditorSimpleDemo) ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/examples/demo/Standard_Editors/tests/test_EnumEditor_demo.py0000644000175100001730000000635500000000000031667 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ This example demonstrates how to test interacting with the various modes of the EnumEditor. The GUI being tested is written in the demo under the same name (minus the preceding 'test') in the outer directory. """ import os import runpy import unittest from traitsui.testing.api import ( DisplayedText, Index, KeyClick, KeySequence, MouseClick, SelectedText, UITester, ) #: Filename of the demo script FILENAME = "EnumEditor_demo.py" #: Path of the demo script DEMO_PATH = os.path.join(os.path.dirname(__file__), "..", FILENAME) class TestEnumEditorDemo(unittest.TestCase): def test_enum_editor_demo(self): demo = runpy.run_path(DEMO_PATH)["demo"] tester = UITester() with tester.create_ui(demo) as ui: simple_enum = tester.find_by_id(ui, "simple") simple_text_enum = tester.find_by_id(ui, "simple_text") radio_enum = tester.find_by_id(ui, "radio") list_enum = tester.find_by_id(ui, "list") text = tester.find_by_id(ui, "text") readonly = tester.find_by_id(ui, "readonly") self.assertEqual(demo.name_list, 'A-495') simple_enum.locate(Index(1)).perform(MouseClick()) self.assertEqual(demo.name_list, 'A-498') for _ in range(5): simple_text_enum.perform(KeyClick("Backspace")) simple_text_enum.perform(KeySequence("R-1226")) simple_text_enum.perform(KeyClick("Enter")) self.assertEqual(demo.name_list, 'R-1226') radio_enum.locate(Index(5)).perform(MouseClick()) self.assertEqual(demo.name_list, 'Foo') list_enum.locate(Index(3)).perform(MouseClick()) self.assertEqual(demo.name_list, 'TS-17') for _ in range(5): text.perform(KeyClick("Backspace")) text.perform(KeySequence("A-498")) text.perform(KeyClick("Enter")) self.assertEqual(demo.name_list, 'A-498') demo.name_list = 'Foo' displayed_simple = simple_enum.inspect(DisplayedText()) disp_simple_text = simple_text_enum.inspect(DisplayedText()) selected_radio = radio_enum.inspect(SelectedText()) selected_list = list_enum.inspect(SelectedText()) displayed_text = text.inspect(DisplayedText()) displayed_readonly = readonly.inspect(DisplayedText()) displayed_selected = [ displayed_simple, disp_simple_text, selected_radio, selected_list, displayed_text, displayed_readonly, ] for text in displayed_selected: self.assertEqual(text, 'Foo') # Run the test(s) unittest.TextTestRunner().run( unittest.TestLoader().loadTestsFromTestCase(TestEnumEditorDemo) ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/examples/demo/Standard_Editors/tests/test_FileEditor_demo.py0000644000175100001730000000401200000000000031626 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ This example demonstrates how to test interacting with a textbox created using FileEditor. The GUI being tested is written in the demo under the same name (minus the preceding 'test') in the outer directory. """ import os import runpy import unittest from traitsui.testing.api import DisplayedText, KeyClick, KeySequence, UITester #: Filename of the demo script FILENAME = "FileEditor_demo.py" #: Path of the demo script DEMO_PATH = os.path.join(os.path.dirname(__file__), "..", FILENAME) class TestFileEditorDemo(unittest.TestCase): def test_run_demo(self): demo = runpy.run_path(DEMO_PATH)["demo"] tester = UITester() with tester.create_ui(demo) as ui: # simple FileEditor simple_file_field = tester.find_by_id(ui, "simple_file") # Modify the value on the GUI # The path does not exist, which is allowed by the trait. # The custom FileEditor will ignore nonexisting file path. simple_file_field.perform(KeySequence("some_path")) simple_file_field.perform(KeyClick("Enter")) self.assertEqual(demo.file_name, "some_path") # Modify the value on the object demo.file_name = FILENAME self.assertEqual( simple_file_field.inspect(DisplayedText()), FILENAME ) # On wx, an assertion error occurs upon disposing the UI. # That error is linked to the custom FileEditor. # See enthought/traitsui#752 # Run the test(s) unittest.TextTestRunner().run( unittest.TestLoader().loadTestsFromTestCase(TestFileEditorDemo) ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/examples/demo/Standard_Editors/tests/test_InstanceEditor_demo.py0000644000175100001730000000422600000000000032522 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ This example demonstrates how to test interacting with a UI created using InstanceEditor. The GUI being tested is written in the demo under the same name (minus the preceding 'test') in the outer directory. """ import os import runpy import unittest from traitsui.testing.api import ( DisplayedText, KeyClick, KeySequence, MouseClick, UITester, ) #: Filename of the demo script FILENAME = "InstanceEditor_demo.py" #: Path of the demo script DEMO_PATH = os.path.join(os.path.dirname(__file__), "..", FILENAME) class TestInstanceEditorDemo(unittest.TestCase): def test_instance_editor_demo(self): demo = runpy.run_path(DEMO_PATH)["demo"] tester = UITester() with tester.create_ui(demo) as ui: simple = tester.find_by_id(ui, "simple") custom = tester.find_by_id(ui, "custom") occupation = custom.find_by_name("occupation") occupation.perform(KeySequence("Job")) occupation.perform(KeyClick("Enter")) self.assertEqual(demo.sample_instance.occupation, "Job") simple.perform(MouseClick()) name = simple.find_by_name("name") name.perform(KeySequence("ABC")) name.perform(KeyClick("Enter")) self.assertEqual(demo.sample_instance.name, "ABC") demo.sample_instance.name = "XYZ" simple_displayed = name.inspect(DisplayedText()) custom_name = custom.find_by_name("name") custom_displayed = custom_name.inspect(DisplayedText()) self.assertEqual(simple_displayed, "XYZ") self.assertEqual(custom_displayed, "XYZ") # Run the test(s) unittest.TextTestRunner().run( unittest.TestLoader().loadTestsFromTestCase(TestInstanceEditorDemo) ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/examples/demo/Standard_Editors/tests/test_ListEditor_demo.py0000644000175100001730000000313600000000000031670 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ This example demonstrates how to test interacting with a ListEditor. The GUI being tested is written in the demo under the same name (minus the preceding 'test') in the outer directory. """ import os import runpy import unittest from traitsui.testing.api import Index, KeyClick, KeySequence, UITester #: Filename of the demo script FILENAME = "ListEditor_demo.py" #: Path of the demo script DEMO_PATH = os.path.join(os.path.dirname(__file__), "..", FILENAME) class TestListEditorDemo(unittest.TestCase): def test_list_editor_demo(self): demo = runpy.run_path(DEMO_PATH)["demo"] tester = UITester() with tester.create_ui(demo) as ui: custom_list = tester.find_by_id(ui, "custom") item1 = custom_list.locate(Index(1)) for _ in range(6): item1.perform(KeyClick("Backspace")) item1.perform(KeySequence("Othello")) item1.perform(KeyClick("Enter")) self.assertEqual( demo.play_list, ["The Merchant of Venice", "Othello", "MacBeth"], ) # Run the test(s) unittest.TextTestRunner().run( unittest.TestLoader().loadTestsFromTestCase(TestListEditorDemo) ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/examples/demo/Standard_Editors/tests/test_RangeEditor_demo.py0000644000175100001730000001543100000000000032012 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ This example demonstrates how to test interacting with the various styles of RangeEditor. The GUI being tested is written in the demo under the same name (minus the preceding 'test') in the outer directory. """ import os import runpy import unittest from traitsui.testing.api import ( DisplayedText, Index, KeyClick, KeySequence, MouseClick, Slider, Textbox, UITester, ) #: Filename of the demo script FILENAME = "RangeEditor_demo.py" #: Path of the demo script DEMO_PATH = os.path.join(os.path.dirname(__file__), "..", FILENAME) class TestRangeEditorDemo(unittest.TestCase): def test_run_demo(self): demo = runpy.run_path(DEMO_PATH)["demo"] tester = UITester() with tester.create_ui(demo) as ui: simple_small = tester.find_by_id(ui, 'simple_small') custom_small = tester.find_by_id(ui, 'custom_small') text_small = tester.find_by_id(ui, 'text_small') readonly_small = tester.find_by_id(ui, 'readonly_small') simple_medium = tester.find_by_id(ui, 'simple_medium') custom_medium = tester.find_by_id(ui, 'custom_medium') text_medium = tester.find_by_id(ui, 'text_medium') readonly_medium = tester.find_by_id(ui, 'readonly_medium') # Testing for SimpleSpinEditor is not supported yet so the simple # and custom styles for the large_range_int are not included here text_large = tester.find_by_id(ui, 'text_large') readonly_large = tester.find_by_id(ui, 'readonly_large') simple_float = tester.find_by_id(ui, 'simple_float') custom_float = tester.find_by_id(ui, 'custom_float') text_float = tester.find_by_id(ui, 'text_float') readonly_float = tester.find_by_id(ui, 'readonly_float') # Tests for the small_int_range ################################## simple_small_slider = simple_small.locate(Slider()) simple_small_slider.perform(KeyClick("Page Up")) self.assertEqual(demo.small_int_range, 2) simple_small_text = simple_small.locate(Textbox()) simple_small_text.perform(KeyClick("Backspace")) simple_small_text.perform(KeyClick("3")) simple_small_text.perform(KeyClick("Enter")) self.assertEqual(demo.small_int_range, 3) custom_small.locate(Index(0)).perform(MouseClick()) self.assertEqual(demo.small_int_range, 1) text_small.perform(KeyClick("0")) text_small.perform(KeyClick("Enter")) self.assertEqual(demo.small_int_range, 10) demo.small_int_range = 7 displayed_small = readonly_small.inspect(DisplayedText()) self.assertEqual(displayed_small, '7') # Tests for the medium_int_range ################################# simple_medium_slider = simple_medium.locate(Slider()) # on this range, page up/down corresponds to a change of 2. simple_medium_slider.perform(KeyClick("Page Up")) self.assertEqual(demo.medium_int_range, 3) simple_medium_text = simple_medium.locate(Textbox()) simple_medium_text.perform(KeyClick("Backspace")) simple_medium_text.perform(KeyClick("4")) simple_medium_text.perform(KeyClick("Enter")) self.assertEqual(demo.medium_int_range, 4) custom_medium_slider = custom_medium.locate(Slider()) custom_medium_slider.perform(KeyClick("Page Down")) self.assertEqual(demo.medium_int_range, 2) custom_medium_text = custom_medium.locate(Textbox()) custom_medium_text.perform(KeyClick("Backspace")) custom_medium_text.perform(KeyClick("1")) custom_medium_text.perform(KeyClick("Enter")) self.assertEqual(demo.medium_int_range, 1) text_medium.perform(KeyClick("0")) text_medium.perform(KeyClick("Enter")) self.assertEqual(demo.medium_int_range, 10) demo.medium_int_range = 7 displayed_medium = readonly_medium.inspect(DisplayedText()) self.assertEqual(displayed_medium, '7') # Tests for the large_int_range ################################## # Testing for SimpleSpinEditor is not supported yet text_large.perform(KeySequence("00")) text_large.perform(KeyClick("Enter")) self.assertEqual(demo.large_int_range, 100) demo.large_int_range = 77 displayed_large = readonly_large.inspect(DisplayedText()) self.assertEqual(displayed_large, '77') # Tests for the float_range ###################################### simple_float_slider = simple_float.locate(Slider()) # on this range, page up/down corresponds to a change of 1.000. simple_float_slider.perform(KeyClick("Page Up")) self.assertEqual(demo.float_range, 1.000) simple_float_text = simple_float.locate(Textbox()) for _ in range(3): simple_float_text.perform(KeyClick("Backspace")) simple_float_text.perform(KeyClick("5")) simple_float_text.perform(KeyClick("Enter")) self.assertEqual(demo.float_range, 1.5) custom_float_slider = custom_float.locate(Slider()) # after the trait is set to 1.5 above, the active range shown by # the LargeRangeSliderEditor for the custom style is [0,11.500] # so a page down is now a decrement of 1.15 custom_float_slider.perform(KeyClick("Page Down")) self.assertEqual(round(demo.float_range, 2), 0.35) custom_float_text = custom_float.locate(Textbox()) for _ in range(5): custom_float_text.perform(KeyClick("Backspace")) custom_float_text.perform(KeySequence("50.0")) custom_float_text.perform(KeyClick("Enter")) self.assertEqual(demo.float_range, 50.0) text_float.perform(KeyClick("5")) text_float.perform(KeyClick("Enter")) self.assertEqual(demo.float_range, 50.05) demo.float_range = 72.0 displayed_float = readonly_float.inspect(DisplayedText()) self.assertEqual(displayed_float, '72.0') # Run the test(s) unittest.TextTestRunner().run( unittest.TestLoader().loadTestsFromTestCase(TestRangeEditorDemo) ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/examples/demo/Standard_Editors/tests/test_TableEditor_demo.py0000644000175100001730000000271500000000000032006 0ustar00runnerdocker00000000000000""" This example demonstrates how to test interacting with a TableEditor. The GUI being tested is written in the demo under the same name (minus the preceding 'test') in the outer directory. """ import os import runpy import unittest from traitsui.testing.api import ( Cell, KeyClick, KeySequence, MouseClick, UITester ) from traitsui.tests._tools import requires_toolkit, ToolkitName #: Filename of the demo script FILENAME = "TableEditor_demo.py" #: Path of the demo script DEMO_PATH = os.path.join(os.path.dirname(__file__), "..", FILENAME) class TestTableEditorDemo(unittest.TestCase): @requires_toolkit([ToolkitName.qt]) def test_list_editor_demo(self): demo = runpy.run_path(DEMO_PATH)["demo"] tester = UITester() with tester.create_ui(demo) as ui: employees_table = tester.find_by_name(ui, "employees") # clicking a cell enters edit mode and selects full text cell_21 = employees_table.locate(Cell(2, 1)) cell_21.perform(MouseClick()) cell_21.perform(KeySequence("Jones")) cell_21.perform(KeyClick("Enter")) self.assertEqual(demo.employees[0].last_name, 'Jones') # third column corresponds to Full Name property cell_32 = employees_table.locate(Cell(3, 2)) cell_32.perform(MouseClick()) # Run the test(s) unittest.TextTestRunner().run( unittest.TestLoader().loadTestsFromTestCase(TestTableEditorDemo) ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/examples/demo/Standard_Editors/tests/test_TextEditor_demo.py0000644000175100001730000000465500000000000031710 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ This example demonstrates how to test interacting with a textbox created using TextEditor. The GUI being tested is written in the demo under the same name (minus the preceding 'test') in the outer directory. """ import os import runpy import unittest from traitsui.testing.api import DisplayedText, KeyClick, KeySequence, UITester #: Filename of the demo script FILENAME = "TextEditor_demo.py" #: Path of the demo script DEMO_PATH = os.path.join(os.path.dirname(__file__), "..", FILENAME) class TestTextEditorDemo(unittest.TestCase): def test_run_demo(self): demo = runpy.run_path(DEMO_PATH)["demo"] tester = UITester() with tester.create_ui(demo) as ui: simple_int_field = tester.find_by_id(ui, "simple_int") custom_int_field = tester.find_by_id(ui, "custom_int") text_int_field = tester.find_by_id(ui, "text_int") readonly_int_field = tester.find_by_id(ui, "readonly_int") # Modify the value to something invalid simple_int_field.perform(KeyClick("Backspace")) simple_int_field.perform(KeySequence("a")) # not a number! # Check the value has not changed self.assertEqual(demo.int_trait, 1) self.assertEqual(custom_int_field.inspect(DisplayedText()), "1") self.assertEqual(text_int_field.inspect(DisplayedText()), "1") self.assertEqual(readonly_int_field.inspect(DisplayedText()), "1") # Modify the value on the GUI to a good value simple_int_field.perform(KeyClick("Backspace")) simple_int_field.perform(KeySequence("2")) # Check self.assertEqual(demo.int_trait, 2) self.assertEqual(custom_int_field.inspect(DisplayedText()), "2") self.assertEqual(text_int_field.inspect(DisplayedText()), "2") self.assertEqual(readonly_int_field.inspect(DisplayedText()), "2") # Run the test(s) unittest.TextTestRunner().run( unittest.TestLoader().loadTestsFromTestCase(TestTextEditorDemo) ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/examples/demo/examples.cfg0000644000175100001730000000027700000000000023100 0ustar00runnerdocker00000000000000[global] destdir = TraitsUI [Traits UI Uber-demo] files = demo.py, traits_ui_demo.jpg, Advanced/*, Applications/*, Dynamic Forms/*, Extras/*, Standard Editors/*, Tools/*, Misc/*, images/* ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/examples/demo/index.rst0000644000175100001730000000003600000000000022433 0ustar00runnerdocker00000000000000.. image:: traits_ui_demo.jpg ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/examples/demo/traits_ui_demo.jpg0000644000175100001730000012430300000000000024307 0ustar00runnerdocker00000000000000JFIFC     C   " P(@rAT@QDPQ :e Aqt#R *( z/,>Ksi:G8n> dTPE@pJOؚpt)i;_ޣJ .Ik_|vO8r.zg;r5[JrxfWw>o-UQ [ؐqt=ԷO yaR /xуx]5M/~O3\*Ǔ}T߹|V~gzaox]R;5m+'y$_qy9#7P RFj9gRp J}=} /Q>&l_&_ϱ<O6O0&C|͖ۨW4aX|>o>;q˄wj"'0c^Ѩ:;YQz'O^/^LMz>y o*ns>y_9jIξOfa]"M~Ƕg;{&ȸMzƃW!(ahHTߤPQPAADQ@U4+AfsDD1A5s:9+0{D} %Wp0[Nr^w>g~_F}wnj[UAk|on3=vxvk߼|<8%DIr#tWrŘ (((x')b#c5[Ӽ\y\Uc<&n_+> -fa>1F *4\TZw5?Vǩ|nA߰@QAE#W!XH!QZj>֭Б隸eg.|oRZŷV+n9./G<<^?c9*#?=Weݟ.폗k]<=+iPt˰)qMqiz}'+Osb:ܝFS<7P8;C E$ckn>X~e$gNVJ̮ZXVVqyk}Ͻoz_~ I~f~:K+2KQ<P 5zM9'c)}-Vr7oNWO?G#GyЯjZ $dl{ESd?BXssib'ik@ pVoU +!jz3Lֵ'z|1@Q@ĿH*^C*K?/g./P;|wv( $BB[ u+<$l{ګ=[8yÕr}:9I^˹.Xᤍ0D1o= QoP ONpye{aq_929b8Z&ןcy/"_'?+_kr>;y HHt+]U,} ޅSjvKqfEV׌l{qM)9]XFm((kzlw*YA^X!:f\>qZeÙnO[y_S6}GT4z~o;9F84cJJzOƀ.ɞׯ:L'?Gz6-gqUF +u>wq"f(2%IbzgE? f\NiiMy!lj+ῧ*';osTYɩёYQ4aEא *+մ41jR..c?D7"}:^1S+VDS%E.`D="b ~$~82uDMwQ _L^1χ?A d[pu O|=7.QWgYjd-M%/\jʾ=s d  !y' lXN]Y5% N9^HxS+.OWZjf)2EuE䫕k U#ڹTHWÝ=sg-YHrm~5fW 2fĉ7m"ڹ _PNեT_HWz=u]6o^nv+5LKֳ$Y;TȞMvoú2ڝ]mqFe .>b%^ FXhfidM ^E8}Xr~ؙɠᡶ#T݊Zmt\s>i $H!'Tj;}F>9 ֘0`PUs%$/b dRNݥУ&hH!#U KCM-i vS)̚$iu  0`Eb#ֳ)j7QwFpAE_C_Kr`]e+qPa]Hy5 w?'a@ J#0`jK6KK-N圪z"2߈V$d^''bvY]l'erW95J 9?ezP ehsPn $-IL0`i 3*}CIG:KdG% *#墼WD߶rXryz *'kM44Q}94,7 F4<%c|P8 #1: ԅSpM}c.L0`heH.64\܍2IA a?C@?նJq'U!b bDԵ< PxjɍÈ CrLm#yC~&tm"ɵ5߂^ 0`i ilst43(4I)@ldF70G=Yg?rn>ԜRoT+Թ %`ұxy R]!P)$!p8>G֥G11~wK~0T[aT#X<$9n23Hᑸ@dJVd[~+_rBbͲkx}Nz$g qk!a$lѨc ^q)~"  >bs7H'3U4k.pBn-#kqR jCEC^^QdN\c0ˆ70C?rq|5`ĭ%`}L u!3'Ӗ3Do~b{A#  DAD 0}4̭[GoJeІGhl ܉g5!C~׽"_01r?Q.#Fȅu> XfW~,;4*Lu)㓲e>@>DhBӱ b 8 sP(E;p@ D(JN8J7&dH,dNB"B?//D{]_ji0KXn:xI(lFZ[aUrH99(4M?_Ht'Ҫa7@8BE.3a1jԠmw3dbFEx:  DAA@OSv{&X{`z46HLj5D_H^;fR$[h{unbk< kvJ[cY+F"ٿ뛆E\Z!RfjW[:+J%>L@"@ ERq <:T>MH8 fkCjXm@b.(^R$_~if =6&0P1<9W|$A8 m!j O:IC~'tt D" pB7# qJQ6IVChYs?h|OHH:7<0a$vN1HP̥ɬ& BT |-a5 `z@"@  L{<33RS+p5 (Ѵm^ Ye=+yXB,U8D14}S K1xiJȫ_ɳ:w="  1ܺ֍\]OѴmFсs10*0lP?*8G>ޓԲ<CkEo2H.siNI)(! -H@$H,,((0}mbd፪Lp8 @Lt"IIYN3zBMrDo^Y^m QCX\`I6#Scikc%K!3$qi2 @A$AFaF >89@2:7Y#222222&/219^ >oXmP?0d"UH´dfcl)qJ@?b< @BBH$H)AF`ϤFfBWjon> x }Ⱦ}_i@D[E'ډILd)[Rٚ#FRȒ1n!n1)ER܍՟GP?3Mq#{cM> gAn\21wnFn,09ilIim)A ݍկGEA^f:4ۤ8ؓ5Cx7 y$ @BBBBz tᚈl*!^xo,Ϙnm0BbiE~z h#\VtU܀̥?( I&@ʖ01 @ +K2f(0HJz22227 p "GP?#h6h6h\$$$$$#Ȫ/Ϸ#########p2 AF%"z8}>@ h6h6h6 y A!!!!!>EX3aBP#bϽ$ D4&eLfF3ZMŢ1.ʅ7 Pn7MO&NN^Q* TuҔ&rGBiܵNQbWoibz_d&ڝJ&,[(fzy2lmjTHHHHHG_FFFFFFFFFFz2 „FI,_>BڙNDOQP)mD|ʑzzleZ|S~z0v,K\fXK; %xzrۺ[v+sPsڜةsS/zVȐ 9[FܙHHHHOje}1bS`0H2222 "WϬ@"@ IIf(n6o*Ę/D= ܛ)r]qm~A$HIp8 Xä7CYlL0dRwKRQ(ֽjA׹FnĤq޲g2l6= @I23. D 6h6H4H2AD 2@ Fa&H @III$H$h6  @ `0 16!01A2@ "3PQaBpq$R?lߓ!'(M4=1s4rߩ&&s"aW=- ?uτh|' l> S_Uo#fV&p:ϼ,4)<^|I*C?Bݺ2;sƭ naS \F튮p4?{-eqyf8ò}s.S O/[Tnu1ݸN!eemtܰݵIrz7_V7kOji)@`" 7ibdk BrWUOiqf&Ld͗u*.Y󍠄g}>?@ 51 9'OJov0p)vA("`\.-[QO;R$QRlv:v$}SQ ݃nC~S ,R?ij$o<6@ -c]]-&BZ_%7 Q݂l9Ag5C"sXԪ*sK If.^JYz㯰ut9oFF~sL!b_vXKw ?t*ˆP=,CVc,ǞxP/tâ7`? :X8=]f6Y7ff b24YxiXk,֬֡#I bڃ6B6-Ph, >UZ llbmM1{Y'ebwB#t" t7+(u~Qe~SXZK !1AQq "2@a#03BRbr$P`sC%t4Sc5p?+ƪ!ii ($c p { '3>$xQ&gPڣXe&_;Ģb*J!*^;qZ8:8FJ1a~-pMk$ b6z|qn_٢l"=[5s({VbGcP20 lp1`86({W|׎IڤHE3surwHAekwYwuF!4Kk7P\ SbSZbc웬lE-$fI?* mCiCgyqOW^ٝ?~y7d݊ROIpcܥ-+;[wxR>,A_")-x՘BM JZ]MS`T.3}q*WsV _j=O g I(n2Gp[mFZwV5ͨHMh)xVXJej) 5QKgx\`ɮ uЃFCEPbJ='@R Z]vA"7/KWSbn(ֿ1ax_M6bP%{3B#loN 6s7(fuŦf/R+FbZkGcc|OGM7븊^աJ[=׮%„7ruGus+,ӱ/"#Uqfҏk;}VlzGWJإkspDNAZsl*]pTD5&]T1A+[ ƃlSe$o(x৔J9Һ2LNUB[$J1qT 9语"N$C@'@VZ"WݟQ7>.U:EJc%}WjكG?hw44j#8zn`̠Ƌ@ BWQtܭ$S0INt g4{ydX8|Yyvc\_pxel 6F6HjہP63]*>iB0B8]zޑ̧lhv7]+_ ֞l ) \>hxNE o(XM Qvy1D#cI!YL,)$M'3>qӑ<cFvUcbK;J'1d5sgD}[m缫Y٢m4ڻӂkȢ$6 ;ޢXҊ[Y.$V8(c+]f5*Y]Fm.k6;lrIbV=E);b m_Gf(ƍ jVGb3`Naʺ]ho @x&\~ӒgZr&g9U@kbVv~hLyK1o`;/FO V,pD>Pj#C]68d|ʻj-o̮g8%]n.U骣:#7&;di Mci+T&bȪ:,Lυs5p'+j,l!ʕV"k ?se;,J 4~@ RW+8wr v,fN@T5E9@B?Eu>A]nwh!wU;՛Y#tڎ+=-_U.a .Xk[c!A-^r%{kޱ?6Wʠh7)V~ڟ[*(N+/q{̓uI(rNA`Y"tؕ%bWIt8 ;i!cu7Od`fScnc}Kz ϨOnz W4~#)Ggb6N5q\Wwr 䎯޾($8R _RANKT@O s;?tBxSkyцW9Xի XcABwWωޥ!`A]l1 #8#`s8,ǬD=촕w9Q>Kr*t SnWq4L|åsBkCNEQ\lN!s]*hs-W!s>U Kw$+綞}l:4\z}߹;{IRQ5EvX/kFDiT^CUlDj !>i2<ԉ.ss)P.\v87*gpN$Ӱio q-j °̴GsqشkV"ek_[X8,]x{خ| 8+M5O\= ~Mj wkϑ^BCəwhGf GwV(lߕɗ[dWyRڟT4c74̽[`}Viy \nR@5ADvO#s {XUlakEͰM`~Or =Ǩ4{!= uZzʧy1 '@'8%;~65,#`>k/ߚƽXNpsn3HO衳ڝlopUuīL|AC&Ql& zіՏ˗:\$yN^5:`eO-?}>Eۥi)9. ʋ m9o|>.A yirw8|h4kA-vU9o$ΡrcNEӠYr>~>}F],XaK'v6֓`ĕYP軔xU^pv ȡqgE' ^N"wH#wnۋrhSl~gllދc}MtՑYe^N梳.ah4̦Tԡ%4 =wWIfŒ.[4Q]wj.C*>cބu0GY ]mbCm뒻]h MOVH;tr + &5ϪSދoJ(3;"-2\Uޙjqiλx6*[C BP ,^eh@7\j6L| 3V;,Ki"^+%eײ^pHK\i[2guڢ(]KԝǪ7T]s] aEe ڟVfD^ˆ7Ҙ$J~9y7SF#j7je42FSbt,1Vsty/aLT"l7pQ6kQBB#7x%"Q5вWCv'DzMiwj39@k5, b7􉪞Ӫk"Cw(L Ǧ,< 77zYl`n0H>bsVF맆4SG}TY&ſ/O-ص2cޛeȢ-{K\6#r9sM,+%2d~pʊ3⥜ 3ª~%X-(#G5-,jUceE5"/k5eq"S{0ˠ!l@nR^migy&m/kM IaܭRj#]c/wjXIs mG5r}b4v3dkMmSf\Xܼ+ "k\#tN*L݆\ddgUhH2 %]%d*Cر{s49 ]qN 69sF\M{P+G?418d^7^oo\Zwzob]Ī %9ANvx恾({\s^Mghq?+!1AQaq 0@P`?!_ZX]PFt\r-,ݼ%`*f$4f[xG Vl14|w#35„53-e@BF6%|g|Q1&Cpfϳ;lr6Nl10h/^u%vtDin1gBR ]!)|k(>GWiB+Uh{ Ff]j#}=^3͓oDSU GzwVŬlS]X?G[Iڼݦqp]1~kMZnާf8t¦\` W"-ҧܢm o7 "ѱkC\pʥ!11ݪә,%yCh4?ݣ o0Ǝ+N.sw ;L$5͌xg0vy79 .PNsYcNc{NaGPl L$a,CQSEDeD鬹~W~_ÑЌ6JY8(]C;|ׁƸZJ!+o'u_kmt2H`6r b$X[0 TիLnZ3PU@sT \OG1z k]RҲ/G>O6Um~׏Tlf6Et"]aekU   sܥ zCϾQ?+T\GH ց?ى>\nvTۇ\˿{`71'X_)VS8_JI]F&`*ig#uOcң,#lF"t09%ZNrZ i&8^[{ ȵ0 2=aHPT7Ku"ݒU5VK=Hј@] PŤ\pNRun !MJm65G0`F{&?BŠ"4@ r\%._w ҪN.8![Z`XCWnVq ֲ0IvW`u:)6< \on9'_rhnTVadv+2Yذ-e{R+Gyث?=9hSI"ԊȨ"F}~}fu-PەO4mbWɼ*9IAڍeAXkWUr @#\KJk=k~ s>Ăө_F]?~ -CeBΥd -r?%JYB" 2m'EϬ~Q)r>|%pAiM(tztcgR$HeV^J],0[sDӬ$tvscĴ|_[tBV7s"+42<7s2{  <*dOD/,6 rl7>j[8jגhW]a؇7 ~ܾ\#*Q݈{M5>d%hYJBl]`f,Yx4cˇQ ׿X ~Q"6lzx=.}▪ݒwESOld9u3^N7e((2j2x:"҃4ūTkh0 ezR(@ W,ueJ-]+v' q0LBa{YvcEA1CȺ@XN#cZůE|LVw֮?\puLiQ`'=^lEym1)NА4kmҪfLsv7&m0gkH3t2Z};?k/0̄H:@ԏčhy@QkW'tWR<3M{?L$c΍E1ʳ|L c\+yccsk ny?+^IEg`Hq/2ĥ[o)S̓{n>s5%AsDy) 0qUZ>c>D,! ~5h wy*m|MxASGjbzBT2q_<7u/j rr|8ǎ|BPNHR .ksdr!Y~zc߆MpCHy`i %kCX(M"6fL~~xc}cE.tJ?.hs5k;2D>:p/W=@ hf}u2mr 憭{4WcʓcrCե(F/?Tp9vXYZ\K55¥xcכ~>z2[xWme Z-~9ARy3K湱j 10)wmE]qbS1q#>y|.Ȏ.CxIbJ&'?H5dR 6wOY7YuI?S=6̓Sٖ1I;kO}ȤMz#C)44"kqv?Jpx/;JzbߜYXM2gFyg%b :3L$.V=5Z0,A% faņ/Znδ/.% 1W~P5߁7 |,@ Dgɫ>RJμ弡qhJ)]br~:ܡyATyJzir%rf?<<ÆI~[уQG&ӝ@gd "&oTOH5mtϴ}J @#DžkgNF7h:uD+L "i-trS |;, }UF-N,% yLsX uEyż,qs+cǃVRLC `,*aA͖sv K!ΒguD~fe I_̋=x;!j)w Jj!4,vјJ0A*!p<:[̝Q֨QMY`Q E5F]XRq!> A7^8\ɯo uM#+S350K`}\^x)O f_ @ xQc)UL'd_ }*wW MJl0LgYNՆ3=(x%LVt3eù~Ox#= :.}j?,5+1G۴p[`p+G)>pG!xLJS5hiVEu;@.uH0@9oⲂA [5fcG%q'NFߪȔm߹H5rb<~R@a<:?\}>Qa@{c1K!YU5e|#]CGhzYWt8 J JYnzNħ>S)d4s;(h~;h hu~d <:/ s _vU|9ߋ39W)CuBeuʠ@<@#1:>՚IfZs lE5j%!(sv/&E=ZF<4 K5#\.PP2)}ȃsH6䛿Zz}yxN?О 1rPHUޮf.!K+[˙2ue=n}kXhʈz<*tj YWh:n fT] H8EM5#xH2&yBELr^z1@r::zYj/bo3 \jlrJku`@NCH9YL'5.\qL&)hq:NШA8PfL䕸s15G9~G)>}MU?Ϗ  #yy¸G"84գЉ4JQqud)T>%M%P>՛5y{П}}&v}QD0 qE3?k)KlOcQk\L{Rŋ.\w5M <ϳ&iS-x_ܝ=b|mOsfr2h~*TIL7;yAOvP&2%q0 H/h^\k;J\d!BRu-Y/>x<蚱d/"8UL ėS)4;%ԁ'} yw/! a>^  !{~ XS3^Yx#|l9%볩RJ*,ޒ+4K; 6F2W{wCnRyCJ@AaaVŏzٔtw7=.eK*TR,B# y:5&SVayChU:*TBa!,XX: S"0b/65c;4c_f38S5 J*T^8ǖ9OHי5!by*T\!0 c,b6m6+Kc{y,*XL|/w4RJE^:KYqRJ`,XxuuHV[){*eq"KB_3Yn4_fbZ! = TAǑP򊧺WU83M))+="TR@A0bǍoNE4Hm{43| ,O{&;hypHqH&G $OavT{SyP YDI{gJCuMIt.-S}o{7E3,QT!A$IeJ*egFY|5pv B<H 0,*T>q;2 ?Dqf,O/8 lj`iZ4?+eW,*R2 e4: /\&-HHX,^X=ǻ_(ۊURVc+,<ݣ {NpaBU ht-}b;U}br*"aa6F[=#WXwX ]%ȜcFÜ{ܹhROhr}AZ9<&8~9~!G,a[ AB.d9Io_y40mȀr8QϪaX+-$,A,Uˡ+%uXckK")G`!c+o(r Xk6g8T& <0Vͽصh4 ٨"7*-MCq:~DJ*EHΆ-t09g*+(?V](jM E%]0,y_G7!>_’AI XmN;"NQ&xua#aLLjn&f:HB6MbD8/Up39sBy7 êUa Syc @<5nn]z7KUnufXS45yzB_QY +߼eYD\h-`mM p+X-/lD"Ռ[{#.)tEg-w77buJSJyGT*}Ew6'A L9{6βЃhٙǧ^̩@*6rV_ (]S;ah'Jo|E!xjZ*2~zGiPen-αLolR" E).F긯\yꯒ* Zkc=m\JeC[Ѣ Ct;hsZPmOsUUdE2Hp(pAԡ^~j#!8a`*q?LBb?  {{k$ᄁ< oϼ3Oo{Lyooq> z,2믾(>Yon8IZ8҂WPᆱ->8ӏz曆ﺺ!  boqH>G]~,n8/;᠞Jk850 M>џ@hbl?>~8n{HY(xeSlGZ8 x//+ȭ_A kŔgT8g싿yF3(i_YBݵoﶺ<~x&* AkyBG#`*,nm3c4p6Nƥ46+[!\oWN]9ݫe")(P6%rjywnj (tD"סZ"Hb>~i†c8!. @痱亗/S?&xq8*{x M5-4%+螣3vH 6b۫AXH;)k* 4TP #{gaYnXj.l)Ӊ " h{0G*9/U[<_rz- KAO*k5oI-/{b{k' 1EȆ)㢢[gՂ[[>9c*b.(/*!10AQa @qPp?1# f~ ט9} -o_̝>7[m+Pr~u|IGZeV8hލY=b hk+31;쩳aG״2h^ f8뉘><)N>'Rkf ;y%Vˊ :Sxm16V//ٔn_oK7⭄vb5qDT9NھDeDl_G[^UQ0|_*߅c0&[,WKGQ6 :Ap |ܨ*?,Mz#qxO8q3Wn g 7%0\-zǦ`(Rj[Ct~x.9#ݕ c(c\DZTFa)a SK7x?,`BT]G{ond A cT5@*DUsӓDj*_(7qFep[P'W)-Q(j܅fO Fʉ\V\åhNK^C&JMƠ-I[OMK}r˗.\rr*T11+Z%J*TIRJ*T_*!1A0Qa q@P?ᅉJش\ ʈK u 'Dʵv~&KHWł2M9p @ 1G j"YIL°~{v`:~ Ak_Ή Se:3drƨ0J%HTh9~?sNbQVe0tX!B-e :62`}ai* 67[X-9Sw) {#^f_GL%/ eX`/i]!bUNg Iv\Z@6Zblo2L@D Z7t୘bHwADXV@*b6e0@ xĮfϷ7hGpiՌk'k-ՈH WId,[jr|b[7~ aq/{*'4X1`."m4;ndDjD\Ϝ(5 ˗._p=tRkE`e~_ НbރGBxKbbh (n 1tl,~HifuSFWzژie˹=*eVadԆ6F]!":5^Cꔴ@؃`xӲol;NfM2Lr%'Q:i"\RH2 rXF[j?FKq%Wr_ R _;һlxTSFQzkӉ3QJri+1jp*:1A<ਣEǸV'/qq_Ȝ` x\uU+j Y[ 9~:ifKm )+apnm]?=7p̻mMȵ\)XPbS[x|88Ғ1()bDL Q8'f}j̏#uNL Ym6XwExwO^Q%aڐ@\` F;SAjz69p҉vDU~4. U#( 0x[ouaQA|..@<"%NyI30U78/(%+ZY۶' Ba_.B2#V˱ %rxFfP9^N` \Ro _p.$/P)o=D F0YPZH"L5߬OTEE{sɵ%]rV",(٧6ٓHrin)mB[ 'Q 7W?.z\K7V"WUno_HEJzWNq-h+RrPO,GMX= .UkV?o PͮS+ WK:#)Dcˁ]`HL3Cx; crZޓfT$ ha߈OMwbBC\yC!bZ[2B0gg8ѝ0YA'd?:w ;eѵ༼=LTMKA*&Tl|ywvW~=(]).Tw̗OPR{UýVHr+Fu6(u-Ӄkm:W0Уye49xm*1{s7CvZ[@yYH29X+XDڻ]F?%q H³WqDk')~PĪmz/xCNrE {g9Զ峺r< U+IURby Y7A0T.AcP^X&BV.&Ә ŧ,B.Q'rl9MWs\+s靜jJ Wfn-N1@dy+Tz80yB˪dID ]dE.% ߬JTsIX J -[$e'x$bx/Mö&Jٲ]#$RUB뤤[Ӄ ^kMit҄L!,Mw&JicX @ڈhGRaT; k ##"%@trr\6&z7ei >bb/Ơ~UPR"$ޗԴlmlIj*@*ijIOS Slڜ<0#b]AĆLˌ" !13-&/b@(R"2dȬ/PV4bV˴i&R`r`dM Ov]W3%P#92k34ac{&AyU- 3>aYMAΠR;N/n°4ǜDZK$Q-x4CK]&A1lhڐ(U]ePCe3XI QWPSaz8PȀ% G {XJmUB|rû[_C͞[NN~ҌPm" |r t;lT¤wTi}2/?CN1e[mH ޻;1WG 4,?DvU7{wUKEy^F/e L' "eUXop .M)QtKbQ%=]wpU*w(Xeux؁1x0bFX"E=m È~blҒN̩rz%F[h4֎XWQ6vӛ6B|,:œPN"D^`ø~\( @_y Dh<ΰ3J9Jը| p*kt .)ySN B6.qWlBޝqv>pPJ5QX L0G@]CLG00)d+S/f.G Պz&9B݊PŭTo&m(_x3cFSnب`+ۦ>Ѻ~z1OYA]TC@0 J1LvoDxmp .Aq-Ÿ|hgfV \7Esv]ݹUվTM:#z+r65zwׯ{WB8ap,fXN8C'_w7XK2!gqfloԨ^%-MWb=e_F C@<3o }1M [ĢyYe/ Pj. .;{w_ua}3~lZОry"\yc:<Aq(c:G"œBt^F/fDۗTP)()`Migr/Bؕ.Ƭgm3ATܼx|}"r\㵽ÉKڡhZ!Uyr+,uXRis`QY]bꤹXĭ58F -y}7.N Rֹe]Ee ᆼ % \ A^X6G׈ukܿӝ`)E+OcKqAx DהRQp](K_3۪%dZ^![n>Å96}0"?@.ýA(P ǤC#Q:?!|zFmk!qrI};LtyK(5l+uQT׼Y5+:-:a߈) h=;BP.KGP 3>RAUeZ|+ZAlJ@ :#<@ lv,~+sNyiJ-* '1NiVe _:ԣeغ6=]2;scqeе{@L50=w,E1Zf"ƋJ_h4Hkx@n+g3GA਩mn5#,PQ3pp蜒(1DQѓ{T(< HdÿC+98Ɔ(sS 2 x'q>7!Gy>#-aɋ!O)μq =,(?W t}ԨߣF9;CoA؛VV- @7@E|  v*oG1M%*P_)53k@UB;^w &X Dӟy+߻MQV^hh fhQ+)CoO6;r +#m꤇*Bi-kv*9Mq8"ދ|A6[})oflm;IUV`S7*1zkڍCTbrʨIAk9Juޓ|\RsJ_ B*%=?3Ho&ٿ &f?r &} /f"3t,װI 7R)!pEDdT*4oy#WվxI*%~ px&^-_2>LF)V4"{ETC˦R=FSX?\ȕ ?HP*[՝uh ol.0 :z41v-˻'5Y&npq /%Y^j3fc_\k[Dea{\^wg*TA^),vJ ^$ E+AGuIҭ׬|x.l'GUz|?6yҬ.+'+`dwNC_0Rx-.%y5-fY}f(0UK~%)rࢇ[.-Pw@(X1j7|u,7Gc6/ᄥJ꒯DK^FoUWx !?\u310E͎t`'rI~}F߸>\{̊|2D٬kS ߕKW2#kAy ~S HӒUKߒĺ`fʘ^洽kSUpA.6Gv_U/ kϕX&7,\ TrNh?Sˉm{h *|'D.c>C6C WOwcHepz:2CgAVN亼r=swH FX3 w|P>ٸ;s|8Id^!Rf > a.@~JfбF v ƹ$x_ݟk|2Dz-|ÐҏaZ &ZLj.^l׉Yx(pf({q7G>jO zt O1w'{QB}C\Y8@raP``Nr@_ -iĦŗ{|'wEbM+JݟSOEhmS EwS6[lk4 ̫G펆^)mݧ焣x#C8AYʓ얻4o#+إDǤ5S +@>l`N a@ga(lSsygU燚zKW5H'1EbqMkB\R(OqKs~8zf}MW tEVT=(HVu*'̬&F%N3ݎP F2Mi+Ἰ<ӼF W ઄7 GFjm(wVv ;*(u6O5D g>mXLBV }[rnjt)Kjhr9/V{hUF%w5~t[r bP wd\̢4oo^ cŢZ$7F[ǜ.d|Ɲ TTL1!(J;dc-W)&^LHe Q-ZҖQ OZ@N\/!P˃(F%[yPyt[W|)\3 A{}"}Pf pq}s,5pwfm/ Ew2bO=)+"j<\2T:,=c K/ .=10G^ rgk*E~7ec!r\;%@Ԍ)Nw6DpZkfZ:W"k6Ev*L|R8_ҁqGlvg-K[WApYS\5Mm[r|S KiROa !qp5CXr*6:@ǺyǾ6'WKVbU`|p K2/͖nppp Ρ*u>iQ[ 1 Hv=Y݊sYLJn3C<# :38WEL}}-k/B\؜ΟN|/KJ'^ulĉߔu; _߷]913neٙ cYGl @.4N̹C-M5P sݿ]zr[l  Rj#@a39',0뇓Qc ۅ~J nA&nTDVg>L_9D:ʜNYrL+2Д>X2B~A_۝؇d=pddav/F (a 䜼nYan-\qwgrwc{u9m/cce"bx&?0;3ĭ;l{x=ٝڕr,rEZ&0n,Gs⬲Fpfٝ=:1).!vKwhyc u#2Cͫ VE+l:2DD Ǥ Uc8l^@CzDnt/`EsԳ}~![3@4=t9`=۳%TȭsU(ťJcCExp%P8‰&Kj/3ʒ_~<23us4Jjifr-Av(`' &#=gGCHCܜ}`g.Lq SʌxV~]Kk:щg)ڎcNz|6^ϫ% dEە`pDA-nvjn N\ c TkZDӼs>"J>%MMs\ŭ~(v ?D# ?2 T?l4ɇ03K+|<DBZՙ h8m)d:ǾMBMZElHDd)B])2sНՁ UL_{,/ Q@ΘȾ Ff=ӭԛ)s%@['9Yr$]0N-X-jQt+Aj-:̠TjpJa4Z$s*yA'RwcD@M` )h}2J5ǻUX C!3 JE=t DZyb%s_T4qs;ЛJY~sJq=7*RHܱC\4NTLMDFgeGRR @%u4'ùTPT/.,`XF* tzbF?oHpt{BlylOv%[lݕl_7sm/3u-ۢ).[:\@(sG?Vv&ΑaźvaZ5Dՙ;ᨿ+rJ 0AW7!N8=ڔ,= $"g<Ş5wg?. !0A|++sG;2ŮjvNw~'&4`M\s././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1684768028.087807 traitsui-8.0.0/traitsui/extras/0000755000175100001730000000000000000000000017337 5ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/extras/__init__.py0000644000175100001730000000000000000000000021436 0ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/extras/_demo_info.py0000644000175100001730000000173200000000000022012 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ This module provides the metadata to contribute example files for the ETS demo application. """ import pkg_resources def info(request): """Return a configuration for contributing demo examples to the Demo application. Parameters ---------- request : dict Information provided by the demo application. Currently this is a placeholder. Returns ------- response : dict """ return dict( version=1, name="TraitsUI Demo", root=pkg_resources.resource_filename("traitsui", "examples/demo"), ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/extras/_demo_legacy.py0000644000175100001730000010365200000000000022327 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ A Traits UI demo that borrows heavily from the design of the wxPython demo. This copy of the demo application will only be maintained until ``ets-demo`` is released and distributed. This allows other ETS packages to continue to be able to import the demo application until ``ets-demo`` is released. (enthought/traitsui#950) """ import contextlib import glob from io import StringIO import operator import os from os import listdir from os.path import ( abspath, basename, dirname, exists, isabs, isdir, join, split, splitext, ) import sys import token import tokenize import traceback from configobj import ConfigObj from pyface.api import ImageResource from traits.api import ( Any, Bool, Button, cached_property, Code, Dict, HasPrivateTraits, HasTraits, HTML, Instance, Property, Str, ) from traitsui.api import ( Action, CodeEditor, Handler, Heading, HGroup, HSplit, HTMLEditor, Include, InstanceEditor, Item, ModelView, ObjectTreeNode, ShellEditor, spring, Tabbed, ToolBar, TreeEditor, TreeNodeObject, UIInfo, UItem, VGroup, View, VSplit, ) # ------------------------------------------------------------------------- # Global data: # ------------------------------------------------------------------------- # Define the code used to populate the 'execfile' dictionary: exec_str = """from traits.api import * """ # The name of the file being looked up for the description of a directory DESCRIPTION_RST_FILENAME = "index.rst" # ---------------------------------------------------------------------------- # Return a 'user-friendly' name for a specified string: # ---------------------------------------------------------------------------- def user_name_for(name): name = name.replace("_", " ") return name[:1].upper() + name[1:] # ------------------------------------------------------------------------- # Parses the contents of a specified source file into module comment and # source text: # ------------------------------------------------------------------------- def extract_docstring_from_source(source): """Return module docstring and source code from python source code. Parameters ---------- source : Str Python source code. Returns ------- docstring : str The first module-level string; i.e. the module docstring. source : str The source code, sans docstring. """ # Reset file and generate python tokens f = StringIO(source) python_tokens = tokenize.generate_tokens(f.readline) for ttype, tstring, tstart, tend, tline in python_tokens: token_name = token.tok_name[ttype] if token_name == "STRING" and tstart[1] == 0: break else: # No docstrings found. Return blank docstring and all the source. return "", source.strip() source_lines = source.splitlines() # Extract module docstring lines and recombine docstring = eval("\n".join(source_lines[tstart[0] - 1 : tend[0]])) source_lines = source_lines[: tstart[0] - 1] + source_lines[tend[0] :] source = "\n".join(source_lines) source = source.strip() return docstring, source def parse_source(file_name): """Return module docstring and source code from python source file. Returns ------- docstring : str The first module-level string; i.e. the module docstring. source : str The source code, sans docstring. """ try: source_code = _read_file(file_name) return extract_docstring_from_source(source_code) except Exception: # Print an error message instead of failing silently. # Ideally, the message would be output to the "log" tab. traceback_text = traceback.format_exc() error_fmt = """Sorry, something went wrong.\n\n{}""" error_msg = error_fmt.format(traceback_text) return (error_msg, "") def _read_file(path, mode='r', encoding='utf8'): """Returns the contents of a specified text file.""" with open(path, mode, encoding=encoding) as fh: result = fh.read() return result # ------------------------------------------------------------------------- # 'DemoFileHandler' class: # ------------------------------------------------------------------------- @contextlib.contextmanager def _set_stdout(std_out): stdout, stderr = sys.stdout, sys.stderr try: sys.stdout = sys.stderr = std_out yield std_out finally: sys.stdout, sys.stderr = stdout, stderr class DemoFileHandler(Handler): # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- #: Run the demo file run_button = Button(image=ImageResource("run"), label="Run") #: The current 'info' object (for use by the 'write' method): info = Instance(UIInfo) def _run_button_changed(self): demo_file = self.info.object with _set_stdout(self): demo_file.run_code() def init(self, info): # Save the reference to the current 'info' object: self.info = info demo_file = info.object with _set_stdout(self): demo_file.init() return True def closed(self, info, is_ok): """Closes the view.""" demo_file = info.object if hasattr(demo_file, 'demo'): demo_file.demo = None # ------------------------------------------------------------------------- # Handles 'print' output: # ------------------------------------------------------------------------- def write(self, text): demo_file = self.info.object demo_file.log += text def flush(self): pass # Create a singleton instance: demo_file_handler = DemoFileHandler() class DemoError(HasPrivateTraits): # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- #: The error message text: msg = Code() # ------------------------------------------------------------------------- # Traits view definitions: # ------------------------------------------------------------------------- traits_view = View( VGroup( Heading("Error in source file"), Item("msg", style="custom", show_label=False), ) ) class DemoButton(HasPrivateTraits): # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- #: The demo to be launched via a button: demo = Instance(HasTraits) #: The demo view item to use: demo_item = Item( "demo", show_label=False, editor=InstanceEditor(label="Run demo...", kind="live"), ) # ------------------------------------------------------------------------- # Traits view definitions: # ------------------------------------------------------------------------- traits_view = View( VGroup( VGroup(Heading("Click the button to run the demo:"), "20"), HGroup(spring, Include("demo_item"), spring), ), resizable=True, ) class ModalDemoButton(DemoButton): # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- #: The demo view item to use: demo_item = Item( "demo", show_label=False, editor=InstanceEditor(label="Run demo...", kind="modal"), ) class DemoTreeNodeObject(TreeNodeObject): # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- #: Cached result of 'tno_has_children': _has_children = Any() #: Cached result of 'tno_get_children': _get_children = Any() # ------------------------------------------------------------------------- # Returns whether chidren of this object are allowed or not: # ------------------------------------------------------------------------- def tno_allows_children(self, node): """Returns whether chidren of this object are allowed or not.""" return self.allows_children # ------------------------------------------------------------------------- # Returns whether or not the object has children: # ------------------------------------------------------------------------- def tno_has_children(self, node=None): """Returns whether or not the object has children.""" if self._has_children is None: self._has_children = self.has_children() return self._has_children # ------------------------------------------------------------------------- # Gets the object's children: # ------------------------------------------------------------------------- def tno_get_children(self, node): """Gets the object's children.""" if self._get_children is None: self._get_children = self.get_children() return self._get_children # ------------------------------------------------------------------------- # Returns whether or not the object has children: # ------------------------------------------------------------------------- def has_children(self, node): """Returns whether or not the object has children.""" raise NotImplementedError # ------------------------------------------------------------------------- # Gets the object's children: # ------------------------------------------------------------------------- def get_children(self): """Gets the object's children.""" raise NotImplementedError class DemoFileBase(DemoTreeNodeObject): #: Parent of this file: parent = Any() #: Name of file system path to this file: path = Property(observe='parent.path,name') #: Name of the file: name = Str() #: UI form of the 'name': nice_name = Property() #: Files don't allow children: allows_children = Bool(False) #: Description of what the demo does: description = HTML() #: The base URL for links: base_url = Property(observe='path') #: The css file for this node. css_filename = Str("default.css") #: Log of all print messages displayed: log = Code() _nice_name = Str() def init(self): self.log = "" # ------------------------------------------------------------------------- # Implementation of the 'path' property: # ------------------------------------------------------------------------- def _get_path(self): return join(self.parent.path, self.name) def _get_base_url(self): if isdir(self.path): base_dir = self.path else: base_dir = dirname(self.path) return base_dir # ------------------------------------------------------------------------- # Implementation of the 'nice_name' property: # ------------------------------------------------------------------------- def _get_nice_name(self): if not self._nice_name: name, ext = splitext(self.name) self._nice_name = user_name_for(name) return self._nice_name def _set_nice_name(self, value): old = self.nice_name self._nice_name = value self.trait_property_changed("nice_name", old, value) # ------------------------------------------------------------------------- # Returns whether or not the object has children: # ------------------------------------------------------------------------- def has_children(self): """Returns whether or not the object has children.""" return False def get_children(self): """Gets the demo file's children.""" return [] class DemoFile(DemoFileBase): #: Source code for the demo: source = Code() #: Demo object whose traits UI is to be displayed: demo = Instance(HasTraits) #: Local namespace for executed code: locals = Dict(Str, Any) def init(self): super().init() description, source = parse_source(self.path) self.description = publish_html_str(description, self.css_filename) self.source = source self.run_code() def run_code(self): """Runs the code associated with this demo file.""" try: # Get the execution context dictionary: locals = self.parent.init_dic locals["__name__"] = "___main___" locals["__file__"] = self.path sys.modules["__main__"].__file__ = self.path exec(self.source, locals, locals) demo = self._get_object("modal_popup", locals) if demo is not None: demo = ModalDemoButton(demo=demo) else: demo = self._get_object("popup", locals) if demo is not None: demo = DemoButton(demo=demo) else: demo = self._get_object("demo", locals) except Exception: traceback.print_exc() else: self.demo = demo self.locals = locals # ------------------------------------------------------------------------- # Get a specified object from the execution dictionary: # ------------------------------------------------------------------------- def _get_object(self, name, dic): object = dic.get(name) or dic.get(name.capitalize()) if object is not None: if isinstance(type(object), type): try: object = object() except Exception: pass if isinstance(object, HasTraits): return object return None # HTML template for displaying an image file: _image_template = """ """ class DemoContentFile(DemoFileBase): def init(self): super().init() file_str = _read_file(self.path) self.description = publish_html_str(file_str, self.css_filename) class DemoImageFile(DemoFileBase): def init(self): super().init() self.description = _image_template.format(self.css_filename, self.path) class DemoPath(DemoTreeNodeObject): """This class represents a directory.""" # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- #: Parent of this package: parent = Any() #: Name of file system path to this package: path = Property(observe='parent.path,name') #: Description of what the demo does: description = Property(HTML, observe="path,css_filename") #: The base URL for links: base_url = Property(observe='path') #: The css file for this node. css_filename = Str("default.css") #: Name of the directory: name = Str() #: UI form of the 'name': nice_name = Property() #: Dictionary containing symbols defined by the path's '__init__.py' file: init_dic = Property() #: Should .py files be included? use_files = Bool(True) #: Paths do allow children: allows_children = Bool(True) #: Configuration dictionary for this node #: This trait is set when a config file exists for the parent of this path. config_dict = Dict() #: Configuration file for this node. config_filename = Str() #: The css file for this node. css_filename = Str("default.css") #: Shadow trait for description property _description = Str() #: Cached value of the nice_name property. _nice_name = Str() #: Dictionary mapping file extensions to callables _file_factory = Dict() def __file_factory_default(self): return { ".htm": DemoContentFile, ".html": DemoContentFile, ".jpeg": DemoImageFile, ".jpg": DemoImageFile, ".png": DemoImageFile, ".py": DemoFile, ".rst": DemoContentFile, ".txt": DemoContentFile, } # ------------------------------------------------------------------------- # Implementation of the 'path' property: # ------------------------------------------------------------------------- def _get_path(self): if self.parent is not None: path = join(self.parent.path, self.name) else: path = self.name return path def _get_base_url(self): if isdir(self.path): base_dir = self.path else: base_dir = dirname(self.path) return base_dir # ------------------------------------------------------------------------- # Implementation of the 'nice_name' property: # ------------------------------------------------------------------------- def _get_nice_name(self): if not self._nice_name: self._nice_name = user_name_for(self.name) return self._nice_name # ------------------------------------------------------------------------- # Setter for the 'nice_name' property: # ------------------------------------------------------------------------- def _set_nice_name(self, value): old = self.nice_name self._nice_name = value self.trait_property_changed("nice_name", old, value) # ------------------------------------------------------------------------- # Implementation of the 'description' property: # ------------------------------------------------------------------------- @cached_property def _get_description(self): index_rst = os.path.join(self.path, DESCRIPTION_RST_FILENAME) if os.path.exists(index_rst): with open(index_rst, "r", encoding="utf-8") as f: description = f.read() else: description = "" if self.css_filename: result = publish_html_str(description, self.css_filename) else: result = publish_html_str(description) return result # ------------------------------------------------------------------------- # Implementation of the 'init_dic' property: # ------------------------------------------------------------------------- def _get_init_dic(self): init_dic = {} description, source = parse_source(join(self.path, "__init__.py")) exec((exec_str + source), init_dic) return init_dic # ------------------------------------------------------------------------- # Returns whether or not the object has children: # ------------------------------------------------------------------------- def has_children(self): """Returns whether or not the object has children.""" path = self.path for name in listdir(path): cur_path = join(path, name) if isdir(cur_path): return True if self.use_files: name, ext = splitext(name) if (ext == ".py") and (name != "__init__"): return True elif ext in self._file_factory: return True return False # ------------------------------------------------------------------------- # Gets the object's children: # ------------------------------------------------------------------------- def get_children(self): """Gets the object's children.""" if self.config_dict or self.config_filename: children = self.get_children_from_config() else: children = self.get_children_from_datastructure() return children # ------------------------------------------------------------------------- # Gets the object's children based on the filesystem structure. # ------------------------------------------------------------------------- def get_children_from_datastructure(self): """Gets the object's children based on the filesystem structure.""" dirs = [] files = [] path = self.path for name in listdir(path): cur_path = join(path, name) if isdir(cur_path): if self.has_py_files(cur_path): dirs.append( DemoPath( parent=self, name=name, css_filename=join('..', self.css_filename), ) ) elif self.use_files: if name != "__init__.py": try: demo_file = self._handle_file(name) demo_file.css_filename = self.css_filename files.append(demo_file) except KeyError: pass sort_key = operator.attrgetter("name") dirs.sort(key=sort_key) files.sort(key=sort_key) return dirs + files # ------------------------------------------------------------------------- # Gets the object's children as specified in its configuration file or # dictionary. # ------------------------------------------------------------------------- def get_children_from_config(self): """ Gets the object's children as specified in its configuration file or dictionary. """ if not self.config_dict: if exists(self.config_filename): try: self.config_dict = ConfigObj(self.config_filename) except Exception: pass if not self.config_dict: return self.get_children_from_datastructure() dirs = [] files = [] for keyword, value in self.config_dict.items(): if not value.get("no_demo"): sourcedir = value.pop("sourcedir", None) if sourcedir is not None: # This is a demo directory. demoobj = DemoPath( parent=self, name=sourcedir, css_filename=join("..", self.css_filename), ) demoobj.nice_name = keyword demoobj.config_dict = value dirs.append(demoobj) else: names = [] filenames = value.pop("files", []) if not isinstance(filenames, list): filenames = [filenames] for filename in filenames: filename = join(self.path, filename) for name in glob.iglob(filename): if basename(name) != "__init__.py": names.append(name) if len(names) > 1: config_dict = {} for name in names: config_dict[basename(name)] = {"files": name} demoobj = DemoPath(parent=self, name="") demoobj.nice_name = keyword demoobj.config_dict = config_dict demoobj.css_filename = os.path.join( "..", self.css_filename ) dirs.append(demoobj) elif len(names) == 1: try: demo_file = self._handle_file(name) files.append(demo_file) demo_file.css_filename = self.css_filename except KeyError: pass sort_key = operator.attrgetter("nice_name") dirs.sort(key=sort_key) files.sort(key=sort_key) return dirs + files # ------------------------------------------------------------------------- # Returns whether the specified path contains any .py files: # ------------------------------------------------------------------------- def has_py_files(self, path): for name in listdir(path): cur_path = join(path, name) if isdir(cur_path): if self.has_py_files(cur_path): return True else: name, ext = splitext(name) if ext == ".py": return True return False def _handle_file(self, filename): """Process a file based on its extension.""" _, ext = splitext(filename) file_factory = self._file_factory[ext] demo_file = file_factory(parent=self, name=filename) return demo_file # ------------------------------------------------------------------------- # Defines the demo tree editor: # ------------------------------------------------------------------------- demo_path_view = View( UItem( "description", style="readonly", editor=HTMLEditor( format_text=True, base_url_name='base_url', ), ), id="demo_path_view", kind='subpanel', ) demo_file_view = View( HSplit( UItem( "description", style="readonly", editor=HTMLEditor( format_text=True, base_url_name='base_url', ), ), VSplit( VGroup( UItem("source", style="custom"), HGroup( spring, UItem( "handler.run_button", ), visible_when="source is not None", ), ), Tabbed( Item( "log", style="readonly", editor=CodeEditor( show_line_numbers=False, selected_color=0xFFFFFF ), label="Output", show_label=False, ), Item( "locals", editor=ShellEditor(share=True), label="Shell", show_label=False, ), UItem( "demo", style="custom", resizable=True, ), ), dock="horizontal", ), ), id="demo_file_view", handler=demo_file_handler, kind='subpanel', ) demo_content_view = View( Tabbed( UItem( "description", style="readonly", editor=HTMLEditor( format_text=True, base_url_name='base_url', ), ), ), handler=demo_file_handler, kind='subpanel', ) demo_tree_editor = TreeEditor( nodes=[ ObjectTreeNode( node_for=[DemoPath], label="nice_name", view=demo_path_view, ), ObjectTreeNode( node_for=[DemoFile], label="nice_name", view=demo_file_view ), ObjectTreeNode( node_for=[DemoContentFile], label="nice_name", view=demo_content_view, ), ObjectTreeNode( node_for=[DemoImageFile], label="nice_name", view=demo_content_view ), ], selected='selected_node', ) next_tool = Action( name='Next', image=ImageResource("next"), tooltip="Go to next file", action="do_next", enabled_when="_next_node is not None", ) previous_tool = Action( name='Previous', image=ImageResource("previous"), tooltip="Go to next file", action="do_previous", enabled_when="_previous_node is not None", ) parent_tool = Action( name='Parent', image=ImageResource("parent"), tooltip="Go to next file", action="do_parent", enabled_when="(selected_node is not None) and " "(object.selected_node.parent is not None)", ) class Demo(ModelView): #: Root path object for locating demo files: model = Instance(DemoPath) #: Path to the root demo directory: path = Str() #: Selected node of the demo path tree. selected_node = Any() #: Title for the demo title = Str() _next_node = Property() _previous_node = Property() def do_next(self, event=None): self.selected_node = self._next_node def do_previous(self, event=None): self.selected_node = self._previous_node def do_parent(self, event=None): if self.selected_node is not None: parent = self.selected_node.parent self.selected_node = parent def init(self, info): info.ui.title = self.title return True def _get__next_node(self): next = None node = self.selected_node children = node.tno_get_children(node) if len(children) > 0: next = children[0] else: parent = node.parent while parent is not None: siblings = parent.tno_get_children(parent) index = siblings.index(node) if index < (len(siblings) - 1): next = siblings[index + 1] break parent, node = parent.parent, parent return next def _get__previous_node(self): previous = None node = self.selected_node parent = node.parent if parent is not None: siblings = parent.tno_get_children(parent) index = siblings.index(node) if index > 0: previous = siblings[index - 1] previous_children = previous.tno_get_children(previous) while len(previous_children) > 0: previous = previous_children[-1] previous_children = previous.tno_get_children(previous) else: previous = parent return previous # ------------------------------------------------------------------------- # Traits view definitions: # ------------------------------------------------------------------------- def default_traits_view(self): # XXX need to do this to hide the instantiation of ToolBar from null # toolkit tests. It would be preferable to have this be declarative. return View( Item( name="model", id="model", show_label=False, editor=demo_tree_editor, ), id="traitsui.demos.demo.Demo", toolbar=ToolBar( previous_tool, parent_tool, next_tool, show_tool_names=True ), resizable=True, width=1200, height=700, ) # ------------------------------------------------------------------------- # Utilities to convert rst strings/files to html. # ------------------------------------------------------------------------- def _get_settings(css_path=None): """Helper function to make settings dictionary consumable by docutils Parameters ---------- css_path: string or None (default) If not None, use the CSS stylesheet. Returns ------- dict """ settings = {'output_encoding': 'unicode'} if css_path is not None: settings['stylesheet_path'] = css_path settings['embed_stylesheet'] = False settings['stylesheet'] = None return settings def publish_html_str(rst_str, css_path=None): """Format RST string to html using `docutils` if available. Otherwise, return the input `rst_str`. Parameters ---------- rst_str: string reStructuredText css_path: string or None (default) If not None, use the CSS stylesheet. Returns ------- string """ try: from docutils.core import publish_string except Exception: return rst_str settings = _get_settings(css_path) return publish_string( rst_str, writer_name='html', settings_overrides=settings ) def publish_html_file(rst_file_path, html_out_path, css_path=None): """Format reStructuredText in `rst_file_path` to html using `docutils` if available. Otherwise, does nothing. Parameters ---------- rst_file_path: string html_out_path: string css_path: string or None (default) If not None, use the CSS stylesheet. Returns ------- None """ try: from docutils.core import publish_file except Exception: return settings = _get_settings(css_path) publish_file( source_path=rst_file_path, destination_path=html_out_path, writer_name='html', settings_overrides=settings, ) # ------------------------------------------------------------------------- # Function to run the demo: # ------------------------------------------------------------------------- def demo( use_files=False, dir_name=None, config_filename="", title="Traits UI Demos", css_filename="default.css", ): if dir_name is None: dir_name = dirname(abspath(sys.argv[0])) path, name = split(dir_name) if len(config_filename) > 0 and not isabs(config_filename): config_filename = join(path, name, config_filename) Demo( path=path, title=title, model=DemoPath( name=dir_name, nice_name=user_name_for(name), use_files=use_files, config_filename=config_filename, css_filename=css_filename, ), ).configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/extras/api.py0000644000175100001730000000137000000000000020463 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Defines 'pseudo' package that imports all of the traits extras symbols. """ from .checkbox_column import CheckboxColumn from .edit_column import EditColumn from .has_dynamic_views import ( DynamicView, DynamicViewSubElement, HasDynamicViews, ) from .progress_column import ProgressColumn from .saving import CanSaveMixin, SaveHandler ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/extras/checkbox_column.py0000644000175100001730000000317000000000000023055 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Defines the table column descriptor used for toggleable columns. """ from traits.etsconfig.api import ETSConfig from ..table_column import ObjectColumn if ETSConfig.toolkit == "wx": from pyface.ui.wx.grid.checkbox_renderer import CheckboxRenderer elif ETSConfig.toolkit in {"qt", "qt4"}: from ..qt.extra.checkbox_renderer import CheckboxRenderer else: raise NotImplementedError("No checkbox renderer for backend") class CheckboxColumn(ObjectColumn): def __init__(self, **traits): """Initializes the object.""" super().__init__(**traits) # force the renderer to be a checkbox renderer self.renderer = CheckboxRenderer() def get_cell_color(self, object): """Returns the cell background color for the column for a specified object. """ # Override the parent class to ALWAYS provide the standard color: return self.cell_color_ def is_editable(self, object): """Returns whether the column is editable for a specified object.""" # Although a checkbox column is always editable, we return this # to keep a standard editor from appearing. The editing is handled # in the renderer's handlers. return False ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/extras/demo.py0000644000175100001730000000112700000000000020636 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # This module preserves the entry point for other ETS package's example script # to import. # This entry point should be deprecated once ets-demo is released. from ._demo_legacy import demo ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/extras/edit_column.py0000644000175100001730000000236200000000000022216 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Defines the table column descriptor used for editing the object represented by the row """ from ..table_column import ObjectColumn class EditColumn(ObjectColumn): def __init__(self, **traits): """Initializes the object.""" super().__init__(**traits) from traitsui.toolkit import toolkit_object EditRenderer = toolkit_object('extra.edit_renderer:EditRenderer') self.renderer = EditRenderer() self.label = "" def get_cell_color(self, object): """Returns the cell background color for the column for a specified object. """ # Override the parent class to ALWAYS provide the standard color: return self.cell_color_ def is_editable(self, object): """Returns whether the column is editable for a specified object.""" return False ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/extras/has_dynamic_views.py0000644000175100001730000003504600000000000023415 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Provides a framework that assembles Traits UI Views at run time, when the view is requested, rather than at the time a class is written. This capability is particularly useful when the object being 'viewed' with a Traits UI is part of a plug-in application -- such as Envisage. In general, this capability allows: * The GUI for an object can be extendable by contributions other than from the original code writer. * The view can be dynamic in that the elements it is composed of can change each time it is requested. * Registration of a handler can be associated with the view contributions. Either the original object writer, or a contributor, can use this framework to declare one or more dynamic views that are composed of sub-elements that only need to exist at the time the view is requested. Users of this framework create a dynamic view by registering a DynamicView declaration. That declaration includes a name that forms the basis for the metadata attributes that are used to identify and order the desired view sub-elements into the view's composition. In addition, the declaration includes any data to be passed into the constructor of the dynamic view and the id that should be used to persist the user's customization of the view. Additionally, this framework allows sub-elements themselves to also be dynamically composed of further sub-elements. For example, a dynamic view could be composed of two sub-elements: 1. The first is a dynamically composed HFlow, which represents a toolbar that can be extended through contributions of toolbar buttons. 2. The second could be a dynamic tabset where each page is also a contribution. Programmers include dynamic sub-elements within their dynamic views by contributing a DynamicViewSubElement into that view. When the framework comes across this contribution while building the view, it replaces that DynamicViewSubElement with a fully initialized Traits ViewSubElement composed in a manner similar to how the elements of the View itself were composed. Each contribution to a dynamic view or sub-element must be an instance of a Traits ViewSubElement and must have associated metadata like the following for each dynamic view or sub-element it will display in: __order : A float value. The framework uses only ViewSubElements with this metadata instantiated when building the dynamic view or sub-element with the specified name. The elements are sorted by ascending order of this value using the standard list sort function. __priority : A float value. The framework resolves any overloading of an order value by picking the first element encountered that has the highest priority value. The other elements with the same view order are not displayed at all. In addition, dynamic view contributions can also provide a 'handler', which behaves like a normal Traits Handler. That is, it can contain methods that are called when model values change and can access the Traits UIInfo object representing the actual UI instances. To provide a handler, append the following metadata to your view sub-element: __handler : A HasTraits instance. The framework will connect listeners to call the handler methods as part of the handler for the dynamic view. """ from traits.api import Any, Bool, Dict, HasTraits, Instance, Str from traitsui.api import View, ViewSubElement, ViewElement from traitsui.delegating_handler import DelegatingHandler # Set up a logger: import logging logger = logging.getLogger(__name__) # ------------------------------------------------------------------------------- # 'DynamicViewSubElement' class: # ------------------------------------------------------------------------------- class DynamicViewSubElement(ViewSubElement): """ Declares a dynamic sub-element of a dynamic view. """ # --------------------------------------------------------------------------- # Trait definitions: # --------------------------------------------------------------------------- # -- Public 'DynamicViewSubElement' Interface ------------------------------- #: Keyword arguments passed in during construction of the actual #: ViewSubElement instance. keywords = Dict() # FIXME: Should be the 'Class' trait but I couldn't get that to work. #: The class of the actual ViewSubElement we are dynamically creating. klass = Any() #: The name of this dynamic sub-element. This controls the metadata #: names identifying the sub-elements that compose this element. name = Str() # ------------------------------------------------------------------------------- # 'DynamicView' class: # ------------------------------------------------------------------------------- class DynamicView(HasTraits): """ Declares a dynamic view. """ # --------------------------------------------------------------------------- # Trait definitions: # --------------------------------------------------------------------------- # -- Public 'DynamicView' Interface ----------------------------------------- #: The ID of the view. This is the ID that the view's preferences will be #: saved under. id = Str() #: The name of the view. This is the name that should be requested when #: calling edit_traits() or configure_traits(). name = Str() #: Keyword arguments passed in during construction of the actual view #: instance. keywords = Dict() #: Indicates whether this view should be the default traits view for objects #: it is contributed to. use_as_default = Bool(False) # ------------------------------------------------------------------------------- # 'HasDynamicViews' class: # ------------------------------------------------------------------------------- class HasDynamicViews(HasTraits): """ Provides of a framework that builds Traits UI Views at run time, when the view is requested, rather than at the time a class is written. """ # --------------------------------------------------------------------------- # Trait definitions: # --------------------------------------------------------------------------- # -- Protected 'HasDynamicViews' Interface ---------------------------------- #: The registry of dynamic views. The key is the view name and the value #: is the declaration of the dynamic view. _dynamic_view_registry = Dict(Str, Instance(DynamicView)) # --------------------------------------------------------------------------- # 'HasTraits' interface: # --------------------------------------------------------------------------- # -- Public Interface ------------------------------------------------------- def trait_view(self, name=None, view_element=None): """Gets or sets a ViewElement associated with an object's class. Extended here to build dynamic views and sub-elements. """ result = None # If a view element was passed instead of a name or None, do not handle # this as a request for a dynamic view and let the standard Traits # trait_view method be called with it. Otherwise, compose the dynamic # view here. if not isinstance(name, ViewElement): # If this is a request for the default view, see if one of our # dynamic views should be the default view: if (view_element is None) and (name is None or len(name) < 1): for dname, declaration in self._dynamic_view_registry.items(): if declaration.use_as_default: result = self._compose_dynamic_view(dname) break # Otherwise, handle if this is a request for a dynamic view: elif (view_element is None) and ( name in self._dynamic_view_registry ): result = self._compose_dynamic_view(name) # If we haven't created a dynamic view so far, then do the standard # traits thing to retrieve the UI element: if result is None: result = super().trait_view(name, view_element) return result # --------------------------------------------------------------------------- # 'HasDynamicViews' interface: # --------------------------------------------------------------------------- # -- Public Interface ------------------------------------------------------- def declare_dynamic_view(self, declaration): """A convenience method to add a new dynamic view declaration to this instance. """ self._dynamic_view_registry[declaration.name] = declaration # -- Protected Interface ---------------------------------------------------- def _build_dynamic_sub_element(self, definition, sub_elements): """Returns the fully composed ViewSubElement from the sub-element contributions to the dynamic sub-element identified by the definition. """ logger.debug( "\tBuilding dynamic sub-element [%s] with elements [%s]", definition.name, sub_elements, ) return definition.klass(*sub_elements, **definition.keywords) def _build_dynamic_view(self, declaration, sub_elements, handler): """Returns a Traits View representing the specified dynamic view composed out of the provided view sub-elements. Implemented as a separate method to allow implementors to override the way in which the instantiated view is configured. """ logger.debug( "\tBuilding dynamic view [%s] with elements [%s]", declaration.name, sub_elements, ) return View( # The view id allows the user's customization of this view, if any, # to be persisted when the view is closed and then that persisted # configuration to be applied when the view is next shown: id=declaration.id, # Include the specified handler: handler=handler, # Build the view out of the sub-elements: *sub_elements, # Include the declaration's keywords. **declaration.keywords, ) def _compose_dynamic_sub_element(self, definition): """Returns a dynamic UI element composed from its contributed parts.""" logger.debug( "Composing dynamic sub-element named [%s] for [%s]", definition.name, self, ) # Retrieve the set of elements that make up this sub-element: elements = self._get_dynamic_elements(definition.name) # Build the sub-element: return self._build_dynamic_sub_element(definition, elements) def _compose_dynamic_view(self, name): """Returns a dynamic view composed from its contributed parts.""" logger.debug("Composing dynamic view [%s] for [%s]", name, self) # Retrieve the declaration of this dynamic view: declaration = self._dynamic_view_registry[name] # Retrieve the set of elements that make up the view: elements = self._get_dynamic_elements(declaration.name) # Build a handler that delegates to the contribution handlers if any # exist: handler = None handlers = self._get_dynamic_handlers(declaration.name, elements) if len(handlers) > 0: handler = DelegatingHandler(sub_handlers=handlers) # Build the view: return self._build_dynamic_view(declaration, elements, handler) def _get_dynamic_elements(self, name): """Returns a list of the current elements meant to go into the composition of a dynamic view or subelement with the specified name. """ # Determine the metadata names used to find the sub-elements included # within this dynamic element: name = name.replace(" ", "_") order_trait_name = "_%s_order" % name priority_trait_name = "_%s_priority" % name # Now find all of the current sub-elements that we will use when # composing our element: all_elements = [ self.trait_view(g) for g in self.trait_views(klass=ViewSubElement) ] elements = [ e for e in all_elements if hasattr(e, order_trait_name) and (getattr(e, order_trait_name) is not None) ] # Filter out any overridden elements. This means taking out the # element with the lower priority whenever two elements have the # same order value: filtered = {} for e in elements: order = getattr(e, order_trait_name) priority = getattr(e, priority_trait_name) or 0 current = filtered.setdefault(order, e) if current is not e: current_priority = getattr(current, priority_trait_name) if current_priority < priority: filtered[order] = e # Sort the contributed elements by their display ordering values: ordering = sorted(filtered) elements = [filtered[order] for order in ordering] # Replace any dynamic sub-element with their full composition. # NOTE: We can't do this in the override of 'trait_view' because # then we get into infinite loops when a dynamic view subelement is # found as a child: for i in range(len(elements)): if isinstance(elements[i], DynamicViewSubElement): e = elements.pop(i) composed = self._compose_dynamic_sub_element(e) elements.insert(i, composed) return elements def _get_dynamic_handlers(self, name, elements): """Return a list of the handlers associated with the current elements meant to go into the dynamic view of the specified name. """ # Determine the metadata name used to declare a handler: name = name.replace(" ", "_") handler_name = "_%s_handler" % name handlers = [ getattr(e, handler_name) for e in elements if hasattr(e, handler_name) and (getattr(e, handler_name) is not None) ] logger.debug("\tFound sub-handlers: %s", handlers) return handlers ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1684768028.087807 traitsui-8.0.0/traitsui/extras/images/0000755000175100001730000000000000000000000020604 5ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/extras/images/next.png0000644000175100001730000000163000000000000022270 0ustar00runnerdocker00000000000000PNG  IHDRagAMA7tEXtSoftwareAdobe ImageReadyqe<*IDATxb?% X@ ϿY&R|l~sßXJa`"bWaeAs}TB41?_l. & A@ _~HaOys$2끮 ^`bbaecb`b?+?B_bX=XBB-={; AhS3 cgca'C/ ӦȖ_{AV󯫙, W>2}c tI 1LPP` Z^{ O_|eY  \\ <  ϲ#ݖg@M ?2| , 1e@̌X|q10p3,Ÿ́^3Õ?޽F/_( fl#'y4 擯 A6bAV !v)O޾"Ő`|КJ@zlKfRed`w?8M gs22ԇI2J1t5` 000@1p2&DϿ{I?I QG]}6001<!@e`xC[sW 0\FOĂ' l!Oow3ݿ. \,HHiv0~#w^IENDB`././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/extras/images/parent.png0000644000175100001730000000161200000000000022603 0ustar00runnerdocker00000000000000PNG  IHDRagAMA7tEXtSoftwareAdobe ImageReadyqe<IDATxb?% X@#cF%&RullL,ǎ?jb``@1ARL @;/פ09j!r`P@`035e`8-դxgUw>``hmg!K+8X2 @YiU0[1ܹlXV3B  _Ex̚u Ò 8baO |d>َEVAoIX8FtxDZ10Ϗ gC g"tN3N ! f!֕56rlSOմ1|}aѯ ~b&+;p0|AL ~D F۬1Tx1@囟 ?``fϿ1<~ #{W9-8~MmNV6 e5S:w .`/O?3/2裟@@33(1a|!myKϧ3ps Fhn`TݙjQ )3s+Odad7w^?ytwǟ0sa`ef F(.H^scc}ʴݹ}ٖ8e`6b(sFŧPdee`//hf@ X H 0xet`IENDB`././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/extras/images/run.png0000644000175100001730000000202100000000000022111 0ustar00runnerdocker00000000000000PNG  IHDRagAMA7tEXtSoftwareAdobe ImageReadyqe<IDATxb?% XJK; f100+rrR8e55i+;x~GbIŮ55dcc>~|]RA##T%a* rrꊞ*< L >ccc)w X~Ġ1p -FF`^GG6>>N70^ׯ&߿4$$$W mãG}:ADAZZAHt!!aǏoܸ| @A @3,[wihY\#'' 6`ݺ_ϟg & ر**2.]:=a߾ˁ1.!!Y ӇoY޽{k/_]AFFMCC_'$$AO߿c^F ,-YLÇ>b>u7o^~5+_JIIs XZZ)(H1/2,Z(@xx?VPPa`aaPY hjwA\\AJJ(W _5W qq `%++.Gϟ89ف)ܹ8q`ӧ˯_?,X0@\moQAQQ 8gP`FP͛gW^jy X@ĭ[WWp缼ڥrr߿bsP>0a2<{f2@0 ܹss %(qrr1̜9=z/F=ã  ,-mqssCX=;&cg+IENDB`././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/extras/progress_column.py0000644000175100001730000000372500000000000023141 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ A column class for for the TableEditor that displays progress bars. """ from traits.api import Bool, Int, Str from traitsui.table_column import ObjectColumn class ProgressColumn(ObjectColumn): """A column which displays trait values as a progress bar Progress values must be an integer value between the maximum and minimum values. By default it is assumed to be a percentage. """ #: Format string to apply to column values. format = Str("%s%%") #: The minimum value for a progress bar. minimum = Int(0) #: The maximum value for a progress bar. maximum = Int(100) #: Whether or not to display the text with the progress bar. #: This may not display with some progress bar styles, eg. on OS X. text_visible = Bool(True) def __init__(self, **traits): super().__init__(**traits) from traitsui.toolkit import toolkit_object ProgressRenderer = toolkit_object( 'extra.progress_renderer:ProgressRenderer' ) self.renderer = ProgressRenderer() def is_editable(self, object): """Returns whether the column is editable for a specified object.""" # Progress columns are always read-only return False def get_minimum(self, object): return self.minimum def get_maximum(self, object): return self.maximum def get_text_visible(self): """Whether or not to display text in column. Note, may not render on some platforms (eg. OS X) due to the rendering style. """ return self.text_visible ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/extras/saving.py0000644000175100001730000002145300000000000021205 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Provides a lightweight framework that removes some of the drudge work involved with implementing user-friendly saving behavior in a Traits UI application. """ # ETS imports from pyface.api import FileDialog, confirm, error, YES, CANCEL from pyface.timer.api import Timer from traits.api import ( HasTraits, Str, Bool, Any, Int, Instance, observe, ) from ..api import Handler class CanSaveMixin(HasTraits): """A mixin-class for objects that wish to support GUI saving via a SaveHandler. It is the responsiblity of the child class to manage its dirty flag, which describes whether its information has changed since its last save. """ filepath = Str() dirty = Bool(False) # ----------------------------------------------------------------- # object interface # ----------------------------------------------------------------- def __getstate__(self): """We don't want to pickle the filepath because this can change, obviously, if the user moves around the pickled file. """ state = super().__getstate__() del state["filepath"] del state["dirty"] return state # ----------------------------------------------------------------- # CanSaveMixin interface # ----------------------------------------------------------------- def validate(self): """Returns whether the information in the object is valid to be saved in tuple form. The first item is the validation state (boolean) and the second item is the message to display if the object did not validate. By default, an object always validates. """ return (True, "") def save(self): """Saves the object to the path specified by its 'filepath' trait. This method should also reset the dirty flag on this object. """ raise NotImplementedError class SaveHandler(Handler): """A Handler that facilates adding saving to a Traits UI application.""" #: The object which is to be saved (subclass of CanSaveMixin). It is assigned #: to info.object in the 'init' method, which in many cases is what you want. #: If not, override that method to set it to something else. saveObject = Any() #: The type of files to show in the save dialogs wildcard = Str("All files (*.*)|*.*") #: The option extension which should appear at the end of all filenames. If #: the user does not explicitly specifiy it, it is appended to the filename. extension = Str() #: This message to display when the Handler requests a save savePromptMessage = Str("Would you like to save?") #: Whether to prompt for a save on exit if the save object is dirty promptOnExit = Bool(True) #: Whether to allow the user to override a validation failure through a #: confirmation dialog. By default, validation errors cannot be overriden. allowValidationBypass = Bool(False) #: Whether to automatically save after a certain amount of time has passed #: since the last save autosave = Bool(False) #: Number of seconds between each autosave. Default is 5 minutes. autosaveInterval = Int(300) #: If it is possible to override validation failures, this specifies whether #: autosave will do so. If False and a validation errors occurs, no save #: will occur. autosaveValidationBypass = Bool(True) #: Protected traits _timer = Instance(Timer) # ----------------------------------------------------------------- # Handler interface # ----------------------------------------------------------------- def init(self, info): """Set the default save object (the object being handled). Also, perform a questionable hack by which we remove the handled object from the keybinding's controllers. This means that a keybinding to 'save' only calls this object, not the object being edited as well. (For reasons unclear, the KeyBinding handler API is radically different from the Action API, which is the reason that this problem exists. Keybindings are a UI concept--they should *not* call the model by default.) """ keybindings = info.ui.key_bindings if keybindings is not None: keybindings.controllers.remove(info.object) self.saveObject = info.object return True def close(self, info, is_ok): """Called when the user requests to close the interface. Returns a boolean indicating whether the window should be allowed to close. """ if self.promptOnExit: return self.promptForSave(info) else: return True def closed(self, info, is_ok): """Called after the window is destroyed. Makes sure that the autosave timer is stopped. """ if self._timer: self._timer.Stop() # ----------------------------------------------------------------- # SaveHandler interface # ----------------------------------------------------------------- def exit(self, info): """Closes the UI unless a save prompt is cancelled. Provided for convenience to be used with a Menu action. """ if self.close(info, True): info.ui.dispose() def save(self, info): """Saves the object to its current filepath. If this is not specified, opens a dialog to select this path. Returns whether the save actually occurred. """ if self.saveObject.filepath == "": return self.saveAs(info) else: return self._validateAndSave() def saveAs(self, info): """Saves the object to a new path, and sets this as the 'filepath' on the object. Returns whether the save actually occurred. """ fileDialog = FileDialog( action="save as", title="Save As", wildcard=self.wildcard, parent=info.ui.control, ) fileDialog.open() if fileDialog.path == "" or fileDialog.return_code == CANCEL: return False else: extLen = len(self.extension) if ( extLen and fileDialog.path[-extLen - 1 :] != "." + self.extension ): fileDialog.path += "." + self.extension self.saveObject.filepath = fileDialog.path return self._validateAndSave() def promptForSave(self, info, cancel=True): """Prompts the user to save the object, if appropriate. Returns whether the user canceled the action that invoked this prompt. """ if self.saveObject.dirty: code = confirm( info.ui.control, self.savePromptMessage, title="Save now?", cancel=cancel, ) if code == CANCEL: return False elif code == YES: if not self.save(info): return self.promptForSave(info, cancel) return True def _autosave(self): """Called by the timer when an autosave should take place.""" if self.saveObject.dirty and self.saveObject.filepath != "": success, message = self.saveObject.validate() if success or ( self.allowValidationBypass and self.autosaveValidationBypass ): self.saveObject.save() @observe("autosave, autosaveInterval, saveObject") def _configure_timer(self, event): """Creates, replaces, or destroys the autosave timer.""" if self._timer: self._timer.Stop() if self.autosave and self.saveObject: self._timer = Timer(self.autosaveInterval * 1000, self._autosave) else: self._timer = None def _validateAndSave(self): """Try to save to the current filepath. Returns whether whether the validation was successful/overridden (and the object saved). """ success, message = self.saveObject.validate() if success: self.saveObject.save() else: title = "Validation error" if ( self.allowValidationBypass and confirm(None, message, title=title) == YES ): self.saveObject.save() success = True else: error(None, message, title=title) return success ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/file_dialog.py0000644000175100001730000004736500000000000020660 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Defines functions and classes used to create pop-up file dialogs for opening and saving files. """ from os import R_OK, W_OK, access, mkdir from os.path import ( basename, dirname, exists, getatime, getctime, getmtime, getsize, isdir, isfile, join, split, splitext, ) from time import localtime, strftime from traits.api import ( Bool, Button, CList, Event, File, HasPrivateTraits, Instance, Int, Interface, Property, Str, cached_property, provides, ) from traits.trait_base import user_name_for from .api import ( CodeEditor, FileEditor, HGroup, HSplit, Handler, HistoryEditor, ImageEditor, InstanceEditor, Item, UIInfo, VGroup, VSplit, View, spring, ) from .ui_traits import AView from pyface.api import ImageResource from pyface.timer.api import do_later from .helper import commatize from .toolkit import toolkit # Maximum text file size to process: MAX_SIZE = 16 * 1024 * 1024 # ------------------------------------------------------------------------- # 'IFileDialogModel' interface: # ------------------------------------------------------------------------- class IFileDialogModel(Interface): """Defines a model extension to a file dialog.""" #: The name of the currently selected file: file_name = File() # ------------------------------------------------------------------------- # 'IFileDialogView' interface: # ------------------------------------------------------------------------- class IFileDialogView(Interface): """Defines a visual extension to a file dialog.""" #: The view to display: view = AView #: Is the view fixed or variable width? is_fixed = Bool() # ------------------------------------------------------------------------- # 'IFileDialogExtension' interface: # ------------------------------------------------------------------------- class IFileDialogExtension(IFileDialogModel, IFileDialogView): """Defines a (convenience) union of the IFileDialogModel and IFileDialogView interfaces. """ # ------------------------------------------------------------------------- # 'MFileDialogModel' mix-in class: # ------------------------------------------------------------------------- @provides(IFileDialogModel) class MFileDialogModel(HasPrivateTraits): #: The name of the currently selected file: file_name = File() # ------------------------------------------------------------------------- # 'MFileDialogView' mix-in class: # ------------------------------------------------------------------------- class MFileDialogView(HasPrivateTraits): """Defines a visual extension to a file dialog.""" #: The view to display: view = AView #: Is the view fixed or variable width? is_fixed = Bool(False) # Create a default implementation: default_view = MFileDialogView() class MFileDialogExtension(MFileDialogModel, MFileDialogView): """Defines a (convenience) union of the MFileDialogModel and MFileDialogView mix-in classes. """ class FileInfo(MFileDialogModel): """Defines a file dialog extension that display various file information.""" #: The size of the file: size = Property(observe="file_name") #: Last file access time: atime = Property(observe="file_name") #: List file modification time: mtime = Property(observe="file_name") #: File creation time (or last metadata change time): ctime = Property(observe="file_name") # -- Traits View Definitions ---------------------------------------------- view = View( VGroup( Item("size", label="File size", style="readonly"), Item("atime", label="Last access", style="readonly"), Item("mtime", label="Last modified", style="readonly"), Item("ctime", label="Created at", style="readonly"), label="File Information", show_border=True, ) ) # -- Property Implementations --------------------------------------------- @cached_property def _get_size(self): try: return commatize(getsize(self.file_name)) + " bytes" except: return "" @cached_property def _get_atime(self): try: return strftime( "%m/%d/%Y %I:%M:%S %p", localtime(getatime(self.file_name)) ) except: return "" @cached_property def _get_mtime(self): try: return strftime( "%m/%d/%Y %I:%M:%S %p", localtime(getmtime(self.file_name)) ) except: return "" @cached_property def _get_ctime(self): try: return strftime( "%m/%d/%Y %I:%M:%S %p", localtime(getctime(self.file_name)) ) except: return "" class TextInfo(MFileDialogModel): """Defines a file dialog extension that displays a file's contents as text.""" #: The file's text content: text = Property(observe="file_name") # -- Traits View Definitions ---------------------------------------------- view = View( Item("text", style="readonly", show_label=False, editor=CodeEditor()) ) # -- Property Implementations --------------------------------------------- @cached_property def _get_text(self): try: if getsize(self.file_name) > MAX_SIZE: return "File too big..." with open(self.file_name, "rb") as fh: data = fh.read() except Exception: return "" if (data.find("\x00") >= 0) or (data.find("\xFF") >= 0): return "File contains binary data..." return data class ImageInfo(MFileDialogModel): """Defines a file dialog extension that display an image file's dimensions and content. """ #: The ImageResource object for the current file: image = Property(observe="file_name") #: The width of the current image: width = Property(observe="image") #: The height of the current image: height = Property(observe="image") # -- Traits View Definitions ---------------------------------------------- view = View( VGroup( VGroup( Item("width", style="readonly"), Item("height", style="readonly"), label="Image Dimensions", show_border=True, ), VGroup( Item("image", show_label=False, editor=ImageEditor()), label="Image", show_border=True, springy=True, ), ) ) # -- Property Implementations --------------------------------------------- @cached_property def _get_image(self): path, name = split(self.file_name) if splitext(name)[1] in (".png", ".gif", ".jpg", ".jpeg"): image = ImageResource(name, search_path=[path]) else: image = ImageResource("unknown") self._cur_image = image.create_image() return image @cached_property def _get_width(self): try: return str(toolkit().image_size(self._cur_image)[0]) + " pixels" except: return "---" @cached_property def _get_height(self): try: return str(toolkit().image_size(self._cur_image)[1]) + " pixels" except: return "---" class CreateDirHandler(Handler): """Controller for the 'create new directory' popup.""" #: The name for the new directory to be created: dir_name = Str() #: The current status message: message = Str() #: The OK and Cancel buttons: ok = Button("OK") cancel = Button("Cancel") # -- Traits View Definitions ---------------------------------------------- view = View( VGroup( HGroup( Item("handler.dir_name", label="Name"), Item( "handler.ok", show_label=False, enabled_when="handler.dir_name.strip() != ''", ), Item("handler.cancel", show_label=False), ), HGroup( Item( "handler.message", show_label=False, style="readonly", springy=True, ), show_border=True, ), ), kind="popup", ) # -- Handler Event Handlers ----------------------------------------------- def handler_ok_changed(self, info): """Handles the user clicking the OK button.""" dir = info.object.file_name if not isdir(dir): dir = dirname(dir) path = join(dir, self.dir_name) try: # Try to create the requested directory: mkdir(path) # Force the file tree view to be refreshed: info.object.reload = True # set the new directory as the currently selected file name: info.object.file_name = path # Close this view: info.ui.dispose(True) except: self.message = ( "Could not create the '%s' directory" % self.dir_name ) def handler_cancel_changed(self, info): """Handles the user clicking the Cancel button.""" info.ui.dispose(False) class FileExistsHandler(Handler): """Controller for the 'file already exists' popup.""" #: The current status message: message = Str() #: The OK and Cancel buttons: ok = Button("OK") cancel = Button("Cancel") # -- Traits View Definitions ---------------------------------------------- view = View( VGroup( HGroup( Item( "handler.message", editor=ImageEditor(image="@icons:dialog-warning"), ), Item("handler.message", style="readonly"), show_labels=False, ), HGroup( spring, Item("handler.ok"), Item("handler.cancel"), show_labels=False, ), ), kind="popup", ) # -- Handler Event Handlers ----------------------------------------------- def handler_ok_changed(self, info): """Handles the user clicking the OK button.""" parent = info.ui.parent info.ui.dispose(True) parent.dispose(True) def handler_cancel_changed(self, info): """Handles the user clicking the Cancel button.""" info.ui.dispose(False) class OpenFileDialog(Handler): """Defines the model and handler for the open file dialog.""" #: The starting and current file path: file_name = File() #: The list of file filters to apply: filter = CList(Str) #: Number of history entries to allow: entries = Int(10) #: The file dialog title: title = Str("Open File") #: The Traits UI persistence id to use: id = Str("traitsui.file_dialog.OpenFileDialog") #: A list of optional file dialog extensions: extensions = CList(IFileDialogModel) # -- Private Traits ------------------------------------------------------- #: The UIInfo object for the view: info = Instance(UIInfo) #: Event fired when the file tree view should be reloaded: reload = Event() #: Event fired when the user double-clicks on a file name: dclick = Event() #: Allow extension models to be added dynamically: extension__ = Instance(IFileDialogModel) #: Is the file dialog for saving a file (or opening a file)? is_save_file = Bool(False) #: Is the currently specified file name valid? is_valid_file = Property(observe="file_name") #: Can a directory be created now? can_create_dir = Property(observe="file_name") #: The OK, Cancel and create directory buttons: ok = Button("OK") cancel = Button("Cancel") create = Button(image="@icons:folder-new", style="toolbar") # -- Handler Class Method Overrides --------------------------------------- def init_info(self, info): """Handles the UIInfo object being initialized during view start-up.""" self.info = info # -- Property Implementations --------------------------------------------- def _get_is_valid_file(self): if self.is_save_file: return isfile(self.file_name) or (not exists(self.file_name)) return isfile(self.file_name) def _get_can_create_dir(self): dir = dirname(self.file_name) return isdir(dir) and access(dir, R_OK | W_OK) # -- Handler Event Handlers ----------------------------------------------- def object_ok_changed(self, info): """Handles the user clicking the OK button.""" if self.is_save_file and exists(self.file_name): do_later(self._file_already_exists) else: info.ui.dispose(True) def object_cancel_changed(self, info): """Handles the user clicking the Cancel button.""" info.ui.dispose(False) def object_create_changed(self, info): """Handles the user clicking the create directory button.""" if not isdir(self.file_name): self.file_name = dirname(self.file_name) CreateDirHandler().edit_traits( context=self, parent=info.create.control ) # -- Traits Event Handlers ------------------------------------------------ def _dclick_changed(self): """Handles the user double-clicking a file name in the file tree view.""" if self.is_valid_file: self.object_ok_changed(self.info) # -- Private Methods ------------------------------------------------------ def open_file_view(self): """Returns the file dialog view to use.""" # Set up the default file dialog view and size information: item = Item( "file_name", id="file_tree", style="custom", show_label=False, width=0.5, editor=FileEditor( filter=self.filter, allow_dir=True, reload_name="reload", dclick_name="dclick", ), ) width = height = 0.20 # Check to see if we have any extensions being added: if len(self.extensions) > 0: # fixme: We should use the actual values of the view's Width and # height traits here to adjust the overall width and height... width *= 2.0 # Assume we can used a fixed width Group: klass = HGroup # Set up to build a list of view Item objects: items = [] # Add each extension to the dialog: for i, extension in enumerate(self.extensions): # Save the extension in a new trait (for use by the View): name = "extension_%d" % i setattr(self, name, extension) extension_view = extension # Sync up the 'file_name' trait with the extension: self.sync_trait("file_name", extension, mutual=True) # Check to see if it also defines the optional IFileDialogView # interface, and if not, use the default view information: if not extension.has_traits_interface(IFileDialogView): extension_view = default_view # Get the view that the extension wants to use: view = extension.trait_view(extension_view.view) # Determine if we should use a splitter for the dialog: if not extension_view.is_fixed: klass = HSplit # Add the extension as a new view item: items.append( Item( name, label=user_name_for(extension.__class__.__name__), show_label=False, style="custom", width=0.5, height=0.5, dock="horizontal", resizable=True, editor=InstanceEditor(view=view, id=name), ) ) # Finally, combine the normal view element with the extensions: item = klass( item, VSplit(id="splitter2", springy=True, *items), id="splitter", ) # Return the resulting view: return View( VGroup( VGroup(item), HGroup( Item( "create", id="create", show_label=False, style="custom", defined_when="is_save_file", enabled_when="can_create_dir", tooltip="Create a new directory", ), Item( "file_name", id="history", editor=HistoryEditor( entries=self.entries, auto_set=True ), springy=True, ), Item( "ok", id="ok", show_label=False, enabled_when="is_valid_file", ), Item("cancel", show_label=False), ), ), title=self.title, id=self.id, kind="livemodal", width=width, height=height, close_result=False, resizable=True, ) def _file_already_exists(self): """Handles prompting the user when the selected file already exists, and the dialog is a 'save file' dialog. """ feh = FileExistsHandler( message=( "The file '%s' already exists.\nDo " "you wish to overwrite it?" ) % basename(self.file_name) ) feh.edit_traits(context=self, parent=self.info.ok.control).trait_set( parent=self.info.ui ) # ------------------------------------------------------------------------- # Returns a file name to open or an empty string if the user cancels the # operation: # ------------------------------------------------------------------------- def open_file(**traits): """Returns a file name to open or an empty string if the user cancels the operation. """ fd = OpenFileDialog(**traits) if fd.edit_traits(view="open_file_view").result: return fd.file_name return "" def save_file(**traits): """Returns a file name to save to or an empty string if the user cancels the operation. In the case where the file selected already exists, the user will be prompted if they want to overwrite the file before the selected file name is returned. """ traits.setdefault("title", "Save File") traits["is_save_file"] = True fd = OpenFileDialog(**traits) if fd.edit_traits(view="open_file_view").result: return fd.file_name return "" # -- Test Case ------------------------------------------------------------ if __name__ == "__main__": print( save_file( extensions=[FileInfo(), TextInfo(), ImageInfo()], filter="Python file (*.py)|*.py", ) ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/group.py0000644000175100001730000005765500000000000017561 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Defines the Group class used to represent a group of items used in a Traits-based user interface. """ from traits.api import ( Bool, Delegate, Float, Instance, List, Property, Range, ReadOnly, Str, TraitError, cached_property, ) from .view_element import ViewSubElement from .item import Item from .include import Include from .ui_traits import SequenceTypes, ContainerDelegate, Orientation, Layout from .dock_window_theme import dock_window_theme, DockWindowTheme # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- # Delegate trait to the object being "shadowed" ShadowDelegate = Delegate("shadow") # Amount of padding to add around item Padding = Range(0, 15, desc="amount of padding to add around each item") class Group(ViewSubElement): """Represents a grouping of items in a user interface view.""" # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- #: A list of Group, Item, and Include objects in this group. content = List(ViewSubElement) #: A unique identifier for the group. id = Str() #: User interface label for the group. How the label is displayed depends #: on the **show_border** attribute, and on the **layout** attribute of #: the group's parent group or view. label = Str() style_sheet = Str() #: Default context object for group items. object = ContainerDelegate #: Default editor style of items in the group. style = ContainerDelegate #: Default docking style of items in group. dock = ContainerDelegate #: Default image to display on notebook tabs. image = ContainerDelegate #: The theme to use for a DockWindow: dock_theme = Instance(DockWindowTheme, allow_none=False) #: Category of elements dragged from view. export = ContainerDelegate #: Spatial orientation of the group's elements. Can be 'vertical' (default) #: or 'horizontal'. orientation = Orientation #: Layout style of the group, which can be one of the following: #: #: * 'normal' (default): Sub-groups are displayed sequentially in a single #: panel. #: * 'flow': Sub-groups are displayed sequentially, and then "wrap" when #: they exceed the available space in the **orientation** direction. #: * 'split': Sub-groups are displayed in a single panel, separated by #: "splitter bars", which the user can drag to adjust the amount of space #: for each sub-group. #: * 'tabbed': Each sub-group appears on a separate tab, labeled with the #: sub-group's *label* text, if any. #: #: This attribute is ignored for groups that contain only items, or contain #: only one sub-group. layout = Layout #: Should the group be scrollable along the direction of orientation? scrollable = Bool(False) #: The number of columns in the group columns = Range(1, 50) #: Should a border be drawn around group? If set to True, the **label** text #: is embedded in the border. If set to False, the label appears as a banner #: above the elements of the group. show_border = Bool(False) #: Should labels be added to items in group? Only items that are directly #: contained in the group are affected. That is, if the group contains #: a sub-group, the display of labels in the sub-group is not affected by #: the attribute on this group. show_labels = Bool(True) #: Should labels be shown to the left of items (True) or the right (False)? #: Only items that are directly contained in the group are affected. That is, #: if the group contains a sub-group, the display of labels in the sub-group #: is not affected by the attribute in this group. If **show_labels** is #: False, this attribute is irrelevant. show_left = Bool(True) #: Is this group the tab that is initially selected? If True, the group's #: tab is displayed when the view is opened. If the **layout** of the group's #: parent is not 'tabbed', this attribute is ignored. selected = Bool(False) #: Should the group use extra space along its parent group's layout #: orientation? springy = Bool(False) #: Optional help text (for top-level group). This help text appears in the #: View-level help window (created by the default help handler), for any #: View that contains *only* this group. Group-level help is ignored for #: nested groups and multiple top-level groups help = Str() #: Pre-condition for including the group in the display. If the expression #: evaluates to False, the group is not defined in the display. Conditions #: for **defined_when** are evaluated only once, when the display is first #: constructed. Use this attribute for conditions based on attributes that #: vary from object to object, but that do not change over time. defined_when = Str() #: Pre-condition for showing the group. If the expression evaluates to False, #: the group and its items are not visible (and they disappear if they were #: previously visible). If the value evaluates to True, the group and items #: become visible. All **visible_when** conditions are checked each time #: that any trait value on an object in the UI's context is changed. #: Therefore, you can use **visible_when** conditions to hide or show #: groups in response to user input. Be aware that this only applies to #: traits in the UI's context. As a result, changes to nested traits that #: don't also change a trait on some object in the context may not #: trigger the expression to be checked. Additionally, the expression needs #: to be a valid python expression given the context. i.e. #: eval(visible_when, globals=globals(), locals=context) should succeed. visible_when = Str() #: Pre-condition for enabling the group. If the expression evaluates to False, #: the group is disabled, that is, none of the widgets accept input. All #: **enabled_when** conditions are checked each time that any trait value #: on an object in the UI's context is changed. Therefore, you can use #: **enabled_when** conditions to enable or disable groups in response to #: user input. Be aware that this only applies to traits in the UI's #: context. As a result, changes to nested traits that don't also change a #: trait on some object in the context may not trigger the expression to be #: checked. Additionally, the expression needs to be a valid python #: expression given the context. i.e. #: eval(enabled_when, globals=globals(), locals=context) should succeed. enabled_when = Str() #: Amount of padding (in pixels) to add around each item in the group. The #: value must be an integer between 0 and 15. (Unlike the Item class, the #: Group class does not support negative padding.) The padding for any #: individual widget is the sum of the padding for its Group, the padding #: for its Item, and the default spacing determined by the toolkit. padding = Padding #: Requested width of the group (calculated from widths of contents) width = Property(Float, observe="content") #: Requested height of the group (calculated from heights of contents) height = Property(Float, observe="content") def __init__(self, *values, **traits): """Initializes the group object.""" super().__init__(**traits) content = self.content # Process any embedded Group options first: for value in values: if (isinstance(value, str)) and (value[0:1] in "-|"): # Parse Group trait options if specified as a string: self._parse(value) # Process all of the data passed to the constructor: for value in values: if isinstance(value, ViewSubElement): content.append(value) elif type(value) in SequenceTypes: # Map (...) or [...] to a Group(): content.append(Group(*value)) elif isinstance(value, str): if value[0:1] in "-|": # We've already parsed Group trait options above: pass elif (value[:1] == "<") and (value[-1:] == ">"): # Convert string to an Include value: content.append(Include(value[1:-1].strip())) else: # Else let the Item class try to make sense of it: content.append(Item(value)) else: raise TypeError("Unrecognized argument type: %s" % value) # Make sure this Group is the container for all its children: self.set_container() # -- Default Trait Values ------------------------------------------------- def _dock_theme_default(self): return dock_window_theme() def get_label(self, ui): """Gets the label to use this group.""" if self.label != "": return self.label return "Group" def is_includable(self): """Returns a Boolean value indicating whether the object is replacable by an Include object. """ return self.id != "" def replace_include(self, view_elements): """Replaces any items that have an **id** attribute with an Include object with the same ID value, and puts the object with the ID into the specified ViewElements object. Parameters ---------- view_elements : ViewElements object A set of Group, Item, and Include objects """ for i, item in enumerate(self.content): if item.is_includable(): id = item.id if id in view_elements.content: raise TraitError( "Duplicate definition for view element '%s'" % id ) self.content[i] = Include(id) view_elements.content[id] = item item.replace_include(view_elements) def get_shadow(self, ui): """Returns a ShadowGroup object for the current Group object, which recursively resolves all embedded Include objects and which replaces each embedded Group object with a corresponding ShadowGroup. """ content = [] groups = 0 level = ui.push_level() for value in self.content: # Recursively replace Include objects: while isinstance(value, Include): value = ui.find(value) # Convert Group objects to ShadowGroup objects, but include Item # objects as is (ignore any 'None' values caused by a failed # Include): if isinstance(value, Group): if self._defined_when(ui, value): content.append(value.get_shadow(ui)) groups += 1 elif isinstance(value, Item): if self._defined_when(ui, value): content.append(value) ui.pop_level(level) # Return the ShadowGroup: return ShadowGroup(shadow=self, content=content, groups=groups) def set_container(self): """Sets the correct container for the content.""" for item in self.content: item.container = self def _defined_when(self, ui, value): """Should the object be defined in the user interface?""" if value.defined_when == "": return True return ui.eval_when(value.defined_when) def _parse(self, value): """Parses Group options specified as a string.""" # Override the defaults, since we only allow 'True' values to be # specified: self.show_border = self.show_labels = self.show_left = False # Parse all of the single or multi-character options: value, empty = self._parse_label(value) value = self._parse_style(value) value = self._option(value, "-", "orientation", "horizontal") value = self._option(value, "|", "orientation", "vertical") value = self._option(value, "=", "layout", "split") value = self._option(value, "^", "layout", "tabbed") value = self._option(value, ">", "show_labels", True) value = self._option(value, "<", "show_left", True) value = self._option(value, "!", "selected", True) show_labels = not (self.show_labels and self.show_left) self.show_left = not self.show_labels self.show_labels = show_labels # Parse all of the punctuation based sub-string options: value = self._split("id", value, ":", str.find, 0, 1) if value != "": self.object = value def _parsed_label(self): """Handles a label being found in the string definition.""" self.show_border = True def __repr__(self): """Returns a "pretty print" version of the Group.""" result = [] items = ",\n".join([item.__repr__() for item in self.content]) if len(items) > 0: result.append(items) options = self._repr_options( "orientation", "show_border", "show_labels", "show_left", "selected", "id", "object", "label", "style", "layout", "style_sheet", ) if options is not None: result.append(options) content = ",\n".join(result) if len(content) == 0: return self.__class__.__name__ + "()" return "%s(\n%s\n)" % (self.__class__.__name__, self._indent(content)) # ------------------------------------------------------------------------- # Property getters/setters for width/height attributes # ------------------------------------------------------------------------- @cached_property def _get_width(self): """Returns the requested width of the Group.""" width = 0.0 for item in self.content: if item.width >= 1: if self.orientation == "horizontal": width += item.width elif self.orientation == "vertical": width = max(width, item.width) if width == 0: width = -1.0 return width @cached_property def _get_height(self): """Returns the requested height of the Group.""" height = 0.0 for item in self.content: if item.height >= 1: if self.orientation == "horizontal": height = max(height, item.height) elif self.orientation == "vertical": height += item.height if height == 0: height = -1.0 return height class HGroup(Group): """A group whose items are laid out horizontally.""" # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- #: Override standard Group trait defaults to give it horizontal group #: behavior: orientation = "horizontal" class VGroup(Group): """A group whose items are laid out vertically.""" # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- #: Override standard Group trait defaults to give it vertical group #: behavior: orientation = "vertical" class VGrid(VGroup): """A group whose items are laid out in 2 columns.""" # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- #: Override standard Group trait defaults to give it grid behavior: columns = 2 class HFlow(HGroup): """A group in which items are laid out horizontally, and "wrap" when they exceed the available horizontal space.. """ # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- #: Override standard Group trait defaults to give it horizontal flow #: behavior: layout = "flow" show_labels = False class VFlow(VGroup): """A group in which items are laid out vertically, and "wrap" when they exceed the available vertical space. """ # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- #: Override standard Group trait defaults to give it vertical flow behavior: layout = "flow" show_labels = False class VFold(VGroup): """A group in which items are laid out vertically and can be collapsed (i.e. 'folded') by clicking their title. """ # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- #: Override standard Group trait defaults to give it vertical folding group #: behavior: layout = "fold" show_labels = False class HSplit(Group): """A horizontal group with splitter bars to separate it from other groups.""" # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- #: Override standard Group trait defaults to give it horizontal splitter #: behavior: layout = "split" orientation = "horizontal" class VSplit(Group): """A vertical group with splitter bars to separate it from other groups.""" # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- #: Override standard Group trait defaults to give it vertical splitter #: behavior: layout = "split" orientation = "vertical" class Tabbed(Group): """A group that is shown as a tabbed notebook.""" # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- #: Override standard Group trait defaults to give it tabbed notebook #: behavior: layout = "tabbed" springy = True class ShadowGroup(Group): """Corresponds to a Group object, but with all embedded Include objects resolved, and with all embedded Group objects replaced by corresponding ShadowGroup objects. """ def __init__(self, shadow, **traits): # Set the 'shadow' trait before all others, to avoid exceptions # when setting those other traits. self.shadow = shadow super().__init__(**traits) # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- #: Group object this is a "shadow" for shadow = ReadOnly() #: Number of ShadowGroups in **content** groups = ReadOnly() #: Name of the group id = ShadowDelegate #: User interface label for the group label = ShadowDelegate #: Default context object for group items object = ShadowDelegate #: Default style of items in the group style = ShadowDelegate #: Default docking style of items in the group dock = ShadowDelegate #: Default image to display on notebook tabs image = ShadowDelegate #: The theme to use for a DockWindow: dock_theme = ShadowDelegate #: Category of elements dragged from the view export = ShadowDelegate #: Spatial orientation of the group orientation = ShadowDelegate #: Layout style of the group layout = ShadowDelegate #: Should the group be scrollable along the direction of orientation? scrollable = ShadowDelegate #: The number of columns in the group columns = ShadowDelegate #: Should a border be drawn around group? show_border = ShadowDelegate #: Should labels be added to items in group? show_labels = ShadowDelegate #: Should labels be shown to the left of items (vs. the right)? show_left = ShadowDelegate #: Is group the initially selected page? selected = ShadowDelegate #: Should the group use extra space along its parent group's layout #: orientation? springy = ShadowDelegate #: Optional help text (for top-level group) help = ShadowDelegate #: Pre-condition for defining the group defined_when = ShadowDelegate #: Pre-condition for showing the group visible_when = ShadowDelegate #: Pre-condition for enabling the group enabled_when = ShadowDelegate #: Amount of padding to add around each item padding = ShadowDelegate #: Style sheet for the panel style_sheet = ShadowDelegate def get_content(self, allow_groups=True): """Returns the contents of the Group within a specified context for building a user interface. This method makes sure that all Group types are of the same type (i.e., Group or Item) and that all Include objects have been replaced by their substituted values. """ # Make a copy of the content: result = self.content[:] # If result includes any ShadowGroups and they are not allowed, # replace them: if self.groups != 0: if not allow_groups: i = 0 while i < len(result): value = result[i] if isinstance(value, ShadowGroup): items = value.get_content(False) result[i : i + 1] = items i += len(items) else: i += 1 elif (self.groups != len(result)) and (self.layout == "normal"): items = [] content = [] for item in result: if isinstance(item, ShadowGroup): self._flush_items(content, items) content.append(item) else: items.append(item) self._flush_items(content, items) result = content # Return the resulting list of objects: return result def get_id(self): """Returns an ID for the group.""" if self.id != "": return self.id return ":".join([item.get_id() for item in self.get_content()]) def set_container(self): """Sets the correct container for the content.""" pass def _flush_items(self, content, items): """Creates a sub-group for any items contained in a specified list.""" if len(items) > 0: content.append( # Set shadow before hand to prevent delegation errors ShadowGroup(shadow=self.shadow).trait_set( groups=0, label="", show_border=False, content=items, show_labels=self.show_labels, show_left=self.show_left, springy=self.springy, orientation=self.orientation, ) ) del items[:] def __repr__(self): """Returns a "pretty print" version of the Group.""" return repr(self.shadow) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/handler.py0000644000175100001730000004733500000000000020034 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Defines the Handler class used to manage and control the editing process in a Traits-based user interface. """ # avoid deprecation warning from inspect import getfullargspec from traits.api import HasPrivateTraits, HasTraits, Instance from .toolkit import toolkit from .help import on_help_call from .view_element import ViewElement from .helper import user_name_for from .ui_info import UIInfo # ------------------------------------------------------------------------- # Closes a DockControl (if allowed by the associated traits UI Handler): # ------------------------------------------------------------------------- def close_dock_control(dock_control): """Closes a DockControl (if allowed by the associated Traits UI Handler).""" # Retrieve the traits UI object set when we created the DockControl: ui = dock_control.data # Ask the traits UI handler if it is OK to close the window: if not ui.handler.close(ui.info, True): # If not, tell the DockWindow not to close it: return False # Otherwise, clean up and close the traits UI: ui.dispose() # And tell the DockWindow to remove the DockControl: return True class Handler(HasPrivateTraits): """Provides access to and control over the run-time workings of a Traits-based user interface. """ def init_info(self, info): """Informs the handler what the UIInfo object for a View will be. This method is called before the UI for the View has been constructed. It is provided so that the handler can save the reference to the UIInfo object in case it exposes viewable traits whose values are properties that depend upon items in the context being edited. """ pass def init(self, info: UIInfo) -> bool: """Initializes the controls of a user interface. This method is called after all user interface elements have been created, but before the user interface is displayed. Override this method to customize the user interface before it is displayed. Parameters ---------- info : UIInfo object The UIInfo object associated with the view Returns ------- initialized : bool A Boolean, indicating whether the user interface was successfully initialized. A True value indicates that the UI can be displayed; a False value indicates that the display operation should be cancelled. The default implementation returns True without taking any other action. """ return True def position(self, info): """Positions a dialog-based user interface on the display. This method is called after the user interface is initialized (by calling init()), but before the user interface is displayed. Override this method to position the window on the display device. The default implementation calls the position() method of the current toolkit. Usually, you do not need to override this method, because you can control the window's placement using the **x** and **y** attributes of the View object. Parameters ---------- info : UIInfo object The UIInfo object associated with the window """ toolkit().position(info.ui) def close(self, info, is_ok): """Handles the user attempting to close a dialog-based user interface. This method is called when the user attempts to close a window, by clicking an **OK** or **Cancel** button, or clicking a Close control on the window). It is called before the window is actually destroyed. Override this method to perform any checks before closing a window. While Traits UI handles "OK" and "Cancel" events automatically, you can use the value of the *is_ok* parameter to implement additional behavior. Parameters ---------- info : UIInfo object The UIInfo object associated with the view is_ok : Boolean Indicates whether the user confirmed the changes (such as by clicking **OK**.) Returns ------- allow_close : bool A Boolean, indicating whether the window should be allowed to close. """ return True def closed(self, info, is_ok): """Handles a dialog-based user interface being closed by the user. This method is called *after* the window is destroyed. Override this method to perform any clean-up tasks needed by the application. Parameters ---------- info : UIInfo object The UIInfo object associated with the view is_ok : Boolean Indicates whether the user confirmed the changes (such as by clicking **OK**.) """ return def revert(self, info): """Handles the **Revert** button being clicked.""" return def apply(self, info): """Handles the **Apply** button being clicked.""" return def show_help(self, info, control=None): """Shows the help associated with the view. This method is called when the user clicks a **Help** button in a Traits user interface. The method calls the global help handler, which might be the default help handler, or might be a custom help handler. See **traitsui.help** for details about the setting the global help handler. Parameters ---------- info : UIInfo object The UIInfo object associated with the view control : UI control The control that invokes the help dialog box """ if control is None: control = info.ui.control on_help_call()(info, control) def perform(self, info, action, event): """Perform computation for an action. The default method looks for a method matching ``action.action`` and calls it (sniffing the signature to determine how to call it for historical reasons). If this is not found, then it calls the :py:meth:`~traitsui.menu.Action.perform` method of the action. Parameters ---------- info : UIInfo instance The UIInfo assicated with the view, if available. action : Action instance The Action that the user invoked. event : ActionEvent instance The ActionEvent associated with the user action. Notes ----- If overriding in a subclass, the method needs to ensure that any standard menu action items that are needed (eg. "Close", "Undo", "Redo", "Help", etc.) get dispatched correctly. """ if action.action != "": method_name = action.action else: method_name = "_{}_clicked".format(action.name.lower()) for object in self.get_perform_handlers(info): method = getattr(object, method_name, None) if method is not None: # call the action method specification = getfullargspec(method) if len(specification.args) == 1: method() else: method(info) # and we are done return # otherwise, call the perform method of the action specification = getfullargspec(action.perform) if len(specification.args) == 1: action.perform() else: action.perform(event) def get_perform_handlers(self, info): """Return a list of objects which can handle actions. This method may be overridden by sub-classes to return a more relevant set of objects. Parameters ---------- info : UIInfo instance or None The UIInfo associated with the view, or None. Returns ------- handlers : list A list of objects that may potentially have action methods on them. """ handlers = [self] if info is not None: additional_objects = ["object", "model"] handlers += [ info.ui.context[name] for name in additional_objects if name in info.ui.context ] return handlers def setattr(self, info, object, name, value): """Handles the user setting a specified object trait's value. This method is called when an editor attempts to set a new value for a specified object trait attribute. Use this method to control what happens when a trait editor tries to set an attribute value. For example, you can use this method to record a history of changes, in order to implement an "undo" mechanism. No result is returned. The default implementation simply calls the built-in setattr() function. If you override this method, make sure that it actually sets the attribute, either by calling the parent method or by setting the attribute directly Parameters ---------- info : UIInfo instance The UIInfo for the current UI object : object The object whose attribute is being set name : string The name of the attribute being set value The value to which the attribute is being set """ setattr(object, name, value) def trait_view_for(self, info, view, object, object_name, trait_name): """Gets a specified View object.""" # If a view element was passed instead of a name or None, return it: if isinstance(view, ViewElement): return view # Generate a series of possible view or method names of the form: # - 'view' # trait_view_for_'view'( object ) # - 'class_view' # trait_view_for_'class_view'( object ) # - 'object_name_view' # trait_view_for_'object_name_view'( object ) # - 'object_name_class_view' # trait_view_for_'object_name_class_view'( object ) # where 'class' is the class name of 'object', 'object' is the object # name, and 'name' is the trait name. It returns the first view # or method result which is defined on the handler: klass = object.__class__.__name__ cname = "%s_%s" % (object_name, trait_name) aview = "" if view: aview = "_" + view names = [ "%s_%s%s" % (cname, klass, aview), "%s%s" % (cname, aview), "%s%s" % (klass, aview), ] if view: names.append(view) for name in names: result = self.trait_view(name) if result is not None: return result method = getattr(self, "trait_view_for_%s" % name, None) if callable(method): result = method(info, object) if result is not None: return result # If nothing is defined on the handler, return either the requested # view on the object itself, or the object's default view: return object.trait_view(view) or object.trait_view() # -- 'DockWindowHandler' interface implementation ------------------------- def can_drop(self, info, object): """Can the specified object be inserted into the view?""" from pyface.dock.api import DockControl if isinstance(object, DockControl): return self.can_import(info, object.export) drop_class = info.ui.view.drop_class return (drop_class is not None) and isinstance(object, drop_class) def can_import(self, info, category): return category in info.ui.view.imports def dock_control_for(self, info, parent, object): """Returns the DockControl object for a specified object.""" from pyface.dock.api import IDockable, DockControl from .dockable_view_element import DockableViewElement try: name = object.name except: try: name = object.label except: name = "" if len(name) == 0: name = user_name_for(object.__class__.__name__) image = None export = "" if isinstance(object, DockControl): dock_control = object image = dock_control.image export = dock_control.export dockable = dock_control.dockable close = dockable.dockable_should_close() if close: dock_control.close(force=True) control = dockable.dockable_get_control(parent) # If DockControl was closed, then reset it to point to the new # control: if close: dock_control.trait_set( control=control, style=parent.owner.style ) dockable.dockable_init_dockcontrol(dock_control) return dock_control elif isinstance(object, IDockable): dockable = object control = dockable.dockable_get_control(parent) else: ui = object.get_dockable_ui(parent) dockable = DockableViewElement(ui=ui) export = ui.view.export control = ui.control dc = DockControl( control=control, name=name, export=export, style=parent.owner.style, image=image, closeable=True, ) dockable.dockable_init_dockcontrol(dc) return dc def open_view_for(self, control, use_mouse=True): """Creates a new view of a specified control.""" from pyface.dock.api import DockWindowShell DockWindowShell(control, use_mouse=use_mouse) def dock_window_empty(self, dock_window): """Handles a DockWindow becoming empty.""" if dock_window.auto_close: dock_window.control.GetParent.Destroy() # -- HasTraits overrides: ------------------------------------------------- def edit_traits( self, view=None, parent=None, kind=None, context=None, handler=None, id="", scrollable=None, **args, ): """Edits the object's traits.""" if context is None: context = self if handler is None: handler = self return self.trait_view(view).ui( context, parent, kind, self.trait_view_elements(), handler, id, scrollable, args, ) def configure_traits( self, filename=None, view=None, kind=None, edit=True, context=None, handler=None, id="", scrollable=None, **args, ): """Configures the object's traits.""" return super().configure_traits( filename, view, kind, edit, context, handler or self, id, scrollable, **args, ) # -- Private Methods: ----------------------------------------------------- def _on_undo(self, info): """Handles an "Undo" change request.""" if info.ui.history is not None: info.ui.history.undo() def _on_redo(self, info): """Handles a "Redo" change request.""" if info.ui.history is not None: info.ui.history.redo() def _on_revert(self, info): """Handles a "Revert all changes" request.""" if info.ui.history is not None: info.ui.history.revert() self.revert(info) def _on_close(self, info): """Handles a "Close" request.""" if (info.ui.owner is not None) and self.close(info, True): info.ui.owner.close() # ------------------------------------------------------------------------- # Default handler: # ------------------------------------------------------------------------- _default_handler = Handler() def default_handler(handler=None): """Returns the global default handler. If *handler* is an instance of Handler, this function sets it as the global default handler. """ global _default_handler if isinstance(handler, Handler): _default_handler = handler return _default_handler class Controller(Handler): """Defines a handler class which provides a view and controller for a specified model. This class is used when implementing a standard MVC-based design. The **model** trait contains most, if not all, of the data being viewed, and can be referenced in a Controller instance's View definition using unadorned trait names. (e.g., ``Item('name')``). """ # -- Trait Definitions ---------------------------------------------------- #: The model this handler defines a view and controller for model = Instance(HasTraits) #: The Info object associated with the controller info = Instance(UIInfo) # -- HasTraits Method Overrides ------------------------------------------- def __init__(self, model=None, **metadata): """Initializes the object and sets the model (if supplied).""" super().__init__(**metadata) if model is not None: self.model = model def trait_context(self): """Returns the default context to use for editing or configuring traits. """ return {"object": self.model, "controller": self, "handler": self} # -- Handler Method Overrides --------------------------------------------- def get_perform_handlers(self, info): """Return a list of objects which can handle actions. By default this returns the Controller instance and the model. Parameters ---------- info : UIInfo instance or None The UIInfo associated with the view, or None. Returns ------- handlers : list A list of objects that may potentially have action methods on them. """ return [self, self.model] def init_info(self, info): """Informs the handler what the UIInfo object for a View will be.""" self.info = info class ModelView(Controller): """Defines a handler class which provides a view and controller for a specified model. This class is useful when creating a variant of the standard MVC-based design. A subclass of ModelView reformulates a number of traits on its **model** object as properties on the ModelView subclass itself, usually in order to convert them into a more user-friendly format. In this design, the ModelView subclass supplies not only the view and the controller, but also, in effect, the model (as a set of properties wrapped around the original model). Because of this, the ModelView context dictionary specifies the ModelView instance itself as the special *object* value, and assigns the original model object as the *model* value. Thus, the traits of the ModelView object can be referenced in its View definition using unadorned trait names. """ # -- HasTraits Method Overrides ------------------------------------------- def trait_context(self): """Returns the default context to use for editing or configuring traits. """ return {"object": self, "handler": self, "model": self.model} class ViewHandler(Handler): pass ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/help.py0000644000175100001730000000320200000000000017330 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Defines the help interface for displaying the help associated with a Traits UI View object. """ from .toolkit import toolkit def default_show_help(info, control): """Default handler for showing the help associated with a view.""" toolkit().show_help(info.ui, control) # The default handler for showing help show_help = default_show_help def on_help_call(new_show_help=None): """Sets a new global help provider function. The help provider function must have a signature of *function*(*info*, *control*), where *info* is a UIInfo object for the current view, and *control* is the UI control that invokes the function (typically, a **Help** button). It is provided in case the help provider needs to position the help window relative to the **Help** button. To retrieve the current help provider function, call this function with no arguments. Parameters ---------- new_show_help : function The function to set as the new global help provider Returns ------- previous : callable The previous global help provider function """ global show_help result = show_help if new_show_help is not None: show_help = new_show_help return result ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/help_template.py0000644000175100001730000000522000000000000021225 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Defines the HTML help templates used for formatting Traits UI help pages. """ from traits.api import HasStrictTraits, Str # Default HTML for a single Item's help window ItemHTML = """
    %s %s
    """ # Default HTML for a complete Group's help window GroupHTML = """ %s
    %s
    """ # Default HTML for a single Item within a Group ItemHelp = """ %s: %s """ # Default HTML for formatting a Group's 'help' trait GroupHelp = """
    %s
    """ # ------------------------------------------------------------------------- # 'HelpTemplate' class: # ------------------------------------------------------------------------- class HelpTemplate(HasStrictTraits): """Contains HTML templates for displaying help.""" item_html = Str(ItemHTML) # Item popup help window HTML document group_html = Str(GroupHTML) # Group help window HTML document item_help = Str(ItemHelp) # Single group item HTML group_help = Str(GroupHelp) # Group level help HTML no_group_help = Str("") # Missing group level help HTML # ------------------------------------------------------------------------- # Gets/Sets the current HelpTemplate in use: # ------------------------------------------------------------------------- _help_template = HelpTemplate() def help_template(template=None): """Gets or sets the current HelpTemplate in use.""" global _help_template if template is not None: _help_template = template return _help_template ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/helper.py0000644000175100001730000001377300000000000017675 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Defines various helper functions that are useful for creating Traits-based user interfaces. """ from operator import itemgetter from traits.api import BaseTraitHandler, CTrait, Enum, TraitError # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- # Layout orientation for a control and its associated editor Orientation = Enum("horizontal", "vertical") # Docking drag bar style: DockStyle = Enum("horizontal", "vertical", "tab", "fixed") def user_name_for(name): """Returns a "user-friendly" name for a specified trait.""" name = name.replace("_", " ") name = name[:1].upper() + name[1:] result = "" last_lower = 0 for c in name: if c.isupper() and last_lower: result += " " last_lower = c.islower() result += c return result def commatize(value): """Formats a specified value as an integer string with embedded commas. For example: commatize( 12345 ) returns "12,345". """ s = str(abs(value)) s = s.rjust(((len(s) + 2) / 3) * 3) result = ",".join([s[i : i + 3] for i in range(0, len(s), 3)]).lstrip() if value >= 0: return result return "-" + result def enum_values_changed(values, strfunc=str): """Recomputes the mappings for a new set of enumeration values.""" if isinstance(values, dict): data = [(strfunc(v), n) for n, v in values.items()] if len(data) > 0: data.sort(key=itemgetter(0)) col = data[0][0].find(":") + 1 if col > 0: data = [(n[col:], v) for n, v in data] elif not isinstance(values, SequenceTypes): handler = values if isinstance(handler, CTrait): handler = handler.handler if not isinstance(handler, BaseTraitHandler): raise TraitError("Invalid value for 'values' specified") if handler.is_mapped: data = [(strfunc(n), n) for n in handler.map.keys()] data.sort(key=itemgetter(0)) else: data = [(strfunc(v), v) for v in handler.values] else: data = [(strfunc(v), v) for v in values] names = [x[0] for x in data] mapping = {} inverse_mapping = {} for name, value in data: mapping[name] = value inverse_mapping[value] = name return (names, mapping, inverse_mapping) def compute_column_widths(available_space, requested, min_widths, user_widths): """Distribute column space amongst columns based on requested space. Widths requests can be specified as one of the following: - a value greater than 1.0 is treated as a fixed width with no flexibility (ie. a minimum width as specified and a weight of 0.0) - a value between 0.0 and 1.0 is treaded as a flexible width column with the specified width as a weight and a minimum width provided by the min_widths entry. - a value less than or equal to 0.0 is treated as a flexible width column with a weight of 0.1 and a minimum width provided by the min_widths parameter. If user widths are supplied then any non-None values override the requested widths, and are treated as having a flexibility of 0. Space is distributed by evaluating each column from smallest weight to largest and seeing if the weighted proportion of the remaining space is more than the minimum, and if so replacing the width with the weighted width. The column is then removed from the available width and the total weight and the analysis continues. Parameters ---------- available_space : int The available horizontal space. requested : list of numbers The requested width or weight for each column. min_widths : None or list of ints The minimum width for each flexible column user_widths : None or list of int or None Any widths specified by the user resizing the columns manually. Returns ------- widths : list of ints The assigned width for each column """ widths = [] weights = [] if min_widths is None: min_widths = [30] * len(requested) # determine flexibility and default width of each column for request, min_width in zip(requested, min_widths): if request >= 1.0: weights.append(0.0) widths.append(int(request)) else: if request <= 0: weights.append(0.1) else: weights.append(request) widths.append(min_width) # if the user has changed the width of a column manually respect that if user_widths is not None: for i, user_width in enumerate(user_widths): if user_width is not None: widths[i] = user_width weights[i] = 0.0 total_weight = sum(weights) if sum(widths) < available_space and total_weight > 0: # do inflexible first, then work up from smallest to largest for i, weight in sorted(enumerate(weights), key=itemgetter(1, 0)): total_weight = sum(weights) stretched = int(weight / total_weight * available_space) widths[i] = max(stretched, widths[i]) # once we have dealt with a column, it no longer counts as flexible # and its space is no longer available weights[i] = 0.0 available_space -= widths[i] return widths # ------------------------------------------------------------------------- # Other definitions: # ------------------------------------------------------------------------- SequenceTypes = (tuple, list) ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1684768028.087807 traitsui-8.0.0/traitsui/images/0000755000175100001730000000000000000000000017276 5ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/images/frame.png0000644000175100001730000000175700000000000021110 0ustar00runnerdocker00000000000000PNG  IHDRabKGD]6G{ pHYs  tIME -ڝ|IDAT8MmHuN).ٙclȈE+!ы 7.jXzbBADE/FAsf7M6yv=~؛?_AzVɛv5^'7,~\]ώ$b7qLŅzMW_{5)Jqqf撰,\k|CmtcM[b1XxZ`H?+e9xI4@^_!%sPjJIzt1~ĝ0Bz GVK"%)-#7quN~y[9¡0--FI]NVeh{B k[bkH9q/g~J\w$94>bSáx<Đ|1Kl{)3*JFjݼMoSAHMڻŊ3jڗWʋZTi#GG57o|>ITD8."ƹw-Ρ'[ۛJr+i|}'m0:6S|7s?G^_\b԰g#Oooti·h + LU$' e$$,U*+ C3_-|Zh2xoy Y8,%3bM'LYuaHoYm"9s0!$:ߏ~PJ]7X+E94jUC9 VVCQT]][?-Q;/0IENDB`././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/include.py0000644000175100001730000000500400000000000020025 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Defines the Include class, which is used to represent a substitutable element within a user interface View. """ from traits.api import Str from .view_element import ViewSubElement class Include(ViewSubElement): """A substitutable user interface element, i.e., a placeholder in a view definition. When a view object constructs an attribute-editing window, any Include objects within the view definition are replaced with a group or item defined elsewhere in the object's inheritance tree, based on matching of the name of the element. If no matching element is found, the Include object is ignored. An Include object can reference a group or item attribute on a parent class or on a subclass. For example, the following class contains a view definition that provides for the possibility that a subclass might add "extra" attributes in the middle of the view:: class Person(HasTraits): name = Str() age = Int() person_view = View('name', Include('extra'), 'age', kind='modal') If you directly create an instance of Person, and edit its attributes, the Include object is ignored. The following class extends Person, and defines a group of "extra" attributes to add to the view defined on Person:: class LocatedPerson(Person): street = Str() city = Str() state = Str() zip = Int() extra = Group('street', 'city', 'state', 'zip') The attribute-editing window for an instance of LocatedPerson displays editors for these extra attributes. """ # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- #: The name of the substitutable content id = Str() def __init__(self, id, **traits): """Initializes the Include object.""" super().__init__(**traits) self.id = id def __repr__(self): """Returns a "pretty print" version of the Include object.""" return "<%s>" % self.id ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/instance_choice.py0000644000175100001730000001257700000000000021535 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Defines the various instance descriptors used by the instance editor and instance editor factory classes. """ from abc import abstractmethod from traits.api import ( ABCHasStrictTraits, Str, Any, Dict, Tuple, Callable, Bool, ) from .ui_traits import AView from .helper import user_name_for class InstanceChoiceItem(ABCHasStrictTraits): # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- #: User interface name for the item name = Str() #: View associated with this item view = AView #: Does this item create new instances? is_factory = Bool(False) def get_name(self, object=None): """Returns the name of the item.""" return self.name def get_view(self): """Returns the view associated with the object.""" return self.view @abstractmethod def get_object(self): """Returns the object associated with the item.""" pass @abstractmethod def is_compatible(self, object): """Indicates whether a specified object is compatible with the item.""" pass def is_selectable(self): """Indicates whether the item can be selected by the user.""" return True def is_droppable(self): """Indicates whether the item supports drag and drop.""" return False class InstanceChoice(InstanceChoiceItem): # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- #: Object associated with the item object = Any() #: The name of the object trait containing its user interface name: name_trait = Str("name") def get_name(self, object=None): """Returns the name of the item.""" if self.name != "": return self.name name = getattr(self.object, self.name_trait, None) if isinstance(name, str): return name return user_name_for(self.object.__class__.__name__) def get_object(self): """Returns the object associated with the item.""" return self.object def is_compatible(self, object): """Indicates whether a specified object is compatible with the item.""" return object is self.object class InstanceFactoryChoice(InstanceChoiceItem): # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- #: Indicates whether an instance compatible with this item can be dragged #: and dropped rather than created droppable = Bool(False) #: Indicates whether the item can be selected by the user selectable = Bool(True) #: A class (or other callable) that can be used to create an item #: compatible with this item klass = Callable() #: Tuple of arguments to pass to **klass** to create an instance args = Tuple() #: Dictionary of arguments to pass to **klass** to create an instance kw_args = Dict(Str, Any) #: Does this item create new instances? This value overrides the default. is_factory = True def get_name(self, object=None): """Returns the name of the item.""" if self.name != "": return self.name name = getattr(object, "name", None) if isinstance(name, str): return name if issubclass(type(self.klass), type): klass = self.klass else: klass = self.get_object().__class__ return user_name_for(klass.__name__) def get_object(self): """Returns the object associated with the item.""" return self.klass(*self.args, **self.kw_args) def is_droppable(self): """Indicates whether the item supports drag and drop.""" return self.droppable def is_compatible(self, object): """Indicates whether a specified object is compatible with the item.""" if issubclass(type(self.klass), type): return isinstance(object, self.klass) return isinstance(object, self.get_object().__class__) def is_selectable(self): """Indicates whether the item can be selected by the user.""" return self.selectable class InstanceDropChoice(InstanceFactoryChoice): # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- #: Indicates whether an instance compatible with this item can be dragged #: and dropped rather than created . This value overrides the default. droppable = True #: Indicates whether the item can be selected by the user. This value #: overrides the default. selectable = False #: Does this item create new instances? This value overrides the default. is_factory = False ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/item.py0000644000175100001730000004550100000000000017346 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Defines the Item class, which is used to represent a single item within a Traits-based user interface. """ import re from traits.api import ( Bool, Callable, Constant, Delegate, Float, Instance, Range, Str, Undefined, Dict, ) from traits.trait_base import user_name_for from .view_element import ViewSubElement from .ui_traits import ContainerDelegate, EditorStyle from .editor_factory import EditorFactory # Pattern of all digits: all_digits = re.compile(r"\d+") # Pattern for finding size infomation embedded in an item description: size_pat = re.compile(r"^(.*)<(.*)>(.*)$", re.MULTILINE | re.DOTALL) # Pattern for finding tooltip infomation embedded in an item description: tooltip_pat = re.compile(r"^(.*)`(.*)`(.*)$", re.MULTILINE | re.DOTALL) # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- # Reference to an EditorFactory: ItemEditor = Instance(EditorFactory, allow_none=True) # Amount of padding to add around an item: Padding = Range(-15, 15, 0, desc="amount of padding to add around item") # ------------------------------------------------------------------------- # 'Item' class: # ------------------------------------------------------------------------- class Item(ViewSubElement): """An element in a Traits-based user interface. Magic: - Items are rendered as layout elements if :attr:`name` is set to special values: * ``name=''``, the item is rendered as a static label * ``name='_'``, the item is rendered as a separator * ``name=' '``, the item is rendered as a 5 pixel spacer * ``name='23'`` (any number), the item is rendered as a spacer of the size specified (number of pixels) """ # FIXME: all the logic for the name = '', '_', ' ', '23' magic is in # _GroupPanel._add_items in qt/ui_panel.py, which is a very unlikely place # to look for it. Ideally, that logic should be in this class. # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- #: A unique identifier for the item. If not set, it defaults to the value #: of **name**. id = Str() #: User interface label for the item in the GUI. If this attribute is not #: set, the label is the value of **name** with slight modifications: #: underscores are replaced by spaces, and the first letter is capitalized. #: If an item's **name** is not specified, its label is displayed as #: static text, without any editor widget. label = Str() #: Name of the trait the item is editing: name = Str() #: Style-sheet to apply to item / group (Qt only) style_sheet = Str() #: Help text describing the purpose of the item. The built-in help handler #: displays this text in a pop-up window if the user clicks the widget's #: label. View-level help displays the help text for all items in a view. #: If this attribute is not set, the built-in help handler generates a #: description based on the trait definition. help = Str() #: The HasTraits object whose trait attribute the item is editing: object = ContainerDelegate #: Presentation style for the item: style = ContainerDelegate #: Docking style for the item: dock = ContainerDelegate #: Image to display on notebook tabs: image = ContainerDelegate #: Category of elements dragged from view: export = ContainerDelegate #: Should a label be displayed for the item? show_label = Delegate("container", "show_labels") #: Editor to use for the item: editor = ItemEditor #: Additional editor traits to be set if default traits editor to be used: editor_args = Dict() #: Should the item use extra space along its Group's non-layout axis? If set to #: True, the widget expands to fill any extra space that is available in the #: display. If set to True for more than one item in the same View, any extra #: space is divided between them. If set to False, the widget uses only #: whatever space it is explicitly (or implicitly) assigned. The default #: value of Undefined means that the use (or non-use) of extra space will be #: determined by the editor associated with the item. resizable = Bool(Undefined) #: Should the item use extra space along its Group's layout axis? For #: example, it a vertical group, should an item expand vertically to use #: any extra space available in the group? springy = Bool(False) #: Should the item's label use emphasized text? If the label is not shown, #: this attribute is ignored. emphasized = Bool(False) #: Should the item receive focus initially? has_focus = Bool(False) #: Pre-condition for including the item in the display. If the expression #: evaluates to False, the item is not defined in the display. Conditions #: for **defined_when** are evaluated only once, when the display is first #: constructed. Use this attribute for conditions based on attributes that #: vary from object to object, but that do not change over time. For example, #: displaying a 'maiden_name' item only for female employees in a company #: database. defined_when = Str() #: Pre-condition for showing the item. If the expression evaluates to False, #: the widget is not visible (and disappears if it was previously visible). #: If the value evaluates to True, the widget becomes visible. All #: **visible_when** conditions are checked each time that any trait value #: on an object in the UI's context is changed. Therefore, you can use #: **visible_when** conditions to hide or show widgets in response to user #: input. Be aware that this only applies to traits in the UI's context. As #: a result, changes to nested traits that don't also change a trait on #: some object in the context may not trigger the expression to be checked. #: Additionally, the expression needs to be a valid python expression given #: the context. i.e. eval(visible_when, globals=globals(), locals=context) #: should succeed. visible_when = Str() #: Pre-condition for enabling the item. If the expression evaluates to False, #: the widget is disabled, that is, it does not accept input. All #: **enabled_when** conditions are checked each time that any trait value #: on an object in the UI's context is changed. Therefore, you can use #: **enabled_when** conditions to enable or disable widgets in response to #: user input. Be aware that this only applies to traits in the UI's #: context. As a result, changes to nested traits that don't also change a #: trait on some object in the context may not trigger the expression to be #: checked. Additionally, the expression needs to be a valid python #: expression given the context. i.e. #: eval(enabled_when, globals=globals(), locals=context) should succeed. enabled_when = Str() #: Amount of extra space, in pixels, to add around the item. Values must be #: integers between -15 and 15. Use negative values to subtract from the #: default spacing. padding = Padding #: Tooltip to display over the item, when the mouse pointer is left idle #: over the widget. Make this text as concise as possible; use the **help** #: attribute to provide more detailed information. tooltip = Str() #: A Callable to use for formatting the contents of the item. This function #: or method is called to create the string representation of the trait value #: to be edited. If the widget does not use a string representation, this #: attribute is ignored. format_func = Callable() #: Python format string to use for formatting the contents of the item. #: The format string is applied to the string representation of the trait #: value before it is displayed in the widget. This attribute is ignored if #: the widget does not use a string representation, or if the #: **format_func** is set. format_str = Str() #: Requested width of the editor (in pixels or fraction of available width). #: For pixel values (i.e. values not in the range from 0.0 to 1.0), the #: actual displayed width is at least the maximum of **width** and the #: optimal width of the widget as calculated by the GUI toolkit. Specify a #: negative value to ignore the toolkit's optimal width. For example, use #: -50 to force a width of 50 pixels. The default value of -1 ensures that #: the toolkit's optimal width is used. #: #: A value in the range from 0.0 to 1.0 specifies the fraction of the #: available width to assign to the editor. Note that the value is not an #: absolute value, but is relative to other item's whose **width** is also #: in the 0.0 to 1.0 range. For example, if you have two item's with a width #: of 0.1, and one item with a width of 0.2, the first two items will each #: receive 25% of the available width, while the third item will receive #: 50% of the available width. The available width is the total width of the #: view minus the width of any item's with fixed pixel sizes (i.e. width #: values not in the 0.0 to 1.0 range). width = Float(-1.0) #: Requested height of the editor (in pixels or fraction of available #: height). For pixel values (i.e. values not in the range from 0.0 to 1.0), #: the actual displayed height is at least the maximum of **height** and the #: optimal height of the widget as calculated by the GUI toolkit. Specify a #: negative value to ignore the toolkit's optimal height. For example, use #: -50 to force a height of 50 pixels. The default value of -1 ensures that #: the toolkit's optimal height is used. #: #: A value in the range from 0.0 to 1.0 specifies the fraction of the #: available height to assign to the editor. Note that the value is not an #: absolute value, but is relative to other item's whose **height** is also #: in the 0.0 to 1.0 range. For example, if you have two item's with a height #: of 0.1, and one item with a height of 0.2, the first two items will each #: receive 25% of the available height, while the third item will receive #: 50% of the available height. The available height is the total height of #: the view minus the height of any item's with fixed pixel sizes (i.e. #: height values not in the 0.0 to 1.0 range). height = Float(-1.0) #: The extended trait name of the trait containing the item's invalid state #: status (passed through to the item's editor): invalid = Str() def __init__(self, value=None, **traits): """Initializes the item object.""" super().__init__(**traits) if value is None: return if not isinstance(value, str): raise TypeError( "The argument to Item must be a string of the " "form: [id:][object.[object.]*][name]['['label']']`tooltip`" "[][#^][$|@|*|~|;style]" ) value, empty = self._parse_label(value) if empty: self.show_label = False value = self._parse_style(value) value = self._parse_size(value) value = self._parse_tooltip(value) value = self._option(value, "#", "resizable", True) value = self._option(value, "^", "emphasized", True) value = self._split("id", value, ":", str.find, 0, 1) value = self._split("object", value, ".", str.rfind, 0, 1) if value != "": self.name = value def is_includable(self): """Returns a Boolean indicating whether the object is replaceable by an Include object. """ return self.id != "" def is_spacer(self): """Returns True if the item represents a spacer or separator.""" name = self.name.strip() return ( (name == "") or (name == "_") or (all_digits.match(name) is not None) ) def get_help(self, ui): """Gets the help text associated with the Item in a specified UI.""" # Return 'None' if the Item is a separator or spacer: if self.is_spacer(): return None # Otherwise, it must be a trait Item: if self.help != "": return self.help object = eval(self.object_, globals(), ui.context) return object.base_trait(self.name).get_help() def get_label(self, ui): """Gets the label to use for a specified Item. If not specified, the label is set as the name of the corresponding trait, replacing '_' with ' ', and capitalizing the first letter (see :func:`user_name_for`). This is called the *user name*. Magic: - if attr:`item.label` is specified, and it begins with '...', the final label is the user name followed by the item label - if attr:`item.label` is specified, and it ends with '...', the final label is the item label followed by the user name """ # Return 'None' if the Item is a separator or spacer: if self.is_spacer(): return None label = self.label if label != "": return label name = self.name object = eval(self.object_, globals(), ui.context) trait = object.base_trait(name) label = user_name_for(name) tlabel = trait.label if tlabel is None: return label if isinstance(tlabel, str): if tlabel[0:3] == "...": return label + tlabel[3:] if tlabel[-3:] == "...": return tlabel[:-3] + label if self.label != "": return self.label return tlabel return tlabel(object, name, label) def get_id(self): """Returns an ID used to identify the item.""" if self.id != "": return self.id return self.name def _parse_size(self, value): """Parses a '' value from the string definition.""" match = size_pat.match(value) if match is not None: data = match.group(2) value = match.group(1) + match.group(3) col = data.find(",") if col < 0: self._set_float("width", data) else: self._set_float("width", data[:col]) self._set_float("height", data[col + 1 :]) return value def _parse_tooltip(self, value): """Parses a *tooltip* value from the string definition.""" match = tooltip_pat.match(value) if match is not None: self.tooltip = match.group(2) value = match.group(1) + match.group(3) return value def _set_float(self, name, value): """Sets a specified trait to a specified string converted to a float.""" value = value.strip() if value != "": setattr(self, name, float(value)) def __repr__(self): """Returns a "pretty print" version of the Item.""" options = self._repr_options( "id", "object", "label", "style", "show_label", "width", "height" ) if options is None: return "Item( '%s' )" % self.name return "Item( '%s'\n%s\n)" % ( self.name, self._indent(options, " "), ) # ------------------------------------------------------------------------- # 'UItem' class: # ------------------------------------------------------------------------- class UItem(Item): """An Item that has no label.""" show_label = Bool(False) # ------------------------------------------------------------------------- # 'Custom' class: # ------------------------------------------------------------------------- class Custom(Item): """An Item using a 'custom' style.""" style = EditorStyle("custom") # ------------------------------------------------------------------------- # 'UCustom' class: # ------------------------------------------------------------------------- class UCustom(Custom): """An Item using a 'custom' style with no label.""" show_label = Bool(False) # ------------------------------------------------------------------------- # 'Readonly' class: # ------------------------------------------------------------------------- class Readonly(Item): """An Item using a 'readonly' style.""" style = EditorStyle("readonly") # ------------------------------------------------------------------------- # 'UReadonly' class: # ------------------------------------------------------------------------- class UReadonly(Readonly): """An Item using a 'readonly' style with no label.""" show_label = Bool(False) # ------------------------------------------------------------------------- # 'Label' class: # ------------------------------------------------------------------------- class Label(Item): """An item that is a label.""" def __init__(self, label, **traits): super().__init__(label=label, **traits) # ------------------------------------------------------------------------- # 'Heading' class: # ------------------------------------------------------------------------- class Heading(Label): """An item that is a fancy label.""" #: Override the 'style' trait to default to the fancy 'custom' style: style = Constant("custom") # ------------------------------------------------------------------------- # 'Spring' class: # ------------------------------------------------------------------------- class Spring(Item): """An item that is a layout "spring".""" # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- #: Name of the trait the item is editing #: Just a dummy trait that exists on all HasTraits objects. It's an Event, #: so it won't cause Traits UI to add any synchronization, and because it #: already exists, it won't force the addition of a new trait with a bogus #: name. name = "trait_modified" #: Should a label be displayed? show_label = Bool(False) #: Editor to use for the item editor = Instance("traitsui.api.NullEditor", ()) #: Should the item use extra space along its Group's layout orientation? springy = True # A pre-defined spring for convenience spring = Spring() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/key_bindings.py0000644000175100001730000002325300000000000021055 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Defines KeyBinding and KeyBindings classes, which manage the mapping of keystroke events into method calls on controller objects that are supplied by the application. """ from traits.api import ( Any, Event, HasPrivateTraits, HasStrictTraits, Instance, List, Property, Str, cached_property, observe, ) from traits.trait_base import SequenceTypes from .editors.key_binding_editor import KeyBindingEditor from .editors.list_editor import ListEditor from .group import HGroup from .handler import ModelView from .item import Item from .toolkit import toolkit from .view import View #: Trait definition for key bindings Binding = Str(editor=KeyBindingEditor()) class KeyBinding(HasStrictTraits): """Binds one or two keystrokes to a method.""" # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- #: First key binding binding1 = Binding #: Second key binding binding2 = Binding #: Description of what application function the method performs description = Str() #: Name of controller method the key is bound to method_name = Str() #: KeyBindings object that "owns" the KeyBinding owner = Instance("KeyBindings") def match(self, binding): return any(binding == x for x in {self.binding1, self.binding2}) def clear_binding(self, binding): if binding == self.binding1: self.binding1 = self.binding2 self.binding2 = "" if binding == self.binding2: self.binding2 = "" # ------------------------------------------------------------------------- # Traits view definitions: # ------------------------------------------------------------------------- traits_view = View( HGroup( Item("binding1"), Item("binding2"), Item("description", style="readonly"), show_labels=False, ) ) class KeyBindings(HasPrivateTraits): """A set of key bindings.""" # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- #: Set of defined key bindings (redefined dynamically) bindings = List(Instance(KeyBinding)) #: Optional prefix to add to each method name prefix = Str() #: Optional suffix to add to each method name suffix = Str() # -- Private Traits ------------------------------------------------------- #: The (optional) list of controllers associated with this KeyBindings #: object. The controllers may also be provided with the 'do' method: controllers = List(transient=True) #: The 'parent' KeyBindings object of this one (if any): parent = Instance("KeyBindings", transient=True) #: The root of the KeyBindings tree this object is part of: root = Property(observe="parent") #: The child KeyBindings of this object (if any): children = List(transient=True) # ------------------------------------------------------------------------- # Traits view definitions: # ------------------------------------------------------------------------- def __init__(self, *bindings, **traits): # initialize bindings if len(bindings) == 1 and isinstance(bindings[0], SequenceTypes): bindings = bindings[0] traits.setdefault("bindings", list(bindings)) for binding in traits["bindings"]: binding.owner = self super().__init__(**traits) def do(self, event, controllers=[], *args, **kw): """Processes a keyboard event.""" if isinstance(controllers, dict): controllers = list(controllers.values()) elif not isinstance(controllers, SequenceTypes): controllers = [controllers] else: controllers = list(controllers) return self._do( toolkit().key_event_to_name(event), controllers, args, kw.get("recursive", False), ) def merge(self, key_bindings): """Merges another set of key bindings into this set.""" binding_dic = {} for binding in self.bindings: binding_dic[binding.method_name] = binding for binding in key_bindings.bindings: binding2 = binding_dic.get(binding.method_name) if binding2 is not None: binding2.binding1 = binding.binding1 binding2.binding2 = binding.binding2 def clone(self, **traits): """Returns a clone of the KeyBindings object.""" return self.__class__(*self.bindings, **traits).trait_set( **self.trait_get("prefix", "suffix") ) def dispose(self): """Dispose of the object.""" if self.parent is not None: self.parent.children.remove(self) del self.controllers del self.children del self.bindings self.parent = self._root = None def edit(self): """Edits a possibly hierarchical set of KeyBindings.""" model_view = KeyBindingsHandler(model=self) ui = model_view.edit_traits() # -- Property Implementations --------------------------------------------- @cached_property def _get_root(self): root = self while root.parent is not None: root = root.parent return root # -- Event Handlers ------------------------------------------------------- @observe('bindings:items:[binding1,binding2]') def _binding_updated(self, event): if event.new != "": for a_binding in self._match_binding(event.new, skip={event.object}): a_binding.clear_binding(event.new) @observe("children.items") def _children_modified(self, event): """Handles child KeyBindings being added to the object.""" # the full children list is changed if isinstance(event.object, KeyBindings): for item in event.new: item.parent = self # the contents of the children list are changed else: for item in event.added: item.parent = self # -- Private Methods ------------------------------------------------------ def _get_bindings(self, bindings): """Returns all of the bindings of this object and all of its children.""" bindings.extend(self.bindings) for child in self.children: child._get_bindings(bindings) return bindings def _do(self, key_name, controllers, args, recursive): """Process the specified key for the specified set of controllers for this KeyBindings object and all of its children. """ # Search through our own bindings for a match: for binding in self._match_binding(key_name): method_name = "%s%s%s" % ( self.prefix, binding.method_name, self.suffix, ) for controller in controllers + self.controllers: method = getattr(controller, method_name, None) if method is not None: result = method(*args) if result is not False: return True if binding.method_name == "edit_bindings": self.edit() return True # If recursive, continue searching through a children's bindings: if recursive: for child in self.children: if child._do(key_name, controllers, args, recursive): return True # Indicate no one processed the key: return False def _match_binding(self, binding, skip=frozenset()): """Return all KeyBinding instances that match the given binding. """ return ( a_binding for a_binding in self.bindings if a_binding not in skip and a_binding.match(binding) ) class KeyBindingsHandler(ModelView): bindings = List(Instance(KeyBinding)) def key_binding_for(self, binding, key_name): """Returns the current binding for a specified key (if any).""" if key_name != "": for a_binding in self._match_binding(key_name, skip={binding}): return a_binding return None def _match_binding(self, binding, skip=frozenset()): """Return all KeyBinding instances that match the given binding. """ return ( a_binding for a_binding in self.bindings if a_binding not in skip and a_binding.match(binding) ) def _bindings_default(self): bindings = list(set(self.model.root._get_bindings([]))) bindings.sort(key=lambda x: (x.binding1[-1:], x.binding1)) return bindings traits_view = View( [ Item( "bindings", style="readonly", show_label=False, editor=ListEditor(style="custom"), ), "|{Click on an entry field, then press the key to " "assign. Double-click a field to clear it.}<>", ], title="Update Key Bindings", kind="livemodal", resizable=True, width=0.4, height=0.4, ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/list_str_adapter.py0000644000175100001730000002746400000000000021763 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Defines adapter interfaces for use with the ListStrEditor. """ from traits.api import ( Any, Bool, Dict, Enum, Event, HasPrivateTraits, Int, Interface, List, Str, observe, provides, ) from .toolkit_traits import Color # ------------------------------------------------------------------------- # 'IListStrAdapter' interface: # ------------------------------------------------------------------------- class IListStrAdapter(Interface): #: The index of the current item being adapted. index = Int() #: Current item being adapted. item = Any() #: The current value (if any). value = Any() #: Does the adapter know how to handle the current *item* or not? accepts = Bool() #: Does the value of *accepts* depend only upon the type of *item*? is_cacheable = Bool() # ------------------------------------------------------------------------- # 'AnIListStrAdapter' class: # ------------------------------------------------------------------------- @provides(IListStrAdapter) class AnIListStrAdapter(HasPrivateTraits): # Implementation of the IListStrAdapter Interface ------------------------ #: The index of the current item being adapted. index = Int() #: Current item being adapted. item = Any() #: The current value (if any). value = Any() #: Does the adapter know how to handle the current *item* or not? accepts = Bool(True) #: Does the value of *accepts* depend only upon the type of *item*? is_cacheable = Bool(True) # ------------------------------------------------------------------------- # 'ListStrAdapter' class: # ------------------------------------------------------------------------- class ListStrAdapter(HasPrivateTraits): """The base class for adapting list items to values that can be edited by a ListStrEditor. """ # Trait Definitions ------------------------------------------------------ #: Specifies the default value for a new list item. default_value = Any("") #: Specifies the default text for a new list item. default_text = Str() #: The default text color for even list items. even_text_color = Color(None, update=True) #: The default text color for odd list items. odd_text_color = Color(None, update=True) #: The default text color for list items. text_color = Color(None, update=True) #: The default background color for even list items. even_bg_color = Color(None, update=True) #: The default background color for odd list items. odd_bg_color = Color(None, update=True) #: The default background color for list items. bg_color = Color(None, update=True) #: The name of the default image to use for list items. image = Str(None, update=True) #: Can the text value of each list item be edited. can_edit = Bool(True) #: Specifies where a dropped item should be placed in the list relative to #: the item it is dropped on. dropped = Enum("after", "before") #: The index of the current item being adapter. index = Int() #: The current item being adapted. item = Any() #: The current value (if any). value = Any() #: The tooltip information for a item. tooltip = Str(update=True) #: List of optional delegated adapters. adapters = List(IListStrAdapter, update=True) # -- Private Trait Definitions -------------------------------------------- #: Cache of attribute handlers. cache = Dict() #: Event fired when the cache is flushed. cache_flushed = Event(update=True) # -- Adapter methods that are sensitive to item type ---------------------- def get_can_edit(self, object, trait, index): """Returns whether the user can edit a specified *object.trait[index]* list item. A True result indicates the value can be edited, while a False result indicates that it cannot be edited. """ return self._result_for("get_can_edit", object, trait, index) def get_drag(self, object, trait, index): """Returns the 'drag' value for a specified *object.trait[index]* list item. A result of *None* means that the item cannot be dragged. """ return self._result_for("get_drag", object, trait, index) def get_can_drop(self, object, trait, index, value): """Returns whether the specified *value* can be dropped on the specified *object.trait[index]* list item. A value of **True** means the *value* can be dropped; and a value of **False** indicates that it cannot be dropped. """ return self._result_for("get_can_drop", object, trait, index, value) def get_dropped(self, object, trait, index, value): """Returns how to handle a specified *value* being dropped on a specified *object.trait[index]* list item. The possible return values are: 'before' Insert the specified *value* before the dropped on item. 'after' Insert the specified *value* after the dropped on item. """ return self._result_for("get_dropped", object, trait, index, value) def get_text_color(self, object, trait, index): """Returns the text color for a specified *object.trait[index]* list item. A result of None means use the default list item text color. """ return self._result_for("get_text_color", object, trait, index) def get_bg_color(self, object, trait, index): """Returns the background color for a specified *object.trait[index]* list item. A result of None means use the default list item background color. """ return self._result_for("get_bg_color", object, trait, index) def get_image(self, object, trait, index): """Returns the name of the image to use for a specified *object.trait[index]* list item. A result of None means no image should be used. Otherwise, the result should either be the name of the image, or an ImageResource item specifying the image to use. """ return self._result_for("get_image", object, trait, index) def get_item(self, object, trait, index): """Returns the value of the *object.trait[index]* list item.""" return self._result_for("get_item", object, trait, index) def get_text(self, object, trait, index): """Returns the text to display for a specified *object.trait[index]* list item. """ return self._result_for("get_text", object, trait, index) def get_tooltip(self, object, trait, index): """Returns a string containing the tooltip to display for a specified *object.trait[index]* list item. Users should return *self.tooltip* to use the Adapter's default, or the empty string to disable the tooltip for this row (in which case the Item's tooltip, if present, will be shown on QT). """ return self._result_for("get_tooltip", object, trait, index) # -- Adapter methods that are not sensitive to item type ------------------ def len(self, object, trait): """Returns the number of items in the specified *object.trait* list.""" # Sometimes, during shutdown, the object has been set to None. if object is None: return 0 else: return len(getattr(object, trait)) def get_default_value(self, object, trait): """Returns a new default value for the specified *object.trait* list.""" return self.default_value def get_default_text(self, object, trait): """Returns the default text for the specified *object.trait* list.""" return self.default_text def get_default_image(self, object, trait): """Returns the default image for the specified *object.trait* list.""" return self.image def get_default_bg_color(self, object, trait): """Returns the default background color for the specified *object.trait* list. """ return self._get_bg_color() def get_default_text_color(self, object, trait): """Returns the default text color for the specified *object.trait* list. """ return self._get_text_color() def set_text(self, object, trait, index, text): """Sets the text for a specified *object.trait[index]* list item to *text*. """ getattr(object, trait)[index] = text def delete(self, object, trait, index): """Deletes the specified *object.trait[index]* list item.""" del getattr(object, trait)[index] def insert(self, object, trait, index, value): """Inserts a new value at the specified *object.trait[index]* list index. """ getattr(object, trait)[index:index] = [value] # -- Private Adapter Implementation Methods ------------------------------- def _get_can_edit(self): return self.can_edit def _get_drag(self): return str(self.item) def _get_can_drop(self): return isinstance(self.value, str) def _get_dropped(self): return self.dropped def _get_text_color(self): if (self.index % 2) == 0: return self.even_text_color_ or self.text_color_ return self.odd_text_color or self.text_color_ def _get_bg_color(self): if (self.index % 2) == 0: return self.even_bg_color_ or self.bg_color_ return self.odd_bg_color or self.bg_color_ def _get_image(self): return self.image def _get_item(self): return self.item def _get_text(self): return str(self.item) def _get_tooltip(self): return self.tooltip # -- Private Methods ------------------------------------------------------ def _result_for(self, name, object, trait, index, value=None): """Returns/Sets the value of the specified *name* attribute for the specified *object.trait[index]* list item. """ self.index = index self.value = value items = getattr(object, trait) if index >= len(items): self.item = item = None else: self.item = item = items[index] item_class = item.__class__ key = "%s:%s" % (item_class.__name__, name) handler = self.cache.get(key) if handler is not None: return handler() trait_name = name[4:] for adapter in self.adapters: adapter.index = index adapter.item = item adapter.value = value if adapter.accepts and (adapter.trait(trait_name) is not None): handler = lambda: getattr( adapter.trait_set( index=self.index, item=self.item, value=self.value ), trait_name, ) if adapter.is_cacheable: break return handler() else: for klass in item_class.__mro__: cname = "%s_%s" % (klass.__name__, trait_name) if self.trait(cname) is not None: handler = lambda: getattr(self, cname) break else: handler = getattr(self, "_" + name) self.cache[key] = handler return handler() @observe("adapters.items.+update") def _flush_cache(self, event): """Flushes the cache when any trait on any adapter changes.""" self.cache = {} self.cache_flushed = True ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/menu.py0000644000175100001730000001623700000000000017360 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Defines the standard menu bar for use with Traits UI windows and panels, and standard actions and buttons. """ from traits.api import Str # Import and rename the needed Pyface elements: from pyface.action.api import ToolBarManager as ToolBar from pyface.action.api import MenuBarManager as MenuBar from pyface.action.api import MenuManager as Menu from pyface.action.api import Group as ActionGroup from pyface.action.api import Action as PyFaceAction # ------------------------------------------------------------------------- # 'Action' class (extends the core pyface Action class): # ------------------------------------------------------------------------- class Action(PyFaceAction): """An action on a menu bar in a Traits UI window or panel.""" # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- #: Pre-condition for showing the action. If the expression evaluates to #: False, the action is not visible (and disappears if it was previously #: visible). If the value evaluates to True, the action becomes visible. #: All **visible_when** conditions are checked each time that any trait #: value on an object in the UI's context is changed. Therefore, you can #: use **visible_when** conditions to hide or show actions in response to #: user input. Be aware that this only applies to traits in the UI's #: context. As a result, changes to nested traits that don't also change a #: trait on some object in the context may not trigger the expression to be #: checked. Additionally, the expression needs to be a valid python #: expression given the context. i.e. #: eval(visible_when, globals=globals(), locals=context) should succeed. visible_when = Str() #: Pre-condition for enabling the action. If the expression evaluates to #: False, the action is disabled, that is, it cannot be selected. All #: **enabled_when** conditions are checked each time that any trait value #: on an object in the UI's context is changed. Therefore, you can use #: **enabled_when** conditions to enable or disable actions in response to #: user input. Be aware that this only applies to traits in the UI's #: context. As a result, changes to nested traits that don't also change a #: trait on some object in the context may not trigger the expression to be #: checked. Additionally, the expression needs to be a valid python #: expression given the context. i.e. #: eval(visible_when, globals=globals(), locals=context) should succeed. enabled_when = Str() #: Boolean expression indicating when the action is displayed with a check #: mark beside it. This attribute applies only to actions that are included #: in menus. checked_when = Str() #: Pre-condition for including the action in the menu bar or toolbar. If #: the expression evaluates to False, the action is not defined in the #: display. Conditions for **defined_when** are evaluated only once, when #: the display is first constructed. defined_when = Str() #: The method to call to perform the action, on the Handler for the window. #: The method must accept a single parameter, which is a UIInfo object. #: Because Actions are associated with Views rather than Handlers, you must #: ensure that the Handler object for a particular window has a method with #: the correct name, for each Action defined on the View for that window. action = Str() # ------------------------------------------------------------------------- # Standard actions and menu bar definitions: # ------------------------------------------------------------------------- #: Menu separator Separator = ActionGroup #: The standard "close window" action CloseAction = Action(name="Close", action="_on_close") #: The standard "undo last change" action UndoAction = Action( name="Undo", action="_on_undo", defined_when="ui.history is not None", enabled_when="ui.history.can_undo", ) #: The standard "redo last undo" action RedoAction = Action( name="Redo", action="_on_redo", defined_when="ui.history is not None", enabled_when="ui.history.can_redo", ) #: The standard "revert all changes" action RevertAction = Action( name="Revert", action="_on_revert", defined_when="ui.history is not None", enabled_when="ui.history.can_undo", ) #: The standard "show help" action HelpAction = Action(name="Help", action="show_help") #: The standard Traits UI menu bar StandardMenuBar = MenuBar( Menu(CloseAction, name="File"), Menu(UndoAction, RedoAction, RevertAction, name="Edit"), Menu(HelpAction, name="Help"), ) # ------------------------------------------------------------------------- # Standard buttons (i.e. actions): # ------------------------------------------------------------------------- NoButton = Action(name="") #: Appears as two buttons: **Undo** and **Redo**. When **Undo** is clicked, the #: most recent change to the data is cancelled, restoring the previous value. #: **Redo** cancels the most recent "undo" operation. UndoButton = Action(name="Undo") #: When the user clicks the **Revert** button, all changes made in the window #: are cancelled and the original values are restored. If the changes have been #: applied to the model (because the user clicked **Apply** or because the #: window is live), the model data is restored as well. The window remains #: open. RevertButton = Action(name="Revert") #: When the user clicks the **Apply** button, all changes made in the window are #: applied to the model. This option is meaningful only for modal windows. ApplyButton = Action(name="Apply") #: When the user clicks the **OK** button, all changes made in the window are #: applied to the model, and the window is closed. OKButton = Action(name="OK") #: When the user clicks the **Cancel** button, all changes made in the window #: are discarded; if the window is live, the model is restored to the values it #: held before the window was opened. The window is then closed. CancelButton = Action(name="Cancel") #: When the user clicks the **Help** button, the current help handler is #: invoked. If the default help handler is used, a pop-up window is displayed, #: which contains the **help** text for the top-level Group (if any), and for #: the items in the view. If the default help handler has been overridden, #: the action is determined by the custom help handler. See #: **traitsui.help**. HelpButton = Action(name="Help") OKCancelButtons = [OKButton, CancelButton] ModalButtons = [ApplyButton, RevertButton, OKButton, CancelButton, HelpButton] LiveButtons = [UndoButton, RevertButton, OKButton, CancelButton, HelpButton] #: The window has no command buttons NoButtons = [NoButton] ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/message.py0000644000175100001730000000760500000000000020037 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Displays a message to the user as a modal window. """ from traits.api import HasPrivateTraits, Str, Float from .view import View from .group import HGroup from .item import Item, spring from pyface.timer.api import do_after # ------------------------------------------------------------------------- # 'Message' class: # ------------------------------------------------------------------------- class Message(HasPrivateTraits): # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- #: The message to be displayed message = Str() # ------------------------------------------------------------------------- # Displays a user specified message: # ------------------------------------------------------------------------- def message(message="", title="Message", buttons=["OK"], parent=None): """Displays a message to the user as a model window with the specified title and buttons. If *buttons* is not specified, a single **OK** button is used, which is appropriate for notifications, where no further action or decision on the user's part is required. """ msg = Message(message=message) ui = msg.edit_traits( parent=parent, view=View( ["message~", "|<>"], title=title, buttons=buttons, kind="modal" ), ) return ui.result def error(message="", title="Message", buttons=["OK", "Cancel"], parent=None): """Displays a message to the user as a modal window with the specified title and buttons. If *buttons* is not specified, **OK** and **Cancel** buttons are used, which is appropriate for confirmations, where the user must decide whether to proceed. Be sure to word the message so that it is clear that clicking **OK** continues the operation. """ msg = Message(message=message) ui = msg.edit_traits( parent=parent, view=View( ["message~", "|<>"], title=title, buttons=buttons, kind="modal" ), ) return ui.result # ------------------------------------------------------------------------- # 'AutoCloseMessage' class: # ------------------------------------------------------------------------- class AutoCloseMessage(HasPrivateTraits): #: The message to be shown: message = Str("Please wait") #: The time (in seconds) to show the message: time = Float(2.0) def show(self, parent=None, title=""): """Display the wait message for a limited duration.""" view = View( HGroup( spring, Item("message", show_label=False, style="readonly"), spring, ), title=title, ) do_after( int(1000.0 * self.time), self.edit_traits(parent=parent, view=view).dispose, ) # ------------------------------------------------------------------------- # Displays a user specified message that closes automatically after a # specified time interval: # ------------------------------------------------------------------------- def auto_close_message( message="Please wait", time=2.0, title="Please wait", parent=None ): """Displays a message to the user as a modal window with no buttons. The window closes automatically after a specified time interval (specified in seconds). """ msg = AutoCloseMessage(message=message, time=time) msg.show(parent=parent, title=title) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/mimedata.py0000644000175100001730000000117100000000000020164 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # Import the toolkit specific version. from traitsui.toolkit import toolkit_object # WIP: Currently only supports qt backend. API might change without # prior notification PyMimeData = toolkit_object("clipboard:PyMimeData") ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1684768028.087807 traitsui-8.0.0/traitsui/null/0000755000175100001730000000000000000000000017003 5ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/null/__init__.py0000644000175100001730000000177500000000000021126 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Define the concrete implementations of the traits Toolkit interface for the 'null' (do nothing) user interface toolkit. This toolkit is provided to handle situations where no recognized traits-compatible UI toolkit is installed, but users still want to use traits for non-UI related tasks. """ # ------------------------------------------------------------------------- # Define the reference to the exported GUIToolkit object: # ------------------------------------------------------------------------- from . import toolkit toolkit = toolkit.GUIToolkit("traitsui", "null", "traitsui.null") ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/null/color_trait.py0000644000175100001730000000777500000000000021716 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Trait definition for a null-based (i.e., no UI) color. """ from traits.api import Trait, TraitError # ------------------------------------------------------------------------- # Convert a number into a wxColour object: # ------------------------------------------------------------------------- def convert_to_color(object, name, value): """Converts a number into a wxColour object.""" if isinstance(value, int): return value & 0xFFFFFF elif isinstance(value, tuple): return (value[0] / 255.0, value[1] / 255.0, value[2] / 255.0) raise TraitError convert_to_color.info = ( "an integer which in hex is of the form 0xRRGGBB, " "where RR is red, GG is green, and BB is blue" ) # ------------------------------------------------------------------------- # Standard colors: # ------------------------------------------------------------------------- standard_colors = { "aquamarine": 0x70DB93, "black": 0x000000, "blue": 0x0000FF, "blue violet": 0x9F5F9F, "brown": 0xA52A2A, "cadet blue": 0x5F9F9F, "coral": 0xFF7F00, "cornflower blue": 0x42426F, "cyan": 0x00FFFF, "dark grey": 0x2F2F2F, "dark green": 0x2F4F2F, "dark olive green": 0x4F4F2F, "dark orchid": 0x9932CC, "dark slate blue": 0x6B238E, "dark slate grey": 0x2F4F4F, "dark turquoise": 0x7093DB, "dim grey": 0x545454, "firebrick": 0x8E2323, "forest green": 0x238E23, "gold": 0xCC7F32, "goldenrod": 0xDBDB70, "grey": 0x808080, "green": 0x00FF00, "green yellow": 0x93DB70, "indian red": 0x4F2F2F, "khaki": 0x9F9F5F, "light blue": 0xBFD8D8, "light grey": 0xC0C0C0, "light steel": 0x000000, "blue": 0x0000FF, "lime green": 0x32CC32, "magenta": 0xFF00FF, "maroon": 0x8E236B, "medium aquamarine": 0x32CC99, "medium blue": 0x3232CC, "medium forest green": 0x6B8E23, "medium goldenrod": 0xEAEAAD, "medium orchid": 0x9370DB, "medium sea green": 0x426F42, "medium slate blue": 0x7F00FF, "medium spring green": 0x7FFF00, "medium turquoise": 0x70DBDB, "medium violet red": 0xDB7093, "midnight blue": 0x2F2F4F, "navy": 0x23238E, "orange": 0xCC3232, "orange red": 0xFF007F, "orchid": 0xDB70DB, "pale green": 0x8FBC8F, "pink": 0xBC8FEA, "plum": 0xEAADEA, "purple": 0xB000FF, "red": 0xFF0000, "salmon": 0x6F4242, "sea green": 0x238E6B, "sienna": 0x8E6B23, "sky blue": 0x3299CC, "slate blue": 0x007FFF, "spring green": 0x00FF7F, "steel blue": 0x236B8E, "tan": 0xDB9370, "thistle": 0xD8BFD8, "turquoise": 0xADEAEA, "violet": 0x4F2F4F, "violet red": 0xCC3299, "wheat": 0xD8D8BF, "white": 0xFFFFFF, "yellow": 0xFFFF00, "yellow green": 0x99CC32, } # ------------------------------------------------------------------------- # Define 'null' specific color traits: # ------------------------------------------------------------------------- ### Note: Declare the editor to be a function which returns the ColorEditor # class from traits ui to avoid circular import issues. For backwards # compatibility with previous Traits versions, the 'editors' folder in Traits # project declares 'from api import *' in its __init__.py. The 'api' in turn # can contain classes that have a Color trait which lead to this file getting # imported. This leads to a circular import when declaring a Color trait. def get_color_editor(*args, **traits): from ..api import ColorEditor return ColorEditor(*args, **traits) # Color traits NullColor = Trait( "white", convert_to_color, standard_colors, editor=get_color_editor ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/null/font_trait.py0000644000175100001730000000746200000000000021537 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Trait definition for a null-based (i.e., no UI) font. """ from traits.api import Trait, TraitHandler, TraitError # ------------------------------------------------------------------------------ # Convert a string into a valid 'wxFont' object (if possible): # ------------------------------------------------------------------------------ # Mapping of strings to valid wxFont families font_families = ["default", "decorative", "roman", "script", "swiss", "modern"] # Mapping of strings to wxFont styles font_styles = ["slant", "italic"] # Mapping of strings wxFont weights font_weights = ["light", "bold"] # Strings to ignore in text representations of fonts font_noise = ["pt", "point", "family"] # ------------------------------------------------------------------------------ # 'TraitFont' class' # ------------------------------------------------------------------------------ class TraitFont(TraitHandler): """Ensures that values assigned to a trait attribute are valid font descriptor strings; the value actually assigned is the corresponding canonical font descriptor string. """ def validate(self, object, name, value): """Validates that the value is a valid font descriptor string.""" try: point_size = family = style = weight = underline = "" facename = [""] for word in value.split(): lword = word.lower() if lword in font_families: family = " " + lword elif lword in font_styles: style = " " + lword elif lword in font_weights: weight = " " + lword elif lword == "underline": underline = " " + lword elif lword not in font_noise: try: int(lword) point_size = lword + " pt" except: facename.append(word) fontstr = ( "%s%s%s%s%s%s" % ( point_size, family, style, weight, underline, " ".join(facename), ) ).strip() return fontstr except Exception: pass raise TraitError(object, name, "a font descriptor string", repr(value)) def info(self): return ( "a string describing a font (e.g. '12 pt bold italic " "swiss family Arial' or 'default 12')" ) # ------------------------------------------------------------------------------ # Define a 'null' specific font trait: # ------------------------------------------------------------------------------ ### Note: Declare the editor to be a function which returns the FontEditor # class from traits ui to avoid circular import issues. For backwards # compatibility with previous Traits versions, the 'editors' folder in Traits # project declares 'from api import *' in its __init__.py. The 'api' in turn # can contain classes that have a Font trait which lead to this file getting # imported. This leads to a circular import when declaring a Font trait. def get_font_editor(*args, **traits): from ..api import FontEditor return FontEditor(*args, **traits) fh = TraitFont() NullFont = Trait( fh.validate(None, None, "Arial 10"), fh, editor=get_font_editor ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/null/rgb_color_trait.py0000644000175100001730000001426000000000000022533 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Trait definitions for an RGB-based color, which is a tuple of the form (*red*, *green*, *blue*), where *red*, *green* and *blue* are floats in the range from 0.0 to 1.0. """ from traits.api import Trait, TraitError from traits.trait_base import SequenceTypes # ------------------------------------------------------------------------- # Convert a number into an RGB tuple: # ------------------------------------------------------------------------- def range_check(value): """Checks that *value* can be converted to a value in the range 0.0 to 1.0. If so, it returns the floating point value; otherwise, it raises a TraitError. """ value = float(value) if 0.0 <= value <= 1.0: return value raise TraitError def convert_to_color(object, name, value): """Converts a tuple or an integer to an RGB color value, or raises a TraitError if that is not possible. """ if (type(value) in SequenceTypes) and (len(value) == 3): return ( range_check(value[0]), range_check(value[1]), range_check(value[2]), ) if isinstance(value, int): num = int(value) return ( (num / 0x10000) / 255.0, ((num // 0x100) & 0xFF) / 255.0, (num & 0xFF) / 255.0, ) raise TraitError convert_to_color.info = ( "a tuple of the form (r,g,b), where r, g, and b " "are floats in the range from 0.0 to 1.0, or an integer which in hex is of " "the form 0xRRGGBB, where RR is red, GG is green, and BB is blue" ) # ------------------------------------------------------------------------- # Standard colors: # ------------------------------------------------------------------------- # RGB versions of standard colors rgb_standard_colors = { "aquamarine": (0.439216, 0.858824, 0.576471), "black": (0.0, 0.0, 0.0), "blue": (0.0, 0.0, 1.0), "blue violet": (0.623529, 0.372549, 0.623529), "brown": (0.647059, 0.164706, 0.164706), "cadet blue": (0.372549, 0.623529, 0.623529), "coral": (1.0, 0.498039, 0.0), "cornflower blue": (0.258824, 0.258824, 0.435294), "cyan": (0.0, 1.0, 1.0), "dark grey": (0.184314, 0.184314, 0.184314), "dark green": (0.184314, 0.309804, 0.184314), "dark olive green": (0.309804, 0.309804, 0.184314), "dark orchid": (0.6, 0.196078, 0.8), "dark slate blue": (0.419608, 0.137255, 0.556863), "dark slate grey": (0.184314, 0.309804, 0.309804), "dark turquoise": (0.439216, 0.576471, 0.858824), "dim grey": (0.329412, 0.329412, 0.329412), "firebrick": (0.556863, 0.137255, 0.137255), "forest green": (0.137255, 0.556863, 0.137255), "gold": (0.8, 0.498039, 0.196078), "goldenrod": (0.858824, 0.858824, 0.439216), "grey": (0.501961, 0.501961, 0.501961), "green": (0.0, 1.0, 0.0), "green yellow": (0.576471, 0.858824, 0.439216), "indian red": (0.309804, 0.184314, 0.184314), "khaki": (0.623529, 0.623529, 0.372549), "light blue": (0.74902, 0.847059, 0.847059), "light grey": (0.752941, 0.752941, 0.752941), "light steel": (0.0, 0.0, 0.0), "blue": (0.0, 0.0, 1.0), "lime green": (0.196078, 0.8, 0.196078), "magenta": (1.0, 0.0, 1.0), "maroon": (0.556863, 0.137255, 0.419608), "medium aquamarine": (0.196078, 0.8, 0.6), "medium blue": (0.196078, 0.196078, 0.8), "medium forest green": (0.419608, 0.556863, 0.137255), "medium goldenrod": (0.917647, 0.917647, 0.678431), "medium orchid": (0.576471, 0.439216, 0.858824), "medium sea green": (0.258824, 0.435294, 0.258824), "medium slate blue": (0.498039, 0.0, 1.0), "medium spring green": (0.498039, 1.0, 0.0), "medium turquoise": (0.439216, 0.858824, 0.858824), "medium violet red": (0.858824, 0.439216, 0.576471), "midnight blue": (0.184314, 0.184314, 0.309804), "navy": (0.137255, 0.137255, 0.556863), "orange": (0.8, 0.196078, 0.196078), "orange red": (1.0, 0.0, 0.498039), "orchid": (0.858824, 0.439216, 0.858824), "pale green": (0.560784, 0.737255, 0.560784), "pink": (0.737255, 0.560784, 0.917647), "plum": (0.917647, 0.678431, 0.917647), "purple": (0.690196, 0.0, 1.0), "red": (1.0, 0.0, 0.0), "salmon": (0.435294, 0.258824, 0.258824), "sea green": (0.137255, 0.556863, 0.419608), "sienna": (0.556863, 0.419608, 0.137255), "sky blue": (0.196078, 0.6, 0.8), "slate blue": (0.0, 0.498039, 1.0), "spring green": (0.0, 1.0, 0.498039), "steel blue": (0.137255, 0.419608, 0.556863), "tan": (0.858824, 0.576471, 0.439216), "thistle": (0.847059, 0.74902, 0.847059), "turquoise": (0.678431, 0.917647, 0.917647), "violet": (0.309804, 0.184314, 0.309804), "violet red": (0.8, 0.196078, 0.6), "wheat": (0.847059, 0.847059, 0.74902), "white": (1.0, 1.0, 1.0), "yellow": (1.0, 1.0, 0.0), "yellow green": (0.6, 0.8, 0.196078), } # ------------------------------------------------------------------------- # Define 'null' specific color trait: # ------------------------------------------------------------------------- ### Note: Declare the editor to be a function which returns the RGBColorEditor # class from traits ui to avoid circular import issues. For backwards # compatibility with previous Traits versions, the 'editors' folder in Traits # project declares 'from api import *' in its __init__.py. The 'api' in turn # can contain classes that have a RGBColor trait which lead to this file getting # imported. This will lead to a circular import when declaring a RGBColor # trait. def get_rgb_color_editor(*args, **traits): from ..editors.rgb_color_editor import ( ToolkitEditorFactory as RGBColorEditor, ) return RGBColorEditor(*args, **traits) # Trait whose value must be an RGB color RGBColor = Trait( "white", convert_to_color, rgb_standard_colors, editor=get_rgb_color_editor ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/null/toolkit.py0000644000175100001730000000275700000000000021055 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Defines the concrete implementations of the traits Toolkit interface for the 'null' (do nothing) user interface toolkit. """ from ..toolkit import Toolkit class GUIToolkit(Toolkit): # ------------------------------------------------------------------------- # GUI toolkit dependent trait definitions: # ------------------------------------------------------------------------- def color_trait(self, *args, **traits): from . import color_trait as ct return ct.NullColor(*args, **traits) def rgb_color_trait(self, *args, **traits): from . import rgb_color_trait as rgbct return rgbct.RGBColor(*args, **traits) def font_trait(self, *args, **traits): from . import font_trait as ft return ft.NullFont(*args, **traits) def kiva_font_trait(self, *args, **traits): from . import font_trait as ft return ft.NullFont(*args, **traits) def constants(self, *args, **traits): constants = { "WindowColor": (236 / 255.0, 233 / 255.0, 216 / 255.0, 1.0) } return constants ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1684768028.0958068 traitsui-8.0.0/traitsui/qt/0000755000175100001730000000000000000000000016455 5ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/qt/__init__.py0000644000175100001730000000267000000000000020573 0ustar00runnerdocker00000000000000# (C) Copyright 2008-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # ------------------------------------------------------------------------------ # Copyright (c) 2007, Riverbank Computing Limited # All rights reserved. # # This software is provided without warranty under the terms of the BSD license. # However, when used with the GPL version of PyQt the additional terms # described in the PyQt GPL exception also apply # # Author: Riverbank Computing Limited # ------------------------------------------------------------------------------ """ Defines the concrete implementations of the traits Toolkit interface for the PyQt user interface toolkit. """ # import pyface.qt before anything else is done so the sipapi # can be set correctly if needed import pyface.qt # ---------------------------------------------------------------------------- # Define the reference to the exported GUIToolkit object: # ---------------------------------------------------------------------------- from . import toolkit # Reference to the GUIToolkit object for Qt. toolkit = toolkit.GUIToolkit("traitsui", "qt", "traitsui.qt") ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/qt/array_editor.py0000644000175100001730000000205500000000000021515 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Defines array editors for the PyQt user interface toolkit. """ from traitsui.editors.array_editor import SimpleEditor as BaseSimpleEditor from .editor import Editor # ------------------------------------------------------------------------- # 'SimpleEditor' class: # ------------------------------------------------------------------------- class SimpleEditor(BaseSimpleEditor, Editor): """Simple style of editor for arrays.""" # FIXME: This class has been re-defined here simply so it inherits from the # PyQt Editor class. pass class ReadonlyEditor(SimpleEditor): #: Set the value of the readonly trait. readonly = True ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/qt/array_view_editor.py0000644000175100001730000000142400000000000022546 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! from traitsui.ui_editors.array_view_editor import ( _ArrayViewEditor as BaseArrayViewEditor, ) from .ui_editor import UIEditor # ------------------------------------------------------------------------- # '_ArrayViewEditor' class: # ------------------------------------------------------------------------- class _ArrayViewEditor(BaseArrayViewEditor, UIEditor): pass ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/qt/boolean_editor.py0000644000175100001730000000645000000000000022021 0ustar00runnerdocker00000000000000# (C) Copyright 2008-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # ------------------------------------------------------------------------------ # Copyright (c) 2007, Riverbank Computing Limited # All rights reserved. # # This software is provided without warranty under the terms of the BSD license. # However, when used with the GPL version of PyQt the additional terms # described in the PyQt GPL exception also apply # # Author: Riverbank Computing Limited # ------------------------------------------------------------------------------ """ Defines the various Boolean editors for the PyQt user interface toolkit. """ from pyface.qt import QtCore, QtGui 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 from .constants import ReadonlyColor 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 = QtGui.QCheckBox() self.control.stateChanged.connect(self.update_object) self.set_tooltip() def dispose(self): if self.control is not None: self.control.stateChanged.disconnect(self.update_object) super().dispose() 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. """ if self.value: self.control.setCheckState(QtCore.Qt.CheckState.Checked) else: self.control.setCheckState(QtCore.Qt.CheckState.Unchecked) 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 = QtGui.QLineEdit() self.control.setReadOnly(True) pal = QtGui.QPalette(self.control.palette()) pal.setColor(QtGui.QPalette.ColorRole.Base, ReadonlyColor) self.control.setPalette(pal) # ------------------------------------------------------------------------- # Updates the editor when the object trait changes external to the editor: # # (Should normally be overridden in a subclass) # ------------------------------------------------------------------------- def update_editor(self): """Updates the editor when the object trait changes externally to the editor. """ if self.value: self.control.setText("True") else: self.control.setText("False") ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/qt/button_editor.py0000644000175100001730000001424000000000000021711 0ustar00runnerdocker00000000000000# (C) Copyright 2008-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # ------------------------------------------------------------------------------ # Copyright (c) 2007, Riverbank Computing Limited # All rights reserved. # # This software is provided without warranty under the terms of the BSD license. # However, when used with the GPL version of PyQt the additional terms # described in the PyQt GPL exception also apply # # Author: Riverbank Computing Limited # ------------------------------------------------------------------------------ """ Defines the various button editors for the PyQt user interface toolkit. """ from pyface.qt import QtCore, QtGui from pyface.api import Image from traits.api import List, Str, observe, on_trait_change from .editor import Editor class SimpleEditor(Editor): """Simple style editor for a button.""" # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- #: The button label label = Str() #: 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() 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.ToolButtonStyle.ToolButtonTextOnly self.control.setText(self.string_value(label)) self.object.observe( 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") # The connection type is set to workaround Qt5 + MacOSX issue with # event dispatching. Without the type set to QueuedConnection, other # widgets may not repaint properly in response to a button click. # See enthought/traitsui#1308 self.control.clicked.connect( self.update_object, type=QtCore.Qt.ConnectionType.QueuedConnection, ) self.set_tooltip() def dispose(self): """Disposes of the contents of an editor.""" if self.factory.values_trait: self.object.observe( self._update_menu, self.factory.values_trait + ".items", remove=True, ) if self.control is not None: self.control.clicked.disconnect(self.update_object) super().dispose() def _label_changed(self, label): self.control.setText(self.string_value(label)) def _update_menu(self, event=None): 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 class CustomEditor(SimpleEditor): """Custom style editor for a button, which can contain an image.""" #: The button image image = Image() #: The mapping of button styles to Qt classes. _STYLE_MAP = { "checkbox": QtGui.QCheckBox, "radio": QtGui.QRadioButton, "toolbar": QtGui.QToolButton, } def init(self, parent): """Finishes initializing the editor by creating the underlying toolkit widget. """ # FIXME: We ignore orientation, width_padding and height_padding. factory = self.factory if factory.label: label = factory.label else: label = self.item.get_label(self.ui) btype = self._STYLE_MAP.get(factory.style, QtGui.QPushButton) self.control = btype() self.control.setText(self.string_value(label)) if factory.image is not None: self.control.setIcon(factory.image.create_icon()) self.sync_value(self.factory.label_value, "label", "from") self.sync_value(self.factory.image_value, "image", "from") self.control.clicked.connect(self.update_object) self.set_tooltip() @observe("image") def _image_updated(self, event): image = event.new self.control.setIcon(image.create_icon()) def dispose(self): """Disposes of the contents of an editor.""" if self.control is not None: self.control.clicked.disconnect(self.update_object) # FIXME: Maybe better to let this class subclass Editor directly # enthought/traitsui#884 Editor.dispose(self) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/qt/check_list_editor.py0000644000175100001730000002271400000000000022513 0ustar00runnerdocker00000000000000# (C) Copyright 2008-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # ------------------------------------------------------------------------------ # Copyright (c) 2007, Riverbank Computing Limited # All rights reserved. # # This software is provided without warranty under the terms of the BSD license. # However, when used with the GPL version of PyQt the additional terms # described in the PyQt GPL exception also apply # # Author: Riverbank Computing Limited # ------------------------------------------------------------------------------ """ Defines the various editors for multi-selection enumerations, for the PyQt user interface toolkit. """ import logging from pyface.qt import QtCore, QtGui, is_pyside from traits.api import Any, Callable, List, Str, TraitError, Tuple from .editor_factory import TextEditor as BaseTextEditor from .editor import EditorWithList logger = logging.getLogger(__name__) # ------------------------------------------------------------------------- # 'SimpleEditor' class: # ------------------------------------------------------------------------- class SimpleEditor(EditorWithList): """Simple style of editor for checklists, which displays a combo box.""" # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- #: Checklist item names names = List(Str) #: Checklist item values values = List() def init(self, parent): """Finishes initializing the editor by creating the underlying toolkit widget. """ self.create_control(parent) super().init(parent) def create_control(self, parent): """Creates the initial editor control.""" self.control = QtGui.QComboBox() self.control.activated[int].connect(self.update_object) def dispose(self): """Disposes of the contents of an editor.""" if self.control is not None: self.control.activated[int].disconnect(self.update_object) super().dispose() def list_updated(self, values): """Handles updates to the list of legal checklist values.""" sv = self.string_value if (len(values) > 0) and isinstance(values[0], str): values = [(x, sv(x, str.capitalize)) for x in values] self.values = valid_values = [x[0] for x in values] self.names = [x[1] for x in values] # Make sure the current value is still legal: modified = False cur_value = parse_value(self.value) for i in range(len(cur_value) - 1, -1, -1): if cur_value[i] not in valid_values: try: del cur_value[i] modified = True except TypeError as e: logger.warning( "Unable to remove non-current value [%s] from " "values %s", cur_value[i], values, ) if modified: if isinstance(self.value, str): cur_value = ",".join(cur_value) self.value = cur_value self.rebuild_editor() def rebuild_editor(self): """Rebuilds the editor after its definition is modified.""" control = self.control control.clear() for name in self.names: control.addItem(name) self.update_editor() def update_object(self, index): """Handles the user selecting a new value from the combo box.""" value = self.values[index] if not isinstance(self.value, str): value = [value] self.value = value def update_editor(self): """Updates the editor when the object trait changes externally to the editor. """ try: self.control.setCurrentIndex( self.values.index(parse_value(self.value)[0]) ) except: pass class CustomEditor(SimpleEditor): """Custom style of editor for checklists, which displays a set of check boxes. """ #: List of tuple(signal, slot) to be disconnected while rebuilding the #: editor. _connections_to_rebuild = List(Tuple(Any, Callable)) def init(self, parent): """Finishes initializing the editor by creating the underlying toolkit widget. """ self.create_control(parent) EditorWithList.init(self, parent) def create_control(self, parent): """Creates the initial editor control.""" self.control = QtGui.QWidget() layout = QtGui.QGridLayout(self.control) layout.setContentsMargins(0, 0, 0, 0) self._mapper = QtCore.QSignalMapper() if is_pyside and QtCore.__version_info__ >= (5, 15): self._mapper.mappedString.connect(self.update_object) else: self._mapper.mapped[str].connect(self.update_object) def dispose(self): """Disposes of the contents of an editor.""" while self._connections_to_rebuild: signal, handler = self._connections_to_rebuild.pop() signal.disconnect(handler) # signal from create_control if self._mapper is not None: if is_pyside and QtCore.__version_info__ >= (5, 15): self._mapper.mappedString.disconnect(self.update_object) else: self._mapper.mapped[str].disconnect(self.update_object) self._mapper = None # enthought/traitsui#884 EditorWithList.dispose(self) def rebuild_editor(self): """Rebuilds the editor after its definition is modified.""" while self._connections_to_rebuild: signal, handler = self._connections_to_rebuild.pop() signal.disconnect(handler) # Clear any existing content: self.clear_layout() cur_value = parse_value(self.value) # Create a sizer to manage the radio buttons: labels = self.names values = self.values n = len(labels) cols = self.factory.cols rows = (n + cols - 1) // cols # incr will keep track of how to increment index so that as we traverse # the grid in row major order, the elements are added to appear in # column major order incr = [n // cols] * cols rem = n % cols for i in range(cols): incr[i] += rem > i incr[-1] = -sum(incr[:-1]) + 1 # e.g for a gird: # 0 2 4 # 1 3 5 # incr should be [2, 2, -3] # Add the set of all possible choices: layout = self.control.layout() index = 0 # populate the layout in row_major order for i in range(rows): for j in range(cols): if n > 0: cb = QtGui.QCheckBox(labels[index]) cb.value = values[index] if cb.value in cur_value: cb.setCheckState(QtCore.Qt.CheckState.Checked) else: cb.setCheckState(QtCore.Qt.CheckState.Unchecked) cb.clicked.connect(self._mapper.map) self._connections_to_rebuild.append( (cb.clicked, self._mapper.map) ) self._mapper.setMapping(cb, labels[index]) layout.addWidget(cb, i, j) index += incr[j] n -= 1 def update_object(self, label): """Handles the user clicking one of the custom check boxes.""" cb = self._mapper.mapping(label) cur_value = parse_value(self.value) if cb.checkState() == QtCore.Qt.CheckState.Checked: cur_value.append(cb.value) elif cb.value in cur_value: cur_value.remove(cb.value) if isinstance(self.value, str): cur_value = ",".join(cur_value) self.value = cur_value def update_editor(self): """Updates the editor when the object trait changes externally to the editor. """ new_values = parse_value(self.value) for cb in self.control.findChildren(QtGui.QCheckBox, None): if cb.value in new_values: cb.setCheckState(QtCore.Qt.CheckState.Checked) else: cb.setCheckState(QtCore.Qt.CheckState.Unchecked) class TextEditor(BaseTextEditor): """Text style of editor for checklists, which displays a text field.""" def update_object(self, event=None): """Handles the user changing the contents of the edit control.""" try: value = str(self.control.text()) value = eval(value) except: pass try: self.value = value except TraitError as excp: pass # ------------------------------------------------------------------------- # Parse a value into a list: # ------------------------------------------------------------------------- def parse_value(value): """Parses a value into a list.""" if value is None: return [] if not isinstance(value, str): return value[:] return [x.strip() for x in value.split(",")] ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/qt/clipboard.py0000644000175100001730000000637600000000000021002 0ustar00runnerdocker00000000000000# (C) Copyright 2008-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # ------------------------------------------------------------------------------ # Copyright (c) 2007, Riverbank Computing Limited # All rights reserved. # # This software is provided without warranty under the terms of the BSD license. # However, when used with the GPL version of PyQt the additional terms # described in the PyQt GPL exception also apply # # Author: Riverbank Computing Limited # ------------------------------------------------------------------------------ """ Implements a wrapper around the PyQt clipboard that handles Python objects using pickle. """ from pyface.qt import QtGui from pyface.ui.qt.mimedata import PyMimeData, str2bytes from traits.api import HasTraits, Instance, Property # ------------------------------------------------------------------------- # '_Clipboard' class: # ------------------------------------------------------------------------- class _Clipboard(HasTraits): """The _Clipboard class provides a wrapper around the PyQt clipboard.""" # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- #: The instance on the clipboard (if any). instance = Property() #: Set if the clipboard contains an instance. has_instance = Property() #: The type of the instance on the clipboard (if any). instance_type = Property() #: The application clipboard. clipboard = Instance(QtGui.QClipboard) # ------------------------------------------------------------------------- # Instance property methods: # ------------------------------------------------------------------------- def _get_instance(self): """The instance getter.""" md = PyMimeData.coerce(self.clipboard.mimeData()) if md is None: return None return md.instance() def _set_instance(self, data): """The instance setter.""" self.clipboard.setMimeData(PyMimeData(data)) def _get_has_instance(self): """The has_instance getter.""" return self.clipboard.mimeData().hasFormat(PyMimeData.MIME_TYPE) def _get_instance_type(self): """The instance_type getter.""" md = PyMimeData.coerce(self.clipboard.mimeData()) if md is None: return None return md.instanceType() # ------------------------------------------------------------------------- # Other trait handlers: # ------------------------------------------------------------------------- def _clipboard_default(self): """Initialise the clipboard.""" return QtGui.QApplication.clipboard() # ------------------------------------------------------------------------- # The singleton clipboard instance. # ------------------------------------------------------------------------- clipboard = _Clipboard() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/qt/code_editor.py0000644000175100001730000002471600000000000021321 0ustar00runnerdocker00000000000000# (C) Copyright 2008-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # ------------------------------------------------------------------------------ # Copyright (c) 2007, Riverbank Computing Limited # All rights reserved. # # This software is provided without warranty under the terms of the BSD license. # However, when used with the GPL version of PyQt the additional terms # described in the PyQt GPL exception also apply # # Author: Riverbank Computing Limited # ------------------------------------------------------------------------------ """ Defines a source code editor and code editor factory, for the PyQt user interface toolkit, useful for tools such as debuggers. """ from pyface.qt import QtGui from pyface.key_pressed_event import KeyPressedEvent from pyface.ui.qt.code_editor.code_widget import AdvancedCodeWidget from traits.api import ( Str, List, Int, Event, Bool, TraitError, observe, ) from traits.trait_base import SequenceTypes from .constants import OKColor, ErrorColor from .editor import Editor from .helper import pixmap_cache # Marker line constants: MARK_MARKER = 0 # Marks a marked line SEARCH_MARKER = 1 # Marks a line matching the current search SELECTED_MARKER = 2 # Marks the currently selected line class SourceEditor(Editor): """Editor for source code which uses the advanced code widget.""" # ------------------------------------------------------------------------- # Pyface PythonEditor interface: # ------------------------------------------------------------------------- #: Event that is fired on keypresses: key_pressed = Event(KeyPressedEvent) # ------------------------------------------------------------------------- # Editor interface: # ------------------------------------------------------------------------- #: The code editor is scrollable. This value overrides the default. scrollable = True # ------------------------------------------------------------------------- # SoureEditor interface: # ------------------------------------------------------------------------- #: Is the editor read only? readonly = Bool(False) #: The currently selected line selected_line = Int() #: The start position of the selected selected_start_pos = Int() #: The end position of the selected selected_end_pos = Int() #: The currently selected text selected_text = Str() #: The list of line numbers to mark mark_lines = List(Int) #: The current line number line = Event() #: The current column column = Event() #: The lines to be dimmed dim_lines = List(Int) dim_color = Str() dim_style_number = Int(16) # 0-15 are reserved for the python lexer #: The lines to have squiggles drawn under them squiggle_lines = List(Int) squiggle_color = Str() #: The lexer to use. lexer = 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. """ self.control = QtGui.QWidget() layout = QtGui.QVBoxLayout(self.control) layout.setContentsMargins(0, 0, 0, 0) self._widget = control = AdvancedCodeWidget( None, lexer=self.factory.lexer ) layout.addWidget(control) factory = self.factory # Set up listeners for the signals we care about code_editor = self._widget.code if self.readonly: code_editor.setReadOnly(True) else: if factory.auto_set: code_editor.textChanged.connect(self.update_object) else: code_editor.focus_lost.connect(self.update_object) if factory.selected_text != "": code_editor.selectionChanged.connect(self._selection_changed) if (factory.line != "") or (factory.column != ""): code_editor.cursorPositionChanged.connect(self._position_changed) code_editor.line_number_widget.setVisible(factory.show_line_numbers) # Make sure the editor has been initialized: self.update_editor() # Set up any event listeners: self.sync_value(factory.mark_lines, "mark_lines", "from", is_list=True) self.sync_value(factory.selected_line, "selected_line", "from") self.sync_value(factory.selected_text, "selected_text", "to") self.sync_value(factory.line, "line") self.sync_value(factory.column, "column") self.sync_value(factory.selected_start_pos, "selected_start_pos", "to") self.sync_value(factory.selected_end_pos, "selected_end_pos", "to") self.sync_value(factory.dim_lines, "dim_lines", "from", is_list=True) if self.factory.dim_color == "": self.dim_color = "grey" else: self.sync_value(factory.dim_color, "dim_color", "from") self.sync_value( factory.squiggle_lines, "squiggle_lines", "from", is_list=True ) if factory.squiggle_color == "": self.squiggle_color = "red" else: self.sync_value(factory.squiggle_color, "squiggle_color", "from") # Set the control tooltip: self.set_tooltip() def dispose(self): """Disposes of the contents of an editor.""" # Make sure that the editor does not try to update as the control is # being destroyed: if not self.factory.auto_set: self._widget.code.focus_lost.disconnect(self.update_object) super().dispose() def update_object(self): """Handles the user entering input data in the edit control.""" if not self._locked: try: value = str(self._widget.code.toPlainText()) if isinstance(self.value, SequenceTypes): value = value.split() self.value = value except TraitError as excp: pass def update_editor(self): """Updates the editor when the object trait changes externally to the editor. """ self._locked = True new_value = self.value if isinstance(new_value, SequenceTypes): new_value = "\n".join([line.rstrip() for line in new_value]) control = self._widget if control.code.toPlainText() != new_value: control.code.setPlainText(new_value) if self.factory.selected_line: # TODO: update the factory selected line pass # TODO: put the cursor somewhere self._locked = False def error(self, excp): """Handles an error that occurs while setting the object's trait value.""" pass # -- UI preference save/restore interface --------------------------------- def restore_prefs(self, prefs): """Restores any saved user preference information associated with the editor. """ if self.factory.key_bindings is not None: key_bindings = prefs.get("key_bindings") if key_bindings is not None: self.factory.key_bindings.merge(key_bindings) def save_prefs(self): """Returns any user preference information associated with the editor.""" return {"key_bindings": self.factory.key_bindings} def _mark_lines_changed(self): """Handles the set of marked lines being changed.""" # FIXME: Not implemented at this time. return def _mark_lines_items_changed(self): self._mark_lines_changed() def _selection_changed(self): self.selected_text = str(self._widget.code.textCursor().selectedText()) start = self._widget.code.textCursor().selectionStart() end = self._widget.code.textCursor().selectionEnd() if start > end: start, end = end, start self.selected_start_pos = start self.selected_end_pos = end def _selected_line_changed(self): """Handles a change in which line is currently selected.""" control = self._widget line = max(1, min(control.lines(), self.selected_line)) _, column = control.get_line_column() control.set_line_column(line, column) if self.factory.auto_scroll: control.centerCursor() def _line_changed(self, line): if not self._locked: _, column = self._widget.get_line_column() self._widget.set_line_column(line, column) if self.factory.auto_scroll: self._widget.centerCursor() def _column_changed(self, column): if not self._locked: line, _ = self._widget.get_line_column() self._widget.set_line_column(line, column) def _position_changed(self): """Handles the cursor position being changed.""" control = self._widget self._locked = True self.line, self.column = control.get_line_column() self._locked = False self.selected_text = control.get_selected_text() if self.factory.auto_scroll: self._widget.centerCursor() def _key_pressed_changed(self, event): """Handles a key being pressed within the editor.""" key_bindings = self.factory.key_bindings if key_bindings: processed = key_bindings.do( event.event, self.ui.handler, self.ui.info ) else: processed = False if not processed and event.event.matches(QtGui.QKeySequence.StandardKey.Find): self._find_widget.show() def _dim_color_changed(self): pass def _squiggle_color_changed(self): pass @observe("dim_lines, squiggle_lines") def _style_document(self, event): self._widget.set_warn_lines(self.squiggle_lines) # Define the simple, custom, text and readonly editors, which will be accessed # by the editor factory for code editors. CustomEditor = SimpleEditor = TextEditor = SourceEditor class ReadonlyEditor(SourceEditor): # Set the value of the readonly trait. readonly = True ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/qt/color_editor.py0000644000175100001730000002342600000000000021522 0ustar00runnerdocker00000000000000# (C) Copyright 2008-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # ------------------------------------------------------------------------------ # Copyright (c) 2007, Riverbank Computing Limited # All rights reserved. # # This software is provided without warranty under the terms of the BSD license. # However, when used with the GPL version of PyQt the additional terms # described in the PyQt GPL exception also apply # # Author: Riverbank Computing Limited # ------------------------------------------------------------------------------ """ Defines the various color editors for the PyQt user interface toolkit. """ from pyface.qt import QtCore, QtGui, is_pyside from traitsui.editors.color_editor import ( ToolkitEditorFactory as BaseToolkitEditorFactory, ) from .editor_factory import ( SimpleEditor as BaseSimpleEditor, TextEditor as BaseTextEditor, ReadonlyEditor as BaseReadonlyEditor, ) from .editor import Editor # Standard color samples: color_samples = [] # --------------------------------------------------------------------------- # The PyQt ToolkitEditorFactory class. # --------------------------------------------------------------------------- ## We need to add qt-specific methods to the editor factory (since all editors ## will be accessing these functions. Making these functions global functions ## in this file does not work quite well, since we want custom editors to ## override these methods easily. class ToolkitEditorFactory(BaseToolkitEditorFactory): """PyQt editor factory for color editors.""" def to_qt_color(self, editor): """Gets the PyQt color equivalent of the object trait.""" if self.mapped: return getattr(editor.object, editor.name + "_") return getattr(editor.object, editor.name) def from_qt_color(self, color): """Gets the application equivalent of a PyQt value.""" return color def str_color(self, color): """Returns the text representation of a specified color value.""" if isinstance(color, QtGui.QColor): alpha = color.alpha() if alpha == 255: return "(%d,%d,%d)" % ( color.red(), color.green(), color.blue(), ) return "(%d,%d,%d,%d)" % ( color.red(), color.green(), color.blue(), alpha, ) return color class SimpleColorEditor(BaseSimpleEditor): """Simple style of color editor, which displays a text field whose background color is the color value. Selecting the text field displays a dialog box for selecting a new color value. """ def popup_editor(self): """Invokes the pop-up editor for an object trait.""" color = self.factory.to_qt_color(self) options = QtGui.QColorDialog.ColorDialogOption.ShowAlphaChannel if not self.factory.use_native_dialog: options |= QtGui.QColorDialog.ColorDialogOption.DontUseNativeDialog color = QtGui.QColorDialog.getColor( color, self.control, "Select Color", options ) if color.isValid(): self.value = self.factory.from_qt_color(color) self.update_editor() def update_editor(self): """Updates the editor when the object trait changes externally to the editor. """ super().update_editor() set_color(self) def string_value(self, color): """Returns the text representation of a specified color value.""" return self.factory.str_color(color) class CustomColorEditor(Editor): """Custom style of color editor, which displays a color editor panel.""" def init(self, parent): """Finishes initializing the editor by creating the underlying toolkit widget. """ self.control, self._simple_field = color_editor_for(self, parent) def dispose(self): """Disposes of the contents of an editor.""" if getattr(self, "_simple_field", None) is not None: self._simple_field.dispose() self._simple_field = None super().dispose() def update_editor(self): """Updates the editor when the object trait changes externally to the editor. """ self._simple_field.update_editor() def update_object_from_swatch(self, color_text): """Updates the object trait when a color swatch is clicked.""" color = QtGui.QColor(*[int(part) for part in color_text.split(",")]) self.value = self.factory.from_qt_color(color) self.update_editor() def string_value(self, color): """Returns the text representation of a specified color value.""" return self.factory.str_color(color) class TextColorEditor(BaseTextEditor): """Text style of color editor, which displays a text field whose background color is the color value. """ def update_object(self): """Handles the user changing the contents of the edit control.""" self.value = str(self.control.text()) set_color(self) def update_editor(self): """Updates the editor when the object trait changes externally to the editor. """ super().update_editor() set_color(self) def string_value(self, color): """Returns the text representation of a specified color value.""" return self.factory.str_color(color) class ReadonlyColorEditor(BaseReadonlyEditor): """Read-only style of color editor, which displays a read-only text field whose background color is the color value. """ def init(self, parent): """Finishes initializing the editor by creating the underlying toolkit widget. """ self.control = QtGui.QLineEdit() self.control.setReadOnly(True) def update_editor(self): """Updates the editor when the object trait changes externally to the editor. """ super().update_editor() set_color(self) def string_value(self, color): """Returns the text representation of a specified color value.""" return self.factory.str_color(color) # ------------------------------------------------------------------------- # Sets the color of the specified editor's color control: # ------------------------------------------------------------------------- def set_color(editor): """Sets the color of the specified color control.""" color = editor.factory.to_qt_color(editor) pal = QtGui.QPalette(editor.control.palette()) pal.setColor(QtGui.QPalette.ColorRole.Base, color) if color.red() > 192 or color.blue() > 192 or color.green() > 192: pal.setColor(QtGui.QPalette.ColorRole.Text, QtCore.Qt.GlobalColor.black) else: pal.setColor(QtGui.QPalette.ColorRole.Text, QtCore.Qt.GlobalColor.white) editor.control.setPalette(pal) # ---------------------------------------------------------------------------- # Creates a custom color editor panel for a specified editor: # ---------------------------------------------------------------------------- class FixedButton(QtGui.QPushButton): """Override to work around a bug in Qt 4.7 on Macs. https://bugreports.qt-project.org/browse/QTBUG-15936 """ def hitButton(self, pos): return QtGui.QAbstractButton.hitButton(self, pos) def color_editor_for(editor, parent): """Creates a custom color editor panel for a specified editor.""" # Create the colour samples if it hasn't already been done. if len(color_samples) == 0: color_choices = (0, 128, 192, 255) for r in color_choices: for g in color_choices: for b in (0, 128, 255): color_samples.append(QtGui.QColor(r, g, b)) root = QtGui.QWidget() panel = QtGui.QHBoxLayout(root) panel.setContentsMargins(0, 0, 0, 0) swatch_editor = editor.factory.simple_editor( editor.ui, editor.object, editor.name, editor.description, None ) swatch_editor.prepare(parent) panel.addWidget(swatch_editor.control) # Add all of the color choice buttons: grid = QtGui.QGridLayout() grid.setSpacing(0) mapper = QtCore.QSignalMapper(panel) rows = 4 cols = len(color_samples) // rows i = 0 sheet_template = """ QPushButton { min-height: 18px; max-height: 18px; min-width: 18px; max-width: 18px; background-color: rgb(%s); } """ for r in range(rows): for c in range(cols): control = FixedButton() color = color_samples[r * cols + c] color_text = "%d,%d,%d,%d" % color.getRgb() control.setStyleSheet(sheet_template % color_text) control.setAttribute(QtCore.Qt.WidgetAttribute.WA_LayoutUsesWidgetRect, True) control.clicked.connect(mapper.map) mapper.setMapping(control, color_text) grid.addWidget(control, r, c) editor.set_tooltip(control) i += 1 if is_pyside and QtCore.__version_info__ >= (5, 15): mapper.mappedString.connect(editor.update_object_from_swatch) else: mapper.mapped[str].connect(editor.update_object_from_swatch) panel.addLayout(grid) return root, swatch_editor # Define the SimpleEditor, CustomEditor, etc. classes which are used by the # editor factory for the color editor. SimpleEditor = SimpleColorEditor CustomEditor = CustomColorEditor TextEditor = TextColorEditor ReadonlyEditor = ReadonlyColorEditor ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/qt/color_trait.py0000644000175100001730000000771400000000000021361 0ustar00runnerdocker00000000000000# (C) Copyright 2008-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # ------------------------------------------------------------------------------ # Copyright (c) 2007, Riverbank Computing Limited # All rights reserved. # # This software is provided without warranty under the terms of the BSD license. # However, when used with the GPL version of PyQt the additional terms # described in the PyQt GPL exception also apply # # Author: Riverbank Computing Limited # ------------------------------------------------------------------------------ """ Trait definition for a PyQt-based color. """ from ast import literal_eval from pyface.qt import QtGui from pyface.color import Color as PyfaceColor from pyface.util.color_helpers import channels_to_ints from pyface.util.color_parser import color_table from traits.api import Trait, TraitError def convert_to_color(object, name, value): """Converts a number into a QColor object.""" # Try the toolkit agnostic format. try: tup = literal_eval(value) except Exception: tup = value if isinstance(value, str): # Allow for spaces in the string value. value = value.replace(" ", "") # is it in the color table? if value in color_table: tup = channels_to_ints(color_table[value]) if isinstance(tup, tuple): if 3 <= len(tup) <= 4 and all(isinstance(x, int) for x in tup): try: color = QtGui.QColor(*tup) except Exception: raise TraitError else: raise TraitError elif isinstance(value, PyfaceColor): color = value.to_toolkit() else: # Let the standard ctors handle the value. try: color = QtGui.QColor(value) except TypeError: raise TraitError if not color.isValid(): raise TraitError return color convert_to_color.info = ( "a string of the form (r,g,b) or (r,g,b,a) where r, " "g, b, and a are integers from 0 to 255, a QColor " "instance, a Qt.GlobalColor, an integer which in hex " "is of the form 0xRRGGBB, a string of the form #RGB, " "#RRGGBB, #RRRGGGBBB or #RRRRGGGGBBBB" ) # ------------------------------------------------------------------------- # Standard colors: # ------------------------------------------------------------------------- standard_colors = {} for name, rgba in color_table.items(): rgba_bytes = channels_to_ints(rgba) standard_colors[str(name)] = QtGui.QColor(*rgba_bytes) # ------------------------------------------------------------------------- # Callable that returns an instance of the PyQtToolkitEditorFactory for color # editors. # ------------------------------------------------------------------------- ### FIXME: We have declared the 'editor' to be a function instead of the # traitsui.qt.color_editor.ToolkitEditorFactory class, since the # latter is leading to too many circular imports. In the future, try to see if # there is a better way to do this. def get_color_editor(*args, **traits): from traitsui.qt.color_editor import ToolkitEditorFactory return ToolkitEditorFactory(*args, **traits) def PyQtColor(default="white", allow_none=False, **metadata): """Defines PyQt-specific color traits.""" if default is None: allow_none = True if allow_none: return Trait( default, None, standard_colors, convert_to_color, editor=get_color_editor, **metadata, ) return Trait( default, standard_colors, convert_to_color, editor=get_color_editor, **metadata, ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/qt/compound_editor.py0000644000175100001730000000536600000000000022233 0ustar00runnerdocker00000000000000# (C) Copyright 2008-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # ------------------------------------------------------------------------------ # Copyright (c) 2007, Riverbank Computing Limited # All rights reserved. # # This software is provided without warranty under the terms of the BSD license. # However, when used with the GPL version of PyQt the additional terms # described in the PyQt GPL exception also apply # # Author: Riverbank Computing Limited # ------------------------------------------------------------------------------ """ Defines the compound editor and the compound editor factory for the PyQt user interface toolkit. """ from pyface.qt import QtGui from traits.api import Str from .editor import Editor class CompoundEditor(Editor): """Editor for compound traits, which displays editors for each of the combined traits, in the appropriate style. """ # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- #: The kind of editor to create for each list item kind = Str() def init(self, parent): """Finishes initializing the editor by creating the underlying toolkit widget. """ self.control = QtGui.QWidget() layout = QtGui.QVBoxLayout(self.control) layout.setContentsMargins(0, 0, 0, 0) # Add all of the component trait editors: self._editors = editors = [] for factory in self.factory.editors: editor = getattr(factory, self.kind)( self.ui, self.object, self.name, self.description, None ) editor.prepare(self.control) layout.addWidget(editor.control) editors.append(editor) def update_editor(self): """Updates the editor when the object trait changes externally to the editor. """ pass def dispose(self): """Disposes of the contents of an editor.""" for editor in self._editors: editor.dispose() super().dispose() class SimpleEditor(CompoundEditor): #: The kind of editor to create for each list item. This value overrides #: the default. kind = "simple_editor" class CustomEditor(CompoundEditor): #: The kind of editor to create for each list item. This value overrides #: the default. kind = "custom_editor" ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/qt/constants.py0000644000175100001730000000401400000000000021042 0ustar00runnerdocker00000000000000# (C) Copyright 2008-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # ------------------------------------------------------------------------------ # Copyright (c) 2007, Riverbank Computing Limited # All rights reserved. # # This software is provided without warranty under the terms of the BSD license. # However, when used with the GPL version of PyQt the additional terms # described in the PyQt GPL exception also apply # # Author: Riverbank Computing Limited # ------------------------------------------------------------------------------ """ Defines constants used by the PyQt implementation of the various text editors and text editor factories. """ from pyface.qt import QtGui from pyface.api import SystemMetrics _palette = QtGui.QApplication.palette() _base = _palette.color(QtGui.QPalette.ColorRole.Base) #: We are in dark mode if field background colour HSV value is dark. is_dark = (_base.value() < 0x80) #: Default dialog title DefaultTitle = "Edit properties" #: Color of valid input OKColor = _base #: Color to highlight input errors if is_dark: ErrorColor = QtGui.QColor(0xcf, 0x66, 0x79) else: ErrorColor = QtGui.QColor(255, 192, 192) #: Color for background of read-only fields ReadonlyColor = _palette.color(QtGui.QPalette.ColorRole.Window) #: Color for background of fields where objects can be dropped DropColor = _palette.color(QtGui.QPalette.ColorRole.Base) #: Color for an editable field EditableColor = _base #: Color for background of windows (like dialog background color) WindowColor = _palette.color(QtGui.QPalette.ColorRole.Window) del _palette #: Screen width screen_dx = SystemMetrics().screen_width #: Screen height screen_dy = SystemMetrics().screen_height ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/qt/csv_list_editor.py0000644000175100001730000000252000000000000022222 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Defines the various text editors for the Qt user interface toolkit. The module is mainly a place-folder for TextEditor factories that have been augmented to also listen to changes in the items of the list object. """ from .text_editor import SimpleEditor as QtSimpleEditor from .text_editor import CustomEditor as QtCustomEditor from .text_editor import ReadonlyEditor as QtReadonlyEditor from ..editors.csv_list_editor import _prepare_method, _dispose_method class SimpleEditor(QtSimpleEditor): """Simple Editor style for CSVListEditor.""" prepare = _prepare_method dispose = _dispose_method class CustomEditor(QtCustomEditor): """Custom Editor style for CSVListEditor.""" prepare = _prepare_method dispose = _dispose_method class ReadonlyEditor(QtReadonlyEditor): """Readonly Editor style for CSVListEditor.""" prepare = _prepare_method dispose = _dispose_method TextEditor = SimpleEditor ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/qt/custom_editor.py0000644000175100001730000000474100000000000021715 0ustar00runnerdocker00000000000000# (C) Copyright 2008-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # ------------------------------------------------------------------------------ # Copyright (c) 2007, Riverbank Computing Limited # All rights reserved. # # This software is provided without warranty under the terms of the BSD license. # However, when used with the GPL version of PyQt the additional terms # described in the PyQt GPL exception also apply # # Author: Riverbank Computing Limited # ------------------------------------------------------------------------------ """ Defines the PyQt implementation of the editor used to wrap a non-Traits based custom control. """ from pyface.qt import QtGui from .editor import Editor # ------------------------------------------------------------------------- # 'CustomEditor' class: # ------------------------------------------------------------------------- class CustomEditor(Editor): """Wrapper for a custom editor control""" # ------------------------------------------------------------------------- # 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.factory if factory is not None: self.control = factory(*((parent, self) + self.factory.args)) if self.control is None: self.control = QtGui.QLabel( "An error occurred creating a custom editor.\n" "Please contact the developer." ) self.control.setStyleSheet("background-color: red; color: white") self.set_tooltip() # ------------------------------------------------------------------------- # Updates the editor when the object trait changes external to the editor: # ------------------------------------------------------------------------- def update_editor(self): """Updates the editor when the object trait changes externally to the editor. """ pass ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/qt/data_frame_editor.py0000644000175100001730000000123200000000000022456 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! from traitsui.ui_editors.data_frame_editor import ( _DataFrameEditor as BaseDataFrameEditor, ) from .ui_editor import UIEditor class _DataFrameEditor(BaseDataFrameEditor, UIEditor): """Qt Toolkit implementation of the DataFrameEditor""" pass ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/qt/date_editor.py0000644000175100001730000002200400000000000021310 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ A Traits UI editor for datetime.date objects. """ import datetime from pyface.qt import QtCore, QtGui from traits.api import Bool, Date, Dict, Instance, Set from .editor import Editor from .editor_factory import ReadonlyEditor as BaseReadonlyEditor from traitsui.editors.date_editor import CellFormat class SimpleEditor(Editor): """Simple Traits UI date editor that wraps QDateEdit.""" def init(self, parent): """Finishes initializing the editor by creating the underlying toolkit widget. """ self.control = QtGui.QDateEdit() if hasattr(self.factory, "qt_date_format"): self.control.setDisplayFormat(self.factory.qt_date_format) if not self.factory.allow_future: self.control.setMaximumDate(QtCore.QDate.currentDate()) if getattr(self.factory, "maximum_date_name", None): obj, extended_name, func = self.parse_extended_name( self.factory.maximum_date_name ) self.factory.maximum_date = func() if getattr(self.factory, "minimum_date_name", None): obj, extended_name, func = self.parse_extended_name( self.factory.minimum_date_name ) self.factory.minimum_date = func() if getattr(self.factory, "minimum_date", None): min_date = QtCore.QDate( self.factory.minimum_date.year, self.factory.minimum_date.month, self.factory.minimum_date.day, ) self.control.setMinimumDate(min_date) if getattr(self.factory, "maximum_date", None): max_date = QtCore.QDate( self.factory.maximum_date.year, self.factory.maximum_date.month, self.factory.maximum_date.day, ) self.control.setMaximumDate(max_date) self.control.dateChanged.connect(self.update_object) def dispose(self): """Disposes of the contents of an editor.""" if self.control is not None: self.control.dateChanged.disconnect(self.update_object) super().dispose() def update_editor(self): """Updates the editor when the object trait changes externally to the editor. """ value = self.value if value: q_date = QtCore.QDate(value.year, value.month, value.day) self.control.setDate(q_date) def update_object(self, q_date): """Handles the user entering input data in the edit control.""" self.value = datetime.date(q_date.year(), q_date.month(), q_date.day()) class CustomEditor(Editor): """Custom Traits UI date editor that wraps QCalendarWidget.""" #: Style used for when a date is unselected. #: Mapping from datetime.date to CellFormat _unselected_styles = Dict(Date, Instance(CellFormat)) #: Selected dates (used when multi_select is true) _selected = Set(Date) def init(self, parent): """Finishes initializing the editor by creating the underlying toolkit widget. """ self.control = QtGui.QCalendarWidget() if not self.factory.allow_future: self.control.setMaximumDate(QtCore.QDate.currentDate()) self.control.clicked.connect(self.update_object) if not self.factory.multi_select: self.control.showSelectedDate() elif self._selected: first_date = min(self._selected) self.control.setCurrentPage(first_date.year, first_date.month) def dispose(self): """Disposes of the contents of an editor.""" if self.control is not None: self.control.clicked.disconnect(self.update_object) super().dispose() def update_editor(self): """Updates the editor when the object trait changes externally to the editor. """ value = self.value if value: if not self.factory.multi_select: q_date = QtCore.QDate(value.year, value.month, value.day) self.control.setSelectedDate(q_date) self.control.showSelectedDate() else: self.apply_unselected_style_to_all() for date in value: self.apply_style(self.factory.selected_style, date) self._selected = set(value) def update_object(self, q_date): """Handles the user entering input data in the edit control.""" value = datetime.date(q_date.year(), q_date.month(), q_date.day()) if self.factory.multi_select: if value in self.value: self.unselect_date(value) else: self.select_date(value) self.value = sorted(self._selected) else: self.value = value def unselect_date(self, date): self._selected.remove(date) self.apply_unselected_style(date) def select_date(self, date): self._selected.add(date) self.apply_style(self.factory.selected_style, date) def set_unselected_style(self, style, date): """Set the style used for a date when it is not selected.""" self._unselected_styles[date] = style if self.factory.multi_select: if date not in self.value: self.apply_style(style, date) else: self.apply_style(style, date) def apply_style(self, style, date): """Apply a given style to a given date.""" qdt = QtCore.QDate(date) textformat = self.control.dateTextFormat(qdt) _apply_cellformat(style, textformat) self.control.setDateTextFormat(qdt, textformat) def apply_unselected_style(self, date): """Apply the style for when a date is unselected.""" # Resets the text format on the given dates textformat = QtGui.QTextCharFormat() if date in self._unselected_styles: _apply_cellformat(self._unselected_styles[date], textformat) qdt = QtCore.QDate(date) self.control.setDateTextFormat(qdt, textformat) def apply_unselected_style_to_all(self): """Make all the selected dates appear unselected.""" for date in self._selected: self.apply_unselected_style(date) # ------------------------------------------------------------------------------ # 'ReadonlyEditor' class: # ------------------------------------------------------------------------------ class ReadonlyEditor(BaseReadonlyEditor): """Readonly Traits UI date editor that uses a QLabel for the view.""" def _get_str_value(self): """Replace the default string value with our own date verision.""" if not self.value: return self.factory.message else: return self.value.strftime(self.factory.strftime) # ------------------------------------------------------------------------ # Helper functions for styling # ------------------------------------------------------------------------ def _apply_cellformat(cf, textformat): """Applies the formatting in the cellformat cf to the QTextCharFormat object provided. """ if cf.italics is not None: textformat.setFontItalic(cf.italics) if cf.underline is not None: textformat.setFontUnderline(cf.underline) if cf.bold is not None: if cf.bold: weight = QtGui.QFont.Weight.Bold else: weight = QtGui.QFont.Weight.Normal textformat.setFontWeight(weight) if cf.bgcolor is not None: textformat.setBackground(_color_to_brush(cf.bgcolor)) if cf.fgcolor is not None: textformat.setForeground(_color_to_brush(cf.fgcolor)) def _textformat_to_cellformat(textformat): """Convert QTextCharFormat to a CellFormat""" bg_brush = textformat.background() fg_brush = textformat.foreground() return CellFormat( italics=textformat.fontItalic(), underline=textformat.fontUnderline(), bold=textformat.fontWeight() == QtGui.QFont.Weight.Bold, bgcolor=_brush_to_color(bg_brush), fgcolor=_brush_to_color(fg_brush), ) def _color_to_brush(color): """Returns a QBrush with the color specified in **color**""" brush = QtGui.QBrush() if isinstance(color, str) and hasattr(QtCore.Qt, color): col = getattr(QtCore.Qt, color) elif isinstance(color, tuple): col = QtGui.QColor() col.setRgb(*color[:4]) else: raise RuntimeError("Invalid color specification '%r'" % color) brush.setColor(col) return brush def _brush_to_color(brush): if brush.style() == 0: # Qt.BrushStyle.NoBrush return None color = brush.color() return (color.red(), color.green(), color.blue(), color.alpha()) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/qt/date_range_editor.py0000644000175100001730000000514400000000000022472 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! import datetime from pyface.qt import QtCore, QtGui from pyface.qt.QtGui import QFont from traits.api import Dict from .date_editor import CustomEditor as DateCustomEditor class CustomEditor(DateCustomEditor): def init(self, parent): if not self.factory.multi_select: raise ValueError("multi_select must be true.") super().init(parent) def update_editor(self): """Updates the editor when the object trait changes externally to the editor. """ start_date, end_date = self.value if start_date is not None and end_date is not None: self._apply_style_to_range(start_date, end_date) elif start_date is None and end_date is None: self.apply_unselected_style_to_all() else: raise ValueError( "The start and end dates must be either both defined or " "both be None. Got {!r}".format(self.value) ) def update_object(self, q_date): """Handles the user entering input data in the edit control.""" value = datetime.date(q_date.year(), q_date.month(), q_date.day()) start_date, end_date = self.value if ( self.factory.allow_no_selection and start_date is not None and end_date is not None and start_date < end_date ): self.value = (None, None) self.apply_unselected_style_to_all() return if start_date is None: start_date = value if end_date is None: end_date = value if start_date != end_date: start_date = value end_date = value elif value > start_date: end_date = value else: start_date = value self.value = (start_date, end_date) self._apply_style_to_range(start_date, end_date) def _apply_style_to_range(self, start_date, end_date): num_days = (end_date - start_date).days + 1 selected_dates = ( start_date + datetime.timedelta(days=i) for i in range(num_days) ) self.apply_unselected_style_to_all() for dt in selected_dates: self.select_date(dt) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/qt/datetime_editor.py0000644000175100001730000001026700000000000022177 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ A Traits UI editor for datetime.datetime objects. """ from pyface.qt import QtGui, is_pyside from pyface.qt.QtCore import QDateTime from traits.api import Datetime, observe from .editor import Editor from .editor_factory import ReadonlyEditor as BaseReadonlyEditor class SimpleEditor(Editor): """Simple Traits UI time editor that wraps QDateTimeEdit.""" #: the earliest datetime allowed by the editor minimum_datetime = Datetime(allow_none=True) #: the latest datetime allowed by the editor maximum_datetime = Datetime(allow_none=True) def init(self, parent): """Finishes initializing the editor by creating the underlying toolkit widget. """ # set min and max early, don't wait for editor sync self.minimum_datetime = self.factory.minimum_datetime self.maximum_datetime = self.factory.maximum_datetime self.control = QtGui.QDateTimeEdit() self.update_minimum_datetime() self.update_maximum_datetime() self.control.dateTimeChanged.connect(self.update_object) def dispose(self): """Disposes of the contents of an editor.""" if self.control is not None: self.control.dateTimeChanged.disconnect(self.update_object) super().dispose() def update_editor(self): """Updates the editor when the object trait changes externally to the editor. """ value = self.value if value: if self.minimum_datetime and self.minimum_datetime > value: value = self.minimum_datetime elif self.maximum_datetime and self.value > self.maximum_datetime: value = self.maximum_datetime try: q_datetime = QDateTime(value) except Exception: pass self.control.setDateTime(q_datetime) self.value = value def update_object(self, q_datetime): """Handles the user entering input data in the edit control.""" try: if is_pyside: self.value = q_datetime.toPython() else: self.value = q_datetime.toPyDateTime() except ValueError: pass @observe('minimum_datetime') def update_minimum_datetime(self, event=None): # sanity checking of values if ( self.minimum_datetime is not None and self.maximum_datetime is not None and self.minimum_datetime > self.maximum_datetime ): self.maximum_datetime = self.minimum_datetime if self.control is not None: if self.minimum_datetime is not None: self.control.setMinimumDateTime( QDateTime(self.minimum_datetime) ) else: self.control.clearMinimumDateTime() @observe('maximum_datetime') def update_maximum_datetime(self, event=None): # sanity checking of values if ( self.minimum_datetime is not None and self.maximum_datetime is not None and self.minimum_datetime > self.maximum_datetime ): self.minimum_datetime = self.maximum_datetime if self.control is not None: if self.maximum_datetime is not None: self.control.setMaximumDateTime( QDateTime(self.maximum_datetime) ) else: self.control.clearMaximumDateTime() class ReadonlyEditor(BaseReadonlyEditor): """Readonly Traits UI time editor that uses a QLabel for the view.""" def _get_str_value(self): """Replace the default string value with our own time version.""" if not self.value: return self.factory.message else: return self.value.strftime(self.factory.strftime) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/qt/directory_editor.py0000644000175100001730000000416100000000000022403 0ustar00runnerdocker00000000000000# (C) Copyright 2008-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # Copyright (c) 2007, Riverbank Computing Limited # All rights reserved. # # This software is provided without warranty under the terms of the BSD license. # However, when used with the GPL version of PyQt the additional terms # described in the PyQt GPL exception also apply # # Author: Riverbank Computing Limited """ Defines various directory editor for the PyQt user interface toolkit. """ from pyface.api import DirectoryDialog from .file_editor import ( SimpleEditor as SimpleFileEditor, CustomEditor as CustomFileEditor, ) class SimpleEditor(SimpleFileEditor): """Simple style of editor for directories, which displays a text field and a **Browse** button that opens a directory-selection dialog box. """ def _create_file_dialog(self): """Creates the correct type of file dialog.""" dlg = DirectoryDialog( parent=self.get_control_widget(), default_path=self._file_name.text(), ) return dlg class CustomEditor(CustomFileEditor): """Custom style of editor for directories, which displays a tree view of the file system. """ def init(self, parent): super().init(parent) self._model.setNameFilterDisables(True) self._model.setNameFilters([""]) def update_object(self, idx): """Handles the user changing the contents of the edit control.""" if self.control is not None: if self._model.isDir(idx): self.value = str(self._model.filePath(idx)) # Trait change handlers -------------------------------------------------- def _filter_changed(self): """Handles the 'filter' trait being changed.""" # name filters don't apply to directories pass ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/qt/drop_editor.py0000644000175100001730000001113600000000000021343 0ustar00runnerdocker00000000000000# (C) Copyright 2009-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # ------------------------------------------------------------------------------ # Copyright (c) 2008, Riverbank Computing Limited # All rights reserved. # # This software is provided without warranty under the terms of the BSD license. # However, when used with the GPL version of PyQt the additional terms # described in the PyQt GPL exception also apply # # Author: Riverbank Computing Limited # ------------------------------------------------------------------------------ """ Defines a drop target editor for the PyQt user interface toolkit. A drop target editor handles drag and drop operations as a drop target. """ from pyface.qt import QtGui, QtCore from .editor import Editor as _BaseEditor from .text_editor import SimpleEditor as Editor from .constants import DropColor from .clipboard import PyMimeData, clipboard class SimpleEditor(Editor): """Simple style of drop editor, which displays a read-only text field that contains the string representation of the object trait's value. """ #: Background color when it is OK to drop objects. ok_color = DropColor def init(self, parent): """Finishes initializing the editor by creating the underlying toolkit widget. """ if self.factory.readonly: self.control = QtGui.QLineEdit(self.str_value) self.control.setReadOnly(True) self.set_tooltip() else: super().init(parent) pal = QtGui.QPalette(self.control.palette()) pal.setColor(QtGui.QPalette.ColorRole.Base, self.ok_color) self.control.setPalette(pal) # Install EventFilter on control to handle DND events. drop_event_filter = _DropEventFilter(self.control) self.control.installEventFilter(drop_event_filter) self.control._qt_editor = self def dispose(self): """Disposes of the content of an editor.""" if self.factory.readonly: # enthought/traitsui#884 _BaseEditor.dispose(self) else: super().dispose() def string_value(self, value): """Returns the text representation of a specified object trait value.""" if value is None: return "" return str(value) def error(self, excp): """Handles an error that occurs while setting the object's trait value.""" pass class _DropEventFilter(QtCore.QObject): def eventFilter(self, source, event): typ = event.type() if typ == QtCore.QEvent.Type.Drop: self.dropEvent(event) elif typ == QtCore.QEvent.Type.DragEnter: self.dragEnterEvent(event) return super().eventFilter(source, event) def dropEvent(self, e): """Handles a Python object being dropped on the tree.""" editor = self.parent()._qt_editor klass = editor.factory.klass if editor.factory.binding: value = getattr(clipboard, "node", None) else: value = e.mimeData().instance() if (klass is None) or isinstance(value, klass): editor._no_update = True try: if hasattr(value, "drop_editor_value"): editor.value = value.drop_editor_value() else: editor.value = value if hasattr(value, "drop_editor_update"): value.drop_editor_update(self) else: self.setText(editor.str_value) finally: editor._no_update = False e.acceptProposedAction() def dragEnterEvent(self, e): """Handles a Python object being dragged over the tree.""" editor = self.parent()._qt_editor if editor.factory.binding: data = getattr(clipboard, "node", None) else: md = e.mimeData() if not isinstance(md, PyMimeData): return data = md.instance() try: editor.object.base_trait(editor.name).validate( editor.object, editor.name, data ) e.acceptProposedAction() except: pass # Define the Text and ReadonlyEditor for use by the editor factory. TextEditor = ReadonlyEditor = SimpleEditor ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/qt/editor.py0000644000175100001730000003456500000000000020332 0ustar00runnerdocker00000000000000# (C) Copyright 2008-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # ------------------------------------------------------------------------------ # Copyright (c) 2007, Riverbank Computing Limited # All rights reserved. # # This software is provided without warranty under the terms of the BSD license. # However, when used with the GPL version of PyQt the additional terms # described in the PyQt GPL exception also apply # # Author: Riverbank Computing Limited # ------------------------------------------------------------------------------ """ Defines the base class for PyQt editors. """ from pyface.qt import QtCore, QtGui 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 PyQt editors for Traits-based UIs.""" def clear_layout(self): """Delete the contents of a control's layout.""" layout = self.control.layout() while True: itm = layout.takeAt(0) if itm is None: break itm.widget().setParent(None) 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.""" if self.control is not None: self.control.setFocus() def update_editor(self): """Updates the editor when the object trait changes externally to the editor. """ new_value = self.str_value if self.control.text() != new_value: self.control.setText(new_value) def get_control_widget(self): """Get the concrete widget for the control. Some editors may use a QLayout instead of a Qwidget, which may not be suitable for certain usages (eg. for parenting other widgets). """ if isinstance(self.control, QtGui.QLayout): return self.control.parentWidget() else: return self.control def set_tooltip_text(self, control, text): """Sets the tooltip for a specified control.""" control.setToolTip(text) 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.setEnabled(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 isinstance(control, QtGui.QWidget): control.setEnabled(enabled) else: for i in range(control.count()): itm = control.itemAt(i) self._enabled_changed_helper( (itm.widget() or itm.layout()), enabled ) def _visible_changed(self, visible): """Handles the **visible** state of the editor being changed.""" if self.label_control is not None: self.label_control.setVisible(visible) if self.control is None: # We are being called after the editor has already gone away. return self._visible_changed_helper(self.control, visible) page = self.control.parent() if ( page is None or page.parent() is None or page.parent().parent() is None or page.layout() is None or page.layout().count() != 1 ): return # The TabWidget (representing the notebook) has a StackedWidget inside it, # which then contains our parent. # Even after the tab is removed, the parent-child relationship between # our container widget (representing the page) and the enclosing TabWidget, # so the following reference is still valid. stack_widget = page.parent() notebook = stack_widget.parent() is_tabbed_group = notebook.property("traits_tabbed_group") if ( notebook is None or not isinstance(notebook, QtGui.QTabWidget) or not is_tabbed_group ): return if not visible: # Store the page number and name on the parent for i in range(0, notebook.count()): if notebook.widget(i) == page: self._tab_index = i self._tab_text = notebook.tabText(i) page.setVisible(False) notebook.removeTab(i) break else: # Check to see if our parent has previously-stored tab # index and text attributes if ( getattr(self, "_tab_index", None) is not None and getattr(self, "_tab_text", None) is not None ): page.setVisible(True) notebook.insertTab(self._tab_index, page, self._tab_text) return def _visible_changed_helper(self, control, visible): """A helper that allows the control to be a layout and recursively manages all its widgets. """ if isinstance(control, QtGui.QWidget): control.setVisible(visible) else: for i in range(control.count()): itm = control.itemAt(i) self._visible_changed_helper( (itm.widget() or itm.layout()), visible ) 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 pal = QtGui.QPalette(item.palette()) if state: color = ErrorColor if getattr(item, "_ok_color", None) is None: item._ok_color = QtGui.QColor( pal.color(QtGui.QPalette.ColorRole.Base) ) else: color = getattr(item, "_ok_color", OKColor) pal.setColor(QtGui.QPalette.ColorRole.Base, color) item.setPalette(pal) def _invalid_changed(self, state): """Handles the editor's invalid state changing.""" self.set_error_state() def perform(self, action, action_event=None): """Performs the action described by a specified Action object.""" self.ui.do_undoable(self._perform, action) def _perform(self, action): method_name = action.action info = self._menu_context["info"] handler = self._menu_context["handler"] object = self._menu_context["object"] selection = self._menu_context["selection"] self._menu_context["action"] = action if method_name.find(".") >= 0: if method_name.find("(") < 0: method_name += "()" try: eval(method_name, globals(), self._menu_context) except: from traitsui.api import raise_to_debug raise_to_debug() return method = getattr(handler, method_name, None) if method is not None: method(info, selection) return if action.on_perform is not None: action.on_perform(selection) return action.perform(selection) 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) def add_to_menu(self, menu_item): """Adds a menu item to the menu bar being constructed.""" action = menu_item.item.action self.eval_when(action.enabled_when, menu_item, "enabled") self.eval_when(action.checked_when, menu_item, "checked") def can_add_to_menu(self, action): """Returns whether the action should be defined in the user interface.""" if action.defined_when != "": try: if not eval( action.defined_when, globals(), self._menu_context ): return False except: from traitsui.api import raise_to_debug raise_to_debug() if action.visible_when != "": try: if not eval( action.visible_when, globals(), self._menu_context ): return False except: from traitsui.api import raise_to_debug raise_to_debug() return True # TODO: move this method, it should be part of ui_panel or some other # place that is responsible for setting up the Qt layout. def set_size_policy(self, direction, resizable, springy, stretch): """Set the size policy of the editor's controller. Based on the "direction" of the group that contains this editor (VGroup or HGroup), set the stretch factor and the resizing policy of the control. Parameters ---------- direction : QtGui.QBoxLayout.Direction Directionality of the group that contains this editor. Either QtGui.QBoxLayout.Direction.LeftToRight or QtGui.QBoxLayout.Direction.TopToBottom resizable : bool True if control should be resizable in the orientation opposite to the group directionality springy : bool True if control should be resizable in the orientation equal to the group directionality stretch : int Stretch factor used by Qt to distribute the total size to each component. """ policy = self.control.sizePolicy() if direction == QtGui.QBoxLayout.Direction.LeftToRight: if springy: policy.setHorizontalStretch(stretch) policy.setHorizontalPolicy(QtGui.QSizePolicy.Policy.Expanding) if resizable: policy.setVerticalStretch(stretch) policy.setVerticalPolicy(QtGui.QSizePolicy.Policy.Expanding) else: # TopToBottom if springy: policy.setVerticalStretch(stretch) policy.setVerticalPolicy(QtGui.QSizePolicy.Policy.Expanding) if resizable: policy.setHorizontalStretch(stretch) policy.setHorizontalPolicy(QtGui.QSizePolicy.Policy.Expanding) self.control.setSizePolicy(policy) class EditorWithList(Editor): """Editor for an object that contains a list.""" # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- #: 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().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().dispose() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/qt/editor_factory.py0000644000175100001730000001202300000000000022042 0ustar00runnerdocker00000000000000# (C) Copyright 2008-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # ------------------------------------------------------------------------------ # Copyright (c) 2007, Riverbank Computing Limited # All rights reserved. # # This software is provided without warranty under the terms of the BSD license. # However, when used with the GPL version of PyQt the additional terms # described in the PyQt GPL exception also apply # # Author: Riverbank Computing Limited # ------------------------------------------------------------------------------ """ Defines the base PyQt classes the various styles of editors used in a Traits-based user interface. """ from pyface.qt import QtCore, QtGui from traits.api import TraitError from .editor import Editor class SimpleEditor(Editor): """Base class for simple style editors, which displays a text field containing the text representation of the object trait value. Clicking in the text field displays an editor-specific dialog box for changing the value. """ def init(self, parent): """Finishes initializing the editor by creating the underlying toolkit widget. """ self.control = _SimpleField(self) self.set_tooltip() # ------------------------------------------------------------------------- # Invokes the pop-up editor for an object trait: # # (Normally overridden in a subclass) # ------------------------------------------------------------------------- def popup_editor(self): """Invokes the pop-up editor for an object trait.""" pass class TextEditor(Editor): """Base class for text style editors, which displays an editable text field, containing a text representation of the object trait value. """ def init(self, parent): """Finishes initializing the editor by creating the underlying toolkit widget. """ self.control = QtGui.QLineEdit(self.str_value) self.control.editingFinished.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.editingFinished.disconnect(self.update_object) super().dispose() def update_object(self): """Handles the user changing the contents of the edit control.""" if self.control is None: return try: self.value = str(self.control.text()) except TraitError as excp: pass class ReadonlyEditor(Editor): """Base class for read-only style editors, which displays a read-only text field, containing a text representation of the object trait value. """ # ------------------------------------------------------------------------- # Finishes initializing the editor by creating the underlying toolkit # widget: # ------------------------------------------------------------------------- text_alignment_map = { "left": QtCore.Qt.AlignmentFlag.AlignLeft, "right": QtCore.Qt.AlignmentFlag.AlignRight, "just": QtCore.Qt.AlignmentFlag.AlignJustify, "top": QtCore.Qt.AlignmentFlag.AlignLeft, "bottom": QtCore.Qt.AlignmentFlag.AlignBottom, "vcenter": QtCore.Qt.AlignmentFlag.AlignVCenter, "hcenter": QtCore.Qt.AlignmentFlag.AlignHCenter, "center": QtCore.Qt.AlignmentFlag.AlignVCenter | QtCore.Qt.AlignmentFlag.AlignHCenter, } def init(self, parent): """Finishes initializing the editor by creating the underlying toolkit widget. """ self.control = QtGui.QLabel(self.str_value) if self.item.resizable is True or self.item.height != -1.0: self.control.setWordWrap(True) alignment = None for item in self.factory.text_alignment.split(","): item_alignment = self.text_alignment_map.get(item, None) if item_alignment: if alignment: alignment = alignment | item_alignment else: alignment = item_alignment if alignment: self.control.setAlignment(alignment) self.set_tooltip() def update_editor(self): """Updates the editor when the object trait changes externally to the editor. """ self.control.setText(self.str_value) class _SimpleField(QtGui.QLineEdit): def __init__(self, editor): QtGui.QLineEdit.__init__(self, editor.str_value) self.setReadOnly(True) self._editor = editor def mouseReleaseEvent(self, e): QtGui.QLineEdit.mouseReleaseEvent(self, e) if e.button() == QtCore.Qt.MouseButton.LeftButton: self._editor.popup_editor() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/qt/enum_editor.py0000644000175100001730000004040000000000000021337 0ustar00runnerdocker00000000000000# (C) Copyright 2009-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # ------------------------------------------------------------------------------ # Copyright (c) 2008, Riverbank Computing Limited # All rights reserved. # # This software is provided without warranty under the terms of the BSD license. # However, when used with the GPL version of PyQt the additional terms # described in the PyQt GPL exception also apply # # Author: Riverbank Computing Limited # ------------------------------------------------------------------------------ """ Defines the various editors and the editor factory for single-selection enumerations, for the PyQt user interface toolkit. """ from functools import reduce from pyface.qt import QtCore, QtGui, is_pyside from traits.api import Bool, Property from traitsui.helper import enum_values_changed from .constants import OKColor, ErrorColor from .editor import Editor completion_mode_map = { "popup": QtGui.QCompleter.CompletionMode.PopupCompletion, "inline": QtGui.QCompleter.CompletionMode.InlineCompletion, } class BaseEditor(Editor): """Base class for enumeration editors.""" #: Current set of enumeration names: names = Property() #: Current mapping from names to values: mapping = Property() #: Current inverse mapping from values to names: inverse_mapping = Property() # ------------------------------------------------------------------------- # BaseEditor Interface # ------------------------------------------------------------------------- def values_changed(self): """Recomputes the cached data based on the underlying enumeration model or the values of the factory. """ ( self._names, self._mapping, self._inverse_mapping, ) = enum_values_changed(self._value(), self.string_value) def rebuild_editor(self): """Rebuilds the contents of the editor whenever the original factory object's **values** trait changes. This is not needed for the Qt backends. """ raise NotImplementedError # ------------------------------------------------------------------------- # Editor Interface # ------------------------------------------------------------------------- def init(self, parent): """Finishes initializing the editor by creating the underlying toolkit widget. """ factory = self.factory if factory.name != "": self._object, self._name, self._value = self.parse_extended_name( factory.name ) self.values_changed() self._object.observe( self._update_values_and_rebuild_editor, self._name + '.items', dispatch="ui", ) else: self._value = lambda: self.factory.values self.values_changed() factory.observe( self._update_values_and_rebuild_editor, "values", dispatch="ui" ) def dispose(self): """Disposes of the contents of an editor.""" if self._object is not None: self._object.observe( self._update_values_and_rebuild_editor, self._name + '.items', remove=True, dispatch="ui", ) else: self.factory.observe( self._update_values_and_rebuild_editor, "values", remove=True, dispatch="ui", ) super().dispose() # ------------------------------------------------------------------------- # Private interface # ------------------------------------------------------------------------- # Trait default handlers ------------------------------------------------- def _get_names(self): """Gets the current set of enumeration names.""" return self._names def _get_mapping(self): """Gets the current mapping.""" return self._mapping def _get_inverse_mapping(self): """Gets the current inverse mapping.""" return self._inverse_mapping # Trait change handlers -------------------------------------------------- def _update_values_and_rebuild_editor(self, event): """Handles the underlying object model's enumeration set or factory's values being changed. """ self.values_changed() self.rebuild_editor() class SimpleEditor(BaseEditor): """Simple style of enumeration editor, which displays a combo box.""" # ------------------------------------------------------------------------- # Editor Interface # ------------------------------------------------------------------------- def init(self, parent): """Finishes initializing the editor by creating the underlying toolkit widget. """ super().init(parent) self.control = control = self.create_combo_box() self._add_items_to_combo_box() control.currentIndexChanged.connect(self.update_object) if self.factory.evaluate is not None: control.setEditable(True) control.completer().setCompletionMode( completion_mode_map[self.factory.completion_mode] ) if self.factory.auto_set: control.editTextChanged.connect(self.update_text_object) else: control.lineEdit().editingFinished.connect( self.update_autoset_text_object ) control.setInsertPolicy(QtGui.QComboBox.InsertPolicy.NoInsert) self._no_enum_update = 0 self.set_tooltip() def update_editor(self): """Updates the editor when the object trait changes externally to the editor. """ if self._no_enum_update == 0: self._no_enum_update += 1 if self.factory.evaluate is None: try: index = self.names.index(self.inverse_mapping[self.value]) self.control.setCurrentIndex(index) except Exception: self.control.setCurrentIndex(-1) else: try: self.control.setEditText(self.str_value) except Exception: self.control.setEditText("") self._no_enum_update -= 1 def error(self, excp): """Handles an error that occurs while setting the object's trait value.""" self._set_background(ErrorColor) def rebuild_editor(self): """Rebuilds the contents of the editor whenever the original factory object's **values** trait changes. """ self.control.blockSignals(True) try: self.control.clear() self._add_items_to_combo_box() finally: self.control.blockSignals(False) self.update_editor() def set_size_policy(self, direction, resizable, springy, stretch): super().set_size_policy(direction, resizable, springy, stretch) if (direction == QtGui.QBoxLayout.Direction.LeftToRight and springy) or ( direction != QtGui.QBoxLayout.Direction.LeftToRight and resizable ): self.control.setSizeAdjustPolicy( QtGui.QComboBox.SizeAdjustPolicy.AdjustToContentsOnFirstShow ) # ------------------------------------------------------------------------- # Private interface # ------------------------------------------------------------------------- def create_combo_box(self): """Returns the QComboBox used for the editor control.""" control = QtGui.QComboBox() control.setSizeAdjustPolicy(QtGui.QComboBox.SizeAdjustPolicy.AdjustToContents) control.setSizePolicy( QtGui.QSizePolicy.Policy.Maximum, QtGui.QSizePolicy.Policy.Fixed ) return control def _add_items_to_combo_box(self): for name in self.names: if self.factory.use_separator and name == self.factory.separator: self.control.insertSeparator(self.control.count()) else: self.control.addItem(name) def _set_background(self, col): le = self.control.lineEdit() pal = QtGui.QPalette(le.palette()) pal.setColor(QtGui.QPalette.ColorRole.Base, col) le.setPalette(pal) # Signal handlers ------------------------------------------------------- def update_object(self, index): """Handles the user selecting a new value from the combo box.""" if self._no_enum_update == 0: self._no_enum_update += 1 try: text = self.names[index] self.value = self.mapping[text] except Exception: from traitsui.api import raise_to_debug raise_to_debug() self._no_enum_update -= 1 def update_text_object(self, text): """Handles the user typing text into the combo box text entry field.""" if self._no_enum_update == 0: value = str(text) try: value = self.mapping[value] except Exception: try: value = self.factory.evaluate(value) except Exception as excp: self.error(excp) return self._no_enum_update += 1 try: self.value = value except Exception as excp: self._no_enum_update -= 1 self.error(excp) return self._set_background(OKColor) self._no_enum_update -= 1 def update_autoset_text_object(self): # Don't get the final text with the editingFinished signal if self.control is not None: text = self.control.lineEdit().text() return self.update_text_object(text) class RadioEditor(BaseEditor): """Enumeration editor, used for the "custom" style, that displays radio buttons. """ #: Is the button layout row-major or column-major? row_major = Bool(False) # ------------------------------------------------------------------------- # Editor Interface # ------------------------------------------------------------------------- def init(self, parent): """Finishes initializing the editor by creating the underlying toolkit widget. """ super().init(parent) self.control = QtGui.QWidget() layout = QtGui.QGridLayout(self.control) layout.setContentsMargins(0, 0, 0, 0) self._mapper = QtCore.QSignalMapper() if is_pyside and QtCore.__version_info__ >= (5, 15): self._mapper.mappedInt.connect(self.update_object) else: self._mapper.mapped.connect(self.update_object) self.rebuild_editor() def update_editor(self): """Updates the editor when the object trait changes externally to the editor. """ layout = self.control.layout() value = self.value for i in range(layout.count()): rb = layout.itemAt(i).widget() rb.setChecked(rb.value == value) def rebuild_editor(self): """Rebuilds the contents of the editor whenever the original factory object's **values** trait changes. """ # Clear any existing content: self.clear_layout() # Get the current trait value: cur_name = self.str_value # Create a sizer to manage the radio buttons: names = self.names mapping = self.mapping n = len(names) cols = self.factory.cols rows = (n + cols - 1) // cols # incr will keep track of how to increment index so that as we traverse # the grid in row major order, the elements are added to appear in # the correct order if self.row_major: incr = [1] * cols else: incr = [n // cols] * cols rem = n % cols for i in range(cols): incr[i] += rem > i incr[-1] = -(reduce(lambda x, y: x + y, incr[:-1], 0) - 1) # e.g for a gird: # 0 2 4 # 1 3 5 # incr should be [2, 2, -3] # Add the set of all possible choices: layout = self.control.layout() index = 0 # populate the layout in row_major order for i in range(rows): for j in range(cols): if n > 0: name = names[index] rb = self.create_button(name) rb.value = mapping[name] rb.setChecked(name == cur_name) # The connection type is set to workaround Qt5 + MacOSX # issue with event dispatching. See enthought/traitsui#1308 rb.clicked.connect( self._mapper.map, type=QtCore.Qt.ConnectionType.QueuedConnection ) self._mapper.setMapping(rb, index) self.set_tooltip(rb) layout.addWidget(rb, i, j) index += int(round(incr[j])) n -= 1 # ------------------------------------------------------------------------- # Private interface # ------------------------------------------------------------------------- def create_button(self, name): """Returns the QAbstractButton used for the radio button.""" label = self.string_value(name, str.capitalize) return QtGui.QRadioButton(label) # Signal handlers ------------------------------------------------------- def update_object(self, index): """Handles the user clicking one of the custom radio buttons.""" try: self.value = self.mapping[self.names[index]] except Exception: pass class ListEditor(BaseEditor): """Enumeration editor, used for the "custom" style, that displays a list box. """ # ------------------------------------------------------------------------- # Editor Interface # ------------------------------------------------------------------------- def init(self, parent): """Finishes initializing the editor by creating the underlying toolkit widget. """ super().init(parent) self.control = QtGui.QListWidget() self.control.currentTextChanged.connect(self.update_object) self.rebuild_editor() self.set_tooltip() def update_editor(self): """Updates the editor when the object trait changes externally to the editor. """ control = self.control try: value = self.inverse_mapping[self.value] for row in range(control.count()): itm = control.item(row) if itm.text() == value: control.setCurrentItem(itm) control.scrollToItem(itm) break except Exception: pass def rebuild_editor(self): """Rebuilds the contents of the editor whenever the original factory object's **values** trait changes. """ self.control.blockSignals(True) self.control.clear() for name in self.names: self.control.addItem(name) self.control.blockSignals(False) self.update_editor() # ------------------------------------------------------------------------- # Private interface # ------------------------------------------------------------------------- # Signal handlers ------------------------------------------------------- def update_object(self, text): """Handles the user selecting a list box item.""" value = str(text) try: value = self.mapping[value] except Exception: try: value = self.factory.evaluate(value) except Exception: pass try: self.value = value except Exception: pass ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1684768028.0958068 traitsui-8.0.0/traitsui/qt/extra/0000755000175100001730000000000000000000000017600 5ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/qt/extra/__init__.py0000644000175100001730000000000000000000000021677 0ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/qt/extra/bounds_editor.py0000644000175100001730000001440300000000000023014 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! from pyface.qt import QtGui, QtCore from traits.api import Float, Any, Str, Union from traitsui.editors.api import RangeEditor from traitsui.qt.editor import Editor from traitsui.qt.extra.range_slider import RangeSlider class _BoundsEditor(Editor): evaluate = Any() min = Any() max = Any() low = Any() high = Any() 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.max = factory.max self.min = factory.min self.evaluate = factory.evaluate self.sync_value(factory.evaluate_name, "evaluate", "from") self.sync_value(factory.low_name, "low", "both") self.sync_value(factory.high_name, "high", "both") self.control = QtGui.QWidget() panel = QtGui.QHBoxLayout(self.control) panel.setContentsMargins(0, 0, 0, 0) self._label_lo = QtGui.QLineEdit(self.format_str % self.low) self._label_lo.editingFinished.connect(self.update_low_on_enter) panel.addWidget(self._label_lo) # The default size is a bit too big and probably doesn't need to grow. sh = self._label_lo.sizeHint() sh.setWidth(sh.width() // 2) self._label_lo.setMaximumSize(sh) self.control.slider = slider = RangeSlider(QtCore.Qt.Orientation.Horizontal) slider.setTracking(factory.auto_set) slider.setMinimum(0) slider.setMaximum(10000) slider.setPageStep(1000) slider.setSingleStep(100) slider.setLow(self._convert_to_slider(self.low)) slider.setHigh(self._convert_to_slider(self.high)) slider.sliderMoved.connect(self.update_object_on_scroll) panel.addWidget(slider) self._label_hi = QtGui.QLineEdit(self.format_str % self.high) self._label_hi.editingFinished.connect(self.update_high_on_enter) panel.addWidget(self._label_hi) # The default size is a bit too big and probably doesn't need to grow. sh = self._label_hi.sizeHint() sh.setWidth(sh.width() // 2) self._label_hi.setMaximumSize(sh) self.set_tooltip(slider) self.set_tooltip(self._label_lo) self.set_tooltip(self._label_hi) def update_low_on_enter(self): try: try: low = eval(str(self._label_lo.text()).strip()) if self.evaluate is not None: low = self.evaluate(low) except Exception as ex: low = self.low self._label_lo.setText(self.format_str % self.low) if not self.factory.is_float: low = int(low) if low > self.high: low = self.high - self._step_size() self._label_lo.setText(self.format_str % low) self.control.slider.setLow(self._convert_to_slider(low)) self.low = low except: pass def update_high_on_enter(self): try: try: high = eval(str(self._label_hi.text()).strip()) if self.evaluate is not None: high = self.evaluate(high) except: high = self.high self._label_hi.setText(self.format_str % self.high) if not self.factory.is_float: high = int(high) if high < self.low: high = self.low + self._step_size() self._label_hi.setText(self.format_str % high) self.control.slider.setHigh(self._convert_to_slider(high)) self.high = high except: pass def update_object_on_scroll(self, pos): low = self._convert_from_slider(self.control.slider.low()) high = self._convert_from_slider(self.control.slider.high()) if self.factory.is_float: self.low = low self.high = high else: self.low = int(low) self.high = int(high) # update the sliders to the int values or the sliders # will jiggle self.control.slider.setLow(self._convert_to_slider(low)) self.control.slider.setHigh(self._convert_to_slider(high)) def update_editor(self): return def _check_max_and_min(self): # check if max & min have been defined: if self.max is None: self.max = self.high if self.min is None: self.min = self.low def _step_size(self): slider_delta = ( self.control.slider.maximum() - self.control.slider.minimum() ) range_delta = self.max - self.min return float(range_delta) / slider_delta def _convert_from_slider(self, slider_val): self._check_max_and_min() return self.min + slider_val * self._step_size() def _convert_to_slider(self, value): self._check_max_and_min() return ( self.control.slider.minimum() + (value - self.min) / self._step_size() ) def _low_changed(self, low): if self.control is None: return if self._label_lo is not None: self._label_lo.setText(self.format_str % low) self.control.slider.setLow(self._convert_to_slider(low)) def _high_changed(self, high): if self.control is None: return if self._label_hi is not None: self._label_hi.setText(self.format_str % high) self.control.slider.setHigh(self._convert_to_slider(self.high)) class BoundsEditor(RangeEditor): min = Union(None, Float) max = Union(None, Float) def _get_simple_editor_class(self): return _BoundsEditor def _get_custom_editor_class(self): return _BoundsEditor ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/qt/extra/checkbox_renderer.py0000644000175100001730000001120100000000000023621 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ A renderer which displays a checked-box for a True value and an unchecked box for a false value. """ # System library imports from pyface.qt import QtCore, QtGui # ETS imports from traitsui.qt.table_editor import TableDelegate class CheckboxRenderer(TableDelegate): """A renderer which displays a checked-box for a True value and an unchecked box for a false value. """ # ------------------------------------------------------------------------- # QAbstractItemDelegate interface # ------------------------------------------------------------------------- def editorEvent(self, event, model, option, index): """Reimplemented to handle mouse button clicks.""" if ( event.type() == QtCore.QEvent.Type.MouseButtonRelease and event.button() == QtCore.Qt.MouseButton.LeftButton ): column = index.model()._editor.columns[index.column()] obj = index.data(QtCore.Qt.ItemDataRole.UserRole) checked = bool(column.get_raw_value(obj)) column.set_value(obj, not checked) return True else: return False def paint(self, painter, option, index): """Reimplemented to paint the checkbox.""" # Determine whether the checkbox is check or unchecked column = index.model()._editor.columns[index.column()] obj = index.data(QtCore.Qt.ItemDataRole.UserRole) checked = column.get_raw_value(obj) # First draw the background painter.save() row_brushes = [option.palette.base(), option.palette.alternateBase()] if option.state & QtGui.QStyle.StateFlag.State_Selected: if option.state & QtGui.QStyle.StateFlag.State_Active: color_group = QtGui.QPalette.ColorGroup.Active else: color_group = QtGui.QPalette.ColorGroup.Inactive bg_brush = option.palette.brush( color_group, QtGui.QPalette.ColorRole.Highlight ) else: bg_brush = index.data(QtCore.Qt.ItemDataRole.BackgroundRole) if bg_brush == NotImplemented or bg_brush is None: if index.model()._editor.factory.alternate_bg_color: bg_brush = row_brushes[index.row() % 2] else: bg_brush = row_brushes[0] painter.fillRect(option.rect, bg_brush) # Then draw the checkbox style = QtGui.QApplication.instance().style() box = QtGui.QStyleOptionButton() box.palette = option.palette # Align the checkbox appropriately. box.rect = option.rect size = style.sizeFromContents( QtGui.QStyle.ContentsType.CT_CheckBox, box, QtCore.QSize(), None ) box.rect.setWidth(size.width()) margin = style.pixelMetric(QtGui.QStyle.PixelMetric.PM_ButtonMargin, box) alignment = column.horizontal_alignment if alignment == "left": box.rect.setLeft(option.rect.left() + margin) elif alignment == "right": box.rect.setLeft(option.rect.right() - size.width() - margin) else: # FIXME: I don't know why I need the 2 pixels, but I do. box.rect.setLeft( option.rect.left() + option.rect.width() // 2 - size.width() // 2 + margin - 2 ) # We mark the checkbox always active even when not selected, so # it's clear if it's ticked or not on OSX. See bug #439 if option.state & QtGui.QStyle.StateFlag.State_Enabled: box.state = QtGui.QStyle.StateFlag.State_Enabled | QtGui.QStyle.StateFlag.State_Active if checked: box.state |= QtGui.QStyle.StateFlag.State_On else: box.state |= QtGui.QStyle.StateFlag.State_Off style.drawControl(QtGui.QStyle.ControlElement.CE_CheckBox, box, painter) painter.restore() def sizeHint(self, option, index): """Reimplemented to provide size hint based on a checkbox""" box = QtGui.QStyleOptionButton() style = QtGui.QApplication.instance().style() return style.sizeFromContents( QtGui.QStyle.ContentsType.CT_CheckBox, box, QtCore.QSize(), None ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/qt/extra/led_editor.py0000644000175100001730000000200300000000000022257 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! from pyface.qt import QtGui from traitsui.qt.editor import Editor from traitsui.basic_editor_factory import BasicEditorFactory from traits.api import Any, Undefined class _LEDEditor(Editor): def init(self, parent): self.control = QtGui.QLCDNumber() self.control.setSegmentStyle(QtGui.QLCDNumber.SegmentStyle.Flat) self.set_tooltip() def update_editor(self): self.control.display(self.str_value) class LEDEditor(BasicEditorFactory): #: The editor class to be created klass = _LEDEditor #: Alignment is not supported for QT backend alignment = Any(Undefined) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/qt/extra/progress_renderer.py0000644000175100001730000000471000000000000023706 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ A renderer which displays a progress bar. """ # System library imports import sys from pyface.qt import QtCore, QtGui # ETS imports from traitsui.qt.table_editor import TableDelegate class ProgressRenderer(TableDelegate): """A renderer which displays a progress bar.""" # ------------------------------------------------------------------------- # QAbstractItemDelegate interface # ------------------------------------------------------------------------- def paint(self, painter, option, index): """Paint the progressbar.""" # Get the column and object column = index.model()._editor.columns[index.column()] obj = index.data(QtCore.Qt.ItemDataRole.UserRole) # set up progress bar options progress_bar_option = QtGui.QStyleOptionProgressBar() progress_bar_option.rect = option.rect progress_bar_option.minimum = column.get_minimum(obj) progress_bar_option.maximum = column.get_maximum(obj) progress_bar_option.progress = int(column.get_raw_value(obj)) progress_bar_option.textVisible = column.get_text_visible() progress_bar_option.text = column.get_value(obj) # Draw it style = QtGui.QApplication.instance().style() if sys.platform == "darwin": # Save painter state, translate painter to cell location, and then # restore painter state after drawing to solve enthought/traitsui#964 # This seems to be mac only. # ref: https://forum.qt.io/topic/105375/qitemdelegate-for-drawing-progress-bar-working-but-won-t-move-off-origin # noqa: E501 painter.save() painter.translate(option.rect.left(), option.rect.top()) style.drawControl( QtGui.QStyle.ControlElement.CE_ProgressBar, progress_bar_option, painter ) painter.restore() else: # For non-mac platforms, just draw. style.drawControl( QtGui.QStyle.ControlElement.CE_ProgressBar, progress_bar_option, painter ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/qt/extra/qt_view.py0000644000175100001730000000572600000000000021642 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Defines a Traits UI View that allows for the customization of Qt-specific widget properties. """ # Standard library imports. import logging # System library imports. from pyface.qt import QtGui # Enthought library imports. from traits.api import File, List, Str from traitsui.view import View from io import open # Logger. logger = logging.getLogger(__name__) class QtView(View): """A View that allows the specification of Qt style sheets. This works with any non-modal View. """ #: An optional string containing a Qt style sheet. style_sheet = Str() #: An optional file path for a Qt style sheet. style_sheet_path = File() #: A list of trait names that defines the order for focus switching via #: Tab/Shift+Tab. If the view contains multiple items for a specified trait # name, the order is undefined. tab_order = List(Str) # ------------------------------------------------------------------------- # Creates a UI user interface object: # ------------------------------------------------------------------------- def ui( self, context, parent=None, kind=None, view_elements=None, handler=None, id="", scrollable=None, args=None, ): ui = super().ui( context, parent, kind, view_elements, handler, id, scrollable, args ) if not ui.control: return ui if self.style_sheet: ui.control.setStyleSheet(self.style_sheet) if self.style_sheet_path: try: with open(self.style_sheet_path, "r", encoding="utf8") as f: ui.control.setStyleSheet(f.read()) except IOError: logger.exception("Error loading Qt style sheet") if len(self.tab_order) >= 2: previous = self._get_editor_control(ui, self.tab_order[0]) for i in range(1, len(self.tab_order)): current = self._get_editor_control(ui, self.tab_order[i]) QtGui.QWidget.setTabOrder(previous, current) previous = current return ui # ------------------------------------------------------------------------- # Private interface: # ------------------------------------------------------------------------- def _get_editor_control(self, ui, name): control = None editors = ui.get_editors(name) if editors: control = editors[0].control else: logger.warning("No item for '%s' trait" % name) return control ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/qt/extra/range_slider.py0000644000175100001730000001533300000000000022615 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! from pyface.qt import QtGui, QtCore class RangeSlider(QtGui.QSlider): """A slider for ranges. This class provides a dual-slider for ranges, where there is a defined maximum and minimum, as is a normal slider, but instead of having a single slider value, there are 2 slider values. This class emits the same signals as the QSlider base class, with the exception of valueChanged """ def __init__(self, *args): super().__init__(*args) self._low = self.minimum() self._high = self.maximum() self.pressed_control = QtGui.QStyle.SubControl.SC_None self.hover_control = QtGui.QStyle.SubControl.SC_None self.click_offset = 0 # 0 for the low, 1 for the high, -1 for both self.active_slider = 0 def low(self): return self._low def setLow(self, low): self._low = low self.update() def high(self): return self._high def setHigh(self, high): self._high = high self.update() def paintEvent(self, event): # based on # http://qt.gitorious.org/qt/qt/blobs/master/src/gui/widgets/qslider.cpp painter = QtGui.QPainter(self) style = QtGui.QApplication.style() for i, value in enumerate([self._low, self._high]): opt = QtGui.QStyleOptionSlider() self.initStyleOption(opt) # Only draw the groove for the first slider so it doesn't get drawn # on top of the existing ones every time if i == 0: opt.subControls = ( QtGui.QStyle.SC_SliderGroove | QtGui.QStyle.SC_SliderHandle ) else: opt.subControls = QtGui.QStyle.SC_SliderHandle if self.tickPosition() != self.NoTicks: opt.subControls |= QtGui.QStyle.SC_SliderTickmarks if self.pressed_control: opt.activeSubControls = self.pressed_control opt.state |= QtGui.QStyle.StateFlag.State_Sunken else: opt.activeSubControls = self.hover_control opt.sliderPosition = value opt.sliderValue = value style.drawComplexControl( QtGui.QStyle.ComplexControl.CC_Slider, opt, painter, self ) def mousePressEvent(self, event): event.accept() style = QtGui.QApplication.style() button = event.button() # In a normal slider control, when the user clicks on a point in the # slider's total range, but not on the slider part of the control the # control would jump the slider value to where the user clicked. # For this control, clicks which are not direct hits will slide both # slider parts if button: opt = QtGui.QStyleOptionSlider() self.initStyleOption(opt) self.active_slider = -1 for i, value in enumerate([self._low, self._high]): opt.sliderPosition = value hit = style.hitTestComplexControl( style.CC_Slider, opt, event.pos(), self ) if hit == style.SC_SliderHandle: self.active_slider = i self.pressed_control = hit self.triggerAction(self.SliderMove) self.setRepeatAction(self.SliderNoAction) self.setSliderDown(True) break if self.active_slider < 0: self.pressed_control = QtGui.QStyle.SC_SliderHandle self.click_offset = self.__pixelPosToRangeValue( self.__pick(event.pos()) ) self.triggerAction(self.SliderMove) self.setRepeatAction(self.SliderNoAction) else: event.ignore() def mouseMoveEvent(self, event): if self.pressed_control != QtGui.QStyle.SC_SliderHandle: event.ignore() return event.accept() new_pos = self.__pixelPosToRangeValue(self.__pick(event.pos())) opt = QtGui.QStyleOptionSlider() self.initStyleOption(opt) if self.active_slider < 0: offset = new_pos - self.click_offset self._high += offset self._low += offset if self._low < self.minimum(): diff = self.minimum() - self._low self._low += diff self._high += diff if self._high > self.maximum(): diff = self.maximum() - self._high self._low += diff self._high += diff elif self.active_slider == 0: if new_pos >= self._high: new_pos = self._high - 1 self._low = new_pos else: if new_pos <= self._low: new_pos = self._low + 1 self._high = new_pos self.click_offset = new_pos self.update() self.sliderMoved.emit(new_pos) def __pick(self, pt): if self.orientation() == QtCore.Qt.Orientation.Horizontal: return pt.x() else: return pt.y() def __pixelPosToRangeValue(self, pos): opt = QtGui.QStyleOptionSlider() self.initStyleOption(opt) style = QtGui.QApplication.style() gr = style.subControlRect( style.CC_Slider, opt, style.SC_SliderGroove, self ) sr = style.subControlRect( style.CC_Slider, opt, style.SC_SliderHandle, self ) if self.orientation() == QtCore.Qt.Orientation.Horizontal: slider_length = sr.width() slider_min = gr.x() slider_max = gr.right() - slider_length + 1 else: slider_length = sr.height() slider_min = gr.y() slider_max = gr.bottom() - slider_length + 1 return style.sliderValueFromPosition( self.minimum(), self.maximum(), pos - slider_min, slider_max - slider_min, opt.upsideDown, ) if __name__ == "__main__": import sys def echo(value): print(value) app = QtGui.QApplication(sys.argv) slider = RangeSlider() slider.setMinimum(0) slider.setMaximum(10000) slider.setLow(0) slider.setHigh(10000) slider.sliderMoved.connect(echo) slider.show() app.exec_() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/qt/extra/table_image_renderer.py0000644000175100001730000000574000000000000024277 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ A renderer which will display a cell-specific image in addition to some text displayed in the same way the default renderer would. """ # System library imports from pyface.qt import QtCore, QtGui # ETS imports from traits.api import Bool from traitsui.qt.table_editor import TableDelegate class TableImageRenderer(TableDelegate): """A renderer which will display a cell-specific image in addition to some text displayed in the same way the default renderer would. """ #: Should the image be scaled to the size of the cell scale_to_cell = Bool(True) # ------------------------------------------------------------------------- # TableImageRenderer interface # ------------------------------------------------------------------------- def get_image_for_obj(self, value, row, col): """Return the image for the cell given the raw cell value and the row and column numbers. """ return None # ------------------------------------------------------------------------- # QAbstractItemDelegate interface # ------------------------------------------------------------------------- def paint(self, painter, option, index): """Overriden to draw images.""" # First draw any text/background by delegating to our superclass QtGui.QStyledItemDelegate.paint(self, painter, option, index) # Now draw the image, if possible value = index.data(QtCore.Qt.ItemDataRole.UserRole) image = self.get_image_for_obj(value, index.row(), index.column()) if image: image = image.create_bitmap() if self.scale_to_cell: w = min(image.width(), option.rect.width()) h = min(image.height(), option.rect.height()) else: w = image.width() h = image.height() x = option.rect.x() y = option.rect.y() + (option.rect.height() - h) / 2 target = QtCore.QRect(x, y, w, h) painter.drawPixmap(target, image) def sizeHint(self, option, index): """Overriden to take image size into account when providing a size hint. """ size = QtGui.QStyledItemDelegate.sizeHint(self, option, index) value = index.data(QtCore.Qt.ItemDataRole.UserRole) image = self.get_image_for_obj(value, index.row(), index.column()) if image: image = image.create_bitmap() size.setWidth(int(max(image.width(), size.width()))) size.setHeight(int(max(image.height(), size.height()))) return size ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/qt/file_editor.py0000644000175100001730000002505000000000000021316 0ustar00runnerdocker00000000000000# (C) Copyright 2008-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # Copyright (c) 2007, Riverbank Computing Limited # All rights reserved. # # This software is provided without warranty under the terms of the BSD license. # However, when used with the GPL version of PyQt the additional terms # described in the PyQt GPL exception also apply # # Author: Riverbank Computing Limited """ Defines file editors for the PyQt user interface toolkit. """ from os.path import abspath, splitext, isfile, exists from pyface.qt import QtCore, QtGui, is_qt4 from pyface.api import FileDialog, OK from traits.api import Any, Callable, List, Event, File, Str, TraitError, Tuple from .editor import Editor from .text_editor import SimpleEditor as SimpleTextEditor from .helper import IconButton # Wildcard filter: filter_trait = List(Str) class SimpleEditor(SimpleTextEditor): """Simple style of file editor, consisting of a text field and a **Browse** button that opens a file-selection dialog box. The user can also drag and drop a file onto this control. """ #: List of tuple(signal, slot) to be removed in dispose. #: First item in the tuple is the Qt signal, the second item is the event #: handler. _connections_to_remove = List(Tuple(Any(), Callable())) #: Wildcard filter to apply to the file dialog: filter = filter_trait def init(self, parent): """Finishes initializing the editor by creating the underlying toolkit widget. """ self.control = QtGui.QWidget() layout = QtGui.QHBoxLayout(self.control) layout.setContentsMargins(0, 0, 0, 0) self._file_name = control = QtGui.QLineEdit() layout.addWidget(control) if self.factory.auto_set: control.textEdited.connect(self.update_object) self._connections_to_remove.append( (control.textEdited, self.update_object) ) else: # Assume enter_set is set, or else the value will never get # updated. control.editingFinished.connect(self.update_object) self._connections_to_remove.append( (control.editingFinished, self.update_object) ) button = IconButton(QtGui.QStyle.StandardPixmap.SP_DirIcon, self.show_file_dialog) layout.addWidget(button) self.set_tooltip(control) self.filter = self.factory.filter self.sync_value(self.factory.filter_name, "filter", "from", is_list=True) def dispose(self): """Disposes of the contents of an editor.""" while self._connections_to_remove: signal, handler = self._connections_to_remove.pop() signal.disconnect(handler) # IconButton.clicked signal should be disconnected here. # (enthought/traitsui#888) # skip the dispose from TextEditor (enthought/traitsui#884) Editor.dispose(self) def update_object(self): """Handles the user changing the contents of the edit control.""" if self.control is not None: file_name = str(self._file_name.text()) try: if self.factory.truncate_ext: file_name = splitext(file_name)[0] self.value = file_name except TraitError as excp: self._file_name.setText(self.value) def update_editor(self): """Updates the editor when the object trait changes externally to the editor. """ self._file_name.setText(self.str_value) def show_file_dialog(self): """Displays the pop-up file dialog.""" # We don't used the canned functions because we don't know how the # file name is to be used (ie. an existing one to be opened or a new # one to be created). dlg = self._create_file_dialog() dlg.open() if dlg.return_code == OK: if self.factory.truncate_ext: self.value = splitext(dlg.path)[0] else: self.value = dlg.path self.update_editor() def get_error_control(self): """Returns the editor's control for indicating error status.""" return self._file_name # -- Private Methods ------------------------------------------------------ def _create_file_dialog(self): """Creates the correct type of file dialog.""" wildcard = " ".join(self.factory.filter) dlg = FileDialog( parent=self.get_control_widget(), default_path=self._file_name.text(), action="save as" if self.factory.dialog_style == "save" else "open", wildcard=wildcard, ) return dlg class CustomEditor(SimpleTextEditor): """Custom style of file editor, consisting of a file system tree view.""" #: Is the file editor scrollable? This value overrides the default. scrollable = True #: Wildcard filter to apply to the file dialog: filter = filter_trait #: The root path of the file tree view. root_path = File() #: Event fired when the file system view should be rebuilt: reload = Event() #: Event fired when the user double-clicks a file: dclick = Event() def init(self, parent): """Finishes initializing the editor by creating the underlying toolkit widget. """ control = _TreeView(self) control.doubleClicked.connect(self._on_dclick) self._model = model = QtGui.QFileSystemModel() current_path = abspath(self.str_value) # Don't apply filters to directories and don't show "." and ".." model.setFilter( QtCore.QDir.Filter.AllDirs | QtCore.QDir.Filter.Files | QtCore.QDir.Filter.Drives | QtCore.QDir.Filter.NoDotAndDotDot ) # Hide filtered out files instead of only disabling them self._model.setNameFilterDisables(False) # Show the user's home directory by default. model.setRootPath(QtCore.QDir.homePath()) control.setModel(model) control.setRootIndex(model.index(QtCore.QDir.homePath())) self.control = control # set the current item if current_path: index = model.index(current_path) control.expand(index) control.setCurrentIndex(index) # Hide the labels at the top and only show the column for the file name self.control.header().hide() for column in range(1, model.columnCount()): self.control.hideColumn(column) factory = self.factory self.filter = factory.filter self.root_path = factory.root_path self.sync_value(factory.filter_name, "filter", "from", is_list=True) self.sync_value(factory.root_path_name, "root_path", "from") self.sync_value(factory.reload_name, "reload", "from") self.sync_value(factory.dclick_name, "dclick", "to") self.set_tooltip() # This is needed to enable horizontal scrollbar. header = self.control.header() if is_qt4: header.setResizeMode(0, QtGui.QHeaderView.ResizeMode.ResizeToContents) else: header.setSectionResizeMode(0, QtGui.QHeaderView.ResizeMode.ResizeToContents) header.setStretchLastSection(False) def dispose(self): """Disposes of the contents of an editor.""" self._model.beginResetModel() self._model.endResetModel() if self.control is not None: self.control.doubleClicked.disconnect(self._on_dclick) # Skip dispose from simple text editor (enthought/traitsui#884) Editor.dispose(self) def update_object(self, idx): """Handles the user changing the contents of the edit control.""" if self.control is not None: path = str(self._model.filePath(idx)) if self.factory.allow_dir or isfile(path): if self.factory.truncate_ext: path = splitext(path)[0] self.value = path def update_editor(self): """Updates the editor when the object trait changes externally to the editor. """ if exists(self.str_value): index = self._model.index(abspath(self.str_value)) self.control.expand(index) self.control.setCurrentIndex(index) # -- Private Methods ------------------------------------------------------ def _on_dclick(self, idx): """Handles the user double-clicking on a file name.""" self.dclick = str(self._model.filePath(idx)) # Trait change handlers -------------------------------------------------- def _filter_changed(self): """Handles the 'filter' trait being changed.""" self._model.setNameFilters(self.filter) def _root_path_changed(self): """Handles the 'root_path' trait being changed.""" path = self.root_path if not path: path = QtCore.QDir.homePath() self._model.setRootPath(path) self.control.setRootIndex(self._model.index(path)) def _reload_changed(self): """Handles the 'reload' trait being changed.""" self._model.refresh() class _TreeView(QtGui.QTreeView): """This is an internal class needed because QAbstractItemView doesn't provide a signal for when the current index changes. """ def __init__(self, editor): super().__init__() self._editor = editor def event(self, event): if event.type() == QtCore.QEvent.Type.ToolTip: index = self.indexAt(event.pos()) if index and index.isValid(): QtGui.QToolTip.showText(event.globalPos(), index.data(), self) else: QtGui.QToolTip.hideText() event.ignore() return True return super().event(event) def keyPressEvent(self, keyevent): key = keyevent.key() if key == QtCore.Qt.Key.Key_Return or key == QtCore.Qt.Key.Key_Enter: self._editor._on_dclick(self.selectedIndexes()[0]) keyevent.accept() QtGui.QTreeView.keyPressEvent(self, keyevent) def currentChanged(self, current, previous): """Reimplemented to tell the editor when the current index has changed.""" super().currentChanged(current, previous) self._editor.update_object(current) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/qt/font_editor.py0000644000175100001730000002104500000000000021345 0ustar00runnerdocker00000000000000# (C) Copyright 2008-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # ------------------------------------------------------------------------------ # Copyright (c) 2007, Riverbank Computing Limited # All rights reserved. # # This software is provided without warranty under the terms of the BSD license. # However, when used with the GPL version of PyQt the additional terms # described in the PyQt GPL exception also apply # # Author: Riverbank Computing Limited # ------------------------------------------------------------------------------ """ Defines the various font editors and the font editor factory, for the PyQt user interface toolkit.. """ from pyface.qt import QtCore, QtGui from traitsui.editors.font_editor import ( ToolkitEditorFactory as BaseToolkitEditorFactory, ) from .editor_factory import ( SimpleEditor as BaseSimpleEditor, TextEditor as BaseTextEditor, ReadonlyEditor as BaseReadonlyEditor, ) from .editor import Editor # Standard font point sizes PointSizes = [ "8", "9", "10", "11", "12", "14", "16", "18", "20", "22", "24", "26", "28", "36", "48", "72", ] # --------------------------------------------------------------------------- # The PyQtToolkitEditorFactory class. # --------------------------------------------------------------------------- ## We need to add qt-specific methods to the editor factory, and so we create ## a subclass of the BaseToolkitEditorFactory. class ToolkitEditorFactory(BaseToolkitEditorFactory): """PyQt editor factory for font editors.""" def to_qt_font(self, editor): """Returns a QFont object corresponding to a specified object's font trait. """ return QtGui.QFont(editor.value) def from_qt_font(self, font): """Gets the application equivalent of a QFont value.""" return font def str_font(self, font): """Returns the text representation of the specified object trait value.""" weight = {QtGui.QFont.Weight.Light: " Light", QtGui.QFont.Weight.Bold: " Bold"}.get( font.weight(), "" ) style = { QtGui.QFont.Style.StyleOblique: " Slant", QtGui.QFont.Style.StyleItalic: " Italic", }.get(font.style(), "") return "%s point %s%s%s" % ( font.pointSize(), font.family(), style, weight, ) class SimpleFontEditor(BaseSimpleEditor): """Simple style of font editor, which displays a text field that contains a text representation of the font value (using that font if possible). Clicking the field displays a font selection dialog box. """ def popup_editor(self): """Invokes the pop-up editor for an object trait.""" fnt, ok = QtGui.QFontDialog.getFont( self.factory.to_qt_font(self), self.control ) if ok: self.value = self.factory.from_qt_font(fnt) self.update_editor() def update_editor(self): """Updates the editor when the object trait changes externally to the editor. """ super().update_editor() set_font(self) def string_value(self, font): """Returns the text representation of a specified font value.""" return self.factory.str_font(font) class CustomFontEditor(Editor): """Custom style of font editor, which displays the following: * A text field containing the text representation of the font value (using that font if possible). * A combo box containing all available type face names. * A combo box containing the available type sizes. """ def init(self, parent): """Finishes initializing the editor by creating the underlying toolkit widget. """ self.control = QtGui.QWidget() layout = QtGui.QVBoxLayout(self.control) layout.setContentsMargins(0, 0, 0, 0) # Add the standard font control: self._font = font = QtGui.QLineEdit(self.str_value) font.editingFinished.connect(self.update_object) layout.addWidget(font) # Add all of the font choice controls: layout2 = QtGui.QHBoxLayout() self._facename = control = QtGui.QFontComboBox() control.setEditable(False) control.currentFontChanged.connect(self.update_object_parts) layout2.addWidget(control) self._point_size = control = QtGui.QComboBox() control.addItems(PointSizes) control.currentIndexChanged.connect(self.update_object_parts) layout2.addWidget(control) # These don't have explicit controls. self._bold = self._italic = False layout.addLayout(layout2) def update_object(self): """Handles the user changing the contents of the font text control.""" self.value = str(self._font.text()) self._set_font(self.factory.to_qt_font(self)) self.update_editor() def update_object_parts(self): """Handles the user modifying one of the font components.""" fnt = self._facename.currentFont() fnt.setBold(self._bold) fnt.setItalic(self._italic) psz = int(self._point_size.currentText()) fnt.setPointSize(psz) self.value = self.factory.from_qt_font(fnt) self._font.setText(self.str_value) self._set_font(fnt) def update_editor(self): """Updates the editor when the object trait changes externally to the editor. """ font = self.factory.to_qt_font(self) self._bold = font.bold() self._italic = font.italic() self._facename.setCurrentFont(font) try: idx = PointSizes.index(str(font.pointSize())) except ValueError: idx = PointSizes.index("9") self._point_size.setCurrentIndex(idx) def string_value(self, font): """Returns the text representation of a specified font value.""" return self.factory.str_font(font) def get_error_control(self): """Returns the editor's control for indicating error status.""" return [self._font, self._facename, self._point_size] # -- Private Methods ------------------------------------------------------ def _set_font(self, font): """Sets the font used by the text control to the specified font.""" font.setPointSize(min(10, font.pointSize())) self._font.setFont(font) class TextFontEditor(BaseTextEditor): """Text style of font editor, which displays an editable text field containing a text representation of the font value (using that font if possible). """ def update_object(self): """Handles the user changing the contents of the edit control.""" self.value = str(self.control.text()) def update_editor(self): """Updates the editor when the object trait changes external to the editor. """ super().update_editor() set_font(self) def string_value(self, font): """Returns the text representation of a specified font value.""" return self.factory.str_font(font) class ReadonlyFontEditor(BaseReadonlyEditor): """Read-only style of font editor, which displays a read-only text field containing a text representation of the font value (using that font if possible). """ def update_editor(self): """Updates the editor when the object trait changes external to the editor. """ super().update_editor() set_font(self) def string_value(self, font): """Returns the text representation of a specified font value.""" return self.factory.str_font(font) # ------------------------------------------------------------------------- # Set the editor control's font to match a specified font: # ------------------------------------------------------------------------- def set_font(editor): """Sets the editor control's font to match a specified font.""" editor.control.setFont(editor.factory.to_qt_font(editor)) # Define the names SimpleEditor, CustomEditor, TextEditor and ReadonlyEditor # which are looked up by the editor factory for the font editor. SimpleEditor = SimpleFontEditor CustomEditor = CustomFontEditor TextEditor = TextFontEditor ReadonlyEditor = ReadonlyFontEditor ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/qt/font_trait.py0000644000175100001730000001507700000000000021212 0ustar00runnerdocker00000000000000# (C) Copyright 2008-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # ------------------------------------------------------------------------------ # Copyright (c) 2007, Riverbank Computing Limited # All rights reserved. # # This software is provided without warranty under the terms of the BSD license. # However, when used with the GPL version of PyQt the additional terms # described in the PyQt GPL exception also apply # # Author: Riverbank Computing Limited # ------------------------------------------------------------------------------ """ Trait definition for a PyQt-based font. """ from pyface.qt import QtGui from pyface.font import Font as PyfaceFont from traits.api import Trait, TraitHandler, TraitError # ------------------------------------------------------------------------- # Convert a string into a valid QFont object (if possible): # ------------------------------------------------------------------------- # Mapping of strings to valid QFont style hints. font_families = { "default": QtGui.QFont.StyleHint.AnyStyle, "decorative": QtGui.QFont.StyleHint.Decorative, "roman": QtGui.QFont.StyleHint.Serif, "script": QtGui.QFont.StyleHint.Cursive, "swiss": QtGui.QFont.StyleHint.SansSerif, "modern": QtGui.QFont.StyleHint.TypeWriter, } # Mapping of strings to QFont styles. font_styles = { "slant": QtGui.QFont.Style.StyleOblique, "oblique": QtGui.QFont.Style.StyleOblique, "italic": QtGui.QFont.Style.StyleItalic, } # Mapping of strings to QFont weights. font_weights = {"light": QtGui.QFont.Weight.Light, "bold": QtGui.QFont.Weight.Bold} # Strings to ignore in text representations of fonts font_noise = ["pt", "point", "family"] def font_to_str(font): """Converts a QFont into a string description of itself.""" style_hint = { QtGui.QFont.StyleHint.Decorative: "decorative", QtGui.QFont.StyleHint.Serif: "roman", QtGui.QFont.StyleHint.Cursive: "script", QtGui.QFont.StyleHint.SansSerif: "swiss", QtGui.QFont.StyleHint.TypeWriter: "modern", }.get(font.styleHint(), "") weight = {QtGui.QFont.Weight.Light: " Light", QtGui.QFont.Weight.Bold: " Bold"}.get( font.weight(), "" ) style = { QtGui.QFont.Style.StyleOblique: " Oblique", QtGui.QFont.Style.StyleItalic: " Italic", }.get(font.style(), "") underline = "" if font.underline(): underline = " underline" return "%s point %s%s%s%s" % ( font.pointSize(), str(font.family()), style, weight, underline, ) def create_traitsfont(value): """Create a TraitsFont object. This can take either a string description, a QFont, or a Pyface Font. """ if isinstance(value, PyfaceFont): return TraitsFont(value.to_toolkit()) if isinstance(value, QtGui.QFont): return TraitsFont(value) point_size = None family = "" style_hint = QtGui.QFont.StyleHint.AnyStyle style = QtGui.QFont.Style.StyleNormal weight = QtGui.QFont.Weight.Normal underline = False facename = [] for word in value.split(): lword = word.lower() if lword in font_families: style_hint = font_families[lword] elif lword in font_styles: style = font_styles[lword] elif lword in font_weights: weight = font_weights[lword] elif lword == "underline": underline = True elif lword not in font_noise: if point_size is None: try: point_size = int(lword) continue except ValueError: pass facename.append(word) if facename: family = " ".join(facename) if family: fnt = TraitsFont(family) else: fnt = TraitsFont() fnt.setStyleHint(style_hint) fnt.setStyle(style) fnt.setWeight(weight) fnt.setUnderline(underline) if point_size is None: fnt.setPointSize(QtGui.QApplication.font().pointSize()) else: fnt.setPointSize(point_size) return fnt class TraitsFont(QtGui.QFont): """A Traits-specific QFont.""" def __reduce_ex__(self, protocol): """Returns the pickleable form of a TraitsFont object.""" return (create_traitsfont, (font_to_str(self),)) def __str__(self): """Returns a printable form of the font.""" return font_to_str(self) # ------------------------------------------------------------------------- # 'TraitPyQtFont' class' # ------------------------------------------------------------------------- class TraitPyQtFont(TraitHandler): """Ensures that values assigned to a trait attribute are valid font descriptor strings; the value actually assigned is the corresponding TraitsFont. """ def validate(self, object, name, value): """Validates that the value is a valid font descriptor string. If so, it returns the corresponding TraitsFont; otherwise, it raises a TraitError. """ if value is None: return None try: return create_traitsfont(value) except: pass raise TraitError(object, name, "a font descriptor string", repr(value)) def info(self): return ( "a string describing a font (e.g. '12 pt bold italic " "swiss family Arial' or 'default 12')" ) # ------------------------------------------------------------------------- # Callable that returns an instance of the PyQtToolkitEditorFactory for font # editors. # ------------------------------------------------------------------------- ### FIXME: We have declared the 'editor' to be a function instead of the # traitsui.qt.font_editor.ToolkitEditorFactory class, since the # latter is leading to too many circular imports. In the future, try to see if # there is a better way to do this. def get_font_editor(*args, **traits): from traitsui.qt.font_editor import ToolkitEditorFactory return ToolkitEditorFactory(*args, **traits) # ------------------------------------------------------------------------- # Define a PyQt specific font trait: # ------------------------------------------------------------------------- PyQtFont = Trait(TraitsFont(), TraitPyQtFont(), editor=get_font_editor) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/qt/helper.py0000644000175100001730000002220300000000000020305 0ustar00runnerdocker00000000000000# (C) Copyright 2008-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # ------------------------------------------------------------------------------ # Copyright (c) 2007, Riverbank Computing Limited # All rights reserved. # # This software is provided without warranty under the terms of the BSD license. # However, when used with the GPL version of PyQt the additional terms # described in the PyQt GPL exception also apply # # Author: Riverbank Computing Limited # ------------------------------------------------------------------------------ """ Defines helper functions and classes used to define PyQt-based trait editors and trait editor factories. """ import os.path from pyface.api import SystemMetrics from pyface.qt import QtCore, QtGui, is_qt5, is_pyqt, qt_api from pyface.ui_traits import convert_image from traits.api import Enum, CTrait, BaseTraitHandler, TraitError from traitsui.ui_traits import SequenceTypes #: Layout orientation for a control and its associated editor Orientation = Enum("horizontal", "vertical") #: Dock-related stubs. DockStyle = Enum("horizontal", "vertical", "tab", "fixed") def pixmap_cache(name, path=None): """Convert an image file name to a cached QPixmap Returns the QPixmap corresponding to a filename. If the filename does not contain a path component, 'path' is used (or if 'path' is not specified, the local 'images' directory is used). """ if name[:1] == "@": image = convert_image(name.replace(" ", "_").lower()) if image is not None: return image.create_image() name_path, name = os.path.split(name) name = name.replace(" ", "_").lower() if name_path: filename = os.path.join(name_path, name) else: if path is None: filename = os.path.join(os.path.dirname(__file__), "images", name) else: filename = os.path.join(path, name) filename = os.path.abspath(filename) if is_qt5: pm = QtGui.QPixmapCache.find(filename) if pm is None: pm = QtGui.QPixmap(filename) QtGui.QPixmapCache.insert(filename, pm) else: pm = QtGui.QPixmap() if not QtGui.QPixmapCache.find(filename, pm): pm.load(filename) QtGui.QPixmapCache.insert(filename, pm) return pm def position_window(window, width=None, height=None, parent=None): """Positions a window on the screen with a specified width and height so that the window completely fits on the screen if possible. """ # Get the available geometry of the screen containing the window. system_metrics = SystemMetrics() screen_dx = system_metrics.screen_width screen_dy = system_metrics.screen_height # Use the frame geometry even though it is very unlikely that the X11 frame # exists at this point. fgeom = window.frameGeometry() width = width or fgeom.width() height = height or fgeom.height() if parent is None: parent = window._parent if parent is None: # Center the popup on the screen. window.move((screen_dx - width) // 2, (screen_dy - height) // 2) return # Calculate the desired size of the popup control: if isinstance(parent, QtGui.QWidget): gpos = parent.mapToGlobal(QtCore.QPoint()) x = gpos.x() y = gpos.y() cdx = parent.width() cdy = parent.height() # Get the frame height of the parent and assume that the window will # have a similar frame. Note that we would really like the height of # just the top of the frame. pw = parent.window() fheight = pw.frameGeometry().height() - pw.height() else: # Special case of parent being a screen position and size tuple (used # to pop-up a dialog for a table cell): x, y, cdx, cdy = parent fheight = 0 x -= (width - cdx) / 2 y += cdy + fheight # Position the window (making sure it will fit on the screen). window.move( max(0, min(x, screen_dx - width)), max(0, min(y, screen_dy - height)) ) def restore_window(ui): """Restores the user preference items for a specified UI.""" prefs = ui.restore_prefs() if prefs is not None: ui.control.setGeometry(*prefs) def save_window(ui): """Saves the user preference items for a specified UI.""" geom = ui.control.geometry() ui.save_prefs((geom.x(), geom.y(), geom.width(), geom.height())) class IconButton(QtGui.QPushButton): """The IconButton class is a push button that contains a small image or a standard icon provided by the current style. """ def __init__(self, icon, slot): """Initialise the button. icon is either the name of an image file or one of the QtGui.QStyle.SP_* values. """ QtGui.QPushButton.__init__(self) # Get the current style. sty = QtGui.QApplication.instance().style() # Get the minimum icon size to use. ico_sz = sty.pixelMetric(QtGui.QStyle.PixelMetric.PM_ButtonIconSize) if isinstance(icon, str): pm = pixmap_cache(icon) # Increase the icon size to accomodate the image if needed. pm_width = pm.width() pm_height = pm.height() if ico_sz < pm_width: ico_sz = pm_width if ico_sz < pm_height: ico_sz = pm_height ico = QtGui.QIcon(pm) else: ico = sty.standardIcon(icon) # Configure the button. self.setIcon(ico) self.setFlat(True) self.setFocusPolicy(QtCore.Qt.FocusPolicy.NoFocus) self.clicked.connect(slot) def sizeHint(self): """Reimplement sizeHint to return a recommended button size based on the size of the icon. Returns ------- size : QtCore.QSize """ self.ensurePolished() # We want the button to have a size similar to the icon. # Using the size computed for a tool button gives a desirable size. option = QtGui.QStyleOptionButton() self.initStyleOption(option) size = self.style().sizeFromContents( QtGui.QStyle.ContentsType.CT_ToolButton, option, option.iconSize ) return size # ------------------------------------------------------------------------ # Text Rendering helpers # ------------------------------------------------------------------------ def wrap_text_with_elision(text, font, width, height): """Wrap paragraphs to fit inside a given size, eliding if too long. Parameters ---------- text : unicode The text to wrap. font : QFont instance The font the text will be rendered in. width : int The width of the box the text will display in. height : int The height of the box the text will display in. Returns ------- lines : list of unicode The lines of text to display, split """ # XXX having an LRU cache on this might be useful font_metrics = QtGui.QFontMetrics(font) line_spacing = font_metrics.lineSpacing() y_offset = 0 lines = [] for paragraph in text.splitlines(): line_start = 0 text_layout = QtGui.QTextLayout(paragraph, font) text_layout.beginLayout() while y_offset + line_spacing <= height: line = text_layout.createLine() if not line.isValid(): break line.setLineWidth(int(width)) line_start = line.textStart() line_end = line_start + line.textLength() line_text = paragraph[line_start:line_end].rstrip() lines.append(line_text) y_offset += line_spacing text_layout.endLayout() if y_offset + line_spacing > height: break if lines and y_offset + line_spacing > height: # elide last line as we ran out of room last_line = paragraph[line_start:] lines[-1] = font_metrics.elidedText( last_line, QtCore.Qt.TextElideMode.ElideRight, width ) return lines # ------------------------------------------------------------------------ # Object lifetime helpers # ------------------------------------------------------------------------ def qobject_is_valid(qobject): """Return whether the wrapped Qt object is still valid. Parameters ---------- qobject : QObject instance The wrapped Qt QObject to inspect. Returns ------- valid : bool Whether or not the underlying C++ object still exists. """ if is_pyqt: from sip import isdeleted return not isdeleted(qobject) elif qt_api == 'pyside2': from shiboken2 import isValid return isValid(qobject) elif qt_api == 'pyside6': from shiboken6 import isValid return isValid(qobject) else: # unknown wrapper raise RuntimeError("Unknown Qt API {qt_api}") ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/qt/history_editor.py0000644000175100001730000000745700000000000022113 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Defines a text editor which displays a text field and maintains a history of previously entered values. """ from pyface.qt import QtGui, is_qt4, is_qt5 from .editor import Editor class _HistoryEditor(Editor): """Simple style text editor, which displays a text field and maintains a history of previously entered values, the maximum number of which is specified by the 'entries' trait of the HistoryEditor factory. """ # ------------------------------------------------------------------------- # 'Editor' interface: # ------------------------------------------------------------------------- def init(self, parent): """Finishes initializing the editor by creating the underlying toolkit widget. """ self.control = control = QtGui.QComboBox() control.setEditable(True) control.setInsertPolicy(QtGui.QComboBox.InsertPolicy.InsertAtTop) if self.factory.entries > 0: control.model().rowsInserted.connect(self._truncate) if self.factory.auto_set: control.editTextChanged.connect(self.update_object) else: if is_qt4 or is_qt5: control.activated[str].connect(self.update_object) else: control.textActivated.connect(self.update_object) self.set_tooltip() def update_object(self, text): """Handles the user entering input data in the edit control.""" if not self._no_update: self.value = str(text) def update_editor(self): """Updates the editor when the object trait changes externally to the editor. """ self._no_update = True self.control.setEditText(self.str_value) self._no_update = False # -- UI preference save/restore interface --------------------------------- def restore_prefs(self, prefs): """Restores any saved user preference information associated with the editor. """ history = prefs.get("history") if history: self._no_update = True self.control.addItems(history[: self.factory.entries]) # Adding items sets the edit text, so we reset it afterwards: self.control.setEditText(self.str_value) self._no_update = False def save_prefs(self): """Returns any user preference information associated with the editor.""" history = [ str(self.control.itemText(index)) for index in range(self.control.count()) ] # If the view closed successfully, update the history with the current # editor value, as long it is different from the current object value: if self.ui.result: current = str(self.control.currentText()) if current != self.str_value: history.insert(0, current) return {"history": history} # ------------------------------------------------------------------------- # '_HistoryEditor' private interface: # ------------------------------------------------------------------------- def _truncate(self, parent, start, end): """Handle items being added to the combo box. If there are too many, remove items at the end. """ diff = self.control.count() - self.factory.entries if diff > 0: for i in range(diff): self.control.removeItem(self.factory.entries) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/qt/html_editor.py0000644000175100001730000001157400000000000021351 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Defines the HTML "editor" for the QT4 user interface toolkit. HTML editors interpret and display HTML-formatted text, but do not modify it. """ import webbrowser from pyface.qt import QtCore, QtGui from traits.api import Str from .editor import Editor try: from pyface.qt import QtWebKit # Subclass of QWebPage for QtWebEngine support class ExternallyOpeningWebPage(QtWebKit.QWebPage): """QWebEnginePage subclass that opens links in system browser This subclass is only used when we are given a QWebEnginePage which is pretending to be a QWebPage and we want the open_external feature of the Editor. This overrides the acceptNavigationRequest method to open links in an external browser. All other navigation requests are handled internally as per the base class. """ def acceptNavigationRequest(self, url, type, isMainFrame): if type == QtWebKit.QWebPage.NavigationTypeLinkClicked: webbrowser.open_new(url.toString()) return False else: return super().acceptNavigationRequest(url, type, isMainFrame) WebView = QtWebKit.QWebView HAS_WEB_VIEW = True except Exception: WebView = QtGui.QTextBrowser HAS_WEB_VIEW = False # ------------------------------------------------------------------------- # 'SimpleEditor' class: # ------------------------------------------------------------------------- class SimpleEditor(Editor): """Simple style editor for HTML.""" # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- #: 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() def init(self, parent): """Finishes initializing the editor by creating the underlying toolkit widget. """ self.control = WebView() self.control.setSizePolicy( QtGui.QSizePolicy.Policy.Expanding, QtGui.QSizePolicy.Policy.Expanding ) if self.factory.open_externally: if HAS_WEB_VIEW: page = self.control.page() if hasattr(page, 'setLinkDelegationPolicy'): # QtWebKit page.setLinkDelegationPolicy( QtWebKit.QWebPage.DelegateAllLinks ) page.linkClicked.connect(self._link_clicked) else: # QtWebEngine pretending to be QtWebKit # We need the subclass defined above instead of the # regular web page so that links are opened externally page = ExternallyOpeningWebPage(self.control) self.control.setPage(page) else: # take over handling clicks on links self.control.setOpenLinks(False) self.control.anchorClicked.connect(self._link_clicked) self.base_url = self.factory.base_url self.sync_value(self.factory.base_url_name, "base_url", "from") def dispose(self): """Disposes of the contents of an editor.""" if self.control is not None and self.factory.open_externally: if HAS_WEB_VIEW: page = self.control.page() if hasattr(page, 'setLinkDelegationPolicy'): # QtWebKit-only cleanup page.linkClicked.disconnect(self._link_clicked) else: # QTextBrowser clean-up self.control.anchorClicked.disconnect(self._link_clicked) super().dispose() 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) if self.base_url and HAS_WEB_VIEW: url = self.base_url if not url.endswith("/"): url += "/" self.control.setHtml(text, QtCore.QUrl.fromLocalFile(url)) else: self.control.setHtml(text) # -- Event Handlers ------------------------------------------------------- def _base_url_changed(self): self.update_editor() def _link_clicked(self, url): webbrowser.open_new(url.toString()) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/qt/image_editor.py0000644000175100001730000002326100000000000021463 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Traits UI 'display only' image editor. """ from pyface.qt.QtGui import QFrame, QPainter, QPalette from pyface.image_resource import ImageResource from pyface.ui_traits import convert_bitmap # FIXME: ImageEditor is a proxy class defined here just for backward # compatibility. The class has been moved to the # traitsui.editors.image_editor file. from traitsui.editors.image_editor import ImageEditor from .editor import Editor # ------------------------------------------------------------------------- # 'QImageView' class: # ------------------------------------------------------------------------- # backported and modified from Enaml ImageView class QImageView(QFrame): """A custom QFrame that will paint a QPixmap as an image. The api is similar to QLabel, but with a few more options to control how the image scales. """ def __init__(self, parent=None): """Initialize a QImageView. Parameters ---------- parent : QWidget or None, optional The parent widget of this image viewer. """ super().__init__(parent) self._pixmap = None self._scaled_contents = False self._allow_upscaling = False self._preserve_aspect_ratio = False self._allow_clipping = False self.setBackgroundRole(QPalette.ColorRole.Window) # -------------------------------------------------------------------------- # Private API # -------------------------------------------------------------------------- def paintEvent(self, event): """A custom paint event handler which draws the image according to the current size constraints. """ pixmap = self._pixmap if pixmap is None: super().paintEvent(event) return pm_size = pixmap.size() pm_width = pm_size.width() pm_height = pm_size.height() if pm_width == 0 or pm_height == 0: super().paintEvent(event) width = self.size().width() height = self.size().height() if not self._scaled_contents: # If the image isn't scaled, it is centered if possible. # Otherwise, it's painted at the origin and clipped. paint_x = max(0, int(width / 2.0 - pm_width / 2.0)) paint_y = max(0, int(height / 2.0 - pm_height / 2.0)) paint_width = pm_width paint_height = pm_height else: # If the image *is* scaled, it's scaled size depends on the # size of the paint area as well as the other scaling flags. if self._preserve_aspect_ratio: pm_ratio = float(pm_width) / pm_height ratio = float(width) / height if ratio >= pm_ratio: if self._allow_upscaling: paint_height = height else: paint_height = min(pm_height, height) paint_width = int(paint_height * pm_ratio) else: if self._allow_upscaling: paint_width = width else: paint_width = min(pm_width, width) paint_height = int(paint_width / pm_ratio) else: if self._allow_upscaling: paint_height = height paint_width = width else: paint_height = min(pm_height, height) paint_width = min(pm_width, width) # In all cases of scaling, we know that the scaled image is # no larger than the paint area, and can thus be centered. paint_x = int(width / 2.0 - paint_width / 2.0) paint_y = int(height / 2.0 - paint_height / 2.0) # Finally, draw the pixmap into the calculated rect. painter = QPainter(self) painter.setRenderHint(QPainter.RenderHint.SmoothPixmapTransform) painter.drawPixmap(paint_x, paint_y, paint_width, paint_height, pixmap) # -------------------------------------------------------------------------- # Public API # -------------------------------------------------------------------------- def sizeHint(self): """Returns a appropriate size hint for the image based on the underlying QPixmap. """ pixmap = self._pixmap if pixmap is not None: return pixmap.size() return super().sizeHint() def minimumSizeHint(self): """Returns a appropriate minimum size hint for the image based on the underlying QPixmap. """ pixmap = self._pixmap if ( pixmap is not None and not self._allow_clipping and not self._scaled_contents ): return pixmap.size() return super().sizeHint() def pixmap(self): """Returns the underlying pixmap for the image view.""" return self._pixmap def setPixmap(self, pixmap): """Set the pixmap to use as the image in the widget. Parameters ---------- pixamp : QPixmap The QPixmap to use as the image in the widget. """ self._pixmap = pixmap self.update() def scaledContents(self): """Returns whether or not the contents scale with the widget size. """ return self._scaled_contents def setScaledContents(self, scaled): """Set whether the contents scale with the widget size. Parameters ---------- scaled : bool If True, the image will be scaled to fit the widget size, subject to the other sizing constraints in place. If False, the image will not scale and will be clipped as required. """ self._scaled_contents = scaled self.update() def allowUpscaling(self): """Returns whether or not the image can be scaled greater than its natural size. """ return self._allow_upscaling def setAllowUpscaling(self, allow): """Set whether or not to allow the image to be scaled beyond its natural size. Parameters ---------- allow : bool If True, then the image may be scaled larger than its natural if it is scaled to fit. If False, the image will never be scaled larger than its natural size. In either case, the image may be scaled smaller. """ self._allow_upscaling = allow self.update() def preserveAspectRatio(self): """Returns whether or not the aspect ratio of the image is maintained during a resize. """ return self._preserve_aspect_ratio def setPreserveAspectRatio(self, preserve): """Set whether or not to preserve the image aspect ratio. Parameters ---------- preserve : bool If True then the aspect ratio of the image will be preserved if it is scaled to fit. Otherwise, the aspect ratio will be ignored. """ self._preserve_aspect_ratio = preserve self.update() def allowClipping(self): """Returns whether or not the image should be clipped in the view.""" return self._preserve_aspect_ratio def setAllowClipping(self, allow): """Set whether or not the image should be clipped in the view. Parameters ---------- allow : bool If True then clipping will be allowed. Otherwise the minimum size hint will be the image size. """ self._allow_clipping = allow self.update() # ------------------------------------------------------------------------- # '_ImageEditor' class: # ------------------------------------------------------------------------- class _ImageEditor(Editor): """Traits UI 'display only' image editor.""" def init(self, parent): """Finishes initializing the editor by creating the underlying toolkit widget. """ image = self.factory.image if image is None: image = self.value self.control = QImageView() if image is not None: self.control.setPixmap(convert_bitmap(image)) else: self.control.setPixmap(None) self.control.setScaledContents(self.factory.scale) self.control.setAllowUpscaling(self.factory.allow_upscaling) self.control.setPreserveAspectRatio(self.factory.preserve_aspect_ratio) self.control.setAllowClipping(self.factory.allow_clipping) self.set_tooltip() def update_editor(self): """Updates the editor when the object trait changes externally to the editor. """ if self.factory.image is None: value = self.value if value is not None: self.control.setPixmap(convert_bitmap(value)) else: self.control.setPixmap(None) self.control.setScaledContents(self.factory.scale) self.control.setAllowUpscaling(self.factory.allow_upscaling) self.control.setPreserveAspectRatio(self.factory.preserve_aspect_ratio) self.control.setAllowClipping(self.factory.allow_clipping) def dispose(self): if self.control is not None: self.control.setPixmap(None) super().dispose() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/qt/image_enum_editor.py0000644000175100001730000002563500000000000022516 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Defines the various image enumeration editors for the PyQt user interface toolkit. """ from pyface.qt import QtCore, QtGui, is_qt4 from .editor import Editor from .enum_editor import BaseEditor as BaseEnumEditor from .enum_editor import SimpleEditor as SimpleEnumEditor from .enum_editor import RadioEditor as CustomEnumEditor from .helper import pixmap_cache class BaseEditor(object): """The base class for the different styles of ImageEnumEditor.""" def get_pixmap(self, name): """Get a pixmap representing a possible object traits value.""" if name is None: return None factory = self.factory name = "".join((factory.prefix, name, factory.suffix)) return pixmap_cache(name, factory._image_path) class ReadonlyEditor(BaseEditor, BaseEnumEditor): """Read-only style of image enumeration editor, which displays a single static image representing the object trait's value. """ def init(self, parent): """Finishes initializing the editor by creating the underlying toolkit widget. """ super().init(parent) self.control = QtGui.QLabel() self.control.setPixmap(self.get_pixmap(self.str_value)) self.set_tooltip() def update_editor(self): """Updates the editor when the object trait changes externally to the editor. """ self.control.setPixmap(self.get_pixmap(self.str_value)) def rebuild_editor(self): """Rebuilds the contents of the editor whenever the original factory object's **values** trait changes. """ pass class SimpleEditor(BaseEditor, SimpleEnumEditor): """Simple style of image enumeration editor, which displays a combo box.""" def create_combo_box(self): """Returns the QComboBox used for the editor control.""" control = ImageEnumComboBox(self) control.setSizePolicy( QtGui.QSizePolicy.Policy.Maximum, QtGui.QSizePolicy.Policy.Maximum ) return control def dispose(self): self.control._dispose() super().dispose() def update_editor(self): """Updates the editor when the object trait changes externally to the editor. """ if self._no_enum_update == 0: self._no_enum_update += 1 try: index = self.names.index(self.inverse_mapping[self.value]) except: self.control.setCurrentIndex(-1) else: cols = self.factory.cols rows = (len(self.names) + cols - 1) / cols row, col = index // cols, index % cols self.control.setModelColumn(col) self.control.setCurrentIndex(row) self._no_enum_update -= 1 def rebuild_editor(self): """Rebuilds the contents of the editor whenever the original factory object's **values** trait changes. """ # unlike other EnumEditor subclasses, this doesn't ever need rebuilding pass # Trait change handlers -------------------------------------------------- def _update_values_and_rebuild_editor(self, event): """Handles the underlying object model's enumeration set or factory's values being changed. """ self.control.model().beginResetModel() try: super()._update_values_and_rebuild_editor(event) finally: # this changes the model, and updates the items self.control.model().endResetModel() class CustomEditor(BaseEditor, CustomEnumEditor): """Simple style of image enumeration editor, which displays a combo box.""" #: Is the button layout row-major or column-major? This value overrides the #: default. row_major = True def create_button(self, name): """Returns the QAbstractButton used for the radio button.""" button = QtGui.QToolButton() button.setAutoExclusive(True) button.setCheckable(True) pixmap = self.get_pixmap(name) button.setIcon(QtGui.QIcon(pixmap)) button.setIconSize(pixmap.size()) return button # ------------------------------------------------------------------------- # Custom Qt objects used in the SimpleEditor: # ------------------------------------------------------------------------- class ImageEnumComboBox(QtGui.QComboBox): """A combo box which displays images instead of text.""" def __init__(self, editor, parent=None): """Reimplemented to store the editor and set a delegate for drawing the items in the popup menu. If there is more than one column, use a TableView instead of ListView for the popup. """ QtGui.QComboBox.__init__(self, parent) self._editor = editor model = ImageEnumModel(editor, self) self.setModel(model) delegate = ImageEnumItemDelegate(editor, self) if editor.factory.cols > 1: view = ImageEnumTablePopupView(self) view.setItemDelegate(delegate) self.setView(view) # Unless we force it, the popup for a combo box will not be wider # than the box itself, so we set a high minimum width. width = 0 for col in range(self._editor.factory.cols): width += view.sizeHintForColumn(col) view.setMinimumWidth(width) else: self.setItemDelegate(delegate) def _dispose(self): """Dispose objects on this widget. To be called by editors.""" # Replace the model with the standard one. # After the editor has disposed itself, the widget may not have been # garbage collected and the model still reacts to events fired # afterwards (e.g. rowCount will be called) and runs into exceptions. # QComboBox requires that the model must not be None. # QStandardItemModel is the default model type when a QComboxBox is # created. self.setModel(QtGui.QStandardItemModel()) def paintEvent(self, event): """Reimplemented to draw the ComboBox frame and paint the image centered in it. """ painter = QtGui.QStylePainter(self) painter.setPen(self.palette().color(QtGui.QPalette.ColorRole.Text)) option = QtGui.QStyleOptionComboBox() self.initStyleOption(option) painter.drawComplexControl(QtGui.QStyle.ComplexControl.CC_ComboBox, option) editor = self._editor pixmap = editor.get_pixmap(editor.inverse_mapping[editor.value]) arrow = self.style().subControlRect( QtGui.QStyle.ComplexControl.CC_ComboBox, option, QtGui.QStyle.SC_ComboBoxArrow, None, ) option.rect.setWidth(int(option.rect.width() - arrow.width())) target = QtGui.QStyle.alignedRect( QtCore.Qt.LayoutDirection.LeftToRight, QtCore.Qt.AlignmentFlag.AlignCenter, pixmap.size(), option.rect, ) painter.drawPixmap(target, pixmap) def sizeHint(self): """Reimplemented to set a size hint based on the size of the larget image. """ size = QtCore.QSize() for name in self._editor.names: size = size.expandedTo(self._editor.get_pixmap(name).size()) option = QtGui.QStyleOptionComboBox() self.initStyleOption(option) size = self.style().sizeFromContents( QtGui.QStyle.ContentsType.CT_ComboBox, option, size, self ) return size class ImageEnumTablePopupView(QtGui.QTableView): def __init__(self, parent): """Configure the appearence of the table view.""" QtGui.QTableView.__init__(self, parent) hheader = self.horizontalHeader() if is_qt4: hheader.setResizeMode(QtGui.QHeaderView.ResizeMode.ResizeToContents) else: hheader.setSectionResizeMode(QtGui.QHeaderView.ResizeMode.ResizeToContents) hheader.hide() vheader = self.verticalHeader() if is_qt4: vheader.setResizeMode(QtGui.QHeaderView.ResizeMode.ResizeToContents) else: vheader.setSectionResizeMode(QtGui.QHeaderView.ResizeMode.ResizeToContents) vheader.hide() self.setShowGrid(False) class ImageEnumItemDelegate(QtGui.QStyledItemDelegate): """An item delegate which draws only images.""" def __init__(self, editor, parent): """Reimplemented to store the editor.""" QtGui.QStyledItemDelegate.__init__(self, parent) self._editor = editor def displayText(self, value, locale): """Reimplemented to display nothing.""" return "" def paint(self, painter, option, mi): """Reimplemented to draw images.""" # Delegate to our superclass to draw the background QtGui.QStyledItemDelegate.paint(self, painter, option, mi) # Now draw the pixmap name = mi.data(QtCore.Qt.ItemDataRole.DisplayRole) pixmap = self._get_pixmap(name) if pixmap is not None: target = QtGui.QStyle.alignedRect( QtCore.Qt.LayoutDirection.LeftToRight, QtCore.Qt.AlignmentFlag.AlignCenter, pixmap.size(), option.rect, ) painter.drawPixmap(target, pixmap) def sizeHint(self, option, mi): """Reimplemented to define a size hint based on the size of the pixmap.""" name = mi.data(QtCore.Qt.ItemDataRole.DisplayRole) pixmap = self._get_pixmap(name) if pixmap is None: return QtCore.QSize() return pixmap.size() def _get_pixmap(self, name): return self._editor.get_pixmap(name) class ImageEnumModel(QtCore.QAbstractTableModel): """A table model for use with the 'simple' style ImageEnumEditor.""" def __init__(self, editor, parent): """Reimplemented to store the editor.""" super().__init__(parent) self._editor = editor def rowCount(self, mi): """Reimplemented to return the number of rows.""" cols = self._editor.factory.cols result = (len(self._editor.names) + cols - 1) // cols return result def columnCount(self, mi): """Reimplemented to return the number of columns.""" return self._editor.factory.cols def data(self, mi, role): """Reimplemented to return the data.""" if role == QtCore.Qt.ItemDataRole.DisplayRole: index = mi.row() * self._editor.factory.cols + mi.column() if index < len(self._editor.names): return self._editor.names[index] return None ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1684768028.0958068 traitsui-8.0.0/traitsui/qt/images/0000755000175100001730000000000000000000000017722 5ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/qt/images/closetab.png0000644000175100001730000000056700000000000022234 0ustar00runnerdocker00000000000000PNG  IHDR(-SPLTE88,.:x',.~*/023F,M `c8z6H q#s$s%t%@@m Cp!kᩄtRNSWIDATx^mG@xͽ;~@I IŠ5,Y)ϦYX.t)!j/9wD1Ɔ!bSáx<Đ|1Kl{)3*JFjݼMoSAHMڻŊ3jڗWʋZTi#GG57o|>ITD8."ƹw-Ρ'[ۛJr+i|}'m0:6S|7s?G^_\b԰g#Oooti·h + LU$' e$$,U*+ C3_-|Zh2xoy Y8,%3bM'LYuaHoYm"9s0!$:ߏ~PJ]7X+E94jUC9 VVCQT]][?-Q;/0IENDB`././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/qt/images/list_editor.png0000755000175100001730000000062400000000000022756 0ustar00runnerdocker00000000000000PNG  IHDRabKGD pHYs  tIME. '!IDAT8˥1nA EN"$H9 hؒ pJNBz ђL @J&\9v_72Mp:=x<>UQENuF, cp8DAD! "4M*"n[txeYFj8vc ^^AA{{srz,˘f=$sJ[k=֢x>v\.FnoXxzIENDB`././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/qt/images/next.png0000644000175100001730000000201600000000000021405 0ustar00runnerdocker00000000000000PNG  IHDRש(PLTEbUdd\]dou.dQ6ˋȕ=)@dS9=(mV[7;'v.r.R͋Έ߶bZh [rXv/҉רbԄכd֕ddˇՉhf әc؊{5衐`Zߠը卌]TThzOW9b c b Z|:k{R  5#}7[zQV;(~7yOB,?~R2!y-ZsL9%K!" JUJ4L: SN% mgLS@8/"qSE<0yY7B/2.=tRNSD W&#T= _q\(!me)pFJRԃBQRX}Ո1tt"f|:Y* !IDAT(c` 0aW"*&#))dm΍)! +V]M\NUM]Cy[;aGCM:2fF[N24&l .Z| mlal}3:&NrrFp6mfK˜=<$[bkZ}|[o  cAX?!N$ɹ I)i32=ŗ_P(Z,b2r&F@bČںzL@8XYF9 3qi:1FCSIENDB`././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/qt/images/previous.png0000644000175100001730000000160200000000000022303 0ustar00runnerdocker00000000000000PNG  IHDRw=gAMAOX2tEXtSoftwareAdobe ImageReadyqe<IDATH͖kHaǏZ4 n&vS, i%`rf 2B@$dm]RDPT%NM7]1ܲ)ά7ޝFt5>ss "MH%`7Y<nxDC3&%@LI<<ҽ*)$XJ8,8x<^1:U/d "f$C^9(a)vB*I`Yy`cu/&sB| e; {1mPOa ⳛz%1.Nt'9ɒxmѐ@8 ~B+)ԕDcdh>g2(L!uhuoJѴl/fP%B9Lc_= 9U."B}Mc^PvmJyYR{| z0S4S % *ah]Q^; 1]9]%Ai3 UxiZqB̉\1P2XE֑ulF|~7b>Dm*~K`O<J"'4jO sL> <ā &YBK,j!=Öfhj:Ni= 0: choice.setCurrentIndex(name) else: # Otherwise, current value is no longer valid, set combobox empty try: self.value = None choice.setCurrentIndex(-1) except: pass def item_for(self, object): """Returns the InstanceChoiceItem for a specified object.""" for item in self.items: if item.is_compatible(object): return item return None def view_for(self, object, item): """Returns the view to use for a specified object.""" view = "" if item is not None: view = item.get_view() if view == "": view = self.view return self.ui.handler.trait_view_for( self.ui.info, view, object, self.object_name, self.name ) def update_object(self, index): """Handles the user selecting a new value from the combo box.""" item = self.items[index] id_item = id(item) object = self._object_cache.get(id_item) if object is None: object = item.get_object() if (not self.factory.editable) and item.is_factory: view = self.view_for(object, self.item_for(object)) view.ui(object, self.control, "modal") if self.factory.cachable: self._object_cache[id_item] = object self.value = object self.resynch_editor() def update_editor(self): """Updates the editor when the object trait changes externally to the editor. """ # Synchronize the editor contents: self.resynch_editor() # Update the selector (if any): choice = self._choice item = self.item_for(self.value) if choice is not None: if item is not None: name = item.get_name(self.value) if self._object_cache is not None: idx = choice.findText(name) if idx < 0: idx = choice.count() choice.addItem(name) choice.setCurrentIndex(idx) else: choice.setText(name) else: # choice can also be a QLineEdit in which case we just leave # text as empty if isinstance(choice, QtGui.QComboBox): choice.setCurrentIndex(-1) def resynch_editor(self): """Resynchronizes the contents of the editor when the object trait changes externally to the editor. """ panel = self._panel if panel is not None: # Dispose of the previous contents of the panel: layout = panel.layout() if layout is None: layout = QtGui.QVBoxLayout(panel) layout.setContentsMargins(0, 0, 0, 0) elif self._ui is not None: self._ui.dispose() self._ui = None else: child = layout.takeAt(0) while child is not None: child = layout.takeAt(0) del child # Create the new content for the panel: stretch = 0 value = self.value if not isinstance(value, HasTraits): str_value = "" if value is not None: str_value = self.str_value control = QtGui.QLabel(str_value) else: view = self.view_for(value, self.item_for(value)) context = value.trait_context() handler = None if isinstance(value, Handler): handler = value context.setdefault("context", self.object) context.setdefault("context_handler", self.ui.handler) self._ui = ui = view.ui( context, panel, "subpanel", value.trait_view_elements(), handler, self.factory.id, ) control = ui.control self.scrollable = ui._scrollable ui.parent = self.ui if view.resizable or view.scrollable or ui._scrollable: stretch = 1 # FIXME: Handle stretch. layout.addWidget(control) def dispose(self): """Disposes of the contents of an editor.""" # Make sure we aren't hanging on to any object refs: self._object_cache = None if self._ui is not None: self._ui.dispose() if self._choice is not None: # _choice can also be a QLineEdit in which case we never set up # this observer. if isinstance(self._choice, QtGui.QComboBox): if self._object is not None: self._object.observe( self.rebuild_items, self._name + ".items", remove=True, dispatch="ui", ) self.factory.observe( self.rebuild_items, "values.items", remove=True, dispatch="ui", ) super().dispose() def error(self, excp): """Handles an error that occurs while setting the object's trait value.""" pass def get_error_control(self): """Returns the editor's control for indicating error status.""" return self._choice or self.control # -- UI preference save/restore interface --------------------------------- def restore_prefs(self, prefs): """Restores any saved user preference information associated with the editor. """ ui = self._ui if (ui is not None) and (prefs.get("id") == ui.id): ui.set_prefs(prefs.get("prefs")) def save_prefs(self): """Returns any user preference information associated with the editor.""" ui = self._ui if (ui is not None) and (ui.id != ""): return {"id": ui.id, "prefs": ui.get_prefs()} return None # -- Traits event handlers ------------------------------------------------ def _view_changed(self, view): self.resynch_editor() class SimpleEditor(CustomEditor): """Simple style of editor for instances, which displays a button. Clicking the button displays a dialog box in which the instance can be edited. """ #: The ui instance for the currently open editor dialog _dialog_ui = Instance("traitsui.ui.UI") #: Class constants: orientation = QtGui.QBoxLayout.Direction.LeftToRight extra = 2 def create_editor(self, parent, layout): """Creates the editor control (a button).""" self._button = QtGui.QPushButton() self._button.setAutoDefault(False) layout.addWidget(self._button) self._button.clicked.connect(self.edit_instance) # Make sure the editor is properly disposed if parent UI is closed self._button.destroyed.connect(self._parent_closed) def edit_instance(self): """Edit the contents of the object trait when the user clicks the button. """ # Create the user interface: factory = self.factory view = self.ui.handler.trait_view_for( self.ui.info, factory.view, self.value, self.object_name, self.name ) self._dialog_ui = self.value.edit_traits( view, kind=factory.kind, id=factory.id ) # Check to see if the view was 'modal', in which case it will already # have been closed (i.e. is None) by the time we get control back: if self._dialog_ui.control is not None: # Position the window on the display: position_window(self._dialog_ui.control) # Chain our undo history to the new user interface if it does not # have its own: if self._dialog_ui.history is None: self._dialog_ui.history = self.ui.history else: self._dialog_ui = None def resynch_editor(self): """Resynchronizes the contents of the editor when the object trait changes externally to the editor. """ button = self._button if button is not None: label = self.factory.label if label == "": label = user_name_for(self.name) button.setText(label) button.setEnabled(isinstance(self.value, HasTraits)) def _parent_closed(self): if self._dialog_ui is not None: if self._dialog_ui.control is not None: self._dialog_ui.control.close() self._dialog_ui.dispose() self._dialog_ui = None ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/qt/key_binding_editor.py0000644000175100001730000001146100000000000022662 0ustar00runnerdocker00000000000000# (C) Copyright 2008-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # ------------------------------------------------------------------------------ # Copyright (c) 2007, Riverbank Computing Limited # All rights reserved. # # This software is provided without warranty under the terms of the BSD license. # However, when used with the GPL version of PyQt the additional terms # described in the PyQt GPL exception also apply # # Author: Riverbank Computing Limited # ------------------------------------------------------------------------------ """ Defines the key binding editor for use with the KeyBinding class. This is a specialized editor used to associate a particular key with a control (i.e., the key binding editor). """ from pyface.qt import QtCore, QtGui from pyface.api import YES, confirm from traits.api import Bool, Event from .editor import Editor from .key_event_to_name import key_event_to_name class KeyBindingEditor(Editor): """An editor for modifying bindings of keys to controls.""" # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- #: Does the editor's control have focus currently? has_focus = Bool(False) #: Keyboard event key = Event() #: Clear field event clear = Event() def init(self, parent): """Finishes initializing the editor by creating the underlying toolkit widget. """ self.control = KeyBindingCtrl(self) def update_editor(self): """Updates the editor when the object trait changes externally to the editor. """ self.control.setText(self.value) def _key_changed(self, event): """Handles a keyboard event.""" binding = self.object key_name = key_event_to_name(event) cur_binding = self.ui.parent.handler.key_binding_for(binding, key_name) if cur_binding is not None: result = confirm( parent=self.control, message=( f"{key_name!r} has already been assigned to " f"'{cur_binding.description}'.\n" "Do you wish to continue?" ), title="Duplicate Key Definition", ) if result != YES: return self.value = key_name # Need to manually update editor because the update to the value # won't get transferred to toolkit object due to loopback protection. self.update_editor() def _clear_changed(self): """Handles a clear field event.""" self.value = "" # Need to manually update editor because the update to the value # won't get transferred to toolkit object due to loopback protection. self.update_editor() class KeyBindingCtrl(QtGui.QLineEdit): """PyQt control for editing key bindings.""" def __init__(self, editor, parent=None): super().__init__(parent) self.setFocusPolicy(QtCore.Qt.FocusPolicy.StrongFocus) self.setMinimumWidth(160) self.setMaximumWidth(160) self.setReadOnly(True) # Save the reference to the controlling editor object: self.editor = editor # Indicate we don't have the focus right now: editor.has_focus = False def keyPressEvent(self, event): """Handle keyboard keys being pressed.""" # Ignore presses of the control and shift keys. if event.key() not in (QtCore.Qt.Key.Key_Control, QtCore.Qt.Key.Key_Shift): self.editor.key = event def paintEvent(self, event): """Updates the screen.""" super().paintEvent(event) if self.editor.has_focus: w = self.width() h = self.height() p = QtGui.QPainter(self) p.setRenderHint(QtGui.QPainter.RenderHint.Antialiasing, True) pen = QtGui.QPen(QtGui.QColor("tomato")) pen.setWidth(2) p.setPen(pen) p.drawRect(1, 1, w - 2, h - 2) p.end() def focusInEvent(self, event): """Handles getting the focus.""" self.editor.has_focus = True self.update() def focusOutEvent(self, event): """Handles losing the focus.""" self.editor.has_focus = False self.update() def mouseDoubleClickEvent(self, event): """Handles the user double clicking the control to clear its contents.""" self.editor.clear = True ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/qt/key_event_to_name.py0000644000175100001730000001331600000000000022526 0ustar00runnerdocker00000000000000# (C) Copyright 2008-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # ------------------------------------------------------------------------------ # Copyright (c) 2007, Riverbank Computing Limited # All rights reserved. # # This software is provided without warranty under the terms of the BSD license. # However, when used with the GPL version of PyQt the additional terms # described in the PyQt GPL exception also apply # # Author: Riverbank Computing Limited # ------------------------------------------------------------------------------ """ Converts a QKeyEvent to a standardized "name". """ from pyface.qt import QtCore # Mapping from PyQt keypad key names to Pyface key names. keypad_map = { QtCore.Qt.Key.Key_Enter: "Enter", QtCore.Qt.Key.Key_0: "Numpad 0", QtCore.Qt.Key.Key_1: "Numpad 1", QtCore.Qt.Key.Key_2: "Numpad 2", QtCore.Qt.Key.Key_3: "Numpad 3", QtCore.Qt.Key.Key_4: "Numpad 4", QtCore.Qt.Key.Key_5: "Numpad 5", QtCore.Qt.Key.Key_6: "Numpad 6", QtCore.Qt.Key.Key_7: "Numpad 7", QtCore.Qt.Key.Key_8: "Numpad 8", QtCore.Qt.Key.Key_9: "Numpad 9", QtCore.Qt.Key.Key_Asterisk: "Multiply", QtCore.Qt.Key.Key_Plus: "Add", QtCore.Qt.Key.Key_Comma: "Separator", QtCore.Qt.Key.Key_Minus: "Subtract", QtCore.Qt.Key.Key_Period: "Decimal", QtCore.Qt.Key.Key_Slash: "Divide", } # Mapping from PyQt non-keypad key names to Pyface key names. key_map = { QtCore.Qt.Key.Key_0: "0", QtCore.Qt.Key.Key_1: "1", QtCore.Qt.Key.Key_2: "2", QtCore.Qt.Key.Key_3: "3", QtCore.Qt.Key.Key_4: "4", QtCore.Qt.Key.Key_5: "5", QtCore.Qt.Key.Key_6: "6", QtCore.Qt.Key.Key_7: "7", QtCore.Qt.Key.Key_8: "8", QtCore.Qt.Key.Key_9: "9", QtCore.Qt.Key.Key_A: "A", QtCore.Qt.Key.Key_B: "B", QtCore.Qt.Key.Key_C: "C", QtCore.Qt.Key.Key_D: "D", QtCore.Qt.Key.Key_E: "E", QtCore.Qt.Key.Key_F: "F", QtCore.Qt.Key.Key_G: "G", QtCore.Qt.Key.Key_H: "H", QtCore.Qt.Key.Key_I: "I", QtCore.Qt.Key.Key_J: "J", QtCore.Qt.Key.Key_K: "K", QtCore.Qt.Key.Key_L: "L", QtCore.Qt.Key.Key_M: "M", QtCore.Qt.Key.Key_N: "N", QtCore.Qt.Key.Key_O: "O", QtCore.Qt.Key.Key_P: "P", QtCore.Qt.Key.Key_Q: "Q", QtCore.Qt.Key.Key_R: "R", QtCore.Qt.Key.Key_S: "S", QtCore.Qt.Key.Key_T: "T", QtCore.Qt.Key.Key_U: "U", QtCore.Qt.Key.Key_V: "V", QtCore.Qt.Key.Key_W: "W", QtCore.Qt.Key.Key_X: "X", QtCore.Qt.Key.Key_Y: "Y", QtCore.Qt.Key.Key_Z: "Z", QtCore.Qt.Key.Key_Space: "Space", QtCore.Qt.Key.Key_Backspace: "Backspace", QtCore.Qt.Key.Key_Tab: "Tab", QtCore.Qt.Key.Key_Enter: "Enter", QtCore.Qt.Key.Key_Return: "Return", QtCore.Qt.Key.Key_Escape: "Esc", QtCore.Qt.Key.Key_Delete: "Delete", QtCore.Qt.Key.Key_Cancel: "Cancel", QtCore.Qt.Key.Key_Clear: "Clear", QtCore.Qt.Key.Key_Shift: "Shift", QtCore.Qt.Key.Key_Menu: "Menu", QtCore.Qt.Key.Key_Pause: "Pause", QtCore.Qt.Key.Key_PageUp: "Page Up", QtCore.Qt.Key.Key_PageDown: "Page Down", QtCore.Qt.Key.Key_End: "End", QtCore.Qt.Key.Key_Home: "Home", QtCore.Qt.Key.Key_Left: "Left", QtCore.Qt.Key.Key_Up: "Up", QtCore.Qt.Key.Key_Right: "Right", QtCore.Qt.Key.Key_Down: "Down", QtCore.Qt.Key.Key_Select: "Select", QtCore.Qt.Key.Key_Print: "Print", QtCore.Qt.Key.Key_Execute: "Execute", QtCore.Qt.Key.Key_Insert: "Insert", QtCore.Qt.Key.Key_Help: "Help", QtCore.Qt.Key.Key_F1: "F1", QtCore.Qt.Key.Key_F2: "F2", QtCore.Qt.Key.Key_F3: "F3", QtCore.Qt.Key.Key_F4: "F4", QtCore.Qt.Key.Key_F5: "F5", QtCore.Qt.Key.Key_F6: "F6", QtCore.Qt.Key.Key_F7: "F7", QtCore.Qt.Key.Key_F8: "F8", QtCore.Qt.Key.Key_F9: "F9", QtCore.Qt.Key.Key_F10: "F10", QtCore.Qt.Key.Key_F11: "F11", QtCore.Qt.Key.Key_F12: "F12", QtCore.Qt.Key.Key_F13: "F13", QtCore.Qt.Key.Key_F14: "F14", QtCore.Qt.Key.Key_F15: "F15", QtCore.Qt.Key.Key_F16: "F16", QtCore.Qt.Key.Key_F17: "F17", QtCore.Qt.Key.Key_F18: "F18", QtCore.Qt.Key.Key_F19: "F19", QtCore.Qt.Key.Key_F20: "F20", QtCore.Qt.Key.Key_F21: "F21", QtCore.Qt.Key.Key_F22: "F22", QtCore.Qt.Key.Key_F23: "F23", QtCore.Qt.Key.Key_F24: "F24", QtCore.Qt.Key.Key_NumLock: "Num Lock", QtCore.Qt.Key.Key_ScrollLock: "Scroll Lock", } # ------------------------------------------------------------------------- # Converts a keystroke event into a corresponding key name: # ------------------------------------------------------------------------- def key_event_to_name(event): """Converts a keystroke event into a corresponding key name.""" key_code = event.key() modifiers = event.modifiers() if modifiers & QtCore.Qt.KeyboardModifier.KeypadModifier: key = keypad_map.get(key_code) else: key = None if key is None: key = key_map.get(key_code, "Unknown-Key") name = "" if modifiers & QtCore.Qt.KeyboardModifier.ControlModifier: name += "Ctrl" if modifiers & QtCore.Qt.KeyboardModifier.AltModifier: name += "-Alt" if name else "Alt" if modifiers & QtCore.Qt.KeyboardModifier.MetaModifier: name += "-Meta" if name else "Meta" if modifiers & QtCore.Qt.KeyboardModifier.ShiftModifier and ( (name != "") or (len(key) > 1) ): name += "-Shift" if name else "Shift" if key: if name: name += "-" name += key return name ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/qt/list_editor.py0000644000175100001730000006617300000000000021365 0ustar00runnerdocker00000000000000# (C) Copyright 2008-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # ------------------------------------------------------------------------------ # Copyright (c) 2007, Riverbank Computing Limited # All rights reserved. # # This software is provided without warranty under the terms of the BSD license. # However, when used with the GPL version of PyQt the additional terms described # in the PyQt GPL exception also apply # # Author: Riverbank Computing Limited # ------------------------------------------------------------------------------ """ Defines the various list editors for the PyQt user interface toolkit. """ from pyface.qt import QtCore, QtGui, is_pyside from pyface.api import ImageResource from traits.api import ( Any, Bool, Callable, Dict, Instance, List, Str, TraitError, ) from traits.trait_base import user_name_for, xgetattr from traitsui.editors.list_editor import ListItemProxy from .editor import Editor from .helper import IconButton from .menu import MakeMenu class SimpleEditor(Editor): """Simple style of editor for lists, which displays a list box with only one item visible at a time. A icon next to the list box displays a menu of operations on the list. """ # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- #: The kind of editor to create for each list item kind = Str() #: Is the list of items being edited mutable? mutable = Bool(True) #: Is the editor scrollable? scrollable = Bool(True) #: Signal mapper allowing to identify which icon button requested a context #: menu mapper = Instance(QtCore.QSignalMapper) buttons = List([]) _list_pane = Instance(QtGui.QWidget) # ------------------------------------------------------------------------- # Class constants: # ------------------------------------------------------------------------- #: Whether the list is displayed in a single row single_row = True # ------------------------------------------------------------------------- # Normal list item menu: # ------------------------------------------------------------------------- #: Menu for modifying the list list_menu = """ Add &Before [_menu_before]: self.add_before() Add &After [_menu_after]: self.add_after() --- &Delete [_menu_delete]: self.delete_item() --- Move &Up [_menu_up]: self.move_up() Move &Down [_menu_down]: self.move_down() Move to &Top [_menu_top]: self.move_top() Move to &Bottom [_menu_bottom]: self.move_bottom() """ # ------------------------------------------------------------------------- # Empty list item menu: # ------------------------------------------------------------------------- empty_list_menu = """ Add: self.add_empty() """ def init(self, parent): """Finishes initializing the editor by creating the underlying toolkit widget. """ # Initialize the trait handler to use: trait_handler = self.factory.trait_handler if trait_handler is None: trait_handler = self.object.base_trait(self.name).handler self._trait_handler = trait_handler if self.scrollable: # Create a scrolled window to hold all of the list item controls: self.control = QtGui.QScrollArea() self.control.setFrameShape(QtGui.QFrame.Shape.NoFrame) self.control.setWidgetResizable(True) self._list_pane = QtGui.QWidget() else: self.control = QtGui.QWidget() self._list_pane = self.control self._list_pane.setSizePolicy( QtGui.QSizePolicy.Policy.Expanding, QtGui.QSizePolicy.Policy.Expanding ) # Create a mapper to identify which icon button requested a contextmenu self.mapper = QtCore.QSignalMapper(self.control) # Create a widget with a grid layout as the container. layout = QtGui.QGridLayout(self._list_pane) layout.setAlignment(QtCore.Qt.AlignmentFlag.AlignLeft | QtCore.Qt.AlignmentFlag.AlignTop) layout.setContentsMargins(0, 0, 0, 0) layout.setSpacing(0) # Remember the editor to use for each individual list item: editor = self.factory.editor if editor is None: editor = trait_handler.item_trait.get_editor() self._editor = getattr(editor, self.kind) # Set up the additional 'list items changed' event handler needed for # a list based trait. Note that we want to fire the update_editor_item # only when the items in the list change and not when intermediate # traits change. Therefore, replace "." by ":" in the extended_name # when setting up the listener. extended_name = self.extended_name.replace(".", ":") self.context_object.on_trait_change( self.update_editor_item, extended_name + "_items?", dispatch="ui" ) self.set_tooltip() def dispose(self): """Disposes of the contents of an editor.""" self._dispose_items() extended_name = self.extended_name.replace(".", ":") self.context_object.on_trait_change( self.update_editor_item, extended_name + "_items?", remove=True ) super().dispose() def update_editor(self): """Updates the editor when the object trait changes externally to the editor. """ self.mapper = QtCore.QSignalMapper(self.control) # Disconnect the editor from any control about to be destroyed: self._dispose_items() list_pane = self._list_pane layout = list_pane.layout() # Create all of the list item trait editors: trait_handler = self._trait_handler resizable = ( trait_handler.minlen != trait_handler.maxlen ) and self.mutable item_trait = trait_handler.item_trait is_fake = resizable and (len(self.value) == 0) if is_fake: self.empty_list() else: self.buttons = [] # Asking the mapper to send the sender to the callback method if is_pyside and QtCore.__version_info__ >= (5, 15): self.mapper.mappedInt.connect(self.popup_menu) else: self.mapper.mapped.connect(self.popup_menu) editor = self._editor for index, value in enumerate(self.value): row, column = divmod(index, self.factory.columns) # Account for the fact that we have number of # pairs column = column * 2 if resizable: # Connecting the new button to the mapper control = IconButton("list_editor.png", self.mapper.map) self.buttons.append(control) # Setting the mapping and asking it to send the index of the # sender to the callback method. Unfortunately just sending # the control does not work for PyQt (tested on 4.11) self.mapper.setMapping(control, index) layout.addWidget(control, row, column + 1) proxy = ListItemProxy( self.object, self.name, index, item_trait, value ) if resizable: control.proxy = proxy peditor = editor( self.ui, proxy, "value", self.description, list_pane ).trait_set(object_name="") peditor.prepare(list_pane) pcontrol = peditor.control pcontrol.proxy = proxy if isinstance(pcontrol, QtGui.QWidget): layout.addWidget(pcontrol, row, column) else: layout.addLayout(pcontrol, row, column) # QScrollArea can have problems if the widget being scrolled is set too # early (ie. before it contains something). if self.scrollable and self.control.widget() is None: self.control.setWidget(list_pane) def update_editor_item(self, event): """Updates the editor when an item in the object trait changes externally to the editor. """ # If this is not a simple, single item update, rebuild entire editor: if (len(event.removed) != 1) or (len(event.added) != 1): self.update_editor() return # Otherwise, find the proxy for this index and update it with the # changed value: for control in self._list_pane.children(): if isinstance(control, QtGui.QLayout): continue proxy = control.proxy if proxy.index == event.index: proxy.value = event.added[0] break def empty_list(self): """Creates an empty list entry (so the user can add a new item).""" # Connecting the new button to the mapper control = IconButton("list_editor.png", self.mapper.map) # Setting the mapping and asking it to send the index of the sender to # callback method. Unfortunately just sending the control does not # work for PyQt (tested on 4.11) self.mapper.setMapping(control, 0) if is_pyside and QtCore.__version_info__ >= (5, 15): self.mapper.mappedInt.connect(self.popup_empty_menu) else: self.mapper.mapped.connect(self.popup_empty_menu) control.is_empty = True self._cur_control = control self.buttons = [control] proxy = ListItemProxy(self.object, self.name, -1, None, None) pcontrol = QtGui.QLabel(" (Empty List)") pcontrol.proxy = control.proxy = proxy layout = self._list_pane.layout() layout.addWidget(control, 0, 1) layout.addWidget(pcontrol, 0, 0) def get_info(self): """Returns the associated object list and current item index.""" proxy = self._cur_control.proxy return (proxy.list, proxy.index) def popup_empty_menu(self, index): """Displays the empty list editor popup menu.""" self._cur_control = control = self.buttons[index] menu = MakeMenu(self.empty_list_menu, self, True, control).menu menu.exec_(control.mapToGlobal(QtCore.QPoint(4, 24))) def popup_menu(self, index): """Displays the list editor popup menu.""" self._cur_control = sender = self.buttons[index] proxy = sender.proxy menu = MakeMenu(self.list_menu, self, True, sender).menu len_list = len(proxy.list) not_full = len_list < self._trait_handler.maxlen self._menu_before.enabled(not_full) self._menu_after.enabled(not_full) self._menu_delete.enabled(len_list > self._trait_handler.minlen) self._menu_up.enabled(index > 0) self._menu_top.enabled(index > 0) self._menu_down.enabled(index < (len_list - 1)) self._menu_bottom.enabled(index < (len_list - 1)) menu.exec_(sender.mapToGlobal(QtCore.QPoint(4, 24))) def add_item(self, offset): """Adds a new value at the specified list index.""" list, index = self.get_info() index += offset item_trait = self._trait_handler.item_trait if self.factory.item_factory: value = self.factory.item_factory( *self.factory.item_factory_args, **self.factory.item_factory_kwargs, ) else: value = item_trait.default_value_for(self.object, self.name) try: self.value = list[:index] + [value] + list[index:] # if the default new item is invalid, we just don't add it to the list. # traits will still give an error message, but we don't want to crash except TraitError: from traitsui.api import raise_to_debug raise_to_debug() self.update_editor() def add_before(self): """Inserts a new item before the current item.""" self.add_item(0) def add_after(self): """Inserts a new item after the current item.""" self.add_item(1) def add_empty(self): """Adds a new item when the list is empty.""" list, index = self.get_info() self.add_item(0) def delete_item(self): """Delete the current item.""" list, index = self.get_info() self.value = list[:index] + list[index + 1 :] self.update_editor() def move_up(self): """Move the current item up one in the list.""" list, index = self.get_info() self.value = ( list[: index - 1] + [list[index], list[index - 1]] + list[index + 1 :] ) self.update_editor() def move_down(self): """Moves the current item down one in the list.""" list, index = self.get_info() self.value = ( list[:index] + [list[index + 1], list[index]] + list[index + 2 :] ) self.update_editor() def move_top(self): """Moves the current item to the top of the list.""" list, index = self.get_info() self.value = [list[index]] + list[:index] + list[index + 1 :] self.update_editor() def move_bottom(self): """Moves the current item to the bottom of the list.""" list, index = self.get_info() self.value = list[:index] + list[index + 1 :] + [list[index]] self.update_editor() # -- Private Methods ------------------------------------------------------ def _dispose_items(self): """Disposes of each current list item.""" layout = self._list_pane.layout() child = layout.takeAt(0) while child is not None: control = child.widget() if control is not None: editor = getattr(control, "_editor", None) if editor is not None: editor.dispose() editor.control = None control.deleteLater() child = layout.takeAt(0) del child # -- Trait initializers ---------------------------------------------------- def _kind_default(self): """Returns a default value for the 'kind' trait.""" return self.factory.style + "_editor" def _mutable_default(self): """Trait handler to set the mutable trait from the factory.""" return self.factory.mutable def _scrollable_default(self): return self.factory.scrollable class CustomEditor(SimpleEditor): """Custom style of editor for lists, which displays the items as a series of text fields. If the list is editable, an icon next to each item displays a menu of operations on the list. """ # ------------------------------------------------------------------------- # Class constants: # ------------------------------------------------------------------------- #: Whether the list is displayed in a single row. This value overrides the #: default. single_row = False class TextEditor(CustomEditor): #: The kind of editor to create for each list item. This value overrides the #: default. kind = "text_editor" class ReadonlyEditor(CustomEditor): #: Is the list of items being edited mutable? This value overrides the #: default. mutable = False class NotebookEditor(Editor): """An editor for lists that displays the list as a "notebook" of tabbed pages. """ #: The "Close Tab" button. close_button = Any() #: Maps tab names to QWidgets representing the tab contents #: TODO: It would be nice to be able to reuse self._pages for this, but #: its keys are not quite what we want. _pagewidgets = Dict() #: Maps names of tabs to their menu QAction instances; used to toggle #: checkboxes _action_dict = Dict() # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- #: Is the notebook editor scrollable? This values overrides the default: scrollable = True #: The currently selected notebook page object: selected = Any() def init(self, parent): """Finishes initializing the editor by creating the underlying toolkit widget. """ self._uis = [] # Create a tab widget to hold each separate object's view: self.control = QtGui.QTabWidget() self.control.currentChanged.connect(self._tab_activated) # minimal dock_style handling if self.factory.dock_style == "tab": self.control.setDocumentMode(True) self.control.tabBar().setDocumentMode(True) elif self.factory.dock_style == "vertical": self.control.setTabPosition(QtGui.QTabWidget.TabPosition.West) # Create the button to close tabs, if necessary: if self.factory.deletable: button = QtGui.QToolButton() button.setAutoRaise(True) button.setToolTip("Remove current tab ") button.setIcon(ImageResource("closetab").create_icon()) self.control.setCornerWidget(button, QtCore.Qt.Corner.TopRightCorner) button.clicked.connect(self.close_current) self.close_button = button if self.factory.show_notebook_menu: # Create the necessary attributes to manage hiding and revealing of # tabs via a context menu self._context_menu = QtGui.QMenu() self.control.customContextMenuRequested.connect( self._context_menu_requested ) self.control.setContextMenuPolicy(QtCore.Qt.ContextMenuPolicy.CustomContextMenu) # Set up the additional 'list items changed' event handler needed for # a list based trait. Note that we want to fire the update_editor_item # only when the items in the list change and not when intermediate # traits change. Therefore, replace "." by ":" in the extended_name # when setting up the listener. extended_name = self.extended_name.replace(".", ":") self.context_object.on_trait_change( self.update_editor_item, extended_name + "_items?", dispatch="ui" ) # Set of selection synchronization: self.sync_value(self.factory.selected, "selected") def update_editor(self): """Updates the editor when the object trait changes externally to the editor. """ # Destroy the views on each current notebook page: self.close_all() # Create a tab page for each object in the trait's value: for object in self.value: ui, view_object, monitoring = self._create_page(object) # Remember the page for later deletion processing: self._uis.append([ui.control, ui, view_object, monitoring]) self._tab_activated(0) if self.selected: self._selected_changed(self.selected) def update_editor_item(self, event): """Handles an update to some subset of the trait's list.""" index = event.index # Delete the page corresponding to each removed item: page_name = self.factory.page_name[1:] for i in event.removed: page, ui, view_object, monitoring = self._uis[index] if monitoring: view_object.on_trait_change( self.update_page_name, page_name, remove=True ) ui.dispose() self.control.removeTab(self.control.indexOf(page)) if self.factory.show_notebook_menu: for name, tmp in self._pagewidgets.items(): if tmp is page: del self._pagewidgets[name] self._context_menu.removeAction(self._action_dict[name]) del self._action_dict[name] del self._uis[index] # Add a page for each added object: first_page = None for object in event.added: ui, view_object, monitoring = self._create_page(object) self._uis[index:index] = [ [ui.control, ui, view_object, monitoring] ] index += 1 if first_page is None: first_page = ui.control if first_page is not None: self.control.setCurrentWidget(first_page) def close_current(self, force=False): """Closes the currently selected tab:""" widget = self.control.currentWidget() for i in range(len(self._uis)): page, ui, _, _ = self._uis[i] if page is widget: if force or ui.handler.close(ui.info, True): del self.value[i] break if self.factory.show_notebook_menu: # Find the name associated with this widget, so we can purge its action # from the menu for name, tmp in self._pagewidgets.items(): if tmp is widget: break else: # Hmm... couldn't find the widget, assume that we don't need to do # anything. return action = self._action_dict[name] self._context_menu.removeAction(action) del self._action_dict[name] del self._pagewidgets[name] return def close_all(self): """Closes all currently open notebook pages.""" page_name = self.factory.page_name[1:] for _, ui, view_object, monitoring in self._uis: if monitoring: view_object.on_trait_change( self.update_page_name, page_name, remove=True ) ui.dispose() # Reset the list of ui's and dictionary of page name counts: self._uis = [] self._pages = {} self.control.clear() def dispose(self): """Disposes of the contents of an editor.""" self.context_object.on_trait_change( self.update_editor_item, self.name + "_items?", remove=True ) self.close_all() super().dispose() def update_page_name(self, object, name, old, new): """Handles the trait defining a particular page's name being changed.""" for i, value in enumerate(self._uis): page, ui, _, _ = value if object is ui.info.object: name = None handler = getattr( self.ui.handler, "%s_%s_page_name" % (self.object_name, self.name), None, ) if handler is not None: name = handler(self.ui.info, object) if name is None: name = str( xgetattr(object, self.factory.page_name[1:], "???") ) self.control.setTabText(self.control.indexOf(page), name) break def _create_page(self, object): # Create the view for the object: view_object = object factory = self.factory if factory.factory is not None: view_object = factory.factory(object) ui = view_object.edit_traits( parent=self.control, view=factory.view, kind=factory.ui_kind ).trait_set(parent=self.ui) # Get the name of the page being added to the notebook: name = "" monitoring = False prefix = "%s_%s_page_" % (self.object_name, self.name) page_name = factory.page_name if page_name[0:1] == ".": name = xgetattr(view_object, page_name[1:], None) monitoring = name is not None if monitoring: handler_name = None method = getattr(self.ui.handler, prefix + "name", None) if method is not None: handler_name = method(self.ui.info, object) if handler_name is not None: name = handler_name else: name = str(name) or "???" view_object.on_trait_change( self.update_page_name, page_name[1:], dispatch="ui" ) else: name = "" elif page_name != "": name = page_name if name == "": name = user_name_for(view_object.__class__.__name__) # Make sure the name is not a duplicate: if not monitoring: self._pages[name] = count = self._pages.get(name, 0) + 1 if count > 1: name += " %d" % count # Return the control for the ui, and whether or not its name is being # monitored: image = None method = getattr(self.ui.handler, prefix + "image", None) if method is not None: image = method(self.ui.info, object) if image is None: self.control.addTab(ui.control, name) else: self.control.addTab(ui.control, image, name) if self.factory.show_notebook_menu: newaction = self._context_menu.addAction(name) newaction.setText(name) newaction.setCheckable(True) newaction.setChecked(True) newaction.triggered.connect( lambda e, name=name: self._menu_action(e, name=name) ) self._action_dict[name] = newaction self._pagewidgets[name] = ui.control return (ui, view_object, monitoring) def _tab_activated(self, idx): """Handles a notebook tab being "activated" (i.e. clicked on) by the user. """ widget = self.control.widget(idx) for page, ui, _, _ in self._uis: if page is widget: self.selected = ui.info.object break def _selected_changed(self, selected): """Handles the **selected** trait being changed.""" for page, ui, _, _ in self._uis: if ui.info and selected is ui.info.object: self.control.setCurrentWidget(page) break deletable = self.factory.deletable deletable_trait = self.factory.deletable_trait if deletable and deletable_trait: enabled = xgetattr(selected, deletable_trait, True) self.close_button.setEnabled(enabled) def _context_menu_requested(self, event): self._context_menu.popup(self.control.mapToGlobal(event)) def _menu_action(self, event, name=""): """Qt signal handler for when a item in a context menu is actually selected. Not that we get this even after the underlying value has already changed. """ action = self._action_dict[name] checked = action.isChecked() if not checked: for ndx in range(self.control.count()): if self.control.tabText(ndx) == name: self.control.removeTab(ndx) else: # TODO: Fix tab order based on the context_object's list self.control.addTab(self._pagewidgets[name], name) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/qt/list_str_editor.py0000644000175100001730000005024000000000000022241 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Traits UI editor for editing lists of strings. """ import collections.abc from pyface.image_resource import ImageResource from pyface.qt import QtCore, QtGui, is_qt4 from traits.api import ( Any, Bool, Dict, Event, Int, Instance, List, Property, Str, TraitListEvent, NO_COMPARE, ) from traitsui.list_str_adapter import ListStrAdapter from .editor import Editor from .list_str_model import ListStrModel from traitsui.menu import Menu class _ListStrEditor(Editor): """Traits UI editor for editing lists of strings.""" # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- # The list view control associated with the editor: list_view = Any() #: The list model associated the editor: model = Instance(ListStrModel) #: The title of the editor: title = Str() #: The current set of selected items (which one is used depends upon the #: initial state of the editor factory 'multi_select' trait): selected = Any() multi_selected = List() #: The current set of selected item indices (which one is used depends upon #: the initial state of the editor factory 'multi_select' trait): selected_index = Int(-1) multi_selected_indices = List(Int) #: The most recently actived item and its index. #: Always trigger change notification. activated = Any(comparison_mode=NO_COMPARE) activated_index = Int(comparison_mode=NO_COMPARE) #: The most recently right_clicked item and its index: right_clicked = Event() right_clicked_index = Event() #: Is the list editor scrollable? This value overrides the default. scrollable = True #: Should the selected item be edited after rebuilding the editor list: edit = Bool(False) #: The adapter from list items to editor values: adapter = Instance(ListStrAdapter) #: Dictionary mapping image names to QIcons images = Dict() #: Dictionary mapping ImageResource objects to QIcons image_resources = Dict() #: The current number of item currently in the list: item_count = Property() #: The current search string: search = Str() # ------------------------------------------------------------------------- # Editor interface: # ------------------------------------------------------------------------- def init(self, parent): """Finishes initializing the editor by creating the underlying toolkit widget. """ factory = self.factory # Set up the adapter to use: self.adapter = factory.adapter self.sync_value(factory.adapter_name, "adapter", "from") # Create the list model and accompanying controls: self.model = ListStrModel(editor=self) self.control = QtGui.QWidget() layout = QtGui.QVBoxLayout(self.control) layout.setContentsMargins(0, 0, 0, 0) layout.setSpacing(0) if factory.title or factory.title_name: header_view = QtGui.QHeaderView(QtCore.Qt.Orientation.Horizontal, self.control) self._header_view = header_view header_view.setModel(self.model) header_view.setMaximumHeight(header_view.sizeHint().height()) if is_qt4: header_view.setResizeMode(QtGui.QHeaderView.ResizeMode.Stretch) else: header_view.setSectionResizeMode(QtGui.QHeaderView.ResizeMode.Stretch) layout.addWidget(header_view) else: self._header_view = None self.list_view = _ListView(self) layout.addWidget(self.list_view) # Set up the list control's event handlers: if factory.multi_select: slot = self._on_rows_selection else: slot = self._on_row_selection selection_model = self.list_view.selectionModel() selection_model.selectionChanged.connect(slot) self.list_view.activated.connect(self._on_activate) # Initialize the editor title: self.title = factory.title self.sync_value(factory.title_name, "title", "from") # Set up the selection listener if factory.multi_select: self.sync_value( factory.selected, "multi_selected", "both", is_list=True ) self.sync_value( factory.selected_index, "multi_selected_indices", "both", is_list=True, ) else: self.sync_value(factory.selected, "selected", "both") self.sync_value(factory.selected_index, "selected_index", "both") # Synchronize other interesting traits as necessary: self.sync_value(factory.activated, "activated", "to") self.sync_value(factory.activated_index, "activated_index", "to") self.sync_value(factory.right_clicked, "right_clicked", "to") self.sync_value( factory.right_clicked_index, "right_clicked_index", "to" ) # Make sure we listen for 'items' changes as well as complete list # replacements: self.context_object.observe( self.update_editor, self.extended_name + ".items", dispatch="ui" ) # Create the mapping from user supplied images to QIcons: for image_resource in factory.images: self._add_image(image_resource) # Refresh the editor whenever the adapter changes: self.observe(self.refresh_editor, "adapter.+update", dispatch="ui") # Set the list control's tooltip: self.set_tooltip() def dispose(self): """Disposes of the contents of an editor.""" self.model.beginResetModel() self.model.endResetModel() self.context_object.observe( self.update_editor, self.extended_name + ".items", remove=True, dispatch="ui", ) self.observe( self.refresh_editor, "adapter.+update", remove=True, dispatch="ui" ) if self._header_view is not None: self._header_view.setModel(None) self._header_view.deleteLater() self._header_view = None self.list_view._dispose() super().dispose() def update_editor(self, event=None): """Updates the editor when the object trait changes externally to the editor. """ if not self._no_update: self.model.beginResetModel() self.model.endResetModel() # restore selection back if self.factory.multi_select: self._multi_selected_changed(self.multi_selected) else: self._selected_changed(self.selected) # ------------------------------------------------------------------------- # ListStrEditor interface: # ------------------------------------------------------------------------- def refresh_editor(self, event=None): """Requests that the underlying list widget to redraw itself.""" self.list_view.viewport().update() def callx(self, func, *args, **kw): """Call a function without allowing the editor to update.""" old = self._no_update self._no_update = True try: func(*args, **kw) finally: self._no_update = old def setx(self, **keywords): """Set one or more attributes without allowing the editor to update.""" old = self._no_notify self._no_notify = True try: for name, value in keywords.items(): setattr(self, name, value) finally: self._no_notify = old def get_image(self, image): """Converts a user specified image to a QIcon.""" if isinstance(image, ImageResource): result = self.image_resources.get(image) if result is not None: return result return self._add_image(image) return self.images.get(image) def is_auto_add(self, index): """Returns whether or not the index is the special 'auto add' item at the end of the list. """ return self.factory.auto_add and ( index >= self.adapter.len(self.object, self.name) ) # ------------------------------------------------------------------------- # Private interface: # ------------------------------------------------------------------------- def _add_image(self, image_resource): """Adds a new image to the image map.""" image = image_resource.create_icon() self.image_resources[image_resource] = image self.images[image_resource.name] = image return image # -- Property Implementations --------------------------------------------- def _get_item_count(self): return self.model.rowCount(None) - self.factory.auto_add # -- Trait Event Handlers ------------------------------------------------- def _selected_changed(self, selected): """Handles the editor's 'selected' trait being changed.""" if not self._no_update: try: selected_index = self.value.index(selected) except ValueError: pass else: self._selected_index_changed(selected_index) def _selected_index_changed(self, selected_index): """Handles the editor's 'selected_index' trait being changed.""" if not self._no_update: smodel = self.list_view.selectionModel() if selected_index == -1: smodel.clearSelection() else: mi = self.model.index(selected_index) smodel.select(mi, QtGui.QItemSelectionModel.SelectionFlag.ClearAndSelect) self.list_view.scrollTo(mi) def _multi_selected_changed(self, selected): """Handles the editor's 'multi_selected' trait being changed.""" if not self._no_update: indices = [] for item in selected: try: indices.append(self.value.index(item)) except ValueError: pass self._multi_selected_indices_changed(indices) def _multi_selected_items_changed(self, event): """Handles the editor's 'multi_selected' trait being modified.""" if not self._no_update: try: added = [self.value.index(item) for item in event.added] removed = [self.value.index(item) for item in event.removed] except ValueError: pass else: event = TraitListEvent(index=0, added=added, removed=removed) self._multi_selected_indices_items_changed(event) def _multi_selected_indices_changed(self, selected_indices): """Handles the editor's 'multi_selected_indices' trait being changed.""" if not self._no_update: smodel = self.list_view.selectionModel() smodel.clearSelection() for selected_index in selected_indices: smodel.select( self.model.index(selected_index), QtGui.QItemSelectionModel.SelectionFlag.Select, ) if selected_indices: self.list_view.scrollTo(self.model.index(selected_indices[-1])) def _multi_selected_indices_items_changed(self, event): """Handles the editor's 'multi_selected_indices' trait being modified.""" if not self._no_update: smodel = self.list_view.selectionModel() for selected_index in event.removed: smodel.select( self.model.index(selected_index), QtGui.QItemSelectionModel.SelectionFlag.Deselect, ) for selected_index in event.added: smodel.select( self.model.index(selected_index), QtGui.QItemSelectionModel.SelectionFlag.Select, ) # -- List Control Event Handlers ------------------------------------------ def _on_activate(self, mi): """Handle a cell being activated.""" self.activated_index = index = mi.row() self.activated = self.adapter.get_item(self.object, self.name, index) def _on_mouse_right_click(self, point): """Handle a mouse right click event""" mi = self.list_view.indexAt(point) if mi.isValid(): self.right_clicked_index = index = mi.row() self.right_clicked = self.adapter.get_item( self.object, self.name, index ) def _on_row_selection(self, added, removed): """Handle the row selection being changed.""" self._no_update = True try: indices = self.list_view.selectionModel().selectedRows() if len(indices): self.selected_index = indices[0].row() self.selected = self.adapter.get_item( self.object, self.name, self.selected_index ) else: self.selected_index = -1 self.selected = None finally: self._no_update = False def _on_rows_selection(self, added, removed): """Handle the rows selection being changed.""" self._no_update = True try: indices = self.list_view.selectionModel().selectedRows() self.multi_selected_indices = indices = [i.row() for i in indices] self.multi_selected = [ self.adapter.get_item(self.object, self.name, i) for i in self.multi_selected_indices ] finally: self._no_update = False def _on_context_menu(self, pos): menu = self.factory.menu index = self.list_view.indexAt(pos).row() if isinstance(menu, str): menu = getattr(self.object, menu, None) if isinstance(menu, collections.abc.Callable): menu = menu(index) if menu is not None: qmenu = menu.create_menu(self.list_view, self) self._menu_context = { "selection": self.object, "object": self.object, "editor": self, "index": index, "info": self.ui.info, "handler": self.ui.handler, } qmenu.exec_(self.list_view.mapToGlobal(pos)) self._menu_context = None # ------------------------------------------------------------------------- # Qt widgets that have been configured to behave as expected by Traits UI: # ------------------------------------------------------------------------- class _ItemDelegate(QtGui.QStyledItemDelegate): """A QStyledItemDelegate which optionally draws horizontal gridlines. (QListView does not support gridlines). """ def __init__(self, editor, parent=None): """Save the editor""" QtGui.QStyledItemDelegate.__init__(self, parent) self._editor = editor def paint(self, painter, option, index): """Overrident to draw gridlines.""" QtGui.QStyledItemDelegate.paint(self, painter, option, index) if self._editor.factory.horizontal_lines: painter.save() painter.setPen(option.palette.color(QtGui.QPalette.ColorRole.Dark)) painter.drawLine( option.rect.bottomLeft(), option.rect.bottomRight() ) painter.restore() class _ListView(QtGui.QListView): """A QListView configured to behave as expected by TraitsUI.""" def __init__(self, editor): """Initialise the object.""" QtGui.QListView.__init__(self) self._editor = editor self.setItemDelegate(_ItemDelegate(editor, self)) self.setModel(editor.model) factory = editor.factory # Configure the selection behavior if factory.multi_select: mode = QtGui.QAbstractItemView.SelectionMode.ExtendedSelection else: mode = QtGui.QAbstractItemView.SelectionMode.SingleSelection self.setSelectionMode(mode) # Configure drag and drop behavior self.setDragEnabled(True) self.setDragDropOverwriteMode(True) self.setDragDropMode(QtGui.QAbstractItemView.DragDropMode.InternalMove) self.setDropIndicatorShown(True) if editor.factory.menu is False: self.setContextMenuPolicy(QtCore.Qt.ContextMenuPolicy.NoContextMenu) elif editor.factory.menu is not False: self.setContextMenuPolicy(QtCore.Qt.ContextMenuPolicy.CustomContextMenu) self.customContextMenuRequested.connect(editor._on_context_menu) # Configure context menu behavior self.setContextMenuPolicy(QtCore.Qt.ContextMenuPolicy.CustomContextMenu) def _dispose(self): """Clean up states in this view.""" self.setModel(None) def mouseReleaseEvent(self, event): """Reimplemented to support listening to right clicked item.""" if event.button() == QtCore.Qt.MouseButton.RightButton: event.accept() self._editor._on_mouse_right_click(event.pos()) super().mouseReleaseEvent(event) def keyPressEvent(self, event): """Reimplemented to support edit, insert, and delete by keyboard.""" editor = self._editor factory = editor.factory # Note that setting 'EditKeyPressed' as an edit trigger does not work on # most platforms, which is why we do this here. if ( event.key() in (QtCore.Qt.Key.Key_Enter, QtCore.Qt.Key.Key_Return) and self.state() != QtGui.QAbstractItemView.State.EditingState and factory.editable and "edit" in factory.operations ): if factory.multi_select: indices = editor.multi_selected_indices row = indices[0] if len(indices) == 1 else -1 else: row = editor.selected_index if row != -1: event.accept() self.edit(editor.model.index(row)) elif ( event.key() in (QtCore.Qt.Key.Key_Backspace, QtCore.Qt.Key.Key_Delete) and factory.editable and "delete" in factory.operations ): event.accept() if factory.multi_select: for row in reversed(sorted(editor.multi_selected_indices)): editor.model.removeRow(row) elif editor.selected_index != -1: # Deleting the selected item will reset cause the ListView's # selection model to select the next item before removing the # originally selected rows. Visually, this looks fine because # the next item is then placed where the deleted item used to # be. However, some internal state is kept which makes the # selected item seem off by one. So we'll reset it manually # here. row = editor.selected_index editor.model.removeRow(row) # Handle the case of deleting the last item in the list. editor.selected_index = min( row, editor.adapter.len(editor.object, editor.name) - 1 ) elif ( event.key() == QtCore.Qt.Key.Key_Insert and factory.editable and "insert" in factory.operations ): event.accept() if factory.multi_select: indices = sorted(editor.multi_selected_indices) row = indices[0] if len(indices) else -1 else: row = editor.selected_index if row == -1: row = editor.adapter.len(editor.object, editor.name) editor.model.insertRow(row) self.setCurrentIndex(editor.model.index(row)) else: QtGui.QListView.keyPressEvent(self, event) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/qt/list_str_model.py0000644000175100001730000002422400000000000022056 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Defines the table model used by the tabular editor. """ from pyface.qt import QtCore, QtGui from traitsui.ui_traits import SequenceTypes # MIME type for internal table drag/drop operations mime_type = "traits-ui-list-str-editor" class ListStrModel(QtCore.QAbstractListModel): """A model for lists of strings.""" def __init__(self, editor, parent=None): """Initialise the object.""" QtCore.QAbstractListModel.__init__(self, parent) self._editor = editor # ------------------------------------------------------------------------- # QAbstractItemModel interface: # ------------------------------------------------------------------------- def rowCount(self, mi): """Reimplemented to return items in the list.""" editor = self._editor n = editor.adapter.len(editor.object, editor.name) if editor.factory.auto_add: n += 1 return n def data(self, mi, role): """Reimplemented to return the data.""" editor = self._editor adapter = editor.adapter index = mi.row() if role == QtCore.Qt.ItemDataRole.DisplayRole or role == QtCore.Qt.ItemDataRole.EditRole: if editor.is_auto_add(index): text = adapter.get_default_text(editor.object, editor.name) else: text = adapter.get_text(editor.object, editor.name, index) if role == QtCore.Qt.ItemDataRole.DisplayRole and text == "": # FIXME: This is a hack to make empty strings editable. text = " " return text elif role == QtCore.Qt.ItemDataRole.DecorationRole: if editor.is_auto_add(index): image = adapter.get_default_image(editor.object, editor.name) else: image = adapter.get_image(editor.object, editor.name, index) image = editor.get_image(image) if image is not None: return image elif role == QtCore.Qt.ItemDataRole.ToolTipRole: tooltip = adapter.get_tooltip(editor.object, editor.name, index) if tooltip: return tooltip elif role == QtCore.Qt.ItemDataRole.BackgroundRole: if editor.is_auto_add(index): color = adapter.get_default_bg_color( editor.object, editor.name ) else: color = adapter.get_bg_color(editor.object, editor.name, index) if color is not None: if isinstance(color, SequenceTypes): q_color = QtGui.QColor(*color) else: q_color = QtGui.QColor(color) return QtGui.QBrush(q_color) elif role == QtCore.Qt.ItemDataRole.ForegroundRole: if editor.is_auto_add(index): color = adapter.get_default_text_color( editor.object, editor.name ) else: color = adapter.get_text_color( editor.object, editor.name, index ) if color is not None: if isinstance(color, SequenceTypes): q_color = QtGui.QColor(*color) else: q_color = QtGui.QColor(color) return QtGui.QBrush(q_color) return None def setData(self, mi, value, role): """Reimplmented to allow for modification of the object trait.""" editor = self._editor if editor.is_auto_add(mi.row()): method = editor.adapter.insert else: method = editor.adapter.set_text editor.callx(method, editor.object, editor.name, mi.row(), value) self.dataChanged.emit(mi, mi) return True def setItemData(self, mi, roles): """Reimplmented to reject all setItemData calls.""" # FIXME: This is a hack to prevent the QListView from clearing out the # old row after a move operation. (The QTableView doesn't do this, for # some reason). This behavior is not overridable so far as I can tell, # but there may be a better way around this issue. Note that we cannot # simply use a CopyAction instead because InternalMove mode is hardcoded # in the Qt source to allow only MoveActions. return False def flags(self, mi): """Reimplemented to set editable status and movable status.""" editor = self._editor index = mi.row() flags = QtCore.Qt.ItemFlag.ItemIsSelectable | QtCore.Qt.ItemFlag.ItemIsEnabled if ( editor.factory.editable and "edit" in editor.factory.operations and editor.adapter.get_can_edit(editor.object, editor.name, index) ): flags |= QtCore.Qt.ItemFlag.ItemIsEditable if ( editor.factory.editable and "move" in editor.factory.operations and editor.adapter.get_drag(editor.object, editor.name, index) is not None ): flags |= QtCore.Qt.ItemFlag.ItemIsDragEnabled | QtCore.Qt.ItemFlag.ItemIsDropEnabled return flags def headerData(self, section, orientation, role): """Reimplemented to return title for vertical header data.""" if ( orientation != QtCore.Qt.Orientation.Horizontal or role != QtCore.Qt.ItemDataRole.DisplayRole ): return None return self._editor.title def insertRow(self, row, parent=QtCore.QModelIndex(), obj=None): """Reimplemented to allow creation of new rows. Added an optional arg to allow the insertion of an existing row object. """ editor = self._editor adapter = editor.adapter if obj is None: obj = adapter.get_default_value(editor.object, editor.name) self.beginInsertRows(parent, row, row) editor.callx( editor.adapter.insert, editor.object, editor.name, row, obj ) self.endInsertRows() return True def insertRows(self, row, count, parent=QtCore.QModelIndex()): """Reimplemented to allow creation of new items.""" editor = self._editor adapter = editor.adapter self.beginInsertRows(parent, row, row + count - 1) for i in range(count): value = adapter.get_default_value(editor.object, editor.name) editor.callx( adapter.insert, editor.object, editor.name, row, value ) self.endInsertRows() return True def removeRows(self, row, count, parent=QtCore.QModelIndex()): """Reimplemented to allow row deletion, as well as reordering via drag and drop. """ editor = self._editor adapter = editor.adapter self.beginRemoveRows(parent, row, row + count - 1) for i in range(count): editor.callx(adapter.delete, editor.object, editor.name, row) self.endRemoveRows() return True def mimeTypes(self): """Reimplemented to expose our internal MIME type for drag and drop operations. """ return [mime_type] def mimeData(self, indexes): """Reimplemented to generate MIME data containing the rows of the current selection. """ mime_data = QtCore.QMimeData() rows = list({index.row() for index in indexes}) data = QtCore.QByteArray(str(rows[0]).encode("utf8")) for row in rows[1:]: data.append((" %i" % row).encode("utf8")) mime_data.setData(mime_type, data) return mime_data def dropMimeData(self, mime_data, action, row, column, parent): """Reimplemented to allow items to be moved.""" if action == QtCore.Qt.DropAction.IgnoreAction: return False data = mime_data.data(mime_type) if data.isNull(): return False current_rows = [int(s) for s in data.data().decode("utf8").split(" ")] self.moveRows(current_rows, parent.row()) return True def supportedDropActions(self): """Reimplemented to allow items to be moved.""" return QtCore.Qt.DropAction.MoveAction # ------------------------------------------------------------------------- # ListStrModel interface: # ------------------------------------------------------------------------- def moveRow(self, old_row, new_row): """Convenience method to move a single row.""" return self.moveRows([old_row], new_row) def moveRows(self, current_rows, new_row): """Moves a sequence of rows (provided as a list of row indexes) to a new row. """ editor = self._editor # Sort rows in descending order so they can be removed without # invalidating the indices. current_rows.sort() current_rows.reverse() # If the the highest selected row is lower than the destination, do an # insertion before rather than after the destination. if current_rows[-1] < new_row: new_row += 1 # Remove selected rows... objects = [] for row in current_rows: if row <= new_row: new_row -= 1 obj = editor.adapter.get_item(editor.object, editor.name, row) objects.insert(0, obj) self.removeRow(row) # ...and add them at the new location. for i, obj in enumerate(objects): self.insertRow(new_row + i, obj=obj) # Update the selection for the new location. if editor.factory.multi_select: editor.setx(multi_selected=objects) editor.multi_selected_indices = list( range(new_row, new_row + len(objects)) ) else: editor.setx(selected=objects[0]) editor.selected_index = new_row ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/qt/menu.py0000644000175100001730000002175600000000000020006 0ustar00runnerdocker00000000000000# (C) Copyright 2008-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # ------------------------------------------------------------------------------ # Copyright (c) 2007, Riverbank Computing Limited # All rights reserved. # # This software is provided without warranty under the terms of the BSD license. # However, when used with the GPL version of PyQt the additional terms # described in the PyQt GPL exception also apply # # Author: Riverbank Computing Limited # ------------------------------------------------------------------------------ """ Dynamically construct PyQt Menus or MenuBars from a supplied string description of the menu. Menu Description Syntax:: submenu_label {help_string} menuitem_label | accelerator {help_string} [~/-name]: code *submenu_label* Label of a sub menu *menuitem_label* Label of a menu item {*help_string*} Help string to display on the status line (optional) *accelerator* Accelerator key (e.g., Ctrl-C) (The '|' and keyname are optional, but must be used together.) [~] The menu item is checkable, but is not checked initially (optional) [/] The menu item is checkable, and is checked initially (optional) [-] The menu item disabled initially (optional) [*name*] Symbolic name used to refer to menu item (optional) *code* Python code invoked when menu item is selected A line beginning with a hyphen (-) is interpreted as a menu separator. """ import re from pyface.qt import QtGui help_pat = re.compile(r"(.*){(.*)}(.*)") options_pat = re.compile(r"(.*)\[(.*)\](.*)") class MakeMenu: """Manages creation of menus.""" def __init__(self, desc, owner, popup=False, window=None): """Initializes the object.""" self.owner = owner if window is None: window = owner self.window = window self.indirect = getattr(owner, "call_menu", None) self.names = {} self.desc = desc.split("\n") self.index = 0 if popup: self.menu = menu = QtGui.QMenu() self.parse(menu, -1) else: self.menu = menu = QtGui.QMenuBar() self.parse(menu, -1) window.setMenuBar(menu) def parse(self, menu, indent): """Recursively parses menu items from the description.""" while True: # Make sure we have not reached the end of the menu description # yet: if self.index >= len(self.desc): return # Get the next menu description line and check its indentation: dline = self.desc[self.index] line = dline.lstrip() indented = len(dline) - len(line) if indented <= indent: return # Indicate that the current line has been processed: self.index += 1 # Check for a blank or comment line: if (line == "") or (line[0:1] == "#"): continue # Check for a menu separator: if line[0:1] == "-": menu.addSeparator() continue # Extract the help string (if any): help = "" match = help_pat.search(line) if match: help = " " + match.group(2).strip() line = match.group(1) + match.group(3) # Check for a menu item: col = line.find(":") if col >= 0: handler = line[col + 1 :].strip() if handler != "": if self.indirect: self.indirect(None, handler) handler = self.indirect else: try: _locl = dict(self=self) exec( "def handler(self=self.owner):\n %s\n" % handler, globals(), _locl, ) handler = _locl["handler"] except: handler = null_handler else: try: _locl = dict(self=self) exec( "def handler(self=self.owner):\n%s\n" % (self.get_body(indented),), globals(), _locl, ) handler = _locl["handler"] except: handler = null_handler not_checked = checked = disabled = False name = key = "" line = line[:col] match = options_pat.search(line) if match: line = match.group(1) + match.group(3) not_checked, checked, disabled, name = option_check( "~/-", match.group(2).strip() ) label = line.strip() col = label.find("|") if col >= 0: key = label[col + 1 :].strip() label = label[:col].strip() act = menu.addAction(label, handler) act.setCheckable(not_checked or checked) act.setStatusTip(help) if key: act.setShortcut(key) if checked: act.setChecked(True) if disabled: act.setEnabled(False) if name: self.names[name] = act setattr(self.owner, name, MakeMenuItem(self, act)) else: # Else must be the start of a sub menu: submenu = QtGui.QMenu(line.strip()) # Recursively parse the sub-menu: self.parse(submenu, indented) # Add the menu to its parent: act = menu.addMenu(submenu) act.setStatusTip(help) def get_body(self, indent): """Returns the body of an inline method.""" result = [] while self.index < len(self.desc): line = self.desc[self.index] if (len(line) - len(line.lstrip())) <= indent: break result.append(line) self.index += 1 result = "\n".join(result).rstrip() if result != "": return result return " pass" def get_action(self, name): """Returns the QAction associated with a specified name.""" if isinstance(name, str): return self.names[name] return name def checked(self, name, check=None): """Checks (or unchecks) a menu item specified by name.""" act = self.get_action(name) if check is None: return act.isChecked() act.setChecked(check) def enabled(self, name, enable=None): """Enables (or disables) a menu item specified by name.""" act = self.get_action(name) if enable is None: return act.isEnabled() act.setEnabled(enable) def label(self, name, label=None): """Gets or sets the label for a menu item.""" act = self.get_action(name) if label is None: return str(act.text()) act.setText(label) class MakeMenuItem: """A menu item for a menu managed by MakeMenu.""" def __init__(self, menu, act): self.menu = menu self.act = act def checked(self, check=None): return self.menu.checked(self.act, check) def toggle(self): checked = not self.checked() self.checked(checked) return checked def enabled(self, enable=None): return self.menu.enabled(self.act, enable) def label(self, label=None): return self.menu.label(self.act, label) # ------------------------------------------------------------------------- # Determine whether a string contains any specified option characters, and # remove them if it does: # ------------------------------------------------------------------------- def option_check(test, string): """Determines whether a string contains any specified option characters, and removes them if it does. """ result = [] for char in test: col = string.find(char) result.append(col >= 0) if col >= 0: string = string[:col] + string[col + 1 :] return result + [string.strip()] # ------------------------------------------------------------------------- # Null menu option selection handler: # ------------------------------------------------------------------------- def null_handler(event): print("null_handler invoked") ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/qt/null_editor.py0000644000175100001730000000164300000000000021353 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Defines a completely empty editor, intended to be used as a spacer. """ from pyface.qt import QtGui from .editor import Editor class NullEditor(Editor): """A completely empty editor.""" def init(self, parent): """Finishes initializing the editor by creating the underlying toolkit widget. """ self.control = QtGui.QWidget() def update_editor(self): """Updates the editor when the object trait changes externally to the editor. """ pass ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/qt/progress_editor.py0000644000175100001730000000603500000000000022245 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! import time from pyface.qt import QtGui, QtCore from traits.api import Instance, Int, Str from pyface.ui.qt.progress_dialog import ProgressDialog from traitsui.qt.editor import Editor class _ProgressDialog(ProgressDialog): def close(self): """Overwritten to disable closing.""" pass class SimpleEditor(Editor): """ Show a progress bar with all the optional goodies """ progress = Instance(_ProgressDialog) #: The message to be displayed along side the progress guage message = Str() #: The starting value min = Int() #: The ending value max = Int() # -- Editor interface ------------------------------------------------------ def init(self, parent): """Finishes initializing the editor by creating the underlying toolkit widget. """ self.control = self.create_control(parent) factory = self.factory self.min = factory.min self.max = factory.max self.message = factory.message self.sync_value(factory.min_name, "min", "from") self.sync_value(factory.max_name, "max", "from") self.sync_value(factory.message_name, "message", "from") self.set_tooltip() def create_control(self, parent): """ Finishes initializing the editor by creating the underlying widget. """ self.progress = _ProgressDialog( title=self.factory.title, message=self.factory.message, min=self.factory.min, max=self.factory.max, can_cancel=self.factory.can_cancel, show_time=self.factory.show_time, show_percent=self.factory.show_percent, ) control = QtGui.QWidget() self.control = control layout = QtGui.QVBoxLayout(self.control) layout.setContentsMargins(0, 0, 0, 0) # The 'guts' of the dialog. self.progress._create_message(control, layout) self.progress._create_gauge(control, layout) self.progress._create_percent(control, layout) self.progress._create_timer(control, layout) self.progress._create_buttons(control, layout) return self.control def update_editor(self): """ Updates the editor when the object trait changes externally to the editor. """ self.progress.min = self.min self.progress.max = self.max self.progress.message = self.message self.progress.update(self.value) def _min_changed(self): self.update_editor() def _max_changed(self): self.update_editor() def _message_changed(self): self.update_editor() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/qt/range_editor.py0000644000175100001730000006307400000000000021503 0ustar00runnerdocker00000000000000# (C) Copyright 2009-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # ------------------------------------------------------------------------------ # Copyright (c) 2008, Riverbank Computing Limited # All rights reserved. # # This software is provided without warranty under the terms of the BSD license. # However, when used with the GPL version of PyQt the additional terms # described in the PyQt GPL exception also apply # # Author: Riverbank Computing Limited # ------------------------------------------------------------------------------ """ Defines the various range editors and the range editor factory, for the PyQt user interface toolkit. """ from math import log10 from pyface.qt import QtCore, QtGui from traits.api import TraitError, Str, Float, Any, Bool from .editor_factory import TextEditor from .editor import Editor from .constants import OKColor, ErrorColor from .helper import IconButton # ------------------------------------------------------------------------- # 'BaseRangeEditor' class: # ------------------------------------------------------------------------- class BaseRangeEditor(Editor): """The base class for Range editors. Using an evaluate trait, if specified, when assigning numbers the object trait. """ # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- #: Function to evaluate floats/ints evaluate = Any() def _set_value(self, 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. """ # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- #: 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.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 = QtGui.QWidget() panel = QtGui.QHBoxLayout(self.control) panel.setContentsMargins(0, 0, 0, 0) fvalue = self.value try: if not (self.low <= fvalue <= self.high): fvalue = self.low fvalue_text = self.string_value(fvalue) except: fvalue_text = "" fvalue = self.low ivalue = self._convert_to_slider(fvalue) self._label_lo = QtGui.QLabel() self._label_lo.setAlignment( QtCore.Qt.AlignmentFlag.AlignRight | QtCore.Qt.AlignmentFlag.AlignVCenter ) if factory.label_width > 0: self._label_lo.setMinimumWidth(int(factory.label_width)) panel.addWidget(self._label_lo) self.control.slider = slider = QtGui.QSlider(QtCore.Qt.Orientation.Horizontal) slider.setTracking(factory.auto_set) slider.setMinimum(0) slider.setMaximum(10000) slider.setPageStep(1000) slider.setSingleStep(100) slider.setValue(ivalue) slider.valueChanged.connect(self.update_object_on_scroll) panel.addWidget(slider) self._label_hi = QtGui.QLabel() panel.addWidget(self._label_hi) if factory.label_width > 0: self._label_hi.setMinimumWidth(int(factory.label_width)) self.control.text = text = QtGui.QLineEdit(fvalue_text) text.editingFinished.connect(self.update_object_on_enter) # The default size is a bit too big and probably doesn't need to grow. sh = text.sizeHint() sh.setWidth(sh.width() // 2) text.setMaximumSize(sh) panel.addWidget(text) low_label = factory.low_label if factory.low_name != "": low_label = self.string_value(self.low) high_label = factory.high_label if factory.high_name != "": high_label = self.string_value(self.high) self._label_lo.setText(low_label) self._label_hi.setText(high_label) self.set_tooltip(slider) self.set_tooltip(self._label_lo) self.set_tooltip(self._label_hi) self.set_tooltip(text) def update_object_on_scroll(self, pos): """Handles the user changing the current slider value.""" value = self._convert_from_slider(pos) blocked = self.control.text.blockSignals(True) try: self.value = value self.control.text.setText(self.string_value(value)) except TraitError as exc: from traitsui.api import raise_to_debug raise_to_debug() finally: self.control.text.blockSignals(blocked) def update_object_on_enter(self): """Handles the user pressing the Enter key in the text field.""" # It is possible the event is processed after the control is removed # from the editor if self.control is None: return try: value = eval(str(self.control.text.text()).strip()) except Exception as ex: # They entered something that didn't eval as a number, (e.g., # 'foo') pretend it didn't happen value = self.value self.control.text.setText(str(value)) # for compound editor, value may be non-numeric if not isinstance(value, (int, float)): return if not self.factory.is_float: value = int(value) # If setting the value yields an error, the resulting error dialog # stealing focus could trigger another editingFinished signal so we # block signals here blocked = self.control.text.blockSignals(True) try: self.value = value blocked = self.control.slider.blockSignals(True) try: self.control.slider.setValue( self._convert_to_slider(self.value) ) finally: self.control.slider.blockSignals(blocked) except TraitError: # They entered something invalid, pretend it didn't happen value = self.value self.control.text.setText(str(value)) finally: self.control.text.blockSignals(blocked) 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: text = self.string_value(value) 1 / (low <= value <= high) except: text = "" value = low ivalue = self._convert_to_slider(value) self.control.text.setText(text) blocked = self.control.slider.blockSignals(True) try: self.control.slider.setValue(ivalue) finally: self.control.slider.blockSignals(blocked) def get_error_control(self): """Returns the editor's control for indicating error status.""" return self.control.text 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._label_lo is not None: self._label_lo.setText(self.string_value(low)) self.update_editor() 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._label_hi is not None: self._label_hi.setText(self.string_value(high)) self.update_editor() def _convert_to_slider(self, value): """Returns the slider setting corresponding to the user-supplied value.""" if self.high > self.low: ivalue = int( (float(value - self.low) / (self.high - self.low)) * 10000.0 ) else: ivalue = self.low if ivalue is None: ivalue = 0 return ivalue def _convert_from_slider(self, ivalue): """Returns the float or integer value corresponding to the slider setting. """ value = self.low + ((float(ivalue) / 10000.0) * (self.high - self.low)) if not self.factory.is_float: value = int(round(value)) return value # ------------------------------------------------------------------------- class LogRangeSliderEditor(SimpleSliderEditor): # ------------------------------------------------------------------------- """A slider editor for log-spaced values""" def _convert_to_slider(self, value): """Returns the slider setting corresponding to the user-supplied value.""" value = max(value, self.low) ivalue = int( (log10(value) - log10(self.low)) / (log10(self.high) - log10(self.low)) * 10000.0 ) return ivalue def _convert_from_slider(self, ivalue): """Returns the float or integer value corresponding to the slider setting. """ value = float(ivalue) / 10000.0 * (log10(self.high) - log10(self.low)) # Do this to handle floating point errors, where fvalue may exceed # self.high. fvalue = min(self.low * 10 ** (value), self.high) if not self.factory.is_float: fvalue = int(round(fvalue)) return fvalue 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. """ # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- #: 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) 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() self.control = QtGui.QWidget() panel = QtGui.QHBoxLayout(self.control) panel.setContentsMargins(0, 0, 0, 0) fvalue = self.value try: fvalue_text = self._format % fvalue 1 / (low <= fvalue <= high) except: fvalue_text = "" fvalue = low if high > low: ivalue = int((float(fvalue - low) / (high - low)) * 10000) else: ivalue = low # Lower limit label: self.control.label_lo = label_lo = QtGui.QLabel() label_lo.setAlignment(QtCore.Qt.AlignmentFlag.AlignRight | QtCore.Qt.AlignmentFlag.AlignVCenter) panel.addWidget(label_lo) # Lower limit button: self.control.button_lo = IconButton( QtGui.QStyle.StandardPixmap.SP_ArrowLeft, self.reduce_range ) panel.addWidget(self.control.button_lo) # Slider: self.control.slider = slider = QtGui.QSlider(QtCore.Qt.Orientation.Horizontal) slider.setTracking(factory.auto_set) slider.setMinimum(0) slider.setMaximum(10000) slider.setPageStep(1000) slider.setSingleStep(100) slider.setValue(ivalue) slider.valueChanged.connect(self.update_object_on_scroll) panel.addWidget(slider) # Upper limit button: self.control.button_hi = IconButton( QtGui.QStyle.StandardPixmap.SP_ArrowRight, self.increase_range ) panel.addWidget(self.control.button_hi) # Upper limit label: self.control.label_hi = label_hi = QtGui.QLabel() panel.addWidget(label_hi) # Text entry: self.control.text = text = QtGui.QLineEdit(fvalue_text) text.editingFinished.connect(self.update_object_on_enter) # The default size is a bit too big and probably doesn't need to grow. sh = text.sizeHint() sh.setWidth(sh.width() // 2) text.setMaximumSize(sh) panel.addWidget(text) label_lo.setText(str(low)) label_hi.setText(str(high)) self.set_tooltip(slider) self.set_tooltip(label_lo) self.set_tooltip(label_hi) self.set_tooltip(text) # Update the ranges and button just in case. self.update_range_ui() def update_object_on_scroll(self, pos): """Handles the user changing the current slider value.""" value = self.cur_low + ( (float(pos) / 10000.0) * (self.cur_high - self.cur_low) ) self.control.text.setText(self._format % value) if self.factory.is_float: self.value = value else: self.value = int(value) def update_object_on_enter(self): """Handles the user pressing the Enter key in the text field.""" # It is possible the event is processed after the control is removed # from the editor if self.control is None: return try: self.value = eval(str(self.control.text.text()).strip()) except TraitError as excp: pass 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: text = self._format % value 1 / (low <= value <= high) except: value = low self.value = value if not self.ui_changing: self.init_range() self.update_range_ui() def update_range_ui(self): """Updates the slider range controls.""" low, high = self.cur_low, self.cur_high value = self.value self._set_format() self.control.label_lo.setText(self._format % low) self.control.label_hi.setText(self._format % high) if high > low: ivalue = int((float(value - low) / (high - low)) * 10000.0) else: ivalue = low blocked = self.control.slider.blockSignals(True) self.control.slider.setValue(ivalue) self.control.slider.blockSignals(blocked) text = self._format % self.value self.control.text.setText(text) self.control.button_lo.setEnabled(low != self.low) self.control.button_hi.setEnabled(high != self.high) 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): """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.ui_changing = True self.value = min(max(self.value, self.cur_low), self.cur_high) self.ui_changing = False self.update_range_ui() def increase_range(self): """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.ui_changing = True self.value = min(max(self.value, self.cur_low), self.cur_high) self.ui_changing = False self.update_range_ui() 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.text 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.""" # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- # 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") low = self.low high = self.high self.control = QtGui.QSpinBox() self.control.setMinimum(low) self.control.setMaximum(high) self.control.setValue(self.value) self.control.valueChanged.connect(self.update_object) if not factory.auto_set: self.control.setKeyboardTracking(False) self.set_tooltip() def update_object(self, value): """Handles the user selecting a new value in the spin box.""" self._locked = True try: self.value = value finally: self._locked = False def update_editor(self): """Updates the editor when the object trait changes externally to the editor. """ if not self._locked: blocked = self.control.blockSignals(True) try: self.control.setValue(int(self.value)) except Exception: from traitsui.api import raise_to_debug raise_to_debug() finally: self.control.blockSignals(blocked) 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.setMinimum(low) self.control.setValue(int(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.setMaximum(high) self.control.setValue(int(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. """ # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- #: Low value for the slider range low = Any() #: High value for the slider range high = Any() #: 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) factory = self.factory if not factory.low_name: self.low = factory.low if not factory.high_name: self.high = factory.high 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") # force value to start in range if self.low is not None and self.low > self.value: self.value = self.low elif self.high is not None and self.high < self.value: self.value = self.low if self.low is not None else self.high def update_object(self): """Handles the user entering input data in the edit control.""" try: value = eval(str(self.control.text())) if self.evaluate is not None: value = self.evaluate(value) if self.low is not None and self.low > value: value = self.low col = ErrorColor elif self.high is not None and self.high < value: value = self.low if self.low is not None else self.high col = ErrorColor else: col = OKColor self.value = value except Exception: col = ErrorColor if self.control is not None: pal = QtGui.QPalette(self.control.palette()) pal.setColor(QtGui.QPalette.ColorRole.Base, col) self.control.setPalette(pal) # ------------------------------------------------------------------------- # 'SimpleEnumEditor' factory adaptor: # ------------------------------------------------------------------------- def SimpleEnumEditor(parent, factory, ui, object, name, description, **kwargs): return CustomEnumEditor( parent, factory, ui, object, name, description, "simple" ) def CustomEnumEditor( parent, factory, ui, object, name, description, style="custom", **kwargs ): """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=list(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: # ------------------------------------------------------------------------- # Mapping between editor factory modes and simple 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, } ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/qt/rgb_color_editor.py0000644000175100001730000000516600000000000022355 0ustar00runnerdocker00000000000000# (C) Copyright 2008-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # ------------------------------------------------------------------------------ # Copyright (c) 2007, Riverbank Computing Limited # All rights reserved. # # This software is provided without warranty under the terms of the BSD license. # However, when used with the GPL version of PyQt the additional terms # described in the PyQt GPL exception also apply # # Author: Riverbank Computing Limited # ------------------------------------------------------------------------------ """ Defines a subclass of the base PyQt color editor factory, for colors that are represented as tuples of the form ( *red*, *green*, *blue* ), where *red*, *green* and *blue* are floats in the range from 0.0 to 1.0. """ from pyface.qt import QtGui from traits.trait_base import SequenceTypes # Note: The ToolkitEditorFactory class imported from color_editor is a # subclass of the abstract ToolkitEditorFactory class # (in traitsui.api) with qt-specific methods defined. # We need to override the implementations of the qt-specific methods here. from .color_editor import ToolkitEditorFactory as BaseColorToolkitEditorFactory # ------------------------------------------------------------------------- # The PyQt4 ToolkitEditorFactory class. # ------------------------------------------------------------------------- class ToolkitEditorFactory(BaseColorToolkitEditorFactory): """PyQt editor factory for color editors.""" def to_qt_color(self, editor): """Gets the PyQt color equivalent of the object trait.""" try: color = getattr(editor.object, editor.name + "_") except AttributeError: color = getattr(editor.object, editor.name) c = QtGui.QColor() c.setRgbF(color[0], color[1], color[2]) return c def from_qt_color(self, color): """Gets the application equivalent of a PyQt value.""" return (color.redF(), color.greenF(), color.blueF()) def str_color(self, color): """Returns the text representation of a specified color value.""" if type(color) in SequenceTypes: return "(%d,%d,%d)" % ( int(color[0] * 255.0), int(color[1] * 255.0), int(color[2] * 255.0), ) return color ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/qt/rgb_color_trait.py0000644000175100001730000000640300000000000022205 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Trait definition for an RGB-based color, which is a tuple of the form (*red*, *green*, *blue*), where *red*, *green* and *blue* are floats in the range from 0.0 to 1.0. """ from traits.api import Trait, TraitError from traits.trait_base import SequenceTypes from traitsui.qt.color_trait import standard_colors # ------------------------------------------------------------------------- # Convert a number into an RGB tuple: # ------------------------------------------------------------------------- def range_check(value): """Checks that *value* can be converted to a value in the range 0.0 to 1.0. If so, it returns the floating point value; otherwise, it raises a TraitError. """ value = float(value) if 0.0 <= value <= 1.0: return value raise TraitError def convert_to_color(object, name, value): """Converts a tuple or an integer to an RGB color value, or raises a TraitError if that is not possible. """ if isinstance(value, SequenceTypes) and len(value) == 3: return ( range_check(value[0]), range_check(value[1]), range_check(value[2]), ) if isinstance(value, int): return ( (value / 0x10000) / 255.0, ((value // 0x100) & 0xFF) / 255.0, (value & 0xFF) / 255.0, ) raise TraitError convert_to_color.info = ( "a tuple of the form (r,g,b), where r, g, and b " "are floats in the range from 0.0 to 1.0, or an integer which in hex is of " "the form 0xRRGGBB, where RR is red, GG is green, and BB is blue" ) # ------------------------------------------------------------------------- # Standard colors: # ------------------------------------------------------------------------- # RGB versions of standard colors: rgb_standard_colors = {} for name, color in standard_colors.items(): rgb_standard_colors[name] = (color.redF(), color.greenF(), color.blueF()) # ------------------------------------------------------------------------- # Define wxPython specific color traits: # ------------------------------------------------------------------------- ### Note: Declare the editor to be a function which returns the RGBColorEditor # class from traits ui to avoid circular import issues. For backwards # compatibility with previous Traits versions, the 'editors' folder in Traits # project declares 'from api import *' in its __init__.py. The 'api' in turn # can contain classes that have a RGBColor trait which lead to this file getting # imported. This will lead to a circular import when declaring a RGBColor # trait. def get_rgb_color_editor(*args, **traits): from .rgb_color_editor import ToolkitEditorFactory return ToolkitEditorFactory(*args, **traits) # Trait whose value must be an RGB color: RGBColor = Trait( "white", convert_to_color, rgb_standard_colors, editor=get_rgb_color_editor ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/qt/search_editor.py0000644000175100001730000000620200000000000021642 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # System library imports from pyface.qt import QtCore, QtGui # ETS imports from .editor import Editor class SearchWidget(QtGui.QLineEdit): # FIXME: This widget needs a search button and a cancel button like the # wxWidgets SearchControl. def __init__(self, desc): """Store the descriptive text for the widget.""" super().__init__() self._desc = str(desc) self._set_descriptive_text() def focusInEvent(self, event): """Handles accepting focus. If the text box contains the default description string, reset the text color and clear the box. """ palette = QtGui.QApplication.instance().palette() self.setPalette(palette) if self.text() == self._desc: self.setText("") self.update() super().focusInEvent(event) def focusOutEvent(self, event): """Handles losing focus. When focus is lost, if the user had typed something, keep that text, otherwise replace it with the default description string. """ if len(self.text()) == 0: self._set_descriptive_text() super().focusOutEvent(event) def _set_descriptive_text(self): """Sets the greyed-out descriptive text.""" palette = QtGui.QApplication.instance().palette() palette.setColor( QtGui.QPalette.ColorRole.Text, palette.color(QtGui.QPalette.ColorRole.Dark) ) self.setPalette(palette) self.setText(self._desc) self.update() class SearchEditor(Editor): def init(self, parent): """Finishes initializing the editor by creating the underlying toolkit widget. """ if QtCore.__version_info__ < (4, 7, 0): control = self.control = SearchWidget(self.factory.text) else: control = self.control = QtGui.QLineEdit() control.setPlaceholderText(self.factory.text) if self.factory.auto_set: control.textEdited.connect(self.update_object) if self.factory.enter_set: control.editingFinished.connect(self.update_object) def update_object(self, event=None): """Handles the user entering input data in the edit control.""" if not self._no_update: self.value = str(self.control.text()) if self.factory.search_event_trait != "": setattr(self.object, self.factory.search_event_trait, True) def update_editor(self): """Updates the editor when the object trait changes externally to the editor. """ if str(self.control.text()) != self.value: self._no_update = True self.control.setText(self.str_value) self._no_update = False ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/qt/set_editor.py0000644000175100001730000003747400000000000021207 0ustar00runnerdocker00000000000000# (C) Copyright 2008-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # ------------------------------------------------------------------------------ # Copyright (c) 2007, Riverbank Computing Limited # All rights reserved. # # This software is provided without warranty under the terms of the BSD license. # However, when used with the GPL version of PyQt the additional terms # described in the PyQt GPL exception also apply # # Author: Riverbank Computing Limited # ------------------------------------------------------------------------------ """ Defines the set editors for the PyQt user interface toolkit. """ from pyface.qt import QtCore, QtGui from traitsui.helper import enum_values_changed from .editor import Editor from traits.api import Instance, Property class SimpleEditor(Editor): """Simple style of editor for sets. The editor displays two list boxes, with buttons for moving the selected items from left to right, or vice versa. If **can_move_all** on the factory is True, then buttons are displayed for moving all the items to one box or the other. If the set is ordered, buttons are displayed for moving the selected item up or down in right-side list box. """ # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- #: The top level QLayout for the editor: root_layout = Instance(QtGui.QLayout) #: Current set of enumeration names: names = Property() #: Current mapping from names to values: mapping = Property() #: Current inverse mapping from values to names: inverse_mapping = Property() #: Is set editor scrollable? This value overrides the default. scrollable = True def init(self, parent): """Finishes initializing the editor by creating the underlying toolkit widget. """ self.control = QtGui.QWidget() self.root_layout = QtGui.QGridLayout(self.control) self.root_layout.setContentsMargins(0, 0, 0, 0) factory = self.factory if factory.name != "": self._object, self._name, self._value = self.parse_extended_name( factory.name ) self.values_changed() self._object.on_trait_change( self._values_changed, self._name, dispatch="ui" ) else: self._value = lambda: self.factory.values self.values_changed() factory.on_trait_change( self._values_changed, "values", dispatch="ui" ) blayout = QtGui.QVBoxLayout() self._unused = self._create_listbox( 0, self._on_unused, self._on_use, factory.left_column_title ) self._use_all = self._unuse_all = self._up = self._down = None if factory.can_move_all: self._use_all = self._create_button( ">>", blayout, self._on_use_all ) self._use = self._create_button(">", blayout, self._on_use) self._unuse = self._create_button("<", blayout, self._on_unuse) if factory.can_move_all: self._unuse_all = self._create_button( "<<", blayout, self._on_unuse_all ) if factory.ordered: self._up = self._create_button("Move Up", blayout, self._on_up) self._down = self._create_button( "Move Down", blayout, self._on_down ) self.root_layout.addLayout(blayout, 1, 1, QtCore.Qt.AlignmentFlag.AlignCenter) self._used = self._create_listbox( 2, self._on_value, self._on_unuse, factory.right_column_title ) self.context_object.on_trait_change( self.update_editor, self.extended_name + "_items?", dispatch="ui" ) def _get_names(self): """Gets the current set of enumeration names.""" return self._names def _get_mapping(self): """Gets the current mapping.""" return self._mapping def _get_inverse_mapping(self): """Gets the current inverse mapping.""" return self._inverse_mapping def _create_listbox(self, col, handler1, handler2, title): """Creates a list box.""" # Add the column title in emphasized text: title_widget = QtGui.QLabel(title) font = QtGui.QFont(title_widget.font()) font.setBold(True) font.setPointSize(font.pointSize() + 1) title_widget.setFont(font) self.root_layout.addWidget(title_widget, 0, col, QtCore.Qt.AlignmentFlag.AlignLeft) # Create the list box and add it to the column: list = QtGui.QListWidget() list.setSelectionMode(QtGui.QAbstractItemView.SelectionMode.ExtendedSelection) self.root_layout.addWidget(list, 1, col) list.itemClicked.connect(handler1) list.itemDoubleClicked.connect(handler2) return list def _create_button(self, label, layout, handler): """Creates a button.""" button = QtGui.QPushButton(label) # The connection type is set to workaround Qt5 + MacOSX issue with # event dispatching. See enthought/traitsui#1308 button.clicked.connect(handler, type=QtCore.Qt.ConnectionType.QueuedConnection) layout.addWidget(button) return button def values_changed(self): """Recomputes the cached data based on the underlying enumeration model or the values of the factory. """ ( self._names, self._mapping, self._inverse_mapping, ) = enum_values_changed(self._value(), self.string_value) def _values_changed(self): """Handles the underlying object model's enumeration set or factory's values being changed. """ self.values_changed() self.update_editor() def update_editor(self): """Updates the editor when the object trait changes externally to the editor. """ # Check for any items having been deleted from the enumeration that are # still present in the object value: mapping = self.inverse_mapping.copy() values = [v for v in self.value if v in mapping] if len(values) < len(self.value): self.value = values # Get a list of the selected items in the right box: used = self._used used_labels = self._get_selected_strings(used) # Get a list of the selected items in the left box: unused = self._unused unused_labels = self._get_selected_strings(unused) # Empty list boxes in preparation for rebuilding from current values: used.clear() unused.clear() # Ensure right list box is kept alphabetized unless insertion # order is relevant: if not self.factory.ordered: values = sorted(values[:]) # Rebuild the right listbox: used_selections = [] for i, value in enumerate(values): label = mapping[value] used.addItem(label) del mapping[value] if label in used_labels: used_selections.append(i) # Rebuild the left listbox: unused_selections = [] unused_items = sorted(mapping.values()) mapping = self.mapping self._unused_items = [mapping[ui] for ui in unused_items] for i, unused_item in enumerate(unused_items): unused.addItem(unused_item) if unused_item in unused_labels: unused_selections.append(i) # If nothing is selected, default selection should be top of left box, # or of right box if left box is empty: if (len(used_selections) == 0) and (len(unused_selections) == 0): if unused.count() == 0: used_selections.append(0) else: unused_selections.append(0) used_count = used.count() for i in used_selections: if i < used_count: used.item(i).setSelected(True) unused_count = unused.count() for i in unused_selections: if i < unused_count: unused.item(i).setSelected(True) self._check_up_down() self._check_left_right() def dispose(self): """Disposes of the contents of an editor.""" if self._object is not None: self._object.on_trait_change( self._values_changed, self._name, remove=True ) else: self.factory.on_trait_change( self._values_changed, "values", remove=True ) self.context_object.on_trait_change( self.update_editor, self.extended_name + "_items?", remove=True ) super().dispose() def get_error_control(self): """Returns the editor's control for indicating error status.""" return [self._unused, self._used] def _on_value(self): if not self.factory.ordered: self._unused.clearSelection() self._check_left_right() self._check_up_down() def _on_unused(self): if not self.factory.ordered: self._used.clearSelection() self._check_left_right() self._check_up_down() def _on_use(self): self._unused_items, self.value = self._transfer_items( self._unused, self._used, self._unused_items, self.value ) def _on_unuse(self): self.value, self._unused_items = self._transfer_items( self._used, self._unused, self.value, self._unused_items ) def _on_use_all(self): self._unused_items, self.value = self._transfer_all( self._unused, self._used, self._unused_items, self.value ) def _on_unuse_all(self): self.value, self._unused_items = self._transfer_all( self._used, self._unused, self.value, self._unused_items ) def _on_up(self): self._move_item(-1) def _on_down(self): self._move_item(1) def _transfer_all(self, list_from, list_to, values_from, values_to): """Transfers all items from one list to another.""" values_from = values_from[:] values_to = values_to[:] list_from.clearSelection() while list_from.count() > 0: index_to = list_to.count() list_from.item(0).setSelected(True) list_to.insertItems( index_to, self._get_selected_strings(list_from) ) list_from.takeItem(0) values_to.append(values_from[0]) del values_from[0] list_to.item(0).setSelected(True) self._check_left_right() self._check_up_down() return (values_from, values_to) def _transfer_items(self, list_from, list_to, values_from, values_to): """Transfers the selected item from one list to another.""" values_from = values_from[:] values_to = values_to[:] index_from = max(self._get_first_selection(list_from), 0) index_to = max(self._get_first_selection(list_to), 0) list_to.clearSelection() # Get the list of strings in the "from" box to be moved: selected_list = self._get_selected_strings(list_from) # fixme: I don't know why I have to reverse the list to get # correct behavior from the ordered list box. Investigate -- LP selected_list.reverse() list_to.insertItems(index_to, selected_list) # Delete the transferred items from the left box: items_from = list_from.selectedItems() for i in range(len(items_from) - 1, -1, -1): list_from.takeItem(list_from.row(items_from[i])) del items_from # Delete the transferred items from the "unused" value list: for item_label in selected_list: val_index_from = values_from.index(self.mapping[item_label]) values_to.insert(index_to, values_from[val_index_from]) del values_from[val_index_from] # If right list is ordered, keep moved items selected: if self.factory.ordered: items = list_to.findItems( item_label, QtCore.Qt.MatchFlag.MatchFixedString | QtCore.Qt.MatchFlag.MatchCaseSensitive, ) if items: items[0].setSelected(True) # Reset the selection in the left box: count = list_from.count() if count > 0: if index_from >= count: index_from = count - 1 list_from.item(index_from).setSelected(True) self._check_left_right() self._check_up_down() return (values_from, values_to) def _move_item(self, direction): """Moves an item up or down within the "used" list.""" # Move the item up/down within the list: listbox = self._used index_from = self._get_first_selection(listbox) index_to = index_from + direction label = listbox.takeItem(index_from).text() listbox.insertItem(index_to, label) listbox.item(index_to).setSelected(True) # Enable the up/down buttons appropriately: self._check_up_down() # Move the item up/down within the editor's trait value: value = self.value if direction < 0: index = index_to values = [value[index_from], value[index_to]] else: index = index_from values = [value[index_to], value[index_from]] self.value = value[:index] + values + value[index + 2 :] def _check_up_down(self): """Sets the proper enabled state for the up and down buttons.""" if self.factory.ordered: selected = self._used.selectedItems() self._up.setEnabled( len(selected) == 1 and selected[0] is not self._used.item(0) ) self._down.setEnabled( len(selected) == 1 and selected[0] is not self._used.item(self._used.count() - 1) ) def _check_left_right(self): """Sets the proper enabled state for the left and right buttons.""" self._use.setEnabled( self._unused.count() > 0 and self._get_first_selection(self._unused) >= 0 ) self._unuse.setEnabled( self._used.count() > 0 and self._get_first_selection(self._used) >= 0 ) if self.factory.can_move_all: self._use_all.setEnabled( self._unused.count() > 0 and self._get_first_selection(self._unused) >= 0 ) self._unuse_all.setEnabled( self._used.count() > 0 and self._get_first_selection(self._used) >= 0 ) # ------------------------------------------------------------------------- # Returns a list of the selected strings in the listbox # ------------------------------------------------------------------------- def _get_selected_strings(self, listbox): """Returns a list of the selected strings in the given *listbox*.""" return [str(itm.text()) for itm in listbox.selectedItems()] # ------------------------------------------------------------------------- # Returns the index of the first (or only) selected item. # ------------------------------------------------------------------------- def _get_first_selection(self, listbox): """Returns the index of the first (or only) selected item.""" select_list = listbox.selectedItems() if len(select_list) == 0: return -1 return listbox.row(select_list[0]) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/qt/shell_editor.py0000644000175100001730000000161600000000000021510 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Editor that displays an interactive Python shell. """ from traitsui.editors.shell_editor import _ShellEditor as BaseShellEditor from .editor import Editor # ------------------------------------------------------------------------- # 'ShellEditor' class: # ------------------------------------------------------------------------- class _ShellEditor(BaseShellEditor, Editor): """Editor that displays an interactive Python shell.""" def init(self, parent): super().init(None) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/qt/styled_date_editor.py0000644000175100001730000001013700000000000022700 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! from pyface.qt import QtCore, QtGui from pyface.qt.QtGui import QFont from traits.api import Dict # For a simple editor style, we just punt and use the same simple editor # as in the default date_editor. from .date_editor import SimpleEditor from .date_editor import CustomEditor as DateCustomEditor class CustomEditor(DateCustomEditor): dates = Dict() styles = Dict() def init(self, parent): super().init(parent) if not self.factory.allow_past: self.control.setMinimumDate(QtCore.QDate.currentDate()) if self.factory.dates_trait and self.factory.styles_trait: self.sync_value(self.factory.dates_trait, "dates", "from") self.sync_value(self.factory.styles_trait, "styles", "from") def _dates_changed(self, old, new): # Someone changed out the entire dict. The easiest, most robust # way to handle this is to reset the text formats of all the dates # in the old dict, and then set the dates in the new dict. if old: [ list(map(self._reset_formatting, dates)) for dates in old.values() ] if new: styles = getattr(self.object, self.factory.styles_trait, None) self._apply_styles(styles, new) def _dates_items_changed(self, event): # Handle the added and changed items groups_to_set = event.added groups_to_set.update(event.changed) styles = getattr(self.object, self.factory.styles_trait, None) self._apply_styles(styles, groups_to_set) # Handle the removed items by resetting them [ list(map(self._reset_formatting, dates)) for dates in event.removed.values() ] def _styles_changed(self, old, new): groups = getattr(self.object, self.factory.dates_trait, {}) if not new: # If no new styles, then reset all the dates to a default style [ list(map(self._reset_formatting, dates)) for dates in groups.values() ] else: self._apply_styles(new, groups) return def _styles_items_changed(self, event): groups = getattr(self.object, self.factory.dates_trait) styles = getattr(self.object, self.factory.styles_trait) names_to_update = list(event.added.keys()) + list(event.changed.keys()) modified_groups = dict( (name, groups[name]) for name in names_to_update ) self._apply_styles(styles, modified_groups) names_to_reset = list(event.removed.keys()) for name in names_to_reset: self._reset_formatting(groups[name]) return # ------------------------------------------------------------------------ # Helper functions # ------------------------------------------------------------------------ def _apply_style(self, style, dates): """**style** is a CellFormat, **dates** is a list of datetime.date""" for dt in dates: self.set_unselected_style(style, dt) return def _apply_styles(self, style_dict, date_dict): """Applies the proper style out of style_dict to every (name,date_list) in date_dict. """ if not style_dict or not date_dict: return for groupname, dates in date_dict.items(): cellformat = style_dict.get(groupname, None) if not cellformat: continue for dt in dates: self.set_unselected_style(cellformat, dt) return def _reset_formatting(self, dates): # Resets the text format on the given dates for dt in dates: self.apply_unselected_style(dt) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/qt/table_editor.py0000644000175100001730000014317400000000000021476 0ustar00runnerdocker00000000000000# (C) Copyright 2009-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # ------------------------------------------------------------------------------ # Copyright (c) 2008, Riverbank Computing Limited # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license. # # When used with the GPL version of PyQt the additional terms described # in the PyQt GPL exception also apply. # # Author: Riverbank Computing Limited # ------------------------------------------------------------------------------ """ Defines the table editor for the PyQt user interface toolkit. """ from pyface.qt import QtCore, QtGui, is_qt4 from pyface.image_resource import ImageResource from pyface.timer.api import do_later from pyface.ui_traits import Image from traits.api import ( Any, Bool, Button, Dict, Event, List, HasTraits, Instance, Int, Property, Str, cached_property, observe, ) from traitsui.api import ( EnumEditor, InstanceEditor, Group, Item, Label, ObjectColumn, TableColumn, TableFilter, UI, View, default_handler, spring, ) from traitsui.editors.table_editor import ( BaseTableEditor, ReversedList, customize_filter, ) from traitsui.ui_traits import SequenceTypes from .editor import Editor from .table_model import TableModel, SortFilterTableModel if is_qt4: def set_qheader_section_resize_mode(header): return header.setResizeMode else: def set_qheader_section_resize_mode(header): return header.setSectionResizeMode class TableEditor(Editor, BaseTableEditor): """Editor that presents data in a table. Optionally, tables can have a set of filters that reduce the set of data displayed, according to their criteria. """ # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- #: The table view control associated with the editor: table_view = Any() def _table_view_default(self): return TableView(editor=self) #: A wrapper around the source model which provides filtering and sorting: model = Instance(SortFilterTableModel) def _model_default(self): return SortFilterTableModel(editor=self) #: The table model associated with the editor: source_model = Instance(TableModel) def _source_model_default(self): return TableModel(editor=self) #: The set of columns currently defined on the editor: columns = List(TableColumn) #: The currently selected row(s), column(s), or cell(s). selected = Any() #: The current selected row selected_row = Property(Any, observe="selected") selected_indices = Property(Any, observe="selected") #: Current filter object (should be a TableFilter or callable or None): filter = Any() #: The indices of the table items currently passing the table filter: filtered_indices = List(Int) #: Current filter summary message filter_summary = Str("All items") #: Update the filtered contents. update_filter = Event() #: The event fired when a cell is clicked on: click = Event() #: The event fired when a cell is double-clicked on: dclick = Event() #: The Traits UI associated with the table editor toolbar: toolbar_ui = Instance(UI) #: The index of the row that was last right clicked on its vertical header header_row = Int() #: Whether to auto-size the columns or not. auto_size = Bool(False) #: Dictionary mapping image names to QIcons images = Dict() #: Dictionary mapping ImageResource objects to QIcons image_resources = Dict() #: An image being converted: image = Image def init(self, parent): """Finishes initializing the editor by creating the underlying toolkit widget.""" factory = self.factory self.filter = factory.filter columns = factory.columns[:] if (len(columns) == 0) and (len(self.value) > 0): columns = [ ObjectColumn(name=name) for name in self.value[0].editable_traits() ] self.columns = columns if factory.table_view_factory is not None: self.table_view = factory.table_view_factory(editor=self) if factory.source_model_factory is not None: self.source_model = factory.source_model_factory(editor=self) if factory.model_factory is not None: self.model = factory.model_factory(editor=self) # Create the table view and model self.model.setDynamicSortFilter(True) self.model.setSourceModel(self.source_model) self.table_view.setModel(self.model) # When sorting is enabled, the first column is initially displayed with # the triangle indicating it is the sort index, even though no sorting # has actually been done. Sort here for UI/model consistency. if self.factory.sortable and not self.factory.reorderable: self.model.sort(0, QtCore.Qt.SortOrder.AscendingOrder) # Connect to the mode specific selection handler and select the first # row/column/cell. Do this before creating the edit_view to make sure # that it has a valid item to use when constructing its view. smodel = self.table_view.selectionModel() mode_slot = getattr(self, "_on_%s_selection" % factory.selection_mode) smodel.selectionChanged.connect(mode_slot) self.table_view.setCurrentIndex(self.model.index(0, 0)) # Create the toolbar if necessary if factory.show_toolbar and len(factory.filters) > 0: main_view = QtGui.QWidget() layout = QtGui.QVBoxLayout(main_view) layout.setContentsMargins(0, 0, 0, 0) self.toolbar_ui = self.edit_traits( parent=parent, kind="subpanel", view=View( Group( Item("filter{View}", editor=factory._filter_editor), Item("filter_summary{Results}", style="readonly"), spring, orientation="horizontal", ), resizable=True, ), ) self.toolbar_ui.parent = self.ui layout.addWidget(self.toolbar_ui.control) layout.addWidget(self.table_view) else: main_view = self.table_view # Create auxiliary editor and encompassing splitter if necessary mode = factory.selection_mode if (factory.edit_view == " ") or mode not in {"row", "rows"}: self.control = main_view else: if factory.orientation == "horizontal": self.control = QtGui.QSplitter(QtCore.Qt.Orientation.Horizontal) else: self.control = QtGui.QSplitter(QtCore.Qt.Orientation.Vertical) self.control.setSizePolicy( QtGui.QSizePolicy.Policy.Expanding, QtGui.QSizePolicy.Policy.Expanding ) self.control.addWidget(main_view) self.control.setStretchFactor(0, 2) # Create the row editor below the table view editor = InstanceEditor(view=factory.edit_view, kind="subpanel") self._ui = self.edit_traits( parent=self.control, kind="subpanel", view=View( Item( "selected_row", style="custom", editor=editor, show_label=False, resizable=True, width=factory.edit_view_width, height=factory.edit_view_height, ), resizable=True, handler=factory.edit_view_handler, ), ) self._ui.parent = self.ui self.control.addWidget(self._ui.control) self.control.setStretchFactor(1, 1) # Connect to the click and double click handlers self.table_view.clicked.connect(self._on_click) self.table_view.doubleClicked.connect(self._on_dclick) # Make sure we listen for 'items' changes as well as complete list # replacements self.context_object.on_trait_change( self.update_editor, self.extended_name + "_items", dispatch="ui" ) # Listen for changes to traits on the objects in the list self.context_object.on_trait_change( self.refresh_editor, self.extended_name + ".-", dispatch="ui" ) # Listen for changes on column definitions self.on_trait_change(self._update_columns, "columns", dispatch="ui") self.on_trait_change( self._update_columns, "columns_items", dispatch="ui" ) # Set up the required externally synchronized traits is_list = mode in ("rows", "columns", "cells") self.sync_value(factory.click, "click", "to") self.sync_value(factory.dclick, "dclick", "to") self.sync_value(factory.columns_name, "columns", is_list=True) self.sync_value(factory.selected, "selected", is_list=is_list) self.sync_value( factory.selected_indices, "selected_indices", is_list=is_list ) self.sync_value(factory.filter_name, "filter", "from") self.sync_value(factory.filtered_indices, "filtered_indices", "to") self.sync_value(factory.update_filter_name, "update_filter", "from") self.auto_size = self.factory.auto_size # Initialize the ItemDelegates for each column self._update_columns() def dispose(self): """Disposes of the contents of an editor.""" self.model.beginResetModel() self.model.endResetModel() # Make sure that the auxiliary UIs are properly disposed if self.toolbar_ui is not None: self.toolbar_ui.dispose() if self._ui is not None: self._ui.dispose() # Remove listener for 'items' changes on object trait self.context_object.on_trait_change( self.update_editor, self.extended_name + "_items", remove=True ) # Remove listener for changes to traits on the objects in the list self.context_object.on_trait_change( self.refresh_editor, self.extended_name + ".-", remove=True ) # Remove listeners for column definition changes self.on_trait_change(self._update_columns, "columns", remove=True) self.on_trait_change( self._update_columns, "columns_items", remove=True ) super().dispose() def update_editor(self): """Updates the editor when the object trait changes externally to the editor.""" if self._no_notify: return self.table_view.setUpdatesEnabled(False) try: filtering = ( len(self.factory.filters) > 0 or self.filter is not None ) if filtering: self._update_filtering() # invalidate the model, but do not reset it. Resetting the model # may cause problems if the selection sync'ed traits are being used # externally to manage the selections self.model.invalidate() self.table_view.resizeColumnsToContents() if self.auto_size: self.table_view.resizeRowsToContents() finally: self.table_view.setUpdatesEnabled(True) def restore_prefs(self, prefs): """Restores any saved user preference information associated with the editor. """ header = self.table_view.horizontalHeader() if header is not None and "column_state" in prefs: header.restoreState(prefs["column_state"]) def save_prefs(self): """Returns any user preference information associated with the editor.""" prefs = {} header = self.table_view.horizontalHeader() if header is not None: prefs["column_state"] = header.saveState().data() return prefs def refresh_editor(self): """Requests that the underlying table widget to redraw itself.""" self.table_view.viewport().update() def create_new_row(self): """Creates a new row object using the provided factory.""" factory = self.factory kw = factory.row_factory_kw.copy() if "__table_editor__" in kw: kw["__table_editor__"] = self return self.ui.evaluate( factory.row_factory, *factory.row_factory_args, **kw ) def items(self): """Returns the raw list of model objects.""" items = self.value if not isinstance(items, SequenceTypes): items = [items] if self.factory and self.factory.reverse: items = ReversedList(items) return items def callx(self, func, *args, **kw): """Call a function without notifying the underlying table view or model.""" old = self._no_notify self._no_notify = True try: func(*args, **kw) finally: self._no_notify = old def setx(self, **keywords): """Set one or more attributes without notifying the underlying table view or model.""" old = self._no_notify self._no_notify = True try: for name, value in keywords.items(): setattr(self, name, value) finally: self._no_notify = old def set_selection(self, objects=[], notify=True): """Sets the current selection to a set of specified objects.""" if not isinstance(objects, list): objects = [objects] mode = self.factory.selection_mode indexes = [] flags = QtGui.QItemSelectionModel.SelectionFlag.ClearAndSelect # In the case of row or column selection, we need a dummy value for the # other dimension that has not been filtered. source_index = self.model.mapToSource(self.model.index(0, 0)) source_row, source_column = source_index.row(), source_index.column() # Selection mode is 'row' or 'rows' if mode.startswith("row"): flags |= QtGui.QItemSelectionModel.SelectionFlag.Rows items = self.items() for obj in objects: try: row = items.index(obj) except ValueError: continue indexes.append(self.source_model.index(row, source_column)) # Selection mode is 'column' or 'columns' elif mode.startswith("column"): flags |= QtGui.QItemSelectionModel.SelectionFlag.Columns for name in objects: column = self._column_index_from_name(name) if column != -1: indexes.append(self.source_model.index(source_row, column)) # Selection mode is 'cell' or 'cells' else: items = self.items() for obj, name in objects: try: row = items.index(obj) except ValueError: continue column = self._column_index_from_name(name) if column != -1: indexes.append(self.source_model.index(row, column)) # Perform the selection so that only one signal is emitted selection = QtGui.QItemSelection() smodel = self.table_view.selectionModel() if smodel is None: # guard against selection during tear-down return for index in indexes: index = self.model.mapFromSource(index) if index.isValid(): smodel.setCurrentIndex( index, QtGui.QItemSelectionModel.SelectionFlag.NoUpdate ) selection.select(index, index) smodel.blockSignals(not notify) try: if len(selection.indexes()): smodel.clear() smodel.select(selection, flags) else: smodel.clear() finally: smodel.blockSignals(False) self.refresh_editor() # ------------------------------------------------------------------------- # Private methods: # ------------------------------------------------------------------------- def _column_index_from_name(self, name): """Returns the index of the column with the given name or -1 if no column exists with that name.""" for i, column in enumerate(self.columns): if name == column.name: return i return -1 def _customize_filters(self, filter): """Allows the user to customize the current set of table filters.""" filter_editor = TableFilterEditor(editor=self) ui = filter_editor.edit_traits(parent=self.control) if ui.result: self.factory.filters = filter_editor.templates self.filter = filter_editor.selected_filter else: self.setx(filter=filter) def _update_filtering(self): """Update the filter summary and the filtered indices.""" items = self.items() num_items = len(items) f = self.filter if f is None: self._filtered_cache = None self.filtered_indices = list(range(num_items)) self.filter_summary = "All %i items" % num_items else: if not callable(f): f = f.filter self._filtered_cache = fc = [f(item) for item in items] self.filtered_indices = fi = [i for i, ok in enumerate(fc) if ok] self.filter_summary = "%i of %i items" % (len(fi), num_items) def _add_image(self, image_resource): """Adds a new image to the image map.""" image = image_resource.create_icon() self.image_resources[image_resource] = image self.images[image_resource.name] = image return image def _get_image(self, image): """Converts a user specified image to a QIcon.""" if isinstance(image, str): self.image = image image = self.image if isinstance(image, ImageResource): result = self.image_resources.get(image) if result is not None: return result return self._add_image(image) return self.images.get(image) def _create_empty_menu(self): """Create a QMenu to display in empty space below the rows. Returns a QMenu or None if no menu to display. """ if not self.factory.editable or self.factory.row_factory is None: return None empty_menu = QtGui.QMenu(self.table_view) action = empty_menu.addAction("Add new item") action.triggered.connect(self._on_context_append) return empty_menu def _create_header_menu(self): """Create a QMenu to display in the vertical header. Returns a QMenu or None if no menu to display. """ header_menu = QtGui.QMenu(self.table_view) if self.factory.editable: if self.factory.row_factory is not None: action = header_menu.addAction("Insert new item") action.triggered.connect(self._on_context_insert) if self.factory.deletable: action = header_menu.addAction("Delete item") action.triggered.connect(self._on_context_remove) if self.factory.reorderable: show_up = (self.header_row > 0) show_down = (self.header_row < self.model.rowCount() - 1) if not header_menu.isEmpty() and (show_up or show_down): header_menu.addSeparator() if show_up: header_menu_up = header_menu.addAction("Move item up") header_menu_up.triggered.connect(self._on_context_move_up) if show_down: header_menu_down = header_menu.addAction("Move item down") header_menu_down.triggered.connect(self._on_context_move_down) if header_menu.isEmpty(): return None else: return header_menu # -- Trait Property getters/setters --------------------------------------- @cached_property def _get_selected_row(self): """Gets the selected row, or the first row if multiple rows are selected.""" mode = self.factory.selection_mode if mode.startswith("column"): return None elif mode == "row": return self.selected try: if mode == "rows": return self.selected[0] elif mode == "cell": return self.selected[0] elif mode == "cells": return self.selected[0][0] except IndexError: return None @cached_property def _get_selected_indices(self): """Gets the row,column indices which match the selected trait""" selection_items = self.table_view.selectionModel().selection() indices = self.model.mapSelectionToSource(selection_items).indexes() if self.factory.selection_mode.startswith("row"): indices = sorted(set(index.row() for index in indices)) elif self.factory.selection_mode.startswith("column"): indices = sorted(set(index.column() for index in indices)) else: indices = [(index.row(), index.column()) for index in indices] if self.factory.selection_mode in {"rows", "columns", "cells"}: return indices elif len(indices) > 0: return indices[0] else: return -1 def _set_selected_indices(self, indices): if not isinstance(indices, list): indices = [indices] selected = [] if self.factory.selection_mode.startswith("row"): for row in indices: selected.append(self.value[row]) elif self.factory.selection_mode.startswith("column"): for col in indices: selected.append(self.columns[col].name) else: for row, col in indices: selected.append((self.value[row], self.columns[col].name)) self.selected = selected self.set_selection(self.selected, False) # -- Trait Change Handlers ------------------------------------------------ def _filter_changed(self, old_filter, new_filter): """Handles the current filter being changed.""" if not self._no_notify: if new_filter is customize_filter: do_later(self._customize_filters, old_filter) else: self._update_filtering() self.model.invalidate() self.set_selection(self.selected) def _update_columns(self): """Handle the column list being changed.""" self.table_view.setItemDelegate(TableDelegate(self.table_view)) for i, column in enumerate(self.columns): if column.renderer: self.table_view.setItemDelegateForColumn(i, column.renderer) self.model.invalidate() self.table_view.resizeColumnsToContents() if self.auto_size: self.table_view.resizeRowsToContents() def _selected_changed(self, new): """Handle the selected row/column/cell being changed externally.""" if not self._no_notify: self.set_selection(self.selected, notify=False) def _update_filter_changed(self): """The filter has changed internally.""" self._filter_changed(self.filter, self.filter) # -- Event Handlers ------------------------------------------------------- def _on_row_selection(self, added, removed): """Handle the row selection being changed.""" items = self.items() indexes = self.table_view.selectionModel().selectedRows() if len(indexes): index = self.model.mapToSource(indexes[0]) selected = items[index.row()] else: selected = None self.setx(selected=selected) self.ui.evaluate(self.factory.on_select, self.selected) def _on_rows_selection(self, added, removed): """Handle the rows selection being changed.""" items = self.items() indexes = self.table_view.selectionModel().selectedRows() selected = [ items[self.model.mapToSource(index).row()] for index in indexes ] self.setx(selected=selected) self.ui.evaluate(self.factory.on_select, self.selected) def _on_column_selection(self, added, removed): """Handle the column selection being changed.""" indexes = self.table_view.selectionModel().selectedColumns() if len(indexes): index = self.model.mapToSource(indexes[0]) selected = self.columns[index.column()].name else: selected = "" self.setx(selected=selected) self.ui.evaluate(self.factory.on_select, self.selected) def _on_columns_selection(self, added, removed): """Handle the columns selection being changed.""" indexes = self.table_view.selectionModel().selectedColumns() selected = [ self.columns[self.model.mapToSource(index).column()].name for index in indexes ] self.setx(selected=selected) self.ui.evaluate(self.factory.on_select, self.selected) def _on_cell_selection(self, added, removed): """Handle the cell selection being changed.""" items = self.items() indexes = self.table_view.selectionModel().selectedIndexes() if len(indexes): index = self.model.mapToSource(indexes[0]) obj = items[index.row()] column_name = self.columns[index.column()].name else: obj = None column_name = "" selected = (obj, column_name) self.setx(selected=selected) self.ui.evaluate(self.factory.on_select, self.selected) def _on_cells_selection(self, added, removed): """Handle the cells selection being changed.""" items = self.items() indexes = self.table_view.selectionModel().selectedIndexes() selected = [] for index in indexes: index = self.model.mapToSource(index) obj = items[index.row()] column_name = self.columns[index.column()].name selected.append((obj, column_name)) self.setx(selected=selected) self.ui.evaluate(self.factory.on_select, self.selected) def _on_click(self, index): """Handle a cell being clicked.""" index = self.model.mapToSource(index) column = self.columns[index.column()] obj = self.items()[index.row()] # Fire the same event on the editor after mapping it to a model object # and column name: self.click = (obj, column) # Invoke the column's click handler: column.on_click(obj) def _on_dclick(self, index): """Handle a cell being double clicked.""" index = self.model.mapToSource(index) column = self.columns[index.column()] obj = self.items()[index.row()] # Fire the same event on the editor after mapping it to a model object # and column name: self.dclick = (obj, column) # Invoke the column's double-click handler: column.on_dclick(obj) def _on_context_insert(self): """Handle 'insert item' being selected from the header context menu.""" self.model.insertRow(self.header_row) def _on_context_append(self): """Handle 'add item' being selected from the empty space context menu.""" self.model.insertRow(self.model.rowCount()) def _on_context_remove(self): """Handle 'remove item' being selected from the header context menu.""" self.model.removeRow(self.header_row) def _on_context_move_up(self): """Handle 'move up' being selected from the header context menu.""" self.model.moveRow(self.header_row, self.header_row - 1) def _on_context_move_down(self): """Handle 'move down' being selected from the header context menu.""" self.model.moveRow(self.header_row, self.header_row + 1) # Define the SimpleEditor class. SimpleEditor = TableEditor # Define the ReadonlyEditor class. ReadonlyEditor = TableEditor # ------------------------------------------------------------------------- # Qt widgets that have been configured to behave as expected by Traits UI: # ------------------------------------------------------------------------- class TableDelegate(QtGui.QStyledItemDelegate): """A QStyledItemDelegate which fetches Traits UI editors.""" def createEditor(self, parent, option, index): """Reimplemented to return the editor for a given index.""" model = index.model() index = model.mapToSource(index) table_editor = model._editor column = table_editor.columns[index.column()] obj = table_editor.items()[index.row()] factory = column.get_editor(obj) style = column.get_style(obj) if factory is None: return None target, name = column.target_name(obj) handler = default_handler() if table_editor.ui.context is None: ui = UI(handler=handler) else: context = table_editor.ui.context.copy() context["table_editor_object"] = context["object"] context["object"] = target ui = UI(handler=handler, context=context) # Create and initialize the editor factory_method = getattr(factory, style + "_editor") editor = factory_method(ui, target, name, "", parent) editor.prepare(parent) control = editor.control control.setParent(parent) # Required for QMouseEvents to propagate to the widget control.setFocusPolicy(QtCore.Qt.FocusPolicy.StrongFocus) # The table view's background will shine through unless the editor # paints its own background control.setAutoFillBackground(True) # Make sure that editors are disposed of correctly # will be disposed in closeEditor of the TableView control._editor = editor return control def updateEditorGeometry(self, editor, option, index): """Update the editor's geometry.""" editor.setGeometry(option.rect) def paint(self, painter, option, index): self.initStyleOption(option, index) if (option.state & QtGui.QStyle.StateFlag.State_Selected) and ( option.state & QtGui.QStyle.StateFlag.State_Active ): factory = self.parent()._editor.factory if factory.selection_bg_color is not None: option.palette.setColor( QtGui.QPalette.ColorRole.Highlight, factory.selection_bg_color_ ) if factory.selection_color is not None: option.palette.setColor( QtGui.QPalette.ColorRole.HighlightedText, factory.selection_color_ ) QtGui.QApplication.style().drawControl( QtGui.QStyle.ControlElement.CE_ItemViewItem, option, painter, None ) class TableView(QtGui.QTableView): """A QTableView configured to behave as expected by TraitsUI.""" _SELECTION_MAP = { "row": ( QtGui.QAbstractItemView.SelectionBehavior.SelectRows, QtGui.QAbstractItemView.SelectionMode.SingleSelection, ), "rows": ( QtGui.QAbstractItemView.SelectionBehavior.SelectRows, QtGui.QAbstractItemView.SelectionMode.ExtendedSelection, ), "column": ( QtGui.QAbstractItemView.SelectionBehavior.SelectColumns, QtGui.QAbstractItemView.SelectionMode.SingleSelection, ), "columns": ( QtGui.QAbstractItemView.SelectionBehavior.SelectColumns, QtGui.QAbstractItemView.SelectionMode.ExtendedSelection, ), "cell": ( QtGui.QAbstractItemView.SelectionBehavior.SelectItems, QtGui.QAbstractItemView.SelectionMode.SingleSelection, ), "cells": ( QtGui.QAbstractItemView.SelectionBehavior.SelectItems, QtGui.QAbstractItemView.SelectionMode.ExtendedSelection, ), } def __init__(self, editor): """Initialise the object.""" QtGui.QTableView.__init__(self) self._initial_size = False self._editor = editor factory = editor.factory # Configure the grid lines. self.setShowGrid(factory.show_lines) # Configure the selection behaviour. self.setCornerButtonEnabled(False) behav, mode = self._SELECTION_MAP[factory.selection_mode] self.setSelectionBehavior(behav) self.setSelectionMode(mode) # Configure the editing behavior. triggers = ( QtGui.QAbstractItemView.EditTrigger.DoubleClicked | QtGui.QAbstractItemView.EditTrigger.SelectedClicked ) if factory.edit_on_first_click and not factory.reorderable: triggers |= QtGui.QAbstractItemView.EditTrigger.CurrentChanged self.setEditTriggers(triggers) # Configure the reordering and sorting behavior. self.setDragEnabled(True) self.viewport().setAcceptDrops(True) self.setDropIndicatorShown(True) if factory.reorderable: self.setDragDropMode(QtGui.QAbstractItemView.DragDropMode.InternalMove) if factory.sortable: self.setSortingEnabled(True) if factory._qt_stylesheet is not None: self.setStyleSheet(factory._qt_stylesheet) self.resizeColumnsToContents() def setModel(self, model): super().setModel(model) self._update_header_sizing() def contextMenuEvent(self, event): """Reimplemented to create context menus for cells and empty space.""" # Determine the logical indices of the cell where click occured hheader, vheader = self.horizontalHeader(), self.verticalHeader() position = event.globalPos() row = vheader.logicalIndexAt(vheader.mapFromGlobal(position)) column = hheader.logicalIndexAt(hheader.mapFromGlobal(position)) # Map the logical row index to a real index for the source model model = self.model() row = model.mapToSource(model.index(row, 0)).row() # Show a context menu for empty space at bottom of table... editor = self._editor if row == -1: empty_menu = editor._create_empty_menu() if empty_menu is not None: event.accept() empty_menu.exec_(position) # ...or show a context menu for a cell. elif column != -1: obj = editor.items()[row] column = editor.columns[column] menu_manager = column.get_menu(obj) if menu_manager is None: menu_manager = editor.factory.menu if menu_manager is not None: event.accept() selected = editor.selected if not isinstance(selected, SequenceTypes): selected = [selected] if obj not in selected: selected = [obj] editor.set_menu_context(selected, obj, column) menu = menu_manager.create_menu(self, controller=editor) menu.exec_(position) def eventFilter(self, obj, event): """Reimplemented to create context menu for the vertical header.""" vheader = self.verticalHeader() if obj is vheader and event.type() == QtCore.QEvent.Type.ContextMenu: position = event.globalPos() editor = self._editor row = vheader.logicalIndexAt(event.pos().y()) if row == -1: empty_menu = editor._create_empty_menu() if empty_menu is not None: event.accept() empty_menu.exec_(position) else: editor.header_row = row header_menu = editor._create_header_menu() if header_menu is not None: event.accept() header_menu.exec_(position) return True else: return QtGui.QTableView.eventFilter(self, obj, event) def resizeEvent(self, event): """Reimplemented to size the table columns when the size of the table changes. Because the layout algorithm requires that the available space be known, we have to wait until the UI that contains this table gives it its initial size.""" QtGui.QTableView.resizeEvent(self, event) if self._editor.auto_size: self.resizeColumnsToContents() self.resizeRowsToContents() else: parent = self.parent() if ( not self._initial_size and parent and (self.isVisible() or isinstance(parent, QtGui.QMainWindow)) ): self._initial_size = True if self._editor.auto_size: self.resizeColumnsToContents() self.resizeRowsToContents() def sizeHint(self): """Reimplemented to define a better size hint for the width of the TableEditor.""" size_hint = QtGui.QTableView.sizeHint(self) # This method is sometimes called by Qt after the editor has been # disposed but before this control has been deleted: if self._editor.factory is None: return size_hint width = self.style().pixelMetric( QtGui.QStyle.PixelMetric.PM_ScrollBarExtent, QtGui.QStyleOptionHeader(), self ) for column in range(len(self._editor.columns)): width += self.sizeHintForColumn(column) size_hint.setWidth(width) return size_hint def sizeHintForColumn(self, column_index): """Reimplemented to support absolute width specification via TableColumns and to improve the metric for autosizing columns.""" editor = self._editor column = editor.columns[column_index] requested_width = column.get_width() # Autosize based on column contents and label width. Qt's default # implementation of this function does content, we handle the label. if requested_width < 1: base_width = QtGui.QTableView.sizeHintForColumn(self, column_index) # Determine what font to use in the calculation font = column.get_text_font(None) if font is None: font = self.font() font.setBold(True) else: font = QtGui.QFont(font) # Determine the width of the column label text = column.get_label() # QFontMetrics.width() is deprecated and Qt docs suggest using # horizontalAdvance() instead, but is only available since Qt 5.11 if QtCore.__version_info__ >= (5, 11): width = QtGui.QFontMetrics(font).horizontalAdvance(text) else: width = QtGui.QFontMetrics(font).width(text) # Add margin to the calculated width as appropriate style = self.style() option = QtGui.QStyleOptionHeader() width += ( style.pixelMetric( QtGui.QStyle.PixelMetric.PM_HeaderGripMargin, option, self ) * 2 ) if editor.factory.sortable and not editor.factory.reorderable: # Add size of sort indicator width += style.pixelMetric( QtGui.QStyle.PixelMetric.PM_HeaderMarkSize, option, self ) # Add distance between sort indicator and text width += style.pixelMetric( QtGui.QStyle.PixelMetric.PM_HeaderMargin, option, self ) return max(base_width, width) # Or else set width absolutely else: return requested_width def resizeColumnsToContents(self): """Support proportional column width specifications.""" # TODO: The proportional size specification approach found in the # TableColumns is not entirely compatible with the ability to # specify the resize_mode. Namely, there are combinations of # specifications that are redundant, and others which are # contradictory. Rework this method so that the various values # for **width** have a well-defined, sensible meaning for each # of the possible values of resize_mode. editor = self._editor available_space = self.viewport().width() hheader = self.horizontalHeader() # Compute sizes for columns with absolute or no size requests proportional = [] for column_index in range(len(editor.columns)): column = editor.columns[column_index] requested_width = column.get_width() if ( column.resize_mode in ("interactive", "stretch") and 0 < requested_width <= 1.0 ): proportional.append((column_index, requested_width)) elif ( column.resize_mode == "interactive" and requested_width < 0 and self._initial_size ): # Keep previous size if initial sizing has been done available_space -= hheader.sectionSize(column_index) else: base_width = hheader.sectionSizeHint(column_index) width = max(base_width, self.sizeHintForColumn(column_index)) hheader.resizeSection(column_index, width) available_space -= width # Now use the remaining space for columns with proportional width # requests for column_index, percent in proportional: base_width = hheader.sectionSizeHint(column_index) width = max(base_width, int(percent * available_space)) hheader.resizeSection(column_index, width) def closeEditor(self, control, hint): # dispose traits editor associated with control if any editor = getattr(control, "_editor", None) if editor is not None: editor.dispose() delattr(control, "_editor") return super().closeEditor(control, hint) def _update_header_sizing(self): """Header section sizing can be done only after a valid model is set. Otherwise results in segfault with Qt5. """ editor = self._editor factory = editor.factory # Configure the row headings. vheader = self.verticalHeader() set_resize_mode = set_qheader_section_resize_mode(vheader) insertable = factory.row_factory is not None if ( factory.editable and (insertable or factory.deletable) ) or factory.reorderable: vheader.installEventFilter(self) set_resize_mode(QtGui.QHeaderView.ResizeMode.ResizeToContents) elif not factory.show_row_labels: vheader.hide() if factory.row_height > 0: vheader.setDefaultSectionSize(factory.row_height) self.setAlternatingRowColors(factory.alternate_bg_color) self.setHorizontalScrollMode(QtGui.QAbstractItemView.ScrollMode.ScrollPerPixel) # Configure the column headings. # We detect if there are any stretchy sections at all; if not, then # we make the last non-fixed-size column stretchy. hheader = self.horizontalHeader() set_resize_mode = set_qheader_section_resize_mode(hheader) resize_mode_map = dict( interactive=QtGui.QHeaderView.ResizeMode.Interactive, fixed=QtGui.QHeaderView.ResizeMode.Fixed, stretch=QtGui.QHeaderView.ResizeMode.Stretch, resize_to_contents=QtGui.QHeaderView.ResizeMode.ResizeToContents, ) stretchable_columns = [] for i, column in enumerate(editor.columns): set_resize_mode(i, resize_mode_map[column.resize_mode]) if column.resize_mode in ("stretch", "interactive"): stretchable_columns.append(i) if not stretchable_columns: # Use the behavior from before the "resize_mode" trait was added # to TableColumn hheader.setStretchLastSection(True) else: # hheader.setSectionResizeMode( # stretchable_columns[-1], QtGui.QHeaderView.ResizeMode.Stretch) hheader.setStretchLastSection(False) if factory.show_column_labels: hheader.setHighlightSections(False) else: hheader.hide() # ------------------------------------------------------------------------- # Editor for configuring the filters available to a TableEditor: # ------------------------------------------------------------------------- class TableFilterEditor(HasTraits): """An editor that manages table filters.""" # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- #: TableEditor this editor is associated with editor = Instance(TableEditor) #: The list of filters filters = List(TableFilter) #: The list of available templates from which filters can be created templates = Property(List(TableFilter), observe="filters") #: The currently selected filter template selected_template = Instance(TableFilter) #: The currently selected filter selected_filter = Instance(TableFilter, allow_none=True) #: The view to use for the current filter selected_filter_view = Property(observe="selected_filter") #: Buttons for add/removing filters add_button = Button("New") remove_button = Button("Delete") # The default view for this editor view = View( Group( Group( Group( Item("add_button", enabled_when="selected_template"), Item( "remove_button", enabled_when="len(templates) > 1 and " "selected_filter is not None", ), orientation="horizontal", show_labels=False, ), Label("Base filter for new filters:"), Item("selected_template", editor=EnumEditor(name="templates")), Item( "selected_filter", style="custom", editor=EnumEditor(name="filters", mode="list"), ), show_labels=False, ), Item( "selected_filter", width=0.75, style="custom", editor=InstanceEditor(view_name="selected_filter_view"), ), id="TableFilterEditorSplit", show_labels=False, layout="split", orientation="horizontal", ), id="traitsui.qt.table_editor.TableFilterEditor", buttons=["OK", "Cancel"], kind="livemodal", resizable=True, width=800, height=400, title="Customize filters", ) # ------------------------------------------------------------------------- # Private methods: # ------------------------------------------------------------------------- # -- Trait Property getter/setters ---------------------------------------- @cached_property def _get_selected_filter_view(self): view = None if self.selected_filter: model = self.editor.model index = model.mapToSource(model.index(0, 0)) if index.isValid(): obj = self.editor.items()[index.row()] else: obj = None view = self.selected_filter.edit_view(obj) return view @cached_property def _get_templates(self): templates = [f for f in self.editor.factory.filters if f.template] templates.extend(self.filters) return templates # -- Trait Change Handlers ------------------------------------------------ def _editor_changed(self): self.filters = [ f.clone_traits() for f in self.editor.factory.filters if not f.template ] self.selected_template = self.templates[0] @observe('add_button') def _create_and_select_new_filter(self, event): """Create a new filter based on the selected template and select it.""" new_filter = self.selected_template.clone_traits() new_filter.template = False new_filter.name = new_filter._name = "New filter" self.filters.append(new_filter) self.selected_filter = new_filter @observe("remove_button") def _delete_selected_filter(self, event): """Delete the currently selected filter.""" if self.selected_template == self.selected_filter: self.selected_template = self.templates[0] index = self.filters.index(self.selected_filter) del self.filters[index] if index < len(self.filters): self.selected_filter = self.filters[index] else: self.selected_filter = None @observe("selected_filter:name") def _update_filter_list(self, event): """A hack to make the EnumEditor watching the list of filters refresh their text when the name of the selected filter changes. """ filters = self.filters self.filters = [] self.filters = filters ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/qt/table_model.py0000644000175100001730000004131300000000000021300 0ustar00runnerdocker00000000000000# (C) Copyright 2009-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # ------------------------------------------------------------------------------ # Copyright (c) 2008, Riverbank Computing Limited # All rights reserved. # # This software is provided without warranty under the terms of the BSD license. # However, when used with the GPL version of PyQt the additional terms described # in the PyQt GPL exception also apply. # # Author: Riverbank Computing Limited # ------------------------------------------------------------------------------ """ Defines the table model used by the table editor. """ import logging from pyface.qt import QtCore, QtGui from traitsui.ui_traits import SequenceTypes from .clipboard import PyMimeData # set up logging for the module logger = logging.getLogger(__name__) # Mapping for trait alignment values to qt horizontal alignment constants h_alignment_map = { "left": QtCore.Qt.AlignmentFlag.AlignLeft, "center": QtCore.Qt.AlignmentFlag.AlignHCenter, "right": QtCore.Qt.AlignmentFlag.AlignRight, } # Mapping for trait alignment values to qt vertical alignment constants v_alignment_map = { "top": QtCore.Qt.AlignmentFlag.AlignTop, "center": QtCore.Qt.AlignmentFlag.AlignVCenter, "bottom": QtCore.Qt.AlignmentFlag.AlignBottom, } # MIME type for internal table drag/drop operations mime_type = "traits-ui-table-editor" def as_qcolor(color): """Convert a color specification (maybe a tuple) into a QColor.""" if isinstance(color, SequenceTypes): return QtGui.QColor(*color) else: return QtGui.QColor(color) class TableModel(QtCore.QAbstractTableModel): """The model for table data.""" def __init__(self, editor, parent=None): """Initialise the object.""" QtCore.QAbstractTableModel.__init__(self, parent) self._editor = editor # ------------------------------------------------------------------------- # QAbstractTableModel interface: # ------------------------------------------------------------------------- def rowCount(self, mi): """Reimplemented to return the number of rows.""" return len(self._editor.items()) def columnCount(self, mi): """Reimplemented to return the number of columns.""" return len(self._editor.columns) def data(self, mi, role): """Reimplemented to return the data.""" obj = self._editor.items()[mi.row()] column = self._editor.columns[mi.column()] if self._editor.factory is None: # XXX This should never happen, but it does, # probably during shutdown, but I haven't investigated return None if role == QtCore.Qt.ItemDataRole.DisplayRole or role == QtCore.Qt.ItemDataRole.EditRole: text = column.get_value(obj) if text is not None: return text elif role == QtCore.Qt.ItemDataRole.DecorationRole: image = self._editor._get_image(column.get_image(obj)) if image is not None: return image elif role == QtCore.Qt.ItemDataRole.ToolTipRole: tooltip = column.get_tooltip(obj) if tooltip: return tooltip elif role == QtCore.Qt.ItemDataRole.FontRole: font = column.get_text_font(obj) if font is None: font = self._editor.factory.cell_font if font is not None: return QtGui.QFont(font) elif role == QtCore.Qt.ItemDataRole.TextAlignmentRole: string = column.get_horizontal_alignment(obj) h_alignment = h_alignment_map.get(string, QtCore.Qt.AlignmentFlag.AlignLeft) string = column.get_vertical_alignment(obj) v_alignment = v_alignment_map.get(string, QtCore.Qt.AlignmentFlag.AlignVCenter) return int(h_alignment | v_alignment) elif role == QtCore.Qt.ItemDataRole.BackgroundRole: color = column.get_cell_color(obj) if color is None: if column.is_editable(obj): color = self._editor.factory.cell_bg_color_ else: color = self._editor.factory.cell_read_only_bg_color_ if color is None: # FIXME: Yes, this is weird. It should work fine to fall through # to the catch-all None at the end, but it doesn't. return None q_color = as_qcolor(color) return QtGui.QBrush(q_color) elif role == QtCore.Qt.ItemDataRole.ForegroundRole: color = column.get_text_color(obj) if color is None: color = self._editor.factory.cell_color_ if color is not None: q_color = as_qcolor(color) return QtGui.QBrush(q_color) elif role == QtCore.Qt.ItemDataRole.UserRole: return obj elif role == QtCore.Qt.ItemDataRole.CheckStateRole: if column.get_type(obj) == "bool" and column.show_checkbox: if column.get_raw_value(obj): return QtCore.Qt.CheckState.Checked else: return QtCore.Qt.CheckState.Unchecked return None def flags(self, mi): """Reimplemented to set editable and movable status.""" editor = self._editor if not mi.isValid(): if editor.factory.reorderable: return QtCore.Qt.ItemFlag.ItemIsDropEnabled else: return QtCore.Qt.ItemFlag.NoItemFlags flags = ( QtCore.Qt.ItemFlag.ItemIsSelectable | QtCore.Qt.ItemFlag.ItemIsEnabled | QtCore.Qt.ItemFlag.ItemIsDragEnabled ) obj = editor.items()[mi.row()] column = editor.columns[mi.column()] if editor.factory: if editor.factory.editable and column.is_editable(obj): flags |= QtCore.Qt.ItemFlag.ItemIsEditable | QtCore.Qt.ItemFlag.ItemIsDropEnabled if editor.factory.reorderable: flags |= QtCore.Qt.ItemFlag.ItemIsDropEnabled if column.get_type(obj) == "bool" and column.show_checkbox: flags |= QtCore.Qt.ItemFlag.ItemIsUserCheckable return flags def headerData(self, section, orientation, role): """Reimplemented to return the header data.""" editor = self._editor if orientation == QtCore.Qt.Orientation.Horizontal: column = editor.columns[section] if role == QtCore.Qt.ItemDataRole.DisplayRole: return column.get_label() elif orientation == QtCore.Qt.Orientation.Vertical: if role == QtCore.Qt.ItemDataRole.DisplayRole: return str(section + 1) if editor.factory is None: # XXX This should never happen, but it does, # probably during shutdown, but I haven't investigated return None if role == QtCore.Qt.ItemDataRole.FontRole: font = editor.factory.label_font if font is not None: return QtGui.QFont(font) elif role == QtCore.Qt.ItemDataRole.BackgroundRole: color = editor.factory.label_bg_color_ if color is not None: return color elif role == QtCore.Qt.ItemDataRole.ForegroundRole: color = editor.factory.label_color_ if color is not None: return color return None def insertRow(self, row, parent=QtCore.QModelIndex(), obj=None): """Reimplemented to allow creation of new rows. Added an optional arg to allow the insertion of an existing row object.""" editor = self._editor if obj is None: obj = editor.create_new_row() self.beginInsertRows(parent, row, row) editor.callx(editor.items().insert, row, obj) self.endInsertRows() return True def insertRows(self, row, count, parent=QtCore.QModelIndex()): """Reimplemented to allow creation of new rows.""" editor = self._editor items = editor.items() self.beginInsertRows(parent, row, row + count - 1) for i in range(count): editor.callx(items.insert, row + i, editor.create_new_row()) self.endInsertRows() return True def removeRows(self, row, count, parent=QtCore.QModelIndex()): """Reimplemented to allow row deletion, as well as reordering via drag and drop.""" editor = self._editor items = editor.items() self.beginRemoveRows(parent, row, row + count - 1) for i in range(count): editor.callx(items.pop, row + i) self.endRemoveRows() return True def mimeTypes(self): """Reimplemented to expose our internal MIME type for drag and drop operations.""" return [mime_type, PyMimeData.MIME_TYPE, PyMimeData.NOPICKLE_MIME_TYPE] def mimeData(self, indexes): """Reimplemented to generate MIME data containing the rows of the current selection.""" editor = self._editor selection_mode = editor.factory.selection_mode if selection_mode.startswith("cell"): data = [ self._get_cell_drag_value(index.row(), index.column()) for index in indexes ] elif selection_mode.startswith("column"): columns = sorted(set(index.column() for index in indexes)) data = self._get_columns_drag_value(columns) else: rows = sorted(set(index.row() for index in indexes)) data = self._get_rows_drag_value(rows) mime_data = PyMimeData.coerce(data) # handle re-ordering via internal drags if editor.factory.reorderable: rows = sorted({index.row() for index in indexes}) data = QtCore.QByteArray(str(id(self)).encode("utf8")) for row in rows: data.append((" %i" % row).encode("utf8")) mime_data.setData(mime_type, data) return mime_data def dropMimeData(self, mime_data, action, row, column, parent): """Reimplemented to allow items to be moved.""" if action == QtCore.Qt.DropAction.IgnoreAction: return False # this is a drag from a table model? data = mime_data.data(mime_type) if not data.isNull() and action == QtCore.Qt.DropAction.MoveAction: id_and_rows = [ int(s) for s in data.data().decode("utf8").split(" ") ] table_id = id_and_rows[0] # is it from ourself? if table_id == id(self): current_rows = id_and_rows[1:] if not parent.isValid(): row = len(self._editor.items()) - 1 else: row = parent.row() self.moveRows(current_rows, row) return True data = PyMimeData.coerce(mime_data).instance() if data is not None: editor = self._editor if row == -1 and column == -1 and parent.isValid(): row = parent.row() column = parent.column() if row != -1 and column != -1: object = editor.items()[row] column = editor.columns[column] if column.is_droppable(object, data): column.set_value(object, data) return True return False def supportedDropActions(self): """Reimplemented to allow items to be moved.""" return QtCore.Qt.DropAction.MoveAction # ------------------------------------------------------------------------- # Utility methods # ------------------------------------------------------------------------- def _get_columns_drag_value(self, columns): """Returns the value to use when the specified columns are dragged or copied and pasted. The parameter *cols* is a list of column indexes. """ return [self._get_column_data(column) for column in columns] def _get_column_data(self, column): """Return the model data for the column as a list""" editor = self._editor column_obj = editor.columns[column] return [column_obj.get_value(item) for item in editor.items()] def _get_rows_drag_value(self, rows): """Returns the value to use when the specified rows are dragged or copied and pasted. The parameter *rows* is a list of row indexes. Return a list of objects. """ items = self._editor.items() return [items[row] for row in rows] def _get_cell_drag_value(self, row, column): """Returns the value to use when the specified cell is dragged or copied and pasted. """ editor = self._editor item = editor.items()[row] drag_value = editor.columns[column].get_drag_value(item) return drag_value # ------------------------------------------------------------------------- # TableModel interface: # ------------------------------------------------------------------------- def moveRow(self, old_row, new_row): """Convenience method to move a single row.""" return self.moveRows([old_row], new_row) def moveRows(self, current_rows, new_row): """Moves a sequence of rows (provided as a list of row indexes) to a new row.""" # Sort rows in descending order so they can be removed without # invalidating the indices. current_rows.sort() current_rows.reverse() # If the the highest selected row is lower than the destination, do an # insertion before rather than after the destination. if current_rows[-1] < new_row: new_row += 1 # Remove selected rows... items = self._editor.items() objects = [] for row in current_rows: if row <= new_row: new_row -= 1 objects.insert(0, items[row]) self.removeRow(row) # ...and add them at the new location. for i, obj in enumerate(objects): self.insertRow(new_row + i, obj=obj) # Update the selection for the new location. self._editor.set_selection(objects) class SortFilterTableModel(QtGui.QSortFilterProxyModel): """A wrapper for the TableModel which provides sorting and filtering capability.""" def __init__(self, editor, parent=None): """Initialise the object.""" QtGui.QSortFilterProxyModel.__init__(self, parent) self._editor = editor # ------------------------------------------------------------------------- # QSortFilterProxyModel interface: # ------------------------------------------------------------------------- def filterAcceptsRow(self, source_row, source_parent): """ "Reimplemented to use a TableFilter for filtering rows.""" if self._editor._filtered_cache is None: return True else: return self._editor._filtered_cache[source_row] def filterAcceptsColumn(self, source_column, source_parent): """Reimplemented to save time, because we always return True.""" return True def lessThan(self, left_mi, right_mi): """Reimplemented to sort according to the 'key' method defined for TableColumn.""" try: editor = self._editor column = editor.columns[left_mi.column()] items = editor.items() left, right = items[left_mi.row()], items[right_mi.row()] return column.key(left) < column.key(right) except Exception as exc: logger.exception(exc) # This will almost certainly segfault, but there does not seem to # be anything sensible we can do. raise # ------------------------------------------------------------------------- # SortFilterTableModel interface: # ------------------------------------------------------------------------- def moveRow(self, old_row, new_row): """Convenience method to move a single row.""" return self.moveRows([old_row], new_row) def moveRows(self, current_rows, new_row): """Delegate to source model with mapped rows.""" source = self.sourceModel() current_rows = [ self.mapToSource(self.index(row, 0)).row() for row in current_rows ] new_row = self.mapToSource(self.index(new_row, 0)).row() source.moveRows(current_rows, new_row) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/qt/tabular_editor.py0000644000175100001730000010000000000000000022016 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ A traits UI editor for editing tabular data (arrays, list of tuples, lists of objects, etc). """ from contextlib import contextmanager from pyface.qt import QtCore, QtGui, is_qt4 from pyface.image_resource import ImageResource from pyface.ui_traits import Image from traits.api import ( Any, Bool, Callable, Dict, Event, HasStrictTraits, Instance, Int, List, NO_COMPARE, Property, TraitListEvent, ) from traitsui.tabular_adapter import TabularAdapter from traitsui.helper import compute_column_widths from .editor import Editor from .tabular_model import TabularModel SCROLL_TO_POSITION_HINT_MAP = { "center": QtGui.QTableView.ScrollHint.PositionAtCenter, "top": QtGui.QTableView.ScrollHint.PositionAtTop, "bottom": QtGui.QTableView.ScrollHint.PositionAtBottom, "visible": QtGui.QTableView.ScrollHint.EnsureVisible, } class HeaderEventFilter(QtCore.QObject): def __init__(self, editor): super().__init__() self.editor = editor def eventFilter(self, obj, event): if event.type() == QtCore.QEvent.Type.ContextMenu: self.editor._on_column_context_menu(event.pos()) return True return False class TabularEditor(Editor): """A traits UI editor for editing tabular data (arrays, list of tuples, lists of objects, etc). """ # -- Trait Definitions ---------------------------------------------------- #: The event fired when a table update is needed: update = Event() #: The event fired when a simple repaint is needed: refresh = Event() #: The current set of selected items (which one is used depends upon the #: initial state of the editor factory 'multi_select' trait): selected = Any() multi_selected = List() #: The current set of selected item indices (which one is used depends upon #: the initial state of the editor factory 'multi_select' trait): selected_row = Int(-1) multi_selected_rows = List(Int) #: The optional extended name of the trait to synchronize the selection #: column with: selected_column = Int(-1) #: The most recently actived item and its index: activated = Any(comparison_mode=NO_COMPARE) activated_row = Int(comparison_mode=NO_COMPARE) #: The most recent left click data: clicked = Instance("TabularEditorEvent") #: The most recent left double click data: dclicked = Instance("TabularEditorEvent") #: The most recent right click data: right_clicked = Instance("TabularEditorEvent") #: The most recent right double click data: right_dclicked = Instance("TabularEditorEvent") #: The most recent column click data: column_clicked = Instance("TabularEditorEvent") #: The most recent column click data: column_right_clicked = Instance("TabularEditorEvent") #: The event triggering scrolling. scroll_to_row = Event(Int) #: The event triggering scrolling. scroll_to_column = Event(Int) #: Is the tabular editor scrollable? This value overrides the default. scrollable = True #: NIT: This doesn't seem to be used anywhere...can I delete? #: # Row index of item to select after rebuilding editor list: #: row = Any() #: Should the selected item be edited after rebuilding the editor list: edit = Bool(False) #: The adapter from trait values to editor values: adapter = Instance(TabularAdapter) #: The table model associated with the editor: model = Instance(TabularModel) #: Dictionary mapping image names to QIcons images = Dict() #: Dictionary mapping ImageResource objects to QIcons image_resources = Dict() #: An image being converted: image = Image header_event_filter = Any() widget_factory = Callable(lambda *args, **kwds: _TableView(*args, **kwds)) # ------------------------------------------------------------------------- # Editor interface: # ------------------------------------------------------------------------- def init(self, parent): """Finishes initializing the editor by creating the underlying toolkit widget. """ factory = self.factory adapter = self.adapter = factory.adapter self.model = TabularModel(editor=self) # Create the control control = self.control = self.widget_factory(self) # Set up the selection listener if factory.multi_select: self.sync_value( factory.selected, "multi_selected", "both", is_list=True ) self.sync_value( factory.selected_row, "multi_selected_rows", "both", is_list=True, ) else: self.sync_value(factory.selected, "selected", "both") self.sync_value(factory.selected_row, "selected_row", "both") # Connect to the mode specific selection handler if factory.multi_select: slot = self._on_rows_selection else: slot = self._on_row_selection selection_model = self.control.selectionModel() selection_model.selectionChanged.connect(slot) # Synchronize other interesting traits as necessary: self.sync_value(factory.update, "update", "from", is_event=True) self.sync_value(factory.refresh, "refresh", "from", is_event=True) self.sync_value(factory.activated, "activated", "to") self.sync_value(factory.activated_row, "activated_row", "to") self.sync_value(factory.clicked, "clicked", "to") self.sync_value(factory.dclicked, "dclicked", "to") self.sync_value(factory.right_clicked, "right_clicked", "to") self.sync_value(factory.right_dclicked, "right_dclicked", "to") self.sync_value(factory.column_clicked, "column_clicked", "to") self.sync_value( factory.column_right_clicked, "column_right_clicked", "to" ) self.sync_value( factory.scroll_to_row, "scroll_to_row", "from", is_event=True ) self.sync_value( factory.scroll_to_column, "scroll_to_column", "from", is_event=True ) # Connect other signals as necessary control.activated.connect(self._on_activate) control.clicked.connect(self._on_click) control.clicked.connect(self._on_right_click) control.doubleClicked.connect(self._on_dclick) control.horizontalHeader().sectionClicked.connect( self._on_column_click ) control.setContextMenuPolicy(QtCore.Qt.ContextMenuPolicy.CustomContextMenu) control.customContextMenuRequested.connect(self._on_context_menu) self.header_event_filter = HeaderEventFilter(self) control.horizontalHeader().installEventFilter(self.header_event_filter) # Make sure we listen for 'items' changes as well as complete list # replacements: try: self.context_object.on_trait_change( self.update_editor, self.extended_name + "_items", dispatch="ui", ) except: pass # If the user has requested automatic update, attempt to set up the # appropriate listeners: if factory.auto_update: self.context_object.on_trait_change( self.refresh_editor, self.extended_name + ".-", dispatch="ui" ) # Create the mapping from user supplied images to QImages: for image_resource in factory.images: self._add_image(image_resource) # Refresh the editor whenever the adapter changes: self.on_trait_change( self.refresh_editor, "adapter.+update", dispatch="ui" ) # Rebuild the editor columns and headers whenever the adapter's # 'columns' changes: self.on_trait_change( self._adapter_columns_updated, "adapter.columns", dispatch="ui" ) def dispose(self): """Disposes of the contents of an editor.""" self.model.beginResetModel() self.model.endResetModel() self.context_object.on_trait_change( self.update_editor, self.extended_name + "_items", remove=True ) if self.factory.auto_update: self.context_object.on_trait_change( self.refresh_editor, self.extended_name + ".-", remove=True ) self.on_trait_change( self.refresh_editor, "adapter.+update", remove=True ) self.on_trait_change( self._adapter_columns_updated, "adapter.columns", remove=True ) self.adapter.cleanup() super().dispose() def update_editor(self): """Updates the editor when the object trait changes externally to the editor. """ if not self._no_update: self.model.beginResetModel() self.model.endResetModel() if self.factory.multi_select: self._multi_selected_changed(self.multi_selected) else: self._selected_changed(self.selected) # ------------------------------------------------------------------------- # TabularEditor interface: # ------------------------------------------------------------------------- def refresh_editor(self): """Requests the table view to redraw itself.""" self.control.viewport().update() def callx(self, func, *args, **kw): """Call a function without allowing the editor to update.""" old = self._no_update self._no_update = True try: func(*args, **kw) finally: self._no_update = old def setx(self, **keywords): """Set one or more attributes without allowing the editor to update.""" old = self._no_notify self._no_notify = True try: for name, value in keywords.items(): setattr(self, name, value) finally: self._no_notify = old # ------------------------------------------------------------------------- # UI preference save/restore interface: # ------------------------------------------------------------------------- def restore_prefs(self, prefs): """Restores any saved user preference information associated with the editor. """ cws = prefs.get("cached_widths") num_columns = len(self.adapter.columns) if cws is not None and num_columns == len(cws): for column in range(num_columns): self.control.setColumnWidth(column, int(cws[column])) def save_prefs(self): """Returns any user preference information associated with the editor.""" widths = [ self.control.columnWidth(column) for column in range(len(self.adapter.columns)) ] return {"cached_widths": widths} # ------------------------------------------------------------------------- # Private methods: # ------------------------------------------------------------------------- def _add_image(self, image_resource): """Adds a new image to the image map.""" image = image_resource.create_icon() self.image_resources[image_resource] = image self.images[image_resource.name] = image return image def _get_image(self, image): """Converts a user specified image to a QIcon.""" if isinstance(image, str): self.image = image image = self.image if isinstance(image, ImageResource): result = self.image_resources.get(image) if result is not None: return result return self._add_image(image) return self.images.get(image) def _mouse_click(self, index, trait): """Generate a TabularEditorEvent event for a specified model index and editor trait name. """ event = TabularEditorEvent( editor=self, row=index.row(), column=index.column() ) setattr(self, trait, event) # -- Trait Event Handlers ------------------------------------------------- def _clicked_changed(self): """When mouse is clicked on a specific cell, update the selected indices first """ if not self.factory.multi_select: self.selected_row = self.clicked.row self.selected_column = self.clicked.column def _column_clicked_changed(self): """When column is clicked, update selected column first""" if not self.factory.multi_select: self.selected_column = self.column_clicked.column def _adapter_columns_updated(self): """Update the view when the adapter columns trait changes. Note that this change handler is added after the UI is instantiated, and removed when the UI is disposed. """ # Invalidate internal state of the view related to the columns n_columns = len(self.adapter.columns) if ( self.control is not None and self.control._user_widths is not None and len(self.control._user_widths) != n_columns ): self.control._user_widths = None self.update_editor() def _update_changed(self): self.update_editor() def _refresh_changed(self): self.refresh_editor() def _selected_changed(self, new): if not self._no_update: if new is None: self._selected_row_changed(-1) else: try: selected_row = self.value.index(new) except Exception: from traitsui.api import raise_to_debug raise_to_debug() else: self._selected_row_changed(selected_row) def _selected_row_changed(self, selected_row): if not self._no_update: smodel = self.control.selectionModel() if selected_row < 0: smodel.clearSelection() else: smodel.select( self.model.index( selected_row, max(self.selected_column, 0) ), QtGui.QItemSelectionModel.SelectionFlag.ClearAndSelect | QtGui.QItemSelectionModel.SelectionFlag.Rows, ) # Once selected, scroll to the row self.scroll_to_row = selected_row def _multi_selected_changed(self, new): if not self._no_update: values = self.value try: rows = [values.index(i) for i in new] except: pass else: self._multi_selected_rows_changed(rows) def _multi_selected_items_changed(self, event): values = self.value try: added = [values.index(item) for item in event.added] removed = [values.index(item) for item in event.removed] except: pass else: list_event = TraitListEvent(index=0, added=added, removed=removed) self._multi_selected_rows_items_changed(list_event) def _multi_selected_rows_changed(self, selected_rows): if not self._no_update: smodel = self.control.selectionModel() selection = QtGui.QItemSelection() for row in selected_rows: selection.select( self.model.index(row, 0), self.model.index(row, 0) ) smodel.clearSelection() smodel.select( selection, QtGui.QItemSelectionModel.SelectionFlag.Select | QtGui.QItemSelectionModel.SelectionFlag.Rows, ) def _multi_selected_rows_items_changed(self, event): if not self._no_update: smodel = self.control.selectionModel() for row in event.removed: smodel.select( self.model.index(row, 0), QtGui.QItemSelectionModel.SelectionFlag.Deselect | QtGui.QItemSelectionModel.SelectionFlag.Rows, ) for row in event.added: smodel.select( self.model.index(row, 0), QtGui.QItemSelectionModel.SelectionFlag.Select | QtGui.QItemSelectionModel.SelectionFlag.Rows, ) def _selected_column_changed(self, selected_column): if not self._no_update: smodel = self.control.selectionModel() if selected_column >= 0: smodel.select( self.model.index( max(self.selected_row, 0), selected_column ), QtGui.QItemSelectionModel.SelectionFlag.ClearAndSelect | QtGui.QItemSelectionModel.SelectionFlag.Rows, ) # Once selected, scroll to the column self.scroll_to_column = selected_column def _scroll_to_row_changed(self, row): """Scroll to the given row.""" scroll_hint = SCROLL_TO_POSITION_HINT_MAP.get( self.factory.scroll_to_position_hint, self.control.ScrollHint.EnsureVisible ) self.control.scrollTo( self.model.index(row, max(self.selected_column, 0)), scroll_hint ) def _scroll_to_column_changed(self, column): """Scroll to the given column.""" scroll_hint = SCROLL_TO_POSITION_HINT_MAP.get( self.factory.scroll_to_position_hint, self.control.ScrollHint.EnsureVisible ) self.control.scrollTo( self.model.index(max(self.selected_row, 0), column), scroll_hint ) # -- Table Control Event Handlers ----------------------------------------- def _on_activate(self, index): """Handle a cell being activated.""" self.activated_row = row = index.row() self.activated = self.adapter.get_item(self.object, self.name, row) def _on_click(self, index): """Handle a cell being clicked.""" self._mouse_click(index, "clicked") def _on_dclick(self, index): """Handle a cell being double clicked.""" self._mouse_click(index, "dclicked") def _on_column_click(self, column): event = TabularEditorEvent(editor=self, row=0, column=column) setattr(self, "column_clicked", event) def _on_right_click(self, column): event = TabularEditorEvent(editor=self, row=0, column=column) setattr(self, "right_clicked", event) def _on_column_right_click(self, column): event = TabularEditorEvent(editor=self, row=0, column=column) setattr(self, "column_right_clicked", event) def _on_row_selection(self, added, removed): """Handle the row selection being changed.""" self._no_update = True try: indexes = self.control.selectionModel().selectedRows() if len(indexes): self.selected_row = indexes[0].row() self.selected = self.adapter.get_item( self.object, self.name, self.selected_row ) else: self.selected_row = -1 self.selected = None finally: self._no_update = False def _on_rows_selection(self, added, removed): """Handle the rows selection being changed.""" self._no_update = True try: indexes = self.control.selectionModel().selectedRows() selected_rows = [] selected = [] for index in indexes: row = index.row() selected_rows.append(row) selected.append( self.adapter.get_item(self.object, self.name, row) ) self.multi_selected_rows = selected_rows self.multi_selected = selected finally: self._no_update = False def _on_context_menu(self, pos): column, row = ( self.control.columnAt(pos.x()), self.control.rowAt(pos.y()), ) menu = self.adapter.get_menu(self.object, self.name, row, column) if menu: self._menu_context = { "selection": self.object, "object": self.object, "editor": self, "column": column, "row": row, "item": self.adapter.get_item(self.object, self.name, row), "info": self.ui.info, "handler": self.ui.handler, } qmenu = menu.create_menu(self.control, self) qmenu.exec_(self.control.mapToGlobal(pos)) self._menu_context = None def _on_column_context_menu(self, pos): column = self.control.columnAt(pos.x()) menu = self.adapter.get_column_menu(self.object, self.name, -1, column) if menu: self._menu_context = { "selection": self.object, "object": self.object, "editor": self, "column": column, "info": self.ui.info, "handler": self.ui.handler, } qmenu = menu.create_menu(self.control, self) qmenu.exec_(self.control.mapToGlobal(pos)) self._menu_context = None else: # If no menu is defined on the adapter, just trigger a click event. self._on_column_right_click(column) class TabularEditorEvent(HasStrictTraits): # The index of the row: row = Int() # The id of the column (either a string or an integer): column = Any() # The row item: item = Property() # -- Private Traits ------------------------------------------------------- # The editor the event is associated with: editor = Instance(TabularEditor) # -- Property Implementations --------------------------------------------- def _get_item(self): editor = self.editor return editor.adapter.get_item(editor.object, editor.name, self.row) # ------------------------------------------------------------------------- # Qt widgets that have been configured to behave as expected by Traits UI: # ------------------------------------------------------------------------- class _ItemDelegate(QtGui.QStyledItemDelegate): """A QStyledItemDelegate which draws its owns gridlines so that we can choose to draw only the horizontal or only the vertical gridlines if appropriate. """ def __init__(self, table_view): """Store which grid lines to draw.""" QtGui.QStyledItemDelegate.__init__(self, table_view) self._horizontal_lines = table_view._editor.factory.horizontal_lines self._vertical_lines = table_view._editor.factory.vertical_lines def paint(self, painter, option, index): """Overrident to draw gridlines.""" QtGui.QStyledItemDelegate.paint(self, painter, option, index) painter.save() # FIXME: 'styleHint' is returning bogus (negative) values. Why? # style = QtGui.QApplication.instance().style() # color = style.styleHint(QtGui.QStyle.StyleHint.SH_Table_GridLineColor, option) # painter.setPen(QtGui.QColor(color)) painter.setPen(option.palette.color(QtGui.QPalette.ColorRole.Dark)) if self._horizontal_lines: painter.drawLine( option.rect.bottomLeft(), option.rect.bottomRight() ) if self._vertical_lines: painter.drawLine(option.rect.topRight(), option.rect.bottomRight()) painter.restore() class _TableView(QtGui.QTableView): """A QTableView configured to behave as expected by TraitsUI.""" def __init__(self, editor): """Initialise the object.""" QtGui.QTableView.__init__(self) self._user_widths = None self._is_resizing = False self._editor = editor self.setModel(editor.model) factory = editor.factory # Configure the row headings vheader = self.verticalHeader() if factory.show_row_titles: vheader.setHighlightSections(False) else: vheader.hide() if factory.show_row_titles and factory.auto_resize_rows: if is_qt4: vheader.setResizeMode( QtGui.QHeaderView.ResizeMode.ResizeToContents ) else: vheader.setSectionResizeMode( QtGui.QHeaderView.ResizeMode.ResizeToContents ) else: # Set a default height for rows. Although setting the resize mode to # ResizeToContents would provide the best sizes, this is far too # expensive when the TabularEditor has a large amount of data. Instead, # we make a reasonable guess based on the minimum size hint and the font # of the first row. size = vheader.minimumSectionSize() # Check if any columns have been added, and use their font, otherwise # use the default font font = None if 0 in editor.adapter.column_map: font = editor.adapter.get_font(editor.object, editor.name, 0) if font is not None: size = max( size, QtGui.QFontMetrics(QtGui.QFont(font)).height() ) vheader.setDefaultSectionSize(size) # Configure the column headings. hheader = self.horizontalHeader() hheader.setStretchLastSection(factory.stretch_last_section) hheader.sectionResized.connect(self.columnResized) if factory.show_titles: hheader.setHighlightSections(False) else: hheader.hide() # Turn off the grid lines--we'll draw our own self.setShowGrid(False) self.setItemDelegate(_ItemDelegate(self)) # Configure the selection behaviour. self.setSelectionBehavior(QtGui.QAbstractItemView.SelectionBehavior.SelectRows) if factory.multi_select: mode = QtGui.QAbstractItemView.SelectionMode.ExtendedSelection else: mode = QtGui.QAbstractItemView.SelectionMode.SingleSelection self.setSelectionMode(mode) # Configure drag and drop behavior self.setDragEnabled(True) if factory.editable: self.viewport().setAcceptDrops(True) if factory.drag_move: self.setDragDropMode(QtGui.QAbstractItemView.DragDropMode.InternalMove) else: self.setDragDropMode(QtGui.QAbstractItemView.DragDropMode.DragDrop) self.setDropIndicatorShown(True) def keyPressEvent(self, event): """Reimplemented to support edit, insert, and delete by keyboard.""" editor = self._editor factory = editor.factory # Note that setting 'EditKeyPressed' as an edit trigger does not work on # most platforms, which is why we do this here. if ( event.key() in (QtCore.Qt.Key.Key_Enter, QtCore.Qt.Key.Key_Return) and self.state() != QtGui.QAbstractItemView.State.EditingState and factory.editable and "edit" in factory.operations ): if factory.multi_select: rows = editor.multi_selected_rows row = rows[0] if len(rows) == 1 else -1 else: row = editor.selected_row if row != -1: event.accept() self.edit(editor.model.index(row, 0)) elif ( event.key() in (QtCore.Qt.Key.Key_Backspace, QtCore.Qt.Key.Key_Delete) and factory.editable and "delete" in factory.operations ): event.accept() if factory.multi_select: for row in reversed(sorted(editor.multi_selected_rows)): editor.model.removeRow(row) elif editor.selected_row != -1: editor.model.removeRow(editor.selected_row) elif ( event.key() == QtCore.Qt.Key.Key_Insert and factory.editable and "insert" in factory.operations ): event.accept() if factory.multi_select: rows = sorted(editor.multi_selected_rows) row = rows[0] if len(rows) else -1 else: row = editor.selected_row if row == -1: row = editor.adapter.len(editor.object, editor.name) editor.model.insertRow(row) self.setCurrentIndex(editor.model.index(row, 0)) else: QtGui.QTableView.keyPressEvent(self, event) def sizeHint(self): """Reimplemented to define a reasonable size hint.""" sh = QtGui.QTableView.sizeHint(self) width = 0 for column in range(len(self._editor.adapter.columns)): width += self.sizeHintForColumn(column) sh.setWidth(width) return sh def resizeEvent(self, event): """Reimplemented to size the table columns when the size of the table changes. Because the layout algorithm requires that the available space be known, we have to wait until the UI that contains this table gives it its initial size. """ super().resizeEvent(event) parent = self.parent() if parent and ( self.isVisible() or isinstance(parent, QtGui.QMainWindow) ): self.resizeColumnsToContents() def sizeHintForColumn(self, column): """Reimplemented to support absolute width specification via TabularAdapters and to avoid scanning all data to determine the size hint. (TabularEditor, unlike TableEditor, is expected to handle very large data sets.) """ editor = self._editor if editor.factory.auto_resize: # Use the default implementation. return super().sizeHintForColumn(column) width = editor.adapter.get_width(editor.object, editor.name, column) if width > 1: return width else: return self.horizontalHeader().sectionSizeHint(column) def resizeColumnsToContents(self): """Reimplemented to support proportional column width specifications. The core part of the computation is carried out in :func:`traitsui.helpers.compute_column_widths` """ editor = self._editor adapter = editor.adapter if editor.factory.auto_resize: # Use the default implementation. return super().resizeColumnsToContents() available_space = self.viewport().width() requested = [] min_widths = [] for column in range(len(adapter.columns)): width = adapter.get_width(editor.object, editor.name, column) requested.append(width) min_widths.append(self.sizeHintForColumn(column)) widths = compute_column_widths( available_space, requested, min_widths, self._user_widths ) hheader = self.horizontalHeader() with self._resizing(): for column, width in enumerate(widths): hheader.resizeSection(column, width) def columnResized(self, index, old, new): """Handle user-driven resizing of columns. This affects the column widths when not using auto-sizing. """ if not self._is_resizing: if self._user_widths is None: self._user_widths = [None] * len(self._editor.adapter.columns) self._user_widths[index] = new if ( self._editor.factory is not None and not self._editor.factory.auto_resize ): self.resizeColumnsToContents() @contextmanager def _resizing(self): """Context manager that guards against recursive column resizing.""" self._is_resizing = True try: yield finally: self._is_resizing = False ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/qt/tabular_model.py0000644000175100001730000003203300000000000021642 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Defines the table model used by the tabular editor. """ import logging from pyface.qt import QtCore, QtGui from traitsui.ui_traits import SequenceTypes from .clipboard import PyMimeData # Mapping for trait alignment values to qt alignment values: alignment_map = { "left": QtCore.Qt.AlignmentFlag.AlignLeft, "right": QtCore.Qt.AlignmentFlag.AlignRight, "center": QtCore.Qt.AlignmentFlag.AlignHCenter, "justify": QtCore.Qt.AlignmentFlag.AlignJustify, } # MIME type for internal table drag/drop operations tabular_mime_type = "traits-ui-tabular-editor" logger = logging.getLogger(__name__) class TabularModel(QtCore.QAbstractTableModel): """The model for tabular data.""" def __init__(self, editor, parent=None): """Initialise the object.""" QtCore.QAbstractTableModel.__init__(self, parent) self._editor = editor # ------------------------------------------------------------------------- # QAbstractItemModel interface: # ------------------------------------------------------------------------- def data(self, mi, role): """Reimplemented to return the data.""" editor = self._editor adapter = editor.adapter obj, name = editor.object, editor.name row, column = mi.row(), mi.column() if role == QtCore.Qt.ItemDataRole.DisplayRole or role == QtCore.Qt.ItemDataRole.EditRole: return adapter.get_text(obj, name, row, column) elif role == QtCore.Qt.ItemDataRole.DecorationRole: image = editor._get_image( adapter.get_image(obj, name, row, column) ) if image is not None: return image elif role == QtCore.Qt.ItemDataRole.ToolTipRole: tooltip = adapter.get_tooltip(obj, name, row, column) if tooltip: return tooltip elif role == QtCore.Qt.ItemDataRole.FontRole: font = adapter.get_font(obj, name, row, column) if font is not None: return QtGui.QFont(font) elif role == QtCore.Qt.ItemDataRole.TextAlignmentRole: string = adapter.get_alignment(obj, name, column) alignment = alignment_map.get(string, QtCore.Qt.AlignmentFlag.AlignLeft) return int(alignment | QtCore.Qt.AlignmentFlag.AlignVCenter) elif role == QtCore.Qt.ItemDataRole.BackgroundRole: color = adapter.get_bg_color(obj, name, row, column) if color is not None: if isinstance(color, SequenceTypes): q_color = QtGui.QColor(*color) else: q_color = QtGui.QColor(color) return QtGui.QBrush(q_color) elif role == QtCore.Qt.ItemDataRole.ForegroundRole: color = adapter.get_text_color(obj, name, row, column) if color is not None: if isinstance(color, SequenceTypes): q_color = QtGui.QColor(*color) else: q_color = QtGui.QColor(color) return QtGui.QBrush(q_color) return None def setData(self, mi, value, role): """Reimplmented to allow for modification for the object trait.""" if role != QtCore.Qt.ItemDataRole.EditRole: return False editor = self._editor obj, name = editor.object, editor.name row, column = mi.row(), mi.column() editor.adapter.set_text(obj, name, row, column, value) self.dataChanged.emit(mi, mi) return True def flags(self, mi): """Reimplemented to set editable status and movable status.""" editor = self._editor row = mi.row() column = mi.column() if not mi.isValid(): return QtCore.Qt.ItemFlag.ItemIsDropEnabled flags = QtCore.Qt.ItemFlag.ItemIsEnabled if editor.factory.selectable: flags |= QtCore.Qt.ItemFlag.ItemIsSelectable # If the adapter defines get_can_edit_cell(), use it to determine # editability over the row-wise get_can_edit(). if ( editor.factory.editable and "edit" in editor.factory.operations and hasattr(editor.adapter, "get_can_edit_cell") ): if editor.adapter.get_can_edit_cell( editor.object, editor.name, row, column ): flags |= QtCore.Qt.ItemFlag.ItemIsEditable elif ( editor.factory.editable and "edit" in editor.factory.operations and editor.adapter.get_can_edit(editor.object, editor.name, row) ): flags |= QtCore.Qt.ItemFlag.ItemIsEditable if ( editor.adapter.get_drag(editor.object, editor.name, row) is not None ): flags |= QtCore.Qt.ItemFlag.ItemIsDragEnabled if editor.factory.editable: flags |= QtCore.Qt.ItemFlag.ItemIsDropEnabled return flags def headerData(self, section, orientation, role): """Reimplemented to return the header data.""" if role != QtCore.Qt.ItemDataRole.DisplayRole: return None editor = self._editor label = None if orientation == QtCore.Qt.Orientation.Vertical: label = editor.adapter.get_row_label(section, editor.object) elif orientation == QtCore.Qt.Orientation.Horizontal: label = editor.adapter.get_label(section, editor.object) return label def rowCount(self, mi): """Reimplemented to return the number of rows.""" editor = self._editor return editor.adapter.len(editor.object, editor.name) def columnCount(self, mi): """Reimplemented to return the number of columns.""" editor = self._editor return len(editor.adapter.columns) def insertRow(self, row, parent=QtCore.QModelIndex(), obj=None): """Reimplemented to allow creation of new rows. Added an optional arg to allow the insertion of an existing row object. """ editor = self._editor adapter = editor.adapter if obj is None: obj = adapter.get_default_value(editor.object, editor.name) self.beginInsertRows(parent, row, row) editor.callx( editor.adapter.insert, editor.object, editor.name, row, obj ) self.endInsertRows() return True def insertRows(self, row, count, parent=QtCore.QModelIndex()): """Reimplemented to allow creation of new items.""" editor = self._editor adapter = editor.adapter self.beginInsertRows(parent, row, row + count - 1) for i in range(count): value = adapter.get_default_value(editor.object, editor.name) editor.callx( adapter.insert, editor.object, editor.name, row, value ) self.endInsertRows() return True def removeRows(self, row, count, parent=QtCore.QModelIndex()): """Reimplemented to allow row deletion, as well as reordering via drag and drop. """ editor = self._editor adapter = editor.adapter self.beginRemoveRows(parent, row, row + count - 1) for i in range(count): editor.callx(adapter.delete, editor.object, editor.name, row) self.endRemoveRows() n = self.rowCount(None) if not editor.factory.multi_select: editor.selected_row = row if row < n else row - 1 else: # FIXME: what should the selection be? editor.multi_selected_rows = [] return True def mimeTypes(self): """Reimplemented to expose our internal MIME type for drag and drop operations. """ return [ tabular_mime_type, PyMimeData.MIME_TYPE, PyMimeData.NOPICKLE_MIME_TYPE, ] def mimeData(self, indexes): """Reimplemented to generate MIME data containing the rows of the current selection. """ rows = sorted({index.row() for index in indexes}) items = [ self._editor.adapter.get_drag( self._editor.object, self._editor.name, row ) for row in rows ] mime_data = PyMimeData.coerce(items) data = QtCore.QByteArray(str(id(self)).encode("utf8")) for row in rows: data.append((" %i" % row).encode("utf8")) mime_data.setData(tabular_mime_type, data) return mime_data def dropMimeData(self, mime_data, action, row, column, parent): """Reimplemented to allow items to be moved.""" if action == QtCore.Qt.DropAction.IgnoreAction: return False # If dropped directly onto the parent, both row and column are -1. # See https://doc.qt.io/qt-5/qabstractitemmodel.html#dropMimeData # When dropped below the list, the "parent" is invalid. if row == -1: if parent.isValid(): row = parent.row() else: row = max(0, self.rowCount(None) - 1) # this is a drag from a tabular model data = mime_data.data(tabular_mime_type) if not data.isNull() and action == QtCore.Qt.DropAction.MoveAction: id_and_rows = [ int(s) for s in data.data().decode("utf8").split(" ") ] table_id = id_and_rows[0] # is it from ourself? if table_id == id(self): current_rows = id_and_rows[1:] self.moveRows(current_rows, row) return True # this is an external drag data = PyMimeData.coerce(mime_data).instance() if data is not None: if not isinstance(data, list): data = [data] editor = self._editor object = editor.object name = editor.name adapter = editor.adapter if all( adapter.get_can_drop(object, name, row, item) for item in data ): for item in reversed(data): self.dropItem(item, row) return True return False def supportedDropActions(self): """Reimplemented to allow items to be moved.""" return QtCore.Qt.DropAction.MoveAction | QtCore.Qt.DropAction.CopyAction # ------------------------------------------------------------------------- # TabularModel interface: # ------------------------------------------------------------------------- def dropItem(self, item, row): """Handle a Python object being dropped onto a row""" editor = self._editor object = editor.object name = editor.name adapter = editor.adapter destination = adapter.get_dropped(object, name, row, item) if destination == "after": row += 1 adapter.insert(object, name, row, item) def moveRow(self, old_row, new_row): """Convenience method to move a single row.""" return self.moveRows([old_row], new_row) def moveRows(self, current_rows, new_row): """Moves a sequence of rows (provided as a list of row indexes) to a new row. """ editor = self._editor if new_row == -1: # row should be nonnegative and less than the row count for this # model. See ``QAbstractItemModel.checkIndex``` for Qt 5.11+ # This is a last resort to prevent segmentation faults. logger.debug( "Received invalid row %d. Adjusting to the last row.", new_row ) new_row = max(0, self.rowCount(None) - 1) # Sort rows in descending order so they can be removed without # invalidating the indices. current_rows.sort() current_rows.reverse() # If the the highest selected row is lower than the destination, do an # insertion before rather than after the destination. if current_rows[-1] < new_row: new_row += 1 # Remove selected rows... objects = [] for row in current_rows: if row <= new_row: new_row -= 1 obj = editor.adapter.get_item(editor.object, editor.name, row) objects.insert(0, obj) self.removeRow(row) # ...and add them at the new location. for i, obj in enumerate(objects): self.insertRow(new_row + i, obj=obj) # Update the selection for the new location. if editor.factory.multi_select: editor.setx(multi_selected=objects) editor.multi_selected_rows = list( range(new_row, new_row + len(objects)) ) else: editor.setx(selected=objects[0]) editor.selected_row = new_row ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1684768028.0998068 traitsui-8.0.0/traitsui/qt/tests/0000755000175100001730000000000000000000000017617 5ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/qt/tests/__init__.py0000644000175100001730000000000000000000000021716 0ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/qt/tests/test_color_trait.py0000644000175100001730000001332000000000000023550 0ustar00runnerdocker00000000000000# (C) Copyright 2008-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! import unittest from pyface.qt import QtGui from pyface.color import Color as PyfaceColor from traits.api import HasStrictTraits, TraitError from traitsui.qt.color_trait import PyQtColor class ObjectWithColor(HasStrictTraits): color = PyQtColor() class ObjectWithColorAllowsNone(HasStrictTraits): color = PyQtColor(allow_none=True) class TestPyQtColor(unittest.TestCase): def test_default(self): obj = ObjectWithColor() self.assertIsInstance(obj.color, QtGui.QColor) self.assertEqual(obj.color.getRgb(), (255, 255, 255, 255)) self.assertIsInstance(obj.color_, QtGui.QColor) self.assertEqual(obj.color_.getRgb(), (255, 255, 255, 255)) def test_tuple_rgb(self): obj = ObjectWithColor(color=(0, 128, 255)) self.assertIsInstance(obj.color, QtGui.QColor) self.assertEqual(obj.color.getRgb(), (0, 128, 255, 255)) self.assertIsInstance(obj.color_, QtGui.QColor) self.assertEqual(obj.color_.getRgb(), (0, 128, 255, 255)) def test_tuple_rgba(self): obj = ObjectWithColor(color=(0, 128, 255, 64)) self.assertIsInstance(obj.color, QtGui.QColor) self.assertEqual(obj.color.getRgb(), (0, 128, 255, 64)) self.assertIsInstance(obj.color_, QtGui.QColor) self.assertEqual(obj.color_.getRgb(), (0, 128, 255, 64)) def test_name_string(self): obj = ObjectWithColor(color="rebeccapurple") self.assertIsInstance(obj.color, QtGui.QColor) self.assertEqual(obj.color.getRgb(), (0x66, 0x33, 0x99, 0xff)) self.assertIsInstance(obj.color_, QtGui.QColor) self.assertEqual(obj.color_.getRgb(), (0x66, 0x33, 0x99, 0xff)) def test_name_string_with_space(self): obj = ObjectWithColor(color="rebecca purple") self.assertIsInstance(obj.color, QtGui.QColor) self.assertEqual(obj.color.getRgb(), (0x66, 0x33, 0x99, 0xff)) self.assertIsInstance(obj.color_, QtGui.QColor) self.assertEqual(obj.color_.getRgb(), (0x66, 0x33, 0x99, 0xff)) def test_rgb_string(self): obj = ObjectWithColor(color="(0, 128, 255)") self.assertIsInstance(obj.color, QtGui.QColor) self.assertEqual(obj.color.getRgb(), (0, 128, 255, 255)) self.assertIsInstance(obj.color_, QtGui.QColor) self.assertEqual(obj.color_.getRgb(), (0, 128, 255, 255)) def test_rgba_string(self): obj = ObjectWithColor(color="(0, 128, 255, 64)") self.assertIsInstance(obj.color, QtGui.QColor) self.assertEqual(obj.color.getRgb(), (0, 128, 255, 64)) self.assertIsInstance(obj.color_, QtGui.QColor) self.assertEqual(obj.color_.getRgb(), (0, 128, 255, 64)) def test_rgb_int_string_3(self): obj = ObjectWithColor(color="#08f") self.assertIsInstance(obj.color, QtGui.QColor) self.assertEqual(obj.color.getRgb(), (0x00, 0x88, 0xff, 0xff)) self.assertIsInstance(obj.color_, QtGui.QColor) self.assertEqual(obj.color_.getRgb(), (0x00, 0x88, 0xff, 0xff)) def test_rgb_int_string_6(self): obj = ObjectWithColor(color="#0088ff") self.assertIsInstance(obj.color, QtGui.QColor) self.assertEqual(obj.color.getRgb(), (0x00, 0x88, 0xff, 0xff)) self.assertIsInstance(obj.color_, QtGui.QColor) self.assertEqual(obj.color_.getRgb(), (0x00, 0x88, 0xff, 0xff)) def test_rgb_int_string_12(self): obj = ObjectWithColor(color="#00008888ffff") self.assertIsInstance(obj.color, QtGui.QColor) self.assertEqual(obj.color.getRgb(), (0x00, 0x88, 0xff, 0xff)) self.assertIsInstance(obj.color_, QtGui.QColor) self.assertEqual(obj.color_.getRgb(), (0x00, 0x88, 0xff, 0xff)) def test_rgb_int(self): obj = ObjectWithColor(color=0x0088ff) self.assertIsInstance(obj.color, QtGui.QColor) self.assertEqual(obj.color.getRgb(), (0x00, 0x88, 0xff, 0xff)) self.assertIsInstance(obj.color_, QtGui.QColor) self.assertEqual(obj.color_.getRgb(), (0x00, 0x88, 0xff, 0xff)) def test_qcolor(self): obj = ObjectWithColor(color=QtGui.QColor(0, 128, 255, 64)) self.assertIsInstance(obj.color, QtGui.QColor) self.assertEqual(obj.color.getRgb(), (0, 128, 255, 64)) self.assertIsInstance(obj.color_, QtGui.QColor) self.assertEqual(obj.color_.getRgb(), (0, 128, 255, 64)) def test_pyface_color(self): obj = ObjectWithColor(color=PyfaceColor(rgba=(0.0, 0.5, 1.0, 0.25))) self.assertIsInstance(obj.color, QtGui.QColor) self.assertEqual(obj.color.getRgb(), (0, 128, 255, 64)) self.assertIsInstance(obj.color_, QtGui.QColor) self.assertEqual(obj.color_.getRgb(), (0, 128, 255, 64)) def test_default_none(self): obj = ObjectWithColorAllowsNone(color=None) self.assertIsNone(obj.color) self.assertIsNone(obj.color_) def test_bad_color(self): with self.assertRaises(TraitError): ObjectWithColor(color="not a color") def test_bad_tuple(self): with self.assertRaises(TraitError): ObjectWithColor(color=(0xff, 0xff)) def test_bad_tuple_not_int(self): with self.assertRaises(TraitError): ObjectWithColor(color=("not an int", 0xff, 0xff)) def test_bad_tuple_string(self): with self.assertRaises(TraitError): ObjectWithColor(color="(0xff, 0xff)") ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/qt/tests/test_font_trait.py0000644000175100001730000001501300000000000023401 0ustar00runnerdocker00000000000000# (C) Copyright 2008-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! import unittest from pyface.font import Font as PyfaceFont from pyface.util.font_parser import simple_parser from pyface.qt.QtGui import QFont, QFontInfo, QApplication from traits.api import HasTraits, TraitError from ..font_trait import ( PyQtFont, TraitsFont, create_traitsfont, font_families, font_styles, font_to_str, font_weights ) class FontExample(HasTraits): font = PyQtFont() class TestPyQtFont(unittest.TestCase): def test_create_traitsfont(self): expected_outcomes = {} expected_outcomes[""] = TraitsFont() for weight, qt_weight in font_weights.items(): expected_outcomes[weight] = TraitsFont() expected_outcomes[weight].setWeight(qt_weight) for style, qt_style in font_styles.items(): expected_outcomes[style] = TraitsFont() expected_outcomes[style].setStyle(qt_style) expected_outcomes["underline"] = TraitsFont() expected_outcomes["underline"].setUnderline(True) expected_outcomes["18"] = TraitsFont() expected_outcomes["18"].setPointSize(18) expected_outcomes["18 pt"] = TraitsFont() expected_outcomes["18 pt"].setPointSize(18) expected_outcomes["18 point"] = TraitsFont() expected_outcomes["18 point"].setPointSize(18) for family, qt_style_hint in font_families.items(): expected_outcomes[family] = TraitsFont() expected_outcomes[family].setStyleHint(qt_style_hint) default_size = QApplication.font().pointSize() expected_outcomes["Courier"] = TraitsFont("Courier", default_size) expected_outcomes["Comic Sans"] = TraitsFont("Comic Sans", default_size) expected_outcomes["18 pt Bold Oblique Underline Comic Sans script"] = TraitsFont( "Comic Sans", 18, QFont.Weight.Bold, False ) expected_outcomes["18 pt Bold Oblique Underline Comic Sans script"].setStyleHint(QFont.StyleHint.Cursive) expected_outcomes["18 pt Bold Oblique Underline Comic Sans script"].setStyle(QFont.Style.StyleOblique) expected_outcomes["18 pt Bold Oblique Underline Comic Sans script"].setUnderline(True) for name, expected in expected_outcomes.items(): with self.subTest(name=name): result = create_traitsfont(name) # test we get expected font self.assertIsInstance(result, TraitsFont) self.assert_qfont_equal(result, expected) # round-trip through font_to_str result_2 = create_traitsfont(font_to_str(result)) self.assert_qfont_equal(result, result_2) def test_create_traitsfont_qfont(self): font = QFont("Comic Sans", 18, QFont.Weight.Bold, False) traits_font = create_traitsfont(font) self.assertIsInstance(traits_font, TraitsFont) self.assert_qfont_equal(traits_font, font) def test_create_traitsfont_pyface_font(self): args = simple_parser("18 pt Bold Oblique Underline Courier") font = PyfaceFont(**args) traits_font = create_traitsfont(font) self.assertIsInstance(traits_font, TraitsFont) self.assert_qfont_equal(traits_font, font.to_toolkit()) def test_font_trait_default(self): obj = FontExample() self.assertIsInstance(obj.font, TraitsFont) self.assert_qfont_equal(obj.font, TraitsFont()) def test_font_trait_str(self): obj = FontExample(font="18 pt Bold Oblique Underline Comic Sans script") qfont = TraitsFont( "Comic Sans", 18, QFont.Weight.Bold, False ) qfont.setStyleHint(QFont.StyleHint.Cursive) qfont.setStyle(QFont.Style.StyleOblique) qfont.setUnderline(True) self.assertIsInstance(obj.font, TraitsFont) self.assert_qfont_equal(obj.font, qfont) def test_font_trait_qfont(self): qfont = TraitsFont( "Comic Sans", 18, QFont.Weight.Bold, False ) qfont.setStyleHint(QFont.StyleHint.Cursive) qfont.setStyle(QFont.Style.StyleOblique) qfont.setUnderline(True) obj = FontExample(font=qfont) self.assertIsInstance(obj.font, TraitsFont) self.assert_qfont_equal(obj.font, qfont) def test_font_trait_pyface_font(self): args = simple_parser("18 pt Bold Oblique Underline Courier typewriter") font = PyfaceFont(**args) obj = FontExample(font=font) self.assertIsInstance(obj.font, TraitsFont) self.assert_qfont_equal(obj.font, font.to_toolkit()) def test_font_trait_none(self): obj = FontExample(font=None) self.assertIsNone(obj.font) def test_font_trait_bad(self): with self.assertRaises(TraitError): obj = FontExample(font=1) def test_traits_font_reduce(self): traits_font = TraitsFont( "Comic Sans", 18, QFont.Weight.Bold, False ) traits_font.setStyleHint(QFont.StyleHint.Cursive) traits_font.setStyle(QFont.Style.StyleOblique) traits_font.setUnderline(True) result = traits_font.__reduce_ex__(None) self.assertEqual( result, ( create_traitsfont, ("18 point Comic Sans Oblique Bold underline",), ), ) def test_traits_font_str(self): traits_font = TraitsFont( "Comic Sans", 18, QFont.Weight.Bold, False ) traits_font.setStyleHint(QFont.StyleHint.Cursive) traits_font.setStyle(QFont.Style.StyleOblique) traits_font.setUnderline(True) result = str(traits_font) self.assertEqual( result, "18 point Comic Sans Oblique Bold underline", ) def assert_qfont_equal(self, font, other): self.assertIsInstance(font, QFont) self.assertIsInstance(other, QFont) self.assertEqual(font.family(), other.family()) self.assertEqual(font.styleHint(), font.styleHint()) self.assertEqual(font.pointSize(), other.pointSize()) self.assertEqual(font.style(), other.style()) self.assertEqual(font.weight(), other.weight()) self.assertEqual(font.underline(), other.underline()) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/qt/tests/test_helper.py0000644000175100001730000001206500000000000022513 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! import textwrap import unittest from pyface.qt import is_pyqt, qt_api, QtCore, QtGui from traitsui.tests._tools import is_mac_os, requires_toolkit, ToolkitName from traitsui.qt.helper import qobject_is_valid, wrap_text_with_elision from traitsui.qt.font_trait import create_traitsfont lorem_ipsum = ( "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod " "tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim " "veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea " "commodo consequat.\n" " \n" "Duis aute irure dolor in reprehenderit in voluptate velit esse cillum " "dolore eu fugiat nulla pariatur.\n" "Excepteur sint occaecat cupidatat non proident, sunt in culpa qui " "officia deserunt mollit anim id est laborum." ) def get_expected_lines(text, width): expected_lines = [] for paragraph in text.splitlines(): if not paragraph.strip(): expected_lines.append("") continue expected_lines += textwrap.wrap(paragraph, width) return expected_lines @requires_toolkit([ToolkitName.qt]) class TestWrapText(unittest.TestCase): def test_wrap_text_basic(self): font = create_traitsfont("Courier") font_metrics = QtGui.QFontMetrics(font) average_char_width = font_metrics.averageCharWidth() line_spacing = font_metrics.lineSpacing() width = 500 * average_char_width height = 100 * line_spacing lines = wrap_text_with_elision(lorem_ipsum, font, width, height) self.assertEqual( lines, [line.rstrip() for line in lorem_ipsum.splitlines()] ) def test_wrap_text_empty(self): font = create_traitsfont("Courier") font_metrics = QtGui.QFontMetrics(font) average_char_width = font_metrics.averageCharWidth() line_spacing = font_metrics.lineSpacing() width = 500 * average_char_width height = 100 * line_spacing lines = wrap_text_with_elision("", font, width, height) self.assertEqual(lines, []) def test_wrap_text_narrow(self): font = create_traitsfont("Courier") font_metrics = QtGui.QFontMetrics(font) average_char_width = font_metrics.averageCharWidth() line_spacing = font_metrics.lineSpacing() width = 20 * average_char_width height = 100 * line_spacing lines = wrap_text_with_elision(lorem_ipsum, font, width, height) # add one char slack as depends on OS, exact font, etc. self.assertTrue(all(len(line) <= 21 for line in lines)) def test_wrap_text_narrow_short(self): font = create_traitsfont("Courier") font_metrics = QtGui.QFontMetrics(font) average_char_width = font_metrics.averageCharWidth() line_spacing = font_metrics.lineSpacing() width = 20 * average_char_width height = 20 * line_spacing lines = wrap_text_with_elision(lorem_ipsum, font, width, height) # add one char slack as depends on OS, exact font, etc. self.assertTrue(all(len(line) <= 21 for line in lines)) # different os elide the last line slightly differently, # just check end of last line shows elision. # In most systems elision is marked with ellipsis # but it has been reported as "..." on NetBSD. if lines[19][-1] == ".": self.assertEqual(lines[19][-3:], "...") else: self.assertEqual(lines[19][-1], "\u2026") def test_wrap_text_short(self): font = create_traitsfont("Courier") font_metrics = QtGui.QFontMetrics(font) average_char_width = font_metrics.averageCharWidth() line_spacing = font_metrics.lineSpacing() width = 500 * average_char_width height = 3 * line_spacing lines = wrap_text_with_elision(lorem_ipsum, font, width, height) expected_lines = get_expected_lines(lorem_ipsum, 500)[:3] self.assertEqual(lines, expected_lines) @unittest.skipIf( is_mac_os and qt_api == "pyside6", "causes next test to segfault on Mac OS/PySide6 (see #1974)", ) def test_qobject_is_valid(self): qobject = QtCore.QObject() if is_pyqt: from sip import delete elif qt_api == "pyside2": from shiboken2 import delete elif qt_api == "pyside6": from shiboken6 import delete else: with self.assertRaises(RuntimeError): qobject_is_valid(qobject) return result = qobject_is_valid(qobject) self.assertTrue(result) delete(qobject) result = qobject_is_valid(qobject) self.assertFalse(result) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/qt/tests/test_tabular_model.py0000644000175100001730000001425300000000000024047 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Tests for TabularModel (an implementation of QAbstractTableModel) """ import unittest from traits.api import HasTraits, List, Str from traitsui.api import Item, TabularEditor, View from traitsui.tabular_adapter import TabularAdapter from traitsui.tests._tools import ( BaseTestMixin, create_ui, is_qt, requires_toolkit, reraise_exceptions, ToolkitName, ) try: from pyface.qt import QtCore except ImportError: # The entire test case should be skipped if the current backend is not Qt # But if it is Qt, then re-raise if is_qt(): raise class DummyHasTraits(HasTraits): names = List(Str) def get_view(adapter): return View( Item( "names", editor=TabularEditor( adapter=adapter, ), ) ) @requires_toolkit([ToolkitName.qt]) class TestTabularModel(BaseTestMixin, unittest.TestCase): def setUp(self): BaseTestMixin.setUp(self) def tearDown(self): BaseTestMixin.tearDown(self) def test_drop_mime_data_below_list(self): # Test dragging an item in the list and drop it below the last item obj = DummyHasTraits(names=["A", "B", "C", "D"]) view = get_view(TabularAdapter(columns=["Name"])) with reraise_exceptions(), create_ui(obj, dict(view=view)) as ui: (editor,) = ui.get_editors("names") model = editor.model # sanity check self.assertEqual(model.rowCount(None), 4) # drag and drop row=1 from within the table. # drag creates a PyMimeData object for dropMimeData to consume. index = model.createIndex(1, 0) mime_data = model.mimeData([index]) # when # dropped below the list, the "parent" is invalid. parent = QtCore.QModelIndex() # invalid index object model.dropMimeData(mime_data, QtCore.Qt.DropAction.MoveAction, -1, -1, parent) # then mime_data = model.mimeData( [ model.createIndex(i, 0) for i in range( model.rowCount(None), ) ] ) content = mime_data.instance() self.assertEqual(content, ["A", "C", "D", "B"]) self.assertEqual(obj.names, content) def test_drop_mime_data_within_list(self): # Test dragging an item in the list and drop it somewhere within the # list obj = DummyHasTraits(names=["A", "B", "C", "D"]) view = get_view(TabularAdapter(columns=["Name"])) with reraise_exceptions(), create_ui(obj, dict(view=view)) as ui: (editor,) = ui.get_editors("names") model = editor.model # sanity check self.assertEqual(model.rowCount(None), 4) # drag and drop from within the table. # drag row index 0 index = model.createIndex(0, 0) mime_data = model.mimeData([index]) # when # drop it to row index 2 parent = model.createIndex(2, 0) model.dropMimeData(mime_data, QtCore.Qt.DropAction.MoveAction, -1, -1, parent) # then mime_data = model.mimeData( [ model.createIndex(i, 0) for i in range( model.rowCount(None), ) ] ) content = mime_data.instance() self.assertEqual(content, ["B", "C", "A", "D"]) self.assertEqual(obj.names, content) def test_copy_item(self): # Test copy 'A' to the row after 'C' obj = DummyHasTraits(names=["A", "B", "C"]) view = get_view(TabularAdapter(columns=["Name"], can_drop=True)) with reraise_exceptions(), create_ui(obj, dict(view=view)) as ui: (editor,) = ui.get_editors("names") model = editor.model # sanity check self.assertEqual(model.rowCount(None), 3) # drag and drop from within the table for copy action. # drag index 0 index = model.createIndex(0, 0) mime_data = model.mimeData([index]) # when # drop to index 2 parent = model.createIndex(2, 0) model.dropMimeData(mime_data, QtCore.Qt.DropAction.CopyAction, -1, -1, parent) # then self.assertEqual(model.rowCount(None), 4) mime_data = model.mimeData( [ model.createIndex(i, 0) for i in range( model.rowCount(None), ) ] ) content = mime_data.instance() self.assertEqual(content, ["A", "B", "C", "A"]) self.assertEqual(obj.names, content) def test_move_rows_invalid_index(self): # Test the last resort to prevent segfault obj = DummyHasTraits(names=["A", "B", "C"]) view = get_view(TabularAdapter(columns=["Name"])) with reraise_exceptions(), create_ui(obj, dict(view=view)) as ui: (editor,) = ui.get_editors("names") model = editor.model # sanity check self.assertEqual(model.rowCount(None), 3) # when # -1 is an invalid row. This should not cause segfault. model.moveRows([1], -1) # then mime_data = model.mimeData( [ model.createIndex(i, 0) for i in range( model.rowCount(None), ) ] ) content = mime_data.instance() self.assertEqual(content, ["A", "C", "B"]) self.assertEqual(obj.names, content) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/qt/tests/test_ui_base.py0000644000175100001730000000253300000000000022642 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! import unittest from traits.api import HasTraits, Int from traitsui.api import Item, View from traitsui.tests._tools import ( create_ui, requires_toolkit, ToolkitName, ) class ObjectWithNumber(HasTraits): number = Int() @requires_toolkit([ToolkitName.qt]) class TestStickyDialog(unittest.TestCase): """Test _StickyDialog used by the UI's Qt backend.""" def test_sticky_dialog_with_parent(self): obj = ObjectWithNumber() obj2 = ObjectWithNumber() parent_view = View(Item("number"), title="Parent") nested = View(Item("number"), resizable=True, title="Nested") with create_ui(obj, dict(view=parent_view)) as ui: with create_ui(obj2, dict(parent=ui.control, view=nested)) as ui2: from pyface.qt import QtCore self.assertFalse( ui2.control.windowState() & QtCore.Qt.WindowState.WindowMaximized ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/qt/tests/test_ui_panel.py0000644000175100001730000002121400000000000023024 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! import unittest from traits.api import Enum, HasTraits, Int, Str, Instance from traitsui.api import HGroup, Item, Group, VGroup, View from traitsui.menu import ToolBar, Action from traitsui.testing.api import Index, IsVisible, MouseClick, UITester from traitsui.tests._tools import ( create_ui, requires_toolkit, process_cascade_events, ToolkitName, ) class FooPanel(HasTraits): my_int = Int(2) my_str = Str("I am a panel/subpanel") toolbar = Instance(ToolBar) def default_traits_view(self): view = View( Item(name="my_int"), Item(name="my_str"), title="FooPanel", buttons=["OK", "Cancel"], toolbar=self.toolbar, ) return view def _toolbar_default(self): return ToolBar(Action(name="Open file")) class FooDialog(HasTraits): panel1 = Instance(FooPanel) panel2 = Instance(FooPanel) view = View( Group(Item("panel1"), Item("panel2"), layout="split", style="custom") ) def _panel1_default(self): return FooPanel() def _panel2_default(self): return FooPanel() class ScrollableGroupExample(HasTraits): my_int = Int(2) my_str = Str("The group is scrollable") scrollable_group_view = View( Group( Item(name="my_int"), Item(name="my_str"), scrollable=True, ), title="FooPanel", kind='subpanel', ) non_scrollable_group_view = View( Group( Item(name="my_int"), Item(name="my_str"), scrollable=False, ), title="FooPanel", kind='subpanel', ) scrollable_group_box_view = View( Group( Item(name="my_int"), Item(name="my_str"), scrollable=True, label="Scrollable View", show_border=True, ), title="FooPanel", kind='subpanel', ) scrollable_labelled_group_view = View( Group( Item(name="my_int"), Item(name="my_str"), scrollable=True, label="Scrollable View", ), title="FooPanel", kind='subpanel', ) class ScrollableGroupVisibleWhen(HasTraits): bar = Str("bar!") baz = Str("Baz?") enabled = Enum("Yes", "No") def default_traits_view(self): view = View( Item("enabled"), HGroup( VGroup( Item("bar"), scrollable=True, visible_when="enabled=='Yes'", id='bar_group', ), VGroup( Item("baz"), scrollable=True, visible_when="enabled=='No'", id='baz_group', ), ), ) return view @requires_toolkit([ToolkitName.qt]) class TestUIPanel(unittest.TestCase): def setup_qt_dock_window(self): from pyface.qt import QtGui # set up the dock window for qt main_window = QtGui.QMainWindow() self.addCleanup(process_cascade_events) self.addCleanup(main_window.close) dock = QtGui.QDockWidget("testing", main_window) dock.setWidget(QtGui.QMainWindow()) return main_window, dock def test_panel_has_toolbar_buttons_qt(self): from pyface.qt import QtGui _, dock = self.setup_qt_dock_window() # add panel panel = FooPanel() with create_ui(panel, dict(parent=dock.widget(), kind="panel")) as ui: dock.widget().setCentralWidget(ui.control) # There should be a toolbar for the panel self.assertIsNotNone(dock.findChild(QtGui.QToolBar)) # There should be buttons too # Not searching from dock because the dock panel has buttons for # popping up and closing the panel self.assertIsNotNone(ui.control.findChild(QtGui.QPushButton)) def test_subpanel_has_toolbar_no_buttons_qt(self): from pyface.qt import QtGui _, dock = self.setup_qt_dock_window() # add panel panel = FooPanel() parent = dock.widget() with create_ui(panel, dict(parent=parent, kind="subpanel")) as ui: dock.widget().setCentralWidget(ui.control) # There should be a toolbar for the subpanel self.assertIsNotNone(dock.findChild(QtGui.QToolBar)) # Buttons should not be shown for subpanel # Not searching from dock because the dock panel has buttons for # popping up and closing the panel self.assertIsNone(ui.control.findChild(QtGui.QPushButton)) def test_subpanel_no_toolbar_nor_button_in_widget(self): from pyface.qt import QtGui # FooDialog uses a QWidget to contain the panels # No attempt should be made for adding the toolbars foo_window = FooDialog() with create_ui(foo_window) as ui: # No toolbar for the dialog self.assertIsNone(ui.control.findChild(QtGui.QToolBar)) # No button self.assertIsNone(ui.control.findChild(QtGui.QPushButton)) # regression test for enthought/traitsui#1512 def test_scrollable_group_visible_when(self): from pyface.qt import QtGui obj = ScrollableGroupVisibleWhen() tester = UITester() with tester.create_ui(obj) as ui: bar_group = tester.find_by_id(ui, 'bar_group') baz_group = tester.find_by_id(ui, 'baz_group') # for a scrollable group the GroupEditors control should be a # QScrollArea not just the QWidget. We want the full area to be # not visible, not just the text box widget. self.assertIsInstance(bar_group._target.control, QtGui.QScrollArea) self.assertIsInstance(baz_group._target.control, QtGui.QScrollArea) self.assertTrue(bar_group.inspect(IsVisible())) self.assertFalse(baz_group.inspect(IsVisible())) enabled_box = tester.find_by_name(ui, 'enabled') baz_item = enabled_box.locate(Index(1)) baz_item.perform(MouseClick()) self.assertTrue(baz_group.inspect(IsVisible())) self.assertFalse(bar_group.inspect(IsVisible())) @requires_toolkit([ToolkitName.qt]) class TestPanelLayout(unittest.TestCase): def test_scrollable_group_typical(self): from pyface.qt import QtGui example = ScrollableGroupExample() ui = example.edit_traits(view=scrollable_group_view) try: mainwindow = ui.control.layout().itemAt(0).widget() scroll_area = mainwindow.centralWidget() self.assertIsInstance(scroll_area, QtGui.QScrollArea) content = scroll_area.widget() self.assertEqual(type(content), QtGui.QWidget) finally: ui.dispose() def test_scrollable_group_box(self): from pyface.qt import QtGui example = ScrollableGroupExample() ui = example.edit_traits(view=scrollable_group_box_view) try: mainwindow = ui.control.layout().itemAt(0).widget() scroll_area = mainwindow.centralWidget() self.assertIsInstance(scroll_area, QtGui.QScrollArea) group_box = scroll_area.widget() self.assertIsInstance(group_box, QtGui.QGroupBox) self.assertEqual(group_box.title(), "Scrollable View") finally: ui.dispose() def test_scrollable_labelled_group(self): from pyface.qt import QtGui example = ScrollableGroupExample() ui = example.edit_traits(view=scrollable_labelled_group_view) try: mainwindow = ui.control.layout().itemAt(0).widget() scroll_area = mainwindow.centralWidget() self.assertIsInstance(scroll_area, QtGui.QScrollArea) content = scroll_area.widget() self.assertEqual(type(content), QtGui.QWidget) finally: ui.dispose() def test_non_scrollable_group_typical(self): from pyface.qt import QtGui example = ScrollableGroupExample(my_str="The group is not scrollable") ui = example.edit_traits(view=non_scrollable_group_view) try: mainwindow = ui.control.layout().itemAt(0).widget() content = mainwindow.centralWidget() self.assertEqual(type(content), QtGui.QWidget) finally: ui.dispose() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/qt/text_editor.py0000644000175100001730000001724600000000000021373 0ustar00runnerdocker00000000000000# (C) Copyright 2008-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # ------------------------------------------------------------------------------ # Copyright (c) 2007, Riverbank Computing Limited # All rights reserved. # # This software is provided without warranty under the terms of the BSD license. # However, when used with the GPL version of PyQt the additional terms # described in the PyQt GPL exception also apply # # Author: Riverbank Computing Limited # ------------------------------------------------------------------------------ """ Defines the various text editors for the PyQt user interface toolkit. """ from pyface.qt import QtCore, QtGui from traits.api import Any, Callable, List, TraitError, Tuple from traitsui.editors.text_editor import evaluate_trait from .editor import Editor 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 = QtGui.QLineEdit #: Background color when input is OK: ok_color = OKColor # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- #: Function used to evaluate textual user input: evaluate = evaluate_trait # -- private trait definitions ------------------------------------------ #: A list of tuple(Qt signal, slot) connected which need to be disconnected #: in dispose. _connections_to_remove = List(Tuple(Any, Callable)) 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 or factory.password: wtype = QtGui.QLineEdit multi_line = wtype is not QtGui.QLineEdit if multi_line: self.scrollable = True control = wtype(self.str_value) if factory.read_only: control.setReadOnly(True) if factory.password: control.setEchoMode(QtGui.QLineEdit.EchoMode.Password) if wtype == QtGui.QTextEdit: control.textChanged.connect(self.update_object) self._connections_to_remove.append( (control.textChanged, self.update_object) ) else: # QLineEdit if factory.auto_set and not factory.is_grid_cell: control.textEdited.connect(self.update_object) self._connections_to_remove.append( (control.textEdited, self.update_object) ) else: control.editingFinished.connect(self.update_object) self._connections_to_remove.append( (control.editingFinished, self.update_object) ) placeholder = self.factory.placeholder if wtype is not QtGui.QTextEdit or QtCore.__version_info__ >= (5, 2): # setPlaceholderText is introduced to QTextEdit since Qt 5.2 control.setPlaceholderText(placeholder) if wtype is not QtGui.QTextEdit and QtCore.__version_info__ >= (5, 2): # setClearButtonEnabled is introduced to QLineEdit since Qt 5.2 control.setClearButtonEnabled(self.factory.cancel_button) self.control = control # default horizontal policy is Expand, set this to Minimum if not (self.item.resizable) and not self.item.springy: policy = self.control.sizePolicy() policy.setHorizontalPolicy(QtGui.QSizePolicy.Policy.Minimum) self.control.setSizePolicy(policy) self.set_error_state(False) self.set_tooltip() def dispose(self): """Disposes of the contents of an editor.""" while self._connections_to_remove: signal, handler = self._connections_to_remove.pop() signal.disconnect(handler) super().dispose() 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.setText(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.""" try: value = self.control.text() except AttributeError: value = self.control.toPlainText() value = str(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.""" #: FIXME: The wx version exposes a wx constant. #: Flag for window style. This value overrides the default. base_style = QtGui.QTextEdit class ReadonlyEditor(BaseReadonlyEditor): """Read-only style of text editor, which displays a read-only text field.""" def init(self, parent): super().init(parent) if self.factory.readonly_allow_selection: flags = ( self.control.textInteractionFlags() | QtCore.Qt.TextInteractionFlag.TextSelectableByMouse ) self.control.setTextInteractionFlags(flags) def update_editor(self): """Updates the editor when the object trait changes externally to the editor. """ new_value = self.str_value if self.factory.password: new_value = "*" * len(new_value) self.control.setText(new_value) # ------------------------------------------------------------------------- # 'TextEditor' class: # ------------------------------------------------------------------------- # Same as SimpleEditor for a text editor. TextEditor = SimpleEditor ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/qt/time_editor.py0000644000175100001730000000417600000000000021343 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ A Traits UI editor for datetime.time objects. """ import datetime from pyface.qt import QtCore, QtGui from .editor import Editor from .editor_factory import ReadonlyEditor as BaseReadonlyEditor class SimpleEditor(Editor): """Simple Traits UI time editor that wraps QTimeEdit.""" def init(self, parent): """Finishes initializing the editor by creating the underlying toolkit widget. """ self.control = QtGui.QTimeEdit() self.control.timeChanged.connect(self.update_object) def update_editor(self): """Updates the editor when the object trait changes externally to the editor. """ value = self.value if value: q_date = QtCore.QTime(value.hour, value.minute, value.second) self.control.setTime(q_date) def update_object(self, q_time): """Handles the user entering input data in the edit control.""" hour = q_time.hour() minute = q_time.minute() second = q_time.second() try: self.value = datetime.time(hour, minute, second) except ValueError: print("Invalid time:", hour, minute, second) raise # ------------------------------------------------------------------------------ # 'ReadonlyEditor' class: # ------------------------------------------------------------------------------ class ReadonlyEditor(BaseReadonlyEditor): """Readonly Traits UI time editor that uses a QLabel for the view.""" def _get_str_value(self): """Replace the default string value with our own time verision.""" if not self.value: return self.factory.message else: return self.value.strftime(self.factory.strftime) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/qt/title_editor.py0000644000175100001730000000420100000000000021513 0ustar00runnerdocker00000000000000# (C) Copyright 2009-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # ------------------------------------------------------------------------------ # Copyright (c) 2008, Riverbank Computing Limited # All rights reserved. # # This software is provided without warranty under the terms of the BSD license. # However, when used with the GPL version of PyQt the additional terms # described in the PyQt GPL exception also apply # # Author: Riverbank Computing Limited # ------------------------------------------------------------------------------ from pyface.qt import QtCore, QtGui from .editor import Editor from pyface.api import HeadingText class SimpleEditor(Editor): def init(self, parent): """Finishes initializing the editor by creating the underlying toolkit widget. """ if isinstance(parent, QtGui.QLayout): parent = parent.parentWidget() self._control = HeadingText(parent=parent, create=False) self._control.create() self.control = self._control.control if self.factory.allow_selection: flags = ( self.control.textInteractionFlags() | QtCore.Qt.TextInteractionFlag.TextSelectableByMouse ) self.control.setTextInteractionFlags(flags) self.set_tooltip() def update_editor(self): """Updates the editor when the object trait changes external to the editor. """ self._control.text = self.str_value def dispose(self): """Cleanly dispose of the editor. This ensures that the wrapped Pyface Widget is cleaned-up. """ if self._control is not None: self._control.destroy() super().dispose() CustomEditor = SimpleEditor ReadonlyEditor = SimpleEditor TextEditor = SimpleEditor ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/qt/toolkit.py0000644000175100001730000003402600000000000020521 0ustar00runnerdocker00000000000000# (C) Copyright 2008-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # ------------------------------------------------------------------------------ # Copyright (c) 2007, Riverbank Computing Limited # All rights reserved. # # This software is provided without warranty under the terms of the BSD license. # However, when used with the GPL version of PyQt the additional terms described # in the PyQt GPL exception also apply. # # Author: Riverbank Computing Limited # ------------------------------------------------------------------------------ """ Defines the concrete implementations of the traits Toolkit interface for the PyQt user interface toolkit. """ # Make sure that importing from this backend is OK: from traitsui.toolkit import assert_toolkit_import assert_toolkit_import(["qt4", "qt"]) # Ensure that we can import Pyface backend. This starts App as a side-effect. from pyface.toolkit import toolkit_object as pyface_toolkit _app = pyface_toolkit("init:_app") from traits.trait_notifiers import set_ui_handler from pyface.api import SystemMetrics from pyface.qt import QtCore, QtGui, qt_api from traitsui.toolkit import Toolkit # ------------------------------------------------------------------------- # Handles UI notification handler requests that occur on a thread other than # the UI thread: # ------------------------------------------------------------------------- _QT_TRAITS_EVENT = QtCore.QEvent.Type(QtCore.QEvent.registerEventType()) class _CallAfter(QtCore.QObject): """This class dispatches a handler so that it executes in the main GUI thread (similar to the wx function). """ # The list of pending calls. _calls = [] # The mutex around the list of pending calls. _calls_mutex = QtCore.QMutex() def __init__(self, handler, *args, **kwds): """Initialise the call.""" QtCore.QObject.__init__(self) # Save the details of the call. self._handler = handler self._args = args self._kwds = kwds # Add this to the list. self._calls_mutex.lock() self._calls.append(self) self._calls_mutex.unlock() # Move to the main GUI thread. self.moveToThread(QtGui.QApplication.instance().thread()) # Post an event to be dispatched on the main GUI thread. Note that # we do not call QTimer.singleShot, which would be simpler, because # that only works on QThreads. We want regular Python threads to work. event = QtCore.QEvent(_QT_TRAITS_EVENT) QtGui.QApplication.instance().postEvent(self, event) def event(self, event): """QObject event handler.""" if event.type() == _QT_TRAITS_EVENT: # Invoke the handler self._handler(*self._args, **self._kwds) # We cannot remove from self._calls here. QObjects don't like being # garbage collected during event handlers (there are tracebacks, # plus maybe a memory leak, I think). QtCore.QTimer.singleShot(0, self._finished) return True else: return QtCore.QObject.event(self, event) def _finished(self): """Remove the call from the list, so it can be garbage collected.""" self._calls_mutex.lock() del self._calls[self._calls.index(self)] self._calls_mutex.unlock() def ui_handler(handler, *args, **kwds): """Handles UI notification handler requests that occur on a thread other than the UI thread. """ _CallAfter(handler, *args, **kwds) # Tell the traits notification handlers to use this UI handler set_ui_handler(ui_handler) class _KeyEventHook(QtCore.QObject): """ Event hook for handling Qt key events. """ def __init__(self, handler): super().__init__() self._handler = handler def eventFilter(self, object, event): if event.type() == QtCore.QEvent.Type.KeyPress: return self._handler(event) else: return QtCore.QObject.eventFilter(self, object, event) class GUIToolkit(Toolkit): """Implementation class for PyQt toolkit.""" def ui_panel(self, ui, parent): """Creates a PyQt panel-based user interface using information from the specified UI object. """ from . import ui_panel ui_panel.ui_panel(ui, parent) def ui_subpanel(self, ui, parent): """Creates a PyQt subpanel-based user interface using information from the specified UI object. """ from . import ui_panel ui_panel.ui_subpanel(ui, parent) def ui_livemodal(self, ui, parent): """Creates a PyQt modal "live update" dialog user interface using information from the specified UI object. """ from . import ui_live ui_live.ui_livemodal(ui, parent) def ui_live(self, ui, parent): """Creates a PyQt non-modal "live update" window user interface using information from the specified UI object. """ from . import ui_live ui_live.ui_live(ui, parent) def ui_modal(self, ui, parent): """Creates a PyQt modal dialog user interface using information from the specified UI object. """ from . import ui_modal ui_modal.ui_modal(ui, parent) def ui_nonmodal(self, ui, parent): """Creates a PyQt non-modal dialog user interface using information from the specified UI object. """ from . import ui_modal ui_modal.ui_nonmodal(ui, parent) def ui_wizard(self, ui, parent): """Creates a PyQt wizard dialog user interface using information from the specified UI object. """ import ui_wizard ui_wizard.ui_wizard(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. """ from . import view_application return view_application.view_application( context, view, kind, handler, id, scrollable, args ) def position(self, ui): """Positions the associated dialog window on the display.""" view = ui.view window = ui.control # Set up the default position of the window: parent = window.parent() if parent is None: px = 0 py = 0 pdx = SystemMetrics().screen_width pdy = SystemMetrics().screen_height else: pos = parent.pos() if int(parent.windowFlags()) & QtCore.Qt.WindowType.Window == 0: pos = parent.mapToGlobal(pos) px = pos.x() py = pos.y() pdx = parent.width() pdy = parent.height() # Get the window's prefered size. size_hint = window.sizeHint() # Calculate the correct width and height for the window: cur_width = size_hint.width() cur_height = size_hint.height() width = view.width height = view.height if width < 0.0: width = cur_width elif width <= 1.0: width = int(width * SystemMetrics().screen_width) else: width = int(width) if height < 0.0: height = cur_height elif height <= 1.0: height = int(height * SystemMetrics().screen_height) else: height = int(height) # Calculate the correct position for the window: x = view.x y = view.y if x < -99999.0: x = px + ((pdx - width) // 2) elif x <= -1.0: x = px + pdx - width + int(x) + 1 elif x < 0.0: x = px + pdx - width + int(x * pdx) elif x <= 1.0: x = px + int(x * pdx) else: x = int(x) if y < -99999.0: y = py + ((pdy - height) // 2) elif y <= -1.0: y = py + pdy - height + int(y) + 1 elif x < 0.0: y = py + pdy - height + int(y * pdy) elif y <= 1.0: y = py + int(y * pdy) else: y = int(y) # Position and size the window as requested: layout = window.layout() if layout.sizeConstraint() == QtGui.QLayout.SizeConstraint.SetFixedSize: layout.setSizeConstraint(QtGui.QLayout.SizeConstraint.SetDefaultConstraint) window.move(max(0, x), max(0, y)) window.setFixedSize(QtCore.QSize(width, height)) else: window.setGeometry(max(0, x), max(0, y), width, height) def show_help(self, ui, control): """Shows a help window for a specified UI and control.""" from . import ui_panel ui_panel.show_help(ui, control) def save_window(self, ui): """Saves user preference information associated with a UI window.""" from . import helper helper.save_window(ui) 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 set_title(self, ui): """Sets the title for the UI window.""" ui.control.setWindowTitle(ui.title) def set_icon(self, ui): """Sets the icon for the UI window.""" from pyface.image_resource import ImageResource if isinstance(ui.icon, ImageResource): ui.control.setWindowIcon(ui.icon.create_icon()) def key_event_to_name(self, event): """Converts a keystroke event into a corresponding key name.""" from . import key_event_to_name return key_event_to_name.key_event_to_name(event) def hook_events(self, ui, control, events=None, handler=None): """Hooks all specified events for all controls in a UI so that they can be routed to the correct event handler. """ if events is None: # FIXME: Implement drag and drop events ala toolkit.py for wx return elif events == "keys": # It's unsafe to parent the event filter with the object it's # filtering, so we store a reference to it here to ensure that it's # not garbage collected prematurely. ui._key_event_hook = _KeyEventHook(handler=handler) control.installEventFilter(ui._key_event_hook) def skip_event(self, event): """Indicates that an event should continue to be processed by the toolkit. """ event.ignore() def destroy_control(self, control): """Destroys a specified GUI toolkit control.""" # Block signals to prevent any editors from being updated (the control # will not be deleted immediately). control.blockSignals(True) # This may be called from within the finished() signal handler so we # need to do the delete after the handler has returned. control.hide() control.deleteLater() def destroy_children(self, control): """Destroys all of the child controls of a specified GUI toolkit control. """ for w in control.children(): # Only destroy widgets. if isinstance(w, QtGui.QWidget): # This may be called from within the finished() signal handler # so we need to do the delete after the handler has returned. w.deleteLater() def image_size(self, image): """Returns a ( width, height ) tuple containing the size of a specified toolkit image. """ return (image.width(), image.height()) 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": QtGui.QApplication.palette().color( QtGui.QPalette.ColorRole.Window ) } def color_trait(self, *args, **traits): from . import color_trait as ct return ct.PyQtColor(*args, **traits) def rgb_color_trait(self, *args, **traits): from . import rgb_color_trait as rgbct return rgbct.RGBColor(*args, **traits) def font_trait(self, *args, **traits): from . import font_trait as ft return ft.PyQtFont(*args, **traits) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/qt/tree_editor.py0000644000175100001730000020275200000000000021344 0ustar00runnerdocker00000000000000# (C) Copyright 2008-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # ------------------------------------------------------------------------------ # Copyright (c) 2007, Riverbank Computing Limited # All rights reserved. # # This software is provided without warranty under the terms of the BSD license. # However, when used with the GPL version of PyQt the additional terms # described in the PyQt GPL exception also apply # # Author: Riverbank Computing Limited # ------------------------------------------------------------------------------ """ Defines the tree editor for the PyQt user interface toolkit. """ import copy import collections.abc from functools import partial from itertools import zip_longest import logging from pyface.qt import QtCore, QtGui, is_qt5 from pyface.api import ImageResource from pyface.ui_traits import convert_image from pyface.timer.api import do_later from traits.api import Any, Event, Int from traitsui.editors.tree_editor import ( CopyAction, CutAction, DeleteAction, NewAction, PasteAction, RenameAction, ) from traitsui.tree_node import ( ITreeNodeAdapterBridge, MultiTreeNode, ObjectTreeNode, TreeNode, ) from traitsui.menu import Menu, Action, Separator from traitsui.ui_traits import SequenceTypes from traitsui.undo import ListUndoItem from .clipboard import clipboard, PyMimeData from .editor import Editor from .helper import pixmap_cache, qobject_is_valid from .tree_node_renderers import WordWrapRenderer logger = logging.getLogger(__name__) # The renderer to use when word_wrap is True. DEFAULT_WRAP_RENDERER = WordWrapRenderer() class SimpleEditor(Editor): """Simple style of tree editor.""" # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- #: Is the tree editor is scrollable? This value overrides the default. scrollable = True #: Allows an external agent to set the tree selection selection = Event() #: The currently selected object selected = Any() #: The event fired when a tree node is activated by double clicking or #: pressing the Enter key on a node. activated = Event() #: The event fired when a tree node is clicked on: click = Event() #: The event fired when a tree node is double-clicked on: dclick = Event() #: The event fired when the application wants to veto an operation: veto = Event() #: The vent fired when the application wants to refresh the viewport. refresh = Event() def init(self, parent): """Finishes initializing the editor by creating the underlying toolkit widget. """ factory = self.factory self._editor = None if factory.editable: # Check to see if the tree view is based on a shared trait editor: if factory.shared_editor: factory_editor = factory.editor # If this is the editor that defines the trait editor panel: if factory_editor is None: # Remember which editor has the trait editor in the # factory: factory._editor = self # Create the trait editor panel: self.control = sa = QtGui.QScrollArea() sa.setFrameShape(QtGui.QFrame.Shape.NoFrame) sa.setWidgetResizable(True) self.control._node_ui = self.control._editor_nid = None # Check to see if there are any existing editors that are # waiting to be bound to the trait editor panel: editors = factory._shared_editors if editors is not None: for editor in factory._shared_editors: # If the editor is part of this UI: if editor.ui is self.ui: # Then bind it to the trait editor panel: editor._editor = self.control # Indicate all pending editors have been processed: factory._shared_editors = None # We only needed to build the trait editor panel, so exit: return # Check to see if the matching trait editor panel has been # created yet: editor = factory_editor._editor if (editor is None) or (editor.ui is not self.ui): # If not, add ourselves to the list of pending editors: shared_editors = factory_editor._shared_editors if shared_editors is None: factory_editor._shared_editors = shared_editors = [] shared_editors.append(self) else: # Otherwise, bind our trait editor panel to the shared one: self._editor = editor.control # Finally, create only the tree control: self.control = self._tree = _TreeWidget(self) else: # If editable, create a tree control and an editor panel: self._tree = _TreeWidget(self) self._editor = sa = QtGui.QScrollArea() sa.setFrameShape(QtGui.QFrame.Shape.NoFrame) sa.setWidgetResizable(True) sa._node_ui = sa._editor_nid = None if factory.orientation == "horizontal": orient = QtCore.Qt.Orientation.Horizontal else: orient = QtCore.Qt.Orientation.Vertical self.control = splitter = QtGui.QSplitter(orient) splitter.setSizePolicy( QtGui.QSizePolicy.Policy.Expanding, QtGui.QSizePolicy.Policy.Expanding ) splitter.addWidget(self._tree) splitter.addWidget(sa) else: # Otherwise, just create the tree control: self.control = self._tree = _TreeWidget(self) # Create our item delegate delegate = TreeItemDelegate() delegate.editor = self self._tree.setItemDelegate(delegate) # From Qt Docs: QAbstractItemView does not take ownership of `delegate` self._item_delegate = delegate # Set up the mapping between objects and tree id's: self._map = {} # Initialize the 'undo state' stack: self._undoable = [] # Synchronize external object traits with the editor: self.sync_value(factory.refresh, "refresh") self.sync_value(factory.selected, "selected") self.sync_value(factory.activated, "activated", "to") self.sync_value(factory.click, "click", "to") self.sync_value(factory.dclick, "dclick", "to") self.sync_value(factory.veto, "veto", "from") def _selection_changed(self, selection): """Handles the **selection** event.""" try: tree = self._tree if not isinstance(selection, str) and isinstance( selection, collections.abc.Iterable ): item_selection = QtGui.QItemSelection() for sel in selection: item = self._object_info(sel)[2] idx = tree.indexFromItem(item) item_selection.append(QtGui.QItemSelectionRange(idx)) tree.selectionModel().select( item_selection, QtGui.QItemSelectionModel.SelectionFlag.ClearAndSelect ) else: tree.setCurrentItem(self._object_info(selection)[2]) except: from traitsui.api import raise_to_debug raise_to_debug() def _selected_changed(self, selected): """Handles the **selected** trait being changed.""" if not self._no_update_selected: self._selection_changed(selected) def _veto_changed(self): """Handles the 'veto' event being fired.""" self._veto = True def _refresh_changed(self): """Update the viewport.""" self._tree.viewport().update() def dispose(self): """Disposes of the contents of an editor.""" if self._tree is not None: # Stop the chatter (specifically about the changing selection). self._tree.blockSignals(True) self._delete_node(self._tree.invisibleRootItem()) self._tree = None super().dispose() def expand_levels(self, nid, levels, expand=True): """Expands from the specified node the specified number of sub-levels.""" if levels > 0: try: expanded, node, object = self._get_node_data(nid) except Exception: # node is either not ready or has been deleted return if self._has_children(node, object): self._expand_node(nid) if expand: nid.setExpanded(True) for cnid in self._nodes_for(nid): self.expand_levels(cnid, levels - 1) def expand_all(self): """Expands all expandable nodes in the tree. Warning: If the Tree contains a large number of items, this function will be very slow. """ # For more info on the QtGui.QTreeWidgetItemIterator object, # see https://doc.qt.io/qt-5/qtreewidgetitemiterator.html iterator = QtGui.QTreeWidgetItemIterator(self._tree) while iterator.value(): item = iterator.value() try: expanded, node, object = self._get_node_data(item) except Exception: # node is either not ready or has been deleted continue if self._has_children(node, object): self._expand_node(item) item.setExpanded(True) iterator += 1 def update_editor(self): """Updates the editor when the object trait changes externally to the editor. """ tree = self._tree if tree is None: return saved_state = {} object, node = self._node_for(self.old_value) old_nid = self._get_object_nid(object, node.get_children_id(object)) if old_nid: self._delete_node(old_nid) object, node = self._node_for(self.old_value) old_nid = self._get_object_nid(object, node.get_children_id(object)) if old_nid: self._delete_node(old_nid) tree.clear() self._map = {} object, node = self._node_for(self.value) if node is not None: if self.factory.hide_root: nid = tree.invisibleRootItem() else: nid = self._create_item(tree, node, object) self._map[id(object)] = [(node.get_children_id(object), nid)] self._add_listeners(node, object) self._set_node_data(nid, (False, node, object)) if self.factory.hide_root or self._has_children(node, object): self._expand_node(nid) if not self.factory.hide_root: nid.setExpanded(True) tree.setCurrentItem(nid) self.expand_levels(nid, self.factory.auto_open, False) ncolumns = self._tree.columnCount() if ncolumns > 1: for i in range(ncolumns): self._tree.resizeColumnToContents(i) # FIXME: Clear the current editor (if any)... def get_error_control(self): """Returns the editor's control for indicating error status.""" return self._tree def _get_brush(self, color): if isinstance(color, SequenceTypes): q_color = QtGui.QColor(*color) else: q_color = QtGui.QColor(color) return QtGui.QBrush(q_color) def _set_column_labels(self, nid, node, object): """Set the column labels.""" column_labels = node.get_column_labels(object) for i, (header, label) in enumerate( zip_longest(self.factory.column_headers[1:], column_labels), 1 ): renderer = node.get_renderer(object, i) handles_text = renderer.handles_text if renderer else False if header is None or label is None or handles_text: nid.setText(i, "") else: nid.setText(i, label) def _create_item(self, nid, node, object, index=None): """Create a new TreeWidgetItem as per word_wrap policy. Index is the index of the new node in the parent: None implies append the child to the end.""" if index is None: cnid = QtGui.QTreeWidgetItem(nid) else: cnid = QtGui.QTreeWidgetItem() nid.insertChild(index, cnid) renderer = node.get_renderer(object) handles_text = getattr(renderer, "handles_text", False) handles_icon = getattr(renderer, "handles_icon", False) if not (self.factory.word_wrap or handles_text): cnid.setText(0, node.get_label(object)) if not handles_icon: cnid.setIcon(0, self._get_icon(node, object)) cnid.setToolTip(0, node.get_tooltip(object)) self._set_column_labels(cnid, node, object) color = node.get_background(object) if color: cnid.setBackground(0, self._get_brush(color)) color = node.get_foreground(object) if color: cnid.setForeground(0, self._get_brush(color)) return cnid def _set_label(self, nid, col=0): """Set the label of the specified item""" if col != 0: # these are handled by _set_column_labels return try: expanded, node, object = self._get_node_data(nid) except Exception: # node is either not ready or has been deleted return renderer = node.get_renderer(object) handles_text = getattr(renderer, "handles_text", False) if self.factory.word_wrap or handles_text: nid.setText(col, "") else: nid.setText(col, node.get_label(object)) def _append_node(self, nid, node, object): """Appends a new node to the specified node.""" return self._insert_node(nid, None, node, object) def _insert_node(self, nid, index, node, object): """Inserts a new node before a specified index into the children of the specified node. """ cnid = self._create_item(nid, node, object, index) has_children = self._has_children(node, object) self._set_node_data(cnid, (False, node, object)) self._map.setdefault(id(object), []).append( (node.get_children_id(object), cnid) ) self._add_listeners(node, object) # Automatically expand the new node (if requested): if has_children: if node.can_auto_open(object): cnid.setExpanded(True) else: # Qt only draws the control that expands the tree if there is a # child. As the tree is being populated lazily we create a # dummy that will be removed when the node is expanded for the # first time. cnid._dummy = QtGui.QTreeWidgetItem(cnid) # Return the newly created node: return cnid def _delete_node(self, nid): """Deletes a specified tree node and all its children.""" for cnid in self._nodes_for(nid): self._delete_node(cnid) # See if it is a dummy. pnid = nid.parent() if pnid is not None and getattr(pnid, "_dummy", None) is nid: pnid.removeChild(nid) del pnid._dummy return try: expanded, node, object = self._get_node_data(nid) except Exception: # The node has already been deleted. pass else: id_object = id(object) object_info = self._map[id_object] for i, info in enumerate(object_info): # QTreeWidgetItem does not have an equal operator, so use id() if id(nid) == id(info[1]): del object_info[i] break if len(object_info) == 0: self._remove_listeners(node, object) del self._map[id_object] if pnid is None: self._tree.takeTopLevelItem(self._tree.indexOfTopLevelItem(nid)) else: pnid.removeChild(nid) # If the deleted node had an active editor panel showing, remove it: # Note: QTreeWidgetItem does not have an equal operator, so use id() if (self._editor is not None) and ( id(nid) == id(self._editor._editor_nid) ): self._clear_editor() def _expand_node(self, nid): """Expands the contents of a specified node (if required).""" try: expanded, node, object = self._get_node_data(nid) except Exception: # The node has already been deleted. return # Lazily populate the item's children: if not expanded: # Remove any dummy node. dummy = getattr(nid, "_dummy", None) if dummy is not None: nid.removeChild(dummy) del nid._dummy for child in node.get_children(object): child, child_node = self._node_for(child) if child_node is not None: self._append_node(nid, child_node, child) # Indicate the item is now populated: self._set_node_data(nid, (True, node, object)) def _nodes_for(self, nid): """Returns all child node ids of a specified node id.""" return [nid.child(i) for i in range(nid.childCount())] def _node_index(self, nid): pnid = nid.parent() if pnid is None: if self.factory.hide_root: pnid = self._tree.invisibleRootItem() if pnid is None: return (None, None, None) for i in range(pnid.childCount()): if pnid.child(i) is nid: try: _, pnode, pobject = self._get_node_data(pnid) except Exception: continue return (pnode, pobject, i) else: # doesn't match any node, so return None return (None, None, None) def _has_children(self, node, object): """Returns whether a specified object has any children.""" return node.allows_children(object) and node.has_children(object) # ------------------------------------------------------------------------- # Returns the icon index for the specified object: # ------------------------------------------------------------------------- STD_ICON_MAP = { "": QtGui.QStyle.StandardPixmap.SP_FileIcon, "": QtGui.QStyle.StandardPixmap.SP_DirClosedIcon, "": QtGui.QStyle.StandardPixmap.SP_DirOpenIcon, } def _get_icon(self, node, object, is_expanded=False): """Returns the index of the specified object icon.""" if not self.factory.show_icons: return QtGui.QIcon() icon_name = node.get_icon(object, is_expanded) if isinstance(icon_name, str): if icon_name.startswith("@"): image_resource = convert_image(icon_name, 4) return image_resource.create_icon() elif icon_name in self.STD_ICON_MAP: icon = self.STD_ICON_MAP[icon_name] return self._tree.style().standardIcon(icon) path = node.get_icon_path(object) if isinstance(path, str): path = [path, node] else: path = path + [node] image_resource = ImageResource(icon_name, path) elif isinstance(icon_name, ImageResource): image_resource = icon_name elif isinstance(icon_name, tuple): if max(icon_name) <= 1.0: # rgb(a) color tuple between 0.0 and 1.0 color = QtGui.QColor.fromRgbF(*icon_name) else: # rgb(a) color tuple between 0 and 255 color = QtGui.QColor.fromRgb(*icon_name) return self._icon_from_color(color) elif isinstance(icon_name, QtGui.QColor): return self._icon_from_color(icon_name) else: raise ValueError( "Icon value must be a string or color or color tuple or " + "IImageResource instance: " + "given {!r}".format(icon_name) ) file_name = image_resource.absolute_path return QtGui.QIcon(pixmap_cache(file_name)) def _icon_from_color(self, color): """Create a square icon filled with the given color.""" pixmap = QtGui.QPixmap(self._tree.iconSize()) pixmap.fill(color) return QtGui.QIcon(pixmap) def _add_listeners(self, node, object): """Adds the event listeners for a specified object.""" if node.allows_children(object): node.when_children_replaced(object, self._children_replaced, False) node.when_children_changed(object, self._children_updated, False) node.when_label_changed(object, self._label_updated, False) node.when_column_labels_change( object, self._column_labels_updated, False ) def _remove_listeners(self, node, object): """Removes any event listeners from a specified object.""" if node.allows_children(object): node.when_children_replaced(object, self._children_replaced, True) node.when_children_changed(object, self._children_updated, True) node.when_label_changed(object, self._label_updated, True) node.when_column_labels_change( object, self._column_labels_updated, True ) def _object_info(self, object, name=""): """Returns the tree node data for a specified object in the form ( expanded, node, nid ). """ info = self._map[id(object)] for name2, nid in info: if name == name2: break else: nid = info[0][1] expanded, node, ignore = self._get_node_data(nid) return (expanded, node, nid) def _object_info_for(self, object, name=""): """Returns the tree node data for a specified object as a list of the form: [ ( expanded, node, nid ), ... ]. """ result = [] for name2, nid in self._map[id(object)]: if name == name2: try: expanded, node, object = self._get_node_data(nid) except Exception: # The node has already been deleted. continue result.append((expanded, node, nid)) return result def _node_for(self, object): """Returns the TreeNode associated with a specified object.""" if ( (isinstance(object, tuple)) and (len(object) == 2) and isinstance(object[1], TreeNode) ): return object # Select all nodes which understand this object: factory = self.factory nodes = [node for node in factory.nodes if node.is_node_for(object)] # If only one found, we're done, return it: if len(nodes) == 1: return (object, nodes[0]) # If none found, give up: if len(nodes) == 0: return (object, ITreeNodeAdapterBridge(adapter=object)) # Use all selected nodes that have the same 'node_for' list as the # first selected node: base = nodes[0].node_for nodes = [node for node in nodes if base == node.node_for] # If only one left, then return that node: if len(nodes) == 1: return (object, nodes[0]) # Otherwise, return a MultiTreeNode based on all selected nodes... # Use the node with no specified children as the root node. If not # found, just use the first selected node as the 'root node': root_node = None for i, node in enumerate(nodes): if node.children == "": root_node = node del nodes[i] break else: root_node = nodes[0] # If we have a matching MultiTreeNode already cached, return it: key = (root_node,) + tuple(nodes) if key in factory.multi_nodes: return (object, factory.multi_nodes[key]) # Otherwise create one, cache it, and return it: factory.multi_nodes[key] = multi_node = MultiTreeNode( root_node=root_node, nodes=nodes ) return (object, multi_node) def _node_for_class(self, klass): """Returns the TreeNode associated with a specified class.""" for node in self.factory.nodes: if issubclass(klass, tuple(node.node_for)): return node return None def _update_icon(self, nid): """Updates the icon for a specified node.""" try: expanded, node, object = self._get_node_data(nid) except Exception: # The node has already been deleted. return renderer = node.get_renderer(object) if renderer is None or not renderer.handles_icon: nid.setIcon(0, self._get_icon(node, object, expanded)) else: nid.setIcon(0, QtGui.QIcon()) def _begin_undo(self): """Begins an "undoable" transaction.""" ui = self.ui self._undoable.append(ui._undoable) if (ui._undoable == -1) and (ui.history is not None): ui._undoable = ui.history.now def _end_undo(self): if self._undoable.pop() == -1: self.ui._undoable = -1 def _get_undo_item(self, object, name, event): return ListUndoItem( object=object, name=name, index=event.index, added=event.added, removed=event.removed, ) def _undoable_append(self, node, object, data, make_copy=True): """Performs an undoable append operation.""" try: self._begin_undo() if make_copy: data = copy.deepcopy(data) node.append_child(object, data) finally: self._end_undo() def _undoable_insert(self, node, object, index, data, make_copy=True): """Performs an undoable insert operation.""" try: self._begin_undo() if make_copy: data = copy.deepcopy(data) node.insert_child(object, index, data) finally: self._end_undo() def _undoable_delete(self, node, object, index): """Performs an undoable delete operation.""" try: self._begin_undo() node.delete_child(object, index) finally: self._end_undo() def _get_object_nid(self, object, name=""): """Gets the ID associated with a specified object (if any).""" info = self._map.get(id(object)) if info is None: return None for name2, nid in info: if name == name2: return nid else: return info[0][1] def _clear_editor(self): """Clears the current editor pane (if any).""" editor = self._editor if editor._node_ui is not None: editor.setWidget(None) editor._node_ui.dispose() editor._node_ui = editor._editor_nid = None # ------------------------------------------------------------------------- # Gets/Sets the node specific data: # ------------------------------------------------------------------------- @staticmethod def _get_node_data(nid): """Gets the node specific data.""" if not qobject_is_valid(nid): raise RuntimeError(f"Qt object {nid} for node nolonger exists.") return nid._py_data @staticmethod def _set_node_data(nid, data): """Sets the node specific data.""" nid._py_data = data # ----- User callable methods: -------------------------------------------- def get_object(self, nid): """Gets the object associated with a specified node.""" return self._get_node_data(nid)[2] def get_parent(self, object, name=""): """Returns the object that is the immmediate parent of a specified object in the tree. """ nid = self._get_object_nid(object, name) if nid is not None: pnid = nid.parent() if pnid is not self._tree.invisibleRootItem(): return self.get_object(pnid) return None def get_node(self, object, name=""): """Returns the node associated with a specified object.""" nid = self._get_object_nid(object, name) if nid is not None: return self._get_node_data(nid)[1] return None # ----- Tree event handlers: ---------------------------------------------- def _on_item_expanded(self, nid): """Handles a tree node being expanded.""" try: _, node, object = self._get_node_data(nid) except Exception: # The node has already been deleted. return # If 'auto_close' requested for this node type, close all of the node's # siblings: if node.can_auto_close(object): parent = nid.parent() if parent is not None: for snid in self._nodes_for(parent): if snid is not nid: snid.setExpanded(False) # Expand the node (i.e. populate its children if they are not there # yet): self._expand_node(nid) self._update_icon(nid) def _on_item_collapsed(self, nid): """Handles a tree node being collapsed.""" self._update_icon(nid) def _on_item_clicked(self, nid, col): """Handles a tree item being clicked.""" try: _, node, object = self._get_node_data(nid) except Exception: # The node has already been deleted. return if node.click(object) is True and self.factory.on_click is not None: self.ui.evaluate(self.factory.on_click, object) # Fire the 'click' event with the object as its value: self.click = object def _on_item_dclicked(self, nid, col): """Handles a tree item being double-clicked.""" try: _, node, object = self._get_node_data(nid) except Exception: # The node has already been deleted. return if node.dclick(object) is True: if self.factory.on_dclick is not None: self.ui.evaluate(self.factory.on_dclick, object) self._veto = True else: self._veto = True # Fire the 'dclick' event with the clicked on object as value: self.dclick = object def _on_item_activated(self, nid, col): """Handles a tree item being activated.""" try: _, node, object = self._get_node_data(nid) except Exception: # The node has already been deleted. return if node.activated(object) is True: if self.factory.on_activated is not None: self.ui.evaluate(self.factory.on_activated, object) self._veto = True else: self._veto = True # Fire the 'activated' event with the clicked on object as value: self.activated = object def _on_tree_sel_changed(self): """Handles a tree node being selected.""" # Get the new selection: nids = self._tree.selectedItems() selected = [] first = True for nid in nids: try: node = self._get_node_data(nid)[2] except Exception: continue selected.append(node) for nid in nids: # If there is a real selection, get the associated object: try: expanded, sel_node, sel_object = self._get_node_data(nid) except Exception: continue # Try to inform the node specific handler of the selection, if # there are multiple selections, we only care about the first if first: node = sel_node object = sel_object not_handled = node.select(sel_object) first = False if len(selected) == 0: nid = None object = None not_handled = True # Set the value of the new selection: if self.factory.selection_mode == "single": self._no_update_selected = True self.selected = object self._no_update_selected = False else: self._no_update_selected = True self.selected = selected self._no_update_selected = False # If no one has been notified of the selection yet, inform the editor's # select handler (if any) of the new selection: if not_handled is True: self.ui.evaluate(self.factory.on_select, object) # Check to see if there is an associated node editor pane: editor = self._editor if editor is not None: # If we already had a node editor, destroy it: editor.setUpdatesEnabled(False) self._clear_editor() # If there is a selected object, create a new editor for it: if object is not None: # Try to chain the undo history to the main undo history: view = node.get_view(object) if view is None or isinstance(view, str): view = object.trait_view(view) if (self.ui.history is not None) or (view.kind == "subpanel"): ui = object.edit_traits( parent=editor, view=view, kind="subpanel" ) else: # Otherwise, just set up our own new one: ui = object.edit_traits( parent=editor, view=view, kind="panel" ) # Make our UI the parent of the new UI: ui.parent = self.ui # Remember the new editor's UI and node info: editor._node_ui = ui editor._editor_nid = nid # Finish setting up the editor: if ui.control.layout() is not None: ui.control.layout().setContentsMargins(0, 0, 0, 0) editor.setWidget(ui.control) # Allow the editor view to show any changes that have occurred: editor.setUpdatesEnabled(True) def _on_context_menu(self, pos): """Handles the user requesting a context menu on a tree node.""" nid = self._tree.itemAt(pos) if nid is None: return try: _, node, object = self._get_node_data(nid) except Exception: return self._data = (node, object, nid) self._context = { "object": object, "editor": self, "node": node, "info": self.ui.info, "handler": self.ui.handler, } # Try to get the parent node of the node clicked on: pnid = nid.parent() if pnid is None or pnid is self._tree.invisibleRootItem(): parent_node = parent_object = None else: _, parent_node, parent_object = self._get_node_data(pnid) self._menu_node = node self._menu_parent_node = parent_node self._menu_parent_object = parent_object menu = node.get_menu(object) if menu is None: # Use the standard, default menu: menu = self._standard_menu(node, object) elif isinstance(menu, Menu): # Use the menu specified by the node: group = menu.find_group(NewAction) if group is not None: # Reset the group for the current usage in case it is shared # - the call to `node.get_add()` is potentially dynamic and # the callback `_menu_new_node()` captures state about this # particular TreeEditor instance group.clear() actions = self._new_actions(node, object) if len(actions) > 0: group.insert(0, Menu(name="New", *actions)) else: # All other values mean no menu should be displayed: menu = None # Only display the menu if a valid menu is defined: if menu is not None: qmenu = menu.create_menu(self._tree, self) qmenu.exec_(self._tree.mapToGlobal(pos)) # Reset all menu related cached values: self._data = ( self._context ) = ( self._menu_node ) = self._menu_parent_node = self._menu_parent_object = None def _standard_menu(self, node, object): """Returns the standard contextual pop-up menu.""" actions = [ CutAction, CopyAction, PasteAction, Separator(), DeleteAction, Separator(), RenameAction, ] # See if the 'New' menu section should be added: items = self._new_actions(node, object) if len(items) > 0: actions[0:0] = [Menu(name="New", *items), Separator()] return Menu(*actions) def _new_actions(self, node, object): """Returns a list of Actions that will create new objects.""" object = self._data[1] items = [] add = node.get_add(object) # return early if there are no items to be added in the tree if len(add) == 0: return items for klass in add: prompt = False factory = None if isinstance(klass, tuple): if len(klass) == 2: klass, prompt = klass elif len(klass) == 3: klass, prompt, factory = klass add_node = self._node_for_class(klass) if add_node is None: continue class_name = klass.__name__ name = add_node.get_name(object) if name == "": name = class_name if factory is None: factory = klass def perform_add(object, factory, prompt): self._menu_new_node(factory, prompt) on_perform = partial(perform_add, factory=factory, prompt=prompt) items.append(Action(name=name, on_perform=on_perform)) return items # ------------------------------------------------------------------------- # Menu action helper methods: # ------------------------------------------------------------------------- def _is_copyable(self, object): parent = self._menu_parent_node if isinstance(parent, ObjectTreeNode): return parent.can_copy(self._menu_parent_object) return (parent is not None) and parent.can_copy(object) def _is_cutable(self, object): parent = self._menu_parent_node if isinstance(parent, ObjectTreeNode): can_cut = parent.can_copy( self._menu_parent_object ) and parent.can_delete(self._menu_parent_object) else: can_cut = ( (parent is not None) and parent.can_copy(object) and parent.can_delete(object) ) return can_cut and self._menu_node.can_delete_me(object) def _is_pasteable(self, object): return self._menu_node.can_add(object, clipboard.instance_type) def _is_deletable(self, object): parent = self._menu_parent_node if isinstance(parent, ObjectTreeNode): can_delete = parent.can_delete(self._menu_parent_object) else: can_delete = (parent is not None) and parent.can_delete(object) return can_delete and self._menu_node.can_delete_me(object) def _is_renameable(self, object): parent = self._menu_parent_node if isinstance(parent, ObjectTreeNode): can_rename = parent.can_rename(self._menu_parent_object) else: can_rename = (parent is not None) and parent.can_rename(object) can_rename = can_rename and self._menu_node.can_rename_me(object) # Set the widget item's editable flag appropriately. nid = self._get_object_nid(object) flags = nid.flags() if can_rename: flags |= QtCore.Qt.ItemFlag.ItemIsEditable else: flags &= ~QtCore.Qt.ItemFlag.ItemIsEditable nid.setFlags(flags) return can_rename def _is_droppable(self, node, object, add_object, for_insert): """Returns whether a given object is droppable on the node.""" if for_insert and (not node.can_insert(object)): return False return node.can_add(object, add_object) def _drop_object(self, node, object, dropped_object, make_copy=True): """Returns a droppable version of a specified object.""" new_object = node.drop_object(object, dropped_object) if (new_object is not dropped_object) or (not make_copy): return new_object return copy.deepcopy(new_object) # ----- pyface.action 'controller' interface implementation: -------------- def add_to_menu(self, menu_item): """Adds a menu item to the menu bar being constructed.""" action = menu_item.item.action self.eval_when(action.enabled_when, menu_item, "enabled") self.eval_when(action.checked_when, menu_item, "checked") def add_to_toolbar(self, toolbar_item): """Adds a toolbar item to the toolbar being constructed.""" self.add_to_menu(toolbar_item) def can_add_to_menu(self, action): """Returns whether the action should be defined in the user interface.""" if action.defined_when != "": if not eval(action.defined_when, globals(), self._context): return False if action.visible_when != "": if not eval(action.visible_when, globals(), self._context): return False return True def can_add_to_toolbar(self, action): """Returns whether the toolbar action should be defined in the user interface. """ return self.can_add_to_menu(action) def perform(self, action, action_event=None): """Performs the action described by a specified Action object.""" self.ui.do_undoable(self._perform, action) def _perform(self, action): node, object, nid = self._data method_name = action.action info = self.ui.info handler = self.ui.handler if method_name.find(".") >= 0: if method_name.find("(") < 0: method_name += "()" try: eval( method_name, globals(), { "object": object, "editor": self, "node": node, "info": info, "handler": handler, }, ) except Exception: from traitsui.api import raise_to_debug raise_to_debug() return method = getattr(handler, method_name, None) if method is not None: method(info, object) return if action.on_perform is not None: action.on_perform(object) # ----- Menu support methods: --------------------------------------------- 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._context): value = False except Exception as e: logger.warning( "Exception (%s) raised when evaluating the " "condition %s. Returning True." % (e, condition) ) setattr(object, trait, value) # ----- Menu event handlers: ---------------------------------------------- def _menu_copy_node(self): """Copies the current tree node object to the paste buffer.""" clipboard.instance = self._data[1] self._data = None def _menu_cut_node(self): """Cuts the current tree node object into the paste buffer.""" node, object, nid = self._data clipboard.instance = object self._data = None self._undoable_delete(*self._node_index(nid)) def _menu_paste_node(self): """Pastes the current contents of the paste buffer into the current node. """ node, object, nid = self._data self._data = None self._undoable_append(node, object, clipboard.instance, True) def _menu_delete_node(self): """Deletes the current node from the tree.""" node, object, nid = self._data self._data = None rc = node.confirm_delete(object) if rc is not False: if rc is not True: if self.ui.history is None: # If no undo history, ask user to confirm the delete: butn = QtGui.QMessageBox.question( self._tree, "Confirm Deletion", "Are you sure you want to delete %s?" % node.get_label(object), QtGui.QMessageBox.StandardButton.Yes | QtGui.QMessageBox.StandardButton.No, ) if butn != QtGui.QMessageBox.StandardButton.Yes: return self._undoable_delete(*self._node_index(nid)) def _menu_rename_node(self): """Rename the current node.""" _, _, nid = self._data self._data = None self._tree.editItem(nid) def _on_nid_changed(self, nid, col): """Handle changes to a widget item.""" # The node data may not have been set up for the nid yet. Ignore it if # it hasn't. try: _, node, object = self._get_node_data(nid) except Exception: return new_label = str(nid.text(col)) old_label = node.get_label(object) if new_label != old_label: if new_label != "": node.set_label(object, new_label) else: self._set_label(nid, col) def _menu_new_node(self, factory, prompt=False): """Adds a new object to the current node.""" node, object, nid = self._data self._data = None new_object = factory() if new_object is None: return # support None-type returns from factory if (not prompt) or new_object.edit_traits( parent=self.control, kind="livemodal" ).result: self._undoable_append(node, object, new_object, False) # Automatically select the new object if editing is being # performed: if self.factory.editable: self._tree.setCurrentItem(nid.child(nid.childCount() - 1)) # ----- Model event handlers: --------------------------------------------- def _children_replaced(self, object, name="", new=None): """Handles the children of a node being completely replaced.""" tree = self._tree for expanded, node, nid in self._object_info_for(object, name): children = node.get_children(object) # Only add/remove the changes if the node has already been # expanded: if expanded: # Delete all current child nodes: for cnid in self._nodes_for(nid): self._delete_node(cnid) # Add all of the children back in as new nodes: for child in children: child, child_node = self._node_for(child) if child_node is not None: self._append_node(nid, child_node, child) else: dummy = getattr(nid, "_dummy", None) if dummy is None and len(children) > 0: # if model now has children add dummy child nid._dummy = QtGui.QTreeWidgetItem(nid) elif dummy is not None and len(children) == 0: # if model no longer has children remove dummy child nid.removeChild(dummy) del nid._dummy # Try to expand the node (if requested): if node.can_auto_open(object): nid.setExpanded(True) def _children_updated(self, object, name, event): """Handles the children of a node being changed.""" # Log the change that was made made (removing '_items' from the end of # the name): name = name[:-6] self.log_change(self._get_undo_item, object, name, event) # Get information about the node that was changed: start = event.index n = len(event.added) end = start + len(event.removed) tree = self._tree for expanded, node, nid in self._object_info_for(object, name): children = node.get_children(object) # If the new children aren't all at the end, remove/add them all: # if (n > 0) and ((start + n) != len( children )): # self._children_replaced( object, name, event ) # return # Only add/remove the changes if the node has already been # expanded: if expanded: # Remove all of the children that were deleted: for cnid in self._nodes_for(nid)[start:end]: self._delete_node(cnid) remaining = len(children) - len(event.removed) child_index = 0 # Add all of the children that were added: for child in event.added: child, child_node = self._node_for(child) if child_node is not None: insert_index = ( (start + child_index) if (start <= remaining) else None ) self._insert_node(nid, insert_index, child_node, child) child_index += 1 else: dummy = getattr(nid, "_dummy", None) if dummy is None and len(children) > 0: # if model now has children add dummy child nid._dummy = QtGui.QTreeWidgetItem(nid) elif dummy is not None and len(children) == 0: # if model no longer has children remove dummy child nid.removeChild(dummy) del nid._dummy # Try to expand the node (if requested): if node.can_auto_open(object): nid.setExpanded(True) def _label_updated(self, object, name, label): """Handles the label of an object being changed.""" # Prevent the itemChanged() signal from being emitted. blk = self._tree.blockSignals(True) try: # Have to use a list rather than a set because nids for PyQt # on Python 3 (QTreeWidgetItem instances) aren't hashable. # This means potentially quadratic behaviour, but the number of # nodes for a particular object shouldn't be high nids = [] for name2, nid in self._map[id(object)]: if nid not in nids: try: node = self._get_node_data(nid)[1] except Exception: # node is either not ready or has been deleted continue nids.append(nid) self._set_label(nid, 0) self._update_icon(nid) finally: self._tree.blockSignals(blk) def _column_labels_updated(self, object, name, new): """Handles the column labels of an object being changed.""" # Prevent the itemChanged() signal from being emitted. blk = self._tree.blockSignals(True) try: nids = [] for name2, nid in self._map[id(object)]: if nid not in nids: try: node = self._get_node_data(nid)[1] except Exception: # node is either not ready or has been deleted continue nids.append(nid) self._set_column_labels(nid, node, object) finally: self._tree.blockSignals(blk) # -- UI preference save/restore interface --------------------------------- def restore_prefs(self, prefs): """Restores any saved user preference information associated with the editor. """ if isinstance(self.control, QtGui.QSplitter): if isinstance(prefs, dict): structure = prefs.get("structure") else: structure = prefs self.control.restoreState(structure) header = self._tree.header() self.control.setExpandsOnDoubleClick(self.factory.expands_on_dclick) if header is not None and "column_state" in prefs: header.restoreState(prefs["column_state"]) def save_prefs(self): """Returns any user preference information associated with the editor.""" prefs = {} if isinstance(self.control, QtGui.QSplitter): prefs["structure"] = self.control.saveState().data() header = self._tree.header() if header is not None: prefs["column_state"] = header.saveState().data() return prefs # -- End UI preference save/restore interface ----------------------------- class _TreeWidget(QtGui.QTreeWidget): """The _TreeWidget class is a specialised QTreeWidget that reimplements the drag'n'drop support so that it hooks into the provided Traits support. """ def __init__(self, editor, parent=None): """Initialise the tree widget.""" QtGui.QTreeWidget.__init__(self, parent) self.setContextMenuPolicy(QtCore.Qt.ContextMenuPolicy.CustomContextMenu) self.setDragEnabled(True) self.setAcceptDrops(True) self.setIconSize(QtCore.QSize(*editor.factory.icon_size)) # Set up headers if necessary. column_count = len(editor.factory.column_headers) if column_count > 0: self.setHeaderHidden(False) self.setColumnCount(column_count) self.setHeaderLabels(editor.factory.column_headers) else: self.setHeaderHidden(True) self.setAlternatingRowColors(editor.factory.alternating_row_colors) padding = editor.factory.vertical_padding if padding > 0: self.setStyleSheet( """ QTreeView::item { padding-top: %spx; padding-bottom: %spx; } """ % (padding, padding) ) if editor.factory.selection_mode == "extended": self.setSelectionMode(QtGui.QAbstractItemView.SelectionMode.ExtendedSelection) self.itemExpanded.connect(editor._on_item_expanded) self.itemCollapsed.connect(editor._on_item_collapsed) self.itemClicked.connect(editor._on_item_clicked) self.itemDoubleClicked.connect(editor._on_item_dclicked) self.itemActivated.connect(editor._on_item_activated) self.itemSelectionChanged.connect(editor._on_tree_sel_changed) self.customContextMenuRequested.connect(editor._on_context_menu) self.itemChanged.connect(editor._on_nid_changed) self._editor = editor self._dragging = None def resizeEvent(self, event): """Overridden to emit sizeHintChanged() of items for word wrapping""" if self._editor.factory.word_wrap: for i in range(self.topLevelItemCount()): mi = self.indexFromItem(self.topLevelItem(i)) id = self.itemDelegate(mi) id.sizeHintChanged.emit(mi) super(self.__class__, self).resizeEvent(event) def startDrag(self, actions): """Reimplemented to start the drag of a tree widget item.""" nid = self.currentItem() if nid is None: return self._dragging = nid # Calculate the hotspot so that the pixmap appears on top of the # original item. nid_rect = self.visualItemRect(nid) hspos = ( self.viewport().mapFromGlobal(QtGui.QCursor.pos()) - nid_rect.topLeft() ) _, node, object = self._editor._get_node_data(nid) # Convert the item being dragged to MIME data. drag_object = node.get_drag_object(object) md = PyMimeData.coerce(drag_object) # Render the item being dragged as a pixmap. rect = nid_rect.intersected(self.viewport().rect()) pm = QtGui.QPixmap(rect.size()) pm.fill(self.palette().base().color()) painter = QtGui.QPainter(pm) if is_qt5: option = self.viewOptions() else: option = QtGui.QStyleOptionViewItem() self.initViewItemOption(option) option.state |= QtGui.QStyle.StateFlag.State_Selected option.rect = QtCore.QRect( nid_rect.topLeft() - rect.topLeft(), nid_rect.size() ) self.itemDelegate().paint(painter, option, self.indexFromItem(nid)) painter.end() # Start the drag. drag = QtGui.QDrag(self) drag.setMimeData(md) drag.setPixmap(pm) drag.setHotSpot(hspos) drag.exec_(actions) def dragEnterEvent(self, e): """Reimplemented to see if the current drag can be handled by the tree. """ # Assume the drag is invalid. e.ignore() # Check if we have a python object instance, we might be interested data = PyMimeData.coerce(e.mimeData()).instance() if data is None: return # We might be able to handle it (but it depends on what the final # target is). e.acceptProposedAction() def dragMoveEvent(self, e): """Reimplemented to see if the current drag can be handled by the particular tree widget item underneath the cursor. """ # Assume the drag is invalid. e.ignore() action, to_node, to_object, to_index, data = self._get_action(e) if action is not None: e.acceptProposedAction() def dropEvent(self, e): """Reimplemented to update the model and tree.""" # Assume the drop is invalid. e.ignore() editor = self._editor dragging = self._dragging self._dragging = None action, to_node, to_object, to_index, data = self._get_action(e) if action == "append": if dragging is not None: data = self._editor._drop_object( to_node, to_object, data, False ) if data is not None: try: editor._begin_undo() editor._undoable_delete(*editor._node_index(dragging)) editor._undoable_append( to_node, to_object, data, False ) finally: editor._end_undo() else: data = editor._drop_object(to_node, to_object, data, True) if data is not None: editor._undoable_append(to_node, to_object, data, False) elif action == "insert": if dragging is not None: data = editor._drop_object(to_node, to_object, data, False) if data is not None: from_node, from_object, from_index = editor._node_index( dragging ) if (to_object is from_object) and (to_index > from_index): to_index -= 1 try: editor._begin_undo() editor._undoable_delete( from_node, from_object, from_index ) editor._undoable_insert( to_node, to_object, to_index, data, False ) finally: editor._end_undo() else: data = self._editor._drop_object( to_node, to_object, data, True ) if data is not None: editor._undoable_insert( to_node, to_object, to_index, data, False ) else: return e.acceptProposedAction() def _get_action(self, event): """Work out what action on what object to perform for a drop event""" # default values to return action = None to_node = None to_object = None to_index = None data = None editor = self._editor # Get the tree widget item under the cursor. nid = self.itemAt(event.pos()) if nid is None: if editor.factory.hide_root: nid = self.invisibleRootItem() else: return (action, to_node, to_object, to_index, data) # Check that the target is not the source of a child of the source. if self._dragging is not None: pnid = nid while pnid is not None: if pnid is self._dragging: return (action, to_node, to_object, to_index, data) pnid = pnid.parent() data = PyMimeData.coerce(event.mimeData()).instance() _, node, object = editor._get_node_data(nid) if ( event.proposedAction() == QtCore.Qt.DropAction.MoveAction and editor._is_droppable(node, object, data, False) ): # append to node being dropped on action = "append" to_node = node to_object = object to_index = None else: # get parent of node being dropped on to_node, to_object, to_index = editor._node_index(nid) if to_node is None: # no parent, can't do anything action = None elif editor._is_droppable(to_node, to_object, data, True): # insert into the parent of the node being dropped on action = "insert" elif editor._is_droppable(to_node, to_object, data, False): # append to the parent of the node being dropped on action = "append" else: # parent can't be modified, can't do anything action = None return (action, to_node, to_object, to_index, data) class TreeItemDelegate(QtGui.QStyledItemDelegate): """A delegate class to draw wrapped text labels""" # FIXME: sizeHint() should return the size required by the label, # which is dependent on the width available, which is different for # each item due to the nested tree structure. However the option.rect # argument available to the sizeHint() is invalid (width=-1) so as a # hack sizeHintChanged is emitted in paint() and the size of drawn # text is returned, as paint() gets a valid option.rect argument. # TODO: add ability to override editor in item delegate def sizeHint(self, option, index): """returns area taken by the text.""" column = index.column() item = self.editor._tree.itemFromIndex(index) expanded, node, instance = self.editor._get_node_data(item) column = index.column() renderer = node.get_renderer(object, column=column) if renderer is None: return super().sizeHint(option, index) size_context = (option, index) size = renderer.size(self.editor, node, column, instance, size_context) if size is None: return QtCore.QSize(1, 21) else: return QtCore.QSize(*size) def updateEditorGeometry(self, editor, option, index): """Update the editor's geometry.""" editor.setGeometry(option.rect) def paint(self, painter, option, index): """Render the contents of the item.""" item = self.editor._tree.itemFromIndex(index) expanded, node, instance = self.editor._get_node_data(item) column = index.column() renderer = node.get_renderer(object, column=column) if renderer is None and self.editor.factory.word_wrap: renderer = DEFAULT_WRAP_RENDERER if renderer is None: super().paint(painter, option, index) else: if not renderer.handles_all: # renderers background and selection highlights # will also render icon and text if flags are set super().paint(painter, option, index) paint_context = (painter, option, index) size = renderer.paint( self.editor, node, column, instance, paint_context ) if size is not None: do_later(self.sizeHintChanged.emit, index) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/qt/tree_node_renderers.py0000644000175100001730000000743700000000000023057 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! from pyface.qt import QtCore, QtGui from pyface.ui_traits import HasBorder from traits.api import Int from traitsui.tree_node_renderer import AbstractTreeNodeRenderer from .helper import wrap_text_with_elision class WordWrapRenderer(AbstractTreeNodeRenderer): """A renderer that wraps the label text across multiple lines.""" #: The padding around the text. padding = HasBorder(0) #: The a good size for the width of the item. width_hint = Int(100) #: The maximum number of lines to show without eliding. max_lines = Int(5) #: The extra border applied by Qt internally # XXX get this dynamically from Qt? How? extra_space = Int(8) def paint(self, editor, node, column, object, paint_context): """Paint word-wrapped text with elision.""" painter, option, index = paint_context text = self.get_label(node, object, column) if editor.factory.show_icons: icon_width = option.decorationSize.width() + self.extra_space icon_height = option.decorationSize.height() else: icon_width = 0 icon_height = 0 x = option.rect.left() + icon_width + self.padding.left y = option.rect.top() + self.padding.top width = ( option.rect.width() - icon_width - self.padding.left - self.padding.right ) height = option.rect.height() - self.padding.top - self.padding.bottom lines = wrap_text_with_elision(text, option.font, width, height) old_pen = painter.pen() if bool(option.state & QtGui.QStyle.StateFlag.State_Selected): painter.setPen(QtGui.QPen(option.palette.highlightedText(), 0)) try: rect = painter.drawText( x, y, width, height, option.displayAlignment, "\n".join(lines) ) finally: painter.setPen(old_pen) def size(self, editor, node, column, object, size_context): """Return the preferred size for the word-wrapped text Parameters ---------- node : ITreeNode instance The tree node to render. column : int The column in the tree that should be rendererd. object : object The underlying object being edited. size_context : object A toolkit-dependent context for performing sizing operations. Returns ------- size : tuple of (width, height) or None """ option, index = size_context font_metrics = QtGui.QFontMetrics(option.font) text = self.get_label(node, object, column) if editor.factory.show_icons: icon_size = option.decorationSize icon_width = icon_size.width() icon_height = icon_size.height() else: icon_width = 0 icon_height = 0 width = ( self.width_hint - icon_width - self.padding.left - self.padding.right ) max_height = self.max_lines * font_metrics.lineSpacing() lines = wrap_text_with_elision(text, option.font, width, max_height) text_height = len(lines) * font_metrics.lineSpacing() height = ( max(icon_height, text_height) + self.padding.top + self.padding.bottom + self.extra_space ) return self.width_hint, height ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/qt/tuple_editor.py0000644000175100001730000000266200000000000021534 0ustar00runnerdocker00000000000000# (C) Copyright 2008-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # ------------------------------------------------------------------------------ # Copyright (c) 2007, Riverbank Computing Limited # All rights reserved. # # This software is provided without warranty under the terms of the BSD license. # However, when used with the GPL version of PyQt the additional terms # described in the PyQt GPL exception also apply # # Author: Riverbank Computing Limited # ------------------------------------------------------------------------------ """ Defines the tuple editor for the PyQt user interface toolkit. """ from traitsui.editors.tuple_editor import SimpleEditor as BaseSimpleEditor from .editor import Editor # ------------------------------------------------------------------------- # 'SimpleEditor' class: # ------------------------------------------------------------------------- class SimpleEditor(BaseSimpleEditor, Editor): """Simple style of editor for tuples. The editor displays an editor for each of the fields in the tuple, based on the type of each field. """ pass ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/qt/ui_base.py0000644000175100001730000003300700000000000020441 0ustar00runnerdocker00000000000000# (C) Copyright 2008-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # ------------------------------------------------------------------------------ # Copyright (c) 2007, Riverbank Computing Limited # All rights reserved. # # This software is provided without warranty under the terms of the BSD license. # However, when used with the GPL version of PyQt the additional terms # described in the PyQt GPL exception also apply # # Author: Riverbank Computing Limited # ------------------------------------------------------------------------------ """Defines the base class for the PyQt-based Traits UI modal and non-modal dialogs. """ from pyface.qt import QtCore, QtGui from pyface.action.schema.api import ( ActionManagerBuilder, MenuBarSchema, ToolBarSchema, ) from traits.api import HasPrivateTraits, Instance from traitsui.base_panel import BasePanel as _BasePanel from traitsui.menu import Action from .constants import DefaultTitle from .editor import Editor from .helper import restore_window, save_window class ButtonEditor(Editor): """Editor for buttons.""" # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- #: Action associated with the button action = Instance(Action) def __init__(self, **traits): # XXX Why does this need to be an Editor subclass? -- CJW HasPrivateTraits.__init__(self, **traits) def perform(self): """Handles the associated button being clicked.""" handler = self.ui.handler self.ui.do_undoable(handler.perform, self.ui.info, self.action, None) class BasePanel(_BasePanel): """Base class for Traits UI panels.""" def add_button( self, action, bbox, role, method=None, enabled=True, name=None, default=False, ): """Creates a button.""" ui = self.ui if (action.defined_when != "") and ( not ui.eval_when(action.defined_when) ): return None if name is None: name = action.name id = action.id button = bbox.addButton(name, role) button.setAutoDefault(False) button.setDefault(default) button.setEnabled(enabled) if (method is None) or (action.enabled_when != "") or (id != ""): editor = ButtonEditor(ui=ui, action=action, control=button) if id != "": ui.info.bind(id, editor) if action.visible_when != "": ui.add_visible(action.visible_when, editor) if action.enabled_when != "": ui.add_enabled(action.enabled_when, editor) if method is None: method = editor.perform if method is not None: button.clicked.connect(method) if action.tooltip != "": button.setToolTip(action.tooltip) return button def _on_undoable(self, event): """Handles a change to the "undoable" state of the undo history""" state = event.new self.undo.setEnabled(state) def _on_redoable(self, event): """Handles a change to the "redoable" state of the undo history.""" state = event.new self.redo.setEnabled(state) def _on_revertable(self, event): """Handles a change to the "revert" state.""" state = event.new self.revert.setEnabled(state) class _StickyDialog(QtGui.QDialog): """A QDialog that will only close if the traits handler allows it.""" def __init__(self, ui, parent): """Initialise the dialog.""" QtGui.QDialog.__init__(self, parent) # Create the main window so we can add toolbars etc. self._mw = QtGui.QMainWindow() layout = QtGui.QVBoxLayout() layout.setContentsMargins(0, 0, 0, 0) layout.addWidget(self._mw) self.setLayout(layout) # Set the dialog's window flags and properties. # On some X11 window managers, using the Dialog flag instead of # just the Window flag could cause the subsequent minimize and maximize # button hint to be ignored such that those actions are not # available (but the window can still be resize unless the size # constraint is fixed). On some X11 window managers, having two # _StickyDialog open where one has a Window flag and other has a Dialog # flag results in the latter (Dialog) to be always placed on top of # the former (Window). flags = QtCore.Qt.WindowType.Window if not ui.view.resizable: flags |= QtCore.Qt.WindowType.WindowSystemMenuHint layout.setSizeConstraint(QtGui.QLayout.SizeConstraint.SetFixedSize) try: flags |= QtCore.Qt.WindowType.WindowCloseButtonHint if ui.view.resizable: # Cast to int to deal with cast-to-int deprecation warning on # PyQt5 5.12.3 flags |= int( QtCore.Qt.WindowType.WindowMinimizeButtonHint | QtCore.Qt.WindowType.WindowMaximizeButtonHint ) except AttributeError: # Either PyQt or Qt is too old. pass self.setWindowFlags(flags) self._ui = ui self._result = None def showEvent(self, e): self.raise_() def closeEvent(self, e): """Reimplemented to check when the clicks the window close button. (Note that QDialog doesn't get a close event when the dialog is closed in any other way.)""" if self._ok_to_close(): QtGui.QDialog.closeEvent(self, e) else: # Ignore the event thereby keeping the dialog open. e.ignore() def keyPressEvent(self, e): """Reimplemented to ignore the Escape key if appropriate, and to ignore the Enter key if no default button has been explicitly set.""" if ( e.key() in (QtCore.Qt.Key.Key_Enter, QtCore.Qt.Key.Key_Return) and not self._ui.view.default_button ): return if e.key() == QtCore.Qt.Key.Key_Escape and not self._ok_to_close(): return QtGui.QDialog.keyPressEvent(self, e) def sizeHint(self): """Reimplemented to provide an appropriate size hint for the window.""" size = QtGui.QDialog.sizeHint(self) view = self._ui.view if view.width > 0: size.setWidth(int(view.width)) if view.height > 0: size.setHeight(int(view.height)) return size def done(self, r): """Reimplemented to ignore calls to accept() or reject() if appropriate.""" # If we already have a result then we already know that we are done. if self._result is not None: QtGui.QDialog.done(self, self._result) elif self._ok_to_close(bool(r)): QtGui.QDialog.done(self, r) def _ok_to_close(self, is_ok=None): """Let the handler decide if the dialog should be closed.""" # The is_ok flag is also the dialog result. If we don't know it then # the the user closed the dialog other than by an 'Ok' or 'Cancel' # button. if is_ok is None: # Use the view's default, if there is one. is_ok = self._ui.view.close_result if is_ok is None: # There is no default, so use False for a modal dialog and True # for a non-modal one. is_ok = not self.isModal() ok_to_close = self._ui.handler.close(self._ui.info, is_ok) if ok_to_close: # Save the result now. self._result = is_ok return ok_to_close class BaseDialog(BasePanel): """Base class for Traits UI dialog boxes.""" #: The different dialog styles. NONMODAL, MODAL, POPUP = list(range(3)) def init(self, ui, parent, style): """Initialise the dialog by creating the controls.""" raise NotImplementedError def create_dialog(self, parent, style): """Create the dialog control.""" self.control = control = _StickyDialog(self.ui, parent) view = self.ui.view control.setModal(style == BaseDialog.MODAL) control.setWindowTitle(view.title or DefaultTitle) control.finished.connect(self._on_finished) def add_contents(self, panel, buttons): """Add a panel (either a widget, layout or None) and optional buttons to the dialog.""" # If the panel is a layout then provide a widget for it. if isinstance(panel, QtGui.QLayout): w = QtGui.QWidget() panel.setContentsMargins(0, 0, 0, 0) w.setLayout(panel) panel = w if panel is not None: self.control._mw.setCentralWidget(panel) # Add the optional buttons. if buttons is not None: self.control.layout().addWidget(buttons) # Add the menu bar, tool bar and status bar (if any). self._add_menubar() self._add_toolbar() self._add_statusbar() def close(self, rc=True): """Close the dialog and set the given return code.""" self.ui.dispose(rc) self.ui = self.control = None @staticmethod def display_ui(ui, parent, style): """Display the UI.""" ui.owner.init(ui, parent, style) ui.control = ui.owner.control ui.control._parent = parent try: ui.prepare_ui() except BaseException: ui.control.setParent(None) ui.control.ui = None ui.control = None ui.owner = None ui.result = False raise ui.handler.position(ui.info) restore_window(ui) # if an item asked for initial focus, give it to them if ui._focus_control is not None: ui._focus_control.setFocus() ui._focus_control = None if style == BaseDialog.NONMODAL: ui.control.show() else: ui.control.setWindowModality(QtCore.Qt.WindowModality.ApplicationModal) ui.control.exec_() def set_icon(self, icon=None): """Sets the dialog's icon.""" from pyface.image_resource import ImageResource if not isinstance(icon, ImageResource): icon = self.default_icon() self.control.setWindowIcon(icon.create_icon()) def _on_error(self, event): """Handles editing errors.""" errors = event.new self.ok.setEnabled(errors == 0) def _add_menubar(self): """Adds a menu bar to the dialog.""" menubar = self.ui.view.menubar if isinstance(menubar, MenuBarSchema): builder = self.ui.view.action_manager_builder menubar = builder.create_action_manager(menubar) if menubar is not None: self._last_group = self._last_parent = None self.control.layout().setMenuBar( menubar.create_menu_bar(self.control, self) ) self._last_group = self._last_parent = None def _add_toolbar(self): """Adds a toolbar to the dialog.""" toolbar = self.ui.view.toolbar if isinstance(toolbar, ToolBarSchema): builder = self.ui.view.action_manager_builder toolbar = builder.create_action_manager(toolbar) if toolbar is not None: self._last_group = self._last_parent = None qt_toolbar = toolbar.create_tool_bar(self.control, self) qt_toolbar.setMovable(False) self.control._mw.addToolBar(qt_toolbar) self._last_group = self._last_parent = None def _add_statusbar(self): """Adds a statusbar to the dialog.""" if self.ui.view.statusbar is not None: control = QtGui.QStatusBar() control.setSizeGripEnabled(self.ui.view.resizable) listeners = [] for item in self.ui.view.statusbar: # Create the status widget with initial text name = item.name item_control = QtGui.QLabel() item_control.setText(self.ui.get_extended_value(name)) # Add the widget to the control with correct size width = abs(item.width) stretch = 0 if width <= 1.0: stretch = int(100 * width) else: item_control.setMinimumWidth(int(width)) control.addWidget(item_control, stretch) # Set up event listener for updating the status text col = name.find(".") obj = "object" if col >= 0: obj = name[:col] name = name[col + 1 :] obj = self.ui.context[obj] set_text = self._set_status_text(item_control) obj.observe(set_text, name, dispatch="ui") listeners.append((obj, set_text, name)) self.control._mw.setStatusBar(control) self.ui._statusbar = listeners def _set_status_text(self, control): """Helper function for _add_statusbar.""" def set_status_text(event): text = event.new control.setText(text) return set_status_text ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/qt/ui_editor.py0000644000175100001730000000162400000000000021015 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Defines the BasicUIEditor class, which allows creating editors that define their function by creating an embedded Traits UI. """ from traitsui.ui_editor import UIEditor as BaseUIEditor from .editor import Editor # ------------------------------------------------------------------------- # 'UIEditor' base class: # ------------------------------------------------------------------------- class UIEditor(BaseUIEditor, Editor): """An editor that creates an embedded Traits UI.""" pass ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/qt/ui_live.py0000644000175100001730000001775600000000000020503 0ustar00runnerdocker00000000000000# (C) Copyright 2008-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # ------------------------------------------------------------------------------ # Copyright (c) 2007, Riverbank Computing Limited # All rights reserved. # # This software is provided without warranty under the terms of the BSD license. # However, when used with the GPL version of PyQt the additional terms # described in the PyQt GPL exception also apply # # Author: Riverbank Computing Limited # ------------------------------------------------------------------------------ """Creates a PyQt user interface for a specified UI object, where the UI is "live", meaning that it immediately updates its underlying object(s). """ from pyface.qt import QtCore, QtGui from traitsui.undo import UndoHistory from traitsui.menu import ( UndoButton, RevertButton, OKButton, CancelButton, HelpButton, ) from .ui_base import BaseDialog from .ui_panel import panel # ------------------------------------------------------------------------- # Create the different 'live update' PyQt user interfaces. # ------------------------------------------------------------------------- def ui_live(ui, parent): """Creates a live, non-modal PyQt user interface for a specified UI object.""" _ui_dialog(ui, parent, BaseDialog.NONMODAL) def ui_livemodal(ui, parent): """Creates a live, modal PyQt user interface for a specified UI object.""" _ui_dialog(ui, parent, BaseDialog.MODAL) def ui_popup(ui, parent): """Creates a live, modal popup PyQt user interface for a specified UI object. """ _ui_dialog(ui, parent, BaseDialog.POPUP) def _ui_dialog(ui, parent, style): """Creates a live PyQt user interface for a specified UI object.""" if ui.owner is None: ui.owner = _LiveWindow() BaseDialog.display_ui(ui, parent, style) class _LiveWindow(BaseDialog): """User interface window that immediately updates its underlying object(s).""" def init(self, ui, parent, style): """Initialise the object. FIXME: Note that we treat MODAL and POPUP as equivalent until we have an example that demonstrates how POPUP is supposed to work. """ self.ui = ui self.control = ui.control view = ui.view history = ui.history if self.control is not None: if history is not None: history.observe( self._on_undoable, "undoable", remove=True, dispatch="ui" ) history.observe( self._on_redoable, "redoable", remove=True, dispatch="ui" ) history.observe( self._on_revertable, "undoable", remove=True, dispatch="ui" ) ui.reset() else: self.create_dialog(parent, style) self.set_icon(view.icon) # Convert the buttons to actions. buttons = [self.coerce_button(button) for button in view.buttons] nr_buttons = len(buttons) no_buttons = (nr_buttons == 1) and self.is_button(buttons[0], "") has_buttons = (not no_buttons) and ( (nr_buttons > 0) or view.undo or view.revert or view.ok or view.cancel ) if has_buttons or (view.menubar is not None): if history is None: history = UndoHistory() else: history = None ui.history = history if (not no_buttons) and (has_buttons or view.help): bbox = QtGui.QDialogButtonBox() # Create the necessary special function buttons. if nr_buttons == 0: if view.undo: self.check_button(buttons, UndoButton) if view.revert: self.check_button(buttons, RevertButton) if view.ok: self.check_button(buttons, OKButton) if view.cancel: self.check_button(buttons, CancelButton) if view.help: self.check_button(buttons, HelpButton) for raw_button, button in zip(view.buttons, buttons): default = raw_button == view.default_button if self.is_button(button, "Undo"): self.undo = self.add_button( button, bbox, QtGui.QDialogButtonBox.ButtonRole.ActionRole, self._on_undo, False, default=default, ) history.observe( self._on_undoable, "undoable", dispatch="ui" ) if history.can_undo: self._on_undoable(True) self.redo = self.add_button( button, bbox, QtGui.QDialogButtonBox.ButtonRole.ActionRole, self._on_redo, False, "Redo", ) history.observe( self._on_redoable, "redoable", dispatch="ui" ) if history.can_redo: self._on_redoable(True) elif self.is_button(button, "Revert"): self.revert = self.add_button( button, bbox, QtGui.QDialogButtonBox.ButtonRole.ResetRole, self._on_revert, False, default=default, ) history.observe( self._on_revertable, "undoable", dispatch="ui" ) if history.can_undo: self._on_revertable(True) elif self.is_button(button, "OK"): self.ok = self.add_button( button, bbox, QtGui.QDialogButtonBox.ButtonRole.AcceptRole, self.control.accept, default=default, ) ui.observe(self._on_error, "errors", dispatch="ui") elif self.is_button(button, "Cancel"): self.add_button( button, bbox, QtGui.QDialogButtonBox.ButtonRole.RejectRole, self.control.reject, default=default, ) elif self.is_button(button, "Help"): self.add_button( button, bbox, QtGui.QDialogButtonBox.ButtonRole.HelpRole, self._on_help, default=default, ) elif not self.is_button(button, ""): self.add_button( button, bbox, QtGui.QDialogButtonBox.ButtonRole.ActionRole, default=default, ) else: bbox = None self.add_contents(panel(ui), bbox) def close(self, rc=True): """Close the dialog and set the given return code.""" super().close(rc) self.undo = self.redo = self.revert = None def _on_finished(self, result): """Handles the user finishing with the dialog.""" accept = bool(result) if not accept and self.ui.history is not None: self._on_revert(None) self.close(accept) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/qt/ui_modal.py0000644000175100001730000001777400000000000020640 0ustar00runnerdocker00000000000000# (C) Copyright 2008-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # ------------------------------------------------------------------------------ # Copyright (c) 2007, Riverbank Computing Limited # All rights reserved. # # This software is provided without warranty under the terms of the BSD license. # However, when used with the GPL version of PyQt the additional terms # described in the PyQt GPL exception also apply # # Author: Riverbank Computing Limited # ------------------------------------------------------------------------------ """Creates a PyQt user interface for a specified UI object. """ from pyface.qt import QtCore, QtGui from traitsui.menu import ( ApplyButton, RevertButton, OKButton, CancelButton, HelpButton, ) from .ui_base import BaseDialog from .ui_panel import panel # ------------------------------------------------------------------------- # Create the different modal PyQt user interfaces. # ------------------------------------------------------------------------- def ui_modal(ui, parent): """Creates a modal PyQt user interface for a specified UI object.""" _ui_dialog(ui, parent, BaseDialog.MODAL) def ui_nonmodal(ui, parent): """Creates a non-modal PyQt user interface for a specified UI object.""" _ui_dialog(ui, parent, BaseDialog.NONMODAL) def _ui_dialog(ui, parent, style): """Creates a PyQt dialog box for a specified UI object. Changes are not immediately applied to the underlying object. The user must click **Apply** or **OK** to apply changes. The user can revert changes by clicking **Revert** or **Cancel**. """ if ui.owner is None: ui.owner = _ModalDialog() BaseDialog.display_ui(ui, parent, style) class _ModalDialog(BaseDialog): """Modal dialog box for Traits-based user interfaces.""" def init(self, ui, parent, style): """Initialise the object.""" self.ui = ui self.control = ui.control view = ui.view revert = apply = False if self.control is not None: if hasattr(self, "revert"): revert = self.revert.isEnabled() if hasattr(self, "apply"): apply = self.apply.isEnabled() ui.reset() else: self.create_dialog(parent, style) # Create the 'context' copies we will need while editing: context = ui.context ui._context = context ui.context = self._copy_context(context) ui._revert = self._copy_context(context) self.set_icon(view.icon) # Convert the buttons to actions. buttons = [self.coerce_button(button) for button in view.buttons] nr_buttons = len(buttons) if (nr_buttons != 1) or (not self.is_button(buttons[0], "")): bbox = QtGui.QDialogButtonBox() # Create the necessary special function buttons. if nr_buttons == 0: if view.apply: self.check_button(buttons, ApplyButton) if view.revert: self.check_button(buttons, RevertButton) if view.ok: self.check_button(buttons, OKButton) if view.cancel: self.check_button(buttons, CancelButton) if view.help: self.check_button(buttons, HelpButton) for raw_button, button in zip(view.buttons, buttons): default = raw_button == view.default_button if self.is_button(button, "Apply"): self.apply = self.add_button( button, bbox, QtGui.QDialogButtonBox.ButtonRole.ApplyRole, self._on_apply, enabled=apply, default=default, ) ui.observe(self._on_applyable, "modified", dispatch="ui") elif self.is_button(button, "Revert"): self.revert = self.add_button( button, bbox, QtGui.QDialogButtonBox.ButtonRole.ResetRole, self._on_revert, enabled=revert, default=default, ) elif self.is_button(button, "OK"): self.ok = self.add_button( button, bbox, QtGui.QDialogButtonBox.ButtonRole.AcceptRole, self.control.accept, default=default, ) ui.observe(self._on_error, "errors", dispatch="ui") elif self.is_button(button, "Cancel"): self.add_button( button, bbox, QtGui.QDialogButtonBox.ButtonRole.RejectRole, self.control.reject, default=default, ) elif self.is_button(button, "Help"): self.add_button( button, bbox, QtGui.QDialogButtonBox.ButtonRole.HelpRole, self._on_help, default=default, ) elif not self.is_button(button, ""): self.add_button( button, bbox, QtGui.QDialogButtonBox.ButtonRole.ActionRole, default=default, ) else: bbox = None self.add_contents(panel(ui), bbox) def close(self, rc=True): """Close the dialog and set the given return code.""" super().close(rc) self.apply = self.revert = self.help = None def _copy_context(self, context): """Creates a copy of a *context* dictionary.""" result = {} for name, value in context.items(): if value is not None: result[name] = value.clone_traits() else: result[name] = None return result def _apply_context(self, from_context, to_context): """Applies the traits in the *from_context* to the *to_context*.""" for name, value in from_context.items(): if value is not None: to_context[name].copy_traits(value) else: to_context[name] = None if to_context is self.ui._context: on_apply = self.ui.view.on_apply if on_apply is not None: on_apply() def _on_finished(self, result): """Handles the user finishing with the dialog.""" accept = bool(result) if accept: self._apply_context(self.ui.context, self.ui._context) else: self._apply_context(self.ui._revert, self.ui._context) self.close(accept) def _on_apply(self): """Handles a request to apply changes.""" ui = self.ui self._apply_context(ui.context, ui._context) self.revert.setEnabled(True) ui.handler.apply(ui.info) ui.modified = False def _on_applyable(self, event): """Handles a change to the "modified" state of the user interface .""" state = event.new self.apply.setEnabled(state) def _on_revert(self): """Handles a request to revert changes.""" ui = self.ui self._apply_context(ui._revert, ui.context) self._apply_context(ui._revert, ui._context) self.revert.setEnabled(False) ui.handler.revert(ui.info) ui.modified = False ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/qt/ui_panel.py0000644000175100001730000013577600000000000020646 0ustar00runnerdocker00000000000000# (C) Copyright 2008-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # ------------------------------------------------------------------------------ # Copyright (c) 2007, Riverbank Computing Limited # All rights reserved. # # This software is provided without warranty under the terms of the BSD license. # However, when used with the GPL version of PyQt the additional terms described # in the PyQt GPL exception also apply. # # Author: Riverbank Computing Limited # ------------------------------------------------------------------------------ """Creates a panel-based PyQt user interface for a specified UI object. """ from html import escape import re from pyface.qt import QtCore, QtGui from traits.api import Any, HasPrivateTraits, Instance, Undefined from traits.observation.api import match from traitsui.api import Group from traitsui.undo import UndoHistory from traitsui.help_template import help_template from traitsui.menu import UndoButton, RevertButton, HelpButton from .helper import position_window from .ui_base import BasePanel from .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+") # ------------------------------------------------------------------------- # Create the different panel-based PyQt user interfaces. # ------------------------------------------------------------------------- def ui_panel(ui, parent): """Creates a panel-based PyQt user interface for a specified UI object.""" _ui_panel_for(ui, parent, False) def ui_subpanel(ui, parent): """Creates a subpanel-based PyQt user interface for a specified UI object. A subpanel does not allow control buttons (other than those specified in the UI object) and does not show headers for view titles. """ _ui_panel_for(ui, parent, True) def _ui_panel_for(ui, parent, is_subpanel): """Creates a panel-based PyQt user interface for a specified UI object.""" ui.control = control = _Panel(ui, parent, is_subpanel).control control._parent = parent control._object = ui.context.get("object") control._ui = ui try: ui.prepare_ui() except: control.setParent(None) del control ui.control = None ui.result = False raise ui.restore_prefs() ui.result = True class _Panel(BasePanel): """PyQt user interface panel for Traits-based user interfaces.""" def __init__(self, ui, parent, is_subpanel): """Initialise the object.""" super().__init__(ui=ui) history = ui.history view = ui.view # Reset any existing history listeners. if history is not None: history.observe( self._on_undoable, "undoable", remove=True, dispatch="ui" ) history.observe( self._on_redoable, "redoable", remove=True, dispatch="ui" ) history.observe( self._on_revertable, "undoable", remove=True, dispatch="ui" ) # Determine if we need any buttons or an 'undo' history. buttons = [self.coerce_button(button) for button in view.buttons] nr_buttons = len(buttons) has_buttons = not is_subpanel and ( nr_buttons != 1 or not self.is_button(buttons[0], "") ) if nr_buttons == 0: if view.undo: self.check_button(buttons, UndoButton) if view.revert: self.check_button(buttons, RevertButton) if view.help: self.check_button(buttons, HelpButton) if not is_subpanel and history is None: for button in buttons: if self.is_button(button, "Undo") or self.is_button( button, "Revert" ): history = ui.history = UndoHistory() break # Create the panel. self.control = panel(ui) # Suppress the title if this is a subpanel or if we think it should be # superceded by the title of an "outer" widget (eg. a dock widget). title = view.title if ( is_subpanel or ( isinstance(parent, QtGui.QMainWindow) and not isinstance(parent.parent(), QtGui.QDialog) ) or isinstance(parent, QtGui.QTabWidget) ): title = "" # Panels must be widgets as it is only the TraitsUI PyQt code that can # handle them being layouts as well. Therefore create a widget if the # panel is not a widget or if we need a title or buttons. if ( not isinstance(self.control, QtGui.QWidget) or title != "" or has_buttons ): w = QtGui.QWidget() layout = QtGui.QVBoxLayout(w) layout.setContentsMargins(0, 0, 0, 0) # Handle any view title. if title != "": layout.addWidget(heading_text(parent=w, text=view.title).control) if isinstance(self.control, QtGui.QWidget): layout.addWidget(self.control) elif isinstance(self.control, QtGui.QLayout): layout.addLayout(self.control) self.control = w # Add any buttons. if has_buttons: # Add the horizontal separator separator = QtGui.QFrame() separator.setFrameStyle( QtGui.QFrame.Shadow.Sunken | QtGui.QFrame.Shape.HLine ) separator.setFixedHeight(2) layout.addWidget(separator) # Add the special function buttons bbox = QtGui.QDialogButtonBox(QtCore.Qt.Orientation.Horizontal) for button in buttons: role = QtGui.QDialogButtonBox.ButtonRole.ActionRole if self.is_button(button, "Undo"): self.undo = self.add_button( button, bbox, role, self._on_undo, False, "Undo" ) self.redo = self.add_button( button, bbox, role, self._on_redo, False, "Redo" ) history.observe( self._on_undoable, "undoable", dispatch="ui" ) history.observe( self._on_redoable, "redoable", dispatch="ui" ) elif self.is_button(button, "Revert"): role = QtGui.QDialogButtonBox.ButtonRole.ResetRole self.revert = self.add_button( button, bbox, role, self._on_revert, False ) history.observe( self._on_revertable, "undoable", dispatch="ui" ) elif self.is_button(button, "Help"): role = QtGui.QDialogButtonBox.ButtonRole.HelpRole self.add_button(button, bbox, role, self._on_help) elif not self.is_button(button, ""): self.add_button(button, bbox, role) layout.addWidget(bbox) # If the UI has a toolbar, should add it to the panel too self._add_toolbar(parent) # Ensure the control has a size hint reflecting the View specification. # Yes, this is a hack, but it's too late to repair this convoluted # control building process, so we do what we have to... self.control.sizeHint = _size_hint_wrapper(self.control.sizeHint, ui) def _add_toolbar(self, parent): """Adds a toolbar to the `parent` (QtWindow)""" if not isinstance(parent, QtGui.QMainWindow): # toolbar cannot be added to non-MainWindow widget return toolbar = self.ui.view.toolbar if toolbar is not None: self._last_group = self._last_parent = None qt_toolbar = toolbar.create_tool_bar(parent, self) qt_toolbar.setMovable(False) parent.addToolBar(qt_toolbar) self._last_group = self._last_parent = None def can_add_to_toolbar(self, action): """Returns whether the toolbar action should be defined in the user interface. """ if action.defined_when == "": return True return self.ui.eval_when(action.defined_when) def panel(ui): """Creates a panel-based PyQt user interface for a specified UI object. This function does not modify the UI object passed to it. The object returned may be either a widget, a layout or None. """ # Bind the context values to the 'info' object: ui.info.bind_context() # Get the content that will be displayed in the user interface: content = ui._groups nr_groups = len(content) if nr_groups == 0: panel = None if nr_groups == 1: panel = _GroupPanel(content[0], ui).control elif nr_groups > 1: panel = QtGui.QTabWidget() _fill_panel(panel, content, ui) panel.ui = ui # If the UI is scrollable then wrap the panel in a scroll area. if ui.scrollable and panel is not None: # Make sure the panel is a widget. if isinstance(panel, QtGui.QLayout): w = QtGui.QWidget() w.setLayout(panel) panel = w sa = QtGui.QScrollArea() sa.setWidget(panel) sa.setWidgetResizable(True) panel = sa 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 %d" % 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 the result is the same type with only one page, collapse it # down into just the page. if isinstance(sub_page, type(panel)) and sub_page.count() == 1: new = sub_page.widget(0) if isinstance(panel, QtGui.QTabWidget): sub_page.removeTab(0) else: sub_page.removeItem(0) elif isinstance(page, QtGui.QWidget): new = page else: new = QtGui.QWidget() if page is not None: new.setLayout(page) layout = new.layout() if layout is not None: layout.setAlignment(QtCore.Qt.AlignmentFlag.AlignLeft | QtCore.Qt.AlignmentFlag.AlignTop) else: new = QtGui.QWidget() layout = QtGui.QVBoxLayout(new) layout.setContentsMargins(0, 0, 0, 0) item_handler(item, layout) # Add the content. if isinstance(panel, QtGui.QTabWidget): panel.addTab(new, page_name) else: panel.addItem(new, page_name) panel.setCurrentIndex(active) def _size_hint_wrapper(f, ui): """Wrap an existing sizeHint method with sizes from a UI object.""" def sizeHint(): size = f() if ui.view is not None: if ui.view.width > 0: size.setWidth(int(ui.view.width)) if ui.view.height > 0: size.setHeight(int(ui.view.height)) return size return sizeHint def show_help(ui, button): """Displays a help window for the specified UI's active Group.""" group = ui._groups[ui._active_group] template = help_template() if group.help != "": header = template.group_help % escape(group.help) else: header = template.no_group_help fields = [] for item in group.get_content(False): if not item.is_spacer(): fields.append( template.item_help % ( escape(item.get_label(ui)), escape(item.get_help(ui)), ) ) html_content = template.group_html % (header, "\n".join(fields)) HTMLHelpWindow(button, html_content, 0.25, 0.33) def show_help_popup(event): """Displays a pop-up help window for a single trait.""" control = event.GetEventObject() template = help_template() # Note: The following check is necessary because under Linux, we get back # a control which does not have the 'help' trait defined (it is the parent # of the object with the 'help' trait): help = getattr(control, "help", None) if help is not None: html_content = template.item_html % (control.GetLabel(), help) HTMLHelpWindow(control, html_content, 0.25, 0.13) class _GroupSplitter(QtGui.QSplitter): """A splitter for a Traits UI Group with layout='split'.""" def __init__(self, group): """Store the group.""" QtGui.QSplitter.__init__(self) self._group = group self._initialized = False def resizeEvent(self, event): """Overridden to position the splitter based on the Group when the application is initializing. Because the splitter layout algorithm requires that the available space be known, we have to wait until the UI that contains this splitter gives it its initial size. """ QtGui.QSplitter.resizeEvent(self, event) parent = self.parent() if ( not self._initialized and parent and (self.isVisible() or isinstance(parent, QtGui.QMainWindow)) ): self._initialized = True self._resize_items() def showEvent(self, event): """Wait for the show event to resize items if necessary.""" QtGui.QSplitter.showEvent(self, event) if not self._initialized: self._initialized = True self._resize_items() def _resize_items(self): """Size the splitter based on the 'width' or 'height' attributes of the Traits UI view elements. """ use_widths = self.orientation() == QtCore.Qt.Orientation.Horizontal # Get the requested size for the items. sizes = [] for item in self._group.content: if use_widths: sizes.append(item.width) else: sizes.append(item.height) # Find out how much space is available. if use_widths: total = self.width() else: total = self.height() # Allocate requested space. avail = total remain = 0 for i, sz in enumerate(sizes): if avail <= 0: break if sz >= 0: if sz >= 1: sz = min(sz, avail) else: sz *= total sz = int(sz) sizes[i] = sz avail -= sz else: remain += 1 # Allocate the remainder to those parts that didn't request a width. if remain > 0: remain = int(avail // remain) for i, sz in enumerate(sizes): if sz < 0: sizes[i] = remain # If all requested a width, allocate the remainder to the last item. else: sizes[-1] += avail self.setSizes(sizes) class _GroupPanel(object): """A sub-panel for a single group of items. It may be either a layout or a widget. """ def __init__(self, group, ui, suppress_label=False): """Initialise the object.""" # Get the contents of the group: content = group.get_content() # Save these for other methods. self.group = group self.ui = ui if group.orientation == "horizontal": self.direction = QtGui.QBoxLayout.Direction.LeftToRight else: self.direction = QtGui.QBoxLayout.Direction.TopToBottom # outer is the top-level widget or layout that will eventually be # returned. sub is the QTabWidget or QToolBox corresponding to any # 'tabbed' or 'fold' layout. It is only used to collapse nested # widgets. inner is the object (not necessarily a layout) that new # controls should be added to. outer = sub = inner = None # Get the group label. if suppress_label: label = "" else: label = group.label # Create a border if requested. if group.show_border: outer = QtGui.QGroupBox(label) inner = QtGui.QBoxLayout(self.direction, outer) elif label != "": outer = inner = QtGui.QBoxLayout(self.direction) inner.addWidget(heading_text(None, text=label).control) # Add the layout specific content. if len(content) == 0: pass elif group.layout == "flow": raise NotImplementedError("'the 'flow' layout isn't implemented") elif group.layout == "split": # Create the splitter. splitter = _GroupSplitter(group) splitter.setOpaqueResize(False) # Mimic wx backend resize behavior if self.direction == QtGui.QBoxLayout.Direction.TopToBottom: splitter.setOrientation(QtCore.Qt.Orientation.Vertical) # Make sure the splitter will expand to fill available space policy = splitter.sizePolicy() policy.setHorizontalStretch(50) policy.setVerticalStretch(50) if group.orientation == "horizontal": policy.setVerticalPolicy(QtGui.QSizePolicy.Policy.Expanding) else: policy.setHorizontalPolicy(QtGui.QSizePolicy.Policy.Expanding) splitter.setSizePolicy(policy) if outer is None: outer = splitter else: inner.addWidget(splitter) # Create an editor. editor = SplitterGroupEditor( control=outer, splitter=splitter, ui=ui ) self._setup_editor(group, editor) self._add_splitter_items(content, splitter) elif group.layout in ("tabbed", "fold"): # Create the TabWidget or ToolBox. if group.layout == "tabbed": sub = QtGui.QTabWidget() else: sub = QtGui.QToolBox() # Give tab/tool widget stretch factor equivalent to default stretch # factory for a resizeable item. See end of '_add_items'. policy = sub.sizePolicy() policy.setHorizontalStretch(50) policy.setVerticalStretch(50) sub.setSizePolicy(policy) _fill_panel(sub, content, self.ui, self._add_page_item) if outer is None: outer = sub else: inner.addWidget(sub) # Create an editor. editor = TabbedFoldGroupEditor(container=sub, control=outer, ui=ui) self._setup_editor(group, editor) else: if group.scrollable: # ensure a widget rather than a layout for the scroll area if outer is None: outer = inner = QtGui.QBoxLayout(self.direction) if isinstance(outer, QtGui.QLayout): widget = QtGui.QWidget() widget.setLayout(outer) outer = widget # now create a scroll area for the widget scroll_area = QtGui.QScrollArea() scroll_area.setWidget(outer) scroll_area.setWidgetResizable(True) outer = scroll_area # See if we need to control the visual appearance of the group. if group.visible_when != "" or group.enabled_when != "": # Make sure that outer is a widget and inner is a layout. # Hiding a layout is not properly supported by Qt (the # workaround in ``traitsui.qt.editor._visible_changed_helper`` # often leaves undesirable blank space). if outer is None: outer = inner = QtGui.QBoxLayout(self.direction) if isinstance(outer, QtGui.QLayout): widget = QtGui.QWidget() widget.setLayout(outer) outer = widget # Create an editor. self._setup_editor(group, GroupEditor(control=outer)) if isinstance(content[0], Group): layout = self._add_groups(content, inner) else: layout = self._add_items(content, inner) layout.setAlignment(QtCore.Qt.AlignmentFlag.AlignLeft | QtCore.Qt.AlignmentFlag.AlignTop) if outer is None: outer = layout elif layout is not inner: inner.addLayout(layout) if group.style_sheet: if isinstance(outer, QtGui.QLayout): inner = outer outer = QtGui.QWidget() outer.setLayout(inner) # ensure this is not empty group if isinstance(outer, QtGui.QWidget): outer.setStyleSheet(group.style_sheet) # Publish the top-level widget, layout or None. self.control = outer # Publish the optional sub-control. self.sub_control = sub def _add_splitter_items(self, content, splitter): """Adds a set of groups or items separated by splitter bars.""" for item in content: # Get a panel for the Item or Group. if isinstance(item, Group): panel = _GroupPanel(item, self.ui, suppress_label=True).control else: panel = self._add_items([item]) # Add the panel to the splitter. if panel is not None: if isinstance(panel, QtGui.QLayout): # A QSplitter needs a widget. w = QtGui.QWidget() panel.setContentsMargins(0, 0, 0, 0) w.setLayout(panel) panel = w layout = panel.layout() if layout is not None: layout.setAlignment( QtCore.Qt.AlignmentFlag.AlignLeft | QtCore.Qt.AlignmentFlag.AlignTop ) splitter.addWidget(panel) def _setup_editor(self, group, editor): """Setup the editor for a group.""" if group.id != "": self.ui.info.bind(group.id, editor) if group.visible_when != "": self.ui.add_visible(group.visible_when, editor) if group.enabled_when != "": self.ui.add_enabled(group.enabled_when, editor) def _add_page_item(self, item, layout): """Adds a single Item to a page based panel.""" self._add_items([item], layout) def _add_groups(self, content, outer): """Adds a list of Group objects to the panel, creating a layout if needed. Return the outermost layout. """ # Use the existing layout if there is one. if outer is None: outer = QtGui.QBoxLayout(self.direction) # Process each group. for subgroup in content: panel = _GroupPanel(subgroup, self.ui).control if isinstance(panel, QtGui.QWidget): outer.addWidget(panel) elif isinstance(panel, QtGui.QLayout): outer.addLayout(panel) else: # The sub-group is empty which seems to be used as a way of # providing some whitespace. outer.addWidget(QtGui.QLabel(" ")) return outer def _label_when(self): """Set the visible and enabled states of all labels as controlled by a 'visible_when' or 'enabled_when' expression. """ self._evaluate_label_condition(self._label_enabled_whens, "enabled") self._evaluate_label_condition(self._label_visible_whens, "visible") def _evaluate_label_condition(self, conditions, kind): """Evaluates a list of (eval, widget) pairs and calls the appropriate method on the label widget to toggle whether it is visible/enabled as needed. """ context = self.ui._get_context(self.ui.context) method_dict = {"visible": "setVisible", "enabled": "setEnabled"} for when, label in conditions: method_to_call = getattr(label, method_dict[kind]) try: cond_value = eval(when, globals(), context) method_to_call(bool(cond_value)) except Exception: # catch errors in the validate_when expression from traitsui.api import raise_to_debug raise_to_debug() def _add_items(self, content, outer=None): """Adds a list of Item objects, creating a layout if needed. Return the outermost layout. """ # Get local references to various objects we need: ui = self.ui info = ui.info handler = ui.handler group = self.group show_left = group.show_left columns = group.columns self._label_enabled_whens = [] self._label_visible_whens = [] # See if a label is needed. show_labels = False for item in content: show_labels |= item.show_label # See if a grid layout is needed. if show_labels or columns > 1: inner = QtGui.QGridLayout() if outer is None: outer = inner else: outer.addLayout(inner) row = 0 if show_left: label_alignment = QtCore.Qt.AlignmentFlag.AlignRight else: label_alignment = QtCore.Qt.AlignmentFlag.AlignLeft else: # Use the existing layout if there is one. if outer is None: outer = QtGui.QBoxLayout(self.direction) inner = outer row = -1 label_alignment = 0 # Process each Item in the list: col = -1 for item in content: # Keep a track of the current logical row and column unless the # layout is not a grid. col += 1 if row >= 0 and col >= columns: col = 0 row += 1 # Get the name in order to determine its type: name = item.name # Convert a blank to a 5 pixel spacer: if name == " ": name = "5" # Check if is a label: if name == "": label = item.label if label != "": self._add_label_item(item, inner, row, col, show_labels) # Check if it is a separator: elif name == "_": self._add_separator_item( item, columns, inner, row, col, show_labels ) # Check if it is a spacer: elif all_digits.match(name): self._add_spacer_item(item, name, inner, row, col, show_labels) else: # Otherwise, it must be a trait Item: object = eval(item.object_, globals(), ui.context) trait = object.base_trait(name) desc = trait.tooltip if desc is None: desc = "Specifies " + trait.desc if trait.desc else "" # Get the editor factory associated with the Item: editor_factory = item.editor if editor_factory is None: editor_factory = trait.get_editor().trait_set( **item.editor_args ) # If still no editor factory found, use a default text editor: if editor_factory is None: from .text_editor import ToolkitEditorFactory editor_factory = ToolkitEditorFactory() # If the item has formatting traits set them in the editor # factory: 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 the item has an invalid state extended trait name, set it # in the editor factory: if item.invalid != "": editor_factory.invalid = item.invalid # Create the requested type of editor from the editor factory: factory_method = getattr( editor_factory, item.style + "_editor" ) editor = factory_method( ui, object, 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 if item.style_sheet: control.setStyleSheet(item.style_sheet) # Set the initial 'enabled' state of the editor from the factory: editor.enabled = editor_factory.enabled # Handle any label. 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 # Add emphasis to the editor control if requested: if item.emphasized: self._add_emphasis(control) # If the item wants focus, remember the control so we can set focus # immediately before opening the UI. if item.has_focus: self.ui._focus_control = control # Set the correct size on the control, as specified by the user: stretch = 0 item_width = item.width item_height = item.height if (item_width != -1) or (item_height != -1): is_horizontal = ( self.direction == QtGui.QBoxLayout.Direction.LeftToRight ) min_size = control.minimumSizeHint() width = min_size.width() height = min_size.height() force_width = False force_height = False if (0.0 < item_width <= 1.0) and is_horizontal: stretch = int(100 * item_width) item_width = int(item_width) if item_width < -1: item_width = -item_width force_width = True else: item_width = max(item_width, width) if (0.0 < item_height <= 1.0) and (not is_horizontal): stretch = int(100 * item_height) item_height = int(item_height) if item_height < -1: item_height = -item_height force_height = True else: item_height = max(item_height, height) control.setMinimumWidth(max(item_width, 0)) control.setMinimumHeight(max(item_height, 0)) if (stretch == 0 or not is_horizontal) and force_width: control.setMaximumWidth(item_width) if (stretch == 0 or is_horizontal) and force_height: control.setMaximumHeight(item_height) # Set size and stretch policies self._set_item_size_policy(editor, item, label, stretch) # Add the created editor control to the layout # FIXME: Need to decide what to do about border_size and padding self._add_widget(inner, control, row, col, show_labels) # ---- Update the UI object # Bind the editor into the UIInfo object name space so it can be # referred to by a Handler while the user interface is active: id = item.id or name info.bind(id, editor, item.id) self.ui._scrollable |= editor.scrollable # Also, add the editors to the list of editors used to construct # the user interface: ui._editors.append(editor) # If the handler wants to be notified when the editor is created, # add it to the list of methods to be called when the UI is # complete: defined = getattr(handler, id + "_defined", None) if defined is not None: ui.add_defined(defined) # If the editor is conditionally visible, add the visibility # 'expression' and the editor to the UI object's list of monitored # objects: if item.visible_when != "": ui.add_visible(item.visible_when, editor) # If the editor is conditionally enabled, add the enabling # 'expression' and the editor to the UI object's list of monitored # objects: if item.enabled_when != "": ui.add_enabled(item.enabled_when, editor) if ( len(self._label_enabled_whens) + len(self._label_visible_whens) ) > 0: for object in self.ui.context.values(): object.on_trait_change( lambda: self._label_when(), dispatch="ui" ) self._label_when() return outer def _add_label_item(self, item, inner, row, col, show_labels): label = item.label # Create the label widget. if item.style == "simple": label = QtGui.QLabel(label) else: label = heading_text(None, text=label).control self._add_widget(inner, label, row, col, show_labels) if item.emphasized: self._add_emphasis(label) if item.visible_when: self._label_visible_whens.append((item.visible_when, label)) if item.enabled_when: self._label_enabled_whens.append((item.enabled_when, label)) def _add_separator_item(self, item, columns, inner, row, col, show_labels): cols = columns # See if the layout is a grid. if row >= 0: # Move to the start of the row if necessary. if col > 0: col = 0 # Allow for the columns. if show_labels: cols *= 2 for i in range(cols): line = QtGui.QFrame() if self.direction == QtGui.QBoxLayout.Direction.LeftToRight: # Add a vertical separator: line.setFrameShape(QtGui.QFrame.Shape.VLine) if row < 0: inner.addWidget(line) else: inner.addWidget(line, i, row) else: # Add a horizontal separator: line.setFrameShape(QtGui.QFrame.Shape.HLine) if row < 0: inner.addWidget(line) else: inner.addWidget(line, row, i) line.setFrameShadow(QtGui.QFrame.Shadow.Sunken) def _add_spacer_item(self, item, name, inner, row, col, show_labels): # If so, add the appropriate amount of space to the layout: n = int(name) if self.direction == QtGui.QBoxLayout.Direction.LeftToRight: # Add a horizontal spacer: spacer = QtGui.QSpacerItem(n, 1) else: # Add a vertical spacer: spacer = QtGui.QSpacerItem(1, n) self._add_widget(inner, spacer, row, col, show_labels) def _set_item_size_policy(self, editor, item, label, stretch): """Set size policy of an item and its label (if any). How it is set: 1) The general rule is that we obey the item.resizable and item.springy settings. An item is considered resizable also if resizable is Undefined but the item is scrollable 2) However, if the labels are on the right, and the item is of a kind that cannot be stretched in horizontal (e.g. a checkbox), we make the label stretchable instead (to avoid big gaps between element and label) If the item is resizable, the _GroupPanel is set to be resizable. """ is_label_left = self.group.show_left is_item_resizable = (item.resizable is True) or ( (item.resizable is Undefined) and editor.scrollable ) is_item_springy = item.springy # handle exceptional case 2) item_policy = editor.control.sizePolicy().horizontalPolicy() if ( label is not None and not is_label_left and item_policy == QtGui.QSizePolicy.Policy.Minimum ): # this item cannot be stretched horizontally, and the label # exists and is on the right -> make label stretchable if necessary if ( self.direction == QtGui.QBoxLayout.Direction.LeftToRight and is_item_springy ): is_item_springy = False self._make_label_h_stretchable(label, stretch or 50) elif ( self.direction == QtGui.QBoxLayout.Direction.TopToBottom and is_item_resizable ): is_item_resizable = False self._make_label_h_stretchable(label, stretch or 50) if is_item_resizable: stretch = stretch or 50 # FIXME: resizable is not defined as trait, were is it used? self.resizable = True elif is_item_springy: stretch = stretch or 50 editor.set_size_policy( self.direction, is_item_resizable, is_item_springy, stretch ) return stretch def _make_label_h_stretchable(self, label, stretch): """Set size policies of a QLabel to be stretchable horizontally. :attr:`stretch` is the stretch factor that Qt uses to distribute the total size to individual elements """ label_policy = label.sizePolicy() label_policy.setHorizontalStretch(stretch) label_policy.setHorizontalPolicy(QtGui.QSizePolicy.Policy.Expanding) label.setSizePolicy(label_policy) def _add_widget( self, layout, w, row, column, show_labels, label_alignment=QtCore.Qt.AlignmentFlag(0), ): """Adds a widget to a layout taking into account the orientation and the position of any labels. """ # If the widget really is a widget then remove any margin so that it # fills the cell. if isinstance(w, QtGui.QWidget): wl = w.layout() if wl is not None: wl.setContentsMargins(0, 0, 0, 0) # See if the layout is a grid. if row < 0: if isinstance(w, QtGui.QWidget): layout.addWidget(w) elif isinstance(w, QtGui.QLayout): layout.addLayout(w) else: layout.addItem(w) else: if self.direction == QtGui.QBoxLayout.Direction.LeftToRight: # Flip the row and column. row, column = column, row if show_labels: # Convert the "logical" column to a "physical" one. column *= 2 # Determine whether to place widget on left or right of # "logical" column. if (label_alignment != 0 and not self.group.show_left) or ( label_alignment == 0 and self.group.show_left ): column += 1 if isinstance(w, QtGui.QWidget): layout.addWidget(w, row, column, label_alignment) elif isinstance(w, QtGui.QLayout): layout.addLayout(w, row, column, label_alignment) else: layout.addItem(w, row, column, 1, 1, label_alignment) 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 (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 = QtGui.QLabel(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?) # control.Bind(wx.EVT_LEFT_UP, show_help_popup) label_control.help = item.get_help(ui) if desc != "": label_control.setToolTip(desc) return label_control def _add_emphasis(self, control): """Adds emphasis to a specified control's font.""" # Set the foreground colour. pal = QtGui.QPalette(control.palette()) pal.setColor(QtGui.QPalette.ColorRole.WindowText, QtGui.QColor(0, 0, 127)) control.setPalette(pal) # Set the font. font = QtGui.QFont(control.font()) font.setBold(True) font.setPointSize(font.pointSize()) control.setFont(font) class GroupEditor(Editor): """A pseudo-editor that allows a group to be managed.""" def __init__(self, **traits): """Initialise the object.""" # We intentionally don't want to call Editor.__init__ here as # GroupEditor does its own thing. However, we still want Traits # machinery to be set up properly. HasPrivateTraits.__init__(self, **traits) self.trait_set(**traits) class SplitterGroupEditor(GroupEditor): """A pseudo-editor that allows a group with a 'split' layout to be managed.""" #: The QSplitter for the group splitter = Instance(_GroupSplitter) # -- UI preference save/restore interface --------------------------------- def restore_prefs(self, prefs): """Restores any saved user preference information associated with the editor. """ if isinstance(prefs, dict): structure = prefs.get("structure") else: structure = prefs self.splitter._initialized = True self.splitter.restoreState(structure) def save_prefs(self): """Returns any user preference information associated with the editor.""" return {"structure": self.splitter.saveState().data()} class TabbedFoldGroupEditor(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())} # ------------------------------------------------------------------------- # 'HTMLHelpWindow' class: # ------------------------------------------------------------------------- class HTMLHelpWindow(QtGui.QDialog): """Window for displaying Traits-based help text with HTML formatting.""" def __init__(self, parent, html_content, scale_dx, scale_dy): """Initializes the object.""" # Local import to avoid a WebKit dependency when one isn't needed. from pyface.qt import QtWebKit QtGui.QDialog.__init__(self, parent) layout = QtGui.QVBoxLayout(self) layout.setContentsMargins(0, 0, 0, 0) # Create the html control html_control = QtWebKit.QWebView() html_control.setSizePolicy( QtGui.QSizePolicy.Policy.Expanding, QtGui.QSizePolicy.Policy.Expanding ) html_control.setHtml(html_content) layout.addWidget(html_control) # Create the OK button bbox = QtGui.QDialogButtonBox( QtGui.QDialogButtonBox.StandardButton.Ok, QtCore.Qt.Orientation.Horizontal ) bbox.accepted.connect(self.accept) layout.addWidget(bbox) # Position and show the dialog position_window(self, parent=parent) self.show() # ------------------------------------------------------------------------- # Creates a Pyface HeadingText control: # ------------------------------------------------------------------------- HeadingText = None def heading_text(*args, create=False, **kw): """Create a Pyface HeadingText control.""" global HeadingText if HeadingText is None: from pyface.api import HeadingText widget = HeadingText(*args, create=create, **kw) widget.create() return widget ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/qt/value_editor.py0000644000175100001730000000171400000000000021514 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Defines the tree-based Python value editor and the value editor factory, for the wxPython user interface toolkit. """ from traitsui.editors.value_editor import _ValueEditor from .editor import Editor class SimpleEditor(_ValueEditor, Editor): """Returns the editor to use for simple style views.""" #: Override the value of the readonly trait. readonly = False class ReadonlyEditor(_ValueEditor, Editor): """Returns the editor to use for readonly style views.""" #: Override the value of the readonly trait. readonly = True ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/qt/video_editor.py0000644000175100001730000004273000000000000021511 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """Traits UI 'display only' video editor.""" from pyface.qt import is_qt5 from pyface.qt.QtCore import QPoint, Qt, QUrl, Signal from pyface.qt.QtGui import QImage, QPainter, QPalette, QSizePolicy from pyface.qt.QtMultimedia import ( QAudio, QMediaPlayer, QVideoFrame, ) from pyface.qt.QtMultimediaWidgets import QVideoWidget from traits.api import ( Any, Bool, Callable, Float, Instance, Range, Str, observe, ) from traitsui.editors.video_editor import AspectRatio, MediaStatus, PlayerState from .editor import Editor #: Map from ApectRatio enum values to Qt aspect ratio behaviours. aspect_ratio_map = { 'ignore': Qt.AspectRatioMode.IgnoreAspectRatio, 'keep': Qt.AspectRatioMode.KeepAspectRatio, 'expand': Qt.AspectRatioMode.KeepAspectRatioByExpanding, } #: Map from PlayerState enum values to QMediaPlayer states. if is_qt5: state_map = { 'stopped': QMediaPlayer.State.StoppedState, 'playing': QMediaPlayer.State.PlayingState, 'paused': QMediaPlayer.State.PausedState, } else: state_map = { 'stopped': QMediaPlayer.PlaybackState.StoppedState, 'playing': QMediaPlayer.PlaybackState.PlayingState, 'paused': QMediaPlayer.PlaybackState.PausedState, } #: Map from QMediaPlayer states to PlayerState enum values. reversed_state_map = {value: key for key, value in state_map.items()} #: Map from QMediaPlayer media status values to MediaStatus enum values. media_status_map = { QMediaPlayer.MediaStatus.NoMedia: 'no_media', QMediaPlayer.MediaStatus.LoadingMedia: 'loading', QMediaPlayer.MediaStatus.LoadedMedia: 'loaded', QMediaPlayer.MediaStatus.StalledMedia: 'stalled', QMediaPlayer.MediaStatus.BufferingMedia: 'buffering', QMediaPlayer.MediaStatus.BufferedMedia: 'buffered', QMediaPlayer.MediaStatus.EndOfMedia: 'end', QMediaPlayer.MediaStatus.InvalidMedia: 'invalid', } if is_qt5: media_status_map[QMediaPlayer.MediaStatus.UnknownMediaStatus] = 'unknown' if is_qt5: # These classes support dynamically modifying the video stream frames but # only work on Qt5 from pyface.qt.QtMultimedia import QAbstractVideoSurface class ImageWidget(QVideoWidget): """Paints a QImage to the window body.""" def __init__(self, parent=None, image_func=None): import numpy as np super().__init__(parent) self.image = QImage() self._np_image = np.zeros(shape=(0, 0, 4)) self.painter = None self.resizeEvent(None) if image_func is None: # Don't bother with creating an ndarray version self.image_func = lambda image, bbox: image, self._np_image else: self.image_func = image_func def resizeEvent(self, event): s = self.size() self.width = s.width() self.height = s.height() def setImage(self, image): self.image, self._np_image = self.image_func( image, (self.width, self.height) ) self.update() def paintEvent(self, event): super().paintEvent(event) if self.painter is None: self.painter = QPainter() self.painter.begin(self) if self.image: self.painter.drawImage(QPoint(0, 0), self.image) self.painter.end() class VideoSurface(QAbstractVideoSurface): frameAvailable = Signal(['QImage']) def __init__(self, widget=None): super().__init__() self.widget = widget def supportedPixelFormats(self, handleType): return [QVideoFrame.Format_RGB32] def present(self, frame): from pyface.qt.QtMultimedia import QAbstractVideoBuffer cloned_frame = QVideoFrame(frame) cloned_frame.map(QAbstractVideoBuffer.ReadOnly) image = QImage( cloned_frame.bits(), cloned_frame.width(), cloned_frame.height(), cloned_frame.bytesPerLine(), QVideoFrame.imageFormatFromPixelFormat( cloned_frame.pixelFormat() ), ) self.frameAvailable.emit(image) return True class VideoEditor(Editor): """Traits UI 'display only' video editor. This editor uses the Qt QMultimedia machinery to display video streams to the screen. Rather than being self-contained, the editor only concerns itself with displaying the video, and provides traits that control behaviour and provide internal state of the control during playback. """ #: Does the drawing onto the image plane control = Instance(QVideoWidget) #: Handles the image pulling so the frames can be processed. Qt5 only. surface = Any() #: The QMediaObject (Qt5) or QUrl (Qt6+) that holds the connection to the #: video stream. media_content = Any() #: The QMediaPlayer that controls playback of the video stream. media_player = Instance(QMediaPlayer) #: The aspect ratio of the video editor. aspect_ratio = AspectRatio() #: The current state of the player, synchronized to the trait named #: by factory.state. state = PlayerState() #: The current playback position of the player, synchronized to the trait #: named by factory.position. position = Float() #: The total duration of the current video, synchronized to the trait #: named by factory.duration. duration = Float() #: The media player playback status (loading, buffering, etc.), #: synchronized to the trait named by factory.media_status. media_status = MediaStatus() #: The percentage of the buffer currently filled, synchronized to the trait #: named by factory.buffer. buffer = Range(0, 100) #: A string holding the video error state, or "" if no error. Synchronized #: to the trait named by factory.video_error. video_error = Str() #: Whether the audio is muted or not, synchronized to the trait named by #: factory.muted. muted = Bool(False) #: The playback volume on a logarithmic scale (perceptually linear), #: synchronized to the trait named by factory.volume. volume = Range(0.0, 100.0) #: The playback rate. Negative values rewind the video. #: Synchronized to the trait named by factory.playback_rate. playback_rate = Float(1.0) #: Function to apply to the image. Takes ref to new frame and a size tuple. #: Synchronized to the trait named by factory.image_func. image_func = Callable() #: The change in position required for an update to be emitted. #: Synchronized to the trait named by factory.notify_interval. #: This is only used on Qt5. notify_interval = Float(1.0) #: Qt6-specific QAudioOutput handler. _audio = Any() def update_to_regular(self): if self.surface is not None: self.surface.frameAvailable.disconnect(self.control.setImage) self.surface = None self.control = QVideoWidget() self.control.setSizePolicy( QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding ) self.control.setBackgroundRole(QPalette.ColorRole.Window) self.media_player.setVideoOutput(self.control) def update_to_functional(self): self.control = ImageWidget(image_func=self.image_func) self.control.setSizePolicy( QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding ) self.control.setBackgroundRole(QPalette.ColorRole.Window) self.surface = VideoSurface(widget=self.control) self.surface.frameAvailable.connect(self.control.setImage) self.media_player.setVideoOutput(self.surface) # ------------------------------------------------------------------------ # Editor interface # ------------------------------------------------------------------------ def init(self, parent): """Initialize the editor by creating the underlying toolkit widget. Parameters ---------- parent : QWidget or None The parent widget for this widget. """ self.control = QVideoWidget() self.control.setSizePolicy( QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding ) self.control.setBackgroundRole(QPalette.ColorRole.Window) if is_qt5: self.media_player = QMediaPlayer(None, QMediaPlayer.VideoSurface) else: self.media_player = QMediaPlayer() self._set_video_url() self.media_player.setVideoOutput(self.control) if is_qt5: self.media_player.setMuted(self.muted) else: from pyface.qt.QtMultimedia import QAudioOutput self._audio = QAudioOutput() self._audio.setMuted(self.muted) self.media_player.setAudioOutput(self._audio) self._update_state() self._update_aspect_ratio() self._update_muted() self._update_volume() self._update_playback_rate() self._update_notify_interval() self._connect_signals() self.set_tooltip() def dispose(self): self._disconnect_signals() if self.media_player is not None: # Avoid a segfault if the media player is currently playing self.media_player.setVideoOutput(None) if not is_qt5: self.media_player.setAudioOutput(None) super().dispose() def update_editor(self): """Update the editor when the object trait changes externally.""" self._set_video_url() # ------------------------------------------------------------------------ # Private interface # ------------------------------------------------------------------------ def _connect_signals(self): if self.media_player is not None: if is_qt5: self.media_player.stateChanged.connect( self._state_changed_emitted ) self.media_player.error.connect(self._error_emitted) self.media_player.bufferStatusChanged.connect( self._buffer_status_changed_emitted ) self.media_player.notifyIntervalChanged.connect( self._notify_interval_changed_emitted ) else: self.media_player.playbackStateChanged.connect( self._state_changed_emitted ) self.media_player.errorOccurred.connect(self._error_emitted) self.media_player.bufferProgressChanged.connect( self._buffer_status_changed_emitted ) self.media_player.positionChanged.connect( self._position_changed_emitted ) self.media_player.durationChanged.connect( self._duration_changed_emitted ) self.media_player.mediaStatusChanged.connect( self._media_status_changed_emitted ) def _disconnect_signals(self): if self.media_player is not None: if is_qt5: self.media_player.stateChanged.disconnect( self._state_changed_emitted ) self.media_player.error.disconnect(self._error_emitted) self.media_player.bufferStatusChanged.disconnect( self._buffer_status_changed_emitted ) self.media_player.notifyIntervalChanged.disconnect( self._notify_interval_changed_emitted ) else: self.media_player.playbackStateChanged.disconnect( self._state_changed_emitted ) self.media_player.errorOccurred.disconnect(self._error_emitted) self.media_player.bufferProgressChanged.disconnect( self._buffer_status_changed_emitted ) self.media_player.positionChanged.disconnect( self._position_changed_emitted ) self.media_player.durationChanged.disconnect( self._duration_changed_emitted ) self.media_player.mediaStatusChanged.disconnect( self._media_status_changed_emitted ) def _set_video_url(self): qurl = QUrl.fromUserInput(self.value) if is_qt5: from pyface.qt.QtMultimedia import QMediaContent if qurl.isValid(): self.media_content = QMediaContent(qurl) else: self.media_content = QMediaContent(None) else: self.media_content = qurl self.control.updateGeometry() # Signal handlers ------------------------------------------------------- def _state_changed_emitted(self, state): self.state = reversed_state_map[state] def _position_changed_emitted(self, position): # Avoid telling Qt about the new position in `_position_changed` with self.updating_value(): self.position = position / 1000.0 def _duration_changed_emitted(self, duration): self.duration = duration / 1000.0 def _error_emitted(self, error): if error != QMediaPlayer.Error.NoError: self.video_error = self.media_player.errorString() else: self.video_error = '' def _media_status_changed_emitted(self, error): self.media_status = media_status_map[self.media_player.mediaStatus()] def _buffer_status_changed_emitted(self, error): if is_qt5: self.buffer = self.media_player.bufferStatus() else: self.buffer = int(self.media_player.bufferProgress() * 100) def _notify_interval_changed_emitted(self, interval): self.notify_interval = interval / 1000.0 # Trait change handlers ------------------------------------------------- @observe('aspect_ratio') def _aspect_ratio_observer(self, event): if self.control is not None: self._update_aspect_ratio() @observe('image_func') def _image_func_observer(self, event): if self.image_func is None: self.update_to_regular() elif not is_qt5: raise ValueError("image_func is not supported on Qt6") else: self.update_to_functional() @observe('media_content') def _media_content_observer(self, event): self.video_error = '' if self.media_player is not None: if is_qt5: self.media_player.setMedia(self.media_content) else: self.media_player.setSource(self.media_content) @observe('muted') def _muted_observer(self, event): if self.media_player is not None: self._update_muted() @observe('playback_rate') def _playback_rate_observer(self, event): if self.media_player is not None: self._update_playback_rate() @observe('position') def _position_observer(self, event): if self.media_player is not None and not self.updating: # position is given in ms self.media_player.setPosition(int(self.position * 1000)) @observe('state') def _state_observer(self, event): if self.media_player is not None: self._update_state() @observe('volume') def _volume_observer(self, event): if self.media_player is not None: self._update_volume() @observe('notify_interval') def _notify_interval_observer(self, event): if self.media_player is not None: self._update_notify_interval() # MediaPlayer management ------------------------------------------------ def _update_aspect_ratio(self): self.control.setAspectRatioMode(aspect_ratio_map[self.aspect_ratio]) def _update_muted(self): if is_qt5: self.media_player.setMuted(self.muted) else: self._audio.setMuted(self.muted) def _update_playback_rate(self): self.media_player.setPlaybackRate(self.playback_rate) def _update_state(self): if self.state == 'stopped': self.media_player.stop() self.control.repaint() elif self.state == 'playing': # XXX forcing a resize so video is scaled correctly on MacOS s = self.control.size() w = s.width() h = s.height() self.media_player.play() self.control.resize(w + 1, h + 1) self.control.resize(w, h) elif self.state == 'paused': self.media_player.pause() def _update_volume(self): linear_volume = QAudio.convertVolume( self.volume / 100.0, QAudio.VolumeScale.LogarithmicVolumeScale, QAudio.VolumeScale.LinearVolumeScale, ) if is_qt5: self.media_player.setVolume(int(linear_volume * 100)) else: self._audio.setVolume(linear_volume) def _update_notify_interval(self): # only used on Qt5 if is_qt5: # interval is given in ms interval = int(self.notify_interval * 1000) self.media_player.setNotifyInterval(interval) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/qt/view_application.py0000644000175100001730000001067300000000000022373 0ustar00runnerdocker00000000000000# (C) Copyright 2008-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # ------------------------------------------------------------------------------ # Copyright (c) 2007, Riverbank Computing Limited # All rights reserved. # # This software is provided without warranty under the terms of the BSD license. # However, when used with the GPL version of PyQt the additional terms # described in the PyQt GPL exception also apply # # Author: Riverbank Computing Limited # ------------------------------------------------------------------------------ """ Creates a PyQt specific modal dialog user interface that runs as a complete application, using information from the specified UI object. """ # Standard library imports. import os # System library imports. from pyface.qt import QtGui # ETS imports. from pyface.util.guisupport import ( is_event_loop_running_qt4, start_event_loop_qt4, ) KEEP_ALIVE_UIS = set() def on_ui_destroyed(object, name, old, destroyed): """Remove the UI object from KEEP_ALIVE_UIS.""" assert name == "destroyed" if destroyed: assert object in KEEP_ALIVE_UIS KEEP_ALIVE_UIS.remove(object) object.on_trait_change(on_ui_destroyed, "destroyed", remove=True) # ------------------------------------------------------------------------- # Creates a 'stand-alone' PyQt application to display a specified traits UI # View: # ------------------------------------------------------------------------- def view_application(context, view, kind, handler, id, scrollable, args): """Creates a stand-alone PyQt application to display a specified traits UI View. 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 object 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. 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. """ if (kind == "panel") or ((kind is None) and (view.kind == "panel")): kind = "modal" app = QtGui.QApplication.instance() if app is None or not is_event_loop_running_qt4(app): return ViewApplication( context, view, kind, handler, id, scrollable, args ).ui.result ui = view.ui( context, kind=kind, handler=handler, id=id, scrollable=scrollable, args=args, ) # If the UI has not been closed yet, we need to keep a reference to # it until it does close. if not ui.destroyed: KEEP_ALIVE_UIS.add(ui) ui.on_trait_change(on_ui_destroyed, "destroyed") return ui.result class ViewApplication(object): """Modal window that contains a stand-alone application.""" def __init__(self, context, view, kind, handler, id, scrollable, args): """Initializes the object.""" self.context = context self.view = view self.kind = kind self.handler = handler self.id = id self.scrollable = scrollable self.args = args # this will block for modal dialogs, but not non-modals self.ui = self.view.ui( self.context, kind=self.kind, handler=self.handler, id=self.id, scrollable=self.scrollable, args=self.args, ) # only non-modal UIs need to have an event loop started for them if kind not in {"modal", "livemodal"}: start_event_loop_qt4() ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1684768028.0998068 traitsui-8.0.0/traitsui/qt4/0000755000175100001730000000000000000000000016541 5ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/qt4/__init__.py0000644000175100001730000000710300000000000020653 0ustar00runnerdocker00000000000000# (C) Copyright 2005-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! from importlib import import_module from importlib.abc import MetaPathFinder, Loader from importlib.machinery import ModuleSpec try: from importlib.metadata import version except ImportError: from importlib_metadata import version from importlib.util import find_spec import os import sys import warnings try: from pyface.ui import ShadowedModuleFinder except ImportError: ShadowedModuleFinder = None from traits.etsconfig.api import ETSConfig if ShadowedModuleFinder is None: # Either pyface is old, or we are in the future when ShadowedModuleFinder # has been retired. Try to import pyface.ui.qt as a test; if it imports # everything is good. try: import pyface.ui.qt # noqa: F401 except ImportError: raise RuntimeError( """TraitsUI is running with an outdated version of Pyface The traitsui.qt4.* modules have moved to traitsui.qt.* and the older versions of Pyface will not be aware of their new location. Update the version of Pyface to at least 8.0.""", DeprecationWarning, stacklevel=2, ) elif any( ( isinstance(finder, ShadowedModuleFinder) and finder.package == "traitsui.qt4." ) for finder in sys.meta_path ): # Importing from traitsui.qt4.* is deprecated # Already have loader registered. warnings.warn( """The traitsui.qt4.* modules have moved to traitsui.qt.* Backward compatibility import hooks are in place. They will be removed in a future release of Pyface. """, DeprecationWarning, stacklevel=2, ) elif ( os.environ.get('ETS_QT4_IMPORTS', None) # environment says we want this or os.environ.get('ETS_TOOLKIT', None) == "qt4" # environment says old qt4 or ETSConfig.toolkit == "qt4" # the ETSConfig toolkit says old qt4 ): # make sure that pyface.ui.qt4 loads before we do anything import pyface.ui.qt4 # Register our loader. This is messing with global state that we do not own # so we only do it when we have other global state telling us to. sys.meta_path.append(ShadowedModuleFinder( package="traitsui.qt4.", true_package="traitsui.qt.", )) # Importing from traitsui.qt4.* is deprecated warnings.warn( """The traitsui.qt4.* modules have moved to traitsui.qt.* Backward compatibility import hooks have been automatically applied. They will be removed in a future release of Pyface. """, DeprecationWarning, stacklevel=2, ) else: # Don't import from this module, use a future warning as we want end-users # of ETS apps to see the hints about environment variables. warnings.warn( """The traitsui.qt4.* modules have moved to traitsui.qt.*. Applications which require backwards compatibility can either: - set the ETS_QT4_IMPORTS environment variable - set the ETS_TOOLKIT environment variable to "qt4", - the ETSConfig.toolkit to "qt4" - install a ShadowedModuleFinder into sys.meta_path:: import sys from pyface.ui import ShadowedModuleFinder sys.meta_path.append(ShadowedModuleFinder( package="traitsui.qt4.", true_package="traitsui.qt.", )) """, FutureWarning, stacklevel=2, ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/table_column.py0000644000175100001730000004664500000000000021066 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Defines the table column descriptor used by the editor and editor factory classes for numeric and table editors. """ import os from traits.api import ( Any, Bool, Callable, Constant, Dict, Enum, Expression, Float, HasPrivateTraits, Instance, Int, Property, Str, Union, ) from traits.trait_base import user_name_for, xgetattr from .editor_factory import EditorFactory from .menu import Menu from .ui_traits import Image, AView, EditorStyle from .toolkit_traits import Color, Font from .view import View # Set up a logger: import logging logger = logging.getLogger(__name__) # Flag used to indicate user has not specified a column label UndefinedLabel = "???" class TableColumn(HasPrivateTraits): """Represents a column in a table editor.""" # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- #: Column label to use for this column: label = Str(UndefinedLabel) #: Type of data contained by the column: # XXX currently no other types supported, but potentially there could be... type = Enum("text", "bool") #: Text color for this column: text_color = Color("black") #: Text font for this column: text_font = Union(None, Font) #: Cell background color for this column: cell_color = Color("white", allow_none=True) #: Cell background color for non-editable columns: read_only_cell_color = Color(0xF4F3EE, allow_none=True) #: Cell graph color: graph_color = Color(0xDDD9CC) #: Horizontal alignment of text in the column: horizontal_alignment = Enum("left", ["left", "center", "right"]) #: Vertical alignment of text in the column: vertical_alignment = Enum("center", ["top", "center", "bottom"]) #: Horizontal cell margin horizontal_margin = Int(4) #: Vertical cell margin vertical_margin = Int(3) #: The image to display in the cell: image = Image #: Renderer used to render the contents of this column: renderer = Any # A toolkit specific renderer #: Is the table column visible (i.e., viewable)? visible = Bool(True) #: Is this column editable? editable = Bool(True) #: Is the column automatically edited/viewed (i.e. should the column editor #: or popup be activated automatically on mouse over)? auto_editable = Bool(False) #: Should a checkbox be displayed instead of True/False? show_checkbox = Bool(True) #: Can external objects be dropped on the column? droppable = Bool(False) #: Context menu to display when this column is right-clicked: menu = Instance(Menu) #: The tooltip to display when the mouse is over the column: tooltip = Str() #: The width of the column (< 0.0: Default, 0.0..1.0: fraction of total #: table width, > 1.0: absolute width in pixels): width = Float(-1.0) #: The width of the column while it is being edited (< 0.0: Default, #: 0.0..1.0: fraction of total table width, > 1.0: absolute width in #: pixels): edit_width = Float(-1.0) #: The height of the column cell's row while it is being edited #: (< 0.0: Default, 0.0..1.0: fraction of total table height, #: > 1.0: absolute height in pixels): edit_height = Float(-1.0) #: The resize mode for this column. This takes precedence over other #: settings (like **width**, above). #: - "interactive": column can be resized by users or programmatically #: - "fixed": users cannot resize the column, but it can be set programmatically #: - "stretch": the column will be resized to fill the available space #: - "resize_to_contents": column will be sized to fit the contents, but then cannot be resized resize_mode = Enum("interactive", "fixed", "stretch", "resize_to_contents") #: The view (if any) to display when clicking a non-editable cell: view = AView #: Optional maximum value a numeric cell value can have: maximum = Float(trait_value=True) # ------------------------------------------------------------------------- #: Returns the actual object being edited: # ------------------------------------------------------------------------- def get_object(self, object): """Returns the actual object being edited.""" return object def get_label(self): """Gets the label of the column.""" return self.label def get_width(self): """Returns the width of the column.""" return self.width def get_edit_width(self, object): """Returns the edit width of the column.""" return self.edit_width def get_edit_height(self, object): """Returns the height of the column cell's row while it is being edited. """ return self.edit_height def get_type(self, object): """Gets the type of data for the column for a specified object.""" return self.type def get_text_color(self, object): """Returns the text color for the column for a specified object.""" return self.text_color_ def get_text_font(self, object): """Returns the text font for the column for a specified object.""" return self.text_font def get_cell_color(self, object): """Returns the cell background color for the column for a specified object. """ if self.is_editable(object): return self.cell_color_ return self.read_only_cell_color_ def get_graph_color(self, object): """Returns the cell background graph color for the column for a specified object. """ return self.graph_color_ def get_horizontal_alignment(self, object): """Returns the horizontal alignment for the column for a specified object. """ return self.horizontal_alignment def get_vertical_alignment(self, object): """Returns the vertical alignment for the column for a specified object. """ return self.vertical_alignment def get_image(self, object): """Returns the image to display for the column for a specified object.""" return self.image def get_renderer(self, object): """Returns the renderer for the column of a specified object.""" return self.renderer def is_editable(self, object): """Returns whether the column is editable for a specified object.""" return self.editable def is_auto_editable(self, object): """Returns whether the column is automatically edited/viewed for a specified object. """ return self.auto_editable def is_droppable(self, object, value): """Returns whether a specified value is valid for dropping on the column for a specified object. """ return self.droppable def get_menu(self, object): """Returns the context menu to display when the user right-clicks on the column for a specified object. """ return self.menu def get_tooltip(self, object): """Returns the tooltip to display when the user mouses over the column for a specified object. """ return self.tooltip def get_view(self, object): """Returns the view to display when clicking a non-editable cell.""" return self.view def get_maximum(self, object): """Returns the maximum value a numeric column can have.""" return self.maximum def on_click(self, object): """Called when the user clicks on the column.""" pass def on_dclick(self, object): """Called when the user clicks on the column.""" pass def cmp(self, object1, object2): """Returns the result of comparing the column of two different objects. This is deprecated. """ return (self.key(object1) > self.key(object2)) - ( self.key(object1) < self.key(object2) ) def __str__(self): """Returns the string representation of the table column.""" return self.get_label() class ObjectColumn(TableColumn): """A column for editing objects.""" # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- #: Name of the object trait associated with this column: name = Str() #: Column label to use for this column: label = Property() #: Trait editor used to edit the contents of this column: editor = Instance(EditorFactory) #: The editor style to use to edit the contents of this column: style = EditorStyle #: Format string to apply to column values: format = Str("%s") #: Format function to apply to column values: format_func = Callable() # ------------------------------------------------------------------------- # Trait view definitions: # ------------------------------------------------------------------------- traits_view = View( [ ["name", "label", "type", "|[Column Information]"], [ "horizontal_alignment{Horizontal}@", "vertical_alignment{Vertical}@", "|[Alignment]", ], ["editable", "9", "droppable", "9", "visible", "-[Options]>"], "|{Column}", ], [ [ "text_color@", "cell_color@", "read_only_cell_color@", "|[UI Colors]", ], "|{Colors}", ], [["text_font@", "|[Font]<>"], "|{Font}"], ["menu@", "|{Menu}"], ["editor@", "|{Editor}"], ) def _get_label(self): """Gets the label of the column.""" if self._label is not None: return self._label return user_name_for(self.name) def _set_label(self, label): old, self._label = self._label, label if old != label: self.trait_property_changed("label", old, label) def get_raw_value(self, object): """Gets the unformatted value of the column for a specified object.""" try: return xgetattr(self.get_object(object), self.name) except Exception as e: from traitsui.api import raise_to_debug raise_to_debug() return None def get_value(self, object): """Gets the formatted value of the column for a specified object.""" try: if self.format_func is not None: return self.format_func(self.get_raw_value(object)) return self.format % (self.get_raw_value(object),) except: logger.exception( "Error occurred trying to format a %s value" % self.__class__.__name__ ) return "Format!" def get_drag_value(self, object): """Returns the drag value for the column.""" return self.get_raw_value(object) def set_value(self, object, value): """Sets the value of the column for a specified object.""" target, name = self.target_name(object) setattr(target, name, value) def get_editor(self, object): """Gets the editor for the column of a specified object.""" if self.editor is not None: return self.editor target, name = self.target_name(object) return target.base_trait(name).get_editor() def get_style(self, object): """Gets the editor style for the column of a specified object.""" return self.style def key(self, object): """Returns the value to use for sorting.""" return self.get_raw_value(object) def is_droppable(self, object, value): """Returns whether a specified value is valid for dropping on the column for a specified object. """ if self.droppable: try: target, name = self.target_name(object) target.base_trait(name).validate(target, name, value) return True except: pass return False def target_name(self, object): """Returns the target object and name for the column.""" object = self.get_object(object) name = self.name col = name.rfind(".") if col < 0: return (object, name) return (xgetattr(object, name[:col]), name[col + 1 :]) class ExpressionColumn(ObjectColumn): """A column for displaying computed values.""" # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- #: The Python expression used to return the value of the column: expression = Expression #: Is this column editable? editable = Constant(False) #: The globals dictionary that should be passed to the expression #: evaluation: globals = Dict() def get_raw_value(self, object): """Gets the unformatted value of the column for a specified object.""" try: return eval(self.expression_, self.globals, {"object": object}) except Exception: logger.exception( "Error evaluating table column expression: %s" % self.expression ) return None class NumericColumn(ObjectColumn): """A column for editing Numeric arrays.""" # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- #: Column label to use for this column label = Property() #: Text color this column when selected selected_text_color = Color("black") #: Text font for this column when selected selected_text_font = Font #: Cell background color for this column when selected selected_cell_color = Color(0xD8FFD8) #: Formatting string for the cell value format = Str("%s") #: Horizontal alignment of text in the column; this value overrides the #: default. horizontal_alignment = "center" def _get_label(self): """Gets the label of the column.""" if self._label is not None: return self._label return self.name def _set_label(self, label): old, self._label = self._label, label if old != label: self.trait_property_changed("label", old, label) def get_type(self, object): """Gets the type of data for the column for a specified object row.""" return self.type def get_text_color(self, object): """Returns the text color for the column for a specified object row.""" if self._is_selected(object): return self.selected_text_color_ return self.text_color_ def get_text_font(self, object): """Returns the text font for the column for a specified object row.""" if self._is_selected(object): return self.selected_text_font return self.text_font def get_cell_color(self, object): """Returns the cell background color for the column for a specified object row. """ if self.is_editable(object): if self._is_selected(object): return self.selected_cell_color_ return self.cell_color_ return self.read_only_cell_color_ def get_horizontal_alignment(self, object): """Returns the horizontal alignment for the column for a specified object row. """ return self.horizontal_alignment def get_vertical_alignment(self, object): """Returns the vertical alignment for the column for a specified object row. """ return self.vertical_alignment def is_editable(self, object): """Returns whether the column is editable for a specified object row.""" return self.editable def is_droppable(self, object, row, value): """Returns whether a specified value is valid for dropping on the column for a specified object row. """ return self.droppable def get_menu(self, object, row): """Returns the context menu to display when the user right-clicks on the column for a specified object row. """ return self.menu def get_value(self, object): """Gets the value of the column for a specified object row.""" try: value = getattr(object, self.name) try: return self.format % (value,) except: return "Format!" except: return "Undefined!" def set_value(self, object, row, value): """Sets the value of the column for a specified object row.""" column = self.get_data_column(object) column[row] = type(column[row])(value) def get_editor(self, object): """Gets the editor for the column of a specified object row.""" return super().get_editor(object) def get_data_column(self, object): """Gets the entire contents of the specified object column.""" return getattr(object, self.name) def _is_selected(self, object): """Returns whether a specified object row is selected.""" if ( hasattr(object, "model_selection") and object.model_selection is not None ): return True return False class ListColumn(TableColumn): """A column for editing lists.""" # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- # Label to use for this column label = Property() #: Index of the list element associated with this column index = Int() # Is this column editable? This value overrides the base class default. editable = False # ------------------------------------------------------------------------- # Trait view definitions: # ------------------------------------------------------------------------- traits_view = View( [ ["index", "label", "type", "|[Column Information]"], ["text_color@", "cell_color@", "|[UI Colors]"], ] ) def _get_label(self): """Gets the label of the column.""" if self._label is not None: return self._label return "Column %d" % (self.index + 1) def _set_label(self, label): old, self._label = self._label, label if old != label: self.trait_property_changed("label", old, label) def get_value(self, object): """Gets the value of the column for a specified object.""" return str(object[self.index]) def set_value(self, object, value): """Sets the value of the column for a specified object.""" object[self.index] = value def get_editor(self, object): """Gets the editor for the column of a specified object.""" return None def key(self, object): """Returns the value to use for sorting.""" return object[self.index] ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/table_filter.py0000644000175100001730000004724400000000000021052 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Defines the filter object used to filter items displayed in a table editor. """ from traits.api import ( Any, Bool, Callable, Enum, Event, Expression, HasPrivateTraits, Instance, List, Str, Trait, ) from .editor_factory import EditorFactory from .group import Group from .include import Include from .item import Item from .menu import Action from .table_column import ObjectColumn from .view import View # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- GenericTableFilterRuleOperation = Trait( "=", { "=": "eq", "<>": "ne", "<": "lt", "<=": "le", ">": "gt", ">=": "ge", "contains": "contains", "starts with": "starts_with", "ends with": "ends_with", }, ) class TableFilter(HasPrivateTraits): """Filter for items displayed in a table.""" # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- #: UI name of this filter (so the user can identify it in the UI) name = Str("Default filter") #: Default name that can be automatically overridden _name = Str("Default filter") #: A user-readable description of what kind of object satisfies the filter desc = Str("All items") #: A callable function that returns whether the passed object is allowed #: by the filter allowed = Callable(lambda object: True, transient=True) #: Is the filter a template (i.e., non-deletable, non-editable)? template = Bool(False) # ------------------------------------------------------------------------- # Class constants: # ------------------------------------------------------------------------- #: Traits that are ignored by the _anytrait_changed() handler ignored_traits = ["_name", "template", "desc"] # ------------------------------------------------------------------------- # Traits view definitions: # ------------------------------------------------------------------------- traits_view = View( "name{Filter name}", "_", Include("filter_view"), title="Edit Filter", width=0.2, buttons=[ "OK", "Cancel", Action( name="Help", action="show_help", defined_when="ui.view_elements.content['filter_view']" ".help_id != ''", ), ], ) searchable_view = View( [ [Include("search_view"), "|[]"], ["handler.status~", "|[]<>"], [ "handler.find_next`Find the next matching item`", "handler.find_previous`Find the previous matching item`", "handler.select`Select all matching items`", "handler.OK`Exit search`", "-<>", ], "|<>", ], title="Search for", kind="livemodal", width=0.25, ) search_view = Group(Include("filter_view")) filter_view = Group() def filter(self, object): """Returns whether a specified object meets the filter or search criteria. """ return self.allowed(object) def description(self): """Returns a user-readable description of what kind of object satisfies the filter. """ return self.desc def edit(self, object): """Edits the contents of the filter.""" return self.edit_traits(view=self.edit_view(object), kind="livemodal") def edit_view(self, object): """Return a view to use for editing the filter. The ''object'' parameter is a sample object for the table that the filter will be applied to. It is supplied in case the filter needs to extract data or metadata from the object. If the table is empty, the ''object'' argument is None. """ return None # ------------------------------------------------------------------------- # 'object' interface: # ------------------------------------------------------------------------- def __str__(self): return self.name # ------------------------------------------------------------------------- # Event handlers: # ------------------------------------------------------------------------- def _anytrait_changed(self, name, old, new): if (name not in self.ignored_traits) and ( (self.name == self._name) or (self.name == "") ): self.name = self._name = self.description() class EvalTableFilter(TableFilter): """A table filter based on evaluating an expression.""" # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- #: Override the standard **name** trait name = "Default evaluation filter" #: Python expression which will be applied to each table item expression = Expression # ------------------------------------------------------------------------- # Traits view definitions: # ------------------------------------------------------------------------- filter_view = Group("expression") def filter(self, object): """Returns whether a specified object meets the filter or search criteria. """ if self._traits is None: self._traits = object.trait_names() try: return eval( self.expression_, globals(), object.trait_get(*self._traits) ) except: return False def description(self): """Returns a user readable description of what kind of object satisfies the filter. """ return self.expression class GenericTableFilterRule(HasPrivateTraits): """A general rule used by a table filter.""" # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- #: Filter this rule is part of filter = Instance("RuleTableFilter") #: Is this rule enabled? enabled = Bool(False) #: Is this rule an 'and' rule or an 'or' rule? and_or = Enum("and", "or") #: EnumEditor used to edit the **name** trait: name_editor = Instance(EditorFactory) #: Name of the object trait that this rule applies to name = Str() #: Operation to be applied in the rule operation = GenericTableFilterRuleOperation #: Editor used to edit the **value** trait value_editor = Instance(EditorFactory) #: Value to use in the operation when applying the rule to an object value = Any() # ------------------------------------------------------------------------- # Class constants: # ------------------------------------------------------------------------- #: Traits that are ignored by the _anytrait_changed() handler ignored_traits = ["filter", "name_editor", "value_editor"] def __init__(self, **traits): super().__init__(**traits) if self.name == "": names = list(self.filter._name_to_value.keys()) if len(names) > 0: names.sort() self.name = names[0] self.enabled = False def _name_changed(self, name): """Handles a change to the value of the **name** trait.""" filter = self.filter if (filter is not None) and (filter._object is not None): self.value = filter._name_to_value.get(name) self.value_editor = filter._object.base_trait(name).get_editor() # ------------------------------------------------------------------------- # Event handlers: # ------------------------------------------------------------------------- def _anytrait_changed(self, name, old, new): if (name not in self.ignored_traits) and (self.filter is not None): self.filter.modified = True if name != "enabled": self.enabled = True def clone_traits(self, traits=None, memo=None, copy=None, **metadata): """Clones a new object from this one, optionally copying only a specified set of traits.""" return ( super() .clone_traits(traits, memo, copy, **metadata) .trait_set(enabled=self.enabled, name=self.name) ) def description(self): """Returns a description of the filter.""" return "%s %s %s" % (self.name, self.operation, self.value) def is_true(self, object): """Returns whether the rule is true for a specified object.""" try: value1 = getattr(object, self.name) type1 = type(value1) value2 = self.value if not isinstance(value2, type1): value2 = type1(value2) return getattr(self, self.operation_)(value1, value2) except: return False # ------------------------------------------------------------------------- # Implemenations of the various rule operations: # ------------------------------------------------------------------------- def eq(self, value1, value2): return value1 == value2 def ne(self, value1, value2): return value1 != value2 def lt(self, value1, value2): return value1 < value2 def le(self, value1, value2): return value1 <= value2 def gt(self, value1, value2): return value1 > value2 def ge(self, value1, value2): return value1 >= value2 def contains(self, value1, value2): return value1.lower().find(value2.lower()) >= 0 def starts_with(self, value1, value2): return value1[: len(value2)].lower() == value2.lower() def ends_with(self, value1, value2): return value1[-len(value2) :].lower() == value2.lower() class GenericTableFilterRuleEnabledColumn(ObjectColumn): """Table column that indicates whether a filter rule is enabled.""" def get_value(self, object): """Returns the traits editor of the column for a specified object.""" if hasattr(object, "enabled") and object.enabled: return "\N{HEAVY CHECK MARK}" else: return "" class GenericTableFilterRuleAndOrColumn(ObjectColumn): """Table column that displays whether a filter rule is conjoining ('and') or disjoining ('or'). """ def get_value(self, object): """Returns the traits editor of the column for a specified object.""" if object.and_or == "or": return "or" return "" class GenericTableFilterRuleNameColumn(ObjectColumn): """Table column for the name of an object trait.""" def get_editor(self, object): """Returns the traits editor of the column for a specified object.""" return object.name_editor class GenericTableFilterRuleValueColumn(ObjectColumn): """Table column for the value of an object trait.""" def get_editor(self, object): """Returns the traits editor of the column for a specified object.""" return object.value_editor # ------------------------------------------------------------------------- # Defines the columns to display in the generic filter rule table: # ------------------------------------------------------------------------- # Columns to display in the table for generic filter rules. generic_table_filter_rule_columns = [ GenericTableFilterRuleAndOrColumn(name="and_or", label="or"), GenericTableFilterRuleNameColumn(name="name"), ObjectColumn(name="operation"), GenericTableFilterRuleValueColumn(name="value"), ] class RuleTableFilter(TableFilter): """A table filter based on rules.""" # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- #: Overrides the default **name** trait name = "Default rule-based filter" #: List of the filter rules to be applied rules = List(GenericTableFilterRule) #: Event fired when the contents of the filter have changed modified = Event() #: Persistence ID of the view view_id = Str("traitsui.table_filter.RuleTableFilter") #: Sample object that the filter will apply to _object = Any() #: Map of trait names and default values _name_to_value = Any() # ------------------------------------------------------------------------- # Traits view definitions: # ------------------------------------------------------------------------- error_view = View( Item( label="A menu or rule based filter can only be created for " "tables with at least one entry" ), title="Error Creating Filter", kind="livemodal", close_result=False, buttons=["Cancel"], ) def filter(self, object): """Returns whether a specified object meets the filter or search criteria. """ is_first = is_true = True for rule in self.rules: if rule.and_or == "or": if is_true and (not is_first): return True is_true = True if is_true: is_true = rule.is_true(object) is_first = False return is_true def description(self): """Returns a user-readable description of the kind of object that satisfies the filter. """ ors = [] ands = [] if len(self.rules) > 0: for rule in self.rules: if rule.and_or == "or": if len(ands) > 0: ors.append(" and ".join(ands)) ands = [] ands.append(rule.description()) if len(ands) > 0: ors.append(" and ".join(ands)) if len(ors) == 1: return ors[0] if len(ors) > 1: return " or ".join(["(%s)" % t for t in ors]) return super().description() def edit_view(self, object): """Return a view to use for editing the filter. The ''object'' parameter is a sample object for the table that the filter will be applied to. It is supplied in case the filter needs to extract data or metadata from the object. If the table is empty, the ''object'' argument is None. """ self._object = object if object is None: return self.edit_traits(view="error_view") names = object.editable_traits() self._name_to_value = object.get(names) return View( [ ["name{Filter name}", "_"], [ Item( "rules", id="rules_table", editor=self._get_table_editor(names), ), "|<>", ], ], id=self.view_id, title="Edit Filter", kind="livemodal", resizable=True, buttons=["OK", "Cancel"], width=0.4, height=0.3, ) def _get_table_editor(self, names): """Returns a table editor to use for editing the filter.""" from traitsui.api import TableEditor from traitsui.editors.api import EnumEditor return TableEditor( columns=generic_table_filter_rule_columns, orientation="vertical", deletable=True, sortable=False, configurable=False, auto_size=False, auto_add=True, row_factory=GenericTableFilterRule, row_factory_kw={ "filter": self, "name_editor": EnumEditor(values=names), }, ) def __getstate__(self): """Returns the state to be pickled. This definition overrides **object**. """ dict = self.__dict__.copy() if "_object" in dict: del dict["_object"] del dict["_name_to_value"] return dict def _rules_changed(self, rules): """Handles a change to the **rules** trait.""" for rule in rules: rule.filter = self # ------------------------------------------------------------------------- # Defines the columns to display in the menu filter rule table: # ------------------------------------------------------------------------- # Columns to display in the table for menu filters. menu_table_filter_rule_columns = [ GenericTableFilterRuleEnabledColumn(name="enabled", label=""), GenericTableFilterRuleNameColumn(name="name"), ObjectColumn(name="operation"), GenericTableFilterRuleValueColumn(name="value"), ] class MenuTableFilter(RuleTableFilter): """A table filter based on a menu of rules.""" # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- #: Overrides the default **name** trait name = "Default menu-based filter" #: Overrides the persistence ID of the view view_id = Str("traitsui.table_filter.MenuTableFilter") def filter(self, object): """Returns whether a specified object meets the filter or search criteria. """ for rule in self.rules: if rule.enabled and (not rule.is_true(object)): return False return True def description(self): """Returns a user8readable description of what kind of object satisfies the filter. """ result = " and ".join( [rule.description() for rule in self.rules if rule.enabled] ) if result != "": return result return "All items" def _get_table_editor(self, names): """Returns a table editor to use for editing the filter.""" from .api import TableEditor from .editors.api import EnumEditor names = self._object.editable_traits() name_editor = EnumEditor(values=names) if len(self.rules) == 0: self.rules = [ GenericTableFilterRule( filter=self, name_editor=name_editor ).trait_set(name=name) for name in names ] for rule in self.rules: rule.enabled = False return TableEditor( columns=menu_table_filter_rule_columns, orientation="vertical", deletable=True, sortable=False, configurable=False, auto_size=False, auto_add=True, row_factory=GenericTableFilterRule, row_factory_kw={"filter": self, "name_editor": name_editor}, ) # ------------------------------------------------------------------------- # Define some standard template filters: # ------------------------------------------------------------------------- EvalFilterTemplate = EvalTableFilter( name="Evaluation filter template", template=True ) RuleFilterTemplate = RuleTableFilter( name="Rule-based filter template", template=True ) MenuFilterTemplate = MenuTableFilter( name="Menu-based filter template", template=True ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/tabular_adapter.py0000644000175100001730000006662500000000000021554 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Defines the adapter classes associated with the Traits UI TabularEditor. """ from traits.api import ( Any, Bool, Dict, Enum, Event, Float, HasPrivateTraits, HasTraits, Instance, Int, Interface, List, Property, Str, Union, cached_property, observe, provides, ) from .toolkit_traits import Color, Font class ITabularAdapter(Interface): #: The row index of the current item being adapted: row = Int() #: The current column id being adapted (if any): column = Any() #: Current item being adapted: item = Any() #: The current value (if any): value = Any() #: The list of columns the adapter supports. The items in the list have the #: same format as the :py:attr:`columns` trait in the #: :py:class:`TabularAdapter` class, with the additional requirement that #: the ``string`` values must correspond to a ``string`` value in the #: associated :py:class:`TabularAdapter` class. columns = List(Str) #: Does the adapter know how to handle the current *item* or not: accepts = Bool() #: Does the value of *accepts* depend only upon the type of *item*? is_cacheable = Bool() @provides(ITabularAdapter) class AnITabularAdapter(HasPrivateTraits): # Implementation of the ITabularAdapter Interface ------------------------ #: The row index of the current item being adapted: row = Int() #: The current column id being adapted (if any): column = Any() #: Current item being adapted: item = Any() #: The current value (if any): value = Any() #: The list of columns the adapter supports. The items in the list have the #: same format as the :py:attr:`columns` trait in the #: :py:class:`TabularAdapter` class, with the additional requirement that #: the ``string`` values must correspond to a ``string`` value in the #: associated :py:class:`TabularAdapter` class. columns = List(Str) #: Does the adapter know how to handle the current *item* or not: accepts = Bool(True) #: Does the value of *accepts* depend only upon the type of *item*? is_cacheable = Bool(True) class TabularAdapter(HasPrivateTraits): """The base class for adapting list items to values that can be edited by a TabularEditor. """ # -- Public Trait Definitions --------------------------------------------- #: A list of columns that should appear in the table. Each entry can have #: one of two forms: ``string`` or ``(string, id)``, where ``string`` is #: the UI name of the column, and ``id`` is a value that identifies that #: column to the adapter. Normally this value is either a trait name or an #: index, but it can be any value that the adapter wants. If only #: ``string`` is specified, then ``id`` is the index of the ``string`` #: within :py:attr:`columns`. columns = List() #: Maps UI name of column to value identifying column to the adapter, if #: different. column_dict = Property() #: Specifies the default value for a new row. This will usually need to be #: overridden. default_value = Any("") #: The default text color for odd table rows. odd_text_color = Color(None, update=True) #: The default text color for even table rows. even_text_color = Color(None, update=True) #: The default text color for table rows. default_text_color = Color(None, update=True) #: The default background color for odd table rows. odd_bg_color = Color(None, update=True) #: The default background color for even table rows. even_bg_color = Color(None, update=True) #: The default background color for table rows. default_bg_color = Color(None, update=True) #: Horizontal alignment to use for a specified column. alignment = Enum("left", "center", "right") #: The Python format string to use for a specified column. format = Str("%s") #: Width of a specified column. width = Float(-1) #: Can the text value of each item be edited? can_edit = Bool(True) #: The value to be dragged for a specified row item. drag = Property() #: Can any arbitrary value be dropped onto the tabular view. can_drop = Bool(False) #: Specifies where a dropped item should be placed in the table relative to #: the item it is dropped on. dropped = Enum("after", "before") #: The font for a row item. font = Font(None) #: The text color for a row item. text_color = Property() #: The background color for a row item. bg_color = Property() #: The name of the default image to use for column items. image = Str(None, update=True) #: The text of a row/column item. text = Property() #: The content of a row/column item (may be any Python value). content = Property() #: The tooltip information for a row/column item. tooltip = Str() #: The context menu for a row/column item. menu = Any() #: The context menu for column header. column_menu = Any() #: List of optional delegated adapters. adapters = List(ITabularAdapter, update=True) # -- Traits Set by the Editor --------------------------------------------- #: The object whose trait is being edited. object = Instance(HasTraits) #: The name of the trait being edited. name = Str() #: The row index of the current item being adapted. row = Int() #: The column index of the current item being adapted. column = Int() #: The current column id being adapted (if any). column_id = Any() #: Current item being adapted. item = Any() #: The current value (if any). value = Any() # -- Private Trait Definitions -------------------------------------------- #: Cache of attribute handlers. cache = Dict() #: Event fired when the cache is flushed. cache_flushed = Event(update=True) #: The mapping from column indices to column identifiers (defined by the #: :py:attr:`columns` trait). column_map = Property(observe="columns") #: The mapping from column indices to column labels (defined by the #: :py:attr:`columns` trait). label_map = Property(observe="columns") #: The name of the trait on a row item containing the value to use #: as a row label. If ``None``, the label will be the empty string. row_label_name = Union(None, Str) #: For each adapter, specifies the column indices the adapter handles. adapter_column_indices = Property(observe="adapters,columns") #: For each adapter, specifies the mapping from column index to column id. adapter_column_map = Property(observe="adapters,columns") # ------------------------------------------------------------------------- # TabularAdapter interface # ------------------------------------------------------------------------- def cleanup(self): """Clean up the adapter to remove references to objects.""" self.trait_setq(object=None, item=None, value=None) # -- Adapter methods that are sensitive to item type ---------------------- def get_alignment(self, object, trait, column): """Returns the alignment style to use for a specified column. The possible values that can be returned are: ``'left'``, ``'center'`` or ``'right'``. All table items share the same alignment for a specified column. """ return self._result_for("get_alignment", object, trait, 0, column) def get_width(self, object, trait, column): """Returns the width to use for a specified column. If the value is <= 0, the column will have a *default* width, which is the same as specifying a width of 0.1. If the value is > 1.0, it is converted to an integer and the result is the width of the column in pixels. This is referred to as a *fixed width* column. If the value is a float such that 0.0 < value <= 1.0, it is treated as the *unnormalized fraction of the available space* that is to be assigned to the column. What this means requires a little explanation. To arrive at the size in pixels of the column at any given time, the editor adds together all of the *unnormalized fraction* values returned for all columns in the table to arrive at a total value. Each *unnormalized fraction* is then divided by the total to create a *normalized fraction*. Each column is then assigned an amount of space in pixels equal to the maximum of 30 or its *normalized fraction* multiplied by the *available space*. The *available space* is defined as the actual width of the table minus the width of all *fixed width* columns. Note that this calculation is performed each time the table is resized in the user interface, thus allowing columns of this type to increase or decrease their width dynamically, while leaving *fixed width* columns unchanged. """ return self._result_for("get_width", object, trait, 0, column) def get_can_edit(self, object, trait, row): """Returns whether the user can edit a specified row. A ``True`` result indicates that the value can be edited, while a ``False`` result indicates that it cannot. """ return self._result_for("get_can_edit", object, trait, row, 0) def get_drag(self, object, trait, row): """Returns the value to be *dragged* for a specified row. A result of ``None`` means that the item cannot be dragged. Note that the value returned does not have to be the actual row item. It can be any value that you want to drag in its place. In particular, if you want the drag target to receive a copy of the row item, you should return a copy or clone of the item in its place. Also note that if multiple items are being dragged, and this method returns ``None`` for any item in the set, no drag operation is performed. """ return self._result_for("get_drag", object, trait, row, 0) def get_can_drop(self, object, trait, row, value): """Returns whether the specified ``value`` can be dropped on the specified row. A value of ``True`` means the ``value`` can be dropped; and a value of ``False`` indicates that it cannot be dropped. The result is used to provide the user positive or negative drag feedback while dragging items over the table. ``value`` will always be a single value, even if multiple items are being dragged. The editor handles multiple drag items by making a separate call to :py:meth:`get_can_drop` for each item being dragged. """ return self._result_for("get_can_drop", object, trait, row, 0, value) def get_dropped(self, object, trait, row, value): """Returns how to handle a specified ``value`` being dropped on a specified row. The possible return values are: - ``'before'``: Insert the specified ``value`` before the dropped on item. - ``'after'``: Insert the specified ``value`` after the dropped on item. Note there is no result indicating *do not drop* since you will have already indicated that the ``object`` can be dropped by the result returned from a previous call to :py:meth:`get_can_drop`. """ return self._result_for("get_dropped", object, trait, row, 0, value) def get_font(self, object, trait, row, column=0): """Returns the font to use for displaying a specified row or cell. A result of ``None`` means use the default font; otherwise a toolkit font object should be returned. Note that all columns for the specified table row will use the font value returned. """ return self._result_for("get_font", object, trait, row, column) def get_text_color(self, object, trait, row, column=0): """Returns the text color to use for a specified row or cell. A result of ``None`` means use the default text color; otherwise a toolkit-compatible color should be returned. Note that all columns for the specified table row will use the text color value returned. """ return self._result_for("get_text_color", object, trait, row, column) def get_bg_color(self, object, trait, row, column=0): """Returns the background color to use for a specified row or cell. A result of ``None`` means use the default background color; otherwise a toolkit-compatible color should be returned. Note that all columns for the specified table row will use the background color value returned. """ return self._result_for("get_bg_color", object, trait, row, column) def get_image(self, object, trait, row, column): """Returns the image to display for a specified cell. A result of ``None`` means no image will be displayed in the specified table cell. Otherwise the result should either be the name of the image, or an :py:class:`~pyface.image_resource.ImageResource` object specifying the image to display. A name is allowed in the case where the image is specified in the :py:class:`~traitsui.editors.tabular_editor.TabularEditor` :py:attr:`~traitsui.editors.tabular_editor.TabularEditor.images` trait. In that case, the name should be the same as the string specified in the :py:class:`~pyface.image_resource.ImageResource` constructor. """ return self._result_for("get_image", object, trait, row, column) def get_format(self, object, trait, row, column): """Returns the Python formatting string to apply to the specified cell. The resulting of formatting with this string will be used as the text to display it in the table. The return can be any Python string containing exactly one old-style Python formatting sequence, such as ``'%.4f'`` or ``'(%5.2f)'``. """ return self._result_for("get_format", object, trait, row, column) def get_text(self, object, trait, row, column): """Returns a string containing the text to display for a specified cell. If the underlying data representation for a specified item is not a string, then it is your responsibility to convert it to one before returning it as the result. """ return self._result_for("get_text", object, trait, row, column) def get_content(self, object, trait, row, column): """Returns the content to display for a specified cell.""" return self._result_for("get_content", object, trait, row, column) def set_text(self, object, trait, row, column, text): """Sets the value for the specified cell. This method is called when the user completes an editing operation on a table cell. The string specified by ``text`` is the value that the user has entered in the table cell. If the underlying data does not store the value as text, it is your responsibility to convert ``text`` to the correct representation used. """ self._result_for("set_text", object, trait, row, column, text) def get_tooltip(self, object, trait, row, column): """Returns a string containing the tooltip to display for a specified cell. You should return the empty string if you do not wish to display a tooltip. """ return self._result_for("get_tooltip", object, trait, row, column) def get_menu(self, object, trait, row, column): """Returns the context menu for a specified cell.""" return self._result_for("get_menu", object, trait, row, column) def get_column_menu(self, object, trait, row, column): """Returns the context menu for a specified column.""" return self._result_for("get_column_menu", object, trait, row, column) # -- Adapter methods that are not sensitive to item type ------------------ def get_item(self, object, trait, row): """Returns the specified row item. The value returned should be the value that exists (or *logically* exists) at the specified ``row`` in your data. If your data is not really a list or array, then you can just use ``row`` as an integer *key* or *token* that can be used to retrieve a corresponding item. The value of ``row`` will always be in the range: 0 <= row < ``len(object, trait)`` (i.e. the result returned by the adapter :py:meth:`len` method). The default implementation assumes the trait defined by ``object.trait`` is a *sequence* and attempts to return the value at index ``row``. If an error occurs, it returns ``None`` instead. This definition should work correctly for lists, tuples and arrays, or any other object that is indexable, but will have to be overridden for all other cases. """ try: return getattr(object, trait)[row] except: return None def len(self, object, trait): """Returns the number of row items in the specified ``object.trait``. The result should be an integer greater than or equal to 0. The default implementation assumes the trait defined by ``object.trait`` is a *sequence* and attempts to return the result of calling ``len(object.trait)``. It will need to be overridden for any type of data which for which :py:func:`len` will not work. """ # Sometimes, during shutdown, the object has been set to None. if object is None: return 0 else: return len(getattr(object, trait)) def get_default_value(self, object, trait): """Returns a new default value for the specified ``object.trait`` list. This method is called when *insert* or *append* operations are allowed and the user requests that a new item be added to the table. The result should be a new instance of whatever underlying representation is being used for table items. The default implementation simply returns the value of the adapter's :py:attr:`default_value` trait. """ return self.default_value def delete(self, object, trait, row): """Deletes the specified row item. This method is only called if the *delete* operation is specified in the :py:class:`~traitsui.editors.tabular_editor.TabularEditor` :py:attr:`~traitsui.editors.tabular_editor.TabularEditor.operation` trait, and the user requests that the item be deleted from the table. The adapter can still choose not to delete the specified item if desired, although that may prove confusing to the user. The default implementation assumes the trait defined by ``object.trait`` is a mutable sequence and attempts to perform a ``del object.trait[row]`` operation. """ del getattr(object, trait)[row] def insert(self, object, trait, row, value): """Inserts ``value`` at the specified ``object.trait[row]`` index. The specified ``value`` can be: - An item being moved from one location in the data to another. - A new item created by a previous call to :py:meth:`~TabularAdapter.get_default_value`. - An item the adapter previously approved via a call to :py:meth:`~TabularAdapter.get_can_drop`. The adapter can still choose not to insert the item into the data, although that may prove confusing to the user. The default implementation assumes the trait defined by ``object.trait`` is a mutable sequence and attempts to perform an ``object.trait[row:row] = [value]`` operation. """ getattr(object, trait)[row:row] = [value] def get_column(self, object, trait, index): """Returns the column id corresponding to a specified column index.""" self.object, self.name = object, trait return self.column_map[index] # -- Property Implementations --------------------------------------------- def _get_drag(self): return self.item def _get_text_color(self): if (self.row % 2) == 1: return self.even_text_color_ or self.default_text_color return self.odd_text_color or self.default_text_color_ def _get_bg_color(self): if (self.row % 2) == 1: return self.even_bg_color_ or self.default_bg_color_ return self.odd_bg_color or self.default_bg_color_ def _get_text(self): return self.get_format( self.object, self.name, self.row, self.column ) % self.get_content(self.object, self.name, self.row, self.column) def _set_text(self, value): if isinstance(self.column_id, int): self.item[self.column_id] = self.value else: # Convert value to the correct trait type. try: trait_handler = self.item.trait(self.column_id).handler setattr( self.item, self.column_id, trait_handler.evaluate(self.value), ) except: setattr(self.item, self.column_id, value) def _get_content(self): if isinstance(self.column_id, int): return self.item[self.column_id] return getattr(self.item, self.column_id) # -- Property Implementations --------------------------------------------- @cached_property def _get_column_dict(self): cols = {} for i, value in enumerate(self.columns): if isinstance(value, str): cols.update({value: value}) else: cols.update({value[0]: value[1]}) return cols @cached_property def _get_column_map(self): map = [] for i, value in enumerate(self.columns): if isinstance(value, str): map.append(i) else: map.append(value[1]) return map def get_label(self, section, obj=None): """Override this method if labels will vary from object to object.""" return self.label_map[section] def get_row_label(self, section, obj=None): if self.row_label_name is None: return None rows = getattr(obj, self.name, None) if rows is None: return None item = rows[section] return getattr(item, self.row_label_name, None) @cached_property def _get_label_map(self): map = [] for i, value in enumerate(self.columns): if isinstance(value, str): map.append(value) else: map.append(value[0]) return map @cached_property def _get_adapter_column_indices(self): labels = self.label_map map = [] for adapter in self.adapters: indices = [] for label in adapter.columns: if not isinstance(label, str): label = label[0] indices.append(labels.index(label)) map.append(indices) return map @cached_property def _get_adapter_column_map(self): labels = self.label_map map = [] for adapter in self.adapters: mapping = {} for label in adapter.columns: id = None if not isinstance(label, str): label, id = label key = labels.index(label) if id is None: id = key mapping[key] = id map.append(mapping) return map # -- Private Methods ------------------------------------------------------ def _result_for(self, name, object, trait, row, column, value=None): """Returns/Sets the value of the specified *name* attribute for the specified *object.trait[row].column* item. """ self.object = object self.name = trait self.row = row self.column = column self.column_id = column_id = self.column_map[column] self.value = value self.item = item = self.get_item(object, trait, row) item_class = item.__class__ key = "%s:%s:%d" % (item_class.__name__, name, column) handler = self.cache.get(key) if handler is not None: return handler() prefix = name[:4] trait_name = name[4:] for i, adapter in enumerate(self.adapters): if column in self.adapter_column_indices[i]: adapter.row = row adapter.item = item adapter.value = value adapter.column = column_id = self.adapter_column_map[i][column] if adapter.accepts: get_name = "%s_%s" % (column_id, trait_name) if adapter.trait(get_name) is not None: if prefix == "get_": handler = lambda: getattr( adapter.trait_set( row=self.row, column=column_id, item=self.item, ), get_name, ) else: handler = lambda: setattr( adapter.trait_set( row=self.row, column=column_id, item=self.item, ), get_name, self.value, ) if adapter.is_cacheable: break return handler() else: if item is not None and hasattr(item_class, "__mro__"): for klass in item_class.__mro__: handler = self._get_handler_for( "%s_%s_%s" % (klass.__name__, column_id, trait_name), prefix, ) or self._get_handler_for( "%s_%s" % (klass.__name__, trait_name), prefix ) if handler is not None: break if handler is None: handler = self._get_handler_for( "%s_%s" % (column_id, trait_name), prefix ) or self._get_handler_for(trait_name, prefix) self.cache[key] = handler return handler() def _get_handler_for(self, name, prefix): """Returns the handler for a specified trait name (or None if not found). """ if self.trait(name) is not None: if prefix == "get_": return lambda: getattr(self, name) return lambda: setattr(self, name, self.value) return None @observe("columns,adapters.items.+update") def _flush_cache(self, event): """Flushes the cache when the columns or any trait on any adapter changes. """ self.cache = {} self.cache_flushed = True ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1684768028.0998068 traitsui-8.0.0/traitsui/testing/0000755000175100001730000000000000000000000017506 5ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/testing/README.txt0000644000175100001730000000117100000000000021204 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ This package provide functionality to help testing GUI applications built using TraitsUI. The top-level ``api`` module re-exposes the public API for convenience. Private modules in this package serve TraitsUI only. """ ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/testing/__init__.py0000644000175100001730000000117100000000000021617 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ This package provide functionality to help testing GUI applications built using TraitsUI. The top-level ``api`` module re-exposes the public API for convenience. Private modules in this package serve TraitsUI only. """ ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/testing/_exception_handling.py0000644000175100001730000000454100000000000024065 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Utility functions for handling exceptions from the GUI event loop. In the test context, uncaught exceptions should cause tests to error. """ from contextlib import contextmanager import logging import sys import traceback _TRAITSUI_LOGGER = logging.getLogger("traitsui") def _serialize_exception(exc_type, value, tb): """Serialize exception and traceback for reporting. This is such that the stack frame is not prevented from being garbage collected. """ return ( str(exc_type), str(value), str("".join(traceback.format_exception(exc_type, value, tb))), ) @contextmanager def reraise_exceptions(logger=_TRAITSUI_LOGGER): """Context manager to capture all exceptions occurred in the context and then reraise a RuntimeError if there are any exceptions captured. Depending on the GUI toolkit backend, unexpected exceptions occurred in the GUI event loop may (1) cause fatal early exit of the test suite or (2) be printed to the console without causing the test to error. This context manager is intended for testing purpose such that unexpected exceptions will result in a test error. Parameters ---------- logger : logging.Logger Logger to use for logging errors. """ serialized_exceptions = [] def excepthook(type, value, tb): serialized = _serialize_exception(type, value, tb) serialized_exceptions.append(serialized) logger.error( "Unexpected error captured in sys excepthook. \n%s", serialized[-1] ) sys.excepthook = excepthook try: yield finally: sys.excepthook = sys.__excepthook__ if serialized_exceptions: msg = "Uncaught exceptions found.\n" msg += "\n".join( "=== Exception (type: {}, value: {}) ===\n" "{}".format(*record) for record in serialized_exceptions ) raise RuntimeError(msg) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/testing/_gui.py0000644000175100001730000000311400000000000021002 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Module containing GUI event processing utility for testing purposes. """ from traits.etsconfig.api import ETSConfig def process_cascade_events(): """Process all posted events, and attempt to process new events posted by the processed events. Cautions: - An infinite cascade of events will cause this function to enter an infinite loop. - There still exists technical difficulties with Qt. On Qt4 + OSX, QEventLoop.processEvents may report false saying it had found no events to process even though it actually had processed some. Consequently the internal loop breaks too early such that there are still cascaded events unprocessed. Problems are also observed on Qt5 + Appveyor occasionally. At the very least, events that are already posted prior to calling this function will be processed. See enthought/traitsui#951 """ if ETSConfig.toolkit.startswith("qt"): from pyface.qt import QtCore event_loop = QtCore.QEventLoop() while event_loop.processEvents(QtCore.QEventLoop.ProcessEventsFlag.AllEvents): pass else: from pyface.api import GUI GUI.process_events() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/testing/api.py0000644000175100001730000000413500000000000020634 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Core API for traitsui.testing Tester ------ - :class:`~.UITester` Interactions (for changing GUI states) -------------------------------------- - :class:`~.KeyClick` - :class:`~.KeySequence` - :class:`~.MouseClick` - :class:`~.MouseDClick` Interactions (for getting GUI states) ------------------------------------- - :class:`~.DisplayedText` - :class:`~.IsChecked` - :class:`~.IsEnabled` - :class:`~.IsVisible` - :class:`~.Selected` - :class:`~.SelectedIndices` - :class:`~.SelectedText` Locations (for locating GUI elements) ------------------------------------- - :class:`~.Cell` - :class:`~.Index` - :class:`~.Slider` - :class:`~.TargetById` - :class:`~.TargetByName` - :class:`~.Textbox` Advanced usage -------------- - :class:`~.TargetRegistry` Exceptions ---------- - :class:`~.Disabled` - :class:`~.InteractionNotSupported` - :class:`~.LocationNotSupported` - :class:`~.TesterError` """ # Note: Imports are ordered to match the docstring, not in the typical # alphabetical order # Tester from .tester.ui_tester import UITester # Interactions (for changing GUI states) from .tester.command import ( MouseClick, MouseDClick, KeyClick, KeySequence ) # Interactions (for getting GUI states) from .tester.query import ( DisplayedText, IsChecked, IsEnabled, IsVisible, Selected, SelectedIndices, SelectedText ) # Locations (for locating GUI elements) from .tester.locator import ( Cell, Index, TargetById, TargetByName, Textbox, Slider ) # Advanced usage from .tester.target_registry import TargetRegistry # Exceptions from .tester.exceptions import ( Disabled, InteractionNotSupported, LocationNotSupported, TesterError, ) ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1684768028.0998068 traitsui-8.0.0/traitsui/testing/data/0000755000175100001730000000000000000000000020417 5ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/testing/data/test.mp40000644000175100001730000000310500000000000022017 0ustar00runnerdocker00000000000000 ftypisomisomiso2avc1mp41freemdatEH, #x264 - core 160 r3011M cde9a93 - H.264/MPEG-4 AVC codec - Copyleft 2003-2020 - http://www.videolan.org/x264.html - options: cabac=1 ref=3 deblock=1:0:0 analyse=0x3:0x113 me=hex subme=7 psy=1 psy_rd=1.00:0.00 mixed_ref=1 me_range=16 chroma_me=1 trellis=1 8x8dct=1 cqm=0 deadzone=21,11 fast_pskip=1 chroma_qp_offset=-2 threads=7 lookahead_threads=1 sliced_threads=0 nr=0 decimate=1 interlaced=0 bluray_compat=0 constrained_intra=0 bframes=3 b_pyramid=2 b_adapt=1 b_bias=0 direct=1 weightb=1 open_gop=0 weightp=2 keyint=250 keyint_min=1 scenecut=40 intra_refresh=0 rc_lookahead=40 rc=crf mbtree=1 crf=25.0 qcomp=0.60 qpmin=0 qpmax=69 qpstep=4 ip_ratio=1.40 aq=1:1.00=eM :zQz٠dBK48S%ńA!cNA_*X moovlmvhd@7trak\tkhd@@$edtselstmdia mdhd@U-hdlrvideVideoHandlerZminfvmhd$dinfdref url stblstsdavc1@HH4avcCd gd AA B`h,stts@stssstscstszstco0budtaZmeta!hdlrmdirappl-ilst%toodataLavf58.45.100././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1684768028.0998068 traitsui-8.0.0/traitsui/testing/tester/0000755000175100001730000000000000000000000021014 5ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/testing/tester/README.txt0000644000175100001730000000167000000000000022516 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ This package contains library code for testing TraitsUI applications via ``UITester`` (in ``ui_tester`` module). Other public modules are also part of the public API. ``_ui_tester_registry`` contains internal implementations to support testing TraitsUI UI and editors. The only modules/packages that are specific about TraitsUI UI and editors are ``ui_tester`` and ``_ui_tester_registry``. The other modules are generic enough to be used with GUI components external to TraitsUI. This allows extensions beyond the scope of TraitsUI. """ ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/testing/tester/__init__.py0000644000175100001730000000167000000000000023131 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ This package contains library code for testing TraitsUI applications via ``UITester`` (in ``ui_tester`` module). Other public modules are also part of the public API. ``_ui_tester_registry`` contains internal implementations to support testing TraitsUI UI and editors. The only modules/packages that are specific about TraitsUI UI and editors are ``ui_tester`` and ``_ui_tester_registry``. The other modules are generic enough to be used with GUI components external to TraitsUI. This allows extensions beyond the scope of TraitsUI. """ ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/testing/tester/_abstract_target_registry.py0000644000175100001730000001040300000000000026624 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Interface of a registry depended on by the UIWrapper. This module is currently labelled as for internal use, but it can be made public when there is an external need. """ import abc class AbstractTargetRegistry(abc.ABC): """Abstract base class which defines the registry interface expected by :class:`~traitsui.testing.tester.ui_wrapper.UIWrapper`. """ @abc.abstractmethod def _get_handler(self, target, interaction): """Return a callable for handling an interaction for a given target. This is a protected method expected to be implemented by a subclass. Parameters ---------- target : any The UI target being operated on. interaction : any Any interaction object. Returns ------- handler : callable(UIWrapper, interaction) -> any The function to handle the given interaction on a target. Raises ------ InteractionNotSupported If the given target and interaction types are not supported by this registry. """ @abc.abstractmethod def _get_interactions(self, target): """Returns all the interactions supported for the given target. This is a protected method expected to be implemented by a subclass. Parameters ---------- target : any The UI target for which supported interactions are queried. Returns ------- interaction_classes : set Supported interaction types for the given target type. """ @abc.abstractmethod def _get_interaction_doc(self, target, interaction_class): """Return the documentation for the given target and interaction type. This is a protected method expected to be implemented by a subclass. Parameters ---------- target : any The UI target for which the interaction will be applied. interaction_class : subclass of type Any class. Returns ------- doc : str Raises ------ InteractionNotSupported If the given target and interaction types are not supported by this registry. """ @abc.abstractmethod def _get_solver(self, target, location): """Return a callable registered for resolving a location for the given target and location. This is a protected method expected to be implemented by a subclass. Parameters ---------- target : any The UI target being operated on. location : subclass of type The location to be resolved on the target. Raises ------ LocationNotSupported If the given locator and target types are not supported. """ @abc.abstractmethod def _get_locations(self, target): """Returns all the location types supported for the given target. This is a protected method expected to be implemented by a subclass. Parameters ---------- target : any The UI target for which supported location types are queried. Returns ------- locators_classes : set Supported locator types for the given target type. """ @abc.abstractmethod def _get_location_doc(self, target, locator_class): """Return the documentation for the given target and locator type. This is a protected method expected to be implemented by a subclass. Parameters ---------- target : any The UI target being operated on. locator_class : subclass of type Any class. Returns ------- doc : str Raises ------ LocationNotSupported If the given locator and target types are not supported. """ ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/testing/tester/_dynamic_target_registry.py0000644000175100001730000001525600000000000026460 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Implement the interface of AbstractTargetRegistry to support dynamic checking on the target. """ import inspect from traitsui.testing.tester._abstract_target_registry import ( AbstractTargetRegistry, ) from traitsui.testing.tester.exceptions import ( InteractionNotSupported, LocationNotSupported, ) class DynamicTargetRegistry(AbstractTargetRegistry): """Registry to support testing targets that satisfy a given criterion. An instance of this registry can be used with ``UITester`` and ``UIWrapper`` such that all given interactions and handlers will be applicable to any target that deemed to be supported by ``can_support``. The general priority rule applies. See :ref:`testing-how-extension-works` in the User Manual for further details. For stricter checks on the target type, use :class:`~traitsui.testing.tester.target_registry.TargetRegistry`. As an example, this registry:: registry = DynamicTargetRegistry( can_support=lambda target: target.__class__ is MyEditorClass, interaction_to_handler={MyAction: handler}, ) has equivalent behaviors compared to this registry:: registry = TargetRegistry() registry.register_interaction(MyEditorClass, MyAction, handler) Parameters ---------- can_support : callable(target) -> bool A callable that return true if the given target is supported by the registry. interaction_to_handler : dict(type, callable) A dictionary mapping from interaction type to handler callables. Each handler callable in the values should have the signature ``callable(UIWrapper, interaction) -> any``, where ``instance`` should have the type defined in the associated key. """ def __init__(self, *, can_support, interaction_to_handler): self.can_support = can_support self.interaction_to_handler = interaction_to_handler def _get_handler(self, target, interaction): """Return a callable for handling an interaction for a given target. This is an implementation for an abstract method. Parameters ---------- target : any The UI target being operated on. interaction : any Any interaction object. Returns ------- handler : callable(UIWrapper, interaction) -> any The function to handle the given interaction on a target. Raises ------ InteractionNotSupported If the given target and interaction types are not supported by this registry. """ if interaction.__class__ not in self._get_interactions(target): raise InteractionNotSupported( target_class=target.__class__, interaction_class=interaction.__class__, supported=list(self._get_interactions(target)), ) return self.interaction_to_handler[interaction.__class__] def _get_interactions(self, target): """Returns all the interactions supported for the given target. This is an implementation for an abstract method. Parameters ---------- target : any The UI target for which supported interactions are queried. Returns ------- interaction_classes : set Supported interaction types for the given target type. """ if self.can_support(target): return set(self.interaction_to_handler) return set() def _get_interaction_doc(self, target, interaction_class): """Return the documentation for the given target and interaction type. This is an implementation for an abstract method. Parameters ---------- target : any The UI target for which the interaction will be applied. interaction_class : subclass of type Any class. Returns ------- doc : str Raises ------ InteractionNotSupported If the given target and interaction types are not supported by this registry. """ if interaction_class not in self._get_interactions(target): raise InteractionNotSupported( target_class=target.__class__, interaction_class=interaction_class, supported=list(self._get_interactions(target)), ) return inspect.getdoc(interaction_class) def _get_solver(self, target, location): """Return a callable registered for resolving a location for the given target and location. This is an implementation for an abstract method. Parameters ---------- target : any The UI target being operated on. location : subclass of type The location to be resolved on the target. Raises ------ LocationNotSupported If the given locator and target types are not supported. """ raise LocationNotSupported( target_class=target.__class__, locator_class=location.__class__, supported=[], ) def _get_locations(self, target): """Returns all the location types supported for the given target. This is an implementation for an abstract method. Parameters ---------- target : any The UI target for which supported location types are queried. Returns ------- locators_classes : set Supported locator types for the given target type. """ return set() def _get_location_doc(self, target, locator_class): """Return the documentation for the given target and locator type. This is an implementation for an abstract method. Parameters ---------- target : any The UI target being operated on. locator_class : subclass of type Any class. Returns ------- doc : str Raises ------ LocationNotSupported If the given locator and target types are not supported. """ raise LocationNotSupported( target_class=target.__class__, locator_class=locator_class, supported=[], ) ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1684768028.0998068 traitsui-8.0.0/traitsui/testing/tester/_ui_tester_registry/0000755000175100001730000000000000000000000025106 5ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/testing/tester/_ui_tester_registry/README.txt0000644000175100001730000000122100000000000026600 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ This package contains implementations for supporting testing with TraitsUI UI and editors. The top-level module ``default_registry`` supplies the implementation to the ``UITester`` and hides implementation details internal to this package. """ ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/testing/tester/_ui_tester_registry/__init__.py0000644000175100001730000000122100000000000027213 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ This package contains implementations for supporting testing with TraitsUI UI and editors. The top-level module ``default_registry`` supplies the implementation to the ``UITester`` and hides implementation details internal to this package. """ ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/testing/tester/_ui_tester_registry/_common_ui_targets.py0000644000175100001730000000601500000000000031337 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ This module contains targets for UIWrapper so that the logic related to them can be reused. To use the logic in these objects, if the class in this module is a base class (indicated by leading _Base) they simply need to subclass this class, override any necessary traits, and then call the register method. """ class BaseSourceWithLocation: """Wrapper base class to hold locator information together with a source (typically an editor). This is useful for cases in which the location information is still necessary when performing actions such as a mouse click or key click. For example, an Enum editor and an index. This class is meant to be subclassed for specific usecases, and the class level attributes overridden. """ # The type of the source object on which the location information will be # evaluated on source_class = None # The type of the locator object that provides location information. # (e.g. locator.Index) locator_class = None # the handlers we want to register for the given source_class # must be given as a list of tuples where the first element is the # interaction class (e.g. command.MouseClick) and the second is the # actual handler function. # See TargetRegistry.register_interaction # for the signature of the callable. handlers = [] def __init__(self, source, location): """ Parameters ---------- source : instance of source_class The source object. Typically this is an editor. location : instance of locator_class The location information of interest """ self.source = source self.location = location @classmethod def register(cls, registry): """Class method to register interactions on a _SourceWithLocation for the given registry. It is expected that this class method will be called by subclasses, and thus interactions would be registered to subclasses rather than the base class. If there are any conflicts, an error will occur. Parameters ---------- registry : TargetRegistry The registry being registered to. """ registry.register_location( target_class=cls.source_class, locator_class=cls.locator_class, solver=lambda wrapper, location: cls(wrapper._target, location), ) for interaction_class, handler in cls.handlers: registry.register_interaction( target_class=cls, interaction_class=interaction_class, handler=handler, ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/testing/tester/_ui_tester_registry/_compat.py0000644000175100001730000000226100000000000027103 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ This module contains functions used by toolkit specific implementation for normalizing differences among toolkits (Qt and Wx). """ def check_key_compat(key): """Check if the given key is a unicode character within the range of values currently supported for emulating key sequences on both Qt and Wx textboxes. Parameters ---------- key : str A unicode character Raises ------ ValueError If the unicode character is not within the supported range of values. """ # Support for more characters can be added when there are needs. if ord(key) < 32 or ord(key) >= 127: raise ValueError( f"Key {key!r} is currently not supported. " f"Supported characters between code point 32 - 126." ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/testing/tester/_ui_tester_registry/_layout.py0000644000175100001730000001005300000000000027133 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ This module contains helper functions for working with layouts inside an editor. For example, converting indices so they count through the layout appropriately. """ def column_major_to_row_major(index, n, num_rows, num_cols): """Helper function to convert an index of a grid layout so that the index counts over the grid in the correct direction. In TraitsUI, grids are typically populated in row major order, however, the elements can be assigned to each entry in the grid so that when displayed they appear in column major order. To access the correct element we may need to convert a column-major based index into a row-major one. Parameters ---------- index : int the index of interest n : int The total number of elements in the layout num_rows : int the number of rows in the layout num_cols : int the number of columns in the layout Returns ------- int The converted index (now corresponding to row_major order) Notes ----- Since elements are populated in row major order, the resulting grid ends up having at most its last row as incomplete. The general approach for the algorithm is to find the coordinates as if we had counted through the grid in column major order, and then convert that back to a row major index. The complications come from the fact that the last row may be missing entries. Consider the example (n=17, num_row=4, num_cols=5) 0 4 8 11 14 1 5 9 12 15 2 6 10 13 16 3 7 / / / Row major order is 0, 4, 11, 14, 1, 5, ... If the given index is 7 then we are in a column of the matrix where all rows are full. This corresponds to the else branch below. From here, we simply find the (i,j) coordinates of the entry 7 above, with the upper left corner representing (0,0). Thus, (i,j) = (3,1). Now, to convert this to a row major index, we can simply take i * num_cols + j. If the given index is 15, then we are in a column where the last row is missing an entry. See the if branch below. In the case the logic used before won't work, because it would expect the grid to be filled. (i.e. if one tried to do this in the same way you would get (i,j) = (3,4)). To adderess this we break up the grid into two grids: 0 4 and 8 11 14 1 5 9 12 15 2 6 10 13 16 3 7 we find the (i2,j2) coordinates of the entry 15 above in grid2, with the upper left corner of grid2 representing (0,0). Hence, (i2,j2) = (1,2). We then find that index if we had counted in row major order for grid2, which would be i2 * num_empty_entries_last_row + j2 = 5, and add that to however many elements would need to be counted over from grid one to reach element 15 if we had been counting in row major order. Which is (num_cols - num_empty_entries_last_row)*(i2+1). """ if index > n: raise ValueError("Index is higher number of elements in layout") num_empty_entries_last_row = num_cols * num_rows - n if num_empty_entries_last_row < 0: raise ValueError("n can not be greater than num_cols * num_rows") if index > num_rows * (num_cols - num_empty_entries_last_row): num_entries_grid1 = num_rows * (num_cols - num_empty_entries_last_row) new_index = index - num_entries_grid1 i2 = new_index % (num_rows - 1) j2 = new_index // (num_rows - 1) return (num_cols - num_empty_entries_last_row) * (i2 + 1) + ( i2 * num_empty_entries_last_row + j2 ) else: i = index % num_rows j = index // num_rows return i * num_cols + j ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/testing/tester/_ui_tester_registry/_traitsui_ui.py0000644000175100001730000000650300000000000030164 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! from traitsui.testing.tester.locator import TargetById, TargetByName def _get_editor_by_name(ui, name): """Return a single Editor from an instance of traitsui.ui.UI with a given extended name. Raise if zero or many editors are found. Parameters ---------- ui : traitsui.ui.UI The UI from which an editor will be retrieved. name : str A single name for retrieving an editor on a UI. Returns ------- editor : Editor The single editor found. """ editors = ui.get_editors(name) all_names = [editor.name for editor in ui._editors] if not editors: raise ValueError( "No editors can be found with name {!r}. " "Found these: {!r}".format(name, all_names) ) if len(editors) > 1: raise ValueError("Found multiple editors with name {!r}.".format(name)) (editor,) = editors return editor def _get_editor_by_id(ui, id): """Return single Editor from an instance of traitsui.ui.UI with the given identifier. Parameters ---------- ui : traitsui.ui.UI The UI from which an editor will be retrieved. id : str Id for finding an item in the UI. Returns ------- editor : Editor The single editor found. """ try: editor = getattr(ui.info, id) except AttributeError: raise ValueError( "No editors found with id {!r}. Got these: {!r}".format( id, ui._names ) ) return editor def register_traitsui_ui_solvers(registry, target_class, traitsui_ui_getter): """Function to register solvers for obtaining nested targets inside a traitsui.ui.UI inside a (parent) target. For example, an instance of TreeEditor may contain a nested ``traitsui.ui.UI`` instance. In that case, the ``target_class`` will the TreeEditor and the ``traitsui_ui_getter`` specifies how to obtain the nested ``traitsui.ui.UI`` UI from it. Parameters ---------- registry : TargetRegistry The registry being registered to target_class : subclass of type The type of a UI target being used as the target_class for the solvers traitsui_ui_getter : callable(target: target_class) -> traitsui.ui.UI A callable specific to the particular target_class to obtain a nested UI. """ registry.register_location( target_class=target_class, locator_class=TargetByName, solver=lambda wrapper, location: ( _get_editor_by_name( ui=traitsui_ui_getter(wrapper._target), name=location.name, ) ), ) registry.register_location( target_class=target_class, locator_class=TargetById, solver=lambda wrapper, location: ( _get_editor_by_id( ui=traitsui_ui_getter(wrapper._target), id=location.id, ) ), ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/testing/tester/_ui_tester_registry/default_registry.py0000644000175100001730000000316600000000000031042 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! import importlib from traits.etsconfig.api import ETSConfig from traitsui.testing.tester.target_registry import TargetRegistry from traitsui.testing.tester._ui_tester_registry._traitsui_ui import ( register_traitsui_ui_solvers, ) from traitsui.ui import UI def get_default_registries(): """Creates a default registry for UITester that is toolkit specific. Returns ------- registries : list of AbstractTargetRegistry The default registries containing implementations for TraitsUI editors that are toolkit specific. """ # side-effect to determine current toolkit from pyface.toolkit import toolkit_object # noqa if ETSConfig.toolkit == "null": registries = [] else: toolkit = {'wx': 'wx', 'qt4': 'qt', 'qt': 'qt'}[ETSConfig.toolkit] this_package, _ = __name__.rsplit(".", 1) module = importlib.import_module( ".default_registry", this_package + '.' + toolkit ) registries = module.get_default_registries() ui_registry = TargetRegistry() register_traitsui_ui_solvers( registry=ui_registry, target_class=UI, traitsui_ui_getter=lambda target: target, ) return [ui_registry] + registries ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1684768028.103807 traitsui-8.0.0/traitsui/testing/tester/_ui_tester_registry/qt/0000755000175100001730000000000000000000000025532 5ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/testing/tester/_ui_tester_registry/qt/README.txt0000644000175100001730000000115600000000000027233 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ This package contains implementations for testing TraitsUI UI editors with Qt. The top-level module ``default_registry`` serves external packages and hides implementation details internal to this package. """ ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/testing/tester/_ui_tester_registry/qt/__init__.py0000644000175100001730000000115600000000000027646 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ This package contains implementations for testing TraitsUI UI editors with Qt. The top-level module ``default_registry`` serves external packages and hides implementation details internal to this package. """ ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/testing/tester/_ui_tester_registry/qt/_control_widget_registry.py0000644000175100001730000000427200000000000033223 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ This module provides a getter to obtain an AbstractTargetRegistry that can support testing any target whose 'control' attribute refers to a QWidget. """ from pyface.qt import QtGui from traitsui.testing.tester._dynamic_target_registry import ( DynamicTargetRegistry, ) from traitsui.testing.tester.query import IsEnabled, IsVisible def _handle_is_enabled(wrapper, interaction): """Return true if the target's control is enabled. Parameters ---------- wrapper : UIWrapper Wrapper on which the target's control should be a QWidget interaction : IsEnabled Not currently used. """ return wrapper._target.control.isEnabled() def _handle_is_visible(wrapper, interaction): """Return true if the target's control is visible. Parameters ---------- wrapper : UIWrapper Wrapper on which the target's control should be a QWidget interaction : IsVisible Not currently used. """ return wrapper._target.control.isVisible() def _is_target_control_a_qt_widget(target): """Return true if the target is accepted by the registry. Parameters ---------- target : any Any UI target Returns ------- is_accepted : bool """ return hasattr(target, "control") and isinstance( target.control, QtGui.QWidget ) def get_widget_registry(): """Return a registry to support any target with an attribute 'control' that is an instance of QWidget. Returns ------- registry : DynamicTargetRegistry A registry that can be used with UIWrapper """ return DynamicTargetRegistry( can_support=_is_target_control_a_qt_widget, interaction_to_handler={ IsEnabled: _handle_is_enabled, IsVisible: _handle_is_visible, }, ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/testing/tester/_ui_tester_registry/qt/_interaction_helpers.py0000644000175100001730000003267000000000000032314 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! import warnings from pyface.qt import QtCore, QtGui from pyface.qt.QtTest import QTest from traitsui.testing.tester._ui_tester_registry._compat import ( check_key_compat, ) from traitsui.testing.tester.exceptions import Disabled from traitsui.qt.key_event_to_name import key_map as _KEY_MAP def key_click(widget, key, delay): """Performs a key click of the given key on the given widget after a delay. Parameters ---------- widget : Qwidget The Qt widget to be key clicked. key : str Standardized (pyface) name for a keyboard event. e.g. "Enter", "Tab", "Space", "0", "1", "A", ... Note: modifiers (e.g. Shift, Alt, etc. are not currently supported) delay : int Time delay (in ms) in which the key click will be performed. """ mapping = {name: event for event, name in _KEY_MAP.items()} if key not in mapping: raise ValueError( "Unknown key {!r}. Expected one of these: {!r}".format( key, sorted(mapping) ) ) QTest.keyClick( widget, mapping[key], QtCore.Qt.KeyboardModifier.NoModifier, delay=delay, ) def check_q_model_index_valid(index): """Checks if a given QModelIndex is valid. Parameters ---------- index : QModelIndex Raises ------ LookupError If the index is not valid. """ if not index.isValid(): row = index.row() column = index.column() raise LookupError( "Unabled to locate item with row {!r} and column {!r}.".format( row, column, ) ) # Generic Handlers ########################################################### def displayed_text_qobject(widget): '''Helper function to define handlers for various Qwidgets to handle query.DisplayedText interactions. Parameters ---------- widget : Qwidget The Qwidget object with text to be displayed. Should be one of the following QWidgets: 1) QtGui.QLineEdit 2) QtGui.QTextEdit or 3) QtGui.QLabel Notes ----- Qt SimpleEditors occassionally use QtGui.QTextEdit as their control, and other times use QtGui.QLineEdit ''' if isinstance(widget, QtGui.QLineEdit): return widget.displayText() elif isinstance(widget, QtGui.QTextEdit): return widget.toPlainText() else: return widget.text() def mouse_click_qwidget(control, delay): """Performs a mouse click on a Qt widget. Parameters ---------- control : Qwidget The Qt widget to be clicked. delay : int Time delay (in ms) in which click will be performed. """ # for QAbstractButtons we do not use QTest.mouseClick as it assumes the # center of the widget as the location to be clicked, which may be # incorrect. For QAbstractButtons we can simply call their click method. if isinstance(control, QtGui.QAbstractButton): if delay > 0: QTest.qSleep(delay) control.click() elif control is not None: QTest.mouseClick( control, QtCore.Qt.MouseButton.LeftButton, delay=delay, ) else: raise ValueError("control is None") def mouse_click_tab_index(tab_widget, index, delay): """Performs a mouse click on a tab at an index in a QtGui.QTabWidget. Parameters ---------- tab_widget : QtGui.QTabWidget The tab widget containing the tab to be clicked. index : int The index of the tab to be clicked. delay : int Time delay (in ms) in which click will be performed. Raises ------ IndexError If the index is out of range. """ if not 0 <= index < tab_widget.count(): raise IndexError(index) tabbar = tab_widget.tabBar() rect = tabbar.tabRect(index) QTest.mouseClick( tabbar, QtCore.Qt.MouseButton.LeftButton, QtCore.Qt.KeyboardModifier.NoModifier, rect.center(), delay=delay, ) def mouse_click_qlayout(layout, index, delay): """Performs a mouse click on a widget at an index in a QLayout. Parameters ---------- layout : Qlayout The layout containing the widget to be clicked index : int The index of the widget in the layout to be clicked Raises ------ IndexError If the index is out of range. """ if not 0 <= index < layout.count(): raise IndexError(index) widget = layout.itemAt(index).widget() mouse_click_qwidget(widget, delay) def mouse_click_item_view(model, view, index, delay): """Perform mouse click on the given QAbstractItemModel (model) and QAbstractItemView (view) with the given row and column. Parameters ---------- model : QAbstractItemModel Model from which QModelIndex will be obtained view : QAbstractItemView View from which the widget identified by the index will be found and mouse click be performed. index : QModelIndex Raises ------ LookupError If the index cannot be located. Note that the index error provides more """ check_q_model_index_valid(index) rect = view.visualRect(index) QTest.mouseClick( view.viewport(), QtCore.Qt.MouseButton.LeftButton, QtCore.Qt.KeyboardModifier.NoModifier, rect.center(), delay=delay, ) def mouse_dclick_item_view(model, view, index, delay): """ Perform mouse double click on the given QAbstractItemModel (model) and QAbstractItemView (view) with the given row and column. Parameters ---------- model : QAbstractItemModel Model from which QModelIndex will be obtained view : QAbstractItemView View from which the widget identified by the index will be found and mouse double click be performed. index : QModelIndex Raises ------ LookupError If the index cannot be located. Note that the index error provides more """ check_q_model_index_valid(index) rect = view.visualRect(index) QTest.mouseDClick( view.viewport(), QtCore.Qt.LeftButton, QtCore.Qt.NoModifier, rect.center(), delay=delay, ) def key_sequence_item_view(model, view, index, sequence, delay=0): """ Perform Key Sequence on the given QAbstractItemModel (model) and QAbstractItemView (view) with the given row and column. Parameters ---------- model : QAbstractItemModel Model from which QModelIndex will be obtained view : QAbstractItemView View from which the widget identified by the index will be found and key sequence be performed. index : QModelIndex sequence : str Sequence of characters to be inserted to the widget identifed by the row and column. Raises ------ Disabled If the widget cannot be edited. LookupError If the index cannot be located. Note that the index error provides more """ check_q_model_index_valid(index) widget = view.indexWidget(index) if widget is None: raise Disabled( "No editable widget for item at row {!r} and column {!r}".format( index.row(), index.column() ) ) QTest.keyClicks(widget, sequence, delay=delay) def key_click_item_view(model, view, index, key, delay=0): """ Perform key press on the given QAbstractItemModel (model) and QAbstractItemView (view) with the given row and column. Parameters ---------- model : QAbstractItemModel Model from which QModelIndex will be obtained view : QAbstractItemView View from which the widget identified by the index will be found and key press be performed. index : int key : str Key to be pressed. Raises ------ Disabled If the widget cannot be edited. LookupError If the index cannot be located. Note that the index error provides more """ check_q_model_index_valid(index) widget = view.indexWidget(index) if widget is None: raise Disabled( "No editable widget for item at row {!r} and column {!r}".format( index.row(), index.column() ) ) key_click(widget, key=key, delay=delay) def get_display_text_item_view(model, view, index): """ Return the textural representation for the given model, row and column. Parameters ---------- model : QAbstractItemModel Model from which QModelIndex will be obtained view : QAbstractItemView View from which the widget identified by the index will be found and key press be performed. index : int Raises ------ LookupError If the index cannot be located. Note that the index error provides more """ check_q_model_index_valid(index) return model.data(index, QtCore.Qt.DisplayRole) def mouse_click_combobox(combobox, index, delay): """Perform a mouse click on a QComboBox at a given index. Paramters --------- combobox : QtGui.ComboBox The combobox to be clicked. index : int The index of the item in the combobox to be clicked. delay : int Time delay (in ms) in which each key click in the sequence will be performed. """ if combobox: q_model_index = combobox.model().index(index, 0) check_q_model_index_valid(q_model_index) mouse_click_item_view( model=combobox.model(), view=combobox.view(), index=q_model_index, delay=delay, ) # Otherwise the click won't get registered. key_click(combobox.view().viewport(), key="Enter", delay=delay) else: warnings.warn( "Attempted to click on a non-existant combobox. Nothing was " "performed." ) def key_sequence_qwidget(control, interaction, delay): """Performs simulated typing of a sequence of keys on the given widget after a delay. Parameters ---------- control : Qwidget The Qt widget to be acted on. interaction : instance of command.KeySequence The interaction object holding the sequence of key inputs to be simulated being typed delay : int Time delay (in ms) in which each key click in the sequence will be performed. Raises ------ Disabled If the widget is not enabled. """ if not control.isEnabled(): raise Disabled("{!r} is disabled.".format(control)) QTest.keyClicks(control, interaction.sequence, delay=delay) def key_sequence_textbox(control, interaction, delay): """Performs simulated typing of a sequence of keys on a widget that is a textbox. The keys are restricted to values also supported for testing wx.TextCtrl. Parameters ---------- control : QWidget The Qt widget intended to hold text for editing. e.g. QLineEdit and QTextEdit interaction : instance of command.KeySequence The interaction object holding the sequence of key inputs to be simulated being typed delay : int Time delay (in ms) in which each key click in the sequence will be performed. """ for key in interaction.sequence: check_key_compat(key) if not control.hasFocus(): key_click(widget=control, key="End", delay=0) key_sequence_qwidget(control=control, interaction=interaction, delay=delay) def key_click_qwidget(control, interaction, delay): """Performs simulated typing of a key on the given widget after a delay. Parameters ---------- control : Qwidget The Qt widget to be acted on. interaction : instance of command.KeyClick The interaction object holding the key input to be simulated being typed delay : int Time delay (in ms) in which the key click will be performed. Raises ------ Disabled If the widget is not enabled. """ if not control.isEnabled(): raise Disabled("{!r} is disabled.".format(control)) key_click(control, interaction.key, delay=delay) def key_click_qslider(control, interaction, delay): """Performs simulated typing of a key on the given slider after a delay. Only allowed keys are: "Left", "Right", "Up", "Down", "Page Up", "Page Down" Also, note that up related keys correspond to an increment on the slider, and down a decrement. Parameters ---------- control : QSlider The Qt slider to be acted on. interaction : instance of command.KeyClick The interaction object holding the key input to be simulated being typed delay : int Time delay (in ms) in which the key click will be performed. Raises ------ ValueError If the interaction.key is not one of the valid keys. """ valid_keys = {"Left", "Right", "Up", "Down", "Page Up", "Page Down"} if interaction.key not in valid_keys: raise ValueError( "Unexpected Key. Supported keys are: {}".format(sorted(valid_keys)) ) else: key_click(control, interaction.key, delay) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/testing/tester/_ui_tester_registry/qt/_registry_helper.py0000644000175100001730000000465400000000000031463 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ This module provides functions for registering interaction handlers and location solvers for common Qt GUI components. """ from traitsui.testing.tester.command import ( KeyClick, KeySequence, MouseClick, ) from traitsui.testing.tester.query import DisplayedText from traitsui.testing.tester._ui_tester_registry.qt import ( _interaction_helpers, ) def register_editable_textbox_handlers(registry, target_class, widget_getter): """Register common interactions for an editable textbox (in Qt) Parameters ---------- registry : TargetRegistry The registry being registered to. target_class : subclass of type The type of target being wrapped in a UIWrapper on which the interaction will be performed. widget_getter : callable(wrapper: UIWrapper) -> QWidget A callable to return a Qt widget for editing text, i.e. QLineEdit or QTextEdit. """ handlers = [ ( KeySequence, ( lambda wrapper, interaction: _interaction_helpers.key_sequence_textbox( widget_getter(wrapper), interaction, wrapper.delay ) ), ), ( KeyClick, ( lambda wrapper, interaction: _interaction_helpers.key_click_qwidget( widget_getter(wrapper), interaction, wrapper.delay ) ), ), ( MouseClick, ( lambda wrapper, _: _interaction_helpers.mouse_click_qwidget( widget_getter(wrapper), wrapper.delay ) ), ), ( DisplayedText, lambda wrapper, _: _interaction_helpers.displayed_text_qobject( widget_getter(wrapper) ), ), ] for interaction_class, handler in handlers: registry.register_interaction( target_class=target_class, interaction_class=interaction_class, handler=handler, ) ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1684768028.103807 traitsui-8.0.0/traitsui/testing/tester/_ui_tester_registry/qt/_traitsui/0000755000175100001730000000000000000000000027535 5ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/testing/tester/_ui_tester_registry/qt/_traitsui/README.txt0000644000175100001730000000130300000000000031230 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ The organization in this package should more or less mirror that of the ``traitsui.qt`` package and contains the corresponding logic for testing. Note that once new implementations are added, they may need to be exposed via the ``default_registry`` module (one level up from this package). """ ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/testing/tester/_ui_tester_registry/qt/_traitsui/__init__.py0000644000175100001730000000130300000000000031643 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ The organization in this package should more or less mirror that of the ``traitsui.qt`` package and contains the corresponding logic for testing. Note that once new implementations are added, they may need to be exposed via the ``default_registry`` module (one level up from this package). """ ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/testing/tester/_ui_tester_registry/qt/_traitsui/boolean_editor.py0000644000175100001730000000314300000000000033075 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! from traitsui.testing.tester.command import MouseClick from traitsui.testing.tester.query import DisplayedText, IsChecked from traitsui.testing.tester._ui_tester_registry.qt import ( _interaction_helpers, ) from traitsui.qt.boolean_editor import ReadonlyEditor, SimpleEditor def register(registry): """Register solvers/handlers specific to qt Boolean Editors for the given registry. If there are any conflicts, an error will occur. Parameters ---------- registry : TargetRegistry """ registry.register_interaction( target_class=SimpleEditor, interaction_class=MouseClick, handler=lambda wrapper, _: _interaction_helpers.mouse_click_qwidget( control=wrapper._target.control, delay=wrapper.delay ), ) registry.register_interaction( target_class=SimpleEditor, interaction_class=IsChecked, handler=lambda wrapper, _: wrapper._target.control.isChecked(), ) registry.register_interaction( target_class=ReadonlyEditor, interaction_class=DisplayedText, handler=lambda wrapper, _: _interaction_helpers.displayed_text_qobject( widget=wrapper._target.control ), ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/testing/tester/_ui_tester_registry/qt/_traitsui/button_editor.py0000644000175100001730000000272600000000000032777 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! from traitsui.testing.tester.command import MouseClick from traitsui.testing.tester.query import DisplayedText from traitsui.testing.tester._ui_tester_registry.qt import ( _interaction_helpers, ) from traitsui.qt.button_editor import CustomEditor, SimpleEditor def register(registry): """Register solvers/handlers specific to qt Button Editors for the given registry. If there are any conflicts, an error will occur. Parameters ---------- registry : TargetRegistry """ handlers = [ ( MouseClick, lambda wrapper, _: _interaction_helpers.mouse_click_qwidget( wrapper._target.control, wrapper.delay ), ), (DisplayedText, lambda wrapper, _: wrapper._target.control.text()), ] for target_class in [SimpleEditor, CustomEditor]: for interaction_class, handler in handlers: registry.register_interaction( target_class=target_class, interaction_class=interaction_class, handler=handler, ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/testing/tester/_ui_tester_registry/qt/_traitsui/check_list_editor.py0000644000175100001730000000506000000000000033566 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! from traitsui.qt.check_list_editor import CustomEditor from traitsui.testing.tester.command import MouseClick from traitsui.testing.tester.locator import Index from traitsui.testing.tester._ui_tester_registry._common_ui_targets import ( BaseSourceWithLocation, ) from traitsui.testing.tester._ui_tester_registry._layout import ( column_major_to_row_major, ) from traitsui.testing.tester._ui_tester_registry.qt import ( _interaction_helpers, ) class _IndexedCustomCheckListEditor(BaseSourceWithLocation): """Wrapper for CheckListEditor + locator.Index""" source_class = CustomEditor locator_class = Index handlers = [ ( MouseClick, ( lambda wrapper, _: _interaction_helpers.mouse_click_qlayout( layout=wrapper._target.source.control.layout(), index=convert_index( layout=wrapper._target.source.control.layout(), index=wrapper._target.location.index, ), delay=wrapper.delay, ) ), ), ] def convert_index(layout, index): """Helper function to convert an index for a QGridLayout so that the index counts over the grid in the correct direction. The grid is always populated in row major order, but it is done so in such a way that the entries appear in column major order. Qlayouts are indexed in the order they are populated, so to access the correct element we may need to convert a column-major based index into a row-major one. Parameters ---------- layout : QGridLayout The layout of interest index : int the index of interest """ n = layout.count() num_cols = layout.columnCount() num_rows = layout.rowCount() return column_major_to_row_major(index, n, num_rows, num_cols) def register(registry): """Register interactions for the given registry. If there are any conflicts, an error will occur. Parameters ---------- registry : TargetRegistry The registry being registered to. """ _IndexedCustomCheckListEditor.register(registry) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/testing/tester/_ui_tester_registry/qt/_traitsui/directory_editor.py0000644000175100001730000000175100000000000033465 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! from traitsui.testing.tester._ui_tester_registry.qt._registry_helper import ( register_editable_textbox_handlers, ) from traitsui.qt.directory_editor import SimpleEditor def register(registry): """Register interactions for the given registry. If there are any conflicts, an error will occur. Parameters ---------- registry : TargetRegistry The registry being registered to. """ register_editable_textbox_handlers( registry=registry, target_class=SimpleEditor, widget_getter=lambda wrapper: wrapper._target._file_name, ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/testing/tester/_ui_tester_registry/qt/_traitsui/editor_factory.py0000644000175100001730000000234200000000000033125 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! from traitsui.testing.tester.query import DisplayedText from traitsui.testing.tester._ui_tester_registry.qt._registry_helper import ( register_editable_textbox_handlers, ) from traitsui.qt.editor_factory import ReadonlyEditor, TextEditor def register(registry): """Register interactions for the given registry. If there are any conflicts, an error will occur. Parameters ---------- registry : TargetRegistry The registry being registered to. """ register_editable_textbox_handlers( registry=registry, target_class=TextEditor, widget_getter=lambda wrapper: wrapper._target.control, ) registry.register_interaction( target_class=ReadonlyEditor, interaction_class=DisplayedText, handler=lambda wrapper, _: wrapper._target.control.text(), ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/testing/tester/_ui_tester_registry/qt/_traitsui/enum_editor.py0000644000175100001730000001443600000000000032431 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! from traitsui.qt.enum_editor import ( ListEditor, RadioEditor, SimpleEditor, ) from traitsui.testing.tester.command import ( KeyClick, KeySequence, MouseClick, ) from traitsui.testing.tester.locator import Index from traitsui.testing.tester.query import DisplayedText, SelectedText from traitsui.testing.tester._ui_tester_registry._common_ui_targets import ( BaseSourceWithLocation, ) from traitsui.testing.tester._ui_tester_registry.qt import ( _interaction_helpers, ) from traitsui.testing.tester._ui_tester_registry._layout import ( column_major_to_row_major, ) class _IndexedListEditor(BaseSourceWithLocation): """Wrapper class for EnumListEditor and Index.""" source_class = ListEditor locator_class = Index handlers = [ ( MouseClick, ( lambda wrapper, _: _interaction_helpers.mouse_click_item_view( model=wrapper._target.source.control.model(), view=wrapper._target.source.control, index=wrapper._target.source.control.model().index( wrapper._target.location.index, 0 ), delay=wrapper.delay, ) ), ), ] class _IndexedRadioEditor(BaseSourceWithLocation): """Wrapper class for EnumRadioEditor and Index.""" source_class = RadioEditor locator_class = Index handlers = [ ( MouseClick, ( lambda wrapper, _: _interaction_helpers.mouse_click_qlayout( layout=wrapper._target.source.control.layout(), index=convert_index( layout=wrapper._target.source.control.layout(), index=wrapper._target.location.index, row_major=wrapper._target.source.row_major, ), delay=wrapper.delay, ) ), ), ] def convert_index(layout, index, row_major): """Helper function to convert an index for a QGridLayout so that the index counts over the grid in the correct direction. The grid is always populated in row major order. The row_major trait of a Radio Enum Editor simply changes what elements are assigned to each entry in the grid, so that when displayed, they appear in column major order. Qlayouts are indexed in the order they are populated, so to access the correct element we may need to convert a column-major based index into a row-major one. Parameters ---------- layout : QGridLayout The layout of interest index : int the index of interest row_major : bool whether or not the grid entries are organized in row major order """ if row_major: return index else: n = layout.count() num_cols = layout.columnCount() num_rows = layout.rowCount() return column_major_to_row_major(index, n, num_rows, num_cols) class _IndexedSimpleEditor(BaseSourceWithLocation): """Wrapper class for Simple EnumEditor and Index.""" source_class = SimpleEditor locator_class = Index handlers = [ ( MouseClick, ( lambda wrapper, _: _interaction_helpers.mouse_click_combobox( combobox=wrapper._target.source.control, index=wrapper._target.location.index, delay=wrapper.delay, ) ), ), ] def radio_selected_text_handler(wrapper, interaction): """Handler function used to query SelectedText for EnumRadioEditor. Parameters ---------- wrapper : UIWrapper The UIWrapper containing that object with text to be displayed. interaction : SelectedText Unused in this function but included to match the expected format of a handler. Should only be SelectedText """ control = wrapper._target.control for index in range(control.layout().count()): if control.layout().itemAt(index).widget().isChecked(): return control.layout().itemAt(index).widget().text() return None def register(registry): """Registry location and interaction handlers for EnumEditor. Parameters ---------- registry : InteractionRegistry """ _IndexedListEditor.register(registry) _IndexedRadioEditor.register(registry) _IndexedSimpleEditor.register(registry) simple_editor_text_handlers = [ ( KeyClick, ( lambda wrapper, interaction: _interaction_helpers.key_click_qwidget( control=wrapper._target.control, interaction=interaction, delay=wrapper.delay, ) ), ), ( KeySequence, ( lambda wrapper, interaction: _interaction_helpers.key_sequence_qwidget( control=wrapper._target.control, interaction=interaction, delay=wrapper.delay, ) ), ), ( DisplayedText, lambda wrapper, _: wrapper._target.control.currentText(), ), ( SelectedText, lambda wrapper, _: wrapper._target.control.currentText(), ), ] for interaction_class, handler in simple_editor_text_handlers: registry.register_interaction( target_class=SimpleEditor, interaction_class=interaction_class, handler=handler, ) registry.register_interaction( target_class=RadioEditor, interaction_class=SelectedText, handler=radio_selected_text_handler, ) registry.register_interaction( target_class=ListEditor, interaction_class=SelectedText, handler=lambda wrapper, _: wrapper._target.control.currentItem().text(), ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/testing/tester/_ui_tester_registry/qt/_traitsui/file_editor.py0000644000175100001730000000174400000000000032402 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! from traitsui.testing.tester._ui_tester_registry.qt._registry_helper import ( register_editable_textbox_handlers, ) from traitsui.qt.file_editor import SimpleEditor def register(registry): """Register interactions for the given registry. If there are any conflicts, an error will occur. Parameters ---------- registry : TargetRegistry The registry being registered to. """ register_editable_textbox_handlers( registry=registry, target_class=SimpleEditor, widget_getter=lambda wrapper: wrapper._target._file_name, ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/testing/tester/_ui_tester_registry/qt/_traitsui/font_editor.py0000644000175100001730000000172000000000000032423 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! from traitsui.testing.tester._ui_tester_registry.qt._registry_helper import ( register_editable_textbox_handlers, ) from traitsui.qt.font_editor import TextFontEditor def register(registry): """Register interactions pertaining to (Qt) FontEditor for the given registry. Parameters ---------- registry : TargetRegistry The registry being registered to. """ register_editable_textbox_handlers( registry=registry, target_class=TextFontEditor, widget_getter=lambda wrapper: wrapper._target.control, ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/testing/tester/_ui_tester_registry/qt/_traitsui/instance_editor.py0000644000175100001730000000563300000000000033270 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! from traitsui.testing.tester.command import MouseClick from traitsui.testing.tester.locator import Index from traitsui.testing.tester.query import SelectedText from traitsui.testing.tester._ui_tester_registry._common_ui_targets import ( BaseSourceWithLocation, ) from traitsui.testing.tester._ui_tester_registry.qt._interaction_helpers import ( # noqa mouse_click_combobox, mouse_click_qwidget, ) from traitsui.testing.tester._ui_tester_registry._traitsui_ui import ( register_traitsui_ui_solvers, ) from traitsui.qt.instance_editor import CustomEditor, SimpleEditor def _get_nested_ui_simple(target): """Obtains a nested UI within a Simple Instance Editor. Parameters ---------- target : instance of SimpleEditor """ return target._dialog_ui def _get_nested_ui_custom(target): """Obtains a nested UI within a Custom Instance Editor. Parameters ---------- target : instance of CustomEditor """ return target._ui def _get_combobox(target): """Obtains a nested combobox within an Instance Editor. Parameters ---------- target : instance of CustomEditor """ return target._choice def _click_combobox_index(wrapper, _): return mouse_click_combobox( combobox=_get_combobox(wrapper._target.source), index=wrapper._target.location.index, delay=wrapper.delay, ) def _get_combobox_text(wrapper, _): return _get_combobox(wrapper._target).currentText() class _IndexedCustomEditor(BaseSourceWithLocation): """Wrapper class for CustomEditors with a selection.""" source_class = CustomEditor locator_class = Index handlers = [ (MouseClick, _click_combobox_index), ] def register(registry): """Register interactions for the given registry. If there are any conflicts, an error will occur. Parameters ---------- registry : TargetRegistry The registry being registered to. """ _IndexedCustomEditor.register(registry) registry.register_interaction( target_class=SimpleEditor, interaction_class=MouseClick, handler=lambda wrapper, _: ( mouse_click_qwidget(wrapper._target._button, delay=wrapper.delay) ), ) register_traitsui_ui_solvers(registry, SimpleEditor, _get_nested_ui_simple) registry.register_interaction( target_class=CustomEditor, interaction_class=SelectedText, handler=_get_combobox_text, ) register_traitsui_ui_solvers(registry, CustomEditor, _get_nested_ui_custom) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/testing/tester/_ui_tester_registry/qt/_traitsui/list_editor.py0000644000175100001730000000743200000000000032436 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! from traitsui.testing.tester.command import MouseClick from traitsui.testing.tester.locator import Index from traitsui.testing.tester._ui_tester_registry._common_ui_targets import ( BaseSourceWithLocation, ) from traitsui.testing.tester._ui_tester_registry._traitsui_ui import ( register_traitsui_ui_solvers, ) from traitsui.testing.tester._ui_tester_registry.qt import ( _interaction_helpers, ) from traitsui.qt.list_editor import CustomEditor, NotebookEditor, SimpleEditor class _IndexedNotebookEditor(BaseSourceWithLocation): """Wrapper for a ListEditor (Notebook) with an index.""" source_class = NotebookEditor locator_class = Index handlers = [ ( MouseClick, ( lambda wrapper, _: _interaction_helpers.mouse_click_tab_index( tab_widget=wrapper._target.source.control, index=wrapper._target.location.index, delay=wrapper.delay, ) ), ), ] @classmethod def register(cls, registry): """Class method to register interactions on a _IndexedNotebookEditor for the given registry. If there are any conflicts, an error will occur. Parameters ---------- registry : TargetRegistry The registry being registered to. """ super().register(registry) register_traitsui_ui_solvers( registry=registry, target_class=cls, traitsui_ui_getter=lambda target: target._get_nested_ui(), ) def _get_nested_ui(self): """Method to get the nested ui corresponding to the List element at the given index. """ return self.source._uis[self.location.index][1] def _get_next_target(list_editor, index): """Gets the target at a given index from a Custom List Editor. Parameters ---------- list_editor : CustomEditor The custom style list editor in which the target is contained. index : int the index of the target of interest in the list Returns ------- traitsui.editor.Editor The obtained target. """ row, column = divmod(index, list_editor.factory.columns) # there are two columns for each list item (one for the item itself, # and another for the list menu button) column = 2 * column grid_layout = list_editor._list_pane.layout() item = grid_layout.itemAtPosition(row, column) if item is None: raise IndexError(index) if list_editor.scrollable: list_editor.control.ensureWidgetVisible(item.widget()) return item.widget()._editor def register(registry): """Register interactions for the given registry. If there are any conflicts, an error will occur. Parameters ---------- registry : TargetRegistry The registry being registered to. """ # NotebookEditor _IndexedNotebookEditor.register(registry) # CustomEditor registry.register_location( target_class=CustomEditor, locator_class=Index, solver=lambda wrapper, location: ( _get_next_target(wrapper._target, location.index) ), ) # SimpleEditor registry.register_location( target_class=SimpleEditor, locator_class=Index, solver=lambda wrapper, location: ( _get_next_target(wrapper._target, location.index) ), ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/testing/tester/_ui_tester_registry/qt/_traitsui/range_editor.py0000644000175100001730000000667400000000000032566 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! from traitsui.qt.range_editor import ( LargeRangeSliderEditor, LogRangeSliderEditor, RangeTextEditor, SimpleSliderEditor, ) from traitsui.testing.tester.command import KeyClick from traitsui.testing.tester.locator import Slider, Textbox from traitsui.testing.tester._ui_tester_registry.qt import ( _interaction_helpers, _registry_helper, ) class LocatedTextbox: """Wrapper class for a located Textbox in Qt. Parameters ---------- textbox : Instance of QtGui.QLineEdit """ def __init__(self, textbox): self.textbox = textbox @classmethod def register(cls, registry): """Class method to register interactions on a LocatedTextbox for the given registry. If there are any conflicts, an error will occur. Parameters ---------- registry : TargetRegistry The registry being registered to. """ _registry_helper.register_editable_textbox_handlers( registry=registry, target_class=cls, widget_getter=lambda wrapper: wrapper._target.textbox, ) class LocatedSlider: """Wrapper class for a located Slider in Qt. Parameters ---------- slider : Instance of QtGui.QSlider """ def __init__(self, slider): self.slider = slider @classmethod def register(cls, registry): """Class method to register interactions on a LocatedSlider for the given registry. If there are any conflicts, an error will occur. Parameters ---------- registry : TargetRegistry The registry being registered to. """ registry.register_interaction( target_class=cls, interaction_class=KeyClick, handler=lambda wrapper, interaction: _interaction_helpers.key_click_qslider( wrapper._target.slider, interaction, wrapper.delay ), ) def register(registry): """Register interactions for the given registry. If there are any conflicts, an error will occur. Parameters ---------- registry : TargetRegistry The registry being registered to. """ targets = [ SimpleSliderEditor, LogRangeSliderEditor, LargeRangeSliderEditor, ] for target_class in targets: registry.register_location( target_class=target_class, locator_class=Textbox, solver=lambda wrapper, _: LocatedTextbox( textbox=wrapper._target.control.text ), ) registry.register_location( target_class=target_class, locator_class=Slider, solver=lambda wrapper, _: LocatedSlider( slider=wrapper._target.control.slider ), ) _registry_helper.register_editable_textbox_handlers( registry=registry, target_class=RangeTextEditor, widget_getter=lambda wrapper: wrapper._target.control, ) LocatedTextbox.register(registry) LocatedSlider.register(registry) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/testing/tester/_ui_tester_registry/qt/_traitsui/table_editor.py0000644000175100001730000001005100000000000032541 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! from traitsui.qt.table_editor import SimpleEditor from traitsui.testing.tester.command import ( MouseClick, MouseDClick, KeyClick, KeySequence, ) from traitsui.testing.tester.locator import Cell from traitsui.testing.tester.query import ( DisplayedText, Selected, SelectedIndices, ) from traitsui.testing.tester._ui_tester_registry._common_ui_targets import ( BaseSourceWithLocation ) from traitsui.testing.tester._ui_tester_registry.qt import ( _interaction_helpers ) def _query_table_editor_selected(wrapper, interaction): selected = wrapper._target.selected if not isinstance(selected, list): if selected is None: return [] else: return [selected] else: return selected def _query_table_editor_selected_indices(wrapper, interaction): selected_indices = wrapper._target.selected_indices if not isinstance(selected_indices, list): if selected_indices == -1: return [] else: return [selected_indices] else: return selected_indices class _SimpleEditorWithCell(BaseSourceWithLocation): source_class = SimpleEditor locator_class = Cell handlers = [ (MouseClick, lambda wrapper, _: wrapper._target._mouse_click( delay=wrapper.delay)), (KeyClick, lambda wrapper, interaction: wrapper._target._key_click( key=interaction.key, delay=wrapper.delay,)), ( KeySequence, lambda wrapper, interaction: wrapper._target._key_sequence( sequence=interaction.sequence, delay=wrapper.delay, ) ), ( DisplayedText, lambda wrapper, _: wrapper._target._get_displayed_text() ), (MouseDClick, lambda wrapper, _: wrapper._target._mouse_dclick( delay=wrapper.delay,)), ] def _get_model_view_index(self): table_view = self.source.table_view return dict( model=table_view.model(), view=table_view, index=table_view.model().index( self.location.row, self.location.column ), ) def _mouse_click(self, delay=0): _interaction_helpers.mouse_click_item_view( **self._get_model_view_index(), delay=delay, ) def _mouse_dclick(self, delay=0): _interaction_helpers.mouse_dclick_item_view( **self._get_model_view_index(), delay=delay, ) def _key_sequence(self, sequence, delay=0): _interaction_helpers.key_sequence_item_view( **self._get_model_view_index(), sequence=sequence, delay=delay, ) def _key_click(self, key, delay=0): _interaction_helpers.key_click_item_view( **self._get_model_view_index(), key=key, delay=delay, ) def _get_displayed_text(self): return _interaction_helpers.get_display_text_item_view( **self._get_model_view_index(), ) def register(registry): """ Register interactions for the given registry. If there are any conflicts, an error will occur. Parameters ---------- registry : TargetRegistry The registry being registered to. """ _SimpleEditorWithCell.register(registry) registry.register_interaction( target_class=SimpleEditor, interaction_class=Selected, handler=_query_table_editor_selected ) registry.register_interaction( target_class=SimpleEditor, interaction_class=SelectedIndices, handler=_query_table_editor_selected_indices ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/testing/tester/_ui_tester_registry/qt/_traitsui/text_editor.py0000644000175100001730000000272100000000000032443 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! from traitsui.testing.tester.query import DisplayedText from traitsui.testing.tester._ui_tester_registry.qt import ( _interaction_helpers, ) from traitsui.testing.tester._ui_tester_registry.qt._registry_helper import ( register_editable_textbox_handlers, ) from traitsui.qt.text_editor import CustomEditor, ReadonlyEditor, SimpleEditor def register(registry): """Register interactions for the given registry. If there are any conflicts, an error will occur. Parameters ---------- registry : TargetRegistry The registry being registered to. """ for target_class in [CustomEditor, SimpleEditor]: register_editable_textbox_handlers( registry=registry, target_class=target_class, widget_getter=lambda wrapper: wrapper._target.control, ) registry.register_interaction( target_class=ReadonlyEditor, interaction_class=DisplayedText, handler=lambda wrapper, _: _interaction_helpers.displayed_text_qobject( wrapper._target.control ), ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/testing/tester/_ui_tester_registry/qt/_traitsui/ui_base.py0000644000175100001730000000263300000000000031522 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! from traitsui.qt.ui_base import ButtonEditor from traitsui.testing.tester.command import MouseClick from traitsui.testing.tester.query import DisplayedText from traitsui.testing.tester._ui_tester_registry.qt import ( _interaction_helpers, ) def register(registry): """Register solvers/handlers specific to qt ui_base Editors for the given registry. If there are any conflicts, an error will occur. Parameters ---------- registry : TargetRegistry """ handlers = [ ( MouseClick, ( lambda wrapper, _: _interaction_helpers.mouse_click_qwidget( wrapper._target.control, wrapper.delay ) ), ), (DisplayedText, lambda wrapper, _: wrapper._target.control.text()), ] for interaction_class, handler in handlers: registry.register_interaction( target_class=ButtonEditor, interaction_class=interaction_class, handler=handler, ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/testing/tester/_ui_tester_registry/qt/default_registry.py0000644000175100001730000000410500000000000031460 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! from traitsui.testing.tester.target_registry import TargetRegistry from traitsui.testing.tester._ui_tester_registry.qt._traitsui import ( boolean_editor, button_editor, check_list_editor, directory_editor, editor_factory, enum_editor, file_editor, font_editor, instance_editor, list_editor, range_editor, table_editor, text_editor, ui_base, ) from ._control_widget_registry import get_widget_registry def get_default_registries(): """Creates the default registries for UITester that are qt specific. Returns ------- registries : list of AbstractTargetRegistry The default registries containing implementations for TraitsUI editors that are qt specific. """ registry = TargetRegistry() # BooleanEditor boolean_editor.register(registry) # ButtonEditor button_editor.register(registry) # CheckListEditor check_list_editor.register(registry) # DirectoryEditor directory_editor.register(registry) # EnumEditor enum_editor.register(registry) # FileEditor file_editor.register(registry) # FontEditor font_editor.register(registry) # TextEditor text_editor.register(registry) # ListEditor list_editor.register(registry) # RangeEditor range_editor.register(registry) # ui_base ui_base.register(registry) # InstanceEditor instance_editor.register(registry) # Editor Factory editor_factory.register(registry) # TableEditor table_editor.register(registry) # The more general registry goes after the more specific registry. return [ registry, get_widget_registry(), ] ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1684768028.103807 traitsui-8.0.0/traitsui/testing/tester/_ui_tester_registry/qt/tests/0000755000175100001730000000000000000000000026674 5ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/testing/tester/_ui_tester_registry/qt/tests/__init__.py0000644000175100001730000000000000000000000030773 0ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/testing/tester/_ui_tester_registry/qt/tests/test_control_widget_registry.py0000644000175100001730000000606300000000000035265 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Tests for QWidget's DynamicTargetRegistry """ import unittest from traitsui.testing.api import IsEnabled, IsVisible from traitsui.testing.tester.exceptions import ( LocationNotSupported, ) from traitsui.testing.tester.ui_wrapper import UIWrapper from traitsui.tests._tools import ( is_qt, requires_toolkit, ToolkitName, ) try: from pyface.qt import QtGui except ImportError: if is_qt(): raise else: from traitsui.testing.tester._ui_tester_registry.qt._control_widget_registry import ( # noqa: E501 get_widget_registry, ) class TargetWithControl: """An object holding a control attribute.""" def __init__(self, control): self.control = control @requires_toolkit([ToolkitName.qt]) class TestQtControlWidgetRegistry(unittest.TestCase): """Test the interface of AbstractTargetRegistry for QWidget's registry""" def setUp(self): self.widget = QtGui.QWidget() self.registry = get_widget_registry() self.target = TargetWithControl(self.widget) self.good_wrapper = UIWrapper( target=self.target, registries=[self.registry], ) def test_is_enabled(self): self.assertTrue(self.good_wrapper.inspect(IsEnabled())) def test_is_disabled(self): self.widget.setEnabled(False) self.assertFalse(self.good_wrapper.inspect(IsEnabled())) def test_is_visible(self): self.widget.setVisible(True) self.assertTrue(self.good_wrapper.inspect(IsVisible())) def test_is_invisible(self): self.widget.setVisible(False) self.assertFalse(self.good_wrapper.inspect(IsVisible())) def test_get_interactions_good_target(self): self.assertEqual( self.registry._get_interactions(self.target), set([IsEnabled, IsVisible]), ) def test_get_interactions_bad_target(self): self.assertEqual(self.registry._get_interactions(None), set()) def test_get_interaction_doc(self): self.assertGreater( len(self.registry._get_interaction_doc(self.target, IsEnabled)), 0 ) self.assertGreater( len(self.registry._get_interaction_doc(self.target, IsVisible)), 0 ) def test_get_location_solver(self): # There are currently no solvers with self.assertRaises(LocationNotSupported): self.registry._get_solver(self.target, None) def test__get_locations(self): self.assertEqual(self.registry._get_locations(self.target), set()) def test_error_get_location_doc(self): with self.assertRaises(LocationNotSupported): self.registry._get_location_doc(self.target, None) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/testing/tester/_ui_tester_registry/qt/tests/test_interaction_helpers.py0000644000175100001730000002110300000000000034343 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! import unittest from unittest import mock from traitsui.tests._tools import ( is_qt, requires_toolkit, ToolkitName, ) from traitsui.testing.tester import command from traitsui.testing.tester.exceptions import Disabled from traitsui.testing.tester._ui_tester_registry.qt import ( _interaction_helpers, ) try: from pyface.qt import QtGui except ImportError: if is_qt(): raise @requires_toolkit([ToolkitName.qt]) class TestInteractions(unittest.TestCase): def test_mouse_click(self): button = QtGui.QPushButton() click_slot = mock.Mock() button.clicked.connect(lambda checked: click_slot(checked)) _interaction_helpers.mouse_click_qwidget(button, 0) self.assertEqual(click_slot.call_count, 1) def test_mouse_click_disabled(self): button = QtGui.QPushButton() button.setEnabled(False) click_slot = mock.Mock() # pyside6 can't connect to a mock button.clicked.connect(lambda checked: click_slot(checked)) # when # clicking won't fail, it just does not do anything. # This is consistent with the actual UI. _interaction_helpers.mouse_click_qwidget(button, 0) # then self.assertEqual(click_slot.call_count, 0) def test_mouse_click_combobox_warns(self): combo = None with self.assertWarns(UserWarning): _interaction_helpers.mouse_click_combobox(combo, 0, 0) def test_key_sequence(self): # test on different Qwidget objects textboxes = [QtGui.QLineEdit(), QtGui.QTextEdit()] for i, textbox in enumerate(textboxes): with self.subTest(widget=textbox.__class__.__name__): change_slot = mock.Mock() textbox.textChanged.connect(lambda *args: change_slot(*args)) # when _interaction_helpers.key_sequence_qwidget( textbox, command.KeySequence("abc"), 0 ) # then if i == 0: self.assertEqual(textbox.text(), "abc") else: self.assertEqual(textbox.toPlainText(), "abc") # each keystroke fires a signal self.assertEqual(change_slot.call_count, 3) # for a QLabel, one can try a key sequence and nothing will happen textbox = QtGui.QLabel() _interaction_helpers.key_sequence_qwidget( textbox, command.KeySequence("abc"), 0 ) self.assertEqual(textbox.text(), "") def test_key_sequence_textbox_with_unicode(self): for code in range(32, 127): with self.subTest(code=code, word=chr(code)): textbox = QtGui.QLineEdit() change_slot = mock.Mock() textbox.textChanged.connect(lambda text: change_slot(text)) # when _interaction_helpers.key_sequence_textbox( textbox, command.KeySequence(chr(code) * 3), delay=0, ) # then self.assertEqual(textbox.text(), chr(code) * 3) self.assertEqual(change_slot.call_count, 3) def test_key_sequence_unsupported_key(self): textbox = QtGui.QLineEdit() with self.assertRaises(ValueError) as exception_context: # QTest does not support this character. _interaction_helpers.key_sequence_textbox( textbox, command.KeySequence(chr(31)), delay=0, ) self.assertIn( "is currently not supported.", str(exception_context.exception), ) def test_key_sequence_backspace_character(self): # Qt does convert backspace character to the backspace key # But we disallow it for now to be consistent with wx. textbox = QtGui.QLineEdit() with self.assertRaises(ValueError) as exception_context: _interaction_helpers.key_sequence_textbox( textbox, command.KeySequence("\b"), delay=0, ) self.assertIn( "is currently not supported.", str(exception_context.exception), ) def test_key_sequence_insert_point_qlineedit(self): textbox = QtGui.QLineEdit() textbox.setText("123") # when _interaction_helpers.key_sequence_textbox( textbox, command.KeySequence("abc"), delay=0, ) # then self.assertEqual(textbox.text(), "123abc") def test_key_sequence_insert_point_qtextedit(self): # The default insertion point moved to the end to be consistent # with QLineEdit textbox = QtGui.QTextEdit() textbox.setText("123") # when _interaction_helpers.key_sequence_textbox( textbox, command.KeySequence("abc"), delay=0, ) # then self.assertEqual(textbox.toPlainText(), "123abc") def test_key_sequence_disabled(self): textbox = QtGui.QLineEdit() textbox.setEnabled(False) # this will fail, because one should not be allowed to set # cursor on the widget to type anything with self.assertRaises(Disabled): _interaction_helpers.key_sequence_qwidget( textbox, command.KeySequence("abc"), 0 ) def test_key_click(self): textbox = QtGui.QLineEdit() change_slot = mock.Mock() textbox.editingFinished.connect(lambda: change_slot()) # sanity check on editingFinished signal _interaction_helpers.key_sequence_qwidget( textbox, command.KeySequence("abc"), 0 ) self.assertEqual(change_slot.call_count, 0) _interaction_helpers.key_click_qwidget( textbox, command.KeyClick("Enter"), 0 ) self.assertEqual(change_slot.call_count, 1) # test on a different Qwidget object - QtGui.QTextEdit() textbox = QtGui.QTextEdit() change_slot = mock.Mock() # Now "Enter" should not finish editing, but instead go to next line textbox.textChanged.connect(lambda: change_slot()) _interaction_helpers.key_click_qwidget( textbox, command.KeyClick("Enter"), 0 ) # The textChanged event appears to be fired twice instead of once # on Windows/PySide6, for reasons as yet undetermined. But for our # purposes it's good enough that it's fired at all. # xref: enthought/traitsui#1895 change_slot.assert_called() self.assertEqual(textbox.toPlainText(), "\n") # for a QLabel, one can try a key click and nothing will happen textbox = QtGui.QLabel() _interaction_helpers.key_click_qwidget( textbox, command.KeyClick("A"), 0 ) self.assertEqual(textbox.text(), "") def test_key_click_disabled(self): textbox = QtGui.QLineEdit() textbox.setEnabled(False) change_slot = mock.Mock() textbox.editingFinished.connect(lambda text: change_slot(text)) with self.assertRaises(Disabled): _interaction_helpers.key_click_qwidget( textbox, command.KeyClick("Enter"), 0 ) self.assertEqual(change_slot.call_count, 0) def test_check_q_model_index_valid(self): self.widget = QtGui.QListWidget() self.items = ["a", "b", "c"] self.widget.addItems(self.items) self.good_q_index = self.widget.model().index(1, 0) self.bad_q_index = self.widget.model().index(10, 0) self.model = self.widget.model() _interaction_helpers.check_q_model_index_valid(self.good_q_index) with self.assertRaises(LookupError): _interaction_helpers.check_q_model_index_valid(self.bad_q_index) def test_key_click_q_slider_helpful_err(self): slider = QtGui.QSlider() with self.assertRaises(ValueError) as exc: _interaction_helpers.key_click_qslider( slider, command.KeyClick("Enter"), 0 ) self.assertIn( "['Down', 'Left', 'Page Down', 'Page Up', 'Right', 'Up']", str(exc.exception), ) ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1684768028.103807 traitsui-8.0.0/traitsui/testing/tester/_ui_tester_registry/tests/0000755000175100001730000000000000000000000026250 5ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/testing/tester/_ui_tester_registry/tests/__init__.py0000644000175100001730000000000000000000000030347 0ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/testing/tester/_ui_tester_registry/tests/test_default_registry.py0000644000175100001730000000162700000000000033243 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! import unittest from traitsui.testing.tester._abstract_target_registry import ( AbstractTargetRegistry, ) from traitsui.testing.tester._ui_tester_registry.default_registry import ( get_default_registries, ) class TestDefaultRegistry(unittest.TestCase): def test_load_default_registries(self): registries = get_default_registries() for registry in registries: self.assertIsInstance(registry, AbstractTargetRegistry) self.assertGreaterEqual(len(registries), 1) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/testing/tester/_ui_tester_registry/tests/test_layout.py0000644000175100001730000001012600000000000031176 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! import unittest from traitsui.testing.tester._ui_tester_registry._layout import ( column_major_to_row_major, ) class TestLayout(unittest.TestCase): def test_column_major_index_too_large(self): # Test when the index is too large for the total number of elements # The indices would look like: # 0 2 3 4 # 1 / / / with self.assertRaises(ValueError): column_major_to_row_major( index=10, n=5, num_rows=2, num_cols=4, ) def test_column_major_index_index_overhanging(self): # This is the layout for displaying numbers from 0-9 with column # major setup: # 0 3 6 8 # 1 4 7 9 # 2 5 / / # The index should be populated (row first) in this order: # 0, 3, 6, 8, 1, 4, 7, 9, 2, 5 actual = column_major_to_row_major( index=9, n=10, num_rows=3, num_cols=4, ) self.assertEqual(actual, 7) def test_column_major_index_in_grid_first_row(self): # Test when the index is small enough to be within the upper, filled # grid. # This is the layout for displaying numbers from 0-9 with column # major setup: # 0 3 6 8 # 1 4 7 9 # 2 5 / / # The index should be populated (row first) in this order: # 0, 3, 6, 8, 1, 4, 7, 9, 2, 5 actual = column_major_to_row_major( index=6, n=10, num_rows=3, num_cols=4, ) self.assertEqual(actual, 2) def test_column_major_index_in_grid_last_row(self): # Test when the index is small enough to be within the upper, filled # grid. # This is the layout for displaying numbers from 0-9 with column # major setup: # 0 3 6 8 # 1 4 7 9 # 2 5 / / # The index should be populated (row first) in this order: # 0, 3, 6, 8, 1, 4, 7, 9, 2, 5 actual = column_major_to_row_major( index=4, n=10, num_rows=3, num_cols=4, ) self.assertEqual(actual, 5) def test_column_major_index_last_row(self): # Test when the index is in the last row # This is the layout for displaying numbers from 0-9 with column # major setup: # 0 3 6 8 # 1 4 7 9 # 2 5 / / # The index should be populated (row first) in this order: # 0, 3, 6, 8, 1, 4, 7, 9, 2, 5 actual = column_major_to_row_major( index=2, n=10, num_rows=3, num_cols=4, ) self.assertEqual(actual, 8) def test_column_major_index_long_overhang(self): # This is the layout for displaying numbers from 0-9 with column # major setup: # 0 2 3 4 # 1 / / / # The index should be populated (row first) in this order: # 0, 2, 3, 4, 1 actual = column_major_to_row_major( index=4, n=5, num_rows=2, num_cols=4, ) self.assertEqual(actual, 3) def test_column_major_index_full_grid(self): # This is the layout for displaying numbers from 0-9 with column # major setup: # 0 3 6 9 12 # 1 4 7 10 13 # 2 5 8 11 14 # The index should be populated (row first) in this order: # 0, 3, 6, 9, 12, 1, 4, 7, 10, 13, 2, 5, 8, 11, 14 actual = column_major_to_row_major( index=11, n=15, num_rows=3, num_cols=5, ) self.assertEqual(actual, 13) ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1684768028.103807 traitsui-8.0.0/traitsui/testing/tester/_ui_tester_registry/wx/0000755000175100001730000000000000000000000025544 5ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/testing/tester/_ui_tester_registry/wx/README.txt0000644000175100001730000000115600000000000027245 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ This package contains implementations for testing TraitsUI UI editors with Wx. The top-level module ``default_registry`` serves external packages and hides implementation details internal to this package. """ ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/testing/tester/_ui_tester_registry/wx/__init__.py0000644000175100001730000000115600000000000027660 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ This package contains implementations for testing TraitsUI UI editors with Wx. The top-level module ``default_registry`` serves external packages and hides implementation details internal to this package. """ ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/testing/tester/_ui_tester_registry/wx/_control_widget_registry.py0000644000175100001730000000427200000000000033235 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ This module provides a getter to obtain an AbstractTargetRegistry that can support testing any target whose 'control' attribute refers to a wx.Window. """ import wx from traitsui.testing.tester._dynamic_target_registry import ( DynamicTargetRegistry, ) from traitsui.testing.tester.query import IsEnabled, IsVisible def _handle_is_enabled(wrapper, interaction): """Return true if the target's control is enabled. Parameters ---------- wrapper : UIWrapper Wrapper on which the target's control should be a wx.Window interaction : IsEnabled Not currently used. """ return wrapper._target.control.IsEnabled() def _handle_is_visible(wrapper, interaction): """Return true if the target's control is visible. Parameters ---------- wrapper : UIWrapper Wrapper on which the target's control should be a wx.Window interaction : IsVisible Not currently used. """ return wrapper._target.control.IsShownOnScreen() def _is_target_control_a_wx_window(target): """Return true if the target has a control that is an instance of wx.Window Parameters ---------- target : any Any UI target Returns ------- is_accepted : bool """ return hasattr(target, "control") and isinstance(target.control, wx.Window) def get_widget_registry(): """Return a registry to support any target with an attribute 'control' that is an instance of wx.Window. Returns ------- registry : DynamicTargetRegistry A registry that can be used with UIWrapper """ return DynamicTargetRegistry( can_support=_is_target_control_a_wx_window, interaction_to_handler={ IsEnabled: _handle_is_enabled, IsVisible: _handle_is_visible, }, ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/testing/tester/_ui_tester_registry/wx/_interaction_helpers.py0000644000175100001730000003305100000000000032320 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! import warnings import wx from traitsui.testing.tester._ui_tester_registry._compat import ( check_key_compat, ) from traitsui.testing.tester.exceptions import Disabled def _create_event(event_type, control): """Creates a wxEvent of a given type Parameters ---------- event_type : wxEventType The type of the event to be created control : The wx control the event is occurring on. Returns ------- wxEvent The created event, of the given type, with the given control set as the event object. """ event = wx.CommandEvent(event_type, control.GetId()) event.SetEventObject(control) return event def mouse_click(func): """Decorator function for mouse clicks. Decorated functions will return if they are not enabled. Additionally, this handles the delay for the click. Parameters ---------- func : callable(*, control, delay, **kwargs) The mouse click function to be decorated. Returns ------- callable The decorated function. """ def mouse_click_handler(*, control, delay, **kwargs): """Defines the decorated function. Paramters --------- control : wxControl The wx Object to be clicked. delay : int Time delay (in ms) in which click will be performed. """ if (not control) or (not control.IsEnabled()): warnings.warn( "Attempted to click on a non-existant or non-enabled control. " "Nothing was performed." ) return wx.MilliSleep(delay) func(control=control, delay=delay, **kwargs) return mouse_click_handler @mouse_click def mouse_click_button(control, delay): """Performs a mouse click on a wx button. Parameters ---------- control : wxButton The wx Object to be clicked. delay: int Time delay (in ms) in which click will be performed. """ click_event = _create_event(wx.wxEVT_COMMAND_BUTTON_CLICKED, control) control.ProcessWindowEvent(click_event) @mouse_click def mouse_click_checkbox(control, delay): """Performs a mouse click on a wx check box. Parameters ---------- control : wxCheckBox The wx Object to be clicked. delay: int Time delay (in ms) in which click will be performed. """ click_event = _create_event(wx.wxEVT_COMMAND_CHECKBOX_CLICKED, control) control.SetValue(not control.GetValue()) control.ProcessWindowEvent(click_event) @mouse_click def mouse_click_combobox_or_choice(control, index, delay): """Performs a mouse click on either a wx combo box or a wx choice on the entry at the given index. Parameters ---------- control : wxComboBox or wxChoice The wx Object to be clicked. index : int The index of the item in the combobox/choice to be clicked delay: int Time delay (in ms) in which click will be performed. Raises ------ TypeError If the control is not a wxComboBox or wxChoice. """ if isinstance(control, wx.ComboBox): click_event = _create_event( wx.wxEVT_COMMAND_COMBOBOX_SELECTED, control ) elif isinstance(control, wx.Choice): click_event = _create_event(wx.wxEVT_COMMAND_CHOICE_SELECTED, control) else: raise TypeError("Only supported controls are wxComboBox or wxChoice") click_event.SetString(control.GetString(index)) control.SetSelection(index) control.ProcessWindowEvent(click_event) @mouse_click def mouse_click_listbox(control, index, delay): """Performs a mouse click on a wx list box on the entry at the given index. Parameters ---------- control : wxListBox The wx Object to be clicked. index : int The index of the item in the list box to be clicked delay: int Time delay (in ms) in which click will be performed. """ click_event = _create_event(wx.wxEVT_COMMAND_LISTBOX_SELECTED, control) control.SetSelection(index) control.ProcessWindowEvent(click_event) @mouse_click def mouse_click_radiobutton(control, delay): """Performs a mouse click on a wx radio button. Parameters ---------- control : wxRadioButton The wx Object to be clicked. delay: int Time delay (in ms) in which click will be performed. """ click_event = _create_event(wx.wxEVT_COMMAND_RADIOBUTTON_SELECTED, control) control.SetValue(not control.GetValue()) control.ProcessWindowEvent(click_event) @mouse_click def mouse_click_object(control, delay): """Performs a mouse click on a wxTextCtrl. Parameters ---------- control : wxObject The wx Object to be clicked. delay: int Time delay (in ms) in which click will be performed. """ if not control.HasFocus(): control.SetFocus() click_event = _create_event(wx.wxEVT_COMMAND_LEFT_CLICK, control) control.ProcessWindowEvent(click_event) def mouse_click_notebook_tab_index(control, index, delay): """Performs a mouseclick on a Noteboook List Editor on the tab specified by index. Parameters ---------- control : wx.Window The control of the DockWindow index : int The index of the child object in the Panel to be clicked delay : int Time delay (in ms) in which click will be performed. """ controls_list = control.GetSizer().GetContents().get_controls() wx.MilliSleep(delay) # find the boundaries of the tab to be clicked bx, by, bdx, bdy = controls_list[index].drag_bounds # find the center tab_center = wx.Point(bx + bdx // 2, by + bdy // 2) click_down_event = wx.MouseEvent(wx.wxEVT_LEFT_DOWN) click_down_event.SetPosition(tab_center) click_up_event = wx.MouseEvent(wx.wxEVT_LEFT_UP) click_up_event.SetPosition(tab_center) control.ProcessEvent(click_down_event) control.ProcessEvent(click_up_event) def mouse_click_checkbox_child_in_panel(control, index, delay): """Performs a mouse click on a child of a Wx Panel. Parameters ---------- control : wx.Panel The Panel containing child objects, one of which will be clicked. index : int The index of the child object in the Panel to be clicked delay : int Time delay (in ms) in which click will be performed. Raises ------ IndexError If the index is out of range. """ children_list = control.GetSizer().GetChildren() if not 0 <= index <= len(children_list) - 1: raise IndexError(index) obj = children_list[index].GetWindow() mouse_click_checkbox(control=obj, delay=delay) def mouse_click_radiobutton_child_in_panel(control, index, delay): """Performs a mouse click on a child of a Wx Panel. Parameters ---------- control : wx.Panel The Panel containing child objects, one of which will be clicked. index : int The index of the child object in the Panel to be clicked delay : int Time delay (in ms) in which click will be performed. Raises ------ IndexError If the index is out of range. """ children_list = control.GetSizer().GetChildren() if not 0 <= index <= len(children_list) - 1: raise IndexError(index) obj = children_list[index].GetWindow() mouse_click_radiobutton(control=obj, delay=delay) def key_click_text_entry( control, interaction, delay, get_selection=lambda control: control.GetSelection(), ): """Performs simulated typing of a key on the given wxObject after a delay. Parameters ---------- control : wxTextEntry The wx Object to be acted on. interaction : instance of command.KeyClick The interaction object holding the key input to be simulated being typed delay : int Time delay (in ms) in which the key click will be performed. get_selection: callable(wx.TextEntry) -> tuple(int, int) Callable that takes an instance of wx.TextEntry and return the current selection span. Default is to call `GetSelection` method. Useful for when the TextEntry.GetSelection is overridden by a subclass that does not conform to the common API. """ if not (control.IsEnabled() and control.IsEditable()): raise Disabled("{!r} is disabled.".format(control)) if not control.HasFocus(): control.SetFocus() control.SetInsertionPointEnd() if interaction.key == "Enter": wx.MilliSleep(delay) event = wx.CommandEvent(wx.EVT_TEXT_ENTER.typeId, control.GetId()) control.ProcessEvent(event) elif interaction.key == "End": wx.MilliSleep(delay) control.SetInsertionPointEnd() elif interaction.key == "Backspace": wx.MilliSleep(delay) start, end = get_selection(control) if end > start: control.Remove(start, end) else: pos = control.GetInsertionPoint() control.Remove(max(0, pos - 1), pos) else: check_key_compat(interaction.key) wx.MilliSleep(delay) control.WriteText(interaction.key) def key_click_combobox(control, interaction, delay): """Performs simulated typing of a key on the given wxComboBox after a delay. Parameters ---------- control : wxComboBox The wx Object to be acted on. interaction : instance of command.KeyClick The interaction object holding the key input to be simulated being typed delay : int Time delay (in ms) in which the key click will be performed. """ key_click_text_entry( control, interaction, delay, get_selection=lambda control: control.GetTextSelection(), ) def key_sequence_text_ctrl(control, interaction, delay): """Performs simulated typing of a sequence of keys on the given wxObject after a delay. Parameters ---------- control : wxTextCtrl The wx Object to be acted on. interaction : instance of command.KeySequence The interaction object holding the sequence of key inputs to be simulated being typed delay : int Time delay (in ms) in which each key click in the sequence will be performed. Raises ------ Disabled If the control is either not enabled or not editable. """ # fail early for char in interaction.sequence: check_key_compat(char) if not (control.IsEnabled() and control.IsEditable()): raise Disabled("{!r} is disabled.".format(control)) if not control.HasFocus(): control.SetFocus() control.SetInsertionPointEnd() for char in interaction.sequence: wx.MilliSleep(delay) control.WriteText(char) wx.SafeYield() def key_click_slider(control, interaction, delay): """Performs simulated typing of a key on the given wxSlider after a delay. Only allowed keys are: "Left", "Right", "Up", "Down", "Page Up", "Page Down" Also, note that up related keys correspond to an increment on the slider, and down a decrement. Parameters ---------- control : wxSlider The wx Object to be acted on. interaction : instance of command.KeyClick The interaction object holding the key input to be simulated being typed delay : int Time delay (in ms) in which the key click will be performed. Raises ------ ValueError If the interaction.key is not one of the valid keys. """ valid_keys = {"Left", "Right", "Up", "Down", "Page Up", "Page Down"} if interaction.key not in valid_keys: raise ValueError( "Unexpected Key. Supported keys are: {}".format(sorted(valid_keys)) ) if not control.HasFocus(): control.SetFocus() value = control.GetValue() if interaction.key in {"Up", "Right"}: position = min(control.GetMax(), value + control.GetLineSize()) elif interaction.key == "Page Up": position = min(control.GetMax(), value + control.GetPageSize()) elif interaction.key == "Page Down": position = max(control.GetMin(), value - control.GetPageSize()) elif interaction.key in {"Down", "Left"}: position = max(control.GetMin(), value - control.GetLineSize()) else: raise ValueError( "Unexpected Key. Supported keys are: {}".format(sorted(valid_keys)) ) wx.MilliSleep(delay) control.SetValue(position) event = wx.ScrollEvent(wx.wxEVT_SCROLL_CHANGED, control.GetId(), position) wx.PostEvent(control, event) def readonly_textbox_displayed_text(control): '''Extracts the displayed text in a wx textbox (either a wx.TextCtrl or wx.StaticText). Parameters ---------- control : wx.TextCtrl or wx.StaticText the textbox object from which the text of interest is displayed Raises ------ TypeError If the control is not either a wx.TextCtrl or wx.StaticText. ''' if isinstance(control, wx.TextCtrl): return control.GetValue() elif isinstance(control, wx.StaticText): return control.GetLabel() raise TypeError( "readonly_textbox_displayed_text expected a control" " of either wx.TextCtrl, or wx.StaticText." " {} was found".format(control) ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/testing/tester/_ui_tester_registry/wx/_registry_helper.py0000644000175100001730000000442000000000000031464 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ This module provides functions for registering interaction handlers and location solvers for common Wx GUI components. """ from traitsui.testing.tester.command import KeyClick, KeySequence, MouseClick from traitsui.testing.tester.query import DisplayedText from traitsui.testing.tester._ui_tester_registry.wx import _interaction_helpers def register_editable_textbox_handlers(registry, target_class, widget_getter): """Register common interactions for an editable textbox (in Wx) Parameters ---------- registry : TargetRegistry The registry being registered to. target_class : subclass of type The type of target being wrapped in a UIWrapper on which the interaction will be performed. widget_getter : callable(wrapper: UIWrapper) -> wx.TextCtrl A callable to return a wx.TextCtrl """ handlers = [ ( KeySequence, ( lambda wrapper, interaction: _interaction_helpers.key_sequence_text_ctrl( widget_getter(wrapper), interaction, wrapper.delay ) ), ), ( KeyClick, ( lambda wrapper, interaction: _interaction_helpers.key_click_text_entry( widget_getter(wrapper), interaction, wrapper.delay ) ), ), ( MouseClick, ( lambda wrapper, _: _interaction_helpers.mouse_click_object( control=widget_getter(wrapper), delay=wrapper.delay ) ), ), (DisplayedText, lambda wrapper, _: widget_getter(wrapper).GetValue()), ] for interaction_class, handler in handlers: registry.register_interaction( target_class=target_class, interaction_class=interaction_class, handler=handler, ) ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1684768028.107807 traitsui-8.0.0/traitsui/testing/tester/_ui_tester_registry/wx/_traitsui/0000755000175100001730000000000000000000000027547 5ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/testing/tester/_ui_tester_registry/wx/_traitsui/README.txt0000644000175100001730000000130300000000000031242 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ The organization in this package should more or less mirror that of the ``traitsui.wx`` package and contains the corresponding logic for testing. Note that once new implementations are added, they may need to be exposed via the ``default_registry`` module (one level up from this package). """ ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/testing/tester/_ui_tester_registry/wx/_traitsui/__init__.py0000644000175100001730000000130300000000000031655 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ The organization in this package should more or less mirror that of the ``traitsui.wx`` package and contains the corresponding logic for testing. Note that once new implementations are added, they may need to be exposed via the ``default_registry`` module (one level up from this package). """ ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/testing/tester/_ui_tester_registry/wx/_traitsui/boolean_editor.py0000644000175100001730000000320400000000000033105 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! from traitsui.wx.boolean_editor import ReadonlyEditor, SimpleEditor from traitsui.testing.tester.command import MouseClick from traitsui.testing.tester.query import DisplayedText, IsChecked from traitsui.testing.tester._ui_tester_registry.wx import _interaction_helpers def register(registry): """Register solvers/handlers specific to wx Boolean Editors for the given registry. If there are any conflicts, an error will occur. Parameters ---------- registry : TargetRegistry """ registry.register_interaction( target_class=SimpleEditor, interaction_class=MouseClick, handler=( lambda wrapper, _: _interaction_helpers.mouse_click_checkbox( control=wrapper._target.control, delay=wrapper.delay ) ), ) registry.register_interaction( target_class=SimpleEditor, interaction_class=IsChecked, handler=lambda wrapper, _: wrapper._target.control.GetValue(), ) registry.register_interaction( target_class=ReadonlyEditor, interaction_class=DisplayedText, handler=lambda wrapper, _: _interaction_helpers.readonly_textbox_displayed_text( control=wrapper._target.control ), ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/testing/tester/_ui_tester_registry/wx/_traitsui/button_editor.py0000644000175100001730000000511400000000000033003 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! import wx from traitsui.wx.button_editor import SimpleEditor, CustomEditor from traitsui.testing.tester.command import MouseClick from traitsui.testing.tester.query import DisplayedText from traitsui.testing.tester._ui_tester_registry.wx import _interaction_helpers def mouse_click_ImageButton(wrapper, interaction): """Performs a mouse click on an pyface.ui.wx.ImageButton object. Parameters ---------- wrapper : UIWrapper The wrapper object wrapping the Custom Button Editor which utilizes an ImageButton. interaction : instance of traitsui.testing.tester.command.MouseClick interaction is unused here, but it is included so that the function matches the expected format for a handler. Note this handler is intended to be used with an interaction_class of a MouseClick. """ control = wrapper._target.control if not control.IsEnabled(): return wx.MilliSleep(wrapper.delay) left_down_event = wx.MouseEvent(wx.wxEVT_LEFT_DOWN) left_up_event = wx.MouseEvent(wx.wxEVT_LEFT_UP) control.ProcessEvent(left_down_event) control.ProcessEvent(left_up_event) def register(registry): """Register solvers/handlers specific to wx Button Editors for the given registry. If there are any conflicts, an error will occur. Parameters ---------- registry : TargetRegistry """ registry.register_interaction( target_class=SimpleEditor, interaction_class=MouseClick, handler=( lambda wrapper, _: _interaction_helpers.mouse_click_button( control=wrapper._target.control, delay=wrapper.delay ) ), ) registry.register_interaction( target_class=SimpleEditor, interaction_class=DisplayedText, handler=lambda wrapper, _: wrapper._target.control.GetLabel(), ) registry.register_interaction( target_class=CustomEditor, interaction_class=MouseClick, handler=mouse_click_ImageButton, ) registry.register_interaction( target_class=CustomEditor, interaction_class=DisplayedText, handler=lambda wrapper, _: wrapper._target.control.GetLabel(), ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/testing/tester/_ui_tester_registry/wx/_traitsui/check_list_editor.py0000644000175100001730000000544400000000000033606 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! import wx from traitsui.wx.check_list_editor import CustomEditor from traitsui.testing.tester.command import MouseClick from traitsui.testing.tester.locator import Index from traitsui.testing.tester._ui_tester_registry._common_ui_targets import ( BaseSourceWithLocation, ) from traitsui.testing.tester._ui_tester_registry._layout import ( column_major_to_row_major, ) from traitsui.testing.tester._ui_tester_registry.wx import _interaction_helpers class _IndexedCustomCheckListEditor(BaseSourceWithLocation): """Wrapper for CheckListEditor + Index""" source_class = CustomEditor locator_class = Index handlers = [ ( MouseClick, ( lambda wrapper, _: _interaction_helpers.mouse_click_checkbox_child_in_panel( control=wrapper._target.source.control, index=convert_index( source=wrapper._target.source, index=wrapper._target.location.index, ), delay=wrapper.delay, ) ), ), ] def convert_index(source, index): """Helper function to convert an index for a GridSizer so that the index counts over the grid in the correct direction. The grid is always populated in row major order, however, the elements are assigned to each entry in the grid so that when displayed they appear in column major order. Sizers are indexed in the order they are populated, so to access the correct element we may need to convert a column-major based index into a row-major one. Parameters ---------- control : CustomEditor The Custom CheckList Editor of interest. Its control is the wx.Panel containing child objects organized with a wx.GridSizer index : int the index of interest """ sizer = source.control.GetSizer() if isinstance(sizer, wx.BoxSizer): return index n = len(source.names) num_cols = sizer.GetCols() num_rows = sizer.GetEffectiveRowsCount() return column_major_to_row_major(index, n, num_rows, num_cols) def register(registry): """Register interactions for the given registry. If there are any conflicts, an error will occur. Parameters ---------- registry : TargetRegistry The registry being registered to. """ _IndexedCustomCheckListEditor.register(registry) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/testing/tester/_ui_tester_registry/wx/_traitsui/directory_editor.py0000644000175100001730000000175000000000000033476 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! from traitsui.wx.directory_editor import SimpleEditor from traitsui.testing.tester._ui_tester_registry.wx._registry_helper import ( register_editable_textbox_handlers, ) def register(registry): """Register interactions for the given registry. If there are any conflicts, an error will occur. Parameters ---------- registry : TargetRegistry The registry being registered to. """ register_editable_textbox_handlers( registry=registry, target_class=SimpleEditor, widget_getter=lambda wrapper: wrapper._target._file_name, ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/testing/tester/_ui_tester_registry/wx/_traitsui/editor_factory.py0000644000175100001730000000256600000000000033147 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! from traitsui.wx.editor_factory import ReadonlyEditor, TextEditor from traitsui.testing.tester.query import DisplayedText from traitsui.testing.tester._ui_tester_registry.wx import _interaction_helpers from traitsui.testing.tester._ui_tester_registry.wx._registry_helper import ( register_editable_textbox_handlers, ) def register(registry): """Register interactions for the given registry. If there are any conflicts, an error will occur. Parameters ---------- registry : TargetRegistry The registry being registered to. """ register_editable_textbox_handlers( registry=registry, target_class=TextEditor, widget_getter=lambda wrapper: wrapper._target.control, ) registry.register_interaction( target_class=ReadonlyEditor, interaction_class=DisplayedText, handler=lambda wrapper, _: _interaction_helpers.readonly_textbox_displayed_text( wrapper._target.control ), ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/testing/tester/_ui_tester_registry/wx/_traitsui/enum_editor.py0000644000175100001730000001534000000000000032436 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! import wx from traitsui.wx.enum_editor import ( ListEditor, RadioEditor, SimpleEditor, ) from traitsui.testing.tester.command import ( KeyClick, KeySequence, MouseClick, ) from traitsui.testing.tester.locator import Index from traitsui.testing.tester.query import DisplayedText, SelectedText from traitsui.testing.tester._ui_tester_registry._common_ui_targets import ( BaseSourceWithLocation, ) from traitsui.testing.tester._ui_tester_registry.wx import _interaction_helpers from traitsui.testing.tester._ui_tester_registry._layout import ( column_major_to_row_major, ) class _IndexedListEditor(BaseSourceWithLocation): """Wrapper class for EnumListEditor and Index.""" source_class = ListEditor locator_class = Index handlers = [ ( MouseClick, ( lambda wrapper, _: _interaction_helpers.mouse_click_listbox( control=wrapper._target.source.control, index=wrapper._target.location.index, delay=wrapper.delay, ) ), ), ] class _IndexedRadioEditor(BaseSourceWithLocation): """Wrapper class for EnumRadioEditor and Index.""" source_class = RadioEditor locator_class = Index handlers = [ ( MouseClick, ( lambda wrapper, _: _interaction_helpers.mouse_click_radiobutton_child_in_panel( control=wrapper._target.source.control, index=convert_index( source=wrapper._target.source, index=wrapper._target.location.index, ), delay=wrapper.delay, ) ), ), ] def convert_index(source, index): """Helper function to convert an index for a GridSizer so that the index counts over the grid in the correct direction. The grid is always populated in row major order, however, the elements are assigned to each entry in the grid so that when displayed they appear in column major order. Sizers are indexed in the order they are populated, so to access the correct element we may need to convert a column-major based index into a row-major one. Parameters ---------- control : RadioEditor The RadioEditor of interest. Its control is the wx.Panel containing child objects organized with a wx.GridSizer index : int the index of interest """ sizer = source.control.GetSizer() if isinstance(sizer, wx.BoxSizer): return index n = len(source.names) num_cols = sizer.GetCols() num_rows = sizer.GetEffectiveRowsCount() return column_major_to_row_major(index, n, num_rows, num_cols) class _IndexedSimpleEditor(BaseSourceWithLocation): """Wrapper class for Simple EnumEditor and Index.""" source_class = SimpleEditor locator_class = Index handlers = [ ( MouseClick, ( lambda wrapper, _: _interaction_helpers.mouse_click_combobox_or_choice( control=wrapper._target.source.control, index=wrapper._target.location.index, delay=wrapper.delay, ) ), ), ] def simple_displayed_selected_text_handler(wrapper, interaction): """Handler function used to query DisplayedText for Simple Enum Editor. Note that depending on the factories evaluaute trait, the control for a Simple Enum Editor can either be a wx.ComboBox or a wx.Choice. Parameters ---------- wrapper : UIWrapper The UIWrapper containing that object with text to be displayed. interaction : DisplayedText Unused in this function but included to match the expected format of a handler. Should only be DisplayedText """ control = wrapper._target.control if isinstance(control, wx.ComboBox): return control.GetValue() else: # wx.Choice return control.GetString(control.GetSelection()) def radio_selected_text_handler(wrapper, interaction): """Handler function used to query SelectedText for EnumRadioEditor. Parameters ---------- wrapper : UIWrapper The UIWrapper containing that object with text that is selected. interaction : SelectedText Unused in this function but included to match the expected format of a handler. Should only be SelectedText """ children_list = wrapper._target.control.GetSizer().GetChildren() for child in children_list: if child.GetWindow().GetValue(): return child.GetWindow().GetLabel() return None def register(registry): """Registry location and interaction handlers for EnumEditor. Parameters ---------- registry : InteractionRegistry """ _IndexedListEditor.register(registry) _IndexedRadioEditor.register(registry) _IndexedSimpleEditor.register(registry) simple_editor_text_handlers = [ ( KeyClick, ( lambda wrapper, interaction: _interaction_helpers.key_click_combobox( control=wrapper._target.control, interaction=interaction, delay=wrapper.delay, ) ), ), ( KeySequence, ( lambda wrapper, interaction: _interaction_helpers.key_sequence_text_ctrl( control=wrapper._target.control, interaction=interaction, delay=wrapper.delay, ) ), ), (DisplayedText, simple_displayed_selected_text_handler), (SelectedText, simple_displayed_selected_text_handler), ] for interaction_class, handler in simple_editor_text_handlers: registry.register_interaction( target_class=SimpleEditor, interaction_class=interaction_class, handler=handler, ) registry.register_interaction( target_class=RadioEditor, interaction_class=SelectedText, handler=radio_selected_text_handler, ) registry.register_interaction( target_class=ListEditor, interaction_class=SelectedText, handler=lambda wrapper, _: wrapper._target.control.GetString( wrapper._target.control.GetSelection() ), ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/testing/tester/_ui_tester_registry/wx/_traitsui/file_editor.py0000644000175100001730000000174300000000000032413 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! from traitsui.wx.file_editor import SimpleEditor from traitsui.testing.tester._ui_tester_registry.wx._registry_helper import ( register_editable_textbox_handlers, ) def register(registry): """Register interactions for the given registry. If there are any conflicts, an error will occur. Parameters ---------- registry : TargetRegistry The registry being registered to. """ register_editable_textbox_handlers( registry=registry, target_class=SimpleEditor, widget_getter=lambda wrapper: wrapper._target._file_name, ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/testing/tester/_ui_tester_registry/wx/_traitsui/font_editor.py0000644000175100001730000000172000000000000032435 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! from traitsui.testing.tester._ui_tester_registry.wx._registry_helper import ( register_editable_textbox_handlers, ) from traitsui.wx.font_editor import TextFontEditor def register(registry): """Register interactions pertaining to (wx) FontEditor for the given registry. Parameters ---------- registry : TargetRegistry The registry being registered to. """ register_editable_textbox_handlers( registry=registry, target_class=TextFontEditor, widget_getter=lambda wrapper: wrapper._target.control, ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/testing/tester/_ui_tester_registry/wx/_traitsui/instance_editor.py0000644000175100001730000000611300000000000033274 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! from traitsui.testing.tester.command import MouseClick from traitsui.testing.tester.locator import Index from traitsui.testing.tester.query import SelectedText from traitsui.testing.tester._ui_tester_registry._common_ui_targets import ( BaseSourceWithLocation, ) from traitsui.testing.tester._ui_tester_registry._traitsui_ui import ( register_traitsui_ui_solvers, ) from traitsui.testing.tester._ui_tester_registry.wx._interaction_helpers import ( # noqa mouse_click_combobox_or_choice, mouse_click_button, ) from traitsui.wx.instance_editor import CustomEditor, SimpleEditor def _get_nested_ui_simple(target): """Obtains a nested UI within a Simple Instance Editor. Parameters ---------- target : instance of SimpleEditor """ return target._dialog_ui def _get_nested_ui_custom(target): """Obtains a nested UI within a Custom Instance Editor. Parameters ---------- target : instance of CustomEditor """ return target._ui def _get_choice(target): """Obtains a nested choice within an Instance Editor. Parameters ---------- target : instance of CustomEditor """ return target._choice def _click_choice_index(wrapper, _): """Perform a click on a choice based on the index.""" return mouse_click_combobox_or_choice( control=_get_choice(wrapper._target.source), index=wrapper._target.location.index, delay=wrapper.delay, ) def _get_choice_text(wrapper, _): """Get the currently displayed text of a choice.""" control = _get_choice(wrapper._target) return control.GetString(control.GetSelection()) class _IndexedCustomEditor(BaseSourceWithLocation): """Wrapper class for CustomEditors with a selection.""" source_class = CustomEditor locator_class = Index handlers = [ (MouseClick, _click_choice_index), ] def register(registry): """Register interactions for the given registry. If there are any conflicts, an error will occur. Parameters ---------- registry : TargetRegistry The registry being registered to. """ _IndexedCustomEditor.register(registry) registry.register_interaction( target_class=SimpleEditor, interaction_class=MouseClick, handler=lambda wrapper, _: mouse_click_button( control=wrapper._target._button, delay=wrapper.delay, ), ) register_traitsui_ui_solvers(registry, SimpleEditor, _get_nested_ui_simple) registry.register_interaction( target_class=CustomEditor, interaction_class=SelectedText, handler=_get_choice_text, ) register_traitsui_ui_solvers(registry, CustomEditor, _get_nested_ui_custom) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/testing/tester/_ui_tester_registry/wx/_traitsui/list_editor.py0000644000175100001730000000741600000000000032452 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! from traitsui.testing.tester.command import MouseClick from traitsui.testing.tester.locator import Index from traitsui.testing.tester._ui_tester_registry._common_ui_targets import ( BaseSourceWithLocation, ) from traitsui.testing.tester._ui_tester_registry._traitsui_ui import ( register_traitsui_ui_solvers, ) from traitsui.testing.tester._ui_tester_registry.wx import _interaction_helpers from traitsui.wx.list_editor import CustomEditor, NotebookEditor, SimpleEditor class _IndexedNotebookEditor(BaseSourceWithLocation): """Wrapper for a ListEditor (Notebook) with an index.""" source_class = NotebookEditor locator_class = Index handlers = [ ( MouseClick, ( lambda wrapper, _: _interaction_helpers.mouse_click_notebook_tab_index( control=wrapper._target.source.control, index=wrapper._target.location.index, delay=wrapper.delay, ) ), ), ] @classmethod def register(cls, registry): """Class method to register interactions on a _IndexedNotebookEditor for the given registry. If there are any conflicts, an error will occur. Parameters ---------- registry : TargetRegistry The registry being registered to. """ super().register(registry) register_traitsui_ui_solvers( registry=registry, target_class=cls, traitsui_ui_getter=lambda target: target._get_nested_ui(), ) def _get_nested_ui(self): """Method to get the nested ui corresponding to the List element at the given index. """ return self.source._uis[self.location.index][0].dockable.ui def _get_next_target(list_editor, index): """Gets the target at a given index from a Custom List Editor. Parameters ---------- list_editor : CustomEditor The custom style list editor in which the target is contained. index : int the index of the target of interest in the list Returns ------- traitsui.editor.Editor The obtained target. """ # each list item gets a corresponding ImageControl item (allows one to # add items to the list before, after, delete, etc.) along with the # item itself. Thus, index is actually an index over the odd elements # of the list of children corresponding to items in the list we would # want to interact with new_index = 2 * index + 1 WindowList = list_editor.control.GetChildren() item = WindowList[new_index] return item._editor def register(registry): """Register interactions for the given registry. If there are any conflicts, an error will occur. Parameters ---------- registry : TargetRegistry The registry being registered to. """ # NotebookEditor _IndexedNotebookEditor.register(registry) # CustomEditor registry.register_location( target_class=CustomEditor, locator_class=Index, solver=lambda wrapper, location: ( _get_next_target(wrapper._target, location.index) ), ) # SimpleEditor registry.register_location( target_class=SimpleEditor, locator_class=Index, solver=lambda wrapper, location: ( _get_next_target(wrapper._target, location.index) ), ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/testing/tester/_ui_tester_registry/wx/_traitsui/range_editor.py0000644000175100001730000000672000000000000032570 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! from traitsui.wx.range_editor import ( LargeRangeSliderEditor, LogRangeSliderEditor, RangeTextEditor, SimpleSliderEditor, ) from traitsui.testing.tester.command import KeyClick from traitsui.testing.tester.locator import Slider, Textbox from traitsui.testing.tester._ui_tester_registry.wx import ( _interaction_helpers, _registry_helper, ) class LocatedTextbox: """Wrapper class for a located Textbox in Wx. Parameters ---------- textbox : Instance of wx.TextCtrl """ def __init__(self, textbox): self.textbox = textbox @classmethod def register(cls, registry): """Class method to register interactions on a LocatedTextbox for the given registry. If there are any conflicts, an error will occur. Parameters ---------- registry : TargetRegistry The registry being registered to. """ _registry_helper.register_editable_textbox_handlers( registry=registry, target_class=cls, widget_getter=lambda wrapper: wrapper._target.textbox, ) class LocatedSlider: """Wrapper class for a located Textbox in Wx. Parameters ---------- slider : Instance of traitsui.wx.helper.Slider (wx.Slider) """ def __init__(self, slider): self.slider = slider @classmethod def register(cls, registry): """Class method to register interactions on a LocatedSlider for the given registry. If there are any conflicts, an error will occur. Parameters ---------- registry : TargetRegistry The registry being registered to. """ registry.register_interaction( target_class=cls, interaction_class=KeyClick, handler=lambda wrapper, interaction: _interaction_helpers.key_click_slider( wrapper._target.slider, interaction, wrapper.delay ), ) def register(registry): """Register interactions for the given registry. If there are any conflicts, an error will occur. Parameters ---------- registry : TargetRegistry The registry being registered to. """ targets = [ SimpleSliderEditor, LogRangeSliderEditor, LargeRangeSliderEditor, ] for target_class in targets: registry.register_location( target_class=target_class, locator_class=Textbox, solver=lambda wrapper, _: LocatedTextbox( textbox=wrapper._target.control.text ), ) registry.register_location( target_class=target_class, locator_class=Slider, solver=lambda wrapper, _: LocatedSlider( slider=wrapper._target.control.slider ), ) _registry_helper.register_editable_textbox_handlers( registry=registry, target_class=RangeTextEditor, widget_getter=lambda wrapper: wrapper._target.control, ) LocatedTextbox.register(registry) LocatedSlider.register(registry) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/testing/tester/_ui_tester_registry/wx/_traitsui/text_editor.py0000644000175100001730000000272000000000000032454 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! from traitsui.wx.text_editor import CustomEditor, ReadonlyEditor, SimpleEditor from traitsui.testing.tester.query import DisplayedText from traitsui.testing.tester._ui_tester_registry.wx import _interaction_helpers from traitsui.testing.tester._ui_tester_registry.wx._registry_helper import ( register_editable_textbox_handlers, ) def register(registry): """Register interactions for the given registry. If there are any conflicts, an error will occur. Parameters ---------- registry : TargetRegistry The registry being registered to. """ for target_class in [CustomEditor, SimpleEditor]: register_editable_textbox_handlers( registry=registry, target_class=target_class, widget_getter=lambda wrapper: wrapper._target.control, ) registry.register_interaction( target_class=ReadonlyEditor, interaction_class=DisplayedText, handler=lambda wrapper, _: _interaction_helpers.readonly_textbox_displayed_text( wrapper._target.control ), ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/testing/tester/_ui_tester_registry/wx/_traitsui/ui_base.py0000644000175100001730000000253400000000000031534 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! from traitsui.wx.ui_base import ButtonEditor from traitsui.testing.tester.command import MouseClick from traitsui.testing.tester.query import DisplayedText from traitsui.testing.tester._ui_tester_registry.wx import _interaction_helpers def register(registry): """Register solvers/handlers specific to wx Button Editors for the given registry. If there are any conflicts, an error will occur. Parameters ---------- registry : TargetRegistry """ registry.register_interaction( target_class=ButtonEditor, interaction_class=MouseClick, handler=( lambda wrapper, _: _interaction_helpers.mouse_click_button( control=wrapper._target.control, delay=wrapper.delay ) ), ) registry.register_interaction( target_class=ButtonEditor, interaction_class=DisplayedText, handler=lambda wrapper, _: wrapper._target.control.GetLabel(), ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/testing/tester/_ui_tester_registry/wx/default_registry.py0000644000175100001730000000376000000000000031500 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! from traitsui.testing.tester.target_registry import TargetRegistry from traitsui.testing.tester._ui_tester_registry.wx._traitsui import ( boolean_editor, button_editor, check_list_editor, directory_editor, editor_factory, enum_editor, file_editor, font_editor, instance_editor, list_editor, range_editor, text_editor, ui_base, ) from ._control_widget_registry import get_widget_registry def get_default_registries(): """Creates the default registries for UITester that are wx specific. Returns ------- registries : list of AbstractTargetRegistry The default registries containing implementations for TraitsUI editors that are wx specific. """ registry = TargetRegistry() # BooleanEditor boolean_editor.register(registry) # ButtonEditor button_editor.register(registry) # CheckListEditor check_list_editor.register(registry) # DirectoryEditor directory_editor.register(registry) # EnumEditor enum_editor.register(registry) # FileEditor file_editor.register(registry) # FontEditor font_editor.register(registry) # TextEditor text_editor.register(registry) # ListEditor list_editor.register(registry) # RangeEditor range_editor.register(registry) # ui_base ui_base.register(registry) # InstanceEditor instance_editor.register(registry) # Editor Factory editor_factory.register(registry) # More general registry follows more specific registry return [ registry, get_widget_registry(), ] ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1684768028.107807 traitsui-8.0.0/traitsui/testing/tester/_ui_tester_registry/wx/tests/0000755000175100001730000000000000000000000026706 5ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/testing/tester/_ui_tester_registry/wx/tests/__init__.py0000644000175100001730000000000000000000000031005 0ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/testing/tester/_ui_tester_registry/wx/tests/test_control_widget_registry.py0000644000175100001730000000552700000000000035303 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Tests for wx.Window's DynamicTargetRegistry """ import unittest from traitsui.testing.api import IsEnabled, IsVisible from traitsui.testing.tester.exceptions import ( LocationNotSupported, ) from traitsui.testing.tester.ui_wrapper import UIWrapper from traitsui.tests._tools import ( is_wx, requires_toolkit, ToolkitName, ) try: import wx except ImportError: if is_wx(): raise else: from traitsui.testing.tester._ui_tester_registry.wx._control_widget_registry import ( # noqa: E501 get_widget_registry, ) class TargetWithControl: """An object holding a control attribute.""" def __init__(self, control): self.control = control @requires_toolkit([ToolkitName.wx]) class TestWxControlWidgetRegistry(unittest.TestCase): def setUp(self): self.widget = wx.Window() self.registry = get_widget_registry() self.target = TargetWithControl(self.widget) self.good_wrapper = UIWrapper( target=self.target, registries=[self.registry], ) def test_is_enabled(self): self.widget.Enable(True) self.assertTrue(self.good_wrapper.inspect(IsEnabled())) def test_is_visible(self): self.widget.Show(True) self.assertTrue(self.good_wrapper.inspect(IsVisible())) def test_is_invisible(self): self.widget.Hide() self.assertFalse(self.good_wrapper.inspect(IsVisible())) def test_get_interactions_good_target(self): self.assertEqual( self.registry._get_interactions(self.target), set([IsEnabled, IsVisible]), ) def test_get_interactions_bad_target(self): self.assertEqual(self.registry._get_interactions(None), set()) def test_get_interaction_doc(self): self.assertGreater( len(self.registry._get_interaction_doc(self.target, IsEnabled)), 0 ) self.assertGreater( len(self.registry._get_interaction_doc(self.target, IsVisible)), 0 ) def test_get_location_solver(self): # There are currently no solvers with self.assertRaises(LocationNotSupported): self.registry._get_solver(self.target, None) def test_get_locations(self): self.assertEqual(self.registry._get_locations(self.target), set()) def test_error_get_location_doc(self): with self.assertRaises(LocationNotSupported): self.registry._get_location_doc(self.target, None) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/testing/tester/_ui_tester_registry/wx/tests/test_interaction_helpers.py0000644000175100001730000001411300000000000034360 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! import unittest from unittest import mock from traitsui.testing.tester import command from traitsui.testing.tester.exceptions import Disabled from traitsui.testing.tester._ui_tester_registry.wx import _interaction_helpers from traitsui.tests._tools import ( is_wx, requires_toolkit, ToolkitName, ) try: import wx except ImportError: if is_wx(): raise @requires_toolkit([ToolkitName.wx]) class TestInteractions(unittest.TestCase): def setUp(self): self.frame = wx.Frame(None) self.frame.Show() def tearDown(self): self.frame.Close() self.frame.Destroy() def test_mouse_click(self): handler = mock.Mock() button = wx.Button(self.frame) button.Bind(wx.EVT_BUTTON, handler) # when _interaction_helpers.mouse_click_button(control=button, delay=0) # then self.assertEqual(handler.call_count, 1) def test_mouse_click_disabled_button(self): handler = mock.Mock() button = wx.Button(self.frame) button.Bind(wx.EVT_BUTTON, handler) button.Enable(False) # when _interaction_helpers.mouse_click_button(control=button, delay=0) # then self.assertEqual(handler.call_count, 0) def test_mouse_click_None_warns(self): control = None with self.assertWarns(UserWarning): _interaction_helpers.mouse_click_button(control=control, delay=0) def test_key_sequence(self): # The insertion point is moved to the end textbox = wx.TextCtrl(self.frame) textbox.SetValue("123") handler = mock.Mock() textbox.Bind(wx.EVT_TEXT, handler) _interaction_helpers.key_sequence_text_ctrl( textbox, command.KeySequence("abc"), 0 ) self.assertEqual(textbox.GetValue(), "123abc") self.assertEqual(handler.call_count, 3) def test_key_sequence_with_unicode(self): handler = mock.Mock() textbox = wx.TextCtrl(self.frame) textbox.Bind(wx.EVT_TEXT, handler) # This range is supported by Qt for code in range(32, 127): with self.subTest(code=code, word=chr(code)): textbox.Clear() handler.reset_mock() # when _interaction_helpers.key_sequence_text_ctrl( textbox, command.KeySequence(chr(code) * 3), delay=0, ) # then self.assertEqual(textbox.Value, chr(code) * 3) self.assertEqual(handler.call_count, 3) def test_key_sequence_with_backspace_unsupported(self): textbox = wx.TextCtrl(self.frame) with self.assertRaises(ValueError) as exception_context: _interaction_helpers.key_sequence_text_ctrl( textbox, command.KeySequence("\b"), 0 ) self.assertIn( "is currently not supported.", str(exception_context.exception), ) def test_key_sequence_disabled(self): textbox = wx.TextCtrl(self.frame) textbox.SetEditable(False) with self.assertRaises(Disabled): _interaction_helpers.key_sequence_text_ctrl( textbox, command.KeySequence("abc"), 0 ) def test_key_click(self): textbox = wx.TextCtrl(self.frame) handler = mock.Mock() textbox.Bind(wx.EVT_TEXT, handler) _interaction_helpers.key_click_text_entry( textbox, command.KeyClick("A"), 0 ) self.assertEqual(textbox.Value, "A") self.assertEqual(handler.call_count, 1) def test_key_click_backspace(self): textbox = wx.TextCtrl(self.frame) textbox.SetValue("A") handler = mock.Mock() textbox.Bind(wx.EVT_TEXT, handler) _interaction_helpers.key_click_text_entry( textbox, command.KeyClick("Backspace"), 0 ) self.assertEqual(textbox.Value, "") self.assertEqual(handler.call_count, 1) def test_key_click_backspace_with_selection(self): textbox = wx.TextCtrl(self.frame) textbox.SetFocus() textbox.SetValue("ABCDE") textbox.SetSelection(0, 4) # sanity check self.assertEqual(textbox.GetStringSelection(), "ABCD") handler = mock.Mock() textbox.Bind(wx.EVT_TEXT, handler) _interaction_helpers.key_click_text_entry( textbox, command.KeyClick("Backspace"), 0 ) self.assertEqual(textbox.Value, "E") self.assertEqual(handler.call_count, 1) def test_key_click_end(self): textbox = wx.TextCtrl(self.frame) textbox.SetValue("ABCDE") textbox.SetInsertionPoint(0) # sanity check self.assertEqual(textbox.GetInsertionPoint(), 0) _interaction_helpers.key_click_text_entry( textbox, command.KeyClick("End"), 0 ) _interaction_helpers.key_click_text_entry( textbox, command.KeyClick("F"), 0 ) self.assertEqual(textbox.Value, "ABCDEF") def test_key_click_disabled(self): textbox = wx.TextCtrl(self.frame) textbox.SetEditable(False) with self.assertRaises(Disabled): _interaction_helpers.key_click_text_entry( textbox, command.KeyClick("Enter"), 0 ) def test_key_click_slider_helpful_err(self): slider = wx.Slider() with self.assertRaises(ValueError) as exc: _interaction_helpers.key_click_slider( slider, command.KeyClick("Enter"), 0 ) self.assertIn( "['Down', 'Left', 'Page Down', 'Page Up', 'Right', 'Up']", str(exc.exception), ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/testing/tester/command.py0000644000175100001730000000410100000000000023000 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ This module defines interaction objects that can be passed to ``UIWrapper.perform`` where the actions represent 'commands'. Implementations for these actions are expected to produce the documented side effects without returning any values. """ class MouseClick: """An object representing the user clicking a mouse button. Currently the left mouse button is assumed. In most circumstances, a widget can still be clicked on even if it is disabled. Therefore unlike key events, if the widget is disabled, implementations should not raise an exception. """ pass class MouseDClick: """ An object representing the user double clicking a mouse button. Currently the left mouse button is assumed. In most circumstances, a widget can still be clicked on even if it is disabled. Therefore unlike key events, if the widget is disabled, implementations should not raise an exception. """ pass class KeySequence: """An object representing the user typing a sequence of keys. Implementations should raise ``Disabled`` if the widget is disabled. Attributes ---------- sequence : str A string that represents a sequence of key inputs. e.g. "Hello World" """ def __init__(self, sequence): self.sequence = sequence class KeyClick: """An object representing the user clicking a key on the keyboard. Implementations should raise ``Disabled`` if the widget is disabled. Attributes ---------- key : str Standardized (pyface) name for a keyboard event. e.g. "Enter", "Tab", "Space", "0", "1", "A", ... """ def __init__(self, key): self.key = key ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/testing/tester/exceptions.py0000644000175100001730000000405700000000000023555 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Custom exceptions for UITester and UIWrapper. """ class TesterError(Exception): """Custom exception for UITester/UIWrapper.""" pass class Disabled(TesterError): """Raised when a simulation fails because the widget is disabled.""" pass class InteractionNotSupported(TesterError): """Raised when an interaction is not supported by a wrapper. Parameters ---------- target_class : subclass of type The type of a UI target being operated on. interaction_class : subclass of type Any class for the interaction. supported : list of types List of supported interaction types. """ def __init__(self, target_class, interaction_class, supported): self.target_class = target_class self.interaction_class = interaction_class self.supported = supported def __str__(self): return ( "No handler is found for target {!r} with interaction {!r}. " "Supported these: {!r}".format( self.target_class, self.interaction_class, self.supported ) ) class LocationNotSupported(TesterError): """Raised when attempt to resolve a location on a UI fails because the location type is not supported. """ def __init__(self, target_class, locator_class, supported): self.target_class = target_class self.locator_class = locator_class self.supported = supported def __str__(self): return ( "Location {!r} is not supported for {!r}. " "Supported these: {!r}".format( self.locator_class, self.target_class, self.supported ) ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/testing/tester/locator.py0000644000175100001730000000341600000000000023035 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ This module defines objects for locating nested UI targets, to be used with ``UIWrapper.locate``. Implementations for these actions are expected to return a value which is a UI target where further location resolution or user interaction can be applied. """ class Cell: """ A locator for locating a target uniquely specified by a row index and a column index. Attributes ---------- row : int 0-based index column : int 0-based index """ def __init__(self, row, column): self.row = row self.column = column class Index: """A locator for locating a target that is uniquely specified by a single 0-based index. Attributes ---------- index : int 0-based index """ def __init__(self, index): self.index = index class TargetByName: """A locator for locating the next UI target using a name. Attributes ---------- name : str """ def __init__(self, name): self.name = name class TargetById: """A locator for locating the next UI target using an id. Attributes ---------- id : str """ def __init__(self, id): self.id = id class Slider: """A locator for locating a nested slider widget within a UI.""" pass class Textbox: """A locator for locating a nested textbox widget within a UI.""" pass ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/testing/tester/query.py0000644000175100001730000000521200000000000022533 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ This module defines interaction objects that can be passed to ``UIWrapper.inspect`` where the actions represent 'queries'. Implementations for these actions are expected to return value(s), ideally without incurring side-effects. """ class Selected: """ Represents an interaction to obtain the currently selected object(s). Implementations should return a list of selected objects, or an empty list if nothing is selected. """ pass class SelectedIndices: """ Represents an interaction to obtain the indices of the currently selected objects. Implementations should return a list of indicies of the selected objects or an empty list if nothing is selected. Note that an index could be an integer (e.g. when selecting from a list or enumerataion, or selecting entire rows or columns of a table), or it could be a tuple (e.g. corresponding to a specific cell at some (row, column) in a table). """ pass class SelectedText: """An object representing an interaction to obtain the displayed (echoed) plain text which is currently selected. E.g. For a Enum List, with one entry currently selected, the displayed selected text would be the label of that entry. Implementations should return a ``str``, or None if nothing is selected. """ pass class DisplayedText: """An object representing an interaction to obtain the displayed (echoed) plain text. E.g. For a textbox using a password styling, the displayed text should be a string of platform-dependent password mask characters. Implementations should return a ``str``. """ pass class IsChecked: """An object representing an interaction to obtain whether a checkable widget (e.g. checkbox) is checked or not. Implementations should return True if checked and False if not. """ pass class IsEnabled: """An object representing an interaction to obtain whether a widget is enabled or not. Implementations should return True if enabled and False if not. """ pass class IsVisible: """An object representing an interaction to obtain whether a widget is visible or not. Implementations should return True if visible and False if not. """ pass ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/testing/tester/target_registry.py0000644000175100001730000002247000000000000024611 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Define the registry object responsible for collecting and reporting implementations for testing various GUI elements. """ import inspect from traitsui.testing.tester._abstract_target_registry import ( AbstractTargetRegistry, ) from traitsui.testing.tester.exceptions import ( InteractionNotSupported, LocationNotSupported, ) class _TargetToKeyRegistry: """Perform the mapping from target to a key to a callable. Internally this is a dict(type, dict(type, callable)), but expose a few methods for better error reporting. """ def __init__(self, exception_maker): """Initializer Parameters ---------- exception_maker : callable(target_class, key, available_keys) A callable that return an exception for when no values are referred to by a given pair of target_class and key. """ self._target_to_key_to_value = {} self.exception_maker = exception_maker def register(self, target_class, key, value): action_to_handler = self._target_to_key_to_value.setdefault( target_class, {} ) if key in action_to_handler: raise ValueError( "A value for target {!r} and key {!r} already " "exists.".format(target_class, key) ) action_to_handler[key] = value def get_value(self, target_class, key): action_to_handler = self._target_to_key_to_value.get(target_class, []) if key not in action_to_handler: raise self.exception_maker( target_class=target_class, key=key, available_keys=list(action_to_handler), ) return action_to_handler[key] def get_keys(self, target_class): """Return all the keys for the given target. Parameters ---------- target_class : subclass of type The type of a UI target being operated on. Returns ------- keys : set """ return set(self._target_to_key_to_value.get(target_class, [])) class TargetRegistry(AbstractTargetRegistry): """An object for registering interaction and location resolution logic for different UI target types. ``register_interaction`` supports extending ``UIWrapper.perform`` and ``UIWrapper.inspect`` for a given UI target type and interaction type. ``register_location`` supports extending ``UIWrapper.locate`` for a given UI target type and location type. See :ref:`testing-how-extension-works` in the User Manual for further details. """ def __init__(self): self._interaction_registry = _TargetToKeyRegistry( exception_maker=( lambda target_class, key, available_keys: ( InteractionNotSupported( target_class=target_class, interaction_class=key, supported=available_keys, ) ) ), ) self._location_registry = _TargetToKeyRegistry( exception_maker=( lambda target_class, key, available_keys: LocationNotSupported( target_class=target_class, locator_class=key, supported=available_keys, ) ), ) def register_interaction(self, target_class, interaction_class, handler): """Register a handler for a given target type and interaction type. Parameters ---------- target_class : subclass of type The type of a UI target being operated on. interaction_class : subclass of type Any class. handler : callable(UIWrapper, interaction) -> any The function to handle the particular interaction on a target. ``interaction`` should be an instance of ``interaction_class``. Raises ------ ValueError If a handler has already be registered for the same target type and interaction class. """ self._interaction_registry.register( target_class=target_class, key=interaction_class, value=handler, ) def _get_handler(self, target, interaction): """Return a callable for handling an interaction for a given target. Parameters ---------- target : any The UI target being operated on. interaction : any Any interaction object. Returns ------- handler : callable(UIWrapper, interaction) -> any The function to handle the given interaction on a target. Raises ------ InteractionNotSupported If the given target and interaction types are not supported by this registry. """ return self._interaction_registry.get_value( target_class=target.__class__, key=interaction.__class__, ) def _get_interactions(self, target): """Returns all the interactions supported for the given target. Parameters ---------- target : any The UI target for which supported interactions are queried. Returns ------- interaction_classes : set Supported interaction types for the given target type. """ return self._interaction_registry.get_keys( target_class=target.__class__ ) def _get_interaction_doc(self, target, interaction_class): """Return the documentation for the given target and interaction type. Parameters ---------- target : any The UI target for which the interaction will be applied. interaction_class : subclass of type Any class. Returns ------- doc : str Raises ------ InteractionNotSupported If the given target and interaction types are not supported by this registry. """ self._interaction_registry.get_value( target_class=target.__class__, key=interaction_class, ) # This maybe configurable in the future via register_interaction return inspect.getdoc(interaction_class) def register_location(self, target_class, locator_class, solver): """Register a solver for resolving the next UI target for the given target type and locator type. Parameters ---------- target_class : subclass of type The type of a UI target being operated on. locator_class : subclass of type Any class. solver : callable(UIWrapper, location) -> any A callable for resolving a location into a new target. The location argument will be an instance of locator_class. Raises ------ ValueError If a solver has already been registered for the given target type and locator type. """ self._location_registry.register( target_class=target_class, key=locator_class, value=solver, ) def _get_solver(self, target, location): """Return a callable registered for resolving a location for the given target and location. Parameters ---------- target : any The UI target being operated on. location : subclass of type The location to be resolved on the target. Raises ------ LocationNotSupported If the given locator and target types are not supported. """ return self._location_registry.get_value( target_class=target.__class__, key=location.__class__, ) def _get_locations(self, target): """Returns all the location types supported for the given target. Parameters ---------- target : any The UI target for which supported location types are queried. Returns ------- locators_classes : set Supported locator types for the given target type. """ return self._location_registry.get_keys(target_class=target.__class__) def _get_location_doc(self, target, locator_class): """Return the documentation for the given target and locator type. Parameters ---------- target : any The UI target being operated on. locator_class : subclass of type Any class. Returns ------- doc : str Raises ------ LocationNotSupported If the given locator and target types are not supported. """ self._location_registry.get_value( target_class=target.__class__, key=locator_class, ) # This maybe configurable in the future via register_location return inspect.getdoc(locator_class) ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1684768028.107807 traitsui-8.0.0/traitsui/testing/tester/tests/0000755000175100001730000000000000000000000022156 5ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/testing/tester/tests/__init__.py0000644000175100001730000000000000000000000024255 0ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/testing/tester/tests/test_registry.py0000644000175100001730000001707700000000000025453 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! import unittest from traitsui.testing.tester.target_registry import ( TargetRegistry, ) from traitsui.testing.tester.exceptions import ( InteractionNotSupported, LocationNotSupported, ) class TestInteractionRegistry(unittest.TestCase): def test_registry_empty(self): class SpecificTarget: pass class Action: pass registry = TargetRegistry() with self.assertRaises(InteractionNotSupported) as exception_context: registry._get_handler(SpecificTarget(), Action()) self.assertEqual( str(exception_context.exception), f"No handler is found for target {SpecificTarget!r} with " f"interaction {Action!r}. Supported these: []", ) def test_register_editor_with_action(self): registry = TargetRegistry() class SpecificEditor: pass class UserAction: pass def handler(wrapper, interaction): pass # when registry.register_interaction( target_class=SpecificEditor, interaction_class=UserAction, handler=handler, ) # then actual = registry._get_handler(SpecificEditor(), UserAction()) self.assertIs(actual, handler) def test_get_interactions_supported(self): registry = TargetRegistry() class SpecificEditor: pass class UserAction: pass class UserAction2: pass def handler(wrapper, interaction): pass # when registry.register_interaction( target_class=SpecificEditor, interaction_class=UserAction, handler=handler, ) registry.register_interaction( target_class=SpecificEditor, interaction_class=UserAction2, handler=handler, ) # then self.assertEqual( registry._get_interactions(SpecificEditor()), {UserAction, UserAction2}, ) def test_action_not_supported_report_supported_action(self): # Test raise InteractionNotSupported contains information about what # actions are supported. class SpecificEditor: pass class SpecificEditor2: pass class UserAction: pass class UserAction2: pass class UserAction3: pass def handler(wrapper, interaction): pass registry = TargetRegistry() registry.register_interaction(SpecificEditor, UserAction, handler) registry.register_interaction(SpecificEditor2, UserAction2, handler) registry.register_interaction(SpecificEditor2, UserAction3, handler) with self.assertRaises(InteractionNotSupported) as exception_context: registry._get_handler(SpecificEditor2(), None) self.assertIn(UserAction2, exception_context.exception.supported) self.assertIn(UserAction3, exception_context.exception.supported) self.assertNotIn(UserAction, exception_context.exception.supported) def test_error_conflict(self): # Test the same target + interaction type cannot be registered twice. class SpecificEditor: pass class UserAction: pass def handler(wrapper, interaction): pass registry = TargetRegistry() registry.register_interaction(SpecificEditor, UserAction, handler) with self.assertRaises(ValueError): registry.register_interaction(SpecificEditor, UserAction, handler) def test_error_get_interaction_doc(self): # The registry is empty registry = TargetRegistry() with self.assertRaises(InteractionNotSupported): registry._get_interaction_doc(2.1, int) def test_get_default_interaction_doc(self): class Action: """Some action.""" pass def handler(wrapper, interaction): pass registry = TargetRegistry() registry.register_interaction( target_class=float, interaction_class=Action, handler=handler, ) actual = registry._get_interaction_doc( target=21.2, interaction_class=Action, ) self.assertEqual(actual, "Some action.") class TestLocationRegistry(unittest.TestCase): def test_location_registry_empty(self): class SpecificTarget: pass class Locator: pass registry = TargetRegistry() with self.assertRaises(LocationNotSupported) as exception_context: registry._get_solver(SpecificTarget(), Locator()) self.assertEqual(exception_context.exception.supported, []) self.assertEqual( str(exception_context.exception), f"Location {Locator!r} is not supported for {SpecificTarget!r}. " "Supported these: []", ) def test_register_location(self): def solver(wrapper, location): return 1 registry = TargetRegistry() registry.register_location( target_class=float, locator_class=str, solver=solver ) self.assertIs(registry._get_solver(2.1, "dummy"), solver) def test_register_location_report_existing(self): def solver(wrapper, location): return 1 registry = TargetRegistry() registry.register_location( target_class=float, locator_class=str, solver=solver ) with self.assertRaises(LocationNotSupported) as exception_context: registry._get_solver(3.4, None) self.assertEqual(exception_context.exception.supported, [str]) def test_get_locations_supported(self): # Test _get_locations return the supported location types. registry = TargetRegistry() class SpecificEditor: pass class Locator1: pass class Locator2: pass def solver(wrapper, location): return 1 # when registry.register_location( target_class=SpecificEditor, locator_class=Locator1, solver=solver, ) registry.register_location( target_class=SpecificEditor, locator_class=Locator2, solver=solver, ) # then self.assertEqual( registry._get_locations(SpecificEditor()), {Locator1, Locator2} ) def test_get_location_help_default(self): class Locator: """Some default documentation.""" pass registry = TargetRegistry() registry.register_location( target_class=float, locator_class=Locator, solver=lambda w, l: 1, ) help_text = registry._get_location_doc( target=2.345, locator_class=Locator, ) self.assertEqual(help_text, "Some default documentation.") def test_error_get_interaction_doc(self): # The registry is empty registry = TargetRegistry() with self.assertRaises(LocationNotSupported): registry._get_location_doc(3.456, int) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/testing/tester/tests/test_ui_tester.py0000644000175100001730000001710200000000000025573 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! import unittest from unittest import mock from pyface.api import GUI from traits.api import ( Button, Instance, HasTraits, Str, ) from traitsui.api import Item, ModelView, View from traitsui.tests._tools import ( process_cascade_events, requires_toolkit, ToolkitName, ) from traitsui.testing.tester.exceptions import InteractionNotSupported from traitsui.testing.tester.target_registry import TargetRegistry from traitsui.testing.tester.ui_tester import ( UITester, ) from traitsui.testing.tester.ui_wrapper import ( UIWrapper, ) class Order(HasTraits): submit_button = Button() submit_label = Str("Submit") class Model(HasTraits): order = Instance(Order, ()) class SimpleApplication(ModelView): model = Instance(Model) @requires_toolkit([ToolkitName.qt, ToolkitName.wx]) class TestUITesterCreateUI(unittest.TestCase): """Test UITester.create_ui""" def test_ui_disposed(self): tester = UITester() order = Order() view = View(Item("submit_button")) with tester.create_ui(order, dict(view=view)) as ui: pass self.assertTrue(ui.destroyed) def test_create_ui_reraise_exception(self): tester = UITester() order = Order() view = View(Item("submit_button")) with self.assertRaises(RuntimeError), self.assertLogs( "traitsui", level="ERROR" ): with tester.create_ui(order, dict(view=view)) as ui: def raise_error(): raise ZeroDivisionError() GUI().invoke_later(raise_error) self.assertIsNone(ui.control) def test_create_ui_respect_auto_process_events_flag(self): tester = UITester(auto_process_events=False) order = Order() view = View(Item("_")) side_effect = mock.Mock() gui = GUI() gui.invoke_later(side_effect) # Make sure all pending events are processed at the end of the test. self.addCleanup(process_cascade_events) with tester.create_ui(order, dict(view=view)) as ui: pass # dispose is called. self.assertIsNone(ui.control) # But the GUI events are not processed. self.assertEqual(side_effect.call_count, 0) @requires_toolkit([ToolkitName.qt, ToolkitName.wx]) class TestUITesterRegistry(unittest.TestCase): """Test maintaining registries.""" def test_traitsui_registry_added(self): # Even if we have a custom registry list, the builtin TraitsUI # registry is always added. custom_registry = TargetRegistry() tester = UITester(registries=[custom_registry]) view = View(Item("submit_button")) with tester.create_ui(Order(), dict(view=view)) as ui: # this relies on TraitsUI builtin registry. wrapper = tester.find_by_name(ui, "submit_button") # custom registry is accessible # sanity check with self.assertRaises(InteractionNotSupported): wrapper.perform(1) custom_registry.register_interaction( target_class=wrapper._target.__class__, interaction_class=int, handler=lambda wrapper, interaction: None, ) wrapper.perform(1) @requires_toolkit([ToolkitName.qt, ToolkitName.wx]) class TestUITesterFindEditor(unittest.TestCase): """Test logic for finding a target.""" def test_interactor_found_if_editor_found(self): tester = UITester() view = View(Item("submit_button")) with tester.create_ui(Order(), dict(view=view)) as ui: wrapper = tester.find_by_name(ui, "submit_button") self.assertIsInstance(wrapper, UIWrapper) (expected,) = ui.get_editors("submit_button") self.assertEqual(wrapper._target, expected) self.assertEqual( wrapper._registries, tester._registries, ) def test_no_editors_found(self): # The view does not have "submit_n_events" tester = UITester() view = View(Item("submit_button")) with tester.create_ui(Order(), dict(view=view)) as ui: with self.assertRaises(ValueError) as exception_context: tester.find_by_name(ui, "submit_n_events") self.assertIn( "No editors can be found", str(exception_context.exception), ) def test_multiple_editors_found(self): # There may be more than one target with the same name. # find_by_name cannot be used in this case. tester = UITester() view = View(Item("submit_button"), Item("submit_button")) with tester.create_ui(Order(), dict(view=view)) as ui: with self.assertRaises(ValueError) as exception_context: tester.find_by_name(ui, "submit_button") self.assertIn( "Found multiple editors", str(exception_context.exception), ) def test_delay_persisted(self): tester = UITester(delay=0.01) view = View(Item("submit_button")) with tester.create_ui(Order(), dict(view=view)) as ui: wrapped = tester.find_by_name(ui, "submit_button") self.assertEqual(wrapped.delay, 0.01) def test_find_by_id(self): tester = UITester(delay=123) item1 = Item("submit_button", id="item1") item2 = Item("submit_button", id="item2") view = View(item1, item2) with tester.create_ui(Order(), dict(view=view)) as ui: wrapper = tester.find_by_id(ui, "item2") self.assertIs(wrapper._target.item, item2) self.assertEqual(wrapper._registries, tester._registries) self.assertEqual(wrapper.delay, tester.delay) def test_find_by_id_multiple(self): # The uniqueness is not enforced. The first one is returned. tester = UITester() item1 = Item("submit_button", id="item1") item2 = Item("submit_button", id="item2") item3 = Item("submit_button", id="item2") view = View(item1, item2, item3) with tester.create_ui(Order(), dict(view=view)) as ui: wrapper = tester.find_by_id(ui, "item2") self.assertIs(wrapper._target.item, item2) def test_auto_process_events_skipped(self): # Event processing can be skipped. tester = UITester(auto_process_events=False) side_effect = mock.Mock() gui = GUI() gui.invoke_later(side_effect) # Make sure all pending events are processed at the end of the test. self.addCleanup(process_cascade_events) view = View(Item("submit_button", id="item")) with tester.create_ui(Order(), dict(view=view)) as ui: tester.find_by_id(ui, "item") self.assertEqual(side_effect.call_count, 0) class TestUITesterGuiFree(unittest.TestCase): """Test GUI free interface on UITester.""" def test_auto_process_events_readonly(self): # auto_process_events can be inspected, but it cannot be changed. tester = UITester(auto_process_events=False) self.assertIs(tester.auto_process_events, False) with self.assertRaises(AttributeError): tester.auto_process_events = True ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/testing/tester/tests/test_ui_wrapper.py0000644000175100001730000003614200000000000025752 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! import io import textwrap import unittest from unittest import mock from pyface.api import GUI from traits.api import HasTraits, Int from traits.testing.api import UnittestTools from traitsui.tests._tools import ( process_cascade_events, requires_toolkit, ToolkitName, ) from traitsui.testing.tester._abstract_target_registry import ( AbstractTargetRegistry, ) from traitsui.testing.tester.exceptions import ( InteractionNotSupported, LocationNotSupported, ) from traitsui.testing.tester.target_registry import ( TargetRegistry, ) from traitsui.testing.tester.ui_wrapper import ( UIWrapper, ) def example_ui_wrapper(**kwargs): """Return an instance of UIWrapper for testing purposes. Parameters ---------- **kwargs Values to use instead of the default values. Returns ------- wrapper: UIWrapper """ values = dict( target=None, registries=[], ) values.update(kwargs) return UIWrapper(**values) class StubRegistry(AbstractTargetRegistry): """A stub implementation of the AbstractTargetRegistry for testing""" def __init__( self, handler=None, solver=None, supported_interaction_classes=(), supported_locator_classes=(), interaction_doc="", location_doc="", ): self.handler = handler self.solver = solver self.supported_interaction_classes = supported_interaction_classes self.supported_locator_classes = supported_locator_classes self.interaction_doc = interaction_doc self.location_doc = location_doc def _get_handler(self, target, interaction): if interaction.__class__ not in self.supported_interaction_classes: raise InteractionNotSupported( target_class=target.__class__, interaction_class=interaction.__class__, supported=list(self.supported_interaction_classes), ) return self.handler def _get_interactions(self, target): return set(self.supported_interaction_classes) def _get_interaction_doc(self, target, interaction_class): return self.interaction_doc def _get_solver(self, target, location): if location.__class__ not in self.supported_locator_classes: raise LocationNotSupported( target_class=target.__class__, locator_class=location.__class__, supported=list(self.supported_locator_classes), ) return self.solver def _get_locations(self, target): return set(self.supported_locator_classes) def _get_location_doc(self, target, locator_class): return self.location_doc # Use of perform/inspect requires the GUI event loop @requires_toolkit([ToolkitName.qt, ToolkitName.wx]) class TestUIWrapperInteractionRegistries(unittest.TestCase): """Test the logic regarding the order of (interaction) registries.""" def test_registry_priority(self): # If two registries have a handler for the same target and interaction # types, the first register is used. registry1 = StubRegistry( handler=lambda w, l: 1, supported_interaction_classes=[str], ) registry2 = StubRegistry( handler=lambda w, l: 2, supported_interaction_classes=[str], ) wrapper = example_ui_wrapper( registries=[registry2, registry1], ) value = wrapper.inspect("some string") self.assertEqual(value, 2) # reverse order wrapper = example_ui_wrapper(registries=[registry1, registry2]) value = wrapper.inspect("some other string") self.assertEqual(value, 1) def test_registry_selection(self): # If the first registry says it can't handle the interaction, the next # registry is tried. registry1 = StubRegistry() registry2_handler = mock.Mock() registry2 = StubRegistry( handler=registry2_handler, supported_interaction_classes=[int], ) wrapper = example_ui_wrapper( registries=[registry1, registry2], ) wrapper.perform(123) self.assertEqual(registry2_handler.call_count, 1) def test_registry_all_declined(self): # If none of the registries can support an interaction, the # exception raised provide information on what actions are # supported. wrapper = example_ui_wrapper( registries=[ StubRegistry(supported_interaction_classes=[int]), StubRegistry(supported_interaction_classes=[float]), ], ) with self.assertRaises(InteractionNotSupported) as exception_context: wrapper.perform(None) self.assertCountEqual( exception_context.exception.supported, [int, float], ) # Use of locate requires the GUI event loop @requires_toolkit([ToolkitName.qt, ToolkitName.wx]) class TestUIWrapperLocationRegistry(unittest.TestCase): """Test the use of registries with locate.""" def test_location_registry_priority(self): registry1 = StubRegistry( solver=lambda w, l: 1, supported_locator_classes=[str], ) registry2 = StubRegistry( solver=lambda w, l: 2, supported_locator_classes=[str], ) wrapper = example_ui_wrapper( registries=[registry2, registry1], ) new_wrapper = wrapper.locate("some string") self.assertEqual(new_wrapper._target, 2) # swap the order wrapper = example_ui_wrapper( registries=[registry1, registry2], ) new_wrapper = wrapper.locate("some other string") self.assertEqual(new_wrapper._target, 1) def test_location_registry_selection(self): # If the first registry says it can't handle the interaction, the next # registry is tried. def solver2(wrapper, location): return 2 registry1 = StubRegistry() registry2 = StubRegistry( solver=solver2, supported_locator_classes=[str], ) wrapper = example_ui_wrapper( registries=[registry1, registry2], ) new_wrapper = wrapper.locate("some string") self.assertEqual(new_wrapper._target, 2) self.assertEqual( new_wrapper._registries, wrapper._registries, ) def test_registry_all_declined(self): # If none of the registries can support a location, the # exception raised provide information on what actions are # supported. wrapper = example_ui_wrapper( registries=[ StubRegistry(supported_locator_classes=[int]), StubRegistry(supported_locator_classes=[float]), ], ) with self.assertRaises(LocationNotSupported) as exception_context: wrapper.locate(None) self.assertCountEqual( exception_context.exception.supported, [int, float], ) class TestUIWrapperHelp(unittest.TestCase): """Test calling UIWrapper.help""" def test_help_message(self): class Action: """Say hello. Say bye. """ pass class Locator: """Return anything you like. Good day! """ pass registry1 = TargetRegistry() registry1.register_interaction( target_class=str, interaction_class=Action, handler=mock.Mock(), ) registry2 = TargetRegistry() registry2.register_location( target_class=str, locator_class=Locator, solver=mock.Mock(), ) wrapper = example_ui_wrapper( target="dummy", registries=[registry1, registry2] ) # when stream = io.StringIO() with mock.patch("sys.stdout", stream): wrapper.help() # then self.assertEqual( stream.getvalue(), textwrap.dedent( f"""\ Interactions ------------ {Action!r} Say hello. Say bye. Locations --------- {Locator!r} Return anything you like. Good day! """ ), ) def test_help_message_priority_interactions(self): # The first registry in the list has the highest priority # The last registry in the list has the least priority high_priority_registry = StubRegistry( supported_locator_classes=[str], supported_interaction_classes=[float], interaction_doc="Interaction: I get a higher priority.", location_doc="Location: I get a higher priority.", ) low_priority_registry = StubRegistry( supported_locator_classes=[str], supported_interaction_classes=[float], interaction_doc="Interaction: I get a lower priority.", location_doc="Location: I get a lower priority.", ) # Put higher priority registry first. wrapper = example_ui_wrapper( target="dummy", registries=[high_priority_registry, low_priority_registry], ) # when stream = io.StringIO() with mock.patch("sys.stdout", stream): wrapper.help() # then self.assertEqual( stream.getvalue(), textwrap.dedent( f"""\ Interactions ------------ {float!r} Interaction: I get a higher priority. Locations --------- {str!r} Location: I get a higher priority. """ ), ) def test_help_message_nothing_is_supported(self): registry = TargetRegistry() wrapper = example_ui_wrapper(registries=[registry]) # when stream = io.StringIO() with mock.patch("sys.stdout", stream): wrapper.help() # then self.assertEqual( stream.getvalue(), textwrap.dedent( """\ Interactions ------------ No interactions are supported. Locations --------- No locations are supported. """ ), ) class NumberHasTraits(HasTraits): number = Int() @requires_toolkit([ToolkitName.qt, ToolkitName.wx]) class TestUIWrapperEventProcessed(unittest.TestCase, UnittestTools): """Test GUI events are processed and exceptions from the GUI event loop are handled. """ def test_event_processed(self): # Test GUI events are processed such that the trait is changed. gui = GUI() model = NumberHasTraits() def handler(wrapper, action): gui.set_trait_later(model, "number", 2) wrapper = example_ui_wrapper( registries=[ StubRegistry( handler=handler, supported_interaction_classes=[float], ), ], ) with self.assertTraitChanges(model, "number"): wrapper.perform(2.123) def test_event_processed_prior_to_resolving_location(self): # Test GUI events are processed prior to resolving location gui = GUI() model = NumberHasTraits() gui.set_trait_later(model, "number", 2) def solver(wrapper, location): return model.number wrapper = example_ui_wrapper( registries=[ StubRegistry( solver=solver, supported_locator_classes=[float], ) ], ) new_wrapper = wrapper.locate(4.567) self.assertEqual(new_wrapper._target, 2) def test_event_processed_with_exception_captured(self): # Test exceptions in the GUI event loop are captured and then cause # the test to error. gui = GUI() def raise_error(): raise ZeroDivisionError() def handler(wrapper, action): gui.invoke_later(raise_error) wrapper = example_ui_wrapper( registries=[ StubRegistry( handler=handler, supported_interaction_classes=[float], ), ], ) with self.assertRaises(RuntimeError), self.assertLogs("traitsui"): wrapper.perform(1.234) def test_exception_not_in_gui(self): # Exceptions from code executed outside of the event loop are # propagated as is. def handler(wrapper, action): raise ZeroDivisionError() wrapper = example_ui_wrapper( registries=[ StubRegistry( handler=handler, supported_interaction_classes=[float], ), ], ) with self.assertRaises(ZeroDivisionError): wrapper.perform(9.99) def test_perform_event_processed_optional(self): # Allow event processing to be switched off. gui = GUI() side_effect = mock.Mock() def handler(wrapper, action): gui.invoke_later(side_effect) wrapper = example_ui_wrapper( registries=[ StubRegistry( handler=handler, supported_interaction_classes=[float], ), ], auto_process_events=False, ) # With auto_process_events set to False, events are not automatically # processed. wrapper.perform(12.345) self.addCleanup(process_cascade_events) self.assertEqual(side_effect.call_count, 0) def test_locate_event_processed_optional(self): # Allow event processing to be switched off. gui = GUI() side_effect = mock.Mock() gui.invoke_later(side_effect) self.addCleanup(process_cascade_events) def solver(wrapper, location): return 1 wrapper = example_ui_wrapper( registries=[ StubRegistry( solver=solver, supported_locator_classes=[float], ), ], auto_process_events=False, ) # With auto_process_events set to False, events are not automatically # processed. new_wrapper = wrapper.locate(1.234) self.assertEqual(side_effect.call_count, 0) self.assertFalse(new_wrapper._auto_process_events) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/testing/tester/ui_tester.py0000644000175100001730000001540000000000000023371 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Define top-level object(s) to support testing TraitsUI applications. """ import contextlib from traitsui.testing._gui import process_cascade_events from traitsui.testing._exception_handling import reraise_exceptions from traitsui.testing.tester._ui_tester_registry.default_registry import ( get_default_registries, ) from traitsui.testing.tester.ui_wrapper import UIWrapper class UITester: """UITester assists testing of GUI applications developed using TraitsUI. See :ref:`testing-traitsui-applications` Section in the User Manual for further details. Parameters ---------- registries : list of TargetRegistry, optional Registries of interaction for different targets, in the order of decreasing priority. If provided, a shallow copy will be made. Additional registries are appended to the list to provide builtin support for TraitsUI UI and editors. delay : int, optional Time delay (in ms) in which actions by the tester are performed. Note it is propagated through to created child wrappers. The delay allows visual confirmation test code is working as desired. Defaults to 0. auto_process_events : bool, optional Whether to process (cascade) GUI events automatically. Default is True. For tests that launch a modal dialog and rely on a recurring timer to poll if the dialog is closed, it may be necessary to set this flag to false in order to avoid deadlocks. Note that this is propagated through to created child wrappers. Attributes ---------- delay : int Time delay (in ms) in which actions by the tester are performed. Note it is propagated through to created child wrappers. The delay allows visual confirmation test code is working as desired. """ def __init__(self, *, registries=None, delay=0, auto_process_events=True): if registries is None: self._registries = [] else: self._registries = registries.copy() # These registries contribute the support for TraitsUI UI and editors. # The find_by_name/find_by_id methods in this class also depend on # one of these registries. self._registries.extend(get_default_registries()) self.delay = delay self._auto_process_events = auto_process_events @property def auto_process_events(self): """Flag to indicate whether to process (cascade) GUI events automatically. This is propagated through to :class:`~traitsui.testing.tester.ui_wrapper.UIWrapper` instances created from this tester. This value can only be set at instantiation. """ # No mutations post-init are allowed because UIWrapper created prior to # the mutation will not respect the new value, and so it would be # confusing if this attribute was allowed to be mutated. return self._auto_process_events @contextlib.contextmanager def create_ui(self, object, ui_kwargs=None): """Context manager to create a UI and dispose it upon exit. This method does not modify the states of the :class:`UITester` and is not a requirement for using other methods on the tester. Parameters ---------- object : HasTraits An instance of HasTraits for which a GUI will be created. ui_kwargs : dict or None, optional Keyword arguments to be provided to ``HasTraits.edit_traits``. Default is to call ``edit_traits`` with no additional keyword arguments. Yields ------ ui : traitsui.ui.UI """ ui_kwargs = {} if ui_kwargs is None else ui_kwargs ui = object.edit_traits(**ui_kwargs) try: yield ui finally: with reraise_exceptions(): if self._auto_process_events: # At the end of a test, there may be events to be # processed. If dispose happens first, those events will be # processed after various editor states are removed, # causing errors. But if we are asked not to process # events, then don't. process_cascade_events() try: ui.dispose() finally: # dispose is not atomic and may push more events to the # event queue. Flush those too unless we are asked not to. if self._auto_process_events: process_cascade_events() def find_by_name(self, ui, name): """Find the TraitsUI editor with the given name and return a new ``UIWrapper`` object for further interactions with the editor. ``name`` is typically a value defined on :attr:`traitsui.item.Item.name`. This name is passed onto :func:`traitsui.ui.UI.get_editors`. Parameters ---------- ui : traitsui.ui.UI The UI created, e.g. by ``create_ui``. name : str A single name for retrieving a target on a UI. Returns ------- wrapper : UIWrapper """ return self._get_wrapper(ui).find_by_name(name=name) def find_by_id(self, ui, id): """Find the TraitsUI editor with the given identifier and return a new ``UIWrapper`` object for further interactions with the editor. ``id`` is typically a value defined on :attr:`traitsui.item.Item.id` or :attr:`traitsui.group.Group.id`. This provides a shortcut to locate a very nested editor in a view. Parameters ---------- ui : traitsui.ui.UI The UI created, e.g. by ``create_ui``. id : str Id for finding an item in the UI. Returns ------- wrapper : UIWrapper """ return self._get_wrapper(ui).find_by_id(id=id) def _get_wrapper(self, ui): """Return a new UIWrapper wrapping the given traitsui.ui.UI. Parameters ---------- ui : traitsui.ui.UI The UI created, e.g. by ``create_ui``. Returns ------- wrapper : UIWrapper """ return UIWrapper( target=ui, registries=self._registries, delay=self.delay, auto_process_events=self._auto_process_events, ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/testing/tester/ui_wrapper.py0000644000175100001730000002732700000000000023556 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Define the top-level object which is responsible for dispatching testing functionality for a given GUI object. The functionality is exposed via ``UITester``, which is a TraitsUI specific tester. """ from contextlib import contextmanager import textwrap from traitsui.testing._exception_handling import ( reraise_exceptions as _reraise_exceptions, ) from traitsui.testing._gui import ( process_cascade_events as _process_cascade_events, ) from traitsui.testing.tester import locator from traitsui.testing.tester.exceptions import ( InteractionNotSupported, LocationNotSupported, ) class UIWrapper: """ An ``UIWrapper`` has the following responsibilities: (1) Wraps a UI target. (2) Search for a nested UI target within the wrapped UI target. (3) Perform user interaction on the UI target, e.g. mouse click. A UI target is an object which can be searched for nested UI targets, either as intermediate things (like editors or a table widget), or as a leaf widget which can be operated upon (e.g. a mouse click). For example, a ``UIWrapper`` may wrap an instance of traitsui.ui.UI in which more UI targets can be located. A ``UIWrapper`` may also wrap a leaf widget on which user interactions can be performed. For locating a nested UI target, the ``locate`` method is provided. For simulating user interactions such as a mouse click or visual inspection, the ``perform`` and ``inspect`` methods are provided. Parameters ---------- target : any An object on which further UI target can be searched for, or can be a leaf target that can be operated on. registries : list of AbstractTargetRegistry Registries of interaction for different target, in the order of decreasing priority. delay : int, optional Time delay (in ms) in which actions by the wrapper are performed. Note it is propagated through to created child wrappers. The delay allows visual confirmation test code is working as desired. Defaults to 0. auto_process_events : bool, optional Whether to process (cascade) GUI events automatically. Default is True. For tests that launch a modal dialog and rely on a recurring timer to poll if the dialog is closed, it may be necessary to set this flag to false in order to avoid deadlocks. Note that this is propagated through to created child wrappers. Attributes ---------- delay : int Time delay (in ms) in which actions by the wrapper are performed. Note it is propagated through to created child wrappers. The delay allows visual confirmation test code is working as desired. _target : any The UI target currently wrapped for interactions or locating nested components. This is a protected attribute intended to be used for extending the testing API. Usage of this attribute may expose the software's internal structure to the tests, developers should use this attribute with discretion based on context. """ def __init__( self, target, *, registries, delay=0, auto_process_events=True ): self._target = target self._registries = registries self._auto_process_events = auto_process_events self.delay = delay def help(self): """Print help messages. (This function is intended for interactive use.) """ # mapping from interaction types to their documentation interaction_to_doc = dict() # mapping from location types to their documentation location_to_doc = dict() # Order registries by their priority (low to high) for registry in self._registries[::-1]: for type_ in registry._get_interactions(self._target): interaction_to_doc[type_] = registry._get_interaction_doc( target=self._target, interaction_class=type_, ) for type_ in registry._get_locations(self._target): location_to_doc[type_] = registry._get_location_doc( target=self._target, locator_class=type_, ) print("Interactions") print("------------") for interaction_type in sorted(interaction_to_doc, key=repr): print(repr(interaction_type)) print( textwrap.indent( interaction_to_doc[interaction_type], prefix=" " ) ) print() if not interaction_to_doc: print("No interactions are supported.") print() print("Locations") print("---------") for locator_type in sorted(location_to_doc, key=repr): print(repr(locator_type)) print( textwrap.indent(location_to_doc[locator_type], prefix=" ") ) print() if not location_to_doc: print("No locations are supported.") print() def locate(self, location): """Attempt to resolve the given location and return a new UIWrapper. Parameters ---------- location : any An instance of a location type supported by the current target. e.g. ``traitsui.testing.api.Index(1)`` Returns ------- wrapper : UIWrapper A new UIWrapper for the given location. Raises ------ LocationNotSupported If the given location is not supported. See Also -------- UIWrapper.help """ return UIWrapper( target=self._get_next_target(location), registries=self._registries, delay=self.delay, auto_process_events=self._auto_process_events, ) def find_by_name(self, name): """Find a target inside the current target using a name. This is equivalent to calling ``locate(TargetByName(name=name))``. Parameters ---------- name : str A single name for retreiving a target on a UI. Returns ------- wrapper : UIWrapper Raises ------ LocationNotSupported If the current target does not support locating another target by a name. See Also -------- traitsui.testing.tester.locator.TargetByName """ return self.locate(locator.TargetByName(name=name)) def find_by_id(self, id): """Find a target inside the current target using an id. This is equivalent to calling ``locate(TargetById(id=id))``. Parameters ---------- id : str Id for finding an item in the UI. Returns ------- wrapper : UIWrapper Raises ------ LocationNotSupported If the current target does not support locating another target by a unique identifier. See Also -------- traitsui.testing.tester.locator.TargetById """ return self.locate(locator.TargetById(id=id)) def perform(self, interaction): """Perform a user interaction that causes side effects. Parameters ---------- interaction : object An instance of an interaction type supported by the current target. e.g. ``traitsui.testing.api.MouseClick()`` Raises ------ InteractionNotSupported If the interaction given is not supported for the current UI target. See Also -------- UIWrapper.help """ self._perform_or_inspect(interaction) def inspect(self, interaction): """Return a value or values for inspection. Parameters ---------- interaction : object An instance of an interaction type supported by the current target. e.g. ``traitsui.testing.api.DisplayedText()`` Returns ------- values : any Any values returned for the given inspection. The type should be documented by the interaction type object. Raises ------ InteractionNotSupported If the interaction given is not supported for the current UI target. See Also -------- UIWrapper.help """ return self._perform_or_inspect(interaction) # Private methods ######################################################### def _perform_or_inspect(self, interaction): """Perform a user interaction or a user inspection. Parameters ---------- interaction : instance of interaction type An object defining the interaction. Returns ------- value : any Raises ------ InteractionNotSupported If the given interaction does not have a corresponding implementation for the wrapped UI target. """ supported = [] for registry in self._registries: try: handler = registry._get_handler( target=self._target, interaction=interaction, ) except InteractionNotSupported as e: supported.extend(e.supported) continue else: context = ( _event_processed if self._auto_process_events else _nullcontext ) with context(): return handler(self, interaction) raise InteractionNotSupported( target_class=self._target.__class__, interaction_class=interaction.__class__, supported=supported, ) def _get_next_target(self, location): """Return the next UI target from the given location. Parameters ---------- location : instance of locator type A location for resolving the next target. Returns ------- new_target : any Raises ------ LocationNotSupport If no solver are provided for resolving the given location in the wrapped UI target. """ supported = set() for registry in self._registries: try: handler = registry._get_solver( target=self._target, location=location, ) except LocationNotSupported as e: supported |= set(e.supported) else: if self._auto_process_events: with _reraise_exceptions(): _process_cascade_events() return handler(self, location) raise LocationNotSupported( target_class=self._target.__class__, locator_class=location.__class__, supported=list(supported), ) @contextmanager def _event_processed(): """Context manager to ensure GUI events are processed upon entering and exiting the context. """ with _reraise_exceptions(): _process_cascade_events() try: yield finally: _process_cascade_events() @contextmanager def _nullcontext(): """Equivalent to contextlib.nullcontext() in Python >= 3.7""" yield ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1684768028.107807 traitsui-8.0.0/traitsui/testing/tests/0000755000175100001730000000000000000000000020650 5ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/testing/tests/__init__.py0000644000175100001730000000000000000000000022747 0ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/testing/tests/test_api.py0000644000175100001730000000300500000000000023030 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Test the api module. """ import unittest class TestApi(unittest.TestCase): def test_tester_import(self): from traitsui.testing.api import UITester # noqa: F401 def test_commands_imports(self): from traitsui.testing.api import ( # noqa: F401 MouseClick, KeyClick, KeySequence, ) def test_query_imports(self): from traitsui.testing.api import ( # noqa: F401 DisplayedText, IsChecked, IsEnabled, IsVisible, SelectedText, ) def test_locator_imports(self): from traitsui.testing.api import ( # noqa: F401 Index, TargetById, TargetByName, Textbox, Slider, ) def test_advanced_usage_imports(self): from traitsui.testing.api import TargetRegistry # noqa: F401 def test_exceptions_imports(self): from traitsui.testing.api import ( # noqa: F401 Disabled, InteractionNotSupported, LocationNotSupported, TesterError, ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/testing/tests/test_exception_handling.py0000644000175100001730000000364100000000000026127 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Tests for traitsui.testing._exception_handling """ import unittest from pyface.api import GUI from traitsui.tests._tools import ( requires_toolkit, ToolkitName, ) from traitsui.testing._exception_handling import reraise_exceptions class TestExceptionHandling(unittest.TestCase): @requires_toolkit([ToolkitName.qt, ToolkitName.wx]) def test_error_from_gui_captured_and_raise(self): def raise_error_1(): raise ZeroDivisionError() def raise_error_2(): raise IndexError() # without the context manager: # - with Qt5, the test run will be aborted prematurely. # - with Qt4, the traceback is printed and the test passes. # - with Wx, the traceback is printed and the test passes. # With the context manager, the exception is always reraised. gui = GUI() with self.assertRaises( RuntimeError ) as exception_context, self.assertLogs("traitsui") as watcher: with reraise_exceptions(): gui.invoke_later(raise_error_1) gui.invoke_later(raise_error_2) gui.invoke_after(100, gui.stop_event_loop) gui.start_event_loop() error_msg = str(exception_context.exception) self.assertIn("ZeroDivisionError", error_msg) self.assertIn("IndexError", error_msg) log_content1, log_content2 = watcher.output self.assertIn("ZeroDivisionError", log_content1) self.assertIn("IndexError", log_content2) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/testing/tests/test_gui.py0000644000175100001730000001346200000000000023053 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Tests for traitsui.testing._gui """ import os import unittest from pyface.api import GUI from traitsui.tests._tools import ( is_qt, is_wx, is_mac_os, process_cascade_events, requires_toolkit, ToolkitName, ) if is_qt(): # Create a QObject that will emit a new event to itself as long as # it has not received enough. from pyface.qt import QtCore class DummyQObject(QtCore.QObject): def __init__(self, max_n_events): super().__init__() self.max_n_events = max_n_events self.n_events = 0 def event(self, event): if event.type() != QtCore.QEvent.Type.User: return super().event(event) self.n_events += 1 if self.n_events < self.max_n_events: new_event = QtCore.QEvent(QtCore.QEvent.Type.User) QtCore.QCoreApplication.postEvent(self, new_event) return True if is_wx(): # Create a wx.EvtHandler that will emit a new event to itself as long as # it has not received enough. import wx import wx.lib.newevent NewEvent, EVT_SOME_NEW_EVENT = wx.lib.newevent.NewEvent() class DummyWxHandler(wx.EvtHandler): def __init__(self, max_n_events): super().__init__() self.max_n_events = max_n_events self.n_events = 0 def TryBefore(self, event): self.n_events += 1 if self.n_events < self.max_n_events: self.post_event() return True def post_event(self): event = NewEvent() wx.PostEvent(self, event) class TestProcessEventsRepeated(unittest.TestCase): """Test process_events actually processes all events, including the events posted by the processed events. """ @requires_toolkit([ToolkitName.qt]) def test_qt_process_events_process_all(self): from pyface.qt import QtCore if QtCore.__version_info__ < (5, 0, 0) and is_mac_os: # On Qt4 and OSX, Qt QEventLoop.processEvents says nothing was " # processed even when there are events processed, causing the " # loop to break too soon. (See enthought/traitsui#951)" self.skipTest( "process_cascade_events is not reliable on Qt4 + OSX" ) is_appveyor = os.environ.get("APPVEYOR", None) is not None if QtCore.__version_info__ <= (5, 12, 6) and is_appveyor: # With Qt + Appveyor, process_cascade_events may # _occasionally_ break out of its loop too early. This is only # seen on Appveyor but has not been reproducible on other Windows # machines (See enthought/traitsui#951) self.skipTest( "process_cascade_events is not reliable on Qt + Appveyor" ) def cleanup(q_object): q_object.deleteLater() # If the test fails, run process events at least the same # number of times as max_n_events for _ in range(q_object.max_n_events): QtCore.QCoreApplication.processEvents( QtCore.QEventLoop.ProcessEventsFlag.AllEvents ) max_n_events = 10 q_object = DummyQObject(max_n_events=max_n_events) self.addCleanup(cleanup, q_object) QtCore.QCoreApplication.postEvent( q_object, QtCore.QEvent(QtCore.QEvent.Type.User) ) # sanity check calling processEvents does not process # cascade of events. QtCore.QCoreApplication.processEvents(QtCore.QEventLoop.ProcessEventsFlag.AllEvents) self.assertEqual(q_object.n_events, 1) # when process_cascade_events() # then actual = q_object.n_events # If process_cascade_events did not do what it promises, then there # are still pending tasks left. Run process events at least the same # number of times as max_n_events to verify for _ in range(max_n_events): QtCore.QCoreApplication.processEvents(QtCore.QEventLoop.ProcessEventsFlag.AllEvents) n_left_behind_events = q_object.n_events - actual msg = ( "Expected {max_n_events} events processed on the objects and zero " "events left on the queue after running process_cascade_events. " "Found {actual} processed with {n_left_behind_events} left " "behind.".format( max_n_events=max_n_events, actual=actual, n_left_behind_events=n_left_behind_events, ) ) self.assertEqual(n_left_behind_events, 0, msg) # If the previous assertion passes but this one fails, that means some # events have gone missing, and that would likely be a problem for the # test setup, not for the process_cascade_events. self.assertEqual(actual, max_n_events, msg) @requires_toolkit([ToolkitName.wx]) def test_wx_process_events_process_all(self): def cleanup(wx_handler): # In case of test failure, always flush the GUI event queue. GUI.process_events() wx_handler.Destroy() max_n_events = 10 wx_handler = DummyWxHandler(max_n_events=max_n_events) self.addCleanup(cleanup, wx_handler) wx_handler.post_event() # when process_cascade_events() # then self.assertEqual(wx_handler.n_events, max_n_events) ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1684768028.1118069 traitsui-8.0.0/traitsui/tests/0000755000175100001730000000000000000000000017173 5ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/tests/__init__.py0000644000175100001730000000000000000000000021272 0ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/tests/_tools.py0000644000175100001730000002122100000000000021042 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! import enum import re import sys from unittest import skipIf, TestSuite from pyface.toolkit import toolkit_object from traits.api import ( pop_exception_handler, push_exception_handler, ) from traits.etsconfig.api import ETSConfig # These functions are imported by traitsui's own tests. They can be redirected # to traitsui.testing and then these aliases can be removed. from traitsui.testing._exception_handling import reraise_exceptions # noqa from traitsui.testing._gui import process_cascade_events # noqa: F401 from traitsui.testing.api import UITester # ######### Testing tools # Toolkit constants class ToolkitName(enum.Enum): wx = "wx" qt = "qt" null = "null" def is_wx(): """Return true if the toolkit backend is wx.""" return ETSConfig.toolkit == ToolkitName.wx.name def is_qt(): """Return true if the toolkit backend is Qt (that includes Qt4 or Qt5, etc.) """ return ETSConfig.toolkit.startswith(ToolkitName.qt.name) def is_qt5(): """Return true if the toolkit backend is Qt5.""" if not is_qt(): return False # Only AFTER confirming Qt's availability... # We lean on Pyface here since the check is complicated. import pyface.qt return pyface.qt.is_qt5 def is_qt6(): """Return true if the toolkit backend is Qt6.""" if not is_qt(): return False # Only AFTER confirming Qt's availability... # We lean on Pyface here since the check is complicated. import pyface.qt return pyface.qt.is_qt6 def is_null(): """Return true if the toolkit backend is null.""" return ETSConfig.toolkit == ToolkitName.null.name def requires_toolkit(toolkits): """Decorator factory for skipping tests if the current toolkit is not one of the given values. Parameters ---------- toolkits : iterable of members of ToolkitName e.g. ``list(ToolkitName)`` to include all toolkits. """ mapping = { ToolkitName.null: is_null, ToolkitName.qt: is_qt, ToolkitName.wx: is_wx, } return skipIf( not any(mapping[toolkit]() for toolkit in toolkits), "Test requires one of these toolkits: {}".format(toolkits), ) #: True if current platform is MacOS is_mac_os = sys.platform.startswith("darwin") def count_calls(func): """Decorator that stores the number of times a function is called. The counter is stored in func._n_counts. """ def wrapped(*args, **kwargs): wrapped._n_calls += 1 return func(*args, **kwargs) wrapped._n_calls = 0 return wrapped def filter_tests(test_suite, exclusion_pattern): filtered_test_suite = TestSuite() for item in test_suite: if isinstance(item, TestSuite): filtered = filter_tests(item, exclusion_pattern) filtered_test_suite.addTest(filtered) else: match = re.search(exclusion_pattern, item.id()) if match is not None: skip_msg = "Test excluded via pattern '{}'".format( exclusion_pattern ) setattr(item, 'setUp', lambda: item.skipTest(skip_msg)) filtered_test_suite.addTest(item) return filtered_test_suite def create_ui(object, ui_kwargs=None): """Context manager for creating a UI and then dispose it when exiting the context. Parameters ---------- object : HasTraits An object from which ``edit_traits`` can be called to create a UI ui_kwargs : dict or None Keyword arguments to be provided to ``edit_traits``. Yields ------ ui: UI """ return UITester().create_ui(object=object, ui_kwargs=ui_kwargs) # ######### Utility tools to test on both qt and wx def get_children(node): if is_wx(): return node.GetChildren() else: return node.children() def press_ok_button(ui): """Press the OK button in a wx or qt dialog.""" if is_wx(): import wx ok_button = ui.control.FindWindowByName("button", ui.control) click_event = wx.CommandEvent( wx.wxEVT_COMMAND_BUTTON_CLICKED, ok_button.GetId() ) ok_button.ProcessEvent(click_event) elif is_qt(): from pyface import qt # press the OK button and close the dialog ok_button = ui.control.findChild(qt.QtGui.QPushButton) ok_button.click() def click_button(button): """Click the button given its control.""" if is_wx(): import wx event = wx.CommandEvent(wx.EVT_BUTTON.typeId, button.GetId()) event.SetEventObject(button) wx.PostEvent(button, event) elif is_qt(): button.click() else: raise NotImplementedError() def is_control_enabled(control): """Return if the given control is enabled or not.""" if is_wx(): return control.IsEnabled() elif is_qt(): return control.isEnabled() else: raise NotImplementedError() def get_dialog_size(ui_control): """Return the size of the dialog. Return a tuple (width, height) with the size of the dialog in pixels. E.g.: >>> get_dialog_size(ui.control) """ if is_wx(): return ui_control.GetSize() elif is_qt(): return ui_control.size().width(), ui_control.size().height() def get_all_button_status(control): """Get status of all 2-state (wx) or checkable (qt) buttons under given control. Assumes all sizer children (wx) or layout items (qt) are buttons. """ button_status = [] if is_wx(): for item in control.GetSizer().GetChildren(): button = item.GetWindow() # Ignore empty buttons (assumption that they are invisible) if button.value != "": button_status.append(button.GetValue()) elif is_qt(): layout = control.layout() for i in range(layout.count()): button = layout.itemAt(i).widget() button_status.append(button.isChecked()) else: raise NotImplementedError() return button_status # ######### Debug tools def apply_on_children(func, node, _level=0): """Print the result of applying a function on `node` and its children.""" print("-" * _level + str(node)) print(" " * _level + str(func(node)) + "\n") for child in get_children(node): apply_on_children(func, child, _level + 1) def wx_print_names(node): """Print the name and id of `node` and its children. Use as:: >>> ui = xxx.edit_traits() >>> wx_print_names(ui.control) """ apply_on_children(lambda n: (n.GetName(), n.GetId()), node) def qt_print_names(node): """Print the name of `node` and its children. Use as:: >>> ui = xxx.edit_traits() >>> qt_print_names(ui.control) """ apply_on_children(lambda n: n.objectName(), node) def wx_announce_when_destroyed(node): """Prints a message when `node` is destroyed. Use as: >>> ui = xxx.edit_traits() >>> apply_on_children(wx_announce_when_destroyed, ui.control) """ _destroy_method = node.Destroy def destroy_wrapped(): print("Destroying:", node) # print 'Stack is' # traceback.print_stack() _destroy_method() print("Destroyed:", node) node.Destroy = destroy_wrapped return "Node {} decorated".format(node.GetName()) def wx_find_event_by_number(evt_num): """Find all wx event names that correspond to a certain event number. Example: >>> wx_find_event_by_number(10010) ['wxEVT_COMMAND_MENU_SELECTED', 'wxEVT_COMMAND_TOOL_CLICKED'] """ import wx possible = [ attr for attr in dir(wx) if attr.startswith("wxEVT") and getattr(wx, attr) == evt_num ] return possible GuiTestAssistant = toolkit_object("util.gui_test_assistant:GuiTestAssistant") no_gui_test_assistant = GuiTestAssistant.__name__ == "Unimplemented" if no_gui_test_assistant: # ensure null toolkit has an inheritable GuiTestAssistant class GuiTestAssistant(object): pass class BaseTestMixin: """This is a mixin class for all test cases in TraitsUI, regardless of whether GUI is involved. Not to be used externally. """ def setUp(self): push_exception_handler(reraise_exceptions=True) def tearDown(self): pop_exception_handler() ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1684768028.1158068 traitsui-8.0.0/traitsui/tests/editors/0000755000175100001730000000000000000000000020644 5ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/tests/editors/__init__.py0000644000175100001730000000000000000000000022743 0ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/tests/editors/test_animatedGIF_editor.py0000644000175100001730000000267700000000000025747 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Tests pertaining to the AnimatedGIFEditor """ import unittest import pkg_resources from traits.api import File, HasTraits from traitsui.api import Item, View from traitsui.tests._tools import ( BaseTestMixin, create_ui, requires_toolkit, ToolkitName, ) filename = pkg_resources.resource_filename( "traitsui", "examples/demo/Extras/images/logo_32x32.gif" ) class AnimatedGIF(HasTraits): gif_file = File(filename) @requires_toolkit([ToolkitName.wx]) class TestAnimatedGIFEditor(BaseTestMixin, unittest.TestCase): def setUp(self): BaseTestMixin.setUp(self) def tearDown(self): BaseTestMixin.tearDown(self) def test_animated_gif_editor(self): from traitsui.wx.animated_gif_editor import AnimatedGIFEditor # Regression test for enthought/traitsui#1071 obj1 = AnimatedGIF() view = View( Item('gif_file', editor=AnimatedGIFEditor(playing='playing')), ) # This should not fail. with create_ui(obj1, dict(view=view)): pass ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/tests/editors/test_boolean_editor.py0000644000175100001730000001035600000000000025247 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! import unittest from traits.api import HasTraits, Bool from traitsui.api import BooleanEditor, Item, View from traitsui.tests._tools import ( BaseTestMixin, requires_toolkit, ToolkitName, ) from traitsui.testing.api import ( DisplayedText, IsChecked, KeyClick, KeySequence, MouseClick, UITester, ) class BoolModel(HasTraits): true_or_false = Bool() # Run this against wx once enthought/traitsui#752 is also fixed for # BooleanEditor @requires_toolkit([ToolkitName.qt, ToolkitName.wx]) class TestBooleanEditor(BaseTestMixin, unittest.TestCase): def setUp(self): BaseTestMixin.setUp(self) def tearDown(self): BaseTestMixin.tearDown(self) def check_click_boolean_changes_trait(self, style): view = View(Item("true_or_false", style=style, editor=BooleanEditor())) obj = BoolModel() tester = UITester() with tester.create_ui(obj, dict(view=view)) as ui: # sanity check self.assertEqual(obj.true_or_false, False) checkbox = tester.find_by_name(ui, "true_or_false") checkbox.perform(MouseClick()) self.assertEqual(obj.true_or_false, True) def test_click_boolean_changes_trait_simple(self): self.check_click_boolean_changes_trait('simple') def test_click_boolean_changes_trait_custom(self): self.check_click_boolean_changes_trait('custom') def test_change_text_boolean_changes_trait(self): view = View(Item("true_or_false", style='text')) obj = BoolModel() tester = UITester() with tester.create_ui(obj, dict(view=view)) as ui: self.assertEqual(obj.true_or_false, False) text_field = tester.find_by_name(ui, 'true_or_false') for _ in range(5): text_field.perform(KeyClick("Backspace")) text_field.perform(KeySequence("True")) text_field.perform(KeyClick("Enter")) self.assertEqual(obj.true_or_false, True) def check_trait_change_shown_in_gui(self, style): view = View(Item("true_or_false", style=style)) obj = BoolModel() tester = UITester() with tester.create_ui(obj, dict(view=view)) as ui: checkbox = tester.find_by_name(ui, "true_or_false") checked = checkbox.inspect(IsChecked()) # sanity check self.assertEqual(checked, False) obj.true_or_false = True checked = checkbox.inspect(IsChecked()) self.assertEqual(checked, True) def test_trait_change_shown_in_gui_simple(self): self.check_trait_change_shown_in_gui('simple') def test_trait_change_shown_in_gui_custom(self): self.check_trait_change_shown_in_gui('custom') def test_trait_change_shown_in_gui_readonly(self): view = View(Item("true_or_false", style='readonly')) obj = BoolModel() tester = UITester() with tester.create_ui(obj, dict(view=view)) as ui: readonly = tester.find_by_name(ui, "true_or_false") displayed = readonly.inspect(DisplayedText()) # sanity check self.assertEqual(displayed, 'False') obj.true_or_false = True displayed = readonly.inspect(DisplayedText()) self.assertEqual(displayed, 'True') def test_trait_change_shown_in_gui_text(self): view = View(Item("true_or_false", style='text')) obj = BoolModel() tester = UITester() with tester.create_ui(obj, dict(view=view)) as ui: text_field = tester.find_by_name(ui, "true_or_false") displayed = text_field.inspect(DisplayedText()) # sanity check self.assertEqual(displayed, 'False') obj.true_or_false = True displayed = text_field.inspect(DisplayedText()) self.assertEqual(displayed, 'True') ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/tests/editors/test_button_editor.py0000644000175100001730000001543200000000000025143 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! import unittest from pyface.api import ImageResource from pyface.ui_traits import Image from traits.api import Bool, Button, HasTraits, List, Str from traits.testing.api import UnittestTools from traitsui.api import ButtonEditor, Item, UItem, View import traitsui.extras from traitsui.tests._tools import ( BaseTestMixin, requires_toolkit, reraise_exceptions, ToolkitName, ) from traitsui.testing.api import DisplayedText, IsEnabled, MouseClick, UITester class ButtonTextEdit(HasTraits): play_button = Button("Play") play_button_label = Str("I'm a play button") play_button_image = Image(ImageResource("run", [traitsui.extras])) values = List() button_enabled = Bool(True) traits_view = View( Item("play_button", style="simple"), Item("play_button", style="custom"), Item("play_button", style="readonly"), Item("play_button", style="text"), ) simple_view = View( UItem("play_button", editor=ButtonEditor(label_value="play_button_label")), Item("play_button_label"), resizable=True, ) custom_view = View( UItem("play_button", editor=ButtonEditor(label_value="play_button_label")), Item("play_button_label"), resizable=True, style="custom", ) custom_image_view = View( UItem("play_button", editor=ButtonEditor(image_value="play_button_image")), resizable=True, style="custom", ) @requires_toolkit([ToolkitName.qt, ToolkitName.wx]) class TestButtonEditor(BaseTestMixin, unittest.TestCase, UnittestTools): def setUp(self): BaseTestMixin.setUp(self) def tearDown(self): BaseTestMixin.tearDown(self) def check_button_text_update(self, view): button_text_edit = ButtonTextEdit() tester = UITester() with tester.create_ui(button_text_edit, dict(view=view)) as ui: button = tester.find_by_name(ui, "play_button") actual = button.inspect(DisplayedText()) self.assertEqual(actual, "I'm a play button") button_text_edit.play_button_label = "New Label" actual = button.inspect(DisplayedText()) self.assertEqual(actual, "New Label") def test_styles(self): # simple smoke test of buttons button_text_edit = ButtonTextEdit() with UITester().create_ui(button_text_edit): pass def test_simple_button_editor(self): self.check_button_text_update(simple_view) # this currently fails on wx, see enthought/traitsui#1654 @requires_toolkit([ToolkitName.qt]) def test_custom_button_editor(self): self.check_button_text_update(custom_view) def check_button_fired_event(self, view): button_text_edit = ButtonTextEdit() tester = UITester() with tester.create_ui(button_text_edit, dict(view=view)) as ui: button = tester.find_by_name(ui, "play_button") with self.assertTraitChanges( button_text_edit, "play_button", count=1 ): button.perform(MouseClick()) def test_simple_button_editor_clicked(self): self.check_button_fired_event(simple_view) def test_custom_button_editor_clicked(self): self.check_button_fired_event(custom_view) def check_button_disabled(self, style): button_text_edit = ButtonTextEdit( button_enabled=False, ) view = View( Item( "play_button", editor=ButtonEditor(), enabled_when="button_enabled", style=style, ), ) tester = UITester() with tester.create_ui(button_text_edit, dict(view=view)) as ui: button = tester.find_by_name(ui, "play_button") self.assertFalse(button.inspect(IsEnabled())) with self.assertTraitDoesNotChange( button_text_edit, "play_button" ): button.perform(MouseClick()) button_text_edit.button_enabled = True self.assertTrue(button.inspect(IsEnabled())) with self.assertTraitChanges( button_text_edit, "play_button", count=1 ): button.perform(MouseClick()) def test_simple_button_editor_disabled(self): self.check_button_disabled("simple") def test_custom_button_editor_disabled(self): self.check_button_disabled("custom") def test_custom_image_value(self): button_text_edit = ButtonTextEdit() tester = UITester() with tester.create_ui( button_text_edit, dict(view=custom_image_view) ) as ui: button = tester.find_by_name(ui, "play_button") default_image = button._target.image self.assertIsInstance(default_image, ImageResource) button_text_edit.play_button_image = ImageResource( 'next', [traitsui.extras] ) self.assertIsInstance(button._target.image, ImageResource) self.assertIsNot(button._target.image, default_image) @requires_toolkit([ToolkitName.qt]) class TestButtonEditorValuesTrait(BaseTestMixin, unittest.TestCase): """The values_trait is only supported by Qt. See discussion enthought/traitsui#879 """ def setUp(self): BaseTestMixin.setUp(self) def tearDown(self): BaseTestMixin.tearDown(self) def get_view(self, style): return View( Item( "play_button", editor=ButtonEditor(values_trait="values"), style=style, ), ) def check_editor_values_trait_init_and_dispose(self, style): # Smoke test to check init and dispose when values_trait is used. instance = ButtonTextEdit(values=["Item1", "Item2"]) view = self.get_view(style=style) with reraise_exceptions(): with UITester().create_ui(instance, dict(view=view)): pass # It is okay to mutate trait after the GUI is disposed. instance.values = ["Item3"] def test_simple_editor_values_trait_init_and_dispose(self): # Smoke test to check init and dispose when values_trait is used. self.check_editor_values_trait_init_and_dispose(style="simple") def test_custom_editor_values_trait_init_and_dispose(self): # Smoke test to check init and dispose when values_trait is used. self.check_editor_values_trait_init_and_dispose(style="custom") ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/tests/editors/test_check_list_editor.py0000644000175100001730000004305700000000000025744 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! import contextlib import unittest from traits.api import HasTraits, List, Str from traitsui.api import CheckListEditor, UItem, View from traitsui.tests._tools import ( BaseTestMixin, create_ui, get_all_button_status, is_qt, is_wx, process_cascade_events, requires_toolkit, reraise_exceptions, ToolkitName, ) from traitsui.testing.api import Index, MouseClick, UITester class ListModel(HasTraits): value = List() def get_view(style): return View( UItem( "value", editor=CheckListEditor( values=["one", "two", "three", "four"], ), style=style, ), resizable=True, ) def get_view_custom_cols(cols): return View( UItem( "value", editor=CheckListEditor( values=["one", "two", "three", "four", "five", "six", "seven"], cols=cols, ), style="custom", ), resizable=True, ) def get_mapped_view(style): return View( UItem( "value", editor=CheckListEditor( values=[(1, "one"), (2, "two"), (3, "three"), (4, "four")], ), style=style, ), resizable=True, ) def get_combobox_text(combobox): """Return the text given a combobox control.""" if is_wx(): return combobox.GetString(combobox.GetSelection()) elif is_qt(): return combobox.currentText() else: raise unittest.SkipTest("Test not implemented for this toolkit") def set_combobox_index(editor, idx): """Set the choice index of a combobox control given editor and index number.""" if is_wx(): import wx choice = editor.control choice.SetSelection(idx) event = wx.CommandEvent(wx.EVT_CHOICE.typeId, choice.GetId()) event.SetString(choice.GetString(idx)) wx.PostEvent(choice, event) elif is_qt(): # Cannot initiate update programatically because of `activated` # event. At least check that it updates as expected when done # manually editor.update_object(idx) else: raise unittest.SkipTest("Test not implemented for this toolkit") def set_text_in_line_edit(line_edit, text): """Set text in text widget and complete editing.""" if is_wx(): import wx line_edit.SetValue(text) event = wx.CommandEvent(wx.EVT_TEXT_ENTER.typeId, line_edit.GetId()) wx.PostEvent(line_edit, event) elif is_qt(): line_edit.setText(text) line_edit.editingFinished.emit() else: raise unittest.SkipTest("Test not implemented for this toolkit") @requires_toolkit([ToolkitName.qt, ToolkitName.wx]) class TestCheckListEditorMapping(BaseTestMixin, unittest.TestCase): def setUp(self): BaseTestMixin.setUp(self) def tearDown(self): BaseTestMixin.tearDown(self) @contextlib.contextmanager def setup_ui(self, model, view): with create_ui(model, dict(view=view)) as ui: yield ui.get_editors("value")[0] def check_checklist_mappings_value_change(self, style): check_list_editor_factory = CheckListEditor( values=["one", "two"], format_func=lambda v: v.upper(), ) formatted_view = View( UItem( "value", editor=check_list_editor_factory, style=style, ) ) model = ListModel() with reraise_exceptions(), self.setup_ui( model, formatted_view ) as editor: self.assertEqual(editor.names, ["ONE", "TWO"]) check_list_editor_factory.values = ["two", "one"] self.assertEqual(editor.names, ["TWO", "ONE"]) def check_checklist_mappings_tuple_value_change(self, style): check_list_editor_factory = CheckListEditor( values=[(1, "one"), (2, "two")], format_func=lambda t: t[1].upper(), ) formatted_view = View( UItem( "value", editor=check_list_editor_factory, style=style, ) ) model = ListModel() with reraise_exceptions(), self.setup_ui( model, formatted_view ) as editor: # FIXME issue enthought/traitsui#841 with self.assertRaises(AssertionError): self.assertEqual(editor.names, ["ONE", "TWO"]) self.assertEqual(editor.names, ["one", "two"]) check_list_editor_factory.values = [(2, "two"), (1, "one")] # FIXME issue enthought/traitsui#841 with self.assertRaises(AssertionError): self.assertEqual(editor.names, ["TWO", "ONE"]) self.assertEqual(editor.names, ["two", "one"]) def check_checklist_mappings_name_change(self, style): class ListModel(HasTraits): value = List() possible_values = List(["one", "two"]) check_list_editor_factory = CheckListEditor( name="object.possible_values", format_func=lambda v: v.upper(), ) formatted_view = View( UItem( "value", editor=check_list_editor_factory, style=style, ) ) model = ListModel() with reraise_exceptions(), self.setup_ui( model, formatted_view ) as editor: self.assertEqual(editor.names, ["ONE", "TWO"]) model.possible_values = ["two", "one"] self.assertEqual(editor.names, ["TWO", "ONE"]) def check_checklist_mappings_tuple_name_change(self, style): class ListModel(HasTraits): value = List() possible_values = List([(1, "one"), (2, "two")]) check_list_editor_factory = CheckListEditor( name="object.possible_values", format_func=lambda t: t[1].upper(), ) formatted_view = View( UItem( "value", editor=check_list_editor_factory, style=style, ) ) model = ListModel() with reraise_exceptions(), self.setup_ui( model, formatted_view ) as editor: # FIXME issue enthought/traitsui#841 with self.assertRaises(AssertionError): self.assertEqual(editor.names, ["ONE", "TWO"]) self.assertEqual(editor.names, ["one", "two"]) model.possible_values = [(2, "two"), (1, "one")] # FIXME issue enthought/traitsui#841 with self.assertRaises(AssertionError): self.assertEqual(editor.names, ["TWO", "ONE"]) self.assertEqual(editor.names, ["two", "one"]) def check_checklist_values_change_after_ui_dispose(self, style): # Check the available values can change after the ui is closed class ListModel(HasTraits): value = List() possible_values = List(["one", "two"]) check_list_editor_factory = CheckListEditor( name="object.possible_values", ) view = View( UItem( "value", editor=check_list_editor_factory, style=style, ) ) model = ListModel() with reraise_exceptions(): with create_ui(model, dict(view=view)): pass model.possible_values = ["two", "one"] def test_simple_editor_mapping_values(self): self.check_checklist_mappings_value_change("simple") def test_simple_editor_mapping_values_tuple(self): self.check_checklist_mappings_tuple_value_change("simple") def test_simple_editor_mapping_name(self): self.check_checklist_mappings_name_change("simple") def test_simple_editor_mapping_name_tuple(self): self.check_checklist_mappings_tuple_name_change("simple") def test_custom_editor_mapping_values(self): # FIXME issue enthought/traitsui#842 if is_wx(): import wx with self.assertRaises(wx._core.wxAssertionError): self.check_checklist_mappings_value_change("custom") else: self.check_checklist_mappings_value_change("custom") def test_custom_editor_mapping_values_tuple(self): # FIXME issue enthought/traitsui#842 if is_wx(): import wx with self.assertRaises(wx._core.wxAssertionError): self.check_checklist_mappings_tuple_value_change("custom") else: self.check_checklist_mappings_tuple_value_change("custom") def test_custom_editor_mapping_name(self): # FIXME issue enthought/traitsui#842 if is_wx(): import wx with self.assertRaises(wx._core.wxAssertionError): self.check_checklist_mappings_name_change("custom") else: self.check_checklist_mappings_name_change("custom") def test_custom_editor_mapping_name_tuple(self): # FIXME issue enthought/traitsui#842 if is_wx(): import wx with self.assertRaises(wx._core.wxAssertionError): self.check_checklist_mappings_tuple_name_change("custom") else: self.check_checklist_mappings_tuple_name_change("custom") def test_simple_editor_checklist_values_change_dispose(self): self.check_checklist_values_change_after_ui_dispose("simple") def test_custom_editor_checklist_values_change_dispose(self): self.check_checklist_values_change_after_ui_dispose("custom") @requires_toolkit([ToolkitName.qt, ToolkitName.wx]) class TestSimpleCheckListEditor(BaseTestMixin, unittest.TestCase): def setUp(self): BaseTestMixin.setUp(self) def tearDown(self): BaseTestMixin.tearDown(self) @contextlib.contextmanager def setup_gui(self, model, view): with create_ui(model, dict(view=view)) as ui: process_cascade_events() editor = ui.get_editors("value")[0] yield editor def test_simple_check_list_editor_text(self): list_edit = ListModel(value=["one"]) with reraise_exceptions(), self.setup_gui( list_edit, get_view("simple") ) as editor: self.assertEqual(get_combobox_text(editor.control), "One") list_edit.value = ["two"] process_cascade_events() self.assertEqual(get_combobox_text(editor.control), "Two") def test_simple_check_list_editor_text_mapped(self): view = get_mapped_view("simple") list_edit = ListModel(value=[1]) with reraise_exceptions(), self.setup_gui(list_edit, view) as editor: # FIXME issue enthought/traitsui#841 with self.assertRaises(AssertionError): self.assertEqual(get_combobox_text(editor.control), "One") self.assertEqual(get_combobox_text(editor.control), "one") list_edit.value = [2] process_cascade_events() # FIXME issue enthought/traitsui#841 with self.assertRaises(AssertionError): self.assertEqual(get_combobox_text(editor.control), "Two") self.assertEqual(get_combobox_text(editor.control), "two") def test_simple_check_list_editor_index(self): list_edit = ListModel(value=["one"]) with reraise_exceptions(), self.setup_gui( list_edit, get_view("simple") ) as editor: self.assertEqual(list_edit.value, ["one"]) set_combobox_index(editor, 1) process_cascade_events() self.assertEqual(list_edit.value, ["two"]) set_combobox_index(editor, 0) process_cascade_events() self.assertEqual(list_edit.value, ["one"]) def test_simple_check_list_editor_invalid_current_values(self): list_edit = ListModel(value=[1, "two", "a", object(), "one"]) with reraise_exceptions(), self.setup_gui( list_edit, get_view("simple") ): self.assertEqual(list_edit.value, ["two", "one"]) def test_simple_check_list_editor_invalid_current_values_str(self): class StrModel(HasTraits): value = Str() str_edit = StrModel(value="alpha, \ttwo, beta,\n lambda, one") with reraise_exceptions(), self.setup_gui( str_edit, get_view("simple") ): self.assertEqual(str_edit.value, "two,one") @requires_toolkit([ToolkitName.qt, ToolkitName.wx]) class TestCustomCheckListEditor(BaseTestMixin, unittest.TestCase): def setUp(self): BaseTestMixin.setUp(self) def tearDown(self): BaseTestMixin.tearDown(self) @contextlib.contextmanager def setup_gui(self, model, view): with create_ui(model, dict(view=view)) as ui: process_cascade_events() editor = ui.get_editors("value")[0] yield editor def test_custom_check_list_editor_button_update(self): list_edit = ListModel() with reraise_exceptions(), self.setup_gui( list_edit, get_view("custom") ) as editor: self.assertEqual( get_all_button_status(editor.control), [False, False, False, False], ) list_edit.value = ["two", "four"] process_cascade_events() self.assertEqual( get_all_button_status(editor.control), [False, True, False, True], ) list_edit.value = ["one", "four"] process_cascade_events() self.assertEqual( get_all_button_status(editor.control), [True, False, False, True], ) def test_custom_check_list_editor_click(self): list_edit = ListModel() tester = UITester() with tester.create_ui(list_edit, dict(view=get_view("custom"))) as ui: self.assertEqual(list_edit.value, []) check_list = tester.find_by_name(ui, "value") item_1 = check_list.locate(Index(1)) item_1.perform(MouseClick()) self.assertEqual(list_edit.value, ["two"]) item_1.perform(MouseClick()) self.assertEqual(list_edit.value, []) def test_custom_check_list_editor_click_initial_value(self): list_edit = ListModel(value=["two"]) tester = UITester() with tester.create_ui(list_edit, dict(view=get_view("custom"))) as ui: self.assertEqual(list_edit.value, ["two"]) check_list = tester.find_by_name(ui, "value") item_1 = check_list.locate(Index(1)) item_1.perform(MouseClick()) self.assertEqual(list_edit.value, []) def test_custom_check_list_editor_invalid_current_values_str(self): class StrModel(HasTraits): value = Str() str_edit = StrModel(value="alpha, \ttwo, three,\n lambda, one") tester = UITester() with tester.create_ui(str_edit, dict(view=get_view("custom"))) as ui: self.assertEqual(str_edit.value, "two,three,one") check_list = tester.find_by_name(ui, "value") item_1 = check_list.locate(Index(1)) item_1.perform(MouseClick()) self.assertEqual(str_edit.value, "three,one") def test_custom_check_list_editor_grid_layout(self): for cols in range(1, 8): list_edit = ListModel() tester = UITester() view = get_view_custom_cols(cols=cols) with tester.create_ui(list_edit, dict(view=view)) as ui: self.assertEqual(list_edit.value, []) check_list = tester.find_by_name(ui, "value") item = check_list.locate(Index(6)) item.perform(MouseClick()) self.assertEqual(list_edit.value, ["seven"]) item.perform(MouseClick()) self.assertEqual(list_edit.value, []) @requires_toolkit([ToolkitName.qt, ToolkitName.wx]) class TestTextCheckListEditor(BaseTestMixin, unittest.TestCase): def setUp(self): BaseTestMixin.setUp(self) def tearDown(self): BaseTestMixin.tearDown(self) @contextlib.contextmanager def setup_gui(self, model, view): with create_ui(model, dict(view=view)) as ui: process_cascade_events() editor = ui.get_editors("value")[0] yield editor def test_text_check_list_object_list(self): list_edit = ListModel() with reraise_exceptions(), self.setup_gui( list_edit, get_view("text") ) as editor: self.assertEqual(list_edit.value, []) set_text_in_line_edit(editor.control, "['one', 'two']") process_cascade_events() self.assertEqual(list_edit.value, ["one", "two"]) def test_text_check_list_object_str(self): class StrModel(HasTraits): value = Str() str_edit = StrModel(value="three, four") with reraise_exceptions(), self.setup_gui( str_edit, get_view("text") ) as editor: self.assertEqual(str_edit.value, "three, four") set_text_in_line_edit(editor.control, "one, two") process_cascade_events() self.assertEqual(str_edit.value, "one, two") ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/tests/editors/test_code_editor.py0000644000175100001730000000634500000000000024545 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! import unittest from traits.has_traits import HasTraits from traits.trait_types import Bool, Enum, Instance, Str from traitsui.handler import ModelView from traitsui.view import View from traitsui.item import Item from traitsui.editors.code_editor import CodeEditor from traitsui.tests._tools import ( BaseTestMixin, create_ui, requires_toolkit, reraise_exceptions, ToolkitName, ) class CodeModel(HasTraits): code = Str("world domination code") class CodeView(ModelView): model = Instance(CodeModel) show_line_numbers = Bool(True) style = Enum("simple", "readonly") def default_traits_view(self): traits_view = View( Item( "model.code", editor=CodeEditor(show_line_numbers=self.show_line_numbers), style=self.style, ) ) return traits_view class TestCodeEditor(BaseTestMixin, unittest.TestCase): def setUp(self): BaseTestMixin.setUp(self) def tearDown(self): BaseTestMixin.tearDown(self) @requires_toolkit([ToolkitName.qt]) def test_code_editor_show_line_numbers(self): """CodeEditor should honor the `show_line_numbers` setting""" def is_line_numbers_visible(ui): from pyface import qt txt_ctrl = ui.control.findChild(qt.QtGui.QPlainTextEdit) return txt_ctrl.line_number_widget.isVisible() def test_line_numbers_visibility(show=True): code_model = CodeModel() code_view = CodeView(model=code_model, show_line_numbers=show) with reraise_exceptions(), create_ui(code_view) as ui: self.assertEqual(is_line_numbers_visible(ui), show) ui.control.close() test_line_numbers_visibility(True) test_line_numbers_visibility(False) @requires_toolkit([ToolkitName.qt]) def test_code_editor_readonly(self): """Test readonly editor style for CodeEditor""" from pyface import qt code_model = CodeModel() code_view = CodeView(model=code_model, style="readonly") with reraise_exceptions(), create_ui(code_view) as ui: txt_ctrl = ui.control.findChild(qt.QtGui.QPlainTextEdit) self.assertTrue(txt_ctrl.isReadOnly()) # Test changing the object's text self.assertEqual(txt_ctrl.toPlainText(), code_model.code) code_model.code += "some more code" self.assertTrue(txt_ctrl.isReadOnly()) self.assertEqual(txt_ctrl.toPlainText(), code_model.code) # Test changing the underlying object code_model2 = CodeModel(code=code_model.code * 2) code_view.model = code_model2 self.assertTrue(txt_ctrl.isReadOnly()) self.assertEqual(txt_ctrl.toPlainText(), code_model.code) ui.control.close() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/tests/editors/test_csv_editor.py0000644000175100001730000000730400000000000024422 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! import unittest from traits.has_traits import HasTraits from traits.trait_types import Float, List, Instance from traitsui.handler import ModelView from traitsui.view import View from traitsui.item import Item from traitsui.editors.csv_list_editor import CSVListEditor import traitsui.editors.csv_list_editor as csv_list_editor from traitsui.tests._tools import ( BaseTestMixin, create_ui, is_wx, is_qt, requires_toolkit, reraise_exceptions, ToolkitName, ) class ListOfFloats(HasTraits): data = List(Float) class ListOfFloatsWithCSVEditor(ModelView): model = Instance(ListOfFloats) traits_view = View( Item(label="Close the window to append data"), Item("model.data", editor=CSVListEditor()), buttons=["OK"], ) class TestCSVEditor(BaseTestMixin, unittest.TestCase): def setUp(self): BaseTestMixin.setUp(self) def tearDown(self): BaseTestMixin.tearDown(self) @requires_toolkit([ToolkitName.qt, ToolkitName.wx]) def test_csv_editor_disposal(self): # Bug: CSVListEditor does not un-hook the traits notifications after # its disposal, causing errors when the hooked data is accessed after # the window is closed (Issue #48) list_of_floats = ListOfFloats(data=[1, 2, 3]) csv_view = ListOfFloatsWithCSVEditor(model=list_of_floats) try: with reraise_exceptions(): with create_ui(csv_view): pass # raise an exception if still hooked list_of_floats.data.append(2) except AttributeError: # if all went well, we should not be here self.fail("AttributeError raised") @requires_toolkit([ToolkitName.qt, ToolkitName.wx]) def test_csv_editor_external_append(self): # Behavior: CSV editor is notified when an element is appended to the # list externally def _wx_get_text_value(ui): txt_ctrl = ui.control.FindWindowByName("text", ui.control) return txt_ctrl.GetValue() def _qt_get_text_value(ui): from pyface import qt txt_ctrl = ui.control.findChild(qt.QtGui.QLineEdit) return txt_ctrl.text() list_of_floats = ListOfFloats(data=[1.0]) csv_view = ListOfFloatsWithCSVEditor(model=list_of_floats) with reraise_exceptions(), create_ui(csv_view) as ui: # add element to list, make sure that editor knows about it list_of_floats.data.append(3.14) # get current value from editor if is_wx(): value_str = _wx_get_text_value(ui) elif is_qt(): value_str = _qt_get_text_value(ui) expected = csv_list_editor._format_list_str([1.0, 3.14]) self.assertEqual(value_str, expected) if __name__ == "__main__": # Executing the file opens the dialog for manual testing list_of_floats = ListOfFloats(data=[1, 2, 3]) csv_view = ListOfFloatsWithCSVEditor(model=list_of_floats) csv_view.configure_traits() # this call will raise an AttributeError in commit # 4ecb2fa8f0ef385d55a2a4062d821b0415777973 # This is because the editor does not un-hook the traits notifications # after its disposal list_of_floats.data.append(2) print(list_of_floats.data) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/tests/editors/test_date_editor.py0000644000175100001730000001451000000000000024541 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! import contextlib import datetime import unittest from traits.api import Date, HasTraits, List from traitsui.api import DateEditor, View, Item from traitsui.editors.date_editor import CellFormat from traitsui.tests._tools import ( BaseTestMixin, create_ui, requires_toolkit, reraise_exceptions, ToolkitName, ) class Foo(HasTraits): dates = List(Date) single_date = Date() def single_select_custom_view(): view = View( Item( name="single_date", style="custom", editor=DateEditor(multi_select=False), ) ) return view def multi_select_custom_view(): view = View( Item( name="dates", style="custom", editor=DateEditor(multi_select=True) ) ) return view def multi_select_selected_color_view(): view = View( Item( name="dates", style="custom", editor=DateEditor( multi_select=True, selected_style=CellFormat(bold=True, bgcolor=(128, 10, 0)), ), ) ) return view @requires_toolkit([ToolkitName.qt]) class TestDateEditorCustomQt(BaseTestMixin, unittest.TestCase): def setUp(self): BaseTestMixin.setUp(self) def tearDown(self): BaseTestMixin.tearDown(self) def test_single_select_qt(self): with self.launch_editor(single_select_custom_view) as (foo, editor): date = datetime.date(2018, 2, 3) self.click_date_on_editor(editor, date) self.assertEqual(foo.single_date, date) def test_multi_select_dates_on_editor(self): with self.launch_editor(multi_select_custom_view) as (foo, editor): dates = [datetime.date(2018, 2, 3), datetime.date(2018, 2, 1)] for date in dates: self.click_date_on_editor(editor, date) for date in dates: self.check_select_status( editor=editor, date=date, selected=True ) self.assertEqual(foo.dates, sorted(dates)) def test_multi_select_qt_styles_reset(self): with self.launch_editor(multi_select_custom_view) as (foo, editor): date = datetime.date(2018, 2, 1) self.click_date_on_editor(editor, date) self.check_select_status(editor=editor, date=date, selected=True) self.click_date_on_editor(editor, date) self.check_select_status(editor=editor, date=date, selected=False) def test_multi_select_qt_set_model_dates(self): # Test setting the dates from the model object. with self.launch_editor(multi_select_custom_view) as (foo, editor): foo.dates = [datetime.date(2010, 1, 2), datetime.date(2010, 2, 1)] for date in foo.dates: self.check_select_status( editor=editor, date=date, selected=True ) def test_custom_selected_color(self): view_factory = multi_select_selected_color_view with self.launch_editor(view_factory) as (foo, editor): date = datetime.date(2011, 3, 4) foo.dates = [date] self.check_date_bgcolor(editor, date, (128, 10, 0)) # -------------------- # Helper methods # -------------------- @contextlib.contextmanager def launch_editor(self, view_factory): foo = Foo() with create_ui(foo, dict(view=view_factory())) as ui: (editor,) = ui._editors yield foo, editor def check_select_status(self, editor, date, selected): from pyface.qt import QtCore, QtGui qdate = QtCore.QDate(date.year, date.month, date.day) textformat = editor.control.dateTextFormat(qdate) if selected: self.assertEqual( textformat.fontWeight(), QtGui.QFont.Weight.Bold, "{!r} is not selected.".format(date), ) self.check_date_bgcolor(editor, date, (0, 128, 0)) else: self.assertEqual( textformat.fontWeight(), QtGui.QFont.Weight.Normal, "{!r} is not unselected.".format(date), ) self.assertEqual( textformat.background().style(), QtCore.Qt.BrushStyle.NoBrush, "Expected brush to have been reset.", ) self.check_date_bgcolor(editor, date, (0, 0, 0)) def click_date_on_editor(self, editor, date): from pyface.qt import QtCore # QCalendarWidget.setSelectedDate modifies internal state # instead of triggering the click signal. # So we call update_object directly editor.update_object(QtCore.QDate(date.year, date.month, date.day)) def check_date_bgcolor(self, editor, date, expected): from pyface.qt import QtCore qdate = QtCore.QDate(date.year, date.month, date.day) textformat = editor.control.dateTextFormat(qdate) color = textformat.background().color() actual = (color.red(), color.green(), color.blue()) self.assertEqual( actual, expected, "Expected color: {!r}. Got color: {!r}".format(expected, actual), ) # Run this test case against wx too once enthought/traitsui#752 is fixed. @requires_toolkit([ToolkitName.qt]) class TestDateEditorInitDispose(unittest.TestCase): """Test the init and dispose of date editor.""" def check_init_and_dispose(self, view): with reraise_exceptions(), create_ui(Foo(), dict(view=view)): pass def test_simple_date_editor(self): view = View( Item( name="single_date", style="simple", ) ) self.check_init_and_dispose(view) def test_custom_date_editor(self): view = View( Item( name="single_date", style="custom", ) ) self.check_init_and_dispose(view) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/tests/editors/test_date_range_editor.py0000644000175100001730000002110600000000000025714 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! import contextlib import datetime import unittest from traits.api import ( Date, HasTraits, TraitError, Tuple, ) from traitsui.api import DateRangeEditor, View, Item from traitsui.tests._tools import ( BaseTestMixin, create_ui, requires_toolkit, ToolkitName, ) class Foo(HasTraits): date_range = Tuple(Date(allow_none=True), Date(allow_none=True)) def default_custom_view(): """Default view of DateRangeEditor""" view = View( Item(name="date_range", style="custom", editor=DateRangeEditor()) ) return view def custom_view_allow_no_range(): """DateRangeEditor with allow_no_selection set to True.""" view = View( Item( name="date_range", style="custom", editor=DateRangeEditor(allow_no_selection=True), ) ) return view class TestDateRangeEditorGeneric(BaseTestMixin, unittest.TestCase): """Tests that are not GUI backend specific.""" def setUp(self): BaseTestMixin.setUp(self) def tearDown(self): BaseTestMixin.tearDown(self) def test_date_range_multi_select_is_constant(self): with self.assertRaises(TraitError): DateRangeEditor(multi_select=False) @requires_toolkit([ToolkitName.qt]) class TestDateRangeEditorQt(BaseTestMixin, unittest.TestCase): def setUp(self): BaseTestMixin.setUp(self) def tearDown(self): BaseTestMixin.tearDown(self) def test_set_date_range_on_model(self): # Test when the date range is set on the model with self.launch_editor(default_custom_view) as (foo, editor): foo.date_range = ( datetime.date(2010, 4, 3), datetime.date(2010, 4, 5), ) expected_selected = [ datetime.date(2010, 4, 3), datetime.date(2010, 4, 4), datetime.date(2010, 4, 5), ] for date in expected_selected: self.check_select_status( editor=editor, date=date, selected=True ) # Outside of the range self.check_select_status( editor=editor, date=datetime.date(2010, 4, 2), selected=False ) self.check_select_status( editor=editor, date=datetime.date(2010, 4, 6), selected=False ) def test_set_one_sided_range_error(self): with self.launch_editor(default_custom_view) as (_, editor): with self.assertRaises(ValueError) as exception_context: editor.value = (datetime.date(2010, 10, 3), None) editor.update_editor() self.assertIn( "The start and end dates must be either both defined or both " "be None", str(exception_context.exception), ) def test_set_reverse_range_on_model(self): with self.launch_editor(default_custom_view) as (foo, editor): foo.date_range = ( datetime.date(2010, 5, 3), datetime.date(2010, 5, 1), ) # None of these dates should be selected dates = [ datetime.date(2010, 5, 1), datetime.date(2010, 5, 2), datetime.date(2010, 5, 3), ] for date in dates: self.check_select_status( editor=editor, date=date, selected=False ) def test_set_date_range_on_editor(self): with self.launch_editor(default_custom_view) as (foo, editor): self.click_date_on_editor(editor, datetime.date(2012, 3, 4)) self.click_date_on_editor(editor, datetime.date(2012, 3, 6)) expected_selected = [ datetime.date(2012, 3, 4), datetime.date(2012, 3, 5), datetime.date(2012, 3, 6), ] for date in expected_selected: self.check_select_status( editor=editor, date=date, selected=True ) self.assertEqual( foo.date_range, (datetime.date(2012, 3, 4), datetime.date(2012, 3, 6)), ) def test_set_date_range_reset_when_click_outside(self): with self.launch_editor(default_custom_view) as (foo, editor): foo.date_range = ( datetime.date(2012, 2, 10), datetime.date(2012, 2, 13), ) self.click_date_on_editor(editor, datetime.date(2012, 2, 4)) self.assertEqual( foo.date_range, (datetime.date(2012, 2, 4), datetime.date(2012, 2, 4)), ) def test_set_date_range_reverse_order(self): # Test setting end date first then start date. with self.launch_editor(default_custom_view) as (foo, editor): self.click_date_on_editor(editor, datetime.date(2012, 3, 6)) self.click_date_on_editor(editor, datetime.date(2012, 3, 4)) expected_selected = [ datetime.date(2012, 3, 4), datetime.date(2012, 3, 5), datetime.date(2012, 3, 6), ] for date in expected_selected: self.check_select_status( editor=editor, date=date, selected=True ) self.assertEqual( foo.date_range, (datetime.date(2012, 3, 4), datetime.date(2012, 3, 6)), ) def test_allow_no_range(self): # Test clicking again will unset the range if allow_no_range is True with self.launch_editor(custom_view_allow_no_range) as (foo, editor): self.click_date_on_editor(editor, datetime.date(2012, 3, 6)) self.click_date_on_editor(editor, datetime.date(2012, 3, 4)) expected_selected = [ datetime.date(2012, 3, 4), datetime.date(2012, 3, 5), datetime.date(2012, 3, 6), ] for date in expected_selected: self.check_select_status( editor=editor, date=date, selected=True ) # Click again self.click_date_on_editor(editor, datetime.date(2012, 3, 1)) for date in expected_selected: self.check_select_status( editor=editor, date=date, selected=False ) self.assertEqual(foo.date_range, (None, None)) # -------------------- # Helper methods # -------------------- @contextlib.contextmanager def launch_editor(self, view_factory): foo = Foo() with create_ui(foo, dict(view=view_factory())) as ui: (editor,) = ui._editors yield foo, editor def check_select_status(self, editor, date, selected): from pyface.qt import QtCore, QtGui qdate = QtCore.QDate(date.year, date.month, date.day) textformat = editor.control.dateTextFormat(qdate) if selected: self.assertEqual( textformat.fontWeight(), QtGui.QFont.Weight.Bold, "{!r} is not selected.".format(date), ) self.assertEqual( textformat.background().color().green(), 128, "Expected non-zero green color value.", ) else: self.assertEqual( textformat.fontWeight(), QtGui.QFont.Weight.Normal, "{!r} is not unselected.".format(date), ) self.assertEqual( textformat.background().style(), QtCore.Qt.BrushStyle.NoBrush, "Expected brush to have been reset.", ) self.assertEqual( textformat.background().color().green(), 0, "Expected color to have been reset.", ) def click_date_on_editor(self, editor, date): from pyface.qt import QtCore # QCalendarWidget.setSelectedDate modifies internal state # instead of triggering the click signal. # So we call update_object directly editor.update_object(QtCore.QDate(date.year, date.month, date.day)) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/tests/editors/test_datetime_editor.py0000644000175100001730000002571300000000000025427 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! import contextlib import datetime import unittest from traits.api import HasTraits, Datetime from traitsui.api import DatetimeEditor, Item, View from traitsui.tests._tools import ( BaseTestMixin, create_ui, GuiTestAssistant, process_cascade_events, requires_toolkit, reraise_exceptions, ToolkitName, no_gui_test_assistant, ) class InstanceWithDatetime(HasTraits): """Demo class to show Datetime editors.""" date_time = Datetime(allow_none=True) def to_datetime(q_datetime): try: return q_datetime.toPyDateTime() except AttributeError: return q_datetime.toPython() def get_date_time_simple_view(editor_factory): view = View( Item( name="date_time", style="simple", editor=editor_factory, ) ) return view @requires_toolkit([ToolkitName.qt]) @unittest.skipIf(no_gui_test_assistant, "No GuiTestAssistant") class TestDatetimeEditorQt(BaseTestMixin, GuiTestAssistant, unittest.TestCase): """Tests for DatetimeEditor using Qt.""" def setUp(self): BaseTestMixin.setUp(self) GuiTestAssistant.setUp(self) def tearDown(self): GuiTestAssistant.tearDown(self) BaseTestMixin.tearDown(self) def test_datetime_editor_simple(self): view = get_date_time_simple_view(DatetimeEditor()) date_time = datetime.datetime(2000, 1, 2, 1, 2, 3) instance = InstanceWithDatetime(date_time=date_time) with reraise_exceptions(), self.launch_editor(instance, view): pass def test_datetime_editor_simple_with_minimum_datetime(self): # Test the editor uses the minimum datetime as expected minimum_datetime = datetime.datetime(2000, 1, 1) editor_factory = DatetimeEditor( minimum_datetime=minimum_datetime, ) view = get_date_time_simple_view(editor_factory) instance = InstanceWithDatetime() with reraise_exceptions(), self.launch_editor( instance, view ) as editor: q_minimum_datetime = editor.control.minimumDateTime() actual_minimum_datetime = to_datetime(q_minimum_datetime) self.assertEqual(actual_minimum_datetime, minimum_datetime) def test_datetime_editor_simple_with_minimum_datetime_out_of_bound(self): # Test when out-of-bound value is given, it is reset to the bounded # value. minimum_datetime = datetime.datetime(2000, 1, 1) editor_factory = DatetimeEditor( minimum_datetime=minimum_datetime, ) view = get_date_time_simple_view(editor_factory) instance = InstanceWithDatetime() with reraise_exceptions(), self.launch_editor( instance, view ) as editor: instance.date_time = datetime.datetime(1980, 1, 1) # does not seem needed to flush the event loop, but just in case. process_cascade_events() displayed_value = to_datetime(editor.control.dateTime()) self.assertEqual(displayed_value, minimum_datetime) self.assertEqual(instance.date_time, minimum_datetime) def test_datetime_editor_mutate_minimum_datetime_after_init(self): # Test if the minimum_datetime on the editor is mutated after init, # the values on the edited object and the view are synchronized. editor_factory = DatetimeEditor( minimum_datetime=datetime.datetime(2000, 1, 1), ) view = get_date_time_simple_view(editor_factory) instance = InstanceWithDatetime() with reraise_exceptions(), self.launch_editor( instance, view ) as editor: # This value is in-range instance.date_time = datetime.datetime(2001, 1, 1) # Now change the editor's minimum datetime such that the original # value is out-of-range new_minimum_datetime = datetime.datetime(2010, 1, 1) editor.minimum_datetime = new_minimum_datetime # does not seem needed to flush the event loop, but just in case. process_cascade_events() displayed_value = to_datetime(editor.control.dateTime()) self.assertEqual(instance.date_time, displayed_value) self.assertEqual(displayed_value, new_minimum_datetime) def test_datetime_editor_mutate_minimum_datetime_bad_order(self): # Test if the minimum_datetime on the editor is mutated after init, # the values on the edited object and the view are synchronized. editor_factory = DatetimeEditor( minimum_datetime=datetime.datetime(2000, 1, 1), maximum_datetime=datetime.datetime(2010, 1, 1), ) view = get_date_time_simple_view(editor_factory) instance = InstanceWithDatetime() with reraise_exceptions(), self.launch_editor( instance, view ) as editor: # This value is in-range instance.date_time = datetime.datetime(2001, 1, 1) # Now change the editor's minimum datetime such that the original # value is out-of-range and the minimum is greater than maximum new_minimum_datetime = datetime.datetime(2020, 1, 1) editor.minimum_datetime = new_minimum_datetime # does not seem needed to flush the event loop, but just in case. process_cascade_events() displayed_value = to_datetime(editor.control.dateTime()) self.assertEqual(instance.date_time, displayed_value) self.assertEqual(displayed_value, new_minimum_datetime) self.assertEqual(editor.maximum_datetime, new_minimum_datetime) def test_datetime_editor_simple_with_maximum_datetime(self): # Test the editor uses the maximum datetime as expected maximum_datetime = datetime.datetime(2000, 1, 1) editor_factory = DatetimeEditor( maximum_datetime=maximum_datetime, ) view = get_date_time_simple_view(editor_factory) instance = InstanceWithDatetime() with reraise_exceptions(), self.launch_editor( instance, view ) as editor: q_maximum_datetime = editor.control.maximumDateTime() # does not seem needed to flush the event loop, but just in case. process_cascade_events() actual_maximum_datetime = to_datetime(q_maximum_datetime) self.assertEqual(actual_maximum_datetime, maximum_datetime) def test_datetime_editor_simple_with_maximum_datetime_out_of_bound(self): maximum_datetime = datetime.datetime(2000, 1, 1) editor_factory = DatetimeEditor( maximum_datetime=maximum_datetime, ) view = get_date_time_simple_view(editor_factory) instance = InstanceWithDatetime() with reraise_exceptions(), self.launch_editor( instance, view ) as editor: # out-of-bound instance.date_time = datetime.datetime(2020, 1, 1) # does not seem needed to flush the event loop, but just in case. process_cascade_events() displayed_value = to_datetime(editor.control.dateTime()) self.assertEqual(displayed_value, maximum_datetime) self.assertEqual(instance.date_time, maximum_datetime) def test_datetime_editor_mutate_maximum_datetime_after_init(self): # Test if the maximum_datetime on the editor is mutated after init, # the values on the edited object and the view are synchronized. editor_factory = DatetimeEditor( maximum_datetime=datetime.datetime(2000, 1, 1), ) view = get_date_time_simple_view(editor_factory) instance = InstanceWithDatetime() with reraise_exceptions(), self.launch_editor( instance, view ) as editor: # This value is in-range instance.date_time = datetime.datetime(1999, 1, 1) # Now change the editor's maximum datetime such that the original # value is out-of-range new_maximum_datetime = datetime.datetime(1900, 1, 1) editor.maximum_datetime = new_maximum_datetime # does not seem needed to flush the event loop, but just in case. process_cascade_events() displayed_value = to_datetime(editor.control.dateTime()) self.assertEqual(instance.date_time, displayed_value) self.assertEqual(displayed_value, new_maximum_datetime) def test_datetime_editor_python_datetime_out_of_bound(self): # The minimum datetime supported by Qt is larger than the minimum # datetime supported by Python. editor_factory = DatetimeEditor() view = get_date_time_simple_view(editor_factory) init_datetime = datetime.datetime(1900, 1, 1) instance = InstanceWithDatetime(date_time=init_datetime) with reraise_exceptions(), self.launch_editor( instance, view ) as editor: # This value is too early and is not supported by Qt # But the editor should not crash new_value = datetime.datetime(1, 1, 1) instance.date_time = new_value # does not seem needed to flush the event loop, but just in case. process_cascade_events() displayed_value = to_datetime(editor.control.dateTime()) # The value has changed to a filled value self.assertNotEqual(instance.date_time, new_value) self.assertEqual(displayed_value, instance.date_time) def test_datetime_editor_qt_datetime_out_of_bound(self): # The maximum datetime supported by Qt is larger than the maximum # datetime supported by Python editor_factory = DatetimeEditor() view = get_date_time_simple_view(editor_factory) init_datetime = datetime.datetime(1900, 1, 1) instance = InstanceWithDatetime(date_time=init_datetime) with reraise_exceptions(), self.launch_editor( instance, view ) as editor: # the user set the datetime on the Qt widget to a value # too large for Python from pyface.qt.QtCore import QDateTime, QDate, QTime q_datetime = QDateTime( QDate(datetime.MAXYEAR + 1, 1, 1), QTime(0, 0) ) editor.control.setDateTime(q_datetime) # Get the displayed value back. displayed_value = to_datetime(editor.control.dateTime()) self.assertEqual(instance.date_time, displayed_value) @contextlib.contextmanager def launch_editor(self, object, view): with create_ui(object, dict(view=view)) as ui: (editor,) = ui._editors yield editor ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/tests/editors/test_default_override.py0000644000175100001730000000752700000000000025613 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! import unittest from traitsui.api import DefaultOverride, EditorFactory from traits.api import HasTraits, Int from traitsui.tests._tools import BaseTestMixin class DummyEditor(EditorFactory): x = Int(10) y = Int(20) def simple_editor(self, ui, object, name, description, parent): return ("simple_editor", self, ui, object, name, description, parent) def custom_editor(self, ui, object, name, description, parent): return ("custom_editor", self, ui, object, name, description, parent) def text_editor(self, ui, object, name, description, parent): return ("text_editor", self, ui, object, name, description, parent) def readonly_editor(self, ui, object, name, description, parent): return ("readonly_editor", self, ui, object, name, description, parent) class NewInt(Int): def create_editor(self): return DummyEditor() class Dummy(HasTraits): x = NewInt() dummy_object = Dummy() do = DefaultOverride(x=15, y=25, format_str="%r") class TestDefaultOverride(BaseTestMixin, unittest.TestCase): def setUp(self): BaseTestMixin.setUp(self) def tearDown(self): BaseTestMixin.tearDown(self) def test_simple_override(self): ( editor_name, editor, ui, obj, name, description, parent, ) = do.simple_editor("ui", dummy_object, "x", "description", "parent") self.assertEqual(editor_name, "simple_editor") self.assertEqual(editor.x, 15) self.assertEqual(editor.y, 25) self.assertEqual(obj, dummy_object) self.assertEqual(name, "x") self.assertEqual(description, "description") self.assertEqual(parent, "parent") def test_text_override(self): ( editor_name, editor, ui, obj, name, description, parent, ) = do.text_editor("ui", dummy_object, "x", "description", "parent") self.assertEqual(editor_name, "text_editor") self.assertEqual(editor.x, 15) self.assertEqual(editor.y, 25) self.assertEqual(obj, dummy_object) self.assertEqual(name, "x") self.assertEqual(description, "description") self.assertEqual(parent, "parent") def test_custom_override(self): ( editor_name, editor, ui, obj, name, description, parent, ) = do.custom_editor("ui", dummy_object, "x", "description", "parent") self.assertEqual(editor_name, "custom_editor") self.assertEqual(editor.x, 15) self.assertEqual(editor.y, 25) self.assertEqual(obj, dummy_object) self.assertEqual(name, "x") self.assertEqual(description, "description") self.assertEqual(parent, "parent") def test_readonly_override(self): ( editor_name, editor, ui, obj, name, description, parent, ) = do.readonly_editor( "ui", dummy_object, "x", "description", "parent" ) self.assertEqual(editor_name, "readonly_editor") self.assertEqual(editor.x, 15) self.assertEqual(editor.y, 25) self.assertEqual(obj, dummy_object) self.assertEqual(name, "x") self.assertEqual(description, "description") self.assertEqual(parent, "parent") ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/tests/editors/test_directory_editor.py0000644000175100001730000000763100000000000025636 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! import unittest from traits.api import Directory, Event, HasTraits from traitsui.api import DirectoryEditor, Item, View from traitsui.testing.api import DisplayedText, KeyClick, KeySequence, UITester from traitsui.tests._tools import ( BaseTestMixin, requires_toolkit, ToolkitName, ) class DirectoryModel(HasTraits): dir_path = Directory() reload_event = Event() # Run this against wx too when enthought/traitsui#752 is also fixed. @requires_toolkit([ToolkitName.qt]) class TestDirectoryEditor(BaseTestMixin, unittest.TestCase): """Test DirectoryEditor.""" def setUp(self): BaseTestMixin.setUp(self) def tearDown(self): BaseTestMixin.tearDown(self) def check_init_and_dispose(self, style): # Test init and dispose by opening and closing the UI view = View(Item("dir_path", editor=DirectoryEditor(), style=style)) obj = DirectoryModel() with UITester().create_ui(obj, dict(view=view)): pass def test_simple_editor_init_and_dispose(self): # This may fail on wx, see enthought/traitsui#889 self.check_init_and_dispose("simple") def test_custom_editor_init_and_dispose(self): self.check_init_and_dispose("custom") def test_custom_editor_reload_changed_after_dispose(self): # Test firing reload event on the model after the UI is disposed. view = View( Item( "dir_path", editor=DirectoryEditor(reload_name="reload_event"), style="custom", ), ) obj = DirectoryModel() with UITester().create_ui(obj, dict(view=view)): pass # should not fail. obj.reload_event = True # Behavior on wx may not be quite the same on other platforms. @requires_toolkit([ToolkitName.qt]) def test_simple_editor_set_text_to_nonexisting_path(self): # Test setting the editor to a nonexisting dirpath # e.g. use case for creating a new file. view = View(Item("dir_path", editor=DirectoryEditor())) obj = DirectoryModel() tester = UITester() with tester.create_ui(obj, dict(view=view)) as ui: dirpath_field = tester.find_by_name(ui, "dir_path") dirpath_field.perform(KeySequence("some_dir")) dirpath_field.perform(KeyClick("Enter")) self.assertEqual(obj.dir_path, "some_dir") def test_simple_editor_display_path(self): # Test the filepath widget is updated to show path view = View(Item("dir_path", editor=DirectoryEditor())) obj = DirectoryModel() tester = UITester() with tester.create_ui(obj, dict(view=view)) as ui: dirpath_field = tester.find_by_name(ui, "dir_path") self.assertEqual(dirpath_field.inspect(DisplayedText()), "") obj.dir_path = "some_dir" self.assertEqual( dirpath_field.inspect(DisplayedText()), "some_dir" ) # Behavior on wx may not be quite the same on other platforms. @requires_toolkit([ToolkitName.qt]) def test_simple_editor_auto_set_text(self): # Test with auto_set set to True. view = View(Item("dir_path", editor=DirectoryEditor(auto_set=True))) obj = DirectoryModel() tester = UITester() with tester.create_ui(obj, dict(view=view)) as ui: dirpath_field = tester.find_by_name(ui, "dir_path") dirpath_field.perform(KeySequence("some_dir")) self.assertEqual(obj.dir_path, "some_dir") ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/tests/editors/test_drop_editor.py0000644000175100001730000000320200000000000024564 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! import unittest from traits.api import HasTraits, Str from traitsui.api import DropEditor, Item, View from traitsui.tests._tools import ( BaseTestMixin, create_ui, requires_toolkit, reraise_exceptions, ToolkitName, ) class Model(HasTraits): value = Str() # Run this test against wx when enthought/traitsui#752 is fixed. @requires_toolkit([ToolkitName.qt]) class TestDropEditor(BaseTestMixin, unittest.TestCase): """Test DropEditor.""" def setUp(self): BaseTestMixin.setUp(self) def tearDown(self): BaseTestMixin.tearDown(self) def test_init_dispose_editable(self): obj = Model() view = View(Item("value", editor=DropEditor(readonly=False))) with reraise_exceptions(): with create_ui(obj, dict(view=view)): pass # Mutating value after UI is closed should be okay. obj.value = "New" def test_init_dispose_readonly(self): obj = Model() view = View(Item("value", editor=DropEditor(readonly=True))) with reraise_exceptions(): with create_ui(obj, dict(view=view)): pass # Mutating value after UI is closed should be okay. obj.value = "New" ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/tests/editors/test_enum_editor.py0000644000175100001730000003655100000000000024601 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! import contextlib import unittest from traits.api import Enum, HasTraits, Int, List from traitsui.api import EnumEditor, UItem, View from traitsui.tests._tools import ( BaseTestMixin, create_ui, is_qt, is_wx, requires_toolkit, reraise_exceptions, ToolkitName, ) from traitsui.testing.api import ( Disabled, DisplayedText, Index, IsEnabled, KeyClick, KeySequence, MouseClick, SelectedText, UITester, ) class EnumModel(HasTraits): value = Enum("one", "two", "three", "four") class EnumModelWithNone(HasTraits): value = Enum(None, "one", "two", "three", "four") def get_view(style): return View(UItem("value", style=style), resizable=True) def get_evaluate_view(style, auto_set=True, mode="radio"): return View( UItem( "value", editor=EnumEditor( evaluate=True, values=["one", "two", "three", "four"], auto_set=auto_set, mode=mode, ), style=style, ), resizable=True, ) def get_radio_view(cols=1): return View( UItem( "value", editor=EnumEditor( values=["one", "two", "three", "four"], cols=cols, mode="radio", ), style="custom", ), resizable=True, ) @requires_toolkit([ToolkitName.qt, ToolkitName.wx]) class TestEnumEditorMapping(BaseTestMixin, unittest.TestCase): def setUp(self): BaseTestMixin.setUp(self) def tearDown(self): BaseTestMixin.tearDown(self) @contextlib.contextmanager def setup_ui(self, model, view): with create_ui(model, dict(view=view)) as ui: yield ui.get_editors("value")[0] def check_enum_mappings_value_change(self, style, mode): class IntEnumModel(HasTraits): value = Int() enum_editor_factory = EnumEditor( values=[0, 1], format_func=lambda v: str(bool(v)).upper(), mode=mode, ) formatted_view = View( UItem( "value", editor=enum_editor_factory, style=style, ) ) with reraise_exceptions(), self.setup_ui( IntEnumModel(), formatted_view ) as editor: self.assertEqual(editor.names, ["FALSE", "TRUE"]) self.assertEqual(editor.mapping, {"FALSE": 0, "TRUE": 1}) self.assertEqual(editor.inverse_mapping, {0: "FALSE", 1: "TRUE"}) enum_editor_factory.values = [1, 0] self.assertEqual(editor.names, ["TRUE", "FALSE"]) self.assertEqual(editor.mapping, {"TRUE": 1, "FALSE": 0}) self.assertEqual(editor.inverse_mapping, {1: "TRUE", 0: "FALSE"}) def check_enum_mappings_name_change(self, style, mode): class IntEnumModel(HasTraits): value = Int() possible_values = List([0, 1]) formatted_view = View( UItem( 'value', editor=EnumEditor( name="object.possible_values", format_func=lambda v: str(bool(v)).upper(), mode=mode, ), style=style, ) ) model = IntEnumModel() with reraise_exceptions(), self.setup_ui( model, formatted_view ) as editor: self.assertEqual(editor.names, ["FALSE", "TRUE"]) self.assertEqual(editor.mapping, {"FALSE": 0, "TRUE": 1}) self.assertEqual(editor.inverse_mapping, {0: "FALSE", 1: "TRUE"}) model.possible_values = [1, 0] self.assertEqual(editor.names, ["TRUE", "FALSE"]) self.assertEqual(editor.mapping, {"TRUE": 1, "FALSE": 0}) self.assertEqual(editor.inverse_mapping, {1: "TRUE", 0: "FALSE"}) def test_simple_editor_mapping_values(self): self.check_enum_mappings_value_change("simple", "radio") def test_simple_editor_mapping_name(self): self.check_enum_mappings_name_change("simple", "radio") def test_radio_editor_mapping_values(self): # FIXME issue enthought/traitsui#842 if is_wx(): import wx with self.assertRaises(wx._core.wxAssertionError): self.check_enum_mappings_value_change("custom", "radio") else: self.check_enum_mappings_value_change("custom", "radio") def test_radio_editor_mapping_name(self): # FIXME issue enthought/traitsui#842 if is_wx(): import wx with self.assertRaises(wx._core.wxAssertionError): self.check_enum_mappings_name_change("custom", "radio") else: self.check_enum_mappings_name_change("custom", "radio") def test_list_editor_mapping_values(self): self.check_enum_mappings_value_change("custom", "list") def test_list_editor_mapping_name(self): self.check_enum_mappings_name_change("custom", "list") @requires_toolkit([ToolkitName.qt, ToolkitName.wx]) class TestSimpleEnumEditor(BaseTestMixin, unittest.TestCase): def setUp(self): BaseTestMixin.setUp(self) def tearDown(self): BaseTestMixin.tearDown(self) def check_enum_text_update(self, view): enum_edit = EnumModel() tester = UITester() with tester.create_ui(enum_edit, dict(view=view)) as ui: combobox = tester.find_by_name(ui, "value") displayed = combobox.inspect(DisplayedText()) self.assertEqual(displayed, "one") combobox.locate(Index(1)).perform(MouseClick()) displayed = combobox.inspect(DisplayedText()) self.assertEqual(displayed, "two") def check_enum_object_update(self, view): enum_edit = EnumModel() tester = UITester() with tester.create_ui(enum_edit, dict(view=view)) as ui: self.assertEqual(enum_edit.value, "one") combobox = tester.find_by_name(ui, "value") for _ in range(3): combobox.perform(KeyClick("Backspace")) combobox.perform(KeySequence("two")) combobox.perform(KeyClick("Enter")) self.assertEqual(enum_edit.value, "two") def check_enum_index_update(self, view): enum_edit = EnumModel() tester = UITester() with tester.create_ui(enum_edit, dict(view=view)) as ui: self.assertEqual(enum_edit.value, "one") combobox = tester.find_by_name(ui, "value") combobox.locate(Index(1)).perform(MouseClick()) self.assertEqual(enum_edit.value, "two") def check_enum_text_bad_update(self, view): enum_edit = EnumModel() tester = UITester() with tester.create_ui(enum_edit, dict(view=view)) as ui: self.assertEqual(enum_edit.value, "one") combobox = tester.find_by_name(ui, "value") for _ in range(3): combobox.perform(KeyClick("Backspace")) combobox.perform(KeyClick("H")) combobox.perform(KeyClick("Enter")) self.assertEqual(enum_edit.value, "one") def test_simple_enum_editor_text(self): self.check_enum_text_update(get_view("simple")) def test_simple_enum_editor_index(self): self.check_enum_index_update(get_view("simple")) def test_simple_evaluate_editor_text(self): self.check_enum_text_update(get_evaluate_view("simple")) def test_simple_evaluate_editor_index(self): self.check_enum_index_update(get_evaluate_view("simple")) def test_simple_evaluate_editor_bad_text(self): self.check_enum_text_bad_update(get_evaluate_view("simple")) def test_simple_evaluate_editor_object(self): self.check_enum_object_update(get_evaluate_view("simple")) def test_simple_evaluate_editor_object_no_auto_set(self): view = get_evaluate_view("simple", auto_set=False) enum_edit = EnumModel() tester = UITester() with tester.create_ui(enum_edit, dict(view=view)) as ui: self.assertEqual(enum_edit.value, "one") combobox = tester.find_by_name(ui, "value") for _ in range(3): combobox.perform(KeyClick("Backspace")) combobox.perform(KeySequence("two")) self.assertEqual(enum_edit.value, "one") combobox.perform(KeyClick("Enter")) self.assertEqual(enum_edit.value, "two") def test_simple_editor_resizable(self): # Smoke test for `qt.enum_editor.SimpleEditor.set_size_policy` enum_edit = EnumModel() resizable_view = View(UItem("value", style="simple", resizable=True)) tester = UITester() with tester.create_ui(enum_edit, dict(view=resizable_view)): pass def test_simple_editor_rebuild_editor_evaluate(self): # Smoke test for `wx.enum_editor.SimpleEditor.rebuild_editor` enum_editor_factory = EnumEditor( evaluate=True, values=["one", "two", "three", "four"], ) view = View(UItem("value", editor=enum_editor_factory, style="simple")) tester = UITester() with tester.create_ui(EnumModel(), dict(view=view)): enum_editor_factory.values = ["one", "two", "three"] def test_simple_editor_disabled(self): enum_edit = EnumModel(value="two") view = View( UItem( "value", style="simple", enabled_when="value == 'one'", editor=EnumEditor(evaluate=True, values=["one", "two"]), ), ) tester = UITester() with tester.create_ui(enum_edit, dict(view=view)) as ui: combobox = tester.find_by_name(ui, "value") with self.assertRaises(Disabled): combobox.perform(KeyClick("Enter")) with self.assertRaises(Disabled): combobox.perform(KeySequence("two")) self.assertFalse(combobox.inspect(IsEnabled())) @requires_toolkit([ToolkitName.qt, ToolkitName.wx]) class TestRadioEnumEditor(BaseTestMixin, unittest.TestCase): def setUp(self): BaseTestMixin.setUp(self) def tearDown(self): BaseTestMixin.tearDown(self) def test_radio_enum_editor_update(self): enum_edit = EnumModel() tester = UITester() with tester.create_ui(enum_edit, dict(view=get_view("custom"))) as ui: # sanity check self.assertEqual(enum_edit.value, "one") radio_editor = tester.find_by_name(ui, "value") self.assertEqual( radio_editor.inspect(SelectedText()), "One", ) enum_edit.value = "two" self.assertEqual( radio_editor.inspect(SelectedText()), "Two", ) def test_radio_enum_editor_pick(self): for cols in range(1, 4): for row_major in [True, False]: enum_edit = EnumModel() tester = UITester() view = get_radio_view(cols=cols) with tester.create_ui(enum_edit, dict(view=view)) as ui: # sanity check self.assertEqual(enum_edit.value, "one") radio_editor = tester.find_by_name(ui, "value") if is_qt(): radio_editor._target.row_major = row_major radio_editor._target.rebuild_editor() item = radio_editor.locate(Index(3)) item.perform(MouseClick()) self.assertEqual(enum_edit.value, "four") def test_radio_enum_none_selected(self): enum_edit = EnumModelWithNone() tester = UITester() with tester.create_ui(enum_edit, dict(view=get_radio_view())) as ui: self.assertEqual(enum_edit.value, None) radio_editor = tester.find_by_name(ui, "value") displayed = radio_editor.inspect(SelectedText()) self.assertEqual(displayed, None) @requires_toolkit([ToolkitName.qt, ToolkitName.wx]) class TestListEnumEditor(BaseTestMixin, unittest.TestCase): def setUp(self): BaseTestMixin.setUp(self) def tearDown(self): BaseTestMixin.tearDown(self) def check_enum_text_update(self, view): enum_edit = EnumModel() tester = UITester() with tester.create_ui(enum_edit, dict(view=view)) as ui: list_editor = tester.find_by_name(ui, "value") displayed = list_editor.inspect(SelectedText()) self.assertEqual(displayed, "one") list_editor.locate(Index(1)).perform(MouseClick()) displayed = list_editor.inspect(SelectedText()) self.assertEqual(displayed, "two") def check_enum_index_update(self, view): enum_edit = EnumModel() tester = UITester() with tester.create_ui(enum_edit, dict(view=view)) as ui: self.assertEqual(enum_edit.value, "one") list_editor = tester.find_by_name(ui, "value") list_editor.locate(Index(1)).perform(MouseClick()) self.assertEqual(enum_edit.value, "two") def test_list_enum_editor_text(self): view = View( UItem( "value", editor=EnumEditor( values=["one", "two", "three", "four"], mode="list", ), style="custom", ), resizable=True, ) self.check_enum_text_update(view) def test_list_enum_editor_index(self): view = View( UItem( "value", editor=EnumEditor( values=["one", "two", "three", "four"], mode="list", ), style="custom", ), resizable=True, ) self.check_enum_index_update(view) def test_list_evaluate_editor_text(self): self.check_enum_text_update(get_evaluate_view("custom", mode="list")) def test_list_evaluate_editor_index(self): self.check_enum_index_update(get_evaluate_view("custom", mode="list")) def test_list_enum_none_selected(self): enum_edit = EnumModelWithNone() view = View( UItem( "value", editor=EnumEditor( # Note that for the list style editor, in order to select # None, it must be one of the displayed options values=[None, "one", "two", "three", "four"], mode="list", ), style="custom", ), resizable=True, ) tester = UITester() with tester.create_ui(enum_edit, dict(view=view)) as ui: self.assertEqual(enum_edit.value, None) list_editor = tester.find_by_name(ui, "value") # As a result the displayed text is actually the string 'None' displayed = list_editor.inspect(SelectedText()) self.assertEqual(displayed, 'None') ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/tests/editors/test_file_editor.py0000644000175100001730000001412200000000000024542 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! import unittest from unittest import mock from pyface.api import OK from traits.api import Event, File, HasTraits from traitsui.api import FileEditor, Item, View from traitsui.tests._tools import ( BaseTestMixin, requires_toolkit, ToolkitName, ) from traitsui.testing.api import DisplayedText, KeyClick, KeySequence, UITester class FileModel(HasTraits): filepath = File() reload_event = Event() existing_filepath = File(exists=True) def trait_set_side_effect(**traits): def side_effect(self, *args, **kwargs): self.trait_set(**traits) return mock.DEFAULT return side_effect @requires_toolkit([ToolkitName.qt, ToolkitName.wx]) class TestSimpleFileEditor(BaseTestMixin, unittest.TestCase): """Test FileEditor (simple style).""" def setUp(self): BaseTestMixin.setUp(self) def tearDown(self): BaseTestMixin.tearDown(self) # Behavior on wx may not be quite the same on other platforms. @requires_toolkit([ToolkitName.qt]) def test_simple_editor_set_text_to_nonexisting_path(self): # Test setting the editor to a nonexisting filepath # e.g. use case for creating a new file. view = View(Item("filepath", editor=FileEditor())) obj = FileModel() tester = UITester() with tester.create_ui(obj, dict(view=view)) as ui: filepath_field = tester.find_by_name(ui, "filepath") filepath_field.perform(KeySequence("some_file.txt")) filepath_field.perform(KeyClick("Enter")) self.assertEqual(obj.filepath, "some_file.txt") def test_simple_editor_display_path(self): # Test the filepath widget is updated to show path view = View(Item("filepath", editor=FileEditor())) obj = FileModel() tester = UITester() with tester.create_ui(obj, dict(view=view)) as ui: filepath_field = tester.find_by_name(ui, "filepath") self.assertEqual(filepath_field.inspect(DisplayedText()), "") obj.filepath = "some_file.txt" self.assertEqual( filepath_field.inspect(DisplayedText()), "some_file.txt" ) # Behavior on wx may not be quite the same on other platforms. @requires_toolkit([ToolkitName.qt]) def test_simple_editor_auto_set_text(self): # Test with auto_set set to True. view = View(Item("filepath", editor=FileEditor(auto_set=True))) obj = FileModel() tester = UITester() with tester.create_ui(obj, dict(view=view)) as ui: filepath_field = tester.find_by_name(ui, "filepath") filepath_field.perform(KeySequence("some_file.txt")) self.assertEqual(obj.filepath, "some_file.txt") def test_simple_editor_reset_text_if_validation_error(self): # Test when the trait validates file existence. view = View(Item("existing_filepath", editor=FileEditor())) obj = FileModel() tester = UITester() with tester.create_ui(obj, dict(view=view)) as ui: filepath_field = tester.find_by_name(ui, "existing_filepath") # when filepath_field.perform(KeySequence("some_file.txt")) filepath_field.perform(KeyClick("Enter")) # then # the file does not exist, the trait is not set. self.assertEqual(obj.existing_filepath, "") # the widget is synchronized to the trait value. self.assertEqual(filepath_field.inspect(DisplayedText()), "") @mock.patch( "pyface.api.FileDialog.open", autospec=True, side_effect=trait_set_side_effect( return_code=OK, path="some_file.txt", ), ) def test_show_file_dialog(self, mock_open): view = View(Item("filepath", editor=FileEditor())) obj = FileModel() tester = UITester() with tester.create_ui(obj, dict(view=view)) as ui: editor = ui.get_editors("filepath")[0] editor.show_file_dialog() self.assertEqual(editor.value, "some_file.txt") @mock.patch( "pyface.api.FileDialog.open", autospec=True, side_effect=trait_set_side_effect( return_code=OK, path="some_file.txt", ), ) def test_show_file_dialog_truncate_ext(self, mock_open): view = View(Item("filepath", editor=FileEditor(truncate_ext=True))) obj = FileModel() tester = UITester() with tester.create_ui(obj, dict(view=view)) as ui: editor = ui.get_editors("filepath")[0] editor.show_file_dialog() self.assertEqual(editor.value, "some_file") # Run this against wx too when enthought/traitsui#752 is also fixed. @requires_toolkit([ToolkitName.qt]) class TestCustomFileEditor(BaseTestMixin, unittest.TestCase): """Test FileEditor (custom style).""" def setUp(self): BaseTestMixin.setUp(self) def tearDown(self): BaseTestMixin.tearDown(self) def test_custom_editor_init_and_dispose(self): # Test init and dispose by opening and closing the UI view = View(Item("filepath", editor=FileEditor(), style="custom")) obj = FileModel() with UITester().create_ui(obj, dict(view=view)): pass def test_custom_editor_reload_changed_after_dispose(self): # Test firing reload event on the model after the UI is disposed. view = View( Item( "filepath", editor=FileEditor(reload_name="reload_event"), style="custom", ), ) obj = FileModel() with UITester().create_ui(obj, dict(view=view)): pass # should not fail. obj.reload_event = True ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/tests/editors/test_font_editor.py0000644000175100001730000000230200000000000024566 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Test FontEditor """ import unittest from traits.api import HasTraits from traitsui.api import Font, Item, View from traitsui.tests._tools import ( requires_toolkit, ToolkitName, ) from traitsui.testing.api import MouseClick, UITester class ObjectWithFont(HasTraits): font_trait = Font() @requires_toolkit([ToolkitName.qt, ToolkitName.wx]) class TestFontEditor(unittest.TestCase): def test_create_and_dispose_text_style(self): # Setting focus on the widget and then disposing the widget # should not cause errors. view = View(Item("font_trait", style="text")) tester = UITester() with tester.create_ui(ObjectWithFont(), dict(view=view)) as ui: wrapper = tester.find_by_name(ui, "font_trait") wrapper.perform(MouseClick()) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/tests/editors/test_html_editor.py0000644000175100001730000002656300000000000024603 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! import unittest from unittest import mock try: from pyface.qt import QtWebkit # noqa: F401 NO_WEBKIT_OR_WEBENGINE = False except ImportError: try: from pyface.qt import QtWebEngine # noqa: F401 NO_WEBKIT_OR_WEBENGINE = False except ImportError: NO_WEBKIT_OR_WEBENGINE = True from traits.api import HasTraits, Str from traitsui.api import HTMLEditor, Item, View from traitsui.tests._tools import ( BaseTestMixin, is_qt, requires_toolkit, ToolkitName, ) from traitsui.testing.api import MouseClick, TargetRegistry, UITester class HTMLModel(HasTraits): """Dummy class for testing HTMLEditor.""" content = Str() model_base_url = Str() def get_view(base_url_name): return View( Item( "content", editor=HTMLEditor( format_text=True, base_url_name=base_url_name, ), ) ) class HTMLContent: """Action to retrieve the HTML content currently displayed. Implementation should return a str, whose content conforms to HTML markup. """ pass def _is_webkit_page(page): """Return true if the given page is a QWebPage from QtWebKit. Intended for handling the compatibility between QtWebKit and QtWebEngine. Parameters ---------- page : QWebEnginePage or QWebPage """ return hasattr(page, "setLinkDelegationPolicy") def qt_get_page_html_content(page): """Return the HTML content currently being viewed. Parameters ---------- page : QWebEnginePage or QWebPage Returns ------- html : str """ qt_allow_view_to_load(page) if _is_webkit_page(page): return page.mainFrame().toHtml() content = [] page.toHtml(content.append) qt_allow_view_to_load(page) return "".join(content) def wait_for_qt_signal(qt_signal, timeout): """Wait for the given Qt signal to fire, or timeout. A mock implementation of QSignalSpy.wait, which is one of the missing bindings in PySide2, and is not available in Qt4. Parameters ---------- qt_signal : signal Qt signal to wait for timeout : int Timeout in milliseconds, to match Qt API. Raises ------ RuntimeError """ from pyface.qt import QtCore # QEventLoop is used instead of QApplication due to observed # hangs with Qt4. event_loop = QtCore.QEventLoop() def exit(*args, **kwargs): event_loop.quit() timeout_timer = QtCore.QTimer() timeout_timer.setSingleShot(True) timeout_timer.setInterval(timeout) timeout_timer.timeout.connect(exit) qt_signal.connect(exit) timeout_timer.start() event_loop.exec_() qt_signal.disconnect(exit) if timeout_timer.isActive(): timeout_timer.stop() else: raise RuntimeError("Timeout waiting for signal.") def qt_allow_view_to_load(loadable, timeout=0.5): """Allow QWebView/QWebPage/QWebEngineView/QWebEnginePage to finish loading. Out of context, this function does not know if the page has started loading. Therefore no timeout error is raised. For most testing purposes, this function should be good enough to avoid interacting with the Qt web page before it has finished loading, at a cost of a slower test. Parameters ---------- loadable : QWebView or QWebPage or QWebEngineView or QWebEnginePage The view / page to allow loading to finish. Any object with the loadFinished signal can be used. timeout : float Timeout in seconds for each signal being observed. """ timeout_ms = round(timeout * 1000) try: wait_for_qt_signal(loadable.loadFinished, timeout=timeout_ms) except RuntimeError: return def qt_mouse_click_web_view(view, delay): """Perform a mouse click at the center of the web view. Note that the page is allowed time to load before and after the mouse click. Parameters ---------- view : QWebView or QWebEngineView """ from pyface.qt import QtCore from pyface.qt.QtTest import QTest qt_allow_view_to_load(view) if view.focusProxy() is not None: # QWebEngineView widget = view.focusProxy() else: # QWebView widget = view try: QTest.mouseClick( widget, QtCore.Qt.MouseButton.LeftButton, QtCore.Qt.KeyboardModifier.NoModifier, delay=delay, ) finally: qt_allow_view_to_load(view) def qt_target_registry(): """Return an instance of TargetRegistry for testing Qt + HTMLEditor Returns ------- target_registry : TargetRegistry """ from traitsui.qt.html_editor import SimpleEditor registry = TargetRegistry() registry.register_interaction( target_class=SimpleEditor, interaction_class=MouseClick, handler=lambda wrapper, _: qt_mouse_click_web_view( wrapper._target.control, wrapper.delay ), ) registry.register_interaction( target_class=SimpleEditor, interaction_class=HTMLContent, handler=lambda wrapper, _: ( qt_get_page_html_content(wrapper._target.control.page()) ), ) return registry def get_custom_ui_tester(): """Return an instance of UITester that contains extended testing functionality for HTMLEditor. These implementations are used by TraitsUI only, are more ad hoc than they would have been if they were made public. """ if is_qt(): return UITester(registries=[qt_target_registry()]) return UITester() # Run this against wx as well once enthought/traitsui#752 is fixed. @requires_toolkit([ToolkitName.qt]) @unittest.skipIf( NO_WEBKIT_OR_WEBENGINE, "Tests require either QtWebKit or QtWebEngine" ) class TestHTMLEditor(BaseTestMixin, unittest.TestCase): """Test HTMLEditor""" def setUp(self): BaseTestMixin.setUp(self) self.tester = get_custom_ui_tester() def tearDown(self): BaseTestMixin.tearDown(self) def test_init_and_dispose(self): # Smoke test to check init and dispose do not fail. model = HTMLModel() view = get_view(base_url_name="") with self.tester.create_ui(model, dict(view=view)): pass def test_base_url_changed(self): # Test if the base_url is changed after the UI closes, nothing # fails because sync_value is unhooked in the base class. model = HTMLModel() view = get_view(base_url_name="model_base_url") with self.tester.create_ui(model, dict(view=view)): pass # It is okay to modify base_url after the UI is closed model.model_base_url = "/new_dir" @requires_toolkit([ToolkitName.qt]) def test_open_internal_link(self): # this test requires Qt because it relies on the link filling up # the entire page through the use of CSS, which isn't supported # by Wx. model = HTMLModel( content="""
    Internal Link """ ) view = View(Item("content", editor=HTMLEditor())) with self.tester.create_ui(model, dict(view=view)) as ui: html_view = self.tester.find_by_name(ui, "content") # when with mock.patch("webbrowser.open_new") as mocked_browser: html_view.perform(MouseClick()) mocked_browser.assert_not_called() @requires_toolkit([ToolkitName.qt]) def test_open_external_link(self): # this test requires Qt because it relies on the link filling up # the entire page through the use of CSS, which isn't supported # by Wx. model = HTMLModel( content=""" External Link """ ) view = View(Item("content", editor=HTMLEditor())) with self.tester.create_ui(model, dict(view=view)) as ui: html_view = self.tester.find_by_name(ui, "content") with mock.patch("webbrowser.open_new") as mocked_browser: html_view.perform(MouseClick()) self.assertIn( "External Link", html_view.inspect(HTMLContent()), ) # See enthought/traitsui#1464 # This is the expected behaviour: # mocked_browser.assert_called_once_with("test://testing") # However, this is the current unexpected behaviour mocked_browser.assert_not_called() @requires_toolkit([ToolkitName.qt]) def test_open_internal_link_externally(self): # this test requires Qt because it relies on the link filling up # the entire page through the use of CSS, which isn't supported # by Wx. model = HTMLModel( content=""" Internal Link """ ) view = View(Item("content", editor=HTMLEditor(open_externally=True))) with self.tester.create_ui(model, dict(view=view)) as ui: html_view = self.tester.find_by_name(ui, "content") with mock.patch("webbrowser.open_new") as mocked_browser: html_view.perform(MouseClick()) self.assertIn( "Internal Link", html_view.inspect(HTMLContent()), ) mocked_browser.assert_called_once_with("test://testing") @requires_toolkit([ToolkitName.qt]) def test_open_external_link_externally(self): model = HTMLModel( content=""" External Link """ ) view = View(Item("content", editor=HTMLEditor(open_externally=True))) with self.tester.create_ui(model, dict(view=view)) as ui: html_view = self.tester.find_by_name(ui, "content") with mock.patch("webbrowser.open_new") as mocked_browser: html_view.perform(MouseClick()) self.assertIn( "External Link", html_view.inspect(HTMLContent()), ) is_webkit = _is_webkit_page(html_view._target.control.page()) if is_webkit: # This is the expected behavior. mocked_browser.assert_called_once_with("test://testing") else: # Expected failure: # See enthought/traitsui#1464 # This is the current unexpected behavior if QtWebEngine is used. mocked_browser.assert_not_called() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/tests/editors/test_image_editor.py0000644000175100001730000001017500000000000024711 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Tests pertaining to the ImageEditor """ import unittest import pkg_resources from pyface.api import Image, ImageResource from traits.api import File, HasTraits from traitsui.api import ImageEditor, Item, View from traitsui.tests._tools import ( BaseTestMixin, create_ui, is_qt, is_mac_os, requires_toolkit, ToolkitName, ) filename1 = pkg_resources.resource_filename( "traitsui", "examples/demo/Extras/images/python-logo.png" ) filename2 = pkg_resources.resource_filename( "traitsui", "examples/demo/Extras/images/info.png" ) class ImageDisplay(HasTraits): image = Image() @requires_toolkit([ToolkitName.wx, ToolkitName.qt]) class TestImageEditor(BaseTestMixin, unittest.TestCase): def test_image_editor_static(self): obj1 = ImageDisplay() view = View( Item( 'image', editor=ImageEditor( image=ImageResource(filename1), ), ) ) # This should not fail. with create_ui(obj1, dict(view=view)) as ui: pass def test_image_editor_resource(self): obj1 = ImageDisplay( image=ImageResource(filename1) ) view = View( Item( 'image', editor=ImageEditor() ) ) # This should not fail. with create_ui(obj1, dict(view=view)) as ui: obj1.image = ImageResource(filename2) def test_image_editor_array(self): try: import numpy as np from pyface.api import ArrayImage except ImportError: self.skipTest("NumPy is not available") gradient1 = np.empty(shape=(256, 256, 3), dtype='uint8') gradient1[:, :, 0] = np.arange(256).reshape(256, 1) gradient1[:, :, 1] = np.arange(256).reshape(1, 256) gradient1[:, :, 2] = np.arange(255, -1, -1).reshape(1, 256) gradient2 = np.empty(shape=(256, 256, 3), dtype='uint8') gradient2[:, :, 0] = np.arange(255, -1, -1).reshape(256, 1) gradient2[:, :, 1] = np.arange(256).reshape(1, 256) gradient2[:, :, 2] = np.arange(255, -1, -1).reshape(1, 256) obj1 = ImageDisplay( image=ArrayImage(data=gradient1) ) view = View( Item( 'image', editor=ImageEditor() ) ) # This should not fail. with create_ui(obj1, dict(view=view)) as ui: obj1.image = ArrayImage(data=gradient2) @unittest.skipIf(is_mac_os, "Segfault on MacOS, see issue #1979") def test_image_editor_pillow(self): try: import PIL.Image from pyface.api import PILImage except ImportError: self.skipTest("Pillow is not available") if is_qt: try: # is ImageQt available as well from PIL.ImageQt import ImageQt except ImportError: self.skipTest("ImageQt is not available") pil_image_1 = PIL.Image.open(filename1) pil_image_2 = PIL.Image.open(filename2) obj1 = ImageDisplay( image=PILImage(image=pil_image_1) ) view = View( Item( 'image', editor=ImageEditor() ) ) # This should not fail. with create_ui(obj1, dict(view=view)) as ui: obj1.image = PILImage(image=pil_image_2) def test_image_editor_none(self): obj1 = ImageDisplay(image=None) view = View( Item( 'image', editor=ImageEditor() ) ) # This should not fail. with create_ui(obj1, dict(view=view)) as ui: pass ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/tests/editors/test_image_enum_editor.py0000644000175100001730000003631500000000000025741 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! import contextlib import sys import unittest from unittest.mock import patch from traits.api import Enum, HasTraits, List from traitsui.api import ImageEnumEditor, UItem, View from traitsui.tests._tools import ( BaseTestMixin, create_ui, is_qt, is_wx, process_cascade_events, requires_toolkit, reraise_exceptions, ToolkitName, ) # Import needed bitmap/pixmap cache and prepare for patching if is_wx(): from traitsui.wx.helper import bitmap_cache as image_cache cache_to_patch = "traitsui.wx.image_enum_editor.bitmap_cache" elif is_qt(): from traitsui.qt.helper import pixmap_cache as image_cache cache_to_patch = "traitsui.qt.image_enum_editor.pixmap_cache" is_linux = sys.platform == 'linux' class EnumModel(HasTraits): value = Enum('top left', 'top right', 'bottom left', 'bottom right') def get_view(style): return View( UItem( 'value', editor=ImageEnumEditor( values=[ 'top left', 'top right', 'bottom left', 'bottom right', ], prefix='@icons:', suffix='_origin', path='dummy_path', ), style=style, ), resizable=True, ) def click_on_image(image_control): """Click on the image controlled by given image_control.""" if is_wx(): import wx event_down = wx.MouseEvent(wx.EVT_LEFT_DOWN.typeId) wx.PostEvent(image_control, event_down) event_up = wx.MouseEvent(wx.EVT_LEFT_UP.typeId) event_up.SetX(0) event_up.SetY(0) wx.PostEvent(image_control, event_up) elif is_qt(): image_control.click() else: raise unittest.SkipTest("Test not implemented for this toolkit") def get_button_strings(control): """Return the list of strings associated with the buttons under given control. Assumes all sizer children (wx) or layout items (qt) are buttons. """ button_strings = [] if is_wx(): for item in control.GetSizer().GetChildren(): button = item.GetWindow() button_strings.append(button.value) elif is_qt(): layout = control.layout() for i in range(layout.count()): button = layout.itemAt(i).widget() button_strings.append(button.value) else: raise unittest.SkipTest("Test not implemented for this toolkit") return button_strings def get_all_button_selected_status(control): """Return a list with selected (wx) or checked (qt) button status under given control. Assumes all sizer children (wx) or layout items (qt) are buttons. """ button_status = [] if is_wx(): for item in control.GetSizer().GetChildren(): button_status.append(item.GetWindow().Selected()) elif is_qt(): layout = control.layout() for i in range(layout.count()): button_status.append(layout.itemAt(i).widget().isChecked()) else: raise unittest.SkipTest("Test not implemented for this toolkit") return button_status def get_button_control(control, button_idx): """Get button control from a specified parent control given a button index. Assumes all sizer children (wx) or layout items (qt) are buttons. """ if is_wx(): return control.GetSizer().GetChildren()[button_idx].GetWindow() elif is_qt(): return control.layout().itemAt(button_idx).widget() else: raise unittest.SkipTest("Test not implemented for this toolkit") @requires_toolkit([ToolkitName.qt]) class TestImageEnumEditorMapping(BaseTestMixin, unittest.TestCase): def setUp(self): BaseTestMixin.setUp(self) def tearDown(self): BaseTestMixin.tearDown(self) @contextlib.contextmanager def setup_ui(self, model, view): with create_ui(model, dict(view=view)) as ui: yield ui.get_editors("value")[0] def check_enum_mappings_value_change(self, style): image_enum_editor_factory = ImageEnumEditor( values=['top left', 'top right'], format_func=lambda v: v.upper(), prefix='@icons:', suffix='_origin', path='dummy_path', ) formatted_view = View( UItem( "value", editor=image_enum_editor_factory, style=style, ) ) with reraise_exceptions(), self.setup_ui( EnumModel(), formatted_view ) as editor: self.assertEqual(editor.names, ["TOP LEFT", "TOP RIGHT"]) self.assertEqual( editor.mapping, {"TOP LEFT": "top left", "TOP RIGHT": "top right"}, ) self.assertEqual( editor.inverse_mapping, {"top left": "TOP LEFT", "top right": "TOP RIGHT"}, ) image_enum_editor_factory.values = ["top right", "top left"] self.assertEqual(editor.names, ["TOP RIGHT", "TOP LEFT"]) self.assertEqual( editor.mapping, {"TOP RIGHT": "top right", "TOP LEFT": "top left"}, ) self.assertEqual( editor.inverse_mapping, {"top right": "TOP RIGHT", "top left": "TOP LEFT"}, ) def check_enum_mappings_name_change(self, style): class PossibleEnumModel(HasTraits): value = value = Enum('top left', 'top right') possible_values = List(['top left', 'top right']) formatted_view = View( UItem( 'value', editor=ImageEnumEditor( name="object.possible_values", format_func=lambda v: v.upper(), prefix='@icons:', suffix='_origin', path='dummy_path', ), style=style, ) ) model = PossibleEnumModel() with reraise_exceptions(), self.setup_ui( model, formatted_view ) as editor: self.assertEqual(editor.names, ["TOP LEFT", "TOP RIGHT"]) self.assertEqual( editor.mapping, {"TOP LEFT": "top left", "TOP RIGHT": "top right"}, ) self.assertEqual( editor.inverse_mapping, {"top left": "TOP LEFT", "top right": "TOP RIGHT"}, ) model.possible_values = ["top right", "top left"] self.assertEqual(editor.names, ["TOP RIGHT", "TOP LEFT"]) self.assertEqual( editor.mapping, {"TOP RIGHT": "top right", "TOP LEFT": "top left"}, ) self.assertEqual( editor.inverse_mapping, {"top right": "TOP RIGHT", "top left": "TOP LEFT"}, ) def test_simple_editor_mapping_values(self): self.check_enum_mappings_value_change("simple") def test_simple_editor_mapping_name(self): self.check_enum_mappings_name_change("simple") def test_custom_editor_mapping_values(self): self.check_enum_mappings_value_change("custom") def test_custom_editor_mapping_name(self): self.check_enum_mappings_name_change("custom") def test_readonly_editor_mapping_values(self): self.check_enum_mappings_value_change("readonly") def test_readonly_editor_name(self): class PossibleEnumModel(HasTraits): value = value = Enum('top left', 'top right') possible_values = List(['top left', 'top right']) formatted_view = View( UItem( 'value', editor=ImageEnumEditor( name="object.possible_values", format_func=lambda v: v.upper(), prefix='@icons:', suffix='_origin', path='dummy_path', ), style="readonly", ) ) model = PossibleEnumModel() with reraise_exceptions(), self.setup_ui( model, formatted_view ) as editor: # Readonly editor doesn't set up full mapping, only check that # str_value is mapped as expected self.assertEqual(model.value, "top left") self.assertEqual(editor.str_value, "TOP LEFT") class TestSimpleImageEnumEditor(BaseTestMixin, unittest.TestCase): def setUp(self): BaseTestMixin.setUp(self) def tearDown(self): BaseTestMixin.tearDown(self) @contextlib.contextmanager def setup_gui(self, model, view): with create_ui(model, dict(view=view)) as ui: process_cascade_events() yield ui.get_editors("value")[0] @requires_toolkit([ToolkitName.qt, ToolkitName.wx]) def test_simple_editor_more_cols(self): # Smoke test for setting up an editor with more than one column enum_edit = EnumModel() view = View( UItem( 'value', editor=ImageEnumEditor( values=[ 'top left', 'top right', 'bottom left', 'bottom right', ], prefix='@icons:', suffix='_origin', path='dummy_path', cols=4, ), style="simple", ), resizable=True, ) with reraise_exceptions(): self.setup_gui(enum_edit, view) @requires_toolkit([ToolkitName.wx]) def test_simple_editor_popup_editor(self): enum_edit = EnumModel() with reraise_exceptions(), self.setup_gui( enum_edit, get_view("simple") ) as editor: self.assertEqual(enum_edit.value, 'top left') # Set up ImageEnumDialog click_on_image(editor.control) process_cascade_events() # Check created buttons image_grid_control = editor.control.GetChildren()[0].GetChildren()[ 0 ] self.assertEqual( get_button_strings(image_grid_control), ['top left', 'top right', 'bottom left', 'bottom right'], ) # Select new image click_on_image(get_button_control(image_grid_control, 1)) process_cascade_events() self.assertEqual(enum_edit.value, 'top right') # Check that dialog window is closed self.assertEqual(list(editor.control.GetChildren()), []) @requires_toolkit([ToolkitName.qt]) def test_simple_editor_combobox(self): enum_edit = EnumModel() with reraise_exceptions(), self.setup_gui( enum_edit, get_view("simple") ) as editor: self.assertEqual(enum_edit.value, 'top left') # Smoke test for ImageEnumItemDelegate painting editor.control.showPopup() process_cascade_events() editor.control.setCurrentIndex(1) editor.control.hidePopup() process_cascade_events() self.assertEqual(enum_edit.value, 'top right') @requires_toolkit([ToolkitName.qt, ToolkitName.wx]) class TestCustomImageEnumEditor(BaseTestMixin, unittest.TestCase): def setUp(self): BaseTestMixin.setUp(self) def tearDown(self): BaseTestMixin.tearDown(self) @contextlib.contextmanager def setup_gui(self, model, view): with create_ui(model, dict(view=view)) as ui: process_cascade_events() yield ui.get_editors("value")[0] def test_custom_editor_more_cols(self): # Smoke test for setting up an editor with more than one column enum_edit = EnumModel() view = View( UItem( 'value', editor=ImageEnumEditor( values=[ 'top left', 'top right', 'bottom left', 'bottom right', ], prefix='@icons:', suffix='_origin', path='dummy_path', cols=4, ), style="custom", ), resizable=True, ) with reraise_exceptions(), self.setup_gui(enum_edit, view): pass def test_custom_editor_selection(self): enum_edit = EnumModel() with reraise_exceptions(), self.setup_gui( enum_edit, get_view("custom") ) as editor: self.assertEqual( get_button_strings(editor.control), ['top left', 'top right', 'bottom left', 'bottom right'], ) self.assertEqual(enum_edit.value, 'top left') self.assertEqual( get_all_button_selected_status(editor.control), [True, False, False, False], ) click_on_image(get_button_control(editor.control, 1)) process_cascade_events() self.assertEqual(enum_edit.value, 'top right') def test_custom_editor_value_changed(self): enum_edit = EnumModel() with reraise_exceptions(), self.setup_gui( enum_edit, get_view("custom") ) as editor: self.assertEqual( get_button_strings(editor.control), ['top left', 'top right', 'bottom left', 'bottom right'], ) self.assertEqual(enum_edit.value, 'top left') self.assertEqual( get_all_button_selected_status(editor.control), [True, False, False, False], ) enum_edit.value = 'top right' process_cascade_events() self.assertEqual( get_all_button_selected_status(editor.control), [False, True, False, False], ) @requires_toolkit([ToolkitName.qt, ToolkitName.wx]) class TestReadOnlyImageEnumEditor(BaseTestMixin, unittest.TestCase): def setUp(self): BaseTestMixin.setUp(self) def tearDown(self): BaseTestMixin.tearDown(self) def test_readonly_editor_value_changed(self): enum_edit = EnumModel() with reraise_exceptions(): with patch( cache_to_patch, wraps=image_cache ) as patched_cache, create_ui( enum_edit, dict(view=get_view("readonly")) ): self.assertEqual(enum_edit.value, 'top left') self.assertEqual( patched_cache.call_args[0][0], "@icons:top left_origin" ) enum_edit.value = 'top right' process_cascade_events() self.assertEqual( patched_cache.call_args[0][0], "@icons:top right_origin" ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/tests/editors/test_instance_editor.py0000644000175100001730000003443000000000000025433 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! import unittest from pyface.toolkit import toolkit_object from traits.api import Button, HasTraits, Instance, List, observe, Str, String from traitsui.api import InstanceEditor, Item, View from traitsui.tests._tools import ( BaseTestMixin, requires_toolkit, ToolkitName, ) from traitsui.testing.api import ( DisplayedText, Index, IsEnabled, KeyClick, KeySequence, MouseClick, SelectedText, TargetByName, UITester, ) ModalDialogTester = toolkit_object( "util.modal_dialog_tester:ModalDialogTester" ) no_modal_dialog_tester = ModalDialogTester.__name__ == "Unimplemented" class EditedInstance(HasTraits): value = Str() traits_view = View(Item("value"), buttons=["OK"]) class NamedInstance(HasTraits): name = Str() value = Str() traits_view = View(Item("name"), Item("value"), buttons=["OK"]) class ObjectWithInstance(HasTraits): inst = Instance(EditedInstance, args=()) class ObjectWithList(HasTraits): inst_list = List(Instance(NamedInstance)) inst = Instance(NamedInstance, args=()) reset_to_none = Button() change_options = Button() def _inst_list_default(self): return [ NamedInstance(name=value, value=value) for value in ['one', 'two', 'three'] ] @observe('reset_to_none') def _reset_inst_to_none(self, event): self.inst = None @observe('change_options') def _modify_inst_list(self, event): self.inst_list = [NamedInstance(name='one', value='one')] simple_view = View(Item("inst"), buttons=["OK"]) custom_view = View(Item("inst", style='custom'), buttons=["OK"]) selection_view = View( Item( "inst", editor=InstanceEditor(name='inst_list'), style='custom', ), buttons=["OK"], ) none_view = View( Item( "inst", editor=InstanceEditor(name='inst_list'), style='custom', ), Item('reset_to_none'), Item('change_options'), buttons=["OK"], ) non_editable_droppable_view = View( Item( "inst", editor=InstanceEditor( editable=False, droppable=True, ), style='custom', ), buttons=["OK"], ) non_editable_droppable_selectable_view = View( Item( "inst", editor=InstanceEditor( name='inst_list', editable=False, droppable=True, ), style='custom', ), buttons=["OK"], ) modal_view = View( Item("inst", style="simple", editor=InstanceEditor(kind="modal")) ) class ValidatedEditedInstance(HasTraits): some_string = String("A", maxlen=3) traits_view = View(Item('some_string')) class ObjectWithValidatedInstance(HasTraits): something = Instance(ValidatedEditedInstance, args=()) traits_view = View( Item('something', editor=InstanceEditor(), style='custom'), buttons=["OK", "Cancel"], ) class ObjectWithValidatedList(HasTraits): inst_list = List(Instance(HasTraits)) inst = Instance(HasTraits, ()) def _inst_list_default(self): return [ ValidatedEditedInstance(some_string=value) for value in ['a', 'b', 'c'] ] @requires_toolkit([ToolkitName.qt, ToolkitName.wx]) class TestInstanceEditor(BaseTestMixin, unittest.TestCase): def setUp(self): BaseTestMixin.setUp(self) def tearDown(self): BaseTestMixin.tearDown(self) def test_simple_editor(self): obj = ObjectWithInstance() tester = UITester() with tester.create_ui(obj, {'view': simple_view}) as ui: instance = tester.find_by_name(ui, "inst") instance.perform(MouseClick()) value_txt = instance.find_by_name("value") value_txt.perform(KeySequence("abc")) self.assertEqual(obj.inst.value, "abc") def test_custom_editor(self): obj = ObjectWithInstance() tester = UITester() with tester.create_ui(obj, {'view': custom_view}) as ui: value_txt = tester.find_by_name(ui, "inst").find_by_name("value") value_txt.perform(KeySequence("abc")) self.assertEqual(obj.inst.value, "abc") def test_custom_editor_with_selection(self): obj = ObjectWithList() tester = UITester() with tester.create_ui(obj, {'view': selection_view}) as ui: # test that the current object is None self.assertIsNone(obj.inst) # test that the displayed text is empty instance = tester.find_by_name(ui, "inst") text = instance.inspect(SelectedText()) self.assertEqual(text, '') # test that changing selection works instance.locate(Index(1)).perform(MouseClick()) self.assertIs(obj.inst, obj.inst_list[1]) # test that the displayed text is correct text = instance.inspect(SelectedText()) self.assertEqual(text, obj.inst_list[1].name) # test editing the view works value_txt = instance.find_by_name("value") value_txt.perform(KeySequence("abc")) self.assertEqual(obj.inst.value, "twoabc") def test_custom_editor_with_selection_initialized(self): obj = ObjectWithList() obj.inst = obj.inst_list[1] tester = UITester() with tester.create_ui(obj, {'view': selection_view}) as ui: # test that the current object is the correct one self.assertIs(obj.inst, obj.inst_list[1]) # test that the displayed text is correct instance = tester.find_by_name(ui, "inst") text = instance.inspect(SelectedText()) self.assertEqual(text, obj.inst.name) # A regression test for issue enthought/traitsui#1725 def test_custom_editor_with_selection_change_option_name(self): obj = ObjectWithList() tester = UITester() with tester.create_ui(obj, {'view': selection_view}) as ui: # test that the current object is None self.assertIsNone(obj.inst) # actually select the first item instance = tester.find_by_name(ui, "inst") instance.locate(Index(0)).perform(MouseClick()) self.assertIs(obj.inst, obj.inst_list[0]) # test that the displayed text is correct after change name_txt = instance.find_by_name("name") for _ in range(3): name_txt.perform(KeyClick("Backspace")) name_txt.perform(KeySequence("Something New")) text = instance.inspect(SelectedText()) self.assertEqual(text, "Something New") self.assertEqual("Something New", obj.inst_list[0].name) def test_custom_editor_resynch_editor(self): edited_inst = EditedInstance(value='hello') obj = ObjectWithInstance(inst=edited_inst) tester = UITester() with tester.create_ui(obj, {'view': custom_view}) as ui: value_txt = tester.find_by_name(ui, "inst").find_by_name("value") displayed = value_txt.inspect(DisplayedText()) self.assertEqual(displayed, "hello") edited_inst.value = "bye" displayed = value_txt.inspect(DisplayedText()) self.assertEqual(displayed, "bye") def test_simple_editor_resynch_editor(self): edited_inst = EditedInstance(value='hello') obj = ObjectWithInstance(inst=edited_inst) tester = UITester() with tester.create_ui(obj, {'view': simple_view}) as ui: instance = tester.find_by_name(ui, "inst") instance.perform(MouseClick()) value_txt = instance.find_by_name("value") displayed = value_txt.inspect(DisplayedText()) self.assertEqual(displayed, "hello") edited_inst.value = "bye" displayed = value_txt.inspect(DisplayedText()) self.assertEqual(displayed, "bye") def test_simple_editor_parent_closed(self): obj = ObjectWithInstance() tester = UITester() with tester.create_ui(obj, {'view': simple_view}) as ui: instance = tester.find_by_name(ui, "inst") instance.perform(MouseClick()) @unittest.skipIf(no_modal_dialog_tester, "ModalDialogTester unavailable") def test_simple_editor_modal(self): # Test launching the instance editor with kind set to 'modal' obj = ObjectWithInstance() ui_tester = UITester() with ui_tester.create_ui(obj, dict(view=modal_view)) as ui: def click_button(): ui_tester.find_by_name(ui, "inst").perform(MouseClick()) def when_opened(tester): with tester.capture_error(): try: dialog_ui = tester.get_dialog_widget()._ui # If auto_process_events was not set to false, this # will block due to deadlocks with ModalDialogTester. ui_tester = UITester(auto_process_events=False) value = ui_tester.find_by_name(dialog_ui, "value") value.perform(KeySequence("Hello")) self.assertEqual(obj.inst.value, "") ok_button = ui_tester.find_by_id(dialog_ui, "OK") ok_button.perform(MouseClick()) finally: # If the block above fails, the dialog will block # forever. Close it regardless. if tester.get_dialog_widget() is not None: tester.close(accept=True) mdtester = ModalDialogTester(click_button) mdtester.open_and_run(when_opened=when_opened) self.assertTrue(mdtester.dialog_was_opened) self.assertEqual(obj.inst.value, "Hello") # A regression test for issue enthought/traitsui#1501 def test_propagate_errors(self): obj = ObjectWithValidatedInstance() ui_tester = UITester() with ui_tester.create_ui(obj) as ui: something_ui = ui_tester.find_by_name(ui, "something") some_string_field = something_ui.locate( TargetByName('some_string') ) some_string_field.perform(KeySequence("abcd")) some_string_field.perform(KeyClick("Enter")) ok_button = ui_tester.find_by_id(ui, "OK") instance_editor_ui = something_ui._target._ui instance_editor_ui_parent = something_ui._target._ui.parent self.assertNotEqual(instance_editor_ui, ui) self.assertEqual(instance_editor_ui_parent, ui) self.assertEqual(instance_editor_ui.errors, ui.errors) self.assertFalse(ok_button.inspect(IsEnabled())) def test_propagate_errors_switch_selection(self): obj = ObjectWithValidatedList() ui_tester = UITester() with ui_tester.create_ui(obj, {'view': selection_view}) as ui: something_ui = ui_tester.find_by_name(ui, "inst") something_ui.locate(Index(0)).perform(MouseClick()) some_string_field = something_ui.locate( TargetByName('some_string') ) some_string_field.perform(KeySequence("bcde")) some_string_field.perform(KeyClick("Enter")) ok_button = ui_tester.find_by_id(ui, "OK") instance_editor_ui = something_ui._target._ui instance_editor_ui_parent = something_ui._target._ui.parent self.assertNotEqual(instance_editor_ui, ui) self.assertEqual(instance_editor_ui_parent, ui) self.assertEqual(instance_editor_ui.errors, ui.errors) self.assertFalse(ok_button.inspect(IsEnabled())) # change to a different selected that is not in an error state something_ui.locate(Index(1)).perform(MouseClick()) self.assertTrue(ok_button.inspect(IsEnabled())) # regression test for enthought/traitsui#1134 def test_none_selected(self): obj = ObjectWithList() tester = UITester() with tester.create_ui(obj, {'view': none_view}) as ui: # test that the current object is None self.assertIsNone(obj.inst) # test that the displayed text is empty to start instance = tester.find_by_name(ui, "inst") text = instance.inspect(SelectedText()) self.assertEqual(text, '') # test that changing selection works and displayed text is correct instance.locate(Index(1)).perform(MouseClick()) self.assertIs(obj.inst, obj.inst_list[1]) text = instance.inspect(SelectedText()) self.assertEqual(text, obj.inst_list[1].name) # test resetting selection to None reset_to_none_button = tester.find_by_name(ui, "reset_to_none") reset_to_none_button.perform(MouseClick()) self.assertIsNone(obj.inst) text = instance.inspect(SelectedText()) self.assertEqual(text, '') # change selection again instance.locate(Index(1)).perform(MouseClick()) self.assertIs(obj.inst, obj.inst_list[1]) text = instance.inspect(SelectedText()) self.assertEqual(text, obj.inst_list[1].name) # test modifying list of selectable options returns current object # to None change_options_button = tester.find_by_name(ui, "change_options") change_options_button.perform(MouseClick()) self.assertIsNone(obj.inst) text = instance.inspect(SelectedText()) self.assertEqual(text, '') # regression test for enthought/traitsui#1478 def test_droppable(self): obj = ObjectWithInstance() obj_with_list = ObjectWithList() tester = UITester() with tester.create_ui(obj, {'view': non_editable_droppable_view}): pass with tester.create_ui( obj_with_list, {'view': non_editable_droppable_selectable_view} ): pass ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/tests/editors/test_list_editor.py0000644000175100001730000002522200000000000024601 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! import shutil import tempfile import unittest from pyface.toolkit import toolkit_object from traits.api import ( Directory, HasStrictTraits, Instance, Int, List, Str, TraitError, ) from traitsui.api import Item, ListEditor, View from traitsui.testing.api import ( DisplayedText, Index, KeyClick, KeySequence, LocationNotSupported, MouseClick, Textbox, UITester, ) from traitsui.tests._tools import ( requires_toolkit, ToolkitName, ) ModalDialogTester = toolkit_object( "util.modal_dialog_tester:ModalDialogTester" ) # 'Person' class: class Person(HasStrictTraits): # Trait definitions: name = Str() age = Int() # Traits view definition: traits_view = View('name', 'age', width=0.18, buttons=['OK', 'Cancel']) def get_people(): # Sample data: return [ Person(name='Dave', age=39), Person(name='Mike', age=28), Person(name='Joe', age=34), Person(name='Tom', age=22), Person(name='Dick', age=63), Person(name='Harry', age=46), Person(name='Sally', age=43), Person(name='Fields', age=31), ] # 'ListTraitTest' class: class ListTraitTest(HasStrictTraits): # Trait definitions: people = List(Instance(Person, ())) num_columns = Int(1) style = Str("custom") # Traits view definitions: def default_traits_view(self): view = View( Item( 'people', label='List', id='list', style=self.style, editor=ListEditor(style=self.style, columns=self.num_columns), ), resizable=True, ) return view class Phonebook(HasStrictTraits): people = List(Instance(Person)) notebook_view = View( Item( "people", style="custom", editor=ListEditor(use_notebook=True), ) ) @requires_toolkit([ToolkitName.qt, ToolkitName.wx]) class TestCustomListEditor(unittest.TestCase): def test_locate_element_and_edit(self): # varying the number of columns in the view tests the logic for # getting the correct nested ui for col in range(1, 5): obj = ListTraitTest(people=get_people(), num_columns=col) tester = UITester() with tester.create_ui(obj) as ui: # sanity check self.assertEqual(obj.people[7].name, "Fields") people_list = tester.find_by_name(ui, "people") item = people_list.locate(Index(7)) name_field = item.find_by_name("name") for _ in range(6): name_field.perform(KeyClick("Backspace")) name_field.perform(KeySequence("David")) displayed = name_field.inspect(DisplayedText()) self.assertEqual(obj.people[7].name, "David") self.assertEqual(displayed, obj.people[7].name) def test_useful_err_message(self): obj = ListTraitTest(people=get_people()) tester = UITester() with tester.create_ui(obj) as ui: with self.assertRaises(LocationNotSupported) as exc: people_list = tester.find_by_name(ui, "people") people_list.locate(Textbox()) self.assertIn(Index, exc.exception.supported) def test_index_out_of_range(self): obj = ListTraitTest(people=get_people()) tester = UITester() with tester.create_ui(obj) as ui: people_list = tester.find_by_name(ui, "people") with self.assertRaises(IndexError): people_list.locate(Index(10)) @requires_toolkit([ToolkitName.qt, ToolkitName.wx]) class TestSimpleListEditor(unittest.TestCase): def test_locate_element_and_edit(self): obj = ListTraitTest(people=get_people(), style="simple") tester = UITester() with tester.create_ui(obj) as ui: # sanity check self.assertEqual(obj.people[7].name, "Fields") people_list = tester.find_by_name(ui, "people") item = people_list.locate(Index(7)) item.perform(MouseClick()) name_field = item.find_by_name("name") for _ in range(6): name_field.perform(KeyClick("Backspace")) name_field.perform(KeySequence("David")) displayed = name_field.inspect(DisplayedText()) self.assertEqual(obj.people[7].name, "David") self.assertEqual(displayed, obj.people[7].name) def test_useful_err_message(self): obj = ListTraitTest(people=get_people(), style="simple") tester = UITester() with tester.create_ui(obj) as ui: with self.assertRaises(LocationNotSupported) as exc: people_list = tester.find_by_name(ui, "people") people_list.locate(Textbox()) self.assertIn(Index, exc.exception.supported) def test_index_out_of_range(self): obj = ListTraitTest(people=get_people(), style="simple") tester = UITester() with tester.create_ui(obj) as ui: people_list = tester.find_by_name(ui, "people") with self.assertRaises(IndexError): people_list.locate(Index(10)) # regression test for enthought/traitsui#1154 @requires_toolkit([ToolkitName.qt]) def test_add_item_fails(self): class Foo(HasStrictTraits): dirs = List(Directory(exists=True)) obj = Foo() tester = UITester(auto_process_events=False) with tester.create_ui(obj) as ui: dirs_list_editor = tester.find_by_name(ui, "dirs") def trigger_error(): try: dirs_list_editor._target.add_empty() except TraitError: return False return True mdtester = ModalDialogTester(trigger_error) mdtester.open_and_run(lambda x: x.close()) # we want an error dialog to open, but don't want to raise a # TraitError and crash the full application self.assertTrue(mdtester.dialog_was_opened) self.assertTrue(mdtester.result) self.assertEqual(len(obj.dirs), 0) # this test hits a problem on wx, see issue enthought/traitsui#1653 @requires_toolkit([ToolkitName.qt]) def test_default_factory(self): temp_dir = tempfile.mkdtemp() def test_callable(): return temp_dir class Foo(HasStrictTraits): dirs = List(Directory(exists=True)) view = View( Item("dirs", editor=ListEditor(item_factory=test_callable)) ) obj = Foo() tester = UITester() with tester.create_ui(obj) as ui: dirs_list_editor = tester.find_by_name(ui, "dirs") # should not raise error (see above test_add_item_fails) dirs_list_editor._target.add_empty() self.assertEqual(len(obj.dirs), 1) shutil.rmtree(temp_dir) def test_default_factory_with_args(self): class Foo(HasStrictTraits): bar = Int() baz = Str() def test_callable(bar, baz=''): return Foo(bar=bar, baz=baz) class TestFoo(HasStrictTraits): foos = List(Foo) view = View( Item( "foos", editor=ListEditor( item_factory=test_callable, item_factory_args=(7,), item_factory_kwargs={'baz': "python"}, ), ) ) obj = TestFoo() tester = UITester() with tester.create_ui(obj) as ui: foos_list_editor = tester.find_by_name(ui, "foos") # should not raise error (see above test_add_item_fails) foos_list_editor._target.add_empty() self.assertEqual(len(obj.foos), 1) self.assertEqual(obj.foos[0].bar, 7) self.assertEqual(obj.foos[0].baz, "python") @requires_toolkit([ToolkitName.qt, ToolkitName.wx]) class TestNotebookListEditor(unittest.TestCase): def test_modify_person_name(self): phonebook = Phonebook( people=get_people(), ) tester = UITester() with tester.create_ui(phonebook, dict(view=notebook_view)) as ui: list_ = tester.find_by_name(ui, "people") list_.locate(Index(1)).perform(MouseClick()) name_field = list_.locate(Index(1)).find_by_name("name") for _ in range(4): name_field.perform(KeyClick("Backspace")) name_field.perform(KeySequence("Pete")) self.assertEqual(phonebook.people[1].name, "Pete") def test_get_person_name(self): person1 = Person() person2 = Person(name="Mary") phonebook = Phonebook( people=[person1, person2], ) tester = UITester() with tester.create_ui(phonebook, dict(view=notebook_view)) as ui: list_ = tester.find_by_name(ui, "people") list_.locate(Index(1)).perform(MouseClick()) name_field = list_.locate(Index(1)).find_by_name("name") actual = name_field.inspect(DisplayedText()) self.assertEqual(actual, "Mary") def test_index_out_of_bound(self): phonebook = Phonebook( people=[], ) tester = UITester() with tester.create_ui(phonebook, dict(view=notebook_view)) as ui: with self.assertRaises(IndexError): tester.find_by_name(ui, "people").locate(Index(0)).perform( MouseClick() ) # regression test for enthought/traitsui#1790 def test_initial_selected(self): class PhoneBookWithSelected(Phonebook): selected = Instance(Person) traits_view = View( Item( "people", style="custom", editor=ListEditor( use_notebook=True, selected='selected', ), ) ) tester = UITester() phonebook = PhoneBookWithSelected(people=get_people()) with tester.create_ui(phonebook) as ui: self.assertEqual( phonebook.selected, phonebook.people[0] ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/tests/editors/test_liststr_editor.py0000644000175100001730000000230400000000000025326 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Test case for ListStrEditor and ListStrAdapter """ import unittest from traits.has_traits import HasTraits from traits.trait_types import List, Str from traitsui.list_str_adapter import ListStrAdapter from traitsui.tests._tools import BaseTestMixin class TraitObject(HasTraits): list_str = List(Str) class TestListStrAdapter(BaseTestMixin, unittest.TestCase): def setUp(self): BaseTestMixin.setUp(self) def tearDown(self): BaseTestMixin.tearDown(self) def test_list_str_adapter_length(self): """Test the ListStringAdapter len method""" object = TraitObject() object.list_str = ["hello"] adapter = ListStrAdapter() self.assertEqual(adapter.len(object, "list_str"), 1) self.assertEqual(adapter.len(None, "list_str"), 0) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/tests/editors/test_liststr_editor_selection.py0000644000175100001730000006372400000000000027410 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Test case for bug (wx, Mac OS X) A ListStrEditor was not checking for valid item indexes under Wx. This was most noticeable when the selected_index was set in the editor factory. """ import contextlib import platform import unittest from traits.has_traits import HasTraits from traits.trait_types import List, Int, Str from traitsui.item import Item from traitsui.view import View from traitsui.editors.list_str_editor import ListStrEditor from traitsui.tests._tools import ( BaseTestMixin, create_ui, is_wx, is_qt, process_cascade_events, requires_toolkit, reraise_exceptions, ToolkitName, ) is_windows = platform.system() == "Windows" class ListStrModel(HasTraits): value = List(["one", "two", "three"]) class ListStrEditorWithSelectedIndex(HasTraits): values = List(Str()) selected_index = Int() selected_indices = List(Int()) selected = Str() def get_view(**kwargs): return View( Item( "value", editor=ListStrEditor(**kwargs), ) ) single_select_view = View( Item( "values", show_label=False, editor=ListStrEditor(selected_index="selected_index", editable=False), ), buttons=["OK"], ) multi_select_view = View( Item( "values", show_label=False, editor=ListStrEditor( multi_select=True, selected_index="selected_indices", editable=False, ), ), buttons=["OK"], ) single_select_item_view = View( Item( "values", show_label=False, editor=ListStrEditor(selected="selected", editable=False), ), buttons=["OK"], ) def get_selected_indices(editor): """Returns a list of the indices of all currently selected list items.""" if is_wx(): import wx # "item" in this context means "index of the item" item = -1 selected = [] while True: item = editor.control.GetNextItem( item, wx.LIST_NEXT_ALL, wx.LIST_STATE_SELECTED ) if item == -1: break selected.append(item) return selected elif is_qt(): indices = editor.list_view.selectionModel().selectedRows() return [i.row() for i in indices] else: raise unittest.SkipTest("Test not implemented for this toolkit") def set_selected_single(editor, index): """Selects a specified item in an editor with multi_select=False.""" if is_wx(): editor.control.Select(index) elif is_qt(): from pyface.qt.QtGui import QItemSelectionModel smodel = editor.list_view.selectionModel() mi = editor.model.index(index) smodel.select(mi, QItemSelectionModel.SelectionFlag.ClearAndSelect) else: raise unittest.SkipTest("Test not implemented for this toolkit") def set_selected_multiple(editor, indices): """Clears old selection and selects specified items in an editor with multi_select=True. """ if is_wx(): clear_selection(editor) for index in indices: editor.control.Select(index) elif is_qt(): from pyface.qt.QtGui import QItemSelectionModel clear_selection(editor) smodel = editor.list_view.selectionModel() for index in indices: mi = editor.model.index(index) smodel.select(mi, QItemSelectionModel.SelectionFlag.Select) else: raise unittest.SkipTest("Test not implemented for this toolkit") def clear_selection(editor): """Clears existing selection.""" if is_wx(): import wx currently_selected = get_selected_indices(editor) # Deselect all currently selected items for selected_index in currently_selected: editor.control.SetItemState( selected_index, 0, wx.LIST_STATE_SELECTED ) elif is_qt(): editor.list_view.selectionModel().clearSelection() else: raise unittest.SkipTest("Test not implemented for this toolkit") def right_click_item(editor, index): """Right clicks on the specified item.""" if is_wx(): import wx control = editor.control event = wx.ListEvent( wx.EVT_LIST_ITEM_RIGHT_CLICK.typeId, control.GetId() ) event.SetIndex(index) wx.PostEvent(control, event) elif is_qt(): from pyface.qt import QtCore from pyface.qt.QtTest import QTest view = editor.list_view q_model_index = view.model().index(index, 0) view.scrollTo(q_model_index) rect = view.visualRect(q_model_index) QTest.mouseClick( view.viewport(), QtCore.Qt.MouseButton.RightButton, QtCore.Qt.KeyboardModifier.NoModifier, rect.center(), ) else: raise unittest.SkipTest("Test not implemented for this toolkit") def right_click_center(editor): """Right click the middle of the widget.""" if is_wx(): import wx control = editor.control event = wx.ListEvent( wx.EVT_LIST_ITEM_RIGHT_CLICK.typeId, control.GetId() ) wx.PostEvent(control, event) elif is_qt(): from pyface.qt import QtCore from pyface.qt.QtTest import QTest view = editor.list_view rect = view.rect() QTest.mouseClick( view.viewport(), QtCore.Qt.MouseButton.RightButton, QtCore.Qt.KeyboardModifier.NoModifier, rect.center(), ) else: raise unittest.SkipTest("Test not implemented for this toolkit") @unittest.skipIf(is_wx(), "Issue enthought/traitsui#752") @requires_toolkit([ToolkitName.qt, ToolkitName.wx]) class TestListStrEditor(BaseTestMixin, unittest.TestCase): def setUp(self): BaseTestMixin.setUp(self) def tearDown(self): BaseTestMixin.tearDown(self) @contextlib.contextmanager def setup_gui(self, model, view): with create_ui(model, dict(view=view)) as ui: process_cascade_events() editor = ui.get_editors("value")[0] yield editor def test_list_str_editor_single_selection(self): with reraise_exceptions(), self.setup_gui( ListStrModel(), get_view() ) as editor: if is_qt(): # No initial selection self.assertEqual(editor.selected_index, -1) self.assertEqual(editor.selected, None) elif is_wx(): # First element selected initially self.assertEqual(editor.selected_index, 0) self.assertEqual(editor.selected, "one") set_selected_single(editor, 1) process_cascade_events() self.assertEqual(editor.selected_index, 1) self.assertEqual(editor.selected, "two") set_selected_single(editor, 2) process_cascade_events() self.assertEqual(editor.selected_index, 2) self.assertEqual(editor.selected, "three") clear_selection(editor) process_cascade_events() self.assertEqual(editor.selected_index, -1) self.assertEqual(editor.selected, None) def test_list_str_editor_multi_selection(self): view = get_view(multi_select=True) with reraise_exceptions(), self.setup_gui( ListStrModel(), view ) as editor: self.assertEqual(editor.multi_selected_indices, []) self.assertEqual(editor.multi_selected, []) set_selected_multiple(editor, [0, 1]) process_cascade_events() self.assertEqual(editor.multi_selected_indices, [0, 1]) self.assertEqual(editor.multi_selected, ["one", "two"]) set_selected_multiple(editor, [2]) process_cascade_events() self.assertEqual(editor.multi_selected_indices, [2]) self.assertEqual(editor.multi_selected, ["three"]) clear_selection(editor) process_cascade_events() self.assertEqual(editor.multi_selected_indices, []) self.assertEqual(editor.multi_selected, []) def test_list_str_editor_single_selection_changed(self): with reraise_exceptions(), self.setup_gui( ListStrModel(), get_view() ) as editor: if is_qt(): # No initial selection self.assertEqual(get_selected_indices(editor), []) elif is_wx(): # First element selected initially self.assertEqual(get_selected_indices(editor), [0]) editor.selected_index = 1 process_cascade_events() self.assertEqual(get_selected_indices(editor), [1]) self.assertEqual(editor.selected, "two") editor.selected = "three" process_cascade_events() self.assertEqual(get_selected_indices(editor), [2]) self.assertEqual(editor.selected_index, 2) # Selected set to invalid value doesn't change anything editor.selected = "four" process_cascade_events() self.assertEqual(get_selected_indices(editor), [2]) self.assertEqual(editor.selected_index, 2) # Selected index changed to editor.selected_index = -1 process_cascade_events() if is_qt(): # -1 clears selection self.assertEqual(get_selected_indices(editor), []) self.assertEqual(editor.selected, None) elif is_wx(): # Visually selects everything but doesn't update `selected` self.assertEqual(editor.selected, "four") self.assertEqual(get_selected_indices(editor), [0, 1, 2]) def test_list_str_editor_multi_selection_changed(self): view = get_view(multi_select=True) with reraise_exceptions(), self.setup_gui( ListStrModel(), view ) as editor: self.assertEqual(get_selected_indices(editor), []) editor.multi_selected_indices = [0, 1] process_cascade_events() self.assertEqual(get_selected_indices(editor), [0, 1]) self.assertEqual(editor.multi_selected, ["one", "two"]) editor.multi_selected = ["three", "one"] process_cascade_events() self.assertEqual(sorted(get_selected_indices(editor)), [0, 2]) self.assertEqual(sorted(editor.multi_selected_indices), [0, 2]) editor.multi_selected = ["three", "four"] process_cascade_events() if is_qt(): # Invalid values assigned to multi_selected are ignored self.assertEqual(get_selected_indices(editor), [2]) self.assertEqual(editor.multi_selected_indices, [2]) elif is_wx(): # Selection indices are not updated at all self.assertEqual(get_selected_indices(editor), [0, 2]) self.assertEqual(editor.multi_selected_indices, [0, 2]) # Setting selected indices to an empty list clears selection editor.multi_selected_indices = [] process_cascade_events() self.assertEqual(get_selected_indices(editor), []) self.assertEqual(editor.multi_selected, []) def test_list_str_editor_multi_selection_items_changed(self): view = get_view(multi_select=True) with reraise_exceptions(), self.setup_gui( ListStrModel(), view ) as editor: self.assertEqual(get_selected_indices(editor), []) editor.multi_selected_indices.extend([0, 1]) process_cascade_events() self.assertEqual(get_selected_indices(editor), [0, 1]) self.assertEqual(editor.multi_selected, ["one", "two"]) editor.multi_selected_indices[1] = 2 process_cascade_events() self.assertEqual(get_selected_indices(editor), [0, 2]) self.assertEqual(editor.multi_selected, ["one", "three"]) editor.multi_selected[0] = "two" process_cascade_events() self.assertEqual(sorted(get_selected_indices(editor)), [1, 2]) self.assertEqual(sorted(editor.multi_selected_indices), [1, 2]) # If a change in multi_selected involves an invalid value, nothing # is changed editor.multi_selected[0] = "four" process_cascade_events() self.assertEqual(sorted(get_selected_indices(editor)), [1, 2]) self.assertEqual(sorted(editor.multi_selected_indices), [1, 2]) def test_list_str_editor_item_count(self): model = ListStrModel() # Without auto_add with reraise_exceptions(), create_ui( model, dict(view=get_view()) ) as ui: process_cascade_events() editor = ui.get_editors("value")[0] self.assertEqual(editor.item_count, 3) # With auto_add with reraise_exceptions(), create_ui( model, dict(view=get_view(auto_add=True)) ) as ui: process_cascade_events() editor = ui.get_editors("value")[0] self.assertEqual(editor.item_count, 3) def test_list_str_editor_refresh_editor(self): # Smoke test for refresh_editor/refresh_ with reraise_exceptions(), self.setup_gui( ListStrModel(), get_view() ) as editor: if is_qt(): editor.refresh_editor() elif is_wx(): editor._refresh() process_cascade_events() @requires_toolkit([ToolkitName.qt]) def test_list_str_editor_update_editor_single_qt(self): # QT editor uses selected items as the source of truth when updating model = ListStrModel() with reraise_exceptions(), self.setup_gui(model, get_view()) as editor: set_selected_single(editor, 0) process_cascade_events() # Sanity check self.assertEqual(editor.selected_index, 0) self.assertEqual(editor.selected, "one") model.value = ["two", "one"] process_cascade_events() # Selected remains "one" and indices are updated accordingly self.assertEqual(get_selected_indices(editor), [1]) self.assertEqual(editor.selected_index, 1) self.assertEqual(editor.selected, "one") # Removing "one" creates a case of no longer valid selection model.value = ["two", "three"] process_cascade_events() # Internal view model selection is reset, but editor selection # values are not (see issue enthought/traitsui#872) self.assertEqual(get_selected_indices(editor), []) self.assertEqual(editor.selected_index, 1) self.assertEqual(editor.selected, "one") @requires_toolkit([ToolkitName.wx]) def test_list_str_editor_update_editor_single_wx(self): # WX editor uses selected indices as the source of truth when updating model = ListStrModel() with reraise_exceptions(), self.setup_gui(model, get_view()) as editor: set_selected_single(editor, 0) process_cascade_events() # Sanity check self.assertEqual(editor.selected_index, 0) self.assertEqual(editor.selected, "one") model.value = ["two", "one"] process_cascade_events() # Selected_index remains 0 and selected is updated accordingly self.assertEqual(get_selected_indices(editor), [0]) self.assertEqual(editor.selected_index, 0) self.assertEqual(editor.selected, "two") # Empty list creates a case of no longer valid selection model.value = [] process_cascade_events() # Internal view model selection is reset, but editor selection # values are not (see issue enthought/traitsui#872) self.assertEqual(get_selected_indices(editor), []) self.assertEqual(editor.selected_index, 0) self.assertEqual(editor.selected, "two") @requires_toolkit([ToolkitName.qt]) def test_list_str_editor_update_editor_multi_qt(self): # QT editor uses selected items as the source of truth when updating model = ListStrModel() view = get_view(multi_select=True) with reraise_exceptions(), self.setup_gui(model, view) as editor: set_selected_multiple(editor, [0]) process_cascade_events() # Sanity check self.assertEqual(editor.multi_selected_indices, [0]) self.assertEqual(editor.multi_selected, ["one"]) model.value = ["two", "one"] process_cascade_events() # Selected remains "one" and indices are updated accordingly self.assertEqual(get_selected_indices(editor), [1]) self.assertEqual(editor.multi_selected_indices, [1]) self.assertEqual(editor.multi_selected, ["one"]) # Removing "one" creates a case of no longer valid selection. model.value = ["two", "three"] process_cascade_events() # Internal view model selection is reset, but editor selection # values are not (see issue enthought/traitsui#872) self.assertEqual(get_selected_indices(editor), []) self.assertEqual(editor.multi_selected_indices, [1]) self.assertEqual(editor.multi_selected, ["one"]) @requires_toolkit([ToolkitName.wx]) def test_list_str_editor_update_editor_multi_wx(self): # WX editor uses selected indices as the source of truth when updating model = ListStrModel() view = get_view(multi_select=True) with reraise_exceptions(), self.setup_gui(model, view) as editor: set_selected_multiple(editor, [0]) process_cascade_events() # Sanity check self.assertEqual(editor.multi_selected_indices, [0]) self.assertEqual(editor.multi_selected, ["one"]) model.value = ["two", "one"] process_cascade_events() # Selected_index remains 0 and selected is updated accordingly self.assertEqual(get_selected_indices(editor), [0]) self.assertEqual(editor.multi_selected_indices, [0]) self.assertEqual(editor.multi_selected, ["two"]) # Empty list creates a case of no longer valid selection model.value = [] process_cascade_events() # Internal view model selection is reset, but editor selection # values are not (see issue enthought/traitsui#872) self.assertEqual(get_selected_indices(editor), []) self.assertEqual(editor.multi_selected_indices, [0]) self.assertEqual(editor.multi_selected, ["two"]) # wx editor doesn't have a `callx` method @requires_toolkit([ToolkitName.qt]) def test_list_str_editor_callx(self): model = ListStrModel() def change_value(model, value): model.value = value with reraise_exceptions(), self.setup_gui(model, get_view()) as editor: set_selected_single(editor, 0) process_cascade_events() # Sanity check self.assertEqual(editor.selected_index, 0) self.assertEqual(editor.selected, "one") editor.callx(change_value, model, ["two", "one"]) process_cascade_events() # Nothing is updated self.assertEqual(get_selected_indices(editor), [0]) self.assertEqual(editor.selected_index, 0) self.assertEqual(editor.selected, "one") # wx editor doesn't have a `setx` method @requires_toolkit([ToolkitName.qt]) def test_list_str_editor_setx(self): with reraise_exceptions(), self.setup_gui( ListStrModel(), get_view() ) as editor: set_selected_single(editor, 0) process_cascade_events() # Sanity check self.assertEqual(editor.selected_index, 0) self.assertEqual(editor.selected, "one") editor.setx(selected="two") process_cascade_events() # Specified attribute is modified self.assertEqual(editor.selected, "two") # But nothing else is updated # FIXME issue enthought/traitsui#867 with self.assertRaises(AssertionError): self.assertEqual(get_selected_indices(editor), [0]) self.assertEqual(editor.selected_index, 0) self.assertEqual(get_selected_indices(editor), [1]) self.assertEqual(editor.selected_index, 1) def test_list_str_editor_horizontal_lines(self): # Smoke test for painting horizontal lines view = get_view(horizontal_lines=True) with reraise_exceptions(), self.setup_gui(ListStrModel(), view): pass def test_list_str_editor_title(self): # Smoke test for adding a title with reraise_exceptions(), self.setup_gui( ListStrModel(), get_view(title="testing") ): pass def test_list_str_editor_right_click(self): class ListStrModelRightClick(HasTraits): value = List(["one", "two", "three"]) right_clicked = Str() right_clicked_index = Int() model = ListStrModelRightClick() view = get_view( right_clicked="object.right_clicked", right_clicked_index="object.right_clicked_index", ) with reraise_exceptions(), self.setup_gui(model, view) as editor: self.assertEqual(model.right_clicked, "") self.assertEqual(model.right_clicked_index, 0) right_click_item(editor, 1) process_cascade_events() self.assertEqual(model.right_clicked, "two") self.assertEqual(model.right_clicked_index, 1) def test_list_str_editor_right_click_out_of_bound(self): class ListStrModelRightClick(HasTraits): value = List([]) right_clicked = Str() right_clicked_index = Int() model = ListStrModelRightClick() view = get_view( right_clicked="object.right_clicked", right_clicked_index="object.right_clicked_index", ) with reraise_exceptions(), self.setup_gui(model, view) as editor: self.assertEqual(model.right_clicked, "") self.assertEqual(model.right_clicked_index, 0) right_click_center(editor) process_cascade_events() self.assertEqual(model.right_clicked, "") self.assertEqual(model.right_clicked_index, 0) class TestListStrEditorSelection(BaseTestMixin, unittest.TestCase): def setUp(self): BaseTestMixin.setUp(self) def tearDown(self): BaseTestMixin.tearDown(self) @requires_toolkit([ToolkitName.wx]) def test_wx_list_str_selected_index(self): # behavior: when starting up, the obj = ListStrEditorWithSelectedIndex( values=["value1", "value2"], selected_index=1 ) with reraise_exceptions(), create_ui( obj, dict(view=single_select_view) ) as ui: editor = ui.get_editors("values")[0] # the following is equivalent to setting the text in the text # control, then pressing OK selected_1 = get_selected_indices(editor) obj.selected_index = 0 selected_2 = get_selected_indices(editor) # the number traits should be between 3 and 8 self.assertEqual(selected_1, [1]) self.assertEqual(selected_2, [0]) @requires_toolkit([ToolkitName.wx]) def test_wx_list_str_multi_selected_index(self): # behavior: when starting up, the obj = ListStrEditorWithSelectedIndex( values=["value1", "value2"], selected_indices=[1] ) with reraise_exceptions(), create_ui( obj, dict(view=multi_select_view) ) as ui: editor = ui.get_editors("values")[0] # the following is equivalent to setting the text in the text # control, then pressing OK selected_1 = get_selected_indices(editor) obj.selected_indices = [0] selected_2 = get_selected_indices(editor) # the number traits should be between 3 and 8 self.assertEqual(selected_1, [1]) self.assertEqual(selected_2, [0]) @requires_toolkit([ToolkitName.qt]) def test_selection_listener_disconnected(self): """Check that selection listeners get correctly disconnected""" from pyface.qt.QtGui import QApplication, QItemSelectionModel from pyface.ui.qt.util.testing import event_loop obj = ListStrEditorWithSelectedIndex(values=["value1", "value2"]) with reraise_exceptions(): qt_app = QApplication.instance() if qt_app is None: qt_app = QApplication([]) # open the UI and run until the dialog is closed with create_ui(obj, dict(view=single_select_item_view)) as ui: pass # now run again and change the selection with create_ui( obj, dict(view=single_select_item_view) ) as ui, event_loop(): editor = ui.get_editors("values")[0] list_view = editor.list_view mi = editor.model.index(1) list_view.selectionModel().select( mi, QItemSelectionModel.SelectionFlag.ClearAndSelect ) obj.selected = "value2" if __name__ == "__main__": # Executing the file opens the dialog for manual testing editor = ListStrEditorWithSelectedIndex( values=["value1", "value2"], selected_index=1 ) editor.configure_traits(view=single_select_view) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/tests/editors/test_range_editor.py0000644000175100001730000004626200000000000024731 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! import platform import unittest from pyface.constant import OK from pyface.toolkit import toolkit_object from traits.api import HasTraits, Float, Int, Range, TraitError from traits.testing.api import UnittestTools from traitsui.api import Item, RangeEditor, UItem, View from traitsui.testing.api import ( DisplayedText, KeyClick, KeySequence, Slider, TargetRegistry, Textbox, UITester, ) from traitsui.tests._tools import ( BaseTestMixin, is_wx, requires_toolkit, ToolkitName, ) ModalDialogTester = toolkit_object( "util.modal_dialog_tester:ModalDialogTester" ) is_windows = platform.system() == "Windows" def _register_simple_spin(registry): """Register interactions for the given registry for a SimpleSpinEditor. If there are any conflicts, an error will occur. This is kept separate from the below register function because the SimpleSpinEditor is not yet implemented on wx. This function can be used with a local reigstry for tests. Parameters ---------- registry : TargetRegistry The registry being registered to. """ from traitsui.testing.tester._ui_tester_registry.qt import ( _registry_helper, ) from traitsui.qt.range_editor import SimpleSpinEditor _registry_helper.register_editable_textbox_handlers( registry=registry, target_class=SimpleSpinEditor, widget_getter=lambda wrapper: wrapper._target.control.lineEdit(), ) class RangeModel(HasTraits): value = Int(1) float_value = Float(0.1) class RangeExcludeLow(HasTraits): x = Range(low=0.0, high=1.0, value=0.1, exclude_low=True) class ModelWithRangeTrait(HasTraits): value = Range(low=0, high=None, value=1) @requires_toolkit([ToolkitName.qt, ToolkitName.wx]) class TestRangeEditor(BaseTestMixin, unittest.TestCase, UnittestTools): def setUp(self): BaseTestMixin.setUp(self) def tearDown(self): BaseTestMixin.tearDown(self) def check_range_enum_editor_format_func(self, style): # RangeEditor with enum mode doesn't support format_func obj = RangeModel() view = View( UItem( "value", editor=RangeEditor( low=1, high=3, format_func=lambda v: "{:02d}".format(v), mode="enum", ), style=style, ) ) tester = UITester() with tester.create_ui(obj, dict(view=view)) as ui: editor = ui.get_editors("value")[0] # No formatting - simple strings self.assertEqual(editor.names[:3], ["1", "2", "3"]) self.assertEqual(editor.mapping, {"1": 1, "2": 2, "3": 3}) self.assertEqual(editor.inverse_mapping, {1: "1", 2: "2", 3: "3"}) def test_simple_editor_format_func(self): self.check_range_enum_editor_format_func("simple") def test_custom_editor_format_func(self): self.check_range_enum_editor_format_func("custom") def check_slider_set_with_text_valid(self, mode): model = RangeModel() view = View( Item("value", editor=RangeEditor(low=1, high=12, mode=mode)) ) tester = UITester() with tester.create_ui(model, dict(view=view)) as ui: # sanity check self.assertEqual(model.value, 1) number_field = tester.find_by_name(ui, "value") text = number_field.locate(Textbox()) text.perform(KeyClick("0")) text.perform(KeyClick("Enter")) displayed = text.inspect(DisplayedText()) self.assertEqual(model.value, 10) self.assertEqual(displayed, str(model.value)) def test_simple_slider_editor_set_with_text_valid(self): return self.check_slider_set_with_text_valid(mode='slider') def test_large_range_slider_editor_set_with_text_valid(self): return self.check_slider_set_with_text_valid(mode='xslider') def test_log_range_slider_editor_set_with_text_valid(self): return self.check_slider_set_with_text_valid(mode='logslider') def test_range_text_editor_set_with_text_valid(self): model = RangeModel() view = View( Item("value", editor=RangeEditor(low=1, high=12, mode="text")) ) tester = UITester() with tester.create_ui(model, dict(view=view)) as ui: # sanity check self.assertEqual(model.value, 1) number_field_text = tester.find_by_name(ui, "value") if is_windows and is_wx(): # For RangeTextEditor on wx and windows, the textbox # automatically gets focus and the full content is selected. # Insertion point is moved to keep the test consistent number_field_text.perform(KeyClick("End")) number_field_text.perform(KeyClick("0")) number_field_text.perform(KeyClick("Enter")) displayed = number_field_text.inspect(DisplayedText()) self.assertEqual(model.value, 10) self.assertEqual(displayed, str(model.value)) def test_range_text_editor_set_with_text_valid_and_none_bound(self): model = ModelWithRangeTrait() tester = UITester() with tester.create_ui(model) as ui: # sanity check self.assertEqual(model.value, 1) number_field_text = tester.find_by_name(ui, "value") if is_windows and is_wx(): # For RangeTextEditor on wx and windows, the textbox # automatically gets focus and the full content is selected. # Insertion point is moved to keep the test consistent number_field_text.perform(KeyClick("End")) number_field_text.perform(KeyClick("0")) number_field_text.perform(KeyClick("Enter")) displayed = number_field_text.inspect(DisplayedText()) self.assertEqual(model.value, 10) self.assertEqual(displayed, str(model.value)) # the tester support code is not yet implemented for Wx SimpleSpinEditor @requires_toolkit([ToolkitName.qt]) def test_simple_spin_editor_set_with_text_valid(self): model = RangeModel() view = View( Item("value", editor=RangeEditor(low=1, high=12, mode="spinner")) ) LOCAL_REGISTRY = TargetRegistry() _register_simple_spin(LOCAL_REGISTRY) tester = UITester(registries=[LOCAL_REGISTRY]) with tester.create_ui(model, dict(view=view)) as ui: # sanity check self.assertEqual(model.value, 1) number_field = tester.find_by_name(ui, "value") # For whatever reason, "End" was not working here number_field.perform(KeyClick("Right")) number_field.perform(KeyClick("0")) displayed = number_field.inspect(DisplayedText()) self.assertEqual(model.value, 10) self.assertEqual(displayed, str(model.value)) # the tester support code is not yet implemented for Wx SimpleSpinEditor @requires_toolkit([ToolkitName.qt]) def test_simple_spin_editor_auto_set_false(self): model = RangeModel() view = View( Item( "value", editor=RangeEditor( low=1, high=12, mode="spinner", auto_set=False, ) ) ) LOCAL_REGISTRY = TargetRegistry() _register_simple_spin(LOCAL_REGISTRY) tester = UITester(registries=[LOCAL_REGISTRY]) with tester.create_ui(model, dict(view=view)) as ui: # sanity check self.assertEqual(model.value, 1) number_field = tester.find_by_name(ui, "value") # For whatever reason, "End" was not working here number_field.perform(KeyClick("Right")) with self.assertTraitDoesNotChange(model, "value"): number_field.perform(KeyClick("0")) displayed = number_field.inspect(DisplayedText()) self.assertEqual(displayed, "10") with self.assertTraitChanges(model, "value"): number_field.perform(KeyClick("Enter")) self.assertEqual(model.value, 10) def check_slider_set_with_text_after_empty(self, mode): model = RangeModel() view = View( Item("value", editor=RangeEditor(low=1, high=12, mode=mode)) ) tester = UITester() with tester.create_ui(model, dict(view=view)) as ui: number_field = tester.find_by_name(ui, "value") text = number_field.locate(Textbox()) # Delete all contents of textbox for _ in range(5): text.perform(KeyClick("Backspace")) text.perform(KeySequence("11")) text.perform(KeyClick("Enter")) displayed = text.inspect(DisplayedText()) self.assertEqual(model.value, 11) self.assertEqual(displayed, str(model.value)) def test_simple_slider_editor_set_with_text_after_empty(self): return self.check_slider_set_with_text_after_empty(mode='slider') def test_large_range_slider_editor_set_with_text_after_empty(self): return self.check_slider_set_with_text_after_empty(mode='xslider') def test_log_range_slider_editor_set_with_text_after_empty(self): return self.check_slider_set_with_text_after_empty(mode='logslider') # on wx the text style editor gives an error whenever the textbox # is empty, even if enter has not been pressed. @requires_toolkit([ToolkitName.qt]) def test_range_text_editor_set_with_text_after_empty(self): model = RangeModel() view = View( Item("value", editor=RangeEditor(low=1, high=12, mode="text")) ) tester = UITester() with tester.create_ui(model, dict(view=view)) as ui: number_field_text = tester.find_by_name(ui, "value") # Delete all contents of textbox for _ in range(5): number_field_text.perform(KeyClick("Backspace")) number_field_text.perform(KeySequence("11")) number_field_text.perform(KeyClick("Enter")) displayed = number_field_text.inspect(DisplayedText()) self.assertEqual(model.value, 11) self.assertEqual(displayed, str(model.value)) # the tester support code is not yet implemented for Wx SimpleSpinEditor @requires_toolkit([ToolkitName.qt]) def test_simple_spin_editor_set_with_text_after_empty(self): model = RangeModel() view = View( Item("value", editor=RangeEditor(low=1, high=12, mode="spinner")) ) LOCAL_REGISTRY = TargetRegistry() _register_simple_spin(LOCAL_REGISTRY) tester = UITester(registries=[LOCAL_REGISTRY]) with tester.create_ui(model, dict(view=view)) as ui: number_field_text = tester.find_by_name(ui, "value") number_field_text.perform(KeyClick("Right")) # Delete all contents of textbox for _ in range(5): number_field_text.perform(KeyClick("Backspace")) number_field_text.perform(KeySequence("11")) number_field_text.perform(KeyClick("Enter")) displayed = number_field_text.inspect(DisplayedText()) self.assertEqual(model.value, 11) self.assertEqual(displayed, str(model.value)) def check_modify_slider(self, mode): model = RangeModel(value=0) view = View( Item("value", editor=RangeEditor(low=0, high=10, mode=mode)) ) tester = UITester() with tester.create_ui(model, dict(view=view)) as ui: number_field = tester.find_by_name(ui, "value") slider = number_field.locate(Slider()) text = number_field.locate(Textbox()) # slider values are converted to a [0, 10000] scale. A single # step is a change of 100 on that scale and a page step is 1000. # Our range in [0, 10] so these correspond to changes of .1 and 1. # Note: when tested manually, the step size seen on OSX and Wx is # different. for _ in range(10): slider.perform(KeyClick("Right")) displayed = text.inspect(DisplayedText()) self.assertEqual(model.value, 1) self.assertEqual(displayed, str(model.value)) slider.perform(KeyClick("Page Up")) displayed = text.inspect(DisplayedText()) self.assertEqual(model.value, 2) self.assertEqual(displayed, str(model.value)) def test_modify_slider_simple_slider(self): return self.check_modify_slider('slider') def test_modify_slider_large_range_slider(self): return self.check_modify_slider('xslider') def test_modify_slider_log_range_slider(self): model = RangeModel() view = View( Item( "float_value", editor=RangeEditor(low=0.1, high=1000000000, mode='logslider'), ) ) tester = UITester() with tester.create_ui(model, dict(view=view)) as ui: number_field = tester.find_by_name(ui, "float_value") slider = number_field.locate(Slider()) text = number_field.locate(Textbox()) # 10 steps is equivalent to 1 page step # on this scale either of those is equivalent to increasing the # trait's value from 10^n to 10^(n+1) for _ in range(10): slider.perform(KeyClick("Right")) displayed = text.inspect(DisplayedText()) self.assertEqual(model.float_value, 1.0) self.assertEqual(displayed, str(model.float_value)) slider.perform(KeyClick("Page Up")) displayed = text.inspect(DisplayedText()) self.assertEqual(model.float_value, 10.0) self.assertEqual(displayed, str(model.float_value)) def test_format_func(self): def num_to_time(num): minutes = int(num / 60) if minutes < 10: minutes_str = '0' + str(minutes) else: minutes_str = str(minutes) seconds = num % 60 if seconds < 10: seconds_str = '0' + str(seconds) else: seconds_str = str(seconds) return minutes_str + ':' + seconds_str model = RangeModel() view = View( Item("float_value", editor=RangeEditor(format_func=num_to_time)) ) tester = UITester() with tester.create_ui(model, dict(view=view)) as ui: float_value_field = tester.find_by_name(ui, "float_value") float_value_text = float_value_field.locate(Textbox()) self.assertEqual( float_value_text.inspect(DisplayedText()), "00:00.1" ) def test_editor_factory_format(self): """ format trait on RangeEditor editor factory has been removed in favor of format_str. """ model = RangeModel() with self.assertRaises(TraitError): view = View( Item("float_value", editor=RangeEditor(format="%s ...")) ) def test_editor_factory_format_str(self): """ format trait on RangeEditor editor factory has been deprecated in favor of format_str. However, behavior should be unchanged. """ model = RangeModel() view = View( Item("float_value", editor=RangeEditor(format_str="%s ...")) ) tester = UITester() with tester.create_ui(model, dict(view=view)) as ui: float_value_field = tester.find_by_name(ui, "float_value") float_value_text = float_value_field.locate(Textbox()) self.assertEqual( float_value_text.inspect(DisplayedText()), "0.1 ..." ) def test_editor_format_str(self): """ The format trait on an Editor instance has been removed. """ model = RangeModel() view = View( Item("float_value", editor=RangeEditor(format_str="%s ...")) ) tester = UITester() with tester.create_ui(model, dict(view=view)) as ui: float_value_field = tester.find_by_name(ui, "float_value") with self.assertRaises(TraitError): float_value_field._target.format = "%s +++" # regression test for enthought/traitsui#737. Hangs with a popup # dialog on wx. (xref: enthought/traitsui#1901) @requires_toolkit([ToolkitName.qt]) def test_set_text_out_of_range(self): model = RangeModel() view = View( Item( 'float_value', editor=RangeEditor(mode='text', low=0.0, high=1) ), ) tester = UITester() with tester.create_ui(model, dict(view=view)) as ui: float_value_field = tester.find_by_name(ui, "float_value") for _ in range(3): float_value_field.perform(KeyClick("Backspace")) # set a value out of the range [0.0, 1] float_value_field.perform(KeySequence("2.0")) float_value_field.perform(KeyClick("Enter")) self.assertTrue(0.0 <= model.float_value <= 1) # regression test for enthought/traitsui#1550 @requires_toolkit([ToolkitName.qt]) def test_modify_out_of_range(self): obj = RangeExcludeLow() tester = UITester(auto_process_events=False) with tester.create_ui(obj) as ui: number_field = tester.find_by_name(ui, "x") text = number_field.locate(Textbox()) # should not fail def set_out_of_range(): text.perform(KeyClick("Backspace")) text.perform(KeyClick("0")) text.perform(KeyClick("Enter")) mdtester = ModalDialogTester(set_out_of_range) mdtester.open_and_run(lambda x: x.close(accept=True)) # regression test for enthought/traitsui#1550 @requires_toolkit([ToolkitName.qt]) def test_modify_out_of_range_with_slider(self): obj = RangeExcludeLow() tester = UITester(auto_process_events=False) with tester.create_ui(obj) as ui: number_field = tester.find_by_name(ui, "x") slider = number_field.locate(Slider()) # slider values are converted to a [0, 10000] scale. A single # step is a change of 100 on that scale and a page step is 1000. # Our range in [0, 10] so these correspond to changes of .1 and 1. # Note: when tested manually, the step size seen on OSX and Wx is # different. # should not fail def move_slider_out_of_range(): slider.perform(KeyClick("Page Down")) mdtester = ModalDialogTester(move_slider_out_of_range) mdtester.open_and_run(lambda x: x.click_button(OK)) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/tests/editors/test_range_editor_spinner.py0000644000175100001730000000751600000000000026466 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Test case for bug (wx, Mac OS X) Editing the text part of a spin control box and pressing the OK button without de-focusing raises an AttributeError:: Traceback (most recent call last): File "ETS/traitsui/traitsui/wx/range_editor.py", line 783, in update_object self.value = self.control.GetValue() AttributeError: 'NoneType' object has no attribute 'GetValue' """ import unittest from traits.has_traits import HasTraits from traits.trait_types import Int from traitsui.item import Item from traitsui.view import View from traitsui.editors.range_editor import RangeEditor from traitsui.tests._tools import ( BaseTestMixin, create_ui, requires_toolkit, reraise_exceptions, ToolkitName, ) class NumberWithSpinnerEditor(HasTraits): """Dialog containing a RangeEditor in 'spinner' mode for an Int.""" number = Int() traits_view = View( Item(label="Enter 4, then press OK without defocusing"), Item("number", editor=RangeEditor(low=3, high=8, mode="spinner")), buttons=["OK"], ) class TestRangeEditorSpinner(BaseTestMixin, unittest.TestCase): def setUp(self): BaseTestMixin.setUp(self) def tearDown(self): BaseTestMixin.tearDown(self) @requires_toolkit([ToolkitName.wx]) def test_wx_spin_control_editing_should_not_crash(self): # Bug: when editing the text part of a spin control box, pressing # the OK button raises an AttributeError on Mac OS X num = NumberWithSpinnerEditor() try: with reraise_exceptions(), create_ui(num) as ui: # the following is equivalent to clicking in the text control # of the range editor, enter a number, and clicking ok without # defocusing # SpinCtrl object spin = ui.control.FindWindowByName("wxSpinCtrl", ui.control) spin.SetFocusFromKbd() # on Windows, a wxSpinCtrl does not have children, and we # cannot do the more fine-grained testing below if len(spin.GetChildren()) == 0: spin.SetValue("4") else: # TextCtrl object of the spin control spintxt = spin.FindWindowByName("text", spin) spintxt.SetValue("4") except AttributeError: # if all went well, we should not be here self.fail("AttributeError raised") @requires_toolkit([ToolkitName.qt]) def test_qt_spin_control_editing(self): # Behavior: when editing the text part of a spin control box, pressing # the OK button updates the value of the HasTraits class from pyface import qt num = NumberWithSpinnerEditor() with reraise_exceptions(), create_ui(num) as ui: # the following is equivalent to clicking in the text control of # the range editor, enter a number, and clicking ok without # defocusing # text element inside the spin control lineedit = ui.control.findChild(qt.QtGui.QLineEdit) lineedit.setFocus() lineedit.setText("4") # if all went well, the number traits has been updated and its value is # 4 self.assertEqual(num.number, 4) if __name__ == "__main__": # Executing the file opens the dialog for manual testing num = NumberWithSpinnerEditor() num.configure_traits() print(num.number) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/tests/editors/test_range_editor_text.py0000644000175100001730000000510500000000000025764 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Test case for bug (wx, Mac OS X) A RangeEditor in mode 'text' for an Int allows values out of range. """ import unittest from traits.has_traits import HasTraits from traits.trait_types import Float, Int from traitsui.item import Item from traitsui.view import View from traitsui.editors.range_editor import RangeEditor from traitsui.testing.tester import command from traitsui.testing.tester.ui_tester import UITester from traitsui.tests._tools import ( BaseTestMixin, requires_toolkit, ToolkitName, ) class NumberWithRangeEditor(HasTraits): """Dialog containing a RangeEditor in 'spinner' mode for an Int.""" number = Int() traits_view = View( Item(label="Range should be 3 to 8. Enter 1, then press OK"), Item("number", editor=RangeEditor(low=3, high=8, mode="text")), buttons=["OK"], ) class FloatWithRangeEditor(HasTraits): """Dialog containing a RangeEditor in 'spinner' mode for an Int.""" number = Float(5.0) traits_view = View( Item("number", editor=RangeEditor(low=0.0, high=12.0)), buttons=["OK"] ) class TestRangeEditorText(BaseTestMixin, unittest.TestCase): def setUp(self): BaseTestMixin.setUp(self) def tearDown(self): BaseTestMixin.tearDown(self) @requires_toolkit([ToolkitName.wx]) def test_wx_text_editing(self): # behavior: when editing the text part of a spin control box, pressing # the OK button should update the value of the HasTraits class # (tests a bug where this fails with an AttributeError) num = NumberWithRangeEditor() tester = UITester() with tester.create_ui(num) as ui: # the following is equivalent to setting the text in the text # control, then pressing OK text = tester.find_by_name(ui, "number") text.perform(command.KeyClick("1")) text.perform(command.KeyClick("Enter")) # the number traits should be between 3 and 8 self.assertTrue(3 <= num.number <= 8) if __name__ == "__main__": # Executing the file opens the dialog for manual testing num = NumberWithRangeEditor() num.configure_traits() print(num.number) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/tests/editors/test_set_editor.py0000644000175100001730000004676700000000000024442 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! import contextlib import unittest from traits.api import HasTraits, List from traitsui.api import SetEditor, UItem, View from traitsui.tests._tools import ( BaseTestMixin, create_ui, click_button, is_control_enabled, is_qt, is_wx, process_cascade_events, requires_toolkit, reraise_exceptions, ToolkitName, ) class ListModel(HasTraits): value = List(["one", "two"]) def get_view(can_move_all=True, ordered=False): return View( UItem( "value", editor=SetEditor( values=["one", "two", "three", "four"], ordered=ordered, can_move_all=can_move_all, ), style="simple", ) ) def get_list_items(list_widget): """Return a list of strings from the list widget.""" items = [] if is_wx(): for i in range(list_widget.GetCount()): items.append(list_widget.GetString(i)) elif is_qt(): for i in range(list_widget.count()): items.append(list_widget.item(i).text()) else: raise unittest.SkipTest("Test not implemented for this toolkit") return items def click_on_item(editor, item_idx, in_used=False): """Simulate a click on an item in a specified list. The function deselects all items in both used and unused lists, then selects an item at index item_idx either in the used list (if in_used=True) or in the unused list. Finally the function simulates a click on the selected item. """ unused_list = editor._unused used_list = editor._used if is_wx(): import wx # First deselect all items for i in range(unused_list.GetCount()): unused_list.Deselect(i) for i in range(used_list.GetCount()): used_list.Deselect(i) # Select the item in the correct list list_with_selection = used_list if in_used else unused_list list_with_selection.SetSelection(item_idx) event = wx.CommandEvent( wx.EVT_LISTBOX.typeId, list_with_selection.GetId() ) wx.PostEvent(editor.control, event) elif is_qt(): for i in range(unused_list.count()): status = (not in_used) and (item_idx == i) unused_list.item(i).setSelected(status) for i in range(used_list.count()): status = (in_used) and (item_idx == i) used_list.item(i).setSelected(status) if in_used: used_list.itemClicked.emit(used_list.item(item_idx)) else: unused_list.itemClicked.emit(unused_list.item(item_idx)) else: raise unittest.SkipTest("Test not implemented for this toolkit") def double_click_on_item(editor, item_idx, in_used=False): """Simulate a double click on an item in a specified list. The function deselects all items in both used and unused lists, then selects an item at index item_idx either in the used list (if in_used=True) or in the unused list. Finally the function simulates a double click on the selected item. """ unused_list = editor._unused used_list = editor._used if is_wx(): import wx # First deselect all items for i in range(unused_list.GetCount()): unused_list.Deselect(i) for i in range(used_list.GetCount()): used_list.Deselect(i) # Select the item in the correct list list_with_selection = used_list if in_used else unused_list list_with_selection.SetSelection(item_idx) event = wx.CommandEvent( wx.EVT_LISTBOX_DCLICK.typeId, list_with_selection.GetId() ) wx.PostEvent(editor.control, event) elif is_qt(): for i in range(unused_list.count()): status = (not in_used) and (item_idx == i) unused_list.item(i).setSelected(status) for i in range(used_list.count()): status = (in_used) and (item_idx == i) used_list.item(i).setSelected(status) if in_used: used_list.itemDoubleClicked.emit(used_list.item(item_idx)) else: unused_list.itemDoubleClicked.emit(unused_list.item(item_idx)) else: raise unittest.SkipTest("Test not implemented for this toolkit") @requires_toolkit([ToolkitName.qt, ToolkitName.wx]) class TestSetEditorMapping(BaseTestMixin, unittest.TestCase): def setUp(self): BaseTestMixin.setUp(self) def tearDown(self): BaseTestMixin.tearDown(self) @contextlib.contextmanager def setup_ui(self, model, view): with create_ui(model, dict(view=view)) as ui: yield ui.get_editors("value")[0] def test_simple_editor_mapping_values(self): class IntListModel(HasTraits): value = List() set_editor_factory = SetEditor( values=[0, 1], format_func=lambda v: str(bool(v)).upper() ) formatted_view = View( UItem( "value", editor=set_editor_factory, style="simple", ) ) with reraise_exceptions(), self.setup_ui( IntListModel(), formatted_view ) as editor: self.assertEqual(editor.names, ["FALSE", "TRUE"]) self.assertEqual(editor.mapping, {"FALSE": 0, "TRUE": 1}) self.assertEqual(editor.inverse_mapping, {0: "FALSE", 1: "TRUE"}) set_editor_factory.values = [1, 0] self.assertEqual(editor.names, ["TRUE", "FALSE"]) self.assertEqual(editor.mapping, {"TRUE": 1, "FALSE": 0}) self.assertEqual(editor.inverse_mapping, {1: "TRUE", 0: "FALSE"}) def test_simple_editor_mapping_name(self): class IntListModel(HasTraits): value = List() possible_values = List([0, 1]) formatted_view = View( UItem( 'value', editor=SetEditor( name="object.possible_values", format_func=lambda v: str(bool(v)).upper(), ), style="simple", ) ) model = IntListModel() with reraise_exceptions(), self.setup_ui( model, formatted_view ) as editor: self.assertEqual(editor.names, ["FALSE", "TRUE"]) self.assertEqual(editor.mapping, {"FALSE": 0, "TRUE": 1}) self.assertEqual(editor.inverse_mapping, {0: "FALSE", 1: "TRUE"}) model.possible_values = [1, 0] self.assertEqual(editor.names, ["TRUE", "FALSE"]) self.assertEqual(editor.mapping, {"TRUE": 1, "FALSE": 0}) self.assertEqual(editor.inverse_mapping, {1: "TRUE", 0: "FALSE"}) @requires_toolkit([ToolkitName.qt, ToolkitName.wx]) class TestSimpleSetEditor(BaseTestMixin, unittest.TestCase): def setUp(self): BaseTestMixin.setUp(self) def tearDown(self): BaseTestMixin.tearDown(self) @contextlib.contextmanager def setup_gui(self, model, view): with create_ui(model, dict(view=view)) as ui: yield ui.get_editors("value")[0] def test_simple_set_editor_use_button(self): # Initiate with non-alphabetical list model = ListModel(value=["two", "one"]) with reraise_exceptions(), self.setup_gui(model, get_view()) as editor: self.assertEqual(get_list_items(editor._unused), ["four", "three"]) # Used list is sorted alphabetically self.assertEqual(get_list_items(editor._used), ["one", "two"]) click_on_item(editor, 1, in_used=False) process_cascade_events() self.assertTrue(is_control_enabled(editor._use)) self.assertFalse(is_control_enabled(editor._unuse)) click_button(editor._use) process_cascade_events() self.assertEqual(get_list_items(editor._unused), ["four"]) # Button inserts at the top self.assertEqual( get_list_items(editor._used), ["three", "one", "two"] ) self.assertEqual(editor._get_selected_strings(editor._used), []) def test_simple_set_editor_unuse_button(self): with reraise_exceptions(), self.setup_gui( ListModel(), get_view() ) as editor: self.assertEqual(get_list_items(editor._unused), ["four", "three"]) self.assertEqual(get_list_items(editor._used), ["one", "two"]) click_on_item(editor, 0, in_used=True) process_cascade_events() self.assertFalse(is_control_enabled(editor._use)) self.assertTrue(is_control_enabled(editor._unuse)) click_button(editor._unuse) process_cascade_events() # Button inserts at the top self.assertEqual( get_list_items(editor._unused), ["one", "four", "three"] ) self.assertEqual(get_list_items(editor._used), ["two"]) def test_simple_set_editor_use_dclick(self): with reraise_exceptions(), self.setup_gui( ListModel(), get_view() ) as editor: self.assertEqual(get_list_items(editor._unused), ["four", "three"]) self.assertEqual(get_list_items(editor._used), ["one", "two"]) double_click_on_item(editor, 1, in_used=False) process_cascade_events() self.assertEqual(get_list_items(editor._unused), ["four"]) # Inserts at the top self.assertEqual( get_list_items(editor._used), ["three", "one", "two"] ) self.assertEqual(editor._get_selected_strings(editor._used), []) def test_simple_set_editor_unuse_dclick(self): with reraise_exceptions(), self.setup_gui( ListModel(), get_view() ) as editor: self.assertEqual(get_list_items(editor._unused), ["four", "three"]) self.assertEqual(get_list_items(editor._used), ["one", "two"]) double_click_on_item(editor, 0, in_used=True) process_cascade_events() # Inserts at the top self.assertEqual( get_list_items(editor._unused), ["one", "four", "three"] ) self.assertEqual(get_list_items(editor._used), ["two"]) def test_simple_set_editor_use_all(self): with reraise_exceptions(), self.setup_gui( ListModel(), get_view() ) as editor: self.assertEqual(get_list_items(editor._unused), ["four", "three"]) self.assertEqual(get_list_items(editor._used), ["one", "two"]) click_on_item(editor, 1, in_used=False) process_cascade_events() self.assertTrue(is_control_enabled(editor._use_all)) self.assertFalse(is_control_enabled(editor._unuse_all)) click_button(editor._use_all) process_cascade_events() self.assertEqual(get_list_items(editor._unused), []) # Button inserts at the end self.assertEqual( get_list_items(editor._used), ["one", "two", "four", "three"] ) def test_simple_set_editor_unuse_all(self): with reraise_exceptions(), self.setup_gui( ListModel(), get_view() ) as editor: self.assertEqual(get_list_items(editor._unused), ["four", "three"]) self.assertEqual(get_list_items(editor._used), ["one", "two"]) click_on_item(editor, 0, in_used=True) process_cascade_events() self.assertFalse(is_control_enabled(editor._use_all)) self.assertTrue(is_control_enabled(editor._unuse_all)) click_button(editor._unuse_all) process_cascade_events() # Button inserts at the end self.assertEqual( get_list_items(editor._unused), ["four", "three", "one", "two"] ) self.assertEqual(get_list_items(editor._used), []) def test_simple_set_editor_move_up(self): with reraise_exceptions(), self.setup_gui( ListModel(), get_view(ordered=True) ) as editor: self.assertEqual(get_list_items(editor._unused), ["four", "three"]) self.assertEqual(get_list_items(editor._used), ["one", "two"]) click_on_item(editor, 1, in_used=True) process_cascade_events() self.assertTrue(is_control_enabled(editor._up)) self.assertFalse(is_control_enabled(editor._down)) click_button(editor._up) process_cascade_events() self.assertEqual(get_list_items(editor._unused), ["four", "three"]) self.assertEqual(get_list_items(editor._used), ["two", "one"]) def test_simple_set_editor_move_down(self): with reraise_exceptions(), self.setup_gui( ListModel(), get_view(ordered=True) ) as editor: self.assertEqual(get_list_items(editor._unused), ["four", "three"]) self.assertEqual(get_list_items(editor._used), ["one", "two"]) click_on_item(editor, 0, in_used=True) process_cascade_events() self.assertFalse(is_control_enabled(editor._up)) self.assertTrue(is_control_enabled(editor._down)) click_button(editor._down) process_cascade_events() self.assertEqual(get_list_items(editor._unused), ["four", "three"]) self.assertEqual(get_list_items(editor._used), ["two", "one"]) def test_simple_set_editor_use_all_button(self): with reraise_exceptions(), self.setup_gui( ListModel(), get_view() ) as editor: self.assertEqual(get_list_items(editor._unused), ["four", "three"]) self.assertEqual(get_list_items(editor._used), ["one", "two"]) click_on_item(editor, 1, in_used=False) process_cascade_events() self.assertTrue(is_control_enabled(editor._use_all)) self.assertFalse(is_control_enabled(editor._unuse_all)) click_button(editor._use_all) process_cascade_events() self.assertEqual(get_list_items(editor._unused), []) # Button inserts at the end self.assertEqual( get_list_items(editor._used), ["one", "two", "four", "three"] ) def test_simple_set_editor_unuse_all_button(self): with reraise_exceptions(), self.setup_gui( ListModel(), get_view() ) as editor: self.assertEqual(get_list_items(editor._unused), ["four", "three"]) self.assertEqual(get_list_items(editor._used), ["one", "two"]) click_on_item(editor, 0, in_used=True) process_cascade_events() self.assertFalse(is_control_enabled(editor._use_all)) self.assertTrue(is_control_enabled(editor._unuse_all)) click_button(editor._unuse_all) process_cascade_events() # Button inserts at the end self.assertEqual( get_list_items(editor._unused), ["four", "three", "one", "two"] ) self.assertEqual(get_list_items(editor._used), []) def test_simple_set_editor_default_selection_unused(self): with reraise_exceptions(), self.setup_gui( ListModel(), get_view() ) as editor: self.assertEqual(get_list_items(editor._unused), ["four", "three"]) self.assertEqual(get_list_items(editor._used), ["one", "two"]) click_button(editor._use) process_cascade_events() # Button inserts at the top self.assertEqual(get_list_items(editor._unused), ["three"]) self.assertEqual( get_list_items(editor._used), ["four", "one", "two"] ) def test_simple_set_editor_default_selection_used(self): # When all items are used, top used item is selected by default list_edit = ListModel(value=["one", "two", "three", "four"]) with reraise_exceptions(), self.setup_gui( list_edit, get_view() ) as editor: self.assertEqual(get_list_items(editor._unused), []) self.assertEqual( get_list_items(editor._used), ["four", "one", "three", "two"] ) click_button(editor._unuse) process_cascade_events() # Button inserts at the top self.assertEqual(get_list_items(editor._unused), ["four"]) self.assertEqual( get_list_items(editor._used), ["one", "three", "two"] ) def test_simple_set_editor_deleted_valid_values(self): editor_factory = SetEditor(values=["one", "two", "three", "four"]) view = View( UItem( "value", editor=editor_factory, style="simple", ) ) list_edit = ListModel() with reraise_exceptions(), self.setup_gui(list_edit, view) as editor: self.assertEqual(get_list_items(editor._unused), ["four", "three"]) self.assertEqual(get_list_items(editor._used), ["one", "two"]) editor_factory.values = ["two", "three", "four"] process_cascade_events() self.assertEqual(get_list_items(editor._unused), ["four", "three"]) # FIXME issue enthought/traitsui#840 if is_wx(): with self.assertRaises(AssertionError): self.assertEqual(get_list_items(editor._used), ["two"]) self.assertEqual(get_list_items(editor._used), ["one", "two"]) else: self.assertEqual(get_list_items(editor._used), ["two"]) self.assertEqual(list_edit.value, ["two"]) def test_simple_set_editor_use_ordered_selected(self): # Initiate with non-alphabetical list model = ListModel(value=["two", "one"]) with reraise_exceptions(), self.setup_gui( model, get_view(ordered=True) ) as editor: self.assertEqual(get_list_items(editor._unused), ["four", "three"]) # Used list maintains the order self.assertEqual(get_list_items(editor._used), ["two", "one"]) click_on_item(editor, 1, in_used=False) process_cascade_events() self.assertTrue(is_control_enabled(editor._use)) self.assertFalse(is_control_enabled(editor._unuse)) click_button(editor._use) process_cascade_events() self.assertEqual(get_list_items(editor._unused), ["four"]) # Button inserts at the top self.assertEqual( get_list_items(editor._used), ["three", "two", "one"] ) self.assertEqual( editor._get_selected_strings(editor._used), ["three"] ) def test_simple_set_editor_unordeder_button_existence(self): with reraise_exceptions(), self.setup_gui( ListModel(), get_view() ) as editor: self.assertIsNone(editor._up) self.assertIsNone(editor._down) def test_simple_set_editor_cant_move_all_button_existence(self): view = get_view(can_move_all=False) with reraise_exceptions(), self.setup_gui(ListModel(), view) as editor: self.assertIsNone(editor._use_all) self.assertIsNone(editor._unuse_all) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/tests/editors/test_shell_editor.py0000644000175100001730000000360000000000000024731 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! import unittest from traits.api import Any, Dict, HasTraits, Str from traitsui.api import Item, ShellEditor, View from traitsui.testing.api import UITester from traitsui.tests._tools import ( BaseTestMixin, requires_toolkit, ToolkitName, ) class ShellTest(HasTraits): locals_str = Str() locals_dict = Dict(Str, Any) def get_str_view(share=False): return View( Item( "locals_str", editor=ShellEditor(share=share), ) ) def get_dict_view(share=False): return View( Item( "locals_dict", editor=ShellEditor(share=share), ) ) @requires_toolkit([ToolkitName.qt, ToolkitName.wx]) class TestShellEditor(BaseTestMixin, unittest.TestCase): def setUp(self): BaseTestMixin.setUp(self) def tearDown(self): BaseTestMixin.tearDown(self) def smoke_test(self, locals_type, share): shell_test = ShellTest() tester = UITester() if locals_type == "str": with tester.create_ui(shell_test, dict(view=get_str_view(share))): pass else: with tester.create_ui(shell_test, dict(view=get_dict_view(share))): pass def test_no_share_dict(self): self.smoke_test("dict", False) def test_share_dict(self): self.smoke_test("dict", True) def test_no_share_str(self): self.smoke_test("str", False) def test_share_str(self): self.smoke_test("str", True) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/tests/editors/test_styled_date_editor.py0000644000175100001730000000400600000000000026124 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! import datetime import unittest from traits.api import Dict, HasTraits, Instance, List, Str from traitsui.api import Item, StyledDateEditor, View from traitsui.editors.date_editor import CellFormat from traitsui.tests._tools import ( BaseTestMixin, create_ui, requires_toolkit, reraise_exceptions, ToolkitName, ) class DateModel(HasTraits): selected_date = Instance(datetime.date) special_days = Dict(Str, List(Instance(datetime.date))) styles_mapping = Dict(Str, Instance(CellFormat)) def get_example_model(): return DateModel( special_days={ "public-holidays": [datetime.date(2020, 1, 1)], "weekends": [datetime.date(2020, 1, 12)], }, styles_mapping={ "public-holidays": CellFormat(bgcolor=(255, 0, 0)), "weekends": CellFormat(bold=True), }, ) # StyledDateEditor is currently only implemented for Qt @requires_toolkit([ToolkitName.qt]) class TestStyledDateEditor(BaseTestMixin, unittest.TestCase): def setUp(self): BaseTestMixin.setUp(self) def tearDown(self): BaseTestMixin.tearDown(self) def test_init_and_dispose(self): # Smoke test to test init and dispose. instance = get_example_model() view = View( Item( "selected_date", editor=StyledDateEditor( dates_trait="special_days", styles_trait="styles_mapping", ), style="custom", ) ) with reraise_exceptions(), create_ui(instance, dict(view=view)): pass ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/tests/editors/test_table_editor.py0000644000175100001730000004742500000000000024726 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! import unittest from unittest.mock import Mock from traits.api import HasTraits, Instance, Int, List, Str, Tuple from traitsui.api import ( Action, EvalTableFilter, Item, ObjectColumn, TableEditor, View, ) from traitsui.tests._tools import ( BaseTestMixin, requires_toolkit, ToolkitName, ) from traitsui.testing.api import ( Cell, Disabled, DisplayedText, KeySequence, KeyClick, MouseClick, MouseDClick, Selected, SelectedIndices, UITester, ) class ListItem(HasTraits): """Items to visualize in a table editor""" value = Str() other_value = Int() class ObjectListWithSelection(HasTraits): values = List(Instance(ListItem)) selected = Instance(ListItem) selections = List(Instance(ListItem)) selected_index = Int() selected_indices = List(Int) selected_column = Str() selected_columns = List(Str) selected_cell = Tuple(Instance(ListItem), Str) selected_cells = List(Tuple(Instance(ListItem), Str)) selected_cell_index = Tuple(Int, Int) selected_cell_indices = List(Tuple(Int, Int)) class ObjectList(HasTraits): values = List(Instance(ListItem)) simple_view = View( Item( "values", show_label=False, editor=TableEditor( columns=[ ObjectColumn(name="value"), ObjectColumn(name="other_value"), ] ), ), buttons=["OK"], ) filtered_view = View( Item( "values", show_label=False, editor=TableEditor( sortable=False, columns=[ ObjectColumn(name="value"), ObjectColumn(name="other_value"), ], filter=EvalTableFilter(expression="other_value > 4"), ), ), buttons=["OK"], ) select_row_view = View( Item( "values", show_label=False, editor=TableEditor( sortable=False, columns=[ ObjectColumn(name="value"), ObjectColumn(name="other_value"), ], selection_mode="row", selected="selected", ), ), buttons=["OK"], ) select_rows_view = View( Item( "values", show_label=False, editor=TableEditor( sortable=False, columns=[ObjectColumn(name="value")], selection_mode="rows", selected="selections", ), ), buttons=["OK"], ) select_row_index_view = View( Item( "values", show_label=False, editor=TableEditor( sortable=False, columns=[ ObjectColumn(name="value"), ObjectColumn(name="other_value"), ], selection_mode="row", selected_indices="selected_index", ), ), buttons=["OK"], ) select_row_indices_view = View( Item( "values", show_label=False, editor=TableEditor( sortable=False, columns=[ ObjectColumn(name="value"), ObjectColumn(name="other_value"), ], selection_mode="rows", selected_indices="selected_indices", ), ), buttons=["OK"], ) select_column_view = View( Item( "values", show_label=False, editor=TableEditor( sortable=False, columns=[ ObjectColumn(name="value"), ObjectColumn(name="other_value"), ], selection_mode="column", selected="selected_column", ), ), buttons=["OK"], ) select_columns_view = View( Item( "values", show_label=False, editor=TableEditor( sortable=False, columns=[ ObjectColumn(name="value"), ObjectColumn(name="other_value"), ], selection_mode="columns", selected="selected_columns", ), ), buttons=["OK"], ) select_column_index_view = View( Item( "values", show_label=False, editor=TableEditor( sortable=False, columns=[ ObjectColumn(name="value"), ObjectColumn(name="other_value"), ], selection_mode="column", selected_indices="selected_index", ), ), buttons=["OK"], ) select_column_indices_view = View( Item( "values", show_label=False, editor=TableEditor( sortable=False, columns=[ ObjectColumn(name="value"), ObjectColumn(name="other_value"), ], selection_mode="columns", selected_indices="selected_indices", ), ), buttons=["OK"], ) select_cell_view = View( Item( "values", show_label=False, editor=TableEditor( sortable=False, columns=[ ObjectColumn(name="value"), ObjectColumn(name="other_value"), ], selection_mode="cell", selected="selected_cell", ), ), buttons=["OK"], ) select_cells_view = View( Item( "values", show_label=False, editor=TableEditor( sortable=False, columns=[ ObjectColumn(name="value"), ObjectColumn(name="other_value"), ], selection_mode="cells", selected="selected_cells", ), ), buttons=["OK"], ) select_cell_index_view = View( Item( "values", show_label=False, editor=TableEditor( sortable=False, columns=[ ObjectColumn(name="value"), ObjectColumn(name="other_value"), ], selection_mode="cell", selected_indices="selected_cell_index", ), ), buttons=["OK"], ) select_cell_indices_view = View( Item( "values", show_label=False, editor=TableEditor( sortable=False, columns=[ ObjectColumn(name="value"), ObjectColumn(name="other_value"), ], selection_mode="cells", selected_indices="selected_cell_indices", ), ), buttons=["OK"], ) edit_on_first_click_false_view = View( Item( "values", show_label=False, editor=TableEditor( sortable=False, columns=[ ObjectColumn(name="value"), ObjectColumn(name="other_value"), ], selection_mode="row", selected="selected", edit_on_first_click=False ), ), buttons=["OK"], ) @requires_toolkit([ToolkitName.qt]) class TestTableEditor(BaseTestMixin, unittest.TestCase): def setUp(self): BaseTestMixin.setUp(self) def tearDown(self): BaseTestMixin.tearDown(self) def test_table_editor(self): object_list = ObjectListWithSelection( values=[ListItem(value=str(i ** 2)) for i in range(10)] ) tester = UITester() with tester.create_ui(object_list, dict(view=simple_view)): pass def test_filtered_table_editor(self): object_list = ObjectListWithSelection( values=[ListItem(other_value=i ** 2) for i in range(10)] ) tester = UITester() with tester.create_ui(object_list, dict(view=filtered_view)) as ui: values = tester.find_by_name(ui, "values") filter = values._target.filter num_filtered_indices = len(values._target.filtered_indices) self.assertIsNotNone(filter) self.assertEqual(num_filtered_indices, 7) def test_table_editor_select_row(self): object_list = ObjectListWithSelection( values=[ListItem(value=str(i ** 2)) for i in range(10)] ) tester = UITester() with tester.create_ui(object_list, dict(view=select_row_view)) as ui: # click the first cell in the 6th row to select the row values_table = tester.find_by_name(ui, "values") row6_cell = values_table.locate(Cell(5, 0)) row6_cell.perform(MouseClick()) selected = values_table.inspect(Selected()) self.assertEqual(selected, [object_list.values[5]]) self.assertIs(selected[0], object_list.values[5]) self.assertIs(object_list.selected, selected[0]) def test_table_editor_select_rows(self): object_list = ObjectListWithSelection( values=[ListItem(value=str(i ** 2)) for i in range(10)] ) object_list.selections = object_list.values[5:7] tester = UITester() with tester.create_ui(object_list, dict(view=select_rows_view)) as ui: values_table = tester.find_by_name(ui, "values") selected = values_table.inspect(Selected()) self.assertEqual(selected, object_list.values[5:7]) def test_table_editor_select_row_index(self): object_list = ObjectListWithSelection( values=[ListItem(value=str(i ** 2)) for i in range(10)] ) object_list.selected_index = 5 tester = UITester() with tester.create_ui(object_list, dict(view=select_row_index_view)) \ as ui: values_table = tester.find_by_name(ui, "values") values_table.locate(Cell(5, 0)).perform(MouseClick()) selected = values_table.inspect(SelectedIndices()) self.assertEqual(selected, [5]) def test_table_editor_select_row_indices(self): object_list = ObjectListWithSelection( values=[ListItem(value=str(i ** 2)) for i in range(10)] ) object_list.selected_indices = [5, 7, 8] view = select_row_indices_view tester = UITester() with tester.create_ui(object_list, dict(view=view)) as ui: values_table = tester.find_by_name(ui, "values") selected = values_table.inspect(SelectedIndices()) self.assertEqual(selected, [5, 7, 8]) def test_table_editor_select_column(self): object_list = ObjectListWithSelection( values=[ListItem(value=str(i ** 2)) for i in range(10)] ) tester = UITester() with tester.create_ui(object_list, dict(view=select_column_view)) \ as ui: values_table = tester.find_by_name(ui, "values") # click a cell in the first column (the "value" column) first_cell = values_table.locate(Cell(0, 0)) first_cell.perform(MouseClick()) selected = values_table.inspect(Selected()) self.assertEqual(selected, ["value"]) self.assertIs(selected[0], "value") self.assertIs(selected[0], object_list.selected_column) def test_table_editor_select_columns(self): object_list = ObjectListWithSelection( values=[ListItem(value=str(i ** 2)) for i in range(10)] ) object_list.selected_columns = ["value", "other_value"] tester = UITester() with tester.create_ui(object_list, dict(view=select_columns_view)) \ as ui: values_table = tester.find_by_name(ui, "values") selected = values_table.inspect(Selected()) self.assertEqual(selected, ["value", "other_value"]) def test_table_editor_select_column_index(self): object_list = ObjectListWithSelection( values=[ListItem(value=str(i ** 2)) for i in range(10)] ) view = select_column_index_view tester = UITester() with tester.create_ui(object_list, dict(view=view)) as ui: # click a cell in the index 1 column values_table = tester.find_by_name(ui, "values") col1_cell = values_table.locate(Cell(0, 1)) col1_cell.perform(MouseClick()) selected = values_table.inspect(SelectedIndices()) self.assertEqual(selected, [1]) def test_table_editor_select_column_indices(self): object_list = ObjectListWithSelection( values=[ListItem(value=str(i ** 2)) for i in range(10)] ) object_list.selected_indices = [0, 1] view = select_column_indices_view tester = UITester() with tester.create_ui(object_list, dict(view=view)) as ui: values_table = tester.find_by_name(ui, "values") selected = values_table.inspect(SelectedIndices()) self.assertEqual(selected, [0, 1]) def test_table_editor_select_cell(self): object_list = ObjectListWithSelection( values=[ListItem(value=str(i ** 2)) for i in range(10)] ) tester = UITester() with tester.create_ui(object_list, dict(view=select_cell_view)) as ui: # click the cell at (5,0) values_table = tester.find_by_name(ui, "values") cell_5_0 = values_table.locate(Cell(5, 0)) cell_5_0.perform(MouseClick()) selected = values_table.inspect(Selected()) self.assertEqual(selected, [(object_list.values[5], "value")]) self.assertIs(selected[0], object_list.selected_cell) def test_table_editor_modify_cell_with_tester(self): object_list = ObjectListWithSelection( values=[ListItem(value=str(i ** 2)) for i in range(10)] ) tester = UITester() with tester.create_ui(object_list, dict(view=select_row_view)) as ui: wrapper = tester.find_by_name(ui, "values").locate(Cell(5, 0)) wrapper.perform(MouseClick()) # activate edit mode wrapper.perform(KeySequence("abc")) self.assertEqual(object_list.selected.value, "abc") # second column refers to an Int type original = object_list.selected.other_value wrapper = tester.find_by_name(ui, "values").locate(Cell(5, 1)) wrapper.perform(MouseClick()) wrapper.perform(KeySequence("abc")) # invalid self.assertEqual(object_list.selected.other_value, original) for _ in range(3): wrapper.perform(KeyClick("Backspace")) wrapper.perform(KeySequence("12")) # now ok self.assertEqual(object_list.selected.other_value, 12) def test_table_editor_check_display_with_tester(self): object_list = ObjectListWithSelection( values=[ListItem(other_value=0)] ) tester = UITester() with tester.create_ui(object_list, dict(view=select_row_view)) as ui: wrapper = tester.find_by_name(ui, "values").locate(Cell(0, 1)) actual = wrapper.inspect(DisplayedText()) self.assertEqual(actual, "0") object_list.values[0].other_value = 123 actual = wrapper.inspect(DisplayedText()) self.assertEqual(actual, "123") def test_table_editor_escape_retain_edit(self): object_list = ObjectListWithSelection( values=[ListItem(other_value=0)] ) tester = UITester() with tester.create_ui(object_list, dict(view=select_row_view)) as ui: cell = tester.find_by_name(ui, "values").locate(Cell(0, 1)) cell.perform(MouseClick()) cell.perform(KeySequence("123")) cell.perform(KeyClick("Esc")) # exit edit mode, did not revert self.assertEqual(object_list.values[0].other_value, 123) def test_table_editor_select_cells(self): object_list = ObjectListWithSelection( values=[ListItem(value=str(i ** 2)) for i in range(10)] ) object_list.selected_cells = [ (object_list.values[5], "value"), (object_list.values[6], "other value"), (object_list.values[8], "value"), ] tester = UITester() with tester.create_ui(object_list, dict(view=select_cells_view)) as ui: values_table = tester.find_by_name(ui, "values") selected = values_table.inspect(Selected()) self.assertEqual( selected, [ (object_list.values[5], "value"), (object_list.values[6], "other value"), (object_list.values[8], "value"), ], ) def test_table_editor_select_cell_index(self): object_list = ObjectListWithSelection( values=[ListItem(value=str(i ** 2)) for i in range(10)] ) view = select_cell_index_view tester = UITester() with tester.create_ui(object_list, dict(view=view)) as ui: # click the cell at (5,1) values_table = tester.find_by_name(ui, "values") cell_5_1 = values_table.locate(Cell(5, 1)) cell_5_1.perform(MouseClick()) selected = values_table.inspect(SelectedIndices()) self.assertEqual(selected, [(5, 1)]) def test_table_editor_select_cell_indices(self): object_list = ObjectListWithSelection( values=[ListItem(value=str(i ** 2)) for i in range(10)] ) object_list.selected_cell_indices = [(5, 0), (6, 1), (8, 0)] view = select_cell_indices_view tester = UITester() with tester.create_ui(object_list, dict(view=view)) as ui: values_table = tester.find_by_name(ui, "values") selected = values_table.inspect(SelectedIndices()) self.assertEqual(selected, [(5, 0), (6, 1), (8, 0)]) def test_progress_column(self): from traitsui.extras.progress_column import ProgressColumn progress_view = View( Item( "values", show_label=False, editor=TableEditor( columns=[ ObjectColumn(name="value"), ProgressColumn(name="other_value"), ] ), ), buttons=["OK"], ) object_list = ObjectList( values=[ListItem(value=str(i ** 2)) for i in range(10)] ) tester = UITester() with tester.create_ui(object_list, dict(view=progress_view)): pass def test_on_perform_action(self): # A test for issue #741, where actions with an on_perform function set # would get called twice object_list = ObjectList( values=[ListItem(value=str(i ** 2)) for i in range(10)] ) mock_function = Mock() action = Action(on_perform=mock_function) tester = UITester() with tester.create_ui(object_list, dict(view=simple_view)) as ui: editor = tester.find_by_name(ui, "values")._target editor.set_menu_context(None, None, None) editor.perform(action) mock_function.assert_called_once() def test_edit_on_first_click_false(self): object_list = ObjectListWithSelection( values=[ListItem(value=str(i ** 2)) for i in range(10)] ) tester = UITester() with tester.create_ui( object_list, dict(view=edit_on_first_click_false_view) ) as ui: wrapper = tester.find_by_name(ui, "values").locate(Cell(5, 0)) # single click will not activate edit mode wrapper.perform(MouseClick()) with self.assertRaises(Disabled): wrapper.perform(KeySequence("abc")) # double click will activate edit mode wrapper.perform(MouseDClick()) wrapper.perform(KeySequence("abc")) self.assertEqual(object_list.values[5].value, "abc") ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/tests/editors/test_tabular_editor.py0000644000175100001730000003442000000000000025260 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! import contextlib import unittest from traits.api import Event, HasTraits, Instance, Int, List, Str from traits.testing.api import UnittestTools from traitsui.api import Item, TabularEditor, View from traitsui.tabular_adapter import TabularAdapter from traitsui.tests._tools import ( BaseTestMixin, create_ui, is_wx, is_qt, process_cascade_events, requires_toolkit, reraise_exceptions, ToolkitName, ) class Person(HasTraits): name = Str() age = Int() def __repr__(self): return "Person(name={self.name!r}, age={self.age!r})".format(self=self) class ReportAdapter(TabularAdapter): columns = [("Name", "name"), ("Age", "age")] class Report(HasTraits): people = List(Person) selected = Instance(Person) selected_row = Int(-1) multi_selected = List(Instance(Person)) selected_rows = List(Int()) # Event for triggering a UI repaint. refresh = Event() # Event for triggering a UI table update. update = Event() def get_view(multi_select=False): if multi_select: return View( Item( name="people", editor=TabularEditor( adapter=ReportAdapter(), selected="multi_selected", selected_row="selected_rows", refresh="refresh", update="update", multi_select=True, ), ) ) else: return View( Item( name="people", editor=TabularEditor( adapter=ReportAdapter(), selected="selected", selected_row="selected_row", refresh="refresh", update="update", ), ) ) def get_selected_rows(editor): """Returns a list of all currently selected rows.""" if is_wx(): import wx # "item" in this context means "row number" item = -1 selected = [] while True: item = editor.control.GetNextItem( item, wx.LIST_NEXT_ALL, wx.LIST_STATE_SELECTED ) if item == -1: break selected.append(item) return selected elif is_qt(): rows = editor.control.selectionModel().selectedRows() return [r.row() for r in rows] else: raise unittest.SkipTest("Test not implemented for this toolkit") def set_selected_single(editor, row): """Selects a specified row in an editor with multi_select=False.""" if is_wx(): editor.control.Select(row) elif is_qt(): from pyface.qt.QtGui import QItemSelectionModel smodel = editor.control.selectionModel() mi = editor.model.index(row, 0) # Add `Rows` flag to select the whole row smodel.select( mi, QItemSelectionModel.SelectionFlag.ClearAndSelect | QItemSelectionModel.SelectionFlag.Rows ) else: raise unittest.SkipTest("Test not implemented for this toolkit") def set_selected_multiple(editor, rows): """Clears old selection and selects specified rows in an editor with multi_select=True. """ if is_wx(): clear_selection(editor) for row in rows: editor.control.Select(row) elif is_qt(): from pyface.qt.QtGui import QItemSelectionModel clear_selection(editor) smodel = editor.control.selectionModel() for row in rows: mi = editor.model.index(row, 0) # Add `Rows` flag to select the whole row smodel.select( mi, QItemSelectionModel.SelectionFlag.Select | QItemSelectionModel.SelectionFlag.Rows ) else: raise unittest.SkipTest("Test not implemented for this toolkit") def clear_selection(editor): """Clears existing selection.""" if is_wx(): import wx currently_selected = get_selected_rows(editor) # Deselect all currently selected items for selected_row in currently_selected: editor.control.SetItemState( selected_row, 0, wx.LIST_STATE_SELECTED ) elif is_qt(): editor.control.selectionModel().clearSelection() else: raise unittest.SkipTest("Test not implemented for this toolkit") @requires_toolkit([ToolkitName.qt, ToolkitName.wx]) class TestTabularEditor(BaseTestMixin, UnittestTools, unittest.TestCase): def setUp(self): BaseTestMixin.setUp(self) def tearDown(self): BaseTestMixin.tearDown(self) @unittest.skipIf(is_wx(), "Issue enthought/traitsui#752") def test_tabular_editor_single_selection(self): with reraise_exceptions(), self.report_and_editor(get_view()) as ( report, editor, ): process_cascade_events() people = report.people self.assertEqual(report.selected_row, -1) self.assertIsNone(report.selected) set_selected_single(editor, 1) process_cascade_events() self.assertEqual(report.selected_row, 1) self.assertEqual(report.selected, people[1]) set_selected_single(editor, 2) process_cascade_events() self.assertEqual(report.selected_row, 2) self.assertEqual(report.selected, people[2]) # Can't clear selection via UI when multi_select=False @unittest.skipIf(is_wx(), "Issue enthought/traitsui#752") def test_tabular_editor_multi_selection(self): view = get_view(multi_select=True) with reraise_exceptions(), self.report_and_editor(view) as ( report, editor, ): process_cascade_events() people = report.people self.assertEqual(report.selected_rows, []) self.assertEqual(report.multi_selected, []) set_selected_multiple(editor, [0, 1]) process_cascade_events() self.assertEqual(report.selected_rows, [0, 1]) self.assertEqual(report.multi_selected, people[:2]) set_selected_multiple(editor, [2]) process_cascade_events() self.assertEqual(report.selected_rows, [2]) self.assertEqual(report.multi_selected, [people[2]]) clear_selection(editor) process_cascade_events() self.assertEqual(report.selected_rows, []) self.assertEqual(report.multi_selected, []) @unittest.skipIf(is_wx(), "Issue enthought/traitsui#752") def test_tabular_editor_single_selection_changed(self): with reraise_exceptions(), self.report_and_editor(get_view()) as ( report, editor, ): process_cascade_events() people = report.people self.assertEqual(get_selected_rows(editor), []) report.selected_row = 1 process_cascade_events() self.assertEqual(get_selected_rows(editor), [1]) self.assertEqual(report.selected, people[1]) report.selected = people[2] process_cascade_events() self.assertEqual(get_selected_rows(editor), [2]) self.assertEqual(report.selected_row, 2) # Selected set to invalid value doesn't change anything report.selected = Person(name="invalid", age=-1) process_cascade_events() self.assertEqual(get_selected_rows(editor), [2]) self.assertEqual(report.selected_row, 2) # -1 clears selection report.selected_row = -1 process_cascade_events() self.assertEqual(get_selected_rows(editor), []) self.assertEqual(report.selected, None) @unittest.skipIf(is_wx(), "Issue enthought/traitsui#752") def test_tabular_editor_multi_selection_changed(self): view = get_view(multi_select=True) with reraise_exceptions(), self.report_and_editor(view) as ( report, editor, ): process_cascade_events() people = report.people self.assertEqual(get_selected_rows(editor), []) report.selected_rows = [0, 1] process_cascade_events() self.assertEqual(get_selected_rows(editor), [0, 1]) self.assertEqual(report.multi_selected, people[:2]) report.multi_selected = [people[2], people[0]] process_cascade_events() self.assertEqual(sorted(get_selected_rows(editor)), [0, 2]) self.assertEqual(sorted(report.selected_rows), [0, 2]) # If there's a single invalid value, nothing is updated invalid_person = Person(name="invalid", age=-1) report.multi_selected = [people[2], invalid_person] process_cascade_events() self.assertEqual(sorted(get_selected_rows(editor)), [0, 2]) self.assertEqual(sorted(report.selected_rows), [0, 2]) # Empty list clears selection report.selected_rows = [] process_cascade_events() self.assertEqual(get_selected_rows(editor), []) self.assertEqual(report.multi_selected, []) @unittest.skipIf(is_wx(), "Issue enthought/traitsui#752") def test_tabular_editor_multi_selection_items_changed(self): view = get_view(multi_select=True) with reraise_exceptions(), self.report_and_editor(view) as ( report, editor, ): process_cascade_events() people = report.people self.assertEqual(get_selected_rows(editor), []) report.selected_rows.extend([0, 1]) process_cascade_events() self.assertEqual(get_selected_rows(editor), [0, 1]) self.assertEqual(report.multi_selected, people[:2]) report.selected_rows[1] = 2 process_cascade_events() self.assertEqual(get_selected_rows(editor), [0, 2]) self.assertEqual(report.multi_selected, people[0:3:2]) report.multi_selected[0] = people[1] process_cascade_events() self.assertEqual(sorted(get_selected_rows(editor)), [1, 2]) self.assertEqual(sorted(report.selected_rows), [1, 2]) # If there's a single invalid value, nothing is updated report.multi_selected[0] = Person(name="invalid", age=-1) process_cascade_events() self.assertEqual(sorted(get_selected_rows(editor)), [1, 2]) self.assertEqual(sorted(report.selected_rows), [1, 2]) @unittest.skipIf(is_wx(), "Issue enthought/traitsui#752") def test_selected_reacts_to_model_changes(self): with self.report_and_editor(get_view()) as (report, editor): people = report.people self.assertIsNone(report.selected) self.assertEqual(report.selected_row, -1) report.selected = people[1] self.assertEqual(report.selected, people[1]) self.assertEqual(report.selected_row, 1) report.selected = None self.assertIsNone(report.selected) self.assertEqual(report.selected_row, -1) report.selected_row = 0 self.assertEqual(report.selected, people[0]) self.assertEqual(report.selected_row, 0) report.selected_row = -1 self.assertIsNone(report.selected) self.assertEqual(report.selected_row, -1) @unittest.skipIf(is_wx(), "Issue enthought/traitsui#752") def test_event_synchronization(self): with self.report_and_editor(get_view()) as (report, editor): with self.assertTraitChanges(editor, "refresh", count=1): report.refresh = True # Should happen every time. with self.assertTraitChanges(editor, "refresh", count=1): report.refresh = True with self.assertTraitChanges(editor, "update", count=1): report.update = True with self.assertTraitChanges(editor, "update", count=1): report.update = True @unittest.skipIf(is_wx(), "Issue enthought/traitsui#752") def test_adapter_columns_changes(self): # Regression test for enthought/traitsui#894 with reraise_exceptions(), self.report_and_editor(get_view()) as ( report, editor, ): # Reproduce the scenario when the column count is reduced. editor.adapter.columns = [ ("Name", "name"), ("Age", "age"), ] # Recalculating column widths take into account the user defined # widths, cached in the view. The cache should be invalidated # when the columns is updated such that recalculation does not # fail. editor.adapter.columns = [("Name", "name")] process_cascade_events() @unittest.skipIf(is_wx(), "Issue enthought/traitsui#752") def test_view_column_resized_attribute_error_workaround(self): # This tests the workaround which checks if `factory` is None before # using it while resizing the columns. # The resize event is processed after UI.dispose is called. # Maybe related to enthought/traits#431 with reraise_exceptions(), self.report_and_editor(get_view()) as ( _, editor, ): editor.adapter.columns = [("Name", "name")] @contextlib.contextmanager def report_and_editor(self, view): """ Context manager to temporarily create and clean up a Report model object and the corresponding TabularEditor. """ report = Report( people=[ Person(name="Theresa", age=60), Person(name="Arlene", age=46), Person(name="Karen", age=40), ] ) with create_ui(report, dict(view=view)) as ui: (editor,) = ui.get_editors("people") yield report, editor ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/tests/editors/test_text_editor.py0000644000175100001730000003354400000000000024620 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! import unittest from packaging.version import Version from traits import __version__ as TRAITS_VERSION from traits.api import ( HasTraits, Str, ) from traits.testing.api import UnittestTools from traitsui.api import TextEditor, View, Item from traitsui.testing.api import ( DisplayedText, KeyClick, KeySequence, MouseClick, InteractionNotSupported, UITester, ) from traitsui.tests._tools import ( BaseTestMixin, GuiTestAssistant, no_gui_test_assistant, requires_toolkit, ToolkitName, ) class Foo(HasTraits): name = Str() nickname = Str() def get_view(style, auto_set): """Return the default view for the Foo object. Parameters ---------- style : str e.g. 'simple', or 'custom' auto_set : bool To be passed directly to the editor factory. """ return View( Item("name", editor=TextEditor(auto_set=auto_set), style=style) ) # Skips tests if the backend is not either qt4 or qt5 @requires_toolkit([ToolkitName.qt]) @unittest.skipIf(no_gui_test_assistant, "No GuiTestAssistant") class TestTextEditorQt( BaseTestMixin, GuiTestAssistant, UnittestTools, unittest.TestCase ): """Test on TextEditor with Qt backend.""" def setUp(self): BaseTestMixin.setUp(self) GuiTestAssistant.setUp(self) def tearDown(self): GuiTestAssistant.tearDown(self) BaseTestMixin.tearDown(self) def test_text_editor_placeholder_text(self): foo = Foo() editor = TextEditor( placeholder="Enter name", ) view = View(Item(name="name", editor=editor)) tester = UITester() with tester.create_ui(foo, dict(view=view)) as ui: (name_editor,) = ui.get_editors("name") self.assertEqual( name_editor.control.placeholderText(), "Enter name", ) def test_text_editor_placeholder_text_and_readonly(self): # Placeholder can be set independently of read_only flag foo = Foo() editor = TextEditor( placeholder="Enter name", read_only=True, ) view = View(Item(name="name", editor=editor)) tester = UITester() with tester.create_ui(foo, dict(view=view)) as ui: (name_editor,) = ui.get_editors("name") self.assertEqual( name_editor.control.placeholderText(), "Enter name", ) def test_text_editor_default_view(self): foo = Foo() tester = UITester() with tester.create_ui(foo) as ui: (name_editor,) = ui.get_editors("name") self.assertEqual( name_editor.control.placeholderText(), "", ) def test_text_editor_custom_style_placeholder(self): # Test against CustomEditor using QTextEdit foo = Foo() view = View( Item( name="name", style="custom", editor=TextEditor(placeholder="Enter name"), ) ) tester = UITester() with tester.create_ui(foo, dict(view=view)) as ui: (name_editor,) = ui.get_editors("name") try: placeholder = name_editor.control.placeholderText() except AttributeError: # placeholderText is introduced to QTextEdit since Qt 5.2 pass else: self.assertEqual(placeholder, "Enter name") def test_cancel_button(self): foo = Foo() view = View( Item( name="name", style="simple", editor=TextEditor(cancel_button=True), ) ) tester = UITester() with tester.create_ui(foo, dict(view=view)) as ui: (name_editor,) = ui.get_editors("name") # isClearButtonEnabled is introduced to QLineEdit since Qt 5.2 if hasattr(name_editor.control, 'isClearButtonEnabled'): self.assertTrue(name_editor.control.isClearButtonEnabled()) # We should be able to run this test case against wx. # Not running them now to avoid test interaction. See enthought/traitsui#752 @requires_toolkit([ToolkitName.qt, ToolkitName.wx]) class TestTextEditor(BaseTestMixin, unittest.TestCase, UnittestTools): """Tests that can be run with any toolkit as long as there is an implementation for simulating user interactions. """ def setUp(self): BaseTestMixin.setUp(self) def tearDown(self): BaseTestMixin.tearDown(self) def check_editor_init_and_dispose(self, style, auto_set): # Smoke test to test setup and tear down of an editor. foo = Foo() view = get_view(style=style, auto_set=auto_set) with UITester().create_ui(foo, dict(view=view)): pass def test_simple_editor_init_and_dispose(self): # Smoke test to test setup and tear down of an editor. self.check_editor_init_and_dispose(style="simple", auto_set=True) def test_custom_editor_init_and_dispose(self): # Smoke test to test setup and tear down of an editor. self.check_editor_init_and_dispose(style="custom", auto_set=True) def test_readonly_editor_init_and_dispose(self): # Smoke test to test setup and tear down of an editor. self.check_editor_init_and_dispose(style="readonly", auto_set=True) def test_simple_editor_init_and_dispose_no_auto_set(self): # Smoke test to test setup and tear down of an editor. self.check_editor_init_and_dispose(style="simple", auto_set=False) def test_custom_editor_init_and_dispose_no_auto_set(self): # Smoke test to test setup and tear down of an editor. self.check_editor_init_and_dispose(style="custom", auto_set=False) def test_simple_auto_set_update_text(self): foo = Foo() view = get_view(style="simple", auto_set=True) tester = UITester() with tester.create_ui(foo, dict(view=view)) as ui: with self.assertTraitChanges(foo, "name", count=3): name_field = tester.find_by_name(ui, "name") name_field.perform(KeySequence("NEW")) # with auto-set the displayed name should match the name trait display_name = name_field.inspect(DisplayedText()) self.assertEqual(foo.name, "NEW") self.assertEqual(display_name, foo.name) # Currently if auto_set is false, and enter_set is false (the default # behavior), on Qt to ensure the text is actually set, Enter will set # the value @requires_toolkit([ToolkitName.qt]) def test_simple_auto_set_false_do_not_update_qt(self): foo = Foo(name="") view = get_view(style="simple", auto_set=False) tester = UITester() with tester.create_ui(foo, dict(view=view)) as ui: name_field = tester.find_by_name(ui, "name") name_field.perform(KeySequence("NEW")) # with auto-set as False the displayed name should match what has # been typed not the trait itself, After "Enter" is pressed it # should match the name trait display_name = name_field.inspect(DisplayedText()) self.assertEqual(foo.name, "") self.assertEqual(display_name, "NEW") name_field.perform(KeyClick("Enter")) display_name = name_field.inspect(DisplayedText()) self.assertEqual(foo.name, "NEW") self.assertEqual(display_name, foo.name) # If auto_set is false, the value can be set by killing the focus. This # can be done by simply moving to another textbox. @requires_toolkit([ToolkitName.wx]) def test_simple_auto_set_false_do_not_update_wx(self): foo = Foo(name="") view = View( Item("name", editor=TextEditor(auto_set=False), style="simple"), Item( "nickname", editor=TextEditor(auto_set=False), style="simple" ), ) tester = UITester() with tester.create_ui(foo, dict(view=view)) as ui: name_field = tester.find_by_name(ui, "name") name_field.perform(KeySequence("NEW")) # with auto-set as False the displayed name should match what has # been typed not the trait itself, After moving to another textbox # it should match the name trait display_name = name_field.inspect(DisplayedText()) self.assertEqual(foo.name, "") self.assertEqual(display_name, "NEW") tester.find_by_name(ui, "nickname").perform(MouseClick()) display_name = name_field.inspect(DisplayedText()) self.assertEqual(foo.name, "NEW") self.assertEqual(display_name, foo.name) def test_custom_auto_set_true_update_text(self): # the auto_set flag is disregard for custom editor. (not true on WX) foo = Foo() view = get_view(auto_set=True, style="custom") tester = UITester() with tester.create_ui(foo, dict(view=view)) as ui: with self.assertTraitChanges(foo, "name", count=3): name_field = tester.find_by_name(ui, "name") name_field.perform(KeySequence("NEW")) # with auto-set the displayed name should match the name trait display_name = name_field.inspect(DisplayedText()) self.assertEqual(foo.name, "NEW") self.assertEqual(display_name, foo.name) @requires_toolkit([ToolkitName.qt]) def test_custom_auto_set_false_update_text(self): # the auto_set flag is disregard for custom editor. (not true on WX) foo = Foo() view = get_view(auto_set=False, style="custom") tester = UITester() with tester.create_ui(foo, dict(view=view)) as ui: name_field = tester.find_by_name(ui, "name") name_field.perform(KeySequence("NEW")) name_field.perform(KeyClick("Enter")) display_name = name_field.inspect(DisplayedText()) self.assertEqual(foo.name, "NEW\n") self.assertEqual(display_name, foo.name) # If auto_set is false, the value can be set by killing the focus. This # can be done by simply moving to another textbox. @requires_toolkit([ToolkitName.wx]) def test_custom_auto_set_false_do_not_update_wx(self): foo = Foo(name="") view = View( Item("name", editor=TextEditor(auto_set=False), style="custom"), Item( "nickname", editor=TextEditor(auto_set=False), style="custom" ), ) tester = UITester() with tester.create_ui(foo, dict(view=view)) as ui: name_field = tester.find_by_name(ui, "name") name_field.perform(KeySequence("NEW")) # with auto-set as False the displayed name should match what has # been typed not the trait itself, After moving to another textbox # it should match the name trait display_name = name_field.inspect(DisplayedText()) self.assertEqual(foo.name, "") self.assertEqual(display_name, "NEW") tester.find_by_name(ui, "nickname").perform(MouseClick()) display_name = name_field.inspect(DisplayedText()) self.assertEqual(foo.name, "NEW") self.assertEqual(display_name, foo.name) def test_readonly_editor(self): foo = Foo(name="A name") view = get_view(style="readonly", auto_set=True) tester = UITester() with tester.create_ui(foo, dict(view=view)) as ui: name_field = tester.find_by_name(ui, "name") with self.assertRaises(InteractionNotSupported): name_field.perform(KeySequence("NEW")) # Trying to type should do nothing with self.assertRaises(InteractionNotSupported): name_field.perform(KeyClick("Space")) display_name = name_field.inspect(DisplayedText()) self.assertEqual(display_name, "A name") def check_format_func_used(self, style): # Regression test for enthought/traitsui#790 # The test will fail with traits < 6.1.0 because the bug # is fixed in traits, see enthought/traitsui#980 for moving those # relevant code to traitsui. foo = Foo(name="william", nickname="bill") view = View( Item("name", format_func=lambda s: s.upper(), style=style), Item("nickname", style=style), ) tester = UITester() with tester.create_ui(foo, dict(view=view)) as ui: display_name = tester.find_by_name(ui, "name").inspect( DisplayedText() ) display_nickname = tester.find_by_name(ui, "nickname").inspect( DisplayedText() ) self.assertEqual(display_name, "WILLIAM") self.assertEqual(display_nickname, "bill") @unittest.skipUnless( Version(TRAITS_VERSION) >= Version("6.1.0"), "This test requires traits >= 6.1.0", ) def test_format_func_used_simple(self): self.check_format_func_used(style='simple') @unittest.skipUnless( Version(TRAITS_VERSION) >= Version("6.1.0"), "This test requires traits >= 6.1.0", ) def test_format_func_used_custom(self): self.check_format_func_used(style='custom') @unittest.skipUnless( Version(TRAITS_VERSION) >= Version("6.1.0"), "This test requires traits >= 6.1.0", ) def test_format_func_used_readonly(self): self.check_format_func_used(style='readonly') ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/tests/editors/test_tree_editor.py0000644000175100001730000002304700000000000024570 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! import unittest from pyface.api import GUI from traits.api import Bool, Button, HasTraits, Instance, List, Str from traitsui.api import ( Handler, Item, ObjectTreeNode, TreeEditor, TreeNode, TreeNodeObject, View, ) from traitsui.testing.api import MouseClick, UITester from traitsui.tests._tools import ( BaseTestMixin, create_ui, requires_toolkit, reraise_exceptions, ToolkitName, ) class BogusWrap(HasTraits): """A bogus class representing a bogus tree.""" name = Str("Totally bogus") class Bogus(HasTraits): """A bogus class representing a bogus tree.""" name = Str("Bogus") bogus_list = List() wrapped_bogus = Instance(BogusWrap) def _wrapped_bogus_default(self): return BogusWrap(bogus_list=[Bogus()]) class BogusHandler(Handler): def object_expand_all_changed(self, info): editor = info.ui.get_editors('bogus')[0] editor.expand_all() class BogusTreeView(HasTraits): """A traitsui view visualizing Bogus objects as trees.""" bogus = Instance(Bogus) hide_root = Bool() word_wrap = Bool() nodes = List(TreeNode) expand_all = Button() def _nodes_default(self): return [ TreeNode(node_for=[Bogus], children="bogus_list", label="=Bogus"), TreeNode(node_for=[BogusWrap], label='name'), ] def default_traits_view(self): tree_editor = TreeEditor( nodes=self.nodes, hide_root=self.hide_root, editable=False, word_wrap=self.word_wrap, ) traits_view = View( Item(name="bogus", id="engine", editor=tree_editor), Item('expand_all'), buttons=["OK"], handler=BogusHandler(), ) return traits_view class BogusTreeNodeObject(TreeNodeObject): """A bogus tree node.""" name = Str("Bogus") bogus_list = List() class BogusTreeNodeObjectView(HasTraits): """A traitsui view visualizing Bogus objects as trees.""" bogus = Instance(BogusTreeNodeObject) hide_root = Bool() nodes = List(TreeNode) def _nodes_default(self): return [ TreeNode( node_for=[BogusTreeNodeObject], children="bogus_list", label="=Bogus", ) ] def default_traits_view(self): tree_editor = TreeEditor( nodes=self.nodes, hide_root=self.hide_root, editable=False ) traits_view = View( Item(name="bogus", id="engine", editor=tree_editor), buttons=["OK"] ) return traits_view class TestTreeView(BaseTestMixin, unittest.TestCase): def setUp(self): BaseTestMixin.setUp(self) def tearDown(self): BaseTestMixin.tearDown(self) # test for wx is failing for other reasons. # It might pass once we receive fix for enthought/pyface#558 @requires_toolkit([ToolkitName.qt]) def test_tree_editor_with_nested_ui(self): tree_editor = TreeEditor( nodes=[ TreeNode( node_for=[Bogus], auto_open=True, children="bogus_list", label="bogus", view=View(Item("name")), ), ], hide_root=True, ) bogus_list = [Bogus()] object_view = BogusTreeView(bogus=Bogus(bogus_list=bogus_list)) view = View(Item("bogus", id="tree", editor=tree_editor)) with reraise_exceptions(), create_ui( object_view, dict(view=view) ) as ui: editor = ui.info.tree editor.selected = bogus_list[0] GUI.process_events() def _test_tree_editor_releases_listeners( self, hide_root, nodes=None, trait="bogus_list", expected_listeners=1 ): """The TreeEditor should release the listener to the root node's children when it's disposed of. """ bogus = Bogus(bogus_list=[Bogus()]) tree_editor_view = BogusTreeView( bogus=bogus, hide_root=hide_root, nodes=nodes ) with reraise_exceptions(), create_ui(tree_editor_view): # The TreeEditor sets a listener on the bogus object's # children list notifiers_list = bogus.trait(trait)._notifiers(False) self.assertEqual(expected_listeners, len(notifiers_list)) # The listener should be removed after the UI has been closed notifiers_list = bogus.trait(trait)._notifiers(False) self.assertEqual(0, len(notifiers_list)) def _test_tree_node_object_releases_listeners( self, hide_root, nodes=None, trait="bogus_list", expected_listeners=1 ): """The TreeEditor should release the listener to the root node's children when it's disposed of. """ bogus = BogusTreeNodeObject(bogus_list=[BogusTreeNodeObject()]) tree_editor_view = BogusTreeNodeObjectView( bogus=bogus, hide_root=hide_root, nodes=nodes ) with reraise_exceptions(), create_ui(tree_editor_view): # The TreeEditor sets a listener on the bogus object's # children list notifiers_list = bogus.trait(trait)._notifiers(False) self.assertEqual(expected_listeners, len(notifiers_list)) if trait == "name": # fire a label change bogus.name = "Something else" else: # change the children bogus.bogus_list.append(BogusTreeNodeObject()) # The listener should be removed after the UI has been closed notifiers_list = bogus.trait(trait)._notifiers(False) self.assertEqual(0, len(notifiers_list)) @requires_toolkit([ToolkitName.qt]) def test_tree_editor_listeners_with_shown_root(self): nodes = [ TreeNode(node_for=[Bogus], children="bogus_list", label="=Bogus") ] self._test_tree_editor_releases_listeners(hide_root=False, nodes=nodes) @requires_toolkit([ToolkitName.qt]) def test_tree_editor_listeners_with_hidden_root(self): nodes = [ TreeNode(node_for=[Bogus], children="bogus_list", label="=Bogus") ] self._test_tree_editor_releases_listeners(hide_root=True, nodes=nodes) @requires_toolkit([ToolkitName.qt]) def test_tree_editor_label_listener(self): nodes = [ TreeNode(node_for=[Bogus], children="bogus_list", label="name") ] self._test_tree_editor_releases_listeners( hide_root=False, nodes=nodes, trait="name" ) @requires_toolkit([ToolkitName.qt]) def test_tree_editor_xgetattr_label_listener(self): nodes = [ TreeNode( node_for=[Bogus], children="bogus_list", label="wrapped_bogus.name", ) ] self._test_tree_editor_releases_listeners( hide_root=False, nodes=nodes, trait="wrapped_bogus", expected_listeners=2, ) @requires_toolkit([ToolkitName.qt]) def test_tree_node_object_listeners_with_shown_root(self): nodes = [ ObjectTreeNode( node_for=[BogusTreeNodeObject], children="bogus_list", label="=Bogus", ) ] self._test_tree_node_object_releases_listeners( nodes=nodes, hide_root=False ) @requires_toolkit([ToolkitName.qt]) def test_tree_node_object_listeners_with_hidden_root(self): nodes = [ ObjectTreeNode( node_for=[BogusTreeNodeObject], children="bogus_list", label="=Bogus", ) ] self._test_tree_node_object_releases_listeners( nodes=nodes, hide_root=True ) @requires_toolkit([ToolkitName.qt]) def test_tree_node_object_label_listener(self): nodes = [ ObjectTreeNode( node_for=[BogusTreeNodeObject], children="bogus_list", label="name", ) ] self._test_tree_node_object_releases_listeners( nodes=nodes, hide_root=False, trait="name" ) @requires_toolkit([ToolkitName.qt, ToolkitName.wx]) def test_smoke_save_restore_prefs(self): bogus = Bogus(bogus_list=[Bogus()]) tree_editor_view = BogusTreeView(bogus=bogus) with create_ui(tree_editor_view) as ui: prefs = ui.get_prefs() ui.set_prefs(prefs) @requires_toolkit([ToolkitName.qt]) def test_smoke_word_wrap(self): bogus = Bogus(bogus_list=[Bogus()]) tree_editor_view = BogusTreeView(bogus=bogus, word_wrap=True) with create_ui(tree_editor_view): pass # regression test for enthought/traitsui#1726 @requires_toolkit([ToolkitName.qt]) def test_expand_all(self): bogus = Bogus(bogus_list=[BogusWrap()]) tree_editor_view = BogusTreeView(bogus=bogus) tester = UITester() with tester.create_ui(tree_editor_view) as ui: expand_all_button = tester.find_by_name(ui, "expand_all") expand_all_button.perform(MouseClick()) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/tests/editors/test_tuple_editor.py0000644000175100001730000000511500000000000024756 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! import unittest from traits.api import HasTraits, Int, Str, Tuple from traitsui.api import Item, View from traits.testing.api import UnittestTools from traitsui.tests._tools import ( BaseTestMixin, create_ui, requires_toolkit, reraise_exceptions, ToolkitName, ) class TupleEditor(HasTraits): """Dialog containing a Tuple of two Int's.""" tup = Tuple(Int, Int, Str) traits_view = View( Item(label="Enter 4 and 6, then press OK"), Item("tup"), buttons=["OK"] ) class TestTupleEditor(BaseTestMixin, unittest.TestCase, UnittestTools): def setUp(self): BaseTestMixin.setUp(self) def tearDown(self): BaseTestMixin.tearDown(self) @requires_toolkit([ToolkitName.qt, ToolkitName.wx]) def test_value_update(self): # Regression test for #179 model = TupleEditor() with create_ui(model): with self.assertTraitChanges(model, "tup", count=1): model.tup = (3, 4, "nono") @requires_toolkit([ToolkitName.qt]) def test_qt_tuple_editor(self): # Behavior: when editing the text of a tuple editor, # value get updated immediately. from pyface import qt val = TupleEditor() with reraise_exceptions(), create_ui(val) as ui: # the following is equivalent to clicking in the text control of # the range editor, enter a number, and clicking ok without # defocusing # text element inside the spin control lineedits = ui.control.findChildren(qt.QtGui.QLineEdit) lineedits[0].setFocus() lineedits[0].clear() lineedits[0].insert("4") lineedits[1].setFocus() lineedits[1].clear() lineedits[1].insert("6") lineedits[2].setFocus() lineedits[2].clear() lineedits[2].insert("fun") # if all went well, the tuple trait has been updated and its value # is (4, 6, "fun") self.assertEqual(val.tup, (4, 6, "fun")) if __name__ == "__main__": # Executing the file opens the dialog for manual testing val = TupleEditor() val.configure_traits() print(val.tup) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/tests/editors/test_video_editor.py0000644000175100001730000000603600000000000024736 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! import os import unittest import subprocess import sys try: import numpy as np # noqa: F401 except ImportError: raise unittest.SkipTest("Can't import NumPy: skipping") import pkg_resources from traits.api import Bool, Callable, File, Float, HasTraits, Range, Str from traitsui.api import ContextValue, Item, View from traitsui.editors.video_editor import MediaStatus, PlayerState, VideoEditor from traitsui.tests._tools import BaseTestMixin, create_ui, is_qt5, is_qt6 filename = pkg_resources.resource_filename('traitsui.testing', 'data/test.mp4') # Is a MacOS machine lacking the standard Metal APIs? metal_api_missing = False if sys.platform == 'darwin' and is_qt6: # TODO: would be nice to detect if Qt build _uses_ Metal result = subprocess.run( ["system_profiler", "SPDisplaysDataType"], capture_output=True, check=True, ) metal_api_missing = (b'Metal Family: Supported' not in result.stdout) class MovieTheater(HasTraits): url = File(filename) state = PlayerState() duration = Float() position = Range(low=0.0, high='duration') error = Str() status = MediaStatus() buffer = Range(0, 100) muted = Bool(True) volume = Range(0.0, 100.0) playback_rate = Float(1.0) image_func = Callable() @unittest.skipIf(not is_qt5() and not is_qt6(), 'Requires Qt5 or 6') @unittest.skipIf(metal_api_missing, "Mac Qt6 video editor requires Metal API") class TestVideoEditor(BaseTestMixin, unittest.TestCase): def setUp(self): BaseTestMixin.setUp(self) def tearDown(self): BaseTestMixin.tearDown(self) def test_video_editor_basics(self): obj = MovieTheater() # Test an editor with trait syncronizations view = View( Item( 'url', editor=VideoEditor( state=ContextValue('state'), position=ContextValue('position'), duration=ContextValue('duration'), video_error=ContextValue('error'), media_status=ContextValue('status'), buffer=ContextValue('buffer'), muted=ContextValue('muted'), volume=ContextValue('volume'), playback_rate=ContextValue('playback_rate'), image_func=ContextValue('image_func'), notify_interval=0.5, ), ), ) with create_ui(obj, {'view': view}): pass # And an editor with no synced traits view = View(Item('url', editor=VideoEditor())) with create_ui(obj, {'view': view}): pass ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1684768028.1158068 traitsui-8.0.0/traitsui/tests/null_backend/0000755000175100001730000000000000000000000021614 5ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/tests/null_backend/__init__.py0000644000175100001730000000000000000000000023713 0ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/tests/null_backend/test_font_trait.py0000644000175100001730000000372300000000000025403 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! import unittest from traits.api import HasTraits from traitsui.toolkit_traits import Font from traitsui.tests._tools import BaseTestMixin, requires_toolkit, ToolkitName class TestFontTrait(BaseTestMixin, unittest.TestCase): def setUp(self): BaseTestMixin.setUp(self) def tearDown(self): BaseTestMixin.tearDown(self) @requires_toolkit([ToolkitName.null]) def test_font_trait_default(self): class Foo(HasTraits): font = Font() f = Foo() self.assertEqual(f.font, "10 pt Arial") @requires_toolkit([ToolkitName.null]) def test_font_trait_examples(self): """ An assigned font string is parsed, and the substrings are put in the order: point size, family, style, weight, underline, facename The words 'pt, 'point' and 'family' are ignored. """ class Foo(HasTraits): font = Font f = Foo(font="Qwerty 10") self.assertEqual(f.font, "10 pt Qwerty") f = Foo(font="nothing") self.assertEqual(f.font, "nothing") f = Foo(font="swiss family arial") self.assertEqual(f.font, "swiss arial") f = Foo(font="12 pt bold italic") self.assertEqual(f.font, "12 pt italic bold") f = Foo(font="123 Foo bar slant") self.assertEqual(f.font, "123 pt slant Foo bar") f = Foo(font="123 point Foo family bar slant") self.assertEqual(f.font, "123 pt slant Foo bar") f = Foo(font="16 xyzzy underline slant") self.assertEqual(f.font, "16 pt slant underline xyzzy") ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/tests/null_backend/test_null_toolkit.py0000644000175100001730000000202000000000000025736 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! import unittest from traits.api import HasTraits, Int from traitsui.tests._tools import BaseTestMixin, requires_toolkit, ToolkitName class TestNullToolkit(BaseTestMixin, unittest.TestCase): def setUp(self): BaseTestMixin.setUp(self) def tearDown(self): BaseTestMixin.tearDown(self) @requires_toolkit([ToolkitName.null]) def test_configure_traits_error(self): """Verify that configure_traits fails with NotImplementedError.""" class Test(HasTraits): x = Int() t = Test() with self.assertRaises(NotImplementedError): t.configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/tests/test_actions.py0000644000175100001730000002355500000000000022256 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Test that menu and toolbar actions are triggered. """ from functools import partial import unittest import pyface from pyface.action.schema.api import ActionSchema, SGroup, SMenu, SMenuBar, SToolBar from traits.has_traits import HasTraits from traits.trait_types import Bool from traitsui.menu import Action, ActionGroup, Menu, MenuBar, ToolBar from traitsui.item import Item from traitsui.view import View from traitsui.tests._tools import ( BaseTestMixin, create_ui, is_mac_os, is_null, requires_toolkit, reraise_exceptions, ToolkitName, ) if is_null(): raise unittest.SkipTest("Not supported using the null backend") TestAction = Action( name="Test", action="test_clicked", tooltip="Click to test" ) class DialogWithToolbar(HasTraits): """Test dialog with toolbar and menu.""" action_successful = Bool(False) def test_clicked(self): self.action_successful = True menubar = MenuBar(Menu(ActionGroup(TestAction), name="&Test menu")) toolbar = ToolBar(ActionGroup(TestAction)) traits_view = View( Item( label="Click the button on the toolbar or the menu item.\n" "The 'Action successful' element should turn to True." ), Item("action_successful", style="readonly"), menubar=menubar, toolbar=toolbar, buttons=[TestAction, "OK"], ) class DialogWithSchema(HasTraits): """Test dialog with toolbar and menu schemas.""" action_successful = Bool(False) def test_clicked(self): self.action_successful = True menubar = SMenuBar( SMenu( SGroup( ActionSchema( action_factory=lambda **traits: TestAction, ), ), name="&Test menu", ), ) toolbar = SToolBar( SGroup( ActionSchema( action_factory=lambda **traits: TestAction, ), ), ) traits_view = View( Item( label="Click the button on the toolbar or the menu item.\n" "The 'Action successful' element should turn to True." ), Item("action_successful", style="readonly"), menubar=menubar, toolbar=toolbar, buttons=[TestAction, "OK"], ) # ----- qt helper functions def _qt_trigger_action(container_class, ui): toolbar = ui.control.findChild(container_class) action = toolbar.actions()[0] action.trigger() def _qt_click_button(ui): from pyface.qt.QtGui import QDialogButtonBox bbox = ui.control.findChild(QDialogButtonBox) button = bbox.buttons()[1] button.click() class TestActions(BaseTestMixin, unittest.TestCase): def _test_actions(self, trigger_action_func): """Template test for wx, qt, menu, and toolbar testing.""" # Behavior: when clicking on a menu or toolbar action, # the corresponding function should be executed # create dialog with toolbar adn menu dialog = DialogWithToolbar() with reraise_exceptions(), create_ui(dialog) as ui: # press toolbar or menu button trigger_action_func(ui) # verify that the action was triggered self.assertTrue(dialog.action_successful) # ----- Qt tests @requires_toolkit([ToolkitName.qt]) def test_qt_toolbar_action(self): # Behavior: when clicking on a toolbar action, the corresponding # function should be executed # Bug: in the Qt4 backend, a # TypeError: perform() takes exactly 2 arguments (1 given) was raised # instead from pyface.ui.qt.action.tool_bar_manager import _ToolBar qt_trigger_toolbar_action = partial(_qt_trigger_action, _ToolBar) self._test_actions(qt_trigger_toolbar_action) @requires_toolkit([ToolkitName.qt]) def test_qt_menu_action(self): # Behavior: when clicking on a menu action, the corresponding function # should be executed # Bug: in the Qt4 backend, a # TypeError: perform() takes exactly 2 arguments (1 given) was raised # instead from pyface.ui.qt.action.menu_manager import _Menu qt_trigger_menu_action = partial( _qt_trigger_action, _Menu ) self._test_actions(qt_trigger_menu_action) @requires_toolkit([ToolkitName.qt]) def test_qt_button_action(self): # Behavior: when clicking on a button action, the corresponding # function should be executed # Bug: in the Qt4 backend, a # TypeError: perform() takes exactly 2 arguments (1 given) was raised # instead self._test_actions(_qt_click_button) # ----- wx tests @unittest.skip( "Problem with triggering toolbar actions. Issue #428 and #1843." ) @requires_toolkit([ToolkitName.wx]) def test_wx_toolbar_action(self): # Behavior: when clicking on a toolbar action, the corresponding # function should be executed import wx def _wx_trigger_toolbar_action(ui): # long road to get at the Id of the toolbar button toolbar_control = ui.control.GetToolBar() toolbar_item = toolbar_control.tool_bar_manager.groups[0].items[0] toolbar_item_wrapper = toolbar_item._wrappers[0] control_id = toolbar_item_wrapper.control_id # build event that clicks the button click_event = wx.CommandEvent( wx.wxEVT_COMMAND_TOOL_CLICKED, control_id ) # send the event to the toolbar toolbar_control.ProcessEvent(click_event) self._test_actions(_wx_trigger_toolbar_action) @requires_toolkit([ToolkitName.wx]) def test_wx_button_action(self): # Behavior: when clicking on a button action, the corresponding # function should be executed import wx def _wx_trigger_button_action(ui): # long road to get at the Id of the toolbar button button_sizer = ui.control.GetSizer().GetChildren()[2].GetSizer() button = button_sizer.GetChildren()[0].GetWindow() control_id = button.GetId() # build event that clicks the button click_event = wx.CommandEvent( wx.wxEVT_COMMAND_BUTTON_CLICKED, control_id ) # send the event to the toolbar ui.control.ProcessEvent(click_event) self._test_actions(_wx_trigger_button_action) # TODO: I couldn't find a way to press menu items programmatically for wx class TestActionSchemas(BaseTestMixin, unittest.TestCase): def setUp(self): BaseTestMixin.setUp(self) def tearDown(self): BaseTestMixin.tearDown(self) def _test_actions(self, trigger_action_func): """Template test for wx, qt, menu, and toolbar testing.""" # Behavior: when clicking on a menu or toolbar action, # the corresponding function should be executed # create dialog with toolbar adn menu dialog = DialogWithSchema() with reraise_exceptions(), create_ui(dialog) as ui: # press toolbar or menu button trigger_action_func(ui) # verify that the action was triggered self.assertTrue(dialog.action_successful) # ----- Qt tests @requires_toolkit([ToolkitName.qt]) def test_qt_toolbar_action(self): # Behavior: when clicking on a toolbar action, the corresponding # function should be executed # Bug: in the Qt4 backend, a # TypeError: perform() takes exactly 2 arguments (1 given) was raised # instead from pyface.ui.qt.action.tool_bar_manager import _ToolBar qt_trigger_toolbar_action = partial(_qt_trigger_action, _ToolBar) self._test_actions(qt_trigger_toolbar_action) @requires_toolkit([ToolkitName.qt]) def test_qt_menu_action(self): # Behavior: when clicking on a menu action, the corresponding function # should be executed # Bug: in the Qt4 backend, a # TypeError: perform() takes exactly 2 arguments (1 given) was raised # instead from pyface.ui.qt.action.menu_manager import _Menu qt_trigger_menu_action = partial(_qt_trigger_action, _Menu) self._test_actions(qt_trigger_menu_action) # ----- wx tests @unittest.skipIf( not is_mac_os, "Problem with triggering toolbar actions on Linux and Windows. Issue #428.", # noqa: E501 ) @requires_toolkit([ToolkitName.wx]) def test_wx_toolbar_action(self): # Behavior: when clicking on a toolbar action, the corresponding # function should be executed import wx def _wx_trigger_toolbar_action(ui): # long road to get at the Id of the toolbar button toolbar_control = ui.control.GetToolBar() toolbar_item = toolbar_control.tool_bar_manager.groups[0].items[0] toolbar_item_wrapper = toolbar_item._wrappers[0] control_id = toolbar_item_wrapper.control_id # build event that clicks the button click_event = wx.CommandEvent( wx.wxEVT_COMMAND_TOOL_CLICKED, control_id ) # send the event to the toolbar toolbar_control.ProcessEvent(click_event) self._test_actions(_wx_trigger_toolbar_action) # TODO: I couldn't find a way to press menu items programmatically for wx if __name__ == "__main__": # Execute from command line for manual testing vw = DialogWithToolbar() vw.configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/tests/test_color_column.py0000644000175100001730000000346500000000000023307 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! from unittest import TestCase from traits.api import HasTraits, Str, Int, List from traitsui.api import View, Group, Item, TableEditor, ObjectColumn, RGBColor from traitsui.color_column import ColorColumn from traitsui.tests._tools import ( BaseTestMixin, create_ui, requires_toolkit, reraise_exceptions, ToolkitName, ) class MyEntry(HasTraits): name = Str() value = Int(0) color = RGBColor() entry_view = View(Group(Item("name"), Item("value"), Item("color"))) my_editor = TableEditor( columns=[ ObjectColumn(name="name"), ObjectColumn(name="value"), ColorColumn(name="color", style="readonly"), ], orientation="vertical", show_toolbar=True, row_factory=MyEntry, ) class MyData(HasTraits): data_list = List(MyEntry) view = View(Item("data_list", editor=my_editor, show_label=False)) class TestColorColumn(BaseTestMixin, TestCase): def setUp(self): BaseTestMixin.setUp(self) def tearDown(self): BaseTestMixin.tearDown(self) @requires_toolkit([ToolkitName.qt, ToolkitName.wx]) def test_color_column(self): # Behaviour: column ui should display without error d1 = MyEntry(name="a", value=2, color=(1.0, 0.3, 0.1)) d2 = MyEntry(name="b", value=3, color=(0.1, 0.0, 0.9)) data = MyData(data_list=[d1, d2]) with reraise_exceptions(), create_ui(data): pass ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/tests/test_context_value.py0000644000175100001730000000512500000000000023467 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! import unittest from traits.testing.unittest_tools import UnittestTools from traits.api import HasTraits, Str from traitsui.tests._tools import BaseTestMixin from traitsui.context_value import ContextValue, CVFloat, CVInt, CVStr, CVType class CVExample(HasTraits): cv_float = CVFloat cv_int = CVInt cv_str = CVStr cv_unicode = CVType(Str, something="meta", sync_value="both") class TestContextvalue(BaseTestMixin, UnittestTools, unittest.TestCase): def setUp(self): BaseTestMixin.setUp(self) def tearDown(self): BaseTestMixin.tearDown(self) def test_context_value(self): cv = ContextValue("trait_name") self.assertEqual(cv.name, "trait_name") def test_cv_float_constant(self): cve = CVExample(cv_float=1.1) self.assertEqual(cve.cv_float, 1.1) def test_cv_float_context_value(self): cv = ContextValue("trait_name") cve = CVExample(cv_float=cv) self.assertIs(cve.cv_float, cv) def test_cv_int_constant(self): cve = CVExample(cv_int=1) self.assertEqual(cve.cv_int, 1) def test_cv_int_context_value(self): cv = ContextValue("trait_name") cve = CVExample(cv_int=cv) self.assertIs(cve.cv_int, cv) def test_cv_str_constant(self): cve = CVExample(cv_str="test") self.assertEqual(cve.cv_str, "test") def test_cv_str_context_value(self): cv = ContextValue("trait_name") cve = CVExample(cv_str=cv) self.assertIs(cve.cv_str, cv) def test_cv_unicode_constant(self): cve = CVExample(cv_unicode="test") self.assertEqual(cve.cv_unicode, "test") def test_cv_unicode_context_value(self): cv = ContextValue("trait_name") cve = CVExample(cv_unicode=cv) self.assertIs(cve.cv_unicode, cv) def test_cv_unicode_not_none(self): with self.assertRaises(Exception): CVExample(cv_unicode=None) def test_metadata(self): cve = CVExample() t = cve.trait("cv_unicode") self.assertEqual(t.something, "meta") self.assertEqual(t.sync_value, "both") t = cve.trait("cv_float") self.assertEqual(t.sync_value, "to") ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/tests/test_controller.py0000644000175100001730000000260400000000000022771 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Test cases for the Controller class. """ import unittest from traits.api import HasTraits, Instance, Str from traitsui.api import Controller from traitsui.tests._tools import BaseTestMixin class FooModel(HasTraits): my_str = Str("hallo") class FooController(Controller): """Test dialog that does nothing useful.""" model = Instance(FooModel) def _model_default(self): return FooModel(my_str="meh") class TestController(BaseTestMixin, unittest.TestCase): def setUp(self): BaseTestMixin.setUp(self) def tearDown(self): BaseTestMixin.tearDown(self) def test_construction(self): # check default constructor. dialog = FooController() self.assertIsNotNone(dialog.model) self.assertEqual(dialog.model.my_str, "meh") # check initialization when `model` is explcitly passed in. new_model = FooModel() dialog = FooController(model=new_model) self.assertIs(dialog.model, new_model) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/tests/test_datetime.py0000644000175100001730000000402400000000000022400 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Test the initialization of a Datetime when given minimum and maximum dates in the DatetimeEditor """ from datetime import datetime, timedelta import unittest from traits.has_traits import HasTraits from traits.trait_types import Datetime from traitsui.editors.datetime_editor import DatetimeEditor from traitsui.item import Item from traitsui.view import View from traitsui.tests._tools import ( BaseTestMixin, create_ui, requires_toolkit, reraise_exceptions, ToolkitName, ) MAX_DATE = datetime.now() MIN_DATE = MAX_DATE - timedelta(days=10) DATE = MAX_DATE - timedelta(days=5) class DatetimeInitDailog(HasTraits): date = Datetime(allow_none=True) def _date_default(self): return DATE traits_view = View(Item('date', editor=DatetimeEditor( maximum_datetime=MAX_DATE, minimum_datetime=MIN_DATE))) class TestDatetimeEditor(BaseTestMixin, unittest.TestCase): def setUp(self): BaseTestMixin.setUp(self) def tearDown(self): BaseTestMixin.tearDown(self) @requires_toolkit([ToolkitName.qt]) def test_date_initialization(self): # Behavior: for a date initialized between maximum and minimum dates in # DatetimeEditor, that date should be unmodified dialog = DatetimeInitDailog() # verify that the date is correctly initialized self.assertEqual(dialog.date, DATE) with reraise_exceptions(), create_ui(dialog) as ui: # verify that the date hasn't changed after creating the ui self.assertEqual(dialog.date, DATE) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/tests/test_editor.py0000644000175100001730000007736600000000000022115 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! import unittest from pyface.toolkit import toolkit_object from traits.api import ( Any, Bool, Event, Float, HasTraits, Int, List, Range, Undefined, ) from traits.trait_base import xgetattr from traitsui.context_value import ContextValue, CVFloat, CVInt from traitsui.editor import Editor from traitsui.editor_factory import EditorFactory from traitsui.handler import default_handler from traitsui.ui import UI from traitsui.testing.api import KeyClick, KeySequence, Textbox, UITester from traitsui.tests._tools import ( BaseTestMixin, GuiTestAssistant, no_gui_test_assistant, requires_toolkit, ToolkitName, ) ModalDialogTester = toolkit_object( "util.modal_dialog_tester:ModalDialogTester" ) no_modal_dialog_tester = ModalDialogTester.__name__ == "Unimplemented" class FakeControl(HasTraits): """A pure Traits object that fakes being a control. It can aither hold a value (mimicking a field), or have an event which is listened to (mimicking a button or similar control). """ #: The value stored in the control. control_value = Any() #: An event which also can be fired. control_event = Event() #: The tooltip text for the control. tooltip = Any() class StubEditorFactory(EditorFactory): """A minimal editor factory This simply holds state that may or may not be copied to the editor. No attempt is made to handle custom/readonly/etc. variation. """ #: Whether or not the traits are events. is_event = Bool() #: Whether or not the traits are events. auxiliary_value = Any(sync_value=True) #: Whether or not the traits are events. auxiliary_cv_int = CVInt #: Whether or not the traits are events. auxiliary_cv_float = CVFloat def _auxiliary_cv_int_default(self): return 0 def _auxiliary_cv_float_default(self): return 0.0 class StubEditor(Editor): """A minimal editor implementaton for a StubEditorFactory. The editor creates a FakeControl instance as its control object and keeps values synchronized either to `control_value` or `control_event` (if `is_event` is True). """ #: Whether or not the traits are events. is_event = Bool() #: An auxiliary value we want to synchronize. auxiliary_value = Any() #: An auxiliary list we want to synchronize. auxiliary_list = List() #: An auxiliary event we want to synchronize. auxiliary_event = Event() #: An auxiliary int we want to synchronize with a context value. auxiliary_cv_int = Int(sync_value="from") #: An auxiliary float we want to synchronize with a context value. auxiliary_cv_float = Float() def init(self, parent): self.control = FakeControl() self.is_event = self.factory.is_event if self.is_event: self.control.on_trait_change(self.update_object, "control_event") else: self.control.on_trait_change(self.update_object, "control_value") self.set_tooltip() def dispose(self): if self.is_event: self.control.on_trait_change( self.update_object, "control_event", remove=True ) else: self.control.on_trait_change( self.update_object, "control_value", remove=True ) super().dispose() def update_editor(self): if self.is_event: self.control.control_event = True else: self.control.control_value = self.value def update_object(self, new): if self.control is not None: if not self._no_update: self._no_update = True try: self.value = new finally: self._no_update = False def set_tooltip_text(self, control, text): control.tooltip = text def set_focus(self, parent): pass class UserObject(HasTraits): """A simple HasTraits class with a variety of state.""" #: The value being edited. user_value = Any("test") #: An auxiliary user value user_auxiliary = Any(10) #: A list user value user_list = List(["one", "two", "three"]) #: A trait with desc metadata. user_desc = Any("test", desc="a trait with desc metadata") #: A trait with tooltip metadata. user_tooltip = Any("test", tooltip="a tooltip") #: An event user value user_event = Event() #: A state that is to be synchronized with the editor. invalid_state = Bool() def create_editor( context=None, object_name="object", name="user_value", factory=None, is_event=False, description="", ): if context is None: user_object = UserObject() context = {"object": user_object} elif "." in object_name: context_name, xname = object_name.split(".", 1) context_object = context[context_name] user_object = xgetattr(context_object, xname) else: user_object = context[object_name] ui = UI(context=context, handler=default_handler()) if factory is None: factory = StubEditorFactory() factory.is_event = is_event editor = StubEditor( parent=None, ui=ui, object_name=object_name, name=name, factory=factory, object=user_object, description=description, ) return editor @unittest.skipIf(no_gui_test_assistant, "No GuiTestAssistant") class TestEditor(BaseTestMixin, GuiTestAssistant, unittest.TestCase): def setUp(self): BaseTestMixin.setUp(self) GuiTestAssistant.setUp(self) def tearDown(self): GuiTestAssistant.tearDown(self) BaseTestMixin.tearDown(self) def change_user_value(self, editor, object, name, value): if editor.is_event: control_name = "control_event" else: control_name = "control_value" # test the value in the control changes with self.assertTraitChanges(editor.control, control_name, count=1): self.gui.set_trait_later(object, name, value) self.event_loop_helper.event_loop_with_timeout(repeat=6) def change_control_value(self, editor, object, name, value): if editor.is_event: control_name = "control_event" else: control_name = "control_value" # test the value in the user object changes with self.assertTraitChanges(object, name, count=1): self.gui.set_trait_later(editor.control, control_name, value) self.event_loop_helper.event_loop_with_timeout(repeat=6) def test_lifecycle(self): editor = create_editor() self.assertEqual(editor.old_value, "test") self.assertEqual(editor.name, "user_value") self.assertEqual(editor.extended_name, "user_value") self.assertEqual(editor.value, "test") self.assertEqual(editor.str_value, "test") self.assertIs(editor.value_trait, editor.object.trait("user_value")) self.assertIs(editor.context_object, editor.ui.context["object"]) editor.prepare(None) # preparation creates the control and sets the control value self.assertEqual(editor.value, "test") self.assertEqual(editor.control.control_value, "test") with self.assertTraitChanges(editor.ui, "modified", count=1): self.change_user_value( editor, editor.object, "user_value", "new test" ) self.assertEqual(editor.value, "new test") self.assertEqual(editor.control.control_value, "new test") self.assertTrue(editor.ui.modified) self.change_control_value( editor, editor.object, "user_value", "even newer test" ) self.assertEqual(editor.value, "even newer test") self.assertEqual(editor.object.user_value, "even newer test") editor.dispose() self.assertIsNone(editor.object) self.assertIsNone(editor.factory) self.assertIsNone(editor.control) def test_context_object(self): user_object = UserObject(user_value="other_test") context = {"object": UserObject(), "other_object": user_object} editor = create_editor(context=context, object_name="other_object") self.assertEqual(editor.old_value, "other_test") self.assertEqual(editor.name, "user_value") self.assertEqual(editor.extended_name, "user_value") self.assertIs(editor.context_object, editor.ui.context["other_object"]) editor.prepare(None) # preparation creates the control and sets the control value self.assertEqual(editor.value, "other_test") self.assertEqual(editor.control.control_value, "other_test") self.change_user_value(editor, user_object, "user_value", "new test") self.assertEqual(editor.value, "new test") self.assertEqual(editor.control.control_value, "new test") self.change_control_value( editor, user_object, "user_value", "even newer test" ) self.assertEqual(editor.value, "even newer test") self.assertEqual(editor.object.user_value, "even newer test") editor.dispose() def test_event_trait(self): editor = create_editor(name="user_event", is_event=True) user_object = editor.object self.assertEqual(editor.name, "user_event") self.assertEqual(editor.extended_name, "user_event") self.assertIs(editor.context_object, editor.ui.context["object"]) editor.prepare(None) # preparation creates the control and sets the control value self.assertIs(editor.value, Undefined) self.change_user_value(editor, user_object, "user_event", True) self.change_control_value(editor, user_object, "user_event", True) editor.dispose() def test_chained_object(self): context = { "object": UserObject( user_auxiliary=UserObject(user_value="other_test") ) } user_object = context["object"].user_auxiliary editor = create_editor( context=context, object_name="object.user_auxiliary" ) self.assertEqual(editor.old_value, "other_test") self.assertEqual(editor.name, "user_value") self.assertEqual(editor.extended_name, "user_auxiliary.user_value") self.assertIs(editor.context_object, editor.ui.context["object"]) editor.prepare(None) # preparation creates the control and sets the control value self.assertEqual(editor.value, "other_test") self.assertEqual(editor.control.control_value, "other_test") self.change_user_value(editor, user_object, "user_value", "new test") self.assertEqual(editor.value, "new test") self.assertEqual(editor.control.control_value, "new test") self.change_control_value( editor, user_object, "user_value", "even newer test" ) self.assertEqual(editor.value, "even newer test") self.assertEqual(editor.object.user_value, "even newer test") # test changing the chained object new_user_object = UserObject(user_value="new object") with self.assertTraitChanges(editor, "object", count=1): self.change_user_value( editor, context["object"], "user_auxiliary", new_user_object ) self.assertEqual(editor.value, "new object") self.assertIs(editor.object, new_user_object) self.assertEqual(editor.object.user_value, "new object") editor.dispose() def test_factory_sync_simple(self): factory = StubEditorFactory(auxiliary_value="test") editor = create_editor(factory=factory) editor.prepare(None) # preparation copies the auxiliary value from the factory self.assertIs(editor.auxiliary_value, "test") editor.dispose() def test_factory_sync_cv_simple(self): factory = StubEditorFactory() editor = create_editor(factory=factory) editor.prepare(None) # preparation copies the auxiliary CV int value from the factory self.assertIs(editor.auxiliary_cv_int, 0) editor.dispose() def test_parse_extended_name(self): context = { "object": UserObject( user_auxiliary=UserObject(user_value="other_test") ), "other_object": UserObject(user_value="another_test"), } editor = create_editor(context=context) editor.prepare(None) # test simple name object, name, getter = editor.parse_extended_name("user_value") value = getter() self.assertIs(object, context["object"]) self.assertEqual(name, "user_value") self.assertEqual(value, "test") # test different context object name object, name, getter = editor.parse_extended_name( "other_object.user_value" ) value = getter() self.assertIs(object, context["other_object"]) self.assertEqual(name, "user_value") self.assertEqual(value, "another_test") # test chained name object, name, getter = editor.parse_extended_name( "object.user_auxiliary.user_value" ) value = getter() self.assertIs(object, context["object"]) self.assertEqual(name, "user_auxiliary.user_value") self.assertEqual(value, "other_test") editor.dispose() def test_tooltip_default(self): context = { "object": UserObject(), } editor = create_editor(context=context) editor.prepare(None) # test tooltip text try: self.assertIsNone(editor.control.tooltip) tooltip_text = editor.tooltip_text() self.assertIsNone(tooltip_text) set_tooltip_result = editor.set_tooltip() self.assertFalse(set_tooltip_result) except Exception: editor.dispose() raise def test_tooltip_from_description(self): context = { "object": UserObject(), } editor = create_editor(context=context, description="a tooltip") editor.prepare(None) # test tooltip text try: self.assertEqual(editor.control.tooltip, "a tooltip") tooltip_text = editor.tooltip_text() self.assertEqual(tooltip_text, "a tooltip") set_tooltip_result = editor.set_tooltip() self.assertTrue(set_tooltip_result) except Exception: editor.dispose() raise def test_tooltip_text_with_tooltip(self): context = { "object": UserObject(), } editor = create_editor(context=context, name='user_tooltip') editor.prepare(None) # test tooltip text try: self.assertEqual(editor.control.tooltip, "a tooltip") tooltip_text = editor.tooltip_text() self.assertEqual(tooltip_text, "a tooltip") set_tooltip_result = editor.set_tooltip() self.assertTrue(set_tooltip_result) except Exception: editor.dispose() raise def test_tooltip_text_with_desc(self): context = { "object": UserObject(), } editor = create_editor(context=context, name='user_desc') editor.prepare(None) # test tooltip text try: self.assertEqual( editor.control.tooltip, "Specifies a trait with desc metadata", ) tooltip_text = editor.tooltip_text() self.assertEqual( tooltip_text, "Specifies a trait with desc metadata", ) set_tooltip_result = editor.set_tooltip() self.assertTrue(set_tooltip_result) except Exception: editor.dispose() raise def test_tooltip_other_control(self): context = { "object": UserObject(), } editor = create_editor(context=context, description="a tooltip") editor.prepare(None) # test tooltip text try: other_control = FakeControl() set_tooltip_result = editor.set_tooltip(other_control) self.assertTrue(set_tooltip_result) self.assertEqual(other_control.tooltip, "a tooltip") except Exception: editor.dispose() raise # Test synchronizing built-in trait values between factory # and editor. def test_factory_sync_invalid_state(self): # Test when object's trait that sets the invalid state changes, # the invalid state on the editor changes factory = StubEditorFactory(invalid="invalid_state") user_object = UserObject(invalid_state=False) context = { "object": user_object, } editor = create_editor(context=context, factory=factory) editor.prepare(None) self.addCleanup(editor.dispose) with self.assertTraitChanges(editor, "invalid", count=1): user_object.invalid_state = True self.assertTrue(editor.invalid) with self.assertTraitChanges(editor, "invalid", count=1): user_object.invalid_state = False self.assertFalse(editor.invalid) # Testing sync_value "from" --------------------------------------------- def test_sync_value_from(self): editor = create_editor() user_object = editor.object editor.prepare(None) with self.assertTraitChanges(editor, "auxiliary_value", count=1): editor.sync_value( "object.user_auxiliary", "auxiliary_value", "from" ) self.assertEqual(editor.auxiliary_value, 10) with self.assertTraitChanges(editor, "auxiliary_value", count=1): user_object.user_auxiliary = 11 self.assertEqual(editor.auxiliary_value, 11) editor.dispose() with self.assertTraitDoesNotChange(editor, "auxiliary_value"): user_object.user_auxiliary = 12 def test_sync_value_from_object(self): editor = create_editor() user_object = editor.object editor.prepare(None) with self.assertTraitChanges(editor, "auxiliary_value", count=1): editor.sync_value("user_auxiliary", "auxiliary_value", "from") self.assertEqual(editor.auxiliary_value, 10) with self.assertTraitChanges(editor, "auxiliary_value", count=1): user_object.user_auxiliary = 11 self.assertEqual(editor.auxiliary_value, 11) editor.dispose() with self.assertTraitDoesNotChange(editor, "auxiliary_value"): user_object.user_auxiliary = 12 def test_sync_value_from_context(self): # set up the editor user_object = UserObject() other_object = UserObject(user_auxiliary=20) context = {"object": user_object, "other_object": other_object} editor = create_editor(context=context) editor.prepare(None) with self.assertTraitChanges(editor, "auxiliary_value", count=1): editor.sync_value( "other_object.user_auxiliary", "auxiliary_value", "from" ) self.assertEqual(editor.auxiliary_value, 20) with self.assertTraitChanges(editor, "auxiliary_value", count=1): other_object.user_auxiliary = 11 self.assertEqual(editor.auxiliary_value, 11) editor.dispose() with self.assertTraitDoesNotChange(editor, "auxiliary_value"): other_object.user_auxiliary = 12 def test_sync_value_from_chained(self): # set up the editor user_object = UserObject(user_auxiliary=UserObject(user_value=20)) context = {"object": user_object} editor = create_editor(context=context) editor.prepare(None) with self.assertTraitChanges(editor, "auxiliary_value", count=1): editor.sync_value( "object.user_auxiliary.user_value", "auxiliary_value", "from" ) self.assertEqual(editor.auxiliary_value, 20) with self.assertTraitChanges(editor, "auxiliary_value", count=1): user_object.user_auxiliary.user_value = 11 self.assertEqual(editor.auxiliary_value, 11) with self.assertTraitChanges(editor, "auxiliary_value", count=1): user_object.user_auxiliary = UserObject(user_value=12) self.assertEqual(editor.auxiliary_value, 12) editor.dispose() with self.assertTraitDoesNotChange(editor, "auxiliary_value"): user_object.user_auxiliary.user_value = 13 def test_sync_value_from_list(self): editor = create_editor() user_object = editor.object editor.prepare(None) with self.assertTraitChanges(editor, "auxiliary_list", count=1): editor.sync_value( "object.user_list", "auxiliary_list", "from", is_list=True ) self.assertEqual(editor.auxiliary_list, ["one", "two", "three"]) with self.assertTraitChanges(editor, "auxiliary_list", count=1): user_object.user_list = ["one", "two"] self.assertEqual(editor.auxiliary_list, ["one", "two"]) with self.assertTraitChanges(editor, "auxiliary_list_items", count=1): user_object.user_list[1:] = ["four", "five"] self.assertEqual(editor.auxiliary_list, ["one", "four", "five"]) editor.dispose() with self.assertTraitDoesNotChange(editor, "auxiliary_list"): user_object.user_list = ["one", "two", "three"] def test_sync_value_from_event(self): editor = create_editor() user_object = editor.object editor.prepare(None) with self.assertTraitDoesNotChange(editor, "auxiliary_event"): editor.sync_value( "object.user_event", "auxiliary_event", "from", is_event=True ) with self.assertTraitChanges(editor, "auxiliary_event", count=1): user_object.user_event = True editor.dispose() with self.assertTraitDoesNotChange(editor, "auxiliary_event"): user_object.user_event = True def test_sync_value_from_cv(self): factory = StubEditorFactory( auxiliary_cv_int=ContextValue("object.user_auxiliary") ) editor = create_editor(factory=factory) user_object = editor.object with self.assertTraitChanges(editor, "auxiliary_cv_int", count=1): editor.prepare(None) self.assertEqual(editor.auxiliary_cv_int, 10) with self.assertTraitChanges(editor, "auxiliary_cv_int", count=1): user_object.user_auxiliary = 11 self.assertEqual(editor.auxiliary_cv_int, 11) editor.dispose() with self.assertTraitDoesNotChange(editor, "auxiliary_cv_int"): user_object.user_auxiliary = 12 # Testing sync_value "to" ----------------------------------------------- def test_sync_value_to(self): editor = create_editor() user_object = editor.object editor.prepare(None) editor.auxiliary_value = 20 with self.assertTraitChanges(user_object, "user_auxiliary", count=1): editor.sync_value("object.user_auxiliary", "auxiliary_value", "to") self.assertEqual(user_object.user_auxiliary, 20) with self.assertTraitChanges(user_object, "user_auxiliary", count=1): editor.auxiliary_value = 11 self.assertEqual(user_object.user_auxiliary, 11) editor.dispose() with self.assertTraitDoesNotChange(user_object, "user_auxiliary"): editor.auxiliary_value = 12 def test_sync_value_to_object(self): editor = create_editor() user_object = editor.object editor.prepare(None) editor.auxiliary_value = 20 with self.assertTraitChanges(user_object, "user_auxiliary", count=1): editor.sync_value("user_auxiliary", "auxiliary_value", "to") self.assertEqual(user_object.user_auxiliary, 20) with self.assertTraitChanges(user_object, "user_auxiliary", count=1): editor.auxiliary_value = 11 self.assertEqual(user_object.user_auxiliary, 11) editor.dispose() with self.assertTraitDoesNotChange(user_object, "user_auxiliary"): editor.auxiliary_value = 12 def test_sync_value_to_context(self): # set up the editor user_object = UserObject() other_object = UserObject() context = {"object": user_object, "other_object": other_object} editor = create_editor(context=context) editor.prepare(None) editor.auxiliary_value = 20 with self.assertTraitChanges(other_object, "user_auxiliary", count=1): editor.sync_value( "other_object.user_auxiliary", "auxiliary_value", "to" ) self.assertEqual(other_object.user_auxiliary, 20) with self.assertTraitChanges(other_object, "user_auxiliary", count=1): editor.auxiliary_value = 11 self.assertEqual(other_object.user_auxiliary, 11) editor.dispose() with self.assertTraitDoesNotChange(other_object, "user_auxiliary"): editor.auxiliary_value = 12 def test_sync_value_to_chained(self): user_object = UserObject(user_auxiliary=UserObject()) context = {"object": user_object} editor = create_editor(context=context) editor.prepare(None) editor.auxiliary_value = 20 with self.assertTraitChanges( user_object.user_auxiliary, "user_value", count=1 ): editor.sync_value( "object.user_auxiliary.user_value", "auxiliary_value", "to" ) self.assertEqual(user_object.user_auxiliary.user_value, 20) with self.assertTraitChanges( user_object.user_auxiliary, "user_value", count=1 ): editor.auxiliary_value = 11 self.assertEqual(user_object.user_auxiliary.user_value, 11) editor.dispose() with self.assertTraitDoesNotChange( user_object.user_auxiliary, "user_value" ): editor.auxiliary_value = 12 def test_sync_value_to_list(self): editor = create_editor() user_object = editor.object editor.prepare(None) with self.assertTraitChanges(user_object, "user_list", count=1): editor.sync_value( "object.user_list", "auxiliary_list", "to", is_list=True ) self.assertEqual(user_object.user_list, []) with self.assertTraitChanges(user_object, "user_list", count=1): editor.auxiliary_list = ["one", "two"] self.assertEqual(user_object.user_list, ["one", "two"]) with self.assertTraitChanges(user_object, "user_list_items", count=1): editor.auxiliary_list[1:] = ["four", "five"] self.assertEqual(user_object.user_list, ["one", "four", "five"]) editor.dispose() with self.assertTraitDoesNotChange(user_object, "user_list"): editor.auxiliary_list = ["one", "two", "three"] def test_sync_value_to_event(self): editor = create_editor() user_object = editor.object editor.prepare(None) with self.assertTraitDoesNotChange(user_object, "user_event"): editor.sync_value( "object.user_event", "auxiliary_event", "to", is_event=True ) with self.assertTraitChanges(user_object, "user_event", count=1): editor.auxiliary_event = True editor.dispose() with self.assertTraitDoesNotChange(user_object, "user_event"): editor.auxiliary_event = True def test_sync_value_to_cv(self): factory = StubEditorFactory( auxiliary_cv_float=ContextValue("object.user_auxiliary") ) editor = create_editor(factory=factory) user_object = editor.object editor.auxiliary_cv_float = 20.0 with self.assertTraitChanges(user_object, "user_auxiliary", count=1): editor.prepare(None) self.assertEqual(user_object.user_auxiliary, 20) with self.assertTraitChanges(user_object, "user_auxiliary", count=1): editor.auxiliary_cv_float = 11.0 self.assertEqual(user_object.user_auxiliary, 11) editor.dispose() with self.assertTraitDoesNotChange(user_object, "user_auxiliary"): editor.auxiliary_cv_float = 12.0 # Testing sync_value "both" ----------------------------------------------- def test_sync_value_both(self): editor = create_editor() user_object = editor.object editor.prepare(None) with self.assertTraitChanges(editor, "auxiliary_value", count=1): editor.sync_value( "object.user_auxiliary", "auxiliary_value", "both" ) self.assertEqual(editor.auxiliary_value, 10) with self.assertTraitChanges(editor, "auxiliary_value", count=1): user_object.user_auxiliary = 11 self.assertEqual(editor.auxiliary_value, 11) with self.assertTraitChanges(user_object, "user_auxiliary", count=1): editor.auxiliary_value = 12 self.assertEqual(user_object.user_auxiliary, 12) editor.dispose() with self.assertTraitDoesNotChange(editor, "auxiliary_value"): user_object.user_auxiliary = 13 with self.assertTraitDoesNotChange(user_object, "user_auxiliary"): editor.auxiliary_value = 14 # regression test for enthought/traitsui#1543 @requires_toolkit([ToolkitName.qt]) @unittest.skipIf(no_modal_dialog_tester, "ModalDialogTester unavailable") def test_editor_error_msg(self): from pyface.qt import QtCore, QtGui class Foo(HasTraits): x = Range(low=0.0, high=1.0, value=0.5, exclude_low=True) foo = Foo() tester = UITester(auto_process_events=False) with tester.create_ui(foo) as ui: x_range = tester.find_by_name(ui, "x") x_range_textbox = x_range.locate(Textbox()) x_range_textbox.perform(KeyClick('Backspace')) x_range_textbox.perform(KeySequence('0')) def trigger_error(): x_range_textbox.perform(KeyClick('Enter')) def check_and_close(mdtester): try: with mdtester.capture_error(): self.assertTrue( mdtester.has_widget( text="The 'x' trait of a Foo instance must be " "0.0 < a floating point number <= 1.0, " "but a value of 0.0 was " "specified.", type_=QtGui.QMessageBox, ) ) self.assertEqual( mdtester.get_dialog_widget().textFormat(), QtCore.Qt.TextFormat.PlainText, ) finally: mdtester.close(accept=True) self.assertTrue(mdtester.dialog_was_opened) mdtester = ModalDialogTester(trigger_error) mdtester.open_and_run(check_and_close) self.assertTrue(mdtester.dialog_was_opened) def test_get_control_widget(self): user_object = UserObject(user_auxiliary=UserObject()) context = {"object": user_object} editor = create_editor(context=context) editor.prepare(None) control = editor.get_control_widget() try: self.assertIsNotNone(control) finally: editor.dispose() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/tests/test_editors_imports.py0000644000175100001730000000124300000000000024032 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! import unittest class TestEditorsImports(unittest.TestCase): def test_editors_import_warns(self): # Importing from traitsui.editors is deprecated with self.assertWarns(DeprecationWarning): from traitsui.editors import BooleanEditor ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/tests/test_handler.py0000644000175100001730000002073500000000000022230 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! from unittest import TestCase, skipIf from pyface.action.api import ActionEvent from traits.api import Any, HasTraits, Bool, TraitError from traitsui.api import ( Action, CloseAction, Handler, HelpAction, RedoAction, RevertAction, UI, UndoAction, ) from traitsui.tests._tools import BaseTestMixin, is_null class PyfaceAction(Action): name = "Test Action" performed = Bool() def perform(self, event): self.performed = True class TraitsUIAction(Action): name = "Test Action" performed = Bool() def perform(self): self.performed = True class SampleHandler(Handler): action_performed = Bool() info_action_performed = Bool() click_performed = Bool() undo_performed = Bool() redo_performed = Bool() revert_performed = Bool() apply_performed = Bool() close_performed = Bool() help_performed = Bool() init_return_value = Any(True) def init(self, info): return self.init_return_value def action_handler(self): self.action_performed = True def info_action_handler(self, info): self.info_action_performed = True def revert(self, info): self.revert_perfomed = True def apply(self, info): self.apply_perfomed = True def show_help(self, info, control=None): self.help_performed = True def _action_clicked(self): self.click_performed = True def _on_undo(self, info): super()._on_undo(info) self.undo_performed = True def _on_redo(self, info): super()._on_redo(info) self.redo_performed = True def _on_revert(self, info): super()._on_revert(info) self.revert_performed = True def _on_close(self, info): super()._on_close(info) self.close_performed = True class SampleObject(HasTraits): object_action_performed = Bool() action_performed = Bool() info_action_performed = Bool() click_performed = Bool() def object_action_handler(self): self.object_action_performed = True def action_handler(self): self.action_performed = True def info_action_handler(self, info): self.info_action_performed = True def _action_click(self): self.click_performed = True class TestHandler(BaseTestMixin, TestCase): def setUp(self): BaseTestMixin.setUp(self) def tearDown(self): BaseTestMixin.tearDown(self) def test_perform_pyface_action(self): object = SampleObject() handler = SampleHandler() action = PyfaceAction() event = ActionEvent() ui = UI(handler=handler, context={"object": object}) info = ui.info handler.perform(info, action, event) self.assertTrue(action.performed) def test_perform_traitsui_action(self): object = SampleObject() handler = SampleHandler() action = TraitsUIAction() event = ActionEvent() ui = UI(handler=handler, context={"object": object}) info = ui.info handler.perform(info, action, event) self.assertTrue(action.performed) self.assertFalse(handler.action_performed) self.assertFalse(handler.info_action_performed) self.assertFalse(handler.click_performed) self.assertFalse(object.action_performed) self.assertFalse(object.info_action_performed) self.assertFalse(object.click_performed) def test_perform_action_handler(self): object = SampleObject() handler = SampleHandler() action = TraitsUIAction(name="action", action="action_handler") event = ActionEvent() ui = UI(handler=handler, context={"object": object}) info = ui.info handler.perform(info, action, event) self.assertTrue(handler.action_performed) self.assertFalse(handler.info_action_performed) self.assertFalse(handler.click_performed) self.assertFalse(object.action_performed) self.assertFalse(object.info_action_performed) self.assertFalse(object.click_performed) self.assertFalse(action.performed) def test_perform_info_action_handler(self): object = SampleObject() handler = SampleHandler() action = TraitsUIAction(name="action", action="info_action_handler") event = ActionEvent() ui = UI(handler=handler, context={"object": object}) info = ui.info handler.perform(info, action, event) self.assertTrue(handler.info_action_performed) self.assertFalse(handler.action_performed) self.assertFalse(handler.click_performed) self.assertFalse(object.action_performed) self.assertFalse(object.info_action_performed) self.assertFalse(object.click_performed) self.assertFalse(action.performed) def test_perform_click_handler(self): object = SampleObject() handler = SampleHandler() action = TraitsUIAction(name="action", action="") event = ActionEvent() ui = UI(handler=handler, context={"object": object}) info = ui.info handler.perform(info, action, event) self.assertTrue(handler.click_performed) self.assertFalse(handler.action_performed) self.assertFalse(handler.info_action_performed) self.assertFalse(object.action_performed) self.assertFalse(object.info_action_performed) self.assertFalse(object.click_performed) self.assertFalse(action.performed) def test_perform_object_handler(self): object = SampleObject() handler = SampleHandler() action = TraitsUIAction(name="action", action="object_action_handler") event = ActionEvent() ui = UI(handler=handler, context={"object": object}) info = ui.info handler.perform(info, action, event) self.assertTrue(object.object_action_performed) self.assertFalse(action.performed) def test_undo_handler(self): object = SampleObject() handler = SampleHandler() action = UndoAction event = ActionEvent() ui = UI(handler=handler, context={"object": object}) info = ui.info handler.perform(info, action, event) self.assertTrue(handler.undo_performed) def test_redo_handler(self): object = SampleObject() handler = SampleHandler() action = RedoAction event = ActionEvent() ui = UI(handler=handler, context={"object": object}) info = ui.info handler.perform(info, action, event) self.assertTrue(handler.redo_performed) def test_revert_handler(self): object = SampleObject() handler = SampleHandler() action = RevertAction event = ActionEvent() ui = UI(handler=handler, context={"object": object}) info = ui.info handler.perform(info, action, event) self.assertTrue(handler.revert_performed) def test_close_handler(self): object = SampleObject() handler = SampleHandler() action = CloseAction event = ActionEvent() ui = UI(handler=handler, context={"object": object}) info = ui.info handler.perform(info, action, event) self.assertTrue(handler.close_performed) def test_help_handler(self): object = SampleObject() handler = SampleHandler() action = HelpAction event = ActionEvent() ui = UI(handler=handler, context={"object": object}) info = ui.info handler.perform(info, action, event) self.assertTrue(handler.help_performed) @skipIf(is_null(), "Null toolkit can't create UI") def test_handler_init_false(self): object = SampleObject() handler = SampleHandler(init_return_value=False) with self.assertRaises(TraitError): object.edit_traits(handler=handler) @skipIf(is_null(), "Null toolkit can't create UI") def test_handler_init_none(self): object = SampleObject() handler = SampleHandler(init_return_value=None) with self.assertRaises(ValueError): object.edit_traits(handler=handler) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/tests/test_helper.py0000644000175100001730000000641400000000000022070 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! from unittest import TestCase from traitsui.helper import compute_column_widths from traitsui.tests._tools import BaseTestMixin class TestComputeColumnWidths(BaseTestMixin, TestCase): def setUp(self): BaseTestMixin.setUp(self) def tearDown(self): BaseTestMixin.tearDown(self) def test_all_default(self): available_space = 200 requested = [-1, -1, -1, -1] widths = compute_column_widths(available_space, requested, None, None) self.assertEqual(widths, [50, 50, 50, 50]) def test_all_fixed(self): available_space = 200 requested = [10, 50, 40, 20] widths = compute_column_widths(available_space, requested, None, None) self.assertEqual(widths, [10, 50, 40, 20]) def test_all_fixed_too_wide(self): available_space = 100 requested = [10, 50, 40, 20] widths = compute_column_widths(available_space, requested, None, None) self.assertEqual(widths, [10, 50, 40, 20]) def test_all_weighted(self): available_space = 200 requested = [0.3, 0.2, 0.25, 0.25] widths = compute_column_widths(available_space, requested, None, None) self.assertEqual(widths, [60, 40, 50, 50]) def test_all_weighted_default_min(self): available_space = 200 requested = [0.4, 0.1, 0.1, 0.4] widths = compute_column_widths(available_space, requested, None, None) self.assertEqual(widths, [70, 30, 30, 70]) def test_mixed(self): available_space = 200 requested = [0.5, 50, 25, 0.75] widths = compute_column_widths(available_space, requested, None, None) self.assertEqual(widths, [50, 50, 25, 75]) def test_mixed_too_wide(self): available_space = 100 requested = [0.5, 50, 25, 0.75] widths = compute_column_widths(available_space, requested, None, None) self.assertEqual(widths, [30, 50, 25, 30]) def test_user_widths(self): available_space = 225 requested = [0.5, 50, 25, 0.75] user_widths = [None, None, 50, None] widths = compute_column_widths( available_space, requested, None, user_widths ) self.assertEqual(widths, [50, 50, 50, 75]) def test_min_widths(self): available_space = 225 requested = [0.5, 50, 0.25, 0.75] min_widths = [30, 100, 50, 30] widths = compute_column_widths( available_space, requested, min_widths, None ) self.assertEqual(widths, [50, 50, 50, 75]) def test_user_and_min_widths(self): available_space = 200 requested = [0.5, 50, 0.25, 0.75] min_widths = [30, 100, 50, 30] user_widths = [None, 75, 25, 50] widths = compute_column_widths( available_space, requested, min_widths, user_widths ) self.assertEqual(widths, [50, 75, 25, 50]) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/tests/test_key_bindings.py0000644000175100001730000002254100000000000023255 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! import unittest from unittest.mock import Mock from traits.testing.api import UnittestTools from traitsui.key_bindings import KeyBinding, KeyBindings, KeyBindingsHandler class Controller1: def __init__(self): self.called = None def m1(self, *args): self.called = ("m1", args) class Controller2(Controller1): def m2(self, *args): self.called = ("m2", args) class TestKeyBinding(UnittestTools, unittest.TestCase): def test_clear_binding_no_match(self): key_binding = KeyBinding(binding1="Ctrl-S", binding2="Ctrl-T") with self.assertTraitDoesNotChange(key_binding, "binding1"): with self.assertTraitDoesNotChange(key_binding, "binding2"): key_binding.clear_binding("Ctrl-U") def test_clear_binding_match_binding2(self): key_binding = KeyBinding(binding1="Ctrl-S", binding2="Ctrl-T") with self.assertTraitDoesNotChange(key_binding, "binding1"): with self.assertTraitChanges(key_binding, "binding2"): key_binding.clear_binding("Ctrl-T") self.assertEqual(key_binding.binding2, "") def test_clear_binding_match_binding1(self): key_binding = KeyBinding(binding1="Ctrl-S", binding2="Ctrl-T") with self.assertTraitChanges(key_binding, "binding1"): with self.assertTraitChanges(key_binding, "binding2"): key_binding.clear_binding("Ctrl-S") self.assertEqual(key_binding.binding1, "Ctrl-T") self.assertEqual(key_binding.binding2, "") def test_match(self): key_binding = KeyBinding(binding1="Ctrl-S", binding2="Ctrl-T") self.assertTrue(key_binding.match("Ctrl-S")) self.assertTrue(key_binding.match("Ctrl-T")) self.assertFalse(key_binding.match("Ctrl-U")) class TestKeyBindings(UnittestTools, unittest.TestCase): def test_init(self): bindings = [ KeyBinding(binding1="Ctrl-S", binding2="Ctrl-T"), KeyBinding(binding1="Ctrl-U", binding2=""), ] key_bindings = KeyBindings(bindings) self.assertEqual(key_bindings.bindings, bindings) def test_init_args(self): bindings = [ KeyBinding(binding1="Ctrl-S", binding2="Ctrl-T"), KeyBinding(binding1="Ctrl-U", binding2=""), ] key_bindings = KeyBindings(*bindings) self.assertEqual(key_bindings.bindings, bindings) def test_init_kwargs(self): bindings = [ KeyBinding(binding1="Ctrl-S", binding2="Ctrl-T"), KeyBinding(binding1="Ctrl-U", binding2=""), ] key_bindings = KeyBindings(bindings=bindings) self.assertEqual(key_bindings.bindings, bindings) def test_merge(self): bindings = [ KeyBinding(binding1="Ctrl-S", binding2="Ctrl-T", method_name="m1"), KeyBinding(binding1="Ctrl-U", binding2="", method_name="m2"), ] key_bindings_1 = KeyBindings(*bindings) key_bindings_2 = KeyBindings( KeyBinding(binding1="Ctrl-V", binding2="Ctrl-W", method_name="m2"), KeyBinding(binding1="Ctrl-S", binding2="", method_name="m3"), ) key_bindings_1.merge(key_bindings_2) self.assertEqual(key_bindings_1.bindings, bindings) key_binding = bindings[1] self.assertEqual(key_binding.binding1, "Ctrl-V") self.assertEqual(key_binding.binding2, "Ctrl-W") def test_clear_bindings_match(self): bindings = [ KeyBinding(binding1="Ctrl-S", binding2="Ctrl-T", method_name="m1"), KeyBinding(binding1="Ctrl-U", binding2="", method_name="m2"), ] key_bindings = KeyBindings(bindings=bindings) with self.assertTraitChanges(bindings[0], "binding2"): bindings[1].binding2 = "Ctrl-T" self.assertIs(bindings[0].binding2, "") def test_clear_bindings_no_match(self): bindings = [ KeyBinding(binding1="Ctrl-S", binding2="Ctrl-T", method_name="m1"), KeyBinding(binding1="Ctrl-U", binding2="", method_name="m2"), ] key_bindings = KeyBindings(bindings=bindings) with self.assertTraitDoesNotChange(bindings[0], "binding1"): with self.assertTraitDoesNotChange(bindings[0], "binding2"): bindings[1].binding2 = "Ctrl-V" def test_do_match(self): bindings = [ KeyBinding(binding1="Ctrl-S", binding2="Ctrl-T", method_name="m1"), KeyBinding(binding1="Ctrl-U", binding2="", method_name="m2"), ] key_bindings = KeyBindings(bindings=bindings) controller = Controller1() # test _do rather than do since it doesn't need a toolkit event result = key_bindings._do("Ctrl-T", [controller], ("args",), False) self.assertTrue(result) self.assertEqual(controller.called, ("m1", ("args",))) def test_do_match_first(self): bindings = [ KeyBinding(binding1="Ctrl-S", binding2="Ctrl-T", method_name="m1"), KeyBinding(binding1="Ctrl-U", binding2="", method_name="m2"), ] key_bindings = KeyBindings(bindings=bindings) controller_1 = Controller1() controller_2 = Controller2() # test _do rather than do since it doesn't need a toolkit event result = key_bindings._do( "Ctrl-T", [controller_1, controller_2], ("args",), False, ) self.assertTrue(result) self.assertEqual(controller_1.called, ("m1", ("args",))) def test_do_match_second(self): bindings = [ KeyBinding(binding1="Ctrl-S", binding2="Ctrl-T", method_name="m1"), KeyBinding(binding1="Ctrl-U", binding2="", method_name="m2"), ] key_bindings = KeyBindings(bindings=bindings) controller_1 = Controller1() controller_2 = Controller2() # test _do rather than do since it doesn't need a toolkit event result = key_bindings._do( "Ctrl-U", [controller_1, controller_2], ("args",), False, ) self.assertTrue(result) self.assertEqual(controller_2.called, ("m2", ("args",))) def test_do_no_match(self): bindings = [ KeyBinding(binding1="Ctrl-S", binding2="Ctrl-T", method_name="m1"), KeyBinding(binding1="Ctrl-U", binding2="", method_name="m2"), ] key_bindings = KeyBindings(bindings=bindings) controller = Controller1() # test _do rather than do since it doesn't need a toolkit event result = key_bindings._do("Ctrl-U", [controller], ("args",), False) self.assertFalse(result) self.assertIsNone(controller.called) def test_do_no_match_complete(self): bindings = [ KeyBinding(binding1="Ctrl-S", binding2="Ctrl-T", method_name="m1"), KeyBinding(binding1="Ctrl-U", binding2="", method_name="m2"), ] key_bindings = KeyBindings(bindings=bindings) controller = Controller1() # test _do rather than do since it doesn't need a toolkit event result = key_bindings._do("Ctrl-V", [controller], ("args",), False) self.assertFalse(result) self.assertIsNone(controller.called) class TestKeyBindingsHandler(unittest.TestCase): def test_key_binding_for_match(self): bindings = [ KeyBinding(binding1="Ctrl-S", binding2="Ctrl-T", method_name="m1"), KeyBinding(binding1="Ctrl-U", binding2="", method_name="m2"), ] key_bindings = KeyBindings(bindings=bindings) handler = KeyBindingsHandler(model=key_bindings) binding = handler.key_binding_for(bindings[0], "Ctrl-U") self.assertIs(binding, bindings[1]) def test_key_binding_for_no_match(self): bindings = [ KeyBinding(binding1="Ctrl-S", binding2="Ctrl-T", method_name="m1"), KeyBinding(binding1="Ctrl-U", binding2="", method_name="m2"), ] key_bindings = KeyBindings(bindings=bindings) handler = KeyBindingsHandler(model=key_bindings) binding = handler.key_binding_for(bindings[0], "Ctrl-V") self.assertIsNone(binding) def test_key_binding_for_match_self(self): bindings = [ KeyBinding(binding1="Ctrl-S", binding2="Ctrl-T", method_name="m1"), KeyBinding(binding1="Ctrl-U", binding2="", method_name="m2"), ] key_bindings = KeyBindings(bindings=bindings) handler = KeyBindingsHandler(model=key_bindings) binding = handler.key_binding_for(bindings[0], "Ctrl-S") self.assertIsNone(binding) def test_key_binding_for_match_empty(self): bindings = [ KeyBinding(binding1="Ctrl-S", binding2="Ctrl-T", method_name="m1"), KeyBinding(binding1="Ctrl-U", binding2="", method_name="m2"), ] key_bindings = KeyBindings(bindings=bindings) handler = KeyBindingsHandler(model=key_bindings) binding = handler.key_binding_for(bindings[0], "") self.assertIsNone(binding) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/tests/test_labels.py0000644000175100001730000001752700000000000022062 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Test the creation and layout of labels. """ import unittest from traits.has_traits import HasTraits from traits.trait_types import Bool, Str from traitsui.view import View from traitsui.item import Item from traitsui.group import VGroup, HGroup from traitsui.tests._tools import ( BaseTestMixin, create_ui, is_control_enabled, is_qt, requires_toolkit, reraise_exceptions, ToolkitName, ) _DIALOG_WIDTH = 500 class ShowRightLabelsDialog(HasTraits): """Dialog with labels on the left/right to test the label text.""" bool_item = Bool(True) traits_view = View( VGroup( VGroup(Item("bool_item"), show_left=False), VGroup(Item("bool_item"), show_left=True), ) ) class HResizeTestDialog(HasTraits): """Dialog with checkbox and text elements and labels on the right. We test the separation between element and label in HGroups. """ bool_item = Bool(True) txt_item = Str() traits_view = View( VGroup( HGroup(Item("bool_item", springy=True), show_left=False), VGroup(Item("txt_item", resizable=True), show_left=False), ), width=_DIALOG_WIDTH, height=100, resizable=True, ) class VResizeTestDialog(HasTraits): """Dialog with checkbox and text elements and labels on the right. We test the separation between element and label in VGroups. """ bool_item = Bool(True) txt_item = Str() traits_view = View( VGroup( VGroup(Item("bool_item", resizable=True), show_left=False), VGroup(Item("txt_item", resizable=True), show_left=False), ), width=_DIALOG_WIDTH, height=100, resizable=True, ) class NoLabelResizeTestDialog(HasTraits): """Test the combination show_label=False, show_left=False.""" bool_item = Bool(True) traits_view = View( VGroup( Item("bool_item", resizable=True, show_label=False), show_left=False, ), resizable=True, ) class EnableWhenDialog(HasTraits): """Test labels for enable when.""" bool_item = Bool(True) labelled_item = Str("test") unlabelled_item = Str("test") traits_view = View( VGroup( Item("bool_item"), Item("labelled_item", enabled_when="bool_item"), Item( "unlabelled_item", enabled_when="bool_item", show_label=False ), ), resizable=True, ) class TestLabels(BaseTestMixin, unittest.TestCase): def setUp(self): BaseTestMixin.setUp(self) def tearDown(self): BaseTestMixin.tearDown(self) @requires_toolkit([ToolkitName.qt]) def test_qt_show_labels_right_without_colon(self): # Behavior: traitsui should not append a colon ':' to labels # that are shown to the *right* of the corresponding elements from pyface import qt dialog = ShowRightLabelsDialog() with reraise_exceptions(), create_ui(dialog) as ui: # get reference to label objects labels = ui.control.findChildren(qt.QtGui.QLabel) # the first is shown to the right, so no colon self.assertFalse(labels[0].text().endswith(":")) # the second is shown to the right, it should have a colon self.assertTrue(labels[1].text().endswith(":")) def _test_qt_labels_right_resizing(self, dialog_class): # Bug: In the Qt backend, resizing a checkbox element with a label on # the right resizes the checkbox, even though it cannot be. # The final effect is that the label remains attached to the right # margin, with a big gap between it and the checkbox. In this case, the # label should be made resizable instead. # On the other hand, a text element should keep the current behavior # and resize. from pyface import qt with reraise_exceptions(), create_ui(dialog_class()) as ui: # all labels labels = ui.control.findChildren(qt.QtGui.QLabel) # the checkbox and its label should be close to one another; the # size of the checkbox should be small checkbox_label = labels[0] checkbox = ui.control.findChild(qt.QtGui.QCheckBox) # horizontal space between checkbox and label should be small h_space = checkbox_label.x() - checkbox.x() self.assertLess(h_space, 100) # and the checkbox size should also be small self.assertLess(checkbox.width(), 100) # the text item and its label should be close to one another; the # size of the text item should be large text_label = labels[0] text = ui.control.findChild(qt.QtGui.QLineEdit) # horizontal space between text and label should be small h_space = text_label.x() - text.x() self.assertLess(h_space, 100) # and the text item size should be large self.assertGreater(text.width(), _DIALOG_WIDTH - 200) # the size of the window should still be 500 self.assertEqual(ui.control.width(), _DIALOG_WIDTH) @requires_toolkit([ToolkitName.qt]) def test_qt_labels_right_resizing_vertical(self): self._test_qt_labels_right_resizing(VResizeTestDialog) @requires_toolkit([ToolkitName.qt]) def test_qt_labels_right_resizing_horizontal(self): self._test_qt_labels_right_resizing(HResizeTestDialog) @requires_toolkit([ToolkitName.qt, ToolkitName.wx]) def test_labels_enabled_when(self): # Behaviour: label should enable/disable along with editor dialog = EnableWhenDialog() with reraise_exceptions(), create_ui(dialog) as ui: labelled_editor = ui.get_editors("labelled_item")[0] if is_qt(): unlabelled_editor = ui.get_editors("unlabelled_item")[0] self.assertIsNone(unlabelled_editor.label_control) self.assertTrue(is_control_enabled(labelled_editor.label_control)) dialog.bool_item = False self.assertFalse(is_control_enabled(labelled_editor.label_control)) dialog.bool_item = True @requires_toolkit([ToolkitName.qt, ToolkitName.wx]) class TestAnyToolkit(BaseTestMixin, unittest.TestCase): """Toolkit-agnostic tests for labels with different orientations.""" def setUp(self): BaseTestMixin.setUp(self) def tearDown(self): BaseTestMixin.tearDown(self) def test_group_show_right_labels(self): with reraise_exceptions(), create_ui(ShowRightLabelsDialog()): pass def test_horizontal_resizable_and_labels(self): with reraise_exceptions(), create_ui(HResizeTestDialog()): pass def test_all_resizable_with_labels(self): with reraise_exceptions(), create_ui(VResizeTestDialog()): pass def test_show_right_with_no_label(self): # Bug: If one set show_left=False, show_label=False on a non-resizable # item like a checkbox, the Qt backend tried to set the label's size # policy and failed because label=None. with reraise_exceptions(), create_ui(NoLabelResizeTestDialog()): pass def test_enable_when_flag(self): with reraise_exceptions(), create_ui(EnableWhenDialog()): pass if __name__ == "__main__": # Execute from command line for manual testing vw = HResizeTestDialog() vw.configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/tests/test_layout.py0000644000175100001730000001230000000000000022115 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Test the layout of elements is consistent with the layout parameters. """ import unittest from traits.api import Enum, HasTraits, Str from traitsui.item import Item, UItem from traitsui.view import View from traitsui.group import HGroup, VGroup from traitsui.testing.api import UITester from traitsui.tests._tools import ( BaseTestMixin, create_ui, requires_toolkit, reraise_exceptions, ToolkitName, ) _DIALOG_WIDTH = 500 _DIALOG_HEIGHT = 500 _TXT_WIDTH = 100 class MultipleTrait(HasTraits): """An object with multiple traits to test layout and alignments.""" txt1 = Str("text1") txt2 = Str("text2") class VResizeDialog(HasTraits): txt = Str("hallo") traits_view = View( VGroup(Item("txt", width=_TXT_WIDTH, resizable=True)), width=_DIALOG_WIDTH, height=_DIALOG_HEIGHT, resizable=True, ) class HResizeDialog(HasTraits): txt = Str("hallo") traits_view = View( HGroup(Item("txt", width=_TXT_WIDTH, resizable=True)), width=_DIALOG_WIDTH, height=_DIALOG_HEIGHT, resizable=True, ) class ObjectWithResizeReadonlyItem(HasTraits): resizable_readonly_item = Enum("first", "second") def default_traits_view(self): return View( VGroup( UItem( "resizable_readonly_item", resizable=True, style="readonly", ), ), height=_DIALOG_HEIGHT, width=_DIALOG_WIDTH, ) class TestLayout(BaseTestMixin, unittest.TestCase): def setUp(self): BaseTestMixin.setUp(self) def tearDown(self): BaseTestMixin.tearDown(self) @requires_toolkit([ToolkitName.qt]) def test_qt_resizable_in_vgroup(self): # Behavior: Item.resizable controls whether a component can resize # along the non-layout axis of its group. In a VGroup, resizing should # work only in the horizontal direction. with reraise_exceptions(), create_ui(VResizeDialog()) as ui: (editor,) = ui.get_editors("txt") text = editor.control # horizontal size should be large self.assertGreater(text.width(), _DIALOG_WIDTH - 100) # vertical size should be unchanged self.assertLess(text.height(), 100) @requires_toolkit([ToolkitName.qt]) def test_qt_resizable_in_hgroup(self): # Behavior: Item.resizable controls whether a component can resize # along the non-layout axis of its group. In a HGroup, resizing should # work only in the vertical direction. with reraise_exceptions(), create_ui(HResizeDialog()) as ui: (editor,) = ui.get_editors("txt") text = editor.control # vertical size should be large self.assertGreater(text.height(), _DIALOG_HEIGHT - 100) # horizontal size should be unchanged # ??? maybe not: some elements (e.g., the text field) have # 'Expanding' as their default behavior # self.assertLess(text.width(), _TXT_WIDTH+100) # regression test for enthought/traitsui#1528 @requires_toolkit([ToolkitName.qt]) def test_qt_resizable_readonly_item(self): tester = UITester() with tester.create_ui(ObjectWithResizeReadonlyItem()) as ui: resizable_readonly_item = tester.find_by_name( ui, "resizable_readonly_item" ) # for resizable item expansion should occur in horizontal but not # vertical direction self.assertLess( resizable_readonly_item._target.control.height(), _DIALOG_HEIGHT, ) self.assertEqual( resizable_readonly_item._target.control.width(), _DIALOG_WIDTH ) @requires_toolkit([ToolkitName.qt, ToolkitName.wx]) class TestOrientation(BaseTestMixin, unittest.TestCase): """Toolkit-agnostic tests on the layout orientations.""" def setUp(self): BaseTestMixin.setUp(self) def tearDown(self): BaseTestMixin.tearDown(self) def test_vertical_layout(self): view = View( VGroup( Item("txt1"), Item("txt2"), ) ) with reraise_exceptions(), create_ui( MultipleTrait(), ui_kwargs=dict(view=view) ): pass def test_horizontal_layout(self): # layout view = View( HGroup( Item("txt1"), Item("txt2"), ) ) with reraise_exceptions(), create_ui( MultipleTrait(), ui_kwargs=dict(view=view) ): pass if __name__ == "__main__": # Execute from command line for manual testing vw = VResizeDialog() vw.configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/tests/test_regression.py0000644000175100001730000000373200000000000022771 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ General regression tests for various fixed bugs. """ from subprocess import check_output, STDOUT import sys import unittest from traits.api import DelegatesTo, Event, HasTraits, Instance, Undefined from traitsui.api import Editor, TextEditor from traitsui.tests._tools import ( BaseTestMixin, ) class Parent(HasTraits): button = Event() class Child(HasTraits): parent = Instance(Parent) button = DelegatesTo("parent") class TestRegression(BaseTestMixin, unittest.TestCase): def setUp(self): BaseTestMixin.setUp(self) def tearDown(self): BaseTestMixin.tearDown(self) def test_editor_on_delegates_to_event(self): """Make sure that DelegatesTo on Events passes Editor creation.""" child = Child(parent=Parent()) editor = Editor( None, factory=TextEditor(), object=child, name="button" ) self.assertIs(editor.old_value, Undefined) def test_attribute_error(self): """Make sure genuine AttributeErrors raise on Editor creation.""" self.assertRaises( AttributeError, Editor, None, factory=TextEditor(), object=Parent(), name="not_a_trait", ) def test_importing_view_does_not_import_toolkit(self): output = check_output( [sys.executable, "-c", "import sys; from traitsui.view import View;" "print(list(sys.modules.keys()))"], stderr=STDOUT ) output = output.decode('ascii') self.assertFalse('QtCore' in output) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/tests/test_shadow_group.py0000644000175100001730000000253200000000000023307 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Tests for the ShadowGroup class. """ import unittest from traitsui.api import Group from traitsui.group import ShadowGroup from traitsui.tests._tools import BaseTestMixin class TestShadowGroup(BaseTestMixin, unittest.TestCase): def setUp(self): BaseTestMixin.setUp(self) def tearDown(self): BaseTestMixin.tearDown(self) def test_creation_sets_shadow_first(self): group = Group() # We end up with a DelegationError if the 'shadow' trait is not set # first. Initialization order is dependent on dictionary order, which # we can't control, so we throw in a good number of other traits to # increase the chance that some other trait is set first. ShadowGroup( label="dummy", show_border=True, show_labels=True, show_left=True, orientation="horizontal", scrollable=True, shadow=group, ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/tests/test_splitter_prefs_restored.py0000644000175100001730000001157200000000000025566 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Test the storing/restoration of split group state. """ import unittest from traits.api import Int from traitsui.api import Action, Group, Handler, HSplit, Item, View from traitsui.tests._tools import ( BaseTestMixin, create_ui, requires_toolkit, ToolkitName, ) class TmpClass(Handler): aa = Int(10) bb = Int(100) def init(self, ui_info): super().init(ui_info) self.save_prefs(ui_info) return True def reset_prefs(self, ui_info): """Reset the split to be equally wide.""" control = getattr(ui_info, "h_split").control width = control.width() control.moveSplitter(width // 2, 1) def restore_prefs(self, ui_info): """Apply the last saved ui preferences.""" ui_info.ui.set_prefs(self._prefs) def save_prefs(self, ui_info): """Save the current ui preferences.""" self._prefs = ui_info.ui.get_prefs() def collapse_right(self, ui_info): """Collapse the split to the right.""" control = getattr(ui_info, "h_split").control width = control.width() control.moveSplitter(width, 1) def collapse_left(self, ui_info): """Collapse the split to the left.""" control = getattr(ui_info, "h_split").control control.moveSplitter(0, 1) view = View( HSplit( Group(Item("aa", resizable=True, width=50), show_border=True), Group(Item("bb", width=100), show_border=True), id="h_split", ), resizable=True, # add actions to test manually. buttons=[ Action(name="collapse left", action="collapse_left"), Action(name="collapse right", action="collapse_right"), Action(name="reset_layout", action="reset_prefs"), Action(name="restore layout", action="restore_prefs"), Action(name="save layout", action="save_prefs"), ], height=300, id="test_view_for_splitter_pref_restore", ) class TestSplitterPrefsRestored(BaseTestMixin, unittest.TestCase): def setUp(self): BaseTestMixin.setUp(self) def tearDown(self): BaseTestMixin.tearDown(self) @requires_toolkit([ToolkitName.qt]) def test_splitter_prefs_are_restored(self): # the keys for the splitter prefs (i.e. prefs['h_split']['structure']) splitter_keys = ("h_split", "structure") def _get_nattr(obj, attr_names=splitter_keys): """Utility function to get a value from a nested dict.""" if obj is None or len(attr_names) == 0: return obj return _get_nattr( obj.get(attr_names[0], None), attr_names=attr_names[1:] ) with create_ui(TmpClass()) as ui: handler = ui.handler # set the layout to a known state handler.reset_prefs(ui.info) # save the current layout and check (sanity test) handler.save_prefs(ui.info) self.assertEqual( _get_nattr(handler._prefs), _get_nattr(ui.get_prefs()) ) # collapse splitter to right and check prefs has been updated handler.collapse_right(ui.info) self.assertNotEqual( _get_nattr(handler._prefs), _get_nattr(ui.get_prefs()) ) # restore the original layout. handler.restore_prefs(ui.info) self.assertEqual( _get_nattr(handler._prefs), _get_nattr(ui.get_prefs()) ) # collapse to left and check handler.collapse_left(ui.info) self.assertNotEqual( _get_nattr(handler._prefs), _get_nattr(ui.get_prefs()) ) # save the collapsed layout handler.save_prefs(ui.info) collapsed_splitter_state = _get_nattr(handler._prefs) self.assertEqual( _get_nattr(handler._prefs), _get_nattr(ui.get_prefs()) ) # create a new ui and check that the splitter remembers the last state # (collapsed) with create_ui(TmpClass()) as ui2: self.assertEqual( collapsed_splitter_state, _get_nattr(ui2.get_prefs()) ) if __name__ == "__main__": # Execute from command line for manual testing # start a session to modify the default layout. TmpClass().configure_traits() # check visually if the layout changes from the prev. session is # restored. TmpClass().configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/tests/test_theme.py0000644000175100001730000000364500000000000021716 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! import pickle import unittest from unittest.mock import patch from traitsui.tests._tools import BaseTestMixin, requires_toolkit, ToolkitName from traitsui.theme import Theme class TestTheme(BaseTestMixin, unittest.TestCase): def setUp(self): BaseTestMixin.setUp(self) self.theme = Theme() def tearDown(self): BaseTestMixin.tearDown(self) def test_theme_pickling(self): # A regression test for issue enthought/traitsui#825 pickle.dumps(self.theme) @requires_toolkit([ToolkitName.wx]) def test_theme_content_color_default(self): import wx self.assertEqual(self.theme.content_color, wx.BLACK) def test_theme_content_color_setter_getter(self): self.theme.content_color = "red" self.assertEqual(self.theme.content_color, "red") @requires_toolkit([ToolkitName.wx]) def test_theme_label_color_default(self): import wx self.assertEqual(self.theme.label_color, wx.BLACK) def test_theme_label_color_setter_getter(self): self.theme.label_color = "red" self.assertEqual(self.theme.label_color, "red") @requires_toolkit([ToolkitName.wx]) def test_theme_get_image_slice(self): self.theme.image = "mock_image" with patch("traitsui.wx.image_slice.image_slice_for") as slice_func: self.theme.image_slice slice_func.assert_called_once() def test_theme_get_image_slice_none(self): self.theme.image = None self.assertIsNone(self.theme.image_slice) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/tests/test_toolkit.py0000644000175100001730000000434700000000000022301 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! from contextlib import contextmanager import unittest from traits.trait_base import ETSConfig import traitsui.toolkit from traitsui.tests._tools import BaseTestMixin @contextmanager def clear_toolkit(): """If a toolkit has been selected, clear it, resetting on exit""" old_ETS_toolkit = ETSConfig._toolkit old_traitsui_toolkit = traitsui.toolkit._toolkit ETSConfig._toolkit = "" traitsui.toolkit._toolkit = None try: yield finally: ETSConfig._toolkit = old_ETS_toolkit traitsui.toolkit._toolkit = old_traitsui_toolkit class TestToolkit(BaseTestMixin, unittest.TestCase): def setUp(self): BaseTestMixin.setUp(self) def tearDown(self): BaseTestMixin.tearDown(self) def test_default_toolkit(self): with clear_toolkit(): # try to import default toolkit - this is just a smoke test tk = traitsui.toolkit.toolkit() self.assertNotEqual(ETSConfig.toolkit, "") self.assertIsInstance(tk, traitsui.toolkit.Toolkit) def test_nonstandard_toolkit(self): with clear_toolkit(): # try to import a non-default toolkit tk = traitsui.toolkit.toolkit("null") self.assertEqual(ETSConfig.toolkit, "null") from traitsui.null import toolkit self.assertIs(tk, toolkit) def test_nonexistent_toolkit(self): with clear_toolkit(): # try to import a non-existent toolkit tk = traitsui.toolkit.toolkit("nosuchtoolkit") # should fail, and give us a standard toolkit, but don't know which # exactly we get what depends on what is installed in test # environment self.assertTrue(ETSConfig.toolkit in {"null", "qt4", "wx", "qt"}) self.assertTrue(tk.toolkit in {"null", "qt4", "wx", "qt"}) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/tests/test_toolkit_traits.py0000644000175100001730000000147000000000000023661 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! import unittest from traits.api import HasTraits from traitsui.toolkit_traits import RGBColor class HasRGBColor(HasTraits): color = RGBColor() class TestRGBColor(unittest.TestCase): # regression test for enthought/traitsui#1531 def test_hex_converion(self): has_rgb_color = HasRGBColor() has_rgb_color.color = 0x000000 self.assertEqual(has_rgb_color.color, (0.0, 0.0, 0.0)) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/tests/test_tree_node.py0000644000175100001730000000275700000000000022563 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Test cases for the TreeNode object. """ import unittest from traits.api import HasStrictTraits, List, Str, This from traits.testing.api import UnittestTools from traitsui.api import TreeNode from traitsui.tests._tools import BaseTestMixin class DummyModel(HasStrictTraits): """Dummy model with children.""" name = Str() children = List(This) class TestTreeNode(BaseTestMixin, UnittestTools, unittest.TestCase): def setUp(self): BaseTestMixin.setUp(self) def tearDown(self): BaseTestMixin.tearDown(self) def test_insert_child(self): # Regression test for #559 model = DummyModel( name="Parent", children=[DummyModel(name="Child0"), DummyModel(name="Child2")], ) node = TreeNode(children="children", node_for=[DummyModel]) node.insert_child(model, 1, DummyModel(name="Child1")) # Assert self.assertEqual(len(model.children), 3) for i in range(3): self.assertEqual(model.children[i].name, "Child{}".format(i)) if __name__ == "__main__": unittest.run() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/tests/test_ui.py0000644000175100001730000003323200000000000021224 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Test cases for the UI object. """ import contextlib import unittest from pyface.api import GUI from traits.api import Property from traits.has_traits import HasTraits, HasStrictTraits from traits.trait_types import Str, Int import traitsui from traitsui.basic_editor_factory import BasicEditorFactory from traitsui.api import Group, Item, spring, View from traitsui.testing.api import DisplayedText, IsEnabled, MouseClick, UITester from traitsui.tests._tools import ( BaseTestMixin, count_calls, create_ui, is_qt, is_wx, process_cascade_events, requires_toolkit, reraise_exceptions, ToolkitName, ) from traitsui.toolkit import toolkit, toolkit_object class FooDialog(HasTraits): """Test dialog that does nothing useful.""" my_int = Int(2) my_str = Str("hallo") traits_view = View(Item("my_int"), Item("my_str"), buttons=["OK"]) class DisallowNewTraits(HasStrictTraits): """Make sure no extra traits are added.""" x = Int(10) traits_view = View(Item("x"), spring) class MaybeInvalidTrait(HasTraits): name = Str() name_is_invalid = Property(observe="name") traits_view = View(Item("name", invalid="name_is_invalid")) def _get_name_is_invalid(self): return len(self.name) < 10 class TestUI(BaseTestMixin, unittest.TestCase): def setUp(self): BaseTestMixin.setUp(self) def tearDown(self): BaseTestMixin.tearDown(self) @requires_toolkit([ToolkitName.wx]) def test_reset_with_destroy_wx(self): # Characterization test: # UI.reset(destroy=True) destroys all ui children of the top control foo = FooDialog() with create_ui(foo) as ui: ui.reset(destroy=True) process_cascade_events() # the top control is still there self.assertIsNotNone(ui.control) # but its children are gone self.assertEqual(len(ui.control.GetChildren()), 0) @requires_toolkit([ToolkitName.qt]) def test_reset_with_destroy_qt(self): # Characterization test: # UI.reset(destroy=True) destroys all ui children of the top control from pyface import qt foo = FooDialog() with create_ui(foo) as ui: # decorate children's `deleteLater` function to check that it is # called on `reset`. check only with the editor parts (only widgets # are scheduled. # See traitsui.qt.toolkit.GUIToolkit.destroy_children) for c in ui.control.children(): c.deleteLater = count_calls(c.deleteLater) ui.reset(destroy=True) # the top control is still there self.assertIsNotNone(ui.control) # but its children are scheduled for removal for c in ui.control.children(): if isinstance(c, qt.QtGui.QWidget): self.assertEqual(c.deleteLater._n_calls, 1) @requires_toolkit([ToolkitName.wx]) def test_reset_without_destroy_wx(self): # Characterization test: # UI.reset(destroy=False) destroys all editor controls, but leaves # editors and ui children intact import wx foo = FooDialog() with create_ui(foo) as ui: self.assertEqual(len(ui._editors), 2) self.assertIsInstance( ui._editors[0], traitsui.wx.text_editor.SimpleEditor ) self.assertIsInstance(ui._editors[0].control, wx.TextCtrl) ui.reset(destroy=False) self.assertEqual(len(ui._editors), 2) self.assertIsInstance( ui._editors[0], traitsui.wx.text_editor.SimpleEditor ) self.assertIsNone(ui._editors[0].control) # children are still there: check first text control text_ctrl = ui.control.FindWindowByName("text") self.assertIsNotNone(text_ctrl) @requires_toolkit([ToolkitName.qt]) def test_reset_without_destroy_qt(self): # Characterization test: # UI.reset(destroy=False) destroys all editor controls, but leaves # editors and ui children intact from pyface import qt foo = FooDialog() with create_ui(foo) as ui: self.assertEqual(len(ui._editors), 2) self.assertIsInstance( ui._editors[0], traitsui.qt.text_editor.SimpleEditor ) self.assertIsInstance(ui._editors[0].control, qt.QtGui.QLineEdit) ui.reset(destroy=False) self.assertEqual(len(ui._editors), 2) self.assertIsInstance( ui._editors[0], traitsui.qt.text_editor.SimpleEditor ) self.assertIsNone(ui._editors[0].control) # children are still there: check first text control text_ctrl = ui.control.findChild(qt.QtGui.QLineEdit) self.assertIsNotNone(text_ctrl) @requires_toolkit([ToolkitName.wx]) def test_destroy_after_ok_wx(self): # Behavior: after pressing 'OK' in a dialog, the method UI.finish is # called and the view control and its children are destroyed foo = FooDialog() tester = UITester() with tester.create_ui(foo) as ui: # keep reference to the control to check that it was destroyed control = ui.control # decorate control's `Destroy` function to check that it is called control.Destroy = count_calls(control.Destroy) # press the OK button and close the dialog ok_button = tester.find_by_id(ui, "OK") self.assertEqual(ok_button.inspect(DisplayedText()), "OK") self.assertTrue(ok_button.inspect(IsEnabled())) ok_button.perform(MouseClick()) self.assertIsNone(ui.control) self.assertEqual(control.Destroy._n_calls, 1) @requires_toolkit([ToolkitName.qt]) def test_destroy_after_ok_qt(self): # Behavior: after pressing 'OK' in a dialog, the method UI.finish is # called and the view control and its children are destroyed foo = FooDialog() tester = UITester() with tester.create_ui(foo) as ui: # keep reference to the control to check that it was deleted control = ui.control # decorate control's `deleteLater` function to check that it is # called control.deleteLater = count_calls(control.deleteLater) # press the OK button and close the dialog ok_button = tester.find_by_id(ui, "OK") self.assertEqual(ok_button.inspect(DisplayedText()), "OK") self.assertTrue(ok_button.inspect(IsEnabled())) ok_button.perform(MouseClick()) self.assertIsNone(ui.control) self.assertEqual(control.deleteLater._n_calls, 1) @requires_toolkit([ToolkitName.qt, ToolkitName.wx]) def test_no_spring_trait(self): obj = DisallowNewTraits() with create_ui(obj): pass self.assertTrue("spring" not in obj.traits()) @requires_toolkit([ToolkitName.qt, ToolkitName.wx]) def test_invalid_state(self): # Regression test for enthought/traitsui#983 obj = MaybeInvalidTrait(name="Name long enough to be valid") with create_ui(obj) as ui: (editor,) = ui.get_editors("name") self.assertFalse(editor.invalid) obj.name = "too short" self.assertTrue(editor.invalid) # Regression test on an AttributeError commonly seen (enthought/traitsui#1145) # Code in ui_panel makes use toolkit specific attributes on the toolkit # specific Editor ToolkitSpecificEditor = toolkit_object("editor:Editor") class DummyObject(HasTraits): number = Int() if is_qt(): from pyface.qt import QtGui, QtCore class CustomWidget(QtGui.QWidget): def __init__(self, editor, parent=None): super().__init__() self._some_editor = editor def sizeHint(self): # This is called if the sibling widget is destroyed (e.g. the # nested UI in EditorWithCustomWidget) while the container # (e.g. QSplitter in EditorWithCustomWidget) has not been # destroyed. The container will want to ask this widget for its # sizeHint in order to resize/repaint the layout. assert self._some_editor.factory is not None return super().sizeHint() class EditorWithCustomWidget(ToolkitSpecificEditor): def init(self, parent): self.control = QtGui.QSplitter(QtCore.Qt.Orientation.Horizontal) widget = CustomWidget(editor=self) self.control.addWidget(widget) self.control.setStretchFactor(0, 2) self._ui = self.edit_traits( parent=self.control, kind="subpanel", view=View(Item("_", label="DUMMY"), width=100, height=100), ) self.control.addWidget(self._ui.control) def dispose(self): self.dispose_inner_ui() super().dispose() def dispose_inner_ui(self): if self._ui is not None: self._ui.dispose() self._ui = None def update_editor(self): pass if is_wx(): # The AttributeError is not seen on Wx. But Destroy needs to be made # asynchronous. import wx from traitsui.wx.helper import TraitsUIPanel class DummyButtonEditor(ToolkitSpecificEditor): def init(self, parent): self.control = wx.Button(parent, -1, "Dummy") self.control.Bind(wx.EVT_BUTTON, self.update_object) def dispose(self): # If the object is deleted too soon, we run into a RuntimeError. self.control.Unbind(wx.EVT_BUTTON) super().dispose() def update_object(self, event): pass def update_editor(self): pass class EditorWithCustomWidget(ToolkitSpecificEditor): # noqa: F811 def init(self, parent): self.control = TraitsUIPanel(parent, -1) sizer = wx.BoxSizer(wx.HORIZONTAL) self._dummy = DummyObject() self._ui = self._dummy.edit_traits( parent=self.control, kind="subpanel", view=View( Item( "number", editor=BasicEditorFactory(klass=DummyButtonEditor), ), ), ) sizer.Add(self._ui.control, 1, wx.EXPAND) self.control.SetSizerAndFit(sizer) def update_editor(self): pass def dispose(self): super().dispose() def dispose_inner_ui(self): if self._ui is not None: self._ui.dispose() self._ui = None class TestUIDispose(BaseTestMixin, unittest.TestCase): """Test disposal of UI.""" def setUp(self): BaseTestMixin.setUp(self) def tearDown(self): BaseTestMixin.tearDown(self) @requires_toolkit([ToolkitName.qt, ToolkitName.wx]) def test_close_ui(self): # Test closing the main window normally. obj = DummyObject() view = View( Group( Item( "number", editor=BasicEditorFactory(klass=EditorWithCustomWidget), ), ), ) ui = obj.edit_traits(view=view) with ensure_destroyed(ui): gui = GUI() gui.invoke_later(close_control, ui.control) with reraise_exceptions(): process_cascade_events() self.assertIsNone(ui.control) @requires_toolkit([ToolkitName.qt, ToolkitName.wx]) def test_dispose_inner_ui(self): obj = DummyObject() view = View( Group( Item( "number", editor=BasicEditorFactory(klass=EditorWithCustomWidget), ), ), ) ui = obj.edit_traits(view=view) (editor,) = ui.get_editors("number") gui = GUI() with ensure_destroyed(ui): # This represents an event handler that causes a nested UI to be # disposed. gui.invoke_later(editor.dispose_inner_ui) # Allowing GUI to process the disposal requests is crucial. # This requirement should be satisfied in production setting # where the dispose method is part of an event handler. # Not doing so before disposing the main UI would be a programming # error in tests settings. with reraise_exceptions(): process_cascade_events() gui.invoke_later(close_control, ui.control) with reraise_exceptions(): process_cascade_events() self.assertIsNone(ui.control) @contextlib.contextmanager def ensure_destroyed(ui): """Ensure the widget is destroyed in the event when test fails.""" try: yield finally: if ui.control is not None: toolkit().destroy_control(ui.control) with reraise_exceptions(): process_cascade_events() def close_control(control): """Close the widget.""" if is_qt(): control.close() elif is_wx(): control.Close() else: raise NotImplementedError("Unexpected toolkit") ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/tests/test_ui_panel.py0000644000175100001730000000614700000000000022410 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Tests to exercise logic for layouts, e.g. VGroup, HGroup. """ import unittest from pyface.toolkit import toolkit_object from pyface.constant import OK try: from pyface.qt import QtWebkit # noqa: F401 NO_WEBKIT_OR_WEBENGINE = False except ImportError: try: from pyface.qt import QtWebEngine # noqa: F401 NO_WEBKIT_OR_WEBENGINE = False except ImportError: NO_WEBKIT_OR_WEBENGINE = True from traits.api import HasTraits, Int from traitsui.api import HelpButton, HGroup, Item, spring, VGroup, View from traitsui.testing.api import MouseClick, UITester from traitsui.tests._tools import ( BaseTestMixin, create_ui, requires_toolkit, ToolkitName, ) ModalDialogTester = toolkit_object( "util.modal_dialog_tester:ModalDialogTester" ) class ObjectWithNumber(HasTraits): number1 = Int() number2 = Int() number3 = Int() class HelpPanel(HasTraits): my_int = Int(2, help='this is the help for my int') def default_traits_view(self): view = View( Item(name="my_int"), title="HelpPanel", buttons=[HelpButton], ) return view @requires_toolkit([ToolkitName.qt, ToolkitName.wx]) class TestUIPanel(BaseTestMixin, unittest.TestCase): def setUp(self): BaseTestMixin.setUp(self) def tearDown(self): BaseTestMixin.tearDown(self) def test_grouped_layout_with_springy(self): # Regression test for enthought/traitsui#1066 obj1 = ObjectWithNumber() view = View( HGroup( VGroup( Item("number1"), ), VGroup( Item("number2"), ), spring, ) ) # This should not fail. with create_ui(obj1, dict(view=view)): pass # Regression test for enthought/traitsui#1538 @requires_toolkit([ToolkitName.qt]) @unittest.skipIf( NO_WEBKIT_OR_WEBENGINE, "Tests require either QtWebKit or QtWebEngine" ) def test_show_help(self): # This help window is not actually modal, when opened it will be the # active window not active modal widget class MyTester(ModalDialogTester): def get_dialog_widget(self): return self._qt_app.activeWindow() panel = HelpPanel() tester = UITester(auto_process_events=False) with tester.create_ui(panel) as ui: help_button = tester.find_by_id(ui, 'Help') def click_help(): # should not fail help_button.perform(MouseClick()) mdtester = MyTester(click_help) mdtester.open_and_run(lambda x: x.click_button(OK)) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/tests/test_ui_traits.py0000644000175100001730000000547200000000000022617 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! import unittest from traits.api import HasStrictTraits from traitsui import ui_traits from traitsui.tests._tools import BaseTestMixin class ObjectWithUITraits(HasStrictTraits): orientation = ui_traits.Orientation style = ui_traits.EditorStyle layout = ui_traits.Layout an_object = ui_traits.AnObject dock_style = ui_traits.DockStyle view_status = ui_traits.ViewStatus() class TestUITraits(BaseTestMixin, unittest.TestCase): def setUp(self): BaseTestMixin.setUp(self) def tearDown(self): BaseTestMixin.tearDown(self) def test_orientation(self): obj = ObjectWithUITraits() self.assertEqual(obj.orientation, "vertical") obj.orientation = "h" self.assertEqual(obj.orientation, "horizontal") def test_editor_style(self): obj = ObjectWithUITraits() self.assertEqual(obj.style, "simple") obj.style = "r" self.assertEqual(obj.style, "readonly") def test_layout(self): obj = ObjectWithUITraits() self.assertEqual(obj.layout, "normal") def test_an_object(self): obj = ObjectWithUITraits() obj.an_object = "[1,2,3][0]" self.assertEqual(obj.an_object, "[1,2,3][0]") actual = eval(obj.an_object_, {}, {}) self.assertEqual(actual, 1) class TestStatusItem(BaseTestMixin, unittest.TestCase): def setUp(self): BaseTestMixin.setUp(self) def tearDown(self): BaseTestMixin.tearDown(self) def test_init(self): item = ui_traits.StatusItem( name="some_trait_name", width=10.0, ) self.assertEqual(item.name, "some_trait_name") def test_init_with_name_and_value(self): # value trumps name. Not sure why but this is the way it is. item = ui_traits.StatusItem( name="some_trait_name", width=10.0, value="some_other_name", ) self.assertEqual(item.name, "some_other_name") class TestViewStatus(BaseTestMixin, unittest.TestCase): def setUp(self): BaseTestMixin.setUp(self) def tearDown(self): BaseTestMixin.tearDown(self) def test_init(self): obj = ObjectWithUITraits() obj.view_status = "some_name" self.assertEqual(len(obj.view_status), 1) (status,) = obj.view_status self.assertIsInstance(status, ui_traits.StatusItem) self.assertEqual(status.name, "some_name") ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/tests/test_undo.py0000644000175100001730000010731400000000000021557 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! import functools import unittest from warnings import catch_warnings from pyface.toolkit import toolkit_object from pyface.undo.api import AbstractCommand from traits.api import Any, HasTraits, Int, List, Str, Tuple from traits.testing.api import UnittestTools from traitsui.tests.test_editor import create_editor from traitsui.undo import AbstractUndoItem, ListUndoItem, UndoHistory, UndoItem from traitsui.tests._tools import BaseTestMixin GuiTestAssistant = toolkit_object("util.gui_test_assistant:GuiTestAssistant") no_gui_test_assistant = GuiTestAssistant.__name__ == "Unimplemented" if no_gui_test_assistant: # ensure null toolkit has an inheritable GuiTestAssistant class GuiTestAssistant(object): pass class SimpleExample(HasTraits): value = Int() str_value = Str() tuple_value = Tuple() list_value = List() any_value = Any() class DummyCommand(AbstractCommand): def do(self): self.data = "do" def undo(self): self.data = "undo" def redo(self): self.data = "redo" class LegacyUndoItem(AbstractUndoItem): def undo(self): pass def redo(self): pass class TestUndoItem(UnittestTools, unittest.TestCase): def test_undo(self): example = SimpleExample(value=11) undo_item = UndoItem( object=example, name='value', old_value=10, new_value=11, ) with self.assertTraitChanges(example, 'value', count=1): undo_item.undo() self.assertEqual(example.value, 10) def test_redo(self): example = SimpleExample(value=10) undo_item = UndoItem( object=example, name='value', old_value=10, new_value=11, ) with self.assertTraitChanges(example, 'value', count=1): undo_item.redo() self.assertEqual(example.value, 11) def test_merge_different_undo_item_type(self): example_1 = SimpleExample() example_2 = SimpleExample() undo_item = UndoItem( object=example_1, name='any_value', ) next_undo_item = ListUndoItem( object=example_2, name='any_value', ) result = undo_item.merge(next_undo_item) self.assertFalse(result) def test_merge_different_objects(self): example_1 = SimpleExample() example_2 = SimpleExample() undo_item = UndoItem( object=example_1, name='value', old_value=10, new_value=11, ) next_undo_item = UndoItem( object=example_2, name='value', old_value=10, new_value=11, ) result = undo_item.merge(next_undo_item) self.assertFalse(result) def test_merge_different_traits(self): example = SimpleExample() undo_item = UndoItem( object=example, name='value', old_value=10, new_value=11, ) next_undo_item = UndoItem( object=example, name='str_value', old_value='foo', new_value='bar', ) result = undo_item.merge(next_undo_item) self.assertFalse(result) def test_merge_different_value_types(self): example = SimpleExample() undo_item = UndoItem( object=example, name='any_value', old_value=10, new_value=11, ) next_undo_item = UndoItem( object=example, name='any_value', old_value=11, new_value='foo', ) result = undo_item.merge(next_undo_item) self.assertFalse(result) def test_merge_numbers(self): example = SimpleExample() undo_item = UndoItem( object=example, name='value', old_value=10, new_value=11, ) next_undo_item = UndoItem( object=example, name='value', old_value=11, new_value=12, ) result = undo_item.merge(next_undo_item) self.assertTrue(result) self.assertEqual(undo_item.old_value, 10) self.assertEqual(undo_item.new_value, 12) def test_merge_str_insert(self): example = SimpleExample() undo_item = UndoItem( object=example, name='str_value', old_value='foo', new_value='bar', ) next_undo_item = UndoItem( object=example, name='str_value', old_value='bar', new_value='bear', ) result = undo_item.merge(next_undo_item) self.assertTrue(result) self.assertEqual(undo_item.old_value, 'foo') self.assertEqual(undo_item.new_value, 'bear') def test_merge_str_delete(self): example = SimpleExample() undo_item = UndoItem( object=example, name='str_value', old_value='foo', new_value='bear', ) next_undo_item = UndoItem( object=example, name='str_value', old_value='bear', new_value='bar', ) result = undo_item.merge(next_undo_item) self.assertTrue(result) self.assertEqual(undo_item.old_value, 'foo') self.assertEqual(undo_item.new_value, 'bar') def test_merge_str_change(self): example = SimpleExample() undo_item = UndoItem( object=example, name='str_value', old_value='foo', new_value='bar', ) next_undo_item = UndoItem( object=example, name='str_value', old_value='bar', new_value='baz', ) result = undo_item.merge(next_undo_item) self.assertTrue(result) self.assertEqual(undo_item.old_value, 'foo') self.assertEqual(undo_item.new_value, 'baz') def test_merge_str_same(self): example = SimpleExample() undo_item = UndoItem( object=example, name='str_value', old_value='foo', new_value='bar', ) next_undo_item = UndoItem( object=example, name='str_value', old_value='bar', new_value='bar', ) result = undo_item.merge(next_undo_item) self.assertTrue(result) self.assertEqual(undo_item.old_value, 'foo') self.assertEqual(undo_item.new_value, 'bar') def test_merge_str_different(self): example = SimpleExample() undo_item = UndoItem( object=example, name='str_value', old_value='foo', new_value='bar', ) next_undo_item = UndoItem( object=example, name='str_value', old_value='bar', new_value='wombat', ) result = undo_item.merge(next_undo_item) self.assertFalse(result) def test_merge_sequence_change(self): example = SimpleExample() undo_item = UndoItem( object=example, name='tuple_value', old_value=('foo', 'bar', 'baz'), new_value=('foo', 'wombat', 'baz'), ) next_undo_item = UndoItem( object=example, name='tuple_value', old_value=('foo', 'wombat', 'baz'), new_value=('foo', 'fizz', 'baz'), ) result = undo_item.merge(next_undo_item) self.assertTrue(result) self.assertEqual(undo_item.old_value, ('foo', 'bar', 'baz')) self.assertEqual(undo_item.new_value, ('foo', 'fizz', 'baz')) def test_merge_sequence_change_different_types(self): example = SimpleExample() undo_item = UndoItem( object=example, name='tuple_value', old_value=('foo', 'bar', 'baz'), new_value=('foo', 'wombat', 'baz'), ) next_undo_item = UndoItem( object=example, name='tuple_value', old_value=('foo', 'wombat', 'baz'), new_value=('foo', 12, 'baz'), ) result = undo_item.merge(next_undo_item) self.assertTrue(result) self.assertEqual(undo_item.old_value, ('foo', 'bar', 'baz')) self.assertEqual(undo_item.new_value, ('foo', 12, 'baz')) def test_merge_sequence_change_not_simple_types(self): example = SimpleExample() undo_item = UndoItem( object=example, name='tuple_value', old_value=('foo', ['bar'], 'baz'), new_value=('foo', ['wombat'], 'baz'), ) next_undo_item = UndoItem( object=example, name='tuple_value', old_value=('foo', ['wombat'], 'baz'), new_value=('foo', ['fizz'], 'baz'), ) result = undo_item.merge(next_undo_item) self.assertTrue(result) self.assertEqual(undo_item.old_value, ('foo', ['bar'], 'baz')) self.assertEqual(undo_item.new_value, ('foo', ['fizz'], 'baz')) def test_merge_sequence_change_multiple_not_simple_types(self): example = SimpleExample() undo_item = UndoItem( object=example, name='tuple_value', old_value=(['foo'], 'bar', 'baz'), new_value=(['foo'], 'wombat', 'baz'), ) next_undo_item = UndoItem( object=example, name='tuple_value', old_value=(['foo'], 'wombat', 'baz'), new_value=(['foo'], 'fizz', 'baz'), ) result = undo_item.merge(next_undo_item) # XXX This is perhaps unexpected. See GitHub issue #1507. self.assertFalse(result) def test_merge_sequence_change_back(self): example = SimpleExample() undo_item = UndoItem( object=example, name='tuple_value', old_value=('foo', 'bar', 'baz'), new_value=('foo', 'wombat', 'baz'), ) next_undo_item = UndoItem( object=example, name='tuple_value', old_value=('foo', 'wombat', 'baz'), new_value=('foo', 'bar', 'baz'), ) result = undo_item.merge(next_undo_item) self.assertFalse(result) def test_merge_sequence_two_changes(self): example = SimpleExample() undo_item = UndoItem( object=example, name='tuple_value', old_value=('foo', 'bar', 'baz'), new_value=('foo', 'wombat', 'baz'), ) next_undo_item = UndoItem( object=example, name='tuple_value', old_value=('foo', 'wombat', 'baz'), new_value=('foo', 'wombat', 'fizz'), ) result = undo_item.merge(next_undo_item) self.assertFalse(result) def test_merge_sequence_change_length(self): example = SimpleExample() undo_item = UndoItem( object=example, name='tuple_value', old_value=('foo', 'bar', 'baz'), new_value=('foo', 'wombat', 'baz'), ) next_undo_item = UndoItem( object=example, name='tuple_value', old_value=('foo', 'wombat', 'baz'), new_value=('foo', 'wombat', 'baz', 'fizz'), ) result = undo_item.merge(next_undo_item) self.assertFalse(result) def test_merge_unhandled_type(self): example = SimpleExample() undo_item = UndoItem( object=example, name='any_value', old_value={'foo', 'bar', 'baz'}, new_value={'foo', 'wombat', 'baz'}, ) next_undo_item = UndoItem( object=example, name='any_value', old_value={'foo', 'wombat', 'baz'}, new_value={'foo', 'fizz', 'baz'}, ) result = undo_item.merge(next_undo_item) self.assertFalse(result) class TestListUndoItem(UnittestTools, unittest.TestCase): def test_undo(self): example = SimpleExample(list_value=['foo', 'wombat', 'baz']) undo_item = ListUndoItem( object=example, name='list_value', index=1, added=['wombat', 'baz'], removed=['bar'], ) with self.assertTraitChanges(example, 'list_value_items', count=1): undo_item.undo() self.assertEqual(example.list_value, ['foo', 'bar']) def test_redo(self): example = SimpleExample(list_value=['foo', 'bar']) undo_item = ListUndoItem( object=example, name='list_value', index=1, added=['wombat', 'baz'], removed=['bar'], ) with self.assertTraitChanges(example, 'list_value_items', count=1): undo_item.redo() self.assertEqual(example.list_value, ['foo', 'wombat', 'baz']) def test_merge_identical(self): example = SimpleExample(list_value=['foo', 'wombat', 'baz']) added = [['wombat'], 'baz'] removed = [['bar']] undo_item = ListUndoItem( object=example, name='list_value', index=1, added=added.copy(), removed=removed.copy(), ) next_undo_item = ListUndoItem( object=example, name='list_value', index=1, added=added.copy(), removed=removed.copy(), ) result = undo_item.merge(next_undo_item) self.assertTrue(result) def test_merge_equal(self): example = SimpleExample(list_value=['foo', 'wombat', 'baz']) removed = [['bar']] undo_item = ListUndoItem( object=example, name='list_value', index=1, added=[['wombat'], 'baz'], removed=removed.copy(), ) next_undo_item = ListUndoItem( object=example, name='list_value', index=1, added=[['wombat'], 'baz'], removed=removed.copy(), ) result = undo_item.merge(next_undo_item) self.assertFalse(result) def test_merge_equal_removed(self): example = SimpleExample(list_value=['foo', 'wombat', 'baz']) added = [['wombat'], 'baz'] undo_item = ListUndoItem( object=example, name='list_value', index=1, added=added.copy(), removed=[['bar']], ) next_undo_item = ListUndoItem( object=example, name='list_value', index=1, added=added.copy(), removed=[['bar']], ) result = undo_item.merge(next_undo_item) self.assertFalse(result) def test_merge_different_objects(self): example_1 = SimpleExample() example_2 = SimpleExample() added = [['wombat'], 'baz'] removed = [['bar']] undo_item = ListUndoItem( object=example_1, name='list_value', index=1, added=added.copy(), removed=removed.copy(), ) next_undo_item = ListUndoItem( object=example_2, name='list_value', index=1, added=added.copy(), removed=removed.copy(), ) result = undo_item.merge(next_undo_item) self.assertFalse(result) def test_merge_different_traits(self): example = SimpleExample() added = [['wombat'], 'baz'] removed = [['bar']] undo_item = ListUndoItem( object=example, name='list_value', index=1, added=added.copy(), removed=removed.copy(), ) next_undo_item = ListUndoItem( object=example, name='any_value', index=1, added=added.copy(), removed=removed.copy(), ) result = undo_item.merge(next_undo_item) self.assertFalse(result) def test_merge_different_index(self): example = SimpleExample() added = [['wombat'], 'baz'] removed = [['bar']] undo_item = ListUndoItem( object=example, name='list_value', index=1, added=added.copy(), removed=removed.copy(), ) next_undo_item = ListUndoItem( object=example, name='any_value', index=0, added=added.copy(), removed=removed.copy(), ) result = undo_item.merge(next_undo_item) self.assertFalse(result) class TestUndoHistory(UnittestTools, unittest.TestCase): def _populate_history(self, history): """Add some simple hostory items.""" self._example = SimpleExample() history.add( UndoItem( object=self._example, name='str_value', old_value='foo', new_value='bar', ) ) history.add( UndoItem( object=self._example, name='str_value', old_value='bar', new_value='wombat', ), ) history.add( UndoItem( object=self._example, name='str_value', old_value='wombat', new_value='baz', ), ) def test_defaults(self): history = UndoHistory() self.assertEqual(history.now, 0) self.assertFalse(history.can_undo) self.assertFalse(history.can_redo) def test_add_empty(self): history = UndoHistory() example = SimpleExample(str_value='foo') undo_item = UndoItem( object=example, name='str_value', old_value='foo', new_value='bar', ) with self.assertTraitChanges(history, 'undoable', count=1): with self.assertTraitDoesNotChange(history, 'redoable'): history.add(undo_item) self.assertEqual(history.now, 1) self.assertTrue(history.can_undo) self.assertFalse(history.can_redo) def test_add_end(self): history = UndoHistory() example = SimpleExample(str_value='foo', value=10) undo_item = UndoItem( object=example, name='value', old_value=0, new_value=10, ) history.add( UndoItem( object=example, name='str_value', old_value='foo', new_value='bar', ) ) with self.assertTraitDoesNotChange(history, 'undoable'): with self.assertTraitDoesNotChange(history, 'redoable'): history.add(undo_item) self.assertEqual(history.now, 2) self.assertTrue(history.can_undo) self.assertFalse(history.can_redo) def test_add_merge(self): history = UndoHistory() example = SimpleExample(str_value='foo', value=10) undo_item = UndoItem( object=example, name='str_value', old_value='foo', new_value='baz', ) history.add( UndoItem( object=example, name='str_value', old_value='foo', new_value='bar', ) ) with self.assertTraitDoesNotChange(history, 'undoable'): with self.assertTraitDoesNotChange(history, 'redoable'): history.add(undo_item) self.assertEqual(history.now, 1) self.assertTrue(history.can_undo) self.assertFalse(history.can_redo) def test_add_end_extend(self): history = UndoHistory() example = SimpleExample(str_value='foo', value=10) undo_item = UndoItem( object=example, name='value', old_value=0, new_value=10, ) history.add( UndoItem( object=example, name='str_value', old_value='foo', new_value='bar', ) ) with self.assertTraitDoesNotChange(history, 'undoable'): with self.assertTraitDoesNotChange(history, 'redoable'): history.add(undo_item, extend=True) self.assertEqual(history.now, 1) self.assertTrue(history.can_undo) self.assertFalse(history.can_redo) def test_add_end_extend_merge(self): history = UndoHistory() example = SimpleExample(str_value='foo', value=10) undo_item = UndoItem( object=example, name='str_value', old_value='foo', new_value='baz', ) history.add( UndoItem( object=example, name='str_value', old_value='foo', new_value='bar', ) ) with self.assertTraitDoesNotChange(history, 'undoable'): with self.assertTraitDoesNotChange(history, 'redoable'): history.add(undo_item, extend=True) self.assertEqual(history.now, 1) self.assertTrue(history.can_undo) self.assertFalse(history.can_redo) # XXX this is testing private state to ensure merge happened self.assertEqual(len(history.stack._stack), 1) def test_add_middle(self): history = UndoHistory() example = SimpleExample(str_value='foo', value=10) undo_item = UndoItem( object=example, name='value', old_value=0, new_value=10, ) history.add( UndoItem( object=example, name='str_value', old_value='foo', new_value='bar', ) ) history.add( UndoItem( object=example, name='str_value', old_value='bar', new_value='wombat', ), ) history.undo() with self.assertTraitDoesNotChange(history, 'undoable'): with self.assertTraitChanges(history, 'redoable', count=1): history.add(undo_item) self.assertEqual(history.now, 2) self.assertTrue(history.can_undo) self.assertFalse(history.can_redo) def test_add_middle_mergeable(self): history = UndoHistory() example = SimpleExample(str_value='foo', value=10) undo_item = UndoItem( object=example, name='str_value', old_value='bar', new_value='baz', ) history.add( UndoItem( object=example, name='str_value', old_value='foo', new_value='bar', ) ) history.add( UndoItem( object=example, name='str_value', old_value='bar', new_value='wombat', ), ) history.undo() with self.assertTraitDoesNotChange(history, 'undoable'): with self.assertTraitChanges(history, 'redoable', count=1): history.add(undo_item) self.assertEqual(history.now, 1) self.assertTrue(history.can_undo) self.assertFalse(history.can_redo) def test_undo_last(self): history = UndoHistory() self._populate_history(history) with self.assertTraitDoesNotChange(history, 'undoable'): with self.assertTraitChanges(history, 'redoable', count=1): with self.assertTraitChanges( self._example, 'anytrait', count=1 ): # noqa: E501 history.undo() self.assertEqual(history.now, 2) self.assertTrue(history.can_undo) self.assertTrue(history.can_redo) def test_undo_first(self): history = UndoHistory() self._populate_history(history) history.undo() history.undo() with self.assertTraitDoesNotChange(history, 'redoable'): with self.assertTraitChanges(history, 'undoable', count=1): with self.assertTraitChanges( self._example, 'anytrait', count=1 ): # noqa: E501 history.undo() self.assertEqual(history.now, 0) self.assertFalse(history.can_undo) self.assertTrue(history.can_redo) def test_undo_middle(self): history = UndoHistory() self._populate_history(history) history.undo() with self.assertTraitDoesNotChange(history, 'redoable'): with self.assertTraitDoesNotChange(history, 'undoable'): with self.assertTraitChanges( self._example, 'anytrait', count=1 ): # noqa: E501 history.undo() self.assertEqual(history.now, 1) self.assertTrue(history.can_undo) self.assertTrue(history.can_redo) def test_redo_last(self): history = UndoHistory() self._populate_history(history) history.undo() with self.assertTraitChanges(history, 'redoable', count=1): with self.assertTraitDoesNotChange(history, 'undoable'): with self.assertTraitChanges( self._example, 'anytrait', count=1 ): # noqa: E501 history.redo() self.assertEqual(history.now, 3) self.assertTrue(history.can_undo) self.assertFalse(history.can_redo) def test_redo_middle(self): history = UndoHistory() self._populate_history(history) history.undo() history.undo() with self.assertTraitDoesNotChange(history, 'redoable'): with self.assertTraitDoesNotChange(history, 'undoable'): with self.assertTraitChanges( self._example, 'anytrait', count=1 ): # noqa: E501 history.redo() self.assertEqual(history.now, 2) self.assertTrue(history.can_undo) self.assertTrue(history.can_redo) def test_redo_first(self): history = UndoHistory() self._populate_history(history) history.undo() history.undo() history.undo() with self.assertTraitDoesNotChange(history, 'redoable'): with self.assertTraitChanges(history, 'undoable', count=1): with self.assertTraitChanges( self._example, 'anytrait', count=1 ): # noqa: E501 history.redo() self.assertEqual(history.now, 1) self.assertTrue(history.can_undo) self.assertTrue(history.can_redo) def test_revert_end(self): history = UndoHistory() self._populate_history(history) with self.assertTraitChanges(history, 'redoable', count=2): with self.assertTraitChanges(history, 'undoable', count=1): with self.assertTraitChanges( self._example, 'anytrait', count=3 ): # noqa: E501 history.revert() self.assertEqual(history.now, 0) self.assertFalse(history.can_undo) self.assertFalse(history.can_redo) def test_revert_middle(self): history = UndoHistory() self._populate_history(history) history.undo() history.undo() with self.assertTraitChanges(history, 'redoable', count=1): with self.assertTraitChanges(history, 'undoable', count=1): with self.assertTraitChanges( self._example, 'anytrait', count=1 ): # noqa: E501 history.revert() self.assertEqual(history.now, 0) self.assertFalse(history.can_undo) self.assertFalse(history.can_redo) def test_revert_start(self): history = UndoHistory() self._populate_history(history) history.undo() history.undo() history.undo() with self.assertTraitDoesNotChange(history, 'undoable'): with self.assertTraitChanges(history, 'redoable', count=1): with self.assertTraitDoesNotChange(self._example, 'anytrait'): history.revert() self.assertEqual(history.now, 0) self.assertFalse(history.can_undo) self.assertFalse(history.can_redo) def test_clear_end(self): history = UndoHistory() self._populate_history(history) with self.assertTraitDoesNotChange(history, 'redoable'): with self.assertTraitChanges(history, 'undoable', count=1): with self.assertTraitDoesNotChange(self._example, 'anytrait'): history.clear() self.assertEqual(history.now, 0) self.assertFalse(history.can_undo) self.assertFalse(history.can_redo) def test_clear_middle(self): history = UndoHistory() self._populate_history(history) history.undo() history.undo() with self.assertTraitChanges(history, 'redoable', count=1): with self.assertTraitChanges(history, 'undoable', count=1): with self.assertTraitDoesNotChange(self._example, 'anytrait'): history.clear() self.assertEqual(history.now, 0) self.assertFalse(history.can_undo) self.assertFalse(history.can_redo) def test_clear_start(self): history = UndoHistory() self._populate_history(history) history.undo() history.undo() history.undo() with self.assertTraitDoesNotChange(history, 'undoable'): with self.assertTraitChanges(history, 'redoable', count=1): with self.assertTraitDoesNotChange(self._example, 'anytrait'): history.clear() self.assertEqual(history.now, 0) self.assertFalse(history.can_undo) self.assertFalse(history.can_redo) def test_extend(self): history = UndoHistory() example = SimpleExample(str_value='foo', value=10) undo_item = UndoItem( object=example, name='value', old_value=0, new_value=10, ) history.add( UndoItem( object=example, name='str_value', old_value='foo', new_value='bar', ) ) with self.assertTraitDoesNotChange(history, 'undoable'): with self.assertTraitDoesNotChange(history, 'redoable'): history.extend(undo_item) self.assertEqual(history.now, 1) self.assertTrue(history.can_undo) self.assertFalse(history.can_redo) def test_extend_merge(self): history = UndoHistory() example = SimpleExample(str_value='foo', value=10) undo_item = UndoItem( object=example, name='str_value', old_value='foo', new_value='baz', ) history.add( UndoItem( object=example, name='str_value', old_value='foo', new_value='bar', ) ) with self.assertTraitDoesNotChange(history, 'undoable'): with self.assertTraitDoesNotChange(history, 'redoable'): history.extend(undo_item) self.assertEqual(history.now, 1) self.assertTrue(history.can_undo) self.assertFalse(history.can_redo) # XXX this is testing private state to ensure merge happened self.assertEqual(len(history.stack._stack), 1) def test_general_command_do(self): history = UndoHistory() command = DummyCommand() history.add(command) self.assertEqual(command.data, "do") @unittest.skipIf(no_gui_test_assistant, "No GuiTestAssistant") class TestEditorUndo(BaseTestMixin, GuiTestAssistant, unittest.TestCase): def setUp(self): BaseTestMixin.setUp(self) GuiTestAssistant.setUp(self) def tearDown(self): GuiTestAssistant.tearDown(self) BaseTestMixin.tearDown(self) def check_history( self, editor, expected_history_now, expected_history_length ): # XXX this is testing private state return ( editor.ui.history.now == expected_history_now and len(editor.ui.history.stack._stack) == expected_history_length ) def undo(self, editor): self.gui.invoke_later(editor.ui.history.undo) self.event_loop_helper.event_loop_with_timeout() def redo(self, editor): self.gui.invoke_later(editor.ui.history.redo) self.event_loop_helper.event_loop_with_timeout() def test_undo(self): editor = create_editor() editor.prepare(None) editor.ui.history = UndoHistory() self.assertEqual(editor.old_value, "test") # Enter 'one' followed by 'two' with editor.updating_value(): self.gui.set_trait_later(editor.control, "control_value", "one") self.gui.set_trait_later(editor.control, "control_value", "two") # Perform an UNDO self.undo(editor) # Expect 2 items in history and pointer at first item self.assertEventuallyTrue( editor, "ui", functools.partial( self.check_history, expected_history_now=1, expected_history_length=2, ), timeout=5.0, ) # Perform a REDO self.redo(editor) # Expect 2 items in history and pointer at second item self.assertEventuallyTrue( editor, "ui", functools.partial( self.check_history, expected_history_now=2, expected_history_length=2, ), timeout=5.0, ) # Enter 'three' with editor.updating_value(): self.gui.set_trait_later(editor.control, "control_value", "three") # Perform an UNDO self.undo(editor) # Expect 3 items in history and pointer at second item self.assertEventuallyTrue( editor, "ui", functools.partial( self.check_history, expected_history_now=2, expected_history_length=3, ), timeout=5.0, ) # Enter 'four' with editor.updating_value(): self.gui.set_trait_later(editor.control, "control_value", "four") self.event_loop_helper.event_loop_with_timeout() # Expect 3 items in history and pointer at second item # Note: Modifying the history after an UNDO, clears the future, # hence, we expect 3 items in the history, not 4 self.assertEventuallyTrue( editor, "ui", functools.partial( self.check_history, expected_history_now=3, expected_history_length=3, ), timeout=5.0, ) # The following sequence after modifying the history had caused # the application to hang, verify it. # Perform an UNDO self.undo(editor) self.undo(editor) self.redo(editor) # Expect 3 items in history and pointer at second item self.assertEventuallyTrue( editor, "ui", functools.partial( self.check_history, expected_history_now=2, expected_history_length=3, ), timeout=5.0, ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/tests/test_view_application.py0000644000175100001730000001576100000000000024153 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! import unittest from pyface.timer.api import CallbackTimer from traits.api import HasTraits, Instance, Int from traitsui.api import Handler, Item, UIInfo, View, toolkit from traitsui.tests._tools import ( BaseTestMixin, GuiTestAssistant, is_qt, no_gui_test_assistant, ) class SimpleModel(HasTraits): cell = Int() class ClosableHandler(Handler): info = Instance(UIInfo) def init(self, info): self.info = info return True def test_close(self): self.info.ui.dispose() simple_view = View( Item("cell"), title="Enter IDs and conditions", buttons=["OK", "Cancel"] ) @unittest.skipIf(no_gui_test_assistant, "No GuiTestAssistant") class TestViewApplication(BaseTestMixin, GuiTestAssistant, unittest.TestCase): def setUp(self): BaseTestMixin.setUp(self) GuiTestAssistant.setUp(self) self.model = SimpleModel() self.handler = ClosableHandler() self.event_loop_timeout = False self.closed = False if is_qt(): if len(self.qt_app.topLevelWidgets()) > 0: with self.event_loop_with_timeout(repeat=5): self.gui.invoke_later(self.qt_app.closeAllWindows) def tearDown(self): GuiTestAssistant.tearDown(self) BaseTestMixin.tearDown(self) def view_application(self, kind, button=None): if button is None: self.gui.invoke_later(self.close_dialog) else: self.gui.invoke_later(self.click_button, text=button) timer = CallbackTimer.single_shot( callback=self.stop_event_loop, interval=1.0 ) try: self.result = toolkit().view_application( context=self.model, view=simple_view, kind=kind, handler=self.handler, ) finally: timer.stop() def view_application_event_loop(self, kind): with self.event_loop_until_condition(lambda: self.closed): self.gui.invoke_later( toolkit().view_application, context=self.model, view=simple_view, kind=kind, handler=self.handler, ) self.gui.invoke_after(100, self.close_dialog) def close_dialog(self): if is_qt(): self.handler.info.ui.control.close() self.closed = True else: raise NotImplementedError("Can't close current backend") def click_button(self, text): if is_qt(): from pyface.qt.QtGui import QPushButton from pyface.ui.qt.util.testing import find_qt_widget button = find_qt_widget( self.handler.info.ui.control, QPushButton, lambda widget: widget.text() == text, ) if button is None: raise RuntimeError("Can't find {} button".format(text)) button.click() self.closed = True else: raise NotImplementedError("Can't click current backend") def stop_event_loop(self): self.gui.stop_event_loop() self.event_loop_timeout = True def test_modal_view_application_close(self): self.view_application("modal") self.assertTrue(self.closed) self.assertFalse(self.event_loop_timeout) self.assertFalse(self.result) def test_nonmodal_view_application_close(self): self.view_application("nonmodal") self.assertTrue(self.closed) self.assertFalse(self.event_loop_timeout) self.assertTrue(self.result) def test_livemodal_view_application_close(self): self.view_application("livemodal") self.assertTrue(self.closed) self.assertFalse(self.event_loop_timeout) self.assertFalse(self.result) def test_live_view_application_close(self): self.view_application("live") self.assertTrue(self.closed) self.assertFalse(self.event_loop_timeout) self.assertTrue(self.result) def test_modal_view_application_ok(self): self.view_application("modal", button="OK") self.assertTrue(self.closed) self.assertFalse(self.event_loop_timeout) self.assertTrue(self.result) def test_nonmodal_view_application_ok(self): self.view_application("nonmodal", button="OK") self.assertTrue(self.closed) self.assertFalse(self.event_loop_timeout) self.assertTrue(self.result) def test_livemodal_view_application_ok(self): self.view_application("livemodal", button="OK") self.assertTrue(self.closed) self.assertFalse(self.event_loop_timeout) self.assertTrue(self.result) def test_live_view_application_ok(self): self.view_application("live", button="OK") self.assertTrue(self.closed) self.assertFalse(self.event_loop_timeout) self.assertTrue(self.result) def test_modal_view_application_cancel(self): self.view_application("modal", button="Cancel") self.assertTrue(self.closed) self.assertFalse(self.event_loop_timeout) self.assertFalse(self.result) def test_nonmodal_view_application_cancel(self): self.view_application("nonmodal", button="Cancel") self.assertTrue(self.closed) self.assertFalse(self.event_loop_timeout) self.assertFalse(self.result) def test_livemodal_view_application_cancel(self): self.view_application("livemodal", button="Cancel") self.assertTrue(self.closed) self.assertFalse(self.event_loop_timeout) self.assertFalse(self.result) def test_live_view_application_cancel(self): self.view_application("live", button="Cancel") self.assertTrue(self.closed) self.assertFalse(self.event_loop_timeout) self.assertFalse(self.result) def test_modal_view_application_eventloop_close(self): self.view_application_event_loop("modal") self.assertTrue(self.closed) self.assertFalse(self.event_loop_timeout) def test_nonmodal_view_application_eventloop_close(self): self.view_application_event_loop("nonmodal") self.assertTrue(self.closed) self.assertFalse(self.event_loop_timeout) def test_livemodal_view_application_eventloop_close(self): self.view_application_event_loop("livemodal") self.assertTrue(self.closed) self.assertFalse(self.event_loop_timeout) def test_live_view_application_eventloop_close(self): self.view_application_event_loop("live") self.assertTrue(self.closed) self.assertFalse(self.event_loop_timeout) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/tests/test_visible_when_layout.py0000644000175100001730000000651100000000000024662 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Test the layout when element appear and disappear with visible_when. """ import unittest from traits.has_traits import HasTraits from traits.trait_types import Enum, Bool, Str from traitsui.group import HGroup, VGroup from traitsui.include import Include from traitsui.item import Item from traitsui.view import View from traitsui.tests._tools import ( BaseTestMixin, create_ui, get_dialog_size, requires_toolkit, reraise_exceptions, ToolkitName, ) _TEXT_WIDTH = 200 _TEXT_HEIGHT = 100 class VisibleWhenProblem(HasTraits): which = Enum("one", "two") on = Bool() txt = Str() onoff_group = HGroup( VGroup( Item("on", resizable=False, width=-100, height=-70), show_left=False, show_border=True, visible_when='which == "one"', ) ) text_group = VGroup( Item("txt", width=-_TEXT_WIDTH, height=-_TEXT_HEIGHT), visible_when='which == "two"', show_border=True, ) traits_view = View( Item("which"), VGroup(Include("onoff_group"), Include("text_group")), resizable=True, buttons=["OK", "Cancel"], ) # XXX Not fixing on wx - CJW # This layout issue was fixed for Qt, but not for Wx. # See https://github.com/enthought/traitsui/pull/56 # This is cosmetic, not trivial to fix, and the Wx backend is currently low # priority. Patches which make this work on Wx will be gladly accepted, but # there are no current plans to work on this. class TestVisibleWhenLayout(BaseTestMixin, unittest.TestCase): def setUp(self): BaseTestMixin.setUp(self) def tearDown(self): BaseTestMixin.tearDown(self) @requires_toolkit([ToolkitName.qt]) def test_visible_when_layout(self): # Bug: The size of a dialog that contains elements that are activated # by "visible_when" can end up being the *sum* of the sizes of the # elements, even though the elements are mutually exclusive (e.g., # a typical case is a dropbox that lets you select different cases). # The expected behavior is that the size of the dialog should be at # most the size of the largest combination of elements. dialog = VisibleWhenProblem() with reraise_exceptions(), create_ui(dialog) as ui: # have the dialog switch from group one to two and back to one dialog.which = "two" dialog.which = "one" # the size of the window should not be larger than the largest # combination (in this case, the `text_group` plus the `which` item size = get_dialog_size(ui.control) # leave some margin for labels, dropbox, etc self.assertLess(size[0], _TEXT_WIDTH + 100) self.assertLess(size[1], _TEXT_HEIGHT + 150) if __name__ == "__main__": # Execute from command line for manual testing vw = VisibleWhenProblem(txt="ciao") ui = vw.configure_traits() ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1684768028.1158068 traitsui-8.0.0/traitsui/tests/ui_editors/0000755000175100001730000000000000000000000021341 5ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/tests/ui_editors/__init__.py0000644000175100001730000000000000000000000023440 0ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/tests/ui_editors/test_data_frame_editor.py0000644000175100001730000004123400000000000026407 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! import unittest try: import numpy as np except ImportError: raise unittest.SkipTest("Can't import NumPy: skipping") else: from numpy.testing import assert_array_equal try: from pandas import DataFrame except ImportError: raise unittest.SkipTest("Can't import Pandas: skipping") from traits.api import Event, HasTraits, Instance from traitsui.item import Item from traitsui.ui_editors.data_frame_editor import ( DataFrameEditor, DataFrameAdapter, ) from traitsui.view import View from traitsui.tests._tools import ( BaseTestMixin, create_ui, requires_toolkit, reraise_exceptions, ToolkitName, ) class DataFrameViewer(HasTraits): data = Instance("pandas.core.frame.DataFrame") view = View(Item("data", editor=DataFrameEditor(), width=400)) format_mapping_view = View( Item( "data", editor=DataFrameEditor(formats={"X": "%05d", "Y": "%s"}), width=400, ) ) font_mapping_view = View( Item( "data", editor=DataFrameEditor(fonts={"X": "Courier 10 bold", "Y": "Swiss"}), width=400, ) ) columns_view = View( Item( "data", editor=DataFrameEditor(columns=["X", ("Zed", "Z"), "missing"]), # noqa width=400, ) ) DATA = [[0, 1, 2], [3, 4, 5], [6, 7, 8], [9, 10, 11]] def sample_data(): df = DataFrame( DATA, index=["one", "two", "three", "four"], columns=["X", "Y", "Z"] ) viewer = DataFrameViewer(data=df) return viewer def sample_data_numerical_index(): df = DataFrame(DATA, index=list(range(1, 5)), columns=["X", "Y", "Z"]) viewer = DataFrameViewer(data=df) return viewer def sample_text_data(): data = [[0, 1, "two"], [3, 4, "five"], [6, 7, "eight"], [9, 10, "eleven"]] df = DataFrame( data, index=["one", "two", "three", "four"], columns=["X", "Y", "Z"] ) viewer = DataFrameViewer(data=df) return viewer class TestDataFrameEditor(BaseTestMixin, unittest.TestCase): def setUp(self): BaseTestMixin.setUp(self) def tearDown(self): BaseTestMixin.tearDown(self) @requires_toolkit([ToolkitName.qt, ToolkitName.wx]) def test_adapter_get_item(self): viewer = sample_data() adapter = DataFrameAdapter() item_0_df = adapter.get_item(viewer, "data", 0) assert_array_equal(item_0_df.values, [[0, 1, 2]]) assert_array_equal(item_0_df.columns, ["X", "Y", "Z"]) self.assertEqual(item_0_df.index[0], "one") @requires_toolkit([ToolkitName.qt, ToolkitName.wx]) def test_adapter_empty_dataframe(self): data = DataFrame() viewer = DataFrameViewer(data=data) adapter = DataFrameAdapter() item_0_df = adapter.get_item(viewer, "data", 0) assert_array_equal(item_0_df.values, np.array([]).reshape(0, 0)) assert_array_equal(item_0_df.columns, []) @requires_toolkit([ToolkitName.qt, ToolkitName.wx]) def test_adapter_no_rows(self): data = DataFrame(columns=["X", "Y", "Z"]) viewer = DataFrameViewer(data=data) adapter = DataFrameAdapter() item_0_df = adapter.get_item(viewer, "data", 0) assert_array_equal(item_0_df.values, np.array([]).reshape(0, 3)) assert_array_equal(item_0_df.columns, ["X", "Y", "Z"]) @requires_toolkit([ToolkitName.qt, ToolkitName.wx]) def test_adapter_get_item_numerical(self): viewer = sample_data_numerical_index() adapter = DataFrameAdapter() item_0_df = adapter.get_item(viewer, "data", 0) assert_array_equal(item_0_df.values, [[0, 1, 2]]) assert_array_equal(item_0_df.columns, ["X", "Y", "Z"]) self.assertEqual(item_0_df.index[0], 1) @requires_toolkit([ToolkitName.qt, ToolkitName.wx]) def test_adapter_delete_start(self): viewer = sample_data() adapter = DataFrameAdapter() adapter.delete(viewer, "data", 0) data = viewer.data assert_array_equal(data.values, [[3, 4, 5], [6, 7, 8], [9, 10, 11]]) assert_array_equal(data.columns, ["X", "Y", "Z"]) assert_array_equal(data.index, ["two", "three", "four"]) @requires_toolkit([ToolkitName.qt, ToolkitName.wx]) def test_adapter_delete_start_numerical_index(self): viewer = sample_data_numerical_index() adapter = DataFrameAdapter() adapter.delete(viewer, "data", 0) data = viewer.data assert_array_equal(data.values, [[3, 4, 5], [6, 7, 8], [9, 10, 11]]) assert_array_equal(data.columns, ["X", "Y", "Z"]) assert_array_equal(data.index, [2, 3, 4]) @requires_toolkit([ToolkitName.qt, ToolkitName.wx]) def test_adapter_delete_middle(self): viewer = sample_data() adapter = DataFrameAdapter() adapter.delete(viewer, "data", 1) data = viewer.data assert_array_equal(data.values, [[0, 1, 2], [6, 7, 8], [9, 10, 11]]) assert_array_equal(data.columns, ["X", "Y", "Z"]) assert_array_equal(data.index, ["one", "three", "four"]) @requires_toolkit([ToolkitName.qt, ToolkitName.wx]) def test_adapter_delete_middle_numerical_index(self): viewer = sample_data_numerical_index() adapter = DataFrameAdapter() adapter.delete(viewer, "data", 1) data = viewer.data assert_array_equal(data.values, [[0, 1, 2], [6, 7, 8], [9, 10, 11]]) assert_array_equal(data.columns, ["X", "Y", "Z"]) assert_array_equal(data.index, [1, 3, 4]) @requires_toolkit([ToolkitName.qt, ToolkitName.wx]) def test_adapter_delete_end(self): viewer = sample_data() adapter = DataFrameAdapter() adapter.delete(viewer, "data", 3) data = viewer.data assert_array_equal(data.values, [[0, 1, 2], [3, 4, 5], [6, 7, 8]]) assert_array_equal(data.columns, ["X", "Y", "Z"]) assert_array_equal(data.index, ["one", "two", "three"]) @requires_toolkit([ToolkitName.qt, ToolkitName.wx]) def test_adapter_delete_end_numerical_index(self): viewer = sample_data_numerical_index() adapter = DataFrameAdapter() adapter.delete(viewer, "data", 3) data = viewer.data assert_array_equal(data.values, [[0, 1, 2], [3, 4, 5], [6, 7, 8]]) assert_array_equal(data.columns, ["X", "Y", "Z"]) assert_array_equal(data.index, [1, 2, 3]) @requires_toolkit([ToolkitName.qt, ToolkitName.wx]) def test_adapter_insert_start(self): viewer = sample_data() adapter = DataFrameAdapter() item = DataFrame( [[-3, -2, -1]], index=["new"], columns=["X", "Y", "Z"] ) adapter.insert(viewer, "data", 0, item) data = viewer.data assert_array_equal( data.values, [[-3, -2, -1], [0, 1, 2], [3, 4, 5], [6, 7, 8], [9, 10, 11]], ) assert_array_equal(data.columns, ["X", "Y", "Z"]) assert_array_equal(data.index, ["new", "one", "two", "three", "four"]) @requires_toolkit([ToolkitName.qt, ToolkitName.wx]) def test_adapter_insert_start_numerical_index(self): viewer = sample_data_numerical_index() adapter = DataFrameAdapter() item = DataFrame([[-3, -2, -1]], index=[0], columns=["X", "Y", "Z"]) adapter.insert(viewer, "data", 0, item) data = viewer.data assert_array_equal( data.values, [[-3, -2, -1], [0, 1, 2], [3, 4, 5], [6, 7, 8], [9, 10, 11]], ) assert_array_equal(data.columns, ["X", "Y", "Z"]) assert_array_equal(data.index, [0, 1, 2, 3, 4]) @requires_toolkit([ToolkitName.qt, ToolkitName.wx]) def test_adapter_insert_middle(self): viewer = sample_data() adapter = DataFrameAdapter() item = DataFrame( [[-3, -2, -1]], index=["new"], columns=["X", "Y", "Z"] ) adapter.insert(viewer, "data", 1, item) data = viewer.data assert_array_equal( data.values, [[0, 1, 2], [-3, -2, -1], [3, 4, 5], [6, 7, 8], [9, 10, 11]], ) assert_array_equal(data.columns, ["X", "Y", "Z"]) assert_array_equal(data.index, ["one", "new", "two", "three", "four"]) @requires_toolkit([ToolkitName.qt, ToolkitName.wx]) def test_adapter_insert_middle_numerical_index(self): viewer = sample_data_numerical_index() adapter = DataFrameAdapter() item = DataFrame([[-3, -2, -1]], index=[0], columns=["X", "Y", "Z"]) adapter.insert(viewer, "data", 1, item) data = viewer.data assert_array_equal( data.values, [[0, 1, 2], [-3, -2, -1], [3, 4, 5], [6, 7, 8], [9, 10, 11]], ) assert_array_equal(data.columns, ["X", "Y", "Z"]) assert_array_equal(data.index, [1, 0, 2, 3, 4]) @requires_toolkit([ToolkitName.qt, ToolkitName.wx]) def test_adapter_insert_end(self): viewer = sample_data() adapter = DataFrameAdapter() item = DataFrame( [[-3, -2, -1]], index=["new"], columns=["X", "Y", "Z"] ) adapter.insert(viewer, "data", 5, item) data = viewer.data assert_array_equal( data.values, [[0, 1, 2], [3, 4, 5], [6, 7, 8], [9, 10, 11], [-3, -2, -1]], ) assert_array_equal(data.columns, ["X", "Y", "Z"]) assert_array_equal(data.index, ["one", "two", "three", "four", "new"]) @requires_toolkit([ToolkitName.qt, ToolkitName.wx]) def test_adapter_insert_end_numerical_index(self): viewer = sample_data_numerical_index() adapter = DataFrameAdapter() item = DataFrame([[-3, -2, -1]], index=[0], columns=["X", "Y", "Z"]) adapter.insert(viewer, "data", 5, item) data = viewer.data assert_array_equal( data.values, [[0, 1, 2], [3, 4, 5], [6, 7, 8], [9, 10, 11], [-3, -2, -1]], ) assert_array_equal(data.columns, ["X", "Y", "Z"]) assert_array_equal(data.index, [1, 2, 3, 4, 0]) @requires_toolkit([ToolkitName.qt, ToolkitName.wx]) def test_data_frame_editor(self): viewer = sample_data() with reraise_exceptions(), create_ui(viewer): pass @requires_toolkit([ToolkitName.qt, ToolkitName.wx]) def test_data_frame_editor_alternate_adapter(self): class AlternateAdapter(DataFrameAdapter): pass alternate_adapter_view = View( Item( "data", editor=DataFrameEditor(adapter=AlternateAdapter()), width=400, ) ) viewer = sample_data() with reraise_exceptions(), create_ui( viewer, dict(view=alternate_adapter_view) ): pass @requires_toolkit([ToolkitName.qt, ToolkitName.wx]) def test_data_frame_editor_numerical_index(self): viewer = sample_data_numerical_index() with reraise_exceptions(), create_ui(viewer): pass @requires_toolkit([ToolkitName.qt, ToolkitName.wx]) def test_data_frame_editor_text_data(self): viewer = sample_text_data() with reraise_exceptions(), create_ui(viewer): pass @requires_toolkit([ToolkitName.qt, ToolkitName.wx]) def test_data_frame_editor_format_mapping(self): viewer = sample_data() with reraise_exceptions(), create_ui( viewer, dict(view=format_mapping_view) ): pass @requires_toolkit([ToolkitName.qt, ToolkitName.wx]) def test_data_frame_editor_font_mapping(self): viewer = sample_data() with reraise_exceptions(), create_ui( viewer, dict(view=font_mapping_view) ): pass @requires_toolkit([ToolkitName.qt, ToolkitName.wx]) def test_data_frame_editor_columns(self): viewer = sample_data() with reraise_exceptions(), create_ui(viewer, dict(view=columns_view)): pass @requires_toolkit([ToolkitName.qt, ToolkitName.wx]) def test_data_frame_editor_with_update_refresh(self): class DataFrameViewer(HasTraits): data = Instance(DataFrame) df_updated = Event() view = View( Item("data", editor=DataFrameEditor(update="df_updated")) ) df = DataFrame( DATA, index=["one", "two", "three", "four"], columns=["X", "Y", "Z"], ) viewer = DataFrameViewer(data=df) with reraise_exceptions(), create_ui(viewer): viewer.df_updated = True @requires_toolkit([ToolkitName.qt, ToolkitName.wx]) def test_data_frame_editor_with_refresh(self): class DataFrameViewer(HasTraits): data = Instance(DataFrame) df_refreshed = Event() view = View( Item("data", editor=DataFrameEditor(refresh="df_refreshed")) ) df = DataFrame( DATA, index=["one", "two", "three", "four"], columns=["X", "Y", "Z"], ) viewer = DataFrameViewer(data=df) with reraise_exceptions(), create_ui(viewer): viewer.df_refreshed = True @requires_toolkit([ToolkitName.qt, ToolkitName.wx]) def test_data_frame_editor_multi_select(self): view = View( Item("data", editor=DataFrameEditor(multi_select=True), width=400) ) viewer = sample_data() with reraise_exceptions(), create_ui(viewer, dict(view=view)): pass @requires_toolkit([ToolkitName.qt, ToolkitName.wx]) def test_adapter_set_text(self): viewer = sample_data() columns = [(column, column) for column in viewer.data.columns] adapter = DataFrameAdapter(columns=columns) adapter.set_text(viewer, 'data', 0, 0, '10') item_0_df = adapter.get_item(viewer, 'data', 0) assert_array_equal(item_0_df.values, [[10, 1, 2]]) assert_array_equal(item_0_df.columns, ['X', 'Y', 'Z']) @requires_toolkit([ToolkitName.qt, ToolkitName.wx]) def test_adapter_set_text_invalid(self): viewer = sample_data() columns = [(column, column) for column in viewer.data.columns] adapter = DataFrameAdapter(columns=columns) adapter.set_text(viewer, 'data', 0, 0, 'invalid') # expect no error, and values unchanged item_0_df = adapter.get_item(viewer, 'data', 0) assert_array_equal(item_0_df.values, [[0, 1, 2]]) assert_array_equal(item_0_df.columns, ['X', 'Y', 'Z']) @requires_toolkit([ToolkitName.qt, ToolkitName.wx]) def test_adapter_set_index_text(self): viewer = sample_data() columns = [('', 'index')] + [ (column, column) for column in viewer.data.columns ] adapter = DataFrameAdapter(columns=columns) adapter.set_text(viewer, 'data', 0, 0, 'NewIndex') item_0_df = adapter.get_item(viewer, 'data', 0) assert_array_equal(item_0_df.values, [[0, 1, 2]]) assert_array_equal(item_0_df.columns, ['X', 'Y', 'Z']) self.assertEqual(item_0_df.index[0], 'NewIndex') @requires_toolkit([ToolkitName.qt, ToolkitName.wx]) def test_adapter_set_index_text_numeric(self): viewer = sample_data_numerical_index() columns = [('', 'index')] + [ (column, column) for column in viewer.data.columns ] adapter = DataFrameAdapter(columns=columns) adapter.set_text(viewer, 'data', 0, 0, 100) item_0_df = adapter.get_item(viewer, 'data', 0) assert_array_equal(item_0_df.values, [[0, 1, 2]]) assert_array_equal(item_0_df.columns, ['X', 'Y', 'Z']) self.assertEqual(item_0_df.index[0], 100) @requires_toolkit([ToolkitName.qt, ToolkitName.wx]) def test_adapter_set_index_text_numeric_invalid(self): viewer = sample_data_numerical_index() columns = [('', 'index')] + [ (column, column) for column in viewer.data.columns ] adapter = DataFrameAdapter(columns=columns) adapter.set_text(viewer, 'data', 0, 0, 'invalid') item_0_df = adapter.get_item(viewer, 'data', 0) assert_array_equal(item_0_df.values, [[0, 1, 2]]) assert_array_equal(item_0_df.columns, ['X', 'Y', 'Z']) self.assertEqual(item_0_df.index[0], 1) def test_scroll_to_row_hint_warnings(self): with self.assertWarns(DeprecationWarning): dfe = DataFrameEditor(scroll_to_row_hint="center") with self.assertWarns(DeprecationWarning): dfe.scroll_to_row_hint ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/theme.py0000644000175100001730000000553200000000000017512 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Defines 'theme' related classes. """ from traits.api import HasPrivateTraits, Property, cached_property from traits.etsconfig.api import ETSConfig from .ui_traits import Image, HasBorder, HasMargin, Alignment class Theme(HasPrivateTraits): # -- Public Traits -------------------------------------------------------- #: The background image to use for the theme: image = Image #: The border inset: border = HasBorder #: The margin to use around the content: content = HasMargin #: The margin to use around the label: label = HasMargin #: The alignment to use for positioning the label: alignment = Alignment(cols=4) #: The color to use for content text (Wx only) content_color = Property() #: The color to use for label text (Wx only) label_color = Property() #: The image slice used to draw the theme (Wx only) image_slice = Property(observe="image") # -- Constructor ---------------------------------------------------------- def __init__(self, image=None, **traits): """Initializes the object.""" if image is not None: self.image = image super().__init__(**traits) # -- Property Implementations --------------------------------------------- def _get_content_color(self): if ETSConfig.toolkit == "wx": import wx if self._content_color is None: color = wx.BLACK islice = self.image_slice if islice is not None: color = islice.content_color self._content_color = color return self._content_color def _set_content_color(self, color): self._content_color = color def _get_label_color(self): if ETSConfig.toolkit == "wx": import wx if self._label_color is None: color = wx.BLACK islice = self.image_slice if islice is not None: color = islice.label_color self._label_color = color return self._label_color def _set_label_color(self, color): self._label_color = color @cached_property def _get_image_slice(self): if self.image is None: return None if ETSConfig.toolkit == "wx": from traitsui.wx.image_slice import image_slice_for return image_slice_for(self.image) #: The default theme: default_theme = Theme() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/toolkit.py0000644000175100001730000002773600000000000020107 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Defines the stub functions used for creating concrete implementations of the standard EditorFactory subclasses supplied with the Traits package. Most of the logic for determining which backend toolkit to use can now be found in :py:mod:`pyface.base_toolkit`. """ import logging from traits.trait_base import ETSConfig from pyface.base_toolkit import Toolkit, find_toolkit logger = logging.getLogger(__name__) not_implemented_message = "the '{}' toolkit does not implement this method" #: The current GUI toolkit object being used: _toolkit = None def assert_toolkit_import(names): """Raise an error if a toolkit with the given name should not be allowed to be imported. """ if ETSConfig.toolkit and ETSConfig.toolkit not in names: raise RuntimeError( "Importing from %s backend after selecting %s " "backend!" % (names[0], ETSConfig.toolkit) ) def toolkit_object(name, raise_exceptions=False): """Return the toolkit specific object with the given name. Parameters ---------- name : str The relative module path and the object name separated by a colon. raise_exceptions : bool Whether or not to raise an exception if the name cannot be imported. Raises ------ TraitError If no working toolkit is found. RuntimeError If no ETSConfig.toolkit is set but the toolkit cannot be loaded for some reason. This is also raised if raise_exceptions is True the backend does not implement the desired object. """ global _toolkit if _toolkit is None: toolkit() obj = _toolkit(name) if raise_exceptions and obj.__name__ == "Unimplemented": raise RuntimeError( "Can't import {} for backend {}".format( repr(name), _toolkit.toolkit ) ) return obj def toolkit(*toolkits): """Selects and returns a low-level GUI toolkit. Use this function to get a reference to the current toolkit. Parameters ---------- *toolkits : strings Toolkit names to try if toolkit not already selected. If not supplied, will try all ``traitsui.toolkits`` entry points until a match is found. Returns ------- toolkit Appropriate concrete Toolkit subclass for selected toolkit. Raises ------ TraitError If no working toolkit is found. RuntimeError If no ETSConfig.toolkit is set but the toolkit cannot be loaded for some reason. """ global _toolkit if _toolkit is None: if len(toolkits) > 0: _toolkit = find_toolkit("traitsui.toolkits", toolkits) else: _toolkit = find_toolkit("traitsui.toolkits") return _toolkit class Toolkit(Toolkit): """Abstract base class for GUI toolkits.""" def ui_panel(self, ui, parent): """Creates a GUI-toolkit-specific panel-based user interface using information from the specified UI object. """ raise NotImplementedError( not_implemented_message.format(ETSConfig.toolkit) ) def ui_subpanel(self, ui, parent): """Creates a GUI-toolkit-specific subpanel-based user interface using information from the specified UI object. """ raise NotImplementedError( not_implemented_message.format(ETSConfig.toolkit) ) def ui_livemodal(self, ui, parent): """Creates a GUI-toolkit-specific modal "live update" dialog user interface using information from the specified UI object. """ raise NotImplementedError( not_implemented_message.format(ETSConfig.toolkit) ) def ui_live(self, ui, parent): """Creates a GUI-toolkit-specific non-modal "live update" window user interface using information from the specified UI object. """ raise NotImplementedError( not_implemented_message.format(ETSConfig.toolkit) ) def ui_modal(self, ui, parent): """Creates a GUI-toolkit-specific modal dialog user interface using information from the specified UI object. """ raise NotImplementedError( not_implemented_message.format(ETSConfig.toolkit) ) def ui_nonmodal(self, ui, parent): """Creates a GUI-toolkit-specific non-modal dialog user interface using information from the specified UI object. """ raise NotImplementedError( not_implemented_message.format(ETSConfig.toolkit) ) def ui_popup(self, ui, parent): """Creates a GUI-toolkit-specific temporary "live update" popup dialog user interface using information from the specified UI object. """ raise NotImplementedError( not_implemented_message.format(ETSConfig.toolkit) ) def ui_popover(self, ui, parent): """Creates a GUI-toolkit-specific temporary "live update" popup dialog user interface using information from the specified UI object. """ raise NotImplementedError( not_implemented_message.format(ETSConfig.toolkit) ) def ui_info(self, ui, parent): """Creates a GUI-toolkit-specific temporary "live update" popup dialog user interface using information from the specified UI object. """ raise NotImplementedError( not_implemented_message.format(ETSConfig.toolkit) ) def ui_wizard(self, ui, parent): """Creates a GUI-toolkit-specific wizard dialog user interface using information from the specified UI object. """ raise NotImplementedError( not_implemented_message.format(ETSConfig.toolkit) ) def view_application( self, context, view, kind=None, handler=None, id="", scrollable=None, args=None, ): """Creates a GUI-toolkit-specific 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. """ raise NotImplementedError( not_implemented_message.format(ETSConfig.toolkit) ) def position(self, ui): """Positions the associated dialog window on the display.""" raise NotImplementedError( not_implemented_message.format(ETSConfig.toolkit) ) def show_help(self, ui, control): """Shows a Help window for a specified UI and control.""" raise NotImplementedError( not_implemented_message.format(ETSConfig.toolkit) ) def set_title(self, ui): """Sets the title for the UI window.""" raise NotImplementedError( not_implemented_message.format(ETSConfig.toolkit) ) def set_icon(self, ui): """Sets the icon for the UI window.""" raise NotImplementedError( not_implemented_message.format(ETSConfig.toolkit) ) def save_window(self, ui): """Saves user preference information associated with a UI window.""" raise NotImplementedError( not_implemented_message.format(ETSConfig.toolkit) ) def rebuild_ui(self, ui): """Rebuilds a UI after a change to the content of the UI.""" raise NotImplementedError( not_implemented_message.format(ETSConfig.toolkit) ) def key_event_to_name(self, event): """Converts a keystroke event into a corresponding key name.""" raise NotImplementedError( not_implemented_message.format(ETSConfig.toolkit) ) def hook_events(self, ui, control, events=None, handler=None): """Hooks all specified events for all controls in a UI so that they can be routed to the correct event handler. """ raise NotImplementedError( not_implemented_message.format(ETSConfig.toolkit) ) def route_event(self, ui, event): """Routes a "hooked" event to the corrent handler method.""" raise NotImplementedError( not_implemented_message.format(ETSConfig.toolkit) ) def skip_event(self, event): """Indicates that an event should continue to be processed by the toolkit. """ raise NotImplementedError( not_implemented_message.format(ETSConfig.toolkit) ) def destroy_control(self, control): """Destroys a specified GUI toolkit control.""" raise NotImplementedError( not_implemented_message.format(ETSConfig.toolkit) ) def destroy_children(self, control): """Destroys all of the child controls of a specified GUI toolkit control. """ raise NotImplementedError( not_implemented_message.format(ETSConfig.toolkit) ) def image_size(self, image): """Returns a ( width, height ) tuple containing the size of a specified toolkit image. """ raise NotImplementedError( not_implemented_message.format(ETSConfig.toolkit) ) 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. """ raise NotImplementedError( not_implemented_message.format(ETSConfig.toolkit) ) # ------------------------------------------------------------------------- # GUI toolkit dependent trait definitions: # ------------------------------------------------------------------------- def color_trait(self, *args, **traits): raise NotImplementedError( not_implemented_message.format(ETSConfig.toolkit) ) def rgb_color_trait(self, *args, **traits): raise NotImplementedError( not_implemented_message.format(ETSConfig.toolkit) ) def rgba_color_trait(self, *args, **traits): raise NotImplementedError( not_implemented_message.format(ETSConfig.toolkit) ) def font_trait(self, *args, **traits): raise NotImplementedError( not_implemented_message.format(ETSConfig.toolkit) ) def kiva_font_trait(self, *args, **traits): raise NotImplementedError( not_implemented_message.format(ETSConfig.toolkit) ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/toolkit_traits.py0000644000175100001730000000607100000000000021462 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! from traits.api import TraitFactory from .toolkit import toolkit def ColorTrait(*args, **traits): """Returns a trait whose value is a GUI toolkit-specific color. A number of different values are accepted for setting the value, including: * tuples of the form (r, g, b) and (r, g, b, a) * strings which match standard color names * strings of the form "(r, g, b)" and "(r, g, b, a)" * integers whose hex value is of the form 0xRRGGBB * toolkit-specific color classes Tuple values are expected to be in the range 0 to 255. Exact behaviour (eg. precisely what values are accepted, and what the "standard" color names are) is toolkit-dependent. The default value is white. The default editor is a ColorEditor. Parameters ---------- default: color The default color for the trait. allow_none: bool Whether to allow None as a value. **metadata Trait metadata to be passed through. """ return toolkit().color_trait(*args, **traits) def RGBColorTrait(*args, **traits): """Returns a trait whose value is a RGB tuple with values from 0 to 1. A number of different values are accepted for setting the value, including: * tuples of the form (r, g, b) with values from 0.0 to 1.0 * strings which match standard color names * integers whose hex value is of the form 0xRRGGBB The default value is (1.0, 1.0, 1.0). The default editor is a RGBColorEditor. Parameters ---------- **metadata Trait metadata to be passed through. """ return toolkit().rgb_color_trait(*args, **traits) def FontTrait(*args, **traits): """Returns a trait whose value is a GUI toolkit-specific font. This trait accepts either a toolkit-specific font object, or a string containing a font description. The string description can contain: * a font name or family. The following generic names are understood: "default", "decorative", "roman", "script", "swiss", and "modern". * a size, in points. * a style, which is one of: "slant" or "italic" * a weight, which is one of: "light" or "bold" * whether the font is underlined, indicated by the inclusion of "underlined". Where values aren't supplied, the application defaults will be used instead. The default value is the application default font, which is toolkit and platform dependent. The default editor is FontEditor. Parameters ---------- **metadata Trait metadata to be passed through. """ return toolkit().font_trait(*args, **traits) Color = TraitFactory(ColorTrait) RGBColor = TraitFactory(RGBColorTrait) Font = TraitFactory(FontTrait) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/tree_node.py0000644000175100001730000015544600000000000020366 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Defines the tree node descriptor used by the tree editor and tree editor factory classes. """ from itertools import zip_longest from traits.api import ( Adapter, Any, Bool, Callable, Dict, HasPrivateTraits, Instance, Interface, isinterface, List, Property, Str, Supports, Union, cached_property, ) from traits.trait_base import ( SequenceTypes, get_resource_path, xgetattr, xsetattr, ) from .ui_traits import AView # ------------------------------------------------------------------------- # 'TreeNode' class: # ------------------------------------------------------------------------- class TreeNode(HasPrivateTraits): """Represents a tree node. Used by the tree editor and tree editor factory classes. """ # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- #: Name of trait containing children (if '', the node is a leaf). Nested #: attributes are allowed, e.g., 'library.books' children = Str() #: Either the name of a trait containing a label, or a constant label, if #: the string starts with '='. label = Str() #: The name of a trait containing a list of labels for any columns. column_labels = Str() #: Either the name of a trait containing a tooltip, or constant tooltip, if #: the string starts with '='. tooltip = Str() #: Name to use for a new instance name = Str() #: Can the object's children be renamed? rename = Bool(True) #: Can the object be renamed? rename_me = Bool(True) #: Can the object's children be copied? copy = Bool(True) #: Can the object's children be deleted? delete = Bool(True) #: Can the object be deleted (if its parent allows it)? delete_me = Bool(True) #: Can children be inserted (vs. appended)? insert = Bool(True) #: Should tree nodes be automatically opened (expanded)? auto_open = Bool(False) #: Automatically close sibling tree nodes? auto_close = Bool(False) #: List of object classes than can be added or copied. Elements of the list #: can be either: #: #: - the klass itself that can be added or copied #: - 2-tuples of the form (klass, prompt) in which case klass is as above #: and prompt is a boolean indicating whether or not to prompt the user #: to specify trait values after instantiation when adding the object. #: - 3-tuples of the form (klass, prompt, factory) in which case klass and #: prompt are as above. factory is a callable that is expected to return #: an instance of klass. Useful if klass has required traits. Otherwise #: added object is created by simply instantiating klass() #: ref: enthought/traits#1502 add = List(Any) #: List of object classes that can be moved move = List(Any) #: List of object classes and/or interfaces that the node applies to node_for = List(Any) #: Tuple of object classes that the node applies to node_for_class = Property(observe="node_for") #: List of object interfaces that the node applies to node_for_interface = Property(observe="node_for") #: Function for formatting the label formatter = Callable() #: Functions for formatting the other columns. column_formatters = List(Union(None, Callable)) #: Function for formatting the tooltip tooltip_formatter = Callable() #: Function for handling selecting an object on_select = Callable() #: Function for handling clicking an object on_click = Callable() #: Function for handling double-clicking an object on_dclick = Callable() #: Function for handling activation of an object #: (double-click or Enter key press when node is in focus) on_activated = Callable() #: View to use for editing the object view = AView #: Right-click context menu. The value can be one of: #: #: - Instance( Menu ): Use this menu as the context menu #: - None: Use the default context menu #: - False: Do not display a context menu menu = Any() #: Name of leaf item icon icon_item = Str("") #: Name of group item icon icon_group = Str("") #: Name of opened group item icon icon_open = Str("") #: Resource path used to locate the node icon icon_path = Str() #: Selector or name for background color background = Any() #: Selector or name for foreground color foreground = Any() # fixme: The 'menu' trait should really be defined as: # Instance( 'traitsui.menu.MenuBar' ), but it doesn't work # right currently. #: A toolkit-appropriate cell renderer (currently Qt only) renderer = Any() #: A cache for listeners that need to keep state. _listener_cache = Dict() def __init__(self, **traits): super().__init__(**traits) if self.icon_path == "": self.icon_path = get_resource_path() # -- Property Implementations --------------------------------------------- @cached_property def _get_node_for_class(self): return tuple( [klass for klass in self.node_for if not isinterface(klass)] ) @cached_property def _get_node_for_interface(self): return [klass for klass in self.node_for if isinterface(klass)] # -- Overridable Methods: ------------------------------------------------- def allows_children(self, object): """Returns whether this object can have children.""" return self.children != "" def has_children(self, object): """Returns whether the object has children.""" return len(self.get_children(object)) > 0 def get_children(self, object): """Gets the object's children.""" return getattr(object, self.children, None) def get_children_id(self, object): """Gets the object's children identifier.""" return self.children def append_child(self, object, child): """Appends a child to the object's children.""" children = self.get_children(object) children.append(child) def insert_child(self, object, index, child): """Inserts a child into the object's children.""" children = self.get_children(object) children[index:index] = [child] def confirm_delete(self, object): """Checks whether a specified object can be deleted. The following return values are possible: - **True** if the object should be deleted with no further prompting. - **False** if the object should not be deleted. - Anything else: Caller should take its default action (which might include prompting the user to confirm deletion). """ return None def delete_child(self, object, index): """Deletes a child at a specified index from the object's children.""" del self.get_children(object)[index] def when_children_replaced(self, object, listener, remove): """Sets up or removes a listener for children being replaced on a specified object. """ object.on_trait_change( listener, self.children, remove=remove, dispatch="ui" ) def when_children_changed(self, object, listener, remove): """Sets up or removes a listener for children being changed on a specified object. """ object.on_trait_change( listener, self.children + "_items", remove=remove, dispatch="ui", ) def get_label(self, object): """Gets the label to display for a specified object.""" label = self.label if label[:1] == "=": return label[1:] label = xgetattr(object, label, "") if self.formatter is None: return label return self.formatter(object, label) def set_label(self, object, label): """Sets the label for a specified object.""" label_name = self.label if label_name[:1] != "=": xsetattr(object, label_name, label) def when_label_changed(self, object, listener, remove): """Sets up or removes a listener for the label being changed on a specified object. """ label = self.label if label[:1] != "=": memo = ("label", label, object, listener) if not remove: def wrapped_listener(target, name, new): """Ensure listener gets called with correct object.""" return listener(object, name, new) self._listener_cache[memo] = wrapped_listener else: wrapped_listener = self._listener_cache.pop(memo, None) if wrapped_listener is None: return object.on_trait_change( wrapped_listener, label, remove=remove, dispatch="ui" ) def get_column_labels(self, object): """Get the labels for any columns that have been defined.""" trait = self.column_labels labels = xgetattr(object, trait, []) formatted = [] for formatter, label in zip_longest(self.column_formatters, labels): # If the list of column formatters is shorter than the list of # labels, then zip_longest() will extend it with Nones. Just pass # the label as preformatted. Similarly, explicitly using None in # the list will pass through the item. if formatter is None: formatted.append(label) else: formatted.append(formatter(label)) return formatted def when_column_labels_change(self, object, listener, remove): """Sets up or removes a listener for the column labels being changed on a specified object. This will fire when either the list is reassigned or when it is modified. I.e., it listens both to the trait change event and the trait_items change event. Implement the listener appropriately to handle either case. """ trait = self.column_labels if trait != "": memo = ("column_label", trait, object, listener) if not remove: def wrapped_listener(target, name, new): """Ensure listener gets called with correct object.""" return listener(object, name, new) self._listener_cache[memo] = wrapped_listener else: wrapped_listener = self._listener_cache.pop(memo, None) if wrapped_listener is None: return object.on_trait_change( wrapped_listener, trait, remove=remove, dispatch="ui" ) object.on_trait_change( wrapped_listener, trait + "_items", remove=remove, dispatch="ui", ) def get_tooltip(self, object): """Gets the tooltip to display for a specified object.""" tooltip = self.tooltip if tooltip == "": return tooltip if tooltip[:1] == "=": return tooltip[1:] tooltip = xgetattr(object, tooltip, "") if self.tooltip_formatter is None: return tooltip return self.tooltip_formatter(object, tooltip) def get_icon(self, object, is_expanded): """Returns the icon for a specified object.""" if not self.allows_children(object): return self.icon_item if is_expanded: return self.icon_open return self.icon_group def get_icon_path(self, object): """Returns the path used to locate an object's icon.""" return self.icon_path def get_name(self, object): """Returns the name to use when adding a new object instance (displayed in the "New" submenu). """ return self.name def get_view(self, object): """Gets the view to use when editing an object.""" return self.view def get_menu(self, object): """Returns the right-click context menu for an object.""" return self.menu def get_background(self, object): background = self.background if isinstance(background, str): background = xgetattr(object, background, default=background) return background def get_foreground(self, object): foreground = self.foreground if isinstance(foreground, str): foreground = xgetattr(object, foreground, default=foreground) return foreground def get_renderer(self, object, column=0): """Return the renderer for the object and column.""" return self.renderer def can_rename(self, object): """Returns whether the object's children can be renamed.""" return self.rename def can_rename_me(self, object): """Returns whether the object can be renamed.""" return self.rename_me def can_copy(self, object): """Returns whether the object's children can be copied.""" return self.copy def can_delete(self, object): """Returns whether the object's children can be deleted.""" return self.delete def can_delete_me(self, object): """Returns whether the object can be deleted.""" return self.delete_me def can_insert(self, object): """Returns whether the object's children can be inserted (vs. appended). """ return self.insert def can_auto_open(self, object): """Returns whether the object's children should be automatically opened. """ return self.auto_open def can_auto_close(self, object): """Returns whether the object's children should be automatically closed. """ return self.auto_close def is_node_for(self, object): """Returns whether this is the node that handles a specified object.""" return isinstance( object, self.node_for_class ) or object.has_traits_interface(*self.node_for_interface) def can_add(self, object, add_object): """Returns whether a given object is droppable on the node.""" klass = self._class_for(add_object) if self.is_addable(klass): return True for item in self.move: if type(item) in SequenceTypes: item = item[0] if issubclass(klass, item): return True return False def get_add(self, object): """Returns the list of classes that can be added to the object.""" return self.add def get_drag_object(self, object): """Returns a draggable version of a specified object.""" return object def drop_object(self, object, dropped_object): """Returns a droppable version of a specified object.""" klass = self._class_for(dropped_object) if self.is_addable(klass): return dropped_object for item in self.move: if type(item) in SequenceTypes: if issubclass(klass, item[0]): return item[1](object, dropped_object) elif issubclass(klass, item): return dropped_object return dropped_object def select(self, object): """Handles an object being selected.""" if self.on_select is not None: self.on_select(object) return None return True def click(self, object): """Handles an object being clicked.""" if self.on_click is not None: self.on_click(object) return None return True def dclick(self, object): """Handles an object being double-clicked.""" if self.on_dclick is not None: self.on_dclick(object) return None return True def activated(self, object): """Handles an object being activated.""" if self.on_activated is not None: self.on_activated(object) return None return True def is_addable(self, klass): """Returns whether a specified object class can be added to the node.""" for item in self.add: if type(item) in SequenceTypes: item = item[0] if issubclass(klass, item): return True return False def _class_for(self, object): """Returns the class of an object.""" if isinstance(object, type): return object return object.__class__ # ------------------------------------------------------------------------- # 'ITreeNode' class # ------------------------------------------------------------------------- class ITreeNode(Interface): def allows_children(self): """Returns whether this object can have children.""" def has_children(self): """Returns whether the object has children.""" def get_children(self): """Gets the object's children.""" def get_children_id(self): """Gets the object's children identifier.""" def append_child(self, child): """Appends a child to the object's children.""" def insert_child(self, index, child): """Inserts a child into the object's children.""" def confirm_delete(self): """Checks whether a specified object can be deleted. The following return values are possible: - **True** if the object should be deleted with no further prompting. - **False** if the object should not be deleted. - Anything else: Caller should take its default action (which might include prompting the user to confirm deletion). """ def delete_child(self, index): """Deletes a child at a specified index from the object's children.""" def when_children_replaced(self, listener, remove): """Sets up or removes a listener for children being replaced on a specified object. """ def when_children_changed(self, listener, remove): """Sets up or removes a listener for children being changed on a specified object. """ def get_label(self): """Gets the label to display for a specified object.""" def set_label(self, label): """Sets the label for a specified object.""" def when_label_changed(self, listener, remove): """Sets up or removes a listener for the label being changed on a specified object. """ def get_column_labels(self, object): """Get the labels for any columns that have been defined.""" def when_column_labels_change(self, object, listener, remove): """Sets up or removes a listener for the column labels being changed on a specified object. This will fire when either the list is reassigned or when it is modified. I.e., it listens both to the trait change event and the trait_items change event. Implement the listener appropriately to handle either case. """ def get_tooltip(self): """Gets the tooltip to display for a specified object.""" def get_icon(self, is_expanded): """Returns the icon for a specified object.""" def get_icon_path(self): """Returns the path used to locate an object's icon.""" def get_name(self): """Returns the name to use when adding a new object instance (displayed in the "New" submenu). """ def get_view(self): """Gets the view to use when editing an object.""" def get_menu(self): """Returns the right-click context menu for an object.""" def can_rename(self): """Returns whether the object's children can be renamed.""" def can_rename_me(self): """Returns whether the object can be renamed.""" def can_copy(self): """Returns whether the object's children can be copied.""" def can_delete(self): """Returns whether the object's children can be deleted.""" def can_delete_me(self): """Returns whether the object can be deleted.""" def can_insert(self): """Returns whether the object's children can be inserted (vs. appended). """ def can_auto_open(self): """Returns whether the object's children should be automatically opened. """ def can_auto_close(self): """Returns whether the object's children should be automatically closed. """ def can_add(self, add_object): """Returns whether a given object is droppable on the node.""" def get_add(self): """Returns the list of classes that can be added to the object.""" def get_drag_object(self): """Returns a draggable version of a specified object.""" def drop_object(self, dropped_object): """Returns a droppable version of a specified object.""" def select(self): """Handles an object being selected.""" def click(self): """Handles an object being clicked.""" def dclick(self): """Handles an object being double-clicked.""" def activated(self): """Handles an object being activated.""" # ------------------------------------------------------------------------- # 'ITreeNodeAdapter' class # ------------------------------------------------------------------------- class ITreeNodeAdapter(Adapter): """Abstract base class for an adapter that implements the ITreeNode interface. Usage: 1. Create a subclass of ITreeNodeAdapter. 2. Register the adapter to define what class (or classes) this is an ITreeNode adapter for (ie. ``register_factory(, ITreeNode, ITreeNodeAdapter)``). 3. Override any of the following methods as necessary, using the ``self.adaptee`` trait to access the adapted object if needed. Note ---- This base class implements all of the ITreeNode interface methods, but does not necessarily provide useful implementations for all of the methods. It allows you to get a new adapter class up and running quickly, but you should carefully review your final adapter implementation class to make sure it behaves correctly in your application. """ def allows_children(self): """Returns whether this object can have children.""" return False def has_children(self): """Returns whether the object has children.""" return False def get_children(self): """Gets the object's children.""" return [] def get_children_id(self): """Gets the object's children identifier.""" return "" def append_child(self, child): """Appends a child to the object's children.""" pass def insert_child(self, index, child): """Inserts a child into the object's children.""" pass def confirm_delete(self): """Checks whether a specified object can be deleted. The following return values are possible: - **True** if the object should be deleted with no further prompting. - **False** if the object should not be deleted. - Anything else: Caller should take its default action (which might include prompting the user to confirm deletion). """ return False def delete_child(self, index): """Deletes a child at a specified index from the object's children.""" pass def when_children_replaced(self, listener, remove): """Sets up or removes a listener for children being replaced on a specified object. """ pass def when_children_changed(self, listener, remove): """Sets up or removes a listener for children being changed on a specified object. """ pass def get_label(self): """Gets the label to display for a specified object.""" return "No label specified" def set_label(self, label): """Sets the label for a specified object.""" pass def when_label_changed(self, listener, remove): """Sets up or removes a listener for the label being changed on a specified object. """ pass def get_column_labels(self): """Get the labels for any columns that have been defined.""" return [] def when_column_labels_change(self, listener, remove): """Sets up or removes a listener for the column labels being changed on a specified object. This will fire when either the list is reassigned or when it is modified. I.e., it listens both to the trait change event and the trait_items change event. Implement the listener appropriately to handle either case. """ pass def get_tooltip(self): """Gets the tooltip to display for a specified object.""" return "" def get_icon(self, is_expanded): """Returns the icon for a specified object. Valid values are '' (file looking icon), '' (closed folder looking icon) and '' (open folder looking icon). """ return "" def get_icon_path(self): """Returns the path used to locate an object's icon.""" return "" def get_name(self): """Returns the name to use when adding a new object instance (displayed in the "New" submenu). """ return "" def get_view(self): """Gets the view to use when editing an object.""" return None def get_menu(self): """Returns the right-click context menu for an object.""" return None def get_background(self): """Returns the background for object""" return None def get_foreground(self): """Returns the foreground for object""" return None def get_renderer(self, column=0): """Returns the renderer for object""" return None def can_rename(self): """Returns whether the object's children can be renamed.""" return False def can_rename_me(self): """Returns whether the object can be renamed.""" return False def can_copy(self): """Returns whether the object's children can be copied.""" return False def can_delete(self): """Returns whether the object's children can be deleted.""" return False def can_delete_me(self): """Returns whether the object can be deleted.""" return False def can_insert(self): """Returns whether the object's children can be inserted (vs. appended). """ return False def can_auto_open(self): """Returns whether the object's children should be automatically opened. """ return False def can_auto_close(self): """Returns whether the object's children should be automatically closed. """ return False def can_add(self, add_object): """Returns whether a given object is droppable on the node.""" return False def get_add(self): """Returns the list of classes that can be added to the object.""" return [] def get_drag_object(self): """Returns a draggable version of a specified object.""" return self.adaptee def drop_object(self, dropped_object): """Returns a droppable version of a specified object.""" return dropped_object def select(self): """Handles an object being selected.""" pass def click(self): """Handles an object being clicked.""" pass def dclick(self): """Handles an object being double-clicked.""" pass def activated(self): """Handles an object being activated.""" pass # ------------------------------------------------------------------------- # 'ITreeNodeAdapterBridge' class # ------------------------------------------------------------------------- class ITreeNodeAdapterBridge(HasPrivateTraits): """Private class for use by a toolkit-specific implementation of the TreeEditor to allow bridging the TreeNode interface used by the editor to the ITreeNode interface used by object adapters. """ #: The ITreeNode adapter being bridged: adapter = Supports(ITreeNode) # -- TreeNode implementation ---------------------------------------------- def allows_children(self, object): """Returns whether this object can have children.""" return self.adapter.allows_children() def has_children(self, object): """Returns whether the object has children.""" return self.adapter.has_children() def get_children(self, object): """Gets the object's children.""" return self.adapter.get_children() def get_children_id(self, object): """Gets the object's children identifier.""" return self.adapter.get_children_id() def append_child(self, object, child): """Appends a child to the object's children.""" return self.adapter.append_child(child) def insert_child(self, object, index, child): """Inserts a child into the object's children.""" return self.adapter.insert_child(index, child) def confirm_delete(self, object): """Checks whether a specified object can be deleted. The following return values are possible: - **True** if the object should be deleted with no further prompting. - **False** if the object should not be deleted. - Anything else: Caller should take its default action (which might include prompting the user to confirm deletion). """ return self.adapter.confirm_delete() def delete_child(self, object, index): """Deletes a child at a specified index from the object's children.""" return self.adapter.delete_child(index) def when_children_replaced(self, object, listener, remove): """Sets up or removes a listener for children being replaced on a specified object. """ return self.adapter.when_children_replaced(listener, remove) def when_children_changed(self, object, listener, remove): """Sets up or removes a listener for children being changed on a specified object. """ return self.adapter.when_children_changed(listener, remove) def get_label(self, object): """Gets the label to display for a specified object.""" return self.adapter.get_label() def set_label(self, object, label): """Sets the label for a specified object.""" return self.adapter.set_label(label) def when_label_changed(self, object, listener, remove): """Sets up or removes a listener for the label being changed on a specified object. """ return self.adapter.when_label_changed(listener, remove) def get_column_labels(self, object): """Get the labels for any columns that have been defined.""" return self.adapter.get_column_labels() def when_column_labels_change(self, object, listener, remove): """Sets up or removes a listener for the column labels being changed on a specified object. This will fire when either the list is reassigned or when it is modified. I.e., it listens both to the trait change event and the trait_items change event. Implement the listener appropriately to handle either case. """ return self.adapter.when_column_labels_change(listener, remove) def get_tooltip(self, object): """Gets the tooltip to display for a specified object.""" return self.adapter.get_tooltip() def get_icon(self, object, is_expanded): """Returns the icon for a specified object.""" return self.adapter.get_icon(is_expanded) def get_icon_path(self, object): """Returns the path used to locate an object's icon.""" return self.adapter.get_icon_path() def get_name(self, object): """Returns the name to use when adding a new object instance (displayed in the "New" submenu). """ return self.adapter.get_name() def get_view(self, object): """Gets the view to use when editing an object.""" return self.adapter.get_view() def get_menu(self, object): """Returns the right-click context menu for an object.""" return self.adapter.get_menu() def get_background(self, object): """Returns the background for object""" return self.adapter.get_background() def get_foreground(self, object): """Returns the foreground for object""" return self.adapter.get_foreground() def get_renderer(self, object, column=0): """Returns the renderer for object""" return self.adapter.get_renderer(column) def can_rename(self, object): """Returns whether the object's children can be renamed.""" return self.adapter.can_rename() def can_rename_me(self, object): """Returns whether the object can be renamed.""" return self.adapter.can_rename_me() def can_copy(self, object): """Returns whether the object's children can be copied.""" return self.adapter.can_copy() def can_delete(self, object): """Returns whether the object's children can be deleted.""" return self.adapter.can_delete() def can_delete_me(self, object): """Returns whether the object can be deleted.""" return self.adapter.can_delete_me() def can_insert(self, object): """Returns whether the object's children can be inserted (vs. appended). """ return self.adapter.can_insert() def can_auto_open(self, object): """Returns whether the object's children should be automatically opened. """ return self.adapter.can_auto_open() def can_auto_close(self, object): """Returns whether the object's children should be automatically closed. """ return self.adapter.can_auto_close() def can_add(self, object, add_object): """Returns whether a given object is droppable on the node.""" return self.adapter.can_add(add_object) def get_add(self, object): """Returns the list of classes that can be added to the object.""" return self.adapter.get_add() def get_drag_object(self, object): """Returns a draggable version of a specified object.""" return self.adapter.get_drag_object() def drop_object(self, object, dropped_object): """Returns a droppable version of a specified object.""" return self.adapter.drop_object(dropped_object) def select(self, object): """Handles an object being selected.""" return self.adapter.select() def click(self, object): """Handles an object being clicked.""" return self.adapter.click() def dclick(self, object): """Handles an object being double-clicked.""" return self.adapter.dclick() def activated(self, object): """Handles an object being activated.""" return self.adapter.activated() # FIXME RTK: add the column_labels API to the following TreeNodes, too. # ------------------------------------------------------------------------- # 'ObjectTreeNode' class # ------------------------------------------------------------------------- class ObjectTreeNode(TreeNode): def allows_children(self, object): """Returns whether this object can have children.""" return object.tno_allows_children(self) def has_children(self, object): """Returns whether the object has children.""" return object.tno_has_children(self) def get_children(self, object): """Gets the object's children.""" return object.tno_get_children(self) def get_children_id(self, object): """Gets the object's children identifier.""" return object.tno_get_children_id(self) def append_child(self, object, child): """Appends a child to the object's children.""" return object.tno_append_child(self, child) def insert_child(self, object, index, child): """Inserts a child into the object's children.""" return object.tno_insert_child(self, index, child) def confirm_delete(self, object): """Checks whether a specified object can be deleted. The following return values are possible: - **True** if the object should be deleted with no further prompting. - **False** if the object should not be deleted. - Anything else: Caller should take its default action (which might include prompting the user to confirm deletion). """ return object.tno_confirm_delete(self) def delete_child(self, object, index): """Deletes a child at a specified index from the object's children.""" return object.tno_delete_child(self, index) def when_children_replaced(self, object, listener, remove): """Sets up or removes a listener for children being replaced on a specified object. """ return object.tno_when_children_replaced(self, listener, remove) def when_children_changed(self, object, listener, remove): """Sets up or removes a listener for children being changed on a specified object. """ return object.tno_when_children_changed(self, listener, remove) def get_label(self, object): """Gets the label to display for a specified object.""" return object.tno_get_label(self) def set_label(self, object, label): """Sets the label for a specified object.""" return object.tno_set_label(self, label) def when_label_changed(self, object, listener, remove): """Sets up or removes a listener for the label being changed on a specified object. """ return object.tno_when_label_changed(self, listener, remove) def get_tooltip(self, object): """Gets the tooltip to display for a specified object.""" return object.tno_get_tooltip(self) def get_icon(self, object, is_expanded): """Returns the icon for a specified object.""" return object.tno_get_icon(self, is_expanded) def get_icon_path(self, object): """Returns the path used to locate an object's icon.""" return object.tno_get_icon_path(self) def get_name(self, object): """Returns the name to use when adding a new object instance (displayed in the "New" submenu). """ return object.tno_get_name(self) def get_view(self, object): """Gets the view to use when editing an object.""" return object.tno_get_view(self) def get_menu(self, object): """Returns the right-click context menu for an object.""" return object.tno_get_menu(self) def can_rename(self, object): """Returns whether the object's children can be renamed.""" return object.tno_can_rename(self) def can_rename_me(self, object): """Returns whether the object can be renamed.""" return object.tno_can_rename_me(self) def can_copy(self, object): """Returns whether the object's children can be copied.""" return object.tno_can_copy(self) def can_delete(self, object): """Returns whether the object's children can be deleted.""" return object.tno_can_delete(self) def can_delete_me(self, object): """Returns whether the object can be deleted.""" return object.tno_can_delete_me(self) def can_insert(self, object): """Returns whether the object's children can be inserted (vs. appended). """ return object.tno_can_insert(self) def can_auto_open(self, object): """Returns whether the object's children should be automatically opened. """ return object.tno_can_auto_open(self) def can_auto_close(self, object): """Returns whether the object's children should be automatically closed. """ return object.tno_can_auto_close(self) def is_node_for(self, object): """Returns whether this is the node that should handle a specified object. """ if isinstance(object, TreeNodeObject): return object.tno_is_node_for(self) return False def can_add(self, object, add_object): """Returns whether a given object is droppable on the node.""" return object.tno_can_add(self, add_object) def get_add(self, object): """Returns the list of classes that can be added to the object.""" return object.tno_get_add(self) def get_drag_object(self, object): """Returns a draggable version of a specified object.""" return object.tno_get_drag_object(self) def drop_object(self, object, dropped_object): """Returns a droppable version of a specified object.""" return object.tno_drop_object(self, dropped_object) def select(self, object): """Handles an object being selected.""" return object.tno_select(self) def click(self, object): """Handles an object being clicked.""" return object.tno_click(self) def dclick(self, object): """Handles an object being double-clicked.""" return object.tno_dclick(self) def activated(self, object): """Handles an object being activated.""" return object.tno_activated(self) class TreeNodeObject(HasPrivateTraits): """Represents the object that corresponds to a tree node.""" #: A cache for listeners that need to keep state. _listener_cache = Dict() def tno_allows_children(self, node): """Returns whether this object allows children.""" return node.children != "" def tno_has_children(self, node): """Returns whether this object has children.""" return len(self.tno_get_children(node)) > 0 def tno_get_children(self, node): """Gets the object's children.""" return getattr(self, node.children, None) def tno_get_children_id(self, node): """Gets the object's children identifier.""" return node.children def tno_append_child(self, node, child): """Appends a child to the object's children.""" self.tno_get_children(node).append(child) def tno_insert_child(self, node, index, child): """Inserts a child into the object's children.""" children = self.tno_get_children(node) children[index:index] = [child] def tno_confirm_delete(self, node): """Checks whether a specified object can be deleted. The following return values are possible: - **True** if the object should be deleted with no further prompting. - **False** if the object should not be deleted. - Anything else: Caller should take its default action (which might include prompting the user to confirm deletion). """ return None def tno_delete_child(self, node, index): """Deletes a child at a specified index from the object's children.""" del self.tno_get_children(node)[index] def tno_when_children_replaced(self, node, listener, remove): """Sets up or removes a listener for children being replaced on a specified object. """ self.on_trait_change( listener, node.children, remove=remove, dispatch="ui" ) def tno_when_children_changed(self, node, listener, remove): """Sets up or removes a listener for children being changed on a specified object. """ self.on_trait_change( listener, node.children + "_items", remove=remove, dispatch="ui", ) def tno_get_label(self, node): """Gets the label to display for a specified object.""" label = node.label if label[:1] == "=": return label[1:] label = xgetattr(self, label) if node.formatter is None: return label return node.formatter(self, label) def tno_set_label(self, node, label): """Sets the label for a specified object.""" label_name = node.label if label_name[:1] != "=": xsetattr(self, label_name, label) def tno_when_label_changed(self, node, listener, remove): """Sets up or removes a listener for the label being changed on a specified object. """ label = node.label if label[:1] != "=": memo = ("label", label, node, listener) if not remove: def wrapped_listener(target, name, new): """Ensure listener gets called with correct object.""" return listener(self, name, new) self._listener_cache[memo] = wrapped_listener else: wrapped_listener = self._listener_cache.pop(memo, None) if wrapped_listener is None: return self.on_trait_change( wrapped_listener, label, remove=remove, dispatch="ui" ) def tno_get_tooltip(self, node): """Gets the tooltip to display for a specified object.""" tooltip = node.tooltip if tooltip == "": return tooltip if tooltip[:1] == "=": return tooltip[1:] tooltip = xgetattr(self, tooltip) if node.tooltip_formatter is None: return tooltip return node.tooltip_formatter(self, tooltip) def tno_get_icon(self, node, is_expanded): """Returns the icon for a specified object.""" if not self.tno_allows_children(node): return node.icon_item if is_expanded: return node.icon_open return node.icon_group def tno_get_icon_path(self, node): """Returns the path used to locate an object's icon.""" return node.icon_path def tno_get_name(self, node): """Returns the name to use when adding a new object instance (displayed in the "New" submenu). """ return node.name def tno_get_view(self, node): """Gets the view to use when editing an object.""" return node.view def tno_get_menu(self, node): """Returns the right-click context menu for an object.""" return node.menu def tno_can_rename(self, node): """Returns whether the object's children can be renamed.""" return node.rename def tno_can_rename_me(self, node): """Returns whether the object can be renamed.""" return node.rename_me def tno_can_copy(self, node): """Returns whether the object's children can be copied.""" return node.copy def tno_can_delete(self, node): """Returns whether the object's children can be deleted.""" return node.delete def tno_can_delete_me(self, node): """Returns whether the object can be deleted.""" return node.delete_me def tno_can_insert(self, node): """Returns whether the object's children can be inserted (vs. appended). """ return node.insert def tno_can_auto_open(self, node): """Returns whether the object's children should be automatically opened. """ return node.auto_open def tno_can_auto_close(self, node): """Returns whether the object's children should be automatically closed. """ return node.auto_close def tno_is_node_for(self, node): """Returns whether this is the node that should handle a specified object. """ return isinstance( self, node.node_for_class ) or self.has_traits_interface(*node.node_for_interface) def tno_can_add(self, node, add_object): """Returns whether a given object is droppable on the node.""" klass = node._class_for(add_object) if node.is_addable(klass): return True for item in node.move: if type(item) in SequenceTypes: item = item[0] if issubclass(klass, item): return True return False def tno_get_add(self, node): """Returns the list of classes that can be added to the object.""" return node.add def tno_get_drag_object(self, node): """Returns a draggable version of a specified object.""" return self def tno_drop_object(self, node, dropped_object): """Returns a droppable version of a specified object.""" if node.is_addable(dropped_object): return dropped_object for item in node.move: if type(item) in SequenceTypes: if isinstance(dropped_object, item[0]): return item[1](self, dropped_object) else: if isinstance(dropped_object, item): return dropped_object def tno_select(self, node): """Handles an object being selected.""" if node.on_select is not None: node.on_select(self) return None return True def tno_click(self, node): """Handles an object being clicked.""" if node.on_click is not None: node.on_click(self) return None return True def tno_dclick(self, node): """Handles an object being double-clicked.""" if node.on_dclick is not None: node.on_dclick(self) return None return True def tno_activated(self, node): """Handles an object being activated.""" if node.on_activated is not None: node.on_activated(self) return None return True # ------------------------------------------------------------------------- # 'MultiTreeNode' object: # ------------------------------------------------------------------------- class MultiTreeNode(TreeNode): # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- #: TreeNode that applies to the base object itself root_node = Instance(TreeNode) #: List of TreeNodes (one for each sub-item list) nodes = List(TreeNode) def allows_children(self, object): """Returns whether this object can have children (True for this class). """ return True def has_children(self, object): """Returns whether this object has children (True for this class).""" return True def get_children(self, object): """Gets the object's children.""" return [(object, node) for node in self.nodes] def get_children_id(self, object): """Gets the object's children identifier.""" return "" def when_children_replaced(self, object, listener, remove): """Sets up or removes a listener for children being replaced on a specified object. """ pass def when_children_changed(self, object, listener, remove): """Sets up or removes a listener for children being changed on a specified object. """ pass def get_label(self, object): """Gets the label to display for a specified object.""" return self.root_node.get_label(object) def set_label(self, object, label): """Sets the label for a specified object.""" return self.root_node.set_label(object, label) def when_label_changed(self, object, listener, remove): """Sets up or removes a listener for the label being changed on a specified object. """ return self.root_node.when_label_changed(object, listener, remove) def get_icon(self, object, is_expanded): """Returns the icon for a specified object.""" return self.root_node.get_icon(object, is_expanded) def get_icon_path(self, object): """Returns the path used to locate an object's icon.""" return self.root_node.get_icon_path(object) def get_name(self, object): """Returns the name to use when adding a new object instance (displayed in the "New" submenu). """ return self.root_node.get_name(object) def get_view(self, object): """Gets the view to use when editing an object.""" return self.root_node.get_view(object) def get_menu(self, object): """Returns the right-click context menu for an object.""" return self.root_node.get_menu(object) def can_rename(self, object): """Returns whether the object's children can be renamed (False for this class). """ return False def can_rename_me(self, object): """Returns whether the object can be renamed (False for this class).""" return False def can_copy(self, object): """Returns whether the object's children can be copied.""" return self.root_node.can_copy(object) def can_delete(self, object): """Returns whether the object's children can be deleted (False for this class). """ return False def can_delete_me(self, object): """Returns whether the object can be deleted (True for this class).""" return True def can_insert(self, object): """Returns whether the object's children can be inserted (False, meaning that children are appended, for this class). """ return False def can_auto_open(self, object): """Returns whether the object's children should be automatically opened. """ return self.root_node.can_auto_open(object) def can_auto_close(self, object): """Returns whether the object's children should be automatically closed. """ return self.root_node.can_auto_close(object) def can_add(self, object, add_object): """Returns whether a given object is droppable on the node (False for this class). """ return False def get_add(self, object): """Returns the list of classes that can be added to the object.""" return [] def get_drag_object(self, object): """Returns a draggable version of a specified object.""" return self.root_node.get_drag_object(object) def drop_object(self, object, dropped_object): """Returns a droppable version of a specified object.""" return self.root_node.drop_object(object, dropped_object) def select(self, object): """Handles an object being selected.""" return self.root_node.select(object) def click(self, object): """Handles an object being clicked.""" return self.root_node.click(object) def dclick(self, object): """Handles an object being double-clicked.""" return self.root_node.dclick(object) def activated(self, object): """Handles an object being activated.""" return self.root_node.activated(object) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/tree_node_renderer.py0000644000175100001730000000474000000000000022242 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! from abc import abstractmethod from traits.api import ABCHasStrictTraits, Bool class AbstractTreeNodeRenderer(ABCHasStrictTraits): """Abstract base class for renderers of tree node items. This is currently only supported for Qt. """ #: Whether the renderer handles rendering everything handles_all = Bool(False) #: Whether the renderer handles rendering any text handles_text = Bool(True) #: Whether the renderer handles rendering the icon or other decoration handles_icon = Bool(False) @abstractmethod def paint(self, node, column, object, paint_context): """Render the node. Parameters ---------- node : ITreeNode instance The tree node to render. column : int The column in the tree that should be rendererd. object : object The underlying object being edited. paint_context : object A toolkit-dependent context for performing paint operations. Returns ------- size : tuple of (width, height) or None Optionally return a new preferred size so that the toolkit can perform better layout. """ raise NotImplementedError() @abstractmethod def size(self, node, column, object, size_context): """Return the preferred size for the item Parameters ---------- node : ITreeNode instance The tree node to render. column : int The column in the tree that should be rendererd. object : object The underlying object being edited. size_context : object A toolkit-dependent context for performing sizing operations. Returns ------- size : tuple of (width, height) or None """ raise NotImplementedError() def get_label(self, node, object, column=0): """Get the label associated with an item and column.""" if column == 0: return node.get_label(object) else: return node.get_column_labels(object)[column] ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/ui.py0000644000175100001730000007321200000000000017025 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Defines the UI class used to represent an active traits-based user interface. """ import shelve import os from warnings import warn from pyface.ui_traits import Image from traits.api import ( Any, Bool, Callable, Dict, Event, HasPrivateTraits, Instance, Int, List, Property, Str, TraitError, observe, ) from traits.trait_base import traits_home, is_str from .editor import Editor from .view_elements import ViewElements from .handler import Handler, ViewHandler from .toolkit import toolkit from .ui_info import UIInfo from .item import Item from .group import Group, ShadowGroup # List of **kind** types for views that must have a **parent** window specified kind_must_have_parent = ("panel", "subpanel") class UI(HasPrivateTraits): """Information about the user interface for a View.""" # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- #: The ViewElements object from which this UI resolves Include items view_elements = Instance(ViewElements) #: Context objects that the UI is editing context = Dict(Str, Any) #: Handler object used for event handling handler = Instance(Handler) #: View template used to construct the user interface view = Instance("traitsui.view.View") #: Panel or dialog associated with the user interface control = Any() #: The parent UI (if any) of this UI parent = Instance("UI") #: Toolkit-specific object that "owns" **control** owner = Any() #: UIInfo object containing context or editor objects info = Instance(UIInfo) #: Result from a modal or wizard dialog: result = Bool(False) #: Undo and Redo history history = Any() #: The KeyBindings object (if any) for this UI: key_bindings = Property(depends_on=["view._key_bindings", "context"]) #: The unique ID for this UI for persistence id = Str() #: Have any modifications been made to UI contents? modified = Bool(False) #: Event when the user interface has changed updated = Event(Bool) #: Title of the dialog, if any title = Str() #: The ImageResource of the icon, if any icon = Image #: Should the created UI have scroll bars? scrollable = Bool(False) #: The number of currently pending editor error conditions errors = Int() #: The code used to rebuild an updated user interface rebuild = Callable() #: Set to True when the UI has finished being destroyed. destroyed = Bool(False) # -- Private Traits ------------------------------------------------------- #: Original context when used with a modal dialog _context = Dict(Str, Any) #: Copy of original context used for reverting changes _revert = Dict(Str, Any) #: List of methods to call once the user interface is created _defined = List() #: List of (visible_when,Editor) pairs _visible = List() #: List of (enabled_when,Editor) pairs _enabled = List() #: List of (checked_when,Editor) pairs _checked = List() #: Search stack used while building a user interface _search = List() #: List of dispatchable Handler methods _dispatchers = List() #: List of editors used to build the user interface _editors = List() #: List of names bound to the **info** object _names = List() #: Index of currently the active group in the user interface _active_group = Int() #: List of top-level groups used to build the user interface _groups = Property() _groups_cache = Any() #: Count of levels of nesting for undoable actions _undoable = Int(-1) #: Code used to rebuild an updated user interface _rebuild = Callable() #: The statusbar listeners that have been set up: _statusbar = List() #: Control which gets focus after UI is created #: Note: this does not track focus after UI creation #: only used by Qt backend. _focus_control = Any() #: Does the UI contain any scrollable widgets? #: #: The _scrollable trait is set correctly, but not used currently because #: its value is arrived at too late to be of use in building the UI. _scrollable = Bool(False) #: Cache for key bindings. _key_bindings = Instance("traitsui.key_bindings.KeyBindings") #: List of traits that are reset when a user interface is recycled #: (i.e. rebuilt). recyclable_traits = [ "_context", "_revert", "_defined", "_visible", "_enabled", "_checked", "_search", "_dispatchers", "_editors", "_names", "_active_group", "_undoable", "_rebuild", "_groups_cache", "_key_bindings", "_focus_control", ] #: List of additional traits that are discarded when a user interface is #: disposed. disposable_traits = [ "view_elements", "info", "handler", "context", "view", "history", "key_bindings", "icon", "rebuild", ] def traits_init(self): """Initializes the traits object.""" self.info = UIInfo(ui=self) self.handler.init_info(self.info) def ui(self, parent, kind): """Creates a user interface from the associated View template object.""" if (parent is None) and (kind in kind_must_have_parent): kind = "live" self.view.on_trait_change( self._updated_changed, "updated", dispatch="ui" ) self.rebuild = getattr(toolkit(), "ui_" + kind) self.rebuild(self, parent) def dispose(self, result=None, abort=False): """Disposes of the contents of a user interface.""" if self.parent is not None: self.parent.errors -= self.errors if result is not None: self.result = result # Only continue if the view has not already been disposed of: if self.control is not None: # Save the user preference information for the user interface: if not abort: self.save_prefs() # Finish disposing of the user interface: self.finish() def recycle(self): """Recycles the user interface prior to rebuilding it.""" # Reset all user interface editors: self.reset(destroy=False) # Discard any context object associated with the ui view control: self.control._object = None # Reset all recyclable traits: self.reset_traits(self.recyclable_traits) def finish(self): """Finishes disposing of a user interface.""" # Destroy the control early to silence cascade events when the UI # enters an inconsistent state. toolkit().destroy_control(self.control) # Reset the contents of the user interface self.reset(destroy=False) # Make sure that 'visible', 'enabled', and 'checked' handlers are not # called after the editor has been disposed: for object in self.context.values(): object.on_trait_change(self._evaluate_when, remove=True) # Notify the handler that the view has been closed: self.handler.closed(self.info, self.result) # Clear the back-link from the UIInfo object to us: self.info.ui = None # Destroy the view control: self.control._object = None self.control = None # Dispose of any KeyBindings object we reference: if self._key_bindings is not None: self._key_bindings.dispose() # Break the linkage to any objects in the context dictionary: self.context.clear() # Remove specified symbols from our dictionary to aid in clean-up: self.reset_traits(self.recyclable_traits) self.reset_traits(self.disposable_traits) self.destroyed = True def reset(self, destroy=True): """Resets the contents of a user interface.""" for editor in self._editors: if editor._ui is not None: # Propagate result to enclosed ui objects: editor._ui.result = self.result editor.dispose() # Zap the control. If there are pending events for the control in # the UI queue, the editor's '_update_editor' method will see that # the control is None and discard the update request: editor.control = None # Remove any statusbar listeners that have been set up: for object, handler, name in self._statusbar: object.observe(handler, name, remove=True, dispatch="ui") del self._statusbar[:] if destroy: toolkit().destroy_children(self.control) for dispatcher in self._dispatchers: dispatcher.remove() def find(self, include): """Finds the definition of the specified Include object in the current user interface building context. """ context = self.context result = None # Get the context 'object' (if available): if len(context) == 1: object = list(context.values())[0] else: object = context.get("object") # Try to use our ViewElements objects: ve = self.view_elements # If none specified, try to get it from the UI context: if (ve is None) and (object is not None): # Use the context object's ViewElements (if available): ve = object.trait_view_elements() # Ask the ViewElements to find the requested item for us: if ve is not None: result = ve.find(include.id, self._search) # If not found, then try to search the 'handler' and 'object' for a # method we can call that will define it: if result is None: handler = context.get("handler") if handler is not None: method = getattr(handler, include.id, None) if callable(method): result = method() if (result is None) and (object is not None): method = getattr(object, include.id, None) if callable(method): result = method() return result def push_level(self): """Returns the current search stack level.""" return len(self._search) def pop_level(self, level): """Restores a previously pushed search stack level.""" del self._search[: len(self._search) - level] def prepare_ui(self): """Performs all processing that occurs after the user interface is created. """ # Invoke all of the editor 'name_defined' methods we've accumulated: info = self.info.trait_set(initialized=False) for method in self._defined: method(info) # Then reset the list, since we don't need it anymore: del self._defined[:] # Synchronize all context traits with associated editor traits: self.sync_view() # Hook all keyboard events: toolkit().hook_events(self, self.control, "keys", self.key_handler) # Hook all events if the handler is an extended 'ViewHandler': handler = self.handler if isinstance(handler, ViewHandler): toolkit().hook_events(self, self.control) # Invoke the handler's 'init' method, and abort if it indicates # failure: started = handler.init(info) if started is False: raise TraitError("User interface creation aborted") elif not isinstance(started, bool): raise ValueError( "Handler.init() must return True or False, but instead " f"returned {started}." ) # For each Handler method whose name is of the form # 'object_name_changed', where 'object' is the name of an object in the # UI's 'context', create a trait notification handler that will call # the method whenever 'object's 'name' trait changes. Also invoke the # method immediately so initial user interface state can be correctly # set: context = self.context for name in self._each_trait_method(handler): if name[-8:] == "_changed": prefix = name[:-8] col = prefix.find("_", 1) if col >= 0: object = context.get(prefix[:col]) if object is not None: method = getattr(handler, name) trait_name = prefix[col + 1 :] self._dispatchers.append( Dispatcher(method, info, object, trait_name) ) if object.base_trait(trait_name).type != "event": method(info) # If there are any Editor object's whose 'visible', 'enabled' or # 'checked' state is controlled by a 'visible_when', 'enabled_when' or # 'checked_when' expression, set up an 'anytrait' changed notification # handler on each object in the 'context' that will cause the # 'visible', 'enabled' or 'checked' state of each affected Editor to be # set. Also trigger the evaluation immediately, so the visible, # enabled or checked state of each Editor can be correctly initialized: if (len(self._visible) + len(self._enabled) + len(self._checked)) > 0: for object in context.values(): object.on_trait_change(self._evaluate_when, dispatch="ui") self._do_evaluate_when(at_init=True) # Indicate that the user interface has been initialized: info.initialized = True def sync_view(self): """Synchronize context object traits with view editor traits.""" for name, object in self.context.items(): self._sync_view(name, object, "sync_to_view", "from") self._sync_view(name, object, "sync_from_view", "to") self._sync_view(name, object, "sync_with_view", "both") def _sync_view(self, name, object, metadata, direction): info = self.info for trait_name, trait in object.traits(**{metadata: is_str}).items(): for sync in getattr(trait, metadata).split(","): try: editor_id, editor_name = [ item.strip() for item in sync.split(".") ] except: raise TraitError( "The '%s' metadata for the '%s' trait in " "the '%s' context object should be of the form: " "'id1.trait1[,...,idn.traitn]." % (metadata, trait_name, name) ) editor = getattr(info, editor_id, None) if editor is not None: editor.sync_value( "%s.%s" % (name, trait_name), editor_name, direction ) else: raise TraitError( "No editor with id = '%s' was found for " "the '%s' metadata for the '%s' trait in the '%s' " "context object." % (editor_id, metadata, trait_name, name) ) def get_extended_value(self, name): """Gets the current value of a specified extended trait name.""" names = name.split(".") if len(names) > 1: value = self.context[names[0]] del names[0] else: value = self.context["object"] for name in names: value = getattr(value, name) return value def restore_prefs(self): """Retrieves and restores any saved user preference information associated with the UI. """ id = self.id if id != "": db = self.get_ui_db() if db is not None: try: ui_prefs = db.get(id) db.close() return self.set_prefs(ui_prefs) except: pass return None def set_prefs(self, prefs): """Sets the values of user preferences for the UI.""" if isinstance(prefs, dict): info = self.info for name in self._names: editor = getattr(info, name, None) if isinstance(editor, Editor) and (editor.ui is self): editor_prefs = prefs.get(name) if editor_prefs is not None: editor.restore_prefs(editor_prefs) if self.key_bindings is not None: key_bindings = prefs.get("$") if key_bindings is not None: self.key_bindings.merge(key_bindings) return prefs.get("") return None def save_prefs(self, prefs=None): """Saves any user preference information associated with the UI.""" if prefs is None: toolkit().save_window(self) return id = self.id if id != "": db = self.get_ui_db(mode="c") if db is not None: db[id] = self.get_prefs(prefs) db.close() def get_prefs(self, prefs=None): """Gets the preferences to be saved for the user interface.""" ui_prefs = {} if prefs is not None: ui_prefs[""] = prefs if self.key_bindings is not None: ui_prefs["$"] = self.key_bindings info = self.info for name in self._names: editor = getattr(info, name, None) if isinstance(editor, Editor) and (editor.ui is self): prefs = editor.save_prefs() if prefs is not None: ui_prefs[name] = prefs return ui_prefs def get_ui_db(self, mode="r"): """Returns a reference to the Traits UI preference database.""" try: return shelve.open( os.path.join(traits_home(), "traits_ui"), flag=mode, protocol=-1, ) except: return None def get_editors(self, name): """Returns a list of editors for the given trait name.""" return [editor for editor in self._editors if editor.name == name] def get_error_controls(self): """Returns the list of editor error controls contained by the user interface. """ controls = [] for editor in self._editors: control = editor.get_error_control() if isinstance(control, list): controls.extend(control) else: controls.append(control) return controls def add_defined(self, method): """Adds a Handler method to the list of methods to be called once the user interface has been constructed. """ self._defined.append(method) def add_visible(self, visible_when, editor): """Adds a conditionally enabled Editor object to the list of monitored 'visible_when' objects. """ try: self._visible.append( (compile(visible_when, "", "eval"), editor) ) except: pass # fixme: Log an error here... def add_enabled(self, enabled_when, editor): """Adds a conditionally enabled Editor object to the list of monitored 'enabled_when' objects. """ try: self._enabled.append( (compile(enabled_when, "", "eval"), editor) ) except: pass # fixme: Log an error here... def add_checked(self, checked_when, editor): """Adds a conditionally enabled (menu) Editor object to the list of monitored 'checked_when' objects. """ try: self._checked.append( (compile(checked_when, "", "eval"), editor) ) except: pass # fixme: Log an error here... def do_undoable(self, action, *args, **kw): """Performs an action that can be undone.""" undoable = self._undoable try: if (undoable == -1) and (self.history is not None): self._undoable = self.history.now action(*args, **kw) finally: if undoable == -1: self._undoable = -1 def route_event(self, event): """Routes a "hooked" event to the correct handler method.""" toolkit().route_event(self, event) def key_handler(self, event, skip=True): """Handles key events.""" key_bindings = self.key_bindings handled = (key_bindings is not None) and key_bindings.do( event, [], self.info, recursive=(self.parent is None) ) if (not handled) and (self.parent is not None): handled = self.parent.key_handler(event, False) if (not handled) and skip: toolkit().skip_event(event) return handled def evaluate(self, function, *args, **kw_args): """Evaluates a specified function in the UI's **context**.""" if function is None: return None if callable(function): return function(*args, **kw_args) context = self.context.copy() context["ui"] = self context["handler"] = self.handler return eval(function, globals(), context)(*args, **kw_args) def eval_when(self, when, result=True): """Evaluates an expression in the UI's **context** and returns the result. """ context = self._get_context(self.context) try: result = eval(when, globals(), context) except: from traitsui.api import raise_to_debug raise_to_debug() del context["ui"] return result def _get_context(self, context): """Gets the context to use for evaluating an expression.""" name = "object" n = len(context) if (n == 2) and ("handler" in context): for name, value in context.items(): if name != "handler": break elif n == 1: name = list(context.keys())[0] value = context.get(name) if value is not None: context2 = value.trait_get() context2.update(context) else: context2 = context.copy() context2["ui"] = self return context2 def _evaluate_when(self): """Set the 'visible', 'enabled', and 'checked' states for all Editors controlled by a 'visible_when', 'enabled_when' or 'checked_when' expression. """ self._do_evaluate_when(at_init=False) def _do_evaluate_when(self, at_init=False): """Set the 'visible', 'enabled', and 'checked' states for all Editors. This function does the job of _evaluate_when. We define it here to work around the traits dispatching mechanism that automatically determines the number of arguments of a notification method. :attr:`at_init` is set to true when this function is called the first time at initialization. In that case, we want to force the state of the items to be set (normally it is set only if it changes). """ self._evaluate_condition(self._visible, "visible", at_init) self._evaluate_condition(self._enabled, "enabled", at_init) self._evaluate_condition(self._checked, "checked", at_init) def _evaluate_condition(self, conditions, trait, at_init=False): """Evaluates a list of (eval, editor) pairs and sets a specified trait on each editor to reflect the Boolean value of the expression. 1) All conditions are evaluated 2) The elements whose condition evaluates to False are updated 3) The elements whose condition evaluates to True are updated E.g., we first make invisible all elements for which 'visible_when' evaluates to False, and then we make visible the ones for which 'visible_when' is True. This avoids mutually exclusive elements to be visible at the same time, and thus making a dialog unnecessarily large. The state of an editor is updated only when it changes, unless at_init is set to True. Parameters ---------- conditions : list of (str, Editor) tuple A list of tuples, each formed by 1) a string that contains a condition that evaluates to either True or False, and 2) the editor whose state depends on the condition trait : str The trait that is set by the condition. Either 'visible, 'enabled', or 'checked'. at_init : bool If False, the state of an editor is set only when it changes (e.g., a visible element would not be updated to visible=True again). If True, the state is always updated (used at initialization). """ context = self._get_context(self.context) # list of elements that should be activated activate = [] # list of elements that should be de-activated deactivate = [] for when, editor in conditions: try: cond_value = eval(when, globals(), context) editor_state = getattr(editor, trait) # add to update lists only if at_init is True (called on # initialization), or if the editor state has to change if cond_value and (at_init or not editor_state): activate.append(editor) if not cond_value and (at_init or editor_state): deactivate.append(editor) except Exception: # catch errors in the validate_when expression from traitsui.api import raise_to_debug raise_to_debug() # update the state of the editors for editor in deactivate: setattr(editor, trait, False) for editor in activate: setattr(editor, trait, True) def _get__groups(self): """Returns the top-level Groups for the view (after resolving Includes. (Implements the **_groups** property.) """ if self._groups_cache is None: shadow_group = self.view.content.get_shadow(self) self._groups_cache = shadow_group.get_content() for item in self._groups_cache: if isinstance(item, Item): self._groups_cache = [ ShadowGroup( shadow=Group(*self._groups_cache), content=self._groups_cache, groups=1, ) ] break return self._groups_cache # -- Property Implementations --------------------------------------------- def _get_key_bindings(self): if self._key_bindings is None: # create a new key_bindings instance lazily view, context = self.view, self.context if (view is None) or (context is None): return None # Get the KeyBindings object to use: values = list(context.values()) key_bindings = view.key_bindings if key_bindings is None: from .key_bindings import KeyBindings self._key_bindings = KeyBindings(controllers=values) else: self._key_bindings = key_bindings.clone(controllers=values) return self._key_bindings # -- Traits Event Handlers ------------------------------------------------ def _errors_changed(self, name, old, new): if self.parent: self.parent.errors = self.parent.errors - old + new def _parent_changed(self, name, old, new): if old is not None: old.errors -= self.errors if new is not None: new.errors += self.errors def _updated_changed(self): if self.rebuild is not None: toolkit().rebuild_ui(self) def _title_changed(self): if self.control is not None: toolkit().set_title(self) def _icon_changed(self): if self.control is not None: toolkit().set_icon(self) @observe("parent, view, context") def _pvc_changed(self, event): parent = self.parent if (parent is not None) and (self.key_bindings is not None): # If we don't have our own history, use our parent's: if self.history is None: self.history = parent.history # Link our KeyBindings object as a child of our parent's # KeyBindings object (if any): if parent.key_bindings is not None: parent.key_bindings.children.append(self.key_bindings) class Dispatcher(object): def __init__(self, method, info, object, method_name): """Initializes the object.""" self.method = method self.info = info self.object = object self.method_name = method_name object.on_trait_change(self.dispatch, method_name, dispatch="ui") def dispatch(self): """Dispatches the method.""" self.method(self.info) def remove(self): """Removes the dispatcher.""" self.object.on_trait_change( self.dispatch, self.method_name, remove=True ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/ui_editor.py0000644000175100001730000000526600000000000020377 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Defines the BasicUIEditor class, which allows creating editors that define their function by creating an embedded Traits UI. """ from traits.api import Instance from .ui import UI from .editor import Editor # ------------------------------------------------------------------------- # 'UIEditor' base class: # ------------------------------------------------------------------------- class UIEditor(Editor): """An editor that creates an embedded Traits UI.""" # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- #: The Traits UI created by the editor editor_ui = Instance(UI) def init(self, parent): """Finishes initializing the editor by creating the underlying toolkit widget. """ self.editor_ui = self.init_ui(parent).trait_set(parent=self.ui) self.control = self.editor_ui.control def init_ui(self, parent): """Creates the traits UI for the editor.""" return self.value.edit_traits( view=self.trait_view(), context={"object": self.value, "editor": self}, parent=parent, ) def update_editor(self): """Updates the editor when the object trait changes external to the editor. """ # Do nothing, since the embedded traits UI should handle the updates # itself, without our meddling: pass def dispose(self): """Disposes of the contents of an editor.""" # Make sure the embedded traits UI is disposed of properly: if self.editor_ui is not None: self.editor_ui.dispose() super().dispose() def get_error_control(self): """Returns the editor's control for indicating error status.""" return self.editor_ui.get_error_controls() # -- UI preference save/restore interface --------------------------------- def restore_prefs(self, prefs): """Restores any saved user preference information associated with the editor. """ self.editor_ui.set_prefs(prefs) def save_prefs(self): """Returns any user preference information associated with the editor.""" return self.editor_ui.get_prefs() ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1684768028.1158068 traitsui-8.0.0/traitsui/ui_editors/0000755000175100001730000000000000000000000020177 5ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/ui_editors/__init__.py0000644000175100001730000000000000000000000022276 0ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/ui_editors/array_view_editor.py0000644000175100001730000001242600000000000024274 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Defines an ArrayViewEditor for displaying 1-d or 2-d arrays of values. """ # -- Imports -------------------------------------------------------------- from traits.api import Instance, Property, List, Str, Bool from ..api import View, Item, TabularEditor, BasicEditorFactory from ..tabular_adapter import TabularAdapter from ..toolkit import toolkit_object from ..toolkit_traits import Font from ..ui_editor import UIEditor # -- Tabular Adapter Definition ------------------------------------------- class ArrayViewAdapter(TabularAdapter): #: Is the array 1D or 2D? is_2d = Bool(True) #: Should array rows and columns be transposed: transpose = Bool(False) alignment = "right" index_text = Property() def _get_index_text(self): return str(self.row) def _get_content(self): if self.is_2d: return self.item[self.column_id] return self.item def get_item(self, object, trait, row): """Returns the value of the *object.trait[row]* item.""" if self.is_2d: if self.transpose: return getattr(object, trait)[:, row] return super().get_item(object, trait, row) return getattr(object, trait)[row] def len(self, object, trait): """Returns the number of items in the specified *object.trait* list.""" if self.transpose: return getattr(object, trait).shape[1] return super().len(object, trait) # Define the actual abstract Traits UI array view editor (each backend should # implement its own editor that inherits from this class. class _ArrayViewEditor(UIEditor): #: Indicate that the editor is scrollable/resizable: scrollable = True #: Should column titles be displayed: show_titles = Bool(False) #: The tabular adapter being used for the editor view: adapter = Instance(ArrayViewAdapter) # -- Private Methods ------------------------------------------------------ def _array_view(self): """Return the view used by the editor.""" return View( Item( "object.object." + self.name, id="tabular_editor", show_label=False, editor=TabularEditor( show_titles=self.show_titles, editable=False, adapter=self.adapter, ), ), id="array_view_editor", resizable=True, ) def init_ui(self, parent): """Creates the Traits UI for displaying the array.""" # Make sure that the value is an array of the correct shape: shape = self.value.shape len_shape = len(shape) if (len_shape == 0) or (len_shape > 2): raise ValueError( "ArrayViewEditor can only display 1D or 2D " "arrays" ) factory = self.factory cols = 1 titles = factory.titles n = len(titles) self.show_titles = n > 0 is_2d = len_shape == 2 if is_2d: index = 1 if factory.transpose: index = 0 cols = shape[index] if self.show_titles: if n > cols: titles = titles[:cols] elif n < cols: if (cols % n) == 0: titles, old_titles, i = [], titles, 0 while len(titles) < cols: titles.extend( "%s%d" % (title, i) for title in old_titles ) i += 1 else: titles.extend([""] * (cols - n)) else: titles = ["Data %d" % i for i in range(cols)] columns = [(title, i) for i, title in enumerate(titles)] if factory.show_index: columns.insert(0, ("Index", "index")) self.adapter = ArrayViewAdapter( is_2d=is_2d, columns=columns, transpose=factory.transpose, format=factory.format, font=factory.font, ) return self.edit_traits( view="_array_view", parent=parent, kind="subpanel" ) # Define the ArrayViewEditor class used by client code: class ArrayViewEditor(BasicEditorFactory): #: The editor implementation class: klass = Property() #: Should an index column be displayed: show_index = Bool(True) #: List of (optional) column titles: titles = List(Str) #: Should the array be logically transposed: transpose = Bool(False) #: The format used to display each array element: format = Str("%s") #: The font to use for displaying each array element: font = Font("Courier 10") def _get_klass(self): """The class used to construct editor objects.""" return toolkit_object("array_view_editor:_ArrayViewEditor") ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/ui_editors/data_frame_editor.py0000644000175100001730000003305500000000000024210 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! import logging import warnings from traits.api import ( Bool, Dict, Enum, Instance, List, Property, Str, Tuple, Union, ) from traitsui.basic_editor_factory import BasicEditorFactory from traitsui.editors.tabular_editor import TabularEditor from traitsui.item import Item from traitsui.tabular_adapter import TabularAdapter from traitsui.toolkit import toolkit_object from traitsui.toolkit_traits import Font from traitsui.ui_editor import UIEditor from traitsui.view import View logger = logging.getLogger(__name__) class DataFrameAdapter(TabularAdapter): """Generic tabular adapter for data frames""" #: The text to use for a generic entry. text = Property() #: The alignment for each cell alignment = Property(Enum("left", "center", "right")) #: The text to use for a row index. index_text = Property() #: The alignment to use for a row index. index_alignment = Property() #: The font to use for each column font = Property() #: The format to use for each column format = Property() #: The format for each element, or a mapping column ID to format. _formats = Union(Str, Dict, default_value="%s") #: The font for each element, or a mapping column ID to font. _fonts = Union(Font, Dict, default_value="Courier 10") def _get_index_alignment(self): import numpy as np index = getattr(self.object, self.name).index if np.issubdtype(index.dtype, np.number): return "right" else: return "left" def _get_alignment(self): import numpy as np column = self.item[self.column_id] if np.issubdtype(column.dtype, np.number): return "right" else: return "left" def _get_font(self): if isinstance(self._fonts, toolkit_object("font_trait:TraitsFont")): return self._fonts else: return self._fonts.get(self.column_id, "Courier 10") def _get_format(self): if isinstance(self._formats, str): return self._formats else: return self._formats.get(self.column_id, "%s") def _get_content(self): return self.item[self.column_id].iloc[0] def _get_text(self): format = self.get_format(self.object, self.name, self.row, self.column) return format % self.get_content( self.object, self.name, self.row, self.column ) def _set_text(self, value): df = getattr(self.object, self.name) dtype = df.iloc[:, self.column].dtype try: value = dtype.type(value) df.iloc[self.row, self.column] = value except Exception: logger.debug( "User entered invalid value %r for column %r row %r", value, self.column, self.row, exc_info=True, ) def _get_index_text(self): return str(self.item.index[0]) def _set_index_text(self, value): index = getattr(self.object, self.name).index dtype = index.dtype try: value = dtype.type(value) index.values[self.row] = value except Exception: logger.debug( "User entered invalid value %r for index", value, exc_info=True ) # ---- Adapter methods that are not sensitive to item type ---------------- def get_item(self, object, trait, row): """Override the base implementation to work with DataFrames This returns a dataframe with one row, rather than a series, since using a dataframe preserves dtypes. """ return getattr(object, trait).iloc[row : row + 1] def delete(self, object, trait, row): """Override the base implementation to work with DataFrames Unavoidably does a copy of the data, setting the trait with the new value. """ import pandas as pd df = getattr(object, trait) if 0 < row < len(df) - 1: new_df = pd.concat([df.iloc[:row, :], df.iloc[row + 1 :, :]]) elif row == 0: new_df = df.iloc[row + 1 :, :] else: new_df = df.iloc[:row, :] setattr(object, trait, new_df) def insert(self, object, trait, row, value): """Override the base implementation to work with DataFrames Unavoidably does a copy of the data, setting the trait with the new value. """ import pandas as pd df = getattr(object, trait) if 0 < row < len(df) - 1: new_df = pd.concat([df.iloc[:row, :], value, df.iloc[row:, :]]) elif row == 0: new_df = pd.concat([value, df]) else: new_df = pd.concat([df, value]) setattr(object, trait, new_df) class _DataFrameEditor(UIEditor): """TraitsUI-based editor implementation for data frames""" #: Indicate that the editor is scrollable/resizable: scrollable = True #: Should column titles be displayed: show_titles = Bool(True) #: The tabular adapter being used for the editor view: adapter = Instance(DataFrameAdapter) # -- Private Methods ------------------------------------------------------ def _target_name(self, name): if name: return "object.object." + name else: return "" def _data_frame_view(self): """Return the view used by the editor.""" return View( Item( self._target_name(self.name), id="tabular_editor", show_label=False, editor=TabularEditor( show_titles=self.factory.show_titles, editable=self.factory.editable, adapter=self.adapter, selected=self._target_name(self.factory.selected), selected_row=self._target_name(self.factory.selected_row), selectable=self.factory.selectable, multi_select=self.factory.multi_select, activated=self._target_name(self.factory.activated), activated_row=self._target_name( self.factory.activated_row ), # noqa clicked=self._target_name(self.factory.clicked), dclicked=self._target_name(self.factory.dclicked), scroll_to_row=self._target_name( self.factory.scroll_to_row ), # noqa scroll_to_position_hint=( self.factory.scroll_to_position_hint ), scroll_to_column=self._target_name( self.factory.scroll_to_column ), # noqa right_clicked=self._target_name( self.factory.right_clicked ), # noqa right_dclicked=self._target_name( self.factory.right_dclicked ), # noqa column_clicked=self._target_name( self.factory.column_clicked ), # noqa column_right_clicked=self._target_name( self.factory.column_right_clicked ), # noqa operations=self.factory.operations, update=self._target_name(self.factory.update), refresh=self._target_name(self.factory.refresh), ), ), id="data_frame_editor", resizable=True, ) def init_ui(self, parent): """Creates the Traits UI for displaying the array.""" factory = self.factory if factory.columns != []: columns = [] for column in factory.columns: if isinstance(column, str): title = column column_id = column else: title, column_id = column if column_id not in self.value.columns: continue columns.append((title, column_id)) else: columns = [ (column_id, column_id) for column_id in self.value.columns ] if factory.show_index: index_name = self.value.index.name if index_name is None: index_name = "" columns.insert(0, (index_name, "index")) if factory.adapter is not None: self.adapter = factory.adapter self.adapter._formats = factory.formats self.adapter._fonts = factory.fonts if not self.adapter.columns: self.adapter.columns = columns else: self.adapter = DataFrameAdapter( columns=columns, _formats=factory.formats, _fonts=factory.fonts ) return self.edit_traits( view="_data_frame_view", parent=parent, kind="subpanel" ) class DataFrameEditor(BasicEditorFactory): """Editor factory for basic data frame editor""" #: The editor implementation class. klass = Property() #: Should an index column be displayed. show_index = Bool(True) #: Should column headers be displayed. show_titles = Bool(True) #: Optional list of either column ID or pairs of (column title, column ID). columns = List(Union(Str, Tuple(Str, Str))) #: The format for each element, or a mapping column ID to format. formats = Union(Str, Dict, default_value="%s") #: The font for each element, or a mapping column ID to font. fonts = Union(Font, Dict, default_value="Courier 10") #: The optional extended name of the trait to synchronize the selection #: values with: selected = Str() #: The optional extended name of the trait to synchronize the selection rows #: with: selected_row = Str() #: Whether or not to allow selection. selectable = Bool(True) #: Whether or not to allow for multiple selections multi_select = Bool(False) #: The optional extended name of the trait to synchronize the activated #: value with: activated = Str() #: The optional extended name of the trait to synchronize the activated #: value's row with: activated_row = Str() #: The optional extended name of the trait to synchronize left click data #: with. The data is a TabularEditorEvent: clicked = Str() #: The optional extended name of the trait to synchronize left double click #: data with. The data is a TabularEditorEvent: dclicked = Str() #: The optional extended name of the Event trait that should be used to #: trigger a scroll-to command. The data is an integer giving the row. scroll_to_row = Str() #: Deprecated: Controls behavior of scroll to row and scroll to column scroll_to_row_hint = Property(Str, observe="scroll_to_position_hint") #: (replacement of scroll_to_row_hint, but more clearly named) #: Controls behavior of scroll to row and scroll to column scroll_to_position_hint = Enum("visible", "center", "top", "bottom") #: The optional extended name of the Event trait that should be used to #: trigger a scroll-to command. The data is an integer giving the column. scroll_to_column = Str() #: The optional extended name of the trait to synchronize right click data #: with. The data is a TabularEditorEvent: right_clicked = Str() #: The optional extended name of the trait to synchronize right double #: clicked data with. The data is a TabularEditorEvent: right_dclicked = Str() #: The optional extended name of the trait to synchronize column #: clicked data with. The data is a TabularEditorEvent: column_clicked = Str() #: The optional extended name of the trait to synchronize column #: right clicked data with. The data is a TabularEditorEvent: column_right_clicked = Str() #: Whether or not the entries can be edited. editable = Bool(False) #: What type of operations are allowed on the list: operations = List( Enum("delete", "insert", "append", "edit", "move"), ["delete", "insert", "append", "edit", "move"], ) #: The optional extended name of the trait used to indicate that a complete #: table update is needed: update = Str() #: The optional extended name of the trait used to indicate that the table #: just needs to be repainted. refresh = Str() #: Set to override the default dataframe adapter adapter = Instance(DataFrameAdapter) def _get_klass(self): """The class used to construct editor objects.""" return toolkit_object("data_frame_editor:_DataFrameEditor") def _get_scroll_to_row_hint(self): warnings.warn( "Use of scroll_to_row_hint trait is deprecated. " "Use scroll_to_position_hint instead.", DeprecationWarning, ) return self.scroll_to_position_hint def _set_scroll_to_row_hint(self, hint): warnings.warn( "Use of scroll_to_row_hint trait is deprecated. " "Use scroll_to_position_hint instead.", DeprecationWarning, ) self.scroll_to_position_hint = hint ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/ui_info.py0000644000175100001730000000316100000000000020034 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Defines the UIInfo class used to represent the object and editor content of an active Traits-based user interface. """ from traits.api import HasPrivateTraits, Instance, Constant, Bool class UIInfo(HasPrivateTraits): """Represents the object and editor content of an active Traits-based user interface """ # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- #: Bound to a UI object at UIInfo construction time ui = Instance("traitsui.ui.UI", allow_none=True) #: Indicates whether the UI has finished initialization initialized = Bool(False) def bind_context(self): """Binds all of the associated context objects as traits of the object. """ for name, value in self.ui.context.items(): self.bind(name, value) def bind(self, name, value, id=None): """Binds a name to a value if it is not already bound.""" if id is None: id = name if not hasattr(self, name): self.add_trait(name, Constant(value)) if id != "": self.ui._names.append(id) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/ui_traits.py0000644000175100001730000001540300000000000020411 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Defines common traits used within the traits.ui package. """ from pyface.ui_traits import ( Alignment, Border, HasBorder, HasMargin, Image, Margin, Position, convert_bitmap, convert_image, ) from traits.api import ( Any, Delegate, Enum, Expression, Float, HasStrictTraits, List, PrefixList, Range, Str, TraitError, TraitType, ) from .helper import SequenceTypes # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- # Orientation trait: Orientation = PrefixList(("vertical", "horizontal")) # Styles for user interface elements: EditorStyle = style_trait = PrefixList( ("simple", "custom", "text", "readonly"), cols=4 ) # Group layout trait: Layout = PrefixList(("normal", "split", "tabbed", "flow", "fold")) # Trait for the default object being edited: AnObject = Expression("object") # The default dock style to use: DockStyle = dock_style_trait = Enum( "fixed", "horizontal", "vertical", "tab", desc="the default docking style to use", ) # The category of elements dragged out of the view: ExportType = Str(desc="the category of elements dragged out of the view") # Delegate a trait value to the object's **container** trait: ContainerDelegate = container_delegate = Delegate( "container", listenable=False ) # An identifier for the external help context: HelpId = help_id_trait = Str(desc="the external help context identifier") # A button to add to a view: AButton = Any() # AButton = Trait( '', Str, Instance( 'traitsui.menu.Action' ) ) # The set of buttons to add to the view: Buttons = List( AButton, desc="the action buttons to add to the bottom of the view" ) # View trait specified by name or instance: AView = Any() # AView = Trait( '', Str, Instance( 'traitsui.view.View' ) ) # FIXME: on AButton and AView: TraitCompound handlers with deferred-import # Instance traits are just broken. The Instance trait tries to update the # top-level CTrait's fast_validate table when the import is resolved. However, # sometimes the CTrait gets copied for unknown reasons and the copy's # fast_validate table is not updated although the TraitCompound's # slow_validates table is modified. # ------------------------------------------------------------------------- # 'StatusItem' class: # ------------------------------------------------------------------------- class StatusItem(HasStrictTraits): #: The name of the trait the status information will be synched with: name = Str("status") #: The width of the status field. The possible values are: #: #: - abs( width ) > 1.0: Width of the field in pixels = abs( width ) #: - abs( width ) <= 1.0: Relative width of the field when compared to #: the other relative width fields. width = Float(0.5) def __init__(self, value=None, **traits): """Initializes the item object.""" super().__init__(**traits) if value is not None: self.name = value # ------------------------------------------------------------------------- # 'ViewStatus' trait: # ------------------------------------------------------------------------- class ViewStatus(TraitType): """Defines a trait whose value must be a single StatusItem instance or a list of StatusItem instances. """ #: Define the default value for the trait: default_value = None #: A description of the type of value this trait accepts: info_text = ( "None, a string, a single StatusItem instance, or a list or " "tuple of strings and/or StatusItem instances" ) def validate(self, object, name, value): """Validates that a specified value is valid for this trait.""" if isinstance(value, str): return [StatusItem(name=value)] if isinstance(value, StatusItem): return [value] if value is None: return value result = [] if isinstance(value, SequenceTypes): for item in value: if isinstance(item, str): result.append(StatusItem(name=item)) elif isinstance(item, StatusItem): result.append(item) else: break else: return result self.error(object, name, value) # ------------------------------------------------------------------------- # 'ATheme' trait: # ------------------------------------------------------------------------- def convert_theme(value, level=3): """Converts a specified value to a Theme if possible.""" if not isinstance(value, str): return value if (value[:1] == "@") and (value.find(":") >= 2): try: from .image.image import ImageLibrary info = ImageLibrary.image_info(value) except: info = None if info is not None: return info.theme from .theme import Theme return Theme(image=convert_image(value, level + 1)) class ATheme(TraitType): """Defines a trait whose value must be a traits UI Theme or a string that can be converted to one. """ #: Define the default value for the trait: default_value = None #: A description of the type of value this trait accepts: info_text = "a Theme or string that can be used to define one" def __init__(self, value=None, **metadata): """Creates an ATheme trait. Parameters ---------- value : string or Theme The default value for the ATheme, either a Theme object, or a string from which a Theme object can be derived. """ super().__init__(convert_theme(value), **metadata) def validate(self, object, name, value): """Validates that a specified value is valid for this trait.""" from .theme import Theme if value is None: return None new_value = convert_theme(value, 4) if isinstance(new_value, Theme): return new_value self.error(object, name, value) # ------------------------------------------------------------------------------- # Other trait definitions: # ------------------------------------------------------------------------- # The spacing between two items: Spacing = Range(-32, 32, 3) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/undo.py0000644000175100001730000003364300000000000017361 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Defines the manager for Undo and Redo history for Traits user interface support. """ import collections.abc from traits.api import ( Bool, Event, HasPrivateTraits, HasStrictTraits, HasTraits, Instance, Int, List, Property, Str, Trait, cached_property, observe, ) from pyface.undo.api import ( AbstractCommand, CommandStack, ICommand, ICommandStack, IUndoManager, UndoManager, ) NumericTypes = (int, float, complex) SimpleTypes = (str, bytes) + NumericTypes class AbstractUndoItem(AbstractCommand): """Abstract base class for undo items. This class is deprecated and will be removed in TraitsUI 8. Any custom subclasses of this class should either subclass from AbstractCommand, or provide the ICommand interface. """ #: A simple default name. name = "Edit" def do(self): """Does nothing. All undo items log events after they have happened, so by default they do not do anything when added to the history. """ pass def undo(self): """Undoes the change.""" raise NotImplementedError def redo(self): """Re-does the change.""" raise NotImplementedError def merge(self, other): """Merges two undo items if possible.""" return False class UndoItem(AbstractUndoItem): """A change to an object trait, which can be undone.""" # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- #: Object the change occurred on object = Instance(HasTraits) #: Name of the trait that changed name = Str() #: Old value of the changed trait old_value = Property() #: New value of the changed trait new_value = Property() def _get_old_value(self): return self._old_value def _set_old_value(self, value): if isinstance(value, list): value = value[:] self._old_value = value def _get_new_value(self): return self._new_value def _set_new_value(self, value): if isinstance(value, list): value = value[:] self._new_value = value def undo(self): """Undoes the change.""" try: setattr(self.object, self.name, self.old_value) except Exception: from traitsui.api import raise_to_debug raise_to_debug() def redo(self): """Re-does the change.""" try: setattr(self.object, self.name, self.new_value) except Exception: from traitsui.api import raise_to_debug raise_to_debug() def merge(self, undo_item): """Merges two undo items if possible.""" # Undo items are potentially mergeable only if they are of the same # class and refer to the same object trait, so check that first: if ( isinstance(undo_item, self.__class__) and (self.object is undo_item.object) and (self.name == undo_item.name) ): v1 = self.new_value v2 = undo_item.new_value t1 = type(v1) if isinstance(v2, t1): if isinstance(v1, str): # Merge two undo items if they have new values which are # strings which only differ by one character (corresponding # to a single character insertion, deletion or replacement # operation in a text editor): n1 = len(v1) n2 = len(v2) if abs(n1 - n2) > 1: return False n = min(n1, n2) i = 0 while (i < n) and (v1[i] == v2[i]): i += 1 if v1[i + (n2 <= n1) :] == v2[i + (n2 >= n1) :]: self.new_value = v2 return True elif isinstance(v1, collections.abc.Sequence): # Merge sequence types only if a single element has changed # from the 'original' value, and the element type is a # simple Python type: v1 = self.old_value if isinstance(v1, collections.abc.Sequence): # Note: wxColour says it's a sequence type, but it # doesn't support 'len', so we handle the exception # just in case other classes have similar behavior: try: if len(v1) == len(v2): diffs = 0 for i, item in enumerate(v1): titem = type(item) item2 = v2[i] if ( (titem not in SimpleTypes) or (not isinstance(item2, titem)) or (item != item2) ): diffs += 1 if diffs >= 2: return False if diffs == 0: return False self.new_value = v2 return True except Exception: pass elif t1 in NumericTypes: # Always merge simple numeric trait changes: self.new_value = v2 return True return False def __repr__(self): """Returns a "pretty print" form of the object.""" n = self.name cn = self.object.__class__.__name__ return "undo( %s.%s = %s )\nredo( %s.%s = %s )" % ( cn, n, self.old_value, cn, n, self.new_value, ) class ListUndoItem(AbstractUndoItem): """A change to a list, which can be undone.""" # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- #: Object that the change occurred on object = Instance(HasTraits) #: Name of the trait that changed name = Str() #: Starting index index = Int() #: Items added to the list added = List() #: Items removed from the list removed = List() def undo(self): """Undoes the change.""" try: list = getattr(self.object, self.name) list[self.index : (self.index + len(self.added))] = self.removed except Exception: from traitsui.api import raise_to_debug raise_to_debug() def redo(self): """Re-does the change.""" try: list = getattr(self.object, self.name) list[self.index : (self.index + len(self.removed))] = self.added except Exception: from traitsui.api import raise_to_debug raise_to_debug() def merge(self, undo_item): """Merges two undo items if possible.""" # Discard undo items that are identical to us. This is to eliminate # the same undo item being created by multiple listeners monitoring the # same list for changes: if ( isinstance(undo_item, self.__class__) and (self.object is undo_item.object) and (self.name == undo_item.name) and (self.index == undo_item.index) ): added = undo_item.added removed = undo_item.removed if (len(self.added) == len(added)) and ( len(self.removed) == len(removed) ): for i, item in enumerate(self.added): if item is not added[i]: break else: for i, item in enumerate(self.removed): if item is not removed[i]: break else: return True return False def __repr__(self): """Returns a 'pretty print' form of the object.""" return "undo( %s.%s[%d:%d] = %s )" % ( self.object.__class__.__name__, self.name, self.index, self.index + len(self.removed), self.added, ) class _MultiUndoItem(AbstractCommand): """The _MultiUndoItem class is an internal command that unifies commands.""" name = "Edit" #: The commands that make up this undo item. commands = List(Instance(ICommand)) def push(self, command): """Append a command, merging if possible.""" if len(self.commands) > 0: merged = self.commands[-1].merge(command) if merged: return self.commands.append(command) def merge(self, other): """Try and merge a command.""" return False def redo(self): """Redo the sub-commands.""" for cmd in self.commands: cmd.redo() def undo(self): """Undo the sub-commands.""" for cmd in self.commands: cmd.undo() class UndoHistory(HasStrictTraits): """Manages a list of undoable changes.""" # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- #: The undo manager for the history. manager = Instance(IUndoManager, allow_none=False) #: The command stack for the history. stack = Instance(ICommandStack, allow_none=False) #: The current position in the list now = Property(Int, observe='stack._index') #: Fired when state changes to undoable undoable = Event(False) #: Fired when state changes to redoable redoable = Event(False) #: Can an action be undone? can_undo = Property(Bool, observe='_can_undo') #: Can an action be redone? can_redo = Property(Bool, observe='_can_redo') _can_undo = Bool() _can_redo = Bool() def add(self, undo_item, extend=False): """Adds an UndoItem to the history.""" if extend: self.extend(undo_item) else: self.manager.active_stack = self.stack self.stack.push(undo_item) def extend(self, undo_item): """Extends the undo history. If possible the method merges the new UndoItem with the last item in the history; otherwise, it appends the new item. """ self.manager.active_stack = self.stack # get the last command in the stack # XXX this is using CommandStack internals that it should not # XXX this should be re-architected to use the macro interface # XXX it possibly should be removed altogether entries = self.stack._stack if len(entries) > 0: command = entries[-1].command if not isinstance(command, _MultiUndoItem): command = _MultiUndoItem(commands=[command]) entries[-1].command = command else: command = _MultiUndoItem(commands=[]) command.push(undo_item) def undo(self): """Undoes an operation.""" if self.can_undo: self.manager.undo() def redo(self): """Redoes an operation.""" if self.can_redo: self.manager.redo() def revert(self): """Reverts all changes made so far and clears the history.""" # undo everything self.manager.active_stack = self.stack self.stack.undo(sequence_nr=-1) self.clear() def clear(self): """Clears the undo history.""" self.manager.active_stack = self.stack self.stack.clear() @observe('manager.stack_updated') def _observe_stack_updated(self, event): """Update undo/redo state.""" self._can_undo = self.manager and self.manager.undo_name != "" self._can_redo = self.manager and self.manager.redo_name != "" @observe('_can_undo') def _observe_can_undo(self, event): self.undoable = event.new @observe('_can_redo') def _observe_can_redo(self, event): self.redoable = event.new @cached_property def _get_now(self): return self.stack._index + 1 def _get_can_undo(self): return self._can_undo def _get_can_redo(self): return self._can_redo def _manager_default(self): manager = UndoManager() return manager def _stack_default(self): stack = CommandStack(undo_manager=self.manager) return stack class UndoHistoryUndoItem(AbstractUndoItem): """An undo item for the undo history.""" # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- #: The undo history to undo or redo history = Instance(UndoHistory) def undo(self): """Undoes the change.""" history = self.history for i in range(history.now - 1, -1, -1): items = history.history[i] for j in range(len(items) - 1, -1, -1): items[j].undo() def redo(self): """Re-does the change.""" history = self.history for i in range(0, history.now): for item in history.history[i]: item.redo() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/value_tree.py0000644000175100001730000003546600000000000020554 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Defines tree node classes and editors for various types of values. """ import inspect from operator import itemgetter from types import FunctionType, MethodType from traits.api import ( Any, Bool, HasPrivateTraits, HasTraits, Instance, List, Str, ) from .tree_node import ObjectTreeNode, TreeNode, TreeNodeObject from .editors.tree_editor import TreeEditor class SingleValueTreeNodeObject(TreeNodeObject): """A tree node for objects of types that have a single value.""" # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- #: The parent of this node parent = Instance(TreeNodeObject) #: Name of the value name = Str() #: User-specified override of the default label label = Str() #: The value itself value = Any() #: Is the value readonly? readonly = Bool(False) def tno_allows_children(self, node): """Returns whether this object can have children (False for this class). """ return False def tno_has_children(self, node): """Returns whether the object has children (False for this class).""" return False def tno_can_rename(self, node): """Returns whether the object's children can be renamed (False for this class). """ return False def tno_can_copy(self, node): """Returns whether the object's children can be copied (True for this class). """ return True def tno_can_delete(self, node): """Returns whether the object's children can be deleted (False for this class). """ return False def tno_can_insert(self, node): """Returns whether the object's children can be inserted (False, meaning children are appended, for this class). """ return False def tno_get_icon(self, node, is_expanded): """Returns the icon for a specified object.""" return "@icons:%s_node" % self.__class__.__name__[:-4].lower() def tno_set_label(self, node, label): """Sets the label for a specified object.""" if label == "?": label = "" self.label = label def tno_get_label(self, node): """Gets the label to display for a specified object.""" if self.label != "": return self.label if self.name == "": return self.format_value(self.value) return "%s: %s" % (self.name, self.format_value(self.value)) def format_value(self, value): """Returns the formatted version of the value.""" return repr(value) def node_for(self, name, value): """Returns the correct node type for a specified value.""" for type, node in basic_types(): if isinstance(value, type): break else: node = OtherNode if inspect.isclass(value): node = ClassNode elif hasattr(value, "__class__"): node = ObjectNode return node( parent=self, name=name, value=value, readonly=self.readonly ) class MultiValueTreeNodeObject(SingleValueTreeNodeObject): """A tree node for objects of types that have multiple values.""" def tno_allows_children(self, node): """Returns whether this object can have children (True for this class).""" return True def tno_has_children(self, node): """Returns whether the object has children (True for this class).""" return True class StringNode(SingleValueTreeNodeObject): """A tree node for strings.""" def format_value(self, value): """Returns the formatted version of the value.""" n = len(value) if len(value) > 80: value = "%s...%s" % (value[:42], value[-35:]) return "%s [%d]" % (repr(value), n) class NoneNode(SingleValueTreeNodeObject): """A tree node for None values.""" pass class BoolNode(SingleValueTreeNodeObject): """A tree node for Boolean values.""" pass class IntNode(SingleValueTreeNodeObject): """A tree node for integer values.""" pass class FloatNode(SingleValueTreeNodeObject): """A tree node for floating point values.""" pass class ComplexNode(SingleValueTreeNodeObject): """A tree node for complex number values.""" pass class OtherNode(SingleValueTreeNodeObject): """A tree node for single-value types for which there is not another node type. """ pass class TupleNode(MultiValueTreeNodeObject): """A tree node for tuples.""" def format_value(self, value): """Returns the formatted version of the value.""" return "Tuple(%d)" % len(value) def tno_has_children(self, node): """Returns whether the object has children, based on the length of the tuple. """ return len(self.value) > 0 def tno_get_children(self, node): """Gets the object's children.""" node_for = self.node_for value = self.value if len(value) > 500: return ( [node_for("[%d]" % i, x) for i, x in enumerate(value[:250])] + [StringNode(value="...", readonly=True)] + [node_for("[%d]" % i, x) for i, x in enumerate(value[-250:])] ) return [node_for("[%d]" % i, x) for i, x in enumerate(value)] class ListNode(TupleNode): """A tree node for lists.""" def format_value(self, value): """Returns the formatted version of the value.""" return "List(%d)" % len(value) def tno_can_delete(self, node): """Returns whether the object's children can be deleted.""" return not self.readonly def tno_can_insert(self, node): """Returns whether the object's children can be inserted (vs. appended). """ return not self.readonly class SetNode(ListNode): """A tree node for sets.""" def format_value(self, value): """Returns the formatted version of the value.""" return "Set(%d)" % len(value) class ArrayNode(TupleNode): """A tree node for arrays.""" def format_value(self, value): """Returns the formatted version of the value.""" return "Array(%s)" % ",".join([str(n) for n in value.shape]) class DictNode(TupleNode): """A tree node for dictionaries.""" def format_value(self, value): """Returns the formatted version of the value.""" return "Dict(%d)" % len(value) def tno_get_children(self, node): """Gets the object's children.""" node_for = self.node_for items = [(repr(k), v) for k, v in self.value.items()] items.sort(key=itemgetter(0)) if len(items) > 500: return ( [node_for("[%s]" % k, v) for k, v in items[:250]] + [StringNode(value="...", readonly=True)] + [node_for("[%s]" % k, v) for k, v in items[-250:]] ) return [node_for("[%s]" % k, v) for k, v in items] def tno_can_delete(self, node): """Returns whether the object's children can be deleted.""" return not self.readonly class FunctionNode(SingleValueTreeNodeObject): """A tree node for functions""" def format_value(self, value): """Returns the formatted version of the value.""" return "Function %s()" % (value.__name__) # --------------------------------------------------------------------------- # 'MethodNode' class: # --------------------------------------------------------------------------- class MethodNode(MultiValueTreeNodeObject): def format_value(self, value): """Returns the formatted version of the value.""" type = "B" if value.__self__ is None: type = "Unb" return "%sound method %s.%s()" % ( type, value.__self__.__class__.__name__, value.__func__.__name__, ) def tno_has_children(self, node): """Returns whether the object has children.""" return self.value.__func__ is not None def tno_get_children(self, node): """Gets the object's children.""" return [self.node_for("Object", self.value.__self__)] class ObjectNode(MultiValueTreeNodeObject): """A tree node for objects.""" def format_value(self, value): """Returns the formatted version of the value.""" try: klass = value.__class__.__name__ except: klass = "???" return "%s(0x%08X)" % (klass, id(value)) def tno_has_children(self, node): """Returns whether the object has children.""" try: return len(self.value.__dict__) > 0 except: return False def tno_get_children(self, node): """Gets the object's children.""" items = [(k, v) for k, v in self.value.__dict__.items()] items.sort(key=itemgetter(0)) return [self.node_for("." + k, v) for k, v in items] class ClassNode(ObjectNode): """A tree node for classes.""" def format_value(self, value): """Returns the formatted version of the value.""" return value.__name__ class TraitsNode(ObjectNode): """A tree node for traits.""" def tno_has_children(self, node): """Returns whether the object has children.""" return len(self._get_names()) > 0 def tno_get_children(self, node): """Gets the object's children.""" names = sorted(self._get_names()) value = self.value node_for = self.node_for nodes = [] for name in names: try: item_value = getattr(value, name, "") except Exception as excp: item_value = "<%s>" % excp nodes.append(node_for("." + name, item_value)) return nodes def _get_names(self): """Gets the names of all defined traits or attributes.""" value = self.value names = {} for name in value.trait_names(type=lambda x: x != "event"): names[name] = None for name in value.__dict__.keys(): names[name] = None return list(names.keys()) def tno_when_children_replaced(self, node, listener, remove): """Sets up or removes a listener for children being replaced on a specified object. """ self._listener = listener self.value.on_trait_change( self._children_replaced, remove=remove, dispatch="ui" ) def _children_replaced(self): self._listener(self) def tno_when_children_changed(self, node, listener, remove): """Sets up or removes a listener for children being changed on a specified object. """ pass class RootNode(MultiValueTreeNodeObject): """A root node.""" def format_value(self, value): """Returns the formatted version of the value.""" return "" def tno_get_children(self, node): """Gets the object's children.""" return [self.node_for("", self.value)] # ------------------------------------------------------------------------- # Define the mapping of object types to nodes: # ------------------------------------------------------------------------- _basic_types = None def basic_types(): global _basic_types if _basic_types is None: # Create the mapping of object types to nodes: _basic_types = [ (type(None), NoneNode), (str, StringNode), (str, StringNode), (bool, BoolNode), (int, IntNode), (float, FloatNode), (complex, ComplexNode), (tuple, TupleNode), (list, ListNode), (set, SetNode), (dict, DictNode), (FunctionType, FunctionNode), (MethodType, MethodNode), (HasTraits, TraitsNode), ] try: from numpy import array _basic_types.append((type(array([1])), ArrayNode)) except ImportError: pass return _basic_types class _ValueTree(HasPrivateTraits): # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- #: List of arbitrary Python values contained in the tree: values = List(SingleValueTreeNodeObject) # ------------------------------------------------------------------------- # Defines the value tree editor(s): # ------------------------------------------------------------------------- # Nodes in a value tree: value_tree_nodes = [ ObjectTreeNode( node_for=[ NoneNode, StringNode, BoolNode, IntNode, FloatNode, ComplexNode, OtherNode, TupleNode, ListNode, ArrayNode, DictNode, SetNode, FunctionNode, MethodNode, ObjectNode, TraitsNode, RootNode, ClassNode, ] ) ] # Editor for a value tree: value_tree_editor = TreeEditor( auto_open=3, hide_root=True, editable=False, nodes=value_tree_nodes ) # Editor for a value tree with a root: value_tree_editor_with_root = TreeEditor( auto_open=3, editable=False, nodes=[ ObjectTreeNode( node_for=[ NoneNode, StringNode, BoolNode, IntNode, FloatNode, ComplexNode, OtherNode, TupleNode, ListNode, ArrayNode, DictNode, SetNode, FunctionNode, MethodNode, ObjectNode, TraitsNode, RootNode, ClassNode, ] ), TreeNode( node_for=[_ValueTree], auto_open=True, children="values", move=[SingleValueTreeNodeObject], copy=False, label="=Values", icon_group="traits_node", icon_open="traits_node", ), ], ) # ------------------------------------------------------------------------- # Defines a 'ValueTree' trait: # ------------------------------------------------------------------------- # Trait for a value tree: ValueTree = Instance(_ValueTree, (), editor=value_tree_editor_with_root) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/view.py0000644000175100001730000004126300000000000017363 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Defines the View class used to represent the structural content of a Traits-based user interface. """ from pyface.ui_traits import Image from pyface.action.schema.api import ActionManagerBuilder from traits.api import ( Any, Bool, Callable, Enum, Event, Float, Instance, List, PrefixList, Str, Trait, ) from .view_element import ViewElement, ViewSubElement from .ui import UI from .ui_traits import ( AButton, AnObject, Buttons, DockStyle, EditorStyle, ExportType, HelpId, SequenceTypes, ViewStatus, ) from .handler import Handler, default_handler from .group import Group from .item import Item from .include import Include # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- # Name of the view trait: AnId = Str(desc="the name of the view") # Contents of the view trait (i.e., a single Group object): Content = Instance(Group, desc="the content of the view") # An optional model/view factory for converting the model into a viewable # 'model_view' object AModelView = Callable( desc="the factory function for converting a model " "into a model/view object" ) # Reference to a Handler object trait: AHandler = Any(desc="the handler for the view") # Dialog window title trait: ATitle = Str(desc="the window title for the view") # User interface 'kind' trait. The values have the following meanings: # # * 'panel': An embeddable panel. This type of window is intended to be used as # part of a larger interface. # * 'subpanel': An embeddable panel that does not display command buttons, # even if the View specifies them. # * 'modal': A modal dialog box that operates on a clone of the object until # the user commits the change. # * 'nonmodal': A nonmodal dialog box that operates on a clone of the object # until the user commits the change # * 'live': A nonmodal dialog box that immediately updates the object. # * 'livemodal': A modal dialog box that immediately updates the object. # * 'popup': A temporary, frameless popup dialog that immediately updates the # object and is active only while the mouse pointer is in the dialog. # * 'info': A temporary, frameless popup dialog that immediately updates the # object and is active only while the dialog is still over the invoking # control. # * 'wizard': A wizard modal dialog box. A wizard contains a sequence of # pages, which can be accessed by clicking **Next** and **Back** buttons. # Changes to attribute values are applied only when the user clicks the # **Finish** button on the last page. AKind = PrefixList( ( "panel", "subpanel", "modal", "nonmodal", "livemodal", "live", "popup", "popover", "info", "wizard", ), default_value='live', desc="the kind of view window to create", cols=4, ) # Apply changes handler: OnApply = Callable( desc="the routine to call when modal changes are applied " "or reverted" ) # Is the dialog window resizable? IsResizable = Bool(False, desc="whether dialog can be resized or not") # Is the view scrollable? IsScrollable = Bool(False, desc="whether view should be scrollable or not") # The valid categories of imported elements that can be dragged into the view: ImportTypes = List( Str, desc="the categories of elements that can be " "dragged into the view" ) # The view position and size traits: Width = Float(-1e6, desc="the width of the view window") Height = Float(-1e6, desc="the height of the view window") XCoordinate = Float(-1e6, desc="the x coordinate of the view window") YCoordinate = Float(-1e6, desc="the y coordinate of the view window") # The result that should be returned if the user clicks the window or dialog # close button or icon CloseResult = Enum( None, True, False, desc="the result to return when the user clicks the " "window or dialog close button or icon", ) # The KeyBindings trait: AKeyBindings = Instance( "traitsui.key_bindings.KeyBindings", desc="the global key bindings for the view", ) class View(ViewElement): """A Traits-based user interface for one or more objects. The attributes of the View object determine the contents and layout of an attribute-editing window. A View object contains a set of Group, Item, and Include objects. A View object can be an attribute of an object derived from HasTraits, or it can be a standalone object. """ # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- #: A unique identifier for the view: id = AnId #: The top-level Group object for the view: content = Content #: The menu bar for the view. Usually requires a custom **handler**. #: This may be either a MenuBarManager or a MenuBarSchema menubar = Any #: The toolbar for the view. Usually requires a custom **handler**. #: This may be either a ToolBarManager or a ToolBarSchema toolbar = Any #: The ActionManagerBuilder to use with menu and toolbar schemas. #: The controller will usually be overridden. action_manager_builder = Any #: Status bar items to add to the view's status bar. The value can be: #: #: - **None**: No status bar for the view (the default). #: - string: Same as [ StatusItem( name = string ) ]. #: - StatusItem: Same as [ StatusItem ]. #: - [ [StatusItem|string], ... ]: Create a status bar with one field for #: each StatusItem in the list (or tuple). The status bar fields are #: defined from left to right in the order specified. A string value is #: converted to: StatusItem( name = string ): statusbar = ViewStatus #: List of button actions to add to the view. The **traitsui.menu** #: module defines standard buttons, such as **OKButton**, and standard sets #: of buttons, such as **ModalButtons**, which can be used to define a value #: for this attribute. This value can also be a list of button name strings, #: such as ``['OK', 'Cancel', 'Help']``. If set to the empty list, the #: view contains a default set of buttons (equivalent to **LiveButtons**: #: Undo/Redo, Revert, OK, Cancel, Help). To suppress buttons in the view, #: use the **NoButtons** variable, defined in **traitsui.menu**. buttons = Buttons #: The default button to activate when Enter is pressed. If not specified, #: pressing Enter will not activate any button. default_button = AButton #: The set of global key bindings for the view. Each time a key is pressed #: while the view has keyboard focus, the key is checked to see if it is one #: of the keys recognized by the KeyBindings object. If it is, the matching #: KeyBinding's method name is checked to see if it is defined on any of the #: object's in the view's context. If it is, the method is invoked. If the #: result of the method is **False**, then the search continues with the #: next object in the context. If any invoked method returns a non-False #: value, processing stops and the key is marked as having been handled. If #: all invoked methods return **False**, or no matching KeyBinding object is #: found, the key is processed normally. If the view has a non-empty *id* #: trait, the contents of the **KeyBindings** object will be saved as part #: of the view's persistent data: key_bindings = AKeyBindings #: The Handler object that provides GUI logic for handling events in the #: window. Set this attribute only if you are using a custom Handler. If #: not set, the default Traits UI Handler is used. handler = AHandler #: The factory function for converting a model into a model/view object: model_view = AModelView #: Title for the view, displayed in the title bar when the view appears as a #: secondary window (i.e., dialog or wizard). If not specified, "Edit #: properties" is used as the title. title = ATitle #: The name of the icon to display in the dialog window title bar: icon = Image #: The kind of user interface to create: kind = AKind #: The default object being edited: object = AnObject #: The default editor style of elements in the view: style = EditorStyle #: The default docking style to use for sub-groups of the view. The following #: values are possible: #: #: * 'fixed': No rearrangement of sub-groups is allowed. #: * 'horizontal': Moveable elements have a visual "handle" to the left by #: which the element can be dragged. #: * 'vertical': Moveable elements have a visual "handle" above them by #: which the element can be dragged. #: * 'tabbed': Moveable elements appear as tabbed pages, which can be #: arranged within the window or "stacked" so that only one appears at #: at a time. dock = DockStyle #: The image to display on notebook tabs: image = Image #: Called when modal changes are applied or reverted: on_apply = OnApply #: Can the user resize the window? resizable = IsResizable #: Can the user scroll the view? If set to True, window-level scroll bars #: appear whenever the window is too small to show all of its contents at #: one time. If set to False, the window does not scroll, but individual #: widgets might still contain scroll bars. scrollable = IsScrollable #: The category of exported elements: export = ExportType #: The valid categories of imported elements: imports = ImportTypes #: External help context identifier, which can be used by a custom help #: handler. This attribute is ignored by the default help handler. help_id = HelpId #: Requested x-coordinate (horizontal position) for the view window. This #: attribute can be specified in the following ways: #: #: * A positive integer: indicates the number of pixels from the left edge #: of the screen to the left edge of the window. #: * A negative integer: indicates the number of pixels from the right edge #: of the screen to the right edge of the window. #: * A floating point value between 0 and 1: indicates the fraction of the #: total screen width between the left edge of the screen and the left edge #: of the window. #: * A floating point value between -1 and 0: indicates the fraction of the #: total screen width between the right edge of the screen and the right #: edge of the window. x = XCoordinate #: Requested y-coordinate (vertical position) for the view window. This #: attribute behaves exactly like the **x** attribute, except that its value #: indicates the position of the top or bottom of the view window relative #: to the top or bottom of the screen. y = YCoordinate #: Requested width for the view window, as an (integer) number of pixels, or #: as a (floating point) fraction of the screen width. width = Width #: Requested height for the view window, as an (integer) number of pixels, or #: as a (floating point) fraction of the screen height. height = Height #: Class of dropped objects that can be added: drop_class = Any() #: Event when the view has been updated: updated = Event() #: What result should be returned if the user clicks the window or dialog #: close button or icon? close_result = CloseResult #: Note: Group objects delegate their 'object' and 'style' traits to the #: View # -- Deprecated Traits (DO NOT USE) --------------------------------------- ok = Bool(False) cancel = Bool(False) undo = Bool(False) redo = Bool(False) apply = Bool(False) revert = Bool(False) help = Bool(False) def __init__(self, *values, **traits): """Initializes the object.""" ViewElement.__init__(self, **traits) self.set_content(*values) def set_content(self, *values): """Sets the content of a view.""" content = [] accum = [] for value in values: if isinstance(value, ViewSubElement): content.append(value) elif type(value) in SequenceTypes: content.append(Group(*value)) elif ( isinstance(value, str) and (value[:1] == "<") and (value[-1:] == ">") ): # Convert string to an Include value: content.append(Include(value[1:-1].strip())) else: content.append(Item(value)) # If there are any 'Item' objects in the content, wrap the content in a # Group: for item in content: if isinstance(item, Item): content = [Group(*content)] break # Wrap all of the content up into a Group and save it as our content: self.content = Group(container=self, *content) def ui( self, context, parent=None, kind=None, view_elements=None, handler=None, id="", scrollable=None, args=None, ): """Creates a **UI** object, which generates the actual GUI window or panel from a set of view elements. 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. parent : window component The window parent of the View object's window kind : string The kind of window to create. See the **AKind** trait for details. If *kind* is unspecified or None, the **kind** attribute of the View object is used. view_elements : ViewElements object The set of Group, Item, and Include objects contained in the view. Do not use this parameter when calling this method directly. 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. """ handler = handler or self.handler or default_handler() if not isinstance(handler, Handler): handler = handler() if args is not None: handler.trait_set(**args) if not isinstance(context, dict): context = context.trait_context() context.setdefault("handler", handler) handler = context["handler"] if self.model_view is not None: context["object"] = self.model_view(context["object"]) self_id = self.id if self_id != "": if id != "": id = "%s:%s" % (self_id, id) else: id = self_id if scrollable is None: scrollable = self.scrollable ui = UI( view=self, context=context, handler=handler, view_elements=view_elements, title=self.title, id=id, scrollable=scrollable, ) if kind is None: kind = self.kind ui.ui(parent, kind) return ui def replace_include(self, view_elements): """Replaces any items that have an ID with an Include object with the same ID, and puts the object with the ID into the specified ViewElements object. """ if self.content is not None: self.content.replace_include(view_elements) def __repr__(self): """Returns a "pretty print" version of the View.""" if self.content is None: return "()" return "( %s )" % ", ".join( [item.__repr__() for item in self.content.content] ) def _action_manager_builder_default(self): return ActionManagerBuilder() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/view_element.py0000644000175100001730000001440000000000000021065 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Defines the abstract ViewElement class that all trait view template items (i.e., View, Group, Item, Include) derive from. """ import re from traits.api import AbstractViewElement, Bool, HasPrivateTraits, Instance from .ui_traits import ( AnObject, DockStyle, EditorStyle, ExportType, HelpId, Image, ) label_pat = re.compile(r"^(.*)\[(.*)\](.*)$", re.MULTILINE | re.DOTALL) label_pat2 = re.compile(r"^(.*){(.*)}(.*)$", re.MULTILINE | re.DOTALL) # ------------------------------------------------------------------------- # 'ViewElement' class (abstract): # ------------------------------------------------------------------------- class ViewElement(HasPrivateTraits): """An element of a view.""" def replace_include(self, view_elements): """Searches the current object's **content** attribute for objects that have an **id** attribute, and replaces each one with an Include object with the same **id** value, and puts the replaced object into the specified ViewElements object. Parameters ---------- view_elements : ViewElements object Object containing Group, Item, and Include objects """ pass # Normally overridden in a subclass def is_includable(self): """Returns whether the object is replacable by an Include object.""" return False # Normally overridden in a subclass class DefaultViewElement(ViewElement): """A view element that can be used as a default value for traits whose value is a view element. """ # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- #: The default context object to edit: object = AnObject #: The default editor style to use: style = EditorStyle #: The default dock style to use: dock = DockStyle #: The default notebook tab image to use: image = Image #: The category of elements dragged out of the view: export = ExportType #: Should labels be added to items in a group? show_labels = Bool(True) # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- # The container trait used by ViewSubElements: Container = Instance(ViewElement, factory=DefaultViewElement) # ------------------------------------------------------------------------- # 'ViewSubElement' class (abstract): # ------------------------------------------------------------------------- class ViewSubElement(ViewElement): """Abstract class representing elements that can be contained in a view.""" # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- #: The object this ViewSubElement is contained in; must be a ViewElement. container = Container #: External help context identifier: help_id = HelpId def _split(self, name, value, char, finder, assign, result): """Splits a string at a specified character.""" col = finder(value, char) if col < 0: return value items = (value[:col].strip(), value[col + 1 :].strip()) if items[assign] != "": setattr(self, name, items[assign]) return items[result] def _option(self, string, option, name, value): """Sets a object trait if a specified option string is found.""" col = string.find(option) if col >= 0: string = string[:col] + string[col + len(option) :] setattr(self, name, value) return string def _parse_style(self, value): """Parses any of the one-character forms of the **style** trait.""" value = self._option(value, "$", "style", "simple") value = self._option(value, "@", "style", "custom") value = self._option(value, "*", "style", "text") value = self._option(value, "~", "style", "readonly") value = self._split("style", value, ";", str.rfind, 1, 0) return value def _parse_label(self, value): """Parses a '[label]' value from the string definition.""" match = label_pat.match(value) if match is not None: self._parsed_label() else: match = label_pat2.match(value) empty = False if match is not None: self.label = match.group(2).strip() empty = self.label == "" value = match.group(1) + match.group(3) return (value, empty) def _parsed_label(self): """Handles a label being found in the string definition.""" pass def _repr_value(self, value, prefix="", suffix="", ignore=""): """Returns a "pretty print" version of a specified Item trait value.""" if value == ignore: return "" return "%s%s%s" % (prefix, value, suffix) def _repr_options(self, *names): """Returns a 'pretty print' version of a list of traits.""" result = [] for name in names: value = getattr(self, name) if value != self.trait(name).default_value_for(self, name): result.append((name, repr(value))) if len(result) > 0: n = max([len(name) for name, value in result]) return ",\n".join( ["%s = %s" % (name.ljust(n), value) for name, value in result] ) return None def _indent(self, string, indent=" "): """Indents each line in a specified string by 4 spaces.""" return "\n".join([indent + s for s in string.split("\n")]) # Register ViewElement as implementing AbstractViewElement # TODO: eventually have ViewElement inherit directly AbstractViewElement.register(ViewElement) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/view_elements.py0000644000175100001730000001362200000000000021255 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Define the ViewElements class, which is used to define a (typically class-based) hierarchical name space of related ViewElement objects. Normally there is a ViewElements object associated with each Traits-based class, which contains all of the ViewElement objects associated with the class. The ViewElements object is also linked to the ViewElements objects of its associated class's parent classes. """ from traits.api import HasStrictTraits, List, Dict, Str, Int, Any, TraitError from .view_element import ViewElement # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- # Trait for contents of a ViewElements object content_trait = Dict(str, ViewElement) class ViewElements(HasStrictTraits): """Defines a hierarchical name space of related ViewElement objects.""" # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- #: Dictionary containing the named ViewElement items content = content_trait def find(self, name, stack=None): """Finds a specified ViewElement within the specified (optional) search context. """ # Assume search starts from the beginning the of the search order: i = 0 # If a stack was specified, see if there is a matching entry in the # stack already: if stack is not None: for ssi in stack: if name == ssi.id: # Match found, resume search at next ViewElements object # in the search order: i = ssi.context + 1 break # Search for a matching name starting at the specified ViewElements # object in the search order: for j, ves in enumerate(self._get_search_order()[i:]): result = ves.content.get(name) if result is not None: # Match found. If there is a stack, push matching name and # ViewElements context onto it: if stack is not None: stack[0:0] = [SearchStackItem(id=name, context=i + j)] # Return the ViewElement object that matched the name: return result # Indicate no match was found: return None def filter_by(self, klass=None): """Returns a sorted list of all names accessible from the ViewElements object that are of a specified (ViewElement) type. """ if klass is None: from . import view klass = view.View result = [] # Add each item in the search order which is of the right class and # which is not already in the result list: for ves in self._get_search_order(): for name, ve in ves.content.items(): if isinstance(ve, klass) and (name not in result): result.append(name) # Sort the resulting list of names: result.sort() # Return the result: return result def _parents__changed(self): self._search_order = None def _parents_items_changed(self): self._search_order = None def _get_search_order(self): if self._search_order is None: self._search_order = self._mro() return self._search_order # ------------------------------------------------------------------------- # Compute the Python 'C3' algorithm used to determine a class's 'mro' # and apply it to the 'parents' of the ViewElements to determine the # correct search order: # ------------------------------------------------------------------------- def _mro(self): return self._merge( [[self]] + [parent._get_search_order()[:] for parent in self.parents] + [self.parents[:]] ) def _merge(self, seqs): result = [] while True: # Remove any empty sequences from the list: seqs = [seq for seq in seqs if len(seq) > 0] if len(seqs) == 0: return result # Find merge candidates among the sequence heads: for seq in seqs: candidate = seq[0] if len([s for s in seqs if candidate in s[1:]]) == 0: break else: raise TraitError("Inconsistent ViewElements hierarchy") # Add the candidate to the result: result.append(candidate) # Then remove the candidate: for seq in seqs: if seq[0] == candidate: del seq[0] def __repr__(self): """Returns a "pretty print" version of the ViewElements object.""" return self.content.__repr__() # ------------------------------------------------------------------------- # Define forward reference traits: # ------------------------------------------------------------------------- ViewElements.add_class_trait("parents", List(ViewElements)) ViewElements.add_class_trait("_search_order", Any) class SearchStackItem(HasStrictTraits): # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- #: Name that was looked up id = Str() #: Index into the 'mro' list of ViewElements that the ID was found in context = Int() ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1684768028.1238067 traitsui-8.0.0/traitsui/wx/0000755000175100001730000000000000000000000016467 5ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/wx/__init__.py0000644000175100001730000000155600000000000020607 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Defines the concrete implementations of the traits Toolkit interface for the wxPython user interface toolkit. """ # ------------------------------------------------------------------------- # Define the reference to the exported GUIToolkit object: # ------------------------------------------------------------------------- from . import toolkit # Reference to the GUIToolkit object for wxPython toolkit = toolkit.GUIToolkit("traitsui", "wx", "traitsui.wx") ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/wx/animated_gif_editor.py0000644000175100001730000000453600000000000023026 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Defines an editor for playing animated GIF files. """ from wx.adv import Animation, GenericAnimationCtrl from traits.api import Bool, Str from traitsui.wx.editor import Editor from traitsui.basic_editor_factory import BasicEditorFactory class _AnimatedGIFEditor(Editor): """Editor that displays an animated GIF file.""" # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- #: Is the animated GIF file currently playing? playing = Bool(True) def init(self, parent): """Finishes initializing the editor by creating the underlying toolkit widget. """ self._animate = Animation(self.value) self.control = GenericAnimationCtrl(parent, -1, self._animate) self.control.SetUseWindowBackgroundColour() self.sync_value(self.factory.playing, "playing", "from") self.set_tooltip() def update_editor(self): """Updates the editor when the object trait changes externally to the editor. """ if not self.playing: self.control.Stop() self.control.LoadFile(self.value) self._file_loaded = True if self.playing: self.control.Play() def _playing_changed(self): if self._file_loaded: if self.playing: self.control.Play() else: self.control.Stop() # ------------------------------------------------------------------------- # Create the editor factory object: # ------------------------------------------------------------------------- # wxPython editor factory for animated GIF editors: class AnimatedGIFEditor(BasicEditorFactory): #: The editor class to be created: klass = _AnimatedGIFEditor #: The optional trait used to control whether the animated GIF file is #: playing or not: playing = Str() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/wx/array_editor.py0000644000175100001730000000205100000000000021523 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Defines array editors for the WX user interface toolkit. """ from traitsui.editors.array_editor import SimpleEditor as BaseSimpleEditor from .editor import Editor # ------------------------------------------------------------------------- # 'SimpleEditor' class: # ------------------------------------------------------------------------- class SimpleEditor(BaseSimpleEditor, Editor): """Simple style of editor for arrays.""" # FIXME: This class has been re-defined here simply so it inherits from the # wx Editor class. pass class ReadonlyEditor(SimpleEditor): #: Set the value of the readonly trait. readonly = True ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/wx/array_view_editor.py0000644000175100001730000000142400000000000022560 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! from traitsui.ui_editors.array_view_editor import ( _ArrayViewEditor as BaseArrayViewEditor, ) from .ui_editor import UIEditor # ------------------------------------------------------------------------- # '_ArrayViewEditor' class: # ------------------------------------------------------------------------- class _ArrayViewEditor(BaseArrayViewEditor, UIEditor): pass ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/wx/boolean_editor.py0000644000175100001730000000500500000000000022026 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Defines the various Boolean editors for the wxPython user interface toolkit. """ import wx 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 from .constants import ReadonlyColor 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 = wx.CheckBox(parent, -1, "") self.control.Bind(wx.EVT_CHECKBOX, self.update_object) self.set_tooltip() def dispose(self): self.control.Unbind(wx.EVT_CHECKBOX) def update_object(self, event): """Handles the user clicking the checkbox.""" self.value = self.control.GetValue() != 0 def update_editor(self): """Updates the editor when the object trait changes externally to the editor. """ self.control.SetValue(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 = wx.TextCtrl(parent, -1, "", style=wx.TE_READONLY) self.control.SetBackgroundColour(ReadonlyColor) # ------------------------------------------------------------------------- # Updates the editor when the object trait changes external to the editor: # # (Should normally be overridden in a subclass) # ------------------------------------------------------------------------- def update_editor(self): """Updates the editor when the object trait changes externally to the editor. """ if self.value: self.control.SetValue("True") else: self.control.SetValue("False") ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/wx/button_editor.py0000644000175100001730000000742600000000000021733 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Defines the various button editors for the wxPython user interface toolkit. """ import wx from pyface.ui_traits import Image from traits.api import Str, observe from .editor import Editor # ------------------------------------------------------------------------- # 'SimpleEditor' class: # ------------------------------------------------------------------------- class SimpleEditor(Editor): """Simple style editor for a button.""" # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- #: The button label label = 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) self.control = wx.Button(parent, -1, self.string_value(label)) self.sync_value(self.factory.label_value, "label", "from") self.control.Bind(wx.EVT_BUTTON, self.update_object) self.set_tooltip() def _label_changed(self, label): self.control.SetLabel(self.string_value(label)) def update_object(self, event): """Handles the user clicking the button by setting the factory value on the object. """ factory = self.factory self.value = factory.value # If there is an associated view, then display it: if factory.view is not None: self.object.edit_traits(view=factory.view, parent=self.control) def update_editor(self): """Updates the editor when the object trait changes externally to the editor. """ pass def dispose(self): """Disposes of the contents of an editor.""" self.control.Unbind(wx.EVT_BUTTON) super().dispose() class CustomEditor(SimpleEditor): """Custom style editor for a button, which can contain an image.""" #: The button image image = Image() def init(self, parent): """Finishes initializing the editor by creating the underlying toolkit widget. """ from pyface.ui.wx.image_button import ImageButton factory = self.factory if self.factory.label: label = self.factory.label else: label = self.item.get_label(self.ui) self._control = ImageButton( parent, label=self.string_value(label), image=factory.image, style=factory.style, orientation=factory.orientation, width_padding=factory.width_padding, height_padding=factory.height_padding, ) self.control = self._control.control self._control.on_trait_change( self.update_object, "clicked", dispatch="ui" ) self.sync_value(self.factory.label_value, "label", "from") self.sync_value(self.factory.image_value, "image", "from") self.set_tooltip() def _label_changed(self, label): self._control.label = self.string_value(label) @observe("image") def _image_updated(self, event): image = event.new self._control.image = image def dispose(self): """Disposes of the contents of an editor.""" self._control.on_trait_change( self.update_object, "clicked", remove=True ) super().dispose() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/wx/check_list_editor.py0000644000175100001730000002050400000000000022520 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Defines the various editors for multi-selection enumerations, for the wxPython user interface toolkit. """ import logging import wx from traits.api import List, Str, TraitError from .editor_factory import TextEditor as BaseTextEditor from .editor import EditorWithList from .helper import TraitsUIPanel from functools import reduce logger = logging.getLogger(__name__) # ------------------------------------------------------------------------- # 'SimpleEditor' class: # ------------------------------------------------------------------------- class SimpleEditor(EditorWithList): """Simple style of editor for checklists, which displays a combo box.""" # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- #: Checklist item names names = List(Str) #: Checklist item values values = List() def init(self, parent): """Finishes initializing the editor by creating the underlying toolkit widget. """ self.create_control(parent) super().init(parent) self.set_tooltip() def dispose(self): self.control.Unbind(wx.EVT_CHOICE) super().dispose() def create_control(self, parent): """Creates the initial editor control.""" self.control = wx.Choice( parent, -1, wx.Point(0, 0), wx.Size(100, 20), [] ) self.control.Bind(wx.EVT_CHOICE, self.update_object) def list_updated(self, values): """Handles updates to the list of legal checklist values.""" sv = self.string_value if (len(values) > 0) and isinstance(values[0], str): values = [(x, sv(x, str.capitalize)) for x in values] self.values = valid_values = [x[0] for x in values] self.names = [x[1] for x in values] # Make sure the current value is still legal: modified = False cur_value = parse_value(self.value) for i in range(len(cur_value) - 1, -1, -1): if cur_value[i] not in valid_values: try: del cur_value[i] modified = True except TypeError as e: logger.warning( "Unable to remove non-current value [%s] from " "values %s", cur_value[i], values, ) if modified: if isinstance(self.value, str): cur_value = ",".join(cur_value) self.value = cur_value self.rebuild_editor() def rebuild_editor(self): """Rebuilds the editor after its definition is modified.""" control = self.control control.Clear() for name in self.names: control.Append(name) self.update_editor() def update_object(self, event): """Handles the user selecting a new value from the combo box.""" value = self.values[self.names.index(event.GetString())] if not isinstance(self.value, str): value = [value] self.value = value def update_editor(self): """Updates the editor when the object trait changes externally to the editor. """ try: self.control.SetSelection( self.values.index(parse_value(self.value)[0]) ) except Exception: pass # ------------------------------------------------------------------------- # 'CustomEditor' class: # ------------------------------------------------------------------------- class CustomEditor(SimpleEditor): """Custom style of editor for checklists, which displays a set of check boxes. """ def create_control(self, parent): """Creates the initial editor control.""" # Create a panel to hold all of the check boxes self.control = panel = TraitsUIPanel(parent, -1) def rebuild_editor(self): """Rebuilds the editor after its definition is modified.""" panel = self.control panel.SetSizer(None) panel.DestroyChildren() cur_value = parse_value(self.value) # Create a sizer to manage the radio buttons: labels = self.names values = self.values n = len(labels) cols = self.factory.cols rows = (n + cols - 1) // cols # incr will keep track of how to increment index so that as we traverse # the grid in row major order, the elements are added to appear in # column major order incr = [n // cols] * cols rem = n % cols for i in range(cols): incr[i] += rem > i incr[-1] = -(reduce(lambda x, y: x + y, incr[:-1], 0) - 1) # e.g for a gird: # 0 2 4 # 1 3 5 # incr should be [2, 2, -3] if cols > 1: sizer = wx.GridSizer(0, cols, 2, 4) else: sizer = wx.BoxSizer(wx.VERTICAL) # Add the set of all possible choices: index = 0 # populate the layout in row_major order for i in range(rows): for j in range(cols): if n > 0: label = labels[index] control = wx.CheckBox(panel, -1, label) control.value = value = values[index] control.SetValue(value in cur_value) panel.Bind( wx.EVT_CHECKBOX, self.update_object, id=control.GetId() ) index += incr[j] n -= 1 else: control = wx.CheckBox(panel, -1, "") control.Show(False) sizer.Add(control, 0, wx.NORTH, 5) # Lay out the controls: panel.SetSizerAndFit(sizer) # FIXME: There are cases where one of the parent panel's of the check # list editor has a fixed 'min size' which prevents the check list # editor from expanding correctly, so we currently are making sure # that all of the parent panels do not have a fixed min size before # doing the layout/refresh: parent = panel.GetParent() while isinstance(parent, wx.Panel): parent.SetMinSize(wx.Size(-1, -1)) panel = parent parent = parent.GetParent() panel.Layout() panel.Refresh() def update_object(self, event): """Handles the user clicking one of the custom check boxes.""" control = event.GetEventObject() cur_value = parse_value(self.value) if control.GetValue(): cur_value.append(control.value) else: cur_value.remove(control.value) if isinstance(self.value, str): cur_value = ",".join(cur_value) self.value = cur_value def update_editor(self): """Updates the editor when the object trait changes externally to the editor. """ new_values = parse_value(self.value) for control in self.control.GetChildren(): if control.IsShown(): control.SetValue(control.value in new_values) class TextEditor(BaseTextEditor): """Text style of editor for checklists, which displays a text field.""" def update_object(self, event): """Handles the user changing the contents of the edit control.""" try: value = self.control.GetValue() value = eval(value) except: pass try: self.value = value except TraitError as excp: pass # ------------------------------------------------------------------------- # Parse a value into a list: # ------------------------------------------------------------------------- def parse_value(value): """Parses a value into a list.""" if value is None: return [] if not isinstance(value, str): return value[:] return [x.strip() for x in value.split(",")] ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/wx/code_editor.py0000644000175100001730000003400400000000000021322 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Defines a source code editor for the wxPython user interface toolkit, useful for tools such as debuggers. """ import wx import wx.stc as stc from traits.api import Str, List, Int, Event, Bool, TraitError, observe from traits.trait_base import SequenceTypes from pyface.api import PythonEditor from pyface.wx.python_stc import faces from .editor import Editor from .constants import OKColor, ErrorColor # Marker line constants: # Marks a marked line MARK_MARKER = 0 # Marks a line matching the current search SEARCH_MARKER = 1 # Marks the currently selected line SELECTED_MARKER = 2 # ------------------------------------------------------------------------- # 'SourceEditor' class: # ------------------------------------------------------------------------- class SourceEditor(Editor): """Editor for source code, which displays a Pyface PythonEditor.""" # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- #: The code editor is scrollable. This value overrides the default. scrollable = True #: Is the editor read only? readonly = Bool(False) #: The currently selected line selected_line = Int() #: The currently selected text selected_text = Str() #: The list of line numbers to mark mark_lines = List(Int) #: The current line number line = Event() #: The current column column = Event() #: calltip clicked event calltip_clicked = Event() #: The STC lexer use lexer = Int() #: The lines to be dimmed dim_lines = List(Int) dim_color = Str() _dim_style_number = Int(16) # 0-15 are reserved for the python lexer #: The lines to have squiggles drawn under them squiggle_lines = List(Int) squiggle_color = Str() def init(self, parent): """Finishes initializing the editor by creating the underlying toolkit widget. """ factory = self.factory self._editor = editor = PythonEditor( parent, show_line_numbers=factory.show_line_numbers ) editor.create() self.control = control = editor.control # There are a number of events which aren't well documented that look # to be useful in future implmentations, below are a subset of the # events that look interesting: # EVT_STC_AUTOCOMP_SELECTION # EVT_STC_HOTSPOT_CLICK # EVT_STC_HOTSPOT_DCLICK # EVT_STC_DOUBLECLICK # EVT_STC_MARGINCLICK control.SetSize(wx.Size(300, 124)) # Clear out the goofy hotkeys for zooming text control.CmdKeyClear(ord("B"), stc.STC_SCMOD_CTRL) control.CmdKeyClear(ord("N"), stc.STC_SCMOD_CTRL) # Set up the events control.Bind(wx.EVT_KILL_FOCUS, self.wx_update_object) control.Bind(stc.EVT_STC_CALLTIP_CLICK, self._calltip_clicked) if factory.auto_scroll and (factory.selected_line != ""): control.Bind(wx.EVT_SIZE, self._update_selected_line) if factory.auto_set: editor.on_trait_change( self.update_object, "changed", dispatch="ui" ) if factory.key_bindings is not None: editor.on_trait_change( self.key_pressed, "key_pressed", dispatch="ui" ) if self.readonly: control.SetReadOnly(True) # Set up the lexer control.SetLexer(stc.STC_LEX_CONTAINER) control.Bind(stc.EVT_STC_STYLENEEDED, self._style_needed) try: self.lexer = getattr(stc, "STC_LEX_" + self.factory.lexer.upper()) except AttributeError: self.lexer = stc.STC_LEX_NULL # Define the markers we use: control.MarkerDefine( MARK_MARKER, stc.STC_MARK_BACKGROUND, background=wx.Colour(factory.mark_color_), ) control.MarkerDefine( SEARCH_MARKER, stc.STC_MARK_BACKGROUND, background=wx.Colour(factory.search_color_), ) control.MarkerDefine( SELECTED_MARKER, stc.STC_MARK_BACKGROUND, background=wx.Colour(factory.selected_color_), ) # Make sure the editor has been initialized: self.update_editor() # Set up any event listeners: self.sync_value(factory.mark_lines, "mark_lines", "from", is_list=True) self.sync_value(factory.selected_line, "selected_line", "from") self.sync_value(factory.selected_text, "selected_text", "to") self.sync_value(factory.line, "line") self.sync_value(factory.column, "column") self.sync_value(factory.calltip_clicked, "calltip_clicked") self.sync_value(factory.dim_lines, "dim_lines", "from", is_list=True) if self.factory.dim_color == "": self.dim_color = "dark grey" else: self.sync_value(factory.dim_color, "dim_color", "from") self.sync_value( factory.squiggle_lines, "squiggle_lines", "from", is_list=True ) if factory.squiggle_color == "": self.squiggle_color = "red" else: self.sync_value(factory.squiggle_color, "squiggle_color", "from") # Check if we need to monitor the line or column position being # changed: if ( (factory.line != "") or (factory.column != "") or (factory.selected_text != "") ): control.Bind(stc.EVT_STC_UPDATEUI, self._position_changed) self.set_tooltip() def wx_update_object(self, event): """Handles the user entering input data in the edit control.""" self.update_object() event.Skip() def update_object(self): """Handles the user entering input data in the edit control.""" if not self._locked: try: value = self.control.GetText() if isinstance(self.value, SequenceTypes): value = value.split() self.value = value self.control.SetBackgroundColour(OKColor) self.control.Refresh() except TraitError as excp: pass def update_editor(self): """Updates the editor when the object trait changes externally to the editor. """ self._locked = True new_value = self.value if isinstance(new_value, SequenceTypes): new_value = "\n".join([line.rstrip() for line in new_value]) control = self.control if control.GetText() != new_value: readonly = control.GetReadOnly() control.SetReadOnly(False) l1 = control.GetFirstVisibleLine() pos = control.GetCurrentPos() control.SetText(new_value) control.GotoPos(pos) control.ScrollToLine(l1) control.SetReadOnly(readonly) self._mark_lines_changed() self._selected_line_changed() self._style_document() self._locked = False def _calltip_clicked(self, event): self.calltip_clicked = True def _mark_lines_changed(self): """Handles the set of marked lines being changed.""" lines = self.mark_lines control = self.control lc = control.GetLineCount() control.MarkerDeleteAll(MARK_MARKER) for line in lines: if 0 < line <= lc: control.MarkerAdd(line - 1, MARK_MARKER) control.Refresh() def _mark_lines_items_changed(self): self._mark_lines_changed() def _selected_line_changed(self): """Handles a change in which line is currently selected.""" line = self.selected_line control = self.control line = max(1, min(control.GetLineCount(), line)) - 1 control.MarkerDeleteAll(SELECTED_MARKER) control.MarkerAdd(line, SELECTED_MARKER) control.GotoLine(line) if self.factory.auto_scroll: control.ScrollToLine(line - (control.LinesOnScreen() // 2)) control.Refresh() def _line_changed(self, line): if not self._locked: self.control.GotoLine(line - 1) def _column_changed(self, column): if not self._locked: control = self.control line = control.LineFromPosition(control.GetCurrentPos()) control.GotoPos(control.PositionFromLine(line) + column - 1) def _position_changed(self, event): """Handles the cursor position being changed.""" control = self.control pos = control.GetCurrentPos() line = control.LineFromPosition(pos) self._locked = True self.line = line + 1 self.column = pos - control.PositionFromLine(line) + 1 self._locked = False self.selected_text = control.GetSelectedText() def key_pressed(self, event): """Handles a key being pressed within the editor.""" self.factory.key_bindings.do( event.event, self.ui.handler, self.ui.info ) def _dim_color_changed(self): self.control.StyleSetForeground(self._dim_style_number, self.dim_color) self.control.StyleSetFaceName(self._dim_style_number, "courier new") self.control.StyleSetSize(self._dim_style_number, faces["size"]) self.control.Refresh() def _squiggle_color_changed(self): self.control.IndicatorSetStyle(2, stc.STC_INDIC_SQUIGGLE) self.control.IndicatorSetForeground(2, self.squiggle_color) self.control.Refresh() @observe("dim_lines, squiggle_lines") def _style_document(self, event=None): """Force the STC to fire a STC_STYLENEEDED event for the entire document. """ self.control.ClearDocumentStyle() self.control.Colourise(0, -1) self.control.Refresh() def _style_needed(self, event): """Handles an STC request for styling for some area.""" position = self.control.GetEndStyled() start_line = self.control.LineFromPosition(position) end = event.GetPosition() end_line = self.control.LineFromPosition(end) # Fixes a strange a bug with the STC widget where creating a new line # after a dimmed line causes it to mysteriously lose its styling if start_line in self.dim_lines: start_line -= 1 # Trying to Colourise only the lines that we want does not seem to work # so we do the whole area and then override the styling on certain # lines if self.lexer != stc.STC_LEX_NULL: self.control.SetLexer(self.lexer) self.control.Colourise(position, end) self.control.SetLexer(stc.STC_LEX_CONTAINER) for line in range(start_line, end_line + 1): # We don't use LineLength here because it includes newline # characters. Styling these leads to strange behavior. position = self.control.PositionFromLine(line) style_length = self.control.GetLineEndPosition(line) - position if line + 1 in self.dim_lines: # Set styling mask to only style text bits, not indicator bits self.control.StartStyling(position) self.control.SetStyling(style_length, self._dim_style_number) elif self.lexer == stc.STC_LEX_NULL: self.control.StartStyling(position) self.control.SetStyling(style_length, stc.STC_STYLE_DEFAULT) if line + 1 in self.squiggle_lines: self.control.StartStyling(position) self.control.SetStyling(style_length, stc.STC_INDIC2_MASK) else: self.control.StartStyling(position) self.control.SetStyling(style_length, stc.STC_STYLE_DEFAULT) def error(self, excp): """Handles an error that occurs while setting the object's trait value.""" self.control.SetBackgroundColour(ErrorColor) self.control.Refresh() def dispose(self): """Disposes of the contents of an editor.""" if self.control is not None: if self.factory.auto_set: self._editor.on_trait_change( self.update_object, "changed", remove=True ) if self.factory.key_bindings is not None: self._editor.on_trait_change( self.key_pressed, "key_pressed", remove=True ) self.control.Unbind(wx.EVT_KILL_FOCUS) self.control.Unbind(stc.EVT_STC_CALLTIP_CLICK) self.control.Unbind(stc.EVT_STC_STYLENEEDED) if ( (self.factory.line != "") or (self.factory.column != "") or (self.factory.selected_text != "") ): self.control.Unbind(stc.EVT_STC_UPDATEUI) super().dispose() # -- UI preference save/restore interface --------------------------------- def restore_prefs(self, prefs): """Restores any saved user preference information associated with the editor. """ if self.factory.key_bindings is not None: key_bindings = prefs.get("key_bindings") if key_bindings is not None: self.factory.key_bindings.merge(key_bindings) def save_prefs(self): """Returns any user preference information associated with the editor.""" return {"key_bindings": self.factory.key_bindings} # Define the simple, custom, text and readonly editors, which will be accessed # by the editor factory for code editors. CustomEditor = SimpleEditor = TextEditor = SourceEditor class ReadonlyEditor(SourceEditor): #: Set the value of the readonly trait. readonly = True ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/wx/color_editor.py0000644000175100001730000003145700000000000021537 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Defines the various color editors for the Wx user interface toolkit. """ import sys import wx import wx.adv from traits.api import List, TraitError from traitsui.editors.color_editor import ( ToolkitEditorFactory as BaseToolkitEditorFactory, ) from .editor_factory import SimpleEditor as BaseSimpleEditor from .editor_factory import ReadonlyEditor as BaseReadonlyEditor from .editor_factory import TextEditor as BaseTextEditor from .color_trait import w3c_color_database from .helper import TraitsUIPanel ColorTypes = (wx.Colour,) # --------------------------------------------------------------------------- # The Wx ToolkitEditorFactory class. # --------------------------------------------------------------------------- class ToolkitEditorFactory(BaseToolkitEditorFactory): """Wx editor factory for color editors.""" def to_wx_color(self, editor, color=None): """Gets the wxPython color equivalent of the object trait.""" if color is None: if self.mapped: color = getattr(editor.object, editor.name + "_") else: color = getattr(editor.object, editor.name) if isinstance(color, tuple): color = wx.Colour(*[int(round(c * 255.0)) for c in color]) return color def from_wx_color(self, color): """Gets the application equivalent of a wxPython value.""" return color.Red(), color.Green(), color.Blue() def str_color(self, color): """Returns the text representation of a specified color value.""" if isinstance(color, ColorTypes): alpha = color.Alpha() if alpha == 255: return "rgb(%d,%d,%d)" % ( color.Red(), color.Green(), color.Blue(), ) return "rgba(%d,%d,%d,%d)" % ( color.Red(), color.Green(), color.Blue(), alpha, ) return str(color) # ------------------------------------------------------------------------------ # 'ColorComboBox' class: # ------------------------------------------------------------------------------ class ColorComboBox(wx.adv.OwnerDrawnComboBox): def OnDrawItem(self, dc, rect, item, flags): r = wx.Rect(rect.x, rect.y, rect.width, rect.height) r.Deflate(3, 0) swatch_size = r.height - 2 color_name = self.GetString(item) dc.DrawText( color_name, r.x + 3, r.y + (r.height - dc.GetCharHeight()) // 2 ) if color_name == "custom": swatch = wx.Rect( r.x + r.width - swatch_size, r.y + 1, swatch_size, swatch_size ) dc.GradientFillLinear( swatch, wx.Colour(255, 255, 0), wx.Colour(0, 0, 255) ) else: color = w3c_color_database.Find(color_name) brush = wx.Brush(color) dc.SetBrush(brush) dc.DrawRectangle( r.x + r.width - swatch_size, r.y + 1, swatch_size, swatch_size ) # ------------------------------------------------------------------------------ # 'SimpleColorEditor' class: # ------------------------------------------------------------------------------ class SimpleColorEditor(BaseSimpleEditor): """Simple style of color editor, which displays a text field whose background color is the color value. Selecting the text field displays a dialog box for selecting a new color value. """ # -------------------------------------------------------------------------- # Invokes the pop-up editor for an object trait: # -------------------------------------------------------------------------- choices = List() def _choices_default(self): """by default, uses the W3C 16 color names.""" return [ "aqua", "black", "blue", "fuchsia", "gray", "green", "lime", "maroon", "navy", "olive", "purple", "red", "silver", "teal", "white", "yellow", "custom", ] def init(self, parent): """ Finishes initializing the editor by creating the underlying widget. """ current_color = self.factory.to_wx_color(self) current_color_name = current_color.GetAsString() self.control = ColorComboBox( parent, -1, current_color_name, wx.Point(0, 0), wx.Size(40, -1), self.choices, style=wx.CB_READONLY, ) self.control.Bind(wx.EVT_COMBOBOX, self.color_selected) def dispose(self): if self.control is not None: self.control.Unbind(wx.EVT_COMBOBOX, handler=self.color_selected) super().dispose() def update_editor(self): """Updates the editor when the object trait changes externally to the editor. """ current_color = self.factory.to_wx_color(self) self.control.SetValue(self.string_value(current_color)) def color_selected(self, event): """ Event for when color is selected """ color_name = self.choices[event.Selection] if color_name == "custom": color_dialog = wx.ColourDialog(self.control) result = color_dialog.ShowModal() if result == wx.ID_CANCEL: return color = color_dialog.GetColourData().GetColour() self.value = self.factory.from_wx_color(color) else: try: color = w3c_color_database.Find(color_name) self.value = self.factory.from_wx_color(color) except ValueError: pass return def string_value(self, color): """Returns the text representation of a specified color value.""" color_name = w3c_color_database.FindName(color) if color_name != "": return color_name return color.GetAsString() # ------------------------------------------------------------------------------ # 'CustomColorEditor' class: # ------------------------------------------------------------------------------ class CustomColorEditor(BaseSimpleEditor): """Simple style of color editor, which displays a text field whose background color is the color value. Selecting the text field displays a dialog box for selecting a new color value. """ def init(self, parent): """ Finishes initializing the editor by creating the underlying widget. """ self.control = self._panel = parent = TraitsUIPanel(parent, -1) sizer = wx.BoxSizer(wx.HORIZONTAL) # 'text_control' is the text display of the color. text_control = wx.TextCtrl( parent, -1, self.str_value, style=wx.TE_PROCESS_ENTER ) text_control.Bind(wx.EVT_KILL_FOCUS, self.update_object) parent.Bind( wx.EVT_TEXT_ENTER, self.update_object, id=text_control.GetId() ) # 'button_control' shows the 'Edit' button. button_control = wx.Button(parent, label="Edit", style=wx.BU_EXACTFIT) button_control.Bind( wx.EVT_BUTTON, self.open_color_dialog, id=button_control.GetId() ) sizer.Add(text_control, wx.ALIGN_LEFT) sizer.AddSpacer(8) sizer.Add(button_control, wx.ALIGN_RIGHT) self.control.SetSizer(sizer) self._text_control = text_control self._button_control = button_control self.set_tooltip() return def update_object(self, event): """Handles the user changing the contents of the edit control.""" if not isinstance(event, wx._core.CommandEvent): event.Skip() return try: # The TextCtrl object was saved as self._text_control in init(). value = self._text_control.GetValue() self.value = w3c_color_database.Find(value) self.set_color() except TraitError: pass def set_color(self): # The CustomColorEditor uses this method instead of the global # set_color function. color = self.factory.to_wx_color(self) self._text_control.SetBackgroundColour(color) self.control.SetBackgroundColour(color) self._text_control.SetValue(self.string_value(color)) def update_editor(self): """Updates the editor when the object trait changes externally to the editor. """ self.set_color() def open_color_dialog(self, event): """Opens the color dialog and sets the value upon return""" color_data = wx.ColourData() color_data.SetColour(self.value) color_dialog = wx.ColourDialog(self.control, color_data) result = color_dialog.ShowModal() if result == wx.ID_CANCEL: return color = color_dialog.GetColourData().GetColour() self.value = color self.set_color() def color_selected(self, event): """ Event for when color is selected """ color = event.GetColour() try: self.value = self.factory.from_wx_color(color) except ValueError: pass return def string_value(self, color): """Returns the text representation of a specified color value.""" color_name = w3c_color_database.FindName(color) if color_name != "": return color_name return self.factory.str_color(color) # ------------------------------------------------------------------------------ # 'ReadonlyColorEditor' class: # ------------------------------------------------------------------------------ class ReadonlyColorEditor(BaseReadonlyEditor): """Read-only style of color editor, which displays a read-only text field whose background color is the color value. """ def init(self, parent): """Finishes initializing the editor by creating the underlying toolkit widget. """ self.control = wx.TextCtrl(parent, style=wx.TE_READONLY) def update_editor(self): """Updates the editor when the object trait changes externally to the editor. """ # super().update_editor() self.control.SetValue(self.string_value(self.value)) set_color(self) def string_value(self, color): """Returns the text representation of a specified color value.""" color_name = w3c_color_database.FindName(color) if color_name != "": return color_name return self.factory.str_color(color) # ------------------------------------------------------------------------------ # 'ReadonlyColorEditor' class: # ------------------------------------------------------------------------------ class TextColorEditor(BaseTextEditor): """Text style of color editor, which displays a text field whose background color is the color value. """ def update_editor(self): """Updates the editor when the object trait changes externally to the editor. """ self.control.SetValue(self.string_value(self.value)) set_color(self) def update_object(self, event): """Handles the user changing the contents of the edit control.""" if not isinstance(event, wx._core.CommandEvent): return try: self.value = w3c_color_database.Find(self.control.GetValue()) set_color(self) except TraitError: pass def string_value(self, color): """Returns the text representation of a specified color value.""" color_name = w3c_color_database.FindName(color) if color_name != "": return color_name return self.factory.str_color(color) # ------------------------------------------------------------------------------ # Sets the color of the specified editor's color control: # ------------------------------------------------------------------------------ def set_color(editor): """Sets the color of the specified color control.""" color = editor.factory.to_wx_color(editor) editor.control.SetBackgroundColour(color) # Define the SimpleEditor, CustomEditor, etc. classes which are used by the # editor factory for the color editor. SimpleEditor = SimpleColorEditor CustomEditor = CustomColorEditor TextEditor = TextColorEditor ReadonlyEditor = ReadonlyColorEditor ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/wx/color_trait.py0000644000175100001730000001424700000000000021372 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Trait definition for a wxPython-based color. """ from ast import literal_eval import wx from pyface.color import Color as PyfaceColor from pyface.util.color_helpers import channels_to_ints from pyface.util.color_parser import color_table from traits.api import Trait, TraitError # ------------------------------------------------------------------------- # W3CColourDatabase # ------------------------------------------------------------------------- class W3CColourDatabase(object): """Proxy for the ColourDatabase which allows for finding W3C colors. This class is necessary because the wx 'green' is the W3C 'lime', and we need some means to lookup the color names since wx has only a few hardcoded. This class is a proxy because AddColour expects a wx.ColourDatabase instance, not an instance of a subclass """ _database = wx.ColourDatabase() def __init__(self): # correct for differences in definitions self._color_names = [ "aqua", "black", "blue", "fuchsia", "gray", "green", "lime", "maroon", "navy", "olive", "purple", "red", "silver", "teal", "white", "yellow", ] self.AddColour("aqua", wx.Colour(0, 0xFF, 0xFF, 255)) self.AddColour("fuchsia", wx.Colour(0xFF, 0, 0xFF, 255)) self.AddColour("green", wx.Colour(0, 0x80, 0, 255)) self.AddColour("lime", wx.Colour(0, 0xFF, 0, 255)) self.AddColour("maroon", wx.Colour(0x80, 0x0, 0, 255)) self.AddColour("navy", wx.Colour(0x00, 0x0, 0x80, 255)) self.AddColour("olive", wx.Colour(0x80, 0x80, 0, 255)) self.AddColour("purple", wx.Colour(0x80, 0x00, 0x80, 255)) self.AddColour("silver", wx.Colour(0xC0, 0xC0, 0xC0, 255)) self.AddColour("teal", wx.Colour(0, 0x80, 0x80, 255)) # add all the standard colours for name, rgba in color_table.items(): rgba_bytes = channels_to_ints(rgba) self.AddColour(name, wx.Colour(*rgba_bytes)) def AddColour(self, name, color): if name not in self._color_names: self._color_names.append(name) return self._database.AddColour(name, color) def Find(self, color_name): return self._database.Find(color_name) def FindName(self, color): for color_name in self._color_names: if self.Find(color_name) == color: return color_name return "" w3c_color_database = W3CColourDatabase() # ------------------------------------------------------------------------- # Convert a number into a wxColour object: # ------------------------------------------------------------------------- def tuple_to_wxcolor(tup): if 3 <= len(tup) <= 4: for c in tup: if not isinstance(c, int): raise TraitError return wx.Colour(*tup) else: raise TraitError def convert_to_color(object, name, value): """Converts a number into a wxColour object.""" if isinstance(value, tuple): return tuple_to_wxcolor(value) elif isinstance(value, PyfaceColor): return value.to_toolkit() elif isinstance(value, wx.Colour): return value elif isinstance(value, str): # Allow for spaces in the string value. value = value.replace(" ", "") if value in standard_colors: return standard_colors[value] # Check for tuple string try: tup = literal_eval(value) except Exception: raise TraitError return tuple_to_wxcolor(tup) else: try: num = int(value) except Exception: raise TraitError return wx.Colour(num // 0x10000, (num // 0x100) & 0xFF, num & 0xFF) raise TraitError convert_to_color.info = ( "a string of the form (r,g,b) or (r,g,b,a) where r, " "g, b, and a are integers from 0 to 255, a wx.Colour " "instance, an integer which in hex is of the form " "0xRRGGBB, where RR is red, GG is green, and BB is " "blue" ) # ------------------------------------------------------------------------- # Standard colors: # ------------------------------------------------------------------------- standard_colors = {} for name in color_table: try: wx_color = w3c_color_database.Find(name) standard_colors[name] = convert_to_color(None, None, wx_color) except: pass # ------------------------------------------------------------------------- # Define wxPython specific color traits: # ------------------------------------------------------------------------- ### Note: Declare the editor to be a function which returns the ColorEditor # class from traits ui to avoid circular import issues. For backwards # compatibility with previous Traits versions, the 'editors' folder in Traits # project declares 'from api import *' in its __init__.py. The 'api' in turn # can contain classes that have a Color trait which lead to this file getting # imported. This leads to a circular import when declaring a Color trait. def get_color_editor(*args, **traits): from .color_editor import ToolkitEditorFactory return ToolkitEditorFactory(*args, **traits) def WxColor(default="white", allow_none=False, **metadata): """Defines wxPython-specific color traits.""" if default is None: allow_none = True if allow_none: return Trait( default, None, standard_colors, convert_to_color, editor=get_color_editor, **metadata, ) return Trait( default, standard_colors, convert_to_color, editor=get_color_editor, **metadata, ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/wx/compound_editor.py0000644000175100001730000000524300000000000022237 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Defines the compound editor and the compound editor factory for the wxPython user interface toolkit. """ import wx from traits.api import Str from .editor import Editor from .helper import TraitsUIPanel # ------------------------------------------------------------------------- # 'CompoundEditor' class: # ------------------------------------------------------------------------- class CompoundEditor(Editor): """Editor for compound traits, which displays editors for each of the combined traits, in the appropriate style. """ # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- #: The kind of editor to create for each list item kind = Str() def init(self, parent): """Finishes initializing the editor by creating the underlying toolkit widget. """ # Create a panel to hold all of the component trait editors: self.control = panel = TraitsUIPanel(parent, -1) sizer = wx.BoxSizer(wx.VERTICAL) # Add all of the component trait editors: self._editors = editors = [] for factory in self.factory.editors: editor = getattr(factory, self.kind)( self.ui, self.object, self.name, self.description, panel ) editor.prepare(panel) sizer.Add( editor.control, 1, wx.TOP | wx.BOTTOM | editor.layout_style, 3 ) editors.append(editor) # Set-up the layout: panel.SetSizerAndFit(sizer) self.set_tooltip() def update_editor(self): """Updates the editor when the object trait changes externally to the editor. """ pass def dispose(self): """Disposes of the contents of an editor.""" for editor in self._editors: editor.dispose() super().dispose() class SimpleEditor(CompoundEditor): #: The kind of editor to create for each list item. This value overrides #: the default. kind = "simple_editor" class CustomEditor(CompoundEditor): #: The kind of editor to create for each list item. This value overrides #: the default. kind = "custom_editor" ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/wx/constants.py0000644000175100001730000000422500000000000021060 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Defines constants used by the wxPython implementation of the various text editors and text editor factories. """ import sys import wx from pyface.api import SystemMetrics #: Define platform and wx version constants: is_mac = sys.platform == "darwin" #: Default dialog title DefaultTitle = "Edit properties" #: Color of valid input OKColor = wx.WHITE #: Color to highlight input errors ErrorColor = wx.Colour(255, 192, 192) #: Color for background of read-only fields ReadonlyColor = wx.Colour(244, 243, 238) #: Color for background of fields where objects can be dropped DropColor = wx.Colour(215, 242, 255) #: Color for an editable field EditableColor = wx.WHITE #: Color for background of windows (like dialog background color) if is_mac: WindowColor = wx.Colour(232, 232, 232) BorderedGroupColor = wx.Colour(224, 224, 224) else: WindowColor = wx.SystemSettings.GetColour(wx.SYS_COLOUR_MENUBAR) #: Default colour for table foreground TableCellColor = wx.BLACK #: Default colour for table background TableCellBackgroundColor = wx.WHITE #: Default colour for table background TableReadOnlyBackgroundColor = ReadonlyColor #: Default colour for table background TableLabelColor = TableCellColor #: Default colour for table background TableLabelBackgroundColor = WindowColor #: Default foreground colour for table selection TableSelectionTextColor = TableCellColor #: Default background colour for table selection (light blue) TableSelectionBackgroundColor = wx.Colour(173, 216, 230) #: Standard width of an image bitmap standard_bitmap_width = 120 #: Width of a scrollbar scrollbar_dx = wx.SystemSettings.GetMetric(wx.SYS_VSCROLL_X) #: Screen width screen_dx = SystemMetrics().screen_width #: Screen height screen_dy = SystemMetrics().screen_height ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/wx/csv_list_editor.py0000644000175100001730000000252700000000000022243 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Defines the various text editors for the wxPython user interface toolkit. The module is mainly a place-folder for TextEditor factories that have been augmented to also listen to changes in the items of the list object. """ from .text_editor import SimpleEditor as WXSimpleEditor from .text_editor import CustomEditor as WXCustomEditor from .text_editor import ReadonlyEditor as WXReadonlyEditor from ..editors.csv_list_editor import _prepare_method, _dispose_method class SimpleEditor(WXSimpleEditor): """Simple Editor style for CSVListEditor.""" prepare = _prepare_method dispose = _dispose_method class CustomEditor(WXCustomEditor): """Custom Editor style for CSVListEditor.""" prepare = _prepare_method dispose = _dispose_method class ReadonlyEditor(WXReadonlyEditor): """Readonly Editor style for CSVListEditor.""" prepare = _prepare_method dispose = _dispose_method TextEditor = SimpleEditor ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/wx/custom_editor.py0000644000175100001730000000312300000000000021720 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Defines the wxPython implementation of the editor used to wrap a non-Traits based custom control. """ import wx from .editor import Editor # ------------------------------------------------------------------------- # 'CustomEditor' class: # ------------------------------------------------------------------------- class CustomEditor(Editor): """Wrapper for a custom editor control""" def init(self, parent): """Finishes initializing the editor by creating the underlying toolkit widget. """ factory = self.factory.factory if factory is not None: self.control = factory(*((parent, self) + self.factory.args)) if self.control is None: self.control = control = wx.StaticText( parent, -1, "An error occurred creating a custom editor.\n" "Please contact the developer.", ) control.SetBackgroundColour(wx.RED) control.SetForegroundColour(wx.WHITE) self.set_tooltip() def update_editor(self): """Updates the editor when the object trait changes externally to the editor. """ pass ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/wx/data_frame_editor.py0000644000175100001730000000123200000000000022470 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! from traitsui.ui_editors.data_frame_editor import ( _DataFrameEditor as BaseDataFrameEditor, ) from .ui_editor import UIEditor class _DataFrameEditor(BaseDataFrameEditor, UIEditor): """Wx Toolkit implementation of the DataFrameEditor""" pass ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/wx/date_editor.py0000644000175100001730000007044300000000000021334 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ A Traits UI editor that wraps a WX calendar panel. Future Work ----------- The class needs to be extend to provide the four basic editor types, Simple, Custom, Text, and ReadOnly. """ import datetime import logging import wx import wx.adv from traitsui.wx.editor import Editor from traitsui.wx.constants import WindowColor from traitsui.wx.text_editor import ReadonlyEditor as TextReadonlyEditor logger = logging.getLogger(__name__) class SimpleEditor(Editor): """ Simple Traits UI date editor. Shows a text box, and a date-picker widget. """ def init(self, parent): """ Finishes initializing the editor by creating the underlying widget. """ date_widget = wx.adv.DatePickerCtrl self.control = date_widget( parent, size=(120, -1), style=wx.adv.DP_DROPDOWN | wx.adv.DP_SHOWCENTURY | wx.adv.DP_ALLOWNONE, ) self.control.Bind(wx.adv.EVT_DATE_CHANGED, self.day_selected) return def day_selected(self, event): """ Event for when calendar is selected, update/create date string. """ date = event.GetDate() # WX sometimes has year == 0 temporarily when doing state changes. if date.IsValid() and date.GetYear() != 0: year = date.GetYear() # wx 2.8.8 has 0-indexed months. month = date.GetMonth() + 1 day = date.GetDay() try: self.value = datetime.date(year, month, day) except ValueError: logger.exception( "Invalid date: %d-%d-%d (y-m-d)", (year, month, day) ) raise return def update_editor(self): """ Updates the editor when the object trait changes externally to the editor. """ if self.value: date = self.control.GetValue() # FIXME: A Trait assignment should support fixing an invalid # date in the widget. if date.IsValid(): # Important: set the day before setting the month, otherwise wx may fail # to set the month. date.SetYear(self.value.year) date.SetDay(self.value.day) # wx 2.8.8 has 0-indexed months. date.SetMonth(self.value.month - 1) self.control.SetValue(date) self.control.Refresh() return # ------------------------------------------------------------------------------ # -- Custom Editor # ------------------------------------------------------------------------------ SELECTED_FG = wx.Colour(255, 0, 0) UNAVAILABLE_FG = wx.Colour(192, 192, 192) DRAG_HIGHLIGHT_FG = wx.Colour(255, 255, 255) DRAG_HIGHLIGHT_BG = wx.Colour(128, 128, 255) try: MOUSE_BOX_FILL = wx.Colour(0, 0, 255, 32) NORMAL_HIGHLIGHT_FG = wx.Colour(0, 0, 0, 0) NORMAL_HIGHLIGHT_BG = wx.Colour(255, 255, 255, 0) # Alpha channel in wx.Colour does not exist prior to version 2.7.1.1 except TypeError: MOUSE_BOX_FILL = wx.Colour(0, 0, 255) NORMAL_HIGHLIGHT_FG = wx.Colour(0, 0, 0) NORMAL_HIGHLIGHT_BG = wx.Colour(255, 255, 255) class wxMouseBoxCalendarCtrl(wx.adv.CalendarCtrl): """ Subclass to add a mouse-over box-selection tool. Description ----------- Add a Mouse drag-box highlight feature that can be used by the CustomEditor to detect user selections. CalendarCtrl must be subclassed to get a device context to draw on top of the Calendar, otherwise the calendar widgets are always painted on top of the box during repaints. """ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.selecting = False self.box_selected = [] self.sel_start = (0, 0) self.sel_end = (0, 0) self.Bind(wx.EVT_RIGHT_DOWN, self.start_select) self.Bind(wx.EVT_RIGHT_UP, self.end_select) self.Bind(wx.EVT_LEAVE_WINDOW, self.end_select) self.Bind(wx.EVT_MOTION, self.on_select) self.Bind(wx.EVT_PAINT, self.on_paint) self.Bind(wx.adv.EVT_CALENDAR_SEL_CHANGED, self.highlight_changed) def boxed_days(self): """ Compute the days that are under the box selection. Returns ------- A list of wx.DateTime objects under the mouse box. """ x1, y1 = self.sel_start x2, y2 = self.sel_end if x1 > x2: x1, x2 = x2, x1 if y1 > y2: y1, y2 = y2, y1 grid = [] for i in range(x1, x2, 15): for j in range(y1, y2, 15): grid.append(wx.Point(i, j)) grid.append(wx.Point(i, y2)) # Avoid jitter along the edge since the final points change. for j in range(y1, y2, 20): grid.append(wx.Point(x2, j)) grid.append(wx.Point(x2, y2)) selected_days = [] for point in grid: (result, date, weekday) = self.HitTest(point) if result == wx.adv.CAL_HITTEST_DAY: if date not in selected_days: selected_days.append(date) return selected_days def highlight_changed(self, event=None): """ Hide the default highlight to take on the selected date attr. Description ----------- A feature of the wx CalendarCtrl is that there are selected days, that always are shown and the user can move around with left-click. But it's confusing and misleading when there are multiple CalendarCtrl objects linked in one editor. So we hide the highlights in this CalendarCtrl by making it mimic the attribute of the selected day. Highlights apparently can't take on a border style, so to be truly invisible, normal days cannot have borders. """ if event: event.Skip() date = self.GetDate() attr = self.GetAttr(date.GetDay()) if attr is None: bg_color = NORMAL_HIGHLIGHT_BG fg_color = NORMAL_HIGHLIGHT_FG else: bg_color = attr.GetBackgroundColour() fg_color = attr.GetTextColour() self.SetHighlightColours(fg_color, bg_color) self.Refresh() return # -- event handlers -------------------------------------------------------- def start_select(self, event): event.Skip() self.selecting = True self.box_selected = [] self.sel_start = (event.m_x, event.m_y) self.sel_end = self.sel_start def end_select(self, event): event.Skip() self.selecting = False self.Refresh() def on_select(self, event): event.Skip() if not self.selecting: return self.sel_end = (event.m_x, event.m_y) self.box_selected = self.boxed_days() self.Refresh() def on_paint(self, event): event.Skip() dc = wx.PaintDC(self) if not self.selecting: return x = self.sel_start[0] y = self.sel_start[1] w = self.sel_end[0] - x h = self.sel_end[1] - y gc = wx.GraphicsContext.Create(dc) pen = gc.CreatePen(wx.BLACK_PEN) gc.SetPen(pen) points = [(x, y), (x + w, y), (x + w, y + h), (x, y + h), (x, y)] gc.DrawLines(points) brush = gc.CreateBrush(wx.Brush(MOUSE_BOX_FILL)) gc.SetBrush(brush) gc.DrawRectangle(x, y, w, h) class MultiCalendarCtrl(wx.Panel): """ WX panel containing calendar widgets for use by the CustomEditor. Description ----------- Handles multi-selection of dates by special handling of the wxMouseBoxCalendarCtrl widget. Doing single-select across multiple calendar widgets is also supported though most of the interesting functionality is then unused. """ def __init__( self, parent, ID, editor, multi_select, shift_to_select, on_mixed_select, allow_future, months, padding, *args, **kwargs, ): super().__init__(parent, ID, *args, **kwargs) self.sizer = wx.BoxSizer() self.SetSizer(self.sizer) self.SetBackgroundColour(WindowColor) self.date = wx.DateTime.Now() self.today = self.date_from_datetime(self.date) # Object attributes self.multi_select = multi_select self.shift_to_select = shift_to_select self.on_mixed_select = on_mixed_select self.allow_future = allow_future self.editor = editor self.selected_days = editor.value self.months = months self.padding = padding self.cal_ctrls = [] # State to remember when a user is doing a shift-click selection. self._first_date = None self._drag_select = [] self._box_select = [] # Set up the individual month frames. for i in range(-(self.months - 1), 1): cal = self._make_calendar_widget(i) self.cal_ctrls.insert(0, cal) if i != 0: self.sizer.AddSpacer(padding) # Initial painting self.selected_list_changed() return def date_from_datetime(self, dt): """ Convert a wx DateTime object to a Python Date object. Parameters ---------- dt : wx.DateTime A valid date to convert to a Python Date object """ new_date = datetime.date(dt.GetYear(), dt.GetMonth() + 1, dt.GetDay()) return new_date def datetime_from_date(self, date): """ Convert a Python Date object to a wx DateTime object. Ignores time. Parameters ---------- date : datetime.Date object A valid date to convert to a wx.DateTime object. Since there is no time information in a Date object the defaults of DateTime are used. """ dt = wx.DateTime() dt.SetYear(date.year) dt.SetMonth(date.month - 1) dt.SetDay(date.day) return dt def shift_datetime(self, old_date, months): """ Create a new DateTime from *old_date* with an offset number of *months*. Parameters ---------- old_date : DateTime The old DateTime to make a date copy of. Does not copy time. months : int A signed int to add or subtract from the old date months. Does not support jumping more than 12 months. """ new_date = wx.DateTime() new_month = old_date.GetMonth() + months new_year = old_date.GetYear() if new_month < 0: new_month += 12 new_year -= 1 elif new_month > 11: new_month -= 12 new_year += 1 new_day = min(old_date.GetDay(), 28) new_date.Set(new_day, new_month, new_year) return new_date def selected_list_changed(self, evt=None): """Update the date colors of the days in the widgets.""" for cal in self.cal_ctrls: cur_month = cal.GetDate().GetMonth() + 1 cur_year = cal.GetDate().GetYear() selected_days = self.selected_days # When multi_select is False wrap in a list to pass the for-loop. if not self.multi_select: if selected_days is None: selected_days = [] else: selected_days = [selected_days] # Reset all the days to the correct colors. for day in range(1, 32): try: paint_day = datetime.date(cur_year, cur_month, day) if not self.allow_future and paint_day > self.today: attr = wx.adv.CalendarDateAttr(colText=UNAVAILABLE_FG) cal.SetAttr(day, attr) elif paint_day in selected_days: attr = wx.adv.CalendarDateAttr(colText=SELECTED_FG) cal.SetAttr(day, attr) else: cal.ResetAttr(day) except ValueError: # Blindly creating Date objects sometimes produces invalid. pass cal.highlight_changed() return def _make_calendar_widget(self, month_offset): """ Add a calendar widget to the screen and hook up callbacks. Parameters ---------- month_offset : int The number of months from today, that the calendar should start at. """ date = self.shift_datetime(self.date, month_offset) panel = wx.Panel(self, -1) cal = wxMouseBoxCalendarCtrl( panel, -1, date, style=wx.adv.CAL_SUNDAY_FIRST | wx.adv.CAL_SEQUENTIAL_MONTH_SELECTION # | wx.adv.CAL_SHOW_HOLIDAYS ) self.sizer.Add(panel) cal.highlight_changed() # Set up control to sync the other calendar widgets and coloring: cal.Bind(wx.adv.EVT_CALENDAR_MONTH, self.month_changed) cal.Bind(wx.adv.EVT_CALENDAR_YEAR, self.month_changed) cal.Bind(wx.EVT_LEFT_DOWN, self._left_down) if self.multi_select: cal.Bind(wx.EVT_LEFT_UP, self._left_up) cal.Bind(wx.EVT_RIGHT_UP, self._process_box_select) cal.Bind(wx.EVT_LEAVE_WINDOW, self._process_box_select) cal.Bind(wx.EVT_MOTION, self._mouse_drag) self.Bind( wx.adv.EVT_CALENDAR_WEEKDAY_CLICKED, self._weekday_clicked, cal, ) return cal def unhighlight_days(self, days): """ Turn off all highlights in all cals, but leave any selected color. Parameters ---------- days : List(Date) The list of dates to add. Possibly includes dates in the future. """ for cal in self.cal_ctrls: c = cal.GetDate() for date in days: if date.year == c.GetYear() and date.month == c.GetMonth() + 1: # Unselected days either need to revert to the # unavailable color, or the default attribute color. if not self.allow_future and ( (date.year, date.month, date.day) > (self.today.year, self.today.month, self.today.day) ): attr = wx.adv.CalendarDateAttr(colText=UNAVAILABLE_FG) else: attr = wx.adv.CalendarDateAttr( colText=NORMAL_HIGHLIGHT_FG, colBack=NORMAL_HIGHLIGHT_BG, ) if date in self.selected_days: attr.SetTextColour(SELECTED_FG) cal.SetAttr(date.day, attr) cal.highlight_changed() return def highlight_days(self, days): """ Color the highlighted list of days across all calendars. Parameters ---------- days : List(Date) The list of dates to add. Possibly includes dates in the future. """ for cal in self.cal_ctrls: c = cal.GetDate() for date in days: if date.year == c.GetYear() and date.month == c.GetMonth() + 1: attr = wx.adv.CalendarDateAttr( colText=DRAG_HIGHLIGHT_FG, colBack=DRAG_HIGHLIGHT_BG ) cal.SetAttr(date.day, attr) cal.highlight_changed() cal.Refresh() def add_days_to_selection(self, days): """ Add a list of days to the selection, using a specified style. Parameters ---------- days : List(Date) The list of dates to add. Possibly includes dates in the future. Description ----------- When a user multi-selects entries and some of those entries are already selected and some are not, what should be the behavior for the seletion? Options:: 'toggle' -- Toggle each day to it's opposite state. 'on' -- Always turn them on. 'off' -- Always turn them off. 'max_change' -- Change all to same state, with most days changing. For example 1 selected and 9 not, then they would all get selected. 'min_change' -- Change all to same state, with min days changing. For example 1 selected and 9 not, then they would all get unselected. """ if not days: return style = self.on_mixed_select new_list = list(self.selected_days) if style == "toggle": for day in days: if self.allow_future or day <= self.today: if day in new_list: new_list.remove(day) else: new_list.append(day) else: already_selected = len([day for day in days if day in new_list]) if style == "on" or already_selected == 0: add_items = True elif style == "off" or already_selected == len(days): add_items = False elif self.on_mixed_select == "max_change" and already_selected <= ( len(days) / 2.0 ): add_items = True elif self.on_mixed_select == "min_change" and already_selected > ( len(days) / 2.0 ): add_items = True else: # Cases where max_change is off or min_change off. add_items = False for day in days: # Skip if we don't allow future, and it's a future day. if self.allow_future or day <= self.today: if add_items and day not in new_list: new_list.append(day) elif not add_items and day in new_list: new_list.remove(day) self.selected_days = new_list # Link the list back to the model to make a Traits List change event. self.editor.value = new_list return def single_select_day(self, dt): """ In non-multiselect switch the selection to a new date. Parameters ---------- dt : wx.DateTime The newly selected date that should become the new calendar selection. Description ----------- Only called when we're using the single-select mode of the calendar widget, so we can assume that the selected_dates is a None or a Date singleton. """ selection = self.date_from_datetime(dt) if dt.IsValid() and (self.allow_future or selection <= self.today): self.selected_days = selection self.selected_list_changed() # Modify the trait on the editor so that the events propagate. self.editor.value = self.selected_days return def _shift_drag_update(self, event): """Shift-drag in progress.""" cal = event.GetEventObject() result, dt, weekday = cal.HitTest(event.GetPosition()) self.unhighlight_days(self._drag_select) self._drag_select = [] # Prepare for an abort, don't highlight new selections. if ( self.shift_to_select and not event.ShiftDown() ) or result != wx.adv.CAL_HITTEST_DAY: cal.highlight_changed() for cal in self.cal_ctrls: cal.Refresh() return # Construct the list of selections. last_date = self.date_from_datetime(dt) if last_date <= self._first_date: first, last = last_date, self._first_date else: first, last = self._first_date, last_date while first <= last: if self.allow_future or first <= self.today: self._drag_select.append(first) first = first + datetime.timedelta(1) self.highlight_days(self._drag_select) return # ------------------------------------------------------------------------ # Event handlers # ------------------------------------------------------------------------ def _process_box_select(self, event): """ Possibly move the calendar box-selected days into our selected days. """ event.Skip() self.unhighlight_days(self._box_select) if not event.Leaving(): self.add_days_to_selection(self._box_select) self.selected_list_changed() self._box_select = [] def _weekday_clicked(self, evt): """A day on the weekday bar has been clicked. Select all days.""" evt.Skip() weekday = evt.GetWeekDay() cal = evt.GetEventObject() month = cal.GetDate().GetMonth() + 1 year = cal.GetDate().GetYear() days = [] # Messy math to compute the dates of each weekday in the month. # Python uses Monday=0, while wx uses Sunday=0. month_start_weekday = (datetime.date(year, month, 1).weekday() + 1) % 7 weekday_offset = (weekday - month_start_weekday) % 7 for day in range(weekday_offset, 31, 7): try: day = datetime.date(year, month, day + 1) if self.allow_future or day <= self.today: days.append(day) except ValueError: pass self.add_days_to_selection(days) self.selected_list_changed() return def _left_down(self, event): """Handle user selection of days.""" event.Skip() cal = event.GetEventObject() result, dt, weekday = cal.HitTest(event.GetPosition()) if result == wx.adv.CAL_HITTEST_DAY and not self.multi_select: self.single_select_day(dt) return # Inter-month-drag selection. A quick no-movement mouse-click is # equivalent to a multi-select of a single day. if ( result == wx.adv.CAL_HITTEST_DAY and (not self.shift_to_select or event.ShiftDown()) and not cal.selecting ): self._first_date = self.date_from_datetime(dt) self._drag_select = [self._first_date] # Start showing the highlight colors with a mouse_drag event. self._mouse_drag(event) return def _left_up(self, event): """Handle the end of a possible run-selection.""" event.Skip() cal = event.GetEventObject() result, dt, weekday = cal.HitTest(event.GetPosition()) # Complete a drag-select operation. if ( result == wx.adv.CAL_HITTEST_DAY and (not self.shift_to_select or event.ShiftDown()) and self._first_date ): last_date = self.date_from_datetime(dt) if last_date <= self._first_date: first, last = last_date, self._first_date else: first, last = self._first_date, last_date newly_selected = [] while first <= last: newly_selected.append(first) first = first + datetime.timedelta(1) self.add_days_to_selection(newly_selected) self.unhighlight_days(newly_selected) # Reset a drag-select operation, even if it wasn't completed because # of a loss of focus or the Shift key prematurely released. self._first_date = None self._drag_select = [] self.selected_list_changed() return def _mouse_drag(self, event): """Called when the mouse in being dragged within the main panel.""" event.Skip() cal = event.GetEventObject() if not cal.selecting and self._first_date: self._shift_drag_update(event) if cal.selecting: self.unhighlight_days(self._box_select) self._box_select = [ self.date_from_datetime(dt) for dt in cal.boxed_days() ] self.highlight_days(self._box_select) return def month_changed(self, evt=None): """ Link the calendars together so if one changes, they all change. TODO: Maybe wx.adv.CAL_HITTEST_INCMONTH could be checked and the event skipped, rather than now where we undo the update after the event has gone through. """ evt.Skip() cal_index = self.cal_ctrls.index(evt.GetEventObject()) current_date = self.cal_ctrls[cal_index].GetDate() for i, cal in enumerate(self.cal_ctrls): # Current month is already updated, just need to shift the others if i != cal_index: new_date = self.shift_datetime(current_date, cal_index - i) cal.SetDate(new_date) cal.highlight_changed() # Back-up if we're not allowed to move into future months. if not self.allow_future: month = self.cal_ctrls[0].GetDate().GetMonth() + 1 year = self.cal_ctrls[0].GetDate().GetYear() if (year, month) > (self.today.year, self.today.month): for i, cal in enumerate(self.cal_ctrls): new_date = self.shift_datetime(wx.DateTime.Now(), -i) cal.SetDate(new_date) cal.highlight_changed() # Redraw the selected days. self.selected_list_changed() class CustomEditor(Editor): """ Show multiple months with MultiCalendarCtrl. Allow multi-select. Trait Listeners --------------- The wx editor directly modifies the *value* trait of the Editor, which is the named trait of the corresponding Item in your View. Therefore you can listen for changes to the user's selection by directly listening to the item changed event. TODO ---- Some more listeners need to be hooked up. For example, in single-select mode, changing the value does not cause the calendar to update. Also, the selection-add and remove is noisy, triggering an event for each addition rather than waiting until everything has been added and removed. Sample ------ Example usage:: class DateListPicker(HasTraits): calendar = List() traits_view = View(Item('calendar', editor=DateEditor(), style='custom', show_label=False)) """ # -- Editor interface ------------------------------------------------------ def init(self, parent): """ Finishes initializing the editor by creating the underlying widget. """ if self.factory.multi_select and not isinstance(self.value, list): raise ValueError("Multi-select is True, but editing a non-list.") elif not self.factory.multi_select and isinstance(self.value, list): raise ValueError("Multi-select is False, but editing a list.") calendar_ctrl = MultiCalendarCtrl( parent, -1, self, self.factory.multi_select, self.factory.shift_to_select, self.factory.on_mixed_select, self.factory.allow_future, self.factory.months, self.factory.padding, ) self.control = calendar_ctrl return def update_editor(self): """ Updates the editor when the object trait changes externally to the editor. """ self.control.selected_list_changed() return # TODO: Write me. Possibly use TextEditor as a model to show a string # representation of the date, and have enter-set do a date evaluation. class TextEditor(SimpleEditor): pass class ReadonlyEditor(TextReadonlyEditor): """Use a TextEditor for the view.""" def _get_str_value(self): """Replace the default string value with our own date verision.""" if not self.value: return self.factory.message else: return self.value.strftime(self.factory.strftime) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/wx/directory_editor.py0000644000175100001730000000456400000000000022424 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Defines various directory editors for the wxPython user interface toolkit. """ from os.path import isdir import wx from pyface.api import DirectoryDialog from .file_editor import ( SimpleEditor as SimpleFileEditor, CustomEditor as CustomFileEditor, PopupFile, ) # ------------------------------------------------------------------------- # 'SimpleEditor' class: # ------------------------------------------------------------------------- class SimpleEditor(SimpleFileEditor): """Simple style of editor for directories, which displays a text field and a **Browse** button that opens a directory-selection dialog box. """ def _create_file_dialog(self): """Creates the correct type of file dialog.""" dlg = DirectoryDialog( parent=self.get_control_widget(), default_path=self._file_name.GetValue(), ) return dlg def _create_file_popup(self): """Creates the correct type of file popup.""" return PopupDirectory( control=self.control, file_name=self.str_value, filter=self.factory.filter, height=300, ) class CustomEditor(CustomFileEditor): """Custom style of editor for directories, which displays a tree view of the file system. """ def get_style(self): """Returns the basic style to use for the control.""" return wx.DIRCTRL_DIR_ONLY | wx.DIRCTRL_EDIT_LABELS def update_object(self, event): """Handles the user changing the contents of the edit control.""" if self.control is not None: path = self.control.GetPath() if isdir(path): self.value = path class PopupDirectory(PopupFile): def get_style(self): """Returns the basic style to use for the popup.""" return wx.DIRCTRL_DIR_ONLY | wx.DIRCTRL_EDIT_LABELS def is_valid(self, path): """Returns whether or not the path is valid.""" return isdir(path) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/wx/dnd_editor.py0000644000175100001730000002324700000000000021164 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Defines the various editors for a drag-and-drop editor, for the wxPython user interface toolkit. A drag-and-drop editor represents its value as a simple image which, depending upon the editor style, can be a drag source only, a drop target only, or both a drag source and a drop target. """ import wx import numpy from pickle import load from traits.api import Bool from pyface.wx.drag_and_drop import ( PythonDropSource, PythonDropTarget, clipboard, ) try: from apptools.io import File except ImportError: File = None try: from apptools.naming.api import Binding except ImportError: Binding = None from pyface.image_resource import ImageResource from .editor import Editor # The image to use when the editor accepts files: file_image = ImageResource("file").create_image() # The image to use when the editor accepts objects: object_image = ImageResource("object").create_image() # The image to use when the editor is disabled: inactive_image = ImageResource("inactive").create_image() # String types: string_type = (str, str) # ------------------------------------------------------------------------- # 'SimpleEditor' class: # ------------------------------------------------------------------------- class SimpleEditor(Editor): """Simply style of editor for a drag-and-drop editor, which is both a drag source and a drop target. """ # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- #: Is the editor a drop target? drop_target = Bool(True) #: Is the editor a drag source? drag_source = Bool(True) def init(self, parent): """Finishes initializing the editor by creating the underlying toolkit widget. """ # Determine the drag/drop type: value = self.value self._is_list = isinstance(value, list) self._is_file = isinstance(value, string_type) or ( self._is_list and (len(value) > 0) and isinstance(value[0], string_type) ) # Get the right image to use: image = self.factory.image if image is not None: image = image.create_image() disabled_image = self.factory.disabled_image if disabled_image is not None: disabled_image = disabled_image.create_image() else: disabled_image = inactive_image image = object_image if self._is_file: image = file_image self._image = image.ConvertToBitmap() if disabled_image is not None: self._disabled_image = disabled_image.ConvertToBitmap() else: data = numpy.reshape( numpy.fromstring(image.GetData(), numpy.uint8), (-1, 3) ) * numpy.array([[0.297, 0.589, 0.114]]) g = data[:, 0] + data[:, 1] + data[:, 2] data[:, 0] = data[:, 1] = data[:, 2] = g image.SetData(numpy.ravel(data.astype(numpy.uint8)).tostring()) image.SetMaskColour(0, 0, 0) self._disabled_image = image.ConvertToBitmap() # Create the control and set up the event handlers: self.control = control = wx.Window( parent, -1, size=wx.Size(image.GetWidth(), image.GetHeight()) ) self.set_tooltip() if self.drop_target: control.SetDropTarget(PythonDropTarget(self)) control.Bind(wx.EVT_LEFT_DOWN, self._left_down) control.Bind(wx.EVT_LEFT_UP, self._left_up) control.Bind(wx.EVT_MOTION, self._mouse_move) control.Bind(wx.EVT_PAINT, self._on_paint) def dispose(self): """Disposes of the contents of an editor.""" control = self.control control.Unbind(wx.EVT_LEFT_DOWN) control.Unbind(wx.EVT_LEFT_UP) control.Unbind(wx.EVT_MOTION) control.Unbind(wx.EVT_PAINT) super().dispose() def update_editor(self): """Updates the editor when the object trait changes externally to the editor. """ return # -- Private Methods ------------------------------------------------------ def _get_drag_data(self, data): """Returns the processed version of a drag request's data.""" if isinstance(data, list): if Binding is not None and isinstance(data[0], Binding): data = [item.obj for item in data] if File is not None and isinstance(data[0], File): data = [item.absolute_path for item in data] if not self._is_file: result = [] for file in data: item = self._unpickle(file) if item is not None: result.append(item) data = result else: if Binding is not None and isinstance(data, Binding): data = data.obj if File is not None and isinstance(data, File): data = data.absolute_path if not self._is_file: object = self._unpickle(data) if object is not None: data = object return data def _unpickle(self, file_name): """Returns the unpickled version of a specified file (if possible).""" with open(file_name, "rb") as fh: try: object = load(fh) except Exception: object = None return object # -- wxPython Event Handlers ---------------------------------------------- def _on_paint(self, event): """Called when the control needs repainting.""" image = self._image control = self.control if not control.IsEnabled(): image = self._disabled_image wdx, wdy = control.GetClientSize() wx.PaintDC(control).DrawBitmap( image, (wdx - image.GetWidth()) // 2, (wdy - image.GetHeight()) // 2, True, ) def _left_down(self, event): """Handles the left mouse button being pressed.""" if self.control.IsEnabled() and self.drag_source: self._x, self._y = event.GetX(), event.GetY() self.control.CaptureMouse() event.Skip() def _left_up(self, event): """Handles the left mouse button being released.""" if self._x is not None: self._x = None self.control.ReleaseMouse() event.Skip() def _mouse_move(self, event): """Handles the mouse being moved.""" if self._x is not None: if ( abs(self._x - event.GetX()) + abs(self._y - event.GetY()) ) >= 3: self.control.ReleaseMouse() self._x = None if self._is_file: FileDropSource(self.control, self.value) else: PythonDropSource(self.control, self.value) event.Skip() # ----- Drag and drop event handlers: ------------------------------------- def wx_dropped_on(self, x, y, data, drag_result): """Handles a Python object being dropped on the tree.""" try: self.value = self._get_drag_data(data) return drag_result except: return wx.DragNone def wx_drag_over(self, x, y, data, drag_result): """Handles a Python object being dragged over the tree.""" try: self.object.base_trait(self.name).validate( self.object, self.name, self._get_drag_data(data) ) return drag_result except: return wx.DragNone class CustomEditor(SimpleEditor): """Custom style of drag-and-drop editor, which is not a drag source.""" # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- #: Is the editor a drag source? This value overrides the default. drag_source = False class ReadonlyEditor(SimpleEditor): """Read-only style of drag-and-drop editor, which is not a drop target.""" # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- #: Is the editor a drop target? This value overrides the default. drop_target = False class FileDropSource(wx.DropSource): """Represents a draggable file.""" def __init__(self, source, files): """Initializes the object.""" self.handler = None self.allow_move = True # Put the data to be dragged on the clipboard: clipboard.data = files clipboard.source = source clipboard.drop_source = self data_object = wx.FileDataObject() if isinstance(files, string_type): files = [files] for file in files: data_object.AddFile(file) # Create the drop source and begin the drag and drop operation: super().__init__(source) self.SetData(data_object) self.result = self.DoDragDrop(True) def on_dropped(self, drag_result): """Called when the data has been dropped.""" return ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/wx/drop_editor.py0000644000175100001730000000654600000000000021366 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Defines a drop target editor for the wxPython user interface toolkit. A drop target editor handles drag and drop operations as a drop target. """ import wx from pyface.wx.drag_and_drop import PythonDropTarget, clipboard from .text_editor import SimpleEditor as Editor from .constants import DropColor # ------------------------------------------------------------------------- # 'SimpleEditor' class: # ------------------------------------------------------------------------- class SimpleEditor(Editor): """Simple style of drop editor, which displays a read-only text field that contains the string representation of the object trait's value. """ #: Background color when it is OK to drop objects. ok_color = DropColor def init(self, parent): """Finishes initializing the editor by creating the underlying toolkit widget. """ if self.factory.readonly: self.control = wx.TextCtrl( parent, -1, self.str_value, style=wx.TE_READONLY ) self.set_tooltip() else: super().init(parent) self.control.SetBackgroundColour(self.ok_color) self.control.SetDropTarget(PythonDropTarget(self)) def string_value(self, value): """Returns the text representation of a specified object trait value.""" if value is None: return "" return str(value) def error(self, excp): """Handles an error that occurs while setting the object's trait value.""" pass # ----- Drag and drop event handlers: ------------------------------------- def wx_dropped_on(self, x, y, data, drag_result): """Handles a Python object being dropped on the tree.""" klass = self.factory.klass value = data if self.factory.binding: value = getattr(clipboard, "node", None) if (klass is None) or isinstance(data, klass): self._no_update = True try: if hasattr(value, "drop_editor_value"): self.value = value.drop_editor_value() else: self.value = value if hasattr(value, "drop_editor_update"): value.drop_editor_update(self.control) else: self.control.SetValue(self.str_value) finally: self._no_update = False return drag_result return wx.DragNone def wx_drag_over(self, x, y, data, drag_result): """Handles a Python object being dragged over the tree.""" if self.factory.binding: data = getattr(clipboard, "node", None) try: self.object.base_trait(self.name).validate( self.object, self.name, data ) return drag_result except: return wx.DragNone # Define the Text and ReadonlyEditor for use by the editor factory. TextEditor = ReadonlyEditor = SimpleEditor ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/wx/editor.py0000644000175100001730000001702000000000000020327 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Defines the base class for wxPython editors. """ import wx from traits.api import HasTraits, Int, Instance, Str, Callable # CIRCULAR IMPORT FIXME: # We are importing from the source instead of from the api in order to # avoid circular imports. The 'toolkit.py' file imports from 'helper' which in # turns imports from this file. Therefore, trying to import # 'traitsui.wx' (which imports the toolkit) will lead to importing # all of the editor factories declared in traitsui.api. In addition,# some of the editor factories have a Color trait defined, and this will lead # to an import of the wx 'toolkit' causing a circular import problem. # Another solution could be to move the GroupEditor object from helper to this # file. from traitsui.editor import Editor as UIEditor from .constants import WindowColor, OKColor, ErrorColor class Editor(UIEditor): """Base class for wxPython editors for Traits-based UIs.""" # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- #: Style for embedding control in a sizer: layout_style = Int(wx.EXPAND) #: The maximum extra padding that should be allowed around the editor: border_size = Int(4) def _control_changed(self, control): """Handles the **control** trait being set.""" if control is not None: control._editor = self def set_focus(self): """Assigns focus to the editor's underlying toolkit widget.""" if self.control is not None: self.control.SetFocus() def update_editor(self): """Updates the editor when the object trait changes externally to the editor. """ new_value = self.str_value if self.control.GetValue() != new_value: self.control.SetValue(new_value) def set_tooltip_text(self, control, text): """Sets the tooltip for a specified control.""" control.SetToolTip(text) def _enabled_changed(self, enabled): """Handles the **enabled** state of the editor being changed.""" control = self.control if control is not None: control.Enable(enabled) control.Refresh() if self.label_control is not None: self.label_control.Enable(enabled) self.label_control.Refresh() def _visible_changed(self, visible): """Handles the **visible** state of the editor being changed.""" control = self.control parent = control.GetParent() # Handle the case where the item whose visibility has changed is a # notebook page: sizer = parent.GetSizer() from pyface.dock.api import DockSizer if isinstance(sizer, DockSizer): dock_controls = sizer.GetContents().get_controls(False) for dock_control in dock_controls: if dock_control.control is control: dock_control.visible = visible sizer.Layout() # Handle a normal item: else: if self.label_control is not None: self.label_control.Show(visible) control.Show(visible) # If only control.Layout() is called there are certain cases where # the newly visible items are sized incorrectly (see ticket 1797) if parent is None: control.Layout() else: parent.Layout() 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() or [] if not isinstance(control, list): control = [control] for item in control: if state: color = ErrorColor if getattr(item, "_ok_color", None) is None: item._ok_color = item.GetBackgroundColour() else: color = getattr(item, "_ok_color", None) if color is None: color = OKColor if isinstance(item, wx.Panel): color = WindowColor item.SetBackgroundColour(color) item.Refresh() def _invalid_changed(self, state): """Handles the editor's invalid state changing.""" self.set_error_state() class EditorWithList(Editor): """Editor for an object that contains a list.""" # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- #: 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_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 ) super().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().dispose() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/wx/editor_factory.py0000644000175100001730000001222100000000000022054 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Defines the base wxPython EditorFactory class and classes the various styles of editors used in a Traits-based user interface. """ import warnings import wx from traits.api import TraitError, Any, Bool, Event, Str from .editor import Editor from .constants import WindowColor class SimpleEditor(Editor): """Base class for simple style editors, which displays a text field containing the text representation of the object trait value. Clicking in the text field displays an editor-specific dialog box for changing the value. """ #: Has the left mouse button been pressed: left_down = Bool(False) def init(self, parent): """Finishes initializing the editor by creating the underlying toolkit widget. """ self.control = self.create_control(parent) self.control.Bind(wx.EVT_LEFT_DOWN, self._enable_popup_editor) self.control.Bind(wx.EVT_LEFT_UP, self._show_popup_editor) self.set_tooltip() def create_control(self, parent): """Creates the control to use for the simple editor.""" return wx.TextCtrl(parent, -1, self.str_value, style=wx.TE_READONLY) # ------------------------------------------------------------------------- # Invokes the pop-up editor for an object trait: # # (Normally overridden in a subclass) # ------------------------------------------------------------------------- def popup_editor(self, event): """Invokes the pop-up editor for an object trait.""" pass def _enable_popup_editor(self, event): """Mark the left mouse button as being pressed currently.""" self.left_down = True def _show_popup_editor(self, event): """Display the popup editor if the left mouse button was pressed previously. """ if self.left_down: self.left_down = False self.popup_editor(event) class TextEditor(Editor): """Base class for text style editors, which displays an editable text field, containing a text representation of the object trait value. """ def init(self, parent): """Finishes initializing the editor by creating the underlying toolkit widget. """ self.control = wx.TextCtrl( parent, -1, self.str_value, style=wx.TE_PROCESS_ENTER ) self.control.Bind(wx.EVT_KILL_FOCUS, self.update_object) parent.Bind( wx.EVT_TEXT_ENTER, self.update_object, id=self.control.GetId() ) self.set_tooltip() def dispose(self): """Disposes of the contents of an editor.""" if self.control is not None: # just in-case parent = self.control.GetParent() parent.Unbind( wx.EVT_TEXT_ENTER, handler=self.update_object, id=self.control.GetId(), ) self.control.Unbind(wx.EVT_KILL_FOCUS, handler=self.update_object) super().dispose() def update_object(self, event): """Handles the user changing the contents of the edit control.""" if isinstance(event, wx.FocusEvent): event.Skip() try: self.value = self.control.GetValue() except TraitError as excp: pass class ReadonlyEditor(Editor): """Base class for read-only style editors, which displays a read-only text field, containing a text representation of the object trait value. """ # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- # layout_style = 0 # Style for imbedding control in a sizer (override) def init(self, parent): """Finishes initializing the editor by creating the underlying toolkit widget. """ if (self.item.resizable is True) or (self.item.height != -1.0): self.control = wx.TextCtrl( parent, -1, self.str_value, style=wx.NO_BORDER | wx.TE_MULTILINE | wx.TE_READONLY, ) self.control.SetBackgroundColour(WindowColor) else: self.control = wx.StaticText( parent, -1, self.str_value, style=wx.ALIGN_LEFT ) self.layout_style = 0 self.set_tooltip() def update_editor(self): """Updates the editor when the object trait changes externally to the editor. """ new_value = self.str_value if (self.item.resizable is True) or (self.item.height != -1.0): if self.control.GetValue() != new_value: self.control.SetValue(new_value) elif self.control.GetLabel() != new_value: self.control.SetLabel(new_value) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/wx/enum_editor.py0000644000175100001730000003562300000000000021364 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Defines the various editors for single-selection enumerations, for the wxPython user interface toolkit. """ import wx from traits.api import Property from traitsui.helper import enum_values_changed from .editor import Editor from .constants import OKColor, ErrorColor from .helper import ( TraitsUIPanel, disconnect, disconnect_no_id, ) from functools import reduce # ------------------------------------------------------------------------- # 'BaseEditor' class: # ------------------------------------------------------------------------- class BaseEditor(Editor): """Base class for enumeration editors.""" # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- #: Current set of enumeration names: names = Property() #: Current mapping from names to values: mapping = Property() #: Current inverse mapping from values to names: inverse_mapping = Property() def init(self, parent): """Finishes initializing the editor by creating the underlying toolkit widget. """ factory = self.factory if factory.name != "": self._object, self._name, self._value = self.parse_extended_name( factory.name ) self.values_changed() self._object.on_trait_change( self._values_changed, " " + self._name, dispatch="ui" ) else: self._value = lambda: self.factory.values self.values_changed() factory.on_trait_change( self._values_changed, "values", dispatch="ui" ) def _get_names(self): """Gets the current set of enumeration names.""" return self._names def _get_mapping(self): """Gets the current mapping.""" return self._mapping def _get_inverse_mapping(self): """Gets the current inverse mapping.""" return self._inverse_mapping def rebuild_editor(self): """Rebuilds the contents of the editor whenever the original factory object's **values** trait changes. """ raise NotImplementedError def values_changed(self): """Recomputes the cached data based on the underlying enumeration model or the values of the factory. """ ( self._names, self._mapping, self._inverse_mapping, ) = enum_values_changed(self._value(), self.string_value) def _values_changed(self): """Handles the underlying object model's enumeration set or factory's values being changed. """ self.values_changed() self.rebuild_editor() def dispose(self): """Disposes of the contents of an editor.""" if self._object is not None: self._object.on_trait_change( self._values_changed, " " + self._name, remove=True ) else: self.factory.on_trait_change( self._values_changed, "values", remove=True ) super().dispose() # ------------------------------------------------------------------------- # 'SimpleEditor' class: # ------------------------------------------------------------------------- class SimpleEditor(BaseEditor): """Simple style of enumeration editor, which displays a combo box.""" def init(self, parent): """Finishes initializing the editor by creating the underlying toolkit widget. """ super().init(parent) factory = self.factory if factory.evaluate is None: self.control = control = wx.Choice( parent, -1, wx.Point(0, 0), wx.Size(-1, -1), self.names ) parent.Bind( wx.EVT_CHOICE, self.update_object, id=self.control.GetId() ) else: self.control = control = wx.ComboBox( parent, -1, "", wx.Point(0, 0), wx.Size(-1, -1), self.names, style=wx.CB_DROPDOWN, ) parent.Bind( wx.EVT_COMBOBOX, self.update_object, id=control.GetId() ) parent.Bind( wx.EVT_TEXT_ENTER, self.update_text_object, id=control.GetId() ) control.Bind(wx.EVT_KILL_FOCUS, self.on_kill_focus) if (not factory.is_grid_cell) and factory.auto_set: parent.Bind( wx.EVT_TEXT, self.update_text_object, id=control.GetId() ) self._no_enum_update = 0 self.set_tooltip() def dispose(self): """Disposes of the contents of an editor.""" disconnect( self.control, wx.EVT_COMBOBOX, wx.EVT_TEXT_ENTER, wx.EVT_TEXT ) disconnect_no_id(self.control, wx.EVT_KILL_FOCUS) super().dispose() def update_object(self, event): """Handles the user selecting a new value from the combo box.""" self._no_enum_update += 1 try: new_value = self.mapping[event.GetString()] if new_value == self.value and self.factory.is_grid_cell: # If the enum editor is in a grid cell and the value did not # change, we want the enum editor to go away, reverting back to # the normal cell appearance. This is for 2 reasons: # 1. it looks nicer # 2. if the grid id suddenly closed, wx freaks & causes a # segfault grid = self.control.Parent.Parent grid.EnableEditing(False) grid.EnableEditing(True) self.value = new_value except: from traitsui.api import raise_to_debug raise_to_debug() self._no_enum_update -= 1 def update_text_object(self, event): """Handles the user typing text into the combo box text entry field.""" if self._no_enum_update == 0: value = self.control.GetValue() try: value = self.mapping[value] except: try: value = self.factory.evaluate(value) except Exception as excp: self.error(excp) return self._no_enum_update += 1 try: self.value = value self.control.SetBackgroundColour(OKColor) self.control.Refresh() except: pass self._no_enum_update -= 1 def on_kill_focus(self, event): """Handles the control losing the keyboard focus.""" self.update_text_object(event) event.Skip() def update_editor(self): """Updates the editor when the object trait changes externally to the editor. """ if self._no_enum_update == 0: if self.factory.evaluate is None: try: self.control.SetStringSelection( self.inverse_mapping[self.value] ) except: pass else: try: self.control.SetValue(self.str_value) except: pass def error(self, excp): """Handles an error that occurs while setting the object's trait value.""" self.control.SetBackgroundColour(ErrorColor) self.control.Refresh() def rebuild_editor(self): """Rebuilds the contents of the editor whenever the original factory object's **values** trait changes. """ # Note: This code is unnecessarily complex due to a strange bug in # wxWidgets implementation of the wx.Combobox control that has strange # behavior when the current text field value is one of the selection # values when 'Clear' is called. In this case, even saving and # restoring the text field value does not work, so we go to great # lengths to detect this case and avoid using 'Clear', but still get # the equivalent visual results. Modify this code at your own risk... control = self.control clear = True cur_name = None if self.factory.evaluate is not None: n = control.GetCount() cur_names = [control.GetString(i) for i in range(n)] cur_name = control.GetValue() if cur_name in self.names: clear = False include = True for i in range(n - 1, -1, -1): if cur_name == cur_names[i]: include = False else: control.Delete(i) for name in self.names: if include or (name != cur_name): control.Append(name) cur_name = None else: point = control.GetInsertionPoint() if clear: control.Clear() control.AppendItems(self.names) if cur_name is not None: self._no_enum_update += 1 control.SetValue(cur_name) control.SetInsertionPoint(point) self._no_enum_update -= 1 self.update_editor() class RadioEditor(BaseEditor): """Enumeration editor, used for the "custom" style, that displays radio buttons. """ def init(self, parent): """Finishes initializing the editor by creating the underlying toolkit widget. """ super().init(parent) # Create a panel to hold all of the radio buttons: self.control = TraitsUIPanel(parent, -1) self.rebuild_editor() def update_object(self, event): """Handles the user clicking one of the custom radio buttons.""" try: self.value = event.GetEventObject().value except: pass def update_editor(self): """Updates the editor when the object trait changes externally to the editor. """ value = self.value for button in self.control.GetChildren(): state = button.value == value button.SetValue(state) if state: button.SetFocus() def rebuild_editor(self): """Rebuilds the contents of the editor whenever the original factory object's **values** trait changes. """ # Clear any existing content: panel = self.control panel.SetSizer(None) panel.DestroyChildren() # Get the current trait value: cur_name = self.str_value # Create a sizer to manage the radio buttons: names = self.names mapping = self.mapping n = len(names) cols = self.factory.cols rows = (n + cols - 1) // cols # incr will keep track of how to increment index so that as we traverse # the grid in row major order, the elements are added to appear in # column major order incr = [n // cols] * cols rem = n % cols for i in range(cols): incr[i] += rem > i incr[-1] = -(reduce(lambda x, y: x + y, incr[:-1], 0) - 1) # e.g for a gird: # 0 2 4 # 1 3 5 # incr should be [2, 2, -3] if cols > 1: sizer = wx.GridSizer(0, cols, 2, 4) else: sizer = wx.BoxSizer(wx.VERTICAL) # Add the set of all possible choices: style = wx.RB_GROUP index = 0 # populate the layout in row_major order for i in range(rows): for j in range(cols): if n > 0: name = label = names[index] label = self.string_value(label, str.capitalize) control = wx.RadioButton(panel, -1, label, style=style) control.value = mapping[name] style = 0 control.SetValue(name == cur_name) panel.Bind( wx.EVT_RADIOBUTTON, self.update_object, id=control.GetId(), ) self.set_tooltip(control) index += incr[j] n -= 1 else: control = wx.RadioButton(panel, -1, "") control.value = "" control.Show(False) sizer.Add(control, 0, wx.NORTH, 5) # Set-up the layout: panel.SetSizerAndFit(sizer) class ListEditor(BaseEditor): """Enumeration editor, used for the "custom" style, that displays a list box. """ def init(self, parent): """Finishes initializing the editor by creating the underlying toolkit widget. """ super().init(parent) # Create a panel to hold all of the radio buttons: self.control = wx.ListBox( parent, -1, wx.Point(0, 0), wx.Size(-1, -1), self.names, style=wx.LB_SINGLE | wx.LB_NEEDED_SB, ) parent.Bind( wx.EVT_LISTBOX, self.update_object, id=self.control.GetId() ) self.set_tooltip() def dispose(self): """Disposes of the contents of an editor.""" disconnect(self.control, wx.EVT_LISTBOX) super().dispose() def update_object(self, event): """Handles the user selecting a list box item.""" if not self._ignore_update: value = self.control.GetStringSelection() try: value = self.mapping[value] except: try: value = self.factory.evaluate(value) except: pass try: self.value = value except: pass def update_editor(self): """Updates the editor when the object trait changes externally to the editor. """ control = self.control try: index = control.FindString(self.inverse_mapping[self.value]) if index >= 0: control.SetSelection(index) except: pass def rebuild_editor(self): """Rebuilds the contents of the editor whenever the original factory object's **values** trait changes. """ self._ignore_update = True self.control.Clear() self.control.AppendItems(self.names) self._ignore_update = False # fixme: Is this line necessary? self.update_editor() ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1684768028.1238067 traitsui-8.0.0/traitsui/wx/extra/0000755000175100001730000000000000000000000017612 5ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/wx/extra/__init__.py0000644000175100001730000000000000000000000021711 0ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/wx/extra/bounds_editor.py0000644000175100001730000001753000000000000023032 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! import wx from traits.api import Float, Any, Str, Union from traitsui.editors.api import RangeEditor from traitsui.wx.editor import Editor from traitsui.wx.helper import TraitsUIPanel, Slider class _BoundsEditor(Editor): evaluate = Any() min = Any() max = Any() low = Any() high = Any() 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 self.min = self.low if not factory.high_name: self.high = factory.high self.max = self.high self.max = factory.max self.min = factory.min self.format_str = factory.format self.evaluate = factory.evaluate self.sync_value(factory.evaluate_name, "evaluate", "from") self.sync_value(factory.low_name, "low", "both") self.sync_value(factory.high_name, "high", "both") self.control = panel = TraitsUIPanel(parent, -1) sizer = wx.FlexGridSizer(2, 3, 0, 0) # low text box self._label_lo = wx.TextCtrl( panel, -1, self.format_str % self.low, size=wx.Size(56, 20), style=wx.TE_PROCESS_ENTER, ) sizer.Add(self._label_lo, 0, wx.ALIGN_CENTER) self._label_lo.Bind(wx.EVT_TEXT_ENTER, self.update_low_on_enter) self._label_lo.Bind(wx.EVT_KILL_FOCUS, self.update_low_on_enter) # low slider self.control.lslider = Slider( panel, -1, 0, 0, 10000, size=wx.Size(100, 20), style=wx.SL_HORIZONTAL | wx.SL_AUTOTICKS, ) self.control.lslider.SetValue(self._convert_to_slider(self.low)) self.control.lslider.SetTickFreq(1000, 1) self.control.lslider.SetPageSize(1000) self.control.lslider.SetLineSize(100) self.control.lslider.Bind(wx.EVT_SCROLL, self.update_object_on_scroll) sizer.Add(self.control.lslider, 1, wx.EXPAND) sizer.AddStretchSpacer(0) # high slider sizer.AddStretchSpacer(0) self.control.rslider = Slider( panel, -1, self._convert_to_slider(self.high), 0, 10000, size=wx.Size(100, 20), style=wx.SL_HORIZONTAL | wx.SL_AUTOTICKS, ) self.control.rslider.SetTickFreq(1000, 1) self.control.rslider.SetPageSize(1000) self.control.rslider.SetLineSize(100) self.control.rslider.Bind(wx.EVT_SCROLL, self.update_object_on_scroll) sizer.Add(self.control.rslider, 1, wx.EXPAND) # high text box self._label_hi = wx.TextCtrl( panel, -1, self.format_str % self.high, size=wx.Size(56, 20), style=wx.TE_PROCESS_ENTER, ) sizer.Add(self._label_hi, 0, wx.ALIGN_CENTER) self._label_hi.Bind(wx.EVT_TEXT_ENTER, self.update_high_on_enter) self._label_hi.Bind(wx.EVT_KILL_FOCUS, self.update_high_on_enter) self.set_tooltip(self.control.lslider) self.set_tooltip(self.control.rslider) self.set_tooltip(self._label_lo) self.set_tooltip(self._label_hi) # Set-up the layout: panel.SetSizerAndFit(sizer) def dispose(self): self._label_hi.Unbind(wx.EVT_TEXT_ENTER) self._label_hi.Unbind(wx.EVT_KILL_FOCUS) self._label_lo.Unbind(wx.EVT_TEXT_ENTER) self._label_lo.Unbind(wx.EVT_KILL_FOCUS) def update_low_on_enter(self, event): if isinstance(event, wx.FocusEvent): event.Skip() try: try: low = eval(str(self._label_lo.GetValue()).strip()) if self.evaluate is not None: low = self.evaluate(low) except Exception as ex: low = self.low self._label_lo.SetValue(self.format_str % self.low) if not self.factory.is_float: low = int(low) if low > self.high: low = self.high - self._step_size() self._label_lo.SetValue(self.format_str % low) self.control.lslider.SetValue(self._convert_to_slider(low)) self.low = low except: pass def update_high_on_enter(self, event): if isinstance(event, wx.FocusEvent): event.Skip() try: try: high = eval(str(self._label_hi.GetValue()).strip()) if self.evaluate is not None: high = self.evaluate(high) except: high = self.high self._label_hi.SetValue(self.format_str % self.high) if not self.factory.is_float: high = int(high) if high < self.low: high = self.low + self._step_size() self._label_hi.SetValue(self.format_str % high) self.control.rslider.SetValue(self._convert_to_slider(high)) self.high = high except: pass def update_object_on_scroll(self, evt): low = self._convert_from_slider(self.control.lslider.GetValue()) high = self._convert_from_slider(self.control.rslider.GetValue()) if low >= high: if evt.Position == self.control.lslider.GetValue(): low = self.high - self._step_size() else: high = self.low + self._step_size() if self.factory.is_float: self.low = low self.high = high else: self.low = int(low) self.high = int(high) # update the sliders to the int values or the sliders # will jiggle self.control.lslider.SetValue(self._convert_to_slider(low)) self.control.rslider.SetValue(self._convert_to_slider(high)) def update_editor(self): return def _check_max_and_min(self): # check if max & min have been defined: if self.max is None: self.max = self.high if self.min is None: self.min = self.low def _step_size(self): slider_delta = ( self.control.lslider.GetMax() - self.control.lslider.GetMin() ) range_delta = self.max - self.min return float(range_delta) / slider_delta def _convert_from_slider(self, slider_val): self._check_max_and_min() return self.min + slider_val * self._step_size() def _convert_to_slider(self, value): self._check_max_and_min() return ( self.control.lslider.GetMin() + (value - self.min) / self._step_size() ) def _low_changed(self, low): if self.control is None: return if self._label_lo is not None: self._label_lo.SetValue(self.format_str % low) self.control.lslider.SetValue(self._convert_to_slider(low)) def _high_changed(self, high): if self.control is None: return if self._label_hi is not None: self._label_hi.SetValue(self.format_str % high) self.control.rslider.SetValue(self._convert_to_slider(self.high)) class BoundsEditor(RangeEditor): min = Union(None, Float) max = Union(None, Float) def _get_simple_editor_class(self): return _BoundsEditor def _get_custom_editor_class(self): return _BoundsEditor ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/wx/extra/led_editor.py0000644000175100001730000000344200000000000022301 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Traits UI 'display only' LED numeric editor. """ from wx.gizmos import ( LEDNumberCtrl, LED_ALIGN_LEFT, LED_ALIGN_CENTER, LED_ALIGN_RIGHT, ) from traits.api import Enum from traitsui.wx.editor import Editor from traitsui.basic_editor_factory import BasicEditorFactory # LED alignment styles: LEDStyles = { "left": LED_ALIGN_LEFT, "center": LED_ALIGN_CENTER, "right": LED_ALIGN_RIGHT, } class _LEDEditor(Editor): """Traits UI 'display only' LED numeric editor.""" def init(self, parent): """Finishes initializing the editor by creating the underlying toolkit widget. """ self.control = LEDNumberCtrl(parent, -1) self.control.SetAlignment(LEDStyles[self.factory.alignment]) self.set_tooltip() def update_editor(self): """Updates the editor when the object trait changes externally to the editor. """ self.control.SetValue(self.str_value) # ------------------------------------------------------------------------- # Create the editor factory object: # ------------------------------------------------------------------------- # wxPython editor factory for LED editors: class LEDEditor(BasicEditorFactory): #: The editor class to be created: klass = _LEDEditor #: The alignment of the numeric text within the control: alignment = Enum("right", "left", "center") ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1684768028.1238067 traitsui-8.0.0/traitsui/wx/extra/windows/0000755000175100001730000000000000000000000021304 5ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/wx/extra/windows/__init__.py0000644000175100001730000000000000000000000023403 0ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/wx/extra/windows/flash_editor.py0000644000175100001730000000352400000000000024325 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Traits UI MS Flash editor. """ import wx if wx.Platform == "__WXMSW__": from wx.lib.flashwin import FlashWindow from traitsui.wx.editor import Editor from traitsui.basic_editor_factory import BasicEditorFactory class _FlashEditor(Editor): """Traits UI Flash editor.""" # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- #: Is the table editor is scrollable? This value overrides the default. scrollable = True def init(self, parent): """Finishes initializing the editor by creating the underlying toolkit widget. """ self.control = FlashWindow(parent) self.set_tooltip() def update_editor(self): """Updates the editor when the object trait changes externally to the editor. """ value = self.str_value.strip() if value.find("://") < 0: value = "file://" + value wx.BeginBusyCursor() self.control.LoadMovie(0, value) wx.EndBusyCursor() # ------------------------------------------------------------------------- # Create the editor factory object: # ------------------------------------------------------------------------- # wxPython editor factory for Flash editors: class FlashEditor(BasicEditorFactory): #: The editor class to be created: klass = _FlashEditor ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/wx/extra/windows/ie_html_editor.py0000644000175100001730000001774500000000000024663 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Traits UI MS Internet Explorer editor. """ import re import webbrowser import wx if wx.Platform == "__WXMSW__": # The new version of IEHTMLWindow (wx 2.8.8.0) is mostly compatible with # the old one, but it has changed the API for handling COM events, so we # cannot use it. try: import wx.lib.iewin_old as iewin except ImportError: import wx.lib.iewin as iewin from traits.api import Bool, Event, Property, Str from traitsui.wx.editor import Editor from traitsui.basic_editor_factory import BasicEditorFactory # ------------------------------------------------------------------------- # Constants # ------------------------------------------------------------------------- RELATIVE_OBJECTS_PATTERN = re.compile( r'src=["\'](?!https?:)([\s\w/\.]+?)["\']', re.IGNORECASE ) class _IEHTMLEditor(Editor): """Traits UI MS Internet Explorer editor.""" # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- #: Is the table editor is scrollable? This value overrides the default. scrollable = True #: External objects referenced in the HTML are relative to this url base_url = Str() #: Event fired when the browser home page should be displayed: home = Event() #: Event fired when the browser should show the previous page: back = Event() #: Event fired when the browser should show the next page: forward = Event() #: Event fired when the browser should stop loading the current page: stop = Event() #: Event fired when the browser should refresh the current page: refresh = Event() #: Event fired when the browser should search the current page: search = Event() #: The current browser status: status = Str() #: The current browser page title: title = Str() #: The URL of the page that just finished loading: page_loaded = Str() #: The current page content as HTML: html = Property() def init(self, parent): """Finishes initializing the editor by creating the underlying toolkit widget. """ self.control = ie = iewin.IEHtmlWindow( parent, -1, style=wx.NO_FULL_REPAINT_ON_RESIZE ) self.set_tooltip() factory = self.factory self.base_url = factory.base_url self.sync_value(factory.home, "home", "from") self.sync_value(factory.back, "back", "from") self.sync_value(factory.forward, "forward", "from") self.sync_value(factory.stop, "stop", "from") self.sync_value(factory.refresh, "refresh", "from") self.sync_value(factory.search, "search", "from") self.sync_value(factory.status, "status", "to") self.sync_value(factory.title, "title", "to") self.sync_value(factory.page_loaded, "page_loaded", "to") self.sync_value(factory.html, "html", "to") self.sync_value(factory.base_url_name, "base_url", "from") parent.Bind(iewin.EVT_StatusTextChange, ie, id=self._status_modified) parent.Bind(iewin.EVT_TitleChange, ie, id=self._title_modified) parent.Bind( iewin.EVT_DocumentComplete, ie, id=self._page_loaded_modified ) parent.Bind(iewin.EVT_NewWindow2, ie, id=self._new_window_modified) parent.Bind(iewin.EVT_BeforeNavigate2, ie, id=self._navigate_requested) def update_editor(self): """Updates the editor when the object trait changes externally to the editor. """ value = self.str_value.strip() # We can correct URLs via the BeforeNavigate Event, but the COM # interface provides no such option for images. Sadly, we are forced # to take a more brute force approach. if self.base_url: rep = lambda m: r'src="%s%s"' % (self.base_url, m.group(1)) value = re.sub(RELATIVE_OBJECTS_PATTERN, rep, value) if value == "": self.control.LoadString("") elif value[:1] == "<": self.control.LoadString(value) elif (value[:4] != "http") or (value.find("://") < 0): try: with open(value, "rb") as file: self.control.LoadStream(file) except: pass else: self.control.Navigate(value) # -- Property Implementations --------------------------------------------- def _get_html(self): return self.control.GetText() def _set_html(self, value): self.control.LoadString(value) # -- Event Handlers ------------------------------------------------------- def _home_changed(self): self.control.GoHome() def _back_changed(self): self.control.GoBack() def _forward_changed(self): self.control.GoForward() def _stop_changed(self): self.control.Stop() def _search_changed(self): self.control.GoSearch() def _refresh_changed(self): self.control.Refresh(iewin.REFRESH_COMPLETELY) def _status_modified(self, event): self.status = event.Text def _title_modified(self, event): self.title = event.Text def _page_loaded_modified(self, event): self.page_loaded = event.URL self.trait_property_changed("html", "", self.html) def _new_window_modified(self, event): # If the event is cancelled, new windows can be disabled. # At this point we've opted to allow new windows pass def _navigate_requested(self, event): # The way NavigateToString works is to navigate to about:blank then # load the supplied HTML into the document property. This borks # relative URLs. if event.URL.startswith("about:"): base = self.base_url if not base.endswith("/"): base += "/" event.URL = base + event.URL[6:] if self.factory.open_externally: event.Cancel = True webbrowser.get("windows-default").open_new(event.URL) # ------------------------------------------------------------------------- # Create the editor factory object: # ------------------------------------------------------------------------- # wxPython editor factory for MS Internet Explorer editors: class IEHTMLEditor(BasicEditorFactory): #: The editor class to be created: klass = _IEHTMLEditor #: External objects referenced in the HTML are relative to this url base_url = Str() #: The object trait containing the base URL base_url_name = Str() #: Should links be opened in an external browser? open_externally = Bool(False) #: Optional name of trait used to tell browser to show Home page: home = Str() #: Optional name of trait used to tell browser to view the previous page: back = Str() #: Optional name of trait used to tell browser to view the next page: forward = Str() #: Optional name of trait used to tell browser to stop loading page: stop = Str() #: Optional name of trait used to tell browser to refresh the current page: refresh = Str() #: Optional name of trait used to tell browser to search the current page: search = Str() #: Optional name of trait used to contain the current browser status: status = Str() #: Optional name of trait used to contain the current browser page title: title = Str() #: Optional name of trait used to contain the URL of the page that just #: completed loading: page_loaded = Str() #: Optional name of trait used to get/set the page content as HTML: html = Str() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/wx/file_editor.py0000644000175100001730000003267300000000000021341 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Defines file editors for the wxPython user interface toolkit. """ from os.path import abspath, split, splitext, isfile, exists import wx from pyface.api import FileDialog, OK from traits.api import List, Str, Event, Any, observe, TraitError from .text_editor import SimpleEditor as SimpleTextEditor from .helper import TraitsUIPanel, PopupControl # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- # Wildcard filter: filter_trait = List(Str) # ------------------------------------------------------------------------- # 'SimpleEditor' class: # ------------------------------------------------------------------------- class SimpleEditor(SimpleTextEditor): """Simple style of file editor, consisting of a text field and a **Browse** button that opens a file-selection dialog box. The user can also drag and drop a file onto this control. """ #: The history control (used if the factory 'entries' > 0): history = Any() #: The popup file control (an Instance( PopupFile )): popup = Any() #: Wildcard filter to apply to the file dialog: filter = filter_trait def init(self, parent): """Finishes initializing the editor by creating the underlying toolkit widget. """ self.control = panel = TraitsUIPanel(parent, -1) sizer = wx.BoxSizer(wx.HORIZONTAL) factory = self.factory if factory.entries > 0: from .history_control import HistoryControl self.history = HistoryControl( entries=factory.entries, auto_set=factory.auto_set ) control = self.history.create_control(panel) pad = 3 button = wx.Button(panel, -1, "...", size=wx.Size(28, -1)) else: if factory.enter_set: control = wx.TextCtrl(panel, -1, "", style=wx.TE_PROCESS_ENTER) panel.Bind( wx.EVT_TEXT_ENTER, self.update_object, id=control.GetId() ) else: control = wx.TextCtrl(panel, -1, "") control.Bind(wx.EVT_KILL_FOCUS, self.update_object) if factory.auto_set: panel.Bind(wx.EVT_TEXT, self.update_object, id=control.GetId()) bmp = wx.ArtProvider.GetBitmap(wx.ART_FOLDER_OPEN, size=(15, 15)) button = wx.BitmapButton(panel, -1, bitmap=bmp) pad = 8 self._file_name = control sizer.Add(control, 1, wx.EXPAND) sizer.Add(button, 0, wx.LEFT | wx.ALIGN_CENTER, pad) panel.Bind(wx.EVT_BUTTON, self.show_file_dialog, id=button.GetId()) panel.SetDropTarget(FileDropTarget(self)) panel.SetSizerAndFit(sizer) self._button = button self.set_tooltip(control) self.filter = factory.filter self.sync_value(factory.filter_name, "filter", "from", is_list=True) def dispose(self): """Disposes of the contents of an editor.""" panel = self.control panel.Unbind(wx.EVT_BUTTON, id=self._button.GetId()) self._button = None if self.history is not None: self.history.dispose() self.history = None else: control, self._file_name = self._file_name, None control.Unbind(wx.EVT_KILL_FOCUS) panel.Unbind(wx.EVT_TEXT_ENTER, id=control.GetId()) panel.Unbind(wx.EVT_TEXT, id=control.GetId()) super().dispose() @observe("history:value") def _history_value_changed(self, event): """Handles the history 'value' trait being changed.""" value = event.new if not self._no_update: self._update(value) def update_object(self, event): """Handles the user changing the contents of the edit control.""" if isinstance(event, wx.FocusEvent): event.Skip() self._update(self._file_name.GetValue()) def update_editor(self): """Updates the editor when the object trait changes externally to the editor. """ if self.history is not None: self._no_update = True self.history.value = self.str_value self._no_update = False else: self._file_name.SetValue(self.str_value) def show_file_dialog(self, event=None): """Displays the pop-up file dialog.""" if self.history is not None: self.popup = self._create_file_popup() else: dlg = self._create_file_dialog() dlg.open() if dlg.return_code == OK: if self.factory.truncate_ext: self.value = splitext(dlg.path)[0] else: self.value = dlg.path self.update_editor() def get_error_control(self): """Returns the editor's control for indicating error status.""" return self._file_name # -- Traits Event Handlers ------------------------------------------------ @observe("popup:value") def _popup_value_changed(self, event): """Handles the popup value being changed.""" file_name = event.new if self.factory.truncate_ext: file_name = splitext(file_name)[0] self.value = file_name self._no_update = True self.history.set_value(self.str_value) self._no_update = False @observe("popup:closed") def _popup_closed_changed(self, event): """Handles the popup control being closed.""" self.popup = None # -- UI preference save/restore interface --------------------------------- def restore_prefs(self, prefs): """Restores any saved user preference information associated with the editor. """ if self.history is not None: self.history.history = prefs.get("history", [])[ : self.factory.entries ] def save_prefs(self): """Returns any user preference information associated with the editor.""" if self.history is not None: return {"history": self.history.history[:]} return None # -- Private Methods ------------------------------------------------------ def _create_file_dialog(self): """Creates the correct type of file dialog.""" if len(self.factory.filter) > 0: wildcard = "|".join(self.factory.filter) else: wildcard = "All Files (*.*)|*.*" dlg = FileDialog( parent=self.get_control_widget(), default_path=self._file_name.GetValue(), action="save as" if self.factory.dialog_style == "save" else "open", wildcard=wildcard, ) return dlg def _create_file_popup(self): """Creates the correct type of file popup.""" return PopupFile( control=self.control, file_name=self.str_value, filter=self.factory.filter, height=300, ) def _update(self, file_name): """Updates the editor value with a specified file name.""" try: if self.factory.truncate_ext: file_name = splitext(file_name)[0] self.value = file_name except TraitError as excp: pass def _get_value(self): """Returns the current file name from the edit control.""" if self.history is not None: return self.history.value return self._file_name.GetValue() class CustomEditor(SimpleTextEditor): """Custom style of file editor, consisting of a file system tree view.""" #: Is the file editor scrollable? This value overrides the default. scrollable = True #: Wildcard filter to apply to the file dialog: filter = filter_trait #: Event fired when the file system view should be rebuilt: reload = Event() #: Event fired when the user double-clicks a file: dclick = Event() def init(self, parent): """Finishes initializing the editor by creating the underlying toolkit widget. """ style = self.get_style() factory = self.factory if (len(factory.filter) > 0) or (factory.filter_name != ""): style |= wx.DIRCTRL_SHOW_FILTERS self.control = wx.GenericDirCtrl(parent, style=style) self._tree = tree = self.control.GetTreeCtrl() id = tree.GetId() tree.Bind(wx.EVT_TREE_SEL_CHANGED, self.update_object, id=id) tree.Bind(wx.EVT_TREE_ITEM_ACTIVATED, self._on_dclick, id=id) tree.Bind(wx.EVT_TREE_ITEM_GETTOOLTIP, self._on_tooltip, id=id) self.filter = factory.filter self.sync_value(factory.filter_name, "filter", "from", is_list=True) self.sync_value(factory.reload_name, "reload", "from") self.sync_value(factory.dclick_name, "dclick", "to") self.set_tooltip() def dispose(self): """Disposes of the contents of an editor.""" tree, self._tree = self._tree, None tree.Unbind(wx.EVT_TREE_SEL_CHANGED) tree.Unbind(wx.EVT_TREE_ITEM_ACTIVATED) super().dispose() def update_object(self, event): """Handles the user changing the contents of the edit control.""" if self.control is not None: path = self.control.GetPath() if self.factory.allow_dir or isfile(path): if self.factory.truncate_ext: path = splitext(path)[0] self.value = path def update_editor(self): """Updates the editor when the object trait changes externally to the editor. """ if exists(self.str_value): self.control.SetPath(self.str_value) def get_style(self): """Returns the basic style to use for the control.""" return wx.DIRCTRL_EDIT_LABELS def get_error_control(self): """Returns the editor's control for indicating error status.""" return self._tree def _filter_changed(self): """Handles the 'filter' trait being changed.""" self.control.SetFilter("|".join(self.filter[:])) def _on_dclick(self, event): """Handles the user double-clicking on a file name.""" self.dclick = self.control.GetPath() def _on_tooltip(self, event): """Handles the user hovering on a file name for a tooltip.""" text = self._tree.GetItemText(event.GetItem()) event.SetToolTip(text) def _reload_changed(self): """Handles the 'reload' trait being changed.""" self.control.ReCreateTree() class PopupFile(PopupControl): #: The initially specified file name: file_name = Str() #: The file name filter to support: filter = filter_trait #: Override of PopupControl trait to make the popup resizable: resizable = True # -- PopupControl Method Overrides ---------------------------------------- def create_control(self, parent): """Creates the file control and gets it ready for use.""" style = self.get_style() if len(self.filter) > 0: style |= wx.DIRCTRL_SHOW_FILTERS self._files = files = wx.GenericDirCtrl( parent, style=style, filter="|".join(self.filter) ) files.SetPath(self.file_name) self._tree = tree = files.GetTreeCtrl() tree.Bind(wx.EVT_TREE_SEL_CHANGED, self._select_file, id=tree.GetId()) def dispose(self): self._tree.Unbind(wx.EVT_TREE_SEL_CHANGED) self._tree = self._files = None def get_style(self): """Returns the base style for this type of popup.""" return wx.DIRCTRL_EDIT_LABELS def is_valid(self, path): """Returns whether or not the path is valid.""" return isfile(path) # -- Private Methods ------------------------------------------------------ def _select_file(self, event): """Handles a file being selected in the file control.""" path = self._files.GetPath() # We have to make sure the selected path is different than the original # path because when a filter is changed we get called with the currently # selected path, even though no file was actually selected by the user. # So we only count it if it is a different path. # # We also check the last character of the path, because under Windows # we get a call when the filter is changed for each drive letter. If the # drive is not available, it can take the 'isfile' call a long time to # time out, so we attempt to ignore them by doing a quick test to see # if it could be a valid file name, and ignore it if it is not: if ( (path != abspath(self.file_name)) and (path[-1:] not in ("/\\")) and self.is_valid(path) ): self.value = path class FileDropTarget(wx.FileDropTarget): """A target for a drag and drop operation, which accepts a file.""" def __init__(self, editor): wx.FileDropTarget.__init__(self) self.editor = editor def OnDropFiles(self, x, y, file_names): self.editor.value = file_names[-1] self.editor.update_editor() return True ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/wx/font_editor.py0000644000175100001730000002456600000000000021372 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Defines the various font editors and the font editor factory, for the wxPython user interface toolkit.. """ import wx from traits.api import Bool from traitsui.editors.font_editor import ( ToolkitEditorFactory as BaseToolkitEditorFactory, ) from .editor_factory import ( SimpleEditor as BaseSimpleEditor, TextEditor as BaseTextEditor, ReadonlyEditor as BaseReadonlyEditor, ) from .editor import Editor from .helper import TraitsUIPanel, disconnect # Standard font point sizes PointSizes = [ "8", "9", "10", "11", "12", "14", "16", "18", "20", "22", "24", "26", "28", "36", "48", "72", ] # All available font styles Styles = ["Normal", "Slant", "Italic"] # All available font weights Weights = ["Normal", "Light", "Bold"] # All available font facenames facenames = None # --------------------------------------------------------------------------- # The wxPython ToolkitEditorFactory class. # --------------------------------------------------------------------------- ## We need to add wx-specific methods to the editor factory, and so we create ## a subclass of the BaseToolkitEditorFactory. class ToolkitEditorFactory(BaseToolkitEditorFactory): """wxPython editor factory for font editors.""" show_style = Bool(False) show_weight = Bool(False) def to_wx_font(self, editor): """Returns a wxFont object corresponding to a specified object's font trait. """ font = editor.value return wx.Font( font.GetPointSize(), font.GetFamily(), font.GetStyle(), font.GetWeight(), font.GetUnderlined(), font.GetFaceName(), ) def from_wx_font(self, font): """Gets the application equivalent of a wxPython Font value.""" return font def str_font(self, font): """Returns the text representation of the specified object trait value.""" weight = { wx.FONTWEIGHT_LIGHT: " Light", wx.FONTWEIGHT_BOLD: " Bold", }.get(font.GetWeight(), "") style = { wx.FONTSTYLE_SLANT: " Slant", wx.FONTSTYLE_ITALIC: " Italic", }.get(font.GetStyle(), "") return "%s point %s%s%s" % ( font.GetPointSize(), font.GetFaceName(), style, weight, ) def all_facenames(self): """Returns a list of all available font facenames.""" global facenames if facenames is None: facenames = sorted(FontEnumerator().facenames()) return facenames # ------------------------------------------------------------------------- # 'SimpleFontEditor' class: # ------------------------------------------------------------------------- class SimpleFontEditor(BaseSimpleEditor): """Simple style of font editor, which displays a text field that contains a text representation of the font value (using that font if possible). Clicking the field displays a font selection dialog box. """ def popup_editor(self, event): """Invokes the pop-up editor for an object trait.""" font_data = wx.FontData() font_data.SetInitialFont(self.factory.to_wx_font(self)) dialog = wx.FontDialog(self.control, font_data) if dialog.ShowModal() == wx.ID_OK: self.value = self.factory.from_wx_font( dialog.GetFontData().GetChosenFont() ) self.update_editor() dialog.Destroy() def update_editor(self): """Updates the editor when the object trait changes externally to the editor. """ super().update_editor() set_font(self) def string_value(self, font): """Returns the text representation of a specified font value.""" return self.factory.str_font(font) class CustomFontEditor(Editor): """Custom style of font editor, which displays the following: * A combo box containing all available type face names. * A combo box containing the available type sizes. * A combo box containing the available type styles """ def init(self, parent): """Finishes initializing the editor by creating the underlying toolkit widget. """ # Create a panel to hold all of the buttons: self.control = panel = TraitsUIPanel(parent, -1) sizer = wx.BoxSizer(wx.VERTICAL) # Add all of the font choice controls: sizer2 = wx.BoxSizer(wx.HORIZONTAL) facenames = self.factory.all_facenames() control = self._facename = wx.Choice( panel, -1, wx.Point(0, 0), wx.Size(-1, -1), facenames ) sizer2.Add(control, 4, wx.EXPAND) panel.Bind(wx.EVT_CHOICE, self.update_object_parts, id=control.GetId()) control = self._point_size = wx.Choice( panel, -1, wx.Point(0, 0), wx.Size(-1, -1), PointSizes ) sizer2.Add(control, 1, wx.EXPAND | wx.LEFT, 3) panel.Bind(wx.EVT_CHOICE, self.update_object_parts, id=control.GetId()) if self.factory.show_style: self._style = wx.Choice( panel, -1, wx.Point(0, 0), wx.Size(-1, -1), Styles ) sizer2.Add(self._style, 1, wx.EXPAND | wx.LEFT, 3) panel.Bind( wx.EVT_CHOICE, self.update_object_parts, id=self._style.GetId() ) if self.factory.show_weight: self._weight = wx.Choice( panel, -1, wx.Point(0, 0), wx.Size(-1, -1), Weights ) sizer2.Add(self._weight, 1, wx.EXPAND | wx.LEFT, 3) panel.Bind( wx.EVT_CHOICE, self.update_object_parts, id=self._weight.GetId(), ) sizer.Add(sizer2, 0, wx.EXPAND) # Set-up the layout: panel.SetSizer(sizer) self.set_tooltip() def dispose(self): """Disposes of the contents of an editor.""" disconnect(self._facename, wx.EVT_CHOICE) disconnect(self._point_size, wx.EVT_CHOICE) if self.factory.show_style: disconnect(self._style, wx.EVT_CHOICE) if self.factory.show_weight: disconnect(self._weight, wx.EVT_CHOICE) super().dispose() def update_object_parts(self, event): """Handles the user modifying one of the font components.""" point_size = int(self._point_size.GetStringSelection()) facename = self._facename.GetStringSelection() style = wx.FONTSTYLE_NORMAL if self.factory.show_style: style += self._style.GetCurrentSelection() weight = wx.FONTWEIGHT_NORMAL if self.factory.show_weight: weight += self._weight.GetCurrentSelection() font = wx.Font( point_size, wx.FONTFAMILY_DEFAULT, style, weight, faceName=facename ) self.value = self.factory.from_wx_font(font) def update_editor(self): """Updates the editor when the object trait changes externally to the editor. """ font = self.factory.to_wx_font(self) try: self._facename.SetStringSelection(font.GetFaceName()) except: self._facename.SetSelection(0) try: self._point_size.SetStringSelection(str(font.GetPointSize())) except: self._point_size.SetSelection(0) def string_value(self, font): """Returns the text representation of a specified font value.""" return self.factory.str_font(font) # ------------------------------------------------------------------------- # 'TextFontEditor' class: # ------------------------------------------------------------------------- class TextFontEditor(BaseTextEditor): """Text style of font editor, which displays an editable text field containing a text representation of the font value (using that font if possible). """ def update_object(self, event): """Handles the user changing the contents of the edit control.""" self.value = self.control.GetValue() def update_editor(self): """Updates the editor when the object trait changes external to the editor. """ super().update_editor() set_font(self) def string_value(self, font): """Returns the text representation of a specified font value.""" return self.factory.str_font(font) class ReadonlyFontEditor(BaseReadonlyEditor): """Read-only style of font editor, which displays a read-only text field containing a text representation of the font value (using that font if possible). """ def update_editor(self): """Updates the editor when the object trait changes external to the editor. """ super().update_editor() set_font(self) def string_value(self, font): """Returns the text representation of a specified font value.""" return self.factory.str_font(font) # ------------------------------------------------------------------------- # Set the editor control's font to match a specified font: # ------------------------------------------------------------------------- def set_font(editor): """Sets the editor control's font to match a specified font.""" font = editor.factory.to_wx_font(editor) font.SetPointSize(min(10, font.GetPointSize())) editor.control.SetFont(font) class FontEnumerator(wx.FontEnumerator): """An enumeration of fonts.""" def facenames(self): """Returns a list of all available font facenames.""" self._facenames = [] self.EnumerateFacenames() return self._facenames def OnFacename(self, facename): """Adds a facename to the list of facenames.""" self._facenames.append(facename) return True # Define the names SimpleEditor, CustomEditor, TextEditor and ReadonlyEditor # which are looked up by the editor factory for the font editor. SimpleEditor = SimpleFontEditor CustomEditor = CustomFontEditor TextEditor = TextFontEditor ReadonlyEditor = ReadonlyFontEditor ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/wx/font_trait.py0000644000175100001730000001340500000000000021215 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Trait definition for a wxPython-based font. """ import wx from pyface.font import Font as PyfaceFont from traits.api import Trait, TraitHandler, TraitError # ------------------------------------------------------------------------- # Convert a string into a valid 'wxFont' object (if possible): # ------------------------------------------------------------------------- # Mapping of strings to valid wxFont families font_families = { "default": wx.FONTFAMILY_DEFAULT, "decorative": wx.FONTFAMILY_DECORATIVE, "roman": wx.FONTFAMILY_ROMAN, "script": wx.FONTFAMILY_SCRIPT, "swiss": wx.FONTFAMILY_SWISS, "modern": wx.FONTFAMILY_MODERN, "typewriter": wx.FONTFAMILY_TELETYPE, } # Mapping of strings to wxFont styles font_styles = { "slant": wx.FONTSTYLE_SLANT, "oblique": wx.FONTSTYLE_SLANT, "italic": wx.FONTSTYLE_ITALIC, } # Mapping of strings wxFont weights font_weights = {"light": wx.FONTWEIGHT_LIGHT, "bold": wx.FONTWEIGHT_BOLD} # Strings to ignore in text representations of fonts font_noise = ["pt", "point", "family"] def font_to_str(font): """Converts a wx.Font into a string description of itself.""" family = { wx.FONTFAMILY_DECORATIVE: "decorative ", wx.FONTFAMILY_ROMAN: "roman ", wx.FONTFAMILY_SCRIPT: "script ", wx.FONTFAMILY_SWISS: "swiss ", wx.FONTFAMILY_MODERN: "modern ", wx.FONTFAMILY_TELETYPE: "typewriter ", }.get(font.GetFamily(), "") weight = {wx.FONTWEIGHT_LIGHT: " Light", wx.FONTWEIGHT_BOLD: " Bold"}.get( font.GetWeight(), "" ) style = {wx.FONTSTYLE_SLANT: " Oblique", wx.FONTSTYLE_ITALIC: " Italic"}.get( font.GetStyle(), "" ) underline = "" if font.GetUnderlined(): underline = " underline" return "%s point %s%s%s%s%s" % ( font.GetPointSize(), family, font.GetFaceName(), style, weight, underline, ) def create_traitsfont(value): """Create a TraitsFont object from a string description.""" if isinstance(value, PyfaceFont): return TraitsFont(value.to_toolkit()) if isinstance(value, wx.Font): return TraitsFont(value) point_size = None family = wx.FONTFAMILY_DEFAULT style = wx.FONTSTYLE_NORMAL weight = wx.FONTWEIGHT_NORMAL underline = 0 facename = [] for word in value.split(): lword = word.lower() if lword in font_families: family = font_families[lword] elif lword in font_styles: style = font_styles[lword] elif lword in font_weights: weight = font_weights[lword] elif lword == "underline": underline = 1 elif lword not in font_noise: if point_size is None: try: point_size = int(lword) continue except ValueError: pass facename.append(word) if facename: font = TraitsFont( point_size or 10, family, style, weight, underline, " ".join(facename) ) return font else: return TraitsFont( point_size or 10, family, style, weight, underline ) class TraitsFont(wx.Font): """A Traits-specific wx.Font.""" def __reduce_ex__(self, protocol): """Returns the pickleable form of a TraitsFont object.""" return (create_traitsfont, (font_to_str(self),)) def __str__(self): """Returns a printable form of the font.""" return font_to_str(self) # ------------------------------------------------------------------------- # 'TraitWXFont' class' # ------------------------------------------------------------------------- class TraitWXFont(TraitHandler): """Ensures that values assigned to a trait attribute are valid font descriptor strings; the value actually assigned is the corresponding TraitsFont. """ def validate(self, object, name, value): """Validates that the value is a valid font descriptor string. If so, it returns the corresponding TraitsFont; otherwise, it raises a TraitError. """ if value is None: return None try: return create_traitsfont(value) except: pass raise TraitError(object, name, "a font descriptor string", repr(value)) def info(self): return ( "a string describing a font (e.g. '12 pt bold italic " "swiss family Arial' or 'default 12')" ) # ------------------------------------------------------------------------- # Define a wxPython specific font trait: # ------------------------------------------------------------------------- ### Note: Declare the editor to be a function which returns the FontEditor # class from traits ui to avoid circular import issues. For backwards # compatibility with previous Traits versions, the 'editors' folder in Traits # project declares 'from api import *' in its __init__.py. The 'api' in turn # can contain classes that have a Font trait which lead to this file getting # imported. This leads to a circular import when declaring a Font trait. def get_font_editor(*args, **traits): from .font_editor import ToolkitEditorFactory return ToolkitEditorFactory(*args, **traits) fh = TraitWXFont() WxFont = Trait( wx.SystemSettings.GetFont(wx.SYS_DEFAULT_GUI_FONT), fh, editor=get_font_editor, ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/wx/helper.py0000644000175100001730000004724500000000000020334 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Defines helper functions and classes used to define wxPython-based trait editors and trait editor factories. """ from operator import itemgetter import wx import wx.lib.scrolledpanel import sys from os.path import join, dirname, abspath from traits.api import ( HasPrivateTraits, Enum, CTrait, Instance, Any, Int, Event, Bool, BaseTraitHandler, TraitError, ) from traitsui.ui_traits import convert_image, SequenceTypes from pyface.api import SystemMetrics from pyface.timer.api import do_later from .constants import standard_bitmap_width from .editor import Editor # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- # Layout orientation for a control and its associated editor Orientation = Enum("horizontal", "vertical") # ------------------------------------------------------------------------- # Data: # ------------------------------------------------------------------------- # Bitmap cache dictionary (indexed by filename) _bitmap_cache = {} ### NOTE: This needs major improvements: app_path = None traits_path = None def bitmap_cache(name, standard_size, path=None): """Converts an image file name to a cached bitmap.""" global app_path, traits_path if name[:1] == "@": image = convert_image(name.replace(" ", "_").lower()) if image is not None: return image.create_image().ConvertToBitmap() if path is None: if traits_path is None: import traitsui.wx traits_path = join(dirname(traitsui.wx.__file__), "images") path = traits_path elif path == "": if app_path is None: app_path = join(dirname(sys.argv[0]), "..", "images") path = app_path filename = abspath(join(path, name.replace(" ", "_").lower() + ".gif")) bitmap = _bitmap_cache.get(filename + ("*"[not standard_size :])) if bitmap is not None: return bitmap std_bitmap = bitmap = wx.Bitmap(wx.Image(filename)) _bitmap_cache[filename] = bitmap dx = bitmap.GetWidth() if dx < standard_bitmap_width: dy = bitmap.GetHeight() std_bitmap = wx.Bitmap(standard_bitmap_width, dy) dc1 = wx.MemoryDC() dc2 = wx.MemoryDC() dc1.SelectObject(std_bitmap) dc2.SelectObject(bitmap) dc1.SetPen(wx.TRANSPARENT_PEN) dc1.SetBrush(wx.WHITE_BRUSH) dc1.DrawRectangle(0, 0, standard_bitmap_width, dy) dc1.Blit((standard_bitmap_width - dx) // 2, 0, dx, dy, dc2, 0, 0) _bitmap_cache[filename + "*"] = std_bitmap if standard_size: return std_bitmap return bitmap # ------------------------------------------------------------------------- # Returns an appropriate width for a wxChoice widget based upon the list of # values it contains: # ------------------------------------------------------------------------- def choice_width(values): """Returns an appropriate width for a wxChoice widget based upon the list of values it contains: """ return max([len(x) for x in values]) * 6 def save_window(ui): """Saves the user preference items for a specified UI.""" control = ui.control ui.save_prefs(control.GetPosition() + control.GetSize()) def restore_window(ui, is_popup=False): """Restores the user preference items for a specified UI.""" prefs = ui.restore_prefs() if prefs is not None: x, y, dx, dy = prefs # Check to see if the window's position is within a display. # If it is not entirely within 1 display, move it and/or # resize it to the closest window closest = find_closest_display(x, y) x, y, dx, dy = get_position_for_display(x, y, dx, dy, closest) if is_popup: position_window(ui.control, dx, dy) else: if (dx, dy) == (0, 0): # The window was saved minimized ui.control.SetSize(x, y, -1, -1) else: ui.control.SetSize(x, y, dx, dy) def find_closest_display(x, y): """For a virtual screen position, find the closest display. There are a few reasons to use this function: * the number of displays changed * the size of the displays changed * the orientation of one or more displays changed. """ closest = None for display_num in range(wx.Display.GetCount()): display = wx.Display(display_num) if closest is None: closest = display else: def _distance(x, y, display): dis_x, dis_y, dis_w, dis_h = display.GetGeometry() dis_mid_x = dis_x + dis_w // 2 dis_mid_y = dis_y + dis_h // 2 return (x - dis_mid_x) ** 2 + (y - dis_mid_y) ** 2 if _distance(x, y, display) < _distance(x, y, closest): closest = display return closest def get_position_for_display(x, y, dx, dy, display): """calculates a valid position and size for a window to fit inside a display """ dis_x, dis_y, dis_w, dis_h = display.GetGeometry() dx = min(dx, dis_w) dy = min(dy, dis_h) if ((x + dx) > (dis_x + dis_w)) or (x < dis_x): x = dis_x if ((y + dy) > (dis_y + dis_h)) or (y < dis_y): y = dis_y return x, y, dx, dy # ------------------------------------------------------------------------- # Positions a window on the screen with a specified width and height so that # the window completely fits on the screen if possible: # ------------------------------------------------------------------------- def position_window(window, width=None, height=None, parent=None): """Positions a window on the screen with a specified width and height so that the window completely fits on the screen if possible. """ dx, dy = window.GetSize() width = width or dx height = height or dy if parent is None: parent = window._parent if parent is None: # Center the popup on the screen: window.SetSize( (SystemMetrics().screen_width - width) // 2, (SystemMetrics().screen_height - height) // 2, width, height, ) return # Calculate the desired size of the popup control: if isinstance(parent, wx.Window): x, y = parent.ClientToScreen(0, 0) parent_dx, parent_dy = parent.GetSize() else: # Special case of parent being a screen position and size tuple (used # to pop-up a dialog for a table cell): x, y, parent_dx, parent_dy = parent adjacent = getattr(window, "_kind", "popup") == "popup" width = min(max(parent_dx, width), SystemMetrics().screen_width) height = min(height, SystemMetrics().screen_height) closest = find_closest_display(x, y) if adjacent: y += parent_dy x, y, dx, dy = get_position_for_display(x, y, width, height, closest) window.SetSize(x, y, dx, dy) def top_level_window_for(control): """Returns the top-level window for a specified control.""" parent = control.GetParent() while parent is not None: control = parent parent = control.GetParent() return control def disconnect(control, *events): """Disconnects a wx event handle from its associated control.""" id = control.GetId() for event in events: control.Unbind(event, id=id) def disconnect_no_id(control, *events): """Disconnects a wx event handle from its associated control.""" for event in events: control.Unbind(event) # ------------------------------------------------------------------------- # Creates a wx.Panel that correctly sets its background color to be the same # as its parents: # ------------------------------------------------------------------------- class TraitsUIPanel(wx.Panel): def __init__(self, parent, *args, **kw): """Creates a wx.Panel that correctly sets its background color to be the same as its parents. """ bg_color = kw.pop("bg_color", None) wx.Panel.__init__(self, parent, *args, **kw) self.Bind(wx.EVT_CHILD_FOCUS, self.OnChildFocus) if bg_color: self.SetBackgroundColour(bg_color) else: # Mac/Win needs this, otherwise background color is black attr = self.GetDefaultAttributes() self.SetBackgroundColour(attr.colBg) def OnChildFocus(self, event): """If the ChildFocusEvent contains one of the Panel's direct children, then we will Skip it to let it pass up the widget hierarchy. Otherwise, we consume the event to make sure it doesn't go any farther. This works around a problem in wx 2.8.8.1 where each Panel in a nested hierarchy generates many events that might consume too many resources. We do, however, let one event bubble up to the top so that it may inform a top-level ScrolledPanel that a descendant has acquired focus. """ if event.GetWindow() in self.GetChildren(): event.Skip() def Destroy(self): self.Unbind(wx.EVT_CHILD_FOCUS) super().Destroy() # ------------------------------------------------------------------------- # 'ChildFocusOverride' class: # ------------------------------------------------------------------------- class ChildFocusOverride(wx.EvtHandler): """Override the scroll-to-focus behaviour in wx 2.8.8's ScrolledWindow C++ implementation for ScrolledPanel. Instantiating this class with the ScrolledPanel will register the new instance as the event handler for the panel. """ def __init__(self, window): self.window = window super().__init__() # Make self the event handler for the window. window.PushEventHandler(self) def ProcessEvent(self, event): if isinstance(event, wx.ChildFocusEvent): # Handle this one with our code and don't let the C++ event handler # get it. return self.window.OnChildFocus(event) else: # Otherwise, just pass this along in the event handler chain. result = self.GetNextHandler().ProcessEvent(event) return result class TraitsUIScrolledPanel(wx.lib.scrolledpanel.ScrolledPanel): def __init__( self, parent, id=-1, pos=wx.DefaultPosition, size=wx.DefaultSize, style=wx.TAB_TRAVERSAL, name="scrolledpanel", ): wx.ScrolledWindow.__init__( self, parent, id, pos=pos, size=size, style=style, name=name ) # FIXME: The ScrolledPanel class calls SetInitialSize in its __init__ # method, but for some reason, that leads to very a small window size. # Calling SetSize seems to work okay, but its not clear why # SetInitialSize does not work. self.SetSize(size) self.SetBackgroundColour(parent.GetBackgroundColour()) # Override the C++ ChildFocus event handler: ChildFocusOverride(self) def Destroy(self): from traitsui.wx.toolkit import _popEventHandlers _popEventHandlers(self, ChildFocusOverride) super().Destroy() def OnChildFocus(self, event): """Handle a ChildFocusEvent. Returns a boolean so it can be used as a library call, too. """ self.ScrollChildIntoView(self.FindFocus()) return True def ScrollChildIntoView(self, child): """Scrolls the panel such that the specified child window is in view. This method overrides the original in the base class so that nested subpanels are handled correctly. """ if child is None: return sppux, sppuy = self.GetScrollPixelsPerUnit() vsx, vsy = self.GetViewStart() crx, cry, crdx, crdy = child.GetRect() subwindow = child.GetParent() while subwindow not in [self, None]: # Make sure that the descendant's position information is relative # to us, not its local parent. pwx, pwy = subwindow.GetRect()[:2] crx, cry = crx + pwx, cry + pwy subwindow = subwindow.GetParent() cr = wx.Rect(crx, cry, crdx, crdy) client_size = self.GetClientSize() new_vsx, new_vsy = -1, -1 # Is it before the left edge? if (cr.x < 0) and (sppux > 0): new_vsx = vsx + (cr.x // sppux) # Is it above the top? if (cr.y < 0) and (sppuy > 0): new_vsy = vsy + (cr.y // sppuy) # For the right and bottom edges, scroll enough to show the whole # control if possible, but if not just scroll such that the top/left # edges are still visible: # Is it past the right edge ? if (cr.right > client_size.width) and (sppux > 0): diff = (cr.right - client_size.width) // sppux if (cr.x - (diff * sppux)) > 0: new_vsx = vsx + diff + 1 else: new_vsx = vsx + (cr.x // sppux) # Is it below the bottom ? if (cr.bottom > client_size.height) and (sppuy > 0): diff = (cr.bottom - client_size.height) // sppuy if (cr.y - (diff * sppuy)) > 0: new_vsy = vsy + diff + 1 else: new_vsy = vsy + (cr.y // sppuy) # Perform the scroll if any adjustments are needed: if (new_vsx != -1) or (new_vsy != -1): self.Scroll(new_vsx, new_vsy) # ------------------------------------------------------------------------- # Initializes standard wx event handlers for a specified control and object: # ------------------------------------------------------------------------- # Standard wx event handlers: handlers = ( (wx.EVT_ERASE_BACKGROUND, "_erase_background"), (wx.EVT_PAINT, "_paint"), (wx.EVT_SIZE, "_size"), (wx.EVT_LEFT_DOWN, "_left_down"), (wx.EVT_LEFT_UP, "_left_up"), (wx.EVT_LEFT_DCLICK, "_left_dclick"), (wx.EVT_MIDDLE_DOWN, "_middle_down"), (wx.EVT_MIDDLE_UP, "_middle_up"), (wx.EVT_MIDDLE_DCLICK, "_middle_dclick"), (wx.EVT_RIGHT_DOWN, "_right_down"), (wx.EVT_RIGHT_UP, "_right_up"), (wx.EVT_RIGHT_DCLICK, "_right_dclick"), (wx.EVT_MOTION, "_motion"), (wx.EVT_ENTER_WINDOW, "_enter"), (wx.EVT_LEAVE_WINDOW, "_leave"), (wx.EVT_MOUSEWHEEL, "_wheel"), ) def init_wx_handlers(control, object, prefix=""): """Initializes a standard set of wx event handlers for a specified control and object using a specified prefix. """ global handlers for handler, name in handlers: method = getattr(object, prefix + name, None) if method is not None: handler(control, method) class GroupEditor(Editor): def __init__(self, **traits): """Initializes the object.""" self.trait_set(**traits) class PopupControl(HasPrivateTraits): # -- Constructor Traits --------------------------------------------------- #: The control the popup should be positioned relative to: control = Instance(wx.Window) #: The minimum width of the popup: width = Int() #: The minimum height of the popup: height = Int() #: Should the popup be resizable? resizable = Bool(False) # -- Public Traits -------------------------------------------------------- #: The value (if any) set by the popup control: value = Any() #: Event fired when the popup control is closed: closed = Event() # -- Private Traits ------------------------------------------------------- #: The popup control: popup = Instance(wx.Window) # -- Public Methods ------------------------------------------------------- def __init__(self, **traits): """Initializes the object.""" super().__init__(**traits) style = wx.SIMPLE_BORDER if self.resizable: style = wx.RESIZE_BORDER self.popup = popup = wx.Frame(None, -1, "", style=style) popup.Bind(wx.EVT_ACTIVATE, self._on_close_popup) self.create_control(popup) self._position_control() popup.Show() def create_control(self): """Creates the control. Must be overridden by a subclass. """ raise NotImplementedError def dispose(self): """Called when the popup is being closed to allow any custom clean-up. Can be overridden by a subclass. """ pass # -- Event Handlers ------------------------------------------------------- def _value_changed(self, value): """Handles the 'value' being changed.""" do_later(self._close_popup) # -- Private Methods ------------------------------------------------------ def _position_control(self): """Initializes the popup control's initial position and size.""" # Calculate the desired size of the popup control: px, cy = self.control.ClientToScreen(0, 0) cdx, cdy = self.control.GetSize() pdx, pdy = self.popup.GetSize() pdx, pdy = max(pdx, cdx, self.width), max(pdy, self.height) # Calculate the best position and size for the pop-up: py = cy + cdy if (py + pdy) > SystemMetrics().screen_height: if (cy - pdy) < 0: bottom = SystemMetrics().screen_height - py if cy > bottom: py, pdy = 0, cy else: pdy = bottom else: py = cy - pdy # Finally, position the popup control: self.popup.SetSize(px, py, pdx, pdy) def _on_close_popup(self, event): """Closes the popup control when it is deactivated.""" if not event.GetActive(): self._close_popup() def _close_popup(self): """Closes the dialog.""" self.popup.Unbind(wx.EVT_ACTIVATE) self.dispose() self.closed = True self.popup.Destroy() self.popup = self.control = None class BufferDC(wx.MemoryDC): """An off-screen buffer class. This class implements a off-screen output buffer. Data is meant to be drawn in the buffer and then blitted directly to the output device context. """ def __init__(self, dc, width=None, height=None): """Initializes the buffer.""" wx.MemoryDC.__init__(self) # If only a single argument is passed, it is assumed to be a wx.Window # and that we have been created within a 'paint' event for that window: if width is None: width, height = dc.GetClientSize() dc = wx.PaintDC(dc) self.dc = dc self.bitmap = wx.Bitmap(width, height) self.SelectObject(self.bitmap) self.SetFont(dc.GetFont()) def copy(self, x=0, y=0): """Performs the blit of the buffer contents to the specified device context location. """ self.dc.Blit( x, y, self.bitmap.GetWidth(), self.bitmap.GetHeight(), self, 0, 0 ) class Slider(wx.Slider): """This is a 'fixed' version of the wx.Slider control which does not erase its background, which can cause a lot of update flicker and is completely unnecessary. """ def __init__(self, *args, **kw): super().__init__(*args, **kw) self.Bind(wx.EVT_ERASE_BACKGROUND, self._erase_background) def _erase_background(self, event): pass def Destroy(self): self.Unbind(wx.EVT_ERASE_BACKGROUND) super().Destroy() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/wx/history_control.py0000644000175100001730000001340300000000000022303 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Defines the a text entry field (actually a combo-box) with a drop-down list of values previously entered into the control. """ import wx from traits.api import HasPrivateTraits, Instance, Str, List, Int, Bool from pyface.timer.api import do_later from .constants import OKColor, ErrorColor # ------------------------------------------------------------------------- # 'HistoryControl' class: # ------------------------------------------------------------------------- class HistoryControl(HasPrivateTraits): #: The UI control: control = Instance(wx.Window) #: The current value of the control: value = Str() #: Should 'value' be updated on every keystroke? auto_set = Bool(False) #: The current history of the control: history = List(Str) #: The maximum number of history entries allowed: entries = Int(10) #: Is the current value valid? error = Bool(False) # -- Public Methods ------------------------------------------------------- def create_control(self, parent): """Creates the control.""" self.control = control = wx.ComboBox( parent, -1, self.value, wx.Point(0, 0), wx.Size(-1, -1), self.history, style=wx.CB_DROPDOWN, ) parent.Bind(wx.EVT_COMBOBOX, self._update_value, id=control.GetId()) control.Bind(wx.EVT_KILL_FOCUS, self._kill_focus) parent.Bind( wx.EVT_TEXT_ENTER, self._update_text_value, id=control.GetId() ) if self.auto_set: parent.Bind( wx.EVT_TEXT, self._update_value_only, id=control.GetId() ) return control def dispose(self): """Disposes of the control at the end of its life cycle.""" control, self.control = self.control, None parent = control.GetParent() parent.Bind(wx.EVT_COMBOBOX, None, id=control.GetId()) parent.Bind(wx.EVT_TEXT_ENTER, None, id=control.GetId()) control.Unbind(wx.EVT_KILL_FOCUS) def set_value(self, value): """Sets the specified value and adds it to the history.""" self._update(value) # -- Traits Event Handlers ------------------------------------------------ def _value_changed(self, value): """Handles the 'value' trait being changed.""" if not self._no_update: control = self.control if control is not None: control.SetValue(value) self._restore = False def _history_changed(self): """Handles the 'history' being changed.""" if not self._no_update: if self._first_time is None: self._first_time = False if (self.value == "") and (len(self.history) > 0): self.value = self.history[0] self._load_history(select=False) def _error_changed(self, error): """Handles the 'error' trait being changed.""" if error: self.control.SetBackgroundColour(ErrorColor) else: self.control.SetBackgroundColour(OKColor) self.control.Refresh() # -- Wx Event Handlers ---------------------------------------------------- def _update_value(self, event): """Handles the user selecting something from the drop-down list of the combobox. """ self._update(event.GetString()) def _update_value_only(self, event): """Handles the user typing into the text field in 'auto_set' mode.""" self._no_update = True self.value = event.GetString() self._no_update = False def _update_text_value(self, event, select=True): """Handles the user typing something into the text field of the combobox. """ if not self._no_update: self._update(self.control.GetValue(), select) def _kill_focus(self, event): """Handles the combobox losing focus.""" self._update_text_value(event, False) event.Skip() # -- Private Methods ------------------------------------------------------ def _update(self, value, select=True): """Updates the value and history list based on a specified value.""" self._no_update = True if value.strip() != "": history = self.history if (len(history) == 0) or (value != history[0]): if value in history: history.remove(value) history.insert(0, value) del history[self.entries :] self._load_history(value, select) self.value = value self._no_update = False def _load_history(self, restore=None, select=True): """Loads the current history list into the control.""" control = self.control control.Freeze() if restore is None: restore = control.GetValue() control.Clear() for value in self.history: control.Append(value) self._restore = True do_later(self._thaw_value, restore, select) def _thaw_value(self, restore, select): """Restores the value of the combobox control.""" control = self.control if control is not None: if self._restore: control.SetValue(restore) if select: control.SetTextSelection(0, len(restore)) control.Thaw() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/wx/history_editor.py0000644000175100001730000000672000000000000022115 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Defines a text editor which displays a text field and maintains a history of previously entered values. """ from traits.api import Any, observe from pyface.timer.api import do_later from .editor import Editor from .history_control import HistoryControl # ------------------------------------------------------------------------- # '_HistoryEditor' class: # ------------------------------------------------------------------------- class _HistoryEditor(Editor): """Simple style text editor, which displays a text field and maintains a history of previously entered values, the maximum number of which is specified by the 'entries' trait of the HistoryEditor factory. """ # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- #: The history control: history = Any() def init(self, parent): """Finishes initializing the editor by creating the underlying toolkit widget. """ self.history = history = HistoryControl( value=self.value, entries=self.factory.entries, auto_set=self.factory.auto_set, ) self.control = history.create_control(parent) self.set_tooltip() def dispose(self): """Disposes of the contents of an editor.""" self.history.dispose() self.history = None super().dispose() @observe("history:value") def _value_changed(self, event): """Handles the history object's 'value' trait being changed.""" if not self._dont_update: history = self.history try: self._dont_update = True self.value = history.value history.error = False except: history.error = True do_later(self.trait_set, _dont_update=False) def update_editor(self): """Updates the editor when the object trait changes externally to the editor. """ if not self._dont_update: self._dont_update = True self.history.value = self.value self.history.error = False self._dont_update = False def error(self, excp): """Handles an error that occurs while setting the object's trait value.""" pass # -- UI preference save/restore interface --------------------------------- def restore_prefs(self, prefs): """Restores any saved user preference information associated with the editor. """ self.history.history = prefs.get("history", [])[: self.factory.entries] def save_prefs(self): """Returns any user preference information associated with the editor.""" # If the view closed successfully, try to update the history with the # current value: if self.ui.result: self._dont_update = True self.history.set_value(self.value) self._dont_update = False return {"history": self.history.history[:]} ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/wx/html_editor.py0000644000175100001730000000663300000000000021363 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Defines the HTML "editor" for the wxPython user interface toolkit. HTML editors interpret and display HTML-formatted text, but do not modify it. """ import os.path import webbrowser import wx.html as wh from traits.api import Str from .editor import Editor class URLResolvingHtmlWindow(wh.HtmlWindow): """Overrides OnOpeningURL method of HtmlWindow to append the base URL local links. """ def __init__(self, parent, open_externally, base_url): wh.HtmlWindow.__init__(self, parent) self.open_externally = open_externally self.base_url = base_url def OnLinkClicked(self, link_info): """Handle the base url and opening in a new browser window for links.""" if self.open_externally: url = link_info.GetHref() if self.base_url and not url.startswith(("http://", "https://")): url = self.base_url + url if not url.startswith(("file://", "http://", "https://")): url = "file://" + url webbrowser.open_new(url) def OnOpeningURL(self, url_type, url): """According to the documentation, this method is supposed to be called for both images and link clicks, but it appears to only be called for image loading, hence the base url handling code in OnLinkClicked. """ if ( self.base_url and not os.path.isabs(url) and not url.startswith(("http://", "https://", self.base_url)) ): return wh.HTML_REDIRECT, self.base_url + url else: return wh.HTML_OPEN, url class SimpleEditor(Editor): """Simple style of editor for HTML, which displays interpreted HTML.""" # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- #: 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() def init(self, parent): """Finishes initializing the editor by creating the underlying toolkit widget. """ self.control = URLResolvingHtmlWindow( parent, self.factory.open_externally, self.base_url ) self.control.SetBorders(2) self.base_url = self.factory.base_url self.sync_value(self.factory.base_url_name, "base_url", "from") 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.SetPage(text) # -- Event Handlers ------------------------------------------------------- def _base_url_changed(self): url = self.base_url if not url.endswith("/"): url += "/" self.control.base_url = url self.update_editor() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/wx/image_control.py0000644000175100001730000001327400000000000021672 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Defines a wxPython ImageControl widget that is used by various trait editors to display trait values iconically. """ import wx class ImageControl(wx.Window): """A wxPython control that displays an image, which can be selected or unselected by mouse clicks. """ #: Pens used to draw the 'selection' marker: _selectedPenDark = wx.Pen( wx.SystemSettings.GetColour(wx.SYS_COLOUR_3DSHADOW), 1, wx.SOLID ) _selectedPenLight = wx.Pen( wx.SystemSettings.GetColour(wx.SYS_COLOUR_3DHIGHLIGHT), 1, wx.SOLID ) def __init__( self, parent, bitmap, selected=None, handler=None, padding=10 ): """Initializes the object.""" if bitmap is not None: size = wx.Size( bitmap.GetWidth() + padding, bitmap.GetHeight() + padding ) else: size = wx.Size(32 + padding, 32 + padding) wx.Window.__init__(self, parent, -1, size=size,) self._bitmap = bitmap self._selected = selected self._handler = handler self._mouse_over = False self._button_down = False # Set up the 'paint' event handler: self.Bind(wx.EVT_PAINT, self._on_paint) # Set up mouse event handlers: self.Bind(wx.EVT_LEFT_DOWN, self._on_left_down) self.Bind(wx.EVT_LEFT_UP, self._on_left_up) self.Bind(wx.EVT_ENTER_WINDOW, self._on_enter) self.Bind(wx.EVT_LEAVE_WINDOW, self._on_leave) def Selected(self, selected=None): """Gets or sets the selection state of the image.""" if selected is not None: selected = selected != 0 if selected != self._selected: if selected: for control in self.GetParent().GetChildren(): if ( isinstance(control, ImageControl) and control.Selected() ): control.Selected(False) break self._selected = selected self.Refresh() return self._selected def Bitmap(self, bitmap=None): """Gets or sets the bitmap image.""" if bitmap is not None: if bitmap != self._bitmap: self._bitmap = bitmap self.Refresh() else: self._bitmap = None return self._bitmap def Handler(self, handler=None): """Gets or sets the click handler.""" if handler is not None: if handler != self._handler: self._handler = handler self.Refresh() return self._handler def _on_enter(self, event=None): """Handles the mouse entering the control.""" if self._selected is not None: self._mouse_over = True self.Refresh() def _on_leave(self, event=None): """Handles the mouse leaving the control.""" if self._mouse_over: self._mouse_over = False self.Refresh() def _on_left_down(self, event=None): """Handles the user pressing the mouse button.""" if self._selected is not None: self.CaptureMouse() self._button_down = True self.Refresh() def _on_left_up(self, event=None): """Handles the user clicking the control.""" need_refresh = self._button_down if need_refresh: self.ReleaseMouse() self._button_down = False if self._selected is not None: wdx, wdy = self.GetClientSize() x = event.GetX() y = event.GetY() if (0 <= x < wdx) and (0 <= y < wdy): if self._selected != -1: self.Selected(True) elif need_refresh: self.Refresh() if self._handler is not None: wx.CallAfter(self._handler, self) return if need_refresh: self.Refresh() def _on_paint(self, event=None): """Handles the control being re-painted.""" wdc = wx.PaintDC(self) wdx, wdy = self.GetClientSize() bitmap = self._bitmap if bitmap is None: return bdx = bitmap.GetWidth() bdy = bitmap.GetHeight() wdc.DrawBitmap(bitmap, (wdx - bdx) // 2, (wdy - bdy) // 2, True) pens = [self._selectedPenLight, self._selectedPenDark] bd = self._button_down if self._mouse_over: wdc.SetBrush(wx.TRANSPARENT_BRUSH) wdc.SetPen(pens[bd]) wdc.DrawLine(0, 0, wdx, 0) wdc.DrawLine(0, 1, 0, wdy) wdc.SetPen(pens[1 - bd]) wdc.DrawLine(wdx - 1, 1, wdx - 1, wdy) wdc.DrawLine(1, wdy - 1, wdx - 1, wdy - 1) if self._selected is True: wdc.SetBrush(wx.TRANSPARENT_BRUSH) wdc.SetPen(pens[bd]) wdc.DrawLine(1, 1, wdx - 1, 1) wdc.DrawLine(1, 1, 1, wdy - 1) wdc.DrawLine(2, 2, wdx - 2, 2) wdc.DrawLine(2, 2, 2, wdy - 2) wdc.SetPen(pens[1 - bd]) wdc.DrawLine(wdx - 2, 2, wdx - 2, wdy - 1) wdc.DrawLine(2, wdy - 2, wdx - 2, wdy - 2) wdc.DrawLine(wdx - 3, 3, wdx - 3, wdy - 2) wdc.DrawLine(3, wdy - 3, wdx - 3, wdy - 3) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/wx/image_editor.py0000644000175100001730000000355100000000000021475 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Traits UI 'display only' image editor. """ from pyface.image_resource import ImageResource from traitsui.ui_traits import convert_bitmap # FIXME: ImageEditor is a proxy class defined here just for backward # compatibility. The class has been moved to the # traitsui.editors.image_editor file. from traitsui.editors.image_editor import ImageEditor from .editor import Editor from .image_control import ImageControl # ------------------------------------------------------------------------- # '_ImageEditor' class: # ------------------------------------------------------------------------- class _ImageEditor(Editor): """Traits UI 'display only' image editor.""" def init(self, parent): """Finishes initializing the editor by creating the underlying toolkit widget. """ image = self.factory.image if image is None: image = self.value if image is not None: bitmap = convert_bitmap(image) else: bitmap = None self.control = ImageControl(parent, bitmap, padding=0) self.set_tooltip() def update_editor(self): """Updates the editor when the object trait changes externally to the editor. """ if self.factory.image is None: value = self.value if value is not None: bitmap = convert_bitmap(value) else: bitmap = None self.control.Bitmap(bitmap) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/wx/image_enum_editor.py0000644000175100001730000001442100000000000022517 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Defines the various image enumeration editors for the wxPython user interface toolkit. """ import wx from traits.api import Any from .editor import Editor from .enum_editor import BaseEditor as BaseEnumEditor from .helper import bitmap_cache, position_window, TraitsUIPanel from .constants import WindowColor from .image_control import ImageControl from traitsui.wx import toolkit # ------------------------------------------------------------------------- # 'ReadonlyEditor' class: # ------------------------------------------------------------------------- class ReadonlyEditor(Editor): """Read-only style of image enumeration editor, which displays a single ImageControl, representing the object trait's value. """ def init(self, parent): """Finishes initializing the editor by creating the underlying toolkit widget. """ self.control = ImageControl( parent, bitmap_cache( "%s%s%s" % (self.factory.prefix, self.str_value, self.factory.suffix), False, self.factory._image_path, ), ) def update_editor(self): """Updates the editor when the object trait changes externally to the editor. """ self.control.Bitmap( bitmap_cache( "%s%s%s" % (self.factory.prefix, self.str_value, self.factory.suffix), False, self.factory._image_path, ) ) class SimpleEditor(ReadonlyEditor): """Simple style of image enumeration editor, which displays an ImageControl, representing the object trait's value. Clicking an image displays a dialog box for selecting an image corresponding to a different value. """ def init(self, parent): """Finishes initializing the editor by creating the underlying toolkit widget. """ super().init(parent) self.control.Selected(True) self.control.Handler(self.popup_editor) self.set_tooltip() def popup_editor(self, control): """Handles the user clicking the ImageControl to display the pop-up dialog. """ ImageEnumDialog(self) class CustomEditor(BaseEnumEditor): """Custom style of image enumeration editor, which displays a grid of ImageControls. The user can click an image to select the corresponding value. """ # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- update_handler = Any # Callback to call when any button clicked def init(self, parent): """Finishes initializing the editor by creating the underlying toolkit widget. """ super().init(parent) # Create the panel to hold the ImageControl buttons: self.control = TraitsUIPanel(parent, -1) self._create_image_grid() def rebuild_editor(self): # Clear any existing content: self.control.SetSizer(None) toolkit.destroy_children(self.control) self._create_image_grid() def _create_image_grid(self): """Populates a specified window with a grid of image buttons.""" panel = self.control # Create the main sizer: if self.factory.cols > 1: sizer = wx.GridSizer(0, self.factory.cols, 0, 0) else: sizer = wx.BoxSizer(wx.VERTICAL) # Add the set of all possible choices: factory = self.factory cur_value = self.value for name in self.names: value = self.mapping[name] control = ImageControl( panel, bitmap_cache( "%s%s%s" % (factory.prefix, name, factory.suffix), False, factory._image_path, ), value == cur_value, self.update_object, ) control.value = value sizer.Add(control, 0, wx.ALL, 2) self.set_tooltip(control) # Finish setting up the control layout: panel.SetSizerAndFit(sizer) def update_object(self, control): """Handles the user clicking on an ImageControl to set an object value.""" self.value = control.value if self.update_handler is not None: self.update_handler() def update_editor(self): """Updates the editor when the object trait changes externally to the editor. """ value = self.value for control in self.control.GetChildren(): control.Selected(value == control.value) class ImageEnumDialog(wx.Frame): """Dialog box for selecting an ImageControl""" def __init__(self, editor): """Initializes the object.""" wx.Frame.__init__(self, editor.control, -1, "", style=wx.SIMPLE_BORDER) self.SetBackgroundColour(WindowColor) self.Bind(wx.EVT_ACTIVATE, self._on_close_dialog) self._closed = False dlg_editor = CustomEditor( self, factory=editor.factory, ui=editor.ui, object=editor.object, name=editor.name, description=editor.description, update_handler=self._close_dialog, ) dlg_editor.init(self) # Wrap the dialog around the image button panel: sizer = wx.BoxSizer(wx.VERTICAL) sizer.Add(dlg_editor.control) sizer.Fit(self) # Position the dialog: position_window(self, parent=editor.control) self.Show() def _on_close_dialog(self, event): """Closes the dialog.""" if not event.GetActive(): self._close_dialog() def _close_dialog(self): """Closes the dialog.""" if not self._closed: self._closed = True self.Destroy() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/wx/image_slice.py0000644000175100001730000003777700000000000021327 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Class to aid in automatically computing the 'slice' points for a specified ImageResource and then drawing it that it can be 'stretched' to fit a larger region than the original image. """ import wx from colorsys import rgb_to_hls from numpy import reshape, fromstring, uint8 from traits.api import HasPrivateTraits, Instance, Int, List, Color, Enum, Bool from pyface.image_resource import ImageResource from .constants import WindowColor from .constants import is_mac import traitsui.wx.constants # ------------------------------------------------------------------------- # Recursively paint the parent's background if they have an associated image # slice. # ------------------------------------------------------------------------- def paint_parent(dc, window): """Recursively paint the parent's background if they have an associated image slice. """ parent = window.GetParent() slice = getattr(parent, "_image_slice", None) if slice is not None: x, y = window.GetPosition() dx, dy = parent.GetSize() slice.fill(dc, -x, -y, dx, dy) else: # Otherwise, just paint the normal window background color: dx, dy = window.GetClientSize() if is_mac and hasattr(window, "_border") and window._border: dc.SetBackgroundMode(wx.TRANSPARENT) dc.SetBrush(wx.Brush(wx.Colour(0, 0, 0, 0))) else: dc.SetBrush(wx.Brush(parent.GetBackgroundColour())) dc.SetPen(wx.TRANSPARENT_PEN) dc.DrawRectangle(0, 0, dx, dy) return slice # ------------------------------------------------------------------------- # 'ImageSlice' class: # ------------------------------------------------------------------------- class ImageSlice(HasPrivateTraits): # -- Trait Definitions ---------------------------------------------------- #: The ImageResource to be sliced and drawn: image = Instance(ImageResource) #: The minimum number of adjacent, identical rows/columns needed to identify #: a repeatable section: threshold = Int(10) #: The maximum number of 'stretchable' rows and columns: stretch_rows = Enum(1, 2) stretch_columns = Enum(1, 2) #: Width/height of the image borders: top = Int() bottom = Int() left = Int() right = Int() #: Width/height of the extended image borders: xtop = Int() xbottom = Int() xleft = Int() xright = Int() #: The color to use for content text: content_color = Instance(wx.Colour) #: The color to use for label text: label_color = Instance(wx.Colour) #: The background color of the image: bg_color = Color #: Should debugging slice lines be drawn? debug = Bool(False) # -- Private Traits ------------------------------------------------------- #: The current image's opaque bitmap: opaque_bitmap = Instance(wx.Bitmap) #: The current image's transparent bitmap: transparent_bitmap = Instance(wx.Bitmap) #: Size of the current image: dx = Int() dy = Int() #: Size of the current image's slices: dxs = List() dys = List() #: Fixed minimum size of current image: fdx = Int() fdy = Int() # -- Public Methods ------------------------------------------------------- def fill(self, dc, x, y, dx, dy, transparent=False): """'Stretch fill' the specified region of a device context with the sliced image. """ # Create the source image dc: idc = wx.MemoryDC() if transparent: idc.SelectObject(self.transparent_bitmap) else: idc.SelectObject(self.opaque_bitmap) # Set up the drawing parameters: sdx, sdy = self.dx, self.dx dxs, dys = self.dxs, self.dys tdx, tdy = dx - self.fdx, dy - self.fdy # Calculate vertical slice sizes to use for source and destination: n = len(dxs) if n == 1: pdxs = [ (0, 0), (1, max(1, tdx // 2)), (sdx - 2, sdx - 2), (1, max(1, tdx - (tdx // 2))), (0, 0), ] elif n == 3: pdxs = [ (dxs[0], dxs[0]), (dxs[1], max(0, tdx)), (0, 0), (0, 0), (dxs[2], dxs[2]), ] else: pdxs = [ (dxs[0], dxs[0]), (dxs[1], max(0, tdx // 2)), (dxs[2], dxs[2]), (dxs[3], max(0, tdx - (tdx // 2))), (dxs[4], dxs[4]), ] # Calculate horizontal slice sizes to use for source and destination: n = len(dys) if n == 1: pdys = [ (0, 0), (1, max(1, tdy // 2)), (sdy - 2, sdy - 2), (1, max(1, tdy - (tdy // 2))), (0, 0), ] elif n == 3: pdys = [ (dys[0], dys[0]), (dys[1], max(0, tdy)), (0, 0), (0, 0), (dys[2], dys[2]), ] else: pdys = [ (dys[0], dys[0]), (dys[1], max(0, tdy // 2)), (dys[2], dys[2]), (dys[3], max(0, tdy - (tdy // 2))), (dys[4], dys[4]), ] # Iterate over each cell, performing a stretch fill from the source # image to the destination window: last_x, last_y = x + dx, y + dy y0, iy0 = y, 0 for idy, wdy in pdys: if y0 >= last_y: break if wdy != 0: x0, ix0 = x, 0 for idx, wdx in pdxs: if x0 >= last_x: break if wdx != 0: self._fill( idc, ix0, iy0, idx, idy, dc, x0, y0, wdx, wdy ) x0 += wdx ix0 += idx y0 += wdy iy0 += idy if self.debug: dc.SetPen(wx.Pen(wx.RED)) dc.DrawLine(x, y + self.top, last_x, y + self.top) dc.DrawLine( x, last_y - self.bottom - 1, last_x, last_y - self.bottom - 1 ) dc.DrawLine(x + self.left, y, x + self.left, last_y) dc.DrawLine( last_x - self.right - 1, y, last_x - self.right - 1, last_y ) # -- Event Handlers ------------------------------------------------------- def _image_changed(self, image): """Handles the 'image' trait being changed.""" # Save the original bitmap as the transparent version: self.transparent_bitmap = ( bitmap ) = image.create_image().ConvertToBitmap() # Save the bitmap size information: self.dx = dx = bitmap.GetWidth() self.dy = dy = bitmap.GetHeight() # Create the opaque version of the bitmap: self.opaque_bitmap = wx.Bitmap(dx, dy) mdc2 = wx.MemoryDC() mdc2.SelectObject(self.opaque_bitmap) mdc2.SetBrush(wx.Brush(WindowColor)) mdc2.SetPen(wx.TRANSPARENT_PEN) mdc2.DrawRectangle(0, 0, dx, dy) mdc = wx.MemoryDC() mdc.SelectObject(bitmap) mdc2.Blit(0, 0, dx, dy, mdc, 0, 0, useMask=True) mdc.SelectObject(wx.NullBitmap) mdc2.SelectObject(wx.NullBitmap) # Finally, analyze the image to find out its characteristics: self._analyze_bitmap() # -- Private Methods ------------------------------------------------------ def _analyze_bitmap(self): """Analyzes the bitmap.""" # Get the image data: threshold = self.threshold bitmap = self.opaque_bitmap dx, dy = self.dx, self.dy image = bitmap.ConvertToImage() # Convert the bitmap data to a numpy array for analysis: data = reshape(image.GetData(), (dy, dx, 3)).astype(uint8) # Find the horizontal slices: matches = [] y, last = 0, dy - 1 max_diff = 0.10 * dx while y < last: y_data = data[y] for y2 in range(y + 1, dy): if abs(y_data - data[y2]).sum() > max_diff: break n = y2 - y if n >= threshold: matches.append((y, n)) y = y2 n = len(matches) if n == 0: if dy > 50: matches = [(0, dy)] else: matches = [(dy // 2, 1)] elif n > self.stretch_rows: matches.sort(key=lambda x: x[1], reverse=True) matches = matches[: self.stretch_rows] # Calculate and save the horizontal slice sizes: self.fdy, self.dys = self._calculate_dxy(dy, matches) # Find the vertical slices: matches = [] x, last = 0, dx - 1 max_diff = 0.10 * dy while x < last: x_data = data[:, x] for x2 in range(x + 1, dx): if abs(x_data - data[:, x2]).sum() > max_diff: break n = x2 - x if n >= threshold: matches.append((x, n)) x = x2 n = len(matches) if n == 0: if dx > 50: matches = [(0, dx)] else: matches = [(dx // 2, 1)] elif n > self.stretch_columns: matches.sort(key=lambda x: x[1], reverse=True) matches = matches[: self.stretch_columns] # Calculate and save the vertical slice sizes: self.fdx, self.dxs = self._calculate_dxy(dx, matches) # Save the border size information: self.top = min(dy // 2, self.dys[0]) self.bottom = min(dy // 2, self.dys[-1]) self.left = min(dx // 2, self.dxs[0]) self.right = min(dx // 2, self.dxs[-1]) # Find the optimal size for the borders (i.e. xleft, xright, ... ): self._find_best_borders(data) # Save the background color: x, y = (dx // 2), (dy // 2) r, g, b = data[y, x] self.bg_color = (0x10000 * r) + (0x100 * g) + b # Find the best contrasting text color (black or white): self.content_color = self._find_best_color(data, x, y) # Find the best contrasting label color: if self.xtop >= self.xbottom: self.label_color = self._find_best_color(data, x, self.xtop // 2) else: self.label_color = self._find_best_color( data, x, dy - (self.xbottom // 2) - 1 ) def _fill(self, idc, ix, iy, idx, idy, dc, x, y, dx, dy): """Performs a stretch fill of a region of an image into a region of a window device context. """ last_x, last_y = x + dx, y + dy while y < last_y: ddy = min(idy, last_y - y) x0 = x while x0 < last_x: ddx = min(idx, last_x - x0) dc.Blit(x0, y, ddx, ddy, idc, ix, iy, useMask=True) x0 += ddx y += ddy def _calculate_dxy(self, d, matches): """Calculate the size of all image slices for a specified set of matches. """ if len(matches) == 1: d1, d2 = matches[0] return (d - d2, [d1, d2, d - d1 - d2]) d1, d2 = matches[0] d3, d4 = matches[1] return (d - d2 - d4, [d1, d2, d3 - d1 - d2, d4, d - d3 - d4]) def _find_best_borders(self, data): """Find the best set of image slice border sizes (e.g. for images with rounded corners, there should exist a better set of borders than the ones computed by the image slice algorithm. """ # Make sure the image size is worth bothering about: dx, dy = self.dx, self.dy if (dx < 5) or (dy < 5): return # Calculate the starting point: left = right = dx // 2 top = bottom = dy // 2 # Calculate the end points: last_y = dy - 1 last_x = dx - 1 # Mark which edges as 'scanning': t = b = l = r = True # Keep looping while at last one edge is still 'scanning': while l or r or t or b: # Calculate the current core area size: height = bottom - top + 1 width = right - left + 1 # Try to extend all edges that are still 'scanning': nl = ( l and (left > 0) and self._is_equal(data, left - 1, top, left, top, 1, height) ) nr = ( r and (right < last_x) and self._is_equal(data, right + 1, top, right, top, 1, height) ) nt = ( t and (top > 0) and self._is_equal(data, left, top - 1, left, top, width, 1) ) nb = ( b and (bottom < last_y) and self._is_equal( data, left, bottom + 1, left, bottom, width, 1 ) ) # Now check the corners of the edges: tl = ( (not nl) or (not nt) or self._is_equal(data, left - 1, top - 1, left, top, 1, 1) ) tr = ( (not nr) or (not nt) or self._is_equal(data, right + 1, top - 1, right, top, 1, 1) ) bl = ( (not nl) or (not nb) or self._is_equal( data, left - 1, bottom + 1, left, bottom, 1, 1 ) ) br = ( (not nr) or (not nb) or self._is_equal( data, right + 1, bottom + 1, right, bottom, 1, 1 ) ) # Calculate the new edge 'scanning' values: l = nl and tl and bl r = nr and tr and br t = nt and tl and tr b = nb and bl and br # Adjust the coordinate of an edge if it is still 'scanning': left -= l right += r top -= t bottom += b # Now compute the best set of image border sizes using the current set # and the ones we just calculated: self.xleft = min(self.left, left) self.xright = min(self.right, dx - right - 1) self.xtop = min(self.top, top) self.xbottom = min(self.bottom, dy - bottom - 1) def _find_best_color(self, data, x, y): """Find the best contrasting text color for a specified pixel coordinate. """ r, g, b = data[y, x] h, l, s = rgb_to_hls(r / 255.0, g / 255.0, b / 255.0) text_color = wx.Colour(wx.BLACK) if l < 0.50: text_color = wx.Colour(wx.WHITE) return text_color def _is_equal(self, data, x0, y0, x1, y1, dx, dy): """Determines if two identically sized regions of an image array are 'the same' (i.e. within some slight color variance of each other). """ return ( abs( data[y0 : y0 + dy, x0 : x0 + dx] - data[y1 : y1 + dy, x1 : x1 + dx] ).sum() < 0.10 * dx * dy ) # ------------------------------------------------------------------------- # Returns a (possibly cached) ImageSlice: # ------------------------------------------------------------------------- image_slice_cache = {} def image_slice_for(image): """Returns a (possibly cached) ImageSlice.""" global image_slice_cache result = image_slice_cache.get(image) if result is None: image_slice_cache[image] = result = ImageSlice(image=image) return result ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1684768028.1278067 traitsui-8.0.0/traitsui/wx/images/0000755000175100001730000000000000000000000017734 5ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/wx/images/cb_hover_off.png0000644000175100001730000000045100000000000023063 0ustar00runnerdocker00000000000000PNG  IHDR* pHYsodIDATx@srENQSS%% ]D]~q|!O~'u42po Q ++@iJLC=!5(nd 1 qLJɲnd{Jƭ7o] V[tEXtAuthorUlead Systems, Inc.>vIENDB`././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/wx/images/cb_hover_on.png0000644000175100001730000000070400000000000022726 0ustar00runnerdocker00000000000000PNG  IHDR* pHYsodPIDATxKAǷ1Puk@{ܩ?b"(mJ̩tYe[Rt-("*Ja:ĎKEq?bRf^̼8é@jM*_3/ݺH$QE,Q>)-ŸM4xRv Ƹ}\o M(`d}nۃ40l?4fZhtJ[Shuu*TtvB󳺫ǝ?Ԯo9.5ueHkΥ\UGca0D$dQу#*3@7!snQу,7Rwb<-QvXW_1$tEXtAuthorUlead Systems, Inc.>vIENDB`././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/wx/images/cb_off.png0000644000175100001730000000033300000000000021657 0ustar00runnerdocker00000000000000PNG  IHDR* pHYsodgIDATxI@@@Ϝ !k0=i}nu.cA0A"HrdlF٠nt|cXs#|*ߴʷm;|*ywݬ=O/)lm~.tEXtAuthorUlead Systems, Inc.>vIENDB`././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/wx/images/cb_on.png0000644000175100001730000000072100000000000021522 0ustar00runnerdocker00000000000000PNG  IHDR* pHYsod]IDATxϻJAU, ,ʇAP$6jBP+D74 as1lXb00;#\Z m T ]ptɾZgI+IZ:,Wzdt0M蟈@⏊< 3xg&67m"<͇_cL>YQAotDrOWdwXqJY>Ϛ,Śpb nMqxHX-!]e:K,&N1џFǽB5 ȩi_.G(C=k,0m|A!}>ŰtEXtAuthorUlead Systems, Inc.>vIENDB`././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/wx/images/file.png0000644000175100001730000000557400000000000021374 0ustar00runnerdocker00000000000000PNG  IHDR pHYs  ~ OiCCPPhotoshop ICC profilexڝSgTS=BKKoR RB&*! J!QEEȠQ, !{kּ> H3Q5 B.@ $pd!s#~<<+"x M0B\t8K@zB@F&S`cbP-`'{[! eDh;VEX0fK9-0IWfH  0Q){`##xFW<+*x<$9E[-qWW.(I+6aa@.y24x6_-"bbϫp@t~,/;m%h^ uf@Wp~<5j>{-]cK'Xto(hw?G%fIq^D$.Tʳ?D*A, `6B$BB dr`)B(Ͱ*`/@4Qhp.U=pa( Aa!ڈbX#!H$ ɈQ"K5H1RT UH=r9\F;2G1Q= C7F dt1r=6Ыhڏ>C03l0.B8, c˱" VcϱwE 6wB aAHXLXNH $4 7 Q'"K&b21XH,#/{C7$C2'ITFnR#,4H#dk9, +ȅ3![ b@qS(RjJ4e2AURݨT5ZBRQ4u9̓IKhhitݕNWGw Ljg(gwLӋT071oUX**| J&*/Tު UUT^S}FU3S ԖUPSSg;goT?~YYLOCQ_ cx,!k u5&|v*=9C3J3WRf?qtN (~))4L1e\kXHQG6EYAJ'\'GgSSݧ M=:.kDwn^Loy}/TmG X $ <5qo</QC]@Caaᄑ.ȽJtq]zۯ6iܟ4)Y3sCQ? 0k߬~OCOg#/c/Wװwa>>r><72Y_7ȷOo_C#dz%gA[z|!?:eAAA!h쐭!ΑiP~aa~ 'W?pX15wCsDDDޛg1O9-J5*>.j<74?.fYXXIlK9.*6nl {/]py.,:@LN8A*%w% yg"/6шC\*NH*Mz쑼5y$3,幄'L Lݛ:v m2=:1qB!Mggfvˬen/kY- BTZ(*geWf͉9+̳ې7ᒶKW-X潬j9(xoʿܔĹdff-[n ڴ VE/(ۻCɾUUMfeI?m]Nmq#׹=TR+Gw- 6 U#pDy  :v{vg/jBFS[b[O>zG499?rCd&ˮ/~јѡ򗓿m|x31^VwwO| (hSЧc3- cHRMz%u0`:o_FIDATxڄ ?]ԭA{e$񦛬f䆣+eS`]\y '@`P!f {"% :OfOJ GxfH{O$$ن`H띟"WkI=2|ci`#+PQzis5[;zGIENDB`././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/wx/images/frame.ico0000644000175100001730000000217600000000000021530 0ustar00runnerdocker00000000000000h(     |/ 6!:$E+9$) c   F#_<domlkhH*s\ }  e{Ʃ{Ûz?2  \) d1(J8C#ТвֻտѼʲ˲ʧR12K@U7|J=UDY9͞ھѽǸζʪN/8?WH\TgMhQ΢ɸƵĞ*t6Rfʫ̿gdS¯dzthmzgieUXȴcH< 5PaƮs`VnczH/))$DqzЦ1;r8Fѡ.8\(m7mfnLx6}/~WX wJ/ 38ˆ #$WZ`-3C?N-4022125CHP @!7[P    d/MB`DdOo  dL1g7W Ok'Uo!SoGe\8PA[9U#;($KN&(T././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/wx/images/group.png0000644000175100001730000000102500000000000021574 0ustar00runnerdocker00000000000000PNG  IHDRagAMA7tEXtSoftwareAdobe ImageReadyqe<IDATxbd```, ־Rc@15址 YH'^{ "(5h_d@~aÚU @@Pkn#. 2 ~+}  o{S,f+@A x?00D3 #aj'@[v + 5>Bbh H(j;`ÀBFpXq R@ 6( ]qD q? P%K-` H @l@,D@|,chvaPb 4r@2s^IENDB`././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/wx/images/hs_color_map.png0000644000175100001730000001173200000000000023113 0ustar00runnerdocker00000000000000PNG  IHDRdL\sBITOIDATxݝ뒳7_S ({yyҟA'~M!-t(!hbO CRdŠ$I CU}K'1AHK-@ 3br"%nou"3$1ָ aHTgc$P]"^E0ލ֔0FoNI1e 0 qzk b -l|$bb2GX-aõX~&]I5WgIai\PdZwR(*'f1B go=ة?1# }EcHG}C`1*}mnT/7ƱRBh|gU()4L(XtP#|Qڭa F(\vǢ>d3LS[Njd1IyeOtdtne=܀%PDD vN |2mqIgxN1O aK: >ye[%e3_KY"=bЏ7՗T%{\j؂X}.c[4?}_&!5Yҙo]v)# R].yJrg cuQb0ФLfMNQH:k7ETIbyWG)#XSSekH/NɚwwyG]7i{=BrZ'iXPV5_°j?Ap4Zv&Sw7\"Q b\'Jan4٧-@O@Y%/܏EIX^`n0$TXr]9 s~!"e.QHR4š$;ØR}&Z g)1my{>YPpCTP6e|hOBh7n KRYT*,aWs7`1 b/v6Pآ'pФd!k -=У&uD>T I4FǶYK?$:2J$S? ſ̍~g!i٬5wh|Rd۞6ٺ> essk(Q4* =˾@lC%< U<#>kq d xF}T&t7pVG@a_Uw7 +oh: `Ԭ7Jx\b{P͟ }B0U^5Ohf B0 5֔THo\+LN U9sHطLu,a~!F}: rq*<>rX4`"5.1~ 7WzF? 25T,<~~ K~Q+&4e+LIKh <4P3$O{#0i68V Q֝JUq/\!I)E{uIT_bq,Š ӘAxWǥqXy#m;0-/!UFYXQ1$ İ7!)+Sz72ь̑߂@#-Z;iuH- ?LlIFu,Or*Io Z,ԨgM'aҽJ8x3icP#7<"}3) if/.סKPVz0x6m! ]!(P5)5 %(*[4b8V LJξbUqb:n, hze|Jdy_KP=Z'$=%y90IuJic ֊~n=TJW%5IZaMV aw Ɠ0CrShzD?+lz[|F`ê_%OqPIȂ嘟"r7 V(ʥ+q`xMo9 ,i=YM ( e:yc Ќ>4b%2iW{R *5ctOj!ă 1m i(&^9 r,@ ѻ&y l&4>RU]R=E*LaWz*v{IǷy+k3qwr;X=+4m/`<"cM>IM_錼2OASs4WD{?>b L5 Fd+ ~ITj ~_ $0ɫQft`|dRR$}]La /iJ |v bGѓFE=UwP\f'8C{Úg}, LijʑhE0cAнqwMQLus 0A#Ƴ$&4{2 'cX 1vqW$XPYc8HH~(a j aMAX]SJcEIb41rĉ'( an") O0$iS/PA>t%0@\E3$ 7tL ICD+ bdUYP@yqP 3}st{ai4ܒ0z`׷ Peiven%)Ko33^ռitfJR(Ǣ={Xt KZ|Xx#`-8yhO|'?wGQp_p,}X|Ħ7XWr~ttܕ ߩ-ğ@f1+!0?eA}SOjO%V:(((Y$ڶ_TΌYMs-iAFL9Є70)! D}TX*fE?H( se&e^vdFQ'k_}I*3fyGY6K+꾩2 UV-V4PŜ?">$6mE6ߤTaZ!Bŋ6\<Tx)<^a(E#^kh^VqI!TWPTrK[Fk/1"AS\@ͪtUJ01"kng[LW9錺$mI~[*5@hob' ƶ?-dIENDB`././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/wx/images/item.png0000644000175100001730000000620500000000000021403 0ustar00runnerdocker00000000000000PNG  IHDRa pHYs   OiCCPPhotoshop ICC profilexڝSgTS=BKKoR RB&*! J!QEEȠQ, !{kּ> H3Q5 B.@ $pd!s#~<<+"x M0B\t8K@zB@F&S`cbP-`'{[! eDh;VEX0fK9-0IWfH  0Q){`##xFW<+*x<$9E[-qWW.(I+6aa@.y24x6_-"bbϫp@t~,/;m%h^ uf@Wp~<5j>{-]cK'Xto(hw?G%fIq^D$.Tʳ?D*A, `6B$BB dr`)B(Ͱ*`/@4Qhp.U=pa( Aa!ڈbX#!H$ ɈQ"K5H1RT UH=r9\F;2G1Q= C7F dt1r=6Ыhڏ>C03l0.B8, c˱" VcϱwE 6wB aAHXLXNH $4 7 Q'"K&b21XH,#/{C7$C2'ITFnR#,4H#dk9, +ȅ3![ b@qS(RjJ4e2AURݨT5ZBRQ4u9̓IKhhitݕNWGw Ljg(gwLӋT071oUX**| J&*/Tު UUT^S}FU3S ԖUPSSg;goT?~YYLOCQ_ cx,!k u5&|v*=9C3J3WRf?qtN (~))4L1e\kXHQG6EYAJ'\'GgSSݧ M=:.kDwn^Loy}/TmG X $ <5qo</QC]@Caaᄑ.ȽJtq]zۯ6iܟ4)Y3sCQ? 0k߬~OCOg#/c/Wװwa>>r><72Y_7ȷOo_C#dz%gA[z|!?:eAAA!h쐭!ΑiP~aa~ 'W?pX15wCsDDDޛg1O9-J5*>.j<74?.fYXXIlK9.*6nl {/]py.,:@LN8A*%w% yg"/6шC\*NH*Mz쑼5y$3,幄'L Lݛ:v m2=:1qB!Mggfvˬen/kY- BTZ(*geWf͉9+̳ې7ᒶKW-X潬j9(xoʿܔĹdff-[n ڴ VE/(ۻCɾUUMfeI?m]Nmq#׹=TR+Gw- 6 U#pDy  :v{vg/jBFS[b[O>zG499?rCd&ˮ/~јѡ򗓿m|x31^VwwO| (hSЧc3- cHRMz%u0`:o_FIDATxڤMkSAX(M]7Rhb*RAэ(Z1!b] jE3~̙9.Zj.8 üUQENuF, cp8DAD! "4M*"n[txeYFj8vc ^^AA{{srz,˘f=$sJ[k=֢x>v\.FnoXxzIENDB`././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/wx/images/nb_open.png0000644000175100001730000000425400000000000022067 0ustar00runnerdocker00000000000000PNG  IHDRfciu pHYs  ~^IDATxSSgqfiUݮutD.- zAPTڦhm,-j&"Hf#/B"*Xksf>/ 3I ǏU?zP&>|TQ7ç@ ξqP()c#^BoT  u${NEJNI%MAvp^oGAk AEQϿTȫ*Ȯl;HR)P2iIuQE;^E!_&2)c)OI(8 '!8(#:TrOfl(&)AVEFw朜o"rPP d{Sl9G{@T,y}oo=E! }RȍY OJ5),@^6TJaSilu>m6s„B}J BQ5l9'KqJGI+qK*m=bv Z8Ib5VZF"SS |KU=|ODљ ER-pԢ!RI!1ZXyWa"[g{Jg2ʼZ8[47HmcŠ l:}쩵GȮ}"E^,&$BŽY#uKaRl/fG3ȴE&)V2Z({wG<~$8gh(l7AZ{%9X ӛXX˒eO #ߓaaEӳ02̂?1ru(0녛Gw>u[bU,VH'eg/JX"hhhvmc߲4H+Mq6퉼pF`Aٴl2k%)ρfg_Xd]ɺ!)<.mq-H?>͒ezsz'#ۭ$fI Kȶg[Y튤֟Xme& |i[1Wq8(؝(i"n5Vj-ZX#q-NoTZ+Reyb-K*YLOcw<(N{9 3'gr_]b,H@<%YX+,KU~^|*;Nc1`dk5p.フJvɶ|^ӄkd,z~Յ/IoGYQn޼9p|ATw'|8ϥӉnKaJ #(FP2dA0a%J #(FP2dA0a%J #(FP2dA0a%J #(FP2dA0a%J #(FP2dA0a%J #(FP2dA0a%J #(FP2dA0a%u¼K(L +@GGxLII :NRbX,,s5/L.<~#H>Ub{?Y_ ^'<F߽ Se/IENDB`././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/wx/images/object.png0000644000175100001730000000564600000000000021723 0ustar00runnerdocker00000000000000PNG  IHDR pHYs  ~ OiCCPPhotoshop ICC profilexڝSgTS=BKKoR RB&*! J!QEEȠQ, !{kּ> H3Q5 B.@ $pd!s#~<<+"x M0B\t8K@zB@F&S`cbP-`'{[! eDh;VEX0fK9-0IWfH  0Q){`##xFW<+*x<$9E[-qWW.(I+6aa@.y24x6_-"bbϫp@t~,/;m%h^ uf@Wp~<5j>{-]cK'Xto(hw?G%fIq^D$.Tʳ?D*A, `6B$BB dr`)B(Ͱ*`/@4Qhp.U=pa( Aa!ڈbX#!H$ ɈQ"K5H1RT UH=r9\F;2G1Q= C7F dt1r=6Ыhڏ>C03l0.B8, c˱" VcϱwE 6wB aAHXLXNH $4 7 Q'"K&b21XH,#/{C7$C2'ITFnR#,4H#dk9, +ȅ3![ b@qS(RjJ4e2AURݨT5ZBRQ4u9̓IKhhitݕNWGw Ljg(gwLӋT071oUX**| J&*/Tު UUT^S}FU3S ԖUPSSg;goT?~YYLOCQ_ cx,!k u5&|v*=9C3J3WRf?qtN (~))4L1e\kXHQG6EYAJ'\'GgSSݧ M=:.kDwn^Loy}/TmG X $ <5qo</QC]@Caaᄑ.ȽJtq]zۯ6iܟ4)Y3sCQ? 0k߬~OCOg#/c/Wװwa>>r><72Y_7ȷOo_C#dz%gA[z|!?:eAAA!h쐭!ΑiP~aa~ 'W?pX15wCsDDDޛg1O9-J5*>.j<74?.fYXXIlK9.*6nl {/]py.,:@LN8A*%w% yg"/6шC\*NH*Mz쑼5y$3,幄'L Lݛ:v m2=:1qB!Mggfvˬen/kY- BTZ(*geWf͉9+̳ې7ᒶKW-X潬j9(xoʿܔĹdff-[n ڴ VE/(ۻCɾUUMfeI?m]Nmq#׹=TR+Gw- 6 U#pDy  :v{vg/jBFS[b[O>zG499?rCd&ˮ/~јѡ򗓿m|x31^VwwO| (hSЧc3- cHRMz%u0`:o_FIDATxLKq*BtPES圻" Arܠ . a(}.?oz<= + 8#Af_+$rIlTMY="{`WYIѧWR~:z͢2=bzg?#s .` 3áXGIENDB`././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/wx/images/open.png0000644000175100001730000000115100000000000021401 0ustar00runnerdocker00000000000000PNG  IHDRagAMA7tEXtSoftwareAdobe ImageReadyqe<IDATxbd```.ˀJ>h@ ? qs1 31#~I0r ܊@2x/!W؀C@>ӿ>  q2N1H; h(#+Xُ[{`W]h3j?5ge`bv;5\ۇ p/wyW8D H~aՎ )@C?$a/ o~%H3_ 4~<?°7b? ? `Qǁ@ x3î? V z+~lalH!PHw :t @۫ D@L cĠ&D@?y(8^ `DC @L3(0IENDB`././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/wx/images/table_add.png0000644000175100001730000000065400000000000022346 0ustar00runnerdocker00000000000000PNG  IHDRabKGD pHYs  tIME g9IDAT8ŒJP\# }N!}Q\8 BAP'Xy.c t1:FZ<=ρY7g@57rabЮjn4Mkf1l%aVe`_ Q)7$ mSEq\tz^-w ~')DjE)*`o{ LSueeYsR%&pw:D   J|@qUJPJφY1QJqWy\$Iͬdx\i~|w$tt.ށۭMPCtIENDB`././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/wx/images/table_colors.png0000644000175100001730000000106600000000000023115 0ustar00runnerdocker00000000000000PNG  IHDRsO/bKGDFFh7SIDATxڥ=hSaޏ&mj?ũuQ]"u$U:ŢPP.$!&$M4%q}c 0Qyt_ƐCˬp~C|=ܬ2'X"EO1?u s91("',FQFTo[m!=9{O-xVVP89`#]mg|K0ti\|X9م;jC}t%foØ#erleUT`d h\MΨ۟#=mȞD:>`D"KňDB`? 凅ppjNKuKe]lc1/Q1O;ar,Wuc hc+Jc 8'/|LbIENDB`././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/wx/images/table_delete_synthetic.png0000644000175100001730000000063200000000000025146 0ustar00runnerdocker00000000000000PNG  IHDRabKGDOIDATxڥAKQʄ6[Edw? %iѦ /Z*[hQ7B:ZQ‘A5{-A w9LY-'嫿]\{ݣ"JP.0sp >n۶?M[Q<JN@jR VAl6Kq Z@H1]My X7,_#B5@n\tqG(A]H = 5kd2qtqrtCXd7GZ;ڱMjnwBzwjvV eFa!=MVDPM4RPj^Ki~ jZ.N f6$,lPTr.\K1;7 Ӑ(qIyJ#jUp~8~op d?lZۦ1Qm}N1O9Њ%&r0σ):&?% ~1y=W« }=W%+{rӺ۝*IENDB`././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/wx/images/table_prefs.png0000644000175100001730000000066100000000000022733 0ustar00runnerdocker00000000000000PNG  IHDRabKGDfIDATxڽKBQD \B B\jhӖ\$-]!A*9"]+mEHL<y D _wv ];rB HۗdRk1Yj hDuM~=F?H)MTU栙BvԽZn0P.IXVNDPU \o/O}ˤR)4gλÝ#l-ad'gǻk~wWLx7Q$( ŶT*ʺ QL&a{&i R6`qj JEQBNg׻zk[ B)'IENDB`././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/wx/images/table_search.png0000644000175100001730000000160200000000000023055 0ustar00runnerdocker00000000000000PNG  IHDRabKGD pHYs B(xtIME +0WIIDAT8˕oSszN{glMqsQ= "xk$^( 6c xiĘ-Jd2M&ufjRnd\G\WK[۝Nj&o|7~ e˲=$rsu ,""'O:+cY*0)خkiyoÄa4Mw p ̒v7PD<~[,0Zhyw{YQs; k%CgiPC$Q*:Z"̗uE H$*cķ `.؎Kz W6'+޵EIѪkX_ĴLrQrǰ]%gJ4Ld-ľ6C_!Қ87;"_tD"_͐-d &r6 /ݡ/PENP:N.B8:2BP''I&Vo9rw*4s'mJ.xO&|(7m  8O*5σJ2oϨ]wq(eğ2~A*ed?s9d_鈭D1ѿ)fFYJNp;ף 2R59]ֲZgUDlFZ! Cn;OKF硔BDi0Z  emVr0.@vZ~a#ҸIENDB`././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/wx/instance_editor.py0000644000175100001730000004374100000000000022224 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Defines the various instance editors for the wxPython user interface toolkit. """ import wx from pyface.wx.drag_and_drop import PythonDropTarget from traits.api import HasTraits, Instance, Property from traitsui.ui_traits import AView from traitsui.helper import user_name_for from traitsui.handler import Handler from traitsui.instance_choice import InstanceChoiceItem from . import toolkit from .editor import Editor from .constants import DropColor from .helper import TraitsUIPanel, position_window OrientationMap = { "default": None, "horizontal": wx.HORIZONTAL, "vertical": wx.VERTICAL, } # ------------------------------------------------------------------------- # 'CustomEditor' class: # ------------------------------------------------------------------------- class CustomEditor(Editor): """Custom style of editor for instances. If selection among instances is allowed, the editor displays a combo box listing instances that can be selected. If the current instance is editable, the editor displays a panel containing trait editors for all the instance's traits. """ #: Background color when an item can be dropped on the editor: ok_color = DropColor #: The orientation of the instance editor relative to the instance selector: orientation = wx.VERTICAL #: Class constant: extra = 0 # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- #: List of InstanceChoiceItem objects used by the editor items = Property() #: The maximum extra padding that should be allowed around the editor: #: (Override of the Editor base class trait) border_size = 0 #: The view to use for displaying the instance: view = AView def init(self, parent): """Finishes initializing the editor by creating the underlying toolkit widget. """ factory = self.factory if factory.name != "": self._object, self._name, self._value = self.parse_extended_name( factory.name ) # Create a panel to hold the object trait's view: if factory.editable: self.control = self._panel = parent = TraitsUIPanel(parent, -1) # Build the instance selector if needed: selectable = factory.selectable droppable = factory.droppable items = self.items for item in items: droppable |= item.is_droppable() selectable |= item.is_selectable() if selectable: self._object_cache = {} item = self.item_for(self.value) if item is not None: self._object_cache[id(item)] = self.value self._choice = choice = wx.Choice( parent, -1, wx.Point(0, 0), wx.Size(-1, -1), [] ) choice.Bind(wx.EVT_CHOICE, self.update_object, id=choice.GetId()) if droppable: self._choice.SetBackgroundColour(self.ok_color) self.set_tooltip(self._choice) if factory.name != "": self._object.on_trait_change( self.rebuild_items, self._name, dispatch="ui" ) self._object.on_trait_change( self.rebuild_items, self._name + "_items", dispatch="ui" ) factory.on_trait_change( self.rebuild_items, "values", dispatch="ui" ) factory.on_trait_change( self.rebuild_items, "values_items", dispatch="ui" ) self.rebuild_items() elif droppable: self._choice = wx.TextCtrl(parent, -1, "", style=wx.TE_READONLY) self._choice.SetBackgroundColour(self.ok_color) self.set_tooltip(self._choice) if droppable: self._choice.SetDropTarget(PythonDropTarget(self)) orientation = OrientationMap[factory.orientation] if orientation is None: orientation = self.orientation if (selectable or droppable) and factory.editable: sizer = wx.BoxSizer(orientation) sizer.Add(self._choice, self.extra, wx.EXPAND) if orientation == wx.VERTICAL: sizer.Add( wx.StaticLine(parent, -1, style=wx.LI_HORIZONTAL), 0, wx.EXPAND | wx.TOP | wx.BOTTOM, 5, ) self.create_editor(parent, sizer) parent.SetSizer(sizer) elif self.control is None: if self._choice is None: self._choice = choice = wx.Choice( parent, -1, wx.Point(0, 0), wx.Size(-1, -1), [] ) choice.Bind( wx.EVT_CHOICE, self.update_object, id=choice.GetId() ) self.control = self._choice else: sizer = wx.BoxSizer(orientation) self.create_editor(parent, sizer) parent.SetSizer(sizer) # Synchronize the 'view' to use: # fixme: A normal assignment can cause a crash (for unknown reasons) in # some cases, so we make sure that no notifications are generated: self.trait_setq(view=factory.view) self.sync_value(factory.view_name, "view", "from") def dispose(self): """Disposes of the contents of an editor.""" # Make sure we aren't hanging on to any object refs: self._object_cache = None if self._ui is not None: self._ui.dispose() choice = self._choice if choice is not None: if isinstance(choice, wx.Choice): choice.Bind(wx.EVT_CHOICE, None, id=choice.GetId()) if self._object is not None: self._object.on_trait_change( self.rebuild_items, self._name, remove=True ) self._object.on_trait_change( self.rebuild_items, self._name + "_items", remove=True ) self.factory.on_trait_change( self.rebuild_items, "values", remove=True ) self.factory.on_trait_change( self.rebuild_items, "values_items", remove=True ) super().dispose() def create_editor(self, parent, sizer): """Creates the editor control.""" self._panel = TraitsUIPanel(parent, -1) sizer.Add(self._panel, 1, wx.EXPAND) def _get_items(self): """Gets the current list of InstanceChoiceItem items.""" if self._items is not None: return self._items factory = self.factory if self._value is not None: values = self._value() + factory.values else: values = factory.values items = [] adapter = factory.adapter for value in values: if not isinstance(value, InstanceChoiceItem): value = adapter(object=value) items.append(value) self._items = items return items def rebuild_items(self): """Rebuilds the object selector list.""" # Clear the current cached values: self._items = None # Rebuild the contents of the selector list: name = None value = self.value choice = self._choice choice.Clear() for item in self.items: if item.is_selectable(): item_name = item.get_name() choice.Append(item_name) if item.is_compatible(value): name = item_name # Reselect the current item if possible: if name is not None: choice.SetStringSelection(name) else: # Otherwise, current value is no longer valid, try to discard it: try: self.value = None except: pass def item_for(self, object): """Returns the InstanceChoiceItem for a specified object.""" for item in self.items: if item.is_compatible(object): return item return None def view_for(self, object, item): """Returns the view to use for a specified object.""" view = "" if item is not None: view = item.get_view() if view == "": view = self.view return self.ui.handler.trait_view_for( self.ui.info, view, object, self.object_name, self.name ) def update_object(self, event): """Handles the user selecting a new value from the combo box.""" name = event.GetString() for item in self.items: if name == item.get_name(): id_item = id(item) object = self._object_cache.get(id_item) if object is None: object = item.get_object() if (not self.factory.editable) and item.is_factory: view = self.view_for(object, self.item_for(object)) view.ui(object, self.control, "modal") if self.factory.cachable: self._object_cache[id_item] = object self.value = object self.resynch_editor() break def update_editor(self): """Updates the editor when the object trait changes externally to the editor. """ # Attach the current object value to the control (for use by # DockWindowFeature): # fixme: This code is somewhat fragile since it assumes that if a # DockControl is involved, the parent of this editor will be the # control being managed by the DockControl. parent = self.control.GetParent() parent._object = self.value dock_control = getattr(parent, "_dock_control", None) if dock_control is not None: dock_control.reset_tab() # Synchronize the editor contents: self.resynch_editor() # Update the selector (if any): choice = self._choice item = self.item_for(self.value) if (choice is not None) and (item is not None): name = item.get_name(self.value) if self._object_cache is not None: if choice.FindString(name) < 0: choice.Append(name) choice.SetStringSelection(name) else: choice.SetValue(name) def resynch_editor(self): """Resynchronizes the contents of the editor when the object trait changes externally to the editor. """ panel = self._panel if panel is not None: # Compute/update the maximum size the panel has ever been: dx, dy = panel.GetSize() mdx = mdy = 0 if self._panel_size is not None: mdx, mdy = self._panel_size self._panel_size = size = wx.Size(max(mdx, dx), max(mdy, dy)) # Dispose of the previous contents of the panel: panel.SetSizer(None) if self._ui is not None: self._ui.dispose() self._ui = None else: for child in panel.GetChildren(): toolkit.destroy_control(child) # Create the new content for the panel: sizer = wx.BoxSizer(wx.VERTICAL) stretch = 0 value = self.value if not isinstance(value, HasTraits): str_value = "" if value is not None: str_value = self.str_value control = wx.StaticText(panel, -1, str_value) else: view = self.view_for(value, self.item_for(value)) context = value.trait_context() handler = None if isinstance(value, Handler): handler = value context.setdefault("context", self.object) context.setdefault("context_handler", self.ui.handler) self._ui = ui = view.ui( context, panel, "subpanel", value.trait_view_elements(), handler, self.factory.id, ) control = ui.control self.scrollable = ui._scrollable ui.parent = self.ui if view.resizable or view.scrollable or ui._scrollable: stretch = 1 # Make sure the panel and its contents are correctly sized (This # code is complicated by the various layout bugs present in wx. # Tamper with it at your own risk!): control.Freeze() if stretch and (size != (20, 20)): control.SetSize(size) panel.SetSize(size) else: panel.SetSize(control.GetSize()) sizer.Add(control, stretch, wx.EXPAND) panel.SetSizer(sizer) control.Thaw() self.control.Layout() parent = self.control.GetParent() parent.Layout() # It is possible that this instance editor is embedded at some level # in a ScrolledWindow. If so, we need to inform the window that the # size of the editor's contents have (potentially) changed: while (parent is not None) and ( not isinstance(parent, wx.ScrolledWindow) ): parent = parent.GetParent() if parent is not None: parent.SendSizeEvent() def error(self, excp): """Handles an error that occurs while setting the object's trait value.""" pass def get_error_control(self): """Returns the editor's control for indicating error status.""" return self._choice or self.control # -- UI preference save/restore interface --------------------------------- def restore_prefs(self, prefs): """Restores any saved user preference information associated with the editor. """ ui = self._ui if (ui is not None) and (prefs.get("id") == ui.id): ui.set_prefs(prefs.get("prefs")) def save_prefs(self): """Returns any user preference information associated with the editor.""" ui = self._ui if (ui is not None) and (ui.id != ""): return {"id": ui.id, "prefs": ui.get_prefs()} return None # -- Drag and drop event handlers ----------------------------------------- def wx_dropped_on(self, x, y, data, drag_result): """Handles a Python object being dropped on the tree.""" for item in self.items: if item.is_droppable() and item.is_compatible(data): if self._object_cache is not None: self.rebuild_items() self.value = data return drag_result return wx.DragNone def wx_drag_over(self, x, y, data, drag_result): """Handles a Python object being dragged over the tree.""" for item in self.items: if item.is_droppable() and item.is_compatible(data): return drag_result return wx.DragNone # -- Traits event handlers ------------------------------------------------ def _view_changed(self, view): self.resynch_editor() class SimpleEditor(CustomEditor): """Simple style of editor for instances, which displays a button. Clicking the button displays a dialog box in which the instance can be edited. """ #: Class constants: orientation = wx.HORIZONTAL extra = 2 #: The ui instance for the currently open editor dialog _dialog_ui = Instance("traitsui.ui.UI") def create_editor(self, parent, sizer): """Creates the editor control (a button).""" self._button = button = wx.Button(parent, -1, "") sizer.Add(button, 1, wx.EXPAND | wx.LEFT, 5) button.Bind(wx.EVT_BUTTON, self.edit_instance, id=button.GetId()) def dispose(self): """Disposes of the contents of an editor.""" button = self._button if button is not None: button.Bind(wx.EVT_BUTTON, None, id=button.GetId()) if self._dialog_ui is not None: self._dialog_ui.dispose() self._dialog_ui = None super().dispose() def edit_instance(self, event): """Edit the contents of the object trait when the user clicks the button. """ # Create the user interface: factory = self.factory view = self.ui.handler.trait_view_for( self.ui.info, factory.view, self.value, self.object_name, self.name ) ui = self.value.edit_traits( view, self.control, factory.kind, id=factory.id ) # Check to see if the view was 'modal', in which case it will already # have been closed (i.e. is None) by the time we get control back: if ui.control is not None: # Position the window on the display: position_window(ui.control) # Chain our undo history to the new user interface if it does not # have its own: if ui.history is None: ui.history = self.ui.history self._dialog_ui = ui def resynch_editor(self): """Resynchronizes the contents of the editor when the object trait changes externally to the editor. """ button = self._button if button is not None: label = self.factory.label if label == "": label = user_name_for(self.name) button.SetLabel(label) button.Enable(isinstance(self.value, HasTraits)) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/wx/key_binding_editor.py0000644000175100001730000001134700000000000022677 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Defines the key binding editor for use with the KeyBinding class. This is a specialized editor used to associate a particular key with a control (i.e., the key binding editor). """ import wx from traits.api import Bool, Event from pyface.api import YES, confirm from .editor import Editor from .key_event_to_name import key_event_to_name # ------------------------------------------------------------------------- # 'KeyBindingEditor' class: # ------------------------------------------------------------------------- class KeyBindingEditor(Editor): """An editor for modifying bindings of keys to controls.""" # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- #: Does the editor's control have focus currently? has_focus = Bool(False) #: Keyboard event key = Event() #: Clear field event clear = Event() def init(self, parent): """Finishes initializing the editor by creating the underlying toolkit widget. """ self.control = KeyBindingCtrl(self, parent, size=wx.Size(160, 19)) def update_editor(self): """Updates the editor when the object trait changes externally to the editor. """ self.control.Refresh() def _key_changed(self, event): """Handles a keyboard event.""" binding = self.object key_name = key_event_to_name(event) cur_binding = binding.owner.key_binding_for(binding, key_name) if cur_binding is not None: result = confirm( parent=self.control, message=( f"{key_name!r} has already been assigned to " f"'{cur_binding.description}'.\n" "Do you wish to continue?" ), title="Duplicate Key Definition", ) if result != YES: return self.value = key_name def _clear_changed(self): """Handles a clear field event.""" self.value = "" class KeyBindingCtrl(wx.Window): """wxPython control for editing key bindings.""" def __init__( self, editor, parent, wid=-1, pos=wx.DefaultPosition, size=wx.DefaultSize, ): super().__init__( parent, wid, pos, size, style=wx.CLIP_CHILDREN | wx.WANTS_CHARS ) # Save the reference to the controlling editor object: self.editor = editor # Indicate we don't have the focus right now: editor.has_focus = False # Set up the 'erase background' event handler: self.Bind(wx.EVT_ERASE_BACKGROUND, self._on_erase_background) # Set up the 'paint' event handler: self.Bind(wx.EVT_PAINT, self._paint) # Set up the focus change handlers: self.Bind(wx.EVT_SET_FOCUS, self._get_focus) self.Bind(wx.EVT_KILL_FOCUS, self._lose_focus) # Set up mouse event handlers: self.Bind(wx.EVT_LEFT_DOWN, self._set_focus) self.Bind(wx.EVT_LEFT_DCLICK, self._clear_contents) # Handle key events: self.Bind(wx.EVT_CHAR, self._on_char) def _on_char(self, event): """Handle keyboard keys being pressed.""" self.editor.key = event def _on_erase_background(self, event): pass def _paint(self, event): """Updates the screen.""" wdc = wx.PaintDC(self) dx, dy = self.GetSize() if self.editor.has_focus: wdc.SetPen(wx.Pen(wx.RED, 2)) wdc.DrawRectangle(1, 1, dx - 1, dy - 1) else: wdc.SetPen(wx.Pen(wx.BLACK)) wdc.DrawRectangle(0, 0, dx, dy) wdc.SetFont(self.GetFont()) wdc.DrawText(self.editor.str_value, 5, 3) def _set_focus(self, event): """Sets the keyboard focus to this window.""" self.SetFocus() def _get_focus(self, event): """Handles getting the focus.""" self.editor.has_focus = True self.Refresh() def _lose_focus(self, event): """Handles losing the focus.""" self.editor.has_focus = False self.Refresh() def _clear_contents(self, event): """Handles the user double clicking the control to clear its contents.""" self.editor.clear = True ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/wx/key_event_to_name.py0000644000175100001730000000704600000000000022543 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Converts a wx.KeyEvent to a standardized "name". """ import wx # Mapping from wxPython special key names to Enable key names key_map = { wx.WXK_BACK: "Backspace", wx.WXK_TAB: "Tab", wx.WXK_RETURN: "Enter", wx.WXK_ESCAPE: "Esc", wx.WXK_DELETE: "Delete", wx.WXK_START: "Start", wx.WXK_LBUTTON: "Left Button", wx.WXK_RBUTTON: "Right Button", wx.WXK_CANCEL: "Cancel", wx.WXK_MBUTTON: "Middle Button", wx.WXK_CLEAR: "Clear", wx.WXK_SHIFT: "Shift", wx.WXK_CONTROL: "Control", wx.WXK_MENU: "Menu", wx.WXK_PAUSE: "Pause", wx.WXK_CAPITAL: "Capital", wx.WXK_PAGEUP: "Page Up", wx.WXK_PAGEDOWN: "Page Down", wx.WXK_END: "End", wx.WXK_HOME: "Home", wx.WXK_LEFT: "Left", wx.WXK_UP: "Up", wx.WXK_RIGHT: "Right", wx.WXK_DOWN: "Down", wx.WXK_SELECT: "Select", wx.WXK_PRINT: "Print", wx.WXK_EXECUTE: "Execute", wx.WXK_SNAPSHOT: "Snapshot", wx.WXK_INSERT: "Insert", wx.WXK_HELP: "Help", wx.WXK_NUMPAD0: "Numpad 0", wx.WXK_NUMPAD1: "Numpad 1", wx.WXK_NUMPAD2: "Numpad 2", wx.WXK_NUMPAD3: "Numpad 3", wx.WXK_NUMPAD4: "Numpad 4", wx.WXK_NUMPAD5: "Numpad 5", wx.WXK_NUMPAD6: "Numpad 6", wx.WXK_NUMPAD7: "Numpad 7", wx.WXK_NUMPAD8: "Numpad 8", wx.WXK_NUMPAD9: "Numpad 9", wx.WXK_MULTIPLY: "Multiply", wx.WXK_ADD: "Add", wx.WXK_SEPARATOR: "Separator", wx.WXK_SUBTRACT: "Subtract", wx.WXK_DECIMAL: "Decimal", wx.WXK_DIVIDE: "Divide", wx.WXK_F1: "F1", wx.WXK_F2: "F2", wx.WXK_F3: "F3", wx.WXK_F4: "F4", wx.WXK_F5: "F5", wx.WXK_F6: "F6", wx.WXK_F7: "F7", wx.WXK_F8: "F8", wx.WXK_F9: "F9", wx.WXK_F10: "F10", wx.WXK_F11: "F11", wx.WXK_F12: "F12", wx.WXK_F13: "F13", wx.WXK_F14: "F14", wx.WXK_F15: "F15", wx.WXK_F16: "F16", wx.WXK_F17: "F17", wx.WXK_F18: "F18", wx.WXK_F19: "F19", wx.WXK_F20: "F20", wx.WXK_F21: "F21", wx.WXK_F22: "F22", wx.WXK_F23: "F23", wx.WXK_F24: "F24", wx.WXK_NUMLOCK: "Num Lock", wx.WXK_SCROLL: "Scroll Lock", } # ------------------------------------------------------------------------- # Converts a keystroke event into a corresponding key name: # ------------------------------------------------------------------------- def key_event_to_name(event): """Converts a keystroke event into a corresponding key name.""" key_code = event.GetKeyCode() if event.ControlDown() and (1 <= key_code <= 26): key = chr(key_code + 96) else: key = key_map.get(key_code) if key is None: try: key = chr(key_code) except: # Handle the case of strange keyboard codes (such as the Apple # keyboard 'apple' key): key = "unknown" name = "" if event.AltDown(): name = "Alt" if event.ControlDown(): name += "-Ctrl" if event.ShiftDown() and ((name != "") or (len(key) > 1)): name += "-Shift" if key == " ": key = "Space" if len(name) > 0: key = key.lower() name += "-" + key if name[:1] == "-": return name[1:] return name ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/wx/list_editor.py0000644000175100001730000006510200000000000021366 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Defines the various list editors for the wxPython user interface toolkit. """ import wx import wx.lib.scrolledpanel as wxsp from traits.api import ( Any, Bool, cached_property, Instance, Property, Str, TraitError, ) from traits.trait_base import user_name_for, xgetattr from traitsui.ui_traits import Image, convert_bitmap from traitsui.editors.list_editor import ListItemProxy, ToolkitEditorFactory from traitsui.dockable_view_element import DockableViewElement from pyface.dock.api import ( DockWindow, DockSizer, DockSection, DockRegion, DockControl, ) from . import toolkit from .constants import scrollbar_dx from .editor import Editor from .menu import MakeMenu from .image_control import ImageControl # ------------------------------------------------------------------------- # 'SimpleEditor' class: # ------------------------------------------------------------------------- class SimpleEditor(Editor): """Simple style of editor for lists, which displays a scrolling list box with only one item visible at a time. A icon next to the list box displays a menu of operations on the list. """ # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- #: The kind of editor to create for each list item kind = Str() #: Is the list of items being edited mutable? mutable = Bool() #: The image used by the editor: image = Image("list_editor") #: The bitmap used by the editor: bitmap = Property() # ------------------------------------------------------------------------- # Class constants: # ------------------------------------------------------------------------- #: Whether the list is displayed in a single row single_row = True # ------------------------------------------------------------------------- # Normal list item menu: # ------------------------------------------------------------------------- #: Menu for modifying the list list_menu = """ Add Before [_menu_before]: self.add_before() Add After [_menu_after]: self.add_after() --- Delete [_menu_delete]: self.delete_item() --- Move Up [_menu_up]: self.move_up() Move Down [_menu_down]: self.move_down() Move to Top [_menu_top]: self.move_top() Move to Bottom [_menu_bottom]: self.move_bottom() """ # ------------------------------------------------------------------------- # Empty list item menu: # ------------------------------------------------------------------------- empty_list_menu = """ Add: self.add_empty() """ def init(self, parent): """Finishes initializing the editor by creating the underlying toolkit widget. """ # Initialize the trait handler to use: trait_handler = self.factory.trait_handler if trait_handler is None: trait_handler = self.object.base_trait(self.name).handler self._trait_handler = trait_handler # Create a scrolled window to hold all of the list item controls: self.control = wxsp.ScrolledPanel(parent, -1) self.control.SetBackgroundColour(parent.GetBackgroundColour()) self.control.SetAutoLayout(True) # Remember the editor to use for each individual list item: editor = self.factory.editor if editor is None: editor = trait_handler.item_trait.get_editor() self._editor = getattr(editor, self.kind) # Set up the additional 'list items changed' event handler needed for # a list based trait. Note that we want to fire the update_editor_item # only when the items in the list change and not when intermediate # traits change. Therefore, replace "." by ":" in the extended_name # when setting up the listener. extended_name = self.extended_name.replace(".", ":") self.context_object.on_trait_change( self.update_editor_item, extended_name + "_items?", dispatch="ui" ) self.set_tooltip() def dispose(self): """Disposes of the contents of an editor.""" extended_name = self.extended_name.replace(".", ":") self.context_object.on_trait_change( self.update_editor_item, extended_name + "_items?", remove=True ) self._dispose_items() super().dispose() def update_editor(self): """Updates the editor when the object trait changes externally to the editor. """ # Disconnect the editor from any control about to be destroyed: self._dispose_items() # Get rid of any previous contents: list_pane = self.control list_pane.SetSizer(None) for child in list_pane.GetChildren(): toolkit.destroy_control(child) # Create all of the list item trait editors: trait_handler = self._trait_handler resizable = ( trait_handler.minlen != trait_handler.maxlen ) and self.mutable item_trait = trait_handler.item_trait factory = self.factory list_sizer = wx.FlexGridSizer( len(self.value), (1 + resizable) * factory.columns, 0, 0 ) j = 0 for i in range(factory.columns): list_sizer.AddGrowableCol(j) j += 1 + resizable values = self.value index = 0 width, height = 0, 0 is_fake = resizable and (values is None or len(values) == 0) if is_fake: values = [item_trait.default_value()[1]] panel_height = 0 editor = self._editor for value in values: width1 = height = 0 if resizable: control = ImageControl( list_pane, self.bitmap, -1, self.popup_menu ) width1, height = control.GetSize() width1 += 4 try: proxy = ListItemProxy( self.object, self.name, index, item_trait, value ) if resizable: control.proxy = proxy peditor = editor( self.ui, proxy, "value", self.description, list_pane ).trait_set(object_name="") peditor.prepare(list_pane) pcontrol = peditor.control pcontrol.proxy = proxy except: if not is_fake: raise pcontrol = wx.Button(list_pane, -1, "sample") pcontrol.Fit() width2, height2 = size = pcontrol.GetSize() pcontrol.SetMinSize(size) width = max(width, width1 + width2) height = max(height, height2) panel_height += height list_sizer.Add(pcontrol, 0, wx.EXPAND) if resizable: list_sizer.Add(control, 0, wx.LEFT | wx.RIGHT, 2) index += 1 list_pane.SetSizer(list_sizer) if not self.mutable: # list_sizer.SetDimension(0,0,width, panel_height) list_pane.SetInitialSize(list_sizer.GetSize()) if is_fake: self._cur_control = control self.empty_list() control.Destroy() pcontrol.Destroy() rows = 1 if not self.single_row: rows = self.factory.rows # Make sure we have valid values set for width and height (in case there # was no data to base them on): if width == 0: width = 100 if panel_height == 0: panel_height = 20 list_pane.SetMinSize( wx.Size( width + ((trait_handler.maxlen > rows) * scrollbar_dx), panel_height, ) ) list_pane.SetupScrolling() list_pane.GetParent().Layout() def update_editor_item(self, obj, name, event): """Updates the editor when an item in the object trait changes externally to the editor. """ # If this is not a simple, single item update, rebuild entire editor: if (len(event.removed) != 1) or (len(event.added) != 1): self.update_editor() return # Otherwise, find the proxy for this index and update it with the # changed value: for control in self.control.GetChildren(): proxy = control.proxy if proxy.index == event.index: proxy.value = event.added[0] break def empty_list(self): """Creates an empty list entry (so the user can add a new item).""" control = ImageControl( self.control, self.bitmap, -1, self.popup_empty_menu ) control.is_empty = True proxy = ListItemProxy(self.object, self.name, -1, None, None) pcontrol = wx.StaticText(self.control, -1, " (Empty List)") pcontrol.proxy = control.proxy = proxy self.reload_sizer([(control, pcontrol)]) def reload_sizer(self, controls, extra=0): """Reloads the layout from the specified list of ( button, proxy ) pairs. """ sizer = self.control.GetSizer() for i in range(2 * len(controls) + extra): sizer.Remove(0) index = 0 for control, pcontrol in controls: sizer.Add(pcontrol, 1, wx.EXPAND) sizer.Add(control, 0, wx.LEFT | wx.RIGHT, 2) control.proxy.index = index index += 1 sizer.Layout() self.control.SetVirtualSize(sizer.GetMinSize()) def get_info(self): """Returns the associated object list and current item index.""" proxy = self._cur_control.proxy return (proxy.list, proxy.index) def popup_empty_menu(self, control): """Displays the empty list editor popup menu.""" self._cur_control = control menu = MakeMenu(self.empty_list_menu, self, True, self.control).menu self.control.PopupMenu(menu, control.GetPosition()) menu.Destroy() def popup_menu(self, control): """Displays the list editor popup menu.""" self._cur_control = control # Makes sure that any text that was entered get's added (Pressure # #145): control.SetFocus() proxy = control.proxy index = proxy.index menu = MakeMenu(self.list_menu, self, True, self.control).menu len_list = len(proxy.list) not_full = len_list < self._trait_handler.maxlen self._menu_before.enabled(not_full) self._menu_after.enabled(not_full) self._menu_delete.enabled(len_list > self._trait_handler.minlen) self._menu_up.enabled(index > 0) self._menu_top.enabled(index > 0) self._menu_down.enabled(index < (len_list - 1)) self._menu_bottom.enabled(index < (len_list - 1)) x, y = control.GetPosition() self.control.PopupMenu(menu, (x + 8, y + 32)) menu.Destroy() def add_item(self, offset): """Adds a new value at the specified list index.""" list, index = self.get_info() index += offset item_trait = self._trait_handler.item_trait if self.factory.item_factory: value = self.factory.item_factory( *self.factory.item_factory_args, **self.factory.item_factory_kwargs, ) else: value = item_trait.default_value_for(self.object, self.name) try: self.value = list[:index] + [value] + list[index:] # if the default new item is invalid, we just don't add it to the list. # traits will still give an error message, but we don't want to crash except TraitError: from traitsui.api import raise_to_debug raise_to_debug() wx.CallAfter(self.update_editor) def add_before(self): """Inserts a new item before the current item.""" self.add_item(0) def add_after(self): """Inserts a new item after the current item.""" self.add_item(1) def add_empty(self): """Adds a new item when the list is empty.""" list, index = self.get_info() self.add_item(0) def delete_item(self): """Delete the current item.""" list, index = self.get_info() self.value = list[:index] + list[index + 1 :] wx.CallAfter(self.update_editor) def move_up(self): """Move the current item up one in the list.""" list, index = self.get_info() self.value = ( list[: index - 1] + [list[index], list[index - 1]] + list[index + 1 :] ) wx.CallAfter(self.update_editor) def move_down(self): """Moves the current item down one in the list.""" list, index = self.get_info() self.value = ( list[:index] + [list[index + 1], list[index]] + list[index + 2 :] ) wx.CallAfter(self.update_editor) def move_top(self): """Moves the current item to the top of the list.""" list, index = self.get_info() self.value = [list[index]] + list[:index] + list[index + 1 :] wx.CallAfter(self.update_editor) def move_bottom(self): """Moves the current item to the bottom of the list.""" list, index = self.get_info() self.value = list[:index] + list[index + 1 :] + [list[index]] wx.CallAfter(self.update_editor) # -- Property Implementations --------------------------------------------- @cached_property def _get_bitmap(self): return convert_bitmap(self.image) # -- Private Methods ------------------------------------------------------ def _dispose_items(self): """Disposes of each current list item.""" for control in self.control.GetChildren(): editor = getattr(control, "_editor", None) if editor is not None: try: editor.dispose() except Exception: pass editor.control = None # -- Trait initializers ---------------------------------------------------- def _kind_default(self): """Returns a default value for the 'kind' trait.""" return self.factory.style + "_editor" def _mutable_default(self): """Trait handler to set the mutable trait from the factory.""" return self.factory.mutable class CustomEditor(SimpleEditor): """Custom style of editor for lists, which displays the items as a series of text fields. If the list is editable, an icon next to each item displays a menu of operations on the list. """ # ------------------------------------------------------------------------- # Class constants: # ------------------------------------------------------------------------- #: Whether the list is displayed in a single row. This value overrides the #: default. single_row = False # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- #: Is the list editor is scrollable? This values overrides the default. scrollable = True # ------------------------------------------------------------------------- # 'TextEditor' class: # ------------------------------------------------------------------------- class TextEditor(CustomEditor): #: The kind of editor to create for each list item. This value overrides the #: default. kind = "text_editor" class ReadonlyEditor(CustomEditor): #: Is the list of items being edited mutable? This value overrides the #: default. mutable = False class NotebookEditor(Editor): """An editor for lists that displays the list as a "notebook" of tabbed pages. """ # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- #: Is the notebook editor scrollable? This values overrides the default: scrollable = True #: The currently selected notebook page object: selected = Any() def init(self, parent): """Finishes initializing the editor by creating the underlying toolkit widget. """ self._uis = [] # Create a DockWindow to hold each separate object's view: theme = self.factory.dock_theme or self.item.container.dock_theme dw = DockWindow(parent, theme=theme) self.control = dw.control self._sizer = DockSizer(DockSection(dock_window=dw)) self.control.SetSizer(self._sizer) # Set up the additional 'list items changed' event handler needed for # a list based trait. Note that we want to fire the update_editor_item # only when the items in the list change and not when intermediate # traits change. Therefore, replace "." by ":" in the extended_name # when setting up the listener. extended_name = self.extended_name.replace(".", ":") self.context_object.on_trait_change( self.update_editor_item, extended_name + "_items?", dispatch="ui" ) # Set of selection synchronization: self.sync_value(self.factory.selected, "selected") def update_editor(self): """Updates the editor when the object trait changes externally to the editor. """ # Make sure the DockWindow is in a correct state: self._sizer.Reset(self.control) # Destroy the views on each current notebook page: self.close_all() # Create a DockControl for each object in the trait's value: uis = self._uis dock_controls = [] for object in self.value: dock_control, view_object, monitoring = self._create_page(object) # Remember the DockControl for later deletion processing: uis.append([dock_control, object, view_object, monitoring]) dock_controls.append(dock_control) if len(uis) == 1: dock_control.dockable.dockable_tab_activated(dock_control, True) # Add the new items to the DockWindow: self.add_controls(dock_controls) if self.ui.info.initialized: self.update_layout() def update_editor_item(self, event): """Handles an update to some subset of the trait's list.""" # Make sure the DockWindow is in a correct state: self._sizer.Reset(self.control) index = event.index # Delete the page corresponding to each removed item: layout = (len(event.removed) + len(event.added)) <= 1 for i in range(len(event.removed)): dock_control, object, view_object, monitoring = self._uis[index] if monitoring: view_object.on_trait_change( self.update_page_name, self.factory.page_name[1:], remove=True, ) dock_control.close(layout=layout, force=True) del self._uis[index] # Add a page for each added object: dock_controls = [] for object in event.added: dock_control, view_object, monitoring = self._create_page(object) self._uis[index:index] = [ [dock_control, object, view_object, monitoring] ] dock_controls.append(dock_control) index += 1 # Add the new items to the DockWindow: self.add_controls(dock_controls) self.update_layout() def close_all(self): """Closes all currently open notebook pages.""" page_name = self.factory.page_name[1:] for dock_control, object, view_object, monitoring in self._uis: if monitoring: view_object.on_trait_change( self.update_page_name, page_name, remove=True ) dock_control.close(layout=False, force=True) # Reset the list of ui's and dictionary of page name counts: self._uis = [] self._pages = {} def dispose(self): """Disposes of the contents of an editor.""" self.context_object.on_trait_change( self.update_editor_item, self.name + "_items?", remove=True ) self.close_all() super().dispose() def add_controls(self, controls): """Adds a group of new DockControls to the view.""" if len(controls) > 0: section = self.control.GetSizer().GetContents() if (len(section.contents) == 0) or ( not isinstance(section.contents[-1], DockRegion) ): section.contents.append(DockRegion(contents=controls)) else: for control in controls: section.contents[-1].add(control, activate=False) # Fire this event to activate the dock control corresponding # to the selected object, if any. self._selected_changed(None, self.selected) def update_layout(self): """Updates the layout of the DockWindow.""" self.control.Layout() self.control.Refresh() def update_page_name(self): """Handles the trait defining a particular page's name being changed.""" changed = False for i, value in enumerate(self._uis): dock_control, user_object, view_object, monitoring = value if dock_control.control is not None: name = None handler = getattr( self.ui.handler, "%s_%s_page_name" % (self.object_name, self.name), None, ) if handler is not None: name = handler(self.ui.info, user_object) if name is None: name = str( xgetattr( view_object, self.factory.page_name[1:], "???" ) ) changed |= dock_control.name != name dock_control.name = name if changed: self.update_layout() def _create_page(self, object): """Creates a DockControl for a specified object.""" # Create the view for the object: view_object = object factory = self.factory if factory.factory is not None: view_object = factory.factory(object) ui = view_object.edit_traits( parent=self.control, view=factory.view, kind=factory.ui_kind ).trait_set(parent=self.ui) # Get the name of the page being added to the notebook: name = "" monitoring = False prefix = "%s_%s_page_" % (self.object_name, self.name) page_name = self.factory.page_name if page_name[0:1] == ".": name = xgetattr(view_object, page_name[1:], None) monitoring = name is not None if monitoring: handler_name = None method = getattr(self.ui.handler, prefix + "name", None) if method is not None: handler_name = method(self.ui.info, object) if handler_name is not None: name = handler_name else: name = str(name) or "???" view_object.on_trait_change( self.update_page_name, page_name[1:], dispatch="ui" ) else: name = "" elif page_name != "": name = page_name if name == "": name = user_name_for(view_object.__class__.__name__) # Make sure the name is not a duplicate: if not monitoring: self._pages[name] = count = self._pages.get(name, 0) + 1 if count > 1: name += " %d" % count # Return a new DockControl for the ui, and whether or not its name is # being monitored: image = None method = getattr(self.ui.handler, prefix + "image", None) if method is not None: image = method(self.ui.info, object) dock_control = DockControl( control=ui.control, id=str(id(ui.control)), name=name, style=factory.dock_style, image=image, export=factory.export, closeable=factory.deletable, dockable=DockableListElement(ui=ui, editor=self), ) return (dock_control, view_object, monitoring) # ------------------------------------------------------------------------- # Activates the corresponding dock window when the 'selected' trait of # the editor is changed. # ------------------------------------------------------------------------- def _selected_changed(self, old, new): """Activates the corresponding dock window when the 'selected' trait of the editor is changed. """ for i, value in enumerate(self._uis): if new == value[1]: value[0].activate() break return class DockableListElement(DockableViewElement): # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- #: The editor this dockable item is associated with: editor = Instance(NotebookEditor) def dockable_close(self, dock_control, force): """Returns whether it is OK to close the control.""" return self.close_dock_control(dock_control, force) def close_dock_control(self, dock_control, abort): """Closes a DockControl.""" if abort: return super().close_dock_control(dock_control, False) view_object = self.ui.context["object"] for i, value in enumerate(self.editor._uis): if view_object is value[2]: del self.editor.value[i] return False def dockable_tab_activated(self, dock_control, activated): """Handles a notebook tab being activated or deactivated. Sets the value of the editor's selected trait to the activated dock_control's object. """ for i, value in enumerate(self.editor._uis): if dock_control is value[0] and activated: self.editor.selected = value[1] break ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/wx/list_str_editor.py0000644000175100001730000010324500000000000022257 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Traits UI editor for editing lists of strings. """ import wx from traits.api import ( Str, Int, List, Bool, Instance, Any, Event, TraitListEvent, Property, Dict, ) # FIXME: ListStrEditor is a proxy class defined here just for backward # compatibility. The class has been moved to the # traitsui.editors.list_editor file. from traitsui.editors.list_str_editor import ListStrEditor from traitsui.list_str_adapter import ListStrAdapter from traitsui.wx.editor import Editor from pyface.image_resource import ImageResource from .helper import disconnect, disconnect_no_id try: from pyface.wx.drag_and_drop import PythonDropSource, PythonDropTarget except: PythonDropSource = PythonDropTarget = None # ------------------------------------------------------------------------- # 'wxListCtrl' class: # ------------------------------------------------------------------------- class wxListCtrl(wx.ListCtrl): """Subclass of wx.ListCtrl to provide correct virtual list behavior.""" def OnGetItemAttr(self, index): """Returns the display attributes to use for the specified list item.""" # fixme: There appears to be a bug in wx in that they do not correctly # manage the reference count for the returned object, and it seems to be # gc'ed before they finish using it. So we store an object reference to # it to prevent it from going away too soon... self._attr = attr = wx.ListItemAttr() editor = self._editor adapter = editor.adapter if editor._is_auto_add(index): bg_color = adapter.get_default_bg_color(editor.object, editor.name) color = adapter.get_default_text_color(editor.object, editor.name) else: bg_color = adapter.get_bg_color(editor.object, editor.name, index) color = adapter.get_text_color(editor.object, editor.name, index) if bg_color is not None: attr.SetBackgroundColour(bg_color) if color is not None: attr.SetTextColour(color) return attr def OnGetItemImage(self, index): """Returns the image index to use for the specified list item.""" editor = self._editor if editor._is_auto_add(index): image = editor.adapter.get_default_image( editor.object, editor.name ) else: image = editor.adapter.get_image(editor.object, editor.name, index) image = editor._get_image(image) if image is not None: return image return -1 def OnGetItemText(self, index, column): """Returns the text to use for the specified list item.""" editor = self._editor if editor._is_auto_add(index): return editor.adapter.get_default_text(editor.object, editor.name) return editor.adapter.get_text(editor.object, editor.name, index) class _ListStrEditor(Editor): """Traits UI editor for editing lists of strings.""" # -- Trait Definitions ---------------------------------------------------- #: The title of the editor: title = Str() #: The current set of selected items (which one is used depends upon the #: initial state of the editor factory 'multi_select' trait): selected = Any() multi_selected = List() #: The current set of selected item indices (which one is used depends upon #: the initial state of the editor factory 'multi_select' trait): selected_index = Int() multi_selected_indices = List(Int) #: The most recently actived item and its index: activated = Any() activated_index = Int() #: The most recently right_clicked item and its index: right_clicked = Event() right_clicked_index = Event() #: Is the list editor scrollable? This value overrides the default. scrollable = True #: Index of item to select after rebuilding editor list: index = Any() #: Should the selected item be edited after rebuilding the editor list: edit = Bool(False) #: The adapter from list items to editor values: adapter = Instance(ListStrAdapter) #: Dictionary mapping image names to wx.ImageList indices: images = Dict() #: Dictionary mapping ImageResource objects to wx.ImageList indices: image_resources = Dict() #: The current number of item currently in the list: item_count = Property() #: The current search string: search = Str() # -- Private traits ------------------------------------------------------- # Not exhaustive list: other private traits in the class implicitly rely on # HasPrivateTraits behavior and have not been declared. NOTE: to avoid name # conflicts and for better readability, declaring is preferable. #: Row above which the mouse was last seen hovering _last_hover_row = Int(-1) #: Content of the tooltip currently shown (or "" if nothing shown) _last_tooltip = Str() def init(self, parent): """Finishes initializing the editor by creating the underlying toolkit widget. """ factory = self.factory # Set up the adapter to use: self.adapter = factory.adapter self.sync_value(factory.adapter_name, "adapter", "from") # Determine the style to use for the list control: style = wx.LC_REPORT | wx.LC_VIRTUAL if factory.editable: style |= wx.LC_EDIT_LABELS if factory.horizontal_lines: style |= wx.LC_HRULES if not factory.multi_select: style |= wx.LC_SINGLE_SEL if (factory.title == "") and (factory.title_name == ""): style |= wx.LC_NO_HEADER # Create the list control and link it back to us: self.control = control = wxListCtrl(parent, -1, style=style) control._editor = self # Create the list control column: control.InsertColumn(0, "") # Set up the list control's event handlers: control.Bind(wx.EVT_LIST_BEGIN_DRAG, self._begin_drag) control.Bind(wx.EVT_LIST_BEGIN_LABEL_EDIT, self._begin_label_edit) control.Bind(wx.EVT_LIST_END_LABEL_EDIT, self._end_label_edit) control.Bind(wx.EVT_LIST_ITEM_SELECTED, self._item_selected) control.Bind(wx.EVT_LIST_ITEM_DESELECTED, self._item_selected) control.Bind(wx.EVT_LIST_ITEM_RIGHT_CLICK, self._right_clicked) control.Bind(wx.EVT_LIST_ITEM_ACTIVATED, self._item_activated) control.Bind(wx.EVT_SIZE, self._size_modified) control.Bind(wx.EVT_MOTION, self._motion) # Handle key events: control.Bind(wx.EVT_CHAR, self._key_pressed) # Handle mouse events: if "edit" in factory.operations: control.Bind(wx.EVT_LEFT_DOWN, self._left_down) # Set up the drag and drop target: if PythonDropTarget is not None: control.SetDropTarget(PythonDropTarget(self)) # Initialize the editor title: self.title = factory.title self.sync_value(factory.title_name, "title", "from") # Set up the selection listener (if necessary): if factory.multi_select: self.sync_value( factory.selected, "multi_selected", "both", is_list=True ) self.sync_value( factory.selected_index, "multi_selected_indices", "both", is_list=True, ) else: self.sync_value(factory.selected, "selected", "both") self.sync_value(factory.selected_index, "selected_index", "both") # Synchronize other interesting traits as necessary: self.sync_value(factory.activated, "activated", "to") self.sync_value(factory.activated_index, "activated_index", "to") self.sync_value(factory.right_clicked, "right_clicked", "to") self.sync_value( factory.right_clicked_index, "right_clicked_index", "to" ) # Make sure we listen for 'items' changes as well as complete list # replacements: self.context_object.on_trait_change( self.update_editor, self.extended_name + "_items", dispatch="ui" ) # Create the mapping from user supplied images to wx.ImageList indices: for image_resource in factory.images: self._add_image(image_resource) # Refresh the editor whenever the adapter changes: self.on_trait_change(self._refresh, "adapter.+update", dispatch="ui") # Set the list control's tooltip: self.set_tooltip() def dispose(self): """Disposes of the contents of an editor.""" disconnect_no_id( self.control, wx.EVT_SIZE, wx.EVT_CHAR, wx.EVT_LEFT_DOWN, wx.EVT_LIST_BEGIN_DRAG, wx.EVT_LIST_BEGIN_LABEL_EDIT, wx.EVT_LIST_END_LABEL_EDIT, wx.EVT_LIST_ITEM_SELECTED, wx.EVT_LIST_ITEM_DESELECTED, wx.EVT_LIST_ITEM_RIGHT_CLICK, wx.EVT_LIST_ITEM_ACTIVATED, wx.EVT_MOTION, ) self.context_object.on_trait_change( self.update_editor, self.extended_name + "_items", remove=True ) self.on_trait_change(self._refresh, "adapter.+update", remove=True) super().dispose() def update_editor(self): """Updates the editor when the object trait changes externally to the editor. """ control = self.control top = control.GetTopItem() pn = control.GetCountPerPage() n = self.adapter.len(self.object, self.name) if self.factory.auto_add: n += 1 control.DeleteAllItems() control.SetItemCount(n) if control.GetItemCount() > 0: control.RefreshItems(0, control.GetItemCount() - 1) control.SetColumnWidth(0, control.GetClientSize()[0]) edit, self.edit = self.edit, False index, self.index = self.index, None if index is not None: if index >= n: index -= 1 if index < 0: index = None if index is None: visible = top + pn - 2 if visible >= 0 and visible < control.GetItemCount(): control.ScrollHint.EnsureVisible(visible) if self.factory.multi_select: for index in self.multi_selected_indices: if 0 <= index < n: control.SetItemState( index, wx.LIST_STATE_SELECTED, wx.LIST_STATE_SELECTED, ) else: if 0 <= self.selected_index < n: control.SetItemState( self.selected_index, wx.LIST_STATE_SELECTED, wx.LIST_STATE_SELECTED, ) return if 0 <= (index - top) < pn: control.ScrollHint.EnsureVisible( min(top + pn - 2, control.GetItemCount() - 1) ) elif index < top: control.ScrollHint.EnsureVisible( min(index + pn - 1, control.GetItemCount() - 1) ) else: control.ScrollHint.EnsureVisible(index) control.SetItemState( index, wx.LIST_STATE_SELECTED | wx.LIST_STATE_FOCUSED, wx.LIST_STATE_SELECTED | wx.LIST_STATE_FOCUSED, ) if edit: control.EditLabel(index) # -- Property Implementations --------------------------------------------- def _get_item_count(self): return self.control.GetItemCount() - self.factory.auto_add # -- Trait Event Handlers ------------------------------------------------- def _title_changed(self, title): """Handles the editor title being changed.""" list_item = wx.ListItem() list_item.SetText(title) self.control.SetColumn(0, list_item) def _selected_changed(self, selected): """Handles the editor's 'selected' trait being changed.""" if not self._no_update: try: self.control.SetItemState( self.value.index(selected), wx.LIST_STATE_SELECTED, wx.LIST_STATE_SELECTED, ) except Exception: pass def _selected_index_changed(self, selected_index): """Handles the editor's 'selected_index' trait being changed.""" if not self._no_update: try: self.control.SetItemState( selected_index, wx.LIST_STATE_SELECTED, wx.LIST_STATE_SELECTED, ) except Exception: pass def _multi_selected_changed(self, selected): """Handles the editor's 'multi_selected' trait being changed.""" if not self._no_update: values = self.value try: self._multi_selected_indices_changed( [values.index(item) for item in selected] ) except Exception: pass def _multi_selected_items_changed(self, event): """Handles the editor's 'multi_selected' trait being modified.""" values = self.value try: self._multi_selected_indices_items_changed( TraitListEvent( index=0, removed=[values.index(item) for item in event.removed], added=[values.index(item) for item in event.added], ) ) except Exception: pass def _multi_selected_indices_changed(self, selected_indices): """Handles the editor's 'multi_selected_indices' trait being changed.""" if not self._no_update: control = self.control selected = self._get_selected() # Select any new items that aren't already selected: for index in selected_indices: if index in selected: selected.remove(index) else: try: control.SetItemState( index, wx.LIST_STATE_SELECTED, wx.LIST_STATE_SELECTED, ) except Exception: pass # Unselect all remaining selected items that aren't selected now: for index in selected: control.SetItemState(index, 0, wx.LIST_STATE_SELECTED) def _multi_selected_indices_items_changed(self, event): """Handles the editor's 'multi_selected_indices' trait being modified.""" control = self.control # Remove all items that are no longer selected: for index in event.removed: control.SetItemState(index, 0, wx.LIST_STATE_SELECTED) # Select all newly added items: for index in event.added: control.SetItemState( index, wx.LIST_STATE_SELECTED, wx.LIST_STATE_SELECTED ) # -- List Control Event Handlers ------------------------------------------ def _begin_drag(self, event): """Handles the user beginning a drag operation with the left mouse button. """ if PythonDropSource is not None: adapter = self.adapter object, name = self.object, self.name index = event.GetIndex() selected = self._get_selected() drag_items = [] # Collect all of the selected items to drag: for index in selected: drag = adapter.get_drag(object, name, index) if drag is None: return drag_items.append(drag) # Save the drag item indices, so that we can later handle a # completed 'move' operation: self._drag_indices = selected try: # If only one item is being dragged, drag it as an item, not a # list: if len(drag_items) == 1: drag_items = drag_items[0] # Perform the drag and drop operation: ds = PythonDropSource(self.control, drag_items) # If moves are allowed and the result was a drag move: if (ds.result == wx.DragMove) and ( self._drag_local or self.factory.drag_move ): # Then delete all of the original items (in reverse order # from highest to lowest, so the indices don't need to be # adjusted): indices = self._drag_indices indices.reverse() for index in indices: adapter.delete(object, name, index) finally: self._drag_indices = None self._drag_local = False def _begin_label_edit(self, event): """Handles the user starting to edit an item label.""" index = event.GetIndex() if (not self._is_auto_add(index)) and ( not self.adapter.get_can_edit(self.object, self.name, index) ): event.Veto() def _end_label_edit(self, event): """Handles the user finishing editing an item label.""" self._set_text_current(event.GetIndex(), event.GetText()) def _item_selected(self, event): """Handles an item being selected.""" self._no_update = True try: get_item = self.adapter.get_item object, name = self.object, self.name selected_indices = self._get_selected() if self.factory.multi_select: self.multi_selected_indices = selected_indices self.multi_selected = [ get_item(object, name, index) for index in selected_indices ] elif len(selected_indices) == 0: self.selected_index = -1 self.selected = None else: self.selected_index = selected_indices[0] self.selected = get_item(object, name, selected_indices[0]) finally: self._no_update = False def _item_activated(self, event): """Handles an item being activated (double-clicked or enter pressed).""" self.activated_index = event.GetIndex() if "edit" in self.factory.operations: self._edit_current() else: self.activated = self.adapter.get_item( self.object, self.name, self.activated_index ) def _right_clicked(self, event): """Handles an item being right clicked.""" index = event.GetIndex() if index == -1: return self.right_clicked_index = index self.right_clicked = self.adapter.get_item( self.object, self.name, index ) def _key_pressed(self, event): key = event.GetKeyCode() control = event.ControlDown() if 32 <= key <= 126: self.search += chr(key).lower() self._search_for_string() elif key in (wx.WXK_HOME, wx.WXK_PAGEUP, wx.WXK_PAGEDOWN): self.search = "" event.Skip() elif key == wx.WXK_END: self.search = "" self._append_new() elif (key == wx.WXK_UP) and control: self._search_for_string(-1) elif (key == wx.WXK_DOWN) and control: self._search_for_string(1) elif key in (wx.WXK_BACK, wx.WXK_DELETE): self._delete_current() elif key == wx.WXK_INSERT: self._insert_current() elif key == wx.WXK_LEFT: self._move_up_current() elif key == wx.WXK_RIGHT: self._move_down_current() elif key == wx.WXK_RETURN: self._edit_current() elif key == 3: # Ctrl-C self._copy_current() elif key == 22: # Ctrl-V self._paste_current() elif key == 24: # Ctrl-X self._cut_current() else: event.Skip() def _size_modified(self, event): """Handles the size of the list control being changed.""" dx, dy = self.control.GetClientSize() self.control.SetColumnWidth(0, dx - 1) event.Skip() def _motion(self, event): """Handles the user moving the mouse.""" row, _ = self.control.HitTest(wx.Point(event.GetX(), event.GetY())) if row != self._last_hover_row: self._last_hover_row = row if row == -1: tooltip = "" else: tooltip = self.adapter.get_tooltip( self.object, self.name, row ) if tooltip != self._last_tooltip: self._last_tooltip = tooltip wx.ToolTip.Enable(False) wx.ToolTip.Enable(True) self.control.SetToolTip(wx.ToolTip(tooltip)) def _left_down(self, event): """Handles the user pressing the left mouse button.""" index, flags = self.control.HitTest( wx.Point(event.GetX(), event.GetY()) ) selected = self._get_selected() if (len(selected) == 1) and (index == selected[0]): self._edit_current() else: event.Skip() # -- Drag and Drop Event Handlers ----------------------------------------- def wx_dropped_on(self, x, y, data, drag_result): """Handles a Python object being dropped on the list control.""" index, flags = self.control.HitTest(wx.Point(x, y)) # If the user dropped it on an empty list, set the target as past the # end of the list: if ( (index == -1) and ((flags & wx.LIST_HITTEST_NOWHERE) != 0) and (self.control.GetItemCount() == 0) ): index = 0 # If we have a valid drop target index, proceed: if index != -1: if not isinstance(data, list): # Handle the case of just a single item being dropped: self._wx_dropped_on(index, data) else: # Handles the case of a list of items being dropped, being # careful to preserve the original order of the source items if # possible: data.reverse() for item in data: self._wx_dropped_on(index, item) # If this was an inter-list drag, mark it as 'local': if self._drag_indices is not None: self._drag_local = True # Return a successful drop result: return drag_result # Indicate we could not process the drop: return wx.DragNone def _wx_dropped_on(self, index, item): """Helper method for handling a single item dropped on the list control. """ adapter = self.adapter object, name = self.object, self.name # Obtain the destination of the dropped item relative to the target: destination = adapter.get_dropped(object, name, index, item) # Adjust the target index accordingly: if destination == "after": index += 1 # Insert the dropped item at the requested position: adapter.insert(object, name, index, item) # If the source for the drag was also this list control, we need to # adjust the original source indices to account for their new position # after the drag operation: indices = self._drag_indices if indices is not None: for i in range(len(indices) - 1, -1, -1): if indices[i] < index: break indices[i] += 1 def wx_drag_over(self, x, y, data, drag_result): """Handles a Python object being dragged over the tree.""" if isinstance(data, list): rc = wx.DragNone for item in data: rc = self.wx_drag_over(x, y, item, drag_result) if rc == wx.DragNone: break return rc index, flags = self.control.HitTest(wx.Point(x, y)) # If the user is dragging over an empty list, set the target to the end # of the list: if ( (index == -1) and ((flags & wx.LIST_HITTEST_NOWHERE) != 0) and (self.control.GetItemCount() == 0) ): index = 0 # If the drag target index is valid and the adapter says it is OK to # drop the data here, then indicate the data can be dropped: if (index != -1) and self.adapter.get_can_drop( self.object, self.name, index, data ): return drag_result # Else indicate that we will not accept the data: return wx.DragNone # -- Private Methods ------------------------------------------------------ def _refresh(self): """Refreshes the contents of the editor's list control.""" self.control.RefreshItems(0, len(self.value) - 1) def _add_image(self, image_resource): """Adds a new image to the wx.ImageList and its associated mapping.""" bitmap = image_resource.create_image().ConvertToBitmap() image_list = self._image_list if image_list is None: self._image_list = image_list = wx.ImageList( bitmap.GetWidth(), bitmap.GetHeight() ) self.control.AssignImageList(image_list, wx.IMAGE_LIST_SMALL) self.image_resources[image_resource] = self.images[ image_resource.name ] = index = image_list.Add(bitmap) return index def _get_image(self, image): """Converts a user specified image to a wx.ListCtrl image index.""" if isinstance(image, ImageResource): result = self.image_resources.get(image) if result is not None: return result return self._add_image(image) return self.images.get(image) def _get_selected(self): """Returns a list of the indices of all currently selected list items.""" selected = [] item = -1 control = self.control while True: item = control.GetNextItem( item, wx.LIST_NEXT_ALL, wx.LIST_STATE_SELECTED ) if item == -1: break selected.append(item) return selected def _search_for_string(self, increment=0): """Searches for the next occurrence of the current search string.""" selected = self._get_selected() if len(selected) > 1: return start = 0 if len(selected) == 1: start = selected[0] + increment get_text = self.adapter.get_text search = self.search object = self.object name = self.name if increment >= 0: items = range(start, self.item_count) else: items = range(start, -1, -1) for index in items: if search in get_text(object, name, index).lower(): self.index = index self.update_editor() break def _append_new(self): """Append a new item to the end of the list control.""" if "append" in self.factory.operations: self.edit = True adapter = self.adapter index = self.control.GetItemCount() if self.factory.auto_add: self.index = index - 1 self.update_editor() else: self.index = index adapter.insert( self.object, self.name, self.index, adapter.get_default_value(self.object, self.name), ) def _copy_current(self): """Copies the currently selected list control item to the clipboard.""" selected = self._get_selected() if len(selected) == 1: index = selected[0] if index < self.item_count: try: from pyface.wx.clipboard import clipboard clipboard.data = self.adapter.get_text( self.object, self.name, index ) except: # Handle the traits.util package not being installed by # just ignoring the request: pass def _cut_current(self): """Cuts the currently selected list control item and places its value in the clipboard. """ ops = self.factory.operations if ("insert" in ops) and ("delete" in ops): selected = self._get_selected() if len(selected) == 1: index = selected[0] if index < self.item_count: try: from pyface.wx.clipboard import clipboard clipboard.data = self.adapter.get_text( self.object, self.name, index ) self.index = index self.adapter.delete(self.object, self.name, index) except: # Handle the traits.util package not being installed # by just ignoring the request: pass def _paste_current(self): """Pastes the clipboard contents into the currently selected list control item. """ if "insert" in self.factory.operations: selected = self._get_selected() if len(selected) == 1: try: from pyface.wx.clipboard import clipboard self._set_text_current( selected[0], clipboard.text_data, insert=True ) except: # Handle the traits.util package not being installed by # just ignoring the request: pass def _insert_current(self): """Inserts a new item after the currently selected list control item.""" if "insert" in self.factory.operations: selected = self._get_selected() if len(selected) == 1: self.index = selected[0] self.edit = True adapter = self.adapter adapter.insert( self.object, self.name, selected[0], adapter.get_default_value(self.object, self.name), ) def _delete_current(self): """Deletes the currently selected items from the list control.""" if "delete" in self.factory.operations: selected = self._get_selected() if len(selected) == 0: return n = self.item_count delete = self.adapter.delete selected.reverse() self.index = selected[-1] for index in selected: if index < n: delete(self.object, self.name, index) def _move_up_current(self): """Moves the currently selected item up one line in the list control.""" if "move" in self.factory.operations: selected = self._get_selected() if len(selected) == 1: index = selected[0] n = self.item_count if 0 < index < n: adapter = self.adapter object, name = self.object, self.name item = adapter.get_item(object, name, index) adapter.delete(object, name, index) self.index = index - 1 adapter.insert(object, name, index - 1, item) def _move_down_current(self): """Moves the currently selected item down one line in the list control.""" if "move" in self.factory.operations: selected = self._get_selected() if len(selected) == 1: index = selected[0] n = self.item_count - 1 if index < n: adapter = self.adapter object, name = self.object, self.name item = adapter.get_item(object, name, index) adapter.delete(object, name, index) self.index = index + 1 adapter.insert(object, name, index + 1, item) def _edit_current(self): """Allows the user to edit the current item in the list control.""" if "edit" in self.factory.operations: selected = self._get_selected() if len(selected) == 1: self.control.EditLabel(selected[0]) def _is_auto_add(self, index): """Returns whether or not the index is the special 'auto add' item at the end of the list. """ return self.factory.auto_add and ( index >= self.adapter.len(self.object, self.name) ) def _set_text_current(self, index, text, insert=False): """Sets the text value of the specified list control item.""" if text.strip() != "": object, name, adapter = self.object, self.name, self.adapter if insert or self._is_auto_add(index): adapter.insert( object, name, index, adapter.get_default_value(object, name), ) self.edit = not insert self.index = index + 1 adapter.set_text(object, name, index, text) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/wx/menu.py0000644000175100001730000002537300000000000020017 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Dynamically construct wxPython Menus or MenuBars from a supplied string description of the menu. Menu Description Syntax:: submenu_label {help_string} menuitem_label | accelerator {help_string} [~/-name]: code *submenu_label* Label of a sub menu *menuitem_label* Label of a menu item {*help_string*} Help string to display on the status line (optional) *accelerator* Accelerator key (e.g., Ctrl-C) (The '|' and keyname are optional, but must be used together.) [~] The menu item is checkable, but is not checked initially (optional) [/] The menu item is checkable, and is checked initially (optional) [-] The menu item disabled initially (optional) [*name*] Symbolic name used to refer to menu item (optional) *code* Python code invoked when menu item is selected A line beginning with a hyphen (-) is interpreted as a menu separator. """ # ========================================================================= # Imports: # ========================================================================= import re import logging import wx logger = logging.getLogger(__name__) # ========================================================================= # Constants: # ========================================================================= help_pat = re.compile(r"(.*){(.*)}(.*)") options_pat = re.compile(r"(.*)\[(.*)\](.*)") # Mapping of key name strings to wxPython key codes key_map = { "F1": wx.WXK_F1, "F2": wx.WXK_F2, "F3": wx.WXK_F3, "F4": wx.WXK_F4, "F5": wx.WXK_F5, "F6": wx.WXK_F6, "F7": wx.WXK_F7, "F8": wx.WXK_F8, "F9": wx.WXK_F9, "F10": wx.WXK_F10, "F11": wx.WXK_F11, "F12": wx.WXK_F12, } class MakeMenu: """Manages creation of menus.""" #: Initialize the globally unique menu ID: cur_id = 1000 def __init__(self, desc, owner, popup=False, window=None): """Initializes the object.""" self.owner = owner if window is None: window = owner self.window = window self.indirect = getattr(owner, "call_menu", None) self.names = {} self.desc = desc.split("\n") self.index = 0 self.keys = [] if popup: self.menu = menu = wx.Menu() self.parse(menu, -1) else: self.menu = menu = wx.MenuBar() self.parse(menu, -1) window.SetMenuBar(menu) if len(self.keys) > 0: window.SetAcceleratorTable(wx.AcceleratorTable(self.keys)) def parse(self, menu, indent): """Recursively parses menu items from the description.""" while True: # Make sure we have not reached the end of the menu description # yet: if self.index >= len(self.desc): return # Get the next menu description line and check its indentation: dline = self.desc[self.index] line = dline.lstrip() indented = len(dline) - len(line) if indented <= indent: return # Indicate that the current line has been processed: self.index += 1 # Check for a blank or comment line: if (line == "") or (line[0:1] == "#"): continue # Check for a menu separator: if line[0:1] == "-": menu.AppendSeparator() continue # Allocate a new menu ID: MakeMenu.cur_id += 1 cur_id = MakeMenu.cur_id # Extract the help string (if any): help = "" match = help_pat.search(line) if match: help = " " + match.group(2).strip() line = match.group(1) + match.group(3) # Check for a menu item: col = line.find(":") if col >= 0: handler = line[col + 1 :].strip() if handler != "": if self.indirect: self.indirect(cur_id, handler) handler = self.indirect else: try: _locl = dict(self=self) exec( "def handler(event, self=self.owner):\n %s\n" % handler, globals(), _locl, ) handler = _locl["handler"] except Exception: logger.exception( "Invalid menu handler {:r}".format(handler) ) handler = null_handler else: try: _locl = dict(self=self) exec( "def handler(event, self=self.owner):\n%s\n" % (self.get_body(indented),), globals(), _locl, ) handler = _locl["handler"] except Exception: logger.exception( "Invalid menu handler {:r}".format(handler) ) handler = null_handler self.window.Bind(wx.EVT_MENU, handler, id=cur_id) not_checked = checked = disabled = False line = line[:col] match = options_pat.search(line) if match: line = match.group(1) + match.group(3) not_checked, checked, disabled, name = option_check( "~/-", match.group(2).strip() ) if name != "": self.names[name] = cur_id setattr(self.owner, name, MakeMenuItem(self, cur_id)) label = line.strip() col = label.find("|") if col >= 0: key = label[col + 1 :].strip() label = "%s%s%s" % (label[:col].strip(), "\t", key) key = key.upper() flag = wx.ACCEL_NORMAL col = key.find("-") if col >= 0: flag = { "CTRL": wx.ACCEL_CTRL, "SHIFT": wx.ACCEL_SHIFT, "ALT": wx.ACCEL_ALT, }.get(key[:col].strip(), wx.ACCEL_CTRL) key = key[col + 1 :].strip() code = key_map.get(key, None) try: if code is None: code = ord(key) self.keys.append( wx.AcceleratorEntry(flag, code, cur_id) ) except: pass menu.Append(cur_id, label, help, not_checked or checked) if checked: menu.Check(cur_id, True) if disabled: menu.Enable(cur_id, False) continue # Else must be the start of a sub menu: submenu = wx.Menu() label = line.strip() # Recursively parse the sub-menu: self.parse(submenu, indented) # Add the menu to its parent: try: menu.AppendMenu(cur_id, label, submenu, help) except: # Handle the case where 'menu' is really a 'MenuBar' (which does # not understand 'MenuAppend'): menu.Append(submenu, label) def get_body(self, indent): """Returns the body of an inline method.""" result = [] while self.index < len(self.desc): line = self.desc[self.index] if (len(line) - len(line.lstrip())) <= indent: break result.append(line) self.index += 1 result = "\n".join(result).rstrip() if result != "": return result return " pass" def get_id(self, name): """Returns the ID associated with a specified name.""" if isinstance(name, str): return self.names[name] return name def checked(self, name, check=None): """Checks (or unchecks) a menu item specified by name.""" if check is None: return self.menu.IsChecked(self.get_id(name)) self.menu.Check(self.get_id(name), check) def enabled(self, name, enable=None): """Enables (or disables) a menu item specified by name.""" if enable is None: return self.menu.IsEnabled(self.get_id(name)) self.menu.Enable(self.get_id(name), enable) def label(self, name, label=None): """Gets or sets the label for a menu item.""" if label is None: return self.menu.GetLabel(self.get_id(name)) self.menu.SetLabel(self.get_id(name), label) class MakeMenuItem: """A menu item for a menu managed by MakeMenu.""" def __init__(self, menu, id): self.menu = menu self.id = id def checked(self, check=None): return self.menu.checked(self.id, check) def toggle(self): checked = not self.checked() self.checked(checked) return checked def enabled(self, enable=None): return self.menu.enabled(self.id, enable) def label(self, label=None): return self.menu.label(self.id, label) # ------------------------------------------------------------------------- # Determine whether a string contains any specified option characters, and # remove them if it does: # ------------------------------------------------------------------------- def option_check(test, string): """Determines whether a string contains any specified option characters, and removes them if it does. """ result = [] for char in test: col = string.find(char) result.append(col >= 0) if col >= 0: string = string[:col] + string[col + 1 :] return result + [string.strip()] # ------------------------------------------------------------------------- # Null menu option selection handler: # ------------------------------------------------------------------------- def null_handler(event): print("null_handler invoked") ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/wx/null_editor.py0000644000175100001730000000176700000000000021374 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Defines a completely empty editor, intended to be used as a spacer. """ import wx from .editor import Editor class NullEditor(Editor): """A completely empty editor.""" def init(self, parent): """Finishes initializing the editor by creating the underlying toolkit widget. """ self.control = control = wx.Window(parent, -1, size=wx.Size(1, 1)) control.SetBackgroundColour(parent.GetBackgroundColour()) def update_editor(self): """Updates the editor when the object trait changes externally to the editor. """ pass ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/wx/popup_editor.py0000644000175100001730000000171200000000000021553 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # FIXME: PopupEditor is a proxy class defined here just for backward # compatibility. The class (which represents the editor factory) has been moved # to the traitsui.editors.list_editor file. from traitsui.editors.popup_editor import ( _PopupEditor as BasePopupEditor, PopupEditor, ) from .ui_editor import UIEditor # ------------------------------------------------------------------------- # '_PopupEditor' class: # ------------------------------------------------------------------------- class _PopupEditor(BasePopupEditor, UIEditor): pass ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/wx/progress_editor.py0000644000175100001730000000625400000000000022262 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! import wx from traits.api import Instance, Int, Str from traitsui.wx.editor import Editor from pyface.ui.wx.progress_dialog import ProgressDialog class _ProgressDialog(ProgressDialog): def close(self): """Overwritten to disable closing.""" pass class SimpleEditor(Editor): """ Show a progress bar with all the optional goodies """ progress = Instance(ProgressDialog) # The message to be displayed along side the progress guage message = Str() # The starting value min = Int() # The ending value max = Int() # -- Editor interface ------------------------------------------------------ def init(self, parent): """Finishes initializing the editor by creating the underlying toolkit widget. """ self.control = self.create_control(parent) factory = self.factory self.min = factory.min self.max = factory.max self.message = factory.message self.sync_value(factory.min_name, "min", "from") self.sync_value(factory.max_name, "max", "from") self.sync_value(factory.message_name, "message", "from") self.set_tooltip() def create_control(self, parent): """ Finishes initializing the editor by creating the underlying widget. """ self.progress = ProgressDialog( title=self.factory.title, message=self.factory.message, min=self.factory.min, max=self.factory.max, can_cancel=self.factory.can_cancel, show_time=self.factory.show_time, show_percent=self.factory.show_percent, ) panel = wx.Panel(parent, -1) sizer = wx.BoxSizer(wx.VERTICAL) panel.SetSizer(sizer) panel.SetAutoLayout(True) panel.SetBackgroundColour(wx.NullColour) self.progress.dialog_size = wx.Size() # The 'guts' of the dialog. self.progress._create_message(panel, sizer) self.progress._create_gauge(panel, sizer) self.progress._create_percent(panel, sizer) self.progress._create_timer(panel, sizer) self.progress._create_buttons(panel, sizer) panel.SetClientSize(self.progress.dialog_size) panel.CentreOnParent() self.control = panel return self.control def update_editor(self): """ Updates the editor when the object trait changes externally to the editor. """ self.progress.min = self.min self.progress.max = self.max self.progress.change_message(self.message) self.progress.update(self.value) def _min_changed(self): self.update_editor() def _max_changed(self): self.update_editor() def _message_changed(self): self.update_editor() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/wx/range_editor.py0000644000175100001730000007334100000000000021513 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Defines the various range editors for the wxPython user interface toolkit. """ import sys import wx from math import log10 from traits.api import TraitError, Str, Float, Any, Bool from .editor_factory import TextEditor from .editor import Editor from .constants import OKColor, ErrorColor from .helper import TraitsUIPanel, Slider if not hasattr(wx, "wx.wxEVT_SCROLL_ENDSCROLL"): wxEVT_SCROLL_ENDSCROLL = wx.wxEVT_SCROLL_CHANGED else: wxEVT_SCROLL_ENDSCROLL = wx.wxEVT_SCROLL_ENDSCROLL # ------------------------------------------------------------------------- # 'BaseRangeEditor' class: # ------------------------------------------------------------------------- class BaseRangeEditor(Editor): """The base class for Range editors. Using an evaluate trait, if specified, when assigning numbers the object trait. """ # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- #: Function to evaluate floats/ints evaluate = Any() def _set_value(self, 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. """ # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- #: Low value for the slider range low = Any() #: High value for the slider range high = Any() #: Flag indicating that the UI is in the process of being updated ui_changing = Bool(False) 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.evaluate = factory.evaluate self.sync_value(factory.evaluate_name, "evaluate", "from") size = wx.DefaultSize if factory.label_width > 0: size = wx.Size(factory.label_width, 20) self.sync_value(factory.low_name, "low", "from") self.sync_value(factory.high_name, "high", "from") self.control = panel = TraitsUIPanel(parent, -1) sizer = wx.BoxSizer(wx.HORIZONTAL) fvalue = self.value if not (self.low <= fvalue <= self.high): fvalue_text = "" fvalue = self.low else: try: fvalue_text = self.string_value(fvalue) except (ValueError, TypeError) as e: fvalue_text = "" ivalue = self._convert_to_slider(fvalue) self._label_lo = wx.StaticText( panel, -1, "999999", size=size, style=wx.ALIGN_RIGHT | wx.ST_NO_AUTORESIZE, ) sizer.Add(self._label_lo, 0, wx.ALIGN_CENTER) panel.slider = slider = Slider( panel, -1, ivalue, 0, 10000, size=wx.Size(80, 20), style=wx.SL_HORIZONTAL | wx.SL_AUTOTICKS, ) slider.SetTickFreq(1000) slider.SetValue(1) slider.SetPageSize(1000) slider.SetLineSize(100) slider.Bind(wx.EVT_SCROLL, self.update_object_on_scroll) sizer.Add(slider, 1, wx.EXPAND) self._label_hi = wx.StaticText(panel, -1, "999999", size=size) sizer.Add(self._label_hi, 0, wx.ALIGN_CENTER) panel.text = text = wx.TextCtrl( panel, -1, fvalue_text, size=wx.Size(56, 20), style=wx.TE_PROCESS_ENTER, ) panel.Bind( wx.EVT_TEXT_ENTER, self.update_object_on_enter, id=text.GetId() ) text.Bind(wx.EVT_KILL_FOCUS, self.update_object_on_enter) sizer.Add(text, 0, wx.LEFT | wx.EXPAND, 4) low_label = factory.low_label if factory.low_name != "": low_label = self.string_value(self.low) high_label = factory.high_label if factory.high_name != "": high_label = self.string_value(self.high) self._label_lo.SetLabel(low_label) self._label_hi.SetLabel(high_label) self.set_tooltip(slider) self.set_tooltip(self._label_lo) self.set_tooltip(self._label_hi) self.set_tooltip(text) # Set-up the layout: panel.SetSizerAndFit(sizer) def update_object_on_scroll(self, event): """Handles the user changing the current slider value.""" value = self._convert_from_slider(event.GetPosition()) event_type = event.GetEventType() if ( (event_type == wxEVT_SCROLL_ENDSCROLL) or ( self.factory.auto_set and (event_type == wx.wxEVT_SCROLL_THUMBTRACK) ) or ( self.factory.enter_set and (event_type == wx.wxEVT_SCROLL_THUMBRELEASE) ) ): try: self.ui_changing = True self.control.text.SetValue(self.string_value(value)) self.value = value except TraitError: pass finally: self.ui_changing = False def update_object_on_enter(self, event): """Handles the user pressing the Enter key in the text field.""" if isinstance(event, wx.FocusEvent): event.Skip() # There are cases where this method is called with self.control == # None. if self.control is None: return try: try: value = self.control.text.GetValue().strip() if self.factory.is_float: value = float(value) else: value = int(value) except Exception as ex: # The user entered something that didn't eval as a number (e.g., 'foo'). # Pretend it didn't happen (i.e. do not change self.value). value = self.value self.control.text.SetValue(str(value)) # for compound editor, value may be non-numeric if not isinstance(value, (int, float)): return self.value = value if not self.ui_changing: self.control.slider.SetValue( self._convert_to_slider(self.value) ) self.control.text.SetBackgroundColour(OKColor) self.control.text.Refresh() if self._error is not None: self._error = None self.ui.errors -= 1 except TraitError: pass 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 super().error(excp) self.set_error_state(True) def update_editor(self): """Updates the editor when the object trait changes externally to the editor. """ value = self.value try: text = self.string_value(value) 1 // (self.low <= value <= self.high) except: text = "" value = self.low ivalue = self._convert_to_slider(value) self.control.text.SetValue(text) self.control.slider.SetValue(ivalue) def _convert_to_slider(self, value): """Returns the slider setting corresponding to the user-supplied value.""" if self.high > self.low: ivalue = int( (float(value - self.low) / (self.high - self.low)) * 10000.0 ) else: ivalue = self.low return ivalue def _convert_from_slider(self, ivalue): """Returns the float or integer value corresponding to the slider setting. """ value = self.low + ((float(ivalue) / 10000.0) * (self.high - self.low)) if not self.factory.is_float: value = int(round(value)) return value def get_error_control(self): """Returns the editor's control for indicating error status.""" return self.control.text 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._label_lo is not None: self._label_lo.SetLabel(self.string_value(low)) self.update_editor() 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._label_hi is not None: self._label_hi.SetLabel(self.string_value(high)) self.update_editor() # ------------------------------------------------------------------------- class LogRangeSliderEditor(SimpleSliderEditor): # ------------------------------------------------------------------------- """A slider editor for log-spaced values""" def _convert_to_slider(self, value): """Returns the slider setting corresponding to the user-supplied value.""" value = max(value, self.low) ivalue = int( (log10(value) - log10(self.low)) / (log10(self.high) - log10(self.low)) * 10000.0 ) return ivalue def _convert_from_slider(self, ivalue): """Returns the float or integer value corresponding to the slider setting. """ value = float(ivalue) / 10000.0 * (log10(self.high) - log10(self.low)) # Do this to handle floating point errors, where fvalue may exceed # self.high. fvalue = min(self.low * 10 ** (value), self.high) if not self.factory.is_float: fvalue = int(round(fvalue)) return fvalue 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. """ # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- #: 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) 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() self.control = panel = TraitsUIPanel(parent, -1) sizer = wx.BoxSizer(wx.HORIZONTAL) fvalue = self.value try: fvalue_text = self._format % fvalue 1 // (low <= fvalue <= high) except: fvalue_text = "" fvalue = low if high > low: ivalue = int((float(fvalue - low) / (high - low)) * 10000) else: ivalue = low # Lower limit label: label_lo = wx.StaticText(panel, -1, "999999") panel.label_lo = label_lo sizer.Add(label_lo, 2, wx.ALIGN_CENTER) # Lower limit button: bmp = wx.ArtProvider.GetBitmap(wx.ART_GO_BACK, size=(15, 15)) button_lo = wx.BitmapButton( panel, -1, bitmap=bmp, size=(-1, 20), style=wx.BU_EXACTFIT | wx.NO_BORDER, ) panel.button_lo = button_lo button_lo.Bind(wx.EVT_BUTTON, self.reduce_range) sizer.Add(button_lo, 1, wx.ALIGN_CENTER) # Slider: panel.slider = slider = Slider( panel, -1, ivalue, 0, 10000, size=wx.Size(80, 20), style=wx.SL_HORIZONTAL | wx.SL_AUTOTICKS, ) slider.SetTickFreq(1000) slider.SetValue(1) slider.SetPageSize(1000) slider.SetLineSize(100) slider.Bind(wx.EVT_SCROLL, self.update_object_on_scroll) sizer.Add(slider, 6, wx.EXPAND) # Upper limit button: bmp = wx.ArtProvider.GetBitmap(wx.ART_GO_FORWARD, size=(15, 15)) button_hi = wx.BitmapButton( panel, -1, bitmap=bmp, size=(-1, 20), style=wx.BU_EXACTFIT | wx.NO_BORDER, ) panel.button_hi = button_hi button_hi.Bind(wx.EVT_BUTTON, self.increase_range) sizer.Add(button_hi, 1, wx.ALIGN_CENTER) # Upper limit label: label_hi = wx.StaticText(panel, -1, "999999") panel.label_hi = label_hi sizer.Add(label_hi, 2, wx.ALIGN_CENTER) # Text entry: panel.text = text = wx.TextCtrl( panel, -1, fvalue_text, size=wx.Size(56, 20), style=wx.TE_PROCESS_ENTER, ) panel.Bind( wx.EVT_TEXT_ENTER, self.update_object_on_enter, id=text.GetId() ) text.Bind(wx.EVT_KILL_FOCUS, self.update_object_on_enter) sizer.Add(text, 0, wx.LEFT | wx.EXPAND, 4) # Set-up the layout: panel.SetSizerAndFit(sizer) label_lo.SetLabel(str(low)) label_hi.SetLabel(str(high)) self.set_tooltip(slider) self.set_tooltip(label_lo) self.set_tooltip(label_hi) self.set_tooltip(text) # Update the ranges and button just in case. self.update_range_ui() def update_object_on_scroll(self, event): """Handles the user changing the current slider value.""" low = self.cur_low high = self.cur_high value = low + ((float(event.GetPosition()) / 10000.0) * (high - low)) self.control.text.SetValue(self._format % value) event_type = event.GetEventType() try: self.ui_changing = True if ( (event_type == wxEVT_SCROLL_ENDSCROLL) or ( self.factory.auto_set and (event_type == wx.wxEVT_SCROLL_THUMBTRACK) ) or ( self.factory.enter_set and (event_type == wx.wxEVT_SCROLL_THUMBRELEASE) ) ): if self.factory.is_float: self.value = value else: self.value = int(value) finally: self.ui_changing = False def update_object_on_enter(self, event): """Handles the user pressing the Enter key in the text field.""" if isinstance(event, wx.FocusEvent): event.Skip() # It is possible the event is processed after the control is removed # from the editor if self.control is None: return try: value = self.control.text.GetValue().strip() try: if self.factory.is_float: value = float(value) else: value = int(value) except Exception as ex: # The user entered something that didn't eval as a number (e.g., 'foo'). # Pretend it didn't happen (i.e. do not change self.value). value = self.value self.control.text.SetValue(str(value)) self.value = value self.control.text.SetBackgroundColour(OKColor) self.control.text.Refresh() # Update the slider range. # Set ui_changing to True to avoid recursion: # the update_range_ui method will try to set the value in the text # box, which will again fire this method if auto_set is True. if not self.ui_changing: self.ui_changing = True self.init_range() self.update_range_ui() self.ui_changing = False if self._error is not None: self._error = None self.ui.errors -= 1 except TraitError as excp: pass 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 super().error(excp) self.set_error_state(True) 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: text = self._format % value 1 / (low <= value <= high) except: value = low self.value = value if not self.ui_changing: self.init_range() self.update_range_ui() def update_range_ui(self): """Updates the slider range controls.""" low, high = self.cur_low, self.cur_high value = self.value self._set_format() self.control.label_lo.SetLabel(self._format % low) self.control.label_hi.SetLabel(self._format % high) if high > low: ivalue = int((float(value - low) / (high - low)) * 10000.0) else: ivalue = low self.control.slider.SetValue(ivalue) text = self._format % self.value self.control.text.SetValue(text) factory = self.factory f_low, f_high = self.low, self.high if low == f_low: self.control.button_lo.Disable() else: self.control.button_lo.Enable() if high == f_high: self.control.button_hi.Disable() else: self.control.button_hi.Enable() def init_range(self): """Initializes the slider range controls.""" value = self.value factory = self.factory 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): """Reduces the extent of the displayed range.""" factory = self.factory 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.ui_changing = True self.value = min(max(self.value, self.cur_low), self.cur_high) self.ui_changing = False self.update_range_ui() def increase_range(self, event): """Increased the extent of the displayed range.""" factory = self.factory 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.ui_changing = True self.value = min(max(self.value, self.cur_low), self.cur_high) self.ui_changing = False self.update_range_ui() 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.text 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.""" # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- #: 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") low = self.low high = self.high self.control = wx.SpinCtrl( parent, -1, self.str_value, min=low, max=high, initial=self.value ) parent.Bind( wx.EVT_SPINCTRL, self.update_object, id=self.control.GetId() ) self.set_tooltip() def update_object(self, event): """Handles the user selecting a new value in the spin box.""" if self.control is None: return self._locked = True try: self.value = self.control.GetValue() finally: self._locked = False def update_editor(self): """Updates the editor when the object trait changes externally to the editor. """ if not self._locked: try: self.control.SetValue(int(self.value)) except: pass 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.SetRange(self.low, self.high) self.control.SetValue(int(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.SetRange(self.low, self.high) self.control.SetValue(int(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. """ # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- #: Low value for the slider range low = Any() #: High value for the slider range high = Any() #: Function to evaluate floats/ints evaluate = Any() def init(self, parent): """Finishes initializing the editor by creating the underlying toolkit widget. """ if not self.factory.low_name: self.low = self.factory.low if not self.factory.high_name: self.high = self.factory.high self.sync_value(self.factory.low_name, "low", "from") self.sync_value(self.factory.high_name, "high", "from") if self.factory.enter_set: control = wx.TextCtrl( parent, -1, self.str_value, style=wx.TE_PROCESS_ENTER ) parent.Bind( wx.EVT_TEXT_ENTER, self.update_object, id=control.GetId() ) else: control = wx.TextCtrl(parent, -1, self.str_value) control.Bind(wx.EVT_KILL_FOCUS, self.update_object) if self.factory.auto_set: parent.Bind(wx.EVT_TEXT, self.update_object, id=control.GetId()) self.evaluate = self.factory.evaluate self.sync_value(self.factory.evaluate_name, "evaluate", "from") self.control = control self.set_tooltip() def update_object(self, event): """Handles the user entering input data in the edit control.""" if isinstance(event, wx.FocusEvent): event.Skip() # It is possible the event is processed after the control is removed # from the editor if self.control is None: return value = self.control.GetValue() # Try to convert the string value entered by the user to a numerical # value. try: if self.evaluate is not None: value = self.evaluate(value) else: if self.factory.is_float: value = float(value) else: value = int(value) except Exception as excp: # The conversion failed. self.error(excp) return if value < self.low or value > self.high: self.set_error_state(True) return # Try to assign the numerical value to the trait. # This may fail because of constraints on the trait. try: self.value = value self.control.SetBackgroundColour(OKColor) self.control.Refresh() if self._error is not None: self._error = None self.ui.errors -= 1 except TraitError as excp: pass 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 super().error(excp) self.set_error_state(True) 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.SetValue(int(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.SetValue(int(self.value)) def SimpleEnumEditor(parent, factory, ui, object, name, description, **kwargs): return CustomEnumEditor( parent, factory, ui, object, name, description, "simple" ) def CustomEnumEditor( parent, factory, ui, object, name, description, style="custom", **kwargs ): """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.EnumEditor( values=list(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: # ------------------------------------------------------------------------- # Mapping between editor factory modes and simple 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, } ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/wx/rgb_color_editor.py0000644000175100001730000000466200000000000022367 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Defines a subclass of the base wxPython color editor factory, for colors that are represented as tuples of the form ( *red*, *green*, *blue* ), where *red*, *green* and *blue* are floats in the range from 0.0 to 1.0. """ import wx from traits.trait_base import SequenceTypes # Note: The ToolkitEditorFactory class imported from color_editor is the # abstract ToolkitEditorFactory class (in traitsui.api) along with # wx-specific methods added via a category. We need to override the # implementations of the wx-specific methods here. from .color_editor import ToolkitEditorFactory as BaseColorToolkitEditorFactory # --------------------------------------------------------------------------- # The wxPython ToolkitEditorFactory class. # --------------------------------------------------------------------------- class ToolkitEditorFactory(BaseColorToolkitEditorFactory): """wxPython editor factory for color editors.""" def to_wx_color(self, editor, color=None): """Gets the wxPython color equivalent of the object trait.""" if color is None: try: color = getattr(editor.object, editor.name + "_") except AttributeError: color = getattr(editor.object, editor.name) if isinstance(color, tuple): return wx.Colour( int(color[0] * 255.0), int(color[1] * 255.0), int(color[2] * 255.0), ) return color def from_wx_color(self, color): """Gets the application equivalent of a wxPython value.""" return ( color.Red() / 255.0, color.Green() / 255.0, color.Blue() / 255.0, ) def str_color(self, color): """Returns the text representation of a specified color value.""" if type(color) in SequenceTypes: return "(%d,%d,%d)" % ( int(color[0] * 255.0), int(color[1] * 255.0), int(color[2] * 255.0), ) return super().str_color(color) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/wx/rgb_color_trait.py0000644000175100001730000001001600000000000022212 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Trait definition for an RGB-based color, which is a tuple of the form (*red*, *green*, *blue*), where *red*, *green* and *blue* are floats in the range from 0.0 to 1.0. """ import wx from traits.api import Trait, TraitError from traits.trait_base import SequenceTypes ### Note: Import from the source rather than the api to avoid circular imports # since some classes declared in the traits UI api define Color traits which # will end up importing this file. from traitsui.editors.rgb_color_editor import RGBColorEditor from traitsui.wx.color_trait import standard_colors, w3c_color_database # ------------------------------------------------------------------------- # Convert a number into an RGB tuple: # ------------------------------------------------------------------------- def range_check(value): """Checks that *value* can be converted to a value in the range 0.0 to 1.0. If so, it returns the floating point value; otherwise, it raises a TraitError. """ value = float(value) if 0.0 <= value <= 1.0: return value raise TraitError def convert_to_color(object, name, value): """Converts a tuple or an integer to an RGB color value, or raises a TraitError if that is not possible. """ if (type(value) in SequenceTypes) and (len(value) == 3): return ( range_check(value[0]), range_check(value[1]), range_check(value[2]), ) if isinstance(value, int): num = int(value) return ( (num // 0x10000) / 255.0, ((num // 0x100) & 0xFF) / 255.0, (num & 0xFF) / 255.0, ) if isinstance(value, wx.Colour): return ( value.Red() / 255.0, value.Green() / 255.0, value.Blue() / 255.0, ) raise TraitError convert_to_color.info = ( "a tuple of the form (r,g,b), where r, g, and b " "are floats in the range from 0.0 to 1.0, or an integer which in hex is of " "the form 0xRRGGBB, where RR is red, GG is green, and BB is blue" ) # ------------------------------------------------------------------------- # Standard colors: # ------------------------------------------------------------------------- # RGB versions of standard colors: rgb_standard_colors = {} for name, color in standard_colors.items(): rgb_standard_colors[name] = ( color.Red() / 255.0, color.Green() / 255.0, color.Blue() / 255.0, ) # Add the W3C colors for color_name in w3c_color_database._color_names: color = w3c_color_database.Find(color_name) rgb_standard_colors[color_name] = ( color.Red() / 255.0, color.Green() / 255.0, color.Blue() / 255.0, ) # ------------------------------------------------------------------------- # Define wxPython specific color traits: # ------------------------------------------------------------------------- ### Note: Declare the editor to be a function which returns the RGBColorEditor # class from traits ui to avoid circular import issues. For backwards # compatibility with previous Traits versions, the 'editors' folder in Traits # project declares 'from api import *' in its __init__.py. The 'api' in turn # can contain classes that have a RGBColor trait which lead to this file getting # imported. This will lead to a circular import when declaring a RGBColor # trait. def get_rgb_color_editor(*args, **traits): from .rgb_color_editor import ToolkitEditorFactory return ToolkitEditorFactory(*args, **traits) # Trait whose value must be an RGB color: RGBColor = Trait( "white", convert_to_color, rgb_standard_colors, editor=get_rgb_color_editor ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/wx/scrubber_editor.py0000644000175100001730000004210300000000000022216 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Traits UI simple, scrubber-based integer or float value editor. """ import wx from math import log10, pow from traits.api import ( Any, BaseRange, BaseEnum, Str, Float, TraitError, observe, ) from traitsui.api import View, Item, EnumEditor # FIXME: ScrubberEditor is a proxy class defined here just for backward # compatibility (represents the editor factory for scrubber editors). # The class has been moved to traitsui.editors.scrubber_editor from traitsui.editors.scrubber_editor import ScrubberEditor from traitsui.wx.editor import Editor from pyface.timer.api import do_after from .constants import ErrorColor from .image_slice import paint_parent from .helper import disconnect, disconnect_no_id, BufferDC class _ScrubberEditor(Editor): """Traits UI simple, scrubber-based integer or float value editor.""" #: The low end of the slider range: low = Any() #: The high end of the slider range: high = Any() #: The smallest allowed increment: increment = Float() #: The current text being displayed: text = Str() #: The mapping to use (only for Enum's): mapping = Any() # -- Class Variables ------------------------------------------------------ text_styles = { "left": wx.TE_LEFT, "center": wx.TE_CENTRE, "right": wx.TE_RIGHT, } def init(self, parent): """Finishes initializing the editor by creating the underlying toolkit widget. """ factory = self.factory # Establish the range of the slider: low_name = high_name = "" low, high = factory.low, factory.high if high <= low: low = high = None handler = self.object.trait(self.name).handler if isinstance(handler, BaseRange): low_name, high_name = handler._low_name, handler._high_name if low_name == "": low = handler._low if high_name == "": high = handler._high elif isinstance(handler, BaseEnum): if handler.name == "": self.mapping = handler.values else: self.sync_value(handler.name, "mapping", "from") low, high = 0, self.high # Create the control: self.control = control = wx.Window( parent, -1, size=wx.Size(50, 18), style=wx.FULL_REPAINT_ON_RESIZE | wx.TAB_TRAVERSAL, ) # Set up the painting event handlers: control.Bind(wx.EVT_ERASE_BACKGROUND, self._erase_background) control.Bind(wx.EVT_PAINT, self._on_paint) control.Bind(wx.EVT_SET_FOCUS, self._set_focus) # Set up mouse event handlers: control.Bind(wx.EVT_LEAVE_WINDOW, self._leave_window) control.Bind(wx.EVT_ENTER_WINDOW, self._enter_window) control.Bind(wx.EVT_LEFT_DOWN, self._left_down) control.Bind(wx.EVT_LEFT_UP, self._left_up) control.Bind(wx.EVT_MOTION, self._motion) control.Bind(wx.EVT_MOUSEWHEEL, self._mouse_wheel) # Set up the control resize handler: control.Bind(wx.EVT_SIZE, self._resize) # Set the tooltip: self._can_set_tooltip = not self.set_tooltip() # Save the values we calculated: self.trait_set(low=low, high=high) self.sync_value(low_name, "low", "from") self.sync_value(high_name, "high", "from") # Force a reset (in case low = high = None, which won't cause a # notification to fire): self._reset_scrubber() def dispose(self): """Disposes of the contents of an editor.""" # Remove all of the wx event handlers: disconnect_no_id( self.control, wx.EVT_ERASE_BACKGROUND, wx.EVT_PAINT, wx.EVT_SET_FOCUS, wx.EVT_LEAVE_WINDOW, wx.EVT_ENTER_WINDOW, wx.EVT_LEFT_DOWN, wx.EVT_LEFT_UP, wx.EVT_MOTION, wx.EVT_MOUSEWHEEL, wx.EVT_SIZE, ) # Disconnect the pop-up text event handlers: self._disconnect_text() super().dispose() def update_editor(self): """Updates the editor when the object trait changes externally to the editor. """ self.text = self.string_value(self.value) self._text_size = None self._refresh() self._enum_completed() def update_object(self, value): """Updates the object when the scrubber value changes.""" if self.mapping is not None: value = self.mapping[int(value)] if value != self.value: try: self.value = value self.update_editor() except TraitError: value = int(value) if value != self.value: self.value = value self.update_editor() def error(self, excp): """Handles an error that occurs while setting the object's trait value.""" pass # -- Trait Event Handlers ------------------------------------------------- def _mapping_changed(self, mapping): """Handles the Enum mapping being changed.""" self.high = len(mapping) - 1 # -- Private Methods ------------------------------------------------------ @observe("low, high") def _reset_scrubber(self, event=None): """Sets the the current tooltip.""" low, high = self.low, self.high if self._can_set_tooltip: if self.mapping is not None: tooltip = "[%s]" % (", ".join(self.mapping)) if len(tooltip) > 80: tooltip = "" elif high is None: tooltip = "" if low is not None: tooltip = "[%g..]" % low elif low is None: tooltip = "[..%g]" % high else: tooltip = "[%g..%g]" % (low, high) self.control.SetToolTip(tooltip) # Establish the slider increment: increment = self.factory.increment if increment <= 0.0: if (low is None) or (high is None) or isinstance(low, int): increment = 1.0 else: increment = pow(10, round(log10((high - low) / 100.0))) self.increment = increment self.update_editor() def _get_text_bounds(self): """Get the window bounds of where the current text should be displayed. """ tdx, tdy, descent, leading = self._get_text_size() wdx, wdy = self.control.GetClientSize() ty = ((wdy - (tdy - descent)) // 2) - 1 alignment = self.factory.alignment if alignment == "left": tx = 0 elif alignment == "center": tx = (wdx - tdx) // 2 else: tx = wdx - tdx return (tx, ty, tdx, tdy) def _get_text_size(self): """Returns the text size information for the window.""" if self._text_size is None: self._text_size = self.control.GetFullTextExtent( self.text.strip() or "M" ) return self._text_size def _refresh(self): """Refreshes the contents of the control.""" if self.control is not None: self.control.Refresh() def _set_scrubber_position(self, event, delta): """Calculates a new scrubber value for a specified mouse position change. """ clicks = 3 increment = self.increment if event.ShiftDown(): increment *= 10.0 clicks = 7 elif event.ControlDown(): increment /= 10.0 value = self._value + (delta / clicks) * increment if self.low is not None: value = max(value, self.low) if self.high is not None: value = min(value, self.high) self.update_object(value) def _delayed_click(self): """Handle a delayed click response.""" self._pending = False def _pop_up_editor(self): """Pop-up a text control to allow the user to enter a value using the keyboard. """ self.control.SetCursor(wx.Cursor(wx.CURSOR_ARROW)) if self.mapping is not None: self._pop_up_enum() else: self._pop_up_text() def _pop_up_enum(self): self._ui = self.object.edit_traits( view=View( Item( self.name, id="drop_down", show_label=False, padding=-4, editor=EnumEditor(name="editor.mapping"), ), kind="subpanel", ), parent=self.control, context={"object": self.object, "editor": self}, ) dx, dy = self.control.GetSize() drop_down = self._ui.info.drop_down.control drop_down.SetSize(0, 0, dx, dy) drop_down.SetFocus() drop_down.Bind(wx.EVT_KILL_FOCUS, self._enum_completed) def _pop_up_text(self): control = self.control self._text = text = wx.TextCtrl( control, -1, str(self.value), size=control.GetSize(), style=self.text_styles[self.factory.alignment] | wx.TE_PROCESS_ENTER, ) text.SetSelection(-1, -1) text.SetFocus() control.Bind(wx.EVT_TEXT_ENTER, self._text_completed, id=text.GetId()) text.Bind(wx.EVT_ENTER_WINDOW, self._enter_text) text.Bind(wx.EVT_LEAVE_WINDOW, self._leave_text) text.Bind(wx.EVT_CHAR, self._key_entered) def _destroy_text(self): """Destroys the current text control.""" self._ignore_focus = self._in_text_window self._disconnect_text() self.control.DestroyChildren() self._text = None def _disconnect_text(self): """Disconnects the event handlers for the pop up text editor.""" if self._text is not None: disconnect(self._text, wx.EVT_TEXT_ENTER) disconnect_no_id( self._text, wx.EVT_KILL_FOCUS, wx.EVT_ENTER_WINDOW, wx.EVT_LEAVE_WINDOW, wx.EVT_CHAR, ) def _init_value(self): """Initializes the current value when the user begins a drag or moves the mouse wheel. """ if self.mapping is not None: try: self._value = list(self.mapping).index(self.value) except: self._value = 0 else: self._value = self.value # --- wxPython Event Handlers --------------------------------------------- def _erase_background(self, event): """Do not erase the background here (do it in the 'on_paint' handler).""" pass def _on_paint(self, event): """Paint the background using the associated ImageSlice object.""" control = self.control dc = BufferDC(control) # Draw the background: factory = self.factory color = factory.color_ if self._x is not None: if factory.active_color_ is not None: color = factory.active_color_ elif self._hover: if factory.hover_color_ is not None: color = factory.hover_color_ if color is None: paint_parent(dc, control) brush = wx.TRANSPARENT_BRUSH else: brush = wx.Brush(color) color = factory.border_color_ if color is not None: pen = wx.Pen(color) else: pen = wx.TRANSPARENT_PEN if (pen != wx.TRANSPARENT_PEN) or (brush != wx.TRANSPARENT_BRUSH): wdx, wdy = control.GetClientSize() dc.SetBrush(brush) dc.SetPen(pen) dc.DrawRectangle(0, 0, wdx, wdy) # Draw the current text value: dc.SetBackgroundMode(wx.TRANSPARENT) dc.SetTextForeground(factory.text_color_) dc.SetFont(control.GetFont()) tx, ty, tdx, tdy = self._get_text_bounds() dc.DrawText(self.text, tx, ty) # Copy the buffer contents to the display: dc.copy() def _resize(self, event): """Handles the control being resized.""" if self._text is not None: self._text.SetSize(self.control.GetSize()) def _set_focus(self, event): """Handle the control getting the keyboard focus.""" if ( (not self._ignore_focus) and (self._x is None) and (self._text is None) ): self._pop_up_editor() event.Skip() def _enter_window(self, event): """Handles the mouse entering the window.""" self._hover = True self.control.SetCursor(wx.Cursor(wx.CURSOR_HAND)) if not self._ignore_focus: self._ignore_focus = True self.control.SetFocus() self._ignore_focus = False if self._x is not None: if self.factory.active_color_ != self.factory.color_: self.control.Refresh() elif self.factory.hover_color_ != self.factory.color_: self.control.Refresh() def _leave_window(self, event): """Handles the mouse leaving the window.""" self._hover = False if self.factory.hover_color_ != self.factory.color_: self.control.Refresh() def _left_down(self, event): """Handles the left mouse being pressed.""" self._x, self._y = event.GetX(), event.GetY() self._pending = True self._init_value() self.control.CaptureMouse() if self.factory.active_color_ != self.factory.hover_color_: self.control.Refresh() do_after(200, self._delayed_click) def _left_up(self, event): """Handles the left mouse button being released.""" self.control.ReleaseMouse() if self._pending: self._pop_up_editor() self._x = self._y = self._value = self._pending = None if self._hover or (self.factory.active_color_ != self.factory.color_): self.control.Refresh() def _motion(self, event): """Handles the mouse moving.""" if self._x is not None: x, y = event.GetX(), event.GetY() dx = x - self._x adx = abs(dx) dy = y - self._y ady = abs(dy) if self._pending: if (adx + ady) < 3: return self._pending = False if adx > ady: delta = dx else: delta = -dy self._set_scrubber_position(event, delta) def _mouse_wheel(self, event): """Handles the mouse wheel moving.""" if self._hover: self._init_value() clicks = 3 if event.ShiftDown(): clicks = 7 delta = clicks * ( event.GetWheelRotation() // event.GetWheelDelta() ) self._set_scrubber_position(event, delta) def _update_value(self, event): """Updates the object value from the current text control value.""" control = event.GetEventObject() try: self.update_object(float(control.GetValue())) return True except TraitError: control.SetBackgroundColour(ErrorColor) control.Refresh() return False def _enter_text(self, event): """Handles the mouse entering the pop-up text control.""" self._in_text_window = True def _leave_text(self, event): """Handles the mouse leaving the pop-up text control.""" self._in_text_window = False def _text_completed(self, event): """Handles the user pressing the 'Enter' key in the text control.""" if isinstance(event, wx.FocusEvent): event.Skip() if self._update_value(event): self._destroy_text() def _enum_completed(self, event=None): """Handles the Enum drop-down control losing focus.""" if self._ui is not None: self._ignore_focus = True disconnect_no_id( self._ui.info.drop_down.control, wx.EVT_KILL_FOCUS ) self._ui.dispose() del self._ui def _key_entered(self, event): """Handles individual key strokes while the text control is active.""" key_code = event.GetKeyCode() if key_code == wx.WXK_ESCAPE: self._destroy_text() return if key_code == wx.WXK_TAB: if self._update_value(event): if event.ShiftDown(): self.control.Navigate(0) else: self.control.Navigate() return event.Skip() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/wx/search_editor.py0000644000175100001730000000556300000000000021665 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # System library imports import wx # Local imports from .editor import Editor class SearchEditor(Editor): def init(self, parent): """Finishes initializing the editor by creating the underlying toolkit widget. """ style = 0 if self.factory.enter_set: style = wx.TE_PROCESS_ENTER self.control = wx.SearchCtrl(parent, -1, value=self.value, style=style) self.control.SetDescriptiveText(self.factory.text) self.control.ShowSearchButton(self.factory.search_button) self.control.ShowCancelButton(self.factory.cancel_button) if self.factory.auto_set: self.control.Bind(wx.EVT_TEXT, self.update_object) if self.factory.enter_set: self.control.Bind(wx.EVT_TEXT_ENTER, self.update_object) self.control.Bind(wx.EVT_SEARCHCTRL_SEARCH_BTN, self.update_object) self.control.Bind(wx.EVT_SEARCHCTRL_CANCEL_BTN, self.clear_text) def dispose(self): if self.control is not None: if self.factory.auto_set: self.control.Unbind(wx.EVT_TEXT, handler=self.update_object) if self.factory.enter_set: self.control.Unbind( wx.EVT_TEXT_ENTER, handler=self.update_object ) self.control.Unbind( wx.EVT_SEARCHCTRL_SEARCH_BTN, handler=self.update_object ) self.control.Unbind( wx.EVT_SEARCHCTRL_CANCEL_BTN, handler=self.clear_text ) super().dispose() def update_object(self, event): """Handles the user entering input data in the edit control.""" if not self._no_update: self.value = self.control.GetValue() if self.factory.search_event_trait != "": setattr(self.object, self.factory.search_event_trait, True) def clear_text(self, event): """Handles the user pressing the cancel search button.""" if not self._no_update: self.control.SetValue("") self.value = "" if self.factory.search_event_trait != "": setattr(self.object, self.factory.search_event_trait, True) def update_editor(self): """Updates the editor when the object trait changes externally to the editor. """ if self.control.GetValue() != self.value: self._no_update = True self.control.SetValue(self.str_value) self._no_update = False ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/wx/set_editor.py0000644000175100001730000004107500000000000021211 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Defines the set editors for the wxPython user interface toolkit. """ # fixme: Add undo/redo support # fixme: Allow factory to handle a TraitListObject for the 'values' trait. import wx from traits.api import Property from traitsui.helper import enum_values_changed from .editor import Editor from .helper import TraitsUIPanel # ------------------------------------------------------------------------- # 'SimpleEditor' class: # ------------------------------------------------------------------------- class SimpleEditor(Editor): """Simple style of editor for sets. The editor displays two list boxes, with buttons for moving the selected items from left to right, or vice versa. If **can_move_all** on the factory is True, then buttons are displayed for moving all the items to one box or the other. If the set is ordered, buttons are displayed for moving the selected item up or down in right-side list box. """ # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- #: Current set of enumeration names: names = Property() #: Current mapping from names to values: mapping = Property() #: Current inverse mapping from values to names: inverse_mapping = Property() #: Is set editor scrollable? This value overrides the default. scrollable = True def init(self, parent): """Finishes initializing the editor by creating the underlying toolkit widget. """ factory = self.factory if factory.name != "": self._object, self._name, self._value = self.parse_extended_name( factory.name ) self.values_changed() self._object.on_trait_change( self._values_changed, self._name, dispatch="ui" ) else: self._value = lambda: self.factory.values self.values_changed() factory.on_trait_change( self._values_changed, "values", dispatch="ui" ) self.control = panel = TraitsUIPanel(parent, -1) hsizer = wx.BoxSizer(wx.HORIZONTAL) vsizer = wx.BoxSizer(wx.VERTICAL) self._unused = self._create_listbox( panel, hsizer, self._on_unused, self._on_use, factory.left_column_title, ) self._use_all = self._unuse_all = self._up = self._down = None if factory.can_move_all: self._use_all = self._create_button( ">>", panel, vsizer, 15, self._on_use_all ) self._use = self._create_button(">", panel, vsizer, 15, self._on_use) self._unuse = self._create_button( "<", panel, vsizer, 0, self._on_unuse ) if factory.can_move_all: self._unuse_all = self._create_button( "<<", panel, vsizer, 15, self._on_unuse_all ) if factory.ordered: self._up = self._create_button( "Move Up", panel, vsizer, 30, self._on_up ) self._down = self._create_button( "Move Down", panel, vsizer, 0, self._on_down ) hsizer.Add(vsizer, 0, wx.LEFT | wx.RIGHT, 8) self._used = self._create_listbox( panel, hsizer, self._on_value, self._on_unuse, factory.right_column_title, ) panel.SetSizer(hsizer) self.context_object.on_trait_change( self.update_editor, self.extended_name + "_items?", dispatch="ui" ) self.set_tooltip() def _get_names(self): """Gets the current set of enumeration names.""" return self._names def _get_mapping(self): """Gets the current mapping.""" return self._mapping def _get_inverse_mapping(self): """Gets the current inverse mapping.""" return self._inverse_mapping def _create_listbox(self, parent, sizer, handler1, handler2, title): """Creates a list box.""" column_sizer = wx.BoxSizer(wx.VERTICAL) # Add the column title in emphasized text: title_widget = wx.StaticText(parent, -1, title) font = title_widget.GetFont() emphasis_font = wx.Font( font.GetPointSize() + 1, font.GetFamily(), font.GetStyle(), wx.BOLD ) title_widget.SetFont(emphasis_font) column_sizer.Add(title_widget, 0, 0) # Create the list box and add it to the column: list = wx.ListBox(parent, -1, style=wx.LB_EXTENDED | wx.LB_NEEDED_SB) column_sizer.Add(list, 1, wx.EXPAND) # Add the column to the SetEditor widget: sizer.Add(column_sizer, 1, wx.EXPAND) # Hook up the event handlers: parent.Bind(wx.EVT_LISTBOX, handler1, id=list.GetId()) parent.Bind(wx.EVT_LISTBOX_DCLICK, handler2, id=list.GetId()) return list def _create_button(self, label, parent, sizer, space_before, handler): """Creates a button.""" button = wx.Button(parent, -1, label, style=wx.BU_EXACTFIT) sizer.AddSpacer(space_before) sizer.Add(button, 0, wx.EXPAND | wx.BOTTOM, 8) parent.Bind(wx.EVT_BUTTON, handler, id=button.GetId()) return button def values_changed(self): """Recomputes the cached data based on the underlying enumeration model or the values of the factory. """ ( self._names, self._mapping, self._inverse_mapping, ) = enum_values_changed(self._value(), self.string_value) def _values_changed(self): """Handles the underlying object model's enumeration set or factory's values being changed. """ self.values_changed() self.update_editor() def update_editor(self): """Updates the editor when the object trait changes externally to the editor. """ # Check for any items having been deleted from the enumeration that are # still present in the object value: mapping = self.inverse_mapping.copy() values = [v for v in self.value if v in mapping] if len(values) < len(self.value): self.value = values return # Get a list of the selected items in the right box: used = self._used used_labels = self._get_selected_strings(used) # Get a list of the selected items in the left box: unused = self._unused unused_labels = self._get_selected_strings(unused) # Empty list boxes in preparation for rebuilding from current values: used.Clear() unused.Clear() # Ensure right list box is kept alphabetized unless insertion # order is relevant: if not self.factory.ordered: values = sorted(values[:]) # Rebuild the right listbox: used_selections = [] for i, value in enumerate(values): label = mapping[value] used.Append(label) del mapping[value] if label in used_labels: used_selections.append(i) # Rebuild the left listbox: unused_selections = [] unused_items = sorted(mapping.values()) mapping = self.mapping self._unused_items = [mapping[ui] for ui in unused_items] for i, unused_item in enumerate(unused_items): unused.Append(unused_item) if unused_item in unused_labels: unused_selections.append(i) # If nothing is selected, default selection should be top of left box, # or of right box if left box is empty: if (len(used_selections) == 0) and (len(unused_selections) == 0): if unused.GetCount() == 0: used_selections.append(0) else: unused_selections.append(0) used_count = used.GetCount() for i in used_selections: if i < used_count: used.SetSelection(i) unused_count = unused.GetCount() for i in unused_selections: if i < unused_count: unused.SetSelection(i) self._check_up_down() self._check_left_right() def dispose(self): """Disposes of the contents of an editor.""" if self._object is not None: self._object.on_trait_change( self._values_changed, self._name, remove=True ) else: self.factory.on_trait_change( self._values_changed, "values", remove=True ) self.context_object.on_trait_change( self.update_editor, self.extended_name + "_items?", remove=True ) super().dispose() def get_error_control(self): """Returns the editor's control for indicating error status.""" return [self._unused, self._used] # ------------------------------------------------------------------------- # Event handlers: # ------------------------------------------------------------------------- def _on_value(self, event): if not self.factory.ordered: self._clear_selection(self._unused) self._check_left_right() self._check_up_down() def _on_unused(self, event): if not self.factory.ordered: self._clear_selection(self._used) self._check_left_right() self._check_up_down() def _on_use(self, event): self._unused_items, self.value = self._transfer_items( self._unused, self._used, self._unused_items, self.value ) def _on_unuse(self, event): self.value, self._unused_items = self._transfer_items( self._used, self._unused, self.value, self._unused_items ) def _on_use_all(self, event): self._unused_items, self.value = self._transfer_all( self._unused, self._used, self._unused_items, self.value ) def _on_unuse_all(self, event): self.value, self._unused_items = self._transfer_all( self._used, self._unused, self.value, self._unused_items ) def _on_up(self, event): self._move_item(-1) def _on_down(self, event): self._move_item(1) # ------------------------------------------------------------------------- # Private methods: # ------------------------------------------------------------------------- # ------------------------------------------------------------------------- # Unselects all items in the given ListBox # ------------------------------------------------------------------------- def _clear_selection(self, box): """Unselects all items in the given ListBox""" for i in box.GetSelections(): box.Deselect(i) def _transfer_all(self, list_from, list_to, values_from, values_to): """Transfers all items from one list to another.""" values_from = values_from[:] values_to = values_to[:] self._clear_selection(list_from) while list_from.GetCount() > 0: index_to = list_to.GetCount() list_from.SetSelection(0) list_to.InsertItems( self._get_selected_strings(list_from), index_to ) list_from.Delete(0) values_to.append(values_from[0]) del values_from[0] list_to.SetSelection(0) self._check_left_right() self._check_up_down() return (values_from, values_to) def _transfer_items(self, list_from, list_to, values_from, values_to): """Transfers the selected item from one list to another.""" values_from = values_from[:] values_to = values_to[:] indices_from = list_from.GetSelections() index_from = max(self._get_first_selection(list_from), 0) index_to = max(self._get_first_selection(list_to), 0) self._clear_selection(list_to) # Get the list of strings in the "from" box to be moved: selected_list = self._get_selected_strings(list_from) # fixme: I don't know why I have to reverse the list to get # correct behavior from the ordered list box. Investigate -- LP selected_list.reverse() list_to.InsertItems(selected_list, index_to) # Delete the transferred items from the left box: for i in range(len(indices_from) - 1, -1, -1): list_from.Delete(indices_from[i]) # Delete the transferred items from the "unused" value list: for item_label in selected_list: val_index_from = values_from.index(self.mapping[item_label]) values_to.insert(index_to, values_from[val_index_from]) del values_from[val_index_from] # If right list is ordered, keep moved items selected: if self.factory.ordered: list_to.SetSelection(list_to.FindString(item_label)) # Reset the selection in the left box: count = list_from.GetCount() if count > 0: if index_from >= count: index_from -= 1 list_from.SetSelection(index_from) self._check_left_right() self._check_up_down() return (values_from, values_to) def _move_item(self, direction): """Moves an item up or down within the "used" list.""" # Move the item up/down within the list: listbox = self._used index_from = self._get_first_selection(listbox) index_to = index_from + direction label = listbox.GetString(index_from) listbox.Deselect(index_from) listbox.Delete(index_from) listbox.Insert(label, index_to) listbox.SetSelection(index_to) # Enable the up/down buttons appropriately: self._check_up_down() # Move the item up/down within the editor's trait value: value = self.value if direction < 0: index = index_to values = [value[index_from], value[index_to]] else: index = index_from values = [value[index_to], value[index_from]] self.value = value[:index] + values + value[index + 2 :] def _check_up_down(self): """Sets the proper enabled state for the up and down buttons.""" if self.factory.ordered: index_selected = self._used.GetSelections() self._up.Enable( (len(index_selected) == 1) and (index_selected[0] > 0) ) self._down.Enable( (len(index_selected) == 1) and (index_selected[0] < (self._used.GetCount() - 1)) ) def _check_left_right(self): """Sets the proper enabled state for the left and right buttons.""" self._use.Enable( self._unused.GetCount() > 0 and self._get_first_selection(self._unused) >= 0 ) self._unuse.Enable( self._used.GetCount() > 0 and self._get_first_selection(self._used) >= 0 ) if self.factory.can_move_all: self._use_all.Enable( (self._unused.GetCount() > 0) and (self._get_first_selection(self._unused) >= 0) ) self._unuse_all.Enable( (self._used.GetCount() > 0) and (self._get_first_selection(self._used) >= 0) ) # ------------------------------------------------------------------------- # Returns a list of the selected strings in the listbox # ------------------------------------------------------------------------- def _get_selected_strings(self, listbox): """Returns a list of the selected strings in the given *listbox*.""" stringlist = [] for label_index in listbox.GetSelections(): stringlist.append(listbox.GetString(label_index)) return stringlist # ------------------------------------------------------------------------- # Returns the index of the first (or only) selected item. # ------------------------------------------------------------------------- def _get_first_selection(self, listbox): """Returns the index of the first (or only) selected item.""" select_list = listbox.GetSelections() if len(select_list) == 0: return -1 return select_list[0] ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/wx/shell_editor.py0000644000175100001730000000154000000000000021516 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Editor that displays an interactive Python shell. """ from traitsui.editors.shell_editor import _ShellEditor as BaseShellEditor from .editor import Editor # ------------------------------------------------------------------------- # 'ShellEditor' class: # ------------------------------------------------------------------------- class _ShellEditor(BaseShellEditor, Editor): """Editor that displays an interactive Python shell.""" pass ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/wx/table_editor.py0000644000175100001730000015270400000000000021507 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Defines the table editor for the wxPython user interface toolkit. """ from operator import itemgetter import wx from pyface.dock.api import ( DockWindow, DockSizer, DockSection, DockRegion, DockControl, ) from pyface.image_resource import ImageResource from pyface.timer.api import do_later from pyface.ui.wx.grid.api import Grid from traits.api import ( Int, List, Instance, Str, Any, Button, Tuple, HasPrivateTraits, Bool, Event, Property, ) from traitsui.api import ( View, Item, UI, InstanceEditor, EnumEditor, Handler, SetEditor, ListUndoItem, ) from traitsui.editors.table_editor import BaseTableEditor, customize_filter from traitsui.menu import Action, ToolBar from traitsui.table_column import TableColumn, ObjectColumn from traitsui.table_filter import TableFilter from traitsui.ui_traits import SequenceTypes from .constants import ( TableCellBackgroundColor, TableCellColor, TableLabelBackgroundColor, TableLabelColor, TableReadOnlyBackgroundColor, TableSelectionBackgroundColor, TableSelectionTextColor, ) from .editor import Editor from .table_model import TableModel, TraitGridSelection from .helper import TraitsUIPanel #: Mapping from TableEditor selection modes to Grid selection modes: GridModes = { "row": "rows", "rows": "rows", "column": "cols", "columns": "cols", "cell": "cell", "cells": "cell", } def _get_color(color, default_color): """Return the color if it is not None, otherwise use default.""" if color is not None: return color return default_color class TableEditor(Editor, BaseTableEditor): """Editor that presents data in a table. Optionally, tables can have a set of filters that reduce the set of data displayed, according to their criteria. """ # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- #: The set of columns currently defined on the editor: columns = List(Instance(TableColumn)) #: Index of currently edited (i.e., selected) table item(s): selected_row_index = Int(-1) selected_row_indices = List(Int) selected_indices = Property() selected_column_index = Int(-1) selected_column_indices = List(Int) selected_cell_index = Tuple(Int, Int) selected_cell_indices = List(Tuple(Int, Int)) #: The currently selected table item(s): selected_row = Any() selected_rows = List() selected_items = Property() selected_column = Any() selected_columns = List() selected_cell = Tuple(Any, Str) selected_cells = List(Tuple(Any, Str)) selected_values = Property() #: The indices of the table items currently passing the table filter: filtered_indices = List(Int) #: The event fired when a cell is clicked on: click = Event() #: The event fired when a cell is double-clicked on: dclick = Event() #: Is the editor in row mode (i.e. not column or cell mode)? in_row_mode = Property() #: Is the editor in column mode (i.e. not row or cell mode)? in_column_mode = Property() #: Current filter object (should be a TableFilter or callable or None): filter = Any() #: The grid widget associated with the editor: grid = Instance(Grid) #: The table model associated with the editor: model = Instance(TableModel) #: TableEditorToolbar associated with the editor: toolbar = Any() #: The Traits UI associated with the table editor toolbar: toolbar_ui = Instance(UI) #: Is the table editor scrollable? This value overrides the default. scrollable = True #: Is 'auto_add' mode in effect? (I.e., new rows are automatically added to #: the end of the table when the user modifies current last row.) auto_add = Bool(False) def init(self, parent): """Finishes initializing the editor by creating the underlying toolkit widget. """ factory = self.factory self.filter = factory.filter self.auto_add = factory.auto_add and (factory.row_factory is not None) columns = factory.columns[:] if (len(columns) == 0) and (len(self.value) > 0): columns = [ ObjectColumn(name=name) for name in self.value[0].editable_traits() ] self.columns = columns self.model = model = TableModel(editor=self, reverse=factory.reverse) model.on_trait_change(self._model_sorted, "sorted", dispatch="ui") mode = factory.selection_mode row_mode = mode in ("row", "rows") selected = None items = model.get_filtered_items() if factory.editable and (len(items) > 0): selected = items[0] if (factory.edit_view == " ") or (not row_mode): self.control = panel = TraitsUIPanel(parent, -1) sizer = wx.BoxSizer(wx.VERTICAL) self._create_toolbar(panel, sizer) # Create the table (i.e. grid) control: hsizer = wx.BoxSizer(wx.HORIZONTAL) self._create_grid(panel, hsizer) sizer.Add(hsizer, 1, wx.EXPAND) else: item = self.item name = item.get_label(self.ui) theme = factory.dock_theme or item.container.dock_theme self.control = dw = DockWindow(parent, theme=theme).control panel = TraitsUIPanel(dw, -1, size=(300, 300)) sizer = wx.BoxSizer(wx.VERTICAL) dc = DockControl( name=name + " Table", id="table", control=panel, style="fixed" ) contents = [DockRegion(contents=[dc])] self._create_toolbar(panel, sizer) selected = None items = model.get_filtered_items() if factory.editable and (len(items) > 0): selected = items[0] # Create the table (i.e. grid) control: hsizer = wx.BoxSizer(wx.HORIZONTAL) self._create_grid(panel, hsizer) sizer.Add(hsizer, 1, wx.EXPAND) # Assign the initial object here, so a valid editor will be built # when the 'edit_traits' call is made: self.selected_row = selected self._ui = ui = self.edit_traits( parent=dw, kind="subpanel", view=View( [ Item( "selected_row", style="custom", editor=InstanceEditor( view=factory.edit_view, kind="subpanel" ), resizable=True, width=factory.edit_view_width, height=factory.edit_view_height, ), "|<>", ], resizable=True, handler=factory.edit_view_handler, ), ) # Set the parent UI of the new UI to our own UI: ui.parent = self.ui # Reset the object so that the sub-sub-view will pick up the # correct history also: self.selected_row = None self.selected_row = selected dc.style = item.dock contents.append( DockRegion( contents=[ DockControl( name=name + " Editor", id="editor", control=ui.control, style=item.dock, ) ] ) ) # Finish setting up the DockWindow: dw.SetSizer( DockSizer( contents=DockSection( contents=contents, is_row=(factory.orientation == "horizontal"), ) ) ) # Set up the required externally synchronized traits (if any): sv = self.sync_value is_list = mode[-1] == "s" sv(factory.click, "click", "to") sv(factory.dclick, "dclick", "to") sv(factory.filter_name, "filter", "from") sv(factory.columns_name, "columns", is_list=True) sv(factory.filtered_indices, "filtered_indices", "to") sv(factory.selected, "selected_%s" % mode, is_list=is_list) if is_list: sv( factory.selected_indices, "selected_%s_indices" % mode[:-1], is_list=True, ) else: sv(factory.selected_indices, "selected_%s_index" % mode) # Listen for the selection changing on the grid: self.grid.on_trait_change( getattr(self, "_selection_%s_updated" % mode), "selection_changed", dispatch="ui", ) # Set the min height of the grid panel to 0, this will provide # a scrollbar if the window is resized such that only the first row # is visible panel.SetMinSize((-1, 0)) # Finish the panel layout setup: panel.SetSizer(sizer) def _create_grid(self, parent, sizer): """Creates the associated grid control used to implement the table.""" factory = self.factory selection_mode = GridModes[factory.selection_mode] if factory.selection_bg_color is None: selection_mode = "" cell_color = _get_color(factory.cell_color, TableCellColor) cell_bg_color = _get_color( factory.cell_bg_color, TableCellBackgroundColor ) cell_read_only_bg_color = _get_color( factory.cell_read_only_bg_color, TableReadOnlyBackgroundColor ) label_bg_color = _get_color( factory.label_bg_color, TableLabelBackgroundColor ) label_color = _get_color(factory.label_color, TableLabelColor) selection_text_color = _get_color( factory.selection_color, TableSelectionTextColor ) selection_bg_color = _get_color( factory.selection_bg_color, TableSelectionBackgroundColor ) self.grid = grid = Grid( parent, model=self.model, enable_lines=factory.show_lines, grid_line_color=factory.line_color, show_row_headers=factory.show_row_labels, show_column_headers=factory.show_column_labels, default_cell_font=factory.cell_font, default_cell_text_color=cell_color, default_cell_bg_color=cell_bg_color, default_cell_read_only_color=cell_read_only_bg_color, default_label_font=factory.label_font, default_label_text_color=label_color, default_label_bg_color=label_bg_color, selection_bg_color=selection_bg_color, selection_text_color=selection_text_color, autosize=factory.auto_size, read_only=not factory.editable, edit_on_first_click=factory.edit_on_first_click, selection_mode=selection_mode, allow_column_sort=factory.sortable, allow_row_sort=False, column_label_height=factory.column_label_height, row_label_width=factory.row_label_width, ) _grid = grid._grid _grid.SetScrollLineY(factory.scroll_dy) # Set the default size for each table row: height = factory.row_height if height <= 0: height = _grid.GetTextExtent("My")[1] + 9 _grid.SetDefaultRowSize(height) # Allow the table to be resizable if the user did not explicitly # specify a number of rows to display: self.scrollable = factory.rows == 0 # Calculate a reasonable default size for the table: if len(self.model.get_filtered_items()) > 0: height = _grid.GetRowSize(0) max_rows = factory.rows or 15 min_width = max(150, 80 * len(self.columns)) if factory.show_column_labels: min_height = _grid.GetColLabelSize() + (max_rows * height) else: min_height = max_rows * height _grid.SetMinSize(wx.Size(min_width, min_height)) # On Linux, there is what appears to be a bug in wx in which the # vertical scrollbar will not be sized properly if the TableEditor is # sized to be shorter than the minimum height specified above. Since # this height is only set to ensure that the TableEditor is sized # correctly during the initial UI layout, we un-set it after this takes # place (addresses ticket 1810) def clear_minimum_height(info): min_size = _grid.GetMinSize() min_size.height = 0 _grid.SetMinSize(min_size) self.ui.add_defined(clear_minimum_height) sizer.Add(grid.control, 1, wx.EXPAND) return grid.control def _create_toolbar(self, parent, sizer): """Creates the table editing toolbar.""" factory = self.factory if not factory.show_toolbar: return toolbar = TableEditorToolbar(parent=parent, editor=self) if (toolbar.control is not None) or (len(factory.filters) > 0): tb_sizer = wx.BoxSizer(wx.HORIZONTAL) if len(factory.filters) > 0: view = View( [ Item( "filter<250>{View}", editor=factory._filter_editor ), "_", Item( "filter_summary<100>{Results}~", object="model", resizable=False, ), "_", "-", ], resizable=True, ) self.toolbar_ui = ui = view.ui( context={"object": self, "model": self.model}, parent=parent, kind="subpanel", ).trait_set(parent=self.ui) tb_sizer.Add(ui.control, 0) if toolbar.control is not None: self.toolbar = toolbar # add padding so the toolbar is right aligned tb_sizer.Add((1, 1), 1, wx.EXPAND) tb_sizer.Add(toolbar.control, 0) sizer.Add(tb_sizer, 0, wx.EXPAND) def dispose(self): """Disposes of the contents of an editor.""" if self.toolbar_ui is not None: self.toolbar_ui.dispose() if self._ui is not None: self._ui.dispose() self.grid.on_trait_change( getattr( self, "_selection_%s_updated" % self.factory.selection_mode ), "selection_changed", remove=True, ) self.model.on_trait_change(self._model_sorted, "sorted", remove=True) self.grid.dispose() self.model.dispose() # Break any links needed to allow garbage collection: self.grid = self.model = self.toolbar = None super().dispose() def update_editor(self): """Updates the editor when the object trait changes externally to the editor. """ # fixme: Do we need to override this method? pass def refresh(self): """Refreshes the editor control.""" self.grid._grid.Refresh() def set_selection(self, objects=[], notify=True): """Sets the current selection to a set of specified objects.""" if not isinstance(objects, SequenceTypes): objects = [objects] self.grid.set_selection( [TraitGridSelection(obj=object) for object in objects], notify=notify, ) def set_extended_selection(self, *pairs): """Sets the current selection to a set of specified object/column pairs. """ if (len(pairs) == 1) and isinstance(pairs[0], list): pairs = pairs[0] grid_selections = [ TraitGridSelection(obj=object, name=name) for object, name in pairs ] self.grid.set_selection(grid_selections) def create_new_row(self): """Creates a new row object using the provided factory.""" factory = self.factory kw = factory.row_factory_kw.copy() if "__table_editor__" in kw: kw["__table_editor__"] = self return self.ui.evaluate( factory.row_factory, *factory.row_factory_args, **kw ) def add_row(self, object=None, index=None): """Adds a specified object as a new row after the specified index.""" filtered_items = self.model.get_filtered_items if index is None: indices = self.selected_indices if len(indices) == 0: indices = [len(filtered_items()) - 1] indices.reverse() else: indices = [index] if object is None: objects = [] for index in indices: object = self.create_new_row() if object is None: if self.in_row_mode: self.set_selection() return objects.append(object) else: objects = [object] items = [] insert_item_after = self.model.insert_filtered_item_after in_row_mode = self.in_row_mode for i, index in enumerate(indices): object = objects[i] index, extend = insert_item_after(index, object) if in_row_mode and (object in filtered_items()): items.append(object) self._add_undo( ListUndoItem( object=self.object, name=self.name, index=index, added=[object], ), extend, ) if in_row_mode: self.set_selection(items) def move_column(self, from_column, to_column): """Moves the specified **from_column** from its current position to just preceding the specified **to_column**. """ columns = self.columns frm = columns.index(from_column) if to_column is None: to = len(columns) else: to = columns.index(to_column) del columns[frm] columns.insert(to - (frm < to), from_column) return True # -- Property Implementations --------------------------------------------- def _get_selected_indices(self): sm = self.factory.selection_mode if sm == "rows": return self.selected_row_indices elif sm == "row": index = self.selected_row_index if index >= 0: return [index] elif sm == "cells": return list({row_col[0] for row_col in self.selected_cell_indices}) elif sm == "cell": index = self.selected_cell_index[0] if index >= 0: return [index] return [] def _get_selected_items(self): sm = self.factory.selection_mode if sm == "rows": return self.selected_rows elif sm == "row": item = self.selected_row if item is not None: return [item] elif sm == "cells": return list({item_name[0] for item_name in self.selected_cells}) elif sm == "cell": item = self.selected_cell[0] if item is not None: return [item] return [] def _get_selected_values(self): if self.in_row_mode: return [(item, "") for item in self.selected_items] if self.in_column_mode: if self.factory.selection_mode == "columns": return [(None, column) for column in self.selected_columns] column = self.selected_column if column != "": return [(None, column)] return [] if self.factory.selection_mode == "cells": return self.selected_cells item = self.selected_cell if item[0] is not None: return [item] return [] def _get_in_row_mode(self): return self.factory.selection_mode in ("row", "rows") def _get_in_column_mode(self): return self.factory.selection_mode in ("column", "columns") # -- UI preference save/restore interface --------------------------------- def restore_prefs(self, prefs): """Restores any saved user preference information associated with the editor. """ factory = self.factory try: filters = prefs.get("filters", None) if filters is not None: factory.filters = [ f for f in factory.filters if f.template ] + [f for f in filters if not f.template] columns = prefs.get("columns") if columns is not None: new_columns = [] all_columns = self.columns + factory.other_columns for column in columns: for column2 in all_columns: if column == column2.get_label(): new_columns.append(column2) break self.columns = new_columns # Restore the column sizes if possible: if not factory.auto_size: widths = prefs.get("widths") if widths is not None: # fixme: Talk to Jason about a better way to do this: self.grid._user_col_size = True set_col_size = self.grid._grid.SetColSize for i, width in enumerate(widths): if width >= 0: set_col_size(i, width) structure = prefs.get("structure") if (structure is not None) and (factory.edit_view != " "): self.control.GetSizer().SetStructure(self.control, structure) except Exception: pass def save_prefs(self): """Returns any user preference information associated with the editor.""" get_col_size = self.grid._grid.GetColSize result = { "filters": [f for f in self.factory.filters if not f.template], "columns": [c.get_label() for c in self.columns], "widths": [get_col_size(i) for i in range(len(self.columns))], } if self.factory.edit_view != " ": result["structure"] = self.control.GetSizer().GetStructure() return result # -- Public Methods ------------------------------------------------------- def filter_modified(self): """Handles updating the selection when some aspect of the current filter has changed. """ values = self.selected_values if len(values) > 0: if self.in_column_mode: self.set_extended_selection(values) else: items = self.model.get_filtered_items() self.set_extended_selection( [item for item in values if item[0] in items] ) # -- Event Handlers ------------------------------------------------------- def _selection_row_updated(self, event): """Handles the user selecting items (rows, columns, cells) in the table. """ gfi = self.model.get_filtered_item rio = self.model.raw_index_of tl = self.grid._grid.GetSelectionBlockTopLeft() br = iter(self.grid._grid.GetSelectionBlockBottomRight()) rows = len(self.model.get_filtered_items()) if self.auto_add: rows -= 1 # Get the row items and indices in the selection: values = [] for row0, col0 in tl: row1, col1 = next(br) for row in range(row0, row1 + 1): if row < rows: values.append((rio(row), gfi(row))) if len(values) > 0: # Sort by increasing row index: values.sort(key=itemgetter(0)) index, row = values[0] else: index, row = -1, None # Save the new selection information: self.trait_set(selected_row_index=index, trait_change_notify=False) self.setx(selected_row=row) # Update the toolbar status: self._update_toolbar(row is not None) # Invoke the user 'on_select' handler: self.ui.evaluate(self.factory.on_select, row) def _selection_rows_updated(self, event): """Handles multiple row selection changes.""" gfi = self.model.get_filtered_item rio = self.model.raw_index_of tl = self.grid._grid.GetSelectionBlockTopLeft() br = iter(self.grid._grid.GetSelectionBlockBottomRight()) rows = len(self.model.get_filtered_items()) if self.auto_add: rows -= 1 # Get the row items and indices in the selection: values = [] for row0, col0 in tl: row1, col1 = next(br) for row in range(row0, row1 + 1): if row < rows: values.append((rio(row), gfi(row))) # Sort by increasing row index: values.sort(key=itemgetter(0)) # Save the new selection information: self.trait_set( selected_row_indices=[v[0] for v in values], trait_change_notify=False, ) rows = [v[1] for v in values] self.setx(selected_rows=rows) # Update the toolbar status: self._update_toolbar(len(values) > 0) # Invoke the user 'on_select' handler: self.ui.evaluate(self.factory.on_select, rows) def _selection_column_updated(self, event): """Handles single column selection changes.""" cols = self.columns tl = self.grid._grid.GetSelectionBlockTopLeft() br = iter(self.grid._grid.GetSelectionBlockBottomRight()) # Get the column items and indices in the selection: values = [] for row0, col0 in tl: row1, col1 = next(br) for col in range(col0, col1 + 1): values.append((col, cols[col].name)) if len(values) > 0: # Sort by increasing column index: values.sort(key=itemgetter(0)) index, column = values[0] else: index, column = -1, "" # Save the new selection information: self.trait_set(selected_column_index=index, trait_change_notify=False) self.setx(selected_column=column) # Invoke the user 'on_select' handler: self.ui.evaluate(self.factory.on_select, column) def _selection_columns_updated(self, event): """Handles multiple column selection changes.""" cols = self.columns tl = self.grid._grid.GetSelectionBlockTopLeft() br = iter(self.grid._grid.GetSelectionBlockBottomRight()) # Get the column items and indices in the selection: values = [] for row0, col0 in tl: row1, col1 = next(br) for col in range(col0, col1 + 1): values.append((col, cols[col].name)) # Sort by increasing row index: values.sort(key=itemgetter(0)) # Save the new selection information: self.trait_set( selected_column_indices=[v[0] for v in values], trait_change_notify=False, ) columns = [v[1] for v in values] self.setx(selected_columns=columns) # Invoke the user 'on_select' handler: self.ui.evaluate(self.factory.on_select, columns) def _selection_cell_updated(self, event): """Handles single cell selection changes.""" tl = self.grid._grid.GetSelectionBlockTopLeft() if len(tl) == 0: return gfi = self.model.get_filtered_item rio = self.model.raw_index_of cols = self.columns br = iter(self.grid._grid.GetSelectionBlockBottomRight()) # Get the column items and indices in the selection: values = [] for row0, col0 in tl: row1, col1 = next(br) for row in range(row0, row1 + 1): item = gfi(row) for col in range(col0, col1 + 1): values.append(((rio(row), col), (item, cols[col].name))) if len(values) > 0: # Sort by increasing row, column index: values.sort(key=itemgetter(0)) index, cell = values[0] else: index, cell = (-1, -1), (None, "") # Save the new selection information: self.trait_set(selected_cell_index=index, trait_change_notify=False) self.setx(selected_cell=cell) # Update the toolbar status: self._update_toolbar(len(values) > 0) # Invoke the user 'on_select' handler: self.ui.evaluate(self.factory.on_select, cell) def _selection_cells_updated(self, event): """Handles multiple cell selection changes.""" gfi = self.model.get_filtered_item rio = self.model.raw_index_of cols = self.columns tl = self.grid._grid.GetSelectionBlockTopLeft() br = iter(self.grid._grid.GetSelectionBlockBottomRight()) # Get the column items and indices in the selection: values = [] for row0, col0 in tl: row1, col1 = next(br) for row in range(row0, row1 + 1): item = gfi(row) for col in range(col0, col1 + 1): values.append(((rio(row), col), (item, cols[col].name))) # Sort by increasing row, column index: values.sort(key=itemgetter(0)) # Save the new selection information: self.setx(selected_cell_indices=[v[0] for v in values]) cells = [v[1] for v in values] self.setx(selected_cells=cells) # Update the toolbar status: self._update_toolbar(len(cells) > 0) # Invoke the user 'on_select' handler: self.ui.evaluate(self.factory.on_select, cells) def _selected_row_changed(self, item): if not self._no_notify: if item is None: self.set_selection(notify=False) else: self.set_selection(item, notify=False) def _selected_row_index_changed(self, row): if not self._no_notify: if row < 0: self.set_selection(notify=False) else: self.set_selection(self.value[row], notify=False) def _selected_rows_changed(self, items): if not self._no_notify: self.set_selection(items, notify=False) def _selected_row_indices_changed(self, indices): if not self._no_notify: value = self.value self.set_selection([value[i] for i in indices], notify=False) def _selected_column_changed(self, name): if not self._no_notify: self.set_extended_selection((None, name)) def _selected_column_index_changed(self, index): if not self._no_notify: if index < 0: self.set_extended_selection() else: self.set_extended_selection( (None, self.model.get_column_name(index)) ) def _selected_columns_changed(self, names): if not self._no_notify: self.set_extended_selection([(None, name) for name in names]) def _selected_column_indices_changed(self, indices): if not self._no_notify: gcn = self.model.get_column_name self.set_extended_selection([(None, gcn(i)) for i in indices]) def _selected_cell_changed(self, cell): if not self._no_notify: self.set_extended_selection([cell]) def _selected_cell_index_changed(self, pair): if not self._no_notify: row, column = pair if (row < 0) or (column < 0): self.set_extended_selection() else: self.set_extended_selection( (self.value[row], self.model.get_column_name(column)) ) def _selected_cells_changed(self, cells): if not self._no_notify: self.set_extended_selection(cells) def _selected_cell_indices_changed(self, pairs): if not self._no_notify: value = self.value gcn = self.model.get_column_name new_selection = [(value[row], gcn(col)) for row, col in pairs] self.set_extended_selection(new_selection) def _update_toolbar(self, has_selection): """Updates the toolbar after a selection change.""" toolbar = self.toolbar if toolbar is not None: no_filter = self.filter is None if has_selection: indices = self.selected_indices start = indices[0] n = len(self.model.get_filtered_items()) - 1 delete = toolbar.delete if self.auto_add: n -= 1 delete.enabled = start <= n else: delete.enabled = True deletable = self.factory.deletable if delete.enabled and callable(deletable): delete.enabled = all( deletable(item) for item in self.selected_items ) toolbar.add.enabled = toolbar.search.enabled = no_filter toolbar.move_up.enabled = no_filter and (start > 0) toolbar.move_down.enabled = no_filter and (indices[-1] < n) else: toolbar.add.enabled = toolbar.search.enabled = no_filter toolbar.delete.enabled = ( toolbar.move_up.enabled ) = toolbar.move_down.enabled = False def _model_sorted(self): """Handles the contents of the model being resorted.""" if self.toolbar is not None: self.toolbar.no_sort.enabled = True values = self.selected_values if len(values) > 0: do_later(self.set_extended_selection, values) def _filter_changed(self, old_filter, new_filter): """Handles the current filter being changed.""" if new_filter is customize_filter: do_later(self._customize_filters, old_filter) elif self.model is not None: if (new_filter is not None) and ( not isinstance(new_filter, TableFilter) ): new_filter = TableFilter(allowed=new_filter) self.model.filter = new_filter self.filter_modified() def _refresh_filters(self, filters): factory = self.factory # hack: The following line forces the 'filters' to be changed... factory.filters = [] factory.filters = filters def _customize_filters(self, filter): """Allows the user to customize the current set of table filters.""" factory = self.factory filter_editor = TableFilterEditor(editor=self, filter=filter) enum_editor = EnumEditor(values=factory.filters[:], mode="list") ui = filter_editor.edit_traits( parent=self.control, view=View( [ [ Item( "filter<200>@", editor=enum_editor, resizable=True ), "|<>", ], ["edit:edit", "new", "apply", "delete:delete", "|<>"], "-", ], title="Customize Filters", kind="livemodal", height=0.25, buttons=["OK", "Cancel"], ), ) if ui.result: self._refresh_filters(enum_editor.values) self.filter = filter_editor.filter else: self.filter = filter def on_no_sort(self): """Handles the user requesting that columns not be sorted.""" self.model.no_column_sort() self.toolbar.no_sort.enabled = False values = self.selected_values if len(values) > 0: self.set_extended_selection(values) def on_move_up(self): """Handles the user requesting to move the current item up one row.""" model = self.model objects = [] for index in self.selected_indices: objects.append(model.get_filtered_item(index)) index -= 1 object = model.get_filtered_item(index) model.delete_filtered_item_at(index) model.insert_filtered_item_after(index, object) if self.in_row_mode: self.set_selection(objects) else: self.set_extended_selection(self.selected_values) def on_move_down(self): """Handles the user requesting to move the current item down one row.""" model = self.model objects = [] indices = self.selected_indices[:] indices.reverse() for index in indices: object = model.get_filtered_item(index) objects.append(object) model.delete_filtered_item_at(index) model.insert_filtered_item_after(index, object) if self.in_row_mode: self.set_selection(objects) else: self.set_extended_selection(self.selected_values) def on_search(self): """Handles the user requesting a table search.""" self.factory.search.edit_traits( parent=self.control, view="searchable_view", handler=TableSearchHandler(editor=self), ) def on_add(self): """Handles the user requesting to add a new row to the table.""" self.add_row() def on_delete(self): """Handles the user requesting to delete the currently selected items of the table. """ # Get the selected row indices: indices = self.selected_indices[:] values = self.selected_values[:] indices.reverse() # Make sure that we don't delete any rows while an editor is open in it self.grid.stop_editing_indices(indices) # Delete the selected rows: for i in indices: index, object = self.model.delete_filtered_item_at(i) self._add_undo( ListUndoItem( object=self.object, name=self.name, index=index, removed=[object], ) ) # Compute the new selection and set it: items = self.model.get_filtered_items() n = len(items) - 1 indices.reverse() for i in range(len(indices) - 1, -1, -1): if indices[i] > n: indices[i] = n if indices[i] < 0: del indices[i] del values[i] n = len(indices) if n > 0: if self.in_row_mode: self.set_selection(list({items[i] for i in indices})) else: self.set_extended_selection( list({(items[indices[i]], values[i][1]) for i in range(n)}) ) else: self._update_toolbar(False) def on_prefs(self): """Handles the user requesting to set the user preference items for the table. """ columns = self.columns[:] columns.extend( [ c for c in (self.factory.columns + self.factory.other_columns) if c not in columns ] ) self.edit_traits( parent=self.control, view=View( [ Item( "columns", resizable=True, editor=SetEditor( values=columns, ordered=True, can_move_all=False ), ), "|<>", ], title="Select and Order Columns", width=0.3, height=0.3, resizable=True, buttons=["Undo", "OK", "Cancel"], kind="livemodal", ), ) def prepare_menu(self, row, column): """Prepares to have a context menu action called.""" object = self.model.get_filtered_item(row) selection = [x.obj for x in self.grid.get_selection()] if object not in selection: self.set_selection(object) selection = [object] self.set_menu_context(selection, object, column) def setx(self, **keywords): """Set one or more attributes without notifying the grid model.""" self._no_notify = True for name, value in keywords.items(): setattr(self, name, value) self._no_notify = False # -- Private Methods: ----------------------------------------------------- def _add_undo(self, undo_item, extend=False): history = self.ui.history if history is not None: history.add(undo_item, extend) class TableFilterEditor(Handler): """Editor that manages table filters.""" #: TableEditor this editor is associated with editor = Instance(TableEditor) #: Current filter filter = Instance(TableFilter, allow_none=True) #: Edit the current filter edit = Button() #: Create a new filter and edit it new = Button() #: Apply the current filter to the editor's table apply = Button() #: Delete the current filter delete = Button() # ------------------------------------------------------------------------- # 'Handler' interface: # ------------------------------------------------------------------------- def init(self, info): """Initializes the controls of a user interface.""" # Save both the original filter object reference and its contents: if self.filter is None: self.filter = info.filter.factory.values[0] self._filter = self.filter self._filter_copy = self.filter.clone_traits() return True def closed(self, info, is_ok): """Handles a dialog-based user interface being closed by the user.""" if not is_ok: # Restore the contents of the original filter: self._filter.copy_traits(self._filter_copy) # ------------------------------------------------------------------------- # Event handlers: # ------------------------------------------------------------------------- def object_filter_changed(self, info): """Handles a new filter being selected.""" filter = info.object.filter info.edit.enabled = not filter.template info.delete.enabled = (not filter.template) and ( len(info.filter.factory.values) > 1 ) def object_edit_changed(self, info): """Handles the user clicking the **Edit** button.""" if info.initialized: items = self.editor.model.get_filtered_items() if len(items) > 0: item = items[0] else: item = None # `item` is now either the first item in the table, or None if # the table is empty. ui = self.filter.edit(item) if ui.result: self._refresh_filters(info) def object_new_changed(self, info): """Handles the user clicking the **New** button.""" if info.initialized: # Get list of available filters and find the current filter in it: factory = info.filter.factory filters = factory.values filter = self.filter index = filters.index(filter) + 1 n = len(filters) while (index < n) and filters[index].template: index += 1 # Create a new filter based on the current filter: new_filter = filter.clone_traits() new_filter.template = False new_filter.name = new_filter._name = "New filter" # Add it to the list of filters: filters.insert(index, new_filter) self._refresh_filters(info) # Set up the new filter as the current filter and edit it: self.filter = new_filter do_later(self._delayed_edit, info) def object_apply_changed(self, info): """Handles the user clicking the **Apply** button.""" if info.initialized: self.init(info) self.editor._refresh_filters(info.filter.factory.values) self.editor.filter = self.filter def object_delete_changed(self, info): """Handles the user clicking the **Delete** button.""" # Get the list of available filters: filters = info.filter.factory.values if info.initialized: # Delete the current filter: index = filters.index(self.filter) del filters[index] # Select a new filter: if index >= len(filters): index -= 1 self.filter = filters[index] self._refresh_filters(info) # ------------------------------------------------------------------------- # Private methods: # ------------------------------------------------------------------------- def _refresh_filters(self, info): """Refresh the filter editor's list of filters.""" factory = info.filter.factory values, factory.values = factory.values, [] factory.values = values def _delayed_edit(self, info): """Edits the current filter, and deletes it if the user cancels the edit. """ ui = self.filter.edit(self.editor.model.get_filtered_item(0)) if not ui.result: self.object_delete_changed(info) else: self._refresh_filters(info) # Allow deletion as long as there is more than 1 filter: if (not self.filter.template) and len(info.filter.factory.values) > 1: info.delete.enabled = True class TableEditorToolbar(HasPrivateTraits): """Toolbar displayed in table editors.""" # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- #: Do not sort columns: no_sort = Instance( Action, { "name": "No Sorting", "tooltip": "Do not sort columns", "action": "on_no_sort", "enabled": False, "image": ImageResource("table_no_sort.png"), }, ) #: Move current object up one row: move_up = Instance( Action, { "name": "Move Up", "tooltip": "Move current item up one row", "action": "on_move_up", "enabled": False, "image": ImageResource("table_move_up.png"), }, ) #: Move current object down one row: move_down = Instance( Action, { "name": "Move Down", "tooltip": "Move current item down one row", "action": "on_move_down", "enabled": False, "image": ImageResource("table_move_down.png"), }, ) #: Search the table: search = Instance( Action, { "name": "Search", "tooltip": "Search table", "action": "on_search", "image": ImageResource("table_search.png"), }, ) #: Add a row: add = Instance( Action, { "name": "Add", "tooltip": "Insert new item", "action": "on_add", "image": ImageResource("table_add.png"), }, ) #: Delete selected row: delete = Instance( Action, { "name": "Delete", "tooltip": "Delete current item", "action": "on_delete", "enabled": False, "image": ImageResource("table_delete.png"), }, ) #: Edit the user preferences: prefs = Instance( Action, { "name": "Preferences", "tooltip": "Set user preferences for table", "action": "on_prefs", "image": ImageResource("table_prefs.png"), }, ) #: The table editor that this is the toolbar for: editor = Instance(TableEditor) #: The toolbar control: control = Any() def __init__(self, parent=None, **traits): super().__init__(**traits) editor = self.editor factory = editor.factory actions = [] if factory.sortable and (not factory.sort_model): actions.append(self.no_sort) if (not editor.in_column_mode) and factory.reorderable: actions.append(self.move_up) actions.append(self.move_down) if editor.in_row_mode and (factory.search is not None): actions.append(self.search) if factory.editable: if (factory.row_factory is not None) and (not factory.auto_add): actions.append(self.add) if (factory.deletable) and (not editor.in_column_mode): actions.append(self.delete) if factory.configurable: actions.append(self.prefs) if len(actions) > 0: toolbar = ToolBar( image_size=(16, 16), show_tool_names=False, show_divider=False, *actions, ) self.control = toolbar.create_tool_bar(parent, self) self.control.SetBackgroundColour(parent.GetBackgroundColour()) # fixme: Why do we have to explictly set the size of the toolbar? # Is there some method that needs to be called to do the # layout? self.control.SetSize(wx.Size(23 * len(actions), 16)) # ------------------------------------------------------------------------- # Pyface/Traits menu/toolbar controller interface: # ------------------------------------------------------------------------- def add_to_menu(self, menu_item): """Adds a menu item to the menu bar being constructed.""" pass def add_to_toolbar(self, toolbar_item): """Adds a toolbar item to the too bar being constructed.""" pass def can_add_to_menu(self, action): """Returns whether the action should be defined in the user interface.""" return True def can_add_to_toolbar(self, action): """Returns whether the toolbar action should be defined in the user interface. """ return True def perform(self, action, action_event=None): """Performs the action described by a specified Action object.""" getattr(self.editor, action.action)() class TableSearchHandler(Handler): """Handler for saerching a table.""" # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- #: The editor that this handler is associated with editor = Instance(TableEditor) #: Find next matching item find_next = Button("Find Next") #: Find previous matching item find_previous = Button("Find Previous") #: Select all matching items select = Button() #: The user is finished searching OK = Button("Close") #: Search status message: status = Str() def handler_find_next_changed(self, info): """Handles the user clicking the **Find** button.""" if info.initialized: editor = self.editor items = editor.model.get_filtered_items() for i in range(editor.selected_row_index + 1, len(items)): if info.object.filter(items[i]): self.status = "Item %d matches" % (i + 1) editor.set_selection(items[i]) editor.selected_row_index = i break else: self.status = "No more matches found" def handler_find_previous_changed(self, info): """Handles the user clicking the **Find previous** button.""" if info.initialized: editor = self.editor items = editor.model.get_filtered_items() for i in range(editor.selected_row_index - 1, -1, -1): if info.object.filter(items[i]): self.status = "Item %d matches" % (i + 1) editor.set_selection(items[i]) editor.selected_row_index = i break else: self.status = "No more matches found" def handler_select_changed(self, info): """Handles the user clicking the **Select** button.""" if info.initialized: editor = self.editor filter = info.object.filter items = [ item for item in editor.model.get_filtered_items() if filter(item) ] editor.set_selection(items) if len(items) == 1: self.status = "1 item selected" else: self.status = "%d items selected" % len(items) def handler_OK_changed(self, info): """Handles the user clicking the OK button.""" if info.initialized: self.close(info, True) # Define the SimpleEditor class. SimpleEditor = TableEditor # Define the ReadonlyEditor class. ReadonlyEditor = TableEditor ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/wx/table_model.py0000644000175100001730000006323300000000000021317 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Defines the table grid model used by the table editor based on the Pyface grid control. """ import logging import wx import wx.grid as wxg from traits.api import ( HasPrivateTraits, Any, Str, Instance, Event, Bool, observe, ) from traits.observation.api import trait from traitsui.api import View, Item, Editor from traitsui.editors.table_editor import ReversedList from traitsui.table_filter import TableFilter from traitsui.ui_traits import SequenceTypes from pyface.ui.wx.grid.api import GridModel, GridSortEvent from pyface.ui.wx.grid.trait_grid_cell_adapter import TraitGridCellAdapter from pyface.timer.api import do_later logger = logging.getLogger(__name__) class TraitGridSelection(HasPrivateTraits): """Structure for holding specification information.""" #: The selected object obj = Any() #: The specific trait selected on the object name = Str() class TableModel(GridModel): """Model for table data.""" # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- #: The editor that created this model editor = Instance(Editor) #: The current filter filter = Instance(TableFilter, allow_none=True) #: Current filter summary message filter_summary = Str("All items") #: Display the table items in reverse order? reverse = Bool(False) #: Event fired when the table has been sorted sorted = Event() #: The current 'auto_add' row auto_add_row = Any() def __init__(self, **traits): """Initializes the object.""" super().__init__(**traits) # Attach trait handlers to the list object: editor = self.editor object = editor.context_object name = " " + editor.extended_name # Set up listeners for any of the model data changing: object.on_trait_change(self._on_data_changed, name, dispatch="ui") object.on_trait_change( self.fire_content_changed, name + ".-", dispatch="ui" ) # Set up listeners for any column definitions changing: editor.on_trait_change(self.update_columns, "columns", dispatch="ui") editor.on_trait_change( self.update_columns, "columns_items", dispatch="ui" ) # Initialize the current filter from the editor's default filter: self.filter = editor.filter # If we are using 'auto_add' mode, create the first 'auto_add' row: if editor.auto_add: self.auto_add_row = row = editor.create_new_row() if row is not None: row.on_trait_change(self.on_auto_add_row, dispatch="ui") # -- TableModel Interface ------------------------------------------------- def dispose(self): """Disposes of the model when it is no longer needed.""" editor = self.editor object = editor.context_object name = " " + editor.extended_name # Remove listeners for any of the model data changing: object.on_trait_change(self._on_data_changed, name, remove=True) object.on_trait_change( self.fire_content_changed, name + ".-", remove=True ) # Remove listeners for any column definitions changing: editor.on_trait_change(self.update_columns, "columns", remove=True) editor.on_trait_change( self.update_columns, "columns_items", remove=True ) # Make sure we have removed listeners from the current filter also: if self.filter is not None: self.filter.on_trait_change(self._filter_modified, remove=True) # Clean-up any links that should be broken: self.editor = None def get_filtered_items(self): """Returns all model items matching the current filter.""" return self.__filtered_items() def get_filtered_item(self, index=0): """Returns a single specified item from those items matching the current filter. """ try: return self.__filtered_items()[index] except Exception: logger.exception( "TableModel error: Request for invalid row %d out of %d", index, len(self.__filtered_items()), ) return None def raw_index_of(self, row): """Returns the raw, unfiltered index corresponding to a specified filtered index. """ if self._filtered_cache is None: return row return self.editor.filtered_indices[row] def insert_filtered_item_after(self, index, item): """Inserts an object after a specified filtered index.""" mapped_index = 0 n = len(self.editor.filtered_indices) if index >= n: if (index != 0) or (n != 0): raise IndexError elif index >= 0: mapped_index = self.editor.filtered_indices[index] + 1 self.__items().insert(mapped_index, item) sorted = self._sort_model() if sorted: mapped_index = self.__items().index(item) self._filtered_cache = None return (mapped_index, sorted) def delete_filtered_item_at(self, index): """Deletes the object at the specified filtered index.""" if index >= len(self.editor.filtered_indices): raise IndexError mapped_index = self.editor.filtered_indices[index] items = self.__items() object = items[mapped_index] del items[mapped_index] self._filtered_cache = None return (mapped_index, object) def update_columns(self): """Updates the table view when columns have been changed.""" self._columns = None self.fire_structure_changed() self.editor.refresh() def no_column_sort(self): """Resets any sorting being performed on the underlying model.""" self._sorter = self._filtered_cache = None self.column_sorted = GridSortEvent(index=-1) # self.fire_structure_changed() # -- Event Handlers ------------------------------------------------------- @observe(trait("filter").match(lambda name, ctrait: True)) def _filter_modified(self, event): """Handles the contents of the filter being changed.""" self._filtered_cache = None self.fire_structure_changed() self.editor.filter_modified() def _click_changed(self, event): """Handles the grid firing a 'click' event.""" row, col = event # Fire the same event on the editor after mapping it to a model object # and column name: object = self.get_filtered_item(row) column = self.__get_column(col) self.editor.click = (object, column) # Check to see if the column has a view to display: view = column.get_view(object) if view is not None: column.get_object(object).edit_traits( view=view, parent=self._bounds_for(row, col) ) # Invoke the column's click handler: column.on_click(object) def _dclick_changed(self, event): """Handles the grid firing a 'dclick' event.""" row, col = event # Fire the same event on the editor after mapping it to a model object # and column name: object = self.get_filtered_item(row) column = self.__get_column(col) self.editor.dclick = (object, column) # Invoke the column's double-click handler: column.on_dclick(object) def on_auto_add_row(self): """Handles the user modifying the current 'auto_add' mode row.""" object = self.auto_add_row object.on_trait_change(self.on_auto_add_row, remove=True) self.auto_add_row = row = self.editor.create_new_row() if row is not None: row.on_trait_change(self.on_auto_add_row, dispatch="ui") do_later( self.editor.add_row, object, len(self.get_filtered_items()) - 2 ) # -- GridModel Interface -------------------------------------------------- def get_column_count(self): """Returns the number of columns for this table.""" return len(self.__get_columns()) def get_column_name(self, index): """Returns the label of the column specified by the (zero-based) index.""" return self.__get_column(index).get_label() def get_column_size(self, index): """Returns the size in pixels of the column indexed by *index*. A value of -1 or None means to use the default. """ return self.__get_column(index).get_width() def get_cols_drag_value(self, cols): """Returns the value to use when the specified columns are dragged or copied and pasted. The parameter *cols* is a list of column indexes. """ return [self.__get_data_column(col) for col in cols] def get_cols_selection_value(self, cols): """Returns a list of TraitGridSelection objects containing the objects corresponding to the grid rows and the traits corresponding to the specified columns. """ values = [] for obj in self.__items(False): values.extend( [ TraitGridSelection( obj=obj, name=self.__get_column_name(col) ) for col in cols ] ) return values def sort_by_column(self, col, reverse=False): """Sorts the model data by the column indexed by *col*.""" # Make sure we allow sorts by column: factory = self.editor.factory if not factory.sortable: return # Flush the object cache: self._filtered_cache = None # Cache the sorting information for later: self._sorter = self.__get_column(col).key self._reverse = reverse # If model sorting is requested, do it now: self._sort_model() # Indicate the we have been sorted: self.sorted = True self.column_sorted = GridSortEvent(index=col, reversed=reverse) def is_column_read_only(self, index): """Returns True if the column specified by the zero-based *index* is read-only. """ return not self.__get_column(index).editable def get_row_count(self): """Return the number of rows for this table.""" return len(self.__filtered_items()) def get_row_name(self, index): """Return the name of the row specified by the (zero-based) *index*.""" return "" def get_rows_drag_value(self, rows): """Returns the value to use when the specified rows are dragged or copied and pasted. The parameter *rows* is a list of row indexes. If there is only one row listed, then return the corresponding trait object. If more than one row is listed, then return a list of objects. """ items = self.__filtered_items() return [items[row] for row in rows] def get_rows_selection_value(self, rows): """Returns a list of TraitGridSelection objects containing the object corresponding to the selected rows. """ items = self.__filtered_items() return [TraitGridSelection(obj=items[row]) for row in rows] def is_row_read_only(self, index): """Returns True if the row specified by the zero-based *index* is read-only. """ return False def get_cell_editor(self, row, col): """Returns the editor for the specified cell.""" if self.editor is None: return None column = self.__get_column(col) object = self.get_filtered_item(row) editor = column.get_editor(object) if editor is None: return None editor._ui = self.editor.ui target, name = column.target_name(object) return TraitGridCellAdapter( editor, target, name, "", context=self.editor.ui.context, style=column.get_style(object), width=column.get_edit_width(object), height=column.get_edit_height(object), ) def get_cell_renderer(self, row, col): """Returns the renderer for the specified cell.""" return self.__get_column(col).get_renderer(self.get_filtered_item(row)) def get_cell_drag_value(self, row, col): """Returns the value to use when the specified cell is dragged or copied and pasted. """ return self.__get_column(col).get_drag_value( self.get_filtered_item(row) ) def get_cell_selection_value(self, row, col): """Returns a TraitGridSelection object specifying the data stored in the table at (*row*, *col*). """ return TraitGridSelection( obj=self.get_filtered_item(row), name=self.__get_column_name(col) ) def resolve_selection(self, selection_list): """Returns a list of (row, col) grid-cell coordinates that correspond to the objects in *selection_list*. For each coordinate, if the row is -1, it indicates that the entire column is selected. Likewise coordinates with a column of -1 indicate an entire row that is selected. For the TableModel, the objects in *selection_list* must be TraitGridSelection objects. """ items = self.__filtered_items() cells = [] for selection in selection_list: row = -1 if selection.obj is not None: try: row = items.index(selection.obj) except ValueError: continue column = -1 if selection.name != "": column = self._get_column_index_by_trait(selection.name) if column is None: continue cells.append((row, column)) return cells def get_cell_context_menu(self, row, col): """Returns a Menu object that generates the appropriate context menu for this cell. """ column = self.__get_column(col) menu = column.get_menu(self.get_filtered_item(row)) editor = self.editor if menu is None: menu = editor.factory.menu if menu is None: menu_name = editor.factory.menu_name if menu_name: menu = getattr(self.editor.object, menu_name, None) if menu is not None: editor.prepare_menu(row, column) return (menu, editor) return None def get_value(self, row, col): """Returns the value stored in the table at (*row*, *col*).""" object = self.get_filtered_item(row) if object is self.auto_add_row: return "" value = self.__get_column(col).get_value(object) formats = self.__get_column_formats(col) if (value is not None) and (formats is not None): format = formats.get(type(value)) if format is not None: try: if callable(format): value = format(value) else: value = format % value except: pass return value def is_valid_cell_value(self, row, col, value): """Tests whether *value* is valid for the cell at (*row*, *col*). Returns True if value is acceptable, and False otherwise.""" return self.__get_column(col).is_droppable( self.get_filtered_item(row), value ) def is_cell_empty(self, row, col): """Returns True if the cell at (*row*, *col*) has a None value, and False otherwise. """ return self.get_value(row, col) is None def is_cell_read_only(self, row, col): """Returns True if the cell at (*row*, *col*) is read-only, and False otherwise. """ return not self.__get_column(col).is_editable( self.get_filtered_item(row) ) def get_cell_bg_color(self, row, col): """Returns a wxColour object specifying the background color of the specified cell. """ return self.__get_column(col).get_cell_color( self.get_filtered_item(row) ) def get_cell_text_color(self, row, col): """Returns a wxColour object specifying the text color of the specified cell. """ column = self.__get_column(col) item = self.get_filtered_item(row) return column.get_text_color(item) def get_cell_font(self, row, col): """Returns a wxFont object specifying the font of the specified cell.""" return self.__get_column(col).get_text_font( self.get_filtered_item(row) ) def get_cell_halignment(self, row, col): """Returns a string specifying the horizontal alignment of the specified cell. Returns 'left' for left alignment, 'right' for right alignment, or 'center' for center alignment. """ return self.__get_column(col).get_horizontal_alignment( self.get_filtered_item(row) ) def get_cell_valignment(self, row, col): """Returns a string specifying the vertical alignment of the specified cell. Returns 'top' for top alignment, 'bottom' for bottom alignment, or 'center' for center alignment. """ return self.__get_column(col).get_vertical_alignment( self.get_filtered_item(row) ) def _insert_rows(self, pos, num_rows): """Inserts *num_rows* at *pos*; fires an event only if a factory method for new rows is defined or the model is not empty. Otherwise, it returns 0. """ count = 0 factory = self.editor.factory.row_factory if factory is None: items = self.__items(False) if len(items) > 0: factory = items[0].__class__ if factory is not None: new_data = [ x for x in [factory() for i in range(num_rows)] if x is not None ] if len(new_data) > 0: count = self._insert_rows_into_model(pos, new_data) self.rows_added = ("added", pos, new_data) return count def _delete_rows(self, pos, num_rows): """Removes rows *pos* through *pos* + *num_rows* from the model.""" row_count = self.get_rows_count() if (pos + num_rows) > row_count: num_rows = row_count - pos return self._delete_rows_from_model(pos, num_rows) def _set_value(self, row, col, value): """Sets the value of the cell at (*row*, *col*) to *value*. Raises a ValueError if the value is vetoed or the cell at the specified position does not exist. """ new_rows = 0 column = self.__get_column(col) obj = None try: obj = self.get_filtered_item(row) except: # Add a new row: new_rows = self._insert_rows(self.get_row_count(), 1) if new_rows > 0: # Now set the value on the new object: try: obj = self.get_filtered_item(self.get_row_count() - 1) except: # fixme: what do we do in this case? veto the set somehow? # raise an exception? pass if obj is not None: self._set_data_on_row(obj, column, value) return new_rows def _move_column(self, frm, to): """Moves a specified **frm** column to before the specified **to** column. Returns **True** if successful; **False** otherwise. """ to_column = None if to < len(self.__get_columns()): to_column = self.__get_column(to) return self.editor.move_column(self.__get_column(frm), to_column) def _set_data_on_row(self, row, column, value): """Sets the cell specified by (*row*, *col*) to *value, which can be either a member of the row object, or a no-argument method on that object. """ column.set_value(row, value) def _insert_rows_into_model(self, pos, new_data): """Inserts the given new rows into the model.""" raw_pos = self.raw_index_of(pos) self.__items()[raw_pos:raw_pos] = new_data def _delete_rows_from_model(self, pos, num_rows): """Deletes the specified rows from the model.""" raw_rows = sorted( [self.raw_index_of(i) for i in range(pos, pos + num_rows)] ) raw_rows.reverse() items = self.__items() for row in raw_rows: del items[row] return num_rows def _on_data_changed(self): """Forces the grid to refresh when the underlying list changes.""" # Invalidate the current cache (if any): self._filtered_cache = None self.fire_structure_changed() def _mouse_cell_changed(self, new): """Handles the user mousing over a specified cell.""" row, col = new column = self.__get_column(col) object = self.get_filtered_item(row) # Update the tooltip if necessary: tooltip = column.get_tooltip(object) if tooltip != self._tooltip: self._tooltip = tooltip self.editor.grid._grid_window.SetToolTip(wx.ToolTip(tooltip)) if column.is_auto_editable(object): x, y, dx, dy = self._bounds_for(row, col) if column.is_editable(object): view = View( Item( name=column.name, editor=column.get_editor(object), style=column.get_style(object), show_label=False, padding=-4, ), kind="info", width=dx, height=dy, ) else: view = column.get_view(object) if view is None: return column.get_object(object).edit_traits( view=view, parent=(x, y, dx, dy) ) def _bounds_for(self, row, col): """Returns the coordinates and size of the specified cell in the form: ( x, y, dx, dy ). """ grid = self.editor.grid coords = wxg.GridCellCoords(row, col) x, y, dx, dy = grid._grid.BlockToDeviceRect(coords, coords) x, y = grid._grid_window.ClientToScreen(x, y) return (x, y, dx, dy) def _sort_model(self): """Sorts the underlying model if that is what the user requested.""" editor = self.editor sorted = editor.factory.sort_model and (self._sorter is not None) if sorted: items = self.__items(False)[:] items.sort(key=self._sorter) if self.reverse ^ self._reverse: items.reverse() editor.value = items return sorted def __items(self, ordered=True): """Returns the raw list of model objects.""" result = self.editor.value if not isinstance(result, SequenceTypes): return [result] if ordered and self.reverse: return ReversedList(result) return result def __filtered_items(self): """Returns the list of all model objects that pass the current filter.""" fc = self._filtered_cache if fc is None: items = self.__items() filter = self.filter if filter is None: nitems = [nitem for nitem in enumerate(items)] self.filter_summary = "All %s items" % len(nitems) else: if not callable(filter): filter = filter.filter nitems = [ nitem for nitem in enumerate(items) if filter(nitem[1]) ] self.filter_summary = "%s of %s items" % ( len(nitems), len(items), ) sorter = self._sorter if sorter is not None: nitems.sort(key=lambda x: sorter(x[1])) if self._reverse: nitems.reverse() self.editor.filtered_indices = [x[0] for x in nitems] self._filtered_cache = fc = [x[1] for x in nitems] if self.auto_add_row is not None: self._filtered_cache.append(self.auto_add_row) return fc def __get_data_column(self, col): """Returns a list of model data from the column indexed by *col*.""" column = self.__get_column(col) return [column.get_value(item) for item in self.__filtered_items()] def __get_columns(self): columns = self._columns if columns is None: self._columns = columns = [ c for c in self.editor.columns if c.visible ] return columns def __get_column(self, col): try: return self.__get_columns()[col] except: return self.__get_columns()[0] def __get_column_name(self, col): return self.__get_column(col).name def __get_column_formats(self, col): return None # Not used/implemented currently def _get_column_index_by_trait(self, name): for i, col in enumerate(self.__get_columns()): if name == col.name: return i ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/wx/tabular_editor.py0000644000175100001730000012164500000000000022052 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ A traits UI editor for editing tabular data (arrays, list of tuples, lists of objects, etc). """ import wx import wx.lib.mixins.listctrl as listmix from traits.api import ( HasStrictTraits, Int, List, Bool, Instance, Any, Event, Property, TraitListEvent, Dict, ) from traitsui.ui_traits import Image from traitsui.tabular_adapter import TabularAdapter from traitsui.wx.editor import Editor from pyface.image_resource import ImageResource from pyface.timer.api import do_later from .constants import is_mac, scrollbar_dx try: from pyface.wx.drag_and_drop import PythonDropSource, PythonDropTarget except: PythonDropSource = PythonDropTarget = None # Mapping for trait alignment values to wx alignment values: alignment_map = { "left": wx.LIST_FORMAT_LEFT, "center": wx.LIST_FORMAT_CENTRE, "right": wx.LIST_FORMAT_RIGHT, } class TextEditMixin(listmix.TextEditMixin): def __init__(self, edit_labels): """edit_labels controls whether the first column is editable""" self.edit_labels = edit_labels listmix.TextEditMixin.__init__(self) def OpenEditor(self, col, row): if col == 0 and not self.edit_labels: return else: return listmix.TextEditMixin.OpenEditor(self, col, row) class wxListCtrl(wx.ListCtrl, TextEditMixin): """Subclass of wx.ListCtrl to provide correct virtual list behavior.""" def __init__( self, parent, ID, pos=wx.DefaultPosition, size=wx.DefaultSize, style=0, can_edit=False, edit_labels=False, ): wx.ListCtrl.__init__(self, parent, ID, pos, size, style) # if the selected is editable, then we have to init the mixin if can_edit: TextEditMixin.__init__(self, edit_labels) def make_editor(self, col_style=wx.LIST_FORMAT_LEFT): # override implementation in base class due to issue with destroying # editor style = wx.TE_PROCESS_ENTER | wx.TE_PROCESS_TAB | wx.TE_RICH2 style |= { wx.LIST_FORMAT_LEFT: wx.TE_LEFT, wx.LIST_FORMAT_RIGHT: wx.TE_RIGHT, wx.LIST_FORMAT_CENTRE: wx.TE_CENTRE, }[col_style] editor = wx.TextCtrl(self, -1, style=style) editor.SetBackgroundColour(self.editorBgColour) editor.SetForegroundColour(self.editorFgColour) font = self.GetFont() editor.SetFont(font) self.curRow = 0 self.curCol = 0 editor.Hide() # Base class does explicit Destroy call here. Should not be needed. # Excised code is as follows: # if hasattr(self, 'editor'): # self.editor.Destroy() # Dropping the reference to the editor should result in the # destruction of the underlying C++ widget just fine. self.editor = editor self.col_style = col_style self.editor.Bind(wx.EVT_CHAR, self.OnChar) self.editor.Bind(wx.EVT_KILL_FOCUS, self.CloseEditor) def SetVirtualData(self, row, col, text): # this method is called but the job is already done by # the _end_label_edit method. Commmented code is availabed # if needed pass # edit = self._editor # return editor.adapter.set_text( editor.object, editor.name, # row, col, text ) def OnGetItemAttr(self, row): """Returns the display attributes to use for the specified list item.""" # fixme: There appears to be a bug in wx in that they do not correctly # manage the reference count for the returned object, and it seems to be # gc'ed before they finish using it. So we store an object reference to # it to prevent it from going away too soon... self._attr = attr = wx.ItemAttr() editor = self._editor object, name = editor.object, editor.name color = editor.adapter.get_bg_color(object, name, row) if color is not None: attr.SetBackgroundColour(color) color = editor.adapter.get_text_color(object, name, row) if color is not None: attr.SetTextColour(color) font = editor.adapter.get_font(object, name, row) if font is not None: attr.SetFont(font) return attr def OnGetItemImage(self, row): """Returns the image index to use for the specified list item.""" editor = self._editor image = editor._get_image( editor.adapter.get_image(editor.object, editor.name, row, 0) ) if image is not None: return image return -1 def OnGetItemColumnImage(self, row, column): """Returns the image index to use for the specified list item.""" editor = self._editor image = editor._get_image( editor.adapter.get_image(editor.object, editor.name, row, column) ) if image is not None: return image return -1 def OnGetItemText(self, row, column): """Returns the text to use for the specified list item.""" editor = self._editor return editor.adapter.get_text(editor.object, editor.name, row, column) class TabularEditor(Editor): """A traits UI editor for editing tabular data (arrays, list of tuples, lists of objects, etc). """ # -- Trait Definitions ---------------------------------------------------- #: The event fired when a table update is needed: update = Event() #: The event fired when a simple repaint is needed: refresh = Event() #: The current set of selected items (which one is used depends upon the #: initial state of the editor factory 'multi_select' trait): selected = Any() multi_selected = List() #: The current set of selected item indices (which one is used depends upon #: the initial state of the editor factory 'multi_select' trait): selected_row = Int(-1) multi_selected_rows = List(Int) #: The most recently actived item and its index: activated = Any() activated_row = Int() #: The most recent left click data: clicked = Instance("TabularEditorEvent") #: The most recent left double click data: dclicked = Instance("TabularEditorEvent") #: The most recent right click data: right_clicked = Instance("TabularEditorEvent") #: The most recent right double click data: right_dclicked = Instance("TabularEditorEvent") #: The most recent column click data: column_clicked = Instance("TabularEditorEvent") #: Is the tabular editor scrollable? This value overrides the default. scrollable = True #: Row index of item to select after rebuilding editor list: row = Any() #: Should the selected item be edited after rebuilding the editor list: edit = Bool(False) #: The adapter from trait values to editor values: adapter = Instance(TabularAdapter) #: Dictionary mapping image names to wx.ImageList indices: images = Dict() #: Dictionary mapping ImageResource objects to wx.ImageList indices: image_resources = Dict() #: An image being converted: image = Image #: Flag for marking whether the update was within the visible area _update_visible = Bool(False) def init(self, parent): """Finishes initializing the editor by creating the underlying toolkit widget. """ factory = self.factory # Set up the adapter to use: self.adapter = factory.adapter # Determine the style to use for the list control: style = wx.LC_REPORT | wx.LC_VIRTUAL | wx.BORDER_NONE if factory.editable_labels: style |= wx.LC_EDIT_LABELS if factory.horizontal_lines: style |= wx.LC_HRULES if factory.vertical_lines: style |= wx.LC_VRULES if not factory.multi_select: style |= wx.LC_SINGLE_SEL if not factory.show_titles: style |= wx.LC_NO_HEADER # Create the list control and link it back to us: self.control = control = wxListCtrl( parent, -1, style=style, can_edit=factory.editable, edit_labels=factory.editable_labels, ) control._editor = self # Create the list control column: # fixme: what do we do here? # control.InsertColumn( 0, '' ) # Set up the list control's event handlers: id = control.GetId() parent.Bind(wx.EVT_LIST_BEGIN_DRAG, self._begin_drag, id=id) parent.Bind( wx.EVT_LIST_BEGIN_LABEL_EDIT, self._begin_label_edit, id=id ) parent.Bind(wx.EVT_LIST_END_LABEL_EDIT, self._end_label_edit, id=id) parent.Bind(wx.EVT_LIST_ITEM_SELECTED, self._item_selected, id=id) parent.Bind(wx.EVT_LIST_ITEM_DESELECTED, self._item_selected, id=id) parent.Bind(wx.EVT_LIST_KEY_DOWN, self._key_down, id=id) parent.Bind(wx.EVT_LIST_ITEM_ACTIVATED, self._item_activated, id=id) parent.Bind(wx.EVT_LIST_COL_END_DRAG, self._size_modified, id=id) parent.Bind( wx.EVT_LIST_COL_RIGHT_CLICK, self._column_right_clicked, id=id ) parent.Bind(wx.EVT_LIST_COL_CLICK, self._column_clicked, id=id) control.Bind(wx.EVT_LEFT_DOWN, self._left_down) control.Bind(wx.EVT_LEFT_DCLICK, self._left_dclick) control.Bind(wx.EVT_RIGHT_DOWN, self._right_down) control.Bind(wx.EVT_RIGHT_DCLICK, self._right_dclick) control.Bind(wx.EVT_MOTION, self._motion) control.Bind(wx.EVT_SIZE, self._size_modified) # Set up the drag and drop target: if PythonDropTarget is not None: control.SetDropTarget(PythonDropTarget(self)) # Set up the selection listener (if necessary): if factory.multi_select: self.sync_value( factory.selected, "multi_selected", "both", is_list=True ) self.sync_value( factory.selected_row, "multi_selected_rows", "both", is_list=True, ) else: self.sync_value(factory.selected, "selected", "both") self.sync_value(factory.selected_row, "selected_row", "both") # Synchronize other interesting traits as necessary: self.sync_value(factory.update, "update", "from", is_event=True) self.sync_value(factory.refresh, "refresh", "from", is_event=True) self.sync_value(factory.activated, "activated", "to") self.sync_value(factory.activated_row, "activated_row", "to") self.sync_value(factory.clicked, "clicked", "to") self.sync_value(factory.dclicked, "dclicked", "to") self.sync_value(factory.right_clicked, "right_clicked", "to") self.sync_value(factory.right_dclicked, "right_dclicked", "to") self.sync_value(factory.column_clicked, "column_clicked", "to") # Make sure we listen for 'items' changes as well as complete list # replacements: try: self.context_object.on_trait_change( self.update_editor, self.extended_name + "_items", dispatch="ui", ) except: pass # If the user has requested automatic update, attempt to set up the # appropriate listeners: if factory.auto_update: self.context_object.on_trait_change( self.refresh_editor, self.extended_name + ".-", dispatch="ui" ) # Create the mapping from user supplied images to wx.ImageList indices: for image_resource in factory.images: self._add_image(image_resource) # Refresh the editor whenever the adapter changes: self.on_trait_change(self._refresh, "adapter.+update", dispatch="ui") # Rebuild the editor columns and headers whenever the adapter's # 'columns' changes: self.on_trait_change( self._rebuild_all, "adapter.columns", dispatch="ui" ) # Make sure the tabular view gets initialized: self._rebuild() # Set the list control's tooltip: self.set_tooltip() def dispose(self): """Disposes of the contents of an editor.""" # Remove all of the wx event handlers: control = self.control parent = control.GetParent() id = control.GetId() parent.Bind(wx.EVT_LIST_BEGIN_DRAG, None, id=id) parent.Bind(wx.EVT_LIST_BEGIN_LABEL_EDIT, None, id=id) parent.Bind(wx.EVT_LIST_END_LABEL_EDIT, None, id=id) parent.Bind(wx.EVT_LIST_ITEM_SELECTED, None, id=id) parent.Bind(wx.EVT_LIST_ITEM_DESELECTED, None, id=id) parent.Bind(wx.EVT_LIST_KEY_DOWN, None, id=id) parent.Bind(wx.EVT_LIST_ITEM_ACTIVATED, None, id=id) parent.Bind(wx.EVT_LIST_COL_END_DRAG, None, id=id) parent.Bind(wx.EVT_LIST_COL_RIGHT_CLICK, None, id=id) parent.Bind(wx.EVT_LIST_COL_CLICK, None, id=id) control.Unbind(wx.EVT_LEFT_DOWN) control.Unbind(wx.EVT_LEFT_DCLICK) control.Unbind(wx.EVT_RIGHT_DOWN) control.Unbind(wx.EVT_RIGHT_DCLICK) control.Unbind(wx.EVT_MOTION) control.Unbind(wx.EVT_SIZE) self.context_object.on_trait_change( self.update_editor, self.extended_name + "_items", remove=True ) if self.factory.auto_update: self.context_object.on_trait_change( self.refresh_editor, self.extended_name + ".-", remove=True ) self.on_trait_change(self._refresh, "adapter.+update", remove=True) self.on_trait_change(self._rebuild_all, "adapter.columns", remove=True) super().dispose() def _update_changed(self, event): """Handles the 'update' event being fired.""" if event is True: self.update_editor() elif isinstance(event, int): self._refresh_row(event) else: self._refresh_editor(event) def refresh_editor(self, item, name, old, new): """Handles a table item attribute being changed.""" self._refresh_editor(item) def _refresh_editor(self, item): """Handles a table item being changed.""" adapter = self.adapter object, name = self.object, self.name agi = adapter.get_item for row in range(adapter.len(object, name)): if item is agi(object, name, row): self._refresh_row(row) return self.update_editor() def _refresh_row(self, row): """Updates the editor control when a specified table row changes.""" self.control.RefreshRect( self.control.GetItemRect(row, wx.LIST_RECT_BOUNDS) ) def _update_editor(self, object, name, old_value, new_value): """Performs updates when the object trait changes. Overloads traitsui.editor.UIEditor """ self._update_visible = True super()._update_editor(object, name, old_value, new_value) def update_editor(self): """Updates the editor when the object trait changes externally to the editor. """ control = self.control n = self.adapter.len(self.object, self.name) top = control.GetTopItem() pn = control.GetCountPerPage() bottom = min(top + pn - 1, n) control.SetItemCount(n) if self._update_visible: control.RefreshItems(0, n - 1) self._update_visible = False if len(self.multi_selected_rows) > 0: self._multi_selected_rows_changed(self.multi_selected_rows) if len(self.multi_selected) > 0: self._multi_selected_changed(self.multi_selected) edit, self.edit = self.edit, False row, self.row = self.row, None if row is not None: if row >= n: row -= 1 if row < 0: row = None if row is None: visible = bottom if visible >= 0 and visible < control.GetItemCount(): control.ScrollHint.EnsureVisible(visible) return if 0 <= (row - top) < pn: control.ScrollHint.EnsureVisible( min(top + pn - 2, control.GetItemCount() - 1) ) elif row < top: control.ScrollHint.EnsureVisible( min(row + pn - 1, control.GetItemCount() - 1) ) else: control.ScrollHint.EnsureVisible(row) control.SetItemState( row, wx.LIST_STATE_SELECTED | wx.LIST_STATE_FOCUSED, wx.LIST_STATE_SELECTED | wx.LIST_STATE_FOCUSED, ) if edit: control.EditLabel(row) # -- Trait Event Handlers ------------------------------------------------- def _selected_changed(self, selected): """Handles the editor's 'selected' trait being changed.""" if not self._no_update: if selected is None: for row in self._get_selected(): self.control.SetItemState(row, 0, wx.LIST_STATE_SELECTED) else: try: self.control.SetItemState( self.value.index(selected), wx.LIST_STATE_SELECTED, wx.LIST_STATE_SELECTED, ) except: pass def _selected_row_changed(self, old, new): """Handles the editor's 'selected_index' trait being changed.""" if not self._no_update: if new < 0: if old >= 0: self.control.SetItemState(old, 0, wx.LIST_STATE_SELECTED) else: self.control.SetItemState( new, wx.LIST_STATE_SELECTED, wx.LIST_STATE_SELECTED ) def _multi_selected_changed(self, selected): """Handles the editor's 'multi_selected' trait being changed.""" if not self._no_update: values = self.value try: self._multi_selected_rows_changed( [values.index(item) for item in selected] ) except: pass def _multi_selected_items_changed(self, event): """Handles the editor's 'multi_selected' trait being modified.""" values = self.value try: self._multi_selected_rows_items_changed( TraitListEvent( index=0, removed=[values.index(item) for item in event.removed], added=[values.index(item) for item in event.added], ) ) except: pass def _multi_selected_rows_changed(self, selected_rows): """Handles the editor's 'multi_selected_rows' trait being changed.""" if not self._no_update: control = self.control selected = self._get_selected() # Select any new items that aren't already selected: for row in selected_rows: if row in selected: selected.remove(row) else: control.SetItemState( row, wx.LIST_STATE_SELECTED, wx.LIST_STATE_SELECTED ) # Unselect all remaining selected items that aren't selected now: for row in selected: control.SetItemState(row, 0, wx.LIST_STATE_SELECTED) def _multi_selected_rows_items_changed(self, event): """Handles the editor's 'multi_selected_rows' trait being modified.""" control = self.control # Remove all items that are no longer selected: for row in event.removed: control.SetItemState(row, 0, wx.LIST_STATE_SELECTED) # Select all newly added items: for row in event.added: control.SetItemState( row, wx.LIST_STATE_SELECTED, wx.LIST_STATE_SELECTED ) def _refresh_changed(self): self.update_editor() # -- List Control Event Handlers ------------------------------------------ def _left_down(self, event): """Handles the left mouse button being pressed.""" self._mouse_click(event, "clicked") def _left_dclick(self, event): """Handles the left mouse button being double clicked.""" self._mouse_click(event, "dclicked") def _right_down(self, event): """Handles the right mouse button being pressed.""" self._mouse_click(event, "right_clicked") def _right_dclick(self, event): """Handles the right mouse button being double clicked.""" self._mouse_click(event, "right_dclicked") def _begin_drag(self, event): """Handles the user beginning a drag operation with the left mouse button. """ if PythonDropSource is not None: adapter = self.adapter object, name = self.object, self.name selected = self._get_selected() drag_items = [] # Collect all of the selected items to drag: for row in selected: drag = adapter.get_drag(object, name, row) if drag is None: return drag_items.append(drag) # Save the drag item indices, so that we can later handle a # completed 'move' operation: self._drag_rows = selected try: # If only one item is being dragged, drag it as an item, not a # list: if len(drag_items) == 1: drag_items = drag_items[0] # Perform the drag and drop operation: ds = PythonDropSource(self.control, drag_items) # If moves are allowed and the result was a drag move: if (ds.result == wx.DragMove) and ( self._drag_local or self.factory.drag_move ): # Then delete all of the original items (in reverse order # from highest to lowest, so the indices don't need to be # adjusted): rows = self._drag_rows rows.reverse() for row in rows: adapter.delete(object, name, row) finally: self._drag_rows = None self._drag_local = False def _begin_label_edit(self, event): """Handles the user starting to edit an item label.""" if not self.adapter.get_can_edit( self.object, self.name, event.GetIndex() ): event.Veto() def _end_label_edit(self, event): """Handles the user finishing editing an item label.""" self.adapter.set_text( self.object, self.name, event.GetIndex(), event.GetColumn(), event.GetText(), ) self.row = event.GetIndex() + 1 def _item_selected(self, event): """Handles an item being selected.""" self._no_update = True try: get_item = self.adapter.get_item object, name = self.object, self.name selected_rows = self._get_selected() if self.factory.multi_select: self.multi_selected_rows = selected_rows self.multi_selected = [ get_item(object, name, row) for row in selected_rows ] elif len(selected_rows) == 0: self.selected_row = -1 self.selected = None else: self.selected_row = selected_rows[0] self.selected = get_item(object, name, selected_rows[0]) finally: self._no_update = False def _item_activated(self, event): """Handles an item being activated (double-clicked or enter pressed).""" self.activated_row = event.GetIndex() self.activated = self.adapter.get_item( self.object, self.name, self.activated_row ) def _key_down(self, event): """Handles the user pressing a key in the list control.""" key = event.GetKeyCode() if key == wx.WXK_PAGEDOWN: self._append_new() elif key in (wx.WXK_BACK, wx.WXK_DELETE): self._delete_current() elif key == wx.WXK_INSERT: self._insert_current() elif key == wx.WXK_LEFT: self._move_up_current() elif key == wx.WXK_RIGHT: self._move_down_current() elif key in (wx.WXK_RETURN, wx.WXK_ESCAPE): self._edit_current() else: event.Skip() def _column_right_clicked(self, event): """Handles the user right-clicking a column header.""" column = event.GetColumn() if (self._cached_widths is not None) and ( 0 <= column < len(self._cached_widths) ): self._cached_widths[column] = None self._size_modified(event) def _column_clicked(self, event): """Handles the right mouse button being double clicked.""" editor_event = TabularEditorEvent( editor=self, row=0, column=event.GetColumn() ) setattr(self, "column_clicked", editor_event) event.Skip() def _size_modified(self, event): """Handles the size of the list control being changed.""" control = self.control n = control.GetColumnCount() if n == 1: dx, dy = control.GetClientSize() control.SetColumnWidth(0, dx - 1) elif n > 1: do_later(self._set_column_widths) event.Skip() def _motion(self, event): """Handles the user moving the mouse.""" x = event.GetX() column = self._get_column(x) row, flags = self.control.HitTest(wx.Point(x, event.GetY())) if (row != self._last_row) or (column != self._last_column): self._last_row, self._last_column = row, column if (row == -1) or (column is None): tooltip = "" else: tooltip = self.adapter.get_tooltip( self.object, self.name, row, column ) if tooltip != self._last_tooltip: self._last_tooltip = tooltip wx.ToolTip.Enable(False) wx.ToolTip.Enable(True) self.control.SetToolTip(wx.ToolTip(tooltip)) # -- Drag and Drop Event Handlers ----------------------------------------- def wx_dropped_on(self, x, y, data, drag_result): """Handles a Python object being dropped on the list control.""" row, flags = self.control.HitTest(wx.Point(x, y)) # If the user dropped it on an empty list, set the target as past the # end of the list: if ( (row == -1) and ((flags & wx.LIST_HITTEST_NOWHERE) != 0) and (self.control.GetItemCount() == 0) ): row = 0 # If we have a valid drop target row, proceed: if row != -1: if not isinstance(data, list): # Handle the case of just a single item being dropped: self._wx_dropped_on(row, data) else: # Handles the case of a list of items being dropped, being # careful to preserve the original order of the source items if # possible: data.reverse() for item in data: self._wx_dropped_on(row, item) # If this was an inter-list drag, mark it as 'local': if self._drag_indices is not None: self._drag_local = True # Return a successful drop result: return drag_result # Indicate we could not process the drop: return wx.DragNone def _wx_dropped_on(self, row, item): """Helper method for handling a single item dropped on the list control. """ adapter = self.adapter object, name = self.object, self.name # Obtain the destination of the dropped item relative to the target: destination = adapter.get_dropped(object, name, row, item) # Adjust the target index accordingly: if destination == "after": row += 1 # Insert the dropped item at the requested position: adapter.insert(object, name, row, item) # If the source for the drag was also this list control, we need to # adjust the original source indices to account for their new position # after the drag operation: rows = self._drag_rows if rows is not None: for i in range(len(rows) - 1, -1, -1): if rows[i] < row: break rows[i] += 1 def wx_drag_over(self, x, y, data, drag_result): """Handles a Python object being dragged over the tree.""" if isinstance(data, list): rc = wx.DragNone for item in data: rc = self.wx_drag_over(x, y, item, drag_result) if rc == wx.DragNone: break return rc row, flags = self.control.HitTest(wx.Point(x, y)) # If the user is dragging over an empty list, set the target to the end # of the list: if ( (row == -1) and ((flags & wx.LIST_HITTEST_NOWHERE) != 0) and (self.control.GetItemCount() == 0) ): row = 0 # If the drag target index is valid and the adapter says it is OK to # drop the data here, then indicate the data can be dropped: if (row != -1) and self.adapter.get_can_drop( self.object, self.name, row, data ): return drag_result # Else indicate that we will not accept the data: return wx.DragNone # -- UI preference save/restore interface --------------------------------- def restore_prefs(self, prefs): """Restores any saved user preference information associated with the editor. """ self._cached_widths = cws = prefs.get("cached_widths") if cws is not None: set_column_width = self.control.SetColumnWidth for i, width in enumerate(cws): if width is not None: set_column_width(i, width) def save_prefs(self): """Returns any user preference information associated with the editor.""" cws = self._cached_widths if cws is not None: cws = [cw if cw is not None and cw >= 0 else None for cw in cws] return {"cached_widths": cws} # -- Private Methods ------------------------------------------------------ def _refresh(self): """Refreshes the contents of the editor's list control.""" n = self.adapter.len(self.object, self.name) if n > 0: self.control.RefreshItems(0, n - 1) def _rebuild(self): """Rebuilds the contents of the editor's list control.""" control = self.control control.ClearAll() adapter, object, name = self.adapter, self.object, self.name adapter.object, adapter.name = object, name get_alignment = adapter.get_alignment get_width = adapter.get_width for i, label in enumerate(adapter.label_map): control.InsertColumn( i, label, alignment_map.get( get_alignment(object, name, i), wx.LIST_FORMAT_LEFT ), ) self._set_column_widths() def _rebuild_all(self): """Rebuilds the structure of the list control, then refreshes its contents. """ self._rebuild() self.update_editor() def _set_column_widths(self): """Set the column widths for the current set of columns.""" control = self.control if control is None: return object, name = self.object, self.name dx, dy = control.GetClientSize() if is_mac: dx -= scrollbar_dx n = control.GetColumnCount() get_width = self.adapter.get_width pdx = 0 wdx = 0.0 widths = [] cached = self._cached_widths current = [control.GetColumnWidth(i) for i in range(n)] if (cached is None) or (len(cached) != n): self._cached_widths = cached = [None] * n for i in range(n): cw = cached[i] if (cw is None) or (-cw == current[i]): width = float(get_width(object, name, i)) if width <= 0.0: width = 0.1 if width <= 1.0: wdx += width cached[i] = -1 else: width = int(width) pdx += width if cw is None: cached[i] = width else: cached[i] = width = current[i] pdx += width widths.append(width) adx = max(0, dx - pdx) control.Freeze() for i in range(n): width = cached[i] if width < 0: width = widths[i] if width <= 1.0: widths[i] = w = max(30, int(round((adx * width) / wdx))) wdx -= width width = w adx -= width cached[i] = -w control.SetColumnWidth(i, width) control.Thaw() def _add_image(self, image_resource): """Adds a new image to the wx.ImageList and its associated mapping.""" bitmap = image_resource.create_image().ConvertToBitmap() image_list = self._image_list if image_list is None: self._image_list = image_list = wx.ImageList( bitmap.GetWidth(), bitmap.GetHeight() ) self.control.AssignImageList(image_list, wx.IMAGE_LIST_SMALL) self.image_resources[image_resource] = self.images[ image_resource.name ] = row = image_list.Add(bitmap) return row def _get_image(self, image): """Converts a user specified image to a wx.ListCtrl image index.""" if isinstance(image, str): self.image = image image = self.image if isinstance(image, ImageResource): result = self.image_resources.get(image) if result is not None: return result return self._add_image(image) return self.images.get(image) def _get_selected(self): """Returns a list of the rows of all currently selected list items.""" selected = [] item = -1 control = self.control # Handle case where the list is cleared if len(self.value) == 0: return selected while True: item = control.GetNextItem( item, wx.LIST_NEXT_ALL, wx.LIST_STATE_SELECTED ) if item == -1: break selected.append(item) return selected def _append_new(self): """Append a new item to the end of the list control.""" if "append" in self.factory.operations: adapter = self.adapter self.row = self.control.GetItemCount() self.edit = True adapter.insert( self.object, self.name, self.row, adapter.get_default_value(self.object, self.name), ) def _insert_current(self): """Inserts a new item after the currently selected list control item.""" if "insert" in self.factory.operations: selected = self._get_selected() if len(selected) == 1: adapter = self.adapter adapter.insert( self.object, self.name, selected[0], adapter.get_default_value(self.object, self.name), ) self.row = selected[0] self.edit = True def _delete_current(self): """Deletes the currently selected items from the list control.""" if "delete" in self.factory.operations: selected = self._get_selected() if len(selected) == 0: return delete = self.adapter.delete selected.reverse() for row in selected: delete(self.object, self.name, row) n = self.adapter.len(self.object, self.name) if not self.factory.multi_select: self.selected_row = self.row = n - 1 if row >= n else row else: # FIXME: What should the selection be? self.multi_selected = [] self.multi_selected_rows = [] def _move_up_current(self): """Moves the currently selected item up one line in the list control.""" if "move" in self.factory.operations: selected = self._get_selected() if len(selected) == 1: row = selected[0] if row > 0: adapter = self.adapter object, name = self.object, self.name item = adapter.get_item(object, name, row) adapter.delete(object, name, row) adapter.insert(object, name, row - 1, item) self.row = row - 1 def _move_down_current(self): """Moves the currently selected item down one line in the list control.""" if "move" in self.factory.operations: selected = self._get_selected() if len(selected) == 1: row = selected[0] if row < (self.control.GetItemCount() - 1): adapter = self.adapter object, name = self.object, self.name item = adapter.get_item(object, name, row) adapter.delete(object, name, row) adapter.insert(object, name, row + 1, item) self.row = row + 1 def _edit_current(self): """Allows the user to edit the current item in the list control.""" if "edit" in self.factory.operations and self.factory.editable_labels: selected = self._get_selected() if len(selected) == 1: self.control.EditLabel(selected[0]) def _get_column(self, x, translate=False): """Returns the column index corresponding to a specified x position.""" if x >= 0: control = self.control for i in range(control.GetColumnCount()): x -= control.GetColumnWidth(i) if x < 0: if translate: return self.adapter.get_column( self.object, self.name, i ) return i return None def _mouse_click(self, event, trait): """Generate a TabularEditorEvent event for a specified mouse event and editor trait name. """ x = event.GetX() row, flags = self.control.HitTest(wx.Point(x, event.GetY())) if row == wx.NOT_FOUND: if self.factory.multi_select: self.multi_selected = [] self.multi_selected_rows = [] else: self.selected = None self.selected_row = -1 else: if self.factory.multi_select and event.ShiftDown(): # Handle shift-click multi-selections because the wx.ListCtrl # does not (by design, apparently). # We must append this to the event queue because the # multi-selection will not be recorded until this event handler # finishes and lets the widget actually handle the event. do_later(self._item_selected, None) setattr( self, trait, TabularEditorEvent( editor=self, row=row, column=self._get_column(x, translate=True), ), ) # wx should continue with additional event handlers. Skip(False) # actually means to skip looking, skip(True) means to keep looking. # This seems backwards to me... event.Skip(True) class TabularEditorEvent(HasStrictTraits): #: The index of the row: row = Int() #: The id of the column (either a string or an integer): column = Any() #: The row item: item = Property() # -- Private Traits ------------------------------------------------------- #: The editor the event is associated with: editor = Instance(TabularEditor) # -- Property Implementations --------------------------------------------- def _get_item(self): editor = self.editor return editor.adapter.get_item(editor.object, editor.name, self.row) ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1684768028.1278067 traitsui-8.0.0/traitsui/wx/tests/0000755000175100001730000000000000000000000017631 5ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/wx/tests/__init__.py0000644000175100001730000000000000000000000021730 0ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/wx/tests/test_color_trait.py0000644000175100001730000001037300000000000023567 0ustar00runnerdocker00000000000000# (C) Copyright 2008-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! import unittest import wx from pyface.color import Color as PyfaceColor from traits.api import HasStrictTraits, TraitError from traitsui.wx.color_trait import WxColor class ObjectWithColor(HasStrictTraits): color = WxColor() class ObjectWithColorAllowsNone(HasStrictTraits): color = WxColor(allow_none=True) class TestWxColor(unittest.TestCase): def test_default(self): obj = ObjectWithColor() self.assertIsInstance(obj.color, wx.Colour) self.assertEqual(obj.color.Get(), (255, 255, 255, 255)) self.assertIsInstance(obj.color_, wx.Colour) self.assertEqual(obj.color_.Get(), (255, 255, 255, 255)) def test_tuple_rgb(self): obj = ObjectWithColor(color=(0, 128, 255)) self.assertIsInstance(obj.color, wx.Colour) self.assertEqual(obj.color.Get(), (0, 128, 255, 255)) self.assertIsInstance(obj.color_, wx.Colour) self.assertEqual(obj.color_.Get(), (0, 128, 255, 255)) def test_tuple_rgba(self): obj = ObjectWithColor(color=(0, 128, 255, 64)) self.assertIsInstance(obj.color, wx.Colour) self.assertEqual(obj.color.Get(), (0, 128, 255, 64)) self.assertIsInstance(obj.color_, wx.Colour) self.assertEqual(obj.color_.Get(), (0, 128, 255, 64)) def test_name_string(self): obj = ObjectWithColor(color="rebeccapurple") self.assertIsInstance(obj.color, wx.Colour) self.assertEqual(obj.color.Get(), (0x66, 0x33, 0x99, 0xff)) self.assertIsInstance(obj.color_, wx.Colour) self.assertEqual(obj.color_.Get(), (0x66, 0x33, 0x99, 0xff)) def test_name_string_with_space(self): obj = ObjectWithColor(color="rebecca purple") self.assertIsInstance(obj.color, wx.Colour) self.assertEqual(obj.color.Get(), (0x66, 0x33, 0x99, 0xff)) self.assertIsInstance(obj.color_, wx.Colour) self.assertEqual(obj.color_.Get(), (0x66, 0x33, 0x99, 0xff)) def test_rgb_string(self): obj = ObjectWithColor(color="(0, 128, 255)") self.assertIsInstance(obj.color, wx.Colour) self.assertEqual(obj.color.Get(), (0, 128, 255, 255)) self.assertIsInstance(obj.color_, wx.Colour) self.assertEqual(obj.color_.Get(), (0, 128, 255, 255)) def test_rgba_string(self): obj = ObjectWithColor(color="(0, 128, 255, 64)") self.assertIsInstance(obj.color, wx.Colour) self.assertEqual(obj.color.Get(), (0, 128, 255, 64)) self.assertIsInstance(obj.color_, wx.Colour) self.assertEqual(obj.color_.Get(), (0, 128, 255, 64)) def test_rgb_int(self): obj = ObjectWithColor(color=0x0088ff) self.assertIsInstance(obj.color, wx.Colour) self.assertEqual(obj.color.Get(), (0x00, 0x88, 0xff, 0xff)) self.assertIsInstance(obj.color_, wx.Colour) self.assertEqual(obj.color_.Get(), (0x00, 0x88, 0xff, 0xff)) def test_pyface_color(self): obj = ObjectWithColor(color=PyfaceColor(rgba=(0.0, 0.5, 1.0, 0.25))) self.assertIsInstance(obj.color, wx.Colour) self.assertEqual(obj.color.Get(), (0, 128, 255, 64)) self.assertIsInstance(obj.color_, wx.Colour) self.assertEqual(obj.color_.Get(), (0, 128, 255, 64)) def test_default_none(self): obj = ObjectWithColorAllowsNone(color=None) self.assertIsNone(obj.color) self.assertIsNone(obj.color_) def test_bad_color(self): with self.assertRaises(TraitError): ObjectWithColor(color="not a color") def test_bad_tuple(self): with self.assertRaises(TraitError): ObjectWithColor(color=(0xff, 0xff)) def test_bad_tuple_not_int(self): with self.assertRaises(TraitError): ObjectWithColor(color=("not an int", 0xff, 0xff)) def test_bad_tuple_string(self): with self.assertRaises(TraitError): ObjectWithColor(color="(0xff, 0xff)") ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/wx/tests/test_font_trait.py0000644000175100001730000001462100000000000023417 0ustar00runnerdocker00000000000000# (C) Copyright 2008-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! import unittest import wx from pyface.font import Font as PyfaceFont from pyface.util.font_parser import simple_parser from traits.api import HasTraits, TraitError from ..font_trait import ( WxFont, TraitsFont, create_traitsfont, font_families, font_styles, font_to_str, font_weights ) class FontExample(HasTraits): font = WxFont() class TestWxFont(unittest.TestCase): def test_create_traitsfont(self): expected_outcomes = {} expected_outcomes[""] = TraitsFont( 10, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL ) for weight, wx_weight in font_weights.items(): expected_outcomes[weight] = TraitsFont( 10, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx_weight ) for style, wx_style in font_styles.items(): expected_outcomes[style] = TraitsFont( 10, wx.FONTFAMILY_DEFAULT, wx_style, wx.FONTWEIGHT_NORMAL ) expected_outcomes["underline"] = TraitsFont( 10, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, True ) for size in ["18", "18 pt", "18 point"]: expected_outcomes[size] = TraitsFont( 18, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL ) for family, wx_family in font_families.items(): expected_outcomes[family] = TraitsFont( 10, wx_family, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL ) expected_outcomes["Courier"] = TraitsFont( 10, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, "Courier", ) expected_outcomes["Comic Sans"] = TraitsFont( 10, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, "Comic Sans", ) expected_outcomes["18 pt Bold Oblique Underline Comic Sans script"] = TraitsFont( 18, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_SLANT, wx.FONTWEIGHT_BOLD, True, "Comic Sans", ) for name, expected in expected_outcomes.items(): with self.subTest(name=name): result = create_traitsfont(name) # test we get expected font self.assertIsInstance(result, TraitsFont) self.assert_wxfont_equal(result, expected) # round-trip trhough font_to_str result_2 = create_traitsfont(font_to_str(result)) self.assert_wxfont_equal(result, result_2) def test_create_traitsfont_wx_font(self): font = wx.Font( 18, wx.FONTFAMILY_SCRIPT, wx.FONTSTYLE_SLANT, wx.FONTWEIGHT_BOLD, True, "Comic Sans", ) traits_font = create_traitsfont(font) self.assertIsInstance(traits_font, TraitsFont) self.assert_wxfont_equal(traits_font, font) def test_create_traitsfont_system_default(self): font = wx.SystemSettings.GetFont(wx.SYS_DEFAULT_GUI_FONT) traits_font = create_traitsfont(font) self.assertIsInstance(traits_font, TraitsFont) self.assert_wxfont_equal(traits_font, font) def test_create_traitsfont_pyface_font(self): args = simple_parser("18 pt Bold Oblique Underline Courier") font = PyfaceFont(**args) traits_font = create_traitsfont(font) self.assertIsInstance(traits_font, TraitsFont) self.assert_wxfont_equal(traits_font, font.to_toolkit()) def test_font_trait_default(self): obj = FontExample() self.assertIsInstance(obj.font, TraitsFont) self.assert_wxfont_equal(obj.font, wx.SystemSettings.GetFont(wx.SYS_DEFAULT_GUI_FONT)) def test_font_trait_str(self): obj = FontExample(font="18 pt Bold Oblique Underline Comic Sans script") wx_font = TraitsFont( 18, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_SLANT, wx.FONTWEIGHT_BOLD, True, "Comic Sans", ) self.assertIsInstance(obj.font, TraitsFont) self.assert_wxfont_equal(obj.font, wx_font) def test_font_trait_wx_font(self): wx_font = TraitsFont( 18, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_SLANT, wx.FONTWEIGHT_BOLD, True, "Comic Sans", ) obj = FontExample(font=wx_font) self.assertIsInstance(obj.font, TraitsFont) self.assert_wxfont_equal(obj.font, wx_font) def test_font_trait_pyface_font(self): args = simple_parser("18 pt Bold Oblique Underline Courier typewriter") font = PyfaceFont(**args) obj = FontExample(font=font) self.assertIsInstance(obj.font, TraitsFont) self.assert_wxfont_equal(obj.font, font.to_toolkit()) def test_font_trait_none(self): obj = FontExample(font=None) self.assertIsNone(obj.font) def test_font_trait_bad(self): with self.assertRaises(TraitError): obj = FontExample(font=1) def test_traits_font_reduce(self): traits_font = TraitsFont( 18, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_SLANT, wx.FONTWEIGHT_BOLD, True, "Comic Sans", ) result = traits_font.__reduce_ex__(None) self.assertEqual( result, ( create_traitsfont, ("18 point Comic Sans Oblique Bold underline",), ), ) def test_traits_font_str(self): traits_font = TraitsFont( 18, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_SLANT, wx.FONTWEIGHT_BOLD, True, "Comic Sans", ) result = str(traits_font) self.assertEqual( result, "18 point Comic Sans Oblique Bold underline", ) def assert_wxfont_equal(self, font, other): self.assertIsInstance(font, wx.Font) self.assertIsInstance(other, wx.Font) self.assertEqual(font.FaceName, font.FaceName) self.assertEqual(font.Family, other.Family) self.assertEqual(font.PointSize, other.PointSize) self.assertEqual(font.Style, other.Style) self.assertEqual(font.Weight, other.Weight) self.assertEqual(font.GetUnderlined(), other.GetUnderlined()) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/wx/text_editor.py0000644000175100001730000001700400000000000021375 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Defines the various text editors for the wxPython user interface toolkit. """ import wx from traits.api import TraitError from traitsui.editors.text_editor import evaluate_trait from .editor import Editor from .editor_factory import ReadonlyEditor as BaseReadonlyEditor from .constants import OKColor # Readonly text editor with view state colors: HoverColor = wx.LIGHT_GREY DownColor = wx.WHITE class SimpleEditor(Editor): """Simple style text editor, which displays a text field.""" #: Flag for window styles: base_style = 0 #: 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 style = self.base_style self.evaluate = factory.evaluate self.sync_value(factory.evaluate_name, "evaluate", "from") if (not factory.multi_line) or factory.password: style &= ~wx.TE_MULTILINE if factory.password: style |= wx.TE_PASSWORD multi_line = (style & wx.TE_MULTILINE) != 0 if multi_line: self.scrollable = True if factory.enter_set and (not multi_line): control = wx.TextCtrl( parent, -1, self.str_value, style=style | wx.TE_PROCESS_ENTER ) parent.Bind( wx.EVT_TEXT_ENTER, self.update_object, id=control.GetId() ) else: control = wx.TextCtrl(parent, -1, self.str_value, style=style) control.Bind(wx.EVT_KILL_FOCUS, self.update_object) if control.IsSingleLine(): control.SetHint(self.factory.placeholder) if factory.auto_set: parent.Bind(wx.EVT_TEXT, self.update_object, id=control.GetId()) self.control = control self.set_error_state(False) self.set_tooltip() def update_object(self, event): """Handles the user entering input data in the edit control.""" if isinstance(event, wx.FocusEvent): # Ensure that the base class' focus event handlers are run, some # built-in behavior may break on some platforms otherwise. event.Skip() 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.SetValue(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.GetValue() 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.""" #: Flag for window style. This value overrides the default. base_style = wx.TE_MULTILINE class ReadonlyEditor(BaseReadonlyEditor): """Read-only style of text editor, which displays a read-only text field.""" def init(self, parent): """Finishes initializing the editor by creating the underlying toolkit widget. """ super().init(parent) if self.factory.view is not None: control = self.control control.Bind(wx.EVT_ENTER_WINDOW, self._enter_window) control.Bind(wx.EVT_LEAVE_WINDOW, self._leave_window) control.Bind(wx.EVT_LEFT_DOWN, self._left_down) control.Bind(wx.EVT_LEFT_UP, self._left_up) def update_editor(self): """Updates the editor when the object trait changes externally to the editor. """ control = self.control new_value = self.str_value if hasattr(self.factory, "password") and self.factory.password: new_value = "*" * len(new_value) if (self.item.resizable is True) or (self.item.height != -1.0): if control.GetValue() != new_value: control.SetValue(new_value) control.SetInsertionPointEnd() elif control.GetLabel() != new_value: control.SetLabel(new_value) def dispose(self): """Disposes of the contents of an editor.""" if self.factory.view is not None: control = self.control control.Unbind(wx.EVT_ENTER_WINDOW) control.Unbind(wx.EVT_LEAVE_WINDOW) control.Unbind(wx.EVT_LEFT_DOWN) control.Unbind(wx.EVT_LEFT_UP) super().dispose() # -- Private Methods ------------------------------------------------------ def _set_color(self): control = self.control if not self._in_window: color = control.GetParent().GetBackgroundColour() elif self._down: color = DownColor else: color = HoverColor control.SetBackgroundColour(color) control.Refresh() # -- wxPython Event Handlers ---------------------------------------------- def _enter_window(self, event): self._in_window = True self._set_color() def _leave_window(self, event): self._in_window = False self._set_color() def _left_down(self, event): self.control.CaptureMouse() self._down = True self._set_color() def _left_up(self, event): self._set_color() if not self._down: return self.control.ReleaseMouse() self._down = False if self._in_window: self.object.edit_traits( view=self.factory.view, parent=self.control ) TextEditor = SimpleEditor ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/wx/time_editor.py0000644000175100001730000000512300000000000021346 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ A Traits UI editor that wraps a WX timer control. Future Work ----------- The only editor provided is an editable and constrained "XX:XX:XX XM" field. At the minimum, a spinner should be provided so the time can be changed without the need for a keyboard. In addition we need to extend to provide all four of the basic editor types, Simple, Custom, Text, and Readonly. """ import datetime import wx.adv from traitsui.wx.editor import Editor from traitsui.wx.text_editor import ReadonlyEditor as TextReadonlyEditor class SimpleEditor(Editor): """ Traits UI time editor. """ def init(self, parent): """ Finishes initializing the editor by creating the underlying toolkit widget. """ tctl = wx.adv.TimePickerCtrl(parent, -1, name="12 hour control") self.control = tctl self.control.Bind(wx.adv.EVT_TIME_CHANGED, self.time_updated) return def time_updated(self, event): """ Event for when the wx time control is updated. """ time = self.control.GetValue() hour = time.GetHour() minute = time.GetMinute() second = time.GetSecond() self.value = datetime.time(hour, minute, second) return def update_editor(self): """ Updates the editor when the object trait changes externally to the editor. """ if self.value: time = self.control.GetValue() time.SetHour(self.value.hour) time.SetMinute(self.value.minute) time.SetSecond(self.value.second) self.control.SetValue(time) return # TODO: Write me. Possibly use TextEditor as a model to show a string # representation of the time, and have enter-set do a time evaluation. class TextEditor(SimpleEditor): pass # TODO: Write me. class CustomEditor(SimpleEditor): pass class ReadonlyEditor(TextReadonlyEditor): """Use a TextEditor for the view.""" def _get_str_value(self): """Replace the default string value with our own time version.""" if self.value is None: return self.factory.message else: return self.value.strftime(self.factory.strftime) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/wx/title_editor.py0000644000175100001730000000344700000000000021540 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Define an editor that displays a string value as a title. """ from .editor import Editor from pyface.api import HeadingText # FIXME: TitleEditor (the editor factory for title editors) is a proxy class # defined here just for backward compatibility. The class has been moved to # traitsui.editors.title_editor. from traitsui.editors.title_editor import TitleEditor # ------------------------------------------------------------------------- # '_TitleEditor' class: # ------------------------------------------------------------------------- class _TitleEditor(Editor): def init(self, parent): """Finishes initializing the editor by creating the underlying toolkit widget. """ self._control = HeadingText(parent=parent, create=False) self._control.create() self.control = self._control.control self.set_tooltip() def update_editor(self): """Updates the editor when the object trait changes external to the editor. """ self._control.text = self.str_value def dispose(self): """Cleanly dispose of the editor. This ensures that the wrapped Pyface Widget is cleaned-up. """ if self._control is not None: self._control.destroy() super().dispose() SimpleEditor = _TitleEditor CustomEditor = _TitleEditor ReadonlyEditor = _TitleEditor TextEditor = _TitleEditor ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/wx/toolkit.py0000644000175100001730000004262100000000000020533 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Defines the concrete implementations of the traits Toolkit interface for the wxPython user interface toolkit. """ # Make sure that importimg from this backend is OK: import logging from traitsui.toolkit import assert_toolkit_import assert_toolkit_import(["wx"]) import wx # Ensure that we can import Pyface backend. This starts App as a side-effect. from pyface.toolkit import toolkit_object as pyface_toolkit _app = pyface_toolkit("init:_app") from traits.api import HasPrivateTraits, Instance from traits.trait_notifiers import set_ui_handler from pyface.api import SystemMetrics from pyface.wx.drag_and_drop import PythonDropTarget from traitsui.theme import Theme from traitsui.ui import UI from traitsui.toolkit import Toolkit from .constants import WindowColor from .helper import position_window logger = logging.getLogger(__name__) #: Mapping from wx events to method suffixes. EventSuffix = { wx.wxEVT_LEFT_DOWN: "left_down", wx.wxEVT_LEFT_DCLICK: "left_dclick", wx.wxEVT_LEFT_UP: "left_up", wx.wxEVT_MIDDLE_DOWN: "middle_down", wx.wxEVT_MIDDLE_DCLICK: "middle_dclick", wx.wxEVT_MIDDLE_UP: "middle_up", wx.wxEVT_RIGHT_DOWN: "right_down", wx.wxEVT_RIGHT_DCLICK: "right_dclick", wx.wxEVT_RIGHT_UP: "right_up", wx.wxEVT_MOTION: "mouse_move", wx.wxEVT_ENTER_WINDOW: "enter", wx.wxEVT_LEAVE_WINDOW: "leave", wx.wxEVT_MOUSEWHEEL: "mouse_wheel", wx.wxEVT_PAINT: "paint", } #: Types of popup views: Popups = {"popup", "popover", "info"} # ------------------------------------------------------------------------- # Traits UI dispatch infrastructure # ------------------------------------------------------------------------- def ui_handler(handler, *args): """Handles UI notification handler requests that occur on a thread other than the UI thread. """ wx.CallAfter(handler, *args) # Tell the traits notification handlers to use this UI handler set_ui_handler(ui_handler) # ------------------------------------------------------------------------- # Wx Toolkit Implementation # ------------------------------------------------------------------------- class GUIToolkit(Toolkit): """Implementation class for wxPython toolkit.""" def ui_panel(self, ui, parent): """Creates a wxPython panel-based user interface using information from the specified UI object. """ from . import ui_panel ui_panel.ui_panel(ui, parent) def ui_subpanel(self, ui, parent): """Creates a wxPython subpanel-based user interface using information from the specified UI object. """ from . import ui_panel ui_panel.ui_subpanel(ui, parent) def ui_livemodal(self, ui, parent): """Creates a wxPython modal "live update" dialog user interface using information from the specified UI object. """ from . import ui_live ui_live.ui_livemodal(ui, parent) def ui_live(self, ui, parent): """Creates a wxPython non-modal "live update" window user interface using information from the specified UI object. """ from . import ui_live ui_live.ui_live(ui, parent) def ui_modal(self, ui, parent): """Creates a wxPython modal dialog user interface using information from the specified UI object. """ from . import ui_modal ui_modal.ui_modal(ui, parent) def ui_nonmodal(self, ui, parent): """Creates a wxPython non-modal dialog user interface using information from the specified UI object. """ from . import ui_modal ui_modal.ui_nonmodal(ui, parent) def ui_popup(self, ui, parent): """Creates a wxPython temporary "live update" popup dialog user interface using information from the specified UI object. """ from . import ui_live ui_live.ui_popup(ui, parent) def ui_popover(self, ui, parent): """Creates a wxPython temporary "live update" popup dialog user interface using information from the specified UI object. """ from . import ui_live ui_live.ui_popover(ui, parent) def ui_info(self, ui, parent): """Creates a wxPython temporary "live update" popup dialog user interface using information from the specified UI object. """ from . import ui_live ui_live.ui_info(ui, parent) def ui_wizard(self, ui, parent): """Creates a wxPython wizard dialog user interface using information from the specified UI object. """ from . import ui_wizard ui_wizard.ui_wizard(ui, parent) def view_application( self, context, view, kind=None, handler=None, id="", scrollable=None, args=None, ): """Creates a wxPython 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. """ from . import view_application return view_application.view_application( context, view, kind, handler, id, scrollable, args ) def position(self, ui): """Positions the associated dialog window on the display.""" view = ui.view window = ui.control # Set up the default position of the window: parent = window.GetParent() if parent is None: px, py = 0, 0 pdx = SystemMetrics().screen_width pdy = SystemMetrics().screen_height else: px, py = parent.GetPosition() pdx, pdy = parent.GetSize() # Calculate the correct width and height for the window: cur_width, cur_height = window.GetSize() width = view.width height = view.height if width < 0.0: width = cur_width elif width <= 1.0: width = int(width * SystemMetrics().screen_width) else: width = int(width) if height < 0.0: height = cur_height elif height <= 1.0: height = int(height * SystemMetrics().screen_height) else: height = int(height) if view.kind in Popups: position_window(window, width, height) return # Calculate the correct position for the window: x = view.x y = view.y if x < -99999.0: # BH- I think this is the case when there is a parent # so this logic tries to place it in the middle of the parent # if possible, otherwise tries an offset from the parent x = px + (pdx - width) // 2 if x < 0: x = px + 20 elif x <= -1.0: x = px + pdx - width + int(x) + 1 elif x < 0.0: x = px + pdx - width + int(x * pdx) elif x <= 1.0: x = px + int(x * pdx) else: x = int(x) if y < -99999.0: # BH- I think this is the case when there is a parent # so this logic tries to place it in the middle of the parent # if possible, otherwise tries an offset from the parent y = py + (pdy - height) // 2 if y < 0: y = py + 20 elif y <= -1.0: y = py + pdy - height + int(y) + 1 elif x < 0.0: y = py + pdy - height + int(y * pdy) elif y <= 1.0: y = py + int(y * pdy) else: y = int(y) # make sure the position is on the visible screen, maybe # the desktop had been resized? x = min(x, wx.DisplaySize()[0]) y = min(y, wx.DisplaySize()[1]) # Position and size the window as requested: window.SetSize(max(0, x), max(0, y), width, height) def show_help(self, ui, control): """Shows a help window for a specified UI and control.""" from . import ui_panel ui_panel.show_help(ui, control) def save_window(self, ui): """Saves user preference information associated with a UI window.""" from . import helper helper.save_window(ui) def rebuild_ui(self, ui): """Rebuilds a UI after a change to the content of the UI.""" parent = size = None if ui.control is not None: size = ui.control.GetSize() parent = ui.control._parent info = ui.info ui.recycle() ui.info = info info.ui = ui ui.rebuild(ui, parent) if parent is not None: ui.control.SetSize(size) sizer = parent.GetSizer() if sizer is not None: sizer.Add(ui.control, 1, wx.EXPAND) def set_title(self, ui): """Sets the title for the UI window.""" ui.control.SetTitle(ui.title) def set_icon(self, ui): """Sets the icon for the UI window.""" from pyface.image_resource import ImageResource if isinstance(ui.icon, ImageResource): ui.control.SetIcon(ui.icon.create_icon()) def key_event_to_name(self, event): """Converts a keystroke event into a corresponding key name.""" from . import key_event_to_name return key_event_to_name.key_event_to_name(event) def hook_events(self, ui, control, events=None, handler=None): """Hooks all specified events for all controls in a UI so that they can be routed to the correct event handler. """ if events is None: events = ( wx.wxEVT_LEFT_DOWN, wx.wxEVT_LEFT_DCLICK, wx.wxEVT_LEFT_UP, wx.wxEVT_MIDDLE_DOWN, wx.wxEVT_MIDDLE_DCLICK, wx.wxEVT_MIDDLE_UP, wx.wxEVT_RIGHT_DOWN, wx.wxEVT_RIGHT_DCLICK, wx.wxEVT_RIGHT_UP, wx.wxEVT_MOTION, wx.wxEVT_ENTER_WINDOW, wx.wxEVT_LEAVE_WINDOW, wx.wxEVT_MOUSEWHEEL, wx.wxEVT_PAINT, ) control.SetDropTarget( PythonDropTarget(DragHandler(ui=ui, control=control)) ) elif events == "keys": events = (wx.wxEVT_CHAR,) if handler is None: handler = ui.route_event id = control.GetId() event_handler = EventHandlerWrapper() connect = event_handler.Connect for event in events: connect(id, id, event, handler) control.PushEventHandler(event_handler) for child in control.GetChildren(): self.hook_events(ui, child, events, handler) def route_event(self, ui, event): """Routes a hooked event to the correct handler method.""" suffix = EventSuffix[event.GetEventType()] control = event.GetEventObject() handler = ui.handler method = None owner = getattr(control, "_owner", None) if owner is not None: method = getattr( handler, "on_%s_%s" % (owner.get_id(), suffix), None ) if method is None: method = getattr(handler, "on_%s" % suffix, None) or getattr( handler, "on_any_event", None ) if (method is None) or (method(ui.info, owner, event) is False): event.Skip() def skip_event(self, event): """Indicates that an event should continue to be processed by the toolkit. """ event.Skip() def destroy_control(self, control): """Destroys a specified GUI toolkit control.""" _popEventHandlers(control) def _destroy_control(control): try: control.Destroy() except Exception: logger.exception( "Wx control %r not destroyed cleanly", control ) wx.CallAfter(_destroy_control, control) def destroy_children(self, control): """Destroys all of the child controls of a specified GUI toolkit control. """ for child in control.GetChildren(): _popEventHandlers(child) wx.CallAfter(control.DestroyChildren) def image_size(self, image): """Returns a ( width, height ) tuple containing the size of a specified toolkit image. """ return (image.GetWidth(), image.GetHeight()) 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": WindowColor} # ------------------------------------------------------------------------- # GUI toolkit dependent trait definitions: # ------------------------------------------------------------------------- def color_trait(self, *args, **traits): from . import color_trait as ct return ct.WxColor(*args, **traits) def rgb_color_trait(self, *args, **traits): from . import rgb_color_trait as rgbct return rgbct.RGBColor(*args, **traits) def font_trait(self, *args, **traits): from . import font_trait as ft return ft.WxFont(*args, **traits) class DragHandler(HasPrivateTraits): """Handler for drag events.""" # ------------------------------------------------------------------------- # Traits definitions: # ------------------------------------------------------------------------- #: The UI associated with the drag handler ui = Instance(UI) #: The wx control associated with the drag handler control = Instance(wx.Window) # -- Drag and drop event handlers: ---------------------------------------- def wx_dropped_on(self, x, y, data, drag_result): """Handles a Python object being dropped on the window.""" return self._drag_event("dropped_on", x, y, data, drag_result) def wx_drag_over(self, x, y, data, drag_result): """Handles a Python object being dragged over the tree.""" return self._drag_event("drag_over", x, y, data, drag_result) def wx_drag_leave(self, data): """Handles a dragged Python object leaving the window.""" return self._drag_event("drag_leave") def _drag_event(self, suffix, x=None, y=None, data=None, drag_result=None): """Handles routing a drag event to the appropriate handler.""" control = self.control handler = self.ui.handler method = None owner = getattr(control, "_owner", None) if owner is not None: method = getattr( handler, "on_%s_%s" % (owner.get_id(), suffix), None ) if method is None: method = getattr(handler, "on_%s" % suffix, None) if method is None: return wx.DragNone if x is None: result = method(self.ui.info, owner) else: result = method(self.ui.info, owner, x, y, data, drag_result) if result is None: result = drag_result return result class EventHandlerWrapper(wx.EvtHandler): """Simple wrapper around wx.EvtHandler used to determine which event handlers were added by traitui. """ pass def _popEventHandlers(ctrl, handler_type=EventHandlerWrapper): """Pop any event handlers that have been pushed on to a window and its children. """ # FIXME: have to special case URLResolvingHtmlWindow because it doesn't # want its EvtHandler cleaned up. See issue #752. from .html_editor import URLResolvingHtmlWindow handler = ctrl.GetEventHandler() while ctrl is not handler: next_handler = handler.GetNextHandler() if isinstance(handler, handler_type): ctrl.PopEventHandler(True) handler = next_handler for child in ctrl.GetChildren(): _popEventHandlers(child, handler_type) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/wx/tree_editor.py0000644000175100001730000016566600000000000021372 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Defines the tree editor for the wxPython user interface toolkit. """ import copy from functools import partial import os import wx try: from pyface.wx.drag_and_drop import PythonDropSource, PythonDropTarget except: PythonDropSource = PythonDropTarget = None from pyface.ui.wx.image_list import ImageList from traits.api import HasStrictTraits, Any, Str, Event, TraitError from traitsui.api import View, TreeNode, ObjectTreeNode, MultiTreeNode from traitsui.editors.tree_editor import ( CopyAction, CutAction, DeleteAction, NewAction, PasteAction, RenameAction, ) from traitsui.undo import ListUndoItem from traitsui.tree_node import ITreeNodeAdapterBridge from traitsui.menu import Menu, Action, Separator from pyface.api import ImageResource from pyface.ui_traits import convert_image from pyface.dock.api import ( DockWindow, DockSizer, DockSection, DockRegion, DockControl, ) from .constants import OKColor from .editor import Editor from .helper import TraitsUIPanel, TraitsUIScrolledPanel # ------------------------------------------------------------------------- # Global data: # ------------------------------------------------------------------------- # Paste buffer for copy/cut/paste operations paste_buffer = None # ------------------------------------------------------------------------- # 'SimpleEditor' class: # ------------------------------------------------------------------------- class SimpleEditor(Editor): """Simple style of tree editor.""" # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- #: Is the tree editor is scrollable? This value overrides the default. scrollable = True #: Allows an external agent to set the tree selection selection = Event() #: The currently selected object selected = Any() #: The event fired when a tree node is activated by double clicking or #: pressing the enter key on a node. activated = Event() #: The event fired when a tree node is clicked on: click = Event() #: The event fired when a tree node is double-clicked on: dclick = Event() #: The event fired when the application wants to veto an operation: veto = Event() def init(self, parent): """Finishes initializing the editor by creating the underlying toolkit widget. """ factory = self.factory style = self._get_style() if factory.editable: # Check to see if the tree view is based on a shared trait editor: if factory.shared_editor: factory_editor = factory.editor # If this is the editor that defines the trait editor panel: if factory_editor is None: # Remember which editor has the trait editor in the # factory: factory._editor = self # Create the trait editor panel: self.control = TraitsUIPanel(parent, -1) self.control._node_ui = self.control._editor_nid = None # Check to see if there are any existing editors that are # waiting to be bound to the trait editor panel: editors = factory._shared_editors if editors is not None: for editor in factory._shared_editors: # If the editor is part of this UI: if editor.ui is self.ui: # Then bind it to the trait editor panel: editor._editor = self.control # Indicate all pending editors have been processed: factory._shared_editors = None # We only needed to build the trait editor panel, so exit: return # Check to see if the matching trait editor panel has been # created yet: editor = factory_editor._editor if (editor is None) or (editor.ui is not self.ui): # If not, add ourselves to the list of pending editors: shared_editors = factory_editor._shared_editors if shared_editors is None: factory_editor._shared_editors = shared_editors = [] shared_editors.append(self) else: # Otherwise, bind our trait editor panel to the shared one: self._editor = editor.control # Finally, create only the tree control: self.control = self._tree = tree = wx.TreeCtrl( parent, -1, style=style ) else: # If editable, create a tree control and an editor panel: self._is_dock_window = True theme = factory.dock_theme or self.item.container.dock_theme self.control = splitter = DockWindow( parent, theme=theme ).control self._tree = tree = wx.TreeCtrl(splitter, -1, style=style) self._editor = editor = TraitsUIScrolledPanel(splitter) editor.SetSizer(wx.BoxSizer(wx.VERTICAL)) editor.SetScrollRate(16, 16) editor.SetMinSize(wx.Size(100, 100)) self._editor._node_ui = self._editor._editor_nid = None item = self.item hierarchy_name = editor_name = "" style = "fixed" name = item.label if name != "": hierarchy_name = name + " Hierarchy" editor_name = name + " Editor" style = item.dock splitter.SetSizer( DockSizer( contents=DockSection( contents=[ DockRegion( contents=[ DockControl( name=hierarchy_name, id="tree", control=tree, style=style, ) ] ), DockRegion( contents=[ DockControl( name=editor_name, id="editor", control=self._editor, style=style, ) ] ), ], is_row=(factory.orientation == "horizontal"), ) ) ) else: # Otherwise, just create the tree control: self.control = self._tree = tree = wx.TreeCtrl( parent, -1, style=style ) # Set up to show tree node icon (if requested): if factory.show_icons: self._image_list = ImageList(*factory.icon_size) tree.AssignImageList(self._image_list) # Set up the mapping between objects and tree id's: self._map = {} # Initialize the 'undo state' stack: self._undoable = [] # Set up the mouse event handlers: tree.Bind(wx.EVT_LEFT_DOWN, self._on_left_down) tree.Bind(wx.EVT_RIGHT_DOWN, self._on_right_down) tree.Bind(wx.EVT_LEFT_DCLICK, self._on_left_dclick) # Set up the tree event handlers: tree.Bind(wx.EVT_TREE_ITEM_EXPANDING, self._on_tree_item_expanding) tree.Bind(wx.EVT_TREE_ITEM_EXPANDED, self._on_tree_item_expanded) tree.Bind(wx.EVT_TREE_ITEM_COLLAPSING, self._on_tree_item_collapsing) tree.Bind(wx.EVT_TREE_ITEM_COLLAPSED, self._on_tree_item_collapse) tree.Bind(wx.EVT_TREE_ITEM_ACTIVATED, self._on_tree_item_activated) tree.Bind(wx.EVT_TREE_SEL_CHANGED, self._on_tree_sel_changed) tree.Bind(wx.EVT_TREE_BEGIN_DRAG, self._on_tree_begin_drag) tree.Bind(wx.EVT_TREE_BEGIN_LABEL_EDIT, self._on_tree_begin_label_edit) tree.Bind(wx.EVT_TREE_END_LABEL_EDIT, self._on_tree_end_label_edit) tree.Bind(wx.EVT_TREE_ITEM_GETTOOLTIP, self._on_tree_item_gettooltip) # Set up general mouse events tree.Bind(wx.EVT_MOTION, self._on_hover) # Synchronize external object traits with the editor: self.sync_value(factory.selected, "selected") self.sync_value(factory.activated, "activated", "to") self.sync_value(factory.click, "click", "to") self.sync_value(factory.dclick, "dclick", "to") self.sync_value(factory.veto, "veto", "from") # Set up the drag and drop target: if PythonDropTarget is not None: tree.SetDropTarget(PythonDropTarget(self)) def dispose(self): """Disposes of the contents of an editor.""" tree = self._tree if tree is not None: tree.Unbind(wx.EVT_LEFT_DOWN) tree.Unbind(wx.EVT_RIGHT_DOWN) tree.Unbind(wx.EVT_LEFT_DCLICK) tree.Unbind(wx.EVT_TREE_ITEM_EXPANDING) tree.Unbind(wx.EVT_TREE_ITEM_EXPANDED) tree.Unbind(wx.EVT_TREE_ITEM_COLLAPSING) tree.Unbind(wx.EVT_TREE_ITEM_COLLAPSED) tree.Unbind(wx.EVT_TREE_ITEM_ACTIVATED) tree.Unbind(wx.EVT_TREE_SEL_CHANGED) tree.Unbind(wx.EVT_TREE_BEGIN_DRAG) tree.Unbind(wx.EVT_TREE_BEGIN_LABEL_EDIT) tree.Unbind(wx.EVT_TREE_END_LABEL_EDIT) tree.Unbind(wx.EVT_TREE_ITEM_GETTOOLTIP) tree.Unbind(wx.EVT_MOTION) nid = self._tree.GetRootItem() if nid.IsOk(): self._delete_node(nid) self._tree = None super().dispose() def _selection_changed(self, selection): """Handles the **selection** event.""" try: self._tree.SelectItem(self._object_info(selection)[2]) except Exception: pass def _selected_changed(self, selected): """Handles the **selected** trait being changed.""" if not self._no_update_selected: self._selection_changed(selected) def _veto_changed(self): """Handles the 'veto' event being fired.""" self._veto = True def _get_style(self): """Returns the style settings used for displaying the wx tree.""" factory = self.factory style = wx.TR_EDIT_LABELS | wx.TR_HAS_BUTTONS | wx.CLIP_CHILDREN # Turn lines off if explicit or for appearance on *nix: if (factory.lines_mode == "off") or ( (factory.lines_mode == "appearance") and (os.name == "posix") ): style |= wx.TR_NO_LINES if factory.hide_root: style |= wx.TR_HIDE_ROOT | wx.TR_LINES_AT_ROOT if factory.selection_mode != "single": style |= wx.TR_MULTIPLE | wx.TR_EXTENDED return style def update_object(self, event): """Handles the user entering input data in the edit control.""" try: self.value = self._get_value() self.control.SetBackgroundColour(OKColor) self.control.Refresh() except TraitError: pass def _save_state(self): tree = self._tree nid = tree.GetRootItem() state = {} if nid.IsOk(): nodes_to_do = [nid] while nodes_to_do: node = nodes_to_do.pop() data = self._get_node_data(node) try: is_expanded = tree.IsExpanded(node) except: is_expanded = True state[hash(data[-1])] = (data[0], is_expanded) for cnid in self._nodes(node): nodes_to_do.append(cnid) return state def _restore_state(self, state): if not state: return tree = self._tree nid = tree.GetRootItem() if nid.IsOk(): nodes_to_do = [nid] while nodes_to_do: node = nodes_to_do.pop() for cnid in self._nodes(node): data = self._get_node_data(cnid) key = hash(data[-1]) if key in state: was_expanded, current_state = state[key] if was_expanded: self._expand_node(cnid) if current_state: tree.Expand(cnid) nodes_to_do.append(cnid) def expand_all(self): """Expands all nodes, starting from the selected node.""" tree = self._tree def _do_expand(nid): expanded, node, object = self._get_node_data(nid) if self._has_children(node, object): tree.SetItemHasChildren(nid, True) self._expand_node(nid) tree.Expand(nid) nid = tree.GetSelection() if nid.IsOk(): nodes_to_do = [nid] while nodes_to_do: node = nodes_to_do.pop() _do_expand(node) for n in self._nodes(node): _do_expand(n) nodes_to_do.append(n) def expand_levels(self, nid, levels, expand=True): """Expands from the specified node the specified number of sub-levels.""" if levels > 0: expanded, node, object = self._get_node_data(nid) if self._has_children(node, object): self._tree.SetItemHasChildren(nid, True) self._expand_node(nid) if expand: self._tree.Expand(nid) for cnid in self._nodes(nid): self.expand_levels(cnid, levels - 1) def update_editor(self): """Updates the editor when the object trait changes externally to the editor. """ tree = self._tree saved_state = {} if tree is not None: nid = tree.GetRootItem() if nid.IsOk(): self._delete_node(nid) object, node = self._node_for(self.value) if node is not None: icon = self._get_icon(node, object) self._root_nid = nid = tree.AddRoot( node.get_label(object), icon, icon ) self._map[id(object)] = [(node.get_children_id(object), nid)] self._add_listeners(node, object) self._set_node_data(nid, (False, node, object)) if self.factory.hide_root or self._has_children(node, object): tree.SetItemHasChildren(nid, True) self._expand_node(nid) if not self.factory.hide_root: tree.Expand(nid) tree.SelectItem(nid) self._on_tree_sel_changed() self.expand_levels(nid, self.factory.auto_open, False) # It seems like in some cases, an explicit Refresh is needed to # trigger a screen update: tree.Refresh() # fixme: Clear the current editor (if any)... def get_error_control(self): """Returns the editor's control for indicating error status.""" return self._tree def _append_node(self, nid, node, object): """Appends a new node to the specified node.""" return self._insert_node(nid, None, node, object) def _insert_node(self, nid, index, node, object): """Inserts a new node before a specified index into the children of the specified node. """ tree = self._tree icon = self._get_icon(node, object) label = node.get_label(object) if index is None: cnid = tree.AppendItem(nid, label, icon, icon) else: cnid = tree.InsertItem(nid, index, label, icon, icon) has_children = self._has_children(node, object) tree.SetItemHasChildren(cnid, has_children) self._set_node_data(cnid, (False, node, object)) self._map.setdefault(id(object), []).append( (node.get_children_id(object), cnid) ) self._add_listeners(node, object) # Automatically expand the new node (if requested): if has_children and node.can_auto_open(object): tree.Expand(cnid) # Return the newly created node: return cnid def _delete_node(self, nid): """Deletes a specified tree node and all of its children.""" for cnid in self._nodes_for(nid): self._delete_node(cnid) expanded, node, object = self._get_node_data(nid) id_object = id(object) object_info = self._map[id_object] for i, info in enumerate(object_info): if nid == info[1]: del object_info[i] break if len(object_info) == 0: self._remove_listeners(node, object) del self._map[id_object] # We set the '_locked' flag here because wx seems to generate a # 'node selected' event when the node is deleted. This can lead to # some bad side effects. So the 'node selected' event handler exits # immediately if the '_locked' flag is set: self._locked = True self._tree.Delete(nid) self._locked = False # If the deleted node had an active editor panel showing, remove it: if (self._editor is not None) and (nid == self._editor._editor_nid): self._clear_editor() def _expand_node(self, nid): """Expands the contents of a specified node (if required).""" expanded, node, object = self._get_node_data(nid) # Lazily populate the item's children: if not expanded: for child in node.get_children(object): child, child_node = self._node_for(child) if child_node is not None: self._append_node(nid, child_node, child) # Indicate the item is now populated: self._set_node_data(nid, (True, node, object)) def _nodes(self, nid): """Returns each of the child nodes of a specified node.""" tree = self._tree cnid, cookie = tree.GetFirstChild(nid) while cnid.IsOk(): yield cnid cnid, cookie = tree.GetNextChild(nid, cookie) def _nodes_for(self, nid): """Returns all child node ids of a specified node id.""" return [cnid for cnid in self._nodes(nid)] def _node_index(self, nid): pnid = self._tree.GetItemParent(nid) if not pnid.IsOk(): return (None, None, None) for i, cnid in enumerate(self._nodes(pnid)): if cnid == nid: ignore, pnode, pobject = self._get_node_data(pnid) return (pnode, pobject, i) def _has_children(self, node, object): """Returns whether a specified object has any children.""" return node.allows_children(object) and node.has_children(object) def _is_droppable(self, node, object, add_object, for_insert): """Returns whether a given object is droppable on the node.""" if for_insert and (not node.can_insert(object)): return False return node.can_add(object, add_object) def _drop_object(self, node, object, dropped_object, make_copy=True): """Returns a droppable version of a specified object.""" new_object = node.drop_object(object, dropped_object) if (new_object is not dropped_object) or (not make_copy): return new_object return copy.deepcopy(new_object) def _get_icon(self, node, object, is_expanded=False): """Returns the index of the specified object icon.""" if self._image_list is None: return -1 icon_name = node.get_icon(object, is_expanded) if isinstance(icon_name, str): if icon_name.startswith("@"): image = convert_image(icon_name, 3) if image is None: return -1 else: if icon_name[:1] == "<": icon_name = icon_name[1:-1] path = self else: path = node.get_icon_path(object) if isinstance(path, str): path = [path, node] else: path.append(node) image = ImageResource(icon_name, path).absolute_path elif isinstance(icon_name, ImageResource): image = icon_name.absolute_path else: raise ValueError( "Icon value must be a string or IImageResource instance: " + "given {!r}".format(icon_name) ) return self._image_list.GetIndex(image) def _add_listeners(self, node, object): """Adds the event listeners for a specified object.""" if node.allows_children(object): node.when_children_replaced(object, self._children_replaced, False) node.when_children_changed(object, self._children_updated, False) node.when_label_changed(object, self._label_updated, False) def _remove_listeners(self, node, object): """Removes any event listeners from a specified object.""" if node.allows_children(object): node.when_children_replaced(object, self._children_replaced, True) node.when_children_changed(object, self._children_updated, True) node.when_label_changed(object, self._label_updated, True) def _object_info(self, object, name=""): """Returns the tree node data for a specified object in the form ( expanded, node, nid ). """ info = self._map[id(object)] for name2, nid in info: if name == name2: break else: nid = info[0][1] expanded, node, ignore = self._get_node_data(nid) return (expanded, node, nid) def _object_info_for(self, object, name=""): """Returns the tree node data for a specified object as a list of the form: [ ( expanded, node, nid ), ... ]. """ result = [] for name2, nid in self._map[id(object)]: if name == name2: expanded, node, ignore = self._get_node_data(nid) result.append((expanded, node, nid)) return result def _node_for(self, object): """Returns the TreeNode associated with a specified object.""" if ( (isinstance(object, tuple)) and (len(object) == 2) and isinstance(object[1], TreeNode) ): return object # Select all nodes which understand this object: factory = self.factory nodes = [ node for node in factory.nodes if object is not None and node.is_node_for(object) ] # If only one found, we're done, return it: if len(nodes) == 1: return (object, nodes[0]) # If none found, try to create an adapted node for the object: if len(nodes) == 0: return (object, ITreeNodeAdapterBridge(adapter=object)) # Use all selected nodes that have the same 'node_for' list as the # first selected node: base = nodes[0].node_for nodes = [node for node in nodes if base == node.node_for] # If only one left, then return that node: if len(nodes) == 1: return (object, nodes[0]) # Otherwise, return a MultiTreeNode based on all selected nodes... # Use the node with no specified children as the root node. If not # found, just use the first selected node as the 'root node': root_node = None for i, node in enumerate(nodes): if node.get_children_id(object) == "": root_node = node del nodes[i] break else: root_node = nodes[0] # If we have a matching MultiTreeNode already cached, return it: key = (root_node,) + tuple(nodes) if key in factory.multi_nodes: return (object, factory.multi_nodes[key]) # Otherwise create one, cache it, and return it: factory.multi_nodes[key] = multi_node = MultiTreeNode( root_node=root_node, nodes=nodes ) return (object, multi_node) def _node_for_class(self, klass): """Returns the TreeNode associated with a specified class.""" for node in self.factory.nodes: if issubclass(klass, tuple(node.node_for)): return node return None def _update_icon(self, event, is_expanded): """Updates the icon for a specified node.""" self._update_icon_for_nid(event.GetItem()) def _update_icon_for_nid(self, nid): """Updates the icon for a specified node ID.""" if self._image_list is not None: expanded, node, object = self._get_node_data(nid) icon = self._get_icon(node, object, expanded) self._tree.SetItemImage(nid, icon, wx.TreeItemIcon_Normal) self._tree.SetItemImage(nid, icon, wx.TreeItemIcon_Selected) def _unpack_event(self, event): """Unpacks an event to see whether a tree item was involved.""" try: point = event.GetPosition() except: point = event.GetPoint() nid = None if hasattr(event, "GetItem"): nid = event.GetItem() if (nid is None) or (not nid.IsOk()): nid, flags = self._tree.HitTest(point) if nid.IsOk(): return self._get_node_data(nid) + (nid, point) return (None, None, None, nid, point) def _hit_test(self, point): """Returns information about the node at a specified point.""" nid, flags = self._tree.HitTest(point) if nid.IsOk(): return self._get_node_data(nid) + (nid, point) return (None, None, None, nid, point) def _begin_undo(self): """Begins an "undoable" transaction.""" ui = self.ui self._undoable.append(ui._undoable) if (ui._undoable == -1) and (ui.history is not None): ui._undoable = ui.history.now def _end_undo(self): if self._undoable.pop() == -1: self.ui._undoable = -1 def _get_undo_item(self, object, name, event): return ListUndoItem( object=object, name=name, index=event.index, added=event.added, removed=event.removed, ) def _undoable_append(self, node, object, data, make_copy=True): """Performs an undoable append operation.""" try: self._begin_undo() if make_copy: data = copy.deepcopy(data) node.append_child(object, data) finally: self._end_undo() def _undoable_insert(self, node, object, index, data, make_copy=True): """Performs an undoable insert operation.""" try: self._begin_undo() if make_copy: data = copy.deepcopy(data) node.insert_child(object, index, data) finally: self._end_undo() def _undoable_delete(self, node, object, index): """Performs an undoable delete operation.""" try: self._begin_undo() node.delete_child(object, index) finally: self._end_undo() def _get_object_nid(self, object, name=""): """Gets the ID associated with a specified object (if any).""" info = self._map.get(id(object)) if info is None: return None for name2, nid in info: if name == name2: return nid else: return info[0][1] def _clear_editor(self): """Clears the current editor pane (if any).""" editor = self._editor if editor._node_ui is not None: editor.SetSizer(None) editor._node_ui.dispose() editor._node_ui = editor._editor_nid = None def _get_node_data(self, nid): """Gets the node specific data.""" if nid == self._root_nid: return self._root_nid_data return self._tree.GetItemData(nid) def _set_node_data(self, nid, data): """Sets the node specific data.""" if nid == self._root_nid: self._root_nid_data = data else: self._tree.SetItemData(nid, data) # ----- User callable methods: -------------------------------------------- def get_object(self, nid): """Gets the object associated with a specified node.""" return self._get_node_data(nid)[2] def get_parent(self, object, name=""): """Returns the object that is the immmediate parent of a specified object in the tree. """ nid = self._get_object_nid(object, name) if nid is not None: pnid = self._tree.GetItemParent(nid) if pnid.IsOk(): return self.get_object(pnid) return None def get_node(self, object, name=""): """Returns the node associated with a specified object.""" nid = self._get_object_nid(object, name) if nid is not None: return self._get_node_data(nid)[1] return None # -- Tree Event Handlers: ------------------------------------------------- def _on_tree_item_expanding(self, event): """Handles a tree node expanding.""" if self._veto: self._veto = False event.Veto() return nid = event.GetItem() tree = self._tree expanded, node, object = self._get_node_data(nid) # If 'auto_close' requested for this node type, close all of the node's # siblings: if node.can_auto_close(object): snid = nid while True: snid = tree.GetPrevSibling(snid) if not snid.IsOk(): break tree.Collapse(snid) snid = nid while True: snid = tree.GetNextSibling(snid) if not snid.IsOk(): break tree.Collapse(snid) # Expand the node (i.e. populate its children if they are not there # yet): self._expand_node(nid) def _on_tree_item_expanded(self, event): """Handles a tree node being expanded.""" self._update_icon(event, True) def _on_tree_item_collapsing(self, event): """Handles a tree node collapsing.""" if self._veto: self._veto = False event.Veto() def _on_tree_item_collapsed(self, event): """Handles a tree node being collapsed.""" self._update_icon(event, False) def _on_tree_sel_changed(self, event=None): """Handles a tree node being selected.""" if self._locked: return # Get the new selection: object = None not_handled = True nids = self._tree.GetSelections() selected = [self._get_node_data(nid)[2] for nid in nids if nid.IsOk()] first = True for nid in nids: if not nid.IsOk(): continue # If there is a real selection, get the associated object: expanded, sel_node, sel_object = self._get_node_data(nid) # Try to inform the node specific handler of the selection, # if there are multiple selections, we only care about the # first (or maybe the last makes more sense?) if first: node = sel_node object = sel_object not_handled = node.select(object) first = False # Set the value of the new selection: if self.factory.selection_mode == "single": self._no_update_selected = True self.selected = object self._no_update_selected = False else: self._no_update_selected = True self.selected = selected self._no_update_selected = False # If no one has been notified of the selection yet, inform the editor's # select handler (if any) of the new selection: if not_handled is True: self.ui.evaluate(self.factory.on_select, object) # Check to see if there is an associated node editor pane: editor = self._editor if editor is not None: # If we already had a node editor, destroy it: editor.Freeze() self._clear_editor() # If there is a selected object, create a new editor for it: if object is not None: # Try to chain the undo history to the main undo history: view = node.get_view(object) if view is None or isinstance(view, str): view = object.trait_view(view) if (self.ui.history is not None) or (view.kind == "subpanel"): ui = object.edit_traits( parent=editor, view=view, kind="subpanel" ) else: # Otherwise, just set up our own new one: ui = object.edit_traits( parent=editor, view=view, kind="panel" ) # Make our UI the parent of the new UI: ui.parent = self.ui # Remember the new editor's UI and node info: editor._node_ui = ui editor._editor_nid = nid # Finish setting up the editor: sizer = wx.BoxSizer(wx.VERTICAL) sizer.Add(ui.control, 1, wx.EXPAND) editor.SetSizer(sizer) editor.Layout() # fixme: The following is a hack needed to make the editor window # (which is a wx.ScrolledWindow) recognize that its contents have # been changed: dx, dy = editor.GetSize() editor.SetSize(wx.Size(dx, dy + 1)) editor.SetSize(wx.Size(dx, dy)) # Allow the editor view to show any changes that have occurred: editor.Thaw() def _on_hover(self, event): """Handles the mouse moving over a tree node""" id, flags = self._tree.HitTest(event.GetPosition()) if flags & wx.TREE_HITTEST_ONITEMLABEL: expanded, node, object = self._get_node_data(id) if self.factory.on_hover is not None: self.ui.evaluate(self.factory.on_hover, object) self._veto = True elif self.factory and self.factory.on_hover is not None: self.ui.evaluate(self.factory.on_hover, None) # allow other events to be processed event.Skip(True) def _on_tree_item_activated(self, event): """Handles a tree item being activated.""" expanded, node, object = self._get_node_data(event.GetItem()) if node.activated(object) is True: if self.factory.on_activated is not None: self.ui.evaluate(self.factory.on_activated, object) self._veto = True else: self._veto = True # Fire the 'activated' event with the clicked on object as value: self.activated = object # FIXME: Firing the dclick event also for backward compatibility on wx. # Change it occur on mouse double click only. self.dclick = object def _on_tree_begin_label_edit(self, event): """Handles the user starting to edit a tree node label.""" item = event.GetItem() parent = self._tree.GetItemParent(item) can_rename = True if parent.IsOk(): expanded, node, object = self._get_node_data(parent) can_rename = node.can_rename(object) if can_rename: expanded, node, object = self._get_node_data(item) if node.can_rename_me(object): return event.Veto() def _on_tree_end_label_edit(self, event): """Handles the user completing tree node label editing.""" label = event.GetLabel() if len(label) > 0: expanded, node, object = self._get_node_data(event.GetItem()) # Tell the node to change the label. If it raises an exception, # that means it didn't like the label, so veto the tree node # change: try: node.set_label(object, label) return except: pass event.Veto() def _on_tree_begin_drag(self, event): """Handles a drag operation starting on a tree node.""" if PythonDropSource is not None: expanded, node, object, nid, point = self._unpack_event(event) if node is not None: try: self._dragging = nid PythonDropSource(self._tree, node.get_drag_object(object)) finally: self._dragging = None def _on_tree_item_gettooltip(self, event): """Handles a tooltip request on a tree node.""" nid = event.GetItem() if nid.IsOk(): node_data = self._get_node_data(nid) if node_data is not None: expanded, node, object = node_data tooltip = node.get_tooltip(object) if tooltip != "": event.SetToolTip(tooltip) event.Skip() def _on_left_dclick(self, event): """Handle left mouse dclick to emit dclick event for associated node.""" # Determine what node (if any) was clicked on: expanded, node, object, nid, point = self._unpack_event(event) # If the mouse is over a node, then process the click: if node is not None: if node.dclick(object) is True: if self.factory.on_dclick is not None: self.ui.evaluate(self.factory.on_dclick, object) self._veto = True else: self._veto = True # Fire the 'dclick' event with the object as its value: # FIXME: This is instead done in _on_item_activated for backward # compatibility only on wx toolkit. # self.dclick = object # Allow normal mouse event processing to occur: event.Skip() def _on_left_down(self, event): """Handles the user right clicking on a tree node.""" # Determine what node (if any) was clicked on: expanded, node, object, nid, point = self._unpack_event(event) # If the mouse is over a node, then process the click: if node is not None: if (node.click(object) is True) and ( self.factory.on_click is not None ): self.ui.evaluate(self.factory.on_click, object) # Fire the 'click' event with the object as its value: self.click = object # Allow normal mouse event processing to occur: event.Skip() def _on_right_down(self, event): """Handles the user right clicking on a tree node.""" expanded, node, object, nid, point = self._unpack_event(event) if node is not None: self._data = (node, object, nid) self._context = { "object": object, "editor": self, "node": node, "info": self.ui.info, "handler": self.ui.handler, } # Try to get the parent node of the node clicked on: pnid = self._tree.GetItemParent(nid) if pnid.IsOk(): ignore, parent_node, parent_object = self._get_node_data(pnid) else: parent_node = parent_object = None self._menu_node = node self._menu_parent_node = parent_node self._menu_parent_object = parent_object menu = node.get_menu(object) if menu is None: # Use the standard, default menu: menu = self._standard_menu(node, object) elif isinstance(menu, Menu): # Use the menu specified by the node: group = menu.find_group(NewAction) if group is not None: # Reset the group for the current usage in case it is # shared - the call to `node.get_add()` is potentially # dynamic and the callback `_menu_new_node()` captures # state about this particular TreeEditor instance group.clear() actions = self._new_actions(node, object) if len(actions) > 0: group.insert(0, Menu(name="New", *actions)) else: # All other values mean no menu should be displayed: menu = None # Only display the menu if a valid menu is defined: if menu is not None: wxmenu = menu.create_menu(self._tree, self) self._tree.PopupMenu(wxmenu, point[0] - 10, point[1] - 10) wxmenu.Destroy() # Reset all menu related cached values: self._data = ( self._context ) = ( self._menu_node ) = self._menu_parent_node = self._menu_parent_object = None def _standard_menu(self, node, object): """Returns the standard contextual pop-up menu.""" actions = [ CutAction, CopyAction, PasteAction, Separator(), DeleteAction, Separator(), RenameAction, ] # See if the 'New' menu section should be added: items = self._new_actions(node, object) if len(items) > 0: actions[0:0] = [Menu(name="New", *items), Separator()] return Menu(*actions) def _new_actions(self, node, object): """Returns a list of Actions that will create new objects.""" object = self._data[1] items = [] add = node.get_add(object) # return early if there are no items to be added in the tree if len(add) == 0: return items for klass in add: prompt = False factory = None if isinstance(klass, tuple): if len(klass) == 2: klass, prompt = klass elif len(klass) == 3: klass, prompt, factory = klass add_node = self._node_for_class(klass) if add_node is None: continue class_name = klass.__name__ name = add_node.get_name(object) if name == "": name = class_name if not factory: factory = klass def perform_add(object, factory, prompt): self._menu_new_node(factory, prompt) on_perform = partial(perform_add, factory=factory, prompt=prompt) items.append(Action(name=name, on_perform=on_perform)) return items def _is_copyable(self, object): parent = self._menu_parent_node if isinstance(parent, ObjectTreeNode): return parent.can_copy(self._menu_parent_object) return (parent is not None) and parent.can_copy(object) def _is_cutable(self, object): parent = self._menu_parent_node if isinstance(parent, ObjectTreeNode): can_cut = parent.can_copy( self._menu_parent_object ) and parent.can_delete(self._menu_parent_object) else: can_cut = ( (parent is not None) and parent.can_copy(object) and parent.can_delete(object) ) return can_cut and self._menu_node.can_delete_me(object) def _is_pasteable(self, object): from pyface.wx.clipboard import clipboard return self._menu_node.can_add(object, clipboard.object_type) def _is_deletable(self, object): parent = self._menu_parent_node if isinstance(parent, ObjectTreeNode): can_delete = parent.can_delete(self._menu_parent_object) else: can_delete = (parent is not None) and parent.can_delete(object) return can_delete and self._menu_node.can_delete_me(object) def _is_renameable(self, object): parent = self._menu_parent_node if isinstance(parent, ObjectTreeNode): can_rename = parent.can_rename(self._menu_parent_object) elif parent is not None: can_rename = parent.can_rename(object) else: can_rename = True return can_rename and self._menu_node.can_rename_me(object) # ----- Drag and drop event handlers: ------------------------------------- def wx_dropped_on(self, x, y, data, drag_result): """Handles a Python object being dropped on the tree.""" if isinstance(data, list): rc = wx.DragNone for item in data: rc = self.wx_dropped_on(x, y, item, drag_result) return rc expanded, node, object, nid, point = self._hit_test(wx.Point(x, y)) if node is not None: if drag_result == wx.DragMove: if not self._is_droppable(node, object, data, False): return wx.DragNone if self._dragging is not None: data = self._drop_object(node, object, data, False) if data is not None: try: self._begin_undo() self._undoable_delete( *self._node_index(self._dragging) ) self._undoable_append(node, object, data, False) finally: self._end_undo() else: data = self._drop_object(node, object, data) if data is not None: self._undoable_append(node, object, data, False) return drag_result to_node, to_object, to_index = self._node_index(nid) if to_node is not None: if self._dragging is not None: data = self._drop_object(node, to_object, data, False) if data is not None: from_node, from_object, from_index = self._node_index( self._dragging ) if (to_object is from_object) and ( to_index > from_index ): to_index -= 1 try: self._begin_undo() self._undoable_delete( from_node, from_object, from_index ) self._undoable_insert( to_node, to_object, to_index, data, False ) finally: self._end_undo() else: data = self._drop_object(to_node, to_object, data) if data is not None: self._undoable_insert( to_node, to_object, to_index, data, False ) return drag_result return wx.DragNone def wx_drag_over(self, x, y, data, drag_result): """Handles a Python object being dragged over the tree.""" expanded, node, object, nid, point = self._hit_test(wx.Point(x, y)) insert = False if (node is not None) and (drag_result == wx.DragCopy): node, object, index = self._node_index(nid) insert = True if (self._dragging is not None) and ( not self._is_drag_ok(self._dragging, data, object) ): return wx.DragNone if (node is not None) and self._is_droppable( node, object, data, insert ): return drag_result return wx.DragNone def _is_drag_ok(self, snid, source, target): if (snid is None) or (target is source): return False for cnid in self._nodes(snid): if not self._is_drag_ok( cnid, self._get_node_data(cnid)[2], target ): return False return True # ----- pyface.action 'controller' interface implementation: -------------- def add_to_menu(self, menu_item): """Adds a menu item to the menu bar being constructed.""" action = menu_item.item.action self.eval_when(action.enabled_when, menu_item, "enabled") self.eval_when(action.checked_when, menu_item, "checked") def add_to_toolbar(self, toolbar_item): """Adds a toolbar item to the toolbar being constructed.""" self.add_to_menu(toolbar_item) def can_add_to_menu(self, action): """Returns whether the action should be defined in the user interface.""" if action.defined_when != "": if not eval(action.defined_when, globals(), self._context): return False if action.visible_when != "": if not eval(action.visible_when, globals(), self._context): return False return True def can_add_to_toolbar(self, action): """Returns whether the toolbar action should be defined in the user interface. """ return self.can_add_to_menu(action) def perform(self, action, action_event=None): """Performs the action described by a specified Action object.""" self.ui.do_undoable(self._perform, action) def _perform(self, action): node, object, nid = self._data method_name = action.action info = self.ui.info handler = self.ui.handler if method_name.find(".") >= 0: if method_name.find("(") < 0: method_name += "()" try: eval( method_name, globals(), { "object": object, "editor": self, "node": node, "info": info, "handler": handler, }, ) except: from traitsui.api import raise_to_debug raise_to_debug() return method = getattr(handler, method_name, None) if method is not None: method(info, object) return if action.on_perform is not None: action.on_perform(object) # ----- Menu support methods: --------------------------------------------- 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 if not eval(condition, globals(), self._context): value = False setattr(object, trait, value) # ----- Menu event handlers: ---------------------------------------------- def _menu_copy_node(self): """Copies the current tree node object to the paste buffer.""" from pyface.wx.clipboard import clipboard clipboard.data = self._data[1] self._data = None def _menu_cut_node(self): """Cuts the current tree node object into the paste buffer.""" from pyface.wx.clipboard import clipboard node, object, nid = self._data clipboard.data = object self._data = None self._undoable_delete(*self._node_index(nid)) def _menu_paste_node(self): """Pastes the current contents of the paste buffer into the current node. """ from pyface.wx.clipboard import clipboard node, object, nid = self._data self._data = None self._undoable_append(node, object, clipboard.object_data, False) def _menu_delete_node(self): """Deletes the current node from the tree.""" node, object, nid = self._data self._data = None rc = node.confirm_delete(object) if rc is not False: if rc is not True: if self.ui.history is None: # If no undo history, ask user to confirm the delete: dlg = wx.MessageDialog( self._tree, "Are you sure you want to delete %s?" % node.get_label(object), "Confirm Deletion", style=wx.OK | wx.CANCEL | wx.ICON_EXCLAMATION, ) if dlg.ShowModal() != wx.ID_OK: return self._undoable_delete(*self._node_index(nid)) def _menu_rename_node(self): """Renames the current tree node.""" node, object, nid = self._data self._data = None object_label = ObjectLabel(label=node.get_label(object)) if object_label.edit_traits().result: label = object_label.label.strip() if label != "": node.set_label(object, label) def _menu_new_node(self, factory, prompt=False): """Adds a new object to the current node.""" node, object, nid = self._data self._data = None new_object = factory() if new_object is None: return # support None-type returns from factory if (not prompt) or new_object.edit_traits( parent=self.control, kind="livemodal" ).result: self._undoable_append(node, object, new_object, False) # Automatically select the new object if editing is being # performed: if self.factory.editable: self._tree.SelectItem(self._tree.GetLastChild(nid)) # -- Model event handlers ------------------------------------------------- def _children_replaced(self, object, name="", new=None): """Handles the children of a node being completely replaced.""" tree = self._tree for expanded, node, nid in self._object_info_for(object, name): children = node.get_children(object) # Only add/remove the changes if the node has already been # expanded: if expanded: # Delete all current child nodes: for cnid in self._nodes_for(nid): self._delete_node(cnid) # Add all of the children back in as new nodes: for child in children: child, child_node = self._node_for(child) if child_node is not None: self._append_node(nid, child_node, child) # Indicate whether the node has any children now: tree.SetItemHasChildren(nid, len(children) > 0) # Try to expand the node (if requested): if node.can_auto_open(object): tree.Expand(nid) def _children_updated(self, object, name, event): """Handles the children of a node being changed.""" # Log the change that was made (removing '_items' from the end of the # name): name = name[:-6] self.log_change(self._get_undo_item, object, name, event) start = event.index end = start + len(event.removed) tree = self._tree for expanded, node, nid in self._object_info_for(object, name): n = len(node.get_children(object)) # Only add/remove the changes if the node has already been # expanded: if expanded: # Remove all of the children that were deleted: for cnid in self._nodes_for(nid)[start:end]: self._delete_node(cnid) # Add all of the children that were added: remaining = n - len(event.removed) child_index = 0 for child in event.added: child, child_node = self._node_for(child) if child_node is not None: insert_index = ( (start + child_index) if (start < remaining) else None ) self._insert_node(nid, insert_index, child_node, child) child_index += 1 # Indicate whether the node has any children now: tree.SetItemHasChildren(nid, n > 0) # Try to expand the node (if requested): root = tree.GetRootItem() if node.can_auto_open(object): if (nid != root) or not self.factory.hide_root: tree.Expand(nid) def _label_updated(self, object, name, label): """Handles the label of an object being changed.""" nids = {} for name2, nid in self._map[id(object)]: if nid not in nids: nids[nid] = None node = self._get_node_data(nid)[1] self._tree.SetItemText(nid, node.get_label(object)) self._update_icon_for_nid(nid) # -- UI preference save/restore interface --------------------------------- def restore_prefs(self, prefs): """Restores any saved user preference information associated with the editor. """ if self._is_dock_window: if isinstance(prefs, dict): structure = prefs.get("structure") else: structure = prefs self.control.GetSizer().SetStructure(self.control, structure) def save_prefs(self): """Returns any user preference information associated with the editor.""" if self._is_dock_window: return {"structure": self.control.GetSizer().GetStructure()} return None # -- End UI preference save/restore interface ----------------------------- class ObjectLabel(HasStrictTraits): """An editable label for an object.""" # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- #: Label to be edited label = Str() # ------------------------------------------------------------------------- # Traits view definition: # ------------------------------------------------------------------------- traits_view = View( "label", title="Edit Label", kind="modal", buttons=["OK", "Cancel"] ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/wx/tuple_editor.py0000644000175100001730000000172600000000000021546 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Defines the tuple editor for the wxPython user interface toolkit. """ from traitsui.editors.tuple_editor import SimpleEditor as BaseSimpleEditor from .editor import Editor # ------------------------------------------------------------------------- # 'SimpleEditor' class: # ------------------------------------------------------------------------- class SimpleEditor(BaseSimpleEditor, Editor): """Simple style of editor for tuples. The editor displays an editor for each of the fields in the tuple, based on the type of each field. """ pass ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/wx/ui_base.py0000644000175100001730000001564700000000000020465 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Defines the base class for the wxPython-based Traits UI modal and non-modal dialogs. """ import wx from pyface.action.schema.api import ( ActionManagerBuilder, MenuBarSchema, ToolBarSchema, ) from traits.api import HasPrivateTraits, Instance from traitsui.base_panel import BasePanel as _BasePanel from traitsui.menu import Action from .editor import Editor from .helper import restore_window class ButtonEditor(Editor): """Editor for buttons.""" # Action associated with the button action = Instance(Action) def __init__(self, **traits): # XXX Why does this need to be an Editor subclass? -- CJW HasPrivateTraits.__init__(self, **traits) def perform(self, event): """Handles the associated button being clicked.""" handler = self.ui.handler self.ui.do_undoable(handler.perform, self.ui.info, self.action, event) class BaseDialog(_BasePanel): """Base class for Traits UI dialog boxes.""" # The different dialog styles. NONMODAL = 0 MODAL = 1 POPUP = 2 POPOVER = 3 INFO = 4 # Types of 'popup' dialogs: POPUP_TYPES = {POPUP, POPOVER, INFO} def init(self, ui, parent, style): """Initialise the dialog by creating the controls.""" raise NotImplementedError @staticmethod def display_ui(ui, parent, style): ui.owner.init(ui, parent, style) ui.control = ui.owner.control ui.control._parent = parent try: ui.prepare_ui() except: ui.control.Destroy() ui.control.ui = None ui.control = None ui.owner = None ui.result = False raise ui.handler.position(ui.info) restore_window(ui, is_popup=(style in BaseDialog.POPUP_TYPES)) ui.control.Layout() # Check if the control is already being displayed modally. This would be # the case if after the window was displayed, some event caused the ui to # get rebuilt (typically when the user fires the 'updated' event on the ui # ). In this case, calling ShowModal again leads to the parent window # hanging even after the control has been closed by clicking OK or Cancel # (maybe the modal mode isn't ending?) if style == BaseDialog.MODAL and not ui.control.IsModal(): ui.control.ShowModal() else: ui.control.Show() def default_icon(self): """Return a default icon for a TraitsUI dialog.""" from pyface.image_resource import ImageResource return ImageResource("frame.ico") def set_icon(self, icon=None): """Sets the frame's icon.""" from pyface.image_resource import ImageResource if not isinstance(icon, ImageResource): icon = self.default_icon() self.control.SetIcon(icon.create_icon()) def add_statusbar(self): """Adds a status bar to the dialog.""" ui = self.ui statusbar = ui.view.statusbar context = ui.context if statusbar is not None: widths = [] listeners = [] control = wx.StatusBar(self.control) control.SetFieldsCount(len(statusbar)) for i, item in enumerate(statusbar): width = abs(item.width) if width <= 1.0: widths.append(-max(1, int(1000 * width))) else: widths.append(int(width)) set_text = self._set_status_text(control, i) name = item.name control.SetStatusText(ui.get_extended_value(name), i) col = name.find(".") object = "object" if col >= 0: object = name[:col] name = name[col + 1 :] object = context[object] object.observe(set_text, name, dispatch="ui") listeners.append((object, set_text, name)) control.SetStatusWidths(widths) self.control.SetStatusBar(control) ui._statusbar = listeners def _set_status_text(self, control, i): def set_status_text(event): text = event.new control.SetStatusText(text, i) return set_status_text def add_menubar(self): """Adds a menu bar to the dialog.""" menubar = self.ui.view.menubar menubar = self.ui.view.menubar if isinstance(menubar, MenuBarSchema): builder = self.ui.view.action_manager_builder menubar = builder.create_action_manager(menubar) if menubar is not None: self._last_group = self._last_parent = None self.control.SetMenuBar( menubar.create_menu_bar(self.control, self) ) self._last_group = self._last_parent = None def add_toolbar(self): """Adds a toolbar to the dialog.""" toolbar = self.ui.view.toolbar if isinstance(toolbar, ToolBarSchema): builder = self.ui.view.action_manager_builder toolbar = builder.create_action_manager(toolbar) if toolbar is not None: self._last_group = self._last_parent = None self.control.SetToolBar( toolbar.create_tool_bar(self.control, self) ) self._last_group = self._last_parent = None def add_button( self, action, sizer, method=None, enabled=True, name=None, default=False, ): """Creates a button.""" ui = self.ui if (action.defined_when != "") and ( not ui.eval_when(action.defined_when) ): return None if name is None: name = action.name id = action.id button = wx.Button(self.control, -1, name) button.Enable(enabled) if default: button.SetDefault() if (method is None) or (action.enabled_when != "") or (id != ""): editor = ButtonEditor(ui=ui, action=action, control=button) if id != "": ui.info.bind(id, editor) if action.visible_when != "": ui.add_visible(action.visible_when, editor) if action.enabled_when != "": ui.add_enabled(action.enabled_when, editor) if method is None: method = editor.perform self.control.Bind(wx.EVT_BUTTON, method, id=button.GetId()) sizer.Add(button, 0, wx.LEFT, 5) if action.tooltip != "": button.SetToolTip(action.tooltip) return button ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/wx/ui_editor.py0000644000175100001730000000174200000000000021030 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Defines the BasicUIEditor class, which allows creating editors that define their function by creating an embedded Traits UI. """ from traitsui.ui_editor import UIEditor as BaseUIEditor from .editor import Editor # ------------------------------------------------------------------------- # 'UIEditor' base class: # ------------------------------------------------------------------------- class UIEditor(BaseUIEditor, Editor): """An editor that creates an embedded Traits UI.""" pass # -- End UI preference save/restore interface ----------------------------- ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/wx/ui_live.py0000644000175100001730000003441100000000000020500 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Creates a wxPython user interface for a specified UI object, where the UI is "live", meaning that it immediately updates its underlying object(s). """ import wx from pyface.api import SystemMetrics from .helper import save_window, TraitsUIScrolledPanel from .ui_base import BaseDialog from .ui_panel import panel from .constants import DefaultTitle, WindowColor, scrollbar_dx from traitsui.undo import UndoHistory from traitsui.menu import ( UndoButton, RevertButton, OKButton, CancelButton, HelpButton, ) # ------------------------------------------------------------------------- # Creates a 'live update' wxPython user interface for a specified UI object: # ------------------------------------------------------------------------- def ui_live(ui, parent): """Creates a live, non-modal wxPython user interface for a specified UI object. """ _ui_dialog(ui, parent, BaseDialog.NONMODAL) def ui_livemodal(ui, parent): """Creates a live, modal wxPython user interface for a specified UI object.""" _ui_dialog(ui, parent, BaseDialog.MODAL) def ui_popup(ui, parent): """Creates a live, temporary popup wxPython user interface for a specified UI object. """ _ui_dialog(ui, parent, BaseDialog.POPUP) def ui_popover(ui, parent): """Creates a live, temporary popup wxPython user interface for a specified UI object. """ _ui_dialog(ui, parent, BaseDialog.POPOVER) def ui_info(ui, parent): """Creates a live, temporary popup wxPython user interface for a specified UI object. """ _ui_dialog(ui, parent, BaseDialog.INFO) def _ui_dialog(ui, parent, style): """Creates a live wxPython user interface for a specified UI object.""" if ui.owner is None: ui.owner = LiveWindow() BaseDialog.display_ui(ui, parent, style) class LiveWindow(BaseDialog): """User interface window that immediately updates its underlying object(s).""" def init(self, ui, parent, style): self.is_modal = style == self.MODAL window_style = 0 view = ui.view if view.resizable: window_style |= wx.RESIZE_BORDER title = view.title if title == "": title = DefaultTitle history = ui.history window = ui.control if window is not None: if history is not None: history.observe( self._on_undoable, "undoable", remove=True, dispatch="ui" ) history.observe( self._on_redoable, "redoable", remove=True, dispatch="ui" ) history.observe( self._on_revertable, "undoable", remove=True, dispatch="ui" ) window.SetSizer(None) ui.reset() else: self.ui = ui if style == self.MODAL: if view.resizable: window_style |= wx.MAXIMIZE_BOX | wx.MINIMIZE_BOX window = wx.Dialog( parent, -1, title, style=window_style | wx.DEFAULT_DIALOG_STYLE, ) elif style == self.NONMODAL: if parent is not None: window_style |= ( wx.FRAME_FLOAT_ON_PARENT | wx.FRAME_NO_TASKBAR ) window = wx.Frame( parent, -1, title, style=window_style | (wx.DEFAULT_FRAME_STYLE & (~wx.RESIZE_BORDER)), ) else: if window_style == 0: window_style = wx.SIMPLE_BORDER if parent is not None: window_style |= ( wx.FRAME_FLOAT_ON_PARENT | wx.FRAME_NO_TASKBAR ) if isinstance(parent, tuple): window = wx.Frame(None, -1, "", style=window_style) window._control_region = parent else: window = wx.Frame(parent, -1, "", style=window_style) window._kind = ui.view.kind self._monitor = MouseMonitor(ui) # Set the correct default window background color: window.SetBackgroundColour(WindowColor) self.control = window window.Bind(wx.EVT_CLOSE, self._on_close_page) window.Bind(wx.EVT_CHAR, self._on_key) self.set_icon(view.icon) buttons = [self.coerce_button(button) for button in view.buttons] nbuttons = len(buttons) no_buttons = (nbuttons == 1) and self.is_button(buttons[0], "") has_buttons = (not no_buttons) and ( (nbuttons > 0) or view.undo or view.revert or view.ok or view.cancel ) if has_buttons or (view.menubar is not None): if history is None: history = UndoHistory() else: history = None ui.history = history # Create the actual trait sheet panel and imbed it in a scrollable # window (if requested): sw_sizer = wx.BoxSizer(wx.VERTICAL) if ui.scrollable: sizer = wx.BoxSizer(wx.VERTICAL) sw = TraitsUIScrolledPanel(window) trait_sheet = panel(ui, sw) sizer.Add(trait_sheet, 1, wx.EXPAND) tsdx, tsdy = trait_sheet.GetSize() sw.SetScrollRate(16, 16) max_dy = (2 * SystemMetrics().screen_height) // 3 sw.SetSizer(sizer) sw.SetSize( wx.Size( tsdx + ((tsdy > max_dy) * scrollbar_dx), min(tsdy, max_dy) ) ) else: sw = panel(ui, window) sw_sizer.Add(sw, 1, wx.EXPAND) sw_sizer.SetMinSize(sw.GetSize()) # Check to see if we need to add any of the special function buttons: if (not no_buttons) and (has_buttons or view.help): sw_sizer.Add(wx.StaticLine(window, -1), 0, wx.EXPAND) b_sizer = wx.BoxSizer(wx.HORIZONTAL) # Convert all button flags to actual button actions if no buttons # were specified in the 'buttons' trait: if nbuttons == 0: if view.undo: self.check_button(buttons, UndoButton) if view.revert: self.check_button(buttons, RevertButton) if view.ok: self.check_button(buttons, OKButton) if view.cancel: self.check_button(buttons, CancelButton) if view.help: self.check_button(buttons, HelpButton) # Create a button for each button action: for raw_button, button in zip(view.buttons, buttons): button = self.coerce_button(button) default = raw_button == view.default_button if self.is_button(button, "Undo"): self.undo = self.add_button( button, b_sizer, self._on_undo, False, default=default ) self.redo = self.add_button( button, b_sizer, self._on_redo, False, "Redo" ) history.observe( self._on_undoable, "undoable", dispatch="ui" ) history.observe( self._on_redoable, "redoable", dispatch="ui" ) if history.can_undo: self._on_undoable(True) if history.can_redo: self._on_redoable(True) elif self.is_button(button, "Revert"): self.revert = self.add_button( button, b_sizer, self._on_revert, False, default=default, ) history.observe( self._on_revertable, "undoable", dispatch="ui" ) if history.can_undo: self._on_revertable(True) elif self.is_button(button, "OK"): self.ok = self.add_button( button, b_sizer, self._on_ok, default=default ) ui.observe(self._on_error, "errors", dispatch="ui") elif self.is_button(button, "Cancel"): self.add_button( button, b_sizer, self._on_cancel, default=default ) elif self.is_button(button, "Help"): self.add_button( button, b_sizer, self._on_help, default=default ) elif not self.is_button(button, ""): self.add_button(button, b_sizer, default=default) sw_sizer.Add(b_sizer, 0, wx.ALIGN_RIGHT | wx.ALL, 5) # Add the menu bar, tool bar and status bar (if any): self.add_menubar() self.add_toolbar() self.add_statusbar() # Lay all of the dialog contents out: window.SetSizer(sw_sizer) window.Fit() def close(self, rc=wx.ID_OK): """Closes the dialog window.""" ui = self.ui ui.result = rc == wx.ID_OK save_window(ui) if self.is_modal: self.control.EndModal(rc) self.control.Unbind(wx.EVT_CLOSE) self.control.Unbind(wx.EVT_CHAR) ui.finish() self.ui = self.undo = self.redo = self.revert = self.control = None def _on_close_page(self, event): """Handles the user clicking the window/dialog "close" button/icon.""" if not self.ui.view.close_result: self._on_cancel(event) else: self._on_ok(event) def _on_close_popup(self, event): """Handles the user giving focus to another window for a 'popup' view.""" if not event.GetActive(): self.close_popup() def close_popup(self): # Close the window if it has not already been closed: if self.ui.info is not None and self.ui.info.ui is not None: if self._on_ok(): self._monitor.Stop() def _on_ok(self, event=None): """Handles the user clicking the **OK** button.""" if self.ui is None or self.control is None: return True if self.ui.handler.close(self.ui.info, True): self.control.Unbind(wx.EVT_ACTIVATE) self.close(wx.ID_OK) return True return False def _on_key(self, event): """Handles the user pressing the Escape key.""" if event.GetKeyCode() == 0x1B: self._on_close_page(event) def _on_cancel(self, event): """Handles a request to cancel all changes.""" if self.ui.handler.close(self.ui.info, False): self._on_revert(event) self.close(wx.ID_CANCEL) def _on_error(self, event): """Handles editing errors.""" errors = event.new self.ok.Enable(errors == 0) def _on_undoable(self, event): """Handles a change to the "undoable" state of the undo history""" state = event.new self.undo.Enable(state) def _on_redoable(self, event): """Handles a change to the "redoable state of the undo history.""" state = event.new self.redo.Enable(state) def _on_revertable(self, event): """Handles a change to the "revert" state.""" state = event.new self.revert.Enable(state) class MouseMonitor(wx.Timer): """Monitors a specified window and closes it the first time the mouse pointer leaves the window. """ def __init__(self, ui): super().__init__() self.ui = ui kind = ui.view.kind self.is_activated = self.is_info = kind == "info" self.border = 3 if kind == "popup": self.border = 10 self.Start(100) def Notify(self): ui = self.ui control = ui.control if ui.control is None: # Looks like someone forgot to tell us that the ui has been closed: self.Stop() return mx, my = wx.GetMousePosition() cx, cy = control.ClientToScreen(0, 0) cdx, cdy = control.GetSize() if self.is_activated: # Don't close the popup if any mouse buttons are currently pressed: ms = wx.GetMouseState() if ms.LeftIsDown() or ms.MiddleIsDown() or ms.RightIsDown(): return # Check for the special case of the mouse pointer having to be # within the original bounds of the object the popup was created # for: if self.is_info: parent = control.GetParent() if isinstance(parent, wx.Window): px, py, pdx, pdy = parent.GetScreenRect() else: px, py, pdx, pdy = control._control_region if ( (mx < px) or (mx >= (px + pdx)) or (my < py) or (my >= (py + pdy)) ): ui.owner.close_popup() self.is_activated = False else: # Allow for a 'dead zone' border around the window to allow for # small motor control problems: border = self.border if ( (mx < (cx - border)) or (mx >= (cx + cdx + border)) or (my < (cy - border)) or (my >= (cy + cdy + border)) ): ui.owner.close_popup() self.is_activated = False elif (cx <= mx < (cx + cdx)) and (cy <= my < (cy + cdy)): self.is_activated = True ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/wx/ui_modal.py0000644000175100001730000002273300000000000020641 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Creates a wxPython user interface for a specified UI object. """ import wx from pyface.api import SystemMetrics from .helper import save_window, TraitsUIScrolledPanel from .ui_base import BaseDialog from .ui_panel import panel from .constants import DefaultTitle, WindowColor, scrollbar_dx from traitsui.menu import ( ApplyButton, RevertButton, OKButton, CancelButton, HelpButton, ) def ui_modal(ui, parent): """Creates a modal wxPython user interface for a specified UI object.""" _ui_dialog(ui, parent, BaseDialog.MODAL) def ui_nonmodal(ui, parent): """Creates a non-modal wxPython user interface for a specified UI object.""" _ui_dialog(ui, parent, BaseDialog.NONMODAL) def _ui_dialog(ui, parent, style): """Creates a wxPython dialog box for a specified UI object. Changes are not immediately applied to the underlying object. The user must click **Apply** or **OK** to apply changes. The user can revert changes by clicking **Revert** or **Cancel**. """ if ui.owner is None: ui.owner = ModalDialog() BaseDialog.display_ui(ui, parent, style) class ModalDialog(BaseDialog): """Modal dialog box for Traits-based user interfaces.""" def init(self, ui, parent, style): self.is_modal = style == self.MODAL style = 0 view = ui.view if view.resizable: style |= wx.RESIZE_BORDER title = view.title if title == "": title = DefaultTitle revert = apply = False window = ui.control if window is not None: window.SetSizer(None) ui.reset() if hasattr(self, "revert"): revert = self.revert.IsEnabled() if hasattr(self, "apply"): apply = self.apply.IsEnabled() else: self.ui = ui if self.is_modal: window = wx.Dialog( parent, -1, title, style=style | wx.DEFAULT_DIALOG_STYLE ) else: window = wx.Frame( parent, -1, title, style=style | (wx.DEFAULT_FRAME_STYLE & (~wx.RESIZE_BORDER)), ) window.SetBackgroundColour(WindowColor) self.control = window self.set_icon(view.icon) window.Bind(wx.EVT_CLOSE, self._on_close_page) window.Bind(wx.EVT_CHAR, self._on_key) # Create the 'context' copies we will need while editing: context = ui.context ui._context = context ui.context = self._copy_context(context) ui._revert = self._copy_context(context) # Create the actual trait sheet panel and imbed it in a scrollable # window (if requested): sw_sizer = wx.BoxSizer(wx.VERTICAL) if ui.scrollable: sizer = wx.BoxSizer(wx.VERTICAL) sw = TraitsUIScrolledPanel(window) trait_sheet = panel(ui, sw) sizer.Add(trait_sheet, 1, wx.EXPAND | wx.ALL, 4) tsdx, tsdy = trait_sheet.GetSize() tsdx += 8 tsdy += 8 sw.SetScrollRate(16, 16) max_dy = (2 * SystemMetrics().screen_height) // 3 sw.SetSizer(sizer) sw.SetSize( wx.Size( tsdx + ((tsdy > max_dy) * scrollbar_dx), min(tsdy, max_dy) ) ) else: sw = panel(ui, window) sw_sizer.Add(sw, 1, wx.EXPAND) buttons = [self.coerce_button(button) for button in view.buttons] nbuttons = len(buttons) if (nbuttons != 1) or (not self.is_button(buttons[0], "")): # Create the necessary special function buttons: sw_sizer.Add(wx.StaticLine(window, -1), 0, wx.EXPAND) b_sizer = wx.BoxSizer(wx.HORIZONTAL) if nbuttons == 0: if view.apply: self.check_button(buttons, ApplyButton) if view.revert: self.check_button(buttons, RevertButton) if view.ok: self.check_button(buttons, OKButton) if view.cancel: self.check_button(buttons, CancelButton) if view.help: self.check_button(buttons, HelpButton) for raw_button, button in zip(view.buttons, buttons): default = raw_button == view.default_button if self.is_button(button, "Apply"): self.apply = self.add_button( button, b_sizer, self._on_apply, apply, default=default ) ui.observe(self._on_applyable, "modified", dispatch="ui") elif self.is_button(button, "Revert"): self.revert = self.add_button( button, b_sizer, self._on_revert, revert, default=default, ) elif self.is_button(button, "OK"): self.ok = self.add_button( button, b_sizer, self._on_ok, default=default ) ui.observe(self._on_error, "errors", dispatch="ui") elif self.is_button(button, "Cancel"): self.add_button( button, b_sizer, self._on_cancel, default=default ) elif self.is_button(button, "Help"): self.add_button( button, b_sizer, self._on_help, default=default ) elif not self.is_button(button, ""): self.add_button(button, b_sizer, default=default) sw_sizer.Add(b_sizer, 0, wx.ALIGN_RIGHT | wx.ALL, 5) # Add the menu bar, tool bar and status bar (if any): self.add_menubar() self.add_toolbar() self.add_statusbar() # Lay all of the dialog contents out: window.SetSizerAndFit(sw_sizer) def close(self, rc=wx.ID_OK): """Closes the dialog window.""" ui = self.ui ui.result = rc == wx.ID_OK save_window(ui) if self.is_modal: self.control.EndModal(rc) self.control.Unbind(wx.EVT_CLOSE) self.control.Unbind(wx.EVT_CHAR) ui.finish() self.ui = self.apply = self.revert = self.help = self.control = None def _copy_context(self, context): """Creates a copy of a *context* dictionary.""" result = {} for name, value in context.items(): if value is not None: result[name] = value.clone_traits() else: result[name] = None return result def _apply_context(self, from_context, to_context): """Applies the traits in the *from_context* to the *to_context*.""" for name, value in from_context.items(): if value is not None: to_context[name].copy_traits(value) else: to_context[name] = None if to_context is self.ui._context: on_apply = self.ui.view.on_apply if on_apply is not None: on_apply() def _on_close_page(self, event): """Handles the user clicking the window/dialog "close" button/icon.""" if self.ui.view.close_result: self._on_ok(event) else: self._on_cancel(event) def _on_ok(self, event=None): """Closes the window and saves changes (if allowed by the handler).""" if self.ui.handler.close(self.ui.info, True): self._apply_context(self.ui.context, self.ui._context) self.close(wx.ID_OK) def _on_cancel(self, event=None): """Closes the window and discards changes (if allowed by the handler).""" if self.ui.handler.close(self.ui.info, False): self._apply_context(self.ui._revert, self.ui._context) self.close(wx.ID_CANCEL) def _on_key(self, event): """Handles the user pressing the Escape key.""" if event.GetKeyCode() == 0x1B: self._on_close_page(event) def _on_apply(self, event): """Handles a request to apply changes.""" ui = self.ui self._apply_context(ui.context, ui._context) if hasattr(self, "revert"): self.revert.Enable(True) ui.handler.apply(ui.info) ui.modified = False def _on_revert(self, event): """Handles a request to revert changes.""" ui = self.ui self._apply_context(ui._revert, ui.context) self._apply_context(ui._revert, ui._context) self.revert.Enable(False) ui.handler.revert(ui.info) ui.modified = False def _on_applyable(self, event): """Handles a change to the "modified" state of the user interface .""" state = event.new self.apply.Enable(state) def _on_error(self, event): """Handles editing errors.""" errors = event.new self.ok.Enable(errors == 0) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/wx/ui_panel.py0000644000175100001730000012236600000000000020647 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Creates a panel-based wxPython user interface for a specified UI object. """ import wx import wx.html as wh import re from html import escape from traits.api import Instance, Undefined from traitsui.api import Group from traitsui.undo import UndoHistory from traitsui.dockable_view_element import DockableViewElement from traitsui.help_template import help_template from traitsui.menu import UndoButton, RevertButton, HelpButton from pyface.api import SystemMetrics from pyface.dock.api import ( DockWindow, DockSizer, DockSection, DockRegion, DockControl, ) from pyface.sizers.flow import FlowSizer from .helper import ( position_window, TraitsUIPanel, TraitsUIScrolledPanel, GroupEditor, ) from .constants import WindowColor from .ui_base import BaseDialog # Pattern of all digits all_digits = re.compile(r"\d+") # Global font used for emphasis emphasis_font = None # Global color used for emphasis emphasis_color = wx.Colour(0, 0, 127) def ui_panel(ui, parent): """Creates a panel-based wxPython user interface for a specified UI object.""" ui_panel_for(ui, parent, True) def ui_subpanel(ui, parent): """Creates a subpanel-based wxPython user interface for a specified UI object. A subpanel does not allow control buttons (other than those specified in the UI object). """ ui_panel_for(ui, parent, False) def ui_panel_for(ui, parent, buttons): """Creates a panel-based wxPython user interface for a specified UI object.""" # Disable screen updates on the parent control while we build the view: parent.Freeze() # Build the view: ui.control = control = Panel(ui, parent, buttons).control # Allow screen updates to occur again: parent.Thaw() control._parent = parent control._object = ui.context.get("object") control._ui = ui try: ui.prepare_ui() except: control.Destroy() ui.control = None ui.result = False raise ui.restore_prefs() ui.result = True class Panel(BaseDialog): """wxPython user interface panel for Traits-based user interfaces.""" def __init__(self, ui, parent, allow_buttons): """Initializes the object.""" self.ui = ui history = None view = ui.view title = view.title # Reset any existing history listeners: history = ui.history if history is not None: history.observe( self._on_undoable, "undoable", remove=True, dispatch="ui" ) history.observe( self._on_redoable, "redoable", remove=True, dispatch="ui" ) history.observe( self._on_revertable, "undoable", remove=True, dispatch="ui" ) # Determine if we need any buttons or an 'undo' history: buttons = [self.coerce_button(button) for button in view.buttons] nbuttons = len(buttons) if nbuttons == 0: if view.undo: self.check_button(buttons, UndoButton) if view.revert: self.check_button(buttons, RevertButton) if view.help: self.check_button(buttons, HelpButton) if allow_buttons and (history is None): for button in buttons: if self.is_button(button, "Undo") or self.is_button( button, "Revert" ): history = UndoHistory() break ui.history = history # Create a container panel to put everything in: cpanel = getattr(self, "control", None) if cpanel is not None: cpanel.SetSizer(None) cpanel.DestroyChildren() else: self.control = cpanel = TraitsUIPanel(parent, -1) # Create the actual trait sheet panel and embed it in a scrollable # window (if requested): sw_sizer = wx.BoxSizer(wx.VERTICAL) if ui.scrollable: sizer = wx.BoxSizer(wx.VERTICAL) sw = TraitsUIScrolledPanel(cpanel) sizer.Add(panel(ui, sw), 1, wx.EXPAND) sw.SetSizerAndFit(sizer) sw.SetScrollRate(16, 16) else: sw = panel(ui, cpanel) if (title != "") and ( not isinstance(getattr(parent, "owner", None), DockWindow) ): sw_sizer.Add( heading_text(cpanel, text=title).control, 0, wx.EXPAND ) self.add_toolbar(sw_sizer) sw_sizer.Add(sw, 1, wx.EXPAND) if allow_buttons and ( (nbuttons != 1) or (not self.is_button(buttons[0], "")) ): # Add the special function buttons: sw_sizer.Add(wx.StaticLine(cpanel, -1), 0, wx.EXPAND) b_sizer = wx.BoxSizer(wx.HORIZONTAL) for button in buttons: if self.is_button(button, "Undo"): self.undo = self.add_button( button, b_sizer, self._on_undo, False ) self.redo = self.add_button( button, b_sizer, self._on_redo, False, "Redo" ) history.observe( self._on_undoable, "undoable", dispatch="ui" ) history.observe( self._on_redoable, "redoable", dispatch="ui" ) elif self.is_button(button, "Revert"): self.revert = self.add_button( button, b_sizer, self._on_revert, False ) history.observe( self._on_revertable, "undoable", dispatch="ui" ) elif self.is_button(button, "Help"): self.add_button(button, b_sizer, self._on_help) elif not self.is_button(button, ""): self.add_button(button, b_sizer) sw_sizer.Add(b_sizer, 0, wx.ALIGN_RIGHT | wx.ALL, 5) cpanel.SetSizerAndFit(sw_sizer) def _on_undoable(self, event): """Handles a change to the "undoable" state of the undo history.""" state = event.new self.undo.Enable(state) def _on_redoable(self, event): """Handles a change to the "redoable" state of the undo history.""" state = event.new self.redo.Enable(state) def _on_revertable(self, event): """Handles a change to the "revert" state.""" state = event.new self.revert.Enable(state) def add_toolbar(self, sizer): """Adds an optional toolbar to the dialog.""" toolbar = self.ui.view.toolbar if toolbar is not None: self._last_group = self._last_parent = None sizer.Add( toolbar.create_tool_bar(self.control, self), 0, wx.EXPAND ) self._last_group = self._last_parent = None # ------------------------------------------------------------------------- # Creates a panel-based wxPython user interface for a specified UI object: # # Note: This version does not modify the UI object passed to it. # ------------------------------------------------------------------------- def panel(ui, parent): """Creates a panel-based wxPython user interface for a specified UI object. This function does not modify the UI object passed to it """ # Bind the context values to the 'info' object: ui.info.bind_context() # Get the content that will be displayed in the user interface: content = ui._groups # If there is 0 or 1 Groups in the content, create a single panel for it: if len(content) <= 1: panel = TraitsUIPanel(parent, -1) if len(content) == 1: # Fill the panel with the Group's content: sg_sizer, resizable, contents = fill_panel_for_group( panel, content[0], ui ) sizer = panel.GetSizer() if sizer is not sg_sizer: sizer.Add(sg_sizer, 1, wx.EXPAND) # Make sure the panel and its contents have been laid out properly: sizer.Fit(panel) # Return the panel that was created: return panel # Create a notebook which will contain a page for each group in the # content: nb = create_notebook_for_items(content, ui, parent, None) nb.ui = ui # Notice when the notebook page changes (to display correct help) ###parent.Bind(wx.EVT_NOTEBOOK_PAGE_CHANGED, _page_changed, id=nb.GetId()) # Return the notebook as the result: return nb # ------------------------------------------------------------------------- # Creates a notebook and adds a list of groups or items to it as separate # pages: # ------------------------------------------------------------------------- def create_notebook_for_items( content, ui, parent, group, item_handler=None, is_dock_window=False ): """Creates a notebook and adds a list of groups or items to it as separate pages. """ if is_dock_window: nb = parent else: dw = DockWindow( parent, handler=ui.handler, handler_args=(ui.info,), id=ui.id ) if group is not None: dw.theme = group.dock_theme nb = dw.control pages = [] count = 0 # Create a notebook page for each group or item in the content: active = 0 for index, item in enumerate(content): if isinstance(item, Group): # Create the group as a nested DockWindow item: if item.selected: active = index sg_sizer, resizable, contents = fill_panel_for_group( nb, item, ui, suppress_label=True, is_dock_window=True ) # If the result is a region (i.e. notebook) with only one page, # collapse it down into just the contents of the region: if isinstance(contents, DockRegion) and ( len(contents.contents) == 1 ): contents = contents.contents[0] # Add the content to the notebook as a new page: pages.append(contents) else: # Create the new page as a simple DockControl containing the # specified set of controls: page_name = item.get_label(ui) count += 1 if page_name == "": page_name = "Page %d" % count sizer = wx.BoxSizer(wx.VERTICAL) panel = TraitsUIPanel(nb, -1) panel.SetSizer(sizer) pages.append( DockControl( name=page_name, image=item.image, id=item.get_id(), style=item.dock, dockable=DockableViewElement(ui=ui, element=item), export=item.export, control=panel, ) ) item_handler(item, panel, sizer) panel.GetSizer().Fit(panel) region = DockRegion(contents=pages, active=active) # If the caller is a DockWindow, return the region as the result: if is_dock_window: return region nb.SetSizer(DockSizer(contents=DockSection(contents=[region]))) # Return the notebook as the result: return nb def _page_changed(event): nb = event.GetEventObject() nb.ui._active_group = event.GetSelection() def show_help(ui, button): """Displays a help window for the specified UI's active Group.""" group = ui._groups[ui._active_group] template = help_template() if group.help != "": header = template.group_help % escape(group.help) else: header = template.no_group_help fields = [] for item in group.get_content(False): if not item.is_spacer(): fields.append( template.item_help % (escape(item.get_label(ui)), escape(item.get_help(ui))) ) html_content = template.group_html % (header, "\n".join(fields)) HTMLHelpWindow(button, html_content, 0.25, 0.33) def show_help_popup(event): """Displays a pop-up help window for a single trait.""" control = event.GetEventObject() template = help_template() # Note: The following check is necessary because under Linux, we get back # a control which does not have the 'help' trait defined (it is the parent # of the object with the 'help' trait): help = getattr(control, "help", None) if help is not None: html_content = template.item_html % (control.GetLabel(), help) HTMLHelpWindow(control, html_content, 0.25, 0.13) def fill_panel_for_group( panel, group, ui, suppress_label=False, is_dock_window=False, create_panel=False, ): """Builds the user interface for a specified Group within a specified Panel. """ fp = FillPanel( panel, group, ui, suppress_label, is_dock_window, create_panel ) return (fp.control or fp.sizer, fp.resizable, fp.dock_contents) class FillPanel(object): """A subpanel for a single group of items.""" def __init__( self, panel, group, ui, suppress_label, is_dock_window, create_panel ): """Initializes the object.""" # Get the contents of the group: content = group.get_content() # Create a group editor object if one is needed: self.control = self.sizer = editor = None self.ui = ui self.group = group self.is_horizontal = group.orientation == "horizontal" layout = group.layout is_scrolled_panel = group.scrollable is_splitter = layout == "split" is_tabbed = layout == "tabbed" id = group.id # Assume our contents are not resizable: self.resizable = False if is_dock_window and (is_splitter or is_tabbed): if is_splitter: self.dock_contents = self.add_dock_window_splitter_items( panel, content, group ) else: self.resizable = group.springy self.dock_contents = create_notebook_for_items( content, ui, panel, group, self.add_notebook_item, True ) return if ( is_dock_window or create_panel or is_scrolled_panel or (id != "") or (group.visible_when != "") or (group.enabled_when != "") ): if is_scrolled_panel: new_panel = TraitsUIScrolledPanel(panel) new_panel.SetMinSize(panel.GetMinSize()) self.resizable = True else: new_panel = TraitsUIPanel(panel, -1) sizer = panel.GetSizer() if sizer is None: sizer = wx.BoxSizer(wx.VERTICAL) panel.SetSizer(sizer) self.control = panel = new_panel if is_splitter or is_tabbed: editor = DockWindowGroupEditor(control=panel, ui=ui) else: editor = GroupEditor(control=panel) if id != "": ui.info.bind(group.id, editor) if group.visible_when != "": ui.add_visible(group.visible_when, editor) if group.enabled_when != "": ui.add_enabled(group.enabled_when, editor) self.panel = panel self.dock_contents = None # Determine the horizontal/vertical orientation of the group: if self.is_horizontal: orientation = wx.HORIZONTAL else: orientation = wx.VERTICAL # Set up a group with or without a border around its contents: label = "" if not suppress_label: label = group.label if group.show_border: box = wx.StaticBox(panel, -1, label) self._set_owner(box, group) self.sizer = wx.StaticBoxSizer(box, orientation) else: if layout == "flow": self.sizer = FlowSizer(orientation) else: self.sizer = wx.BoxSizer(orientation) if label != "": self.sizer.Add( heading_text(panel, text=label).control, 0, wx.EXPAND | wx.LEFT | wx.TOP | wx.RIGHT, 4, ) # If no sizer has been specified for the panel yet, make the new sizer # the layout sizer for the panel: if panel.GetSizer() is None: panel.SetSizer(self.sizer) # Set up scrolling now that the sizer has been set: if is_scrolled_panel: if self.is_horizontal: panel.SetupScrolling(scroll_y=False) else: panel.SetupScrolling(scroll_x=False) if is_splitter: dw = DockWindow( panel, handler=ui.handler, handler_args=(ui.info,), id=ui.id, theme=group.dock_theme, ).control if editor is not None: editor.dock_window = dw dw.SetSizer( DockSizer( contents=self.add_dock_window_splitter_items( dw, content, group ) ) ) self.sizer.Add(dw, 1, wx.EXPAND) elif len(content) > 0: if is_tabbed: self.resizable = group.springy dw = create_notebook_for_items( content, ui, panel, group, self.add_notebook_item ) if editor is not None: editor.dock_window = dw self.sizer.Add(dw, self.resizable, wx.EXPAND) # Check if content is all Group objects: elif layout == "fold": self.resizable = True self.sizer.Add( self.create_fold_for_items(panel, content), 1, wx.EXPAND ) elif isinstance(content[0], Group): # If so, add them to the panel and exit: self.add_groups(content, panel) else: self.add_items(content, panel, self.sizer) # If the caller is a DockWindow, we need to define the content we are # adding to it: if is_dock_window: self.dock_contents = DockRegion( contents=[ DockControl( name=group.get_label(self.ui), image=group.image, id=group.get_id(), style=group.dock, dockable=DockableViewElement(ui=ui, element=group), export=group.export, control=panel, ) ] ) def add_dock_window_splitter_items(self, window, content, group): """Adds a set of groups or items separated by splitter bars to a DockWindow. """ contents = [ self.add_dock_window_splitter_item(window, item, group) for item in content ] # Create a splitter group to hold the contents: result = DockSection(contents=contents, is_row=self.is_horizontal) # If nothing is resizable, then mark each DockControl as such: if not self.resizable: for item in result.get_controls(): item.resizable = False # Return the DockSection we created: return result def add_dock_window_splitter_item(self, window, item, group): """Adds a single group or item to a DockWindow.""" if isinstance(item, Group): sizer, resizable, contents = fill_panel_for_group( window, item, self.ui, suppress_label=True, is_dock_window=True ) self.resizable |= resizable return contents orientation = wx.VERTICAL if self.is_horizontal: orientation = wx.HORIZONTAL sizer = wx.BoxSizer(orientation) panel = TraitsUIPanel(window, -1) panel.SetSizer(sizer) self.add_items([item], panel, sizer) return DockRegion( contents=[ DockControl( name=item.get_label(self.ui), image=item.image, id=item.get_id(), style=item.dock, dockable=DockableViewElement(ui=self.ui, element=item), export=item.export, control=panel, ) ] ) def create_fold_for_items(self, window, content): """Adds a set of groups or items as vertical notebook pages to a vertical notebook. """ raise NotImplementedError("VFold is not implemented for Wx backend") def create_fold_for_item(self, notebook, item): """Adds a single group or item to a vertical notebook.""" # Create a new notebook page: page = notebook.create_page() # Create the page contents: if isinstance(item, Group): panel, resizable, contents = fill_panel_for_group( page.parent, item, self.ui, suppress_label=True, create_panel=True, ) else: panel = TraitsUIPanel(page.parent, -1) sizer = wx.BoxSizer(wx.VERTICAL) panel.SetSizer(sizer) self.add_items([item], panel, sizer) # Set the page name and control: page.name = item.get_label(self.ui) page.control = panel # Return the new notebook page: return page def add_notebook_item(self, item, parent, sizer): """Adds a single Item to a notebook.""" self.add_items([item], parent, sizer) def add_groups(self, content, panel): """Adds a list of Group objects to the panel.""" sizer = self.sizer # Process each group: for subgroup in content: # Add the sub-group to the panel: sg_sizer, sg_resizable, contents = fill_panel_for_group( panel, subgroup, self.ui ) # If the sub-group is resizable: if sg_resizable: # Then so are we: self.resizable = True # Add the sub-group so that it can be resized by the layout: sizer.Add(sg_sizer, 1, wx.EXPAND | wx.ALL, 2) else: style = wx.EXPAND | wx.ALL growable = 0 if self.is_horizontal: if subgroup.springy: growable = 1 sizer.Add(sg_sizer, growable, style, 2) def _label_when(self): """Set the visible and enabled states of all labels as controlled by a 'visible_when' or 'enabled_when' expression. """ self._evaluate_label_condition(self._label_enabled_whens, "enabled") self._evaluate_label_condition(self._label_visible_whens, "visible") def _evaluate_label_condition(self, conditions, kind): """Evaluates a list of (eval, widget) pairs and calls the appropriate method on the label widget to toggle whether it is visible/enabled as needed. """ context = self.ui._get_context(self.ui.context) method_dict = {"visible": "Show", "enabled": "Enable"} for when, label in conditions: method_to_call = getattr(label, method_dict[kind]) try: cond_value = eval(when, globals(), context) method_to_call(bool(cond_value)) except Exception: # catch errors in the validate_when expression from traitsui.api import raise_to_debug raise_to_debug() def add_items(self, content, panel, sizer): """Adds a list of Item objects to the panel.""" # Get local references to various objects we need: ui = self.ui info = ui.info handler = ui.handler group = self.group show_left = group.show_left padding = group.padding col = -1 col_incr = 1 self.label_flags = 0 self._label_enabled_whens = [] self._label_visible_whens = [] show_labels = False for item in content: show_labels |= item.show_label if (not self.is_horizontal) and (show_labels or (group.columns > 1)): # For a vertical list of Items with labels or multiple columns, use # a 'FlexGridSizer': self.label_pad = 0 cols = group.columns if show_labels: cols *= 2 col_incr = 2 flags = wx.TOP | wx.BOTTOM border_size = 1 item_sizer = wx.FlexGridSizer(0, cols, 0, 5) if show_left: self.label_flags = wx.ALIGN_RIGHT if show_labels: for i in range(1, cols, 2): item_sizer.AddGrowableCol(i) else: # Otherwise, the current sizer will work as is: self.label_pad = 4 cols = 1 flags = wx.ALL border_size = 1 item_sizer = sizer # Process each Item in the list: for item in content: # Get the name in order to determine its type: name = item.name # Check if is a label: if name == "": label = item.label if label != "": # Update the column counter: col += col_incr # If we are building a multi-column layout with labels, # just add space in the next column: if (cols > 1) and show_labels: item_sizer.Add((1, 1)) if item.style == "simple": # Add a simple text label: label = wx.StaticText( panel, -1, label, style=wx.ALIGN_LEFT ) item_sizer.Add(label, 0, wx.EXPAND) else: # Add the label to the sizer: label = heading_text(panel, text=label).control item_sizer.Add( label, 0, wx.TOP | wx.BOTTOM | wx.EXPAND, 3 ) if item.emphasized: self._add_emphasis(label) if item.visible_when: self._label_visible_whens.append( (item.visible_when, label) ) if item.enabled_when: self._label_enabled_whens.append( (item.enabled_when, label) ) # Continue on to the next Item in the list: continue # Update the column counter: col += col_incr # Check if it is a separator: if name == "_": for i in range(cols): if self.is_horizontal: # Add a vertical separator: line = wx.StaticLine(panel, -1, style=wx.LI_VERTICAL) item_sizer.Add( line, 0, wx.LEFT | wx.RIGHT | wx.EXPAND, 2 ) else: # Add a horizontal separator: line = wx.StaticLine(panel, -1, style=wx.LI_HORIZONTAL) item_sizer.Add( line, 0, wx.TOP | wx.BOTTOM | wx.EXPAND, 2 ) self._set_owner(line, item) # Continue on to the next Item in the list: continue # Convert a blank to a 5 pixel spacer: if name == " ": name = "5" # Check if it is a spacer: if all_digits.match(name): # If so, add the appropriate amount of space to the sizer: n = int(name) if self.is_horizontal: item_sizer.Add((n, 1)) else: spacer = (1, n) item_sizer.Add(spacer) if show_labels: item_sizer.Add(spacer) # Continue on to the next Item in the list: continue # Otherwise, it must be a trait Item: object = eval(item.object_, globals(), ui.context) trait = object.base_trait(name) desc = trait.tooltip if desc is None: desc = "Specifies " + trait.desc if trait.desc else "" label = None # If we are displaying labels on the left, add the label to the # user interface: if show_left: if item.show_label: label = self.create_label( item, ui, desc, panel, item_sizer, border=group.show_border, ) elif (cols > 1) and show_labels: label = self.dummy_label(panel, item_sizer) # Get the editor factory associated with the Item: editor_factory = item.editor if editor_factory is None: editor_factory = trait.get_editor() # If still no editor factory found, use a default text editor: if editor_factory is None: from .text_editor import ToolkitEditorFactory editor_factory = ToolkitEditorFactory() # If the item has formatting traits set them in the editor # factory: 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 the item has an invalid state extended trait name, set it # in the editor factory: if item.invalid != "": editor_factory.invalid = item.invalid # Set up the background image (if used): item_panel = panel # Create the requested type of editor from the editor factory: factory_method = getattr(editor_factory, item.style + "_editor") editor = factory_method( ui, object, name, item.tooltip, item_panel ).trait_set(item=item, object_name=item.object) # Tell editor to actually build the editing widget: editor.prepare(item_panel) # Set the initial 'enabled' state of the editor from the factory: editor.enabled = editor_factory.enabled # Add emphasis to the editor control if requested: if item.emphasized: self._add_emphasis(editor.control) # Give the editor focus if it requested it: if item.has_focus: editor.control.SetFocus() # Adjust the maximum border size based on the editor's settings: border_size = min(border_size, editor.border_size) # Set up the reference to the correct 'control' to use in the # following section, depending upon whether we have wrapped an # ImagePanel around the editor control or not: control = editor.control width, height = control.GetSize() # Set the correct size on the control, as specified by the user: scrollable = editor.scrollable item_width = item.width item_height = item.height growable = 0 if (item_width != -1.0) or (item_height != -1.0): if (0.0 < item_width <= 1.0) and self.is_horizontal: growable = int(1000.0 * item_width) item_width = -1 item_width = int(item_width) if item_width < -1: item_width = -item_width elif item_width != -1: item_width = max(item_width, width) if (0.0 < item_height <= 1.0) and (not self.is_horizontal): growable = int(1000.0 * item_height) item_height = -1 item_height = int(item_height) if item_height < -1: item_height = -item_height elif item_height != -1: item_height = max(item_height, height) control.SetMinSize(wx.Size(item_width, item_height)) # Bind the item to the control and all of its children: self._set_owner(control, item) # Bind the editor into the UIInfo object name space so it can be # referred to by a Handler while the user interface is active: id = item.id or name info.bind(id, editor, item.id) # Also, add the editors to the list of editors used to construct # the user interface: ui._editors.append(editor) # If the handler wants to be notified when the editor is created, # add it to the list of methods to be called when the UI is # complete: defined = getattr(handler, id + "_defined", None) if defined is not None: ui.add_defined(defined) # If the editor is conditionally visible, add the visibility # 'expression' and the editor to the UI object's list of monitored # objects: if item.visible_when != "": ui.add_visible(item.visible_when, editor) # If the editor is conditionally enabled, add the enabling # 'expression' and the editor to the UI object's list of monitored # objects: if item.enabled_when != "": ui.add_enabled(item.enabled_when, editor) # Add the created editor control to the sizer with the appropriate # layout flags and values: ui._scrollable |= scrollable item_resizable = (item.resizable is True) or ( (item.resizable is Undefined) and scrollable ) if item_resizable: growable = growable or 500 self.resizable = True elif item.springy: growable = growable or 500 # The following is a hack to allow 'readonly' text fields to # work correctly (wx has a bug that setting wx.EXPAND on a # StaticText control seems to cause the text to be aligned higher # than it would be otherwise, causing it to misalign with its # label). layout_style = editor.layout_style if not show_labels: layout_style |= wx.EXPAND item_sizer.Add( control, growable, flags | layout_style, max(0, border_size + padding + item.padding), ) # If we are displaying labels on the right, add the label to the # user interface: if not show_left: if item.show_label: label = self.create_label( item, ui, desc, panel, item_sizer, "", wx.RIGHT ) elif (cols > 1) and show_labels: label = self.dummy_label(panel, item_sizer) # If the Item is resizable, and we are using a multi-column grid: if item_resizable and (cols > 1): # Mark the entire row as growable: item_sizer.AddGrowableRow(col // cols) # Save the reference to the label control (if any) in the editor: editor.label_control = label if ( len(self._label_enabled_whens) + len(self._label_visible_whens) ) > 0: for object in self.ui.context.values(): object.on_trait_change( lambda: self._label_when(), dispatch="ui" ) # If we created a grid sizer, add it to the original sizer: if item_sizer is not sizer: growable = 0 if self.resizable: growable = 1 sizer.Add(item_sizer, growable, wx.EXPAND | wx.ALL, 2) def create_label( self, item, ui, desc, parent, sizer, suffix=":", pad_side=wx.LEFT, border=False, ): """Creates an item label.""" label = item.get_label(ui) if (label == "") or (label[-1:] in "?=:;,.<>/\\\"'-+#|"): suffix = "" control = wx.StaticText( parent, -1, label + suffix, style=wx.ALIGN_LEFT | wx.SIMPLE_BORDER if border else wx.NO_BORDER, ) self._set_owner(control, item) if item.emphasized: self._add_emphasis(control) # XXX: Turning off help popups for now # control.Bind(wx.EVT_LEFT_UP, show_help_popup) control.help = item.get_help(ui) control.SetToolTip(wx.ToolTip(item.get_help(ui))) sizer.Add( control, 0, self.label_flags | wx.ALIGN_TOP | pad_side, self.label_pad, ) if desc != "": control.SetToolTip(desc) return control def dummy_label(self, parent, sizer): """Creates an item label.""" control = wx.StaticText(parent, -1, "", style=wx.ALIGN_RIGHT) sizer.Add(control, 0) return control def _add_emphasis(self, control): """Adds emphasis to a specified control's font.""" global emphasis_font control.SetForegroundColour(emphasis_color) if emphasis_font is None: font = control.GetFont() emphasis_font = wx.Font( font.GetPointSize() + 1, font.GetFamily(), font.GetStyle(), wx.BOLD, ) control.SetFont(emphasis_font) def _set_owner(self, control, owner): control._owner = owner for child in control.GetChildren(): self._set_owner(child, owner) class DockWindowGroupEditor(GroupEditor): """Editor for a group which displays a DockWindow.""" # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- #: DockWindow for the group dock_window = Instance(wx.Window) # -- UI preference save/restore interface --------------------------------- def restore_prefs(self, prefs): """Restores any saved user preference information associated with the editor. """ if isinstance(prefs, dict): structure = prefs.get("structure") else: structure = prefs self.dock_window.GetSizer().SetStructure(self.dock_window, structure) self.dock_window.Layout() def save_prefs(self): """Returns any user preference information associated with the editor.""" return {"structure": self.dock_window.GetSizer().GetStructure()} # -- End UI preference save/restore interface ----------------------------- class HTMLHelpWindow(wx.Frame): """Window for displaying Traits-based help text with HTML formatting.""" def __init__(self, parent, html_content, scale_dx, scale_dy): """Initializes the object.""" wx.Frame.__init__(self, parent, -1, "Help", style=wx.SIMPLE_BORDER) self.SetBackgroundColour(WindowColor) # Wrap the dialog around the image button panel: sizer = wx.BoxSizer(wx.VERTICAL) html_control = wh.HtmlWindow(self) html_control.SetBorders(2) html_control.SetPage(html_content) sizer.Add(html_control, 1, wx.EXPAND) sizer.Add(wx.StaticLine(self, -1), 0, wx.EXPAND) b_sizer = wx.BoxSizer(wx.HORIZONTAL) button = wx.Button(self, -1, "OK") self.Bind(wx.EVT_BUTTON, self._on_ok, id=button.GetId()) b_sizer.Add(button, 0) sizer.Add(b_sizer, 0, wx.ALIGN_RIGHT | wx.ALL, 5) self.SetSizer(sizer) self.SetSize( wx.Size( int(scale_dx * SystemMetrics().screen_width), int(scale_dy * SystemMetrics().screen_height), ) ) # Position and show the dialog: position_window(self, parent=parent) self.Show() def _on_ok(self, event): """Handles the window being closed.""" self.Unbind(wx.EVT_BUTTON) self.Destroy() # ------------------------------------------------------------------------- # Creates a Pyface HeadingText control: # ------------------------------------------------------------------------- HeadingText = None def heading_text(*args, create=False, **kw): """Create a Pyface HeadingText control.""" global HeadingText if HeadingText is None: from pyface.api import HeadingText widget = HeadingText(*args, create=create, **kw) widget.create() return widget ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/wx/ui_window.py0000644000175100001730000000553000000000000021050 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ A base class for creating custom Traits UI windows. """ import wx from traits.api import HasPrivateTraits, Instance, Property from .helper import init_wx_handlers, BufferDC # ------------------------------------------------------------------------- # 'UIWindow' class: # ------------------------------------------------------------------------- class UIWindow(HasPrivateTraits): """A base class for creating custom Traits UI windows.""" #: The wx.Window associated with this custom window: control = Instance(wx.Window) #: The initial size of the window: size = Instance(wx.Size, (-1, -1)) #: The current width of the window: width = Property() #: The current height of the window: height = Property() # -- Public Methods ------------------------------------------------------- def __init__(self, parent, **traits): """Creates and initializes the window.""" super().__init__(**traits) self.control = wx.Window( parent, -1, size=self.size, style=wx.FULL_REPAINT_ON_RESIZE ) init_wx_handlers(self.control, self) def refresh(self, x=None, y=None, dx=None, dy=None): """Refreshes the contents of the window.""" if self.control is not None: if x is None: self.control.Refresh() else: self.control.Refresh(x, y, dx, dy) def capture(self): """Capture the mouse.""" self.control.CaptureMouse() def release(self): """Release the mouse.""" self.control.ReleaseMouse() # -- wxPython Event Handlers ---------------------------------------------- def _erase_background(self, event): """Never, ever, do anything in this handler.""" pass def _paint(self, event): """Paints the contents of the window.""" dc = BufferDC(self.control) self._paint_dc(dc) dc.copy() def _paint_dc(self, dc): """This method should be overridden by sub-classes to do the actual window painting. """ pass # -- Property Implementations --------------------------------------------- def _get_width(self): return self.control.GetClientSize()[0] def _set_width(self, width): self.control.SetSize(width, self.height) def _get_height(self): return self.control.GetClientSize()[1] def _set_height(self, height): self.control.SetSize(self.width, height) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/wx/ui_wizard.py0000644000175100001730000001703700000000000021046 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Creates a wizard-based wxPython user interface for a specified UI object. A wizard is a dialog box that displays a series of pages, which the user can navigate with forward and back buttons. """ import wx import wx.adv as wz from traits.api import Str, Union from .constants import DefaultTitle from .helper import restore_window, save_window, GroupEditor from .ui_panel import fill_panel_for_group # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- # Trait that allows only None or a string value none_str_trait = Union(None, Str, default_value="") def ui_wizard(ui, parent): """Creates a wizard-based wxPython user interface for a specified UI object. """ # Create the copy of the 'context' we will need while editing: ui._context = context = ui.context new_context = { name: None if value is None else value.clone_traits() for name, value in context.items() } ui.context = new_context # Now bind the context values to the 'info' object: ui.info.bind_context() # Create the wxPython wizard window: title = ui.view.title if title == "": title = DefaultTitle ui.control = wizard = wz.Wizard(parent, -1, title) # Create all of the wizard pages: pages = [] editor_pages = [] info = ui.info shadow_group = ui.view.content.get_shadow(ui) min_dx = min_dy = 0 # Create a dictionary mapping each contained group in shadow_group to # its id and enabled_when fields. group_fields_mapping = {} for group in shadow_group.get_content(): # FIXME: When the group's id or enabled_when or visible_when is # set, the "fill_panel_for_group" will create a new Panel to hold the # contents of the group (instead of adding them to the page itself). # This leads to an incorrect sizing of the panel(not sure why # actually): example would be 'test_ui2.py' in # Traits/integrationtests/ui. In addition, # it leads to incorrect bindings (of the id) on the UIInfo object: # the id is bound to the GroupEditor created in "fill_panel.." # instead of the PageGroupEditor created here. # A simple solution is to clear out these fields before calling # "fill_panel_for_group", and then reset these traits. group_fields_mapping[group] = (group.id, group.enabled_when) (group.id, group.enabled_when) = ("", "") page = UIWizardPage(wizard, editor_pages) pages.append(page) fill_panel_for_group(page, group, ui) # Size the page correctly, then calculate cumulative minimum size: sizer = page.GetSizer() sizer.Fit(page) size = sizer.CalcMin() min_dx = max(min_dx, size.GetWidth()) min_dy = max(min_dy, size.GetHeight()) # If necessary, create a PageGroupEditor and attach it to the right # places: (group.id, group.enabled_when) = group_fields_mapping[group] if group.id or group.enabled_when: page.editor = editor = PageGroupEditor(control=page) if group.id: page.id = group.id editor_pages.append(page) info.bind(page.id, editor) if group.enabled_when: ui.add_enabled(group.enabled_when, editor) # Size the wizard correctly: wizard.SetPageSize(wx.Size(min_dx, min_dy)) # Set up the wizard 'page changing' event handler: wizard.Bind(wz.EVT_WIZARD_PAGE_CHANGING, page_changing) # Size the wizard and the individual pages appropriately: prev_page = pages[0] wizard.FitToPage(prev_page) # Link the pages together: for page in pages[1:]: page.SetPrev(prev_page) prev_page.SetNext(page) prev_page = page # Finalize the display of the wizard: try: ui.prepare_ui() except: ui.control.Destroy() ui.control.ui = None ui.control = None ui.result = False raise # Position the wizard on the display: ui.handler.position(ui.info) # Restore the user_preference items for the user interface: restore_window(ui) # Run the wizard: if wizard.RunWizard(pages[0]): # If successful, apply the modified context to the original context: original = ui._context for name, value in ui.context.items(): if value is not None: original[name].copy_traits(value) else: original[name] = None ui.result = True else: ui.result = False # Clean up loose ends, like restoring the original context: wizard.Unbind(wz.EVT_WIZARD_PAGE_CHANGING, handler=page_changing) save_window(ui) ui.finish() ui.context = ui._context ui._context = {} def page_changing(event): """Handles the user attempting to change the current wizard page.""" # Get the page the user is trying to go to: page = event.GetPage() if event.GetDirection(): new_page = page.GetNext() else: new_page = page.GetPrev() # If the page has a disabled PageGroupEditor object, veto the page change: if ( (new_page is not None) and (new_page.editor is not None) and (not new_page.editor.enabled) ): event.Veto() # If their is a message associated with the editor, display it: msg = new_page.editor.msg if msg != "": wx.MessageBox(msg) class UIWizardPage(wz.WizardPage): """A page within a wizard interface.""" def __init__(self, wizard, pages): super().__init__(wizard) self.next = self.previous = self.editor = None self.pages = pages def SetNext(self, page): """Sets the next page after this one.""" self.next = page def SetPrev(self, page): """Sets the previous page to this one.""" self.previous = page def GetNext(self): """Returns the next page after this one.""" editor = self.editor if (editor is not None) and (editor.next != ""): next_ = editor.next if next_ is None: return None for page in self.pages: if page.id == next_: return page return self.next def GetPrev(self): """Returns the previous page to this one.""" editor = self.editor if (editor is not None) and (editor.previous != ""): previous = editor.previous if previous is None: return None for page in self.pages: if page.id == previous: return page return self.previous class PageGroupEditor(GroupEditor): """Editor for a group, which displays a page.""" # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- #: ID of next page to display next = none_str_trait #: ID of previous page to display previous = none_str_trait #: Message to display if user can't link to page msg = Str() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/wx/value_editor.py0000644000175100001730000000171400000000000021526 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Defines the tree-based Python value editor and the value editor factory, for the wxPython user interface toolkit. """ from traitsui.editors.value_editor import _ValueEditor from .editor import Editor class SimpleEditor(_ValueEditor, Editor): """Returns the editor to use for simple style views.""" #: Override the value of the readonly trait. readonly = False class ReadonlyEditor(_ValueEditor, Editor): """Returns the editor to use for readonly style views.""" #: Override the value of the readonly trait. readonly = True ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/wx/view_application.py0000644000175100001730000001073400000000000022403 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Creates a wxPython specific modal dialog user interface that runs as a complete application, using information from the specified UI object. """ # Standard library imports. import logging import os import sys # System library imports. import wx # ETS imports. from pyface.util.guisupport import ( is_event_loop_running_wx, start_event_loop_wx, ) logger = logging.getLogger(__name__) # File to redirect output to. If '', output goes to stdout. redirect_filename = "" KEEP_ALIVE_UIS = set() def on_ui_destroyed(object, name, old, destroyed): """Remove the UI object from KEEP_ALIVE_UIS.""" assert name == "destroyed" if destroyed: assert object in KEEP_ALIVE_UIS KEEP_ALIVE_UIS.remove(object) object.on_trait_change(on_ui_destroyed, "destroyed", remove=True) # ------------------------------------------------------------------------- # Creates a 'stand-alone' wx Application to display a specified traits UI View: # ------------------------------------------------------------------------- def view_application(context, view, kind, handler, id, scrollable, args): """Creates a stand-alone wx Application to display a specified traits UI View. 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 object 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. 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. """ if (kind == "panel") or ((kind is None) and (view.kind == "panel")): kind = "modal" app = wx.GetApp() if app is None or not is_event_loop_running_wx(app): return ViewApplication( context, view, kind, handler, id, scrollable, args ).ui.result ui = view.ui( context, kind=kind, handler=handler, id=id, scrollable=scrollable, args=args, ) # If the UI has not been closed yet, we need to keep a reference to # it until it does close. if not ui.destroyed: KEEP_ALIVE_UIS.add(ui) ui.on_trait_change(on_ui_destroyed, "destroyed") return ui.result # ------------------------------------------------------------------------- # 'ViewApplication' class: # ------------------------------------------------------------------------- class ViewApplication(wx.App): """Modal window that contains a stand-alone application.""" def __init__(self, context, view, kind, handler, id, scrollable, args): """Initializes the object.""" self.context = context self.view = view self.kind = kind self.handler = handler self.id = id self.scrollable = scrollable self.args = args if redirect_filename.strip() != "": super().__init__(1, redirect_filename) else: super().__init__(0) # Start the event loop in an IPython-conforming manner. try: start_event_loop_wx(self) except Exception: logger.exception("Event loop failed to close cleanly:") def OnInit(self): """Handles application initialization.""" self.ui = self.view.ui( self.context, kind=self.kind, handler=self.handler, id=self.id, scrollable=self.scrollable, args=self.args, ) return True ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1684768028.063807 traitsui-8.0.0/traitsui.egg-info/0000755000175100001730000000000000000000000017523 5ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768027.0 traitsui-8.0.0/traitsui.egg-info/PKG-INFO0000644000175100001730000001275100000000000020626 0ustar00runnerdocker00000000000000Metadata-Version: 2.1 Name: traitsui Version: 8.0.0 Summary: Traits-capable user interfaces Author-email: Enthought License: This software is OSI Certified Open Source Software. OSI Certified is a certification mark of the Open Source Initiative. Copyright (c) 2004-2023, Enthought, Inc. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Enthought, Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. Project-URL: source, https://github.com/enthought/traitsui Project-URL: docs, https://docs.enthought.com/traitsui Keywords: gui,traits,traitsui,pyqt,pyside,wxpython Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: BSD License Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python :: 3 Requires-Python: >=3.7 Description-Content-Type: text/x-rst Provides-Extra: docs Provides-Extra: editors Provides-Extra: examples Provides-Extra: pyqt5 Provides-Extra: pyqt6 Provides-Extra: pyside2 Provides-Extra: pyside6 Provides-Extra: test Provides-Extra: wx License-File: LICENSE.txt ============================================ TraitsUI: Traits-capable windowing framework ============================================ The TraitsUI project provides a toolkit-independent GUI abstraction layer, which is used to support the "visualization" features of the `Traits `__ package. You can write a model using the Traits API and specify a GUI using the TraitsUI API (views, items, editors, etc.), and let TraitsUI and your selected toolkit back-end (Qt or Wx) take care of the details of displaying them. Example ------- Given a Traits model like the following:: from traits.api import HasTraits, Str, Range, Enum class Person(HasTraits): name = Str('Jane Doe') age = Range(low=0) gender = Enum('female', 'male') person = Person(age=30) And using TraitsUI to specify and display a GUI view:: from traitsui.api import Item, RangeEditor, View person_view = View( Item('name'), Item('gender'), Item('age', editor=RangeEditor(mode='spinner', low=0, high=150)), buttons=['OK', 'Cancel'], resizable=True, ) person.configure_traits(view=person_view) It creates a GUI which looks like this: .. image:: https://raw.github.com/enthought/traitsui/main/README_example.png Important Links --------------- - Website and Documentation: ``__ * User Manual ``__ * Tutorial ``__ * API Documentation ``__ - Source code repository: ``__ * Issue tracker: ``__ - Download releases: ``__ - Mailing list: ``__ Installation ------------ If you want to run traitsui, you must also install: - Traits ``__ - Pyface ``__ You will also need one of the following backends: - wxPython - PySide2 - PyQt5 Backends have additional dependencies and there are optional dependencies on NumPy and Pandas for some editors. TraitsUI along with all dependencies can be installed in a straightforward way using the `Enthought Deployment Manager `__, ``pip`` or other package managers. .. end_of_long_description Running the Test Suite ---------------------- To run the test suite, you will need to install Git and `EDM `__ as well as have a Python environment which has install `Click `__ available. You can then follow the instructions in ``etstool.py``. In particular:: > python etstool.py test_all will run tests in all supported environments automatically. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768027.0 traitsui-8.0.0/traitsui.egg-info/SOURCES.txt0000644000175100001730000012505500000000000021417 0ustar00runnerdocker00000000000000CHANGES.txt LICENSE.txt MANIFEST.in README.rst README_example.png edm.yaml image_LICENSE.txt image_LICENSE_Eclipse.txt image_LICENSE_Nuvola.txt image_LICENSE_OOo.txt pyproject.toml docs/Makefile docs/README.rst docs/api.css docs/make.bat docs/traits_ui.ppt docs/traits_ui_slides.pdf docs/traitsuidocreadme.txt docs/releases/README.rst docs/source/changelog.rst docs/source/conf.py docs/source/e-logo-rev.png docs/source/index.rst docs/source/_static/default.css docs/source/_static/e-logo-rev.jpg docs/source/_static/et.png docs/source/_templates/layout.html docs/source/_templates/search.html docs/source/api/index.rst docs/source/demos/index.rst docs/source/traitsui_dev_guide/index.rst docs/source/traitsui_dev_guide/testing.rst docs/source/traitsui_user_manual/adapters.rst docs/source/traitsui_user_manual/advanced_view.rst docs/source/traitsui_user_manual/custom_view.rst docs/source/traitsui_user_manual/factories_advanced_extra.rst docs/source/traitsui_user_manual/factories_basic.rst docs/source/traitsui_user_manual/factory_intro.rst docs/source/traitsui_user_manual/glossary.rst docs/source/traitsui_user_manual/handler.rst docs/source/traitsui_user_manual/index.rst docs/source/traitsui_user_manual/intro.rst docs/source/traitsui_user_manual/predefined_traits.rst docs/source/traitsui_user_manual/testing.rst docs/source/traitsui_user_manual/tips.rst docs/source/traitsui_user_manual/view.rst docs/source/traitsui_user_manual/examples/configure_traits_view.py docs/source/traitsui_user_manual/examples/configure_traits_view_buttons.py docs/source/traitsui_user_manual/examples/configure_traits_view_group.py docs/source/traitsui_user_manual/examples/default_trait_editors.py docs/source/traitsui_user_manual/examples/default_traits_view.py docs/source/traitsui_user_manual/examples/default_traits_view2.py docs/source/traitsui_user_manual/examples/enum_editor.py docs/source/traitsui_user_manual/examples/handler_override.py docs/source/traitsui_user_manual/examples/instance_editor_selection.py docs/source/traitsui_user_manual/examples/key_bindings.py docs/source/traitsui_user_manual/examples/mixed_styles.py docs/source/traitsui_user_manual/examples/multi_object_view.py docs/source/traitsui_user_manual/examples/multiple_views.py docs/source/traitsui_user_manual/examples/tree_editor.py docs/source/traitsui_user_manual/examples/wizard.py docs/source/traitsui_user_manual/images/ArrayEditor_demo.png docs/source/traitsui_user_manual/images/Auto_update_TabularEditor_demo.png docs/source/traitsui_user_manual/images/BooleanEditor_demo.png docs/source/traitsui_user_manual/images/ButtonEditor_demo.png docs/source/traitsui_user_manual/images/CSVListEditor_demo.png docs/source/traitsui_user_manual/images/CheckListEditor_demo.png docs/source/traitsui_user_manual/images/CodeEditor_demo.png docs/source/traitsui_user_manual/images/ColorEditor_demo.png docs/source/traitsui_user_manual/images/CompoundEditor_demo.png docs/source/traitsui_user_manual/images/DataFrameEditor_demo.png docs/source/traitsui_user_manual/images/DatetimeEditor_demo.png docs/source/traitsui_user_manual/images/DirectoryEditor_demo.png docs/source/traitsui_user_manual/images/Dynamic_EnumEditor_demo.png docs/source/traitsui_user_manual/images/EnumEditor_demo.png docs/source/traitsui_user_manual/images/FileEditor_demo.png docs/source/traitsui_user_manual/images/FontEditor_demo.png docs/source/traitsui_user_manual/images/HTMLEditor_demo.png docs/source/traitsui_user_manual/images/HTML_editor.png docs/source/traitsui_user_manual/images/ImageEnumEditor_demo.png docs/source/traitsui_user_manual/images/InstanceEditor_demo.png docs/source/traitsui_user_manual/images/ListEditor_demo.png docs/source/traitsui_user_manual/images/ListStrEditor_demo.png docs/source/traitsui_user_manual/images/RGBColorEditor_demo.png docs/source/traitsui_user_manual/images/RangeEditor_demo.png docs/source/traitsui_user_manual/images/SetEditor_demo.png docs/source/traitsui_user_manual/images/TableEditor_demo.png docs/source/traitsui_user_manual/images/TextEditor_demo.png docs/source/traitsui_user_manual/images/TitleEditor_demo.png docs/source/traitsui_user_manual/images/TreeEditor_demo.png docs/source/traitsui_user_manual/images/TupleEditor_demo.png docs/source/traitsui_user_manual/images/alter_title_after.png docs/source/traitsui_user_manual/images/alter_title_before.png docs/source/traitsui_user_manual/images/array_view_editor.jpg docs/source/traitsui_user_manual/images/color_picker_windows.jpg docs/source/traitsui_user_manual/images/custom_enum_editor.jpg docs/source/traitsui_user_manual/images/default_text_editor.png docs/source/traitsui_user_manual/images/delete_item_icon.png docs/source/traitsui_user_manual/images/font_dialog_windows.jpg docs/source/traitsui_user_manual/images/html_code_editor.png docs/source/traitsui_user_manual/images/insert_item_icon.png docs/source/traitsui_user_manual/images/key_binding_editor.jpg docs/source/traitsui_user_manual/images/led_editor.png docs/source/traitsui_user_manual/images/list_with_context_menu.png docs/source/traitsui_user_manual/images/move_down_icon.png docs/source/traitsui_user_manual/images/move_up_icon.png docs/source/traitsui_user_manual/images/no_sort_icon.png docs/source/traitsui_user_manual/images/notebook_list_editor.jpg docs/source/traitsui_user_manual/images/read_only_editor.jpg docs/source/traitsui_user_manual/images/search_table_icon.png docs/source/traitsui_user_manual/images/shell_editor.jpg docs/source/traitsui_user_manual/images/simple_enum_editor_closed.jpg docs/source/traitsui_user_manual/images/simple_enum_editor_open.jpg docs/source/traitsui_user_manual/images/table_column_selection.jpg docs/source/traitsui_user_manual/images/table_prefs.png docs/source/traitsui_user_manual/images/tabular_editor.jpg docs/source/traitsui_user_manual/images/text_editor.jpg docs/source/traitsui_user_manual/images/text_editor_integers.png docs/source/traitsui_user_manual/images/text_editor_strings.png docs/source/traitsui_user_manual/images/text_editors_passwords.png docs/source/traitsui_user_manual/images/ui_for_ex1.jpg docs/source/traitsui_user_manual/images/ui_for_ex12.jpg docs/source/traitsui_user_manual/images/ui_for_ex13.jpg docs/source/traitsui_user_manual/images/ui_for_ex16.png docs/source/traitsui_user_manual/images/ui_for_ex2.jpg docs/source/traitsui_user_manual/images/ui_for_ex3.jpg docs/source/traitsui_user_manual/images/ui_for_ex4.jpg docs/source/traitsui_user_manual/images/ui_for_ex7.jpg docs/source/traitsui_user_manual/images/value_editor.png docs/source/traitsui_user_manual/images/wizard_dialog_1.png docs/source/traitsui_user_manual/images/wizard_dialog_2.png docs/source/traitsui_user_manual/scripts/regenerate_example_screenshots.py docs/source/traitsui_user_manual/testing/substitutions.rst docs/source/traitsui_user_manual/testing/discussions/automated_vs_manual_test.rst docs/source/traitsui_user_manual/testing/discussions/compatibility_pyface_testing.rst docs/source/traitsui_user_manual/testing/discussions/debugging-gui-tests-at-runtime.rst docs/source/traitsui_user_manual/testing/discussions/event_loop_and_exceptions.rst docs/source/traitsui_user_manual/testing/discussions/target_interaction_location.rst docs/source/traitsui_user_manual/testing/discussions/working_of_extensions.rst docs/source/traitsui_user_manual/testing/howtos/add_new_interaction.rst docs/source/traitsui_user_manual/testing/howtos/add_new_location.rst docs/source/traitsui_user_manual/testing/references/examples.rst docs/source/traitsui_user_manual/testing/tutorials/first_test.rst docs/source/traitsui_user_manual/testing/tutorials/test_with_nested_object.rst docs/source/traitsui_user_manual/testing/tutorials/images/first_test/init-app.png docs/source/traitsui_user_manual/testing/tutorials/images/first_test/modified-fields.png docs/source/traitsui_user_manual/testing/tutorials/images/test_with_nested_object/init-app.png docs/source/tutorials/index.rst docs/source/tutorials/traits_ui_scientific_app.rst docs/source/tutorials/code_snippets/code_block0.py docs/source/tutorials/code_snippets/code_block1.py docs/source/tutorials/code_snippets/container.py docs/source/tutorials/code_snippets/echo_box.py docs/source/tutorials/code_snippets/event_loop.py docs/source/tutorials/code_snippets/event_loop_qt.py docs/source/tutorials/code_snippets/event_loop_wx.py docs/source/tutorials/code_snippets/interactive.py docs/source/tutorials/code_snippets/mpl_figure_editor.py docs/source/tutorials/code_snippets/thread_example.py docs/source/tutorials/code_snippets/traits_thread.py docs/source/tutorials/images/application1.png docs/source/tutorials/images/application2.png docs/source/tutorials/images/application3.png docs/source/tutorials/images/code_block1.png docs/source/tutorials/images/container.png docs/source/tutorials/images/interactive.png docs/source/tutorials/images/mpl_figure_editor.png docs/source/tutorials/images/traits_thread.png examples/README.rst examples/demo/examples.cfg examples/demo/index.rst examples/demo/traits_ui_demo.jpg examples/demo/Advanced/Adapted_tree_editor_demo.py examples/demo/Advanced/Apply_Revert_handler_demo.py examples/demo/Advanced/Auto_editable_readonly_table_cells.py examples/demo/Advanced/Auto_update_TabularEditor_demo.py examples/demo/Advanced/Date_editor_demo.py examples/demo/Advanced/Date_range_editor_demo.py examples/demo/Advanced/Dynamic_EnumEditor_demo.py examples/demo/Advanced/Dynamic_range_trait_and_editor.py examples/demo/Advanced/Dynamic_views_demo.py examples/demo/Advanced/Dynamically_changing_buttons_demo.py examples/demo/Advanced/HDF5_tree_demo.py examples/demo/Advanced/HDF5_tree_demo2.py examples/demo/Advanced/History_demo.py examples/demo/Advanced/Invalid_state_handling.py examples/demo/Advanced/ListStrAdapter_demo.py examples/demo/Advanced/ListStrEditor_demo.py examples/demo/Advanced/List_editor_notebook_selection_demo.py examples/demo/Advanced/List_editors_demo.py examples/demo/Advanced/MVC_demo.py examples/demo/Advanced/Multi_select_string_list.py examples/demo/Advanced/Multi_thread_demo.py examples/demo/Advanced/Multi_thread_demo_2.py examples/demo/Advanced/NumPy_array_tabular_editor_demo.py examples/demo/Advanced/NumPy_array_view_editor_demo.py examples/demo/Advanced/Popup_Dialog_demo.py examples/demo/Advanced/Property_List_demo.py examples/demo/Advanced/Scrubber_editor_demo.py examples/demo/Advanced/Settable_cached_property.py examples/demo/Advanced/Statusbar_demo.py examples/demo/Advanced/String_list_ui_editor.py examples/demo/Advanced/Table_editor_with_checkbox_column.py examples/demo/Advanced/Table_editor_with_context_menu_demo.py examples/demo/Advanced/Table_editor_with_live_search_and_cell_editor.py examples/demo/Advanced/Table_editor_with_progress_column.py examples/demo/Advanced/Tabular_editor_demo.py examples/demo/Advanced/Tabular_editor_with_context_menu_demo.py examples/demo/Advanced/Time_editor_demo.py examples/demo/Advanced/Tree_editor_required_traits_demo.py examples/demo/Advanced/index.rst examples/demo/Advanced/test_fixed.h5 examples/demo/Advanced/test_fixed_compressed.h5 examples/demo/Advanced/test_h5pydata.h5 examples/demo/Advanced/test_table_dc.h5 examples/demo/Advanced/test_table_no_dc.h5 examples/demo/Advanced/tests/test_List_editor_notebook_selection_demo.py examples/demo/Applications/Python_source_browser.py examples/demo/Applications/converter.py examples/demo/Applications/index.rst examples/demo/Applications/images/GG5.png examples/demo/Applications/images/TFB.png examples/demo/Applications/images/blue_ball.png examples/demo/Applications/images/header.png examples/demo/Applications/images/notebook_close.png examples/demo/Applications/images/notebook_open.png examples/demo/Applications/images/red_ball.png examples/demo/Applications/tests/test_converter.py examples/demo/Dynamic_Forms/dynamic_form_using_instances.py examples/demo/Dynamic_Forms/dynamic_range_editor.py examples/demo/Dynamic_Forms/dynamic_selector.py examples/demo/Dynamic_Forms/enabled_when.py examples/demo/Dynamic_Forms/index.rst examples/demo/Dynamic_Forms/visible_when.py examples/demo/Dynamic_Forms/tests/test_visible_when.py examples/demo/Extras/Image_editor_demo.py examples/demo/Extras/LED_display.py examples/demo/Extras/Tree_editor_with_TreeNodeRenderer.py examples/demo/Extras/animated_GIF.py examples/demo/Extras/index.rst examples/demo/Extras/images/info.png examples/demo/Extras/images/logo_32x32.gif examples/demo/Extras/images/logo_48x48.gif examples/demo/Extras/images/logo_64x64.gif examples/demo/Extras/images/python-logo.png examples/demo/Extras/tests/__init__.py examples/demo/Extras/tests/test_Image_editor_demo.py examples/demo/Extras/windows/flash.py examples/demo/Extras/windows/internet_explorer.py examples/demo/Misc/demo_group_size.py examples/demo/Misc/index.rst examples/demo/Misc/using_springs.py examples/demo/Standard_Editors/ArrayEditor_demo.py examples/demo/Standard_Editors/BooleanEditor_demo.py examples/demo/Standard_Editors/BooleanEditor_simple_demo.py examples/demo/Standard_Editors/ButtonEditor_demo.py examples/demo/Standard_Editors/ButtonEditor_simple_demo.py examples/demo/Standard_Editors/CSVListEditor_demo.py examples/demo/Standard_Editors/CheckListEditor_demo.py examples/demo/Standard_Editors/CheckListEditor_simple_demo.py examples/demo/Standard_Editors/CodeEditor_demo.py examples/demo/Standard_Editors/ColorEditor_demo.py examples/demo/Standard_Editors/CompoundEditor_demo.py examples/demo/Standard_Editors/DataFrameEditor_demo.py examples/demo/Standard_Editors/DatetimeEditor_demo.py examples/demo/Standard_Editors/DirectoryEditor_demo.py examples/demo/Standard_Editors/EnumEditor_demo.py examples/demo/Standard_Editors/FileEditor_demo.py examples/demo/Standard_Editors/FontEditor_demo.py examples/demo/Standard_Editors/HTMLEditor_demo.py examples/demo/Standard_Editors/ImageEnumEditor_demo.py examples/demo/Standard_Editors/InstanceEditor_demo.py examples/demo/Standard_Editors/ListEditor_demo.py examples/demo/Standard_Editors/RGBColorEditor_demo.py examples/demo/Standard_Editors/RangeEditor_demo.py examples/demo/Standard_Editors/SetEditor_demo.py examples/demo/Standard_Editors/TableEditor_demo.py examples/demo/Standard_Editors/TextEditor_demo.py examples/demo/Standard_Editors/TitleEditor_demo.py examples/demo/Standard_Editors/TreeEditor_demo.py examples/demo/Standard_Editors/TupleEditor_demo.py examples/demo/Standard_Editors/VideoEditor_demo.py examples/demo/Standard_Editors/index.rst examples/demo/Standard_Editors/File_Dialog/File_Open.py examples/demo/Standard_Editors/File_Dialog/File_Open_with_Custom_Extension.py examples/demo/Standard_Editors/File_Dialog/File_Open_with_FileInfo_Extension.py examples/demo/Standard_Editors/File_Dialog/File_Open_with_ImageInfo_Extension.py examples/demo/Standard_Editors/File_Dialog/File_Open_with_Multiple_Extensions.py examples/demo/Standard_Editors/File_Dialog/File_Open_with_TextInfo_Extension.py examples/demo/Standard_Editors/tests/test_BooleanEditor_demo.py examples/demo/Standard_Editors/tests/test_BooleanEditor_simple_demo.py examples/demo/Standard_Editors/tests/test_ButtonEditor_demo.py examples/demo/Standard_Editors/tests/test_ButtonEditor_simple_demo.py examples/demo/Standard_Editors/tests/test_CheckListEditor_simple_demo.py examples/demo/Standard_Editors/tests/test_EnumEditor_demo.py examples/demo/Standard_Editors/tests/test_FileEditor_demo.py examples/demo/Standard_Editors/tests/test_InstanceEditor_demo.py examples/demo/Standard_Editors/tests/test_ListEditor_demo.py examples/demo/Standard_Editors/tests/test_RangeEditor_demo.py examples/demo/Standard_Editors/tests/test_TableEditor_demo.py examples/demo/Standard_Editors/tests/test_TextEditor_demo.py examples/tutorials/default.css examples/tutorials/tutor.py examples/tutorials/doc_examples/default.css examples/tutorials/doc_examples/doc_examples.rst examples/tutorials/doc_examples/lesson.desc examples/tutorials/doc_examples/examples/include_extra.py examples/tutorials/doc_examples/examples/lesson.desc examples/tutorials/doc_examples/examples/override_editor.py examples/tutorials/doc_examples/examples/view_attributes.py examples/tutorials/doc_examples/examples/view_multi_object.py examples/tutorials/doc_examples/examples/view_standalone.py examples/tutorials/traitsui_4.0/buttons.py examples/tutorials/traitsui_4.0/default.css examples/tutorials/traitsui_4.0/deferred.py examples/tutorials/traitsui_4.0/items.py examples/tutorials/traitsui_4.0/model_view.py examples/tutorials/traitsui_4.0/tutorial.desc examples/tutorials/traitsui_4.0/editors/animated_gif.py examples/tutorials/traitsui_4.0/editors/flash.py examples/tutorials/traitsui_4.0/editors/ie_html.py examples/tutorials/traitsui_4.0/editors/led.py examples/tutorials/traitsui_4.0/editors/tutorial.desc examples/tutorials/traitsui_4.0/editors/tabular_editor/numpy_array.py examples/tutorials/traitsui_4.0/editors/tabular_editor/python_source_browser.py examples/tutorials/traitsui_4.0/editors/tabular_editor/sm_person_example.py examples/tutorials/traitsui_4.0/editors/tabular_editor/tabular_editor.htm examples/tutorials/traitsui_4.0/editors/tabular_editor/tabular_editor.rst examples/tutorials/traitsui_4.0/editors/tabular_editor/tutorial.desc integrationtests/styled_date_editor_test.py integrationtests/test_all_examples.py integrationtests/ui/array_editor_test.py integrationtests/ui/buttons_test.py integrationtests/ui/check_list_editor_test.py integrationtests/ui/check_list_editor_test2.py integrationtests/ui/code_editor_test.py integrationtests/ui/enum_dynamic_test.py integrationtests/ui/enum_editor_test.py integrationtests/ui/html_editor_test.py integrationtests/ui/instance_drag_test.py integrationtests/ui/instance_editor_test.py integrationtests/ui/instance_editor_test2.py integrationtests/ui/instance_editor_test3.py integrationtests/ui/instance_editor_test4.py integrationtests/ui/instance_editor_test5.py integrationtests/ui/instance_editor_test6.py integrationtests/ui/large_range_editor.py integrationtests/ui/list_traits_ui_test.py integrationtests/ui/set_dynamic_test.py integrationtests/ui/shell_editor_test.py integrationtests/ui/table_editor_focus_bug.py integrationtests/ui/table_editor_test.py integrationtests/ui/table_editor_test2.py integrationtests/ui/table_list_editor_test.py integrationtests/ui/test_ui.py integrationtests/ui/test_ui3.py integrationtests/ui/test_ui4.py integrationtests/ui/test_ui5.py integrationtests/ui/text_editor_invalid.py integrationtests/ui/tree_editor_test.py integrationtests/ui/images/bottom_left_origin.gif integrationtests/ui/images/bottom_right_origin.gif integrationtests/ui/images/top_left_origin.gif integrationtests/ui/images/top_right_origin.gif traitsui/__init__.py traitsui/api.py traitsui/base_panel.py traitsui/basic_editor_factory.py traitsui/color_column.py traitsui/context_value.py traitsui/default_dock_window_theme.py traitsui/delegating_handler.py traitsui/dock_window_theme.py traitsui/dockable_view_element.py traitsui/editor.py traitsui/editor_factory.py traitsui/file_dialog.py traitsui/group.py traitsui/handler.py traitsui/help.py traitsui/help_template.py traitsui/helper.py traitsui/include.py traitsui/instance_choice.py traitsui/item.py traitsui/key_bindings.py traitsui/list_str_adapter.py traitsui/menu.py traitsui/message.py traitsui/mimedata.py traitsui/table_column.py traitsui/table_filter.py traitsui/tabular_adapter.py traitsui/theme.py traitsui/toolkit.py traitsui/toolkit_traits.py traitsui/tree_node.py traitsui/tree_node_renderer.py traitsui/ui.py traitsui/ui_editor.py traitsui/ui_info.py traitsui/ui_traits.py traitsui/undo.py traitsui/value_tree.py traitsui/view.py traitsui/view_element.py traitsui/view_elements.py traitsui.egg-info/PKG-INFO traitsui.egg-info/SOURCES.txt traitsui.egg-info/dependency_links.txt traitsui.egg-info/entry_points.txt traitsui.egg-info/requires.txt traitsui.egg-info/top_level.txt traitsui/editors/__init__.py traitsui/editors/api.py traitsui/editors/array_editor.py traitsui/editors/boolean_editor.py traitsui/editors/button_editor.py traitsui/editors/check_list_editor.py traitsui/editors/code_editor.py traitsui/editors/color_editor.py traitsui/editors/compound_editor.py traitsui/editors/csv_list_editor.py traitsui/editors/custom_editor.py traitsui/editors/date_editor.py traitsui/editors/date_range_editor.py traitsui/editors/datetime_editor.py traitsui/editors/default_override.py traitsui/editors/directory_editor.py traitsui/editors/dnd_editor.py traitsui/editors/drop_editor.py traitsui/editors/enum_editor.py traitsui/editors/file_editor.py traitsui/editors/font_editor.py traitsui/editors/history_editor.py traitsui/editors/html_editor.py traitsui/editors/image_editor.py traitsui/editors/image_enum_editor.py traitsui/editors/instance_editor.py traitsui/editors/key_binding_editor.py traitsui/editors/list_editor.py traitsui/editors/list_str_editor.py traitsui/editors/null_editor.py traitsui/editors/popup_editor.py traitsui/editors/progress_editor.py traitsui/editors/range_editor.py traitsui/editors/rgb_color_editor.py traitsui/editors/scrubber_editor.py traitsui/editors/search_editor.py traitsui/editors/set_editor.py traitsui/editors/shell_editor.py traitsui/editors/styled_date_editor.py traitsui/editors/table_editor.py traitsui/editors/tabular_editor.py traitsui/editors/text_editor.py traitsui/editors/time_editor.py traitsui/editors/title_editor.py traitsui/editors/tree_editor.py traitsui/editors/tuple_editor.py traitsui/editors/value_editor.py traitsui/editors/video_editor.py traitsui/examples/demo/examples.cfg traitsui/examples/demo/index.rst traitsui/examples/demo/traits_ui_demo.jpg traitsui/examples/demo/Advanced/Adapted_tree_editor_demo.py traitsui/examples/demo/Advanced/Apply_Revert_handler_demo.py traitsui/examples/demo/Advanced/Auto_editable_readonly_table_cells.py traitsui/examples/demo/Advanced/Auto_update_TabularEditor_demo.py traitsui/examples/demo/Advanced/Date_editor_demo.py traitsui/examples/demo/Advanced/Date_range_editor_demo.py traitsui/examples/demo/Advanced/Dynamic_EnumEditor_demo.py traitsui/examples/demo/Advanced/Dynamic_range_trait_and_editor.py traitsui/examples/demo/Advanced/Dynamic_views_demo.py traitsui/examples/demo/Advanced/Dynamically_changing_buttons_demo.py traitsui/examples/demo/Advanced/HDF5_tree_demo.py traitsui/examples/demo/Advanced/HDF5_tree_demo2.py traitsui/examples/demo/Advanced/History_demo.py traitsui/examples/demo/Advanced/Invalid_state_handling.py traitsui/examples/demo/Advanced/ListStrAdapter_demo.py traitsui/examples/demo/Advanced/ListStrEditor_demo.py traitsui/examples/demo/Advanced/List_editor_notebook_selection_demo.py traitsui/examples/demo/Advanced/List_editors_demo.py traitsui/examples/demo/Advanced/MVC_demo.py traitsui/examples/demo/Advanced/Multi_select_string_list.py traitsui/examples/demo/Advanced/Multi_thread_demo.py traitsui/examples/demo/Advanced/Multi_thread_demo_2.py traitsui/examples/demo/Advanced/NumPy_array_tabular_editor_demo.py traitsui/examples/demo/Advanced/NumPy_array_view_editor_demo.py traitsui/examples/demo/Advanced/Popup_Dialog_demo.py traitsui/examples/demo/Advanced/Property_List_demo.py traitsui/examples/demo/Advanced/Scrubber_editor_demo.py traitsui/examples/demo/Advanced/Settable_cached_property.py traitsui/examples/demo/Advanced/Statusbar_demo.py traitsui/examples/demo/Advanced/String_list_ui_editor.py traitsui/examples/demo/Advanced/Table_editor_with_checkbox_column.py traitsui/examples/demo/Advanced/Table_editor_with_context_menu_demo.py traitsui/examples/demo/Advanced/Table_editor_with_live_search_and_cell_editor.py traitsui/examples/demo/Advanced/Table_editor_with_progress_column.py traitsui/examples/demo/Advanced/Tabular_editor_demo.py traitsui/examples/demo/Advanced/Tabular_editor_with_context_menu_demo.py traitsui/examples/demo/Advanced/Time_editor_demo.py traitsui/examples/demo/Advanced/Tree_editor_required_traits_demo.py traitsui/examples/demo/Advanced/index.rst traitsui/examples/demo/Advanced/test_fixed.h5 traitsui/examples/demo/Advanced/test_fixed_compressed.h5 traitsui/examples/demo/Advanced/test_h5pydata.h5 traitsui/examples/demo/Advanced/test_table_dc.h5 traitsui/examples/demo/Advanced/test_table_no_dc.h5 traitsui/examples/demo/Advanced/tests/test_List_editor_notebook_selection_demo.py traitsui/examples/demo/Applications/Python_source_browser.py traitsui/examples/demo/Applications/converter.py traitsui/examples/demo/Applications/index.rst traitsui/examples/demo/Applications/images/GG5.png traitsui/examples/demo/Applications/images/TFB.png traitsui/examples/demo/Applications/images/blue_ball.png traitsui/examples/demo/Applications/images/header.png traitsui/examples/demo/Applications/images/notebook_close.png traitsui/examples/demo/Applications/images/notebook_open.png traitsui/examples/demo/Applications/images/red_ball.png traitsui/examples/demo/Applications/tests/test_converter.py traitsui/examples/demo/Dynamic_Forms/dynamic_form_using_instances.py traitsui/examples/demo/Dynamic_Forms/dynamic_range_editor.py traitsui/examples/demo/Dynamic_Forms/dynamic_selector.py traitsui/examples/demo/Dynamic_Forms/enabled_when.py traitsui/examples/demo/Dynamic_Forms/index.rst traitsui/examples/demo/Dynamic_Forms/visible_when.py traitsui/examples/demo/Dynamic_Forms/tests/test_visible_when.py traitsui/examples/demo/Extras/Image_editor_demo.py traitsui/examples/demo/Extras/LED_display.py traitsui/examples/demo/Extras/Tree_editor_with_TreeNodeRenderer.py traitsui/examples/demo/Extras/animated_GIF.py traitsui/examples/demo/Extras/index.rst traitsui/examples/demo/Extras/images/info.png traitsui/examples/demo/Extras/images/logo_32x32.gif traitsui/examples/demo/Extras/images/logo_48x48.gif traitsui/examples/demo/Extras/images/logo_64x64.gif traitsui/examples/demo/Extras/images/python-logo.png traitsui/examples/demo/Extras/tests/__init__.py traitsui/examples/demo/Extras/tests/test_Image_editor_demo.py traitsui/examples/demo/Extras/windows/flash.py traitsui/examples/demo/Extras/windows/internet_explorer.py traitsui/examples/demo/Misc/demo_group_size.py traitsui/examples/demo/Misc/index.rst traitsui/examples/demo/Misc/using_springs.py traitsui/examples/demo/Standard_Editors/ArrayEditor_demo.py traitsui/examples/demo/Standard_Editors/BooleanEditor_demo.py traitsui/examples/demo/Standard_Editors/BooleanEditor_simple_demo.py traitsui/examples/demo/Standard_Editors/ButtonEditor_demo.py traitsui/examples/demo/Standard_Editors/ButtonEditor_simple_demo.py traitsui/examples/demo/Standard_Editors/CSVListEditor_demo.py traitsui/examples/demo/Standard_Editors/CheckListEditor_demo.py traitsui/examples/demo/Standard_Editors/CheckListEditor_simple_demo.py traitsui/examples/demo/Standard_Editors/CodeEditor_demo.py traitsui/examples/demo/Standard_Editors/ColorEditor_demo.py traitsui/examples/demo/Standard_Editors/CompoundEditor_demo.py traitsui/examples/demo/Standard_Editors/DataFrameEditor_demo.py traitsui/examples/demo/Standard_Editors/DatetimeEditor_demo.py traitsui/examples/demo/Standard_Editors/DirectoryEditor_demo.py traitsui/examples/demo/Standard_Editors/EnumEditor_demo.py traitsui/examples/demo/Standard_Editors/FileEditor_demo.py traitsui/examples/demo/Standard_Editors/FontEditor_demo.py traitsui/examples/demo/Standard_Editors/HTMLEditor_demo.py traitsui/examples/demo/Standard_Editors/ImageEnumEditor_demo.py traitsui/examples/demo/Standard_Editors/InstanceEditor_demo.py traitsui/examples/demo/Standard_Editors/ListEditor_demo.py traitsui/examples/demo/Standard_Editors/RGBColorEditor_demo.py traitsui/examples/demo/Standard_Editors/RangeEditor_demo.py traitsui/examples/demo/Standard_Editors/SetEditor_demo.py traitsui/examples/demo/Standard_Editors/TableEditor_demo.py traitsui/examples/demo/Standard_Editors/TextEditor_demo.py traitsui/examples/demo/Standard_Editors/TitleEditor_demo.py traitsui/examples/demo/Standard_Editors/TreeEditor_demo.py traitsui/examples/demo/Standard_Editors/TupleEditor_demo.py traitsui/examples/demo/Standard_Editors/VideoEditor_demo.py traitsui/examples/demo/Standard_Editors/index.rst traitsui/examples/demo/Standard_Editors/File_Dialog/File_Open.py traitsui/examples/demo/Standard_Editors/File_Dialog/File_Open_with_Custom_Extension.py traitsui/examples/demo/Standard_Editors/File_Dialog/File_Open_with_FileInfo_Extension.py traitsui/examples/demo/Standard_Editors/File_Dialog/File_Open_with_ImageInfo_Extension.py traitsui/examples/demo/Standard_Editors/File_Dialog/File_Open_with_Multiple_Extensions.py traitsui/examples/demo/Standard_Editors/File_Dialog/File_Open_with_TextInfo_Extension.py traitsui/examples/demo/Standard_Editors/tests/test_BooleanEditor_demo.py traitsui/examples/demo/Standard_Editors/tests/test_BooleanEditor_simple_demo.py traitsui/examples/demo/Standard_Editors/tests/test_ButtonEditor_demo.py traitsui/examples/demo/Standard_Editors/tests/test_ButtonEditor_simple_demo.py traitsui/examples/demo/Standard_Editors/tests/test_CheckListEditor_simple_demo.py traitsui/examples/demo/Standard_Editors/tests/test_EnumEditor_demo.py traitsui/examples/demo/Standard_Editors/tests/test_FileEditor_demo.py traitsui/examples/demo/Standard_Editors/tests/test_InstanceEditor_demo.py traitsui/examples/demo/Standard_Editors/tests/test_ListEditor_demo.py traitsui/examples/demo/Standard_Editors/tests/test_RangeEditor_demo.py traitsui/examples/demo/Standard_Editors/tests/test_TableEditor_demo.py traitsui/examples/demo/Standard_Editors/tests/test_TextEditor_demo.py traitsui/extras/__init__.py traitsui/extras/_demo_info.py traitsui/extras/_demo_legacy.py traitsui/extras/api.py traitsui/extras/checkbox_column.py traitsui/extras/demo.py traitsui/extras/edit_column.py traitsui/extras/has_dynamic_views.py traitsui/extras/progress_column.py traitsui/extras/saving.py traitsui/extras/images/next.png traitsui/extras/images/parent.png traitsui/extras/images/previous.png traitsui/extras/images/reload.png traitsui/extras/images/run.png traitsui/images/frame.png traitsui/null/__init__.py traitsui/null/color_trait.py traitsui/null/font_trait.py traitsui/null/rgb_color_trait.py traitsui/null/toolkit.py traitsui/qt/__init__.py traitsui/qt/array_editor.py traitsui/qt/array_view_editor.py traitsui/qt/boolean_editor.py traitsui/qt/button_editor.py traitsui/qt/check_list_editor.py traitsui/qt/clipboard.py traitsui/qt/code_editor.py traitsui/qt/color_editor.py traitsui/qt/color_trait.py traitsui/qt/compound_editor.py traitsui/qt/constants.py traitsui/qt/csv_list_editor.py traitsui/qt/custom_editor.py traitsui/qt/data_frame_editor.py traitsui/qt/date_editor.py traitsui/qt/date_range_editor.py traitsui/qt/datetime_editor.py traitsui/qt/directory_editor.py traitsui/qt/drop_editor.py traitsui/qt/editor.py traitsui/qt/editor_factory.py traitsui/qt/enum_editor.py traitsui/qt/file_editor.py traitsui/qt/font_editor.py traitsui/qt/font_trait.py traitsui/qt/helper.py traitsui/qt/history_editor.py traitsui/qt/html_editor.py traitsui/qt/image_editor.py traitsui/qt/image_enum_editor.py traitsui/qt/instance_editor.py traitsui/qt/key_binding_editor.py traitsui/qt/key_event_to_name.py traitsui/qt/list_editor.py traitsui/qt/list_str_editor.py traitsui/qt/list_str_model.py traitsui/qt/menu.py traitsui/qt/null_editor.py traitsui/qt/progress_editor.py traitsui/qt/range_editor.py traitsui/qt/rgb_color_editor.py traitsui/qt/rgb_color_trait.py traitsui/qt/search_editor.py traitsui/qt/set_editor.py traitsui/qt/shell_editor.py traitsui/qt/styled_date_editor.py traitsui/qt/table_editor.py traitsui/qt/table_model.py traitsui/qt/tabular_editor.py traitsui/qt/tabular_model.py traitsui/qt/text_editor.py traitsui/qt/time_editor.py traitsui/qt/title_editor.py traitsui/qt/toolkit.py traitsui/qt/tree_editor.py traitsui/qt/tree_node_renderers.py traitsui/qt/tuple_editor.py traitsui/qt/ui_base.py traitsui/qt/ui_editor.py traitsui/qt/ui_live.py traitsui/qt/ui_modal.py traitsui/qt/ui_panel.py traitsui/qt/value_editor.py traitsui/qt/video_editor.py traitsui/qt/view_application.py traitsui/qt/extra/__init__.py traitsui/qt/extra/bounds_editor.py traitsui/qt/extra/checkbox_renderer.py traitsui/qt/extra/led_editor.py traitsui/qt/extra/progress_renderer.py traitsui/qt/extra/qt_view.py traitsui/qt/extra/range_slider.py traitsui/qt/extra/table_image_renderer.py traitsui/qt/images/closetab.png traitsui/qt/images/frame.png traitsui/qt/images/list_editor.png traitsui/qt/images/next.png traitsui/qt/images/previous.png traitsui/qt/tests/__init__.py traitsui/qt/tests/test_color_trait.py traitsui/qt/tests/test_font_trait.py traitsui/qt/tests/test_helper.py traitsui/qt/tests/test_tabular_model.py traitsui/qt/tests/test_ui_base.py traitsui/qt/tests/test_ui_panel.py traitsui/qt4/__init__.py traitsui/testing/README.txt traitsui/testing/__init__.py traitsui/testing/_exception_handling.py traitsui/testing/_gui.py traitsui/testing/api.py traitsui/testing/data/test.mp4 traitsui/testing/tester/README.txt traitsui/testing/tester/__init__.py traitsui/testing/tester/_abstract_target_registry.py traitsui/testing/tester/_dynamic_target_registry.py traitsui/testing/tester/command.py traitsui/testing/tester/exceptions.py traitsui/testing/tester/locator.py traitsui/testing/tester/query.py traitsui/testing/tester/target_registry.py traitsui/testing/tester/ui_tester.py traitsui/testing/tester/ui_wrapper.py traitsui/testing/tester/_ui_tester_registry/README.txt traitsui/testing/tester/_ui_tester_registry/__init__.py traitsui/testing/tester/_ui_tester_registry/_common_ui_targets.py traitsui/testing/tester/_ui_tester_registry/_compat.py traitsui/testing/tester/_ui_tester_registry/_layout.py traitsui/testing/tester/_ui_tester_registry/_traitsui_ui.py traitsui/testing/tester/_ui_tester_registry/default_registry.py traitsui/testing/tester/_ui_tester_registry/qt/README.txt traitsui/testing/tester/_ui_tester_registry/qt/__init__.py traitsui/testing/tester/_ui_tester_registry/qt/_control_widget_registry.py traitsui/testing/tester/_ui_tester_registry/qt/_interaction_helpers.py traitsui/testing/tester/_ui_tester_registry/qt/_registry_helper.py traitsui/testing/tester/_ui_tester_registry/qt/default_registry.py traitsui/testing/tester/_ui_tester_registry/qt/_traitsui/README.txt traitsui/testing/tester/_ui_tester_registry/qt/_traitsui/__init__.py traitsui/testing/tester/_ui_tester_registry/qt/_traitsui/boolean_editor.py traitsui/testing/tester/_ui_tester_registry/qt/_traitsui/button_editor.py traitsui/testing/tester/_ui_tester_registry/qt/_traitsui/check_list_editor.py traitsui/testing/tester/_ui_tester_registry/qt/_traitsui/directory_editor.py traitsui/testing/tester/_ui_tester_registry/qt/_traitsui/editor_factory.py traitsui/testing/tester/_ui_tester_registry/qt/_traitsui/enum_editor.py traitsui/testing/tester/_ui_tester_registry/qt/_traitsui/file_editor.py traitsui/testing/tester/_ui_tester_registry/qt/_traitsui/font_editor.py traitsui/testing/tester/_ui_tester_registry/qt/_traitsui/instance_editor.py traitsui/testing/tester/_ui_tester_registry/qt/_traitsui/list_editor.py traitsui/testing/tester/_ui_tester_registry/qt/_traitsui/range_editor.py traitsui/testing/tester/_ui_tester_registry/qt/_traitsui/table_editor.py traitsui/testing/tester/_ui_tester_registry/qt/_traitsui/text_editor.py traitsui/testing/tester/_ui_tester_registry/qt/_traitsui/ui_base.py traitsui/testing/tester/_ui_tester_registry/qt/tests/__init__.py traitsui/testing/tester/_ui_tester_registry/qt/tests/test_control_widget_registry.py traitsui/testing/tester/_ui_tester_registry/qt/tests/test_interaction_helpers.py traitsui/testing/tester/_ui_tester_registry/tests/__init__.py traitsui/testing/tester/_ui_tester_registry/tests/test_default_registry.py traitsui/testing/tester/_ui_tester_registry/tests/test_layout.py traitsui/testing/tester/_ui_tester_registry/wx/README.txt traitsui/testing/tester/_ui_tester_registry/wx/__init__.py traitsui/testing/tester/_ui_tester_registry/wx/_control_widget_registry.py traitsui/testing/tester/_ui_tester_registry/wx/_interaction_helpers.py traitsui/testing/tester/_ui_tester_registry/wx/_registry_helper.py traitsui/testing/tester/_ui_tester_registry/wx/default_registry.py traitsui/testing/tester/_ui_tester_registry/wx/_traitsui/README.txt traitsui/testing/tester/_ui_tester_registry/wx/_traitsui/__init__.py traitsui/testing/tester/_ui_tester_registry/wx/_traitsui/boolean_editor.py traitsui/testing/tester/_ui_tester_registry/wx/_traitsui/button_editor.py traitsui/testing/tester/_ui_tester_registry/wx/_traitsui/check_list_editor.py traitsui/testing/tester/_ui_tester_registry/wx/_traitsui/directory_editor.py traitsui/testing/tester/_ui_tester_registry/wx/_traitsui/editor_factory.py traitsui/testing/tester/_ui_tester_registry/wx/_traitsui/enum_editor.py traitsui/testing/tester/_ui_tester_registry/wx/_traitsui/file_editor.py traitsui/testing/tester/_ui_tester_registry/wx/_traitsui/font_editor.py traitsui/testing/tester/_ui_tester_registry/wx/_traitsui/instance_editor.py traitsui/testing/tester/_ui_tester_registry/wx/_traitsui/list_editor.py traitsui/testing/tester/_ui_tester_registry/wx/_traitsui/range_editor.py traitsui/testing/tester/_ui_tester_registry/wx/_traitsui/text_editor.py traitsui/testing/tester/_ui_tester_registry/wx/_traitsui/ui_base.py traitsui/testing/tester/_ui_tester_registry/wx/tests/__init__.py traitsui/testing/tester/_ui_tester_registry/wx/tests/test_control_widget_registry.py traitsui/testing/tester/_ui_tester_registry/wx/tests/test_interaction_helpers.py traitsui/testing/tester/tests/__init__.py traitsui/testing/tester/tests/test_registry.py traitsui/testing/tester/tests/test_ui_tester.py traitsui/testing/tester/tests/test_ui_wrapper.py traitsui/testing/tests/__init__.py traitsui/testing/tests/test_api.py traitsui/testing/tests/test_exception_handling.py traitsui/testing/tests/test_gui.py traitsui/tests/__init__.py traitsui/tests/_tools.py traitsui/tests/test_actions.py traitsui/tests/test_color_column.py traitsui/tests/test_context_value.py traitsui/tests/test_controller.py traitsui/tests/test_datetime.py traitsui/tests/test_editor.py traitsui/tests/test_editors_imports.py traitsui/tests/test_handler.py traitsui/tests/test_helper.py traitsui/tests/test_key_bindings.py traitsui/tests/test_labels.py traitsui/tests/test_layout.py traitsui/tests/test_regression.py traitsui/tests/test_shadow_group.py traitsui/tests/test_splitter_prefs_restored.py traitsui/tests/test_theme.py traitsui/tests/test_toolkit.py traitsui/tests/test_toolkit_traits.py traitsui/tests/test_tree_node.py traitsui/tests/test_ui.py traitsui/tests/test_ui_panel.py traitsui/tests/test_ui_traits.py traitsui/tests/test_undo.py traitsui/tests/test_view_application.py traitsui/tests/test_visible_when_layout.py traitsui/tests/editors/__init__.py traitsui/tests/editors/test_animatedGIF_editor.py traitsui/tests/editors/test_boolean_editor.py traitsui/tests/editors/test_button_editor.py traitsui/tests/editors/test_check_list_editor.py traitsui/tests/editors/test_code_editor.py traitsui/tests/editors/test_csv_editor.py traitsui/tests/editors/test_date_editor.py traitsui/tests/editors/test_date_range_editor.py traitsui/tests/editors/test_datetime_editor.py traitsui/tests/editors/test_default_override.py traitsui/tests/editors/test_directory_editor.py traitsui/tests/editors/test_drop_editor.py traitsui/tests/editors/test_enum_editor.py traitsui/tests/editors/test_file_editor.py traitsui/tests/editors/test_font_editor.py traitsui/tests/editors/test_html_editor.py traitsui/tests/editors/test_image_editor.py traitsui/tests/editors/test_image_enum_editor.py traitsui/tests/editors/test_instance_editor.py traitsui/tests/editors/test_list_editor.py traitsui/tests/editors/test_liststr_editor.py traitsui/tests/editors/test_liststr_editor_selection.py traitsui/tests/editors/test_range_editor.py traitsui/tests/editors/test_range_editor_spinner.py traitsui/tests/editors/test_range_editor_text.py traitsui/tests/editors/test_set_editor.py traitsui/tests/editors/test_shell_editor.py traitsui/tests/editors/test_styled_date_editor.py traitsui/tests/editors/test_table_editor.py traitsui/tests/editors/test_tabular_editor.py traitsui/tests/editors/test_text_editor.py traitsui/tests/editors/test_tree_editor.py traitsui/tests/editors/test_tuple_editor.py traitsui/tests/editors/test_video_editor.py traitsui/tests/null_backend/__init__.py traitsui/tests/null_backend/test_font_trait.py traitsui/tests/null_backend/test_null_toolkit.py traitsui/tests/ui_editors/__init__.py traitsui/tests/ui_editors/test_data_frame_editor.py traitsui/ui_editors/__init__.py traitsui/ui_editors/array_view_editor.py traitsui/ui_editors/data_frame_editor.py traitsui/wx/__init__.py traitsui/wx/animated_gif_editor.py traitsui/wx/array_editor.py traitsui/wx/array_view_editor.py traitsui/wx/boolean_editor.py traitsui/wx/button_editor.py traitsui/wx/check_list_editor.py traitsui/wx/code_editor.py traitsui/wx/color_editor.py traitsui/wx/color_trait.py traitsui/wx/compound_editor.py traitsui/wx/constants.py traitsui/wx/csv_list_editor.py traitsui/wx/custom_editor.py traitsui/wx/data_frame_editor.py traitsui/wx/date_editor.py traitsui/wx/directory_editor.py traitsui/wx/dnd_editor.py traitsui/wx/drop_editor.py traitsui/wx/editor.py traitsui/wx/editor_factory.py traitsui/wx/enum_editor.py traitsui/wx/file_editor.py traitsui/wx/font_editor.py traitsui/wx/font_trait.py traitsui/wx/helper.py traitsui/wx/history_control.py traitsui/wx/history_editor.py traitsui/wx/html_editor.py traitsui/wx/image_control.py traitsui/wx/image_editor.py traitsui/wx/image_enum_editor.py traitsui/wx/image_slice.py traitsui/wx/instance_editor.py traitsui/wx/key_binding_editor.py traitsui/wx/key_event_to_name.py traitsui/wx/list_editor.py traitsui/wx/list_str_editor.py traitsui/wx/menu.py traitsui/wx/null_editor.py traitsui/wx/popup_editor.py traitsui/wx/progress_editor.py traitsui/wx/range_editor.py traitsui/wx/rgb_color_editor.py traitsui/wx/rgb_color_trait.py traitsui/wx/scrubber_editor.py traitsui/wx/search_editor.py traitsui/wx/set_editor.py traitsui/wx/shell_editor.py traitsui/wx/table_editor.py traitsui/wx/table_model.py traitsui/wx/tabular_editor.py traitsui/wx/text_editor.py traitsui/wx/time_editor.py traitsui/wx/title_editor.py traitsui/wx/toolkit.py traitsui/wx/tree_editor.py traitsui/wx/tuple_editor.py traitsui/wx/ui_base.py traitsui/wx/ui_editor.py traitsui/wx/ui_live.py traitsui/wx/ui_modal.py traitsui/wx/ui_panel.py traitsui/wx/ui_window.py traitsui/wx/ui_wizard.py traitsui/wx/value_editor.py traitsui/wx/view_application.py traitsui/wx/extra/__init__.py traitsui/wx/extra/bounds_editor.py traitsui/wx/extra/led_editor.py traitsui/wx/extra/windows/__init__.py traitsui/wx/extra/windows/flash_editor.py traitsui/wx/extra/windows/ie_html_editor.py traitsui/wx/images/cb_hover_off.png traitsui/wx/images/cb_hover_on.png traitsui/wx/images/cb_off.png traitsui/wx/images/cb_on.png traitsui/wx/images/file.png traitsui/wx/images/frame.ico traitsui/wx/images/group.png traitsui/wx/images/hs_color_map.png traitsui/wx/images/item.png traitsui/wx/images/list_editor.png traitsui/wx/images/nb_open.png traitsui/wx/images/object.png traitsui/wx/images/open.png traitsui/wx/images/table_add.png traitsui/wx/images/table_colors.png traitsui/wx/images/table_delete.png traitsui/wx/images/table_delete_synthetic.png traitsui/wx/images/table_display.png traitsui/wx/images/table_move_down.png traitsui/wx/images/table_move_up.png traitsui/wx/images/table_no_sort.png traitsui/wx/images/table_prefs.png traitsui/wx/images/table_search.png traitsui/wx/images/table_synthetic.png traitsui/wx/images/table_undelete.png traitsui/wx/tests/__init__.py traitsui/wx/tests/test_color_trait.py traitsui/wx/tests/test_font_trait.py././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768027.0 traitsui-8.0.0/traitsui.egg-info/dependency_links.txt0000644000175100001730000000000100000000000023571 0ustar00runnerdocker00000000000000 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768027.0 traitsui-8.0.0/traitsui.egg-info/entry_points.txt0000644000175100001730000000026400000000000023023 0ustar00runnerdocker00000000000000[etsdemo_data] demo = traitsui.extras._demo_info:info [traitsui.toolkits] null = traitsui.null:toolkit qt = traitsui.qt:toolkit qt4 = traitsui.qt:toolkit wx = traitsui.wx:toolkit ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768027.0 traitsui-8.0.0/traitsui.egg-info/requires.txt0000644000175100001730000000056200000000000022126 0ustar00runnerdocker00000000000000traits>=6.2 pyface>=8.0 [:python_version < "3.8"] importlib-metadata>=3.6 [docs] enthought-sphinx-theme sphinx sphinx-copybutton configobj [editors] numpy pandas [examples] apptools h5py numpy pandas pillow tables [pyqt5] pyqt5 pygments [pyqt6] pyqt6 pygments [pyside2] pyside2 pygments [pyside6] pyside6 pygments [test] packaging numpy [wx] wxPython>=4 numpy ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768027.0 traitsui-8.0.0/traitsui.egg-info/top_level.txt0000644000175100001730000000001100000000000022245 0ustar00runnerdocker00000000000000traitsui

    b-(Q_5XbCm ~7p!F&a[lP`⧁U7"3NN:d1O ]Ԡ;xT!p}D"L$8hSS%7|A,`߉rQRBɕVX l9fs?Ta%%a)nAJ> ?CP],<18rQDYVƐ',hU(ki** U _@n64K(l* B<묵[-MŶ*,< .閻5;! ," H ?6XÂyZXp ?3(-Z@&A " y-5>╥ -2nhгԂzL1#A7LsiDjbpok$2y%+ JH72 Sbۂx:6iS;!˲0aCo!KڑGwىě#1F٠鱋KGq܂3dƞ(;cu y4/3n+ٰE#:5 ^AD 0;`_ X1OdA LBCDN֠hcMN8\0?09œs9hp?>OHIɓ`N011_\] yPBI(rG쒘'yB l 7T!T\#B2Mm:GiB&Dcv iy«8ȂB>t*jkA bר C<{- ! ,# H`?4O;O6ʠb4n]EO)BL76kĕ&\bgŲ F{dž'ѩ{,F=2>[Ϟɳd) {je5O%yZq0YAe_,,p3Zv 1bXSN}X:.t ؊^OZyƓɚ!1{K8/bG{uֻ1co zaMG<*<EWR n8Xs+֜Qt(tZdRb`^VXa@B|0yA=fTB0@@:l<NhNgDYqB8ml<]`#:H'B PRϐCHbhPLp#`$CJ2U E(3 " D,UvY,_rMks e&BXmz矌F)_@jhL .t Z5*(h)J]¥C˞aI8o; Yg8JcƜ8!C" T"QL"eԓ6a_D  4WaCD0MASD"%P_ctQÌvq/O|l(n%iSX!F[9HPw7AC1="x"H,NmdApKQKcF'QH2/bH A$ # b;䑇?.Sta?`C86Sp.$3h`83?xݡ D'tJPFs )qyB,,(C`J\;_4↧@s&KP;(*B:kB@*vz+H\+:l;< :>$! ,& *ǏbT‹=FN.QafEg7-,-t] >VŃ2Ӄ(K\xcbś9Yp{Hc@..-3iYT P/!)a 2bϩM栵z`x19R>R%[Z-Ybgv|//PlɇxQ)73fPSJ`cJbiJ;" Q/V(2 <Xx#'dhP rH㶋]Iu̘262C8|C5 yӏ(,  "}pć?Na]KDAdK3+U7.,AS(ePQDaB#6]C>"q?a3 PO=5#0C.r3Ï]td864r^TiB9TQcs2yNyb7DLp!pvA"($ȰCу><-|Q`, Ѧ Z\٠OG2&7-&km|󆎘eA.XnkܰtZjf!CQitBO2@&5Ldl4fǭB pKSIIR`RB$I*rEɥZ+$a :zX BA(Pv|]p:^iG^d 1xL\P]#?Gdl p 9D~ BdAA?ErN?Th'2P?B8ؐqL%8 "\p9$xߠ!$$]aC=Y  |!Zkn|3/ 9E c V`6k=@̐b xF$}hǡsq7pActa h*l^t)C1A;P,A5v )D| *d]y2,&lLÌ'r,(ʰVK p0sHZC|q!P6mȒ 3LmdC B68LKqC1s]3r! ,!( 8\Ȱ! 60РPË -6b(xC܃GlƔ͓)w@k) Deaτ Gq6je dJS%#FKmjF9nij"ɯj2CG8F,I@L:lŕ;W'7mIr/Q.85DCw|88t1*1?}3@՘?(:S- eBI5V 01ΎDd7f7tU(/` ZLdvQM(@P@29&1"KD|A>R@,G;)5R >_Gă,q@6*Lix4n6B ̴ v +K#(14+, 44p0 ;,)Ғ,̴nKD#_! ٜnf 30c G60sVo#BK3 fl ]s<5ƾ (r! ,#) Hp>\Ȱ!?P2AË bJTT*b"GS hӦ'$j g”͔+wXq 3fIk'L5UJm:yhK&5Lc^lT[šFʪhdZZ^=ĖFBk!ډWMcAh{€qGX['tfݛz%)iA]ZvׄzFib; L]C@n.5jLa8g8<5Li.а=pm%_^/*pVC99f3}>|B7:E  ;E`v [0-AaՀ ,rXIsA)G5"$x#&bLI$S\#`2 YȂB# (tC J !Ebs+)(:Kon2M+n¬‘ \k+~!!˃,D4 <4̵ѡf-30bédEs p4ԛƗJ6hT!"vJ@ÃZ\ȑ ِReLt@7q"@v9g]z \ēC |xlj͈biN(APQaJ((@ң 60,C]3,:4aH< u6iAHJ*'dӊ [5Ӻj?\!B[ G#\bH+r+˻!l  xM6_42pL]ll oOc3p o a{ͳD 2f"7ehCK0@Bd#GP;?0ɐWY;@}DtpPMqtY@5tiL v} `i9ui'(pL(פĩD4҈ıA2̴R@5(0Bj(\E K,yª)2 Ҫ@-E~>L3 *4r Yl#42*p ٫l6d %e ̮|՚ k3lLTArp RBZqD{2]2{7B6D 2K77 ! ,") H`? *\O;3Ga/xNj?ё5 9HP;`,$15E98CkL$UTp-uǗ  L D4A3EQ|oVG{SpA~7'| ɀ j)c*C*h6؎<&?. kLӊ DZ^H#>KD6H^.(4!2-).M̼o+pLZ{ro + Bɢ /nL\ZN-+ g5TP˖z2/,_P@! , ' H`?|lcC=W/O3F\Sh+AO:8O  N#xu"@[/_?7-DF t|RhHm;0$O\yL4TUaF(WvjϩNEȬ[zb!7,٨Plk~p_)si'?<[b&Mk}Ǒw(ȕfdXs ^Lov!Z7l.TjXIDk{$j#Z~Y8Dat'ֿf&c;|xP2QkZY.i;hpX Ptא' '99dN /cCPS5ta ~ =tbUpP#@PE}(JN98|AIC_beB ?x9,&55ag#K9$,aMBIi{Nd;n҈>nوz)(4r b,nqd2sH+`l+uf# pȴӴƪpd{H^klllJ;.׶rHD@<4 n24di4 3ِrM&AC#M#C1W6ÞƂA&! ,& H\h0Aa?鈷=qBqCc xL0e*Am<C.n9QnŔY<;sCuF!UL;?yX-U&bҼIrXЪz)'lSU:zb֟cFO<?>ۼV ;Jj2PXSj.X=~g'lWc;q+ ӸX>+ =+L}onzm x:-{sDyU\%V :`\Y,-}|rB EcTq |RpEE0 5C @Pi1gP;<" Rb(6l6'qE%i]dHP;~Ix.gt)K6P;@#@F &%@ 03KR͝j* v2s *Pj竲LZ+3LcfH0#!J+$_j6BK4" S+.X{!l0뮭0 IB=pR56(YrOLP@! ,% HyamßQPx,v& _z#CɮoD5ʘ1b2a”Pm_<@LP[H5Ѭk{3[ b!<0SkD@ \)(|\#d#By! , $ Hߟ"AÇJ(XEbQH؆J9~]С'MjLLp2EC*Ʉ hDΞ@ hyKSQI29ROS5UzVvz1D['7x@̳v <&ڠ!I-Oz 714vzyN8vejfJ<k9r,1_9.ڄ٘uHv%*POIN|t0|LxiWɽ5tkk{МϨ ?U?:Sŀ\nq3@ 17Ap HP;px':D8)_TGy$@ĉYnB/჌hcD4Ȏ<N"dCd3AK6dGJ (4G6NB)PSpb6B^#%jAd(dr|~nMf̢4Њ,R$LcH*B(Hdfhj+:*!,'f^yi6p8 xsFP@! , $c͍ ڵð#F'NAoĨ(ѡ „ !h/O)S4ԨPnKe˕+YjuGv@,\pA$Xn8:8-@8ĉQBIqzG/N@,xT@/ƈL됐%)$8z۠#Dp?od#I8_ ~C IDڠ)餪agKH ~#&z6i`ybfL,rA' yL4l6"'DDM2sȷLcH+*&`4z-n!ކʼP,![o{z5Cf4kope  ςc@! ,& gC?*\poK4\2…_"QSF< IR %‰lcrar4̩SM&Lw# IR)ӝNoA5aTzdN5j)@&c&ƏB3F*K %lEO) P ڐ'#kVێsk=Nݶ =6ڠd쁱M.k=Rjnqa,k.bpN~ Eh~^{G6}cCGJvmgjXs%4[d ؆7]tT)768x2)u D:4G)Cݱ.( pBX@-NQ6ZCaJ,l C2p\QFqcP_09D ?NUH6xi6`ǔD1hh 9dF:]`e#i7s[ͫ¶訯qfՎ,M\H1E̐!+* WSw#!Ez1 j@S@53!rS̹-4 +bsNj2A=Sz|J9U ر&LM`1s7Ćp ޽|Qm@cWt3Ǎb%ԘwZdFG‹7[5yM%ж\;4Lm6dd?ۂߑB%X7]-{CMg:5Y{l@iwwC`SPlG+,GBdqH~iS[/r:ȏj8{`x% =! Y:q_ 3fp.G;<}fGmcD4҈:]hCij22-dMHB禜:Нa'Zf+E>#٠0PSJ2ЧQ?dJ0pÍeQ$w!2 3>S l<amN!{f<44d 045Pm7o+P@h1Ũ 5(! ,") Hߟ*\XP]0Ȱ]\r򁐢v(oʌ":zTGIy4LѠb%G)yLY(.cDQq83@'[cQ1$T EVplYx[aJCf-nidHCX\'tA \|0_~1.TtPb<AȦR'HD+vfdJ2j KGsj -ljm; ÎQ6,F6G Yg[:ng;\n0g^,1QzG]#]5%A?Q4vKr t6Є5" \Bў;@CM(ˉp!x0s5&@pX~?+|#>@_iCG1a 7\cHfY=XҎ_H#cܣSȢgh Ý" G2QpO֜P,Ƒ{fM;yF PDn NZ %}w fl1G)Cn$i6DD=(Pn3`KJN щ0 pn l :J&e &< 3pLn;rD;EC#2!hqS@Bs1ǭв 0*e2arHpAn0A6@` +ƶ !,$* H  *\Pa; EyJذbÇ1p呱 ŏ/)wpH+P tA7t) 2!H,)[,E ({έ> T;././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/examples/demo/Extras/images/logo_64x64.gif0000644000175100001730000016002700000000000025616 0ustar00runnerdocker00000000000000GIF89a@@Qj%[eil_"q0\X|ӏ;yvx@񶺾쇾IҐ@[&}>cwXt?Ͱxy-ˑvmH^|QtuwiMz0u=\52'[ɆtiiK*c~m?Wu$yp:n4͢LzUوa0R-XnU[kM{=tY~ɵ݇+ämÇ5=y<$u]Gͤckv}MpHj~Qnz|Yv!h/j1HTk}mor?}0yWr_;izx/Tޠjm2ֆ\z䕖N«ʑ1Oh{*%rDw·쑬dϘH˗jۆ3d|L2^u϶MvfTTG]By#UmjlnZ޸zIؘ_{z.vnG#`};3(cȩjo`xCp! NETSCAPE2.0! ,@@ H*\ȰÇ#JHŋ 5qa#2tرFA`.IQ:/PhIߕx qϞV )v))ӠPna ?IjYrm.h%+e_Xv궞_gKПhQ߿v7&HPM{egW=Mz[S ʀb-F۸kA]\#j# vpZҟ[;wCZ&$n6ǑD(o=;TDe }$'&8ӟ TʅDcDx#$%Z ry9s $@ ~3W)(@wyQ$p P#`v86dYd:c۵`} #KJb:`12.(-4葘tD $iye(i :]BGzbd ~3 B)e:jL5J("~1?pK+ 'pZBN'*򙇤%Ưt$Gښ&ɞ,P^<#TQu&-~8[֎zhG(2Xz+N ڑr[ suPſ{ښ0!̮$QjA`P#Bd*~QKFzrAA4ҁ,WL)FIo-؁BcJGH5˖B[Tt]QW'.;tE&OipZ+EV7w%mjk8ѓ~T%%jMKL-i?9`LY*}$d7`vFN|Y74w/T3Y8Hld-zӂ/gUI0OD2;†g-=xu[Nqw#l`Pn bR]=Џ. 3A / 'v %qЄȂQppC0=0DhN`g o}6|& 9 kbY:GL1$ iqd7T;I<16q $Zdbc>AL"y! , /8 HA5:ȰÆ Yŋy`1<Q/xaKgC3%(4B|XfM(@7sA: eC 3JҞPL>ݰcÆ.,D&S^X[om bE׵x.^a֦yw]חm?<,5(^٫ǗώA `H'9E̵L7Z1ۡ^fW [S "7/p&i$So?yP=|G]\C^<&Qb#c^vBq*5 gd_wQD!*$&4҈zfڇp y>Xbw?N.at. OAC:bbX0܊K"ӆX< ~cϒ5d;й[DKIf:&=z#V#DBEÓ9gV2IzCb8^&#-Ye&!D~oꇠ0$z*@氵QAlzR*@*L2K7."Yr?U`c¹VIgRK4[ NYd@+˹(2?X`o*/o)ɊTAO7ԃ1o5.k3=@-Ht8Օ|Sɵ(H(3;Dw(7tsx@"E8@-;7H]7~=3YB%Z9 AYdz߇M"SxTyBl| lvRm9#l"|iϻm'#BihR#I*RKFe' ϱ~R#@L2*HrixEhaJoͫ "`B[q-UbP#. \c"$ qzCNGD-3e,zFϵL3B^;lNPt(BBLfuI]1k ]KŰk#DH{{C:FYK၎x޵=7>SxԠ˿8AeU(,23ގ<l<8qW酃c?3nb tC0kKHÇkiH7_ YL HN=UbP&;̓kR o`_?.uA 6MdB x4# !6dy! asMb Ģ2ⅱ! , -6 HA wÇyAF3jl$Ȓ8QIlq /cx;yR"E.sB M-S'(L؉ѪVn`S0@YU+h7#c75bݺmhю1ԹZnȻc׮z#!C ΫY+N Ãc0a}qCDfb˖WĘ>]z]C pfp@A%̹EEb^r_ߓ \P(n{'1kE}84+[73vO]q}&$;M d^(. ӀwpСT?KGaBN*Ό&PsItc,6 &8#;r "uI2IN2Θ$$ F.(K tp$;&$e $PG+39B~QBBr&Yg: "ٹ< 'ȁZZDžuŽ$!)B 栨py( S $*(r*TCbZ:2 Э~(򌤍Xy*|ҨC&H rIw^F@G-Y0H xRL5rE%I{?tC:(0H-p{?> KqFc~u28l+b.GD .'Q#i.sC@I/E4ÔsC=oRku3aU#HBOlA#w]VGP(lA 2|}W}'I<"9.h|/Utr-H( #n<3W`7?~#/<FwhX ?3zKbi!EU pD}-$.',r.` ~pI (1 x&63l.WCw! |t|T%. `0A `@A:IxA%@?[ˆA* ~R! , +6 H\Ȱ!FUСņYBWC>h ƌ_ƟHKƒ %x@٫ o2ĉs(:O=ЛP> jϋ:3+קŠ"V޴NݰV; „ʃ-ܷs׫KJo;Kwcz& AA9ʲuWf̒w!qYPP3KO>=@-Ou7v?fP݁Xy 7—v8wE.Q/<_f.~dӂI &TwB.xSH޴ `$RqEE.%h;3z@h @`CΑ>I$Y@)$3ʕWH#KG-Ո InjU% 1,~.ZK! I 4?ȥ7B M|x3ò o/kP A Vf! , *4 HkC:x !J fʘQoWT%/-<(D1+ E'@П gS?a*0bLj \B0ԠoݔNW(u 7. T z6fҤrwa0`Â.<HFVeTxDhM Me&S啨ʑ m͙Da6qk'* $F H0d"ԩbxΞ}&':Q8 E u@7(CXr8%Ġ:B٧ zw"Ԕ?IL(T-BjzbL4hlNw`BQP#~@\ Bn+CyyíwTc8AX+#9P# 8XP#i낃t`upcYl}@rI.قmW?X[Yd |cGތF7Ai-[n?\qs Z=aVWxݒ xea7\]%~llu\/U @lswuRTUoL&OX{S0L. FM72,z2'{Ŏ܈-B5G@! ,%1 HA*<ȰF#1N4B9K3M.5y vhqcbdd ېna\yUYC`Z]_}'`.XJ1/3_G8TX`a(8|Fb y54Cꕰz/ܹ!o  _/Co! , "/ HF*\(S xHHQ3j%Ol"oP&On K4hٝRP C+P_}bϳv‚vQ]P=nޯ}mt@<_p7߶q\]޼O+D87M<0mD$؍,Fh AtHttH!&H!"Ra^΅M+#&Hc)816 *B22Id6 T()eC&`PWVI&aVʓ>Qpysyǝxƙ@ r "M4Ɯ`L8020]$DiuZZC'@:j4b*{*u k"k݊kZ'? kOy>ݴI77AI@G -҂pb-1px`UmE|Od`BTE5PB5ܰx'B,]h2VgB8S,7V75G83:,? odPPh2! , %0 APO5ZȰ#Bl1ЉAmXWx, b† %klbB2i&58ŨƇt>!Cļ|憙Pe*_X̘?auiճTNE0@ 1wXPΜZ*`^mK0avʌC{ZD _|ӌXزfZV([@7JYȑsYrvAoHP\SA8TBuPI]te^ G!C=.ںN|t1DM~3O|6@DD"(P#mҠ%RRBq3 /]06-pvtF4WD@ 9)/U&4OAMxKP6 3^y;`JdR$;Yf G}o*c‰Jr)fOi;T+t&@sJhp)&@!h:Pj)^:rw bJy( qꫮN*(tbBD" 0` P D*CDODS\@xZ;RDBׁKirE4A$ܞe73p1[>-+B& RgfZ 0̵C^d$aMߵPYyY0@@<8lQj?N%hL*pV38[ g\A2UX3PM)>U%sSWbq)"{g]@! ,'2 F *\ȰFU  9_6L$ nU,)#G@93(@d`+[&(X=Hmބӝ`%FHjEZ3dS(x o(0Gƭ ױd -.7vrK0u/aMrZDkַ]ՋFB W.pѠ-5aŒ\/n@6[Duy R e6l#op}\gw`~ޏy05}c<^=tyD.4gF  w- 29m\KV)&^d&#&4ф" Hʈ>D9RA6@( /Ief*:NYp}Rf gH%NG5 袨䙧}N)a>)Es:tꩢ2B,|`駝';=Ȑ'!2 (,֊hs!A@_A8P ֡+m<[h.+a|ҁCLK(z\t @» i[B 'H.1Q¹7t0sD(~2G߸k'{4p=tpP!CtaDv2s7+ /]T[39Bls3IHtVa;QC8R \u5^0Ni7Wjwd/ȵeKZM(jH'?GQ4ēԙE=;vWU;Z- x F> vCNfϊ\[[n!;L5+D8-ڬpS C&|6  G:B ݵؼv$nc؁r(A$芬}{w4JPU& '?m%H1: Fp&H?@-N ŸDA WO};ЁC4M s.t;F޼`#k-?IN0?v6{zb#B(|H\T^v#"ؐ? PQGQIy*Xh!t`1 v( b (dā t/c =Z5:x 1_ArL $H* &Cx!\RD$d6Fj2U'\A<|Bfȃm Z)E7=iMvjơ^"Td3Ԉ=(kn!2Ԉ iQ J)± 3Ij7. .RZj<*ȸю;'搈A=Kĸ2AϚ2hXښA%+... ъ9pHǾ vR]#T,1ipZЀ BIHL3C\sF6c'mmH00sv߰Z4wUrC6$FAT=70X}f( *mG{hmK7̡MGvG0 w` 'qbBtqs'H]K9y垍 xNҳTۀky䨫vMǍ,Fwݶ_ϢQ~+h ǣ5v 7__#[bTb,r~CI0! , .7 H`F:42ȰÇ 3&B3 ey]H`#oH:ذC#Kfl$̎zwtٰa҇E1e>īQ9yBQEЇxkM7@ӆ eћY.$װfmV(xİdUs]x@_D&DP/È#pG\j F@$"Mi9gʼpjֱ7/Mȹh@Q.\|[VJ5oFkP^L&s?ϡOh5 CcN :xzIzuFi/4҈?ݸ+u zE A@Tvq'|ԑ@Ĉ QN~A0cNJm:A,"Ë2؟ @`#ċ/7R#Dc/I-roZMbg!dd2I#MK(c/pYO+Px6X(NC 0ahGճAĞd.#&`=vh$7Ł5|JG &+t'f%JX!+r0P(itcC>-rbK.iKI˜K$CD(l(+#r6.R\H*6! W Pw`ʟ݄?PƎpMd(r"u7g`8țWwNe1@;\?hԳ(KiXXK6<@R=-6PB`=zAܔa7CwsS x|v W V.]S=m9e@6A/ppwM3v#ĠJ;;yѺ;@ K^rH"g :I^w(=ATQC`O- ozoȀ>% ! , .7 HA\ȰFT簢Ł] 3KCxM;6L(FQ+AE5mnؐNd4P,66[4>eA*C.Ŋr(`tjjn|MyZ,((`|P]lk2loc֚^˰7"L?AIpIh><ȉc؉-7EInPgicd&EOpzb{w/ǙK@:*tz?cPc.F (X^.PK9Ew "~*\QT݀*6iDA!ᄹ?bU ly D(!A6E#}x @0 !Ā;'T kd TfIei/Î)#9Ui;@]WxA%ɟn&PBU@pY_BC{v;ghTxqK(PCM:OJ췟+RfvRjCF7I^tj] 7DKXk 7A(PD<;ܳz&*(V bأ-pM*r?뎴oja FG UR#äǏKVS;T2jeK6.gMJ;3%ͭi@Nl͌B}P,Fu̿@q IP#8P;P5r mPQY0XM.c6( wsu/7Bp˽/8HWT7MK85O"1 kU7M)Hx|1X}S=)-z|Ե<{5CQC>"ÓX; #! , -6 H"MPrځٜ3Z Q$ Tw.:tԶm;(; Y]M4ϓ[13*M.N.g-wbIUdkyE%FGO=^u_bIr̙McBu.܈p 3#b.ܴQhA# .U60 7w"="0,ex=f \D0/|px 3DE.=yǣp'^W&((X@( }\s>&0T .iY_b&yHdui (q+5Kj%(3G BBk*g\;y qo~XRij  a #.d92 ݇ d`'::3K( gd\- 3qL-i|AC@-4`?!$;D0Ł!D_ch75}CnJԈ%xlPf'0],R]-UpCjkP`5/ݤnw#427uCԆ?D5л8[ԽC aO.dFCl}"~Huo0H y^_P{AT (+9 7! , )4 H5BȰ!B СE53Fގ1.^1]۱cä+G:l,®+Yn@R\ʲ${168JT1膫0zbd:N&&"Rj ;T2iA Zplk°7s Ԃ4$X\׮ l0aЕ]6v `#"iB* z mvxѺ7W$ZTI8F䂍>}nO/\{ ݍ1$G ' O Q1:0Nʫx0}Wc~ZB}F"M0uIKK!u9o~8HQI6XKy"B qKPlp3Q4 T H_EKHULg"Y#x=f . ÌbgUڝ9Cg%m 8fD0.XU"P&=rצ*h= /xI_^)Uxzpk X 7,(.. ΁&n$ĀD0At@@"DI 9+AzrmA@N wٹ.b0Cgok5n~L0N b6LqQ|?xB10s ,,'1@QOi$!B6D;@O !-S#m> .R=R#Z#D%;ܐuqM",ͧ <͐-Iqz#4%59u^tF܋G^g9r]u ^":I?@ty@! , (3 H!\Ȱ`2[Ë1L̘3]&uɠIOv[cÎ1<]|ب=!Qy6)8dxЁ*7TsR0-F+vLi+ܰÂ$H UX .,N bГs _i_n2^:+K'hP"IAUlNޜI܊'+1l?)xJg0gFEWl{5o*UOu $lxŶh^zje2|&<' rz0]s?J U }Q`ZVDJ(.$!PL^ Ѝwfb%@F~ _"X.@BN -K%o'\\ e-`C</[Z{u㓘,WfXa)6v?AH.Pp曣 KLI40fb%(5(J%BdVr;L2 0d"S7g#YpA柩 .]!,M}6YI5 .W0CQ.L@-`EG $EP8 5bOvI 5#1ƶL2$.1;2ƈ }3.Ќq;@1 D3C '4@F2EXg>H==xp5 WrSIS]g3kOm3pǽW:u; H.-! , $1 H5BȐa#x[ʏ`*T1t2iGE6|XW;rnm 0ꈠBDu^ V'Lm!FDt%4H`5V^656 ګر%F<ؼ:N") Pn%lڗkM0 g(ps4MQK ,[i3YTI&.0#c&`A IfP$CIRgia@Hi! (4@*9$ L`-y`n.EAt&AG0/JvP In5E$`9t'57xX0&)~%ܐԈwU#ߜgF^! , -+ HF *\Ȱ@OI谢ENqeDClNT\areHTӑ$;v&xCAjDӄ wA|ZLJPF-T?dwժ˪%QpsaAl@zaW=AgW= )d1kͭFS<0؁$-Jچڀ-z`Tν|֨ C*W+TU4.;6I2?SF3_koX5j-jI`:(w"&W,ݬbI$ z A oMА.._B12qAArP` gp,GBAU#xJTFhecc "eŘPO.r\F# /@d IKzL @ɣHl3z2 ~QhiB@tFjh)xew5D*z11f%)2q;+z(HF"jtAq2Z+(B|ʼBU#Hx,H#d q[/1 9AT xk#<*[/ . 3 R+L E: 7/xP(-󢜆wl䍏+I5Ag@B *xj& :jr\5 ڭ'مNAh {c lInMkMpwahQ:6ȔOK[DEKUjQl0c7nt]7bF0͢O,F8WS rkzÇ0CS2u7MD< o.TDbEeZt#VD8cDiI)D4AT&!<cK-2tMrc03.A;TpD6L3-ލP@%D1AuC c2e /XS +c 1DL0AO /騣{p.b&<3g8 Eg1\ ~!"4] (R NA$}GIxJS0'| {)[1 $ BTHR-$e;,PC.F(D$-0zix po%UKKLB / /D#C-I¿$lDB¢@"L, 15Ú?F D0 ]|9¤XhtM5bW .|0  O,}zI P 5 ro׍dp"B\Lq35C{7EF7ohN'Dd͌;b%PXoJݔe}79U/Wbsf==B8I! , .) H`2(Ԉo/+o7! , -+ Hy:ȰC<Ë5L"5H`E|H*WFq\,Ydg(̙ 7FpGʖ)eg TyԥU&!gF(͹sV=l,)tYG[KTnfL$D;")tKӓ +`#g]55k ͚N iF%6\u?c&v:LTPmr<{E֜TԨZu^03LSӜY׆D-|lĂ*t<1t3t PDWpC=AՈ🀇ٳя&Dx"HR "$l K iD7Q< ۨ";4E75C+E1<9G ,lH!σ-6f`P BUk졎*`(D!- 0$P8 )lK=X@G"kp 0@tHOn;`y.qb7|N=̣;<;rC=`eƩAsA7ܰC]peFZO":5 99BLEA/W)n$6"? Tp=(xq=Dp&@-n dP0ۀf$cl+˭&K :K Qf#)S&LQYMudcJГi)1ԜT͙A \YԶAqװ0oj4xNy lD$n#s֑ 7gd^Ee'ʗ h7^0{ P"0S$ZO4E('L`#JFP@xc? UT0Xq`4-"Wt/A0äB 1D>N> !O;tQ-ܰ.Fy"$>E8#gCMY/cϊm@9hP2=C`i050` s%H'hp!7`*xR#RP %lstv!S#h({r0_Pl`/PKt1F0 12 L"xo;\<1dKB|k//cȃ`0 P (mA. i҂<  !t 0 rgt72bd5-t%dP27~  e'dbtyDlop\|.:lkӌKyw Ihwυ.5ONAk3y.InDk'1櫷7뵫88 .n'i/uZiW͜8&=+oQqʼn-. ٲ傂"y4SDO5\pvO`Wr&=>^C{Y>:eߵoHJ>ݞK9#uI:{$xϞȨ3l}dvO,T2e 1\!ƚs T@ 6xA72/u3ɥ=p %(J%z܂ 6,75ұ$P\ EPrͯYF]j5 *z霽"Aۜ÷] O,xKz)ۅ<]`j)VWWlp H /̃ %`{@<}C%Wq,` !*-'157sw>6:`ТNݹ+*=f t6(p*h-"5P#UD!Eh1?kPЗ.DpK;k܅Æ6='Lbxq S0ߍ y2$7E!G!5l-]#99B" d@7?\=iO Ple \2Ђv2>;b@| ']tq=S ec:k(C?QPx x ȣ=$г?S!h77晻ޜ'A0>9.azwBB_>@@>'λ'7H! , '2 S*\Ȱ?[6.j\;uK8 xƆzNwdx%cM" ޾ZdKP.7z;F *`U)X[ E+1*lF+W"nKO#XأAO- T`#r4]+B qȌPjAv[zH:h-XDŽqԊjȑ["<:̉\SlN)XF3F |ajՊ͇JӦe>ULٳ5? c|txt) EZ>Z`H"9," !4b p﮻ T 9P4"x +˩1Pr!0>H|3hM a`0ɦn$g-^s =̑d4S1m OgMzpQ /lWA lb(BC&PK-ʹ "ma"x&i_  ,ޢ n#hF>#{zB(OD(ҹ9G(Yd" u".D^;. #CPLϺѻ@p=.0.@x@A~O'ILpA7O|續($ ! , *4 HF*\Ȱ!Fm`„3jP#7l u>&.2Juҡ?.쨸rcwz4{:Cؓ=*ءB5WN(j e}3eFL54P*Y%OY{zVT ECDc*P k^bEf B [V`apSXs"DaΜ>3ElWjTB΋0RnV,.FhF?|3:pģ {Ebr؜!npO<7'`[,L`W"0dOGGB@\ц,(!}H0M=4v؄!xF KTP 4t3 -(1wAͻq o<NdHc=ڻtE6_<3bkt /t ~&,xU C?iù.( ljM(IlgdI(D~o@p<8# " l prG-Dj}~(K/$A~R#CH"6~E-?!HC-=!P`  @<ȐMp !@HBR†t p` ?o a^bD’! ,,5 Hp`*\Ȱ`#d$AذŁD$H`!".,60gJ"ő]b:ta2$̅3(JW*XS˟ɢcjQ\sݻQJET]hw(%.E쁼UҤi•a 2}V֧FmnDg:TMB0(FfĜ=u (ҁ3zHvUG&}2+g͔ `뺳yjHDR9K{\.Pϗμ}EXdwc9"1df1 n?hSXFw@p9=Ȅ>Wh(P#?LBrX_̘[hsE| u;PX zbC]1,rYPl`HK2t Y-5lP \TxYetQ n% P'UBg=Ll)hCĀPЉ;XwCt$+)XڥC`Pj;DFDp6 /5꫍t 5i"pk=X 6x3$AN]~:(*zq 1^r)t1ɝېץ+Onz!h 1]6TKm }]p6zD'EDƭ" j+ĹFk^(D(|AC2'dn6]APG< Xc#HCg\go1 Y@3tL}w$zI|1# CэC: BO0.~0.'A!29"72#ׂqJލ|/< 'iJAD-;zsyl\ܮu`1# p@wo1@ EAH,L  D@AfR ! ,-6 HF *\ȰF A谢Ň %@AC*T a0ȋdQaw:zra#hD@V,)4hQL@j[ 8RJҪSI„ lبPLiݻT)`}Kݝ;F36F?v,2l茕몎F2Sn\KxMn X0Od~vJl#CE Sʬ[,zM l`P5Lx7ΓAHv/W "AtWPAo 66iz3Z! a+[OAMհ7,5"$("CM mL^1@'/4 ^  9$Z  :xŹD9Ԉ?<؁ ApO 0b ~]`6擁Q !MebA$RL"yL^Y$'<+3UYQ#Q/qy"#Bl蠙&tءtI`=cAC (蔷c|7@f$~;-ǃaI~$43#kU`%il38?4V 2I@  YS3Bk ȈCD-/PTtv([K-egt I(F ᄓK.~('墈#o utxGDl. t浭H뛼ۍޚw`#'{"D(L?;"읅#K<M6<폸7G-E뻳](z_۟U(?PH`W"s[ JPh|X_7{d.# \B+d @! ,.8 H`F *\ȰFi䰢EJ`DC*()?YTرCE3A@XtGL4ONAZ1aBӓ:t1%Hс(]ʴL tNng)צ@Jm5j#O veۯru,Rz]Lg^WAQFA ᅍYZ8|}51;5'y*c6pζaew*X_I;v̮WE, @wP:Ti lH>۳/^ k.`R$PӎЍX BN^%PSXw`7cK xqKF=tq0K}Ŏ+u?4b %K7`M PDWuPQ@K/8x;lCYq/ԘA^na DVuӖ;CkERЁjg:S\$(E 6B:4X=׵<6 r;ŢD*aƒ9Г|Ev7ފFB@~7dcKrH7U0 Ky=3 H73\qtf25b :E퐍}52N oI]FЁyPW6Whcv_qw+" #Kd +icgF8&0܇&PE.ftE *h)$ВA VW>\"PXatP.]D r}WB0qt uzGM c ^Oy9*z "*$+,A1â>K +@ S#`k T!IҊXuLJw3 Db캀ػ$b&e%OA'ma+y0&,%Db+MblQ9j0 Saeo!g$8C1 y-"F B3Ձ.D5pbI Bi"4Ј8y@D( G{yaѻ,@0,, V {8! b󥯅 t5vx$B!L7dHDJ b+ [ d'! ,.7 Hy:Ȱà E!ŋ-#"A(14(PyLЪC#;T C?Z1AQ x铤*z|J( t$]ڔ`+|J&N:t(eӟ-1Ď:*Εjy]Lc{7j\1D2iÎ b@Ք9 WSPJP?+@:q7(V͉!>J}R\άWOWm{cȥ@X_p9-Qo)c5f6ZT Aqf_.^v+}x yOZ0 "1؇hA O@PYcφW7P;B]af1.s`?%:6u9RT7_ 2_'8C? Li\1"I\~գ$Sc"x&QP0n)?h8 +Xq,zdFy[< Q" "ײyPHCP#xI|JHo#@}N"~3=6~/p./n] >./Ȼ@!P<pY0#?WpxW[<6d! <&? {`FZH xeυr ϗ6"f\|#bČ (ƀ! ,+6 H\Ȱ!F bE|' Fᅊ Ta0CZ1&ʔ`:\ 8@Mdh|ɘ]%ɛ: j $1ԋlP8VґNj1iǎzwN:t j+J@ٰ!X_@!Q*-Ҍ_lXlif!ϋ"_Q-Shty%6z5ѭIO&AV ;vh Ǩѣ5oÀ'o3H0[#*l] Comλ+ŀLuA1 ҥ7f[t׼Wtgk:P%h(:Eo{z4ExE$ltMugHn[ q;ͯ:|QIwU(VAikC@pX*ye]K zLVij…y UC?p"`;b>R9 .RXZqYAxtb GUu Rb#eW1CD$\$ɜqIݗyظCGdD&,hhd=3L> O#DI R SM<*P#X]=- ;3oq"t4L'UXb ..$~TR<+ G>1iD~PRŶcJ(0k#nA0KxN ;qG=僩 JNJ2A5o֫2x@$P5n#;\s-C7Ƹ\ 5D3/lu]rOb"8 iS@.Y,w=y#{I8P~Ԓ#+7jwP^yYd 3 ŷ3o{7<#H 39z l0vLy##/޻#C㉤,1\o߾9p~z8+:A^ߎ;H,~=!D׵@@%"P@Oį0@(raZTdI@! , (3 H5BȰaAwB5)qx!U 2>Ǖ)Z̛G;v&P%HPF]1L̘]|)N&q"A~dSiҤ ;]7IMP@O*.;SfѪ &eAOԵرL~E (lٳgZV;<_7xc LϢ-+^5L`6pD0Iz"6xC(t- ZP#xҁ3+dg(V+@<۠ $J,1MlDB~JL @ibmIk#LJ`*# +/.bf0]P^+*cff@("CK.Pk1i\m-x<$]yq*0;@*/Q(\3K8sA @ dK.15ʊXNv-Y<& d@=@=8#l &7<\K+v-8"9ۄ`AߗcNxy?.z铻:y{ 훨#DhƟN:/lD|C~2=<BڋbOs"M6_dyS7\ f,U! , %1 H&C  eGh Fhq+? K=tb+T #$؏ >@I]%/8& OU OWLh'-a¡q;CcoeƊv5MMm ^7A_#mVJgs=rBP&.mXPMAUz I%TWfDbTnU@x{AP'X7XO#hU^-xP#XwDClf}p`CBh7T t?pC"]tqc'7xYP %G$ǴhPV XDLJ$!"ZK? #bx b.DB .| ~(n(pcJ&o_ Qbn0b,f+md1kj9q Oۯ$:t n,ݹh'.H-8haHI( \K9#C%(@-#E7F5W3MEj/b@.ݪ#l< rK'. /lb&K_3g#G8_I88L+:K:X;v|l/ ^y깇úI ;+&s9k.t{8HKk1'Iԭc3K(D D吩}_#5%i ! , "/5H6:詡Ç h@'c1ëE /(Q2&Btb8嫧/8pLeGU' M9u3PPi=Pncj=?u_ք2ԪjMXźNӵ{'мlݚ)ͺewA<\cקq2nܢEAR^dE⣝n?ߟ=3[n}͚@GFj>ޠ7vE짃۟Ͱ{Slɗwsq.r7]Tof?`'_#՜08 ]dŋ'gas6}h`#vՖ}Eh8V:b#*@`sHNztGj ~Q#9VH@ŗ`v5Lz(E UX"gGAd^@tzbJi o,2JIdꇣ))vZfPi2H"׈=`2 DSk"nڈ.h-{@H-H{l"z+䂨nVn-њ.˭6Djm {Ch\ `@R(w<:#@ϠY820 /<'3. #l&Go9[rYH/<ћd \<#l85MsY?3d}k'5-`/'tC[}HƮ@xL?nc_.Ho! , %0 l`*\ȰCBԠ-C*Wx"HO&tГ_(rҎIg`M;rv^ =5Z+YnذcG|I Ta@g(MBꫪLC*AD PYVRO Ts ߾P6@Jළ%kte2mrʝ  Kjٲ!y M@)"i=ryDТqTi2nȋ&򆍀`3L@ȃbΒq$ں;x)d|J=uaMcVOH 6 KK 6B\Z^hV=?֠EȘK95oU!A ! zȅ{$5%a=mHP V4bPL⥓?WבA?05Z7RYtsPG5" -zCOI>gk1A]tQyG# 0CaIȺe AKI.ӈ=?TL=W3@ʺP$AMNJZs&oH&66.$ o0ƧFŠ(B~\mɑ:;T#Zr0G3K;Ԣ# y N~D7D%)킿!עQy"Q̴A8xF \}aj -; 4o*ӵ$Q1.N@D.`KG|wVuuZMP(4@EqMfb D% PXG3^I g?db5Kܒ0zYd3Y8H+cIg YDo³ڳ!3$KfH_z;#?" /Rݐm=ͯ~_8D7@rNx4 wMc{ >NYMoBH (  „x# ça ! , *4 HF*\pa#K0`Ëv5>`ƓK<@OQ2(x@@tdž]ΌIq%Hɜ:`RhfF bZ=oC ;M6 eСXۺׄaL8%Z ejVp 0bvP%2.4._ k61;ТP%(Հwȏa"9ТuD3=:;FҠ mR0 uXYsب rCqzJ4U7_Bpa^n BApeEay |e|ƁcbuxGi uPdX=-w7Ml~y\X Xfɲ~:0p`u|Fxatꐊ2pՍ4@y:́1%VE n_X=]y#gt1_o`|X 8 ATaB2s{` y#ADLJO:Pi#ۋ5" z I$"TM,榃vN`t.&k+.0+n IGtpA A *BE#q4AEd j({pT7mLߴm>[gH@|⪱͡䌥$PG-rdmԳ2ͬ@-U@5׵ ٳ\C+uI9=Q.PpwvEݲ,=kˡ.DEk(JDݎLגD(|ԯudxg;K(d $d#;"}-~.A Ν:{;; D d D$Q[> }r[ b"<sDHApTSh bpr |ta 1pl ?\A  '6a.^:A|80EHObXOb! , +5 HP`*\Ȱ7? lH 3~qqŏ-r5dCOjJ2Myh\ara3deÆG`HeOALҴYzXږbӊޘZhY}UF٪AY X_8伅VocpLHfGXY :&Ԣww._8J̮5ul((̡hsPЕҨk f Kզz)lt&rt`KrWRîu9x:w?LRUhXE4"C18`==td:wHvbLȐ'\x}H V,BQ#tXyWa5UO*.$-.rH u"Z-,ؐ?>L&$V^/5M ۜu@Bᥓ1*Xg\E&X(7 ~3 dށyp2A(~)}c3?b~ kv `driʐ?~d49Fߠ,PD@Ǐ Y7-Bж l#@B%p8 1p &"?\"U'biBR+7E qTH2 u"$r\Bv\K;W$PFr>A#ȼ.|tSdOp m\u+Qx<7\¶B @$Hj,f{60\1t@-0yG\Zƞ@TLYԂ_q65]P/lyg38s$o˜-.~Y| E|AD|'A"M2tQ#?gyo7&>|AY8ЁpLf7Mx4[# a/ñ<`o^a:PHdCf @%]4Yx<4mH҄[_xtdž.FZC I0ݰk]"?3v[_v";fP:vmV/J٪3+n 7p`8ᘠÅABȫqURllf&Q^]>m E!83l)G%" "Q- 6u"k١B dٜn*&<ŏ/">Fmc Q@0r@́U[u Fxx%Ņe0Ʉn`FBGrws8auȒ|СrџA^1C"y G\XMFhMɖY2^֣952A``_ݨaQ.DP#`:8)BuxL:uJ.JP懶).$%Kx-z"z"k#*j_>ڪB**>Hq$5 9뷊ࢭ-ܧ_y:`AfC 0a8;," wظd5t&.g$DZKŪ}zsG4v$4.\"yplP('2M$H 5drIVAA3+bIVt愂 HL'x Ѥ&p̄vH" 9I@&1L$ ?($<(~eP*#t  tB &EH@;././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/examples/demo/Extras/images/python-logo.png0000644000175100001730000002331200000000000026274 0ustar00runnerdocker00000000000000PNG  IHDR !esBIT|dtEXtSoftwarewww.inkscape.org< IDATxy`Tw}M&3",67**lZZVk]ZZ_E@"@ld_&3}aI2IfL2?ɽs=wCq"I,D! C``D_t ttt0Icc#EQ_k.& O:uJb(! CW`V+k\5^D:uJ/}M3F0[nlnnp\SgrUI AT! B0Q1e^Ӊ8qRFj2. IOOw 5yj5Ft:|>RMM ]\\lѣGgXlg(~:BYYYN GzBniiauvv2{zzX&̴8 d29yEJk4FnySSS] ,0٣9p&-vơC$Νz< ZͬTVVE>m۶)jjjR"j8Nlw}b8xloW\O06 SS\.׈r7v͞=:5 >h-333 !\6Vuvttp^x? )^a‚ٲeK/`oYCц_Dݻ_}A[o?pDg}ॗ^ a7oqgZ)K̴-[LS\\lD^ۍ9rDg8Nػwoܹs"h;d-Dׯ_߽b }hD~r{zz. ݺukXx14]KK _|;?//1<_Rz뭷rT*r;w޾HA(^oW [N}jBRIhrq3ϴ,^FVZ[~}w F?3sfM6 =miii'!JB|3Xr'?I' \!n5k֨Cx=lkXtq4UUUz@k(J4}"nne޼y1?mBR'vJKKENO!PYYizIcU#%rC*:n[ENjd" iRT #SI\f}!i3AKr#ʇJ\%X,`83yU0ýἒHUfSQ,q/D(&HXֈ%H&5qLi ާ$\dbaRSS*Z=6J̙L&K_WW/GQW`N'l_tm,ZommpϟeWcES]]-zVXc;rcc#nkkL&7p*\WWdzZcvzbJsiٳgG5 ={dLoʕ#Bl۶m\ZMݾ}bCIOOƻ 'f˗|>oY,-&RI۱cGy-ZH;LIĝJbrܾh"\.wd2R;.d2۸qcg8h^^cɒ%GM0K.ՔRt"Fn]](ҦyEEy߾}r׋x<t֭w}rDт(cf4iJի6&7LFΝ;+XYY[~}<1D0RԡJsNhtl-))5LpÆ JVK Įx<iL d< E;{vؑpYٳgEĵvZ… c>VI.~W_}5~wH$rfy#s͚5}v@+D0_K;p8f{mz*++kPHE͛7?sFR%H8͛g*-->偘]&9jp ={ HZ  FX,/vPT*8f".{ꩧZݛc\.4 HȰ/X0V'%B w{ÏNz뭽k׮ vzZL&JVVc2AÁI$b@ "#<2r~醬q1 ^O5 dXMdAT 5;;1ZBc"h^NG$G"$, 3~\BBX,ִBg:3^0AoD"qM$ rf`Ο?'K05h1>B &Όɓ?03ϝ;L1ZhWWQ[[+ iӦ'`诿zp/(YFl2p Al`V^:u(49rf# z*R#==ݑD(L!7772+< EX2i\]+AT! B0Q1ձ"Z8)zlbFlsL*e|<g||'}6'0".GZ HjֱF%US-tC8!BCLO XZ\05ǍÙ$m+i]o_m|t{C8.h3rAHRFgqL6czS ps ~ !I!d幰l>ݸL)./)&LVR8>>v |~~ $<&nSs >ї6cIj|R \&,1|yZER$X s\5=W=W`& !HJoe^O3V/u%#I!柗/z18lTIǴd#01@53mZVͧ`0 =':D-*^FW4\׊Qhb(=ZQN`9@Pei1m8;-MLi-~b̷@f% #7B0j:,̫Rݛ l({PS"78V$YmqxG&EonpƖ VۭbI⏟0v$T0O8g$f=(gMX0VӮЎ6gpk*[>#4&%L0o$* QY'D0.RG'EwS MBZ ggz6oZ4EL88<@?!V&!9֬>a3`W;5C/- i,$D0jjFX[9i+:^4bm wlnERr'\35EWG>zs0Kr4wpFy&x5XFv>2i fZhxrn.G3䶆Lfʊ\]p;RpuM v\`^oN ߰|VOο\52@+2ʓ£ɓ"#/&`|6Vud NU3ld %2Z0t RPl=/Q{9L?O&gH)ٹRE4B+5/-$2mqܴXnQ9Y-vz_V[qL+Kg$^DwZZ  )xu_xdIH{â&{f*W}^.3Ě8Y徜6p V Ըfw,T;wXe 푀@ x5tlhd: ̏koáSbRxٽAx(z5 ܤv,JaPj{qd3G.`SnLܸ W!bM\&{ .A쳠ڒ 3@%`|"sz\SK~KN4¸'`3S& [,y`;9>c 2nH`S8n&<%߅i&$+k=@w-޹]0$ S9Sbe!ݕ4mG#dncJHfI`JTs0N@}-WʄR0NyD`eupߩ/ Z3 V9 t8aze~nC#A֯z~ +Ѡ\PfSZF$\0| ;snC'Mb)(j,,"7M}F`aã.owwrf~%dsԐŒHMOq"txm/bXkGF/3ʴ(fӪHV4tQ,[ph{`TJ ` ATu@@:{| < `& o @SQ-5 lKŐ$)| +b @(6r'C04E|YX%QUfìȞ}A%9Cym 3ryHN =7~^TG6w<C1 f7@]= @#}HkHVlu cB0QA *3`{Nة fd(lUE4Э#u-Ǝ~VZG}aymVb$ntdzSҗ!bs{} ֽr!麪i4 szX\(S8((p!k JfKٯe(d'7dMr{IgXg=jf)Js,(JEqϷ1mַ+UxSXh+iR}I-*;NoXLDa8 IkKsG]{J^uH\ pܨBAgG\Ut]&ܹ=w֯m^P6:>ipޯ^C?m,+p?ż~?hv0Op}]ʮVkY+.3Xl~7WɘC,X:H?:xZ?k%cCK.DX|o■osƄ-W/~.UH;UheΒ,ASm*p/?~OuGĉYߜnW9k-=̯j (`ɴ>gkc(`b!cnYN0:~Y,_\LvE 'EϾ,߶Dj9L:]] ٷfWk[Pst&C/_^RXXkZ4\tLbu&+ܖߥ3sb,GVi/m/;z.-=r{_T}&R;Y0ၓg/M:y‚Crۋ)R;fƿ(O{xBLJ~ŞYMi>;z?QZUYd屙6+9or8Y`iXei\[ך(2 >\ 5GZ /rX:;ا5X/m2ܴPUAWv-j`8yڲ!9,[2`eRj Wvb^{rc[fڕ"z|ۢJ߼U0Ep']QPlX+ɱ)̴i5؁|>?i| ۷Qf?!,AW\e߱s#:~ P+6jmuN5v((-8%1B}وNy%9v>xΏE I'Ձ:(XWU %&}nfohjJY*PrS&V J:X+T#W3nY4KZ?-f\n/7ZPj!0 4wqWyt*ҥ3m :-X C*rrٌe92@SzR{}r0 זi ~xNXNoY<+YM̄H3WH>w?O2feIvau( :6(rb;֛)ą6%duRV+_[cF=ro4s#K VdwO"p $04?CB󎖶>mpqn 懐1,ؘP@~-5 Y k+b 䱂I:5Fu$p ` }jÐV+6z}.|W7(ų9OMљNVeIx`U5$X0=j=u,^ھ?s+TG(~|$,Lde4a F#7UW sq7,(3Ϗ6 9%HxUeƅOM,*K~ s-=_9-3,Ai +x[K/^B͈\0M?s c]YtJF@ E&4uls:41oȋqY3]J;V-PIDATd%SdS.L94&಼Frl 4uNy!K W, oM5<3߂tIp,X/)bnZP$_lQ_ZQ0KQpDhRXl fJTr9r"8@Cg?Zq_\zW][|aKXsS'.^ߨ7s">]bވerD<t+^N6>ɇkAyMpwrtwY##/q`;VϏ+}eפFs,/Shԓ V'-ec->zlK7@"xZ^9eo~˃*BbᆅP_I(UsLLVe^qeGAX<{X2ZI raNOlIɻ\|kaJg܏vR ejX;&-'Y!75}fc+d"~bwR,v'壯OIfIblߟgBF?\]6g6_Q(֞ŋҜtO5~ڮ\C~y-J29=I!{oYb_T'~pcDѬg3XTbw(Pb>GFLS'k52/[dTw<-6T&Ea;FCb#wPqYʛpo-op^!m0åS)^ױdԖ?wJ}ktH$*p ,4g!uy_h ̛ !T=c,kLV'Ec{/u7wΧGjhE{,}p]:UxExwrKQkKxy1ЭQn!dЩ~J CjKõWiw\NO:huPJe'7[Q95'.\ڧ13RP#gE=j]cR"gEQE(ε0k.q.v*Y=zz*U-s+ɶ-,ϋh6N /v.0$gpWv|-uy|m1BV:[k  \ib׺p7wү3x,/Su<;>%:yқi.nEhVh~Y[g9()΂L4G&/}L`&{!3"D *D!fZ-vJ' C )IV 7}][ Y # C` AT! B0QA *DlG @IENDB`././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/examples/demo/Extras/index.rst0000644000175100001730000000011100000000000023673 0ustar00runnerdocker00000000000000These demos showcase some extra behaviors that are possible with TraitsUI././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1684768028.079807 traitsui-8.0.0/traitsui/examples/demo/Extras/tests/0000755000175100001730000000000000000000000023203 5ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/examples/demo/Extras/tests/__init__.py0000644000175100001730000000000000000000000025302 0ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/examples/demo/Extras/tests/test_Image_editor_demo.py0000644000175100001730000000201600000000000030207 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ This file provides tests for the demo of the same name located in the directory one level up. """ import os import runpy import unittest #: Filename of the demo script FILENAME = "Image_editor_demo.py" #: Path of the demo script DEMO_PATH = os.path.join(os.path.dirname(__file__), "..", FILENAME) class TestImageEditorDemo(unittest.TestCase): def test_image_path_exists(self): (search_path,) = runpy.run_path(DEMO_PATH)["search_path"] self.assertTrue(os.path.exists(search_path)) # Run the test(s) unittest.TextTestRunner().run( unittest.TestLoader().loadTestsFromTestCase(TestImageEditorDemo) ) ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1684768028.079807 traitsui-8.0.0/traitsui/examples/demo/Extras/windows/0000755000175100001730000000000000000000000023533 5ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/examples/demo/Extras/windows/flash.py0000644000175100001730000000321000000000000025176 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Demo showing how to use the Windows specific Flash editor. """ # Imports: from traitsui.wx.extra.windows.flash_editor import FlashEditor from traits.api import Enum, HasTraits from traitsui.api import View, HGroup, Item # The demo class: class FlashDemo(HasTraits): # The Flash file to display: flash = Enum( 'http://www.ianag.com/arcade/swf/sudoku.swf', 'http://www.ianag.com/arcade/swf/f-336.swf', 'http://www.ianag.com/arcade/swf/f-3D-Reversi-1612.swf', 'http://www.ianag.com/arcade/swf/game_234.swf', 'http://www.ianag.com/arcade/swf/flashmanwm.swf', 'http://www.ianag.com/arcade/swf/2379_gyroball.swf', 'http://www.ianag.com/arcade/swf/f-1416.swf', 'http://www.ianag.com/arcade/swf/mah_jongg.swf', 'http://www.ianag.com/arcade/swf/game_e4fe4e55fedc2f502be627ee6df716c5.swf', 'http://www.ianag.com/arcade/swf/rhumb.swf', ) # The view to display: view = View( HGroup(Item('flash', label='Pick a game to play')), '_', Item('flash', show_label=False, editor=FlashEditor()), ) # Create the demo: demo = FlashDemo() # Run the demo (if invoked from the command line): if __name__ == '__main__': demo.configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/examples/demo/Extras/windows/internet_explorer.py0000644000175100001730000000555200000000000027664 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Demo showing how to use the Windows specific Internet Explorer editor. """ # Imports: from traitsui.wx.extra.windows.ie_html_editor import IEHTMLEditor from traits.api import Str, List, Button, HasTraits from traitsui.api import View, VGroup, HGroup, Item, TextEditor, ListEditor # The web page class: class WebPage(HasTraits): # The URL to display: url = Str('http://code.enthought.com') # The page title: title = Str() # The page status: status = Str() # The browser navigation buttons: back = Button('<--') forward = Button('-->') home = Button('Home') stop = Button('Stop') refresh = Button('Refresh') search = Button('Search') # The view to display: view = View( HGroup( 'back', 'forward', 'home', 'stop', 'refresh', 'search', '_', Item('status', style='readonly'), show_labels=False, ), Item( 'url', show_label=False, editor=IEHTMLEditor( home='home', back='back', forward='forward', stop='stop', refresh='refresh', search='search', title='title', status='status', ), ), ) # The demo class: class InternetExplorerDemo(HasTraits): # A URL to display: url = Str('http://') # The list of web pages being browsed: pages = List(WebPage) # The view to display: view = View( VGroup( Item( 'url', label='Location', editor=TextEditor(auto_set=False, enter_set=True), ) ), Item( 'pages', show_label=False, style='custom', editor=ListEditor( use_notebook=True, deletable=True, dock_style='tab', export='DockWindowShell', page_name='.title', ), ), ) # Event handlers: def _url_changed(self, url): self.pages.append(WebPage(url=url.strip())) # Create the demo: demo = InternetExplorerDemo( pages=[ WebPage(url='http://code.enthought.com/projects/traits/'), WebPage(url='http://dmorrill.com'), ] ) # Run the demo (if invoked from the command line): if __name__ == '__main__': demo.configure_traits() ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1684768028.079807 traitsui-8.0.0/traitsui/examples/demo/Misc/0000755000175100001730000000000000000000000021466 5ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/examples/demo/Misc/demo_group_size.py0000644000175100001730000000574000000000000025240 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Control the height and width of a Group TraitsUI does not permit explicit specification of the height or width of a Group (or its descendants). The workaround is to put each Group whose size you wish to control into its own View class, which can be an Item (hence can be size-controlled) in the larger View. Sometimes it is necessary to repeat such surgery at several levels to get the desired layout. We separate the left and right groups by a splitter (HSplit), and also make the window itself resizable. """ from numpy import linspace, pi, sin from traits.api import Code, HasTraits, Instance, Str, Int # UItem is Unlabeled Item from traitsui.api import ( View, Item, UItem, HSplit, InstanceEditor, VGroup, HGroup, ) class InstanceUItem(UItem): """Convenience class for including an Instance in a View""" style = Str('custom') editor = Instance(InstanceEditor, ()) class CodeView(HasTraits): """Defines a sub-view whose size we wish to explicitly control.""" n = Int(123) code = Code() view = View( # This HGroup keeps 'n' from over-widening, by implicitly placing # a spring to the right of the item. HGroup(Item('n')), UItem('code'), resizable=True, ) class VerticalBar(HasTraits): """Defines a sub-view whose size we wish to explicitly control.""" a = Str('abcdefg') b = Int(123) view = View( VGroup( Item('a'), Item('b'), show_border=True, ), ) class BigView(HasTraits): """Defines the top-level view. It contains two side-by-side panels (a vertical bar and a code editor under an integer) whose relative sizes we wish to control explicitly. If these were simply defined as Groups, we could not control their sizes. But by extracting them as separate classes with their own views, the resizing becomes possible, because they are loaded as Items now. """ bar = Instance(VerticalBar, ()) code = Instance(CodeView) view = View( HSplit( # Specify pixel width of each sub-view. (Or, to specify # proportionate width, use a width < 1.0) # We specify the height here for sake of demonstration; # it could also be specified for the top-level view. InstanceUItem('bar', width=150), InstanceUItem('code', width=500, height=500), show_border=True, ), resizable=True, ) x = linspace(-2 * pi, 2 * pi, 100) pv = CodeView(code=open(__file__).read()) bv = BigView(code=pv) bv.configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/examples/demo/Misc/index.rst0000644000175100001730000000030400000000000023324 0ustar00runnerdocker00000000000000These examples demonstrate two useful features of TraitsUI - Controlling the height and width of Groups in your TraitsUI application, - Spacing widgets in your TraitsUI application using springs.././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/examples/demo/Misc/using_springs.py0000644000175100001730000000561000000000000024734 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Spacing widgets using springs *Springs* are a simple technique for adding space before, after, or between Traits UI editors (a.k.a. widgets). By default, Traits UI arranges widgets immediately adjacent to each other -- from left to right in a horizontal group, or from top to bottom in a vertical group. Sometimes this works well, but sometimes it results in widgets that are placed confusingly, or have been unattractively stretched to fill available space. When you place a *spring* in a horizontal group, Traits UI will not try to fill that space with an adjacent widget, instead allowing empty space which varies depending on the overall size of the containing group. """ from traits.api import HasTraits, Button from traitsui.api import View, VGroup, HGroup, Item, spring, Label # dummy button which will be used repeatedly to demonstrate widget spacing: button = Item('ignore', show_label=False) class SpringDemo(HasTraits): ignore = Button('Ignore') view = View( VGroup( '10', Label(label='Spring in a horizontal group moves widget right:'), '10', HGroup( button, button, show_border=True, label='Left justified (no springs)', ), HGroup( spring, button, button, show_border=True, label='Right justified with a spring ' 'before any buttons', ), HGroup( button, spring, button, show_border=True, label='Left and right justified with a ' 'spring between buttons', ), HGroup( button, button, spring, button, button, spring, button, button, show_border=True, label='Left, center and right justified ' 'with springs after the 2nd and 4th ' 'buttons', ), spring, Label( 'Spring in vertical group moves widget down ' '(does not work on Wx backend).' ), button, ), width=600, height=600, resizable=True, title='Spring Demo', buttons=['OK'], ) demo = SpringDemo() if __name__ == '__main__': demo.configure_traits() ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1684768028.083807 traitsui-8.0.0/traitsui/examples/demo/Standard_Editors/0000755000175100001730000000000000000000000024024 5ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/examples/demo/Standard_Editors/ArrayEditor_demo.py0000644000175100001730000000224200000000000027627 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """Demo of the ArrayEditor""" import numpy as np from traits.api import Array, HasPrivateTraits from traitsui.api import ArrayEditor, Item, View from traitsui.menu import NoButtons class ArrayEditorTest(HasPrivateTraits): three = Array(np.int64, (3, 3)) four = Array( np.float64, (4, 4), editor=ArrayEditor(width=-50), ) view = View( Item('three', label='3x3 Integer'), '_', Item('three', label='Integer Read-only', style='readonly'), '_', Item('four', label='4x4 Float'), '_', Item('four', label='Float Read-only', style='readonly'), buttons=NoButtons, resizable=True, ) demo = ArrayEditorTest() if __name__ == '__main__': demo.configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/examples/demo/Standard_Editors/BooleanEditor_demo.py0000644000175100001730000000372600000000000030140 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Implementation of a BooleanEditor demo plugin for Traits UI demo program. This demo shows each of the four styles of the BooleanEditor. Please refer to the `BooleanEditor API docs`_ for further information. .. _BooleanEditor API docs: https://docs.enthought.com/traitsui/api/traitsui.editors.boolean_editor.html#traitsui.editors.boolean_editor.BooleanEditor """ from traits.api import HasTraits, Bool from traitsui.api import Item, Group, View # ------------------------------------------------------------------------- # Demo Class # ------------------------------------------------------------------------- class BooleanEditorDemo(HasTraits): """This class specifies the details of the BooleanEditor demo.""" # To demonstrate any given Trait editor, an appropriate Trait is required. boolean_trait = Bool() # Items are used to define the demo display - one Item per editor style bool_group = Group( Item('boolean_trait', style='simple', label='Simple', id='simple'), Item('_'), Item('boolean_trait', style='custom', label='Custom', id='custom'), Item('_'), Item('boolean_trait', style='text', label='Text', id='text'), Item('_'), Item( 'boolean_trait', style='readonly', label='ReadOnly', id='readonly' ), ) # Demo view traits_view = View( bool_group, title='BooleanEditor', buttons=['OK'], width=300 ) # Hook for 'demo.py' demo = BooleanEditorDemo() # Run the demo (if invoked from the command line): if __name__ == '__main__': demo.configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/examples/demo/Standard_Editors/BooleanEditor_simple_demo.py0000644000175100001730000000511700000000000031505 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Boolean editor (checkbox or text) A Boolean (True/False) trait is displayed and edited as a checkbox, by default. It can also be displayed as text 'True'/'False', either editable or read-only. This example also shows how to listen for a change in a trait, and take action when its value changes. It also demonstrates how to add vertical space and a Label (plain text which is not editable.) Please refer to the `BooleanEditor API docs`_ for further information. .. _BooleanEditor API docs: https://docs.enthought.com/traitsui/api/traitsui.editors.boolean_editor.html#traitsui.editors.boolean_editor.BooleanEditor """ from traits.api import HasTraits, Bool, Int from traitsui.api import Item, Label, Group, View class BooleanEditorDemo(HasTraits): """Defines the main BooleanEditor demo class.""" # a boolean trait to view: my_boolean_trait = Bool() count_changes = Int(0) # When the trait's value changes, do something. # The listener method is named '_TraitName_changed', where # 'TraitName' is the name of the trait being monitored. def _my_boolean_trait_changed(self): self.count_changes += 1 # Demo view traits_view = View( '10', # vertical space Item('my_boolean_trait', style='simple', id='simple'), '10', # vertical space # We put this label in its own group so that it will be left justified. # Otherwise it will line up with other edit fields (indented): Group( Label( 'The same Boolean trait can also be displayed and edited as ' 'text (True/False):' ) ), '10', # vertical space Item( 'my_boolean_trait', style='readonly', label='Read-only style', id='readonly', ), Item('my_boolean_trait', style='text', label='Text style', id='text'), '10', 'count_changes', title='Boolean trait', buttons=['OK'], resizable=True, ) # Create the demo view (but do not yet display it): demo = BooleanEditorDemo() # Display and edit the demo (if invoked from the command line): if __name__ == '__main__': demo.configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/examples/demo/Standard_Editors/ButtonEditor_demo.py0000644000175100001730000000407500000000000030032 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Implementation of a ButtonEditor demo plugin for Traits UI demo program. This demo shows each of the two styles of the ButtonEditor. (As of this writing, they are identical.) Please refer to the `ButtonEditor API docs`_ for further information. .. _ButtonEditor API docs: https://docs.enthought.com/traitsui/api/traitsui.editors.button_editor.html#traitsui.editors.button_editor.ButtonEditor """ from traits.api import HasTraits, Button, observe from traitsui.api import Item, View, Group, message # ------------------------------------------------------------------------- # Demo Class # ------------------------------------------------------------------------- class ButtonEditorDemo(HasTraits): """This class specifies the details of the ButtonEditor demo.""" # To demonstrate any given Trait editor, an appropriate Trait is required. fire_event = Button('Click Me') @observe('fire_event') def _button_clicked_message(self, event): message("Button clicked!") # ButtonEditor display # (Note that Text and ReadOnly versions are not applicable) event_group = Group( Item('fire_event', style='simple', label='Simple', id="simple"), Item('_'), Item('fire_event', style='custom', label='Custom', id="custom"), Item('_'), Item(label='[text style unavailable]'), Item('_'), Item(label='[readonly style unavailable]'), ) # Demo view traits_view = View( event_group, title='ButtonEditor', buttons=['OK'], width=250 ) # Create the demo: demo = ButtonEditorDemo() # Run the demo (if invoked from the command line): if __name__ == '__main__': demo.configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/examples/demo/Standard_Editors/ButtonEditor_simple_demo.py0000644000175100001730000000317000000000000031376 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Button editor A Button trait is displayed as a button in a Traits UI view. When the button is clicked, Traits UI will execute a method of your choice (a 'listener'). In this example, the listener just increments a click counter. Please refer to the `ButtonEditor API docs`_ for further information. .. _ButtonEditor API docs: https://docs.enthought.com/traitsui/api/traitsui.editors.button_editor.html#traitsui.editors.button_editor.ButtonEditor """ from traits.api import HasTraits, Button, Int, observe from traitsui.api import Item, View class ButtonEditorDemo(HasTraits): """Defines the main ButtonEditor demo class.""" # Define a Button trait: my_button_trait = Button('Click Me') click_counter = Int(0) # When the button is clicked, do something. @observe("my_button_trait") def _increment_counter(self, event): self.click_counter += 1 # Demo view: traits_view = View( 'my_button_trait', Item('click_counter', style='readonly'), title='ButtonEditor', buttons=['OK'], resizable=True, ) # Create the demo: demo = ButtonEditorDemo() # Run the demo (if invoked from the command line): if __name__ == '__main__': demo.configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/examples/demo/Standard_Editors/CSVListEditor_demo.py0000644000175100001730000001520300000000000030041 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Demonstrate the CSVListEditor class. This editor allows the user to enter a *single* line of input text, containing comma-separated values (or another separator may be specified). Your program specifies an element Trait type of Int, Float, Str, Enum, or Range. Please refer to the `CSVListEditor API docs`_ for further information. .. _CSVListEditor API docs: https://docs.enthought.com/traitsui/api/traitsui.editors.csv_list_editor.html#traitsui.editors.csv_list_editor.CSVListEditor """ from traits.api import ( HasTraits, List, Int, Float, Enum, Range, Str, Button, Property, observe, ) from traitsui.api import ( View, Item, Label, Heading, VGroup, HGroup, UItem, spring, TextEditor, CSVListEditor, ) class CSVListEditorDemo(HasTraits): list1 = List(Int) list2 = List(Float) list3 = List(Str, maxlen=3) list4 = List(Enum('red', 'green', 'blue', 2, 3)) list5 = List(Range(low=0.0, high=10.0)) # 'low' and 'high' are used to demonstrate lists containing dynamic ranges. low = Float(0.0) high = Float(1.0) list6 = List(Range(low=-1.0, high='high')) list7 = List(Range(low='low', high='high')) pop1 = Button("Pop from first list") sort1 = Button("Sort first list") # This will be str(self.list1). list1str = Property(Str, observe='list1') traits_view = View( HGroup( # This VGroup forms the column of CSVListEditor examples. VGroup( Item( 'list1', label="List(Int)", editor=CSVListEditor(ignore_trailing_sep=False), tooltip='options: ignore_trailing_sep=False', ), Item( 'list1', label="List(Int)", style='readonly', editor=CSVListEditor(), ), Item( 'list2', label="List(Float)", editor=CSVListEditor(enter_set=True, auto_set=False), tooltip='options: enter_set=True, auto_set=False', ), Item( 'list3', label="List(Str, maxlen=3)", editor=CSVListEditor(), ), Item( 'list4', label="List(Enum('red', 'green', 'blue', 2, 3))", editor=CSVListEditor(sep=None), tooltip='options: sep=None', ), Item( 'list5', label="List(Range(low=0.0, high=10.0))", editor=CSVListEditor(), ), Item( 'list6', label="List(Range(low=-1.0, high='high'))", editor=CSVListEditor(), ), Item( 'list7', label="List(Range(low='low', high='high'))", editor=CSVListEditor(), ), springy=True, ), # This VGroup forms the right column; it will display the # Python str representation of the lists. VGroup( UItem( 'list1str', editor=TextEditor(), enabled_when='False', width=240, ), UItem( 'list1str', editor=TextEditor(), enabled_when='False', width=240, ), UItem( 'list2', editor=TextEditor(), enabled_when='False', width=240, ), UItem( 'list3', editor=TextEditor(), enabled_when='False', width=240, ), UItem( 'list4', editor=TextEditor(), enabled_when='False', width=240, ), UItem( 'list5', editor=TextEditor(), enabled_when='False', width=240, ), UItem( 'list6', editor=TextEditor(), enabled_when='False', width=240, ), UItem( 'list7', editor=TextEditor(), enabled_when='False', width=240, ), ), ), '_', HGroup('low', 'high', spring, UItem('pop1'), UItem('sort1')), Heading("Notes"), Label( "Hover over a list to see which editor options are set, " "if any." ), Label( "The editor of the first list, List(Int), uses " "ignore_trailing_sep=False, so a trailing comma is " "an error." ), Label("The second list is a read-only view of the first list."), Label( "The editor of the List(Float) example has enter_set=True " "and auto_set=False; press Enter to validate." ), Label("The List(Str) example will accept at most 3 elements."), Label( "The editor of the List(Enum(...)) example uses sep=None, " "i.e. whitespace acts as a separator." ), Label( "The last three List(Range(...)) examples take neither, one or " "both of their limits from the Low and High fields below." ), width=720, title="CSVListEditor Demonstration", ) def _list1_default(self): return [1, 4, 0, 10] def _get_list1str(self): return str(self.list1) @observe("pop1") def _pop_from_list1(self, event): if len(self.list1) > 0: x = self.list1.pop() print(x) @observe('sort1') def _sort_list1(self, event): self.list1.sort() if __name__ == "__main__": demo = CSVListEditorDemo() demo.configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/examples/demo/Standard_Editors/CheckListEditor_demo.py0000644000175100001730000000612500000000000030426 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Implementation of a CheckListEditor demo plugin for the Traits UI demo program. For each of three CheckListEditor column formations, this demo shows each of the four styles of the CheckListEditor. Please refer to the `CheckListEditor API docs`_ for further information. .. _CheckListEditor API docs: https://docs.enthought.com/traitsui/api/traitsui.editors.check_list_editor.html#traitsui.editors.check_list_editor.CheckListEditor """ from traits.api import HasTraits, List from traitsui.api import Item, Group, View, CheckListEditor # Define the demo class: class CheckListEditorDemo(HasTraits): """Define the main CheckListEditor demo class.""" # Define a trait for each of three formations: checklist_4col = List( editor=CheckListEditor(values=['one', 'two', 'three', 'four'], cols=4) ) checklist_2col = List( editor=CheckListEditor(values=['one', 'two', 'three', 'four'], cols=2) ) checklist_1col = List( editor=CheckListEditor(values=['one', 'two', 'three', 'four'], cols=1) ) # CheckListEditor display with four columns: cl_4_group = Group( Item('checklist_4col', style='simple', label='Simple'), Item('_'), Item('checklist_4col', style='custom', label='Custom'), Item('_'), Item('checklist_4col', style='text', label='Text'), Item('_'), Item('checklist_4col', style='readonly', label='ReadOnly'), label='4-column', ) # CheckListEditor display with two columns: cl_2_group = Group( Item('checklist_2col', style='simple', label='Simple'), Item('_'), Item('checklist_2col', style='custom', label='Custom'), Item('_'), Item('checklist_2col', style='text', label='Text'), Item('_'), Item('checklist_2col', style='readonly', label='ReadOnly'), label='2-column', ) # CheckListEditor display with one column: cl_1_group = Group( Item('checklist_1col', style='simple', label='Simple'), Item('_'), Item('checklist_1col', style='custom', label='Custom'), Item('_'), Item('checklist_1col', style='text', label='Text'), Item('_'), Item('checklist_1col', style='readonly', label='ReadOnly'), label='1-column', ) # The view includes one group per column formation. These will be # displayed on separate tabbed panels. traits_view = View( cl_4_group, cl_2_group, cl_1_group, title='CheckListEditor', buttons=['OK'], resizable=True, ) # Create the demo: demo = CheckListEditorDemo() # Run the demo (if invoked from the command line): if __name__ == '__main__': demo.configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/examples/demo/Standard_Editors/CheckListEditor_simple_demo.py0000644000175100001730000000516700000000000032004 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Checklist editor for a List of strings The checklist editor provides a simple way for the user to select multiple items from a list of known strings. This example demonstrates the checklist editor's two most useful styles: * 'custom' displays all the strings in columns next to checkboxes. * 'readonly' displays only the selected strings, as a Python list of strings. We do *not* demonstrate two styles which are not as useful for this editor: * 'text' is like 'readonly' except editable. It will accept a list of strings or numbers or even expressions. This is useful for quick, non-production data entry, but it ignores the editor's list of valid 'values'. * 'simple' (the default) only lets you select one item at a time, from a drop-down widget. Please refer to the `CheckListEditor API docs`_ for further information. .. _CheckListEditor API docs: https://docs.enthought.com/traitsui/api/traitsui.editors.check_list_editor.html#traitsui.editors.check_list_editor.CheckListEditor """ from traits.api import HasTraits, List from traitsui.api import UItem, Group, View, CheckListEditor, Label class CheckListEditorDemo(HasTraits): """Define the main CheckListEditor simple demo class.""" # Specify the strings to be displayed in the checklist: checklist = List( editor=CheckListEditor( values=['one', 'two', 'three', 'four', 'five', 'six'], cols=2 ) ) # CheckListEditor display with two columns: checklist_group = Group( '10', # insert vertical space (10 empty pixels) Label('The custom style lets you select items from a checklist:'), UItem('checklist', style='custom', id="custom"), '10', '_', '10', # horizontal line with vertical space above and below Label( 'The readonly style shows you which items are selected, ' 'as a Python list:' ), UItem('checklist', style='readonly', id="readonly"), ) traits_view = View( checklist_group, title='CheckListEditor', buttons=['OK'], resizable=True, ) # Create the demo: demo = CheckListEditorDemo() # Run the demo (if invoked from the command line): if __name__ == '__main__': demo.configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/examples/demo/Standard_Editors/CodeEditor_demo.py0000644000175100001730000000321700000000000027426 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Implementation of a CodeEditor demo plugin for Traits UI demo program. This demo shows each of the four styles of the CodeEditor. Please refer to the `CodeEditor API docs`_ for further information. .. _CodeEditor API docs: https://docs.enthought.com/traitsui/api/traitsui.editors.code_editor.html#traitsui.editors.code_editor.CodeEditor """ from traits.api import HasTraits, Code from traitsui.api import Item, Group, View # The main demo class: class CodeEditorDemo(HasTraits): """Defines the CodeEditor demo class.""" # Define a trait to view: code_sample = Code('import sys\n\nsys.print("hello world!")') # Display specification: code_group = Group( Item('code_sample', style='simple', label='Simple'), Item('_'), Item('code_sample', style='custom', label='Custom'), Item('_'), Item('code_sample', style='text', label='Text'), Item('_'), Item('code_sample', style='readonly', label='ReadOnly'), ) # Demo view: traits_view = View( code_group, title='CodeEditor', width=600, height=600, buttons=['OK'] ) # Create the demo: demo = CodeEditorDemo() # Run the demo (if invoked from the command line): if __name__ == "__main__": demo.configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/examples/demo/Standard_Editors/ColorEditor_demo.py0000644000175100001730000000363200000000000027633 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ **WARNING** This demo might not work as expected and some documented features might be missing. ------------------------------------------------------------------------------- Implementation of a ColorEditor demo plugin for Traits UI demo program. This demo shows each of the four styles of the ColorEditor. Please refer to the `ColorEditor API docs`_ for further information. .. _ColorEditor API docs: https://docs.enthought.com/traitsui/api/traitsui.editors.color_editor.html#traitsui.editors.color_editor.ColorEditor """ # Issues related to the demo warning: # enthought/traitsui#946 from traits.api import HasTraits from traitsui.api import Item, Group, View, Color # Demo class definition: class ColorEditorDemo(HasTraits): """Defines the main ColorEditor demo.""" # Define a Color trait to view: color_trait = Color() # Items are used to define the demo display, one item per editor style: color_group = Group( Item('color_trait', style='simple', label='Simple'), Item('_'), Item('color_trait', style='custom', label='Custom'), Item('_'), Item('color_trait', style='text', label='Text'), Item('_'), Item('color_trait', style='readonly', label='ReadOnly'), ) # Demo view traits_view = View( color_group, title='ColorEditor', buttons=['OK'], resizable=True ) # Create the demo: demo = ColorEditorDemo() # Run the demo (if invoked from the command line): if __name__ == '__main__': demo.configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/examples/demo/Standard_Editors/CompoundEditor_demo.py0000644000175100001730000000402700000000000030340 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ **WARNING** This demo might not work as expected and some documented features might be missing. ------------------------------------------------------------------------------- Implementation of a CompoundEditor demo plugin for Traits UI demo program. This demo shows each of the four styles of the CompoundEditor. Please refer to the `CompoundEditor API docs`_ for further information. .. _CompoundEditor API docs: https://docs.enthought.com/traitsui/api/traitsui.editors.compound_editor.html#traitsui.editors.compound_editor.CompoundEditor """ # Issue related to the demo warning: enthought/traitsui#945 from traits.api import Enum, HasTraits, Range, Union from traitsui.api import Item, Group, View # Define the demo class: class CompoundEditorDemo(HasTraits): """Defines the main CompoundEditor demo class.""" # Define a compound trait to view: compound_trait = Union( Range(1, 6), Enum('a', 'b', 'c', 'd', 'e', 'f'), default_value=1 ) # Display specification (one Item per editor style): comp_group = Group( Item('compound_trait', style='simple', label='Simple'), Item('_'), Item('compound_trait', style='custom', label='Custom'), Item('_'), Item('compound_trait', style='text', label='Text'), Item('_'), Item('compound_trait', style='readonly', label='ReadOnly'), ) # Demo view: traits_view = View( comp_group, title='CompoundEditor', buttons=['OK'], resizable=True ) # Create the demo: demo = CompoundEditorDemo() # Run the demo (if invoked from the command line): if __name__ == '__main__': demo.configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/examples/demo/Standard_Editors/DataFrameEditor_demo.py0000644000175100001730000000600300000000000030374 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # DataFrameEditor_demo.py -- Example of using dataframe editors # Dataset from https://www.kaggle.com/mokosan/lord-of-the-rings-character-data """ **WARNING** This demo might not work as expected and some documented features might be missing. ------------------------------------------------------------------------------- To run this demonstration successfully, you must have **pandas** installed. Please refer to the `DataFrameEditor API docs`_ for further information. .. _DataFrameEditor API docs: https://docs.enthought.com/traitsui/api/traitsui.ui_editors.data_frame_editor.html#traitsui.ui_editors.data_frame_editor.DataFrameEditor """ # Issue related to the demo warning: enthought/traitsui#944 import numpy as np from pandas import DataFrame from traits.api import HasTraits, Instance from traitsui.api import View, Item from traitsui.ui_editors.data_frame_editor import DataFrameEditor class DataFrameEditorDemo(HasTraits): df = Instance(DataFrame) traits_view = View( Item( 'df', show_label=False, editor=DataFrameEditor( formats={ 'RuntimeInMinutes': '%.4d', 'BudgetInMillions': '%d', 'BoxOfficeRevenueInMillions': '%d', 'AcademyAwardNominations': '%d', 'AcademyAwardWins': '%d', 'RottenTomatoesScore': '%.2f', } ), ), title="DataFrameEditor", resizable=True, id='traitsui.demo.Applications.data_frame_editor_demo', ) # Sample Data lotrMovieData = np.array( [ [558, 281, 2917.0, 30, 17, 94.0], [178, 93, 871.5, 13, 4, 91.0], [179, 94, 926.0, 6, 2, 96.0], [201, 94, 1120, 11, 11, 95.0], [462.0, 675, 2932.0, 7, 1, 66.33333333], [169, 200, 1021.0, 3, 1, 64.0], [161, 217, 958.4, 3, 0, 75.0], [144, 250, 956.0, 1, 0, 60.0], ] ) col_names = [ 'RuntimeInMinutes', 'BudgetInMillions', 'BoxOfficeRevenueInMillions', 'AcademyAwardNominations', 'AcademyAwardWins', 'RottenTomatoesScore', ] index_names = [ 'The Lord of the Rings Series', 'The Fellowship of the Ring', 'The Two Towers ', 'The Return of the King', 'The Hobbit Series', 'The Unexpected Journey', 'The Desolation of Smaug', 'The Battle of the Five Armies', ] # Create & run the demo df = DataFrame(data=lotrMovieData, columns=col_names, index=index_names) demo = DataFrameEditorDemo(df=df) # Run the demo (if invoked from the command line): if __name__ == '__main__': demo.configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/examples/demo/Standard_Editors/DatetimeEditor_demo.py0000644000175100001730000000377500000000000030321 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ **WARNING** This demo might not work as expected and some documented features might be missing. ------------------------------------------------------------------------------- A Traits UI editor that edits a datetime panel. Please refer to the `DatetimeEditor API docs`_ for further information. .. _DatetimeEditor API docs: https://docs.enthought.com/traitsui/api/traitsui.editors.datetime_editor.html#traitsui.editors.datetime_editor.DatetimeEditor """ # Issue related to the demo warning: enthought/traitsui#943 import datetime from traits.api import HasTraits, Datetime, Str from traitsui.api import View, Item, Group class DateEditorDemo(HasTraits): """Demo class to show Datetime editors.""" datetime = Datetime(allow_none=True) info_string = Str('The editors for Traits Datetime objects.') traits_view = View( Item( 'info_string', show_label=False, style='readonly', ), Group( Item( 'datetime', label='Simple date editor', ), Item( 'datetime', style='readonly', label='ReadOnly editor', ), label='Default settings for editors', ), resizable=True, ) def _datetime_changed(self): """Print each time the date value is changed in the editor.""" print(self.datetime) # -- Set Up The Demo ------------------------------------------------------ demo = DateEditorDemo(datetime=datetime.datetime.now()) if __name__ == "__main__": demo.configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/examples/demo/Standard_Editors/DirectoryEditor_demo.py0000644000175100001730000000375700000000000030531 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ **WARNING** This demo might not work as expected and some documented features might be missing. ------------------------------------------------------------------------------- Implementation of a DirectoryEditor demo plugin for Traits UI demo program. This demo shows each of the four styles of the DirectoryEditor. Please refer to the `DirectoryEditor API docs`_ for further information. .. _DirectoryEditor API docs: https://docs.enthought.com/traitsui/api/traitsui.editors.directory_editor.html#traitsui.editors.directory_editor.DirectoryEditor """ # Issue related to the demo warning: enthought/traitsui#889 from traits.api import HasTraits, Directory from traitsui.api import Item, Group, View # Define the demo class: class DirectoryEditorDemo(HasTraits): """Define the main DirectoryEditor demo class.""" # Define a Directory trait to view: dir_name = Directory() # Display specification (one Item per editor style): dir_group = Group( Item('dir_name', style='simple', label='Simple'), Item('_'), Item('dir_name', style='custom', label='Custom'), Item('_'), Item('dir_name', style='text', label='Text'), Item('_'), Item('dir_name', style='readonly', label='ReadOnly'), ) # Demo view: traits_view = View( dir_group, title='DirectoryEditor', width=400, height=600, buttons=['OK'], resizable=True, ) # Create the demo: demo = DirectoryEditorDemo() # Run the demo (if invoked from the command line): if __name__ == '__main__': demo.configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/examples/demo/Standard_Editors/EnumEditor_demo.py0000644000175100001730000000627300000000000027465 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Enum editor The Enum editor provides a simple way for the user to choose one item from a list of known values (normally strings or numbers). An Enum trait can take any value from a specified list of values. These values are typically strings, integers, or floats, but can also be None or hashable tuples. An Enum can be displayed / edited in one of five styles: * 'simple' displays a drop-down list of allowed values * 'custom' by default, displays one or more columns of radio buttons (only one of which is selected at a time). * 'custom' in 'list' mode (see source code below), displays a list of all the allowed values at once. * 'readonly' displays the current value as non-editable text. * 'text' displays the current value as text. You can also edit this text, but your text must be in the list of allowed values. Please refer to the `EnumEditor API docs`_ for further information. .. _EnumEditor API docs: https://docs.enthought.com/traitsui/api/traitsui.editors.enum_editor.html#traitsui.editors.enum_editor.EnumEditor """ from traits.api import HasTraits, Enum from traitsui.api import Item, Group, View, EnumEditor class EnumEditorDemo(HasTraits): """Defines the main EnumEditor demo class.""" # Define an Enum trait to view. name_list = Enum( 'A-495', 'A-498', 'R-1226', 'TS-17', 'TS-18', 'Foo', 12345, (11, 7), None, ) # Items are used to define the display, one Item per editor style: enum_group = Group( Item('name_list', style='simple', label='Simple', id="simple"), Item('_'), # The simple style also supports text entry: Item( 'name_list', style='simple', label='Simple (text entry)', editor=EnumEditor( values=name_list, completion_mode='popup', evaluate=True ), id="simple_text", ), Item('_'), # The custom style defaults to radio button mode: Item('name_list', style='custom', label='Custom radio', id="radio"), Item('_'), # The custom style can also display in list mode, with extra work: Item( 'name_list', style='custom', label='Custom list', editor=EnumEditor(values=name_list, mode='list'), id="list", ), Item('_'), Item('name_list', style='text', label='Text', id="text"), Item('_'), Item('name_list', style='readonly', label='ReadOnly', id="readonly"), ) # Demo view: traits_view = View( enum_group, title='EnumEditor', buttons=['OK'], resizable=True ) # Create the demo: demo = EnumEditorDemo() # Run the demo (if invoked from the command line): if __name__ == '__main__': demo.configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/examples/demo/Standard_Editors/FileEditor_demo.py0000644000175100001730000000370300000000000027433 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ **WARNING** This demo might not work as expected and some documented features might be missing. ------------------------------------------------------------------------------- Implementation of a FileEditor demo plugin for Traits UI demo program. This demo shows each of the four styles of the FileEditor. Please refer to the `FileEditor API docs`_ for further information. .. _FileEditor API docs: https://docs.enthought.com/traitsui/api/traitsui.editors.file_editor.html#traitsui.editors.file_editor.FileEditor """ # Issue related to the demo warning: enthought/traitsui#889 from traits.api import HasTraits, File from traitsui.api import Item, Group, View # Define the demo class: class FileEditorDemo(HasTraits): """Defines the main FileEditor demo class.""" # Define a File trait to view: file_name = File() # Display specification (one Item per editor style): file_group = Group( Item('file_name', style='simple', label='Simple', id='simple_file'), Item('_'), Item('file_name', style='custom', label='Custom'), Item('_'), Item('file_name', style='text', label='Text'), Item('_'), Item('file_name', style='readonly', label='ReadOnly'), ) # Demo view: traits_view = View( file_group, title='FileEditor', width=400, height=600, buttons=['OK'], resizable=True, ) # Create the demo: demo = FileEditorDemo() # Run the demo (if invoked from the command line): if __name__ == '__main__': demo.configure_traits() ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1684768028.083807 traitsui-8.0.0/traitsui/examples/demo/Standard_Editors/File_Dialog/0000755000175100001730000000000000000000000026162 5ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/examples/demo/Standard_Editors/File_Dialog/File_Open.py0000644000175100001730000001126400000000000030400 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ **WARNING** This demo might not work as expected and some documented features might be missing. ------------------------------------------------------------------------------- This demonstrates one of the simplest cases of using the TraitsUI file dialog to select a file for opening (i.e. reading or editing). The first question of course is why use the TraitsUI file dialog at all, when the standard OS file dialog is also available? And the answer is that you can use either, but the advantages of using the TraitsUI file dialog are: - It supports history. That is, each time the user selects a file for opening, the file is added to a persistent history list, similar to many applications *Open recent...* function, but built directly into the file dialog. The amount of history remembered can be specified by the developer, with the default being the last 10 files opened. - It is resizable. Some standard OS file dialogs are not resizable, which can be very annoying to the user trying to select a file through a tiny peephole view of the file system. In addition, if the user resizes the dialog, the new size and position will be persisted, so that the file dialog will appear in the same location the next time the user wants to open a file. - There is a very nice synergy between the file system view and the history list. Quite often users shuttle between several *favorite* locations in the file system when opening files. The TraitsUI file dialog automatically discovers these favorite locations just by the user opening files. When a user opens the file dialog, they can select a previously opened file from the history list, which then automatically causes the file system view to expand the selected file's containing folder, thus allowing them to select a different file in the same location. Since the history list is updated each time a user selects a file, It tends to automatically discover a *working set* of favorite directories just through simple use, without the user having to explicitly designate them as such. - It's customizable. The TraitsUI file dialog accepts extension objects which can be used to display additional file information or even modify the selection behavior of the dialog. Several extensions are provided with TraitsUI (and are demonstrated in some of the other examples), and you are free to write your own by implementing a very simple interface. - The history and user settings are customizable per application. Just by setting a unique id in the file dialog request, you can specify that the history and window size and position information are specific to your application. If you have file dialog extensions added, the user can reorder, resize and reconfigure the overall file dialog layout, including your extensions, and have their custom settings restored each time they use the file dialog. If you do not specify a unique id, then the history and user settings default to the system-wide settings for the file dialog. It's your choice. - It's easy to use. That's what this particular example is all about. So take a look at the source code for this example to see how easy it is... """ # Issue related to the demo warning: enthought/traitsui#953 from traits.api import HasTraits, File, Button from traitsui.api import View, HGroup, Item from traitsui.file_dialog import open_file # -- FileDialogDemo Class ------------------------------------------------- class FileDialogDemo(HasTraits): # The name of the selected file: file_name = File() # The button used to display the file dialog: open = Button('Open...') # -- Traits View Definitions ---------------------------------------------- traits_view = View( HGroup( Item('open', show_label=False), '_', Item('file_name', style='readonly', springy=True), ), width=0.5, ) # -- Traits Event Handlers ------------------------------------------------ def _open_changed(self): """Handles the user clicking the 'Open...' button.""" file_name = open_file() if file_name != '': self.file_name = file_name # Create the demo: demo = FileDialogDemo() # Run the demo (if invoked from the command line): if __name__ == '__main__': demo.configure_traits() ././@PaxHeader0000000000000000000000000000020500000000000011452 xustar0000000000000000111 path=traitsui-8.0.0/traitsui/examples/demo/Standard_Editors/File_Dialog/File_Open_with_Custom_Extension.py 22 mtime=1684768013.0 traitsui-8.0.0/traitsui/examples/demo/Standard_Editors/File_Dialog/File_Open_with_Custom_Extension.p0000644000175100001730000000664600000000000034640 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ **WARNING** This demo might not work as expected and some documented features might be missing. ------------------------------------------------------------------------------- This demonstrates using the TraitsUI file dialog with a custom written file dialog extension, in this case an extension called **LineCountInfo**, which displays the number of text lines in the currently selected file. For more information about why you would want to use the TraitsUI file dialog over the standard OS file dialog, select the **File Open** demo. """ # Issue related to the demo warning: enthought/traitsui#953 from os.path import getsize from traits.api import HasTraits, File, Button, Property, cached_property from traitsui.api import View, VGroup, HGroup, Item from traitsui.file_dialog import open_file, MFileDialogModel # -- LineCountInfo Class -------------------------------------------------- class LineCountInfo(MFileDialogModel): """Defines a file dialog extension that displays the number of text lines in the currently selected file. """ # The number of text lines in the currently selected file: lines = Property(observe='file_name') # -- Traits View Definitions ---------------------------------------------- traits_view = View( VGroup( Item('lines', style='readonly'), label='Line Count Info', show_border=True, ) ) # -- Property Implementations --------------------------------------------- @cached_property def _get_lines(self): try: if getsize(self.file_name) > 10000000: return 'File too big...' with open(self.file_name, 'r', encoding='utf8') as fh: data = fh.read() except OSError: return '' if (data.find('\x00') >= 0) or (data.find('\xFF') >= 0): return 'File contains binary data...' return '{:n} lines'.format(len(data.splitlines())) # -- FileDialogDemo Class ------------------------------------------------- # Demo specific file dialig id: demo_id = 'traitsui.demo.standard_editors.file_dialog.line_count_info' class FileDialogDemo(HasTraits): # The name of the selected file: file_name = File() # The button used to display the file dialog: open = Button('Open...') # -- Traits View Definitions ---------------------------------------------- traits_view = View( HGroup( Item('open', show_label=False), '_', Item('file_name', style='readonly', springy=True), ), width=0.5, ) # -- Traits Event Handlers ------------------------------------------------ def _open_changed(self): """Handles the user clicking the 'Open...' button.""" file_name = open_file(extensions=LineCountInfo(), id=demo_id) if file_name != '': self.file_name = file_name # Create the demo: demo = FileDialogDemo() # Run the demo (if invoked from the command line): if __name__ == '__main__': demo.configure_traits() ././@PaxHeader0000000000000000000000000000020700000000000011454 xustar0000000000000000113 path=traitsui-8.0.0/traitsui/examples/demo/Standard_Editors/File_Dialog/File_Open_with_FileInfo_Extension.py 22 mtime=1684768013.0 traitsui-8.0.0/traitsui/examples/demo/Standard_Editors/File_Dialog/File_Open_with_FileInfo_Extension0000644000175100001730000000462700000000000034620 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ **WARNING** This demo might not work as expected and some documented features might be missing. ------------------------------------------------------------------------------- This demonstrates using the TraitsUI file dialog with a file dialog extension, in this case, the **FileInfo** extension, which displays information about the currently selected file, such as: - File size. - Date and time last accessed. - Date and time last modified. - Date and time last created. For more information about why you would want to use the TraitsUI file dialog over the standard OS file dialog, select the **File Open** demo. For a demonstration of writing a custom file dialog extension, select the **File Open with Custom Extension** demo. """ # Issue related to the demo warning: enthought/traitsui#953 from traits.api import HasTraits, File, Button from traitsui.api import View, HGroup, Item from traitsui.file_dialog import open_file, FileInfo # -- FileDialogDemo Class ------------------------------------------------- # Demo specific file dialig id: demo_id = 'traitsui.demo.standard_editors.file_dialog.file_info' class FileDialogDemo(HasTraits): # The name of the selected file: file_name = File() # The button used to display the file dialog: open = Button('Open...') # -- Traits View Definitions ---------------------------------------------- traits_view = View( HGroup( Item('open', show_label=False), '_', Item('file_name', style='readonly', springy=True), ), width=0.5, ) # -- Traits Event Handlers ------------------------------------------------ def _open_changed(self): """Handles the user clicking the 'Open...' button.""" file_name = open_file(extensions=FileInfo(), id=demo_id) if file_name != '': self.file_name = file_name # Create the demo: demo = FileDialogDemo() # Run the demo (if invoked from the command line): if __name__ == '__main__': demo.configure_traits() ././@PaxHeader0000000000000000000000000000021000000000000011446 xustar0000000000000000114 path=traitsui-8.0.0/traitsui/examples/demo/Standard_Editors/File_Dialog/File_Open_with_ImageInfo_Extension.py 22 mtime=1684768013.0 traitsui-8.0.0/traitsui/examples/demo/Standard_Editors/File_Dialog/File_Open_with_ImageInfo_Extensio0000644000175100001730000000550200000000000034576 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ **WARNING** This demo might not work as expected and some documented features might be missing. ------------------------------------------------------------------------------- This demonstrates using the TraitsUI file dialog with a file dialog extension, in this case, the **ImageInfo** extension, which displays (if possible) the contents, width and height information for the currently selected image file so that the user can quickly verify they are opening the correct file before leaving the file dialog. For more information about why you would want to use the TraitsUI file dialog over the standard OS file dialog, select the **File Open** demo. For a demonstration of writing a custom file dialog extension, select the **File Open with Custom Extension** demo. This example also shows setting a file name filter which only allows common image file formats (i.e. ``*.png``, ``*.gif``, ``*.jpg``, ``*.jpeg``) files to be viewed and selected. """ # Issue related to the demo warning: enthought/traitsui#953 from traits.api import HasTraits, File, Button from traitsui.api import View, HGroup, Item from traitsui.file_dialog import open_file, ImageInfo # -- FileDialogDemo Class ------------------------------------------------- # Demo specific file dialig id: demo_id = 'traitsui.demo.standard_editors.file_dialog.image_info' # The image filters description: filters = [ 'PNG file (*.png)|*.png', 'GIF file (*.gif)|*.gif', 'JPG file (*.jpg)|*.jpg', 'JPEG file (*.jpeg)|*.jpeg', ] class FileDialogDemo(HasTraits): # The name of the selected file: file_name = File() # The button used to display the file dialog: open = Button('Open...') # -- Traits View Definitions ---------------------------------------------- traits_view = View( HGroup( Item('open', show_label=False), '_', Item('file_name', style='readonly', springy=True), ), width=0.5, ) # -- Traits Event Handlers ------------------------------------------------ def _open_changed(self): """Handles the user clicking the 'Open...' button.""" file_name = open_file( extensions=ImageInfo(), filter=filters, id=demo_id ) if file_name != '': self.file_name = file_name # Create the demo: demo = FileDialogDemo() # Run the demo (if invoked from the command line): if __name__ == '__main__': demo.configure_traits() ././@PaxHeader0000000000000000000000000000021000000000000011446 xustar0000000000000000114 path=traitsui-8.0.0/traitsui/examples/demo/Standard_Editors/File_Dialog/File_Open_with_Multiple_Extensions.py 22 mtime=1684768013.0 traitsui-8.0.0/traitsui/examples/demo/Standard_Editors/File_Dialog/File_Open_with_Multiple_Extension0000644000175100001730000000551700000000000034717 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ **WARNING** This demo might not work as expected and some documented features might be missing. ------------------------------------------------------------------------------- This demonstrates using the TraitsUI file dialog with multiple file dialog extensions, in this case, the **FileInfo**, **TextInfo** and **ImageInfo** extensions. For more information about why you would want to use the TraitsUI file dialog over the standard OS file dialog, select the **File Open** demo. For a demonstration of writing a custom file dialog extension, select the **File Open with Custom Extension** demo. Suggestion: Try resizing the dialog and dragging the various file dialog extensions around to create a better arrangement than the rather cramped default vertical arrangement. Close the dialog, then re-open it to see that your new arrangement has been correctly restored. Try a different file dialog demo to verify that the customizations are not affected by any of the other demos because this demo specifies a custom id when invoking the file dialog. """ # Issue related to the demo warning: enthought/traitsui#953 from traits.api import HasTraits, File, Button from traitsui.api import View, HGroup, Item from traitsui.file_dialog import open_file, FileInfo, TextInfo, ImageInfo # -- FileDialogDemo Class ------------------------------------------------- # Demo specific file dialig id: demo_id = 'traitsui.demo.standard_editors.file_dialog.multiple_info' # The list of file dialog extensions to use: extensions = [FileInfo(), TextInfo(), ImageInfo()] class FileDialogDemo(HasTraits): # The name of the selected file: file_name = File() # The button used to display the file dialog: open = Button('Open...') # -- Traits View Definitions ---------------------------------------------- traits_view = View( HGroup( Item('open', show_label=False), '_', Item('file_name', style='readonly', springy=True), ), width=0.5, ) # -- Traits Event Handlers ------------------------------------------------ def _open_changed(self): """Handles the user clicking the 'Open...' button.""" file_name = open_file(extensions=extensions, id=demo_id) if file_name != '': self.file_name = file_name # Create the demo: demo = FileDialogDemo() # Run the demo (if invoked from the command line): if __name__ == '__main__': demo.configure_traits() ././@PaxHeader0000000000000000000000000000020700000000000011454 xustar0000000000000000113 path=traitsui-8.0.0/traitsui/examples/demo/Standard_Editors/File_Dialog/File_Open_with_TextInfo_Extension.py 22 mtime=1684768013.0 traitsui-8.0.0/traitsui/examples/demo/Standard_Editors/File_Dialog/File_Open_with_TextInfo_Extension0000644000175100001730000000515000000000000034655 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ **WARNING** This demo might not work as expected and some documented features might be missing. ------------------------------------------------------------------------------- This demonstrates using the TraitsUI file dialog with a file dialog extension, in this case, the **TextInfo** extension, which displays (if possible) the contents of the currently selected file in a read-only text editor so the user can quickly verify they are opening the correct file before leaving the file dialog. For more information about why you would want to use the TraitsUI file dialog over the standard OS file dialog, select the **File Open** demo. For a demonstration of writing a custom file dialog extension, select the **File Open with Custom Extension** demo. This example also shows setting a file name filter which only allows Python source (i.e. ``*.py``) files to be viewed and selected. """ # Issue related to the demo warning: enthought/traitsui#953 from traits.api import HasTraits, File, Button from traitsui.api import View, HGroup, Item from traitsui.file_dialog import open_file, TextInfo # -- FileDialogDemo Class ------------------------------------------------- # Demo specific file dialog id: demo_id = 'traitsui.demo.standard_editors.file_dialog.text_info' class FileDialogDemo(HasTraits): # The name of the selected file: file_name = File() # The button used to display the file dialog: open = Button('Open...') # -- Traits View Definitions ---------------------------------------------- traits_view = View( HGroup( Item('open', show_label=False), '_', Item('file_name', style='readonly', springy=True), ), width=0.5, ) # -- Traits Event Handlers ------------------------------------------------ def _open_changed(self): """Handles the user clicking the 'Open...' button.""" file_name = open_file( extensions=TextInfo(), filter='Python file (*.py)|*.py', id=demo_id ) if file_name != '': self.file_name = file_name # Create the demo: demo = FileDialogDemo() # Run the demo (if invoked from the command line): if __name__ == '__main__': demo.configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/examples/demo/Standard_Editors/FontEditor_demo.py0000644000175100001730000000407300000000000027463 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Font editor A Font editor in a Traits UI allows the user to select a font from the operating system. Typically, you then pass the Font trait to another UI editor, which uses it to display text. You can also read the Font trait as a string, or access its individual attributes (note that these attributes are specific to the UI toolkit -- QT or WX.) The default 'simple' Font editor style is usually the most useful and powerful style - it pops up a font selection dialog which is specific to the OS and toolkit. This example also displays some other less common style choices. Please refer to the `FontEditor API docs`_ for further information. .. _FontEditor API docs: https://docs.enthought.com/traitsui/api/traitsui.editors.font_editor.html#traitsui.editors.font_editor.FontEditor """ from traits.api import HasTraits from traitsui.api import Item, Group, View, Font class FontEditorDemo(HasTraits): """Defines the main FontEditor demo class.""" # Define a Font trait to view: my_font_trait = Font() # Display specification (one Item per editor style): font_group = Group( Item('my_font_trait', style='simple', label='Simple'), Item('_'), Item('my_font_trait', style='custom', label='Custom'), Item('_'), Item('my_font_trait', style='text', label='Text'), Item('_'), Item('my_font_trait', style='readonly', label='ReadOnly'), ) # Demo view: traits_view = View( font_group, title='FontEditor', buttons=['OK'], resizable=True ) # Create the demo: demo = FontEditorDemo() # Run the demo (if invoked from the command line): if __name__ == '__main__': demo.configure_traits() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1684768013.0 traitsui-8.0.0/traitsui/examples/demo/Standard_Editors/HTMLEditor_demo.py0000644000175100001730000000462500000000000027324 0ustar00runnerdocker00000000000000# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ HTML editor The HTML editor displays simple formatted HTML text in a Traits UI view. If the text is held in an HTML trait, then the HTMLEditor is the default. If the text is held in a Str trait, then you may specify the HTMLEditor explicitly if you wish to display it as HTML. The supported subset of HTML tags and features depends on the UI toolkit (WX or QT). This editor does not support style sheets. It does not support WYSIWYG editing of the text, though the unformatted text can be edited in a plain text editor. The HTML editor can optionally be configured to do simple formatting of lists and paragraphs without HTML tags, by setting the editor's 'format_text' parameter True. Please refer to the `HTMLEditor API docs`_ for further information. .. _HTMLEditor API docs: https://docs.enthought.com/traitsui/api/traitsui.editors.html_editor.html#traitsui.editors.html_editor.HTMLEditor """ from traits.api import HasTraits, HTML from traitsui.api import UItem, View, HTMLEditor # Sample text to display as HTML: header, plus module docstring, plus # some lists. The docstring and lists will be auto-formatted # (format_text=True). sample_text = ( """